@totemsdk/wots-lease 1.0.0 → 1.0.2

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 (67) hide show
  1. package/LICENSE +202 -21
  2. package/NOTICE +60 -0
  3. package/dist/axia.d.ts +1 -1
  4. package/dist/axia.js +41 -28
  5. package/dist/certificate.d.ts +37 -0
  6. package/dist/certificate.js +69 -0
  7. package/dist/cjs/axia.d.ts +33 -0
  8. package/dist/cjs/axia.js +145 -0
  9. package/dist/cjs/certificate.d.ts +37 -0
  10. package/dist/cjs/certificate.js +74 -0
  11. package/dist/cjs/device.d.ts +18 -0
  12. package/dist/cjs/device.js +34 -0
  13. package/dist/cjs/errors.d.ts +71 -0
  14. package/dist/cjs/errors.js +135 -0
  15. package/dist/cjs/hybrid.d.ts +37 -0
  16. package/dist/cjs/hybrid.js +145 -0
  17. package/dist/cjs/index.d.ts +17 -0
  18. package/dist/cjs/index.js +45 -0
  19. package/dist/cjs/journal.d.ts +30 -0
  20. package/dist/cjs/journal.js +369 -0
  21. package/dist/cjs/local.d.ts +83 -0
  22. package/dist/cjs/local.js +440 -0
  23. package/dist/cjs/onchain.d.ts +54 -0
  24. package/dist/cjs/onchain.js +314 -0
  25. package/dist/cjs/package.json +1 -0
  26. package/dist/cjs/quorum.d.ts +75 -0
  27. package/dist/cjs/quorum.js +307 -0
  28. package/dist/cjs/replicated-backup.d.ts +30 -0
  29. package/dist/cjs/replicated-backup.js +52 -0
  30. package/dist/cjs/seed.d.ts +41 -0
  31. package/dist/cjs/seed.js +84 -0
  32. package/dist/cjs/stubs.d.ts +50 -0
  33. package/dist/cjs/stubs.js +125 -0
  34. package/dist/cjs/types.d.ts +272 -0
  35. package/dist/cjs/types.js +5 -0
  36. package/dist/cjs/watermark.d.ts +51 -0
  37. package/dist/cjs/watermark.js +328 -0
  38. package/dist/device.js +2 -6
  39. package/dist/errors.d.ts +26 -0
  40. package/dist/errors.js +49 -25
  41. package/dist/hybrid.d.ts +4 -1
  42. package/dist/hybrid.js +72 -26
  43. package/dist/index.d.ts +6 -2
  44. package/dist/index.js +12 -38
  45. package/dist/journal.d.ts +14 -4
  46. package/dist/journal.js +327 -49
  47. package/dist/local.d.ts +31 -1
  48. package/dist/local.js +267 -68
  49. package/dist/onchain.d.ts +3 -1
  50. package/dist/onchain.js +63 -28
  51. package/dist/quorum.d.ts +15 -0
  52. package/dist/quorum.js +54 -15
  53. package/dist/replicated-backup.d.ts +30 -0
  54. package/dist/replicated-backup.js +48 -0
  55. package/dist/seed.d.ts +41 -0
  56. package/dist/seed.js +80 -0
  57. package/dist/stubs.d.ts +2 -0
  58. package/dist/stubs.js +20 -10
  59. package/dist/types.d.ts +74 -1
  60. package/dist/types.js +1 -2
  61. package/dist/watermark.d.ts +22 -1
  62. package/dist/watermark.js +133 -16
  63. package/package.json +25 -15
  64. package/rust/pkg/package.json +3 -3
  65. package/rust/pkg/wots_lease_wasm_bg.wasm +0 -0
  66. package/rust/pkg-node/package.json +3 -3
  67. package/rust/pkg-node/wots_lease_wasm_bg.wasm +0 -0
package/dist/journal.js CHANGED
@@ -1,13 +1,35 @@
1
- "use strict";
2
1
  /**
3
- * LeaseJournal — append-only audit log for WOTS key-use events.
4
- * Preserves the full history: reserved → committed | burned | reserved-expired.
2
+ * LeaseJournal — physically append-only audit log for WOTS key-use events.
3
+ *
4
+ * Layout v1 stores one immutable record per sequence. The legacy whole-array
5
+ * key is retained as migration input/forensic history but is never rewritten.
6
+ * The SHA-256 chain format is intentionally unchanged (RFC-007 OQ3).
5
7
  */
6
- Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.LeaseJournal = void 0;
8
- const crypto_1 = require("crypto");
9
- const core_1 = require("@totemsdk/core");
10
- const STORAGE_KEY = 'totem_wots_journal';
8
+ import { sha256 } from '@noble/hashes/sha256.js';
9
+ import { bytesToHex } from '@noble/hashes/utils.js';
10
+ import { NoopLogger } from '@totemsdk/core';
11
+ import { StorageError } from '@totemsdk/storage/errors';
12
+ const LEGACY_STORAGE_KEY = 'totem_wots_journal';
13
+ const ENTRY_PREFIX = 'totem_wots_journal:v1:entry:';
14
+ const RECORD_VERSION = 1;
15
+ const SEQUENCE_WIDTH = 16;
16
+ const MAX_INDEX_COMPONENT = 63;
17
+ const HASH_PATTERN = /^[0-9a-f]{64}$/;
18
+ const KEY_PATTERN = /^(\d{16}):([0-9a-f]{64})$/;
19
+ const STATUSES = new Set([
20
+ 'reserved',
21
+ 'committed',
22
+ 'burned',
23
+ 'reserved-expired',
24
+ ]);
25
+ const storageTails = new WeakMap();
26
+ function withStorageLock(storage, fn) {
27
+ const prior = storageTails.get(storage) ?? Promise.resolve();
28
+ const run = prior.then(fn, fn);
29
+ storageTails.set(storage, run.then(() => undefined, () => undefined));
30
+ return run;
31
+ }
32
+ /** Existing commitment format; property names/order and SHA-256 stay unchanged. */
11
33
  function hashEntry(entry) {
12
34
  const encoder = new TextEncoder();
13
35
  const data = {
@@ -21,67 +43,323 @@ function hashEntry(entry) {
21
43
  d: entry.deviceId,
22
44
  };
23
45
  const bytes = encoder.encode(JSON.stringify(data));
24
- return (0, crypto_1.createHash)('sha256').update(bytes).digest('hex');
46
+ return bytesToHex(sha256(bytes));
47
+ }
48
+ function cloneEntry(entry) {
49
+ return {
50
+ treeId: entry.treeId,
51
+ branchId: entry.branchId,
52
+ wotsIndex: entry.wotsIndex,
53
+ indices: { ...entry.indices },
54
+ status: entry.status,
55
+ ...(entry.reservationId !== undefined ? { reservationId: entry.reservationId } : {}),
56
+ ...(entry.payloadHash !== undefined ? { payloadHash: entry.payloadHash } : {}),
57
+ ...(entry.txId !== undefined ? { txId: entry.txId } : {}),
58
+ timestamp: entry.timestamp,
59
+ deviceId: entry.deviceId,
60
+ ...(entry.previousHash !== undefined ? { previousHash: entry.previousHash } : {}),
61
+ ...(entry.hash !== undefined ? { hash: entry.hash } : {}),
62
+ };
63
+ }
64
+ function asObject(value, key) {
65
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
66
+ throw new StorageError(`LeaseJournal: invalid object at ${key}`, 'corrupt', { key });
67
+ }
68
+ return value;
69
+ }
70
+ function requiredString(object, field, key) {
71
+ const value = object[field];
72
+ if (typeof value !== 'string' || value.length === 0) {
73
+ throw new StorageError(`LeaseJournal: invalid ${field} at ${key}`, 'corrupt', { key });
74
+ }
75
+ return value;
76
+ }
77
+ function optionalString(object, field, key) {
78
+ const value = object[field];
79
+ if (value === undefined)
80
+ return undefined;
81
+ if (typeof value !== 'string') {
82
+ throw new StorageError(`LeaseJournal: invalid ${field} at ${key}`, 'corrupt', { key });
83
+ }
84
+ return value;
85
+ }
86
+ function requiredInteger(object, field, key) {
87
+ const value = object[field];
88
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
89
+ throw new StorageError(`LeaseJournal: invalid ${field} at ${key}`, 'corrupt', { key });
90
+ }
91
+ return value;
92
+ }
93
+ function validateEntry(value, key) {
94
+ const object = asObject(value, key);
95
+ const indices = asObject(object.indices, `${key}.indices`);
96
+ const status = requiredString(object, 'status', key);
97
+ if (!STATUSES.has(status)) {
98
+ throw new StorageError(`LeaseJournal: invalid status at ${key}`, 'corrupt', { key });
99
+ }
100
+ const timestamp = object.timestamp;
101
+ if (typeof timestamp !== 'number' || !Number.isFinite(timestamp) || timestamp < 0) {
102
+ throw new StorageError(`LeaseJournal: invalid timestamp at ${key}`, 'corrupt', { key });
103
+ }
104
+ const validated = cloneEntry({
105
+ treeId: requiredString(object, 'treeId', key),
106
+ branchId: requiredString(object, 'branchId', key),
107
+ wotsIndex: requiredInteger(object, 'wotsIndex', key),
108
+ indices: {
109
+ addressIndex: requiredInteger(indices, 'addressIndex', `${key}.indices`),
110
+ l1: requiredInteger(indices, 'l1', `${key}.indices`),
111
+ l2: requiredInteger(indices, 'l2', `${key}.indices`),
112
+ },
113
+ status,
114
+ reservationId: optionalString(object, 'reservationId', key),
115
+ payloadHash: optionalString(object, 'payloadHash', key),
116
+ txId: optionalString(object, 'txId', key),
117
+ timestamp,
118
+ deviceId: requiredString(object, 'deviceId', key),
119
+ previousHash: optionalString(object, 'previousHash', key),
120
+ hash: optionalString(object, 'hash', key),
121
+ });
122
+ const { addressIndex, l1, l2 } = validated.indices;
123
+ if (addressIndex > MAX_INDEX_COMPONENT || l1 > MAX_INDEX_COMPONENT || l2 > MAX_INDEX_COMPONENT) {
124
+ throw new StorageError(`LeaseJournal: indices out of range at ${key}`, 'corrupt', { key });
125
+ }
126
+ const expectedIndex = addressIndex * 64 * 64 + l1 * 64 + l2;
127
+ if (validated.wotsIndex !== expectedIndex) {
128
+ throw new StorageError(`LeaseJournal: wotsIndex/indices mismatch at ${key}`, 'corrupt', { key });
129
+ }
130
+ return validated;
131
+ }
132
+ function verifyChain(entries, source) {
133
+ for (let i = 0; i < entries.length; i += 1) {
134
+ const entry = entries[i];
135
+ if (entry.hash === undefined || !HASH_PATTERN.test(entry.hash)) {
136
+ throw new StorageError(`LeaseJournal: invalid hash at ${source}[${i}]`, 'corrupt');
137
+ }
138
+ const expectedHash = hashEntry(entry);
139
+ if (entry.hash !== expectedHash) {
140
+ throw new StorageError(`LeaseJournal: hash mismatch at ${source}[${i}]`, 'corrupt');
141
+ }
142
+ if (i === 0) {
143
+ if (entry.previousHash !== undefined) {
144
+ throw new StorageError(`LeaseJournal: first entry has previousHash at ${source}[0]`, 'corrupt');
145
+ }
146
+ }
147
+ else if (entry.previousHash !== entries[i - 1].hash) {
148
+ throw new StorageError(`LeaseJournal: previousHash mismatch at ${source}[${i}]`, 'corrupt');
149
+ }
150
+ }
151
+ }
152
+ function entriesEqual(a, b) {
153
+ return a.treeId === b.treeId
154
+ && a.branchId === b.branchId
155
+ && a.wotsIndex === b.wotsIndex
156
+ && a.indices.addressIndex === b.indices.addressIndex
157
+ && a.indices.l1 === b.indices.l1
158
+ && a.indices.l2 === b.indices.l2
159
+ && a.status === b.status
160
+ && a.reservationId === b.reservationId
161
+ && a.payloadHash === b.payloadHash
162
+ && a.txId === b.txId
163
+ && a.timestamp === b.timestamp
164
+ && a.deviceId === b.deviceId
165
+ && a.previousHash === b.previousHash
166
+ && a.hash === b.hash;
167
+ }
168
+ function sequenceText(sequence) {
169
+ return sequence.toString(10).padStart(SEQUENCE_WIDTH, '0');
170
+ }
171
+ function recordKey(sequence, hash) {
172
+ return `${ENTRY_PREFIX}${sequenceText(sequence)}:${hash}`;
25
173
  }
26
- class LeaseJournal {
27
- constructor(storage, logger = new core_1.NoopLogger()) {
174
+ function parseRecordKey(key) {
175
+ const relative = key.slice(ENTRY_PREFIX.length);
176
+ const match = KEY_PATTERN.exec(relative);
177
+ if (!match) {
178
+ throw new StorageError(`LeaseJournal: malformed record key ${key}`, 'corrupt', { key });
179
+ }
180
+ const sequence = Number(match[1]);
181
+ if (!Number.isSafeInteger(sequence)) {
182
+ throw new StorageError(`LeaseJournal: invalid sequence in ${key}`, 'corrupt', { key });
183
+ }
184
+ return { key, sequence, hash: match[2] };
185
+ }
186
+ export class LeaseJournal {
187
+ constructor(storage, logger = new NoopLogger()) {
28
188
  this.storage = storage;
29
189
  this.logger = logger;
30
190
  this.entries = [];
31
- this._initialized = false;
191
+ this.initialized = false;
192
+ this.initializePromise = null;
193
+ this.appendTail = Promise.resolve();
32
194
  }
33
195
  async initialize() {
34
- if (this._initialized)
196
+ if (this.initialized)
35
197
  return;
36
- const stored = await this.storage.get(STORAGE_KEY);
37
- this.entries = stored ?? [];
38
- if (this.entries.length > 0) {
39
- const ok = this._verifyChain();
40
- if (!ok) {
41
- this.logger.error('[LeaseJournal] hash chain integrity check FAILED — journal may be tampered');
42
- throw new Error('Journal hash chain integrity check failed');
198
+ if (!this.initializePromise) {
199
+ this.initializePromise = withStorageLock(this.storage, () => this.initializeOnce()).catch((error) => {
200
+ this.initializePromise = null;
201
+ throw error;
202
+ });
203
+ }
204
+ await this.initializePromise;
205
+ }
206
+ async initializeOnce() {
207
+ const physical = await this.readPhysicalRecords();
208
+ const legacy = await this.readLegacyEntries();
209
+ if (legacy !== null) {
210
+ const commonLength = Math.min(physical.length, legacy.length);
211
+ for (let i = 0; i < commonLength; i += 1) {
212
+ if (!entriesEqual(physical[i], legacy[i])) {
213
+ throw new StorageError(`LeaseJournal: legacy/v1 divergence at sequence ${i}`, 'corrupt');
214
+ }
215
+ }
216
+ for (let i = physical.length; i < legacy.length; i += 1) {
217
+ await this.writeRecord(i, legacy[i]);
218
+ }
219
+ }
220
+ const replayed = legacy !== null && legacy.length > physical.length
221
+ ? await this.readPhysicalRecords()
222
+ : physical;
223
+ this.entries = replayed.map(cloneEntry);
224
+ this.initialized = true;
225
+ }
226
+ async readLegacyEntries() {
227
+ const raw = await this.storage.get(LEGACY_STORAGE_KEY);
228
+ if (raw === null)
229
+ return null;
230
+ if (!Array.isArray(raw)) {
231
+ throw new StorageError('LeaseJournal: legacy journal is not an array', 'corrupt', { key: LEGACY_STORAGE_KEY });
232
+ }
233
+ const entries = raw.map((entry, index) => validateEntry(entry, `${LEGACY_STORAGE_KEY}[${index}]`));
234
+ verifyChain(entries, LEGACY_STORAGE_KEY);
235
+ return entries;
236
+ }
237
+ async readPhysicalRecords() {
238
+ const parsed = (await this.storage.keys())
239
+ .filter((key) => key.startsWith(ENTRY_PREFIX))
240
+ .map(parseRecordKey)
241
+ .sort((a, b) => a.sequence - b.sequence || a.key.localeCompare(b.key));
242
+ for (let i = 0; i < parsed.length; i += 1) {
243
+ if (i > 0 && parsed[i - 1].sequence === parsed[i].sequence) {
244
+ throw new StorageError(`LeaseJournal: fork at sequence ${parsed[i].sequence}`, 'corrupt');
245
+ }
246
+ if (parsed[i].sequence !== i) {
247
+ throw new StorageError(`LeaseJournal: sequence gap at ${i}`, 'corrupt');
43
248
  }
44
249
  }
45
- this._initialized = true;
46
- }
47
- async append(entry) {
48
- const prev = this.entries[this.entries.length - 1];
49
- entry.previousHash = prev?.hash;
50
- entry.hash = hashEntry(entry);
51
- this.entries.push(entry);
52
- await this.storage.set(STORAGE_KEY, this.entries);
53
- this.logger.debug(`[LeaseJournal] ${entry.status} treeId=${entry.treeId} idx=${entry.wotsIndex} txId=${entry.txId ?? '-'} hash=${entry.hash.slice(0, 8)}`);
54
- }
55
- _verifyChain() {
56
- for (let i = 0; i < this.entries.length; i++) {
57
- const e = this.entries[i];
58
- const expectedHash = hashEntry(e);
59
- if (e.hash !== expectedHash) {
60
- this.logger.error(`[LeaseJournal] hash mismatch at entry ${i}: expected ${expectedHash}, stored ${e.hash}`);
61
- return false;
250
+ const entries = [];
251
+ for (const descriptor of parsed) {
252
+ const raw = await this.storage.get(descriptor.key);
253
+ if (raw === null) {
254
+ throw new StorageError(`LeaseJournal: listed record is missing`, 'corrupt', { key: descriptor.key });
62
255
  }
63
- if (i > 0) {
64
- const prevHash = this.entries[i - 1].hash;
65
- if (e.previousHash !== prevHash) {
66
- this.logger.error(`[LeaseJournal] previousHash mismatch at entry ${i}: expected ${prevHash}, stored ${e.previousHash}`);
67
- return false;
256
+ const record = asObject(raw, descriptor.key);
257
+ const formatVersion = record.formatVersion;
258
+ if (formatVersion !== RECORD_VERSION) {
259
+ throw new StorageError('LeaseJournal: unsupported record version', 'corrupt', {
260
+ key: descriptor.key,
261
+ detectedVersion: typeof formatVersion === 'number' ? formatVersion : undefined,
262
+ unsupportedVersion: typeof formatVersion === 'number' ? formatVersion : undefined,
263
+ });
264
+ }
265
+ if (record.sequence !== descriptor.sequence) {
266
+ throw new StorageError('LeaseJournal: sequence/key mismatch', 'corrupt', { key: descriptor.key });
267
+ }
268
+ const entry = validateEntry(record.entry, `${descriptor.key}.entry`);
269
+ if (entry.hash !== descriptor.hash) {
270
+ throw new StorageError('LeaseJournal: hash/key mismatch', 'corrupt', { key: descriptor.key });
271
+ }
272
+ entries.push(entry);
273
+ }
274
+ verifyChain(entries, ENTRY_PREFIX);
275
+ return entries;
276
+ }
277
+ async writeRecord(sequence, entry) {
278
+ if (entry.hash === undefined) {
279
+ throw new StorageError(`LeaseJournal: entry ${sequence} has no hash`, 'corrupt');
280
+ }
281
+ const key = recordKey(sequence, entry.hash);
282
+ const record = {
283
+ formatVersion: RECORD_VERSION,
284
+ sequence,
285
+ entry: cloneEntry(entry),
286
+ };
287
+ await this.storage.set(key, record);
288
+ }
289
+ append(entry) {
290
+ const run = this.appendTail.then(async () => this.appendOne(entry));
291
+ this.appendTail = run.catch(() => undefined);
292
+ return run;
293
+ }
294
+ async appendOne(input) {
295
+ await this.initialize();
296
+ await withStorageLock(this.storage, async () => {
297
+ // Refresh under the shared lock so multiple journal instances that share
298
+ // the same adapter object cannot allocate the same sequence or slot.
299
+ this.entries = (await this.readPhysicalRecords()).map(cloneEntry);
300
+ const entry = validateEntry(cloneEntry(input), 'append');
301
+ if (entry.status === 'reserved' && this.entries.some((existing) => existing.treeId === entry.treeId && existing.wotsIndex === entry.wotsIndex)) {
302
+ throw new StorageError(`LeaseJournal: WOTS slot ${entry.treeId}/${entry.wotsIndex} is already recorded`, 'write-failed');
303
+ }
304
+ if (entry.status !== 'reserved' && entry.reservationId) {
305
+ const latest = [...this.entries].reverse().find((existing) => existing.reservationId === entry.reservationId);
306
+ if (latest?.status === entry.status) {
307
+ if (latest.treeId !== entry.treeId || latest.wotsIndex !== entry.wotsIndex
308
+ || (entry.status === 'committed' && latest.txId !== entry.txId)) {
309
+ throw new StorageError(`LeaseJournal: reservation ${entry.reservationId} is already ${latest.status} for a different use`, 'write-failed');
310
+ }
311
+ if (latest.previousHash === undefined)
312
+ delete input.previousHash;
313
+ else
314
+ input.previousHash = latest.previousHash;
315
+ input.hash = latest.hash;
316
+ return;
317
+ }
318
+ if (latest && latest.status !== 'reserved') {
319
+ throw new StorageError(`LeaseJournal: reservation ${entry.reservationId} is already ${latest.status}`, 'write-failed');
68
320
  }
69
321
  }
322
+ const previous = this.entries[this.entries.length - 1];
323
+ if (previous?.hash !== undefined)
324
+ entry.previousHash = previous.hash;
325
+ else
326
+ delete entry.previousHash;
327
+ entry.hash = hashEntry(entry);
328
+ const sequence = this.entries.length;
329
+ const sequencePrefix = `${ENTRY_PREFIX}${sequenceText(sequence)}:`;
330
+ if ((await this.storage.keys()).some((key) => key.startsWith(sequencePrefix))) {
331
+ throw new StorageError(`LeaseJournal: sequence ${sequence} already exists`, 'write-failed');
332
+ }
333
+ await this.writeRecord(sequence, entry);
334
+ this.entries.push(entry);
335
+ if (entry.previousHash === undefined)
336
+ delete input.previousHash;
337
+ else
338
+ input.previousHash = entry.previousHash;
339
+ input.hash = entry.hash;
340
+ this.logger.debug(`[LeaseJournal] ${entry.status} treeId=${entry.treeId} idx=${entry.wotsIndex} `
341
+ + `txId=${entry.txId ?? '-'} hash=${entry.hash.slice(0, 8)}`);
342
+ });
343
+ }
344
+ ensureInitialized() {
345
+ if (!this.initialized) {
346
+ throw new Error('LeaseJournal not initialized — call initialize() first');
70
347
  }
71
- return true;
72
348
  }
73
349
  getAll() {
74
- return [...this.entries];
350
+ this.ensureInitialized();
351
+ return this.entries.map(cloneEntry);
75
352
  }
76
353
  getByTree(treeId) {
77
- return this.entries.filter((e) => e.treeId === treeId);
354
+ this.ensureInitialized();
355
+ return this.entries.filter((entry) => entry.treeId === treeId).map(cloneEntry);
78
356
  }
79
357
  getByReservation(reservationId) {
80
- return [...this.entries].reverse().find((e) => e.reservationId === reservationId);
358
+ this.ensureInitialized();
359
+ const entry = [...this.entries].reverse().find((candidate) => candidate.reservationId === reservationId);
360
+ return entry ? cloneEntry(entry) : undefined;
81
361
  }
82
362
  async clear() {
83
- this.entries = [];
84
- await this.storage.remove(STORAGE_KEY);
363
+ throw new StorageError('LeaseJournal is append-only and cannot be cleared', 'write-failed');
85
364
  }
86
365
  }
87
- exports.LeaseJournal = LeaseJournal;
package/dist/local.d.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * All WOTS safety guaranteed by the local WotsWatermarkStore + LeaseJournal.
6
6
  */
7
7
  import type { StorageAdapter, LoggerAdapter } from '@totemsdk/core';
8
- import type { WotsLeaseProvider, ReserveParams, LeaseReservation, LeaseCertificate, LocalWatermark, SyncResult, SigningIndices } from './types.js';
8
+ import type { WotsLeaseProvider, ReserveParams, LeaseReservation, LeaseCertificate, LocalWatermark, SyncResult, SigningIndices, WatermarkSeed, SeedWatermarkOptions } from './types.js';
9
9
  import { LeaseJournal } from './journal.js';
10
10
  export declare class LocalLeaseProvider implements WotsLeaseProvider {
11
11
  private readonly logger;
@@ -13,9 +13,26 @@ export declare class LocalLeaseProvider implements WotsLeaseProvider {
13
13
  private readonly leaseStore;
14
14
  private readonly journal;
15
15
  private _initialized;
16
+ private initializePromise;
17
+ private mutationTail;
16
18
  private readonly deviceId;
19
+ /**
20
+ * Optional CAS primitive (AUD-004). When the adapter exposes
21
+ * `conditionalUpdate`, a per-slot claim key is written atomically before the
22
+ * reservation so two providers over distinct adapter objects on the same
23
+ * durable medium cannot both claim one slot. Adapters without CAS fall back
24
+ * to the existing refresh() + check behavior.
25
+ */
26
+ private readonly cas?;
17
27
  constructor(storage: StorageAdapter, logger?: LoggerAdapter, deviceId?: string);
28
+ private static detectCas;
18
29
  initialize(): Promise<void>;
30
+ private initializeOnce;
31
+ /**
32
+ * Rebuild the no-reuse watermark from immutable journal history and leases.
33
+ * The journal is audit authority; the watermark remains allocation authority.
34
+ */
35
+ private reconcileDurableSafetyState;
19
36
  /** Burn all active (non-expired) leases that have no matching treeId.
20
37
  * On restart, leases with missing treeId (from before the treeId field was added)
21
38
  * must be burned to prevent index reuse. */
@@ -28,9 +45,22 @@ export declare class LocalLeaseProvider implements WotsLeaseProvider {
28
45
  * slot is already taken.
29
46
  */
30
47
  reserveSpecificKeyUse(params: ReserveParams, indices: SigningIndices): Promise<LeaseReservation>;
48
+ private reserveSpecificKeyUseUnlocked;
49
+ private withMutationLock;
31
50
  commitKeyUse(reservationId: string, txId: string): Promise<void>;
51
+ private commitKeyUseUnlocked;
32
52
  burnReservation(reservationId: string, reason: string): Promise<void>;
53
+ private burnReservationUnlocked;
33
54
  getLocalWatermark(treeId: string): Promise<LocalWatermark>;
55
+ /**
56
+ * Export a portable watermark seed for restore (RFC-013 §9).
57
+ */
58
+ exportWatermark(treeId: string): Promise<WatermarkSeed>;
59
+ /**
60
+ * Seed the local watermark from an exported/operator/on-chain snapshot.
61
+ * Forward-only by default; refuses to start below the true high-water mark.
62
+ */
63
+ seedWatermark(seed: WatermarkSeed, options?: SeedWatermarkOptions): Promise<void>;
34
64
  /** List all tree IDs known to the local watermark store. */
35
65
  listTrees(): string[];
36
66
  /** Expose the journal for quorum/on-chain providers to merge remote entries. */