clinkx 1.0.0 → 1.1.1
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 +95 -0
- package/README.md +184 -112
- package/clinkx-workflows/dist/chairman-knobs.d.ts +24 -0
- package/clinkx-workflows/dist/chairman-knobs.js +27 -0
- package/clinkx-workflows/dist/chairman-knobs.js.map +1 -0
- package/clinkx-workflows/dist/clink-client.d.ts +10 -0
- package/clinkx-workflows/dist/clink-client.js +39 -1
- package/clinkx-workflows/dist/clink-client.js.map +1 -1
- package/clinkx-workflows/dist/config.d.ts +29 -1
- package/clinkx-workflows/dist/config.js +35 -2
- package/clinkx-workflows/dist/config.js.map +1 -1
- package/clinkx-workflows/dist/context-preflight.d.ts +5 -0
- package/clinkx-workflows/dist/context-preflight.js.map +1 -1
- package/clinkx-workflows/dist/council-execution-policy.d.ts +68 -0
- package/clinkx-workflows/dist/council-execution-policy.js +110 -0
- package/clinkx-workflows/dist/council-execution-policy.js.map +1 -0
- package/clinkx-workflows/dist/council-expander.d.ts +9 -8
- package/clinkx-workflows/dist/council-expander.js +18 -9
- package/clinkx-workflows/dist/council-expander.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/activity-contract.d.ts +5557 -4
- package/clinkx-workflows/dist/dashboard/activity-contract.js +55 -3
- package/clinkx-workflows/dist/dashboard/activity-contract.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/activity-reader.d.ts +9 -5
- package/clinkx-workflows/dist/dashboard/activity-reader.js +15 -2
- package/clinkx-workflows/dist/dashboard/activity-reader.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/api-routes.d.ts +4 -0
- package/clinkx-workflows/dist/dashboard/api-routes.js +133 -25
- package/clinkx-workflows/dist/dashboard/api-routes.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/context-builder-invocation-contract.d.ts +3865 -0
- package/clinkx-workflows/dist/dashboard/context-builder-invocation-contract.js +69 -0
- package/clinkx-workflows/dist/dashboard/context-builder-invocation-contract.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/context-run-reader.d.ts +36 -2
- package/clinkx-workflows/dist/dashboard/context-run-reader.js +142 -19
- package/clinkx-workflows/dist/dashboard/context-run-reader.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/contracts.d.ts +20027 -480
- package/clinkx-workflows/dist/dashboard/contracts.js +133 -6
- package/clinkx-workflows/dist/dashboard/contracts.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/index.d.ts +4 -0
- package/clinkx-workflows/dist/dashboard/index.js +4 -0
- package/clinkx-workflows/dist/dashboard/index.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/invocation-contract.d.ts +786 -0
- package/clinkx-workflows/dist/dashboard/invocation-contract.js +201 -0
- package/clinkx-workflows/dist/dashboard/invocation-contract.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/lazy-content.d.ts +14 -1
- package/clinkx-workflows/dist/dashboard/lazy-content.js +53 -2
- package/clinkx-workflows/dist/dashboard/lazy-content.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/native-transcript/contract.d.ts +90 -0
- package/clinkx-workflows/dist/dashboard/native-transcript/contract.js +31 -0
- package/clinkx-workflows/dist/dashboard/native-transcript/contract.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/native-transcript/projection.d.ts +12 -0
- package/clinkx-workflows/dist/dashboard/native-transcript/projection.js +286 -0
- package/clinkx-workflows/dist/dashboard/native-transcript/projection.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/progress-report-contract.d.ts +147 -4
- package/clinkx-workflows/dist/dashboard/progress-report-contract.js +11 -11
- package/clinkx-workflows/dist/dashboard/progress-report-contract.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/progress-report-reader.js +4 -56
- package/clinkx-workflows/dist/dashboard/progress-report-reader.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/progress-report-status-store.d.ts +2 -1
- package/clinkx-workflows/dist/dashboard/progress-report-status-store.js +6 -5
- package/clinkx-workflows/dist/dashboard/progress-report-status-store.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/prompt-builder-library-contract.d.ts +194 -0
- package/clinkx-workflows/dist/dashboard/prompt-builder-library-contract.js +89 -0
- package/clinkx-workflows/dist/dashboard/prompt-builder-library-contract.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/prompt-builder-library-store.d.ts +26 -0
- package/clinkx-workflows/dist/dashboard/prompt-builder-library-store.js +102 -0
- package/clinkx-workflows/dist/dashboard/prompt-builder-library-store.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/request-admission.js +4 -0
- package/clinkx-workflows/dist/dashboard/request-admission.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/scanner.d.ts +3 -0
- package/clinkx-workflows/dist/dashboard/scanner.js +52 -8
- package/clinkx-workflows/dist/dashboard/scanner.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/server-factory.d.ts +14 -1
- package/clinkx-workflows/dist/dashboard/server-factory.js +73 -21
- package/clinkx-workflows/dist/dashboard/server-factory.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/session-log-contract.d.ts +223 -56
- package/clinkx-workflows/dist/dashboard/session-log-contract.js +26 -0
- package/clinkx-workflows/dist/dashboard/session-log-contract.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/session-log-journal.js +10 -3
- package/clinkx-workflows/dist/dashboard/session-log-journal.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/session-log-redaction.d.ts +9 -1
- package/clinkx-workflows/dist/dashboard/session-log-redaction.js +793 -59
- package/clinkx-workflows/dist/dashboard/session-log-redaction.js.map +1 -1
- package/clinkx-workflows/dist/dashboard-ui/assets/index-JPHQVtn9.js +106 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/style-DOMHk-j6.css +1 -0
- package/clinkx-workflows/dist/dashboard-ui/index.html +2 -2
- package/clinkx-workflows/dist/delegate-finalizer-policy.d.ts +26 -0
- package/clinkx-workflows/dist/delegate-finalizer-policy.js +72 -0
- package/clinkx-workflows/dist/delegate-finalizer-policy.js.map +1 -0
- package/clinkx-workflows/dist/delegate-signature.d.ts +14 -0
- package/clinkx-workflows/dist/delegate-signature.js +14 -0
- package/clinkx-workflows/dist/delegate-signature.js.map +1 -0
- package/clinkx-workflows/dist/delegate-validate.d.ts +32 -2
- package/clinkx-workflows/dist/delegate-validate.js +51 -0
- package/clinkx-workflows/dist/delegate-validate.js.map +1 -1
- package/clinkx-workflows/dist/engine.d.ts +113 -3
- package/clinkx-workflows/dist/engine.js +476 -80
- package/clinkx-workflows/dist/engine.js.map +1 -1
- package/clinkx-workflows/dist/errors.d.ts +22 -2
- package/clinkx-workflows/dist/errors.js +30 -2
- package/clinkx-workflows/dist/errors.js.map +1 -1
- package/clinkx-workflows/dist/fidelity.d.ts +47 -1
- package/clinkx-workflows/dist/fidelity.js +54 -1
- package/clinkx-workflows/dist/fidelity.js.map +1 -1
- package/clinkx-workflows/dist/fingerprint.d.ts +24 -6
- package/clinkx-workflows/dist/fingerprint.js +32 -9
- package/clinkx-workflows/dist/fingerprint.js.map +1 -1
- package/clinkx-workflows/dist/index.js +53 -1
- package/clinkx-workflows/dist/index.js.map +1 -1
- package/clinkx-workflows/dist/logger.d.ts +1 -1
- package/clinkx-workflows/dist/logger.js +17 -3
- package/clinkx-workflows/dist/logger.js.map +1 -1
- package/clinkx-workflows/dist/monotonic-clock.d.ts +19 -0
- package/clinkx-workflows/dist/monotonic-clock.js +22 -0
- package/clinkx-workflows/dist/monotonic-clock.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/contract.d.ts +52 -52
- package/clinkx-workflows/dist/plan-review/publication.d.ts +4 -4
- package/clinkx-workflows/dist/plan-review/run-state.d.ts +16 -16
- package/clinkx-workflows/dist/roster-contract.d.ts +889 -0
- package/clinkx-workflows/dist/roster-contract.js +180 -0
- package/clinkx-workflows/dist/roster-contract.js.map +1 -0
- package/clinkx-workflows/dist/rosters.d.ts +14 -0
- package/clinkx-workflows/dist/rosters.js +169 -0
- package/clinkx-workflows/dist/rosters.js.map +1 -0
- package/clinkx-workflows/dist/run-health.d.ts +7 -0
- package/clinkx-workflows/dist/run-health.js +25 -0
- package/clinkx-workflows/dist/run-health.js.map +1 -0
- package/clinkx-workflows/dist/schema.d.ts +64 -0
- package/clinkx-workflows/dist/schema.js +24 -0
- package/clinkx-workflows/dist/schema.js.map +1 -1
- package/clinkx-workflows/dist/server.js +2 -0
- package/clinkx-workflows/dist/server.js.map +1 -1
- package/clinkx-workflows/dist/shutdown.d.ts +15 -2
- package/clinkx-workflows/dist/shutdown.js +60 -16
- package/clinkx-workflows/dist/shutdown.js.map +1 -1
- package/clinkx-workflows/dist/state-schema.d.ts +23 -0
- package/clinkx-workflows/dist/state.d.ts +5 -0
- package/clinkx-workflows/dist/state.js +128 -21
- package/clinkx-workflows/dist/state.js.map +1 -1
- package/clinkx-workflows/dist/stdio-watchdog.d.ts +52 -0
- package/clinkx-workflows/dist/stdio-watchdog.js +212 -0
- package/clinkx-workflows/dist/stdio-watchdog.js.map +1 -0
- package/clinkx-workflows/dist/types.d.ts +6 -0
- package/clinkx-workflows/dist/validators/council.d.ts +50 -50
- package/clinkx-workflows/dist/validators/plan-review.d.ts +3 -3
- package/clinkx-workflows/dist/workflow-tools.d.ts +70 -0
- package/clinkx-workflows/dist/workflow-tools.js +760 -168
- package/clinkx-workflows/dist/workflow-tools.js.map +1 -1
- package/clinkx-workflows/templates/council/answer-chairman.txt +3 -0
- package/clinkx-workflows/templates/council/answer-cross-reviewer.txt +3 -0
- package/clinkx-workflows/templates/council/answer-specialist.txt +3 -0
- package/clinkx-workflows/templates/council/chairman.txt +3 -0
- package/clinkx-workflows/templates/council/code-review-chairman.txt +3 -0
- package/clinkx-workflows/templates/council/code-review-cross-reviewer.txt +3 -0
- package/clinkx-workflows/templates/council/code-review-reviewer.txt +3 -0
- package/clinkx-workflows/templates/council/debug-chairman.txt +3 -0
- package/clinkx-workflows/templates/council/debug-cross-reviewer.txt +3 -0
- package/clinkx-workflows/templates/council/debug-debugger.txt +3 -0
- package/clinkx-workflows/templates/council/discover-analyst.txt +3 -0
- package/clinkx-workflows/templates/council/discover-chairman.txt +3 -0
- package/clinkx-workflows/templates/council/discover-cross-reviewer.txt +3 -0
- package/clinkx-workflows/templates/council/member.txt +3 -0
- package/clinkx-workflows/templates/council/plan-review-chairman.txt +3 -0
- package/clinkx-workflows/templates/council/plan-review-cross-reviewer.txt +3 -0
- package/clinkx-workflows/templates/council/plan-review-reviewer.txt +3 -0
- package/clinkx-workflows/templates/council/reviewer.txt +3 -0
- package/clinkx-workflows/workflows/council-answer.yaml +3 -0
- package/clinkx-workflows/workflows/council-code-review.yaml +3 -0
- package/clinkx-workflows/workflows/council-debug.yaml +3 -0
- package/clinkx-workflows/workflows/council-default.yaml +7 -0
- package/clinkx-workflows/workflows/council-discover.yaml +3 -0
- package/clinkx-workflows/workflows/council-plan-review.yaml +3 -0
- package/conf/adapters/agy.json +9 -2
- package/conf/adapters/claude.json +7 -0
- package/conf/adapters/codex.json +7 -0
- package/conf/adapters/glm.json +19 -3
- package/conf/adapters/grok.json +9 -2
- package/conf/adapters/hapi/agy.json +9 -2
- package/conf/adapters/hapi/claude.json +7 -0
- package/conf/adapters/hapi/codex.json +7 -0
- package/conf/adapters/hapi/glm.json +20 -4
- package/conf/rosters/default-council.json +21 -0
- package/dist/activity-journal.d.ts +17 -3
- package/dist/activity-journal.js +404 -79
- package/dist/activity-journal.js.map +1 -1
- package/dist/codex/agent-tools.js +36 -0
- package/dist/codex/agent-tools.js.map +1 -1
- package/dist/codex/app-server-client.d.ts +3 -0
- package/dist/codex/app-server-client.js +11 -0
- package/dist/codex/app-server-client.js.map +1 -1
- package/dist/codex/app-server-registry.d.ts +18 -0
- package/dist/codex/app-server-registry.js +88 -2
- package/dist/codex/app-server-registry.js.map +1 -1
- package/dist/codex/client-types.d.ts +7 -1
- package/dist/codex/protocol.d.ts +4 -4
- package/dist/config.d.ts +220 -23
- package/dist/config.js +104 -1
- package/dist/config.js.map +1 -1
- package/dist/context-builder/bundle.d.ts +13 -0
- package/dist/context-builder/bundle.js +46 -0
- package/dist/context-builder/bundle.js.map +1 -1
- package/dist/context-builder/delegate-finalizer-effective.d.ts +33 -0
- package/dist/context-builder/delegate-finalizer-effective.js +82 -0
- package/dist/context-builder/delegate-finalizer-effective.js.map +1 -0
- package/dist/context-builder/delegate-packet.d.ts +18 -0
- package/dist/context-builder/delegate-packet.js +73 -9
- package/dist/context-builder/delegate-packet.js.map +1 -1
- package/dist/context-builder/delegate-reconstruction.d.ts +29 -0
- package/dist/context-builder/delegate-reconstruction.js +110 -41
- package/dist/context-builder/delegate-reconstruction.js.map +1 -1
- package/dist/context-builder/delegate-validation.d.ts +31 -0
- package/dist/context-builder/delegate-validation.js +87 -10
- package/dist/context-builder/delegate-validation.js.map +1 -1
- package/dist/context-builder/delegate.d.ts +9 -0
- package/dist/context-builder/delegate.js +210 -59
- package/dist/context-builder/delegate.js.map +1 -1
- package/dist/context-builder/diagnostics.d.ts +1 -1
- package/dist/context-builder/diagnostics.js.map +1 -1
- package/dist/context-builder/discovery-progress.d.ts +25 -0
- package/dist/context-builder/discovery-progress.js +64 -0
- package/dist/context-builder/discovery-progress.js.map +1 -0
- package/dist/context-builder/discovery-watchdog.d.ts +15 -0
- package/dist/context-builder/discovery-watchdog.js +39 -0
- package/dist/context-builder/discovery-watchdog.js.map +1 -0
- package/dist/context-builder/discovery.d.ts +7 -0
- package/dist/context-builder/discovery.js +119 -7
- package/dist/context-builder/discovery.js.map +1 -1
- package/dist/context-builder/external-gaps.d.ts +73 -0
- package/dist/context-builder/external-gaps.js +248 -0
- package/dist/context-builder/external-gaps.js.map +1 -0
- package/dist/context-builder/globs.d.ts +1 -0
- package/dist/context-builder/globs.js +15 -0
- package/dist/context-builder/globs.js.map +1 -1
- package/dist/context-builder/handler.d.ts +42 -1
- package/dist/context-builder/handler.js +690 -62
- package/dist/context-builder/handler.js.map +1 -1
- package/dist/context-builder/invocation-sidecar.d.ts +41 -0
- package/dist/context-builder/invocation-sidecar.js +189 -0
- package/dist/context-builder/invocation-sidecar.js.map +1 -0
- package/dist/context-builder/lifecycle-contract.d.ts +51 -51
- package/dist/context-builder/mutation-lock.d.ts +32 -0
- package/dist/context-builder/mutation-lock.js +321 -0
- package/dist/context-builder/mutation-lock.js.map +1 -0
- package/dist/context-builder/review-coverage.d.ts +32 -0
- package/dist/context-builder/review-coverage.js +130 -23
- package/dist/context-builder/review-coverage.js.map +1 -1
- package/dist/context-builder/schema.d.ts +76 -1
- package/dist/context-builder/schema.js +21 -1
- package/dist/context-builder/schema.js.map +1 -1
- package/dist/context-builder/secrets.d.ts +31 -0
- package/dist/context-builder/secrets.js +871 -84
- package/dist/context-builder/secrets.js.map +1 -1
- package/dist/context-builder/status.d.ts +1 -1
- package/dist/context-builder/status.js +1 -0
- package/dist/context-builder/status.js.map +1 -1
- package/dist/context-builder/storage.d.ts +29 -5
- package/dist/context-builder/storage.js +311 -236
- package/dist/context-builder/storage.js.map +1 -1
- package/dist/context-builder/versions.d.ts +19 -4
- package/dist/context-builder/versions.js +19 -4
- package/dist/context-builder/versions.js.map +1 -1
- package/dist/context-builder/workflow-runner.d.ts +6 -0
- package/dist/context-builder/workflow-runner.js +363 -173
- package/dist/context-builder/workflow-runner.js.map +1 -1
- package/dist/dashboard-adapter-catalog.js +7 -0
- package/dist/dashboard-adapter-catalog.js.map +1 -1
- package/dist/dashboard-cli.d.ts +2 -0
- package/dist/dashboard-cli.js +40 -2
- package/dist/dashboard-cli.js.map +1 -1
- package/dist/dashboard-companion/client.d.ts +1 -0
- package/dist/dashboard-companion/client.js +22 -3
- package/dist/dashboard-companion/client.js.map +1 -1
- package/dist/dashboard-companion/protocol.js +4 -1
- package/dist/dashboard-companion/protocol.js.map +1 -1
- package/dist/dashboard-gateway/index.d.ts +1 -0
- package/dist/dashboard-gateway/index.js +33 -2
- package/dist/dashboard-gateway/index.js.map +1 -1
- package/dist/handler.js +5 -1
- package/dist/handler.js.map +1 -1
- package/dist/index.d.ts +18 -1
- package/dist/index.js +145 -7
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +1 -1
- package/dist/logger.js +18 -3
- package/dist/logger.js.map +1 -1
- package/dist/parsers/agy-stream-json.d.ts +14 -0
- package/dist/parsers/agy-stream-json.js +172 -0
- package/dist/parsers/agy-stream-json.js.map +1 -0
- package/dist/parsers/extract.js +5 -1
- package/dist/parsers/extract.js.map +1 -1
- package/dist/parsers/observed-invocation.d.ts +20 -0
- package/dist/parsers/observed-invocation.js +42 -0
- package/dist/parsers/observed-invocation.js.map +1 -0
- package/dist/pipeline.d.ts +13 -0
- package/dist/pipeline.js +60 -10
- package/dist/pipeline.js.map +1 -1
- package/dist/progress-report/lock.d.ts +74 -0
- package/dist/progress-report/lock.js +343 -0
- package/dist/progress-report/lock.js.map +1 -0
- package/dist/progress-report/schema.d.ts +808 -0
- package/dist/progress-report/schema.js +119 -0
- package/dist/progress-report/schema.js.map +1 -0
- package/dist/progress-report/store.d.ts +66 -0
- package/dist/progress-report/store.js +766 -0
- package/dist/progress-report/store.js.map +1 -0
- package/dist/progress-report/tools.d.ts +20 -0
- package/dist/progress-report/tools.js +141 -0
- package/dist/progress-report/tools.js.map +1 -0
- package/dist/prompt-mode.d.ts +1 -1
- package/dist/prompt-mode.js +7 -2
- package/dist/prompt-mode.js.map +1 -1
- package/dist/runner.d.ts +7 -0
- package/dist/runner.js +10 -3
- package/dist/runner.js.map +1 -1
- package/dist/safety.d.ts +50 -0
- package/dist/safety.js +182 -1
- package/dist/safety.js.map +1 -1
- package/dist/schema.d.ts +8 -0
- package/dist/schema.js +20 -0
- package/dist/schema.js.map +1 -1
- package/dist/server.d.ts +6 -1
- package/dist/server.js +94 -8
- package/dist/server.js.map +1 -1
- package/dist/session-log-stream.d.ts +1 -1
- package/dist/session-log-stream.js +280 -33
- package/dist/session-log-stream.js.map +1 -1
- package/dist/stdio-activity.d.ts +28 -0
- package/dist/stdio-activity.js +72 -0
- package/dist/stdio-activity.js.map +1 -0
- package/docs/configuration.md +426 -0
- package/docs/security.md +293 -0
- package/package.json +5 -5
- package/clinkx-workflows/dist/dashboard-ui/assets/index-DOkMXTfB.js +0 -25
- package/clinkx-workflows/dist/dashboard-ui/assets/style-ChJsn0KD.css +0 -1
|
@@ -22,6 +22,657 @@ const MAX_YAML_SOURCE_CHARS = 5_000_000;
|
|
|
22
22
|
const MAX_JS_KEY_LOOKBACK = 32_768;
|
|
23
23
|
const MAX_TS_TYPE_CHARS = 4_096;
|
|
24
24
|
const MAX_TS_TYPE_DEPTH = 32;
|
|
25
|
+
/**
|
|
26
|
+
* Fail-closed provenance for a body whose deterministic scan work exceeded
|
|
27
|
+
* its budget. The budget is a backstop, not the performance mechanism: the
|
|
28
|
+
* type-body header scan is a deterministic delimiter-aware linear scan, and
|
|
29
|
+
* the other index-backed detectors are linear and memoized per text, so
|
|
30
|
+
* realistic and generated corpora never trip it. Known residuals: the
|
|
31
|
+
* dense_fields shape stays super-linear but bounded (tracked follow-up), and
|
|
32
|
+
* the value-span/redaction regexes still have catastrophic-backtracking
|
|
33
|
+
* shapes (member-call-chain, backslash runs; see the scan-wedge issue doc)
|
|
34
|
+
* that can wedge inside a single regex match before the budget trips. Work
|
|
35
|
+
* is charged at real interrupt points (per header candidate, mid index
|
|
36
|
+
* build), so exhaustion unwinds and fails closed. Revisit the calibration
|
|
37
|
+
* if a legitimate body ever reports this reason.
|
|
38
|
+
*/
|
|
39
|
+
export const SANITIZE_BUDGET_EXHAUSTED_PROVENANCE = "sanitize_budget_exhausted";
|
|
40
|
+
// Calibrated at >= 20x the linearized realistic-shape cost, with headroom
|
|
41
|
+
// for the dense_fields adversarial shape (measured ~4_000 work units per
|
|
42
|
+
// char at 256 KiB; see tests/context-builder/secrets-perf.test.ts). Larger
|
|
43
|
+
// or denser pathological bodies exhaust it by design and fail closed.
|
|
44
|
+
const SECRET_SCAN_WORK_PER_CHAR = 6_000;
|
|
45
|
+
const SECRET_SCAN_WORK_FLOOR = 4_000_000;
|
|
46
|
+
class SecretScanBudgetExhausted extends Error {
|
|
47
|
+
}
|
|
48
|
+
let activeSecretScanSession;
|
|
49
|
+
let secretScanWorkTotal = 0;
|
|
50
|
+
let secretScanBudgetOverride;
|
|
51
|
+
function chargeWork(units) {
|
|
52
|
+
secretScanWorkTotal += units;
|
|
53
|
+
const session = activeSecretScanSession;
|
|
54
|
+
if (session != null) {
|
|
55
|
+
session.work += units;
|
|
56
|
+
if (session.work > session.limit) {
|
|
57
|
+
throw new SecretScanBudgetExhausted();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Runs one public detector entry point inside a per-body session. Nested
|
|
63
|
+
* entry points (sanitize -> scan/redact/residual) share the outer budget and
|
|
64
|
+
* memo; exhaustion unwinds to the outermost entry, which fails closed.
|
|
65
|
+
*/
|
|
66
|
+
function withSecretScanSession(text, work, onExhausted) {
|
|
67
|
+
if (activeSecretScanSession != null) {
|
|
68
|
+
return work();
|
|
69
|
+
}
|
|
70
|
+
activeSecretScanSession = {
|
|
71
|
+
limit: secretScanBudgetOverride ??
|
|
72
|
+
SECRET_SCAN_WORK_FLOOR + SECRET_SCAN_WORK_PER_CHAR * text.length,
|
|
73
|
+
work: 0,
|
|
74
|
+
texts: new Map(),
|
|
75
|
+
};
|
|
76
|
+
try {
|
|
77
|
+
return work();
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (error instanceof SecretScanBudgetExhausted) {
|
|
81
|
+
return onExhausted();
|
|
82
|
+
}
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
activeSecretScanSession = undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function analysisFor(text) {
|
|
90
|
+
const session = activeSecretScanSession;
|
|
91
|
+
if (session == null) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
let analysis = session.texts.get(text);
|
|
95
|
+
if (analysis == null) {
|
|
96
|
+
analysis = {};
|
|
97
|
+
session.texts.set(text, analysis);
|
|
98
|
+
}
|
|
99
|
+
return analysis;
|
|
100
|
+
}
|
|
101
|
+
/** Test-only: deterministic work units consumed by `work`. */
|
|
102
|
+
export function measureSecretScanWorkForTests(work) {
|
|
103
|
+
const before = secretScanWorkTotal;
|
|
104
|
+
const result = work();
|
|
105
|
+
return { result, work: secretScanWorkTotal - before };
|
|
106
|
+
}
|
|
107
|
+
/** Test-only: run `work` with a fixed per-body work budget. */
|
|
108
|
+
export function withSecretScanBudgetForTests(limit, work) {
|
|
109
|
+
const previous = secretScanBudgetOverride;
|
|
110
|
+
secretScanBudgetOverride = limit;
|
|
111
|
+
try {
|
|
112
|
+
return work();
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
secretScanBudgetOverride = previous;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** Test-only: (start, brace) offset of every type-body header match. */
|
|
119
|
+
export function typeBodyHeaderMatchesForTests(text) {
|
|
120
|
+
const headers = collectTypeBodyHeaders(text);
|
|
121
|
+
const pairs = [];
|
|
122
|
+
for (let slot = 0; slot < headers.starts.length; slot++) {
|
|
123
|
+
const start = headers.starts[slot];
|
|
124
|
+
const brace = headers.braces[slot];
|
|
125
|
+
if (start != null && brace != null) {
|
|
126
|
+
pairs.push({ start, brace });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return pairs;
|
|
130
|
+
}
|
|
131
|
+
function budgetExhaustedFinding() {
|
|
132
|
+
return contentFinding(SANITIZE_BUDGET_EXHAUSTED_PROVENANCE, 1);
|
|
133
|
+
}
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// Linear per-text indexes.
|
|
136
|
+
//
|
|
137
|
+
// The detectors below used to rescan a fresh 32 KiB lookback slice per '='
|
|
138
|
+
// (regex matchAll over the window plus a brace scan, per query). Each index is
|
|
139
|
+
// built once per distinct body-level string per scan session, memoized in the
|
|
140
|
+
// SecretTextAnalysis slots, and queried with binary searches / O(1) lookups.
|
|
141
|
+
// Query semantics replicate the legacy slice behavior exactly, including the
|
|
142
|
+
// slice-relative `^`/`\b` window edge (a header or typed field starting
|
|
143
|
+
// exactly at the window start counts) — see the per-query comments.
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
const TYPED_FIELD_PATTERN = /(?:^|[;{}\r\n])[ \t]*(?:(?:public|private|protected|static|readonly|declare|abstract|override)[ \t]+)*([A-Za-z_$][A-Za-z0-9_$-]*)(?:[?!])?[ \t]*:[ \t]*/g;
|
|
146
|
+
const TYPED_FIELD_MODIFIERS = [
|
|
147
|
+
"public",
|
|
148
|
+
"private",
|
|
149
|
+
"protected",
|
|
150
|
+
"static",
|
|
151
|
+
"readonly",
|
|
152
|
+
"declare",
|
|
153
|
+
"abstract",
|
|
154
|
+
"override",
|
|
155
|
+
];
|
|
156
|
+
const MAX_TS_TYPE_ONLY_FIELD_CHARS = 4_096;
|
|
157
|
+
// Same composition as the per-call `new RegExp` the malformed-YAML span walk
|
|
158
|
+
// used to compile for every candidate value start.
|
|
159
|
+
const MALFORMED_YAML_CONTINUES_PATTERN = new RegExp(`^(?:&[A-Za-z0-9_.-]+[ \\t]+|!!?[^ \\t\\r\\n]+[ \\t]+|!<[^>\\r\\n]+>[ \\t]+)*${YAML_BLOCK_HEADER_PATTERN_SOURCE}(?:[ \\t]+#.*)?$`);
|
|
160
|
+
/**
|
|
161
|
+
* (start, brace) offset of every `class`/`interface`/`type` header, from a
|
|
162
|
+
* delimiter-aware linear scan equivalent to matchAll of
|
|
163
|
+
* /\b(?:class|interface|type)\s+[A-Za-z_$][A-Za-z0-9_$]*[^{};]*\{/g (the
|
|
164
|
+
* pattern's greedy `[^{};]*` backtracks one char at a time per candidate, so
|
|
165
|
+
* a header succeeds exactly when the first `{`/`}`/`;` after the ident is
|
|
166
|
+
* `{`; resolving that stopper through an amortized forward scan removed the
|
|
167
|
+
* quadratic blowup on dense failed-header bodies). Deliberately not
|
|
168
|
+
* quote-aware: matches inside
|
|
169
|
+
* strings and comments are golden-pinned semantics. `charge` gates the
|
|
170
|
+
* budget charges: the whole-text index build charges here, while the bounded
|
|
171
|
+
* lookback-slice fallback does not (its caller already charges the slice
|
|
172
|
+
* length, and the slice volume per body is a pre-existing property).
|
|
173
|
+
*/
|
|
174
|
+
function collectTypeBodyHeaders(text, charge = true) {
|
|
175
|
+
const length = text.length;
|
|
176
|
+
const starts = [];
|
|
177
|
+
const braces = [];
|
|
178
|
+
if (charge) {
|
|
179
|
+
chargeWork(length);
|
|
180
|
+
}
|
|
181
|
+
// Stopper lookup (first `{`/`}`/`;` at or after a cursor) via one amortized
|
|
182
|
+
// forward scan: region [regionStart, regionStopper) provably holds no
|
|
183
|
+
// stopper, so every candidate cursor inside it reuses regionStopper — a
|
|
184
|
+
// dense run of failed candidates pays one scan, not one per candidate.
|
|
185
|
+
// The absent-stopper sentinel is regionStopper === length, so the
|
|
186
|
+
// text[stopper] === "{" success test fails naturally at EOF. No index
|
|
187
|
+
// array is allocated: the lookback-slice fallback below runs thousands of
|
|
188
|
+
// times per body on the dense shapes, and a per-call Int32Array plus full
|
|
189
|
+
// backward pass dominated the wall clock there.
|
|
190
|
+
let regionStart = 0;
|
|
191
|
+
let regionStopper = -1;
|
|
192
|
+
const stopperAfter = (from) => {
|
|
193
|
+
if (regionStart <= from && from <= regionStopper) {
|
|
194
|
+
return regionStopper;
|
|
195
|
+
}
|
|
196
|
+
let index = from;
|
|
197
|
+
while (index < length) {
|
|
198
|
+
const code = text.charCodeAt(index);
|
|
199
|
+
if (code === 0x7b || code === 0x7d || code === 0x3b) {
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
index++;
|
|
203
|
+
}
|
|
204
|
+
regionStart = from;
|
|
205
|
+
regionStopper = index;
|
|
206
|
+
return index;
|
|
207
|
+
};
|
|
208
|
+
// Fresh locator per invocation: literal alternation plus lookahead, no
|
|
209
|
+
// backtracking structure. The regex \b stays the boundary authority (word
|
|
210
|
+
// chars are [A-Za-z0-9_], so `$class` matches while `xclass` and `Class`
|
|
211
|
+
// do not); lastIndex is managed by hand so a successful header can
|
|
212
|
+
// swallow its span the way matchAll's lastIndex jump did.
|
|
213
|
+
const locator = /\b(?:class|interface|type)(?=\s)/g;
|
|
214
|
+
let keyword;
|
|
215
|
+
while ((keyword = locator.exec(text)) != null) {
|
|
216
|
+
const start = keyword.index;
|
|
217
|
+
let cursor = start + keyword[0].length;
|
|
218
|
+
while (cursor < length && isJsWhitespaceChar(text[cursor] ?? "")) {
|
|
219
|
+
cursor++;
|
|
220
|
+
}
|
|
221
|
+
if (cursor >= length || !isIdentStartCode(text.charCodeAt(cursor))) {
|
|
222
|
+
if (charge) {
|
|
223
|
+
chargeWork(8);
|
|
224
|
+
}
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const identStart = cursor;
|
|
228
|
+
while (cursor < length && isIdentCharCode(text.charCodeAt(cursor))) {
|
|
229
|
+
cursor++;
|
|
230
|
+
}
|
|
231
|
+
if (charge) {
|
|
232
|
+
chargeWork(8 + (cursor - identStart));
|
|
233
|
+
}
|
|
234
|
+
const stopper = stopperAfter(cursor);
|
|
235
|
+
// Failed candidates never jump the locator: it is already past the
|
|
236
|
+
// keyword, and the very next keyword may still succeed
|
|
237
|
+
// (`class A; type B {`). Jumping to the failure-stopper + 1 would also
|
|
238
|
+
// be safe; this scan commits to no-jump.
|
|
239
|
+
if (text.charCodeAt(stopper) === 0x7b) {
|
|
240
|
+
starts.push(start);
|
|
241
|
+
braces.push(stopper);
|
|
242
|
+
locator.lastIndex = stopper + 1;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return { starts, braces };
|
|
246
|
+
}
|
|
247
|
+
function typeBodyIndexFor(text) {
|
|
248
|
+
const analysis = analysisFor(text);
|
|
249
|
+
const cached = analysis?.typeBody;
|
|
250
|
+
if (cached != null) {
|
|
251
|
+
return cached;
|
|
252
|
+
}
|
|
253
|
+
// collectTypeBodyHeaders charges the amortized stopper scan and, per
|
|
254
|
+
// candidate, 8 + identLength — real work with honest budget interrupt
|
|
255
|
+
// points between candidates (plain JS checks, not event-loop yields). This
|
|
256
|
+
// charge covers the stack pass below; mid-enumeration exhaustion unwinds
|
|
257
|
+
// and fails closed instead of leaving a partial index in the session memo.
|
|
258
|
+
const headers = collectTypeBodyHeaders(text);
|
|
259
|
+
chargeWork(text.length);
|
|
260
|
+
const headerStarts = headers.starts;
|
|
261
|
+
const headerBraces = headers.braces;
|
|
262
|
+
const pastEnd = text.length + 1;
|
|
263
|
+
// Per-position close/exceed facts for every `{`, from one stack pass. The
|
|
264
|
+
// stack bases are strictly increasing (unique per open frame), so the frame
|
|
265
|
+
// that newly exceeds MAX_TS_TYPE_DEPTH when depth becomes d is exactly the
|
|
266
|
+
// one with base d - 33, found by binary search.
|
|
267
|
+
const closeAt = new Int32Array(text.length);
|
|
268
|
+
const exceedAt = new Int32Array(text.length);
|
|
269
|
+
closeAt.fill(pastEnd);
|
|
270
|
+
exceedAt.fill(pastEnd);
|
|
271
|
+
const stackPos = [];
|
|
272
|
+
const stackBase = [];
|
|
273
|
+
const stackExceed = [];
|
|
274
|
+
const stackHeaderSlot = [];
|
|
275
|
+
let depth = 0;
|
|
276
|
+
const headerSlots = new Map();
|
|
277
|
+
for (let slot = 0; slot < headerBraces.length; slot++) {
|
|
278
|
+
headerSlots.set(headerBraces[slot] ?? -1, slot);
|
|
279
|
+
}
|
|
280
|
+
for (let index = 0; index < text.length; index++) {
|
|
281
|
+
const code = text.charCodeAt(index);
|
|
282
|
+
if (code === 0x7b) {
|
|
283
|
+
stackPos.push(index);
|
|
284
|
+
stackBase.push(depth);
|
|
285
|
+
stackExceed.push(pastEnd);
|
|
286
|
+
stackHeaderSlot.push(headerSlots.get(index) ?? -1);
|
|
287
|
+
depth++;
|
|
288
|
+
const exceedBase = depth - (MAX_TS_TYPE_DEPTH + 1);
|
|
289
|
+
const frame = binarySearchExact(stackBase, exceedBase);
|
|
290
|
+
if (frame >= 0 && (stackExceed[frame] ?? pastEnd) === pastEnd) {
|
|
291
|
+
stackExceed[frame] = index + 1;
|
|
292
|
+
}
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (code === 0x7d) {
|
|
296
|
+
const pos = stackPos.pop();
|
|
297
|
+
const base = stackBase.pop();
|
|
298
|
+
const exceed = stackExceed.pop();
|
|
299
|
+
const headerSlot = stackHeaderSlot.pop();
|
|
300
|
+
depth--;
|
|
301
|
+
if (pos == null || base == null || exceed == null || headerSlot == null) {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
closeAt[pos] = index + 1;
|
|
305
|
+
exceedAt[pos] = exceed;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// Frames never popped: closeAt stays pastEnd; keep any recorded exceed.
|
|
310
|
+
for (let frame = 0; frame < stackPos.length; frame++) {
|
|
311
|
+
const pos = stackPos[frame];
|
|
312
|
+
const exceed = stackExceed[frame];
|
|
313
|
+
if (pos == null || exceed == null) {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
exceedAt[pos] = exceed;
|
|
317
|
+
}
|
|
318
|
+
const index = {
|
|
319
|
+
starts: Int32Array.from(headerStarts),
|
|
320
|
+
braces: Int32Array.from(headerBraces),
|
|
321
|
+
closeAt,
|
|
322
|
+
exceedAt,
|
|
323
|
+
pastEnd,
|
|
324
|
+
};
|
|
325
|
+
if (analysis != null) {
|
|
326
|
+
analysis.typeBody = index;
|
|
327
|
+
}
|
|
328
|
+
return index;
|
|
329
|
+
}
|
|
330
|
+
function typedFieldCandidatesFor(text) {
|
|
331
|
+
const analysis = analysisFor(text);
|
|
332
|
+
const cached = analysis?.typedFields;
|
|
333
|
+
if (cached != null) {
|
|
334
|
+
return cached;
|
|
335
|
+
}
|
|
336
|
+
chargeWork(2 * text.length);
|
|
337
|
+
const matchStarts = [];
|
|
338
|
+
const keyStarts = [];
|
|
339
|
+
const keyEnds = [];
|
|
340
|
+
const typeStarts = [];
|
|
341
|
+
for (const match of text.matchAll(TYPED_FIELD_PATTERN)) {
|
|
342
|
+
const rawKey = match[1] ?? "";
|
|
343
|
+
const matchStart = match.index ?? 0;
|
|
344
|
+
const keyStart = matchStart + match[0].lastIndexOf(rawKey);
|
|
345
|
+
matchStarts.push(matchStart);
|
|
346
|
+
keyStarts.push(keyStart);
|
|
347
|
+
keyEnds.push(keyStart + rawKey.length);
|
|
348
|
+
typeStarts.push(matchStart + match[0].length);
|
|
349
|
+
}
|
|
350
|
+
const candidates = {
|
|
351
|
+
matchStarts: Int32Array.from(matchStarts),
|
|
352
|
+
keyStarts: Int32Array.from(keyStarts),
|
|
353
|
+
keyEnds: Int32Array.from(keyEnds),
|
|
354
|
+
typeStarts: Int32Array.from(typeStarts),
|
|
355
|
+
count: matchStarts.length,
|
|
356
|
+
};
|
|
357
|
+
if (analysis != null) {
|
|
358
|
+
analysis.typedFields = candidates;
|
|
359
|
+
}
|
|
360
|
+
return candidates;
|
|
361
|
+
}
|
|
362
|
+
function newlineOffsetsFor(text) {
|
|
363
|
+
const analysis = analysisFor(text);
|
|
364
|
+
const cached = analysis?.newlineOffsets;
|
|
365
|
+
if (cached != null) {
|
|
366
|
+
return cached;
|
|
367
|
+
}
|
|
368
|
+
chargeWork(text.length);
|
|
369
|
+
const offsets = [];
|
|
370
|
+
for (let index = 0; index < text.length; index++) {
|
|
371
|
+
if (text.charCodeAt(index) === 0x0a) {
|
|
372
|
+
offsets.push(index);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const result = Int32Array.from(offsets);
|
|
376
|
+
if (analysis != null) {
|
|
377
|
+
analysis.newlineOffsets = result;
|
|
378
|
+
}
|
|
379
|
+
return result;
|
|
380
|
+
}
|
|
381
|
+
function nextSemicolonFor(text) {
|
|
382
|
+
const analysis = analysisFor(text);
|
|
383
|
+
const cached = analysis?.nextSemicolon;
|
|
384
|
+
if (cached != null) {
|
|
385
|
+
return cached;
|
|
386
|
+
}
|
|
387
|
+
chargeWork(text.length);
|
|
388
|
+
const result = new Int32Array(text.length + 1);
|
|
389
|
+
result.fill(-1);
|
|
390
|
+
for (let index = text.length - 1; index >= 0; index--) {
|
|
391
|
+
result[index] =
|
|
392
|
+
text.charCodeAt(index) === 0x3b ? index : result[index + 1] ?? -1;
|
|
393
|
+
}
|
|
394
|
+
if (analysis != null) {
|
|
395
|
+
analysis.nextSemicolon = result;
|
|
396
|
+
}
|
|
397
|
+
return result;
|
|
398
|
+
}
|
|
399
|
+
function yamlBlockHeaderIndexFor(text) {
|
|
400
|
+
const analysis = analysisFor(text);
|
|
401
|
+
const cached = analysis?.yamlBlockHeaders;
|
|
402
|
+
if (cached != null) {
|
|
403
|
+
return cached;
|
|
404
|
+
}
|
|
405
|
+
chargeWork(text.length);
|
|
406
|
+
const starts = [0];
|
|
407
|
+
for (let index = 0; index < text.length; index++) {
|
|
408
|
+
if (text.charCodeAt(index) === 0x0a) {
|
|
409
|
+
starts.push(index + 1);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const result = { lineStarts: Int32Array.from(starts) };
|
|
413
|
+
if (analysis != null) {
|
|
414
|
+
analysis.yamlBlockHeaders = result;
|
|
415
|
+
}
|
|
416
|
+
return result;
|
|
417
|
+
}
|
|
418
|
+
/** Rightmost index with values[index] < target (-1 when none). */
|
|
419
|
+
function binarySearchLastLessThan(values, target) {
|
|
420
|
+
let low = 0;
|
|
421
|
+
let high = values.length - 1;
|
|
422
|
+
let result = -1;
|
|
423
|
+
while (low <= high) {
|
|
424
|
+
const mid = (low + high) >>> 1;
|
|
425
|
+
if ((values[mid] ?? 0) < target) {
|
|
426
|
+
result = mid;
|
|
427
|
+
low = mid + 1;
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
high = mid - 1;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return result;
|
|
434
|
+
}
|
|
435
|
+
/** Exact-match index in a sorted-ascending array (-1 when absent). */
|
|
436
|
+
function binarySearchExact(values, target) {
|
|
437
|
+
let low = 0;
|
|
438
|
+
let high = values.length - 1;
|
|
439
|
+
while (low <= high) {
|
|
440
|
+
const mid = (low + high) >>> 1;
|
|
441
|
+
const value = values[mid] ?? 0;
|
|
442
|
+
if (value === target) {
|
|
443
|
+
return mid;
|
|
444
|
+
}
|
|
445
|
+
if (value < target) {
|
|
446
|
+
low = mid + 1;
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
high = mid - 1;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return -1;
|
|
453
|
+
}
|
|
454
|
+
function isJsWordCode(code) {
|
|
455
|
+
return ((code >= 0x30 && code <= 0x39) ||
|
|
456
|
+
(code >= 0x41 && code <= 0x5a) ||
|
|
457
|
+
(code >= 0x61 && code <= 0x7a) ||
|
|
458
|
+
code === 0x5f ||
|
|
459
|
+
code === 0x24);
|
|
460
|
+
}
|
|
461
|
+
function isIdentStartCode(code) {
|
|
462
|
+
return ((code >= 0x41 && code <= 0x5a) ||
|
|
463
|
+
(code >= 0x61 && code <= 0x7a) ||
|
|
464
|
+
code === 0x5f ||
|
|
465
|
+
code === 0x24);
|
|
466
|
+
}
|
|
467
|
+
function isIdentCharCode(code) {
|
|
468
|
+
return isJsWordCode(code);
|
|
469
|
+
}
|
|
470
|
+
function isJsWhitespaceChar(char) {
|
|
471
|
+
return /\s/.test(char);
|
|
472
|
+
}
|
|
473
|
+
function isLineTerminatorCode(code) {
|
|
474
|
+
return (code === 0x0a || code === 0x0d || code === 0x2028 || code === 0x2029);
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Brace offset of a `class`/`interface`/`type` header starting exactly at
|
|
478
|
+
* `start` (the slice-relative `\b` window edge: a header beginning exactly at
|
|
479
|
+
* the lookback window start counts even when the preceding character would
|
|
480
|
+
* block the word boundary in the full text). `-1` when no header completes
|
|
481
|
+
* before `limit`.
|
|
482
|
+
*/
|
|
483
|
+
function typeBodyHeaderBraceAt(text, start, limit) {
|
|
484
|
+
const keywordLength = text.startsWith("interface", start)
|
|
485
|
+
? "interface".length
|
|
486
|
+
: text.startsWith("class", start)
|
|
487
|
+
? "class".length
|
|
488
|
+
: text.startsWith("type", start)
|
|
489
|
+
? "type".length
|
|
490
|
+
: -1;
|
|
491
|
+
if (keywordLength < 0) {
|
|
492
|
+
return -1;
|
|
493
|
+
}
|
|
494
|
+
let cursor = start + keywordLength;
|
|
495
|
+
if (cursor >= limit || !isJsWhitespaceChar(text[cursor] ?? "")) {
|
|
496
|
+
return -1;
|
|
497
|
+
}
|
|
498
|
+
while (cursor < limit && isJsWhitespaceChar(text[cursor] ?? "")) {
|
|
499
|
+
cursor++;
|
|
500
|
+
}
|
|
501
|
+
if (cursor >= limit || !isIdentStartCode(text.charCodeAt(cursor))) {
|
|
502
|
+
return -1;
|
|
503
|
+
}
|
|
504
|
+
cursor++;
|
|
505
|
+
while (cursor < limit && isIdentCharCode(text.charCodeAt(cursor))) {
|
|
506
|
+
cursor++;
|
|
507
|
+
}
|
|
508
|
+
// `[^{};]*` runs to the first stopper; only `{` can complete the header.
|
|
509
|
+
while (cursor < limit) {
|
|
510
|
+
const code = text.charCodeAt(cursor);
|
|
511
|
+
if (code === 0x7b) {
|
|
512
|
+
return cursor;
|
|
513
|
+
}
|
|
514
|
+
if (code === 0x7d || code === 0x3b) {
|
|
515
|
+
return -1;
|
|
516
|
+
}
|
|
517
|
+
cursor++;
|
|
518
|
+
}
|
|
519
|
+
return -1;
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Index-backed equivalent of the legacy isInsideBoundedTypeBody(prefix,
|
|
523
|
+
* offset) where `prefix` was the caller's window slice starting at
|
|
524
|
+
* `windowStart` (0 for full-text callers). Falls back to the exact legacy
|
|
525
|
+
* slice scan when a header match straddles the window start — the one
|
|
526
|
+
* configuration where slice sub-matches exist that a whole-text matchAll
|
|
527
|
+
* never records.
|
|
528
|
+
*/
|
|
529
|
+
function isInsideBoundedTypeBodyAt(text, windowStart, offset) {
|
|
530
|
+
const lo = Math.max(0, windowStart, offset - MAX_JS_KEY_LOOKBACK);
|
|
531
|
+
if (lo >= offset) {
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
const index = typeBodyIndexFor(text);
|
|
535
|
+
chargeWork(8);
|
|
536
|
+
const straddling = binarySearchLastLessThan(index.starts, lo);
|
|
537
|
+
if (straddling >= 0 &&
|
|
538
|
+
(index.braces[straddling] ?? -1) >= lo) {
|
|
539
|
+
chargeWork(offset - lo);
|
|
540
|
+
return isInsideBoundedTypeBodyBefore(text.slice(lo, offset));
|
|
541
|
+
}
|
|
542
|
+
let brace = -1;
|
|
543
|
+
const lastVisible = binarySearchLastLessThan(index.braces, offset);
|
|
544
|
+
if (lastVisible >= 0 &&
|
|
545
|
+
(index.starts[lastVisible] ?? -1) >= lo) {
|
|
546
|
+
brace = index.braces[lastVisible] ?? -1;
|
|
547
|
+
}
|
|
548
|
+
if (lo > 0 && isJsWordCode(text.charCodeAt(lo - 1))) {
|
|
549
|
+
const synthetic = typeBodyHeaderBraceAt(text, lo, offset);
|
|
550
|
+
if (synthetic > brace) {
|
|
551
|
+
brace = synthetic;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
if (brace < 0) {
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
return ((index.closeAt[brace] ?? index.pastEnd) > offset &&
|
|
558
|
+
(index.exceedAt[brace] ?? index.pastEnd) > offset);
|
|
559
|
+
}
|
|
560
|
+
/** Exact legacy scan over an already-sliced lookback `before`. */
|
|
561
|
+
function isInsideBoundedTypeBodyBefore(before) {
|
|
562
|
+
// Shared header scan on the slice: offset 0 counts as a boundary, exactly
|
|
563
|
+
// the slice-relative \b semantics the slice's own matchAll had. Only the
|
|
564
|
+
// last header's brace feeds the depth walk (legacy last-match semantics).
|
|
565
|
+
// Uncharged: the caller already charges the slice length, and this fallback
|
|
566
|
+
// runs per straddling lookback query (thousands of 32 KiB slices on the
|
|
567
|
+
// dense shapes), so double-charging here would exhaust the budget.
|
|
568
|
+
const classBrace = collectTypeBodyHeaders(before, false).braces.at(-1) ?? -1;
|
|
569
|
+
if (classBrace < 0) {
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
let depth = 0;
|
|
573
|
+
for (let index = classBrace; index < before.length; index++) {
|
|
574
|
+
if (before[index] === "{") {
|
|
575
|
+
depth++;
|
|
576
|
+
}
|
|
577
|
+
else if (before[index] === "}") {
|
|
578
|
+
depth--;
|
|
579
|
+
if (depth === 0) {
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (depth > MAX_TS_TYPE_DEPTH) {
|
|
584
|
+
return false;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return depth > 0;
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Parses `[ \t]*(modifier[ \t]+)*key[?!]?[ \t]*:[ \t]*` starting exactly at
|
|
591
|
+
* `start` — the typed-field pattern with the boundary alternative elided.
|
|
592
|
+
* This reproduces the regex's greedy modifier consumption with rightmost
|
|
593
|
+
* backtracking (e.g. `readonly : T` parses with key `readonly`), which is
|
|
594
|
+
* what a window slice's slice-relative `^` anchoring would see at position 0.
|
|
595
|
+
*/
|
|
596
|
+
function matchTypedFieldContinuationAt(text, start, limit) {
|
|
597
|
+
let cursor = start;
|
|
598
|
+
while (cursor < limit &&
|
|
599
|
+
(text.charCodeAt(cursor) === 0x20 || text.charCodeAt(cursor) === 0x09)) {
|
|
600
|
+
cursor++;
|
|
601
|
+
}
|
|
602
|
+
const modifierStarts = [];
|
|
603
|
+
for (;;) {
|
|
604
|
+
let matched = -1;
|
|
605
|
+
for (const modifier of TYPED_FIELD_MODIFIERS) {
|
|
606
|
+
if (text.startsWith(modifier, cursor)) {
|
|
607
|
+
matched = modifier.length;
|
|
608
|
+
break;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
if (matched < 0 ||
|
|
612
|
+
cursor + matched >= limit ||
|
|
613
|
+
(text.charCodeAt(cursor + matched) !== 0x20 &&
|
|
614
|
+
text.charCodeAt(cursor + matched) !== 0x09)) {
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
modifierStarts.push(cursor);
|
|
618
|
+
cursor += matched;
|
|
619
|
+
while (cursor < limit &&
|
|
620
|
+
(text.charCodeAt(cursor) === 0x20 || text.charCodeAt(cursor) === 0x09)) {
|
|
621
|
+
cursor++;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
for (;;) {
|
|
625
|
+
const keyStart = cursor;
|
|
626
|
+
if (keyStart < limit && isIdentStartCode(text.charCodeAt(keyStart))) {
|
|
627
|
+
let keyEnd = keyStart + 1;
|
|
628
|
+
// The regex key class is [A-Za-z0-9_$-]*: hyphens are legal key
|
|
629
|
+
// continuations (e.g. `api-key: string;`) even though they are not
|
|
630
|
+
// word characters.
|
|
631
|
+
while (keyEnd < limit &&
|
|
632
|
+
(isIdentCharCode(text.charCodeAt(keyEnd)) ||
|
|
633
|
+
text.charCodeAt(keyEnd) === 0x2d)) {
|
|
634
|
+
keyEnd++;
|
|
635
|
+
}
|
|
636
|
+
let afterKey = keyEnd;
|
|
637
|
+
const maybeOptional = text.charCodeAt(afterKey);
|
|
638
|
+
if (maybeOptional === 0x3f || maybeOptional === 0x21) {
|
|
639
|
+
afterKey++;
|
|
640
|
+
}
|
|
641
|
+
while (afterKey < limit &&
|
|
642
|
+
(text.charCodeAt(afterKey) === 0x20 ||
|
|
643
|
+
text.charCodeAt(afterKey) === 0x09)) {
|
|
644
|
+
afterKey++;
|
|
645
|
+
}
|
|
646
|
+
if (afterKey < limit && text.charCodeAt(afterKey) === 0x3a) {
|
|
647
|
+
let typeStart = afterKey + 1;
|
|
648
|
+
while (typeStart < limit &&
|
|
649
|
+
(text.charCodeAt(typeStart) === 0x20 ||
|
|
650
|
+
text.charCodeAt(typeStart) === 0x09)) {
|
|
651
|
+
typeStart++;
|
|
652
|
+
}
|
|
653
|
+
return { keyStart, keyEnd, typeStart };
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
if (modifierStarts.length === 0) {
|
|
657
|
+
return undefined;
|
|
658
|
+
}
|
|
659
|
+
cursor = modifierStarts.pop() ?? start;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
/** Memoized whole-text comment mask; only called on body-level strings. */
|
|
663
|
+
function commentMaskedFor(text) {
|
|
664
|
+
const analysis = analysisFor(text);
|
|
665
|
+
const cached = analysis?.commentMasked;
|
|
666
|
+
if (cached != null) {
|
|
667
|
+
return cached;
|
|
668
|
+
}
|
|
669
|
+
chargeWork(2 * text.length);
|
|
670
|
+
const masked = maskJsCommentTrivia(text);
|
|
671
|
+
if (analysis != null) {
|
|
672
|
+
analysis.commentMasked = masked;
|
|
673
|
+
}
|
|
674
|
+
return masked;
|
|
675
|
+
}
|
|
25
676
|
const PLACEHOLDER_VALUES = new Set([
|
|
26
677
|
"changeme",
|
|
27
678
|
"example",
|
|
@@ -77,6 +728,9 @@ export function scanSecretPath(rootRelativePath) {
|
|
|
77
728
|
return dedupeFindings(findings);
|
|
78
729
|
}
|
|
79
730
|
export function scanHighConfidenceSecretContent(text) {
|
|
731
|
+
return withSecretScanSession(text, () => scanHighConfidenceSecretContentInSession(text), () => [budgetExhaustedFinding()]);
|
|
732
|
+
}
|
|
733
|
+
function scanHighConfidenceSecretContentInSession(text) {
|
|
80
734
|
const findings = [];
|
|
81
735
|
const privateKeyLine = findPatternLine(text, PRIVATE_KEY_BLOCK_PATTERN);
|
|
82
736
|
if (privateKeyLine != null) {
|
|
@@ -98,7 +752,14 @@ export function scanHighConfidenceSecretContent(text) {
|
|
|
98
752
|
export function isBlockingSecretFinding(finding) {
|
|
99
753
|
return finding.provenance !== "assignment_secret";
|
|
100
754
|
}
|
|
755
|
+
/**
|
|
756
|
+
* Budget exhaustion here fails closed by removing the whole body: a partial
|
|
757
|
+
* redaction cannot be proven complete.
|
|
758
|
+
*/
|
|
101
759
|
export function redactHighConfidenceAssignmentSecrets(text) {
|
|
760
|
+
return withSecretScanSession(text, () => redactHighConfidenceAssignmentSecretsInSession(text), () => "");
|
|
761
|
+
}
|
|
762
|
+
function redactHighConfidenceAssignmentSecretsInSession(text) {
|
|
102
763
|
const secretAssignments = [...iterateAssignmentSecrets(text)].filter((assignment) => assignment.confidence === "high").sort((left, right) => left.valueStart - right.valueStart);
|
|
103
764
|
let redacted = text;
|
|
104
765
|
for (let index = secretAssignments.length - 1; index >= 0; index--) {
|
|
@@ -116,6 +777,9 @@ export function redactHighConfidenceAssignmentSecrets(text) {
|
|
|
116
777
|
* content the primary assignment grammar did not classify.
|
|
117
778
|
*/
|
|
118
779
|
export function sanitizeHighConfidenceSecretContent(text, residualDetector = hasConservativeResidualSecret) {
|
|
780
|
+
return withSecretScanSession(text, () => sanitizeHighConfidenceSecretContentInSession(text, residualDetector), () => ({ status: "blocked", content: "", findings: [budgetExhaustedFinding()] }));
|
|
781
|
+
}
|
|
782
|
+
function sanitizeHighConfidenceSecretContentInSession(text, residualDetector) {
|
|
119
783
|
const findings = scanHighConfidenceSecretContent(text);
|
|
120
784
|
if (findings.some(isBlockingSecretFinding)) {
|
|
121
785
|
return { status: "blocked", content: "", findings };
|
|
@@ -146,6 +810,10 @@ export function sanitizeHighConfidenceSecretContent(text, residualDetector = has
|
|
|
146
810
|
* YAML block continuations with a separate, intentionally conservative lexer.
|
|
147
811
|
*/
|
|
148
812
|
export function hasConservativeResidualSecret(text) {
|
|
813
|
+
return withSecretScanSession(text, () => hasConservativeResidualSecretInSession(text), () => true);
|
|
814
|
+
}
|
|
815
|
+
function hasConservativeResidualSecretInSession(text) {
|
|
816
|
+
chargeWork(text.length * (1 + WELL_KNOWN_TOKEN_PATTERNS.length));
|
|
149
817
|
if (PRIVATE_KEY_BLOCK_PATTERN.test(text)) {
|
|
150
818
|
return true;
|
|
151
819
|
}
|
|
@@ -176,7 +844,7 @@ export function hasDisallowedSecretMaterial(text, allowSecrets) {
|
|
|
176
844
|
if (allowSecrets === true) {
|
|
177
845
|
return false;
|
|
178
846
|
}
|
|
179
|
-
return hasHighConfidenceSecret(text) || hasConservativeResidualSecret(text);
|
|
847
|
+
return withSecretScanSession(text, () => hasHighConfidenceSecret(text) || hasConservativeResidualSecret(text), () => true);
|
|
180
848
|
}
|
|
181
849
|
export function assertNoHighConfidenceSecretsInTask(task) {
|
|
182
850
|
if (!hasDisallowedSecretMaterial(task, false)) {
|
|
@@ -244,6 +912,7 @@ function isSecretDirectorySegment(segment) {
|
|
|
244
912
|
segment === "credentials");
|
|
245
913
|
}
|
|
246
914
|
function findPatternLine(text, pattern) {
|
|
915
|
+
chargeWork(2 * text.length);
|
|
247
916
|
const lines = text.split(/\r?\n/);
|
|
248
917
|
for (let index = 0; index < lines.length; index++) {
|
|
249
918
|
if (pattern.test(lines[index] ?? "")) {
|
|
@@ -371,6 +1040,19 @@ function isRedactionMarkerValue(value) {
|
|
|
371
1040
|
.trim().length === 0));
|
|
372
1041
|
}
|
|
373
1042
|
function analyzeYamlStructuralSecrets(text) {
|
|
1043
|
+
const analysis = analysisFor(text);
|
|
1044
|
+
const cached = analysis?.yaml;
|
|
1045
|
+
if (cached != null) {
|
|
1046
|
+
return cached;
|
|
1047
|
+
}
|
|
1048
|
+
const result = analyzeYamlStructuralSecretsUncached(text);
|
|
1049
|
+
if (analysis != null) {
|
|
1050
|
+
analysis.yaml = result;
|
|
1051
|
+
}
|
|
1052
|
+
return result;
|
|
1053
|
+
}
|
|
1054
|
+
function analyzeYamlStructuralSecretsUncached(text) {
|
|
1055
|
+
chargeWork(text.length);
|
|
374
1056
|
if (text.length > MAX_YAML_SOURCE_CHARS) {
|
|
375
1057
|
return { ...emptyYamlStructuralAnalysis(), recognized: true, exhausted: true };
|
|
376
1058
|
}
|
|
@@ -415,6 +1097,7 @@ function analyzeYamlStructuralSecrets(text) {
|
|
|
415
1097
|
let current = node;
|
|
416
1098
|
const active = new Set();
|
|
417
1099
|
for (let depth = 0; depth <= MAX_YAML_DEPTH; depth++) {
|
|
1100
|
+
chargeWork(1);
|
|
418
1101
|
if (current == null || ++nodesSeen > MAX_YAML_NODES) {
|
|
419
1102
|
analysis.exhausted = true;
|
|
420
1103
|
return undefined;
|
|
@@ -436,6 +1119,7 @@ function analyzeYamlStructuralSecrets(text) {
|
|
|
436
1119
|
if (node == null || analysis.exhausted) {
|
|
437
1120
|
return;
|
|
438
1121
|
}
|
|
1122
|
+
chargeWork(1);
|
|
439
1123
|
nodesSeen++;
|
|
440
1124
|
if (nodesSeen > MAX_YAML_NODES || depth > MAX_YAML_DEPTH) {
|
|
441
1125
|
analysis.exhausted = true;
|
|
@@ -644,6 +1328,12 @@ function emptyYamlStructuralAnalysis() {
|
|
|
644
1328
|
};
|
|
645
1329
|
}
|
|
646
1330
|
function hasMalformedYamlSensitiveSecret(text) {
|
|
1331
|
+
const analysis = analysisFor(text);
|
|
1332
|
+
const cached = analysis?.malformedYamlSecret;
|
|
1333
|
+
if (cached != null) {
|
|
1334
|
+
return cached;
|
|
1335
|
+
}
|
|
1336
|
+
let result = false;
|
|
647
1337
|
// Valid TypeScript class fields are not YAML merely because a nested type
|
|
648
1338
|
// property has a sensitive name. Remove only declarations whose bounded RHS
|
|
649
1339
|
// is an unambiguous reference; literal/object RHS values remain visible to
|
|
@@ -656,37 +1346,48 @@ function hasMalformedYamlSensitiveSecret(text) {
|
|
|
656
1346
|
continue;
|
|
657
1347
|
}
|
|
658
1348
|
if (spanContainsHighEntropyLiteral(span)) {
|
|
659
|
-
|
|
1349
|
+
result = true;
|
|
1350
|
+
break;
|
|
660
1351
|
}
|
|
661
1352
|
}
|
|
662
|
-
|
|
1353
|
+
if (analysis != null) {
|
|
1354
|
+
analysis.malformedYamlSecret = result;
|
|
1355
|
+
}
|
|
1356
|
+
return result;
|
|
663
1357
|
}
|
|
664
1358
|
function malformedYamlValueSpan(text, valueStart) {
|
|
665
|
-
const
|
|
1359
|
+
const { lineStarts } = yamlBlockHeaderIndexFor(text);
|
|
1360
|
+
const lineSlot = binarySearchLastLessThan(lineStarts, valueStart + 1);
|
|
1361
|
+
let lineIndex = lineSlot < 0 ? 0 : lineSlot;
|
|
1362
|
+
const firstLineEnd = lineEndViaStarts(text, lineStarts, lineIndex);
|
|
666
1363
|
const firstLine = text.slice(valueStart, firstLineEnd);
|
|
667
1364
|
const firstValue = firstLine.replace(/^[ \t]*/, "");
|
|
668
1365
|
const continues = firstValue.length === 0 ||
|
|
669
1366
|
firstValue.startsWith("#") ||
|
|
670
|
-
|
|
1367
|
+
MALFORMED_YAML_CONTINUES_PATTERN.test(firstValue);
|
|
671
1368
|
if (!continues) {
|
|
672
1369
|
return firstLine;
|
|
673
1370
|
}
|
|
674
|
-
const keyLineStart =
|
|
1371
|
+
const keyLineStart = lineStarts[lineIndex] ?? 0;
|
|
675
1372
|
const keyIndent = leadingIndentWidth(text.slice(keyLineStart, valueStart));
|
|
676
1373
|
let cursor = firstLineEnd;
|
|
677
1374
|
let end = firstLineEnd;
|
|
678
1375
|
let lines = 0;
|
|
679
1376
|
while (cursor < text.length && lines <= 512 && cursor - valueStart < 32_768) {
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
: text[cursor] === "\n"
|
|
683
|
-
? 1
|
|
684
|
-
: 0;
|
|
685
|
-
if (newlineLength === 0) {
|
|
1377
|
+
chargeWork(1);
|
|
1378
|
+
if (lineIndex + 1 >= lineStarts.length) {
|
|
686
1379
|
break;
|
|
687
1380
|
}
|
|
688
|
-
const lineStart =
|
|
689
|
-
|
|
1381
|
+
const lineStart = lineStarts[lineIndex + 1] ?? text.length;
|
|
1382
|
+
if (text.charCodeAt(cursor) === 0x0d) {
|
|
1383
|
+
if (text.charCodeAt(cursor + 1) !== 0x0a) {
|
|
1384
|
+
break;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
else if (text.charCodeAt(cursor) !== 0x0a) {
|
|
1388
|
+
break;
|
|
1389
|
+
}
|
|
1390
|
+
const lineEnd = lineEndViaStarts(text, lineStarts, lineIndex + 1);
|
|
690
1391
|
const line = text.slice(lineStart, lineEnd);
|
|
691
1392
|
if (line.trim().length > 0 && leadingIndentWidth(line) <= keyIndent) {
|
|
692
1393
|
break;
|
|
@@ -694,25 +1395,89 @@ function malformedYamlValueSpan(text, valueStart) {
|
|
|
694
1395
|
end = lineEnd;
|
|
695
1396
|
cursor = lineEnd;
|
|
696
1397
|
lines++;
|
|
1398
|
+
lineIndex++;
|
|
697
1399
|
}
|
|
698
1400
|
return text.slice(valueStart, end);
|
|
699
1401
|
}
|
|
1402
|
+
/** End offset (exclusive of the line terminator) of line `lineIndex`. */
|
|
1403
|
+
function lineEndViaStarts(text, lineStarts, lineIndex) {
|
|
1404
|
+
if (lineIndex + 1 >= lineStarts.length) {
|
|
1405
|
+
return text.length;
|
|
1406
|
+
}
|
|
1407
|
+
const nextStart = lineStarts[lineIndex + 1] ?? text.length;
|
|
1408
|
+
const newline = nextStart - 1;
|
|
1409
|
+
return newline > 0 && text.charCodeAt(newline - 1) === 0x0d
|
|
1410
|
+
? newline - 1
|
|
1411
|
+
: newline;
|
|
1412
|
+
}
|
|
700
1413
|
function maskConfidentlySafeTypeOnlyFields(text) {
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
1414
|
+
// Manual linear scan reproducing
|
|
1415
|
+
// /(^|[;{}\r\n])(prefix)([\s\S]{1,4096}?);/gm.replace semantics exactly:
|
|
1416
|
+
// `^` only at line starts, `;`-terminated lazy type bodies resolved through
|
|
1417
|
+
// the next-semicolon index, and the scan resuming after each match's `;`
|
|
1418
|
+
// whether or not the match was rewritten.
|
|
1419
|
+
const nextSemicolon = nextSemicolonFor(text);
|
|
1420
|
+
const isTypeBodyBoundaryChar = (code) => code === 0x3b ||
|
|
1421
|
+
code === 0x7b ||
|
|
1422
|
+
code === 0x7d ||
|
|
1423
|
+
code === 0x0d ||
|
|
1424
|
+
code === 0x0a;
|
|
1425
|
+
let output = "";
|
|
1426
|
+
let copied = 0;
|
|
1427
|
+
let position = 0;
|
|
1428
|
+
while (position < text.length) {
|
|
1429
|
+
chargeWork(1);
|
|
1430
|
+
const atLineStart = position === 0 || isLineTerminatorCode(text.charCodeAt(position - 1));
|
|
1431
|
+
let boundaryLength = -1;
|
|
1432
|
+
let continuation;
|
|
1433
|
+
if (atLineStart) {
|
|
1434
|
+
continuation = matchTypedFieldContinuationAt(text, position, text.length);
|
|
1435
|
+
if (continuation != null) {
|
|
1436
|
+
boundaryLength = 0;
|
|
1437
|
+
}
|
|
706
1438
|
}
|
|
707
|
-
|
|
708
|
-
|
|
1439
|
+
if (boundaryLength < 0 && isTypeBodyBoundaryChar(text.charCodeAt(position))) {
|
|
1440
|
+
const afterBoundary = matchTypedFieldContinuationAt(text, position + 1, text.length);
|
|
1441
|
+
if (afterBoundary != null) {
|
|
1442
|
+
boundaryLength = 1;
|
|
1443
|
+
continuation = afterBoundary;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
if (boundaryLength < 0 || continuation == null) {
|
|
1447
|
+
position++;
|
|
1448
|
+
continue;
|
|
1449
|
+
}
|
|
1450
|
+
const typeStart = continuation.typeStart;
|
|
1451
|
+
const terminator = typeStart + 1 <= text.length ? nextSemicolon[typeStart + 1] ?? -1 : -1;
|
|
1452
|
+
if (terminator < 0 || terminator - typeStart > MAX_TS_TYPE_ONLY_FIELD_CHARS) {
|
|
1453
|
+
// The lazy group cannot reach a `;` in range. Prefix backtracking never
|
|
1454
|
+
// rescues the match: a shorter modifier run leaves a keyword followed by
|
|
1455
|
+
// whitespace, which the key production cannot re-parse.
|
|
1456
|
+
position++;
|
|
1457
|
+
continue;
|
|
1458
|
+
}
|
|
1459
|
+
const type = text.slice(typeStart, terminator);
|
|
1460
|
+
if (isInsideBoundedTypeBodyAt(text, 0, position + boundaryLength) &&
|
|
1461
|
+
looksLikeTsTypeExpression(type, true)) {
|
|
1462
|
+
output += text.slice(copied, position);
|
|
1463
|
+
output += text.slice(position, position + boundaryLength);
|
|
1464
|
+
for (let index = position + boundaryLength; index <= terminator; index++) {
|
|
1465
|
+
const code = text.charCodeAt(index);
|
|
1466
|
+
output += code === 0x0a || code === 0x0d ? text[index] ?? "" : " ";
|
|
1467
|
+
}
|
|
1468
|
+
copied = terminator + 1;
|
|
1469
|
+
}
|
|
1470
|
+
position = terminator + 1;
|
|
1471
|
+
}
|
|
1472
|
+
return output + text.slice(copied);
|
|
709
1473
|
}
|
|
710
1474
|
function maskConfidentlySafeTypedFields(text) {
|
|
711
|
-
const masked =
|
|
1475
|
+
const masked = commentMaskedFor(text);
|
|
712
1476
|
const safeSpans = [];
|
|
713
1477
|
let quote;
|
|
714
1478
|
let escaped = false;
|
|
715
1479
|
for (let equalsAt = 0; equalsAt < masked.length; equalsAt++) {
|
|
1480
|
+
chargeWork(1);
|
|
716
1481
|
const character = masked[equalsAt] ?? "";
|
|
717
1482
|
if (quote != null) {
|
|
718
1483
|
if (!escaped && character === quote) {
|
|
@@ -731,8 +1496,9 @@ function maskConfidentlySafeTypedFields(text) {
|
|
|
731
1496
|
/[=>]/.test(masked[equalsAt + 1] ?? "")) {
|
|
732
1497
|
continue;
|
|
733
1498
|
}
|
|
1499
|
+
chargeWork(32);
|
|
734
1500
|
const windowStart = Math.max(0, equalsAt - MAX_JS_KEY_LOOKBACK);
|
|
735
|
-
const field = findBoundedTypedField(masked
|
|
1501
|
+
const field = findBoundedTypedField(masked, windowStart, equalsAt);
|
|
736
1502
|
if (field == null || field === "exhausted") {
|
|
737
1503
|
continue;
|
|
738
1504
|
}
|
|
@@ -773,6 +1539,7 @@ function isSafeTypedFieldReference(source, value) {
|
|
|
773
1539
|
/^keyof\s+typeof\s+[A-Za-z_$][A-Za-z0-9_$]*(?:(?:\.|::)[A-Za-z_$][A-Za-z0-9_$]*)*$/.test(expression))));
|
|
774
1540
|
}
|
|
775
1541
|
function malformedYamlSensitiveValueStarts(text) {
|
|
1542
|
+
chargeWork(4 * text.length);
|
|
776
1543
|
const starts = [];
|
|
777
1544
|
const keyToken = String.raw `[A-Za-z_$][A-Za-z0-9_$-]*(?:\.[A-Za-z_$][A-Za-z0-9_$-]*)*`;
|
|
778
1545
|
for (const match of text.matchAll(/^[ \t]*(["'])((?:\\.|(?!\1)[^\r\n])*)\1[ \t]*:/gm)) {
|
|
@@ -827,22 +1594,31 @@ function redactYamlScalarSource(source) {
|
|
|
827
1594
|
return redactYamlBlockPayload(source);
|
|
828
1595
|
}
|
|
829
1596
|
function lineNumberAtOffset(text, offset) {
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
if (text[index] === "\n") {
|
|
833
|
-
line++;
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
return line;
|
|
1597
|
+
const newlineOffsets = newlineOffsetsFor(text);
|
|
1598
|
+
return 1 + binarySearchLastLessThan(newlineOffsets, offset) + 1;
|
|
837
1599
|
}
|
|
838
1600
|
function analyzeJsStructuralSecrets(text, primaryRedaction = true) {
|
|
839
|
-
const
|
|
1601
|
+
const analysis = analysisFor(text);
|
|
1602
|
+
const slot = primaryRedaction ? "jsPrimary" : "jsResidual";
|
|
1603
|
+
const cached = analysis?.[slot];
|
|
1604
|
+
if (cached != null) {
|
|
1605
|
+
return cached;
|
|
1606
|
+
}
|
|
1607
|
+
const result = analyzeJsStructuralSecretsUncached(text, primaryRedaction);
|
|
1608
|
+
if (analysis != null) {
|
|
1609
|
+
analysis[slot] = result;
|
|
1610
|
+
}
|
|
1611
|
+
return result;
|
|
1612
|
+
}
|
|
1613
|
+
function analyzeJsStructuralSecretsUncached(text, primaryRedaction) {
|
|
1614
|
+
const masked = commentMaskedFor(text);
|
|
840
1615
|
const matches = [];
|
|
841
1616
|
let unsafeSensitiveContext = false;
|
|
842
1617
|
let quote;
|
|
843
1618
|
let escaped = false;
|
|
844
1619
|
let scanned = 0;
|
|
845
1620
|
for (let cursor = 0; cursor < masked.length; cursor++) {
|
|
1621
|
+
chargeWork(1);
|
|
846
1622
|
if (++scanned > 1_000_000) {
|
|
847
1623
|
return { exhausted: true, unsafeSensitiveContext, matches };
|
|
848
1624
|
}
|
|
@@ -864,6 +1640,7 @@ function analyzeJsStructuralSecrets(text, primaryRedaction = true) {
|
|
|
864
1640
|
/[=>]/.test(masked[cursor + 1] ?? "")) {
|
|
865
1641
|
continue;
|
|
866
1642
|
}
|
|
1643
|
+
chargeWork(32);
|
|
867
1644
|
const key = sensitiveJsKeyBefore(text, masked, cursor);
|
|
868
1645
|
if (key === "exhausted") {
|
|
869
1646
|
const span = residualJsValueSpan(text, masked, cursor + 1);
|
|
@@ -899,7 +1676,7 @@ function analyzeJsStructuralSecrets(text, primaryRedaction = true) {
|
|
|
899
1676
|
continue;
|
|
900
1677
|
}
|
|
901
1678
|
const typedFieldWindowStart = Math.max(0, cursor - MAX_JS_KEY_LOOKBACK);
|
|
902
|
-
const typedField = findBoundedTypedField(masked
|
|
1679
|
+
const typedField = findBoundedTypedField(masked, typedFieldWindowStart, cursor);
|
|
903
1680
|
const safeTypedReference = typedField != null &&
|
|
904
1681
|
typedField !== "exhausted" &&
|
|
905
1682
|
reconstructed != null &&
|
|
@@ -993,11 +1770,11 @@ function spanContainsHighEntropyLiteral(span) {
|
|
|
993
1770
|
}
|
|
994
1771
|
function sensitiveJsKeyBefore(text, masked, equalsAt) {
|
|
995
1772
|
const windowStart = Math.max(0, equalsAt - MAX_JS_KEY_LOOKBACK);
|
|
996
|
-
const
|
|
997
|
-
const typedField = findSensitiveTypedField(prefix, windowStart);
|
|
1773
|
+
const typedField = findSensitiveTypedField(masked, windowStart, equalsAt);
|
|
998
1774
|
if (typedField != null) {
|
|
999
1775
|
return typedField;
|
|
1000
1776
|
}
|
|
1777
|
+
const prefix = masked.slice(windowStart, equalsAt);
|
|
1001
1778
|
const bracket = /\[\s*(["'])((?:\\.|(?!\1)[^\r\n])*)\1\s*\]\s*$/.exec(prefix);
|
|
1002
1779
|
if (bracket != null) {
|
|
1003
1780
|
const encoded = bracket[2] ?? "";
|
|
@@ -1079,8 +1856,8 @@ function classifyTsTypeAnnotation(value) {
|
|
|
1079
1856
|
}
|
|
1080
1857
|
return stack.length === 0 ? "valid" : "unclassifiable";
|
|
1081
1858
|
}
|
|
1082
|
-
function findSensitiveTypedField(
|
|
1083
|
-
const field = findBoundedTypedField(
|
|
1859
|
+
function findSensitiveTypedField(source, windowStart, windowEnd) {
|
|
1860
|
+
const field = findBoundedTypedField(source, windowStart, windowEnd);
|
|
1084
1861
|
if (field === "exhausted" || field == null || field.sensitive) {
|
|
1085
1862
|
return field === "exhausted" || field == null
|
|
1086
1863
|
? field
|
|
@@ -1088,23 +1865,30 @@ function findSensitiveTypedField(prefix, absoluteStart) {
|
|
|
1088
1865
|
}
|
|
1089
1866
|
return undefined;
|
|
1090
1867
|
}
|
|
1091
|
-
function findBoundedTypedField(
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1868
|
+
function findBoundedTypedField(source, windowStart, windowEnd) {
|
|
1869
|
+
// Index-backed enumeration. A window slice's matchAll could additionally
|
|
1870
|
+
// match at position 0 through the `^` alternative (no boundary character
|
|
1871
|
+
// consumed) — including continuations of a declaration whose boundary sits
|
|
1872
|
+
// just before the window and mid-identifier splits. That candidate comes
|
|
1873
|
+
// first in slice scan order; precomputed matches starting inside it would
|
|
1874
|
+
// have been consumed by the slice scan, so iteration resumes from its end.
|
|
1875
|
+
const candidates = typedFieldCandidatesFor(source);
|
|
1876
|
+
chargeWork(8);
|
|
1877
|
+
const synthetic = matchTypedFieldContinuationAt(source, windowStart, windowEnd);
|
|
1878
|
+
const evaluate = (keyStart, keyEnd, typeStart) => {
|
|
1879
|
+
const rawKey = source.slice(keyStart, keyEnd);
|
|
1880
|
+
if (!isInsideBoundedTypeBodyAt(source, windowStart, keyStart)) {
|
|
1881
|
+
return "continue";
|
|
1882
|
+
}
|
|
1883
|
+
// The legacy window ended at windowEnd, so the type annotation under
|
|
1884
|
+
// examination is bounded by it exactly as the slice was.
|
|
1885
|
+
const type = source.slice(typeStart, windowEnd).trim();
|
|
1102
1886
|
const classification = classifyTsTypeAnnotation(type);
|
|
1103
1887
|
if (classification === "member_boundary") {
|
|
1104
|
-
continue;
|
|
1888
|
+
return "continue";
|
|
1105
1889
|
}
|
|
1106
1890
|
if (classification === "nested_candidate") {
|
|
1107
|
-
continue;
|
|
1891
|
+
return "continue";
|
|
1108
1892
|
}
|
|
1109
1893
|
if (classification === "unclassifiable") {
|
|
1110
1894
|
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(rawKey) &&
|
|
@@ -1114,41 +1898,34 @@ function findBoundedTypedField(prefix, absoluteStart) {
|
|
|
1114
1898
|
}
|
|
1115
1899
|
// The first candidate whose entire suffix is a balanced type annotation is
|
|
1116
1900
|
// the outer field. Nested object-type properties leave unmatched closers.
|
|
1117
|
-
const start = absoluteStart + relativeKeyStart;
|
|
1118
1901
|
return {
|
|
1119
|
-
start,
|
|
1120
|
-
end:
|
|
1902
|
+
start: keyStart,
|
|
1903
|
+
end: keyEnd,
|
|
1121
1904
|
sensitive: isSensitiveDecodedKey(rawKey),
|
|
1122
1905
|
};
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
if (before[index] === "{") {
|
|
1139
|
-
depth++;
|
|
1140
|
-
}
|
|
1141
|
-
else if (before[index] === "}") {
|
|
1142
|
-
depth--;
|
|
1143
|
-
if (depth === 0) {
|
|
1144
|
-
return false;
|
|
1145
|
-
}
|
|
1906
|
+
};
|
|
1907
|
+
if (synthetic != null) {
|
|
1908
|
+
chargeWork(4);
|
|
1909
|
+
const outcome = evaluate(synthetic.keyStart, synthetic.keyEnd, synthetic.typeStart);
|
|
1910
|
+
if (outcome !== "continue") {
|
|
1911
|
+
return outcome;
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
const resumeFrom = synthetic != null ? synthetic.typeStart : windowStart;
|
|
1915
|
+
let slot = binarySearchLastLessThan(candidates.matchStarts, resumeFrom) + 1;
|
|
1916
|
+
while (slot < candidates.count) {
|
|
1917
|
+
chargeWork(4);
|
|
1918
|
+
const typeStart = candidates.typeStarts[slot] ?? 0;
|
|
1919
|
+
if (typeStart > windowEnd) {
|
|
1920
|
+
break;
|
|
1146
1921
|
}
|
|
1147
|
-
|
|
1148
|
-
|
|
1922
|
+
const outcome = evaluate(candidates.keyStarts[slot] ?? typeStart, candidates.keyEnds[slot] ?? typeStart, typeStart);
|
|
1923
|
+
if (outcome !== "continue") {
|
|
1924
|
+
return outcome;
|
|
1149
1925
|
}
|
|
1926
|
+
slot++;
|
|
1150
1927
|
}
|
|
1151
|
-
return
|
|
1928
|
+
return undefined;
|
|
1152
1929
|
}
|
|
1153
1930
|
function findTsFieldAssignmentEquals(text, start, end) {
|
|
1154
1931
|
for (let index = start; index < end; index++) {
|
|
@@ -1253,12 +2030,15 @@ function* iterateAssignmentSecrets(text) {
|
|
|
1253
2030
|
if (!js.exhausted) {
|
|
1254
2031
|
yield* js.matches;
|
|
1255
2032
|
}
|
|
1256
|
-
const commentMasked =
|
|
2033
|
+
const commentMasked = commentMaskedFor(text);
|
|
2034
|
+
const nextSemicolon = nextSemicolonFor(commentMasked);
|
|
2035
|
+
chargeWork(2 * text.length);
|
|
1257
2036
|
let line = 1;
|
|
1258
2037
|
let lineCursor = 0;
|
|
1259
2038
|
const keyPattern = new RegExp(ASSIGNMENT_KEY_PATTERN.source, ASSIGNMENT_KEY_PATTERN.flags);
|
|
1260
2039
|
let match;
|
|
1261
2040
|
while ((match = keyPattern.exec(commentMasked)) != null) {
|
|
2041
|
+
chargeWork(16);
|
|
1262
2042
|
const matchStart = match.index;
|
|
1263
2043
|
for (let index = lineCursor; index < matchStart; index++) {
|
|
1264
2044
|
if (text[index] === "\n") {
|
|
@@ -1293,19 +2073,21 @@ function* iterateAssignmentSecrets(text) {
|
|
|
1293
2073
|
if (delimiter === ":") {
|
|
1294
2074
|
const declarationStart = text.lastIndexOf("\n", matchStart - 1) + 1;
|
|
1295
2075
|
const declarationEnd = lineEndIndex(text, cursor);
|
|
1296
|
-
const typeTerminator =
|
|
2076
|
+
const typeTerminator = cursor + 1 <= commentMasked.length
|
|
2077
|
+
? nextSemicolon[cursor + 1] ?? -1
|
|
2078
|
+
: -1;
|
|
1297
2079
|
const typeOnlyDeclaration = typeTerminator >= 0 &&
|
|
1298
2080
|
typeTerminator <= declarationEnd
|
|
1299
2081
|
? commentMasked.slice(cursor + 1, typeTerminator).trim()
|
|
1300
2082
|
: "";
|
|
1301
2083
|
if (typeOnlyDeclaration.length > 0 &&
|
|
1302
|
-
|
|
2084
|
+
isInsideBoundedTypeBodyAt(commentMasked, 0, keyStart) &&
|
|
1303
2085
|
looksLikeTsTypeExpression(typeOnlyDeclaration, true)) {
|
|
1304
2086
|
continue;
|
|
1305
2087
|
}
|
|
1306
2088
|
const assignmentEquals = findTsFieldAssignmentEquals(commentMasked, cursor + 1, declarationEnd);
|
|
1307
2089
|
if (assignmentEquals != null) {
|
|
1308
|
-
const typedField = findBoundedTypedField(commentMasked
|
|
2090
|
+
const typedField = findBoundedTypedField(commentMasked, declarationStart, assignmentEquals);
|
|
1309
2091
|
if (typedField != null) {
|
|
1310
2092
|
continue;
|
|
1311
2093
|
}
|
|
@@ -1373,7 +2155,7 @@ function* iterateAssignmentSecrets(text) {
|
|
|
1373
2155
|
const tailStart = skipHorizontalWhitespace(text, valueEnd + 1);
|
|
1374
2156
|
if (text[tailStart] === "+") {
|
|
1375
2157
|
valueStart = cursor;
|
|
1376
|
-
const logicalSpan = residualJsValueSpan(text,
|
|
2158
|
+
const logicalSpan = residualJsValueSpan(text, commentMaskedFor(text), cursor);
|
|
1377
2159
|
valueEnd = cursor + logicalSpan.content.length;
|
|
1378
2160
|
}
|
|
1379
2161
|
}
|
|
@@ -1577,8 +2359,11 @@ function residualJsValueSpan(text, commentMasked, valueStart) {
|
|
|
1577
2359
|
(cursor - valueStart >= 32_768 || lineCount >= 64),
|
|
1578
2360
|
};
|
|
1579
2361
|
}
|
|
2362
|
+
// Returns `text` itself when nothing is masked: callers key memo Maps by the
|
|
2363
|
+
// result, and Node 24's V8 compares a distinct-but-equal string per lookup.
|
|
1580
2364
|
function maskJsCommentTrivia(text) {
|
|
1581
2365
|
const output = text.split("");
|
|
2366
|
+
let changed = false;
|
|
1582
2367
|
let quote;
|
|
1583
2368
|
let escaped = false;
|
|
1584
2369
|
for (let index = 0; index < text.length; index++) {
|
|
@@ -1595,6 +2380,7 @@ function maskJsCommentTrivia(text) {
|
|
|
1595
2380
|
continue;
|
|
1596
2381
|
}
|
|
1597
2382
|
if (character === "/" && text[index + 1] === "*") {
|
|
2383
|
+
changed = true;
|
|
1598
2384
|
output[index] = " ";
|
|
1599
2385
|
output[index + 1] = " ";
|
|
1600
2386
|
index += 2;
|
|
@@ -1613,6 +2399,7 @@ function maskJsCommentTrivia(text) {
|
|
|
1613
2399
|
continue;
|
|
1614
2400
|
}
|
|
1615
2401
|
if (character === "/" && text[index + 1] === "/") {
|
|
2402
|
+
changed = true;
|
|
1616
2403
|
output[index] = " ";
|
|
1617
2404
|
output[index + 1] = " ";
|
|
1618
2405
|
index += 2;
|
|
@@ -1623,7 +2410,7 @@ function maskJsCommentTrivia(text) {
|
|
|
1623
2410
|
index--;
|
|
1624
2411
|
}
|
|
1625
2412
|
}
|
|
1626
|
-
return output.join("");
|
|
2413
|
+
return changed ? output.join("") : text;
|
|
1627
2414
|
}
|
|
1628
2415
|
function shannonEntropy(value) {
|
|
1629
2416
|
const counts = new Map();
|