@zackbart/connecta 0.34.0 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +393 -0
- package/README.md +62 -105
- package/SECURITY.md +3 -2
- package/bin/connecta.mjs +41 -16
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.d.ts +2 -1
- package/dist/access-tokens.js +66 -30
- package/dist/activity.d.ts +1 -1
- package/dist/activity.js +2 -0
- package/dist/agent-config.d.ts +23 -0
- package/dist/agent-config.js +59 -0
- package/dist/agent-description.js +16 -0
- package/dist/agent-identity.js +4 -4
- package/dist/agent-observation.js +39 -0
- package/dist/auth/clerk-sdk/client.d.ts +2 -0
- package/dist/auth/clerk-sdk/generated.js +554 -563
- package/dist/auth/clerk-transport.js +70 -3
- package/dist/auth/clerk.d.ts +13 -0
- package/dist/auth/clerk.js +653 -246
- package/dist/auth/cloudflare-access.js +46 -42
- package/dist/auth/downstream-client-metadata.js +2 -2
- package/dist/auth/downstream-oauth.js +2051 -1160
- package/dist/auth/downstream-revocation.js +88 -88
- package/dist/auth/native-call.js +20 -0
- package/dist/auth/native.js +184 -0
- package/dist/auth/oauth-start-reset.js +11 -5
- package/dist/auth/oauth-token-response.js +121 -102
- package/dist/auth/oauth-v2-migration.js +97 -78
- package/dist/auth/renewable-grant.js +10 -0
- package/dist/auth/static-oauth.js +663 -271
- package/dist/auth/vercel-agent.js +266 -0
- package/dist/auth-admission.js +204 -0
- package/dist/auth-elicitation.js +350 -241
- package/dist/authorization-handoff.js +164 -90
- package/dist/call-admission-context.js +3 -10
- package/dist/call-admission.js +2 -0
- package/dist/call-classification.js +9 -0
- package/dist/call-recovery.js +32 -13
- package/dist/canonical-json.js +8 -0
- package/dist/catalog-cache.js +369 -213
- package/dist/catalog-service.js +659 -530
- package/dist/catalog.js +5 -1
- package/dist/concurrency.js +0 -25
- package/dist/config-defaults.d.ts +1 -0
- package/dist/config-defaults.js +1 -0
- package/dist/config-schema.d.ts +3 -1
- package/dist/config-schema.js +13 -3
- package/dist/config.d.ts +40 -6
- package/dist/config.js +40 -26
- package/dist/connector-caller.d.ts +4 -0
- package/dist/connector-caller.js +17 -4
- package/dist/connectors/api-connector.d.ts +15 -7
- package/dist/connectors/api-connector.js +147 -57
- package/dist/connectors/guarded-fetch.d.ts +51 -17
- package/dist/connectors/guarded-fetch.js +215 -121
- package/dist/connectors/negotiation-cache.js +20 -10
- package/dist/connectors/option-shapes.js +1 -1
- package/dist/connectors/remote-mcp.d.ts +12 -30
- package/dist/connectors/remote-mcp.js +960 -564
- package/dist/d1.js +10 -243
- package/dist/derived-context.js +5 -0
- package/dist/describe-config.d.ts +5 -1
- package/dist/describe-config.js +3 -0
- package/dist/downstream-input.js +347 -260
- package/dist/errors.d.ts +87 -6
- package/dist/errors.js +114 -4
- package/dist/execute.js +87 -50
- package/dist/executor-admission.js +3 -0
- package/dist/executors/quickjs.js +163 -112
- package/dist/guarded-transport.js +50 -0
- package/dist/guest-runtime.js +1 -1
- package/dist/http/client.js +439 -0
- package/dist/http/errors.js +558 -0
- package/dist/http/fields.js +49 -0
- package/dist/http/hatch-paging.js +318 -0
- package/dist/http/hatch.js +1279 -0
- package/dist/http/json-schema.js +267 -0
- package/dist/http/node-transport.js +241 -0
- package/dist/http/oauth.js +162 -0
- package/dist/http/paging.js +228 -0
- package/dist/http/provider.js +221 -0
- package/dist/http/safety.js +82 -0
- package/dist/http/services.js +79 -0
- package/dist/http/tools.js +383 -0
- package/dist/http/vendor-index.js +138 -0
- package/dist/http/vendor.js +448 -0
- package/dist/http/write-request.js +66 -0
- package/dist/http-policy.d.ts +11 -0
- package/dist/http-policy.js +45 -0
- package/dist/inbound-credential.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -0
- package/dist/invocation-auth.js +30 -19
- package/dist/invocation.js +221 -87
- package/dist/mcp-result.js +0 -4
- package/dist/membership.d.ts +4 -0
- package/dist/meta-output.js +1 -1
- package/dist/meta-tools.js +534 -408
- package/dist/native-recovery.js +52 -0
- package/dist/native-tools.js +382 -0
- package/dist/node.d.ts +4 -3
- package/dist/node.js +45 -8
- package/dist/oauth-handoff.js +177 -104
- package/dist/oauth-partition.js +6 -20
- package/dist/oauth-sealing.js +6 -7
- package/dist/operation-vocabulary.js +101 -0
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +17 -4
- package/dist/operator-ui/agents.d.ts +5 -4
- package/dist/operator-ui/agents.js +17 -7
- package/dist/operator-ui/config-summary.js +8 -4
- package/dist/operator-ui/connection-map.js +9 -6
- package/dist/operator-ui/contract.d.ts +7 -3
- package/dist/operator-ui/endpoints.js +5 -14
- package/dist/operator-ui/fix-prompts.d.ts +1 -1
- package/dist/operator-ui/fix-prompts.js +0 -8
- package/dist/operator-ui/generated.js +3 -3
- package/dist/operator-ui/health.js +52 -0
- package/dist/operator-ui/model.d.ts +2 -3
- package/dist/operator-ui/overview.js +11 -19
- package/dist/operator-ui/setup-commands.js +0 -4
- package/dist/operator-ui/view.d.ts +5 -29
- package/dist/operator-ui/view.js +3 -59
- package/dist/program-writes.js +47 -26
- package/dist/provider-call-facts.d.ts +115 -0
- package/dist/provider-call-facts.js +182 -0
- package/dist/provider-call.d.ts +112 -0
- package/dist/provider-call.js +271 -0
- package/dist/provider.d.ts +71 -32
- package/dist/provider.js +134 -40
- package/dist/providers/_shared/google/hatch.js +91 -0
- package/dist/providers/_shared/google/kit.js +537 -0
- package/dist/providers/_shared/google/result-size.js +15 -2
- package/dist/providers/_shared/google/service-account.d.ts +2 -1
- package/dist/providers/_shared/google/service-account.js +8 -4
- package/dist/providers/_shared/google/workspace.d.ts +8 -209
- package/dist/providers/_shared/google/workspace.js +7 -426
- package/dist/providers/_shared/rest/compact-index.js +86 -0
- package/dist/providers/_shared/rest/dispatch.js +3 -23
- package/dist/providers/_shared/rest/kit.js +546 -0
- package/dist/providers/_shared/rest/operation-index.js +230 -14
- package/dist/providers/_shared/rest/response-media.js +11 -0
- package/dist/providers/_shared/rest/tools.js +101 -78
- package/dist/providers/_shared/rest/value-safety.js +353 -195
- package/dist/providers/_shared/sensitive-fields.js +27 -0
- package/dist/providers/breeze/api.js +584 -0
- package/dist/providers/breeze/index.d.ts +3 -3
- package/dist/providers/breeze/index.js +67 -1420
- package/dist/providers/breeze/openapi.generated.js +60 -0
- package/dist/providers/breeze/skill.generated.js +2 -6
- package/dist/providers/breeze/tools.js +696 -0
- package/dist/providers/ccb/api.js +496 -0
- package/dist/providers/ccb/index.d.ts +17 -37
- package/dist/providers/ccb/index.js +77 -1702
- package/dist/providers/ccb/lib/constants.d.ts +34 -0
- package/dist/providers/ccb/lib/constants.js +74 -0
- package/dist/providers/ccb/lib/guide.js +9 -0
- package/dist/providers/ccb/lib/verdicts.js +387 -0
- package/dist/providers/ccb/openapi.generated.js +397 -0
- package/dist/providers/ccb/safety.js +295 -0
- package/dist/providers/ccb/skill.generated.js +2 -6
- package/dist/providers/ccb/tools.js +853 -0
- package/dist/providers/cloudflare/api.js +1066 -0
- package/dist/providers/cloudflare/index.d.ts +7 -22
- package/dist/providers/cloudflare/index.js +80 -194
- package/dist/providers/cloudflare/lib/config.d.ts +16 -0
- package/dist/providers/cloudflare/lib/config.js +2 -0
- package/dist/providers/cloudflare/lib/discovery.js +29 -0
- package/dist/providers/cloudflare/{graphql.js → lib/graphql.js} +1 -1
- package/dist/providers/cloudflare/lib/hatch.js +103 -0
- package/dist/providers/cloudflare/lib/path-bounds.js +28 -0
- package/dist/providers/cloudflare/lib/response-media.js +75 -0
- package/dist/providers/cloudflare/openapi.generated.js +3299 -3208
- package/dist/providers/cloudflare/{value-safety.js → safety.js} +538 -18
- package/dist/providers/cloudflare/skill.generated.js +2 -10
- package/dist/providers/cloudflare/tools.js +360 -0
- package/dist/providers/docs/api.js +51 -0
- package/dist/providers/docs/index.d.ts +1 -12
- package/dist/providers/docs/index.js +53 -1165
- package/dist/providers/docs/lib/constants.d.ts +18 -0
- package/dist/providers/docs/lib/constants.js +18 -0
- package/dist/providers/docs/lib/discovery.js +26 -0
- package/dist/providers/docs/lib/requests.js +48 -0
- package/dist/providers/docs/lib/schemas.js +168 -0
- package/dist/providers/docs/openapi.generated.js +16 -0
- package/dist/providers/docs/safety.js +54 -0
- package/dist/providers/docs/skill.generated.js +2 -6
- package/dist/providers/docs/tools.js +802 -0
- package/dist/providers/drive/api.js +177 -0
- package/dist/providers/drive/index.d.ts +1 -12
- package/dist/providers/drive/index.js +38 -1448
- package/dist/providers/drive/lib/constants.d.ts +6 -0
- package/dist/providers/drive/lib/constants.js +6 -0
- package/dist/providers/drive/lib/contracts.js +532 -0
- package/dist/providers/drive/lib/hatch.js +197 -0
- package/dist/providers/drive/openapi.generated.js +77 -0
- package/dist/providers/drive/safety.js +90 -0
- package/dist/providers/drive/skill.generated.js +2 -6
- package/dist/providers/drive/tools.js +1290 -0
- package/dist/providers/forms/api.js +71 -0
- package/dist/providers/forms/index.d.ts +1 -12
- package/dist/providers/forms/index.js +49 -1145
- package/dist/providers/forms/lib/constants.d.ts +14 -0
- package/dist/providers/forms/lib/constants.js +19 -0
- package/dist/providers/forms/lib/hatch.js +72 -0
- package/dist/providers/forms/openapi.generated.js +23 -0
- package/dist/providers/forms/safety.js +34 -0
- package/dist/providers/forms/skill.generated.js +2 -8
- package/dist/providers/forms/tools.js +1024 -0
- package/dist/providers/github/api.js +150 -0
- package/dist/providers/github/index.d.ts +4 -5
- package/dist/providers/github/index.js +30 -87
- package/dist/providers/github/{auth.d.ts → lib/auth.d.ts} +5 -3
- package/dist/providers/github/{auth.js → lib/auth.js} +40 -43
- package/dist/providers/github/lib/discovery.js +132 -0
- package/dist/providers/github/lib/operations.js +58 -0
- package/dist/providers/github/{scope.d.ts → lib/scope.d.ts} +2 -0
- package/dist/providers/github/{scope.js → lib/scope.js} +25 -1
- package/dist/providers/github/openapi.generated.js +1216 -0
- package/dist/providers/github/safety.js +216 -0
- package/dist/providers/github/skill.generated.js +2 -5
- package/dist/providers/github/tools.js +506 -0
- package/dist/providers/gmail/api.js +114 -0
- package/dist/providers/gmail/index.d.ts +1 -12
- package/dist/providers/gmail/index.js +73 -1621
- package/dist/providers/gmail/lib/constants.d.ts +13 -0
- package/dist/providers/gmail/lib/constants.js +16 -0
- package/dist/providers/gmail/lib/index.js +173 -0
- package/dist/providers/gmail/lib/schemas.js +176 -0
- package/dist/providers/gmail/lib/threading.js +120 -0
- package/dist/providers/gmail/openapi.generated.js +92 -0
- package/dist/providers/gmail/safety.js +21 -0
- package/dist/providers/gmail/skill.generated.js +2 -6
- package/dist/providers/gmail/tools.js +1435 -0
- package/dist/providers/overflow/api.js +1008 -0
- package/dist/providers/overflow/index.d.ts +3 -17
- package/dist/providers/overflow/index.js +60 -1488
- package/dist/providers/overflow/skill.generated.js +2 -5
- package/dist/providers/overflow/tools.js +873 -0
- package/dist/providers/planning-center/api.js +467 -0
- package/dist/providers/planning-center/index.d.ts +6 -35
- package/dist/providers/planning-center/index.js +69 -2445
- package/dist/providers/planning-center/lib/constants.d.ts +58 -0
- package/dist/providers/planning-center/lib/constants.js +83 -0
- package/dist/providers/planning-center/lib/discovery.js +40 -0
- package/dist/providers/planning-center/lib/guide.js +39 -0
- package/dist/providers/planning-center/lib/json-api.js +83 -0
- package/dist/providers/planning-center/lib/verdicts.js +1758 -0
- package/dist/providers/planning-center/openapi.generated.js +1764 -0
- package/dist/providers/planning-center/safety.js +188 -0
- package/dist/providers/planning-center/skill.generated.js +2 -27
- package/dist/providers/planning-center/tools.js +1010 -0
- package/dist/providers/sheets/api.js +133 -0
- package/dist/providers/sheets/index.d.ts +4 -16
- package/dist/providers/sheets/index.js +61 -1172
- package/dist/providers/sheets/lib/constants.d.ts +14 -0
- package/dist/providers/sheets/lib/constants.js +14 -0
- package/dist/providers/sheets/lib/hatch.js +163 -0
- package/dist/providers/sheets/lib/requests.js +105 -0
- package/dist/providers/sheets/lib/schemas.js +138 -0
- package/dist/providers/sheets/openapi.generated.js +30 -0
- package/dist/providers/sheets/safety.js +27 -0
- package/dist/providers/sheets/skill.generated.js +2 -7
- package/dist/providers/sheets/tools.js +734 -0
- package/dist/providers/slides/api.js +147 -0
- package/dist/providers/slides/index.d.ts +2 -10
- package/dist/providers/slides/index.js +48 -2555
- package/dist/providers/slides/lib/constants.d.ts +11 -0
- package/dist/providers/slides/lib/constants.js +11 -0
- package/dist/providers/slides/lib/requests.js +60 -0
- package/dist/providers/slides/lib/schemas.js +565 -0
- package/dist/providers/slides/openapi.generated.js +18 -0
- package/dist/providers/slides/skill.generated.js +2 -6
- package/dist/providers/slides/tools.js +1968 -0
- package/dist/providers/stripe/api.js +401 -0
- package/dist/providers/stripe/index.d.ts +7 -27
- package/dist/providers/stripe/index.js +75 -191
- package/dist/providers/stripe/lib/config.d.ts +4 -0
- package/dist/providers/stripe/lib/config.js +2 -0
- package/dist/providers/stripe/openapi.generated.js +4 -3
- package/dist/providers/stripe/{value-safety.js → safety.js} +5 -5
- package/dist/providers/stripe/skill.generated.js +2 -10
- package/dist/providers/stripe/tools.js +77 -0
- package/dist/providers/tithely/api.js +531 -0
- package/dist/providers/tithely/index.d.ts +19 -0
- package/dist/providers/tithely/index.js +86 -965
- package/dist/providers/tithely/skill.generated.js +2 -5
- package/dist/providers/tithely/tools.js +426 -0
- package/dist/providers/vercel/api.js +551 -0
- package/dist/providers/vercel/index.d.ts +11 -25
- package/dist/providers/vercel/index.js +67 -261
- package/dist/providers/vercel/lib/config.d.ts +2 -0
- package/dist/providers/vercel/lib/config.js +2 -0
- package/dist/providers/vercel/lib/index.js +117 -0
- package/dist/providers/vercel/lib/logs.js +100 -0
- package/dist/providers/vercel/lib/pin.js +270 -0
- package/dist/providers/vercel/openapi.generated.js +398 -376
- package/dist/providers/vercel/{value-safety.js → safety.js} +261 -42
- package/dist/providers/vercel/skill.generated.js +2 -7
- package/dist/providers/vercel/tools.js +642 -0
- package/dist/redaction-identity.js +47 -0
- package/dist/redaction-marker.js +125 -0
- package/dist/registry.d.ts +30 -28
- package/dist/registry.js +446 -413
- package/dist/routes/access-tokens.js +1 -1
- package/dist/routes/actor-labels.js +1 -1
- package/dist/routes/connect.js +1 -1
- package/dist/routes/dispatch.js +103 -0
- package/dist/routes/mcp.js +170 -79
- package/dist/routes/oauth.js +3 -1
- package/dist/routes/operator.js +2 -2
- package/dist/routes/shared.d.ts +5 -4
- package/dist/routes/shared.js +244 -177
- package/dist/routes/skills.js +10 -8
- package/dist/routes/ui-config.js +27 -3
- package/dist/routes/ui-data.js +29 -22
- package/dist/routes/ui.js +1 -1
- package/dist/runtime/call-admission.js +0 -26
- package/dist/runtime/call-entry.js +30 -0
- package/dist/runtime/call.js +556 -0
- package/dist/runtime/connector-scope.js +7 -0
- package/dist/runtime/credential-probe.js +34 -0
- package/dist/runtime/executor.js +40 -0
- package/dist/runtime/native-storage.js +63 -0
- package/dist/runtime/registry.js +27 -0
- package/dist/runtime/request-scope.js +72 -0
- package/dist/runtime/run.js +15 -7
- package/dist/runtime/shared-read.js +34 -5
- package/dist/runtime/storage.js +34 -21
- package/dist/sent-secrets.d.ts +194 -37
- package/dist/sent-secrets.js +1743 -340
- package/dist/server.js +5 -56
- package/dist/skills.js +330 -224
- package/dist/sqlite.js +15 -14
- package/dist/storage/d1-copy.js +255 -0
- package/dist/storage/d1-driver.js +15 -0
- package/dist/storage/keys.js +28 -0
- package/dist/storage/sql-driver.js +88 -0
- package/dist/storage/sql-native.js +544 -0
- package/dist/storage/sql-schema.d.ts +6 -0
- package/dist/storage/sql-schema.js +15 -0
- package/dist/storage/sql.d.ts +14 -20
- package/dist/storage/sql.js +29 -535
- package/dist/timeout.js +0 -26
- package/dist/tool-safety.js +20 -10
- package/dist/types.d.ts +11 -99
- package/dist/upward.js +3 -0
- package/dist/usage-guide.js +3 -1
- package/dist/validate.d.ts +4 -2
- package/dist/validate.js +86 -9
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +312 -292
- package/dist/workspace-subject.js +5 -1
- package/dist/write-outcome.js +454 -34
- package/documentation/README.md +27 -7
- package/documentation/architecture.md +2550 -186
- package/documentation/auth.md +821 -98
- package/documentation/code-mode.md +56 -21
- package/documentation/deploying.md +164 -20
- package/documentation/hosted-mcp-migration.md +460 -0
- package/documentation/integrating.md +144 -105
- package/documentation/meta-tools.md +411 -34
- package/documentation/operating.md +28 -4
- package/documentation/operator-tests.md +11 -2
- package/documentation/operator-ui.md +103 -33
- package/documentation/provider-auth-migration.md +38 -63
- package/documentation/provider-migration-0.29.md +10 -5
- package/documentation/provider-migration-0.35.md +975 -0
- package/documentation/providers.md +965 -0
- package/examples/worker/AGENTS.md +23 -9
- package/examples/worker/README.md +254 -135
- package/examples/worker/alchemy.run.ts +116 -0
- package/examples/worker/gitignore +10 -0
- package/examples/worker/src/connecta.config.ts +23 -21
- package/examples/worker/wrangler.jsonc +14 -5
- package/guides/connecting-agents.md +157 -0
- package/guides/how-it-works.md +156 -0
- package/guides/safety-and-trust.md +153 -0
- package/package.json +27 -42
- package/templates/node/.env.example +3 -0
- package/templates/node/README.md +22 -2
- package/templates/node/docker-compose.yml +1 -0
- package/templates/node/package.json +3 -2
- package/templates/node/src/connecta.config.ts +8 -0
- package/dist/catalog-drift.d.ts +0 -134
- package/dist/catalog-drift.js +0 -493
- package/dist/providers/_shared/request-token.js +0 -83
- package/dist/providers/_shared/rest/operation-index.d.ts +0 -133
- package/dist/providers/_shared/rest/tools.d.ts +0 -141
- package/dist/providers/_shared/rest/value-safety.d.ts +0 -201
- package/dist/providers/basecamp/index.d.ts +0 -43
- package/dist/providers/basecamp/index.js +0 -997
- package/dist/providers/basecamp/skill.generated.js +0 -9
- package/dist/providers/cloudflare/rest.d.ts +0 -37
- package/dist/providers/cloudflare/rest.js +0 -1078
- package/dist/providers/github/hosted.js +0 -218
- package/dist/providers/github/rest.js +0 -321
- package/dist/providers/infisical/index.d.ts +0 -7
- package/dist/providers/infisical/index.js +0 -713
- package/dist/providers/infisical/skill.generated.js +0 -8
- package/dist/providers/linear/index.d.ts +0 -55
- package/dist/providers/linear/index.js +0 -352
- package/dist/providers/linear/skill.generated.js +0 -8
- package/dist/providers/mixpanel/index.d.ts +0 -57
- package/dist/providers/mixpanel/index.js +0 -468
- package/dist/providers/mixpanel/operations.js +0 -20
- package/dist/providers/mixpanel/rest.js +0 -603
- package/dist/providers/mixpanel/skill.generated.js +0 -10
- package/dist/providers/mixpanel/value-safety.js +0 -16
- package/dist/providers/notion/index.d.ts +0 -65
- package/dist/providers/notion/index.js +0 -368
- package/dist/providers/notion/openapi.generated.js +0 -75
- package/dist/providers/notion/rest.d.ts +0 -24
- package/dist/providers/notion/rest.js +0 -1226
- package/dist/providers/notion/skill.generated.js +0 -11
- package/dist/providers/notion/value-safety.js +0 -56
- package/dist/providers/revenuecat/index.d.ts +0 -49
- package/dist/providers/revenuecat/index.js +0 -557
- package/dist/providers/revenuecat/skill.generated.js +0 -11
- package/dist/providers/stripe/rest.d.ts +0 -14
- package/dist/providers/stripe/rest.js +0 -402
- package/dist/providers/vercel/rest.d.ts +0 -20
- package/dist/providers/vercel/rest.js +0 -1020
- package/dist/providers/wispr-flow/index.d.ts +0 -14
- package/dist/providers/wispr-flow/index.js +0 -117
- package/dist/providers/wispr-flow/skill.generated.js +0 -8
- /package/dist/providers/github/{key.js → lib/key.js} +0 -0
package/dist/catalog-drift.js
DELETED
|
@@ -1,493 +0,0 @@
|
|
|
1
|
-
// Web-API only, like the rest of the core: a manifest comparison that ran on
|
|
2
|
-
// Node but not on Workers would leave half the deployments unable to tell a
|
|
3
|
-
// stale allowlist from a current one.
|
|
4
|
-
import { classifyTool } from "./tool-safety.js";
|
|
5
|
-
import { failureRecord, logFailure } from "./operator-record.js";
|
|
6
|
-
/** A count, or 0 when the seam returned something that is not one. */
|
|
7
|
-
function boundedCount(value) {
|
|
8
|
-
return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : 0;
|
|
9
|
-
}
|
|
10
|
-
/** An ISO-8601 date-time with a zone, as `Date.parse` reads it on every runtime. */
|
|
11
|
-
const ISO_INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
12
|
-
/**
|
|
13
|
-
* The report's time, re-serialized from the instant it names, so the string
|
|
14
|
-
* that leaves is always connecta's own ISO-8601 UTC form, never the seam's.
|
|
15
|
-
*/
|
|
16
|
-
function observationTime(value) {
|
|
17
|
-
if (typeof value !== "string" || !ISO_INSTANT.test(value))
|
|
18
|
-
return undefined;
|
|
19
|
-
const time = Date.parse(value);
|
|
20
|
-
return Number.isFinite(time) ? new Date(time).toISOString() : undefined;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Rebuild a drift report as bounded counts and a canonical timestamp.
|
|
24
|
-
*
|
|
25
|
-
* `Connector.catalogDrift()` sits on the open plugin seam, and what it returns
|
|
26
|
-
* lands in the body of unauthenticated `/health`, on connector status, in the
|
|
27
|
-
* operator page, and in `connecta doctor`. TypeScript constrains neither an
|
|
28
|
-
* extra enumerable property nor what `observedAt` holds at runtime, so "counts
|
|
29
|
-
* and a time and nothing else" is *made* true here — at the boundary where
|
|
30
|
-
* third-party output becomes a response — rather than trusted. A report whose
|
|
31
|
-
* `observedAt` names no instant is no observation connecta can date, and is
|
|
32
|
-
* dropped whole (INV-6): a plugin forwarding a downstream's report could
|
|
33
|
-
* otherwise put any text there. The activity path reconstructs its five
|
|
34
|
-
* fields for the same reason.
|
|
35
|
-
*/
|
|
36
|
-
export function boundedCatalogDrift(report) {
|
|
37
|
-
if (!report || typeof report !== "object")
|
|
38
|
-
return undefined;
|
|
39
|
-
const observedAt = observationTime(report.observedAt);
|
|
40
|
-
if (observedAt === undefined)
|
|
41
|
-
return undefined;
|
|
42
|
-
return {
|
|
43
|
-
observedAt,
|
|
44
|
-
unclassifiedTools: boundedCount(report.unclassifiedTools),
|
|
45
|
-
unservedTools: boundedCount(report.unservedTools),
|
|
46
|
-
annotationConflicts: boundedCount(report.annotationConflicts),
|
|
47
|
-
schemaChanges: boundedCount(report.schemaChanges),
|
|
48
|
-
...(boundedCount(report.droppedTools ?? 0) > 0 ? { droppedTools: boundedCount(report.droppedTools ?? 0) } : {}),
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
const encoder = new TextEncoder();
|
|
52
|
-
/**
|
|
53
|
-
* Most JSON values one tool's schemas may hold before its digest refuses to
|
|
54
|
-
* compute. A digest vouches for a whole schema or for nothing, so a schema
|
|
55
|
-
* past the bound is not hashed in part: the computation throws, and the
|
|
56
|
-
* caller serves every digested review unverified (INV-1).
|
|
57
|
-
*/
|
|
58
|
-
const MAX_SCHEMA_DIGEST_NODES = 100_000;
|
|
59
|
-
/** Object keys in the order `JSON.stringify` emits a key-sorted object. */
|
|
60
|
-
function canonicalKeys(value) {
|
|
61
|
-
const entries = Object.entries(value).filter(([, item]) => item !== undefined && typeof item !== "function" && typeof item !== "symbol");
|
|
62
|
-
entries.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
63
|
-
// A plain object enumerates integer-like keys first, in numeric order.
|
|
64
|
-
// Recorded digests were taken from a key-sorted object, so reproduce its
|
|
65
|
-
// enumeration rather than the sort alone.
|
|
66
|
-
const sorted = Object.fromEntries(entries);
|
|
67
|
-
return Object.keys(sorted).map((key) => [key, sorted[key]]);
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Deterministic JSON with object keys sorted, so key order is not a schema
|
|
71
|
-
* change. Iterative and complete: every leaf at every depth reaches the
|
|
72
|
-
* digest, and a schema deeper than the host stack cannot exhaust it. Throws
|
|
73
|
-
* past {@link MAX_SCHEMA_DIGEST_NODES}, and on any value JSON cannot carry.
|
|
74
|
-
*/
|
|
75
|
-
function canonicalJson(root) {
|
|
76
|
-
const out = [];
|
|
77
|
-
const stack = [{ value: root }];
|
|
78
|
-
let nodes = 0;
|
|
79
|
-
while (stack.length > 0) {
|
|
80
|
-
const task = stack.pop();
|
|
81
|
-
if ("text" in task) {
|
|
82
|
-
out.push(task.text);
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
if (++nodes > MAX_SCHEMA_DIGEST_NODES) {
|
|
86
|
-
throw new Error(`schema has more than ${MAX_SCHEMA_DIGEST_NODES} values; refusing to digest it in part.`);
|
|
87
|
-
}
|
|
88
|
-
const value = task.value;
|
|
89
|
-
if (value === null || value === undefined) {
|
|
90
|
-
out.push("null");
|
|
91
|
-
}
|
|
92
|
-
else if (typeof value === "string" || typeof value === "boolean") {
|
|
93
|
-
out.push(JSON.stringify(value));
|
|
94
|
-
}
|
|
95
|
-
else if (typeof value === "number") {
|
|
96
|
-
out.push(Number.isFinite(value) ? JSON.stringify(value) : "null");
|
|
97
|
-
}
|
|
98
|
-
else if (typeof value === "function" || typeof value === "symbol") {
|
|
99
|
-
// Only array items reach here; object keys holding these are dropped.
|
|
100
|
-
out.push("null");
|
|
101
|
-
}
|
|
102
|
-
else if (typeof value !== "object") {
|
|
103
|
-
throw new Error(`schema holds a ${typeof value}, which JSON cannot carry.`);
|
|
104
|
-
}
|
|
105
|
-
else if (Array.isArray(value)) {
|
|
106
|
-
out.push("[");
|
|
107
|
-
stack.push({ text: "]" });
|
|
108
|
-
for (let index = value.length - 1; index >= 0; index -= 1) {
|
|
109
|
-
stack.push({ value: value[index] });
|
|
110
|
-
if (index > 0)
|
|
111
|
-
stack.push({ text: "," });
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
else {
|
|
115
|
-
out.push("{");
|
|
116
|
-
stack.push({ text: "}" });
|
|
117
|
-
const entries = canonicalKeys(value);
|
|
118
|
-
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
119
|
-
const [key, item] = entries[index];
|
|
120
|
-
stack.push({ value: item });
|
|
121
|
-
stack.push({ text: `${index > 0 ? "," : ""}${JSON.stringify(key)}:` });
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
return out.join("");
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Digest the schemas of one downstream tool.
|
|
129
|
-
*
|
|
130
|
-
* Canonical rather than literal, unlike the catalog fingerprint in
|
|
131
|
-
* `src/catalog-fingerprint.ts`: that one is deliberately conservative because a
|
|
132
|
-
* spurious cache write is cheap, while a spurious drift finding spends a
|
|
133
|
-
* maintainer's attention on a downstream that reordered its JSON keys.
|
|
134
|
-
* Description and annotations are excluded — a reworded description is P1's
|
|
135
|
-
* business, and an annotation change is already its own drift category.
|
|
136
|
-
*
|
|
137
|
-
* Rejects rather than digesting part of a schema: a digest that ignored some
|
|
138
|
-
* leaf would let a reviewed read keep its verdict after that leaf changed.
|
|
139
|
-
*/
|
|
140
|
-
export async function vettedSchemaDigest(tool) {
|
|
141
|
-
const bytes = encoder.encode(canonicalJson({
|
|
142
|
-
inputSchema: tool.inputSchema ?? null,
|
|
143
|
-
outputSchema: tool.outputSchema ?? null,
|
|
144
|
-
}));
|
|
145
|
-
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
|
|
146
|
-
return `sha256:${[...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Build a provider's manifest from the lists it already maintains.
|
|
150
|
-
*
|
|
151
|
-
* Throws when a name is classified twice, because a tool that is both a read
|
|
152
|
-
* and a write is a review mistake that must not boot: the annotation the
|
|
153
|
-
* connector would apply depends on which branch runs first, and "safe by
|
|
154
|
-
* default" cannot be decided by ordering.
|
|
155
|
-
*/
|
|
156
|
-
export function vettedCatalog(input) {
|
|
157
|
-
const tools = new Map();
|
|
158
|
-
const digestFor = (name) => input.schemaDigests?.[name];
|
|
159
|
-
for (const name of input.reads) {
|
|
160
|
-
const digest = digestFor(name);
|
|
161
|
-
tools.set(name, {
|
|
162
|
-
verdict: "read-only",
|
|
163
|
-
...(digest !== undefined ? { schemaDigest: digest } : {}),
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
for (const [name, verdict] of input.writes) {
|
|
167
|
-
if (tools.has(name)) {
|
|
168
|
-
throw new Error(`vettedCatalog() classified "${name}" as both a read and a write.`);
|
|
169
|
-
}
|
|
170
|
-
const digest = digestFor(name);
|
|
171
|
-
tools.set(name, {
|
|
172
|
-
verdict,
|
|
173
|
-
...(digest !== undefined ? { schemaDigest: digest } : {}),
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
for (const name of Object.keys(input.schemaDigests ?? {})) {
|
|
177
|
-
if (!tools.has(name)) {
|
|
178
|
-
throw new Error(`vettedCatalog() recorded a schema digest for unclassified tool "${name}".`);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
return { version: 1, tools };
|
|
182
|
-
}
|
|
183
|
-
const VERDICTS = {
|
|
184
|
-
read: "read-only",
|
|
185
|
-
write: "additive",
|
|
186
|
-
destructive: "destructive",
|
|
187
|
-
};
|
|
188
|
-
const PUBLIC_VERDICTS = {
|
|
189
|
-
"read-only": "read",
|
|
190
|
-
additive: "write",
|
|
191
|
-
destructive: "destructive",
|
|
192
|
-
};
|
|
193
|
-
const SCHEMA_DIGEST = /^sha256:[0-9a-f]{64}$/;
|
|
194
|
-
const REVIEWED_TOOL_KEYS = new Set(["verdict", "reason", "schemaDigest"]);
|
|
195
|
-
function isRecord(value) {
|
|
196
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
197
|
-
}
|
|
198
|
-
/**
|
|
199
|
-
* Validate a public {@link ToolClassification} once, reading every value a
|
|
200
|
-
* single time, into the manifest the registry classifies with and a
|
|
201
|
-
* deep-frozen copy of the record.
|
|
202
|
-
*
|
|
203
|
-
* Every structural mistake throws, naming `owner`, because a classification
|
|
204
|
-
* that half-applies is worse than one that refuses to boot (INV-11). Unknown
|
|
205
|
-
* keys throw too: a misspelled `verdict` must not quietly leave a write
|
|
206
|
-
* unclassified.
|
|
207
|
-
*/
|
|
208
|
-
function parseClassification(classification, owner) {
|
|
209
|
-
function fail(detail) {
|
|
210
|
-
throw new Error(`[connecta] ${owner} classify ${detail}`);
|
|
211
|
-
}
|
|
212
|
-
if (!isRecord(classification))
|
|
213
|
-
fail("must be an object with a tools record.");
|
|
214
|
-
for (const key of Object.keys(classification)) {
|
|
215
|
-
if (key !== "tools" && key !== "unlisted")
|
|
216
|
-
fail(`has unknown key "${key}"; only "tools" and "unlisted" are accepted.`);
|
|
217
|
-
}
|
|
218
|
-
const unlisted = classification.unlisted;
|
|
219
|
-
if (unlisted !== undefined && unlisted !== "hide")
|
|
220
|
-
fail('unlisted must be "hide" when set.');
|
|
221
|
-
const input = classification.tools;
|
|
222
|
-
if (!isRecord(input))
|
|
223
|
-
fail("tools must be a record of tool name to verdict.");
|
|
224
|
-
const tools = new Map();
|
|
225
|
-
const frozen = {};
|
|
226
|
-
for (const [name, entry] of Object.entries(input)) {
|
|
227
|
-
if (!name || name.trim() !== name) {
|
|
228
|
-
fail(`tool name "${name}" is empty or has surrounding whitespace.`);
|
|
229
|
-
}
|
|
230
|
-
const record = typeof entry === "string" ? { verdict: entry } : isRecord(entry) ? { ...entry } : {};
|
|
231
|
-
for (const key of Object.keys(record)) {
|
|
232
|
-
if (!REVIEWED_TOOL_KEYS.has(key))
|
|
233
|
-
fail(`tool "${name}" has unknown key "${key}".`);
|
|
234
|
-
}
|
|
235
|
-
const { verdict: publicVerdict, reason, schemaDigest: digest } = record;
|
|
236
|
-
const verdict = typeof publicVerdict === "string" && Object.hasOwn(VERDICTS, publicVerdict) ? VERDICTS[publicVerdict] : undefined;
|
|
237
|
-
if (verdict === undefined) {
|
|
238
|
-
fail(`tool "${name}" needs verdict "read", "write", or "destructive".`);
|
|
239
|
-
}
|
|
240
|
-
if (reason !== undefined && (typeof reason !== "string" || !reason.trim())) {
|
|
241
|
-
fail(`tool "${name}" reason must be a non-empty string.`);
|
|
242
|
-
}
|
|
243
|
-
if (digest !== undefined && (typeof digest !== "string" || !SCHEMA_DIGEST.test(digest))) {
|
|
244
|
-
fail(`tool "${name}" schemaDigest must be "sha256:" and 64 lowercase hex digits.`);
|
|
245
|
-
}
|
|
246
|
-
tools.set(name, {
|
|
247
|
-
verdict,
|
|
248
|
-
...(typeof digest === "string" ? { schemaDigest: digest } : {}),
|
|
249
|
-
});
|
|
250
|
-
frozen[name] =
|
|
251
|
-
typeof entry === "string"
|
|
252
|
-
? PUBLIC_VERDICTS[verdict]
|
|
253
|
-
: Object.freeze({
|
|
254
|
-
verdict: PUBLIC_VERDICTS[verdict],
|
|
255
|
-
...(typeof reason === "string" ? { reason } : {}),
|
|
256
|
-
...(typeof digest === "string" ? { schemaDigest: digest } : {}),
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
return {
|
|
260
|
-
catalog: { version: 1, tools, ...(unlisted ? { unlisted } : {}) },
|
|
261
|
-
frozen: Object.freeze({ tools: Object.freeze(frozen), ...(unlisted ? { unlisted } : {}) }),
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
/** Validate a {@link ToolClassification} and build the manifest it describes. */
|
|
265
|
-
export function reviewedCatalog(classification, owner) {
|
|
266
|
-
return parseClassification(classification, owner).catalog;
|
|
267
|
-
}
|
|
268
|
-
/**
|
|
269
|
-
* Validate a {@link ToolClassification} and return a deep-frozen copy of it:
|
|
270
|
-
* what a connector carries as `classification`, so neither the caller's
|
|
271
|
-
* original object nor a write through the connector can change a verdict
|
|
272
|
-
* after review.
|
|
273
|
-
*/
|
|
274
|
-
export function reviewedClassification(classification, owner) {
|
|
275
|
-
return parseClassification(classification, owner).frozen;
|
|
276
|
-
}
|
|
277
|
-
/**
|
|
278
|
-
* One reviewed tool as served: a fresh object, never one a connector, a
|
|
279
|
-
* cache, or an earlier read holds.
|
|
280
|
-
*
|
|
281
|
-
* A listed read fills downstream silence, and an explicit write annotation on
|
|
282
|
-
* it wins: `destructiveHint: true` or `readOnlyHint: false` on a classified
|
|
283
|
-
* read is the downstream telling us the review is stale. A listed write or
|
|
284
|
-
* destructive tool stays a write whatever the downstream claims; there
|
|
285
|
-
* connecta knows what the tool does, and a claim to the contrary is a
|
|
286
|
-
* downstream bug rather than news
|
|
287
|
-
* ([#310](https://github.com/zackbart/connecta/issues/310),
|
|
288
|
-
* [#315](https://github.com/zackbart/connecta/issues/315)). A tool no review
|
|
289
|
-
* lists keeps only an explicit read annotation, the downstream's own word
|
|
290
|
-
* being the only evidence there is. A reviewed tool whose recorded schema
|
|
291
|
-
* digest the live tool no longer matches, or could not be checked against,
|
|
292
|
-
* keeps no reviewed verdict and is a write.
|
|
293
|
-
*/
|
|
294
|
-
function servedTool(catalog, fact, lapsed, override) {
|
|
295
|
-
const definition = structuredClone(fact);
|
|
296
|
-
const reviewed = catalog.tools.get(fact.name);
|
|
297
|
-
definition.classification = classifyTool(fact, override, reviewed
|
|
298
|
-
? {
|
|
299
|
-
verdict: PUBLIC_VERDICTS[reviewed.verdict],
|
|
300
|
-
stale: lapsed.has(fact.name),
|
|
301
|
-
}
|
|
302
|
-
: undefined);
|
|
303
|
-
const downstream = definition.annotations ?? {};
|
|
304
|
-
const record = catalog.tools.get(definition.name);
|
|
305
|
-
const annotate = (annotations) => ({
|
|
306
|
-
...definition,
|
|
307
|
-
annotations: override === undefined
|
|
308
|
-
? annotations
|
|
309
|
-
: { ...annotations, readOnlyHint: override === "read", destructiveHint: override === "write" },
|
|
310
|
-
});
|
|
311
|
-
if (record?.verdict === "destructive") {
|
|
312
|
-
return annotate({ ...downstream, readOnlyHint: false, destructiveHint: true });
|
|
313
|
-
}
|
|
314
|
-
if (record && lapsed.has(definition.name)) {
|
|
315
|
-
// A review vouches for the schema it read. When the live schema no longer
|
|
316
|
-
// matches that digest, or the digest could not be checked, the verdict is
|
|
317
|
-
// about some other tool: the read becomes a write until a release reviews
|
|
318
|
-
// it again (INV-1).
|
|
319
|
-
return annotate({ ...downstream, readOnlyHint: false });
|
|
320
|
-
}
|
|
321
|
-
if (record?.verdict === "read-only") {
|
|
322
|
-
if (downstream.destructiveHint === true || downstream.readOnlyHint === false) {
|
|
323
|
-
return annotate(downstream);
|
|
324
|
-
}
|
|
325
|
-
return annotate({
|
|
326
|
-
...downstream,
|
|
327
|
-
readOnlyHint: true,
|
|
328
|
-
destructiveHint: downstream.destructiveHint ?? false,
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
if (record?.verdict === "additive") {
|
|
332
|
-
return annotate({ ...downstream, readOnlyHint: false });
|
|
333
|
-
}
|
|
334
|
-
// Fill-in only: a silent tool is not read-only, so drift fails closed onto
|
|
335
|
-
// `call_destructive_tool`, and it gets no `destructiveHint` it has not
|
|
336
|
-
// earned. An explicit read claim on a name no review lists is believed.
|
|
337
|
-
return annotate({ ...downstream, readOnlyHint: downstream.readOnlyHint ?? false });
|
|
338
|
-
}
|
|
339
|
-
/**
|
|
340
|
-
* Whether the downstream's own annotation contradicts what a release reviewed.
|
|
341
|
-
*
|
|
342
|
-
* Only an *explicit* contradiction counts. Silence is the ordinary case the
|
|
343
|
-
* classification exists to fill, and an unclassified tool is already counted
|
|
344
|
-
* as an addition rather than twice.
|
|
345
|
-
*/
|
|
346
|
-
function contradicts(record, definition) {
|
|
347
|
-
const downstream = definition.annotations ?? {};
|
|
348
|
-
if (record.verdict === "read-only") {
|
|
349
|
-
return downstream.readOnlyHint === false || downstream.destructiveHint === true;
|
|
350
|
-
}
|
|
351
|
-
return downstream.readOnlyHint === true;
|
|
352
|
-
}
|
|
353
|
-
/**
|
|
354
|
-
* Names of served tools whose schemas no longer match the digest a release
|
|
355
|
-
* recorded for them. A manifest that recorded no digest for a tool cannot have
|
|
356
|
-
* an opinion about its schema, so it does not pay for a hash either. Throws
|
|
357
|
-
* when a digest cannot be computed; the caller decides what that means.
|
|
358
|
-
*/
|
|
359
|
-
async function changedSchemas(catalog, tools) {
|
|
360
|
-
const changed = new Set();
|
|
361
|
-
for (const definition of tools) {
|
|
362
|
-
const recorded = catalog.tools.get(definition.name)?.schemaDigest;
|
|
363
|
-
if (recorded !== undefined && recorded !== (await vettedSchemaDigest(definition))) {
|
|
364
|
-
changed.add(definition.name);
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
return changed;
|
|
368
|
-
}
|
|
369
|
-
/** Every served tool whose review recorded a digest: what cannot be verified. */
|
|
370
|
-
function digestedTools(catalog, tools) {
|
|
371
|
-
return new Set(tools
|
|
372
|
-
.filter((definition) => catalog.tools.get(definition.name)?.schemaDigest !== undefined)
|
|
373
|
-
.map((definition) => definition.name));
|
|
374
|
-
}
|
|
375
|
-
function countDrift(catalog, tools, changed) {
|
|
376
|
-
let unclassifiedTools = 0;
|
|
377
|
-
let annotationConflicts = 0;
|
|
378
|
-
const served = new Set();
|
|
379
|
-
for (const definition of tools) {
|
|
380
|
-
served.add(definition.name);
|
|
381
|
-
const record = catalog.tools.get(definition.name);
|
|
382
|
-
if (!record) {
|
|
383
|
-
unclassifiedTools += 1;
|
|
384
|
-
continue;
|
|
385
|
-
}
|
|
386
|
-
if (contradicts(record, definition))
|
|
387
|
-
annotationConflicts += 1;
|
|
388
|
-
}
|
|
389
|
-
let unservedTools = 0;
|
|
390
|
-
for (const name of catalog.tools.keys()) {
|
|
391
|
-
if (!served.has(name))
|
|
392
|
-
unservedTools += 1;
|
|
393
|
-
}
|
|
394
|
-
return {
|
|
395
|
-
unclassifiedTools,
|
|
396
|
-
unservedTools,
|
|
397
|
-
annotationConflicts,
|
|
398
|
-
schemaChanges: changed.size,
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
/**
|
|
402
|
-
* Compare a live catalog with the manifest and count what moved.
|
|
403
|
-
*
|
|
404
|
-
* Counts only, and by construction: there is nowhere here to put a tool name,
|
|
405
|
-
* a schema, or a downstream string, so no later surface has to remember to
|
|
406
|
-
* strip one. Reads nothing but the tools it was handed — the caller already
|
|
407
|
-
* fetched them to serve a request, and this function never fetches anything.
|
|
408
|
-
*/
|
|
409
|
-
export async function detectCatalogDrift(catalog, tools) {
|
|
410
|
-
return countDrift(catalog, tools, await changedSchemas(catalog, tools));
|
|
411
|
-
}
|
|
412
|
-
/**
|
|
413
|
-
* Each connector object's review, validated the first time a registry reads
|
|
414
|
-
* it. Read once per object, so a getter or a record mutated later cannot
|
|
415
|
-
* hand a later registry (a principal's, say) a different review.
|
|
416
|
-
*/
|
|
417
|
-
const reviews = new WeakMap();
|
|
418
|
-
/**
|
|
419
|
-
* The review `connector.classification` declares, or undefined when it
|
|
420
|
-
* declares none. Throws on a malformed one (INV-11).
|
|
421
|
-
*/
|
|
422
|
-
export function catalogReviewOf(connector) {
|
|
423
|
-
let review = reviews.get(connector);
|
|
424
|
-
if (review === undefined) {
|
|
425
|
-
const classification = connector.classification;
|
|
426
|
-
review = classification === undefined ? null : reviewedCatalog(classification, `connector "${connector.id}"`);
|
|
427
|
-
reviews.set(connector, review);
|
|
428
|
-
}
|
|
429
|
-
return review ?? undefined;
|
|
430
|
-
}
|
|
431
|
-
/**
|
|
432
|
-
* Classify downstream facts with a review: what the registry serves on every
|
|
433
|
-
* read, from whichever cache layer the facts came from.
|
|
434
|
-
*
|
|
435
|
-
* Every tool returned is a fresh object, so nothing a caller does to one can
|
|
436
|
-
* reach the facts, `memo`, or a later read. `memo` holds digest verification
|
|
437
|
-
* per facts array; the registry passes one only for deep-frozen arrays it
|
|
438
|
-
* owns. A failed verification is not kept, so the next read tries again, and
|
|
439
|
-
* meanwhile no digested review vouches for a read (INV-1).
|
|
440
|
-
*/
|
|
441
|
-
export async function classifyCatalog(catalog, connectorId, facts, logger, memo, overrides) {
|
|
442
|
-
let lapsed = memo?.get(facts);
|
|
443
|
-
if (!lapsed) {
|
|
444
|
-
try {
|
|
445
|
-
lapsed = await changedSchemas(catalog, facts);
|
|
446
|
-
memo?.set(facts, lapsed);
|
|
447
|
-
}
|
|
448
|
-
catch (error) {
|
|
449
|
-
lapsed = digestedTools(catalog, facts);
|
|
450
|
-
logFailure(logger, "schema digest check failed; serving digested reviews as writes", failureRecord({ connector: connectorId }, error));
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
const unverified = lapsed;
|
|
454
|
-
return facts
|
|
455
|
-
.filter((fact) => catalog.unlisted !== "hide" || catalog.tools.has(fact.name))
|
|
456
|
-
.map((fact) => servedTool(catalog, fact, unverified, overrides?.[fact.name]));
|
|
457
|
-
}
|
|
458
|
-
/**
|
|
459
|
-
* The drift each reviewed connector object showed on its last listing in this
|
|
460
|
-
* runtime. Keyed by the connector rather than by registry, so a principal's
|
|
461
|
-
* registry and the root report one observation, as they serve one connector.
|
|
462
|
-
*/
|
|
463
|
-
const observations = new WeakMap();
|
|
464
|
-
/**
|
|
465
|
-
* Compare a listing a refresh just received with the connector's review, and
|
|
466
|
-
* keep the counts as its latest observation.
|
|
467
|
-
*
|
|
468
|
-
* The check happens where the tools are already in hand, while serving a
|
|
469
|
-
* refresh the deployment asked for. It adds no request of its own, which is
|
|
470
|
-
* the whole boundary: connecta watches a contract while it is serving a
|
|
471
|
-
* refresh, and never initiates one to go looking
|
|
472
|
-
* ([#179](https://github.com/zackbart/connecta/issues/179),
|
|
473
|
-
* [#343](https://github.com/zackbart/connecta/issues/343)).
|
|
474
|
-
*/
|
|
475
|
-
export async function observeReviewedDrift(connector, catalog, listed, logger) {
|
|
476
|
-
try {
|
|
477
|
-
const changed = await changedSchemas(catalog, listed);
|
|
478
|
-
observations.set(connector, {
|
|
479
|
-
observedAt: new Date().toISOString(),
|
|
480
|
-
...countDrift(catalog, listed, changed),
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
|
-
catch (error) {
|
|
484
|
-
// A drift check is a report about a catalog, never a condition for
|
|
485
|
-
// serving one. Keep the last good observation rather than replacing it
|
|
486
|
-
// with a lie, and let the refresh through; classification fails closed.
|
|
487
|
-
logFailure(logger, "catalog drift check failed", failureRecord({ connector: connector.id }, error));
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
/** The latest drift {@link observeReviewedDrift} kept for `connector`. */
|
|
491
|
-
export function observedCatalogDrift(connector) {
|
|
492
|
-
return observations.get(connector);
|
|
493
|
-
}
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/** In-memory tokens and credential-bound login flights shared only inside one request. */
|
|
2
|
-
import { ConnectorCallError } from "../../errors.js";
|
|
3
|
-
const LOGIN_TIMEOUT_MS = 15_000;
|
|
4
|
-
const MAX_CACHED_TOKENS = 512;
|
|
5
|
-
/** Each provider instance owns its cache, so no credential crosses an API origin. */
|
|
6
|
-
export function requestTokenCache(source) {
|
|
7
|
-
const tokens = new Map();
|
|
8
|
-
const requests = new WeakMap();
|
|
9
|
-
function start(auth, ctx) {
|
|
10
|
-
const controller = new AbortController();
|
|
11
|
-
const timer = setTimeout(() => controller.abort(new ConnectorCallError("unavailable", "Machine identity login timed out.")), LOGIN_TIMEOUT_MS);
|
|
12
|
-
const flight = { controller, waiters: 0, settled: false, promise: Promise.resolve("") };
|
|
13
|
-
flight.promise = auth
|
|
14
|
-
.mint({ ...ctx, signal: controller.signal })
|
|
15
|
-
.then((minted) => {
|
|
16
|
-
// An abandoned login must not seed a token after cancellation.
|
|
17
|
-
controller.signal.throwIfAborted();
|
|
18
|
-
tokens.delete(auth.key);
|
|
19
|
-
tokens.set(auth.key, minted);
|
|
20
|
-
while (tokens.size > MAX_CACHED_TOKENS)
|
|
21
|
-
tokens.delete(tokens.keys().next().value);
|
|
22
|
-
return minted.accessToken;
|
|
23
|
-
})
|
|
24
|
-
.finally(() => {
|
|
25
|
-
flight.settled = true;
|
|
26
|
-
clearTimeout(timer);
|
|
27
|
-
});
|
|
28
|
-
// All waiters may cancel before the transport observes its abort.
|
|
29
|
-
void flight.promise.catch(() => { });
|
|
30
|
-
return flight;
|
|
31
|
-
}
|
|
32
|
-
async function follow(flight, signal) {
|
|
33
|
-
signal?.throwIfAborted();
|
|
34
|
-
flight.waiters++;
|
|
35
|
-
let onAbort;
|
|
36
|
-
try {
|
|
37
|
-
const aborted = new Promise((_, reject) => {
|
|
38
|
-
if (!signal)
|
|
39
|
-
return;
|
|
40
|
-
onAbort = () => reject(signal.reason);
|
|
41
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
42
|
-
});
|
|
43
|
-
return await Promise.race([flight.promise, aborted]);
|
|
44
|
-
}
|
|
45
|
-
finally {
|
|
46
|
-
if (onAbort)
|
|
47
|
-
signal?.removeEventListener("abort", onAbort);
|
|
48
|
-
flight.waiters--;
|
|
49
|
-
if (!flight.settled && flight.waiters === 0)
|
|
50
|
-
flight.controller.abort();
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return {
|
|
54
|
-
async token(ctx, rejected) {
|
|
55
|
-
ctx.signal?.throwIfAborted();
|
|
56
|
-
const auth = await source(ctx);
|
|
57
|
-
ctx.signal?.throwIfAborted();
|
|
58
|
-
const cached = tokens.get(auth.key);
|
|
59
|
-
if (cached && cached.accessToken !== rejected && cached.expiresAt > Date.now())
|
|
60
|
-
return cached.accessToken;
|
|
61
|
-
const scope = ctx.requestScope ?? ctx;
|
|
62
|
-
let perCredential = requests.get(scope);
|
|
63
|
-
if (!perCredential) {
|
|
64
|
-
perCredential = new Map();
|
|
65
|
-
requests.set(scope, perCredential);
|
|
66
|
-
}
|
|
67
|
-
let request = perCredential.get(auth.key);
|
|
68
|
-
if (!request) {
|
|
69
|
-
request = {};
|
|
70
|
-
perCredential.set(auth.key, request);
|
|
71
|
-
}
|
|
72
|
-
if (rejected !== undefined) {
|
|
73
|
-
// Keep the recovery outcome for the request, including failures. Later
|
|
74
|
-
// 401s can never turn this into a fresh-login loop.
|
|
75
|
-
request.recovery ??= start(auth, ctx);
|
|
76
|
-
return await follow(request.recovery, ctx.signal);
|
|
77
|
-
}
|
|
78
|
-
if (!request.initial || request.initial.settled || request.initial.controller.signal.aborted)
|
|
79
|
-
request.initial = start(auth, ctx);
|
|
80
|
-
return await follow(request.initial, ctx.signal);
|
|
81
|
-
},
|
|
82
|
-
};
|
|
83
|
-
}
|