@zackbart/connecta 0.34.1 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +418 -0
- package/README.md +65 -105
- package/SECURITY.md +3 -2
- package/bin/connecta.mjs +41 -16
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.d.ts +2 -1
- package/dist/access-tokens.js +66 -30
- package/dist/activity.d.ts +1 -1
- package/dist/activity.js +2 -0
- package/dist/agent-config.d.ts +23 -0
- package/dist/agent-config.js +59 -0
- package/dist/agent-description.js +16 -0
- package/dist/agent-identity.js +4 -4
- package/dist/agent-observation.js +39 -0
- package/dist/auth/clerk-sdk/client.d.ts +2 -0
- package/dist/auth/clerk-sdk/generated.js +554 -563
- package/dist/auth/clerk-transport.js +70 -3
- package/dist/auth/clerk.d.ts +13 -0
- package/dist/auth/clerk.js +653 -246
- package/dist/auth/cloudflare-access.js +46 -42
- package/dist/auth/downstream-client-metadata.js +2 -2
- package/dist/auth/downstream-oauth.js +2044 -1161
- package/dist/auth/downstream-revocation.js +88 -88
- package/dist/auth/native-call.js +20 -0
- package/dist/auth/native.js +184 -0
- package/dist/auth/oauth-start-reset.js +11 -5
- package/dist/auth/oauth-token-response.js +121 -102
- package/dist/auth/oauth-v2-migration.js +97 -78
- package/dist/auth/renewable-grant.js +10 -0
- package/dist/auth/static-oauth.js +751 -281
- package/dist/auth/vercel-agent.js +266 -0
- package/dist/auth-admission.js +204 -0
- package/dist/auth-elicitation.js +350 -241
- package/dist/authorization-handoff.js +164 -90
- package/dist/call-admission-context.js +3 -10
- package/dist/call-admission.js +2 -0
- package/dist/call-classification.js +9 -0
- package/dist/call-recovery.js +32 -13
- package/dist/canonical-json.js +8 -0
- package/dist/catalog-cache.js +369 -213
- package/dist/catalog-service.js +659 -530
- package/dist/catalog.js +5 -1
- package/dist/concurrency.js +0 -25
- package/dist/config-defaults.d.ts +1 -0
- package/dist/config-defaults.js +1 -0
- package/dist/config-schema.d.ts +3 -1
- package/dist/config-schema.js +13 -3
- package/dist/config.d.ts +40 -6
- package/dist/config.js +40 -26
- package/dist/connector-caller.d.ts +4 -0
- package/dist/connector-caller.js +17 -4
- package/dist/connectors/api-connector.d.ts +29 -7
- package/dist/connectors/api-connector.js +145 -57
- package/dist/connectors/guarded-fetch.d.ts +51 -17
- package/dist/connectors/guarded-fetch.js +215 -121
- package/dist/connectors/negotiation-cache.js +20 -10
- package/dist/connectors/remote-mcp.d.ts +10 -30
- package/dist/connectors/remote-mcp.js +955 -563
- package/dist/d1.js +10 -243
- package/dist/derived-context.js +5 -0
- package/dist/describe-config.d.ts +3 -1
- package/dist/describe-config.js +2 -0
- package/dist/downstream-input.js +347 -260
- package/dist/errors.d.ts +87 -6
- package/dist/errors.js +114 -4
- package/dist/execute.js +87 -50
- package/dist/executor-admission.js +3 -0
- package/dist/executors/quickjs.js +163 -112
- package/dist/guarded-transport.js +50 -0
- package/dist/guest-runtime.js +1 -1
- package/dist/http/client.js +439 -0
- package/dist/http/errors.js +558 -0
- package/dist/http/fields.js +49 -0
- package/dist/http/hatch-paging.js +318 -0
- package/dist/http/hatch.js +1279 -0
- package/dist/http/json-schema.js +267 -0
- package/dist/http/node-transport.js +241 -0
- package/dist/http/oauth.js +162 -0
- package/dist/http/paging.js +227 -0
- package/dist/http/provider.js +221 -0
- package/dist/http/safety.js +82 -0
- package/dist/http/services.js +79 -0
- package/dist/http/tools.js +379 -0
- package/dist/http/vendor-index.js +138 -0
- package/dist/http/vendor.js +448 -0
- package/dist/http/write-request.js +66 -0
- package/dist/http-policy.d.ts +11 -0
- package/dist/http-policy.js +45 -0
- package/dist/inbound-credential.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -0
- package/dist/invocation-auth.js +30 -19
- package/dist/invocation.js +218 -85
- package/dist/mcp-result.js +0 -4
- package/dist/membership.d.ts +4 -0
- package/dist/meta-output.js +1 -1
- package/dist/meta-tools.js +531 -406
- package/dist/native-recovery.js +52 -0
- package/dist/native-tools.js +382 -0
- package/dist/node.d.ts +4 -3
- package/dist/node.js +45 -8
- package/dist/oauth-handoff.js +177 -104
- package/dist/oauth-partition.js +6 -20
- package/dist/oauth-sealing.js +6 -7
- package/dist/operation-vocabulary.js +101 -0
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +17 -4
- package/dist/operator-ui/contract.d.ts +8 -3
- package/dist/operator-ui/generated.js +4 -4
- package/dist/operator-ui/model.d.ts +2 -3
- package/dist/program-writes.js +47 -26
- package/dist/provider-call-facts.d.ts +115 -0
- package/dist/provider-call-facts.js +182 -0
- package/dist/provider-call.d.ts +112 -0
- package/dist/provider-call.js +271 -0
- package/dist/provider.d.ts +65 -31
- package/dist/provider.js +116 -39
- package/dist/providers/_shared/google/hatch.js +91 -0
- package/dist/providers/_shared/google/kit.js +427 -0
- package/dist/providers/_shared/google/result-size.js +15 -2
- package/dist/providers/_shared/google/service-account.d.ts +2 -1
- package/dist/providers/_shared/google/service-account.js +8 -4
- package/dist/providers/_shared/google/workspace.d.ts +6 -210
- package/dist/providers/_shared/google/workspace.js +6 -425
- package/dist/providers/_shared/rest/compact-index.js +86 -0
- package/dist/providers/_shared/rest/kit.js +146 -0
- package/dist/providers/_shared/rest/operation-index.js +230 -14
- package/dist/providers/_shared/rest/response-media.js +11 -0
- package/dist/providers/_shared/rest/value-safety.js +353 -195
- package/dist/providers/_shared/sensitive-fields.js +27 -0
- package/dist/providers/breeze/api.js +584 -0
- package/dist/providers/breeze/index.d.ts +1 -3
- package/dist/providers/breeze/index.js +67 -1420
- package/dist/providers/breeze/openapi.generated.js +60 -0
- package/dist/providers/breeze/skill.generated.js +2 -6
- package/dist/providers/breeze/tools.js +696 -0
- package/dist/providers/ccb/api.js +496 -0
- package/dist/providers/ccb/index.d.ts +15 -37
- package/dist/providers/ccb/index.js +77 -1702
- package/dist/providers/ccb/lib/constants.d.ts +34 -0
- package/dist/providers/ccb/lib/constants.js +74 -0
- package/dist/providers/ccb/lib/guide.js +9 -0
- package/dist/providers/ccb/lib/verdicts.js +387 -0
- package/dist/providers/ccb/openapi.generated.js +397 -0
- package/dist/providers/ccb/safety.js +295 -0
- package/dist/providers/ccb/skill.generated.js +2 -6
- package/dist/providers/ccb/tools.js +853 -0
- package/dist/providers/cloudflare/api.js +1066 -0
- package/dist/providers/cloudflare/index.d.ts +5 -22
- package/dist/providers/cloudflare/index.js +79 -197
- package/dist/providers/cloudflare/lib/config.d.ts +16 -0
- package/dist/providers/cloudflare/lib/config.js +2 -0
- package/dist/providers/cloudflare/lib/discovery.js +29 -0
- package/dist/providers/cloudflare/{graphql.js → lib/graphql.js} +1 -1
- package/dist/providers/cloudflare/lib/hatch.js +103 -0
- package/dist/providers/cloudflare/lib/path-bounds.js +28 -0
- package/dist/providers/cloudflare/lib/response-media.js +75 -0
- package/dist/providers/cloudflare/openapi.generated.js +4 -3
- package/dist/providers/cloudflare/{value-safety.js → safety.js} +487 -0
- package/dist/providers/cloudflare/skill.generated.js +2 -10
- package/dist/providers/cloudflare/tools.js +360 -0
- package/dist/providers/docs/api.js +51 -0
- package/dist/providers/docs/index.d.ts +1 -12
- package/dist/providers/docs/index.js +53 -1165
- package/dist/providers/docs/lib/constants.d.ts +18 -0
- package/dist/providers/docs/lib/constants.js +18 -0
- package/dist/providers/docs/lib/discovery.js +26 -0
- package/dist/providers/docs/lib/requests.js +48 -0
- package/dist/providers/docs/lib/schemas.js +168 -0
- package/dist/providers/docs/openapi.generated.js +16 -0
- package/dist/providers/docs/safety.js +54 -0
- package/dist/providers/docs/skill.generated.js +2 -6
- package/dist/providers/docs/tools.js +801 -0
- package/dist/providers/drive/api.js +177 -0
- package/dist/providers/drive/index.d.ts +1 -12
- package/dist/providers/drive/index.js +38 -1448
- package/dist/providers/drive/lib/constants.d.ts +6 -0
- package/dist/providers/drive/lib/constants.js +6 -0
- package/dist/providers/drive/lib/contracts.js +532 -0
- package/dist/providers/drive/lib/hatch.js +197 -0
- package/dist/providers/drive/openapi.generated.js +77 -0
- package/dist/providers/drive/safety.js +90 -0
- package/dist/providers/drive/skill.generated.js +2 -6
- package/dist/providers/drive/tools.js +1290 -0
- package/dist/providers/forms/api.js +71 -0
- package/dist/providers/forms/index.d.ts +1 -12
- package/dist/providers/forms/index.js +49 -1145
- package/dist/providers/forms/lib/constants.d.ts +14 -0
- package/dist/providers/forms/lib/constants.js +19 -0
- package/dist/providers/forms/lib/hatch.js +72 -0
- package/dist/providers/forms/openapi.generated.js +23 -0
- package/dist/providers/forms/safety.js +34 -0
- package/dist/providers/forms/skill.generated.js +2 -8
- package/dist/providers/forms/tools.js +1023 -0
- package/dist/providers/github/api.js +150 -0
- package/dist/providers/github/index.d.ts +4 -5
- package/dist/providers/github/index.js +30 -87
- package/dist/providers/github/{auth.d.ts → lib/auth.d.ts} +5 -3
- package/dist/providers/github/{auth.js → lib/auth.js} +40 -43
- package/dist/providers/github/lib/discovery.js +132 -0
- package/dist/providers/github/lib/operations.js +58 -0
- package/dist/providers/github/{scope.d.ts → lib/scope.d.ts} +2 -0
- package/dist/providers/github/{scope.js → lib/scope.js} +25 -1
- package/dist/providers/github/openapi.generated.js +1216 -0
- package/dist/providers/github/safety.js +216 -0
- package/dist/providers/github/skill.generated.js +2 -5
- package/dist/providers/github/tools.js +506 -0
- package/dist/providers/gmail/api.js +114 -0
- package/dist/providers/gmail/index.d.ts +1 -12
- package/dist/providers/gmail/index.js +73 -1621
- package/dist/providers/gmail/lib/constants.d.ts +13 -0
- package/dist/providers/gmail/lib/constants.js +16 -0
- package/dist/providers/gmail/lib/index.js +173 -0
- package/dist/providers/gmail/lib/schemas.js +176 -0
- package/dist/providers/gmail/lib/threading.js +120 -0
- package/dist/providers/gmail/openapi.generated.js +92 -0
- package/dist/providers/gmail/safety.js +21 -0
- package/dist/providers/gmail/skill.generated.js +2 -6
- package/dist/providers/gmail/tools.js +1435 -0
- package/dist/providers/overflow/api.js +1008 -0
- package/dist/providers/overflow/index.d.ts +1 -17
- package/dist/providers/overflow/index.js +60 -1488
- package/dist/providers/overflow/skill.generated.js +2 -5
- package/dist/providers/overflow/tools.js +873 -0
- package/dist/providers/planning-center/api.js +467 -0
- package/dist/providers/planning-center/index.d.ts +4 -35
- package/dist/providers/planning-center/index.js +69 -2445
- package/dist/providers/planning-center/lib/constants.d.ts +58 -0
- package/dist/providers/planning-center/lib/constants.js +83 -0
- package/dist/providers/planning-center/lib/discovery.js +40 -0
- package/dist/providers/planning-center/lib/guide.js +39 -0
- package/dist/providers/planning-center/lib/json-api.js +83 -0
- package/dist/providers/planning-center/lib/verdicts.js +1758 -0
- package/dist/providers/planning-center/openapi.generated.js +1764 -0
- package/dist/providers/planning-center/safety.js +188 -0
- package/dist/providers/planning-center/skill.generated.js +2 -27
- package/dist/providers/planning-center/tools.js +1010 -0
- package/dist/providers/quickbooks/api.js +299 -0
- package/dist/providers/quickbooks/index.d.ts +13 -0
- package/dist/providers/quickbooks/index.js +93 -0
- package/dist/providers/quickbooks/lib/query.js +118 -0
- package/dist/providers/quickbooks/lib/writes.js +168 -0
- package/dist/providers/quickbooks/safety.js +222 -0
- package/dist/providers/quickbooks/skill.generated.js +5 -0
- package/dist/providers/quickbooks/tools.js +117 -0
- package/dist/providers/sheets/api.js +133 -0
- package/dist/providers/sheets/index.d.ts +4 -16
- package/dist/providers/sheets/index.js +61 -1172
- package/dist/providers/sheets/lib/constants.d.ts +14 -0
- package/dist/providers/sheets/lib/constants.js +14 -0
- package/dist/providers/sheets/lib/hatch.js +163 -0
- package/dist/providers/sheets/lib/requests.js +105 -0
- package/dist/providers/sheets/lib/schemas.js +138 -0
- package/dist/providers/sheets/openapi.generated.js +30 -0
- package/dist/providers/sheets/safety.js +27 -0
- package/dist/providers/sheets/skill.generated.js +2 -7
- package/dist/providers/sheets/tools.js +733 -0
- package/dist/providers/slides/api.js +147 -0
- package/dist/providers/slides/index.d.ts +2 -10
- package/dist/providers/slides/index.js +48 -2555
- package/dist/providers/slides/lib/constants.d.ts +11 -0
- package/dist/providers/slides/lib/constants.js +11 -0
- package/dist/providers/slides/lib/requests.js +60 -0
- package/dist/providers/slides/lib/schemas.js +565 -0
- package/dist/providers/slides/openapi.generated.js +18 -0
- package/dist/providers/slides/skill.generated.js +2 -6
- package/dist/providers/slides/tools.js +1968 -0
- package/dist/providers/stripe/api.js +401 -0
- package/dist/providers/stripe/index.d.ts +5 -27
- package/dist/providers/stripe/index.js +74 -210
- package/dist/providers/stripe/lib/config.d.ts +4 -0
- package/dist/providers/stripe/lib/config.js +2 -0
- package/dist/providers/stripe/openapi.generated.js +4 -3
- package/dist/providers/stripe/{value-safety.js → safety.js} +5 -5
- package/dist/providers/stripe/skill.generated.js +2 -11
- package/dist/providers/stripe/tools.js +77 -0
- package/dist/providers/tithely/api.js +531 -0
- package/dist/providers/tithely/index.d.ts +17 -0
- package/dist/providers/tithely/index.js +86 -965
- package/dist/providers/tithely/skill.generated.js +2 -5
- package/dist/providers/tithely/tools.js +426 -0
- package/dist/providers/vercel/api.js +551 -0
- package/dist/providers/vercel/index.d.ts +9 -25
- package/dist/providers/vercel/index.js +65 -260
- package/dist/providers/vercel/lib/config.d.ts +2 -0
- package/dist/providers/vercel/lib/config.js +2 -0
- package/dist/providers/vercel/lib/index.js +117 -0
- package/dist/providers/vercel/lib/logs.js +100 -0
- package/dist/providers/vercel/lib/pin.js +270 -0
- package/dist/providers/vercel/openapi.generated.js +10 -5
- package/dist/providers/vercel/{value-safety.js → safety.js} +247 -38
- package/dist/providers/vercel/skill.generated.js +2 -7
- package/dist/providers/vercel/tools.js +642 -0
- package/dist/redaction-identity.js +47 -0
- package/dist/redaction-marker.js +125 -0
- package/dist/registry.d.ts +21 -15
- package/dist/registry.js +409 -376
- package/dist/routes/access-tokens.js +1 -1
- package/dist/routes/actor-labels.js +1 -1
- package/dist/routes/connect.js +1 -1
- package/dist/routes/dispatch.js +103 -0
- package/dist/routes/mcp.js +170 -79
- package/dist/routes/oauth.js +6 -1
- package/dist/routes/operator.js +2 -2
- package/dist/routes/shared.d.ts +5 -4
- package/dist/routes/shared.js +244 -177
- package/dist/routes/skills.js +10 -8
- package/dist/routes/ui-config.js +27 -3
- package/dist/routes/ui-data.js +21 -20
- package/dist/routes/ui.js +1 -1
- package/dist/runtime/call-admission.js +0 -26
- package/dist/runtime/call-entry.js +30 -0
- package/dist/runtime/call.js +552 -0
- package/dist/runtime/connector-scope.js +7 -0
- package/dist/runtime/credential-probe.js +34 -0
- package/dist/runtime/executor.js +40 -0
- package/dist/runtime/native-storage.js +63 -0
- package/dist/runtime/registry.js +27 -0
- package/dist/runtime/request-scope.js +72 -0
- package/dist/runtime/run.js +15 -7
- package/dist/runtime/shared-read.js +34 -5
- package/dist/runtime/storage.js +34 -21
- package/dist/sent-secrets.d.ts +194 -37
- package/dist/sent-secrets.js +1743 -340
- package/dist/server.js +5 -56
- package/dist/skills.js +322 -223
- package/dist/sqlite.js +15 -14
- package/dist/storage/d1-copy.js +255 -0
- package/dist/storage/d1-driver.js +15 -0
- package/dist/storage/keys.js +28 -0
- package/dist/storage/sql-driver.js +88 -0
- package/dist/storage/sql-native.js +544 -0
- package/dist/storage/sql-schema.d.ts +6 -0
- package/dist/storage/sql-schema.js +15 -0
- package/dist/storage/sql.d.ts +14 -20
- package/dist/storage/sql.js +29 -535
- package/dist/timeout.js +0 -26
- package/dist/tool-safety.js +20 -10
- package/dist/types.d.ts +9 -109
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +0 -1
- package/dist/upward.js +3 -0
- package/dist/usage-guide.js +3 -1
- package/dist/validate.d.ts +4 -2
- package/dist/validate.js +86 -9
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +312 -292
- package/dist/workspace-subject.js +5 -1
- package/dist/write-outcome.js +454 -34
- package/documentation/README.md +28 -7
- package/documentation/architecture.md +2409 -255
- package/documentation/auth.md +867 -114
- package/documentation/code-mode.md +49 -16
- package/documentation/deploying.md +164 -20
- package/documentation/hosted-mcp-migration.md +460 -0
- package/documentation/integrating.md +129 -144
- package/documentation/meta-tools.md +409 -32
- package/documentation/operating.md +27 -3
- package/documentation/operator-tests.md +3 -0
- package/documentation/operator-ui.md +84 -48
- package/documentation/provider-auth-migration.md +16 -63
- package/documentation/provider-migration-0.29.md +10 -5
- package/documentation/provider-migration-0.35.md +975 -0
- package/documentation/providers.md +1347 -0
- package/examples/worker/AGENTS.md +23 -9
- package/examples/worker/README.md +254 -135
- package/examples/worker/alchemy.run.ts +116 -0
- package/examples/worker/gitignore +10 -0
- package/examples/worker/src/connecta.config.ts +23 -21
- package/examples/worker/wrangler.jsonc +14 -5
- package/guides/connecting-agents.md +157 -0
- package/guides/how-it-works.md +156 -0
- package/guides/safety-and-trust.md +153 -0
- package/package.json +30 -50
- package/templates/node/.env.example +3 -0
- package/templates/node/README.md +22 -2
- package/templates/node/docker-compose.yml +1 -0
- package/templates/node/package.json +3 -2
- package/templates/node/src/connecta.config.ts +8 -0
- package/dist/catalog-drift.d.ts +0 -138
- package/dist/catalog-drift.js +0 -578
- package/dist/operator-ui/agents.d.ts +0 -74
- package/dist/operator-ui/agents.js +0 -104
- package/dist/operator-ui/config-summary.js +0 -565
- package/dist/operator-ui/connection-map.js +0 -376
- package/dist/operator-ui/endpoints.js +0 -221
- package/dist/operator-ui/fix-prompts.d.ts +0 -18
- package/dist/operator-ui/fix-prompts.js +0 -87
- package/dist/operator-ui/health.js +0 -52
- package/dist/operator-ui/member-page.js +0 -97
- package/dist/operator-ui/overview.js +0 -123
- package/dist/operator-ui/setup-commands.js +0 -51
- package/dist/operator-ui/view.d.ts +0 -401
- package/dist/operator-ui/view.js +0 -705
- package/dist/providers/_shared/request-token.js +0 -83
- package/dist/providers/_shared/rest/dispatch.js +0 -41
- package/dist/providers/_shared/rest/operation-index.d.ts +0 -133
- package/dist/providers/_shared/rest/tools.d.ts +0 -141
- package/dist/providers/_shared/rest/tools.js +0 -569
- package/dist/providers/_shared/rest/value-safety.d.ts +0 -201
- package/dist/providers/basecamp/index.d.ts +0 -45
- package/dist/providers/basecamp/index.js +0 -997
- package/dist/providers/basecamp/skill.generated.js +0 -9
- package/dist/providers/cloudflare/rest.d.ts +0 -37
- package/dist/providers/cloudflare/rest.js +0 -1097
- package/dist/providers/github/hosted.js +0 -218
- package/dist/providers/github/rest.js +0 -321
- package/dist/providers/infisical/index.d.ts +0 -7
- package/dist/providers/infisical/index.js +0 -713
- package/dist/providers/infisical/skill.generated.js +0 -8
- package/dist/providers/intercom/index.d.ts +0 -24
- package/dist/providers/intercom/index.js +0 -121
- package/dist/providers/intercom/skill.generated.js +0 -8
- package/dist/providers/linear/index.d.ts +0 -55
- package/dist/providers/linear/index.js +0 -537
- package/dist/providers/linear/skill.generated.js +0 -8
- package/dist/providers/mixpanel/index.d.ts +0 -59
- package/dist/providers/mixpanel/index.js +0 -472
- package/dist/providers/mixpanel/operations.js +0 -20
- package/dist/providers/mixpanel/rest.js +0 -603
- package/dist/providers/mixpanel/skill.generated.js +0 -10
- package/dist/providers/mixpanel/value-safety.js +0 -16
- package/dist/providers/notion/index.d.ts +0 -67
- package/dist/providers/notion/index.js +0 -368
- package/dist/providers/notion/openapi.generated.js +0 -75
- package/dist/providers/notion/rest.d.ts +0 -24
- package/dist/providers/notion/rest.js +0 -1226
- package/dist/providers/notion/skill.generated.js +0 -11
- package/dist/providers/notion/value-safety.js +0 -56
- package/dist/providers/revenuecat/index.d.ts +0 -51
- package/dist/providers/revenuecat/index.js +0 -697
- package/dist/providers/revenuecat/skill.generated.js +0 -11
- package/dist/providers/stripe/rest.d.ts +0 -14
- package/dist/providers/stripe/rest.js +0 -402
- package/dist/providers/vercel/rest.d.ts +0 -20
- package/dist/providers/vercel/rest.js +0 -1020
- package/dist/providers/wispr-flow/index.d.ts +0 -14
- package/dist/providers/wispr-flow/index.js +0 -117
- package/dist/providers/wispr-flow/skill.generated.js +0 -8
- /package/dist/providers/github/{key.js → lib/key.js} +0 -0
|
@@ -0,0 +1,1066 @@
|
|
|
1
|
+
// Cloudflare's pinned REST contracts, confinement, framing and failure semantics.
|
|
2
|
+
import { responseMedia } from "../_shared/rest/response-media.js";
|
|
3
|
+
import { EXCLUSIVE_PATH_IDS } from "./lib/path-bounds.js";
|
|
4
|
+
import { RESPONSE_MEDIA } from "./lib/response-media.js";
|
|
5
|
+
import { Effect, Option, Schema } from "effect";
|
|
6
|
+
import { ConnectorCallError } from "../../errors.js";
|
|
7
|
+
import { Refusal, StatusFailure, toConnectorCallError, } from "../../http/errors.js";
|
|
8
|
+
import { Budget, Credentials, Classification, Dispatches, Signal } from "../../http/services.js";
|
|
9
|
+
import { restSafety } from "../../http/safety.js";
|
|
10
|
+
import { get, post, vendor as declareVendor, } from "../../http/vendor.js";
|
|
11
|
+
import { callMemo, Operation as ContextView } from "../../runtime/call.js";
|
|
12
|
+
import { checked, indexedVendor } from "../_shared/rest/kit.js";
|
|
13
|
+
import { Integer } from "../../http/json-schema.js";
|
|
14
|
+
import { cloudflarePage } from "../../http/hatch-paging.js";
|
|
15
|
+
import { OperationIndex } from "../_shared/rest/operation-index.js";
|
|
16
|
+
import { valueSafety, withholdEmbeddedUrlsIn, refuseWithheldLinks, refuseWithheldValues, refuseWithheldUpload, refuseWithheldBase64, } from "../_shared/rest/value-safety.js";
|
|
17
|
+
import { CLOUDFLARE_VALUE_SAFETY } from "./safety.js";
|
|
18
|
+
import { openapi } from "./openapi.generated.js";
|
|
19
|
+
import { cloudflareDiscovery } from "./lib/discovery.js";
|
|
20
|
+
import { CLOUDFLARE_API_BASE } from "./lib/config.js";
|
|
21
|
+
export const MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
|
|
22
|
+
/** Zone-ownership reads a pin check may spend: per tool call, and per five minutes per connector. */
|
|
23
|
+
const MAX_LOOKUPS_PER_CALL = 3;
|
|
24
|
+
const MAX_LOOKUPS_PER_WINDOW = 60;
|
|
25
|
+
const LOOKUP_WINDOW_MS = 300_000;
|
|
26
|
+
export const READ_CAP_MS = 60_000;
|
|
27
|
+
export const WRITE_STEPS = Object.freeze({ lookup1: "once", lookup2: "once", lookup3: "once", write: "once" });
|
|
28
|
+
/**
|
|
29
|
+
* POSTs that only read, admitted by `cloudflare_api_read`, each reviewed
|
|
30
|
+
* against the pinned spec for four disqualifiers: it persists state, it is
|
|
31
|
+
* billed per call or runs a model, it sends traffic outside Cloudflare, or it
|
|
32
|
+
* activates something. Requests that look like reads but are not stay writes:
|
|
33
|
+
* D1 `…/query` and `…/raw` and Durable Object `…/query/v2` run SQL that can
|
|
34
|
+
* write; Workers AI `…/ai/run`, AI Search and AutoRAG search (query rewriting
|
|
35
|
+
* and reranking run billed models), browser rendering, web search and brand
|
|
36
|
+
* logo search run models or fetch the web; monetization "checks" activate an
|
|
37
|
+
* entitlement; flag evaluation is metered; robots.txt bulk reads fetch
|
|
38
|
+
* origins; previews, tests, scans, exports and ownership challenges create
|
|
39
|
+
* state or send traffic; and token or credential POSTs are refused outright
|
|
40
|
+
* (`CLOUDFLARE_VALUE_SAFETY`).
|
|
41
|
+
*/
|
|
42
|
+
export const READ_POSTS = [
|
|
43
|
+
[
|
|
44
|
+
"POST",
|
|
45
|
+
"/accounts/{account_id}/resource-library/applications/lookup",
|
|
46
|
+
"Looks up application names in a sentence; changes no application or rule.",
|
|
47
|
+
],
|
|
48
|
+
[
|
|
49
|
+
"POST",
|
|
50
|
+
"/accounts/{account_id}/data-security/posture/finding_types/validate",
|
|
51
|
+
"Checks a candidate finding definition without creating or reserving it.",
|
|
52
|
+
],
|
|
53
|
+
["POST", "/accounts/{account_id}/analytics/query/{dataset}/summary", "Analytics query; aggregates only."],
|
|
54
|
+
["POST", "/accounts/{account_id}/analytics/query/{dataset}/timeseries", "Analytics query; aggregates only."],
|
|
55
|
+
["POST", "/accounts/{account_id}/analytics/query/{dataset}/top-n", "Analytics query; aggregates only."],
|
|
56
|
+
[
|
|
57
|
+
"POST",
|
|
58
|
+
"/accounts/{account_id}/analytics/query/data-security/content-findings/top-n",
|
|
59
|
+
"Analytics query; aggregates only.",
|
|
60
|
+
],
|
|
61
|
+
[
|
|
62
|
+
"POST",
|
|
63
|
+
"/accounts/{account_id}/analytics/query/data-security/findings/summary",
|
|
64
|
+
"Analytics query; aggregates only.",
|
|
65
|
+
],
|
|
66
|
+
[
|
|
67
|
+
"POST",
|
|
68
|
+
"/accounts/{account_id}/analytics/query/data-security/findings/timeseries",
|
|
69
|
+
"Analytics query; aggregates only.",
|
|
70
|
+
],
|
|
71
|
+
[
|
|
72
|
+
"POST",
|
|
73
|
+
"/accounts/{account_id}/analytics_engine/sql",
|
|
74
|
+
"Analytics Engine SQL is SELECT-only; the body is the query.",
|
|
75
|
+
],
|
|
76
|
+
["POST", "/accounts/{account_tag}/analytics/sql", "SQL API over analytics datasets; read-only by contract."],
|
|
77
|
+
["POST", "/analytics/sql", "SQL API over analytics datasets; read-only by contract."],
|
|
78
|
+
["POST", "/accounts/{account_id}/logs/explorer/query/sql", "Log Explorer SQL query; reads stored logs."],
|
|
79
|
+
["POST", "/zones/{zone_id}/logs/explorer/query/sql", "Log Explorer SQL query; reads stored logs."],
|
|
80
|
+
["POST", "/accounts/{account_id}/storage/kv/namespaces/{namespace_id}/bulk/get", "Reads several KV values at once."],
|
|
81
|
+
["POST", "/accounts/{account_id}/vectorize/v2/indexes/{index_name}/query", "Nearest-vector query; no model runs."],
|
|
82
|
+
["POST", "/accounts/{account_id}/vectorize/v2/indexes/{index_name}/get_by_ids", "Reads vectors by id."],
|
|
83
|
+
["POST", "/accounts/{account_id}/workers/observability/telemetry/query", "Runs a telemetry query; saves nothing."],
|
|
84
|
+
["POST", "/accounts/{account_id}/workers/observability/telemetry/keys", "Lists telemetry keys."],
|
|
85
|
+
["POST", "/accounts/{account_id}/workers/observability/telemetry/values", "Lists telemetry values."],
|
|
86
|
+
[
|
|
87
|
+
"POST",
|
|
88
|
+
"/accounts/{account_id}/browser-extension/config/logs/extension-events/search",
|
|
89
|
+
"Searches extension event logs.",
|
|
90
|
+
],
|
|
91
|
+
[
|
|
92
|
+
"POST",
|
|
93
|
+
"/accounts/{account_id}/browser-extension/config/logs/extension-events/timeseries",
|
|
94
|
+
"Extension event aggregates.",
|
|
95
|
+
],
|
|
96
|
+
["POST", "/accounts/{account_id}/billable/usage", "Filtered usage read; Cloudflare documents it read-only."],
|
|
97
|
+
["POST", "/accounts/{account_id}/cloudforce-one/events", "Filters and lists threat events."],
|
|
98
|
+
["POST", "/accounts/{account_id}/cloudforce-one/requests", "Lists intelligence requests."],
|
|
99
|
+
["POST", "/accounts/{account_id}/cloudforce-one/requests/priority", "Lists priority intelligence requirements."],
|
|
100
|
+
["POST", "/accounts/{account_id}/cloudforce-one/requests/{request_id}/asset", "Lists request assets."],
|
|
101
|
+
["POST", "/accounts/{account_id}/cloudforce-one/requests/{request_id}/message", "Lists request messages."],
|
|
102
|
+
[
|
|
103
|
+
"POST",
|
|
104
|
+
"/accounts/{account_id}/cloudforce-one/v2/brand-protection/takedown-notices/lookup",
|
|
105
|
+
"Looks up takedown notices.",
|
|
106
|
+
],
|
|
107
|
+
["POST", "/accounts/{account_id}/cloudforce-one/v2/collections/{collection_id}/search", "Searches collection items."],
|
|
108
|
+
["POST", "/accounts/{account_id}/cloudforce-one/v2/priority-intelligence/quota", "Reads the PIR quota."],
|
|
109
|
+
["POST", "/accounts/{account_id}/cloudforce-one/rules/validate", "Validates a rule; stores nothing."],
|
|
110
|
+
["POST", "/accounts/{account_id}/cloudforce-one/rules/structured/validate", "Validates a rule; stores nothing."],
|
|
111
|
+
["POST", "/accounts/{account_id}/access/custom_pages/validate", "Validates a page template; stores nothing."],
|
|
112
|
+
["POST", "/accounts/{account_id}/dlp/patterns/validate", "Validates a regex; stores nothing."],
|
|
113
|
+
["POST", "/accounts/{account_id}/pipelines/v1/validate_sql", "Validates SQL; stores nothing."],
|
|
114
|
+
["POST", "/accounts/{account_id}/logpush/validate/origin", "Validates logpull options; stores nothing."],
|
|
115
|
+
["POST", "/zones/{zone_id}/logpush/validate/origin", "Validates logpull options; stores nothing."],
|
|
116
|
+
["POST", "/accounts/{account_id}/logpush/transformers/preview", "Stateless transformer preview."],
|
|
117
|
+
["POST", "/accounts/{account_id}/registrar/domain-check", "Domain availability check."],
|
|
118
|
+
["POST", "/accounts/{account_id}/registrar/domain-transfer-check", "Transfer eligibility check."],
|
|
119
|
+
["POST", "/accounts/{account_id}/registrar-sandbox/domain-check", "Domain availability check."],
|
|
120
|
+
["POST", "/accounts/{account_id}/pay-per-crawl/zones_can_be_enabled/query", "Reads a zone setting."],
|
|
121
|
+
["POST", "/accounts/{account_id}/ai-gateway/billing/topup/eligibility", "Eligibility check; starts nothing."],
|
|
122
|
+
["POST", "/accounts/{account_id}/ai-gateway/billing/topup/status", "Reads a payment status."],
|
|
123
|
+
["POST", "/accounts/{account_id}/request-tracer/trace", "Simulated trace; sends no traffic."],
|
|
124
|
+
["POST", "/accounts/{account_id}/magic/cloud/resources/policy-preview", "Evaluates a policy against the catalog."],
|
|
125
|
+
["POST", "/accounts/{account_id}/magic/cloud/onramps/{onramp_id}/export", "Renders Terraform; changes nothing."],
|
|
126
|
+
["POST", "/zones/{zone_id}/email/sending/subdomains/preview", "Previews DNS records; stores nothing."],
|
|
127
|
+
["POST", "/zones/{zone_id}/token_validation/rules/preview", "Previews a rule; stores nothing."],
|
|
128
|
+
];
|
|
129
|
+
/**
|
|
130
|
+
* Operations `cloudflare_api_upload` sends, by method and template: Workers
|
|
131
|
+
* scripts and versions (including Workers for Platforms), KV values, R2
|
|
132
|
+
* objects, Pages deployments, Images, Stream, DNS zone-file import, and
|
|
133
|
+
* Snippets. Other multipart operations are refused rather than guessed at.
|
|
134
|
+
*/
|
|
135
|
+
export const UPLOADS = [
|
|
136
|
+
["PUT", "/accounts/{account_id}/workers/scripts/{script_name}"],
|
|
137
|
+
["PUT", "/accounts/{account_id}/workers/scripts/{script_name}/content"],
|
|
138
|
+
["PATCH", "/accounts/{account_id}/workers/scripts/{script_name}/settings"],
|
|
139
|
+
["POST", "/accounts/{account_id}/workers/scripts/{script_name}/versions"],
|
|
140
|
+
["PUT", "/accounts/{account_id}/workers/dispatch/namespaces/{dispatch_namespace}/scripts/{script_name}"],
|
|
141
|
+
["PUT", "/accounts/{account_id}/workers/dispatch/namespaces/{dispatch_namespace}/scripts/{script_name}/content"],
|
|
142
|
+
["PATCH", "/accounts/{account_id}/workers/dispatch/namespaces/{dispatch_namespace}/scripts/{script_name}/settings"],
|
|
143
|
+
["PUT", "/accounts/{account_id}/workers/services/{service_name}/environments/{environment_name}/content"],
|
|
144
|
+
["PUT", "/accounts/{account_id}/storage/kv/namespaces/{namespace_id}/values/{key_name}"],
|
|
145
|
+
["PUT", "/accounts/{account_id}/r2/buckets/{bucket_name}/objects/{object_key}"],
|
|
146
|
+
["POST", "/accounts/{account_id}/pages/projects/{project_name}/deployments"],
|
|
147
|
+
["POST", "/accounts/{account_id}/images/v1"],
|
|
148
|
+
["POST", "/accounts/{account_id}/stream"],
|
|
149
|
+
["PUT", "/accounts/{account_id}/stream/{identifier}/captions/{language}"],
|
|
150
|
+
["POST", "/zones/{zone_id}/dns_records/import"],
|
|
151
|
+
["PUT", "/zones/{zone_id}/snippets/{snippet_name}"],
|
|
152
|
+
];
|
|
153
|
+
/**
|
|
154
|
+
* Operations with no account or zone in their path that a pinned connector
|
|
155
|
+
* still admits, each holding to the pin some other way: the identity reads
|
|
156
|
+
* name only the credential's own user, the three lists are filtered to the
|
|
157
|
+
* pin, zone creation must name a pinned account, and the rest is public data.
|
|
158
|
+
* Every other unscoped operation is refused under a pin (default deny).
|
|
159
|
+
*/
|
|
160
|
+
export const PIN_SAFE_UNSCOPED = new Set([
|
|
161
|
+
"GET /user",
|
|
162
|
+
"GET /user/tokens/verify",
|
|
163
|
+
"GET /accounts",
|
|
164
|
+
"GET /zones",
|
|
165
|
+
"POST /zones",
|
|
166
|
+
"GET /memberships",
|
|
167
|
+
"GET /ips",
|
|
168
|
+
// CWS category definitions and filter-builder schema are global catalogs,
|
|
169
|
+
// not account policy, credential, or deployment records.
|
|
170
|
+
"GET /cws/categories/apis",
|
|
171
|
+
"GET /cws/categories/applications",
|
|
172
|
+
"GET /cws/categories/applications/{application_key}/versions",
|
|
173
|
+
"GET /cws/categories/applications/{application_key}/versions/latest",
|
|
174
|
+
"GET /cws/categories/applications/{application_key}/versions/{version_tag}",
|
|
175
|
+
"GET /cws/categories/operations",
|
|
176
|
+
"GET /cws/filter-schema",
|
|
177
|
+
"POST /graphql",
|
|
178
|
+
]);
|
|
179
|
+
/** Why each family of unscoped operations is refused under a pin; `test` checks every one is covered. */
|
|
180
|
+
export const PIN_REFUSED_UNSCOPED = {
|
|
181
|
+
accounts: "creates or moves accounts outside the pin",
|
|
182
|
+
analytics: "queries datasets across every account; use POST /accounts/{account_tag}/analytics/sql",
|
|
183
|
+
api: "is an internal health route",
|
|
184
|
+
billing: "is user-level billing",
|
|
185
|
+
certificates: "manages Origin CA certificates across every zone the user holds",
|
|
186
|
+
internal: "is an internal route",
|
|
187
|
+
live: "is an internal health route",
|
|
188
|
+
memberships: "changes or reads one of the user's memberships, which may be another account's",
|
|
189
|
+
oauth: "lists user-level OAuth scopes",
|
|
190
|
+
organizations: "spans several accounts",
|
|
191
|
+
pages: "uses a Pages upload token, not this connector's credential",
|
|
192
|
+
produce: "writes to a pipeline the path does not scope",
|
|
193
|
+
radar: "is not a GET",
|
|
194
|
+
ready: "is an internal health route",
|
|
195
|
+
"signed-url": "is an internal test route",
|
|
196
|
+
subscriptions: "reads or acknowledges a stream the path does not scope",
|
|
197
|
+
tenants: "spans several accounts",
|
|
198
|
+
user: "acts on user-level settings shared by every account",
|
|
199
|
+
workers: "triggers a deploy hook the path does not scope",
|
|
200
|
+
zones: "is not a list or a create",
|
|
201
|
+
};
|
|
202
|
+
/** Request headers the generic tools accept; both are R2's. */
|
|
203
|
+
export const HEADERS = {
|
|
204
|
+
"cf-r2-jurisdiction": "R2 only: default, eu, us, fedramp, or fedramp-high, for a bucket outside the default jurisdiction.",
|
|
205
|
+
"cf-r2-storage-class": "R2 only: Standard or InfrequentAccess.",
|
|
206
|
+
};
|
|
207
|
+
const HEADER_VALUES = {
|
|
208
|
+
"cf-r2-jurisdiction": ["default", "eu", "us", "fedramp", "fedramp-high"],
|
|
209
|
+
"cf-r2-storage-class": ["Standard", "InfrequentAccess"],
|
|
210
|
+
};
|
|
211
|
+
/** Path parameters whose values may carry an encoded "/": R2 object keys and KV key names. */
|
|
212
|
+
const SLASH_PARAMS = ["object_key", "key_name"];
|
|
213
|
+
export function record(value) {
|
|
214
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
215
|
+
}
|
|
216
|
+
export function text(value) {
|
|
217
|
+
return typeof value === "string" && value.trim() !== "" ? value : undefined;
|
|
218
|
+
}
|
|
219
|
+
export function invalid(message) {
|
|
220
|
+
throw new ConnectorCallError("invalid_args", message);
|
|
221
|
+
}
|
|
222
|
+
function errorsOf(body) {
|
|
223
|
+
const errors = record(body)["errors"];
|
|
224
|
+
return Array.isArray(errors)
|
|
225
|
+
? errors.filter((entry) => typeof entry === "object" && entry !== null)
|
|
226
|
+
: [];
|
|
227
|
+
}
|
|
228
|
+
/** Flatten Cloudflare's error array (and any nested chain) into one line. */
|
|
229
|
+
export function describeErrors(errors) {
|
|
230
|
+
const parts = [];
|
|
231
|
+
const walk = (list) => {
|
|
232
|
+
for (const entry of list) {
|
|
233
|
+
if (typeof entry !== "object" || entry === null) {
|
|
234
|
+
parts.push("Unknown error");
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
const code = typeof entry.code === "number" ? entry.code : undefined;
|
|
238
|
+
const message = typeof entry.message === "string" ? entry.message.slice(0, 500) : "Unknown error";
|
|
239
|
+
parts.push(code === undefined ? message : `${code}: ${message}`);
|
|
240
|
+
if (Array.isArray(entry.error_chain))
|
|
241
|
+
walk(entry.error_chain);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
walk(errors.slice(0, 10));
|
|
245
|
+
return parts.length > 0 ? parts.join("; ") : "Cloudflare reported no detail.";
|
|
246
|
+
}
|
|
247
|
+
function errorCodes(errors) {
|
|
248
|
+
const codes = new Set();
|
|
249
|
+
const walk = (list) => {
|
|
250
|
+
for (const entry of list) {
|
|
251
|
+
if (typeof entry !== "object" || entry === null)
|
|
252
|
+
continue;
|
|
253
|
+
if (typeof entry.code === "number")
|
|
254
|
+
codes.add(entry.code);
|
|
255
|
+
if (Array.isArray(entry.error_chain))
|
|
256
|
+
walk(entry.error_chain);
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
walk(errors);
|
|
260
|
+
return codes;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Credential-shaped Cloudflare error codes that are *not* already implied by a
|
|
264
|
+
* 401 or 403: a missing or malformed `Authorization` header, and the legacy
|
|
265
|
+
* key/email headers. These arrive on HTTP 400, so status alone would misfile
|
|
266
|
+
* them as an argument problem the agent could repair.
|
|
267
|
+
*
|
|
268
|
+
* Deliberately excludes 10000. Cloudflare returns 10000 for "Authentication
|
|
269
|
+
* error" but has also been observed reusing it as a generic validation code,
|
|
270
|
+
* so routing on it would risk telling an agent its token was broken when its
|
|
271
|
+
* arguments were. Genuine 10000 auth failures arrive with 401 or 403.
|
|
272
|
+
*
|
|
273
|
+
* Cloudflare publishes no table mapping codes to causes; these six come from
|
|
274
|
+
* community reports and probing. Prefer `verify_credential` when a diagnosis
|
|
275
|
+
* matters. Cloudflare also rate-limits authentication failures separately
|
|
276
|
+
* (429 with code 10502), one more reason to verify once rather than retry.
|
|
277
|
+
*/
|
|
278
|
+
const AUTH_ERROR_CODES = new Set([1001, 6003, 6111, 9103, 9106, 9107]);
|
|
279
|
+
/**
|
|
280
|
+
* The reviewed value-safety table over the pinned index: refusals, redaction
|
|
281
|
+
* of every successful body, and which operations withhold error text (any
|
|
282
|
+
* reviewed operation, or one whose request accepts a credential, header, or
|
|
283
|
+
* environment value, since a vendor error may echo what was submitted).
|
|
284
|
+
*/
|
|
285
|
+
export const SAFETY = valueSafety(CLOUDFLARE_VALUE_SAFETY, () => cloudflareIndex);
|
|
286
|
+
const rejected = () => ({
|
|
287
|
+
code: "invalid_args",
|
|
288
|
+
words: (detail) => detail,
|
|
289
|
+
});
|
|
290
|
+
const authFailure = () => ({
|
|
291
|
+
code: "auth_required",
|
|
292
|
+
words: (detail) => `${detail} The configured credential was rejected. An operator must check or replace it in this connection; call verify_credential to confirm.`,
|
|
293
|
+
});
|
|
294
|
+
export const cloudflareErrors = {
|
|
295
|
+
meanings: { unknownId: "not_found", provesAbsence: true, conflicts: [] },
|
|
296
|
+
numericFacts: {
|
|
297
|
+
retryAfterMs: {
|
|
298
|
+
header: "retry-after",
|
|
299
|
+
format: "retry-after",
|
|
300
|
+
review: "HTTP Retry-After delay for failure recovery.",
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
provider: "Cloudflare",
|
|
304
|
+
facts: (body) => ({ codes: [...errorCodes(errorsOf(body))] }),
|
|
305
|
+
factVocabulary: { codes: [...AUTH_ERROR_CODES] },
|
|
306
|
+
headers: ["cf-ray"],
|
|
307
|
+
detail: ({ status, facts, headers, withheld, body }) => {
|
|
308
|
+
const codes = facts.codes;
|
|
309
|
+
const described = withheld
|
|
310
|
+
? `${codes.length ? `Cloudflare error code ${codes.join(", ")}.` : "Cloudflare reported an error."} Its text is withheld because this operation handles credentials.`
|
|
311
|
+
: describeErrors(errorsOf(withholdEmbeddedUrlsIn(body)));
|
|
312
|
+
const detail = errorsOf(body).length || withheld
|
|
313
|
+
? `Cloudflare returned HTTP ${status}: ${described.trim()}`
|
|
314
|
+
: `Cloudflare returned HTTP ${status}.`;
|
|
315
|
+
return `${detail}${/[.!?]$/.test(detail) ? "" : "."}${headers["cf-ray"] ? ` Ray ${headers["cf-ray"]}.` : ""}`;
|
|
316
|
+
},
|
|
317
|
+
classify: [
|
|
318
|
+
({ status, facts }) => status !== 429 && facts.codes.some((code) => AUTH_ERROR_CODES.has(code)) ? authFailure() : undefined,
|
|
319
|
+
],
|
|
320
|
+
statuses: {
|
|
321
|
+
429: ({ retryAfterMs }) => ({
|
|
322
|
+
code: "rate_limited",
|
|
323
|
+
retryAfterMs: retryAfterMs ?? 300_000,
|
|
324
|
+
words: (detail) => `${detail} The documented limit is 1,200 requests per five minutes per user, counted across the dashboard and every token.`,
|
|
325
|
+
}),
|
|
326
|
+
401: authFailure,
|
|
327
|
+
403: () => ({
|
|
328
|
+
code: "provider_permission_denied",
|
|
329
|
+
words: (detail) => `${detail} Verify the credential first; if it is valid, the token or user lacks this permission or resource.`,
|
|
330
|
+
}),
|
|
331
|
+
405: rejected,
|
|
332
|
+
415: rejected,
|
|
333
|
+
404: () => ({
|
|
334
|
+
code: "not_found",
|
|
335
|
+
words: (detail) => `${detail} Confirm the id with list_zones, list_accounts, or a list read.`,
|
|
336
|
+
}),
|
|
337
|
+
},
|
|
338
|
+
unreadable: "Cloudflare returned malformed JSON.",
|
|
339
|
+
};
|
|
340
|
+
function isEnvelope(body) {
|
|
341
|
+
const value = record(body);
|
|
342
|
+
return typeof value["success"] === "boolean" && ("result" in value || "errors" in value);
|
|
343
|
+
}
|
|
344
|
+
// --- Pins --------------------------------------------------------------------
|
|
345
|
+
const ACCOUNT_KEYS = new Set(["account_id", "accountId", "account.id", "account_tag", "accountTag"]);
|
|
346
|
+
const ZONE_KEYS = new Set(["zone_id", "zoneId", "zone.id", "zone_tag", "zoneTag"]);
|
|
347
|
+
/** Every account and zone id a call names in its path, query, or top-level body. */
|
|
348
|
+
function scopeIds(call) {
|
|
349
|
+
const template = call.op.path.slice(1).split("/");
|
|
350
|
+
const accounts = [];
|
|
351
|
+
const zones = [];
|
|
352
|
+
template.forEach((part, index) => {
|
|
353
|
+
if (!part.startsWith("{"))
|
|
354
|
+
return;
|
|
355
|
+
const value = call.params[part.slice(1, -1)];
|
|
356
|
+
if (value === undefined)
|
|
357
|
+
return;
|
|
358
|
+
if (template[index - 1] === "accounts")
|
|
359
|
+
accounts.push(decodeURIComponent(value));
|
|
360
|
+
if (template[index - 1] === "zones")
|
|
361
|
+
zones.push(decodeURIComponent(value));
|
|
362
|
+
});
|
|
363
|
+
const scan = (source) => {
|
|
364
|
+
for (const [key, value] of Object.entries(source)) {
|
|
365
|
+
const values = Array.isArray(value) ? value : [value];
|
|
366
|
+
for (const item of values) {
|
|
367
|
+
if (typeof item !== "string")
|
|
368
|
+
continue;
|
|
369
|
+
if (ACCOUNT_KEYS.has(key))
|
|
370
|
+
accounts.push(item);
|
|
371
|
+
if (ZONE_KEYS.has(key))
|
|
372
|
+
zones.push(item);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
scan(record(call.query));
|
|
377
|
+
const body = record(call.body);
|
|
378
|
+
scan(body);
|
|
379
|
+
const account = text(record(body["account"])["id"]);
|
|
380
|
+
if (account)
|
|
381
|
+
accounts.push(account);
|
|
382
|
+
const zone = text(record(body["zone"])["id"]);
|
|
383
|
+
if (zone)
|
|
384
|
+
zones.push(zone);
|
|
385
|
+
return { accounts, zones, template };
|
|
386
|
+
}
|
|
387
|
+
// --- Framing -----------------------------------------------------------------
|
|
388
|
+
/** A pre-framed upload body, carried through `RestCall.body` to `encode`. */
|
|
389
|
+
export class RawUpload {
|
|
390
|
+
body;
|
|
391
|
+
contentType;
|
|
392
|
+
constructor(body, contentType) {
|
|
393
|
+
this.body = body;
|
|
394
|
+
this.contentType = contentType;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
export function bytesFromBase64(value, field) {
|
|
398
|
+
try {
|
|
399
|
+
const binary = atob(value);
|
|
400
|
+
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
return invalid(`${field} must be valid base64.`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
function queryOf(call) {
|
|
407
|
+
const query = {};
|
|
408
|
+
const scalar = (item) => typeof item === "string" || typeof item === "number" || typeof item === "boolean";
|
|
409
|
+
for (const [name, value] of Object.entries(call.query)) {
|
|
410
|
+
if (value === undefined)
|
|
411
|
+
continue;
|
|
412
|
+
if (scalar(value) || (Array.isArray(value) && value.every(scalar)))
|
|
413
|
+
query[name] = value;
|
|
414
|
+
else
|
|
415
|
+
invalid(`Cloudflare query parameter ${name} takes a string, number, boolean, or a list of them.`);
|
|
416
|
+
}
|
|
417
|
+
return query;
|
|
418
|
+
}
|
|
419
|
+
// --- The vendor ----------------------------------------------------------------
|
|
420
|
+
class CloudflareIndex extends OperationIndex {
|
|
421
|
+
contract(...args) {
|
|
422
|
+
const contract = super.contract(...args);
|
|
423
|
+
const exclusive = EXCLUSIVE_PATH_IDS[`${contract.method} ${contract.path}`] ?? [];
|
|
424
|
+
return {
|
|
425
|
+
...contract,
|
|
426
|
+
parameters: contract.parameters.map((param) => param.in === "path" && exclusive.includes(param.name)
|
|
427
|
+
? { ...param, schema: { ...param.schema, minimum: 1 } }
|
|
428
|
+
: param),
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
resolve(method, path) {
|
|
432
|
+
const match = super.resolve(method, path);
|
|
433
|
+
for (const name of EXCLUSIVE_PATH_IDS[`${match.op.method} ${match.op.path}`] ?? []) {
|
|
434
|
+
if (Number(decodeURIComponent(match.params[name])) < 1) {
|
|
435
|
+
throw new ConnectorCallError("invalid_args", `Not sent: Cloudflare path parameter ${name} must be at least 1. Read cloudflare_api_details for the pinned contract.`, {
|
|
436
|
+
validation: { issues: [{ path: `/path/${name}`, code: "minimum", expected: "at least 1" }] },
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return match;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
export const cloudflareIndex = new CloudflareIndex(cloudflareDiscovery(openapi), {
|
|
444
|
+
vendor: "cloudflare",
|
|
445
|
+
title: "Cloudflare",
|
|
446
|
+
slashParams: SLASH_PARAMS,
|
|
447
|
+
});
|
|
448
|
+
export const getUser = get("getUser", "/user", { success: Schema.Unknown });
|
|
449
|
+
export const verifyToken = get("verifyToken", "/user/tokens/verify", { success: Schema.Unknown });
|
|
450
|
+
export const verifyAccountToken = get("verifyAccountToken", "/accounts/:account_id/tokens/verify", {
|
|
451
|
+
params: { account_id: Schema.String },
|
|
452
|
+
success: Schema.Unknown,
|
|
453
|
+
});
|
|
454
|
+
const PageQuery = { page: Schema.optionalKey(Integer), per_page: Schema.optionalKey(Integer) };
|
|
455
|
+
export const listAccounts = get("listAccounts", "/accounts", {
|
|
456
|
+
query: { ...PageQuery, name: Schema.optionalKey(Schema.String) },
|
|
457
|
+
success: Schema.Unknown,
|
|
458
|
+
});
|
|
459
|
+
export const listZones = get("listZones", "/zones", {
|
|
460
|
+
query: {
|
|
461
|
+
...PageQuery,
|
|
462
|
+
name: Schema.optionalKey(Schema.String),
|
|
463
|
+
status: Schema.optionalKey(Schema.String),
|
|
464
|
+
"account.id": Schema.optionalKey(Schema.String),
|
|
465
|
+
},
|
|
466
|
+
success: Schema.Unknown,
|
|
467
|
+
});
|
|
468
|
+
export const graphqlQuery = post("graphqlQuery", "/graphql", {
|
|
469
|
+
payload: Schema.Struct({
|
|
470
|
+
query: Schema.String,
|
|
471
|
+
variables: Schema.optionalKey(Schema.Record(Schema.String, Schema.Unknown)),
|
|
472
|
+
operationName: Schema.optionalKey(Schema.String),
|
|
473
|
+
}),
|
|
474
|
+
success: Schema.Struct({
|
|
475
|
+
data: Schema.optionalKey(Schema.Unknown),
|
|
476
|
+
errors: Schema.optionalKey(Schema.Union([Schema.Array(Schema.Unknown), Schema.Null])),
|
|
477
|
+
}),
|
|
478
|
+
});
|
|
479
|
+
const typed = [getUser, verifyToken, verifyAccountToken, listAccounts, listZones];
|
|
480
|
+
const indexed = indexedVendor("cloudflare", cloudflareIndex, typed);
|
|
481
|
+
const OBJECT_DOWNLOADS = new Set([
|
|
482
|
+
"/accounts/{account_id}/r2/buckets/{bucket_name}/objects/{object_key}",
|
|
483
|
+
"/accounts/{account_id}/storage/kv/namespaces/{namespace_id}/values/{key_name}",
|
|
484
|
+
]);
|
|
485
|
+
/** Reviewed repairs for the pin's script label and caller-controlled object media. */
|
|
486
|
+
function cloudflareResponseMedia(status, operation, contentType = "") {
|
|
487
|
+
if (status < 200 || status > 299 || operation.flags?.["ownership"] === true)
|
|
488
|
+
return undefined;
|
|
489
|
+
const path = operation.template.replace(/:([^/]+)/g, "{$1}");
|
|
490
|
+
const type = contentType.split(";", 1)[0].trim().toLowerCase();
|
|
491
|
+
const json = /^(?:application\/json|[^/]+\/[^/]+\+json)$/.test(type);
|
|
492
|
+
if (operation.method === "GET" && OBJECT_DOWNLOADS.has(path))
|
|
493
|
+
return json ? undefined : "binary";
|
|
494
|
+
if (RESPONSE_MEDIA[`${operation.method} ${path}`]?.includes("string"))
|
|
495
|
+
return json ? undefined : type.startsWith("text/") ? "text" : "binary";
|
|
496
|
+
return responseMedia(RESPONSE_MEDIA, status, operation, contentType);
|
|
497
|
+
}
|
|
498
|
+
export const CloudflareApi = {
|
|
499
|
+
...indexed,
|
|
500
|
+
api: declareVendor("cloudflare", ...typed, graphqlQuery).api,
|
|
501
|
+
indexed: {
|
|
502
|
+
operations: [...indexed.indexed.operations, "POST /graphql"].sort(),
|
|
503
|
+
api: () => {
|
|
504
|
+
const rest = Object.values(indexed.indexed.api().groups["operations"].endpoints);
|
|
505
|
+
return declareVendor("cloudflare", ...rest, graphqlQuery).api;
|
|
506
|
+
},
|
|
507
|
+
},
|
|
508
|
+
textBodies: {
|
|
509
|
+
review: "Pinned text/XML/CSV/SSE/NDJSON operations and the three reviewed raw Worker-script aliases accept text. Ownership lookups remain JSON-only (#1000).",
|
|
510
|
+
forResponse: (status, operation, type = "") => cloudflareResponseMedia(status, operation, type) === "text" ? "text" : undefined,
|
|
511
|
+
},
|
|
512
|
+
binaryBodies: {
|
|
513
|
+
review: "Pinned binary operations, three raw Worker-script aliases, and GET KV/R2 objects (any stored media) retain bounded bytes with sent-secret checks. Ownership lookups remain JSON-only (#1000).",
|
|
514
|
+
forResponse: (status, operation, type) => cloudflareResponseMedia(status, operation, type) === "binary",
|
|
515
|
+
},
|
|
516
|
+
};
|
|
517
|
+
const canonical = (operation) => operation.template.replace(/\/:([^/]+)/g, "/{$1}");
|
|
518
|
+
export const safety = () => {
|
|
519
|
+
const reviewed = restSafety(SAFETY, cloudflareIndex);
|
|
520
|
+
return {
|
|
521
|
+
reviewed: {
|
|
522
|
+
...reviewed,
|
|
523
|
+
resolve: (method, path) => method === "POST" && path === "/graphql" ? { method, template: path } : reviewed.resolve(method, path),
|
|
524
|
+
redact: (body, operation) => isEnvelope(body)
|
|
525
|
+
? {
|
|
526
|
+
...record(SAFETY.redact(body, operation.method, canonical(operation))),
|
|
527
|
+
result: SAFETY.redact(body["result"], operation.method, canonical(operation)),
|
|
528
|
+
}
|
|
529
|
+
: SAFETY.redact(body, operation.method, canonical(operation)),
|
|
530
|
+
withholdsErrors: (operation) => operation.method === "POST" && operation.template === "/graphql" ? false : reviewed.withholdsErrors(operation),
|
|
531
|
+
},
|
|
532
|
+
sensitiveFields: { names: [], choice: "drop" },
|
|
533
|
+
};
|
|
534
|
+
};
|
|
535
|
+
export const result = (raw, answer) => Effect.suspend(() => {
|
|
536
|
+
let body = raw;
|
|
537
|
+
if (answer.bodyFormat === "text" && typeof body === "string") {
|
|
538
|
+
const type = (answer.headers.get("content-type") ?? "").toLowerCase();
|
|
539
|
+
body = /\b(?:x-ndjson|jsonl|stream\+json)\b/.test(type)
|
|
540
|
+
? body
|
|
541
|
+
.split("\n")
|
|
542
|
+
.filter((line) => line.trim() !== "")
|
|
543
|
+
.map((line) => {
|
|
544
|
+
try {
|
|
545
|
+
return JSON.parse(line);
|
|
546
|
+
}
|
|
547
|
+
catch {
|
|
548
|
+
return { line };
|
|
549
|
+
}
|
|
550
|
+
})
|
|
551
|
+
: body === ""
|
|
552
|
+
? null
|
|
553
|
+
: body;
|
|
554
|
+
}
|
|
555
|
+
if (answer.operation.flags?.["ownership"] !== true && isEnvelope(body) && body["success"] === false)
|
|
556
|
+
return Effect.fail(new StatusFailure({
|
|
557
|
+
status: answer.status === 200 ? 400 : answer.status,
|
|
558
|
+
headers: answer.headers,
|
|
559
|
+
body,
|
|
560
|
+
operation: answer.operation,
|
|
561
|
+
}));
|
|
562
|
+
return Effect.succeed(body ?? null);
|
|
563
|
+
});
|
|
564
|
+
export const authenticate = (options) => Effect.gen(function* () {
|
|
565
|
+
const values = yield* (yield* Credentials).values.pipe(Effect.mapError((error) => new Refusal({ error })));
|
|
566
|
+
if (options.auth.type === "globalApiKey") {
|
|
567
|
+
const email = values?.["email"]?.trim(), apiKey = values?.["apiKey"]?.trim();
|
|
568
|
+
if (!email || !apiKey)
|
|
569
|
+
return yield* Effect.fail(new Refusal({
|
|
570
|
+
error: new ConnectorCallError("auth_required", "No Cloudflare Global API Key and account email are configured for this connector. An operator must add both in this connection in the operator UI."),
|
|
571
|
+
}));
|
|
572
|
+
return { "X-Auth-Email": email, "X-Auth-Key": apiKey };
|
|
573
|
+
}
|
|
574
|
+
const token = values?.["value"]?.trim();
|
|
575
|
+
if (!token)
|
|
576
|
+
return yield* Effect.fail(new Refusal({
|
|
577
|
+
error: new ConnectorCallError("auth_required", "No Cloudflare API token is configured for this connector. An operator must add one in this connection in the operator UI."),
|
|
578
|
+
}));
|
|
579
|
+
return { Authorization: `Bearer ${token}` };
|
|
580
|
+
});
|
|
581
|
+
const attempt = (run) => Effect.tryPromise({ try: run, catch: (error) => error }).pipe(Effect.catch((error) => error instanceof ConnectorCallError ? Effect.fail(new Refusal({ error })) : Effect.die(error)));
|
|
582
|
+
const failure = (error) => error instanceof ConnectorCallError ? Effect.fail(new Refusal({ error })) : Effect.die(error);
|
|
583
|
+
const runtimes = new WeakMap();
|
|
584
|
+
export function cloudflareRuntime(connection, options) {
|
|
585
|
+
let runtime = runtimes.get(connection);
|
|
586
|
+
if (!runtime) {
|
|
587
|
+
runtime = makeRuntime(connection, options);
|
|
588
|
+
runtimes.set(connection, runtime);
|
|
589
|
+
}
|
|
590
|
+
return runtime;
|
|
591
|
+
}
|
|
592
|
+
function makeRuntime(connection, options) {
|
|
593
|
+
const operations = cloudflareIndex;
|
|
594
|
+
const { auth, pin } = options;
|
|
595
|
+
const pinnedAccounts = pin?.accountIds ? new Set(pin.accountIds) : undefined;
|
|
596
|
+
const pinnedZones = pin?.zoneIds ? new Set(pin.zoneIds) : undefined;
|
|
597
|
+
// --- Pin enforcement (default deny) ---------------------------------------
|
|
598
|
+
/** Zone id → owning account id, learned from a GET /zones/{id} read or a zone list. */
|
|
599
|
+
const owners = new Map();
|
|
600
|
+
/** Times of internal ownership reads, bounded per rolling window. */
|
|
601
|
+
const lookups = [];
|
|
602
|
+
const lookupKey = {};
|
|
603
|
+
const pinRefusal = (what) => {
|
|
604
|
+
throw new ConnectorCallError("provider_permission_denied", `${what} is outside this connector's pin. The deployment pins it to ${[
|
|
605
|
+
pinnedAccounts ? `accounts ${[...pinnedAccounts].join(", ")}` : "",
|
|
606
|
+
pinnedZones ? `zones ${[...pinnedZones].join(", ")}` : "",
|
|
607
|
+
]
|
|
608
|
+
.filter(Boolean)
|
|
609
|
+
.join(" and ")}; a different account or zone needs its own connector.`, { retryable: false });
|
|
610
|
+
};
|
|
611
|
+
const assertAccount = (account) => {
|
|
612
|
+
if (!pinnedAccounts?.has(account))
|
|
613
|
+
pinRefusal(`Account ${account}`);
|
|
614
|
+
};
|
|
615
|
+
/**
|
|
616
|
+
* Charge one internal ownership read: at most `MAX_LOOKUPS_PER_CALL` per
|
|
617
|
+
* tool call and `MAX_LOOKUPS_PER_WINDOW` per five minutes per connector, so
|
|
618
|
+
* one call cannot spend the user's shared request limit on pin checks.
|
|
619
|
+
*/
|
|
620
|
+
const chargeLookup = (ctx) => {
|
|
621
|
+
const state = callMemo(ctx, lookupKey, () => ({ used: 0 }));
|
|
622
|
+
const used = state.used;
|
|
623
|
+
if (used >= MAX_LOOKUPS_PER_CALL) {
|
|
624
|
+
throw new ConnectorCallError("invalid_args", `This call names more than ${MAX_LOOKUPS_PER_CALL} zones whose account the pin must check. Name fewer zones per call, or list them in pin.zoneIds.`);
|
|
625
|
+
}
|
|
626
|
+
const now = Date.now();
|
|
627
|
+
while (lookups.length > 0 && lookups[0] <= now - LOOKUP_WINDOW_MS)
|
|
628
|
+
lookups.shift();
|
|
629
|
+
if (lookups.length >= MAX_LOOKUPS_PER_WINDOW) {
|
|
630
|
+
throw new ConnectorCallError("rate_limited", "This connector's zone-ownership checks reached their per-window budget.", { retryAfterMs: Math.max(1_000, lookups[0] + LOOKUP_WINDOW_MS - now) });
|
|
631
|
+
}
|
|
632
|
+
lookups.push(now);
|
|
633
|
+
state.used = used + 1;
|
|
634
|
+
return state.used;
|
|
635
|
+
};
|
|
636
|
+
const assertZone = (zone) => Effect.gen(function* () {
|
|
637
|
+
if (pinnedZones?.has(zone))
|
|
638
|
+
return;
|
|
639
|
+
yield* checked(() => {
|
|
640
|
+
if (!pinnedAccounts)
|
|
641
|
+
pinRefusal(`Zone ${zone}`);
|
|
642
|
+
});
|
|
643
|
+
let owner = owners.get(zone);
|
|
644
|
+
if (owner === undefined) {
|
|
645
|
+
const view = yield* Effect.serviceOption(ContextView);
|
|
646
|
+
const ctx = Option.isSome(view) ? view.value : undefined;
|
|
647
|
+
const lookup = yield* checked(() => {
|
|
648
|
+
if (!ctx)
|
|
649
|
+
throw new Error("Ownership lookup has no call context.");
|
|
650
|
+
return chargeLookup(ctx);
|
|
651
|
+
});
|
|
652
|
+
yield* (yield* Budget).charge.pipe(Effect.catch(failure));
|
|
653
|
+
const reply = yield* connection
|
|
654
|
+
.sendPath({ method: "GET", path: `/zones/${encodeURIComponent(zone)}`, template: "/zones/{zone_id}" }, { effect: "read", step: `lookup${lookup}`, flags: { ownership: true } })
|
|
655
|
+
.pipe(Effect.catch((error) => {
|
|
656
|
+
if (error._tag === "UnreadableBody")
|
|
657
|
+
return Effect.succeed({ status: 200, data: undefined });
|
|
658
|
+
if (error._tag !== "ResponseTooLarge")
|
|
659
|
+
return Effect.fail(error);
|
|
660
|
+
return Dispatches.use((dispatches) => {
|
|
661
|
+
const own = dispatches.filter((dispatch) => dispatch.operation?.flags?.["ownership"] === true).at(-1);
|
|
662
|
+
const length = Number(own?.headers?.get("content-length"));
|
|
663
|
+
// Legacy pin reads swallowed a streamed-body decode failure, but not a declared ceiling.
|
|
664
|
+
return own?.status === 200 && !(length > MAX_RESPONSE_BYTES)
|
|
665
|
+
? Effect.succeed({ status: 200, data: undefined })
|
|
666
|
+
: Effect.fail(error);
|
|
667
|
+
});
|
|
668
|
+
}));
|
|
669
|
+
owner = text(record(record(record(reply.data)["result"])["account"])["id"]) ?? "";
|
|
670
|
+
if (owner)
|
|
671
|
+
owners.set(zone, owner);
|
|
672
|
+
}
|
|
673
|
+
yield* checked(() => {
|
|
674
|
+
if (!owner || !pinnedAccounts?.has(owner))
|
|
675
|
+
pinRefusal(`Zone ${zone}`);
|
|
676
|
+
});
|
|
677
|
+
});
|
|
678
|
+
const admit = (call) => Effect.gen(function* () {
|
|
679
|
+
if (call.method !== "GET" &&
|
|
680
|
+
call.method !== "HEAD" &&
|
|
681
|
+
!READ_POSTS.some(([method, path]) => method === call.method && path === call.op.path)) {
|
|
682
|
+
yield* checked(() => refuseWithheldLinks([call.query, call.path], "Cloudflare"));
|
|
683
|
+
if (call.body instanceof RawUpload)
|
|
684
|
+
yield* attempt(() => refuseWithheldUpload(call.body.body, "Cloudflare"));
|
|
685
|
+
else
|
|
686
|
+
yield* checked(() => refuseWithheldLinks(call.body, "Cloudflare"));
|
|
687
|
+
if (call.method === "PUT" &&
|
|
688
|
+
call.op.path === "/accounts/{account_id}/storage/kv/namespaces/{namespace_id}/bulk" &&
|
|
689
|
+
Array.isArray(call.body)) {
|
|
690
|
+
for (const entry of call.body)
|
|
691
|
+
if (record(entry)["base64"] === true)
|
|
692
|
+
yield* attempt(() => refuseWithheldBase64(record(entry)["value"], "Cloudflare", 25 * 1024 * 1024));
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
if (!pin)
|
|
696
|
+
return;
|
|
697
|
+
const { accounts, zones, template } = scopeIds(call);
|
|
698
|
+
yield* checked(() => {
|
|
699
|
+
const scoped = template.some((part, index) => part.startsWith("{") && (template[index - 1] === "accounts" || template[index - 1] === "zones"));
|
|
700
|
+
if (!scoped) {
|
|
701
|
+
const key = `${call.op.method} ${call.op.path}`;
|
|
702
|
+
if (!PIN_SAFE_UNSCOPED.has(key) && !(call.op.method === "GET" && template[0] === "radar")) {
|
|
703
|
+
const reason = PIN_REFUSED_UNSCOPED[template[0] ?? ""] ?? "names no account or zone this pin can verify";
|
|
704
|
+
throw new ConnectorCallError("provider_permission_denied", `${key} ${reason}, so a pinned connector refuses it.`, { retryable: false });
|
|
705
|
+
}
|
|
706
|
+
if (key === "POST /zones") {
|
|
707
|
+
const account = text(record(record(call.body)["account"])["id"]);
|
|
708
|
+
if (!account)
|
|
709
|
+
invalid("A pinned connector creates a zone only in a pinned account: set body.account.id.");
|
|
710
|
+
assertAccount(account);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
for (const account of accounts)
|
|
714
|
+
assertAccount(account);
|
|
715
|
+
});
|
|
716
|
+
for (const zone of zones)
|
|
717
|
+
yield* assertZone(zone);
|
|
718
|
+
});
|
|
719
|
+
/** A pinned connector's account and zone lists show only what the pin admits. */
|
|
720
|
+
const filterListed = (op, result) => {
|
|
721
|
+
if (!pin || op.method !== "GET" || !Array.isArray(result))
|
|
722
|
+
return result;
|
|
723
|
+
if (op.path === "/accounts")
|
|
724
|
+
return result.filter((item) => pinnedAccounts?.has(String(record(item)["id"])));
|
|
725
|
+
if (op.path === "/memberships") {
|
|
726
|
+
return result.filter((item) => pinnedAccounts?.has(String(record(record(item)["account"])["id"])));
|
|
727
|
+
}
|
|
728
|
+
if (op.path === "/zones") {
|
|
729
|
+
return result.filter((item) => {
|
|
730
|
+
const zone = record(item);
|
|
731
|
+
const id = String(zone["id"]);
|
|
732
|
+
const account = String(record(zone["account"])["id"]);
|
|
733
|
+
if (account && id)
|
|
734
|
+
owners.set(id, account);
|
|
735
|
+
return pinnedZones?.has(id) || pinnedAccounts?.has(account);
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
return result;
|
|
739
|
+
};
|
|
740
|
+
// --- Framing ---------------------------------------------------------------
|
|
741
|
+
const encode = (call) => {
|
|
742
|
+
const query = queryOf(call);
|
|
743
|
+
const headers = { ...call.headers };
|
|
744
|
+
if (call.body instanceof RawUpload) {
|
|
745
|
+
if (call.body.contentType)
|
|
746
|
+
headers["Content-Type"] = call.body.contentType;
|
|
747
|
+
return { query, headers, rawBody: call.body.body };
|
|
748
|
+
}
|
|
749
|
+
if (call.body === undefined)
|
|
750
|
+
return { query, headers };
|
|
751
|
+
const type = (operations.bodyType(call.op) ?? "application/json").toLowerCase();
|
|
752
|
+
if (/^text\/|javascript/.test(type)) {
|
|
753
|
+
if (typeof call.body !== "string")
|
|
754
|
+
invalid(`${call.op.method} ${call.op.path} takes a ${type} body: send it as a string.`);
|
|
755
|
+
return { query, headers: { ...headers, "Content-Type": type }, rawBody: call.body };
|
|
756
|
+
}
|
|
757
|
+
if (/ndjson|jsonl/.test(type)) {
|
|
758
|
+
const lines = typeof call.body === "string"
|
|
759
|
+
? call.body
|
|
760
|
+
: Array.isArray(call.body)
|
|
761
|
+
? call.body.map((item) => JSON.stringify(item)).join("\n")
|
|
762
|
+
: invalid(`${call.op.method} ${call.op.path} takes newline-delimited JSON: send a list of objects.`);
|
|
763
|
+
return { query, headers: { ...headers, "Content-Type": type }, rawBody: lines };
|
|
764
|
+
}
|
|
765
|
+
if (type === "application/x-www-form-urlencoded") {
|
|
766
|
+
const form = new URLSearchParams();
|
|
767
|
+
for (const [name, value] of Object.entries(record(call.body))) {
|
|
768
|
+
if (value !== undefined && value !== null)
|
|
769
|
+
form.append(name, typeof value === "string" ? value : JSON.stringify(value));
|
|
770
|
+
}
|
|
771
|
+
return { query, headers: { ...headers, "Content-Type": type }, rawBody: form.toString() };
|
|
772
|
+
}
|
|
773
|
+
if (typeof call.body === "string")
|
|
774
|
+
invalid(`${call.op.method} ${call.op.path} takes a JSON body, not a string.`);
|
|
775
|
+
if (type.endsWith("+json"))
|
|
776
|
+
return { query, headers: { ...headers, "Content-Type": type }, rawBody: JSON.stringify(call.body) };
|
|
777
|
+
return { query, headers, body: call.body };
|
|
778
|
+
};
|
|
779
|
+
/** Fill configured default ids into `{account_id}` and `{zone_id}` placeholders. */
|
|
780
|
+
const fillDefaults = (path) => {
|
|
781
|
+
if (!options.accountId && !options.zoneId)
|
|
782
|
+
return path;
|
|
783
|
+
const parts = path.split("/");
|
|
784
|
+
return parts
|
|
785
|
+
.map((part, position) => {
|
|
786
|
+
const previous = parts[position - 1];
|
|
787
|
+
if (options.accountId && previous === "accounts" && /^\{(account_id|account_tag|accountId)\}$/.test(part)) {
|
|
788
|
+
return encodeURIComponent(options.accountId);
|
|
789
|
+
}
|
|
790
|
+
if (options.zoneId && previous === "zones" && /^\{(zone_id|zone_identifier|zoneId)\}$/.test(part)) {
|
|
791
|
+
return encodeURIComponent(options.zoneId);
|
|
792
|
+
}
|
|
793
|
+
return part;
|
|
794
|
+
})
|
|
795
|
+
.join("/");
|
|
796
|
+
};
|
|
797
|
+
const scope = (call) => {
|
|
798
|
+
// NDJSON operations take a list of records; frame it before validation,
|
|
799
|
+
// so the index checks the string the vendor receives.
|
|
800
|
+
const type = (operations.bodyType(call.op) ?? "").toLowerCase();
|
|
801
|
+
if (/ndjson|jsonl/.test(type)) {
|
|
802
|
+
let rows;
|
|
803
|
+
if (Array.isArray(call.body))
|
|
804
|
+
rows = call.body;
|
|
805
|
+
else if (typeof call.body === "string") {
|
|
806
|
+
try {
|
|
807
|
+
rows = call.body
|
|
808
|
+
.split(/\r?\n/)
|
|
809
|
+
.filter((line) => line.trim())
|
|
810
|
+
.map((line) => JSON.parse(line));
|
|
811
|
+
}
|
|
812
|
+
catch {
|
|
813
|
+
invalid(`${call.op.method} ${call.op.path} takes valid newline-delimited JSON objects.`);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
if (rows) {
|
|
817
|
+
if (!rows.every((item) => typeof item === "object" && item !== null && !Array.isArray(item)))
|
|
818
|
+
invalid(`${call.op.method} ${call.op.path} takes newline-delimited JSON: send a list of objects.`);
|
|
819
|
+
for (const row of rows)
|
|
820
|
+
refuseWithheldValues(row, "Cloudflare");
|
|
821
|
+
if (Array.isArray(call.body))
|
|
822
|
+
call = { ...call, body: rows.map((item) => JSON.stringify(item)).join("\n") };
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
for (const [name, value] of Object.entries(call.headers ?? {})) {
|
|
826
|
+
if (!/^\/accounts\/\{[^/]+\}\/r2\//.test(call.op.path)) {
|
|
827
|
+
invalid(`${name} applies only to R2 operations under /accounts/{account_id}/r2/.`);
|
|
828
|
+
}
|
|
829
|
+
if (!HEADER_VALUES[name]?.includes(value)) {
|
|
830
|
+
invalid(`${name} must be one of ${HEADER_VALUES[name]?.join(", ") ?? "its documented values"}.`);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
return call;
|
|
834
|
+
};
|
|
835
|
+
/** Argument- and auth-dependent refusals; the table's own run in the generic tools. */
|
|
836
|
+
const refuse = (call) => {
|
|
837
|
+
const { method, op } = call;
|
|
838
|
+
if (method === "GET")
|
|
839
|
+
return undefined;
|
|
840
|
+
if (auth === "globalApiKey" && /^\/(?:user|memberships)(?:\/|$)/.test(op.path)) {
|
|
841
|
+
return "A Global API Key connector never writes user-level settings or memberships: the key is the user's own identity. Make that change in the Cloudflare dashboard.";
|
|
842
|
+
}
|
|
843
|
+
return undefined;
|
|
844
|
+
};
|
|
845
|
+
/** Next page: page numbers from result_info, or a cursor. */
|
|
846
|
+
const page = (body, call) => {
|
|
847
|
+
if (!isEnvelope(body))
|
|
848
|
+
return undefined;
|
|
849
|
+
const info = record(body["result_info"]);
|
|
850
|
+
if (Object.keys(info).length === 0)
|
|
851
|
+
return undefined;
|
|
852
|
+
const rawCursor = text(info["cursor"]) ?? text(record(info["cursors"])["after"]);
|
|
853
|
+
// Never echo or offer a modified cursor: it would not address the next page.
|
|
854
|
+
const cursor = rawCursor === undefined || withholdEmbeddedUrlsIn(rawCursor) === rawCursor ? rawCursor : undefined;
|
|
855
|
+
if (cursor !== undefined || "cursor" in info || "cursors" in info) {
|
|
856
|
+
const accepted = operations.contract(call.op).parameters.map((parameter) => parameter.name);
|
|
857
|
+
const param = accepted.includes("cursor") ? "cursor" : accepted.includes("after") ? "after" : "cursor";
|
|
858
|
+
const truncated = info["is_truncated"];
|
|
859
|
+
const hasMore = cursor !== undefined && truncated !== false && truncated !== "false";
|
|
860
|
+
return { hasMore, ...(hasMore ? { next: cursor, param } : {}) };
|
|
861
|
+
}
|
|
862
|
+
const current = typeof info["page"] === "number" ? info["page"] : undefined;
|
|
863
|
+
if (current === undefined)
|
|
864
|
+
return undefined;
|
|
865
|
+
const perPage = typeof info["per_page"] === "number" ? info["per_page"] : 0;
|
|
866
|
+
const totalPages = typeof info["total_pages"] === "number" ? info["total_pages"] : undefined;
|
|
867
|
+
const totalCount = typeof info["total_count"] === "number" ? info["total_count"] : undefined;
|
|
868
|
+
const count = typeof info["count"] === "number" ? info["count"] : 0;
|
|
869
|
+
const hasMore = totalPages !== undefined
|
|
870
|
+
? current < totalPages
|
|
871
|
+
: totalCount !== undefined
|
|
872
|
+
? current * perPage < totalCount
|
|
873
|
+
: perPage > 0 && count >= perPage;
|
|
874
|
+
return { hasMore, ...(hasMore ? { next: String(current + 1), param: "page" } : {}) };
|
|
875
|
+
};
|
|
876
|
+
const checkServer = (server) => server === undefined ? undefined : `This operation is served from ${server}, which this connector does not reach.`;
|
|
877
|
+
const call = (method, path, input = {}) => {
|
|
878
|
+
path = fillDefaults(path);
|
|
879
|
+
const { op, params } = operations.resolve(method, path);
|
|
880
|
+
return scope({
|
|
881
|
+
method,
|
|
882
|
+
path,
|
|
883
|
+
op,
|
|
884
|
+
params,
|
|
885
|
+
query: input.query ?? {},
|
|
886
|
+
body: input.body,
|
|
887
|
+
...(input.headers ? { headers: input.headers } : {}),
|
|
888
|
+
});
|
|
889
|
+
};
|
|
890
|
+
const send = (call, envelope = false) => checked(() => {
|
|
891
|
+
const refusal = checkServer(call.op.server);
|
|
892
|
+
if (refusal)
|
|
893
|
+
invalid(refusal);
|
|
894
|
+
}).pipe(Effect.andThen(admit(call)), Effect.andThen(checked(() => {
|
|
895
|
+
const framing = call.op.path === "/graphql" ? { body: call.body } : encode(call);
|
|
896
|
+
const query = Object.entries(framing.query ?? {}).flatMap(([name, value]) => (Array.isArray(value) ? value : [value]).map((value) => [name, String(value)]));
|
|
897
|
+
return {
|
|
898
|
+
method: call.method,
|
|
899
|
+
path: call.path,
|
|
900
|
+
template: call.op.path,
|
|
901
|
+
query,
|
|
902
|
+
...(framing.headers
|
|
903
|
+
? {
|
|
904
|
+
headers: Object.fromEntries(Object.entries(framing.headers).filter((entry) => entry[1] !== undefined)),
|
|
905
|
+
}
|
|
906
|
+
: {}),
|
|
907
|
+
...(framing.rawBody !== undefined
|
|
908
|
+
? { rawBody: framing.rawBody }
|
|
909
|
+
: framing.body !== undefined
|
|
910
|
+
? { json: JSON.stringify(framing.body) }
|
|
911
|
+
: {}),
|
|
912
|
+
};
|
|
913
|
+
})), Effect.flatMap((request) => connection.sendPath(request)), Effect.flatMap((reply) => checked(() => {
|
|
914
|
+
const body = reply.data;
|
|
915
|
+
const result = isEnvelope(body) ? filterListed(call.op, body["result"]) : body;
|
|
916
|
+
let info = isEnvelope(body) ? withholdEmbeddedUrlsIn(body["result_info"]) : undefined;
|
|
917
|
+
if (envelope &&
|
|
918
|
+
isEnvelope(body) &&
|
|
919
|
+
info !== undefined &&
|
|
920
|
+
result !== body["result"] &&
|
|
921
|
+
Array.isArray(result)) {
|
|
922
|
+
// Validate before dropping estate-wide counts; retain only page-level
|
|
923
|
+
// continuation metadata, never an unfiltered total for the pinned scope.
|
|
924
|
+
cloudflarePage.next(body, { path: call.path, query: call.query, headers: {} });
|
|
925
|
+
const original = record(info);
|
|
926
|
+
const totalPages = original["total_pages"] ??
|
|
927
|
+
(typeof original["total_count"] === "number" &&
|
|
928
|
+
typeof original["per_page"] === "number" &&
|
|
929
|
+
original["per_page"] > 0
|
|
930
|
+
? Math.ceil(original["total_count"] / original["per_page"])
|
|
931
|
+
: undefined);
|
|
932
|
+
info = {
|
|
933
|
+
page: original["page"],
|
|
934
|
+
per_page: original["per_page"],
|
|
935
|
+
count: result.length,
|
|
936
|
+
total_pages: totalPages,
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
const data = isEnvelope(body) && envelope
|
|
940
|
+
? { ...body, ...(info !== undefined ? { result_info: info } : {}), result }
|
|
941
|
+
: result;
|
|
942
|
+
const next = call.op.path === "/graphql" ? undefined : page(body, call);
|
|
943
|
+
return { status: reply.status, data, ...(next ? { page: next } : {}) };
|
|
944
|
+
})));
|
|
945
|
+
const read = (method, path, input = {}) => checked(() => call(method, path, input)).pipe(Effect.flatMap(send));
|
|
946
|
+
const verify = () => {
|
|
947
|
+
if (auth === "globalApiKey")
|
|
948
|
+
return read("GET", "/user").pipe(Effect.map(({ data }) => {
|
|
949
|
+
const user = record(data);
|
|
950
|
+
return { auth, status: "active", id: user["id"] ?? null, email: user["email"] ?? null, scope: "user" };
|
|
951
|
+
}));
|
|
952
|
+
return read("GET", "/user/tokens/verify").pipe(Effect.map(({ data }) => {
|
|
953
|
+
const token = record(data);
|
|
954
|
+
return {
|
|
955
|
+
auth,
|
|
956
|
+
status: token["status"] ?? null,
|
|
957
|
+
id: token["id"] ?? null,
|
|
958
|
+
expiresOn: token["expires_on"] ?? null,
|
|
959
|
+
scope: "user",
|
|
960
|
+
};
|
|
961
|
+
}), Effect.catch((error) => Effect.gen(function* () {
|
|
962
|
+
const account = options.accountId ?? (pin?.accountIds?.length === 1 ? pin.accountIds[0] : undefined);
|
|
963
|
+
const mapped = toConnectorCallError(error, cloudflareErrors, {
|
|
964
|
+
classification: yield* Classification,
|
|
965
|
+
dispatches: yield* Dispatches,
|
|
966
|
+
withholds: connection.safety.withholdsErrors,
|
|
967
|
+
signal: yield* Signal,
|
|
968
|
+
});
|
|
969
|
+
if (!account || mapped.code !== "auth_required")
|
|
970
|
+
return yield* Effect.fail(error);
|
|
971
|
+
const { data } = yield* read("GET", `/accounts/${encodeURIComponent(account)}/tokens/verify`);
|
|
972
|
+
const token = record(data);
|
|
973
|
+
return {
|
|
974
|
+
auth,
|
|
975
|
+
status: token["status"] ?? null,
|
|
976
|
+
id: token["id"] ?? null,
|
|
977
|
+
expiresOn: token["expires_on"] ?? null,
|
|
978
|
+
scope: "account",
|
|
979
|
+
accountId: account,
|
|
980
|
+
};
|
|
981
|
+
})));
|
|
982
|
+
};
|
|
983
|
+
return {
|
|
984
|
+
send,
|
|
985
|
+
sendEnvelope: (call) => send(call, true),
|
|
986
|
+
read,
|
|
987
|
+
call,
|
|
988
|
+
verify,
|
|
989
|
+
assertAccount,
|
|
990
|
+
assertZone,
|
|
991
|
+
refuse,
|
|
992
|
+
fillDefaults,
|
|
993
|
+
scope,
|
|
994
|
+
checkServer,
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
export function describeProbe(data, options) {
|
|
998
|
+
const verified = record(data);
|
|
999
|
+
if (options.auth.type === "globalApiKey")
|
|
1000
|
+
return { ok: true, message: `Global API Key verified for ${String(verified["email"])}.` };
|
|
1001
|
+
return verified["status"] === "active"
|
|
1002
|
+
? {
|
|
1003
|
+
ok: true,
|
|
1004
|
+
message: `Token verified: active${verified["scope"] === "account" ? ` (account token for ${String(verified["accountId"])})` : ""}.`,
|
|
1005
|
+
}
|
|
1006
|
+
: { ok: false, message: `Token status is "${String(verified["status"])}".` };
|
|
1007
|
+
}
|
|
1008
|
+
function idsOf(value, path) {
|
|
1009
|
+
if (value === undefined)
|
|
1010
|
+
return undefined;
|
|
1011
|
+
if (!Array.isArray(value) ||
|
|
1012
|
+
value.length === 0 ||
|
|
1013
|
+
!value.every((item) => typeof item === "string" && item.trim() !== "")) {
|
|
1014
|
+
throw new Error(`${path} must be a non-empty list of ids.`);
|
|
1015
|
+
}
|
|
1016
|
+
return value.map((item) => item.trim());
|
|
1017
|
+
}
|
|
1018
|
+
function optionalId(value, path) {
|
|
1019
|
+
if (value === undefined)
|
|
1020
|
+
return undefined;
|
|
1021
|
+
if (typeof value !== "string" || value.trim() === "" || /[/?#\s]/.test(value.trim())) {
|
|
1022
|
+
throw new Error(`${path} must be a Cloudflare id.`);
|
|
1023
|
+
}
|
|
1024
|
+
return value.trim();
|
|
1025
|
+
}
|
|
1026
|
+
/** Normalized construction-only routing and confinement settings. */
|
|
1027
|
+
export function settings(options) {
|
|
1028
|
+
const auth = options.auth.type;
|
|
1029
|
+
if (auth !== "apiToken" && auth !== "globalApiKey")
|
|
1030
|
+
throw new Error("remoteMcp() with ConnectaConfig.classification for hosted MCP connections; use a distinct id beside the API connector.");
|
|
1031
|
+
const maxConcurrency = options.maxConcurrency ?? 6;
|
|
1032
|
+
if (!Number.isInteger(maxConcurrency) || maxConcurrency < 1) {
|
|
1033
|
+
throw new Error(`maxConcurrency must be a positive integer.`);
|
|
1034
|
+
}
|
|
1035
|
+
let pin;
|
|
1036
|
+
if (options.pin !== undefined) {
|
|
1037
|
+
const accountIds = idsOf(options.pin.accountIds, "pin.accountIds");
|
|
1038
|
+
const zoneIds = idsOf(options.pin.zoneIds, "pin.zoneIds");
|
|
1039
|
+
if (!accountIds && !zoneIds)
|
|
1040
|
+
throw new Error(`pin names accountIds, zoneIds, or both.`);
|
|
1041
|
+
pin = {
|
|
1042
|
+
...(accountIds ? { accountIds } : {}),
|
|
1043
|
+
...(zoneIds ? { zoneIds } : {}),
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
if (auth === "globalApiKey") {
|
|
1047
|
+
const unpinned = options.unpinned;
|
|
1048
|
+
if (unpinned !== undefined && unpinned !== true) {
|
|
1049
|
+
throw new Error(`unpinned must be true when set.`);
|
|
1050
|
+
}
|
|
1051
|
+
if (pin && unpinned)
|
|
1052
|
+
throw new Error(`takes pin or unpinned: true, not both.`);
|
|
1053
|
+
if (!pin && !unpinned) {
|
|
1054
|
+
throw new Error(`requires pin: { accountIds, zoneIds } with globalApiKey auth, or unpinned: true: a Global API Key reaches every account and zone its user can.`);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
const accountId = optionalId(options.accountId, "accountId");
|
|
1058
|
+
const zoneId = optionalId(options.zoneId, "zoneId");
|
|
1059
|
+
if (pin && accountId && pin.accountIds && !pin.accountIds.includes(accountId)) {
|
|
1060
|
+
throw new Error(`accountId ${accountId} is outside its pin.`);
|
|
1061
|
+
}
|
|
1062
|
+
if (pin && zoneId && pin.zoneIds && !pin.accountIds && !pin.zoneIds.includes(zoneId)) {
|
|
1063
|
+
throw new Error(`zoneId ${zoneId} is outside its pin.`);
|
|
1064
|
+
}
|
|
1065
|
+
return { auth, baseUrl: options.baseUrl?.trim() || CLOUDFLARE_API_BASE, accountId, zoneId, pin };
|
|
1066
|
+
}
|