@zackbart/connecta 0.34.1 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +418 -0
- package/README.md +65 -105
- package/SECURITY.md +3 -2
- package/bin/connecta.mjs +41 -16
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.d.ts +2 -1
- package/dist/access-tokens.js +66 -30
- package/dist/activity.d.ts +1 -1
- package/dist/activity.js +2 -0
- package/dist/agent-config.d.ts +23 -0
- package/dist/agent-config.js +59 -0
- package/dist/agent-description.js +16 -0
- package/dist/agent-identity.js +4 -4
- package/dist/agent-observation.js +39 -0
- package/dist/auth/clerk-sdk/client.d.ts +2 -0
- package/dist/auth/clerk-sdk/generated.js +554 -563
- package/dist/auth/clerk-transport.js +70 -3
- package/dist/auth/clerk.d.ts +13 -0
- package/dist/auth/clerk.js +653 -246
- package/dist/auth/cloudflare-access.js +46 -42
- package/dist/auth/downstream-client-metadata.js +2 -2
- package/dist/auth/downstream-oauth.js +2044 -1161
- package/dist/auth/downstream-revocation.js +88 -88
- package/dist/auth/native-call.js +20 -0
- package/dist/auth/native.js +184 -0
- package/dist/auth/oauth-start-reset.js +11 -5
- package/dist/auth/oauth-token-response.js +121 -102
- package/dist/auth/oauth-v2-migration.js +97 -78
- package/dist/auth/renewable-grant.js +10 -0
- package/dist/auth/static-oauth.js +751 -281
- package/dist/auth/vercel-agent.js +266 -0
- package/dist/auth-admission.js +204 -0
- package/dist/auth-elicitation.js +350 -241
- package/dist/authorization-handoff.js +164 -90
- package/dist/call-admission-context.js +3 -10
- package/dist/call-admission.js +2 -0
- package/dist/call-classification.js +9 -0
- package/dist/call-recovery.js +32 -13
- package/dist/canonical-json.js +8 -0
- package/dist/catalog-cache.js +369 -213
- package/dist/catalog-service.js +659 -530
- package/dist/catalog.js +5 -1
- package/dist/concurrency.js +0 -25
- package/dist/config-defaults.d.ts +1 -0
- package/dist/config-defaults.js +1 -0
- package/dist/config-schema.d.ts +3 -1
- package/dist/config-schema.js +13 -3
- package/dist/config.d.ts +40 -6
- package/dist/config.js +40 -26
- package/dist/connector-caller.d.ts +4 -0
- package/dist/connector-caller.js +17 -4
- package/dist/connectors/api-connector.d.ts +29 -7
- package/dist/connectors/api-connector.js +145 -57
- package/dist/connectors/guarded-fetch.d.ts +51 -17
- package/dist/connectors/guarded-fetch.js +215 -121
- package/dist/connectors/negotiation-cache.js +20 -10
- package/dist/connectors/remote-mcp.d.ts +10 -30
- package/dist/connectors/remote-mcp.js +955 -563
- package/dist/d1.js +10 -243
- package/dist/derived-context.js +5 -0
- package/dist/describe-config.d.ts +3 -1
- package/dist/describe-config.js +2 -0
- package/dist/downstream-input.js +347 -260
- package/dist/errors.d.ts +87 -6
- package/dist/errors.js +114 -4
- package/dist/execute.js +87 -50
- package/dist/executor-admission.js +3 -0
- package/dist/executors/quickjs.js +163 -112
- package/dist/guarded-transport.js +50 -0
- package/dist/guest-runtime.js +1 -1
- package/dist/http/client.js +439 -0
- package/dist/http/errors.js +558 -0
- package/dist/http/fields.js +49 -0
- package/dist/http/hatch-paging.js +318 -0
- package/dist/http/hatch.js +1279 -0
- package/dist/http/json-schema.js +267 -0
- package/dist/http/node-transport.js +241 -0
- package/dist/http/oauth.js +162 -0
- package/dist/http/paging.js +227 -0
- package/dist/http/provider.js +221 -0
- package/dist/http/safety.js +82 -0
- package/dist/http/services.js +79 -0
- package/dist/http/tools.js +379 -0
- package/dist/http/vendor-index.js +138 -0
- package/dist/http/vendor.js +448 -0
- package/dist/http/write-request.js +66 -0
- package/dist/http-policy.d.ts +11 -0
- package/dist/http-policy.js +45 -0
- package/dist/inbound-credential.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -0
- package/dist/invocation-auth.js +30 -19
- package/dist/invocation.js +218 -85
- package/dist/mcp-result.js +0 -4
- package/dist/membership.d.ts +4 -0
- package/dist/meta-output.js +1 -1
- package/dist/meta-tools.js +531 -406
- package/dist/native-recovery.js +52 -0
- package/dist/native-tools.js +382 -0
- package/dist/node.d.ts +4 -3
- package/dist/node.js +45 -8
- package/dist/oauth-handoff.js +177 -104
- package/dist/oauth-partition.js +6 -20
- package/dist/oauth-sealing.js +6 -7
- package/dist/operation-vocabulary.js +101 -0
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +17 -4
- package/dist/operator-ui/contract.d.ts +8 -3
- package/dist/operator-ui/generated.js +4 -4
- package/dist/operator-ui/model.d.ts +2 -3
- package/dist/program-writes.js +47 -26
- package/dist/provider-call-facts.d.ts +115 -0
- package/dist/provider-call-facts.js +182 -0
- package/dist/provider-call.d.ts +112 -0
- package/dist/provider-call.js +271 -0
- package/dist/provider.d.ts +65 -31
- package/dist/provider.js +116 -39
- package/dist/providers/_shared/google/hatch.js +91 -0
- package/dist/providers/_shared/google/kit.js +427 -0
- package/dist/providers/_shared/google/result-size.js +15 -2
- package/dist/providers/_shared/google/service-account.d.ts +2 -1
- package/dist/providers/_shared/google/service-account.js +8 -4
- package/dist/providers/_shared/google/workspace.d.ts +6 -210
- package/dist/providers/_shared/google/workspace.js +6 -425
- package/dist/providers/_shared/rest/compact-index.js +86 -0
- package/dist/providers/_shared/rest/kit.js +146 -0
- package/dist/providers/_shared/rest/operation-index.js +230 -14
- package/dist/providers/_shared/rest/response-media.js +11 -0
- package/dist/providers/_shared/rest/value-safety.js +353 -195
- package/dist/providers/_shared/sensitive-fields.js +27 -0
- package/dist/providers/breeze/api.js +584 -0
- package/dist/providers/breeze/index.d.ts +1 -3
- package/dist/providers/breeze/index.js +67 -1420
- package/dist/providers/breeze/openapi.generated.js +60 -0
- package/dist/providers/breeze/skill.generated.js +2 -6
- package/dist/providers/breeze/tools.js +696 -0
- package/dist/providers/ccb/api.js +496 -0
- package/dist/providers/ccb/index.d.ts +15 -37
- package/dist/providers/ccb/index.js +77 -1702
- package/dist/providers/ccb/lib/constants.d.ts +34 -0
- package/dist/providers/ccb/lib/constants.js +74 -0
- package/dist/providers/ccb/lib/guide.js +9 -0
- package/dist/providers/ccb/lib/verdicts.js +387 -0
- package/dist/providers/ccb/openapi.generated.js +397 -0
- package/dist/providers/ccb/safety.js +295 -0
- package/dist/providers/ccb/skill.generated.js +2 -6
- package/dist/providers/ccb/tools.js +853 -0
- package/dist/providers/cloudflare/api.js +1066 -0
- package/dist/providers/cloudflare/index.d.ts +5 -22
- package/dist/providers/cloudflare/index.js +79 -197
- package/dist/providers/cloudflare/lib/config.d.ts +16 -0
- package/dist/providers/cloudflare/lib/config.js +2 -0
- package/dist/providers/cloudflare/lib/discovery.js +29 -0
- package/dist/providers/cloudflare/{graphql.js → lib/graphql.js} +1 -1
- package/dist/providers/cloudflare/lib/hatch.js +103 -0
- package/dist/providers/cloudflare/lib/path-bounds.js +28 -0
- package/dist/providers/cloudflare/lib/response-media.js +75 -0
- package/dist/providers/cloudflare/openapi.generated.js +4 -3
- package/dist/providers/cloudflare/{value-safety.js → safety.js} +487 -0
- package/dist/providers/cloudflare/skill.generated.js +2 -10
- package/dist/providers/cloudflare/tools.js +360 -0
- package/dist/providers/docs/api.js +51 -0
- package/dist/providers/docs/index.d.ts +1 -12
- package/dist/providers/docs/index.js +53 -1165
- package/dist/providers/docs/lib/constants.d.ts +18 -0
- package/dist/providers/docs/lib/constants.js +18 -0
- package/dist/providers/docs/lib/discovery.js +26 -0
- package/dist/providers/docs/lib/requests.js +48 -0
- package/dist/providers/docs/lib/schemas.js +168 -0
- package/dist/providers/docs/openapi.generated.js +16 -0
- package/dist/providers/docs/safety.js +54 -0
- package/dist/providers/docs/skill.generated.js +2 -6
- package/dist/providers/docs/tools.js +801 -0
- package/dist/providers/drive/api.js +177 -0
- package/dist/providers/drive/index.d.ts +1 -12
- package/dist/providers/drive/index.js +38 -1448
- package/dist/providers/drive/lib/constants.d.ts +6 -0
- package/dist/providers/drive/lib/constants.js +6 -0
- package/dist/providers/drive/lib/contracts.js +532 -0
- package/dist/providers/drive/lib/hatch.js +197 -0
- package/dist/providers/drive/openapi.generated.js +77 -0
- package/dist/providers/drive/safety.js +90 -0
- package/dist/providers/drive/skill.generated.js +2 -6
- package/dist/providers/drive/tools.js +1290 -0
- package/dist/providers/forms/api.js +71 -0
- package/dist/providers/forms/index.d.ts +1 -12
- package/dist/providers/forms/index.js +49 -1145
- package/dist/providers/forms/lib/constants.d.ts +14 -0
- package/dist/providers/forms/lib/constants.js +19 -0
- package/dist/providers/forms/lib/hatch.js +72 -0
- package/dist/providers/forms/openapi.generated.js +23 -0
- package/dist/providers/forms/safety.js +34 -0
- package/dist/providers/forms/skill.generated.js +2 -8
- package/dist/providers/forms/tools.js +1023 -0
- package/dist/providers/github/api.js +150 -0
- package/dist/providers/github/index.d.ts +4 -5
- package/dist/providers/github/index.js +30 -87
- package/dist/providers/github/{auth.d.ts → lib/auth.d.ts} +5 -3
- package/dist/providers/github/{auth.js → lib/auth.js} +40 -43
- package/dist/providers/github/lib/discovery.js +132 -0
- package/dist/providers/github/lib/operations.js +58 -0
- package/dist/providers/github/{scope.d.ts → lib/scope.d.ts} +2 -0
- package/dist/providers/github/{scope.js → lib/scope.js} +25 -1
- package/dist/providers/github/openapi.generated.js +1216 -0
- package/dist/providers/github/safety.js +216 -0
- package/dist/providers/github/skill.generated.js +2 -5
- package/dist/providers/github/tools.js +506 -0
- package/dist/providers/gmail/api.js +114 -0
- package/dist/providers/gmail/index.d.ts +1 -12
- package/dist/providers/gmail/index.js +73 -1621
- package/dist/providers/gmail/lib/constants.d.ts +13 -0
- package/dist/providers/gmail/lib/constants.js +16 -0
- package/dist/providers/gmail/lib/index.js +173 -0
- package/dist/providers/gmail/lib/schemas.js +176 -0
- package/dist/providers/gmail/lib/threading.js +120 -0
- package/dist/providers/gmail/openapi.generated.js +92 -0
- package/dist/providers/gmail/safety.js +21 -0
- package/dist/providers/gmail/skill.generated.js +2 -6
- package/dist/providers/gmail/tools.js +1435 -0
- package/dist/providers/overflow/api.js +1008 -0
- package/dist/providers/overflow/index.d.ts +1 -17
- package/dist/providers/overflow/index.js +60 -1488
- package/dist/providers/overflow/skill.generated.js +2 -5
- package/dist/providers/overflow/tools.js +873 -0
- package/dist/providers/planning-center/api.js +467 -0
- package/dist/providers/planning-center/index.d.ts +4 -35
- package/dist/providers/planning-center/index.js +69 -2445
- package/dist/providers/planning-center/lib/constants.d.ts +58 -0
- package/dist/providers/planning-center/lib/constants.js +83 -0
- package/dist/providers/planning-center/lib/discovery.js +40 -0
- package/dist/providers/planning-center/lib/guide.js +39 -0
- package/dist/providers/planning-center/lib/json-api.js +83 -0
- package/dist/providers/planning-center/lib/verdicts.js +1758 -0
- package/dist/providers/planning-center/openapi.generated.js +1764 -0
- package/dist/providers/planning-center/safety.js +188 -0
- package/dist/providers/planning-center/skill.generated.js +2 -27
- package/dist/providers/planning-center/tools.js +1010 -0
- package/dist/providers/quickbooks/api.js +299 -0
- package/dist/providers/quickbooks/index.d.ts +13 -0
- package/dist/providers/quickbooks/index.js +93 -0
- package/dist/providers/quickbooks/lib/query.js +118 -0
- package/dist/providers/quickbooks/lib/writes.js +168 -0
- package/dist/providers/quickbooks/safety.js +222 -0
- package/dist/providers/quickbooks/skill.generated.js +5 -0
- package/dist/providers/quickbooks/tools.js +117 -0
- package/dist/providers/sheets/api.js +133 -0
- package/dist/providers/sheets/index.d.ts +4 -16
- package/dist/providers/sheets/index.js +61 -1172
- package/dist/providers/sheets/lib/constants.d.ts +14 -0
- package/dist/providers/sheets/lib/constants.js +14 -0
- package/dist/providers/sheets/lib/hatch.js +163 -0
- package/dist/providers/sheets/lib/requests.js +105 -0
- package/dist/providers/sheets/lib/schemas.js +138 -0
- package/dist/providers/sheets/openapi.generated.js +30 -0
- package/dist/providers/sheets/safety.js +27 -0
- package/dist/providers/sheets/skill.generated.js +2 -7
- package/dist/providers/sheets/tools.js +733 -0
- package/dist/providers/slides/api.js +147 -0
- package/dist/providers/slides/index.d.ts +2 -10
- package/dist/providers/slides/index.js +48 -2555
- package/dist/providers/slides/lib/constants.d.ts +11 -0
- package/dist/providers/slides/lib/constants.js +11 -0
- package/dist/providers/slides/lib/requests.js +60 -0
- package/dist/providers/slides/lib/schemas.js +565 -0
- package/dist/providers/slides/openapi.generated.js +18 -0
- package/dist/providers/slides/skill.generated.js +2 -6
- package/dist/providers/slides/tools.js +1968 -0
- package/dist/providers/stripe/api.js +401 -0
- package/dist/providers/stripe/index.d.ts +5 -27
- package/dist/providers/stripe/index.js +74 -210
- package/dist/providers/stripe/lib/config.d.ts +4 -0
- package/dist/providers/stripe/lib/config.js +2 -0
- package/dist/providers/stripe/openapi.generated.js +4 -3
- package/dist/providers/stripe/{value-safety.js → safety.js} +5 -5
- package/dist/providers/stripe/skill.generated.js +2 -11
- package/dist/providers/stripe/tools.js +77 -0
- package/dist/providers/tithely/api.js +531 -0
- package/dist/providers/tithely/index.d.ts +17 -0
- package/dist/providers/tithely/index.js +86 -965
- package/dist/providers/tithely/skill.generated.js +2 -5
- package/dist/providers/tithely/tools.js +426 -0
- package/dist/providers/vercel/api.js +551 -0
- package/dist/providers/vercel/index.d.ts +9 -25
- package/dist/providers/vercel/index.js +65 -260
- package/dist/providers/vercel/lib/config.d.ts +2 -0
- package/dist/providers/vercel/lib/config.js +2 -0
- package/dist/providers/vercel/lib/index.js +117 -0
- package/dist/providers/vercel/lib/logs.js +100 -0
- package/dist/providers/vercel/lib/pin.js +270 -0
- package/dist/providers/vercel/openapi.generated.js +10 -5
- package/dist/providers/vercel/{value-safety.js → safety.js} +247 -38
- package/dist/providers/vercel/skill.generated.js +2 -7
- package/dist/providers/vercel/tools.js +642 -0
- package/dist/redaction-identity.js +47 -0
- package/dist/redaction-marker.js +125 -0
- package/dist/registry.d.ts +21 -15
- package/dist/registry.js +409 -376
- package/dist/routes/access-tokens.js +1 -1
- package/dist/routes/actor-labels.js +1 -1
- package/dist/routes/connect.js +1 -1
- package/dist/routes/dispatch.js +103 -0
- package/dist/routes/mcp.js +170 -79
- package/dist/routes/oauth.js +6 -1
- package/dist/routes/operator.js +2 -2
- package/dist/routes/shared.d.ts +5 -4
- package/dist/routes/shared.js +244 -177
- package/dist/routes/skills.js +10 -8
- package/dist/routes/ui-config.js +27 -3
- package/dist/routes/ui-data.js +21 -20
- package/dist/routes/ui.js +1 -1
- package/dist/runtime/call-admission.js +0 -26
- package/dist/runtime/call-entry.js +30 -0
- package/dist/runtime/call.js +552 -0
- package/dist/runtime/connector-scope.js +7 -0
- package/dist/runtime/credential-probe.js +34 -0
- package/dist/runtime/executor.js +40 -0
- package/dist/runtime/native-storage.js +63 -0
- package/dist/runtime/registry.js +27 -0
- package/dist/runtime/request-scope.js +72 -0
- package/dist/runtime/run.js +15 -7
- package/dist/runtime/shared-read.js +34 -5
- package/dist/runtime/storage.js +34 -21
- package/dist/sent-secrets.d.ts +194 -37
- package/dist/sent-secrets.js +1743 -340
- package/dist/server.js +5 -56
- package/dist/skills.js +322 -223
- package/dist/sqlite.js +15 -14
- package/dist/storage/d1-copy.js +255 -0
- package/dist/storage/d1-driver.js +15 -0
- package/dist/storage/keys.js +28 -0
- package/dist/storage/sql-driver.js +88 -0
- package/dist/storage/sql-native.js +544 -0
- package/dist/storage/sql-schema.d.ts +6 -0
- package/dist/storage/sql-schema.js +15 -0
- package/dist/storage/sql.d.ts +14 -20
- package/dist/storage/sql.js +29 -535
- package/dist/timeout.js +0 -26
- package/dist/tool-safety.js +20 -10
- package/dist/types.d.ts +9 -109
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +0 -1
- package/dist/upward.js +3 -0
- package/dist/usage-guide.js +3 -1
- package/dist/validate.d.ts +4 -2
- package/dist/validate.js +86 -9
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +312 -292
- package/dist/workspace-subject.js +5 -1
- package/dist/write-outcome.js +454 -34
- package/documentation/README.md +28 -7
- package/documentation/architecture.md +2409 -255
- package/documentation/auth.md +867 -114
- package/documentation/code-mode.md +49 -16
- package/documentation/deploying.md +164 -20
- package/documentation/hosted-mcp-migration.md +460 -0
- package/documentation/integrating.md +129 -144
- package/documentation/meta-tools.md +409 -32
- package/documentation/operating.md +27 -3
- package/documentation/operator-tests.md +3 -0
- package/documentation/operator-ui.md +84 -48
- package/documentation/provider-auth-migration.md +16 -63
- package/documentation/provider-migration-0.29.md +10 -5
- package/documentation/provider-migration-0.35.md +975 -0
- package/documentation/providers.md +1347 -0
- package/examples/worker/AGENTS.md +23 -9
- package/examples/worker/README.md +254 -135
- package/examples/worker/alchemy.run.ts +116 -0
- package/examples/worker/gitignore +10 -0
- package/examples/worker/src/connecta.config.ts +23 -21
- package/examples/worker/wrangler.jsonc +14 -5
- package/guides/connecting-agents.md +157 -0
- package/guides/how-it-works.md +156 -0
- package/guides/safety-and-trust.md +153 -0
- package/package.json +30 -50
- package/templates/node/.env.example +3 -0
- package/templates/node/README.md +22 -2
- package/templates/node/docker-compose.yml +1 -0
- package/templates/node/package.json +3 -2
- package/templates/node/src/connecta.config.ts +8 -0
- package/dist/catalog-drift.d.ts +0 -138
- package/dist/catalog-drift.js +0 -578
- package/dist/operator-ui/agents.d.ts +0 -74
- package/dist/operator-ui/agents.js +0 -104
- package/dist/operator-ui/config-summary.js +0 -565
- package/dist/operator-ui/connection-map.js +0 -376
- package/dist/operator-ui/endpoints.js +0 -221
- package/dist/operator-ui/fix-prompts.d.ts +0 -18
- package/dist/operator-ui/fix-prompts.js +0 -87
- package/dist/operator-ui/health.js +0 -52
- package/dist/operator-ui/member-page.js +0 -97
- package/dist/operator-ui/overview.js +0 -123
- package/dist/operator-ui/setup-commands.js +0 -51
- package/dist/operator-ui/view.d.ts +0 -401
- package/dist/operator-ui/view.js +0 -705
- package/dist/providers/_shared/request-token.js +0 -83
- package/dist/providers/_shared/rest/dispatch.js +0 -41
- package/dist/providers/_shared/rest/operation-index.d.ts +0 -133
- package/dist/providers/_shared/rest/tools.d.ts +0 -141
- package/dist/providers/_shared/rest/tools.js +0 -569
- package/dist/providers/_shared/rest/value-safety.d.ts +0 -201
- package/dist/providers/basecamp/index.d.ts +0 -45
- package/dist/providers/basecamp/index.js +0 -997
- package/dist/providers/basecamp/skill.generated.js +0 -9
- package/dist/providers/cloudflare/rest.d.ts +0 -37
- package/dist/providers/cloudflare/rest.js +0 -1097
- package/dist/providers/github/hosted.js +0 -218
- package/dist/providers/github/rest.js +0 -321
- package/dist/providers/infisical/index.d.ts +0 -7
- package/dist/providers/infisical/index.js +0 -713
- package/dist/providers/infisical/skill.generated.js +0 -8
- package/dist/providers/intercom/index.d.ts +0 -24
- package/dist/providers/intercom/index.js +0 -121
- package/dist/providers/intercom/skill.generated.js +0 -8
- package/dist/providers/linear/index.d.ts +0 -55
- package/dist/providers/linear/index.js +0 -537
- package/dist/providers/linear/skill.generated.js +0 -8
- package/dist/providers/mixpanel/index.d.ts +0 -59
- package/dist/providers/mixpanel/index.js +0 -472
- package/dist/providers/mixpanel/operations.js +0 -20
- package/dist/providers/mixpanel/rest.js +0 -603
- package/dist/providers/mixpanel/skill.generated.js +0 -10
- package/dist/providers/mixpanel/value-safety.js +0 -16
- package/dist/providers/notion/index.d.ts +0 -67
- package/dist/providers/notion/index.js +0 -368
- package/dist/providers/notion/openapi.generated.js +0 -75
- package/dist/providers/notion/rest.d.ts +0 -24
- package/dist/providers/notion/rest.js +0 -1226
- package/dist/providers/notion/skill.generated.js +0 -11
- package/dist/providers/notion/value-safety.js +0 -56
- package/dist/providers/revenuecat/index.d.ts +0 -51
- package/dist/providers/revenuecat/index.js +0 -697
- package/dist/providers/revenuecat/skill.generated.js +0 -11
- package/dist/providers/stripe/rest.d.ts +0 -14
- package/dist/providers/stripe/rest.js +0 -402
- package/dist/providers/vercel/rest.d.ts +0 -20
- package/dist/providers/vercel/rest.js +0 -1020
- package/dist/providers/wispr-flow/index.d.ts +0 -14
- package/dist/providers/wispr-flow/index.js +0 -117
- package/dist/providers/wispr-flow/skill.generated.js +0 -8
- /package/dist/providers/github/{key.js → lib/key.js} +0 -0
package/dist/sent-secrets.js
CHANGED
|
@@ -1,24 +1,41 @@
|
|
|
1
|
+
import { authCall, authMethod } from "./auth/native.js";
|
|
2
|
+
import { Effect } from "effect";
|
|
1
3
|
// Credentials and submitted private argument values used by one upstream request. Memory
|
|
2
4
|
// only; never part of a context's public shape, a failure record, storage, or a log. Web APIs only.
|
|
3
|
-
import {
|
|
4
|
-
import { ConnectorCallError } from "./errors.js";
|
|
5
|
+
import { classificationCode, ConnectorCallError } from "./errors.js";
|
|
5
6
|
import { credentialUrlViews } from "./credential-url.js";
|
|
7
|
+
import { RequestScope, requestOwner } from "./runtime/request-scope.js";
|
|
8
|
+
import { viewSecrets, wrapViewCredentials } from "./runtime/call.js";
|
|
6
9
|
import { visitPrivateCallArguments } from "./argument-redaction.js";
|
|
7
|
-
|
|
10
|
+
import { keptErrorName, redactedCopyOf } from "./redaction-identity.js";
|
|
11
|
+
import { REDACTED } from "./redaction-marker.js";
|
|
8
12
|
const encoder = new TextEncoder();
|
|
9
|
-
const contexts = new WeakMap();
|
|
10
|
-
const requests = new WeakMap();
|
|
11
|
-
const wrappedCredentials = new WeakSet();
|
|
12
13
|
const sensitiveName = /key|token|secret|password|auth|signature|session/i;
|
|
13
14
|
const explicitSecretName = /secret|password/i;
|
|
15
|
+
const identifierName = /(?:id|user(?:name)?|login|email)$|^public/i;
|
|
16
|
+
// Continuation cursors are named tokens but are public result state, returned changed on the
|
|
17
|
+
// next page; matching their fragments would corrupt the cursor the agent pages with.
|
|
18
|
+
const continuationName = /^(?:next_?)?page_?token$|^(?:next|continuation|sync)_?token$/i;
|
|
14
19
|
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
20
|
// Short credentials can match protocol fields or legitimate configuration.
|
|
16
21
|
const MIN_SECRET_LENGTH = 8;
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
+
// K: every contiguous run this long of a registered form is matched wherever it appears. It
|
|
23
|
+
// equals MIN_SECRET_LENGTH, so every registered credential and every private value of at least K
|
|
24
|
+
// units is fragment-indexed. Eight units keeps ordinary words, dates, counts and auth framing out
|
|
25
|
+
// of the index: a public run must repeat eight consecutive units of a secret's form to match.
|
|
26
|
+
const FRAGMENT_LENGTH = MIN_SECRET_LENGTH;
|
|
27
|
+
// A private value shorter than this is matched only as a whole field value: a leaf, key, number,
|
|
28
|
+
// boolean or decoded view equal to one of its forms. One to three units recur inside ordinary
|
|
29
|
+
// words, counts and ids, so a substring match would rewrite them everywhere.
|
|
30
|
+
const SUBSTRING_LENGTH = 4;
|
|
31
|
+
// Form kinds, weakest first, by the length of the value they came from. A tiny private value
|
|
32
|
+
// (1-3 units) is matched whole only. A short one (4-7) is also matched as a substring of payload
|
|
33
|
+
// text and decoded views. Longer private values and credentials are matched whole and by every
|
|
34
|
+
// K-unit run, wherever they occur.
|
|
35
|
+
const TINY = 1;
|
|
36
|
+
const SHORT = 2;
|
|
37
|
+
const PRIVATE = 3;
|
|
38
|
+
const CREDENTIAL = 4;
|
|
22
39
|
// Submitted values register at most this many form code units and argument nodes. A form
|
|
23
40
|
// that does not fit withholds every string long enough to contain it.
|
|
24
41
|
const MAX_PRIVATE_FORM_CHARS = 524_288;
|
|
@@ -40,26 +57,103 @@ const POWERS = new Int32Array(FORM_PREFIX + 1);
|
|
|
40
57
|
POWERS[0] = 1;
|
|
41
58
|
for (let length = 1; length <= FORM_PREFIX; length++)
|
|
42
59
|
POWERS[length] = Math.imul(POWERS[length - 1], HASH_BASE);
|
|
60
|
+
// Fragments are indexed by two independent polynomial hashes, at most MAX_FRAGMENTS distinct ones
|
|
61
|
+
// (8 bytes each, in a table at most half full); a larger set withholds every run of K units.
|
|
62
|
+
const SECOND_BASE = 0x5b_d1_e9_95;
|
|
63
|
+
const MAX_FRAGMENTS = 524_288;
|
|
64
|
+
// A unit's weight in a K-unit window, first and second position, under each base.
|
|
65
|
+
const [FIRST_TOP, FIRST_NEXT] = [POWERS[FRAGMENT_LENGTH - 1], POWERS[FRAGMENT_LENGTH - 2]];
|
|
66
|
+
let [SECOND_TOP, SECOND_NEXT] = [1, 1];
|
|
67
|
+
for (let length = 1; length < FRAGMENT_LENGTH; length++) {
|
|
68
|
+
SECOND_TOP = Math.imul(SECOND_TOP, SECOND_BASE);
|
|
69
|
+
if (length < FRAGMENT_LENGTH - 1)
|
|
70
|
+
SECOND_NEXT = Math.imul(SECOND_NEXT, SECOND_BASE);
|
|
71
|
+
}
|
|
43
72
|
// One redaction call visits at most this many structured entries, this deep. A string is
|
|
44
73
|
// decoded at most MAX_DECODE_LEVELS JSON layers, counting nested JSON strings it was parsed from.
|
|
45
74
|
const MAX_WALK_NODES = 1_048_576;
|
|
46
75
|
const MAX_WALK_DEPTH = 512;
|
|
47
76
|
const MAX_DECODE_LEVELS = 8;
|
|
77
|
+
// A decoded layer is scanned only around the escapes it decoded, this far past the widest form
|
|
78
|
+
// on each side: a match holding no newly decoded unit was already found a layer up. Wider forms
|
|
79
|
+
// scan the whole layer.
|
|
80
|
+
const MAX_WINDOW_UNITS = 4_096;
|
|
81
|
+
// Leaves too large to match joined within the work budget are matched alone; a run crossing
|
|
82
|
+
// two of them is matched this many units deep on each side of their seam.
|
|
83
|
+
const SEAM_UNITS = 512;
|
|
84
|
+
// Base64 or base64url text no declared field names is decoded and checked too: a whole view of
|
|
85
|
+
// at least ENCODED_FIELD_UNITS, or a run inside one long enough to carry the smallest piece the
|
|
86
|
+
// scan matches, up to MAX_ENCODED_LEVELS encodings deep. A run is a maximal span of base64 and
|
|
87
|
+
// base64url units, decoded from its first. L units decode to floor(3L/4) bytes, so p bytes that
|
|
88
|
+
// start a bytes into the encoding (a = 0, 1 or 2, its alignment) need L >= ceil(4(a + p)/3),
|
|
89
|
+
// least at a = 0. Padding follows a run and is never counted, so a padded and an unpadded
|
|
90
|
+
// encoding decode alike. The smallest piece is a short private value, four units and so at least
|
|
91
|
+
// four bytes, when payload rules apply and one is registered: ceil(16/3) = 6 units. Otherwise it
|
|
92
|
+
// is K units, at least eight bytes: ceil(32/3) = 11. A whole view needs no more than the first.
|
|
93
|
+
const ENCODED_FIELD_UNITS = 6;
|
|
94
|
+
const SHORT_RUN_UNITS = 6;
|
|
95
|
+
const FRAGMENT_RUN_UNITS = 11;
|
|
96
|
+
const MAX_ENCODED_LEVELS = 3;
|
|
97
|
+
// Checked encoded content this long is remembered for the request, so a later pass over the
|
|
98
|
+
// same bytes under the same set is not decoded and scanned again.
|
|
99
|
+
const MEMO_UNITS = 4_096;
|
|
48
100
|
// Work for one redaction call, across parsing and every scanned view. Credential-only sets
|
|
49
101
|
// keep the multi-MiB document and skill-file reads they always allowed.
|
|
50
102
|
const PRIVATE_SCAN_WORK = 64 * 1_048_576;
|
|
51
103
|
const CREDENTIAL_SCAN_WORK = 1_024 * 1_048_576;
|
|
52
|
-
// Connecta and
|
|
53
|
-
// so a submitted value such as "text" or
|
|
104
|
+
// Connecta, MCP and JSON-RPC framing: these names and tag values are fixed words, copied
|
|
105
|
+
// unchanged, so a submitted value such as "text" or one holding the word "resource" cannot
|
|
106
|
+
// rename an envelope member or rewrite a content type. They carry no downstream-chosen content
|
|
107
|
+
// beyond the word itself. Any other key, and every other value, is checked like text.
|
|
54
108
|
const FRAMING_KEYS = new Set(("content structuredContent isError _meta resultType dev.connecta/format type text data mimeType resource uri " +
|
|
55
109
|
"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"
|
|
110
|
+
"nextAction uncertainCall result logs failure inputRequests requestState method params mode requestedSchema url " +
|
|
111
|
+
"jsonrpc id tools resources resourceTemplates uriTemplate contents prompts messages role arguments nextCursor " +
|
|
112
|
+
"title description inputSchema outputSchema size writeOutcome").split(" "));
|
|
57
113
|
const TAGS = new Set(("type:text type:image type:audio type:resource type:resource_link format:json format:text format:paged " +
|
|
58
114
|
"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
|
-
|
|
61
|
-
//
|
|
62
|
-
const
|
|
115
|
+
"resultType:input_required mode:form mode:url method:elicitation/create jsonrpc:2.0 writeOutcome:unknown").split(" "));
|
|
116
|
+
// Outcome flags: a boolean under these names is kept, since a boolean carries one bit whatever
|
|
117
|
+
// produced it. A number or string under them is matched like any other.
|
|
118
|
+
const FACTS = new Set(["isError", "ok", "retryable", "truncated", "hasMore"]);
|
|
119
|
+
// An error's diagnostics. Its own accessors under these names, such as V8's stack, are read once
|
|
120
|
+
// into its copy, as is whatever its chain answers for them; any other own accessor is never
|
|
121
|
+
// invoked. Accessors its class chain defines are read once too.
|
|
122
|
+
const ERROR_ACCESSORS = new Set(["stack", "message", "cause", "code", "name"]);
|
|
123
|
+
// Parts the checker reads: text; a number's canonical text, withheld whole when it changes; a
|
|
124
|
+
// declared base64 field, also decoded whole and withheld as the encoded marker; and the bytes a
|
|
125
|
+
// typed array or ArrayBuffer holds, as byte-valued units, withheld whole when anything matches.
|
|
126
|
+
const TEXT = 0;
|
|
127
|
+
const NUMBER = 1;
|
|
128
|
+
const BINARY = 2;
|
|
129
|
+
const BYTES = 3;
|
|
130
|
+
/** An accessor met while looking up an inherited field: never invoked. */
|
|
131
|
+
const WITHHELD_ACCESSOR = Symbol("accessor");
|
|
132
|
+
/** The engine's own getters for what a typed array, DataView or ArrayBuffer holds, so no
|
|
133
|
+
* subclass answers for them, and the built-in element types by the name a typed array reports. */
|
|
134
|
+
const getter = (prototype, name) => Object.getOwnPropertyDescriptor(prototype, name).get;
|
|
135
|
+
const TYPED = Object.getPrototypeOf(Uint8Array.prototype);
|
|
136
|
+
const TYPED_NAME = getter(TYPED, Symbol.toStringTag);
|
|
137
|
+
const TYPED_BUFFER = getter(TYPED, "buffer");
|
|
138
|
+
const TYPED_OFFSET = getter(TYPED, "byteOffset");
|
|
139
|
+
const TYPED_LENGTH = getter(TYPED, "byteLength");
|
|
140
|
+
const VIEW_BUFFER = getter(DataView.prototype, "buffer");
|
|
141
|
+
const VIEW_OFFSET = getter(DataView.prototype, "byteOffset");
|
|
142
|
+
const VIEW_LENGTH = getter(DataView.prototype, "byteLength");
|
|
143
|
+
const BUFFER_LENGTH = getter(ArrayBuffer.prototype, "byteLength");
|
|
144
|
+
// Typed arrays whose elements are not their bytes; a Uint8Array's and a Uint8ClampedArray's are.
|
|
145
|
+
const ELEMENTS = new Map(("Int8Array Int16Array Uint16Array Int32Array Uint32Array Float16Array Float32Array Float64Array " +
|
|
146
|
+
"BigInt64Array BigUint64Array")
|
|
147
|
+
.split(" ")
|
|
148
|
+
.map((name) => [name, globalThis[name]])
|
|
149
|
+
.filter((entry) => typeof entry[1] === "function"));
|
|
150
|
+
const DECIMAL = Array.from({ length: 256 }, (_, byte) => String(byte));
|
|
151
|
+
/** Wrapper objects JSON reads as their primitive, read from the engine's slot. */
|
|
152
|
+
const BOXED = [
|
|
153
|
+
[String, String.prototype.valueOf],
|
|
154
|
+
[Number, Number.prototype.valueOf],
|
|
155
|
+
[Boolean, Boolean.prototype.valueOf],
|
|
156
|
+
];
|
|
63
157
|
/** Spend work that fits the call's remaining budget. Work that does not fit is refused
|
|
64
158
|
* without spending, so its field is withheld while smaller fields can still be checked. */
|
|
65
159
|
function spend(scan, work) {
|
|
@@ -68,15 +162,199 @@ function spend(scan, work) {
|
|
|
68
162
|
scan.budget.work -= work;
|
|
69
163
|
return true;
|
|
70
164
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
165
|
+
/** Base64 fields MCP and Connecta declare: resource blobs, image and audio data, content a
|
|
166
|
+
* provider marks `format: "base64"`, and a REST body's `base64` beside its `contentType`. They
|
|
167
|
+
* are checked as the bytes they carry. `field` reads the snapshot, never the original again. */
|
|
168
|
+
function binaryField(field, key) {
|
|
169
|
+
return ((key === "blob" && typeof field("uri") === "string") ||
|
|
170
|
+
(key === "data" && (field("type") === "image" || field("type") === "audio")) ||
|
|
171
|
+
(key === "content" && field("format") === "base64") ||
|
|
172
|
+
(key === "base64" && typeof field("contentType") === "string"));
|
|
173
|
+
}
|
|
174
|
+
function parse(text) {
|
|
175
|
+
try {
|
|
176
|
+
return { json: JSON.parse(text) };
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return undefined; // Prose rather than serialized JSON.
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/** A data property found on `item` or its prototypes, or undefined. An accessor on the way is
|
|
183
|
+
* never invoked: it reads as `accessor`. */
|
|
184
|
+
function inherited(item, name, accessor) {
|
|
185
|
+
for (let at = item; at; at = Object.getPrototypeOf(at)) {
|
|
186
|
+
const descriptor = Object.getOwnPropertyDescriptor(at, name);
|
|
187
|
+
if (descriptor)
|
|
188
|
+
return "value" in descriptor ? descriptor.value : accessor;
|
|
189
|
+
}
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
/** The bytes a typed array, DataView or ArrayBuffer holds, copied into a fresh built-in
|
|
193
|
+
* Uint8Array through the engine's slots, with the element type of a typed array whose elements
|
|
194
|
+
* are not those bytes. Undefined for any other value. */
|
|
195
|
+
function binaryOf(item) {
|
|
196
|
+
let held;
|
|
197
|
+
let elements;
|
|
198
|
+
if (ArrayBuffer.isView(item)) {
|
|
199
|
+
const name = TYPED_NAME.call(item);
|
|
200
|
+
const typed = name !== undefined;
|
|
201
|
+
held = new Uint8Array((typed ? TYPED_BUFFER : VIEW_BUFFER).call(item), (typed ? TYPED_OFFSET : VIEW_OFFSET).call(item), (typed ? TYPED_LENGTH : VIEW_LENGTH).call(item));
|
|
202
|
+
elements = ELEMENTS.get(name);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
if (!(item instanceof ArrayBuffer))
|
|
206
|
+
return undefined;
|
|
207
|
+
try {
|
|
208
|
+
BUFFER_LENGTH.call(item);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return undefined; // Its prototype, not an ArrayBuffer.
|
|
212
|
+
}
|
|
213
|
+
held = new Uint8Array(item);
|
|
214
|
+
}
|
|
215
|
+
const bytes = new Uint8Array(held.length);
|
|
216
|
+
bytes.set(held);
|
|
217
|
+
return { bytes, elements };
|
|
218
|
+
}
|
|
219
|
+
/** A String, Number or Boolean wrapper's primitive, as JSON reads it; undefined otherwise. */
|
|
220
|
+
function unboxed(item) {
|
|
221
|
+
for (const [type, valueOf] of BOXED) {
|
|
222
|
+
if (!(item instanceof type))
|
|
223
|
+
continue;
|
|
224
|
+
try {
|
|
225
|
+
return { value: valueOf.call(item) };
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return undefined; // Its prototype, not a wrapper.
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
/** Bytes as a string of byte-valued code units, in bounded batches. */
|
|
234
|
+
function latin1(bytes) {
|
|
235
|
+
const chunks = [];
|
|
236
|
+
for (let offset = 0; offset < bytes.length; offset += 0x8000)
|
|
237
|
+
chunks.push(String.fromCharCode(...bytes.subarray(offset, offset + 0x8000)));
|
|
238
|
+
return chunks.join("");
|
|
239
|
+
}
|
|
240
|
+
/** The UTF-8 text a string of byte-valued code units decodes to, invalid sequences replaced. */
|
|
241
|
+
function utf8(binary) {
|
|
242
|
+
const bytes = new Uint8Array(binary.length);
|
|
243
|
+
for (let index = 0; index < binary.length; index++)
|
|
244
|
+
bytes[index] = binary.charCodeAt(index);
|
|
245
|
+
return new TextDecoder("utf-8", { fatal: false, ignoreBOM: true }).decode(bytes);
|
|
246
|
+
}
|
|
247
|
+
/** Base64 or base64url text decoded to byte-valued code units, or undefined when it is not. */
|
|
248
|
+
function decodeBase64(text) {
|
|
249
|
+
let standard = text.includes("-") || text.includes("_") ? text.replace(/-/g, "+").replace(/_/g, "/") : text;
|
|
250
|
+
// An unpadded encoding cut one unit past a whole quad still decodes up to that quad.
|
|
251
|
+
let bare = standard.length;
|
|
252
|
+
while (bare > 0 && standard.charCodeAt(bare - 1) === 0x3d)
|
|
253
|
+
bare--;
|
|
254
|
+
if (bare % 4 === 1)
|
|
255
|
+
standard = standard.slice(0, bare - 1);
|
|
256
|
+
try {
|
|
257
|
+
return atob(standard);
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/** Base64 text without its trailing padding. */
|
|
264
|
+
function unpadded(text) {
|
|
265
|
+
let end = text.length;
|
|
266
|
+
while (end > 0 && text.charCodeAt(end - 1) === 0x3d)
|
|
267
|
+
end--;
|
|
268
|
+
return end === text.length ? text : text.slice(0, end);
|
|
269
|
+
}
|
|
270
|
+
const BASE64_UNIT = new Uint8Array(65_536);
|
|
271
|
+
for (const unit of "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/-_")
|
|
272
|
+
BASE64_UNIT[unit.charCodeAt(0)] = 1;
|
|
273
|
+
/** Base64 or base64url runs worth decoding, padding aside: the whole text when it is one, at
|
|
274
|
+
* least ENCODED_FIELD_UNITS long, or each run inside it at least `minimum` long. */
|
|
275
|
+
function encodedRuns(text, minimum) {
|
|
276
|
+
const runs = [];
|
|
277
|
+
for (let at = 0; at < text.length;) {
|
|
278
|
+
if (!BASE64_UNIT[text.charCodeAt(at)]) {
|
|
279
|
+
at++;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
let end = at + 1;
|
|
283
|
+
while (end < text.length && BASE64_UNIT[text.charCodeAt(end)])
|
|
284
|
+
end++;
|
|
285
|
+
const run = end - at;
|
|
286
|
+
let padded = end;
|
|
287
|
+
while (padded < text.length && padded - end < 2 && text.charCodeAt(padded) === 0x3d)
|
|
288
|
+
padded++;
|
|
289
|
+
if (at === 0 && padded === text.length ? run >= ENCODED_FIELD_UNITS : run >= minimum)
|
|
290
|
+
runs.push(text.slice(at, end));
|
|
291
|
+
at = padded;
|
|
292
|
+
}
|
|
293
|
+
return runs;
|
|
294
|
+
}
|
|
295
|
+
/** Percent escapes decoded to the byte-valued units they name, or undefined when nothing
|
|
296
|
+
* decodes. With `form`, a `+` is a space too, as form encoding writes one, with or without a
|
|
297
|
+
* percent escape beside it. */
|
|
298
|
+
function percentDecoded(text, form) {
|
|
299
|
+
let found = false;
|
|
300
|
+
const decoded = text.replace(form ? /%([0-9A-Fa-f]{2})|\+/g : /%([0-9A-Fa-f]{2})/g, (_, digits) => {
|
|
301
|
+
found = true;
|
|
302
|
+
return digits === undefined ? " " : String.fromCharCode(parseInt(digits, 16));
|
|
303
|
+
});
|
|
304
|
+
return found ? decoded : undefined;
|
|
305
|
+
}
|
|
306
|
+
function unescapeLayer(text) {
|
|
307
|
+
const parts = [];
|
|
308
|
+
const layer = { text: "", at: [], from: [], width: [] };
|
|
309
|
+
let [cursor, length] = [0, 0];
|
|
310
|
+
for (let index = text.indexOf("\\"); index >= 0 && index < text.length - 1; index = text.indexOf("\\", index)) {
|
|
311
|
+
const code = text.charCodeAt(index + 1);
|
|
312
|
+
let unit = shortEscape[code] ?? -1;
|
|
313
|
+
let width = 2;
|
|
314
|
+
if (unit < 0 && code === 0x75 && index + 6 <= text.length) {
|
|
315
|
+
unit = 0;
|
|
316
|
+
for (let offset = 2; offset < 6 && unit >= 0; offset++) {
|
|
317
|
+
const digit = hex(text.charCodeAt(index + offset));
|
|
318
|
+
unit = digit < 0 ? -1 : unit * 16 + digit;
|
|
319
|
+
}
|
|
320
|
+
width = 6;
|
|
321
|
+
}
|
|
322
|
+
if (unit < 0) {
|
|
323
|
+
index++;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
parts.push(text.slice(cursor, index), String.fromCharCode(unit));
|
|
327
|
+
length += index - cursor;
|
|
328
|
+
layer.at.push(length++);
|
|
329
|
+
layer.from.push(index);
|
|
330
|
+
layer.width.push(width);
|
|
331
|
+
cursor = index + width;
|
|
332
|
+
index = cursor;
|
|
333
|
+
}
|
|
334
|
+
if (!layer.at.length)
|
|
335
|
+
return undefined;
|
|
336
|
+
parts.push(text.slice(cursor));
|
|
337
|
+
layer.text = parts.join("");
|
|
338
|
+
return layer;
|
|
339
|
+
}
|
|
340
|
+
/** The unit a decoded layer's unit came from in the text above it: the first unit of its
|
|
341
|
+
* escape, or with `last` the escape's final unit. */
|
|
342
|
+
function above(layer, unit, last) {
|
|
343
|
+
const { at, from, width } = layer;
|
|
344
|
+
let [low, high] = [0, at.length];
|
|
345
|
+
while (low < high) {
|
|
346
|
+
const middle = (low + high) >>> 1;
|
|
347
|
+
if (at[middle] <= unit)
|
|
348
|
+
low = middle + 1;
|
|
349
|
+
else
|
|
350
|
+
high = middle;
|
|
351
|
+
}
|
|
352
|
+
const escape = low - 1;
|
|
353
|
+
if (escape < 0)
|
|
354
|
+
return unit;
|
|
355
|
+
if (at[escape] === unit)
|
|
356
|
+
return last ? from[escape] + width[escape] - 1 : from[escape];
|
|
357
|
+
return from[escape] + width[escape] + (unit - at[escape] - 1);
|
|
80
358
|
}
|
|
81
359
|
/** One payload-free warning per configured connector, regardless of requests. */
|
|
82
360
|
export function shortSecretWarning() {
|
|
@@ -186,6 +464,158 @@ class Unescaped {
|
|
|
186
464
|
return this.take(1, code);
|
|
187
465
|
}
|
|
188
466
|
}
|
|
467
|
+
/** A view that sets aside an incomplete JSON escape its text ends with, keeping source
|
|
468
|
+
* offsets, and records where it began. Wrapped around every decoded layer, so an escape that
|
|
469
|
+
* only a later layer reveals is set aside there too. Only the final six units can be one. */
|
|
470
|
+
class ClosedView {
|
|
471
|
+
from;
|
|
472
|
+
start = 0;
|
|
473
|
+
end = 0;
|
|
474
|
+
open = -1;
|
|
475
|
+
pending = [];
|
|
476
|
+
ended = false;
|
|
477
|
+
constructor(from) {
|
|
478
|
+
this.from = from;
|
|
479
|
+
}
|
|
480
|
+
next() {
|
|
481
|
+
while (!this.ended && this.pending.length < 7) {
|
|
482
|
+
const code = this.from.next();
|
|
483
|
+
if (code < 0) {
|
|
484
|
+
this.ended = true;
|
|
485
|
+
const tail = String.fromCharCode(...this.pending.map((unit) => unit.code));
|
|
486
|
+
const match = /\\(?:u[0-9a-fA-F]{0,3})?$/.exec(tail);
|
|
487
|
+
if (match) {
|
|
488
|
+
this.open = this.pending[match.index].start;
|
|
489
|
+
this.pending.length = match.index;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
else
|
|
493
|
+
this.pending.push({ code, start: this.from.start, end: this.from.end });
|
|
494
|
+
}
|
|
495
|
+
const unit = this.pending.shift();
|
|
496
|
+
if (!unit)
|
|
497
|
+
return -1;
|
|
498
|
+
this.start = unit.start;
|
|
499
|
+
this.end = unit.end;
|
|
500
|
+
return unit.code;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
function hexLetter(code) {
|
|
504
|
+
const lower = code | 0x20;
|
|
505
|
+
return lower >= 0x61 && lower <= 0x66;
|
|
506
|
+
}
|
|
507
|
+
/** Every K-unit run of every form of a value at least K long, as two independent 32-bit
|
|
508
|
+
* polynomial hashes in an open-addressed table: a match needs both to agree, so a false one is
|
|
509
|
+
* vanishingly rare and only ever redacts more. Index and scan are linear in the forms and the
|
|
510
|
+
* scanned text, whatever the number of values. A form's leading auth framing (`Bearer `,
|
|
511
|
+
* `token `) is public, so runs that start inside it are not indexed: a run holding K units of
|
|
512
|
+
* the value is indexed from the value's own forms. A run may start inside a percent escape
|
|
513
|
+
* whose `%` was cut away, or right after a `%` that is not the value's, so both cases of its
|
|
514
|
+
* first two hex letters are indexed. */
|
|
515
|
+
class Fragments {
|
|
516
|
+
complete = true;
|
|
517
|
+
size = 0;
|
|
518
|
+
firsts = new Int32Array(1_024);
|
|
519
|
+
seconds = new Int32Array(1_024);
|
|
520
|
+
shift = 22;
|
|
521
|
+
/** One bit per first hash's top 16 mixed bits: a clear bit rules a run out in one load, so
|
|
522
|
+
* only a run that might match computes its second hash and probes the table. */
|
|
523
|
+
filter = new Int32Array(2_048);
|
|
524
|
+
constructor(forms) {
|
|
525
|
+
for (const [form, frame] of forms) {
|
|
526
|
+
let first = 0;
|
|
527
|
+
let second = 0;
|
|
528
|
+
for (let index = 0; index < form.length; index++) {
|
|
529
|
+
const old = index >= FRAGMENT_LENGTH ? form.charCodeAt(index - FRAGMENT_LENGTH) : 0;
|
|
530
|
+
const code = form.charCodeAt(index);
|
|
531
|
+
first = (Math.imul(first - Math.imul(old, FIRST_TOP), HASH_BASE) + code) | 0;
|
|
532
|
+
second = (Math.imul(second - Math.imul(old, SECOND_TOP), SECOND_BASE) + code) | 0;
|
|
533
|
+
const at = index + 1 - FRAGMENT_LENGTH;
|
|
534
|
+
if (at < frame)
|
|
535
|
+
continue;
|
|
536
|
+
const [lead, next] = [form.charCodeAt(at), form.charCodeAt(at + 1)];
|
|
537
|
+
for (const a of hexLetter(lead) ? [0, (lead ^ 0x20) - lead] : [0])
|
|
538
|
+
for (const b of hexLetter(next) ? [0, (next ^ 0x20) - next] : [0])
|
|
539
|
+
this.insert((first + Math.imul(a, FIRST_TOP) + Math.imul(b, FIRST_NEXT)) | 0, (second + Math.imul(a, SECOND_TOP) + Math.imul(b, SECOND_NEXT)) | 0);
|
|
540
|
+
// Past capacity, every run of K units is withheld instead.
|
|
541
|
+
if (this.size > MAX_FRAGMENTS) {
|
|
542
|
+
[this.complete, this.size, this.firsts, this.seconds] = [false, 0, new Int32Array(1), new Int32Array(1)];
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
/** Add one fragment; repeated runs, as in a self-similar credential, are stored once. */
|
|
549
|
+
insert(first, second) {
|
|
550
|
+
const mixed = Math.imul(first, 0x9e_37_79_b1);
|
|
551
|
+
this.filter[mixed >>> 21] |= 1 << ((mixed >>> 16) & 31);
|
|
552
|
+
const key = second || 1;
|
|
553
|
+
const mask = this.firsts.length - 1;
|
|
554
|
+
for (let slot = Math.imul(first, 0x9e_37_79_b1) >>> this.shift;; slot = (slot + 1) & mask) {
|
|
555
|
+
if (!this.seconds[slot]) {
|
|
556
|
+
this.firsts[slot] = first;
|
|
557
|
+
this.seconds[slot] = key;
|
|
558
|
+
// At most half full, so a probe ends quickly; past capacity the index is abandoned.
|
|
559
|
+
if (++this.size * 2 > this.firsts.length && this.size <= MAX_FRAGMENTS)
|
|
560
|
+
this.grow();
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
if (this.firsts[slot] === first && this.seconds[slot] === key)
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
grow() {
|
|
568
|
+
const [firsts, seconds] = [this.firsts, this.seconds];
|
|
569
|
+
[this.firsts, this.seconds, this.shift, this.size] = [
|
|
570
|
+
new Int32Array(firsts.length * 2),
|
|
571
|
+
new Int32Array(firsts.length * 2),
|
|
572
|
+
this.shift - 1,
|
|
573
|
+
0,
|
|
574
|
+
];
|
|
575
|
+
for (let slot = 0; slot < firsts.length; slot++)
|
|
576
|
+
if (seconds[slot])
|
|
577
|
+
this.insert(firsts[slot], seconds[slot]);
|
|
578
|
+
}
|
|
579
|
+
has(first, second) {
|
|
580
|
+
const key = second || 1;
|
|
581
|
+
const mask = this.firsts.length - 1;
|
|
582
|
+
for (let slot = Math.imul(first, 0x9e_37_79_b1) >>> this.shift; this.seconds[slot]; slot = (slot + 1) & mask)
|
|
583
|
+
if (this.firsts[slot] === first && this.seconds[slot] === key)
|
|
584
|
+
return true;
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
/** Scan text from `from` to `to`, percent hex folded as forms are, reporting each run's span. */
|
|
588
|
+
scan(text, from, to, spans, marks) {
|
|
589
|
+
if (!this.size)
|
|
590
|
+
return;
|
|
591
|
+
const codes = new Uint16Array(FRAGMENT_LENGTH);
|
|
592
|
+
const { filter } = this;
|
|
593
|
+
const base = spans.length;
|
|
594
|
+
let [first, percent, slot] = [0, 0, 0];
|
|
595
|
+
for (let at = from; at < to; at++) {
|
|
596
|
+
let code = text.charCodeAt(at);
|
|
597
|
+
if (code === 0x25)
|
|
598
|
+
percent = 2;
|
|
599
|
+
else if (percent) {
|
|
600
|
+
percent--;
|
|
601
|
+
if (code >= 0x61 && code <= 0x66)
|
|
602
|
+
code -= 0x20;
|
|
603
|
+
}
|
|
604
|
+
first = (Math.imul(first - Math.imul(codes[slot], FIRST_TOP), HASH_BASE) + code) | 0;
|
|
605
|
+
codes[slot] = code;
|
|
606
|
+
slot = (slot + 1) & (FRAGMENT_LENGTH - 1);
|
|
607
|
+
const mixed = Math.imul(first, 0x9e_37_79_b1);
|
|
608
|
+
if ((filter[mixed >>> 21] & (1 << ((mixed >>> 16) & 31))) === 0 || at + 1 - from < FRAGMENT_LENGTH)
|
|
609
|
+
continue;
|
|
610
|
+
// The window's units, oldest first, are the ring from the next slot round.
|
|
611
|
+
let second = 0;
|
|
612
|
+
for (let unit = 0; unit < FRAGMENT_LENGTH; unit++)
|
|
613
|
+
second = (Math.imul(second, SECOND_BASE) + codes[(slot + unit) & (FRAGMENT_LENGTH - 1)]) | 0;
|
|
614
|
+
if (this.has(first, second))
|
|
615
|
+
push(spans, base, at + 1 - FRAGMENT_LENGTH, at + 1, marks);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
189
619
|
/** Merge one view's spans as they arrive, ends ascending. A span wholly inside an emitted
|
|
190
620
|
* placeholder is that placeholder; a private value containing its text spans more. */
|
|
191
621
|
function push(spans, base, start, end, marks) {
|
|
@@ -217,23 +647,144 @@ function markers(text) {
|
|
|
217
647
|
marks.push(at);
|
|
218
648
|
return marks;
|
|
219
649
|
}
|
|
650
|
+
/** Spans from every view and scanner, ordered and merged. */
|
|
651
|
+
function merged(spans) {
|
|
652
|
+
const order = Array.from({ length: spans.length / 2 }, (_, index) => index * 2).sort((a, b) => spans[a] - spans[b]);
|
|
653
|
+
const out = [];
|
|
654
|
+
for (const at of order) {
|
|
655
|
+
if (out.length && out[out.length - 1] >= spans[at])
|
|
656
|
+
out[out.length - 1] = Math.max(out[out.length - 1], spans[at + 1]);
|
|
657
|
+
else
|
|
658
|
+
out.push(spans[at], spans[at + 1]);
|
|
659
|
+
}
|
|
660
|
+
return out;
|
|
661
|
+
}
|
|
220
662
|
/** Replace each merged span once, from the original text: no replacement can split another
|
|
221
663
|
* view's match. Header-line scrubbing belongs to an actual echo. */
|
|
222
664
|
function replace(text, spans) {
|
|
223
|
-
const
|
|
665
|
+
const ranges = merged(headerValues(text, spans));
|
|
224
666
|
const parts = [];
|
|
225
667
|
let cursor = 0;
|
|
226
|
-
for (let index = 0; index <
|
|
227
|
-
|
|
228
|
-
|
|
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;
|
|
668
|
+
for (let index = 0; index < ranges.length; index += 2) {
|
|
669
|
+
parts.push(text.slice(cursor, ranges[index]), REDACTED);
|
|
670
|
+
cursor = ranges[index + 1];
|
|
233
671
|
}
|
|
234
672
|
parts.push(text.slice(cursor));
|
|
235
673
|
return parts.join("").replace(headerLine, `$1$2${REDACTED}`);
|
|
236
674
|
}
|
|
675
|
+
/** A unit of a header line's name, as `headerLine` reads one. */
|
|
676
|
+
function headerNameUnit(code) {
|
|
677
|
+
return ((code >= 0x61 && code <= 0x7a) || (code >= 0x41 && code <= 0x5a) || (code >= 0x30 && code <= 0x39) || code === 0x2d);
|
|
678
|
+
}
|
|
679
|
+
/** Whitespace as `headerLine`'s `\s` reads it around a header's `:`. */
|
|
680
|
+
function headerSpace(text, at) {
|
|
681
|
+
return /\s/.test(text[at]);
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* The echo's spans with the value of every sensitive header line (`headerLine`'s names) added,
|
|
685
|
+
* read on the text before the echo is replaced (#703). An echoed private value can take part of a
|
|
686
|
+
* header's name, its `:`, or a credential prefix such as `Bearer `, so the line cannot be read
|
|
687
|
+
* after replacement; read here, the whole value goes, every cookie pair and any short tail with
|
|
688
|
+
* it. A line that is not a sensitive header keeps its text. One forward pass; nothing is kept.
|
|
689
|
+
*/
|
|
690
|
+
function headerValues(text, spans) {
|
|
691
|
+
const out = [...spans];
|
|
692
|
+
for (let line = 0; line < text.length;) {
|
|
693
|
+
let at = line;
|
|
694
|
+
while (at < text.length && (text[at] === " " || text[at] === "\t"))
|
|
695
|
+
at++;
|
|
696
|
+
const name = at;
|
|
697
|
+
while (at < text.length && headerNameUnit(text.charCodeAt(at)))
|
|
698
|
+
at++;
|
|
699
|
+
let next = at;
|
|
700
|
+
while (next < text.length && text[next] !== "\n" && text[next] !== "\r")
|
|
701
|
+
next++;
|
|
702
|
+
if (at > name) {
|
|
703
|
+
const word = text.slice(name, at).toLowerCase();
|
|
704
|
+
if (word === "cookie" || word === "set-cookie" || /key|token|secret|auth|signature|session/.test(word)) {
|
|
705
|
+
let colon = at;
|
|
706
|
+
while (colon < text.length && headerSpace(text, colon))
|
|
707
|
+
colon++;
|
|
708
|
+
if (text[colon] === ":") {
|
|
709
|
+
let value = colon + 1;
|
|
710
|
+
while (value < text.length && headerSpace(text, value))
|
|
711
|
+
value++;
|
|
712
|
+
let end = value;
|
|
713
|
+
while (end < text.length && text[end] !== "\n" && text[end] !== "\r")
|
|
714
|
+
end++;
|
|
715
|
+
if (end > value)
|
|
716
|
+
out.push(value, end);
|
|
717
|
+
next = Math.max(next, end);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
line = next + 1;
|
|
722
|
+
}
|
|
723
|
+
return out;
|
|
724
|
+
}
|
|
725
|
+
/** Spans of the joined pieces, replaced in each piece they touch: a match across pieces leaves
|
|
726
|
+
* every other piece, and the structure around them, intact. */
|
|
727
|
+
function replaceParts(parts, spans) {
|
|
728
|
+
if (!spans.length)
|
|
729
|
+
return parts;
|
|
730
|
+
const ranges = merged(spans);
|
|
731
|
+
let offset = 0;
|
|
732
|
+
let at = 0;
|
|
733
|
+
return parts.map((part) => {
|
|
734
|
+
const end = offset + part.length;
|
|
735
|
+
while (at < ranges.length && ranges[at + 1] <= offset)
|
|
736
|
+
at += 2;
|
|
737
|
+
const local = [];
|
|
738
|
+
for (let index = at; part && index < ranges.length && ranges[index] < end; index += 2)
|
|
739
|
+
local.push(Math.max(0, ranges[index] - offset), Math.min(part.length, ranges[index + 1] - offset));
|
|
740
|
+
offset = end;
|
|
741
|
+
return local.length ? replace(part, local) : part;
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
/** Spans of serialized JSON's leaf tokens, in order: each string's contents and each number. */
|
|
745
|
+
function jsonTokens(text) {
|
|
746
|
+
const tokens = [];
|
|
747
|
+
for (let at = 0; at < text.length; at++) {
|
|
748
|
+
const code = text.charCodeAt(at);
|
|
749
|
+
if (code === 0x22) {
|
|
750
|
+
const start = at + 1;
|
|
751
|
+
for (at = start; at < text.length && text.charCodeAt(at) !== 0x22; at++)
|
|
752
|
+
if (text.charCodeAt(at) === 0x5c)
|
|
753
|
+
at++;
|
|
754
|
+
tokens.push(start, at);
|
|
755
|
+
}
|
|
756
|
+
else if (code === 0x2d || (code >= 0x30 && code <= 0x39)) {
|
|
757
|
+
const start = at;
|
|
758
|
+
while (at + 1 < text.length && /[-+.0-9eE]/.test(text[at + 1]))
|
|
759
|
+
at++;
|
|
760
|
+
tokens.push(start, at + 1);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
return tokens;
|
|
764
|
+
}
|
|
765
|
+
/** Pieces this set cannot fully check: withhold each one at least `limit` long, and every run
|
|
766
|
+
* of adjacent shorter pieces that together reach it. */
|
|
767
|
+
function withholdRuns(parts, safe, limit) {
|
|
768
|
+
const out = [...safe];
|
|
769
|
+
let [start, length] = [0, 0];
|
|
770
|
+
const close = (end) => {
|
|
771
|
+
if (length >= limit)
|
|
772
|
+
for (let index = start; index < end; index++)
|
|
773
|
+
if (parts[index])
|
|
774
|
+
out[index] = REDACTED;
|
|
775
|
+
};
|
|
776
|
+
for (let index = 0; index < parts.length; index++) {
|
|
777
|
+
if (parts[index].length < limit) {
|
|
778
|
+
length += parts[index].length;
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
close(index);
|
|
782
|
+
out[index] = REDACTED;
|
|
783
|
+
[start, length] = [index + 1, 0];
|
|
784
|
+
}
|
|
785
|
+
close(parts.length);
|
|
786
|
+
return out;
|
|
787
|
+
}
|
|
237
788
|
/** A run of one occurrence, whose step is set by the next occurrence that extends it. */
|
|
238
789
|
const UNEXTENDED = { step: 0, stride: 0, stepHash: 0, stepPower: 0 };
|
|
239
790
|
/** Runs ordered by their head check's due count: a binary min-heap. */
|
|
@@ -283,6 +834,8 @@ function hash(text) {
|
|
|
283
834
|
class Matcher {
|
|
284
835
|
/** The shortest form left unindexed; any string this long might contain it. */
|
|
285
836
|
unindexed = Infinity;
|
|
837
|
+
/** The longest indexed form, whole: a match is never wider. */
|
|
838
|
+
widest = 0;
|
|
286
839
|
root = new Int32Array(65_536);
|
|
287
840
|
branches = new Map();
|
|
288
841
|
child;
|
|
@@ -293,10 +846,10 @@ class Matcher {
|
|
|
293
846
|
truncated = new Map();
|
|
294
847
|
fail;
|
|
295
848
|
longest;
|
|
296
|
-
short;
|
|
297
849
|
starts;
|
|
298
850
|
hashes;
|
|
299
|
-
|
|
851
|
+
/** Forms of at least `spanKind` report spans; weaker ones only steer the open-tail search. */
|
|
852
|
+
constructor(forms, spanKind = PRIVATE) {
|
|
300
853
|
// Credentials are indexed first; sorting is stable, so registration order is otherwise kept.
|
|
301
854
|
const indexed = [];
|
|
302
855
|
let units = 0;
|
|
@@ -307,12 +860,13 @@ class Matcher {
|
|
|
307
860
|
else {
|
|
308
861
|
units += length;
|
|
309
862
|
indexed.push(entry);
|
|
863
|
+
this.widest = Math.max(this.widest, entry[0].length);
|
|
310
864
|
}
|
|
311
865
|
}
|
|
312
866
|
const size = units + 1;
|
|
313
867
|
[this.child, this.fail] = [new Int32Array(size), new Int32Array(size)];
|
|
314
868
|
[this.unit, this.depth, this.longest] = [new Uint16Array(size), new Uint16Array(size), new Uint16Array(size)];
|
|
315
|
-
|
|
869
|
+
this.kind = new Uint8Array(size);
|
|
316
870
|
const parent = new Int32Array(size);
|
|
317
871
|
let states = 1;
|
|
318
872
|
let longestForm = 0;
|
|
@@ -367,8 +921,7 @@ class Matcher {
|
|
|
367
921
|
const state = order[index];
|
|
368
922
|
const fail = this.depth[state] > 1 ? this.step(this.fail[parent[state]], this.unit[state]) : 0;
|
|
369
923
|
this.fail[state] = fail;
|
|
370
|
-
this.longest[state] = this.kind[state] >=
|
|
371
|
-
this.short[state] = this.kind[state] === SHORT ? 1 : this.short[fail];
|
|
924
|
+
this.longest[state] = this.kind[state] >= spanKind ? this.depth[state] : this.longest[fail];
|
|
372
925
|
}
|
|
373
926
|
this.starts = new Int32Array(1 << (32 - Math.clz32(longestForm)));
|
|
374
927
|
this.hashes = new Int32Array(this.starts.length);
|
|
@@ -390,7 +943,7 @@ class Matcher {
|
|
|
390
943
|
}
|
|
391
944
|
/** Scan one view, mapping each match back to the source units it came from. False when its
|
|
392
945
|
* long-form checks exceed the remaining work or MAX_PENDING_RUNS, so the view is withheld. */
|
|
393
|
-
scan(
|
|
946
|
+
scan(text, from, to, found, budget, marks) {
|
|
394
947
|
let state = 0;
|
|
395
948
|
let count = 0;
|
|
396
949
|
let percent = 0;
|
|
@@ -399,7 +952,8 @@ class Matcher {
|
|
|
399
952
|
let tails;
|
|
400
953
|
const base = found.spans.length;
|
|
401
954
|
const mask = this.starts.length - 1;
|
|
402
|
-
for (let
|
|
955
|
+
for (let at = from; at < to; at++) {
|
|
956
|
+
let code = text.charCodeAt(at);
|
|
403
957
|
if (code === 0x25)
|
|
404
958
|
percent = 2;
|
|
405
959
|
else if (percent) {
|
|
@@ -407,15 +961,13 @@ class Matcher {
|
|
|
407
961
|
if (code >= 0x61 && code <= 0x66)
|
|
408
962
|
code -= 0x20;
|
|
409
963
|
}
|
|
410
|
-
this.starts[count & mask] =
|
|
964
|
+
this.starts[count & mask] = at;
|
|
411
965
|
this.hashes[count++ & mask] = rolling;
|
|
412
966
|
rolling = (Math.imul(rolling, HASH_BASE) + code) | 0;
|
|
413
967
|
state = this.step(state, code);
|
|
414
968
|
const length = this.longest[state];
|
|
415
969
|
if (length)
|
|
416
|
-
push(found.spans, base, this.starts[(count - length) & mask],
|
|
417
|
-
if (this.short[state])
|
|
418
|
-
found.short = true;
|
|
970
|
+
push(found.spans, base, this.starts[(count - length) & mask], at + 1, marks);
|
|
419
971
|
const groups = this.depth[state] === FORM_PREFIX ? this.truncated.get(state) : undefined;
|
|
420
972
|
if (groups) {
|
|
421
973
|
if (CHECK_WORK * groups.length > budget.work)
|
|
@@ -440,7 +992,7 @@ class Matcher {
|
|
|
440
992
|
while (runs?.items.length && runs.items[0].due === count) {
|
|
441
993
|
const run = runs.items[0];
|
|
442
994
|
if (run.group.hashes.has((rolling - Math.imul(run.before, run.group.power)) | 0))
|
|
443
|
-
push(found.spans, base, run.start,
|
|
995
|
+
push(found.spans, base, run.start, at + 1, marks);
|
|
444
996
|
run.count--;
|
|
445
997
|
run.due += run.step;
|
|
446
998
|
run.before = (Math.imul(run.before, run.stepPower) + run.stepHash) | 0;
|
|
@@ -448,10 +1000,44 @@ class Matcher {
|
|
|
448
1000
|
runs.settle();
|
|
449
1001
|
}
|
|
450
1002
|
}
|
|
451
|
-
if (this.kind[state] === SHORT && this.depth[state] === count)
|
|
452
|
-
found.exact = true;
|
|
453
1003
|
return true;
|
|
454
1004
|
}
|
|
1005
|
+
/** The source start of the earliest suffix of a view that a form begins with but the view
|
|
1006
|
+
* ends inside: any nonempty prefix of an indexed form, or a long form whose
|
|
1007
|
+
* indexed prefix matched and whose length runs past the view. -1 when there is none. */
|
|
1008
|
+
openAt(units) {
|
|
1009
|
+
let state = 0;
|
|
1010
|
+
let count = 0;
|
|
1011
|
+
let percent = 0;
|
|
1012
|
+
const mask = this.starts.length - 1;
|
|
1013
|
+
// Long-form occurrences, as source start and the count by which the form would end.
|
|
1014
|
+
const longs = [];
|
|
1015
|
+
for (let code = units.next(); code >= 0; code = units.next()) {
|
|
1016
|
+
if (code === 0x25)
|
|
1017
|
+
percent = 2;
|
|
1018
|
+
else if (percent) {
|
|
1019
|
+
percent--;
|
|
1020
|
+
if (code >= 0x61 && code <= 0x66)
|
|
1021
|
+
code -= 0x20;
|
|
1022
|
+
}
|
|
1023
|
+
this.starts[count++ & mask] = units.start;
|
|
1024
|
+
state = this.step(state, code);
|
|
1025
|
+
const groups = this.depth[state] === FORM_PREFIX ? this.truncated.get(state) : undefined;
|
|
1026
|
+
if (groups && longs.length < 2 * MAX_PENDING_RUNS) {
|
|
1027
|
+
const length = Math.max(...groups.map((group) => group.length));
|
|
1028
|
+
longs.push(this.starts[(count - FORM_PREFIX) & mask], count - FORM_PREFIX + length);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
const depth = this.depth[state];
|
|
1032
|
+
let open = depth > 0 ? this.starts[(count - depth) & mask] : -1;
|
|
1033
|
+
for (let index = 0; index < longs.length; index += 2) {
|
|
1034
|
+
if (longs[index + 1] > count || longs.length >= 2 * MAX_PENDING_RUNS) {
|
|
1035
|
+
open = open < 0 ? longs[index] : Math.min(open, longs[index]);
|
|
1036
|
+
break;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
return open;
|
|
1040
|
+
}
|
|
455
1041
|
/** Add an occurrence to a run when it continues the run's step and stride. The prefix has
|
|
456
1042
|
* period `step`, so the text between occurrences is the prefix's first `step` units. */
|
|
457
1043
|
extend(run, count, start) {
|
|
@@ -474,51 +1060,113 @@ class Matcher {
|
|
|
474
1060
|
return true;
|
|
475
1061
|
}
|
|
476
1062
|
}
|
|
1063
|
+
/** Spans of a decoded layer's units, in the source text. */
|
|
1064
|
+
function toSource(layers, unit, last) {
|
|
1065
|
+
for (let index = layers.length - 1; index >= 0; index--)
|
|
1066
|
+
unit = above(layers[index], unit, last);
|
|
1067
|
+
return unit;
|
|
1068
|
+
}
|
|
1069
|
+
/** The ranges of a decoded layer to scan: `reach` units on each side of every escape it decoded,
|
|
1070
|
+
* merged, or the whole layer when that is no larger. */
|
|
1071
|
+
function windows(layer, reach) {
|
|
1072
|
+
const length = layer.text.length;
|
|
1073
|
+
if (reach > MAX_WINDOW_UNITS)
|
|
1074
|
+
return [0, length];
|
|
1075
|
+
const ranges = [];
|
|
1076
|
+
let units = 0;
|
|
1077
|
+
for (const unit of layer.at) {
|
|
1078
|
+
const [start, end] = [Math.max(0, unit - reach + 1), Math.min(length, unit + reach)];
|
|
1079
|
+
if (ranges.length && ranges[ranges.length - 1] >= start) {
|
|
1080
|
+
units += end - ranges[ranges.length - 1];
|
|
1081
|
+
ranges[ranges.length - 1] = end;
|
|
1082
|
+
}
|
|
1083
|
+
else {
|
|
1084
|
+
units += end - start;
|
|
1085
|
+
ranges.push(start, end);
|
|
1086
|
+
}
|
|
1087
|
+
if (units >= length)
|
|
1088
|
+
return [0, length];
|
|
1089
|
+
}
|
|
1090
|
+
return ranges;
|
|
1091
|
+
}
|
|
477
1092
|
export class SentSecrets {
|
|
478
1093
|
/** Normalized form to its strongest kind. */
|
|
479
1094
|
forms = new Map();
|
|
1095
|
+
/** A form's leading units that encode only public auth framing; absent is none. */
|
|
1096
|
+
frames = new Map();
|
|
480
1097
|
recipients = new Set();
|
|
1098
|
+
mintedStates = new Set();
|
|
1099
|
+
/** Encoded content this set already found clean, and whether under payload rules, which
|
|
1100
|
+
* cover envelope ones. Cleared whenever the set changes. */
|
|
1101
|
+
memo = new Map();
|
|
481
1102
|
privateChars = 0;
|
|
1103
|
+
/** The longest form; whether any comes from a value shorter than K; whether any is not ASCII. */
|
|
1104
|
+
widestForm = 0;
|
|
1105
|
+
smallForms = false;
|
|
1106
|
+
unicodeForms = false;
|
|
482
1107
|
/** The shortest form left unregistered; any string this long might contain it. */
|
|
483
1108
|
unmatched = Infinity;
|
|
484
|
-
unicode = false;
|
|
485
1109
|
views = {};
|
|
486
|
-
store(form, kind) {
|
|
1110
|
+
store(form, kind, frame = 0) {
|
|
487
1111
|
const prior = this.forms.get(form) ?? 0;
|
|
488
|
-
|
|
1112
|
+
const priorFrame = prior ? (this.frames.get(form) ?? 0) : Infinity;
|
|
1113
|
+
const nextFrame = Math.min(frame, priorFrame);
|
|
1114
|
+
if (prior >= kind && nextFrame === priorFrame)
|
|
489
1115
|
return;
|
|
490
1116
|
if (kind !== CREDENTIAL && !prior) {
|
|
491
1117
|
if (this.privateChars + form.length > MAX_PRIVATE_FORM_CHARS)
|
|
492
1118
|
return this.unregistered(form.length);
|
|
493
1119
|
this.privateChars += form.length;
|
|
494
1120
|
}
|
|
495
|
-
this.forms.set(form, kind);
|
|
496
|
-
|
|
1121
|
+
this.forms.set(form, Math.max(prior, kind));
|
|
1122
|
+
if (nextFrame)
|
|
1123
|
+
this.frames.set(form, nextFrame);
|
|
1124
|
+
else
|
|
1125
|
+
this.frames.delete(form);
|
|
1126
|
+
this.widestForm = Math.max(this.widestForm, form.length);
|
|
1127
|
+
this.smallForms ||= kind < PRIVATE;
|
|
1128
|
+
this.unicodeForms ||= /[\u0080-\uffff]/.test(form);
|
|
497
1129
|
this.views = {};
|
|
1130
|
+
this.memo = new Map();
|
|
498
1131
|
for (const recipient of this.recipients)
|
|
499
|
-
recipient.store(form, kind);
|
|
1132
|
+
recipient.store(form, kind, frame);
|
|
500
1133
|
}
|
|
501
1134
|
unregistered(length) {
|
|
502
1135
|
if (length >= this.unmatched)
|
|
503
1136
|
return;
|
|
504
1137
|
this.unmatched = length;
|
|
505
1138
|
this.views = {};
|
|
1139
|
+
this.memo = new Map();
|
|
506
1140
|
for (const recipient of this.recipients)
|
|
507
1141
|
recipient.unregistered(length);
|
|
508
1142
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
1143
|
+
/** Every form of `value`. Its first `frame` units are public auth framing, which no form's
|
|
1144
|
+
* fragment index starts in: a base64 form's framing ends at its last unit encoding only it. */
|
|
1145
|
+
register(value, kind, frame = 0) {
|
|
1146
|
+
const encodings = (text) => {
|
|
1147
|
+
const forms = [text, new URLSearchParams({ value: text }).toString().slice(6)];
|
|
1148
|
+
try {
|
|
1149
|
+
forms.push(encodeURIComponent(text), encodeURI(text));
|
|
1150
|
+
}
|
|
1151
|
+
catch {
|
|
1152
|
+
// A lone surrogate has no URI form; its other forms remain.
|
|
1153
|
+
}
|
|
1154
|
+
return forms;
|
|
1155
|
+
};
|
|
1156
|
+
const forms = encodings(value);
|
|
1157
|
+
const partial = frame > 0 && frame < Infinity;
|
|
1158
|
+
const framing = partial ? encodings(value.slice(0, frame)).map((form) => form.length) : [];
|
|
517
1159
|
const encoded = base64(value);
|
|
518
|
-
|
|
519
|
-
for (const form of
|
|
1160
|
+
const bytes = partial ? Math.floor((encoder.encode(value.slice(0, frame)).length * 4) / 3) : 0;
|
|
1161
|
+
for (const form of [encoded, encoded.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")]) {
|
|
1162
|
+
forms.push(form);
|
|
1163
|
+
framing.push(bytes);
|
|
1164
|
+
}
|
|
1165
|
+
forms.forEach((form, index) => {
|
|
1166
|
+
const framed = frame === Infinity ? frame : frame ? (framing[index] ?? 0) : 0;
|
|
520
1167
|
for (const variant of wireForms(form))
|
|
521
|
-
this.store(variant, kind);
|
|
1168
|
+
this.store(variant, kind, framed);
|
|
1169
|
+
});
|
|
522
1170
|
}
|
|
523
1171
|
/** The request receives existing and future secrets from every context. */
|
|
524
1172
|
include(source) {
|
|
@@ -526,19 +1174,27 @@ export class SentSecrets {
|
|
|
526
1174
|
return;
|
|
527
1175
|
source.recipients.add(this);
|
|
528
1176
|
for (const [form, kind] of source.forms)
|
|
529
|
-
this.store(form, kind);
|
|
1177
|
+
this.store(form, kind, source.frames.get(form) ?? 0);
|
|
530
1178
|
this.unregistered(source.unmatched);
|
|
531
1179
|
}
|
|
532
|
-
|
|
1180
|
+
/** A credential, as sent and in the auth framings a vendor may echo it with. Its first
|
|
1181
|
+
* `frame` units, such as a header's `Basic `, are framing too. */
|
|
1182
|
+
add(value, frame = 0) {
|
|
533
1183
|
if (value.length < MIN_SECRET_LENGTH)
|
|
534
1184
|
return;
|
|
535
|
-
|
|
536
|
-
|
|
1185
|
+
this.register(value, CREDENTIAL, frame);
|
|
1186
|
+
for (const framing of ["Bearer ", "token "])
|
|
1187
|
+
this.register(framing + value, CREDENTIAL, framing.length + frame);
|
|
537
1188
|
}
|
|
538
1189
|
/** Explicit secrets use the same floor as every other credential. */
|
|
539
1190
|
secret(value) {
|
|
540
1191
|
this.add(value);
|
|
541
1192
|
}
|
|
1193
|
+
/** An identifier sent beside a credential, such as a confidential client's ID or a Basic
|
|
1194
|
+
* username: matched whole only, since consent URLs and metadata carry its parts by design. */
|
|
1195
|
+
identifier(value) {
|
|
1196
|
+
this.add(value, Infinity);
|
|
1197
|
+
}
|
|
542
1198
|
/** Submitted writeOnly strings and numbers join this request's set, including property
|
|
543
1199
|
* names inside a private object. Empty strings, booleans, and null carry no text. */
|
|
544
1200
|
arguments(args, schema) {
|
|
@@ -550,7 +1206,7 @@ export class SentSecrets {
|
|
|
550
1206
|
return;
|
|
551
1207
|
if (value.length > MAX_PRIVATE_FORM_CHARS)
|
|
552
1208
|
return this.unregistered(value.length);
|
|
553
|
-
this.register(value, value.length < MIN_SECRET_LENGTH ? SHORT : PRIVATE);
|
|
1209
|
+
this.register(value, value.length < SUBSTRING_LENGTH ? TINY : value.length < MIN_SECRET_LENGTH ? SHORT : PRIVATE);
|
|
554
1210
|
};
|
|
555
1211
|
const collect = (value, depth) => {
|
|
556
1212
|
if (++nodes > MAX_ARGUMENT_NODES || depth > MAX_ARGUMENT_DEPTH)
|
|
@@ -570,37 +1226,42 @@ export class SentSecrets {
|
|
|
570
1226
|
};
|
|
571
1227
|
visitPrivateCallArguments(args, schema, (value) => collect(value, 0));
|
|
572
1228
|
}
|
|
573
|
-
/** Structural fields must be refused, never repaired into different URLs.
|
|
1229
|
+
/** Structural fields must be refused, never repaired into different URLs. A field the checker
|
|
1230
|
+
* would change under the boundary's rules is refused: one holding a whole form or any K-unit
|
|
1231
|
+
* run of one, in any view of the lattice. With `ignoreCase`, as for a host URL parsing
|
|
1232
|
+
* lowercased, the lowercased field is also checked against lowercased forms. */
|
|
574
1233
|
contains(value, ignoreCase = false) {
|
|
575
|
-
if (value.length >= this.unmatched)
|
|
1234
|
+
if (value.length >= Math.min(this.unmatched, this.unmatched < Infinity ? FRAGMENT_LENGTH : Infinity))
|
|
576
1235
|
return true;
|
|
577
1236
|
if (!this.forms.size)
|
|
578
1237
|
return false;
|
|
579
|
-
|
|
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)
|
|
1238
|
+
if (this.checked([value], [TEXT], this.scan(false))[0] !== value)
|
|
583
1239
|
return true;
|
|
584
|
-
|
|
585
|
-
return !scanned || found.spans.length > 0;
|
|
1240
|
+
return ignoreCase && this.folded().contains(value.toLowerCase());
|
|
586
1241
|
}
|
|
587
1242
|
containsUrl(value) {
|
|
588
1243
|
const { hosts, components, joined } = credentialUrlViews(value);
|
|
589
1244
|
return (hosts.some((host) => this.contains(host, true)) || [...components, ...joined].some((view) => this.contains(view)));
|
|
590
1245
|
}
|
|
591
1246
|
header(value) {
|
|
592
|
-
this.add(value);
|
|
593
1247
|
const framed = /^(?:Bearer|token|Basic)\s+(.+)$/i.exec(value);
|
|
1248
|
+
this.add(value, framed ? value.length - framed[1].length : 0);
|
|
594
1249
|
if (!framed)
|
|
595
1250
|
return;
|
|
596
1251
|
this.add(framed[1]);
|
|
597
1252
|
if (/^Basic\s/i.test(value)) {
|
|
598
1253
|
try {
|
|
599
1254
|
const decoded = atob(framed[1].replace(/-/g, "+").replace(/_/g, "/"));
|
|
600
|
-
this.add(decoded);
|
|
601
1255
|
const colon = decoded.indexOf(":");
|
|
1256
|
+
// Beside a password long enough to register, the username is an identifier, such as
|
|
1257
|
+
// a client ID; beside none or a placeholder, it is the credential itself.
|
|
1258
|
+
const paired = colon !== -1 && decoded.length - colon - 1 >= MIN_SECRET_LENGTH;
|
|
1259
|
+
this.add(decoded, paired ? colon + 1 : 0);
|
|
602
1260
|
if (colon !== -1) {
|
|
603
|
-
|
|
1261
|
+
if (paired)
|
|
1262
|
+
this.identifier(decoded.slice(0, colon));
|
|
1263
|
+
else
|
|
1264
|
+
this.add(decoded.slice(0, colon));
|
|
604
1265
|
this.secret(decoded.slice(colon + 1));
|
|
605
1266
|
}
|
|
606
1267
|
}
|
|
@@ -630,7 +1291,7 @@ export class SentSecrets {
|
|
|
630
1291
|
for (const [name, value] of url.searchParams) {
|
|
631
1292
|
if (explicitSecretName.test(name))
|
|
632
1293
|
this.secret(value);
|
|
633
|
-
else if (sensitiveName.test(name))
|
|
1294
|
+
else if (sensitiveName.test(name) && !continuationName.test(name))
|
|
634
1295
|
this.add(value);
|
|
635
1296
|
}
|
|
636
1297
|
// The OAuth SDK sends token requests as form data. Do not read a body
|
|
@@ -647,15 +1308,80 @@ export class SentSecrets {
|
|
|
647
1308
|
}
|
|
648
1309
|
/** Serialized wire text about to be returned, stored, or paged, under payload rules. */
|
|
649
1310
|
text(value) {
|
|
650
|
-
return this.idle() ? value : this.
|
|
1311
|
+
return this.idle() ? value : this.project(value, this.scan(true)).value;
|
|
651
1312
|
}
|
|
652
|
-
/**
|
|
653
|
-
*
|
|
654
|
-
*
|
|
1313
|
+
/** A plain-data snapshot of `value` with every match removed: own enumerable data properties
|
|
1314
|
+
* (an error's own fields, non-enumerable ones included), read once each. Accessors are never
|
|
1315
|
+
* invoked, except an error's stack and the accessors its class defines, read once into the
|
|
1316
|
+
* copy. The original object is never returned, so nothing read later can differ from what was
|
|
1317
|
+
* checked. Payload rules apply where downstream or guest data enters; the envelope pass at the
|
|
1318
|
+
* agent boundary repeats every check except short private values, which would rewrite
|
|
1319
|
+
* Connecta's own identifiers, counts and addresses. `values` applies payload rules to every
|
|
1320
|
+
* value of a parsed vendor body but keeps its object keys, which result safety and projections
|
|
1321
|
+
* match by name: a renamed key would hide a field from the rule that withholds it. JSON inside
|
|
1322
|
+
* a string is a value, so its keys are still checked. The connector's result and failures are
|
|
1323
|
+
* checked again under payload rules, keys included, at the `api()` boundary and the invocation. */
|
|
655
1324
|
redact(value, mode = "payload") {
|
|
656
1325
|
if (this.idle())
|
|
657
1326
|
return value;
|
|
658
|
-
return this.
|
|
1327
|
+
return this.project(value, this.scan(mode !== "envelope"), mode === "values").value;
|
|
1328
|
+
}
|
|
1329
|
+
/** Whether payload redaction would change anything in `value`, for fields that must be
|
|
1330
|
+
* refused rather than rewritten. */
|
|
1331
|
+
changes(value) {
|
|
1332
|
+
return !this.idle() && this.project(value, this.scan(true)).changed;
|
|
1333
|
+
}
|
|
1334
|
+
/** Relay state and links Connecta minted and authenticates are framing, not payload: they
|
|
1335
|
+
* hold sealed downstream state or Connecta's own facts, never downstream text or arguments,
|
|
1336
|
+
* and a coincidental match must not corrupt them. Never admit downstream-authored state
|
|
1337
|
+
* here. Exact value membership survives JSON serialization within this request. */
|
|
1338
|
+
mintedState(value) {
|
|
1339
|
+
this.mintedStates.add(value);
|
|
1340
|
+
return value;
|
|
1341
|
+
}
|
|
1342
|
+
/** Base64, or base64url with `url`, of bytes Connecta returns to an agent. No later pass
|
|
1343
|
+
* reads through an encoding the output does not declare, so the bytes are checked first: as
|
|
1344
|
+
* byte-valued units, as UTF-8 text, and through any base64 they carry, then the encoding
|
|
1345
|
+
* itself. Content holding a match encodes the placeholder instead. Downstream content takes
|
|
1346
|
+
* payload rules. `claims` is JSON Connecta assembled, such as a page cursor: its own names,
|
|
1347
|
+
* counts and fingerprints beside downstream values it already redacted under payload rules.
|
|
1348
|
+
* Every string in it is checked under payload rules, since each downstream value a cursor
|
|
1349
|
+
* holds is one, and the whole text and its encoding under the boundary's, so Connecta's keys
|
|
1350
|
+
* and counts never meet a short private value. Checked so, it is remembered as clean for later
|
|
1351
|
+
* passes over the same set. */
|
|
1352
|
+
encode(bytes, { url = false, claims = false } = {}) {
|
|
1353
|
+
const binary = latin1(bytes);
|
|
1354
|
+
const finish = (text) => {
|
|
1355
|
+
const encoded = btoa(text);
|
|
1356
|
+
return url ? encoded.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "") : encoded;
|
|
1357
|
+
};
|
|
1358
|
+
if (this.idle())
|
|
1359
|
+
return finish(binary);
|
|
1360
|
+
const encoded = finish(binary);
|
|
1361
|
+
const clean = claims
|
|
1362
|
+
? this.claimsClean(binary, encoded)
|
|
1363
|
+
: this.checked([encoded], [BINARY], this.scan(true))[0] === encoded;
|
|
1364
|
+
if (!clean)
|
|
1365
|
+
return finish(REDACTED);
|
|
1366
|
+
if (claims || encoded.length >= MEMO_UNITS)
|
|
1367
|
+
this.memo.set(encoded, true);
|
|
1368
|
+
return encoded;
|
|
1369
|
+
}
|
|
1370
|
+
/** Text pieces a provider reads as one lane, such as rich-text runs with metadata between
|
|
1371
|
+
* them: checked joined, each match replaced in every piece it touches. */
|
|
1372
|
+
parts(parts) {
|
|
1373
|
+
if (this.idle())
|
|
1374
|
+
return parts;
|
|
1375
|
+
return this.checked(parts, parts.map(() => TEXT), this.scan(true));
|
|
1376
|
+
}
|
|
1377
|
+
/** Complete and unfinished forms in bounded text, all found against its original bytes.
|
|
1378
|
+
* Replacing a complete match first could hide a longer open match containing it. Even a
|
|
1379
|
+
* one-unit shared prefix is withheld: a read boundary gives no evidence it is public text.
|
|
1380
|
+
* Each decoded layer sets aside a trailing escape the read cut short, which is withheld. */
|
|
1381
|
+
redactOpenTail(text) {
|
|
1382
|
+
if (this.idle())
|
|
1383
|
+
return text;
|
|
1384
|
+
return this.checked([text], [TEXT], this.scan(true), true)[0];
|
|
659
1385
|
}
|
|
660
1386
|
/** Credentials alone: later calls keep the private arguments they submit, and cached
|
|
661
1387
|
* catalogs never depend on one request's arguments. */
|
|
@@ -672,6 +1398,25 @@ export class SentSecrets {
|
|
|
672
1398
|
matcher() {
|
|
673
1399
|
return (this.views.matcher ??= new Matcher(this.forms));
|
|
674
1400
|
}
|
|
1401
|
+
/** Short private forms, matched as substrings of payload text. */
|
|
1402
|
+
shortMatcher() {
|
|
1403
|
+
if (this.views.short === undefined) {
|
|
1404
|
+
const forms = [...this.forms].filter(([, kind]) => kind === SHORT);
|
|
1405
|
+
this.views.short = forms.length ? new Matcher(forms, SHORT) : null;
|
|
1406
|
+
}
|
|
1407
|
+
return this.views.short;
|
|
1408
|
+
}
|
|
1409
|
+
/** Forms with no indexed fragment, such as identifiers, matched whole. */
|
|
1410
|
+
wholeMatcher() {
|
|
1411
|
+
if (this.views.whole === undefined) {
|
|
1412
|
+
const forms = [...this.forms].filter(([form, kind]) => kind >= PRIVATE && (this.frames.get(form) ?? 0) > form.length - FRAGMENT_LENGTH);
|
|
1413
|
+
this.views.whole = forms.length ? new Matcher(forms) : null;
|
|
1414
|
+
}
|
|
1415
|
+
return this.views.whole;
|
|
1416
|
+
}
|
|
1417
|
+
fragments() {
|
|
1418
|
+
return (this.views.fragments ??= new Fragments([...this.forms].filter(([, kind]) => kind >= PRIVATE).map(([form]) => [form, this.frames.get(form) ?? 0])));
|
|
1419
|
+
}
|
|
675
1420
|
credentials() {
|
|
676
1421
|
if (!this.privateChars && this.unmatched === Infinity)
|
|
677
1422
|
return this;
|
|
@@ -679,266 +1424,912 @@ export class SentSecrets {
|
|
|
679
1424
|
const view = new SentSecrets();
|
|
680
1425
|
for (const [form, kind] of this.forms)
|
|
681
1426
|
if (kind === CREDENTIAL)
|
|
682
|
-
view.store(form, kind);
|
|
1427
|
+
view.store(form, kind, this.frames.get(form) ?? 0);
|
|
683
1428
|
this.views.credentials = view;
|
|
684
1429
|
}
|
|
685
1430
|
return this.views.credentials;
|
|
686
1431
|
}
|
|
687
|
-
/**
|
|
688
|
-
|
|
689
|
-
|
|
1432
|
+
/** Every form lowercased, for fields such as hosts that reach output lowercased. */
|
|
1433
|
+
folded() {
|
|
1434
|
+
if (!this.views.folded) {
|
|
1435
|
+
const view = new SentSecrets();
|
|
1436
|
+
for (const [form, kind] of this.forms)
|
|
1437
|
+
for (const low of wireForms(form.toLowerCase()))
|
|
1438
|
+
view.store(low, kind, this.frames.get(form) ?? 0);
|
|
1439
|
+
this.views.folded = view;
|
|
1440
|
+
}
|
|
1441
|
+
return this.views.folded;
|
|
1442
|
+
}
|
|
1443
|
+
/** The shortest text that might hold something this set cannot check, which is withheld: a
|
|
1444
|
+
* K-unit run of a value too large to register (or a whole short one), or of a form past the
|
|
1445
|
+
* fragment index's or a matcher's capacity. Envelope scans rely on the payload scans for
|
|
1446
|
+
* unregistered values. */
|
|
1447
|
+
limit(scan) {
|
|
1448
|
+
let limit = this.fragments().complete ? Infinity : FRAGMENT_LENGTH;
|
|
1449
|
+
limit = Math.min(limit, this.wholeMatcher()?.unindexed ?? Infinity);
|
|
1450
|
+
if (scan.payload && this.unmatched < Infinity)
|
|
1451
|
+
limit = Math.min(limit, this.unmatched, FRAGMENT_LENGTH);
|
|
1452
|
+
if (scan.payload)
|
|
1453
|
+
limit = Math.min(limit, this.shortMatcher()?.unindexed ?? Infinity);
|
|
1454
|
+
return limit;
|
|
690
1455
|
}
|
|
691
|
-
/**
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
1456
|
+
/** Whether an open tail could begin a form this set never registered or indexed. */
|
|
1457
|
+
uncheckedTail() {
|
|
1458
|
+
return this.unmatched < Infinity || this.matcher().unindexed < Infinity;
|
|
1459
|
+
}
|
|
1460
|
+
/** A form whole: a leaf, key, number, boolean or decoded view equal to one, as it stands or
|
|
1461
|
+
* under JSON escapes. This is how a tiny private value is matched. */
|
|
1462
|
+
whole(text) {
|
|
1463
|
+
let view = text;
|
|
1464
|
+
for (let layer = 0; view !== undefined && layer <= MAX_DECODE_LEVELS; layer++) {
|
|
1465
|
+
if (view.length <= this.widestForm && this.forms.has(view.includes("%") ? wireForms(view)[0] : view))
|
|
1466
|
+
return true;
|
|
1467
|
+
// An escape is at most six units for one, so a longer text cannot decode to a form.
|
|
1468
|
+
view = view.length <= 6 * this.widestForm && view.includes("\\") ? unescapeLayer(view)?.text : undefined;
|
|
1469
|
+
}
|
|
1470
|
+
return false;
|
|
1471
|
+
}
|
|
1472
|
+
/** Every K-unit run of a registered form, every identifier form, and in payload text every
|
|
1473
|
+
* short private form, in the raw text and each JSON-unescaped layer, mapped to source spans
|
|
1474
|
+
* before any replacement. A layer decodes while the one above holds an escape; each is scanned
|
|
1475
|
+
* only around the escapes it decoded, since any other match was found a layer up. Work beyond
|
|
1476
|
+
* the call's budget returns undefined, which withholds the text. */
|
|
1477
|
+
spansOf(text, scan, short) {
|
|
1478
|
+
const spans = [];
|
|
1479
|
+
const fragments = this.fragments();
|
|
1480
|
+
const matchers = [this.wholeMatcher(), short ? this.shortMatcher() : null].filter((matcher) => matcher !== null);
|
|
1481
|
+
const scanners = Number(fragments.size > 0) + matchers.length;
|
|
1482
|
+
if (!scanners)
|
|
1483
|
+
return { spans, deep: false };
|
|
697
1484
|
const marks = markers(text);
|
|
698
|
-
|
|
699
|
-
|
|
1485
|
+
if (!spend(scan, 2 * text.length * scanners))
|
|
1486
|
+
return undefined;
|
|
1487
|
+
fragments.scan(text, 0, text.length, spans, marks);
|
|
1488
|
+
for (const matcher of matchers)
|
|
1489
|
+
if (!matcher.scan(text, 0, text.length, { spans }, scan.budget, marks))
|
|
700
1490
|
return undefined;
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
1491
|
+
const reach = Math.max(FRAGMENT_LENGTH, ...matchers.map((matcher) => matcher.widest));
|
|
1492
|
+
const layers = [];
|
|
1493
|
+
for (let upper = text; upper.includes("\\");) {
|
|
1494
|
+
// Decoding a layer copies text past its escapes, far cheaper per unit than scanning it.
|
|
1495
|
+
if (!spend(scan, Math.ceil(upper.length / 8)))
|
|
1496
|
+
return undefined;
|
|
1497
|
+
const layer = unescapeLayer(upper);
|
|
1498
|
+
if (!layer)
|
|
1499
|
+
break;
|
|
1500
|
+
if (layers.length === MAX_DECODE_LEVELS)
|
|
1501
|
+
return { spans, deep: true };
|
|
1502
|
+
layers.push(layer);
|
|
1503
|
+
const ranges = windows(layer, reach);
|
|
1504
|
+
let units = 0;
|
|
1505
|
+
for (let at = 0; at < ranges.length; at += 2)
|
|
1506
|
+
units += ranges[at + 1] - ranges[at];
|
|
1507
|
+
if (!spend(scan, 2 * units * scanners))
|
|
705
1508
|
return undefined;
|
|
706
|
-
|
|
1509
|
+
const local = [];
|
|
1510
|
+
for (let at = 0; at < ranges.length; at += 2) {
|
|
1511
|
+
const [start, end] = [ranges[at], ranges[at + 1]];
|
|
1512
|
+
fragments.scan(layer.text, start, end, local, undefined);
|
|
1513
|
+
for (const matcher of matchers)
|
|
1514
|
+
if (!matcher.scan(layer.text, start, end, { spans: local }, scan.budget))
|
|
1515
|
+
return undefined;
|
|
1516
|
+
}
|
|
1517
|
+
const ordered = merged(local);
|
|
1518
|
+
const base = spans.length;
|
|
1519
|
+
for (let at = 0; at < ordered.length; at += 2)
|
|
1520
|
+
push(spans, base, toSource(layers, ordered[at], false), toSource(layers, ordered[at + 1] - 1, true) + 1, marks);
|
|
1521
|
+
upper = layer.text;
|
|
1522
|
+
}
|
|
1523
|
+
// Auth framing is never indexed, but an echo carrying it goes whole, as a header line does.
|
|
1524
|
+
for (let at = 0; at < spans.length; at += 2) {
|
|
1525
|
+
const framing = /(?:Bearer|token)(?: |\+|%20)$/.exec(text.slice(Math.max(0, spans[at] - 9), spans[at]));
|
|
1526
|
+
if (framing)
|
|
1527
|
+
spans[at] -= framing[0].length;
|
|
707
1528
|
}
|
|
708
|
-
return
|
|
1529
|
+
return { spans, deep: false };
|
|
709
1530
|
}
|
|
710
|
-
/**
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
1531
|
+
/** Add the span of an unfinished form the text ends inside, at any decoded layer. */
|
|
1532
|
+
openSpans(text, spans) {
|
|
1533
|
+
let open = -1;
|
|
1534
|
+
for (let layer = 0, more = true; more; layer++) {
|
|
1535
|
+
if (layer > MAX_DECODE_LEVELS) {
|
|
1536
|
+
spans.push(0, text.length);
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
let units = new Source(text);
|
|
1540
|
+
const closed = [];
|
|
1541
|
+
let decoded;
|
|
1542
|
+
for (let depth = 0; depth <= layer; depth++) {
|
|
1543
|
+
const view = new ClosedView(units);
|
|
1544
|
+
closed.push(view);
|
|
1545
|
+
units = depth < layer ? (decoded = new Unescaped(view)) : view;
|
|
1546
|
+
}
|
|
1547
|
+
const at = this.matcher().openAt(units);
|
|
1548
|
+
for (const start of [at, ...closed.map((view) => view.open)])
|
|
1549
|
+
if (start >= 0)
|
|
1550
|
+
open = open < 0 ? start : Math.min(open, start);
|
|
1551
|
+
more = layer === 0 ? text.includes("\\") : decoded.decoded > 0 && decoded.slashes > 0;
|
|
1552
|
+
}
|
|
1553
|
+
if (open >= 0)
|
|
1554
|
+
spans.push(open, text.length);
|
|
719
1555
|
}
|
|
720
|
-
/**
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
*
|
|
730
|
-
*
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
1556
|
+
/** The one string checker. Every string that can reach an agent, an executor or storage is
|
|
1557
|
+
* checked here, under the whole lattice: projection leaves and numbers, object keys, declared
|
|
1558
|
+
* binary fields, the bytes of typed arrays and ArrayBuffers, cursor claims and every encoding
|
|
1559
|
+
* Connecta produces, error fields, provider lanes, bounded reads and structural fields.
|
|
1560
|
+
* `parts` is one lane: matched joined, raw and through its JSON layers, each match replaced in
|
|
1561
|
+
* every part it touches, a tiny private value withholding a part it equals, and every part
|
|
1562
|
+
* withheld that the set cannot fully check. Then every text and declared binary part is
|
|
1563
|
+
* decoded in each of its text views (see textViews), its base64 and base64url runs and a
|
|
1564
|
+
* declared field's whole content are decoded and checked as bytes, UTF-8 and nested base64,
|
|
1565
|
+
* and so are byte parts; a part a decoded view matches is withheld whole. Each decode is
|
|
1566
|
+
* charged to the call's budget, and a part it cannot pay for is withheld. A `bounded` lane is
|
|
1567
|
+
* text cut short, which also loses an unfinished tail. Nothing else matches a string that is
|
|
1568
|
+
* returned: the matching steps below are called only from here. */
|
|
1569
|
+
checked(parts, kinds, scan, bounded = false) {
|
|
1570
|
+
const safe = this.safeParts(parts, kinds, scan, bounded);
|
|
1571
|
+
this.encodedLeaves(parts, kinds, safe, scan);
|
|
1572
|
+
return safe;
|
|
1573
|
+
}
|
|
1574
|
+
/** Cursor claims: the whole text and its encoding under the boundary's rules, then every
|
|
1575
|
+
* string in the JSON under payload rules. Keys and numbers are Connecta's own names and
|
|
1576
|
+
* counts: a downstream value enters a cursor only as a string. */
|
|
1577
|
+
claimsClean(binary, encoded) {
|
|
1578
|
+
if (this.checked([encoded], [BINARY], this.scan(false))[0] !== encoded)
|
|
1579
|
+
return false;
|
|
1580
|
+
const text = utf8(binary);
|
|
1581
|
+
const parsed = parse(text);
|
|
1582
|
+
const strings = [];
|
|
1583
|
+
const collect = (value) => {
|
|
1584
|
+
if (typeof value === "string")
|
|
1585
|
+
strings.push(value);
|
|
1586
|
+
else if (value !== null && typeof value === "object")
|
|
1587
|
+
for (const item of Object.values(value))
|
|
1588
|
+
collect(item);
|
|
1589
|
+
};
|
|
1590
|
+
if (parsed)
|
|
1591
|
+
collect(parsed.json);
|
|
1592
|
+
else
|
|
1593
|
+
strings.push(text);
|
|
1594
|
+
const scan = this.scan(true);
|
|
1595
|
+
return strings.every((claim) => !this.project(claim, scan).changed);
|
|
1596
|
+
}
|
|
1597
|
+
/** The shortest base64 run inside a view that is decoded: long enough to carry the smallest
|
|
1598
|
+
* piece this scan matches (see SHORT_RUN_UNITS). */
|
|
1599
|
+
runUnits(scan) {
|
|
1600
|
+
return scan.payload && this.shortMatcher() ? SHORT_RUN_UNITS : FRAGMENT_RUN_UNITS;
|
|
1601
|
+
}
|
|
1602
|
+
/** The text views base64 is found in: the text; one layer of its JSON escapes decoded; its
|
|
1603
|
+
* percent escapes decoded; and its form decoding, a `+` read as a space with any percent
|
|
1604
|
+
* escapes decoded, whether or not it holds one. Two decodes chained, such as percent escapes
|
|
1605
|
+
* inside JSON escapes, are not a view. Undefined when the work budget cannot pay for them. */
|
|
1606
|
+
textViews(text, scan) {
|
|
1607
|
+
const escaped = text.includes("\\");
|
|
1608
|
+
const percent = text.includes("%");
|
|
1609
|
+
const plus = text.includes("+");
|
|
1610
|
+
const decodes = Number(escaped) + Number(percent) + Number(plus);
|
|
1611
|
+
if (!decodes)
|
|
1612
|
+
return [text];
|
|
1613
|
+
if (!spend(scan, decodes * Math.ceil(text.length / 8)))
|
|
1614
|
+
return undefined;
|
|
1615
|
+
const views = [text];
|
|
1616
|
+
for (const view of [
|
|
1617
|
+
escaped ? unescapeLayer(text)?.text : undefined,
|
|
1618
|
+
percent ? percentDecoded(text, false) : undefined,
|
|
1619
|
+
plus ? percentDecoded(text, true) : undefined,
|
|
1620
|
+
])
|
|
1621
|
+
if (view !== undefined && !views.includes(view))
|
|
1622
|
+
views.push(view);
|
|
1623
|
+
return views;
|
|
1624
|
+
}
|
|
1625
|
+
/** Text and declared binary parts carrying base64 or base64url, in any text view, the whole
|
|
1626
|
+
* view or a run inside it, and byte parts, whose decoded bytes or their UTF-8 text hold a
|
|
1627
|
+
* match are withheld whole: no later pass reads through an encoding the output does not
|
|
1628
|
+
* declare. A declared field is also decoded whole, as its reader decodes it. Every part's
|
|
1629
|
+
* decoded views are matched together, as one lane between unmatched separators. Each decode
|
|
1630
|
+
* is paid for from the call's work budget; a part the budget cannot check is withheld. */
|
|
1631
|
+
encodedLeaves(parts, kinds, safe, scan) {
|
|
1632
|
+
// A run decodes to a group of views, once per call however many parts repeat it, and so do
|
|
1633
|
+
// byte parts; each part's groups are recorded as (part, group) pairs. Group -1 is content
|
|
1634
|
+
// the work budget could not check, which withholds every part holding it.
|
|
1635
|
+
const views = [];
|
|
1636
|
+
const viewGroups = [];
|
|
1637
|
+
const groups = new Map();
|
|
1638
|
+
const byteGroups = new Map();
|
|
1639
|
+
const pairLeaves = [];
|
|
1640
|
+
const pairGroups = [];
|
|
1641
|
+
let size = 0;
|
|
1642
|
+
const minimum = this.runUnits(scan);
|
|
1643
|
+
/** Views of bytes: the bytes themselves unless the lane already matched them, their UTF-8
|
|
1644
|
+
* text when a form is not ASCII, and the base64 they carry. */
|
|
1645
|
+
const add = (binary, group, level, self = true) => {
|
|
1646
|
+
if (self) {
|
|
1647
|
+
views.push(binary);
|
|
1648
|
+
viewGroups.push(group);
|
|
738
1649
|
}
|
|
739
|
-
|
|
740
|
-
|
|
1650
|
+
if (this.unicodeForms && /[\x80-\xff]/.test(binary)) {
|
|
1651
|
+
if (!spend(scan, Math.ceil(binary.length / 8)))
|
|
1652
|
+
return false;
|
|
1653
|
+
views.push(utf8(binary));
|
|
1654
|
+
viewGroups.push(group);
|
|
1655
|
+
}
|
|
1656
|
+
if (level < MAX_ENCODED_LEVELS)
|
|
1657
|
+
for (const run of encodedRuns(binary, minimum)) {
|
|
1658
|
+
if (!spend(scan, Math.ceil(run.length / 8)))
|
|
1659
|
+
return false;
|
|
1660
|
+
const inner = decodeBase64(run);
|
|
1661
|
+
if (inner && !add(inner, group, level + 1))
|
|
1662
|
+
return false;
|
|
1663
|
+
}
|
|
1664
|
+
return true;
|
|
1665
|
+
};
|
|
1666
|
+
const groupOf = (run) => {
|
|
1667
|
+
let group = groups.get(run);
|
|
1668
|
+
if (group === undefined) {
|
|
1669
|
+
group = size++;
|
|
1670
|
+
const bytes = spend(scan, Math.ceil(run.length / 8)) ? decodeBase64(run) : null;
|
|
1671
|
+
if (bytes === null || (bytes && !add(bytes, group, 1)))
|
|
1672
|
+
group = -1;
|
|
1673
|
+
groups.set(run, group);
|
|
741
1674
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
1675
|
+
return group;
|
|
1676
|
+
};
|
|
1677
|
+
const bytesGroup = (bytes) => {
|
|
1678
|
+
let group = byteGroups.get(bytes);
|
|
1679
|
+
if (group === undefined) {
|
|
1680
|
+
group = size++;
|
|
1681
|
+
if (!add(bytes, group, 1, false))
|
|
1682
|
+
group = -1;
|
|
1683
|
+
byteGroups.set(bytes, group);
|
|
748
1684
|
}
|
|
1685
|
+
return group;
|
|
1686
|
+
};
|
|
1687
|
+
leaves: for (let index = 0; index < parts.length; index++) {
|
|
1688
|
+
// The part as text matching left it: a run it replaced in place, such as a registered base64
|
|
1689
|
+
// form, leaves no decodable remnant of K units; a match only decoding reveals withholds all.
|
|
1690
|
+
// Binary and bytes any match changed are withheld whole already.
|
|
1691
|
+
const kind = kinds[index];
|
|
1692
|
+
const text = safe[index];
|
|
1693
|
+
if (kind === NUMBER || text === REDACTED || this.memo.get(text) === true)
|
|
1694
|
+
continue;
|
|
1695
|
+
if ((!scan.payload && this.memo.has(text)) || (kind !== TEXT && text !== parts[index]))
|
|
1696
|
+
continue;
|
|
1697
|
+
const pair = (group) => {
|
|
1698
|
+
if (group < 0) {
|
|
1699
|
+
safe[index] = REDACTED;
|
|
1700
|
+
return false;
|
|
1701
|
+
}
|
|
1702
|
+
pairLeaves.push(index);
|
|
1703
|
+
pairGroups.push(group);
|
|
1704
|
+
return true;
|
|
1705
|
+
};
|
|
1706
|
+
if (kind === BYTES) {
|
|
1707
|
+
pair(bytesGroup(text));
|
|
1708
|
+
continue;
|
|
1709
|
+
}
|
|
1710
|
+
const textViews = this.textViews(text, scan);
|
|
1711
|
+
if (!textViews) {
|
|
1712
|
+
safe[index] = REDACTED;
|
|
1713
|
+
continue;
|
|
1714
|
+
}
|
|
1715
|
+
for (const view of textViews)
|
|
1716
|
+
for (const run of encodedRuns(view, minimum))
|
|
1717
|
+
if (!pair(groupOf(run)))
|
|
1718
|
+
continue leaves;
|
|
1719
|
+
if (kind === BINARY)
|
|
1720
|
+
pair(groupOf(unpadded(text)));
|
|
749
1721
|
}
|
|
750
|
-
|
|
1722
|
+
if (!views.length)
|
|
1723
|
+
return;
|
|
1724
|
+
const matched = new Uint8Array(size);
|
|
1725
|
+
const withhold = (at) => (matched[viewGroups[at]] = 1);
|
|
1726
|
+
try {
|
|
1727
|
+
if (scan.payload && this.smallForms)
|
|
1728
|
+
views.forEach((view, at) => this.whole(view) && withhold(at));
|
|
1729
|
+
const lane = views.join("");
|
|
1730
|
+
const matches = this.spansOf(lane, scan, scan.payload);
|
|
1731
|
+
if (!matches)
|
|
1732
|
+
return views.forEach((_, at) => withhold(at));
|
|
1733
|
+
if (matches.deep)
|
|
1734
|
+
views.forEach((view, at) => view.includes("\\") && withhold(at));
|
|
1735
|
+
const starts = [];
|
|
1736
|
+
for (let at = 0, offset = 0; at < views.length; offset += views[at++].length + 1)
|
|
1737
|
+
starts.push(offset);
|
|
1738
|
+
const ranges = merged(matches.spans);
|
|
1739
|
+
for (let at = 0; at < ranges.length; at += 2) {
|
|
1740
|
+
let [low, high] = [0, starts.length];
|
|
1741
|
+
while (low < high) {
|
|
1742
|
+
const middle = (low + high) >>> 1;
|
|
1743
|
+
if (starts[middle] <= ranges[at])
|
|
1744
|
+
low = middle + 1;
|
|
1745
|
+
else
|
|
1746
|
+
high = middle;
|
|
1747
|
+
}
|
|
1748
|
+
for (let view = Math.max(0, low - 1); view < starts.length && starts[view] < ranges[at + 1]; view++)
|
|
1749
|
+
withhold(view);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
finally {
|
|
1753
|
+
for (let pair = 0; pair < pairLeaves.length; pair++)
|
|
1754
|
+
if (matched[pairGroups[pair]])
|
|
1755
|
+
safe[pairLeaves[pair]] = REDACTED;
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
/** The parts' joined text, matched once, with each part's replacement. A tiny or short
|
|
1759
|
+
* private value equal to a whole part withholds it. A number or a serialized container that
|
|
1760
|
+
* a match touches is withheld whole. A bounded lane also loses an unfinished tail, and is
|
|
1761
|
+
* withheld whole when the set could not tell where one begins. */
|
|
1762
|
+
safeParts(parts, kinds, scan, bounded) {
|
|
1763
|
+
if (!parts.length)
|
|
1764
|
+
return parts;
|
|
1765
|
+
const text = parts.join("");
|
|
1766
|
+
// A form too large to register or index could begin anywhere in what was read.
|
|
1767
|
+
const matches = bounded && this.uncheckedTail() ? undefined : this.spansOf(text, scan, scan.payload);
|
|
1768
|
+
if (bounded && (!matches || matches.deep))
|
|
1769
|
+
return parts.map((part) => part && REDACTED);
|
|
1770
|
+
if (bounded)
|
|
1771
|
+
this.openSpans(text, matches.spans);
|
|
1772
|
+
let safe = matches ? replaceParts(parts, matches.spans) : this.leafByLeaf(parts, scan);
|
|
1773
|
+
if (matches?.deep)
|
|
1774
|
+
safe = safe.map((next, index) => (parts[index].includes("\\") ? REDACTED : next));
|
|
1775
|
+
if (scan.payload && this.smallForms)
|
|
1776
|
+
safe = safe.map((next, index) => (this.whole(parts[index]) ? REDACTED : next));
|
|
1777
|
+
const limit = this.limit(scan);
|
|
1778
|
+
if (limit < Infinity)
|
|
1779
|
+
safe = withholdRuns(parts, safe, limit);
|
|
1780
|
+
return safe.map((next, index) => (next !== parts[index] && kinds[index] === NUMBER ? REDACTED : next));
|
|
1781
|
+
}
|
|
1782
|
+
/** Leaves too large to match joined within the work budget: each is matched alone, in order,
|
|
1783
|
+
* and one that does not fit is withheld without spending what a smaller one needs. A run
|
|
1784
|
+
* crossing from one kept leaf into the next, through any short leaves between them, is then
|
|
1785
|
+
* matched SEAM_UNITS deep on each side of the seam. */
|
|
1786
|
+
leafByLeaf(parts, scan) {
|
|
1787
|
+
const spans = parts.map((part) => {
|
|
1788
|
+
const matches = this.spansOf(part, scan, scan.payload);
|
|
1789
|
+
return !matches || (matches.deep && part.includes("\\")) ? undefined : matches.spans;
|
|
1790
|
+
});
|
|
1791
|
+
let pieces = [];
|
|
1792
|
+
const seam = () => {
|
|
1793
|
+
const text = pieces.map((piece) => piece.text).join("");
|
|
1794
|
+
const matches = pieces.length > 1 ? this.spansOf(text, scan, scan.payload) : { spans: [], deep: false };
|
|
1795
|
+
const ranges = matches ? merged(matches.spans) : [];
|
|
1796
|
+
let offset = 0;
|
|
1797
|
+
for (const piece of pieces) {
|
|
1798
|
+
const end = offset + piece.text.length;
|
|
1799
|
+
if (!matches || matches.deep)
|
|
1800
|
+
spans[piece.index] = undefined;
|
|
1801
|
+
for (let at = 0; at < ranges.length; at += 2)
|
|
1802
|
+
if (ranges[at] < end && ranges[at + 1] > offset)
|
|
1803
|
+
spans[piece.index]?.push(piece.start + Math.max(0, ranges[at] - offset), piece.start + Math.min(piece.text.length, ranges[at + 1] - offset));
|
|
1804
|
+
offset = end;
|
|
1805
|
+
}
|
|
1806
|
+
pieces = [];
|
|
1807
|
+
};
|
|
1808
|
+
parts.forEach((part, index) => {
|
|
1809
|
+
if (!spans[index])
|
|
1810
|
+
return seam();
|
|
1811
|
+
if (part.length <= 2 * SEAM_UNITS)
|
|
1812
|
+
return void pieces.push({ index, start: 0, text: part });
|
|
1813
|
+
pieces.push({ index, start: 0, text: part.slice(0, SEAM_UNITS) });
|
|
1814
|
+
seam();
|
|
1815
|
+
pieces.push({ index, start: part.length - SEAM_UNITS, text: part.slice(-SEAM_UNITS) });
|
|
1816
|
+
});
|
|
1817
|
+
seam();
|
|
1818
|
+
return parts.map((part, index) => {
|
|
1819
|
+
const own = spans[index];
|
|
1820
|
+
return !own ? part && REDACTED : own.length ? replace(part, own) : part;
|
|
1821
|
+
});
|
|
751
1822
|
}
|
|
752
|
-
/**
|
|
753
|
-
*
|
|
754
|
-
*
|
|
755
|
-
|
|
756
|
-
|
|
1823
|
+
/** Serialized JSON whose keys, syntax and leaves together hold a K-unit run of a form: one
|
|
1824
|
+
* spanning tokens, which no leaf holds alone, withholds the whole text. A run inside one
|
|
1825
|
+
* string or number is a leaf's, already matched in the projection. */
|
|
1826
|
+
spansTokens(text, scan) {
|
|
1827
|
+
if (!this.fragments().size)
|
|
1828
|
+
return false;
|
|
1829
|
+
if (!spend(scan, 2 * text.length))
|
|
1830
|
+
return true;
|
|
1831
|
+
const spans = [];
|
|
1832
|
+
this.fragments().scan(text, 0, text.length, spans, undefined);
|
|
1833
|
+
if (!spans.length)
|
|
1834
|
+
return false;
|
|
1835
|
+
const tokens = jsonTokens(text);
|
|
1836
|
+
const ranges = merged(spans);
|
|
1837
|
+
for (let at = 0; at < ranges.length; at += 2) {
|
|
1838
|
+
let [low, high] = [0, tokens.length / 2];
|
|
1839
|
+
while (low < high) {
|
|
1840
|
+
const middle = (low + high) >>> 1;
|
|
1841
|
+
if (tokens[middle * 2 + 1] <= ranges[at])
|
|
1842
|
+
low = middle + 1;
|
|
1843
|
+
else
|
|
1844
|
+
high = middle;
|
|
1845
|
+
}
|
|
1846
|
+
if (low * 2 >= tokens.length || tokens[low * 2] > ranges[at] || tokens[low * 2 + 1] < ranges[at + 1])
|
|
1847
|
+
return true;
|
|
1848
|
+
}
|
|
1849
|
+
return false;
|
|
1850
|
+
}
|
|
1851
|
+
/** The output boundary. One walk reads every field once, from its descriptor, and projects
|
|
1852
|
+
* every string and number leaf in serialization order, nested JSON strings parsed and errors'
|
|
1853
|
+
* own fields included, declared binary fields and the bytes and numbers typed binary holds
|
|
1854
|
+
* among them; the one checker matches the joined projection, so a form split across adjacent
|
|
1855
|
+
* leaves is found, and only the affected piece of each leaf is written back. Keys and scalars
|
|
1856
|
+
* go through the same checker alone. Fixed framing words and tags, outcome booleans, and on an
|
|
1857
|
+
* error its classification code and a name its registered class gives it (redaction-identity's
|
|
1858
|
+
* fixed allowlist) are kept: nothing downstream chooses them. Every other field is matched,
|
|
1859
|
+
* whatever its name; an error's other name is projected like any field and withheld whole when
|
|
1860
|
+
* anything changes any part of it.
|
|
1861
|
+
*
|
|
1862
|
+
* What returns is what was checked, and only JSON data: strings, numbers, booleans, null,
|
|
1863
|
+
* plain objects and arrays, a fresh built-in Uint8Array, and error copies. A caller's container
|
|
1864
|
+
* is returned as it stands only as verified plain data, so a later read returns the same
|
|
1865
|
+
* values: its prototype is Object.prototype, Array.prototype or null, nothing on its chain is a
|
|
1866
|
+
* toJSON, it holds no symbol field and an array no field but its indices (a hole checked
|
|
1867
|
+
* through its prototypes), every field is a data property this walk read, and nothing in it
|
|
1868
|
+
* changed. Anything else is a copy. An error always is, of its own class, with the accessors in
|
|
1869
|
+
* ERROR_ACCESSORS and those its chain defines read once into fields and a toJSON shadowed, never
|
|
1870
|
+
* read; an object of any other class is plain data, its chain's accessors unread; an array that
|
|
1871
|
+
* is not plain is a plain array of its indices; a String, Number or Boolean wrapper is its
|
|
1872
|
+
* primitive. A typed array, DataView or ArrayBuffer becomes a fresh Uint8Array of the bytes it
|
|
1873
|
+
* held, which are checked as bytes, and as numbers with any other elements it held. A function
|
|
1874
|
+
* or symbol is withheld. A container's own accessor is never invoked: its copy leaves it out,
|
|
1875
|
+
* or holds the placeholder at an array index. A toJSON is called once, as serialization would,
|
|
1876
|
+
* and replaced by its checked result. A change copies every container on its path on first
|
|
1877
|
+
* write, so no caller's value is ever mutated, and a container past the node budget is
|
|
1878
|
+
* withheld whole. Copies hold writable, configurable data properties and keep the identity
|
|
1879
|
+
* facts registered in redaction-identity. */
|
|
1880
|
+
project(value, scan, keepKeys = false) {
|
|
1881
|
+
const parts = [];
|
|
1882
|
+
const kinds = [];
|
|
1883
|
+
const frames = [];
|
|
1884
|
+
const slots = [];
|
|
1885
|
+
const owners = [];
|
|
1886
|
+
// Structural changes found while walking: a withheld or checked field, a copied container.
|
|
1887
|
+
const replaced = [];
|
|
1888
|
+
// Caller's arrays that are not plain data: copied even where nothing in them changed.
|
|
1889
|
+
const copied = [];
|
|
1890
|
+
const nested = [];
|
|
757
1891
|
const active = new Set();
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
1892
|
+
// Keys and scalars repeat across rows; each is checked once per call.
|
|
1893
|
+
const seen = { keys: new Map(), scalars: new Map() };
|
|
1894
|
+
// Errors' unregistered names, as walked: a copy whose name changed holds the placeholder.
|
|
1895
|
+
const named = [];
|
|
1896
|
+
// Typed binary: where its copy was put, and the leaves its numbers and bytes span.
|
|
1897
|
+
const binaries = [];
|
|
1898
|
+
// The accessors each prototype's chain defines, found once per call.
|
|
1899
|
+
const chains = new Map();
|
|
1900
|
+
let changed = false;
|
|
1901
|
+
const touch = (owner) => {
|
|
1902
|
+
changed = true;
|
|
1903
|
+
if (owner >= 0)
|
|
1904
|
+
nested[owner].dirty = true;
|
|
1905
|
+
};
|
|
1906
|
+
const withheld = (owner) => {
|
|
1907
|
+
touch(owner);
|
|
1908
|
+
return REDACTED;
|
|
1909
|
+
};
|
|
1910
|
+
/** A key or scalar's text, through the one checker alone. */
|
|
1911
|
+
const check = (cache, text, kind) => {
|
|
1912
|
+
let result = cache.get(text);
|
|
1913
|
+
if (result === undefined)
|
|
1914
|
+
cache.set(text, (result = this.checked([text], [kind], scan)[0]));
|
|
1915
|
+
return result;
|
|
1916
|
+
};
|
|
1917
|
+
const replace = (frame, slot, next, owner) => {
|
|
1918
|
+
replaced.push([frame, slot, next]);
|
|
1919
|
+
touch(owner);
|
|
1920
|
+
};
|
|
1921
|
+
const leaf = (text, kind, frame, slot, owner) => {
|
|
1922
|
+
parts.push(text);
|
|
1923
|
+
kinds.push(kind);
|
|
1924
|
+
frames.push(frame);
|
|
1925
|
+
slots.push(slot);
|
|
1926
|
+
owners.push(owner);
|
|
1927
|
+
};
|
|
1928
|
+
/** A field put on a copy or on parsed JSON: an array index by assignment, any other key as a
|
|
1929
|
+
* writable, configurable data property, enumerable as it was. A frozen or read-only original
|
|
1930
|
+
* never makes a write throw, and no inherited setter runs. */
|
|
1931
|
+
const put = (holder, slot, next, enumerable = true) => {
|
|
1932
|
+
if (typeof slot === "number")
|
|
1933
|
+
holder[slot] = next;
|
|
1934
|
+
else
|
|
1935
|
+
Object.defineProperty(holder, slot, {
|
|
1936
|
+
value: next,
|
|
1937
|
+
writable: true,
|
|
1938
|
+
configurable: true,
|
|
1939
|
+
enumerable: Object.getOwnPropertyDescriptor(holder, slot)?.enumerable ?? enumerable,
|
|
1940
|
+
});
|
|
1941
|
+
};
|
|
1942
|
+
/** A caller's container copied from its descriptors, never invoking an accessor: an array as
|
|
1943
|
+
* a plain array of its indices, a hole staying one and an accessor the placeholder; a plain
|
|
1944
|
+
* object as its own data fields. */
|
|
1945
|
+
const shallow = (frame) => {
|
|
1946
|
+
const item = frame.target;
|
|
1947
|
+
if (Array.isArray(item)) {
|
|
1948
|
+
const length = item.length;
|
|
1949
|
+
const out = [];
|
|
1950
|
+
out.length = length;
|
|
1951
|
+
for (let index = 0; index < length; index++) {
|
|
1952
|
+
const descriptor = Object.getOwnPropertyDescriptor(item, index);
|
|
1953
|
+
if (descriptor)
|
|
1954
|
+
out[index] = "value" in descriptor ? descriptor.value : REDACTED;
|
|
1955
|
+
}
|
|
1956
|
+
return redactedCopyOf(item, out);
|
|
1957
|
+
}
|
|
1958
|
+
const out = Object.create(Object.getPrototypeOf(item));
|
|
1959
|
+
for (const name of Object.getOwnPropertyNames(item)) {
|
|
1960
|
+
const descriptor = Object.getOwnPropertyDescriptor(item, name);
|
|
1961
|
+
if ("value" in descriptor)
|
|
1962
|
+
put(out, name, descriptor.value, descriptor.enumerable);
|
|
1963
|
+
}
|
|
1964
|
+
return redactedCopyOf(item, out);
|
|
1965
|
+
};
|
|
1966
|
+
// toJSON results, which replace their container in its slot and may be copied after.
|
|
1967
|
+
const serialized = new Set();
|
|
1968
|
+
/** A caller's container walked in a slot. A toJSON result is remembered there, so a copy it
|
|
1969
|
+
* gets replaces it in that slot. */
|
|
1970
|
+
const walked = (frame, item, slot, owned) => {
|
|
1971
|
+
const own = { target: item, shared: !owned, parent: frame, slot };
|
|
1972
|
+
if (!owned && serialized.has(item))
|
|
1973
|
+
(frame.children ??= new Map()).set(slot, own);
|
|
1974
|
+
return own;
|
|
1975
|
+
};
|
|
1976
|
+
const writable = (frame) => {
|
|
1977
|
+
if (!frame.shared)
|
|
1978
|
+
return frame.target;
|
|
1979
|
+
if (!frame.copy) {
|
|
1980
|
+
frame.copy = shallow(frame);
|
|
1981
|
+
put(writable(frame.parent), frame.slot, frame.copy);
|
|
1982
|
+
}
|
|
1983
|
+
return frame.copy;
|
|
1984
|
+
};
|
|
1985
|
+
/** The accessors a prototype's chain defines below Object.prototype, by name. */
|
|
1986
|
+
const accessorsOf = (prototype) => {
|
|
1987
|
+
let names = chains.get(prototype);
|
|
1988
|
+
if (!names) {
|
|
1989
|
+
names = [];
|
|
1990
|
+
for (let at = prototype; at && at !== Object.prototype; at = Object.getPrototypeOf(at))
|
|
1991
|
+
for (const name of Object.getOwnPropertyNames(at)) {
|
|
1992
|
+
const descriptor = Object.getOwnPropertyDescriptor(at, name);
|
|
1993
|
+
if (descriptor && !("value" in descriptor) && !names.includes(name))
|
|
1994
|
+
names.push(name);
|
|
1995
|
+
}
|
|
1996
|
+
chains.set(prototype, names);
|
|
1997
|
+
}
|
|
1998
|
+
return names;
|
|
1999
|
+
};
|
|
2000
|
+
const visit = (item, frame, slot, depth, level, owner, owned) => {
|
|
2001
|
+
if (typeof item === "string") {
|
|
2002
|
+
if (this.mintedStates.has(item))
|
|
2003
|
+
return item;
|
|
2004
|
+
if (level < MAX_DECODE_LEVELS && /^\s*["[{]/.test(item) && spend(scan, item.length)) {
|
|
2005
|
+
const parsed = parse(item);
|
|
2006
|
+
if (parsed) {
|
|
2007
|
+
const entry = { frame, slot, json: undefined, owner, crossing: false, dirty: false };
|
|
2008
|
+
entry.crossing = this.spansTokens(item, scan);
|
|
2009
|
+
const index = nested.push(entry) - 1;
|
|
2010
|
+
// Parsed JSON is this walk's own, so it is written in place.
|
|
2011
|
+
const holder = { target: entry, shared: false, parent: undefined, slot: "json" };
|
|
2012
|
+
entry.json = visit(parsed.json, holder, "json", depth + 1, level + 1, index, true);
|
|
2013
|
+
return item;
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
leaf(item, TEXT, frame, slot, owner);
|
|
2017
|
+
return item;
|
|
2018
|
+
}
|
|
2019
|
+
if (typeof item === "number" || typeof item === "bigint") {
|
|
2020
|
+
leaf(String(item), NUMBER, frame, slot, owner);
|
|
2021
|
+
return item;
|
|
2022
|
+
}
|
|
2023
|
+
if (typeof item === "boolean" || item === null)
|
|
2024
|
+
return check(seen.scalars, String(item), NUMBER) === String(item) ? item : withheld(owner);
|
|
2025
|
+
// A function carries no data, and every exit's serializer would call a toJSON it holds; a
|
|
2026
|
+
// symbol carries none either. Both are withheld.
|
|
2027
|
+
if (typeof item === "function" || typeof item === "symbol")
|
|
2028
|
+
return withheld(owner);
|
|
763
2029
|
if (typeof item !== "object")
|
|
764
2030
|
return item;
|
|
765
2031
|
if (active.has(item) || depth > MAX_WALK_DEPTH)
|
|
766
|
-
return
|
|
767
|
-
|
|
768
|
-
|
|
2032
|
+
return withheld(owner);
|
|
2033
|
+
active.add(item);
|
|
2034
|
+
try {
|
|
2035
|
+
return container(item, frame, slot, depth, level, owner, owned);
|
|
2036
|
+
}
|
|
2037
|
+
catch {
|
|
2038
|
+
return withheld(owner); // A proxy trap or a toJSON that threw.
|
|
2039
|
+
}
|
|
2040
|
+
finally {
|
|
2041
|
+
active.delete(item);
|
|
2042
|
+
}
|
|
2043
|
+
};
|
|
2044
|
+
/** A child that changed: written into parsed JSON this walk owns, or recorded as a change
|
|
2045
|
+
* to a caller's container. */
|
|
2046
|
+
const settle = (frame, slot, next, owner) => {
|
|
2047
|
+
if (frame.shared)
|
|
2048
|
+
return replace(frame, slot, next, owner);
|
|
2049
|
+
put(frame.target, slot, next);
|
|
2050
|
+
touch(owner);
|
|
2051
|
+
};
|
|
2052
|
+
/** Entries within the node budget are walked; a container past it is withheld whole. */
|
|
2053
|
+
const fits = (size) => {
|
|
769
2054
|
if (size > scan.budget.nodes)
|
|
770
|
-
return
|
|
2055
|
+
return false;
|
|
771
2056
|
scan.budget.nodes -= size;
|
|
772
|
-
|
|
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;
|
|
2057
|
+
return true;
|
|
778
2058
|
};
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
if (!
|
|
783
|
-
|
|
784
|
-
|
|
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);
|
|
2059
|
+
/** A declared base64 field: a leaf the checker also decodes whole, unless this set already
|
|
2060
|
+
* found the same content clean. */
|
|
2061
|
+
const declared = (text, frame, slot, owner) => {
|
|
2062
|
+
if (this.memo.get(text) !== true && (scan.payload || !this.memo.has(text)))
|
|
2063
|
+
leaf(text, BINARY, frame, slot, owner);
|
|
2064
|
+
return text;
|
|
811
2065
|
};
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
2066
|
+
/** Typed binary as the fresh Uint8Array of its bytes. Its leaves are the numbers JSON shows
|
|
2067
|
+
* for it and for its copy, each element's and each byte's, and its bytes; a match in any
|
|
2068
|
+
* withholds it whole, as does a size past the node budget. */
|
|
2069
|
+
const typed = ({ bytes, elements }, frame, slot, owner) => {
|
|
2070
|
+
const numbers = elements ? new elements(bytes.buffer) : undefined;
|
|
2071
|
+
if (!fits(bytes.length + (numbers?.length ?? 0) + 1) || !spend(scan, Math.ceil(bytes.length / 8)))
|
|
2072
|
+
return withheld(owner);
|
|
2073
|
+
// The leaves write nowhere: the copy's slot is settled once they are checked.
|
|
2074
|
+
const sink = { target: [], shared: false, parent: undefined, slot };
|
|
2075
|
+
const start = parts.length;
|
|
2076
|
+
if (numbers)
|
|
2077
|
+
for (let index = 0; index < numbers.length; index++)
|
|
2078
|
+
leaf(String(numbers[index]), NUMBER, sink, index, owner);
|
|
2079
|
+
for (let index = 0; index < bytes.length; index++)
|
|
2080
|
+
leaf(DECIMAL[bytes[index]], NUMBER, sink, index, owner);
|
|
2081
|
+
leaf(latin1(bytes), BYTES, sink, 0, owner);
|
|
2082
|
+
binaries.push([frame, slot, owner, start, parts.length]);
|
|
2083
|
+
return bytes;
|
|
2084
|
+
};
|
|
2085
|
+
const container = (item, frame, slot, depth, level, owner, owned) => {
|
|
2086
|
+
const error = item instanceof Error;
|
|
2087
|
+
if (!error && !owned) {
|
|
2088
|
+
const held = binaryOf(item);
|
|
2089
|
+
if (held)
|
|
2090
|
+
return typed(held, frame, slot, owner);
|
|
2091
|
+
const primitive = unboxed(item);
|
|
2092
|
+
if (primitive)
|
|
2093
|
+
return visit(primitive.value, frame, slot, depth + 1, level, owner, false);
|
|
2094
|
+
// What serialization would read: a toJSON result, checked in place of the container.
|
|
2095
|
+
const toJSON = inherited(item, "toJSON", WITHHELD_ACCESSOR);
|
|
2096
|
+
if (toJSON === WITHHELD_ACCESSOR)
|
|
2097
|
+
return withheld(owner);
|
|
2098
|
+
if (typeof toJSON === "function") {
|
|
2099
|
+
const result = toJSON.call(item, String(slot));
|
|
2100
|
+
if (result !== null && typeof result === "object")
|
|
2101
|
+
serialized.add(result);
|
|
2102
|
+
return visit(result, frame, slot, depth + 1, level, owner, false);
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
return Array.isArray(item)
|
|
2106
|
+
? list(item, frame, slot, depth, level, owner, owned)
|
|
2107
|
+
: record(item, error, frame, slot, depth, level, owner, owned);
|
|
2108
|
+
};
|
|
2109
|
+
/** An array's indices, each read once. A caller's array stands only as verified plain data;
|
|
2110
|
+
* one that is not is copied as a plain array of its indices, which drops other fields. */
|
|
2111
|
+
const list = (item, frame, slot, depth, level, owner, owned) => {
|
|
2112
|
+
const length = item.length;
|
|
2113
|
+
if (!fits(length))
|
|
2114
|
+
return withheld(owner);
|
|
2115
|
+
const own = walked(frame, item, slot, owned);
|
|
2116
|
+
for (let index = 0; index < length; index++) {
|
|
2117
|
+
const descriptor = Object.getOwnPropertyDescriptor(item, index);
|
|
2118
|
+
if (!descriptor) {
|
|
2119
|
+
// A hole serializes as what its prototypes hold at its index: a data field is checked
|
|
2120
|
+
// and copied in, an accessor is withheld unread, and nothing leaves the hole as it is.
|
|
2121
|
+
const found = owned ? undefined : inherited(item, String(index), WITHHELD_ACCESSOR);
|
|
2122
|
+
if (found === WITHHELD_ACCESSOR)
|
|
2123
|
+
replace(own, index, REDACTED, owner);
|
|
2124
|
+
else if (found !== undefined) {
|
|
2125
|
+
replaced.push([own, index, found]);
|
|
2126
|
+
const next = visit(found, own, index, depth + 1, level, owner, owned);
|
|
2127
|
+
if (next !== found)
|
|
2128
|
+
settle(own, index, next, owner);
|
|
2129
|
+
}
|
|
2130
|
+
continue;
|
|
2131
|
+
}
|
|
2132
|
+
if (!("value" in descriptor)) {
|
|
2133
|
+
replace(own, index, REDACTED, owner);
|
|
2134
|
+
continue;
|
|
2135
|
+
}
|
|
2136
|
+
const next = visit(descriptor.value, own, index, depth + 1, level, owner, owned);
|
|
2137
|
+
if (next !== descriptor.value)
|
|
2138
|
+
settle(own, index, next, owner);
|
|
2139
|
+
}
|
|
2140
|
+
// Fields other than its indices are not read: JSON, which the MCP wire, the result stash and
|
|
2141
|
+
// every executor marshal with, never serializes them, and listing them would allocate a
|
|
2142
|
+
// string per index.
|
|
2143
|
+
if (!owned && (Object.getPrototypeOf(item) !== Array.prototype || Object.getOwnPropertySymbols(item).length > 0))
|
|
2144
|
+
copied.push(own);
|
|
2145
|
+
return item;
|
|
2146
|
+
};
|
|
2147
|
+
const record = (item, error, frame, slot, depth, level, owner, owned) => {
|
|
2148
|
+
const names = Object.getOwnPropertyNames(item);
|
|
2149
|
+
if (!fits(names.length))
|
|
2150
|
+
return withheld(owner);
|
|
2151
|
+
const prototype = Object.getPrototypeOf(item);
|
|
2152
|
+
let plain = owned ||
|
|
2153
|
+
(!error &&
|
|
2154
|
+
(prototype === Object.prototype || prototype === null) &&
|
|
2155
|
+
Object.getOwnPropertySymbols(item).length === 0);
|
|
2156
|
+
// Every field is read once, and every decision below reads what was read.
|
|
2157
|
+
const keys = [];
|
|
2158
|
+
const values = [];
|
|
2159
|
+
const visible = [];
|
|
2160
|
+
for (const name of names) {
|
|
2161
|
+
const descriptor = Object.getOwnPropertyDescriptor(item, name);
|
|
2162
|
+
if (!descriptor)
|
|
2163
|
+
continue;
|
|
2164
|
+
// An error is never replaced by its toJSON: its copy shadows one.
|
|
2165
|
+
if ("value" in descriptor)
|
|
2166
|
+
values.push(error && name === "toJSON" ? undefined : descriptor.value);
|
|
2167
|
+
else if (error && descriptor.get && ERROR_ACCESSORS.has(name)) {
|
|
2168
|
+
// V8 renders Error.stack through an own accessor: it is read once into the copy.
|
|
2169
|
+
try {
|
|
2170
|
+
values.push(Reflect.apply(descriptor.get, item, []));
|
|
2171
|
+
}
|
|
2172
|
+
catch {
|
|
2173
|
+
continue; // A getter that throws leaves its field out of the copy.
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
820
2176
|
else {
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
2177
|
+
// Never invoked: the container is copied without it.
|
|
2178
|
+
plain = false;
|
|
2179
|
+
continue;
|
|
824
2180
|
}
|
|
2181
|
+
keys.push(name);
|
|
2182
|
+
visible.push(descriptor.enumerable === true);
|
|
825
2183
|
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
2184
|
+
// A key is checked like any text; an error's toJSON shadow keeps its name, so it shadows.
|
|
2185
|
+
// `keepKeys` leaves the walked value's own keys, never those of JSON inside a string.
|
|
2186
|
+
const rename = (key) => FRAMING_KEYS.has(key) || (error && key === "toJSON") || (keepKeys && level === 0 && !error)
|
|
2187
|
+
? key
|
|
2188
|
+
: check(seen.keys, key, TEXT);
|
|
2189
|
+
const renamed = keys.map(rename);
|
|
2190
|
+
if (renamed.some((name, index) => name !== keys[index]))
|
|
2191
|
+
plain = false;
|
|
2192
|
+
// An error's copy is of its class. Every accessor its chain defines, such as DOMException's
|
|
2193
|
+
// name and message or an SDK error's status, and whatever the chain answers for the names
|
|
2194
|
+
// in ERROR_ACCESSORS, is read once and held as a checked field under its checked name, so
|
|
2195
|
+
// no later read of the copy invokes a getter. A toJSON, data or accessor, is never read: the
|
|
2196
|
+
// copy shadows it, so no serializer can run. Any other copy is plain data.
|
|
2197
|
+
let extras;
|
|
2198
|
+
if (error && prototype) {
|
|
2199
|
+
for (const name of [...ERROR_ACCESSORS, ...accessorsOf(prototype)]) {
|
|
2200
|
+
if (name === "toJSON" || keys.includes(name) || extras?.some(([taken]) => taken === name))
|
|
2201
|
+
continue;
|
|
2202
|
+
if (!(name in item))
|
|
2203
|
+
continue;
|
|
2204
|
+
try {
|
|
2205
|
+
(extras ??= []).push([name, Reflect.get(item, name)]);
|
|
2206
|
+
}
|
|
2207
|
+
catch {
|
|
2208
|
+
(extras ??= []).push([name, undefined]); // A getter that throws leaves its field shadowed.
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
if (!keys.includes("toJSON") && inherited(item, "toJSON", WITHHELD_ACCESSOR) !== undefined)
|
|
2212
|
+
(extras ??= []).push(["toJSON", undefined]);
|
|
2213
|
+
}
|
|
2214
|
+
// A container walked as it stands: a caller's, copied on first write, or parsed JSON this
|
|
2215
|
+
// walk owns, written in place. Any other is copied now, field by field.
|
|
2216
|
+
const own = plain
|
|
2217
|
+
? walked(frame, item, slot, owned)
|
|
2218
|
+
: {
|
|
2219
|
+
target: Object.create(error || prototype === null ? prototype : Object.prototype),
|
|
2220
|
+
shared: false,
|
|
2221
|
+
parent: frame,
|
|
2222
|
+
slot,
|
|
2223
|
+
};
|
|
2224
|
+
const out = own.target;
|
|
2225
|
+
const field = (name) => values[keys.indexOf(name)];
|
|
2226
|
+
const count = keys.length + (extras?.length ?? 0);
|
|
2227
|
+
for (let index = 0; index < count; index++) {
|
|
2228
|
+
const extra = index >= keys.length ? extras[index - keys.length] : undefined;
|
|
2229
|
+
const key = extra ? extra[0] : keys[index];
|
|
2230
|
+
const value = extra ? extra[1] : values[index];
|
|
2231
|
+
const name = extra ? rename(key) : renamed[index];
|
|
2232
|
+
// Two keys renamed alike keep the first; the second's leaves write nowhere.
|
|
2233
|
+
const target = !plain && Object.hasOwn(out, name) ? { target: {}, shared: false, parent: undefined, slot: name } : own;
|
|
2234
|
+
let next;
|
|
2235
|
+
if ((typeof value === "string" && TAGS.has(`${key}:${value}`)) ||
|
|
2236
|
+
(typeof value === "boolean" && FACTS.has(key)) ||
|
|
2237
|
+
(error && typeof value === "string" && key === "code" && classificationCode(value) !== undefined))
|
|
2238
|
+
next = value;
|
|
2239
|
+
// An error's name is kept only as one its registered class gives it. Any other is a field
|
|
2240
|
+
// like any other, projected with its neighbours, and withheld whole once anything changes
|
|
2241
|
+
// any part of it; the copy keeps its class.
|
|
2242
|
+
else if (error && key === "name" && typeof value === "string" && keptErrorName(prototype, value))
|
|
2243
|
+
next = value;
|
|
2244
|
+
else if (typeof value === "string" && binaryField(field, key))
|
|
2245
|
+
next = declared(value, target, name, owner);
|
|
2246
|
+
else {
|
|
2247
|
+
next = visit(value, target, name, depth + 1, level, owner, owned);
|
|
2248
|
+
if (error && key === "name" && typeof value === "string" && target === own)
|
|
2249
|
+
named.push([out, name, value]);
|
|
2250
|
+
}
|
|
2251
|
+
if (target !== own)
|
|
2252
|
+
continue;
|
|
2253
|
+
if (!plain)
|
|
2254
|
+
put(out, name, next, extra ? false : visible[index]);
|
|
2255
|
+
else if (next !== value)
|
|
2256
|
+
settle(own, name, next, owner);
|
|
2257
|
+
}
|
|
2258
|
+
if (plain)
|
|
2259
|
+
return item;
|
|
2260
|
+
touch(owner);
|
|
2261
|
+
return redactedCopyOf(item, out);
|
|
830
2262
|
};
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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;
|
|
2263
|
+
const root = { target: { value: undefined }, shared: false, parent: undefined, slot: "value" };
|
|
2264
|
+
root.target["value"] = visit(value, root, "value", 0, 0, -1, false);
|
|
2265
|
+
const safe = this.checked(parts, kinds, scan);
|
|
2266
|
+
// Structure first, then the leaves inside it and typed binary, then nested JSON from the
|
|
2267
|
+
// innermost out.
|
|
2268
|
+
// A slot replaced by a toJSON result walked there takes the copy that result already got,
|
|
2269
|
+
// never the original.
|
|
2270
|
+
for (const [frame, slot, next] of replaced) {
|
|
2271
|
+
const child = frame.children?.get(slot);
|
|
2272
|
+
put(writable(frame), slot, child && child.target === next && child.copy ? child.copy : next);
|
|
846
2273
|
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
//
|
|
2274
|
+
for (const frame of copied)
|
|
2275
|
+
writable(frame);
|
|
2276
|
+
let marker;
|
|
2277
|
+
for (let index = 0; index < parts.length; index++) {
|
|
2278
|
+
const next = safe[index];
|
|
2279
|
+
const kind = kinds[index];
|
|
2280
|
+
if (next === parts[index]) {
|
|
2281
|
+
if (kind === BINARY && next.length >= MEMO_UNITS)
|
|
2282
|
+
this.memo.set(next, scan.payload);
|
|
2283
|
+
continue;
|
|
2284
|
+
}
|
|
2285
|
+
const holder = writable(frames[index]);
|
|
2286
|
+
// Declared binary is withheld as the encoded marker, itself checked; a typed number on an
|
|
2287
|
+
// error is dropped rather than turned into text.
|
|
2288
|
+
if (kind === BINARY) {
|
|
2289
|
+
const encoded = btoa(REDACTED);
|
|
2290
|
+
marker ??= this.checked([encoded], [BINARY], scan)[0] === encoded ? encoded : "";
|
|
2291
|
+
}
|
|
2292
|
+
put(holder, slots[index], kind === BINARY ? marker : kind === NUMBER && holder instanceof Error ? undefined : next);
|
|
2293
|
+
touch(owners[index]);
|
|
860
2294
|
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
try {
|
|
870
|
-
binary = atob(value);
|
|
2295
|
+
for (const [frame, slot, owner, start, end] of binaries) {
|
|
2296
|
+
let clean = true;
|
|
2297
|
+
for (let index = start; clean && index < end; index++)
|
|
2298
|
+
clean = safe[index] === parts[index];
|
|
2299
|
+
if (clean)
|
|
2300
|
+
continue;
|
|
2301
|
+
put(writable(frame), slot, REDACTED);
|
|
2302
|
+
touch(owner);
|
|
871
2303
|
}
|
|
872
|
-
|
|
873
|
-
|
|
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))
|
|
2304
|
+
for (let index = nested.length - 1; index >= 0; index--) {
|
|
2305
|
+
const entry = nested[index];
|
|
2306
|
+
if (!entry.crossing && !entry.dirty)
|
|
903
2307
|
continue;
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
if (!blocks.includes(block))
|
|
907
|
-
return block;
|
|
908
|
-
const text = first ? redacted : "";
|
|
909
|
-
first = false;
|
|
910
|
-
return { ...block, text };
|
|
911
|
-
});
|
|
2308
|
+
put(writable(entry.frame), entry.slot, entry.crossing ? REDACTED : JSON.stringify(entry.json));
|
|
2309
|
+
touch(entry.owner);
|
|
912
2310
|
}
|
|
913
|
-
|
|
2311
|
+
for (const [copy, slot, name] of named)
|
|
2312
|
+
if (copy[slot] !== name)
|
|
2313
|
+
put(copy, slot, REDACTED);
|
|
2314
|
+
return { value: root.target["value"], changed };
|
|
914
2315
|
}
|
|
915
2316
|
}
|
|
916
2317
|
export function sentSecretsFor(ctx) {
|
|
917
|
-
|
|
918
|
-
if (!secrets) {
|
|
919
|
-
secrets = new SentSecrets();
|
|
920
|
-
contexts.set(ctx, secrets);
|
|
921
|
-
}
|
|
922
|
-
sentSecretsForRequest(ctx.requestScope ?? ctx).include(secrets);
|
|
923
|
-
return secrets;
|
|
2318
|
+
return viewSecrets(ctx);
|
|
924
2319
|
}
|
|
925
2320
|
/** One identity per upstream request, including all discovery and call work. */
|
|
926
2321
|
export function sentSecretsForRequest(scope, secrets) {
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
requests.set(scope, request);
|
|
931
|
-
}
|
|
932
|
-
else if (secrets) {
|
|
2322
|
+
const retained = requestOwner(scope).retained;
|
|
2323
|
+
const request = (retained.secrets ??= secrets ?? new SentSecrets());
|
|
2324
|
+
if (secrets)
|
|
933
2325
|
request.include(secrets);
|
|
934
|
-
}
|
|
935
2326
|
return request;
|
|
936
2327
|
}
|
|
937
2328
|
/** The agent-facing choke point. Apply to values before bridge serialization
|
|
938
2329
|
* and to serialized wire text, so JSON escapes and structured strings share
|
|
939
2330
|
* the same rule. Payloads were redacted where they entered; this boundary
|
|
940
|
-
* repeats
|
|
941
|
-
*
|
|
2331
|
+
* repeats every check but short private values around Connecta's own
|
|
2332
|
+
* envelopes, whose identifiers and counts are never private data. */
|
|
942
2333
|
export function redactAgentOutput(secrets, value) {
|
|
943
2334
|
return secrets.redact(value, "envelope");
|
|
944
2335
|
}
|
|
@@ -948,50 +2339,62 @@ export function redactAgentOutput(secrets, value) {
|
|
|
948
2339
|
export function agentOutputOperations(create, requestScope) {
|
|
949
2340
|
return Object.fromEntries(Object.keys(create(requestScope ?? {})).map((name) => [
|
|
950
2341
|
name,
|
|
951
|
-
|
|
2342
|
+
authMethod((...args) => Effect.suspend(() => {
|
|
952
2343
|
const scope = requestScope ?? {};
|
|
953
2344
|
const secrets = sentSecretsForRequest(scope);
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
}
|
|
960
|
-
},
|
|
2345
|
+
const owner = requestOwner(scope);
|
|
2346
|
+
if (!owner.open)
|
|
2347
|
+
return Effect.fail(owner.controller.signal.reason);
|
|
2348
|
+
return authCall(create(scope)[name], args).pipe(Effect.provideService(RequestScope, requestOwner(scope)), Effect.map((value) => redactAgentOutput(secrets, value)), Effect.mapError((error) => redactAgentOutput(secrets, error)));
|
|
2349
|
+
})),
|
|
961
2350
|
]));
|
|
962
2351
|
}
|
|
963
|
-
/** Slot reads cover custom handlers too, including keys put in query strings.
|
|
2352
|
+
/** Slot reads cover custom handlers too, including keys put in query strings. A field named
|
|
2353
|
+
* as an identifier (an application or client ID, a username, a public key) is matched whole. */
|
|
2354
|
+
export function registerCredentialValues(secrets, values) {
|
|
2355
|
+
for (const [field, value] of Object.entries(values ?? {})) {
|
|
2356
|
+
if (explicitSecretName.test(field))
|
|
2357
|
+
secrets.secret(value);
|
|
2358
|
+
else if (identifierName.test(field))
|
|
2359
|
+
secrets.identifier(value);
|
|
2360
|
+
else
|
|
2361
|
+
secrets.add(value);
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
964
2364
|
export function trackCredentialReads(ctx) {
|
|
965
|
-
|
|
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
|
-
};
|
|
2365
|
+
wrapViewCredentials(ctx);
|
|
992
2366
|
}
|
|
2367
|
+
/** A connector's result or error as it enters: every connector's credentials and the
|
|
2368
|
+
* request's private arguments, under payload rules. */
|
|
993
2369
|
export function redactSentSecrets(ctx, value) {
|
|
994
|
-
return
|
|
2370
|
+
return requestSentSecrets(ctx).redact(value);
|
|
2371
|
+
}
|
|
2372
|
+
/** Everything the context's request sent: every connector's credentials and the
|
|
2373
|
+
* request's private arguments, which a context's own set does not hold. */
|
|
2374
|
+
export function requestSentSecrets(ctx) {
|
|
2375
|
+
sentSecretsFor(ctx);
|
|
2376
|
+
return sentSecretsForRequest(ctx.requestScope ?? ctx);
|
|
2377
|
+
}
|
|
2378
|
+
/** Downstream text, or a parsed body, about to be cut short, trimmed, normalized or
|
|
2379
|
+
* extracted from. Redact it whole first: a cut through an echoed form can leave a remnant
|
|
2380
|
+
* shorter than any fragment the output boundary matches, and a normalized number or
|
|
2381
|
+
* extracted substring may no longer hold the form at all. */
|
|
2382
|
+
export function redactBeforeCut(ctx, value) {
|
|
2383
|
+
return requestSentSecrets(ctx).redact(value);
|
|
2384
|
+
}
|
|
2385
|
+
/** Base64, or base64url with `url`, of bytes or UTF-8 text Connecta returns to an agent: a
|
|
2386
|
+
* download, an attachment, a REST body, a page cursor. Every such encoding goes through here,
|
|
2387
|
+
* since no later pass reads through an encoding the output does not declare: the content is
|
|
2388
|
+
* checked first with the request's set, and content holding a match encodes the placeholder.
|
|
2389
|
+
* A cursor passes `claims`: connecta's own counts and fingerprints beside downstream values it
|
|
2390
|
+
* already redacted (`redactSentSecrets`), checked under the boundary's rules. */
|
|
2391
|
+
export function encodeForAgent(ctx, content, options) {
|
|
2392
|
+
return requestSentSecrets(ctx).encode(typeof content === "string" ? encoder.encode(content) : content, options);
|
|
2393
|
+
}
|
|
2394
|
+
/** Downstream text read only up to a bound, as a field an agent reads: whole forms are
|
|
2395
|
+
* replaced, and so is a tail that begins one the read stopped inside. */
|
|
2396
|
+
export function redactBoundedRead(ctx, text) {
|
|
2397
|
+
return requestSentSecrets(ctx).redactOpenTail(text);
|
|
995
2398
|
}
|
|
996
2399
|
/** Sanitize listing facts before any cache or consumer receives them. A name
|
|
997
2400
|
* cannot be rewritten without changing dispatch, and dropping one entry would
|