@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,307 @@
1
+ "use strict";
2
+ /**
3
+ * P2PQuorumLeaseProvider — Layer 4: multi-device quorum lease coordination.
4
+ *
5
+ * Safety model
6
+ * ────────────
7
+ * The local provider remains the authoritative watermark (it can only move
8
+ * forward). The quorum adds a second, independent check: before a reservation
9
+ * is handed to the caller, at least `minAttestations` peers must confirm that
10
+ * the same (treeId, indices) slot is free in *their* view of the world. This
11
+ * catches the failure mode where two devices share a seed and both believe
12
+ * they own the next slot — the peers see the double-reservation and refuse
13
+ * to attest.
14
+ *
15
+ * Wire protocol
16
+ * ─────────────
17
+ * Peers speak the same LEASE_RESERVE / LEASE_COMMIT / LEASE_BURN /
18
+ * LEASE_WATERMARK message shapes as the lookup protocol, so any peer that
19
+ * implements that protocol can participate: a personal lookup node, another
20
+ * device running this provider, or an in-memory test peer.
21
+ *
22
+ * Failure semantics
23
+ * ─────────────────
24
+ * - If quorum cannot be reached, the local reservation is burned and
25
+ * QuorumUnavailableError is thrown. The slot is NOT reusable (burned),
26
+ * which is the safe direction: a slot that might have been attested by
27
+ * some peers must never be handed out again.
28
+ * - If a peer reports the slot as taken, the local reservation is burned
29
+ * and QuorumConflictError is thrown.
30
+ */
31
+ Object.defineProperty(exports, "__esModule", { value: true });
32
+ exports.P2PQuorumLeaseProvider = void 0;
33
+ const errors_js_1 = require("./errors.js");
34
+ const certificate_js_1 = require("./certificate.js");
35
+ function sameIndices(a, b) {
36
+ return a.addressIndex === b.addressIndex && a.l1 === b.l1 && a.l2 === b.l2;
37
+ }
38
+ class P2PQuorumLeaseProvider {
39
+ constructor(config) {
40
+ /**
41
+ * localReservationId -> (peerId -> the reservation id that peer issued).
42
+ * Commit/burn must reference each peer's own id, because a peer only knows
43
+ * the reservation it issued, not the local provider's id (AUD-044).
44
+ */
45
+ this.peerReservationIds = new Map();
46
+ this._initialized = false;
47
+ this.peers = config.peers;
48
+ this.minAttestations = config.minAttestations ?? 1;
49
+ this.local = config.local;
50
+ this.requestTimeoutMs = config.requestTimeoutMs ?? 5000;
51
+ this.requireQuorumOnCommit = config.requireQuorumOnCommit ?? false;
52
+ this.certificateSigner = config.certificateSigner;
53
+ }
54
+ async initialize() {
55
+ if (this._initialized)
56
+ return;
57
+ await this.local.initialize();
58
+ this._initialized = true;
59
+ }
60
+ async ensureInit() {
61
+ if (!this._initialized)
62
+ await this.initialize();
63
+ }
64
+ // -------------------------------------------------------------------------
65
+ // Reserve — local first, then quorum attestation
66
+ // -------------------------------------------------------------------------
67
+ async reserveKeyUse(params) {
68
+ await this.ensureInit();
69
+ // 1. Local reservation (authoritative, monotonic watermark).
70
+ const reservation = await this.local.reserveKeyUse(params);
71
+ // 2. Collect attestations from peers for the SAME indices.
72
+ // On failure the local reservation is burned — the slot is never reused.
73
+ let attestations;
74
+ try {
75
+ attestations = await this.attestKeyUse(params, reservation.indices);
76
+ }
77
+ catch (err) {
78
+ const reason = err instanceof errors_js_1.QuorumConflictError
79
+ ? `quorum conflict: ${err.message}`
80
+ : 'quorum not reached';
81
+ await this.local.burnReservation(reservation.reservationId, reason);
82
+ throw err;
83
+ }
84
+ this.recordPeerReservationIds(reservation.reservationId, attestations);
85
+ const certificate = {
86
+ reservationId: reservation.reservationId,
87
+ treeId: params.treeId,
88
+ branchId: params.branchId,
89
+ deviceId: params.deviceId,
90
+ indices: reservation.indices,
91
+ purpose: params.purpose,
92
+ payloadHash: params.payloadHash,
93
+ issuedBy: this.certificateSigner?.name ?? 'p2p-quorum',
94
+ issuedAt: Date.now(),
95
+ expiresAt: reservation.expiresAt,
96
+ signature: '',
97
+ attestations,
98
+ };
99
+ certificate.signature = this.certificateSigner ? await (0, certificate_js_1.signCertificate)(this.certificateSigner, certificate) : '';
100
+ return { ...reservation, certificate };
101
+ }
102
+ /**
103
+ * Collect quorum attestations for a reservation that was already made
104
+ * locally (used by HybridLeaseProvider so the local slot and the attested
105
+ * slot are the same). Throws QuorumUnavailableError / QuorumConflictError
106
+ * on failure — the caller owns the local reservation and must burn it.
107
+ */
108
+ async attestKeyUse(params, indices) {
109
+ await this.ensureInit();
110
+ const attestations = [];
111
+ const conflicts = [];
112
+ await Promise.all(this.peers.map(async (peer) => {
113
+ try {
114
+ const resp = await peer.request({
115
+ type: 'LEASE_RESERVE',
116
+ payload: {
117
+ treeId: params.treeId,
118
+ branchId: params.branchId,
119
+ deviceId: params.deviceId,
120
+ ttlMs: params.ttlMs,
121
+ payloadHash: params.payloadHash,
122
+ purpose: params.purpose,
123
+ indices,
124
+ },
125
+ }, this.requestTimeoutMs);
126
+ if (resp.type === 'ERROR') {
127
+ conflicts.push(String(resp.payload.message ?? 'peer error'));
128
+ return;
129
+ }
130
+ const body = resp.payload;
131
+ const peerIndices = body.reservation?.indices;
132
+ if (!peerIndices || !sameIndices(peerIndices, indices)) {
133
+ conflicts.push('peer returned mismatched indices');
134
+ return;
135
+ }
136
+ const peerReservationId = body.reservation?.reservationId;
137
+ attestations.push({
138
+ peerId: peer.peerId,
139
+ indices,
140
+ expiresAt: body.reservation?.expiresAt ?? Date.now() + (params.ttlMs ?? 120000),
141
+ ...(typeof peerReservationId === 'string' && peerReservationId.length > 0
142
+ ? { peerReservationId }
143
+ : {}),
144
+ });
145
+ }
146
+ catch {
147
+ // Peer unreachable — counted as no attestation.
148
+ }
149
+ }));
150
+ if (conflicts.length > 0) {
151
+ throw new errors_js_1.QuorumConflictError(params.treeId, indices);
152
+ }
153
+ if (attestations.length < this.minAttestations) {
154
+ throw new errors_js_1.QuorumUnavailableError(this.minAttestations, attestations.length);
155
+ }
156
+ return attestations;
157
+ }
158
+ // -------------------------------------------------------------------------
159
+ // Commit / burn — local authoritative, broadcast to peers
160
+ // -------------------------------------------------------------------------
161
+ async commitKeyUse(reservationId, txId) {
162
+ await this.ensureInit();
163
+ await this.local.commitKeyUse(reservationId, txId);
164
+ const acks = await this.broadcastAck('LEASE_COMMIT', (peerId) => ({
165
+ reservationId: this.peerReservationIdFor(reservationId, peerId),
166
+ txId,
167
+ }));
168
+ if (this.requireQuorumOnCommit && acks < this.minAttestations) {
169
+ throw new errors_js_1.QuorumUnavailableError(this.minAttestations, acks);
170
+ }
171
+ }
172
+ async burnReservation(reservationId, reason) {
173
+ await this.ensureInit();
174
+ await this.local.burnReservation(reservationId, reason);
175
+ await this.broadcastAck('LEASE_BURN', (peerId) => ({
176
+ reservationId: this.peerReservationIdFor(reservationId, peerId),
177
+ reason,
178
+ }));
179
+ }
180
+ /** Record which peer issued which reservation id for a local reservation. */
181
+ recordPeerReservationIds(localReservationId, attestations) {
182
+ const byPeer = new Map();
183
+ for (const attestation of attestations) {
184
+ if (attestation.peerReservationId) {
185
+ byPeer.set(attestation.peerId, attestation.peerReservationId);
186
+ }
187
+ }
188
+ if (byPeer.size > 0)
189
+ this.peerReservationIds.set(localReservationId, byPeer);
190
+ }
191
+ /**
192
+ * The id to send a peer for a local reservation. Peers only know the id they
193
+ * issued; fall back to the local id only when no peer id was recorded (e.g.
194
+ * attestations gathered outside `reserveKeyUse`).
195
+ */
196
+ peerReservationIdFor(localReservationId, peerId) {
197
+ return this.peerReservationIds.get(localReservationId)?.get(peerId) ?? localReservationId;
198
+ }
199
+ async broadcastAck(type, payloadFor) {
200
+ let acks = 0;
201
+ await Promise.all(this.peers.map(async (peer) => {
202
+ try {
203
+ const resp = await peer.request({ type, payload: payloadFor(peer.peerId) }, this.requestTimeoutMs);
204
+ if (resp.type !== 'ERROR')
205
+ acks++;
206
+ }
207
+ catch {
208
+ // Peer unreachable — no ack.
209
+ }
210
+ }));
211
+ return acks;
212
+ }
213
+ // -------------------------------------------------------------------------
214
+ // Watermark / journal
215
+ // -------------------------------------------------------------------------
216
+ async getLocalWatermark(treeId) {
217
+ await this.ensureInit();
218
+ return this.local.getLocalWatermark(treeId);
219
+ }
220
+ async publishWatermark(treeId) {
221
+ await this.ensureInit();
222
+ const wm = await this.local.getLocalWatermark(treeId);
223
+ await Promise.all(this.peers.map(async (peer) => {
224
+ try {
225
+ await peer.request({
226
+ type: 'LEASE_WATERMARK',
227
+ payload: {
228
+ treeId,
229
+ addressCursor: wm.addressCursor,
230
+ l1Cursor: wm.l1Cursor,
231
+ l2Cursor: wm.l2Cursor,
232
+ unavailableCount: wm.unavailableCount,
233
+ lastSyncTimestamp: wm.lastSyncTimestamp ?? Date.now(),
234
+ },
235
+ }, this.requestTimeoutMs);
236
+ }
237
+ catch {
238
+ // Peer unreachable — watermark publish is best-effort.
239
+ }
240
+ }));
241
+ }
242
+ async syncLeaseJournal() {
243
+ await this.ensureInit();
244
+ const conflicts = [];
245
+ let advancedTo;
246
+ for (const treeId of this.local.listTrees()) {
247
+ const localWm = await this.local.getLocalWatermark(treeId);
248
+ for (const peer of this.peers) {
249
+ try {
250
+ const resp = await peer.request({
251
+ type: 'LEASE_WATERMARK',
252
+ payload: { treeId, query: true },
253
+ }, this.requestTimeoutMs);
254
+ if (resp.type === 'ERROR')
255
+ continue;
256
+ const p = resp.payload;
257
+ if (typeof p.addressCursor !== 'number')
258
+ continue;
259
+ const remoteFlat = p.addressCursor * 64 * 64 + p.l1Cursor * 64 + p.l2Cursor;
260
+ const localFlat = localWm.addressCursor * 64 * 64 + localWm.l1Cursor * 64 + localWm.l2Cursor;
261
+ if (remoteFlat > localFlat) {
262
+ const advanced = await this.local.advanceToRemoteWatermark(treeId, {
263
+ addressCursor: p.addressCursor,
264
+ l1Cursor: p.l1Cursor,
265
+ l2Cursor: p.l2Cursor,
266
+ });
267
+ if (advanced) {
268
+ advancedTo = {
269
+ addressIndex: p.addressCursor,
270
+ l1: p.l1Cursor,
271
+ l2: p.l2Cursor,
272
+ };
273
+ }
274
+ }
275
+ else if (remoteFlat < localFlat) {
276
+ conflicts.push({
277
+ treeId,
278
+ localIndex: localFlat,
279
+ remoteIndex: remoteFlat,
280
+ timestamp: Date.now(),
281
+ });
282
+ }
283
+ }
284
+ catch {
285
+ // Peer unreachable — skip.
286
+ }
287
+ }
288
+ }
289
+ return { synced: conflicts.length === 0, conflicts, advancedTo };
290
+ }
291
+ async verifyLeaseCertificate(cert) {
292
+ if (!cert)
293
+ return false;
294
+ if (cert.expiresAt <= Date.now())
295
+ return false;
296
+ // An unsigned certificate carries no authenticity evidence.
297
+ if (!(await (0, certificate_js_1.certificateSignatureVerified)(cert, this.certificateSigner))) {
298
+ return false;
299
+ }
300
+ const attestations = cert.attestations ?? [];
301
+ if (attestations.length < this.minAttestations)
302
+ return false;
303
+ return attestations.every((a) => sameIndices(a.indices, cert.indices) &&
304
+ a.expiresAt > Date.now());
305
+ }
306
+ }
307
+ exports.P2PQuorumLeaseProvider = P2PQuorumLeaseProvider;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * RFC-057 — replicated WOTS lease-journal backup.
3
+ *
4
+ * The local `LeaseJournal` remains authoritative for allocation. This adapter
5
+ * mirrors the physically append-only journal into a replicated log for audit
6
+ * and disaster recovery, and restores entries into a local journal in order.
7
+ * It never allocates or advances a signing slot.
8
+ */
9
+ import type { ReplicatedLog } from '@totemsdk/replication';
10
+ import type { JournalEntry } from './types.js';
11
+ export interface LeaseJournalRestoreTarget {
12
+ append(entry: JournalEntry): Promise<void>;
13
+ }
14
+ export declare class ReplicatedLeaseJournalBackup {
15
+ private readonly log;
16
+ constructor(log: ReplicatedLog<JournalEntry>);
17
+ /** Mirror one local journal entry. */
18
+ append(entry: JournalEntry): Promise<{
19
+ length: number;
20
+ head: string;
21
+ }>;
22
+ /** Read all mirrored entries, verifying the local hash-chain links in order. */
23
+ readAll(): Promise<JournalEntry[]>;
24
+ /**
25
+ * Restore into a local journal. The local journal's own duplicate-slot and
26
+ * reservation checks remain the final guard; this method only preserves the
27
+ * replicated order and links.
28
+ */
29
+ restoreInto(target: LeaseJournalRestoreTarget): Promise<number>;
30
+ }
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ /**
3
+ * RFC-057 — replicated WOTS lease-journal backup.
4
+ *
5
+ * The local `LeaseJournal` remains authoritative for allocation. This adapter
6
+ * mirrors the physically append-only journal into a replicated log for audit
7
+ * and disaster recovery, and restores entries into a local journal in order.
8
+ * It never allocates or advances a signing slot.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.ReplicatedLeaseJournalBackup = void 0;
12
+ const errors_1 = require("@totemsdk/storage/errors");
13
+ class ReplicatedLeaseJournalBackup {
14
+ constructor(log) {
15
+ this.log = log;
16
+ }
17
+ /** Mirror one local journal entry. */
18
+ async append(entry) {
19
+ return this.log.append(entry);
20
+ }
21
+ /** Read all mirrored entries, verifying the local hash-chain links in order. */
22
+ async readAll() {
23
+ const entries = [];
24
+ for await (const entry of this.log.readSince())
25
+ entries.push(entry.record);
26
+ let previous;
27
+ for (const [index, entry] of entries.entries()) {
28
+ if (index === 0) {
29
+ if (entry.previousHash !== undefined) {
30
+ throw new errors_1.StorageError('replicated lease journal first entry has previousHash', 'corrupt');
31
+ }
32
+ }
33
+ else if (entry.previousHash !== previous?.hash) {
34
+ throw new errors_1.StorageError(`replicated lease journal chain mismatch at ${index}`, 'corrupt');
35
+ }
36
+ previous = entry;
37
+ }
38
+ return entries;
39
+ }
40
+ /**
41
+ * Restore into a local journal. The local journal's own duplicate-slot and
42
+ * reservation checks remain the final guard; this method only preserves the
43
+ * replicated order and links.
44
+ */
45
+ async restoreInto(target) {
46
+ const entries = await this.readAll();
47
+ for (const entry of entries)
48
+ await target.append(entry);
49
+ return entries.length;
50
+ }
51
+ }
52
+ exports.ReplicatedLeaseJournalBackup = ReplicatedLeaseJournalBackup;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @module @totemsdk/wots-lease/seed
3
+ *
4
+ * Watermark seeding for import/restore (RFC-013 §9). The safety property is
5
+ * "never reissue a used leaf". A local watermark alone is insufficient across
6
+ * devices/restores, so seeding draws from, in priority order:
7
+ *
8
+ * 1. the on-chain watermark cursor (advances the local watermark),
9
+ * 2. an exported watermark blob,
10
+ * 3. an operator-provided high-water mark.
11
+ *
12
+ * If none is available and the caller has not declared the keyspace fresh,
13
+ * seeding **fails closed** — starting at 0 would reissue spent leaves.
14
+ */
15
+ import type { LocalLeaseProvider } from './local.js';
16
+ import type { OnchainWatermarkProvider } from './onchain.js';
17
+ import type { LocalWatermark, WatermarkSeed } from './types.js';
18
+ export interface SeedLeaseWatermarkParams {
19
+ readonly local: LocalLeaseProvider;
20
+ readonly treeId: string;
21
+ /** Layer 5 — the on-chain cursor is authoritative when reachable. */
22
+ readonly onchain?: OnchainWatermarkProvider;
23
+ /** Exported watermark blob. */
24
+ readonly exported?: WatermarkSeed;
25
+ /** Operator-provided flat high-water mark. */
26
+ readonly operatorHighWaterMark?: number;
27
+ /** Declares the keyspace fresh/unspent, so cursor 0 is safe. */
28
+ readonly fresh?: boolean;
29
+ }
30
+ /**
31
+ * Seed a local watermark from the best available source, requiring at least one
32
+ * source unless `fresh` is set. Returns the resulting local watermark.
33
+ *
34
+ * @throws {WatermarkSeedRequiredError} when no source is available and the
35
+ * keyspace is not declared fresh.
36
+ * @throws {WatermarkSeedRegressionError} when a supplied seed is behind the
37
+ * current cursor.
38
+ */
39
+ export declare function seedLeaseWatermark(params: SeedLeaseWatermarkParams): Promise<LocalWatermark>;
40
+ /** Export a portable watermark blob for backup/restore. */
41
+ export declare function exportLeaseWatermark(local: LocalLeaseProvider, treeId: string): Promise<WatermarkSeed>;
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ /**
3
+ * @module @totemsdk/wots-lease/seed
4
+ *
5
+ * Watermark seeding for import/restore (RFC-013 §9). The safety property is
6
+ * "never reissue a used leaf". A local watermark alone is insufficient across
7
+ * devices/restores, so seeding draws from, in priority order:
8
+ *
9
+ * 1. the on-chain watermark cursor (advances the local watermark),
10
+ * 2. an exported watermark blob,
11
+ * 3. an operator-provided high-water mark.
12
+ *
13
+ * If none is available and the caller has not declared the keyspace fresh,
14
+ * seeding **fails closed** — starting at 0 would reissue spent leaves.
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.seedLeaseWatermark = seedLeaseWatermark;
18
+ exports.exportLeaseWatermark = exportLeaseWatermark;
19
+ const watermark_js_1 = require("./watermark.js");
20
+ const errors_js_1 = require("./errors.js");
21
+ function flatOf(w) {
22
+ return (0, watermark_js_1.flatIndex)({ addressIndex: w.addressCursor, l1: w.l1Cursor, l2: w.l2Cursor });
23
+ }
24
+ /**
25
+ * Seed a local watermark from the best available source, requiring at least one
26
+ * source unless `fresh` is set. Returns the resulting local watermark.
27
+ *
28
+ * @throws {WatermarkSeedRequiredError} when no source is available and the
29
+ * keyspace is not declared fresh.
30
+ * @throws {WatermarkSeedRegressionError} when a supplied seed is behind the
31
+ * current cursor.
32
+ */
33
+ async function seedLeaseWatermark(params) {
34
+ const { local, treeId } = params;
35
+ // 1. On-chain cursor — advances the same local watermark (monotonic).
36
+ if (params.onchain) {
37
+ try {
38
+ await params.onchain.syncLeaseJournal();
39
+ }
40
+ catch {
41
+ // Chain unreachable — fall through to exported/operator sources below.
42
+ }
43
+ }
44
+ const current = await local.getLocalWatermark(treeId);
45
+ let bestFlat = flatOf({ addressCursor: current.addressCursor, l1Cursor: current.l1Cursor, l2Cursor: current.l2Cursor });
46
+ let best;
47
+ // 2. Exported blob.
48
+ if (params.exported) {
49
+ if (flatOf(params.exported) > bestFlat) {
50
+ best = params.exported;
51
+ bestFlat = flatOf(params.exported);
52
+ }
53
+ }
54
+ // 3. Operator high-water mark.
55
+ if (params.operatorHighWaterMark !== undefined) {
56
+ const indices = (0, watermark_js_1.fromFlatIndex)(params.operatorHighWaterMark);
57
+ const candidate = {
58
+ version: 1,
59
+ treeId,
60
+ addressCursor: indices.addressIndex,
61
+ l1Cursor: indices.l1,
62
+ l2Cursor: indices.l2,
63
+ unavailable: [],
64
+ };
65
+ if (flatOf(candidate) > bestFlat) {
66
+ best = candidate;
67
+ bestFlat = flatOf(candidate);
68
+ }
69
+ }
70
+ if (best) {
71
+ await local.seedWatermark(best);
72
+ }
73
+ if (!params.onchain &&
74
+ !params.exported &&
75
+ params.operatorHighWaterMark === undefined &&
76
+ !params.fresh) {
77
+ throw new errors_js_1.WatermarkSeedRequiredError(treeId);
78
+ }
79
+ return local.getLocalWatermark(treeId);
80
+ }
81
+ /** Export a portable watermark blob for backup/restore. */
82
+ function exportLeaseWatermark(local, treeId) {
83
+ return local.exportWatermark(treeId);
84
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Implementations for Layers 3–5 of the WotsLeaseProvider hierarchy.
3
+ *
4
+ * Layer 3 (PersonalLeaseNodeProvider) — HTTP client for a personal @totemsdk/lookup-node
5
+ * that has lease coordination enabled ({ lease: { enabled: true } }).
6
+ * The node exposes REST endpoints under /v1/lease/*.
7
+ *
8
+ * Layers 4–5 (P2PQuorumLeaseProvider, OnchainWatermarkProvider) — implemented in
9
+ * quorum.ts and onchain.ts; re-exported here for backward compatibility.
10
+ */
11
+ import type { WotsLeaseProvider, ReserveParams, LeaseReservation, LeaseCertificate, LocalWatermark, SyncResult, PersonalLeaseNodeConfig } from './types.js';
12
+ export { P2PQuorumLeaseProvider } from './quorum.js';
13
+ export { OnchainWatermarkProvider } from './onchain.js';
14
+ /**
15
+ * Layer 3 — personal lookup-node lease coordinator.
16
+ *
17
+ * Calls the HTTP REST API exposed by a running @totemsdk/lookup-node that has
18
+ * lease coordination enabled. All write operations are forwarded to the node's
19
+ * LeaseCoordinator so the node acts as the source-of-truth watermark journal
20
+ * for high-value transactions.
21
+ *
22
+ * Wrap with HybridLeaseProvider so local reservations always succeed even when
23
+ * the personal node is temporarily unreachable:
24
+ *
25
+ * ```ts
26
+ * const provider = new HybridLeaseProvider({
27
+ * local: new LocalLeaseProvider(storage),
28
+ * node: new PersonalLeaseNodeProvider({ nodeUrl, nodePubkey }),
29
+ * threshold: 10, // escalate to node for txns >= 10 MIN
30
+ * });
31
+ * ```
32
+ */
33
+ export declare class PersonalLeaseNodeProvider implements WotsLeaseProvider {
34
+ private readonly baseUrl;
35
+ private readonly nodePubkey;
36
+ private readonly authToken?;
37
+ private readonly certificateSigner?;
38
+ private readonly verify?;
39
+ constructor(config: PersonalLeaseNodeConfig);
40
+ private headers;
41
+ private post;
42
+ private get;
43
+ reserveKeyUse(params: ReserveParams): Promise<LeaseReservation>;
44
+ commitKeyUse(reservationId: string, txId: string): Promise<void>;
45
+ burnReservation(reservationId: string, reason: string): Promise<void>;
46
+ getLocalWatermark(treeId: string): Promise<LocalWatermark>;
47
+ publishWatermark(treeId: string): Promise<void>;
48
+ syncLeaseJournal(): Promise<SyncResult>;
49
+ verifyLeaseCertificate(cert?: LeaseCertificate): Promise<boolean>;
50
+ }