@rotorsoft/act 1.31.29 → 1.32.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -26,7 +26,7 @@ import {
26
26
  sleep,
27
27
  store,
28
28
  validate
29
- } from "./chunk-34WL4DPW.js";
29
+ } from "./chunk-5NFBMYTY.js";
30
30
  import {
31
31
  ActorSchema,
32
32
  CausationEventSchema,
@@ -53,7 +53,7 @@ import {
53
53
  pii_split,
54
54
  pii_strip,
55
55
  sensitive
56
- } from "./chunk-7IBF4FTQ.js";
56
+ } from "./chunk-BI3UNNVA.js";
57
57
 
58
58
  // src/signals.ts
59
59
  process.once("SIGINT", async (arg) => {
@@ -213,7 +213,8 @@ var ALL_CATEGORIES = [
213
213
  "snapshot-drift",
214
214
  "routing-health",
215
215
  "correlation-gaps",
216
- "clock-anomalies"
216
+ "clock-anomalies",
217
+ "unreadable-events"
217
218
  ];
218
219
  async function* audit(deps, categories, options = {}) {
219
220
  const requested = new Set(categories ?? [...ALL_CATEGORIES]);
@@ -223,7 +224,9 @@ async function* audit(deps, categories, options = {}) {
223
224
  );
224
225
  const need_stats = passes.some((p) => p.on_stat !== void 0);
225
226
  const need_streams = passes.some((p) => p.on_stream !== void 0);
226
- const need_events = passes.some((p) => p.on_event !== void 0);
227
+ const need_events = passes.some(
228
+ (p) => p.on_event !== void 0 || p.on_unreadable !== void 0
229
+ );
227
230
  if (need_stats) {
228
231
  const stats = await deps.store().query_stats(
229
232
  {},
@@ -239,15 +242,57 @@ async function* audit(deps, categories, options = {}) {
239
242
  });
240
243
  }
241
244
  if (need_events) {
242
- await deps.store().query((event) => {
243
- for (const p of passes) p.on_event?.(event);
244
- }, options.query);
245
+ await scan_events_resiliently(
246
+ deps,
247
+ options.query,
248
+ (event) => {
249
+ for (const p of passes) p.on_event?.(event);
250
+ },
251
+ (row) => {
252
+ for (const p of passes) p.on_unreadable?.(row);
253
+ }
254
+ );
245
255
  }
246
256
  for (const p of passes) await p.finalize?.(deps);
247
257
  for (const p of passes) {
248
258
  for (const f of p.drain()) yield f;
249
259
  }
250
260
  }
261
+ async function scan_events_resiliently(deps, query, on_event, on_unreadable) {
262
+ let cursor = query?.after;
263
+ let remaining = query?.limit;
264
+ for (; ; ) {
265
+ let seen = 0;
266
+ try {
267
+ await deps.store().query(
268
+ (event) => {
269
+ seen++;
270
+ cursor = event.id;
271
+ on_event(event);
272
+ },
273
+ {
274
+ ...query,
275
+ after: cursor,
276
+ ...remaining !== void 0 && { limit: remaining }
277
+ }
278
+ );
279
+ return;
280
+ } catch (error) {
281
+ if (query?.backward) throw error;
282
+ let row;
283
+ await deps.store().query(
284
+ (e) => {
285
+ row = { stream: e.stream, event_id: e.id, error };
286
+ },
287
+ { ...query, after: cursor, limit: 1, with_pii: false }
288
+ );
289
+ if (!row) throw error;
290
+ on_unreadable(row);
291
+ if (remaining !== void 0) remaining -= seen;
292
+ cursor = row.event_id;
293
+ }
294
+ }
295
+ }
251
296
  var make_schema_pass = (deps) => {
252
297
  const findings = [];
253
298
  return {
@@ -612,6 +657,22 @@ function restart_is_supported(deps, head_event_name) {
612
657
  const state2 = deps.event_to_state.get(head_event_name);
613
658
  return state2?.snap !== void 0;
614
659
  }
660
+ function make_unreadable_events_pass() {
661
+ const findings = [];
662
+ return {
663
+ category: "unreadable-events",
664
+ on_unreadable({ stream, event_id, error }) {
665
+ findings.push({
666
+ category: "unreadable-events",
667
+ stream,
668
+ event_id,
669
+ reason: "pii_decrypt_failed",
670
+ error
671
+ });
672
+ },
673
+ drain: () => findings
674
+ };
675
+ }
615
676
  var PASS_FACTORIES = {
616
677
  schema: make_schema_pass,
617
678
  "deprecated-load": make_deprecated_load_pass,
@@ -621,7 +682,8 @@ var PASS_FACTORIES = {
621
682
  "snapshot-drift": make_snapshot_drift_pass,
622
683
  "routing-health": make_routing_health_pass,
623
684
  "correlation-gaps": make_correlation_gaps_pass,
624
- "clock-anomalies": make_clock_anomalies_pass
685
+ "clock-anomalies": make_clock_anomalies_pass,
686
+ "unreadable-events": make_unreadable_events_pass
625
687
  };
626
688
 
627
689
  // src/internal/autoclose-policy.ts
@@ -1752,7 +1814,19 @@ var CorrelateCycle = class {
1752
1814
  }
1753
1815
  }
1754
1816
  },
1755
- { ...query, after }
1817
+ // Decline the sensitive payload: this scan reads `name`/`stream`/`id`
1818
+ // to resolve reaction targets and throws the rest away. Asking for a
1819
+ // payload it discards made one unreadable row stop EVERY stream's
1820
+ // reactions — correlate is the sole producer of the work mark, so a
1821
+ // decrypt failure here strands the whole app with nothing blocked and
1822
+ // nothing naming the row (#1675).
1823
+ //
1824
+ // A dynamic `.to(event => …)` resolver therefore sees `pii: null`.
1825
+ // That is deliberate and is itself a fix: correlate runs actor-less,
1826
+ // so handing a resolver decrypted plaintext is un-gated disclosure —
1827
+ // the defect #1673 corrected on the drain return and #1277 before it.
1828
+ // `data` already carries no sensitive keys; nothing merges them here.
1829
+ { ...query, after, with_pii: false }
1756
1830
  );
1757
1831
  const streams = [];
1758
1832
  for (const [stream, entry] of correlated) {
@@ -2094,12 +2168,23 @@ async function fetch(leased, eventLimit) {
2094
2168
  leased.map(async ({ stream, source, at, lagging }) => {
2095
2169
  const events = [];
2096
2170
  const stream_exact = source !== void 0 && is_literal_source(source) ? true : void 0;
2097
- await store().query((e) => events.push(e), {
2098
- stream: source,
2099
- stream_exact,
2100
- after: at,
2101
- limit: eventLimit
2102
- });
2171
+ try {
2172
+ await store().query((e) => events.push(e), {
2173
+ stream: source,
2174
+ stream_exact,
2175
+ after: at,
2176
+ limit: eventLimit
2177
+ });
2178
+ } catch (error) {
2179
+ return {
2180
+ stream,
2181
+ source,
2182
+ at,
2183
+ lagging,
2184
+ events: [],
2185
+ error: error?.message ?? String(error)
2186
+ };
2187
+ }
2103
2188
  return { stream, source, at, lagging, events };
2104
2189
  })
2105
2190
  );
@@ -2754,6 +2839,17 @@ async function run_drain_cycle(ops, registry, batch_handlers, misrouted, store_f
2754
2839
  const handled = await Promise.all(
2755
2840
  leased.map((lease) => {
2756
2841
  const entry = fetch_map.get(lease.stream);
2842
+ if (entry.fetch.error !== void 0) {
2843
+ const error = `Fetch failed for ${lease.stream}: ${entry.fetch.error}`;
2844
+ log().error(error);
2845
+ const failed = {
2846
+ lease,
2847
+ handled: 0,
2848
+ acked_at: lease.at,
2849
+ error
2850
+ };
2851
+ return Promise.resolve(failed);
2852
+ }
2757
2853
  const at = entry.fetch.events.at(-1)?.id || fetch_window_at;
2758
2854
  const { payloads } = entry;
2759
2855
  const exhausted = budget_exhausted(
@@ -5539,7 +5635,7 @@ function slice() {
5539
5635
  return builder;
5540
5636
  },
5541
5637
  withLane: (config2) => {
5542
- register_lane(config2, lanes);
5638
+ register_lane(resolveLaneConfig(config2), lanes);
5543
5639
  return builder;
5544
5640
  },
5545
5641
  on: (event) => reaction_on(event, events, builder),