@openlfcp/storage 0.1.0-rc.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/memory.js ADDED
@@ -0,0 +1,367 @@
1
+ import { bytesEqual, LfcpError, toHex, } from "@openlfcp/core";
2
+ import { InMemoryActorSequenceReservation } from "./sequence.js";
3
+ import { InMemorySnapshotSequenceReservation, } from "./snapshot-sequence.js";
4
+ /** A deep copy: every Uint8Array copied, everything frozen. Nothing stored aliases a caller's buffer. */
5
+ function own(value) {
6
+ if (value instanceof Uint8Array)
7
+ return Uint8Array.from(value);
8
+ if (Array.isArray(value))
9
+ return Object.freeze(value.map(own));
10
+ if (value !== null && typeof value === "object")
11
+ return Object.freeze(Object.fromEntries(Object.entries(value).map(([k, v]) => [k, own(v)])));
12
+ return value;
13
+ }
14
+ const hex = toHex;
15
+ const byBytes = (a, b) => {
16
+ const [x, y] = [hex(a), hex(b)];
17
+ return x < y ? -1 : x > y ? 1 : 0;
18
+ };
19
+ const tuple = (resource, actor, seq) => `${hex(resource)}:${hex(actor)}:${seq}`;
20
+ const emptyState = () => ({
21
+ records: new Map(),
22
+ heads: new Map(),
23
+ conflicts: new Map(),
24
+ epochs: new Map(),
25
+ units: new Map(),
26
+ keyPackages: new Map(),
27
+ snapshots: new Map(),
28
+ resources: new Map(),
29
+ routes: new Map(),
30
+ outbound: new Map(),
31
+ checkpoints: new Map(),
32
+ syncStates: new Map(),
33
+ marks: new Map(),
34
+ });
35
+ /** Rows are immutable, so a staged copy of the maps is enough for all-or-nothing batches. */
36
+ const stage = (s) => ({
37
+ records: new Map(s.records),
38
+ heads: new Map(s.heads),
39
+ conflicts: new Map(s.conflicts),
40
+ epochs: new Map([...s.epochs].map(([k, v]) => [k, new Map(v)])),
41
+ units: new Map(s.units),
42
+ keyPackages: new Map(s.keyPackages),
43
+ snapshots: new Map(s.snapshots),
44
+ resources: new Map(s.resources),
45
+ routes: new Map(s.routes),
46
+ outbound: new Map(s.outbound),
47
+ checkpoints: new Map(s.checkpoints),
48
+ syncStates: new Map(s.syncStates),
49
+ marks: new Map(s.marks),
50
+ });
51
+ /** Stores an immutable object under its ID; the same ID with other bytes is refused. */
52
+ function putImmutable(map, id, row, what) {
53
+ const old = map.get(hex(id));
54
+ if (old !== undefined) {
55
+ if (!bytesEqual(old.bytes, row.bytes))
56
+ throw new LfcpError("INVALID_STRUCTURE", `${what} ${hex(id)} is stored with other bytes`);
57
+ return;
58
+ }
59
+ map.set(hex(id), own(row));
60
+ }
61
+ function apply(s, w) {
62
+ switch (w.op) {
63
+ case "put-control-records":
64
+ for (const r of w.records)
65
+ putImmutable(s.records, r.recordId, r, "Control Record");
66
+ return;
67
+ case "set-control-head":
68
+ s.heads.set(hex(w.resourceId), own(w.head));
69
+ return;
70
+ case "set-control-conflict":
71
+ if (w.conflict === null)
72
+ s.conflicts.delete(hex(w.resourceId));
73
+ else
74
+ s.conflicts.set(hex(w.resourceId), own(w.conflict));
75
+ return;
76
+ case "put-epoch": {
77
+ const key = hex(w.resourceId);
78
+ const epochs = s.epochs.get(key) ?? new Map();
79
+ const stored = epochs.get(BigInt(w.epoch.epoch));
80
+ epochs.set(BigInt(w.epoch.epoch), own({
81
+ ...w.epoch,
82
+ closedBy: w.epoch.closedBy ?? stored?.closedBy ?? null,
83
+ dekRef: w.epoch.dekRef ?? stored?.dekRef ?? null,
84
+ }));
85
+ s.epochs.set(key, epochs);
86
+ return;
87
+ }
88
+ case "expect-previous-unit":
89
+ return; // a precondition, checked by commit()
90
+ case "put-data-unit": {
91
+ const old = s.units.get(hex(w.unit.unitId));
92
+ if (old !== undefined && !bytesEqual(old.bytes, w.unit.bytes))
93
+ throw new LfcpError("INVALID_STRUCTURE", `Data Unit ${hex(w.unit.unitId)} is stored with other bytes`);
94
+ const base = old ?? w.unit;
95
+ s.units.set(hex(w.unit.unitId), own({
96
+ ...pickUnit(base),
97
+ status: w.status,
98
+ detail: w.detail ?? null,
99
+ accepted: w.accepted ?? old?.accepted ?? false,
100
+ }));
101
+ return;
102
+ }
103
+ case "set-data-unit-status": {
104
+ const old = known(s, w.unitId);
105
+ s.units.set(hex(w.unitId), own({ ...old, status: w.status, detail: w.detail ?? null }));
106
+ return;
107
+ }
108
+ case "set-accepted": {
109
+ const old = known(s, w.unitId);
110
+ s.units.set(hex(w.unitId), own({ ...old, accepted: w.accepted }));
111
+ return;
112
+ }
113
+ case "put-key-package":
114
+ putImmutable(s.keyPackages, w.row.packageId, w.row, "Key Package");
115
+ return;
116
+ case "delete-snapshot":
117
+ s.snapshots.delete(hex(w.snapshotId));
118
+ return;
119
+ case "put-snapshot":
120
+ putImmutable(s.snapshots, w.row.snapshotId, w.row, "Snapshot");
121
+ return;
122
+ case "put-resource":
123
+ s.resources.set(hex(w.row.resourceId), own(w.row));
124
+ return;
125
+ case "put-route":
126
+ s.routes.set(hex(w.resourceId), own(w.route));
127
+ return;
128
+ case "enqueue":
129
+ putImmutable(s.outbound, w.item.itemId, w.item, "outbound item");
130
+ return;
131
+ case "update-outbound": {
132
+ const old = s.outbound.get(hex(w.itemId));
133
+ if (old === undefined)
134
+ throw new LfcpError("INVALID_STRUCTURE", `no outbound item ${hex(w.itemId)}`);
135
+ s.outbound.set(hex(w.itemId), own({
136
+ ...old,
137
+ ...(w.attempts === undefined ? {} : { attempts: w.attempts }),
138
+ ...(w.lastAttempt === undefined ? {} : { lastAttempt: w.lastAttempt }),
139
+ ...(w.nextAttempt === undefined ? {} : { nextAttempt: w.nextAttempt }),
140
+ ...(w.blocked === undefined ? {} : { blocked: w.blocked }),
141
+ }));
142
+ return;
143
+ }
144
+ case "dequeue":
145
+ s.outbound.delete(hex(w.itemId));
146
+ return;
147
+ case "put-profile-checkpoint":
148
+ s.checkpoints.set(hex(w.checkpoint.resourceId), own(w.checkpoint));
149
+ return;
150
+ case "put-sync-state":
151
+ s.syncStates.set(hex(w.row.resourceId), own(w.row));
152
+ return;
153
+ case "put-local-mark":
154
+ if (w.value === null)
155
+ s.marks.delete(w.key);
156
+ else
157
+ s.marks.set(w.key, w.value);
158
+ return;
159
+ }
160
+ }
161
+ const pickUnit = (u) => ({
162
+ unitId: u.unitId,
163
+ resourceId: u.resourceId,
164
+ dataEpoch: u.dataEpoch,
165
+ actor: u.actor,
166
+ actorSeq: u.actorSeq,
167
+ prevDataUnitId: u.prevDataUnitId,
168
+ controlHead: u.controlHead,
169
+ bytes: u.bytes,
170
+ });
171
+ function known(s, unitId) {
172
+ const u = s.units.get(hex(unitId));
173
+ if (u === undefined)
174
+ throw new LfcpError("INVALID_STRUCTURE", `no Data Unit ${hex(unitId)}`);
175
+ return u;
176
+ }
177
+ const ACCEPTED_ORDER = (a, b) => byBytes(a.actor, b.actor) ||
178
+ (a.actorSeq < b.actorSeq ? -1 : a.actorSeq > b.actorSeq ? 1 : 0) ||
179
+ byBytes(a.unitId, b.unitId);
180
+ /**
181
+ * FOR TESTS AND DEVELOPMENT ONLY: an LfcpStorage in process memory, lost on
182
+ * restart, with the same semantic contracts as a durable adapter (exact
183
+ * bytes copied in and out, immutable objects, all-or-nothing batches with
184
+ * their preconditions). Deterministic: no clock, no randomness, ordered
185
+ * results.
186
+ */
187
+ export class InMemoryLfcpStorage {
188
+ #state = emptyState();
189
+ #actorCounter = new InMemoryActorSequenceReservation();
190
+ #snapshotCounter = new InMemorySnapshotSequenceReservation();
191
+ /** Reservations fail closed when the counter is behind this Principal's own stored objects (§9, §29). */
192
+ actorSequences = {
193
+ reserveNext: async (resource, principal) => {
194
+ const next = await this.#actorCounter.reserveNext(resource, principal);
195
+ const max = [...this.#state.units.values()]
196
+ .filter((u) => bytesEqual(u.resourceId, resource) && bytesEqual(u.actor, principal))
197
+ .reduce((m, u) => (u.actorSeq > m ? BigInt(u.actorSeq) : m), 0n);
198
+ if (next <= max)
199
+ throw new LfcpError("SEQUENCE_REUSE", `the actor sequence state is behind the stored units of this Principal (${max}); refusing to reserve (§9)`);
200
+ return next;
201
+ },
202
+ };
203
+ snapshotSequences = {
204
+ reserveNext: async (resource, epoch, publisher) => {
205
+ const next = await this.#snapshotCounter.reserveNext(resource, epoch, publisher);
206
+ const max = [...this.#state.snapshots.values()]
207
+ .filter((x) => bytesEqual(x.resourceId, resource) &&
208
+ x.dataEpoch === epoch &&
209
+ bytesEqual(x.publisher, publisher))
210
+ .reduce((m, x) => (x.snapshotSeq > m ? x.snapshotSeq : m), 0n);
211
+ if (next <= max)
212
+ throw new LfcpError("SEQUENCE_REUSE", `the Snapshot Sequence state is behind the stored Snapshots of this publisher (${max}); refusing to reserve (§29)`);
213
+ return next;
214
+ },
215
+ };
216
+ commit(writes) {
217
+ const next = stage(this.#state);
218
+ for (const w of writes) {
219
+ if (w.op !== "set-control-head")
220
+ continue;
221
+ const current = next.heads.get(hex(w.resourceId))?.head ?? null;
222
+ const matches = current === null
223
+ ? w.expected === null
224
+ : w.expected !== null && bytesEqual(current, w.expected);
225
+ if (!matches)
226
+ return Promise.resolve(Object.freeze({
227
+ ok: false,
228
+ reason: "CONTROL_HEAD_MISMATCH",
229
+ resourceId: own(w.resourceId),
230
+ current: current === null ? null : own(current),
231
+ }));
232
+ }
233
+ for (const w of writes) {
234
+ if (w.op !== "expect-previous-unit")
235
+ continue;
236
+ let latest;
237
+ for (const u of next.units.values())
238
+ if (u.accepted &&
239
+ bytesEqual(u.resourceId, w.resourceId) &&
240
+ bytesEqual(u.actor, w.actor) &&
241
+ (latest === undefined || u.actorSeq > latest.actorSeq))
242
+ latest = u;
243
+ const current = latest?.unitId ?? null;
244
+ const matches = current === null
245
+ ? w.previous === null
246
+ : w.previous !== null && bytesEqual(current, w.previous);
247
+ if (!matches)
248
+ return Promise.resolve(Object.freeze({
249
+ ok: false,
250
+ reason: "PREVIOUS_UNIT_MISMATCH",
251
+ resourceId: own(w.resourceId),
252
+ actor: own(w.actor),
253
+ current: current === null ? null : own(current),
254
+ }));
255
+ }
256
+ try {
257
+ for (const w of writes)
258
+ apply(next, w);
259
+ }
260
+ catch (e) {
261
+ return Promise.reject(e);
262
+ }
263
+ this.#state = next;
264
+ return Promise.resolve(Object.freeze({ ok: true }));
265
+ }
266
+ control = {
267
+ record: (id) => Promise.resolve(copyOf(this.#state.records.get(hex(id)))),
268
+ records: (resource) => Promise.resolve([...this.#state.records.values()]
269
+ .filter((r) => bytesEqual(r.resourceId, resource))
270
+ .sort((a, b) => a.controlSeq < b.controlSeq
271
+ ? -1
272
+ : a.controlSeq > b.controlSeq
273
+ ? 1
274
+ : byBytes(a.recordId, b.recordId))
275
+ .map(own)),
276
+ head: (resource) => Promise.resolve(copyOf(this.#state.heads.get(hex(resource)))),
277
+ conflict: (resource) => Promise.resolve(copyOf(this.#state.conflicts.get(hex(resource)))),
278
+ epochs: (resource) => Promise.resolve([...(this.#state.epochs.get(hex(resource))?.values() ?? [])]
279
+ .sort((a, b) => (a.epoch < b.epoch ? -1 : a.epoch > b.epoch ? 1 : 0))
280
+ .map(own)),
281
+ };
282
+ dataUnits = {
283
+ get: (id) => Promise.resolve(copyOf(this.#state.units.get(hex(id)))),
284
+ at: (resource, actor, seq) => Promise.resolve(this.#at(resource, actor, seq).map(own)),
285
+ range: (resource, actor, from, to) => Promise.resolve(this.#units((u) => bytesEqual(u.resourceId, resource) &&
286
+ bytesEqual(u.actor, actor) &&
287
+ u.actorSeq >= from &&
288
+ u.actorSeq <= to)),
289
+ withStatus: (resource, status) => Promise.resolve(this.#units((u) => bytesEqual(u.resourceId, resource) && u.status === status)),
290
+ acceptedAt: (resource, actor, seq) => Promise.resolve(copyOf(this.#at(resource, actor, seq).find((u) => u.accepted)?.unitId)),
291
+ recordSeen: (unit) => {
292
+ const firstSeen = !this.#state.units.has(hex(unit.unitId));
293
+ if (firstSeen) {
294
+ const next = stage(this.#state);
295
+ apply(next, { op: "put-data-unit", unit, status: "seen" });
296
+ this.#state = next;
297
+ }
298
+ else if (!bytesEqual(this.#state.units.get(hex(unit.unitId)).bytes, unit.bytes)) {
299
+ return Promise.reject(new LfcpError("INVALID_STRUCTURE", `Data Unit ${hex(unit.unitId)} is stored with other bytes`));
300
+ }
301
+ const unitIds = this.#at(unit.resourceId, unit.actor, unit.actorSeq)
302
+ .map((u) => own(u.unitId))
303
+ .sort(byBytes);
304
+ return Promise.resolve(Object.freeze({ unitIds: Object.freeze(unitIds), firstSeen }));
305
+ },
306
+ };
307
+ #at(resource, actor, seq) {
308
+ const key = tuple(resource, actor, seq);
309
+ return [...this.#state.units.values()]
310
+ .filter((u) => tuple(u.resourceId, u.actor, u.actorSeq) === key)
311
+ .sort((a, b) => byBytes(a.unitId, b.unitId));
312
+ }
313
+ #units(keep) {
314
+ return [...this.#state.units.values()].filter(keep).sort(ACCEPTED_ORDER).map(own);
315
+ }
316
+ keyPackages = {
317
+ get: (id) => Promise.resolve(copyOf(this.#state.keyPackages.get(hex(id)))),
318
+ list: (resource, filter = {}) => Promise.resolve([...this.#state.keyPackages.values()]
319
+ .filter((k) => bytesEqual(k.resourceId, resource) &&
320
+ (filter.epoch === undefined || k.dataEpoch === filter.epoch) &&
321
+ (filter.recipient === undefined || bytesEqual(k.recipient, filter.recipient)))
322
+ .sort((a, b) => a.dataEpoch < b.dataEpoch
323
+ ? -1
324
+ : a.dataEpoch > b.dataEpoch
325
+ ? 1
326
+ : byBytes(a.packageId, b.packageId))
327
+ .map(own)),
328
+ };
329
+ snapshots = {
330
+ get: (id) => Promise.resolve(copyOf(this.#state.snapshots.get(hex(id)))),
331
+ list: (resource, filter = {}) => Promise.resolve([...this.#state.snapshots.values()]
332
+ .filter((s) => bytesEqual(s.resourceId, resource) &&
333
+ (filter.epoch === undefined || s.dataEpoch === filter.epoch))
334
+ .sort((a, b) => (a.dataEpoch < b.dataEpoch ? -1 : a.dataEpoch > b.dataEpoch ? 1 : 0) ||
335
+ byBytes(a.publisher, b.publisher) ||
336
+ (a.snapshotSeq < b.snapshotSeq ? -1 : a.snapshotSeq > b.snapshotSeq ? 1 : 0))
337
+ .map(own)),
338
+ };
339
+ resources = {
340
+ get: (resource) => Promise.resolve(copyOf(this.#state.resources.get(hex(resource)))),
341
+ list: () => Promise.resolve([...this.#state.resources.values()]
342
+ .sort((a, b) => byBytes(a.resourceId, b.resourceId))
343
+ .map(own)),
344
+ route: (resource) => Promise.resolve(copyOf(this.#state.routes.get(hex(resource)))),
345
+ };
346
+ outbound = {
347
+ list: (resource) => Promise.resolve([...this.#state.outbound.values()]
348
+ .filter((o) => resource === undefined || bytesEqual(o.resourceId, resource))
349
+ .map(own)),
350
+ get: (id) => Promise.resolve(copyOf(this.#state.outbound.get(hex(id)))),
351
+ };
352
+ profileState = {
353
+ checkpoint: (resource) => Promise.resolve(copyOf(this.#state.checkpoints.get(hex(resource)))),
354
+ };
355
+ syncState = {
356
+ get: (resource) => Promise.resolve(copyOf(this.#state.syncStates.get(hex(resource)))),
357
+ };
358
+ localMarks = {
359
+ get: (key) => Promise.resolve(this.#state.marks.get(key)),
360
+ list: (prefix) => Promise.resolve([...this.#state.marks]
361
+ .filter(([key]) => key.startsWith(prefix))
362
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
363
+ .map(([key, value]) => ({ key, value }))),
364
+ };
365
+ }
366
+ const copyOf = (value) => value === undefined ? undefined : own(value);
367
+ //# sourceMappingURL=memory.js.map
@@ -0,0 +1,53 @@
1
+ import { type DataEpoch, type PrincipalId, type ResourceId } from "@openlfcp/core";
2
+ /**
3
+ * Client secret material (LFCP-034): Principal private keys, Resource DEKs
4
+ * and invitation secrets live only in a SecretStore. Public storage rows
5
+ * hold a SecretRef, a name that reveals nothing about the value.
6
+ *
7
+ * A SecretStore cannot be enumerated: there is no list operation, so no
8
+ * caller can dump its contents into a log or a diagnostic. Backends are
9
+ * platform specific: app.secretStorage in Obsidian (LFCP-059), a file
10
+ * keystore in Node (LFCP-035). No cloud KMS is required.
11
+ *
12
+ * Ordering rule, since a SecretStore is usually a different backend from
13
+ * the public rows and cannot join their atomic batch: write the secret
14
+ * first, then commit the row that references it. A reference without a
15
+ * value is a recoverable gap; a value without a reference is harmless.
16
+ */
17
+ export type SecretKind = "principal-signing-key" | "principal-agreement-key" | "resource-dek" | "invitation-secret";
18
+ /** The name of a secret: "lfcp-secret:<kind>:<id>". It never contains secret material. */
19
+ export type SecretRef = string & {
20
+ readonly __secretRef: true;
21
+ };
22
+ /** A secret reference; `id` is a public name such as hex IDs joined with ".". */
23
+ export declare function secretRef(kind: SecretKind, id: string): SecretRef;
24
+ /** True when `text` is a well-formed secret reference. */
25
+ export declare function isSecretRef(text: unknown): text is SecretRef;
26
+ /** The reference of the DEK of `epoch` of `resource`. */
27
+ export declare const dekSecretRef: (resource: ResourceId, epoch: DataEpoch) => SecretRef;
28
+ /** The reference of a local Principal's private signing or agreement key. */
29
+ export declare const principalKeySecretRef: (principal: PrincipalId, which: "signing" | "agreement") => SecretRef;
30
+ export interface SecretStore {
31
+ /** Stores a copy of `value` under `ref`, replacing any previous value. Durable before it resolves. */
32
+ put(ref: SecretRef, value: Uint8Array): Promise<void>;
33
+ /** A copy of the value, or undefined. */
34
+ get(ref: SecretRef): Promise<Uint8Array | undefined>;
35
+ delete(ref: SecretRef): Promise<void>;
36
+ }
37
+ declare const INSPECT: unique symbol;
38
+ /**
39
+ * FOR TESTS AND DEVELOPMENT ONLY: secrets in plain process memory, lost on
40
+ * restart. Renders as "[InMemorySecretStore]" in JSON and inspection, so it
41
+ * cannot leak values into logs by accident.
42
+ */
43
+ export declare class InMemorySecretStore implements SecretStore {
44
+ #private;
45
+ put(ref: SecretRef, value: Uint8Array): Promise<void>;
46
+ get(ref: SecretRef): Promise<Uint8Array | undefined>;
47
+ delete(ref: SecretRef): Promise<void>;
48
+ toJSON(): string;
49
+ toString(): string;
50
+ [INSPECT](): string;
51
+ }
52
+ export {};
53
+ //# sourceMappingURL=secrets.d.ts.map
@@ -0,0 +1,60 @@
1
+ import { dataEpoch, LfcpError, toHex, } from "@openlfcp/core";
2
+ const KINDS = new Set([
3
+ "principal-signing-key",
4
+ "principal-agreement-key",
5
+ "resource-dek",
6
+ "invitation-secret",
7
+ ]);
8
+ const ID = /^[A-Za-z0-9._-]{1,200}$/;
9
+ /** A secret reference; `id` is a public name such as hex IDs joined with ".". */
10
+ export function secretRef(kind, id) {
11
+ if (!KINDS.has(kind))
12
+ throw new LfcpError("UNSUPPORTED_VALUE", `unknown secret kind ${kind}`);
13
+ if (!ID.test(id))
14
+ throw new LfcpError("UNSUPPORTED_VALUE", "a secret reference id is [A-Za-z0-9._-]{1,200}");
15
+ return `lfcp-secret:${kind}:${id}`;
16
+ }
17
+ /** True when `text` is a well-formed secret reference. */
18
+ export function isSecretRef(text) {
19
+ if (typeof text !== "string")
20
+ return false;
21
+ const m = /^lfcp-secret:([a-z-]+):(.+)$/.exec(text);
22
+ return m !== null && KINDS.has(m[1]) && ID.test(m[2]);
23
+ }
24
+ /** The reference of the DEK of `epoch` of `resource`. */
25
+ export const dekSecretRef = (resource, epoch) => secretRef("resource-dek", `${toHex(resource)}.${dataEpoch(epoch)}`);
26
+ /** The reference of a local Principal's private signing or agreement key. */
27
+ export const principalKeySecretRef = (principal, which) => secretRef(which === "signing" ? "principal-signing-key" : "principal-agreement-key", toHex(principal));
28
+ const INSPECT = Symbol.for("nodejs.util.inspect.custom");
29
+ /**
30
+ * FOR TESTS AND DEVELOPMENT ONLY: secrets in plain process memory, lost on
31
+ * restart. Renders as "[InMemorySecretStore]" in JSON and inspection, so it
32
+ * cannot leak values into logs by accident.
33
+ */
34
+ export class InMemorySecretStore {
35
+ #values = new Map();
36
+ put(ref, value) {
37
+ if (!isSecretRef(ref))
38
+ return Promise.reject(new LfcpError("UNSUPPORTED_VALUE", "not a secret reference"));
39
+ this.#values.set(ref, Uint8Array.from(value));
40
+ return Promise.resolve();
41
+ }
42
+ get(ref) {
43
+ const v = this.#values.get(ref);
44
+ return Promise.resolve(v === undefined ? undefined : Uint8Array.from(v));
45
+ }
46
+ delete(ref) {
47
+ this.#values.delete(ref);
48
+ return Promise.resolve();
49
+ }
50
+ toJSON() {
51
+ return "[InMemorySecretStore]";
52
+ }
53
+ toString() {
54
+ return "[InMemorySecretStore]";
55
+ }
56
+ [INSPECT]() {
57
+ return "[InMemorySecretStore]";
58
+ }
59
+ }
60
+ //# sourceMappingURL=secrets.js.map
@@ -0,0 +1,55 @@
1
+ import { type ActorSequence, type PrincipalId, type ResourceId } from "@openlfcp/core";
2
+ /**
3
+ * Actor sequence allocation (LFCP-WIRE-01 §8, §12).
4
+ *
5
+ * A writer's Data Unit nonce is 0x00000000 || uint64_be(seq) under a key
6
+ * that depends on (Resource, Data Epoch, Principal). A sequence used twice
7
+ * for one (Resource, Principal) can reuse a nonce under the same key, so
8
+ * sequences are scoped to (Resource, Principal), start at 1, and never go
9
+ * back, not even when the Data Epoch changes.
10
+ */
11
+ export interface ActorSequenceReservation {
12
+ /**
13
+ * Reserves the next actor sequence for `principal` writing to `resource`.
14
+ *
15
+ * Contract: the reservation is durable before the promise resolves (a
16
+ * crash after it resolves can never hand out the same value again), and
17
+ * the same (resource, principal, seq) is never returned twice, across
18
+ * Data Epochs, restarts and concurrent callers. A caller that cannot use
19
+ * a reserved sequence abandons it; it is never returned to the pool.
20
+ *
21
+ * When the state is lost and cannot be reconstructed safely, the writer
22
+ * must switch to a new Principal for that Resource (§8). When the space
23
+ * is exhausted (2^64 - 1 was handed out) it rejects with OUT_OF_RANGE.
24
+ *
25
+ * The durable implementations belong to the storage tasks: LFCP-034
26
+ * (storage abstraction), LFCP-035 (Node persistence adapter, "atomic
27
+ * enough to prevent sequence reuse") and LFCP-036 (pending outbound
28
+ * queue: a retry never regenerates a Data Unit with the same sequence).
29
+ */
30
+ reserveNext(resource: ResourceId, principal: PrincipalId): Promise<ActorSequence>;
31
+ }
32
+ /** The sequence after `last` (1 when nothing was reserved yet); OUT_OF_RANGE after 2^64 - 1. */
33
+ export declare function nextActorSequence(last: ActorSequence | undefined): ActorSequence;
34
+ /**
35
+ * FOR TESTS AND DEVELOPMENT ONLY. NOT CRASH-SAFE: the state lives in
36
+ * memory, so a restart starts again at 1 and reuses nonces. Production
37
+ * writers need a durable ActorSequenceReservation (LFCP-034, LFCP-035).
38
+ */
39
+ export declare class InMemoryActorSequenceReservation implements ActorSequenceReservation {
40
+ #private;
41
+ reserveNext(resource: ResourceId, principal: PrincipalId): Promise<ActorSequence>;
42
+ }
43
+ /**
44
+ * A local check that no (resource, principal, seq) is used twice, as a
45
+ * second line of defence where Data Units are created. It remembers what
46
+ * it has seen in memory only; it does not replace a durable reservation.
47
+ */
48
+ export declare class SequenceReuseGuard {
49
+ #private;
50
+ /** Records the tuple, or throws SEQUENCE_REUSE when it was recorded before. */
51
+ claim(resource: ResourceId, principal: PrincipalId, seq: ActorSequence): void;
52
+ /** True when the tuple was claimed before. */
53
+ has(resource: ResourceId, principal: PrincipalId, seq: ActorSequence): boolean;
54
+ }
55
+ //# sourceMappingURL=sequence.d.ts.map
@@ -0,0 +1,55 @@
1
+ import { actorSequence, LfcpError, toHex, UINT64_MAX, } from "@openlfcp/core";
2
+ /** The sequence after `last` (1 when nothing was reserved yet); OUT_OF_RANGE after 2^64 - 1. */
3
+ export function nextActorSequence(last) {
4
+ if (last === undefined)
5
+ return actorSequence(1n);
6
+ if (last >= UINT64_MAX)
7
+ throw new LfcpError("OUT_OF_RANGE", "the actor sequence space is exhausted; use a new Principal for this Resource (§8)");
8
+ return actorSequence(last + 1n);
9
+ }
10
+ const tupleKey = (resource, principal) => `${toHex(resource)}:${toHex(principal)}`;
11
+ /**
12
+ * FOR TESTS AND DEVELOPMENT ONLY. NOT CRASH-SAFE: the state lives in
13
+ * memory, so a restart starts again at 1 and reuses nonces. Production
14
+ * writers need a durable ActorSequenceReservation (LFCP-034, LFCP-035).
15
+ */
16
+ export class InMemoryActorSequenceReservation {
17
+ #last = new Map();
18
+ reserveNext(resource, principal) {
19
+ try {
20
+ const key = tupleKey(resource, principal);
21
+ const next = nextActorSequence(this.#last.get(key));
22
+ this.#last.set(key, next);
23
+ return Promise.resolve(next);
24
+ }
25
+ catch (e) {
26
+ return Promise.reject(e);
27
+ }
28
+ }
29
+ }
30
+ /**
31
+ * A local check that no (resource, principal, seq) is used twice, as a
32
+ * second line of defence where Data Units are created. It remembers what
33
+ * it has seen in memory only; it does not replace a durable reservation.
34
+ */
35
+ export class SequenceReuseGuard {
36
+ #seen = new Map();
37
+ /** Records the tuple, or throws SEQUENCE_REUSE when it was recorded before. */
38
+ claim(resource, principal, seq) {
39
+ const s = actorSequence(seq);
40
+ const key = tupleKey(resource, principal);
41
+ let used = this.#seen.get(key);
42
+ if (used === undefined) {
43
+ used = new Set();
44
+ this.#seen.set(key, used);
45
+ }
46
+ if (used.has(s))
47
+ throw new LfcpError("SEQUENCE_REUSE", `actor sequence ${s} was already used for this (Resource, Principal) (§8)`);
48
+ used.add(s);
49
+ }
50
+ /** True when the tuple was claimed before. */
51
+ has(resource, principal, seq) {
52
+ return this.#seen.get(tupleKey(resource, principal))?.has(seq) ?? false;
53
+ }
54
+ }
55
+ //# sourceMappingURL=sequence.js.map
@@ -0,0 +1,39 @@
1
+ import { type DataEpoch, type PrincipalId, type ResourceId } from "@openlfcp/core";
2
+ /**
3
+ * Snapshot Sequence allocation (LFCP-WIRE-01 §29, §29.1.2). A Snapshot's
4
+ * nonce is 0x00000000 || uint64_be(snapshot_sequence) under a key that
5
+ * depends on (Resource, Data Epoch, publisher), so a sequence used twice
6
+ * for one (resource, data_epoch, publisher) can reuse a nonce. Sequences
7
+ * start at 1 and never go back.
8
+ */
9
+ export interface SnapshotSequenceReservation {
10
+ /**
11
+ * Reserves the next Snapshot Sequence for `publisher` in `epoch` of
12
+ * `resource`. Same contract as ActorSequenceReservation: durable before
13
+ * the promise resolves, never the same value twice for one tuple, across
14
+ * restarts and concurrent callers; an unused reservation is abandoned,
15
+ * never returned. OUT_OF_RANGE once 2^64 - 1 was handed out. Durable
16
+ * implementations belong to LFCP-034 and LFCP-035.
17
+ */
18
+ reserveNext(resource: ResourceId, epoch: DataEpoch, publisher: PrincipalId): Promise<bigint>;
19
+ }
20
+ /**
21
+ * FOR TESTS AND DEVELOPMENT ONLY. NOT CRASH-SAFE: a restart starts again at
22
+ * 1 and reuses nonces. Production publishers need a durable
23
+ * SnapshotSequenceReservation (LFCP-034, LFCP-035).
24
+ */
25
+ export declare class InMemorySnapshotSequenceReservation implements SnapshotSequenceReservation {
26
+ #private;
27
+ reserveNext(resource: ResourceId, epoch: DataEpoch, publisher: PrincipalId): Promise<bigint>;
28
+ }
29
+ /**
30
+ * A local check that no (resource, data_epoch, publisher, sequence) is used
31
+ * twice, as a second line of defence where Snapshots are created. In memory
32
+ * only; it does not replace a durable reservation.
33
+ */
34
+ export declare class SnapshotSequenceGuard {
35
+ #private;
36
+ /** Records the tuple, or throws SEQUENCE_REUSE when it was recorded before. */
37
+ claim(resource: ResourceId, epoch: DataEpoch, publisher: PrincipalId, seq: bigint): void;
38
+ }
39
+ //# sourceMappingURL=snapshot-sequence.d.ts.map
@@ -0,0 +1,34 @@
1
+ import { dataEpoch, LfcpError, toHex, UINT64_MAX, } from "@openlfcp/core";
2
+ const tupleKey = (resource, epoch, publisher) => `${toHex(resource)}:${dataEpoch(epoch)}:${toHex(publisher)}`;
3
+ /**
4
+ * FOR TESTS AND DEVELOPMENT ONLY. NOT CRASH-SAFE: a restart starts again at
5
+ * 1 and reuses nonces. Production publishers need a durable
6
+ * SnapshotSequenceReservation (LFCP-034, LFCP-035).
7
+ */
8
+ export class InMemorySnapshotSequenceReservation {
9
+ #last = new Map();
10
+ reserveNext(resource, epoch, publisher) {
11
+ const key = tupleKey(resource, epoch, publisher);
12
+ const last = this.#last.get(key) ?? 0n;
13
+ if (last >= UINT64_MAX)
14
+ return Promise.reject(new LfcpError("OUT_OF_RANGE", "the Snapshot Sequence space is exhausted (§29)"));
15
+ this.#last.set(key, last + 1n);
16
+ return Promise.resolve(last + 1n);
17
+ }
18
+ }
19
+ /**
20
+ * A local check that no (resource, data_epoch, publisher, sequence) is used
21
+ * twice, as a second line of defence where Snapshots are created. In memory
22
+ * only; it does not replace a durable reservation.
23
+ */
24
+ export class SnapshotSequenceGuard {
25
+ #seen = new Set();
26
+ /** Records the tuple, or throws SEQUENCE_REUSE when it was recorded before. */
27
+ claim(resource, epoch, publisher, seq) {
28
+ const key = `${tupleKey(resource, epoch, publisher)}:${seq}`;
29
+ if (this.#seen.has(key))
30
+ throw new LfcpError("SEQUENCE_REUSE", `Snapshot Sequence ${seq} was already used for this (Resource, epoch, publisher) (§29)`);
31
+ this.#seen.add(key);
32
+ }
33
+ }
34
+ //# sourceMappingURL=snapshot-sequence.js.map