@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
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { canonicalJson } from "../../../canonical-json.js";
|
|
2
|
+
import { parseIndexDetails } from "./compact-index.js";
|
|
1
3
|
// The pinned operation index a REST connector validates against before it
|
|
2
4
|
// sends anything. `scripts/generate-openapi.mjs` compiles a vendor's OpenAPI
|
|
3
5
|
// document into `OpenApiData` (search rows plus request-side details as one
|
|
@@ -114,16 +116,26 @@ export class OperationIndex {
|
|
|
114
116
|
#vendor;
|
|
115
117
|
#segments;
|
|
116
118
|
#slashParams;
|
|
119
|
+
#uriTemplates;
|
|
120
|
+
get pathMatching() {
|
|
121
|
+
return this.#uriTemplates ? "uri-template" : "segments";
|
|
122
|
+
}
|
|
117
123
|
#details;
|
|
124
|
+
#fields;
|
|
125
|
+
#requestSchemas;
|
|
118
126
|
#words;
|
|
119
127
|
/**
|
|
120
128
|
* `vendor` is the tool-name prefix; `title` the display name in refusals.
|
|
121
129
|
* `slashParams` names path parameters whose values may carry an encoded
|
|
122
130
|
* "/" (`%2F`), such as an object key, and only as the path's last segment.
|
|
131
|
+
* `pathMatching: "uri-template"` opts into decoded literals and parameters
|
|
132
|
+
* embedded in a segment, such as Google's `{id}:batchUpdate`. The default
|
|
133
|
+
* retains exact literal segments and whole-segment parameters.
|
|
123
134
|
*/
|
|
124
135
|
constructor(data, names) {
|
|
125
136
|
const { title } = names;
|
|
126
137
|
this.#slashParams = new Set(names.slashParams ?? []);
|
|
138
|
+
this.#uriTemplates = names.pathMatching === "uri-template";
|
|
127
139
|
this.#data = data;
|
|
128
140
|
this.#title = title;
|
|
129
141
|
this.#vendor = names.vendor;
|
|
@@ -145,6 +157,46 @@ export class OperationIndex {
|
|
|
145
157
|
this.#segments = this.ops.map((op) => op.path.slice(1).split("/"));
|
|
146
158
|
this.methods = [...METHODS].filter((method) => this.ops.some((op) => op.method === method));
|
|
147
159
|
}
|
|
160
|
+
/** Content binding for discovery continuations; labels alone never identify an index. */
|
|
161
|
+
async cursorRevision() {
|
|
162
|
+
const bytes = new TextEncoder().encode(canonicalJson([
|
|
163
|
+
this.#data,
|
|
164
|
+
this.revision,
|
|
165
|
+
[...this.#slashParams].sort(),
|
|
166
|
+
...(this.#uriTemplates ? ["uri-template"] : []),
|
|
167
|
+
]));
|
|
168
|
+
return Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
169
|
+
}
|
|
170
|
+
/** The request and response properties this operation declares; none when the index carries no fields. */
|
|
171
|
+
fields(op) {
|
|
172
|
+
if (!this.#fields) {
|
|
173
|
+
const data = this.#data.fields;
|
|
174
|
+
if (typeof data === "string") {
|
|
175
|
+
// Generated REST indexes share response paths and complete rows. Kit
|
|
176
|
+
// vendor indexes retain their directly declared body/response fields.
|
|
177
|
+
const { p, d, o } = JSON.parse(data);
|
|
178
|
+
const rows = d.map((ids) => ids.map((id) => ({ in: "response", path: p[id] })));
|
|
179
|
+
this.#fields = o.map((id) => rows[id]);
|
|
180
|
+
}
|
|
181
|
+
else
|
|
182
|
+
this.#fields = data ?? [];
|
|
183
|
+
}
|
|
184
|
+
return this.#fields[op.row] ?? [];
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* The names a result's first dot-path segment may take: the success
|
|
188
|
+
* response's own properties, or its items' properties when it is an array.
|
|
189
|
+
* Undefined when the operation declares no response contract.
|
|
190
|
+
*/
|
|
191
|
+
responseRoots(op) {
|
|
192
|
+
const paths = this.fields(op).filter((field) => field.in === "response");
|
|
193
|
+
if (paths.length === 0)
|
|
194
|
+
return undefined;
|
|
195
|
+
return new Set(paths.map((field) => field.path
|
|
196
|
+
.replace(/^\[\]\./, "")
|
|
197
|
+
.split(".")[0]
|
|
198
|
+
.replace(/\[\]$/, "")));
|
|
199
|
+
}
|
|
148
200
|
/** The operation with this exact method and path template, if any. */
|
|
149
201
|
operation(method, template) {
|
|
150
202
|
return this.ops.find((op) => op.method === method && op.path === template);
|
|
@@ -165,11 +217,29 @@ export class OperationIndex {
|
|
|
165
217
|
return;
|
|
166
218
|
let score = 0;
|
|
167
219
|
for (const [index, part] of template.entries()) {
|
|
168
|
-
if (
|
|
220
|
+
if (!this.#uriTemplates) {
|
|
221
|
+
if (part.startsWith("{"))
|
|
222
|
+
continue;
|
|
223
|
+
if (part !== segments[index])
|
|
224
|
+
return;
|
|
225
|
+
score += 1;
|
|
169
226
|
continue;
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
227
|
+
}
|
|
228
|
+
const parameter = /^(.*)\{([^{}]+)\}(.*)$/.exec(part);
|
|
229
|
+
const value = decodeURIComponent(segments[index]);
|
|
230
|
+
if (parameter) {
|
|
231
|
+
if (!value.startsWith(parameter[1]) ||
|
|
232
|
+
!value.endsWith(parameter[3]) ||
|
|
233
|
+
value.length <= parameter[1].length + parameter[3].length)
|
|
234
|
+
return;
|
|
235
|
+
if (parameter[1] || parameter[3])
|
|
236
|
+
score += 0.5;
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
if (part !== value)
|
|
240
|
+
return;
|
|
241
|
+
score += 1;
|
|
242
|
+
}
|
|
173
243
|
}
|
|
174
244
|
if (score > best.score)
|
|
175
245
|
best = { score, rows: [row] };
|
|
@@ -195,9 +265,34 @@ export class OperationIndex {
|
|
|
195
265
|
}
|
|
196
266
|
const params = {};
|
|
197
267
|
template.forEach((part, index) => {
|
|
198
|
-
if (
|
|
199
|
-
|
|
268
|
+
if (!this.#uriTemplates) {
|
|
269
|
+
if (part.startsWith("{"))
|
|
270
|
+
params[part.slice(1, -1)] = segments[index];
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const parameter = /^(.*)\{([^{}]+)\}(.*)$/.exec(part);
|
|
274
|
+
if (parameter) {
|
|
275
|
+
const raw = segments[index];
|
|
276
|
+
const prefix = parameter[1], suffix = parameter[3];
|
|
277
|
+
params[parameter[2]] =
|
|
278
|
+
raw.startsWith(prefix) && raw.endsWith(suffix)
|
|
279
|
+
? raw.slice(prefix.length, suffix ? -suffix.length : undefined)
|
|
280
|
+
: encodeURIComponent(decodeURIComponent(raw).slice(prefix.length, suffix ? -suffix.length : undefined));
|
|
281
|
+
}
|
|
200
282
|
});
|
|
283
|
+
const issues = [];
|
|
284
|
+
const repair = [];
|
|
285
|
+
for (const [name, at, , schema] of this.#row(op)?.[0] ?? []) {
|
|
286
|
+
if (at === "path" && params[name] !== undefined)
|
|
287
|
+
this.#check(schema, decodeURIComponent(params[name]), `/path/${name}`, issues, repair);
|
|
288
|
+
}
|
|
289
|
+
if (issues.length) {
|
|
290
|
+
const shown = issues
|
|
291
|
+
.slice(0, 5)
|
|
292
|
+
.map((issue) => `${issue.path} (${issue.code}: ${issue.expected})`)
|
|
293
|
+
.join("; ");
|
|
294
|
+
invalid(`Not sent: the path does not match ${op.method} ${op.path} in the pinned API index: ${shown}. Substitute real ids matching ${this.#vendorTool("details")}.${issues.some((issue) => issue.expected?.includes("safe integer")) ? " Integer ids use canonical ASCII decimal notation within the safe-integer range." : ""}`, issues.slice(0, MAX_ISSUES));
|
|
295
|
+
}
|
|
201
296
|
return { op, params };
|
|
202
297
|
}
|
|
203
298
|
/** Up to five operations whose paths most resemble a missed one. */
|
|
@@ -231,6 +326,9 @@ export class OperationIndex {
|
|
|
231
326
|
* the path, summary, operationId, or tag (plural-insensitive, prefixes of
|
|
232
327
|
* three letters count), and path matches weigh most. A query that starts
|
|
233
328
|
* with "/" also matches the templates that fit it.
|
|
329
|
+
* Standard hatches opt into partial matches, ordered by matched term count
|
|
330
|
+
* before relevance, including the person/people alias. Legacy callers keep
|
|
331
|
+
* the all-term contract.
|
|
234
332
|
*/
|
|
235
333
|
search(query, options) {
|
|
236
334
|
this.#words ??= this.ops.map((op) => ({
|
|
@@ -261,6 +359,12 @@ export class OperationIndex {
|
|
|
261
359
|
if (set.has(term))
|
|
262
360
|
return 2;
|
|
263
361
|
for (const word of set) {
|
|
362
|
+
if (options.partial && ((term === "person" && word === "people") || (term === "people" && word === "person")))
|
|
363
|
+
return 2;
|
|
364
|
+
if (options.partial &&
|
|
365
|
+
((term.endsWith("y") && word === `${term.slice(0, -1)}ies`) ||
|
|
366
|
+
(word.endsWith("y") && term === `${word.slice(0, -1)}ies`)))
|
|
367
|
+
return 2;
|
|
264
368
|
if (word === `${term}s` || term === `${word}s` || word === `${term}es` || term === `${word}es`)
|
|
265
369
|
return 2;
|
|
266
370
|
if (term.length >= 3 && word.startsWith(term))
|
|
@@ -280,12 +384,13 @@ export class OperationIndex {
|
|
|
280
384
|
matched += 1;
|
|
281
385
|
score += path * 3 + summary * 1.5 + id;
|
|
282
386
|
}
|
|
283
|
-
if (!fits.has(op.row) && (terms.length === 0 || matched < terms.length))
|
|
387
|
+
if (!fits.has(op.row) && (terms.length === 0 || matched < (options.partial ? 1 : terms.length)))
|
|
284
388
|
return [];
|
|
285
|
-
return [{ op, score }];
|
|
389
|
+
return [{ op, score, matched }];
|
|
286
390
|
});
|
|
287
391
|
return ranked
|
|
288
|
-
.sort((a, b) => b.
|
|
392
|
+
.sort((a, b) => (options.partial ? b.matched - a.matched : 0) ||
|
|
393
|
+
b.score - a.score ||
|
|
289
394
|
a.op.path.split("/").length - b.op.path.split("/").length ||
|
|
290
395
|
a.op.path.localeCompare(b.op.path))
|
|
291
396
|
.slice(0, options.limit)
|
|
@@ -302,8 +407,16 @@ export class OperationIndex {
|
|
|
302
407
|
const declared = row ? row[0] : [];
|
|
303
408
|
// A generator may omit plain-string path parameters (`pathParams:
|
|
304
409
|
// "typed"`); the template still names them, so the contract does too.
|
|
305
|
-
const implied = this.#segments[op.row].
|
|
306
|
-
|
|
410
|
+
const implied = this.#segments[op.row].flatMap((part) => {
|
|
411
|
+
const name = this.#uriTemplates
|
|
412
|
+
? /\{([^{}]+)\}/.exec(part)?.[1]
|
|
413
|
+
: part.startsWith("{")
|
|
414
|
+
? part.slice(1, -1)
|
|
415
|
+
: undefined;
|
|
416
|
+
return name !== undefined && !declared.some(([key, at]) => at === "path" && key === name)
|
|
417
|
+
? [[name, "path", 1, { t: "string" }]]
|
|
418
|
+
: [];
|
|
419
|
+
});
|
|
307
420
|
const parameters = [...implied, ...declared]
|
|
308
421
|
.filter(([name]) => only === undefined || name === only)
|
|
309
422
|
.map(([name, at, required, schema, description]) => ({
|
|
@@ -317,7 +430,22 @@ export class OperationIndex {
|
|
|
317
430
|
if (row && row[1] !== 0) {
|
|
318
431
|
const [contentType, schema] = row[1];
|
|
319
432
|
const node = this.#deref(schema);
|
|
320
|
-
|
|
433
|
+
const full = this.#requestSchema(op);
|
|
434
|
+
if (full && only === undefined) {
|
|
435
|
+
body = { contentType, ...(row[1][2] === 1 ? { required: true } : {}), schema: full };
|
|
436
|
+
}
|
|
437
|
+
else if (full && only !== undefined && full["properties"]?.[only]) {
|
|
438
|
+
body = {
|
|
439
|
+
contentType,
|
|
440
|
+
schema: {
|
|
441
|
+
type: "object",
|
|
442
|
+
properties: { [only]: full["properties"][only] },
|
|
443
|
+
...(Array.isArray(full["required"]) && full["required"].includes(only) ? { required: [only] } : {}),
|
|
444
|
+
...(full["$defs"] ? { $defs: full["$defs"] } : {}),
|
|
445
|
+
},
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
else if (only !== undefined && node.p?.[only] !== undefined) {
|
|
321
449
|
body = {
|
|
322
450
|
contentType,
|
|
323
451
|
schema: {
|
|
@@ -402,8 +530,40 @@ export class OperationIndex {
|
|
|
402
530
|
#vendorTool(kind) {
|
|
403
531
|
return `${this.#vendor}_api_${kind}`;
|
|
404
532
|
}
|
|
533
|
+
#requestSchema(op) {
|
|
534
|
+
if (!this.#data.requestSchemas)
|
|
535
|
+
return undefined;
|
|
536
|
+
this.#requestSchemas ??= JSON.parse(this.#data.requestSchemas);
|
|
537
|
+
const graph = this.#requestSchemas;
|
|
538
|
+
const root = graph.roots[op.row];
|
|
539
|
+
if (!root)
|
|
540
|
+
return undefined;
|
|
541
|
+
const definitions = {};
|
|
542
|
+
const visit = (value) => {
|
|
543
|
+
if (!value || typeof value !== "object")
|
|
544
|
+
return;
|
|
545
|
+
if (Array.isArray(value)) {
|
|
546
|
+
for (const child of value)
|
|
547
|
+
visit(child);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
const object = value;
|
|
551
|
+
if (typeof object["$ref"] === "string" && object["$ref"].startsWith("#/$defs/")) {
|
|
552
|
+
const name = object["$ref"].slice(8);
|
|
553
|
+
if (!Object.hasOwn(definitions, name)) {
|
|
554
|
+
definitions[name] = graph.definitions[name] ?? {};
|
|
555
|
+
visit(definitions[name]);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
for (const child of Object.values(object))
|
|
559
|
+
visit(child);
|
|
560
|
+
};
|
|
561
|
+
visit(root);
|
|
562
|
+
const resolved = typeof root["$ref"] === "string" ? graph.definitions[root["$ref"].slice(8)] : root;
|
|
563
|
+
return { ...resolved, ...(Object.keys(definitions).length ? { $defs: definitions } : {}) };
|
|
564
|
+
}
|
|
405
565
|
#row(op) {
|
|
406
|
-
this.#details ??=
|
|
566
|
+
this.#details ??= parseIndexDetails(this.#data);
|
|
407
567
|
const row = this.#details.o[op.row];
|
|
408
568
|
return row === 0 ? undefined : row;
|
|
409
569
|
}
|
|
@@ -427,6 +587,12 @@ export class OperationIndex {
|
|
|
427
587
|
out["format"] = resolved.f;
|
|
428
588
|
if (resolved.n === 1)
|
|
429
589
|
out["nullable"] = true;
|
|
590
|
+
if (resolved.l !== undefined)
|
|
591
|
+
out["minimum"] = resolved.l;
|
|
592
|
+
if (resolved.u !== undefined)
|
|
593
|
+
out["maximum"] = resolved.u;
|
|
594
|
+
if (resolved.deprecated === true)
|
|
595
|
+
out["deprecated"] = true;
|
|
430
596
|
if (resolved.a)
|
|
431
597
|
out["anyOf"] = resolved.a.map((branch) => this.#schema(branch));
|
|
432
598
|
if (resolved.p) {
|
|
@@ -446,6 +612,35 @@ export class OperationIndex {
|
|
|
446
612
|
if (issues.length >= MAX_ISSUES)
|
|
447
613
|
return;
|
|
448
614
|
const schema = this.#deref(node);
|
|
615
|
+
const path = at.startsWith("/path/");
|
|
616
|
+
const types = typesOf(schema);
|
|
617
|
+
if (path && !schema.a && !types.includes("string") && (types.includes("integer") || types.includes("number"))) {
|
|
618
|
+
const integer = types.includes("integer") && !types.includes("number");
|
|
619
|
+
const numeric = typeof value === "string" ? Number(value) : NaN;
|
|
620
|
+
const syntax = integer ? /^(?:0|-?[1-9][0-9]*)$/ : /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?$/;
|
|
621
|
+
if (typeof value !== "string" ||
|
|
622
|
+
!syntax.test(value) ||
|
|
623
|
+
Object.is(numeric, -0) ||
|
|
624
|
+
!Number.isFinite(numeric) ||
|
|
625
|
+
Math.abs(numeric) > Number.MAX_SAFE_INTEGER ||
|
|
626
|
+
(integer && !Number.isSafeInteger(numeric))) {
|
|
627
|
+
issues.push({
|
|
628
|
+
path: at,
|
|
629
|
+
code: "type",
|
|
630
|
+
expected: integer ? "a canonical ASCII decimal safe integer" : "a canonical ASCII decimal number",
|
|
631
|
+
});
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
value = numeric;
|
|
635
|
+
}
|
|
636
|
+
else if (path && types.length && !types.includes("string") && !schema.a) {
|
|
637
|
+
if (types.includes("boolean") && (value === "true" || value === "false"))
|
|
638
|
+
value = value === "true";
|
|
639
|
+
else {
|
|
640
|
+
issues.push({ path: at, code: "type", expected: types.join(" or ") });
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
449
644
|
if (schema.x === 1)
|
|
450
645
|
return;
|
|
451
646
|
// Conflicting composed constraints (disjoint enums or types) admit nothing,
|
|
@@ -454,6 +649,28 @@ export class OperationIndex {
|
|
|
454
649
|
issues.push({ path: at, code: "enum", expected: "no value: this field's combined constraints admit none" });
|
|
455
650
|
return;
|
|
456
651
|
}
|
|
652
|
+
// Only newly opted-in bounded schemas enforce scalar numeric values;
|
|
653
|
+
// existing indexes retain their previous validation behavior. HTTP query
|
|
654
|
+
// strings may represent numbers, but must match the declared numeric type.
|
|
655
|
+
if (schema.l !== undefined || schema.u !== undefined) {
|
|
656
|
+
if (value === null && schema.n === 1)
|
|
657
|
+
return;
|
|
658
|
+
const integer = typesOf(schema).includes("integer");
|
|
659
|
+
const syntax = integer ? /^-?\d+$/ : /^-?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
660
|
+
const numeric = typeof value === "number"
|
|
661
|
+
? value
|
|
662
|
+
: at.startsWith("/query/") && typeof value === "string" && syntax.test(value)
|
|
663
|
+
? Number(value)
|
|
664
|
+
: NaN;
|
|
665
|
+
if (!Number.isFinite(numeric) || (integer && !Number.isSafeInteger(numeric))) {
|
|
666
|
+
issues.push({ path: at, code: "type", expected: integer ? "integer" : "finite number" });
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
if (schema.l !== undefined && numeric < schema.l)
|
|
670
|
+
issues.push({ path: at, code: "minimum", expected: `at least ${schema.l}` });
|
|
671
|
+
if (schema.u !== undefined && numeric > schema.u)
|
|
672
|
+
issues.push({ path: at, code: "maximum", expected: `at most ${schema.u}` });
|
|
673
|
+
}
|
|
457
674
|
if (value === null || value === "")
|
|
458
675
|
return;
|
|
459
676
|
if (schema.a) {
|
|
@@ -472,7 +689,6 @@ export class OperationIndex {
|
|
|
472
689
|
}
|
|
473
690
|
return;
|
|
474
691
|
}
|
|
475
|
-
const types = typesOf(schema);
|
|
476
692
|
if (schema.e && (typeof value === "string" || typeof value === "number" || typeof value === "boolean")) {
|
|
477
693
|
if (!schema.e.some((option) => option === value || String(option) === String(value))) {
|
|
478
694
|
issues.push({ path: at, code: "enum", expected: oneOf(schema.e.map(String)) });
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Match a reviewed non-JSON success media type only on its pinned operation. */
|
|
2
|
+
export function responseMedia(media, status, operation, contentType = "") {
|
|
3
|
+
if (status < 200 || status > 299)
|
|
4
|
+
return undefined;
|
|
5
|
+
const type = contentType.split(";", 1)[0].trim().toLowerCase();
|
|
6
|
+
const path = operation.template.replace(/:([^/]+)/g, "{$1}");
|
|
7
|
+
if (!type ||
|
|
8
|
+
!media[`${operation.method} ${path}`]?.some((declared) => declared === type || (declared.endsWith("/*") && type.startsWith(declared.slice(0, -1)))))
|
|
9
|
+
return undefined;
|
|
10
|
+
return /^text\/|\bxml\b|\bcsv\b|\b(?:x-ndjson|jsonl|stream\+json)\b/.test(type) ? "text" : "binary";
|
|
11
|
+
}
|
|
@@ -1,6 +1,41 @@
|
|
|
1
|
+
// Connecta's generic REST connector over one vendor's pinned operation index.
|
|
2
|
+
//
|
|
3
|
+
// `restTools(vendor)` returns four tools named `<vendor>_api_search`,
|
|
4
|
+
// `_api_details`, `_api_read`, and `_api_write`. The first two read the index
|
|
5
|
+
// and send nothing. `_api_read` sends GET, plus the vendor's reviewed
|
|
6
|
+
// read-only POSTs; classification is per tool name, so that rule is enforced
|
|
7
|
+
// here in the handler, not inferred. `_api_write` sends every other method and
|
|
8
|
+
// is always destructive, with `method` and `path` top-level arguments so a
|
|
9
|
+
// host's approval prompt shows them.
|
|
10
|
+
//
|
|
11
|
+
// Every call is matched to an operation and checked against its contract
|
|
12
|
+
// before transport (unknown path → nearest operations; unknown or missing
|
|
13
|
+
// parameters → `validation.issues`), then the vendor's reviewed refusals run,
|
|
14
|
+
// then the vendor frames the request. Every vendor answers in one envelope:
|
|
15
|
+
// `{ status, data, page?: { hasMore, next?, param? } }`, with an optional
|
|
16
|
+
// `select` projection and no default projection. A write is dispatched once
|
|
17
|
+
// (INV-9): nothing here retries it or replays it against another
|
|
18
|
+
// implementation.
|
|
19
|
+
//
|
|
20
|
+
// A vendor adopts this module with configuration only: its transport, its
|
|
21
|
+
// failure mapper, and optional `scope` (defaults and pins), `encode` (body
|
|
22
|
+
// and query framing), `page` (cursor extraction), `refuse` (argument-dependent refusals),
|
|
23
|
+
// `readPosts`, and an idempotency header. Every vendor ships a reviewed
|
|
24
|
+
// value-safety table (`valueSafety`, `value-safety.ts` here): its refusals run
|
|
25
|
+
// in the generic tools before any request, and its redaction runs on every
|
|
26
|
+
// successful body in `callRest`, after `result` and before cursors and
|
|
27
|
+
// `select`. Optional hooks cover the rest: `path` (fill default ids before
|
|
28
|
+
// matching), `admit` (an awaited check in `callRest`, which every tool passes,
|
|
29
|
+
// such as an account pin), `result` (unwrap a vendor envelope), a reviewed
|
|
30
|
+
// `headers` allowlist, and `textBodies`. Named tools reuse `restCall` and
|
|
31
|
+
// `callRest` to share the same validation-free request path.
|
|
32
|
+
import { callClassification } from "../../../call-classification.js";
|
|
33
|
+
import { providerFailureMessage } from "../../../write-outcome.js";
|
|
1
34
|
import { guardedFetch, } from "../../../connectors/guarded-fetch.js";
|
|
2
35
|
import { IDEMPOTENCY_KEY_MAX_LENGTH, recordRecovery } from "../../../call-recovery.js";
|
|
3
|
-
import {
|
|
36
|
+
import { providerRequests } from "../../../provider-call.js";
|
|
37
|
+
import { ConnectorCallError, keyResendAdvice, unavailableCallError } from "../../../errors.js";
|
|
38
|
+
import { encodeForAgent, redactBeforeCut } from "../../../sent-secrets.js";
|
|
4
39
|
const READ = { readOnlyHint: true };
|
|
5
40
|
const DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true };
|
|
6
41
|
const MAX_SELECT = 50;
|
|
@@ -10,6 +45,24 @@ function isRecord(value) {
|
|
|
10
45
|
function invalid(message) {
|
|
11
46
|
throw new ConnectorCallError("invalid_args", message);
|
|
12
47
|
}
|
|
48
|
+
/** A header value as fetch sends it: leading and trailing HTTP whitespace removed. */
|
|
49
|
+
function sentValue(value) {
|
|
50
|
+
return value.replace(/^[\t\n\r ]+|[\t\n\r ]+$/g, "");
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Why fetch would not send `value` as a header value the vendor receives:
|
|
54
|
+
* blank once trimmed, or a byte fetch rejects (CR, LF, NUL, any other control
|
|
55
|
+
* character but tab, DEL, or a code point past Latin-1). Undici refuses the
|
|
56
|
+
* whole request for any of them; workerd at least for CR, LF and NUL.
|
|
57
|
+
*/
|
|
58
|
+
function unsendableKey(value) {
|
|
59
|
+
const sent = sentValue(value);
|
|
60
|
+
if (sent === "")
|
|
61
|
+
return "is blank; pass a key with at least one non-whitespace character";
|
|
62
|
+
if (!/^[\t\x20-\x7e\x80-\xff]*$/.test(sent))
|
|
63
|
+
return "contains a character an HTTP header cannot carry (a control character, or one past Latin-1); pass a printable key";
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
13
66
|
/**
|
|
14
67
|
* A guarded transport whose requests, response bodies included, end after
|
|
15
68
|
* `timeoutMs`: a stream the vendor never closes cannot hold a call open past
|
|
@@ -67,13 +120,6 @@ async function failureBody(response) {
|
|
|
67
120
|
return undefined;
|
|
68
121
|
}
|
|
69
122
|
}
|
|
70
|
-
function base64(bytes) {
|
|
71
|
-
let binary = "";
|
|
72
|
-
for (let offset = 0; offset < bytes.length; offset += 0x8000) {
|
|
73
|
-
binary += String.fromCharCode(...bytes.subarray(offset, offset + 0x8000));
|
|
74
|
-
}
|
|
75
|
-
return btoa(binary);
|
|
76
|
-
}
|
|
77
123
|
async function successBody(vendor, response, ctx) {
|
|
78
124
|
const type = (response.headers.get("content-type") ?? "").toLowerCase();
|
|
79
125
|
try {
|
|
@@ -109,7 +155,8 @@ async function successBody(vendor, response, ctx) {
|
|
|
109
155
|
return text === "" ? null : text;
|
|
110
156
|
}
|
|
111
157
|
const bytes = await response.bytes();
|
|
112
|
-
|
|
158
|
+
// Checked before encoding: no later pass reads through base64 a field does not declare.
|
|
159
|
+
return bytes.length === 0 ? null : { contentType: type, bytes: bytes.length, base64: encodeForAgent(ctx, bytes) };
|
|
113
160
|
}
|
|
114
161
|
catch (error) {
|
|
115
162
|
if (error instanceof ConnectorCallError)
|
|
@@ -127,36 +174,6 @@ async function successBody(vendor, response, ctx) {
|
|
|
127
174
|
throw unavailableCallError(error, undefined, `${vendor.title} answered, but its response could not be read.`);
|
|
128
175
|
}
|
|
129
176
|
}
|
|
130
|
-
/**
|
|
131
|
-
* Network failures that prove nothing was sent, but only when `fetch` itself
|
|
132
|
-
* rejected before any response existed: no connection was ever established,
|
|
133
|
-
* so the vendor cannot have acted. Once a response arrived, the same codes
|
|
134
|
-
* while reading its body prove nothing of the kind.
|
|
135
|
-
*/
|
|
136
|
-
const NEVER_CONNECTED = new Set([
|
|
137
|
-
"ECONNREFUSED",
|
|
138
|
-
"ENOTFOUND",
|
|
139
|
-
"EAI_AGAIN",
|
|
140
|
-
"EAI_FAIL",
|
|
141
|
-
"EHOSTUNREACH",
|
|
142
|
-
]);
|
|
143
|
-
/**
|
|
144
|
-
* A request that is not safe to repeat (a write without an idempotency key)
|
|
145
|
-
* must never be advertised as retryable after it may have reached the vendor
|
|
146
|
-
* (INV-9). Definite answers pass through: refusals and 4xx verdicts, a failure
|
|
147
|
-
* proven to precede any connection, and timeouts, which the invocation layer
|
|
148
|
-
* already reports as `write_outcome_unknown`. Exported so a named write tool
|
|
149
|
-
* with its own transport (an upload) applies the same rule.
|
|
150
|
-
*/
|
|
151
|
-
export function unknownOutcome(vendor, error, responded) {
|
|
152
|
-
if (!(error instanceof ConnectorCallError) || error.code !== "unavailable" || !error.retryable)
|
|
153
|
-
return error;
|
|
154
|
-
const code = error.details?.code;
|
|
155
|
-
if (code === "timeout" || (!responded && code !== undefined && NEVER_CONNECTED.has(code)))
|
|
156
|
-
return error;
|
|
157
|
-
return new ConnectorCallError("connector_call_failed", `${error.message} Whether the write took effect at ${vendor.title} is unknown: it carried no idempotency key, ` +
|
|
158
|
-
"so check the target before repeating it.", { retryable: false });
|
|
159
|
-
}
|
|
160
177
|
/**
|
|
161
178
|
* Send one prepared call and read it into the envelope. No validation and no
|
|
162
179
|
* refusal tables run here: the generic tools do both first, and named tools
|
|
@@ -169,14 +186,19 @@ export async function callRest(vendor, call, ctx, visibleHeaders = {}) {
|
|
|
169
186
|
invalid(send);
|
|
170
187
|
await vendor.admit?.(call, ctx);
|
|
171
188
|
const framing = vendor.encode ? vendor.encode(call) : defaultFraming(vendor, call);
|
|
172
|
-
// Safe to repeat: a read, or a write that carried the vendor's idempotency key.
|
|
173
189
|
const header = vendor.idempotencyHeader?.toLowerCase();
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
(
|
|
177
|
-
(header !== undefined && Object.keys(visibleHeaders).some((name) => name.toLowerCase() === header));
|
|
190
|
+
const idempotencyKey = header === undefined
|
|
191
|
+
? undefined
|
|
192
|
+
: Object.entries(visibleHeaders).find(([name, value]) => name.toLowerCase() === header && unsendableKey(value) === undefined)?.[1];
|
|
178
193
|
const request = {
|
|
179
194
|
method: call.method,
|
|
195
|
+
classification: call.method === "GET" ||
|
|
196
|
+
call.method === "HEAD" ||
|
|
197
|
+
(vendor.readPosts ?? []).some(([method, path]) => method === call.method && path === call.op.path)
|
|
198
|
+
? "read"
|
|
199
|
+
: "write",
|
|
200
|
+
// Only a key the vendor receives proves the write repeatable.
|
|
201
|
+
...(idempotencyKey === undefined ? {} : { idempotencyKey }),
|
|
180
202
|
path: call.path,
|
|
181
203
|
...(framing.query ? { query: framing.query } : {}),
|
|
182
204
|
...(framing.headers ? { headers: framing.headers } : {}),
|
|
@@ -187,30 +209,22 @@ export async function callRest(vendor, call, ctx, visibleHeaders = {}) {
|
|
|
187
209
|
? { body: framing.body }
|
|
188
210
|
: {}),
|
|
189
211
|
};
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const page = vendor.page?.(vendor.result ? body : data, call);
|
|
207
|
-
return { status: response.status, data, ...(page ? { page } : {}) };
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
catch (error) {
|
|
211
|
-
// The whole transport call: connect/send failures, 5xx, and a lost body alike.
|
|
212
|
-
throw repeatable ? error : unknownOutcome(vendor, error, responded);
|
|
213
|
-
}
|
|
212
|
+
const write = (callClassification(ctx) ?? request.classification) === "write";
|
|
213
|
+
return await send(request, ctx, async (response) => {
|
|
214
|
+
// Mappers cut the vendor's prose short, so it is redacted whole first.
|
|
215
|
+
if (!response.ok)
|
|
216
|
+
throw vendor.failure(response.status, response.headers, redactBeforeCut(ctx, await failureBody(response)), call, write);
|
|
217
|
+
// HEAD answers with headers alone; they are its data.
|
|
218
|
+
const body = call.method === "HEAD" ? headerData(response.headers) : await successBody(vendor, response, ctx);
|
|
219
|
+
// Order: unwrap the vendor's envelope, then redact (every success, HEAD
|
|
220
|
+
// header data and named tools included), then cursors (and, in the
|
|
221
|
+
// tools, `select`). Cursors read the envelope when the vendor unwraps
|
|
222
|
+
// one, and the redacted body otherwise.
|
|
223
|
+
const unwrapped = vendor.result && call.method !== "HEAD" ? vendor.result(body, call, response, ctx) : body;
|
|
224
|
+
const data = vendor.valueSafety.redact(unwrapped, call.method, call.op.path);
|
|
225
|
+
const page = vendor.page?.(vendor.result ? body : data, call);
|
|
226
|
+
return { status: response.status, data, ...(page ? { page } : {}) };
|
|
227
|
+
});
|
|
214
228
|
}
|
|
215
229
|
/** A HEAD response's headers as data, without cookies. */
|
|
216
230
|
function headerData(headers) {
|
|
@@ -340,6 +354,9 @@ export function restTools(vendor) {
|
|
|
340
354
|
op.method === "HEAD" ||
|
|
341
355
|
readPosts.some(([method, path]) => method === op.method && path === op.path);
|
|
342
356
|
const writeMethods = index.methods.filter((method) => method !== "GET" && method !== "HEAD");
|
|
357
|
+
// The methods an idempotency key is sent with: never DELETE (see the handler).
|
|
358
|
+
const keyed = vendor.idempotencyHeader ? writeMethods.filter((method) => method !== "DELETE") : [];
|
|
359
|
+
const keyedText = keyed.join(" or ");
|
|
343
360
|
const head = index.methods.includes("HEAD");
|
|
344
361
|
const readMethods = [...new Set(["GET", ...(head ? ["HEAD"] : []), ...readPosts.map(([method]) => method)])];
|
|
345
362
|
const routeOf = (op) => (reads(op) ? tool("read") : tool("write"));
|
|
@@ -490,7 +507,7 @@ export function restTools(vendor) {
|
|
|
490
507
|
{
|
|
491
508
|
name: tool("write"),
|
|
492
509
|
description: `Call a ${title} API write (${writeMethods.join(", ")}); method and path are stated for approval. ` +
|
|
493
|
-
`Checked against the pinned index first.${
|
|
510
|
+
`Checked against the pinned index first.${keyed.length ? ` Pass idempotencyKey to retry a ${keyedText} safely${keyed.length < writeMethods.length ? "; a DELETE sends no key" : ""}.` : ""}`,
|
|
494
511
|
annotations: DESTRUCTIVE,
|
|
495
512
|
inputSchema: closed({
|
|
496
513
|
method: { type: "string", enum: writeMethods, description: "The operation's HTTP method." },
|
|
@@ -507,7 +524,7 @@ export function restTools(vendor) {
|
|
|
507
524
|
type: "string",
|
|
508
525
|
minLength: 1,
|
|
509
526
|
maxLength: IDEMPOTENCY_KEY_MAX_LENGTH,
|
|
510
|
-
description:
|
|
527
|
+
description: `Reuse to retry this exact ${keyedText}; one is generated and returned when omitted.`,
|
|
511
528
|
},
|
|
512
529
|
}
|
|
513
530
|
: {}),
|
|
@@ -539,6 +556,11 @@ export function restTools(vendor) {
|
|
|
539
556
|
index.check(call.op, call.query, call.body);
|
|
540
557
|
const header = vendor.idempotencyHeader && call.method !== "DELETE" ? vendor.idempotencyHeader : undefined;
|
|
541
558
|
const given = typeof args["idempotencyKey"] === "string" ? args["idempotencyKey"] : undefined;
|
|
559
|
+
// A blank key reaches the vendor empty, and one fetch cannot carry
|
|
560
|
+
// never leaves: neither proves that a retry cannot repeat the write.
|
|
561
|
+
const unsendable = given === undefined ? undefined : unsendableKey(given);
|
|
562
|
+
if (unsendable)
|
|
563
|
+
invalid(`idempotencyKey ${unsendable}, or omit it.`);
|
|
542
564
|
const key = header ? (given ?? crypto.randomUUID()) : undefined;
|
|
543
565
|
// Recorded before dispatch: if the invocation deadline interrupts this
|
|
544
566
|
// call, no failure of ours arrives, and the invocation returns the key
|
|
@@ -552,16 +574,17 @@ export function restTools(vendor) {
|
|
|
552
574
|
catch (error) {
|
|
553
575
|
// A generated key exists nowhere else, so a failure must carry it:
|
|
554
576
|
// only that key lets the caller check or retry this exact write.
|
|
577
|
+
// The key stays in the message as a diagnostic; the advice to
|
|
578
|
+
// reuse it is a segment the invocation composes only where the key
|
|
579
|
+
// protects the whole logical call (keyResendAdvice), and exists
|
|
580
|
+
// only once the ledger shows a dispatched request carrying it.
|
|
555
581
|
if (!key || given || !(error instanceof ConnectorCallError))
|
|
556
582
|
throw error;
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
...(error.repair ? { repair: error.repair } : {}),
|
|
563
|
-
...(error.details ? { details: error.details } : {}),
|
|
564
|
-
});
|
|
583
|
+
const failure = providerFailureMessage(error, `${error.message} ${header}: ${key}.`);
|
|
584
|
+
if (!providerRequests(ctx)?.some((request) => request.dispatched && request.key === key))
|
|
585
|
+
throw failure;
|
|
586
|
+
throw keyResendAdvice(failure, key, " Reusing it with the exact original arguments retries this write without repeating it only while " +
|
|
587
|
+
"the vendor retains the key; after that, look the object up before retrying.");
|
|
565
588
|
}
|
|
566
589
|
},
|
|
567
590
|
},
|