(request: Request, token: VerifiedToken)
| 166 | * and 403s carry the client fingerprint. |
| 167 | */ |
| 168 | const finishAuthorized = (request: Request, token: VerifiedToken): Effect.Effect<AuthOutcome> => |
| 169 | Effect.gen(function* () { |
| 170 | // OLD `mcpApp` annotated with parseBody = (POST && isAuthorized) BEFORE |
| 171 | // org-authz, so a verified-but-no/revoked-org POST still captured |
| 172 | // mcp.rpc.method/id. The body is read via `request.clone().text()` |
| 173 | // (annotateMcpRequest -> readJsonRpcEnvelope), so it never consumes the |
| 174 | // original stream a downstream dispatch reads — safe on every path, |
| 175 | // including the Forbidden short-circuit. Keep parseBody keyed on POST, |
| 176 | // not on the org outcome, to preserve that telemetry. |
| 177 | const parseBody = request.method === "POST"; |
| 178 | |
| 179 | // URL is the source of truth for the active org when pinned — the org's |
| 180 | // slug (`/acme/mcp`, what the install card prints) or a legacy org id |
| 181 | // (`/org_xxx/mcp`), carried in the header by `prepareMcpOrgScope`; the |
| 182 | // bare `/mcp` falls back to the token's `org_id`. Either way |
| 183 | // `orgAuth.authorize` resolves the selector and re-checks live WorkOS |
| 184 | // membership below, so the URL is a selector, not a trust boundary. |
| 185 | const organizationSelector = mcpOrganizationFromRequest(request) ?? token.organizationId; |
| 186 | if (!organizationSelector) { |
| 187 | yield* annotateMcpRequest(request, { token, parseBody }); |
| 188 | return forbidden(NO_ORGANIZATION_MESSAGE, -32001); |
| 189 | } |
| 190 | |
| 191 | // Capture success-vs-failure explicitly instead of collapsing both into |
| 192 | // `null`, then classify the failure (see the classification table on |
| 193 | // ORGANIZATION_AUTHORIZE_UNAVAILABLE above): a definitive WorkOS 4xx |
| 194 | // denial fails CLOSED as Forbidden, anything else is a transient error |
| 195 | // that must become a retryable 503 with the session left intact. |
| 196 | const authorizeResult = yield* orgAuth |
| 197 | .authorize(token.accountId, organizationSelector) |
| 198 | .pipe( |
| 199 | Effect.result, |
| 200 | Effect.withSpan("mcp.auth.authorize_organization", { |
| 201 | attributes: { |
| 202 | "mcp.auth.organization_selector": organizationSelector, |
| 203 | }, |
| 204 | }), |
| 205 | ); |
| 206 | |
| 207 | yield* annotateMcpRequest(request, { token, parseBody }); |
| 208 | |
| 209 | if (Result.isFailure(authorizeResult)) { |
| 210 | if (isDefinitiveWorkOSDenial(authorizeResult.failure)) { |
| 211 | // WorkOS ANSWERED and said no (revoked key, forbidden, deleted |
| 212 | // org). Deterministic denial — same as a successful lookup with no |
| 213 | // membership, so the Forbidden/condemn path applies. |
| 214 | yield* Effect.annotateCurrentSpan({ |
| 215 | "mcp.auth.outcome": "denied", |
| 216 | "mcp.auth.organization_authorize_error": String(authorizeResult.failure).slice( |
| 217 | 0, |
| 218 | 500, |
| 219 | ), |
| 220 | }); |
| 221 | return forbidden(NO_ORGANIZATION_MESSAGE, -32001); |
| 222 | } |
| 223 | yield* Effect.annotateCurrentSpan({ |
| 224 | "mcp.auth.outcome": "system_error", |
| 225 | "mcp.auth.system_error.reason": "organization_authorize", |
no test coverage detected