@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
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ /**
3
+ * HybridLeaseProvider — recommended production default.
4
+ *
5
+ * Always uses local first, then gates on node cert and/or quorum attestation
6
+ * for transactions above a configurable value threshold.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.HybridLeaseProvider = void 0;
10
+ const certificate_js_1 = require("./certificate.js");
11
+ class HybridLeaseProvider {
12
+ constructor(config) {
13
+ this.local = config.local;
14
+ this.node = config.node;
15
+ this.quorum = config.quorum;
16
+ this.onchain = config.onchain;
17
+ this.threshold = config.threshold ?? Infinity;
18
+ this.certificateSigner = config.certificateSigner;
19
+ }
20
+ isHighValue(params) {
21
+ if (!params.valueHint)
22
+ return false;
23
+ const val = parseFloat(params.valueHint);
24
+ return !isNaN(val) && val >= this.threshold;
25
+ }
26
+ async reserveKeyUse(params) {
27
+ // Cross-instance/restore safety (RFC-013 §8/§9): before a high-value
28
+ // reservation, anchor the local cursor to the on-chain watermark so a second
29
+ // instance of the same seed cannot be handed a leaf we already used.
30
+ if (this.isHighValue(params) && this.onchain) {
31
+ try {
32
+ await this.onchain.syncLeaseJournal();
33
+ }
34
+ catch {
35
+ // Chain unreachable — local monotonic watermark still guards reuse.
36
+ }
37
+ }
38
+ const reservation = await this.local.reserveKeyUse(params);
39
+ if (this.isHighValue(params)) {
40
+ // Layer 4 — quorum attestation over the SAME local slot.
41
+ if (this.quorum) {
42
+ try {
43
+ const attestations = await this.quorum.attestKeyUse(params, reservation.indices);
44
+ const certificate = {
45
+ reservationId: reservation.reservationId,
46
+ treeId: params.treeId,
47
+ branchId: params.branchId,
48
+ deviceId: params.deviceId,
49
+ indices: reservation.indices,
50
+ purpose: params.purpose,
51
+ payloadHash: params.payloadHash,
52
+ issuedBy: this.certificateSigner?.name ?? 'p2p-quorum',
53
+ issuedAt: Date.now(),
54
+ expiresAt: reservation.expiresAt,
55
+ signature: '',
56
+ attestations,
57
+ };
58
+ certificate.signature = this.certificateSigner
59
+ ? await (0, certificate_js_1.signCertificate)(this.certificateSigner, certificate)
60
+ : '';
61
+ return { ...reservation, certificate };
62
+ }
63
+ catch {
64
+ // Quorum unavailable — fall through to node / local only.
65
+ }
66
+ }
67
+ // Layer 3 — personal lookup node certificate.
68
+ if (this.node) {
69
+ try {
70
+ const nodeReservation = await this.node.reserveKeyUse(params);
71
+ return { ...reservation, certificate: nodeReservation.certificate };
72
+ }
73
+ catch {
74
+ // Node unavailable — continue with local only
75
+ }
76
+ }
77
+ }
78
+ return reservation;
79
+ }
80
+ commitKeyUse(reservationId, txId) {
81
+ return this.local.commitKeyUse(reservationId, txId);
82
+ }
83
+ burnReservation(reservationId, reason) {
84
+ return this.local.burnReservation(reservationId, reason);
85
+ }
86
+ getLocalWatermark(treeId) {
87
+ return this.local.getLocalWatermark(treeId);
88
+ }
89
+ async publishWatermark(treeId) {
90
+ // Anchor to the chain first (authoritative cross-instance cursor), then the
91
+ // personal node, then the local no-op.
92
+ if (this.onchain) {
93
+ try {
94
+ await this.onchain.publishWatermark(treeId);
95
+ return;
96
+ }
97
+ catch {
98
+ // fall through
99
+ }
100
+ }
101
+ if (this.node) {
102
+ try {
103
+ await this.node.publishWatermark(treeId);
104
+ return;
105
+ }
106
+ catch {
107
+ // fall through
108
+ }
109
+ }
110
+ await this.local.publishWatermark(treeId);
111
+ }
112
+ async syncLeaseJournal() {
113
+ // Read the authoritative on-chain cursor first so a restore/second instance
114
+ // advances past every leaf published by anyone holding this seed.
115
+ if (this.onchain) {
116
+ try {
117
+ return await this.onchain.syncLeaseJournal();
118
+ }
119
+ catch {
120
+ // fall through
121
+ }
122
+ }
123
+ if (this.node) {
124
+ try {
125
+ return await this.node.syncLeaseJournal();
126
+ }
127
+ catch {
128
+ // fall through
129
+ }
130
+ }
131
+ return this.local.syncLeaseJournal();
132
+ }
133
+ async verifyLeaseCertificate(cert) {
134
+ if (!cert)
135
+ return this.local.verifyLeaseCertificate(cert);
136
+ // Locally-assembled quorum certificates authenticate against our issuer.
137
+ if (cert.issuedBy === (this.certificateSigner?.name ?? 'p2p-quorum')) {
138
+ return (0, certificate_js_1.certificateSignatureVerified)(cert, this.certificateSigner);
139
+ }
140
+ if (this.node)
141
+ return this.node.verifyLeaseCertificate(cert);
142
+ return false;
143
+ }
144
+ }
145
+ exports.HybridLeaseProvider = HybridLeaseProvider;
@@ -0,0 +1,17 @@
1
+ export type { UnavailableReason, SigningIndices, LeaseStatus, TreeWatermark, WotsWatermarkState, LeaseCertificate, LeaseReservation, ReserveParams, LocalWatermark, WatermarkSeed, SeedWatermarkOptions, ConflictRecord, SyncResult, WotsLeaseProvider, JournalEntry, DeviceKeyRange, PersonalLeaseNodeConfig, QuorumAttestation, QuorumPeer, CertificateSigner, P2PQuorumLeaseProviderConfig, OnchainWatermarkProviderConfig, } from './types.js';
2
+ export { WotsWatermarkStore, flatIndex, fromFlatIndex, } from './watermark.js';
3
+ export { LeaseJournal } from './journal.js';
4
+ export { LocalLeaseProvider } from './local.js';
5
+ export { seedLeaseWatermark, exportLeaseWatermark } from './seed.js';
6
+ export type { SeedLeaseWatermarkParams } from './seed.js';
7
+ export { AxiaLeaseProvider } from './axia.js';
8
+ export type { AxiaLeaseProviderConfig } from './axia.js';
9
+ export { HybridLeaseProvider } from './hybrid.js';
10
+ export type { HybridLeaseProviderConfig } from './hybrid.js';
11
+ export { PersonalLeaseNodeProvider, P2PQuorumLeaseProvider, OnchainWatermarkProvider, } from './stubs.js';
12
+ export { P2PQuorumLeaseProvider as QuorumLeaseProvider } from './quorum.js';
13
+ export { OnchainWatermarkProvider as ChainWatermarkProvider } from './onchain.js';
14
+ export { allocateDeviceRange, deviceSlotForAddressIndex } from './device.js';
15
+ export { WatermarkMonotonicityError, WatermarkExhaustedError, WatermarkSeedRegressionError, WatermarkSeedRequiredError, LeaseNotFoundError, IndicesUnavailableError, PersonalLeaseNodeNotConfiguredError, P2PQuorumNotImplementedError, OnchainWatermarkNotImplementedError, QuorumUnavailableError, QuorumConflictError, OnchainWatermarkError, DeviceRangeViolationError, } from './errors.js';
16
+ export { ReplicatedLeaseJournalBackup } from './replicated-backup.js';
17
+ export type { LeaseJournalRestoreTarget } from './replicated-backup.js';
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ReplicatedLeaseJournalBackup = exports.DeviceRangeViolationError = exports.OnchainWatermarkError = exports.QuorumConflictError = exports.QuorumUnavailableError = exports.OnchainWatermarkNotImplementedError = exports.P2PQuorumNotImplementedError = exports.PersonalLeaseNodeNotConfiguredError = exports.IndicesUnavailableError = exports.LeaseNotFoundError = exports.WatermarkSeedRequiredError = exports.WatermarkSeedRegressionError = exports.WatermarkExhaustedError = exports.WatermarkMonotonicityError = exports.deviceSlotForAddressIndex = exports.allocateDeviceRange = exports.ChainWatermarkProvider = exports.QuorumLeaseProvider = exports.OnchainWatermarkProvider = exports.P2PQuorumLeaseProvider = exports.PersonalLeaseNodeProvider = exports.HybridLeaseProvider = exports.AxiaLeaseProvider = exports.exportLeaseWatermark = exports.seedLeaseWatermark = exports.LocalLeaseProvider = exports.LeaseJournal = exports.fromFlatIndex = exports.flatIndex = exports.WotsWatermarkStore = void 0;
4
+ var watermark_js_1 = require("./watermark.js");
5
+ Object.defineProperty(exports, "WotsWatermarkStore", { enumerable: true, get: function () { return watermark_js_1.WotsWatermarkStore; } });
6
+ Object.defineProperty(exports, "flatIndex", { enumerable: true, get: function () { return watermark_js_1.flatIndex; } });
7
+ Object.defineProperty(exports, "fromFlatIndex", { enumerable: true, get: function () { return watermark_js_1.fromFlatIndex; } });
8
+ var journal_js_1 = require("./journal.js");
9
+ Object.defineProperty(exports, "LeaseJournal", { enumerable: true, get: function () { return journal_js_1.LeaseJournal; } });
10
+ var local_js_1 = require("./local.js");
11
+ Object.defineProperty(exports, "LocalLeaseProvider", { enumerable: true, get: function () { return local_js_1.LocalLeaseProvider; } });
12
+ var seed_js_1 = require("./seed.js");
13
+ Object.defineProperty(exports, "seedLeaseWatermark", { enumerable: true, get: function () { return seed_js_1.seedLeaseWatermark; } });
14
+ Object.defineProperty(exports, "exportLeaseWatermark", { enumerable: true, get: function () { return seed_js_1.exportLeaseWatermark; } });
15
+ var axia_js_1 = require("./axia.js");
16
+ Object.defineProperty(exports, "AxiaLeaseProvider", { enumerable: true, get: function () { return axia_js_1.AxiaLeaseProvider; } });
17
+ var hybrid_js_1 = require("./hybrid.js");
18
+ Object.defineProperty(exports, "HybridLeaseProvider", { enumerable: true, get: function () { return hybrid_js_1.HybridLeaseProvider; } });
19
+ var stubs_js_1 = require("./stubs.js");
20
+ Object.defineProperty(exports, "PersonalLeaseNodeProvider", { enumerable: true, get: function () { return stubs_js_1.PersonalLeaseNodeProvider; } });
21
+ Object.defineProperty(exports, "P2PQuorumLeaseProvider", { enumerable: true, get: function () { return stubs_js_1.P2PQuorumLeaseProvider; } });
22
+ Object.defineProperty(exports, "OnchainWatermarkProvider", { enumerable: true, get: function () { return stubs_js_1.OnchainWatermarkProvider; } });
23
+ var quorum_js_1 = require("./quorum.js");
24
+ Object.defineProperty(exports, "QuorumLeaseProvider", { enumerable: true, get: function () { return quorum_js_1.P2PQuorumLeaseProvider; } });
25
+ var onchain_js_1 = require("./onchain.js");
26
+ Object.defineProperty(exports, "ChainWatermarkProvider", { enumerable: true, get: function () { return onchain_js_1.OnchainWatermarkProvider; } });
27
+ var device_js_1 = require("./device.js");
28
+ Object.defineProperty(exports, "allocateDeviceRange", { enumerable: true, get: function () { return device_js_1.allocateDeviceRange; } });
29
+ Object.defineProperty(exports, "deviceSlotForAddressIndex", { enumerable: true, get: function () { return device_js_1.deviceSlotForAddressIndex; } });
30
+ var errors_js_1 = require("./errors.js");
31
+ Object.defineProperty(exports, "WatermarkMonotonicityError", { enumerable: true, get: function () { return errors_js_1.WatermarkMonotonicityError; } });
32
+ Object.defineProperty(exports, "WatermarkExhaustedError", { enumerable: true, get: function () { return errors_js_1.WatermarkExhaustedError; } });
33
+ Object.defineProperty(exports, "WatermarkSeedRegressionError", { enumerable: true, get: function () { return errors_js_1.WatermarkSeedRegressionError; } });
34
+ Object.defineProperty(exports, "WatermarkSeedRequiredError", { enumerable: true, get: function () { return errors_js_1.WatermarkSeedRequiredError; } });
35
+ Object.defineProperty(exports, "LeaseNotFoundError", { enumerable: true, get: function () { return errors_js_1.LeaseNotFoundError; } });
36
+ Object.defineProperty(exports, "IndicesUnavailableError", { enumerable: true, get: function () { return errors_js_1.IndicesUnavailableError; } });
37
+ Object.defineProperty(exports, "PersonalLeaseNodeNotConfiguredError", { enumerable: true, get: function () { return errors_js_1.PersonalLeaseNodeNotConfiguredError; } });
38
+ Object.defineProperty(exports, "P2PQuorumNotImplementedError", { enumerable: true, get: function () { return errors_js_1.P2PQuorumNotImplementedError; } });
39
+ Object.defineProperty(exports, "OnchainWatermarkNotImplementedError", { enumerable: true, get: function () { return errors_js_1.OnchainWatermarkNotImplementedError; } });
40
+ Object.defineProperty(exports, "QuorumUnavailableError", { enumerable: true, get: function () { return errors_js_1.QuorumUnavailableError; } });
41
+ Object.defineProperty(exports, "QuorumConflictError", { enumerable: true, get: function () { return errors_js_1.QuorumConflictError; } });
42
+ Object.defineProperty(exports, "OnchainWatermarkError", { enumerable: true, get: function () { return errors_js_1.OnchainWatermarkError; } });
43
+ Object.defineProperty(exports, "DeviceRangeViolationError", { enumerable: true, get: function () { return errors_js_1.DeviceRangeViolationError; } });
44
+ var replicated_backup_js_1 = require("./replicated-backup.js");
45
+ Object.defineProperty(exports, "ReplicatedLeaseJournalBackup", { enumerable: true, get: function () { return replicated_backup_js_1.ReplicatedLeaseJournalBackup; } });
@@ -0,0 +1,30 @@
1
+ /**
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).
7
+ */
8
+ import type { StorageAdapter, LoggerAdapter } from '@totemsdk/core';
9
+ import type { JournalEntry } from './types.js';
10
+ export declare class LeaseJournal {
11
+ private readonly storage;
12
+ private readonly logger;
13
+ private entries;
14
+ private initialized;
15
+ private initializePromise;
16
+ private appendTail;
17
+ constructor(storage: StorageAdapter, logger?: LoggerAdapter);
18
+ initialize(): Promise<void>;
19
+ private initializeOnce;
20
+ private readLegacyEntries;
21
+ private readPhysicalRecords;
22
+ private writeRecord;
23
+ append(entry: JournalEntry): Promise<void>;
24
+ private appendOne;
25
+ private ensureInitialized;
26
+ getAll(): JournalEntry[];
27
+ getByTree(treeId: string): JournalEntry[];
28
+ getByReservation(reservationId: string): JournalEntry | undefined;
29
+ clear(): Promise<void>;
30
+ }
@@ -0,0 +1,369 @@
1
+ "use strict";
2
+ /**
3
+ * LeaseJournal — physically append-only audit log for WOTS key-use events.
4
+ *
5
+ * Layout v1 stores one immutable record per sequence. The legacy whole-array
6
+ * key is retained as migration input/forensic history but is never rewritten.
7
+ * The SHA-256 chain format is intentionally unchanged (RFC-007 OQ3).
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.LeaseJournal = void 0;
11
+ const sha256_js_1 = require("@noble/hashes/sha256.js");
12
+ const utils_js_1 = require("@noble/hashes/utils.js");
13
+ const core_1 = require("@totemsdk/core");
14
+ const errors_1 = require("@totemsdk/storage/errors");
15
+ const LEGACY_STORAGE_KEY = 'totem_wots_journal';
16
+ const ENTRY_PREFIX = 'totem_wots_journal:v1:entry:';
17
+ const RECORD_VERSION = 1;
18
+ const SEQUENCE_WIDTH = 16;
19
+ const MAX_INDEX_COMPONENT = 63;
20
+ const HASH_PATTERN = /^[0-9a-f]{64}$/;
21
+ const KEY_PATTERN = /^(\d{16}):([0-9a-f]{64})$/;
22
+ const STATUSES = new Set([
23
+ 'reserved',
24
+ 'committed',
25
+ 'burned',
26
+ 'reserved-expired',
27
+ ]);
28
+ const storageTails = new WeakMap();
29
+ function withStorageLock(storage, fn) {
30
+ const prior = storageTails.get(storage) ?? Promise.resolve();
31
+ const run = prior.then(fn, fn);
32
+ storageTails.set(storage, run.then(() => undefined, () => undefined));
33
+ return run;
34
+ }
35
+ /** Existing commitment format; property names/order and SHA-256 stay unchanged. */
36
+ function hashEntry(entry) {
37
+ const encoder = new TextEncoder();
38
+ const data = {
39
+ t: entry.treeId,
40
+ b: entry.branchId,
41
+ i: entry.wotsIndex,
42
+ s: entry.status,
43
+ r: entry.reservationId,
44
+ p: entry.previousHash,
45
+ ts: entry.timestamp,
46
+ d: entry.deviceId,
47
+ };
48
+ const bytes = encoder.encode(JSON.stringify(data));
49
+ return (0, utils_js_1.bytesToHex)((0, sha256_js_1.sha256)(bytes));
50
+ }
51
+ function cloneEntry(entry) {
52
+ return {
53
+ treeId: entry.treeId,
54
+ branchId: entry.branchId,
55
+ wotsIndex: entry.wotsIndex,
56
+ indices: { ...entry.indices },
57
+ status: entry.status,
58
+ ...(entry.reservationId !== undefined ? { reservationId: entry.reservationId } : {}),
59
+ ...(entry.payloadHash !== undefined ? { payloadHash: entry.payloadHash } : {}),
60
+ ...(entry.txId !== undefined ? { txId: entry.txId } : {}),
61
+ timestamp: entry.timestamp,
62
+ deviceId: entry.deviceId,
63
+ ...(entry.previousHash !== undefined ? { previousHash: entry.previousHash } : {}),
64
+ ...(entry.hash !== undefined ? { hash: entry.hash } : {}),
65
+ };
66
+ }
67
+ function asObject(value, key) {
68
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
69
+ throw new errors_1.StorageError(`LeaseJournal: invalid object at ${key}`, 'corrupt', { key });
70
+ }
71
+ return value;
72
+ }
73
+ function requiredString(object, field, key) {
74
+ const value = object[field];
75
+ if (typeof value !== 'string' || value.length === 0) {
76
+ throw new errors_1.StorageError(`LeaseJournal: invalid ${field} at ${key}`, 'corrupt', { key });
77
+ }
78
+ return value;
79
+ }
80
+ function optionalString(object, field, key) {
81
+ const value = object[field];
82
+ if (value === undefined)
83
+ return undefined;
84
+ if (typeof value !== 'string') {
85
+ throw new errors_1.StorageError(`LeaseJournal: invalid ${field} at ${key}`, 'corrupt', { key });
86
+ }
87
+ return value;
88
+ }
89
+ function requiredInteger(object, field, key) {
90
+ const value = object[field];
91
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
92
+ throw new errors_1.StorageError(`LeaseJournal: invalid ${field} at ${key}`, 'corrupt', { key });
93
+ }
94
+ return value;
95
+ }
96
+ function validateEntry(value, key) {
97
+ const object = asObject(value, key);
98
+ const indices = asObject(object.indices, `${key}.indices`);
99
+ const status = requiredString(object, 'status', key);
100
+ if (!STATUSES.has(status)) {
101
+ throw new errors_1.StorageError(`LeaseJournal: invalid status at ${key}`, 'corrupt', { key });
102
+ }
103
+ const timestamp = object.timestamp;
104
+ if (typeof timestamp !== 'number' || !Number.isFinite(timestamp) || timestamp < 0) {
105
+ throw new errors_1.StorageError(`LeaseJournal: invalid timestamp at ${key}`, 'corrupt', { key });
106
+ }
107
+ const validated = cloneEntry({
108
+ treeId: requiredString(object, 'treeId', key),
109
+ branchId: requiredString(object, 'branchId', key),
110
+ wotsIndex: requiredInteger(object, 'wotsIndex', key),
111
+ indices: {
112
+ addressIndex: requiredInteger(indices, 'addressIndex', `${key}.indices`),
113
+ l1: requiredInteger(indices, 'l1', `${key}.indices`),
114
+ l2: requiredInteger(indices, 'l2', `${key}.indices`),
115
+ },
116
+ status,
117
+ reservationId: optionalString(object, 'reservationId', key),
118
+ payloadHash: optionalString(object, 'payloadHash', key),
119
+ txId: optionalString(object, 'txId', key),
120
+ timestamp,
121
+ deviceId: requiredString(object, 'deviceId', key),
122
+ previousHash: optionalString(object, 'previousHash', key),
123
+ hash: optionalString(object, 'hash', key),
124
+ });
125
+ const { addressIndex, l1, l2 } = validated.indices;
126
+ if (addressIndex > MAX_INDEX_COMPONENT || l1 > MAX_INDEX_COMPONENT || l2 > MAX_INDEX_COMPONENT) {
127
+ throw new errors_1.StorageError(`LeaseJournal: indices out of range at ${key}`, 'corrupt', { key });
128
+ }
129
+ const expectedIndex = addressIndex * 64 * 64 + l1 * 64 + l2;
130
+ if (validated.wotsIndex !== expectedIndex) {
131
+ throw new errors_1.StorageError(`LeaseJournal: wotsIndex/indices mismatch at ${key}`, 'corrupt', { key });
132
+ }
133
+ return validated;
134
+ }
135
+ function verifyChain(entries, source) {
136
+ for (let i = 0; i < entries.length; i += 1) {
137
+ const entry = entries[i];
138
+ if (entry.hash === undefined || !HASH_PATTERN.test(entry.hash)) {
139
+ throw new errors_1.StorageError(`LeaseJournal: invalid hash at ${source}[${i}]`, 'corrupt');
140
+ }
141
+ const expectedHash = hashEntry(entry);
142
+ if (entry.hash !== expectedHash) {
143
+ throw new errors_1.StorageError(`LeaseJournal: hash mismatch at ${source}[${i}]`, 'corrupt');
144
+ }
145
+ if (i === 0) {
146
+ if (entry.previousHash !== undefined) {
147
+ throw new errors_1.StorageError(`LeaseJournal: first entry has previousHash at ${source}[0]`, 'corrupt');
148
+ }
149
+ }
150
+ else if (entry.previousHash !== entries[i - 1].hash) {
151
+ throw new errors_1.StorageError(`LeaseJournal: previousHash mismatch at ${source}[${i}]`, 'corrupt');
152
+ }
153
+ }
154
+ }
155
+ function entriesEqual(a, b) {
156
+ return a.treeId === b.treeId
157
+ && a.branchId === b.branchId
158
+ && a.wotsIndex === b.wotsIndex
159
+ && a.indices.addressIndex === b.indices.addressIndex
160
+ && a.indices.l1 === b.indices.l1
161
+ && a.indices.l2 === b.indices.l2
162
+ && a.status === b.status
163
+ && a.reservationId === b.reservationId
164
+ && a.payloadHash === b.payloadHash
165
+ && a.txId === b.txId
166
+ && a.timestamp === b.timestamp
167
+ && a.deviceId === b.deviceId
168
+ && a.previousHash === b.previousHash
169
+ && a.hash === b.hash;
170
+ }
171
+ function sequenceText(sequence) {
172
+ return sequence.toString(10).padStart(SEQUENCE_WIDTH, '0');
173
+ }
174
+ function recordKey(sequence, hash) {
175
+ return `${ENTRY_PREFIX}${sequenceText(sequence)}:${hash}`;
176
+ }
177
+ function parseRecordKey(key) {
178
+ const relative = key.slice(ENTRY_PREFIX.length);
179
+ const match = KEY_PATTERN.exec(relative);
180
+ if (!match) {
181
+ throw new errors_1.StorageError(`LeaseJournal: malformed record key ${key}`, 'corrupt', { key });
182
+ }
183
+ const sequence = Number(match[1]);
184
+ if (!Number.isSafeInteger(sequence)) {
185
+ throw new errors_1.StorageError(`LeaseJournal: invalid sequence in ${key}`, 'corrupt', { key });
186
+ }
187
+ return { key, sequence, hash: match[2] };
188
+ }
189
+ class LeaseJournal {
190
+ constructor(storage, logger = new core_1.NoopLogger()) {
191
+ this.storage = storage;
192
+ this.logger = logger;
193
+ this.entries = [];
194
+ this.initialized = false;
195
+ this.initializePromise = null;
196
+ this.appendTail = Promise.resolve();
197
+ }
198
+ async initialize() {
199
+ if (this.initialized)
200
+ return;
201
+ if (!this.initializePromise) {
202
+ this.initializePromise = withStorageLock(this.storage, () => this.initializeOnce()).catch((error) => {
203
+ this.initializePromise = null;
204
+ throw error;
205
+ });
206
+ }
207
+ await this.initializePromise;
208
+ }
209
+ async initializeOnce() {
210
+ const physical = await this.readPhysicalRecords();
211
+ const legacy = await this.readLegacyEntries();
212
+ if (legacy !== null) {
213
+ const commonLength = Math.min(physical.length, legacy.length);
214
+ for (let i = 0; i < commonLength; i += 1) {
215
+ if (!entriesEqual(physical[i], legacy[i])) {
216
+ throw new errors_1.StorageError(`LeaseJournal: legacy/v1 divergence at sequence ${i}`, 'corrupt');
217
+ }
218
+ }
219
+ for (let i = physical.length; i < legacy.length; i += 1) {
220
+ await this.writeRecord(i, legacy[i]);
221
+ }
222
+ }
223
+ const replayed = legacy !== null && legacy.length > physical.length
224
+ ? await this.readPhysicalRecords()
225
+ : physical;
226
+ this.entries = replayed.map(cloneEntry);
227
+ this.initialized = true;
228
+ }
229
+ async readLegacyEntries() {
230
+ const raw = await this.storage.get(LEGACY_STORAGE_KEY);
231
+ if (raw === null)
232
+ return null;
233
+ if (!Array.isArray(raw)) {
234
+ throw new errors_1.StorageError('LeaseJournal: legacy journal is not an array', 'corrupt', { key: LEGACY_STORAGE_KEY });
235
+ }
236
+ const entries = raw.map((entry, index) => validateEntry(entry, `${LEGACY_STORAGE_KEY}[${index}]`));
237
+ verifyChain(entries, LEGACY_STORAGE_KEY);
238
+ return entries;
239
+ }
240
+ async readPhysicalRecords() {
241
+ const parsed = (await this.storage.keys())
242
+ .filter((key) => key.startsWith(ENTRY_PREFIX))
243
+ .map(parseRecordKey)
244
+ .sort((a, b) => a.sequence - b.sequence || a.key.localeCompare(b.key));
245
+ for (let i = 0; i < parsed.length; i += 1) {
246
+ if (i > 0 && parsed[i - 1].sequence === parsed[i].sequence) {
247
+ throw new errors_1.StorageError(`LeaseJournal: fork at sequence ${parsed[i].sequence}`, 'corrupt');
248
+ }
249
+ if (parsed[i].sequence !== i) {
250
+ throw new errors_1.StorageError(`LeaseJournal: sequence gap at ${i}`, 'corrupt');
251
+ }
252
+ }
253
+ const entries = [];
254
+ for (const descriptor of parsed) {
255
+ const raw = await this.storage.get(descriptor.key);
256
+ if (raw === null) {
257
+ throw new errors_1.StorageError(`LeaseJournal: listed record is missing`, 'corrupt', { key: descriptor.key });
258
+ }
259
+ const record = asObject(raw, descriptor.key);
260
+ const formatVersion = record.formatVersion;
261
+ if (formatVersion !== RECORD_VERSION) {
262
+ throw new errors_1.StorageError('LeaseJournal: unsupported record version', 'corrupt', {
263
+ key: descriptor.key,
264
+ detectedVersion: typeof formatVersion === 'number' ? formatVersion : undefined,
265
+ unsupportedVersion: typeof formatVersion === 'number' ? formatVersion : undefined,
266
+ });
267
+ }
268
+ if (record.sequence !== descriptor.sequence) {
269
+ throw new errors_1.StorageError('LeaseJournal: sequence/key mismatch', 'corrupt', { key: descriptor.key });
270
+ }
271
+ const entry = validateEntry(record.entry, `${descriptor.key}.entry`);
272
+ if (entry.hash !== descriptor.hash) {
273
+ throw new errors_1.StorageError('LeaseJournal: hash/key mismatch', 'corrupt', { key: descriptor.key });
274
+ }
275
+ entries.push(entry);
276
+ }
277
+ verifyChain(entries, ENTRY_PREFIX);
278
+ return entries;
279
+ }
280
+ async writeRecord(sequence, entry) {
281
+ if (entry.hash === undefined) {
282
+ throw new errors_1.StorageError(`LeaseJournal: entry ${sequence} has no hash`, 'corrupt');
283
+ }
284
+ const key = recordKey(sequence, entry.hash);
285
+ const record = {
286
+ formatVersion: RECORD_VERSION,
287
+ sequence,
288
+ entry: cloneEntry(entry),
289
+ };
290
+ await this.storage.set(key, record);
291
+ }
292
+ append(entry) {
293
+ const run = this.appendTail.then(async () => this.appendOne(entry));
294
+ this.appendTail = run.catch(() => undefined);
295
+ return run;
296
+ }
297
+ async appendOne(input) {
298
+ await this.initialize();
299
+ await withStorageLock(this.storage, async () => {
300
+ // Refresh under the shared lock so multiple journal instances that share
301
+ // the same adapter object cannot allocate the same sequence or slot.
302
+ this.entries = (await this.readPhysicalRecords()).map(cloneEntry);
303
+ const entry = validateEntry(cloneEntry(input), 'append');
304
+ if (entry.status === 'reserved' && this.entries.some((existing) => existing.treeId === entry.treeId && existing.wotsIndex === entry.wotsIndex)) {
305
+ throw new errors_1.StorageError(`LeaseJournal: WOTS slot ${entry.treeId}/${entry.wotsIndex} is already recorded`, 'write-failed');
306
+ }
307
+ if (entry.status !== 'reserved' && entry.reservationId) {
308
+ const latest = [...this.entries].reverse().find((existing) => existing.reservationId === entry.reservationId);
309
+ if (latest?.status === entry.status) {
310
+ if (latest.treeId !== entry.treeId || latest.wotsIndex !== entry.wotsIndex
311
+ || (entry.status === 'committed' && latest.txId !== entry.txId)) {
312
+ throw new errors_1.StorageError(`LeaseJournal: reservation ${entry.reservationId} is already ${latest.status} for a different use`, 'write-failed');
313
+ }
314
+ if (latest.previousHash === undefined)
315
+ delete input.previousHash;
316
+ else
317
+ input.previousHash = latest.previousHash;
318
+ input.hash = latest.hash;
319
+ return;
320
+ }
321
+ if (latest && latest.status !== 'reserved') {
322
+ throw new errors_1.StorageError(`LeaseJournal: reservation ${entry.reservationId} is already ${latest.status}`, 'write-failed');
323
+ }
324
+ }
325
+ const previous = this.entries[this.entries.length - 1];
326
+ if (previous?.hash !== undefined)
327
+ entry.previousHash = previous.hash;
328
+ else
329
+ delete entry.previousHash;
330
+ entry.hash = hashEntry(entry);
331
+ const sequence = this.entries.length;
332
+ const sequencePrefix = `${ENTRY_PREFIX}${sequenceText(sequence)}:`;
333
+ if ((await this.storage.keys()).some((key) => key.startsWith(sequencePrefix))) {
334
+ throw new errors_1.StorageError(`LeaseJournal: sequence ${sequence} already exists`, 'write-failed');
335
+ }
336
+ await this.writeRecord(sequence, entry);
337
+ this.entries.push(entry);
338
+ if (entry.previousHash === undefined)
339
+ delete input.previousHash;
340
+ else
341
+ input.previousHash = entry.previousHash;
342
+ input.hash = entry.hash;
343
+ this.logger.debug(`[LeaseJournal] ${entry.status} treeId=${entry.treeId} idx=${entry.wotsIndex} `
344
+ + `txId=${entry.txId ?? '-'} hash=${entry.hash.slice(0, 8)}`);
345
+ });
346
+ }
347
+ ensureInitialized() {
348
+ if (!this.initialized) {
349
+ throw new Error('LeaseJournal not initialized — call initialize() first');
350
+ }
351
+ }
352
+ getAll() {
353
+ this.ensureInitialized();
354
+ return this.entries.map(cloneEntry);
355
+ }
356
+ getByTree(treeId) {
357
+ this.ensureInitialized();
358
+ return this.entries.filter((entry) => entry.treeId === treeId).map(cloneEntry);
359
+ }
360
+ getByReservation(reservationId) {
361
+ this.ensureInitialized();
362
+ const entry = [...this.entries].reverse().find((candidate) => candidate.reservationId === reservationId);
363
+ return entry ? cloneEntry(entry) : undefined;
364
+ }
365
+ async clear() {
366
+ throw new errors_1.StorageError('LeaseJournal is append-only and cannot be cleared', 'write-failed');
367
+ }
368
+ }
369
+ exports.LeaseJournal = LeaseJournal;