@totemsdk/wots-lease 1.0.1 → 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 (55) hide show
  1. package/LICENSE +202 -21
  2. package/NOTICE +60 -0
  3. package/dist/axia.js +21 -25
  4. package/dist/certificate.js +7 -12
  5. package/dist/cjs/axia.d.ts +33 -0
  6. package/dist/cjs/axia.js +145 -0
  7. package/dist/cjs/certificate.d.ts +37 -0
  8. package/dist/cjs/certificate.js +74 -0
  9. package/dist/cjs/device.d.ts +18 -0
  10. package/dist/cjs/device.js +34 -0
  11. package/dist/cjs/errors.d.ts +71 -0
  12. package/dist/cjs/errors.js +135 -0
  13. package/dist/cjs/hybrid.d.ts +37 -0
  14. package/dist/cjs/hybrid.js +145 -0
  15. package/dist/cjs/index.d.ts +17 -0
  16. package/dist/cjs/index.js +45 -0
  17. package/dist/cjs/journal.d.ts +30 -0
  18. package/dist/cjs/journal.js +369 -0
  19. package/dist/cjs/local.d.ts +83 -0
  20. package/dist/cjs/local.js +440 -0
  21. package/dist/cjs/onchain.d.ts +54 -0
  22. package/dist/cjs/onchain.js +314 -0
  23. package/dist/cjs/package.json +1 -0
  24. package/dist/cjs/quorum.d.ts +75 -0
  25. package/dist/cjs/quorum.js +307 -0
  26. package/dist/cjs/replicated-backup.d.ts +30 -0
  27. package/dist/cjs/replicated-backup.js +52 -0
  28. package/dist/cjs/seed.d.ts +41 -0
  29. package/dist/cjs/seed.js +84 -0
  30. package/dist/cjs/stubs.d.ts +50 -0
  31. package/dist/cjs/stubs.js +125 -0
  32. package/dist/cjs/types.d.ts +272 -0
  33. package/dist/cjs/types.js +5 -0
  34. package/dist/cjs/watermark.d.ts +51 -0
  35. package/dist/cjs/watermark.js +328 -0
  36. package/dist/device.js +2 -6
  37. package/dist/errors.js +14 -31
  38. package/dist/hybrid.js +4 -8
  39. package/dist/index.d.ts +2 -0
  40. package/dist/index.js +12 -43
  41. package/dist/journal.js +35 -38
  42. package/dist/local.js +29 -33
  43. package/dist/onchain.js +23 -27
  44. package/dist/quorum.js +9 -13
  45. package/dist/replicated-backup.d.ts +30 -0
  46. package/dist/replicated-backup.js +48 -0
  47. package/dist/seed.js +7 -11
  48. package/dist/stubs.js +5 -11
  49. package/dist/types.js +1 -2
  50. package/dist/watermark.js +11 -18
  51. package/package.json +22 -13
  52. package/rust/pkg/package.json +2 -2
  53. package/rust/pkg/wots_lease_wasm_bg.wasm +0 -0
  54. package/rust/pkg-node/package.json +2 -2
  55. package/rust/pkg-node/wots_lease_wasm_bg.wasm +0 -0
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ /**
3
+ * Implementations for Layers 3–5 of the WotsLeaseProvider hierarchy.
4
+ *
5
+ * Layer 3 (PersonalLeaseNodeProvider) — HTTP client for a personal @totemsdk/lookup-node
6
+ * that has lease coordination enabled ({ lease: { enabled: true } }).
7
+ * The node exposes REST endpoints under /v1/lease/*.
8
+ *
9
+ * Layers 4–5 (P2PQuorumLeaseProvider, OnchainWatermarkProvider) — implemented in
10
+ * quorum.ts and onchain.ts; re-exported here for backward compatibility.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.PersonalLeaseNodeProvider = exports.OnchainWatermarkProvider = exports.P2PQuorumLeaseProvider = void 0;
14
+ const certificate_js_1 = require("./certificate.js");
15
+ var quorum_js_1 = require("./quorum.js");
16
+ Object.defineProperty(exports, "P2PQuorumLeaseProvider", { enumerable: true, get: function () { return quorum_js_1.P2PQuorumLeaseProvider; } });
17
+ var onchain_js_1 = require("./onchain.js");
18
+ Object.defineProperty(exports, "OnchainWatermarkProvider", { enumerable: true, get: function () { return onchain_js_1.OnchainWatermarkProvider; } });
19
+ /**
20
+ * Layer 3 — personal lookup-node lease coordinator.
21
+ *
22
+ * Calls the HTTP REST API exposed by a running @totemsdk/lookup-node that has
23
+ * lease coordination enabled. All write operations are forwarded to the node's
24
+ * LeaseCoordinator so the node acts as the source-of-truth watermark journal
25
+ * for high-value transactions.
26
+ *
27
+ * Wrap with HybridLeaseProvider so local reservations always succeed even when
28
+ * the personal node is temporarily unreachable:
29
+ *
30
+ * ```ts
31
+ * const provider = new HybridLeaseProvider({
32
+ * local: new LocalLeaseProvider(storage),
33
+ * node: new PersonalLeaseNodeProvider({ nodeUrl, nodePubkey }),
34
+ * threshold: 10, // escalate to node for txns >= 10 MIN
35
+ * });
36
+ * ```
37
+ */
38
+ class PersonalLeaseNodeProvider {
39
+ constructor(config) {
40
+ this.baseUrl = config.nodeUrl.replace(/\/$/, '');
41
+ this.nodePubkey = config.nodePubkey;
42
+ this.authToken = config.authToken;
43
+ this.certificateSigner = config.certificateSigner;
44
+ this.verify = config.verify;
45
+ }
46
+ headers() {
47
+ const h = { 'Content-Type': 'application/json' };
48
+ if (this.authToken)
49
+ h['Authorization'] = `Bearer ${this.authToken}`;
50
+ return h;
51
+ }
52
+ async post(path, body) {
53
+ const res = await fetch(`${this.baseUrl}${path}`, {
54
+ method: 'POST',
55
+ headers: this.headers(),
56
+ body: JSON.stringify(body),
57
+ });
58
+ if (!res.ok) {
59
+ const text = await res.text().catch(() => '');
60
+ throw new Error(`PersonalLeaseNodeProvider: ${path} HTTP ${res.status} — ${text.slice(0, 200)}`);
61
+ }
62
+ return res.json();
63
+ }
64
+ async get(path) {
65
+ const res = await fetch(`${this.baseUrl}${path}`, { headers: this.headers() });
66
+ if (!res.ok) {
67
+ const text = await res.text().catch(() => '');
68
+ throw new Error(`PersonalLeaseNodeProvider: GET ${path} HTTP ${res.status} — ${text.slice(0, 200)}`);
69
+ }
70
+ return res.json();
71
+ }
72
+ async reserveKeyUse(params) {
73
+ const data = await this.post('/v1/lease/reserve', {
74
+ treeId: params.treeId,
75
+ branchId: params.branchId,
76
+ deviceId: params.deviceId,
77
+ ttlMs: params.ttlMs,
78
+ payloadHash: params.payloadHash,
79
+ purpose: params.purpose,
80
+ });
81
+ return { ...data.reservation, certificate: data.certificate };
82
+ }
83
+ async commitKeyUse(reservationId, txId) {
84
+ const data = await this.post('/v1/lease/commit', { reservationId, txId });
85
+ if (!data.ok)
86
+ throw new Error(`PersonalLeaseNodeProvider: commit failed — ${data.error ?? 'unknown'}`);
87
+ }
88
+ async burnReservation(reservationId, reason) {
89
+ const data = await this.post('/v1/lease/burn', { reservationId, reason });
90
+ if (!data.ok)
91
+ throw new Error(`PersonalLeaseNodeProvider: burn failed — ${data.error ?? 'unknown'}`);
92
+ }
93
+ async getLocalWatermark(treeId) {
94
+ return this.get(`/v1/lease/watermark/${encodeURIComponent(treeId)}`);
95
+ }
96
+ async publishWatermark(treeId) {
97
+ await this.post(`/v1/lease/watermark/${encodeURIComponent(treeId)}/publish`, {});
98
+ }
99
+ async syncLeaseJournal() {
100
+ return this.get('/v1/lease/journal/sync');
101
+ }
102
+ async verifyLeaseCertificate(cert) {
103
+ if (!cert)
104
+ return false;
105
+ if (cert.issuedBy !== this.nodePubkey)
106
+ return false;
107
+ if (cert.expiresAt <= Date.now())
108
+ return false;
109
+ // AUD-016: fail closed. The provider can only verify a certificate when it
110
+ // holds a verifier; an `issuedBy` match is not authenticity evidence.
111
+ const signer = this.certificateSigner?.verify
112
+ ? this.certificateSigner
113
+ : this.verify
114
+ ? {
115
+ publicKeyDigest: this.nodePubkey,
116
+ sign: async () => {
117
+ throw new Error('PersonalLeaseNodeProvider holds a verify-only signer and cannot sign');
118
+ },
119
+ verify: this.verify,
120
+ }
121
+ : undefined;
122
+ return (0, certificate_js_1.certificateSignatureVerified)(cert, signer);
123
+ }
124
+ }
125
+ exports.PersonalLeaseNodeProvider = PersonalLeaseNodeProvider;
@@ -0,0 +1,272 @@
1
+ /**
2
+ * @totemsdk/wots-lease — canonical v3 watermark types and WotsLeaseProvider interface
3
+ */
4
+ import type { LocalLeaseProvider } from './local.js';
5
+ import type { StorageAdapter } from '@totemsdk/core';
6
+ export type UnavailableReason = 'reserved' | 'committed' | 'burned' | 'reserved-expired';
7
+ export interface SigningIndices {
8
+ addressIndex: number;
9
+ l1: number;
10
+ l2: number;
11
+ }
12
+ export type LeaseStatus = 'pending' | 'active' | 'expired' | 'finalized' | 'cancelled';
13
+ export interface TreeWatermark {
14
+ treeId: string;
15
+ deviceId?: string;
16
+ branchId?: string;
17
+ addressCursor: number;
18
+ l1Cursor: number;
19
+ l2Cursor: number;
20
+ unavailable: Record<number, UnavailableReason>;
21
+ lastSyncTimestamp?: number;
22
+ }
23
+ export interface WotsWatermarkState {
24
+ version: 3;
25
+ trees: Record<string, TreeWatermark>;
26
+ }
27
+ export interface QuorumAttestation {
28
+ peerId: string;
29
+ indices: SigningIndices;
30
+ expiresAt: number;
31
+ signature?: string;
32
+ /**
33
+ * The reservation id the peer issued for these indices in its own journal.
34
+ * Commit/burn broadcasts must reference the peer's id, not the local one
35
+ * (AUD-044) — each peer only knows the reservation it issued.
36
+ */
37
+ peerReservationId?: string;
38
+ }
39
+ /**
40
+ * Identity used to authenticate lease certificates (Layer 4/5).
41
+ *
42
+ * A certificate's `signature` is a signature over the canonical certificate
43
+ * payload (see certificate.ts). Verifiers reject unsigned certificates.
44
+ */
45
+ export interface CertificateSigner {
46
+ /** Hex (0x-prefixed or bare) public key digest of the issuing identity. */
47
+ publicKeyDigest: string;
48
+ /** Identity label stored on issued certificates via `issuedBy`. */
49
+ name?: string;
50
+ /** Sign the canonical certificate message. */
51
+ sign(message: Uint8Array): Promise<Uint8Array>;
52
+ /** Verify a signature over the canonical certificate message. */
53
+ verify?(message: Uint8Array, signature: Uint8Array): Promise<boolean>;
54
+ }
55
+ export interface LeaseCertificate {
56
+ reservationId: string;
57
+ treeId: string;
58
+ branchId?: string;
59
+ deviceId?: string;
60
+ indices: SigningIndices;
61
+ purpose?: string;
62
+ payloadHash?: string;
63
+ issuedBy: string;
64
+ issuedAt: number;
65
+ expiresAt: number;
66
+ signature: string;
67
+ /** Layer 4 — quorum attestations collected from P2P peers. */
68
+ attestations?: QuorumAttestation[];
69
+ /** Layer 5 — content hash of the on-chain watermark TX (sha3-256 of TxPoW bytes). */
70
+ txpowid?: string;
71
+ }
72
+ export interface LeaseReservation {
73
+ reservationId: string;
74
+ indices: SigningIndices;
75
+ expiresAt: number;
76
+ certificate?: LeaseCertificate;
77
+ leaseToken?: string;
78
+ }
79
+ export interface ReserveParams {
80
+ treeId: string;
81
+ branchId?: string;
82
+ purpose?: string;
83
+ deviceId?: string;
84
+ ttlMs?: number;
85
+ payloadHash?: string;
86
+ valueHint?: string;
87
+ }
88
+ export interface LocalWatermark {
89
+ treeId: string;
90
+ addressCursor: number;
91
+ l1Cursor: number;
92
+ l2Cursor: number;
93
+ unavailableCount: number;
94
+ capacity: number;
95
+ lastSyncTimestamp?: number;
96
+ }
97
+ /**
98
+ * A portable, self-describing watermark snapshot (RFC-013 §9). Used to seed a
99
+ * freshly-provisioned local watermark from an exported blob, an operator-supplied
100
+ * high-water mark, or the on-chain cursor, so a restored wallet never starts
101
+ * below the true high-water mark and reissues a spent leaf.
102
+ */
103
+ export interface WatermarkSeed {
104
+ readonly version: 1;
105
+ readonly treeId: string;
106
+ readonly addressCursor: number;
107
+ readonly l1Cursor: number;
108
+ readonly l2Cursor: number;
109
+ /** Flat indices already consumed (reserved/committed/burned). */
110
+ readonly unavailable?: readonly number[];
111
+ }
112
+ export interface SeedWatermarkOptions {
113
+ /**
114
+ * Permit a seed whose cursor is behind the current local cursor. Defaults to
115
+ * `false` — regression re-exposes used leaves, so seeding fails closed.
116
+ */
117
+ readonly allowRegression?: boolean;
118
+ }
119
+ export interface ConflictRecord {
120
+ treeId: string;
121
+ localIndex: number;
122
+ remoteIndex: number;
123
+ timestamp: number;
124
+ }
125
+ export interface SyncResult {
126
+ synced: boolean;
127
+ conflicts: ConflictRecord[];
128
+ advancedTo?: SigningIndices;
129
+ }
130
+ export interface WotsLeaseProvider {
131
+ reserveKeyUse(params: ReserveParams): Promise<LeaseReservation>;
132
+ commitKeyUse(reservationId: string, txId: string): Promise<void>;
133
+ burnReservation(reservationId: string, reason: string): Promise<void>;
134
+ getLocalWatermark(treeId: string): Promise<LocalWatermark>;
135
+ publishWatermark(treeId: string): Promise<void>;
136
+ syncLeaseJournal(): Promise<SyncResult>;
137
+ verifyLeaseCertificate(cert?: LeaseCertificate): Promise<boolean>;
138
+ }
139
+ export interface JournalEntry {
140
+ treeId: string;
141
+ branchId: string;
142
+ wotsIndex: number;
143
+ indices: SigningIndices;
144
+ status: 'reserved' | 'committed' | 'burned' | 'reserved-expired';
145
+ reservationId?: string;
146
+ payloadHash?: string;
147
+ txId?: string;
148
+ timestamp: number;
149
+ deviceId: string;
150
+ previousHash?: string;
151
+ hash?: string;
152
+ }
153
+ export interface DeviceKeyRange {
154
+ deviceId: string;
155
+ startAddressIndex: number;
156
+ endAddressIndex: number;
157
+ addressCount: number;
158
+ }
159
+ export interface PersonalLeaseNodeConfig {
160
+ nodeUrl: string;
161
+ nodePubkey: string;
162
+ authToken?: string;
163
+ /**
164
+ * Identity that authenticates certificates issued by this node. When it
165
+ * carries a `verify()` function, `verifyLeaseCertificate` uses it. Without
166
+ * any verifier the provider FAILS CLOSED (AUD-016) — an identity match on
167
+ * `issuedBy` alone is never enough.
168
+ */
169
+ certificateSigner?: CertificateSigner;
170
+ /**
171
+ * Verify-only cryptographic verifier for certificates issued by the node.
172
+ * The provider holds no signing key, so this is the recommended way to make
173
+ * `verifyLeaseCertificate` functional. When neither this nor a
174
+ * `certificateSigner.verify` is supplied, the provider rejects every
175
+ * certificate (fail closed).
176
+ */
177
+ verify?: (message: Uint8Array, signature: Uint8Array) => Promise<boolean>;
178
+ }
179
+ /**
180
+ * Layer 4 — P2P quorum lease coordination.
181
+ *
182
+ * `peers` is the set of quorum members this device coordinates with. Each
183
+ * entry is a transport-agnostic RPC handle: the provider sends the same
184
+ * LEASE_RESERVE / LEASE_COMMIT / LEASE_BURN wire messages used by the
185
+ * lookup protocol, so any peer that speaks that protocol can participate
186
+ * (lookup nodes, other devices, or in-memory test peers).
187
+ */
188
+ export interface QuorumPeer {
189
+ peerId: string;
190
+ request(message: {
191
+ type: 'LEASE_RESERVE' | 'LEASE_COMMIT' | 'LEASE_BURN' | 'LEASE_WATERMARK';
192
+ payload: Record<string, unknown>;
193
+ }, timeoutMs?: number): Promise<{
194
+ type: string;
195
+ payload: Record<string, unknown>;
196
+ }>;
197
+ }
198
+ export interface P2PQuorumLeaseProviderConfig {
199
+ /** Quorum members to coordinate with (excluding self). */
200
+ peers: QuorumPeer[];
201
+ /** Minimum attestations required for a reservation to be considered quorum-approved. Default: 1. */
202
+ minAttestations?: number;
203
+ /** Local provider used for the authoritative local watermark + journal. */
204
+ local: LocalLeaseProvider;
205
+ /** Timeout per peer request. Default: 5_000. */
206
+ requestTimeoutMs?: number;
207
+ /** Require quorum approval on commit as well as reserve. Default: false. */
208
+ requireQuorumOnCommit?: boolean;
209
+ /**
210
+ * Identity that authenticates issued certificates. When set, reserved
211
+ * certificates carry a real signature; without it, certificates are
212
+ * issued unsigned and will FAIL verification.
213
+ */
214
+ certificateSigner?: CertificateSigner;
215
+ }
216
+ /**
217
+ * Layer 5 — on-chain watermark anchoring.
218
+ *
219
+ * `chain` is any ChainStateProvider (hosted, Minima RPC, or lookup node).
220
+ * The provider spends a dedicated watermark coin whose STATE(0) holds the
221
+ * flat watermark cursor; every publish advances it on-chain so the watermark
222
+ * is verifiable by third parties without trusting this device.
223
+ */
224
+ export interface OnchainWatermarkProviderConfig {
225
+ /** Chain access for coin queries, proofs, and broadcasting. */
226
+ chain: {
227
+ getCoin(coinId: string): Promise<{
228
+ coinid: string;
229
+ address: string;
230
+ amount: string;
231
+ tokenid: string;
232
+ state?: unknown[];
233
+ } | null>;
234
+ getProof(coinId: string): Promise<{
235
+ data: unknown;
236
+ }>;
237
+ broadcastTxPoW(txpowHex: string): Promise<{
238
+ success: boolean;
239
+ txpowid?: string;
240
+ message?: string;
241
+ }>;
242
+ getTip?(): Promise<{
243
+ block: number;
244
+ }>;
245
+ };
246
+ /** Coin ID of the dedicated watermark coin. */
247
+ watermarkCoinId: string;
248
+ /** Address the watermark coin currently sits at (spending address). */
249
+ watermarkAddress: string;
250
+ /** Token ID of the watermark coin. Default: '0x00'. */
251
+ tokenId?: string;
252
+ /** Amount of the watermark coin in MIN base units. Default: '1'. */
253
+ amount?: string;
254
+ /** Local provider used for the authoritative local watermark + journal. */
255
+ local: LocalLeaseProvider;
256
+ /** Signer for the watermark coin's script (SIGNEDBY digest). Also authenticates issued certificates. */
257
+ signer: {
258
+ publicKeyDigest: string;
259
+ sign(message: Uint8Array): Promise<Uint8Array>;
260
+ verify?(message: Uint8Array, signature: Uint8Array): Promise<boolean>;
261
+ };
262
+ /** Port holding the flat watermark cursor in the coin state. Default: 0. */
263
+ statePort?: number;
264
+ /** Minimum blocks between on-chain publishes (rate limit). Default: 1. */
265
+ minBlocksBetweenPublishes?: number;
266
+ /**
267
+ * Optional durable storage for the watermark coin's identity. When set, the
268
+ * provider persists the advanced watermark coin ID after each publish so the
269
+ * rollover survives restarts. Default: in-memory only.
270
+ */
271
+ storage?: StorageAdapter;
272
+ }
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ /**
3
+ * @totemsdk/wots-lease — canonical v3 watermark types and WotsLeaseProvider interface
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,51 @@
1
+ /**
2
+ * WotsWatermarkStore — canonical v3 implementation
3
+ *
4
+ * - Reads v1 (flat usedIndices) and v2 (per-address usedIndices) automatically on load.
5
+ * - Writes only v3.
6
+ * - Enforces monotonicity: unavailable map can only grow, cursors can only advance.
7
+ * - Storage key unchanged ('totem_wots_watermark') to avoid storage churn.
8
+ */
9
+ import type { StorageAdapter, LoggerAdapter } from '@totemsdk/core';
10
+ import type { WotsWatermarkState, TreeWatermark, SigningIndices, UnavailableReason, LocalWatermark, WatermarkSeed, SeedWatermarkOptions } from './types.js';
11
+ export declare function flatIndex(idx: SigningIndices): number;
12
+ export declare function fromFlatIndex(flat: number): SigningIndices;
13
+ export declare function assertValidSigningIndices(indices: SigningIndices): void;
14
+ export declare class WotsWatermarkStore {
15
+ private readonly storage;
16
+ private state;
17
+ private _initialized;
18
+ private readonly logger;
19
+ constructor(storage: StorageAdapter, logger?: LoggerAdapter);
20
+ initialize(): Promise<void>;
21
+ private ensureInit;
22
+ private persist;
23
+ /**
24
+ * Reload the persisted watermark and merge it monotonically into the local
25
+ * state. A provider that shares its storage with another provider (AUD-004)
26
+ * must call this inside its mutation critical section before allocating or
27
+ * marking, so it observes writes it did not make and never re-issues a slot
28
+ * another instance already took.
29
+ */
30
+ refresh(): Promise<void>;
31
+ private getOrCreateTree;
32
+ getNextIndices(treeId: string): SigningIndices;
33
+ markUnavailable(treeId: string, indices: SigningIndices, reason: UnavailableReason): Promise<void>;
34
+ isUnavailable(treeId: string, indices: SigningIndices): boolean;
35
+ save(treeId: string, patch: Partial<TreeWatermark>): Promise<void>;
36
+ getLocalWatermark(treeId: string): LocalWatermark;
37
+ /**
38
+ * Export a portable seed for a tree (RFC-013 §9). Includes the cursor and the
39
+ * flat indices already consumed, so it can rebuild the no-reuse state exactly.
40
+ */
41
+ exportSeed(treeId: string): WatermarkSeed;
42
+ /**
43
+ * Seed the watermark from an exported/operator/on-chain snapshot. Forward-only
44
+ * by default: a seed behind the current cursor raises
45
+ * {@link WatermarkSeedRegressionError} so a restore can never re-expose used
46
+ * leaves. The unavailable set is unioned, never shrunk.
47
+ */
48
+ seed(seed: WatermarkSeed, options?: SeedWatermarkOptions): Promise<void>;
49
+ getRawState(): WotsWatermarkState;
50
+ clear(): Promise<void>;
51
+ }