@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
|
@@ -28,15 +28,25 @@
|
|
|
28
28
|
// statuses but never its text (`withholdsErrors`), unless its review
|
|
29
29
|
// found its errors cannot echo a stored secret (`vendorErrors`).
|
|
30
30
|
//
|
|
31
|
-
// Residual risk
|
|
31
|
+
// Residual risk: a secret a person typed into a free-text
|
|
32
32
|
// name, description, log line, or identifier is out of scope.
|
|
33
33
|
//
|
|
34
|
+
// Sent-secret redaction runs first, in the provider kit's parse pass, so a
|
|
35
|
+
// rule that decides on a value can find the placeholder where the vendor
|
|
36
|
+
// wrote what the rule matches. Every such decision input fails closed
|
|
37
|
+
// (#703): an altered type discriminator applies every resource rule; an
|
|
38
|
+
// altered label or type makes a secret record or a header rule; an altered
|
|
39
|
+
// URL scheme or parameter name withholds the whole URL or its value; an
|
|
40
|
+
// altered key withholds its value; a placeholder that may stand for a
|
|
41
|
+
// credential's public prefix withholds the remainder after it.
|
|
42
|
+
//
|
|
34
43
|
// The key-name rules combine Cloudflare's and Vercel's former engines.
|
|
35
44
|
// URL policy and exemptions are reviewed per vendor in its table (`keep`,
|
|
36
45
|
// `fields`, `scopeMaps`, `envBodies`, `embeddedUrls`).
|
|
37
46
|
import { ConnectorCallError } from "../../../errors.js";
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
import { bareReferenceEnd, characterReferenceEnd, REDACTED, redactionAltered, splitRemainder, } from "../../../redaction-marker.js";
|
|
48
|
+
/** What a removed secret reads as: the placeholder sent-secret redaction writes too. */
|
|
49
|
+
export { REDACTED };
|
|
40
50
|
/** Credential vocabulary over normalized names, matched anywhere; mirrors `scripts/value-safety.mjs`. */
|
|
41
51
|
export const CREDENTIAL_WORDS = [
|
|
42
52
|
"token",
|
|
@@ -127,6 +137,8 @@ const LABELLED_VALUE_FIELDS = [...ENV_VALUE_FIELDS, "args", "text"];
|
|
|
127
137
|
const URL_PARAM_CONTAINS = /token|secret|password|passwd|signature|^sig$|key|auth|code|jwt|credential|bypass/i;
|
|
128
138
|
const URL_PARAM_SUFFIX = /(token|secret|password|passphrase|key|signature|sig|credential|auth|authorization|jwt)$/;
|
|
129
139
|
const URL_SHAPE = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
140
|
+
/** What makes a string text rather than one whole URL: whitespace, quotes, escapes, entities, Markdown. */
|
|
141
|
+
const TEXT = /[\s<>"'`\\]|&(?:#|[a-z]+;)|\]\(/i;
|
|
130
142
|
/** The fields of a header, cookie, or query rule that hold the matched or set value. */
|
|
131
143
|
const HEADER_RULE_VALUES = ["value", "values", "args"];
|
|
132
144
|
/** Keys whose absolute URL is a route or redirect destination. */
|
|
@@ -195,6 +207,48 @@ export function credentialName(name) {
|
|
|
195
207
|
function isRecord(value) {
|
|
196
208
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
197
209
|
}
|
|
210
|
+
/** A rule's test over one decision input; an input redaction altered passes it (#703). */
|
|
211
|
+
function decides(input, test) {
|
|
212
|
+
return typeof input === "string" && (redactionAltered(input) || test(input));
|
|
213
|
+
}
|
|
214
|
+
/** Value safety's own `@keys` placeholders, which no redaction of a vendor's key wrote. */
|
|
215
|
+
const PLACEHOLDER_KEYS = new WeakSet();
|
|
216
|
+
/**
|
|
217
|
+
* Whether redaction altered one of a record's keys, so no key-based decision
|
|
218
|
+
* about it can be read (#703). Parsed JSON keeps its keys; text the kit
|
|
219
|
+
* redacted whole and a result hook parsed after it (NDJSON, log streams) can
|
|
220
|
+
* carry the placeholder in one.
|
|
221
|
+
*/
|
|
222
|
+
function keysAltered(value) {
|
|
223
|
+
return !PLACEHOLDER_KEYS.has(value) && Object.keys(value).some(redactionAltered);
|
|
224
|
+
}
|
|
225
|
+
/** What may come before a URL's first placeholder when redaction took part of its scheme or authority. */
|
|
226
|
+
const SCHEME_PREFIX = /^(?:[a-z][a-z0-9+.-]*(?::\/{0,2})?)?$/i;
|
|
227
|
+
/**
|
|
228
|
+
* Whether redaction altered the scheme a whole-string URL decision reads
|
|
229
|
+
* (#703): the text is no longer URL-shaped, everything before its first
|
|
230
|
+
* placeholder could begin a URL, and more than placeholders follows it. A
|
|
231
|
+
* URL a vendor sends holds no whitespace.
|
|
232
|
+
*/
|
|
233
|
+
function alteredScheme(text) {
|
|
234
|
+
if (!redactionAltered(text) || URL_SHAPE.test(text) || /\s/.test(text))
|
|
235
|
+
return false;
|
|
236
|
+
const at = text.indexOf(REDACTED);
|
|
237
|
+
return SCHEME_PREFIX.test(text.slice(0, at)) && text.slice(at).replaceAll(REDACTED, "") !== "";
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Every place a parameter name can sit in a URL's path, query, or fragment:
|
|
241
|
+
* each run before an `=` since the last `/`, `?`, `&`, `#`, `;`, or `=`. A
|
|
242
|
+
* name redaction altered, or a separator it took, leaves the placeholder in
|
|
243
|
+
* one of them.
|
|
244
|
+
*/
|
|
245
|
+
function parameterNames(text) {
|
|
246
|
+
return text.split(/[/?&#;]/).flatMap((part) => part.split("=").slice(0, -1));
|
|
247
|
+
}
|
|
248
|
+
/** Whether a fragment carries a placeholder other than the one value safety writes for a withheld fragment. */
|
|
249
|
+
function alteredFragment(fragment) {
|
|
250
|
+
return redactionAltered(fragment) && fragment !== REDACTED;
|
|
251
|
+
}
|
|
198
252
|
/** A URL without userinfo, credential query parameters, or a credential-looking fragment; anything unparseable is withheld. */
|
|
199
253
|
function sanitizeUrl(value, credentials = true) {
|
|
200
254
|
let url;
|
|
@@ -224,8 +278,24 @@ function sanitizeUrl(value, credentials = true) {
|
|
|
224
278
|
// An ordinary URL comes back exactly as the vendor sent it.
|
|
225
279
|
return changed ? url.toString() : value;
|
|
226
280
|
}
|
|
227
|
-
|
|
228
|
-
|
|
281
|
+
// Embedded URLs in reviewed REST text lose credential-bearing queries and
|
|
282
|
+
// fragments. AWS and Google signing parameters are withheld globally. The
|
|
283
|
+
// destination scanner also drives generic write admission for copied placeholders.
|
|
284
|
+
/** An `=`, plain or encoded, as a signing parameter's name ends with one. */
|
|
285
|
+
const EQUALS = /=|%(?:25)*3d|&(?:amp(?:;|(?=#))|#0*38;?|#x0*26;?)*(?:#0*61;?|#x0*3d;?|equals;?)|\\u003d/iy;
|
|
286
|
+
const EQUALS_BEFORE = new RegExp(`(?:${EQUALS.source})$`, "i");
|
|
287
|
+
const AMP_REFERENCE = String.raw `&(?:amp(?:;|(?=#))|#0*38;?|#x0*26;?)*`;
|
|
288
|
+
/** Fixed ASCII name units, matched in source coordinates without decoding or rewriting the text. */
|
|
289
|
+
function signingName(text) {
|
|
290
|
+
return Array.from(text, (char) => {
|
|
291
|
+
const codes = [...new Set([char.toLowerCase().charCodeAt(0), char.toUpperCase().charCodeAt(0)])];
|
|
292
|
+
const hex = codes.map((code) => code.toString(16)).join("|");
|
|
293
|
+
const named = char === "-" ? "|(?:hyphen|dash);" : "";
|
|
294
|
+
return `(?:${char === "." ? "\\." : char}|%(?:25)*(?:${hex})|${AMP_REFERENCE}(?:#0*(?:${codes.join("|")});?|#x0*(?:${hex});?${named})|\\\\(?:u00|x)(?:${hex}))`;
|
|
295
|
+
}).join("");
|
|
296
|
+
}
|
|
297
|
+
/** Established signing names and withheld suffixes, retaining each unit's source spelling. */
|
|
298
|
+
const SIGNING_PARAM = new RegExp(`${signingName("x-")}(?:${signingName("amz")}|${signingName("goog")})${signingName("-")}(?:(?:${["signature", "credential", "security-token"].map(signingName).join("|")})(?:${EQUALS.source})|\\[redacted\\])`, "gi");
|
|
229
299
|
/**
|
|
230
300
|
* One encoded character at the start of link text: a percent escape, an HTML
|
|
231
301
|
* character reference, a `\u` or `\x` escape, or a Markdown backslash escape.
|
|
@@ -237,8 +307,6 @@ const TRAILING_ESCAPE = /(?:%(?:25){0,4}([0-9a-f]{2})|&(?:#0{0,6}(\d{1,7})|#x0{0
|
|
|
237
307
|
const ESCAPES = /%([0-9a-f]{2})|&(?:#(\d{1,7})|#x([0-9a-f]{1,6})|([a-z]{2,8}));?|\\u([0-9a-f]{4})|\\x([0-9a-f]{2})|\\(?=[^a-z0-9])/gi;
|
|
238
308
|
/** One slash however it is spelled, read forwards. */
|
|
239
309
|
const SLASH = /\/|\\\/|%(?:25){0,4}2f|�{0,6}47;?|�{0,6}2f;?|/?|\\u002f|\\x2f/iy;
|
|
240
|
-
/** An S3 endpoint host: global, regional, dual-stack, or accelerated. */
|
|
241
|
-
const S3_ENDPOINT = String.raw `s3(?:[.-](?:dualstack|accelerate|external-1|[a-z]{2}(?:-[a-z]{1,12}){1,3}-\d{1,2})){0,2}\.amazonaws\.com`;
|
|
242
310
|
const NAMED = {
|
|
243
311
|
amp: "&",
|
|
244
312
|
colon: ":",
|
|
@@ -259,10 +327,6 @@ const NAMED = {
|
|
|
259
327
|
rsqb: "]",
|
|
260
328
|
period: ".",
|
|
261
329
|
};
|
|
262
|
-
/** A host name's label character: the subdomain labels before a reviewed name are these alone. */
|
|
263
|
-
const LABEL = /[a-z0-9.-]/i;
|
|
264
|
-
/** The longest host name DNS allows: the bound on authority inspection. */
|
|
265
|
-
const MAX_HOST = 253;
|
|
266
330
|
/** The longest scheme read back from its `:`. */
|
|
267
331
|
const MAX_SCHEME = 32;
|
|
268
332
|
const SCHEME_CHAR = /[a-z0-9+.-]/i;
|
|
@@ -347,33 +411,6 @@ function authorityAt(text, at) {
|
|
|
347
411
|
start--;
|
|
348
412
|
return start < colon && (start === 0 || OPENS_LINK.test(text[start - 1])) ? start : -1;
|
|
349
413
|
}
|
|
350
|
-
/** Where a host's optional trailing dot and port end after `at`, or `-1` when a `:` holds no port (userinfo's password). */
|
|
351
|
-
function afterPort(text, at) {
|
|
352
|
-
if (text[at] === ".")
|
|
353
|
-
at++;
|
|
354
|
-
if (text[at] !== ":")
|
|
355
|
-
return at;
|
|
356
|
-
let digits = at + 1;
|
|
357
|
-
while (digits - at <= 5 && /\d/.test(text[digits] ?? ""))
|
|
358
|
-
digits++;
|
|
359
|
-
return digits > at + 1 ? digits : -1;
|
|
360
|
-
}
|
|
361
|
-
/**
|
|
362
|
-
* Whether a host or path segment ends at `at`: a slash, query, or fragment
|
|
363
|
-
* however spelled, or the link's end. Never another
|
|
364
|
-
* name character or an `@`: `file.notion.so@evil.com` names userinfo, and
|
|
365
|
-
* `prod-files-secure@other` and `prod-files-securex` are other segments.
|
|
366
|
-
*/
|
|
367
|
-
function boundary(text, at) {
|
|
368
|
-
if (at < 0)
|
|
369
|
-
return false;
|
|
370
|
-
if (at >= text.length)
|
|
371
|
-
return true;
|
|
372
|
-
if (endsLink(text.charCodeAt(at), text[at]))
|
|
373
|
-
return true;
|
|
374
|
-
SLASH.lastIndex = at;
|
|
375
|
-
return SLASH.test(text) || opener(text, at) !== undefined;
|
|
376
|
-
}
|
|
377
414
|
/** Whether the `#` at `at` belongs to a numeric character reference (`&`) rather than opening a fragment. */
|
|
378
415
|
function reference(text, at) {
|
|
379
416
|
return text.charCodeAt(at - 1) === 0x26 && /[0-9x]/i.test(text[at + 1] ?? "");
|
|
@@ -394,47 +431,21 @@ function endsValue(text, at) {
|
|
|
394
431
|
// `\/` is a JSON-escaped slash inside the value; any other backslash escapes what follows it.
|
|
395
432
|
return code === 0x5c && text.charCodeAt(at + 1) !== 0x2f;
|
|
396
433
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
let end = start;
|
|
413
|
-
while (end < text.length && end - start <= MAX_HOST && LABEL.test(text[end]))
|
|
414
|
-
end++;
|
|
415
|
-
if (end === start || end - start > MAX_HOST)
|
|
416
|
-
return undefined;
|
|
417
|
-
const port = afterPort(text, end);
|
|
418
|
-
if (!boundary(text, port))
|
|
419
|
-
return undefined;
|
|
420
|
-
const name = text.slice(start, end).toLowerCase().replace(/\.$/, "");
|
|
421
|
-
if (matcher.domains.some((domain) => name === domain || name.endsWith(`.${domain}`)))
|
|
422
|
-
return end;
|
|
423
|
-
if (matcher.buckets.some((bucket) => name.startsWith(`${bucket}.`) && matcher.endpoint.test(name.slice(bucket.length + 1)))) {
|
|
424
|
-
return end;
|
|
425
|
-
}
|
|
426
|
-
if (!matcher.endpoint.test(name))
|
|
427
|
-
return undefined;
|
|
428
|
-
SLASH.lastIndex = port;
|
|
429
|
-
if (!SLASH.test(text))
|
|
430
|
-
return undefined;
|
|
431
|
-
const segment = SLASH.lastIndex;
|
|
432
|
-
for (const bucket of matcher.buckets) {
|
|
433
|
-
const after = segment + bucket.length;
|
|
434
|
-
if (text.slice(segment, after).toLowerCase() === bucket && boundary(text, after))
|
|
435
|
-
return after;
|
|
436
|
-
}
|
|
437
|
-
return undefined;
|
|
434
|
+
/**
|
|
435
|
+
* Where a word at `start` holds a placeholder in place of a link's scheme
|
|
436
|
+
* and host (`[redacted]/pkg?token=…`, `https:[redacted]…`), or `-1`:
|
|
437
|
+
* redaction took the authority that makes text a link (#703). A Markdown
|
|
438
|
+
* label (`[redacted](…)`) is not one; its destination is read as a link.
|
|
439
|
+
*/
|
|
440
|
+
function markerLed(text, start) {
|
|
441
|
+
let at = start;
|
|
442
|
+
while (at < text.length && at - start <= MAX_SCHEME + 3 && /[a-z0-9+.:/-]/i.test(text[at]))
|
|
443
|
+
at++;
|
|
444
|
+
return text.startsWith(REDACTED, at) &&
|
|
445
|
+
text[at + REDACTED.length] !== "(" &&
|
|
446
|
+
SCHEME_PREFIX.test(text.slice(start, at))
|
|
447
|
+
? at
|
|
448
|
+
: -1;
|
|
438
449
|
}
|
|
439
450
|
/** The authority immediately after a destination's scheme and slashes, however escaped. */
|
|
440
451
|
function destinationHost(text, start) {
|
|
@@ -472,11 +483,9 @@ function markdownStart(text, at) {
|
|
|
472
483
|
}
|
|
473
484
|
/**
|
|
474
485
|
* Read a destination once. Admission ends at its actual CommonMark close,
|
|
475
|
-
* counting balanced and escaped parentheses
|
|
476
|
-
* its separate fail-closed span through whitespace, a quote, or an angle
|
|
477
|
-
* bracket, since parentheses can also be part of a signed value.
|
|
486
|
+
* counting balanced and escaped parentheses, before inspecting its query.
|
|
478
487
|
*/
|
|
479
|
-
function linkSpan(text, start, host, inline
|
|
488
|
+
function linkSpan(text, start, host, inline) {
|
|
480
489
|
let depth = 0;
|
|
481
490
|
let escaped = false;
|
|
482
491
|
let cut = -1;
|
|
@@ -496,7 +505,7 @@ function linkSpan(text, start, host, inline, redact) {
|
|
|
496
505
|
else if (code === 0x29 && depth-- === 0)
|
|
497
506
|
break;
|
|
498
507
|
}
|
|
499
|
-
if (
|
|
508
|
+
if (end < host || cut >= 0)
|
|
500
509
|
continue;
|
|
501
510
|
if (code === 0x3f || (code === 0x23 && !reference(text, end))) {
|
|
502
511
|
cut = end;
|
|
@@ -510,27 +519,26 @@ function linkSpan(text, start, host, inline, redact) {
|
|
|
510
519
|
}
|
|
511
520
|
}
|
|
512
521
|
}
|
|
513
|
-
if (redact && cut >= 0) {
|
|
514
|
-
while (end < text.length && !endsLink(text.charCodeAt(end), text[end]))
|
|
515
|
-
end++;
|
|
516
|
-
}
|
|
517
522
|
return { cut, open, end };
|
|
518
523
|
}
|
|
519
524
|
/**
|
|
520
|
-
*
|
|
521
|
-
*
|
|
522
|
-
*
|
|
523
|
-
* redaction choose their own span boundary, and both stay linear.
|
|
525
|
+
* Visit each destination once, respecting its CommonMark close and encoded
|
|
526
|
+
* delimiters. Redaction (`failClosed`) also reads a link whose scheme and
|
|
527
|
+
* host redaction took (#703); admission never does.
|
|
524
528
|
*/
|
|
525
|
-
function*
|
|
526
|
-
const
|
|
527
|
-
// The first URL delimiter in the current text run also protects relative
|
|
528
|
-
// paths/queries such as /proxy/https://… and image?url=//… .
|
|
529
|
-
let firstReference = -1;
|
|
529
|
+
function* linkSpans(text, failClosed = false) {
|
|
530
|
+
const markers = failClosed && redactionAltered(text);
|
|
530
531
|
for (let at = 0; at < text.length; at++) {
|
|
531
532
|
const char = text[at];
|
|
532
|
-
|
|
533
|
-
|
|
533
|
+
const marker = markers && (at === 0 || OPENS_LINK.test(text[at - 1])) ? markerLed(text, at) : -1;
|
|
534
|
+
if (marker >= 0) {
|
|
535
|
+
// A placeholder opening a word stands where a link's scheme and host may have been.
|
|
536
|
+
// Its run is read once, query or not, and the scan resumes after it.
|
|
537
|
+
const host = marker + REDACTED.length;
|
|
538
|
+
const span = linkSpan(text, host, host, false);
|
|
539
|
+
const start = at;
|
|
540
|
+
at = Math.max(at, span.end - 1);
|
|
541
|
+
yield { ...span, start, host };
|
|
534
542
|
continue;
|
|
535
543
|
}
|
|
536
544
|
if (char === "]" && text[at + 1] === "(") {
|
|
@@ -538,19 +546,10 @@ function* hostLinks(text, hosts, redact) {
|
|
|
538
546
|
if (destination !== undefined) {
|
|
539
547
|
const angle = text[destination] === "<";
|
|
540
548
|
const start = destination + (angle ? 1 : 0);
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
// Host policy must still attribute the leading authority to the vendor.
|
|
544
|
-
const host = matcher
|
|
545
|
-
? authority === undefined
|
|
546
|
-
? undefined
|
|
547
|
-
: fileHost(text, authority, matcher)
|
|
548
|
-
: (authority ?? start);
|
|
549
|
-
const span = linkSpan(text, start, host, !angle, redact);
|
|
549
|
+
const host = destinationHost(text, start) ?? start;
|
|
550
|
+
const span = linkSpan(text, start, host, !angle);
|
|
550
551
|
at = Math.max(destination, span.end) - 1;
|
|
551
|
-
|
|
552
|
-
if (host !== undefined)
|
|
553
|
-
yield span;
|
|
552
|
+
yield { ...span, start, host };
|
|
554
553
|
continue;
|
|
555
554
|
}
|
|
556
555
|
}
|
|
@@ -561,37 +560,16 @@ function* hostLinks(text, hosts, redact) {
|
|
|
561
560
|
SLASH.lastIndex = next;
|
|
562
561
|
if (SLASH.test(text)) {
|
|
563
562
|
const authority = SLASH.lastIndex;
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
const host = matcher ? fileHost(text, authority, matcher) : authority;
|
|
570
|
-
const span = linkSpan(text, authority, host, false, redact);
|
|
571
|
-
at = span.end - 1;
|
|
572
|
-
firstReference = -1;
|
|
573
|
-
if (host !== undefined)
|
|
574
|
-
yield span;
|
|
575
|
-
continue;
|
|
576
|
-
}
|
|
563
|
+
const begin = authorityAt(text, authority);
|
|
564
|
+
const span = linkSpan(text, authority, authority, false);
|
|
565
|
+
const start = begin >= 0 ? begin : at;
|
|
566
|
+
at = span.end - 1;
|
|
567
|
+
yield { ...span, start, host: authority };
|
|
577
568
|
}
|
|
578
|
-
if (firstReference < 0)
|
|
579
|
-
firstReference = at;
|
|
580
569
|
}
|
|
581
570
|
}
|
|
582
|
-
if (firstReference < 0 && (char === ":" || char === "?" || (char === "#" && !reference(text, at)))) {
|
|
583
|
-
firstReference = at;
|
|
584
|
-
}
|
|
585
571
|
}
|
|
586
572
|
}
|
|
587
|
-
/**
|
|
588
|
-
* `text` with every pre-signed link withheld: at one of `hosts` (Notion's
|
|
589
|
-
* file storage, say), its whole query and fragment; anywhere, a signing
|
|
590
|
-
* parameter's value. Everything else comes back byte for byte.
|
|
591
|
-
*/
|
|
592
|
-
export function withholdSignedLinks(text, hosts) {
|
|
593
|
-
return withholdSigningParams(withholdLinkQueries(text, hosts));
|
|
594
|
-
}
|
|
595
573
|
/** A credential-named parameter in a URL, rather than an assignment in prose. */
|
|
596
574
|
function credentialQuery(text) {
|
|
597
575
|
let decoded = decodeLinkText(text, false);
|
|
@@ -605,8 +583,12 @@ function credentialQuery(text) {
|
|
|
605
583
|
decoded = next;
|
|
606
584
|
}
|
|
607
585
|
const fragment = decoded.indexOf("#");
|
|
608
|
-
if (fragment >= 0
|
|
609
|
-
|
|
586
|
+
if (fragment >= 0) {
|
|
587
|
+
const words = decodeLinkText(decoded.slice(fragment + 1));
|
|
588
|
+
// A fragment redaction altered cannot be read (#703).
|
|
589
|
+
if (/token|secret|key|code|auth/i.test(words) || alteredFragment(words))
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
610
592
|
const query = fragment < 0 ? decoded : decoded.slice(0, fragment);
|
|
611
593
|
for (let at = query[0] === "?" ? 1 : 0; at < query.length;) {
|
|
612
594
|
const next = query.indexOf("&", at);
|
|
@@ -614,6 +596,10 @@ function credentialQuery(text) {
|
|
|
614
596
|
const pair = decodeLinkText(query.slice(at, end));
|
|
615
597
|
const equals = pair.indexOf("=");
|
|
616
598
|
at = end + 1;
|
|
599
|
+
// A component redaction altered with no readable name (its name, or its `=`, taken) may be a
|
|
600
|
+
// credential parameter's value, however encoded (#703).
|
|
601
|
+
if (pair !== REDACTED && redactionAltered(equals < 0 ? pair : pair.slice(0, equals)))
|
|
602
|
+
return true;
|
|
617
603
|
if (equals < 0)
|
|
618
604
|
continue;
|
|
619
605
|
const name = pair.slice(0, equals);
|
|
@@ -625,42 +611,62 @@ function credentialQuery(text) {
|
|
|
625
611
|
return false;
|
|
626
612
|
}
|
|
627
613
|
/** Remove a link's entire query/fragment, keeping its surrounding syntax. */
|
|
628
|
-
function withholdLinkQueries(text
|
|
614
|
+
function withholdLinkQueries(text) {
|
|
629
615
|
let out = "";
|
|
630
616
|
let copied = 0;
|
|
631
|
-
for (const { cut, open, end } of
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
if (!
|
|
617
|
+
for (const { cut, open, end, start, host } of linkSpans(text, true)) {
|
|
618
|
+
// A parameter name redaction altered cannot be read, so the whole link goes (#703).
|
|
619
|
+
const whole = alteredLink(text.slice(host, end));
|
|
620
|
+
if (!whole && (cut < 0 || !credentialQuery(text.slice(cut, end))))
|
|
635
621
|
continue;
|
|
622
|
+
const from = Math.max(whole ? start : cut, copied);
|
|
623
|
+
// A reference's final semicolon belongs to the withheld query, not the surrounding prose.
|
|
624
|
+
// Search only this disjoint span, once, even when it ends in a long punctuation run.
|
|
625
|
+
const ampersand = text.slice(from, end).lastIndexOf("&");
|
|
626
|
+
const referenceEnd = ampersand < 0 ? -1 : characterReferenceEnd(text, from + ampersand);
|
|
636
627
|
let keep = end;
|
|
637
|
-
while (keep >
|
|
628
|
+
while (keep > from + 1 && TRAILING.test(text[keep - 1])) {
|
|
638
629
|
// A signing pass may already have replaced the final parameter. Its
|
|
639
630
|
// closing bracket belongs to the value, not the surrounding Markdown.
|
|
640
|
-
if (text.endsWith(REDACTED, keep))
|
|
631
|
+
if (keep === referenceEnd || text.endsWith(REDACTED, keep))
|
|
641
632
|
break;
|
|
642
633
|
keep--;
|
|
643
634
|
}
|
|
644
635
|
// A query with nothing in it.
|
|
645
|
-
if (keep <=
|
|
636
|
+
if (keep <= from + 1)
|
|
646
637
|
continue;
|
|
647
|
-
out += `${text.slice(copied,
|
|
638
|
+
out += `${text.slice(copied, from)}${whole ? "" : open}${REDACTED}`;
|
|
648
639
|
copied = keep;
|
|
649
640
|
}
|
|
650
641
|
return copied === 0 ? text : out + text.slice(copied);
|
|
651
642
|
}
|
|
643
|
+
/** Whether redaction altered a parameter name in a link's own path, query, or fragment (#703). */
|
|
644
|
+
function alteredLink(link) {
|
|
645
|
+
return redactionAltered(link) && parameterNames(decodeLinkText(link)).some(redactionAltered);
|
|
646
|
+
}
|
|
652
647
|
function withholdSigningParams(anchored) {
|
|
653
648
|
let out = "";
|
|
654
649
|
let copied = 0;
|
|
655
650
|
SIGNING_PARAM.lastIndex = 0;
|
|
656
|
-
|
|
657
|
-
|
|
651
|
+
let signing = SIGNING_PARAM.exec(anchored);
|
|
652
|
+
let altered = alteredParameter(anchored, 0);
|
|
653
|
+
while (signing || altered) {
|
|
654
|
+
const named = signing ? signing.index + signing[0].length : -1;
|
|
655
|
+
const alteredStart = altered?.start ?? -1;
|
|
656
|
+
const boundary = altered?.value ?? -1;
|
|
657
|
+
// A complete altered-name boundary takes precedence over its withheld family suffix.
|
|
658
|
+
const inferred = alteredStart >= 0 && (named < 0 || alteredStart < signing.index || (alteredStart < named && boundary > named));
|
|
659
|
+
const start = inferred ? alteredStart : named;
|
|
658
660
|
// Skip an existing complete placeholder, but still remove any suffix.
|
|
659
661
|
const already = anchored.startsWith(REDACTED, start);
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
662
|
+
const from = inferred && altered?.value !== undefined ? altered.value : already ? start + REDACTED.length : start;
|
|
663
|
+
const end = valueEnd(anchored, from, inferred && altered?.value === undefined);
|
|
664
|
+
// Resume both scans after this value, as one scan would.
|
|
663
665
|
SIGNING_PARAM.lastIndex = end;
|
|
666
|
+
if (named >= 0 && named <= end)
|
|
667
|
+
signing = SIGNING_PARAM.exec(anchored);
|
|
668
|
+
if (altered && altered.start <= end)
|
|
669
|
+
altered = alteredParameter(anchored, end);
|
|
664
670
|
if (end === start || (already && end === start + REDACTED.length))
|
|
665
671
|
continue;
|
|
666
672
|
out += `${anchored.slice(copied, start)}${REDACTED}`;
|
|
@@ -668,18 +674,79 @@ function withholdSigningParams(anchored) {
|
|
|
668
674
|
}
|
|
669
675
|
return copied === 0 ? anchored : out + anchored.slice(copied);
|
|
670
676
|
}
|
|
677
|
+
const NAME_CHAR = /[\w.%~;-]/;
|
|
678
|
+
const NAME_ESCAPE = new RegExp(Array.from("abcdefghijklmnopqrstuvwxyz0123456789_.~-", signingName).join("|"), "iy");
|
|
679
|
+
/** Where a parameter's value ends, reading placeholders and character references whole (#703). */
|
|
680
|
+
function valueEnd(text, at, inferred) {
|
|
681
|
+
while (at < text.length) {
|
|
682
|
+
const reference = Math.max(characterReferenceEnd(text, at, inferred), bareReferenceEnd(text, at, inferred));
|
|
683
|
+
if (text.startsWith(REDACTED, at))
|
|
684
|
+
at += REDACTED.length;
|
|
685
|
+
else if (reference > at)
|
|
686
|
+
at = reference;
|
|
687
|
+
else if (endsValue(text, at))
|
|
688
|
+
break;
|
|
689
|
+
else
|
|
690
|
+
at++;
|
|
691
|
+
}
|
|
692
|
+
return at;
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* The next placeholder that may hide a signing parameter, at or after `from`,
|
|
696
|
+
* and its value boundary when a name is recognized (#703). Redaction may have taken a signing name, its
|
|
697
|
+
* `=`, and the start of its value, so a placeholder followed by any of a
|
|
698
|
+
* value's base64 or base64url characters (`+`, `/`, `=`, `-`, `_` included)
|
|
699
|
+
* or `%` escapes goes with them, however short. References count when they
|
|
700
|
+
* spell credential data or an ampersand, including a short numeric prefix
|
|
701
|
+
* when no semicolon ends the reference. One inside a name ending at an
|
|
702
|
+
* encoded `=` (`=`, `\u003d`) gives that value's start. A placeholder
|
|
703
|
+
* right after an `=` with nothing readable after it is a withheld value.
|
|
704
|
+
* Each placeholder is read once, resuming after its run.
|
|
705
|
+
*/
|
|
706
|
+
function alteredParameter(text, from) {
|
|
707
|
+
for (let at = text.indexOf(REDACTED, from); at >= 0;) {
|
|
708
|
+
const tail = splitRemainder(text, at, endsValue);
|
|
709
|
+
// Read a possible name before inferring a value remainder: a name's encoded `=` or dash
|
|
710
|
+
// can otherwise look like the end of the remainder. A leading partial reference belongs
|
|
711
|
+
// to the name too; the placeholder may have removed its opening characters.
|
|
712
|
+
if (!EQUALS_BEFORE.test(text.slice(Math.max(0, at - 64), at))) {
|
|
713
|
+
// The tail already read any earlier placeholders. Only its last one can lead a
|
|
714
|
+
// name beyond that tail; keep the redaction start so no earlier data is exposed.
|
|
715
|
+
let end = text.lastIndexOf(REDACTED, tail.end - REDACTED.length) + REDACTED.length;
|
|
716
|
+
while (end < text.length) {
|
|
717
|
+
EQUALS.lastIndex = end;
|
|
718
|
+
if (EQUALS.test(text))
|
|
719
|
+
return { start: at, value: EQUALS.lastIndex };
|
|
720
|
+
NAME_ESCAPE.lastIndex = end;
|
|
721
|
+
// A later placeholder gets its own scan; never look ahead across it twice.
|
|
722
|
+
if (text.startsWith(REDACTED, end))
|
|
723
|
+
break;
|
|
724
|
+
else if (NAME_CHAR.test(text[end]))
|
|
725
|
+
end++;
|
|
726
|
+
else if (NAME_ESCAPE.test(text))
|
|
727
|
+
end = NAME_ESCAPE.lastIndex;
|
|
728
|
+
else {
|
|
729
|
+
const reference = bareReferenceEnd(text, end, true);
|
|
730
|
+
if (reference === end)
|
|
731
|
+
break;
|
|
732
|
+
end = reference;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
if (tail.readable)
|
|
737
|
+
return { start: at };
|
|
738
|
+
at = text.indexOf(REDACTED, Math.max(tail.end, at + REDACTED.length));
|
|
739
|
+
}
|
|
740
|
+
return undefined;
|
|
741
|
+
}
|
|
671
742
|
/**
|
|
672
743
|
* Reviewed REST text policy: only credential-bearing URL queries/fragments
|
|
673
744
|
* change. Ordinary links, including external proxies, retain their bytes.
|
|
674
|
-
*
|
|
745
|
+
* The established AWS/Google signing names are withheld in their source spelling anywhere.
|
|
675
746
|
*/
|
|
676
747
|
export function withholdEmbeddedUrls(text) {
|
|
677
748
|
return withholdSigningParams(withholdLinkQueries(text));
|
|
678
749
|
}
|
|
679
|
-
/** Every string inside a JSON value through `withholdSignedLinks`; unchanged values keep their identity. */
|
|
680
|
-
export function withholdSignedLinksIn(value, hosts, depth = 0) {
|
|
681
|
-
return mapStrings(value, (text) => withholdSignedLinks(text, hosts), depth);
|
|
682
|
-
}
|
|
683
750
|
/** Apply the reviewed text policy to a selected value or vendor error body. */
|
|
684
751
|
export function withholdEmbeddedUrlsIn(value) {
|
|
685
752
|
return mapStrings(value, withholdEmbeddedUrls, 0);
|
|
@@ -732,19 +799,20 @@ function decodeLinkText(text, percent = true) {
|
|
|
732
799
|
* Only the link's own destination is read: a Markdown link ends at its
|
|
733
800
|
* closing `)`, so `[f](…?a=1)[redacted]` is ordinary text after it.
|
|
734
801
|
* The word alone (`unredacted`) or the placeholder away from a file link is
|
|
735
|
-
* a person's text.
|
|
736
|
-
* to match that policy, preserving Notion's external-proxy boundary.
|
|
802
|
+
* a person's text. Every destination uses the same policy regardless of host.
|
|
737
803
|
*/
|
|
738
|
-
export function carriesWithheldLink(value
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
const inspect = (text) => {
|
|
742
|
-
for (const { cut, end } of hostLinks(text, hosts, false)) {
|
|
804
|
+
export function carriesWithheldLink(value) {
|
|
805
|
+
return carriesString(value, (text) => {
|
|
806
|
+
for (const { cut, end } of linkSpans(text)) {
|
|
743
807
|
if (cut >= 0 && decodeLinkText(text.slice(cut, end)).includes(REDACTED))
|
|
744
808
|
return true;
|
|
745
809
|
}
|
|
746
810
|
return false;
|
|
747
|
-
};
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
function carriesString(value, inspect) {
|
|
814
|
+
const pending = [{ value }];
|
|
815
|
+
const ancestors = new WeakSet();
|
|
748
816
|
// JSON structure is bounded by the admitted request's bytes. Visit every key
|
|
749
817
|
// and value iteratively, without imposing a smaller node/depth limit. Reject
|
|
750
818
|
// cycles from direct callers; repeated non-cyclic references remain valid.
|
|
@@ -760,7 +828,7 @@ export function carriesWithheldLink(value, hosts) {
|
|
|
760
828
|
continue;
|
|
761
829
|
}
|
|
762
830
|
if (ancestors.has(item)) {
|
|
763
|
-
throw new ConnectorCallError("invalid_args", "Not sent:
|
|
831
|
+
throw new ConnectorCallError("invalid_args", "Not sent: value admission cannot inspect cyclic JSON.");
|
|
764
832
|
}
|
|
765
833
|
ancestors.add(item);
|
|
766
834
|
pending.push({ value: item, leave: true });
|
|
@@ -779,6 +847,25 @@ export function carriesWithheldLink(value, hosts) {
|
|
|
779
847
|
}
|
|
780
848
|
return false;
|
|
781
849
|
}
|
|
850
|
+
/** Whole marker values and withheld destinations are refused; paths and queries also refuse containment. */
|
|
851
|
+
export function refuseWithheldValues(value, title, containment = false) {
|
|
852
|
+
if (!carriesString(value, (text) => {
|
|
853
|
+
const decoded = decodeLinkText(text);
|
|
854
|
+
if (containment)
|
|
855
|
+
return redactionAltered(text) || redactionAltered(decoded);
|
|
856
|
+
if (decoded === REDACTED)
|
|
857
|
+
return true;
|
|
858
|
+
for (const candidate of [text, decoded])
|
|
859
|
+
for (const { start, end } of linkSpans(candidate))
|
|
860
|
+
if (redactionAltered(decodeLinkText(candidate.slice(start, end))))
|
|
861
|
+
return true;
|
|
862
|
+
return false;
|
|
863
|
+
}))
|
|
864
|
+
return;
|
|
865
|
+
throw new ConnectorCallError("invalid_args", containment
|
|
866
|
+
? `Not sent: ${title} path or query contains a withheld placeholder ([redacted]). Supply the original path/query value or omit the parameter.`
|
|
867
|
+
: `Not sent: ${title} arguments contain a whole withheld value ([redacted]) or a withheld link. Supply the original value/link or omit the field. Longer prose may mention the marker.`);
|
|
868
|
+
}
|
|
782
869
|
/** Refuse a withheld destination copied into customer data, before any dispatch. */
|
|
783
870
|
export function refuseWithheldLinks(value, title) {
|
|
784
871
|
if (!carriesWithheldLink(value))
|
|
@@ -867,24 +954,52 @@ function labelsOf(value) {
|
|
|
867
954
|
labels.push(target["key"]);
|
|
868
955
|
return labels;
|
|
869
956
|
}
|
|
870
|
-
/**
|
|
957
|
+
/**
|
|
958
|
+
* Whether a record's label names a credential, so its value or args are one.
|
|
959
|
+
* A label redaction altered, or keys that may hide one, does too (#703).
|
|
960
|
+
*/
|
|
871
961
|
function credentialLabelled(value) {
|
|
872
|
-
|
|
962
|
+
if (!isRecord(value))
|
|
963
|
+
return false;
|
|
964
|
+
const target = value["target"];
|
|
965
|
+
return (keysAltered(value) ||
|
|
966
|
+
(isRecord(target) && keysAltered(target)) ||
|
|
967
|
+
labelsOf(value).some((label) => decides(label, credentialName)));
|
|
968
|
+
}
|
|
969
|
+
/** A record typed as a secret (`secret_text`); a type redaction altered may have been one (#703). */
|
|
970
|
+
function typedSecret(value) {
|
|
971
|
+
return keysAltered(value) || decides(value["type"], (type) => /secret/i.test(type));
|
|
972
|
+
}
|
|
973
|
+
/** A list item typed as an environment variable; a type redaction altered may have been one (#703). */
|
|
974
|
+
function envTyped(value) {
|
|
975
|
+
if (!isRecord(value))
|
|
976
|
+
return false;
|
|
977
|
+
return keysAltered(value) || decides(String(value["type"]), (type) => /env/i.test(type));
|
|
873
978
|
}
|
|
874
|
-
/** A header, cookie, or query rule (`request.headers`, `header`, `cookie`, `query`, …). */
|
|
979
|
+
/** A header, cookie, or query rule (`request.headers`, `header`, `cookie`, `query`, …); likewise (#703). */
|
|
875
980
|
function headerLike(value) {
|
|
876
|
-
return isRecord(value) &&
|
|
981
|
+
return isRecord(value) && (keysAltered(value) || decides(value["type"], (type) => /header|cookie|query/i.test(type)));
|
|
877
982
|
}
|
|
878
983
|
/** A route-like object: its `headers` are set on requests or responses. */
|
|
879
984
|
function routeLike(value) {
|
|
880
|
-
return ["src", "source", "dest", "destination"].some((key) => key in value);
|
|
985
|
+
return keysAltered(value) || ["src", "source", "dest", "destination"].some((key) => key in value);
|
|
881
986
|
}
|
|
882
987
|
/** Every value of a headers map, or of a list of `{ key, value }` header pairs, withheld. */
|
|
883
988
|
function headerValues(value) {
|
|
884
989
|
if (isRecord(value))
|
|
885
990
|
return Object.fromEntries(Object.keys(value).map((name) => [name, REDACTED]));
|
|
886
991
|
if (Array.isArray(value)) {
|
|
887
|
-
|
|
992
|
+
// A pair whose keys redaction altered may hold its value under any of them (#703).
|
|
993
|
+
return value.map((item) => !isRecord(item)
|
|
994
|
+
? item
|
|
995
|
+
: keysAltered(item)
|
|
996
|
+
? Object.fromEntries(Object.entries(item).map(([key, entry]) => [
|
|
997
|
+
key,
|
|
998
|
+
key === "value" || redactionAltered(key) ? withheld(entry) : entry,
|
|
999
|
+
]))
|
|
1000
|
+
: "value" in item
|
|
1001
|
+
? { ...item, value: REDACTED }
|
|
1002
|
+
: item);
|
|
888
1003
|
}
|
|
889
1004
|
return value;
|
|
890
1005
|
}
|
|
@@ -959,7 +1074,7 @@ function applyPath(body, raw, transform) {
|
|
|
959
1074
|
if (!Array.isArray(current))
|
|
960
1075
|
return current;
|
|
961
1076
|
return current.map((item) => {
|
|
962
|
-
if (segment === "[?env]" &&
|
|
1077
|
+
if (segment === "[?env]" && !envTyped(item))
|
|
963
1078
|
return item;
|
|
964
1079
|
if (segment === "[?credential]" && !credentialLabelled(item))
|
|
965
1080
|
return item;
|
|
@@ -979,7 +1094,9 @@ function applyPath(body, raw, transform) {
|
|
|
979
1094
|
if (segment === "@keys") {
|
|
980
1095
|
if (!isRecord(current))
|
|
981
1096
|
return current;
|
|
982
|
-
|
|
1097
|
+
const keyed = Object.fromEntries(Object.values(current).map((item, position) => [`${REDACTED} ${position + 1}`, item]));
|
|
1098
|
+
PLACEHOLDER_KEYS.add(keyed);
|
|
1099
|
+
return keyed;
|
|
983
1100
|
}
|
|
984
1101
|
if (!isRecord(current) || !Object.hasOwn(current, segment))
|
|
985
1102
|
return current;
|
|
@@ -1003,7 +1120,8 @@ function scrubEnv(value, depth) {
|
|
|
1003
1120
|
if (depth > 32)
|
|
1004
1121
|
return REDACTED;
|
|
1005
1122
|
if (Array.isArray(value)) {
|
|
1006
|
-
|
|
1123
|
+
// A `NAME=value` string, or one whose name and `=` redaction may have taken (#703).
|
|
1124
|
+
return value.map((item) => typeof item === "string" && (item.includes("=") || redactionAltered(item)) ? REDACTED : scrubEnv(item, depth + 1));
|
|
1007
1125
|
}
|
|
1008
1126
|
if (!isRecord(value))
|
|
1009
1127
|
return value;
|
|
@@ -1041,6 +1159,34 @@ function scrubValues(value, depth) {
|
|
|
1041
1159
|
}
|
|
1042
1160
|
return out;
|
|
1043
1161
|
}
|
|
1162
|
+
/** Every rule's paths and withheld fields: the rule for an object whose type redaction altered (#703). */
|
|
1163
|
+
const ANY_RULE = new WeakMap();
|
|
1164
|
+
function anyRule(resources) {
|
|
1165
|
+
let rule = ANY_RULE.get(resources);
|
|
1166
|
+
if (!rule) {
|
|
1167
|
+
const paths = Object.values(resources.rules).flatMap((each) => [...(each.paths ?? []), ...(each.withheld ?? [])]);
|
|
1168
|
+
rule = { reason: "A type redaction altered may be any reviewed resource.", paths: [...new Set(paths)] };
|
|
1169
|
+
ANY_RULE.set(resources, rule);
|
|
1170
|
+
}
|
|
1171
|
+
return rule;
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* What a record's type discriminator selects. A type redaction altered, or
|
|
1175
|
+
* one an altered key may hide, selects every rule at once (#703).
|
|
1176
|
+
*/
|
|
1177
|
+
function typed(record, key, rules, any) {
|
|
1178
|
+
const type = record[key];
|
|
1179
|
+
if (redactionAltered(type) || (type === undefined && keysAltered(record)))
|
|
1180
|
+
return any();
|
|
1181
|
+
return typeof type === "string" && Object.hasOwn(rules, type) ? rules[type] : undefined;
|
|
1182
|
+
}
|
|
1183
|
+
/** What the discriminator of the object an event carries (`data.object`) selects, for its partial copies. */
|
|
1184
|
+
function typedInner(record, key, rules, any) {
|
|
1185
|
+
const inner = record[key];
|
|
1186
|
+
if (isRecord(inner))
|
|
1187
|
+
return typed(inner, key, rules, any);
|
|
1188
|
+
return inner === undefined && keysAltered(record) ? any() : undefined;
|
|
1189
|
+
}
|
|
1044
1190
|
/**
|
|
1045
1191
|
* Apply resource rules to every discriminated object in a body, innermost
|
|
1046
1192
|
* first, and to the partial copies events carry beside one. Records the
|
|
@@ -1054,10 +1200,9 @@ function resourcePass(value, resources, verbatim, depth) {
|
|
|
1054
1200
|
if (!isRecord(value))
|
|
1055
1201
|
return value;
|
|
1056
1202
|
let out = Object.fromEntries(Object.entries(value).map(([key, item]) => [key, resourcePass(item, resources, verbatim, depth + 1)]));
|
|
1057
|
-
const
|
|
1203
|
+
const any = () => anyRule(resources);
|
|
1058
1204
|
// An event's `previous_attributes` is a partial copy of `data.object`: every reviewed path applies to it.
|
|
1059
|
-
const
|
|
1060
|
-
const innerRule = isRecord(inner) ? ruleFor(inner[resources.key]) : undefined;
|
|
1205
|
+
const innerRule = typedInner(value, resources.key, resources.rules, any);
|
|
1061
1206
|
if (innerRule) {
|
|
1062
1207
|
for (const partial of resources.partials ?? []) {
|
|
1063
1208
|
let copy = out[partial];
|
|
@@ -1068,7 +1213,7 @@ function resourcePass(value, resources, verbatim, depth) {
|
|
|
1068
1213
|
out = { ...out, [partial]: copy };
|
|
1069
1214
|
}
|
|
1070
1215
|
}
|
|
1071
|
-
const rule =
|
|
1216
|
+
const rule = typed(value, resources.key, resources.rules, any);
|
|
1072
1217
|
if (!rule)
|
|
1073
1218
|
return out;
|
|
1074
1219
|
for (const path of rule.paths ?? [])
|
|
@@ -1101,22 +1246,33 @@ function heuristic(value, exempt) {
|
|
|
1101
1246
|
// generic names in customer data must retain their original bytes.
|
|
1102
1247
|
if (!exempt.reviewedUrls)
|
|
1103
1248
|
return URL_SHAPE.test(current) ? sanitizeUrl(current) : current;
|
|
1104
|
-
|
|
1105
|
-
|
|
1249
|
+
// A placeholder's own brackets are not Markdown; a URL whose scheme redaction
|
|
1250
|
+
// altered may still carry userinfo, so it is withheld whole (#703).
|
|
1251
|
+
const altered = redactionAltered(current);
|
|
1252
|
+
if (TEXT.test(altered ? current.replaceAll(REDACTED, "") : current))
|
|
1253
|
+
return current;
|
|
1254
|
+
if (altered && alteredScheme(current))
|
|
1255
|
+
return REDACTED;
|
|
1256
|
+
return URL_SHAPE.test(current) ? sanitizeUrl(current, false) : current;
|
|
1106
1257
|
}
|
|
1107
1258
|
if (Array.isArray(current))
|
|
1108
1259
|
return current.map((item) => walk(item, trail, parent, depth + 1));
|
|
1109
1260
|
if (!isRecord(current))
|
|
1110
1261
|
return current;
|
|
1111
|
-
const labelled = credentialLabelled(current) || (
|
|
1112
|
-
const
|
|
1262
|
+
const labelled = credentialLabelled(current) || typedSecret(current);
|
|
1263
|
+
const placeholders = PLACEHOLDER_KEYS.has(current);
|
|
1264
|
+
const destination = parent === "delivery" || "endpoint" in current || "deliveryFormat" in current || keysAltered(current);
|
|
1113
1265
|
const out = {};
|
|
1114
1266
|
const verbatim = exempt.verbatim.get(current);
|
|
1115
1267
|
for (const [key, item] of Object.entries(current)) {
|
|
1116
1268
|
const path = [...trail, key];
|
|
1117
1269
|
const normalized = normalizedName(key);
|
|
1118
1270
|
const review = Object.hasOwn(exempt.fields, key) ? exempt.fields[key] : undefined;
|
|
1119
|
-
if (
|
|
1271
|
+
if (!placeholders && redactionAltered(key)) {
|
|
1272
|
+
// A key redaction altered may have named a credential (#703).
|
|
1273
|
+
out[key] = withheld(item);
|
|
1274
|
+
}
|
|
1275
|
+
else if (review?.verdict === "redact") {
|
|
1120
1276
|
out[key] = withheld(item);
|
|
1121
1277
|
}
|
|
1122
1278
|
else if (labelled && LABELLED_VALUE_FIELDS.includes(key)) {
|
|
@@ -1153,8 +1309,11 @@ function heuristic(value, exempt) {
|
|
|
1153
1309
|
else if (key === "headers" && routeLike(current)) {
|
|
1154
1310
|
out[key] = headerValues(item);
|
|
1155
1311
|
}
|
|
1156
|
-
else if (DESTINATION_KEYS.includes(key) &&
|
|
1312
|
+
else if (DESTINATION_KEYS.includes(key) &&
|
|
1313
|
+
typeof item === "string" &&
|
|
1314
|
+
(URL_SHAPE.test(item) || alteredScheme(item))) {
|
|
1157
1315
|
// An external destination can carry its secret in the path; relative paths stay.
|
|
1316
|
+
// One whose scheme redaction altered does not parse, so it is withheld (#703).
|
|
1158
1317
|
out[key] = originOnly(item);
|
|
1159
1318
|
}
|
|
1160
1319
|
else if (typeof item === "string" && verbatim?.has(key)) {
|
|
@@ -1208,27 +1367,26 @@ export function valueSafety(table, index) {
|
|
|
1208
1367
|
}
|
|
1209
1368
|
const resources = table.embeddedUrls.resources;
|
|
1210
1369
|
if (resources) {
|
|
1370
|
+
// A type redaction altered takes every resource's reviewed text paths (#703).
|
|
1371
|
+
const all = [...new Set(Object.values(resources.paths).flat())];
|
|
1372
|
+
const any = () => all;
|
|
1211
1373
|
const scan = (value, depth) => {
|
|
1212
1374
|
if (depth > MAX_DEPTH || value === null || typeof value !== "object")
|
|
1213
1375
|
return value;
|
|
1214
1376
|
if (Array.isArray(value))
|
|
1215
1377
|
return value.map((item) => scan(item, depth + 1));
|
|
1216
1378
|
let clean = Object.fromEntries(Object.entries(value).map(([key, item]) => [key, scan(item, depth + 1)]));
|
|
1217
|
-
const
|
|
1218
|
-
|
|
1219
|
-
if (typeof innerType === "string" && Object.hasOwn(resources.paths, innerType)) {
|
|
1379
|
+
const innerPaths = typedInner(value, resources.key, resources.paths, any);
|
|
1380
|
+
if (innerPaths) {
|
|
1220
1381
|
for (const partial of resources.partials ?? []) {
|
|
1221
1382
|
if (!Object.hasOwn(clean, partial))
|
|
1222
1383
|
continue;
|
|
1223
|
-
for (const path of
|
|
1384
|
+
for (const path of innerPaths)
|
|
1224
1385
|
clean[partial] = applyPath(clean[partial], path, withholdEmbeddedUrlsIn);
|
|
1225
1386
|
}
|
|
1226
1387
|
}
|
|
1227
|
-
const
|
|
1228
|
-
|
|
1229
|
-
for (const path of resources.paths[type])
|
|
1230
|
-
clean = applyPath(clean, path, withholdEmbeddedUrlsIn);
|
|
1231
|
-
}
|
|
1388
|
+
for (const path of typed(value, resources.key, resources.paths, any) ?? [])
|
|
1389
|
+
clean = applyPath(clean, path, withholdEmbeddedUrlsIn);
|
|
1232
1390
|
return clean;
|
|
1233
1391
|
};
|
|
1234
1392
|
out = scan(out, 0);
|