@tailrace/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1924 @@
1
+ // src/errors-docs.ts
2
+ var DOCS_ERRORS_BASE = "https://tailrace.dev/docs/reference/errors";
3
+ function withDocsUrl(code, message) {
4
+ const suffix = `\u2192 ${DOCS_ERRORS_BASE}/${code}`;
5
+ if (message.includes(suffix) || message.includes(`${DOCS_ERRORS_BASE}/${code}`)) {
6
+ return message;
7
+ }
8
+ return `${message} ${suffix}`;
9
+ }
10
+
11
+ // src/errors.ts
12
+ var TailraceError = class extends Error {
13
+ /** Stable, machine-readable error code. */
14
+ code;
15
+ constructor(code, message) {
16
+ super(withDocsUrl(code, message));
17
+ this.name = new.target.name;
18
+ this.code = code;
19
+ Object.setPrototypeOf(this, new.target.prototype);
20
+ }
21
+ };
22
+ var PolicyViolationError = class extends TailraceError {
23
+ decisions;
24
+ constructor(message, decisions) {
25
+ super("POLICY_VIOLATION", message);
26
+ this.decisions = decisions;
27
+ }
28
+ };
29
+ var PolicyValidationError = class extends TailraceError {
30
+ path;
31
+ constructor(message, path) {
32
+ super("POLICY_INVALID", message);
33
+ this.path = path;
34
+ }
35
+ };
36
+ var InvariantViolationError = class extends TailraceError {
37
+ constructor(message) {
38
+ super("INVARIANT", message);
39
+ }
40
+ };
41
+ var VaultError = class extends TailraceError {
42
+ constructor(message) {
43
+ super("VAULT", message);
44
+ }
45
+ };
46
+ var RecognizerError = class extends TailraceError {
47
+ constructor(message) {
48
+ super("RECOGNIZER", message);
49
+ }
50
+ };
51
+ var NotImplementedError = class extends TailraceError {
52
+ constructor(message) {
53
+ super("NOT_IMPLEMENTED", message);
54
+ }
55
+ };
56
+
57
+ // src/types.ts
58
+ var SECRET_ENTITY_CLASSES = [
59
+ "api_key",
60
+ "jwt",
61
+ "private_key",
62
+ "high_entropy_secret",
63
+ "connection_string"
64
+ ];
65
+ var PII_ENTITY_CLASSES = [
66
+ "email",
67
+ "phone",
68
+ "credit_card",
69
+ "iban",
70
+ "ssn",
71
+ "ip_address",
72
+ "url_credentials"
73
+ ];
74
+ var NER_ENTITY_CLASSES = ["person", "location", "organization"];
75
+
76
+ // src/policy/boundary.ts
77
+ function boundaryKey(boundary) {
78
+ switch (boundary.kind) {
79
+ case "model":
80
+ return boundary.provider;
81
+ case "tool":
82
+ return `tool:${boundary.name}:${boundary.direction}`;
83
+ case "mcp":
84
+ return `mcp:${boundary.server}/${boundary.tool}`;
85
+ case "telemetry":
86
+ return "telemetry";
87
+ case "egress":
88
+ return `egress:${boundary.sink}`;
89
+ }
90
+ }
91
+ function isEgressBoundaryKey(key) {
92
+ return key === "egress" || key.startsWith("egress:");
93
+ }
94
+ function boundaryKindOf(keyOrPattern) {
95
+ if (keyOrPattern === "telemetry") return "telemetry";
96
+ if (isEgressBoundaryKey(keyOrPattern)) return "egress";
97
+ if (keyOrPattern.startsWith("tool:")) return "tool";
98
+ if (keyOrPattern.startsWith("mcp:")) return "mcp";
99
+ return "model";
100
+ }
101
+ function globMatch(pattern, key) {
102
+ if (!pattern.includes("*")) return pattern === key;
103
+ const parts = pattern.split("*");
104
+ if (parts.length === 1) return pattern === key;
105
+ let pos = 0;
106
+ for (let i = 0; i < parts.length; i++) {
107
+ const part = parts[i];
108
+ if (part.length === 0) {
109
+ if (i === 0) continue;
110
+ if (i === parts.length - 1) return true;
111
+ continue;
112
+ }
113
+ if (i === 0) {
114
+ if (!key.startsWith(part)) return false;
115
+ pos = part.length;
116
+ continue;
117
+ }
118
+ if (i === parts.length - 1) {
119
+ return key.endsWith(part) && key.length - part.length >= pos;
120
+ }
121
+ const idx = key.indexOf(part, pos);
122
+ if (idx === -1) return false;
123
+ pos = idx + part.length;
124
+ }
125
+ return true;
126
+ }
127
+ function matchBoundaryPatterns(key, patterns) {
128
+ const kind = boundaryKindOf(key);
129
+ const matched = patterns.filter((p) => boundaryKindOf(p) === kind && globMatch(p, key));
130
+ matched.sort((a, b) => {
131
+ const aGlob = a.includes("*") ? 1 : 0;
132
+ const bGlob = b.includes("*") ? 1 : 0;
133
+ if (aGlob !== bGlob) return aGlob - bGlob;
134
+ return b.length - a.length;
135
+ });
136
+ return matched;
137
+ }
138
+
139
+ // src/policy/resolve.ts
140
+ var ACTION_RANK = {
141
+ block: 5,
142
+ review: 4,
143
+ tokenize: 3,
144
+ detokenize: 3,
145
+ mask: 2,
146
+ allow: 1
147
+ };
148
+ var SECRET_SET = new Set(SECRET_ENTITY_CLASSES);
149
+ var BLOCK_PII_SET = /* @__PURE__ */ new Set([...PII_ENTITY_CLASSES, ...NER_ENTITY_CLASSES]);
150
+ function moreRestrictive(a, b) {
151
+ return ACTION_RANK[a] >= ACTION_RANK[b] ? a : b;
152
+ }
153
+ function lookupEntity(entities, entity, pathPrefix) {
154
+ const exact = entities.get(entity);
155
+ if (exact !== void 0) return { rule: exact, path: `${pathPrefix}.${entity}` };
156
+ if (BLOCK_PII_SET.has(entity)) {
157
+ const blockPii = entities.get("block-pii");
158
+ if (blockPii !== void 0) return { rule: blockPii, path: `${pathPrefix}.block-pii` };
159
+ }
160
+ const star = entities.get("*");
161
+ if (star !== void 0) return { rule: star, path: `${pathPrefix}.*` };
162
+ return null;
163
+ }
164
+ function findBoundaryCandidate(boundaries, patterns, key, entity, pathPrefix) {
165
+ const matched = matchBoundaryPatterns(key, patterns);
166
+ for (const pattern of matched) {
167
+ const compiled = boundaries.find((b) => b.pattern === pattern);
168
+ if (compiled === void 0) continue;
169
+ const hit = lookupEntity(compiled.entities, entity, `${pathPrefix}.${pattern}.entities`);
170
+ if (hit !== null) return hit;
171
+ }
172
+ return null;
173
+ }
174
+ function collectCandidates(policy, entity, boundary, identity) {
175
+ const key = boundaryKey(boundary);
176
+ const agent = identity.agent || "default";
177
+ const out = [];
178
+ const push = (c) => {
179
+ if (c !== null) out.push(c);
180
+ };
181
+ const ident = policy.identities.get(agent);
182
+ if (ident !== void 0) {
183
+ push(
184
+ findBoundaryCandidate(
185
+ ident.boundaries,
186
+ ident.boundaryPatterns,
187
+ key,
188
+ entity,
189
+ `identities.${agent}.boundaries`
190
+ )
191
+ );
192
+ push(lookupEntity(ident.entities, entity, `identities.${agent}.entities`));
193
+ }
194
+ push(
195
+ findBoundaryCandidate(policy.boundaries, policy.boundaryPatterns, key, entity, "boundaries")
196
+ );
197
+ push(lookupEntity(policy.entities, entity, "entities"));
198
+ out.push({
199
+ rule: { action: policy.defaultsAction },
200
+ path: "defaults.action"
201
+ });
202
+ return out;
203
+ }
204
+ function resolve(policy, entity, boundary, identity) {
205
+ const candidates = collectCandidates(policy, entity, boundary, identity);
206
+ const winning = candidates[0];
207
+ let action = winning.rule.action;
208
+ let path = winning.path;
209
+ let format = winning.rule.format;
210
+ let dangerouslyAllowSecrets = winning.rule.dangerouslyAllowSecrets === true;
211
+ if (SECRET_SET.has(entity) && action === "allow" && !dangerouslyAllowSecrets && !policy.dangerouslyAllowSecrets) {
212
+ for (const c of candidates) {
213
+ if (c.rule.action === "block") {
214
+ action = "block";
215
+ path = c.path;
216
+ format = c.rule.format;
217
+ dangerouslyAllowSecrets = c.rule.dangerouslyAllowSecrets === true;
218
+ break;
219
+ }
220
+ }
221
+ }
222
+ return {
223
+ action,
224
+ rule: path.replace(/^\./, ""),
225
+ ...format !== void 0 ? { format } : {},
226
+ ...dangerouslyAllowSecrets ? { dangerouslyAllowSecrets: true } : {}
227
+ };
228
+ }
229
+
230
+ // src/console.ts
231
+ function getConsole() {
232
+ const c = globalThis.console;
233
+ return c;
234
+ }
235
+
236
+ // src/vault/alphabet.ts
237
+ var TOKEN_ID_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
238
+ var TOKEN_ID_LENGTH = 8;
239
+ var TOKEN_ID_CHAR_CLASS = "[a-z0-9]";
240
+ function assertAlphabetMatchesCharClass() {
241
+ if (TOKEN_ID_ALPHABET.length !== 36) {
242
+ throw new Error("TOKEN_ID_ALPHABET must have 36 characters (a-z0-9)");
243
+ }
244
+ const re = new RegExp(`^${TOKEN_ID_CHAR_CLASS}$`);
245
+ for (const ch of TOKEN_ID_ALPHABET) {
246
+ if (!re.test(ch)) {
247
+ throw new Error(
248
+ `TOKEN_ID_ALPHABET char "${ch}" is not matched by TOKEN_ID_CHAR_CLASS ${TOKEN_ID_CHAR_CLASS}`
249
+ );
250
+ }
251
+ }
252
+ for (const ch of "abcdefghijklmnopqrstuvwxyz0123456789") {
253
+ if (!TOKEN_ID_ALPHABET.includes(ch)) {
254
+ throw new Error(`TOKEN_ID_ALPHABET missing char-class member "${ch}"`);
255
+ }
256
+ }
257
+ }
258
+ assertAlphabetMatchesCharClass();
259
+ function bytesToTokenId(bytes) {
260
+ let n = 0n;
261
+ const take = Math.min(bytes.length, 8);
262
+ for (let i = 0; i < take; i++) {
263
+ n = n << 8n | BigInt(bytes[i]);
264
+ }
265
+ const base = BigInt(TOKEN_ID_ALPHABET.length);
266
+ let out = "";
267
+ for (let i = 0; i < TOKEN_ID_LENGTH; i++) {
268
+ out = TOKEN_ID_ALPHABET[Number(n % base)] + out;
269
+ n = n / base;
270
+ }
271
+ return out;
272
+ }
273
+ var FORMAT_PRESERVE_ENTITIES = /* @__PURE__ */ new Set([
274
+ "email",
275
+ "phone",
276
+ "credit_card"
277
+ ]);
278
+
279
+ // src/vault/crypto.ts
280
+ var textEncoder = new TextEncoder();
281
+ var textDecoder = new TextDecoder();
282
+ function bytesToHex(bytes) {
283
+ let out = "";
284
+ for (let i = 0; i < bytes.length; i++) {
285
+ out += bytes[i].toString(16).padStart(2, "0");
286
+ }
287
+ return out;
288
+ }
289
+ function bytesToBase64(bytes) {
290
+ let binary = "";
291
+ for (let i = 0; i < bytes.length; i++) {
292
+ binary += String.fromCharCode(bytes[i]);
293
+ }
294
+ return btoa(binary);
295
+ }
296
+ function base64ToBytes(b64) {
297
+ const binary = atob(b64);
298
+ const out = new Uint8Array(binary.length);
299
+ for (let i = 0; i < binary.length; i++) {
300
+ out[i] = binary.charCodeAt(i);
301
+ }
302
+ return out;
303
+ }
304
+ async function importHmacKey(raw) {
305
+ return crypto.subtle.importKey("raw", raw.slice(), { name: "HMAC", hash: "SHA-256" }, false, [
306
+ "sign"
307
+ ]);
308
+ }
309
+ async function hmacSha256(key, message) {
310
+ const cryptoKey = await importHmacKey(key);
311
+ const sig = await crypto.subtle.sign("HMAC", cryptoKey, message.slice());
312
+ return new Uint8Array(sig);
313
+ }
314
+ async function sha256Hex(value) {
315
+ const digest = await crypto.subtle.digest("SHA-256", textEncoder.encode(value));
316
+ return bytesToHex(new Uint8Array(digest));
317
+ }
318
+ function randomBytes(length) {
319
+ const out = new Uint8Array(length);
320
+ crypto.getRandomValues(out);
321
+ return out;
322
+ }
323
+ async function storageKeyMaterial(masterKey) {
324
+ const derived = await hmacSha256(masterKey, textEncoder.encode("storage"));
325
+ return crypto.subtle.importKey("raw", derived.slice(), { name: "AES-GCM" }, false, [
326
+ "encrypt",
327
+ "decrypt"
328
+ ]);
329
+ }
330
+ async function encryptAtRest(masterKey, plaintext) {
331
+ const key = await storageKeyMaterial(masterKey);
332
+ const iv = randomBytes(12);
333
+ const ct = await crypto.subtle.encrypt(
334
+ { name: "AES-GCM", iv: iv.slice() },
335
+ key,
336
+ textEncoder.encode(plaintext)
337
+ );
338
+ const packed = new Uint8Array(iv.length + ct.byteLength);
339
+ packed.set(iv, 0);
340
+ packed.set(new Uint8Array(ct), iv.length);
341
+ return bytesToBase64(packed);
342
+ }
343
+ async function decryptAtRest(masterKey, packedB64) {
344
+ const packed = base64ToBytes(packedB64);
345
+ if (packed.length < 13) {
346
+ throw new Error("ciphertext too short");
347
+ }
348
+ const iv = packed.subarray(0, 12);
349
+ const ct = packed.subarray(12);
350
+ const key = await storageKeyMaterial(masterKey);
351
+ const pt = await crypto.subtle.decrypt({ name: "AES-GCM", iv: iv.slice() }, key, ct.slice());
352
+ return textDecoder.decode(pt);
353
+ }
354
+ var ephemeralMasterKey;
355
+ var warnedEphemeral = false;
356
+ function envVaultKey() {
357
+ const g = globalThis;
358
+ return g.process?.env?.["TAILRACE_VAULT_KEY"];
359
+ }
360
+ function resolveMasterKey(explicit) {
361
+ if (explicit !== void 0 && explicit.length > 0) {
362
+ return textEncoder.encode(explicit);
363
+ }
364
+ const fromEnv = envVaultKey();
365
+ if (fromEnv !== void 0 && fromEnv.length > 0) {
366
+ return textEncoder.encode(fromEnv);
367
+ }
368
+ if (ephemeralMasterKey === void 0) {
369
+ ephemeralMasterKey = randomBytes(32);
370
+ if (!warnedEphemeral) {
371
+ warnedEphemeral = true;
372
+ getConsole()?.warn(
373
+ "[tailrace] No vault key configured; using ephemeral per-process key. Tokens will not survive restarts."
374
+ );
375
+ }
376
+ }
377
+ return ephemeralMasterKey;
378
+ }
379
+
380
+ // src/vault/token.ts
381
+ var textEncoder2 = new TextEncoder();
382
+ var LABEL_RE = new RegExp(
383
+ `<([A-Z][A-Z0-9_]*)_(${TOKEN_ID_CHAR_CLASS}{${TOKEN_ID_LENGTH}})>`,
384
+ "g"
385
+ );
386
+ var FPE_EMAIL_RE = new RegExp(
387
+ `\\b(${TOKEN_ID_CHAR_CLASS}{${TOKEN_ID_LENGTH}})@redacted\\.example\\b`,
388
+ "g"
389
+ );
390
+ var FPE_PHONE_RE = /\+1555(\d{7})\b/g;
391
+ var FPE_CARD_RE = /\b(9(?:[\d][\s-]?){12,18}\d)\b/g;
392
+ function normalizeValue(entity, value) {
393
+ const trimmed = value.trim();
394
+ if (entity === "email") return trimmed.toLowerCase();
395
+ if (entity === "phone" || entity === "credit_card") return trimmed.replace(/\D/g, "");
396
+ return trimmed;
397
+ }
398
+ function entityLabel(entity) {
399
+ return entity.toUpperCase();
400
+ }
401
+ function labelToken(entity, tokenId) {
402
+ return `<${entityLabel(entity)}_${tokenId}>`;
403
+ }
404
+ async function deriveWorkflowKey(masterKey, workflowId) {
405
+ return hmacSha256(masterKey, textEncoder2.encode(workflowId));
406
+ }
407
+ async function deriveTokenId(workflowKey, entity, normalizedValue) {
408
+ const entityBytes = textEncoder2.encode(entity);
409
+ const valueBytes = textEncoder2.encode(normalizedValue);
410
+ const msg = new Uint8Array(entityBytes.length + 1 + valueBytes.length);
411
+ msg.set(entityBytes, 0);
412
+ msg[entityBytes.length] = 0;
413
+ msg.set(valueBytes, entityBytes.length + 1);
414
+ const mac = await hmacSha256(workflowKey, msg);
415
+ return bytesToTokenId(mac);
416
+ }
417
+ function tokenIdToDigits(tokenId, count) {
418
+ let out = "";
419
+ let acc = 0;
420
+ for (let i = 0; out.length < count; i++) {
421
+ const c = tokenId.charCodeAt(i % tokenId.length);
422
+ acc = acc * 33 + c + i >>> 0;
423
+ out += String(acc % 10);
424
+ }
425
+ return out.slice(0, count);
426
+ }
427
+ function luhnCheckDigit(payloadWithoutCheck) {
428
+ let sum = 0;
429
+ let alt = true;
430
+ for (let i = payloadWithoutCheck.length - 1; i >= 0; i--) {
431
+ let n = Number(payloadWithoutCheck[i]);
432
+ if (alt) {
433
+ n *= 2;
434
+ if (n > 9) n -= 9;
435
+ }
436
+ sum += n;
437
+ alt = !alt;
438
+ }
439
+ return (10 - sum % 10) % 10;
440
+ }
441
+ function formatPreserveCard(original, tokenId) {
442
+ const digits = original.replace(/\D/g, "");
443
+ const len = Math.max(digits.length, 13);
444
+ const bodyLen = len - 1;
445
+ let body = tokenIdToDigits(tokenId, bodyLen);
446
+ body = "9" + body.slice(1);
447
+ const check = luhnCheckDigit(body);
448
+ const fake = body + String(check);
449
+ let di = 0;
450
+ let out = "";
451
+ for (let i = 0; i < original.length; i++) {
452
+ const ch = original[i];
453
+ if (/\d/.test(ch)) {
454
+ out += fake[di] ?? "0";
455
+ di++;
456
+ } else {
457
+ out += ch;
458
+ }
459
+ }
460
+ while (di < fake.length) {
461
+ out += fake[di];
462
+ di++;
463
+ }
464
+ return out;
465
+ }
466
+ function formatToken(entity, tokenId, format, originalValue) {
467
+ if (format === "preserve") {
468
+ if (entity === "email") return `${tokenId}@redacted.example`;
469
+ if (entity === "phone") return `+1555${tokenIdToDigits(tokenId, 7)}`;
470
+ if (entity === "credit_card") return formatPreserveCard(originalValue, tokenId);
471
+ getConsole()?.warn(
472
+ `[tailrace] format: "preserve" is not supported for entity "${entity}"; falling back to label token`
473
+ );
474
+ }
475
+ return labelToken(entity, tokenId);
476
+ }
477
+ function maskLabel(entity) {
478
+ return `[${entityLabel(entity)}]`;
479
+ }
480
+
481
+ // src/actions/pointer.ts
482
+ function unescapePointer(token) {
483
+ return token.replace(/~1/g, "/").replace(/~0/g, "~");
484
+ }
485
+ function cloneJson(value) {
486
+ if (typeof structuredClone === "function") {
487
+ return structuredClone(value);
488
+ }
489
+ return JSON.parse(JSON.stringify(value));
490
+ }
491
+ function getStringAtPointer(root, pointer) {
492
+ if (pointer === "" || pointer === "/") {
493
+ return null;
494
+ }
495
+ const parts = pointer.startsWith("/") ? pointer.slice(1).split("/") : pointer.split("/");
496
+ let cur = root;
497
+ for (const raw of parts) {
498
+ const key = unescapePointer(raw);
499
+ if (cur === null || typeof cur !== "object") return null;
500
+ if (Array.isArray(cur)) {
501
+ const idx = Number(key);
502
+ if (!Number.isInteger(idx) || idx < 0 || idx >= cur.length) return null;
503
+ cur = cur[idx];
504
+ } else {
505
+ if (!(key in cur)) return null;
506
+ cur = cur[key];
507
+ }
508
+ }
509
+ return typeof cur === "string" ? cur : null;
510
+ }
511
+ function setStringAtPointer(root, pointer, value) {
512
+ if (pointer === "" || pointer === "/") return;
513
+ const parts = pointer.startsWith("/") ? pointer.slice(1).split("/") : pointer.split("/");
514
+ let cur = root;
515
+ for (let i = 0; i < parts.length - 1; i++) {
516
+ const key = unescapePointer(parts[i]);
517
+ if (cur === null || typeof cur !== "object") return;
518
+ if (Array.isArray(cur)) {
519
+ const idx = Number(key);
520
+ cur = cur[idx];
521
+ } else {
522
+ cur = cur[key];
523
+ }
524
+ }
525
+ const last = unescapePointer(parts[parts.length - 1]);
526
+ if (cur === null || typeof cur !== "object") return;
527
+ if (Array.isArray(cur)) {
528
+ const idx = Number(last);
529
+ if (Number.isInteger(idx) && idx >= 0 && idx < cur.length && typeof cur[idx] === "string") {
530
+ cur[idx] = value;
531
+ }
532
+ } else if (typeof cur[last] === "string") {
533
+ cur[last] = value;
534
+ }
535
+ }
536
+
537
+ // src/actions/apply.ts
538
+ function collapseOverlaps(items) {
539
+ if (items.length === 0) return [];
540
+ const byPath = /* @__PURE__ */ new Map();
541
+ for (const item of items) {
542
+ const path = item.span.path ?? "";
543
+ const list = byPath.get(path) ?? [];
544
+ list.push(item);
545
+ byPath.set(path, list);
546
+ }
547
+ const out = [];
548
+ for (const group of byPath.values()) {
549
+ const sorted = [...group].sort(
550
+ (a, b) => a.span.start - b.span.start || b.span.end - a.span.end
551
+ );
552
+ const kept = [];
553
+ for (const item of sorted) {
554
+ const action = item.decision.action;
555
+ if (action === "restore_miss") continue;
556
+ const asAction = action;
557
+ let overlaps = false;
558
+ for (let i = 0; i < kept.length; i++) {
559
+ const other = kept[i];
560
+ if (item.span.start < other.span.end && item.span.end > other.span.start) {
561
+ overlaps = true;
562
+ const winner = moreRestrictive(asAction, other.action);
563
+ if (winner === asAction && asAction !== other.action) {
564
+ kept[i] = { ...item, action: asAction };
565
+ } else if (winner !== other.action) {
566
+ kept[i] = { ...other, action: winner };
567
+ }
568
+ break;
569
+ }
570
+ }
571
+ if (!overlaps) kept.push({ ...item, action: asAction });
572
+ }
573
+ out.push(...kept);
574
+ }
575
+ return out;
576
+ }
577
+ function extractValue(input, span) {
578
+ if (typeof input === "string") {
579
+ return input.slice(span.start, span.end);
580
+ }
581
+ const path = span.path ?? "";
582
+ const leaf = getStringAtPointer(input, path);
583
+ if (leaf === null) return "";
584
+ return leaf.slice(span.start, span.end);
585
+ }
586
+ function replaceInString(text, start, end, replacement) {
587
+ return text.slice(0, start) + replacement + text.slice(end);
588
+ }
589
+ async function applyActions(input, items, ctx) {
590
+ const collapsed = collapseOverlaps(items);
591
+ const blocks = collapsed.filter((i) => i.action === "block");
592
+ if (blocks.length > 0 && ctx.applyBlockAs !== "mask") {
593
+ const decisions2 = blocks.map((b) => ({ ...b.decision, action: "block" }));
594
+ const first = decisions2[0];
595
+ throw new PolicyViolationError(
596
+ `policy blocked entity "${first.entity}" via rule "${first.rule}"`,
597
+ decisions2
598
+ );
599
+ }
600
+ const reviews = collapsed.filter((i) => i.action === "review");
601
+ if (reviews.length > 0) {
602
+ throw new PolicyViolationError(
603
+ `policy review is not implemented (rule "${reviews[0].decision.rule}")`,
604
+ reviews.map((r) => r.decision)
605
+ );
606
+ }
607
+ const workflowKey = await deriveWorkflowKey(ctx.masterKey, ctx.workflowId);
608
+ let output = typeof input === "string" ? input : cloneJson(input);
609
+ const byPath = /* @__PURE__ */ new Map();
610
+ for (const item of collapsed) {
611
+ const path = item.span.path ?? "";
612
+ const list = byPath.get(path) ?? [];
613
+ list.push(item);
614
+ byPath.set(path, list);
615
+ }
616
+ const decisions = [];
617
+ for (const [path, group] of byPath) {
618
+ group.sort((a, b) => b.span.start - a.span.start);
619
+ let leaf;
620
+ if (typeof output === "string") {
621
+ leaf = output;
622
+ } else {
623
+ leaf = getStringAtPointer(output, path) ?? "";
624
+ }
625
+ for (const item of group) {
626
+ const resolvedAction = item.action;
627
+ const action = resolvedAction === "block" && ctx.applyBlockAs === "mask" ? "mask" : resolvedAction;
628
+ const value = item.value || extractValue(input, item.span);
629
+ let replacement = value;
630
+ if (action === "mask") {
631
+ replacement = maskLabel(item.span.entity);
632
+ } else if (action === "tokenize") {
633
+ const normalized = normalizeValue(item.span.entity, value);
634
+ const tokenId = await deriveTokenId(workflowKey, item.span.entity, normalized);
635
+ const token = formatToken(item.span.entity, tokenId, item.format, value);
636
+ await ctx.vault.put({
637
+ workflowId: ctx.workflowId,
638
+ token,
639
+ entity: item.span.entity,
640
+ value
641
+ });
642
+ replacement = token;
643
+ } else if (action === "allow" || action === "detokenize") {
644
+ replacement = value;
645
+ }
646
+ leaf = replaceInString(leaf, item.span.start, item.span.end, replacement);
647
+ const decision = resolvedAction === "block" && ctx.applyBlockAs === "mask" ? { ...item.decision, action: "block", appliedAs: "mask" } : item.decision;
648
+ decisions.push(decision);
649
+ }
650
+ if (typeof output === "string") {
651
+ output = leaf;
652
+ } else {
653
+ setStringAtPointer(output, path, leaf);
654
+ }
655
+ }
656
+ decisions.sort((a, b) => a.span.start - b.span.start || a.span.path.localeCompare(b.span.path));
657
+ return { output, decisions };
658
+ }
659
+
660
+ // src/actions/restore.ts
661
+ function findTokensInLeaf(leaf, path) {
662
+ const hits = [];
663
+ LABEL_RE.lastIndex = 0;
664
+ for (const m of leaf.matchAll(LABEL_RE)) {
665
+ const full = m[0];
666
+ const label = m[1];
667
+ const idx = m.index ?? 0;
668
+ hits.push({
669
+ start: idx,
670
+ end: idx + full.length,
671
+ token: full,
672
+ entity: label.toLowerCase(),
673
+ path
674
+ });
675
+ }
676
+ FPE_EMAIL_RE.lastIndex = 0;
677
+ for (const m of leaf.matchAll(FPE_EMAIL_RE)) {
678
+ const full = m[0];
679
+ const idx = m.index ?? 0;
680
+ hits.push({
681
+ start: idx,
682
+ end: idx + full.length,
683
+ token: full,
684
+ entity: "email",
685
+ path
686
+ });
687
+ }
688
+ FPE_PHONE_RE.lastIndex = 0;
689
+ for (const m of leaf.matchAll(FPE_PHONE_RE)) {
690
+ const full = m[0];
691
+ const idx = m.index ?? 0;
692
+ hits.push({
693
+ start: idx,
694
+ end: idx + full.length,
695
+ token: full,
696
+ entity: "phone",
697
+ path
698
+ });
699
+ }
700
+ FPE_CARD_RE.lastIndex = 0;
701
+ for (const m of leaf.matchAll(FPE_CARD_RE)) {
702
+ const full = m[0];
703
+ const idx = m.index ?? 0;
704
+ hits.push({
705
+ start: idx,
706
+ end: idx + full.length,
707
+ token: full,
708
+ entity: "credit_card",
709
+ path
710
+ });
711
+ }
712
+ hits.sort((a, b) => a.start - b.start || b.end - a.end);
713
+ const deduped = [];
714
+ for (const hit of hits) {
715
+ const overlap = deduped.some((d) => hit.start < d.end && hit.end > d.start);
716
+ if (!overlap) deduped.push(hit);
717
+ }
718
+ return deduped;
719
+ }
720
+ function collectLeaves(input) {
721
+ if (typeof input === "string") {
722
+ return [{ path: "", text: input }];
723
+ }
724
+ const out = [];
725
+ const walk = (value, path) => {
726
+ if (typeof value === "string") {
727
+ out.push({ path, text: value });
728
+ return;
729
+ }
730
+ if (value === null || typeof value !== "object") return;
731
+ if (Array.isArray(value)) {
732
+ for (let i = 0; i < value.length; i++) walk(value[i], `${path}/${i}`);
733
+ return;
734
+ }
735
+ for (const [k, v] of Object.entries(value)) {
736
+ const esc = k.replace(/~/g, "~0").replace(/\//g, "~1");
737
+ walk(v, `${path}/${esc}`);
738
+ }
739
+ };
740
+ walk(input, "");
741
+ return out;
742
+ }
743
+ async function restoreInput(input, ctx) {
744
+ const key = boundaryKey(ctx.boundary);
745
+ if (!isEgressBoundaryKey(key) || ctx.boundary.kind !== "egress") {
746
+ throw new InvariantViolationError("restore is only allowed at egress boundaries");
747
+ }
748
+ let output = typeof input === "string" ? input : cloneJson(input);
749
+ const decisions = [];
750
+ const leaves = collectLeaves(input);
751
+ for (const leaf of leaves) {
752
+ const hits = findTokensInLeaf(leaf.text, leaf.path);
753
+ hits.sort((a, b) => b.start - a.start);
754
+ let text = typeof output === "string" ? output : getStringAtPointer(output, leaf.path) ?? leaf.text;
755
+ for (const hit of hits) {
756
+ const record = await ctx.vault.get(ctx.workflowId, hit.token);
757
+ const contentHash = await sha256Hex(hit.token);
758
+ if (record === null) {
759
+ decisions.push({
760
+ action: "restore_miss",
761
+ entity: hit.entity,
762
+ boundary: ctx.boundary,
763
+ identity: ctx.identity,
764
+ rule: "restore",
765
+ span: { path: hit.path, start: hit.start, end: hit.end },
766
+ contentHash
767
+ });
768
+ continue;
769
+ }
770
+ text = text.slice(0, hit.start) + record.value + text.slice(hit.end);
771
+ decisions.push({
772
+ action: "detokenize",
773
+ entity: record.entity,
774
+ boundary: ctx.boundary,
775
+ identity: ctx.identity,
776
+ rule: "restore",
777
+ span: { path: hit.path, start: hit.start, end: hit.end },
778
+ contentHash
779
+ });
780
+ }
781
+ if (typeof output === "string") {
782
+ output = text;
783
+ } else {
784
+ setStringAtPointer(output, leaf.path, text);
785
+ }
786
+ }
787
+ decisions.sort((a, b) => a.span.start - b.span.start || a.span.path.localeCompare(b.span.path));
788
+ return { output, decisions };
789
+ }
790
+
791
+ // src/actions/stream-cut.ts
792
+ function computeStreamEmitEnd(length, spans, holdback, final) {
793
+ if (final) return length;
794
+ if (length <= holdback) return 0;
795
+ let cut = length - holdback;
796
+ for (const span of spans) {
797
+ if (span.start < cut && span.end > cut) {
798
+ cut = Math.min(cut, span.start);
799
+ }
800
+ }
801
+ return Math.max(0, cut);
802
+ }
803
+
804
+ // src/audit/emitter.ts
805
+ function warnSinkFailure(err) {
806
+ const message = err instanceof Error ? err.message : String(err);
807
+ getConsole()?.warn(`[tailrace] audit sink failed (swallowed): ${message}`);
808
+ }
809
+ function createAuditEmitter(sinks = [], onDecision) {
810
+ return {
811
+ emit(type, workflowId, decisions) {
812
+ if (onDecision !== void 0) {
813
+ try {
814
+ onDecision(decisions);
815
+ } catch (err) {
816
+ warnSinkFailure(err);
817
+ }
818
+ }
819
+ if (sinks.length === 0) return;
820
+ const event = {
821
+ type,
822
+ workflowId,
823
+ timestamp: Date.now(),
824
+ decisions
825
+ };
826
+ for (const sink of sinks) {
827
+ try {
828
+ const result = sink.emit(event);
829
+ if (result !== void 0 && typeof result.then === "function") {
830
+ void result.catch((err) => {
831
+ warnSinkFailure(err);
832
+ });
833
+ }
834
+ } catch (err) {
835
+ warnSinkFailure(err);
836
+ }
837
+ }
838
+ }
839
+ };
840
+ }
841
+
842
+ // src/detect/merge.ts
843
+ var DEFAULT_THRESHOLD = 0.6;
844
+ function mergeSpans(spans, opts = {}) {
845
+ const defaultThreshold = opts.defaultThreshold ?? DEFAULT_THRESHOLD;
846
+ const thresholds = opts.thresholds ?? {};
847
+ const thresholdFor = (entity) => thresholds[entity] ?? defaultThreshold;
848
+ const byEntity = /* @__PURE__ */ new Map();
849
+ for (const span of spans) {
850
+ if (span.confidence < thresholdFor(span.entity)) continue;
851
+ const group = byEntity.get(span.entity);
852
+ if (group) group.push(span);
853
+ else byEntity.set(span.entity, [span]);
854
+ }
855
+ const out = [];
856
+ for (const group of byEntity.values()) {
857
+ group.sort((a, b) => a.start - b.start || a.end - b.end);
858
+ let current = group[0];
859
+ for (let i = 1; i < group.length; i++) {
860
+ const span = group[i];
861
+ if (span.start < current.end) {
862
+ current = {
863
+ ...current,
864
+ end: Math.max(current.end, span.end),
865
+ confidence: Math.max(current.confidence, span.confidence)
866
+ };
867
+ } else {
868
+ out.push(current);
869
+ current = span;
870
+ }
871
+ }
872
+ out.push(current);
873
+ }
874
+ out.sort((a, b) => a.start - b.start || a.end - b.end);
875
+ return out;
876
+ }
877
+
878
+ // src/detect/object-scan.ts
879
+ var DEFAULT_MAX_DEPTH = 32;
880
+ function escapePointer(key) {
881
+ return key.replace(/~/g, "~0").replace(/\//g, "~1");
882
+ }
883
+ function scanObject(input, scanLeaf, maxDepth = DEFAULT_MAX_DEPTH) {
884
+ const out = [];
885
+ const seen = /* @__PURE__ */ new WeakSet();
886
+ const walk = (value, path, depth) => {
887
+ if (typeof value === "string") {
888
+ for (const span of scanLeaf(value)) out.push({ ...span, path });
889
+ return;
890
+ }
891
+ if (value === null || typeof value !== "object") return;
892
+ if (seen.has(value)) return;
893
+ if (depth >= maxDepth) return;
894
+ seen.add(value);
895
+ if (Array.isArray(value)) {
896
+ for (let i = 0; i < value.length; i++) {
897
+ walk(value[i], `${path}/${i}`, depth + 1);
898
+ }
899
+ } else {
900
+ for (const [key, child] of Object.entries(value)) {
901
+ const childPath = `${path}/${escapePointer(key)}`;
902
+ for (const span of scanLeaf(key)) out.push({ ...span, path: childPath });
903
+ walk(child, childPath, depth + 1);
904
+ }
905
+ }
906
+ };
907
+ walk(input, "", 0);
908
+ return out;
909
+ }
910
+
911
+ // src/detect/primitives.ts
912
+ function shannonEntropy(s) {
913
+ if (s.length === 0) return 0;
914
+ const counts = /* @__PURE__ */ new Map();
915
+ for (let i = 0; i < s.length; i++) {
916
+ const ch = s[i];
917
+ counts.set(ch, (counts.get(ch) ?? 0) + 1);
918
+ }
919
+ let entropy = 0;
920
+ for (const c of counts.values()) {
921
+ const p = c / s.length;
922
+ entropy -= p * Math.log2(p);
923
+ }
924
+ return entropy;
925
+ }
926
+ function hasBase62Charset(s) {
927
+ let lower = false;
928
+ let upper = false;
929
+ let digit = false;
930
+ for (let i = 0; i < s.length; i++) {
931
+ const code = s.charCodeAt(i);
932
+ if (code >= 97 && code <= 122) lower = true;
933
+ else if (code >= 65 && code <= 90) upper = true;
934
+ else if (code >= 48 && code <= 57) digit = true;
935
+ }
936
+ return lower && upper && digit;
937
+ }
938
+ function luhnValid(digits) {
939
+ if (digits.length === 0) return false;
940
+ let sum = 0;
941
+ let double = false;
942
+ for (let i = digits.length - 1; i >= 0; i--) {
943
+ let d = digits.charCodeAt(i) - 48;
944
+ if (d < 0 || d > 9) return false;
945
+ if (double) {
946
+ d *= 2;
947
+ if (d > 9) d -= 9;
948
+ }
949
+ sum += d;
950
+ double = !double;
951
+ }
952
+ return sum % 10 === 0;
953
+ }
954
+ function ibanValid(iban) {
955
+ const compact = iban.replace(/\s+/g, "").toUpperCase();
956
+ if (!/^[A-Z]{2}[0-9]{2}[A-Z0-9]{10,30}$/.test(compact)) return false;
957
+ const rearranged = compact.slice(4) + compact.slice(0, 4);
958
+ let remainder = 0;
959
+ for (let i = 0; i < rearranged.length; i++) {
960
+ const code = rearranged.charCodeAt(i);
961
+ const chunk = code >= 65 ? (code - 55).toString() : String.fromCharCode(code);
962
+ for (let j = 0; j < chunk.length; j++) {
963
+ remainder = (remainder * 10 + (chunk.charCodeAt(j) - 48)) % 97;
964
+ }
965
+ }
966
+ return remainder === 1;
967
+ }
968
+ function decodeBase64UrlJson(segment) {
969
+ try {
970
+ const b64 = segment.replace(/-/g, "+").replace(/_/g, "/");
971
+ const pad = b64.length % 4 === 0 ? "" : "=".repeat(4 - b64.length % 4);
972
+ const json = atob(b64 + pad);
973
+ const parsed = JSON.parse(json);
974
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
975
+ return parsed;
976
+ } catch {
977
+ return null;
978
+ }
979
+ }
980
+ function isJwtHeader(header) {
981
+ const alg = header["alg"];
982
+ if (typeof alg !== "string" || alg.length === 0) return false;
983
+ const typ = header["typ"];
984
+ if (typ !== void 0 && typeof typ === "string" && typ.toUpperCase() !== "JWT") return false;
985
+ return true;
986
+ }
987
+ function parseIpv4(s) {
988
+ const parts = s.split(".");
989
+ if (parts.length !== 4) return null;
990
+ const octets = [];
991
+ for (const part of parts) {
992
+ if (!/^\d{1,3}$/.test(part)) return null;
993
+ const n = Number(part);
994
+ if (n > 255) return null;
995
+ if (part.length > 1 && part[0] === "0") return null;
996
+ octets.push(n);
997
+ }
998
+ return [octets[0], octets[1], octets[2], octets[3]];
999
+ }
1000
+ function isPrivateOrReservedIpv4(octets) {
1001
+ const [a, b] = octets;
1002
+ if (a === 10) return true;
1003
+ if (a === 172 && b >= 16 && b <= 31) return true;
1004
+ if (a === 192 && b === 168) return true;
1005
+ if (a === 127) return true;
1006
+ if (a === 169 && b === 254) return true;
1007
+ if (a === 100 && b >= 64 && b <= 127) return true;
1008
+ if (a === 0) return true;
1009
+ if (a === 192 && b === 0 && octets[2] === 2) return true;
1010
+ if (a === 198 && (b === 18 || b === 19)) return true;
1011
+ if (a === 198 && b === 51 && octets[2] === 100) return true;
1012
+ if (a === 203 && b === 0 && octets[2] === 113) return true;
1013
+ if (a >= 224) return true;
1014
+ return false;
1015
+ }
1016
+ function isPrivateOrReservedIpv6(address) {
1017
+ const addr = address.toLowerCase();
1018
+ if (addr === "::1" || addr === "::") return true;
1019
+ if (addr.startsWith("fe8") || addr.startsWith("fe9") || addr.startsWith("fea") || addr.startsWith("feb"))
1020
+ return true;
1021
+ if (addr.startsWith("fc") || addr.startsWith("fd")) return true;
1022
+ if (addr.startsWith("2001:db8")) return true;
1023
+ if (addr.startsWith("ff")) return true;
1024
+ return false;
1025
+ }
1026
+
1027
+ // src/detect/recognizers/shared.ts
1028
+ function scanPatterns(text, patterns, entity, recognizer) {
1029
+ const spans = [];
1030
+ for (const { re, confidence } of patterns) {
1031
+ re.lastIndex = 0;
1032
+ let m;
1033
+ while ((m = re.exec(text)) !== null) {
1034
+ spans.push({ entity, start: m.index, end: m.index + m[0].length, confidence, recognizer });
1035
+ if (m[0].length === 0) re.lastIndex++;
1036
+ }
1037
+ }
1038
+ return spans;
1039
+ }
1040
+
1041
+ // src/detect/recognizers/pii.ts
1042
+ var EMAIL_PATTERNS = [
1043
+ // TLD length >= 2 is enforced by the `{2,}` (docs/detection.md §2).
1044
+ { re: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, confidence: 1 }
1045
+ ];
1046
+ var emailRecognizer = {
1047
+ id: "email",
1048
+ entities: ["email"],
1049
+ tier: 0,
1050
+ scan: (text) => scanPatterns(text, EMAIL_PATTERNS, "email", "email")
1051
+ };
1052
+ var PHONE_PATTERNS = [
1053
+ // E.164 / international with a `+` country prefix -> confidence 1.0.
1054
+ {
1055
+ re: /(?<![\w+])\+\d{1,3}[\s.-]?\(?\d{1,4}\)?(?:[\s.-]?\d{2,4}){1,4}(?![\w])/g,
1056
+ confidence: 1
1057
+ },
1058
+ // Grouped national format with separators -> confidence 0.8. Requires separators so we
1059
+ // don't match bare digit runs like order numbers.
1060
+ {
1061
+ re: /(?<![\w+])\(?\d{2,4}\)?[\s.-]\d{2,4}[\s.-]\d{2,4}(?:[\s.-]\d{2,4})?(?![\w])/g,
1062
+ confidence: 0.8
1063
+ }
1064
+ ];
1065
+ function digitCount(s) {
1066
+ let n = 0;
1067
+ for (let i = 0; i < s.length; i++) {
1068
+ const c = s.charCodeAt(i);
1069
+ if (c >= 48 && c <= 57) n++;
1070
+ }
1071
+ return n;
1072
+ }
1073
+ var phoneRecognizer = {
1074
+ id: "phone",
1075
+ entities: ["phone"],
1076
+ tier: 0,
1077
+ scan: (text) => {
1078
+ const spans = scanPatterns(text, PHONE_PATTERNS, "phone", "phone");
1079
+ return spans.filter((s) => {
1080
+ const digits = digitCount(text.slice(s.start, s.end));
1081
+ return digits >= 7 && digits <= 15;
1082
+ });
1083
+ }
1084
+ };
1085
+ var CREDIT_CARD_RE = /(?<![\d.])\d(?:[ -]?\d){12,18}(?![\d.])/g;
1086
+ var creditCardRecognizer = {
1087
+ id: "credit_card",
1088
+ entities: ["credit_card"],
1089
+ tier: 0,
1090
+ scan: (text) => {
1091
+ const spans = [];
1092
+ CREDIT_CARD_RE.lastIndex = 0;
1093
+ let m;
1094
+ while ((m = CREDIT_CARD_RE.exec(text)) !== null) {
1095
+ const digits = m[0].replace(/[^\d]/g, "");
1096
+ if (digits.length < 13 || digits.length > 19) continue;
1097
+ if (!luhnValid(digits)) continue;
1098
+ spans.push({
1099
+ entity: "credit_card",
1100
+ start: m.index,
1101
+ end: m.index + m[0].length,
1102
+ confidence: 1,
1103
+ recognizer: "credit_card"
1104
+ });
1105
+ }
1106
+ return spans;
1107
+ }
1108
+ };
1109
+ var IBAN_RE = /(?<![A-Za-z0-9])[A-Z]{2}\d{2}(?:\s?[A-Z0-9]){10,30}(?![A-Za-z0-9])/g;
1110
+ var ibanRecognizer = {
1111
+ id: "iban",
1112
+ entities: ["iban"],
1113
+ tier: 0,
1114
+ scan: (text) => {
1115
+ const spans = [];
1116
+ IBAN_RE.lastIndex = 0;
1117
+ let m;
1118
+ while ((m = IBAN_RE.exec(text)) !== null) {
1119
+ const raw = m[0].trimEnd();
1120
+ if (!ibanValid(raw)) continue;
1121
+ spans.push({
1122
+ entity: "iban",
1123
+ start: m.index,
1124
+ end: m.index + raw.length,
1125
+ confidence: 1,
1126
+ recognizer: "iban"
1127
+ });
1128
+ }
1129
+ return spans;
1130
+ }
1131
+ };
1132
+ var SSN_FORMATTED_RE = /(?<!\d)(\d{3})-(\d{2})-(\d{4})(?!\d)/g;
1133
+ var SSN_UNFORMATTED_RE = /(?<!\d)(\d{3})(\d{2})(\d{4})(?!\d)/g;
1134
+ var SSN_CONTEXT_RE = /ssn|social/gi;
1135
+ var SSN_CONTEXT_WINDOW = 30;
1136
+ function ssnPartsValid(area, group, serial) {
1137
+ const a = Number(area);
1138
+ if (a === 0 || a === 666 || a >= 900) return false;
1139
+ if (Number(group) === 0) return false;
1140
+ if (Number(serial) === 0) return false;
1141
+ return true;
1142
+ }
1143
+ var ssnRecognizer = {
1144
+ id: "ssn",
1145
+ entities: ["ssn"],
1146
+ tier: 0,
1147
+ scan: (text) => {
1148
+ const spans = [];
1149
+ SSN_FORMATTED_RE.lastIndex = 0;
1150
+ let m;
1151
+ while ((m = SSN_FORMATTED_RE.exec(text)) !== null) {
1152
+ if (!ssnPartsValid(m[1], m[2], m[3])) continue;
1153
+ spans.push({
1154
+ entity: "ssn",
1155
+ start: m.index,
1156
+ end: m.index + m[0].length,
1157
+ confidence: 1,
1158
+ recognizer: "ssn"
1159
+ });
1160
+ }
1161
+ const contextEnds = [];
1162
+ SSN_CONTEXT_RE.lastIndex = 0;
1163
+ let c;
1164
+ while ((c = SSN_CONTEXT_RE.exec(text)) !== null) contextEnds.push(c.index + c[0].length);
1165
+ if (contextEnds.length > 0) {
1166
+ SSN_UNFORMATTED_RE.lastIndex = 0;
1167
+ while ((m = SSN_UNFORMATTED_RE.exec(text)) !== null) {
1168
+ if (!ssnPartsValid(m[1], m[2], m[3])) continue;
1169
+ const start = m.index;
1170
+ const near = contextEnds.some((end) => start >= end && start - end <= SSN_CONTEXT_WINDOW);
1171
+ if (!near) continue;
1172
+ spans.push({
1173
+ entity: "ssn",
1174
+ start,
1175
+ end: start + m[0].length,
1176
+ confidence: 0.8,
1177
+ recognizer: "ssn"
1178
+ });
1179
+ }
1180
+ }
1181
+ return spans;
1182
+ }
1183
+ };
1184
+ var IPV4_RE = /(?<![\d.])\d{1,3}(?:\.\d{1,3}){3}(?![\d.])/g;
1185
+ var IPV6_RE = /(?<![:.\w])(?:(?:[A-Fa-f0-9]{1,4}:){7}[A-Fa-f0-9]{1,4}|(?:[A-Fa-f0-9]{1,4}:){1,7}:|(?:[A-Fa-f0-9]{1,4}:){1,6}:[A-Fa-f0-9]{1,4}|(?:[A-Fa-f0-9]{1,4}:){1,5}(?::[A-Fa-f0-9]{1,4}){1,2}|(?:[A-Fa-f0-9]{1,4}:){1,4}(?::[A-Fa-f0-9]{1,4}){1,3}|(?:[A-Fa-f0-9]{1,4}:){1,3}(?::[A-Fa-f0-9]{1,4}){1,4}|(?:[A-Fa-f0-9]{1,4}:){1,2}(?::[A-Fa-f0-9]{1,4}){1,5}|[A-Fa-f0-9]{1,4}:(?::[A-Fa-f0-9]{1,4}){1,6}|:(?:(?::[A-Fa-f0-9]{1,4}){1,7}|:)|::(?:ffff(?::0{1,4})?:)?(?:(?:25[0-5]|(?:2[0-4]|1?[0-9])?[0-9])\.){3}(?:25[0-5]|(?:2[0-4]|1?[0-9])?[0-9]))(?![:.\w])/g;
1186
+ function ipAddressRecognizer(includePrivateIps = false) {
1187
+ return {
1188
+ id: "ip_address",
1189
+ entities: ["ip_address"],
1190
+ tier: 0,
1191
+ scan: (text) => {
1192
+ const spans = [];
1193
+ IPV4_RE.lastIndex = 0;
1194
+ let m;
1195
+ while ((m = IPV4_RE.exec(text)) !== null) {
1196
+ const octets = parseIpv4(m[0]);
1197
+ if (octets === null) continue;
1198
+ if (!includePrivateIps && isPrivateOrReservedIpv4(octets)) continue;
1199
+ spans.push({
1200
+ entity: "ip_address",
1201
+ start: m.index,
1202
+ end: m.index + m[0].length,
1203
+ confidence: 1,
1204
+ recognizer: "ip_address"
1205
+ });
1206
+ }
1207
+ IPV6_RE.lastIndex = 0;
1208
+ while ((m = IPV6_RE.exec(text)) !== null) {
1209
+ if (!includePrivateIps && isPrivateOrReservedIpv6(m[0])) continue;
1210
+ spans.push({
1211
+ entity: "ip_address",
1212
+ start: m.index,
1213
+ end: m.index + m[0].length,
1214
+ confidence: 1,
1215
+ recognizer: "ip_address"
1216
+ });
1217
+ }
1218
+ return spans;
1219
+ }
1220
+ };
1221
+ }
1222
+ var URL_CREDENTIALS_PATTERNS = [
1223
+ { re: /[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^\s/:@]+:[^\s/@]+@[^\s"'<>]+/g, confidence: 1 }
1224
+ ];
1225
+ var urlCredentialsRecognizer = {
1226
+ id: "url_credentials",
1227
+ entities: ["url_credentials"],
1228
+ tier: 0,
1229
+ scan: (text) => scanPatterns(text, URL_CREDENTIALS_PATTERNS, "url_credentials", "url_credentials")
1230
+ };
1231
+ var STATIC_PII_RECOGNIZERS = [
1232
+ emailRecognizer,
1233
+ phoneRecognizer,
1234
+ creditCardRecognizer,
1235
+ ibanRecognizer,
1236
+ ssnRecognizer,
1237
+ urlCredentialsRecognizer
1238
+ ];
1239
+
1240
+ // src/detect/recognizers/secrets.ts
1241
+ var API_KEY_PATTERNS = [
1242
+ { re: /sk-ant-[A-Za-z0-9_-]{20,}/g, confidence: 1 },
1243
+ // Anthropic
1244
+ { re: /sk-[A-Za-z0-9]{20,}/g, confidence: 1 },
1245
+ // OpenAI (legacy)
1246
+ { re: /sk_(?:live|test)_[0-9A-Za-z]{16,}/g, confidence: 1 },
1247
+ // Stripe secret
1248
+ { re: /pk_(?:live|test)_[0-9A-Za-z]{16,}/g, confidence: 0.8 },
1249
+ // Stripe publishable (low-confidence)
1250
+ { re: /gh[opsur]_[A-Za-z0-9]{36,}/g, confidence: 1 },
1251
+ // GitHub token
1252
+ { re: /github_pat_[A-Za-z0-9_]{22,}/g, confidence: 1 },
1253
+ // GitHub fine-grained PAT
1254
+ { re: /AKIA[0-9A-Z]{16}/g, confidence: 1 },
1255
+ // AWS access key id
1256
+ { re: /xox[bpars]-[A-Za-z0-9-]{10,}/g, confidence: 1 },
1257
+ // Slack
1258
+ { re: /AIza[0-9A-Za-z_-]{35}/g, confidence: 1 },
1259
+ // Google
1260
+ { re: /SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/g, confidence: 1 },
1261
+ // SendGrid
1262
+ { re: /key-[0-9a-zA-Z]{32}/g, confidence: 0.8 },
1263
+ // Mailgun
1264
+ { re: /glpat-[A-Za-z0-9_-]{20}/g, confidence: 1 },
1265
+ // GitLab
1266
+ { re: /npm_[A-Za-z0-9]{36}/g, confidence: 1 },
1267
+ // npm
1268
+ { re: /dop_v1_[0-9a-f]{64}/g, confidence: 1 }
1269
+ // DigitalOcean
1270
+ ];
1271
+ var apiKeyRecognizer = {
1272
+ id: "api_key",
1273
+ entities: ["api_key"],
1274
+ tier: 0,
1275
+ scan: (text) => scanPatterns(text, API_KEY_PATTERNS, "api_key", "api_key")
1276
+ };
1277
+ var JWT_RE = /eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g;
1278
+ var jwtRecognizer = {
1279
+ id: "jwt",
1280
+ entities: ["jwt"],
1281
+ tier: 0,
1282
+ scan: (text) => {
1283
+ const spans = [];
1284
+ JWT_RE.lastIndex = 0;
1285
+ let m;
1286
+ while ((m = JWT_RE.exec(text)) !== null) {
1287
+ const header = decodeBase64UrlJson(m[0].slice(0, m[0].indexOf(".")));
1288
+ if (header !== null && isJwtHeader(header)) {
1289
+ spans.push({
1290
+ entity: "jwt",
1291
+ start: m.index,
1292
+ end: m.index + m[0].length,
1293
+ confidence: 1,
1294
+ recognizer: "jwt"
1295
+ });
1296
+ }
1297
+ }
1298
+ return spans;
1299
+ }
1300
+ };
1301
+ var PEM_PATTERNS = [
1302
+ {
1303
+ re: /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----(?:[\s\S]*?-----END (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----)?/g,
1304
+ confidence: 1
1305
+ }
1306
+ ];
1307
+ var privateKeyRecognizer = {
1308
+ id: "private_key",
1309
+ entities: ["private_key"],
1310
+ tier: 0,
1311
+ scan: (text) => scanPatterns(text, PEM_PATTERNS, "private_key", "private_key")
1312
+ };
1313
+ var TOKEN_RE = /[A-Za-z0-9+/_-]{20,64}/g;
1314
+ var KEYWORD_RE = /(?:secret|token|password|passwd|pwd|api[_-]?key|apikey|auth|credential|bearer)/gi;
1315
+ var ASSIGN_QUOTED_RE = /[=:]\s*["'`]([A-Za-z0-9+/=_-]{20,64})["'`]/g;
1316
+ var ENTROPY_THRESHOLD = 4;
1317
+ var KEYWORD_WINDOW = 40;
1318
+ var highEntropySecretRecognizer = {
1319
+ id: "high_entropy_secret",
1320
+ entities: ["high_entropy_secret"],
1321
+ tier: 0,
1322
+ scan: (text) => {
1323
+ const spans = [];
1324
+ const keywordEnds = [];
1325
+ KEYWORD_RE.lastIndex = 0;
1326
+ let k;
1327
+ while ((k = KEYWORD_RE.exec(text)) !== null) keywordEnds.push(k.index + k[0].length);
1328
+ const quotedStarts = /* @__PURE__ */ new Set();
1329
+ ASSIGN_QUOTED_RE.lastIndex = 0;
1330
+ let q;
1331
+ while ((q = ASSIGN_QUOTED_RE.exec(text)) !== null) {
1332
+ quotedStarts.add(q.index + q[0].indexOf(q[1]));
1333
+ }
1334
+ TOKEN_RE.lastIndex = 0;
1335
+ let m;
1336
+ while ((m = TOKEN_RE.exec(text)) !== null) {
1337
+ const tok = m[0];
1338
+ if (!hasBase62Charset(tok)) continue;
1339
+ if (shannonEntropy(tok) <= ENTROPY_THRESHOLD) continue;
1340
+ const start = m.index;
1341
+ const nearKeyword = keywordEnds.some((end) => start >= end && start - end <= KEYWORD_WINDOW);
1342
+ if (!nearKeyword && !quotedStarts.has(start)) continue;
1343
+ spans.push({
1344
+ entity: "high_entropy_secret",
1345
+ start,
1346
+ end: start + tok.length,
1347
+ confidence: 0.8,
1348
+ recognizer: "high_entropy_secret"
1349
+ });
1350
+ }
1351
+ return spans;
1352
+ }
1353
+ };
1354
+ var CONNECTION_STRING_PATTERNS = [
1355
+ {
1356
+ // URI schemes with user:pass@ userinfo.
1357
+ re: /(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|rediss?|amqp):\/\/[^\s:@/]+:[^\s@/]+@[^\s"'<>]+/gi,
1358
+ confidence: 1
1359
+ },
1360
+ {
1361
+ // ADO-style "Server=...;Password=...;".
1362
+ re: /(?:Server|Data Source)=[^;]+;(?:[^;]*;)*?\s*Password=[^;\s"']+/gi,
1363
+ confidence: 1
1364
+ }
1365
+ ];
1366
+ var connectionStringRecognizer = {
1367
+ id: "connection_string",
1368
+ entities: ["connection_string"],
1369
+ tier: 0,
1370
+ scan: (text) => scanPatterns(text, CONNECTION_STRING_PATTERNS, "connection_string", "connection_string")
1371
+ };
1372
+ var SECRET_RECOGNIZERS = [
1373
+ apiKeyRecognizer,
1374
+ jwtRecognizer,
1375
+ privateKeyRecognizer,
1376
+ highEntropySecretRecognizer,
1377
+ connectionStringRecognizer
1378
+ ];
1379
+
1380
+ // src/detect/recognizers/index.ts
1381
+ function builtinRecognizers(opts = {}) {
1382
+ return [
1383
+ ...SECRET_RECOGNIZERS,
1384
+ ...STATIC_PII_RECOGNIZERS,
1385
+ ipAddressRecognizer(opts.includePrivateIps ?? false)
1386
+ ];
1387
+ }
1388
+
1389
+ // src/detect/engine.ts
1390
+ function createDetectionEngine(opts = {}) {
1391
+ const recognizers = [
1392
+ ...opts.useBuiltins === false ? [] : builtinRecognizers({ includePrivateIps: opts.includePrivateIps ?? false }),
1393
+ ...opts.recognizers ?? []
1394
+ ];
1395
+ const mergeOpts = {
1396
+ ...opts.thresholds !== void 0 ? { thresholds: opts.thresholds } : {},
1397
+ ...opts.defaultThreshold !== void 0 ? { defaultThreshold: opts.defaultThreshold } : {}
1398
+ };
1399
+ const runRecognizers = (text) => {
1400
+ const spans = [];
1401
+ for (const recognizer of recognizers) {
1402
+ const result = recognizer.scan(text);
1403
+ if (result instanceof Promise) {
1404
+ throw new NotImplementedError(
1405
+ "async (Tier 1) recognizers are wired in a later milestone; Tier 0 detection is synchronous"
1406
+ );
1407
+ }
1408
+ for (const span of result) spans.push(span);
1409
+ }
1410
+ return spans;
1411
+ };
1412
+ const scanLeaf = (text) => mergeSpans(runRecognizers(text), mergeOpts);
1413
+ const detect = (input) => typeof input === "string" ? scanLeaf(input) : scanObject(input, scanLeaf);
1414
+ return { recognizers, detect };
1415
+ }
1416
+
1417
+ // src/policy/normalize.ts
1418
+ function normalizeRule(value) {
1419
+ if (typeof value === "string") {
1420
+ return { action: value };
1421
+ }
1422
+ return {
1423
+ action: value.action,
1424
+ ...value.format !== void 0 ? { format: value.format } : {},
1425
+ ...value.dangerouslyAllowSecrets !== void 0 ? { dangerouslyAllowSecrets: value.dangerouslyAllowSecrets } : {}
1426
+ };
1427
+ }
1428
+
1429
+ // src/policy/validate.ts
1430
+ var VALID_ACTIONS = /* @__PURE__ */ new Set([
1431
+ "allow",
1432
+ "mask",
1433
+ "tokenize",
1434
+ "block",
1435
+ "review",
1436
+ "detokenize"
1437
+ ]);
1438
+ function assertAction(action, path, underEgress) {
1439
+ if (!VALID_ACTIONS.has(action)) {
1440
+ throw new PolicyValidationError(`unknown action "${String(action)}"`, path);
1441
+ }
1442
+ if (action === "review") {
1443
+ throw new NotImplementedError("review action ships in v0.2");
1444
+ }
1445
+ if (action === "detokenize" && !underEgress) {
1446
+ throw new PolicyValidationError("detokenize is only valid under egress boundary keys", path);
1447
+ }
1448
+ }
1449
+ function validateRuleValue(value, path, underEgress) {
1450
+ const rule = normalizeRule(value);
1451
+ assertAction(rule.action, `${path}.action`, underEgress);
1452
+ if (rule.format !== void 0 && rule.format !== "preserve" && rule.format !== "label") {
1453
+ throw new PolicyValidationError(`unknown format "${String(rule.format)}"`, `${path}.format`);
1454
+ }
1455
+ }
1456
+ function validateEntities(entities, path, underEgress) {
1457
+ if (entities === void 0) return;
1458
+ for (const [entity, value] of Object.entries(entities)) {
1459
+ if (value === void 0) continue;
1460
+ validateRuleValue(value, `${path}.${entity}`, underEgress);
1461
+ }
1462
+ }
1463
+ function validateScope(scope, path) {
1464
+ validateEntities(scope.entities, `${path}.entities`, false);
1465
+ if (scope.boundaries === void 0) return;
1466
+ for (const [key, body] of Object.entries(scope.boundaries)) {
1467
+ const underEgress = isEgressBoundaryKey(key);
1468
+ validateEntities(body?.entities, `${path}.boundaries.${key}.entities`, underEgress);
1469
+ }
1470
+ }
1471
+ function validatePolicy(doc) {
1472
+ if (doc.defaults?.action !== void 0) {
1473
+ assertAction(doc.defaults.action, "defaults.action", false);
1474
+ }
1475
+ validateScope(doc, "");
1476
+ if (doc.identities === void 0) return;
1477
+ for (const [agent, scope] of Object.entries(doc.identities)) {
1478
+ validateScope(scope, `identities.${agent}`);
1479
+ }
1480
+ }
1481
+
1482
+ // src/policy/compile.ts
1483
+ function compileEntities(entities) {
1484
+ const map = /* @__PURE__ */ new Map();
1485
+ if (entities === void 0) return map;
1486
+ for (const [key, value] of Object.entries(entities)) {
1487
+ if (value === void 0) continue;
1488
+ map.set(key, normalizeRule(value));
1489
+ }
1490
+ return map;
1491
+ }
1492
+ function compileBoundaries(boundaries) {
1493
+ const compiled = [];
1494
+ if (boundaries === void 0) {
1495
+ return { boundaries: compiled, boundaryPatterns: [] };
1496
+ }
1497
+ for (const [pattern, body] of Object.entries(boundaries)) {
1498
+ compiled.push({
1499
+ pattern,
1500
+ entities: compileEntities(body?.entities)
1501
+ });
1502
+ }
1503
+ compiled.sort((a, b) => {
1504
+ const aGlob = a.pattern.includes("*") ? 1 : 0;
1505
+ const bGlob = b.pattern.includes("*") ? 1 : 0;
1506
+ if (aGlob !== bGlob) return aGlob - bGlob;
1507
+ return b.pattern.length - a.pattern.length;
1508
+ });
1509
+ return {
1510
+ boundaries: compiled,
1511
+ boundaryPatterns: compiled.map((b) => b.pattern)
1512
+ };
1513
+ }
1514
+ function compileScope(scope) {
1515
+ const entities = compileEntities(scope.entities);
1516
+ const { boundaries, boundaryPatterns } = compileBoundaries(scope.boundaries);
1517
+ return { entities, boundaries, boundaryPatterns };
1518
+ }
1519
+ function warnUnsupportedPreserve(entities, path) {
1520
+ for (const [entity, rule] of entities) {
1521
+ if (rule.format !== "preserve") continue;
1522
+ if (FORMAT_PRESERVE_ENTITIES.has(entity)) continue;
1523
+ getConsole()?.warn(
1524
+ `[tailrace] format: "preserve" is not supported for entity "${entity}" at ${path}.${entity}; falling back to label token`
1525
+ );
1526
+ }
1527
+ }
1528
+ function warnPreserveFallbacks(compiled) {
1529
+ warnUnsupportedPreserve(compiled.entities, "entities");
1530
+ for (const boundary of compiled.boundaries) {
1531
+ warnUnsupportedPreserve(boundary.entities, `boundaries.${boundary.pattern}.entities`);
1532
+ }
1533
+ for (const [agent, ident] of compiled.identities) {
1534
+ warnUnsupportedPreserve(ident.entities, `identities.${agent}.entities`);
1535
+ for (const boundary of ident.boundaries) {
1536
+ warnUnsupportedPreserve(
1537
+ boundary.entities,
1538
+ `identities.${agent}.boundaries.${boundary.pattern}.entities`
1539
+ );
1540
+ }
1541
+ }
1542
+ }
1543
+ function compilePolicy(doc) {
1544
+ validatePolicy(doc);
1545
+ const { boundaries, boundaryPatterns } = compileBoundaries(doc.boundaries);
1546
+ const identities = /* @__PURE__ */ new Map();
1547
+ if (doc.identities !== void 0) {
1548
+ for (const [agent, scope] of Object.entries(doc.identities)) {
1549
+ identities.set(agent, compileScope(scope));
1550
+ }
1551
+ }
1552
+ const compiled = {
1553
+ defaultsAction: doc.defaults?.action ?? "allow",
1554
+ dangerouslyAllowSecrets: doc.dangerouslyAllowSecrets === true,
1555
+ entities: compileEntities(doc.entities),
1556
+ boundaries,
1557
+ boundaryPatterns,
1558
+ identities
1559
+ };
1560
+ warnPreserveFallbacks(compiled);
1561
+ return compiled;
1562
+ }
1563
+
1564
+ // src/policy/default.ts
1565
+ var DEFAULT_TOKENIZE_PII = [
1566
+ "email",
1567
+ "phone",
1568
+ "credit_card",
1569
+ "iban",
1570
+ "ssn"
1571
+ ];
1572
+ function defaultPolicy() {
1573
+ const entities = {};
1574
+ for (const entity of SECRET_ENTITY_CLASSES) {
1575
+ entities[entity] = "block";
1576
+ }
1577
+ for (const entity of DEFAULT_TOKENIZE_PII) {
1578
+ entities[entity] = "tokenize";
1579
+ }
1580
+ entities.url_credentials = "block";
1581
+ return {
1582
+ defaults: { action: "allow" },
1583
+ entities,
1584
+ boundaries: {
1585
+ "egress:*": { entities: { "*": "detokenize" } }
1586
+ }
1587
+ };
1588
+ }
1589
+
1590
+ // src/vault/keys.ts
1591
+ var vaultKeys = /* @__PURE__ */ new WeakMap();
1592
+ function registerVaultKey(vault, masterKey) {
1593
+ vaultKeys.set(vault, masterKey);
1594
+ }
1595
+ function getVaultKey(vault) {
1596
+ return vaultKeys.get(vault);
1597
+ }
1598
+
1599
+ // src/vault/shared.ts
1600
+ function storageKey(workflowId, token) {
1601
+ return `tailrace:v1:${workflowId}:${token}`;
1602
+ }
1603
+ async function encodeRecord(masterKey, record, expiresAt) {
1604
+ const ciphertext = await encryptAtRest(masterKey, record.value);
1605
+ const encoded = {
1606
+ entity: record.entity,
1607
+ ciphertext,
1608
+ ...expiresAt !== void 0 ? { expiresAt } : {}
1609
+ };
1610
+ return JSON.stringify(encoded);
1611
+ }
1612
+ async function decodeRecord(masterKey, raw, now = Date.now()) {
1613
+ let parsed;
1614
+ try {
1615
+ parsed = JSON.parse(raw);
1616
+ } catch {
1617
+ throw new VaultError("corrupt vault record");
1618
+ }
1619
+ if (parsed.expiresAt !== void 0 && parsed.expiresAt <= now) {
1620
+ return null;
1621
+ }
1622
+ const value = await decryptAtRest(masterKey, parsed.ciphertext);
1623
+ return {
1624
+ value,
1625
+ entity: parsed.entity,
1626
+ ...parsed.expiresAt !== void 0 ? { expiresAt: parsed.expiresAt } : {}
1627
+ };
1628
+ }
1629
+ function assertNoCollision(existing, value, token) {
1630
+ if (existing !== null && existing.value !== value) {
1631
+ throw new VaultError(`token collision for ${token}`);
1632
+ }
1633
+ }
1634
+
1635
+ // src/vault/memory.ts
1636
+ var DEFAULT_TTL_SECONDS = 86400;
1637
+ function memoryVault(opts) {
1638
+ const masterKey = resolveMasterKey(opts?.key);
1639
+ const ttlSeconds = opts?.ttlSeconds ?? DEFAULT_TTL_SECONDS;
1640
+ const store = /* @__PURE__ */ new Map();
1641
+ const sweep = (now) => {
1642
+ for (const [key, entry] of store) {
1643
+ if (entry.expiresAt !== void 0 && entry.expiresAt <= now) {
1644
+ store.delete(key);
1645
+ }
1646
+ }
1647
+ };
1648
+ const vault = {
1649
+ async put(input) {
1650
+ const now = Date.now();
1651
+ sweep(now);
1652
+ const key = storageKey(input.workflowId, input.token);
1653
+ const existingRaw = store.get(key);
1654
+ if (existingRaw !== void 0) {
1655
+ const existing = await decodeRecord(masterKey, existingRaw.raw, now);
1656
+ assertNoCollision(existing, input.value, input.token);
1657
+ if (existing !== null) return;
1658
+ }
1659
+ const expiresAt = input.expiresAt ?? (ttlSeconds > 0 ? now + ttlSeconds * 1e3 : void 0);
1660
+ const raw = await encodeRecord(
1661
+ masterKey,
1662
+ { value: input.value, entity: input.entity },
1663
+ expiresAt
1664
+ );
1665
+ store.set(key, {
1666
+ raw,
1667
+ ...expiresAt !== void 0 ? { expiresAt } : {}
1668
+ });
1669
+ },
1670
+ async get(workflowId, token) {
1671
+ const now = Date.now();
1672
+ sweep(now);
1673
+ const entry = store.get(storageKey(workflowId, token));
1674
+ if (entry === void 0) return null;
1675
+ const decoded = await decodeRecord(masterKey, entry.raw, now);
1676
+ if (decoded === null) {
1677
+ store.delete(storageKey(workflowId, token));
1678
+ return null;
1679
+ }
1680
+ return { value: decoded.value, entity: decoded.entity };
1681
+ },
1682
+ async purge(workflowId) {
1683
+ const prefix = `tailrace:v1:${workflowId}:`;
1684
+ for (const key of store.keys()) {
1685
+ if (key.startsWith(prefix)) store.delete(key);
1686
+ }
1687
+ }
1688
+ };
1689
+ registerVaultKey(vault, masterKey);
1690
+ return vault;
1691
+ }
1692
+
1693
+ // src/audit/sinks.ts
1694
+ function consoleSink() {
1695
+ return {
1696
+ emit(event) {
1697
+ getConsole()?.log(JSON.stringify(event));
1698
+ }
1699
+ };
1700
+ }
1701
+ function jsonlSink(writer) {
1702
+ return {
1703
+ async emit(event) {
1704
+ await writer.write(JSON.stringify(event) + "\n");
1705
+ }
1706
+ };
1707
+ }
1708
+
1709
+ // src/vault/kv.ts
1710
+ var DEFAULT_TTL_SECONDS2 = 86400;
1711
+ function kvVault(kv, opts) {
1712
+ const masterKey = resolveMasterKey(opts?.key);
1713
+ const ttlSeconds = opts?.ttlSeconds ?? DEFAULT_TTL_SECONDS2;
1714
+ const vault = {
1715
+ async put(input) {
1716
+ const key = storageKey(input.workflowId, input.token);
1717
+ const existingRaw = await kv.get(key);
1718
+ if (existingRaw !== null) {
1719
+ const existing = await decodeRecord(masterKey, existingRaw);
1720
+ assertNoCollision(existing, input.value, input.token);
1721
+ if (existing !== null) return;
1722
+ }
1723
+ const now = Date.now();
1724
+ const expiresAt = input.expiresAt ?? (ttlSeconds > 0 ? now + ttlSeconds * 1e3 : void 0);
1725
+ const raw = await encodeRecord(
1726
+ masterKey,
1727
+ { value: input.value, entity: input.entity },
1728
+ expiresAt
1729
+ );
1730
+ const putOpts = expiresAt !== void 0 ? { expirationTtl: Math.max(1, Math.ceil((expiresAt - now) / 1e3)) } : ttlSeconds > 0 ? { expirationTtl: ttlSeconds } : void 0;
1731
+ if (putOpts !== void 0) {
1732
+ await kv.put(key, raw, putOpts);
1733
+ } else {
1734
+ await kv.put(key, raw);
1735
+ }
1736
+ },
1737
+ async get(workflowId, token) {
1738
+ const raw = await kv.get(storageKey(workflowId, token));
1739
+ if (raw === null) return null;
1740
+ const decoded = await decodeRecord(masterKey, raw);
1741
+ if (decoded === null) {
1742
+ await kv.delete(storageKey(workflowId, token));
1743
+ return null;
1744
+ }
1745
+ return { value: decoded.value, entity: decoded.entity };
1746
+ },
1747
+ async purge(workflowId) {
1748
+ }
1749
+ };
1750
+ registerVaultKey(vault, masterKey);
1751
+ return vault;
1752
+ }
1753
+
1754
+ // src/index.ts
1755
+ function isVault(value) {
1756
+ return typeof value.put === "function";
1757
+ }
1758
+ function isPolicySource(value) {
1759
+ return typeof value.load === "function";
1760
+ }
1761
+ function extractSpanValue(input, span) {
1762
+ if (typeof input === "string") {
1763
+ return input.slice(span.start, span.end);
1764
+ }
1765
+ const path = span.path ?? "";
1766
+ const leaf = getStringAtPointer(input, path);
1767
+ if (leaf === null) return "";
1768
+ return leaf.slice(span.start, span.end);
1769
+ }
1770
+ function resolveVault(options) {
1771
+ if (options.vault === void 0) {
1772
+ const vault2 = memoryVault();
1773
+ return { vault: vault2, masterKey: getVaultKey(vault2) ?? resolveMasterKey() };
1774
+ }
1775
+ if (isVault(options.vault)) {
1776
+ const vault2 = options.vault;
1777
+ return { vault: vault2, masterKey: getVaultKey(vault2) ?? resolveMasterKey() };
1778
+ }
1779
+ const vault = memoryVault(options.vault);
1780
+ return { vault, masterKey: getVaultKey(vault) ?? resolveMasterKey(options.vault.key) };
1781
+ }
1782
+ function createTailrace(options = {}) {
1783
+ const engine = createDetectionEngine({
1784
+ ...options.recognizers !== void 0 ? { recognizers: options.recognizers } : {},
1785
+ ...options.includePrivateIps !== void 0 ? { includePrivateIps: options.includePrivateIps } : {}
1786
+ });
1787
+ const { vault, masterKey } = resolveVault(options);
1788
+ const audit = createAuditEmitter(options.audit?.sinks ?? [], options.onDecision);
1789
+ let compiled = null;
1790
+ let unsubscribe;
1791
+ const loadAndCompile = async () => {
1792
+ if (compiled !== null) return compiled;
1793
+ let doc;
1794
+ if (options.policy === void 0) {
1795
+ doc = defaultPolicy();
1796
+ } else if (isPolicySource(options.policy)) {
1797
+ doc = await options.policy.load();
1798
+ if (options.policy.subscribe !== void 0 && unsubscribe === void 0) {
1799
+ unsubscribe = options.policy.subscribe((next) => {
1800
+ compiled = compilePolicy(next);
1801
+ });
1802
+ }
1803
+ } else {
1804
+ doc = options.policy;
1805
+ }
1806
+ compiled = compilePolicy(doc);
1807
+ return compiled;
1808
+ };
1809
+ const check = async (input, ctx, options2) => {
1810
+ const workflowId = ctx.workflowId ?? "default";
1811
+ const identity = {
1812
+ agent: ctx.identity.agent || "default",
1813
+ ...ctx.identity.claims !== void 0 ? { claims: ctx.identity.claims } : {}
1814
+ };
1815
+ const policy = await loadAndCompile();
1816
+ const spans = engine.detect(input);
1817
+ const items = [];
1818
+ for (const span of spans) {
1819
+ const value = extractSpanValue(input, span);
1820
+ const resolved = resolve(policy, span.entity, ctx.boundary, identity);
1821
+ const contentHash = await sha256Hex(value);
1822
+ const decision = {
1823
+ action: resolved.action,
1824
+ entity: span.entity,
1825
+ boundary: ctx.boundary,
1826
+ identity,
1827
+ rule: resolved.rule,
1828
+ span: {
1829
+ path: span.path ?? "",
1830
+ start: span.start,
1831
+ end: span.end
1832
+ },
1833
+ contentHash
1834
+ };
1835
+ items.push({
1836
+ span,
1837
+ decision,
1838
+ value,
1839
+ ...resolved.format !== void 0 ? { format: resolved.format } : {}
1840
+ });
1841
+ }
1842
+ const stream = options2?.stream;
1843
+ const streamString = stream !== void 0 && typeof input === "string";
1844
+ try {
1845
+ if (streamString) {
1846
+ const text = input;
1847
+ const emitEnd = computeStreamEmitEnd(
1848
+ text.length,
1849
+ items.map((i) => i.span),
1850
+ stream.holdback,
1851
+ stream.final === true
1852
+ );
1853
+ if (options2?.applyBlockAs !== "mask") {
1854
+ const blocks = items.filter((i) => i.decision.action === "block");
1855
+ if (blocks.length > 0) {
1856
+ const decisions3 = blocks.map((b) => ({ ...b.decision, action: "block" }));
1857
+ const first = decisions3[0];
1858
+ throw new PolicyViolationError(
1859
+ `policy blocked entity "${first.entity}" via rule "${first.rule}"`,
1860
+ decisions3
1861
+ );
1862
+ }
1863
+ }
1864
+ const toApply = items.filter((i) => i.span.end <= emitEnd);
1865
+ const prefix = text.slice(0, emitEnd);
1866
+ const remainder = text.slice(emitEnd);
1867
+ if (prefix.length === 0) {
1868
+ return { output: "", decisions: [], blocked: false, remainder };
1869
+ }
1870
+ const { output: output2, decisions: decisions2 } = await applyActions(prefix, toApply, {
1871
+ vault,
1872
+ masterKey,
1873
+ workflowId,
1874
+ ...options2?.applyBlockAs !== void 0 ? { applyBlockAs: options2.applyBlockAs } : {}
1875
+ });
1876
+ audit.emit("check", workflowId, decisions2);
1877
+ return { output: output2, decisions: decisions2, blocked: false, remainder };
1878
+ }
1879
+ const { output, decisions } = await applyActions(input, items, {
1880
+ vault,
1881
+ masterKey,
1882
+ workflowId,
1883
+ ...options2?.applyBlockAs !== void 0 ? { applyBlockAs: options2.applyBlockAs } : {}
1884
+ });
1885
+ audit.emit("check", workflowId, decisions);
1886
+ return { output, decisions, blocked: false };
1887
+ } catch (err) {
1888
+ if (err instanceof PolicyViolationError) {
1889
+ audit.emit("check", workflowId, err.decisions);
1890
+ }
1891
+ throw err;
1892
+ }
1893
+ };
1894
+ const restore = async (input, ctx) => {
1895
+ const workflowId = ctx.workflowId ?? "default";
1896
+ const identity = {
1897
+ agent: ctx.identity.agent || "default",
1898
+ ...ctx.identity.claims !== void 0 ? { claims: ctx.identity.claims } : {}
1899
+ };
1900
+ const { output, decisions } = await restoreInput(input, {
1901
+ vault,
1902
+ workflowId,
1903
+ boundary: ctx.boundary,
1904
+ identity
1905
+ });
1906
+ audit.emit("restore", workflowId, decisions);
1907
+ return { output, decisions, blocked: false };
1908
+ };
1909
+ return { check, restore };
1910
+ }
1911
+ function definePolicy(doc) {
1912
+ validatePolicy(doc);
1913
+ return doc;
1914
+ }
1915
+ function defineRecognizer(recognizer) {
1916
+ return recognizer;
1917
+ }
1918
+ function staticPolicy(doc) {
1919
+ return { load: () => Promise.resolve(doc) };
1920
+ }
1921
+
1922
+ export { InvariantViolationError, NER_ENTITY_CLASSES, NotImplementedError, PII_ENTITY_CLASSES, PolicyValidationError, PolicyViolationError, RecognizerError, SECRET_ENTITY_CLASSES, TailraceError, VaultError, consoleSink, createTailrace, definePolicy, defineRecognizer, jsonlSink, kvVault, memoryVault, staticPolicy };
1923
+ //# sourceMappingURL=index.js.map
1924
+ //# sourceMappingURL=index.js.map