@zackbart/connecta 0.28.1 → 0.30.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 +855 -5
- package/README.md +125 -116
- package/bin/connecta.mjs +121 -139
- package/bin/meta-tool-names.d.mts +2 -0
- package/bin/meta-tool-names.mjs +9 -0
- package/bin/version.mjs +3 -0
- package/dist/access-tokens.js +53 -63
- package/dist/activity-request.js +8 -0
- package/dist/activity.d.ts +76 -42
- package/dist/activity.js +110 -42
- package/dist/argument-redaction.js +230 -0
- package/dist/auth/clerk-sdk/client.d.ts +65 -0
- package/dist/auth/clerk-sdk/client.js +5 -0
- package/dist/auth/clerk-sdk/generated.js +1845 -0
- package/dist/auth/clerk-transport.js +19 -0
- package/dist/auth/clerk.d.ts +17 -7
- package/dist/auth/clerk.js +189 -107
- package/dist/auth/cloudflare-access.js +25 -26
- package/dist/auth/downstream-client-metadata.d.ts +29 -0
- package/dist/auth/downstream-client-metadata.js +133 -0
- package/dist/auth/downstream-oauth.js +1584 -1580
- package/dist/auth/downstream-revocation.js +106 -0
- package/dist/auth/oauth-token-response.js +231 -0
- package/dist/auth/oauth-v2-migration.js +116 -0
- package/dist/auth/static-oauth.js +93 -71
- package/dist/auth-elicitation.js +288 -0
- package/dist/authorization-handoff.js +111 -0
- package/dist/branding.d.ts +8 -16
- package/dist/branding.js +26 -59
- package/dist/byte-read-response.js +39 -0
- package/dist/call-admission.js +12 -16
- package/dist/catalog-cache.js +739 -0
- package/dist/catalog-drift.d.ts +50 -15
- package/dist/catalog-drift.js +365 -128
- package/dist/catalog-fingerprint.js +2 -4
- package/dist/catalog-freshness.js +13 -0
- package/dist/catalog-limits.js +2 -2
- package/dist/catalog-service.js +409 -225
- package/dist/catalog.js +43 -84
- package/dist/concurrency.js +3 -4
- package/dist/config-defaults.d.ts +39 -0
- package/dist/config-defaults.js +46 -0
- package/dist/config-schema.d.ts +181 -0
- package/dist/config-schema.js +435 -0
- package/dist/config-value-sources.js +102 -0
- package/dist/config.d.ts +633 -0
- package/dist/config.js +508 -0
- package/dist/connector-access.d.ts +1 -0
- package/dist/connector-access.js +13 -15
- package/dist/connector-caller.d.ts +8 -0
- package/dist/connector-caller.js +15 -6
- package/dist/connector-scope.js +1 -1
- package/dist/connectors/api-connector.d.ts +59 -7
- package/dist/connectors/api-connector.js +106 -19
- package/dist/connectors/api.js +4 -1
- package/dist/connectors/guarded-fetch.d.ts +144 -0
- package/dist/connectors/guarded-fetch.js +87 -38
- package/dist/connectors/negotiation-cache.js +46 -0
- package/dist/connectors/option-shapes.js +27 -0
- package/dist/connectors/remote-mcp.d.ts +30 -4
- package/dist/connectors/remote-mcp.js +1774 -320
- package/dist/connectors/resource-uri.js +335 -0
- package/dist/credential-contract.d.ts +10 -0
- package/dist/credential-rules.js +1 -3
- package/dist/credential-url.js +108 -0
- package/dist/credentials.d.ts +6 -1
- package/dist/credentials.js +27 -18
- package/dist/d1.d.ts +130 -0
- package/dist/d1.js +290 -0
- package/dist/describe-config.d.ts +165 -0
- package/dist/describe-config.js +315 -0
- package/dist/described.js +106 -0
- package/dist/downstream-input-context.js +128 -0
- package/dist/downstream-input.js +325 -0
- package/dist/errors.d.ts +132 -31
- package/dist/errors.js +175 -87
- package/dist/execute.js +798 -318
- package/dist/executor-admission.d.ts +1 -0
- package/dist/executor-admission.js +11 -15
- package/dist/executor-contract.d.ts +20 -0
- package/dist/executor-contract.js +48 -0
- package/dist/executor-result.js +30 -13
- package/dist/executors/quickjs-child.js +15 -9
- package/dist/executors/quickjs-protocol.js +5 -6
- package/dist/executors/quickjs-runtime.js +152 -59
- package/dist/executors/quickjs.js +49 -39
- package/dist/favicon.js +1 -2
- package/dist/fix-prompt.js +1 -3
- package/dist/guest-runtime.js +238 -0
- package/dist/guest-types.d.ts +218 -0
- package/dist/guest-types.js +1 -0
- package/dist/html-security.js +24 -0
- package/dist/identity.js +1 -3
- package/dist/inbound-credential.js +52 -0
- package/dist/index.d.ts +24 -256
- package/dist/index.js +65 -478
- package/dist/invocation-auth.js +69 -0
- package/dist/invocation.js +262 -79
- package/dist/json-schema.d.ts +1 -1
- package/dist/mcp-client-context.js +35 -0
- package/dist/mcp-result.js +11 -7
- package/dist/meta-output.js +61 -0
- package/dist/meta-tool-names.d.ts +2 -0
- package/dist/meta-tool-names.js +3 -0
- package/dist/meta-tools.js +419 -272
- package/dist/module-contracts.d.ts +8 -23
- package/dist/node.d.ts +0 -2
- package/dist/node.js +2 -2
- package/dist/oauth-callback-outcome.js +1 -3
- package/dist/oauth-handoff.js +194 -0
- package/dist/oauth-sealing.js +20 -0
- package/dist/operator-record.d.ts +137 -0
- package/dist/operator-record.js +420 -0
- package/dist/operator-ui/assets.js +31 -0
- package/dist/operator-ui/contract.d.ts +72 -0
- package/dist/operator-ui/contract.js +1 -0
- package/dist/operator-ui/fix-prompts.d.ts +18 -0
- package/dist/operator-ui/generated.js +5 -4
- package/dist/operator-ui/model.d.ts +12 -87
- package/dist/operator-ui/model.js +2 -9
- package/dist/operator-ui/setup-commands.js +1 -3
- package/dist/operator-ui/view.d.ts +323 -0
- package/dist/operator-ui/view.js +62 -189
- package/dist/page-styles.js +2 -3
- package/dist/program-result.js +5 -0
- package/dist/program-source.js +14 -2
- package/dist/{exempt-writes.js → program-writes.js} +60 -19
- package/dist/provider.d.ts +142 -0
- package/dist/provider.js +187 -0
- package/dist/providers/_shared/google/result-size.js +57 -0
- package/dist/providers/_shared/google/service-account.d.ts +58 -0
- package/dist/providers/_shared/google/service-account.js +500 -0
- package/dist/providers/_shared/google/workspace.d.ts +276 -0
- package/dist/providers/_shared/google/workspace.js +518 -0
- package/dist/providers/_shared/request-token.js +83 -0
- package/dist/providers/{basecamp.d.ts → basecamp/index.d.ts} +6 -28
- package/dist/providers/basecamp/index.js +997 -0
- package/dist/providers/basecamp/skill.generated.js +9 -0
- package/dist/providers/{breeze.d.ts → breeze/index.d.ts} +2 -2
- package/dist/providers/{breeze.js → breeze/index.js} +184 -140
- package/dist/providers/breeze/skill.generated.js +9 -0
- package/dist/providers/{ccb.d.ts → ccb/index.d.ts} +2 -2
- package/dist/providers/{ccb.js → ccb/index.js} +271 -116
- package/dist/providers/ccb/skill.generated.js +9 -0
- package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
- package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
- package/dist/providers/cloudflare/skill.generated.js +10 -0
- package/dist/providers/docs/index.d.ts +54 -0
- package/dist/providers/docs/index.js +1237 -0
- package/dist/providers/docs/skill.generated.js +9 -0
- package/dist/providers/drive/index.d.ts +53 -0
- package/dist/providers/drive/index.js +1523 -0
- package/dist/providers/drive/skill.generated.js +9 -0
- package/dist/providers/forms/index.d.ts +54 -0
- package/dist/providers/forms/index.js +1217 -0
- package/dist/providers/forms/skill.generated.js +11 -0
- package/dist/providers/github/auth.d.ts +32 -0
- package/dist/providers/github/auth.js +361 -0
- package/dist/providers/github/hosted.js +218 -0
- package/dist/providers/github/index.d.ts +13 -0
- package/dist/providers/github/index.js +140 -0
- package/dist/providers/github/key.js +100 -0
- package/dist/providers/github/rest.js +321 -0
- package/dist/providers/github/scope.d.ts +29 -0
- package/dist/providers/github/scope.js +124 -0
- package/dist/providers/github/skill.generated.js +8 -0
- package/dist/providers/gmail/index.d.ts +51 -0
- package/dist/providers/gmail/index.js +1436 -0
- package/dist/providers/gmail/skill.generated.js +9 -0
- package/dist/providers/infisical/index.d.ts +7 -0
- package/dist/providers/infisical/index.js +713 -0
- package/dist/providers/infisical/skill.generated.js +8 -0
- package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
- package/dist/providers/linear/index.js +352 -0
- package/dist/providers/linear/skill.generated.js +8 -0
- package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
- package/dist/providers/mixpanel/index.js +416 -0
- package/dist/providers/mixpanel/skill.generated.js +9 -0
- package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
- package/dist/providers/{notion.js → notion/index.js} +226 -239
- package/dist/providers/notion/skill.generated.js +10 -0
- package/dist/providers/{overflow.d.ts → overflow/index.d.ts} +2 -2
- package/dist/providers/{overflow.js → overflow/index.js} +74 -111
- package/dist/providers/overflow/skill.generated.js +8 -0
- package/dist/providers/{planning-center.d.ts → planning-center/index.d.ts} +2 -2
- package/dist/providers/{planning-center.js → planning-center/index.js} +264 -104
- package/dist/providers/planning-center/skill.generated.js +10 -0
- package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
- package/dist/providers/revenuecat/index.js +557 -0
- package/dist/providers/revenuecat/skill.generated.js +11 -0
- package/dist/providers/sheets/index.d.ts +54 -0
- package/dist/providers/sheets/index.js +1237 -0
- package/dist/providers/sheets/skill.generated.js +10 -0
- package/dist/providers/slides/index.d.ts +53 -0
- package/dist/providers/slides/index.js +2645 -0
- package/dist/providers/slides/skill.generated.js +9 -0
- package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
- package/dist/providers/stripe/index.js +266 -0
- package/dist/providers/stripe/skill.generated.js +11 -0
- package/dist/providers/{tithely.d.ts → tithely/index.d.ts} +2 -2
- package/dist/providers/{tithely.js → tithely/index.js} +60 -91
- package/dist/providers/tithely/skill.generated.js +8 -0
- package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
- package/dist/providers/vercel/index.js +1038 -0
- package/dist/providers/vercel/mcp-ownership.js +212 -0
- package/dist/providers/vercel/skill.generated.js +11 -0
- package/dist/registry.d.ts +110 -171
- package/dist/registry.js +588 -927
- package/dist/request-state.js +21 -0
- package/dist/resource-read.js +103 -0
- package/dist/result-shapes.js +17 -49
- package/dist/retired-toolkits.js +1 -1
- package/dist/routes/access-tokens.js +3 -6
- package/dist/routes/activity-context.js +21 -0
- package/dist/routes/activity-disclosure.js +40 -0
- package/dist/routes/activity.js +59 -21
- package/dist/routes/actor-labels.js +3 -50
- package/dist/routes/connect.js +221 -0
- package/dist/routes/credentials.js +15 -27
- package/dist/routes/mcp.js +365 -130
- package/dist/routes/oauth-client-metadata.js +15 -0
- package/dist/routes/oauth-management.js +45 -107
- package/dist/routes/oauth.js +110 -57
- package/dist/routes/operator.js +3 -3
- package/dist/routes/shared.d.ts +25 -64
- package/dist/routes/shared.js +160 -80
- package/dist/routes/skills.js +30 -0
- package/dist/routes/ui-config.js +225 -0
- package/dist/routes/ui-data.js +40 -35
- package/dist/routes/ui.js +66 -56
- package/dist/runtime/admission.js +4 -2
- package/dist/runtime/call-admission.js +3 -1
- package/dist/runtime/connector-scope.js +1 -4
- package/dist/runtime/run.js +10 -39
- package/dist/runtime/services.js +12 -117
- package/dist/runtime/shared-read.js +1 -3
- package/dist/runtime/storage.js +8 -1
- package/dist/sent-secrets.d.ts +93 -0
- package/dist/sent-secrets.js +1013 -0
- package/dist/server.js +28 -71
- package/dist/skills.js +416 -174
- package/dist/sqlite.d.ts +48 -0
- package/dist/sqlite.js +181 -0
- package/dist/storage/keys.js +443 -0
- package/dist/storage/memory.js +16 -4
- package/dist/storage/sql.d.ts +66 -0
- package/dist/storage/sql.js +543 -0
- package/dist/timeout.js +4 -2
- package/dist/tool-name.js +17 -0
- package/dist/tool-safety.d.ts +9 -35
- package/dist/tool-safety.js +20 -40
- package/dist/types.d.ts +336 -56
- package/dist/types.js +0 -1
- package/dist/ui.d.ts +7 -19
- package/dist/ui.js +76 -90
- package/dist/url-safety.js +3 -6
- package/dist/usage-guide.js +146 -0
- package/dist/validate.d.ts +7 -0
- package/dist/validate.js +309 -47
- package/dist/version.d.ts +1 -7
- package/dist/version.js +3 -7
- package/dist/worker.d.ts +11 -0
- package/dist/worker.js +435 -0
- package/documentation/README.md +63 -0
- package/documentation/architecture.md +696 -231
- package/documentation/auth.md +1069 -546
- package/documentation/code-mode.md +662 -356
- package/documentation/deploying.md +146 -0
- package/documentation/integrating.md +159 -0
- package/documentation/meta-tools.md +570 -216
- package/documentation/operating.md +135 -0
- package/documentation/operator-tests.md +97 -0
- package/documentation/operator-ui.md +132 -0
- package/documentation/provider-migration-0.29.md +65 -0
- package/examples/worker/AGENTS.md +34 -12
- package/examples/worker/README.md +293 -255
- package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
- package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
- package/examples/worker/src/connecta.config.ts +151 -0
- package/examples/worker/src/index.ts +14 -197
- package/examples/worker/wrangler.jsonc +15 -35
- package/package.json +126 -54
- package/templates/node/.dockerignore +1 -2
- package/templates/node/.env.example +20 -21
- package/templates/node/AGENTS.md +21 -17
- package/templates/node/Dockerfile +3 -5
- package/templates/node/README.md +140 -58
- package/templates/node/docker-compose.yml +11 -12
- package/templates/node/package.json +4 -2
- package/templates/node/src/connecta.config.ts +119 -0
- package/templates/node/src/index.ts +9 -154
- package/templates/node/src/provision-token.ts +17 -0
- package/AGENTS.md +0 -170
- package/dist/artifacts/connector.d.ts +0 -58
- package/dist/artifacts/connector.js +0 -730
- package/dist/artifacts/document.js +0 -86
- package/dist/artifacts/guide.js +0 -140
- package/dist/artifacts/html-scan.js +0 -329
- package/dist/artifacts/json.js +0 -78
- package/dist/artifacts/kv-store.d.ts +0 -20
- package/dist/artifacts/kv-store.js +0 -140
- package/dist/artifacts/markdown.js +0 -478
- package/dist/artifacts/module.d.ts +0 -37
- package/dist/artifacts/module.js +0 -66
- package/dist/artifacts/operations.d.ts +0 -197
- package/dist/artifacts/operations.js +0 -1002
- package/dist/artifacts/refresh.d.ts +0 -55
- package/dist/artifacts/refresh.js +0 -236
- package/dist/artifacts/routes.js +0 -206
- package/dist/artifacts/types.d.ts +0 -214
- package/dist/artifacts/types.js +0 -22
- package/dist/artifacts/validate.d.ts +0 -62
- package/dist/artifacts/validate.js +0 -651
- package/dist/artifacts.d.ts +0 -6
- package/dist/artifacts.js +0 -9
- package/dist/auth/bearer.d.ts +0 -11
- package/dist/auth/bearer.js +0 -50
- package/dist/providers/basecamp.js +0 -445
- package/dist/providers/linear.js +0 -217
- package/dist/providers/mixpanel.js +0 -247
- package/dist/providers/revenuecat.js +0 -317
- package/dist/providers/stripe.js +0 -253
- package/dist/providers/vercel.js +0 -1253
- package/dist/storage/file.d.ts +0 -15
- package/dist/storage/file.js +0 -455
- package/ethos.md +0 -93
- package/examples/worker/src/cloudflare-kv.ts +0 -43
- package/examples/worker/src/d1-activity-row.ts +0 -155
- package/examples/worker/src/d1-activity.ts +0 -147
- package/examples/worker/src/d1-storage.ts +0 -138
- package/examples/worker/src/r2-artifact-blobs.ts +0 -29
- package/templates/node/src/file-activity.ts +0 -182
|
@@ -0,0 +1,1013 @@
|
|
|
1
|
+
// Credentials and submitted private argument values used by one upstream request. Memory
|
|
2
|
+
// only; never part of a context's public shape, a failure record, storage, or a log. Web APIs only.
|
|
3
|
+
import { carryFailureFacts } from "./operator-record.js";
|
|
4
|
+
import { ConnectorCallError } from "./errors.js";
|
|
5
|
+
import { credentialUrlViews } from "./credential-url.js";
|
|
6
|
+
import { visitPrivateCallArguments } from "./argument-redaction.js";
|
|
7
|
+
const REDACTED = "[redacted]";
|
|
8
|
+
const encoder = new TextEncoder();
|
|
9
|
+
const contexts = new WeakMap();
|
|
10
|
+
const requests = new WeakMap();
|
|
11
|
+
const wrappedCredentials = new WeakSet();
|
|
12
|
+
const sensitiveName = /key|token|secret|password|auth|signature|session/i;
|
|
13
|
+
const explicitSecretName = /secret|password/i;
|
|
14
|
+
const headerLine = /(^|[\r\n])([\t ]*(?:cookie|set-cookie|[a-z0-9-]*(?:key|token|secret|auth|signature|session)[a-z0-9-]*)\s*:\s*)[^\r\n]*/gi;
|
|
15
|
+
// Short credentials can match protocol fields or legitimate configuration.
|
|
16
|
+
const MIN_SECRET_LENGTH = 8;
|
|
17
|
+
// Form kinds, weakest first. A short private value redacts exact structured leaves and
|
|
18
|
+
// withholds prose; longer private values and credentials are replaced wherever they occur.
|
|
19
|
+
const SHORT = 1;
|
|
20
|
+
const PRIVATE = 2;
|
|
21
|
+
const CREDENTIAL = 3;
|
|
22
|
+
// Submitted values register at most this many form code units and argument nodes. A form
|
|
23
|
+
// that does not fit withholds every string long enough to contain it.
|
|
24
|
+
const MAX_PRIVATE_FORM_CHARS = 524_288;
|
|
25
|
+
const MAX_ARGUMENT_NODES = 4_096;
|
|
26
|
+
const MAX_ARGUMENT_DEPTH = 32;
|
|
27
|
+
// The matcher indexes each form's first FORM_PREFIX units, credentials first, up to
|
|
28
|
+
// MAX_MATCHER_UNITS for every registration together. A longer form is confirmed by a rolling
|
|
29
|
+
// hash once its prefix matches. A form left out withholds every string long enough to contain it.
|
|
30
|
+
// Forms sharing a prefix and a length share each check, and overlapping prefix occurrences in
|
|
31
|
+
// self-similar text share one run of checks. A scanned view holds at most MAX_PENDING_RUNS, and
|
|
32
|
+
// each check, which costs about as much as scanning two code units, is paid for from the call's
|
|
33
|
+
// work budget before it is queued; a view that needs more is withheld.
|
|
34
|
+
const FORM_PREFIX = 256;
|
|
35
|
+
const MAX_MATCHER_UNITS = 524_288;
|
|
36
|
+
const MAX_PENDING_RUNS = 16_384;
|
|
37
|
+
const CHECK_WORK = 4;
|
|
38
|
+
const HASH_BASE = 0x01_00_01_93;
|
|
39
|
+
const POWERS = new Int32Array(FORM_PREFIX + 1);
|
|
40
|
+
POWERS[0] = 1;
|
|
41
|
+
for (let length = 1; length <= FORM_PREFIX; length++)
|
|
42
|
+
POWERS[length] = Math.imul(POWERS[length - 1], HASH_BASE);
|
|
43
|
+
// One redaction call visits at most this many structured entries, this deep. A string is
|
|
44
|
+
// decoded at most MAX_DECODE_LEVELS JSON layers, counting nested JSON strings it was parsed from.
|
|
45
|
+
const MAX_WALK_NODES = 1_048_576;
|
|
46
|
+
const MAX_WALK_DEPTH = 512;
|
|
47
|
+
const MAX_DECODE_LEVELS = 8;
|
|
48
|
+
// Work for one redaction call, across parsing and every scanned view. Credential-only sets
|
|
49
|
+
// keep the multi-MiB document and skill-file reads they always allowed.
|
|
50
|
+
const PRIVATE_SCAN_WORK = 64 * 1_048_576;
|
|
51
|
+
const CREDENTIAL_SCAN_WORK = 1_024 * 1_048_576;
|
|
52
|
+
// Connecta and MCP framing: these names, tag values, and typed facts are copied unchanged,
|
|
53
|
+
// so a submitted value such as "text" or "true" cannot rewrite an envelope or its outcome.
|
|
54
|
+
const FRAMING_KEYS = new Set(("content structuredContent isError _meta resultType dev.connecta/format type text data mimeType resource uri " +
|
|
55
|
+
"blob annotations ok format valueFormat error code message stack name cause details retryable retryAfterMs " +
|
|
56
|
+
"nextAction uncertainCall result logs failure inputRequests requestState method params mode requestedSchema url").split(" "));
|
|
57
|
+
const TAGS = new Set(("type:text type:image type:audio type:resource type:resource_link format:json format:text format:paged " +
|
|
58
|
+
"valueFormat:json valueFormat:text dev.connecta/format:json dev.connecta/format:text resultType:complete " +
|
|
59
|
+
"resultType:input_required mode:form mode:url method:elicitation/create jsonrpc:2.0").split(" "));
|
|
60
|
+
const FACTS = new Set(["isError", "ok", "retryable", "retryAfterMs", "truncated", "hasMore"]);
|
|
61
|
+
// Downstream and guest prose: a short private value anywhere in it withholds the whole string.
|
|
62
|
+
const PROSE_KEYS = new Set(["message", "stack", "error", "logs", "text", "result"]);
|
|
63
|
+
/** Spend work that fits the call's remaining budget. Work that does not fit is refused
|
|
64
|
+
* without spending, so its field is withheld while smaller fields can still be checked. */
|
|
65
|
+
function spend(scan, work) {
|
|
66
|
+
if (work > scan.budget.work)
|
|
67
|
+
return false;
|
|
68
|
+
scan.budget.work -= work;
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
// Walk markers: a container was opened, or an entry is dropped from the copy.
|
|
72
|
+
const OPEN = Symbol("open");
|
|
73
|
+
const DROP = Symbol("drop");
|
|
74
|
+
/** Define one entry on a copy. An own accessor here is Error.stack, already snapshotted. */
|
|
75
|
+
function define(copy, item, key, name, value) {
|
|
76
|
+
const descriptor = Object.getOwnPropertyDescriptor(item, key);
|
|
77
|
+
Object.defineProperty(copy, name, "value" in descriptor
|
|
78
|
+
? { ...descriptor, value, ...(name !== key ? { configurable: true } : {}) }
|
|
79
|
+
: { value, configurable: true, writable: true });
|
|
80
|
+
}
|
|
81
|
+
/** One payload-free warning per configured connector, regardless of requests. */
|
|
82
|
+
export function shortSecretWarning() {
|
|
83
|
+
let warned = false;
|
|
84
|
+
return (value, logger) => {
|
|
85
|
+
if (warned || !value || value.length >= MIN_SECRET_LENGTH)
|
|
86
|
+
return;
|
|
87
|
+
warned = true;
|
|
88
|
+
const fact = { code: "short_secret_not_redacted" };
|
|
89
|
+
logger.warn("[connecta] Credentials shorter than 8 characters are not redacted from echoes; use longer secrets.", fact);
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/** Percent escapes compare case-insensitively: hex letters within two units after `%` fold
|
|
93
|
+
* to uppercase in forms and scanned text alike. A match can begin inside that pair, so a
|
|
94
|
+
* form's first one or two letters may arrive folded as well. */
|
|
95
|
+
function wireForms(value) {
|
|
96
|
+
const fold = (text) => text.replace(/[a-f]/g, (letter) => letter.toUpperCase());
|
|
97
|
+
const form = value.replace(/%[^%]?[^%]?/g, fold);
|
|
98
|
+
return [...new Set([form, fold(form.slice(0, 1)) + form.slice(1), fold(form.slice(0, 2)) + form.slice(2)])];
|
|
99
|
+
}
|
|
100
|
+
function base64(value) {
|
|
101
|
+
let binary = "";
|
|
102
|
+
for (const byte of encoder.encode(value))
|
|
103
|
+
binary += String.fromCharCode(byte);
|
|
104
|
+
return btoa(binary);
|
|
105
|
+
}
|
|
106
|
+
class Source {
|
|
107
|
+
text;
|
|
108
|
+
start = 0;
|
|
109
|
+
end = 0;
|
|
110
|
+
constructor(text) {
|
|
111
|
+
this.text = text;
|
|
112
|
+
}
|
|
113
|
+
next() {
|
|
114
|
+
if (this.end >= this.text.length)
|
|
115
|
+
return -1;
|
|
116
|
+
this.start = this.end++;
|
|
117
|
+
return this.text.charCodeAt(this.start);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const shortEscape = { 34: 34, 47: 47, 92: 92, 98: 8, 102: 12, 110: 10, 114: 13, 116: 9 };
|
|
121
|
+
/** A hex digit's value, or -1. */
|
|
122
|
+
function hex(code) {
|
|
123
|
+
const lower = code | 0x20;
|
|
124
|
+
return code >= 0x30 && code <= 0x39 ? code - 0x30 : lower >= 0x61 && lower <= 0x66 ? lower - 0x57 : -1;
|
|
125
|
+
}
|
|
126
|
+
/** One layer of JSON string escapes decoded. It counts the escapes it decoded and the
|
|
127
|
+
* backslashes it produced, which tell whether another layer could decode more. */
|
|
128
|
+
class Unescaped {
|
|
129
|
+
from;
|
|
130
|
+
start = 0;
|
|
131
|
+
end = 0;
|
|
132
|
+
decoded = 0;
|
|
133
|
+
slashes = 0;
|
|
134
|
+
codes = new Int32Array(8);
|
|
135
|
+
starts = new Int32Array(8);
|
|
136
|
+
ends = new Int32Array(8);
|
|
137
|
+
head = 0;
|
|
138
|
+
size = 0;
|
|
139
|
+
constructor(from) {
|
|
140
|
+
this.from = from;
|
|
141
|
+
}
|
|
142
|
+
fill(count) {
|
|
143
|
+
for (; this.size < count; this.size++) {
|
|
144
|
+
const code = this.from.next();
|
|
145
|
+
if (code < 0)
|
|
146
|
+
return false;
|
|
147
|
+
const at = (this.head + this.size) & 7;
|
|
148
|
+
this.codes[at] = code;
|
|
149
|
+
this.starts[at] = this.from.start;
|
|
150
|
+
this.ends[at] = this.from.end;
|
|
151
|
+
}
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
at(offset) {
|
|
155
|
+
return this.codes[(this.head + offset) & 7];
|
|
156
|
+
}
|
|
157
|
+
take(count, code) {
|
|
158
|
+
this.start = this.starts[this.head];
|
|
159
|
+
this.end = this.ends[(this.head + count - 1) & 7];
|
|
160
|
+
this.head = (this.head + count) & 7;
|
|
161
|
+
this.size -= count;
|
|
162
|
+
if (count > 1)
|
|
163
|
+
this.decoded++;
|
|
164
|
+
if (code === 0x5c)
|
|
165
|
+
this.slashes++;
|
|
166
|
+
return code;
|
|
167
|
+
}
|
|
168
|
+
next() {
|
|
169
|
+
if (!this.fill(1))
|
|
170
|
+
return -1;
|
|
171
|
+
const code = this.at(0);
|
|
172
|
+
if (code === 0x5c && this.fill(2)) {
|
|
173
|
+
const escaped = shortEscape[this.at(1)];
|
|
174
|
+
if (escaped !== undefined)
|
|
175
|
+
return this.take(2, escaped);
|
|
176
|
+
if (this.at(1) === 0x75 && this.fill(6)) {
|
|
177
|
+
let unit = 0;
|
|
178
|
+
for (let offset = 2; offset < 6 && unit >= 0; offset++) {
|
|
179
|
+
const digit = hex(this.at(offset));
|
|
180
|
+
unit = digit < 0 ? -1 : unit * 16 + digit;
|
|
181
|
+
}
|
|
182
|
+
if (unit >= 0)
|
|
183
|
+
return this.take(6, unit);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return this.take(1, code);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/** Merge one view's spans as they arrive, ends ascending. A span wholly inside an emitted
|
|
190
|
+
* placeholder is that placeholder; a private value containing its text spans more. */
|
|
191
|
+
function push(spans, base, start, end, marks) {
|
|
192
|
+
if (marks) {
|
|
193
|
+
let low = 0;
|
|
194
|
+
let high = marks.length;
|
|
195
|
+
while (low < high) {
|
|
196
|
+
const middle = (low + high) >>> 1;
|
|
197
|
+
if (marks[middle] <= start)
|
|
198
|
+
low = middle + 1;
|
|
199
|
+
else
|
|
200
|
+
high = middle;
|
|
201
|
+
}
|
|
202
|
+
if (low && end <= marks[low - 1] + REDACTED.length)
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
while (spans.length > base && spans[spans.length - 1] >= start) {
|
|
206
|
+
start = Math.min(start, spans[spans.length - 2]);
|
|
207
|
+
spans.length -= 2;
|
|
208
|
+
}
|
|
209
|
+
spans.push(start, end);
|
|
210
|
+
}
|
|
211
|
+
function markers(text) {
|
|
212
|
+
let at = text.indexOf(REDACTED);
|
|
213
|
+
if (at < 0)
|
|
214
|
+
return undefined;
|
|
215
|
+
const marks = [];
|
|
216
|
+
for (; at >= 0; at = text.indexOf(REDACTED, at + REDACTED.length))
|
|
217
|
+
marks.push(at);
|
|
218
|
+
return marks;
|
|
219
|
+
}
|
|
220
|
+
/** Replace each merged span once, from the original text: no replacement can split another
|
|
221
|
+
* view's match. Header-line scrubbing belongs to an actual echo. */
|
|
222
|
+
function replace(text, spans) {
|
|
223
|
+
const order = Array.from({ length: spans.length / 2 }, (_, index) => index * 2).sort((a, b) => spans[a] - spans[b]);
|
|
224
|
+
const parts = [];
|
|
225
|
+
let cursor = 0;
|
|
226
|
+
for (let index = 0; index < order.length;) {
|
|
227
|
+
const start = spans[order[index]];
|
|
228
|
+
let end = spans[order[index] + 1];
|
|
229
|
+
while (++index < order.length && spans[order[index]] <= end)
|
|
230
|
+
end = Math.max(end, spans[order[index] + 1]);
|
|
231
|
+
parts.push(text.slice(cursor, start), REDACTED);
|
|
232
|
+
cursor = end;
|
|
233
|
+
}
|
|
234
|
+
parts.push(text.slice(cursor));
|
|
235
|
+
return parts.join("").replace(headerLine, `$1$2${REDACTED}`);
|
|
236
|
+
}
|
|
237
|
+
/** A run of one occurrence, whose step is set by the next occurrence that extends it. */
|
|
238
|
+
const UNEXTENDED = { step: 0, stride: 0, stepHash: 0, stepPower: 0 };
|
|
239
|
+
/** Runs ordered by their head check's due count: a binary min-heap. */
|
|
240
|
+
class Runs {
|
|
241
|
+
items = [];
|
|
242
|
+
add(run) {
|
|
243
|
+
let at = this.items.length;
|
|
244
|
+
for (let parent = (at - 1) >> 1; at && this.items[parent].due > run.due; parent = (at - 1) >> 1) {
|
|
245
|
+
this.items[at] = this.items[parent];
|
|
246
|
+
at = parent;
|
|
247
|
+
}
|
|
248
|
+
this.items[at] = run;
|
|
249
|
+
}
|
|
250
|
+
/** Restore the order after the earliest run's head advanced, dropping it once empty. */
|
|
251
|
+
settle() {
|
|
252
|
+
// An empty run leaves, and the last run sifts down from the root in its place.
|
|
253
|
+
const run = this.items[0].count ? this.items[0] : this.items.pop();
|
|
254
|
+
const size = this.items.length;
|
|
255
|
+
if (!size)
|
|
256
|
+
return;
|
|
257
|
+
let at = 0;
|
|
258
|
+
for (let child = 1; child < size; child = at * 2 + 1) {
|
|
259
|
+
if (child + 1 < size && this.items[child + 1].due < this.items[child].due)
|
|
260
|
+
child++;
|
|
261
|
+
if (this.items[child].due >= run.due)
|
|
262
|
+
break;
|
|
263
|
+
this.items[at] = this.items[child];
|
|
264
|
+
at = child;
|
|
265
|
+
}
|
|
266
|
+
this.items[at] = run;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/** A polynomial hash of code units, modulo 2^32. Equal text always hashes equally, so a
|
|
270
|
+
* collision can only redact more. */
|
|
271
|
+
function hash(text) {
|
|
272
|
+
let value = 0;
|
|
273
|
+
for (let index = 0; index < text.length; index++)
|
|
274
|
+
value = (Math.imul(value, HASH_BASE) + text.charCodeAt(index)) | 0;
|
|
275
|
+
return value;
|
|
276
|
+
}
|
|
277
|
+
/** Aho-Corasick over registered forms: amortized constant work per scanned code unit, however
|
|
278
|
+
* many forms are registered, reporting the longest form ending at each unit. Only a bounded
|
|
279
|
+
* prefix of each form is indexed. A longer form is confirmed where its prefix matches by the
|
|
280
|
+
* view's rolling hash once the form's last unit arrives, so no text is read twice; checks are
|
|
281
|
+
* budgeted, and their queue is bounded. States live in typed arrays: the root's transitions in a
|
|
282
|
+
* table, each state's first child inline, and only further branches in a map. */
|
|
283
|
+
class Matcher {
|
|
284
|
+
/** The shortest form left unindexed; any string this long might contain it. */
|
|
285
|
+
unindexed = Infinity;
|
|
286
|
+
root = new Int32Array(65_536);
|
|
287
|
+
branches = new Map();
|
|
288
|
+
child;
|
|
289
|
+
unit;
|
|
290
|
+
depth;
|
|
291
|
+
kind;
|
|
292
|
+
/** Forms longer than FORM_PREFIX, by the state their prefix reaches. */
|
|
293
|
+
truncated = new Map();
|
|
294
|
+
fail;
|
|
295
|
+
longest;
|
|
296
|
+
short;
|
|
297
|
+
starts;
|
|
298
|
+
hashes;
|
|
299
|
+
constructor(forms) {
|
|
300
|
+
// Credentials are indexed first; sorting is stable, so registration order is otherwise kept.
|
|
301
|
+
const indexed = [];
|
|
302
|
+
let units = 0;
|
|
303
|
+
for (const entry of [...forms].sort((a, b) => b[1] - a[1])) {
|
|
304
|
+
const length = Math.min(entry[0].length, FORM_PREFIX);
|
|
305
|
+
if (units + length > MAX_MATCHER_UNITS)
|
|
306
|
+
this.unindexed = Math.min(this.unindexed, entry[0].length);
|
|
307
|
+
else {
|
|
308
|
+
units += length;
|
|
309
|
+
indexed.push(entry);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const size = units + 1;
|
|
313
|
+
[this.child, this.fail] = [new Int32Array(size), new Int32Array(size)];
|
|
314
|
+
[this.unit, this.depth, this.longest] = [new Uint16Array(size), new Uint16Array(size), new Uint16Array(size)];
|
|
315
|
+
[this.kind, this.short] = [new Uint8Array(size), new Uint8Array(size)];
|
|
316
|
+
const parent = new Int32Array(size);
|
|
317
|
+
let states = 1;
|
|
318
|
+
let longestForm = 0;
|
|
319
|
+
for (const [form, kind] of indexed) {
|
|
320
|
+
const length = Math.min(form.length, FORM_PREFIX);
|
|
321
|
+
let state = 0;
|
|
322
|
+
for (let index = 0; index < length; index++) {
|
|
323
|
+
const code = form.charCodeAt(index);
|
|
324
|
+
let next = this.next(state, code);
|
|
325
|
+
if (!next) {
|
|
326
|
+
next = states++;
|
|
327
|
+
if (!state)
|
|
328
|
+
this.root[code] = next;
|
|
329
|
+
else if (!this.child[state])
|
|
330
|
+
this.child[state] = next;
|
|
331
|
+
else
|
|
332
|
+
this.branches.set(state * 65_536 + code, next);
|
|
333
|
+
parent[next] = state;
|
|
334
|
+
this.unit[next] = code;
|
|
335
|
+
this.depth[next] = this.depth[state] + 1;
|
|
336
|
+
}
|
|
337
|
+
state = next;
|
|
338
|
+
}
|
|
339
|
+
if (length === form.length)
|
|
340
|
+
this.kind[state] = Math.max(this.kind[state], kind);
|
|
341
|
+
else {
|
|
342
|
+
const groups = this.truncated.get(state) ?? [];
|
|
343
|
+
this.truncated.set(state, groups);
|
|
344
|
+
const group = groups.find((group) => group.length === form.length);
|
|
345
|
+
if (group)
|
|
346
|
+
group.hashes.add(hash(form));
|
|
347
|
+
else {
|
|
348
|
+
let power = 1;
|
|
349
|
+
for (let index = 0; index < form.length; index++)
|
|
350
|
+
power = Math.imul(power, HASH_BASE);
|
|
351
|
+
groups.push({ length: form.length, power, hashes: new Set([hash(form)]) });
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
longestForm = Math.max(longestForm, length);
|
|
355
|
+
}
|
|
356
|
+
// Breadth-first, by a counting sort on depth: a failure link and its outputs depend only on
|
|
357
|
+
// shallower states.
|
|
358
|
+
const offsets = new Int32Array(FORM_PREFIX + 2);
|
|
359
|
+
for (let state = 1; state < states; state++)
|
|
360
|
+
offsets[this.depth[state] + 1]++;
|
|
361
|
+
for (let depth = 1; depth < offsets.length; depth++)
|
|
362
|
+
offsets[depth] += offsets[depth - 1];
|
|
363
|
+
const order = new Int32Array(states);
|
|
364
|
+
for (let state = 1; state < states; state++)
|
|
365
|
+
order[offsets[this.depth[state]]++] = state;
|
|
366
|
+
for (let index = 0; index < states - 1; index++) {
|
|
367
|
+
const state = order[index];
|
|
368
|
+
const fail = this.depth[state] > 1 ? this.step(this.fail[parent[state]], this.unit[state]) : 0;
|
|
369
|
+
this.fail[state] = fail;
|
|
370
|
+
this.longest[state] = this.kind[state] >= PRIVATE ? this.depth[state] : this.longest[fail];
|
|
371
|
+
this.short[state] = this.kind[state] === SHORT ? 1 : this.short[fail];
|
|
372
|
+
}
|
|
373
|
+
this.starts = new Int32Array(1 << (32 - Math.clz32(longestForm)));
|
|
374
|
+
this.hashes = new Int32Array(this.starts.length);
|
|
375
|
+
}
|
|
376
|
+
/** The child reached by one code unit, or 0. A first child is stored with its unit. */
|
|
377
|
+
next(state, code) {
|
|
378
|
+
if (!state)
|
|
379
|
+
return this.root[code];
|
|
380
|
+
const child = this.child[state];
|
|
381
|
+
return child && this.unit[child] === code ? child : (this.branches.get(state * 65_536 + code) ?? 0);
|
|
382
|
+
}
|
|
383
|
+
step(state, code) {
|
|
384
|
+
for (;;) {
|
|
385
|
+
const next = this.next(state, code);
|
|
386
|
+
if (next || !state)
|
|
387
|
+
return next;
|
|
388
|
+
state = this.fail[state];
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
/** Scan one view, mapping each match back to the source units it came from. False when its
|
|
392
|
+
* long-form checks exceed the remaining work or MAX_PENDING_RUNS, so the view is withheld. */
|
|
393
|
+
scan(units, found, budget, marks) {
|
|
394
|
+
let state = 0;
|
|
395
|
+
let count = 0;
|
|
396
|
+
let percent = 0;
|
|
397
|
+
let rolling = 0;
|
|
398
|
+
let runs;
|
|
399
|
+
let tails;
|
|
400
|
+
const base = found.spans.length;
|
|
401
|
+
const mask = this.starts.length - 1;
|
|
402
|
+
for (let code = units.next(); code >= 0; code = units.next()) {
|
|
403
|
+
if (code === 0x25)
|
|
404
|
+
percent = 2;
|
|
405
|
+
else if (percent) {
|
|
406
|
+
percent--;
|
|
407
|
+
if (code >= 0x61 && code <= 0x66)
|
|
408
|
+
code -= 0x20;
|
|
409
|
+
}
|
|
410
|
+
this.starts[count & mask] = units.start;
|
|
411
|
+
this.hashes[count++ & mask] = rolling;
|
|
412
|
+
rolling = (Math.imul(rolling, HASH_BASE) + code) | 0;
|
|
413
|
+
state = this.step(state, code);
|
|
414
|
+
const length = this.longest[state];
|
|
415
|
+
if (length)
|
|
416
|
+
push(found.spans, base, this.starts[(count - length) & mask], units.end, marks);
|
|
417
|
+
if (this.short[state])
|
|
418
|
+
found.short = true;
|
|
419
|
+
const groups = this.depth[state] === FORM_PREFIX ? this.truncated.get(state) : undefined;
|
|
420
|
+
if (groups) {
|
|
421
|
+
if (CHECK_WORK * groups.length > budget.work)
|
|
422
|
+
return false;
|
|
423
|
+
budget.work -= CHECK_WORK * groups.length;
|
|
424
|
+
const before = this.hashes[(count - FORM_PREFIX) & mask];
|
|
425
|
+
const start = this.starts[(count - FORM_PREFIX) & mask];
|
|
426
|
+
runs ??= new Runs();
|
|
427
|
+
tails ??= new Map();
|
|
428
|
+
for (const group of groups) {
|
|
429
|
+
const tail = tails.get(group);
|
|
430
|
+
if (tail?.count && this.extend(tail, count, start))
|
|
431
|
+
continue;
|
|
432
|
+
if (runs.items.length >= MAX_PENDING_RUNS)
|
|
433
|
+
return false;
|
|
434
|
+
const due = count - FORM_PREFIX + group.length;
|
|
435
|
+
const run = { group, count: 1, due, before, start, last: count, lastStart: start, ...UNEXTENDED };
|
|
436
|
+
tails.set(group, run);
|
|
437
|
+
runs.add(run);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
while (runs?.items.length && runs.items[0].due === count) {
|
|
441
|
+
const run = runs.items[0];
|
|
442
|
+
if (run.group.hashes.has((rolling - Math.imul(run.before, run.group.power)) | 0))
|
|
443
|
+
push(found.spans, base, run.start, units.end, marks);
|
|
444
|
+
run.count--;
|
|
445
|
+
run.due += run.step;
|
|
446
|
+
run.before = (Math.imul(run.before, run.stepPower) + run.stepHash) | 0;
|
|
447
|
+
run.start += run.stride;
|
|
448
|
+
runs.settle();
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (this.kind[state] === SHORT && this.depth[state] === count)
|
|
452
|
+
found.exact = true;
|
|
453
|
+
return true;
|
|
454
|
+
}
|
|
455
|
+
/** Add an occurrence to a run when it continues the run's step and stride. The prefix has
|
|
456
|
+
* period `step`, so the text between occurrences is the prefix's first `step` units. */
|
|
457
|
+
extend(run, count, start) {
|
|
458
|
+
const step = count - run.last;
|
|
459
|
+
const stride = start - run.lastStart;
|
|
460
|
+
if (run.step ? step !== run.step || stride !== run.stride : step > FORM_PREFIX)
|
|
461
|
+
return false;
|
|
462
|
+
if (!run.step) {
|
|
463
|
+
const mask = this.starts.length - 1;
|
|
464
|
+
run.step = step;
|
|
465
|
+
run.stride = stride;
|
|
466
|
+
run.stepPower = POWERS[step];
|
|
467
|
+
// The rolling hashes before both occurrences differ by the units between them.
|
|
468
|
+
const previous = Math.imul(this.hashes[(run.last - FORM_PREFIX) & mask], run.stepPower);
|
|
469
|
+
run.stepHash = (this.hashes[(count - FORM_PREFIX) & mask] - previous) | 0;
|
|
470
|
+
}
|
|
471
|
+
run.count++;
|
|
472
|
+
run.last = count;
|
|
473
|
+
run.lastStart = start;
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
export class SentSecrets {
|
|
478
|
+
/** Normalized form to its strongest kind. */
|
|
479
|
+
forms = new Map();
|
|
480
|
+
recipients = new Set();
|
|
481
|
+
privateChars = 0;
|
|
482
|
+
/** The shortest form left unregistered; any string this long might contain it. */
|
|
483
|
+
unmatched = Infinity;
|
|
484
|
+
unicode = false;
|
|
485
|
+
views = {};
|
|
486
|
+
store(form, kind) {
|
|
487
|
+
const prior = this.forms.get(form) ?? 0;
|
|
488
|
+
if (prior >= kind)
|
|
489
|
+
return;
|
|
490
|
+
if (kind !== CREDENTIAL && !prior) {
|
|
491
|
+
if (this.privateChars + form.length > MAX_PRIVATE_FORM_CHARS)
|
|
492
|
+
return this.unregistered(form.length);
|
|
493
|
+
this.privateChars += form.length;
|
|
494
|
+
}
|
|
495
|
+
this.forms.set(form, kind);
|
|
496
|
+
this.unicode ||= /[\u0080-\uffff]/.test(form);
|
|
497
|
+
this.views = {};
|
|
498
|
+
for (const recipient of this.recipients)
|
|
499
|
+
recipient.store(form, kind);
|
|
500
|
+
}
|
|
501
|
+
unregistered(length) {
|
|
502
|
+
if (length >= this.unmatched)
|
|
503
|
+
return;
|
|
504
|
+
this.unmatched = length;
|
|
505
|
+
this.views = {};
|
|
506
|
+
for (const recipient of this.recipients)
|
|
507
|
+
recipient.unregistered(length);
|
|
508
|
+
}
|
|
509
|
+
register(value, kind) {
|
|
510
|
+
const forms = [value, new URLSearchParams({ value }).toString().slice(6)];
|
|
511
|
+
try {
|
|
512
|
+
forms.push(encodeURIComponent(value), encodeURI(value));
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
// A lone surrogate has no URI form; its other forms remain.
|
|
516
|
+
}
|
|
517
|
+
const encoded = base64(value);
|
|
518
|
+
forms.push(encoded, encoded.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""));
|
|
519
|
+
for (const form of forms)
|
|
520
|
+
for (const variant of wireForms(form))
|
|
521
|
+
this.store(variant, kind);
|
|
522
|
+
}
|
|
523
|
+
/** The request receives existing and future secrets from every context. */
|
|
524
|
+
include(source) {
|
|
525
|
+
if (source === this)
|
|
526
|
+
return;
|
|
527
|
+
source.recipients.add(this);
|
|
528
|
+
for (const [form, kind] of source.forms)
|
|
529
|
+
this.store(form, kind);
|
|
530
|
+
this.unregistered(source.unmatched);
|
|
531
|
+
}
|
|
532
|
+
add(value) {
|
|
533
|
+
if (value.length < MIN_SECRET_LENGTH)
|
|
534
|
+
return;
|
|
535
|
+
for (const form of [value, `Bearer ${value}`, `token ${value}`])
|
|
536
|
+
this.register(form, CREDENTIAL);
|
|
537
|
+
}
|
|
538
|
+
/** Explicit secrets use the same floor as every other credential. */
|
|
539
|
+
secret(value) {
|
|
540
|
+
this.add(value);
|
|
541
|
+
}
|
|
542
|
+
/** Submitted writeOnly strings and numbers join this request's set, including property
|
|
543
|
+
* names inside a private object. Empty strings, booleans, and null carry no text. */
|
|
544
|
+
arguments(args, schema) {
|
|
545
|
+
if (!schema)
|
|
546
|
+
return;
|
|
547
|
+
let nodes = 0;
|
|
548
|
+
const submit = (value) => {
|
|
549
|
+
if (!value)
|
|
550
|
+
return;
|
|
551
|
+
if (value.length > MAX_PRIVATE_FORM_CHARS)
|
|
552
|
+
return this.unregistered(value.length);
|
|
553
|
+
this.register(value, value.length < MIN_SECRET_LENGTH ? SHORT : PRIVATE);
|
|
554
|
+
};
|
|
555
|
+
const collect = (value, depth) => {
|
|
556
|
+
if (++nodes > MAX_ARGUMENT_NODES || depth > MAX_ARGUMENT_DEPTH)
|
|
557
|
+
return this.unregistered(1);
|
|
558
|
+
if (typeof value === "string")
|
|
559
|
+
submit(value);
|
|
560
|
+
else if (typeof value === "number" || typeof value === "bigint")
|
|
561
|
+
submit(String(value));
|
|
562
|
+
else if (value !== null && typeof value === "object")
|
|
563
|
+
for (const key of Object.keys(value)) {
|
|
564
|
+
if (nodes > MAX_ARGUMENT_NODES)
|
|
565
|
+
return;
|
|
566
|
+
if (!Array.isArray(value))
|
|
567
|
+
submit(key);
|
|
568
|
+
collect(value[key], depth + 1);
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
visitPrivateCallArguments(args, schema, (value) => collect(value, 0));
|
|
572
|
+
}
|
|
573
|
+
/** Structural fields must be refused, never repaired into different URLs. */
|
|
574
|
+
contains(value, ignoreCase = false) {
|
|
575
|
+
if (value.length >= this.unmatched)
|
|
576
|
+
return true;
|
|
577
|
+
if (!this.forms.size)
|
|
578
|
+
return false;
|
|
579
|
+
const found = { spans: [], short: false, exact: false };
|
|
580
|
+
const lower = () => new Matcher([...this.forms].flatMap(([form, kind]) => wireForms(form.toLowerCase()).map((low) => [low, kind])));
|
|
581
|
+
const matcher = ignoreCase ? (this.views.lower ??= lower()) : this.matcher();
|
|
582
|
+
if (value.length >= matcher.unindexed)
|
|
583
|
+
return true;
|
|
584
|
+
const scanned = matcher.scan(new Source(ignoreCase ? value.toLowerCase() : value), found, { work: Infinity });
|
|
585
|
+
return !scanned || found.spans.length > 0;
|
|
586
|
+
}
|
|
587
|
+
containsUrl(value) {
|
|
588
|
+
const { hosts, components, joined } = credentialUrlViews(value);
|
|
589
|
+
return (hosts.some((host) => this.contains(host, true)) || [...components, ...joined].some((view) => this.contains(view)));
|
|
590
|
+
}
|
|
591
|
+
header(value) {
|
|
592
|
+
this.add(value);
|
|
593
|
+
const framed = /^(?:Bearer|token|Basic)\s+(.+)$/i.exec(value);
|
|
594
|
+
if (!framed)
|
|
595
|
+
return;
|
|
596
|
+
this.add(framed[1]);
|
|
597
|
+
if (/^Basic\s/i.test(value)) {
|
|
598
|
+
try {
|
|
599
|
+
const decoded = atob(framed[1].replace(/-/g, "+").replace(/_/g, "/"));
|
|
600
|
+
this.add(decoded);
|
|
601
|
+
const colon = decoded.indexOf(":");
|
|
602
|
+
if (colon !== -1) {
|
|
603
|
+
this.add(decoded.slice(0, colon));
|
|
604
|
+
this.secret(decoded.slice(colon + 1));
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
catch {
|
|
608
|
+
/* An invalid Basic value is still registered verbatim. */
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
else
|
|
612
|
+
this.secret(framed[1]);
|
|
613
|
+
}
|
|
614
|
+
request(input, init) {
|
|
615
|
+
const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));
|
|
616
|
+
for (const [name, value] of headers) {
|
|
617
|
+
if (explicitSecretName.test(name))
|
|
618
|
+
this.secret(value);
|
|
619
|
+
else if (name === "cookie" || sensitiveName.test(name))
|
|
620
|
+
this.header(value);
|
|
621
|
+
if (name === "cookie") {
|
|
622
|
+
for (const cookie of value.split(";")) {
|
|
623
|
+
const equals = cookie.indexOf("=");
|
|
624
|
+
if (equals !== -1)
|
|
625
|
+
this.add(cookie.slice(equals + 1).trim());
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
const url = new URL(input instanceof Request ? input.url : String(input));
|
|
630
|
+
for (const [name, value] of url.searchParams) {
|
|
631
|
+
if (explicitSecretName.test(name))
|
|
632
|
+
this.secret(value);
|
|
633
|
+
else if (sensitiveName.test(name))
|
|
634
|
+
this.add(value);
|
|
635
|
+
}
|
|
636
|
+
// The OAuth SDK sends token requests as form data. Do not read a body
|
|
637
|
+
// stream or clone a Request: registration must not consume its payload.
|
|
638
|
+
if (headers.get("content-type")?.startsWith("application/x-www-form-urlencoded") &&
|
|
639
|
+
(typeof init?.body === "string" || init?.body instanceof URLSearchParams)) {
|
|
640
|
+
for (const [name, value] of new URLSearchParams(init.body)) {
|
|
641
|
+
if (/^(?:client_secret|password)$/i.test(name))
|
|
642
|
+
this.secret(value);
|
|
643
|
+
else if (/^(?:refresh_token|code|client_assertion)$/i.test(name))
|
|
644
|
+
this.add(value);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
/** Serialized wire text about to be returned, stored, or paged, under payload rules. */
|
|
649
|
+
text(value) {
|
|
650
|
+
return this.idle() ? value : this.wire(value, this.scan(true), 0, true, 0);
|
|
651
|
+
}
|
|
652
|
+
/** Copy only what changes, including non-enumerable Error fields; never retain a raw cause.
|
|
653
|
+
* Payload rules apply where downstream or guest data enters; envelopes repeat literal
|
|
654
|
+
* replacement. */
|
|
655
|
+
redact(value, mode = "payload") {
|
|
656
|
+
if (this.idle())
|
|
657
|
+
return value;
|
|
658
|
+
return this.walk(value, this.scan(mode !== "envelope"), mode === "prose" || typeof value === "string", 0, 0);
|
|
659
|
+
}
|
|
660
|
+
/** Credentials alone: later calls keep the private arguments they submit, and cached
|
|
661
|
+
* catalogs never depend on one request's arguments. */
|
|
662
|
+
redactCredentials(value) {
|
|
663
|
+
return this.credentials().redact(value, "envelope");
|
|
664
|
+
}
|
|
665
|
+
idle() {
|
|
666
|
+
return !this.forms.size && this.unmatched === Infinity;
|
|
667
|
+
}
|
|
668
|
+
scan(payload) {
|
|
669
|
+
const work = this.privateChars || this.unmatched < Infinity ? PRIVATE_SCAN_WORK : CREDENTIAL_SCAN_WORK;
|
|
670
|
+
return { budget: { work, nodes: MAX_WALK_NODES }, payload };
|
|
671
|
+
}
|
|
672
|
+
matcher() {
|
|
673
|
+
return (this.views.matcher ??= new Matcher(this.forms));
|
|
674
|
+
}
|
|
675
|
+
credentials() {
|
|
676
|
+
if (!this.privateChars && this.unmatched === Infinity)
|
|
677
|
+
return this;
|
|
678
|
+
if (!this.views.credentials) {
|
|
679
|
+
const view = new SentSecrets();
|
|
680
|
+
for (const [form, kind] of this.forms)
|
|
681
|
+
if (kind === CREDENTIAL)
|
|
682
|
+
view.store(form, kind);
|
|
683
|
+
this.views.credentials = view;
|
|
684
|
+
}
|
|
685
|
+
return this.views.credentials;
|
|
686
|
+
}
|
|
687
|
+
/** Strings at least this long might hold a form that was never matched. */
|
|
688
|
+
floor(scan) {
|
|
689
|
+
return Math.min(scan.payload ? this.unmatched : Infinity, this.matcher().unindexed);
|
|
690
|
+
}
|
|
691
|
+
/** Every raw and JSON-unescaped match, mapped to source spans before any replacement. Text
|
|
692
|
+
* decodes one more layer while the last one could still hold an escape, up to
|
|
693
|
+
* MAX_DECODE_LEVELS counting the JSON strings it came from. Work beyond the call's budget
|
|
694
|
+
* or that depth returns undefined, which withholds the string. */
|
|
695
|
+
find(text, scan, escapes, level) {
|
|
696
|
+
const found = { spans: [], short: false, exact: false };
|
|
697
|
+
const marks = markers(text);
|
|
698
|
+
for (let layer = 0, more = true; more; layer++) {
|
|
699
|
+
if (level + layer > MAX_DECODE_LEVELS || !spend(scan, 2 * text.length * (layer + 1)))
|
|
700
|
+
return undefined;
|
|
701
|
+
let units = new Source(text);
|
|
702
|
+
for (let depth = 0; depth < layer; depth++)
|
|
703
|
+
units = new Unescaped(units);
|
|
704
|
+
if (!this.matcher().scan(units, found, scan.budget, marks))
|
|
705
|
+
return undefined;
|
|
706
|
+
more = escapes && (units instanceof Unescaped ? units.decoded > 0 && units.slashes > 0 : text.includes("\\"));
|
|
707
|
+
}
|
|
708
|
+
return found;
|
|
709
|
+
}
|
|
710
|
+
/** A structured leaf, property name, or prose. A short private form withholds prose
|
|
711
|
+
* wherever it occurs, and a leaf only when the whole leaf is that form. */
|
|
712
|
+
string(value, scan, prose, level = 0) {
|
|
713
|
+
if (value.length >= this.floor(scan))
|
|
714
|
+
return REDACTED;
|
|
715
|
+
const found = this.find(value, scan, true, level);
|
|
716
|
+
if (!found || (scan.payload && (prose ? found.short : found.exact)))
|
|
717
|
+
return REDACTED;
|
|
718
|
+
return found.spans.length ? replace(value, found.spans) : value;
|
|
719
|
+
}
|
|
720
|
+
/** A number, boolean, or null matching by its canonical text becomes a placeholder string. */
|
|
721
|
+
scalar(value, scan) {
|
|
722
|
+
const text = String(value);
|
|
723
|
+
if ((typeof value === "number" || typeof value === "bigint") && text.length >= this.floor(scan))
|
|
724
|
+
return REDACTED;
|
|
725
|
+
const found = this.find(text, scan, false, 0);
|
|
726
|
+
return !found || found.exact || found.spans.length ? REDACTED : value;
|
|
727
|
+
}
|
|
728
|
+
/** JSON objects, arrays, and string literals are parsed and walked, nested JSON strings
|
|
729
|
+
* included; their serialized bytes must then hold no literal the walk cannot see, such as
|
|
730
|
+
* one spanning tokens. Other text is prose or a structured leaf. */
|
|
731
|
+
wire(value, scan, depth, prose, level) {
|
|
732
|
+
if (value.length >= this.floor(scan))
|
|
733
|
+
return REDACTED;
|
|
734
|
+
if (level < MAX_DECODE_LEVELS && /^\s*["[{]/.test(value) && spend(scan, value.length)) {
|
|
735
|
+
let parsed;
|
|
736
|
+
try {
|
|
737
|
+
parsed = { json: JSON.parse(value) };
|
|
738
|
+
}
|
|
739
|
+
catch {
|
|
740
|
+
// Not JSON.
|
|
741
|
+
}
|
|
742
|
+
if (parsed) {
|
|
743
|
+
const { json } = parsed;
|
|
744
|
+
const walked = this.walk(json, scan, prose && typeof json === "string", depth, level + 1);
|
|
745
|
+
const text = walked === json ? value : JSON.stringify(walked);
|
|
746
|
+
const found = this.find(text, scan, false, level);
|
|
747
|
+
return found && !found.spans.length && text.length < this.floor(scan) ? text : REDACTED;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return this.string(value, scan, prose, level);
|
|
751
|
+
}
|
|
752
|
+
/** Iterative and copy-on-write: only containers on a changed path are copied. A container's
|
|
753
|
+
* entries are counted against the node budget before it is walked; a cycle or a container
|
|
754
|
+
* past MAX_WALK_DEPTH is withheld, and one past the node budget is checked as serialized text. */
|
|
755
|
+
walk(value, scan, prose, depth, level) {
|
|
756
|
+
const stack = [];
|
|
757
|
+
const active = new Set();
|
|
758
|
+
const visit = (item, prose, depth) => {
|
|
759
|
+
if (typeof item === "string")
|
|
760
|
+
return this.wire(item, scan, depth, prose, level);
|
|
761
|
+
if (item === null || typeof item === "number" || typeof item === "bigint" || typeof item === "boolean")
|
|
762
|
+
return scan.payload ? this.scalar(item, scan) : item;
|
|
763
|
+
if (typeof item !== "object")
|
|
764
|
+
return item;
|
|
765
|
+
if (active.has(item) || depth > MAX_WALK_DEPTH)
|
|
766
|
+
return REDACTED;
|
|
767
|
+
const names = Array.isArray(item) ? undefined : Object.getOwnPropertyNames(item);
|
|
768
|
+
const size = names ? names.length : item.length;
|
|
769
|
+
if (size > scan.budget.nodes)
|
|
770
|
+
return this.serialized(item, scan, level);
|
|
771
|
+
scan.budget.nodes -= size;
|
|
772
|
+
if (!size)
|
|
773
|
+
return item;
|
|
774
|
+
active.add(item);
|
|
775
|
+
const typed = item instanceof Error || typeof item.retryable === "boolean";
|
|
776
|
+
stack.push({ item, names, size, index: 0, prose, depth, typed, key: "", name: "", field: undefined });
|
|
777
|
+
return OPEN;
|
|
778
|
+
};
|
|
779
|
+
// Settle the top frame's next entry, drop it, or open its container.
|
|
780
|
+
const enter = (frame) => {
|
|
781
|
+
const { item, names } = frame;
|
|
782
|
+
if (!names)
|
|
783
|
+
return visit((frame.field = item[frame.index]), frame.prose, frame.depth + 1);
|
|
784
|
+
const key = (frame.key = frame.name = names[frame.index]);
|
|
785
|
+
const descriptor = Object.getOwnPropertyDescriptor(item, key);
|
|
786
|
+
let prose = PROSE_KEYS.has(key);
|
|
787
|
+
if (descriptor && "value" in descriptor)
|
|
788
|
+
frame.field = descriptor.value;
|
|
789
|
+
// V8 lazily renders Error.stack through an own accessor. Snapshot it
|
|
790
|
+
// before rebuilding an error, so its diagnostic is redacted too.
|
|
791
|
+
else if (key === "stack" && item instanceof Error) {
|
|
792
|
+
frame.field = item.stack;
|
|
793
|
+
prose = true;
|
|
794
|
+
}
|
|
795
|
+
// JSON and errors carry data properties. A downstream-authored getter
|
|
796
|
+
// is not a safe way to expose a diagnostic to an agent.
|
|
797
|
+
else
|
|
798
|
+
return DROP;
|
|
799
|
+
const field = frame.field;
|
|
800
|
+
if (!FRAMING_KEYS.has(key))
|
|
801
|
+
frame.name = this.string(key, scan, false);
|
|
802
|
+
if ((typeof field === "string" && TAGS.has(`${key}:${field}`)) ||
|
|
803
|
+
(FACTS.has(key) && (typeof field === "boolean" || typeof field === "number")))
|
|
804
|
+
return field;
|
|
805
|
+
if (frame.typed && (key === "code" || key === "name") && typeof field === "string")
|
|
806
|
+
return this.credentials().string(field, { ...scan, payload: false }, false);
|
|
807
|
+
if (key === "blob" && "uri" in item && typeof field === "string")
|
|
808
|
+
return this.blob(field, scan);
|
|
809
|
+
const child = key === "content" ? this.joinedContent(field, scan, frame.depth, level) : field;
|
|
810
|
+
return visit(child, prose, frame.depth + 1);
|
|
811
|
+
};
|
|
812
|
+
// Record a settled entry. The first change copies the container's earlier entries.
|
|
813
|
+
const settle = (frame, value) => {
|
|
814
|
+
const { item, names, index } = frame;
|
|
815
|
+
if (!frame.copy) {
|
|
816
|
+
if (value === frame.field && frame.name === frame.key)
|
|
817
|
+
return;
|
|
818
|
+
if (!names)
|
|
819
|
+
frame.copy = item.slice(0, index);
|
|
820
|
+
else {
|
|
821
|
+
frame.copy = item instanceof Error ? Object.create(Object.getPrototypeOf(item)) : {};
|
|
822
|
+
for (const key of names.slice(0, index))
|
|
823
|
+
define(frame.copy, item, key, key, item[key]);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
if (!names)
|
|
827
|
+
frame.copy[index] = value;
|
|
828
|
+
else if (value !== DROP)
|
|
829
|
+
define(frame.copy, item, frame.key, frame.name, value);
|
|
830
|
+
};
|
|
831
|
+
let settled = visit(value, prose, depth);
|
|
832
|
+
while (stack.length) {
|
|
833
|
+
const frame = stack[stack.length - 1];
|
|
834
|
+
if (settled === OPEN)
|
|
835
|
+
settled = enter(frame);
|
|
836
|
+
if (settled === OPEN)
|
|
837
|
+
continue;
|
|
838
|
+
settle(frame, settled);
|
|
839
|
+
settled = OPEN;
|
|
840
|
+
if (++frame.index < frame.size)
|
|
841
|
+
continue;
|
|
842
|
+
stack.pop();
|
|
843
|
+
active.delete(frame.item);
|
|
844
|
+
const { item, copy } = frame;
|
|
845
|
+
settled = !copy ? item : item instanceof Error ? carryFailureFacts(item, copy) : copy;
|
|
846
|
+
}
|
|
847
|
+
return settled;
|
|
848
|
+
}
|
|
849
|
+
/** A container larger than the node budget is scanned as serialized text, Error fields
|
|
850
|
+
* included: kept unchanged when nothing registered appears, otherwise withheld whole. */
|
|
851
|
+
serialized(item, scan, level) {
|
|
852
|
+
let text;
|
|
853
|
+
try {
|
|
854
|
+
text = JSON.stringify(item, (_key, value) => value instanceof Error
|
|
855
|
+
? Object.fromEntries(Object.getOwnPropertyNames(value).map((name) => [name, Reflect.get(value, name)]))
|
|
856
|
+
: value);
|
|
857
|
+
}
|
|
858
|
+
catch {
|
|
859
|
+
// A cycle or bigint cannot be serialized, so the container is withheld.
|
|
860
|
+
}
|
|
861
|
+
if (text === undefined || text.length >= this.floor(scan))
|
|
862
|
+
return REDACTED;
|
|
863
|
+
const found = this.find(text, scan, true, level);
|
|
864
|
+
return found && !found.spans.length && !(scan.payload && found.short) ? item : REDACTED;
|
|
865
|
+
}
|
|
866
|
+
/** Skill supporting files may contain arbitrary bytes, including echoes. */
|
|
867
|
+
blob(value, scan) {
|
|
868
|
+
let binary;
|
|
869
|
+
try {
|
|
870
|
+
binary = atob(value);
|
|
871
|
+
}
|
|
872
|
+
catch {
|
|
873
|
+
return this.string(value, scan, false);
|
|
874
|
+
}
|
|
875
|
+
// Byte-valued code units match ASCII forms directly. Bytes are data, never prose.
|
|
876
|
+
let redacted = this.string(binary, scan, false);
|
|
877
|
+
if (this.unicode && redacted !== REDACTED) {
|
|
878
|
+
// A UTF-8 view also detects non-ASCII and mixed escaped echoes. Never re-encode that
|
|
879
|
+
// view: an arbitrary supporting file may contain invalid UTF-8 or a leading BOM.
|
|
880
|
+
const view = new TextDecoder("utf-8", { fatal: false, ignoreBOM: true }).decode(Uint8Array.from(redacted, (char) => char.charCodeAt(0)));
|
|
881
|
+
if (this.string(view, scan, false) !== view)
|
|
882
|
+
redacted = REDACTED;
|
|
883
|
+
}
|
|
884
|
+
const encoded = redacted === binary ? value : btoa(redacted);
|
|
885
|
+
// A file's encoding can itself equal a secret. Withhold that file as
|
|
886
|
+
// a valid encoded placeholder, never insert prose into its base64 field.
|
|
887
|
+
if (this.string(encoded, scan, false) === encoded)
|
|
888
|
+
return encoded;
|
|
889
|
+
const withheld = btoa(REDACTED);
|
|
890
|
+
return this.string(withheld, scan, false) === withheld ? withheld : "";
|
|
891
|
+
}
|
|
892
|
+
/** Match across blocks before isError/JSON unwrapping concatenates them. */
|
|
893
|
+
joinedContent(value, scan, depth, level) {
|
|
894
|
+
if (!Array.isArray(value))
|
|
895
|
+
return value;
|
|
896
|
+
const blocks = value.filter((block) => block?.type === "text" && typeof block.text === "string");
|
|
897
|
+
if (blocks.length < 2)
|
|
898
|
+
return value;
|
|
899
|
+
for (const separator of ["", "\n"]) {
|
|
900
|
+
const joined = blocks.map((block) => block.text).join(separator);
|
|
901
|
+
const redacted = this.wire(joined, scan, depth, true, level);
|
|
902
|
+
if (redacted === blocks.map((block) => this.wire(block.text, scan, depth, true, level)).join(separator))
|
|
903
|
+
continue;
|
|
904
|
+
let first = true;
|
|
905
|
+
return value.map((block) => {
|
|
906
|
+
if (!blocks.includes(block))
|
|
907
|
+
return block;
|
|
908
|
+
const text = first ? redacted : "";
|
|
909
|
+
first = false;
|
|
910
|
+
return { ...block, text };
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
return value;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
export function sentSecretsFor(ctx) {
|
|
917
|
+
let secrets = contexts.get(ctx);
|
|
918
|
+
if (!secrets) {
|
|
919
|
+
secrets = new SentSecrets();
|
|
920
|
+
contexts.set(ctx, secrets);
|
|
921
|
+
}
|
|
922
|
+
sentSecretsForRequest(ctx.requestScope ?? ctx).include(secrets);
|
|
923
|
+
return secrets;
|
|
924
|
+
}
|
|
925
|
+
/** One identity per upstream request, including all discovery and call work. */
|
|
926
|
+
export function sentSecretsForRequest(scope, secrets) {
|
|
927
|
+
let request = requests.get(scope);
|
|
928
|
+
if (!request) {
|
|
929
|
+
request = secrets ?? new SentSecrets();
|
|
930
|
+
requests.set(scope, request);
|
|
931
|
+
}
|
|
932
|
+
else if (secrets) {
|
|
933
|
+
request.include(secrets);
|
|
934
|
+
}
|
|
935
|
+
return request;
|
|
936
|
+
}
|
|
937
|
+
/** The agent-facing choke point. Apply to values before bridge serialization
|
|
938
|
+
* and to serialized wire text, so JSON escapes and structured strings share
|
|
939
|
+
* the same rule. Payloads were redacted where they entered; this boundary
|
|
940
|
+
* repeats credential and literal private-value replacement around Connecta's
|
|
941
|
+
* own envelopes, whose identifiers and counts are never private data. */
|
|
942
|
+
export function redactAgentOutput(secrets, value) {
|
|
943
|
+
return secrets.redact(value, "envelope");
|
|
944
|
+
}
|
|
945
|
+
/** Wrap the complete operation table, including rejections. Adding an operation
|
|
946
|
+
* cannot add an unredacted exit. A direct invocation owns a fresh scope unless
|
|
947
|
+
* its HTTP request supplied one. */
|
|
948
|
+
export function agentOutputOperations(create, requestScope) {
|
|
949
|
+
return Object.fromEntries(Object.keys(create(requestScope ?? {})).map((name) => [
|
|
950
|
+
name,
|
|
951
|
+
async (...args) => {
|
|
952
|
+
const scope = requestScope ?? {};
|
|
953
|
+
const secrets = sentSecretsForRequest(scope);
|
|
954
|
+
try {
|
|
955
|
+
return redactAgentOutput(secrets, await create(scope)[name](...args));
|
|
956
|
+
}
|
|
957
|
+
catch (error) {
|
|
958
|
+
throw redactAgentOutput(secrets, error);
|
|
959
|
+
}
|
|
960
|
+
},
|
|
961
|
+
]));
|
|
962
|
+
}
|
|
963
|
+
/** Slot reads cover custom handlers too, including keys put in query strings. */
|
|
964
|
+
export function trackCredentialReads(ctx) {
|
|
965
|
+
if (!ctx.credential || wrappedCredentials.has(ctx))
|
|
966
|
+
return;
|
|
967
|
+
wrappedCredentials.add(ctx);
|
|
968
|
+
const credential = ctx.credential;
|
|
969
|
+
ctx.credential = {
|
|
970
|
+
async get(field) {
|
|
971
|
+
const value = await credential.get(field);
|
|
972
|
+
if (value) {
|
|
973
|
+
const secrets = sentSecretsFor(ctx);
|
|
974
|
+
if (field && explicitSecretName.test(field))
|
|
975
|
+
secrets.secret(value);
|
|
976
|
+
else
|
|
977
|
+
secrets.add(value);
|
|
978
|
+
}
|
|
979
|
+
return value;
|
|
980
|
+
},
|
|
981
|
+
async getAll() {
|
|
982
|
+
const values = await credential.getAll();
|
|
983
|
+
for (const [field, value] of Object.entries(values ?? {})) {
|
|
984
|
+
if (explicitSecretName.test(field))
|
|
985
|
+
sentSecretsFor(ctx).secret(value);
|
|
986
|
+
else
|
|
987
|
+
sentSecretsFor(ctx).add(value);
|
|
988
|
+
}
|
|
989
|
+
return values;
|
|
990
|
+
},
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
export function redactSentSecrets(ctx, value) {
|
|
994
|
+
return sentSecretsFor(ctx).redact(value);
|
|
995
|
+
}
|
|
996
|
+
/** Sanitize listing facts before any cache or consumer receives them. A name
|
|
997
|
+
* cannot be rewritten without changing dispatch, and dropping one entry would
|
|
998
|
+
* publish a partial catalog, so refuse the complete listing instead. */
|
|
999
|
+
export function redactCatalog(ctx, tools) {
|
|
1000
|
+
sentSecretsFor(ctx);
|
|
1001
|
+
const redacted = sentSecretsForRequest(ctx.requestScope ?? ctx).redactCredentials(tools);
|
|
1002
|
+
if (redacted.some((tool, index) => tool.name !== tools[index].name)) {
|
|
1003
|
+
throw new ConnectorCallError("connector_call_failed", "Downstream catalog contains a tool name that echoes a sent credential; refusing the complete catalog.", { retryable: false });
|
|
1004
|
+
}
|
|
1005
|
+
return redacted;
|
|
1006
|
+
}
|
|
1007
|
+
/** Register only after the transport has assembled the request it sends. */
|
|
1008
|
+
export function sentSecretsFetch(ctx, send = fetch) {
|
|
1009
|
+
return (input, init) => {
|
|
1010
|
+
sentSecretsFor(ctx).request(input, init);
|
|
1011
|
+
return send(input, init);
|
|
1012
|
+
};
|
|
1013
|
+
}
|