@zackbart/connecta 0.34.1 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +363 -0
- package/README.md +62 -106
- package/SECURITY.md +3 -2
- package/bin/connecta.mjs +41 -16
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.d.ts +2 -1
- package/dist/access-tokens.js +66 -30
- package/dist/activity.d.ts +1 -1
- package/dist/activity.js +2 -0
- package/dist/agent-config.d.ts +23 -0
- package/dist/agent-config.js +59 -0
- package/dist/agent-description.js +16 -0
- package/dist/agent-identity.js +4 -4
- package/dist/agent-observation.js +39 -0
- package/dist/auth/clerk-sdk/client.d.ts +2 -0
- package/dist/auth/clerk-sdk/generated.js +554 -563
- package/dist/auth/clerk-transport.js +70 -3
- package/dist/auth/clerk.d.ts +13 -0
- package/dist/auth/clerk.js +653 -246
- package/dist/auth/cloudflare-access.js +46 -42
- package/dist/auth/downstream-client-metadata.js +2 -2
- package/dist/auth/downstream-oauth.js +2051 -1160
- package/dist/auth/downstream-revocation.js +88 -88
- package/dist/auth/native-call.js +20 -0
- package/dist/auth/native.js +184 -0
- package/dist/auth/oauth-start-reset.js +11 -5
- package/dist/auth/oauth-token-response.js +121 -102
- package/dist/auth/oauth-v2-migration.js +97 -78
- package/dist/auth/renewable-grant.js +10 -0
- package/dist/auth/static-oauth.js +663 -271
- package/dist/auth/vercel-agent.js +266 -0
- package/dist/auth-admission.js +204 -0
- package/dist/auth-elicitation.js +350 -241
- package/dist/authorization-handoff.js +164 -90
- package/dist/call-admission-context.js +3 -10
- package/dist/call-admission.js +2 -0
- package/dist/call-classification.js +9 -0
- package/dist/call-recovery.js +32 -13
- package/dist/canonical-json.js +8 -0
- package/dist/catalog-cache.js +369 -213
- package/dist/catalog-service.js +659 -530
- package/dist/catalog.js +5 -1
- package/dist/concurrency.js +0 -25
- package/dist/config-defaults.d.ts +1 -0
- package/dist/config-defaults.js +1 -0
- package/dist/config-schema.d.ts +3 -1
- package/dist/config-schema.js +13 -3
- package/dist/config.d.ts +40 -6
- package/dist/config.js +40 -26
- package/dist/connector-caller.d.ts +4 -0
- package/dist/connector-caller.js +17 -4
- package/dist/connectors/api-connector.d.ts +12 -7
- package/dist/connectors/api-connector.js +142 -56
- package/dist/connectors/guarded-fetch.d.ts +51 -17
- package/dist/connectors/guarded-fetch.js +215 -121
- package/dist/connectors/negotiation-cache.js +20 -10
- package/dist/connectors/remote-mcp.d.ts +10 -30
- package/dist/connectors/remote-mcp.js +955 -563
- package/dist/d1.js +10 -243
- package/dist/derived-context.js +5 -0
- package/dist/describe-config.d.ts +3 -1
- package/dist/describe-config.js +2 -0
- package/dist/downstream-input.js +347 -260
- package/dist/errors.d.ts +87 -6
- package/dist/errors.js +114 -4
- package/dist/execute.js +87 -50
- package/dist/executor-admission.js +3 -0
- package/dist/executors/quickjs.js +163 -112
- package/dist/guarded-transport.js +50 -0
- package/dist/guest-runtime.js +1 -1
- package/dist/http/client.js +439 -0
- package/dist/http/errors.js +558 -0
- package/dist/http/fields.js +49 -0
- package/dist/http/hatch-paging.js +318 -0
- package/dist/http/hatch.js +1279 -0
- package/dist/http/json-schema.js +267 -0
- package/dist/http/node-transport.js +241 -0
- package/dist/http/oauth.js +162 -0
- package/dist/http/paging.js +228 -0
- package/dist/http/provider.js +221 -0
- package/dist/http/safety.js +82 -0
- package/dist/http/services.js +79 -0
- package/dist/http/tools.js +383 -0
- package/dist/http/vendor-index.js +138 -0
- package/dist/http/vendor.js +448 -0
- package/dist/http/write-request.js +66 -0
- package/dist/http-policy.d.ts +11 -0
- package/dist/http-policy.js +45 -0
- package/dist/inbound-credential.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -0
- package/dist/invocation-auth.js +30 -19
- package/dist/invocation.js +218 -85
- package/dist/mcp-result.js +0 -4
- package/dist/membership.d.ts +4 -0
- package/dist/meta-output.js +1 -1
- package/dist/meta-tools.js +531 -406
- package/dist/native-recovery.js +52 -0
- package/dist/native-tools.js +382 -0
- package/dist/node.d.ts +4 -3
- package/dist/node.js +45 -8
- package/dist/oauth-handoff.js +177 -104
- package/dist/oauth-partition.js +6 -20
- package/dist/oauth-sealing.js +6 -7
- package/dist/operation-vocabulary.js +101 -0
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +17 -4
- package/dist/operator-ui/agents.d.ts +5 -4
- package/dist/operator-ui/agents.js +17 -7
- package/dist/operator-ui/config-summary.js +5 -4
- package/dist/operator-ui/connection-map.js +7 -5
- package/dist/operator-ui/contract.d.ts +7 -3
- package/dist/operator-ui/endpoints.js +4 -13
- package/dist/operator-ui/fix-prompts.d.ts +1 -1
- package/dist/operator-ui/fix-prompts.js +0 -8
- package/dist/operator-ui/generated.js +3 -3
- package/dist/operator-ui/model.d.ts +2 -3
- package/dist/operator-ui/overview.js +5 -2
- package/dist/operator-ui/setup-commands.js +0 -4
- package/dist/operator-ui/view.d.ts +2 -25
- package/dist/operator-ui/view.js +2 -76
- package/dist/program-writes.js +47 -26
- package/dist/provider-call-facts.d.ts +115 -0
- package/dist/provider-call-facts.js +182 -0
- package/dist/provider-call.d.ts +112 -0
- package/dist/provider-call.js +271 -0
- package/dist/provider.d.ts +65 -31
- package/dist/provider.js +116 -39
- package/dist/providers/_shared/google/hatch.js +91 -0
- package/dist/providers/_shared/google/kit.js +537 -0
- package/dist/providers/_shared/google/result-size.js +15 -2
- package/dist/providers/_shared/google/service-account.d.ts +2 -1
- package/dist/providers/_shared/google/service-account.js +8 -4
- package/dist/providers/_shared/google/workspace.d.ts +6 -210
- package/dist/providers/_shared/google/workspace.js +6 -425
- package/dist/providers/_shared/rest/compact-index.js +86 -0
- package/dist/providers/_shared/rest/dispatch.js +3 -23
- package/dist/providers/_shared/rest/kit.js +546 -0
- package/dist/providers/_shared/rest/operation-index.js +230 -14
- package/dist/providers/_shared/rest/response-media.js +11 -0
- package/dist/providers/_shared/rest/tools.js +101 -78
- package/dist/providers/_shared/rest/value-safety.js +353 -195
- package/dist/providers/_shared/sensitive-fields.js +27 -0
- package/dist/providers/breeze/api.js +584 -0
- package/dist/providers/breeze/index.d.ts +1 -3
- package/dist/providers/breeze/index.js +67 -1420
- package/dist/providers/breeze/openapi.generated.js +60 -0
- package/dist/providers/breeze/skill.generated.js +2 -6
- package/dist/providers/breeze/tools.js +696 -0
- package/dist/providers/ccb/api.js +496 -0
- package/dist/providers/ccb/index.d.ts +15 -37
- package/dist/providers/ccb/index.js +77 -1702
- package/dist/providers/ccb/lib/constants.d.ts +34 -0
- package/dist/providers/ccb/lib/constants.js +74 -0
- package/dist/providers/ccb/lib/guide.js +9 -0
- package/dist/providers/ccb/lib/verdicts.js +387 -0
- package/dist/providers/ccb/openapi.generated.js +397 -0
- package/dist/providers/ccb/safety.js +295 -0
- package/dist/providers/ccb/skill.generated.js +2 -6
- package/dist/providers/ccb/tools.js +853 -0
- package/dist/providers/cloudflare/api.js +1066 -0
- package/dist/providers/cloudflare/index.d.ts +5 -22
- package/dist/providers/cloudflare/index.js +79 -197
- package/dist/providers/cloudflare/lib/config.d.ts +16 -0
- package/dist/providers/cloudflare/lib/config.js +2 -0
- package/dist/providers/cloudflare/lib/discovery.js +29 -0
- package/dist/providers/cloudflare/{graphql.js → lib/graphql.js} +1 -1
- package/dist/providers/cloudflare/lib/hatch.js +103 -0
- package/dist/providers/cloudflare/lib/path-bounds.js +28 -0
- package/dist/providers/cloudflare/lib/response-media.js +75 -0
- package/dist/providers/cloudflare/openapi.generated.js +4 -3
- package/dist/providers/cloudflare/{value-safety.js → safety.js} +487 -0
- package/dist/providers/cloudflare/skill.generated.js +2 -10
- package/dist/providers/cloudflare/tools.js +360 -0
- package/dist/providers/docs/api.js +51 -0
- package/dist/providers/docs/index.d.ts +1 -12
- package/dist/providers/docs/index.js +53 -1165
- package/dist/providers/docs/lib/constants.d.ts +18 -0
- package/dist/providers/docs/lib/constants.js +18 -0
- package/dist/providers/docs/lib/discovery.js +26 -0
- package/dist/providers/docs/lib/requests.js +48 -0
- package/dist/providers/docs/lib/schemas.js +168 -0
- package/dist/providers/docs/openapi.generated.js +16 -0
- package/dist/providers/docs/safety.js +54 -0
- package/dist/providers/docs/skill.generated.js +2 -6
- package/dist/providers/docs/tools.js +802 -0
- package/dist/providers/drive/api.js +177 -0
- package/dist/providers/drive/index.d.ts +1 -12
- package/dist/providers/drive/index.js +38 -1448
- package/dist/providers/drive/lib/constants.d.ts +6 -0
- package/dist/providers/drive/lib/constants.js +6 -0
- package/dist/providers/drive/lib/contracts.js +532 -0
- package/dist/providers/drive/lib/hatch.js +197 -0
- package/dist/providers/drive/openapi.generated.js +77 -0
- package/dist/providers/drive/safety.js +90 -0
- package/dist/providers/drive/skill.generated.js +2 -6
- package/dist/providers/drive/tools.js +1290 -0
- package/dist/providers/forms/api.js +71 -0
- package/dist/providers/forms/index.d.ts +1 -12
- package/dist/providers/forms/index.js +49 -1145
- package/dist/providers/forms/lib/constants.d.ts +14 -0
- package/dist/providers/forms/lib/constants.js +19 -0
- package/dist/providers/forms/lib/hatch.js +72 -0
- package/dist/providers/forms/openapi.generated.js +23 -0
- package/dist/providers/forms/safety.js +34 -0
- package/dist/providers/forms/skill.generated.js +2 -8
- package/dist/providers/forms/tools.js +1024 -0
- package/dist/providers/github/api.js +150 -0
- package/dist/providers/github/index.d.ts +4 -5
- package/dist/providers/github/index.js +30 -87
- package/dist/providers/github/{auth.d.ts → lib/auth.d.ts} +5 -3
- package/dist/providers/github/{auth.js → lib/auth.js} +40 -43
- package/dist/providers/github/lib/discovery.js +132 -0
- package/dist/providers/github/lib/operations.js +58 -0
- package/dist/providers/github/{scope.d.ts → lib/scope.d.ts} +2 -0
- package/dist/providers/github/{scope.js → lib/scope.js} +25 -1
- package/dist/providers/github/openapi.generated.js +1216 -0
- package/dist/providers/github/safety.js +216 -0
- package/dist/providers/github/skill.generated.js +2 -5
- package/dist/providers/github/tools.js +506 -0
- package/dist/providers/gmail/api.js +114 -0
- package/dist/providers/gmail/index.d.ts +1 -12
- package/dist/providers/gmail/index.js +73 -1621
- package/dist/providers/gmail/lib/constants.d.ts +13 -0
- package/dist/providers/gmail/lib/constants.js +16 -0
- package/dist/providers/gmail/lib/index.js +173 -0
- package/dist/providers/gmail/lib/schemas.js +176 -0
- package/dist/providers/gmail/lib/threading.js +120 -0
- package/dist/providers/gmail/openapi.generated.js +92 -0
- package/dist/providers/gmail/safety.js +21 -0
- package/dist/providers/gmail/skill.generated.js +2 -6
- package/dist/providers/gmail/tools.js +1435 -0
- package/dist/providers/overflow/api.js +1008 -0
- package/dist/providers/overflow/index.d.ts +1 -17
- package/dist/providers/overflow/index.js +60 -1488
- package/dist/providers/overflow/skill.generated.js +2 -5
- package/dist/providers/overflow/tools.js +873 -0
- package/dist/providers/planning-center/api.js +467 -0
- package/dist/providers/planning-center/index.d.ts +4 -35
- package/dist/providers/planning-center/index.js +69 -2445
- package/dist/providers/planning-center/lib/constants.d.ts +58 -0
- package/dist/providers/planning-center/lib/constants.js +83 -0
- package/dist/providers/planning-center/lib/discovery.js +40 -0
- package/dist/providers/planning-center/lib/guide.js +39 -0
- package/dist/providers/planning-center/lib/json-api.js +83 -0
- package/dist/providers/planning-center/lib/verdicts.js +1758 -0
- package/dist/providers/planning-center/openapi.generated.js +1764 -0
- package/dist/providers/planning-center/safety.js +188 -0
- package/dist/providers/planning-center/skill.generated.js +2 -27
- package/dist/providers/planning-center/tools.js +1010 -0
- package/dist/providers/sheets/api.js +133 -0
- package/dist/providers/sheets/index.d.ts +4 -16
- package/dist/providers/sheets/index.js +61 -1172
- package/dist/providers/sheets/lib/constants.d.ts +14 -0
- package/dist/providers/sheets/lib/constants.js +14 -0
- package/dist/providers/sheets/lib/hatch.js +163 -0
- package/dist/providers/sheets/lib/requests.js +105 -0
- package/dist/providers/sheets/lib/schemas.js +138 -0
- package/dist/providers/sheets/openapi.generated.js +30 -0
- package/dist/providers/sheets/safety.js +27 -0
- package/dist/providers/sheets/skill.generated.js +2 -7
- package/dist/providers/sheets/tools.js +734 -0
- package/dist/providers/slides/api.js +147 -0
- package/dist/providers/slides/index.d.ts +2 -10
- package/dist/providers/slides/index.js +48 -2555
- package/dist/providers/slides/lib/constants.d.ts +11 -0
- package/dist/providers/slides/lib/constants.js +11 -0
- package/dist/providers/slides/lib/requests.js +60 -0
- package/dist/providers/slides/lib/schemas.js +565 -0
- package/dist/providers/slides/openapi.generated.js +18 -0
- package/dist/providers/slides/skill.generated.js +2 -6
- package/dist/providers/slides/tools.js +1968 -0
- package/dist/providers/stripe/api.js +401 -0
- package/dist/providers/stripe/index.d.ts +5 -27
- package/dist/providers/stripe/index.js +74 -210
- package/dist/providers/stripe/lib/config.d.ts +4 -0
- package/dist/providers/stripe/lib/config.js +2 -0
- package/dist/providers/stripe/openapi.generated.js +4 -3
- package/dist/providers/stripe/{value-safety.js → safety.js} +5 -5
- package/dist/providers/stripe/skill.generated.js +2 -11
- package/dist/providers/stripe/tools.js +77 -0
- package/dist/providers/tithely/api.js +531 -0
- package/dist/providers/tithely/index.d.ts +17 -0
- package/dist/providers/tithely/index.js +86 -965
- package/dist/providers/tithely/skill.generated.js +2 -5
- package/dist/providers/tithely/tools.js +426 -0
- package/dist/providers/vercel/api.js +551 -0
- package/dist/providers/vercel/index.d.ts +9 -25
- package/dist/providers/vercel/index.js +66 -260
- package/dist/providers/vercel/lib/config.d.ts +2 -0
- package/dist/providers/vercel/lib/config.js +2 -0
- package/dist/providers/vercel/lib/index.js +117 -0
- package/dist/providers/vercel/lib/logs.js +100 -0
- package/dist/providers/vercel/lib/pin.js +270 -0
- package/dist/providers/vercel/openapi.generated.js +10 -5
- package/dist/providers/vercel/{value-safety.js → safety.js} +247 -38
- package/dist/providers/vercel/skill.generated.js +2 -7
- package/dist/providers/vercel/tools.js +642 -0
- package/dist/redaction-identity.js +47 -0
- package/dist/redaction-marker.js +125 -0
- package/dist/registry.d.ts +21 -15
- package/dist/registry.js +409 -376
- package/dist/routes/access-tokens.js +1 -1
- package/dist/routes/actor-labels.js +1 -1
- package/dist/routes/connect.js +1 -1
- package/dist/routes/dispatch.js +103 -0
- package/dist/routes/mcp.js +170 -79
- package/dist/routes/oauth.js +3 -1
- package/dist/routes/operator.js +2 -2
- package/dist/routes/shared.d.ts +5 -4
- package/dist/routes/shared.js +244 -177
- package/dist/routes/skills.js +10 -8
- package/dist/routes/ui-config.js +27 -3
- package/dist/routes/ui-data.js +21 -20
- package/dist/routes/ui.js +1 -1
- package/dist/runtime/call-admission.js +0 -26
- package/dist/runtime/call-entry.js +30 -0
- package/dist/runtime/call.js +556 -0
- package/dist/runtime/connector-scope.js +7 -0
- package/dist/runtime/credential-probe.js +34 -0
- package/dist/runtime/executor.js +40 -0
- package/dist/runtime/native-storage.js +63 -0
- package/dist/runtime/registry.js +27 -0
- package/dist/runtime/request-scope.js +72 -0
- package/dist/runtime/run.js +15 -7
- package/dist/runtime/shared-read.js +34 -5
- package/dist/runtime/storage.js +34 -21
- package/dist/sent-secrets.d.ts +194 -37
- package/dist/sent-secrets.js +1743 -340
- package/dist/server.js +5 -56
- package/dist/skills.js +322 -222
- package/dist/sqlite.js +15 -14
- package/dist/storage/d1-copy.js +255 -0
- package/dist/storage/d1-driver.js +15 -0
- package/dist/storage/keys.js +28 -0
- package/dist/storage/sql-driver.js +88 -0
- package/dist/storage/sql-native.js +544 -0
- package/dist/storage/sql-schema.d.ts +6 -0
- package/dist/storage/sql-schema.js +15 -0
- package/dist/storage/sql.d.ts +14 -20
- package/dist/storage/sql.js +29 -535
- package/dist/timeout.js +0 -26
- package/dist/tool-safety.js +20 -10
- package/dist/types.d.ts +7 -109
- package/dist/upward.js +3 -0
- package/dist/usage-guide.js +3 -1
- package/dist/validate.d.ts +4 -2
- package/dist/validate.js +86 -9
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +312 -292
- package/dist/workspace-subject.js +5 -1
- package/dist/write-outcome.js +454 -34
- package/documentation/README.md +27 -7
- package/documentation/architecture.md +2547 -226
- package/documentation/auth.md +821 -98
- package/documentation/code-mode.md +49 -16
- package/documentation/deploying.md +164 -20
- package/documentation/hosted-mcp-migration.md +460 -0
- package/documentation/integrating.md +129 -144
- package/documentation/meta-tools.md +409 -32
- package/documentation/operating.md +27 -3
- package/documentation/operator-tests.md +3 -0
- package/documentation/operator-ui.md +68 -45
- package/documentation/provider-auth-migration.md +16 -63
- package/documentation/provider-migration-0.29.md +10 -5
- package/documentation/provider-migration-0.35.md +975 -0
- package/documentation/providers.md +965 -0
- package/examples/worker/AGENTS.md +23 -9
- package/examples/worker/README.md +254 -135
- package/examples/worker/alchemy.run.ts +116 -0
- package/examples/worker/gitignore +10 -0
- package/examples/worker/src/connecta.config.ts +23 -21
- package/examples/worker/wrangler.jsonc +14 -5
- package/guides/connecting-agents.md +157 -0
- package/guides/how-it-works.md +156 -0
- package/guides/safety-and-trust.md +153 -0
- package/package.json +27 -46
- package/templates/node/.env.example +3 -0
- package/templates/node/README.md +22 -2
- package/templates/node/docker-compose.yml +1 -0
- package/templates/node/package.json +3 -2
- package/templates/node/src/connecta.config.ts +8 -0
- package/dist/catalog-drift.d.ts +0 -138
- package/dist/catalog-drift.js +0 -578
- package/dist/providers/_shared/request-token.js +0 -83
- package/dist/providers/_shared/rest/operation-index.d.ts +0 -133
- package/dist/providers/_shared/rest/tools.d.ts +0 -141
- package/dist/providers/_shared/rest/value-safety.d.ts +0 -201
- package/dist/providers/basecamp/index.d.ts +0 -45
- package/dist/providers/basecamp/index.js +0 -997
- package/dist/providers/basecamp/skill.generated.js +0 -9
- package/dist/providers/cloudflare/rest.d.ts +0 -37
- package/dist/providers/cloudflare/rest.js +0 -1097
- package/dist/providers/github/hosted.js +0 -218
- package/dist/providers/github/rest.js +0 -321
- package/dist/providers/infisical/index.d.ts +0 -7
- package/dist/providers/infisical/index.js +0 -713
- package/dist/providers/infisical/skill.generated.js +0 -8
- package/dist/providers/intercom/index.d.ts +0 -24
- package/dist/providers/intercom/index.js +0 -121
- package/dist/providers/intercom/skill.generated.js +0 -8
- package/dist/providers/linear/index.d.ts +0 -55
- package/dist/providers/linear/index.js +0 -537
- package/dist/providers/linear/skill.generated.js +0 -8
- package/dist/providers/mixpanel/index.d.ts +0 -59
- package/dist/providers/mixpanel/index.js +0 -472
- package/dist/providers/mixpanel/operations.js +0 -20
- package/dist/providers/mixpanel/rest.js +0 -603
- package/dist/providers/mixpanel/skill.generated.js +0 -10
- package/dist/providers/mixpanel/value-safety.js +0 -16
- package/dist/providers/notion/index.d.ts +0 -67
- package/dist/providers/notion/index.js +0 -368
- package/dist/providers/notion/openapi.generated.js +0 -75
- package/dist/providers/notion/rest.d.ts +0 -24
- package/dist/providers/notion/rest.js +0 -1226
- package/dist/providers/notion/skill.generated.js +0 -11
- package/dist/providers/notion/value-safety.js +0 -56
- package/dist/providers/revenuecat/index.d.ts +0 -51
- package/dist/providers/revenuecat/index.js +0 -697
- package/dist/providers/revenuecat/skill.generated.js +0 -11
- package/dist/providers/stripe/rest.d.ts +0 -14
- package/dist/providers/stripe/rest.js +0 -402
- package/dist/providers/vercel/rest.d.ts +0 -20
- package/dist/providers/vercel/rest.js +0 -1020
- package/dist/providers/wispr-flow/index.d.ts +0 -14
- package/dist/providers/wispr-flow/index.js +0 -117
- package/dist/providers/wispr-flow/skill.generated.js +0 -8
- /package/dist/providers/github/{key.js → lib/key.js} +0 -0
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
// Google Workspace on the provider kit (decision 0010; C.7 step 1 under 0011).
|
|
2
|
+
// Shared by every Workspace product; never its own export. `workspace.ts`
|
|
3
|
+
// keeps shared options, connection parsing and subject resolution;
|
|
4
|
+
// `service-account.ts` keeps the private token exchange and cache.
|
|
5
|
+
// This module adds what every product needs: the Google error
|
|
6
|
+
// table, the delegated machine grant, a value-safety resolver over the
|
|
7
|
+
// product's own operations, a request adapter that keeps the legacy request
|
|
8
|
+
// bytes and write wording, and the step-1 catalog bridge.
|
|
9
|
+
import { Effect, Option, Schema } from "effect";
|
|
10
|
+
import { callClassification } from "../../../call-classification.js";
|
|
11
|
+
import { deriveContext } from "../../../derived-context.js";
|
|
12
|
+
import { ConnectorCallError } from "../../../errors.js";
|
|
13
|
+
import { attachFailureFacts } from "../../../operator-record.js";
|
|
14
|
+
import { Refusal, StatusFailure, toConnectorCallError, } from "../../../http/errors.js";
|
|
15
|
+
import { Classification, Dispatches, Signal } from "../../../http/services.js";
|
|
16
|
+
import { read, write } from "../../../http/tools.js";
|
|
17
|
+
import { callMemo, Operation } from "../../../runtime/call.js";
|
|
18
|
+
import { providerFailureMessage, unsentAfterUnauthorized } from "../../../write-outcome.js";
|
|
19
|
+
import { delegatedToken, forgetDelegatedToken } from "./service-account.js";
|
|
20
|
+
import { subjectFor, workspaceConnection } from "./workspace.js";
|
|
21
|
+
import { tagWorkspaceSubject } from "../../../workspace-subject.js";
|
|
22
|
+
import { googleNamedOperation, requireGoogleIndex } from "./hatch.js";
|
|
23
|
+
function asRecord(value) {
|
|
24
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
25
|
+
}
|
|
26
|
+
function asArray(value) {
|
|
27
|
+
return Array.isArray(value) ? value : [];
|
|
28
|
+
}
|
|
29
|
+
function reasonsOf(tokens) {
|
|
30
|
+
const error = asRecord(asRecord(tokens)["error"]);
|
|
31
|
+
const reasons = new Set();
|
|
32
|
+
for (const entry of [...asArray(error["errors"]), ...asArray(error["details"])]) {
|
|
33
|
+
const reason = asRecord(entry)["reason"];
|
|
34
|
+
if (typeof reason === "string")
|
|
35
|
+
reasons.add(reason);
|
|
36
|
+
}
|
|
37
|
+
if (typeof error["status"] === "string")
|
|
38
|
+
reasons.add(error["status"]);
|
|
39
|
+
return [...reasons];
|
|
40
|
+
}
|
|
41
|
+
/** Public reason spellings shared by classification and the pre-redaction vocabulary. */
|
|
42
|
+
const REASONS = {
|
|
43
|
+
disabled: ["accessNotConfigured", "SERVICE_DISABLED"],
|
|
44
|
+
rateLimited: ["rateLimitExceeded", "userRateLimitExceeded", "RATE_LIMIT_EXCEEDED"],
|
|
45
|
+
insufficientScope: ["insufficientPermissions", "ACCESS_TOKEN_SCOPE_INSUFFICIENT"],
|
|
46
|
+
exportSize: ["exportSizeLimitExceeded"],
|
|
47
|
+
domainPolicy: ["domainPolicy"],
|
|
48
|
+
filePermission: ["insufficientFilePermissions", "forbidden"],
|
|
49
|
+
precondition: ["FAILED_PRECONDITION", "failedPrecondition"],
|
|
50
|
+
aborted: ["ABORTED", "aborted"],
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Google's failure table, in the legacy client's order: Google's own
|
|
54
|
+
* specific reasons decide first, whatever the status; only a refused
|
|
55
|
+
* precondition or an abort with nothing more specific to say is left for
|
|
56
|
+
* the revision guard (the `revisionGuarded` request flag), then the status.
|
|
57
|
+
* Each verdict's words are the legacy client's, so the messages stay.
|
|
58
|
+
*/
|
|
59
|
+
export function googleErrors(product, options = {}) {
|
|
60
|
+
const { provider, api, scopes, notFound } = product;
|
|
61
|
+
const has = (facts, names) => names.some((name) => facts.reasons.includes(name));
|
|
62
|
+
return {
|
|
63
|
+
provider,
|
|
64
|
+
facts: (tokens) => ({ reasons: reasonsOf(tokens) }),
|
|
65
|
+
factVocabulary: { reasons: Object.values(REASONS).flat() },
|
|
66
|
+
numericFacts: {
|
|
67
|
+
retryAfterMs: {
|
|
68
|
+
header: "retry-after",
|
|
69
|
+
format: "retry-after",
|
|
70
|
+
review: "HTTP Retry-After delay for failure recovery.",
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
detail: ({ status, body }) => {
|
|
74
|
+
const message = asRecord(asRecord(body)["error"])["message"];
|
|
75
|
+
// The body arrives redacted whole, before anything here cuts it.
|
|
76
|
+
const said = typeof message === "string" ? message.trim().slice(0, 500) : "";
|
|
77
|
+
return said ? `${provider}: ${said}${/[.!?]$/.test(said) ? "" : "."}` : `${provider} returned HTTP ${status}.`;
|
|
78
|
+
},
|
|
79
|
+
classify: [
|
|
80
|
+
// Every 3xx the transport lets through (300, 304, 305, 306) is a redirect first.
|
|
81
|
+
({ status }) => status >= 300 && status < 400
|
|
82
|
+
? {
|
|
83
|
+
code: "connector_call_failed",
|
|
84
|
+
retryable: false,
|
|
85
|
+
words: () => `${provider} answered HTTP ${status} with a redirect; this connection talks to exactly one origin and never forwards its credential to another.`,
|
|
86
|
+
}
|
|
87
|
+
: undefined,
|
|
88
|
+
({ status }) => status === 401
|
|
89
|
+
? {
|
|
90
|
+
code: "auth_required",
|
|
91
|
+
words: (detail) => `${detail} Google rejected a freshly minted delegated token. The domain-wide delegation grant or the service account may have been revoked.`,
|
|
92
|
+
}
|
|
93
|
+
: undefined,
|
|
94
|
+
({ facts }) => has(facts, REASONS.disabled)
|
|
95
|
+
? {
|
|
96
|
+
code: "connector_call_failed",
|
|
97
|
+
retryable: false,
|
|
98
|
+
words: (detail) => `${detail} The ${api} is not enabled in the service account's Google Cloud project; an operator enables it there.`,
|
|
99
|
+
}
|
|
100
|
+
: undefined,
|
|
101
|
+
({ status, facts, retryAfterMs }) => (status === 429 && !options.standard) || has(facts, REASONS.rateLimited)
|
|
102
|
+
? {
|
|
103
|
+
code: "rate_limited",
|
|
104
|
+
retryAfterMs,
|
|
105
|
+
words: (detail) => `${detail} Google meters this API per user and per project.`,
|
|
106
|
+
advice: "; wait before retrying",
|
|
107
|
+
}
|
|
108
|
+
: undefined,
|
|
109
|
+
({ facts }) => has(facts, REASONS.insufficientScope)
|
|
110
|
+
? {
|
|
111
|
+
code: "provider_permission_denied",
|
|
112
|
+
words: (detail) => `${detail} The delegated token lacks a scope this call needs. The Admin console's domain-wide delegation entry must list exactly: ${scopes.join(",")}.`,
|
|
113
|
+
}
|
|
114
|
+
: undefined,
|
|
115
|
+
({ facts }) => has(facts, REASONS.exportSize)
|
|
116
|
+
? {
|
|
117
|
+
code: "connector_call_failed",
|
|
118
|
+
retryable: false,
|
|
119
|
+
words: (detail) => `${detail} The item is larger than Google will export in this format. Download the original file instead, or export a smaller part.`,
|
|
120
|
+
}
|
|
121
|
+
: undefined,
|
|
122
|
+
({ facts }) => has(facts, REASONS.domainPolicy)
|
|
123
|
+
? {
|
|
124
|
+
code: "provider_permission_denied",
|
|
125
|
+
retryable: false,
|
|
126
|
+
words: (detail) => `${detail} A Workspace domain policy forbids this action for this account; only a Workspace administrator can change that.`,
|
|
127
|
+
}
|
|
128
|
+
: undefined,
|
|
129
|
+
({ facts }) => has(facts, REASONS.filePermission)
|
|
130
|
+
? {
|
|
131
|
+
code: "provider_permission_denied",
|
|
132
|
+
retryable: false,
|
|
133
|
+
words: (detail) => `${detail} This account does not have the permission on this item that the action needs — it may be shared view-only, or not shared with this user. Its owner can grant more.`,
|
|
134
|
+
}
|
|
135
|
+
: undefined,
|
|
136
|
+
// A write that named the revision it was made against hears "the item
|
|
137
|
+
// moved on" as a refused precondition or an abort. Fixed words, so no
|
|
138
|
+
// provider ever matches on Google's prose.
|
|
139
|
+
({ status, facts, operation }) => operation.flags?.["revisionGuarded"] === true &&
|
|
140
|
+
(status === 400 || status === 409) &&
|
|
141
|
+
has(facts, [...REASONS.precondition, ...REASONS.aborted])
|
|
142
|
+
? {
|
|
143
|
+
code: "conflict",
|
|
144
|
+
words: () => `${provider} refused the write because the item changed after the revision it named.`,
|
|
145
|
+
advice: " Re-read it for the current revision, reapply the change, and retry.",
|
|
146
|
+
}
|
|
147
|
+
: undefined,
|
|
148
|
+
// Google uses the same reason for a product switched off for this user
|
|
149
|
+
// and for a stale write, so the classification asserts neither.
|
|
150
|
+
({ status, facts }) => status === 400 && has(facts, REASONS.precondition)
|
|
151
|
+
? {
|
|
152
|
+
code: "connector_call_failed",
|
|
153
|
+
retryable: false,
|
|
154
|
+
words: (detail) => `${detail} Google refused the request's precondition; its message says which.`,
|
|
155
|
+
}
|
|
156
|
+
: undefined,
|
|
157
|
+
],
|
|
158
|
+
statuses: {
|
|
159
|
+
401: () => ({ code: "auth_required" }),
|
|
160
|
+
403: () => ({
|
|
161
|
+
code: "provider_permission_denied",
|
|
162
|
+
retryable: false,
|
|
163
|
+
words: (detail) => `${detail} Google refused the request; the account may lack access to this item, or a Workspace policy may block it. Google does not say which. Ask the item owner to grant access or a Workspace administrator to review the policy.`,
|
|
164
|
+
}),
|
|
165
|
+
404: () => notFound === "absent"
|
|
166
|
+
? { code: "not_found", words: (detail) => `${detail} Re-read the id from a list or search tool.` }
|
|
167
|
+
: {
|
|
168
|
+
code: "connector_call_failed",
|
|
169
|
+
retryable: false,
|
|
170
|
+
words: (detail) => `${detail} The id is unknown or not visible to this account; Google does not distinguish them.`,
|
|
171
|
+
},
|
|
172
|
+
400: () => ({ code: "invalid_args" }),
|
|
173
|
+
422: () => ({ code: "invalid_args" }),
|
|
174
|
+
serverError: ({ retryAfterMs }) => ({
|
|
175
|
+
code: "unavailable",
|
|
176
|
+
retryAfterMs,
|
|
177
|
+
words: (detail) => `${detail} Google is failing upstream.`,
|
|
178
|
+
}),
|
|
179
|
+
otherwise: () => ({ code: "connector_call_failed", retryable: false }),
|
|
180
|
+
},
|
|
181
|
+
unreadable: `${provider} returned a successful response that is not JSON.`,
|
|
182
|
+
...(options.standard
|
|
183
|
+
? {
|
|
184
|
+
meanings: {
|
|
185
|
+
unknownId: notFound === "absent" ? "not_found" : "connector_call_failed",
|
|
186
|
+
provesAbsence: notFound === "absent",
|
|
187
|
+
conflicts: options.revisionConflicts
|
|
188
|
+
? ["A revision-guarded batch update returned ABORTED or FAILED_PRECONDITION with HTTP 400 or 409"]
|
|
189
|
+
: [],
|
|
190
|
+
},
|
|
191
|
+
bodyFailures: {
|
|
192
|
+
review: "An incomplete Google response cannot establish the requested result, including named binary reads.",
|
|
193
|
+
statusOnly: [],
|
|
194
|
+
readFailed: `${provider}'s response could not be read completely.`,
|
|
195
|
+
},
|
|
196
|
+
statuses: {
|
|
197
|
+
401: () => ({ code: "auth_required" }),
|
|
198
|
+
403: () => ({
|
|
199
|
+
code: "provider_permission_denied",
|
|
200
|
+
retryable: false,
|
|
201
|
+
words: (detail) => `${detail} The account may lack access, or a Workspace policy may block this request.`,
|
|
202
|
+
}),
|
|
203
|
+
404: () => notFound === "absent"
|
|
204
|
+
? { code: "not_found" }
|
|
205
|
+
: {
|
|
206
|
+
code: "connector_call_failed",
|
|
207
|
+
retryable: false,
|
|
208
|
+
words: (detail) => `${detail} The id is unknown or not visible to this account; Google does not distinguish them.`,
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
}
|
|
212
|
+
: {}),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
// --- Value safety ------------------------------------------------------------------
|
|
216
|
+
/** One declared operation as a matcher over actual paths: `:name` matches one encoded segment. */
|
|
217
|
+
function templateMatcher(template) {
|
|
218
|
+
const segments = template.split("/").map((segment) => {
|
|
219
|
+
const parameter = /^:([A-Za-z0-9_]+)(.*)$/.exec(segment);
|
|
220
|
+
const literal = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
221
|
+
return parameter ? `[^/]+${literal(parameter[2])}` : literal(segment);
|
|
222
|
+
});
|
|
223
|
+
return new RegExp(`^${segments.join("/")}$`);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Google's value safety. Workspace answers are the user's own documents and
|
|
227
|
+
* mail, returned to the user who can open them; no response carries a
|
|
228
|
+
* credential or a stored secret, and the token a request sent is redacted by
|
|
229
|
+
* the kit's sent-secret pass before anything reads a body. So no field is
|
|
230
|
+
* rewritten and failures keep Google's (redacted) message, as they did on the
|
|
231
|
+
* legacy client. Every request resolves to one of the product's declared
|
|
232
|
+
* operations, so a path no tool declares never reaches the network.
|
|
233
|
+
*/
|
|
234
|
+
export function googleSafety(provider, operations) {
|
|
235
|
+
const declared = operations.map((operation) => {
|
|
236
|
+
const { method, path } = operation;
|
|
237
|
+
return { method, template: path, matcher: templateMatcher(path) };
|
|
238
|
+
});
|
|
239
|
+
return {
|
|
240
|
+
reviewed: {
|
|
241
|
+
resolve: (method, path) => {
|
|
242
|
+
const found = declared.find((entry) => entry.method === method && entry.matcher.test(path));
|
|
243
|
+
if (!found)
|
|
244
|
+
throw new ConnectorCallError("invalid_args", `This ${provider} operation is not one the connection declares.`);
|
|
245
|
+
return { method: found.method, template: found.template };
|
|
246
|
+
},
|
|
247
|
+
redact: (data) => data,
|
|
248
|
+
withholdsErrors: () => false,
|
|
249
|
+
},
|
|
250
|
+
sensitiveFields: { names: [], choice: "drop" },
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
// --- The delegated grant -----------------------------------------------------------
|
|
254
|
+
const connections = new WeakMap();
|
|
255
|
+
/**
|
|
256
|
+
* The checked shared options, parsed once per construction: every definition
|
|
257
|
+
* callback receives the same validated options object, and the key is
|
|
258
|
+
* decoded and checked only the first time.
|
|
259
|
+
*/
|
|
260
|
+
export function workspaceOf(factory, options) {
|
|
261
|
+
let connection = connections.get(options);
|
|
262
|
+
if (!connection) {
|
|
263
|
+
connection = workspaceConnection(factory, options);
|
|
264
|
+
connections.set(options, connection);
|
|
265
|
+
}
|
|
266
|
+
return connection;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* A machine grant over the service account's delegated tokens. Built at
|
|
270
|
+
* construction, outside every caller context (INV-7); each send evaluates it
|
|
271
|
+
* with that call's services. The subject resolves once per call, from
|
|
272
|
+
* deployment config and the admitted caller only, before any token is
|
|
273
|
+
* minted. Tokens are the shared in-memory cache's: only token strings and
|
|
274
|
+
* expiries cross requests.
|
|
275
|
+
*/
|
|
276
|
+
export function googleGrant(product, connection) {
|
|
277
|
+
const { provider, scopes } = product;
|
|
278
|
+
const errors = googleErrors(product);
|
|
279
|
+
const subjectKey = {};
|
|
280
|
+
return Effect.gen(function* () {
|
|
281
|
+
const operation = yield* Effect.serviceOption(Operation);
|
|
282
|
+
const ctx = Option.isSome(operation) ? operation.value : undefined;
|
|
283
|
+
if (!ctx)
|
|
284
|
+
return yield* Effect.die(new Error(`${provider} needs the call's connector context to resolve its subject.`));
|
|
285
|
+
const callSignal = yield* Signal;
|
|
286
|
+
// One resolution per call: a tool that sends several requests acts as one account throughout.
|
|
287
|
+
const tokenRequest = async () => ({
|
|
288
|
+
account: connection.account,
|
|
289
|
+
subject: await callMemo(ctx, subjectKey, () => subjectFor(provider, connection.subject, ctx)),
|
|
290
|
+
scopes,
|
|
291
|
+
});
|
|
292
|
+
const use = (work) => Effect.tryPromise({
|
|
293
|
+
try: (interrupted) => work(deriveContext(ctx, { signal: callSignal ? AbortSignal.any([callSignal, interrupted]) : interrupted })),
|
|
294
|
+
catch: (error) => error,
|
|
295
|
+
}).pipe(Effect.catch((error) => error instanceof ConnectorCallError ? Effect.fail(new Refusal({ error })) : Effect.die(error)));
|
|
296
|
+
return {
|
|
297
|
+
token: use(async (view) => delegatedToken(await tokenRequest(), view)),
|
|
298
|
+
// The token a 401 refused is forgotten, never a newer one, then minted anew.
|
|
299
|
+
refresh: (stale) => use(async (view) => {
|
|
300
|
+
const request = await tokenRequest();
|
|
301
|
+
await forgetDelegatedToken(request, stale);
|
|
302
|
+
return delegatedToken(request, view);
|
|
303
|
+
}),
|
|
304
|
+
// Unreachable with GOOGLE_GRANT_RENEWAL, which answers a replay's 401 from its body.
|
|
305
|
+
rejected: Effect.suspend(() => Effect.fail(new Refusal({
|
|
306
|
+
error: toConnectorCallError(new StatusFailure({
|
|
307
|
+
status: 401,
|
|
308
|
+
headers: new Headers(),
|
|
309
|
+
body: undefined,
|
|
310
|
+
operation: { method: "GET", template: "" },
|
|
311
|
+
}), errors, { classification: "read", dispatches: [] }),
|
|
312
|
+
}))),
|
|
313
|
+
discard: (rejected) => use(async () => forgetDelegatedToken(await tokenRequest(), rejected)).pipe(Effect.ignore),
|
|
314
|
+
};
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Renewal as the legacy client did it: a read's 401 earns one fresh token
|
|
319
|
+
* and one replay, and a second 401 is answered from its own body; a write's
|
|
320
|
+
* 401 forgets its token and is never sent again, and the next call mints.
|
|
321
|
+
*/
|
|
322
|
+
export const GOOGLE_GRANT_RENEWAL = { replayRejection: "answer", writeRenewal: "discard" };
|
|
323
|
+
/** Tag the built connector with whose account it acts as (#837, decisions/0006). */
|
|
324
|
+
export function tagWorkspace(factory) {
|
|
325
|
+
return (connector, options) => {
|
|
326
|
+
tagWorkspaceSubject(connector, workspaceOf(factory, options).subject);
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
const outcomes = new WeakMap();
|
|
330
|
+
/** The phase of a failure a Google request returned; undefined for anything else. */
|
|
331
|
+
export function googleOutcomeOf(failure) {
|
|
332
|
+
return failure !== null && typeof failure === "object" ? outcomes.get(failure) : undefined;
|
|
333
|
+
}
|
|
334
|
+
function noted(failure, outcome) {
|
|
335
|
+
outcomes.set(failure, Object.freeze({ ...outcome }));
|
|
336
|
+
return failure;
|
|
337
|
+
}
|
|
338
|
+
/** Query pairs as the legacy transport encoded them: undefined dropped, arrays repeated, values stringified. */
|
|
339
|
+
function queryPairs(query) {
|
|
340
|
+
const pairs = [];
|
|
341
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
342
|
+
if (value === undefined)
|
|
343
|
+
continue;
|
|
344
|
+
if (Array.isArray(value))
|
|
345
|
+
for (const item of value)
|
|
346
|
+
pairs.push([key, String(item)]);
|
|
347
|
+
else
|
|
348
|
+
pairs.push([key, String(value)]);
|
|
349
|
+
}
|
|
350
|
+
return pairs;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Words the legacy client gave a served write whose answer proves nothing:
|
|
354
|
+
* no answer, a 5xx, a redirect, or a 2xx whose reply could not be read. Only
|
|
355
|
+
* the message and retry flag are its; the core still decides the outcome,
|
|
356
|
+
* which for each of these is unknown.
|
|
357
|
+
*/
|
|
358
|
+
function unknownWrite(message, status) {
|
|
359
|
+
return new Refusal({
|
|
360
|
+
error: attachFailureFacts(new ConnectorCallError("connector_call_failed", message, { retryable: false }), status === undefined ? {} : { httpStatus: status }),
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
export function googleClient(connection, provider, configuration = {}) {
|
|
364
|
+
if (configuration.standard || configuration.index)
|
|
365
|
+
requireGoogleIndex(configuration.index);
|
|
366
|
+
const send = (request, accept, options, binary = false) => Effect.gen(function* () {
|
|
367
|
+
if (configuration.index) {
|
|
368
|
+
yield* Effect.try({
|
|
369
|
+
try: () => {
|
|
370
|
+
if (!request.template)
|
|
371
|
+
throw new ConnectorCallError("invalid_args", "This indexed Google named request needs its declared operation template.");
|
|
372
|
+
googleNamedOperation(configuration.index, request.method, request.path, request.template);
|
|
373
|
+
},
|
|
374
|
+
catch: (error) => new Refusal({ error: error }),
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
const active = yield* Effect.serviceOption(Operation);
|
|
378
|
+
const callWrite = (yield* Classification) === "write";
|
|
379
|
+
const dispatches = yield* Dispatches;
|
|
380
|
+
const signal = yield* Signal;
|
|
381
|
+
// Match the legacy wording rule even for direct calls: their GET
|
|
382
|
+
// lookups have no served verdict. Physical write accounting stays the
|
|
383
|
+
// core's Classification; only these Google-specific words use this.
|
|
384
|
+
const served = Option.isSome(active) && active.value ? callClassification(active.value) : undefined;
|
|
385
|
+
const write = (served ?? (request.method === "GET" ? "read" : "write")) === "write";
|
|
386
|
+
const operation = yield* connection.safety.resolve(request.method, request.path);
|
|
387
|
+
return yield* connection
|
|
388
|
+
.sendPath({
|
|
389
|
+
method: request.method,
|
|
390
|
+
path: request.path,
|
|
391
|
+
template: operation.template,
|
|
392
|
+
query: queryPairs(request.query),
|
|
393
|
+
headers: { accept, ...request.headers },
|
|
394
|
+
...(request.body !== undefined ? { json: JSON.stringify(request.body) } : {}),
|
|
395
|
+
...(request.rawBody !== undefined ? { rawBody: request.rawBody } : {}),
|
|
396
|
+
}, {
|
|
397
|
+
...(options?.revisionGuarded || binary
|
|
398
|
+
? {
|
|
399
|
+
flags: {
|
|
400
|
+
...(options?.revisionGuarded ? { revisionGuarded: true } : {}),
|
|
401
|
+
...(binary ? { googleBytes: true } : {}),
|
|
402
|
+
},
|
|
403
|
+
}
|
|
404
|
+
: {}),
|
|
405
|
+
...(binary && options?.maxBytes !== undefined ? { prefix: { maxBytes: options.maxBytes } } : {}),
|
|
406
|
+
...(options?.effect ? { effect: options.effect } : {}),
|
|
407
|
+
...(options?.step ? { step: options.step } : {}),
|
|
408
|
+
})
|
|
409
|
+
.pipe(Effect.catch((failure) => Effect.fail(settled(failure, dispatches.find((entry) => entry.failure === failure), write, callWrite, signal))));
|
|
410
|
+
});
|
|
411
|
+
function settled(failure, own, write, callWrite, signal) {
|
|
412
|
+
// A lookup inside a write call has not sent the write. Keep the lookup's
|
|
413
|
+
// own verdict; only a call-owned request gets uncertain-write wording.
|
|
414
|
+
write = write && own?.request?.auxiliary !== true;
|
|
415
|
+
const status = own?.status;
|
|
416
|
+
const dispatched = own?.request?.dispatched === true;
|
|
417
|
+
const at = (phase, code = status) => ({ dispatched, phase, ...(code === undefined ? {} : { status: code }) });
|
|
418
|
+
if (!dispatched)
|
|
419
|
+
return noted(failure, { dispatched: false, phase: "before-send" });
|
|
420
|
+
if (configuration.standard) {
|
|
421
|
+
// Preserve phase evidence for partial-create recovery; settlement and
|
|
422
|
+
// reconciliation wording belong to the kit in standard mode.
|
|
423
|
+
const phase = failure._tag === "TransportFailure"
|
|
424
|
+
? "awaiting-response"
|
|
425
|
+
: failure._tag === "UnreadableBody" || failure._tag === "ResponseTooLarge"
|
|
426
|
+
? "reading-body"
|
|
427
|
+
: failure._tag === "RedirectRefused" || (status !== undefined && status >= 300 && status < 400)
|
|
428
|
+
? "redirected"
|
|
429
|
+
: status === 401
|
|
430
|
+
? "unauthorized"
|
|
431
|
+
: status !== undefined && status >= 500
|
|
432
|
+
? "server-error"
|
|
433
|
+
: "refused";
|
|
434
|
+
return noted(failure, at(phase));
|
|
435
|
+
}
|
|
436
|
+
switch (failure._tag) {
|
|
437
|
+
case "StatusFailure": {
|
|
438
|
+
const code = failure.status;
|
|
439
|
+
if (code >= 300 && code < 400)
|
|
440
|
+
return noted(write
|
|
441
|
+
? unknownWrite(`${provider} answered HTTP ${code} with a redirect, which this connection does not follow, so whether the request was applied is unknown. Re-read its target before repeating it.`, status)
|
|
442
|
+
: failure, at("redirected", code));
|
|
443
|
+
if (write && code >= 500)
|
|
444
|
+
return noted(unknownWrite(`${provider} answered HTTP ${code} after receiving the request, so its outcome is unknown — it may have been applied. Re-read its target before repeating it.`, status), at("server-error", code));
|
|
445
|
+
if (callWrite && own?.request?.auxiliary !== true && code === 401) {
|
|
446
|
+
// Never sent again (INV-9); the next call mints a fresh token.
|
|
447
|
+
const refusal = toConnectorCallError(failure, connection.errors, {
|
|
448
|
+
classification: "write",
|
|
449
|
+
dispatches: [own],
|
|
450
|
+
withholds: connection.safety.withholdsErrors,
|
|
451
|
+
signal,
|
|
452
|
+
});
|
|
453
|
+
return noted(new Refusal({ error: unsentAfterUnauthorized(provider, refusal) }), at("unauthorized", 401));
|
|
454
|
+
}
|
|
455
|
+
return noted(failure, at("refused", code));
|
|
456
|
+
}
|
|
457
|
+
case "RedirectRefused":
|
|
458
|
+
return noted(write
|
|
459
|
+
? unknownWrite(`${provider} answered HTTP ${failure.status} with a redirect, which this connection does not follow, so whether the request was applied is unknown. Re-read its target before repeating it.`, status)
|
|
460
|
+
: failure, at("redirected", failure.status));
|
|
461
|
+
case "TransportFailure":
|
|
462
|
+
return noted(write
|
|
463
|
+
? unknownWrite(`${provider} was sent the request but no answer came back, so it may or may not have been applied. Re-read its target before repeating it.`, status)
|
|
464
|
+
: failure, at("awaiting-response", undefined));
|
|
465
|
+
case "ResponseTooLarge":
|
|
466
|
+
case "UnreadableBody":
|
|
467
|
+
// An error status keeps its meaning whatever went wrong reading it.
|
|
468
|
+
if (status !== undefined && status >= 400)
|
|
469
|
+
return noted(failure, at(write && status >= 500 ? "server-error" : "refused"));
|
|
470
|
+
return noted(write
|
|
471
|
+
? unknownWrite(`${provider} accepted the request but its reply could not be read; the change probably applied. Re-read before repeating it.`, status)
|
|
472
|
+
: failure, at("reading-body"));
|
|
473
|
+
default:
|
|
474
|
+
return noted(failure, at("refused"));
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
json: (request, options) => Effect.map(send(request, "application/json", options), (answer) => answer.data),
|
|
479
|
+
bytes: (request, accept = "*/*", options) => send(request, accept, options, true),
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Reword a failure a Google request returned, keeping its verdict, its
|
|
484
|
+
* provenance and its phase: what a product adds after an uncertain create.
|
|
485
|
+
*/
|
|
486
|
+
export function googleFailureMessage(connection, failure, message) {
|
|
487
|
+
return Effect.gen(function* () {
|
|
488
|
+
const error = toConnectorCallError(failure, connection.errors, {
|
|
489
|
+
classification: yield* Classification,
|
|
490
|
+
dispatches: yield* Dispatches,
|
|
491
|
+
withholds: connection.safety.withholdsErrors,
|
|
492
|
+
signal: yield* Signal,
|
|
493
|
+
});
|
|
494
|
+
const reworded = new Refusal({ error: providerFailureMessage(error, message(error.message)) });
|
|
495
|
+
const outcome = googleOutcomeOf(failure);
|
|
496
|
+
return yield* Effect.fail(outcome ? noted(reworded, outcome) : reworded);
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
/** Arguments arrive validated against the exact legacy `inputSchema` by `api()` before any handler runs. */
|
|
500
|
+
const ARGUMENTS = Schema.Record(Schema.String, Schema.Unknown);
|
|
501
|
+
/**
|
|
502
|
+
* The step-1 catalog bridge. The tool is a kit `read` or `write` (its
|
|
503
|
+
* lease, write capacity, transport, failure mapping and result safety are
|
|
504
|
+
* the kit's), but its catalog keeps the legacy declaration's bytes:
|
|
505
|
+
* annotations, input and output schemas. `api()` validates every call, from
|
|
506
|
+
* every entry path, against that exact input schema before the handler runs
|
|
507
|
+
* (`callTool` in `src/connectors/api-connector.ts`, fail-closed), so the
|
|
508
|
+
* decoder accepts whatever reaches it and keeps the caller's key order. Step
|
|
509
|
+
* 2 replaces these schemas with Effect declarations (0011).
|
|
510
|
+
*/
|
|
511
|
+
export function googleTool(connection, declaration) {
|
|
512
|
+
const { annotations } = declaration;
|
|
513
|
+
const spec = {
|
|
514
|
+
name: declaration.name,
|
|
515
|
+
description: declaration.description,
|
|
516
|
+
input: ARGUMENTS,
|
|
517
|
+
output: Schema.Unknown,
|
|
518
|
+
...(declaration.writeSteps ? { writeSteps: declaration.writeSteps } : {}),
|
|
519
|
+
run: (args) => Effect.gen(function* () {
|
|
520
|
+
const operation = yield* Effect.serviceOption(Operation);
|
|
521
|
+
const ctx = Option.isSome(operation) ? operation.value : undefined;
|
|
522
|
+
if (!ctx)
|
|
523
|
+
return yield* Effect.die(new Error(`${declaration.name} needs the call's connector context.`));
|
|
524
|
+
return yield* declaration.run(args, ctx);
|
|
525
|
+
}).pipe(
|
|
526
|
+
// A product's own refusals are thrown ConnectorCallErrors, as on the legacy client.
|
|
527
|
+
Effect.catchDefect((defect) => defect instanceof ConnectorCallError ? Effect.fail(new Refusal({ error: defect })) : Effect.die(defect))),
|
|
528
|
+
};
|
|
529
|
+
const built = annotations.readOnlyHint === true && annotations.destructiveHint !== true
|
|
530
|
+
? read(connection, spec)
|
|
531
|
+
: write(connection, { ...spec, destructive: annotations.destructiveHint === true });
|
|
532
|
+
return Object.assign(built, {
|
|
533
|
+
annotations: declaration.annotations,
|
|
534
|
+
inputSchema: declaration.inputSchema,
|
|
535
|
+
outputSchema: declaration.outputSchema,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* around its rows — page objects, counts, ids — so a product sizes its rows
|
|
13
13
|
* against this and its envelope never needs measuring.
|
|
14
14
|
*/
|
|
15
|
+
import { redactBeforeCut } from "../../../sent-secrets.js";
|
|
15
16
|
/** The size one Workspace result is built to stay under, in UTF-8 JSON bytes. */
|
|
16
17
|
export const RESULT_BUDGET_BYTES = 192 * 1024;
|
|
17
18
|
const encoder = new TextEncoder();
|
|
@@ -24,9 +25,21 @@ export function jsonBytes(value) {
|
|
|
24
25
|
* `text` cut so that its JSON string encoding — quotes and escapes included —
|
|
25
26
|
* takes at most `maxBytes` UTF-8 bytes, with `marker(dropped)` appended when
|
|
26
27
|
* anything was cut. The cut never splits a surrogate pair, and the marker is
|
|
27
|
-
* counted inside the bound, so the result always fits.
|
|
28
|
+
* counted inside the bound, so the result always fits. Text that must be cut
|
|
29
|
+
* has every secret the call's request sent replaced first: a cut through an
|
|
30
|
+
* echo would leave a fragment no later redaction can find.
|
|
28
31
|
*/
|
|
29
|
-
export function clampText(
|
|
32
|
+
export function clampText(ctx, original, maxBytes, marker) {
|
|
33
|
+
if (jsonBytes(original) <= maxBytes)
|
|
34
|
+
return original;
|
|
35
|
+
return clampRedactedText(redactBeforeCut(ctx, original), maxBytes, marker);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* {@link clampText} for text whose sent secrets the caller already replaced,
|
|
39
|
+
* whole, before cutting any of it: a reader that pages one text from offsets
|
|
40
|
+
* must count every page in the same redacted text.
|
|
41
|
+
*/
|
|
42
|
+
export function clampRedactedText(text, maxBytes, marker) {
|
|
30
43
|
if (jsonBytes(text) <= maxBytes)
|
|
31
44
|
return text;
|
|
32
45
|
const fits = (length) => {
|
|
@@ -47,7 +47,8 @@ export interface DelegatedTokenRequest {
|
|
|
47
47
|
* what happened inside its own request — a token, Google's refusal, or
|
|
48
48
|
* `abandoned` for anything else — and a follower that hears `abandoned`, or
|
|
49
49
|
* reaches the deadline first, mints for itself. A refusal is rebuilt for each
|
|
50
|
-
* follower from its code and
|
|
50
|
+
* follower from its code, message and resend advice, never handed over as the
|
|
51
|
+
* owner's object.
|
|
51
52
|
*/
|
|
52
53
|
export declare function delegatedToken(request: DelegatedTokenRequest, ctx: ConnectorContext): Promise<string>;
|
|
53
54
|
/**
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* and never the material that proves it.
|
|
24
24
|
*/
|
|
25
25
|
import { guardedFetch, retryAfterMs } from "../../../connectors/guarded-fetch.js";
|
|
26
|
-
import { ConnectorCallError } from "../../../errors.js";
|
|
26
|
+
import { ConnectorCallError, resendAdvice, resendAdviceOf } from "../../../errors.js";
|
|
27
27
|
/** Google's OAuth 2.0 token endpoint, and the JWT audience it requires. */
|
|
28
28
|
const GOOGLE_TOKEN_ORIGIN = "https://oauth2.googleapis.com";
|
|
29
29
|
const GOOGLE_TOKEN_URL = `${GOOGLE_TOKEN_ORIGIN}/token`;
|
|
@@ -259,6 +259,7 @@ async function signAssertion(account, subject, scopes, nowSeconds) {
|
|
|
259
259
|
}
|
|
260
260
|
// --- Token exchange --------------------------------------------------------------
|
|
261
261
|
const tokenTransport = guardedFetch({
|
|
262
|
+
auxiliary: true,
|
|
262
263
|
provider: "Google OAuth",
|
|
263
264
|
baseUrl: GOOGLE_TOKEN_ORIGIN,
|
|
264
265
|
headers: { Accept: "application/json" },
|
|
@@ -309,7 +310,7 @@ function tokenFailure(account, scopes, status, headers, payload) {
|
|
|
309
310
|
return new ConnectorCallError("provider_permission_denied", `Google refused domain-wide delegation for ${account.clientEmail}.${said} A Workspace super admin must authorize ${client} in the Admin console (Security → Access and data control → API controls → Manage Domain Wide Delegation) with exactly these scopes: ${scopes.join(",")}. A new or changed grant can take up to 24 hours to apply.`);
|
|
310
311
|
}
|
|
311
312
|
if (code === "invalid_grant") {
|
|
312
|
-
return new ConnectorCallError("auth_required", `Google refused the delegated token.${said} The mapped Workspace user may not exist, may be suspended, or may be outside the domain that granted delegation; the service account key may have been deleted or disabled; or this host's clock may be minutes off. Check the deployment's subject mapping and the key before retrying
|
|
313
|
+
return resendAdvice(new ConnectorCallError("auth_required", `Google refused the delegated token.${said} The mapped Workspace user may not exist, may be suspended, or may be outside the domain that granted delegation; the service account key may have been deleted or disabled; or this host's clock may be minutes off. Check the deployment's subject mapping and the key.`), " before retrying");
|
|
313
314
|
}
|
|
314
315
|
if (code === "invalid_client" || code === "disabled_client") {
|
|
315
316
|
return new ConnectorCallError("auth_required", `Google rejected service account ${account.clientEmail} itself.${said} The account or its key may be disabled or deleted; create a new JSON key and redeploy.`);
|
|
@@ -436,7 +437,8 @@ function follow(flight, signal) {
|
|
|
436
437
|
* what happened inside its own request — a token, Google's refusal, or
|
|
437
438
|
* `abandoned` for anything else — and a follower that hears `abandoned`, or
|
|
438
439
|
* reaches the deadline first, mints for itself. A refusal is rebuilt for each
|
|
439
|
-
* follower from its code and
|
|
440
|
+
* follower from its code, message and resend advice, never handed over as the
|
|
441
|
+
* owner's object.
|
|
440
442
|
*/
|
|
441
443
|
export async function delegatedToken(request, ctx) {
|
|
442
444
|
const key = await cacheKey(request);
|
|
@@ -451,7 +453,8 @@ export async function delegatedToken(request, ctx) {
|
|
|
451
453
|
if (outcome.kind === "token")
|
|
452
454
|
return outcome.minted.accessToken;
|
|
453
455
|
if (outcome.kind === "refused") {
|
|
454
|
-
|
|
456
|
+
const refused = new ConnectorCallError(outcome.code, outcome.message, outcome.retryAfterMs === undefined ? {} : { retryAfterMs: outcome.retryAfterMs });
|
|
457
|
+
throw outcome.advice === undefined ? refused : resendAdvice(refused, outcome.advice);
|
|
455
458
|
}
|
|
456
459
|
if (flights.get(key) === flight)
|
|
457
460
|
flights.delete(key);
|
|
@@ -478,6 +481,7 @@ export async function delegatedToken(request, ctx) {
|
|
|
478
481
|
code: error.code,
|
|
479
482
|
message: error.message,
|
|
480
483
|
retryAfterMs: error.retryAfterMs,
|
|
484
|
+
advice: resendAdviceOf(error),
|
|
481
485
|
}
|
|
482
486
|
: { kind: "abandoned" });
|
|
483
487
|
throw error;
|