@zackbart/connecta 0.34.0 → 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 +393 -0
- package/README.md +62 -105
- 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 +15 -7
- package/dist/connectors/api-connector.js +147 -57
- 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/option-shapes.js +1 -1
- package/dist/connectors/remote-mcp.d.ts +12 -30
- package/dist/connectors/remote-mcp.js +960 -564
- package/dist/d1.js +10 -243
- package/dist/derived-context.js +5 -0
- package/dist/describe-config.d.ts +5 -1
- package/dist/describe-config.js +3 -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 +221 -87
- 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 +534 -408
- 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 +8 -4
- package/dist/operator-ui/connection-map.js +9 -6
- package/dist/operator-ui/contract.d.ts +7 -3
- package/dist/operator-ui/endpoints.js +5 -14
- 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/health.js +52 -0
- package/dist/operator-ui/model.d.ts +2 -3
- package/dist/operator-ui/overview.js +11 -19
- package/dist/operator-ui/setup-commands.js +0 -4
- package/dist/operator-ui/view.d.ts +5 -29
- package/dist/operator-ui/view.js +3 -59
- 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 +71 -32
- package/dist/provider.js +134 -40
- 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 +8 -209
- package/dist/providers/_shared/google/workspace.js +7 -426
- 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 +3 -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 +17 -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 +7 -22
- package/dist/providers/cloudflare/index.js +80 -194
- 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 +3299 -3208
- package/dist/providers/cloudflare/{value-safety.js → safety.js} +538 -18
- 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 +3 -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 +6 -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 +7 -27
- package/dist/providers/stripe/index.js +75 -191
- 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 -10
- package/dist/providers/stripe/tools.js +77 -0
- package/dist/providers/tithely/api.js +531 -0
- package/dist/providers/tithely/index.d.ts +19 -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 +11 -25
- package/dist/providers/vercel/index.js +67 -261
- 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 +398 -376
- package/dist/providers/vercel/{value-safety.js → safety.js} +261 -42
- 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 +30 -28
- package/dist/registry.js +446 -413
- 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 +29 -22
- 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 +330 -224
- 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 +11 -99
- 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 +2550 -186
- package/documentation/auth.md +821 -98
- package/documentation/code-mode.md +56 -21
- package/documentation/deploying.md +164 -20
- package/documentation/hosted-mcp-migration.md +460 -0
- package/documentation/integrating.md +144 -105
- package/documentation/meta-tools.md +411 -34
- package/documentation/operating.md +28 -4
- package/documentation/operator-tests.md +11 -2
- package/documentation/operator-ui.md +103 -33
- package/documentation/provider-auth-migration.md +38 -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 -42
- 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 -134
- package/dist/catalog-drift.js +0 -493
- 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 -43
- 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 -1078
- 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/linear/index.d.ts +0 -55
- package/dist/providers/linear/index.js +0 -352
- package/dist/providers/linear/skill.generated.js +0 -8
- package/dist/providers/mixpanel/index.d.ts +0 -57
- package/dist/providers/mixpanel/index.js +0 -468
- 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 -65
- 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 -49
- package/dist/providers/revenuecat/index.js +0 -557
- 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
|
@@ -1,15 +1,72 @@
|
|
|
1
1
|
import { UnauthorizedError } from "@modelcontextprotocol/client";
|
|
2
2
|
import { Deferred, Effect } from "effect";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { byteReadResponse } from "../byte-read-response.js";
|
|
4
|
+
import { deriveContext } from "../derived-context.js";
|
|
5
|
+
import { ConnectorCallError, resendAdvice } from "../errors.js";
|
|
6
|
+
import { oauthPartitionFor, retainOAuthPartition } from "../oauth-partition.js";
|
|
7
|
+
import { keepErrorName } from "../redaction-identity.js";
|
|
8
|
+
import { fromSignal, runEdge } from "../runtime/run.js";
|
|
5
9
|
import { SentSecrets } from "../sent-secrets.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
10
|
+
import { OAUTH_FLOW_TTL_SECONDS, OAUTH_REFRESH_LEASE_SECONDS, oauthFlowKeys, oauthGrantKeys, oauthRefreshActiveKeys, oauthRefreshKeys, oauthRefreshSpentKeys, } from "../storage/keys.js";
|
|
11
|
+
import { authenticateRemoteClient, downstreamClientMetadata, remoteClientAuthMethod, } from "./downstream-client-metadata.js";
|
|
12
|
+
import { revokeDownstreamGrant } from "./downstream-revocation.js";
|
|
13
|
+
import { authAwait, authCall, authCatch, authDetached, authFlow, authInvoke, authMethod, authOwnMethod, authProgram, authStorage, authThen, authValue, registerAuth, registerAuthMethod, } from "./native.js";
|
|
14
|
+
import { isOAuthCredentialRequest, isRefreshTokenRequest, refreshResponseOutcome, sdkSafeTokenResponse, tokenGrantType, } from "./oauth-token-response.js";
|
|
15
|
+
import { deleteV2Keys, discoveryIssuer, readV2Grant, } from "./oauth-v2-migration.js";
|
|
16
|
+
/** @internal Keep the SDK's cloned body reads inside the code exchange's request. */
|
|
17
|
+
export function codeExchangeResponse(response, signal) {
|
|
18
|
+
if (!response.body)
|
|
19
|
+
return response;
|
|
20
|
+
const reader = response.body.getReader();
|
|
21
|
+
void reader.closed.catch(() => { });
|
|
22
|
+
let closed = false;
|
|
23
|
+
let abort;
|
|
24
|
+
const release = () => {
|
|
25
|
+
closed = true;
|
|
26
|
+
signal.removeEventListener("abort", abort);
|
|
27
|
+
reader.releaseLock();
|
|
28
|
+
};
|
|
29
|
+
const body = new ReadableStream({
|
|
30
|
+
start(controller) {
|
|
31
|
+
abort = () => {
|
|
32
|
+
if (closed)
|
|
33
|
+
return;
|
|
34
|
+
void reader.cancel().catch(() => { });
|
|
35
|
+
release();
|
|
36
|
+
controller.error(signal.reason);
|
|
37
|
+
};
|
|
38
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
39
|
+
if (signal.aborted)
|
|
40
|
+
abort();
|
|
41
|
+
},
|
|
42
|
+
async pull(controller) {
|
|
43
|
+
try {
|
|
44
|
+
const next = await reader.read();
|
|
45
|
+
if (closed)
|
|
46
|
+
return;
|
|
47
|
+
if (next.done) {
|
|
48
|
+
release();
|
|
49
|
+
controller.close();
|
|
50
|
+
}
|
|
51
|
+
else
|
|
52
|
+
controller.enqueue(next.value);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (!closed) {
|
|
56
|
+
release();
|
|
57
|
+
controller.error(error);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
cancel() {
|
|
62
|
+
if (!closed) {
|
|
63
|
+
void reader.cancel().catch(() => { });
|
|
64
|
+
release();
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
return byteReadResponse(new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers }));
|
|
69
|
+
}
|
|
13
70
|
/** RFC 6749 section 3.3: one or more scope tokens, separated by single spaces. */
|
|
14
71
|
const OAUTH_SCOPE = /^[\x21\x23-\x5B\x5D-\x7E]+(?: [\x21\x23-\x5B\x5D-\x7E]+)*$/;
|
|
15
72
|
/** Refuse, at construction, a configured scope no authorization server could parse. */
|
|
@@ -20,16 +77,13 @@ export function assertOAuthScope(connectorId, scope) {
|
|
|
20
77
|
}
|
|
21
78
|
/**
|
|
22
79
|
* The context an explicit authorization start runs under: allowed to begin
|
|
23
|
-
* consent,
|
|
24
|
-
* owner partition
|
|
25
|
-
* and calls never do, which is what
|
|
80
|
+
* consent, in the same request scope, and derived so the registry's sealer,
|
|
81
|
+
* owner partition, and every other call-state entry carry across the copy.
|
|
82
|
+
* Only `startAuth` builds one; status reads and calls never do, which is what
|
|
83
|
+
* keeps them from starting authorization.
|
|
26
84
|
*/
|
|
27
85
|
export function authorizingContext(ctx) {
|
|
28
|
-
return
|
|
29
|
-
...ctx,
|
|
30
|
-
requestScope: ctx.requestScope ?? ctx,
|
|
31
|
-
allowAuthorization: true,
|
|
32
|
-
}), oauthPartitionFor(ctx));
|
|
86
|
+
return deriveContext(ctx, { allowAuthorization: true });
|
|
33
87
|
}
|
|
34
88
|
/**
|
|
35
89
|
* A callback whose consent another callback already claimed: fixed text, and
|
|
@@ -42,6 +96,7 @@ export class OAuthCallbackClaimedError extends ConnectorCallError {
|
|
|
42
96
|
this.name = "OAuthCallbackClaimedError";
|
|
43
97
|
}
|
|
44
98
|
}
|
|
99
|
+
keepErrorName(OAuthCallbackClaimedError, "OAuthCallbackClaimedError");
|
|
45
100
|
/** A local destination guard proved that no OAuth HTTP request left. */
|
|
46
101
|
export class OAuthRequestNotSentError extends ConnectorCallError {
|
|
47
102
|
}
|
|
@@ -73,18 +128,51 @@ const REFRESH_REQUEST_DEADLINE_MS = 20_000;
|
|
|
73
128
|
const REFRESH_WAIT_DEADLINE_MS = 35_000;
|
|
74
129
|
function refreshWait(signal) {
|
|
75
130
|
const deadline = new AbortController();
|
|
76
|
-
|
|
131
|
+
let timer = setTimeout(() => deadline.abort(), REFRESH_WAIT_DEADLINE_MS);
|
|
77
132
|
let closed = false;
|
|
133
|
+
let transferred = false;
|
|
134
|
+
let requestStarted = false;
|
|
78
135
|
const abort = () => deadline.abort(aborted(signal));
|
|
79
136
|
signal?.addEventListener("abort", abort, { once: true });
|
|
80
137
|
if (signal?.aborted)
|
|
81
138
|
abort();
|
|
82
139
|
return {
|
|
83
140
|
signal: deadline.signal,
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
141
|
+
get requestStarted() {
|
|
142
|
+
return requestStarted;
|
|
143
|
+
},
|
|
144
|
+
run: authMethod((operation) => {
|
|
145
|
+
return authFlow(function* () {
|
|
146
|
+
// A race's losing operation may still start. Check before constructing it,
|
|
147
|
+
// then again before handing a late result back to preparation.
|
|
148
|
+
if (deadline.signal.aborted)
|
|
149
|
+
return (yield Effect.fail(aborted(deadline.signal)));
|
|
150
|
+
return (yield authThen(Effect.raceAllFirst([fromSignal(deadline.signal), authCall(operation, [])]), (value) => {
|
|
151
|
+
if (deadline.signal.aborted)
|
|
152
|
+
throw aborted(deadline.signal);
|
|
153
|
+
return value;
|
|
154
|
+
}));
|
|
155
|
+
});
|
|
156
|
+
}),
|
|
157
|
+
transfer: () => {
|
|
158
|
+
if (deadline.signal.aborted)
|
|
159
|
+
throw aborted(deadline.signal);
|
|
160
|
+
if (transferred)
|
|
161
|
+
return;
|
|
162
|
+
// Synchronous with CAS submission: cancellation can prevent the write
|
|
163
|
+
// only before this point. Keep the independent preparation timer alive.
|
|
164
|
+
signal?.removeEventListener("abort", abort);
|
|
165
|
+
transferred = true;
|
|
166
|
+
},
|
|
167
|
+
beginRequest: () => {
|
|
168
|
+
if (deadline.signal.aborted)
|
|
169
|
+
throw aborted(deadline.signal);
|
|
170
|
+
requestStarted = true;
|
|
171
|
+
clearTimeout(timer);
|
|
172
|
+
timer = setTimeout(() => {
|
|
173
|
+
deadline.abort(new DOMException("OAuth refresh timed out", "TimeoutError"));
|
|
174
|
+
}, REFRESH_REQUEST_DEADLINE_MS);
|
|
175
|
+
},
|
|
88
176
|
close: () => {
|
|
89
177
|
if (closed)
|
|
90
178
|
return;
|
|
@@ -123,9 +211,16 @@ function tokenResponse(tokens) {
|
|
|
123
211
|
return Response.json(answer);
|
|
124
212
|
}
|
|
125
213
|
function refreshTokensDigest(tokens) {
|
|
126
|
-
|
|
127
|
-
return oauthStateDigest(JSON.stringify(Object.fromEntries(Object.entries(answer).sort(([a], [b]) => a.localeCompare(b)))));
|
|
214
|
+
return runEdge(authProgram(refreshTokensDigestNative, [tokens]));
|
|
128
215
|
}
|
|
216
|
+
const refreshTokensDigestNative = /* @__PURE__ */ registerAuth(refreshTokensDigest, function (tokens) {
|
|
217
|
+
return authFlow(function* () {
|
|
218
|
+
const { issuer: _issuer, ...answer } = tokens;
|
|
219
|
+
return (yield authCall(oauthStateDigest, [
|
|
220
|
+
JSON.stringify(Object.fromEntries(Object.entries(answer).sort(([a], [b]) => a.localeCompare(b)))),
|
|
221
|
+
]));
|
|
222
|
+
});
|
|
223
|
+
});
|
|
129
224
|
function requestedRefreshToken(init) {
|
|
130
225
|
return init?.body instanceof URLSearchParams
|
|
131
226
|
? init.body.get("refresh_token")
|
|
@@ -154,254 +249,91 @@ export class OAuthRefreshCoordinator {
|
|
|
154
249
|
constructor(retainPartition = () => () => { }) {
|
|
155
250
|
this.retainPartition = retainPartition;
|
|
156
251
|
}
|
|
157
|
-
coordinatedFetch(provider, baseFetch, requestSignal, defer, resourceFetch = baseFetch) {
|
|
158
|
-
return
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const exchange = grantType === "authorization_code";
|
|
173
|
-
if (exchange) {
|
|
174
|
-
const answered = await provider.claimCodeExchange();
|
|
175
|
-
if (answered)
|
|
176
|
-
return answered;
|
|
177
|
-
}
|
|
178
|
-
const dispatch = () => {
|
|
179
|
-
if (requestSignal?.aborted)
|
|
180
|
-
throw aborted(requestSignal);
|
|
181
|
-
const signal = requestSignal
|
|
182
|
-
? init?.signal
|
|
252
|
+
coordinatedFetch(provider, baseFetch, requestSignal, defer, resourceFetch = baseFetch, prepareCredentialRequest) {
|
|
253
|
+
return authMethod((input, init) => {
|
|
254
|
+
return authFlow(function* () {
|
|
255
|
+
// Freeze the form before any await: classification and the spent digest
|
|
256
|
+
// must describe the exact token sent, even if its caller mutates a form.
|
|
257
|
+
init = init
|
|
258
|
+
? { ...init, ...(init.body instanceof URLSearchParams ? { body: new URLSearchParams(init.body) } : {}) }
|
|
259
|
+
: undefined;
|
|
260
|
+
// Production adapters resolve the required host policy before a code or
|
|
261
|
+
// refresh claim can consume its single-use credential. Custom SDK leaves
|
|
262
|
+
// may supply their own policy; this hook performs no I/O and owns no data.
|
|
263
|
+
if (isOAuthCredentialRequest(init))
|
|
264
|
+
prepareCredentialRequest?.();
|
|
265
|
+
if (isRefreshTokenRequest(init)) {
|
|
266
|
+
const signal = requestSignal && init?.signal
|
|
183
267
|
? AbortSignal.any([requestSignal, init.signal])
|
|
184
|
-
: requestSignal
|
|
185
|
-
|
|
186
|
-
// Credential-bearing OAuth requests use the raw send path. A redirect
|
|
187
|
-
// is a dispatched failure, never authority to send the form again.
|
|
188
|
-
return tokenRequest
|
|
189
|
-
? baseFetch(input, { ...init, redirect: "manual", ...(signal ? { signal } : {}) })
|
|
190
|
-
: resourceFetch(input, { ...init, ...(signal ? { signal } : {}) });
|
|
191
|
-
};
|
|
192
|
-
// Awaited here so workerd associates a rejection with the fetch the SDK
|
|
193
|
-
// is already awaiting, not with an adopted inner promise.
|
|
194
|
-
const response = await (exchange ? provider.dispatchCodeExchange(dispatch, input) : dispatch());
|
|
195
|
-
if (!tokenRequest)
|
|
196
|
-
return response;
|
|
197
|
-
// A code exchange's failure is the SDK's to log as well.
|
|
198
|
-
const forSdk = await sdkSafeTokenResponse(response, exchange ? (tokens) => provider.recordCodeExchangeTokens(tokens) : undefined);
|
|
199
|
-
if (exchange && forSdk !== response)
|
|
200
|
-
provider.recordCodeExchangeRefusal(forSdk);
|
|
201
|
-
return forSdk;
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
async refresh(provider, input, init, baseFetch, signal, defer) {
|
|
205
|
-
const waiting = refreshWait(signal);
|
|
206
|
-
try {
|
|
207
|
-
// A verdict belongs to one refresh; never let an older one decide this.
|
|
208
|
-
provider.recordRefreshFailure(undefined);
|
|
209
|
-
const epoch = await waiting.run(() => provider.flowEpoch());
|
|
210
|
-
const requested = requestedRefreshToken(init);
|
|
211
|
-
for (let attempt = 0; attempt < 8; attempt++) {
|
|
212
|
-
if (signal?.aborted)
|
|
213
|
-
throw aborted(signal);
|
|
214
|
-
const joined = this.flights.get(epoch);
|
|
215
|
-
if (joined) {
|
|
216
|
-
const outcome = await waitForRefreshFlight(joined, signal, waiting.signal);
|
|
217
|
-
if (outcome.status === "failed") {
|
|
218
|
-
provider.recordRefreshFailure(outcome.verdict);
|
|
219
|
-
throw outcome.error;
|
|
220
|
-
}
|
|
221
|
-
if (outcome.status === "refreshed")
|
|
222
|
-
return provider.adoptRefresh(outcome.tokens);
|
|
223
|
-
continue;
|
|
224
|
-
}
|
|
225
|
-
// Preparation reads share the same bound as later claim waits.
|
|
226
|
-
const current = await waiting.run(() => provider.storedTokens());
|
|
227
|
-
if (this.flights.has(epoch))
|
|
228
|
-
continue;
|
|
229
|
-
if (current?.refresh_token &&
|
|
230
|
-
(current.refresh_token !== requested || provider.refreshedSinceRead(current, epoch))) {
|
|
231
|
-
return provider.adoptRefresh(current);
|
|
232
|
-
}
|
|
233
|
-
if (current?.refresh_token !== requested) {
|
|
234
|
-
provider.recordRefreshFailure({ kind: "dead" });
|
|
235
|
-
return Response.json({ error: "invalid_grant", error_description: "Refresh token is no longer active." }, { status: 400 });
|
|
236
|
-
}
|
|
237
|
-
const redemption = this.redeem(provider, epoch, requested, input, init, baseFetch, signal, waiting);
|
|
238
|
-
// The refresh owns its completion after dispatch, including storage.
|
|
239
|
-
// Attach it before yielding so a cancelled Worker keeps that work alive.
|
|
240
|
-
try {
|
|
241
|
-
detach(Effect.promise(() => redemption), defer ? { waitUntil: defer } : undefined);
|
|
268
|
+
: (requestSignal ?? init?.signal ?? undefined);
|
|
269
|
+
return (yield authInvoke(this, "refresh", [provider, input, init, baseFetch, signal, defer]));
|
|
242
270
|
}
|
|
243
|
-
|
|
244
|
-
|
|
271
|
+
const grantType = tokenGrantType(init);
|
|
272
|
+
const tokenRequest = isOAuthCredentialRequest(init);
|
|
273
|
+
const exchange = grantType === "authorization_code";
|
|
274
|
+
if (exchange) {
|
|
275
|
+
const answered = (yield authInvoke(provider, "claimCodeExchange", []));
|
|
276
|
+
if (answered)
|
|
277
|
+
return answered;
|
|
245
278
|
}
|
|
246
|
-
|
|
247
|
-
return
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
279
|
+
const dispatch = authMethod(() => {
|
|
280
|
+
return authFlow(function* () {
|
|
281
|
+
if (requestSignal?.aborted)
|
|
282
|
+
throw aborted(requestSignal);
|
|
283
|
+
const signal = requestSignal
|
|
284
|
+
? init?.signal
|
|
285
|
+
? AbortSignal.any([requestSignal, init.signal])
|
|
286
|
+
: requestSignal
|
|
287
|
+
: init?.signal;
|
|
288
|
+
// Credential-bearing OAuth requests use the raw send path. A redirect
|
|
289
|
+
// is a dispatched failure, never authority to send the form again.
|
|
290
|
+
return (yield tokenRequest
|
|
291
|
+
? authCall(baseFetch, [input, { ...init, redirect: "manual", ...(signal ? { signal } : {}) }])
|
|
292
|
+
: authCall(resourceFetch, [input, { ...init, ...(signal ? { signal } : {}) }]));
|
|
293
|
+
}.bind(this));
|
|
294
|
+
});
|
|
295
|
+
// Await the composed fetch here so workerd associates a rejection with
|
|
296
|
+
// the fetch the SDK is already awaiting.
|
|
297
|
+
const response = (yield exchange
|
|
298
|
+
? authInvoke(provider, "dispatchCodeExchange", [dispatch, input])
|
|
299
|
+
: authCall(dispatch, []));
|
|
300
|
+
if (!tokenRequest)
|
|
301
|
+
return response;
|
|
302
|
+
// A code exchange's failure is the SDK's to log as well.
|
|
303
|
+
const forSdk = (yield authCall(sdkSafeTokenResponse, [
|
|
304
|
+
response,
|
|
305
|
+
exchange
|
|
306
|
+
? authMethod((tokens) => {
|
|
307
|
+
return authInvoke(provider, "recordCodeExchangeTokens", [tokens]);
|
|
308
|
+
})
|
|
309
|
+
: undefined,
|
|
251
310
|
]));
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
throw aborted(signal);
|
|
258
|
-
if (waiting.signal.aborted)
|
|
259
|
-
throw provider.refreshContended();
|
|
260
|
-
throw error;
|
|
261
|
-
}
|
|
262
|
-
finally {
|
|
263
|
-
waiting.close();
|
|
264
|
-
}
|
|
311
|
+
if (exchange && forSdk !== response)
|
|
312
|
+
provider.recordCodeExchangeRefusal(forSdk);
|
|
313
|
+
return forSdk;
|
|
314
|
+
}.bind(this));
|
|
315
|
+
});
|
|
265
316
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
try {
|
|
287
|
-
const claim = await provider.claimRefresh(epoch, requested, signal, waiting);
|
|
288
|
-
if (claim instanceof Response) {
|
|
289
|
-
const tokens = await waiting.run(() => provider.storedTokens());
|
|
290
|
-
if (tokens)
|
|
291
|
-
settle({ status: "refreshed", tokens });
|
|
292
|
-
else
|
|
293
|
-
settle({ status: "retired" });
|
|
294
|
-
return claim;
|
|
295
|
-
}
|
|
296
|
-
lease = claim;
|
|
297
|
-
}
|
|
298
|
-
catch (error) {
|
|
299
|
-
fail(error, provider.refreshVerdict());
|
|
300
|
-
throw error;
|
|
301
|
-
}
|
|
302
|
-
const releaseLease = async (verdict, resolution = "ambiguous") => {
|
|
303
|
-
await provider.releaseRefresh(lease, verdict, undefined, resolution);
|
|
304
|
-
};
|
|
305
|
-
let outcome;
|
|
306
|
-
const deadline = new AbortController();
|
|
307
|
-
let dispatched = false;
|
|
308
|
-
let timer;
|
|
309
|
-
try {
|
|
310
|
-
const read = provider
|
|
311
|
-
.dispatchRefresh(lease, () => {
|
|
312
|
-
if (signal?.aborted)
|
|
313
|
-
throw aborted(signal);
|
|
314
|
-
if (init?.signal?.aborted)
|
|
315
|
-
throw aborted(init.signal);
|
|
316
|
-
dispatched = true;
|
|
317
|
-
// Preparation remains bounded through the send transition. After
|
|
318
|
-
// dispatch, caller cancellation must not prevent an answer's commit.
|
|
319
|
-
waiting.close();
|
|
320
|
-
// Start the deadline at dispatch, including the response body read.
|
|
321
|
-
timer = setTimeout(() => {
|
|
322
|
-
deadline.abort(new DOMException("OAuth refresh timed out", "TimeoutError"));
|
|
323
|
-
}, REFRESH_REQUEST_DEADLINE_MS);
|
|
324
|
-
return baseFetch(input, { ...init, redirect: "manual", signal: deadline.signal });
|
|
325
|
-
}, waiting)
|
|
326
|
-
.then((response) => {
|
|
327
|
-
if (deadline.signal.aborted) {
|
|
328
|
-
void response.body?.cancel().catch(() => { });
|
|
329
|
-
throw aborted(deadline.signal);
|
|
330
|
-
}
|
|
331
|
-
return refreshResponseOutcome(response, deadline.signal);
|
|
332
|
-
});
|
|
333
|
-
// The deadline can settle the race before a late fetch/body rejects.
|
|
334
|
-
void read.catch(() => { });
|
|
335
|
-
outcome = await runEdge(Effect.raceAllFirst([
|
|
336
|
-
Effect.tryPromise({ try: () => read, catch: (error) => error }),
|
|
337
|
-
fromSignal(deadline.signal),
|
|
338
|
-
]));
|
|
339
|
-
if (deadline.signal.aborted)
|
|
340
|
-
throw aborted(deadline.signal);
|
|
341
|
-
}
|
|
342
|
-
catch (error) {
|
|
343
|
-
if (!dispatched) {
|
|
344
|
-
// A cancelled or timed-out unsent holder leaves a safely expiring
|
|
345
|
-
// claim. Cleanup must not retain its local flight or partition pin.
|
|
346
|
-
if (!waiting.signal.aborted && !signal?.aborted && !init?.signal?.aborted) {
|
|
347
|
-
await waiting.run(() => releaseLease()).catch(() => { });
|
|
348
|
-
if (provider.refreshVerdict()?.kind === "dead") {
|
|
349
|
-
await waiting.run(() => provider.discardRefusedGrant(requested, epoch)).catch(() => { });
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
const failure = !signal?.aborted && waiting.signal.aborted ? provider.refreshContended() : error;
|
|
353
|
-
fail(failure, provider.refreshVerdict());
|
|
354
|
-
throw failure;
|
|
355
|
-
}
|
|
356
|
-
// A sent request without a definitive answer may have rotated the token.
|
|
357
|
-
// Persist the refusal before dropping tokens, so interrupted cleanup or a
|
|
358
|
-
// later write of the same fingerprint cannot make it redeemable again.
|
|
359
|
-
const failure = new UnauthorizedError("OAuth refresh outcome is unknown; authorization required.");
|
|
360
|
-
await releaseLease({ kind: "dead" });
|
|
361
|
-
await provider.discardRefusedGrant(requested, epoch);
|
|
362
|
-
fail(failure, { kind: "dead" });
|
|
363
|
-
throw failure;
|
|
364
|
-
}
|
|
365
|
-
finally {
|
|
366
|
-
if (timer !== undefined)
|
|
367
|
-
clearTimeout(timer);
|
|
368
|
-
}
|
|
369
|
-
// An answer exists from here on, and is committed whatever the owner does.
|
|
370
|
-
if (outcome.failure) {
|
|
371
|
-
// Drop a refused grant while the flight still stands: a caller arriving
|
|
372
|
-
// meanwhile joins it instead of redeeming the dead token again.
|
|
373
|
-
await provider.discardRefusedGrant(requested, epoch);
|
|
374
|
-
await releaseLease(outcome.verdict, "resolved");
|
|
375
|
-
fail(outcome.failure, outcome.verdict);
|
|
376
|
-
return outcome.forSdk;
|
|
377
|
-
}
|
|
378
|
-
// The server consumed the old refresh token; the SDK merges it back the
|
|
379
|
-
// same way when a rotation omits one.
|
|
380
|
-
const accepted = {
|
|
381
|
-
...outcome.tokens,
|
|
382
|
-
...(outcome.tokens.refresh_token === undefined && requested !== null ? { refresh_token: requested } : {}),
|
|
383
|
-
};
|
|
384
|
-
let committed;
|
|
385
|
-
try {
|
|
386
|
-
committed = await provider.storeRefresh(epoch, requested, accepted, issuer);
|
|
387
|
-
}
|
|
388
|
-
catch {
|
|
389
|
-
const failure = new UnauthorizedError("OAuth refresh could not be committed; authorization required.");
|
|
390
|
-
await releaseLease({ kind: "dead" }, "resolved");
|
|
391
|
-
await provider.discardRefusedGrant(requested, epoch);
|
|
392
|
-
fail(failure, { kind: "dead" });
|
|
393
|
-
throw failure;
|
|
394
|
-
}
|
|
395
|
-
if (!committed) {
|
|
396
|
-
// Restart, Disconnect, or another consent decides the newer grant.
|
|
397
|
-
await releaseLease({ kind: "dead" }, "resolved");
|
|
398
|
-
const failure = new UnauthorizedError("OAuth refresh grant is no longer active.");
|
|
399
|
-
fail(failure, { kind: "dead" });
|
|
400
|
-
throw failure;
|
|
401
|
-
}
|
|
402
|
-
await provider.releaseRefresh(lease, undefined, committed, "resolved");
|
|
403
|
-
settle({ status: "refreshed", tokens: committed });
|
|
404
|
-
return provider.adoptRefresh(committed);
|
|
317
|
+
refresh(provider, input, init, baseFetch, signal, defer) {
|
|
318
|
+
return runEdge(authOwnMethod(OAuthRefreshCoordinator.prototype, this, "refresh", [
|
|
319
|
+
provider,
|
|
320
|
+
input,
|
|
321
|
+
init,
|
|
322
|
+
baseFetch,
|
|
323
|
+
signal,
|
|
324
|
+
defer,
|
|
325
|
+
]));
|
|
326
|
+
}
|
|
327
|
+
redeem(provider, epoch, requested, input, init, baseFetch, waiting) {
|
|
328
|
+
return runEdge(authOwnMethod(OAuthRefreshCoordinator.prototype, this, "redeem", [
|
|
329
|
+
provider,
|
|
330
|
+
epoch,
|
|
331
|
+
requested,
|
|
332
|
+
input,
|
|
333
|
+
init,
|
|
334
|
+
baseFetch,
|
|
335
|
+
waiting,
|
|
336
|
+
]));
|
|
405
337
|
}
|
|
406
338
|
/** A reset fenced `epoch`: wake its joiners now rather than at the answer. */
|
|
407
339
|
retire(epoch) {
|
|
@@ -411,6 +343,235 @@ export class OAuthRefreshCoordinator {
|
|
|
411
343
|
this.flights.delete(epoch);
|
|
412
344
|
Deferred.doneUnsafe(flight.outcome, Effect.succeed({ status: "retired" }));
|
|
413
345
|
}
|
|
346
|
+
static {
|
|
347
|
+
registerAuthMethod(OAuthRefreshCoordinator.prototype, "refresh", function (provider, input, init, baseFetch, signal, defer) {
|
|
348
|
+
return authFlow(function* () {
|
|
349
|
+
if (signal?.aborted)
|
|
350
|
+
throw aborted(signal);
|
|
351
|
+
const waiting = refreshWait(signal);
|
|
352
|
+
let redeeming = false;
|
|
353
|
+
try {
|
|
354
|
+
// A verdict belongs to one refresh; never let an older one decide this.
|
|
355
|
+
provider.recordRefreshFailure(undefined);
|
|
356
|
+
const epoch = (yield authInvoke(waiting, "run", [
|
|
357
|
+
authMethod(() => {
|
|
358
|
+
return authInvoke(provider, "flowEpoch", []);
|
|
359
|
+
}),
|
|
360
|
+
]));
|
|
361
|
+
const requested = requestedRefreshToken(init);
|
|
362
|
+
for (let attempt = 0; attempt < 8; attempt++) {
|
|
363
|
+
if (signal?.aborted)
|
|
364
|
+
throw aborted(signal);
|
|
365
|
+
const joined = this.flights.get(epoch);
|
|
366
|
+
if (joined) {
|
|
367
|
+
const outcome = (yield authCall(waitForRefreshFlight, [
|
|
368
|
+
joined,
|
|
369
|
+
signal,
|
|
370
|
+
waiting.signal,
|
|
371
|
+
]));
|
|
372
|
+
if (signal?.aborted)
|
|
373
|
+
throw aborted(signal);
|
|
374
|
+
if (outcome.status === "failed") {
|
|
375
|
+
provider.recordRefreshFailure(outcome.verdict);
|
|
376
|
+
throw outcome.error;
|
|
377
|
+
}
|
|
378
|
+
if (outcome.status === "refreshed")
|
|
379
|
+
return provider.adoptRefresh(outcome.tokens);
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
// Preparation reads share the same bound as later claim waits.
|
|
383
|
+
const current = (yield authInvoke(waiting, "run", [
|
|
384
|
+
authMethod(() => {
|
|
385
|
+
return authInvoke(provider, "storedTokens", [waiting.signal]);
|
|
386
|
+
}),
|
|
387
|
+
]));
|
|
388
|
+
if (this.flights.has(epoch))
|
|
389
|
+
continue;
|
|
390
|
+
if (current?.refresh_token &&
|
|
391
|
+
(current.refresh_token !== requested || provider.refreshedSinceRead(current, epoch))) {
|
|
392
|
+
return provider.adoptRefresh(current);
|
|
393
|
+
}
|
|
394
|
+
if (current?.refresh_token !== requested) {
|
|
395
|
+
provider.recordRefreshFailure({ kind: "dead" });
|
|
396
|
+
return Response.json({ error: "invalid_grant", error_description: "Refresh token is no longer active." }, { status: 400 });
|
|
397
|
+
}
|
|
398
|
+
redeeming = true;
|
|
399
|
+
// Claim submission transfers renewal, including preparation, token
|
|
400
|
+
// request and commit, to a bounded native coordinator. Only
|
|
401
|
+
// this caller's wait races its signal; the coordinator receives
|
|
402
|
+
// no request/call Context authority or caller-owned deadline.
|
|
403
|
+
const redemption = authDetached(authInvoke(this, "redeem", [
|
|
404
|
+
provider,
|
|
405
|
+
epoch,
|
|
406
|
+
requested,
|
|
407
|
+
input,
|
|
408
|
+
init,
|
|
409
|
+
baseFetch,
|
|
410
|
+
waiting,
|
|
411
|
+
]).pipe(Effect.ensuring(Effect.sync(() => waiting.close()))), defer);
|
|
412
|
+
return (yield signal ? Effect.raceFirst(redemption, fromSignal(signal)) : redemption);
|
|
413
|
+
}
|
|
414
|
+
throw provider.refreshContended();
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
if (signal?.aborted)
|
|
418
|
+
throw aborted(signal);
|
|
419
|
+
if (waiting.signal.aborted && !waiting.requestStarted)
|
|
420
|
+
throw provider.refreshContended();
|
|
421
|
+
throw error;
|
|
422
|
+
}
|
|
423
|
+
finally {
|
|
424
|
+
if (!redeeming)
|
|
425
|
+
waiting.close();
|
|
426
|
+
}
|
|
427
|
+
}.bind(this));
|
|
428
|
+
}, OAuthRefreshCoordinator.prototype.refresh);
|
|
429
|
+
registerAuthMethod(OAuthRefreshCoordinator.prototype, "redeem", function (provider, epoch, requested, input, init, baseFetch, waiting) {
|
|
430
|
+
return authFlow(function* () {
|
|
431
|
+
const flight = { outcome: Deferred.makeUnsafe() };
|
|
432
|
+
this.flights.set(epoch, flight);
|
|
433
|
+
// Pin the owner partition until the flight settles, whoever is waiting.
|
|
434
|
+
const release = this.retainPartition();
|
|
435
|
+
const settle = (outcome) => {
|
|
436
|
+
if (this.flights.get(epoch) === flight)
|
|
437
|
+
this.flights.delete(epoch);
|
|
438
|
+
release();
|
|
439
|
+
// Last, because joined fibers resume inside this call.
|
|
440
|
+
Deferred.doneUnsafe(flight.outcome, Effect.succeed(outcome));
|
|
441
|
+
};
|
|
442
|
+
const fail = (error, verdict) => {
|
|
443
|
+
if (verdict)
|
|
444
|
+
provider.recordRefreshFailure(verdict);
|
|
445
|
+
settle({ status: "failed", error, ...(verdict ? { verdict } : {}) });
|
|
446
|
+
};
|
|
447
|
+
// The authorization server this redemption answers to, fixed now.
|
|
448
|
+
const issuer = provider.refreshIssuer(epoch);
|
|
449
|
+
let lease;
|
|
450
|
+
try {
|
|
451
|
+
const claim = (yield authInvoke(provider, "claimRefresh", [epoch, requested, undefined, waiting]));
|
|
452
|
+
if (claim instanceof Response) {
|
|
453
|
+
const tokens = (yield authInvoke(waiting, "run", [
|
|
454
|
+
authMethod(() => {
|
|
455
|
+
return authInvoke(provider, "storedTokens", [waiting.signal]);
|
|
456
|
+
}),
|
|
457
|
+
]));
|
|
458
|
+
if (tokens)
|
|
459
|
+
settle({ status: "refreshed", tokens });
|
|
460
|
+
else
|
|
461
|
+
settle({ status: "retired" });
|
|
462
|
+
return claim;
|
|
463
|
+
}
|
|
464
|
+
lease = claim;
|
|
465
|
+
}
|
|
466
|
+
catch (error) {
|
|
467
|
+
fail(error, provider.refreshVerdict());
|
|
468
|
+
throw error;
|
|
469
|
+
}
|
|
470
|
+
const releaseLease = authMethod((verdict, resolution = "ambiguous") => {
|
|
471
|
+
return authFlow(function* () {
|
|
472
|
+
(yield authInvoke(provider, "releaseRefresh", [lease, verdict, undefined, resolution]));
|
|
473
|
+
}.bind(this));
|
|
474
|
+
});
|
|
475
|
+
let outcome;
|
|
476
|
+
const deadline = waiting.signal;
|
|
477
|
+
let dispatched = false;
|
|
478
|
+
try {
|
|
479
|
+
const read = authFlow(function* () {
|
|
480
|
+
const response = (yield authInvoke(provider, "dispatchRefresh", [
|
|
481
|
+
lease,
|
|
482
|
+
authMethod(() => authFlow(function* () {
|
|
483
|
+
waiting.beginRequest();
|
|
484
|
+
dispatched = true;
|
|
485
|
+
return (yield authCall(baseFetch, [
|
|
486
|
+
input,
|
|
487
|
+
{ ...init, redirect: "manual", signal: deadline },
|
|
488
|
+
]));
|
|
489
|
+
}.bind(this))),
|
|
490
|
+
waiting,
|
|
491
|
+
]));
|
|
492
|
+
if (deadline.aborted) {
|
|
493
|
+
void response.body?.cancel().catch(() => { });
|
|
494
|
+
throw aborted(deadline);
|
|
495
|
+
}
|
|
496
|
+
return (yield authCall(refreshResponseOutcome, [response, deadline]));
|
|
497
|
+
}.bind(this));
|
|
498
|
+
outcome = (yield Effect.raceFirst(read, fromSignal(deadline)));
|
|
499
|
+
if (deadline.aborted)
|
|
500
|
+
throw aborted(deadline);
|
|
501
|
+
}
|
|
502
|
+
catch (error) {
|
|
503
|
+
if (!dispatched) {
|
|
504
|
+
// Only the coordinator's own preparation deadline can interrupt a
|
|
505
|
+
// submitted claim. Caller cancellation no longer reaches this work.
|
|
506
|
+
if (!waiting.signal.aborted) {
|
|
507
|
+
(yield authCatch(authInvoke(waiting, "run", [
|
|
508
|
+
authMethod(() => {
|
|
509
|
+
return authCall(releaseLease, []);
|
|
510
|
+
}),
|
|
511
|
+
]), () => { }));
|
|
512
|
+
if (provider.refreshVerdict()?.kind === "dead") {
|
|
513
|
+
(yield authCatch(authInvoke(waiting, "run", [
|
|
514
|
+
authMethod(() => {
|
|
515
|
+
return authInvoke(provider, "discardRefusedGrant", [requested, epoch]);
|
|
516
|
+
}),
|
|
517
|
+
]), () => { }));
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
const failure = waiting.signal.aborted ? provider.refreshContended() : error;
|
|
521
|
+
fail(failure, provider.refreshVerdict());
|
|
522
|
+
throw failure;
|
|
523
|
+
}
|
|
524
|
+
// A sent request without a definitive answer may have rotated the token.
|
|
525
|
+
// Persist the refusal before dropping tokens, so interrupted cleanup or a
|
|
526
|
+
// later write of the same fingerprint cannot make it redeemable again.
|
|
527
|
+
const failure = new UnauthorizedError("OAuth refresh outcome is unknown; authorization required.");
|
|
528
|
+
(yield authCall(releaseLease, [{ kind: "dead" }]));
|
|
529
|
+
(yield authInvoke(provider, "discardRefusedGrant", [requested, epoch]));
|
|
530
|
+
fail(failure, { kind: "dead" });
|
|
531
|
+
throw failure;
|
|
532
|
+
}
|
|
533
|
+
finally {
|
|
534
|
+
waiting.close();
|
|
535
|
+
}
|
|
536
|
+
// An answer exists from here on, and is committed whatever the owner does.
|
|
537
|
+
if (outcome.failure) {
|
|
538
|
+
// Drop a refused grant while the flight still stands: a caller arriving
|
|
539
|
+
// meanwhile joins it instead of redeeming the dead token again.
|
|
540
|
+
(yield authInvoke(provider, "discardRefusedGrant", [requested, epoch]));
|
|
541
|
+
(yield authCall(releaseLease, [outcome.verdict, "resolved"]));
|
|
542
|
+
fail(outcome.failure, outcome.verdict);
|
|
543
|
+
return outcome.forSdk;
|
|
544
|
+
}
|
|
545
|
+
// The server consumed the old refresh token; the SDK merges it back the
|
|
546
|
+
// same way when a rotation omits one.
|
|
547
|
+
const accepted = {
|
|
548
|
+
...outcome.tokens,
|
|
549
|
+
...(outcome.tokens.refresh_token === undefined && requested !== null ? { refresh_token: requested } : {}),
|
|
550
|
+
};
|
|
551
|
+
let committed;
|
|
552
|
+
try {
|
|
553
|
+
committed = (yield authInvoke(provider, "storeRefresh", [epoch, requested, accepted, issuer]));
|
|
554
|
+
}
|
|
555
|
+
catch {
|
|
556
|
+
const failure = new UnauthorizedError("OAuth refresh could not be committed; authorization required.");
|
|
557
|
+
(yield authCall(releaseLease, [{ kind: "dead" }, "resolved"]));
|
|
558
|
+
(yield authInvoke(provider, "discardRefusedGrant", [requested, epoch]));
|
|
559
|
+
fail(failure, { kind: "dead" });
|
|
560
|
+
throw failure;
|
|
561
|
+
}
|
|
562
|
+
if (!committed) {
|
|
563
|
+
// Restart, Disconnect, or another consent decides the newer grant.
|
|
564
|
+
(yield authCall(releaseLease, [{ kind: "dead" }, "resolved"]));
|
|
565
|
+
const failure = new UnauthorizedError("OAuth refresh grant is no longer active.");
|
|
566
|
+
fail(failure, { kind: "dead" });
|
|
567
|
+
throw failure;
|
|
568
|
+
}
|
|
569
|
+
(yield authInvoke(provider, "releaseRefresh", [lease, undefined, committed, "resolved"]));
|
|
570
|
+
settle({ status: "refreshed", tokens: committed });
|
|
571
|
+
return provider.adoptRefresh(committed);
|
|
572
|
+
}.bind(this));
|
|
573
|
+
}, OAuthRefreshCoordinator.prototype.redeem);
|
|
574
|
+
}
|
|
414
575
|
}
|
|
415
576
|
/**
|
|
416
577
|
* One refresh coordinator per owner partition, for one connector runtime.
|
|
@@ -452,18 +613,23 @@ const MAX_GRANT_WRITES = 32;
|
|
|
452
613
|
* flow record's fifteen minutes.
|
|
453
614
|
*/
|
|
454
615
|
const PENDING_AUTHORIZATION_MAX_AGE_MS = 10 * 60 * 1000;
|
|
455
|
-
|
|
456
|
-
return state
|
|
457
|
-
? {
|
|
458
|
-
issuer: discoveryIssuer(state),
|
|
459
|
-
tokenEndpoint: state.authorizationServerMetadata?.token_endpoint,
|
|
460
|
-
...(state.authorizationServerMetadata?.authorization_response_iss_parameter_supported === true
|
|
461
|
-
? { issRequired: true }
|
|
462
|
-
: {}),
|
|
463
|
-
discovery: await oauthStateDigest(JSON.stringify(state)),
|
|
464
|
-
}
|
|
465
|
-
: {};
|
|
616
|
+
function discoveryBinding(state) {
|
|
617
|
+
return runEdge(authProgram(discoveryBindingNative, [state]));
|
|
466
618
|
}
|
|
619
|
+
const discoveryBindingNative = /* @__PURE__ */ registerAuth(discoveryBinding, function (state) {
|
|
620
|
+
return authFlow(function* () {
|
|
621
|
+
return state
|
|
622
|
+
? {
|
|
623
|
+
issuer: discoveryIssuer(state),
|
|
624
|
+
tokenEndpoint: state.authorizationServerMetadata?.token_endpoint,
|
|
625
|
+
...(state.authorizationServerMetadata?.authorization_response_iss_parameter_supported === true
|
|
626
|
+
? { issRequired: true }
|
|
627
|
+
: {}),
|
|
628
|
+
discovery: (yield authCall(oauthStateDigest, [JSON.stringify(state)])),
|
|
629
|
+
}
|
|
630
|
+
: {};
|
|
631
|
+
});
|
|
632
|
+
});
|
|
467
633
|
function plainObject(value) {
|
|
468
634
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
469
635
|
}
|
|
@@ -589,7 +755,7 @@ export class KvOAuthProvider {
|
|
|
589
755
|
constructor(connectorId, storage, redirectUri, refreshCoordinator, allowAuthorization = true,
|
|
590
756
|
/** Present when the deployment's vault can seal; the grant is then ciphertext. */
|
|
591
757
|
sealer,
|
|
592
|
-
/**
|
|
758
|
+
/** SDK hook ownership; renewal ignores it after claim submission. */
|
|
593
759
|
signal,
|
|
594
760
|
/** Stable connector configuration bound to dynamically registered clients. */
|
|
595
761
|
clientBinding,
|
|
@@ -624,129 +790,83 @@ export class KvOAuthProvider {
|
|
|
624
790
|
const client = this.clientOptions?.client;
|
|
625
791
|
return downstreamClientMetadata(this.redirectUri, this.scope, this.clientOptions?.name, client ? remoteClientAuthMethod(client) : "none");
|
|
626
792
|
}
|
|
793
|
+
/** A preparation await owns no continuation after its call/request closes.
|
|
794
|
+
* null explicitly selects coordinator/reset ownership, never the caller. */
|
|
795
|
+
wait(work, signal = this.signal) {
|
|
796
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "wait", [work, signal]));
|
|
797
|
+
}
|
|
798
|
+
/** Legacy helpers must fence their inner reads/deletes too, not just our join. */
|
|
799
|
+
preparationStorage(signal) {
|
|
800
|
+
if (!signal)
|
|
801
|
+
return this.storage;
|
|
802
|
+
const storage = this.storage;
|
|
803
|
+
return {
|
|
804
|
+
...storage,
|
|
805
|
+
get: authMethod((key) => {
|
|
806
|
+
return authInvoke(this, "wait", [
|
|
807
|
+
authMethod(() => {
|
|
808
|
+
return authStorage(storage, "get", [key]);
|
|
809
|
+
}),
|
|
810
|
+
signal,
|
|
811
|
+
]);
|
|
812
|
+
}),
|
|
813
|
+
list: authMethod((prefix) => {
|
|
814
|
+
return authInvoke(this, "wait", [
|
|
815
|
+
authMethod(() => {
|
|
816
|
+
return authStorage(storage, "list", [prefix]);
|
|
817
|
+
}),
|
|
818
|
+
signal,
|
|
819
|
+
]);
|
|
820
|
+
}),
|
|
821
|
+
delete: authMethod((key) => {
|
|
822
|
+
return authInvoke(this, "wait", [
|
|
823
|
+
authMethod(() => {
|
|
824
|
+
return authStorage(storage, "delete", [key]);
|
|
825
|
+
}),
|
|
826
|
+
signal,
|
|
827
|
+
]);
|
|
828
|
+
}),
|
|
829
|
+
};
|
|
830
|
+
}
|
|
627
831
|
// --- the grant record ---------------------------------------------------
|
|
628
832
|
/** The grant's additional authenticated data: record and epoch. */
|
|
629
833
|
grantLabel(epoch) {
|
|
630
834
|
return `${GRANT}:${epoch}`;
|
|
631
835
|
}
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
const stored = {
|
|
635
|
-
connectaOAuth: GRANT_VERSION,
|
|
636
|
-
epoch: grant.epoch,
|
|
637
|
-
...(grant.cleanupPending ? { cleanupPending: true } : {}),
|
|
638
|
-
...(grant.flow !== undefined ? { flow: grant.flow } : {}),
|
|
639
|
-
};
|
|
640
|
-
if (Object.keys(grant.body).length > 0) {
|
|
641
|
-
if (this.sealer) {
|
|
642
|
-
stored.sealed = await this.sealer.seal(this.grantLabel(grant.epoch), JSON.stringify(grant.body));
|
|
643
|
-
}
|
|
644
|
-
else {
|
|
645
|
-
stored.body = grant.body;
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
return JSON.stringify(stored);
|
|
836
|
+
encodeGrant(grant, signal = this.signal) {
|
|
837
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "encodeGrant", [grant, signal]));
|
|
649
838
|
}
|
|
650
839
|
/**
|
|
651
840
|
* Open a stored grant. A body that does not open — tampered, under a
|
|
652
841
|
* rotated key, moved from another connector, owner, or epoch, or sealed
|
|
653
842
|
* with no sealer configured — reads as empty: authorization again.
|
|
654
843
|
*/
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
return { epoch: INITIAL_EPOCH, body: {} };
|
|
658
|
-
const stored = storedGrant(raw);
|
|
659
|
-
if (!stored)
|
|
660
|
-
return { epoch: UNREADABLE_EPOCH, body: {} };
|
|
661
|
-
let body = stored.body;
|
|
662
|
-
if (stored.sealed !== undefined) {
|
|
663
|
-
body = undefined;
|
|
664
|
-
try {
|
|
665
|
-
if (this.sealer)
|
|
666
|
-
body = parsed(await this.sealer.open(this.grantLabel(stored.epoch), stored.sealed));
|
|
667
|
-
}
|
|
668
|
-
catch {
|
|
669
|
-
this.sealer?.warn(`[connecta] connector "${this.connectorId}" has a sealed OAuth grant ` +
|
|
670
|
-
"the configured vault cannot open; treating it as absent, so the " +
|
|
671
|
-
"connector needs authorization again. A changed vault key or " +
|
|
672
|
-
"tampered storage causes this.");
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
return {
|
|
676
|
-
epoch: stored.epoch,
|
|
677
|
-
...(stored.cleanupPending ? { cleanupPending: true } : {}),
|
|
678
|
-
...(stored.flow !== undefined ? { flow: stored.flow } : {}),
|
|
679
|
-
body: grantBody(body),
|
|
680
|
-
};
|
|
844
|
+
openGrant(raw, signal = this.signal) {
|
|
845
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "openGrant", [raw, signal]));
|
|
681
846
|
}
|
|
682
847
|
/**
|
|
683
848
|
* The grant as stored now, migrating layout 2 on first sight. A plaintext
|
|
684
849
|
* body, written before the vault could seal, is sealed where it lies by
|
|
685
850
|
* compare-and-set; a failure leaves it readable for the next write to seal.
|
|
686
851
|
*/
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
if (raw === null)
|
|
690
|
-
raw = await this.migrate();
|
|
691
|
-
if (raw !== null && storedGrant(raw)?.cleanupPending)
|
|
692
|
-
raw = await this.cleanupV2(raw);
|
|
693
|
-
const grant = await this.openGrant(raw);
|
|
694
|
-
if (raw !== null && this.sealer && storedGrant(raw)?.body !== undefined && !this.signal?.aborted) {
|
|
695
|
-
const sealed = await this.encodeGrant(grant);
|
|
696
|
-
if (await this.storage.compareAndSet(GRANT, raw, sealed).catch(() => false))
|
|
697
|
-
raw = sealed;
|
|
698
|
-
}
|
|
699
|
-
return { raw, grant };
|
|
852
|
+
readGrant(signal = this.signal) {
|
|
853
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "readGrant", [signal]));
|
|
700
854
|
}
|
|
701
|
-
/**
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
*/
|
|
705
|
-
async migrate() {
|
|
706
|
-
const found = await readV2Grant(this.storage, this.sealer);
|
|
707
|
-
// A request that has already left writes nothing, this included.
|
|
708
|
-
if (!found || this.signal?.aborted)
|
|
709
|
-
return null;
|
|
710
|
-
const prefix = found.disconnected ? DISCONNECTED_EPOCH_PREFIX : ACTIVE_EPOCH_PREFIX;
|
|
711
|
-
const encoded = await this.encodeGrant({
|
|
712
|
-
epoch: `${prefix}${crypto.randomUUID()}`,
|
|
713
|
-
body: found.body,
|
|
714
|
-
cleanupPending: true,
|
|
715
|
-
});
|
|
716
|
-
let written;
|
|
717
|
-
try {
|
|
718
|
-
written = await this.storage.compareAndSet(GRANT, null, encoded);
|
|
719
|
-
}
|
|
720
|
-
catch {
|
|
721
|
-
// A store's own error can quote the value it refused.
|
|
722
|
-
throw this.credentialWriteError();
|
|
723
|
-
}
|
|
724
|
-
const raw = written ? encoded : await this.storage.get(GRANT);
|
|
725
|
-
return raw;
|
|
855
|
+
/** Layout 2 migration and its cleanup retain the reading operation's owner. */
|
|
856
|
+
migrate(signal) {
|
|
857
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "migrate", [signal]));
|
|
726
858
|
}
|
|
727
859
|
/** Deletion is retried on every read, including reads after a reset. */
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
if (!(await deleteV2Keys(this.storage)))
|
|
731
|
-
return raw;
|
|
732
|
-
const grant = await this.openGrant(raw);
|
|
733
|
-
delete grant.cleanupPending;
|
|
734
|
-
const cleared = await this.encodeGrant(grant);
|
|
735
|
-
if (await this.storage.compareAndSet(GRANT, raw, cleared))
|
|
736
|
-
return cleared;
|
|
737
|
-
return this.storage.get(GRANT);
|
|
738
|
-
}
|
|
739
|
-
catch {
|
|
740
|
-
// The durable marker survives both deletion and marker-clear failures.
|
|
741
|
-
return raw;
|
|
742
|
-
}
|
|
860
|
+
cleanupV2(raw, signal = this.signal) {
|
|
861
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "cleanupV2", [raw, signal]));
|
|
743
862
|
}
|
|
744
863
|
/** The grant a bound flow reads: a replaced epoch fails it. */
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
864
|
+
boundGrant(signal = this.signal) {
|
|
865
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "boundGrant", [signal]));
|
|
866
|
+
}
|
|
867
|
+
/** SDK preparation is call/request-owned; coordinator reads explicitly opt out. */
|
|
868
|
+
activeGrant() {
|
|
869
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "activeGrant", []));
|
|
750
870
|
}
|
|
751
871
|
/**
|
|
752
872
|
* The one way a grant changes: read, change, compare-and-set, again while
|
|
@@ -756,58 +876,12 @@ export class KvOAuthProvider {
|
|
|
756
876
|
* writes after the request's abort too. `change` returning undefined
|
|
757
877
|
* leaves the grant as it is.
|
|
758
878
|
*/
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
if (!options.commit && this.signal?.aborted)
|
|
762
|
-
return false;
|
|
763
|
-
const { raw, grant } = await this.readGrant();
|
|
764
|
-
const epoch = options.epoch ?? (this.epoch ??= grant.epoch);
|
|
765
|
-
if (grant.epoch !== epoch) {
|
|
766
|
-
if (options.quiet || !this.bound)
|
|
767
|
-
return false;
|
|
768
|
-
throw this.flowSuperseded();
|
|
769
|
-
}
|
|
770
|
-
// A disconnect's epoch holds nothing until an explicit start replaces it.
|
|
771
|
-
if (isDisconnectedEpoch(epoch))
|
|
772
|
-
return false;
|
|
773
|
-
const next = change(grant);
|
|
774
|
-
if (next === undefined)
|
|
775
|
-
return true;
|
|
776
|
-
const encoded = await this.encodeGrant(next);
|
|
777
|
-
if (!options.commit && this.signal?.aborted)
|
|
778
|
-
return false;
|
|
779
|
-
let written;
|
|
780
|
-
try {
|
|
781
|
-
written = await this.storage.compareAndSet(GRANT, raw, encoded);
|
|
782
|
-
}
|
|
783
|
-
catch {
|
|
784
|
-
// A store's own error can quote the value it refused.
|
|
785
|
-
throw this.credentialWriteError();
|
|
786
|
-
}
|
|
787
|
-
if (written) {
|
|
788
|
-
if (next.epoch !== grant.epoch)
|
|
789
|
-
this.epoch = next.epoch;
|
|
790
|
-
return true;
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
throw this.credentialWriteError();
|
|
879
|
+
updateGrant(change, options = {}) {
|
|
880
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "updateGrant", [change, options]));
|
|
794
881
|
}
|
|
795
882
|
/** Store a client or tokens for `issuer`'s grant, replacing another server's. */
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
const body = issuer !== undefined && grant.body.issuer !== issuer ? { issuer } : { ...grant.body };
|
|
799
|
-
if (field === "client")
|
|
800
|
-
body.client = value;
|
|
801
|
-
else {
|
|
802
|
-
body.tokens = value;
|
|
803
|
-
delete body.refreshConsent;
|
|
804
|
-
if (refreshConsent)
|
|
805
|
-
body.refreshConsent = refreshConsent;
|
|
806
|
-
}
|
|
807
|
-
return this.withIssuer(grant, body);
|
|
808
|
-
});
|
|
809
|
-
// A flow may invalidate what it wrote itself.
|
|
810
|
-
this.seen[field] = fingerprint(field === "client" ? value.value : value);
|
|
883
|
+
writeCredential(field, value, issuer, refreshConsent) {
|
|
884
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "writeCredential", [field, value, issuer, refreshConsent]));
|
|
811
885
|
}
|
|
812
886
|
withIssuer(grant, body) {
|
|
813
887
|
if (grant.body.issuer !== undefined && grant.body.issuer !== body.issuer) {
|
|
@@ -825,21 +899,11 @@ export class KvOAuthProvider {
|
|
|
825
899
|
configuredAuthorizationBinding() {
|
|
826
900
|
return undefined;
|
|
827
901
|
}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
return configured
|
|
831
|
-
? { ...configured, issuer: grant.body.issuer }
|
|
832
|
-
: {
|
|
833
|
-
...(await discoveryBinding(grant.body.discovery)),
|
|
834
|
-
issuer: grant.body.issuer,
|
|
835
|
-
clientId: grant.body.client?.value.client_id,
|
|
836
|
-
};
|
|
902
|
+
bindingForGrant(grant) {
|
|
903
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "bindingForGrant", [grant]));
|
|
837
904
|
}
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
if (!binding || fingerprint(binding) !== fingerprint(await this.bindingForGrant(grant))) {
|
|
841
|
-
throw this.flowSuperseded();
|
|
842
|
-
}
|
|
905
|
+
checkFlowBinding(grant) {
|
|
906
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "checkFlowBinding", [grant]));
|
|
843
907
|
}
|
|
844
908
|
/** Validate RFC 9207 against the server this consent selected. */
|
|
845
909
|
validateCallbackIssuer(issuer) {
|
|
@@ -854,73 +918,25 @@ export class KvOAuthProvider {
|
|
|
854
918
|
flowLabel(key, epoch) {
|
|
855
919
|
return `${key}:${epoch}`;
|
|
856
920
|
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
const stored = { connectaOAuthFlow: FLOW_VERSION, ...rest };
|
|
860
|
-
if (verifier !== undefined) {
|
|
861
|
-
if (this.sealer) {
|
|
862
|
-
stored.verifier = await this.sealer.seal(this.flowLabel(key, flow.epoch), verifier);
|
|
863
|
-
stored.sealed = true;
|
|
864
|
-
}
|
|
865
|
-
else {
|
|
866
|
-
stored.verifier = verifier;
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
return JSON.stringify(stored);
|
|
921
|
+
encodeFlow(key, flow) {
|
|
922
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "encodeFlow", [key, flow]));
|
|
870
923
|
}
|
|
871
924
|
/** A flow record, its verifier opened only when `withVerifier`. */
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
if (!plainObject(value) ||
|
|
875
|
-
value.connectaOAuthFlow !== FLOW_VERSION ||
|
|
876
|
-
typeof value.epoch !== "string" ||
|
|
877
|
-
typeof value.at !== "number" ||
|
|
878
|
-
(value.url !== undefined && typeof value.url !== "string") ||
|
|
879
|
-
(value.verifier !== undefined && typeof value.verifier !== "string")) {
|
|
880
|
-
return undefined;
|
|
881
|
-
}
|
|
882
|
-
const flow = {
|
|
883
|
-
epoch: value.epoch,
|
|
884
|
-
at: value.at,
|
|
885
|
-
...(plainObject(value.binding) ? { binding: value.binding } : {}),
|
|
886
|
-
...(typeof value.url === "string" ? { url: value.url } : {}),
|
|
887
|
-
...(value.consumed === true ? { consumed: true } : {}),
|
|
888
|
-
};
|
|
889
|
-
if (!withVerifier || typeof value.verifier !== "string")
|
|
890
|
-
return flow;
|
|
891
|
-
if (value.sealed !== true)
|
|
892
|
-
return { ...flow, verifier: value.verifier };
|
|
893
|
-
try {
|
|
894
|
-
if (!this.sealer)
|
|
895
|
-
return undefined;
|
|
896
|
-
return { ...flow, verifier: await this.sealer.open(this.flowLabel(key, flow.epoch), value.verifier) };
|
|
897
|
-
}
|
|
898
|
-
catch {
|
|
899
|
-
this.sealer?.warn(`[connecta] connector "${this.connectorId}" has a sealed OAuth consent ` +
|
|
900
|
-
"the configured vault cannot open; its callback is refused.");
|
|
901
|
-
return undefined;
|
|
902
|
-
}
|
|
925
|
+
openFlow(key, raw, withVerifier) {
|
|
926
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "openFlow", [key, raw, withVerifier]));
|
|
903
927
|
}
|
|
904
928
|
/** The live epoch's latest consent, until a callback claims it. */
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
if (grant.flow === undefined)
|
|
908
|
-
return undefined;
|
|
909
|
-
const key = oauthFlowKeys.flow(grant.flow);
|
|
910
|
-
const raw = await this.storage.get(key);
|
|
911
|
-
const flow = raw === null ? undefined : await this.openFlow(key, raw, false);
|
|
912
|
-
return flow && !flow.consumed && flow.epoch === grant.epoch && flow.url !== undefined
|
|
913
|
-
? { ...flow, url: flow.url }
|
|
914
|
-
: undefined;
|
|
929
|
+
latestConsent() {
|
|
930
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "latestConsent", []));
|
|
915
931
|
}
|
|
916
932
|
// --- flows and epochs -----------------------------------------------------
|
|
917
933
|
/** The live epoch; an owner with no grant yet has the initial one. */
|
|
918
|
-
|
|
919
|
-
return (
|
|
934
|
+
liveEpoch() {
|
|
935
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "liveEpoch", []));
|
|
920
936
|
}
|
|
921
937
|
/** True only after an operator disconnect, until an explicit start replaces it. */
|
|
922
|
-
|
|
923
|
-
return
|
|
938
|
+
operatorDisconnected() {
|
|
939
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "operatorDisconnected", []));
|
|
924
940
|
}
|
|
925
941
|
isOperatorDisconnectedEpoch(epoch) {
|
|
926
942
|
return isDisconnectedEpoch(epoch);
|
|
@@ -930,15 +946,12 @@ export class KvOAuthProvider {
|
|
|
930
946
|
* `api()` call or start — in the live epoch, and return it. Nothing about
|
|
931
947
|
* the grant is decided here: a grant belongs to one server by construction.
|
|
932
948
|
*/
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
this.epoch = epoch;
|
|
936
|
-
this.bound = true;
|
|
937
|
-
return epoch;
|
|
949
|
+
beginFlow() {
|
|
950
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "beginFlow", []));
|
|
938
951
|
}
|
|
939
952
|
/** The epoch this flow is bound to, or the live one. */
|
|
940
|
-
|
|
941
|
-
return
|
|
953
|
+
flowEpoch() {
|
|
954
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "flowEpoch", []));
|
|
942
955
|
}
|
|
943
956
|
/**
|
|
944
957
|
* Bind a code exchange: to the epoch its verified consent was written in, or
|
|
@@ -946,25 +959,12 @@ export class KvOAuthProvider {
|
|
|
946
959
|
* replaced fails here, before the SDK can send anything. What the grant
|
|
947
960
|
* holds now is all a refused code may invalidate.
|
|
948
961
|
*/
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
const epoch = this.callback?.flow.epoch ?? this.epoch ?? grant.epoch;
|
|
952
|
-
if (grant.epoch !== epoch)
|
|
953
|
-
throw this.flowSuperseded();
|
|
954
|
-
this.epoch = epoch;
|
|
955
|
-
this.bound = true;
|
|
956
|
-
this.codeExchange = true;
|
|
957
|
-
await this.checkFlowBinding(grant);
|
|
958
|
-
this.seen = {
|
|
959
|
-
client: fingerprint(grant.body.client?.value),
|
|
960
|
-
tokens: fingerprint(grant.body.tokens),
|
|
961
|
-
};
|
|
962
|
+
bindFlow() {
|
|
963
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "bindFlow", []));
|
|
962
964
|
}
|
|
963
965
|
/** Retain an already verified snapshot only for the exact same state. */
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
return true;
|
|
967
|
-
return this.verifyState(state);
|
|
966
|
+
verifyCallbackState(state) {
|
|
967
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "verifyCallbackState", [state]));
|
|
968
968
|
}
|
|
969
969
|
/** Whether `verifyState` matched a consent on this provider. */
|
|
970
970
|
verified() {
|
|
@@ -972,73 +972,45 @@ export class KvOAuthProvider {
|
|
|
972
972
|
}
|
|
973
973
|
/** The fixed, retryable failure of a flow a later reset superseded. */
|
|
974
974
|
flowSuperseded() {
|
|
975
|
-
return new ConnectorCallError("unavailable", `Connector "${this.connectorId}" authorization changed while this request was in flight; try again
|
|
975
|
+
return resendAdvice(new ConnectorCallError("unavailable", `Connector "${this.connectorId}" authorization changed while this request was in flight.`), "; try again");
|
|
976
976
|
}
|
|
977
977
|
/**
|
|
978
978
|
* A credential write that storage refused, in fixed text and with no cause
|
|
979
979
|
* attached. Retryable: the grant it held is untouched.
|
|
980
980
|
*/
|
|
981
981
|
credentialWriteError() {
|
|
982
|
-
return new ConnectorCallError("unavailable", `Connector "${this.connectorId}" could not store its OAuth credentials; try again shortly
|
|
982
|
+
return resendAdvice(new ConnectorCallError("unavailable", `Connector "${this.connectorId}" could not store its OAuth credentials.`), "; try again shortly");
|
|
983
983
|
}
|
|
984
984
|
/** @internal Other requests kept refreshing faster than this one could join. */
|
|
985
985
|
refreshContended() {
|
|
986
986
|
this.recordRefreshFailure({ kind: "contended" });
|
|
987
|
-
return new ConnectorCallError("unavailable", `Connector "${this.connectorId}" could not refresh its OAuth grant while other requests were; retry
|
|
987
|
+
return resendAdvice(new ConnectorCallError("unavailable", `Connector "${this.connectorId}" could not refresh its OAuth grant while other requests were.`), "; retry");
|
|
988
988
|
}
|
|
989
989
|
/**
|
|
990
990
|
* A passive call cannot start consent. A waiter deadline remains retryable;
|
|
991
991
|
* any dispatched failure requires consent because its token is spent.
|
|
992
992
|
*/
|
|
993
993
|
authorizationRefused() {
|
|
994
|
+
// The SDK catches a refresh cancellation and falls back to consent. Keep
|
|
995
|
+
// the ended caller's own reason instead of turning it into an auth verdict.
|
|
996
|
+
if (this.signal?.aborted)
|
|
997
|
+
throw aborted(this.signal);
|
|
994
998
|
const failure = this.refreshFailure;
|
|
995
999
|
if (failure?.kind === "contended")
|
|
996
1000
|
return this.refreshContended();
|
|
997
1001
|
return new UnauthorizedError("Authorization required. Use authorize_connector or Connect to start consent.");
|
|
998
1002
|
}
|
|
999
1003
|
// --- the SDK's hooks --------------------------------------------------------
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
throw this.flowSuperseded();
|
|
1011
|
-
return {
|
|
1012
|
-
client_id: client.clientId,
|
|
1013
|
-
issuer: client.issuer,
|
|
1014
|
-
token_endpoint_auth_method: this.clientMetadata.token_endpoint_auth_method,
|
|
1015
|
-
...(client.clientSecret !== undefined ? { client_secret: client.clientSecret } : {}),
|
|
1016
|
-
};
|
|
1017
|
-
}
|
|
1018
|
-
if (!body.client || (ctx && body.issuer !== ctx.issuer))
|
|
1019
|
-
return undefined;
|
|
1020
|
-
this.seen.client = fingerprint(body.client.value);
|
|
1021
|
-
return ctx ? { ...body.client.value, issuer: ctx.issuer } : body.client.value;
|
|
1022
|
-
}
|
|
1023
|
-
async saveClientInformation(info, ctx) {
|
|
1024
|
-
const { body } = await this.boundGrant();
|
|
1025
|
-
const registrationPath = this.clientOptions?.client
|
|
1026
|
-
? "static"
|
|
1027
|
-
: body.client?.value.client_id === info.client_id && body.client.registrationPath
|
|
1028
|
-
? body.client.registrationPath
|
|
1029
|
-
: this.clientMetadataUrl &&
|
|
1030
|
-
body.discovery?.authorizationServerMetadata?.client_id_metadata_document_supported === true
|
|
1031
|
-
? "cimd"
|
|
1032
|
-
: "dcr";
|
|
1033
|
-
await this.writeCredential("client", {
|
|
1034
|
-
value: this.clientOptions?.client ? { client_id: this.clientOptions.client.clientId } : info,
|
|
1035
|
-
registrationPath,
|
|
1036
|
-
...(this.clientBinding !== undefined ? { binding: this.clientBinding } : {}),
|
|
1037
|
-
}, ctx?.issuer);
|
|
1038
|
-
}
|
|
1039
|
-
async registrationPath() {
|
|
1040
|
-
const { body } = await this.boundGrant();
|
|
1041
|
-
return body.client?.registrationPath;
|
|
1004
|
+
clientInformation(ctx) {
|
|
1005
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "clientInformation", [
|
|
1006
|
+
ctx,
|
|
1007
|
+
]));
|
|
1008
|
+
}
|
|
1009
|
+
saveClientInformation(info, ctx) {
|
|
1010
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "saveClientInformation", [info, ctx]));
|
|
1011
|
+
}
|
|
1012
|
+
registrationPath() {
|
|
1013
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "registrationPath", []));
|
|
1042
1014
|
}
|
|
1043
1015
|
/** Intake and saved URLs share a guard. Include the current grant's secrets
|
|
1044
1016
|
* so a fresh request can reject an older cache before sending credentials. */
|
|
@@ -1068,9 +1040,9 @@ export class KvOAuthProvider {
|
|
|
1068
1040
|
secrets.secret(value);
|
|
1069
1041
|
}
|
|
1070
1042
|
if (body.client?.value.client_secret)
|
|
1071
|
-
secrets.
|
|
1043
|
+
secrets.identifier(body.client.value.client_id);
|
|
1072
1044
|
if (this.clientOptions?.client?.clientSecret)
|
|
1073
|
-
secrets.
|
|
1045
|
+
secrets.identifier(this.clientOptions.client.clientId);
|
|
1074
1046
|
const urls = [];
|
|
1075
1047
|
const collect = (item) => {
|
|
1076
1048
|
if (typeof item === "string") {
|
|
@@ -1086,27 +1058,16 @@ export class KvOAuthProvider {
|
|
|
1086
1058
|
collect(field);
|
|
1087
1059
|
};
|
|
1088
1060
|
collect(value);
|
|
1089
|
-
if ([secrets, this.clientOptions?.secrets].some((source) => source && (source.
|
|
1061
|
+
if ([secrets, this.clientOptions?.secrets].some((source) => source && (source.changes(value) || urls.some((url) => source.containsUrl(url))))) {
|
|
1090
1062
|
throw new ConnectorCallError("connector_call_failed", "OAuth metadata or consent URL contains a credential; refusing authorization.", { retryable: false });
|
|
1091
1063
|
}
|
|
1092
1064
|
}
|
|
1093
1065
|
/** Only metadata bound to the discovered issuer can supply operator origins. */
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
const state = body.discovery;
|
|
1097
|
-
if (!state ||
|
|
1098
|
-
state.authorizationServerMetadata?.issuer !== state.authorizationServerUrl ||
|
|
1099
|
-
body.issuer !== state.authorizationServerUrl)
|
|
1100
|
-
return undefined;
|
|
1101
|
-
this.assertCredentialFree(state, body);
|
|
1102
|
-
return new URL(state.authorizationServerUrl).origin;
|
|
1066
|
+
validatedIssuerOrigin() {
|
|
1067
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "validatedIssuerOrigin", []));
|
|
1103
1068
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
const state = body.discovery;
|
|
1107
|
-
this.assertCredentialFree(state, body);
|
|
1108
|
-
this.consentDiscovery = state;
|
|
1109
|
-
return state;
|
|
1069
|
+
discoveryState() {
|
|
1070
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "discoveryState", []));
|
|
1110
1071
|
}
|
|
1111
1072
|
/**
|
|
1112
1073
|
* Discovery names the grant's server: the same one keeps the client and
|
|
@@ -1114,74 +1075,33 @@ export class KvOAuthProvider {
|
|
|
1114
1075
|
* without a cached record — a restart's carried client, say — and what a
|
|
1115
1076
|
* different server leaves behind would never be sent to it anyway.
|
|
1116
1077
|
*/
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
const issuer = discoveryIssuer(state);
|
|
1120
|
-
if (this.clientOptions?.client && issuer !== this.clientOptions.client.issuer)
|
|
1121
|
-
throw this.flowSuperseded();
|
|
1122
|
-
if (!this.allowAuthorization && !(await this.boundGrant()).body.client)
|
|
1123
|
-
throw this.authorizationRefused();
|
|
1124
|
-
this.consentDiscovery = state;
|
|
1125
|
-
await this.updateGrant((grant) => {
|
|
1126
|
-
const body = grant.body.issuer !== issuer ? (issuer !== undefined ? { issuer } : {}) : { ...grant.body };
|
|
1127
|
-
this.assertCredentialFree(state, grant.body);
|
|
1128
|
-
body.discovery = state;
|
|
1129
|
-
return this.withIssuer(grant, body);
|
|
1130
|
-
});
|
|
1078
|
+
saveDiscoveryState(state) {
|
|
1079
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "saveDiscoveryState", [state]));
|
|
1131
1080
|
}
|
|
1132
1081
|
/**
|
|
1133
1082
|
* The grant's tokens, for the server that issued them only. An
|
|
1134
1083
|
* issuer-aware read records what it handed the SDK, so the coordinator can
|
|
1135
1084
|
* tell a refresh another request completed since.
|
|
1136
1085
|
*/
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
this.seen.tokens = fingerprint(tokens);
|
|
1143
|
-
if (!ctx)
|
|
1144
|
-
return tokens;
|
|
1145
|
-
this.refreshBasis = { accessToken: tokens.access_token, epoch: grant.epoch, issuer: ctx.issuer };
|
|
1146
|
-
return { ...tokens, issuer: ctx.issuer };
|
|
1147
|
-
}
|
|
1148
|
-
async saveTokens(tokens, ctx) {
|
|
1149
|
-
if (this.refreshFailure?.kind === "dead")
|
|
1150
|
-
throw this.authorizationRefused();
|
|
1151
|
-
// The coordinator stored this refresh already, or handed on another's.
|
|
1152
|
-
if (this.storedRefresh !== undefined && tokens.access_token === this.storedRefresh)
|
|
1153
|
-
return;
|
|
1154
|
-
const refreshConsent = this.exchangeConsent &&
|
|
1155
|
-
tokens.refresh_token !== undefined &&
|
|
1156
|
-
this.exchangeConsent.digest === (await oauthStateDigest(tokens.refresh_token))
|
|
1157
|
-
? this.exchangeConsent
|
|
1158
|
-
: undefined;
|
|
1159
|
-
await this.writeCredential("tokens", tokens, ctx?.issuer, refreshConsent);
|
|
1160
|
-
this.refreshFailure = undefined;
|
|
1086
|
+
tokens(ctx) {
|
|
1087
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "tokens", [ctx]));
|
|
1088
|
+
}
|
|
1089
|
+
saveTokens(tokens, ctx) {
|
|
1090
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "saveTokens", [tokens, ctx]));
|
|
1161
1091
|
}
|
|
1162
1092
|
/**
|
|
1163
1093
|
* OAuth `state`: 256 random bits the consent URL carries and the callback
|
|
1164
1094
|
* must present, so nobody holding a consent URL can complete it with their
|
|
1165
1095
|
* own account (login CSRF). Stored, by digest, with the consent.
|
|
1166
1096
|
*/
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
if (!this.allowAuthorization)
|
|
1176
|
-
throw this.authorizationRefused();
|
|
1177
|
-
this.consent = { ...this.consent, verifier };
|
|
1178
|
-
}
|
|
1179
|
-
async codeVerifier() {
|
|
1180
|
-
const verifier = this.callback?.flow.verifier ?? this.consent.verifier;
|
|
1181
|
-
if (verifier === undefined) {
|
|
1182
|
-
throw new Error(`No PKCE code verifier for "${this.connectorId}"`);
|
|
1183
|
-
}
|
|
1184
|
-
return verifier;
|
|
1097
|
+
state() {
|
|
1098
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "state", []));
|
|
1099
|
+
}
|
|
1100
|
+
saveCodeVerifier(verifier) {
|
|
1101
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "saveCodeVerifier", [verifier]));
|
|
1102
|
+
}
|
|
1103
|
+
codeVerifier() {
|
|
1104
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "codeVerifier", []));
|
|
1185
1105
|
}
|
|
1186
1106
|
/**
|
|
1187
1107
|
* Store the consent: its flow record first, by state digest with the
|
|
@@ -1189,76 +1109,16 @@ export class KvOAuthProvider {
|
|
|
1189
1109
|
* reset that replaced the epoch fails the start, and the record is removed,
|
|
1190
1110
|
* so no start hands out a consent its callback could not complete.
|
|
1191
1111
|
*/
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
throw this.authorizationRefused();
|
|
1195
|
-
this.assertCredentialFree(authorizationUrl.toString(), (await this.boundGrant()).body, true);
|
|
1196
|
-
const state = this.consent.state ?? authorizationUrl.searchParams.get("state");
|
|
1197
|
-
if (!state)
|
|
1198
|
-
throw new Error(`OAuth consent for "${this.connectorId}" carries no state`);
|
|
1199
|
-
const client = this.clientOptions?.client;
|
|
1200
|
-
if (client && !(await this.boundGrant()).body.client) {
|
|
1201
|
-
await this.saveClientInformation({ client_id: client.clientId }, { issuer: client.issuer });
|
|
1202
|
-
}
|
|
1203
|
-
const configured = this.configuredAuthorizationBinding();
|
|
1204
|
-
if (configured?.issuer !== undefined) {
|
|
1205
|
-
const issuer = configured.issuer;
|
|
1206
|
-
// Static adapters do not save discovery or client configuration. Publish
|
|
1207
|
-
// their server identity before consent so a changed endpoint fences the
|
|
1208
|
-
// old grant before any code exchange can leave.
|
|
1209
|
-
await this.updateGrant((grant) => grant.body.issuer === issuer ? undefined : this.withIssuer(grant, { issuer }));
|
|
1210
|
-
}
|
|
1211
|
-
const epoch = await this.flowEpoch();
|
|
1212
|
-
this.epoch = epoch;
|
|
1213
|
-
const digest = await oauthStateDigest(state);
|
|
1214
|
-
const key = oauthFlowKeys.flow(digest);
|
|
1215
|
-
const url = authorizationUrl.toString();
|
|
1216
|
-
const { verifier } = this.consent;
|
|
1217
|
-
const grant = await this.boundGrant();
|
|
1218
|
-
this.assertCredentialFree(url, grant.body, true);
|
|
1219
|
-
const binding = this.configuredAuthorizationBinding() ?? {
|
|
1220
|
-
...(await discoveryBinding(this.consentDiscovery ?? grant.body.discovery)),
|
|
1221
|
-
issuer: discoveryIssuer(this.consentDiscovery) ?? grant.body.issuer,
|
|
1222
|
-
clientId: authorizationUrl.searchParams.get("client_id") ?? grant.body.client?.value.client_id,
|
|
1223
|
-
};
|
|
1224
|
-
const record = await this.encodeFlow(key, {
|
|
1225
|
-
epoch,
|
|
1226
|
-
at: Date.now(),
|
|
1227
|
-
binding,
|
|
1228
|
-
url,
|
|
1229
|
-
...(verifier !== undefined ? { verifier } : {}),
|
|
1230
|
-
});
|
|
1231
|
-
if (this.signal?.aborted)
|
|
1232
|
-
return;
|
|
1233
|
-
if (!(await this.storage.compareAndSet(key, null, record, { ttlSeconds: OAUTH_FLOW_TTL_SECONDS }))) {
|
|
1234
|
-
throw new Error(`OAuth consent for "${this.connectorId}" reused a state`);
|
|
1235
|
-
}
|
|
1236
|
-
let published = false;
|
|
1237
|
-
try {
|
|
1238
|
-
published = await this.updateGrant((grant) => ({ ...grant, flow: digest }), { epoch });
|
|
1239
|
-
}
|
|
1240
|
-
finally {
|
|
1241
|
-
if (!published)
|
|
1242
|
-
await this.storage.compareAndSet(key, record, null).catch(() => { });
|
|
1243
|
-
}
|
|
1244
|
-
if (published)
|
|
1245
|
-
this.published = { epoch, url };
|
|
1112
|
+
redirectToAuthorization(authorizationUrl) {
|
|
1113
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "redirectToAuthorization", [authorizationUrl]));
|
|
1246
1114
|
}
|
|
1247
1115
|
/** The consent this provider stored, while its epoch is still live. */
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
if (!published)
|
|
1251
|
-
return undefined;
|
|
1252
|
-
if (epochOf(await this.storage.get(GRANT)) !== published.epoch)
|
|
1253
|
-
throw this.flowSuperseded();
|
|
1254
|
-
this.assertCredentialFree(published.url, (await this.boundGrant()).body, true);
|
|
1255
|
-
return published.url;
|
|
1116
|
+
consentUrl(signal = this.signal) {
|
|
1117
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "consentUrl", [signal]));
|
|
1256
1118
|
}
|
|
1257
1119
|
/** The live epoch's pending consent URL, if one is unclaimed. */
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
this.assertCredentialFree(url, (await this.boundGrant()).body, true);
|
|
1261
|
-
return url;
|
|
1120
|
+
pendingAuthorizationUrl() {
|
|
1121
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "pendingAuthorizationUrl", []));
|
|
1262
1122
|
}
|
|
1263
1123
|
/**
|
|
1264
1124
|
* The pending consent URL, only while it is still worth handing out again:
|
|
@@ -1268,39 +1128,16 @@ export class KvOAuthProvider {
|
|
|
1268
1128
|
* `invalid_client` drops the client, and its URL would send the operator to
|
|
1269
1129
|
* a consent the server has already refused.
|
|
1270
1130
|
*/
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
if (!consent || Math.abs(Date.now() - consent.at) >= PENDING_AUTHORIZATION_MAX_AGE_MS) {
|
|
1274
|
-
return undefined;
|
|
1275
|
-
}
|
|
1276
|
-
this.assertCredentialFree(consent.url, (await this.boundGrant()).body, true);
|
|
1277
|
-
let clientId;
|
|
1278
|
-
try {
|
|
1279
|
-
clientId = new URL(consent.url).searchParams.get("client_id");
|
|
1280
|
-
}
|
|
1281
|
-
catch {
|
|
1282
|
-
return undefined;
|
|
1283
|
-
}
|
|
1284
|
-
const client = await this.clientInformation();
|
|
1285
|
-
return clientId !== null && client?.client_id === clientId ? consent.url : undefined;
|
|
1131
|
+
reusablePendingAuthorizationUrl() {
|
|
1132
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "reusablePendingAuthorizationUrl", []));
|
|
1286
1133
|
}
|
|
1287
1134
|
/**
|
|
1288
1135
|
* Find the consent a callback's `state` names. One read whatever the
|
|
1289
1136
|
* answer; the callback route's refusals pay the same. A claimed consent
|
|
1290
1137
|
* no longer matches.
|
|
1291
1138
|
*/
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
if (candidate === null)
|
|
1295
|
-
return false;
|
|
1296
|
-
const key = oauthFlowKeys.flow(await oauthStateDigest(candidate));
|
|
1297
|
-
const raw = await this.storage.get(key);
|
|
1298
|
-
const flow = raw === null ? undefined : await this.openFlow(key, raw, true);
|
|
1299
|
-
if (raw === null || !flow || flow.consumed)
|
|
1300
|
-
return false;
|
|
1301
|
-
this.callback = { key, raw, flow, claimed: false };
|
|
1302
|
-
this.epoch = flow.epoch;
|
|
1303
|
-
return true;
|
|
1139
|
+
verifyState(candidate) {
|
|
1140
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "verifyState", [candidate]));
|
|
1304
1141
|
}
|
|
1305
1142
|
/**
|
|
1306
1143
|
* @internal The first half of the fence a code exchange crosses after every
|
|
@@ -1312,43 +1149,15 @@ export class KvOAuthProvider {
|
|
|
1312
1149
|
* exchange succeeds; the SDK's one retry after a refused code is answered
|
|
1313
1150
|
* with that refusal, never sent again.
|
|
1314
1151
|
*/
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
return this.exchangeRefusal.clone();
|
|
1318
|
-
await this.claimConsent();
|
|
1319
|
-
return undefined;
|
|
1152
|
+
claimCodeExchange() {
|
|
1153
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "claimCodeExchange", []));
|
|
1320
1154
|
}
|
|
1321
1155
|
/** Terminate the verified consent on an error, sharing the code exchange CAS. */
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
await this.claimConsent();
|
|
1328
|
-
this.callback = undefined;
|
|
1329
|
-
}
|
|
1330
|
-
async claimConsent() {
|
|
1331
|
-
const callback = this.callback;
|
|
1332
|
-
if (!callback)
|
|
1333
|
-
throw this.flowSuperseded();
|
|
1334
|
-
if (callback.claimed)
|
|
1335
|
-
throw new OAuthCallbackClaimedError(this.connectorId);
|
|
1336
|
-
callback.claimed = true;
|
|
1337
|
-
const { epoch, at } = callback.flow;
|
|
1338
|
-
const claimed = JSON.stringify({
|
|
1339
|
-
connectaOAuthFlow: FLOW_VERSION,
|
|
1340
|
-
epoch,
|
|
1341
|
-
at,
|
|
1342
|
-
consumed: true,
|
|
1343
|
-
});
|
|
1344
|
-
const remaining = Math.ceil((at + OAUTH_FLOW_TTL_SECONDS * 1000 - Date.now()) / 1000);
|
|
1345
|
-
const ttlSeconds = Math.min(OAUTH_FLOW_TTL_SECONDS, Math.max(1, remaining));
|
|
1346
|
-
if (await this.storage.compareAndSet(callback.key, callback.raw, claimed, { ttlSeconds })) {
|
|
1347
|
-
return;
|
|
1348
|
-
}
|
|
1349
|
-
if (epochOf(await this.storage.get(GRANT)) !== epoch)
|
|
1350
|
-
throw this.flowSuperseded();
|
|
1351
|
-
throw new OAuthCallbackClaimedError(this.connectorId);
|
|
1156
|
+
consumeAuthError() {
|
|
1157
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "consumeAuthError", []));
|
|
1158
|
+
}
|
|
1159
|
+
claimConsent() {
|
|
1160
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "claimConsent", []));
|
|
1352
1161
|
}
|
|
1353
1162
|
/**
|
|
1354
1163
|
* @internal The second half: read the live epoch and, in the reaction to
|
|
@@ -1358,86 +1167,24 @@ export class KvOAuthProvider {
|
|
|
1358
1167
|
* ordered before the send without a lock across requests, and its tokens
|
|
1359
1168
|
* are refused by the grant's compare-and-set: one more consent.
|
|
1360
1169
|
*/
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
if (!this.codeExchange || epoch === undefined)
|
|
1364
|
-
throw this.flowSuperseded();
|
|
1365
|
-
const raw = await this.storage.get(GRANT);
|
|
1366
|
-
const grant = await this.openGrant(raw);
|
|
1367
|
-
if (grant.epoch !== epoch)
|
|
1368
|
-
throw this.flowSuperseded();
|
|
1369
|
-
await this.checkFlowBinding(grant);
|
|
1370
|
-
if (endpoint !== undefined && this.callback?.flow.binding?.tokenEndpoint !== String(endpoint)) {
|
|
1371
|
-
throw this.flowSuperseded();
|
|
1372
|
-
}
|
|
1373
|
-
// Opening ciphertext may yield. Re-read the exact record after opening,
|
|
1374
|
-
// then check and send in the same reaction, without another await.
|
|
1375
|
-
return this.storage.get(GRANT).then((current) => {
|
|
1376
|
-
if (current !== raw)
|
|
1377
|
-
throw this.flowSuperseded();
|
|
1378
|
-
const sent = send();
|
|
1379
|
-
sent.catch(() => { });
|
|
1380
|
-
return sent;
|
|
1381
|
-
});
|
|
1170
|
+
dispatchCodeExchange(send, endpoint) {
|
|
1171
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "dispatchCodeExchange", [send, endpoint]));
|
|
1382
1172
|
}
|
|
1383
1173
|
/** @internal Keep a refused code's answer for the SDK's retry to read. */
|
|
1384
1174
|
recordCodeExchangeRefusal(forSdk) {
|
|
1385
1175
|
this.exchangeRefusal = forSdk.clone();
|
|
1386
1176
|
}
|
|
1387
1177
|
/** @internal Observe resolution while accepting the code response, never on a later SDK save. */
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
if (tokens.refresh_token === undefined)
|
|
1391
|
-
return;
|
|
1392
|
-
try {
|
|
1393
|
-
const digest = await oauthStateDigest(tokens.refresh_token);
|
|
1394
|
-
const resolution = await this.storage.get(oauthRefreshSpentKeys.spent(digest));
|
|
1395
|
-
const spent = storedRefreshSpent(resolution);
|
|
1396
|
-
// Observing the exact resolved storage write before consent completion
|
|
1397
|
-
// proves their order without comparing isolate clocks. A consent that
|
|
1398
|
-
// saw outstanding/ambiguous cannot become eligible on a delayed save.
|
|
1399
|
-
if (spent?.state === "resolved")
|
|
1400
|
-
this.exchangeConsent = { digest, resolution: resolution };
|
|
1401
|
-
}
|
|
1402
|
-
catch {
|
|
1403
|
-
throw this.credentialWriteError();
|
|
1404
|
-
}
|
|
1178
|
+
recordCodeExchangeTokens(tokens) {
|
|
1179
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "recordCodeExchangeTokens", [tokens]));
|
|
1405
1180
|
}
|
|
1406
1181
|
/**
|
|
1407
1182
|
* Remove what the SDK asks, but only as this flow saw it: a refused code or
|
|
1408
1183
|
* refresh says nothing about a client or tokens another flow wrote since.
|
|
1409
1184
|
* The verifier lives in its consent, which the claim already spent.
|
|
1410
1185
|
*/
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
all: ["client", "tokens", "discovery"],
|
|
1414
|
-
client: ["client"],
|
|
1415
|
-
tokens: ["tokens"],
|
|
1416
|
-
discovery: ["discovery"],
|
|
1417
|
-
verifier: [],
|
|
1418
|
-
}[scope];
|
|
1419
|
-
if (fields.length > 0) {
|
|
1420
|
-
await this.updateGrant((grant) => {
|
|
1421
|
-
const body = { ...grant.body };
|
|
1422
|
-
let changed = false;
|
|
1423
|
-
for (const field of fields) {
|
|
1424
|
-
const current = field === "client" ? body.client?.value : body[field];
|
|
1425
|
-
if (current === undefined)
|
|
1426
|
-
continue;
|
|
1427
|
-
if (field !== "discovery" && fingerprint(current) !== this.seen[field])
|
|
1428
|
-
continue;
|
|
1429
|
-
delete body[field];
|
|
1430
|
-
changed = true;
|
|
1431
|
-
}
|
|
1432
|
-
return changed ? { ...grant, body } : undefined;
|
|
1433
|
-
}, { quiet: true });
|
|
1434
|
-
}
|
|
1435
|
-
// The SDK invalidates after a refused refresh and then starts over, which
|
|
1436
|
-
// on `invalid_client` means registering a new client. A passive request
|
|
1437
|
-
// must not begin authorization, so it ends here, as auth_required.
|
|
1438
|
-
if (!this.allowAuthorization && this.refreshFailure?.kind === "dead") {
|
|
1439
|
-
throw this.authorizationRefused();
|
|
1440
|
-
}
|
|
1186
|
+
invalidateCredentials(scope) {
|
|
1187
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "invalidateCredentials", [scope]));
|
|
1441
1188
|
}
|
|
1442
1189
|
// --- refresh, for the coordinator ---------------------------------------------
|
|
1443
1190
|
/**
|
|
@@ -1449,229 +1196,32 @@ export class KvOAuthProvider {
|
|
|
1449
1196
|
this.refreshFailure = failure;
|
|
1450
1197
|
}
|
|
1451
1198
|
/** @internal A shared-storage claim precedes every refresh dispatch. */
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
const spentKey = oauthRefreshSpentKeys.spent(digest);
|
|
1460
|
-
while (!waiting.signal.aborted) {
|
|
1461
|
-
if (signal?.aborted)
|
|
1462
|
-
throw aborted(signal);
|
|
1463
|
-
const current = await wait(() => this.storedTokens());
|
|
1464
|
-
if (current?.refresh_token !== requested || (current && this.refreshedSinceRead(current, epoch))) {
|
|
1465
|
-
if (!current)
|
|
1466
|
-
this.recordRefreshFailure({ kind: "dead" });
|
|
1467
|
-
return current ? this.adoptRefresh(current) : Response.json({ error: "invalid_grant" }, { status: 400 });
|
|
1468
|
-
}
|
|
1469
|
-
const raw = await wait(() => this.storage.get(key));
|
|
1470
|
-
const stored = raw === null ? undefined : parsed(raw);
|
|
1471
|
-
if (plainObject(stored) &&
|
|
1472
|
-
stored.completed === true &&
|
|
1473
|
-
current &&
|
|
1474
|
-
stored.tokenDigest === (await wait(() => refreshTokensDigest(current)))) {
|
|
1475
|
-
return this.adoptRefresh(current);
|
|
1476
|
-
}
|
|
1477
|
-
if (plainObject(stored) && (plainObject(stored.verdict) || stored.completed === true)) {
|
|
1478
|
-
this.recordRefreshFailure({ kind: "dead" });
|
|
1479
|
-
await wait(() => this.discardRefusedGrant(requested, epoch));
|
|
1480
|
-
throw new UnauthorizedError("OAuth refresh grant is no longer active.");
|
|
1481
|
-
}
|
|
1482
|
-
if (plainObject(stored) && stored.state !== "claimed") {
|
|
1483
|
-
// Never compare another isolate's clock for a sent request. The
|
|
1484
|
-
// durable record survives expiry of this storage-owned liveness key.
|
|
1485
|
-
const active = typeof stored.activeKey === "string"
|
|
1486
|
-
? await wait(() => this.storage.get(stored.activeKey))
|
|
1487
|
-
: null;
|
|
1488
|
-
if (active === null) {
|
|
1489
|
-
const refused = JSON.stringify({ ...stored, verdict: { kind: "dead" } });
|
|
1490
|
-
if (await wait(() => this.storage.compareAndSet(key, raw, refused))) {
|
|
1491
|
-
await wait(() => this.resolveRefreshSpent({ key, spentKey, spentExpected: null, raw: raw }, "ambiguous"));
|
|
1492
|
-
this.recordRefreshFailure({ kind: "dead" });
|
|
1493
|
-
await wait(() => this.discardRefusedGrant(requested, epoch));
|
|
1494
|
-
throw new UnauthorizedError("OAuth refresh outcome is unknown; authorization required.");
|
|
1495
|
-
}
|
|
1496
|
-
continue;
|
|
1497
|
-
}
|
|
1498
|
-
}
|
|
1499
|
-
else {
|
|
1500
|
-
const spentExpected = await wait(() => this.storage.get(spentKey));
|
|
1501
|
-
if (spentExpected !== null) {
|
|
1502
|
-
const spent = storedRefreshSpent(spentExpected);
|
|
1503
|
-
const consent = (await wait(() => this.boundGrant())).body.refreshConsent;
|
|
1504
|
-
if (spent?.state !== "resolved" ||
|
|
1505
|
-
spent.epoch === epoch ||
|
|
1506
|
-
consent?.digest !== digest ||
|
|
1507
|
-
consent.resolution !== spentExpected) {
|
|
1508
|
-
// Reset can sweep the old lease while its request remains live.
|
|
1509
|
-
// Only storage-owned expiry can make an outstanding send ambiguous.
|
|
1510
|
-
if (spent?.state === "outstanding" && (await wait(() => this.storage.get(spent.activeKey))) === null) {
|
|
1511
|
-
await wait(() => this.storage.compareAndSet(spentKey, spentExpected, JSON.stringify({ ...spent, state: "ambiguous" })));
|
|
1512
|
-
}
|
|
1513
|
-
this.recordRefreshFailure({ kind: "dead" });
|
|
1514
|
-
await wait(() => this.discardRefusedGrant(requested, epoch));
|
|
1515
|
-
throw new UnauthorizedError("OAuth refresh token was already spent; authorization required.");
|
|
1516
|
-
}
|
|
1517
|
-
}
|
|
1518
|
-
if (raw === null ||
|
|
1519
|
-
(plainObject(stored) && typeof stored.expiresAt === "number" && stored.expiresAt <= this.refreshNow())) {
|
|
1520
|
-
// Clock skew can replace an unsent claim; its dispatch CAS then loses.
|
|
1521
|
-
const claimed = JSON.stringify({
|
|
1522
|
-
connectaOAuthRefresh: 1,
|
|
1523
|
-
holder: crypto.randomUUID(),
|
|
1524
|
-
state: "claimed",
|
|
1525
|
-
expiresAt: this.refreshNow() + OAUTH_REFRESH_LEASE_SECONDS * 1000,
|
|
1526
|
-
});
|
|
1527
|
-
let won;
|
|
1528
|
-
try {
|
|
1529
|
-
won = await wait(() => this.storage.compareAndSet(key, raw, claimed));
|
|
1530
|
-
}
|
|
1531
|
-
catch {
|
|
1532
|
-
throw this.credentialWriteError();
|
|
1533
|
-
}
|
|
1534
|
-
if (won) {
|
|
1535
|
-
try {
|
|
1536
|
-
const latest = await wait(() => this.storedTokens());
|
|
1537
|
-
if (latest?.refresh_token !== requested || (latest && this.refreshedSinceRead(latest, epoch))) {
|
|
1538
|
-
await wait(() => this.releaseRefresh({ key, spentKey, spentExpected, raw: claimed }));
|
|
1539
|
-
return latest
|
|
1540
|
-
? this.adoptRefresh(latest)
|
|
1541
|
-
: Response.json({ error: "invalid_grant" }, { status: 400 });
|
|
1542
|
-
}
|
|
1543
|
-
if (signal?.aborted)
|
|
1544
|
-
throw aborted(signal);
|
|
1545
|
-
return { key, spentKey, spentExpected, raw: claimed };
|
|
1546
|
-
}
|
|
1547
|
-
catch (error) {
|
|
1548
|
-
// An unsent claim can expire safely. A caller that has left must
|
|
1549
|
-
// settle its local flight without awaiting cleanup storage.
|
|
1550
|
-
if (!waiting.signal.aborted && !signal?.aborted) {
|
|
1551
|
-
await wait(() => this.releaseRefresh({ key, spentKey, spentExpected, raw: claimed }));
|
|
1552
|
-
}
|
|
1553
|
-
throw error;
|
|
1554
|
-
}
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
}
|
|
1558
|
-
await runEdge(Effect.raceAllFirst([
|
|
1559
|
-
Effect.sleep(delay),
|
|
1560
|
-
fromSignal(waiting.signal),
|
|
1561
|
-
...(signal ? [fromSignal(signal)] : []),
|
|
1562
|
-
]));
|
|
1563
|
-
delay = Math.min(250, delay * 2);
|
|
1564
|
-
}
|
|
1565
|
-
}
|
|
1566
|
-
catch (error) {
|
|
1567
|
-
if (signal?.aborted)
|
|
1568
|
-
throw aborted(signal);
|
|
1569
|
-
if (!waiting.signal.aborted)
|
|
1570
|
-
throw error;
|
|
1571
|
-
}
|
|
1572
|
-
finally {
|
|
1573
|
-
if (!sharedWait)
|
|
1574
|
-
waiting.close();
|
|
1575
|
-
}
|
|
1576
|
-
throw this.refreshContended();
|
|
1199
|
+
claimRefresh(epoch, requested, signal, sharedWait) {
|
|
1200
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "claimRefresh", [
|
|
1201
|
+
epoch,
|
|
1202
|
+
requested,
|
|
1203
|
+
signal,
|
|
1204
|
+
sharedWait,
|
|
1205
|
+
]));
|
|
1577
1206
|
}
|
|
1578
1207
|
/** @internal Only the successful claimed-to-dispatched CAS may send. */
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
const epoch = await waiting.run(() => this.flowEpoch());
|
|
1582
|
-
const activeKey = oauthRefreshActiveKeys.holder(epoch, stored.holder);
|
|
1583
|
-
await waiting.run(() => this.storage.set(activeKey, JSON.stringify({ connectaOAuthRefresh: 1 }), {
|
|
1584
|
-
ttlSeconds: OAUTH_REFRESH_LEASE_SECONDS,
|
|
1585
|
-
}));
|
|
1586
|
-
const dispatched = JSON.stringify({ ...stored, state: "dispatched", activeKey });
|
|
1587
|
-
const transition = this.storage.compareAndSet(lease.key, lease.raw, dispatched).then((won) => {
|
|
1588
|
-
if (!won)
|
|
1589
|
-
throw this.refreshContended();
|
|
1590
|
-
lease.raw = dispatched;
|
|
1591
|
-
if (waiting.signal.aborted)
|
|
1592
|
-
throw aborted(waiting.signal);
|
|
1593
|
-
});
|
|
1594
|
-
void transition.catch(() => { });
|
|
1595
|
-
await waiting.run(() => transition);
|
|
1596
|
-
// One CAS owns the fingerprint across every epoch. Only a later consent
|
|
1597
|
-
// that observed a resolved write may replace it. No TTL or deletion path.
|
|
1598
|
-
lease.spentRaw = JSON.stringify({
|
|
1599
|
-
connectaOAuthRefreshSpent: 1,
|
|
1600
|
-
epoch,
|
|
1601
|
-
holder: stored.holder,
|
|
1602
|
-
activeKey,
|
|
1603
|
-
state: "outstanding",
|
|
1604
|
-
});
|
|
1605
|
-
const preparation = this.storage
|
|
1606
|
-
.compareAndSet(lease.spentKey, lease.spentExpected, lease.spentRaw)
|
|
1607
|
-
.catch(() => {
|
|
1608
|
-
this.recordRefreshFailure({ kind: "dead" });
|
|
1609
|
-
throw new UnauthorizedError("OAuth refresh dispatch could not be recorded; authorization required.");
|
|
1610
|
-
})
|
|
1611
|
-
.then((unspent) => {
|
|
1612
|
-
if (!unspent) {
|
|
1613
|
-
this.recordRefreshFailure({ kind: "dead" });
|
|
1614
|
-
throw new UnauthorizedError("OAuth refresh token was already spent; authorization required.");
|
|
1615
|
-
}
|
|
1616
|
-
// A storage operation can finish after its waiter left. Never send from
|
|
1617
|
-
// abandoned preparation, even if both CAS operations succeeded.
|
|
1618
|
-
if (waiting.signal.aborted)
|
|
1619
|
-
throw aborted(waiting.signal);
|
|
1620
|
-
const sent = send();
|
|
1621
|
-
sent.catch(() => { });
|
|
1622
|
-
return { sent };
|
|
1623
|
-
});
|
|
1624
|
-
void preparation.catch(() => { });
|
|
1625
|
-
const { sent } = await waiting.run(() => preparation);
|
|
1626
|
-
return sent;
|
|
1208
|
+
dispatchRefresh(lease, send, waiting) {
|
|
1209
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "dispatchRefresh", [lease, send, waiting]));
|
|
1627
1210
|
}
|
|
1628
1211
|
/** @internal Release by CAS after a commit or a classified failure. */
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
: JSON.stringify({
|
|
1635
|
-
...stored,
|
|
1636
|
-
...(tokens
|
|
1637
|
-
? { completed: true, tokenDigest: await refreshTokensDigest(tokens) }
|
|
1638
|
-
: { verdict: { kind: "dead" } }),
|
|
1639
|
-
});
|
|
1640
|
-
// Verdicts contain fixed typed facts; no downstream causes or text.
|
|
1641
|
-
if (verdict)
|
|
1642
|
-
this.recordRefreshFailure(verdict);
|
|
1643
|
-
await this.resolveRefreshSpent(lease, resolution);
|
|
1644
|
-
const released = await this.storage.compareAndSet(lease.key, lease.raw, next).catch(() => false);
|
|
1645
|
-
if (released && stored.activeKey)
|
|
1646
|
-
await this.storage.delete(stored.activeKey).catch(() => { });
|
|
1647
|
-
}
|
|
1648
|
-
async resolveRefreshSpent(lease, state) {
|
|
1649
|
-
try {
|
|
1650
|
-
const raw = lease.spentRaw ?? (await this.storage.get(lease.spentKey));
|
|
1651
|
-
const spent = storedRefreshSpent(raw);
|
|
1652
|
-
const holder = parsed(lease.raw).holder;
|
|
1653
|
-
if (spent?.state !== "outstanding" || spent.holder !== holder)
|
|
1654
|
-
return;
|
|
1655
|
-
// The timestamp belongs to this resolution write, never to dispatch.
|
|
1656
|
-
// Consent copies the exact stored receipt, so wall clocks grant no sends.
|
|
1657
|
-
await this.storage.compareAndSet(lease.spentKey, raw, JSON.stringify({
|
|
1658
|
-
...spent,
|
|
1659
|
-
state,
|
|
1660
|
-
...(state === "resolved" ? { resolvedAt: Date.now() } : {}),
|
|
1661
|
-
}));
|
|
1662
|
-
}
|
|
1663
|
-
catch {
|
|
1664
|
-
// A lost resolution answer leaves outstanding or the durable outcome;
|
|
1665
|
-
// either blocks replay until a later consent observes a resolved write.
|
|
1666
|
-
}
|
|
1212
|
+
releaseRefresh(lease, verdict, tokens, resolution = "ambiguous") {
|
|
1213
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "releaseRefresh", [lease, verdict, tokens, resolution]));
|
|
1214
|
+
}
|
|
1215
|
+
resolveRefreshSpent(lease, state) {
|
|
1216
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "resolveRefreshSpent", [lease, state]));
|
|
1667
1217
|
}
|
|
1668
1218
|
/** @internal The same typed verdict for local and cross-isolate joiners. */
|
|
1669
1219
|
refreshVerdict() {
|
|
1670
1220
|
return this.refreshFailure;
|
|
1671
1221
|
}
|
|
1672
1222
|
/** @internal The grant's tokens as stored, in the flow's epoch. */
|
|
1673
|
-
|
|
1674
|
-
return (
|
|
1223
|
+
storedTokens(signal = null) {
|
|
1224
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "storedTokens", [signal]));
|
|
1675
1225
|
}
|
|
1676
1226
|
/** @internal True when a refresh landed after this flow's issuer-aware read. */
|
|
1677
1227
|
refreshedSinceRead(current, epoch) {
|
|
@@ -1694,25 +1244,8 @@ export class KvOAuthProvider {
|
|
|
1694
1244
|
* grant, or another epoch is there now. A token response names no issuer:
|
|
1695
1245
|
* the one the refreshed grant was bound to is stamped here.
|
|
1696
1246
|
*/
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
try {
|
|
1700
|
-
const { raw, grant } = await this.readGrant();
|
|
1701
|
-
if (grant.epoch !== epoch || (issuer !== undefined && grant.body.issuer !== issuer))
|
|
1702
|
-
return undefined;
|
|
1703
|
-
if (grant.body.tokens?.refresh_token !== requested)
|
|
1704
|
-
return grant.body.tokens;
|
|
1705
|
-
const committed = issuer !== undefined ? { ...tokens, issuer } : tokens;
|
|
1706
|
-
const encoded = await this.encodeGrant({ ...grant, body: { ...grant.body, tokens: committed } });
|
|
1707
|
-
if (await this.storage.compareAndSet(GRANT, raw, encoded))
|
|
1708
|
-
return committed;
|
|
1709
|
-
}
|
|
1710
|
-
catch {
|
|
1711
|
-
// A failed or lost CAS answer may still have committed. Re-read the
|
|
1712
|
-
// epoch and retry these in-memory tokens without another HTTP request.
|
|
1713
|
-
}
|
|
1714
|
-
}
|
|
1715
|
-
throw this.credentialWriteError();
|
|
1247
|
+
storeRefresh(epoch, requested, tokens, issuer) {
|
|
1248
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "storeRefresh", [epoch, requested, tokens, issuer]));
|
|
1716
1249
|
}
|
|
1717
1250
|
/**
|
|
1718
1251
|
* @internal Drop the tokens the authorization server just refused, only
|
|
@@ -1720,18 +1253,13 @@ export class KvOAuthProvider {
|
|
|
1720
1253
|
* that completed meanwhile survives. Best effort: if storage fails, the
|
|
1721
1254
|
* next refresh is refused again and ends the same way.
|
|
1722
1255
|
*/
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
}, { epoch, quiet: true, commit: true });
|
|
1731
|
-
}
|
|
1732
|
-
catch {
|
|
1733
|
-
// See above: a refusal that could not be recorded recurs, it does not hide.
|
|
1734
|
-
}
|
|
1256
|
+
discardRefusedGrant(refreshToken, epoch, beforeWrite, signal) {
|
|
1257
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "discardRefusedGrant", [
|
|
1258
|
+
refreshToken,
|
|
1259
|
+
epoch,
|
|
1260
|
+
beforeWrite,
|
|
1261
|
+
signal,
|
|
1262
|
+
]));
|
|
1735
1263
|
}
|
|
1736
1264
|
// --- restart and disconnect ---------------------------------------------------
|
|
1737
1265
|
/**
|
|
@@ -1743,44 +1271,21 @@ export class KvOAuthProvider {
|
|
|
1743
1271
|
* callbacks fail the epoch check anyway and the records expire.
|
|
1744
1272
|
*/
|
|
1745
1273
|
resetAuthorization(operatorDisconnected = false, preserveClient = false) {
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1274
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "resetAuthorization", [
|
|
1275
|
+
operatorDisconnected,
|
|
1276
|
+
preserveClient,
|
|
1277
|
+
]));
|
|
1749
1278
|
}
|
|
1750
1279
|
/** Remove the local grant before a bounded, best-effort RFC 7009 request. */
|
|
1751
1280
|
disconnectAuthorization(send) {
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
const carried = preserveClient && !operatorDisconnected ? this.carriableClient(grant) : undefined;
|
|
1761
|
-
const encoded = await this.encodeGrant({ epoch, body: carried ?? {}, cleanupPending: true });
|
|
1762
|
-
let written;
|
|
1763
|
-
try {
|
|
1764
|
-
written = await this.storage.compareAndSet(GRANT, raw, encoded);
|
|
1765
|
-
}
|
|
1766
|
-
catch {
|
|
1767
|
-
throw this.credentialWriteError();
|
|
1768
|
-
}
|
|
1769
|
-
if (written) {
|
|
1770
|
-
this.refreshCoordinator?.retire(grant.epoch);
|
|
1771
|
-
try {
|
|
1772
|
-
await this.cleanupV2(encoded);
|
|
1773
|
-
await this.sweepConsents();
|
|
1774
|
-
await this.sweepRefreshLeases();
|
|
1775
|
-
}
|
|
1776
|
-
finally {
|
|
1777
|
-
if (revoke)
|
|
1778
|
-
await revokeDownstreamGrant(grant.body, revoke, this.clientOptions?.client);
|
|
1779
|
-
}
|
|
1780
|
-
return;
|
|
1781
|
-
}
|
|
1782
|
-
}
|
|
1783
|
-
throw this.credentialWriteError();
|
|
1281
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "disconnectAuthorization", [send]));
|
|
1282
|
+
}
|
|
1283
|
+
performReset(operatorDisconnected, preserveClient, revoke) {
|
|
1284
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "performReset", [
|
|
1285
|
+
operatorDisconnected,
|
|
1286
|
+
preserveClient,
|
|
1287
|
+
revoke,
|
|
1288
|
+
]));
|
|
1784
1289
|
}
|
|
1785
1290
|
/**
|
|
1786
1291
|
* Remove every consent of an epoch no longer live. The records are read
|
|
@@ -1788,47 +1293,11 @@ export class KvOAuthProvider {
|
|
|
1788
1293
|
* the live one then can never complete; each goes by compare-and-set
|
|
1789
1294
|
* against what was read. Best effort.
|
|
1790
1295
|
*/
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
const consents = [];
|
|
1794
|
-
for (const key of await this.storage.list(oauthFlowKeys.prefix)) {
|
|
1795
|
-
const raw = await this.storage.get(key);
|
|
1796
|
-
if (raw !== null)
|
|
1797
|
-
consents.push([key, raw]);
|
|
1798
|
-
}
|
|
1799
|
-
const live = epochOf(await this.storage.get(GRANT));
|
|
1800
|
-
for (const [key, raw] of consents) {
|
|
1801
|
-
const flow = parsed(raw);
|
|
1802
|
-
if (plainObject(flow) && flow.epoch === live)
|
|
1803
|
-
continue;
|
|
1804
|
-
await this.storage.compareAndSet(key, raw, null);
|
|
1805
|
-
}
|
|
1806
|
-
}
|
|
1807
|
-
catch {
|
|
1808
|
-
// Hygiene only: the epoch, not this, fences a stale consent.
|
|
1809
|
-
}
|
|
1296
|
+
sweepConsents() {
|
|
1297
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "sweepConsents", []));
|
|
1810
1298
|
}
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
const records = [];
|
|
1814
|
-
// A reset may sweep an old lease while its HTTP request is still live.
|
|
1815
|
-
// Keep holder liveness until completion or its storage-owned TTL: spent
|
|
1816
|
-
// records in later epochs still use it to distinguish live from crashed.
|
|
1817
|
-
for (const key of await this.storage.list(oauthRefreshKeys.prefix)) {
|
|
1818
|
-
const raw = await this.storage.get(key);
|
|
1819
|
-
if (raw !== null)
|
|
1820
|
-
records.push([key, raw]);
|
|
1821
|
-
}
|
|
1822
|
-
const live = epochOf(await this.storage.get(GRANT));
|
|
1823
|
-
for (const [key, raw] of records) {
|
|
1824
|
-
if (!key.startsWith(`${oauthRefreshKeys.prefix}${live}:`)) {
|
|
1825
|
-
await this.storage.compareAndSet(key, raw, null);
|
|
1826
|
-
}
|
|
1827
|
-
}
|
|
1828
|
-
}
|
|
1829
|
-
catch {
|
|
1830
|
-
// Stale dispatch records cannot grant credentials: epochs never recur.
|
|
1831
|
-
}
|
|
1299
|
+
sweepRefreshLeases() {
|
|
1300
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "sweepRefreshLeases", []));
|
|
1832
1301
|
}
|
|
1833
1302
|
/**
|
|
1834
1303
|
* The registration a forced restart may carry into its new epoch, or
|
|
@@ -1876,4 +1345,1426 @@ export class KvOAuthProvider {
|
|
|
1876
1345
|
},
|
|
1877
1346
|
};
|
|
1878
1347
|
}
|
|
1348
|
+
static {
|
|
1349
|
+
registerAuthMethod(KvOAuthProvider.prototype, "wait", function (work, signal) {
|
|
1350
|
+
signal = signal === undefined ? this.signal : signal;
|
|
1351
|
+
return authFlow(function* () {
|
|
1352
|
+
if (signal?.aborted)
|
|
1353
|
+
throw aborted(signal);
|
|
1354
|
+
const value = signal
|
|
1355
|
+
? (yield authAwait(authCall(work, []), { signal }.signal))
|
|
1356
|
+
: (yield authCall(work, []));
|
|
1357
|
+
if (signal?.aborted)
|
|
1358
|
+
throw aborted(signal);
|
|
1359
|
+
return value;
|
|
1360
|
+
});
|
|
1361
|
+
}, KvOAuthProvider.prototype.wait);
|
|
1362
|
+
registerAuthMethod(KvOAuthProvider.prototype, "encodeGrant", function (grant, signal) {
|
|
1363
|
+
signal = signal === undefined ? this.signal : signal;
|
|
1364
|
+
return authFlow(function* () {
|
|
1365
|
+
this.assertCredentialFree({ issuer: grant.body.issuer, discovery: grant.body.discovery }, grant.body);
|
|
1366
|
+
const stored = {
|
|
1367
|
+
connectaOAuth: GRANT_VERSION,
|
|
1368
|
+
epoch: grant.epoch,
|
|
1369
|
+
...(grant.cleanupPending ? { cleanupPending: true } : {}),
|
|
1370
|
+
...(grant.flow !== undefined ? { flow: grant.flow } : {}),
|
|
1371
|
+
};
|
|
1372
|
+
if (Object.keys(grant.body).length > 0) {
|
|
1373
|
+
if (this.sealer) {
|
|
1374
|
+
stored.sealed = (yield authInvoke(this, "wait", [
|
|
1375
|
+
authMethod(() => {
|
|
1376
|
+
return authInvoke(this.sealer, "seal", [
|
|
1377
|
+
this.grantLabel(grant.epoch),
|
|
1378
|
+
JSON.stringify(grant.body),
|
|
1379
|
+
]);
|
|
1380
|
+
}),
|
|
1381
|
+
signal,
|
|
1382
|
+
]));
|
|
1383
|
+
}
|
|
1384
|
+
else {
|
|
1385
|
+
stored.body = grant.body;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
return JSON.stringify(stored);
|
|
1389
|
+
}.bind(this));
|
|
1390
|
+
}, KvOAuthProvider.prototype.encodeGrant);
|
|
1391
|
+
registerAuthMethod(KvOAuthProvider.prototype, "openGrant", function (raw, signal) {
|
|
1392
|
+
signal = signal === undefined ? this.signal : signal;
|
|
1393
|
+
return authFlow(function* () {
|
|
1394
|
+
if (raw === null)
|
|
1395
|
+
return { epoch: INITIAL_EPOCH, body: {} };
|
|
1396
|
+
const stored = storedGrant(raw);
|
|
1397
|
+
if (!stored)
|
|
1398
|
+
return { epoch: UNREADABLE_EPOCH, body: {} };
|
|
1399
|
+
let body = stored.body;
|
|
1400
|
+
if (stored.sealed !== undefined) {
|
|
1401
|
+
body = undefined;
|
|
1402
|
+
try {
|
|
1403
|
+
if (this.sealer)
|
|
1404
|
+
body = parsed((yield authInvoke(this, "wait", [
|
|
1405
|
+
authMethod(() => {
|
|
1406
|
+
return authInvoke(this.sealer, "open", [
|
|
1407
|
+
this.grantLabel(stored.epoch),
|
|
1408
|
+
stored.sealed,
|
|
1409
|
+
]);
|
|
1410
|
+
}),
|
|
1411
|
+
signal,
|
|
1412
|
+
])));
|
|
1413
|
+
}
|
|
1414
|
+
catch {
|
|
1415
|
+
if (signal?.aborted)
|
|
1416
|
+
throw aborted(signal);
|
|
1417
|
+
this.sealer?.warn(`[connecta] connector "${this.connectorId}" has a sealed OAuth grant ` +
|
|
1418
|
+
"the configured vault cannot open; treating it as absent, so the " +
|
|
1419
|
+
"connector needs authorization again. A changed vault key or " +
|
|
1420
|
+
"tampered storage causes this.");
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
return {
|
|
1424
|
+
epoch: stored.epoch,
|
|
1425
|
+
...(stored.cleanupPending ? { cleanupPending: true } : {}),
|
|
1426
|
+
...(stored.flow !== undefined ? { flow: stored.flow } : {}),
|
|
1427
|
+
body: grantBody(body),
|
|
1428
|
+
};
|
|
1429
|
+
}.bind(this));
|
|
1430
|
+
}, KvOAuthProvider.prototype.openGrant);
|
|
1431
|
+
registerAuthMethod(KvOAuthProvider.prototype, "readGrant", function (signal) {
|
|
1432
|
+
signal = signal === undefined ? this.signal : signal;
|
|
1433
|
+
return authFlow(function* () {
|
|
1434
|
+
let raw = (yield authInvoke(this, "wait", [
|
|
1435
|
+
authMethod(() => {
|
|
1436
|
+
return authStorage(this.storage, "get", [GRANT]);
|
|
1437
|
+
}),
|
|
1438
|
+
signal,
|
|
1439
|
+
]));
|
|
1440
|
+
if (raw === null)
|
|
1441
|
+
raw = (yield authInvoke(this, "migrate", [signal]));
|
|
1442
|
+
if (raw !== null && storedGrant(raw)?.cleanupPending)
|
|
1443
|
+
raw = (yield authInvoke(this, "cleanupV2", [raw, signal]));
|
|
1444
|
+
const grant = (yield authInvoke(this, "openGrant", [raw, signal]));
|
|
1445
|
+
if (raw !== null && this.sealer && storedGrant(raw)?.body !== undefined) {
|
|
1446
|
+
const sealed = (yield authInvoke(this, "encodeGrant", [grant, signal]));
|
|
1447
|
+
if ((yield authInvoke(this, "wait", [
|
|
1448
|
+
authMethod(() => {
|
|
1449
|
+
return authCatch(authStorage(this.storage, "compareAndSet", [GRANT, raw, sealed]), () => false);
|
|
1450
|
+
}),
|
|
1451
|
+
signal,
|
|
1452
|
+
])))
|
|
1453
|
+
raw = sealed;
|
|
1454
|
+
}
|
|
1455
|
+
return { raw, grant };
|
|
1456
|
+
}.bind(this));
|
|
1457
|
+
}, KvOAuthProvider.prototype.readGrant);
|
|
1458
|
+
registerAuthMethod(KvOAuthProvider.prototype, "migrate", function (signal) {
|
|
1459
|
+
return authFlow(function* () {
|
|
1460
|
+
const sealer = this.sealer;
|
|
1461
|
+
const found = (yield authInvoke(this, "wait", [
|
|
1462
|
+
authMethod(() => {
|
|
1463
|
+
return authCall(readV2Grant, [
|
|
1464
|
+
this.preparationStorage(signal),
|
|
1465
|
+
sealer && {
|
|
1466
|
+
...sealer,
|
|
1467
|
+
open: authMethod((label, value) => {
|
|
1468
|
+
return authInvoke(this, "wait", [
|
|
1469
|
+
authMethod(() => {
|
|
1470
|
+
return authInvoke(sealer, "open", [label, value]);
|
|
1471
|
+
}),
|
|
1472
|
+
signal,
|
|
1473
|
+
]);
|
|
1474
|
+
}),
|
|
1475
|
+
},
|
|
1476
|
+
]);
|
|
1477
|
+
}),
|
|
1478
|
+
signal,
|
|
1479
|
+
]));
|
|
1480
|
+
if (!found)
|
|
1481
|
+
return null;
|
|
1482
|
+
const prefix = found.disconnected ? DISCONNECTED_EPOCH_PREFIX : ACTIVE_EPOCH_PREFIX;
|
|
1483
|
+
const encoded = (yield authInvoke(this, "encodeGrant", [
|
|
1484
|
+
{
|
|
1485
|
+
epoch: `${prefix}${crypto.randomUUID()}`,
|
|
1486
|
+
body: found.body,
|
|
1487
|
+
cleanupPending: true,
|
|
1488
|
+
},
|
|
1489
|
+
signal,
|
|
1490
|
+
]));
|
|
1491
|
+
let written;
|
|
1492
|
+
try {
|
|
1493
|
+
written = (yield authInvoke(this, "wait", [
|
|
1494
|
+
authMethod(() => {
|
|
1495
|
+
return authStorage(this.storage, "compareAndSet", [GRANT, null, encoded]);
|
|
1496
|
+
}),
|
|
1497
|
+
signal,
|
|
1498
|
+
]));
|
|
1499
|
+
}
|
|
1500
|
+
catch {
|
|
1501
|
+
if (signal?.aborted)
|
|
1502
|
+
throw aborted(signal);
|
|
1503
|
+
throw this.credentialWriteError();
|
|
1504
|
+
}
|
|
1505
|
+
return (yield written
|
|
1506
|
+
? authValue(() => encoded)
|
|
1507
|
+
: authInvoke(this, "wait", [
|
|
1508
|
+
authMethod(() => {
|
|
1509
|
+
return authStorage(this.storage, "get", [GRANT]);
|
|
1510
|
+
}),
|
|
1511
|
+
signal,
|
|
1512
|
+
]));
|
|
1513
|
+
}.bind(this));
|
|
1514
|
+
}, KvOAuthProvider.prototype.migrate);
|
|
1515
|
+
registerAuthMethod(KvOAuthProvider.prototype, "cleanupV2", function (raw, signal) {
|
|
1516
|
+
signal = signal === undefined ? this.signal : signal;
|
|
1517
|
+
return authFlow(function* () {
|
|
1518
|
+
try {
|
|
1519
|
+
if (!(yield authInvoke(this, "wait", [
|
|
1520
|
+
authMethod(() => {
|
|
1521
|
+
return authCall(deleteV2Keys, [this.preparationStorage(signal)]);
|
|
1522
|
+
}),
|
|
1523
|
+
signal,
|
|
1524
|
+
])))
|
|
1525
|
+
return raw;
|
|
1526
|
+
const grant = (yield authInvoke(this, "openGrant", [raw, signal]));
|
|
1527
|
+
delete grant.cleanupPending;
|
|
1528
|
+
const cleared = (yield authInvoke(this, "encodeGrant", [grant, signal]));
|
|
1529
|
+
if ((yield authInvoke(this, "wait", [
|
|
1530
|
+
authMethod(() => {
|
|
1531
|
+
return authStorage(this.storage, "compareAndSet", [GRANT, raw, cleared]);
|
|
1532
|
+
}),
|
|
1533
|
+
signal,
|
|
1534
|
+
])))
|
|
1535
|
+
return cleared;
|
|
1536
|
+
return (yield authInvoke(this, "wait", [
|
|
1537
|
+
authMethod(() => {
|
|
1538
|
+
return authStorage(this.storage, "get", [GRANT]);
|
|
1539
|
+
}),
|
|
1540
|
+
signal,
|
|
1541
|
+
]));
|
|
1542
|
+
}
|
|
1543
|
+
catch {
|
|
1544
|
+
if (signal?.aborted)
|
|
1545
|
+
throw aborted(signal);
|
|
1546
|
+
// The durable marker survives both deletion and marker-clear failures.
|
|
1547
|
+
return raw;
|
|
1548
|
+
}
|
|
1549
|
+
}.bind(this));
|
|
1550
|
+
}, KvOAuthProvider.prototype.cleanupV2);
|
|
1551
|
+
registerAuthMethod(KvOAuthProvider.prototype, "boundGrant", function (signal) {
|
|
1552
|
+
signal = signal === undefined ? this.signal : signal;
|
|
1553
|
+
return authFlow(function* () {
|
|
1554
|
+
const { grant } = (yield authInvoke(this, "readGrant", [signal]));
|
|
1555
|
+
if (this.bound && grant.epoch !== this.epoch)
|
|
1556
|
+
throw this.flowSuperseded();
|
|
1557
|
+
return grant;
|
|
1558
|
+
}.bind(this));
|
|
1559
|
+
}, KvOAuthProvider.prototype.boundGrant);
|
|
1560
|
+
registerAuthMethod(KvOAuthProvider.prototype, "activeGrant", function () {
|
|
1561
|
+
return authInvoke(this, "boundGrant", []);
|
|
1562
|
+
}, KvOAuthProvider.prototype.activeGrant);
|
|
1563
|
+
registerAuthMethod(KvOAuthProvider.prototype, "updateGrant", function (change, options = {}) {
|
|
1564
|
+
return authFlow(function* () {
|
|
1565
|
+
const signal = options.signal ?? (options.commit ? null : this.signal);
|
|
1566
|
+
for (let attempt = 0; attempt < MAX_GRANT_WRITES; attempt++) {
|
|
1567
|
+
if (!options.commit && this.signal?.aborted)
|
|
1568
|
+
return false;
|
|
1569
|
+
const { raw, grant } = (yield authInvoke(this, "readGrant", [signal]));
|
|
1570
|
+
const epoch = options.epoch ?? (this.epoch ??= grant.epoch);
|
|
1571
|
+
if (grant.epoch !== epoch) {
|
|
1572
|
+
if (options.quiet || !this.bound)
|
|
1573
|
+
return false;
|
|
1574
|
+
throw this.flowSuperseded();
|
|
1575
|
+
}
|
|
1576
|
+
// A disconnect's epoch holds nothing until an explicit start replaces it.
|
|
1577
|
+
if (isDisconnectedEpoch(epoch))
|
|
1578
|
+
return false;
|
|
1579
|
+
const next = change(grant);
|
|
1580
|
+
if (next === undefined)
|
|
1581
|
+
return true;
|
|
1582
|
+
const encoded = (yield authInvoke(this, "encodeGrant", [next, signal]));
|
|
1583
|
+
if (!options.commit && this.signal?.aborted)
|
|
1584
|
+
return false;
|
|
1585
|
+
let written;
|
|
1586
|
+
try {
|
|
1587
|
+
written = (yield authInvoke(this, "wait", [
|
|
1588
|
+
authMethod(() => {
|
|
1589
|
+
return authStorage(this.storage, "compareAndSet", [GRANT, raw, encoded], options.beforeWrite && (() => options.beforeWrite?.()));
|
|
1590
|
+
}),
|
|
1591
|
+
signal,
|
|
1592
|
+
]));
|
|
1593
|
+
}
|
|
1594
|
+
catch {
|
|
1595
|
+
if (signal?.aborted)
|
|
1596
|
+
throw aborted(signal);
|
|
1597
|
+
// A store's own error can quote the value it refused.
|
|
1598
|
+
throw this.credentialWriteError();
|
|
1599
|
+
}
|
|
1600
|
+
if (written) {
|
|
1601
|
+
if (next.epoch !== grant.epoch)
|
|
1602
|
+
this.epoch = next.epoch;
|
|
1603
|
+
return true;
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
throw this.credentialWriteError();
|
|
1607
|
+
}.bind(this));
|
|
1608
|
+
}, KvOAuthProvider.prototype.updateGrant);
|
|
1609
|
+
registerAuthMethod(KvOAuthProvider.prototype, "writeCredential", function (field, value, issuer, refreshConsent) {
|
|
1610
|
+
return authFlow(function* () {
|
|
1611
|
+
(yield authInvoke(this, "updateGrant", [
|
|
1612
|
+
(grant) => {
|
|
1613
|
+
const body = issuer !== undefined && grant.body.issuer !== issuer ? { issuer } : { ...grant.body };
|
|
1614
|
+
if (field === "client")
|
|
1615
|
+
body.client = value;
|
|
1616
|
+
else {
|
|
1617
|
+
body.tokens = value;
|
|
1618
|
+
delete body.refreshConsent;
|
|
1619
|
+
if (refreshConsent)
|
|
1620
|
+
body.refreshConsent = refreshConsent;
|
|
1621
|
+
}
|
|
1622
|
+
return this.withIssuer(grant, body);
|
|
1623
|
+
},
|
|
1624
|
+
]));
|
|
1625
|
+
// A flow may invalidate what it wrote itself.
|
|
1626
|
+
this.seen[field] = fingerprint(field === "client" ? value.value : value);
|
|
1627
|
+
}.bind(this));
|
|
1628
|
+
}, KvOAuthProvider.prototype.writeCredential);
|
|
1629
|
+
registerAuthMethod(KvOAuthProvider.prototype, "bindingForGrant", function (grant) {
|
|
1630
|
+
return authFlow(function* () {
|
|
1631
|
+
const configured = this.configuredAuthorizationBinding();
|
|
1632
|
+
return configured
|
|
1633
|
+
? { ...configured, issuer: grant.body.issuer }
|
|
1634
|
+
: {
|
|
1635
|
+
...(yield authInvoke(this, "wait", [
|
|
1636
|
+
authMethod(() => {
|
|
1637
|
+
return authCall(discoveryBinding, [grant.body.discovery]);
|
|
1638
|
+
}),
|
|
1639
|
+
])),
|
|
1640
|
+
issuer: grant.body.issuer,
|
|
1641
|
+
clientId: grant.body.client?.value.client_id,
|
|
1642
|
+
};
|
|
1643
|
+
}.bind(this));
|
|
1644
|
+
}, KvOAuthProvider.prototype.bindingForGrant);
|
|
1645
|
+
registerAuthMethod(KvOAuthProvider.prototype, "checkFlowBinding", function (grant) {
|
|
1646
|
+
return authFlow(function* () {
|
|
1647
|
+
const binding = this.callback?.flow.binding;
|
|
1648
|
+
if (!binding ||
|
|
1649
|
+
fingerprint(binding) !== fingerprint((yield authInvoke(this, "bindingForGrant", [grant])))) {
|
|
1650
|
+
throw this.flowSuperseded();
|
|
1651
|
+
}
|
|
1652
|
+
}.bind(this));
|
|
1653
|
+
}, KvOAuthProvider.prototype.checkFlowBinding);
|
|
1654
|
+
registerAuthMethod(KvOAuthProvider.prototype, "encodeFlow", function (key, flow) {
|
|
1655
|
+
return authFlow(function* () {
|
|
1656
|
+
const { verifier, ...rest } = flow;
|
|
1657
|
+
const stored = { connectaOAuthFlow: FLOW_VERSION, ...rest };
|
|
1658
|
+
if (verifier !== undefined) {
|
|
1659
|
+
if (this.sealer) {
|
|
1660
|
+
stored.verifier = (yield authInvoke(this, "wait", [
|
|
1661
|
+
authMethod(() => {
|
|
1662
|
+
return authInvoke(this.sealer, "seal", [
|
|
1663
|
+
this.flowLabel(key, flow.epoch),
|
|
1664
|
+
verifier,
|
|
1665
|
+
]);
|
|
1666
|
+
}),
|
|
1667
|
+
]));
|
|
1668
|
+
stored.sealed = true;
|
|
1669
|
+
}
|
|
1670
|
+
else {
|
|
1671
|
+
stored.verifier = verifier;
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
return JSON.stringify(stored);
|
|
1675
|
+
}.bind(this));
|
|
1676
|
+
}, KvOAuthProvider.prototype.encodeFlow);
|
|
1677
|
+
registerAuthMethod(KvOAuthProvider.prototype, "openFlow", function (key, raw, withVerifier) {
|
|
1678
|
+
return authFlow(function* () {
|
|
1679
|
+
const value = parsed(raw);
|
|
1680
|
+
if (!plainObject(value) ||
|
|
1681
|
+
value.connectaOAuthFlow !== FLOW_VERSION ||
|
|
1682
|
+
typeof value.epoch !== "string" ||
|
|
1683
|
+
typeof value.at !== "number" ||
|
|
1684
|
+
(value.url !== undefined && typeof value.url !== "string") ||
|
|
1685
|
+
(value.verifier !== undefined && typeof value.verifier !== "string")) {
|
|
1686
|
+
return undefined;
|
|
1687
|
+
}
|
|
1688
|
+
const flow = {
|
|
1689
|
+
epoch: value.epoch,
|
|
1690
|
+
at: value.at,
|
|
1691
|
+
...(plainObject(value.binding) ? { binding: value.binding } : {}),
|
|
1692
|
+
...(typeof value.url === "string" ? { url: value.url } : {}),
|
|
1693
|
+
...(value.consumed === true ? { consumed: true } : {}),
|
|
1694
|
+
};
|
|
1695
|
+
if (!withVerifier || typeof value.verifier !== "string")
|
|
1696
|
+
return flow;
|
|
1697
|
+
if (value.sealed !== true)
|
|
1698
|
+
return { ...flow, verifier: value.verifier };
|
|
1699
|
+
try {
|
|
1700
|
+
if (!this.sealer)
|
|
1701
|
+
return undefined;
|
|
1702
|
+
return {
|
|
1703
|
+
...flow,
|
|
1704
|
+
verifier: (yield authInvoke(this, "wait", [
|
|
1705
|
+
authMethod(() => {
|
|
1706
|
+
return authInvoke(this.sealer, "open", [
|
|
1707
|
+
this.flowLabel(key, flow.epoch),
|
|
1708
|
+
value.verifier,
|
|
1709
|
+
]);
|
|
1710
|
+
}),
|
|
1711
|
+
])),
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
catch {
|
|
1715
|
+
if (this.signal?.aborted)
|
|
1716
|
+
throw aborted(this.signal);
|
|
1717
|
+
this.sealer?.warn(`[connecta] connector "${this.connectorId}" has a sealed OAuth consent ` +
|
|
1718
|
+
"the configured vault cannot open; its callback is refused.");
|
|
1719
|
+
return undefined;
|
|
1720
|
+
}
|
|
1721
|
+
}.bind(this));
|
|
1722
|
+
}, KvOAuthProvider.prototype.openFlow);
|
|
1723
|
+
registerAuthMethod(KvOAuthProvider.prototype, "latestConsent", function () {
|
|
1724
|
+
return authFlow(function* () {
|
|
1725
|
+
const grant = (yield authInvoke(this, "boundGrant", []));
|
|
1726
|
+
if (grant.flow === undefined)
|
|
1727
|
+
return undefined;
|
|
1728
|
+
const key = oauthFlowKeys.flow(grant.flow);
|
|
1729
|
+
const raw = (yield authInvoke(this, "wait", [
|
|
1730
|
+
authMethod(() => {
|
|
1731
|
+
return authStorage(this.storage, "get", [key]);
|
|
1732
|
+
}),
|
|
1733
|
+
]));
|
|
1734
|
+
const flow = raw === null ? undefined : (yield authInvoke(this, "openFlow", [key, raw, false]));
|
|
1735
|
+
return flow && !flow.consumed && flow.epoch === grant.epoch && flow.url !== undefined
|
|
1736
|
+
? { ...flow, url: flow.url }
|
|
1737
|
+
: undefined;
|
|
1738
|
+
}.bind(this));
|
|
1739
|
+
}, KvOAuthProvider.prototype.latestConsent);
|
|
1740
|
+
registerAuthMethod(KvOAuthProvider.prototype, "liveEpoch", function () {
|
|
1741
|
+
return authFlow(function* () {
|
|
1742
|
+
return (yield authInvoke(this, "readGrant", [])).grant.epoch;
|
|
1743
|
+
}.bind(this));
|
|
1744
|
+
}, KvOAuthProvider.prototype.liveEpoch);
|
|
1745
|
+
registerAuthMethod(KvOAuthProvider.prototype, "operatorDisconnected", function () {
|
|
1746
|
+
return authFlow(function* () {
|
|
1747
|
+
return isDisconnectedEpoch((yield authInvoke(this, "liveEpoch", [])));
|
|
1748
|
+
}.bind(this));
|
|
1749
|
+
}, KvOAuthProvider.prototype.operatorDisconnected);
|
|
1750
|
+
registerAuthMethod(KvOAuthProvider.prototype, "beginFlow", function () {
|
|
1751
|
+
return authFlow(function* () {
|
|
1752
|
+
const epoch = (yield authInvoke(this, "liveEpoch", []));
|
|
1753
|
+
this.epoch = epoch;
|
|
1754
|
+
this.bound = true;
|
|
1755
|
+
return epoch;
|
|
1756
|
+
}.bind(this));
|
|
1757
|
+
}, KvOAuthProvider.prototype.beginFlow);
|
|
1758
|
+
registerAuthMethod(KvOAuthProvider.prototype, "flowEpoch", function () {
|
|
1759
|
+
return authFlow(function* () {
|
|
1760
|
+
return this.epoch ?? (yield authInvoke(this, "liveEpoch", []));
|
|
1761
|
+
}.bind(this));
|
|
1762
|
+
}, KvOAuthProvider.prototype.flowEpoch);
|
|
1763
|
+
registerAuthMethod(KvOAuthProvider.prototype, "bindFlow", function () {
|
|
1764
|
+
return authFlow(function* () {
|
|
1765
|
+
const { grant } = (yield authInvoke(this, "readGrant", []));
|
|
1766
|
+
const epoch = this.callback?.flow.epoch ?? this.epoch ?? grant.epoch;
|
|
1767
|
+
if (grant.epoch !== epoch)
|
|
1768
|
+
throw this.flowSuperseded();
|
|
1769
|
+
this.epoch = epoch;
|
|
1770
|
+
this.bound = true;
|
|
1771
|
+
this.codeExchange = true;
|
|
1772
|
+
(yield authInvoke(this, "checkFlowBinding", [grant]));
|
|
1773
|
+
this.seen = {
|
|
1774
|
+
client: fingerprint(grant.body.client?.value),
|
|
1775
|
+
tokens: fingerprint(grant.body.tokens),
|
|
1776
|
+
};
|
|
1777
|
+
}.bind(this));
|
|
1778
|
+
}, KvOAuthProvider.prototype.bindFlow);
|
|
1779
|
+
registerAuthMethod(KvOAuthProvider.prototype, "verifyCallbackState", function (state) {
|
|
1780
|
+
return authFlow(function* () {
|
|
1781
|
+
if (this.callback?.key ===
|
|
1782
|
+
oauthFlowKeys.flow((yield authInvoke(this, "wait", [
|
|
1783
|
+
authMethod(() => {
|
|
1784
|
+
return authCall(oauthStateDigest, [state]);
|
|
1785
|
+
}),
|
|
1786
|
+
]))))
|
|
1787
|
+
return true;
|
|
1788
|
+
return (yield authInvoke(this, "verifyState", [state]));
|
|
1789
|
+
}.bind(this));
|
|
1790
|
+
}, KvOAuthProvider.prototype.verifyCallbackState);
|
|
1791
|
+
registerAuthMethod(KvOAuthProvider.prototype, "clientInformation", function (ctx) {
|
|
1792
|
+
return authFlow(function* () {
|
|
1793
|
+
if (this.signal?.aborted)
|
|
1794
|
+
throw aborted(this.signal);
|
|
1795
|
+
const { body } = (yield authInvoke(this, "activeGrant", []));
|
|
1796
|
+
if (this.signal?.aborted)
|
|
1797
|
+
throw aborted(this.signal);
|
|
1798
|
+
const client = this.clientOptions?.client;
|
|
1799
|
+
if (!body.client && !this.allowAuthorization)
|
|
1800
|
+
throw this.authorizationRefused();
|
|
1801
|
+
if (client) {
|
|
1802
|
+
if ((ctx && ctx.issuer !== client.issuer) || body.issuer !== client.issuer)
|
|
1803
|
+
throw this.flowSuperseded();
|
|
1804
|
+
if (body.client &&
|
|
1805
|
+
(body.client.value.client_id !== client.clientId || body.client.binding !== this.clientBinding))
|
|
1806
|
+
throw this.flowSuperseded();
|
|
1807
|
+
return {
|
|
1808
|
+
client_id: client.clientId,
|
|
1809
|
+
issuer: client.issuer,
|
|
1810
|
+
token_endpoint_auth_method: this.clientMetadata.token_endpoint_auth_method,
|
|
1811
|
+
...(client.clientSecret !== undefined ? { client_secret: client.clientSecret } : {}),
|
|
1812
|
+
};
|
|
1813
|
+
}
|
|
1814
|
+
if (!body.client || (ctx && body.issuer !== ctx.issuer))
|
|
1815
|
+
return undefined;
|
|
1816
|
+
this.seen.client = fingerprint(body.client.value);
|
|
1817
|
+
return ctx ? { ...body.client.value, issuer: ctx.issuer } : body.client.value;
|
|
1818
|
+
}.bind(this));
|
|
1819
|
+
}, KvOAuthProvider.prototype.clientInformation);
|
|
1820
|
+
registerAuthMethod(KvOAuthProvider.prototype, "saveClientInformation", function (info, ctx) {
|
|
1821
|
+
return authFlow(function* () {
|
|
1822
|
+
try {
|
|
1823
|
+
const { body } = (yield authInvoke(this, "boundGrant", []));
|
|
1824
|
+
const registrationPath = this.clientOptions?.client
|
|
1825
|
+
? "static"
|
|
1826
|
+
: body.client?.value.client_id === info.client_id && body.client.registrationPath
|
|
1827
|
+
? body.client.registrationPath
|
|
1828
|
+
: this.clientMetadataUrl &&
|
|
1829
|
+
body.discovery?.authorizationServerMetadata?.client_id_metadata_document_supported === true
|
|
1830
|
+
? "cimd"
|
|
1831
|
+
: "dcr";
|
|
1832
|
+
(yield authInvoke(this, "writeCredential", [
|
|
1833
|
+
"client",
|
|
1834
|
+
{
|
|
1835
|
+
value: this.clientOptions?.client ? { client_id: this.clientOptions.client.clientId } : info,
|
|
1836
|
+
registrationPath,
|
|
1837
|
+
...(this.clientBinding !== undefined ? { binding: this.clientBinding } : {}),
|
|
1838
|
+
},
|
|
1839
|
+
ctx?.issuer,
|
|
1840
|
+
]));
|
|
1841
|
+
}
|
|
1842
|
+
catch (error) {
|
|
1843
|
+
if (!this.signal?.aborted)
|
|
1844
|
+
throw error;
|
|
1845
|
+
}
|
|
1846
|
+
}.bind(this));
|
|
1847
|
+
}, KvOAuthProvider.prototype.saveClientInformation);
|
|
1848
|
+
registerAuthMethod(KvOAuthProvider.prototype, "registrationPath", function () {
|
|
1849
|
+
return authFlow(function* () {
|
|
1850
|
+
const { body } = (yield authInvoke(this, "boundGrant", []));
|
|
1851
|
+
return body.client?.registrationPath;
|
|
1852
|
+
}.bind(this));
|
|
1853
|
+
}, KvOAuthProvider.prototype.registrationPath);
|
|
1854
|
+
registerAuthMethod(KvOAuthProvider.prototype, "validatedIssuerOrigin", function () {
|
|
1855
|
+
return authFlow(function* () {
|
|
1856
|
+
const { body } = (yield authInvoke(this, "boundGrant", []));
|
|
1857
|
+
const state = body.discovery;
|
|
1858
|
+
if (!state ||
|
|
1859
|
+
state.authorizationServerMetadata?.issuer !== state.authorizationServerUrl ||
|
|
1860
|
+
body.issuer !== state.authorizationServerUrl)
|
|
1861
|
+
return undefined;
|
|
1862
|
+
this.assertCredentialFree(state, body);
|
|
1863
|
+
return new URL(state.authorizationServerUrl).origin;
|
|
1864
|
+
}.bind(this));
|
|
1865
|
+
}, KvOAuthProvider.prototype.validatedIssuerOrigin);
|
|
1866
|
+
registerAuthMethod(KvOAuthProvider.prototype, "discoveryState", function () {
|
|
1867
|
+
return authFlow(function* () {
|
|
1868
|
+
if (this.signal?.aborted)
|
|
1869
|
+
throw aborted(this.signal);
|
|
1870
|
+
const { body } = (yield authInvoke(this, "activeGrant", []));
|
|
1871
|
+
if (this.signal?.aborted)
|
|
1872
|
+
throw aborted(this.signal);
|
|
1873
|
+
const state = body.discovery;
|
|
1874
|
+
this.assertCredentialFree(state, body);
|
|
1875
|
+
this.consentDiscovery = state;
|
|
1876
|
+
return state;
|
|
1877
|
+
}.bind(this));
|
|
1878
|
+
}, KvOAuthProvider.prototype.discoveryState);
|
|
1879
|
+
registerAuthMethod(KvOAuthProvider.prototype, "saveDiscoveryState", function (state) {
|
|
1880
|
+
return authFlow(function* () {
|
|
1881
|
+
this.assertCredentialFree(state, (yield authInvoke(this, "boundGrant", [])).body);
|
|
1882
|
+
const issuer = discoveryIssuer(state);
|
|
1883
|
+
if (this.clientOptions?.client && issuer !== this.clientOptions.client.issuer)
|
|
1884
|
+
throw this.flowSuperseded();
|
|
1885
|
+
if (!this.allowAuthorization && !(yield authInvoke(this, "boundGrant", [])).body.client)
|
|
1886
|
+
throw this.authorizationRefused();
|
|
1887
|
+
this.consentDiscovery = state;
|
|
1888
|
+
(yield authInvoke(this, "updateGrant", [
|
|
1889
|
+
(grant) => {
|
|
1890
|
+
const body = grant.body.issuer !== issuer ? (issuer !== undefined ? { issuer } : {}) : { ...grant.body };
|
|
1891
|
+
this.assertCredentialFree(state, grant.body);
|
|
1892
|
+
body.discovery = state;
|
|
1893
|
+
return this.withIssuer(grant, body);
|
|
1894
|
+
},
|
|
1895
|
+
]));
|
|
1896
|
+
}.bind(this));
|
|
1897
|
+
}, KvOAuthProvider.prototype.saveDiscoveryState);
|
|
1898
|
+
registerAuthMethod(KvOAuthProvider.prototype, "tokens", function (ctx) {
|
|
1899
|
+
return authFlow(function* () {
|
|
1900
|
+
if (this.signal?.aborted)
|
|
1901
|
+
throw aborted(this.signal);
|
|
1902
|
+
const grant = (yield authInvoke(this, "activeGrant", []));
|
|
1903
|
+
if (this.signal?.aborted)
|
|
1904
|
+
throw aborted(this.signal);
|
|
1905
|
+
const { tokens } = grant.body;
|
|
1906
|
+
if (!tokens || (ctx && grant.body.issuer !== ctx.issuer))
|
|
1907
|
+
return undefined;
|
|
1908
|
+
this.seen.tokens = fingerprint(tokens);
|
|
1909
|
+
if (!ctx)
|
|
1910
|
+
return tokens;
|
|
1911
|
+
this.refreshBasis = { accessToken: tokens.access_token, epoch: grant.epoch, issuer: ctx.issuer };
|
|
1912
|
+
return { ...tokens, issuer: ctx.issuer };
|
|
1913
|
+
}.bind(this));
|
|
1914
|
+
}, KvOAuthProvider.prototype.tokens);
|
|
1915
|
+
registerAuthMethod(KvOAuthProvider.prototype, "saveTokens", function (tokens, ctx) {
|
|
1916
|
+
return authFlow(function* () {
|
|
1917
|
+
if (this.signal?.aborted)
|
|
1918
|
+
throw aborted(this.signal);
|
|
1919
|
+
if (this.refreshFailure?.kind === "dead")
|
|
1920
|
+
throw this.authorizationRefused();
|
|
1921
|
+
// The coordinator stored this refresh already, or handed on another's.
|
|
1922
|
+
if (this.storedRefresh !== undefined && tokens.access_token === this.storedRefresh)
|
|
1923
|
+
return;
|
|
1924
|
+
const refreshConsent = this.exchangeConsent &&
|
|
1925
|
+
tokens.refresh_token !== undefined &&
|
|
1926
|
+
this.exchangeConsent.digest ===
|
|
1927
|
+
(yield authInvoke(this, "wait", [
|
|
1928
|
+
authMethod(() => {
|
|
1929
|
+
return authCall(oauthStateDigest, [tokens.refresh_token]);
|
|
1930
|
+
}),
|
|
1931
|
+
]))
|
|
1932
|
+
? this.exchangeConsent
|
|
1933
|
+
: undefined;
|
|
1934
|
+
(yield authInvoke(this, "writeCredential", ["tokens", tokens, ctx?.issuer, refreshConsent]));
|
|
1935
|
+
this.refreshFailure = undefined;
|
|
1936
|
+
}.bind(this));
|
|
1937
|
+
}, KvOAuthProvider.prototype.saveTokens);
|
|
1938
|
+
registerAuthMethod(KvOAuthProvider.prototype, "state", function () {
|
|
1939
|
+
return authValue(() => {
|
|
1940
|
+
if (!this.allowAuthorization)
|
|
1941
|
+
throw this.authorizationRefused();
|
|
1942
|
+
const state = randomState();
|
|
1943
|
+
this.consent = { state };
|
|
1944
|
+
return state;
|
|
1945
|
+
});
|
|
1946
|
+
}, KvOAuthProvider.prototype.state);
|
|
1947
|
+
registerAuthMethod(KvOAuthProvider.prototype, "saveCodeVerifier", function (verifier) {
|
|
1948
|
+
return authValue(() => {
|
|
1949
|
+
if (this.signal?.aborted)
|
|
1950
|
+
throw aborted(this.signal);
|
|
1951
|
+
if (!this.allowAuthorization)
|
|
1952
|
+
throw this.authorizationRefused();
|
|
1953
|
+
this.consent = { ...this.consent, verifier };
|
|
1954
|
+
});
|
|
1955
|
+
}, KvOAuthProvider.prototype.saveCodeVerifier);
|
|
1956
|
+
registerAuthMethod(KvOAuthProvider.prototype, "codeVerifier", function () {
|
|
1957
|
+
return authValue(() => {
|
|
1958
|
+
const verifier = this.callback?.flow.verifier ?? this.consent.verifier;
|
|
1959
|
+
if (verifier === undefined) {
|
|
1960
|
+
throw new Error(`No PKCE code verifier for "${this.connectorId}"`);
|
|
1961
|
+
}
|
|
1962
|
+
return verifier;
|
|
1963
|
+
});
|
|
1964
|
+
}, KvOAuthProvider.prototype.codeVerifier);
|
|
1965
|
+
registerAuthMethod(KvOAuthProvider.prototype, "redirectToAuthorization", function (authorizationUrl) {
|
|
1966
|
+
return authFlow(function* () {
|
|
1967
|
+
if (this.signal?.aborted)
|
|
1968
|
+
return;
|
|
1969
|
+
if (!this.allowAuthorization)
|
|
1970
|
+
throw this.authorizationRefused();
|
|
1971
|
+
this.assertCredentialFree(authorizationUrl.toString(), (yield authInvoke(this, "boundGrant", [])).body, true);
|
|
1972
|
+
const state = this.consent.state ?? authorizationUrl.searchParams.get("state");
|
|
1973
|
+
if (!state)
|
|
1974
|
+
throw new Error(`OAuth consent for "${this.connectorId}" carries no state`);
|
|
1975
|
+
const client = this.clientOptions?.client;
|
|
1976
|
+
if (client && !(yield authInvoke(this, "boundGrant", [])).body.client) {
|
|
1977
|
+
(yield authInvoke(this, "saveClientInformation", [
|
|
1978
|
+
{ client_id: client.clientId },
|
|
1979
|
+
{ issuer: client.issuer },
|
|
1980
|
+
]));
|
|
1981
|
+
}
|
|
1982
|
+
const configured = this.configuredAuthorizationBinding();
|
|
1983
|
+
if (configured?.issuer !== undefined) {
|
|
1984
|
+
const issuer = configured.issuer;
|
|
1985
|
+
// Static adapters do not save discovery or client configuration. Publish
|
|
1986
|
+
// their server identity before consent so a changed endpoint fences the
|
|
1987
|
+
// old grant before any code exchange can leave.
|
|
1988
|
+
(yield authInvoke(this, "updateGrant", [
|
|
1989
|
+
(grant) => (grant.body.issuer === issuer ? undefined : this.withIssuer(grant, { issuer })),
|
|
1990
|
+
]));
|
|
1991
|
+
}
|
|
1992
|
+
const epoch = (yield authInvoke(this, "flowEpoch", []));
|
|
1993
|
+
this.epoch = epoch;
|
|
1994
|
+
const digest = (yield authInvoke(this, "wait", [
|
|
1995
|
+
authMethod(() => {
|
|
1996
|
+
return authCall(oauthStateDigest, [state]);
|
|
1997
|
+
}),
|
|
1998
|
+
]));
|
|
1999
|
+
const key = oauthFlowKeys.flow(digest);
|
|
2000
|
+
const url = authorizationUrl.toString();
|
|
2001
|
+
const { verifier } = this.consent;
|
|
2002
|
+
const grant = (yield authInvoke(this, "boundGrant", []));
|
|
2003
|
+
this.assertCredentialFree(url, grant.body, true);
|
|
2004
|
+
const binding = this.configuredAuthorizationBinding() ?? {
|
|
2005
|
+
...(yield authInvoke(this, "wait", [
|
|
2006
|
+
authMethod(() => {
|
|
2007
|
+
return authCall(discoveryBinding, [this.consentDiscovery ?? grant.body.discovery]);
|
|
2008
|
+
}),
|
|
2009
|
+
])),
|
|
2010
|
+
issuer: discoveryIssuer(this.consentDiscovery) ?? grant.body.issuer,
|
|
2011
|
+
clientId: authorizationUrl.searchParams.get("client_id") ?? grant.body.client?.value.client_id,
|
|
2012
|
+
};
|
|
2013
|
+
const record = (yield authInvoke(this, "encodeFlow", [
|
|
2014
|
+
key,
|
|
2015
|
+
{
|
|
2016
|
+
epoch,
|
|
2017
|
+
at: Date.now(),
|
|
2018
|
+
binding,
|
|
2019
|
+
url,
|
|
2020
|
+
...(verifier !== undefined ? { verifier } : {}),
|
|
2021
|
+
},
|
|
2022
|
+
]));
|
|
2023
|
+
if (this.signal?.aborted)
|
|
2024
|
+
return;
|
|
2025
|
+
if (!(yield authInvoke(this, "wait", [
|
|
2026
|
+
authMethod(() => {
|
|
2027
|
+
return authStorage(this.storage, "compareAndSet", [
|
|
2028
|
+
key,
|
|
2029
|
+
null,
|
|
2030
|
+
record,
|
|
2031
|
+
{ ttlSeconds: OAUTH_FLOW_TTL_SECONDS },
|
|
2032
|
+
]);
|
|
2033
|
+
}),
|
|
2034
|
+
]))) {
|
|
2035
|
+
throw new Error(`OAuth consent for "${this.connectorId}" reused a state`);
|
|
2036
|
+
}
|
|
2037
|
+
let published = false;
|
|
2038
|
+
try {
|
|
2039
|
+
published = (yield authInvoke(this, "updateGrant", [
|
|
2040
|
+
(grant) => ({ ...grant, flow: digest }),
|
|
2041
|
+
{ epoch },
|
|
2042
|
+
]));
|
|
2043
|
+
}
|
|
2044
|
+
finally {
|
|
2045
|
+
if (!published)
|
|
2046
|
+
(yield authCatch(authInvoke(this, "wait", [
|
|
2047
|
+
authMethod(() => {
|
|
2048
|
+
return authStorage(this.storage, "compareAndSet", [key, record, null]);
|
|
2049
|
+
}),
|
|
2050
|
+
]), () => { }));
|
|
2051
|
+
}
|
|
2052
|
+
if (published)
|
|
2053
|
+
this.published = { epoch, url };
|
|
2054
|
+
}.bind(this));
|
|
2055
|
+
}, KvOAuthProvider.prototype.redirectToAuthorization);
|
|
2056
|
+
registerAuthMethod(KvOAuthProvider.prototype, "consentUrl", function (signal) {
|
|
2057
|
+
signal = signal === undefined ? this.signal : signal;
|
|
2058
|
+
return authFlow(function* () {
|
|
2059
|
+
const published = this.published;
|
|
2060
|
+
if (!published)
|
|
2061
|
+
return undefined;
|
|
2062
|
+
if (epochOf((yield authInvoke(this, "wait", [
|
|
2063
|
+
authMethod(() => {
|
|
2064
|
+
return authStorage(this.storage, "get", [GRANT]);
|
|
2065
|
+
}),
|
|
2066
|
+
signal,
|
|
2067
|
+
]))) !== published.epoch)
|
|
2068
|
+
throw this.flowSuperseded();
|
|
2069
|
+
this.assertCredentialFree(published.url, (yield authInvoke(this, "boundGrant", [signal])).body, true);
|
|
2070
|
+
return published.url;
|
|
2071
|
+
}.bind(this));
|
|
2072
|
+
}, KvOAuthProvider.prototype.consentUrl);
|
|
2073
|
+
registerAuthMethod(KvOAuthProvider.prototype, "pendingAuthorizationUrl", function () {
|
|
2074
|
+
return authFlow(function* () {
|
|
2075
|
+
const url = (yield authInvoke(this, "latestConsent", []))?.url;
|
|
2076
|
+
this.assertCredentialFree(url, (yield authInvoke(this, "boundGrant", [])).body, true);
|
|
2077
|
+
return url;
|
|
2078
|
+
}.bind(this));
|
|
2079
|
+
}, KvOAuthProvider.prototype.pendingAuthorizationUrl);
|
|
2080
|
+
registerAuthMethod(KvOAuthProvider.prototype, "reusablePendingAuthorizationUrl", function () {
|
|
2081
|
+
return authFlow(function* () {
|
|
2082
|
+
const consent = (yield authInvoke(this, "latestConsent", []));
|
|
2083
|
+
if (!consent || Math.abs(Date.now() - consent.at) >= PENDING_AUTHORIZATION_MAX_AGE_MS) {
|
|
2084
|
+
return undefined;
|
|
2085
|
+
}
|
|
2086
|
+
this.assertCredentialFree(consent.url, (yield authInvoke(this, "boundGrant", [])).body, true);
|
|
2087
|
+
let clientId;
|
|
2088
|
+
try {
|
|
2089
|
+
clientId = new URL(consent.url).searchParams.get("client_id");
|
|
2090
|
+
}
|
|
2091
|
+
catch {
|
|
2092
|
+
return undefined;
|
|
2093
|
+
}
|
|
2094
|
+
const client = (yield authInvoke(this, "clientInformation", []));
|
|
2095
|
+
return clientId !== null && client?.client_id === clientId ? consent.url : undefined;
|
|
2096
|
+
}.bind(this));
|
|
2097
|
+
}, KvOAuthProvider.prototype.reusablePendingAuthorizationUrl);
|
|
2098
|
+
registerAuthMethod(KvOAuthProvider.prototype, "verifyState", function (candidate) {
|
|
2099
|
+
return authFlow(function* () {
|
|
2100
|
+
this.callback = undefined;
|
|
2101
|
+
if (candidate === null)
|
|
2102
|
+
return false;
|
|
2103
|
+
const key = oauthFlowKeys.flow((yield authInvoke(this, "wait", [
|
|
2104
|
+
authMethod(() => {
|
|
2105
|
+
return authCall(oauthStateDigest, [candidate]);
|
|
2106
|
+
}),
|
|
2107
|
+
])));
|
|
2108
|
+
const raw = (yield authInvoke(this, "wait", [
|
|
2109
|
+
authMethod(() => {
|
|
2110
|
+
return authStorage(this.storage, "get", [key]);
|
|
2111
|
+
}),
|
|
2112
|
+
]));
|
|
2113
|
+
const flow = raw === null ? undefined : (yield authInvoke(this, "openFlow", [key, raw, true]));
|
|
2114
|
+
if (raw === null || !flow || flow.consumed)
|
|
2115
|
+
return false;
|
|
2116
|
+
this.callback = { key, raw, flow, claimed: false };
|
|
2117
|
+
this.epoch = flow.epoch;
|
|
2118
|
+
return true;
|
|
2119
|
+
}.bind(this));
|
|
2120
|
+
}, KvOAuthProvider.prototype.verifyState);
|
|
2121
|
+
registerAuthMethod(KvOAuthProvider.prototype, "claimCodeExchange", function () {
|
|
2122
|
+
return authFlow(function* () {
|
|
2123
|
+
if (this.exchangeRefusal)
|
|
2124
|
+
return this.exchangeRefusal.clone();
|
|
2125
|
+
(yield authInvoke(this, "claimConsent", []));
|
|
2126
|
+
return undefined;
|
|
2127
|
+
}.bind(this));
|
|
2128
|
+
}, KvOAuthProvider.prototype.claimCodeExchange);
|
|
2129
|
+
registerAuthMethod(KvOAuthProvider.prototype, "consumeAuthError", function () {
|
|
2130
|
+
return authFlow(function* () {
|
|
2131
|
+
const { grant } = (yield authInvoke(this, "readGrant", []));
|
|
2132
|
+
if (!this.callback || grant.epoch !== this.callback.flow.epoch)
|
|
2133
|
+
throw this.flowSuperseded();
|
|
2134
|
+
(yield authInvoke(this, "checkFlowBinding", [grant]));
|
|
2135
|
+
(yield authInvoke(this, "claimConsent", []));
|
|
2136
|
+
this.callback = undefined;
|
|
2137
|
+
}.bind(this));
|
|
2138
|
+
}, KvOAuthProvider.prototype.consumeAuthError);
|
|
2139
|
+
registerAuthMethod(KvOAuthProvider.prototype, "claimConsent", function () {
|
|
2140
|
+
return authFlow(function* () {
|
|
2141
|
+
const callback = this.callback;
|
|
2142
|
+
if (!callback)
|
|
2143
|
+
throw this.flowSuperseded();
|
|
2144
|
+
if (callback.claimed)
|
|
2145
|
+
throw new OAuthCallbackClaimedError(this.connectorId);
|
|
2146
|
+
callback.claimed = true;
|
|
2147
|
+
const { epoch, at } = callback.flow;
|
|
2148
|
+
const claimed = JSON.stringify({
|
|
2149
|
+
connectaOAuthFlow: FLOW_VERSION,
|
|
2150
|
+
epoch,
|
|
2151
|
+
at,
|
|
2152
|
+
consumed: true,
|
|
2153
|
+
});
|
|
2154
|
+
const remaining = Math.ceil((at + OAUTH_FLOW_TTL_SECONDS * 1000 - Date.now()) / 1000);
|
|
2155
|
+
const ttlSeconds = Math.min(OAUTH_FLOW_TTL_SECONDS, Math.max(1, remaining));
|
|
2156
|
+
if ((yield authInvoke(this, "wait", [
|
|
2157
|
+
authMethod(() => {
|
|
2158
|
+
return authStorage(this.storage, "compareAndSet", [
|
|
2159
|
+
callback.key,
|
|
2160
|
+
callback.raw,
|
|
2161
|
+
claimed,
|
|
2162
|
+
{ ttlSeconds },
|
|
2163
|
+
]);
|
|
2164
|
+
}),
|
|
2165
|
+
]))) {
|
|
2166
|
+
return;
|
|
2167
|
+
}
|
|
2168
|
+
if (epochOf((yield authInvoke(this, "wait", [
|
|
2169
|
+
authMethod(() => {
|
|
2170
|
+
return authStorage(this.storage, "get", [GRANT]);
|
|
2171
|
+
}),
|
|
2172
|
+
]))) !== epoch)
|
|
2173
|
+
throw this.flowSuperseded();
|
|
2174
|
+
throw new OAuthCallbackClaimedError(this.connectorId);
|
|
2175
|
+
}.bind(this));
|
|
2176
|
+
}, KvOAuthProvider.prototype.claimConsent);
|
|
2177
|
+
registerAuthMethod(KvOAuthProvider.prototype, "dispatchCodeExchange", function (send, endpoint) {
|
|
2178
|
+
return authFlow(function* () {
|
|
2179
|
+
const epoch = this.epoch;
|
|
2180
|
+
if (!this.codeExchange || epoch === undefined)
|
|
2181
|
+
throw this.flowSuperseded();
|
|
2182
|
+
const raw = (yield authInvoke(this, "wait", [
|
|
2183
|
+
authMethod(() => {
|
|
2184
|
+
return authStorage(this.storage, "get", [GRANT]);
|
|
2185
|
+
}),
|
|
2186
|
+
]));
|
|
2187
|
+
const grant = (yield authInvoke(this, "openGrant", [raw]));
|
|
2188
|
+
if (grant.epoch !== epoch)
|
|
2189
|
+
throw this.flowSuperseded();
|
|
2190
|
+
(yield authInvoke(this, "checkFlowBinding", [grant]));
|
|
2191
|
+
if (endpoint !== undefined && this.callback?.flow.binding?.tokenEndpoint !== String(endpoint)) {
|
|
2192
|
+
throw this.flowSuperseded();
|
|
2193
|
+
}
|
|
2194
|
+
// Opening ciphertext may yield. Re-read the exact record after opening,
|
|
2195
|
+
// then check and send in the same reaction, without another await.
|
|
2196
|
+
return (yield authInvoke(this, "wait", [
|
|
2197
|
+
authMethod(() => {
|
|
2198
|
+
return authThen(authStorage(this.storage, "get", [GRANT]), authMethod((current) => {
|
|
2199
|
+
return authFlow(function* () {
|
|
2200
|
+
if (this.signal?.aborted)
|
|
2201
|
+
throw aborted(this.signal);
|
|
2202
|
+
if (current !== raw)
|
|
2203
|
+
throw this.flowSuperseded();
|
|
2204
|
+
const sent = send();
|
|
2205
|
+
sent.catch(() => { });
|
|
2206
|
+
return (yield authValue(() => sent));
|
|
2207
|
+
}.bind(this));
|
|
2208
|
+
}));
|
|
2209
|
+
}),
|
|
2210
|
+
]));
|
|
2211
|
+
}.bind(this));
|
|
2212
|
+
}, KvOAuthProvider.prototype.dispatchCodeExchange);
|
|
2213
|
+
registerAuthMethod(KvOAuthProvider.prototype, "recordCodeExchangeTokens", function (tokens) {
|
|
2214
|
+
return authFlow(function* () {
|
|
2215
|
+
this.exchangeConsent = undefined;
|
|
2216
|
+
if (tokens.refresh_token === undefined)
|
|
2217
|
+
return;
|
|
2218
|
+
try {
|
|
2219
|
+
const digest = (yield authInvoke(this, "wait", [
|
|
2220
|
+
authMethod(() => {
|
|
2221
|
+
return authCall(oauthStateDigest, [tokens.refresh_token]);
|
|
2222
|
+
}),
|
|
2223
|
+
]));
|
|
2224
|
+
const resolution = (yield authInvoke(this, "wait", [
|
|
2225
|
+
authMethod(() => {
|
|
2226
|
+
return authStorage(this.storage, "get", [oauthRefreshSpentKeys.spent(digest)]);
|
|
2227
|
+
}),
|
|
2228
|
+
]));
|
|
2229
|
+
const spent = storedRefreshSpent(resolution);
|
|
2230
|
+
// Observing the exact resolved storage write before consent completion
|
|
2231
|
+
// proves their order without comparing isolate clocks. A consent that
|
|
2232
|
+
// saw outstanding/ambiguous cannot become eligible on a delayed save.
|
|
2233
|
+
if (spent?.state === "resolved")
|
|
2234
|
+
this.exchangeConsent = { digest, resolution: resolution };
|
|
2235
|
+
}
|
|
2236
|
+
catch {
|
|
2237
|
+
if (this.signal?.aborted)
|
|
2238
|
+
throw aborted(this.signal);
|
|
2239
|
+
throw this.credentialWriteError();
|
|
2240
|
+
}
|
|
2241
|
+
}.bind(this));
|
|
2242
|
+
}, KvOAuthProvider.prototype.recordCodeExchangeTokens);
|
|
2243
|
+
registerAuthMethod(KvOAuthProvider.prototype, "invalidateCredentials", function (scope) {
|
|
2244
|
+
return authFlow(function* () {
|
|
2245
|
+
if (this.signal?.aborted)
|
|
2246
|
+
throw aborted(this.signal);
|
|
2247
|
+
const fields = {
|
|
2248
|
+
all: ["client", "tokens", "discovery"],
|
|
2249
|
+
client: ["client"],
|
|
2250
|
+
tokens: ["tokens"],
|
|
2251
|
+
discovery: ["discovery"],
|
|
2252
|
+
verifier: [],
|
|
2253
|
+
}[scope];
|
|
2254
|
+
if (fields.length > 0) {
|
|
2255
|
+
(yield authInvoke(this, "updateGrant", [
|
|
2256
|
+
(grant) => {
|
|
2257
|
+
const body = { ...grant.body };
|
|
2258
|
+
let changed = false;
|
|
2259
|
+
for (const field of fields) {
|
|
2260
|
+
const current = field === "client" ? body.client?.value : body[field];
|
|
2261
|
+
if (current === undefined)
|
|
2262
|
+
continue;
|
|
2263
|
+
if (field !== "discovery" && fingerprint(current) !== this.seen[field])
|
|
2264
|
+
continue;
|
|
2265
|
+
delete body[field];
|
|
2266
|
+
changed = true;
|
|
2267
|
+
}
|
|
2268
|
+
return changed ? { ...grant, body } : undefined;
|
|
2269
|
+
},
|
|
2270
|
+
{ quiet: true },
|
|
2271
|
+
]));
|
|
2272
|
+
}
|
|
2273
|
+
// The SDK invalidates after a refused refresh and then starts over, which
|
|
2274
|
+
// on `invalid_client` means registering a new client. A passive request
|
|
2275
|
+
// must not begin authorization, so it ends here, as auth_required.
|
|
2276
|
+
if (!this.allowAuthorization && this.refreshFailure?.kind === "dead") {
|
|
2277
|
+
throw this.authorizationRefused();
|
|
2278
|
+
}
|
|
2279
|
+
}.bind(this));
|
|
2280
|
+
}, KvOAuthProvider.prototype.invalidateCredentials);
|
|
2281
|
+
registerAuthMethod(KvOAuthProvider.prototype, "claimRefresh", function (epoch, requested, signal, sharedWait) {
|
|
2282
|
+
return authFlow(function* () {
|
|
2283
|
+
const waiting = sharedWait ?? refreshWait(signal);
|
|
2284
|
+
const wait = waiting.run;
|
|
2285
|
+
let delay = 10;
|
|
2286
|
+
try {
|
|
2287
|
+
const digest = (yield authCall(wait, [
|
|
2288
|
+
authMethod(() => {
|
|
2289
|
+
return authCall(oauthStateDigest, [requested ?? ""]);
|
|
2290
|
+
}),
|
|
2291
|
+
]));
|
|
2292
|
+
const key = oauthRefreshKeys.lease(epoch, digest);
|
|
2293
|
+
const spentKey = oauthRefreshSpentKeys.spent(digest);
|
|
2294
|
+
while (!waiting.signal.aborted) {
|
|
2295
|
+
const current = (yield authCall(wait, [
|
|
2296
|
+
authMethod(() => {
|
|
2297
|
+
return authInvoke(this, "storedTokens", [waiting.signal]);
|
|
2298
|
+
}),
|
|
2299
|
+
]));
|
|
2300
|
+
if (current?.refresh_token !== requested || (current && this.refreshedSinceRead(current, epoch))) {
|
|
2301
|
+
if (!current)
|
|
2302
|
+
this.recordRefreshFailure({ kind: "dead" });
|
|
2303
|
+
return current
|
|
2304
|
+
? this.adoptRefresh(current)
|
|
2305
|
+
: Response.json({ error: "invalid_grant" }, { status: 400 });
|
|
2306
|
+
}
|
|
2307
|
+
const raw = (yield authCall(wait, [
|
|
2308
|
+
authMethod(() => {
|
|
2309
|
+
return authStorage(this.storage, "get", [key]);
|
|
2310
|
+
}),
|
|
2311
|
+
]));
|
|
2312
|
+
const stored = raw === null ? undefined : parsed(raw);
|
|
2313
|
+
if (plainObject(stored) &&
|
|
2314
|
+
stored.completed === true &&
|
|
2315
|
+
current &&
|
|
2316
|
+
stored.tokenDigest ===
|
|
2317
|
+
(yield authCall(wait, [
|
|
2318
|
+
authMethod(() => {
|
|
2319
|
+
return authCall(refreshTokensDigest, [current]);
|
|
2320
|
+
}),
|
|
2321
|
+
]))) {
|
|
2322
|
+
return this.adoptRefresh(current);
|
|
2323
|
+
}
|
|
2324
|
+
if (plainObject(stored) && (plainObject(stored.verdict) || stored.completed === true)) {
|
|
2325
|
+
this.recordRefreshFailure({ kind: "dead" });
|
|
2326
|
+
(yield authCall(wait, [
|
|
2327
|
+
authMethod(() => {
|
|
2328
|
+
return authInvoke(this, "discardRefusedGrant", [
|
|
2329
|
+
requested,
|
|
2330
|
+
epoch,
|
|
2331
|
+
waiting.transfer,
|
|
2332
|
+
waiting.signal,
|
|
2333
|
+
]);
|
|
2334
|
+
}),
|
|
2335
|
+
]));
|
|
2336
|
+
throw new UnauthorizedError("OAuth refresh grant is no longer active.");
|
|
2337
|
+
}
|
|
2338
|
+
if (plainObject(stored) && stored.state !== "claimed") {
|
|
2339
|
+
// Never compare another isolate's clock for a sent request. The
|
|
2340
|
+
// durable record survives expiry of this storage-owned liveness key.
|
|
2341
|
+
const active = typeof stored.activeKey === "string"
|
|
2342
|
+
? (yield authCall(wait, [
|
|
2343
|
+
authMethod(() => {
|
|
2344
|
+
return authStorage(this.storage, "get", [stored.activeKey]);
|
|
2345
|
+
}),
|
|
2346
|
+
]))
|
|
2347
|
+
: null;
|
|
2348
|
+
if (active === null) {
|
|
2349
|
+
const refused = JSON.stringify({ ...stored, verdict: { kind: "dead" } });
|
|
2350
|
+
if ((yield authCall(wait, [
|
|
2351
|
+
authMethod(() => {
|
|
2352
|
+
return authStorage(this.storage, "compareAndSet", [key, raw, refused], () => waiting.transfer());
|
|
2353
|
+
}),
|
|
2354
|
+
]))) {
|
|
2355
|
+
(yield authCall(wait, [
|
|
2356
|
+
authMethod(() => {
|
|
2357
|
+
return authInvoke(this, "resolveRefreshSpent", [
|
|
2358
|
+
{ key, spentKey, spentExpected: null, raw: raw },
|
|
2359
|
+
"ambiguous",
|
|
2360
|
+
]);
|
|
2361
|
+
}),
|
|
2362
|
+
]));
|
|
2363
|
+
this.recordRefreshFailure({ kind: "dead" });
|
|
2364
|
+
(yield authCall(wait, [
|
|
2365
|
+
authMethod(() => {
|
|
2366
|
+
return authInvoke(this, "discardRefusedGrant", [
|
|
2367
|
+
requested,
|
|
2368
|
+
epoch,
|
|
2369
|
+
waiting.transfer,
|
|
2370
|
+
waiting.signal,
|
|
2371
|
+
]);
|
|
2372
|
+
}),
|
|
2373
|
+
]));
|
|
2374
|
+
throw new UnauthorizedError("OAuth refresh outcome is unknown; authorization required.");
|
|
2375
|
+
}
|
|
2376
|
+
continue;
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
else {
|
|
2380
|
+
const spentExpected = (yield authCall(wait, [
|
|
2381
|
+
authMethod(() => {
|
|
2382
|
+
return authStorage(this.storage, "get", [spentKey]);
|
|
2383
|
+
}),
|
|
2384
|
+
]));
|
|
2385
|
+
if (spentExpected !== null) {
|
|
2386
|
+
const spent = storedRefreshSpent(spentExpected);
|
|
2387
|
+
const consent = (yield authCall(wait, [
|
|
2388
|
+
authMethod(() => {
|
|
2389
|
+
return authInvoke(this, "boundGrant", [waiting.signal]);
|
|
2390
|
+
}),
|
|
2391
|
+
])).body.refreshConsent;
|
|
2392
|
+
if (spent?.state !== "resolved" ||
|
|
2393
|
+
spent.epoch === epoch ||
|
|
2394
|
+
consent?.digest !== digest ||
|
|
2395
|
+
consent.resolution !== spentExpected) {
|
|
2396
|
+
// Reset can sweep the old lease while its request remains live.
|
|
2397
|
+
// Only storage-owned expiry can make an outstanding send ambiguous.
|
|
2398
|
+
if (spent?.state === "outstanding" &&
|
|
2399
|
+
(yield authCall(wait, [
|
|
2400
|
+
authMethod(() => {
|
|
2401
|
+
return authStorage(this.storage, "get", [spent.activeKey]);
|
|
2402
|
+
}),
|
|
2403
|
+
])) === null) {
|
|
2404
|
+
(yield authCall(wait, [
|
|
2405
|
+
authMethod(() => {
|
|
2406
|
+
return authStorage(this.storage, "compareAndSet", [spentKey, spentExpected, JSON.stringify({ ...spent, state: "ambiguous" })], () => waiting.transfer());
|
|
2407
|
+
}),
|
|
2408
|
+
]));
|
|
2409
|
+
}
|
|
2410
|
+
this.recordRefreshFailure({ kind: "dead" });
|
|
2411
|
+
(yield authCall(wait, [
|
|
2412
|
+
authMethod(() => {
|
|
2413
|
+
return authInvoke(this, "discardRefusedGrant", [
|
|
2414
|
+
requested,
|
|
2415
|
+
epoch,
|
|
2416
|
+
waiting.transfer,
|
|
2417
|
+
waiting.signal,
|
|
2418
|
+
]);
|
|
2419
|
+
}),
|
|
2420
|
+
]));
|
|
2421
|
+
throw new UnauthorizedError("OAuth refresh token was already spent; authorization required.");
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
if (raw === null ||
|
|
2425
|
+
(plainObject(stored) &&
|
|
2426
|
+
typeof stored.expiresAt === "number" &&
|
|
2427
|
+
stored.expiresAt <= this.refreshNow())) {
|
|
2428
|
+
// Clock skew can replace an unsent claim; its dispatch CAS then loses.
|
|
2429
|
+
const claimed = JSON.stringify({
|
|
2430
|
+
connectaOAuthRefresh: 1,
|
|
2431
|
+
holder: crypto.randomUUID(),
|
|
2432
|
+
state: "claimed",
|
|
2433
|
+
expiresAt: this.refreshNow() + OAUTH_REFRESH_LEASE_SECONDS * 1000,
|
|
2434
|
+
});
|
|
2435
|
+
let won;
|
|
2436
|
+
try {
|
|
2437
|
+
won = (yield authCall(wait, [
|
|
2438
|
+
authMethod(() => {
|
|
2439
|
+
return authStorage(this.storage, "compareAndSet", [key, raw, claimed], () => waiting.transfer());
|
|
2440
|
+
}),
|
|
2441
|
+
]));
|
|
2442
|
+
}
|
|
2443
|
+
catch {
|
|
2444
|
+
throw this.credentialWriteError();
|
|
2445
|
+
}
|
|
2446
|
+
if (won) {
|
|
2447
|
+
try {
|
|
2448
|
+
const latest = (yield authCall(wait, [
|
|
2449
|
+
authMethod(() => {
|
|
2450
|
+
return authInvoke(this, "storedTokens", [waiting.signal]);
|
|
2451
|
+
}),
|
|
2452
|
+
]));
|
|
2453
|
+
if (latest?.refresh_token !== requested || (latest && this.refreshedSinceRead(latest, epoch))) {
|
|
2454
|
+
(yield authCall(wait, [
|
|
2455
|
+
authMethod(() => {
|
|
2456
|
+
return authInvoke(this, "releaseRefresh", [
|
|
2457
|
+
{ key, spentKey, spentExpected, raw: claimed },
|
|
2458
|
+
]);
|
|
2459
|
+
}),
|
|
2460
|
+
]));
|
|
2461
|
+
return latest
|
|
2462
|
+
? this.adoptRefresh(latest)
|
|
2463
|
+
: Response.json({ error: "invalid_grant" }, { status: 400 });
|
|
2464
|
+
}
|
|
2465
|
+
return { key, spentKey, spentExpected, raw: claimed };
|
|
2466
|
+
}
|
|
2467
|
+
catch (error) {
|
|
2468
|
+
if (!waiting.signal.aborted) {
|
|
2469
|
+
(yield authCall(wait, [
|
|
2470
|
+
authMethod(() => {
|
|
2471
|
+
return authInvoke(this, "releaseRefresh", [
|
|
2472
|
+
{ key, spentKey, spentExpected, raw: claimed },
|
|
2473
|
+
]);
|
|
2474
|
+
}),
|
|
2475
|
+
]));
|
|
2476
|
+
}
|
|
2477
|
+
throw error;
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
(yield Effect.raceAllFirst([Effect.sleep(delay), fromSignal(waiting.signal)]));
|
|
2483
|
+
delay = Math.min(250, delay * 2);
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
catch (error) {
|
|
2487
|
+
if (!waiting.signal.aborted)
|
|
2488
|
+
throw error;
|
|
2489
|
+
}
|
|
2490
|
+
finally {
|
|
2491
|
+
if (!sharedWait)
|
|
2492
|
+
waiting.close();
|
|
2493
|
+
}
|
|
2494
|
+
throw this.refreshContended();
|
|
2495
|
+
}.bind(this));
|
|
2496
|
+
}, KvOAuthProvider.prototype.claimRefresh);
|
|
2497
|
+
registerAuthMethod(KvOAuthProvider.prototype, "dispatchRefresh", function (lease, send, waiting) {
|
|
2498
|
+
return authFlow(function* () {
|
|
2499
|
+
const stored = parsed(lease.raw);
|
|
2500
|
+
const epoch = (yield authInvoke(waiting, "run", [
|
|
2501
|
+
authMethod(() => {
|
|
2502
|
+
return authInvoke(this, "flowEpoch", []);
|
|
2503
|
+
}),
|
|
2504
|
+
]));
|
|
2505
|
+
const activeKey = oauthRefreshActiveKeys.holder(epoch, stored.holder);
|
|
2506
|
+
(yield authInvoke(waiting, "run", [
|
|
2507
|
+
authMethod(() => {
|
|
2508
|
+
return authStorage(this.storage, "set", [
|
|
2509
|
+
activeKey,
|
|
2510
|
+
JSON.stringify({ connectaOAuthRefresh: 1 }),
|
|
2511
|
+
{
|
|
2512
|
+
ttlSeconds: OAUTH_REFRESH_LEASE_SECONDS,
|
|
2513
|
+
},
|
|
2514
|
+
]);
|
|
2515
|
+
}),
|
|
2516
|
+
]));
|
|
2517
|
+
const dispatched = JSON.stringify({
|
|
2518
|
+
...stored,
|
|
2519
|
+
state: "dispatched",
|
|
2520
|
+
activeKey,
|
|
2521
|
+
});
|
|
2522
|
+
const won = (yield authInvoke(waiting, "run", [
|
|
2523
|
+
authMethod(() => authStorage(this.storage, "compareAndSet", [lease.key, lease.raw, dispatched])),
|
|
2524
|
+
]));
|
|
2525
|
+
if (!won)
|
|
2526
|
+
throw this.refreshContended();
|
|
2527
|
+
lease.raw = dispatched;
|
|
2528
|
+
if (waiting.signal.aborted)
|
|
2529
|
+
throw aborted(waiting.signal);
|
|
2530
|
+
// One CAS owns the fingerprint across every epoch. Only a later consent
|
|
2531
|
+
// that observed a resolved write may replace it. No TTL or deletion path.
|
|
2532
|
+
lease.spentRaw = JSON.stringify({
|
|
2533
|
+
connectaOAuthRefreshSpent: 1,
|
|
2534
|
+
epoch,
|
|
2535
|
+
holder: stored.holder,
|
|
2536
|
+
activeKey,
|
|
2537
|
+
state: "outstanding",
|
|
2538
|
+
});
|
|
2539
|
+
let unspent;
|
|
2540
|
+
try {
|
|
2541
|
+
unspent = (yield authInvoke(waiting, "run", [
|
|
2542
|
+
authMethod(() => authStorage(this.storage, "compareAndSet", [
|
|
2543
|
+
lease.spentKey,
|
|
2544
|
+
lease.spentExpected,
|
|
2545
|
+
lease.spentRaw,
|
|
2546
|
+
])),
|
|
2547
|
+
]));
|
|
2548
|
+
}
|
|
2549
|
+
catch {
|
|
2550
|
+
this.recordRefreshFailure({ kind: "dead" });
|
|
2551
|
+
throw new UnauthorizedError("OAuth refresh dispatch could not be recorded; authorization required.");
|
|
2552
|
+
}
|
|
2553
|
+
if (!unspent) {
|
|
2554
|
+
this.recordRefreshFailure({ kind: "dead" });
|
|
2555
|
+
throw new UnauthorizedError("OAuth refresh token was already spent; authorization required.");
|
|
2556
|
+
}
|
|
2557
|
+
// Final owner check and physical submission share one reaction.
|
|
2558
|
+
// Submission already transferred ownership to the coordinator. Only
|
|
2559
|
+
// its own deadline can stop this send; the departed caller cannot.
|
|
2560
|
+
if (waiting.signal.aborted)
|
|
2561
|
+
throw aborted(waiting.signal);
|
|
2562
|
+
return (yield authCall(send, []));
|
|
2563
|
+
}.bind(this));
|
|
2564
|
+
}, KvOAuthProvider.prototype.dispatchRefresh);
|
|
2565
|
+
registerAuthMethod(KvOAuthProvider.prototype, "releaseRefresh", function (lease, verdict, tokens, resolution = "ambiguous") {
|
|
2566
|
+
return authFlow(function* () {
|
|
2567
|
+
const stored = parsed(lease.raw);
|
|
2568
|
+
// Only an unsent claim may be removed. Dispatched outcomes never reopen it.
|
|
2569
|
+
const next = stored.state === "claimed"
|
|
2570
|
+
? null
|
|
2571
|
+
: JSON.stringify({
|
|
2572
|
+
...stored,
|
|
2573
|
+
...(tokens
|
|
2574
|
+
? {
|
|
2575
|
+
completed: true,
|
|
2576
|
+
tokenDigest: (yield authCall(refreshTokensDigest, [tokens])),
|
|
2577
|
+
}
|
|
2578
|
+
: { verdict: { kind: "dead" } }),
|
|
2579
|
+
});
|
|
2580
|
+
// Verdicts contain fixed typed facts; no downstream causes or text.
|
|
2581
|
+
if (verdict)
|
|
2582
|
+
this.recordRefreshFailure(verdict);
|
|
2583
|
+
(yield authInvoke(this, "resolveRefreshSpent", [lease, resolution]));
|
|
2584
|
+
const released = (yield authCatch(authStorage(this.storage, "compareAndSet", [lease.key, lease.raw, next]), () => false));
|
|
2585
|
+
if (released && stored.activeKey)
|
|
2586
|
+
(yield authCatch(authStorage(this.storage, "delete", [stored.activeKey]), () => { }));
|
|
2587
|
+
}.bind(this));
|
|
2588
|
+
}, KvOAuthProvider.prototype.releaseRefresh);
|
|
2589
|
+
registerAuthMethod(KvOAuthProvider.prototype, "resolveRefreshSpent", function (lease, state) {
|
|
2590
|
+
return authFlow(function* () {
|
|
2591
|
+
try {
|
|
2592
|
+
const raw = lease.spentRaw ?? (yield authStorage(this.storage, "get", [lease.spentKey]));
|
|
2593
|
+
const spent = storedRefreshSpent(raw);
|
|
2594
|
+
const holder = parsed(lease.raw).holder;
|
|
2595
|
+
if (spent?.state !== "outstanding" || spent.holder !== holder)
|
|
2596
|
+
return;
|
|
2597
|
+
// The timestamp belongs to this resolution write, never to dispatch.
|
|
2598
|
+
// Consent copies the exact stored receipt, so wall clocks grant no sends.
|
|
2599
|
+
(yield authStorage(this.storage, "compareAndSet", [
|
|
2600
|
+
lease.spentKey,
|
|
2601
|
+
raw,
|
|
2602
|
+
JSON.stringify({
|
|
2603
|
+
...spent,
|
|
2604
|
+
state,
|
|
2605
|
+
...(state === "resolved" ? { resolvedAt: Date.now() } : {}),
|
|
2606
|
+
}),
|
|
2607
|
+
]));
|
|
2608
|
+
}
|
|
2609
|
+
catch {
|
|
2610
|
+
// A lost resolution answer leaves outstanding or the durable outcome;
|
|
2611
|
+
// either blocks replay until a later consent observes a resolved write.
|
|
2612
|
+
}
|
|
2613
|
+
}.bind(this));
|
|
2614
|
+
}, KvOAuthProvider.prototype.resolveRefreshSpent);
|
|
2615
|
+
registerAuthMethod(KvOAuthProvider.prototype, "storedTokens", function (signal = null) {
|
|
2616
|
+
return authFlow(function* () {
|
|
2617
|
+
return (yield authInvoke(this, "boundGrant", [signal])).body.tokens;
|
|
2618
|
+
}.bind(this));
|
|
2619
|
+
}, KvOAuthProvider.prototype.storedTokens);
|
|
2620
|
+
registerAuthMethod(KvOAuthProvider.prototype, "storeRefresh", function (epoch, requested, tokens, issuer) {
|
|
2621
|
+
return authFlow(function* () {
|
|
2622
|
+
for (let attempt = 0; attempt < MAX_GRANT_WRITES; attempt++) {
|
|
2623
|
+
try {
|
|
2624
|
+
const { raw, grant } = (yield authInvoke(this, "readGrant", [null]));
|
|
2625
|
+
if (grant.epoch !== epoch || (issuer !== undefined && grant.body.issuer !== issuer))
|
|
2626
|
+
return undefined;
|
|
2627
|
+
if (grant.body.tokens?.refresh_token !== requested)
|
|
2628
|
+
return grant.body.tokens;
|
|
2629
|
+
const committed = issuer !== undefined ? { ...tokens, issuer } : tokens;
|
|
2630
|
+
const encoded = (yield authInvoke(this, "encodeGrant", [
|
|
2631
|
+
{ ...grant, body: { ...grant.body, tokens: committed } },
|
|
2632
|
+
null,
|
|
2633
|
+
]));
|
|
2634
|
+
if ((yield authStorage(this.storage, "compareAndSet", [GRANT, raw, encoded])))
|
|
2635
|
+
return committed;
|
|
2636
|
+
}
|
|
2637
|
+
catch {
|
|
2638
|
+
// A failed or lost CAS answer may still have committed. Re-read the
|
|
2639
|
+
// epoch and retry these in-memory tokens without another HTTP request.
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
throw this.credentialWriteError();
|
|
2643
|
+
}.bind(this));
|
|
2644
|
+
}, KvOAuthProvider.prototype.storeRefresh);
|
|
2645
|
+
registerAuthMethod(KvOAuthProvider.prototype, "discardRefusedGrant", function (refreshToken, epoch, beforeWrite, signal) {
|
|
2646
|
+
return authFlow(function* () {
|
|
2647
|
+
try {
|
|
2648
|
+
(yield authInvoke(this, "updateGrant", [
|
|
2649
|
+
(grant) => {
|
|
2650
|
+
if (grant.body.tokens?.refresh_token !== refreshToken)
|
|
2651
|
+
return undefined;
|
|
2652
|
+
const { tokens: _refused, ...body } = grant.body;
|
|
2653
|
+
return { ...grant, body };
|
|
2654
|
+
},
|
|
2655
|
+
{
|
|
2656
|
+
epoch,
|
|
2657
|
+
quiet: true,
|
|
2658
|
+
commit: true,
|
|
2659
|
+
...(beforeWrite ? { beforeWrite } : {}),
|
|
2660
|
+
...(signal ? { signal } : {}),
|
|
2661
|
+
},
|
|
2662
|
+
]));
|
|
2663
|
+
}
|
|
2664
|
+
catch {
|
|
2665
|
+
// See above: a refusal that could not be recorded recurs, it does not hide.
|
|
2666
|
+
}
|
|
2667
|
+
}.bind(this));
|
|
2668
|
+
}, KvOAuthProvider.prototype.discardRefusedGrant);
|
|
2669
|
+
registerAuthMethod(KvOAuthProvider.prototype, "resetAuthorization", function (operatorDisconnected = false, preserveClient = false) {
|
|
2670
|
+
return authFlow(function* () {
|
|
2671
|
+
const operation = authInvoke(this, "performReset", [operatorDisconnected, preserveClient]);
|
|
2672
|
+
if (!this.onReset)
|
|
2673
|
+
return (yield operation);
|
|
2674
|
+
const reset = runEdge(operation);
|
|
2675
|
+
this.onReset?.(reset);
|
|
2676
|
+
return (yield authValue(() => reset));
|
|
2677
|
+
}.bind(this));
|
|
2678
|
+
}, KvOAuthProvider.prototype.resetAuthorization);
|
|
2679
|
+
registerAuthMethod(KvOAuthProvider.prototype, "disconnectAuthorization", function (send) {
|
|
2680
|
+
return authFlow(function* () {
|
|
2681
|
+
const operation = authInvoke(this, "performReset", [true, false, send]);
|
|
2682
|
+
if (!this.onReset)
|
|
2683
|
+
return (yield operation);
|
|
2684
|
+
const reset = runEdge(operation);
|
|
2685
|
+
this.onReset?.(reset);
|
|
2686
|
+
return (yield authValue(() => reset));
|
|
2687
|
+
}.bind(this));
|
|
2688
|
+
}, KvOAuthProvider.prototype.disconnectAuthorization);
|
|
2689
|
+
registerAuthMethod(KvOAuthProvider.prototype, "performReset", function (operatorDisconnected, preserveClient, revoke) {
|
|
2690
|
+
return authFlow(function* () {
|
|
2691
|
+
const epoch = `${operatorDisconnected ? DISCONNECTED_EPOCH_PREFIX : ACTIVE_EPOCH_PREFIX}${crypto.randomUUID()}`;
|
|
2692
|
+
for (let attempt = 0; attempt < MAX_GRANT_WRITES; attempt++) {
|
|
2693
|
+
const { raw, grant } = (yield authInvoke(this, "readGrant", [null]));
|
|
2694
|
+
const carried = preserveClient && !operatorDisconnected ? this.carriableClient(grant) : undefined;
|
|
2695
|
+
const encoded = (yield authInvoke(this, "encodeGrant", [
|
|
2696
|
+
{ epoch, body: carried ?? {}, cleanupPending: true },
|
|
2697
|
+
null,
|
|
2698
|
+
]));
|
|
2699
|
+
let written;
|
|
2700
|
+
try {
|
|
2701
|
+
written = (yield authStorage(this.storage, "compareAndSet", [GRANT, raw, encoded]));
|
|
2702
|
+
}
|
|
2703
|
+
catch {
|
|
2704
|
+
throw this.credentialWriteError();
|
|
2705
|
+
}
|
|
2706
|
+
if (written) {
|
|
2707
|
+
this.refreshCoordinator?.retire(grant.epoch);
|
|
2708
|
+
try {
|
|
2709
|
+
(yield authInvoke(this, "cleanupV2", [encoded, null]));
|
|
2710
|
+
(yield authInvoke(this, "sweepConsents", []));
|
|
2711
|
+
(yield authInvoke(this, "sweepRefreshLeases", []));
|
|
2712
|
+
}
|
|
2713
|
+
finally {
|
|
2714
|
+
if (revoke)
|
|
2715
|
+
(yield authCall(revokeDownstreamGrant, [grant.body, revoke, this.clientOptions?.client]));
|
|
2716
|
+
}
|
|
2717
|
+
return;
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
throw this.credentialWriteError();
|
|
2721
|
+
}.bind(this));
|
|
2722
|
+
}, KvOAuthProvider.prototype.performReset);
|
|
2723
|
+
registerAuthMethod(KvOAuthProvider.prototype, "sweepConsents", function () {
|
|
2724
|
+
return authFlow(function* () {
|
|
2725
|
+
try {
|
|
2726
|
+
const consents = [];
|
|
2727
|
+
for (const key of (yield authStorage(this.storage, "list", [oauthFlowKeys.prefix]))) {
|
|
2728
|
+
const raw = (yield authStorage(this.storage, "get", [key]));
|
|
2729
|
+
if (raw !== null)
|
|
2730
|
+
consents.push([key, raw]);
|
|
2731
|
+
}
|
|
2732
|
+
const live = epochOf((yield authStorage(this.storage, "get", [GRANT])));
|
|
2733
|
+
for (const [key, raw] of consents) {
|
|
2734
|
+
const flow = parsed(raw);
|
|
2735
|
+
if (plainObject(flow) && flow.epoch === live)
|
|
2736
|
+
continue;
|
|
2737
|
+
(yield authStorage(this.storage, "compareAndSet", [key, raw, null]));
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
catch {
|
|
2741
|
+
// Hygiene only: the epoch, not this, fences a stale consent.
|
|
2742
|
+
}
|
|
2743
|
+
}.bind(this));
|
|
2744
|
+
}, KvOAuthProvider.prototype.sweepConsents);
|
|
2745
|
+
registerAuthMethod(KvOAuthProvider.prototype, "sweepRefreshLeases", function () {
|
|
2746
|
+
return authFlow(function* () {
|
|
2747
|
+
try {
|
|
2748
|
+
const records = [];
|
|
2749
|
+
// A reset may sweep an old lease while its HTTP request is still live.
|
|
2750
|
+
// Keep holder liveness until completion or its storage-owned TTL: spent
|
|
2751
|
+
// records in later epochs still use it to distinguish live from crashed.
|
|
2752
|
+
for (const key of (yield authStorage(this.storage, "list", [oauthRefreshKeys.prefix]))) {
|
|
2753
|
+
const raw = (yield authStorage(this.storage, "get", [key]));
|
|
2754
|
+
if (raw !== null)
|
|
2755
|
+
records.push([key, raw]);
|
|
2756
|
+
}
|
|
2757
|
+
const live = epochOf((yield authStorage(this.storage, "get", [GRANT])));
|
|
2758
|
+
for (const [key, raw] of records) {
|
|
2759
|
+
if (!key.startsWith(`${oauthRefreshKeys.prefix}${live}:`)) {
|
|
2760
|
+
(yield authStorage(this.storage, "compareAndSet", [key, raw, null]));
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
catch {
|
|
2765
|
+
// Stale dispatch records cannot grant credentials: epochs never recur.
|
|
2766
|
+
}
|
|
2767
|
+
}.bind(this));
|
|
2768
|
+
}, KvOAuthProvider.prototype.sweepRefreshLeases);
|
|
2769
|
+
}
|
|
1879
2770
|
}
|