@zackbart/connecta 0.34.1 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +363 -0
- package/README.md +62 -106
- package/SECURITY.md +3 -2
- package/bin/connecta.mjs +41 -16
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.d.ts +2 -1
- package/dist/access-tokens.js +66 -30
- package/dist/activity.d.ts +1 -1
- package/dist/activity.js +2 -0
- package/dist/agent-config.d.ts +23 -0
- package/dist/agent-config.js +59 -0
- package/dist/agent-description.js +16 -0
- package/dist/agent-identity.js +4 -4
- package/dist/agent-observation.js +39 -0
- package/dist/auth/clerk-sdk/client.d.ts +2 -0
- package/dist/auth/clerk-sdk/generated.js +554 -563
- package/dist/auth/clerk-transport.js +70 -3
- package/dist/auth/clerk.d.ts +13 -0
- package/dist/auth/clerk.js +653 -246
- package/dist/auth/cloudflare-access.js +46 -42
- package/dist/auth/downstream-client-metadata.js +2 -2
- package/dist/auth/downstream-oauth.js +2051 -1160
- package/dist/auth/downstream-revocation.js +88 -88
- package/dist/auth/native-call.js +20 -0
- package/dist/auth/native.js +184 -0
- package/dist/auth/oauth-start-reset.js +11 -5
- package/dist/auth/oauth-token-response.js +121 -102
- package/dist/auth/oauth-v2-migration.js +97 -78
- package/dist/auth/renewable-grant.js +10 -0
- package/dist/auth/static-oauth.js +663 -271
- package/dist/auth/vercel-agent.js +266 -0
- package/dist/auth-admission.js +204 -0
- package/dist/auth-elicitation.js +350 -241
- package/dist/authorization-handoff.js +164 -90
- package/dist/call-admission-context.js +3 -10
- package/dist/call-admission.js +2 -0
- package/dist/call-classification.js +9 -0
- package/dist/call-recovery.js +32 -13
- package/dist/canonical-json.js +8 -0
- package/dist/catalog-cache.js +369 -213
- package/dist/catalog-service.js +659 -530
- package/dist/catalog.js +5 -1
- package/dist/concurrency.js +0 -25
- package/dist/config-defaults.d.ts +1 -0
- package/dist/config-defaults.js +1 -0
- package/dist/config-schema.d.ts +3 -1
- package/dist/config-schema.js +13 -3
- package/dist/config.d.ts +40 -6
- package/dist/config.js +40 -26
- package/dist/connector-caller.d.ts +4 -0
- package/dist/connector-caller.js +17 -4
- package/dist/connectors/api-connector.d.ts +12 -7
- package/dist/connectors/api-connector.js +142 -56
- package/dist/connectors/guarded-fetch.d.ts +51 -17
- package/dist/connectors/guarded-fetch.js +215 -121
- package/dist/connectors/negotiation-cache.js +20 -10
- package/dist/connectors/remote-mcp.d.ts +10 -30
- package/dist/connectors/remote-mcp.js +955 -563
- package/dist/d1.js +10 -243
- package/dist/derived-context.js +5 -0
- package/dist/describe-config.d.ts +3 -1
- package/dist/describe-config.js +2 -0
- package/dist/downstream-input.js +347 -260
- package/dist/errors.d.ts +87 -6
- package/dist/errors.js +114 -4
- package/dist/execute.js +87 -50
- package/dist/executor-admission.js +3 -0
- package/dist/executors/quickjs.js +163 -112
- package/dist/guarded-transport.js +50 -0
- package/dist/guest-runtime.js +1 -1
- package/dist/http/client.js +439 -0
- package/dist/http/errors.js +558 -0
- package/dist/http/fields.js +49 -0
- package/dist/http/hatch-paging.js +318 -0
- package/dist/http/hatch.js +1279 -0
- package/dist/http/json-schema.js +267 -0
- package/dist/http/node-transport.js +241 -0
- package/dist/http/oauth.js +162 -0
- package/dist/http/paging.js +228 -0
- package/dist/http/provider.js +221 -0
- package/dist/http/safety.js +82 -0
- package/dist/http/services.js +79 -0
- package/dist/http/tools.js +383 -0
- package/dist/http/vendor-index.js +138 -0
- package/dist/http/vendor.js +448 -0
- package/dist/http/write-request.js +66 -0
- package/dist/http-policy.d.ts +11 -0
- package/dist/http-policy.js +45 -0
- package/dist/inbound-credential.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -0
- package/dist/invocation-auth.js +30 -19
- package/dist/invocation.js +218 -85
- package/dist/mcp-result.js +0 -4
- package/dist/membership.d.ts +4 -0
- package/dist/meta-output.js +1 -1
- package/dist/meta-tools.js +531 -406
- package/dist/native-recovery.js +52 -0
- package/dist/native-tools.js +382 -0
- package/dist/node.d.ts +4 -3
- package/dist/node.js +45 -8
- package/dist/oauth-handoff.js +177 -104
- package/dist/oauth-partition.js +6 -20
- package/dist/oauth-sealing.js +6 -7
- package/dist/operation-vocabulary.js +101 -0
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +17 -4
- package/dist/operator-ui/agents.d.ts +5 -4
- package/dist/operator-ui/agents.js +17 -7
- package/dist/operator-ui/config-summary.js +5 -4
- package/dist/operator-ui/connection-map.js +7 -5
- package/dist/operator-ui/contract.d.ts +7 -3
- package/dist/operator-ui/endpoints.js +4 -13
- package/dist/operator-ui/fix-prompts.d.ts +1 -1
- package/dist/operator-ui/fix-prompts.js +0 -8
- package/dist/operator-ui/generated.js +3 -3
- package/dist/operator-ui/model.d.ts +2 -3
- package/dist/operator-ui/overview.js +5 -2
- package/dist/operator-ui/setup-commands.js +0 -4
- package/dist/operator-ui/view.d.ts +2 -25
- package/dist/operator-ui/view.js +2 -76
- package/dist/program-writes.js +47 -26
- package/dist/provider-call-facts.d.ts +115 -0
- package/dist/provider-call-facts.js +182 -0
- package/dist/provider-call.d.ts +112 -0
- package/dist/provider-call.js +271 -0
- package/dist/provider.d.ts +65 -31
- package/dist/provider.js +116 -39
- package/dist/providers/_shared/google/hatch.js +91 -0
- package/dist/providers/_shared/google/kit.js +537 -0
- package/dist/providers/_shared/google/result-size.js +15 -2
- package/dist/providers/_shared/google/service-account.d.ts +2 -1
- package/dist/providers/_shared/google/service-account.js +8 -4
- package/dist/providers/_shared/google/workspace.d.ts +6 -210
- package/dist/providers/_shared/google/workspace.js +6 -425
- package/dist/providers/_shared/rest/compact-index.js +86 -0
- package/dist/providers/_shared/rest/dispatch.js +3 -23
- package/dist/providers/_shared/rest/kit.js +546 -0
- package/dist/providers/_shared/rest/operation-index.js +230 -14
- package/dist/providers/_shared/rest/response-media.js +11 -0
- package/dist/providers/_shared/rest/tools.js +101 -78
- package/dist/providers/_shared/rest/value-safety.js +353 -195
- package/dist/providers/_shared/sensitive-fields.js +27 -0
- package/dist/providers/breeze/api.js +584 -0
- package/dist/providers/breeze/index.d.ts +1 -3
- package/dist/providers/breeze/index.js +67 -1420
- package/dist/providers/breeze/openapi.generated.js +60 -0
- package/dist/providers/breeze/skill.generated.js +2 -6
- package/dist/providers/breeze/tools.js +696 -0
- package/dist/providers/ccb/api.js +496 -0
- package/dist/providers/ccb/index.d.ts +15 -37
- package/dist/providers/ccb/index.js +77 -1702
- package/dist/providers/ccb/lib/constants.d.ts +34 -0
- package/dist/providers/ccb/lib/constants.js +74 -0
- package/dist/providers/ccb/lib/guide.js +9 -0
- package/dist/providers/ccb/lib/verdicts.js +387 -0
- package/dist/providers/ccb/openapi.generated.js +397 -0
- package/dist/providers/ccb/safety.js +295 -0
- package/dist/providers/ccb/skill.generated.js +2 -6
- package/dist/providers/ccb/tools.js +853 -0
- package/dist/providers/cloudflare/api.js +1066 -0
- package/dist/providers/cloudflare/index.d.ts +5 -22
- package/dist/providers/cloudflare/index.js +79 -197
- package/dist/providers/cloudflare/lib/config.d.ts +16 -0
- package/dist/providers/cloudflare/lib/config.js +2 -0
- package/dist/providers/cloudflare/lib/discovery.js +29 -0
- package/dist/providers/cloudflare/{graphql.js → lib/graphql.js} +1 -1
- package/dist/providers/cloudflare/lib/hatch.js +103 -0
- package/dist/providers/cloudflare/lib/path-bounds.js +28 -0
- package/dist/providers/cloudflare/lib/response-media.js +75 -0
- package/dist/providers/cloudflare/openapi.generated.js +4 -3
- package/dist/providers/cloudflare/{value-safety.js → safety.js} +487 -0
- package/dist/providers/cloudflare/skill.generated.js +2 -10
- package/dist/providers/cloudflare/tools.js +360 -0
- package/dist/providers/docs/api.js +51 -0
- package/dist/providers/docs/index.d.ts +1 -12
- package/dist/providers/docs/index.js +53 -1165
- package/dist/providers/docs/lib/constants.d.ts +18 -0
- package/dist/providers/docs/lib/constants.js +18 -0
- package/dist/providers/docs/lib/discovery.js +26 -0
- package/dist/providers/docs/lib/requests.js +48 -0
- package/dist/providers/docs/lib/schemas.js +168 -0
- package/dist/providers/docs/openapi.generated.js +16 -0
- package/dist/providers/docs/safety.js +54 -0
- package/dist/providers/docs/skill.generated.js +2 -6
- package/dist/providers/docs/tools.js +802 -0
- package/dist/providers/drive/api.js +177 -0
- package/dist/providers/drive/index.d.ts +1 -12
- package/dist/providers/drive/index.js +38 -1448
- package/dist/providers/drive/lib/constants.d.ts +6 -0
- package/dist/providers/drive/lib/constants.js +6 -0
- package/dist/providers/drive/lib/contracts.js +532 -0
- package/dist/providers/drive/lib/hatch.js +197 -0
- package/dist/providers/drive/openapi.generated.js +77 -0
- package/dist/providers/drive/safety.js +90 -0
- package/dist/providers/drive/skill.generated.js +2 -6
- package/dist/providers/drive/tools.js +1290 -0
- package/dist/providers/forms/api.js +71 -0
- package/dist/providers/forms/index.d.ts +1 -12
- package/dist/providers/forms/index.js +49 -1145
- package/dist/providers/forms/lib/constants.d.ts +14 -0
- package/dist/providers/forms/lib/constants.js +19 -0
- package/dist/providers/forms/lib/hatch.js +72 -0
- package/dist/providers/forms/openapi.generated.js +23 -0
- package/dist/providers/forms/safety.js +34 -0
- package/dist/providers/forms/skill.generated.js +2 -8
- package/dist/providers/forms/tools.js +1024 -0
- package/dist/providers/github/api.js +150 -0
- package/dist/providers/github/index.d.ts +4 -5
- package/dist/providers/github/index.js +30 -87
- package/dist/providers/github/{auth.d.ts → lib/auth.d.ts} +5 -3
- package/dist/providers/github/{auth.js → lib/auth.js} +40 -43
- package/dist/providers/github/lib/discovery.js +132 -0
- package/dist/providers/github/lib/operations.js +58 -0
- package/dist/providers/github/{scope.d.ts → lib/scope.d.ts} +2 -0
- package/dist/providers/github/{scope.js → lib/scope.js} +25 -1
- package/dist/providers/github/openapi.generated.js +1216 -0
- package/dist/providers/github/safety.js +216 -0
- package/dist/providers/github/skill.generated.js +2 -5
- package/dist/providers/github/tools.js +506 -0
- package/dist/providers/gmail/api.js +114 -0
- package/dist/providers/gmail/index.d.ts +1 -12
- package/dist/providers/gmail/index.js +73 -1621
- package/dist/providers/gmail/lib/constants.d.ts +13 -0
- package/dist/providers/gmail/lib/constants.js +16 -0
- package/dist/providers/gmail/lib/index.js +173 -0
- package/dist/providers/gmail/lib/schemas.js +176 -0
- package/dist/providers/gmail/lib/threading.js +120 -0
- package/dist/providers/gmail/openapi.generated.js +92 -0
- package/dist/providers/gmail/safety.js +21 -0
- package/dist/providers/gmail/skill.generated.js +2 -6
- package/dist/providers/gmail/tools.js +1435 -0
- package/dist/providers/overflow/api.js +1008 -0
- package/dist/providers/overflow/index.d.ts +1 -17
- package/dist/providers/overflow/index.js +60 -1488
- package/dist/providers/overflow/skill.generated.js +2 -5
- package/dist/providers/overflow/tools.js +873 -0
- package/dist/providers/planning-center/api.js +467 -0
- package/dist/providers/planning-center/index.d.ts +4 -35
- package/dist/providers/planning-center/index.js +69 -2445
- package/dist/providers/planning-center/lib/constants.d.ts +58 -0
- package/dist/providers/planning-center/lib/constants.js +83 -0
- package/dist/providers/planning-center/lib/discovery.js +40 -0
- package/dist/providers/planning-center/lib/guide.js +39 -0
- package/dist/providers/planning-center/lib/json-api.js +83 -0
- package/dist/providers/planning-center/lib/verdicts.js +1758 -0
- package/dist/providers/planning-center/openapi.generated.js +1764 -0
- package/dist/providers/planning-center/safety.js +188 -0
- package/dist/providers/planning-center/skill.generated.js +2 -27
- package/dist/providers/planning-center/tools.js +1010 -0
- package/dist/providers/sheets/api.js +133 -0
- package/dist/providers/sheets/index.d.ts +4 -16
- package/dist/providers/sheets/index.js +61 -1172
- package/dist/providers/sheets/lib/constants.d.ts +14 -0
- package/dist/providers/sheets/lib/constants.js +14 -0
- package/dist/providers/sheets/lib/hatch.js +163 -0
- package/dist/providers/sheets/lib/requests.js +105 -0
- package/dist/providers/sheets/lib/schemas.js +138 -0
- package/dist/providers/sheets/openapi.generated.js +30 -0
- package/dist/providers/sheets/safety.js +27 -0
- package/dist/providers/sheets/skill.generated.js +2 -7
- package/dist/providers/sheets/tools.js +734 -0
- package/dist/providers/slides/api.js +147 -0
- package/dist/providers/slides/index.d.ts +2 -10
- package/dist/providers/slides/index.js +48 -2555
- package/dist/providers/slides/lib/constants.d.ts +11 -0
- package/dist/providers/slides/lib/constants.js +11 -0
- package/dist/providers/slides/lib/requests.js +60 -0
- package/dist/providers/slides/lib/schemas.js +565 -0
- package/dist/providers/slides/openapi.generated.js +18 -0
- package/dist/providers/slides/skill.generated.js +2 -6
- package/dist/providers/slides/tools.js +1968 -0
- package/dist/providers/stripe/api.js +401 -0
- package/dist/providers/stripe/index.d.ts +5 -27
- package/dist/providers/stripe/index.js +74 -210
- package/dist/providers/stripe/lib/config.d.ts +4 -0
- package/dist/providers/stripe/lib/config.js +2 -0
- package/dist/providers/stripe/openapi.generated.js +4 -3
- package/dist/providers/stripe/{value-safety.js → safety.js} +5 -5
- package/dist/providers/stripe/skill.generated.js +2 -11
- package/dist/providers/stripe/tools.js +77 -0
- package/dist/providers/tithely/api.js +531 -0
- package/dist/providers/tithely/index.d.ts +17 -0
- package/dist/providers/tithely/index.js +86 -965
- package/dist/providers/tithely/skill.generated.js +2 -5
- package/dist/providers/tithely/tools.js +426 -0
- package/dist/providers/vercel/api.js +551 -0
- package/dist/providers/vercel/index.d.ts +9 -25
- package/dist/providers/vercel/index.js +66 -260
- package/dist/providers/vercel/lib/config.d.ts +2 -0
- package/dist/providers/vercel/lib/config.js +2 -0
- package/dist/providers/vercel/lib/index.js +117 -0
- package/dist/providers/vercel/lib/logs.js +100 -0
- package/dist/providers/vercel/lib/pin.js +270 -0
- package/dist/providers/vercel/openapi.generated.js +10 -5
- package/dist/providers/vercel/{value-safety.js → safety.js} +247 -38
- package/dist/providers/vercel/skill.generated.js +2 -7
- package/dist/providers/vercel/tools.js +642 -0
- package/dist/redaction-identity.js +47 -0
- package/dist/redaction-marker.js +125 -0
- package/dist/registry.d.ts +21 -15
- package/dist/registry.js +409 -376
- package/dist/routes/access-tokens.js +1 -1
- package/dist/routes/actor-labels.js +1 -1
- package/dist/routes/connect.js +1 -1
- package/dist/routes/dispatch.js +103 -0
- package/dist/routes/mcp.js +170 -79
- package/dist/routes/oauth.js +3 -1
- package/dist/routes/operator.js +2 -2
- package/dist/routes/shared.d.ts +5 -4
- package/dist/routes/shared.js +244 -177
- package/dist/routes/skills.js +10 -8
- package/dist/routes/ui-config.js +27 -3
- package/dist/routes/ui-data.js +21 -20
- package/dist/routes/ui.js +1 -1
- package/dist/runtime/call-admission.js +0 -26
- package/dist/runtime/call-entry.js +30 -0
- package/dist/runtime/call.js +556 -0
- package/dist/runtime/connector-scope.js +7 -0
- package/dist/runtime/credential-probe.js +34 -0
- package/dist/runtime/executor.js +40 -0
- package/dist/runtime/native-storage.js +63 -0
- package/dist/runtime/registry.js +27 -0
- package/dist/runtime/request-scope.js +72 -0
- package/dist/runtime/run.js +15 -7
- package/dist/runtime/shared-read.js +34 -5
- package/dist/runtime/storage.js +34 -21
- package/dist/sent-secrets.d.ts +194 -37
- package/dist/sent-secrets.js +1743 -340
- package/dist/server.js +5 -56
- package/dist/skills.js +322 -222
- package/dist/sqlite.js +15 -14
- package/dist/storage/d1-copy.js +255 -0
- package/dist/storage/d1-driver.js +15 -0
- package/dist/storage/keys.js +28 -0
- package/dist/storage/sql-driver.js +88 -0
- package/dist/storage/sql-native.js +544 -0
- package/dist/storage/sql-schema.d.ts +6 -0
- package/dist/storage/sql-schema.js +15 -0
- package/dist/storage/sql.d.ts +14 -20
- package/dist/storage/sql.js +29 -535
- package/dist/timeout.js +0 -26
- package/dist/tool-safety.js +20 -10
- package/dist/types.d.ts +7 -109
- package/dist/upward.js +3 -0
- package/dist/usage-guide.js +3 -1
- package/dist/validate.d.ts +4 -2
- package/dist/validate.js +86 -9
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +312 -292
- package/dist/workspace-subject.js +5 -1
- package/dist/write-outcome.js +454 -34
- package/documentation/README.md +27 -7
- package/documentation/architecture.md +2547 -226
- package/documentation/auth.md +821 -98
- package/documentation/code-mode.md +49 -16
- package/documentation/deploying.md +164 -20
- package/documentation/hosted-mcp-migration.md +460 -0
- package/documentation/integrating.md +129 -144
- package/documentation/meta-tools.md +409 -32
- package/documentation/operating.md +27 -3
- package/documentation/operator-tests.md +3 -0
- package/documentation/operator-ui.md +68 -45
- package/documentation/provider-auth-migration.md +16 -63
- package/documentation/provider-migration-0.29.md +10 -5
- package/documentation/provider-migration-0.35.md +975 -0
- package/documentation/providers.md +965 -0
- package/examples/worker/AGENTS.md +23 -9
- package/examples/worker/README.md +254 -135
- package/examples/worker/alchemy.run.ts +116 -0
- package/examples/worker/gitignore +10 -0
- package/examples/worker/src/connecta.config.ts +23 -21
- package/examples/worker/wrangler.jsonc +14 -5
- package/guides/connecting-agents.md +157 -0
- package/guides/how-it-works.md +156 -0
- package/guides/safety-and-trust.md +153 -0
- package/package.json +27 -46
- package/templates/node/.env.example +3 -0
- package/templates/node/README.md +22 -2
- package/templates/node/docker-compose.yml +1 -0
- package/templates/node/package.json +3 -2
- package/templates/node/src/connecta.config.ts +8 -0
- package/dist/catalog-drift.d.ts +0 -138
- package/dist/catalog-drift.js +0 -578
- package/dist/providers/_shared/request-token.js +0 -83
- package/dist/providers/_shared/rest/operation-index.d.ts +0 -133
- package/dist/providers/_shared/rest/tools.d.ts +0 -141
- package/dist/providers/_shared/rest/value-safety.d.ts +0 -201
- package/dist/providers/basecamp/index.d.ts +0 -45
- package/dist/providers/basecamp/index.js +0 -997
- package/dist/providers/basecamp/skill.generated.js +0 -9
- package/dist/providers/cloudflare/rest.d.ts +0 -37
- package/dist/providers/cloudflare/rest.js +0 -1097
- package/dist/providers/github/hosted.js +0 -218
- package/dist/providers/github/rest.js +0 -321
- package/dist/providers/infisical/index.d.ts +0 -7
- package/dist/providers/infisical/index.js +0 -713
- package/dist/providers/infisical/skill.generated.js +0 -8
- package/dist/providers/intercom/index.d.ts +0 -24
- package/dist/providers/intercom/index.js +0 -121
- package/dist/providers/intercom/skill.generated.js +0 -8
- package/dist/providers/linear/index.d.ts +0 -55
- package/dist/providers/linear/index.js +0 -537
- package/dist/providers/linear/skill.generated.js +0 -8
- package/dist/providers/mixpanel/index.d.ts +0 -59
- package/dist/providers/mixpanel/index.js +0 -472
- package/dist/providers/mixpanel/operations.js +0 -20
- package/dist/providers/mixpanel/rest.js +0 -603
- package/dist/providers/mixpanel/skill.generated.js +0 -10
- package/dist/providers/mixpanel/value-safety.js +0 -16
- package/dist/providers/notion/index.d.ts +0 -67
- package/dist/providers/notion/index.js +0 -368
- package/dist/providers/notion/openapi.generated.js +0 -75
- package/dist/providers/notion/rest.d.ts +0 -24
- package/dist/providers/notion/rest.js +0 -1226
- package/dist/providers/notion/skill.generated.js +0 -11
- package/dist/providers/notion/value-safety.js +0 -56
- package/dist/providers/revenuecat/index.d.ts +0 -51
- package/dist/providers/revenuecat/index.js +0 -697
- package/dist/providers/revenuecat/skill.generated.js +0 -11
- package/dist/providers/stripe/rest.d.ts +0 -14
- package/dist/providers/stripe/rest.js +0 -402
- package/dist/providers/vercel/rest.d.ts +0 -20
- package/dist/providers/vercel/rest.js +0 -1020
- package/dist/providers/wispr-flow/index.d.ts +0 -14
- package/dist/providers/wispr-flow/index.js +0 -117
- package/dist/providers/wispr-flow/skill.generated.js +0 -8
- /package/dist/providers/github/{key.js → lib/key.js} +0 -0
package/dist/auth/clerk.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
1
2
|
import { authorizationCredential, authorizationRequest, isMachineCredential } from "../inbound-credential.js";
|
|
2
|
-
import {
|
|
3
|
+
import { fromSignal, runEdge } from "../runtime/run.js";
|
|
4
|
+
import { authAll, authCall, authDetached, authFetch, authFlow, authInvoke, authMethod, authProgram, authValue, registerAuth, } from "./native.js";
|
|
5
|
+
// Clerk as the OAuth 2.1 authorization server; connecta is the resource server.
|
|
6
|
+
// Single tenant, no tenant-tag requirement, optional organizationId,
|
|
7
|
+
// allowedDomains, and gate() with ~60s identity caching.
|
|
3
8
|
import { decodeJwt } from "@clerk/backend/jwt";
|
|
4
|
-
import { byteReadResponse } from "../byte-read-response.js";
|
|
5
9
|
import { failureRecord, logFailure } from "../operator-record.js";
|
|
6
10
|
import { assertNoRetiredToolkitOptions } from "../retired-toolkits.js";
|
|
11
|
+
import { createByteReadingClerkClient } from "./clerk-transport.js";
|
|
7
12
|
function logDenial(reason, failure) {
|
|
8
13
|
logFailure(console, "Clerk request denied", failureRecord({ reason }, failure));
|
|
9
14
|
}
|
|
@@ -53,6 +58,19 @@ function fapiUrl(publishableKey) {
|
|
|
53
58
|
}
|
|
54
59
|
const GATE_ALLOWED_TTL_MS = 60 * 1000;
|
|
55
60
|
const GATE_FORBIDDEN_TTL_MS = 30 * 1000;
|
|
61
|
+
const BACKEND_LOOKUP_TIMEOUT_MS = 10_000;
|
|
62
|
+
// Per organization-mode clerkAuth/isolate, shared by membership and the extra
|
|
63
|
+
// opaque verification. Clerk's quota is per instance (100/10s development,
|
|
64
|
+
// 1,000/10s production), not per deployment. These cap bursts, not request
|
|
65
|
+
// rates: ingress rate limiting must protect the shared quota. No queue or
|
|
66
|
+
// request-owned object is retained; only counters and a backoff timestamp.
|
|
67
|
+
const ORGANIZATION_MAX_BACKEND_LOOKUPS = 16;
|
|
68
|
+
const ORGANIZATION_MAX_BACKEND_LOOKUPS_PER_IDENTITY = 2;
|
|
69
|
+
const BACKEND_RETRY_DEFAULT_MS = 10_000;
|
|
70
|
+
const BACKEND_RETRY_MAX_MS = 60_000;
|
|
71
|
+
// A user-filtered membership list holds at most one row, and a token
|
|
72
|
+
// verification describes one token.
|
|
73
|
+
const MAX_BACKEND_RESPONSE_BYTES = 65_536;
|
|
56
74
|
const ACTIVITY_LABEL_TTL_MS = 5 * 60 * 1000;
|
|
57
75
|
const ACTIVITY_LABEL_MISS_TTL_MS = 30 * 1000;
|
|
58
76
|
const ACTIVITY_LABEL_LOOKUP_TIMEOUT_MS = 1_250;
|
|
@@ -170,6 +188,168 @@ function normalizeOAuthClientIds(value) {
|
|
|
170
188
|
}
|
|
171
189
|
return new Set(value);
|
|
172
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* Checked at construction because the ID becomes a Backend API path segment,
|
|
193
|
+
* and a typo would otherwise refuse every person at request time.
|
|
194
|
+
*/
|
|
195
|
+
function normalizeOrganizationId(value, allowedOAuthClientIds) {
|
|
196
|
+
if (value === undefined)
|
|
197
|
+
return undefined;
|
|
198
|
+
if (typeof value !== "string" || !/^org_[A-Za-z0-9_]{27}$/.test(value)) {
|
|
199
|
+
throw new Error("clerkAuth: `organizationId` is not a Clerk organization ID. Copy it from " +
|
|
200
|
+
"the Clerk dashboard: `org_` followed by 27 letters and digits.");
|
|
201
|
+
}
|
|
202
|
+
// On a shared instance every deployment's OAuth clients and tokens come
|
|
203
|
+
// from one issuer, so the resource-bound audience is what keeps a token for
|
|
204
|
+
// one deployment out of another. An unbound-token fallback removes it.
|
|
205
|
+
if (allowedOAuthClientIds.size > 0) {
|
|
206
|
+
throw new Error("clerkAuth: `organizationId` requires resource-bound OAuth tokens. Remove " +
|
|
207
|
+
"`allowedOAuthClientIds` and enable Clerk's resource audiences.");
|
|
208
|
+
}
|
|
209
|
+
return value;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Is this user a member of the organization, according to Clerk now? The
|
|
213
|
+
* given signal and a ten-second deadline bound the call. Any response that is
|
|
214
|
+
* not a clear answer throws, so the caller refuses.
|
|
215
|
+
*/
|
|
216
|
+
function organizationMember(secretKey, organizationId, userId, signal) {
|
|
217
|
+
return runEdge(authProgram(organizationMemberNative, [secretKey, organizationId, userId, signal]));
|
|
218
|
+
}
|
|
219
|
+
const organizationMemberNative = /* @__PURE__ */ registerAuth(organizationMember, function (secretKey, organizationId, userId, signal) {
|
|
220
|
+
return authFlow(function* () {
|
|
221
|
+
const url = new URL(`https://api.clerk.com/v1/organizations/${encodeURIComponent(organizationId)}/memberships`);
|
|
222
|
+
url.searchParams.set("user_id", userId);
|
|
223
|
+
const list = (yield authCall(backendJson, [
|
|
224
|
+
url,
|
|
225
|
+
{ headers: { Authorization: `Bearer ${secretKey}`, "Clerk-API-Version": "2026-05-12" } },
|
|
226
|
+
signal,
|
|
227
|
+
]));
|
|
228
|
+
if (!isRecord(list) || !Array.isArray(list.data)) {
|
|
229
|
+
throw new Error("Organization membership response invalid");
|
|
230
|
+
}
|
|
231
|
+
// The filter narrows the list; membership is still read exactly from each
|
|
232
|
+
// row, so an ignored or widened filter cannot admit someone else's row.
|
|
233
|
+
return list.data.some((membership) => isRecord(membership) &&
|
|
234
|
+
membership.object === "organization_membership" &&
|
|
235
|
+
isRecord(membership.organization) &&
|
|
236
|
+
membership.organization.id === organizationId &&
|
|
237
|
+
isRecord(membership.public_user_data) &&
|
|
238
|
+
membership.public_user_data.user_id === userId);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
/**
|
|
242
|
+
* One Backend API request, bounded by the signal, a ten-second deadline, and
|
|
243
|
+
* `MAX_BACKEND_RESPONSE_BYTES`. A redirect, a non-2xx status, or an unreadable
|
|
244
|
+
* body throws. Every exit aborts the request and cancels its body, so no
|
|
245
|
+
* upstream outlives the caller's decision (INV-7).
|
|
246
|
+
*/
|
|
247
|
+
function backendJson(url, init, signal) {
|
|
248
|
+
return runEdge(authProgram(backendJsonNative, [url, init, signal]));
|
|
249
|
+
}
|
|
250
|
+
const backendJsonNative = /* @__PURE__ */ registerAuth(backendJson, function (url, init, signal) {
|
|
251
|
+
return authFlow(function* () {
|
|
252
|
+
const settled = new AbortController();
|
|
253
|
+
const request = AbortSignal.any([signal, settled.signal, AbortSignal.timeout(BACKEND_LOOKUP_TIMEOUT_MS)]);
|
|
254
|
+
let response;
|
|
255
|
+
try {
|
|
256
|
+
// Workers reject `redirect: "error"`; `manual` returns a redirect
|
|
257
|
+
// unfollowed, and a redirect is not an answer. Workers and Node return
|
|
258
|
+
// its 3xx; an `opaqueredirect` has status 0, which `ok` refuses.
|
|
259
|
+
response = (yield authFetch(url, { ...init, redirect: "manual", signal: request }));
|
|
260
|
+
if (response.status === 429)
|
|
261
|
+
throw new BackendRateLimited(backendRetryMs(response.headers.get("Retry-After")));
|
|
262
|
+
if (response.status >= 300 && response.status < 400)
|
|
263
|
+
throw new Error("Clerk Backend API request redirected");
|
|
264
|
+
if (!response.ok)
|
|
265
|
+
throw new Error("Clerk Backend API request failed");
|
|
266
|
+
return JSON.parse((yield authCall(boundedText, [response, request])));
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
settled.abort();
|
|
270
|
+
void response?.body?.cancel().catch(() => { });
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
/** Only locally parsed, bounded timing facts leave the response's request. */
|
|
275
|
+
class BackendRateLimited extends Error {
|
|
276
|
+
retryMs;
|
|
277
|
+
constructor(retryMs) {
|
|
278
|
+
super("Clerk Backend API rate limited");
|
|
279
|
+
this.retryMs = retryMs;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
class OrganizationLookupBusy extends Error {
|
|
283
|
+
retrySeconds;
|
|
284
|
+
constructor(retrySeconds) {
|
|
285
|
+
super("Clerk organization lookup busy");
|
|
286
|
+
this.retrySeconds = retrySeconds;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/** Clerk sends seconds; accept an HTTP date too. Never trust an unbounded delay. */
|
|
290
|
+
function backendRetryMs(value) {
|
|
291
|
+
let delay = BACKEND_RETRY_DEFAULT_MS;
|
|
292
|
+
if (value !== null) {
|
|
293
|
+
const parsed = /^\d+$/.test(value)
|
|
294
|
+
? Number(value) * 1_000
|
|
295
|
+
: /^[A-Z][a-z]{2}, \d{2} [A-Z][a-z]{2} \d{4} \d{2}:\d{2}:\d{2} GMT$/.test(value)
|
|
296
|
+
? Date.parse(value) - Date.now()
|
|
297
|
+
: NaN;
|
|
298
|
+
if (!Number.isNaN(parsed))
|
|
299
|
+
delay = parsed;
|
|
300
|
+
}
|
|
301
|
+
return Math.max(1_000, Math.min(BACKEND_RETRY_MAX_MS, delay));
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* The body as text, read as bytes (see `byteReadResponse`) and refused past
|
|
305
|
+
* `MAX_BACKEND_RESPONSE_BYTES`. An abort cancels a read the upstream has
|
|
306
|
+
* stopped feeding, so the caller's deadline holds while a body stays open.
|
|
307
|
+
*/
|
|
308
|
+
function boundedText(response, signal) {
|
|
309
|
+
return runEdge(authProgram(boundedTextNative, [response, signal]));
|
|
310
|
+
}
|
|
311
|
+
const boundedTextNative = /* @__PURE__ */ registerAuth(boundedText, function (response, signal) {
|
|
312
|
+
return authFlow(function* () {
|
|
313
|
+
const reader = response.body?.getReader();
|
|
314
|
+
if (!reader)
|
|
315
|
+
return "";
|
|
316
|
+
void reader.closed.catch(() => { });
|
|
317
|
+
const cancel = () => void reader.cancel().catch(() => { });
|
|
318
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
319
|
+
const decoder = new TextDecoder();
|
|
320
|
+
let size = 0;
|
|
321
|
+
let text = "";
|
|
322
|
+
try {
|
|
323
|
+
while (true) {
|
|
324
|
+
signal.throwIfAborted();
|
|
325
|
+
const { done, value } = (yield authInvoke(reader, "read", []));
|
|
326
|
+
signal.throwIfAborted();
|
|
327
|
+
if (done)
|
|
328
|
+
return text + decoder.decode();
|
|
329
|
+
size += value.byteLength;
|
|
330
|
+
if (size > MAX_BACKEND_RESPONSE_BYTES)
|
|
331
|
+
throw new Error("Clerk Backend API response too large");
|
|
332
|
+
text += decoder.decode(value, { stream: true });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
finally {
|
|
336
|
+
signal.removeEventListener("abort", cancel);
|
|
337
|
+
reader.releaseLock();
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
/**
|
|
342
|
+
* Valid only while the clock reads from the lookup's start to start + ttl.
|
|
343
|
+
* A step back before the start expires the entry, and a step forward can
|
|
344
|
+
* only expire it early.
|
|
345
|
+
*/
|
|
346
|
+
function withinTtl(started, ttl) {
|
|
347
|
+
const age = Date.now() - started;
|
|
348
|
+
return age >= 0 && age < ttl;
|
|
349
|
+
}
|
|
350
|
+
function isRecord(value) {
|
|
351
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
352
|
+
}
|
|
173
353
|
/** Only verified claims reach this comparison; resource URLs match exactly. */
|
|
174
354
|
function oauthBindingRejection(claims, resource, clientId, allowedClientIds) {
|
|
175
355
|
const bindings = ["aud", "resource"].filter((key) => Object.prototype.hasOwnProperty.call(claims, key));
|
|
@@ -190,38 +370,42 @@ function oauthBindingRejection(claims, resource, clientId, allowedClientIds) {
|
|
|
190
370
|
* endpoint directly after SDK authentication, without a shared response cache
|
|
191
371
|
* or SDK patch. Its subject/client and active verdict must still agree.
|
|
192
372
|
*/
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
method: "POST",
|
|
196
|
-
headers: {
|
|
197
|
-
Authorization: `Bearer ${secretKey}`,
|
|
198
|
-
"Content-Type": "application/json",
|
|
199
|
-
"Clerk-API-Version": "2026-05-12",
|
|
200
|
-
},
|
|
201
|
-
body: JSON.stringify({ access_token: token }),
|
|
202
|
-
redirect: "error",
|
|
203
|
-
signal: AbortSignal.any([signal, AbortSignal.timeout(10_000)]),
|
|
204
|
-
});
|
|
205
|
-
if (!response.ok)
|
|
206
|
-
throw new Error("Opaque OAuth verification failed");
|
|
207
|
-
const claims = await byteReadResponse(response).json();
|
|
208
|
-
if (!claims ||
|
|
209
|
-
typeof claims !== "object" ||
|
|
210
|
-
Array.isArray(claims) ||
|
|
211
|
-
!("object" in claims) ||
|
|
212
|
-
claims.object !== "clerk_idp_oauth_access_token" ||
|
|
213
|
-
!("subject" in claims) ||
|
|
214
|
-
claims.subject !== userId ||
|
|
215
|
-
!("client_id" in claims) ||
|
|
216
|
-
claims.client_id !== clientId ||
|
|
217
|
-
!("revoked" in claims) ||
|
|
218
|
-
claims.revoked !== false ||
|
|
219
|
-
!("expired" in claims) ||
|
|
220
|
-
claims.expired !== false) {
|
|
221
|
-
throw new Error("Opaque OAuth verification response invalid");
|
|
222
|
-
}
|
|
223
|
-
return claims;
|
|
373
|
+
function opaqueOAuthClaims(token, secretKey, userId, clientId, signal) {
|
|
374
|
+
return runEdge(authProgram(opaqueOAuthClaimsNative, [token, secretKey, userId, clientId, signal]));
|
|
224
375
|
}
|
|
376
|
+
const opaqueOAuthClaimsNative = /* @__PURE__ */ registerAuth(opaqueOAuthClaims, function (token, secretKey, userId, clientId, signal) {
|
|
377
|
+
return authFlow(function* () {
|
|
378
|
+
const claims = (yield authCall(backendJson, [
|
|
379
|
+
"https://api.clerk.com/v1/oauth_applications/access_tokens/verify",
|
|
380
|
+
{
|
|
381
|
+
method: "POST",
|
|
382
|
+
headers: {
|
|
383
|
+
Authorization: `Bearer ${secretKey}`,
|
|
384
|
+
"Content-Type": "application/json",
|
|
385
|
+
"Clerk-API-Version": "2026-05-12",
|
|
386
|
+
},
|
|
387
|
+
body: JSON.stringify({ access_token: token }),
|
|
388
|
+
},
|
|
389
|
+
signal,
|
|
390
|
+
]));
|
|
391
|
+
if (!claims ||
|
|
392
|
+
typeof claims !== "object" ||
|
|
393
|
+
Array.isArray(claims) ||
|
|
394
|
+
!("object" in claims) ||
|
|
395
|
+
claims.object !== "clerk_idp_oauth_access_token" ||
|
|
396
|
+
!("subject" in claims) ||
|
|
397
|
+
claims.subject !== userId ||
|
|
398
|
+
!("client_id" in claims) ||
|
|
399
|
+
claims.client_id !== clientId ||
|
|
400
|
+
!("revoked" in claims) ||
|
|
401
|
+
claims.revoked !== false ||
|
|
402
|
+
!("expired" in claims) ||
|
|
403
|
+
claims.expired !== false) {
|
|
404
|
+
throw new Error("Opaque OAuth verification response invalid");
|
|
405
|
+
}
|
|
406
|
+
return claims;
|
|
407
|
+
});
|
|
408
|
+
});
|
|
225
409
|
/**
|
|
226
410
|
* The domain of an email address, lowercased for comparison, or null when the
|
|
227
411
|
* address does not have exactly one well-formed domain to read.
|
|
@@ -244,7 +428,8 @@ function emailDomain(email) {
|
|
|
244
428
|
/**
|
|
245
429
|
* Clerk inbound auth.
|
|
246
430
|
*
|
|
247
|
-
* `allowedDomains
|
|
431
|
+
* `organizationId`, `allowedDomains`, and `gate` decide who is admitted; each
|
|
432
|
+
* configured check must pass.
|
|
248
433
|
*/
|
|
249
434
|
/** `"/<pool>"` for a pool endpoint path, null for `/mcp` and anything else. */
|
|
250
435
|
function mcpPoolSuffix(pathname) {
|
|
@@ -256,12 +441,9 @@ export function clerkAuth(opts) {
|
|
|
256
441
|
// Before the Clerk client, so a malformed key fails as a connecta
|
|
257
442
|
// configuration error rather than however the SDK happens to treat it.
|
|
258
443
|
const frontendApiUrl = fapiUrl(opts.publishableKey);
|
|
259
|
-
const clerk = createByteReadingClerkClient({
|
|
260
|
-
secretKey: opts.secretKey,
|
|
261
|
-
publishableKey: opts.publishableKey,
|
|
262
|
-
});
|
|
263
444
|
const allowedDomains = normalizeAllowedDomains(opts.allowedDomains);
|
|
264
445
|
const allowedOAuthClientIds = normalizeOAuthClientIds(opts.allowedOAuthClientIds);
|
|
446
|
+
const organizationId = normalizeOrganizationId(opts.organizationId, allowedOAuthClientIds);
|
|
265
447
|
const requestedScopes = opts.scopes ?? ["openid", "profile", "email"];
|
|
266
448
|
if (!Array.isArray(requestedScopes) ||
|
|
267
449
|
requestedScopes.some((scope) => typeof scope !== "string" || !/^[\x21\x23-\x5b\x5d-\x7e]+$/.test(scope))) {
|
|
@@ -269,8 +451,64 @@ export function clerkAuth(opts) {
|
|
|
269
451
|
}
|
|
270
452
|
const scopes = [...requestedScopes];
|
|
271
453
|
const gateCache = new Map();
|
|
454
|
+
const membershipCache = new Map();
|
|
455
|
+
// Membership lookups started, in order, and the latest start among the
|
|
456
|
+
// entries the cache has evicted.
|
|
457
|
+
let membershipLookupsStarted = 0;
|
|
458
|
+
let evictedMembershipSequence = 0;
|
|
459
|
+
let activeOrganizationLookups = 0;
|
|
460
|
+
const organizationLookupsByIdentity = new Map();
|
|
461
|
+
// Wall-clock end of the current 429 backoff. Like the membership TTLs it
|
|
462
|
+
// assumes the clock does not step back: a rollback lengthens the remaining
|
|
463
|
+
// window by the size of the step.
|
|
464
|
+
let organizationRetryUntil = 0;
|
|
465
|
+
const organizationLookup = authMethod((userId, signal, lookup) => {
|
|
466
|
+
return authFlow(function* () {
|
|
467
|
+
const now = Date.now();
|
|
468
|
+
if (now < organizationRetryUntil) {
|
|
469
|
+
throw new OrganizationLookupBusy(Math.ceil((organizationRetryUntil - now) / 1000));
|
|
470
|
+
}
|
|
471
|
+
const activeForIdentity = organizationLookupsByIdentity.get(userId) ?? 0;
|
|
472
|
+
if (activeOrganizationLookups >= ORGANIZATION_MAX_BACKEND_LOOKUPS ||
|
|
473
|
+
activeForIdentity >= ORGANIZATION_MAX_BACKEND_LOOKUPS_PER_IDENTITY) {
|
|
474
|
+
throw new OrganizationLookupBusy(1);
|
|
475
|
+
}
|
|
476
|
+
signal.throwIfAborted();
|
|
477
|
+
activeOrganizationLookups++;
|
|
478
|
+
organizationLookupsByIdentity.set(userId, activeForIdentity + 1);
|
|
479
|
+
let released = false;
|
|
480
|
+
const release = () => {
|
|
481
|
+
if (released)
|
|
482
|
+
return;
|
|
483
|
+
released = true;
|
|
484
|
+
activeOrganizationLookups--;
|
|
485
|
+
const remaining = organizationLookupsByIdentity.get(userId) - 1;
|
|
486
|
+
if (remaining === 0)
|
|
487
|
+
organizationLookupsByIdentity.delete(userId);
|
|
488
|
+
else
|
|
489
|
+
organizationLookupsByIdentity.set(userId, remaining);
|
|
490
|
+
};
|
|
491
|
+
// workerd may discard the awaited continuation when its caller leaves.
|
|
492
|
+
// Release in that request's own abort handler too; finally is idempotent.
|
|
493
|
+
// The same caller signal cancels backendJson's request-owned upstream.
|
|
494
|
+
signal.addEventListener("abort", release, { once: true });
|
|
495
|
+
try {
|
|
496
|
+
return (yield authCall(lookup, []));
|
|
497
|
+
}
|
|
498
|
+
catch (error) {
|
|
499
|
+
if (error instanceof BackendRateLimited) {
|
|
500
|
+
organizationRetryUntil = Math.max(organizationRetryUntil, Date.now() + error.retryMs);
|
|
501
|
+
throw new OrganizationLookupBusy(Math.ceil((organizationRetryUntil - Date.now()) / 1000));
|
|
502
|
+
}
|
|
503
|
+
throw error;
|
|
504
|
+
}
|
|
505
|
+
finally {
|
|
506
|
+
signal.removeEventListener("abort", release);
|
|
507
|
+
release();
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
});
|
|
272
511
|
const activityLabelCache = new Map();
|
|
273
|
-
const pendingActivityLabels = new Map();
|
|
274
512
|
const inFlightActivityLabelIds = new Set();
|
|
275
513
|
let activeActivityLabelLookups = 0;
|
|
276
514
|
const resolveBase = (baseUrl) => opts.publicUrl ?? baseUrl;
|
|
@@ -280,58 +518,57 @@ export function clerkAuth(opts) {
|
|
|
280
518
|
exp: Date.now() + (label ? ACTIVITY_LABEL_TTL_MS : ACTIVITY_LABEL_MISS_TTL_MS),
|
|
281
519
|
});
|
|
282
520
|
};
|
|
283
|
-
const resolveActivityLabel =
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
521
|
+
const resolveActivityLabel = authMethod((userId, owner) => {
|
|
522
|
+
return authFlow(function* () {
|
|
523
|
+
if (owner?.aborted)
|
|
524
|
+
return undefined;
|
|
525
|
+
const cached = readIdentityCache(activityLabelCache, userId);
|
|
526
|
+
if (cached)
|
|
527
|
+
return cached.label;
|
|
528
|
+
// Each reader owns its lookup and transport. Keep raw concurrency bounded
|
|
529
|
+
// even if a custom transport fails to settle after cancellation.
|
|
530
|
+
if (inFlightActivityLabelIds.has(userId) || activeActivityLabelLookups >= ACTIVITY_LABEL_MAX_IN_FLIGHT) {
|
|
531
|
+
return undefined;
|
|
532
|
+
}
|
|
533
|
+
const ended = new AbortController();
|
|
534
|
+
const signal = owner ? AbortSignal.any([owner, ended.signal]) : ended.signal;
|
|
535
|
+
activeActivityLabelLookups++;
|
|
536
|
+
inFlightActivityLabelIds.add(userId);
|
|
537
|
+
const lookup = authFlow(function* () {
|
|
538
|
+
try {
|
|
539
|
+
signal.throwIfAborted();
|
|
540
|
+
const user = (yield authInvoke(createByteReadingClerkClient(opts, signal).users, "getUser", [
|
|
541
|
+
userId,
|
|
542
|
+
]));
|
|
543
|
+
if (signal.aborted)
|
|
544
|
+
return undefined;
|
|
545
|
+
const label = activityLabelForUser(user);
|
|
546
|
+
cacheActivityLabel(userId, label);
|
|
547
|
+
return label;
|
|
548
|
+
}
|
|
549
|
+
catch {
|
|
550
|
+
if (!signal.aborted)
|
|
551
|
+
cacheActivityLabel(userId, undefined);
|
|
552
|
+
return undefined;
|
|
553
|
+
}
|
|
554
|
+
finally {
|
|
555
|
+
activeActivityLabelLookups--;
|
|
556
|
+
inFlightActivityLabelIds.delete(userId);
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
const timer = setTimeout(() => ended.abort(), ACTIVITY_LABEL_LOOKUP_TIMEOUT_MS);
|
|
560
|
+
try {
|
|
561
|
+
return (yield Effect.raceFirst(authDetached(lookup), fromSignal(signal)));
|
|
562
|
+
}
|
|
563
|
+
catch {
|
|
564
|
+
return undefined;
|
|
565
|
+
}
|
|
566
|
+
finally {
|
|
321
567
|
clearTimeout(timer);
|
|
322
|
-
|
|
323
|
-
}
|
|
324
|
-
const timer = setTimeout(() => {
|
|
325
|
-
cacheActivityLabel(userId, undefined);
|
|
326
|
-
finish(undefined);
|
|
327
|
-
}, ACTIVITY_LABEL_LOOKUP_TIMEOUT_MS);
|
|
328
|
-
void upstream.then(finish);
|
|
329
|
-
}).finally(() => {
|
|
330
|
-
pendingActivityLabels.delete(userId);
|
|
568
|
+
ended.abort();
|
|
569
|
+
}
|
|
331
570
|
});
|
|
332
|
-
|
|
333
|
-
return lookup;
|
|
334
|
-
};
|
|
571
|
+
});
|
|
335
572
|
const challenge = (request, baseUrl) => {
|
|
336
573
|
const tokenPresent = authorizationCredential(request).kind !== "absent";
|
|
337
574
|
const error = tokenPresent ? `error="invalid_token", ` : "";
|
|
@@ -356,67 +593,181 @@ export function clerkAuth(opts) {
|
|
|
356
593
|
status: 403,
|
|
357
594
|
headers: { "Content-Type": "application/json" },
|
|
358
595
|
});
|
|
596
|
+
const organizationBusy = (error) => {
|
|
597
|
+
logDenial("organization_lookup_busy");
|
|
598
|
+
return Response.json({
|
|
599
|
+
error: {
|
|
600
|
+
code: "organization_lookup_busy",
|
|
601
|
+
message: "Organization admission is temporarily busy. Retry later.",
|
|
602
|
+
retryable: true,
|
|
603
|
+
},
|
|
604
|
+
}, { status: 503, headers: { "Retry-After": String(error.retrySeconds) } });
|
|
605
|
+
};
|
|
359
606
|
/**
|
|
360
607
|
* The domain half of admission. Fails CLOSED on every uncertainty — no
|
|
361
608
|
* primary email, an unverified one, a malformed address, or the lookup
|
|
362
609
|
* itself failing — because "we could not tell" and "they belong here" must
|
|
363
|
-
* not be the same answer for a membership rule.
|
|
610
|
+
* not be the same answer for a membership rule. A failed or cancelled
|
|
611
|
+
* lookup is `undefined`, not a verdict, so it refuses without being cached.
|
|
364
612
|
*/
|
|
365
|
-
const checkDomain =
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
613
|
+
const checkDomain = authMethod((userId, clerk) => {
|
|
614
|
+
return authFlow(function* () {
|
|
615
|
+
if (!allowedDomains)
|
|
616
|
+
return true;
|
|
617
|
+
let email;
|
|
618
|
+
try {
|
|
619
|
+
const user = (yield authInvoke(clerk.users, "getUser", [userId]));
|
|
620
|
+
const primary = user.emailAddresses?.find((address) => address.id === user.primaryEmailAddressId);
|
|
621
|
+
if (primary?.verification?.status === "verified") {
|
|
622
|
+
email = primary.emailAddress;
|
|
623
|
+
}
|
|
374
624
|
}
|
|
625
|
+
catch (error) {
|
|
626
|
+
logDenial("email_lookup_failed", error);
|
|
627
|
+
return undefined;
|
|
628
|
+
}
|
|
629
|
+
const domain = email ? emailDomain(email) : null;
|
|
630
|
+
if (!domain) {
|
|
631
|
+
logDenial("verified_email_invalid");
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
634
|
+
if (!allowedDomains.has(domain)) {
|
|
635
|
+
logDenial("email_domain_denied");
|
|
636
|
+
return false;
|
|
637
|
+
}
|
|
638
|
+
return true;
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
/** A cached answer still within its TTL, counted from its lookup's start. */
|
|
642
|
+
const cachedMembership = (key) => {
|
|
643
|
+
const verdict = membershipCache.get(key);
|
|
644
|
+
if (verdict?.member === undefined)
|
|
645
|
+
return undefined;
|
|
646
|
+
if (!withinTtl(verdict.started, verdict.member ? GATE_ALLOWED_TTL_MS : GATE_FORBIDDEN_TTL_MS)) {
|
|
647
|
+
membershipCache.set(key, { started: verdict.started, sequence: verdict.sequence });
|
|
648
|
+
return undefined;
|
|
375
649
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
650
|
+
membershipCache.delete(key);
|
|
651
|
+
membershipCache.set(key, verdict);
|
|
652
|
+
return verdict.member;
|
|
653
|
+
};
|
|
654
|
+
/**
|
|
655
|
+
* Cache a lookup's answer unless a lookup that started later has already
|
|
656
|
+
* written this key. Evicting an entry raises a floor under every key,
|
|
657
|
+
* because the evicted entry no longer guards its own: a lookup that started
|
|
658
|
+
* before it never writes. Either way an older answer never replaces a
|
|
659
|
+
* newer verdict. A floor-suppressed answer invalidates an older entry for
|
|
660
|
+
* its own key rather than leaving that older verdict authoritative.
|
|
661
|
+
*/
|
|
662
|
+
const cacheMembership = (key, verdict) => {
|
|
663
|
+
const current = membershipCache.get(key);
|
|
664
|
+
if (current && current.sequence > verdict.sequence)
|
|
665
|
+
return;
|
|
666
|
+
if (verdict.sequence <= evictedMembershipSequence) {
|
|
667
|
+
// The floor may have advanced past both lookups while an older verdict
|
|
668
|
+
// for this key remained cached. Suppressing the newer answer must not
|
|
669
|
+
// preserve that older admission. Forget it; the floor still prevents
|
|
670
|
+
// either lookup (or an evicted, newer one) from being overwritten.
|
|
671
|
+
if (current && current.sequence < verdict.sequence)
|
|
672
|
+
membershipCache.delete(key);
|
|
673
|
+
return;
|
|
388
674
|
}
|
|
389
|
-
|
|
675
|
+
membershipCache.delete(key);
|
|
676
|
+
membershipCache.set(key, verdict);
|
|
677
|
+
if (membershipCache.size <= GATE_CACHE_MAX_IDENTITIES)
|
|
678
|
+
return;
|
|
679
|
+
const [oldestKey, oldest] = membershipCache.entries().next().value;
|
|
680
|
+
membershipCache.delete(oldestKey);
|
|
681
|
+
evictedMembershipSequence = Math.max(evictedMembershipSequence, oldest.sequence);
|
|
390
682
|
};
|
|
683
|
+
/**
|
|
684
|
+
* The organization half of admission, checked before the domain allowlist
|
|
685
|
+
* and `gate`, for every token type and session. No claim answers it or is
|
|
686
|
+
* read: an OAuth JWT verifies offline and can outlive a removed membership
|
|
687
|
+
* by a day, and a member's token is audience-bound to this deployment
|
|
688
|
+
* whichever organization they chose at consent. Membership always comes
|
|
689
|
+
* from the Backend API, cached per organization and user for 60 seconds
|
|
690
|
+
* from the start of the lookup that answered (30 for a non-member), since
|
|
691
|
+
* Clerk answered no earlier. With a clock that does not step back unseen
|
|
692
|
+
* within the TTL window, every request reaching this check 60 seconds or
|
|
693
|
+
* more after a member's removal is refused. An unseen rollback within that
|
|
694
|
+
* window can extend admission beyond 60 seconds of elapsed time.
|
|
695
|
+
*
|
|
696
|
+
* Each request that misses the cache runs and owns its own lookup. On
|
|
697
|
+
* Workers one request may not touch another's fetch, signal, or body, so
|
|
698
|
+
* concurrent misses within the plain-counter bounds each ask Clerk, and a caller that leaves cancels only
|
|
699
|
+
* its own lookup. A failed or abandoned lookup is not an answer, so it
|
|
700
|
+
* refuses without being cached.
|
|
701
|
+
*/
|
|
702
|
+
const checkOrganization = authMethod((userId, signal) => {
|
|
703
|
+
return authFlow(function* () {
|
|
704
|
+
if (!organizationId)
|
|
705
|
+
return true;
|
|
706
|
+
const key = `${organizationId}/${userId}`;
|
|
707
|
+
const cached = cachedMembership(key);
|
|
708
|
+
if (cached !== undefined)
|
|
709
|
+
return cached;
|
|
710
|
+
const started = Date.now();
|
|
711
|
+
const sequence = ++membershipLookupsStarted;
|
|
712
|
+
let member;
|
|
713
|
+
try {
|
|
714
|
+
member = (yield authCall(organizationLookup, [
|
|
715
|
+
userId,
|
|
716
|
+
signal,
|
|
717
|
+
authMethod(() => {
|
|
718
|
+
return authCall(organizationMember, [opts.secretKey, organizationId, userId, signal]);
|
|
719
|
+
}),
|
|
720
|
+
]));
|
|
721
|
+
}
|
|
722
|
+
catch (error) {
|
|
723
|
+
if (error instanceof OrganizationLookupBusy)
|
|
724
|
+
return organizationBusy(error);
|
|
725
|
+
logDenial("organization_lookup_failed");
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
cacheMembership(key, { member, started, sequence });
|
|
729
|
+
if (!member)
|
|
730
|
+
logDenial("organization_membership_denied");
|
|
731
|
+
return member;
|
|
732
|
+
});
|
|
733
|
+
});
|
|
391
734
|
/**
|
|
392
735
|
* Is this authenticated user admitted? The domain allowlist and `gate` both
|
|
393
736
|
* have to say yes, and the allowlist runs first so an outsider never reaches
|
|
394
737
|
* operator gate code. One cached verdict covers both, so composing them costs
|
|
395
738
|
* no more Clerk calls than `gate` alone did.
|
|
396
739
|
*/
|
|
397
|
-
const checkGate =
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
740
|
+
const checkGate = authMethod((userId, clerk, signal) => {
|
|
741
|
+
return authFlow(function* () {
|
|
742
|
+
if (!opts.gate && !allowedDomains)
|
|
743
|
+
return true;
|
|
744
|
+
const hit = readIdentityCache(gateCache, userId);
|
|
745
|
+
if (hit)
|
|
746
|
+
return hit.allowed;
|
|
747
|
+
let allowed;
|
|
748
|
+
try {
|
|
749
|
+
const domainAllowed = (yield authCall(checkDomain, [userId, clerk]));
|
|
750
|
+
if (domainAllowed === undefined)
|
|
751
|
+
return false;
|
|
752
|
+
signal.throwIfAborted();
|
|
753
|
+
allowed = domainAllowed && (opts.gate ? (yield authInvoke(opts, "gate", [userId, clerk])) : true);
|
|
754
|
+
// `clerk` is bound to this signal; a gate may turn its cancelled
|
|
755
|
+
// lookup into `false`. Only a completed, uncancelled verdict is cached.
|
|
756
|
+
signal.throwIfAborted();
|
|
757
|
+
if (domainAllowed && !allowed)
|
|
758
|
+
logDenial("gate_denied");
|
|
759
|
+
}
|
|
760
|
+
catch {
|
|
761
|
+
logDenial("gate_failed");
|
|
762
|
+
return false;
|
|
763
|
+
}
|
|
764
|
+
writeIdentityCache(gateCache, userId, {
|
|
765
|
+
allowed,
|
|
766
|
+
exp: Date.now() + (allowed ? GATE_ALLOWED_TTL_MS : GATE_FORBIDDEN_TTL_MS),
|
|
767
|
+
});
|
|
768
|
+
return allowed;
|
|
417
769
|
});
|
|
418
|
-
|
|
419
|
-
};
|
|
770
|
+
});
|
|
420
771
|
return {
|
|
421
772
|
kind: "clerk",
|
|
422
773
|
interactiveOperator: true,
|
|
@@ -436,126 +787,182 @@ export function clerkAuth(opts) {
|
|
|
436
787
|
...(opts.signInUrl ? { signInUrl: opts.signInUrl } : {}),
|
|
437
788
|
...(opts.signUpUrl ? { signUpUrl: opts.signUpUrl } : {}),
|
|
438
789
|
},
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
pathname === "/.well-known/oauth-protected-resource
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
if (request.method === "OPTIONS") {
|
|
450
|
-
return new Response(null, { status: 204, headers: CORS_HEADERS });
|
|
451
|
-
}
|
|
452
|
-
const base = resolveBase(baseUrl);
|
|
453
|
-
const pool = pathname.startsWith("/.well-known/oauth-protected-resource/mcp/")
|
|
454
|
-
? mcpPoolSuffix(pathname.slice("/.well-known/oauth-protected-resource".length))
|
|
455
|
-
: null;
|
|
456
|
-
if (pathname === "/.well-known/oauth-protected-resource" ||
|
|
457
|
-
pathname === "/.well-known/oauth-protected-resource/mcp" ||
|
|
458
|
-
pool) {
|
|
459
|
-
return Response.json({
|
|
460
|
-
resource: `${base}/mcp${pool ?? ""}`,
|
|
461
|
-
authorization_servers: [frontendApiUrl],
|
|
462
|
-
bearer_methods_supported: ["header"],
|
|
463
|
-
scopes_supported: scopes,
|
|
464
|
-
}, { headers: CORS_HEADERS });
|
|
465
|
-
}
|
|
466
|
-
return null;
|
|
467
|
-
},
|
|
468
|
-
async authorize(request, baseUrl) {
|
|
469
|
-
const credential = authorizationCredential(request);
|
|
470
|
-
const tokenPresent = credential.kind !== "absent";
|
|
471
|
-
if (credential.kind === "invalid" || isMachineCredential(request)) {
|
|
472
|
-
logDenial("authorization_header_invalid");
|
|
473
|
-
return { ok: false, response: unauthorized(baseUrl, tokenPresent, request) };
|
|
474
|
-
}
|
|
475
|
-
const browserOAuthRoute = /^\/(?:connect|oauth\/callback)\//.test(new URL(request.url).pathname);
|
|
476
|
-
let userId;
|
|
477
|
-
let sessionCookies;
|
|
478
|
-
try {
|
|
479
|
-
const pathname = new URL(request.url).pathname;
|
|
480
|
-
const isMcp = pathname === "/mcp" || pathname.startsWith("/mcp/");
|
|
481
|
-
const state = await clerk.authenticateRequest(authorizationRequest(request), {
|
|
482
|
-
acceptsToken: isMcp ? "oauth_token" : "session_token",
|
|
483
|
-
});
|
|
484
|
-
const browserOAuth = !tokenPresent && browserOAuthRoute;
|
|
485
|
-
// Consent can outlast Clerk's session JWT. Preserve the SDK's browser
|
|
486
|
-
// handshake, which returns here with a refreshed, verified session.
|
|
487
|
-
if (browserOAuth && state.status === "handshake" && state.headers.has("location")) {
|
|
488
|
-
return {
|
|
489
|
-
ok: false,
|
|
490
|
-
response: new Response(null, {
|
|
491
|
-
status: 307,
|
|
492
|
-
headers: state.headers,
|
|
493
|
-
}),
|
|
494
|
-
};
|
|
790
|
+
handleMetadata: authMethod(function (request, baseUrl) {
|
|
791
|
+
return authValue(() => {
|
|
792
|
+
const { pathname } = new URL(request.url);
|
|
793
|
+
const protectedResource = pathname === "/.well-known/oauth-protected-resource" ||
|
|
794
|
+
pathname === "/.well-known/oauth-protected-resource/mcp" ||
|
|
795
|
+
/^\/\.well-known\/oauth-protected-resource\/mcp\/[a-z0-9_-]+$/.test(pathname);
|
|
796
|
+
if (!protectedResource)
|
|
797
|
+
return null;
|
|
798
|
+
if (request.method !== "GET" && request.method !== "HEAD" && request.method !== "OPTIONS") {
|
|
799
|
+
return new Response(null, { status: 405, headers: { ...CORS_HEADERS, Allow: "GET, HEAD, OPTIONS" } });
|
|
495
800
|
}
|
|
496
|
-
if (
|
|
497
|
-
|
|
498
|
-
const auth = state.toAuth();
|
|
499
|
-
if (!auth?.isAuthenticated) {
|
|
500
|
-
logDenial("authentication_failed");
|
|
501
|
-
return {
|
|
502
|
-
ok: false,
|
|
503
|
-
response: unauthorized(baseUrl, tokenPresent, request),
|
|
504
|
-
};
|
|
801
|
+
if (request.method === "OPTIONS") {
|
|
802
|
+
return new Response(null, { status: 204, headers: CORS_HEADERS });
|
|
505
803
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
804
|
+
const base = resolveBase(baseUrl);
|
|
805
|
+
const pool = pathname.startsWith("/.well-known/oauth-protected-resource/mcp/")
|
|
806
|
+
? mcpPoolSuffix(pathname.slice("/.well-known/oauth-protected-resource".length))
|
|
807
|
+
: null;
|
|
808
|
+
if (pathname === "/.well-known/oauth-protected-resource" ||
|
|
809
|
+
pathname === "/.well-known/oauth-protected-resource/mcp" ||
|
|
810
|
+
pool) {
|
|
811
|
+
return Response.json({
|
|
812
|
+
resource: `${base}/mcp${pool ?? ""}`,
|
|
813
|
+
authorization_servers: [frontendApiUrl],
|
|
814
|
+
bearer_methods_supported: ["header"],
|
|
815
|
+
scopes_supported: scopes,
|
|
816
|
+
}, { headers: CORS_HEADERS });
|
|
817
|
+
}
|
|
818
|
+
return null;
|
|
819
|
+
});
|
|
820
|
+
}),
|
|
821
|
+
authorize: authMethod(function (request, baseUrl) {
|
|
822
|
+
return authFlow(function* () {
|
|
823
|
+
const credential = authorizationCredential(request);
|
|
824
|
+
const tokenPresent = credential.kind !== "absent";
|
|
825
|
+
if (credential.kind === "invalid" || isMachineCredential(request)) {
|
|
826
|
+
logDenial("authorization_header_invalid");
|
|
827
|
+
return { ok: false, response: unauthorized(baseUrl, tokenPresent, request) };
|
|
828
|
+
}
|
|
829
|
+
const finished = new AbortController();
|
|
830
|
+
const signal = AbortSignal.any([request.signal, finished.signal]);
|
|
831
|
+
const timer = setTimeout(() => finished.abort(new Error("Clerk authentication timed out")), BACKEND_LOOKUP_TIMEOUT_MS);
|
|
832
|
+
const pending = new Set();
|
|
833
|
+
// SDK-created verification clients, JWKS, handshakes, domain lookups and
|
|
834
|
+
// gate calls all close over this authorization's signal, never another
|
|
835
|
+
// request's client. The long-lived client above serves display labels only.
|
|
836
|
+
const clerk = createByteReadingClerkClient(opts, signal, pending);
|
|
837
|
+
try {
|
|
838
|
+
signal.throwIfAborted();
|
|
839
|
+
const browserOAuthRoute = /^\/(?:connect|oauth\/callback)\//.test(new URL(request.url).pathname);
|
|
840
|
+
let userId;
|
|
841
|
+
let sessionCookies;
|
|
842
|
+
try {
|
|
843
|
+
const pathname = new URL(request.url).pathname;
|
|
844
|
+
const isMcp = pathname === "/mcp" || pathname.startsWith("/mcp/");
|
|
845
|
+
const state = (yield authInvoke(clerk, "authenticateRequest", [
|
|
846
|
+
authorizationRequest(request),
|
|
847
|
+
{
|
|
848
|
+
acceptsToken: isMcp ? "oauth_token" : "session_token",
|
|
849
|
+
},
|
|
850
|
+
]));
|
|
851
|
+
const browserOAuth = !tokenPresent && browserOAuthRoute;
|
|
852
|
+
// Consent can outlast Clerk's session JWT. Preserve the SDK's browser
|
|
853
|
+
// handshake, which returns here with a refreshed, verified session.
|
|
854
|
+
if (browserOAuth && state.status === "handshake" && state.headers.has("location")) {
|
|
855
|
+
return {
|
|
856
|
+
ok: false,
|
|
857
|
+
response: new Response(null, {
|
|
858
|
+
status: 307,
|
|
859
|
+
headers: state.headers,
|
|
860
|
+
}),
|
|
861
|
+
};
|
|
517
862
|
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
863
|
+
if (browserOAuth)
|
|
864
|
+
sessionCookies = state.headers?.getSetCookie();
|
|
865
|
+
const auth = state.toAuth();
|
|
866
|
+
if (!auth?.isAuthenticated) {
|
|
867
|
+
logDenial("authentication_failed");
|
|
868
|
+
return {
|
|
869
|
+
ok: false,
|
|
870
|
+
response: unauthorized(baseUrl, tokenPresent, request),
|
|
871
|
+
};
|
|
521
872
|
}
|
|
873
|
+
if (isMcp) {
|
|
874
|
+
const resource = `${resolveBase(baseUrl)}/mcp${mcpPoolSuffix(pathname) ?? ""}`;
|
|
875
|
+
if (auth.tokenType !== "oauth_token") {
|
|
876
|
+
logDenial("token_type_mismatch");
|
|
877
|
+
return { ok: false, response: unauthorized(baseUrl, tokenPresent, request) };
|
|
878
|
+
}
|
|
879
|
+
const token = (yield authInvoke(auth, "getToken", []));
|
|
880
|
+
let claims;
|
|
881
|
+
if (token.startsWith("oat_")) {
|
|
882
|
+
try {
|
|
883
|
+
const verify = authMethod(() => {
|
|
884
|
+
return authCall(opaqueOAuthClaims, [
|
|
885
|
+
token,
|
|
886
|
+
opts.secretKey,
|
|
887
|
+
auth.userId,
|
|
888
|
+
auth.clientId,
|
|
889
|
+
signal,
|
|
890
|
+
]);
|
|
891
|
+
});
|
|
892
|
+
claims = (yield organizationId
|
|
893
|
+
? authCall(organizationLookup, [auth.userId ?? "", signal, verify])
|
|
894
|
+
: authCall(verify, []));
|
|
895
|
+
}
|
|
896
|
+
catch (error) {
|
|
897
|
+
if (error instanceof OrganizationLookupBusy)
|
|
898
|
+
return { ok: false, response: organizationBusy(error) };
|
|
899
|
+
logDenial("oauth_verification_failed");
|
|
900
|
+
return { ok: false, response: unauthorized(baseUrl, tokenPresent, request) };
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
else {
|
|
904
|
+
claims = decodeJwt(token).payload;
|
|
905
|
+
}
|
|
906
|
+
const rejection = oauthBindingRejection(claims, resource, auth.clientId, allowedOAuthClientIds);
|
|
907
|
+
if (rejection) {
|
|
908
|
+
logDenial(rejection);
|
|
909
|
+
return { ok: false, response: unauthorized(baseUrl, tokenPresent, request) };
|
|
910
|
+
}
|
|
911
|
+
// The membership lookup is keyed by subject, so on a shared
|
|
912
|
+
// instance the issuer and subject must be this instance's and the
|
|
913
|
+
// SDK's. An opaque token's claims come from this instance's Backend
|
|
914
|
+
// API, which has already matched the subject and carries no issuer.
|
|
915
|
+
if (organizationId &&
|
|
916
|
+
!token.startsWith("oat_") &&
|
|
917
|
+
(claims.iss !== frontendApiUrl || claims.sub !== auth.userId)) {
|
|
918
|
+
logDenial("token_identity_mismatch");
|
|
919
|
+
return { ok: false, response: unauthorized(baseUrl, tokenPresent, request) };
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
else {
|
|
923
|
+
if (auth.tokenType !== "session_token") {
|
|
924
|
+
logDenial("token_type_mismatch");
|
|
925
|
+
return { ok: false, response: unauthorized(baseUrl, tokenPresent, request) };
|
|
926
|
+
}
|
|
927
|
+
// Browser session origins retain their existing deployment pin.
|
|
928
|
+
const azp = auth.sessionClaims?.azp;
|
|
929
|
+
const origin = new URL(resolveBase(baseUrl)).origin;
|
|
930
|
+
if (azp && azp !== origin) {
|
|
931
|
+
logDenial("session_origin_mismatch");
|
|
932
|
+
return { ok: false, response: unauthorized(baseUrl, tokenPresent, request) };
|
|
933
|
+
}
|
|
934
|
+
// The session's active organization is not read; membership is.
|
|
935
|
+
if (organizationId &&
|
|
936
|
+
(auth.sessionClaims.iss !== frontendApiUrl || auth.sessionClaims.sub !== auth.userId)) {
|
|
937
|
+
logDenial("token_identity_mismatch");
|
|
938
|
+
return { ok: false, response: unauthorized(baseUrl, tokenPresent, request) };
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
userId = auth.userId ?? undefined;
|
|
522
942
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
const rejection = oauthBindingRejection(claims, resource, auth.clientId, allowedOAuthClientIds);
|
|
527
|
-
if (rejection) {
|
|
528
|
-
logDenial(rejection);
|
|
529
|
-
return { ok: false, response: unauthorized(baseUrl, tokenPresent, request) };
|
|
943
|
+
catch {
|
|
944
|
+
logDenial("authentication_failed");
|
|
945
|
+
return { ok: false, response: unauthorized(baseUrl, true, request) };
|
|
530
946
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
logDenial("token_type_mismatch");
|
|
535
|
-
return { ok: false, response: unauthorized(baseUrl, tokenPresent, request) };
|
|
947
|
+
if (!userId) {
|
|
948
|
+
logDenial("user_missing");
|
|
949
|
+
return { ok: false, response: unauthorized(baseUrl, true, request) };
|
|
536
950
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
if (
|
|
541
|
-
|
|
542
|
-
return { ok: false, response: unauthorized(baseUrl, tokenPresent, request) };
|
|
951
|
+
const membership = (yield authCall(checkOrganization, [userId, signal]));
|
|
952
|
+
if (membership instanceof Response)
|
|
953
|
+
return { ok: false, response: membership };
|
|
954
|
+
if (!membership || !(yield authCall(checkGate, [userId, clerk, signal]))) {
|
|
955
|
+
return { ok: false, response: forbidden() };
|
|
543
956
|
}
|
|
957
|
+
signal.throwIfAborted();
|
|
958
|
+
return { ok: true, userId, ...(sessionCookies?.length ? { sessionCookies } : {}) };
|
|
544
959
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
logDenial("user_missing");
|
|
553
|
-
return { ok: false, response: unauthorized(baseUrl, true, request) };
|
|
554
|
-
}
|
|
555
|
-
if (!(await checkGate(userId))) {
|
|
556
|
-
return { ok: false, response: forbidden() };
|
|
557
|
-
}
|
|
558
|
-
return { ok: true, userId, ...(sessionCookies?.length ? { sessionCookies } : {}) };
|
|
559
|
-
},
|
|
960
|
+
finally {
|
|
961
|
+
clearTimeout(timer);
|
|
962
|
+
finished.abort();
|
|
963
|
+
(yield authAll(pending, true));
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
}),
|
|
560
967
|
};
|
|
561
968
|
}
|