@zackbart/connecta 0.34.1 → 0.36.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 +418 -0
- package/README.md +65 -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 +2044 -1161
- 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 +751 -281
- 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 +29 -7
- package/dist/connectors/api-connector.js +145 -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/remote-mcp.d.ts +10 -30
- package/dist/connectors/remote-mcp.js +955 -563
- package/dist/d1.js +10 -243
- package/dist/derived-context.js +5 -0
- package/dist/describe-config.d.ts +3 -1
- package/dist/describe-config.js +2 -0
- package/dist/downstream-input.js +347 -260
- package/dist/errors.d.ts +87 -6
- package/dist/errors.js +114 -4
- package/dist/execute.js +87 -50
- package/dist/executor-admission.js +3 -0
- package/dist/executors/quickjs.js +163 -112
- package/dist/guarded-transport.js +50 -0
- package/dist/guest-runtime.js +1 -1
- package/dist/http/client.js +439 -0
- package/dist/http/errors.js +558 -0
- package/dist/http/fields.js +49 -0
- package/dist/http/hatch-paging.js +318 -0
- package/dist/http/hatch.js +1279 -0
- package/dist/http/json-schema.js +267 -0
- package/dist/http/node-transport.js +241 -0
- package/dist/http/oauth.js +162 -0
- package/dist/http/paging.js +227 -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 +379 -0
- package/dist/http/vendor-index.js +138 -0
- package/dist/http/vendor.js +448 -0
- package/dist/http/write-request.js +66 -0
- package/dist/http-policy.d.ts +11 -0
- package/dist/http-policy.js +45 -0
- package/dist/inbound-credential.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -0
- package/dist/invocation-auth.js +30 -19
- package/dist/invocation.js +218 -85
- package/dist/mcp-result.js +0 -4
- package/dist/membership.d.ts +4 -0
- package/dist/meta-output.js +1 -1
- package/dist/meta-tools.js +531 -406
- package/dist/native-recovery.js +52 -0
- package/dist/native-tools.js +382 -0
- package/dist/node.d.ts +4 -3
- package/dist/node.js +45 -8
- package/dist/oauth-handoff.js +177 -104
- package/dist/oauth-partition.js +6 -20
- package/dist/oauth-sealing.js +6 -7
- package/dist/operation-vocabulary.js +101 -0
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +17 -4
- package/dist/operator-ui/contract.d.ts +8 -3
- package/dist/operator-ui/generated.js +4 -4
- package/dist/operator-ui/model.d.ts +2 -3
- package/dist/program-writes.js +47 -26
- package/dist/provider-call-facts.d.ts +115 -0
- package/dist/provider-call-facts.js +182 -0
- package/dist/provider-call.d.ts +112 -0
- package/dist/provider-call.js +271 -0
- package/dist/provider.d.ts +65 -31
- package/dist/provider.js +116 -39
- package/dist/providers/_shared/google/hatch.js +91 -0
- package/dist/providers/_shared/google/kit.js +427 -0
- package/dist/providers/_shared/google/result-size.js +15 -2
- package/dist/providers/_shared/google/service-account.d.ts +2 -1
- package/dist/providers/_shared/google/service-account.js +8 -4
- package/dist/providers/_shared/google/workspace.d.ts +6 -210
- package/dist/providers/_shared/google/workspace.js +6 -425
- package/dist/providers/_shared/rest/compact-index.js +86 -0
- package/dist/providers/_shared/rest/kit.js +146 -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/value-safety.js +353 -195
- package/dist/providers/_shared/sensitive-fields.js +27 -0
- package/dist/providers/breeze/api.js +584 -0
- package/dist/providers/breeze/index.d.ts +1 -3
- package/dist/providers/breeze/index.js +67 -1420
- package/dist/providers/breeze/openapi.generated.js +60 -0
- package/dist/providers/breeze/skill.generated.js +2 -6
- package/dist/providers/breeze/tools.js +696 -0
- package/dist/providers/ccb/api.js +496 -0
- package/dist/providers/ccb/index.d.ts +15 -37
- package/dist/providers/ccb/index.js +77 -1702
- package/dist/providers/ccb/lib/constants.d.ts +34 -0
- package/dist/providers/ccb/lib/constants.js +74 -0
- package/dist/providers/ccb/lib/guide.js +9 -0
- package/dist/providers/ccb/lib/verdicts.js +387 -0
- package/dist/providers/ccb/openapi.generated.js +397 -0
- package/dist/providers/ccb/safety.js +295 -0
- package/dist/providers/ccb/skill.generated.js +2 -6
- package/dist/providers/ccb/tools.js +853 -0
- package/dist/providers/cloudflare/api.js +1066 -0
- package/dist/providers/cloudflare/index.d.ts +5 -22
- package/dist/providers/cloudflare/index.js +79 -197
- package/dist/providers/cloudflare/lib/config.d.ts +16 -0
- package/dist/providers/cloudflare/lib/config.js +2 -0
- package/dist/providers/cloudflare/lib/discovery.js +29 -0
- package/dist/providers/cloudflare/{graphql.js → lib/graphql.js} +1 -1
- package/dist/providers/cloudflare/lib/hatch.js +103 -0
- package/dist/providers/cloudflare/lib/path-bounds.js +28 -0
- package/dist/providers/cloudflare/lib/response-media.js +75 -0
- package/dist/providers/cloudflare/openapi.generated.js +4 -3
- package/dist/providers/cloudflare/{value-safety.js → safety.js} +487 -0
- package/dist/providers/cloudflare/skill.generated.js +2 -10
- package/dist/providers/cloudflare/tools.js +360 -0
- package/dist/providers/docs/api.js +51 -0
- package/dist/providers/docs/index.d.ts +1 -12
- package/dist/providers/docs/index.js +53 -1165
- package/dist/providers/docs/lib/constants.d.ts +18 -0
- package/dist/providers/docs/lib/constants.js +18 -0
- package/dist/providers/docs/lib/discovery.js +26 -0
- package/dist/providers/docs/lib/requests.js +48 -0
- package/dist/providers/docs/lib/schemas.js +168 -0
- package/dist/providers/docs/openapi.generated.js +16 -0
- package/dist/providers/docs/safety.js +54 -0
- package/dist/providers/docs/skill.generated.js +2 -6
- package/dist/providers/docs/tools.js +801 -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 +1023 -0
- package/dist/providers/github/api.js +150 -0
- package/dist/providers/github/index.d.ts +4 -5
- package/dist/providers/github/index.js +30 -87
- package/dist/providers/github/{auth.d.ts → lib/auth.d.ts} +5 -3
- package/dist/providers/github/{auth.js → lib/auth.js} +40 -43
- package/dist/providers/github/lib/discovery.js +132 -0
- package/dist/providers/github/lib/operations.js +58 -0
- package/dist/providers/github/{scope.d.ts → lib/scope.d.ts} +2 -0
- package/dist/providers/github/{scope.js → lib/scope.js} +25 -1
- package/dist/providers/github/openapi.generated.js +1216 -0
- package/dist/providers/github/safety.js +216 -0
- package/dist/providers/github/skill.generated.js +2 -5
- package/dist/providers/github/tools.js +506 -0
- package/dist/providers/gmail/api.js +114 -0
- package/dist/providers/gmail/index.d.ts +1 -12
- package/dist/providers/gmail/index.js +73 -1621
- package/dist/providers/gmail/lib/constants.d.ts +13 -0
- package/dist/providers/gmail/lib/constants.js +16 -0
- package/dist/providers/gmail/lib/index.js +173 -0
- package/dist/providers/gmail/lib/schemas.js +176 -0
- package/dist/providers/gmail/lib/threading.js +120 -0
- package/dist/providers/gmail/openapi.generated.js +92 -0
- package/dist/providers/gmail/safety.js +21 -0
- package/dist/providers/gmail/skill.generated.js +2 -6
- package/dist/providers/gmail/tools.js +1435 -0
- package/dist/providers/overflow/api.js +1008 -0
- package/dist/providers/overflow/index.d.ts +1 -17
- package/dist/providers/overflow/index.js +60 -1488
- package/dist/providers/overflow/skill.generated.js +2 -5
- package/dist/providers/overflow/tools.js +873 -0
- package/dist/providers/planning-center/api.js +467 -0
- package/dist/providers/planning-center/index.d.ts +4 -35
- package/dist/providers/planning-center/index.js +69 -2445
- package/dist/providers/planning-center/lib/constants.d.ts +58 -0
- package/dist/providers/planning-center/lib/constants.js +83 -0
- package/dist/providers/planning-center/lib/discovery.js +40 -0
- package/dist/providers/planning-center/lib/guide.js +39 -0
- package/dist/providers/planning-center/lib/json-api.js +83 -0
- package/dist/providers/planning-center/lib/verdicts.js +1758 -0
- package/dist/providers/planning-center/openapi.generated.js +1764 -0
- package/dist/providers/planning-center/safety.js +188 -0
- package/dist/providers/planning-center/skill.generated.js +2 -27
- package/dist/providers/planning-center/tools.js +1010 -0
- package/dist/providers/quickbooks/api.js +299 -0
- package/dist/providers/quickbooks/index.d.ts +13 -0
- package/dist/providers/quickbooks/index.js +93 -0
- package/dist/providers/quickbooks/lib/query.js +118 -0
- package/dist/providers/quickbooks/lib/writes.js +168 -0
- package/dist/providers/quickbooks/safety.js +222 -0
- package/dist/providers/quickbooks/skill.generated.js +5 -0
- package/dist/providers/quickbooks/tools.js +117 -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 +733 -0
- package/dist/providers/slides/api.js +147 -0
- package/dist/providers/slides/index.d.ts +2 -10
- package/dist/providers/slides/index.js +48 -2555
- package/dist/providers/slides/lib/constants.d.ts +11 -0
- package/dist/providers/slides/lib/constants.js +11 -0
- package/dist/providers/slides/lib/requests.js +60 -0
- package/dist/providers/slides/lib/schemas.js +565 -0
- package/dist/providers/slides/openapi.generated.js +18 -0
- package/dist/providers/slides/skill.generated.js +2 -6
- package/dist/providers/slides/tools.js +1968 -0
- package/dist/providers/stripe/api.js +401 -0
- package/dist/providers/stripe/index.d.ts +5 -27
- package/dist/providers/stripe/index.js +74 -210
- package/dist/providers/stripe/lib/config.d.ts +4 -0
- package/dist/providers/stripe/lib/config.js +2 -0
- package/dist/providers/stripe/openapi.generated.js +4 -3
- package/dist/providers/stripe/{value-safety.js → safety.js} +5 -5
- package/dist/providers/stripe/skill.generated.js +2 -11
- package/dist/providers/stripe/tools.js +77 -0
- package/dist/providers/tithely/api.js +531 -0
- package/dist/providers/tithely/index.d.ts +17 -0
- package/dist/providers/tithely/index.js +86 -965
- package/dist/providers/tithely/skill.generated.js +2 -5
- package/dist/providers/tithely/tools.js +426 -0
- package/dist/providers/vercel/api.js +551 -0
- package/dist/providers/vercel/index.d.ts +9 -25
- package/dist/providers/vercel/index.js +65 -260
- package/dist/providers/vercel/lib/config.d.ts +2 -0
- package/dist/providers/vercel/lib/config.js +2 -0
- package/dist/providers/vercel/lib/index.js +117 -0
- package/dist/providers/vercel/lib/logs.js +100 -0
- package/dist/providers/vercel/lib/pin.js +270 -0
- package/dist/providers/vercel/openapi.generated.js +10 -5
- package/dist/providers/vercel/{value-safety.js → safety.js} +247 -38
- package/dist/providers/vercel/skill.generated.js +2 -7
- package/dist/providers/vercel/tools.js +642 -0
- package/dist/redaction-identity.js +47 -0
- package/dist/redaction-marker.js +125 -0
- package/dist/registry.d.ts +21 -15
- package/dist/registry.js +409 -376
- package/dist/routes/access-tokens.js +1 -1
- package/dist/routes/actor-labels.js +1 -1
- package/dist/routes/connect.js +1 -1
- package/dist/routes/dispatch.js +103 -0
- package/dist/routes/mcp.js +170 -79
- package/dist/routes/oauth.js +6 -1
- package/dist/routes/operator.js +2 -2
- package/dist/routes/shared.d.ts +5 -4
- package/dist/routes/shared.js +244 -177
- package/dist/routes/skills.js +10 -8
- package/dist/routes/ui-config.js +27 -3
- package/dist/routes/ui-data.js +21 -20
- package/dist/routes/ui.js +1 -1
- package/dist/runtime/call-admission.js +0 -26
- package/dist/runtime/call-entry.js +30 -0
- package/dist/runtime/call.js +552 -0
- package/dist/runtime/connector-scope.js +7 -0
- package/dist/runtime/credential-probe.js +34 -0
- package/dist/runtime/executor.js +40 -0
- package/dist/runtime/native-storage.js +63 -0
- package/dist/runtime/registry.js +27 -0
- package/dist/runtime/request-scope.js +72 -0
- package/dist/runtime/run.js +15 -7
- package/dist/runtime/shared-read.js +34 -5
- package/dist/runtime/storage.js +34 -21
- package/dist/sent-secrets.d.ts +194 -37
- package/dist/sent-secrets.js +1743 -340
- package/dist/server.js +5 -56
- package/dist/skills.js +322 -223
- 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 +9 -109
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +0 -1
- 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 +28 -7
- package/documentation/architecture.md +2409 -255
- package/documentation/auth.md +867 -114
- package/documentation/code-mode.md +49 -16
- package/documentation/deploying.md +164 -20
- package/documentation/hosted-mcp-migration.md +460 -0
- package/documentation/integrating.md +129 -144
- package/documentation/meta-tools.md +409 -32
- package/documentation/operating.md +27 -3
- package/documentation/operator-tests.md +3 -0
- package/documentation/operator-ui.md +84 -48
- package/documentation/provider-auth-migration.md +16 -63
- package/documentation/provider-migration-0.29.md +10 -5
- package/documentation/provider-migration-0.35.md +975 -0
- package/documentation/providers.md +1347 -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 +30 -50
- package/templates/node/.env.example +3 -0
- package/templates/node/README.md +22 -2
- package/templates/node/docker-compose.yml +1 -0
- package/templates/node/package.json +3 -2
- package/templates/node/src/connecta.config.ts +8 -0
- package/dist/catalog-drift.d.ts +0 -138
- package/dist/catalog-drift.js +0 -578
- package/dist/operator-ui/agents.d.ts +0 -74
- package/dist/operator-ui/agents.js +0 -104
- package/dist/operator-ui/config-summary.js +0 -565
- package/dist/operator-ui/connection-map.js +0 -376
- package/dist/operator-ui/endpoints.js +0 -221
- package/dist/operator-ui/fix-prompts.d.ts +0 -18
- package/dist/operator-ui/fix-prompts.js +0 -87
- package/dist/operator-ui/health.js +0 -52
- package/dist/operator-ui/member-page.js +0 -97
- package/dist/operator-ui/overview.js +0 -123
- package/dist/operator-ui/setup-commands.js +0 -51
- package/dist/operator-ui/view.d.ts +0 -401
- package/dist/operator-ui/view.js +0 -705
- package/dist/providers/_shared/request-token.js +0 -83
- package/dist/providers/_shared/rest/dispatch.js +0 -41
- 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/tools.js +0 -569
- package/dist/providers/_shared/rest/value-safety.d.ts +0 -201
- package/dist/providers/basecamp/index.d.ts +0 -45
- package/dist/providers/basecamp/index.js +0 -997
- package/dist/providers/basecamp/skill.generated.js +0 -9
- package/dist/providers/cloudflare/rest.d.ts +0 -37
- package/dist/providers/cloudflare/rest.js +0 -1097
- package/dist/providers/github/hosted.js +0 -218
- package/dist/providers/github/rest.js +0 -321
- package/dist/providers/infisical/index.d.ts +0 -7
- package/dist/providers/infisical/index.js +0 -713
- package/dist/providers/infisical/skill.generated.js +0 -8
- package/dist/providers/intercom/index.d.ts +0 -24
- package/dist/providers/intercom/index.js +0 -121
- package/dist/providers/intercom/skill.generated.js +0 -8
- package/dist/providers/linear/index.d.ts +0 -55
- package/dist/providers/linear/index.js +0 -537
- package/dist/providers/linear/skill.generated.js +0 -8
- package/dist/providers/mixpanel/index.d.ts +0 -59
- package/dist/providers/mixpanel/index.js +0 -472
- package/dist/providers/mixpanel/operations.js +0 -20
- package/dist/providers/mixpanel/rest.js +0 -603
- package/dist/providers/mixpanel/skill.generated.js +0 -10
- package/dist/providers/mixpanel/value-safety.js +0 -16
- package/dist/providers/notion/index.d.ts +0 -67
- package/dist/providers/notion/index.js +0 -368
- package/dist/providers/notion/openapi.generated.js +0 -75
- package/dist/providers/notion/rest.d.ts +0 -24
- package/dist/providers/notion/rest.js +0 -1226
- package/dist/providers/notion/skill.generated.js +0 -11
- package/dist/providers/notion/value-safety.js +0 -56
- package/dist/providers/revenuecat/index.d.ts +0 -51
- package/dist/providers/revenuecat/index.js +0 -697
- package/dist/providers/revenuecat/skill.generated.js +0 -11
- package/dist/providers/stripe/rest.d.ts +0 -14
- package/dist/providers/stripe/rest.js +0 -402
- package/dist/providers/vercel/rest.d.ts +0 -20
- package/dist/providers/vercel/rest.js +0 -1020
- package/dist/providers/wispr-flow/index.d.ts +0 -14
- package/dist/providers/wispr-flow/index.js +0 -117
- package/dist/providers/wispr-flow/skill.generated.js +0 -8
- /package/dist/providers/github/{key.js → lib/key.js} +0 -0
|
@@ -1,13 +1,34 @@
|
|
|
1
1
|
import { auth, OAuthError, UnauthorizedError } from "@modelcontextprotocol/client";
|
|
2
|
+
import { Effect } from "effect";
|
|
2
3
|
import { byteReadResponse } from "../byte-read-response.js";
|
|
3
|
-
import {
|
|
4
|
-
import { ConnectorCallError, msg } from "../errors.js";
|
|
5
|
-
import {
|
|
4
|
+
import { deriveContext } from "../derived-context.js";
|
|
5
|
+
import { ConnectorCallError, msg, resendAdviceOf, withResendAdvice } from "../errors.js";
|
|
6
|
+
import { guardedTransport, sendCredentialPost } from "../guarded-transport.js";
|
|
7
|
+
import { oauthPartitionFor, retainOAuthPartition } from "../oauth-partition.js";
|
|
6
8
|
import { oauthSealerFor } from "../oauth-sealing.js";
|
|
7
9
|
import { describeFailure } from "../operator-record.js";
|
|
8
|
-
import {
|
|
10
|
+
import { assertProviderCallOpen, bindProviderResponse, dispatchProviderRequest, providerRequest, readRequest, recordingFetch, recordProviderResponse, renewedAfterUnauthorized, takeProviderHandoff, vendorRefusal, } from "../provider-call.js";
|
|
11
|
+
import { viewSignal } from "../runtime/call.js";
|
|
12
|
+
import { runEdge } from "../runtime/run.js";
|
|
13
|
+
import { SharedRead } from "../runtime/shared-read.js";
|
|
14
|
+
import { sentSecretsFetch, sentSecretsFor, shortSecretWarning } from "../sent-secrets.js";
|
|
15
|
+
import { unsentAfterUnauthorized } from "../write-outcome.js";
|
|
9
16
|
import { trackRemoteClientRequest } from "./downstream-client-metadata.js";
|
|
17
|
+
import { assertOAuthScope, authorizingContext, codeExchangeResponse, KvOAuthProvider, refreshCoordinatorsByPartition, } from "./downstream-oauth.js";
|
|
18
|
+
import { authView } from "./native-call.js";
|
|
19
|
+
import { authCall, authFlow, authInvoke, authMethod, authOwnMethod, authProgram, authValue, registerAuth, registerAuthMethod, } from "./native.js";
|
|
10
20
|
import { trackOAuthStartReset } from "./oauth-start-reset.js";
|
|
21
|
+
import { bindRenewableGrant, OAUTH_CHALLENGE_MAX_BYTES } from "./renewable-grant.js";
|
|
22
|
+
/**
|
|
23
|
+
* Token-request header values Connecta itself sends, by lowercase name: JSON
|
|
24
|
+
* content negotiation and the media types maintained providers configure.
|
|
25
|
+
* Registering one would redact a vendor's name wherever its fragments recur.
|
|
26
|
+
* Any other configured value, under any name, may carry a credential and is
|
|
27
|
+
* registered.
|
|
28
|
+
*/
|
|
29
|
+
export const PUBLIC_TOKEN_REQUEST_HEADERS = new Map([
|
|
30
|
+
["accept", new Set(["application/json", "application/vnd.ccbchurch.v2+json"])],
|
|
31
|
+
]);
|
|
11
32
|
/**
|
|
12
33
|
* Downstream OAuth for a hand-written `api()` connector: the authorization
|
|
13
34
|
* code grant against endpoints and a pre-registered client the deployment
|
|
@@ -33,6 +54,15 @@ const RESERVED_AUTHORIZATION_PARAMS = new Set([
|
|
|
33
54
|
"code_challenge_method",
|
|
34
55
|
"resource",
|
|
35
56
|
]);
|
|
57
|
+
/** Protocol response fields have their own validation, never static assertions. */
|
|
58
|
+
const RESERVED_CALLBACK_PARAMS = new Set([
|
|
59
|
+
"code",
|
|
60
|
+
"state",
|
|
61
|
+
"iss",
|
|
62
|
+
"error",
|
|
63
|
+
"error_description",
|
|
64
|
+
"error_uri",
|
|
65
|
+
]);
|
|
36
66
|
/** Token-request headers the grant owns: client authentication and framing. */
|
|
37
67
|
const RESERVED_TOKEN_HEADERS = new Set([
|
|
38
68
|
"authorization",
|
|
@@ -71,65 +101,91 @@ function settingsFor(id, config) {
|
|
|
71
101
|
if (!config || typeof config !== "object") {
|
|
72
102
|
throw new Error(`[connecta] connector "${id}" oauth must be an object.`);
|
|
73
103
|
}
|
|
104
|
+
const invalid = (detail) => {
|
|
105
|
+
throw new Error(`[connecta] connector "${id}" oauth.${detail}`);
|
|
106
|
+
};
|
|
74
107
|
const authorizationEndpoint = configuredUrl(id, "authorizationEndpoint", config.authorizationEndpoint);
|
|
75
108
|
const tokenEndpoint = configuredUrl(id, "tokenEndpoint", config.tokenEndpoint);
|
|
109
|
+
const revocationEndpoint = config.revocationEndpoint === undefined
|
|
110
|
+
? undefined
|
|
111
|
+
: configuredUrl(id, "revocationEndpoint", config.revocationEndpoint);
|
|
112
|
+
if (revocationEndpoint?.search)
|
|
113
|
+
invalid(`revocationEndpoint must not carry a query.`);
|
|
114
|
+
const revocationRequestFormat = config.revocationRequestFormat ?? "form";
|
|
115
|
+
if ((revocationRequestFormat !== "form" && revocationRequestFormat !== "json") ||
|
|
116
|
+
(config.revocationRequestFormat !== undefined && !revocationEndpoint))
|
|
117
|
+
invalid(`revocationRequestFormat requires an endpoint and form or json.`);
|
|
76
118
|
if (tokenEndpoint.search) {
|
|
77
|
-
|
|
119
|
+
invalid(`tokenEndpoint must not carry a query.`);
|
|
78
120
|
}
|
|
79
121
|
if (typeof config.clientId !== "string" || config.clientId.trim() === "") {
|
|
80
|
-
|
|
122
|
+
invalid(`clientId must be a non-empty string.`);
|
|
81
123
|
}
|
|
82
124
|
if (config.clientSecret !== undefined && (typeof config.clientSecret !== "string" || config.clientSecret === "")) {
|
|
83
125
|
// Usually an unset environment variable read as "". Never echo it.
|
|
84
|
-
|
|
126
|
+
invalid(`clientSecret must be a non-empty string when set.`);
|
|
85
127
|
}
|
|
86
128
|
const clientAuthentication = config.tokenEndpointAuthMethod ?? (config.clientSecret !== undefined ? "client_secret_basic" : "none");
|
|
87
129
|
if (!CLIENT_AUTHENTICATION.has(clientAuthentication)) {
|
|
88
|
-
|
|
89
|
-
'"client_secret_basic", "client_secret_post", or "none".');
|
|
130
|
+
invalid(`tokenEndpointAuthMethod must be ` + '"client_secret_basic", "client_secret_post", or "none".');
|
|
90
131
|
}
|
|
91
132
|
if (clientAuthentication === "none" && config.clientSecret !== undefined) {
|
|
92
133
|
throw new Error(`[connecta] connector "${id}" oauth declares a clientSecret with ` +
|
|
93
134
|
'tokenEndpointAuthMethod "none"; a public client sends no secret.');
|
|
94
135
|
}
|
|
95
136
|
if (clientAuthentication !== "none" && config.clientSecret === undefined) {
|
|
96
|
-
|
|
137
|
+
invalid(`tokenEndpointAuthMethod ` + `"${clientAuthentication}" needs a clientSecret.`);
|
|
97
138
|
}
|
|
98
139
|
assertOAuthScope(id, config.scope);
|
|
99
140
|
const authorizationParams = [];
|
|
100
141
|
for (const [name, value] of Object.entries(config.authorizationParams ?? {})) {
|
|
101
142
|
if (RESERVED_AUTHORIZATION_PARAMS.has(name)) {
|
|
102
|
-
|
|
103
|
-
`"${name}"; the authorization request owns it.`);
|
|
143
|
+
invalid(`authorizationParams may not set ` + `"${name}"; the authorization request owns it.`);
|
|
104
144
|
}
|
|
105
145
|
if (typeof value !== "string") {
|
|
106
|
-
|
|
146
|
+
invalid(`authorizationParams.${name} must be a string.`);
|
|
107
147
|
}
|
|
108
148
|
authorizationParams.push([name, value]);
|
|
109
149
|
}
|
|
150
|
+
const expectedCallbackParameters = [];
|
|
151
|
+
if (config.expectedCallbackParameters !== undefined &&
|
|
152
|
+
(!config.expectedCallbackParameters ||
|
|
153
|
+
typeof config.expectedCallbackParameters !== "object" ||
|
|
154
|
+
Array.isArray(config.expectedCallbackParameters))) {
|
|
155
|
+
invalid(`expectedCallbackParameters must be a string record.`);
|
|
156
|
+
}
|
|
157
|
+
for (const [name, value] of Object.entries(config.expectedCallbackParameters ?? {})) {
|
|
158
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(name) ||
|
|
159
|
+
RESERVED_CALLBACK_PARAMS.has(name) ||
|
|
160
|
+
typeof value !== "string" ||
|
|
161
|
+
value === "") {
|
|
162
|
+
invalid(`expectedCallbackParameters must use nonempty parameter names ` +
|
|
163
|
+
"(ASCII letters, digits, dots, underscores or hyphens), nonempty string values, and no OAuth response fields.");
|
|
164
|
+
}
|
|
165
|
+
expectedCallbackParameters.push([name, value]);
|
|
166
|
+
}
|
|
110
167
|
const tokenRequestHeaders = [];
|
|
111
168
|
for (const [name, value] of Object.entries(config.tokenRequestHeaders ?? {})) {
|
|
112
169
|
if (!HEADER_NAME.test(name)) {
|
|
113
|
-
|
|
170
|
+
invalid(`tokenRequestHeaders has an invalid header name.`);
|
|
114
171
|
}
|
|
115
172
|
if (RESERVED_TOKEN_HEADERS.has(name.toLowerCase())) {
|
|
116
|
-
|
|
173
|
+
invalid(`tokenRequestHeaders may not set ` + `"${name}"; the token request owns it.`);
|
|
117
174
|
}
|
|
118
175
|
if (typeof value !== "string" || value.includes("\r") || value.includes("\n") || value.includes("\0")) {
|
|
119
|
-
|
|
176
|
+
invalid(`tokenRequestHeaders.${name} must be a single-line string.`);
|
|
120
177
|
}
|
|
121
178
|
tokenRequestHeaders.push([name, value]);
|
|
122
179
|
}
|
|
123
180
|
const origins = Array.isArray(config.apiOrigins) ? config.apiOrigins : [];
|
|
124
181
|
if (origins.length === 0) {
|
|
125
|
-
|
|
126
|
-
"origin; ctx.oauth.fetch sends the access token nowhere else.");
|
|
182
|
+
invalid(`apiOrigins must name at least one ` + "origin; ctx.oauth.fetch sends the access token nowhere else.");
|
|
127
183
|
}
|
|
128
184
|
const apiOrigins = new Set();
|
|
129
185
|
for (const origin of origins) {
|
|
130
186
|
const url = configuredUrl(id, "apiOrigins", origin);
|
|
131
187
|
if (url.origin !== origin) {
|
|
132
|
-
|
|
188
|
+
invalid(`apiOrigins entries must be exact ` +
|
|
133
189
|
'origins, like "https://api.example.com", with no path or trailing slash.');
|
|
134
190
|
}
|
|
135
191
|
apiOrigins.add(url.origin);
|
|
@@ -137,12 +193,15 @@ function settingsFor(id, config) {
|
|
|
137
193
|
return {
|
|
138
194
|
authorizationEndpoint,
|
|
139
195
|
tokenEndpoint,
|
|
196
|
+
...(revocationEndpoint ? { revocationEndpoint } : {}),
|
|
197
|
+
revocationRequestFormat,
|
|
140
198
|
clientId: config.clientId,
|
|
141
199
|
...(config.clientSecret !== undefined ? { clientSecret: config.clientSecret } : {}),
|
|
142
200
|
clientAuthentication,
|
|
143
201
|
...(config.scope !== undefined ? { scope: config.scope } : {}),
|
|
144
202
|
pkce: config.pkce ?? true,
|
|
145
203
|
authorizationParams,
|
|
204
|
+
expectedCallbackParameters,
|
|
146
205
|
tokenRequestHeaders,
|
|
147
206
|
apiOrigins,
|
|
148
207
|
serverUrl: new URL(origins[0]),
|
|
@@ -159,8 +218,8 @@ class StaticOAuthProvider extends KvOAuthProvider {
|
|
|
159
218
|
settings;
|
|
160
219
|
/** Set only for the code exchange of a PKCE-less flow. */
|
|
161
220
|
exchangeCode;
|
|
162
|
-
constructor(connectorId, ctx, coordinator, settings) {
|
|
163
|
-
super(connectorId, ctx.storage, `${ctx.baseUrl}/oauth/callback/${connectorId}`, coordinator, ctx.allowAuthorization === true, oauthSealerFor(ctx), ctx
|
|
221
|
+
constructor(connectorId, ctx, coordinator, settings, requestOwned = false) {
|
|
222
|
+
super(connectorId, ctx.storage, `${ctx.baseUrl}/oauth/callback/${connectorId}`, coordinator, ctx.allowAuthorization === true, oauthSealerFor(ctx), viewSignal(ctx, requestOwned),
|
|
164
223
|
// No client binding: the client is configuration, so there is no
|
|
165
224
|
// registration for a restart to carry forward or discard.
|
|
166
225
|
undefined, (reset) => trackOAuthStartReset(ctx.requestScope ?? ctx, reset), undefined, settings.scope, undefined, { secrets: sentSecretsFor(ctx) });
|
|
@@ -171,7 +230,17 @@ class StaticOAuthProvider extends KvOAuthProvider {
|
|
|
171
230
|
* never written to storage, sealed or otherwise: it is deployment config,
|
|
172
231
|
* not grant state, and an operator disconnect cannot delete it.
|
|
173
232
|
*/
|
|
174
|
-
|
|
233
|
+
clientInformation() {
|
|
234
|
+
return runEdge(authOwnMethod(StaticOAuthProvider.prototype, this, "clientInformation", []));
|
|
235
|
+
}
|
|
236
|
+
configuredAuthorizationBinding() {
|
|
237
|
+
return {
|
|
238
|
+
issuer: this.settings.identity,
|
|
239
|
+
clientId: this.settings.clientId,
|
|
240
|
+
tokenEndpoint: this.settings.tokenEndpoint.href,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
configuredClientInformation() {
|
|
175
244
|
const { settings } = this;
|
|
176
245
|
return {
|
|
177
246
|
client_id: settings.clientId,
|
|
@@ -181,15 +250,21 @@ class StaticOAuthProvider extends KvOAuthProvider {
|
|
|
181
250
|
issuer: settings.identity,
|
|
182
251
|
};
|
|
183
252
|
}
|
|
184
|
-
|
|
253
|
+
revocationSnapshot(body) {
|
|
254
|
+
const { settings } = this;
|
|
185
255
|
return {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
256
|
+
body: {
|
|
257
|
+
...body,
|
|
258
|
+
client: { value: this.configuredClientInformation() },
|
|
259
|
+
discovery: {
|
|
260
|
+
authorizationServerUrl: settings.identity,
|
|
261
|
+
authorizationServerMetadata: { revocation_endpoint: settings.revocationEndpoint.href },
|
|
262
|
+
},
|
|
263
|
+
},
|
|
189
264
|
};
|
|
190
265
|
}
|
|
191
|
-
|
|
192
|
-
|
|
266
|
+
saveClientInformation() {
|
|
267
|
+
return runEdge(authOwnMethod(StaticOAuthProvider.prototype, this, "saveClientInformation", []));
|
|
193
268
|
}
|
|
194
269
|
/**
|
|
195
270
|
* Discovery as a constant. Supplying both halves keeps the SDK from
|
|
@@ -198,28 +273,15 @@ class StaticOAuthProvider extends KvOAuthProvider {
|
|
|
198
273
|
* The metadata names no `issuer`, so the SDK binds grants to the configured
|
|
199
274
|
* token endpoint and does not demand an RFC 9207 `iss` it cannot know.
|
|
200
275
|
*/
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const metadata = {
|
|
204
|
-
authorization_endpoint: settings.authorizationEndpoint.href,
|
|
205
|
-
token_endpoint: settings.tokenEndpoint.href,
|
|
206
|
-
response_types_supported: ["code"],
|
|
207
|
-
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
208
|
-
code_challenge_methods_supported: ["S256"],
|
|
209
|
-
token_endpoint_auth_methods_supported: [settings.clientAuthentication],
|
|
210
|
-
};
|
|
211
|
-
return {
|
|
212
|
-
authorizationServerUrl: settings.identity,
|
|
213
|
-
authorizationServerMetadata: metadata,
|
|
214
|
-
resourceMetadata: { resource: settings.serverUrl.origin },
|
|
215
|
-
};
|
|
276
|
+
discoveryState() {
|
|
277
|
+
return runEdge(authOwnMethod(StaticOAuthProvider.prototype, this, "discoveryState", []));
|
|
216
278
|
}
|
|
217
|
-
|
|
218
|
-
|
|
279
|
+
saveDiscoveryState() {
|
|
280
|
+
return runEdge(authOwnMethod(StaticOAuthProvider.prototype, this, "saveDiscoveryState", []));
|
|
219
281
|
}
|
|
220
282
|
/** No RFC 8707 `resource` parameter: a plain REST API names no resource. */
|
|
221
|
-
|
|
222
|
-
return
|
|
283
|
+
validateResourceURL() {
|
|
284
|
+
return runEdge(authOwnMethod(StaticOAuthProvider.prototype, this, "validateResourceURL", []));
|
|
223
285
|
}
|
|
224
286
|
/**
|
|
225
287
|
* The code exchange without `code_verifier`, for a flow whose authorization
|
|
@@ -239,21 +301,120 @@ class StaticOAuthProvider extends KvOAuthProvider {
|
|
|
239
301
|
exchanging(code) {
|
|
240
302
|
this.exchangeCode = code;
|
|
241
303
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
304
|
+
redirectToAuthorization(authorizationUrl) {
|
|
305
|
+
return runEdge(authOwnMethod(StaticOAuthProvider.prototype, this, "redirectToAuthorization", [authorizationUrl]));
|
|
306
|
+
}
|
|
307
|
+
static {
|
|
308
|
+
registerAuthMethod(StaticOAuthProvider.prototype, "clientInformation", function () {
|
|
309
|
+
return authValue(() => this.configuredClientInformation());
|
|
310
|
+
}, StaticOAuthProvider.prototype.clientInformation);
|
|
311
|
+
registerAuthMethod(StaticOAuthProvider.prototype, "saveClientInformation", function () {
|
|
312
|
+
// Nothing to save: the SDK only saves a client it registered or stamped.
|
|
313
|
+
return Effect.void;
|
|
314
|
+
}, StaticOAuthProvider.prototype.saveClientInformation);
|
|
315
|
+
registerAuthMethod(StaticOAuthProvider.prototype, "discoveryState", function () {
|
|
316
|
+
return authValue(() => {
|
|
317
|
+
const { settings } = this;
|
|
318
|
+
const metadata = {
|
|
319
|
+
authorization_endpoint: settings.authorizationEndpoint.href,
|
|
320
|
+
token_endpoint: settings.tokenEndpoint.href,
|
|
321
|
+
response_types_supported: ["code"],
|
|
322
|
+
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
323
|
+
code_challenge_methods_supported: ["S256"],
|
|
324
|
+
token_endpoint_auth_methods_supported: [settings.clientAuthentication],
|
|
325
|
+
};
|
|
326
|
+
return {
|
|
327
|
+
authorizationServerUrl: settings.identity,
|
|
328
|
+
authorizationServerMetadata: metadata,
|
|
329
|
+
resourceMetadata: { resource: settings.serverUrl.origin },
|
|
330
|
+
};
|
|
331
|
+
});
|
|
332
|
+
}, StaticOAuthProvider.prototype.discoveryState);
|
|
333
|
+
registerAuthMethod(StaticOAuthProvider.prototype, "saveDiscoveryState", function () {
|
|
334
|
+
// Configuration is the discovery; there is nothing to remember.
|
|
335
|
+
return Effect.void;
|
|
336
|
+
}, StaticOAuthProvider.prototype.saveDiscoveryState);
|
|
337
|
+
registerAuthMethod(StaticOAuthProvider.prototype, "validateResourceURL", function () {
|
|
338
|
+
return authValue(() => {
|
|
339
|
+
return undefined;
|
|
340
|
+
});
|
|
341
|
+
}, StaticOAuthProvider.prototype.validateResourceURL);
|
|
342
|
+
registerAuthMethod(StaticOAuthProvider.prototype, "redirectToAuthorization", function (authorizationUrl) {
|
|
343
|
+
return authFlow(function* () {
|
|
344
|
+
for (const [name, value] of this.settings.authorizationParams) {
|
|
345
|
+
authorizationUrl.searchParams.set(name, value);
|
|
346
|
+
}
|
|
347
|
+
if (!this.settings.pkce) {
|
|
348
|
+
authorizationUrl.searchParams.delete("code_challenge");
|
|
349
|
+
authorizationUrl.searchParams.delete("code_challenge_method");
|
|
350
|
+
}
|
|
351
|
+
(yield authCall(KvOAuthProvider.prototype.redirectToAuthorization, [authorizationUrl], this));
|
|
352
|
+
}.bind(this));
|
|
353
|
+
}, StaticOAuthProvider.prototype.redirectToAuthorization);
|
|
251
354
|
}
|
|
252
355
|
}
|
|
253
356
|
const AUTH_REQUIRED_MESSAGE = "Authorization required — open the URL to connect.";
|
|
357
|
+
/** Release the resource body before renewal, bounding even the facade's discarded challenge. */
|
|
358
|
+
function readChallenge(response, signal, method) {
|
|
359
|
+
return runEdge(authProgram(readChallengeNative, [response, signal, method]));
|
|
360
|
+
}
|
|
361
|
+
const readChallengeNative = /* @__PURE__ */ registerAuth(readChallenge, function (response, signal, method) {
|
|
362
|
+
return authFlow(function* () {
|
|
363
|
+
if (method?.toUpperCase() === "HEAD") {
|
|
364
|
+
void response.body?.cancel().catch(() => { });
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const oversized = () => new ConnectorCallError("connector_call_failed", "The OAuth challenge exceeded its response ceiling.", {
|
|
368
|
+
retryable: false,
|
|
369
|
+
});
|
|
370
|
+
if (Number(response.headers.get("content-length")) > OAUTH_CHALLENGE_MAX_BYTES) {
|
|
371
|
+
void response.body?.cancel().catch(() => { });
|
|
372
|
+
throw oversized();
|
|
373
|
+
}
|
|
374
|
+
if (!response.body)
|
|
375
|
+
return;
|
|
376
|
+
const reader = response.body.getReader();
|
|
377
|
+
const cancel = () => {
|
|
378
|
+
void reader.cancel().catch(() => { });
|
|
379
|
+
};
|
|
380
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
381
|
+
if (signal.aborted)
|
|
382
|
+
cancel();
|
|
383
|
+
let total = 0;
|
|
384
|
+
try {
|
|
385
|
+
while (true) {
|
|
386
|
+
let chunk;
|
|
387
|
+
try {
|
|
388
|
+
chunk = (yield authInvoke(reader, "read", []));
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
// Like the guarded client, a broken challenge body cannot replace its 401.
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
if (signal.aborted)
|
|
395
|
+
throw signal.reason;
|
|
396
|
+
if (chunk.done)
|
|
397
|
+
break;
|
|
398
|
+
total += chunk.value.byteLength;
|
|
399
|
+
if (total > OAUTH_CHALLENGE_MAX_BYTES) {
|
|
400
|
+
cancel();
|
|
401
|
+
throw oversized();
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
finally {
|
|
406
|
+
signal.removeEventListener("abort", cancel);
|
|
407
|
+
reader.releaseLock();
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
});
|
|
254
411
|
/** The grant behind one `api()` connector's `oauth`, as the hooks it exposes. */
|
|
255
412
|
export function staticOAuth(id, config) {
|
|
256
413
|
const settings = settingsFor(id, config);
|
|
414
|
+
const callbackParametersMatch = (params) => settings.expectedCallbackParameters.every(([name, expected]) => {
|
|
415
|
+
const values = params?.getAll(name);
|
|
416
|
+
return values?.length === 1 && values[0] === expected;
|
|
417
|
+
});
|
|
257
418
|
const issuerContext = { issuer: settings.identity };
|
|
258
419
|
const coordinatorFor = refreshCoordinatorsByPartition();
|
|
259
420
|
// Keyed by request scope, so nothing here outlives the request that set it.
|
|
@@ -261,266 +422,575 @@ export function staticOAuth(id, config) {
|
|
|
261
422
|
const rejectedScopes = new WeakSet();
|
|
262
423
|
const scopeOf = (ctx) => ctx.requestScope ?? ctx;
|
|
263
424
|
const warnShortSecret = shortSecretWarning();
|
|
264
|
-
const providerFor = (ctx) => {
|
|
425
|
+
const providerFor = (ctx, requestOwned = false) => {
|
|
265
426
|
warnShortSecret(settings.clientSecret, ctx.logger);
|
|
266
|
-
return new StaticOAuthProvider(id, ctx, coordinatorFor(ctx), settings);
|
|
427
|
+
return new StaticOAuthProvider(id, ctx, coordinatorFor(ctx), settings, requestOwned);
|
|
267
428
|
};
|
|
268
429
|
/**
|
|
269
|
-
* The
|
|
430
|
+
* The owned credential transport, with the configured token-request
|
|
270
431
|
* headers laid over the SDK's own. With discovery and registration answered
|
|
271
432
|
* from configuration the token endpoint is the only URL the SDK fetches here;
|
|
272
433
|
* the check keeps the headers on it even if that ever changes.
|
|
273
434
|
*/
|
|
274
|
-
const tokenEndpointFetch =
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
435
|
+
const tokenEndpointFetch = authMethod((secrets, input, init = {}) => {
|
|
436
|
+
return authFlow(function* () {
|
|
437
|
+
const url = new URL(input);
|
|
438
|
+
const request = { ...init, redirect: "manual" };
|
|
439
|
+
if (settings.tokenRequestHeaders.length && `${url.origin}${url.pathname}` === settings.identity) {
|
|
440
|
+
const headers = new Headers(init.headers);
|
|
441
|
+
for (const [name, value] of settings.tokenRequestHeaders)
|
|
442
|
+
headers.set(name, value);
|
|
443
|
+
for (const [name, value] of settings.tokenRequestHeaders)
|
|
444
|
+
if (!PUBLIC_TOKEN_REQUEST_HEADERS.get(name.toLowerCase())?.has(value))
|
|
445
|
+
secrets.header(value);
|
|
446
|
+
request.headers = headers;
|
|
447
|
+
}
|
|
448
|
+
trackRemoteClientRequest(secrets, input, request);
|
|
449
|
+
// The coordinator or code-exchange wrapper authorizes this send. Refresh
|
|
450
|
+
// claim submission may outlive the caller; this transport uses no
|
|
451
|
+
// request-owned capability and changes no closed resource-call ledger.
|
|
452
|
+
const dispatch = {
|
|
453
|
+
dispatched: false,
|
|
454
|
+
auxiliary: true,
|
|
455
|
+
provider: id,
|
|
456
|
+
method: request.method ?? "GET",
|
|
457
|
+
};
|
|
458
|
+
secrets.request(input, request);
|
|
459
|
+
const response = (yield authCall(sendCredentialPost, [
|
|
460
|
+
input,
|
|
461
|
+
request,
|
|
462
|
+
() => dispatchProviderRequest(undefined, dispatch, request.headers),
|
|
463
|
+
]));
|
|
464
|
+
recordProviderResponse(dispatch, response.status);
|
|
465
|
+
return byteReadResponse(response);
|
|
466
|
+
});
|
|
467
|
+
});
|
|
288
468
|
/**
|
|
289
469
|
* The SDK's `auth()` over this provider. The token endpoint is fetched
|
|
290
470
|
* through the refresh coordinator, with permanent spent fingerprints,
|
|
291
471
|
* rotation commits, and re-consent verdicts. Credential-bearing requests
|
|
292
472
|
* never follow redirects in either OAuth adapter.
|
|
293
473
|
*/
|
|
294
|
-
const runAuth = (provider, ctx, exchange) =>
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
474
|
+
const runAuth = authMethod((provider, ctx, exchange, requestOwned = false) => {
|
|
475
|
+
return authFlow(function* () {
|
|
476
|
+
const secrets = sentSecretsFor(ctx);
|
|
477
|
+
return (yield authCall(auth, [
|
|
478
|
+
provider,
|
|
479
|
+
{
|
|
480
|
+
serverUrl: settings.serverUrl,
|
|
481
|
+
...exchange,
|
|
482
|
+
fetchFn: coordinatorFor(ctx).coordinatedFetch(provider, authMethod((input, init) => {
|
|
483
|
+
return authFlow(function* () {
|
|
484
|
+
if (exchange) {
|
|
485
|
+
// A code exchange has no submitted refresh claim transferring
|
|
486
|
+
// ownership. Only explicit scope revocation stops a callback:
|
|
487
|
+
// the production callback route remains uninterruptible and
|
|
488
|
+
// does not revoke its own scope while exchanging a code.
|
|
489
|
+
return (yield authView(ctx, authMethod((signal) => {
|
|
490
|
+
return authFlow(function* () {
|
|
491
|
+
assertProviderCallOpen(ctx, id, true);
|
|
492
|
+
const response = (yield authCall(tokenEndpointFetch, [
|
|
493
|
+
secrets,
|
|
494
|
+
input,
|
|
495
|
+
{
|
|
496
|
+
...init,
|
|
497
|
+
signal: init?.signal ? AbortSignal.any([init.signal, signal]) : signal,
|
|
498
|
+
},
|
|
499
|
+
]));
|
|
500
|
+
// An override can ignore abort. Never hand its late tokens
|
|
501
|
+
// to the SDK, which would otherwise persist both credentials.
|
|
502
|
+
if (signal.aborted) {
|
|
503
|
+
void response.body?.cancel().catch(() => { });
|
|
504
|
+
throw signal.reason;
|
|
505
|
+
}
|
|
506
|
+
// The SDK clones and reads this body after fetch resolves.
|
|
507
|
+
// Revocation must cancel that read even for a custom fetch.
|
|
508
|
+
return codeExchangeResponse(response, signal);
|
|
509
|
+
});
|
|
510
|
+
}), true));
|
|
511
|
+
}
|
|
512
|
+
return (yield authCall(tokenEndpointFetch, [secrets, input, init]));
|
|
513
|
+
});
|
|
514
|
+
}), viewSignal(ctx, requestOwned || exchange !== undefined), ctx.defer, undefined, () => {
|
|
515
|
+
guardedTransport(true);
|
|
516
|
+
}),
|
|
517
|
+
},
|
|
518
|
+
]));
|
|
519
|
+
});
|
|
298
520
|
});
|
|
299
521
|
// No cause, as in remoteMcp(): the `UnauthorizedError` class is the whole
|
|
300
522
|
// verdict, and a logger rendering the chain renders whatever it says.
|
|
301
523
|
const authRequiredError = () => new ConnectorCallError("downstream_oauth_required", `Connector "${id}" requires authorization — call authorize_connector({ connector: "${id}" }) and open the returned URL.`);
|
|
302
524
|
const disconnectedMessage = `Connector "${id}" was disconnected by an operator — explicitly start authorization to reconnect it.`;
|
|
303
525
|
/**
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
* refresh. A second 401 is `auth_required`, latched for the request scope
|
|
307
|
-
* so a program's later calls do not each spend a refresh on it.
|
|
526
|
+
* The token to send after `spent` drew a 401: one another request has
|
|
527
|
+
* already rotated in, or else one coordinated refresh.
|
|
308
528
|
*/
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
324
|
-
const signal = init.signal && ctx.signal ? AbortSignal.any([init.signal, ctx.signal]) : (init.signal ?? ctx.signal);
|
|
325
|
-
const send = async (accessToken) => {
|
|
326
|
-
sentSecretsFor(ctx).add(accessToken);
|
|
327
|
-
const sent = new Headers(headers);
|
|
328
|
-
sent.set("Authorization", `Bearer ${accessToken}`);
|
|
329
|
-
// The token rides to a declared origin and no further: a redirect is
|
|
330
|
-
// handed back to the handler unfollowed rather than re-sent anywhere.
|
|
331
|
-
// The handler reads the answer with `.json()` or `.text()`, which read
|
|
332
|
-
// bytes here: workerd quotes a text read's non-text Content-Type in
|
|
333
|
-
// its own log, out of the handler's reach.
|
|
334
|
-
const response = await sentSecretsFetch(ctx)(url, {
|
|
335
|
-
...init,
|
|
336
|
-
headers: sent,
|
|
337
|
-
redirect: "manual",
|
|
338
|
-
...(signal ? { signal } : {}),
|
|
529
|
+
const renewedToken = authMethod((provider, ctx, spent, requestOwned = false) => {
|
|
530
|
+
return authFlow(function* () {
|
|
531
|
+
// Only renewal whose claim CAS was submitted may survive this call.
|
|
532
|
+
// Race each preparation read too, and never resume SDK work or consume a
|
|
533
|
+
// rotated token after closure, even if a storage promise completes late.
|
|
534
|
+
const live = authMethod((work) => {
|
|
535
|
+
return authFlow(function* () {
|
|
536
|
+
if (ctx.signal?.aborted)
|
|
537
|
+
throw ctx.signal.reason;
|
|
538
|
+
const value = (yield authView(ctx, work, requestOwned));
|
|
539
|
+
if (ctx.signal?.aborted)
|
|
540
|
+
throw ctx.signal.reason;
|
|
541
|
+
return value;
|
|
542
|
+
});
|
|
339
543
|
});
|
|
340
|
-
|
|
544
|
+
const current = (yield authCall(live, [
|
|
545
|
+
authMethod(() => {
|
|
546
|
+
return authInvoke(provider, "tokens", [issuerContext]);
|
|
547
|
+
}),
|
|
548
|
+
]));
|
|
549
|
+
if (current && current.access_token !== spent)
|
|
550
|
+
return current.access_token;
|
|
551
|
+
if ((yield authCall(live, [
|
|
552
|
+
authMethod(() => {
|
|
553
|
+
return authCall(runAuth, [provider, ctx, undefined, requestOwned]);
|
|
554
|
+
}),
|
|
555
|
+
])) !== "AUTHORIZED")
|
|
556
|
+
throw authRequiredError();
|
|
557
|
+
const refreshed = (yield authCall(live, [
|
|
558
|
+
authMethod(() => {
|
|
559
|
+
return authInvoke(provider, "tokens", [issuerContext]);
|
|
560
|
+
}),
|
|
561
|
+
]));
|
|
562
|
+
if (!refreshed)
|
|
563
|
+
throw authRequiredError();
|
|
564
|
+
return refreshed.access_token;
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
/** One call's grant use; submitted refresh work keeps the coordinator's owner. */
|
|
568
|
+
const grantFor = (ctx, requestOwned = false) => {
|
|
569
|
+
let provider;
|
|
570
|
+
const use = authMethod((work) => {
|
|
571
|
+
return authView(ctx, authMethod((signal) => {
|
|
572
|
+
return authFlow(function* () {
|
|
573
|
+
assertProviderCallOpen(ctx, id, requestOwned);
|
|
574
|
+
if (rejectedScopes.has(scopeOf(ctx)))
|
|
575
|
+
throw authRequiredError();
|
|
576
|
+
const live = deriveContext(ctx, { signal });
|
|
577
|
+
const release = retainOAuthPartition(oauthPartitionFor(live));
|
|
578
|
+
try {
|
|
579
|
+
provider ??= new SharedRead(() => authView(live, authMethod(() => {
|
|
580
|
+
return authFlow(function* () {
|
|
581
|
+
const value = providerFor(live, requestOwned);
|
|
582
|
+
const epoch = (yield authInvoke(value, "beginFlow", []));
|
|
583
|
+
if (value.isOperatorDisconnectedEpoch(epoch)) {
|
|
584
|
+
throw new ConnectorCallError("downstream_oauth_required", disconnectedMessage);
|
|
585
|
+
}
|
|
586
|
+
return value;
|
|
587
|
+
});
|
|
588
|
+
}), requestOwned), undefined, ctx.requestScope ?? ctx);
|
|
589
|
+
return (yield authCall(work, [(yield provider.join(signal)), live]));
|
|
590
|
+
}
|
|
591
|
+
catch (error) {
|
|
592
|
+
if (signal.aborted)
|
|
593
|
+
throw signal.reason;
|
|
594
|
+
if (error instanceof UnauthorizedError) {
|
|
595
|
+
rejectedScopes.add(scopeOf(ctx));
|
|
596
|
+
throw authRequiredError();
|
|
597
|
+
}
|
|
598
|
+
throw error;
|
|
599
|
+
}
|
|
600
|
+
finally {
|
|
601
|
+
release();
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
}), requestOwned);
|
|
605
|
+
});
|
|
606
|
+
return {
|
|
607
|
+
token: authMethod(() => {
|
|
608
|
+
return authCall(use, [
|
|
609
|
+
authMethod((provider) => {
|
|
610
|
+
return authFlow(function* () {
|
|
611
|
+
const tokens = (yield authInvoke(provider, "tokens", [issuerContext]));
|
|
612
|
+
if (!tokens)
|
|
613
|
+
throw authRequiredError();
|
|
614
|
+
return tokens.access_token;
|
|
615
|
+
});
|
|
616
|
+
}),
|
|
617
|
+
]);
|
|
618
|
+
}),
|
|
619
|
+
refresh: authMethod((stale) => {
|
|
620
|
+
return authCall(use, [
|
|
621
|
+
authMethod((provider, live) => {
|
|
622
|
+
return authCall(renewedToken, [provider, live, stale, requestOwned]);
|
|
623
|
+
}),
|
|
624
|
+
]);
|
|
625
|
+
}),
|
|
626
|
+
rejected: authMethod(() => {
|
|
627
|
+
return authView(ctx, authMethod(() => {
|
|
628
|
+
return authValue(() => {
|
|
629
|
+
rejectedScopes.add(scopeOf(ctx));
|
|
630
|
+
throw authRequiredError();
|
|
631
|
+
});
|
|
632
|
+
}), requestOwned);
|
|
633
|
+
}),
|
|
341
634
|
};
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
635
|
+
};
|
|
636
|
+
/**
|
|
637
|
+
* One request through the owner's grant. A 401 earns exactly one recovery
|
|
638
|
+
* ({@link renewedToken}). A read is then sent once more; a write never is
|
|
639
|
+
* (INV-9), since a server may act on a request before it rejects the
|
|
640
|
+
* token. Whatever recovery meets is never reported as retryable: an
|
|
641
|
+
* authorization verdict keeps its code, and anything else (a disconnect or
|
|
642
|
+
* reset racing the refresh) leaves the outcome unknown. A second 401 is
|
|
643
|
+
* `auth_required`, latched for the request scope so a program's later
|
|
644
|
+
* calls do not each spend a refresh on it.
|
|
645
|
+
*/
|
|
646
|
+
const authorizedFetch = authMethod((ctx, input, init = {}) => {
|
|
647
|
+
return authFlow(function* () {
|
|
648
|
+
let prepared = takeProviderHandoff(init);
|
|
649
|
+
const auxiliary = prepared?.auxiliary === true;
|
|
650
|
+
// Own the complete token preparation and fetch helper. The coordinator
|
|
651
|
+
// still owns a submitted refresh claim independently of this wait.
|
|
652
|
+
return (yield authView(ctx, authMethod((owned) => {
|
|
653
|
+
return authFlow(function* () {
|
|
654
|
+
// A call that already ended reads no token and sends nothing more.
|
|
655
|
+
assertProviderCallOpen(ctx, id, auxiliary);
|
|
656
|
+
const owner = prepared;
|
|
657
|
+
const url = new URL(input);
|
|
658
|
+
if (!settings.apiOrigins.has(url.origin)) {
|
|
659
|
+
throw new ConnectorCallError("connector_call_failed", `Connector "${id}" sends its OAuth access token only to its declared ` +
|
|
660
|
+
`apiOrigins, and ${url.origin} is not one of them.`, { retryable: false });
|
|
661
|
+
}
|
|
662
|
+
if (init.body instanceof ReadableStream) {
|
|
663
|
+
throw new TypeError(`Connector "${id}": ctx.oauth.fetch may replay a request once after a ` +
|
|
664
|
+
"refresh, so it cannot send a stream body. Buffer the body first.");
|
|
665
|
+
}
|
|
666
|
+
const headers = new Headers(init.headers);
|
|
667
|
+
if (headers.has("authorization")) {
|
|
668
|
+
throw new ConnectorCallError("connector_call_failed", `Connector "${id}": a request through ctx.oauth.fetch may not set ` +
|
|
669
|
+
"Authorization; the connector's grant owns it.", { retryable: false });
|
|
670
|
+
}
|
|
671
|
+
const signal = init.signal ? AbortSignal.any([init.signal, owned]) : owned;
|
|
672
|
+
ctx = deriveContext(ctx, { signal });
|
|
673
|
+
const detachAnswers = [];
|
|
674
|
+
const send = authMethod((accessToken) => {
|
|
675
|
+
return authFlow(function* () {
|
|
676
|
+
if (signal?.aborted)
|
|
677
|
+
throw signal.reason;
|
|
678
|
+
sentSecretsFor(ctx).add(accessToken);
|
|
679
|
+
const sent = new Headers(headers);
|
|
680
|
+
sent.set("Authorization", `Bearer ${accessToken}`);
|
|
681
|
+
// The token rides to a declared origin and no further: a redirect is
|
|
682
|
+
// handed back to the handler unfollowed rather than re-sent anywhere.
|
|
683
|
+
// The handler reads the answer with `.json()` or `.text()`, which read
|
|
684
|
+
// bytes here: workerd quotes a text read's non-text Content-Type in
|
|
685
|
+
// its own log, out of the handler's reach.
|
|
686
|
+
const request = {
|
|
687
|
+
...init,
|
|
688
|
+
headers: sent,
|
|
689
|
+
redirect: "manual",
|
|
690
|
+
...(signal ? { signal } : {}),
|
|
691
|
+
};
|
|
692
|
+
const dispatch = prepared ?? providerRequest(ctx, auxiliary, id);
|
|
693
|
+
prepared = undefined;
|
|
694
|
+
dispatchProviderRequest(ctx, dispatch, sent, request.method ?? "GET");
|
|
695
|
+
// A native waiter can close before fetch settles. This leaf
|
|
696
|
+
// observer records only immutable response facts, and cancels
|
|
697
|
+
// a late body; it never resumes request-owned preparation.
|
|
698
|
+
const pending = sentSecretsFetch(ctx)(url, request).then((response) => {
|
|
699
|
+
recordProviderResponse(dispatch, response.status);
|
|
700
|
+
if (response.status === 401) {
|
|
701
|
+
// Closure can settle the handler before renewal's catch maps
|
|
702
|
+
// this answer. Preserve its OAuth verdict in the ledger first;
|
|
703
|
+
// only the final write decider reports it. A live renewal keeps
|
|
704
|
+
// its own precise verdict and never replays a write.
|
|
705
|
+
const abandoned = () => {
|
|
706
|
+
vendorRefusal(unsentAfterUnauthorized(`Connector "${id}"'s API`, signal.reason, "downstream_oauth_required"), dispatch);
|
|
707
|
+
};
|
|
708
|
+
if (signal.aborted)
|
|
709
|
+
abandoned();
|
|
710
|
+
else {
|
|
711
|
+
signal.addEventListener("abort", abandoned, { once: true });
|
|
712
|
+
detachAnswers.push(() => signal.removeEventListener("abort", abandoned));
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
// A custom fetch may answer after revocation. Its physical status
|
|
716
|
+
// remains a fact, but its body belongs to no live helper or handler.
|
|
717
|
+
if (signal.aborted)
|
|
718
|
+
void response.body?.cancel().catch(() => { });
|
|
719
|
+
return response;
|
|
720
|
+
});
|
|
721
|
+
const response = (yield authValue(() => pending));
|
|
722
|
+
if (signal.aborted)
|
|
723
|
+
throw signal.reason;
|
|
724
|
+
const answer = byteReadResponse(response);
|
|
725
|
+
bindProviderResponse(answer, dispatch, owner);
|
|
726
|
+
return { response: answer, dispatch };
|
|
727
|
+
});
|
|
728
|
+
});
|
|
729
|
+
const release = retainOAuthPartition(oauthPartitionFor(ctx));
|
|
730
|
+
try {
|
|
731
|
+
if (signal?.aborted)
|
|
732
|
+
throw signal.reason;
|
|
733
|
+
const grant = grantFor(ctx, auxiliary);
|
|
734
|
+
const stale = (yield authInvoke(grant, "token", []));
|
|
735
|
+
const { response: first, dispatch } = (yield authCall(send, [stale]));
|
|
736
|
+
if (first.status !== 401)
|
|
737
|
+
return first;
|
|
738
|
+
try {
|
|
739
|
+
(yield authCall(readChallenge, [first, signal, init.method]));
|
|
740
|
+
}
|
|
741
|
+
catch (error) {
|
|
742
|
+
if (signal.aborted)
|
|
743
|
+
throw signal.reason;
|
|
744
|
+
if (!readRequest(dispatch)) {
|
|
745
|
+
throw vendorRefusal(unsentAfterUnauthorized(`Connector "${id}"'s API`, error, "downstream_oauth_required"), dispatch);
|
|
746
|
+
}
|
|
747
|
+
throw error;
|
|
748
|
+
}
|
|
749
|
+
if (!readRequest(dispatch)) {
|
|
750
|
+
let renewal;
|
|
751
|
+
try {
|
|
752
|
+
const fresh = (yield authInvoke(grant, "refresh", [stale]));
|
|
753
|
+
if (signal?.aborted)
|
|
754
|
+
throw signal.reason;
|
|
755
|
+
// The call's ledger keeps this 401's verdict, or that renewal
|
|
756
|
+
// replaced the token this request sent (Q8). The same token back
|
|
757
|
+
// renewed nothing.
|
|
758
|
+
renewedAfterUnauthorized(dispatch, stale, fresh);
|
|
759
|
+
if (fresh === stale)
|
|
760
|
+
renewal = authRequiredError();
|
|
761
|
+
}
|
|
762
|
+
catch (error) {
|
|
763
|
+
if (error instanceof UnauthorizedError)
|
|
764
|
+
rejectedScopes.add(scopeOf(ctx));
|
|
765
|
+
renewal = error instanceof UnauthorizedError ? authRequiredError() : error;
|
|
766
|
+
}
|
|
767
|
+
throw vendorRefusal(unsentAfterUnauthorized(`Connector "${id}"'s API`, renewal, "downstream_oauth_required"), dispatch);
|
|
768
|
+
}
|
|
769
|
+
const accessToken = (yield authInvoke(grant, "refresh", [stale]));
|
|
770
|
+
if (signal?.aborted)
|
|
771
|
+
throw signal.reason;
|
|
772
|
+
renewedAfterUnauthorized(dispatch, stale, accessToken);
|
|
773
|
+
const { response: second } = (yield authCall(send, [accessToken]));
|
|
774
|
+
if (second.status !== 401)
|
|
775
|
+
return second;
|
|
776
|
+
void second.body?.cancel().catch(() => { });
|
|
777
|
+
return (yield authInvoke(grant, "rejected", []));
|
|
778
|
+
}
|
|
779
|
+
catch (error) {
|
|
780
|
+
// The SDK can replace cancellation with its passive-consent refusal,
|
|
781
|
+
// and write recovery normally replaces errors with a 401 verdict. A
|
|
782
|
+
// closed capability must still fail promptly with its own close reason;
|
|
783
|
+
// the invocation's ledger retains the write's uncertainty and handoff.
|
|
784
|
+
if (signal?.aborted)
|
|
785
|
+
throw signal.reason;
|
|
786
|
+
if (error instanceof UnauthorizedError) {
|
|
787
|
+
rejectedScopes.add(scopeOf(ctx));
|
|
788
|
+
throw authRequiredError();
|
|
789
|
+
}
|
|
790
|
+
throw error;
|
|
791
|
+
}
|
|
792
|
+
finally {
|
|
793
|
+
for (const detach of detachAnswers)
|
|
794
|
+
detach();
|
|
795
|
+
release();
|
|
796
|
+
}
|
|
797
|
+
});
|
|
798
|
+
}), auxiliary));
|
|
799
|
+
});
|
|
800
|
+
});
|
|
801
|
+
const status = authMethod((ctx) => {
|
|
802
|
+
return authFlow(function* () {
|
|
803
|
+
if (rejectedScopes.has(scopeOf(ctx))) {
|
|
804
|
+
return {
|
|
805
|
+
state: "auth_required",
|
|
806
|
+
message: "Authorization required \u2014 the downstream rejected this connector's OAuth grant.",
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
// A status read never starts authorization: this provider cannot, and
|
|
810
|
+
// nothing here calls the SDK. A stored grant for the configured token
|
|
811
|
+
// endpoint is healthy until a call says otherwise — the downstream is
|
|
812
|
+
// asked nothing.
|
|
348
813
|
const provider = providerFor(ctx);
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const epoch = await provider.beginFlow();
|
|
352
|
-
if (provider.isOperatorDisconnectedEpoch(epoch)) {
|
|
353
|
-
throw new ConnectorCallError("downstream_oauth_required", disconnectedMessage);
|
|
814
|
+
if ((yield authInvoke(provider, "operatorDisconnected", []))) {
|
|
815
|
+
return { state: "auth_required", message: disconnectedMessage };
|
|
354
816
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
const
|
|
364
|
-
if (
|
|
365
|
-
|
|
817
|
+
return (yield authInvoke(provider, "tokens", [issuerContext]))
|
|
818
|
+
? { state: "ok" }
|
|
819
|
+
: { state: "auth_required", message: AUTH_REQUIRED_MESSAGE };
|
|
820
|
+
});
|
|
821
|
+
});
|
|
822
|
+
const startAuth = authMethod((original, opts) => {
|
|
823
|
+
return authFlow(function* () {
|
|
824
|
+
const ctx = authorizingContext(original);
|
|
825
|
+
const provider = providerFor(ctx);
|
|
826
|
+
if (ctx.signal?.aborted)
|
|
827
|
+
throw ctx.signal.reason;
|
|
828
|
+
const disconnected = opts?.force
|
|
829
|
+
? false
|
|
830
|
+
: (yield authInvoke(provider, "operatorDisconnected", []));
|
|
831
|
+
if (ctx.signal?.aborted)
|
|
832
|
+
throw ctx.signal.reason;
|
|
833
|
+
if (opts?.force || disconnected) {
|
|
834
|
+
(yield authInvoke(provider, "resetAuthorization", []));
|
|
835
|
+
rejectedScopes.delete(scopeOf(original));
|
|
366
836
|
}
|
|
367
837
|
else {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const
|
|
371
|
-
if (
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
rejectedScopes.add(scopeOf(ctx));
|
|
380
|
-
throw authRequiredError();
|
|
381
|
-
}
|
|
382
|
-
catch (error) {
|
|
383
|
-
if (error instanceof UnauthorizedError) {
|
|
384
|
-
rejectedScopes.add(scopeOf(ctx));
|
|
385
|
-
throw authRequiredError();
|
|
838
|
+
// Hand back a recent consent URL rather than overwrite the verifier
|
|
839
|
+
// the operator may be mid-consent on.
|
|
840
|
+
const pending = (yield authInvoke(provider, "reusablePendingAuthorizationUrl", []));
|
|
841
|
+
if (pending) {
|
|
842
|
+
return {
|
|
843
|
+
state: "auth_required",
|
|
844
|
+
authorizationUrl: pending,
|
|
845
|
+
authorizationReused: true,
|
|
846
|
+
message: AUTH_REQUIRED_MESSAGE,
|
|
847
|
+
};
|
|
848
|
+
}
|
|
386
849
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
// A status read never starts authorization: this provider cannot, and
|
|
401
|
-
// nothing here calls the SDK. A stored grant for the configured token
|
|
402
|
-
// endpoint is healthy until a call says otherwise — the downstream is
|
|
403
|
-
// asked nothing.
|
|
404
|
-
const provider = providerFor(ctx);
|
|
405
|
-
if (await provider.operatorDisconnected()) {
|
|
406
|
-
return { state: "auth_required", message: disconnectedMessage };
|
|
407
|
-
}
|
|
408
|
-
return (await provider.tokens(issuerContext))
|
|
409
|
-
? { state: "ok" }
|
|
410
|
-
: { state: "auth_required", message: AUTH_REQUIRED_MESSAGE };
|
|
411
|
-
};
|
|
412
|
-
const startAuth = async (original, opts) => {
|
|
413
|
-
const ctx = authorizingContext(original);
|
|
414
|
-
const provider = providerFor(ctx);
|
|
415
|
-
if (ctx.signal?.aborted)
|
|
416
|
-
throw ctx.signal.reason;
|
|
417
|
-
const disconnected = opts?.force ? false : await provider.operatorDisconnected();
|
|
418
|
-
if (ctx.signal?.aborted)
|
|
419
|
-
throw ctx.signal.reason;
|
|
420
|
-
if (opts?.force || disconnected) {
|
|
421
|
-
await provider.resetAuthorization();
|
|
422
|
-
rejectedScopes.delete(scopeOf(original));
|
|
423
|
-
}
|
|
424
|
-
else {
|
|
425
|
-
// Hand back a recent consent URL rather than overwrite the verifier
|
|
426
|
-
// the operator may be mid-consent on.
|
|
427
|
-
const pending = await provider.reusablePendingAuthorizationUrl();
|
|
428
|
-
if (pending) {
|
|
850
|
+
if (ctx.signal?.aborted)
|
|
851
|
+
throw ctx.signal.reason;
|
|
852
|
+
try {
|
|
853
|
+
// A grant from a since-repointed token endpoint reads as absent here,
|
|
854
|
+
// and the consent this start begins replaces it.
|
|
855
|
+
(yield authInvoke(provider, "beginFlow", []));
|
|
856
|
+
if ((yield authInvoke(provider, "tokens", [issuerContext]))) {
|
|
857
|
+
return { state: "ok", message: "Already authorized \u2014 connection is healthy." };
|
|
858
|
+
}
|
|
859
|
+
if ((yield authCall(runAuth, [provider, ctx])) === "AUTHORIZED") {
|
|
860
|
+
return { state: "ok", message: "Already authorized \u2014 connection is healthy." };
|
|
861
|
+
}
|
|
862
|
+
const authorizationUrl = (yield authInvoke(provider, "consentUrl", []));
|
|
429
863
|
return {
|
|
430
864
|
state: "auth_required",
|
|
431
|
-
authorizationUrl:
|
|
432
|
-
authorizationReused: true,
|
|
865
|
+
...(authorizationUrl !== undefined ? { authorizationUrl } : {}),
|
|
433
866
|
message: AUTH_REQUIRED_MESSAGE,
|
|
434
867
|
};
|
|
435
868
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
return {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
869
|
+
catch (error) {
|
|
870
|
+
if (error instanceof UnauthorizedError) {
|
|
871
|
+
return { state: "auth_required", message: AUTH_REQUIRED_MESSAGE };
|
|
872
|
+
}
|
|
873
|
+
// Reaches the agent through authorize_connector. Text already in
|
|
874
|
+
// connecta's words passes with its advice to try again, since this is
|
|
875
|
+
// no write's failure (token responses are rebuilt before the SDK reads
|
|
876
|
+
// them); anything else is told from its record.
|
|
877
|
+
return {
|
|
878
|
+
state: "error",
|
|
879
|
+
message: error instanceof ConnectorCallError || error instanceof OAuthError
|
|
880
|
+
? withResendAdvice(msg(error), resendAdviceOf(error))
|
|
881
|
+
: describeFailure(id, error),
|
|
882
|
+
};
|
|
448
883
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
if (error instanceof UnauthorizedError) {
|
|
458
|
-
return { state: "auth_required", message: AUTH_REQUIRED_MESSAGE };
|
|
884
|
+
});
|
|
885
|
+
});
|
|
886
|
+
const disconnectAuth = authMethod((ctx) => {
|
|
887
|
+
return authFlow(function* () {
|
|
888
|
+
rejectedScopes.delete(scopeOf(ctx));
|
|
889
|
+
if (!settings.revocationEndpoint) {
|
|
890
|
+
(yield authInvoke(providerFor(ctx), "resetAuthorization", [true]));
|
|
891
|
+
return;
|
|
459
892
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
// exchange must write under exactly that one.
|
|
476
|
-
const provider = providerFor(ctx);
|
|
477
|
-
callbackProviders.set(scopeOf(ctx), provider);
|
|
478
|
-
return provider.verifyState(state);
|
|
479
|
-
};
|
|
480
|
-
const finishAuth = async (code, ctx, callbackParams) => {
|
|
481
|
-
const provider = callbackProviders.get(scopeOf(ctx)) ?? providerFor(ctx);
|
|
482
|
-
callbackProviders.delete(scopeOf(ctx));
|
|
483
|
-
const authorizationCode = callbackParams?.get("code") ?? code;
|
|
484
|
-
const iss = callbackParams?.get("iss") ?? undefined;
|
|
485
|
-
// Every exchange names and claims its own consent, including without PKCE.
|
|
486
|
-
const consent = callbackParams?.get("state") ?? null;
|
|
487
|
-
if (!consent || !(await provider.verifyCallbackState(consent))) {
|
|
488
|
-
throw new ConnectorCallError("connector_call_failed", `Connector "${id}" authorization callback matches no pending consent; nothing was exchanged.`);
|
|
489
|
-
}
|
|
490
|
-
provider.validateCallbackIssuer(callbackParams?.get("iss") ?? null);
|
|
491
|
-
provider.exchanging(authorizationCode);
|
|
492
|
-
await provider.bindFlow();
|
|
493
|
-
const result = await runAuth(provider, ctx, {
|
|
494
|
-
authorizationCode,
|
|
495
|
-
...(iss !== undefined ? { iss } : {}),
|
|
893
|
+
(yield authInvoke(providerFor(ctx), "disconnectAuthorization", [
|
|
894
|
+
authMethod((input, init = {}) => {
|
|
895
|
+
// Register the standard form's credentials before any JSON framing.
|
|
896
|
+
trackRemoteClientRequest(sentSecretsFor(ctx), input, init);
|
|
897
|
+
if (settings.revocationRequestFormat === "json" && init.body instanceof URLSearchParams) {
|
|
898
|
+
const params = new URLSearchParams(init.body);
|
|
899
|
+
params.delete("token_type_hint");
|
|
900
|
+
const headers = new Headers(init.headers);
|
|
901
|
+
headers.set("Content-Type", "application/json");
|
|
902
|
+
headers.set("Accept", "application/json");
|
|
903
|
+
init = { ...init, headers, body: JSON.stringify(Object.fromEntries(params)) };
|
|
904
|
+
}
|
|
905
|
+
return authCall(tokenEndpointFetch, [sentSecretsFor(ctx), input, init]);
|
|
906
|
+
}),
|
|
907
|
+
]));
|
|
496
908
|
});
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
909
|
+
});
|
|
910
|
+
const verifyState = authMethod((state, ctx) => {
|
|
911
|
+
return authFlow(function* () {
|
|
912
|
+
// The provider that verified the state captured its flow's epoch, and the
|
|
913
|
+
// exchange must write under exactly that one.
|
|
914
|
+
const provider = providerFor(ctx);
|
|
915
|
+
callbackProviders.set(scopeOf(ctx), provider);
|
|
916
|
+
return (yield authInvoke(provider, "verifyState", [state]));
|
|
917
|
+
});
|
|
918
|
+
});
|
|
919
|
+
const finishAuth = authMethod((code, ctx, callbackParams) => {
|
|
920
|
+
return authView(ctx, authMethod(() => {
|
|
921
|
+
return authFlow(function* () {
|
|
922
|
+
const provider = callbackProviders.get(scopeOf(ctx)) ?? providerFor(ctx);
|
|
923
|
+
callbackProviders.delete(scopeOf(ctx));
|
|
924
|
+
const authorizationCode = callbackParams?.get("code") ?? code;
|
|
925
|
+
const iss = callbackParams?.get("iss") ?? undefined;
|
|
926
|
+
// Every exchange names and claims its own consent, including without PKCE.
|
|
927
|
+
const consent = callbackParams?.get("state") ?? null;
|
|
928
|
+
if (!consent || !(yield authInvoke(provider, "verifyCallbackState", [consent]))) {
|
|
929
|
+
throw new ConnectorCallError("connector_call_failed", `Connector "${id}" authorization callback matches no pending consent; nothing was exchanged.`);
|
|
930
|
+
}
|
|
931
|
+
if (!callbackParametersMatch(callbackParams)) {
|
|
932
|
+
throw new ConnectorCallError("connector_call_failed", `Connector "${id}" authorization callback did not match its configured parameters; nothing was exchanged.`);
|
|
933
|
+
}
|
|
934
|
+
provider.validateCallbackIssuer(callbackParams?.get("iss") ?? null);
|
|
935
|
+
provider.exchanging(authorizationCode);
|
|
936
|
+
(yield authInvoke(provider, "bindFlow", []));
|
|
937
|
+
const result = (yield authCall(runAuth, [
|
|
938
|
+
provider,
|
|
939
|
+
ctx,
|
|
940
|
+
{
|
|
941
|
+
authorizationCode,
|
|
942
|
+
...(iss !== undefined ? { iss } : {}),
|
|
943
|
+
},
|
|
944
|
+
]));
|
|
945
|
+
if (result !== "AUTHORIZED") {
|
|
946
|
+
throw new UnauthorizedError("Failed to authorize");
|
|
947
|
+
}
|
|
948
|
+
rejectedScopes.delete(scopeOf(ctx));
|
|
949
|
+
});
|
|
950
|
+
}), true);
|
|
951
|
+
});
|
|
502
952
|
return {
|
|
503
|
-
status:
|
|
504
|
-
startAuth:
|
|
505
|
-
disconnectAuth:
|
|
506
|
-
verifyState:
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
953
|
+
status: retainAuthPartition(status, 0),
|
|
954
|
+
startAuth: retainAuthPartition(startAuth, 0),
|
|
955
|
+
disconnectAuth: retainAuthPartition(disconnectAuth, 0),
|
|
956
|
+
verifyState: retainAuthPartition(verifyState, 1),
|
|
957
|
+
verifyCallbackParameters: retainAuthPartition(authMethod((params, _ctx) => authValue(() => callbackParametersMatch(params))), 1),
|
|
958
|
+
verifyCallbackIssuer: retainAuthPartition(authMethod((issuer, ctx) => {
|
|
959
|
+
return authValue(() => {
|
|
960
|
+
const provider = callbackProviders.get(scopeOf(ctx));
|
|
961
|
+
if (!provider)
|
|
962
|
+
return false;
|
|
963
|
+
provider.validateCallbackIssuer(issuer);
|
|
964
|
+
return true;
|
|
965
|
+
});
|
|
966
|
+
}), 1),
|
|
967
|
+
consumeAuthError: retainAuthPartition(authMethod((ctx) => {
|
|
968
|
+
return authFlow(function* () {
|
|
969
|
+
const provider = callbackProviders.get(scopeOf(ctx));
|
|
970
|
+
callbackProviders.delete(scopeOf(ctx));
|
|
971
|
+
if (!provider)
|
|
972
|
+
throw new ConnectorCallError("connector_call_failed", "OAuth error callback matches no pending consent.");
|
|
973
|
+
(yield authInvoke(provider, "consumeAuthError", []));
|
|
974
|
+
});
|
|
975
|
+
}), 0),
|
|
976
|
+
finishAuth: retainAuthPartition(finishAuth, 1),
|
|
977
|
+
access: (ctx) => bindRenewableGrant({
|
|
978
|
+
fetch: recordingFetch(authMethod((input, init) => {
|
|
979
|
+
return authCall(authorizedFetch, [ctx, input, init]);
|
|
980
|
+
})),
|
|
981
|
+
}, grantFor(ctx)),
|
|
525
982
|
};
|
|
526
983
|
}
|
|
984
|
+
// Hook calls keep the existing owner partition pinned while their native
|
|
985
|
+
// program settles. Each Promise shell enters its own registered program.
|
|
986
|
+
function retainAuthPartition(operation, contextIndex = 0) {
|
|
987
|
+
return authMethod((...args) => authFlow(function* () {
|
|
988
|
+
const release = retainOAuthPartition(oauthPartitionFor(args[contextIndex]));
|
|
989
|
+
try {
|
|
990
|
+
return (yield authCall(operation, args));
|
|
991
|
+
}
|
|
992
|
+
finally {
|
|
993
|
+
release();
|
|
994
|
+
}
|
|
995
|
+
}));
|
|
996
|
+
}
|