@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,48 +1,41 @@
|
|
|
1
|
+
import { confinementBase, REDIRECT_STATUSES, BODILESS_STATUSES } from "../http-policy.js";
|
|
2
|
+
export { retryAfterMs } from "../http-policy.js";
|
|
3
|
+
import { callerSignal, duringViewCall, settleOnClose, viewSignal } from "../runtime/call.js";
|
|
1
4
|
/**
|
|
2
5
|
* The safety machinery every hand-written HTTP surface needs, extracted once
|
|
3
6
|
* (#341): path confinement, encoded query and framed bodies, per-request
|
|
4
7
|
* credential headers applied last, a refused redirect, a bounded read, and
|
|
5
|
-
* network-failure normalization. It owns no
|
|
6
|
-
*
|
|
8
|
+
* network-failure normalization. It owns no vendor-specific meaning and never
|
|
9
|
+
* invents an auth scheme, because the same 403 means an ungranted
|
|
7
10
|
* capability at one provider and a token scope at another, and those want
|
|
8
11
|
* opposite next moves. The provider's `authenticate` supplies headers and its
|
|
9
|
-
* mapper turns one `GuardedResponse` into a result or a typed failure.
|
|
12
|
+
* mapper turns one `GuardedResponse` into a result or a typed failure. Core
|
|
13
|
+
* then settles retry advice and write outcome from the served classification
|
|
14
|
+
* and dispatch facts, consistently across every provider (INV-9).
|
|
10
15
|
*
|
|
11
16
|
* Not exported this release: the migrations proved the shape preserves behavior
|
|
12
17
|
* for connectors that already had this machinery, not that it is the right
|
|
13
18
|
* public surface. Web APIs only.
|
|
14
19
|
*/
|
|
15
20
|
import { ConnectorCallError, unavailableCallError } from "../errors.js";
|
|
21
|
+
import { assertProviderCallOpen, dispatchProviderRequest, handoffProviderRequest, isRecordingFetch, recordProviderResponse, providerRequest, responseProviderRequest, vendorRefusal, } from "../provider-call.js";
|
|
22
|
+
import { providerCallFailure, providerCallResult } from "../write-outcome.js";
|
|
16
23
|
import { sentSecretsFor } from "../sent-secrets.js";
|
|
17
|
-
/** Parse delta-seconds or an HTTP-date into a non-negative wait window. */
|
|
18
|
-
export function retryAfterMs(headers) {
|
|
19
|
-
const raw = headers.get("retry-after");
|
|
20
|
-
if (!raw)
|
|
21
|
-
return undefined;
|
|
22
|
-
const seconds = Number(raw.trim());
|
|
23
|
-
if (Number.isFinite(seconds)) {
|
|
24
|
-
return seconds < 0 ? undefined : Math.trunc(seconds * 1000);
|
|
25
|
-
}
|
|
26
|
-
// Require the HTTP-date shape, so Date.parse cannot reinterpret "-1" as
|
|
27
|
-
// a calendar date on runtimes that accept loose date strings.
|
|
28
|
-
if (!/^[A-Za-z]{3}, \d{2} [A-Za-z]{3} \d{4} \d{2}:\d{2}:\d{2} GMT$/.test(raw.trim()))
|
|
29
|
-
return undefined;
|
|
30
|
-
const date = Date.parse(raw);
|
|
31
|
-
return Number.isFinite(date) ? Math.max(0, date - Date.now()) : undefined;
|
|
32
|
-
}
|
|
33
24
|
/**
|
|
34
25
|
* The transport half of a guarded connector that authenticates through its
|
|
35
26
|
* own `api()` OAuth grant: no header of the helper's making, and every request
|
|
36
27
|
* sent through `ctx.oauth.fetch`, which adds the calling owner's bearer token,
|
|
37
28
|
* refuses any origin outside the grant's `apiOrigins`, and spends one
|
|
38
|
-
* coordinated refresh
|
|
39
|
-
* `auth_required
|
|
29
|
+
* coordinated refresh on a 401: a read is replayed once before answering
|
|
30
|
+
* `auth_required`, and a write is never sent again (INV-9). Spread it into
|
|
31
|
+
* the options:
|
|
40
32
|
* `guardedFetch({ provider, baseUrl, maxResponseBytes, ...oauthBearer(provider) })`.
|
|
41
33
|
*
|
|
42
34
|
* Confinement still happens here first, so a request reaches the grant only
|
|
43
35
|
* once it is provably beneath `baseUrl`; the grant's origin check is the second
|
|
44
36
|
* fence, not the only one.
|
|
45
37
|
*/
|
|
38
|
+
const oauthSenders = new WeakSet();
|
|
46
39
|
export function oauthBearer(provider) {
|
|
47
40
|
const send = (url, init, ctx) => {
|
|
48
41
|
const grant = ctx.oauth;
|
|
@@ -53,37 +46,10 @@ export function oauthBearer(provider) {
|
|
|
53
46
|
}
|
|
54
47
|
return grant.fetch(url, init);
|
|
55
48
|
};
|
|
49
|
+
oauthSenders.add(send);
|
|
56
50
|
return { authenticate: () => ({}), fetch: send };
|
|
57
51
|
}
|
|
58
|
-
/** Statuses that instruct a client to re-send somewhere else. Never followed. */
|
|
59
|
-
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
60
|
-
/** Statuses the HTTP spec defines as carrying no body at all. */
|
|
61
|
-
const BODILESS_STATUSES = new Set([204, 205, 304]);
|
|
62
52
|
const decoder = new TextDecoder();
|
|
63
|
-
/**
|
|
64
|
-
* A base URL is deployment configuration, so a bad one is a structural mistake
|
|
65
|
-
* and throws where it is written rather than on the first call that uses it.
|
|
66
|
-
*/
|
|
67
|
-
function confinementBase(provider, baseUrl) {
|
|
68
|
-
let base;
|
|
69
|
-
try {
|
|
70
|
-
base = new URL(baseUrl);
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
throw new Error(`${provider} baseUrl must be an absolute URL.`);
|
|
74
|
-
}
|
|
75
|
-
const loopback = base.hostname === "localhost" || base.hostname === "127.0.0.1" || base.hostname === "[::1]";
|
|
76
|
-
if (base.protocol !== "https:" && !(base.protocol === "http:" && loopback)) {
|
|
77
|
-
throw new Error(`${provider} baseUrl must be https (http is allowed only for a loopback proxy or test double).`);
|
|
78
|
-
}
|
|
79
|
-
if (base.username || base.password) {
|
|
80
|
-
throw new Error(`${provider} baseUrl must not embed URL credentials; authentication belongs in headers.`);
|
|
81
|
-
}
|
|
82
|
-
if (base.search || base.hash) {
|
|
83
|
-
throw new Error(`${provider} baseUrl must not carry a query or fragment.`);
|
|
84
|
-
}
|
|
85
|
-
return base;
|
|
86
|
-
}
|
|
87
53
|
/**
|
|
88
54
|
* Resolve a provider-relative path beneath the base and prove it stayed there.
|
|
89
55
|
*
|
|
@@ -132,13 +98,32 @@ function readableBody(response) {
|
|
|
132
98
|
return body && typeof body.getReader === "function" ? body : null;
|
|
133
99
|
}
|
|
134
100
|
/** Read a stream to the ceiling and stop there — never past it. */
|
|
135
|
-
|
|
101
|
+
function ownedReader(stream, signal) {
|
|
136
102
|
const reader = stream.getReader();
|
|
103
|
+
void reader.closed.catch(() => { });
|
|
104
|
+
const abort = () => {
|
|
105
|
+
void reader.cancel().catch(() => { });
|
|
106
|
+
};
|
|
107
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
108
|
+
if (signal.aborted)
|
|
109
|
+
abort();
|
|
110
|
+
return {
|
|
111
|
+
reader,
|
|
112
|
+
close: () => {
|
|
113
|
+
signal.removeEventListener("abort", abort);
|
|
114
|
+
reader.releaseLock();
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
async function drain(provider, stream, limit, signal) {
|
|
119
|
+
const { reader, close } = ownedReader(stream, signal);
|
|
137
120
|
const chunks = [];
|
|
138
121
|
let total = 0;
|
|
139
122
|
try {
|
|
140
123
|
while (true) {
|
|
141
124
|
const { done, value } = await reader.read();
|
|
125
|
+
if (signal.aborted)
|
|
126
|
+
throw signal.reason;
|
|
142
127
|
if (done)
|
|
143
128
|
break;
|
|
144
129
|
if (!value)
|
|
@@ -152,7 +137,7 @@ async function drain(provider, stream, limit) {
|
|
|
152
137
|
}
|
|
153
138
|
}
|
|
154
139
|
finally {
|
|
155
|
-
|
|
140
|
+
close();
|
|
156
141
|
}
|
|
157
142
|
const body = new Uint8Array(total);
|
|
158
143
|
let offset = 0;
|
|
@@ -174,7 +159,7 @@ async function drain(provider, stream, limit) {
|
|
|
174
159
|
* with a parse error and turns a response nobody was allowed to read into an
|
|
175
160
|
* empty success.
|
|
176
161
|
*/
|
|
177
|
-
function boundedResponse(provider, response, limit) {
|
|
162
|
+
function boundedResponse(provider, response, limit, signal) {
|
|
178
163
|
if (BODILESS_STATUSES.has(response.status)) {
|
|
179
164
|
const emptyJson = async () => undefined;
|
|
180
165
|
return {
|
|
@@ -195,7 +180,7 @@ function boundedResponse(provider, response, limit) {
|
|
|
195
180
|
const prefix = async (maxBytes) => {
|
|
196
181
|
const keep = Math.min(Math.max(0, Math.trunc(maxBytes)), limit);
|
|
197
182
|
if (stream)
|
|
198
|
-
return await readPrefix(stream, keep);
|
|
183
|
+
return await readPrefix(stream, keep, signal);
|
|
199
184
|
// A runtime with no body stream buffers the body regardless; the
|
|
200
185
|
// ceiling still bounds what is accepted.
|
|
201
186
|
const buffer = new Uint8Array(await response.arrayBuffer());
|
|
@@ -207,7 +192,7 @@ function boundedResponse(provider, response, limit) {
|
|
|
207
192
|
let read;
|
|
208
193
|
const bytes = () => {
|
|
209
194
|
read ??= stream
|
|
210
|
-
? drain(provider, stream, limit)
|
|
195
|
+
? drain(provider, stream, limit, signal)
|
|
211
196
|
: response.arrayBuffer().then((buffer) => {
|
|
212
197
|
if (buffer.byteLength > limit) {
|
|
213
198
|
throw oversized(provider, limit, `${buffer.byteLength} bytes`);
|
|
@@ -232,7 +217,7 @@ function boundedResponse(provider, response, limit) {
|
|
|
232
217
|
ok: response.ok,
|
|
233
218
|
headers: response.headers,
|
|
234
219
|
cancel: async () => {
|
|
235
|
-
await response.body?.cancel();
|
|
220
|
+
await (stream ?? response.body)?.cancel();
|
|
236
221
|
},
|
|
237
222
|
bytes,
|
|
238
223
|
text,
|
|
@@ -248,14 +233,16 @@ function boundedResponse(provider, response, limit) {
|
|
|
248
233
|
* A BYOB reader could stop at `keep + 1` exactly, but not every runtime's
|
|
249
234
|
* fetch body offers one, and one chunk is a bounded overrun.
|
|
250
235
|
*/
|
|
251
|
-
async function readPrefix(stream, keep) {
|
|
252
|
-
const reader = stream
|
|
236
|
+
async function readPrefix(stream, keep, signal) {
|
|
237
|
+
const { reader, close } = ownedReader(stream, signal);
|
|
253
238
|
const chunks = [];
|
|
254
239
|
let total = 0;
|
|
255
240
|
let truncated = false;
|
|
256
241
|
try {
|
|
257
242
|
while (true) {
|
|
258
243
|
const { done, value } = await reader.read();
|
|
244
|
+
if (signal.aborted)
|
|
245
|
+
throw signal.reason;
|
|
259
246
|
if (done)
|
|
260
247
|
break;
|
|
261
248
|
if (!value)
|
|
@@ -273,7 +260,7 @@ async function readPrefix(stream, keep) {
|
|
|
273
260
|
await reader.cancel().catch(() => { });
|
|
274
261
|
}
|
|
275
262
|
finally {
|
|
276
|
-
|
|
263
|
+
close();
|
|
277
264
|
}
|
|
278
265
|
const bytes = new Uint8Array(total);
|
|
279
266
|
let offset = 0;
|
|
@@ -293,6 +280,15 @@ async function jsonResult(read) {
|
|
|
293
280
|
return { parseError: cause };
|
|
294
281
|
}
|
|
295
282
|
}
|
|
283
|
+
// Each response a mapper reads, and the ledger entry of the request it answered.
|
|
284
|
+
const answeredRequests = new WeakMap();
|
|
285
|
+
/**
|
|
286
|
+
* Test-only legacy response lookup for renewal-provenance fixtures outside
|
|
287
|
+
* the kit. Production renewal uses the kit ledger. Undefined for other objects.
|
|
288
|
+
*/
|
|
289
|
+
export function answeredRequest(response) {
|
|
290
|
+
return answeredRequests.get(response);
|
|
291
|
+
}
|
|
296
292
|
/**
|
|
297
293
|
* Build the transport a connector sends every request through. `maxResponseBytes`
|
|
298
294
|
* is required rather than defaulted: what counts as an absurd response is a fact
|
|
@@ -303,84 +299,182 @@ export function guardedFetch(options) {
|
|
|
303
299
|
if (!Number.isInteger(limit) || limit < 1) {
|
|
304
300
|
throw new Error(`${provider} maxResponseBytes must be a whole number of bytes >= 1; received ${String(limit)}.`);
|
|
305
301
|
}
|
|
302
|
+
const authFirst = options.authBeforeExecution;
|
|
303
|
+
if (authFirst &&
|
|
304
|
+
(typeof authFirst.evidence !== "string" || !authFirst.evidence.trim() || typeof authFirst.matches !== "function")) {
|
|
305
|
+
throw new Error(`${provider} authBeforeExecution needs nonempty evidence and a narrow matches predicate.`);
|
|
306
|
+
}
|
|
306
307
|
const base = confinementBase(provider, options.baseUrl);
|
|
307
308
|
// "" for a root base, so joining never doubles the separator.
|
|
308
309
|
const basePath = base.pathname.replace(/\/+$/, "");
|
|
309
310
|
const constantHeaders = { ...options.headers };
|
|
310
311
|
const send = options.fetch ?? ((url, init) => fetch(url, init));
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
options.admit?.(url, request);
|
|
317
|
-
const headers = { ...constantHeaders };
|
|
318
|
-
for (const [name, value] of Object.entries(request.headers ?? {})) {
|
|
319
|
-
if (value !== undefined)
|
|
320
|
-
headers[name] = value;
|
|
321
|
-
}
|
|
322
|
-
if (request.body !== undefined && !hasHeader(headers, "content-type")) {
|
|
323
|
-
headers["Content-Type"] = "application/json";
|
|
312
|
+
// The reviewed predicate is trusted code; one that throws proves nothing.
|
|
313
|
+
const authenticatedFirst = (response, request) => {
|
|
314
|
+
try {
|
|
315
|
+
return (authFirst?.matches({ status: 401, headers: response.headers, method: request.method, path: request.path }) ===
|
|
316
|
+
true);
|
|
324
317
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
// worth failing on, not a precedence question worth resolving quietly.
|
|
328
|
-
for (const [name, value] of Object.entries(await options.authenticate(ctx))) {
|
|
329
|
-
const shadow = hasHeader(headers, name);
|
|
330
|
-
if (shadow) {
|
|
331
|
-
throw new ConnectorCallError("invalid_args", `A ${provider} request may not set the ${shadow} header; authentication is connector-owned.`);
|
|
332
|
-
}
|
|
333
|
-
headers[name] = value;
|
|
334
|
-
sentSecretsFor(ctx).header(value);
|
|
318
|
+
catch {
|
|
319
|
+
return false;
|
|
335
320
|
}
|
|
321
|
+
};
|
|
322
|
+
return async function guarded(request, ctx, map) {
|
|
323
|
+
const auxiliary = options.auxiliary === true || request.auxiliary === true;
|
|
324
|
+
// A call that already ended sends nothing more, not even a credential read.
|
|
325
|
+
assertProviderCallOpen(ctx, provider, auxiliary);
|
|
326
|
+
const signal = viewSignal(ctx, auxiliary);
|
|
327
|
+
let dispatch = providerRequest(ctx, auxiliary, provider, {
|
|
328
|
+
method: request.method,
|
|
329
|
+
...(request.classification ? { classification: request.classification } : {}),
|
|
330
|
+
...(request.idempotencyKey === undefined ? {} : { key: request.idempotencyKey }),
|
|
331
|
+
});
|
|
336
332
|
let response;
|
|
333
|
+
let result;
|
|
337
334
|
try {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
335
|
+
// Own the whole helper, including refusal cleanup between I/O and the
|
|
336
|
+
// mapper. Settle its ledger outside the race so revocation cannot skip
|
|
337
|
+
// the dispatched write's final verdict.
|
|
338
|
+
result = await settleOnClose(ctx, () => requestOnce(request, ctx, map), auxiliary);
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
// Auxiliary preparation has always refused an ended request in the
|
|
342
|
+
// provider's words, even if closure wins the whole-helper race first.
|
|
343
|
+
const source = callerSignal(ctx);
|
|
344
|
+
if (auxiliary || !(source?.aborted && error === source.reason))
|
|
345
|
+
assertProviderCallOpen(ctx, provider, auxiliary);
|
|
346
|
+
// No rejection proves a dispatched write unsent: fetch may have made
|
|
347
|
+
// several attempts behind it (a 421 or HTTP/2 refused-stream resend, a
|
|
348
|
+
// retrying dispatcher), whatever the last one reports.
|
|
349
|
+
throw providerCallFailure(ctx, { title: provider }, vendorRefusal(error, dispatch), {
|
|
350
|
+
dispatched: dispatch.dispatched && !auxiliary,
|
|
351
|
+
...(response ? { status: response.status } : {}),
|
|
352
|
+
fallback: dispatch.classification ?? "write",
|
|
353
|
+
repeatable: request.idempotencyKey !== undefined,
|
|
354
|
+
authBeforeExecution: dispatch.authBeforeExecution === true,
|
|
355
|
+
...(auxiliary ? {} : { request: dispatch }),
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
const unknown = providerCallResult(ctx, dispatch.classification ?? "write", dispatch);
|
|
359
|
+
if (unknown)
|
|
360
|
+
throw unknown;
|
|
361
|
+
return result;
|
|
362
|
+
async function requestOnce(request, ctx, map) {
|
|
363
|
+
if (request.body !== undefined && request.rawBody !== undefined) {
|
|
364
|
+
throw new Error(`A ${provider} request cannot carry both a JSON body and a raw body.`);
|
|
365
|
+
}
|
|
366
|
+
const url = confinedUrl(provider, base, basePath, request);
|
|
367
|
+
options.admit?.(url, request);
|
|
368
|
+
const headers = { ...constantHeaders };
|
|
369
|
+
for (const [name, value] of Object.entries(request.headers ?? {})) {
|
|
370
|
+
if (value !== undefined) {
|
|
371
|
+
const previous = hasHeader(headers, name);
|
|
372
|
+
if (previous)
|
|
373
|
+
delete headers[previous];
|
|
374
|
+
headers[name] = value;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (request.body !== undefined && !hasHeader(headers, "content-type")) {
|
|
378
|
+
headers["Content-Type"] = "application/json";
|
|
379
|
+
}
|
|
380
|
+
// Authentication is applied last and may not be shadowed. A caller-shaped
|
|
381
|
+
// header that reaches this point wearing the credential's name is a bug
|
|
382
|
+
// worth failing on, not a precedence question worth resolving quietly.
|
|
383
|
+
let authentication;
|
|
384
|
+
try {
|
|
385
|
+
authentication = await duringViewCall(ctx, () => Promise.resolve(options.authenticate(ctx)), auxiliary);
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
assertProviderCallOpen(ctx, provider, auxiliary);
|
|
389
|
+
throw error;
|
|
390
|
+
}
|
|
391
|
+
for (const [name, value] of Object.entries(authentication)) {
|
|
354
392
|
const shadow = hasHeader(headers, name);
|
|
355
393
|
if (shadow) {
|
|
356
|
-
throw new ConnectorCallError("invalid_args", `A ${provider} request
|
|
394
|
+
throw new ConnectorCallError("invalid_args", `A ${provider} request may not set the ${shadow.toLowerCase()} header; authentication is connector-owned.`);
|
|
357
395
|
}
|
|
358
396
|
headers[name] = value;
|
|
397
|
+
sentSecretsFor(ctx).header(value);
|
|
398
|
+
}
|
|
399
|
+
try {
|
|
400
|
+
const body = request.body !== undefined ? JSON.stringify(request.body) : request.rawBody;
|
|
401
|
+
const init = {
|
|
402
|
+
method: request.method,
|
|
403
|
+
headers,
|
|
404
|
+
...(body !== undefined ? { body } : {}),
|
|
405
|
+
// A redirect is an instruction to re-send the credential to whatever
|
|
406
|
+
// origin `Location` names, and a confinement a redirect can undo was
|
|
407
|
+
// never one. Refused below rather than followed.
|
|
408
|
+
redirect: "manual",
|
|
409
|
+
signal,
|
|
410
|
+
};
|
|
411
|
+
sentSecretsFor(ctx).request(url, init);
|
|
412
|
+
for (const [name, value] of Object.entries(request.visibleHeaders ?? {})) {
|
|
413
|
+
const shadow = hasHeader(headers, name);
|
|
414
|
+
if (shadow) {
|
|
415
|
+
throw new ConnectorCallError("invalid_args", `A ${provider} request sets the ${shadow.toLowerCase()} header twice.`);
|
|
416
|
+
}
|
|
417
|
+
headers[name] = value;
|
|
418
|
+
}
|
|
419
|
+
if (oauthSenders.has(send) && isRecordingFetch(ctx.oauth?.fetch))
|
|
420
|
+
handoffProviderRequest(init, dispatch);
|
|
421
|
+
else
|
|
422
|
+
dispatchProviderRequest(ctx, dispatch, headers, init.method);
|
|
423
|
+
response = await duringViewCall(ctx, async () => {
|
|
424
|
+
const answer = await send(url.toString(), init, ctx);
|
|
425
|
+
// A custom transport may ignore cancellation. Its late response
|
|
426
|
+
// owns no live mapper or body after revocation.
|
|
427
|
+
if (signal.aborted) {
|
|
428
|
+
void answer.body?.cancel().catch(() => { });
|
|
429
|
+
throw signal.reason;
|
|
430
|
+
}
|
|
431
|
+
return answer;
|
|
432
|
+
}, auxiliary);
|
|
433
|
+
const answered = responseProviderRequest(response, dispatch);
|
|
434
|
+
if (answered)
|
|
435
|
+
dispatch = answered;
|
|
436
|
+
else
|
|
437
|
+
recordProviderResponse(dispatch, response.status);
|
|
438
|
+
}
|
|
439
|
+
catch (cause) {
|
|
440
|
+
// A cancelled wait is the caller's own reason, not a network failure.
|
|
441
|
+
if (signal.aborted && cause === signal.reason)
|
|
442
|
+
throw cause;
|
|
443
|
+
if (cause instanceof ConnectorCallError)
|
|
444
|
+
throw cause;
|
|
445
|
+
throw unavailableCallError(cause, url.href, `Could not reach the ${provider} API.`, signal);
|
|
446
|
+
}
|
|
447
|
+
if (response.status === 401 && authenticatedFirst(response, request))
|
|
448
|
+
dispatch.authBeforeExecution = true;
|
|
449
|
+
if (REDIRECT_STATUSES.has(response.status)) {
|
|
450
|
+
await response.body?.cancel().catch(() => { });
|
|
451
|
+
throw new ConnectorCallError("connector_call_failed", `${provider} answered HTTP ${response.status} with a redirect; this connector talks to exactly one origin and never forwards its credential to another.`, { retryable: false });
|
|
452
|
+
}
|
|
453
|
+
const declared = Number(response.headers.get("content-length"));
|
|
454
|
+
// A prefix read takes only what it was asked for, so a long body is not
|
|
455
|
+
// a reason to refuse it; the ceiling still bounds what is read. A HEAD
|
|
456
|
+
// response has no body: its Content-Length describes the GET
|
|
457
|
+
// representation, which is exactly what a HEAD asks about.
|
|
458
|
+
if (Number.isFinite(declared) &&
|
|
459
|
+
declared > limit &&
|
|
460
|
+
request.method !== "HEAD" &&
|
|
461
|
+
!(request.prefixOnly && response.ok)) {
|
|
462
|
+
await response.body?.cancel().catch(() => { });
|
|
463
|
+
throw oversized(provider, limit, `a declared ${declared} bytes`);
|
|
464
|
+
}
|
|
465
|
+
const answer = boundedResponse(provider, response, limit, signal);
|
|
466
|
+
answeredRequests.set(answer, dispatch);
|
|
467
|
+
const abort = () => {
|
|
468
|
+
void response?.body?.cancel().catch(() => { });
|
|
469
|
+
};
|
|
470
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
471
|
+
try {
|
|
472
|
+
return await duringViewCall(ctx, () => Promise.resolve(map(answer, ctx)), auxiliary);
|
|
473
|
+
}
|
|
474
|
+
finally {
|
|
475
|
+
signal.removeEventListener("abort", abort);
|
|
359
476
|
}
|
|
360
|
-
response = await send(url.toString(), init, ctx);
|
|
361
|
-
}
|
|
362
|
-
catch (cause) {
|
|
363
|
-
if (cause instanceof ConnectorCallError)
|
|
364
|
-
throw cause;
|
|
365
|
-
throw unavailableCallError(cause, url.href, `Could not reach the ${provider} API.`, ctx.signal);
|
|
366
|
-
}
|
|
367
|
-
if (REDIRECT_STATUSES.has(response.status)) {
|
|
368
|
-
await response.body?.cancel().catch(() => { });
|
|
369
|
-
throw new ConnectorCallError("connector_call_failed", `${provider} answered HTTP ${response.status} with a redirect; this connector talks to exactly one origin and never forwards its credential to another.`, { retryable: false });
|
|
370
|
-
}
|
|
371
|
-
const declared = Number(response.headers.get("content-length"));
|
|
372
|
-
// A prefix read takes only what it was asked for, so a long body is not
|
|
373
|
-
// a reason to refuse it; the ceiling still bounds what is read. A HEAD
|
|
374
|
-
// response has no body: its Content-Length describes the GET
|
|
375
|
-
// representation, which is exactly what a HEAD asks about.
|
|
376
|
-
if (Number.isFinite(declared) &&
|
|
377
|
-
declared > limit &&
|
|
378
|
-
request.method !== "HEAD" &&
|
|
379
|
-
!(request.prefixOnly && response.ok)) {
|
|
380
|
-
await response.body?.cancel().catch(() => { });
|
|
381
|
-
throw oversized(provider, limit, `a declared ${declared} bytes`);
|
|
382
477
|
}
|
|
383
|
-
return await map(boundedResponse(provider, response, limit), ctx);
|
|
384
478
|
};
|
|
385
479
|
}
|
|
386
480
|
/** The stored spelling of a header already present, case-insensitively. */
|
|
@@ -1,13 +1,18 @@
|
|
|
1
|
+
import { runEdge } from "../runtime/run.js";
|
|
2
|
+
import { authFlow, authProgram, registerAuth, authStorage } from "../auth/native.js";
|
|
1
3
|
import { isSpecType } from "@modelcontextprotocol/client";
|
|
2
4
|
import { failureRecord, logFailure } from "../operator-record.js";
|
|
3
5
|
import { sentSecretsForRequest } from "../sent-secrets.js";
|
|
4
6
|
import { assertDownstreamOutputSafe } from "../downstream-input-context.js";
|
|
5
7
|
import { negotiationKeys, NEGOTIATION_TTL_SECONDS } from "../storage/keys.js";
|
|
6
8
|
/** Verdicts carry data only. Clients, signals and secrets remain request-local. */
|
|
7
|
-
export
|
|
9
|
+
export function readNegotiation(ctx, digest) {
|
|
10
|
+
return runEdge(authProgram(readNegotiationNative, [ctx, digest]));
|
|
11
|
+
}
|
|
12
|
+
const readNegotiationNative = /* @__PURE__ */ registerAuth(readNegotiation, (ctx, digest) => authFlow(function* () {
|
|
8
13
|
let prior;
|
|
9
14
|
try {
|
|
10
|
-
const raw =
|
|
15
|
+
const raw = (yield authStorage(ctx.storage, "get", [negotiationKeys.verdict(digest)]));
|
|
11
16
|
if (!raw)
|
|
12
17
|
return;
|
|
13
18
|
const value = JSON.parse(raw);
|
|
@@ -21,26 +26,31 @@ export async function readNegotiation(ctx, digest) {
|
|
|
21
26
|
catch (error) {
|
|
22
27
|
logFailure(ctx.logger, "negotiation cache read failed", failureRecord({}, error));
|
|
23
28
|
}
|
|
24
|
-
// Privacy refusals are not cache misses and must not enter cache diagnostics.
|
|
25
29
|
if (prior)
|
|
26
30
|
assertDownstreamOutputSafe(ctx.requestScope ?? ctx, prior);
|
|
27
31
|
return prior;
|
|
32
|
+
}));
|
|
33
|
+
export function storeNegotiation(ctx, digest, prior) {
|
|
34
|
+
return runEdge(authProgram(storeNegotiationNative, [ctx, digest, prior]));
|
|
28
35
|
}
|
|
29
|
-
|
|
36
|
+
const storeNegotiationNative = /* @__PURE__ */ registerAuth(storeNegotiation, (ctx, digest, prior) => authFlow(function* () {
|
|
30
37
|
if (ctx.signal?.aborted)
|
|
31
38
|
return;
|
|
32
39
|
assertDownstreamOutputSafe(ctx.requestScope ?? ctx, prior);
|
|
33
|
-
// DiscoverResult instructions, metadata and capability keys can echo auth.
|
|
34
40
|
const clean = sentSecretsForRequest(ctx.requestScope ?? ctx).redactCredentials(prior);
|
|
35
41
|
if (clean.kind === "modern" && !isSpecType.DiscoverResult(clean.discover))
|
|
36
42
|
return;
|
|
37
43
|
try {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
(yield authStorage(ctx.storage, "set", [
|
|
45
|
+
negotiationKeys.verdict(digest),
|
|
46
|
+
JSON.stringify({
|
|
47
|
+
prior: clean,
|
|
48
|
+
expiresAt: Date.now() + NEGOTIATION_TTL_SECONDS * 1000,
|
|
49
|
+
}),
|
|
50
|
+
{ ttlSeconds: NEGOTIATION_TTL_SECONDS },
|
|
51
|
+
]));
|
|
42
52
|
}
|
|
43
53
|
catch (error) {
|
|
44
54
|
logFailure(ctx.logger, "negotiation cache write failed", failureRecord({}, error));
|
|
45
55
|
}
|
|
46
|
-
}
|
|
56
|
+
}));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type RemoteOAuthClient } from "../auth/downstream-client-metadata.js";
|
|
2
2
|
import type { FetchLike, Transport } from "@modelcontextprotocol/client";
|
|
3
3
|
import { ConnectorCallError } from "../errors.js";
|
|
4
|
-
import type { Connector, ConnectorCallAdmissionPolicy, ConnectorContext, ConnectorCredentialConfig, ConnectorUsageGuide, Logger
|
|
4
|
+
import type { Connector, ConnectorCallAdmissionPolicy, ConnectorContext, ConnectorCredentialConfig, ConnectorUsageGuide, Logger } from "../types.js";
|
|
5
5
|
/**
|
|
6
6
|
* A static downstream credential supplied through the connection in the
|
|
7
7
|
* operator UI rather than baked into deployment source.
|
|
@@ -29,7 +29,7 @@ interface RemoteMcpCredentialAuth {
|
|
|
29
29
|
* an empty string) sends the stored value verbatim, for an endpoint that
|
|
30
30
|
* reads a bare key. A scheme whose last token is `Basic` declares
|
|
31
31
|
* HTTP Basic credentials: the stored `user:secret` is base64-encoded first,
|
|
32
|
-
* so `"Basic"` produces `Basic <base64>` and
|
|
32
|
+
* so `"Basic"` produces `Basic <base64>` and a nested
|
|
33
33
|
* `"Bearer Basic"` produces `Bearer Basic <base64>`.
|
|
34
34
|
*/
|
|
35
35
|
scheme?: string | null;
|
|
@@ -52,20 +52,6 @@ export type RemoteMcpAuth = {
|
|
|
52
52
|
/** Space-separated default scopes when the resource server does not advertise them. */
|
|
53
53
|
scope?: string;
|
|
54
54
|
};
|
|
55
|
-
/**
|
|
56
|
-
* Apply a maintained provider's slot copy and header framing to credential
|
|
57
|
-
* auth the deployment left bare.
|
|
58
|
-
*
|
|
59
|
-
* A provider knows what its own key is called and how the endpoint expects it
|
|
60
|
-
* framed; a deployment that states either one keeps its answer. Every other
|
|
61
|
-
* auth shape passes through untouched, so a provider can hand this its whole
|
|
62
|
-
* `auth` option without branching first.
|
|
63
|
-
*/
|
|
64
|
-
export declare function withCredentialDefaults(auth: RemoteMcpAuth, defaults: {
|
|
65
|
-
credential: ConnectorCredentialConfig;
|
|
66
|
-
/** Provider framing; omit to leave the `Bearer` default in place. */
|
|
67
|
-
scheme?: string | null;
|
|
68
|
-
}): RemoteMcpAuth;
|
|
69
55
|
export type RemoteMcpRedirectPolicy = "none" | "same-origin";
|
|
70
56
|
export interface RemoteMcpOptions {
|
|
71
57
|
url: string;
|
|
@@ -96,20 +82,12 @@ export interface RemoteMcpOptions {
|
|
|
96
82
|
skills?: boolean;
|
|
97
83
|
auth?: RemoteMcpAuth;
|
|
98
84
|
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* tool is read-only only when it says so explicitly; a listed tool whose
|
|
104
|
-
* `schemaDigest` no longer matches, or cannot be checked, is served as a
|
|
105
|
-
* write. Setting it also reports
|
|
106
|
-
* catalog drift against the list: unclassified, unserved, contradicted, and
|
|
107
|
-
* schema-changed tools, as counts. The connector carries it as
|
|
108
|
-
* `classification`, and its `listTools` returns the downstream's listing
|
|
109
|
-
* unclassified: the registry classifies every read. Omit it to keep the
|
|
110
|
-
* downstream's own annotations, which still fail closed.
|
|
85
|
+
* Non-secret resource context headers. Never credentials: use auth for those.
|
|
86
|
+
* Auth-owned, HTTP transport/session and OAuth client-auth headers are
|
|
87
|
+
* reserved. Cannot be combined with inline headers auth. Sent only at the
|
|
88
|
+
* resource URL path; echoed values are redacted at eight characters or more.
|
|
111
89
|
*/
|
|
112
|
-
|
|
90
|
+
headers?: Record<string, string>;
|
|
113
91
|
/**
|
|
114
92
|
* Downstream MCP version-negotiation mode. Defaults to `"auto"`, which
|
|
115
93
|
* probes with `server/discover` and falls back to the legacy lifecycle when
|
|
@@ -123,7 +101,9 @@ export interface RemoteMcpOptions {
|
|
|
123
101
|
* rejected. `"same-origin"` follows at most five redirects while preserving
|
|
124
102
|
* standard 301/302/303/307/308 method semantics. Cross-origin redirects and
|
|
125
103
|
* HTTPS downgrades are always refused, so credentials never cross the
|
|
126
|
-
* configured request's origin.
|
|
104
|
+
* configured request's origin. A write is never sent to a redirect target
|
|
105
|
+
* under either policy: a redirect answering a write fails it as an unknown
|
|
106
|
+
* outcome (INV-9), so set `url` to the endpoint that answers directly.
|
|
127
107
|
*/
|
|
128
108
|
redirects?: RemoteMcpRedirectPolicy;
|
|
129
109
|
/**
|