@aiwg/cli 2026.8.27 → 2026.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,191 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ export const BUILTIN_CLASSIFICATIONS = {
3
+ public: { rank: 0, description: 'Approved for unrestricted disclosure.' },
4
+ internal: { rank: 10, description: 'Routine non-public operational information.' },
5
+ confidential: { rank: 20, description: 'Sensitive business or operational information.' },
6
+ 'restricted-infrastructure': { rank: 30, description: 'Topology, access path, recovery, or detailed infrastructure information.' },
7
+ 'restricted-identity': { rank: 40, description: 'Named-user, identity-provider, authentication, or entitlement information.' },
8
+ };
9
+ export const SECURE_KIND_DEFAULTS = {
10
+ ITAsset: 'restricted-infrastructure',
11
+ ITService: 'restricted-infrastructure',
12
+ ITNetworkState: 'restricted-infrastructure',
13
+ OpsInventory: 'restricted-infrastructure',
14
+ OpsPlaybook: 'internal',
15
+ IncidentReport: 'confidential',
16
+ IdentityAudit: 'restricted-identity',
17
+ DREvidence: 'restricted-infrastructure',
18
+ RawAuditEvidence: 'restricted-infrastructure',
19
+ };
20
+ export const SECURE_CATEGORY_DEFAULTS = {
21
+ 'raw-audit': 'restricted-infrastructure',
22
+ 'identity-audit': 'restricted-identity',
23
+ 'network-inventory': 'restricted-infrastructure',
24
+ 'dr-evidence': 'restricted-infrastructure',
25
+ 'sanitized-summary': 'internal',
26
+ generic: 'internal',
27
+ };
28
+ function stableId(value, label) {
29
+ if (!/^[a-z0-9][a-z0-9.-]{0,127}$/.test(value)) {
30
+ throw new Error(`${label} must be a lowercase stable identifier`);
31
+ }
32
+ }
33
+ export function resolveClassificationDefinitions(policy = {}) {
34
+ const classes = { ...BUILTIN_CLASSIFICATIONS };
35
+ for (const [id, definition] of Object.entries(policy.classes ?? {})) {
36
+ stableId(id, 'classification');
37
+ if (!Number.isSafeInteger(definition.rank) || definition.rank < 0 || definition.rank > 10_000) {
38
+ throw new Error(`classification '${id}' rank must be an integer from 0 through 10000`);
39
+ }
40
+ const builtin = BUILTIN_CLASSIFICATIONS[id];
41
+ if (builtin && builtin.rank !== definition.rank) {
42
+ throw new Error(`built-in classification '${id}' rank cannot be changed`);
43
+ }
44
+ classes[id] = { ...definition, ...(builtin ? { rank: builtin.rank } : {}) };
45
+ }
46
+ return classes;
47
+ }
48
+ function mergeHandling(parent, child) {
49
+ return {
50
+ ...(parent ?? {}),
51
+ ...(child ?? {}),
52
+ ...(child?.allowedSinks ? { allowedSinks: [...child.allowedSinks] } : parent?.allowedSinks ? { allowedSinks: [...parent.allowedSinks] } : {}),
53
+ };
54
+ }
55
+ /** Resolve explicit metadata, parent inheritance, secure kind/category defaults, then the policy default. */
56
+ export function resolveArtifactGovernance(input) {
57
+ const policy = input.policy ?? {};
58
+ const definitions = resolveClassificationDefinitions(policy);
59
+ const explicitRequired = new Set(policy.requireExplicitForKinds ?? []);
60
+ if (explicitRequired.has(input.kind) && input.metadata?.classification === undefined) {
61
+ throw new Error(`artifact kind '${input.kind}' requires an explicit classification`);
62
+ }
63
+ const candidates = [
64
+ [input.metadata?.classification, 'artifact'],
65
+ [input.parent?.classification, 'parent'],
66
+ [policy.defaultsByKind?.[input.kind] ?? SECURE_KIND_DEFAULTS[input.kind], 'kind-default'],
67
+ [policy.defaultsByCategory?.[input.category] ?? SECURE_CATEGORY_DEFAULTS[input.category], 'category-default'],
68
+ [policy.defaultClassification ?? 'internal', 'policy-default'],
69
+ ];
70
+ const selected = candidates.find(([value]) => value !== undefined);
71
+ const classification = selected?.[0];
72
+ if (!classification || !definitions[classification]) {
73
+ throw new Error(`unknown or missing classification '${classification ?? '<missing>'}'`);
74
+ }
75
+ const handling = mergeHandling(input.parent?.handling, input.metadata?.handling);
76
+ for (const sink of handling.allowedSinks ?? [])
77
+ stableId(sink, 'allowed sink');
78
+ return {
79
+ classification,
80
+ classificationRank: definitions[classification].rank,
81
+ classificationSource: selected[1],
82
+ ...(input.metadata?.owner ?? input.parent?.owner ? { owner: input.metadata?.owner ?? input.parent?.owner } : {}),
83
+ handling,
84
+ };
85
+ }
86
+ function defaultSinkMaxRank(sink) {
87
+ switch (sink.visibility) {
88
+ case 'public': return BUILTIN_CLASSIFICATIONS.public.rank;
89
+ case 'private': return BUILTIN_CLASSIFICATIONS['restricted-infrastructure'].rank;
90
+ case 'restricted': return BUILTIN_CLASSIFICATIONS['restricted-identity'].rank;
91
+ case 'unknown': return null;
92
+ }
93
+ }
94
+ function approvalIsValid(approval, artifactId, sinkId, now) {
95
+ if (!approval)
96
+ return false;
97
+ if (!approval.id || !approval.actor || !approval.reason.trim())
98
+ return false;
99
+ if (approval.artifactId !== artifactId || approval.sinkId !== sinkId)
100
+ return false;
101
+ const approvedAt = Date.parse(approval.approvedAt);
102
+ if (!Number.isFinite(approvedAt) || approvedAt > now)
103
+ return false;
104
+ if (approval.expiresAt !== undefined) {
105
+ const expiresAt = Date.parse(approval.expiresAt);
106
+ if (!Number.isFinite(expiresAt) || expiresAt <= now || expiresAt <= approvedAt)
107
+ return false;
108
+ }
109
+ return true;
110
+ }
111
+ function reasonDigest(reason) {
112
+ return `sha256:${createHash('sha256').update(reason).digest('hex')}`;
113
+ }
114
+ function correlationDigest(value) {
115
+ return `sha256:${createHash('sha256').update(value).digest('hex')}`;
116
+ }
117
+ function safeArtifactKind(value) {
118
+ return /^[A-Za-z][A-Za-z0-9.-]{0,127}$/.test(value) ? value : correlationDigest(value);
119
+ }
120
+ /** Evaluate an artifact against a known sink before disclosure. Audit data never contains payload content. */
121
+ export function evaluatePublicationGate(input) {
122
+ const occurredAt = (input.now ?? new Date()).toISOString();
123
+ const now = Date.parse(occurredAt);
124
+ const sinkId = input.sink?.id ?? 'unknown';
125
+ const reasons = [];
126
+ const sink = input.sink;
127
+ if (!sink || sink.visibility === 'unknown') {
128
+ reasons.push('unknown-sink-visibility');
129
+ }
130
+ else {
131
+ stableId(sink.id, 'sink');
132
+ if (input.governance.handling.allowedSinks && !input.governance.handling.allowedSinks.includes(sink.id)) {
133
+ reasons.push('sink-not-allowed');
134
+ }
135
+ const definitions = input.classes ?? resolveClassificationDefinitions();
136
+ const configuredMax = sink.maxClassification;
137
+ const maxRank = configuredMax === undefined
138
+ ? defaultSinkMaxRank(sink)
139
+ : definitions[configuredMax]?.rank;
140
+ if (maxRank === undefined || maxRank === null)
141
+ reasons.push('unknown-sink-classification-limit');
142
+ else if (input.governance.classificationRank > maxRank)
143
+ reasons.push('classification-exceeds-sink');
144
+ const crossRepo = Boolean(input.sourceRepository && sink.repository && input.sourceRepository !== sink.repository);
145
+ if (crossRepo && input.governance.handling.crossRepo === 'deny')
146
+ reasons.push('cross-repo-denied');
147
+ if (crossRepo && input.governance.handling.crossRepo === 'approval-required')
148
+ reasons.push('cross-repo-approval-required');
149
+ }
150
+ const validApproval = approvalIsValid(input.approval, input.artifactId, sinkId, now);
151
+ const overridableReasons = new Set(['classification-exceeds-sink', 'cross-repo-approval-required']);
152
+ const approvalCoversReasons = reasons.length > 0 && reasons.every((reason) => overridableReasons.has(reason));
153
+ let decision;
154
+ if (reasons.length === 0)
155
+ decision = 'allow';
156
+ else if (validApproval && approvalCoversReasons && sink && sink.visibility !== 'unknown')
157
+ decision = 'override';
158
+ else if (sink?.acceptsSanitizedSummary)
159
+ decision = 'summarize';
160
+ else
161
+ decision = 'deny';
162
+ const audit = {
163
+ schemaVersion: 'ops-publication-decision.aiwg.io/v1',
164
+ eventId: randomUUID(),
165
+ occurredAt,
166
+ artifactId: correlationDigest(input.artifactId),
167
+ artifactKind: safeArtifactKind(input.artifactKind),
168
+ classification: input.governance.classification,
169
+ sinkId,
170
+ decision,
171
+ reasonCodes: [...reasons].sort(),
172
+ ...(decision === 'override' && input.approval ? {
173
+ approvalId: input.approval.id,
174
+ approvalActor: input.approval.actor,
175
+ approvalReasonDigest: reasonDigest(input.approval.reason),
176
+ } : {}),
177
+ };
178
+ return { decision, allowed: decision === 'allow' || decision === 'override', reasonCodes: audit.reasonCodes, audit };
179
+ }
180
+ /** Produce a payload-free summary suitable for a second, separately gated publication attempt. */
181
+ export function createSanitizedSummary(input) {
182
+ return {
183
+ schemaVersion: 'ops-sanitized-summary.aiwg.io/v1',
184
+ artifactFingerprint: `sha256:${createHash('sha256').update(input.artifactId).digest('hex')}`,
185
+ artifactKind: input.artifactKind,
186
+ ...(input.status ? { status: input.status } : {}),
187
+ omittedFields: input.omittedFields,
188
+ redactionClasses: [...new Set(input.redactionClasses)].sort(),
189
+ };
190
+ }
191
+ //# sourceMappingURL=classification.js.map
@@ -0,0 +1,5 @@
1
+ export * from './redaction.js';
2
+ export * from './classification.js';
3
+ export * from './retention.js';
4
+ export * from './boundary.js';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,324 @@
1
+ import { createHmac } from 'node:crypto';
2
+ import { Transform } from 'node:stream';
3
+ import { TextDecoder } from 'node:util';
4
+ export class RedactionError extends Error {
5
+ code;
6
+ constructor(code, message) {
7
+ super(message);
8
+ this.code = code;
9
+ this.name = 'RedactionError';
10
+ }
11
+ }
12
+ const DEFAULT_LIMITS = {
13
+ maxInputBytes: 8 * 1024 * 1024,
14
+ maxDepth: 32,
15
+ maxNodes: 100_000,
16
+ maxObjectKeys: 10_000,
17
+ maxArrayItems: 100_000,
18
+ };
19
+ const BUILTIN_SENSITIVE_KEY = /(?:^|[_-])(?:authorization|auth[_-]?header|cookie|set[_-]?cookie|credential|password|passwd|secret|token|api[_-]?key|private[_-]?key|connection[_-]?string|client[_-]?secret)(?:$|[_-])/i;
20
+ const BUILTIN_PATTERNS = [
21
+ {
22
+ id: 'private-key',
23
+ pattern: /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----/g,
24
+ },
25
+ {
26
+ id: 'authorization-header',
27
+ pattern: /(\bAuthorization\s*:\s*)([^\r\n]+)/gi,
28
+ preservePrefix: true,
29
+ },
30
+ {
31
+ id: 'cookie-header',
32
+ pattern: /(\b(?:Cookie|Set-Cookie)\s*:\s*)([^\r\n]+)/gi,
33
+ preservePrefix: true,
34
+ },
35
+ {
36
+ id: 'url-query-secret',
37
+ pattern: /([?&](?:access[_-]?token|refresh[_-]?token|api[_-]?key|token|secret|password|passwd|signature|sig)=)([^&#\s]+)/gi,
38
+ preservePrefix: true,
39
+ },
40
+ {
41
+ id: 'sensitive-assignment',
42
+ pattern: /((?:^|[\s,{])["']?(?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|password|passwd|secret|client[_-]?secret|credential)["']?\s*(?:[:=]|\s+)\s*")([^"\r\n]*)(")/gim,
43
+ preservePrefix: true,
44
+ preserveSuffix: true,
45
+ },
46
+ {
47
+ id: 'sensitive-assignment',
48
+ pattern: /((?:^|[\s,{])["']?(?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|password|passwd|secret|client[_-]?secret|credential)["']?\s*(?:[:=]|\s+)\s*')([^'\r\n]*)(')/gim,
49
+ preservePrefix: true,
50
+ preserveSuffix: true,
51
+ },
52
+ {
53
+ id: 'sensitive-assignment',
54
+ pattern: /((?:^|[\s,{])["']?(?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|password|passwd|secret|client[_-]?secret|credential)["']?\s*(?:[:=]|\s+)\s*)([^\s"',;}\]]{4,})/gim,
55
+ preservePrefix: true,
56
+ },
57
+ {
58
+ id: 'environment-secret',
59
+ pattern: /(\b[A-Z][A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|PASSWD|API_KEY|PRIVATE_KEY)\s*=\s*)(?:"[^"]+"|'[^']+'|[^\s]+)/g,
60
+ preservePrefix: true,
61
+ },
62
+ {
63
+ id: 'provider-token',
64
+ pattern: /\b(?:sk-(?:proj-)?|gh[pousr]_|github_pat_|xox[baprs]-|AKIA)[A-Za-z0-9._~+/=-]{6,}\b/g,
65
+ },
66
+ {
67
+ id: 'bearer-token',
68
+ pattern: /(\bBearer\s+)([A-Za-z0-9._~+/=-]{6,})\b/gi,
69
+ preservePrefix: true,
70
+ },
71
+ {
72
+ id: 'connection-string',
73
+ pattern: /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqps?):\/\/[^\s"'<>]+/gi,
74
+ },
75
+ {
76
+ id: 'url-userinfo',
77
+ pattern: /(https?:\/\/[^/\s:@]+:)([^/\s@]+)(@[^\s"'<>]+)/gi,
78
+ },
79
+ ];
80
+ function resolvedLimits(options) {
81
+ const limits = { ...DEFAULT_LIMITS, ...(options.limits ?? {}) };
82
+ for (const [name, value] of Object.entries(limits)) {
83
+ if (!Number.isSafeInteger(value) || value < 1) {
84
+ throw new RedactionError('LIMIT_EXCEEDED', `${name} must be a positive safe integer`);
85
+ }
86
+ }
87
+ return limits;
88
+ }
89
+ function validatePatternSource(pattern) {
90
+ if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(pattern.id)) {
91
+ throw new RedactionError('INVALID_PATTERN', 'organization redaction pattern IDs must be lowercase stable identifiers');
92
+ }
93
+ if (pattern.pattern.length === 0 || pattern.pattern.length > 512) {
94
+ throw new RedactionError('INVALID_PATTERN', `organization pattern '${pattern.id}' must contain 1-512 characters`);
95
+ }
96
+ if (/\\[1-9]|\(\?<[=!]|\([^)]*[+*][^)]*\)[+*{]/.test(pattern.pattern)) {
97
+ throw new RedactionError('INVALID_PATTERN', `organization pattern '${pattern.id}' uses a disallowed high-risk construct`);
98
+ }
99
+ const requestedFlags = pattern.flags ?? 'g';
100
+ if (/[^gimsuy]/.test(requestedFlags)) {
101
+ throw new RedactionError('INVALID_PATTERN', `organization pattern '${pattern.id}' has unsupported flags`);
102
+ }
103
+ const flags = requestedFlags.includes('g') ? requestedFlags : `${requestedFlags}g`;
104
+ try {
105
+ return new RegExp(pattern.pattern, flags);
106
+ }
107
+ catch {
108
+ throw new RedactionError('INVALID_PATTERN', `organization pattern '${pattern.id}' is not a valid regular expression`);
109
+ }
110
+ }
111
+ function fingerprint(value, key) {
112
+ if (key === undefined)
113
+ return undefined;
114
+ return `hmac-sha256:${createHmac('sha256', key).update(value).digest('hex').slice(0, 16)}`;
115
+ }
116
+ function marker(classification, value, options) {
117
+ const length = Buffer.byteLength(value);
118
+ const valueFingerprint = fingerprint(value, options.fingerprintKey);
119
+ const attributes = [
120
+ options.includeLength === false ? null : `len=${length}`,
121
+ valueFingerprint ? `fp=${valueFingerprint}` : null,
122
+ ].filter((part) => part !== null);
123
+ return {
124
+ marker: `[REDACTED:${classification}${attributes.length ? `;${attributes.join(';')}` : ''}]`,
125
+ finding: {
126
+ class: classification,
127
+ ...(options.includeLength === false ? {} : { length }),
128
+ ...(valueFingerprint ? { fingerprint: valueFingerprint } : {}),
129
+ },
130
+ };
131
+ }
132
+ function applyPattern(input, definition, options, findings) {
133
+ definition.pattern.lastIndex = 0;
134
+ return input.replace(definition.pattern, (...args) => {
135
+ const full = String(args[0]);
136
+ if (definition.id === 'url-userinfo') {
137
+ const prefix = String(args[1]);
138
+ const secret = String(args[2]);
139
+ const suffix = String(args[3]);
140
+ const redacted = marker(definition.id, secret, options);
141
+ findings.push(redacted.finding);
142
+ return `${prefix}${redacted.marker}${suffix}`;
143
+ }
144
+ if (definition.preservePrefix) {
145
+ const prefix = String(args[1]);
146
+ const secret = String(args[2] ?? full.slice(prefix.length));
147
+ if (secret.startsWith('[REDACTED:'))
148
+ return full;
149
+ const redacted = marker(definition.id, secret, options);
150
+ findings.push(redacted.finding);
151
+ const suffix = definition.preserveSuffix ? String(args[3] ?? '') : '';
152
+ return `${prefix}${redacted.marker}${suffix}`;
153
+ }
154
+ const redacted = marker(definition.id, full, options);
155
+ findings.push(redacted.finding);
156
+ return redacted.marker;
157
+ });
158
+ }
159
+ function decodedSecretClass(value) {
160
+ if (value.length < 24 || value.length > 16 * 1024 || value.length % 4 === 1)
161
+ return null;
162
+ try {
163
+ const decoded = Buffer.from(value, 'base64').toString('utf8');
164
+ if (!decoded || decoded.includes('\uFFFD'))
165
+ return null;
166
+ if (/-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----/.test(decoded))
167
+ return 'encoded-private-key';
168
+ if (/(?:api[_-]?key|token|password|passwd|secret|authorization)\s*[:=]/i.test(decoded))
169
+ return 'encoded-secret';
170
+ if (/(?:sk-(?:proj-)?|gh[pousr]_|github_pat_|xox[baprs]-|AKIA)[A-Za-z0-9._~+/=-]{6,}/.test(decoded))
171
+ return 'encoded-secret';
172
+ return null;
173
+ }
174
+ catch {
175
+ return null;
176
+ }
177
+ }
178
+ /** Redact secret-bearing values from a complete text value. */
179
+ export function redactText(input, options = {}) {
180
+ const limits = resolvedLimits(options);
181
+ if (Buffer.byteLength(input) > limits.maxInputBytes) {
182
+ throw new RedactionError('LIMIT_EXCEEDED', 'text exceeds the configured redaction byte limit');
183
+ }
184
+ const findings = [];
185
+ let text = input;
186
+ for (const definition of BUILTIN_PATTERNS) {
187
+ text = applyPattern(text, definition, options, findings);
188
+ }
189
+ for (const configured of options.organizationPatterns ?? []) {
190
+ const definition = {
191
+ id: `organization-${configured.id}`,
192
+ pattern: validatePatternSource(configured),
193
+ };
194
+ text = applyPattern(text, definition, options, findings);
195
+ }
196
+ text = text.replace(/\b[A-Za-z0-9+/]{24,}={0,2}\b/g, (candidate) => {
197
+ const classification = decodedSecretClass(candidate);
198
+ if (!classification)
199
+ return candidate;
200
+ const redacted = marker(classification, candidate, options);
201
+ findings.push(redacted.finding);
202
+ return redacted.marker;
203
+ });
204
+ return { text, sensitivity: findings.length ? 'sensitive' : 'none', findings };
205
+ }
206
+ function sensitiveKey(key, options) {
207
+ if (BUILTIN_SENSITIVE_KEY.test(key))
208
+ return true;
209
+ let matched = false;
210
+ for (const source of options.sensitiveKeyPatterns ?? []) {
211
+ if (source.length === 0 || source.length > 256) {
212
+ throw new RedactionError('INVALID_PATTERN', 'sensitive key patterns must contain 1-256 characters');
213
+ }
214
+ try {
215
+ if (new RegExp(source, 'i').test(key))
216
+ matched = true;
217
+ }
218
+ catch {
219
+ throw new RedactionError('INVALID_PATTERN', 'a sensitive key pattern is not a valid regular expression');
220
+ }
221
+ }
222
+ return matched;
223
+ }
224
+ /** Recursively redact nested JSON/YAML-compatible values while preserving shape and field names. */
225
+ export function redactStructured(input, options = {}) {
226
+ const limits = resolvedLimits(options);
227
+ const findings = [];
228
+ const active = new WeakSet();
229
+ let nodes = 0;
230
+ const visit = (value, path, depth, key) => {
231
+ nodes += 1;
232
+ if (nodes > limits.maxNodes || depth > limits.maxDepth) {
233
+ throw new RedactionError('LIMIT_EXCEEDED', 'structured value exceeds configured redaction traversal limits');
234
+ }
235
+ if (key !== undefined && sensitiveKey(key, options)) {
236
+ const serialized = typeof value === 'string' ? value : JSON.stringify(value);
237
+ const redacted = marker('sensitive-field', serialized ?? String(value), options);
238
+ findings.push({ ...redacted.finding, path });
239
+ return redacted.marker;
240
+ }
241
+ if (value === null || typeof value === 'boolean' || typeof value === 'number')
242
+ return value;
243
+ if (typeof value === 'bigint')
244
+ return String(value);
245
+ if (typeof value === 'string') {
246
+ const redacted = redactText(value, options);
247
+ findings.push(...redacted.findings.map((finding) => ({ ...finding, path })));
248
+ return redacted.text;
249
+ }
250
+ if (typeof value !== 'object') {
251
+ throw new RedactionError('UNSUPPORTED_VALUE', `unsupported structured value at ${path}`);
252
+ }
253
+ if (active.has(value))
254
+ throw new RedactionError('UNSUPPORTED_VALUE', `circular structured value at ${path}`);
255
+ active.add(value);
256
+ try {
257
+ if (Array.isArray(value)) {
258
+ if (value.length > limits.maxArrayItems) {
259
+ throw new RedactionError('LIMIT_EXCEEDED', `array exceeds configured item limit at ${path}`);
260
+ }
261
+ return value.map((item, index) => visit(item, `${path}/${index}`, depth + 1));
262
+ }
263
+ const entries = Object.entries(value);
264
+ if (entries.length > limits.maxObjectKeys) {
265
+ throw new RedactionError('LIMIT_EXCEEDED', `object exceeds configured key limit at ${path}`);
266
+ }
267
+ return Object.fromEntries(entries.map(([childKey, child]) => [
268
+ childKey,
269
+ visit(child, `${path}/${childKey.replaceAll('~', '~0').replaceAll('/', '~1')}`, depth + 1, childKey),
270
+ ]));
271
+ }
272
+ finally {
273
+ active.delete(value);
274
+ }
275
+ };
276
+ const value = visit(input, '', 0);
277
+ return { value, sensitivity: findings.length ? 'sensitive' : 'none', findings };
278
+ }
279
+ /**
280
+ * A bounded streaming interface. It buffers one logical output value and emits
281
+ * only after full-value sanitization, so secrets split across chunks cannot
282
+ * escape. Exceeding the configured limit fails without emitting partial data.
283
+ */
284
+ export class RedactionTransform extends Transform {
285
+ options;
286
+ chunks = [];
287
+ bytes = 0;
288
+ limits;
289
+ constructor(options = {}) {
290
+ super();
291
+ this.options = options;
292
+ this.limits = resolvedLimits(options);
293
+ }
294
+ _transform(chunk, encoding, callback) {
295
+ const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding);
296
+ this.bytes += value.length;
297
+ if (this.bytes > this.limits.maxInputBytes) {
298
+ callback(new RedactionError('LIMIT_EXCEEDED', 'stream exceeds the configured redaction byte limit'));
299
+ return;
300
+ }
301
+ this.chunks.push(value);
302
+ callback();
303
+ }
304
+ _flush(callback) {
305
+ try {
306
+ let source;
307
+ try {
308
+ source = new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(this.chunks));
309
+ }
310
+ catch {
311
+ throw new RedactionError('INVALID_UTF8', 'stream is not valid UTF-8');
312
+ }
313
+ this.push(redactText(source, this.options).text);
314
+ callback();
315
+ }
316
+ catch (error) {
317
+ callback(error);
318
+ }
319
+ }
320
+ }
321
+ export function createRedactionTransform(options = {}) {
322
+ return new RedactionTransform(options);
323
+ }
324
+ //# sourceMappingURL=redaction.js.map