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