@zackbart/connecta 0.34.0 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +393 -0
- package/README.md +62 -105
- package/SECURITY.md +3 -2
- package/bin/connecta.mjs +41 -16
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.d.ts +2 -1
- package/dist/access-tokens.js +66 -30
- package/dist/activity.d.ts +1 -1
- package/dist/activity.js +2 -0
- package/dist/agent-config.d.ts +23 -0
- package/dist/agent-config.js +59 -0
- package/dist/agent-description.js +16 -0
- package/dist/agent-identity.js +4 -4
- package/dist/agent-observation.js +39 -0
- package/dist/auth/clerk-sdk/client.d.ts +2 -0
- package/dist/auth/clerk-sdk/generated.js +554 -563
- package/dist/auth/clerk-transport.js +70 -3
- package/dist/auth/clerk.d.ts +13 -0
- package/dist/auth/clerk.js +653 -246
- package/dist/auth/cloudflare-access.js +46 -42
- package/dist/auth/downstream-client-metadata.js +2 -2
- package/dist/auth/downstream-oauth.js +2051 -1160
- package/dist/auth/downstream-revocation.js +88 -88
- package/dist/auth/native-call.js +20 -0
- package/dist/auth/native.js +184 -0
- package/dist/auth/oauth-start-reset.js +11 -5
- package/dist/auth/oauth-token-response.js +121 -102
- package/dist/auth/oauth-v2-migration.js +97 -78
- package/dist/auth/renewable-grant.js +10 -0
- package/dist/auth/static-oauth.js +663 -271
- package/dist/auth/vercel-agent.js +266 -0
- package/dist/auth-admission.js +204 -0
- package/dist/auth-elicitation.js +350 -241
- package/dist/authorization-handoff.js +164 -90
- package/dist/call-admission-context.js +3 -10
- package/dist/call-admission.js +2 -0
- package/dist/call-classification.js +9 -0
- package/dist/call-recovery.js +32 -13
- package/dist/canonical-json.js +8 -0
- package/dist/catalog-cache.js +369 -213
- package/dist/catalog-service.js +659 -530
- package/dist/catalog.js +5 -1
- package/dist/concurrency.js +0 -25
- package/dist/config-defaults.d.ts +1 -0
- package/dist/config-defaults.js +1 -0
- package/dist/config-schema.d.ts +3 -1
- package/dist/config-schema.js +13 -3
- package/dist/config.d.ts +40 -6
- package/dist/config.js +40 -26
- package/dist/connector-caller.d.ts +4 -0
- package/dist/connector-caller.js +17 -4
- package/dist/connectors/api-connector.d.ts +15 -7
- package/dist/connectors/api-connector.js +147 -57
- package/dist/connectors/guarded-fetch.d.ts +51 -17
- package/dist/connectors/guarded-fetch.js +215 -121
- package/dist/connectors/negotiation-cache.js +20 -10
- package/dist/connectors/option-shapes.js +1 -1
- package/dist/connectors/remote-mcp.d.ts +12 -30
- package/dist/connectors/remote-mcp.js +960 -564
- package/dist/d1.js +10 -243
- package/dist/derived-context.js +5 -0
- package/dist/describe-config.d.ts +5 -1
- package/dist/describe-config.js +3 -0
- package/dist/downstream-input.js +347 -260
- package/dist/errors.d.ts +87 -6
- package/dist/errors.js +114 -4
- package/dist/execute.js +87 -50
- package/dist/executor-admission.js +3 -0
- package/dist/executors/quickjs.js +163 -112
- package/dist/guarded-transport.js +50 -0
- package/dist/guest-runtime.js +1 -1
- package/dist/http/client.js +439 -0
- package/dist/http/errors.js +558 -0
- package/dist/http/fields.js +49 -0
- package/dist/http/hatch-paging.js +318 -0
- package/dist/http/hatch.js +1279 -0
- package/dist/http/json-schema.js +267 -0
- package/dist/http/node-transport.js +241 -0
- package/dist/http/oauth.js +162 -0
- package/dist/http/paging.js +228 -0
- package/dist/http/provider.js +221 -0
- package/dist/http/safety.js +82 -0
- package/dist/http/services.js +79 -0
- package/dist/http/tools.js +383 -0
- package/dist/http/vendor-index.js +138 -0
- package/dist/http/vendor.js +448 -0
- package/dist/http/write-request.js +66 -0
- package/dist/http-policy.d.ts +11 -0
- package/dist/http-policy.js +45 -0
- package/dist/inbound-credential.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -0
- package/dist/invocation-auth.js +30 -19
- package/dist/invocation.js +221 -87
- package/dist/mcp-result.js +0 -4
- package/dist/membership.d.ts +4 -0
- package/dist/meta-output.js +1 -1
- package/dist/meta-tools.js +534 -408
- package/dist/native-recovery.js +52 -0
- package/dist/native-tools.js +382 -0
- package/dist/node.d.ts +4 -3
- package/dist/node.js +45 -8
- package/dist/oauth-handoff.js +177 -104
- package/dist/oauth-partition.js +6 -20
- package/dist/oauth-sealing.js +6 -7
- package/dist/operation-vocabulary.js +101 -0
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +17 -4
- package/dist/operator-ui/agents.d.ts +5 -4
- package/dist/operator-ui/agents.js +17 -7
- package/dist/operator-ui/config-summary.js +8 -4
- package/dist/operator-ui/connection-map.js +9 -6
- package/dist/operator-ui/contract.d.ts +7 -3
- package/dist/operator-ui/endpoints.js +5 -14
- package/dist/operator-ui/fix-prompts.d.ts +1 -1
- package/dist/operator-ui/fix-prompts.js +0 -8
- package/dist/operator-ui/generated.js +3 -3
- package/dist/operator-ui/health.js +52 -0
- package/dist/operator-ui/model.d.ts +2 -3
- package/dist/operator-ui/overview.js +11 -19
- package/dist/operator-ui/setup-commands.js +0 -4
- package/dist/operator-ui/view.d.ts +5 -29
- package/dist/operator-ui/view.js +3 -59
- package/dist/program-writes.js +47 -26
- package/dist/provider-call-facts.d.ts +115 -0
- package/dist/provider-call-facts.js +182 -0
- package/dist/provider-call.d.ts +112 -0
- package/dist/provider-call.js +271 -0
- package/dist/provider.d.ts +71 -32
- package/dist/provider.js +134 -40
- package/dist/providers/_shared/google/hatch.js +91 -0
- package/dist/providers/_shared/google/kit.js +537 -0
- package/dist/providers/_shared/google/result-size.js +15 -2
- package/dist/providers/_shared/google/service-account.d.ts +2 -1
- package/dist/providers/_shared/google/service-account.js +8 -4
- package/dist/providers/_shared/google/workspace.d.ts +8 -209
- package/dist/providers/_shared/google/workspace.js +7 -426
- package/dist/providers/_shared/rest/compact-index.js +86 -0
- package/dist/providers/_shared/rest/dispatch.js +3 -23
- package/dist/providers/_shared/rest/kit.js +546 -0
- package/dist/providers/_shared/rest/operation-index.js +230 -14
- package/dist/providers/_shared/rest/response-media.js +11 -0
- package/dist/providers/_shared/rest/tools.js +101 -78
- package/dist/providers/_shared/rest/value-safety.js +353 -195
- package/dist/providers/_shared/sensitive-fields.js +27 -0
- package/dist/providers/breeze/api.js +584 -0
- package/dist/providers/breeze/index.d.ts +3 -3
- package/dist/providers/breeze/index.js +67 -1420
- package/dist/providers/breeze/openapi.generated.js +60 -0
- package/dist/providers/breeze/skill.generated.js +2 -6
- package/dist/providers/breeze/tools.js +696 -0
- package/dist/providers/ccb/api.js +496 -0
- package/dist/providers/ccb/index.d.ts +17 -37
- package/dist/providers/ccb/index.js +77 -1702
- package/dist/providers/ccb/lib/constants.d.ts +34 -0
- package/dist/providers/ccb/lib/constants.js +74 -0
- package/dist/providers/ccb/lib/guide.js +9 -0
- package/dist/providers/ccb/lib/verdicts.js +387 -0
- package/dist/providers/ccb/openapi.generated.js +397 -0
- package/dist/providers/ccb/safety.js +295 -0
- package/dist/providers/ccb/skill.generated.js +2 -6
- package/dist/providers/ccb/tools.js +853 -0
- package/dist/providers/cloudflare/api.js +1066 -0
- package/dist/providers/cloudflare/index.d.ts +7 -22
- package/dist/providers/cloudflare/index.js +80 -194
- package/dist/providers/cloudflare/lib/config.d.ts +16 -0
- package/dist/providers/cloudflare/lib/config.js +2 -0
- package/dist/providers/cloudflare/lib/discovery.js +29 -0
- package/dist/providers/cloudflare/{graphql.js → lib/graphql.js} +1 -1
- package/dist/providers/cloudflare/lib/hatch.js +103 -0
- package/dist/providers/cloudflare/lib/path-bounds.js +28 -0
- package/dist/providers/cloudflare/lib/response-media.js +75 -0
- package/dist/providers/cloudflare/openapi.generated.js +3299 -3208
- package/dist/providers/cloudflare/{value-safety.js → safety.js} +538 -18
- package/dist/providers/cloudflare/skill.generated.js +2 -10
- package/dist/providers/cloudflare/tools.js +360 -0
- package/dist/providers/docs/api.js +51 -0
- package/dist/providers/docs/index.d.ts +1 -12
- package/dist/providers/docs/index.js +53 -1165
- package/dist/providers/docs/lib/constants.d.ts +18 -0
- package/dist/providers/docs/lib/constants.js +18 -0
- package/dist/providers/docs/lib/discovery.js +26 -0
- package/dist/providers/docs/lib/requests.js +48 -0
- package/dist/providers/docs/lib/schemas.js +168 -0
- package/dist/providers/docs/openapi.generated.js +16 -0
- package/dist/providers/docs/safety.js +54 -0
- package/dist/providers/docs/skill.generated.js +2 -6
- package/dist/providers/docs/tools.js +802 -0
- package/dist/providers/drive/api.js +177 -0
- package/dist/providers/drive/index.d.ts +1 -12
- package/dist/providers/drive/index.js +38 -1448
- package/dist/providers/drive/lib/constants.d.ts +6 -0
- package/dist/providers/drive/lib/constants.js +6 -0
- package/dist/providers/drive/lib/contracts.js +532 -0
- package/dist/providers/drive/lib/hatch.js +197 -0
- package/dist/providers/drive/openapi.generated.js +77 -0
- package/dist/providers/drive/safety.js +90 -0
- package/dist/providers/drive/skill.generated.js +2 -6
- package/dist/providers/drive/tools.js +1290 -0
- package/dist/providers/forms/api.js +71 -0
- package/dist/providers/forms/index.d.ts +1 -12
- package/dist/providers/forms/index.js +49 -1145
- package/dist/providers/forms/lib/constants.d.ts +14 -0
- package/dist/providers/forms/lib/constants.js +19 -0
- package/dist/providers/forms/lib/hatch.js +72 -0
- package/dist/providers/forms/openapi.generated.js +23 -0
- package/dist/providers/forms/safety.js +34 -0
- package/dist/providers/forms/skill.generated.js +2 -8
- package/dist/providers/forms/tools.js +1024 -0
- package/dist/providers/github/api.js +150 -0
- package/dist/providers/github/index.d.ts +4 -5
- package/dist/providers/github/index.js +30 -87
- package/dist/providers/github/{auth.d.ts → lib/auth.d.ts} +5 -3
- package/dist/providers/github/{auth.js → lib/auth.js} +40 -43
- package/dist/providers/github/lib/discovery.js +132 -0
- package/dist/providers/github/lib/operations.js +58 -0
- package/dist/providers/github/{scope.d.ts → lib/scope.d.ts} +2 -0
- package/dist/providers/github/{scope.js → lib/scope.js} +25 -1
- package/dist/providers/github/openapi.generated.js +1216 -0
- package/dist/providers/github/safety.js +216 -0
- package/dist/providers/github/skill.generated.js +2 -5
- package/dist/providers/github/tools.js +506 -0
- package/dist/providers/gmail/api.js +114 -0
- package/dist/providers/gmail/index.d.ts +1 -12
- package/dist/providers/gmail/index.js +73 -1621
- package/dist/providers/gmail/lib/constants.d.ts +13 -0
- package/dist/providers/gmail/lib/constants.js +16 -0
- package/dist/providers/gmail/lib/index.js +173 -0
- package/dist/providers/gmail/lib/schemas.js +176 -0
- package/dist/providers/gmail/lib/threading.js +120 -0
- package/dist/providers/gmail/openapi.generated.js +92 -0
- package/dist/providers/gmail/safety.js +21 -0
- package/dist/providers/gmail/skill.generated.js +2 -6
- package/dist/providers/gmail/tools.js +1435 -0
- package/dist/providers/overflow/api.js +1008 -0
- package/dist/providers/overflow/index.d.ts +3 -17
- package/dist/providers/overflow/index.js +60 -1488
- package/dist/providers/overflow/skill.generated.js +2 -5
- package/dist/providers/overflow/tools.js +873 -0
- package/dist/providers/planning-center/api.js +467 -0
- package/dist/providers/planning-center/index.d.ts +6 -35
- package/dist/providers/planning-center/index.js +69 -2445
- package/dist/providers/planning-center/lib/constants.d.ts +58 -0
- package/dist/providers/planning-center/lib/constants.js +83 -0
- package/dist/providers/planning-center/lib/discovery.js +40 -0
- package/dist/providers/planning-center/lib/guide.js +39 -0
- package/dist/providers/planning-center/lib/json-api.js +83 -0
- package/dist/providers/planning-center/lib/verdicts.js +1758 -0
- package/dist/providers/planning-center/openapi.generated.js +1764 -0
- package/dist/providers/planning-center/safety.js +188 -0
- package/dist/providers/planning-center/skill.generated.js +2 -27
- package/dist/providers/planning-center/tools.js +1010 -0
- package/dist/providers/sheets/api.js +133 -0
- package/dist/providers/sheets/index.d.ts +4 -16
- package/dist/providers/sheets/index.js +61 -1172
- package/dist/providers/sheets/lib/constants.d.ts +14 -0
- package/dist/providers/sheets/lib/constants.js +14 -0
- package/dist/providers/sheets/lib/hatch.js +163 -0
- package/dist/providers/sheets/lib/requests.js +105 -0
- package/dist/providers/sheets/lib/schemas.js +138 -0
- package/dist/providers/sheets/openapi.generated.js +30 -0
- package/dist/providers/sheets/safety.js +27 -0
- package/dist/providers/sheets/skill.generated.js +2 -7
- package/dist/providers/sheets/tools.js +734 -0
- package/dist/providers/slides/api.js +147 -0
- package/dist/providers/slides/index.d.ts +2 -10
- package/dist/providers/slides/index.js +48 -2555
- package/dist/providers/slides/lib/constants.d.ts +11 -0
- package/dist/providers/slides/lib/constants.js +11 -0
- package/dist/providers/slides/lib/requests.js +60 -0
- package/dist/providers/slides/lib/schemas.js +565 -0
- package/dist/providers/slides/openapi.generated.js +18 -0
- package/dist/providers/slides/skill.generated.js +2 -6
- package/dist/providers/slides/tools.js +1968 -0
- package/dist/providers/stripe/api.js +401 -0
- package/dist/providers/stripe/index.d.ts +7 -27
- package/dist/providers/stripe/index.js +75 -191
- package/dist/providers/stripe/lib/config.d.ts +4 -0
- package/dist/providers/stripe/lib/config.js +2 -0
- package/dist/providers/stripe/openapi.generated.js +4 -3
- package/dist/providers/stripe/{value-safety.js → safety.js} +5 -5
- package/dist/providers/stripe/skill.generated.js +2 -10
- package/dist/providers/stripe/tools.js +77 -0
- package/dist/providers/tithely/api.js +531 -0
- package/dist/providers/tithely/index.d.ts +19 -0
- package/dist/providers/tithely/index.js +86 -965
- package/dist/providers/tithely/skill.generated.js +2 -5
- package/dist/providers/tithely/tools.js +426 -0
- package/dist/providers/vercel/api.js +551 -0
- package/dist/providers/vercel/index.d.ts +11 -25
- package/dist/providers/vercel/index.js +67 -261
- package/dist/providers/vercel/lib/config.d.ts +2 -0
- package/dist/providers/vercel/lib/config.js +2 -0
- package/dist/providers/vercel/lib/index.js +117 -0
- package/dist/providers/vercel/lib/logs.js +100 -0
- package/dist/providers/vercel/lib/pin.js +270 -0
- package/dist/providers/vercel/openapi.generated.js +398 -376
- package/dist/providers/vercel/{value-safety.js → safety.js} +261 -42
- package/dist/providers/vercel/skill.generated.js +2 -7
- package/dist/providers/vercel/tools.js +642 -0
- package/dist/redaction-identity.js +47 -0
- package/dist/redaction-marker.js +125 -0
- package/dist/registry.d.ts +30 -28
- package/dist/registry.js +446 -413
- package/dist/routes/access-tokens.js +1 -1
- package/dist/routes/actor-labels.js +1 -1
- package/dist/routes/connect.js +1 -1
- package/dist/routes/dispatch.js +103 -0
- package/dist/routes/mcp.js +170 -79
- package/dist/routes/oauth.js +3 -1
- package/dist/routes/operator.js +2 -2
- package/dist/routes/shared.d.ts +5 -4
- package/dist/routes/shared.js +244 -177
- package/dist/routes/skills.js +10 -8
- package/dist/routes/ui-config.js +27 -3
- package/dist/routes/ui-data.js +29 -22
- package/dist/routes/ui.js +1 -1
- package/dist/runtime/call-admission.js +0 -26
- package/dist/runtime/call-entry.js +30 -0
- package/dist/runtime/call.js +556 -0
- package/dist/runtime/connector-scope.js +7 -0
- package/dist/runtime/credential-probe.js +34 -0
- package/dist/runtime/executor.js +40 -0
- package/dist/runtime/native-storage.js +63 -0
- package/dist/runtime/registry.js +27 -0
- package/dist/runtime/request-scope.js +72 -0
- package/dist/runtime/run.js +15 -7
- package/dist/runtime/shared-read.js +34 -5
- package/dist/runtime/storage.js +34 -21
- package/dist/sent-secrets.d.ts +194 -37
- package/dist/sent-secrets.js +1743 -340
- package/dist/server.js +5 -56
- package/dist/skills.js +330 -224
- package/dist/sqlite.js +15 -14
- package/dist/storage/d1-copy.js +255 -0
- package/dist/storage/d1-driver.js +15 -0
- package/dist/storage/keys.js +28 -0
- package/dist/storage/sql-driver.js +88 -0
- package/dist/storage/sql-native.js +544 -0
- package/dist/storage/sql-schema.d.ts +6 -0
- package/dist/storage/sql-schema.js +15 -0
- package/dist/storage/sql.d.ts +14 -20
- package/dist/storage/sql.js +29 -535
- package/dist/timeout.js +0 -26
- package/dist/tool-safety.js +20 -10
- package/dist/types.d.ts +11 -99
- package/dist/upward.js +3 -0
- package/dist/usage-guide.js +3 -1
- package/dist/validate.d.ts +4 -2
- package/dist/validate.js +86 -9
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +312 -292
- package/dist/workspace-subject.js +5 -1
- package/dist/write-outcome.js +454 -34
- package/documentation/README.md +27 -7
- package/documentation/architecture.md +2550 -186
- package/documentation/auth.md +821 -98
- package/documentation/code-mode.md +56 -21
- package/documentation/deploying.md +164 -20
- package/documentation/hosted-mcp-migration.md +460 -0
- package/documentation/integrating.md +144 -105
- package/documentation/meta-tools.md +411 -34
- package/documentation/operating.md +28 -4
- package/documentation/operator-tests.md +11 -2
- package/documentation/operator-ui.md +103 -33
- package/documentation/provider-auth-migration.md +38 -63
- package/documentation/provider-migration-0.29.md +10 -5
- package/documentation/provider-migration-0.35.md +975 -0
- package/documentation/providers.md +965 -0
- package/examples/worker/AGENTS.md +23 -9
- package/examples/worker/README.md +254 -135
- package/examples/worker/alchemy.run.ts +116 -0
- package/examples/worker/gitignore +10 -0
- package/examples/worker/src/connecta.config.ts +23 -21
- package/examples/worker/wrangler.jsonc +14 -5
- package/guides/connecting-agents.md +157 -0
- package/guides/how-it-works.md +156 -0
- package/guides/safety-and-trust.md +153 -0
- package/package.json +27 -42
- package/templates/node/.env.example +3 -0
- package/templates/node/README.md +22 -2
- package/templates/node/docker-compose.yml +1 -0
- package/templates/node/package.json +3 -2
- package/templates/node/src/connecta.config.ts +8 -0
- package/dist/catalog-drift.d.ts +0 -134
- package/dist/catalog-drift.js +0 -493
- package/dist/providers/_shared/request-token.js +0 -83
- package/dist/providers/_shared/rest/operation-index.d.ts +0 -133
- package/dist/providers/_shared/rest/tools.d.ts +0 -141
- package/dist/providers/_shared/rest/value-safety.d.ts +0 -201
- package/dist/providers/basecamp/index.d.ts +0 -43
- package/dist/providers/basecamp/index.js +0 -997
- package/dist/providers/basecamp/skill.generated.js +0 -9
- package/dist/providers/cloudflare/rest.d.ts +0 -37
- package/dist/providers/cloudflare/rest.js +0 -1078
- package/dist/providers/github/hosted.js +0 -218
- package/dist/providers/github/rest.js +0 -321
- package/dist/providers/infisical/index.d.ts +0 -7
- package/dist/providers/infisical/index.js +0 -713
- package/dist/providers/infisical/skill.generated.js +0 -8
- package/dist/providers/linear/index.d.ts +0 -55
- package/dist/providers/linear/index.js +0 -352
- package/dist/providers/linear/skill.generated.js +0 -8
- package/dist/providers/mixpanel/index.d.ts +0 -57
- package/dist/providers/mixpanel/index.js +0 -468
- package/dist/providers/mixpanel/operations.js +0 -20
- package/dist/providers/mixpanel/rest.js +0 -603
- package/dist/providers/mixpanel/skill.generated.js +0 -10
- package/dist/providers/mixpanel/value-safety.js +0 -16
- package/dist/providers/notion/index.d.ts +0 -65
- package/dist/providers/notion/index.js +0 -368
- package/dist/providers/notion/openapi.generated.js +0 -75
- package/dist/providers/notion/rest.d.ts +0 -24
- package/dist/providers/notion/rest.js +0 -1226
- package/dist/providers/notion/skill.generated.js +0 -11
- package/dist/providers/notion/value-safety.js +0 -56
- package/dist/providers/revenuecat/index.d.ts +0 -49
- package/dist/providers/revenuecat/index.js +0 -557
- package/dist/providers/revenuecat/skill.generated.js +0 -11
- package/dist/providers/stripe/rest.d.ts +0 -14
- package/dist/providers/stripe/rest.js +0 -402
- package/dist/providers/vercel/rest.d.ts +0 -20
- package/dist/providers/vercel/rest.js +0 -1020
- package/dist/providers/wispr-flow/index.d.ts +0 -14
- package/dist/providers/wispr-flow/index.js +0 -117
- package/dist/providers/wispr-flow/skill.generated.js +0 -8
- /package/dist/providers/github/{key.js → lib/key.js} +0 -0
|
@@ -0,0 +1,1279 @@
|
|
|
1
|
+
// Internal kit hatch (decisions 0010 B.4, 0011 S1, 0014). Unreachable from src/index.ts.
|
|
2
|
+
// The operation index validates requests; only an explicit verdict routes
|
|
3
|
+
// them. Connection.sendPath owns admission, dispatch and result safety.
|
|
4
|
+
//
|
|
5
|
+
// Argument and envelope names follow the REST family's hatch (`path`,
|
|
6
|
+
// `method`, `query`, `body`, `{ status, data }`), with S5's `fields` for its
|
|
7
|
+
// `select`. Discovery stays inside 0014's budget: search returns compact
|
|
8
|
+
// entries (8 at most, 16 KiB of JSON at most, with an explicit continuation
|
|
9
|
+
// cursor) and details return at most 32 KiB, cutting only optional parts and
|
|
10
|
+
// listing them in `truncated`; a required parameter or body field is never cut.
|
|
11
|
+
import { Effect, Schema } from "effect";
|
|
12
|
+
import { lexicalSearchContent } from "../catalog.js";
|
|
13
|
+
import { registerOperationVocabulary } from "../operation-vocabulary.js";
|
|
14
|
+
import { ConnectorCallError } from "../errors.js";
|
|
15
|
+
import { keyResendAdvice } from "../errors.js";
|
|
16
|
+
import { IDEMPOTENCY_KEY_MAX_LENGTH } from "../call-recovery.js";
|
|
17
|
+
import { providerFailureMessage } from "../write-outcome.js";
|
|
18
|
+
import { redactionAltered } from "../redaction-marker.js";
|
|
19
|
+
import { refuseWithheldValues } from "../providers/_shared/rest/value-safety.js";
|
|
20
|
+
import { Refusal, toConnectorCallError } from "./errors.js";
|
|
21
|
+
import { fieldsInput, fieldsProblem, selectFields } from "./fields.js";
|
|
22
|
+
import { Integer, isMaxChars, isMinChars } from "./json-schema.js";
|
|
23
|
+
import { boundCursor, StandardPage } from "./paging.js";
|
|
24
|
+
import { Classification, Dispatches, IdempotencyKey, RequestSecrets, Signal } from "./services.js";
|
|
25
|
+
import { read, write } from "./tools.js";
|
|
26
|
+
const indexedOperations = new WeakMap();
|
|
27
|
+
function operationsOf(index) {
|
|
28
|
+
let operations = indexedOperations.get(index);
|
|
29
|
+
if (!operations) {
|
|
30
|
+
const first = new Map();
|
|
31
|
+
for (const op of index.ops) {
|
|
32
|
+
const key = `${op.method} ${op.path}`;
|
|
33
|
+
// Keep the first match, as the previous per-key Array.find did.
|
|
34
|
+
if (!first.has(key))
|
|
35
|
+
first.set(key, op);
|
|
36
|
+
}
|
|
37
|
+
operations = first;
|
|
38
|
+
indexedOperations.set(index, operations);
|
|
39
|
+
}
|
|
40
|
+
return operations;
|
|
41
|
+
}
|
|
42
|
+
/** 0014's starting discovery budgets, measured as serialized JSON bytes of the tool's own result. */
|
|
43
|
+
export const SEARCH_BUDGET = { hits: 6, maxHits: 8, bytes: 16 * 1024, summaryChars: 300, refusalChars: 512 };
|
|
44
|
+
export const DETAILS_BUDGET_BYTES = 32 * 1024;
|
|
45
|
+
const text = (min, max) => Schema.String.check(isMinChars(min), isMaxChars(max));
|
|
46
|
+
const ObjectValue = Schema.Record(Schema.String, Schema.Unknown);
|
|
47
|
+
const Envelope = Schema.Struct({
|
|
48
|
+
status: Integer.annotate({ description: "HTTP status." }),
|
|
49
|
+
data: Schema.Unknown.pipe(Schema.annotateKey({ description: "Response body after value safety and the connection's privacy filtering." })),
|
|
50
|
+
});
|
|
51
|
+
const HATCH_FIELDS = fieldsInput("Dot paths to keep in data, applied to each item of an array, after value safety and privacy filtering.");
|
|
52
|
+
function invalid(message) {
|
|
53
|
+
throw new ConnectorCallError("invalid_args", message);
|
|
54
|
+
}
|
|
55
|
+
/** Keep the index's structured argument repairs, while unexpected failures remain withheld defects. */
|
|
56
|
+
function checked(run) {
|
|
57
|
+
return Effect.try({ try: run, catch: (error) => error }).pipe(Effect.catch((error) => error instanceof ConnectorCallError ? Effect.fail(new Refusal({ error })) : Effect.die(error)));
|
|
58
|
+
}
|
|
59
|
+
function record(value) {
|
|
60
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
61
|
+
}
|
|
62
|
+
function withheldCursor(value) {
|
|
63
|
+
return redactionAltered(value) || (typeof value === "string" && /%5bredacted%5d/i.test(value));
|
|
64
|
+
}
|
|
65
|
+
/** Detect the signed envelope, including damaged signatures, without admitting it as a record id. */
|
|
66
|
+
function signedCursorShape(value) {
|
|
67
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9_-]+$/.test(value))
|
|
68
|
+
return false;
|
|
69
|
+
try {
|
|
70
|
+
const decoded = JSON.parse(atob(value.replace(/-/g, "+").replace(/_/g, "/")));
|
|
71
|
+
return record(decoded) && decoded["v"] === 2 && ["d", "c", "m"].every((key) => typeof decoded[key] === "string");
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** A method enum; a single method renders as a plain enum, not a one-branch anyOf. */
|
|
78
|
+
function methodEnum(methods) {
|
|
79
|
+
return methods.length === 1 ? Schema.Literal(methods[0]) : Schema.Literals(methods);
|
|
80
|
+
}
|
|
81
|
+
const bytes = (value) => new TextEncoder().encode(JSON.stringify(value)).length;
|
|
82
|
+
/** The binding carries ordered string pairs, preserving scalar arrays as repeated parameters. */
|
|
83
|
+
function parameters(values) {
|
|
84
|
+
return Object.entries(values).flatMap(([name, value]) => {
|
|
85
|
+
if (value === undefined)
|
|
86
|
+
return [];
|
|
87
|
+
return (Array.isArray(value) ? value : [value]).map((item) => {
|
|
88
|
+
if (typeof item !== "string" &&
|
|
89
|
+
typeof item !== "boolean" &&
|
|
90
|
+
!(typeof item === "number" && Number.isFinite(item))) {
|
|
91
|
+
invalid(`Parameter ${name} takes a string, finite number, boolean, or a list of them.`);
|
|
92
|
+
}
|
|
93
|
+
return [name, String(item)];
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
const cap = (text, chars) => {
|
|
98
|
+
if (text.length <= chars)
|
|
99
|
+
return text;
|
|
100
|
+
let end = chars - 1;
|
|
101
|
+
if (/[\uD800-\uDBFF]/.test(text[end - 1]))
|
|
102
|
+
end--;
|
|
103
|
+
return `${text.slice(0, end)}…`;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Every piece of a contract, in order: long labels, parameters, then the body.
|
|
107
|
+
* An object body travels as its frame (every keyword but `properties` and
|
|
108
|
+
* `required`: type, description, additionalProperties, …) and one piece per
|
|
109
|
+
* property, each carrying whether it is required, so pages and chunks
|
|
110
|
+
* together reconstruct the whole schema. Any other body travels whole.
|
|
111
|
+
*/
|
|
112
|
+
function piecesOf(contract, capped) {
|
|
113
|
+
const schema = contract.body?.schema;
|
|
114
|
+
const properties = schema?.["properties"];
|
|
115
|
+
const required = new Set(Array.isArray(schema?.["required"]) ? schema["required"] : []);
|
|
116
|
+
const object = properties !== null && typeof properties === "object" && !Array.isArray(properties);
|
|
117
|
+
const frame = object
|
|
118
|
+
? Object.fromEntries(Object.entries(schema).filter(([key]) => key !== "properties" && key !== "required"))
|
|
119
|
+
: undefined;
|
|
120
|
+
return [
|
|
121
|
+
...(capped.operationId ? [{ kind: "text", of: "operationId", value: contract.operationId }] : []),
|
|
122
|
+
...(capped.summary ? [{ kind: "text", of: "summary", value: contract.summary }] : []),
|
|
123
|
+
...(capped.refusal ? [{ kind: "text", of: "refusal", value: contract.refusal }] : []),
|
|
124
|
+
...contract.parameters.map((value) => ({ kind: "parameter", of: `parameters.${value.name}`, value })),
|
|
125
|
+
...(contract.body === undefined
|
|
126
|
+
? []
|
|
127
|
+
: frame
|
|
128
|
+
? [
|
|
129
|
+
{ kind: "frame", of: "body.schema", value: frame },
|
|
130
|
+
...Object.entries(properties).map(([name, value]) => ({
|
|
131
|
+
kind: "property",
|
|
132
|
+
of: `body.schema.properties.${name}`,
|
|
133
|
+
name,
|
|
134
|
+
value,
|
|
135
|
+
required: required.has(name),
|
|
136
|
+
})),
|
|
137
|
+
]
|
|
138
|
+
: [{ kind: "body", of: "body.schema", value: contract.body.schema }]),
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
/** The standard hatch over an operation index; a missing verdict refuses every method. */
|
|
142
|
+
export function hatch(options) {
|
|
143
|
+
const { name, index, connection } = options;
|
|
144
|
+
if (options.binding?.index !== undefined && options.binding.index !== index)
|
|
145
|
+
throw new Error("This hatch binding requires its reviewed operation index.");
|
|
146
|
+
const title = options.title ?? name;
|
|
147
|
+
if (options.textBodies !== undefined && options.textBodies !== true)
|
|
148
|
+
throw new Error("Hatch textBodies must opt in with true.");
|
|
149
|
+
if (options.fieldsRoot !== undefined && !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(options.fieldsRoot))
|
|
150
|
+
throw new Error("A hatch fieldsRoot must be a property name.");
|
|
151
|
+
const headerFields = Object.fromEntries(Object.entries(options.headers ?? {}).map(([key, entry]) => {
|
|
152
|
+
if (!/^[a-z0-9-]+$/.test(key) ||
|
|
153
|
+
!entry.description.trim() ||
|
|
154
|
+
!entry.values.length ||
|
|
155
|
+
entry.values.some((value) => !value || /[\r\n]/.test(value)))
|
|
156
|
+
throw new Error("Hatch headers need lowercase names, descriptions and closed values.");
|
|
157
|
+
return [
|
|
158
|
+
key,
|
|
159
|
+
Schema.optionalKey(Schema.Literals([...entry.values]).annotate({ description: entry.description })),
|
|
160
|
+
];
|
|
161
|
+
}));
|
|
162
|
+
const Headers = Object.keys(headerFields).length ? Schema.Struct(headerFields) : undefined;
|
|
163
|
+
const Fields = options.fieldsRoot
|
|
164
|
+
? fieldsInput(`Dot paths to keep in data. Envelope fields begin with ${options.fieldsRoot}, e.g. ["${options.fieldsRoot}.id"] or ["${options.fieldsRoot}[].id"], where declared.`)
|
|
165
|
+
: HATCH_FIELDS;
|
|
166
|
+
const Body = options.textBodies
|
|
167
|
+
? Schema.Union([ObjectValue, Schema.Array(Schema.Unknown), Schema.String])
|
|
168
|
+
: undefined;
|
|
169
|
+
if (options.writeEnabled !== undefined && typeof options.writeEnabled !== "boolean")
|
|
170
|
+
throw new Error("Hatch writeEnabled must be a boolean.");
|
|
171
|
+
if (options.binding && !options.binding.review.trim())
|
|
172
|
+
throw new Error("A hatch binding needs a vendor review.");
|
|
173
|
+
const idempotency = options.idempotency
|
|
174
|
+
? { ...options.idempotency, methods: [...options.idempotency.methods] }
|
|
175
|
+
: undefined;
|
|
176
|
+
if (!name.trim())
|
|
177
|
+
throw new Error("A hatch needs a nonempty tool prefix.");
|
|
178
|
+
const operations = operationsOf(index);
|
|
179
|
+
const known = (key) => operations.has(key);
|
|
180
|
+
// Snapshot reviews: a retained declaration cannot widen a served hatch.
|
|
181
|
+
const verdicts = new Map(Object.entries(options.verdicts));
|
|
182
|
+
for (const [key, verdict] of verdicts) {
|
|
183
|
+
if (!known(key))
|
|
184
|
+
throw new Error(`Hatch verdict ${key} is not in the API index.`);
|
|
185
|
+
if (!["read", "write", "destructive", "refuse"].includes(verdict)) {
|
|
186
|
+
throw new Error(`Hatch verdict ${key} must be read, write, destructive, or refuse.`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const named = new Map(Object.entries(options.binding?.named ?? options.named ?? {}));
|
|
190
|
+
for (const [key, tool] of named) {
|
|
191
|
+
if (!known(key) || !tool.trim())
|
|
192
|
+
throw new Error(`Hatch named tool for ${key} needs an indexed operation.`);
|
|
193
|
+
}
|
|
194
|
+
const verdictOf = (op) => verdicts.get(`${op.method} ${op.path}`) ?? "refuse";
|
|
195
|
+
const refusalOf = (op) => connection.safety.refusal?.({ method: op.method, template: op.path }) ?? options.binding?.refusal?.(op);
|
|
196
|
+
const discoveryRefusals = new Map();
|
|
197
|
+
if (options.binding?.discoveryRefusal)
|
|
198
|
+
for (const op of index.ops) {
|
|
199
|
+
const reason = options.binding.discoveryRefusal(op);
|
|
200
|
+
if (reason !== undefined)
|
|
201
|
+
discoveryRefusals.set(`${op.method} ${op.path}`, reason);
|
|
202
|
+
}
|
|
203
|
+
const discoveryVerdictOf = (op) => discoveryRefusals.has(`${op.method} ${op.path}`) ? "refuse" : verdictOf(op);
|
|
204
|
+
const discoveryRefusal = (op) => discoveryRefusals.get(`${op.method} ${op.path}`) ??
|
|
205
|
+
(verdictOf(op) === "refuse"
|
|
206
|
+
? (refusalOf(op) ?? `Connecta refuses ${op.method} ${op.path}: it has no admitted hatch verdict.`)
|
|
207
|
+
: undefined);
|
|
208
|
+
const pages = new Map(Object.entries(options.pages ?? {}).map(([key, declared]) => {
|
|
209
|
+
const adapters = Array.isArray(declared) ? declared : [declared];
|
|
210
|
+
if (!adapters.length)
|
|
211
|
+
throw new Error(`Hatch paging for ${key} needs a cursor adapter.`);
|
|
212
|
+
const seen = new Set();
|
|
213
|
+
const snapshot = adapters.map((adapter) => {
|
|
214
|
+
if (!known(key) ||
|
|
215
|
+
verdicts.get(key) !== "read" ||
|
|
216
|
+
!adapter?.param.trim() ||
|
|
217
|
+
!["query", "body"].includes(adapter.in) ||
|
|
218
|
+
typeof adapter.next !== "function" ||
|
|
219
|
+
typeof adapter.decode !== "function" ||
|
|
220
|
+
(adapter.prepareQuery !== undefined && typeof adapter.prepareQuery !== "function") ||
|
|
221
|
+
(adapter.total !== undefined && typeof adapter.total !== "function")) {
|
|
222
|
+
throw new Error(`Hatch paging for ${key} needs an indexed read and a cursor adapter.`);
|
|
223
|
+
}
|
|
224
|
+
const location = `${adapter.in}:${adapter.param}`;
|
|
225
|
+
if (seen.has(location))
|
|
226
|
+
throw new Error(`Hatch paging for ${key} repeats a cursor parameter.`);
|
|
227
|
+
seen.add(location);
|
|
228
|
+
const op = operations.get(key);
|
|
229
|
+
const contract = index.contract(op);
|
|
230
|
+
const body = contract.body?.schema;
|
|
231
|
+
if (adapter.in === "query"
|
|
232
|
+
? !contract.parameters.some((param) => param.in === "query" && param.name === adapter.param)
|
|
233
|
+
: !Object.hasOwn(body?.properties ?? {}, adapter.param)) {
|
|
234
|
+
throw new Error(`Hatch paging for ${key} must name a declared ${adapter.in} parameter.`);
|
|
235
|
+
}
|
|
236
|
+
return Object.freeze({ ...adapter, ...(adapter.headers ? { headers: [...adapter.headers] } : {}) });
|
|
237
|
+
});
|
|
238
|
+
return [key, snapshot];
|
|
239
|
+
}));
|
|
240
|
+
const callEnvelope = Schema.Struct({
|
|
241
|
+
...Envelope.fields,
|
|
242
|
+
...(options.binding?.truncated
|
|
243
|
+
? {
|
|
244
|
+
truncated: Schema.optionalKey(Schema.Array(Schema.String).annotate({ description: "Dot paths cut for size, including the data prefix." })),
|
|
245
|
+
}
|
|
246
|
+
: {}),
|
|
247
|
+
...(options.binding?.warnings
|
|
248
|
+
? {
|
|
249
|
+
warnings: Schema.optionalKey(Schema.Array(Schema.String).annotate({
|
|
250
|
+
description: "Reviewed warnings preserved after fields selection.",
|
|
251
|
+
})),
|
|
252
|
+
}
|
|
253
|
+
: {}),
|
|
254
|
+
});
|
|
255
|
+
const hasTotals = [...pages.values()].some((adapters) => adapters.some((adapter) => adapter.total !== undefined));
|
|
256
|
+
const envelope = pages.size
|
|
257
|
+
? Schema.Struct({
|
|
258
|
+
...callEnvelope.fields,
|
|
259
|
+
page: Schema.optionalKey(Schema.Struct({
|
|
260
|
+
...StandardPage.fields,
|
|
261
|
+
...(hasTotals ? { total: Schema.optionalKey(Integer.check(Schema.isGreaterThanOrEqualTo(0))) } : {}),
|
|
262
|
+
param: Schema.String.annotate({
|
|
263
|
+
description: options.pageCursor === "argument"
|
|
264
|
+
? "Vendor parameter restored from cursor; pass nextCursor as the top-level cursor."
|
|
265
|
+
: "Put nextCursor in this vendor parameter, unchanged.",
|
|
266
|
+
}),
|
|
267
|
+
in: Schema.Literals(["query", "body"]),
|
|
268
|
+
})),
|
|
269
|
+
})
|
|
270
|
+
: callEnvelope;
|
|
271
|
+
const tool = (kind) => `${name}_api_${kind}`;
|
|
272
|
+
const route = (op) => {
|
|
273
|
+
const verdict = discoveryVerdictOf(op);
|
|
274
|
+
const authored = named.get(`${op.method} ${op.path}`);
|
|
275
|
+
return {
|
|
276
|
+
...(verdict === "refuse" || (verdict !== "read" && options.writeEnabled === false)
|
|
277
|
+
? {}
|
|
278
|
+
: { tool: tool(verdict === "read" ? "read" : "write") }),
|
|
279
|
+
...(authored ? { named: authored } : {}),
|
|
280
|
+
};
|
|
281
|
+
};
|
|
282
|
+
const updateWords = ["update", "edit", "modify", "change", "set", "apply", "make", "rename"];
|
|
283
|
+
const verbs = {
|
|
284
|
+
delete: ["delete", "remove", "destroy", "cancel", "stop"],
|
|
285
|
+
remove: ["delete", "remove", "destroy"],
|
|
286
|
+
cancel: ["cancel", "stop", "delete"],
|
|
287
|
+
stop: ["cancel", "stop", "delete"],
|
|
288
|
+
update: updateWords,
|
|
289
|
+
edit: updateWords,
|
|
290
|
+
make: updateWords,
|
|
291
|
+
set: updateWords,
|
|
292
|
+
apply: updateWords,
|
|
293
|
+
change: updateWords,
|
|
294
|
+
rename: updateWords,
|
|
295
|
+
create: ["create", "add", "new"],
|
|
296
|
+
add: ["create", "add", "new"],
|
|
297
|
+
get: ["get", "retrieve", "read"],
|
|
298
|
+
read: ["get", "retrieve", "read"],
|
|
299
|
+
retrieve: ["get", "retrieve", "read"],
|
|
300
|
+
fetch: ["get", "retrieve", "read"],
|
|
301
|
+
details: ["get", "retrieve", "read"],
|
|
302
|
+
detail: ["get", "retrieve", "read"],
|
|
303
|
+
info: ["get", "retrieve", "read"],
|
|
304
|
+
view: ["get", "retrieve", "read"],
|
|
305
|
+
status: ["get", "retrieve", "read", "list"],
|
|
306
|
+
history: ["list", "find", "search"],
|
|
307
|
+
list: ["list", "find", "search"],
|
|
308
|
+
find: ["list", "find", "search"],
|
|
309
|
+
search: ["list", "find", "search"],
|
|
310
|
+
refund: ["refund", "reimburse"],
|
|
311
|
+
charge: ["charge"],
|
|
312
|
+
issue: ["issue"],
|
|
313
|
+
process: ["process"],
|
|
314
|
+
send: ["send"],
|
|
315
|
+
};
|
|
316
|
+
const readActions = new Set([
|
|
317
|
+
"get",
|
|
318
|
+
"read",
|
|
319
|
+
"list",
|
|
320
|
+
"find",
|
|
321
|
+
"search",
|
|
322
|
+
"details",
|
|
323
|
+
"detail",
|
|
324
|
+
"status",
|
|
325
|
+
"history",
|
|
326
|
+
"info",
|
|
327
|
+
"view",
|
|
328
|
+
"retrieve",
|
|
329
|
+
"fetch",
|
|
330
|
+
]);
|
|
331
|
+
const writeActions = new Set([
|
|
332
|
+
"create",
|
|
333
|
+
"add",
|
|
334
|
+
"update",
|
|
335
|
+
"edit",
|
|
336
|
+
"make",
|
|
337
|
+
"set",
|
|
338
|
+
"apply",
|
|
339
|
+
"change",
|
|
340
|
+
"rename",
|
|
341
|
+
"send",
|
|
342
|
+
"delete",
|
|
343
|
+
"remove",
|
|
344
|
+
"cancel",
|
|
345
|
+
"stop",
|
|
346
|
+
"refund",
|
|
347
|
+
"charge",
|
|
348
|
+
"issue",
|
|
349
|
+
"process",
|
|
350
|
+
]);
|
|
351
|
+
const queryWords = (query) => lexicalSearchContent(query)
|
|
352
|
+
.split(/[^a-z0-9]+/)
|
|
353
|
+
.filter((word) => word.length >= 2);
|
|
354
|
+
const writeVerbs = new Set([
|
|
355
|
+
"create",
|
|
356
|
+
"add",
|
|
357
|
+
"update",
|
|
358
|
+
"edit",
|
|
359
|
+
"make",
|
|
360
|
+
"set",
|
|
361
|
+
"apply",
|
|
362
|
+
"change",
|
|
363
|
+
"rename",
|
|
364
|
+
"send",
|
|
365
|
+
"delete",
|
|
366
|
+
"remove",
|
|
367
|
+
"cancel",
|
|
368
|
+
"stop",
|
|
369
|
+
]);
|
|
370
|
+
const functionWords = new Set([
|
|
371
|
+
"by",
|
|
372
|
+
"its",
|
|
373
|
+
"my",
|
|
374
|
+
"your",
|
|
375
|
+
"their",
|
|
376
|
+
"his",
|
|
377
|
+
"her",
|
|
378
|
+
"no",
|
|
379
|
+
"yes",
|
|
380
|
+
"at",
|
|
381
|
+
"as",
|
|
382
|
+
"is",
|
|
383
|
+
"be",
|
|
384
|
+
"or",
|
|
385
|
+
"not",
|
|
386
|
+
]);
|
|
387
|
+
let resourceTerms;
|
|
388
|
+
const isNoun = (term) => {
|
|
389
|
+
if (functionWords.has(term) || /^\d+$/.test(term))
|
|
390
|
+
return false;
|
|
391
|
+
if (options.binding?.searchNoun?.(term) === false)
|
|
392
|
+
return false;
|
|
393
|
+
if (term.length < 5 || !/\d/.test(term))
|
|
394
|
+
return true;
|
|
395
|
+
resourceTerms ??= new Set(index.ops.flatMap((op) => queryWords(op.path.replace(/\{[^}]*\}/g, ""))));
|
|
396
|
+
return resourceTerms.has(term);
|
|
397
|
+
};
|
|
398
|
+
const queryTerms = (query) => {
|
|
399
|
+
const terms = queryWords(query).filter((term) => !functionWords.has(term));
|
|
400
|
+
const resourcePhrase = queryWords(query.split(/(?:^|\s)(?:for|of|in|on|by|with)(?=\s|$)/i)[0]).filter(isNoun);
|
|
401
|
+
const lookup = terms.find((term) => readActions.has(term));
|
|
402
|
+
const first = terms[0];
|
|
403
|
+
// issue/process remain nouns unless a leading verb has another noun to act on.
|
|
404
|
+
const leadingVerb = first !== undefined &&
|
|
405
|
+
Object.hasOwn(verbs, first) &&
|
|
406
|
+
(!["issue", "process", "refund", "charge"].includes(first) ||
|
|
407
|
+
(!lookup && terms.slice(1).some((term) => !readActions.has(term) && !/^\d+$/.test(term))));
|
|
408
|
+
// An explicit leading write verb wins; a later read word (list, status) is its object.
|
|
409
|
+
if (first !== undefined && terms.length > 1 && writeVerbs.has(first)) {
|
|
410
|
+
// In renaming/setting, the destination after "to" is a supplied value.
|
|
411
|
+
const object = ["rename", "change", "set", "make"].includes(first)
|
|
412
|
+
? queryWords(query.split(/\bto\b/i)[0]).filter((term) => !functionWords.has(term))
|
|
413
|
+
: terms;
|
|
414
|
+
const nouns = object.slice(1).filter(isNoun);
|
|
415
|
+
return {
|
|
416
|
+
terms,
|
|
417
|
+
resourcePhrase,
|
|
418
|
+
nouns,
|
|
419
|
+
suppliedValue: object.length > 1 && !nouns.length,
|
|
420
|
+
verbWords: (first === "make" && /\bmake\s+(?:an?|new)\b/i.test(query)) ||
|
|
421
|
+
(first === "set" && /\bset\s+up\b/i.test(query))
|
|
422
|
+
? verbs.create
|
|
423
|
+
: verbs[first],
|
|
424
|
+
intent: "write",
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
const verb = lookup ?? (leadingVerb ? first : undefined);
|
|
428
|
+
return {
|
|
429
|
+
terms,
|
|
430
|
+
resourcePhrase,
|
|
431
|
+
suppliedValue: false,
|
|
432
|
+
nouns: terms.filter((term, position) => isNoun(term) &&
|
|
433
|
+
!readActions.has(term) &&
|
|
434
|
+
!(position === 0 && leadingVerb && !["refund", "charge"].includes(term))),
|
|
435
|
+
verbWords: verb === undefined ? undefined : verbs[verb],
|
|
436
|
+
intent: (lookup ? "read" : leadingVerb && writeActions.has(first) ? "write" : undefined),
|
|
437
|
+
};
|
|
438
|
+
};
|
|
439
|
+
// Compare inflections symmetrically: campus/campuses must not stem campus to campu.
|
|
440
|
+
const plural = (word) => (word.endsWith("y") ? `${word.slice(0, -1)}ies` : `${word}es`);
|
|
441
|
+
const sameNoun = (word, term) => word === term ||
|
|
442
|
+
word === `${term}s` ||
|
|
443
|
+
term === `${word}s` ||
|
|
444
|
+
word === plural(term) ||
|
|
445
|
+
term === plural(word) ||
|
|
446
|
+
(word === "people" && term === "person") ||
|
|
447
|
+
(word === "person" && term === "people") ||
|
|
448
|
+
(word === "mail" && term === "email") ||
|
|
449
|
+
(word === "email" && term === "mail");
|
|
450
|
+
const matches = (word, term) => sameNoun(word, term) || (term.length >= 3 && word.startsWith(term));
|
|
451
|
+
// Intent and the requested verb precede resource and noun-phrase ties.
|
|
452
|
+
// Stable lexical order remains the final key; every match stays reachable.
|
|
453
|
+
const rank = (found, query, pathQuery) => {
|
|
454
|
+
if (pathQuery)
|
|
455
|
+
return [...found];
|
|
456
|
+
const { nouns, intent, verbWords } = query;
|
|
457
|
+
const headNouns = query.resourcePhrase.length ? query.resourcePhrase : nouns;
|
|
458
|
+
const phrase = intent === undefined && nouns.length > 1;
|
|
459
|
+
// Modifiers may precede the resource in action/read queries. Noun phrases
|
|
460
|
+
// retain their broader resource match without the half-coverage threshold.
|
|
461
|
+
const half = Math.ceil(nouns.length / 2);
|
|
462
|
+
const features = found
|
|
463
|
+
.map((op, position) => {
|
|
464
|
+
const parts = op.path.toLowerCase().split("/").filter(Boolean);
|
|
465
|
+
const literal = parts.filter((part) => !part.startsWith("{")).flatMap(words);
|
|
466
|
+
const parameter = parts.findIndex((part) => part.startsWith("{"));
|
|
467
|
+
const root = parameter < 0 ? parts : parts.slice(0, parameter);
|
|
468
|
+
const description = words(`${op.summary} ${op.operationId}`);
|
|
469
|
+
const verbal = [...description, ...words(op.path)];
|
|
470
|
+
const text = [...verbal, ...words(op.tag)];
|
|
471
|
+
const resources = parts.filter((part) => !part.startsWith("{") && !/^v\d+$/.test(part)).flatMap(words);
|
|
472
|
+
const authored = named.get(`${op.method} ${op.path}`);
|
|
473
|
+
const summary = phrase ? queryWords(op.summary) : [];
|
|
474
|
+
return {
|
|
475
|
+
op,
|
|
476
|
+
position,
|
|
477
|
+
parts,
|
|
478
|
+
literal,
|
|
479
|
+
root,
|
|
480
|
+
parameter,
|
|
481
|
+
text,
|
|
482
|
+
resources,
|
|
483
|
+
terminal: parts
|
|
484
|
+
.filter((part) => !part.startsWith("{") && !/^v\d+$/.test(part))
|
|
485
|
+
.slice(-1)
|
|
486
|
+
.some((part) => words(part).some((word) => sameNoun(word, headNouns.at(-1) ?? "") || sameNoun(word, headNouns.join("")))),
|
|
487
|
+
readDescription: readActions.has(description[0] ?? ""),
|
|
488
|
+
authoredCoverage: nouns.filter((term) => description.slice(1).some((word) => !readActions.has(word) && matches(word, term))).length,
|
|
489
|
+
authored: Number(authored !== undefined && !authored.startsWith(`${name}_api_`)),
|
|
490
|
+
phrasePrefix: Number(nouns.every((term, position) => summary[position] === term)),
|
|
491
|
+
identity: [...literal, ...words(op.operationId)],
|
|
492
|
+
verbMatch: verbWords !== undefined && verbal.some((word) => verbWords.includes(word)),
|
|
493
|
+
descriptive: nouns.filter((term) => description.some((word) => matches(word, term))).length,
|
|
494
|
+
covered: nouns.filter((term) => text.some((word) => matches(word, term))).length,
|
|
495
|
+
};
|
|
496
|
+
})
|
|
497
|
+
.filter(({ identity, covered }) => {
|
|
498
|
+
return (nouns.length === 0 ||
|
|
499
|
+
covered === nouns.length ||
|
|
500
|
+
((intent === undefined || covered >= half) &&
|
|
501
|
+
nouns.some((term) => identity.some((word) => matches(word, term)))));
|
|
502
|
+
});
|
|
503
|
+
const terminalExists = features.some((feature) => feature.terminal);
|
|
504
|
+
const focus = nouns.find((term) => features.some(({ literal }) => literal.some((word) => sameNoun(word, term))));
|
|
505
|
+
const resourceNouns = nouns.filter((term) => features.some(({ literal }) => literal.some((word) => sameNoun(word, term))));
|
|
506
|
+
const resourceOnly = nouns.length === 1 || (query.terms[0] === "list" && resourceNouns.length === 1);
|
|
507
|
+
const admittedCoverage = Math.max(0, ...features
|
|
508
|
+
.filter(({ op, verbMatch }) => verbMatch &&
|
|
509
|
+
(intent === "read"
|
|
510
|
+
? discoveryVerdictOf(op) === "read"
|
|
511
|
+
: ["write", "destructive"].includes(discoveryVerdictOf(op))))
|
|
512
|
+
.map(({ covered }) => covered));
|
|
513
|
+
const keyed = features.map(({ op, position, parts, literal, root, parameter, verbMatch, covered, descriptive, resources, terminal, readDescription, authoredCoverage, authored, phrasePrefix, }) => ({
|
|
514
|
+
op,
|
|
515
|
+
position,
|
|
516
|
+
intent: Number(intent === undefined ||
|
|
517
|
+
(intent === "read"
|
|
518
|
+
? discoveryVerdictOf(op) === "read"
|
|
519
|
+
: ["write", "destructive"].includes(discoveryVerdictOf(op))) ||
|
|
520
|
+
(discoveryVerdictOf(op) === "refuse" && verbMatch && covered > admittedCoverage)),
|
|
521
|
+
verbMatch: Number(verbMatch),
|
|
522
|
+
covered,
|
|
523
|
+
phrasePrefix,
|
|
524
|
+
completeAuthored: Number(authored && descriptive === nouns.length),
|
|
525
|
+
phraseCoverage: nouns.filter((term) => resources.some((word) => sameNoun(word, term))).length,
|
|
526
|
+
resourceHead: Number(terminal || (terminalExists && authored && authoredCoverage === nouns.length && readDescription)),
|
|
527
|
+
readable: Number(discoveryVerdictOf(op) === "read"),
|
|
528
|
+
focused: Number((intent !== undefined || resourceOnly) &&
|
|
529
|
+
focus !== undefined &&
|
|
530
|
+
literal.some((word) => sameNoun(word, focus))),
|
|
531
|
+
exact: Number(focus !== undefined && sameNoun((root.at(-1) ?? "").replace(/-list(?:-all)?$/, ""), focus)),
|
|
532
|
+
nested: parameter < 0 ? 0 : parts.slice(parameter).filter((part) => !part.startsWith("{")).length,
|
|
533
|
+
namespace: Number(focus !== undefined && sameNoun((root[0] ?? "").replace(/-list(?:-all)?$/, ""), focus)),
|
|
534
|
+
depth: parts.length,
|
|
535
|
+
authored,
|
|
536
|
+
}));
|
|
537
|
+
return keyed
|
|
538
|
+
.sort((a, b) => b.intent - a.intent ||
|
|
539
|
+
b.verbMatch - a.verbMatch ||
|
|
540
|
+
b.focused - a.focused ||
|
|
541
|
+
(phrase
|
|
542
|
+
? b.covered - a.covered ||
|
|
543
|
+
b.phrasePrefix - a.phrasePrefix ||
|
|
544
|
+
b.resourceHead - a.resourceHead ||
|
|
545
|
+
(a.resourceHead && b.resourceHead ? b.readable - a.readable : 0) ||
|
|
546
|
+
a.depth - a.authored - (b.depth - b.authored) ||
|
|
547
|
+
b.phraseCoverage - a.phraseCoverage ||
|
|
548
|
+
b.authored - a.authored
|
|
549
|
+
: 0) ||
|
|
550
|
+
(query.terms[0] === "list" && !resourceOnly ? b.completeAuthored - a.completeAuthored : 0) ||
|
|
551
|
+
(resourceOnly
|
|
552
|
+
? b.exact - a.exact ||
|
|
553
|
+
a.nested - b.nested ||
|
|
554
|
+
b.namespace - a.namespace ||
|
|
555
|
+
a.depth - b.depth ||
|
|
556
|
+
b.authored - a.authored
|
|
557
|
+
: 0) ||
|
|
558
|
+
a.position - b.position)
|
|
559
|
+
.map(({ op }) => op);
|
|
560
|
+
};
|
|
561
|
+
// Find operations by the request or response properties they declare: a
|
|
562
|
+
// query word must equal a word of a property's name. Rarer words weigh more,
|
|
563
|
+
// and words three in four operations carry (id, status) decide nothing.
|
|
564
|
+
// Reads, then the shallowest best match, lead.
|
|
565
|
+
const words = (text) => text
|
|
566
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
567
|
+
.toLowerCase()
|
|
568
|
+
.split(/[^a-z0-9]+/)
|
|
569
|
+
.filter((word) => word.length >= 2);
|
|
570
|
+
const same = (term, word) => term === word || term === `${word}s` || word === `${term}s`;
|
|
571
|
+
const byField = (query, method, found) => {
|
|
572
|
+
const candidates = index.ops.filter((op) => method === undefined || op.method === method);
|
|
573
|
+
const leaf = (field) => words(field.path.split(".").at(-1));
|
|
574
|
+
// Eligibility and rarity count every declared path, nested ones included; depth only lowers rank.
|
|
575
|
+
const carries = (op, term) => index.fields(op).some((field) => leaf(field).some((word) => same(term, word)));
|
|
576
|
+
const weights = new Map([...new Set(words(query))].flatMap((term) => {
|
|
577
|
+
const share = candidates.filter((op) => carries(op, term)).length;
|
|
578
|
+
return share > 0 && share <= Math.max(1, (candidates.length * 3) / 4)
|
|
579
|
+
? [[term, Math.log2(1 + candidates.length / share)]]
|
|
580
|
+
: [];
|
|
581
|
+
}));
|
|
582
|
+
if (weights.size === 0)
|
|
583
|
+
return [];
|
|
584
|
+
const order = new Map(index.ops.map((op, position) => [op, position]));
|
|
585
|
+
return candidates
|
|
586
|
+
.filter((op) => !found.has(op))
|
|
587
|
+
.flatMap((op) => {
|
|
588
|
+
const best = index
|
|
589
|
+
.fields(op)
|
|
590
|
+
.map((field) => ({
|
|
591
|
+
field,
|
|
592
|
+
score: [...weights].reduce((sum, [term, weight]) => sum + (leaf(field).some((word) => same(term, word)) ? weight : 0), 0),
|
|
593
|
+
depth: field.path.split(".").length + (field.in === "body" ? 0.5 : 0),
|
|
594
|
+
}))
|
|
595
|
+
.filter(({ score }) => score > 0)
|
|
596
|
+
.sort((a, b) => b.score - a.score || a.depth - b.depth)[0];
|
|
597
|
+
return best ? [{ op, ...best }] : [];
|
|
598
|
+
})
|
|
599
|
+
.sort((a, b) => b.score - a.score ||
|
|
600
|
+
Number(b.op.method === "GET") - Number(a.op.method === "GET") ||
|
|
601
|
+
a.depth - b.depth ||
|
|
602
|
+
a.op.path.split("/").length - b.op.path.split("/").length ||
|
|
603
|
+
order.get(a.op) - order.get(b.op))
|
|
604
|
+
.map(({ op, field }) => ({ op, field }));
|
|
605
|
+
};
|
|
606
|
+
// Nouns an operation's text or declared field names cover.
|
|
607
|
+
const coverage = (op, nouns) => {
|
|
608
|
+
const own = [
|
|
609
|
+
...words(`${op.summary} ${op.operationId} ${op.path} ${op.tag}`),
|
|
610
|
+
...index.fields(op).flatMap((field) => words(field.path.split(".").at(-1))),
|
|
611
|
+
];
|
|
612
|
+
return nouns.filter((term) => own.some((word) => matches(word, term))).length;
|
|
613
|
+
};
|
|
614
|
+
const methodsWith = (wanted) => index.methods.filter((method) => index.ops.some((op) => op.method === method && wanted(verdictOf(op))));
|
|
615
|
+
const readMethods = methodsWith((verdict) => verdict === "read");
|
|
616
|
+
const defaultRead = readMethods.includes("GET") || readMethods.length !== 1 ? "GET" : readMethods[0];
|
|
617
|
+
const writeMethods = methodsWith((verdict) => verdict === "write" || verdict === "destructive");
|
|
618
|
+
if (idempotency &&
|
|
619
|
+
(!idempotency.methods.length || idempotency.methods.some((method) => !writeMethods.includes(method))))
|
|
620
|
+
throw new Error("Hatch idempotency methods must name at least one admitted write method.");
|
|
621
|
+
const Method = methodEnum(index.methods);
|
|
622
|
+
const Path = text(2, 2048).annotate({
|
|
623
|
+
description: `Concrete path from search with ids filled in${options.pathExample ? `, e.g. ${options.pathExample}` : ""}. No query string.`,
|
|
624
|
+
});
|
|
625
|
+
const Query = ObjectValue.annotate({ description: "Query parameters as JSON, named as details lists them." });
|
|
626
|
+
const Routing = {
|
|
627
|
+
tool: Schema.optionalKey(Schema.String.annotate({ description: "The tool that calls it; absent when refused." })),
|
|
628
|
+
named: Schema.optionalKey(Schema.String.annotate({
|
|
629
|
+
description: "A named tool serving this operation with a projected result; prefer it.",
|
|
630
|
+
})),
|
|
631
|
+
};
|
|
632
|
+
const Matched = Schema.optionalKey(Schema.String.annotate({
|
|
633
|
+
description: "The request or response field this operation was found by; call its tool to read the field.",
|
|
634
|
+
}));
|
|
635
|
+
const RefusalReason = Schema.optionalKey(Schema.String.annotate({
|
|
636
|
+
description: "Reviewed refusal reason, separate from the pinned summary. Cut text is marked in truncated; details serves the complete text, paged if needed.",
|
|
637
|
+
}));
|
|
638
|
+
const searchTool = tool("search");
|
|
639
|
+
const detailsTool = tool("details");
|
|
640
|
+
const tools = [
|
|
641
|
+
read(connection, {
|
|
642
|
+
name: searchTool,
|
|
643
|
+
description: `Find ${title} API operations by keyword or path. Returns up to ${SEARCH_BUDGET.maxHits} compact matches with routing and separate refusal reasons; reads no account data.`,
|
|
644
|
+
input: Schema.Struct({
|
|
645
|
+
query: text(1, 200).annotate({ description: "Keywords or a path, e.g. refund charge." }),
|
|
646
|
+
method: Schema.optionalKey(Method.annotate({ description: "Only operations with this method." })),
|
|
647
|
+
limit: Schema.optionalKey(Integer.check(Schema.isBetween({ minimum: 1, maximum: SEARCH_BUDGET.maxHits })).annotate({
|
|
648
|
+
description: "Most operations to return.",
|
|
649
|
+
default: SEARCH_BUDGET.hits,
|
|
650
|
+
})),
|
|
651
|
+
cursor: Schema.optionalKey(text(1, 512).annotate({ description: "page.nextCursor from the previous search with the same query." })),
|
|
652
|
+
}),
|
|
653
|
+
output: Schema.Struct({
|
|
654
|
+
operations: Schema.Array(Schema.Struct({
|
|
655
|
+
method: Method,
|
|
656
|
+
path: Schema.String,
|
|
657
|
+
operationId: Schema.String,
|
|
658
|
+
summary: Schema.String,
|
|
659
|
+
refusal: Schema.optionalKey(text(1, SEARCH_BUDGET.refusalChars).annotate({
|
|
660
|
+
description: "Reviewed refusal, capped separately from summary. A cut reason is marked in truncated; details returns it whole.",
|
|
661
|
+
})),
|
|
662
|
+
...Routing,
|
|
663
|
+
matched: Matched,
|
|
664
|
+
})),
|
|
665
|
+
revision: Schema.String.annotate({ description: "Operation index revision." }),
|
|
666
|
+
truncated: Schema.optionalKey(Schema.Array(Schema.String).annotate({
|
|
667
|
+
description: `Entry labels cut for size; ${detailsTool} on that method and path returns them whole.`,
|
|
668
|
+
})),
|
|
669
|
+
page: StandardPage,
|
|
670
|
+
}),
|
|
671
|
+
run: (args) => Effect.gen(function* () {
|
|
672
|
+
const scope = {
|
|
673
|
+
discoveryFormat: "summary-refusal-1",
|
|
674
|
+
...(options.binding?.cursorScope === undefined ? {} : { deploymentPolicy: options.binding.cursorScope }),
|
|
675
|
+
revision: yield* Effect.promise(() => index.cursorRevision()),
|
|
676
|
+
query: args.query,
|
|
677
|
+
...(args.method ? { method: args.method } : {}),
|
|
678
|
+
};
|
|
679
|
+
const continuation = args.cursor === undefined ? undefined : yield* boundCursor.decode(searchTool, scope, args.cursor);
|
|
680
|
+
const offset = continuation === undefined ? 0 : Number(continuation);
|
|
681
|
+
if (!Number.isSafeInteger(offset) || (continuation !== undefined && !/^[1-9][0-9]*$/.test(continuation)))
|
|
682
|
+
return yield* Effect.fail(new Refusal({
|
|
683
|
+
error: new ConnectorCallError("invalid_args", "This search cursor is not one search returned."),
|
|
684
|
+
}));
|
|
685
|
+
const pathQuery = args.query.trim().startsWith("/");
|
|
686
|
+
const query = queryTerms(args.query);
|
|
687
|
+
const primary = rank(yield* checked(() => index.search(pathQuery
|
|
688
|
+
? args.query
|
|
689
|
+
: [...query.terms, ...(query.suppliedValue ? (query.verbWords ?? []) : [])].join(" "), {
|
|
690
|
+
...scope,
|
|
691
|
+
limit: index.ops.length,
|
|
692
|
+
partial: !pathQuery && query.terms.length > 0,
|
|
693
|
+
})), query, pathQuery);
|
|
694
|
+
// Operations found only by a field they return or take rank after every path, id or summary match.
|
|
695
|
+
const ranked = [
|
|
696
|
+
...primary.map((op) => ({ op })),
|
|
697
|
+
...byField(pathQuery ? args.query : (query.nouns.length ? query.nouns : query.terms).join(" "), args.method, new Set(primary)).filter(({ op }) => pathQuery ||
|
|
698
|
+
query.intent === undefined ||
|
|
699
|
+
coverage(op, query.nouns) >= Math.ceil(query.nouns.length / 2)),
|
|
700
|
+
];
|
|
701
|
+
const reachable = ranked.filter(({ op }) => [...op.path].length <= 2048).length;
|
|
702
|
+
if (continuation !== undefined && offset >= reachable)
|
|
703
|
+
return yield* Effect.fail(new Refusal({
|
|
704
|
+
error: new ConnectorCallError("invalid_args", "This search cursor is not one search returned."),
|
|
705
|
+
}));
|
|
706
|
+
// Search entries are compact: long labels are cut (details serves them whole), and a
|
|
707
|
+
// path the read and write tools cannot take (over 2,048 characters) is never routed.
|
|
708
|
+
const truncated = [];
|
|
709
|
+
const operations = ranked
|
|
710
|
+
.filter(({ op }) => [...op.path].length <= 2048)
|
|
711
|
+
.slice(offset, offset + (args.limit ?? SEARCH_BUDGET.hits))
|
|
712
|
+
.map(({ op, field }, position) => {
|
|
713
|
+
const { named: authored, ...routed } = route(op);
|
|
714
|
+
const refusal = discoveryRefusal(op);
|
|
715
|
+
const label = (key, text, max = SEARCH_BUDGET.summaryChars) => {
|
|
716
|
+
if (text.length > max)
|
|
717
|
+
truncated.push(`operations[${position}].${key}`);
|
|
718
|
+
return cap(text, max);
|
|
719
|
+
};
|
|
720
|
+
return {
|
|
721
|
+
method: op.method,
|
|
722
|
+
path: op.path,
|
|
723
|
+
operationId: label("operationId", op.operationId),
|
|
724
|
+
summary: label("summary", op.summary),
|
|
725
|
+
...(refusal ? { refusal: label("refusal", refusal, SEARCH_BUDGET.refusalChars) } : {}),
|
|
726
|
+
...routed,
|
|
727
|
+
// A named tool's projection may omit the matched field, so a field match points at the hatch only.
|
|
728
|
+
...(field
|
|
729
|
+
? { matched: label("matched", `${field.in} field ${field.path}`) }
|
|
730
|
+
: authored
|
|
731
|
+
? { named: authored }
|
|
732
|
+
: {}),
|
|
733
|
+
};
|
|
734
|
+
});
|
|
735
|
+
// The budget holds for the whole answer, page and cursor included, before it leaves the tool;
|
|
736
|
+
// what it cuts stays reachable by cursor.
|
|
737
|
+
const answer = function* () {
|
|
738
|
+
const next = offset + operations.length;
|
|
739
|
+
const shown = truncated.filter((path) => Number(/\[(\d+)\]/.exec(path)[1]) < operations.length);
|
|
740
|
+
return {
|
|
741
|
+
operations,
|
|
742
|
+
revision: index.revision,
|
|
743
|
+
...(shown.length ? { truncated: shown } : {}),
|
|
744
|
+
page: next < reachable
|
|
745
|
+
? { hasMore: true, nextCursor: yield* boundCursor.encode(searchTool, scope, String(next)) }
|
|
746
|
+
: { hasMore: false },
|
|
747
|
+
};
|
|
748
|
+
};
|
|
749
|
+
let result = yield* answer();
|
|
750
|
+
while (operations.length > 1 && bytes(result) > SEARCH_BUDGET.bytes) {
|
|
751
|
+
operations.pop();
|
|
752
|
+
result = yield* answer();
|
|
753
|
+
}
|
|
754
|
+
return result;
|
|
755
|
+
}),
|
|
756
|
+
}),
|
|
757
|
+
read(connection, {
|
|
758
|
+
name: detailsTool,
|
|
759
|
+
description: `Read one ${title} API operation's request contract and refusal, including methods outside the call schemas. Reads no account data.`,
|
|
760
|
+
input: Schema.Struct({
|
|
761
|
+
method: Method.annotate({ description: "The operation's HTTP method." }),
|
|
762
|
+
path: Path.annotate({ description: "The path template from search, or a concrete path." }),
|
|
763
|
+
param: Schema.optionalKey(text(1, 200).annotate({ description: "Return only this parameter, in full." })),
|
|
764
|
+
cursor: Schema.optionalKey(text(1, 1024).annotate({
|
|
765
|
+
description: "page.nextCursor from the previous details call with the same method, path and param.",
|
|
766
|
+
})),
|
|
767
|
+
}),
|
|
768
|
+
output: Schema.Struct({
|
|
769
|
+
method: Method,
|
|
770
|
+
path: Schema.String,
|
|
771
|
+
operationId: Schema.String,
|
|
772
|
+
summary: Schema.String,
|
|
773
|
+
refusal: RefusalReason,
|
|
774
|
+
server: Schema.optionalKey(Schema.String),
|
|
775
|
+
parameters: Schema.Array(Schema.Unknown).annotate({ description: "Path and query parameters." }),
|
|
776
|
+
body: Schema.optionalKey(ObjectValue.annotate({
|
|
777
|
+
description: "Body content type and schema. When paged: this page's properties, the required names it delivers or chunks, and the schema's other keywords on the page that carries them.",
|
|
778
|
+
})),
|
|
779
|
+
verdict: Schema.Literals(["read", "write", "destructive", "refuse"]),
|
|
780
|
+
...Routing,
|
|
781
|
+
revision: Schema.String,
|
|
782
|
+
chunk: Schema.optionalKey(Schema.Struct({
|
|
783
|
+
of: Schema.String.annotate({ description: "The piece this text belongs to, such as parameters.limit." }),
|
|
784
|
+
text: Schema.String.annotate({
|
|
785
|
+
description: "Part of that piece's JSON; join the parts in page order, then parse them.",
|
|
786
|
+
}),
|
|
787
|
+
})),
|
|
788
|
+
truncated: Schema.optionalKey(Schema.Array(Schema.String).annotate({
|
|
789
|
+
description: "Labels cut for size; their full text arrives as chunks.",
|
|
790
|
+
})),
|
|
791
|
+
page: Schema.optionalKey(StandardPage.annotate({
|
|
792
|
+
description: "Present when the contract exceeds one answer: pass nextCursor back for the next pieces.",
|
|
793
|
+
})),
|
|
794
|
+
}),
|
|
795
|
+
run: (args) => Effect.gen(function* () {
|
|
796
|
+
const { op, contract } = yield* checked(() => {
|
|
797
|
+
const op = index.operation(args.method, args.path) ?? index.resolve(args.method, args.path).op;
|
|
798
|
+
const refusal = discoveryRefusal(op);
|
|
799
|
+
return { op, contract: { ...index.contract(op, args.param), ...(refusal ? { refusal } : {}) } };
|
|
800
|
+
});
|
|
801
|
+
const tail = { verdict: discoveryVerdictOf(op), ...route(op), revision: index.revision };
|
|
802
|
+
const whole = { ...contract, ...tail };
|
|
803
|
+
if (args.cursor === undefined && bytes(whole) <= DETAILS_BUDGET_BYTES)
|
|
804
|
+
return whole;
|
|
805
|
+
// Bounded pages: every parameter and body property arrives whole on some page, and a
|
|
806
|
+
// piece too large for one page arrives as consecutive chunks of its JSON. Nothing is dropped.
|
|
807
|
+
const scope = {
|
|
808
|
+
discoveryFormat: "summary-refusal-1",
|
|
809
|
+
...(options.binding?.cursorScope === undefined ? {} : { deploymentPolicy: options.binding.cursorScope }),
|
|
810
|
+
revision: yield* Effect.promise(() => index.cursorRevision()),
|
|
811
|
+
method: args.method,
|
|
812
|
+
path: args.path,
|
|
813
|
+
...(args.param ? { param: args.param } : {}),
|
|
814
|
+
};
|
|
815
|
+
let at = { i: 0, o: 0 };
|
|
816
|
+
if (args.cursor !== undefined) {
|
|
817
|
+
const decoded = yield* boundCursor.decode(detailsTool, scope, args.cursor);
|
|
818
|
+
const parsed = yield* Effect.try({
|
|
819
|
+
try: () => JSON.parse(decoded),
|
|
820
|
+
catch: () => undefined,
|
|
821
|
+
}).pipe(Effect.orElseSucceed(() => undefined));
|
|
822
|
+
at = parsed;
|
|
823
|
+
}
|
|
824
|
+
const capped = {
|
|
825
|
+
operationId: contract.operationId.length > SEARCH_BUDGET.summaryChars,
|
|
826
|
+
summary: contract.summary.length > SEARCH_BUDGET.summaryChars,
|
|
827
|
+
refusal: (contract.refusal?.length ?? 0) > SEARCH_BUDGET.refusalChars,
|
|
828
|
+
};
|
|
829
|
+
const pieces = piecesOf(contract, capped);
|
|
830
|
+
if (!Number.isSafeInteger(at?.i) ||
|
|
831
|
+
!Number.isSafeInteger(at?.o) ||
|
|
832
|
+
at.i < 0 ||
|
|
833
|
+
at.o < 0 ||
|
|
834
|
+
at.i >= pieces.length ||
|
|
835
|
+
(args.cursor !== undefined && at.i === 0 && at.o === 0) ||
|
|
836
|
+
(at.o > 0 &&
|
|
837
|
+
(() => {
|
|
838
|
+
const json = JSON.stringify(pieces[at.i].value);
|
|
839
|
+
return at.o >= json.length || /[\uD800-\uDBFF]/.test(json[at.o - 1]);
|
|
840
|
+
})()))
|
|
841
|
+
return yield* Effect.fail(new Refusal({
|
|
842
|
+
error: new ConnectorCallError("invalid_args", "This details cursor is not one details returned."),
|
|
843
|
+
}));
|
|
844
|
+
const base = {
|
|
845
|
+
method: contract.method,
|
|
846
|
+
path: contract.path,
|
|
847
|
+
operationId: cap(contract.operationId, SEARCH_BUDGET.summaryChars),
|
|
848
|
+
summary: cap(contract.summary, SEARCH_BUDGET.summaryChars),
|
|
849
|
+
...(contract.refusal ? { refusal: cap(contract.refusal, SEARCH_BUDGET.refusalChars) } : {}),
|
|
850
|
+
...(contract.server ? { server: contract.server } : {}),
|
|
851
|
+
};
|
|
852
|
+
const truncated = ["operationId", "summary", "refusal"].filter((key) => capped[key]);
|
|
853
|
+
const parameters = [];
|
|
854
|
+
const properties = {};
|
|
855
|
+
const required = [];
|
|
856
|
+
let frame;
|
|
857
|
+
let body;
|
|
858
|
+
let chunk;
|
|
859
|
+
// A paged object body carries the frame keywords on the page that delivers them,
|
|
860
|
+
// this page's properties, and the required names of the properties it delivers or chunks.
|
|
861
|
+
const compose = (page) => ({
|
|
862
|
+
...base,
|
|
863
|
+
parameters,
|
|
864
|
+
...(contract.body === undefined
|
|
865
|
+
? {}
|
|
866
|
+
: {
|
|
867
|
+
body: {
|
|
868
|
+
contentType: contract.body.contentType,
|
|
869
|
+
...(contract.body.required ? { required: true } : {}),
|
|
870
|
+
schema: body ?? {
|
|
871
|
+
...frame,
|
|
872
|
+
properties,
|
|
873
|
+
...(required.length ? { required } : {}),
|
|
874
|
+
},
|
|
875
|
+
},
|
|
876
|
+
}),
|
|
877
|
+
...tail,
|
|
878
|
+
...(chunk ? { chunk } : {}),
|
|
879
|
+
...(truncated.length ? { truncated } : {}),
|
|
880
|
+
page,
|
|
881
|
+
});
|
|
882
|
+
// Room for the largest cursor this contract can need.
|
|
883
|
+
const reserve = {
|
|
884
|
+
hasMore: true,
|
|
885
|
+
nextCursor: yield* boundCursor.encode(detailsTool, scope, `{"i":${pieces.length},"o":${Number.MAX_SAFE_INTEGER}}`),
|
|
886
|
+
};
|
|
887
|
+
const fits = () => bytes(compose(reserve)) <= DETAILS_BUDGET_BYTES;
|
|
888
|
+
let { i, o } = at;
|
|
889
|
+
while (i < pieces.length) {
|
|
890
|
+
const piece = pieces[i];
|
|
891
|
+
if (o === 0 && piece.kind !== "text") {
|
|
892
|
+
if (piece.kind === "parameter")
|
|
893
|
+
parameters.push(piece.value);
|
|
894
|
+
else if (piece.kind === "body")
|
|
895
|
+
body = piece.value;
|
|
896
|
+
else if (piece.kind === "frame")
|
|
897
|
+
frame = piece.value;
|
|
898
|
+
else {
|
|
899
|
+
properties[piece.name] = piece.value;
|
|
900
|
+
if (piece.required)
|
|
901
|
+
required.push(piece.name);
|
|
902
|
+
}
|
|
903
|
+
if (fits()) {
|
|
904
|
+
i += 1;
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
if (piece.kind === "parameter")
|
|
908
|
+
parameters.pop();
|
|
909
|
+
else if (piece.kind === "body")
|
|
910
|
+
body = undefined;
|
|
911
|
+
else if (piece.kind === "frame")
|
|
912
|
+
frame = undefined;
|
|
913
|
+
else {
|
|
914
|
+
delete properties[piece.name];
|
|
915
|
+
if (piece.required)
|
|
916
|
+
required.pop();
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
// A piece that cannot share a page arrives alone, in as many chunks as it needs; a
|
|
920
|
+
// chunked required property keeps its name in this page's required list.
|
|
921
|
+
if (parameters.length === 0 &&
|
|
922
|
+
Object.keys(properties).length === 0 &&
|
|
923
|
+
body === undefined &&
|
|
924
|
+
frame === undefined) {
|
|
925
|
+
const json = JSON.stringify(piece.value);
|
|
926
|
+
if (piece.kind === "property" && piece.required)
|
|
927
|
+
required.push(piece.name);
|
|
928
|
+
chunk = { of: piece.of, text: "" };
|
|
929
|
+
let low = o + 1;
|
|
930
|
+
let high = json.length;
|
|
931
|
+
let end = Math.min(o + 1, json.length);
|
|
932
|
+
while (low <= high) {
|
|
933
|
+
const middle = Math.floor((low + high) / 2);
|
|
934
|
+
chunk.text = json.slice(o, middle);
|
|
935
|
+
if (fits()) {
|
|
936
|
+
end = middle;
|
|
937
|
+
low = middle + 1;
|
|
938
|
+
}
|
|
939
|
+
else
|
|
940
|
+
high = middle - 1;
|
|
941
|
+
}
|
|
942
|
+
// Never split a surrogate pair across chunks.
|
|
943
|
+
if (end < json.length && end > o + 1 && /[\uD800-\uDBFF]/.test(json[end - 1]))
|
|
944
|
+
end -= 1;
|
|
945
|
+
chunk.text = json.slice(o, end);
|
|
946
|
+
o = end;
|
|
947
|
+
if (o >= json.length) {
|
|
948
|
+
i += 1;
|
|
949
|
+
o = 0;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
break;
|
|
953
|
+
}
|
|
954
|
+
return compose(i < pieces.length
|
|
955
|
+
? { hasMore: true, nextCursor: yield* boundCursor.encode(detailsTool, scope, JSON.stringify({ i, o })) }
|
|
956
|
+
: { hasMore: false });
|
|
957
|
+
}),
|
|
958
|
+
}),
|
|
959
|
+
];
|
|
960
|
+
const run = (kind, args) => Effect.gen(function* () {
|
|
961
|
+
yield* checked(() => {
|
|
962
|
+
refuseWithheldValues({ path: args.path, query: args.query }, name, true);
|
|
963
|
+
refuseWithheldValues(args.body, name);
|
|
964
|
+
});
|
|
965
|
+
const method = args.method ?? defaultRead;
|
|
966
|
+
const path = yield* checked(() => options.binding?.path?.(args.path) ?? args.path);
|
|
967
|
+
yield* checked(() => refuseWithheldValues({ path }, name, true));
|
|
968
|
+
const { op, params } = yield* checked(() => index.resolve(method, path));
|
|
969
|
+
const call = () => ({
|
|
970
|
+
method,
|
|
971
|
+
path,
|
|
972
|
+
op,
|
|
973
|
+
params,
|
|
974
|
+
query,
|
|
975
|
+
body,
|
|
976
|
+
...(args.headers ? { headers: args.headers } : {}),
|
|
977
|
+
});
|
|
978
|
+
let query = args.query ?? {};
|
|
979
|
+
let body = yield* checked(() => options.binding?.prepareBody ? options.binding.prepareBody(args.body, op) : args.body);
|
|
980
|
+
if (options.binding?.query)
|
|
981
|
+
query = yield* checked(() => options.binding.query(call()));
|
|
982
|
+
yield* checked(() => {
|
|
983
|
+
const verdict = verdictOf(op);
|
|
984
|
+
if (verdict === "refuse") {
|
|
985
|
+
invalid(refusalOf(op) ??
|
|
986
|
+
options.binding?.refuse?.(call()) ??
|
|
987
|
+
`Connecta refuses ${method} ${op.path}: it has no admitted hatch verdict.`);
|
|
988
|
+
}
|
|
989
|
+
if ((verdict === "read") !== (kind === "read")) {
|
|
990
|
+
invalid(`${method} ${op.path} must be called with ${tool(verdict === "read" ? "read" : "write")}.`);
|
|
991
|
+
}
|
|
992
|
+
});
|
|
993
|
+
const adapters = pages.get(`${method} ${op.path}`) ?? [];
|
|
994
|
+
let adapter = adapters[0];
|
|
995
|
+
const at = (entry) => entry.in === "query" ? query[entry.param] : record(body) ? body[entry.param] : undefined;
|
|
996
|
+
const supplied = adapters.filter((entry) => at(entry) !== undefined);
|
|
997
|
+
const separateCursor = options.pageCursor === "argument";
|
|
998
|
+
yield* checked(() => {
|
|
999
|
+
if (supplied.length > 1)
|
|
1000
|
+
invalid("Choose one paging direction; do not combine cursor parameters.");
|
|
1001
|
+
if (args.cursor !== undefined && !adapter)
|
|
1002
|
+
invalid("This operation does not page; omit cursor.");
|
|
1003
|
+
if (args.cursor !== undefined && supplied.length)
|
|
1004
|
+
invalid("Pass cursor without a raw paging anchor.");
|
|
1005
|
+
if (separateCursor && supplied.some((entry) => entry.initialAnchor && signedCursorShape(at(entry))))
|
|
1006
|
+
invalid("Pass the signed paging cursor as top-level cursor, not as a raw query anchor.");
|
|
1007
|
+
});
|
|
1008
|
+
const withoutCursors = (values, location) => Object.fromEntries(Object.entries(values).filter(([key]) => !adapters.some((entry) => entry.in === location && entry.param === key)));
|
|
1009
|
+
const scope = adapter
|
|
1010
|
+
? {
|
|
1011
|
+
...(options.binding?.cursorScope === undefined ? {} : { deploymentPolicy: options.binding.cursorScope }),
|
|
1012
|
+
revision: yield* Effect.promise(() => index.cursorRevision()),
|
|
1013
|
+
method,
|
|
1014
|
+
path,
|
|
1015
|
+
query: withoutCursors(query, "query"),
|
|
1016
|
+
...(adapters.some((entry) => entry.in === "body")
|
|
1017
|
+
? { body: withoutCursors(record(body) ? body : {}, "body") }
|
|
1018
|
+
: body === undefined
|
|
1019
|
+
? {}
|
|
1020
|
+
: { body }),
|
|
1021
|
+
...(args.fields ? { fields: args.fields } : {}),
|
|
1022
|
+
...(args.headers ? { headers: args.headers } : {}),
|
|
1023
|
+
}
|
|
1024
|
+
: {};
|
|
1025
|
+
if (supplied[0])
|
|
1026
|
+
adapter = supplied[0];
|
|
1027
|
+
const value = separateCursor ? args.cursor : adapter && at(adapter);
|
|
1028
|
+
// Timestamp pagination can also carry a first-page time-window anchor.
|
|
1029
|
+
// Only an explicit adapter opt-in admits a plain numeric anchor; opaque
|
|
1030
|
+
// string continuations still require a signed token.
|
|
1031
|
+
const numericAnchor = !separateCursor &&
|
|
1032
|
+
adapter?.initialAnchor &&
|
|
1033
|
+
(typeof value === "number" || (typeof value === "string" && /^\d+$/.test(value)));
|
|
1034
|
+
if (adapter && numericAnchor) {
|
|
1035
|
+
const restored = yield* checked(() => adapter.decode(value));
|
|
1036
|
+
if (adapter.in === "query")
|
|
1037
|
+
query = { ...query, [adapter.param]: restored };
|
|
1038
|
+
else
|
|
1039
|
+
body = { ...(record(body) ? body : {}), [adapter.param]: restored };
|
|
1040
|
+
}
|
|
1041
|
+
else if (adapter && value !== undefined) {
|
|
1042
|
+
if (adapter.cursorInputMessage && !signedCursorShape(value))
|
|
1043
|
+
yield* checked(() => invalid(adapter.cursorInputMessage));
|
|
1044
|
+
if (typeof value !== "string")
|
|
1045
|
+
yield* checked(() => invalid("Pass the opaque page.nextCursor unchanged."));
|
|
1046
|
+
const cursor = yield* boundCursor.decode(tool(kind), scope, value);
|
|
1047
|
+
const restored = yield* checked(() => {
|
|
1048
|
+
let token;
|
|
1049
|
+
try {
|
|
1050
|
+
token = JSON.parse(cursor);
|
|
1051
|
+
}
|
|
1052
|
+
catch {
|
|
1053
|
+
invalid("This paging cursor contains no valid vendor value.");
|
|
1054
|
+
}
|
|
1055
|
+
if (separateCursor) {
|
|
1056
|
+
if (!record(token))
|
|
1057
|
+
invalid("This paging cursor contains no valid vendor parameter.");
|
|
1058
|
+
const payload = token;
|
|
1059
|
+
const selected = adapters.find((entry) => entry.param === payload["param"] && entry.in === payload["in"]);
|
|
1060
|
+
if (!selected)
|
|
1061
|
+
invalid("This paging cursor belongs to a different vendor parameter.");
|
|
1062
|
+
adapter = selected;
|
|
1063
|
+
token = token["value"];
|
|
1064
|
+
}
|
|
1065
|
+
if ((typeof token !== "string" && typeof token !== "number") || withheldCursor(token)) {
|
|
1066
|
+
invalid("This paging cursor contains no usable vendor value; withheld values cannot be sent.");
|
|
1067
|
+
}
|
|
1068
|
+
const decoded = adapter.decode(token);
|
|
1069
|
+
if (withheldCursor(decoded))
|
|
1070
|
+
invalid("This paging cursor contains a withheld vendor value.");
|
|
1071
|
+
return decoded;
|
|
1072
|
+
});
|
|
1073
|
+
if (adapter.in === "query")
|
|
1074
|
+
query = { ...query, [adapter.param]: restored };
|
|
1075
|
+
else
|
|
1076
|
+
body = { ...(record(body) ? body : {}), [adapter.param]: restored };
|
|
1077
|
+
}
|
|
1078
|
+
else if (separateCursor && supplied.length) {
|
|
1079
|
+
yield* checked(() => {
|
|
1080
|
+
if (!adapter?.initialAnchor)
|
|
1081
|
+
invalid(adapter?.cursorInputMessage ?? "This vendor continuation is accepted only as an opaque cursor.");
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
if (adapter?.prepareQuery)
|
|
1085
|
+
query = yield* checked(() => adapter.prepareQuery(query));
|
|
1086
|
+
const request = yield* checked(() => {
|
|
1087
|
+
refuseWithheldValues({ path, query }, name, true);
|
|
1088
|
+
refuseWithheldValues(body, name);
|
|
1089
|
+
const refusal = options.binding?.refuse?.(call());
|
|
1090
|
+
if (refusal)
|
|
1091
|
+
invalid(refusal);
|
|
1092
|
+
const contentType = index.bodyType(op)?.split(";", 1)[0].trim().toLowerCase();
|
|
1093
|
+
if (op.server && options.binding?.allowOtherServers !== true)
|
|
1094
|
+
invalid(`${method} ${op.path} uses a different server; this connection cannot send it.`);
|
|
1095
|
+
if (options.binding?.allowOtherBodyTypes !== true &&
|
|
1096
|
+
contentType &&
|
|
1097
|
+
contentType !== "application/x-www-form-urlencoded" &&
|
|
1098
|
+
!/^application\/(?:[^/]+\+)?json$/.test(contentType)) {
|
|
1099
|
+
invalid(`${method} ${op.path} takes ${contentType}; this connection's path binding supports only JSON and form bodies.`);
|
|
1100
|
+
}
|
|
1101
|
+
if ((method === "GET" || method === "HEAD") && body !== undefined)
|
|
1102
|
+
invalid(`A ${method} operation takes query, not body.`);
|
|
1103
|
+
index.check(op, query, body);
|
|
1104
|
+
if (args.fields?.length) {
|
|
1105
|
+
const roots = index.responseRoots(op);
|
|
1106
|
+
if (!roots)
|
|
1107
|
+
invalid(`${method} ${op.path} declares no response properties, so fields cannot narrow it.`);
|
|
1108
|
+
const problem = fieldsProblem(args.fields, roots, "data");
|
|
1109
|
+
if (problem) {
|
|
1110
|
+
if (options.fieldsRoot && roots.has(options.fieldsRoot))
|
|
1111
|
+
invalid(`${problem.message} Use fields:["${options.fieldsRoot}.id"] or fields:["${options.fieldsRoot}[].id"] for declared record or list fields.`);
|
|
1112
|
+
throw problem;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
if (options.binding)
|
|
1116
|
+
return undefined;
|
|
1117
|
+
return {
|
|
1118
|
+
method,
|
|
1119
|
+
path,
|
|
1120
|
+
template: op.path,
|
|
1121
|
+
query: parameters(query),
|
|
1122
|
+
...(args.headers ? { headers: args.headers } : {}),
|
|
1123
|
+
...(body === undefined
|
|
1124
|
+
? {}
|
|
1125
|
+
: contentType === "application/x-www-form-urlencoded"
|
|
1126
|
+
? { form: parameters(record(body) ? body : invalid("A form body must be an object.")) }
|
|
1127
|
+
: { json: JSON.stringify(body) }),
|
|
1128
|
+
};
|
|
1129
|
+
});
|
|
1130
|
+
const key = kind === "write" ? (yield* IdempotencyKey)?.key : undefined;
|
|
1131
|
+
const sendOptions = adapter?.headers ? { responseHeaders: adapter.headers } : undefined;
|
|
1132
|
+
const send = options.binding
|
|
1133
|
+
? options.binding.send(call(), sendOptions)
|
|
1134
|
+
: connection.sendPath(request, sendOptions);
|
|
1135
|
+
const answer = yield* send.pipe(Effect.catch((failure) => Effect.gen(function* () {
|
|
1136
|
+
if (!key || args.idempotencyKey)
|
|
1137
|
+
return yield* Effect.fail(failure);
|
|
1138
|
+
const dispatches = yield* Dispatches;
|
|
1139
|
+
const error = toConnectorCallError(failure, connection.errors, {
|
|
1140
|
+
classification: yield* Classification,
|
|
1141
|
+
dispatches,
|
|
1142
|
+
withholds: connection.safety.withholdsErrors,
|
|
1143
|
+
signal: yield* Signal,
|
|
1144
|
+
});
|
|
1145
|
+
const diagnostic = providerFailureMessage(error, `${error.message} ${idempotency.header}: ${key}.`);
|
|
1146
|
+
return yield* Effect.fail(new Refusal({
|
|
1147
|
+
error: dispatches.some((d) => d.request?.dispatched && d.request.key === key)
|
|
1148
|
+
? keyResendAdvice(diagnostic, key, " Reusing it with the exact original arguments retries this write without repeating it only while the vendor retains the key; after that, look the object up before retrying.")
|
|
1149
|
+
: diagnostic,
|
|
1150
|
+
}));
|
|
1151
|
+
})));
|
|
1152
|
+
const next = adapter
|
|
1153
|
+
? yield* checked(() => adapter.next(answer.data, { path, query, headers: answer.headers ?? {} }))
|
|
1154
|
+
: undefined;
|
|
1155
|
+
let page;
|
|
1156
|
+
if (adapter && next !== undefined) {
|
|
1157
|
+
const total = adapter.total ? yield* checked(() => adapter.total(answer.data)) : undefined;
|
|
1158
|
+
const secrets = yield* RequestSecrets;
|
|
1159
|
+
if (next !== null &&
|
|
1160
|
+
typeof next !== "object" &&
|
|
1161
|
+
(withheldCursor(next) || secrets.redact(String(next)) !== String(next))) {
|
|
1162
|
+
yield* checked(() => {
|
|
1163
|
+
throw new ConnectorCallError("connector_call_failed", "The vendor returned a withheld paging cursor.", {
|
|
1164
|
+
retryable: false,
|
|
1165
|
+
});
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
page = {
|
|
1169
|
+
hasMore: next !== null,
|
|
1170
|
+
...(typeof total === "number" && Number.isSafeInteger(total) && total >= 0 ? { total } : {}),
|
|
1171
|
+
...(next === null || typeof next === "object"
|
|
1172
|
+
? {}
|
|
1173
|
+
: {
|
|
1174
|
+
nextCursor: yield* boundCursor.encode(tool(kind), scope, JSON.stringify(separateCursor ? { param: adapter.param, in: adapter.in, value: next } : next), (json) => secrets.encode(new TextEncoder().encode(json), { url: true, claims: true })),
|
|
1175
|
+
}),
|
|
1176
|
+
param: adapter.param,
|
|
1177
|
+
in: adapter.in,
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
return {
|
|
1181
|
+
status: answer.status,
|
|
1182
|
+
data: selectFields(answer.data === undefined ? null : answer.data, args.fields),
|
|
1183
|
+
...(page ? { page } : {}),
|
|
1184
|
+
...(options.binding?.truncated && "truncated" in answer && answer.truncated?.length
|
|
1185
|
+
? { truncated: answer.truncated }
|
|
1186
|
+
: {}),
|
|
1187
|
+
...(options.binding?.warnings && "warnings" in answer && answer.warnings?.length
|
|
1188
|
+
? { warnings: answer.warnings }
|
|
1189
|
+
: {}),
|
|
1190
|
+
...(key ? { idempotencyKey: key } : {}),
|
|
1191
|
+
};
|
|
1192
|
+
});
|
|
1193
|
+
const note = (sentence) => (sentence ? ` ${sentence}` : "");
|
|
1194
|
+
const nonGetReads = readMethods.filter((method) => method !== "GET" && method !== "HEAD");
|
|
1195
|
+
// Field order follows the REST hatch: path, method, body, query, then fields.
|
|
1196
|
+
const readFields = { path: Path };
|
|
1197
|
+
// `method` is omitted only when GET is the one reviewed read method; a sole other method is the
|
|
1198
|
+
// default, and several non-GET read methods must be named.
|
|
1199
|
+
if (readMethods.length > 0 && !(readMethods.length === 1 && readMethods[0] === "GET")) {
|
|
1200
|
+
const methods = methodEnum(readMethods);
|
|
1201
|
+
readFields["method"] =
|
|
1202
|
+
readMethods.includes("GET") || readMethods.length === 1
|
|
1203
|
+
? Schema.optionalKey(methods.annotate({
|
|
1204
|
+
description: `${defaultRead} by default; another method only for an operation search routes here.`,
|
|
1205
|
+
}))
|
|
1206
|
+
: methods.annotate({ description: "The reviewed read method of the operation search routes here." });
|
|
1207
|
+
}
|
|
1208
|
+
if (nonGetReads.length)
|
|
1209
|
+
readFields["body"] = Schema.optionalKey((Body ?? ObjectValue).annotate({ description: "Body for a reviewed read-only POST." }));
|
|
1210
|
+
readFields["query"] = Schema.optionalKey(Query);
|
|
1211
|
+
readFields["fields"] = Schema.optionalKey(Fields);
|
|
1212
|
+
if (Headers)
|
|
1213
|
+
readFields["headers"] = Schema.optionalKey(Headers);
|
|
1214
|
+
if (options.pageCursor === "argument")
|
|
1215
|
+
readFields["cursor"] = Schema.optionalKey(text(1, 4096).annotate({
|
|
1216
|
+
description: "Opaque page.nextCursor from this same read; omit raw query anchors when continuing.",
|
|
1217
|
+
}));
|
|
1218
|
+
const readSpec = {
|
|
1219
|
+
...(options.writeSteps ? { writeSteps: options.writeSteps } : {}),
|
|
1220
|
+
name: tool("read"),
|
|
1221
|
+
description: `Call a reviewed ${title} API read${nonGetReads.length ? `, including reviewed read-only ${nonGetReads.join(" and ")}` : ""}. The operation index checks arguments before anything is sent.${note(options.notes?.read)}`,
|
|
1222
|
+
input: Schema.Struct(readFields),
|
|
1223
|
+
output: envelope,
|
|
1224
|
+
// The explicit per-operation read table replaces REST readPosts. The
|
|
1225
|
+
// builder's review permits its non-GET reads; run checks the exact row.
|
|
1226
|
+
review: "Only operations explicitly classified read in the hatch's snapshotted verdict table are admitted.",
|
|
1227
|
+
};
|
|
1228
|
+
// Let the Schema infer the program arguments rather than the shared field declarations.
|
|
1229
|
+
tools.push(read(connection, { ...readSpec, run: (args) => run("read", args) }));
|
|
1230
|
+
if (writeMethods.length > 0 && options.writeEnabled !== false) {
|
|
1231
|
+
const writeFields = {
|
|
1232
|
+
method: methodEnum(writeMethods).annotate({ description: "The operation's HTTP method." }),
|
|
1233
|
+
path: Path,
|
|
1234
|
+
query: Schema.optionalKey(Query),
|
|
1235
|
+
body: Schema.optionalKey((Body ?? Schema.Union([ObjectValue, Schema.Array(Schema.Unknown)])).annotate({
|
|
1236
|
+
description: "Request body as JSON fields, named as details lists them; form operations are encoded for you.",
|
|
1237
|
+
})),
|
|
1238
|
+
fields: Schema.optionalKey(Fields),
|
|
1239
|
+
};
|
|
1240
|
+
if (Headers)
|
|
1241
|
+
writeFields["headers"] = Schema.optionalKey(Headers);
|
|
1242
|
+
if (idempotency)
|
|
1243
|
+
writeFields["idempotencyKey"] = Schema.optionalKey(text(1, IDEMPOTENCY_KEY_MAX_LENGTH).annotate({
|
|
1244
|
+
description: "Reuse this key with the exact original arguments; a key is generated and returned when omitted for a supported method.",
|
|
1245
|
+
}));
|
|
1246
|
+
tools.push(write(connection, {
|
|
1247
|
+
...(options.writeSteps ? { writeSteps: options.writeSteps } : {}),
|
|
1248
|
+
name: tool("write"),
|
|
1249
|
+
description: `Call a reviewed ${title} API write; method and path are stated for approval, and the operation index checks arguments first.${note(options.notes?.write)}`,
|
|
1250
|
+
input: Schema.Struct(writeFields),
|
|
1251
|
+
output: idempotency
|
|
1252
|
+
? Schema.Struct({ ...callEnvelope.fields, idempotencyKey: Schema.optionalKey(Schema.String) })
|
|
1253
|
+
: callEnvelope,
|
|
1254
|
+
...(idempotency
|
|
1255
|
+
? {
|
|
1256
|
+
idempotency: {
|
|
1257
|
+
header: idempotency.header,
|
|
1258
|
+
key: (args) => idempotency.methods.includes(args["method"])
|
|
1259
|
+
? (args["idempotencyKey"] ?? crypto.randomUUID())
|
|
1260
|
+
: undefined,
|
|
1261
|
+
},
|
|
1262
|
+
}
|
|
1263
|
+
: {}),
|
|
1264
|
+
destructive: true,
|
|
1265
|
+
run: (args) => run("write", args),
|
|
1266
|
+
}));
|
|
1267
|
+
}
|
|
1268
|
+
registerOperationVocabulary(tools[0], function* () {
|
|
1269
|
+
for (const op of index.ops) {
|
|
1270
|
+
const verdict = discoveryVerdictOf(op);
|
|
1271
|
+
yield {
|
|
1272
|
+
text: `${op.path.slice(0, 384)} ${op.operationId.slice(0, 128)} ${op.summary.slice(0, 384)} ${op.tag.slice(0, 128)}`,
|
|
1273
|
+
route: verdict === "refuse" ? searchTool : route(op).tool,
|
|
1274
|
+
write: verdict !== "read",
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
});
|
|
1278
|
+
return tools;
|
|
1279
|
+
}
|