@loomwork/event-runtime 0.0.0-stage → 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/LICENSE +202 -0
- package/NOTICE +8 -0
- package/README.md +95 -2
- package/dist/BranchRegistry.d.ts +140 -0
- package/dist/BranchRegistry.d.ts.map +1 -0
- package/dist/BranchRegistry.js +66 -0
- package/dist/BranchRegistry.js.map +1 -0
- package/dist/CoalescingEventStore.d.ts +97 -0
- package/dist/CoalescingEventStore.d.ts.map +1 -0
- package/dist/CoalescingEventStore.js +180 -0
- package/dist/CoalescingEventStore.js.map +1 -0
- package/dist/InMemoryArchiveStore.d.ts +19 -0
- package/dist/InMemoryArchiveStore.d.ts.map +1 -0
- package/dist/InMemoryArchiveStore.js +49 -0
- package/dist/InMemoryArchiveStore.js.map +1 -0
- package/dist/InMemoryAsyncEventStore.d.ts +111 -0
- package/dist/InMemoryAsyncEventStore.d.ts.map +1 -0
- package/dist/InMemoryAsyncEventStore.js +374 -0
- package/dist/InMemoryAsyncEventStore.js.map +1 -0
- package/dist/InMemoryBranchRegistry.d.ts +31 -0
- package/dist/InMemoryBranchRegistry.d.ts.map +1 -0
- package/dist/InMemoryBranchRegistry.js +167 -0
- package/dist/InMemoryBranchRegistry.js.map +1 -0
- package/dist/ProfilingEventStore.d.ts +75 -0
- package/dist/ProfilingEventStore.d.ts.map +1 -0
- package/dist/ProfilingEventStore.js +133 -0
- package/dist/ProfilingEventStore.js.map +1 -0
- package/dist/ShardPlan.d.ts +87 -0
- package/dist/ShardPlan.d.ts.map +1 -0
- package/dist/ShardPlan.js +193 -0
- package/dist/ShardPlan.js.map +1 -0
- package/dist/ShardedEventStore.d.ts +149 -0
- package/dist/ShardedEventStore.d.ts.map +1 -0
- package/dist/ShardedEventStore.js +468 -0
- package/dist/ShardedEventStore.js.map +1 -0
- package/dist/SubscriberRegistry.d.ts +59 -0
- package/dist/SubscriberRegistry.d.ts.map +1 -0
- package/dist/SubscriberRegistry.js +113 -0
- package/dist/SubscriberRegistry.js.map +1 -0
- package/dist/TenantScopedArchiveStore.d.ts +36 -0
- package/dist/TenantScopedArchiveStore.d.ts.map +1 -0
- package/dist/TenantScopedArchiveStore.js +96 -0
- package/dist/TenantScopedArchiveStore.js.map +1 -0
- package/dist/TenantScopedEventStore.d.ts +59 -0
- package/dist/TenantScopedEventStore.d.ts.map +1 -0
- package/dist/TenantScopedEventStore.js +227 -0
- package/dist/TenantScopedEventStore.js.map +1 -0
- package/dist/archive.d.ts +68 -0
- package/dist/archive.d.ts.map +1 -0
- package/dist/archive.js +207 -0
- package/dist/archive.js.map +1 -0
- package/dist/archiveSpi.d.ts +165 -0
- package/dist/archiveSpi.d.ts.map +1 -0
- package/dist/archiveSpi.js +184 -0
- package/dist/archiveSpi.js.map +1 -0
- package/dist/creationMarker.d.ts +13 -0
- package/dist/creationMarker.d.ts.map +1 -0
- package/dist/creationMarker.js +17 -0
- package/dist/creationMarker.js.map +1 -0
- package/dist/decoratorPlan.typecheck.d.ts +2 -0
- package/dist/decoratorPlan.typecheck.d.ts.map +1 -0
- package/dist/decoratorPlan.typecheck.js +41 -0
- package/dist/decoratorPlan.typecheck.js.map +1 -0
- package/dist/eventKindClass.d.ts +40 -0
- package/dist/eventKindClass.d.ts.map +1 -0
- package/dist/eventKindClass.js +99 -0
- package/dist/eventKindClass.js.map +1 -0
- package/dist/executionIdOrder.d.ts +21 -0
- package/dist/executionIdOrder.d.ts.map +1 -0
- package/dist/executionIdOrder.js +43 -0
- package/dist/executionIdOrder.js.map +1 -0
- package/dist/fencing.d.ts +21 -0
- package/dist/fencing.d.ts.map +1 -0
- package/dist/fencing.js +36 -0
- package/dist/fencing.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +126 -0
- package/dist/index.js.map +1 -0
- package/dist/installOptional.d.ts +52 -0
- package/dist/installOptional.d.ts.map +1 -0
- package/dist/installOptional.js +112 -0
- package/dist/installOptional.js.map +1 -0
- package/dist/keyUnavailable.d.ts +67 -0
- package/dist/keyUnavailable.d.ts.map +1 -0
- package/dist/keyUnavailable.js +87 -0
- package/dist/keyUnavailable.js.map +1 -0
- package/dist/leases.d.ts +177 -0
- package/dist/leases.d.ts.map +1 -0
- package/dist/leases.js +142 -0
- package/dist/leases.js.map +1 -0
- package/dist/payloadShredded.d.ts +30 -0
- package/dist/payloadShredded.d.ts.map +1 -0
- package/dist/payloadShredded.js +67 -0
- package/dist/payloadShredded.js.map +1 -0
- package/dist/scaleDecorators.bench.d.ts +2 -0
- package/dist/scaleDecorators.bench.d.ts.map +1 -0
- package/dist/scaleDecorators.bench.js +93 -0
- package/dist/scaleDecorators.bench.js.map +1 -0
- package/dist/sequenceRange.bench.d.ts +2 -0
- package/dist/sequenceRange.bench.d.ts.map +1 -0
- package/dist/sequenceRange.bench.js +46 -0
- package/dist/sequenceRange.bench.js.map +1 -0
- package/dist/sequenceRange.d.ts +10 -0
- package/dist/sequenceRange.d.ts.map +1 -0
- package/dist/sequenceRange.js +35 -0
- package/dist/sequenceRange.js.map +1 -0
- package/dist/timers.d.ts +87 -0
- package/dist/timers.d.ts.map +1 -0
- package/dist/timers.js +68 -0
- package/dist/timers.js.map +1 -0
- package/dist/types.d.ts +634 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +429 -0
- package/dist/types.js.map +1 -0
- package/dist/writeConflicts.d.ts +46 -0
- package/dist/writeConflicts.d.ts.map +1 -0
- package/dist/writeConflicts.js +63 -0
- package/dist/writeConflicts.js.map +1 -0
- package/memory.d.ts +4 -0
- package/memory.js +13 -0
- package/package.json +67 -3
- package/sequence.d.ts +1 -0
- package/sequence.js +5 -0
- package/src/BranchRegistry.ts +161 -0
- package/src/CoalescingEventStore.ts +214 -0
- package/src/InMemoryArchiveStore.ts +69 -0
- package/src/InMemoryAsyncEventStore.ts +425 -0
- package/src/InMemoryBranchRegistry.ts +178 -0
- package/src/ProfilingEventStore.ts +153 -0
- package/src/ShardPlan.ts +235 -0
- package/src/ShardedEventStore.ts +557 -0
- package/src/SubscriberRegistry.ts +115 -0
- package/src/TenantScopedArchiveStore.ts +114 -0
- package/src/TenantScopedEventStore.ts +266 -0
- package/src/archive.ts +248 -0
- package/src/archiveSpi.ts +308 -0
- package/src/creationMarker.ts +15 -0
- package/src/decoratorPlan.typecheck.ts +52 -0
- package/src/eventKindClass.ts +127 -0
- package/src/executionIdOrder.ts +38 -0
- package/src/fencing.ts +31 -0
- package/src/index.ts +151 -0
- package/src/installOptional.ts +136 -0
- package/src/keyUnavailable.ts +98 -0
- package/src/leases.ts +319 -0
- package/src/payloadShredded.ts +63 -0
- package/src/scaleDecorators.bench.ts +107 -0
- package/src/sequenceRange.bench.ts +51 -0
- package/src/sequenceRange.ts +35 -0
- package/src/timers.ts +150 -0
- package/src/types.ts +857 -0
- 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
|
+
}
|