@rotorsoft/act 1.31.29 → 1.32.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.cjs CHANGED
@@ -524,6 +524,7 @@ var QuerySchema = import_zod2.z.object({
524
524
  backward: import_zod2.z.boolean().optional(),
525
525
  correlation: import_zod2.z.string().optional(),
526
526
  with_snaps: import_zod2.z.boolean().optional(),
527
+ with_pii: import_zod2.z.boolean().optional(),
527
528
  stream_exact: import_zod2.z.boolean().optional()
528
529
  }).readonly();
529
530
 
@@ -881,7 +882,8 @@ var InMemoryStore = class {
881
882
  }
882
883
  // Attach the isolated PII payload (or null) to an event before handing it to
883
884
  // a caller. Allocation-free for events without PII — by far the common case.
884
- _with_pii(e) {
885
+ _with_pii(e, wanted = true) {
886
+ if (!wanted) return e;
885
887
  const pii = this._pii.get(e.stream)?.get(e.id);
886
888
  return pii ? { ...e, pii } : e;
887
889
  }
@@ -952,7 +954,12 @@ var InMemoryStore = class {
952
954
  if (floor_id >= 0 && e.id < floor_id) break;
953
955
  if (query.created_after && e.created <= query.created_after) continue;
954
956
  await Promise.resolve(
955
- callback(this._with_pii(e))
957
+ callback(
958
+ this._with_pii(
959
+ e,
960
+ query?.with_pii !== false
961
+ )
962
+ )
956
963
  );
957
964
  count++;
958
965
  if (query?.limit && count >= query.limit) break;
@@ -967,7 +974,12 @@ var InMemoryStore = class {
967
974
  if (query?.created_before && e.created >= query.created_before)
968
975
  continue;
969
976
  await Promise.resolve(
970
- callback(this._with_pii(e))
977
+ callback(
978
+ this._with_pii(
979
+ e,
980
+ query?.with_pii !== false
981
+ )
982
+ )
971
983
  );
972
984
  count++;
973
985
  if (query?.limit && count >= query.limit) break;
@@ -1858,7 +1870,8 @@ var ALL_CATEGORIES = [
1858
1870
  "snapshot-drift",
1859
1871
  "routing-health",
1860
1872
  "correlation-gaps",
1861
- "clock-anomalies"
1873
+ "clock-anomalies",
1874
+ "unreadable-events"
1862
1875
  ];
1863
1876
  async function* audit(deps, categories, options = {}) {
1864
1877
  const requested = new Set(categories ?? [...ALL_CATEGORIES]);
@@ -1868,7 +1881,9 @@ async function* audit(deps, categories, options = {}) {
1868
1881
  );
1869
1882
  const need_stats = passes.some((p) => p.on_stat !== void 0);
1870
1883
  const need_streams = passes.some((p) => p.on_stream !== void 0);
1871
- const need_events = passes.some((p) => p.on_event !== void 0);
1884
+ const need_events = passes.some(
1885
+ (p) => p.on_event !== void 0 || p.on_unreadable !== void 0
1886
+ );
1872
1887
  if (need_stats) {
1873
1888
  const stats = await deps.store().query_stats(
1874
1889
  {},
@@ -1884,15 +1899,57 @@ async function* audit(deps, categories, options = {}) {
1884
1899
  });
1885
1900
  }
1886
1901
  if (need_events) {
1887
- await deps.store().query((event) => {
1888
- for (const p of passes) p.on_event?.(event);
1889
- }, options.query);
1902
+ await scan_events_resiliently(
1903
+ deps,
1904
+ options.query,
1905
+ (event) => {
1906
+ for (const p of passes) p.on_event?.(event);
1907
+ },
1908
+ (row) => {
1909
+ for (const p of passes) p.on_unreadable?.(row);
1910
+ }
1911
+ );
1890
1912
  }
1891
1913
  for (const p of passes) await p.finalize?.(deps);
1892
1914
  for (const p of passes) {
1893
1915
  for (const f of p.drain()) yield f;
1894
1916
  }
1895
1917
  }
1918
+ async function scan_events_resiliently(deps, query, on_event, on_unreadable) {
1919
+ let cursor = query?.after;
1920
+ let remaining = query?.limit;
1921
+ for (; ; ) {
1922
+ let seen = 0;
1923
+ try {
1924
+ await deps.store().query(
1925
+ (event) => {
1926
+ seen++;
1927
+ cursor = event.id;
1928
+ on_event(event);
1929
+ },
1930
+ {
1931
+ ...query,
1932
+ after: cursor,
1933
+ ...remaining !== void 0 && { limit: remaining }
1934
+ }
1935
+ );
1936
+ return;
1937
+ } catch (error) {
1938
+ if (query?.backward) throw error;
1939
+ let row;
1940
+ await deps.store().query(
1941
+ (e) => {
1942
+ row = { stream: e.stream, event_id: e.id, error };
1943
+ },
1944
+ { ...query, after: cursor, limit: 1, with_pii: false }
1945
+ );
1946
+ if (!row) throw error;
1947
+ on_unreadable(row);
1948
+ if (remaining !== void 0) remaining -= seen;
1949
+ cursor = row.event_id;
1950
+ }
1951
+ }
1952
+ }
1896
1953
  var make_schema_pass = (deps) => {
1897
1954
  const findings = [];
1898
1955
  return {
@@ -2257,6 +2314,22 @@ function restart_is_supported(deps, head_event_name) {
2257
2314
  const state2 = deps.event_to_state.get(head_event_name);
2258
2315
  return state2?.snap !== void 0;
2259
2316
  }
2317
+ function make_unreadable_events_pass() {
2318
+ const findings = [];
2319
+ return {
2320
+ category: "unreadable-events",
2321
+ on_unreadable({ stream, event_id, error }) {
2322
+ findings.push({
2323
+ category: "unreadable-events",
2324
+ stream,
2325
+ event_id,
2326
+ reason: "pii_decrypt_failed",
2327
+ error
2328
+ });
2329
+ },
2330
+ drain: () => findings
2331
+ };
2332
+ }
2260
2333
  var PASS_FACTORIES = {
2261
2334
  schema: make_schema_pass,
2262
2335
  "deprecated-load": make_deprecated_load_pass,
@@ -2266,7 +2339,8 @@ var PASS_FACTORIES = {
2266
2339
  "snapshot-drift": make_snapshot_drift_pass,
2267
2340
  "routing-health": make_routing_health_pass,
2268
2341
  "correlation-gaps": make_correlation_gaps_pass,
2269
- "clock-anomalies": make_clock_anomalies_pass
2342
+ "clock-anomalies": make_clock_anomalies_pass,
2343
+ "unreadable-events": make_unreadable_events_pass
2270
2344
  };
2271
2345
 
2272
2346
  // src/internal/autoclose-policy.ts
@@ -3397,7 +3471,19 @@ var CorrelateCycle = class {
3397
3471
  }
3398
3472
  }
3399
3473
  },
3400
- { ...query, after }
3474
+ // Decline the sensitive payload: this scan reads `name`/`stream`/`id`
3475
+ // to resolve reaction targets and throws the rest away. Asking for a
3476
+ // payload it discards made one unreadable row stop EVERY stream's
3477
+ // reactions — correlate is the sole producer of the work mark, so a
3478
+ // decrypt failure here strands the whole app with nothing blocked and
3479
+ // nothing naming the row (#1675).
3480
+ //
3481
+ // A dynamic `.to(event => …)` resolver therefore sees `pii: null`.
3482
+ // That is deliberate and is itself a fix: correlate runs actor-less,
3483
+ // so handing a resolver decrypted plaintext is un-gated disclosure —
3484
+ // the defect #1673 corrected on the drain return and #1277 before it.
3485
+ // `data` already carries no sensitive keys; nothing merges them here.
3486
+ { ...query, after, with_pii: false }
3401
3487
  );
3402
3488
  const streams = [];
3403
3489
  for (const [stream, entry] of correlated) {
@@ -3739,12 +3825,23 @@ async function fetch(leased, eventLimit) {
3739
3825
  leased.map(async ({ stream, source, at, lagging }) => {
3740
3826
  const events = [];
3741
3827
  const stream_exact = source !== void 0 && is_literal_source(source) ? true : void 0;
3742
- await store2().query((e) => events.push(e), {
3743
- stream: source,
3744
- stream_exact,
3745
- after: at,
3746
- limit: eventLimit
3747
- });
3828
+ try {
3829
+ await store2().query((e) => events.push(e), {
3830
+ stream: source,
3831
+ stream_exact,
3832
+ after: at,
3833
+ limit: eventLimit
3834
+ });
3835
+ } catch (error) {
3836
+ return {
3837
+ stream,
3838
+ source,
3839
+ at,
3840
+ lagging,
3841
+ events: [],
3842
+ error: error?.message ?? String(error)
3843
+ };
3844
+ }
3748
3845
  return { stream, source, at, lagging, events };
3749
3846
  })
3750
3847
  );
@@ -4399,6 +4496,17 @@ async function run_drain_cycle(ops, registry, batch_handlers, misrouted, store_f
4399
4496
  const handled = await Promise.all(
4400
4497
  leased.map((lease) => {
4401
4498
  const entry = fetch_map.get(lease.stream);
4499
+ if (entry.fetch.error !== void 0) {
4500
+ const error = `Fetch failed for ${lease.stream}: ${entry.fetch.error}`;
4501
+ log().error(error);
4502
+ const failed = {
4503
+ lease,
4504
+ handled: 0,
4505
+ acked_at: lease.at,
4506
+ error
4507
+ };
4508
+ return Promise.resolve(failed);
4509
+ }
4402
4510
  const at = entry.fetch.events.at(-1)?.id || fetch_window_at;
4403
4511
  const { payloads } = entry;
4404
4512
  const exhausted = budget_exhausted(