@zackbart/connecta 0.34.0 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +393 -0
- package/README.md +62 -105
- package/SECURITY.md +3 -2
- package/bin/connecta.mjs +41 -16
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.d.ts +2 -1
- package/dist/access-tokens.js +66 -30
- package/dist/activity.d.ts +1 -1
- package/dist/activity.js +2 -0
- package/dist/agent-config.d.ts +23 -0
- package/dist/agent-config.js +59 -0
- package/dist/agent-description.js +16 -0
- package/dist/agent-identity.js +4 -4
- package/dist/agent-observation.js +39 -0
- package/dist/auth/clerk-sdk/client.d.ts +2 -0
- package/dist/auth/clerk-sdk/generated.js +554 -563
- package/dist/auth/clerk-transport.js +70 -3
- package/dist/auth/clerk.d.ts +13 -0
- package/dist/auth/clerk.js +653 -246
- package/dist/auth/cloudflare-access.js +46 -42
- package/dist/auth/downstream-client-metadata.js +2 -2
- package/dist/auth/downstream-oauth.js +2051 -1160
- package/dist/auth/downstream-revocation.js +88 -88
- package/dist/auth/native-call.js +20 -0
- package/dist/auth/native.js +184 -0
- package/dist/auth/oauth-start-reset.js +11 -5
- package/dist/auth/oauth-token-response.js +121 -102
- package/dist/auth/oauth-v2-migration.js +97 -78
- package/dist/auth/renewable-grant.js +10 -0
- package/dist/auth/static-oauth.js +663 -271
- package/dist/auth/vercel-agent.js +266 -0
- package/dist/auth-admission.js +204 -0
- package/dist/auth-elicitation.js +350 -241
- package/dist/authorization-handoff.js +164 -90
- package/dist/call-admission-context.js +3 -10
- package/dist/call-admission.js +2 -0
- package/dist/call-classification.js +9 -0
- package/dist/call-recovery.js +32 -13
- package/dist/canonical-json.js +8 -0
- package/dist/catalog-cache.js +369 -213
- package/dist/catalog-service.js +659 -530
- package/dist/catalog.js +5 -1
- package/dist/concurrency.js +0 -25
- package/dist/config-defaults.d.ts +1 -0
- package/dist/config-defaults.js +1 -0
- package/dist/config-schema.d.ts +3 -1
- package/dist/config-schema.js +13 -3
- package/dist/config.d.ts +40 -6
- package/dist/config.js +40 -26
- package/dist/connector-caller.d.ts +4 -0
- package/dist/connector-caller.js +17 -4
- package/dist/connectors/api-connector.d.ts +15 -7
- package/dist/connectors/api-connector.js +147 -57
- package/dist/connectors/guarded-fetch.d.ts +51 -17
- package/dist/connectors/guarded-fetch.js +215 -121
- package/dist/connectors/negotiation-cache.js +20 -10
- package/dist/connectors/option-shapes.js +1 -1
- package/dist/connectors/remote-mcp.d.ts +12 -30
- package/dist/connectors/remote-mcp.js +960 -564
- package/dist/d1.js +10 -243
- package/dist/derived-context.js +5 -0
- package/dist/describe-config.d.ts +5 -1
- package/dist/describe-config.js +3 -0
- package/dist/downstream-input.js +347 -260
- package/dist/errors.d.ts +87 -6
- package/dist/errors.js +114 -4
- package/dist/execute.js +87 -50
- package/dist/executor-admission.js +3 -0
- package/dist/executors/quickjs.js +163 -112
- package/dist/guarded-transport.js +50 -0
- package/dist/guest-runtime.js +1 -1
- package/dist/http/client.js +439 -0
- package/dist/http/errors.js +558 -0
- package/dist/http/fields.js +49 -0
- package/dist/http/hatch-paging.js +318 -0
- package/dist/http/hatch.js +1279 -0
- package/dist/http/json-schema.js +267 -0
- package/dist/http/node-transport.js +241 -0
- package/dist/http/oauth.js +162 -0
- package/dist/http/paging.js +228 -0
- package/dist/http/provider.js +221 -0
- package/dist/http/safety.js +82 -0
- package/dist/http/services.js +79 -0
- package/dist/http/tools.js +383 -0
- package/dist/http/vendor-index.js +138 -0
- package/dist/http/vendor.js +448 -0
- package/dist/http/write-request.js +66 -0
- package/dist/http-policy.d.ts +11 -0
- package/dist/http-policy.js +45 -0
- package/dist/inbound-credential.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -0
- package/dist/invocation-auth.js +30 -19
- package/dist/invocation.js +221 -87
- package/dist/mcp-result.js +0 -4
- package/dist/membership.d.ts +4 -0
- package/dist/meta-output.js +1 -1
- package/dist/meta-tools.js +534 -408
- package/dist/native-recovery.js +52 -0
- package/dist/native-tools.js +382 -0
- package/dist/node.d.ts +4 -3
- package/dist/node.js +45 -8
- package/dist/oauth-handoff.js +177 -104
- package/dist/oauth-partition.js +6 -20
- package/dist/oauth-sealing.js +6 -7
- package/dist/operation-vocabulary.js +101 -0
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +17 -4
- package/dist/operator-ui/agents.d.ts +5 -4
- package/dist/operator-ui/agents.js +17 -7
- package/dist/operator-ui/config-summary.js +8 -4
- package/dist/operator-ui/connection-map.js +9 -6
- package/dist/operator-ui/contract.d.ts +7 -3
- package/dist/operator-ui/endpoints.js +5 -14
- package/dist/operator-ui/fix-prompts.d.ts +1 -1
- package/dist/operator-ui/fix-prompts.js +0 -8
- package/dist/operator-ui/generated.js +3 -3
- package/dist/operator-ui/health.js +52 -0
- package/dist/operator-ui/model.d.ts +2 -3
- package/dist/operator-ui/overview.js +11 -19
- package/dist/operator-ui/setup-commands.js +0 -4
- package/dist/operator-ui/view.d.ts +5 -29
- package/dist/operator-ui/view.js +3 -59
- package/dist/program-writes.js +47 -26
- package/dist/provider-call-facts.d.ts +115 -0
- package/dist/provider-call-facts.js +182 -0
- package/dist/provider-call.d.ts +112 -0
- package/dist/provider-call.js +271 -0
- package/dist/provider.d.ts +71 -32
- package/dist/provider.js +134 -40
- package/dist/providers/_shared/google/hatch.js +91 -0
- package/dist/providers/_shared/google/kit.js +537 -0
- package/dist/providers/_shared/google/result-size.js +15 -2
- package/dist/providers/_shared/google/service-account.d.ts +2 -1
- package/dist/providers/_shared/google/service-account.js +8 -4
- package/dist/providers/_shared/google/workspace.d.ts +8 -209
- package/dist/providers/_shared/google/workspace.js +7 -426
- package/dist/providers/_shared/rest/compact-index.js +86 -0
- package/dist/providers/_shared/rest/dispatch.js +3 -23
- package/dist/providers/_shared/rest/kit.js +546 -0
- package/dist/providers/_shared/rest/operation-index.js +230 -14
- package/dist/providers/_shared/rest/response-media.js +11 -0
- package/dist/providers/_shared/rest/tools.js +101 -78
- package/dist/providers/_shared/rest/value-safety.js +353 -195
- package/dist/providers/_shared/sensitive-fields.js +27 -0
- package/dist/providers/breeze/api.js +584 -0
- package/dist/providers/breeze/index.d.ts +3 -3
- package/dist/providers/breeze/index.js +67 -1420
- package/dist/providers/breeze/openapi.generated.js +60 -0
- package/dist/providers/breeze/skill.generated.js +2 -6
- package/dist/providers/breeze/tools.js +696 -0
- package/dist/providers/ccb/api.js +496 -0
- package/dist/providers/ccb/index.d.ts +17 -37
- package/dist/providers/ccb/index.js +77 -1702
- package/dist/providers/ccb/lib/constants.d.ts +34 -0
- package/dist/providers/ccb/lib/constants.js +74 -0
- package/dist/providers/ccb/lib/guide.js +9 -0
- package/dist/providers/ccb/lib/verdicts.js +387 -0
- package/dist/providers/ccb/openapi.generated.js +397 -0
- package/dist/providers/ccb/safety.js +295 -0
- package/dist/providers/ccb/skill.generated.js +2 -6
- package/dist/providers/ccb/tools.js +853 -0
- package/dist/providers/cloudflare/api.js +1066 -0
- package/dist/providers/cloudflare/index.d.ts +7 -22
- package/dist/providers/cloudflare/index.js +80 -194
- package/dist/providers/cloudflare/lib/config.d.ts +16 -0
- package/dist/providers/cloudflare/lib/config.js +2 -0
- package/dist/providers/cloudflare/lib/discovery.js +29 -0
- package/dist/providers/cloudflare/{graphql.js → lib/graphql.js} +1 -1
- package/dist/providers/cloudflare/lib/hatch.js +103 -0
- package/dist/providers/cloudflare/lib/path-bounds.js +28 -0
- package/dist/providers/cloudflare/lib/response-media.js +75 -0
- package/dist/providers/cloudflare/openapi.generated.js +3299 -3208
- package/dist/providers/cloudflare/{value-safety.js → safety.js} +538 -18
- package/dist/providers/cloudflare/skill.generated.js +2 -10
- package/dist/providers/cloudflare/tools.js +360 -0
- package/dist/providers/docs/api.js +51 -0
- package/dist/providers/docs/index.d.ts +1 -12
- package/dist/providers/docs/index.js +53 -1165
- package/dist/providers/docs/lib/constants.d.ts +18 -0
- package/dist/providers/docs/lib/constants.js +18 -0
- package/dist/providers/docs/lib/discovery.js +26 -0
- package/dist/providers/docs/lib/requests.js +48 -0
- package/dist/providers/docs/lib/schemas.js +168 -0
- package/dist/providers/docs/openapi.generated.js +16 -0
- package/dist/providers/docs/safety.js +54 -0
- package/dist/providers/docs/skill.generated.js +2 -6
- package/dist/providers/docs/tools.js +802 -0
- package/dist/providers/drive/api.js +177 -0
- package/dist/providers/drive/index.d.ts +1 -12
- package/dist/providers/drive/index.js +38 -1448
- package/dist/providers/drive/lib/constants.d.ts +6 -0
- package/dist/providers/drive/lib/constants.js +6 -0
- package/dist/providers/drive/lib/contracts.js +532 -0
- package/dist/providers/drive/lib/hatch.js +197 -0
- package/dist/providers/drive/openapi.generated.js +77 -0
- package/dist/providers/drive/safety.js +90 -0
- package/dist/providers/drive/skill.generated.js +2 -6
- package/dist/providers/drive/tools.js +1290 -0
- package/dist/providers/forms/api.js +71 -0
- package/dist/providers/forms/index.d.ts +1 -12
- package/dist/providers/forms/index.js +49 -1145
- package/dist/providers/forms/lib/constants.d.ts +14 -0
- package/dist/providers/forms/lib/constants.js +19 -0
- package/dist/providers/forms/lib/hatch.js +72 -0
- package/dist/providers/forms/openapi.generated.js +23 -0
- package/dist/providers/forms/safety.js +34 -0
- package/dist/providers/forms/skill.generated.js +2 -8
- package/dist/providers/forms/tools.js +1024 -0
- package/dist/providers/github/api.js +150 -0
- package/dist/providers/github/index.d.ts +4 -5
- package/dist/providers/github/index.js +30 -87
- package/dist/providers/github/{auth.d.ts → lib/auth.d.ts} +5 -3
- package/dist/providers/github/{auth.js → lib/auth.js} +40 -43
- package/dist/providers/github/lib/discovery.js +132 -0
- package/dist/providers/github/lib/operations.js +58 -0
- package/dist/providers/github/{scope.d.ts → lib/scope.d.ts} +2 -0
- package/dist/providers/github/{scope.js → lib/scope.js} +25 -1
- package/dist/providers/github/openapi.generated.js +1216 -0
- package/dist/providers/github/safety.js +216 -0
- package/dist/providers/github/skill.generated.js +2 -5
- package/dist/providers/github/tools.js +506 -0
- package/dist/providers/gmail/api.js +114 -0
- package/dist/providers/gmail/index.d.ts +1 -12
- package/dist/providers/gmail/index.js +73 -1621
- package/dist/providers/gmail/lib/constants.d.ts +13 -0
- package/dist/providers/gmail/lib/constants.js +16 -0
- package/dist/providers/gmail/lib/index.js +173 -0
- package/dist/providers/gmail/lib/schemas.js +176 -0
- package/dist/providers/gmail/lib/threading.js +120 -0
- package/dist/providers/gmail/openapi.generated.js +92 -0
- package/dist/providers/gmail/safety.js +21 -0
- package/dist/providers/gmail/skill.generated.js +2 -6
- package/dist/providers/gmail/tools.js +1435 -0
- package/dist/providers/overflow/api.js +1008 -0
- package/dist/providers/overflow/index.d.ts +3 -17
- package/dist/providers/overflow/index.js +60 -1488
- package/dist/providers/overflow/skill.generated.js +2 -5
- package/dist/providers/overflow/tools.js +873 -0
- package/dist/providers/planning-center/api.js +467 -0
- package/dist/providers/planning-center/index.d.ts +6 -35
- package/dist/providers/planning-center/index.js +69 -2445
- package/dist/providers/planning-center/lib/constants.d.ts +58 -0
- package/dist/providers/planning-center/lib/constants.js +83 -0
- package/dist/providers/planning-center/lib/discovery.js +40 -0
- package/dist/providers/planning-center/lib/guide.js +39 -0
- package/dist/providers/planning-center/lib/json-api.js +83 -0
- package/dist/providers/planning-center/lib/verdicts.js +1758 -0
- package/dist/providers/planning-center/openapi.generated.js +1764 -0
- package/dist/providers/planning-center/safety.js +188 -0
- package/dist/providers/planning-center/skill.generated.js +2 -27
- package/dist/providers/planning-center/tools.js +1010 -0
- package/dist/providers/sheets/api.js +133 -0
- package/dist/providers/sheets/index.d.ts +4 -16
- package/dist/providers/sheets/index.js +61 -1172
- package/dist/providers/sheets/lib/constants.d.ts +14 -0
- package/dist/providers/sheets/lib/constants.js +14 -0
- package/dist/providers/sheets/lib/hatch.js +163 -0
- package/dist/providers/sheets/lib/requests.js +105 -0
- package/dist/providers/sheets/lib/schemas.js +138 -0
- package/dist/providers/sheets/openapi.generated.js +30 -0
- package/dist/providers/sheets/safety.js +27 -0
- package/dist/providers/sheets/skill.generated.js +2 -7
- package/dist/providers/sheets/tools.js +734 -0
- package/dist/providers/slides/api.js +147 -0
- package/dist/providers/slides/index.d.ts +2 -10
- package/dist/providers/slides/index.js +48 -2555
- package/dist/providers/slides/lib/constants.d.ts +11 -0
- package/dist/providers/slides/lib/constants.js +11 -0
- package/dist/providers/slides/lib/requests.js +60 -0
- package/dist/providers/slides/lib/schemas.js +565 -0
- package/dist/providers/slides/openapi.generated.js +18 -0
- package/dist/providers/slides/skill.generated.js +2 -6
- package/dist/providers/slides/tools.js +1968 -0
- package/dist/providers/stripe/api.js +401 -0
- package/dist/providers/stripe/index.d.ts +7 -27
- package/dist/providers/stripe/index.js +75 -191
- package/dist/providers/stripe/lib/config.d.ts +4 -0
- package/dist/providers/stripe/lib/config.js +2 -0
- package/dist/providers/stripe/openapi.generated.js +4 -3
- package/dist/providers/stripe/{value-safety.js → safety.js} +5 -5
- package/dist/providers/stripe/skill.generated.js +2 -10
- package/dist/providers/stripe/tools.js +77 -0
- package/dist/providers/tithely/api.js +531 -0
- package/dist/providers/tithely/index.d.ts +19 -0
- package/dist/providers/tithely/index.js +86 -965
- package/dist/providers/tithely/skill.generated.js +2 -5
- package/dist/providers/tithely/tools.js +426 -0
- package/dist/providers/vercel/api.js +551 -0
- package/dist/providers/vercel/index.d.ts +11 -25
- package/dist/providers/vercel/index.js +67 -261
- package/dist/providers/vercel/lib/config.d.ts +2 -0
- package/dist/providers/vercel/lib/config.js +2 -0
- package/dist/providers/vercel/lib/index.js +117 -0
- package/dist/providers/vercel/lib/logs.js +100 -0
- package/dist/providers/vercel/lib/pin.js +270 -0
- package/dist/providers/vercel/openapi.generated.js +398 -376
- package/dist/providers/vercel/{value-safety.js → safety.js} +261 -42
- package/dist/providers/vercel/skill.generated.js +2 -7
- package/dist/providers/vercel/tools.js +642 -0
- package/dist/redaction-identity.js +47 -0
- package/dist/redaction-marker.js +125 -0
- package/dist/registry.d.ts +30 -28
- package/dist/registry.js +446 -413
- package/dist/routes/access-tokens.js +1 -1
- package/dist/routes/actor-labels.js +1 -1
- package/dist/routes/connect.js +1 -1
- package/dist/routes/dispatch.js +103 -0
- package/dist/routes/mcp.js +170 -79
- package/dist/routes/oauth.js +3 -1
- package/dist/routes/operator.js +2 -2
- package/dist/routes/shared.d.ts +5 -4
- package/dist/routes/shared.js +244 -177
- package/dist/routes/skills.js +10 -8
- package/dist/routes/ui-config.js +27 -3
- package/dist/routes/ui-data.js +29 -22
- package/dist/routes/ui.js +1 -1
- package/dist/runtime/call-admission.js +0 -26
- package/dist/runtime/call-entry.js +30 -0
- package/dist/runtime/call.js +556 -0
- package/dist/runtime/connector-scope.js +7 -0
- package/dist/runtime/credential-probe.js +34 -0
- package/dist/runtime/executor.js +40 -0
- package/dist/runtime/native-storage.js +63 -0
- package/dist/runtime/registry.js +27 -0
- package/dist/runtime/request-scope.js +72 -0
- package/dist/runtime/run.js +15 -7
- package/dist/runtime/shared-read.js +34 -5
- package/dist/runtime/storage.js +34 -21
- package/dist/sent-secrets.d.ts +194 -37
- package/dist/sent-secrets.js +1743 -340
- package/dist/server.js +5 -56
- package/dist/skills.js +330 -224
- package/dist/sqlite.js +15 -14
- package/dist/storage/d1-copy.js +255 -0
- package/dist/storage/d1-driver.js +15 -0
- package/dist/storage/keys.js +28 -0
- package/dist/storage/sql-driver.js +88 -0
- package/dist/storage/sql-native.js +544 -0
- package/dist/storage/sql-schema.d.ts +6 -0
- package/dist/storage/sql-schema.js +15 -0
- package/dist/storage/sql.d.ts +14 -20
- package/dist/storage/sql.js +29 -535
- package/dist/timeout.js +0 -26
- package/dist/tool-safety.js +20 -10
- package/dist/types.d.ts +11 -99
- package/dist/upward.js +3 -0
- package/dist/usage-guide.js +3 -1
- package/dist/validate.d.ts +4 -2
- package/dist/validate.js +86 -9
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +312 -292
- package/dist/workspace-subject.js +5 -1
- package/dist/write-outcome.js +454 -34
- package/documentation/README.md +27 -7
- package/documentation/architecture.md +2550 -186
- package/documentation/auth.md +821 -98
- package/documentation/code-mode.md +56 -21
- package/documentation/deploying.md +164 -20
- package/documentation/hosted-mcp-migration.md +460 -0
- package/documentation/integrating.md +144 -105
- package/documentation/meta-tools.md +411 -34
- package/documentation/operating.md +28 -4
- package/documentation/operator-tests.md +11 -2
- package/documentation/operator-ui.md +103 -33
- package/documentation/provider-auth-migration.md +38 -63
- package/documentation/provider-migration-0.29.md +10 -5
- package/documentation/provider-migration-0.35.md +975 -0
- package/documentation/providers.md +965 -0
- package/examples/worker/AGENTS.md +23 -9
- package/examples/worker/README.md +254 -135
- package/examples/worker/alchemy.run.ts +116 -0
- package/examples/worker/gitignore +10 -0
- package/examples/worker/src/connecta.config.ts +23 -21
- package/examples/worker/wrangler.jsonc +14 -5
- package/guides/connecting-agents.md +157 -0
- package/guides/how-it-works.md +156 -0
- package/guides/safety-and-trust.md +153 -0
- package/package.json +27 -42
- package/templates/node/.env.example +3 -0
- package/templates/node/README.md +22 -2
- package/templates/node/docker-compose.yml +1 -0
- package/templates/node/package.json +3 -2
- package/templates/node/src/connecta.config.ts +8 -0
- package/dist/catalog-drift.d.ts +0 -134
- package/dist/catalog-drift.js +0 -493
- package/dist/providers/_shared/request-token.js +0 -83
- package/dist/providers/_shared/rest/operation-index.d.ts +0 -133
- package/dist/providers/_shared/rest/tools.d.ts +0 -141
- package/dist/providers/_shared/rest/value-safety.d.ts +0 -201
- package/dist/providers/basecamp/index.d.ts +0 -43
- package/dist/providers/basecamp/index.js +0 -997
- package/dist/providers/basecamp/skill.generated.js +0 -9
- package/dist/providers/cloudflare/rest.d.ts +0 -37
- package/dist/providers/cloudflare/rest.js +0 -1078
- package/dist/providers/github/hosted.js +0 -218
- package/dist/providers/github/rest.js +0 -321
- package/dist/providers/infisical/index.d.ts +0 -7
- package/dist/providers/infisical/index.js +0 -713
- package/dist/providers/infisical/skill.generated.js +0 -8
- package/dist/providers/linear/index.d.ts +0 -55
- package/dist/providers/linear/index.js +0 -352
- package/dist/providers/linear/skill.generated.js +0 -8
- package/dist/providers/mixpanel/index.d.ts +0 -57
- package/dist/providers/mixpanel/index.js +0 -468
- package/dist/providers/mixpanel/operations.js +0 -20
- package/dist/providers/mixpanel/rest.js +0 -603
- package/dist/providers/mixpanel/skill.generated.js +0 -10
- package/dist/providers/mixpanel/value-safety.js +0 -16
- package/dist/providers/notion/index.d.ts +0 -65
- package/dist/providers/notion/index.js +0 -368
- package/dist/providers/notion/openapi.generated.js +0 -75
- package/dist/providers/notion/rest.d.ts +0 -24
- package/dist/providers/notion/rest.js +0 -1226
- package/dist/providers/notion/skill.generated.js +0 -11
- package/dist/providers/notion/value-safety.js +0 -56
- package/dist/providers/revenuecat/index.d.ts +0 -49
- package/dist/providers/revenuecat/index.js +0 -557
- package/dist/providers/revenuecat/skill.generated.js +0 -11
- package/dist/providers/stripe/rest.d.ts +0 -14
- package/dist/providers/stripe/rest.js +0 -402
- package/dist/providers/vercel/rest.d.ts +0 -20
- package/dist/providers/vercel/rest.js +0 -1020
- package/dist/providers/wispr-flow/index.d.ts +0 -14
- package/dist/providers/wispr-flow/index.js +0 -117
- package/dist/providers/wispr-flow/skill.generated.js +0 -8
- /package/dist/providers/github/{key.js → lib/key.js} +0 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// Provider kit (decision 0010, B.4). Unreachable from src/index.ts.
|
|
2
|
+
//
|
|
3
|
+
// One Schema is a tool's type, its argument decoder, and its catalog JSON
|
|
4
|
+
// Schema. Effect's generator is correct but spells some things differently
|
|
5
|
+
// from the catalogs agents already read, and catalog bytes may not change
|
|
6
|
+
// without a recorded decision. This renders the "connecta dialect": the same
|
|
7
|
+
// constraints, minus what JSON Schema already defaults or Effect adds for its
|
|
8
|
+
// own decoder.
|
|
9
|
+
//
|
|
10
|
+
// - `additionalProperties: true` is the default and is dropped. Inputs are
|
|
11
|
+
// closed (`onExcessProperty: "error"`), and their root always names
|
|
12
|
+
// `required`, as every hand-written input does.
|
|
13
|
+
// - Nothing the decoder enforces is dropped. `Schema.Int` refuses integers
|
|
14
|
+
// past 2^53 and says so (`minimum`/`maximum`); catalogs have always meant
|
|
15
|
+
// any whole number, so kit Schemas use `Integer` below, which decodes and
|
|
16
|
+
// renders exactly that.
|
|
17
|
+
// - String lengths count code points, as JSON Schema and api()'s validator
|
|
18
|
+
// (@cfworker/json-schema's `ucs2length`) do: an astral character is one,
|
|
19
|
+
// a combining mark is one, and a lone surrogate is one. Effect's
|
|
20
|
+
// `isMinLength`/`isMaxLength` count UTF-16 units and render an
|
|
21
|
+
// approximation, so kit Schemas use `isMinChars`/`isMaxChars` below, and an
|
|
22
|
+
// input whose check renders approximately, or not at all, is refused at
|
|
23
|
+
// construction.
|
|
24
|
+
// - An array's own keywords (`maxItems`, `description`) come before `items`.
|
|
25
|
+
// - `anyOf` over primitive branches with disjoint types, each constrained only
|
|
26
|
+
// by keywords that apply to its own type, becomes one `type` array. Bare
|
|
27
|
+
// object and array branches also collapse. JSON
|
|
28
|
+
// Schema applies `minLength` only to strings and `minimum` only to numbers,
|
|
29
|
+
// so the two spellings accept exactly the same values.
|
|
30
|
+
//
|
|
31
|
+
// Only the stable `effect` Schema module is used here.
|
|
32
|
+
import { Schema } from "effect";
|
|
33
|
+
const NUMERIC = new Set(["minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum", "multipleOf"]);
|
|
34
|
+
const OWN_KEYWORDS = {
|
|
35
|
+
string: new Set(["minLength", "maxLength", "pattern", "format"]),
|
|
36
|
+
integer: NUMERIC,
|
|
37
|
+
number: NUMERIC,
|
|
38
|
+
boolean: new Set(),
|
|
39
|
+
null: new Set(),
|
|
40
|
+
// Bare object/array unions are exact too; constrained branches keep anyOf.
|
|
41
|
+
object: new Set(),
|
|
42
|
+
array: new Set(["items", "minItems", "maxItems", "uniqueItems"]),
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* A whole number, as JSON Schema's `type: integer` and api()'s validator mean
|
|
46
|
+
* it: finite, with no safe-integer bound. Use it, not `Schema.Int`, wherever a
|
|
47
|
+
* catalog or a request encodes an integer.
|
|
48
|
+
*/
|
|
49
|
+
export const Integer = Schema.Finite.check(Schema.makeFilter((value) => Number.isInteger(value), {
|
|
50
|
+
expected: "an integer",
|
|
51
|
+
toJsonSchema: () => ({ type: "integer" }),
|
|
52
|
+
}));
|
|
53
|
+
/**
|
|
54
|
+
* A string's length as JSON Schema counts it: code points, with a lone
|
|
55
|
+
* surrogate counted as one. The same loop as api()'s validator.
|
|
56
|
+
*/
|
|
57
|
+
export function codePoints(value) {
|
|
58
|
+
let count = 0;
|
|
59
|
+
for (let index = 0; index < value.length; count++) {
|
|
60
|
+
const unit = value.charCodeAt(index++);
|
|
61
|
+
if (unit >= 0xd800 && unit <= 0xdbff && index < value.length && (value.charCodeAt(index) & 0xfc00) === 0xdc00) {
|
|
62
|
+
index++;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return count;
|
|
66
|
+
}
|
|
67
|
+
/** At least `minimum` code points, rendered `minLength` exactly. */
|
|
68
|
+
export function isMinChars(minimum) {
|
|
69
|
+
return Schema.makeFilter((value) => codePoints(value) >= minimum, {
|
|
70
|
+
expected: `a string of at least ${minimum} characters`,
|
|
71
|
+
toJsonSchema: () => ({ minLength: minimum }),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/** At most `maximum` code points, rendered `maxLength` exactly. */
|
|
75
|
+
export function isMaxChars(maximum) {
|
|
76
|
+
return Schema.makeFilter((value) => codePoints(value) <= maximum, {
|
|
77
|
+
expected: `a string of at most ${maximum} characters`,
|
|
78
|
+
toJsonSchema: () => ({ maxLength: maximum }),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
const JSON_TYPES = {
|
|
82
|
+
String: "string",
|
|
83
|
+
Number: "number",
|
|
84
|
+
Boolean: "boolean",
|
|
85
|
+
Arrays: "array",
|
|
86
|
+
Objects: "object",
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Refuse an input whose checks the catalog cannot state exactly: a check
|
|
90
|
+
* with no JSON Schema rendering, or one Effect marks approximate (a UTF-16
|
|
91
|
+
* string length, a RegExp whose flags differ). The validator api() runs on
|
|
92
|
+
* the catalog and the decoder would then disagree about some value.
|
|
93
|
+
*/
|
|
94
|
+
function exactChecks(ast, seen = new Set()) {
|
|
95
|
+
if (seen.has(ast))
|
|
96
|
+
return;
|
|
97
|
+
seen.add(ast);
|
|
98
|
+
// Transformations can change what the decoder accepts; the inline renderer
|
|
99
|
+
// has no reviewed exactness contract for them.
|
|
100
|
+
if (ast.encoding)
|
|
101
|
+
throw new Error("A kit input encoding has no exact catalog spelling.");
|
|
102
|
+
const type = JSON_TYPES[ast._tag];
|
|
103
|
+
const visit = (checks) => {
|
|
104
|
+
for (const check of checks) {
|
|
105
|
+
if (check._tag === "FilterGroup") {
|
|
106
|
+
visit(check.checks);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const render = check.annotations?.toJsonSchema;
|
|
110
|
+
const rendered = render?.({ type, schemas: [] });
|
|
111
|
+
if (rendered === undefined || Array.isArray(rendered)) {
|
|
112
|
+
throw new Error(`A kit input check (${String(check.annotations?.expected ?? "a filter")}) has no exact catalog spelling; ` +
|
|
113
|
+
"use the kit's isMinChars/isMaxChars or Integer, or a check that renders exactly.");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
if (ast.checks)
|
|
118
|
+
visit(ast.checks);
|
|
119
|
+
switch (ast._tag) {
|
|
120
|
+
case "Objects":
|
|
121
|
+
for (const property of ast.propertySignatures)
|
|
122
|
+
exactChecks(property.type, seen);
|
|
123
|
+
for (const index of ast.indexSignatures) {
|
|
124
|
+
exactChecks(index.parameter, seen);
|
|
125
|
+
exactChecks(index.type, seen);
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
case "Arrays":
|
|
129
|
+
for (const element of [...ast.elements, ...ast.rest])
|
|
130
|
+
exactChecks(element, seen);
|
|
131
|
+
return;
|
|
132
|
+
case "Union":
|
|
133
|
+
for (const member of ast.types)
|
|
134
|
+
exactChecks(member, seen);
|
|
135
|
+
return;
|
|
136
|
+
case "Suspend":
|
|
137
|
+
exactChecks(ast.thunk(), seen);
|
|
138
|
+
return;
|
|
139
|
+
case "String":
|
|
140
|
+
case "Number":
|
|
141
|
+
case "Boolean":
|
|
142
|
+
case "Null":
|
|
143
|
+
case "Unknown":
|
|
144
|
+
case "Never":
|
|
145
|
+
case "Literal":
|
|
146
|
+
return;
|
|
147
|
+
default:
|
|
148
|
+
throw new Error(`A kit input AST form (${ast._tag}) has no exact catalog spelling.`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/** Preserve an explicitly empty required list in a legacy output catalog. */
|
|
152
|
+
export function emptyRequired(schema) {
|
|
153
|
+
return schema.annotate({ "connecta/emptyRequired": true });
|
|
154
|
+
}
|
|
155
|
+
function restoreRequired(node, ast) {
|
|
156
|
+
if (ast.annotations?.["connecta/emptyRequired"] === true) {
|
|
157
|
+
if (node["type"] !== "object" || (Array.isArray(node["required"]) && node["required"].length > 0))
|
|
158
|
+
throw new Error("emptyRequired requires an object with no required properties.");
|
|
159
|
+
node["required"] = [];
|
|
160
|
+
}
|
|
161
|
+
if (ast._tag === "Objects") {
|
|
162
|
+
const properties = node["properties"];
|
|
163
|
+
for (const property of ast.propertySignatures) {
|
|
164
|
+
const child = properties?.[String(property.name)];
|
|
165
|
+
if (child && typeof child === "object")
|
|
166
|
+
restoreRequired(child, property.type);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
else if (ast._tag === "Arrays") {
|
|
170
|
+
const item = node["items"];
|
|
171
|
+
if (item && typeof item === "object" && ast.rest[0])
|
|
172
|
+
restoreRequired(item, ast.rest[0]);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/** Render a Schema as catalog JSON Schema. Throws on a schema that needs `$defs`, or an inexact input check. */
|
|
176
|
+
export function catalogSchema(schema, mode) {
|
|
177
|
+
if (mode === "input")
|
|
178
|
+
exactChecks(schema.ast);
|
|
179
|
+
const document = Schema.toJsonSchemaDocument(schema, mode === "input" ? { onExcessProperty: "error" } : {});
|
|
180
|
+
if (Object.keys(document.definitions).length > 0) {
|
|
181
|
+
throw new Error("A catalog schema is rendered inline; remove the identifier that asked for a definition.");
|
|
182
|
+
}
|
|
183
|
+
restoreRequired(document.schema, schema.ast);
|
|
184
|
+
let root = normalize(document.schema);
|
|
185
|
+
if (mode === "input" &&
|
|
186
|
+
root["type"] === "object" &&
|
|
187
|
+
root["additionalProperties"] === false &&
|
|
188
|
+
!("properties" in root)) {
|
|
189
|
+
const { type, ...rest } = root;
|
|
190
|
+
root = { type, properties: {}, ...rest };
|
|
191
|
+
}
|
|
192
|
+
if (mode === "input" && root["type"] === "object" && !("required" in root)) {
|
|
193
|
+
return Object.fromEntries(Object.entries(root).flatMap(([key, value]) => key === "properties"
|
|
194
|
+
? [
|
|
195
|
+
[key, value],
|
|
196
|
+
["required", []],
|
|
197
|
+
]
|
|
198
|
+
: [[key, value]]));
|
|
199
|
+
}
|
|
200
|
+
return root;
|
|
201
|
+
}
|
|
202
|
+
function normalize(value) {
|
|
203
|
+
if (Array.isArray(value))
|
|
204
|
+
return value.map(normalize);
|
|
205
|
+
if (!value || typeof value !== "object")
|
|
206
|
+
return value;
|
|
207
|
+
const node = value;
|
|
208
|
+
const out = {};
|
|
209
|
+
for (const [key, child] of Object.entries(node)) {
|
|
210
|
+
if (key === "additionalProperties" && child === true)
|
|
211
|
+
continue;
|
|
212
|
+
if (key === "properties") {
|
|
213
|
+
out[key] = Object.fromEntries(Object.entries(child).map(([name, schema]) => [name, normalize(schema)]));
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (key === "anyOf") {
|
|
217
|
+
const branches = child.map(normalize);
|
|
218
|
+
const merged = collapse(branches);
|
|
219
|
+
if (merged)
|
|
220
|
+
Object.assign(out, merged);
|
|
221
|
+
else
|
|
222
|
+
out[key] = branches;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
out[key] = key === "items" || key === "additionalProperties" ? normalize(child) : child;
|
|
226
|
+
}
|
|
227
|
+
// A discrete integer constraint may render in one allOf member beside its integer type.
|
|
228
|
+
// Flatten disjoint keywords so enum/default/max remain one usable parameter contract.
|
|
229
|
+
const members = out["allOf"];
|
|
230
|
+
if (out["type"] === "integer" && Array.isArray(members) && members.length === 1) {
|
|
231
|
+
const part = members[0];
|
|
232
|
+
if (Array.isArray(part["enum"]) &&
|
|
233
|
+
Object.keys(part).every((key) => ["enum", "minimum", "maximum", "description", "default"].includes(key) &&
|
|
234
|
+
(!(key in out) || out[key] === part[key]))) {
|
|
235
|
+
delete out["allOf"];
|
|
236
|
+
Object.assign(out, part);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// Hand-written catalogs spell an array's own keywords before its `items`.
|
|
240
|
+
if ("items" in out) {
|
|
241
|
+
const { items, ...rest } = out;
|
|
242
|
+
return { ...rest, items };
|
|
243
|
+
}
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
246
|
+
/** One `type` array for disjoint branches whose constraints cannot collide, else undefined. */
|
|
247
|
+
function collapse(branches) {
|
|
248
|
+
const types = [];
|
|
249
|
+
const merged = {};
|
|
250
|
+
for (const branch of branches) {
|
|
251
|
+
const type = branch["type"];
|
|
252
|
+
if (typeof type !== "string" || types.includes(type) || !OWN_KEYWORDS[type])
|
|
253
|
+
return undefined;
|
|
254
|
+
for (const [key, keyword] of Object.entries(branch)) {
|
|
255
|
+
if (key === "type")
|
|
256
|
+
continue;
|
|
257
|
+
if (!OWN_KEYWORDS[type].has(key) || key in merged)
|
|
258
|
+
return undefined;
|
|
259
|
+
merged[key] = keyword;
|
|
260
|
+
}
|
|
261
|
+
types.push(type);
|
|
262
|
+
}
|
|
263
|
+
// integer and number share keywords, so they never merge.
|
|
264
|
+
if (types.includes("integer") && types.includes("number"))
|
|
265
|
+
return undefined;
|
|
266
|
+
return { type: types, ...merged };
|
|
267
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// Node-only: owned undici dispatcher and Node stream adapters, reached via /node.
|
|
2
|
+
import { Readable } from "node:stream";
|
|
3
|
+
import { isIP } from "node:net";
|
|
4
|
+
import { createBrotliDecompress, createGunzip, createInflate } from "node:zlib";
|
|
5
|
+
import { Client, ProxyAgent } from "undici";
|
|
6
|
+
import { ConnectorCallError } from "../errors.js";
|
|
7
|
+
/** Pull-based bridge: Node 22's Readable.toWeb can enqueue after Web cancellation. */
|
|
8
|
+
function responseBody(body, signal) {
|
|
9
|
+
const chunks = body[Symbol.asyncIterator]();
|
|
10
|
+
let closed = false;
|
|
11
|
+
return new ReadableStream({
|
|
12
|
+
async pull(controller) {
|
|
13
|
+
try {
|
|
14
|
+
const chunk = await chunks.next();
|
|
15
|
+
if (closed)
|
|
16
|
+
return;
|
|
17
|
+
if (chunk.done) {
|
|
18
|
+
closed = true;
|
|
19
|
+
controller.close();
|
|
20
|
+
}
|
|
21
|
+
else
|
|
22
|
+
controller.enqueue(chunk.value);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (closed)
|
|
26
|
+
return;
|
|
27
|
+
closed = true;
|
|
28
|
+
controller.error(signal.aborted ? signal.reason : error);
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
cancel() {
|
|
32
|
+
closed = true;
|
|
33
|
+
body.destroy();
|
|
34
|
+
// A pending next() can still settle: it checks closed before touching
|
|
35
|
+
// the canceled controller, and every iterator rejection is observed.
|
|
36
|
+
void chunks.return?.().catch(() => { });
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* One Client per send, no shared/global dispatcher or interceptor. request()
|
|
42
|
+
* avoids fetch's 421 retry; HTTP/1.1 and non-idempotent dispatch disable the
|
|
43
|
+
* HTTP/2 REFUSED_STREAM and pipeline recovery paths, for every method.
|
|
44
|
+
* The response/abort owns the client until the body ends or is cancelled.
|
|
45
|
+
*/
|
|
46
|
+
export function createNodeGuardedTransport(proxy) {
|
|
47
|
+
let proxyUri;
|
|
48
|
+
let proxyToken;
|
|
49
|
+
if (proxy !== undefined) {
|
|
50
|
+
try {
|
|
51
|
+
const url = new URL(proxy);
|
|
52
|
+
if (!["http:", "https:"].includes(url.protocol) || url.pathname !== "/" || url.search || url.hash)
|
|
53
|
+
throw new Error();
|
|
54
|
+
if (url.username || url.password) {
|
|
55
|
+
if (!url.username)
|
|
56
|
+
throw new Error();
|
|
57
|
+
proxyToken = `Basic ${Buffer.from(`${decodeURIComponent(url.username)}:${decodeURIComponent(url.password)}`).toString("base64")}`;
|
|
58
|
+
}
|
|
59
|
+
url.username = "";
|
|
60
|
+
url.password = "";
|
|
61
|
+
proxyUri = url.href;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// Configuration can contain passwords: never include the input or cause.
|
|
65
|
+
throw new Error("proxy must be an absolute HTTP(S) proxy URL with no path, query or fragment.");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
delaysDispatch: true,
|
|
70
|
+
async send(url, init, beforeDispatch = () => { }) {
|
|
71
|
+
const framed = new Request(url, init);
|
|
72
|
+
if (!framed.headers.has("user-agent"))
|
|
73
|
+
framed.headers.set("user-agent", "connecta");
|
|
74
|
+
let dispatched = false;
|
|
75
|
+
const dispatch = () => {
|
|
76
|
+
framed.signal.throwIfAborted();
|
|
77
|
+
if (dispatched)
|
|
78
|
+
throw new Error("A transport send cannot open a second connection.");
|
|
79
|
+
beforeDispatch();
|
|
80
|
+
dispatched = true;
|
|
81
|
+
};
|
|
82
|
+
const clientOptions = { allowH2: false, pipelining: 0, interceptors: { Client: [] } };
|
|
83
|
+
let tunnelClient;
|
|
84
|
+
const client = proxyUri
|
|
85
|
+
? new ProxyAgent({
|
|
86
|
+
uri: proxyUri,
|
|
87
|
+
...(proxyToken ? { token: proxyToken } : {}),
|
|
88
|
+
proxyTunnel: true,
|
|
89
|
+
...clientOptions,
|
|
90
|
+
interceptors: { Agent: [], Client: [] },
|
|
91
|
+
// Neither side of CONNECT uses a Pool, interceptor or global dispatcher.
|
|
92
|
+
clientFactory: (origin, options) => {
|
|
93
|
+
const connect = options.connect;
|
|
94
|
+
return (tunnelClient = new Client(origin, {
|
|
95
|
+
...options,
|
|
96
|
+
...clientOptions,
|
|
97
|
+
connect: (parameters, callback) => {
|
|
98
|
+
const name = parameters.servername;
|
|
99
|
+
// ProxyAgent 6 explicitly sets SNI to the proxy hostname.
|
|
100
|
+
// Node 26 rejects IP SNI. Omit it for literal IPs; tls.connect
|
|
101
|
+
// still checks the certificate against its connection host.
|
|
102
|
+
connect({ ...parameters, ...(name && isIP(name.replace(/^\[|\]$/g, "")) ? { servername: "" } : {}) }, callback);
|
|
103
|
+
},
|
|
104
|
+
}));
|
|
105
|
+
},
|
|
106
|
+
factory: (origin, options) => {
|
|
107
|
+
const connect = options.connect;
|
|
108
|
+
return new Client(origin, {
|
|
109
|
+
...options,
|
|
110
|
+
...clientOptions,
|
|
111
|
+
connect: (options, callback) => {
|
|
112
|
+
let settled = false;
|
|
113
|
+
connect(options, (error, socket) => {
|
|
114
|
+
// undici 6's refused HTTP CONNECT can invoke its callback twice.
|
|
115
|
+
if (settled)
|
|
116
|
+
return;
|
|
117
|
+
settled = true;
|
|
118
|
+
if (error || !socket) {
|
|
119
|
+
// A CONNECT reset is pre-send, never a recoverable
|
|
120
|
+
// UND_ERR_SOCKET that Client may silently reconnect.
|
|
121
|
+
callback(new Error("Outbound proxy tunnel failed."), null);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
dispatch();
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
socket.destroy();
|
|
129
|
+
callback(error, null);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
callback(null, socket);
|
|
133
|
+
});
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
},
|
|
137
|
+
})
|
|
138
|
+
: new Client(url.origin, clientOptions);
|
|
139
|
+
const destroy = () => Promise.allSettled([tunnelClient?.destroy(), client.destroy()]);
|
|
140
|
+
const abort = () => {
|
|
141
|
+
void destroy();
|
|
142
|
+
};
|
|
143
|
+
framed.signal.addEventListener("abort", abort, { once: true });
|
|
144
|
+
try {
|
|
145
|
+
framed.signal.throwIfAborted();
|
|
146
|
+
if (!proxyUri)
|
|
147
|
+
dispatch();
|
|
148
|
+
const answer = await client.request({
|
|
149
|
+
origin: url.origin,
|
|
150
|
+
path: `${url.pathname}${url.search}`,
|
|
151
|
+
method: framed.method,
|
|
152
|
+
headers: Object.fromEntries(framed.headers),
|
|
153
|
+
// Keep serialized SDK/provider bodies sized: undici computes their exact
|
|
154
|
+
// UTF-8 Content-Length instead of using chunked transfer encoding.
|
|
155
|
+
body: typeof init.body === "string"
|
|
156
|
+
? init.body
|
|
157
|
+
: init.body instanceof URLSearchParams
|
|
158
|
+
? init.body.toString()
|
|
159
|
+
: framed.body
|
|
160
|
+
? Readable.fromWeb(framed.body)
|
|
161
|
+
: null,
|
|
162
|
+
signal: framed.signal,
|
|
163
|
+
idempotent: false,
|
|
164
|
+
maxRedirections: 0,
|
|
165
|
+
});
|
|
166
|
+
const headers = new Headers();
|
|
167
|
+
for (const [name, values] of Object.entries(answer.headers)) {
|
|
168
|
+
for (const value of Array.isArray(values) ? values : values === undefined ? [] : [values]) {
|
|
169
|
+
headers.append(name, value);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const raw = answer.body;
|
|
173
|
+
// Undici emits AbortError even for a deliberate destroy() with no
|
|
174
|
+
// consumer (HEAD or an unsupported encoding). Active Web/decode stream
|
|
175
|
+
// listeners still receive read failures; cleanup always has an observer.
|
|
176
|
+
raw.on("error", () => { });
|
|
177
|
+
const close = () => {
|
|
178
|
+
framed.signal.removeEventListener("abort", abort);
|
|
179
|
+
void destroy();
|
|
180
|
+
};
|
|
181
|
+
raw.once("close", close);
|
|
182
|
+
if (framed.method === "HEAD" || [204, 205, 304].includes(answer.statusCode)) {
|
|
183
|
+
raw.destroy();
|
|
184
|
+
return new Response(null, { status: answer.statusCode, headers });
|
|
185
|
+
}
|
|
186
|
+
let body = raw;
|
|
187
|
+
// Native fetch decodes content encodings before exposing response bytes.
|
|
188
|
+
// Bound the number of decoders; an unsupported chain fails closed.
|
|
189
|
+
const encodings = (headers.get("content-encoding") ?? "")
|
|
190
|
+
.split(",")
|
|
191
|
+
.map((value) => value.trim())
|
|
192
|
+
.filter(Boolean);
|
|
193
|
+
const unreadable = () => {
|
|
194
|
+
raw.destroy();
|
|
195
|
+
// Preserve the physical status (notably a write's 401) even when
|
|
196
|
+
// the content encoding cannot be read. A body failure is not a lost reply.
|
|
197
|
+
return new Response(new ReadableStream({
|
|
198
|
+
start(controller) {
|
|
199
|
+
controller.error(new Error("Unsupported response content encoding."));
|
|
200
|
+
},
|
|
201
|
+
}), { status: answer.statusCode, headers });
|
|
202
|
+
};
|
|
203
|
+
if (encodings.length > 5)
|
|
204
|
+
return unreadable();
|
|
205
|
+
for (const encoding of encodings.reverse()) {
|
|
206
|
+
if (encoding === "identity")
|
|
207
|
+
continue;
|
|
208
|
+
const decode = encoding === "gzip" || encoding === "x-gzip"
|
|
209
|
+
? createGunzip()
|
|
210
|
+
: encoding === "deflate"
|
|
211
|
+
? createInflate()
|
|
212
|
+
: encoding === "br"
|
|
213
|
+
? createBrotliDecompress()
|
|
214
|
+
: undefined;
|
|
215
|
+
if (!decode)
|
|
216
|
+
return unreadable();
|
|
217
|
+
const previous = body;
|
|
218
|
+
previous.once("error", (error) => decode.destroy(error));
|
|
219
|
+
decode.once("close", () => previous.destroy());
|
|
220
|
+
body = previous.pipe(decode);
|
|
221
|
+
}
|
|
222
|
+
return new Response(responseBody(body, framed.signal), { status: answer.statusCode, headers });
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
// ProxyAgent destroys its destination Agent before its CONNECT Client.
|
|
226
|
+
// An unfinished CONNECT holds the former's connector open: cancel both
|
|
227
|
+
// together so a reset/abort cannot deadlock cleanup.
|
|
228
|
+
framed.signal.removeEventListener("abort", abort);
|
|
229
|
+
await destroy();
|
|
230
|
+
if (framed.signal.aborted)
|
|
231
|
+
throw framed.signal.reason;
|
|
232
|
+
if (proxyUri)
|
|
233
|
+
throw new ConnectorCallError("unavailable", dispatched
|
|
234
|
+
? "The proxied request did not return a confirmed response."
|
|
235
|
+
: "The outbound proxy tunnel could not be established; the destination request was not sent.", { retryable: false });
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
export const nodeGuardedTransport = createNodeGuardedTransport();
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// Refresh-once middleware (decision 0010, B.2). Unreachable from src/index.ts.
|
|
2
|
+
//
|
|
3
|
+
// Refresh-once for a connection that authenticates with a token it can
|
|
4
|
+
// renew: a downstream OAuth grant, a service-account assertion, a machine
|
|
5
|
+
// login. A 401 earns one renewal. A read is then replayed once; a second 401
|
|
6
|
+
// is the grant's own refusal. A write is never sent again (INV-9): the
|
|
7
|
+
// renewal still runs, so the next deliberate call has a fresh token, and this
|
|
8
|
+
// call answers with the write's own 401.
|
|
9
|
+
//
|
|
10
|
+
// This file decides nothing about that write. The transport recorded its
|
|
11
|
+
// answer in the call's dispatch ledger when the resource server gave it, and
|
|
12
|
+
// api()'s boundary delegates the verdict to the core's settleWrite. The
|
|
13
|
+
// middleware records facts only, including a successful renewal. By default a failed
|
|
14
|
+
// renewal is discarded; the static bridge can opt into the core's grant diagnostic.
|
|
15
|
+
// Neither path can turn the write into a known refusal or advertise a retry; the
|
|
16
|
+
// ledger notes only a renewal that succeeded, so the agent is not sent to
|
|
17
|
+
// re-authorize a credential that already works. Even a wrapper here that
|
|
18
|
+
// did resend would be stopped by the transport's one-dispatch guard.
|
|
19
|
+
//
|
|
20
|
+
// The core's positive-read classifier is authoritative: only a positively
|
|
21
|
+
// classified GET/HEAD is replayable. Overrides force a write, and a reviewed
|
|
22
|
+
// POST or an auxiliary label cannot bypass the physical-write reservation.
|
|
23
|
+
//
|
|
24
|
+
// The grant itself (storage, coordination across isolates, the token
|
|
25
|
+
// request, the request-scope latch) stays in src/auth/.
|
|
26
|
+
import { Cause, Context, Effect, Option } from "effect";
|
|
27
|
+
import * as HttpClient from "effect/http/HttpClient";
|
|
28
|
+
import * as HttpClientResponse from "effect/http/HttpClientResponse";
|
|
29
|
+
import { readRequest, renewedAfterUnauthorized } from "../provider-call.js";
|
|
30
|
+
import { renewableGrantFor } from "../auth/renewable-grant.js";
|
|
31
|
+
import { ConnectorCallError } from "../errors.js";
|
|
32
|
+
import { responseRequest } from "./client.js";
|
|
33
|
+
import { Refusal, RequestRefused } from "./errors.js";
|
|
34
|
+
import { Dispatches, Signal, duringCall } from "./services.js";
|
|
35
|
+
/** The calling owner's renewable token, as the operations a transport needs from it. */
|
|
36
|
+
export class Grant extends Context.Service()("connecta/http/Grant") {
|
|
37
|
+
}
|
|
38
|
+
/** Bridge a production static OAuth facade without adding anything to its public interface. */
|
|
39
|
+
export function grantFromOAuth(oauth) {
|
|
40
|
+
const grant = renewableGrantFor(oauth);
|
|
41
|
+
const use = (work) => Effect.tryPromise({
|
|
42
|
+
try: work,
|
|
43
|
+
catch: (error) => new Refusal({
|
|
44
|
+
error: error instanceof ConnectorCallError
|
|
45
|
+
? error
|
|
46
|
+
: new ConnectorCallError("connector_call_failed", "The OAuth grant could not be used.", {
|
|
47
|
+
retryable: false,
|
|
48
|
+
}),
|
|
49
|
+
}),
|
|
50
|
+
});
|
|
51
|
+
const missing = () => Promise.reject(new ConnectorCallError("connector_call_failed", "This OAuth facade has no renewable grant."));
|
|
52
|
+
return {
|
|
53
|
+
token: use(grant ? () => grant.token() : missing),
|
|
54
|
+
refresh: (stale) => use(grant ? () => grant.refresh(stale) : missing),
|
|
55
|
+
rejected: use(grant ? () => grant.rejected() : missing),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** Headers for `GuardedOptions.authenticate`: the grant's bearer token. */
|
|
59
|
+
export const bearer = Grant.use((grant) => Effect.map(grant.token, (token) => ({ Authorization: `Bearer ${token}` })));
|
|
60
|
+
/** The bearer token a prepared request carried, as the grant issued it. */
|
|
61
|
+
function sentToken(request) {
|
|
62
|
+
return (request.headers["authorization"] ?? "").replace(/^Bearer /, "");
|
|
63
|
+
}
|
|
64
|
+
/** Wrap a guarded client whose `authenticate` is {@link bearer}. */
|
|
65
|
+
export function refreshOnce(guarded, options) {
|
|
66
|
+
if (options?.writeRenewal === "discard" && options.writeUnauthorized)
|
|
67
|
+
throw new Error("A write that is not renewed has no renewal facts to map.");
|
|
68
|
+
const inner = guarded.client;
|
|
69
|
+
const challengeFailure = (entry) => Effect.gen(function* () {
|
|
70
|
+
const error = new ConnectorCallError("connector_call_failed", "The OAuth challenge exceeded its response ceiling.", { retryable: false });
|
|
71
|
+
const refusal = new Refusal({
|
|
72
|
+
error: !readRequest(entry ?? {}) && options?.writeUnauthorized
|
|
73
|
+
? options.writeUnauthorized({ _tag: "Failed", failure: error })
|
|
74
|
+
: error,
|
|
75
|
+
});
|
|
76
|
+
const dispatch = entry ? (yield* Dispatches).find((item) => item.request === entry) : undefined;
|
|
77
|
+
if (dispatch)
|
|
78
|
+
dispatch.failure = refusal;
|
|
79
|
+
return yield* Effect.fail(refusal);
|
|
80
|
+
});
|
|
81
|
+
const execute = (request) => Effect.gen(function* () {
|
|
82
|
+
const response = yield* inner.execute(request).pipe(Effect.catchTag("ResponseTooLarge", (failure) => {
|
|
83
|
+
const entry = responseRequest(failure);
|
|
84
|
+
return options?.challengeMaxBytes !== undefined && entry?.status === 401
|
|
85
|
+
? challengeFailure(entry)
|
|
86
|
+
: Effect.fail(failure);
|
|
87
|
+
}));
|
|
88
|
+
if (response.status !== 401)
|
|
89
|
+
return response;
|
|
90
|
+
// Read under the ceiling now, so nothing is left open while renewing.
|
|
91
|
+
const reply = yield* guarded
|
|
92
|
+
.read(response, options?.challengeMaxBytes)
|
|
93
|
+
.pipe(Effect.catchTag("ResponseTooLarge", (failure) => options?.challengeMaxBytes !== undefined
|
|
94
|
+
? challengeFailure(responseRequest(response))
|
|
95
|
+
: Effect.fail(failure)));
|
|
96
|
+
const grant = yield* Grant;
|
|
97
|
+
const stale = sentToken(response.request);
|
|
98
|
+
const entry = responseRequest(response);
|
|
99
|
+
if (!readRequest(entry ?? {}) && options?.writeRenewal === "discard") {
|
|
100
|
+
// No renewal: the next deliberate call obtains a fresh token. The
|
|
101
|
+
// write's own 401 is answered; nothing is marked renewed (Q8).
|
|
102
|
+
if (grant.discard)
|
|
103
|
+
yield* grant.discard(stale);
|
|
104
|
+
return HttpClientResponse.fromWeb(request, new Response(reply.text ?? "", { status: reply.status, headers: reply.headers }));
|
|
105
|
+
}
|
|
106
|
+
if (!readRequest(entry ?? {})) {
|
|
107
|
+
// Whatever renewal meets is the authorization server's, not the
|
|
108
|
+
// write's. The ledger keeps only whether it succeeded, which decides
|
|
109
|
+
// whether the agent still needs the handoff, never the outcome.
|
|
110
|
+
const renewal = yield* Effect.exit(grant.refresh(stale));
|
|
111
|
+
// Recorded on this request's own ledger entry, and only for a token
|
|
112
|
+
// other than the one it sent; the core decides from it (Q8).
|
|
113
|
+
if (renewal._tag === "Success" && !(yield* Signal)?.aborted)
|
|
114
|
+
renewedAfterUnauthorized(entry, stale, renewal.value);
|
|
115
|
+
if (options?.writeUnauthorized) {
|
|
116
|
+
const cause = renewal._tag === "Failure" ? Cause.findErrorOption(renewal.cause) : Option.none();
|
|
117
|
+
const failure = Option.isSome(cause) ? cause.value : undefined;
|
|
118
|
+
const facts = renewal._tag === "Success"
|
|
119
|
+
? { _tag: renewal.value === stale ? "Unchanged" : "Renewed" }
|
|
120
|
+
: {
|
|
121
|
+
_tag: "Failed",
|
|
122
|
+
failure: failure?._tag === "Refusal"
|
|
123
|
+
? failure.error
|
|
124
|
+
: (failure ?? new Error("OAuth renewal did not complete.")),
|
|
125
|
+
};
|
|
126
|
+
// This is still the resource request's own 401. The common failure
|
|
127
|
+
// mapper attaches its provenance; settleWrite decides the outcome.
|
|
128
|
+
return yield* Effect.fail(new Refusal({ error: options.writeUnauthorized(facts) }));
|
|
129
|
+
}
|
|
130
|
+
return HttpClientResponse.fromWeb(request, new Response(reply.text ?? "", { status: reply.status, headers: reply.headers }));
|
|
131
|
+
}
|
|
132
|
+
const fresh = yield* grant.refresh(stale);
|
|
133
|
+
if ((yield* Signal)?.aborted)
|
|
134
|
+
return yield* Effect.fail(new RequestRefused({ message: "This call has ended; no renewal work may continue." }));
|
|
135
|
+
renewedAfterUnauthorized(entry, stale, fresh);
|
|
136
|
+
// `authenticate` runs again on the replay and reads the renewed token.
|
|
137
|
+
const replay = yield* inner
|
|
138
|
+
.execute(request)
|
|
139
|
+
.pipe(Effect.catchTag("ResponseTooLarge", (failure) => options?.challengeMaxBytes !== undefined && responseRequest(failure)?.status === 401
|
|
140
|
+
? grant.rejected
|
|
141
|
+
: Effect.fail(failure)));
|
|
142
|
+
if (replay.status !== 401)
|
|
143
|
+
return replay;
|
|
144
|
+
if (options?.replayRejection === "answer") {
|
|
145
|
+
// The connection reads and redacts this answer like any other.
|
|
146
|
+
if (grant.discard)
|
|
147
|
+
yield* grant.discard(sentToken(replay.request));
|
|
148
|
+
return replay;
|
|
149
|
+
}
|
|
150
|
+
yield* Effect.ignore(guarded.read(replay));
|
|
151
|
+
return yield* grant.rejected;
|
|
152
|
+
}).pipe(duringCall, Effect.catchTag("RequestRefused", (failure) => Signal.use((signal) => Effect.fail(signal?.aborted
|
|
153
|
+
? new Refusal({
|
|
154
|
+
error: new ConnectorCallError("connector_call_failed", "This call has ended; no renewal work may continue."),
|
|
155
|
+
})
|
|
156
|
+
: failure))));
|
|
157
|
+
return {
|
|
158
|
+
provider: guarded.provider,
|
|
159
|
+
client: HttpClient.makeWith((request) => Effect.flatMap(request, execute), (request) => Effect.succeed(request)),
|
|
160
|
+
read: guarded.read,
|
|
161
|
+
};
|
|
162
|
+
}
|