@zackbart/connecta 0.34.1 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +363 -0
- package/README.md +62 -106
- package/SECURITY.md +3 -2
- package/bin/connecta.mjs +41 -16
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.d.ts +2 -1
- package/dist/access-tokens.js +66 -30
- package/dist/activity.d.ts +1 -1
- package/dist/activity.js +2 -0
- package/dist/agent-config.d.ts +23 -0
- package/dist/agent-config.js +59 -0
- package/dist/agent-description.js +16 -0
- package/dist/agent-identity.js +4 -4
- package/dist/agent-observation.js +39 -0
- package/dist/auth/clerk-sdk/client.d.ts +2 -0
- package/dist/auth/clerk-sdk/generated.js +554 -563
- package/dist/auth/clerk-transport.js +70 -3
- package/dist/auth/clerk.d.ts +13 -0
- package/dist/auth/clerk.js +653 -246
- package/dist/auth/cloudflare-access.js +46 -42
- package/dist/auth/downstream-client-metadata.js +2 -2
- package/dist/auth/downstream-oauth.js +2051 -1160
- package/dist/auth/downstream-revocation.js +88 -88
- package/dist/auth/native-call.js +20 -0
- package/dist/auth/native.js +184 -0
- package/dist/auth/oauth-start-reset.js +11 -5
- package/dist/auth/oauth-token-response.js +121 -102
- package/dist/auth/oauth-v2-migration.js +97 -78
- package/dist/auth/renewable-grant.js +10 -0
- package/dist/auth/static-oauth.js +663 -271
- package/dist/auth/vercel-agent.js +266 -0
- package/dist/auth-admission.js +204 -0
- package/dist/auth-elicitation.js +350 -241
- package/dist/authorization-handoff.js +164 -90
- package/dist/call-admission-context.js +3 -10
- package/dist/call-admission.js +2 -0
- package/dist/call-classification.js +9 -0
- package/dist/call-recovery.js +32 -13
- package/dist/canonical-json.js +8 -0
- package/dist/catalog-cache.js +369 -213
- package/dist/catalog-service.js +659 -530
- package/dist/catalog.js +5 -1
- package/dist/concurrency.js +0 -25
- package/dist/config-defaults.d.ts +1 -0
- package/dist/config-defaults.js +1 -0
- package/dist/config-schema.d.ts +3 -1
- package/dist/config-schema.js +13 -3
- package/dist/config.d.ts +40 -6
- package/dist/config.js +40 -26
- package/dist/connector-caller.d.ts +4 -0
- package/dist/connector-caller.js +17 -4
- package/dist/connectors/api-connector.d.ts +12 -7
- package/dist/connectors/api-connector.js +142 -56
- package/dist/connectors/guarded-fetch.d.ts +51 -17
- package/dist/connectors/guarded-fetch.js +215 -121
- package/dist/connectors/negotiation-cache.js +20 -10
- package/dist/connectors/remote-mcp.d.ts +10 -30
- package/dist/connectors/remote-mcp.js +955 -563
- package/dist/d1.js +10 -243
- package/dist/derived-context.js +5 -0
- package/dist/describe-config.d.ts +3 -1
- package/dist/describe-config.js +2 -0
- package/dist/downstream-input.js +347 -260
- package/dist/errors.d.ts +87 -6
- package/dist/errors.js +114 -4
- package/dist/execute.js +87 -50
- package/dist/executor-admission.js +3 -0
- package/dist/executors/quickjs.js +163 -112
- package/dist/guarded-transport.js +50 -0
- package/dist/guest-runtime.js +1 -1
- package/dist/http/client.js +439 -0
- package/dist/http/errors.js +558 -0
- package/dist/http/fields.js +49 -0
- package/dist/http/hatch-paging.js +318 -0
- package/dist/http/hatch.js +1279 -0
- package/dist/http/json-schema.js +267 -0
- package/dist/http/node-transport.js +241 -0
- package/dist/http/oauth.js +162 -0
- package/dist/http/paging.js +228 -0
- package/dist/http/provider.js +221 -0
- package/dist/http/safety.js +82 -0
- package/dist/http/services.js +79 -0
- package/dist/http/tools.js +383 -0
- package/dist/http/vendor-index.js +138 -0
- package/dist/http/vendor.js +448 -0
- package/dist/http/write-request.js +66 -0
- package/dist/http-policy.d.ts +11 -0
- package/dist/http-policy.js +45 -0
- package/dist/inbound-credential.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -0
- package/dist/invocation-auth.js +30 -19
- package/dist/invocation.js +218 -85
- package/dist/mcp-result.js +0 -4
- package/dist/membership.d.ts +4 -0
- package/dist/meta-output.js +1 -1
- package/dist/meta-tools.js +531 -406
- package/dist/native-recovery.js +52 -0
- package/dist/native-tools.js +382 -0
- package/dist/node.d.ts +4 -3
- package/dist/node.js +45 -8
- package/dist/oauth-handoff.js +177 -104
- package/dist/oauth-partition.js +6 -20
- package/dist/oauth-sealing.js +6 -7
- package/dist/operation-vocabulary.js +101 -0
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +17 -4
- package/dist/operator-ui/agents.d.ts +5 -4
- package/dist/operator-ui/agents.js +17 -7
- package/dist/operator-ui/config-summary.js +5 -4
- package/dist/operator-ui/connection-map.js +7 -5
- package/dist/operator-ui/contract.d.ts +7 -3
- package/dist/operator-ui/endpoints.js +4 -13
- package/dist/operator-ui/fix-prompts.d.ts +1 -1
- package/dist/operator-ui/fix-prompts.js +0 -8
- package/dist/operator-ui/generated.js +3 -3
- package/dist/operator-ui/model.d.ts +2 -3
- package/dist/operator-ui/overview.js +5 -2
- package/dist/operator-ui/setup-commands.js +0 -4
- package/dist/operator-ui/view.d.ts +2 -25
- package/dist/operator-ui/view.js +2 -76
- package/dist/program-writes.js +47 -26
- package/dist/provider-call-facts.d.ts +115 -0
- package/dist/provider-call-facts.js +182 -0
- package/dist/provider-call.d.ts +112 -0
- package/dist/provider-call.js +271 -0
- package/dist/provider.d.ts +65 -31
- package/dist/provider.js +116 -39
- package/dist/providers/_shared/google/hatch.js +91 -0
- package/dist/providers/_shared/google/kit.js +537 -0
- package/dist/providers/_shared/google/result-size.js +15 -2
- package/dist/providers/_shared/google/service-account.d.ts +2 -1
- package/dist/providers/_shared/google/service-account.js +8 -4
- package/dist/providers/_shared/google/workspace.d.ts +6 -210
- package/dist/providers/_shared/google/workspace.js +6 -425
- package/dist/providers/_shared/rest/compact-index.js +86 -0
- package/dist/providers/_shared/rest/dispatch.js +3 -23
- package/dist/providers/_shared/rest/kit.js +546 -0
- package/dist/providers/_shared/rest/operation-index.js +230 -14
- package/dist/providers/_shared/rest/response-media.js +11 -0
- package/dist/providers/_shared/rest/tools.js +101 -78
- package/dist/providers/_shared/rest/value-safety.js +353 -195
- package/dist/providers/_shared/sensitive-fields.js +27 -0
- package/dist/providers/breeze/api.js +584 -0
- package/dist/providers/breeze/index.d.ts +1 -3
- package/dist/providers/breeze/index.js +67 -1420
- package/dist/providers/breeze/openapi.generated.js +60 -0
- package/dist/providers/breeze/skill.generated.js +2 -6
- package/dist/providers/breeze/tools.js +696 -0
- package/dist/providers/ccb/api.js +496 -0
- package/dist/providers/ccb/index.d.ts +15 -37
- package/dist/providers/ccb/index.js +77 -1702
- package/dist/providers/ccb/lib/constants.d.ts +34 -0
- package/dist/providers/ccb/lib/constants.js +74 -0
- package/dist/providers/ccb/lib/guide.js +9 -0
- package/dist/providers/ccb/lib/verdicts.js +387 -0
- package/dist/providers/ccb/openapi.generated.js +397 -0
- package/dist/providers/ccb/safety.js +295 -0
- package/dist/providers/ccb/skill.generated.js +2 -6
- package/dist/providers/ccb/tools.js +853 -0
- package/dist/providers/cloudflare/api.js +1066 -0
- package/dist/providers/cloudflare/index.d.ts +5 -22
- package/dist/providers/cloudflare/index.js +79 -197
- package/dist/providers/cloudflare/lib/config.d.ts +16 -0
- package/dist/providers/cloudflare/lib/config.js +2 -0
- package/dist/providers/cloudflare/lib/discovery.js +29 -0
- package/dist/providers/cloudflare/{graphql.js → lib/graphql.js} +1 -1
- package/dist/providers/cloudflare/lib/hatch.js +103 -0
- package/dist/providers/cloudflare/lib/path-bounds.js +28 -0
- package/dist/providers/cloudflare/lib/response-media.js +75 -0
- package/dist/providers/cloudflare/openapi.generated.js +4 -3
- package/dist/providers/cloudflare/{value-safety.js → safety.js} +487 -0
- package/dist/providers/cloudflare/skill.generated.js +2 -10
- package/dist/providers/cloudflare/tools.js +360 -0
- package/dist/providers/docs/api.js +51 -0
- package/dist/providers/docs/index.d.ts +1 -12
- package/dist/providers/docs/index.js +53 -1165
- package/dist/providers/docs/lib/constants.d.ts +18 -0
- package/dist/providers/docs/lib/constants.js +18 -0
- package/dist/providers/docs/lib/discovery.js +26 -0
- package/dist/providers/docs/lib/requests.js +48 -0
- package/dist/providers/docs/lib/schemas.js +168 -0
- package/dist/providers/docs/openapi.generated.js +16 -0
- package/dist/providers/docs/safety.js +54 -0
- package/dist/providers/docs/skill.generated.js +2 -6
- package/dist/providers/docs/tools.js +802 -0
- package/dist/providers/drive/api.js +177 -0
- package/dist/providers/drive/index.d.ts +1 -12
- package/dist/providers/drive/index.js +38 -1448
- package/dist/providers/drive/lib/constants.d.ts +6 -0
- package/dist/providers/drive/lib/constants.js +6 -0
- package/dist/providers/drive/lib/contracts.js +532 -0
- package/dist/providers/drive/lib/hatch.js +197 -0
- package/dist/providers/drive/openapi.generated.js +77 -0
- package/dist/providers/drive/safety.js +90 -0
- package/dist/providers/drive/skill.generated.js +2 -6
- package/dist/providers/drive/tools.js +1290 -0
- package/dist/providers/forms/api.js +71 -0
- package/dist/providers/forms/index.d.ts +1 -12
- package/dist/providers/forms/index.js +49 -1145
- package/dist/providers/forms/lib/constants.d.ts +14 -0
- package/dist/providers/forms/lib/constants.js +19 -0
- package/dist/providers/forms/lib/hatch.js +72 -0
- package/dist/providers/forms/openapi.generated.js +23 -0
- package/dist/providers/forms/safety.js +34 -0
- package/dist/providers/forms/skill.generated.js +2 -8
- package/dist/providers/forms/tools.js +1024 -0
- package/dist/providers/github/api.js +150 -0
- package/dist/providers/github/index.d.ts +4 -5
- package/dist/providers/github/index.js +30 -87
- package/dist/providers/github/{auth.d.ts → lib/auth.d.ts} +5 -3
- package/dist/providers/github/{auth.js → lib/auth.js} +40 -43
- package/dist/providers/github/lib/discovery.js +132 -0
- package/dist/providers/github/lib/operations.js +58 -0
- package/dist/providers/github/{scope.d.ts → lib/scope.d.ts} +2 -0
- package/dist/providers/github/{scope.js → lib/scope.js} +25 -1
- package/dist/providers/github/openapi.generated.js +1216 -0
- package/dist/providers/github/safety.js +216 -0
- package/dist/providers/github/skill.generated.js +2 -5
- package/dist/providers/github/tools.js +506 -0
- package/dist/providers/gmail/api.js +114 -0
- package/dist/providers/gmail/index.d.ts +1 -12
- package/dist/providers/gmail/index.js +73 -1621
- package/dist/providers/gmail/lib/constants.d.ts +13 -0
- package/dist/providers/gmail/lib/constants.js +16 -0
- package/dist/providers/gmail/lib/index.js +173 -0
- package/dist/providers/gmail/lib/schemas.js +176 -0
- package/dist/providers/gmail/lib/threading.js +120 -0
- package/dist/providers/gmail/openapi.generated.js +92 -0
- package/dist/providers/gmail/safety.js +21 -0
- package/dist/providers/gmail/skill.generated.js +2 -6
- package/dist/providers/gmail/tools.js +1435 -0
- package/dist/providers/overflow/api.js +1008 -0
- package/dist/providers/overflow/index.d.ts +1 -17
- package/dist/providers/overflow/index.js +60 -1488
- package/dist/providers/overflow/skill.generated.js +2 -5
- package/dist/providers/overflow/tools.js +873 -0
- package/dist/providers/planning-center/api.js +467 -0
- package/dist/providers/planning-center/index.d.ts +4 -35
- package/dist/providers/planning-center/index.js +69 -2445
- package/dist/providers/planning-center/lib/constants.d.ts +58 -0
- package/dist/providers/planning-center/lib/constants.js +83 -0
- package/dist/providers/planning-center/lib/discovery.js +40 -0
- package/dist/providers/planning-center/lib/guide.js +39 -0
- package/dist/providers/planning-center/lib/json-api.js +83 -0
- package/dist/providers/planning-center/lib/verdicts.js +1758 -0
- package/dist/providers/planning-center/openapi.generated.js +1764 -0
- package/dist/providers/planning-center/safety.js +188 -0
- package/dist/providers/planning-center/skill.generated.js +2 -27
- package/dist/providers/planning-center/tools.js +1010 -0
- package/dist/providers/sheets/api.js +133 -0
- package/dist/providers/sheets/index.d.ts +4 -16
- package/dist/providers/sheets/index.js +61 -1172
- package/dist/providers/sheets/lib/constants.d.ts +14 -0
- package/dist/providers/sheets/lib/constants.js +14 -0
- package/dist/providers/sheets/lib/hatch.js +163 -0
- package/dist/providers/sheets/lib/requests.js +105 -0
- package/dist/providers/sheets/lib/schemas.js +138 -0
- package/dist/providers/sheets/openapi.generated.js +30 -0
- package/dist/providers/sheets/safety.js +27 -0
- package/dist/providers/sheets/skill.generated.js +2 -7
- package/dist/providers/sheets/tools.js +734 -0
- package/dist/providers/slides/api.js +147 -0
- package/dist/providers/slides/index.d.ts +2 -10
- package/dist/providers/slides/index.js +48 -2555
- package/dist/providers/slides/lib/constants.d.ts +11 -0
- package/dist/providers/slides/lib/constants.js +11 -0
- package/dist/providers/slides/lib/requests.js +60 -0
- package/dist/providers/slides/lib/schemas.js +565 -0
- package/dist/providers/slides/openapi.generated.js +18 -0
- package/dist/providers/slides/skill.generated.js +2 -6
- package/dist/providers/slides/tools.js +1968 -0
- package/dist/providers/stripe/api.js +401 -0
- package/dist/providers/stripe/index.d.ts +5 -27
- package/dist/providers/stripe/index.js +74 -210
- package/dist/providers/stripe/lib/config.d.ts +4 -0
- package/dist/providers/stripe/lib/config.js +2 -0
- package/dist/providers/stripe/openapi.generated.js +4 -3
- package/dist/providers/stripe/{value-safety.js → safety.js} +5 -5
- package/dist/providers/stripe/skill.generated.js +2 -11
- package/dist/providers/stripe/tools.js +77 -0
- package/dist/providers/tithely/api.js +531 -0
- package/dist/providers/tithely/index.d.ts +17 -0
- package/dist/providers/tithely/index.js +86 -965
- package/dist/providers/tithely/skill.generated.js +2 -5
- package/dist/providers/tithely/tools.js +426 -0
- package/dist/providers/vercel/api.js +551 -0
- package/dist/providers/vercel/index.d.ts +9 -25
- package/dist/providers/vercel/index.js +66 -260
- package/dist/providers/vercel/lib/config.d.ts +2 -0
- package/dist/providers/vercel/lib/config.js +2 -0
- package/dist/providers/vercel/lib/index.js +117 -0
- package/dist/providers/vercel/lib/logs.js +100 -0
- package/dist/providers/vercel/lib/pin.js +270 -0
- package/dist/providers/vercel/openapi.generated.js +10 -5
- package/dist/providers/vercel/{value-safety.js → safety.js} +247 -38
- package/dist/providers/vercel/skill.generated.js +2 -7
- package/dist/providers/vercel/tools.js +642 -0
- package/dist/redaction-identity.js +47 -0
- package/dist/redaction-marker.js +125 -0
- package/dist/registry.d.ts +21 -15
- package/dist/registry.js +409 -376
- package/dist/routes/access-tokens.js +1 -1
- package/dist/routes/actor-labels.js +1 -1
- package/dist/routes/connect.js +1 -1
- package/dist/routes/dispatch.js +103 -0
- package/dist/routes/mcp.js +170 -79
- package/dist/routes/oauth.js +3 -1
- package/dist/routes/operator.js +2 -2
- package/dist/routes/shared.d.ts +5 -4
- package/dist/routes/shared.js +244 -177
- package/dist/routes/skills.js +10 -8
- package/dist/routes/ui-config.js +27 -3
- package/dist/routes/ui-data.js +21 -20
- package/dist/routes/ui.js +1 -1
- package/dist/runtime/call-admission.js +0 -26
- package/dist/runtime/call-entry.js +30 -0
- package/dist/runtime/call.js +556 -0
- package/dist/runtime/connector-scope.js +7 -0
- package/dist/runtime/credential-probe.js +34 -0
- package/dist/runtime/executor.js +40 -0
- package/dist/runtime/native-storage.js +63 -0
- package/dist/runtime/registry.js +27 -0
- package/dist/runtime/request-scope.js +72 -0
- package/dist/runtime/run.js +15 -7
- package/dist/runtime/shared-read.js +34 -5
- package/dist/runtime/storage.js +34 -21
- package/dist/sent-secrets.d.ts +194 -37
- package/dist/sent-secrets.js +1743 -340
- package/dist/server.js +5 -56
- package/dist/skills.js +322 -222
- package/dist/sqlite.js +15 -14
- package/dist/storage/d1-copy.js +255 -0
- package/dist/storage/d1-driver.js +15 -0
- package/dist/storage/keys.js +28 -0
- package/dist/storage/sql-driver.js +88 -0
- package/dist/storage/sql-native.js +544 -0
- package/dist/storage/sql-schema.d.ts +6 -0
- package/dist/storage/sql-schema.js +15 -0
- package/dist/storage/sql.d.ts +14 -20
- package/dist/storage/sql.js +29 -535
- package/dist/timeout.js +0 -26
- package/dist/tool-safety.js +20 -10
- package/dist/types.d.ts +7 -109
- package/dist/upward.js +3 -0
- package/dist/usage-guide.js +3 -1
- package/dist/validate.d.ts +4 -2
- package/dist/validate.js +86 -9
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +312 -292
- package/dist/workspace-subject.js +5 -1
- package/dist/write-outcome.js +454 -34
- package/documentation/README.md +27 -7
- package/documentation/architecture.md +2547 -226
- package/documentation/auth.md +821 -98
- package/documentation/code-mode.md +49 -16
- package/documentation/deploying.md +164 -20
- package/documentation/hosted-mcp-migration.md +460 -0
- package/documentation/integrating.md +129 -144
- package/documentation/meta-tools.md +409 -32
- package/documentation/operating.md +27 -3
- package/documentation/operator-tests.md +3 -0
- package/documentation/operator-ui.md +68 -45
- package/documentation/provider-auth-migration.md +16 -63
- package/documentation/provider-migration-0.29.md +10 -5
- package/documentation/provider-migration-0.35.md +975 -0
- package/documentation/providers.md +965 -0
- package/examples/worker/AGENTS.md +23 -9
- package/examples/worker/README.md +254 -135
- package/examples/worker/alchemy.run.ts +116 -0
- package/examples/worker/gitignore +10 -0
- package/examples/worker/src/connecta.config.ts +23 -21
- package/examples/worker/wrangler.jsonc +14 -5
- package/guides/connecting-agents.md +157 -0
- package/guides/how-it-works.md +156 -0
- package/guides/safety-and-trust.md +153 -0
- package/package.json +27 -46
- package/templates/node/.env.example +3 -0
- package/templates/node/README.md +22 -2
- package/templates/node/docker-compose.yml +1 -0
- package/templates/node/package.json +3 -2
- package/templates/node/src/connecta.config.ts +8 -0
- package/dist/catalog-drift.d.ts +0 -138
- package/dist/catalog-drift.js +0 -578
- package/dist/providers/_shared/request-token.js +0 -83
- package/dist/providers/_shared/rest/operation-index.d.ts +0 -133
- package/dist/providers/_shared/rest/tools.d.ts +0 -141
- package/dist/providers/_shared/rest/value-safety.d.ts +0 -201
- package/dist/providers/basecamp/index.d.ts +0 -45
- package/dist/providers/basecamp/index.js +0 -997
- package/dist/providers/basecamp/skill.generated.js +0 -9
- package/dist/providers/cloudflare/rest.d.ts +0 -37
- package/dist/providers/cloudflare/rest.js +0 -1097
- package/dist/providers/github/hosted.js +0 -218
- package/dist/providers/github/rest.js +0 -321
- package/dist/providers/infisical/index.d.ts +0 -7
- package/dist/providers/infisical/index.js +0 -713
- package/dist/providers/infisical/skill.generated.js +0 -8
- package/dist/providers/intercom/index.d.ts +0 -24
- package/dist/providers/intercom/index.js +0 -121
- package/dist/providers/intercom/skill.generated.js +0 -8
- package/dist/providers/linear/index.d.ts +0 -55
- package/dist/providers/linear/index.js +0 -537
- package/dist/providers/linear/skill.generated.js +0 -8
- package/dist/providers/mixpanel/index.d.ts +0 -59
- package/dist/providers/mixpanel/index.js +0 -472
- package/dist/providers/mixpanel/operations.js +0 -20
- package/dist/providers/mixpanel/rest.js +0 -603
- package/dist/providers/mixpanel/skill.generated.js +0 -10
- package/dist/providers/mixpanel/value-safety.js +0 -16
- package/dist/providers/notion/index.d.ts +0 -67
- package/dist/providers/notion/index.js +0 -368
- package/dist/providers/notion/openapi.generated.js +0 -75
- package/dist/providers/notion/rest.d.ts +0 -24
- package/dist/providers/notion/rest.js +0 -1226
- package/dist/providers/notion/skill.generated.js +0 -11
- package/dist/providers/notion/value-safety.js +0 -56
- package/dist/providers/revenuecat/index.d.ts +0 -51
- package/dist/providers/revenuecat/index.js +0 -697
- package/dist/providers/revenuecat/skill.generated.js +0 -11
- package/dist/providers/stripe/rest.d.ts +0 -14
- package/dist/providers/stripe/rest.js +0 -402
- package/dist/providers/vercel/rest.d.ts +0 -20
- package/dist/providers/vercel/rest.js +0 -1020
- package/dist/providers/wispr-flow/index.d.ts +0 -14
- package/dist/providers/wispr-flow/index.js +0 -117
- package/dist/providers/wispr-flow/skill.generated.js +0 -8
- /package/dist/providers/github/{key.js → lib/key.js} +0 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
// Internal HTTP failure contract (decision 0010, B.3). Unreachable from src/index.ts.
|
|
2
|
+
//
|
|
3
|
+
// Every way a kit request can fail, as a tagged error, and the one mapping
|
|
4
|
+
// from those to the wire `ConnectorCallError`. A provider supplies meaning
|
|
5
|
+
// only where meaning differs by vendor: what 401, 403, and 404 say (required,
|
|
6
|
+
// because the same 403 is an ungranted capability at one vendor and a token
|
|
7
|
+
// scope at another), which machine-readable facts its error bodies and
|
|
8
|
+
// headers carry, rules that cut across statuses, and its wording.
|
|
9
|
+
//
|
|
10
|
+
// Classification and wording are two steps. A vendor's `facts` reads a copy
|
|
11
|
+
// of the error body that keeps only tokens, finite numbers, and booleans.
|
|
12
|
+
// Declared public fact values and parsed numeric sources are captured before sent-secret redaction;
|
|
13
|
+
// wording reads a separate extraction from the redacted body. The vendor's
|
|
14
|
+
// rules read those facts and the reviewed `headers` allowlist, nothing
|
|
15
|
+
// else, and return a `Verdict`: a code and retry advice. Only after that does
|
|
16
|
+
// the kit build the vendor's detail line, and the verdict's `words` render
|
|
17
|
+
// the message from it. A rule's input has no prose to read, and when a rule
|
|
18
|
+
// runs, this failure's prose does not exist yet.
|
|
19
|
+
//
|
|
20
|
+
// This file decides no write's outcome. The core does (src/write-outcome.ts):
|
|
21
|
+
// the transport records every request in the core ledger as it leaves, and
|
|
22
|
+
// `api()`'s boundary hands the call's failure to `providerCallFailure`, which
|
|
23
|
+
// reads that ledger over the whole logical call (INV-9). What the kit adds is
|
|
24
|
+
// provenance, recorded where it was observed: a failure that is its own
|
|
25
|
+
// request's served answer is marked as that request's (`vendorRefusal`), so a
|
|
26
|
+
// refusing status counts only for the request that received it. Advice to
|
|
27
|
+
// send a call again is a segment of its own (`resendAdvice`), never part of
|
|
28
|
+
// the message, so output composes it only where the outcome allows.
|
|
29
|
+
// Q8 policy is decided once by the core's `settleWrite` (#917), from the
|
|
30
|
+
// ledger entries this kit forwards its matched proof and renewals to. A
|
|
31
|
+
// write answered 401 stays unknown; only renewal removes its handoff, and
|
|
32
|
+
// only a reviewed authentication-before-execution declaration proves refusal.
|
|
33
|
+
import { Data } from "effect";
|
|
34
|
+
import { ConnectorCallError, isConnectorCallErrorCode, resendAdvice, unavailableCallError, } from "../errors.js";
|
|
35
|
+
import { attachFailureFacts } from "../operator-record.js";
|
|
36
|
+
import { vendorRefusal } from "../provider-call.js";
|
|
37
|
+
import { providerFailureMessage } from "../write-outcome.js";
|
|
38
|
+
/** A request the kit refused before sending: confinement, a shadowed credential header. */
|
|
39
|
+
export class RequestRefused extends Data.TaggedError("RequestRefused") {
|
|
40
|
+
}
|
|
41
|
+
/** `fetch` rejected. `failure` is read for its errno only and never surfaces. */
|
|
42
|
+
export class TransportFailure extends Data.TaggedError("TransportFailure") {
|
|
43
|
+
}
|
|
44
|
+
/** The vendor answered with a redirect; the credential is never forwarded. */
|
|
45
|
+
export class RedirectRefused extends Data.TaggedError("RedirectRefused") {
|
|
46
|
+
}
|
|
47
|
+
/** The body crossed the connector's byte ceiling, declared or streamed. */
|
|
48
|
+
export class ResponseTooLarge extends Data.TaggedError("ResponseTooLarge") {
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* A logical write was about to be handed to `fetch` a second time in one
|
|
52
|
+
* call, by any path (an Effect retry, a redirect follower, a renewal that
|
|
53
|
+
* replays, a reconnect, a retry under a new request id). Raised below all of
|
|
54
|
+
* them, before anything leaves; `first` is the permanent reservation, which may still be preparing.
|
|
55
|
+
*/
|
|
56
|
+
export class ResendRefused extends Data.TaggedError("ResendRefused") {
|
|
57
|
+
}
|
|
58
|
+
/** A non-2xx answer (or an in-band refusal mapped as one), with the body parsed when it was JSON. */
|
|
59
|
+
export class StatusFailure extends Data.TaggedError("StatusFailure") {
|
|
60
|
+
}
|
|
61
|
+
/** A successful status whose body is not JSON. Its text never surfaces. */
|
|
62
|
+
export class UnreadableBody extends Data.TaggedError("UnreadableBody") {
|
|
63
|
+
constructor(options = {}) {
|
|
64
|
+
super(options);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** The vendor's own typed verdict: an in-band refusal, a missing credential. */
|
|
68
|
+
export class Refusal extends Data.TaggedError("Refusal") {
|
|
69
|
+
}
|
|
70
|
+
// One completeness-checked vocabulary for the typed client boundary.
|
|
71
|
+
const TAGS = {
|
|
72
|
+
RequestRefused: true,
|
|
73
|
+
TransportFailure: true,
|
|
74
|
+
RedirectRefused: true,
|
|
75
|
+
ResponseTooLarge: true,
|
|
76
|
+
ResendRefused: true,
|
|
77
|
+
StatusFailure: true,
|
|
78
|
+
UnreadableBody: true,
|
|
79
|
+
Refusal: true,
|
|
80
|
+
};
|
|
81
|
+
export function isHttpFailure(error) {
|
|
82
|
+
if (typeof error !== "object" || error === null || !("_tag" in error))
|
|
83
|
+
return false;
|
|
84
|
+
return typeof error._tag === "string" && Object.hasOwn(TAGS, error._tag);
|
|
85
|
+
}
|
|
86
|
+
/** Refuse empty evidence at connection construction (INV-11); prose is never proof by itself. */
|
|
87
|
+
export function validateVendorErrors(vendor) {
|
|
88
|
+
errorMeanings(vendor);
|
|
89
|
+
captureErrorReviews(vendor);
|
|
90
|
+
const proof = vendor.authBeforeExecution;
|
|
91
|
+
if (proof && (typeof proof.evidence !== "string" || !proof.evidence.trim() || typeof proof.matches !== "function")) {
|
|
92
|
+
throw new Error("An authBeforeExecution declaration requires nonempty evidence and a narrow raw-fact predicate.");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// No 409 default: only a vendor that documents its meaning may name a conflict.
|
|
96
|
+
// The core decides refusal from the served answer's provenance, never this code.
|
|
97
|
+
const DEFAULTS = {
|
|
98
|
+
400: () => ({ code: "invalid_args" }),
|
|
99
|
+
422: () => ({ code: "invalid_args" }),
|
|
100
|
+
429: ({ retryAfterMs: wait }) => ({ code: "rate_limited", retryAfterMs: wait }),
|
|
101
|
+
};
|
|
102
|
+
const serverErrorDefault = ({ retryAfterMs: wait }) => ({ code: "unavailable", retryAfterMs: wait });
|
|
103
|
+
const otherwiseDefault = () => ({ code: "connector_call_failed", retryable: false });
|
|
104
|
+
const TOKEN = /^[A-Za-z0-9_.:[\]-]{1,128}$/;
|
|
105
|
+
/** How many entries of a list the detail line sees. Classification sees them all. */
|
|
106
|
+
const DISPLAYED = 8;
|
|
107
|
+
const MAX_HEADER = 128;
|
|
108
|
+
const MAX_DEPTH = 32;
|
|
109
|
+
const MAX_WAIT_MS = 86_400_000;
|
|
110
|
+
const errorReviews = new WeakMap();
|
|
111
|
+
/** Metadata snapshots contain no request data, promises or services. */
|
|
112
|
+
function captureErrorReviews(vendor) {
|
|
113
|
+
const previous = errorReviews.get(vendor);
|
|
114
|
+
if (previous)
|
|
115
|
+
return previous;
|
|
116
|
+
const presence = {};
|
|
117
|
+
for (const [name, review] of Object.entries(vendor.presence ?? {})) {
|
|
118
|
+
if (!TOKEN.test(name) ||
|
|
119
|
+
!Array.isArray(review.path) ||
|
|
120
|
+
review.path.length === 0 ||
|
|
121
|
+
review.path.length > MAX_DEPTH ||
|
|
122
|
+
review.path.some((part) => typeof part !== "string" || !part) ||
|
|
123
|
+
!["string", "text"].includes(review.content))
|
|
124
|
+
throw new Error("Error presence reviews need a name, a bounded property path and string or text content.");
|
|
125
|
+
presence[name] = Object.freeze({ path: Object.freeze([...review.path]), content: review.content });
|
|
126
|
+
}
|
|
127
|
+
const display = {};
|
|
128
|
+
for (const [name, review] of Object.entries(vendor.displayFacts ?? {})) {
|
|
129
|
+
if (!TOKEN.test(name) || !Number.isSafeInteger(review.maxItems) || review.maxItems < 1 || !review.review.trim())
|
|
130
|
+
throw new Error("Displayed error facts need a positive whole maxItems and a nonempty review.");
|
|
131
|
+
display[name] = review.maxItems;
|
|
132
|
+
}
|
|
133
|
+
const numbers = {};
|
|
134
|
+
for (const [name, review] of Object.entries(vendor.numericFacts ?? {})) {
|
|
135
|
+
const path = review.path;
|
|
136
|
+
const header = review.header;
|
|
137
|
+
if (!TOKEN.test(name) ||
|
|
138
|
+
(header === undefined
|
|
139
|
+
? !Array.isArray(path) ||
|
|
140
|
+
path.length === 0 ||
|
|
141
|
+
path.length > MAX_DEPTH ||
|
|
142
|
+
path.some((part) => typeof part !== "string" || !part)
|
|
143
|
+
: path !== undefined ||
|
|
144
|
+
typeof header !== "string" ||
|
|
145
|
+
!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(header) ||
|
|
146
|
+
header.length > MAX_HEADER) ||
|
|
147
|
+
(review.format !== undefined &&
|
|
148
|
+
!["integer", "seconds", "epoch-seconds", "retry-after"].includes(review.format)) ||
|
|
149
|
+
(name === "retryAfterMs" && (review.format === undefined || review.format === "integer")) ||
|
|
150
|
+
typeof review.review !== "string" ||
|
|
151
|
+
!review.review.trim())
|
|
152
|
+
throw new Error("Numeric error facts need a name, an exact path or lowercase header, a numeric format and a nonempty review.");
|
|
153
|
+
numbers[name] = Object.freeze({
|
|
154
|
+
...(header === undefined ? { path: Object.freeze([...path]) } : { header }),
|
|
155
|
+
format: review.format ?? "integer",
|
|
156
|
+
review: review.review,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
const captured = {
|
|
160
|
+
...(vendor.bodyFailures
|
|
161
|
+
? {
|
|
162
|
+
bodyFailures: (() => {
|
|
163
|
+
const review = vendor.bodyFailures;
|
|
164
|
+
if (!review.review?.trim() ||
|
|
165
|
+
!review.readFailed?.trim() ||
|
|
166
|
+
!Array.isArray(review.statusOnly) ||
|
|
167
|
+
review.statusOnly.some((status) => !Number.isInteger(status) || status < 400 || status > 599 || status === 401))
|
|
168
|
+
throw new Error("Body-failure handling needs a review, fixed wording and reviewed error statuses other than 401.");
|
|
169
|
+
return Object.freeze({ ...review, statusOnly: Object.freeze([...review.statusOnly]) });
|
|
170
|
+
})(),
|
|
171
|
+
}
|
|
172
|
+
: {}),
|
|
173
|
+
presence: Object.freeze(presence),
|
|
174
|
+
display: Object.freeze(display),
|
|
175
|
+
numbers: Object.freeze(numbers),
|
|
176
|
+
vocabulary: Object.freeze(Object.fromEntries(Object.entries(vendor.factVocabulary ?? {}).map(([name, values]) => {
|
|
177
|
+
if (!TOKEN.test(name) || !Array.isArray(values) || values.some((value) => boundedFact(value) === undefined))
|
|
178
|
+
throw new Error("Error fact vocabularies need token names and finite public token, number or boolean values.");
|
|
179
|
+
return [name, new Set(values)];
|
|
180
|
+
}))),
|
|
181
|
+
};
|
|
182
|
+
errorReviews.set(vendor, captured);
|
|
183
|
+
return captured;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* The body as a vendor's `facts` may read it: tokens, finite numbers,
|
|
187
|
+
* booleans, and null, under keys that are tokens. Prose, URLs, and anything
|
|
188
|
+
* deeper than 32 levels are gone before any vendor code runs.
|
|
189
|
+
*/
|
|
190
|
+
function tokensOf(value, depth = 0) {
|
|
191
|
+
if (typeof value === "string")
|
|
192
|
+
return TOKEN.test(value) ? value : undefined;
|
|
193
|
+
if (typeof value === "number")
|
|
194
|
+
return Number.isFinite(value) ? value : undefined;
|
|
195
|
+
if (typeof value === "boolean" || value === null)
|
|
196
|
+
return value;
|
|
197
|
+
if (typeof value !== "object" || depth >= MAX_DEPTH)
|
|
198
|
+
return undefined;
|
|
199
|
+
if (Array.isArray(value))
|
|
200
|
+
return value.map((entry) => tokensOf(entry, depth + 1));
|
|
201
|
+
return Object.fromEntries(Object.entries(value).flatMap(([key, entry]) => (TOKEN.test(key) ? [[key, tokensOf(entry, depth + 1)]] : [])));
|
|
202
|
+
}
|
|
203
|
+
function boundedFact(value) {
|
|
204
|
+
if (typeof value === "string")
|
|
205
|
+
return TOKEN.test(value) ? value : undefined;
|
|
206
|
+
if (typeof value === "number")
|
|
207
|
+
return Number.isFinite(value) ? value : undefined;
|
|
208
|
+
return typeof value === "boolean" ? value : undefined;
|
|
209
|
+
}
|
|
210
|
+
/** Keep tokens, finite numbers, and booleans, in complete lists; anything else is dropped. */
|
|
211
|
+
function bounded(facts) {
|
|
212
|
+
return Object.fromEntries(Object.entries(facts)
|
|
213
|
+
.filter(([name]) => TOKEN.test(name))
|
|
214
|
+
.map(([name, value]) => [
|
|
215
|
+
name,
|
|
216
|
+
Array.isArray(value)
|
|
217
|
+
? value.map(boundedFact).filter((fact) => fact !== undefined)
|
|
218
|
+
: boundedFact(value),
|
|
219
|
+
]));
|
|
220
|
+
}
|
|
221
|
+
const classifierFacts = new WeakMap();
|
|
222
|
+
function vocabularyFacts(facts, vendor) {
|
|
223
|
+
return Object.fromEntries(Object.entries(captureErrorReviews(vendor).vocabulary).map(([name, values]) => {
|
|
224
|
+
const value = facts[name];
|
|
225
|
+
return [
|
|
226
|
+
name,
|
|
227
|
+
Array.isArray(value)
|
|
228
|
+
? Object.freeze(value.filter((entry) => values.has(entry)))
|
|
229
|
+
: value !== undefined && values.has(value)
|
|
230
|
+
? value
|
|
231
|
+
: undefined,
|
|
232
|
+
];
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
/** Read only declared public facts before redaction; never retain the original body or an unreviewed value. */
|
|
236
|
+
export function captureClassifierFacts(body, vendor, headers = new Headers()) {
|
|
237
|
+
const review = captureErrorReviews(vendor);
|
|
238
|
+
const vocabulary = Object.keys(review.vocabulary).length > 0;
|
|
239
|
+
if (!vocabulary && !Object.keys(review.numbers).length)
|
|
240
|
+
return undefined;
|
|
241
|
+
return {
|
|
242
|
+
vendor,
|
|
243
|
+
facts: vocabulary ? vocabularyFacts(bounded(vendor.facts(tokensOf(body))), vendor) : {},
|
|
244
|
+
numbers: numericFacts(body, headers, review.numbers),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/** Reject coercions and unbounded text; only parsed integers or bounded wait windows leave this function. */
|
|
248
|
+
function numericValue(value, format) {
|
|
249
|
+
if (typeof value === "string") {
|
|
250
|
+
if (value.length > MAX_HEADER)
|
|
251
|
+
return undefined;
|
|
252
|
+
const raw = value.trim();
|
|
253
|
+
if (format === "retry-after" && /^[A-Za-z]{3}, \d{2} [A-Za-z]{3} \d{4} \d{2}:\d{2}:\d{2} GMT$/.test(raw)) {
|
|
254
|
+
const date = Date.parse(raw);
|
|
255
|
+
return Number.isFinite(date) ? Math.min(MAX_WAIT_MS, Math.max(0, date - Date.now())) : undefined;
|
|
256
|
+
}
|
|
257
|
+
if (!/^\d+$/.test(raw))
|
|
258
|
+
return undefined;
|
|
259
|
+
value = Number(raw);
|
|
260
|
+
}
|
|
261
|
+
if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value) || value < 0)
|
|
262
|
+
return undefined;
|
|
263
|
+
const integer = Math.min(Number.MAX_SAFE_INTEGER, value);
|
|
264
|
+
if (format === "integer")
|
|
265
|
+
return integer;
|
|
266
|
+
const milliseconds = integer * 1000 - (format === "epoch-seconds" ? Date.now() : 0);
|
|
267
|
+
return Math.min(MAX_WAIT_MS, Math.max(0, milliseconds));
|
|
268
|
+
}
|
|
269
|
+
function numericFacts(body, headers, reviews) {
|
|
270
|
+
return Object.freeze(Object.fromEntries(Object.entries(reviews).map(([name, review]) => {
|
|
271
|
+
let value = review.header === undefined ? body : headers.get(review.header);
|
|
272
|
+
for (const part of review.path ?? [])
|
|
273
|
+
value =
|
|
274
|
+
value !== null && typeof value === "object" && Object.hasOwn(value, part)
|
|
275
|
+
? value[part]
|
|
276
|
+
: undefined;
|
|
277
|
+
return [name, numericValue(value, review.format ?? "integer")];
|
|
278
|
+
})));
|
|
279
|
+
}
|
|
280
|
+
/** Keep classification facts off the failure's enumerable data and its redacted detail body. */
|
|
281
|
+
export function withClassifierFacts(failure, captured) {
|
|
282
|
+
if (captured)
|
|
283
|
+
classifierFacts.set(failure, captured);
|
|
284
|
+
return failure;
|
|
285
|
+
}
|
|
286
|
+
/** The detail line gets eight list entries by default; reviewed recovery fields retain their fixed bound. */
|
|
287
|
+
function displayed(facts, vendor) {
|
|
288
|
+
const { display } = captureErrorReviews(vendor);
|
|
289
|
+
return Object.fromEntries(Object.entries(facts).map(([name, value]) => [
|
|
290
|
+
name,
|
|
291
|
+
Array.isArray(value) ? value.slice(0, display[name] ?? DISPLAYED) : value,
|
|
292
|
+
]));
|
|
293
|
+
}
|
|
294
|
+
function visibleHeaders(headers, allowed) {
|
|
295
|
+
const out = Object.create(null);
|
|
296
|
+
for (const name of allowed ?? []) {
|
|
297
|
+
const value = headers.get(name);
|
|
298
|
+
if (value !== null && value.length <= MAX_HEADER && !hasControl(value))
|
|
299
|
+
out[name.toLowerCase()] = value;
|
|
300
|
+
}
|
|
301
|
+
return out;
|
|
302
|
+
}
|
|
303
|
+
function hasControl(value) {
|
|
304
|
+
for (let index = 0; index < value.length; index++) {
|
|
305
|
+
const code = value.charCodeAt(index);
|
|
306
|
+
if (code < 0x20 || code === 0x7f)
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
/** Shed a defect's text and typed identity when any request withholds vendor text. */
|
|
312
|
+
export function withheldDefect(defect, options,
|
|
313
|
+
// Custom probe programs may send outside this ledger, so always withhold their defects.
|
|
314
|
+
always = false) {
|
|
315
|
+
try {
|
|
316
|
+
if (!always && !options.dispatches.some(({ operation }) => operation && options.withholds(operation)))
|
|
317
|
+
return defect;
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
// A broken visibility hook cannot authorize exposing a defect's text.
|
|
321
|
+
}
|
|
322
|
+
return new Error("A provider hook withholding vendor text failed.");
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* The single mapping from a kit failure to what an agent reads. It decides
|
|
326
|
+
* no write: the core does, from its ledger (`providerCallFailure` at the
|
|
327
|
+
* `api()` boundary, and Q8's `settleWrite` once per logical call). A failure
|
|
328
|
+
* that is its own request's served answer carries that request as its
|
|
329
|
+
* provenance (`vendorRefusal`, which holds only for a refusing status: a 4xx
|
|
330
|
+
* other than 408). A write call records each 401 its requests met
|
|
331
|
+
* ({@link recordUnauthorized}) and words a blocked resend.
|
|
332
|
+
*/
|
|
333
|
+
export function toConnectorCallError(failure, vendor, options) {
|
|
334
|
+
if (options.classification === "write")
|
|
335
|
+
recordUnauthorized(vendor, options);
|
|
336
|
+
// A served read can still issue a physical write (POST or a method override).
|
|
337
|
+
// A blocked resend always reports that reservation's original attempt.
|
|
338
|
+
return failure._tag === "ResendRefused"
|
|
339
|
+
? resent(failure.first, vendor, options)
|
|
340
|
+
: answered(failure, verdict(failure, vendor, options), options.dispatches);
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Provenance (b), recorded where it was observed: `error` is its own
|
|
344
|
+
* request's answer, so it carries that request's core ledger entry. The core
|
|
345
|
+
* counts a refusal only for a refusing status on that entry, and only when it
|
|
346
|
+
* is the one request the whole logical call sent.
|
|
347
|
+
*/
|
|
348
|
+
function answered(failure, error, dispatches) {
|
|
349
|
+
const own = dispatches.find((dispatch) => dispatch.failure === failure);
|
|
350
|
+
return own?.request ? vendorRefusal(error, own.request) : error;
|
|
351
|
+
}
|
|
352
|
+
const RESEND = "A second attempt to send it was stopped before it left: connecta sends a write once.";
|
|
353
|
+
/**
|
|
354
|
+
* The transport stopped a second dispatch of a write. The call reports what
|
|
355
|
+
* the first attempt was answered, with that attempt's provenance, so the
|
|
356
|
+
* core keeps a sole served refusal known and anything else that left unknown.
|
|
357
|
+
* A first attempt that never left reports its own failure.
|
|
358
|
+
*/
|
|
359
|
+
function resent(first, vendor, options) {
|
|
360
|
+
const own = first.failure ?? (first.state === "answered" && !ok(first.status) ? answerOf(first) : undefined);
|
|
361
|
+
if (own === undefined) {
|
|
362
|
+
return new ConnectorCallError("connector_call_failed", !first.write || first.request?.auxiliary
|
|
363
|
+
? `The ${JSON.stringify(first.step ?? "write")} write step's slot is already taken. ` +
|
|
364
|
+
`Declare a separate writeSteps entry for each request the tool must send. ${RESEND}`
|
|
365
|
+
: `This call had already sent its write to ${vendor.provider}` +
|
|
366
|
+
`${first.state === "answered" ? `, which answered HTTP ${first.status}` : ""}. ${RESEND}`, { retryable: false });
|
|
367
|
+
}
|
|
368
|
+
const error = verdict(own, vendor, options, first);
|
|
369
|
+
const marked = first.request ? vendorRefusal(error, first.request) : error;
|
|
370
|
+
return providerFailureMessage(marked, `${marked.message} ${RESEND}`);
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Record, on the core ledger entry of each write request answered 401, the
|
|
374
|
+
* facts Q8 is decided from (`settleWrite`): that answer's own vendor verdict
|
|
375
|
+
* (`vendorRefusal`, from its recorded answer even when no body was read), and
|
|
376
|
+
* whether the vendor's reviewed `authBeforeExecution` evidence covers it
|
|
377
|
+
* (`ProviderRequest.authBeforeExecution`). It decides nothing: a write's
|
|
378
|
+
* outcome, its code and its handoff are the core's.
|
|
379
|
+
*/
|
|
380
|
+
export function recordUnauthorized(vendor, options) {
|
|
381
|
+
const proof = vendor.authBeforeExecution;
|
|
382
|
+
for (const dispatch of options.dispatches) {
|
|
383
|
+
if (!dispatch.write || dispatch.state !== "answered" || dispatch.status !== 401 || !dispatch.request)
|
|
384
|
+
continue;
|
|
385
|
+
const recorded = recordedAnswer(dispatch);
|
|
386
|
+
if (proof !== undefined &&
|
|
387
|
+
proof.evidence.trim().length > 0 &&
|
|
388
|
+
proof.matches(classificationInput(recorded, vendor, options.withholds?.(recorded.operation) === true)))
|
|
389
|
+
dispatch.request.authBeforeExecution = true;
|
|
390
|
+
vendorRefusal(verdict(recorded, vendor, options), dispatch.request);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
/** Use a parsed body only when it belongs to this recorded status; ceilings and later failures cannot supply its code. */
|
|
394
|
+
function recordedAnswer(dispatch) {
|
|
395
|
+
return dispatch.failure?._tag === "StatusFailure" && dispatch.failure.status === dispatch.status
|
|
396
|
+
? dispatch.failure
|
|
397
|
+
: answerOf(dispatch);
|
|
398
|
+
}
|
|
399
|
+
/** A recorded answer as the failure it was, from the facts the ledger kept (its body is gone). */
|
|
400
|
+
function answerOf(dispatch) {
|
|
401
|
+
return new StatusFailure({
|
|
402
|
+
status: dispatch.status ?? 0,
|
|
403
|
+
headers: dispatch.headers ?? new Headers(),
|
|
404
|
+
body: undefined,
|
|
405
|
+
operation: dispatch.operation ?? { method: "", template: "" },
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
function ok(status) {
|
|
409
|
+
return status !== undefined && status >= 200 && status <= 299;
|
|
410
|
+
}
|
|
411
|
+
function verdict(failure, vendor, options, dispatch = options.dispatches.find((entry) => entry.failure === failure)) {
|
|
412
|
+
switch (failure._tag) {
|
|
413
|
+
case "Refusal":
|
|
414
|
+
return failure.error;
|
|
415
|
+
case "RequestRefused":
|
|
416
|
+
return new ConnectorCallError("invalid_args", failure.message);
|
|
417
|
+
case "TransportFailure":
|
|
418
|
+
return unavailableCallError(failure.failure, failure.host, `Could not reach the ${vendor.provider} API.`, options.signal);
|
|
419
|
+
case "RedirectRefused":
|
|
420
|
+
return new ConnectorCallError("connector_call_failed", `${vendor.provider} answered HTTP ${failure.status} with a redirect; this connector talks to exactly one origin and never forwards its credential to another.`, { retryable: false });
|
|
421
|
+
case "ResponseTooLarge": {
|
|
422
|
+
if (dispatch?.status !== undefined &&
|
|
423
|
+
captureErrorReviews(vendor).bodyFailures?.statusOnly.includes(dispatch.status)) {
|
|
424
|
+
// The ceiling discarded the body, not the answered status. Only opted-in
|
|
425
|
+
// statuses use the vendor's status-only rules; 401/Q8 stays unchanged.
|
|
426
|
+
const mapped = statusVerdict(new StatusFailure({
|
|
427
|
+
status: dispatch.status,
|
|
428
|
+
headers: new Headers(),
|
|
429
|
+
body: undefined,
|
|
430
|
+
unreadable: true,
|
|
431
|
+
operation: dispatch.operation ?? { method: "", template: "" },
|
|
432
|
+
}), vendor, options.withholds?.(dispatch.operation ?? { method: "", template: "" }) === true, !dispatch.write);
|
|
433
|
+
return providerFailureMessage(mapped, failure.detail);
|
|
434
|
+
}
|
|
435
|
+
const uncertainWrite = options.classification === "write" &&
|
|
436
|
+
dispatch?.request?.auxiliary !== true &&
|
|
437
|
+
dispatch?.status !== undefined &&
|
|
438
|
+
Number.isInteger(dispatch.status) &&
|
|
439
|
+
ok(dispatch.status);
|
|
440
|
+
return attachFailureFacts(new ConnectorCallError("connector_call_failed", uncertainWrite
|
|
441
|
+
? `${failure.detail} Whether the write took effect is unknown; check the target before repeating it.`
|
|
442
|
+
: failure.detail, { retryable: false }), dispatch?.status === undefined ? {} : { httpStatus: dispatch.status });
|
|
443
|
+
}
|
|
444
|
+
case "ResendRefused":
|
|
445
|
+
return new ConnectorCallError("connector_call_failed", `This call already sent a write to ${vendor.provider}; a second attempt was stopped before it left.`, { retryable: false });
|
|
446
|
+
case "UnreadableBody": {
|
|
447
|
+
const unreadable = failure.readFailed
|
|
448
|
+
? (captureErrorReviews(vendor).bodyFailures?.readFailed ?? vendor.unreadable)
|
|
449
|
+
: failure.unexpectedText
|
|
450
|
+
? `${vendor.provider} returned unexpected text/HTML for a JSON response.`
|
|
451
|
+
: vendor.unreadable;
|
|
452
|
+
return new ConnectorCallError("connector_call_failed", options.classification === "write" && dispatch?.request?.auxiliary !== true
|
|
453
|
+
? (!failure.unexpectedText && vendor.unreadableWrite) ||
|
|
454
|
+
`${unreadable} Whether the write took effect is unknown; check the target before repeating it.`
|
|
455
|
+
: unreadable, { retryable: false });
|
|
456
|
+
}
|
|
457
|
+
case "StatusFailure":
|
|
458
|
+
return statusVerdict(failure, vendor, options.withholds?.(failure.operation) === true, options.dispatches.some((dispatch) => dispatch.failure === failure && !dispatch.write));
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
function statusVerdict(failure, vendor, withheld, read) {
|
|
462
|
+
const input = classificationInput(failure, vendor, withheld);
|
|
463
|
+
const { headers, presence } = input;
|
|
464
|
+
// Classification first, over facts and headers only.
|
|
465
|
+
const decided = classified(input, vendor);
|
|
466
|
+
// The transport already applied the core's positive-read method/header rule.
|
|
467
|
+
// A 421 advises an explicit retry only for that request; no resend happens here.
|
|
468
|
+
const { code, retryable, retryAfterMs: wait, current, words, advice, } = failure.status === 421 && read ? { ...decided, code: "unavailable", retryable: true } : decided;
|
|
469
|
+
// Then the wording. The body reaches `detail` only when this operation's
|
|
470
|
+
// value-safety review lets its vendor text through.
|
|
471
|
+
const detail = vendor.detail({
|
|
472
|
+
status: failure.status,
|
|
473
|
+
facts: displayed(bounded(vendor.facts(tokensOf(failure.body))), vendor),
|
|
474
|
+
presence,
|
|
475
|
+
numbers: numericFacts(failure.body, failure.headers, captureErrorReviews(vendor).numbers),
|
|
476
|
+
headers,
|
|
477
|
+
withheld,
|
|
478
|
+
...(withheld ? {} : { body: failure.body }),
|
|
479
|
+
});
|
|
480
|
+
const error = new ConnectorCallError(code, words ? words(detail) : detail, {
|
|
481
|
+
...(retryable === undefined ? {} : { retryable }),
|
|
482
|
+
...(wait === undefined ? {} : { retryAfterMs: wait }),
|
|
483
|
+
...(current ? { current } : {}),
|
|
484
|
+
});
|
|
485
|
+
attachFailureFacts(error, { httpStatus: failure.status });
|
|
486
|
+
return advice === undefined ? error : resendAdvice(error, advice);
|
|
487
|
+
}
|
|
488
|
+
/** Only membership and whitespace are examined; no text is copied into classification. */
|
|
489
|
+
function structuralPresence(body, reviews) {
|
|
490
|
+
return Object.fromEntries(Object.entries(reviews ?? {}).map(([name, review]) => {
|
|
491
|
+
let value = body;
|
|
492
|
+
for (const part of review.path) {
|
|
493
|
+
value =
|
|
494
|
+
value !== null && typeof value === "object" && Object.hasOwn(value, part)
|
|
495
|
+
? value[part]
|
|
496
|
+
: undefined;
|
|
497
|
+
}
|
|
498
|
+
const strings = typeof value === "string"
|
|
499
|
+
? [value]
|
|
500
|
+
: review.content === "text" && Array.isArray(value)
|
|
501
|
+
? value.filter((entry) => typeof entry === "string")
|
|
502
|
+
: [];
|
|
503
|
+
return [
|
|
504
|
+
name,
|
|
505
|
+
{ present: strings.some((text) => text.length > 0), nonBlank: strings.some((text) => text.trim().length > 0) },
|
|
506
|
+
];
|
|
507
|
+
}));
|
|
508
|
+
}
|
|
509
|
+
function classificationInput(failure, vendor, withheld) {
|
|
510
|
+
const redacted = bounded(vendor.facts(tokensOf(failure.body)));
|
|
511
|
+
const captured = classifierFacts.get(failure);
|
|
512
|
+
const numbers = captured?.vendor === vendor
|
|
513
|
+
? captured.numbers
|
|
514
|
+
: numericFacts(failure.body, failure.headers, captureErrorReviews(vendor).numbers);
|
|
515
|
+
return {
|
|
516
|
+
status: failure.status,
|
|
517
|
+
facts: { ...redacted, ...(captured?.vendor === vendor ? captured.facts : vocabularyFacts(redacted, vendor)) },
|
|
518
|
+
presence: structuralPresence(failure.body, captureErrorReviews(vendor).presence),
|
|
519
|
+
numbers,
|
|
520
|
+
headers: visibleHeaders(failure.headers, vendor.headers),
|
|
521
|
+
retryAfterMs: numbers["retryAfterMs"],
|
|
522
|
+
operation: failure.operation,
|
|
523
|
+
withheld,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* A frozen copy of a vendor's declared meanings, refused when inconsistent
|
|
528
|
+
* (INV-11): `not_found` exactly when the vendor tells absence from a
|
|
529
|
+
* permission gap, and each conflict source a one-line documented fact.
|
|
530
|
+
*/
|
|
531
|
+
export function errorMeanings(vendor) {
|
|
532
|
+
const declared = vendor.meanings;
|
|
533
|
+
if (declared === undefined)
|
|
534
|
+
return undefined;
|
|
535
|
+
const { unknownId, provesAbsence, conflicts } = declared;
|
|
536
|
+
if (!isConnectorCallErrorCode(unknownId) ||
|
|
537
|
+
typeof provesAbsence !== "boolean" ||
|
|
538
|
+
(unknownId === "not_found") !== provesAbsence ||
|
|
539
|
+
!Array.isArray(conflicts) ||
|
|
540
|
+
conflicts.some((fact) => typeof fact !== "string" || !fact.trim() || /[\r\n]/.test(fact))) {
|
|
541
|
+
throw new Error(`${vendor.provider} error meanings need unknownId "not_found" exactly when provesAbsence is true, and one-line conflict facts.`);
|
|
542
|
+
}
|
|
543
|
+
return Object.freeze({ unknownId, provesAbsence, conflicts: Object.freeze([...conflicts]) });
|
|
544
|
+
}
|
|
545
|
+
function classified(input, vendor) {
|
|
546
|
+
for (const rule of vendor.classify ?? []) {
|
|
547
|
+
const decided = rule(input);
|
|
548
|
+
if (decided)
|
|
549
|
+
return decided;
|
|
550
|
+
}
|
|
551
|
+
const table = vendor.statuses;
|
|
552
|
+
const rule = table[input.status] ??
|
|
553
|
+
(input.status >= 500 && input.status <= 599 ? (table.serverError ?? serverErrorDefault) : undefined) ??
|
|
554
|
+
DEFAULTS[input.status] ??
|
|
555
|
+
table.otherwise ??
|
|
556
|
+
otherwiseDefault;
|
|
557
|
+
return rule(input);
|
|
558
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Internal provider kit (decision 0011 S5). Unreachable from src/index.ts.
|
|
2
|
+
//
|
|
3
|
+
// `fields` narrows a result to the dot paths an agent names. It runs on the
|
|
4
|
+
// result a tool already built (after value safety, the privacy projection and
|
|
5
|
+
// any named projection), so it can only remove. A list narrows each item.
|
|
6
|
+
import { Schema } from "effect";
|
|
7
|
+
import { ConnectorCallError } from "../errors.js";
|
|
8
|
+
import { isMaxChars, isMinChars } from "./json-schema.js";
|
|
9
|
+
const MAX_FIELDS = 50;
|
|
10
|
+
/** The `fields` argument: at most 50 dot paths of 1 to 200 characters. */
|
|
11
|
+
export function fieldsInput(description) {
|
|
12
|
+
return Schema.Array(Schema.String.check(isMinChars(1), isMaxChars(200)))
|
|
13
|
+
.check(Schema.isMaxLength(MAX_FIELDS))
|
|
14
|
+
.annotate({ description });
|
|
15
|
+
}
|
|
16
|
+
function record(value) {
|
|
17
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The refusal for a path whose first segment is not a property the result can
|
|
21
|
+
* carry, if any. Named tools pass their output Schema's property names and
|
|
22
|
+
* the hatch its operation's declared response roots, so a bad path fails
|
|
23
|
+
* before anything is sent.
|
|
24
|
+
*/
|
|
25
|
+
export function fieldsProblem(fields, names, of) {
|
|
26
|
+
const bad = fields?.find((field) => !names.has(field.replace(/\[\](?=\.|$)/g, "").split(".")[0]));
|
|
27
|
+
if (bad === undefined)
|
|
28
|
+
return undefined;
|
|
29
|
+
const choices = names.size > 0 && names.size <= 40 ? `: one of ${[...names].join(", ")}` : "";
|
|
30
|
+
return new ConnectorCallError("invalid_args", `fields path ${bad} does not begin with a property of ${of}${choices}.`);
|
|
31
|
+
}
|
|
32
|
+
/** Keep only the named dot paths; arrays are narrowed item by item. */
|
|
33
|
+
export function selectFields(data, fields) {
|
|
34
|
+
if (!fields?.length)
|
|
35
|
+
return data;
|
|
36
|
+
const pick = (value, paths) => {
|
|
37
|
+
if (Array.isArray(value))
|
|
38
|
+
return value.map((item) => pick(item, paths));
|
|
39
|
+
if (!record(value))
|
|
40
|
+
return value;
|
|
41
|
+
const children = new Map();
|
|
42
|
+
for (const path of paths) {
|
|
43
|
+
const [key, ...tail] = path.replace(/\[\](?=\.|$)/g, "").split(".");
|
|
44
|
+
children.set(key, [...(children.get(key) ?? []), tail.join(".")]);
|
|
45
|
+
}
|
|
46
|
+
return Object.fromEntries([...children].flatMap(([key, tails]) => Object.hasOwn(value, key) ? [[key, tails.includes("") ? value[key] : pick(value[key], tails)]] : []));
|
|
47
|
+
};
|
|
48
|
+
return pick(data, fields);
|
|
49
|
+
}
|