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