@loomwork/event-runtime 0.0.0-stage → 0.1.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.
Files changed (153) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +8 -0
  3. package/README.md +95 -2
  4. package/dist/BranchRegistry.d.ts +140 -0
  5. package/dist/BranchRegistry.d.ts.map +1 -0
  6. package/dist/BranchRegistry.js +66 -0
  7. package/dist/BranchRegistry.js.map +1 -0
  8. package/dist/CoalescingEventStore.d.ts +97 -0
  9. package/dist/CoalescingEventStore.d.ts.map +1 -0
  10. package/dist/CoalescingEventStore.js +180 -0
  11. package/dist/CoalescingEventStore.js.map +1 -0
  12. package/dist/InMemoryArchiveStore.d.ts +19 -0
  13. package/dist/InMemoryArchiveStore.d.ts.map +1 -0
  14. package/dist/InMemoryArchiveStore.js +49 -0
  15. package/dist/InMemoryArchiveStore.js.map +1 -0
  16. package/dist/InMemoryAsyncEventStore.d.ts +111 -0
  17. package/dist/InMemoryAsyncEventStore.d.ts.map +1 -0
  18. package/dist/InMemoryAsyncEventStore.js +374 -0
  19. package/dist/InMemoryAsyncEventStore.js.map +1 -0
  20. package/dist/InMemoryBranchRegistry.d.ts +31 -0
  21. package/dist/InMemoryBranchRegistry.d.ts.map +1 -0
  22. package/dist/InMemoryBranchRegistry.js +167 -0
  23. package/dist/InMemoryBranchRegistry.js.map +1 -0
  24. package/dist/ProfilingEventStore.d.ts +75 -0
  25. package/dist/ProfilingEventStore.d.ts.map +1 -0
  26. package/dist/ProfilingEventStore.js +133 -0
  27. package/dist/ProfilingEventStore.js.map +1 -0
  28. package/dist/ShardPlan.d.ts +87 -0
  29. package/dist/ShardPlan.d.ts.map +1 -0
  30. package/dist/ShardPlan.js +193 -0
  31. package/dist/ShardPlan.js.map +1 -0
  32. package/dist/ShardedEventStore.d.ts +149 -0
  33. package/dist/ShardedEventStore.d.ts.map +1 -0
  34. package/dist/ShardedEventStore.js +468 -0
  35. package/dist/ShardedEventStore.js.map +1 -0
  36. package/dist/SubscriberRegistry.d.ts +59 -0
  37. package/dist/SubscriberRegistry.d.ts.map +1 -0
  38. package/dist/SubscriberRegistry.js +113 -0
  39. package/dist/SubscriberRegistry.js.map +1 -0
  40. package/dist/TenantScopedArchiveStore.d.ts +36 -0
  41. package/dist/TenantScopedArchiveStore.d.ts.map +1 -0
  42. package/dist/TenantScopedArchiveStore.js +96 -0
  43. package/dist/TenantScopedArchiveStore.js.map +1 -0
  44. package/dist/TenantScopedEventStore.d.ts +59 -0
  45. package/dist/TenantScopedEventStore.d.ts.map +1 -0
  46. package/dist/TenantScopedEventStore.js +227 -0
  47. package/dist/TenantScopedEventStore.js.map +1 -0
  48. package/dist/archive.d.ts +68 -0
  49. package/dist/archive.d.ts.map +1 -0
  50. package/dist/archive.js +207 -0
  51. package/dist/archive.js.map +1 -0
  52. package/dist/archiveSpi.d.ts +165 -0
  53. package/dist/archiveSpi.d.ts.map +1 -0
  54. package/dist/archiveSpi.js +184 -0
  55. package/dist/archiveSpi.js.map +1 -0
  56. package/dist/creationMarker.d.ts +13 -0
  57. package/dist/creationMarker.d.ts.map +1 -0
  58. package/dist/creationMarker.js +17 -0
  59. package/dist/creationMarker.js.map +1 -0
  60. package/dist/decoratorPlan.typecheck.d.ts +2 -0
  61. package/dist/decoratorPlan.typecheck.d.ts.map +1 -0
  62. package/dist/decoratorPlan.typecheck.js +41 -0
  63. package/dist/decoratorPlan.typecheck.js.map +1 -0
  64. package/dist/eventKindClass.d.ts +40 -0
  65. package/dist/eventKindClass.d.ts.map +1 -0
  66. package/dist/eventKindClass.js +99 -0
  67. package/dist/eventKindClass.js.map +1 -0
  68. package/dist/executionIdOrder.d.ts +21 -0
  69. package/dist/executionIdOrder.d.ts.map +1 -0
  70. package/dist/executionIdOrder.js +43 -0
  71. package/dist/executionIdOrder.js.map +1 -0
  72. package/dist/fencing.d.ts +21 -0
  73. package/dist/fencing.d.ts.map +1 -0
  74. package/dist/fencing.js +36 -0
  75. package/dist/fencing.js.map +1 -0
  76. package/dist/index.d.ts +39 -0
  77. package/dist/index.d.ts.map +1 -0
  78. package/dist/index.js +126 -0
  79. package/dist/index.js.map +1 -0
  80. package/dist/installOptional.d.ts +52 -0
  81. package/dist/installOptional.d.ts.map +1 -0
  82. package/dist/installOptional.js +112 -0
  83. package/dist/installOptional.js.map +1 -0
  84. package/dist/keyUnavailable.d.ts +67 -0
  85. package/dist/keyUnavailable.d.ts.map +1 -0
  86. package/dist/keyUnavailable.js +87 -0
  87. package/dist/keyUnavailable.js.map +1 -0
  88. package/dist/leases.d.ts +177 -0
  89. package/dist/leases.d.ts.map +1 -0
  90. package/dist/leases.js +142 -0
  91. package/dist/leases.js.map +1 -0
  92. package/dist/payloadShredded.d.ts +30 -0
  93. package/dist/payloadShredded.d.ts.map +1 -0
  94. package/dist/payloadShredded.js +67 -0
  95. package/dist/payloadShredded.js.map +1 -0
  96. package/dist/scaleDecorators.bench.d.ts +2 -0
  97. package/dist/scaleDecorators.bench.d.ts.map +1 -0
  98. package/dist/scaleDecorators.bench.js +93 -0
  99. package/dist/scaleDecorators.bench.js.map +1 -0
  100. package/dist/sequenceRange.bench.d.ts +2 -0
  101. package/dist/sequenceRange.bench.d.ts.map +1 -0
  102. package/dist/sequenceRange.bench.js +46 -0
  103. package/dist/sequenceRange.bench.js.map +1 -0
  104. package/dist/sequenceRange.d.ts +10 -0
  105. package/dist/sequenceRange.d.ts.map +1 -0
  106. package/dist/sequenceRange.js +35 -0
  107. package/dist/sequenceRange.js.map +1 -0
  108. package/dist/timers.d.ts +87 -0
  109. package/dist/timers.d.ts.map +1 -0
  110. package/dist/timers.js +68 -0
  111. package/dist/timers.js.map +1 -0
  112. package/dist/types.d.ts +634 -0
  113. package/dist/types.d.ts.map +1 -0
  114. package/dist/types.js +429 -0
  115. package/dist/types.js.map +1 -0
  116. package/dist/writeConflicts.d.ts +46 -0
  117. package/dist/writeConflicts.d.ts.map +1 -0
  118. package/dist/writeConflicts.js +63 -0
  119. package/dist/writeConflicts.js.map +1 -0
  120. package/memory.d.ts +4 -0
  121. package/memory.js +13 -0
  122. package/package.json +67 -3
  123. package/sequence.d.ts +1 -0
  124. package/sequence.js +5 -0
  125. package/src/BranchRegistry.ts +161 -0
  126. package/src/CoalescingEventStore.ts +214 -0
  127. package/src/InMemoryArchiveStore.ts +69 -0
  128. package/src/InMemoryAsyncEventStore.ts +425 -0
  129. package/src/InMemoryBranchRegistry.ts +178 -0
  130. package/src/ProfilingEventStore.ts +153 -0
  131. package/src/ShardPlan.ts +235 -0
  132. package/src/ShardedEventStore.ts +557 -0
  133. package/src/SubscriberRegistry.ts +115 -0
  134. package/src/TenantScopedArchiveStore.ts +114 -0
  135. package/src/TenantScopedEventStore.ts +266 -0
  136. package/src/archive.ts +248 -0
  137. package/src/archiveSpi.ts +308 -0
  138. package/src/creationMarker.ts +15 -0
  139. package/src/decoratorPlan.typecheck.ts +52 -0
  140. package/src/eventKindClass.ts +127 -0
  141. package/src/executionIdOrder.ts +38 -0
  142. package/src/fencing.ts +31 -0
  143. package/src/index.ts +151 -0
  144. package/src/installOptional.ts +136 -0
  145. package/src/keyUnavailable.ts +98 -0
  146. package/src/leases.ts +319 -0
  147. package/src/payloadShredded.ts +63 -0
  148. package/src/scaleDecorators.bench.ts +107 -0
  149. package/src/sequenceRange.bench.ts +51 -0
  150. package/src/sequenceRange.ts +35 -0
  151. package/src/timers.ts +150 -0
  152. package/src/types.ts +857 -0
  153. package/src/writeConflicts.ts +56 -0
package/src/index.ts ADDED
@@ -0,0 +1,151 @@
1
+ export { TenantScopedEventStore } from "./TenantScopedEventStore";
2
+ export { CoalescingEventStore } from "./CoalescingEventStore";
3
+ export type { InboxStreams } from "./types";
4
+ export type { CoalescingEventStoreOptions } from "./CoalescingEventStore";
5
+ export type { DestructionProof } from "./keyUnavailable";
6
+ export {
7
+ KEY_UNAVAILABLE_CODE,
8
+ KeyUnavailableError,
9
+ erasureWindowView,
10
+ isErasureWindow,
11
+ isKeyUnavailable,
12
+ recordsErasureIntent,
13
+ } from "./keyUnavailable";
14
+ export {
15
+ PAYLOAD_SHREDDED_BRAND,
16
+ brandPayloadShredded,
17
+ isPayloadShredded,
18
+ } from "./payloadShredded";
19
+ export { ShardSealingMismatchError, ShardedEventStore } from "./ShardedEventStore";
20
+ export type { MigrationResult, ShardedEventStoreOptions } from "./ShardedEventStore";
21
+ export { ShardPlan, DEFAULT_BUCKETS } from "./ShardPlan";
22
+ export type { ShardPlacement, ShardPlanJSON } from "./ShardPlan";
23
+ export { SubscriberRegistry, bucketOf } from "./SubscriberRegistry";
24
+ export { ProfilingEventStore } from "./ProfilingEventStore";
25
+ export type { WriteProfile } from "./ProfilingEventStore";
26
+ export { InMemoryAsyncEventStore } from "./InMemoryAsyncEventStore";
27
+ export { compareExecutionIds } from "./executionIdOrder";
28
+ export type { InMemoryAsyncEventStoreOptions } from "./InMemoryAsyncEventStore";
29
+ export type {
30
+ AcquireFenceIfResult,
31
+ AppendIfUnchangedResult,
32
+ TipObservation,
33
+ EventStore,
34
+ EventStoreOptions,
35
+ ExecutionEnumerable,
36
+ ExecutionPage,
37
+ OptionalEventStoreMethod,
38
+ SealMarker,
39
+ SealingCapability,
40
+ AtRestEventReader,
41
+ } from "./types";
42
+ export {
43
+ decoratedInnerOf,
44
+ forwardStoreMarkers,
45
+ installOptional,
46
+ refusalsOf,
47
+ } from "./installOptional";
48
+ export type { DecoratorPlan } from "./installOptional";
49
+ export {
50
+ OPTIONAL_EVENT_STORE_METHODS,
51
+ headOf,
52
+ readRangeOf,
53
+ appendBatchOf,
54
+ INBOX_STREAM_PREFIX,
55
+ INBOX_TOPIC_SEQ_BASE,
56
+ INBOX_REGISTRY_SUFFIX,
57
+ INBOX_LAYOUT_STREAM,
58
+ QUARANTINE_STREAM_PREFIX,
59
+ quarantineStreamId,
60
+ chainIdOfFirst,
61
+ hasInboxStreamShape,
62
+ inboxChainOf,
63
+ RESERVED_LATE_PREFIX,
64
+ archiveChainOf,
65
+ reservedLateKey,
66
+ reservedLateOrigin,
67
+ inboxRegistryStreamId,
68
+ inboxStreamsOfChain,
69
+ inboxStreamsOfChainDeep,
70
+ inboxTopicHash,
71
+ inboxTopicStreamId,
72
+ isReservedStream,
73
+ iterateExecutionIds,
74
+ RESERVED_STREAM_PREFIX,
75
+ EVENT_STORE_SPI_VERSION,
76
+ } from "./types";
77
+ export { StaleFenceError, fenceOf } from "./fencing";
78
+ export { creationMarker } from "./creationMarker";
79
+ export {
80
+ EVENT_KIND_CLASS,
81
+ isAnsweringKind,
82
+ isQuiescent,
83
+ type EventKindClass,
84
+ } from "./eventKindClass";
85
+ export {
86
+ InMemoryLeaseStore,
87
+ LEASE_STORE_SPI_VERSION,
88
+ assertNoBatchSteal,
89
+ assertSteal,
90
+ assertTtl,
91
+ type AcquireOptions,
92
+ type InMemoryLeaseStoreOptions,
93
+ type Lease,
94
+ type LeaseStore,
95
+ type StealProof,
96
+ } from "./leases";
97
+ export { DuplicateEnvelopeIdError, DuplicateSequenceError } from "./writeConflicts";
98
+ export { lowerBoundBySequence, sliceBySequence } from "./sequenceRange";
99
+ export { InMemoryBranchRegistry } from "./InMemoryBranchRegistry";
100
+ export type { InMemoryBranchRegistryOptions } from "./InMemoryBranchRegistry";
101
+ export { InMemoryTimerStore } from "./timers";
102
+ export type { TimerRecord, TimerStore } from "./timers";
103
+ export type {
104
+ BranchRef,
105
+ BranchRegistry,
106
+ LineageRecord,
107
+ ListRefsOptions,
108
+ PromoteOptions,
109
+ } from "./BranchRegistry";
110
+ export {
111
+ BRANCH_REGISTRY_SPI_VERSION,
112
+ BranchPromotionConflictError,
113
+ assertListRefsLimit,
114
+ } from "./BranchRegistry";
115
+ export {
116
+ ARCHIVE_STORE_SPI_VERSION,
117
+ ArchiveConflictError,
118
+ ArchiveDigestError,
119
+ ArchiveIncarnationError,
120
+ ArchiveUnserializableError,
121
+ ARCHIVE_LEGACY_CHECKPOINT_EVENT,
122
+ OPTIONAL_ARCHIVE_STORE_METHODS,
123
+ assertReplaceable,
124
+ classifyAppend,
125
+ headerOf,
126
+ } from "./archiveSpi";
127
+ export type {
128
+ ArchiveExecutionPage,
129
+ ArchiveSegment,
130
+ ArchiveSegmentHeader,
131
+ ArchiveSegmentKind,
132
+ ArchiveStore,
133
+ } from "./archiveSpi";
134
+ export {
135
+ SENDER_ERASED,
136
+ archivedReservedStreams,
137
+ assertReservedSegmentDigest,
138
+ assertSegmentDigest,
139
+ buildReservedSegment,
140
+ buildSegment,
141
+ isSenderErased,
142
+ maskSender,
143
+ pageIds,
144
+ reservedSegmentDigest,
145
+ sameReservedEnvelope,
146
+ segmentDigest,
147
+ senderDigestOf,
148
+ senderErasedMarker,
149
+ } from "./archive";
150
+ export { TenantScopedArchiveStore } from "./TenantScopedArchiveStore";
151
+ export { InMemoryArchiveStore } from "./InMemoryArchiveStore";
@@ -0,0 +1,136 @@
1
+ /**
2
+ * How a store decorator handles one OPTIONAL SPI method of the store it wraps (ADR 0074
3
+ * Amendment, L1):
4
+ *
5
+ * - `"forward"` — call the inner store's method verbatim (bound to the inner store);
6
+ * - a function — the decorator's own implementation (it transforms ids, payloads, …);
7
+ * - `{ refuse: reason }` — deliberately NOT offered, with the reason recorded. The method is
8
+ * left uninstalled, so callers see exactly what they see for a store without it.
9
+ *
10
+ * What the compiler enforces: every key in `M` is REQUIRED (`-?` strips the `?` a mapped type
11
+ * over `S` would otherwise inherit from the optional SPI methods). So a decorator whose plan is
12
+ * `satisfies DecoratorPlan<EventStore, OptionalEventStoreMethod>` fails to compile if it omits
13
+ * any optional method — including one added to the SPI later, since `OptionalEventStoreMethod`
14
+ * is derived from the interface. Pinned by `decoratorPlan.typecheck.ts`, which `pnpm typecheck`
15
+ * checks. What it cannot enforce: that a decorator uses a plan at all — that is the decorator
16
+ * conformance kit's job, and the `packages/spec` fitness test makes every decorator run it.
17
+ */
18
+ export type DecoratorPlan<S, M extends keyof S> = {
19
+ readonly [K in M]-?: "forward" | NonNullable<S[K]> | { readonly refuse: string };
20
+ };
21
+
22
+ // Refusals, keyed by the decorated object. A WeakMap so a discarded store takes its entry
23
+ // with it; read back by the decorator conformance kit through `refusalsOf`.
24
+ const refusals = new WeakMap<object, ReadonlyMap<string, string>>();
25
+
26
+ /**
27
+ * The encryption-boundary marker a store exposes structurally (`EncryptingEventStore.keys`,
28
+ * `EncryptingSnapshotStore.keys`). `loom doctor` and `LogLifecycle`'s compaction/archive
29
+ * guard read it by shape, so a decorator stacked ABOVE an encrypting store must carry it up —
30
+ * otherwise `Coalescing(Encrypting(pg))` looks like a plain store and compaction would write
31
+ * the decrypted fold to a plaintext snapshot store (ADR 0022 Amendment).
32
+ */
33
+ const ENCRYPTION_MARKER = "keys";
34
+ /** `EncryptingSnapshotStore.sealsWholeState` (ADR 0130 D8), carried up the same way. */
35
+ const WHOLE_STATE_MARKER = "sealsWholeState";
36
+
37
+ /**
38
+ * Install each optional method of `plan` on `target`, **iff the inner store has it** — so a
39
+ * capability probe (`if (store.copyPrefix)`) answers exactly as it would against `inner`,
40
+ * and a decorator never advertises a capability its backend lacks. O(|M|) once, at
41
+ * construction; zero cost per call (a forward is a bound method, not a trampoline).
42
+ *
43
+ * A function entry is bound to `bindTo` when given (a decorator whose implementation uses
44
+ * `this`), else installed as-is. `{ refuse }` entries are recorded for {@link refusalsOf}.
45
+ *
46
+ * Also carries the inner store's structural encryption marker (`keys`) onto `target` when
47
+ * `target` does not define its own — see {@link ENCRYPTION_MARKER}.
48
+ */
49
+ export function installOptional<S extends object, M extends keyof S>(
50
+ target: S,
51
+ inner: S,
52
+ plan: DecoratorPlan<S, M>,
53
+ bindTo?: object
54
+ ): void {
55
+ const writable = target as Record<PropertyKey, unknown>;
56
+ const source = inner as Record<PropertyKey, unknown>;
57
+ let refused: Map<string, string> | undefined;
58
+ for (const method of Object.keys(plan) as M[]) {
59
+ const entry = plan[method] as unknown;
60
+ if (typeof entry === "object" && entry !== null) {
61
+ refused ??= new Map();
62
+ refused.set(String(method), (entry as { refuse: string }).refuse);
63
+ continue;
64
+ }
65
+ const innerMethod = source[method as PropertyKey];
66
+ if (typeof innerMethod !== "function") continue; // the backend lacks it: so do we
67
+ if (entry === "forward") {
68
+ writable[method as PropertyKey] = (innerMethod as (...a: unknown[]) => unknown).bind(inner);
69
+ } else if (typeof entry === "function") {
70
+ writable[method as PropertyKey] =
71
+ bindTo === undefined ? entry : (entry as (...a: unknown[]) => unknown).bind(bindTo);
72
+ }
73
+ }
74
+ if (refused !== undefined) refusals.set(target, refused);
75
+ forwardStoreMarkers(target, inner);
76
+ }
77
+
78
+ /** `TenantScopedEventStore.tenantScope` (and the archive/snapshot twins), carried up the same way (ADR 0138 H3). */
79
+ const TENANT_MARKER = "tenantScope";
80
+
81
+ /**
82
+ * ADR 0141 - the snapshot codec a store WRITES (`"none" | "zstd"`), carried up through every snapshot
83
+ * decorator so the composition root (`defineConfig`) reads what the outermost store really does and can
84
+ * refuse a mismatch, instead of the opt-in being silently dropped by a wrapper.
85
+ */
86
+ const CODEC_MARKER = "snapshotCodec";
87
+
88
+ // Who a decorator wraps, keyed by the decorator: lets a guard walk a stack whose layers hide their
89
+ // inner store (a closure-built decorator exposes no `inner`). A WeakMap, so it never keeps a store alive.
90
+ const wrapped = new WeakMap<object, object>();
91
+
92
+ /** The store `target` was built over (recorded by {@link forwardStoreMarkers}), or `undefined`. */
93
+ export function decoratedInnerOf(target: object): object | undefined {
94
+ return wrapped.get(target);
95
+ }
96
+
97
+ /**
98
+ * Carry the STRUCTURAL markers other layers read by shape from `inner` up to the decorator `target`
99
+ * (unless `target` defines its own): the encryption marker `keys`, `sealsWholeState` (ADR 0130 D8) and
100
+ * the tenant marker `tenantScope` (ADR 0138 H3), the snapshot codec marker `snapshotCodec` (ADR 0141), and record `target` as wrapping `inner`. EVERY store
101
+ * decorator (event, snapshot, archive) must call this - {@link installOptional} does - or a guard that
102
+ * reads a marker on the outermost store sees a plain, unscoped one. The decorator conformance kit
103
+ * asserts it for each decorator in the repo.
104
+ */
105
+ export function forwardStoreMarkers(target: object, inner: object): void {
106
+ const writable = target as Record<PropertyKey, unknown>;
107
+ const source = inner as Record<PropertyKey, unknown>;
108
+ wrapped.set(target, inner);
109
+ const marker = source[ENCRYPTION_MARKER];
110
+ if (typeof marker === "object" && marker !== null && writable[ENCRYPTION_MARKER] === undefined) {
111
+ writable[ENCRYPTION_MARKER] = marker;
112
+ }
113
+ // ADR 0130 D8 - a snapshot store that seals its WHOLE state says so structurally; a decorator
114
+ // above it must not hide that (the doctor / kernel would read a sealed store as plaintext).
115
+ if (source[WHOLE_STATE_MARKER] === true && writable[WHOLE_STATE_MARKER] === undefined) {
116
+ writable[WHOLE_STATE_MARKER] = true;
117
+ }
118
+ // ADR 0138 H3 - a decorator above a tenant-scoped store is still scoped to that tenant.
119
+ const tenant = source[TENANT_MARKER];
120
+ if (typeof tenant === "string" && writable[TENANT_MARKER] === undefined) {
121
+ writable[TENANT_MARKER] = tenant;
122
+ }
123
+ // ADR 0141 - a decorator above a codec-writing snapshot store still writes that codec.
124
+ const codec = source[CODEC_MARKER];
125
+ if ((codec === "none" || codec === "zstd") && writable[CODEC_MARKER] === undefined) {
126
+ writable[CODEC_MARKER] = codec;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * The optional methods a decorator deliberately refused through {@link installOptional},
132
+ * with their reasons. Empty for a store that refused nothing (or did not use the helper).
133
+ */
134
+ export function refusalsOf(store: object): ReadonlyMap<string, string> {
135
+ return refusals.get(store) ?? new Map();
136
+ }
@@ -0,0 +1,98 @@
1
+ import type { EventEnvelope } from "@loomwork/core";
2
+
3
+ /** The stable `code` of {@link KeyUnavailableError}. */
4
+ export const KEY_UNAVAILABLE_CODE = "KEY_UNAVAILABLE" as const;
5
+
6
+ /**
7
+ * ADR 0130 residuals D7 - a sealing store holds ciphertext for a subject whose key THIS node's key
8
+ * store does not have, and the log carries NO `PAYLOAD_SHREDDED` tombstone for it: the node is
9
+ * misconfigured (a different or empty key store), not erased. It is deliberately not
10
+ * `PayloadShreddedError`, which stays exclusively the erasure outcome. Lives beside the store error
11
+ * contract so the kernel can recognise it without depending on the crypto plugin.
12
+ *
13
+ * Carries the subject id and stream id, never ciphertext or key material. The kernel translates it
14
+ * to a denial-shaped `SealedStreamUnreadableError` (operator-only detail `key-unavailable`).
15
+ */
16
+ export class KeyUnavailableError extends Error {
17
+ readonly code = KEY_UNAVAILABLE_CODE;
18
+ constructor(
19
+ readonly subjectId: string,
20
+ readonly streamId?: string,
21
+ /** The stream's workflow id (a plaintext column), so the kernel can word its refusal. */
22
+ readonly workflowId?: string,
23
+ /**
24
+ * The stream with each sealed field replaced by a placeholder (its status and tip are
25
+ * plaintext). Lets the erasure lifecycle (`EraseCoordinator`, `exportSubject`) judge a stream
26
+ * in the ShredIncomplete window (key destroyed, tombstone not yet written). Never ciphertext.
27
+ */
28
+ readonly redacted?: readonly EventEnvelope[]
29
+ ) {
30
+ super(
31
+ `the encryption key for subject "${subjectId}"${streamId ? ` (stream "${streamId}")` : ""} is not available to this node and the log records no erasure (no PAYLOAD_SHREDDED): this node's key store lacks the key. It is not a crypto-shred.`
32
+ );
33
+ this.name = "KeyUnavailableError";
34
+ Object.setPrototypeOf(this, KeyUnavailableError.prototype);
35
+ }
36
+ }
37
+
38
+ /** True for a {@link KeyUnavailableError}, by its stable `code` (survives duplicate package copies). */
39
+ export function isKeyUnavailable(e: unknown): e is KeyUnavailableError {
40
+ return (e as { code?: unknown } | null)?.code === KEY_UNAVAILABLE_CODE;
41
+ }
42
+
43
+ /**
44
+ * ADR 0130 residuals D7 (round 4) - does this stream carry the DURABLE ERASE INTENT of `subjectId`?
45
+ * `shredSubject` appends an `ERASURE_PENDING` (an existing, ignorable, pure-audit kernel kind) naming
46
+ * the key's subject to each affected execution's log BEFORE it destroys the key. It is the only
47
+ * evidence that distinguishes the ShredIncomplete window (key destroyed, tombstone not yet written:
48
+ * intent present) from a node that simply never had the key (no intent). `subjectId` omitted accepts
49
+ * any intent.
50
+ */
51
+ export function recordsErasureIntent(
52
+ events: readonly Pick<EventEnvelope, "eventType" | "payload">[],
53
+ subjectId?: string
54
+ ): boolean {
55
+ return events.some(
56
+ (e) =>
57
+ e.eventType === "ERASURE_PENDING" &&
58
+ (subjectId === undefined ||
59
+ (e.payload as { subjectId?: unknown } | null)?.subjectId === subjectId)
60
+ );
61
+ }
62
+
63
+ /**
64
+ * ADR 0130 D7 (round 5) - proof that the KEY STORE destroyed `subjectId`'s key (`KeyStore.wasDestroyed`,
65
+ * or the operator's explicit assertion `() => true`). The log proves intent, never destruction.
66
+ */
67
+ export type DestructionProof = (subjectId: string) => boolean | Promise<boolean>;
68
+
69
+ /**
70
+ * The retry window, decided: a {@link KeyUnavailableError} carrying a redacted view is the genuine
71
+ * ShredIncomplete window ONLY when that view records this subject's erase intent AND `destroyed`
72
+ * proves the key store destroyed the key (a dangling intent whose key still lives elsewhere is not
73
+ * the window). No proof, or a false one, fails closed. Returns the redacted view in that case,
74
+ * `undefined` otherwise (the caller rethrows).
75
+ */
76
+ export async function erasureWindowView(
77
+ err: unknown,
78
+ destroyed?: DestructionProof
79
+ ): Promise<readonly EventEnvelope[] | undefined> {
80
+ if (!isKeyUnavailable(err) || !err.redacted) return undefined;
81
+ return (await isErasureWindow(err, err.redacted, destroyed)) ? err.redacted : undefined;
82
+ }
83
+
84
+ /**
85
+ * The ONE gate of the retry window: `err` is a {@link KeyUnavailableError} AND `events` (the
86
+ * stream's log, redacted or raw) records this subject's erase intent AND `destroyed` proves the key
87
+ * store destroyed the key. Every read in the window (event log, sealed checkpoint) uses it, so
88
+ * they cannot drift. No proof, or a false one, fails closed.
89
+ */
90
+ export async function isErasureWindow(
91
+ err: unknown,
92
+ events: readonly Pick<EventEnvelope, "eventType" | "payload">[],
93
+ destroyed?: DestructionProof
94
+ ): Promise<boolean> {
95
+ if (!isKeyUnavailable(err)) return false;
96
+ if (!recordsErasureIntent(events, err.subjectId)) return false;
97
+ return destroyed !== undefined && (await destroyed(err.subjectId)) === true;
98
+ }