@totemsdk/wots-lease 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/axia.d.ts +33 -0
- package/dist/axia.js +123 -0
- package/dist/device.d.ts +18 -0
- package/dist/device.js +30 -0
- package/dist/errors.d.ts +26 -0
- package/dist/errors.js +48 -0
- package/dist/hybrid.d.ts +34 -0
- package/dist/hybrid.js +64 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +8 -0
- package/dist/journal.d.ts +19 -0
- package/dist/journal.js +39 -0
- package/dist/local.d.ts +29 -0
- package/dist/local.js +163 -0
- package/dist/stubs.d.ts +43 -0
- package/dist/stubs.js +92 -0
- package/dist/types.d.ts +104 -0
- package/dist/types.js +4 -0
- package/dist/watermark.d.ts +30 -0
- package/dist/watermark.js +198 -0
- package/package.json +42 -0
- package/src/__tests__/watermark.test.ts +200 -0
- package/src/axia.ts +169 -0
- package/src/device.ts +40 -0
- package/src/errors.ts +57 -0
- package/src/hybrid.ts +98 -0
- package/src/index.ts +51 -0
- package/src/journal.ts +52 -0
- package/src/local.ts +196 -0
- package/src/stubs.ts +106 -0
- package/src/types.ts +119 -0
- package/src/watermark.ts +234 -0
package/dist/axia.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AxiaLeaseProvider — wraps the Axia API's /v1/wots-hardened endpoints.
|
|
3
|
+
*
|
|
4
|
+
* Callers do NOT need to call /finalize separately — commitKeyUse() does it.
|
|
5
|
+
*/
|
|
6
|
+
import type { StorageAdapter, LoggerAdapter } from '@totemsdk/core';
|
|
7
|
+
import type { WotsLeaseProvider, ReserveParams, LeaseReservation, LeaseCertificate, LocalWatermark, SyncResult } from './types.js';
|
|
8
|
+
export interface AxiaLeaseProviderConfig {
|
|
9
|
+
apiUrl: string;
|
|
10
|
+
apiKey: string;
|
|
11
|
+
rootPublicKey: string;
|
|
12
|
+
storage: StorageAdapter;
|
|
13
|
+
logger?: LoggerAdapter;
|
|
14
|
+
}
|
|
15
|
+
export declare class AxiaLeaseProvider implements WotsLeaseProvider {
|
|
16
|
+
private readonly watermark;
|
|
17
|
+
private readonly leaseStore;
|
|
18
|
+
private readonly journal;
|
|
19
|
+
private readonly apiUrl;
|
|
20
|
+
private readonly apiKey;
|
|
21
|
+
private readonly rootPublicKey;
|
|
22
|
+
private readonly logger;
|
|
23
|
+
private _initialized;
|
|
24
|
+
constructor(config: AxiaLeaseProviderConfig);
|
|
25
|
+
initialize(): Promise<void>;
|
|
26
|
+
reserveKeyUse(params: ReserveParams): Promise<LeaseReservation>;
|
|
27
|
+
commitKeyUse(reservationId: string, txId: string): Promise<void>;
|
|
28
|
+
burnReservation(reservationId: string, reason: string): Promise<void>;
|
|
29
|
+
getLocalWatermark(treeId: string): Promise<LocalWatermark>;
|
|
30
|
+
publishWatermark(_treeId: string): Promise<void>;
|
|
31
|
+
syncLeaseJournal(): Promise<SyncResult>;
|
|
32
|
+
verifyLeaseCertificate(cert?: LeaseCertificate): Promise<boolean>;
|
|
33
|
+
}
|
package/dist/axia.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AxiaLeaseProvider — wraps the Axia API's /v1/wots-hardened endpoints.
|
|
3
|
+
*
|
|
4
|
+
* Callers do NOT need to call /finalize separately — commitKeyUse() does it.
|
|
5
|
+
*/
|
|
6
|
+
import { NoopLogger, LeaseStore } from '@totemsdk/core';
|
|
7
|
+
import { prepareLease, finalizeLease } from '@totemsdk/core';
|
|
8
|
+
import { WotsWatermarkStore, flatIndex } from './watermark.js';
|
|
9
|
+
import { LeaseJournal } from './journal.js';
|
|
10
|
+
import { LeaseNotFoundError } from './errors.js';
|
|
11
|
+
export class AxiaLeaseProvider {
|
|
12
|
+
constructor(config) {
|
|
13
|
+
this._initialized = false;
|
|
14
|
+
this.apiUrl = config.apiUrl;
|
|
15
|
+
this.apiKey = config.apiKey;
|
|
16
|
+
this.rootPublicKey = config.rootPublicKey;
|
|
17
|
+
this.logger = config.logger ?? new NoopLogger();
|
|
18
|
+
this.watermark = new WotsWatermarkStore(config.storage, this.logger);
|
|
19
|
+
this.leaseStore = new LeaseStore(config.storage, this.logger);
|
|
20
|
+
this.journal = new LeaseJournal(config.storage, this.logger);
|
|
21
|
+
}
|
|
22
|
+
async initialize() {
|
|
23
|
+
if (this._initialized)
|
|
24
|
+
return;
|
|
25
|
+
await this.watermark.initialize();
|
|
26
|
+
await this.leaseStore.initialize();
|
|
27
|
+
await this.journal.initialize();
|
|
28
|
+
this._initialized = true;
|
|
29
|
+
}
|
|
30
|
+
async reserveKeyUse(params) {
|
|
31
|
+
if (!this._initialized)
|
|
32
|
+
await this.initialize();
|
|
33
|
+
const txId = params.payloadHash ?? `tx-${Date.now()}`;
|
|
34
|
+
const resp = await prepareLease(this.apiUrl, this.apiKey, {
|
|
35
|
+
txId,
|
|
36
|
+
rootPublicKey: this.rootPublicKey,
|
|
37
|
+
to: params.treeId,
|
|
38
|
+
amount: params.valueHint ?? '0',
|
|
39
|
+
ttlMs: params.ttlMs ?? 120000,
|
|
40
|
+
});
|
|
41
|
+
const indices = resp.lease;
|
|
42
|
+
const expiresAt = Date.now() + (params.ttlMs ?? 120000);
|
|
43
|
+
const reservationId = resp.txId;
|
|
44
|
+
const lease = {
|
|
45
|
+
leaseId: reservationId,
|
|
46
|
+
leaseToken: resp.leaseToken,
|
|
47
|
+
indices,
|
|
48
|
+
expiresAt,
|
|
49
|
+
status: 'active',
|
|
50
|
+
createdAt: Date.now(),
|
|
51
|
+
leaseTTL: params.ttlMs ?? 120000,
|
|
52
|
+
};
|
|
53
|
+
await this.leaseStore.save(lease);
|
|
54
|
+
await this.journal.append({
|
|
55
|
+
treeId: params.treeId,
|
|
56
|
+
branchId: params.branchId ?? 'default',
|
|
57
|
+
wotsIndex: flatIndex(indices),
|
|
58
|
+
indices,
|
|
59
|
+
status: 'reserved',
|
|
60
|
+
payloadHash: params.payloadHash,
|
|
61
|
+
timestamp: Date.now(),
|
|
62
|
+
deviceId: params.deviceId ?? 'axia',
|
|
63
|
+
});
|
|
64
|
+
return { reservationId, indices, expiresAt, leaseToken: resp.leaseToken };
|
|
65
|
+
}
|
|
66
|
+
async commitKeyUse(reservationId, txId) {
|
|
67
|
+
if (!this._initialized)
|
|
68
|
+
await this.initialize();
|
|
69
|
+
const lease = this.leaseStore.get(reservationId);
|
|
70
|
+
if (!lease)
|
|
71
|
+
throw new LeaseNotFoundError(reservationId);
|
|
72
|
+
await finalizeLease(this.apiUrl, this.apiKey, lease.leaseToken, txId);
|
|
73
|
+
const indices = lease.indices;
|
|
74
|
+
await this.watermark.markUnavailable(lease.indices.addressIndex.toString(), indices, 'committed');
|
|
75
|
+
await this.leaseStore.updateStatus(reservationId, 'finalized');
|
|
76
|
+
await this.journal.append({
|
|
77
|
+
treeId: reservationId,
|
|
78
|
+
branchId: 'default',
|
|
79
|
+
wotsIndex: flatIndex(indices),
|
|
80
|
+
indices,
|
|
81
|
+
status: 'committed',
|
|
82
|
+
txId,
|
|
83
|
+
timestamp: Date.now(),
|
|
84
|
+
deviceId: 'axia',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
async burnReservation(reservationId, reason) {
|
|
88
|
+
if (!this._initialized)
|
|
89
|
+
await this.initialize();
|
|
90
|
+
const lease = this.leaseStore.get(reservationId);
|
|
91
|
+
if (!lease)
|
|
92
|
+
throw new LeaseNotFoundError(reservationId);
|
|
93
|
+
const indices = lease.indices;
|
|
94
|
+
await this.watermark.markUnavailable(lease.indices.addressIndex.toString(), indices, 'burned');
|
|
95
|
+
await this.leaseStore.updateStatus(reservationId, 'cancelled');
|
|
96
|
+
this.logger.warn(`[AxiaLeaseProvider] Burning reservation ${reservationId}: ${reason}`);
|
|
97
|
+
await this.journal.append({
|
|
98
|
+
treeId: reservationId,
|
|
99
|
+
branchId: 'default',
|
|
100
|
+
wotsIndex: flatIndex(indices),
|
|
101
|
+
indices,
|
|
102
|
+
status: 'burned',
|
|
103
|
+
timestamp: Date.now(),
|
|
104
|
+
deviceId: 'axia',
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
async getLocalWatermark(treeId) {
|
|
108
|
+
if (!this._initialized)
|
|
109
|
+
await this.initialize();
|
|
110
|
+
return this.watermark.getLocalWatermark(treeId);
|
|
111
|
+
}
|
|
112
|
+
async publishWatermark(_treeId) {
|
|
113
|
+
// Server maintains its own watermark via the lease API
|
|
114
|
+
}
|
|
115
|
+
async syncLeaseJournal() {
|
|
116
|
+
return { synced: true, conflicts: [] };
|
|
117
|
+
}
|
|
118
|
+
async verifyLeaseCertificate(cert) {
|
|
119
|
+
if (cert === undefined)
|
|
120
|
+
return true;
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
package/dist/device.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device keyspace partitioning — Layer 2.
|
|
3
|
+
*
|
|
4
|
+
* Deterministic address-index range assignment per device slot.
|
|
5
|
+
* Does NOT accept a raw seed; the caller derives the TreeKey in @totemsdk/core.
|
|
6
|
+
*
|
|
7
|
+
* Layout (8 device slots × 8 addresses each = 64 total):
|
|
8
|
+
* device 0 → addressIndex 0–7
|
|
9
|
+
* device 1 → addressIndex 8–15
|
|
10
|
+
* device 2 → addressIndex 16–23
|
|
11
|
+
* ...
|
|
12
|
+
*/
|
|
13
|
+
import type { DeviceKeyRange } from './types.js';
|
|
14
|
+
export declare function allocateDeviceRange(params: {
|
|
15
|
+
deviceSlot: number;
|
|
16
|
+
deviceId: string;
|
|
17
|
+
}): DeviceKeyRange;
|
|
18
|
+
export declare function deviceSlotForAddressIndex(addressIndex: number): number;
|
package/dist/device.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device keyspace partitioning — Layer 2.
|
|
3
|
+
*
|
|
4
|
+
* Deterministic address-index range assignment per device slot.
|
|
5
|
+
* Does NOT accept a raw seed; the caller derives the TreeKey in @totemsdk/core.
|
|
6
|
+
*
|
|
7
|
+
* Layout (8 device slots × 8 addresses each = 64 total):
|
|
8
|
+
* device 0 → addressIndex 0–7
|
|
9
|
+
* device 1 → addressIndex 8–15
|
|
10
|
+
* device 2 → addressIndex 16–23
|
|
11
|
+
* ...
|
|
12
|
+
*/
|
|
13
|
+
const ADDRESSES_PER_DEVICE = 8;
|
|
14
|
+
const MAX_DEVICE_SLOTS = 8;
|
|
15
|
+
export function allocateDeviceRange(params) {
|
|
16
|
+
if (params.deviceSlot < 0 || params.deviceSlot >= MAX_DEVICE_SLOTS) {
|
|
17
|
+
throw new RangeError(`Device slot must be 0–${MAX_DEVICE_SLOTS - 1}, got ${params.deviceSlot}`);
|
|
18
|
+
}
|
|
19
|
+
const startAddressIndex = params.deviceSlot * ADDRESSES_PER_DEVICE;
|
|
20
|
+
const endAddressIndex = startAddressIndex + ADDRESSES_PER_DEVICE - 1;
|
|
21
|
+
return {
|
|
22
|
+
deviceId: params.deviceId,
|
|
23
|
+
startAddressIndex,
|
|
24
|
+
endAddressIndex,
|
|
25
|
+
addressCount: ADDRESSES_PER_DEVICE,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function deviceSlotForAddressIndex(addressIndex) {
|
|
29
|
+
return Math.floor(addressIndex / ADDRESSES_PER_DEVICE);
|
|
30
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export declare class WatermarkMonotonicityError extends Error {
|
|
2
|
+
constructor(message: string);
|
|
3
|
+
}
|
|
4
|
+
export declare class WatermarkExhaustedError extends Error {
|
|
5
|
+
readonly treeId: string;
|
|
6
|
+
constructor(treeId: string);
|
|
7
|
+
}
|
|
8
|
+
export declare class LeaseNotFoundError extends Error {
|
|
9
|
+
readonly reservationId: string;
|
|
10
|
+
constructor(reservationId: string);
|
|
11
|
+
}
|
|
12
|
+
export declare class PersonalLeaseNodeNotConfiguredError extends Error {
|
|
13
|
+
constructor();
|
|
14
|
+
}
|
|
15
|
+
export declare class P2PQuorumNotImplementedError extends Error {
|
|
16
|
+
constructor();
|
|
17
|
+
}
|
|
18
|
+
export declare class OnchainWatermarkNotImplementedError extends Error {
|
|
19
|
+
constructor();
|
|
20
|
+
}
|
|
21
|
+
export declare class DeviceRangeViolationError extends Error {
|
|
22
|
+
readonly addressIndex: number;
|
|
23
|
+
readonly allowedStart: number;
|
|
24
|
+
readonly allowedEnd: number;
|
|
25
|
+
constructor(addressIndex: number, allowedStart: number, allowedEnd: number);
|
|
26
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export class WatermarkMonotonicityError extends Error {
|
|
2
|
+
constructor(message) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = 'WatermarkMonotonicityError';
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export class WatermarkExhaustedError extends Error {
|
|
8
|
+
constructor(treeId) {
|
|
9
|
+
super(`WOTS keyspace exhausted for tree: ${treeId}`);
|
|
10
|
+
this.treeId = treeId;
|
|
11
|
+
this.name = 'WatermarkExhaustedError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export class LeaseNotFoundError extends Error {
|
|
15
|
+
constructor(reservationId) {
|
|
16
|
+
super(`Lease reservation not found: ${reservationId}`);
|
|
17
|
+
this.reservationId = reservationId;
|
|
18
|
+
this.name = 'LeaseNotFoundError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export class PersonalLeaseNodeNotConfiguredError extends Error {
|
|
22
|
+
constructor() {
|
|
23
|
+
super('PersonalLeaseNodeProvider requires a running lookup node. ' +
|
|
24
|
+
'Deploy @totemsdk/lookup-node and configure { nodeUrl, nodePubkey }.');
|
|
25
|
+
this.name = 'PersonalLeaseNodeNotConfiguredError';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export class P2PQuorumNotImplementedError extends Error {
|
|
29
|
+
constructor() {
|
|
30
|
+
super('P2PQuorumLeaseProvider is not yet implemented.');
|
|
31
|
+
this.name = 'P2PQuorumNotImplementedError';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export class OnchainWatermarkNotImplementedError extends Error {
|
|
35
|
+
constructor() {
|
|
36
|
+
super('OnchainWatermarkProvider is not yet implemented.');
|
|
37
|
+
this.name = 'OnchainWatermarkNotImplementedError';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export class DeviceRangeViolationError extends Error {
|
|
41
|
+
constructor(addressIndex, allowedStart, allowedEnd) {
|
|
42
|
+
super(`Address index ${addressIndex} is outside device range [${allowedStart}, ${allowedEnd}]`);
|
|
43
|
+
this.addressIndex = addressIndex;
|
|
44
|
+
this.allowedStart = allowedStart;
|
|
45
|
+
this.allowedEnd = allowedEnd;
|
|
46
|
+
this.name = 'DeviceRangeViolationError';
|
|
47
|
+
}
|
|
48
|
+
}
|
package/dist/hybrid.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HybridLeaseProvider — recommended production default.
|
|
3
|
+
*
|
|
4
|
+
* Always uses local first, then gates on node cert and/or quorum attestation
|
|
5
|
+
* for transactions above a configurable value threshold.
|
|
6
|
+
*/
|
|
7
|
+
import type { WotsLeaseProvider, ReserveParams, LeaseReservation, LeaseCertificate, LocalWatermark, SyncResult } from './types.js';
|
|
8
|
+
import type { LocalLeaseProvider } from './local.js';
|
|
9
|
+
import type { PersonalLeaseNodeProvider } from './stubs.js';
|
|
10
|
+
import type { P2PQuorumLeaseProvider } from './stubs.js';
|
|
11
|
+
import type { OnchainWatermarkProvider } from './stubs.js';
|
|
12
|
+
export interface HybridLeaseProviderConfig {
|
|
13
|
+
local: LocalLeaseProvider;
|
|
14
|
+
node?: PersonalLeaseNodeProvider;
|
|
15
|
+
quorum?: P2PQuorumLeaseProvider;
|
|
16
|
+
onchain?: OnchainWatermarkProvider;
|
|
17
|
+
threshold?: number;
|
|
18
|
+
}
|
|
19
|
+
export declare class HybridLeaseProvider implements WotsLeaseProvider {
|
|
20
|
+
private readonly local;
|
|
21
|
+
private readonly node?;
|
|
22
|
+
private readonly quorum?;
|
|
23
|
+
private readonly onchain?;
|
|
24
|
+
private readonly threshold;
|
|
25
|
+
constructor(config: HybridLeaseProviderConfig);
|
|
26
|
+
private isHighValue;
|
|
27
|
+
reserveKeyUse(params: ReserveParams): Promise<LeaseReservation>;
|
|
28
|
+
commitKeyUse(reservationId: string, txId: string): Promise<void>;
|
|
29
|
+
burnReservation(reservationId: string, reason: string): Promise<void>;
|
|
30
|
+
getLocalWatermark(treeId: string): Promise<LocalWatermark>;
|
|
31
|
+
publishWatermark(treeId: string): Promise<void>;
|
|
32
|
+
syncLeaseJournal(): Promise<SyncResult>;
|
|
33
|
+
verifyLeaseCertificate(cert?: LeaseCertificate): Promise<boolean>;
|
|
34
|
+
}
|
package/dist/hybrid.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HybridLeaseProvider — recommended production default.
|
|
3
|
+
*
|
|
4
|
+
* Always uses local first, then gates on node cert and/or quorum attestation
|
|
5
|
+
* for transactions above a configurable value threshold.
|
|
6
|
+
*/
|
|
7
|
+
export class HybridLeaseProvider {
|
|
8
|
+
constructor(config) {
|
|
9
|
+
this.local = config.local;
|
|
10
|
+
this.node = config.node;
|
|
11
|
+
this.quorum = config.quorum;
|
|
12
|
+
this.onchain = config.onchain;
|
|
13
|
+
this.threshold = config.threshold ?? Infinity;
|
|
14
|
+
}
|
|
15
|
+
isHighValue(params) {
|
|
16
|
+
if (!params.valueHint)
|
|
17
|
+
return false;
|
|
18
|
+
const val = parseFloat(params.valueHint);
|
|
19
|
+
return !isNaN(val) && val >= this.threshold;
|
|
20
|
+
}
|
|
21
|
+
async reserveKeyUse(params) {
|
|
22
|
+
const reservation = await this.local.reserveKeyUse(params);
|
|
23
|
+
if (this.isHighValue(params)) {
|
|
24
|
+
if (this.node) {
|
|
25
|
+
try {
|
|
26
|
+
const nodeReservation = await this.node.reserveKeyUse(params);
|
|
27
|
+
return { ...reservation, certificate: nodeReservation.certificate };
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Node unavailable — continue with local only
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return reservation;
|
|
35
|
+
}
|
|
36
|
+
commitKeyUse(reservationId, txId) {
|
|
37
|
+
return this.local.commitKeyUse(reservationId, txId);
|
|
38
|
+
}
|
|
39
|
+
burnReservation(reservationId, reason) {
|
|
40
|
+
return this.local.burnReservation(reservationId, reason);
|
|
41
|
+
}
|
|
42
|
+
getLocalWatermark(treeId) {
|
|
43
|
+
return this.local.getLocalWatermark(treeId);
|
|
44
|
+
}
|
|
45
|
+
publishWatermark(treeId) {
|
|
46
|
+
if (this.node) {
|
|
47
|
+
return this.node.publishWatermark(treeId).catch(() => this.local.publishWatermark(treeId));
|
|
48
|
+
}
|
|
49
|
+
return this.local.publishWatermark(treeId);
|
|
50
|
+
}
|
|
51
|
+
syncLeaseJournal() {
|
|
52
|
+
if (this.node) {
|
|
53
|
+
return this.node.syncLeaseJournal().catch(() => this.local.syncLeaseJournal());
|
|
54
|
+
}
|
|
55
|
+
return this.local.syncLeaseJournal();
|
|
56
|
+
}
|
|
57
|
+
async verifyLeaseCertificate(cert) {
|
|
58
|
+
if (!cert)
|
|
59
|
+
return this.local.verifyLeaseCertificate(cert);
|
|
60
|
+
if (this.node)
|
|
61
|
+
return this.node.verifyLeaseCertificate(cert);
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type { UnavailableReason, SigningIndices, LeaseStatus, TreeWatermark, WotsWatermarkState, LeaseCertificate, LeaseReservation, ReserveParams, LocalWatermark, ConflictRecord, SyncResult, WotsLeaseProvider, JournalEntry, DeviceKeyRange, PersonalLeaseNodeConfig, } 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 { AxiaLeaseProvider } from './axia.js';
|
|
6
|
+
export type { AxiaLeaseProviderConfig } from './axia.js';
|
|
7
|
+
export { HybridLeaseProvider } from './hybrid.js';
|
|
8
|
+
export type { HybridLeaseProviderConfig } from './hybrid.js';
|
|
9
|
+
export { PersonalLeaseNodeProvider, P2PQuorumLeaseProvider, OnchainWatermarkProvider, } from './stubs.js';
|
|
10
|
+
export { allocateDeviceRange, deviceSlotForAddressIndex } from './device.js';
|
|
11
|
+
export { WatermarkMonotonicityError, WatermarkExhaustedError, LeaseNotFoundError, PersonalLeaseNodeNotConfiguredError, P2PQuorumNotImplementedError, OnchainWatermarkNotImplementedError, DeviceRangeViolationError, } from './errors.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { WotsWatermarkStore, flatIndex, fromFlatIndex, } from './watermark.js';
|
|
2
|
+
export { LeaseJournal } from './journal.js';
|
|
3
|
+
export { LocalLeaseProvider } from './local.js';
|
|
4
|
+
export { AxiaLeaseProvider } from './axia.js';
|
|
5
|
+
export { HybridLeaseProvider } from './hybrid.js';
|
|
6
|
+
export { PersonalLeaseNodeProvider, P2PQuorumLeaseProvider, OnchainWatermarkProvider, } from './stubs.js';
|
|
7
|
+
export { allocateDeviceRange, deviceSlotForAddressIndex } from './device.js';
|
|
8
|
+
export { WatermarkMonotonicityError, WatermarkExhaustedError, LeaseNotFoundError, PersonalLeaseNodeNotConfiguredError, P2PQuorumNotImplementedError, OnchainWatermarkNotImplementedError, DeviceRangeViolationError, } from './errors.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LeaseJournal — append-only audit log for WOTS key-use events.
|
|
3
|
+
* Preserves the full history: reserved → committed | burned | reserved-expired.
|
|
4
|
+
*/
|
|
5
|
+
import type { StorageAdapter, LoggerAdapter } from '@totemsdk/core';
|
|
6
|
+
import type { JournalEntry } from './types.js';
|
|
7
|
+
export declare class LeaseJournal {
|
|
8
|
+
private readonly storage;
|
|
9
|
+
private readonly logger;
|
|
10
|
+
private entries;
|
|
11
|
+
private _initialized;
|
|
12
|
+
constructor(storage: StorageAdapter, logger?: LoggerAdapter);
|
|
13
|
+
initialize(): Promise<void>;
|
|
14
|
+
append(entry: JournalEntry): Promise<void>;
|
|
15
|
+
getAll(): JournalEntry[];
|
|
16
|
+
getByTree(treeId: string): JournalEntry[];
|
|
17
|
+
getByReservation(reservationId: string): JournalEntry | undefined;
|
|
18
|
+
clear(): Promise<void>;
|
|
19
|
+
}
|
package/dist/journal.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LeaseJournal — append-only audit log for WOTS key-use events.
|
|
3
|
+
* Preserves the full history: reserved → committed | burned | reserved-expired.
|
|
4
|
+
*/
|
|
5
|
+
import { NoopLogger } from '@totemsdk/core';
|
|
6
|
+
const STORAGE_KEY = 'totem_wots_journal';
|
|
7
|
+
export class LeaseJournal {
|
|
8
|
+
constructor(storage, logger = new NoopLogger()) {
|
|
9
|
+
this.storage = storage;
|
|
10
|
+
this.logger = logger;
|
|
11
|
+
this.entries = [];
|
|
12
|
+
this._initialized = false;
|
|
13
|
+
}
|
|
14
|
+
async initialize() {
|
|
15
|
+
if (this._initialized)
|
|
16
|
+
return;
|
|
17
|
+
const stored = await this.storage.get(STORAGE_KEY);
|
|
18
|
+
this.entries = stored ?? [];
|
|
19
|
+
this._initialized = true;
|
|
20
|
+
}
|
|
21
|
+
async append(entry) {
|
|
22
|
+
this.entries.push(entry);
|
|
23
|
+
await this.storage.set(STORAGE_KEY, this.entries);
|
|
24
|
+
this.logger.debug(`[LeaseJournal] ${entry.status} treeId=${entry.treeId} idx=${entry.wotsIndex} txId=${entry.txId ?? '-'}`);
|
|
25
|
+
}
|
|
26
|
+
getAll() {
|
|
27
|
+
return [...this.entries];
|
|
28
|
+
}
|
|
29
|
+
getByTree(treeId) {
|
|
30
|
+
return this.entries.filter((e) => e.treeId === treeId);
|
|
31
|
+
}
|
|
32
|
+
getByReservation(reservationId) {
|
|
33
|
+
return [...this.entries].reverse().find((e) => e.reservationId === reservationId);
|
|
34
|
+
}
|
|
35
|
+
async clear() {
|
|
36
|
+
this.entries = [];
|
|
37
|
+
await this.storage.remove(STORAGE_KEY);
|
|
38
|
+
}
|
|
39
|
+
}
|
package/dist/local.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LocalLeaseProvider — Layer 1: local durable ledger.
|
|
3
|
+
*
|
|
4
|
+
* Single-device, no network coordination.
|
|
5
|
+
* All WOTS safety guaranteed by the local WotsWatermarkStore + LeaseJournal.
|
|
6
|
+
*/
|
|
7
|
+
import type { StorageAdapter, LoggerAdapter } from '@totemsdk/core';
|
|
8
|
+
import type { WotsLeaseProvider, ReserveParams, LeaseReservation, LeaseCertificate, LocalWatermark, SyncResult } from './types.js';
|
|
9
|
+
export declare class LocalLeaseProvider implements WotsLeaseProvider {
|
|
10
|
+
private readonly logger;
|
|
11
|
+
private readonly watermark;
|
|
12
|
+
private readonly leaseStore;
|
|
13
|
+
private readonly journal;
|
|
14
|
+
private _initialized;
|
|
15
|
+
private readonly deviceId;
|
|
16
|
+
/** In-memory map: reservationId → treeId (lost on restart; watermark survives). */
|
|
17
|
+
private readonly _treeIdByReservation;
|
|
18
|
+
constructor(storage: StorageAdapter, logger?: LoggerAdapter, deviceId?: string);
|
|
19
|
+
initialize(): Promise<void>;
|
|
20
|
+
private recoverExpiredReservations;
|
|
21
|
+
reserveKeyUse(params: ReserveParams): Promise<LeaseReservation>;
|
|
22
|
+
commitKeyUse(reservationId: string, txId: string): Promise<void>;
|
|
23
|
+
burnReservation(reservationId: string, reason: string): Promise<void>;
|
|
24
|
+
getLocalWatermark(treeId: string): Promise<LocalWatermark>;
|
|
25
|
+
publishWatermark(_treeId: string): Promise<void>;
|
|
26
|
+
syncLeaseJournal(): Promise<SyncResult>;
|
|
27
|
+
verifyLeaseCertificate(cert?: LeaseCertificate): Promise<boolean>;
|
|
28
|
+
private getDeviceRange;
|
|
29
|
+
}
|
package/dist/local.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LocalLeaseProvider — Layer 1: local durable ledger.
|
|
3
|
+
*
|
|
4
|
+
* Single-device, no network coordination.
|
|
5
|
+
* All WOTS safety guaranteed by the local WotsWatermarkStore + LeaseJournal.
|
|
6
|
+
*/
|
|
7
|
+
import { NoopLogger } from '@totemsdk/core';
|
|
8
|
+
import { LeaseStore } from '@totemsdk/core';
|
|
9
|
+
import { WotsWatermarkStore, flatIndex } from './watermark.js';
|
|
10
|
+
import { LeaseJournal } from './journal.js';
|
|
11
|
+
import { LeaseNotFoundError, DeviceRangeViolationError } from './errors.js';
|
|
12
|
+
function randomId() {
|
|
13
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
14
|
+
return crypto.randomUUID();
|
|
15
|
+
}
|
|
16
|
+
return Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
17
|
+
}
|
|
18
|
+
export class LocalLeaseProvider {
|
|
19
|
+
constructor(storage, logger = new NoopLogger(), deviceId = 'local') {
|
|
20
|
+
this.logger = logger;
|
|
21
|
+
this._initialized = false;
|
|
22
|
+
/** In-memory map: reservationId → treeId (lost on restart; watermark survives). */
|
|
23
|
+
this._treeIdByReservation = new Map();
|
|
24
|
+
this.watermark = new WotsWatermarkStore(storage, logger);
|
|
25
|
+
this.leaseStore = new LeaseStore(storage, logger);
|
|
26
|
+
this.journal = new LeaseJournal(storage, logger);
|
|
27
|
+
this.deviceId = deviceId;
|
|
28
|
+
}
|
|
29
|
+
async initialize() {
|
|
30
|
+
if (this._initialized)
|
|
31
|
+
return;
|
|
32
|
+
await this.watermark.initialize();
|
|
33
|
+
await this.leaseStore.initialize();
|
|
34
|
+
await this.journal.initialize();
|
|
35
|
+
await this.recoverExpiredReservations();
|
|
36
|
+
this._initialized = true;
|
|
37
|
+
}
|
|
38
|
+
async recoverExpiredReservations() {
|
|
39
|
+
const now = Date.now();
|
|
40
|
+
for (const lease of this.leaseStore.getAll()) {
|
|
41
|
+
if (lease.status === 'active' && lease.expiresAt < now) {
|
|
42
|
+
const treeId = this._treeIdByReservation.get(lease.leaseId) ?? 'default';
|
|
43
|
+
await this.watermark.markUnavailable(treeId, lease.indices, 'reserved-expired');
|
|
44
|
+
await this.leaseStore.updateStatus(lease.leaseId, 'expired');
|
|
45
|
+
await this.journal.append({
|
|
46
|
+
treeId,
|
|
47
|
+
branchId: 'default',
|
|
48
|
+
wotsIndex: flatIndex(lease.indices),
|
|
49
|
+
indices: lease.indices,
|
|
50
|
+
status: 'reserved-expired',
|
|
51
|
+
timestamp: now,
|
|
52
|
+
deviceId: this.deviceId,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async reserveKeyUse(params) {
|
|
58
|
+
if (!this._initialized)
|
|
59
|
+
await this.initialize();
|
|
60
|
+
const treeId = params.treeId;
|
|
61
|
+
const ttlMs = params.ttlMs ?? 120000;
|
|
62
|
+
const indices = this.watermark.getNextIndices(treeId);
|
|
63
|
+
if (params.deviceId) {
|
|
64
|
+
const range = this.getDeviceRange(params.deviceId);
|
|
65
|
+
if (range && (indices.addressIndex < range.start || indices.addressIndex > range.end)) {
|
|
66
|
+
throw new DeviceRangeViolationError(indices.addressIndex, range.start, range.end);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const reservationId = randomId();
|
|
70
|
+
const expiresAt = Date.now() + ttlMs;
|
|
71
|
+
const lease = {
|
|
72
|
+
leaseId: reservationId,
|
|
73
|
+
leaseToken: reservationId,
|
|
74
|
+
indices,
|
|
75
|
+
expiresAt,
|
|
76
|
+
status: 'active',
|
|
77
|
+
createdAt: Date.now(),
|
|
78
|
+
leaseTTL: ttlMs,
|
|
79
|
+
};
|
|
80
|
+
await this.leaseStore.save(lease);
|
|
81
|
+
// Track treeId in memory so commit/burn can reference the right watermark tree
|
|
82
|
+
this._treeIdByReservation.set(reservationId, treeId);
|
|
83
|
+
await this.journal.append({
|
|
84
|
+
treeId,
|
|
85
|
+
branchId: params.branchId ?? 'default',
|
|
86
|
+
wotsIndex: flatIndex(indices),
|
|
87
|
+
indices,
|
|
88
|
+
status: 'reserved',
|
|
89
|
+
payloadHash: params.payloadHash,
|
|
90
|
+
timestamp: Date.now(),
|
|
91
|
+
deviceId: params.deviceId ?? this.deviceId,
|
|
92
|
+
});
|
|
93
|
+
return { reservationId, indices, expiresAt };
|
|
94
|
+
}
|
|
95
|
+
async commitKeyUse(reservationId, txId) {
|
|
96
|
+
if (!this._initialized)
|
|
97
|
+
await this.initialize();
|
|
98
|
+
const lease = this.leaseStore.get(reservationId);
|
|
99
|
+
if (!lease)
|
|
100
|
+
throw new LeaseNotFoundError(reservationId);
|
|
101
|
+
const treeId = this._treeIdByReservation.get(reservationId) ?? 'default';
|
|
102
|
+
const indices = lease.indices;
|
|
103
|
+
await this.watermark.markUnavailable(treeId, indices, 'committed');
|
|
104
|
+
await this.leaseStore.updateStatus(reservationId, 'finalized');
|
|
105
|
+
this._treeIdByReservation.delete(reservationId);
|
|
106
|
+
await this.journal.append({
|
|
107
|
+
treeId,
|
|
108
|
+
branchId: 'default',
|
|
109
|
+
wotsIndex: flatIndex(indices),
|
|
110
|
+
indices,
|
|
111
|
+
status: 'committed',
|
|
112
|
+
txId,
|
|
113
|
+
timestamp: Date.now(),
|
|
114
|
+
deviceId: this.deviceId,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
async burnReservation(reservationId, reason) {
|
|
118
|
+
if (!this._initialized)
|
|
119
|
+
await this.initialize();
|
|
120
|
+
const lease = this.leaseStore.get(reservationId);
|
|
121
|
+
if (!lease)
|
|
122
|
+
throw new LeaseNotFoundError(reservationId);
|
|
123
|
+
const treeId = this._treeIdByReservation.get(reservationId) ?? 'default';
|
|
124
|
+
const indices = lease.indices;
|
|
125
|
+
await this.watermark.markUnavailable(treeId, indices, 'burned');
|
|
126
|
+
await this.leaseStore.updateStatus(reservationId, 'cancelled');
|
|
127
|
+
this._treeIdByReservation.delete(reservationId);
|
|
128
|
+
this.logger.warn(`[LocalLeaseProvider] Burning reservation ${reservationId}: ${reason}`);
|
|
129
|
+
await this.journal.append({
|
|
130
|
+
treeId,
|
|
131
|
+
branchId: 'default',
|
|
132
|
+
wotsIndex: flatIndex(indices),
|
|
133
|
+
indices,
|
|
134
|
+
status: 'burned',
|
|
135
|
+
timestamp: Date.now(),
|
|
136
|
+
deviceId: this.deviceId,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
async getLocalWatermark(treeId) {
|
|
140
|
+
if (!this._initialized)
|
|
141
|
+
await this.initialize();
|
|
142
|
+
return this.watermark.getLocalWatermark(treeId);
|
|
143
|
+
}
|
|
144
|
+
async publishWatermark(_treeId) {
|
|
145
|
+
// Layer 1: no-op
|
|
146
|
+
}
|
|
147
|
+
async syncLeaseJournal() {
|
|
148
|
+
// Layer 1: no-op
|
|
149
|
+
return { synced: true, conflicts: [] };
|
|
150
|
+
}
|
|
151
|
+
async verifyLeaseCertificate(cert) {
|
|
152
|
+
if (cert === undefined)
|
|
153
|
+
return true;
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
getDeviceRange(deviceId) {
|
|
157
|
+
const match = deviceId.match(/^device-?(\d+)$/i);
|
|
158
|
+
if (!match)
|
|
159
|
+
return null;
|
|
160
|
+
const slot = parseInt(match[1], 10);
|
|
161
|
+
return { start: slot * 8, end: slot * 8 + 7 };
|
|
162
|
+
}
|
|
163
|
+
}
|