@rotorsoft/act 1.8.0 → 1.10.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.
Files changed (42) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/@types/act.d.ts +34 -2
  3. package/dist/@types/act.d.ts.map +1 -1
  4. package/dist/@types/adapters/in-memory-store.d.ts +12 -0
  5. package/dist/@types/adapters/in-memory-store.d.ts.map +1 -1
  6. package/dist/@types/builders/act-builder.d.ts.map +1 -1
  7. package/dist/@types/builders/state-builder.d.ts +31 -1
  8. package/dist/@types/builders/state-builder.d.ts.map +1 -1
  9. package/dist/@types/internal/event-sourcing.d.ts +7 -2
  10. package/dist/@types/internal/event-sourcing.d.ts.map +1 -1
  11. package/dist/@types/internal/index.d.ts +1 -0
  12. package/dist/@types/internal/index.d.ts.map +1 -1
  13. package/dist/@types/internal/reactions.d.ts +5 -4
  14. package/dist/@types/internal/reactions.d.ts.map +1 -1
  15. package/dist/@types/internal/sensitive.d.ts +147 -0
  16. package/dist/@types/internal/sensitive.d.ts.map +1 -0
  17. package/dist/@types/internal/tracing.d.ts.map +1 -1
  18. package/dist/@types/types/action.d.ts +57 -0
  19. package/dist/@types/types/action.d.ts.map +1 -1
  20. package/dist/@types/types/registry.d.ts +9 -1
  21. package/dist/@types/types/registry.d.ts.map +1 -1
  22. package/dist/@types/types/schemas.d.ts +36 -0
  23. package/dist/@types/types/schemas.d.ts.map +1 -1
  24. package/dist/{chunk-I4L224TZ.js → chunk-3ZTFNAY7.js} +46 -6
  25. package/dist/chunk-3ZTFNAY7.js.map +1 -0
  26. package/dist/chunk-XSBT63QX.js +267 -0
  27. package/dist/chunk-XSBT63QX.js.map +1 -0
  28. package/dist/index.cjs +335 -82
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.js +155 -32
  31. package/dist/index.js.map +1 -1
  32. package/dist/test/index.cjs +106 -60
  33. package/dist/test/index.cjs.map +1 -1
  34. package/dist/test/index.js +11 -11
  35. package/dist/test/index.js.map +1 -1
  36. package/dist/types/index.cjs +52 -34
  37. package/dist/types/index.cjs.map +1 -1
  38. package/dist/types/index.js +9 -3
  39. package/package.json +2 -2
  40. package/dist/chunk-I4L224TZ.js.map +0 -1
  41. package/dist/chunk-PMAZTOSO.js +0 -164
  42. package/dist/chunk-PMAZTOSO.js.map +0 -1
package/dist/index.cjs CHANGED
@@ -51,6 +51,8 @@ __export(index_exports, {
51
51
  NonRetryableError: () => NonRetryableError,
52
52
  PackageSchema: () => PackageSchema,
53
53
  QuerySchema: () => QuerySchema,
54
+ REDACTED: () => REDACTED,
55
+ SHREDDED: () => SHREDDED,
54
56
  SNAP_EVENT: () => SNAP_EVENT,
55
57
  StreamClosedError: () => StreamClosedError,
56
58
  TOMBSTONE_EVENT: () => TOMBSTONE_EVENT,
@@ -67,6 +69,7 @@ __export(index_exports, {
67
69
  port: () => port,
68
70
  projection: () => projection,
69
71
  scoped: () => scoped,
72
+ sensitive: () => sensitive,
70
73
  sleep: () => sleep,
71
74
  slice: () => slice,
72
75
  state: () => state,
@@ -380,55 +383,150 @@ var NonRetryableError = class extends Error {
380
383
  };
381
384
 
382
385
  // src/utils.ts
383
- var import_zod3 = require("zod");
386
+ var import_zod4 = require("zod");
384
387
 
385
388
  // src/config.ts
386
389
  var fs = __toESM(require("fs"), 1);
387
- var import_zod2 = require("zod");
390
+ var import_zod3 = require("zod");
388
391
 
389
392
  // src/types/schemas.ts
393
+ var import_zod2 = require("zod");
394
+
395
+ // src/internal/sensitive.ts
390
396
  var import_zod = require("zod");
391
- var ZodEmpty = import_zod.z.record(import_zod.z.string(), import_zod.z.never());
392
- var ActorSchema = import_zod.z.object({
393
- id: import_zod.z.string(),
394
- name: import_zod.z.string()
397
+ var REDACTED = "[REDACTED]";
398
+ var SHREDDED = "[SHREDDED]";
399
+ var _registry = import_zod.z.registry();
400
+ function is_pii(schema) {
401
+ let cur = schema;
402
+ while (true) {
403
+ if (_registry.has(cur)) return true;
404
+ const inner = cur._def?.innerType;
405
+ if (!inner || inner === cur) return false;
406
+ cur = inner;
407
+ }
408
+ }
409
+ function pii_fields(schema) {
410
+ const shape = schema.shape;
411
+ if (!shape || typeof shape !== "object") return [];
412
+ const fields = [];
413
+ for (const key of Object.keys(shape)) {
414
+ if (is_pii(shape[key])) fields.push(key);
415
+ }
416
+ return fields;
417
+ }
418
+ function pii_split(emitted, fields) {
419
+ const rec = emitted.data;
420
+ const clean = {};
421
+ const pii = {};
422
+ for (const k of Object.keys(rec)) {
423
+ if (fields.includes(k)) pii[k] = rec[k];
424
+ else clean[k] = rec[k];
425
+ }
426
+ return { name: emitted.name, data: clean, pii };
427
+ }
428
+ function pii_merge(event, fields) {
429
+ const data = event.data;
430
+ const pii = event.pii;
431
+ if (pii != null) {
432
+ return {
433
+ ...event,
434
+ data: { ...data, ...pii }
435
+ };
436
+ }
437
+ const shredded = { ...data };
438
+ for (const f of fields) shredded[f] = SHREDDED;
439
+ return {
440
+ ...event,
441
+ data: shredded
442
+ };
443
+ }
444
+ function pii_gate(event, fields, predicate, actor) {
445
+ const data = event.data;
446
+ if (event.pii == null) {
447
+ const shredded = { ...data };
448
+ for (const f of fields) shredded[f] = SHREDDED;
449
+ return {
450
+ ...event,
451
+ data: shredded
452
+ };
453
+ }
454
+ const allowed = !!actor && !!predicate && predicate(event, actor);
455
+ if (allowed) {
456
+ return {
457
+ ...event,
458
+ data: {
459
+ ...data,
460
+ ...event.pii
461
+ }
462
+ };
463
+ }
464
+ const redacted = { ...data };
465
+ for (const f of fields) redacted[f] = REDACTED;
466
+ return {
467
+ ...event,
468
+ data: redacted
469
+ };
470
+ }
471
+ function pii_strip(event, fields) {
472
+ const data = event.data;
473
+ const stripped = {};
474
+ for (const k of Object.keys(data)) {
475
+ if (!fields.includes(k)) stripped[k] = data[k];
476
+ }
477
+ const { pii: _drop_pii, ...rest } = event;
478
+ return {
479
+ ...rest,
480
+ data: stripped
481
+ };
482
+ }
483
+
484
+ // src/types/schemas.ts
485
+ var ZodEmpty = import_zod2.z.record(import_zod2.z.string(), import_zod2.z.never());
486
+ function sensitive(schema) {
487
+ _registry.add(schema, { sensitive: true });
488
+ return schema;
489
+ }
490
+ var ActorSchema = import_zod2.z.object({
491
+ id: import_zod2.z.string(),
492
+ name: import_zod2.z.string()
395
493
  }).loose().readonly();
396
- var TargetSchema = import_zod.z.object({
397
- stream: import_zod.z.string(),
494
+ var TargetSchema = import_zod2.z.object({
495
+ stream: import_zod2.z.string(),
398
496
  actor: ActorSchema,
399
- expectedVersion: import_zod.z.number().optional()
497
+ expectedVersion: import_zod2.z.number().optional()
400
498
  }).loose().readonly();
401
- var CausationEventSchema = import_zod.z.object({
402
- id: import_zod.z.number(),
403
- name: import_zod.z.string(),
404
- stream: import_zod.z.string()
499
+ var CausationEventSchema = import_zod2.z.object({
500
+ id: import_zod2.z.number(),
501
+ name: import_zod2.z.string(),
502
+ stream: import_zod2.z.string()
405
503
  });
406
- var EventMetaSchema = import_zod.z.object({
407
- correlation: import_zod.z.string(),
408
- causation: import_zod.z.object({
409
- action: TargetSchema.and(import_zod.z.object({ name: import_zod.z.string() })).optional(),
504
+ var EventMetaSchema = import_zod2.z.object({
505
+ correlation: import_zod2.z.string(),
506
+ causation: import_zod2.z.object({
507
+ action: TargetSchema.and(import_zod2.z.object({ name: import_zod2.z.string() })).optional(),
410
508
  event: CausationEventSchema.optional()
411
509
  })
412
510
  }).readonly();
413
- var CommittedMetaSchema = import_zod.z.object({
414
- id: import_zod.z.number(),
415
- stream: import_zod.z.string(),
416
- version: import_zod.z.number(),
417
- created: import_zod.z.date(),
511
+ var CommittedMetaSchema = import_zod2.z.object({
512
+ id: import_zod2.z.number(),
513
+ stream: import_zod2.z.string(),
514
+ version: import_zod2.z.number(),
515
+ created: import_zod2.z.date(),
418
516
  meta: EventMetaSchema
419
517
  }).readonly();
420
- var QuerySchema = import_zod.z.object({
421
- stream: import_zod.z.string().optional(),
422
- names: import_zod.z.string().array().optional(),
423
- before: import_zod.z.number().optional(),
424
- after: import_zod.z.number().optional(),
425
- limit: import_zod.z.number().optional(),
426
- created_before: import_zod.z.date().optional(),
427
- created_after: import_zod.z.date().optional(),
428
- backward: import_zod.z.boolean().optional(),
429
- correlation: import_zod.z.string().optional(),
430
- with_snaps: import_zod.z.boolean().optional(),
431
- stream_exact: import_zod.z.boolean().optional()
518
+ var QuerySchema = import_zod2.z.object({
519
+ stream: import_zod2.z.string().optional(),
520
+ names: import_zod2.z.string().array().optional(),
521
+ before: import_zod2.z.number().optional(),
522
+ after: import_zod2.z.number().optional(),
523
+ limit: import_zod2.z.number().optional(),
524
+ created_before: import_zod2.z.date().optional(),
525
+ created_after: import_zod2.z.date().optional(),
526
+ backward: import_zod2.z.boolean().optional(),
527
+ correlation: import_zod2.z.string().optional(),
528
+ with_snaps: import_zod2.z.boolean().optional(),
529
+ stream_exact: import_zod2.z.boolean().optional()
432
530
  }).readonly();
433
531
 
434
532
  // src/types/index.ts
@@ -448,13 +546,13 @@ var LogLevels = [
448
546
  ];
449
547
 
450
548
  // src/config.ts
451
- var PackageSchema = import_zod2.z.object({
452
- name: import_zod2.z.string().min(1),
453
- version: import_zod2.z.string().min(1),
454
- description: import_zod2.z.string().min(1).optional(),
455
- author: import_zod2.z.object({ name: import_zod2.z.string().min(1), email: import_zod2.z.string().optional() }).optional().or(import_zod2.z.string().min(1)).optional(),
456
- license: import_zod2.z.string().min(1).optional(),
457
- dependencies: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.string()).optional()
549
+ var PackageSchema = import_zod3.z.object({
550
+ name: import_zod3.z.string().min(1),
551
+ version: import_zod3.z.string().min(1),
552
+ description: import_zod3.z.string().min(1).optional(),
553
+ author: import_zod3.z.object({ name: import_zod3.z.string().min(1), email: import_zod3.z.string().optional() }).optional().or(import_zod3.z.string().min(1)).optional(),
554
+ license: import_zod3.z.string().min(1).optional(),
555
+ dependencies: import_zod3.z.record(import_zod3.z.string(), import_zod3.z.string()).optional()
458
556
  });
459
557
  var FALLBACK_PACKAGE = {
460
558
  name: "act-fallback",
@@ -472,10 +570,10 @@ var getPackage = () => {
472
570
  };
473
571
  var pkgLoadError;
474
572
  var BaseSchema = PackageSchema.extend({
475
- env: import_zod2.z.enum(Environments),
476
- logLevel: import_zod2.z.enum(LogLevels),
477
- logSingleLine: import_zod2.z.boolean(),
478
- sleepMs: import_zod2.z.number().int().min(0).max(5e3)
573
+ env: import_zod3.z.enum(Environments),
574
+ logLevel: import_zod3.z.enum(LogLevels),
575
+ logSingleLine: import_zod3.z.boolean(),
576
+ sleepMs: import_zod3.z.number().int().min(0).max(5e3)
479
577
  });
480
578
  var { NODE_ENV, LOG_LEVEL, LOG_SINGLE_LINE, SLEEP_MS } = process.env;
481
579
  var env = NODE_ENV || "development";
@@ -506,8 +604,8 @@ var validate = (target, payload, schema) => {
506
604
  try {
507
605
  return schema ? schema.parse(payload) : payload;
508
606
  } catch (error) {
509
- if (error instanceof import_zod3.ZodError) {
510
- throw new ValidationError(target, payload, (0, import_zod3.prettifyError)(error));
607
+ if (error instanceof import_zod4.ZodError) {
608
+ throw new ValidationError(target, payload, (0, import_zod4.prettifyError)(error));
511
609
  }
512
610
  throw new ValidationError(target, payload, error);
513
611
  }
@@ -687,11 +785,24 @@ var InMemoryStore = class {
687
785
  _maxEventIdByStream = /* @__PURE__ */ new Map();
688
786
  // global max non-snapshot event id — fast pre-check for source-less streams in claim()
689
787
  _maxNonSnapEventId = -1;
788
+ // stream → (event_id → cloned sensitive payload). Two-level so `forget_pii`
789
+ // is O(1) — drop the inner Map for the stream and the wipe is done — mirroring
790
+ // the `DELETE WHERE stream = ?` scope that durable adapters get from their
791
+ // stream index. Entries exist only for events committed with a non-null
792
+ // `pii` field; absence means "no PII" (returned as `null` on load).
793
+ _pii = /* @__PURE__ */ new Map();
690
794
  _resetIndexes() {
691
795
  this._events.length = 0;
692
796
  this._streamVersions.clear();
693
797
  this._maxEventIdByStream.clear();
694
798
  this._maxNonSnapEventId = -1;
799
+ this._pii.clear();
800
+ }
801
+ // Attach the isolated PII payload (or null) to an event before handing it to
802
+ // a caller. Allocation-free for events without PII — by far the common case.
803
+ _withPii(e) {
804
+ const pii = this._pii.get(e.stream)?.get(e.id);
805
+ return pii ? { ...e, pii } : e;
695
806
  }
696
807
  /**
697
808
  * Dispose of the store and clear all events.
@@ -747,7 +858,9 @@ var InMemoryStore = class {
747
858
  continue;
748
859
  if (query.after && e.id <= query.after) break;
749
860
  if (query.created_after && e.created <= query.created_after) break;
750
- await Promise.resolve(callback(e));
861
+ await Promise.resolve(
862
+ callback(this._withPii(e))
863
+ );
751
864
  count++;
752
865
  if (query?.limit && count >= query.limit) break;
753
866
  }
@@ -759,7 +872,9 @@ var InMemoryStore = class {
759
872
  if (query?.created_after && e.created <= query.created_after) continue;
760
873
  if (query?.before && e.id >= query.before) break;
761
874
  if (query?.created_before && e.created >= query.created_before) break;
762
- await Promise.resolve(callback(e));
875
+ await Promise.resolve(
876
+ callback(this._withPii(e))
877
+ );
763
878
  count++;
764
879
  if (query?.limit && count >= query.limit) break;
765
880
  }
@@ -788,7 +903,7 @@ var InMemoryStore = class {
788
903
  }
789
904
  let version = currentVersion + 1;
790
905
  let lastNonSnapId = -1;
791
- const committed = msgs.map(({ name, data }) => {
906
+ const committed = msgs.map(({ name, data, pii }) => {
792
907
  const c = {
793
908
  id: this._events.length,
794
909
  stream,
@@ -799,9 +914,17 @@ var InMemoryStore = class {
799
914
  meta
800
915
  };
801
916
  this._events.push(c);
917
+ if (pii != null) {
918
+ let perStream = this._pii.get(stream);
919
+ if (!perStream) {
920
+ perStream = /* @__PURE__ */ new Map();
921
+ this._pii.set(stream, perStream);
922
+ }
923
+ perStream.set(c.id, structuredClone(pii));
924
+ }
802
925
  if (name !== SNAP_EVENT) lastNonSnapId = c.id;
803
926
  version++;
804
- return c;
927
+ return this._withPii(c);
805
928
  });
806
929
  this._streamVersions.set(stream, version - 1);
807
930
  if (lastNonSnapId >= 0) {
@@ -982,6 +1105,21 @@ var InMemoryStore = class {
982
1105
  * @param input - Stream names or a filter selecting the streams to unblock.
983
1106
  * @returns Count of streams that were actually flipped (were blocked).
984
1107
  */
1108
+ /**
1109
+ * Wipe the sensitive-data payload for every event on the stream — see
1110
+ * {@link Store.forget_pii}. O(1) drop of the stream's inner Map; the size of
1111
+ * that Map is the count of events that had PII. Idempotent: a second call
1112
+ * finds no inner Map and returns `0`.
1113
+ *
1114
+ * @param stream - Target stream.
1115
+ * @returns Count of events whose isolated PII payload was deleted.
1116
+ */
1117
+ async forget_pii(stream) {
1118
+ await sleep();
1119
+ const count = this._pii.get(stream)?.size ?? 0;
1120
+ this._pii.delete(stream);
1121
+ return count;
1122
+ }
985
1123
  async unblock(input) {
986
1124
  await sleep();
987
1125
  let count = 0;
@@ -2294,7 +2432,7 @@ async function scan(source, opts = {}, callback) {
2294
2432
  }
2295
2433
  };
2296
2434
  }
2297
- async function load(me, stream, callback, asOf) {
2435
+ async function load(me, stream, callback, asOf, actor) {
2298
2436
  const timeTravel = !!asOf && Object.values(asOf).some((v) => v !== void 0);
2299
2437
  const cached = timeTravel ? void 0 : await cache2().get(stream);
2300
2438
  const cache_hit = !!cached;
@@ -2306,15 +2444,16 @@ async function load(me, stream, callback, asOf) {
2306
2444
  let event;
2307
2445
  await store2().query(
2308
2446
  (e) => {
2309
- event = e;
2310
2447
  version = e.version;
2448
+ const typed = e;
2449
+ event = me.view(typed, actor);
2311
2450
  if (e.name === SNAP_EVENT) {
2312
2451
  state2 = e.data;
2313
2452
  snaps++;
2314
2453
  patches = 0;
2315
2454
  replayed++;
2316
2455
  } else if (me.patch[e.name]) {
2317
- state2 = (0, import_act_patch.patch)(state2, me.patch[e.name](event, state2));
2456
+ state2 = (0, import_act_patch.patch)(state2, me.patch[e.name](typed, state2));
2318
2457
  patches++;
2319
2458
  replayed++;
2320
2459
  } else if (e.name !== TOMBSTONE_EVENT) {
@@ -2357,7 +2496,13 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
2357
2496
  const maxRetries = opts?.maxRetries ?? 0;
2358
2497
  for (let attempt = 0; ; attempt++) {
2359
2498
  try {
2360
- const snapshot = await load(me, stream);
2499
+ const snapshot = await load(
2500
+ me,
2501
+ stream,
2502
+ void 0,
2503
+ void 0,
2504
+ target.actor
2505
+ );
2361
2506
  if (snapshot.event?.name === TOMBSTONE_EVENT)
2362
2507
  throw new StreamClosedError(stream);
2363
2508
  const expected = expectedVersion ?? snapshot.event?.version;
@@ -2394,10 +2539,10 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
2394
2539
  }
2395
2540
  }
2396
2541
  }
2397
- const emitted = tuples.map(([name, data]) => ({
2398
- name,
2399
- data: skipValidation ? data : validate(name, data, me.events[name])
2400
- }));
2542
+ const emitted = tuples.map(([name, data]) => {
2543
+ const validated2 = skipValidation ? data : validate(name, data, me.events[name]);
2544
+ return me.message({ name, data: validated2 });
2545
+ });
2401
2546
  const meta = {
2402
2547
  correlation: reactingTo?.meta.correlation || correlator({
2403
2548
  action: action2,
@@ -2409,8 +2554,8 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
2409
2554
  action: {
2410
2555
  name: action2,
2411
2556
  ...target
2412
- // payload intentionally omitted: it can be large or contain PII,
2413
- // and callers correlate via the correlation id when they need it.
2557
+ // payload intentionally omitted from causation metadata
2558
+ // callers correlate via the correlation id when they need it.
2414
2559
  },
2415
2560
  event: reactingTo ? {
2416
2561
  id: reactingTo.id,
@@ -2443,7 +2588,7 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
2443
2588
  state2 = (0, import_act_patch.patch)(state2, p);
2444
2589
  patches++;
2445
2590
  return {
2446
- event,
2591
+ event: me.view(event, target.actor),
2447
2592
  state: state2,
2448
2593
  version: event.version,
2449
2594
  patches,
@@ -2528,7 +2673,7 @@ var traced = (inner, exit, entry) => (async (...args) => {
2528
2673
  return result;
2529
2674
  });
2530
2675
  function buildEs(logger, correlator = defaultCorrelator) {
2531
- const boundAction = (me, actionName, target, payload, reactingTo, skipValidation = false) => action(
2676
+ const bound_action = (me, actionName, target, payload, reactingTo, skipValidation = false) => action(
2532
2677
  me,
2533
2678
  actionName,
2534
2679
  target,
@@ -2541,7 +2686,7 @@ function buildEs(logger, correlator = defaultCorrelator) {
2541
2686
  return {
2542
2687
  snap,
2543
2688
  load,
2544
- action: boundAction,
2689
+ action: bound_action,
2545
2690
  tombstone
2546
2691
  };
2547
2692
  }
@@ -2571,7 +2716,7 @@ function buildEs(logger, correlator = defaultCorrelator) {
2571
2716
  );
2572
2717
  }),
2573
2718
  action: traced(
2574
- boundAction,
2719
+ bound_action,
2575
2720
  (snapshots, _me, _action, target) => {
2576
2721
  const committed = snapshots.filter((s) => s.event);
2577
2722
  if (committed.length) {
@@ -2864,7 +3009,7 @@ var DrainController = class {
2864
3009
  };
2865
3010
 
2866
3011
  // src/internal/merge.ts
2867
- var import_zod4 = require("zod");
3012
+ var import_zod5 = require("zod");
2868
3013
  function baseTypeName(zodType) {
2869
3014
  let t = zodType;
2870
3015
  while (typeof t.unwrap === "function") {
@@ -2873,7 +3018,7 @@ function baseTypeName(zodType) {
2873
3018
  return t.constructor.name;
2874
3019
  }
2875
3020
  function mergeSchemas(existing, incoming, stateName) {
2876
- if (existing instanceof import_zod4.ZodObject && incoming instanceof import_zod4.ZodObject) {
3021
+ if (existing instanceof import_zod5.ZodObject && incoming instanceof import_zod5.ZodObject) {
2877
3022
  const existingShape = existing.shape;
2878
3023
  const incomingShape = incoming.shape;
2879
3024
  for (const key of Object.keys(incomingShape)) {
@@ -3028,7 +3173,14 @@ function finalize(lease, handled, at, error, options, logger, failed_at) {
3028
3173
  };
3029
3174
  }
3030
3175
  function buildHandle(deps) {
3031
- const { logger, boundDo, boundLoad, boundQuery, boundQueryArray } = deps;
3176
+ const {
3177
+ logger,
3178
+ bound_do,
3179
+ bound_load,
3180
+ bound_query,
3181
+ bound_query_array,
3182
+ bound_forget
3183
+ } = deps;
3032
3184
  return async (lease, payloads) => {
3033
3185
  if (payloads.length === 0) return { lease, handled: 0, acked_at: lease.at };
3034
3186
  const stream = lease.stream;
@@ -3037,14 +3189,15 @@ function buildHandle(deps) {
3037
3189
  if (lease.retry > 0)
3038
3190
  logger.warn(`Retrying ${stream}@${at} (${lease.retry}).`);
3039
3191
  const scopedApp = {
3040
- do: boundDo,
3041
- load: boundLoad,
3042
- query: boundQuery,
3043
- query_array: boundQueryArray
3192
+ do: bound_do,
3193
+ load: bound_load,
3194
+ query: bound_query,
3195
+ query_array: bound_query_array,
3196
+ forget: bound_forget
3044
3197
  };
3045
3198
  for (const payload of payloads) {
3046
3199
  const { event, handler } = payload;
3047
- scopedApp.do = (action2, target, actionPayload, reactingTo, skipValidation) => boundDo(
3200
+ scopedApp.do = (action2, target, actionPayload, reactingTo, skipValidation) => bound_do(
3048
3201
  action2,
3049
3202
  target,
3050
3203
  actionPayload,
@@ -3073,7 +3226,9 @@ function buildHandle(deps) {
3073
3226
  function buildHandleBatch(logger) {
3074
3227
  return async (lease, payloads, batchHandler) => {
3075
3228
  const stream = lease.stream;
3076
- const events = payloads.map((p) => p.event);
3229
+ const events = payloads.map(
3230
+ (p) => p.event
3231
+ );
3077
3232
  const options = payloads[0].options;
3078
3233
  if (lease.retry > 0)
3079
3234
  logger.warn(`Retrying batch ${stream}@${events[0].id} (${lease.retry}).`);
@@ -3256,6 +3411,7 @@ var Act = class {
3256
3411
  _bound_load = this.load.bind(this);
3257
3412
  _bound_query = this.query.bind(this);
3258
3413
  _bound_query_array = this.query_array.bind(this);
3414
+ _bound_forget = this.forget.bind(this);
3259
3415
  /** Reaction dispatchers built once and handed to runDrainCycle each cycle. */
3260
3416
  _handle;
3261
3417
  _handle_batch;
@@ -3301,10 +3457,11 @@ var Act = class {
3301
3457
  this._cd = buildDrain(this._logger);
3302
3458
  this._handle = buildHandle({
3303
3459
  logger: this._logger,
3304
- boundDo: this._bound_do,
3305
- boundLoad: this._bound_load,
3306
- boundQuery: this._bound_query,
3307
- boundQueryArray: this._bound_query_array
3460
+ bound_do: this._bound_do,
3461
+ bound_load: this._bound_load,
3462
+ bound_query: this._bound_query,
3463
+ bound_query_array: this._bound_query_array,
3464
+ bound_forget: this._bound_forget
3308
3465
  });
3309
3466
  this._handle_batch = buildHandleBatch(this._logger);
3310
3467
  const {
@@ -3536,7 +3693,7 @@ var Act = class {
3536
3693
  return snapshots;
3537
3694
  });
3538
3695
  }
3539
- async load(stateOrName, stream, callback, asOf) {
3696
+ async load(stateOrName, stream, callback, asOf, actor) {
3540
3697
  return this._scoped(async () => {
3541
3698
  let merged;
3542
3699
  if (typeof stateOrName === "string") {
@@ -3546,7 +3703,7 @@ var Act = class {
3546
3703
  } else {
3547
3704
  merged = this._states.get(stateOrName.name) || stateOrName;
3548
3705
  }
3549
- return await this._es.load(merged, stream, callback, asOf);
3706
+ return await this._es.load(merged, stream, callback, asOf, actor);
3550
3707
  });
3551
3708
  }
3552
3709
  /**
@@ -3640,6 +3797,34 @@ var Act = class {
3640
3797
  return events;
3641
3798
  });
3642
3799
  }
3800
+ /**
3801
+ * Wipe the sensitive-data payload for every event on the stream — see
3802
+ * {@link IAct.forget}. Application-level half of #566.
3803
+ *
3804
+ * Throws on adapters without `Store.forget_pii`, invalidates the cache
3805
+ * entry for the stream, emits the `forgotten` lifecycle event with the
3806
+ * row count. Idempotent: a second call returns `{eventCount: 0}` and
3807
+ * does NOT re-emit.
3808
+ *
3809
+ * @param stream - Target stream.
3810
+ * @returns `{eventCount}` — number of events whose PII column was wiped.
3811
+ */
3812
+ async forget(stream) {
3813
+ return this._scoped(async () => {
3814
+ const s = store2();
3815
+ if (!s.forget_pii) {
3816
+ throw new Error(
3817
+ `Store does not implement forget_pii \u2014 adapter cannot comply with sensitive-data erasure. Use an adapter that declares pii_isolation: true (e.g. @rotorsoft/act on the in-memory store).`
3818
+ );
3819
+ }
3820
+ const eventCount = await s.forget_pii(stream);
3821
+ await cache2().invalidate(stream);
3822
+ if (eventCount > 0) {
3823
+ this.emit("forgotten", { stream, at: /* @__PURE__ */ new Date(), eventCount });
3824
+ }
3825
+ return { eventCount };
3826
+ });
3827
+ }
3643
3828
  /**
3644
3829
  * Processes pending reactions by draining uncommitted events from the event store.
3645
3830
  *
@@ -4240,9 +4425,13 @@ function validateLaneReferences(registry, lanes) {
4240
4425
  }
4241
4426
  function act() {
4242
4427
  const states = /* @__PURE__ */ new Map();
4428
+ const _sf = /* @__PURE__ */ new Map();
4429
+ const _dp = /* @__PURE__ */ new Map();
4243
4430
  const registry = {
4244
4431
  actions: {},
4245
- events: {}
4432
+ events: {},
4433
+ sensitive_fields: (eventName) => _sf.get(eventName) ?? [],
4434
+ disclosure_predicate: (stateName) => _dp.get(stateName) ?? null
4246
4435
  };
4247
4436
  const pendingProjections = [];
4248
4437
  const batchHandlers = /* @__PURE__ */ new Map();
@@ -4353,6 +4542,58 @@ function act() {
4353
4542
  }
4354
4543
  finalizeDeprecations();
4355
4544
  validateLaneReferences(registry, lanes);
4545
+ for (const [eventName, reg] of Object.entries(
4546
+ registry.events
4547
+ )) {
4548
+ const fields = pii_fields(reg.schema);
4549
+ if (fields.length === 0) continue;
4550
+ _sf.set(eventName, fields);
4551
+ for (const [name, reaction] of reg.reactions) {
4552
+ const inner = reaction.handler;
4553
+ const wrapped = (event, stream, app) => inner(pii_strip(event, fields), stream, app);
4554
+ Object.defineProperty(wrapped, "name", { value: inner.name });
4555
+ reaction.handler = wrapped;
4556
+ reg.reactions.set(name, reaction);
4557
+ }
4558
+ }
4559
+ for (const state2 of states.values()) {
4560
+ if (state2.disclose) _dp.set(state2.name, state2.disclose);
4561
+ const fields_by_event = /* @__PURE__ */ new Map();
4562
+ for (const eventName of Object.keys(state2.events)) {
4563
+ const fields = _sf.get(eventName);
4564
+ if (fields) fields_by_event.set(eventName, fields);
4565
+ }
4566
+ if (fields_by_event.size === 0) continue;
4567
+ if (state2.snap) {
4568
+ const offending = [...fields_by_event.keys()];
4569
+ throw new Error(
4570
+ `State "${state2.name}" cannot snapshot \u2014 events {${offending.join(", ")}} carry sensitive fields. Snapshots write derived state into __snapshot__.data, which forget_pii cannot reach. Remove .snap() or remove sensitive(...) markers.`
4571
+ );
4572
+ }
4573
+ const disclose = state2.disclose ?? null;
4574
+ state2.view = (event, actor) => {
4575
+ const fields = fields_by_event.get(event.name);
4576
+ return fields ? pii_gate(event, fields, disclose, actor) : event;
4577
+ };
4578
+ state2.message = (validated) => {
4579
+ const fields = fields_by_event.get(validated.name);
4580
+ return fields ? pii_split(validated, fields) : validated;
4581
+ };
4582
+ for (const [eventName, fields] of fields_by_event) {
4583
+ const original = state2.patch[eventName];
4584
+ state2.patch[eventName] = (event, s) => original(pii_merge(event, fields), s);
4585
+ }
4586
+ }
4587
+ for (const [target, original] of batchHandlers) {
4588
+ const wrapped = async (events, stream) => {
4589
+ const stripped = events.map((e) => {
4590
+ const f = _sf.get(e.name);
4591
+ return f ? pii_strip(e, f) : e;
4592
+ });
4593
+ return original(stripped, stream);
4594
+ };
4595
+ batchHandlers.set(target, wrapped);
4596
+ }
4356
4597
  _built = true;
4357
4598
  }
4358
4599
  return new Act(
@@ -4520,7 +4761,12 @@ function state(entry) {
4520
4761
  name,
4521
4762
  init,
4522
4763
  patch: defaultPatch,
4523
- on: {}
4764
+ on: {},
4765
+ // Step delegates initialized as identity. `act().build()`
4766
+ // overrides on states with `sensitive(...)` events to bake in
4767
+ // the gate / split.
4768
+ view: (event) => event,
4769
+ message: (validated) => validated
4524
4770
  };
4525
4771
  const builder = action_builder(internal);
4526
4772
  return Object.assign(builder, {
@@ -4572,6 +4818,10 @@ function action_builder(state2) {
4572
4818
  internal.snap = snap2;
4573
4819
  return builder;
4574
4820
  },
4821
+ discloses(disclose) {
4822
+ internal.disclose = disclose;
4823
+ return builder;
4824
+ },
4575
4825
  build() {
4576
4826
  return internal;
4577
4827
  }
@@ -4762,6 +5012,8 @@ function writeLine(writer, line) {
4762
5012
  NonRetryableError,
4763
5013
  PackageSchema,
4764
5014
  QuerySchema,
5015
+ REDACTED,
5016
+ SHREDDED,
4765
5017
  SNAP_EVENT,
4766
5018
  StreamClosedError,
4767
5019
  TOMBSTONE_EVENT,
@@ -4778,6 +5030,7 @@ function writeLine(writer, line) {
4778
5030
  port,
4779
5031
  projection,
4780
5032
  scoped,
5033
+ sensitive,
4781
5034
  sleep,
4782
5035
  slice,
4783
5036
  state,