@abelxiaoxing/cadence 1.8.4 → 2.0.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/README.md +324 -117
- package/THIRD_PARTY_NOTICES.md +15 -0
- package/agents/design-explorer.md +14 -18
- package/agents/diagnosis-worker.md +14 -14
- package/agents/implementation-worker.md +16 -17
- package/config/workflow-task.example.json +7 -0
- package/docs/remote-access.md +60 -0
- package/docs/reuse-and-distribution.md +44 -0
- package/docs/service-management.md +108 -0
- package/licenses/bundled-web.txt +6245 -0
- package/licenses/pi-web-ui-MIT.txt +21 -0
- package/licenses/pi-web-ui-reuse.md +22 -0
- package/package.json +92 -20
- package/src/index.ts +21 -1802
- package/src/native-build.ts +6 -0
- package/src/native-client.ts +103 -0
- package/src/native-export.ts +58 -0
- package/src/native-history.ts +359 -0
- package/src/operator-cli.mjs +453 -1937
- package/src/plan-proposal.ts +87 -0
- package/src/sdk-worker.mjs +549 -0
- package/src/service/contracts/active.ts +82 -0
- package/src/service/contracts/budget.ts +148 -0
- package/src/service/contracts/build.ts +67 -0
- package/src/service/contracts/capture.ts +48 -0
- package/src/service/contracts/core.ts +33 -0
- package/src/service/contracts/current.ts +261 -0
- package/src/service/contracts/domain.ts +68 -0
- package/src/service/contracts/history.ts +2245 -0
- package/src/service/contracts/models.ts +139 -0
- package/src/service/coordination.ts +862 -0
- package/src/service/private-files.ts +204 -0
- package/src/service-cli.mjs +5592 -0
- package/src/service-client.ts +747 -0
- package/src/service-contract.ts +245 -0
- package/src/service-host.mjs +22382 -0
- package/src/service-http-client.ts +425 -0
- package/src/service-wire.ts +372 -0
- package/src/task-shell.ts +1045 -0
- package/src/task-store.ts +479 -0
- package/src/task-types.ts +184 -0
- package/src/verification-diagnostics.ts +29 -72
- package/src/worker-capture.mjs +1197 -0
- package/src/worker-public.mjs +1753 -0
- package/src/worker-redaction.mjs +608 -0
- package/src/worker-tools.mjs +202 -0
- package/web/dist/assets/KaTeX_AMS-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold.woff +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Bold.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-Bold.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic.woff +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Italic.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-Italic.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic.woff +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/web/dist/assets/KaTeX_Math-Italic.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-Italic.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Script-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Script-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular.ttf +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular.woff +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/web/dist/assets/cadence.css +2 -0
- package/web/dist/assets/cadence.js +7 -0
- package/web/dist/assets/markdown.js +296 -0
- package/web/dist/assets/react.js +9 -0
- package/web/dist/assets/rolldown-runtime.js +1 -0
- package/web/dist/assets/timeline-client.js +41 -0
- package/web/dist/assets/vis-timeline.css +1274 -0
- package/web/dist/assets/vis-timeline.js +32 -0
- package/web/dist/index.html +18 -0
- package/web/dist/manifest.json +505 -0
- package/config/plan-draft.example.json +0 -125
- package/config/plan-draft.multiple-tasks.example.json +0 -375
- package/config/plan-draft.quick.example.json +0 -78
- package/prompts/abel-design.md +0 -382
- package/prompts/abel-diagnose.md +0 -71
- package/prompts/abel-implement.md +0 -284
- package/prompts/abel-init.md +0 -52
- package/src/activation.ts +0 -44
- package/src/activity-contracts.ts +0 -82
- package/src/agent-registry.ts +0 -53
- package/src/apply-transaction.ts +0 -1437
- package/src/artifact-store.ts +0 -213
- package/src/build-windows-job.mjs +0 -95
- package/src/candidate-artifact.ts +0 -143
- package/src/candidate-context.ts +0 -289
- package/src/candidate-patch.ts +0 -271
- package/src/candidate-workspace.ts +0 -80
- package/src/canonical.ts +0 -16
- package/src/change-contract-schema.ts +0 -191
- package/src/change-contract.ts +0 -324
- package/src/change-verification.ts +0 -1617
- package/src/contracts.ts +0 -2466
- package/src/control-contracts.ts +0 -382
- package/src/delivery-compiler.ts +0 -1805
- package/src/delivery-revision.ts +0 -42
- package/src/design-control.ts +0 -1299
- package/src/design-diagnostics.ts +0 -181
- package/src/design-journal.ts +0 -1773
- package/src/directory-sync.ts +0 -36
- package/src/durable-contracts.ts +0 -227
- package/src/durable-workflow.ts +0 -822
- package/src/evidence-draft.ts +0 -122
- package/src/execution-profile.ts +0 -64
- package/src/execution-retention.ts +0 -77
- package/src/file-snapshot.ts +0 -163
- package/src/implement-continuation.ts +0 -559
- package/src/implement-graph.ts +0 -690
- package/src/implement-plan.ts +0 -72
- package/src/install-config.d.mts +0 -6
- package/src/isolation-backend.ts +0 -353
- package/src/model-source.ts +0 -22
- package/src/openspec-cli.ts +0 -494
- package/src/operator-cli-entry.mjs +0 -24
- package/src/operator-tools.ts +0 -309
- package/src/package-candidate-process.ts +0 -206
- package/src/package-candidate.ts +0 -24
- package/src/package-delivery.ts +0 -486
- package/src/package-state.ts +0 -320
- package/src/package-verification.ts +0 -635
- package/src/package-workflow.ts +0 -305
- package/src/packet-runtime.ts +0 -459
- package/src/phase-execution.ts +0 -2163
- package/src/pi-adapter.ts +0 -19
- package/src/plan-draft-summary.ts +0 -527
- package/src/plan-draft.ts +0 -502
- package/src/remove-path.ts +0 -32
- package/src/run-state.ts +0 -420
- package/src/run-store.ts +0 -1179
- package/src/safe-path.ts +0 -77
- package/src/scoped-grep-worker.mjs +0 -23
- package/src/scoped-tools.ts +0 -775
- package/src/single-task-draft.ts +0 -142
- package/src/sqlite-schema.ts +0 -171
- package/src/state-root.ts +0 -215
- package/src/storage-schema.ts +0 -360
- package/src/subagent-activity.ts +0 -1417
- package/src/subagent-process.ts +0 -476
- package/src/task-ledger.ts +0 -1338
- package/src/transport-budget.ts +0 -50
- package/src/verification-capability.ts +0 -1191
- package/src/verification-environment-io.ts +0 -389
- package/src/verification-environment.ts +0 -162
- package/src/verification-identity.ts +0 -77
- package/src/verification-scan.ts +0 -64
- package/src/windows-job-backend.ts +0 -419
- package/src/windows-job.c +0 -149
- package/src/workflow-engine.ts +0 -28
- package/src/workflow-policy.ts +0 -1670
- package/src/workflow-recovery-policy.ts +0 -97
- package/src/workflow-scheduling.ts +0 -105
- package/src/workflow-state-machine.ts +0 -4341
- package/src/workflow-status.ts +0 -576
- package/src/workspace-io-entry.mjs +0 -76
- package/src/workspace-io-worker.mjs +0 -1312
- package/src/workspace-io.ts +0 -89
- package/src/workspace-store.ts +0 -634
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ContractError,
|
|
3
|
+
canonicalJson,
|
|
4
|
+
LIMITS,
|
|
5
|
+
utf8Bytes,
|
|
6
|
+
} from "./worker-public.mjs";
|
|
7
|
+
|
|
8
|
+
const marker = "[redacted]";
|
|
9
|
+
const sensitive =
|
|
10
|
+
/^(?:authorization|proxy-authorization|cookie|set-cookie|password|passwd|secret|token|api[-_]?key|access[-_]?token|refresh[-_]?token|credentials|headers|env)$/i;
|
|
11
|
+
/** Own data only. Deliberately never reads a getter, toJSON, exception or prototype method. */
|
|
12
|
+
export function own(value, name) {
|
|
13
|
+
if (!value || typeof value !== "object") throw new ContractError();
|
|
14
|
+
const d = Object.getOwnPropertyDescriptor(value, name);
|
|
15
|
+
if (!d) return undefined;
|
|
16
|
+
if (!("value" in d)) throw new ContractError();
|
|
17
|
+
return d.value;
|
|
18
|
+
}
|
|
19
|
+
export function ownArray(value, maximum = LIMITS.dataFrames) {
|
|
20
|
+
if (!Array.isArray(value)) throw new ContractError();
|
|
21
|
+
const length = own(value, "length");
|
|
22
|
+
if (!Number.isSafeInteger(length) || length > maximum)
|
|
23
|
+
throw new ContractError("oversize");
|
|
24
|
+
return Array.from({ length }, (_, index) => own(value, String(index)));
|
|
25
|
+
}
|
|
26
|
+
function sourceBytes(value) {
|
|
27
|
+
let bytes = 0;
|
|
28
|
+
for (let i = 0; i < value.length; i++) {
|
|
29
|
+
const code = value.charCodeAt(i);
|
|
30
|
+
if (code < 0x80) bytes++;
|
|
31
|
+
else if (code < 0x800) bytes += 2;
|
|
32
|
+
else if (
|
|
33
|
+
code >= 0xd800 &&
|
|
34
|
+
code <= 0xdbff &&
|
|
35
|
+
value.charCodeAt(i + 1) >= 0xdc00 &&
|
|
36
|
+
value.charCodeAt(i + 1) <= 0xdfff
|
|
37
|
+
) {
|
|
38
|
+
bytes += 4;
|
|
39
|
+
i++;
|
|
40
|
+
} else if (code >= 0xd800 && code <= 0xdfff) throw new ContractError();
|
|
41
|
+
else bytes += 3;
|
|
42
|
+
}
|
|
43
|
+
return bytes;
|
|
44
|
+
}
|
|
45
|
+
/** Charge before cloning. Used at synchronous source ingress as well as public tool values. */
|
|
46
|
+
export function measureData(value, maximum = LIMITS.workerQueueBytes) {
|
|
47
|
+
let bytes = 0;
|
|
48
|
+
let nodes = 0;
|
|
49
|
+
const seen = new Set();
|
|
50
|
+
function visit(v, depth) {
|
|
51
|
+
if (++nodes > LIMITS.dataFrames || depth > 64)
|
|
52
|
+
throw new ContractError("oversize");
|
|
53
|
+
if (typeof v === "string") bytes += sourceBytes(v) * 6 + 2;
|
|
54
|
+
else if (v === null || typeof v === "boolean") bytes += 5;
|
|
55
|
+
else if (typeof v === "number" && Number.isFinite(v)) bytes += 32;
|
|
56
|
+
else if (v && typeof v === "object") {
|
|
57
|
+
if (
|
|
58
|
+
seen.has(v) ||
|
|
59
|
+
![Object.prototype, Array.prototype, null].includes(
|
|
60
|
+
Object.getPrototypeOf(v),
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
throw new ContractError();
|
|
64
|
+
seen.add(v);
|
|
65
|
+
bytes += 2;
|
|
66
|
+
const names = Reflect.ownKeys(v);
|
|
67
|
+
if (names.length > LIMITS.dataFrames) throw new ContractError("oversize");
|
|
68
|
+
for (const key of names) {
|
|
69
|
+
if (Array.isArray(v) && key === "length") continue;
|
|
70
|
+
if (typeof key !== "string") throw new ContractError();
|
|
71
|
+
bytes += sourceBytes(key) * 6 + 4;
|
|
72
|
+
visit(own(v, key), depth + 1);
|
|
73
|
+
}
|
|
74
|
+
seen.delete(v);
|
|
75
|
+
} else throw new ContractError();
|
|
76
|
+
if (bytes > maximum) throw new ContractError("oversize");
|
|
77
|
+
}
|
|
78
|
+
visit(value, 0);
|
|
79
|
+
return bytes;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Streaming text sanitizer: literal prefixes, ANSI sequences and heuristic value
|
|
83
|
+
* states survive chunk boundaries. No truncation-to-success; bounded ambiguous
|
|
84
|
+
* URL authorities refuse. This is not universal business-secret detection. */
|
|
85
|
+
export class TextSanitizer {
|
|
86
|
+
constructor(secrets = []) {
|
|
87
|
+
if (
|
|
88
|
+
secrets.some((s) => typeof s !== "string") ||
|
|
89
|
+
secrets.reduce((n, s) => n + utf8Bytes(s), 0) >
|
|
90
|
+
LIMITS.workerRequestBytes * 2
|
|
91
|
+
)
|
|
92
|
+
throw new ContractError("oversize");
|
|
93
|
+
this.secrets = [];
|
|
94
|
+
this.addSecrets(secrets);
|
|
95
|
+
this.pending = "";
|
|
96
|
+
this.heuristic = "";
|
|
97
|
+
this.escape = "";
|
|
98
|
+
this.skipping = false;
|
|
99
|
+
this.redacted = false;
|
|
100
|
+
this.ended = false;
|
|
101
|
+
}
|
|
102
|
+
/** Owner-only updates. Preserve all stream state and prior literals; bound the
|
|
103
|
+
* union before retaining any new entries. Never evict a retired secret. */
|
|
104
|
+
addSecrets(secrets) {
|
|
105
|
+
if (this.ended || !Array.isArray(secrets)) throw new ContractError();
|
|
106
|
+
let incoming = 0;
|
|
107
|
+
for (const s of secrets) {
|
|
108
|
+
if (typeof s !== "string") throw new ContractError();
|
|
109
|
+
incoming += utf8Bytes(s);
|
|
110
|
+
if (incoming > LIMITS.workerRequestBytes * 2)
|
|
111
|
+
throw new ContractError("oversize");
|
|
112
|
+
}
|
|
113
|
+
let bytes = this.secrets.reduce((n, s) => n + utf8Bytes(s), 0);
|
|
114
|
+
const added = new Set();
|
|
115
|
+
for (const s of secrets) {
|
|
116
|
+
if (!s || this.secrets.includes(s) || added.has(s)) continue;
|
|
117
|
+
bytes += utf8Bytes(s);
|
|
118
|
+
if (bytes > LIMITS.workerRequestBytes * 2)
|
|
119
|
+
throw new ContractError("oversize");
|
|
120
|
+
added.add(s);
|
|
121
|
+
}
|
|
122
|
+
this.secrets = this.secrets
|
|
123
|
+
.concat([...added])
|
|
124
|
+
.sort((a, b) => b.length - a.length);
|
|
125
|
+
}
|
|
126
|
+
get retainedBytes() {
|
|
127
|
+
return utf8Bytes(this.pending + this.heuristic + this.escape);
|
|
128
|
+
}
|
|
129
|
+
write(input, final = false) {
|
|
130
|
+
if (this.ended || typeof input !== "string") throw new ContractError();
|
|
131
|
+
let plain = "";
|
|
132
|
+
for (const ch of input) {
|
|
133
|
+
if (this.escape) {
|
|
134
|
+
this.escape += ch;
|
|
135
|
+
if (this.escape.length > 8192) throw new ContractError("oversize");
|
|
136
|
+
if (
|
|
137
|
+
(this.escape.startsWith("\x1b[") &&
|
|
138
|
+
this.escape.length > 2 &&
|
|
139
|
+
/[@-~]/.test(ch)) ||
|
|
140
|
+
(this.escape.startsWith("\x1b]") &&
|
|
141
|
+
(ch === "\x07" || this.escape.endsWith("\x1b\\"))) ||
|
|
142
|
+
(this.escape.length === 2 && !"[]".includes(ch))
|
|
143
|
+
)
|
|
144
|
+
this.escape = "";
|
|
145
|
+
this.redacted = true;
|
|
146
|
+
} else if (ch === "\x1b") this.escape = ch;
|
|
147
|
+
else if (
|
|
148
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: strip terminal controls before literal matching
|
|
149
|
+
/[\x00-\x08\x0b-\x1f\x7f-\x9f\u202a-\u202e\u2066-\u2069]/.test(ch)
|
|
150
|
+
)
|
|
151
|
+
this.redacted = true;
|
|
152
|
+
else plain += ch;
|
|
153
|
+
}
|
|
154
|
+
this.pending += plain;
|
|
155
|
+
let literal = "";
|
|
156
|
+
let at = 0;
|
|
157
|
+
while (at < this.pending.length) {
|
|
158
|
+
const rest = this.pending.slice(at);
|
|
159
|
+
// A longer literal prefix takes precedence over a shorter full match.
|
|
160
|
+
if (
|
|
161
|
+
!final &&
|
|
162
|
+
this.secrets.some((s) => s.length > rest.length && s.startsWith(rest))
|
|
163
|
+
)
|
|
164
|
+
break;
|
|
165
|
+
const match = this.secrets.find((s) => rest.startsWith(s));
|
|
166
|
+
if (match) {
|
|
167
|
+
literal += marker;
|
|
168
|
+
at += match.length;
|
|
169
|
+
this.redacted = true;
|
|
170
|
+
} else {
|
|
171
|
+
const code = this.pending.charCodeAt(at);
|
|
172
|
+
if (
|
|
173
|
+
!final &&
|
|
174
|
+
at + 1 === this.pending.length &&
|
|
175
|
+
code >= 0xd800 &&
|
|
176
|
+
code <= 0xdbff
|
|
177
|
+
)
|
|
178
|
+
break;
|
|
179
|
+
const cp = this.pending.codePointAt(at);
|
|
180
|
+
if (cp >= 0xd800 && cp <= 0xdfff) throw new ContractError();
|
|
181
|
+
literal += String.fromCodePoint(cp);
|
|
182
|
+
at += cp > 0xffff ? 2 : 1;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
this.pending = this.pending.slice(at);
|
|
186
|
+
this.heuristic += literal;
|
|
187
|
+
let output = "";
|
|
188
|
+
while (this.heuristic) {
|
|
189
|
+
if (this.skipping) {
|
|
190
|
+
const end = this.heuristic.search(/[\s,;"'<>]/);
|
|
191
|
+
if (end < 0) {
|
|
192
|
+
this.heuristic = "";
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
this.heuristic = this.heuristic.slice(end);
|
|
196
|
+
this.skipping = false;
|
|
197
|
+
}
|
|
198
|
+
const match =
|
|
199
|
+
/\b(?:Bearer\s+|(?:api[-_]?key|access[-_]?token|refresh[-_]?token|password|passwd|secret|token|authorization)\s*[:=]\s*)/i.exec(
|
|
200
|
+
this.heuristic,
|
|
201
|
+
);
|
|
202
|
+
const url = /https?:\/\//i.exec(this.heuristic);
|
|
203
|
+
if (url && (!match || url.index < match.index)) {
|
|
204
|
+
output += this.heuristic.slice(0, url.index);
|
|
205
|
+
this.heuristic = this.heuristic.slice(url.index);
|
|
206
|
+
const start = url[0].length;
|
|
207
|
+
const end = this.heuristic.slice(start).search(/[/\s?#"'<>]/);
|
|
208
|
+
if (end < 0 && !final) {
|
|
209
|
+
if (utf8Bytes(this.heuristic) > 8192)
|
|
210
|
+
throw new ContractError("oversize");
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
const size = end < 0 ? this.heuristic.length : start + end;
|
|
214
|
+
const authority = this.heuristic.slice(start, size);
|
|
215
|
+
const pos = authority.lastIndexOf("@");
|
|
216
|
+
output +=
|
|
217
|
+
this.heuristic.slice(0, start) +
|
|
218
|
+
(pos < 0 ? authority : `${marker}@${authority.slice(pos + 1)}`);
|
|
219
|
+
this.redacted ||= pos >= 0;
|
|
220
|
+
this.heuristic = this.heuristic.slice(size);
|
|
221
|
+
} else if (match) {
|
|
222
|
+
output += this.heuristic.slice(0, match.index) + match[0] + marker;
|
|
223
|
+
this.heuristic = this.heuristic.slice(match.index + match[0].length);
|
|
224
|
+
this.skipping = true;
|
|
225
|
+
this.redacted = true;
|
|
226
|
+
} else {
|
|
227
|
+
// Finite heuristic names plus delimiters. Potentially unbounded whitespace
|
|
228
|
+
// after a candidate is held/refused, never released as an exposed value.
|
|
229
|
+
const candidate =
|
|
230
|
+
/(?:Bearer|api[-_]?key|access[-_]?token|refresh[-_]?token|password|passwd|secret|token|authorization)\s*$/i.exec(
|
|
231
|
+
this.heuristic,
|
|
232
|
+
);
|
|
233
|
+
const keep = final
|
|
234
|
+
? 0
|
|
235
|
+
: Math.max(
|
|
236
|
+
64,
|
|
237
|
+
candidate ? this.heuristic.length - candidate.index : 0,
|
|
238
|
+
);
|
|
239
|
+
if (keep > 8192) throw new ContractError("oversize");
|
|
240
|
+
let take = Math.max(0, this.heuristic.length - keep);
|
|
241
|
+
// The held heuristic suffix is measured in JS code units, but each
|
|
242
|
+
// emitted field/chunk must remain scalar UTF-8. Retain the whole pair
|
|
243
|
+
// when the suffix boundary would bisect a supplementary character.
|
|
244
|
+
const before = this.heuristic.charCodeAt(take - 1);
|
|
245
|
+
if (before >= 0xd800 && before <= 0xdbff) take--;
|
|
246
|
+
output += this.heuristic.slice(0, take);
|
|
247
|
+
this.heuristic = this.heuristic.slice(take);
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (final) {
|
|
252
|
+
this.ended = true;
|
|
253
|
+
this.escape = "";
|
|
254
|
+
}
|
|
255
|
+
return output;
|
|
256
|
+
}
|
|
257
|
+
end() {
|
|
258
|
+
return this.write("", true);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
export function sanitizeText(value, secrets = []) {
|
|
262
|
+
const sanitizer = new TextSanitizer(secrets);
|
|
263
|
+
return sanitizer.write(value, true);
|
|
264
|
+
}
|
|
265
|
+
/** Bounded own-data structural projection. Sensitive fields are never traversed.
|
|
266
|
+
* Only tool ordinary data may enter here; SDK/provider envelopes must be selected
|
|
267
|
+
* by the capture adapter instead of passed wholesale. */
|
|
268
|
+
export function sanitizeData(value, secrets = []) {
|
|
269
|
+
measureData(value);
|
|
270
|
+
function visit(v) {
|
|
271
|
+
if (typeof v === "string") return sanitizeText(v, secrets);
|
|
272
|
+
if (v === null || typeof v !== "object") return v;
|
|
273
|
+
if (Array.isArray(v)) return ownArray(v).map(visit);
|
|
274
|
+
const result = Object.create(null);
|
|
275
|
+
for (const key of Object.keys(v).sort()) {
|
|
276
|
+
const safeKey = sanitizeText(key, secrets);
|
|
277
|
+
if (Object.hasOwn(result, safeKey)) throw new ContractError();
|
|
278
|
+
result[safeKey] = sensitive.test(key) ? marker : visit(own(v, key));
|
|
279
|
+
}
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
282
|
+
return canonicalJson(visit(value), LIMITS.workerQueueBytes);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Streaming JSON projection. Escapes are decoded before literal matching,
|
|
286
|
+
* sensitive values (including subtrees) are replaced without buffering them.
|
|
287
|
+
* Partial mode is only for genuinely unfinished SDK argument declarations. */
|
|
288
|
+
export class DetailSanitizer {
|
|
289
|
+
constructor(encoding, secrets = [], partial = false) {
|
|
290
|
+
this.encoding = encoding;
|
|
291
|
+
this.secrets = secrets;
|
|
292
|
+
this.partial = partial;
|
|
293
|
+
this.limited = false;
|
|
294
|
+
this.text = new TextSanitizer(secrets);
|
|
295
|
+
this.stack = [];
|
|
296
|
+
this.root = "value";
|
|
297
|
+
this.inString = false;
|
|
298
|
+
this.escape = "";
|
|
299
|
+
this.key = "";
|
|
300
|
+
this.keyMode = false;
|
|
301
|
+
this.suppressed = false;
|
|
302
|
+
this.token = "";
|
|
303
|
+
this.redacted = false;
|
|
304
|
+
}
|
|
305
|
+
get retainedBytes() {
|
|
306
|
+
return (
|
|
307
|
+
this.text.retainedBytes +
|
|
308
|
+
utf8Bytes(this.escape + this.key + this.token) +
|
|
309
|
+
this.stack.reduce(
|
|
310
|
+
(sum, entry) =>
|
|
311
|
+
sum +
|
|
312
|
+
utf8Bytes(entry.lastKey ?? "") +
|
|
313
|
+
utf8Bytes(entry.lastOutputKey ?? ""),
|
|
314
|
+
0,
|
|
315
|
+
)
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
write(input, final = false) {
|
|
319
|
+
if (this.limited) return "";
|
|
320
|
+
try {
|
|
321
|
+
return this.project(input, final);
|
|
322
|
+
} catch (error) {
|
|
323
|
+
if (
|
|
324
|
+
!this.partial ||
|
|
325
|
+
(error instanceof ContractError && error.code === "oversize")
|
|
326
|
+
)
|
|
327
|
+
throw error;
|
|
328
|
+
// Malformed partial argument syntax is an observation, not executable
|
|
329
|
+
// canonical JSON. Preserve the safe prefix and explicitly omit the
|
|
330
|
+
// undecodable suffix; never guess a key/value boundary or leak escapes.
|
|
331
|
+
this.limited = true;
|
|
332
|
+
this.redacted = true;
|
|
333
|
+
return "[redacted: unsupported partial arguments]";
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
project(input, final = false) {
|
|
337
|
+
if (this.encoding === "utf8") {
|
|
338
|
+
const output = this.text.write(input, final);
|
|
339
|
+
this.redacted = this.text.redacted;
|
|
340
|
+
return output;
|
|
341
|
+
}
|
|
342
|
+
let output = "";
|
|
343
|
+
const encoded = (s) => JSON.stringify(s).slice(1, -1);
|
|
344
|
+
const top = () => this.stack.at(-1);
|
|
345
|
+
const done = () => {
|
|
346
|
+
if (top()) {
|
|
347
|
+
top().expect = "comma";
|
|
348
|
+
top().sensitive = false;
|
|
349
|
+
top().count++;
|
|
350
|
+
} else this.root = "done";
|
|
351
|
+
};
|
|
352
|
+
const begin = () => {
|
|
353
|
+
const parent = top();
|
|
354
|
+
if ((parent?.expect ?? this.root) !== "value") throw new ContractError();
|
|
355
|
+
const hide = !!parent?.hide || !!parent?.sensitive;
|
|
356
|
+
if (parent?.sensitive && !parent.hide) {
|
|
357
|
+
output += JSON.stringify(marker);
|
|
358
|
+
this.redacted = true;
|
|
359
|
+
}
|
|
360
|
+
return hide;
|
|
361
|
+
};
|
|
362
|
+
const primitive = () => {
|
|
363
|
+
if (
|
|
364
|
+
!/^(?:true|false|null|-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?)$/.test(
|
|
365
|
+
this.token,
|
|
366
|
+
)
|
|
367
|
+
) {
|
|
368
|
+
if (!this.partial) throw new ContractError();
|
|
369
|
+
// An invalid unquoted token may contain escaped secrets outside JSON
|
|
370
|
+
// strings. Do not pretend it is ordinary final data or decode guesses.
|
|
371
|
+
if (!this.suppressed) output += marker;
|
|
372
|
+
this.redacted = true;
|
|
373
|
+
this.limited = true;
|
|
374
|
+
this.token = "";
|
|
375
|
+
done();
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const value = JSON.parse(this.token);
|
|
379
|
+
if (typeof value === "number" && !Number.isFinite(value))
|
|
380
|
+
throw new ContractError();
|
|
381
|
+
if (!this.suppressed) output += this.token;
|
|
382
|
+
this.token = "";
|
|
383
|
+
done();
|
|
384
|
+
};
|
|
385
|
+
for (const ch of input) {
|
|
386
|
+
if (this.inString) {
|
|
387
|
+
let decoded = ch;
|
|
388
|
+
if (this.escape) {
|
|
389
|
+
this.escape += ch;
|
|
390
|
+
if (
|
|
391
|
+
this.escape === "\\u" ||
|
|
392
|
+
(this.escape.startsWith("\\u") && this.escape.length < 6)
|
|
393
|
+
)
|
|
394
|
+
continue;
|
|
395
|
+
decoded = JSON.parse(`"${this.escape}"`);
|
|
396
|
+
this.escape = "";
|
|
397
|
+
} else if (ch === "\\") {
|
|
398
|
+
this.escape = ch;
|
|
399
|
+
continue;
|
|
400
|
+
} else if (ch === '"') {
|
|
401
|
+
if (this.keyMode) {
|
|
402
|
+
const parent = top();
|
|
403
|
+
if (
|
|
404
|
+
!this.partial &&
|
|
405
|
+
parent.lastKey !== undefined &&
|
|
406
|
+
this.key <= parent.lastKey
|
|
407
|
+
)
|
|
408
|
+
throw new ContractError();
|
|
409
|
+
parent.lastKey = this.key;
|
|
410
|
+
parent.sensitive = sensitive.test(this.key);
|
|
411
|
+
parent.expect = "colon";
|
|
412
|
+
if (!parent.hide) {
|
|
413
|
+
const safeKey = sanitizeText(this.key, this.secrets);
|
|
414
|
+
if (
|
|
415
|
+
!this.partial &&
|
|
416
|
+
parent.lastOutputKey !== undefined &&
|
|
417
|
+
safeKey <= parent.lastOutputKey
|
|
418
|
+
)
|
|
419
|
+
throw new ContractError();
|
|
420
|
+
parent.lastOutputKey = safeKey;
|
|
421
|
+
this.redacted ||= safeKey !== this.key;
|
|
422
|
+
output += JSON.stringify(safeKey);
|
|
423
|
+
}
|
|
424
|
+
this.key = "";
|
|
425
|
+
} else {
|
|
426
|
+
if (!this.suppressed) output += `${encoded(this.text.end())}"`;
|
|
427
|
+
this.redacted ||= this.text.redacted;
|
|
428
|
+
done();
|
|
429
|
+
}
|
|
430
|
+
this.inString = false;
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (this.keyMode) {
|
|
434
|
+
this.key += decoded;
|
|
435
|
+
if (utf8Bytes(this.key) > 8192) throw new ContractError("oversize");
|
|
436
|
+
} else if (!this.suppressed)
|
|
437
|
+
output += encoded(this.text.write(decoded));
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
if (this.token && /[\s,\]}]/.test(ch)) primitive();
|
|
441
|
+
if (this.token) {
|
|
442
|
+
this.token += ch;
|
|
443
|
+
if (this.token.length > 128) throw new ContractError("oversize");
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
if (/\s/.test(ch)) continue;
|
|
447
|
+
const parent = top();
|
|
448
|
+
if (ch === '"') {
|
|
449
|
+
this.keyMode = parent?.kind === "object" && parent.expect === "key";
|
|
450
|
+
this.suppressed = this.keyMode ? parent.hide : begin();
|
|
451
|
+
this.inString = true;
|
|
452
|
+
this.text = new TextSanitizer(this.secrets);
|
|
453
|
+
if (!this.keyMode && !this.suppressed) output += '"';
|
|
454
|
+
} else if (ch === "{" || ch === "[") {
|
|
455
|
+
const hide = begin();
|
|
456
|
+
if (!hide) output += ch;
|
|
457
|
+
if (this.stack.length >= 64) throw new ContractError("oversize");
|
|
458
|
+
this.stack.push({
|
|
459
|
+
kind: ch === "{" ? "object" : "array",
|
|
460
|
+
expect: ch === "{" ? "key" : "value",
|
|
461
|
+
hide,
|
|
462
|
+
sensitive: false,
|
|
463
|
+
count: 0,
|
|
464
|
+
lastKey: undefined,
|
|
465
|
+
lastOutputKey: undefined,
|
|
466
|
+
});
|
|
467
|
+
} else if (ch === "}" || ch === "]") {
|
|
468
|
+
if (!parent || (ch === "}") !== (parent.kind === "object"))
|
|
469
|
+
throw new ContractError();
|
|
470
|
+
if (
|
|
471
|
+
parent.expect !== "comma" &&
|
|
472
|
+
!(
|
|
473
|
+
parent.count === 0 &&
|
|
474
|
+
parent.expect === (parent.kind === "object" ? "key" : "value")
|
|
475
|
+
)
|
|
476
|
+
)
|
|
477
|
+
throw new ContractError();
|
|
478
|
+
if (!parent.hide) output += ch;
|
|
479
|
+
this.stack.pop();
|
|
480
|
+
done();
|
|
481
|
+
} else if (ch === ":") {
|
|
482
|
+
if (parent?.expect !== "colon") throw new ContractError();
|
|
483
|
+
parent.expect = "value";
|
|
484
|
+
if (!parent.hide) output += ch;
|
|
485
|
+
} else if (ch === ",") {
|
|
486
|
+
if (parent?.expect !== "comma") throw new ContractError();
|
|
487
|
+
parent.expect = parent.kind === "object" ? "key" : "value";
|
|
488
|
+
parent.count = Math.max(1, parent.count);
|
|
489
|
+
if (!parent.hide) output += ch;
|
|
490
|
+
} else {
|
|
491
|
+
this.suppressed = begin();
|
|
492
|
+
this.token = ch;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (final) {
|
|
496
|
+
if (this.token) primitive();
|
|
497
|
+
if (this.partial) {
|
|
498
|
+
// This field is UTF-8 partial source, not canonical final arguments.
|
|
499
|
+
// Retain unfinished strings/keys without manufacturing closing syntax.
|
|
500
|
+
if (this.inString && !this.suppressed) {
|
|
501
|
+
output += this.keyMode
|
|
502
|
+
? JSON.stringify(sanitizeText(this.key, this.secrets)).slice(0, -1)
|
|
503
|
+
: encoded(this.text.end());
|
|
504
|
+
}
|
|
505
|
+
if (this.escape) {
|
|
506
|
+
output += marker;
|
|
507
|
+
this.redacted = true;
|
|
508
|
+
this.limited = true;
|
|
509
|
+
}
|
|
510
|
+
} else if (
|
|
511
|
+
this.inString ||
|
|
512
|
+
this.escape ||
|
|
513
|
+
this.stack.length ||
|
|
514
|
+
this.root !== "done"
|
|
515
|
+
)
|
|
516
|
+
throw new ContractError();
|
|
517
|
+
this.redacted ||= this.text.redacted;
|
|
518
|
+
}
|
|
519
|
+
return output;
|
|
520
|
+
}
|
|
521
|
+
end() {
|
|
522
|
+
return this.write("", true);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** Canonical ordinary JSON without materializing a whole large tool body.
|
|
527
|
+
* The caller holds the TextSanitizer prefix/expansion allowance. Traversal tables,
|
|
528
|
+
* sanitized keys and source/output slices are charged BEFORE copying, and held
|
|
529
|
+
* across each yield (including the consumer's original awaited writes). Iterator
|
|
530
|
+
* close releases only these no-longer-used copies, never an emitter write lease. */
|
|
531
|
+
export function* sanitizedJsonChunks(value, secrets, reserve) {
|
|
532
|
+
measureData(value, LIMITS.detailBytes * 8);
|
|
533
|
+
function* visit(v) {
|
|
534
|
+
if (typeof v === "string") {
|
|
535
|
+
yield '"';
|
|
536
|
+
const sanitizer = new TextSanitizer(secrets);
|
|
537
|
+
for (let at = 0; at < v.length; at += 1024) {
|
|
538
|
+
// At most three UTF-8 bytes per UTF-16 unit, with conservative literal
|
|
539
|
+
// replacement/JSON escaping expansion. Reserve before slice/projection.
|
|
540
|
+
const slice = reserve(Math.min(1024, v.length - at) * 36 + 64);
|
|
541
|
+
try {
|
|
542
|
+
const clean = sanitizer.write(v.slice(at, at + 1024));
|
|
543
|
+
yield JSON.stringify(clean).slice(1, -1);
|
|
544
|
+
} finally {
|
|
545
|
+
slice.release();
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
// Remaining known-literal/heuristic prefixes and their expansion remain
|
|
549
|
+
// covered by the caller's prefix lease until this final yield settles.
|
|
550
|
+
yield JSON.stringify(sanitizer.end()).slice(1, -1);
|
|
551
|
+
yield '"';
|
|
552
|
+
} else if (v === null || typeof v !== "object") yield JSON.stringify(v);
|
|
553
|
+
else if (Array.isArray(v)) {
|
|
554
|
+
yield "[";
|
|
555
|
+
const length = own(v, "length");
|
|
556
|
+
for (let index = 0; index < length; index++) {
|
|
557
|
+
if (index) yield ",";
|
|
558
|
+
yield* visit(own(v, String(index)));
|
|
559
|
+
}
|
|
560
|
+
yield "]";
|
|
561
|
+
} else {
|
|
562
|
+
const table = reserve(64);
|
|
563
|
+
let previous;
|
|
564
|
+
try {
|
|
565
|
+
// No Object.keys/sort copy before reservation. These are validated own
|
|
566
|
+
// data objects; key references and their UTF-16 storage are conservative
|
|
567
|
+
// traversal charges, not a claim about all SDK/V8 memory.
|
|
568
|
+
const keys = [];
|
|
569
|
+
for (const key in v) {
|
|
570
|
+
if (!Object.hasOwn(v, key)) continue;
|
|
571
|
+
const bytes = sourceBytes(key);
|
|
572
|
+
// Same completed-key bound as the owner/partial lexical projection.
|
|
573
|
+
if (bytes > 8192) throw new ContractError("oversize");
|
|
574
|
+
table.grow(bytes * 2 + 16);
|
|
575
|
+
keys.push(key);
|
|
576
|
+
}
|
|
577
|
+
keys.sort();
|
|
578
|
+
yield "{";
|
|
579
|
+
let lastKey;
|
|
580
|
+
for (let index = 0; index < keys.length; index++) {
|
|
581
|
+
const key = keys[index];
|
|
582
|
+
// Whole-key sanitization, escaped output and retained comparison key.
|
|
583
|
+
// Keep the old comparison lease until the replacement is allocated.
|
|
584
|
+
const current = reserve(sourceBytes(key) * 24 + 64);
|
|
585
|
+
try {
|
|
586
|
+
const safe = sanitizeText(key, secrets);
|
|
587
|
+
if (lastKey !== undefined && safe <= lastKey)
|
|
588
|
+
throw new ContractError();
|
|
589
|
+
lastKey = safe;
|
|
590
|
+
previous?.release();
|
|
591
|
+
previous = current;
|
|
592
|
+
if (index) yield ",";
|
|
593
|
+
yield `${JSON.stringify(safe)}:`;
|
|
594
|
+
if (sensitive.test(key)) yield JSON.stringify(marker);
|
|
595
|
+
else yield* visit(own(v, key));
|
|
596
|
+
} finally {
|
|
597
|
+
if (previous !== current) current.release();
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
yield "}";
|
|
601
|
+
} finally {
|
|
602
|
+
previous?.release();
|
|
603
|
+
table.release();
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
yield* visit(value);
|
|
608
|
+
}
|