zklicensing 0.1.0-beta.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.
Files changed (41) hide show
  1. package/LICENSE +104 -0
  2. package/README.md +603 -0
  3. package/build/archiveBootstrap.d.ts +60 -0
  4. package/build/archiveBootstrap.js +234 -0
  5. package/build/buyClient.d.ts +58 -0
  6. package/build/buyClient.js +162 -0
  7. package/build/contractInterface.d.ts +55 -0
  8. package/build/contractInterface.js +213 -0
  9. package/build/countryStats.d.ts +37 -0
  10. package/build/countryStats.js +89 -0
  11. package/build/deployClient.d.ts +168 -0
  12. package/build/deployClient.js +172 -0
  13. package/build/freezeClient.d.ts +44 -0
  14. package/build/freezeClient.js +59 -0
  15. package/build/generationClient.d.ts +54 -0
  16. package/build/generationClient.js +113 -0
  17. package/build/index.d.ts +72 -0
  18. package/build/index.js +258 -0
  19. package/build/licenseProof.d.ts +26 -0
  20. package/build/licenseProof.js +41 -0
  21. package/build/licenseRecord.d.ts +39 -0
  22. package/build/licenseRecord.js +6 -0
  23. package/build/licenseStore.d.ts +49 -0
  24. package/build/licenseStore.js +303 -0
  25. package/build/manifestVerify.d.ts +30 -0
  26. package/build/manifestVerify.js +95 -0
  27. package/build/migrateClient.d.ts +43 -0
  28. package/build/migrateClient.js +47 -0
  29. package/build/ownership.d.ts +34 -0
  30. package/build/ownership.js +147 -0
  31. package/build/refundClient.d.ts +52 -0
  32. package/build/refundClient.js +92 -0
  33. package/build/renewClient.d.ts +49 -0
  34. package/build/renewClient.js +135 -0
  35. package/build/statsClient.d.ts +96 -0
  36. package/build/statsClient.js +28 -0
  37. package/build/verifyCore.d.ts +26 -0
  38. package/build/verifyCore.js +57 -0
  39. package/build/verifyService.d.ts +3 -0
  40. package/build/verifyService.js +690 -0
  41. package/package.json +118 -0
@@ -0,0 +1,39 @@
1
+ /**
2
+ * licenseRecord.ts
3
+ *
4
+ * The `LicenseRecord` shape and related pure types/constants. Split out from
5
+ * licenseStore.ts (which pulls in Node `fs`) so browser-side modules — the
6
+ * buy client, the stats client, the country-stats computation — can consume
7
+ * the same type without dragging in filesystem code. licenseStore.ts
8
+ * re-exports these to keep existing Node imports working.
9
+ */
10
+ export type LicenseRecord = {
11
+ zkAppAddress: string;
12
+ licenseHash: string;
13
+ expirySlot: number;
14
+ purchaseSlot: number;
15
+ purchaseAmount: string;
16
+ txHash: string;
17
+ createdAt: string;
18
+ buyerAddress?: string;
19
+ buyerCountry?: string;
20
+ pending?: boolean;
21
+ provedAtSlot?: number;
22
+ initialExpirySlot?: number;
23
+ tier?: 'monthly' | 'yearly' | 'fiveYear';
24
+ renewedAtLeastOnce?: boolean;
25
+ refunded?: boolean;
26
+ released?: boolean;
27
+ releaseSlot?: number;
28
+ renewals?: Array<{
29
+ slot: number;
30
+ amount: string;
31
+ newExpiry: number;
32
+ txHash?: string;
33
+ }>;
34
+ refundWaiverAcceptedAt?: string;
35
+ escrowless?: boolean;
36
+ };
37
+ export declare const PENDING_REAP_SLOTS = 60;
38
+ export type LicenseStatus = 'active' | 'refunded' | 'expired' | 'unknown';
39
+ //# sourceMappingURL=licenseRecord.d.ts.map
@@ -0,0 +1,6 @@
1
+ // Copyright (c) 2025-2026 zkLicensing project developers
2
+ // All rights reserved.
3
+ // Reap pending records this many slots after prove time if no event ever lands.
4
+ // 60 slots ≈ 90 minutes — a generous window for tx inclusion under devnet conditions.
5
+ export const PENDING_REAP_SLOTS = 60;
6
+ //# sourceMappingURL=licenseRecord.js.map
@@ -0,0 +1,49 @@
1
+ import { MerkleMap, MerkleMapWitness } from 'o1js';
2
+ import { type LicenseRecord as _LicenseRecord, type LicenseStatus as _LicenseStatus } from './licenseRecord.js';
3
+ export type LicenseRecord = _LicenseRecord;
4
+ export type LicenseStatus = _LicenseStatus;
5
+ export declare const PENDING_REAP_SLOTS = 60;
6
+ export declare function licenseStorePath(): string;
7
+ export declare function readRecords(): Promise<LicenseRecord[]>;
8
+ export declare function buildMap(records: LicenseRecord[], zkAppAddress: string): MerkleMap;
9
+ export declare function buildEscrowMap(records: LicenseRecord[], zkAppAddress: string): MerkleMap;
10
+ export declare function serializeWitness(witness: MerkleMapWitness): {
11
+ isLefts: boolean[];
12
+ siblings: string[];
13
+ };
14
+ export declare function deserializeWitness(json: {
15
+ isLefts: boolean[];
16
+ siblings: string[];
17
+ }): MerkleMapWitness;
18
+ export declare function getWitness(zkAppAddress: string, licenseHash: string): Promise<{
19
+ root: string;
20
+ currentValue: string;
21
+ witness: MerkleMapWitness;
22
+ serialized: ReturnType<typeof serializeWitness>;
23
+ }>;
24
+ export declare function getLicense(zkAppAddress: string, licenseHash: string): Promise<LicenseRecord | null>;
25
+ export declare function licenseStatus(record: LicenseRecord | null, currentSlot: number): LicenseStatus;
26
+ export declare function upsertLicense(record: LicenseRecord): Promise<void>;
27
+ export declare function updateExpiry(zkAppAddress: string, licenseHash: string, expirySlot: number, opts?: {
28
+ markRenewed?: boolean;
29
+ renewal?: {
30
+ slot: number;
31
+ amount: string;
32
+ newExpiry: number;
33
+ txHash?: string;
34
+ };
35
+ }): Promise<boolean>;
36
+ export declare function recordRefund(zkAppAddress: string, licenseHash: string): Promise<boolean>;
37
+ export declare function recordRelease(zkAppAddress: string, licenseHash: string, releaseSlot?: number): Promise<boolean>;
38
+ export declare function confirmRecord(zkAppAddress: string, licenseHash: string): Promise<boolean>;
39
+ export declare function applyLicenseIssued(zkAppAddress: string, licenseHash: string, fields: {
40
+ expirySlot: number;
41
+ purchaseSlot: number;
42
+ purchaseAmount: string;
43
+ tier?: 'monthly' | 'yearly' | 'fiveYear';
44
+ escrowless?: boolean;
45
+ }): Promise<void>;
46
+ export declare function deleteRecord(zkAppAddress: string, licenseHash: string): Promise<boolean>;
47
+ export declare function getStalePendingRecords(currentSlot: number, maxAgeSlots?: number): Promise<LicenseRecord[]>;
48
+ export declare function getReleasableRecords(currentSlot: number): Promise<LicenseRecord[]>;
49
+ //# sourceMappingURL=licenseStore.d.ts.map
@@ -0,0 +1,303 @@
1
+ // Copyright (c) 2025-2026 zkLicensing project developers
2
+ // All rights reserved.
3
+ /**
4
+ * licenseStore.ts
5
+ *
6
+ * Off-chain MerkleMap state for the licensing system.
7
+ *
8
+ * Records are keyed by (zkAppAddress, licenseHash). Each LicensingApp deploys
9
+ * its own on-chain MerkleMap, so a passphrase used on two different apps
10
+ * produces the same `licenseHash` but two distinct records. Lookups that omit
11
+ * `zkAppAddress` would collide across apps — every API here takes it.
12
+ *
13
+ * Used by:
14
+ * - interact.ts / buyLicense.ts (CLI) — reads/writes directly
15
+ * - keeperService.ts — exposes the same logic over HTTP
16
+ * - proofServer.ts — reads expirySlot for a given licenseHash
17
+ */
18
+ import fs from 'fs/promises';
19
+ import { Bool, Field, MerkleMap, MerkleMapWitness, Poseidon, PublicKey, UInt32, UInt64 } from 'o1js';
20
+ import { GRACE_PERIOD_N, REFUND_WINDOW_N } from './contractInterface.js';
21
+ import { PENDING_REAP_SLOTS as _PENDING_REAP_SLOTS } from './licenseRecord.js';
22
+ export const PENDING_REAP_SLOTS = _PENDING_REAP_SLOTS;
23
+ export function licenseStorePath() {
24
+ return process.env.LICENSE_STORE_PATH ?? 'licenses.json';
25
+ }
26
+ export async function readRecords() {
27
+ let raw;
28
+ try {
29
+ raw = JSON.parse(await fs.readFile(licenseStorePath(), 'utf8'));
30
+ }
31
+ catch {
32
+ return [];
33
+ }
34
+ if (!Array.isArray(raw))
35
+ return [];
36
+ const records = raw;
37
+ // Records pre-dating composite keying lack `zkAppAddress` and cannot be
38
+ // safely matched against any specific zkApp. Drop them with a warning so
39
+ // bootstrap rebuilds them fresh from on-chain events.
40
+ const filtered = records.filter(r => typeof r.zkAppAddress === 'string' && r.zkAppAddress.length > 0);
41
+ if (filtered.length < records.length) {
42
+ console.warn(`[licenseStore] dropped ${records.length - filtered.length} record(s) missing zkAppAddress — re-bootstrap to recover`);
43
+ }
44
+ return filtered;
45
+ }
46
+ async function writeRecords(records) {
47
+ await fs.writeFile(licenseStorePath(), JSON.stringify(records, null, 2));
48
+ }
49
+ // Serialize all read-modify-write operations against licenses.json. Each
50
+ // mutating function does (readRecords → mutate → writeRecords); without a lock,
51
+ // two concurrent calls can interleave such that the second one's read predates
52
+ // the first one's write, and the second one's write then clobbers the first.
53
+ // This was visible during archive sync (applyEventsToStore fires many writes
54
+ // back-to-back) racing with /confirm/* writes from buyer flows.
55
+ let writeChain = Promise.resolve();
56
+ function withWriteLock(fn) {
57
+ const result = writeChain.then(fn, fn);
58
+ writeChain = result.then(() => { }, () => { });
59
+ return result;
60
+ }
61
+ // Records belonging to a specific zkApp. Each zkApp owns an independent
62
+ // on-chain MerkleMap, so all map operations must filter first.
63
+ function recordsForApp(records, zkAppAddress) {
64
+ return records.filter(r => r.zkAppAddress === zkAppAddress);
65
+ }
66
+ // Rebuild the license MerkleMap for a single zkApp. Only inserts entries
67
+ // with expirySlot > 0.
68
+ export function buildMap(records, zkAppAddress) {
69
+ const map = new MerkleMap();
70
+ for (const r of recordsForApp(records, zkAppAddress)) {
71
+ if (r.expirySlot !== 0) {
72
+ map.set(Field(r.licenseHash), Field(r.expirySlot));
73
+ }
74
+ }
75
+ return map;
76
+ }
77
+ // Rebuild the escrow MerkleMap for a single zkApp:
78
+ // licenseHash → escrowCommitment(amount, slot, buyerKey). Only includes
79
+ // entries with live escrow (not released, not refunded) and a known buyer.
80
+ export function buildEscrowMap(records, zkAppAddress) {
81
+ const map = new MerkleMap();
82
+ for (const r of recordsForApp(records, zkAppAddress)) {
83
+ if (!r.released && !r.refunded && r.purchaseSlot > 0 && r.buyerAddress) {
84
+ const buyerKey = PublicKey.fromBase58(r.buyerAddress);
85
+ const commitment = Poseidon.hash([
86
+ UInt64.from(BigInt(r.purchaseAmount)).value,
87
+ UInt32.from(r.purchaseSlot).value,
88
+ buyerKey.x,
89
+ buyerKey.isOdd.toField(),
90
+ ]);
91
+ map.set(Field(r.licenseHash), commitment);
92
+ }
93
+ }
94
+ return map;
95
+ }
96
+ // Serialize a MerkleMapWitness to plain JSON (safe to send over HTTP).
97
+ export function serializeWitness(witness) {
98
+ return {
99
+ isLefts: witness.isLefts.map((b) => b.toBoolean()),
100
+ siblings: witness.siblings.map((f) => f.toString()),
101
+ };
102
+ }
103
+ // Reconstruct a MerkleMapWitness from serialized JSON.
104
+ export function deserializeWitness(json) {
105
+ return new MerkleMapWitness(json.isLefts.map((b) => Bool(b)), json.siblings.map((s) => Field(s)));
106
+ }
107
+ // Returns the witness for a licenseHash key in the given zkApp's map.
108
+ export async function getWitness(zkAppAddress, licenseHash) {
109
+ const records = await readRecords();
110
+ const map = buildMap(records, zkAppAddress);
111
+ const key = Field(licenseHash);
112
+ const witness = map.getWitness(key);
113
+ return {
114
+ root: map.getRoot().toString(),
115
+ currentValue: map.get(key).toString(),
116
+ witness,
117
+ serialized: serializeWitness(witness),
118
+ };
119
+ }
120
+ // Look up a single license record by its composite key.
121
+ export async function getLicense(zkAppAddress, licenseHash) {
122
+ const records = await readRecords();
123
+ return records.find(r => r.zkAppAddress === zkAppAddress && r.licenseHash === licenseHash) ?? null;
124
+ }
125
+ // Determine the status of a license. A license in the 7-day grace window is
126
+ // still considered 'active' so renewal is not urgently forced.
127
+ export function licenseStatus(record, currentSlot) {
128
+ if (!record)
129
+ return 'unknown';
130
+ if (record.expirySlot === 0)
131
+ return 'refunded';
132
+ if (record.expirySlot + GRACE_PERIOD_N < currentSlot)
133
+ return 'expired';
134
+ return 'active';
135
+ }
136
+ // Add or update a license record (upsert by composite key).
137
+ export async function upsertLicense(record) {
138
+ if (!record.zkAppAddress) {
139
+ throw new Error('upsertLicense: record.zkAppAddress is required');
140
+ }
141
+ return withWriteLock(async () => {
142
+ const records = await readRecords();
143
+ const idx = records.findIndex(r => r.zkAppAddress === record.zkAppAddress && r.licenseHash === record.licenseHash);
144
+ if (idx >= 0) {
145
+ records[idx] = record;
146
+ }
147
+ else {
148
+ records.push(record);
149
+ }
150
+ await writeRecords(records);
151
+ });
152
+ }
153
+ // Update the expirySlot for an existing record (used by renew).
154
+ // `opts.markRenewed` flips renewedAtLeastOnce so stats can detect first-renewal
155
+ // without re-walking events. `opts.renewal` appends to the renewals[] log so
156
+ // stats can compute renewal revenue from the store alone.
157
+ export async function updateExpiry(zkAppAddress, licenseHash, expirySlot, opts = {}) {
158
+ return withWriteLock(async () => {
159
+ const records = await readRecords();
160
+ const record = records.find(r => r.zkAppAddress === zkAppAddress && r.licenseHash === licenseHash);
161
+ if (!record)
162
+ return false;
163
+ record.expirySlot = expirySlot;
164
+ if (opts.markRenewed)
165
+ record.renewedAtLeastOnce = true;
166
+ if (opts.renewal) {
167
+ const renewals = (record.renewals ??= []);
168
+ // Dedup by newExpiry — confirm-time and event-sync may both try to log
169
+ // the same renewal, but each renewal advances expiry by a unique tier
170
+ // amount, so newExpiry is a stable identifier.
171
+ if (!renewals.some(r => r.newExpiry === opts.renewal.newExpiry)) {
172
+ renewals.push(opts.renewal);
173
+ }
174
+ }
175
+ await writeRecords(records);
176
+ return true;
177
+ });
178
+ }
179
+ // Clear license and escrow data after a confirmed refund. Also flips `refunded`.
180
+ export async function recordRefund(zkAppAddress, licenseHash) {
181
+ return withWriteLock(async () => {
182
+ const records = await readRecords();
183
+ const record = records.find(r => r.zkAppAddress === zkAppAddress && r.licenseHash === licenseHash);
184
+ if (!record)
185
+ return false;
186
+ record.expirySlot = 0;
187
+ record.purchaseSlot = 0;
188
+ record.purchaseAmount = '0';
189
+ record.refunded = true;
190
+ await writeRecords(records);
191
+ return true;
192
+ });
193
+ }
194
+ // Mark escrow as released. Preserves purchaseAmount/purchaseSlot so stats can
195
+ // compute released revenue. The `released` flag is the truth signal —
196
+ // buildEscrowMap and getReleasableRecords filter on it directly.
197
+ export async function recordRelease(zkAppAddress, licenseHash, releaseSlot) {
198
+ return withWriteLock(async () => {
199
+ const records = await readRecords();
200
+ const record = records.find(r => r.zkAppAddress === zkAppAddress && r.licenseHash === licenseHash);
201
+ if (!record)
202
+ return false;
203
+ record.released = true;
204
+ if (releaseSlot !== undefined)
205
+ record.releaseSlot = releaseSlot;
206
+ await writeRecords(records);
207
+ return true;
208
+ });
209
+ }
210
+ // Clear the pending flag on a record after a chain event confirms it landed.
211
+ export async function confirmRecord(zkAppAddress, licenseHash) {
212
+ return withWriteLock(async () => {
213
+ const records = await readRecords();
214
+ const record = records.find(r => r.zkAppAddress === zkAppAddress && r.licenseHash === licenseHash);
215
+ if (!record)
216
+ return false;
217
+ if (!record.pending)
218
+ return false;
219
+ delete record.pending;
220
+ delete record.provedAtSlot;
221
+ await writeRecords(records);
222
+ return true;
223
+ });
224
+ }
225
+ // Apply a licenseIssued event atomically: confirm any pending /confirm/buy
226
+ // write, backfill initialExpirySlot/tier if missing, or create a fresh record.
227
+ // Doing the whole read-decide-write inside a single lock acquisition avoids the
228
+ // stale-snapshot hazard that would arise if archiveBootstrap called
229
+ // getLicense + confirmRecord + upsertLicense as three separate locked steps —
230
+ // a concurrent write between them could be clobbered by the upsert's
231
+ // pre-confirm snapshot.
232
+ export async function applyLicenseIssued(zkAppAddress, licenseHash, fields) {
233
+ return withWriteLock(async () => {
234
+ const records = await readRecords();
235
+ const record = records.find(r => r.zkAppAddress === zkAppAddress && r.licenseHash === licenseHash);
236
+ let dirty = false;
237
+ if (record) {
238
+ if (record.pending) {
239
+ delete record.pending;
240
+ delete record.provedAtSlot;
241
+ dirty = true;
242
+ }
243
+ if (record.initialExpirySlot === undefined) {
244
+ record.initialExpirySlot = fields.expirySlot;
245
+ dirty = true;
246
+ }
247
+ if (!record.tier && fields.tier) {
248
+ record.tier = fields.tier;
249
+ dirty = true;
250
+ }
251
+ if (fields.escrowless === true && record.escrowless !== true) {
252
+ record.escrowless = true;
253
+ dirty = true;
254
+ }
255
+ }
256
+ else {
257
+ records.push({
258
+ zkAppAddress,
259
+ licenseHash,
260
+ expirySlot: fields.expirySlot,
261
+ purchaseSlot: fields.purchaseSlot,
262
+ purchaseAmount: fields.purchaseAmount,
263
+ txHash: '',
264
+ createdAt: new Date().toISOString(),
265
+ initialExpirySlot: fields.expirySlot,
266
+ ...(fields.tier ? { tier: fields.tier } : {}),
267
+ ...(fields.escrowless === true ? { escrowless: true } : {}),
268
+ });
269
+ dirty = true;
270
+ }
271
+ if (dirty)
272
+ await writeRecords(records);
273
+ });
274
+ }
275
+ // Delete a record entirely (used to reap stale pending entries the buyer never submitted).
276
+ export async function deleteRecord(zkAppAddress, licenseHash) {
277
+ return withWriteLock(async () => {
278
+ const records = await readRecords();
279
+ const idx = records.findIndex(r => r.zkAppAddress === zkAppAddress && r.licenseHash === licenseHash);
280
+ if (idx < 0)
281
+ return false;
282
+ records.splice(idx, 1);
283
+ await writeRecords(records);
284
+ return true;
285
+ });
286
+ }
287
+ // List pending records whose prove slot is older than `currentSlot - maxAgeSlots`.
288
+ // Used by the reaper to drop records the buyer never actually signed/submitted.
289
+ export async function getStalePendingRecords(currentSlot, maxAgeSlots = PENDING_REAP_SLOTS) {
290
+ const records = await readRecords();
291
+ return records.filter((r) => r.pending && typeof r.provedAtSlot === 'number' && currentSlot - r.provedAtSlot > maxAgeSlots);
292
+ }
293
+ // Return all records where the refund window has passed and escrow is still live.
294
+ // Filters on the released/refunded flags so we don't re-release or release after refund.
295
+ export async function getReleasableRecords(currentSlot) {
296
+ const records = await readRecords();
297
+ return records.filter((r) => !r.released &&
298
+ !r.refunded &&
299
+ r.purchaseSlot > 0 &&
300
+ r.zkAppAddress &&
301
+ currentSlot > r.purchaseSlot + REFUND_WINDOW_N);
302
+ }
303
+ //# sourceMappingURL=licenseStore.js.map
@@ -0,0 +1,30 @@
1
+ /**
2
+ * manifestVerify.ts
3
+ *
4
+ * Browser + Node Ed25519 signature verification for platform-signed manifests
5
+ * (currently: the generation-list served at GET /apps/:appId, next: the
6
+ * sales-freeze manifest). The keeper signs with the platform manifest key
7
+ * loaded via PLATFORM_MANIFEST_KEY_FILE; the SDK pins the public key against
8
+ * a caller-supplied allow-list and verifies before trusting anything.
9
+ *
10
+ * Runtime: browsers and Node ≥ 20 expose `globalThis.crypto.subtle`. The
11
+ * SDK's `engines.node` is `>=20` for this reason — Node 18 child workers
12
+ * (spawned by `node --test`) do not populate `globalThis.crypto`.
13
+ *
14
+ * Canonicalization must match `src/manifestSign.ts` byte-for-byte:
15
+ * - sorted object keys
16
+ * - no whitespace
17
+ * - arrays preserved in order
18
+ * Any divergence silently breaks verification, so keep the algorithm dead
19
+ * simple and mirrored.
20
+ */
21
+ export declare function canonicalize(value: unknown): string;
22
+ /**
23
+ * Verify a base64 Ed25519 signature over a canonicalized payload against
24
+ * one of the pinned base64 public keys. Returns true iff at least one
25
+ * pinned key verifies. Fails closed on any error (unknown key format,
26
+ * WebCrypto rejection, missing subtle) — callers must interpret `false`
27
+ * as "do not trust."
28
+ */
29
+ export declare function verifyManifestSignature(payload: unknown, signatureBase64: string, pinnedPublicKeysBase64: readonly string[]): Promise<boolean>;
30
+ //# sourceMappingURL=manifestVerify.d.ts.map
@@ -0,0 +1,95 @@
1
+ // Copyright (c) 2025-2026 zkLicensing project developers
2
+ // All rights reserved.
3
+ /**
4
+ * manifestVerify.ts
5
+ *
6
+ * Browser + Node Ed25519 signature verification for platform-signed manifests
7
+ * (currently: the generation-list served at GET /apps/:appId, next: the
8
+ * sales-freeze manifest). The keeper signs with the platform manifest key
9
+ * loaded via PLATFORM_MANIFEST_KEY_FILE; the SDK pins the public key against
10
+ * a caller-supplied allow-list and verifies before trusting anything.
11
+ *
12
+ * Runtime: browsers and Node ≥ 20 expose `globalThis.crypto.subtle`. The
13
+ * SDK's `engines.node` is `>=20` for this reason — Node 18 child workers
14
+ * (spawned by `node --test`) do not populate `globalThis.crypto`.
15
+ *
16
+ * Canonicalization must match `src/manifestSign.ts` byte-for-byte:
17
+ * - sorted object keys
18
+ * - no whitespace
19
+ * - arrays preserved in order
20
+ * Any divergence silently breaks verification, so keep the algorithm dead
21
+ * simple and mirrored.
22
+ */
23
+ // Canonical JSON: sorted keys, no whitespace, arrays preserved in order.
24
+ // Mirror of src/manifestSign.ts:canonicalize. Divergence here silently
25
+ // breaks verification.
26
+ export function canonicalize(value) {
27
+ if (value === null || typeof value !== 'object')
28
+ return JSON.stringify(value);
29
+ if (Array.isArray(value))
30
+ return '[' + value.map(canonicalize).join(',') + ']';
31
+ const keys = Object.keys(value).sort();
32
+ return '{' + keys.map(k => JSON.stringify(k) + ':' + canonicalize(value[k])).join(',') + '}';
33
+ }
34
+ function base64ToBytes(b64) {
35
+ // atob is present in every modern JS runtime we target (browser + Node ≥ 16).
36
+ const bin = atob(b64);
37
+ const out = new Uint8Array(bin.length);
38
+ for (let i = 0; i < bin.length; i++)
39
+ out[i] = bin.charCodeAt(i);
40
+ return out;
41
+ }
42
+ function utf8Bytes(s) {
43
+ return new TextEncoder().encode(s);
44
+ }
45
+ // SPKI DER wrapper around a raw 32-byte Ed25519 public key.
46
+ // Same prefix used by src/manifestSign.ts:verifyPayload — WebCrypto needs
47
+ // SPKI to import; the wire format is raw base64 for space.
48
+ const ED25519_SPKI_PREFIX = new Uint8Array([
49
+ 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00,
50
+ ]);
51
+ function rawEd25519ToSpki(raw) {
52
+ if (raw.length !== 32)
53
+ throw new Error(`Ed25519 pubkey must be 32 bytes, got ${raw.length}`);
54
+ const out = new Uint8Array(ED25519_SPKI_PREFIX.length + raw.length);
55
+ out.set(ED25519_SPKI_PREFIX, 0);
56
+ out.set(raw, ED25519_SPKI_PREFIX.length);
57
+ return out;
58
+ }
59
+ /**
60
+ * Verify a base64 Ed25519 signature over a canonicalized payload against
61
+ * one of the pinned base64 public keys. Returns true iff at least one
62
+ * pinned key verifies. Fails closed on any error (unknown key format,
63
+ * WebCrypto rejection, missing subtle) — callers must interpret `false`
64
+ * as "do not trust."
65
+ */
66
+ export async function verifyManifestSignature(payload, signatureBase64, pinnedPublicKeysBase64) {
67
+ if (!pinnedPublicKeysBase64.length)
68
+ return false;
69
+ const subtle = globalThis.crypto?.subtle;
70
+ if (!subtle)
71
+ return false;
72
+ const messageBytes = utf8Bytes(canonicalize(payload));
73
+ let signatureBytes;
74
+ try {
75
+ signatureBytes = base64ToBytes(signatureBase64);
76
+ }
77
+ catch {
78
+ return false;
79
+ }
80
+ for (const pinB64 of pinnedPublicKeysBase64) {
81
+ try {
82
+ const spki = rawEd25519ToSpki(base64ToBytes(pinB64));
83
+ const key = await subtle.importKey('spki', spki, { name: 'Ed25519' }, false, ['verify']);
84
+ const ok = await subtle.verify({ name: 'Ed25519' }, key, signatureBytes, messageBytes);
85
+ if (ok)
86
+ return true;
87
+ }
88
+ catch {
89
+ // Try the next pin — one bad entry in the pin list must not mask
90
+ // a valid signature under a different pin.
91
+ }
92
+ }
93
+ return false;
94
+ }
95
+ //# sourceMappingURL=manifestVerify.js.map
@@ -0,0 +1,43 @@
1
+ /**
2
+ * migrateClient.ts
3
+ *
4
+ * Browser-safe client for /prove/migrate. Assembles a single-hop cross-
5
+ * generation proof against the current live map — the keeper walks the
6
+ * deploymentHistory once, gathers all three witnesses (ancestor / pred /
7
+ * live-empty), and returns a signable tx.
8
+ *
9
+ * Failure surfaces:
10
+ * - 404 "license refunded or unknown": no leaf anywhere in history for
11
+ * this licenseHash. Caller should not retry — it's terminal.
12
+ * - 409 "already migrated": leaf is present in the live map. Caller
13
+ * should refresh their license record and stop.
14
+ * - 409 "migration in flight": another caller is proving right now.
15
+ * `retryAfterMs` on the error tells the caller how long to wait
16
+ * before trying again. See `ProveMigrateError.retryAfterMs`.
17
+ */
18
+ export interface ProveMigrateRequest {
19
+ keeperUrl: string;
20
+ licenseHash: string;
21
+ appId: string;
22
+ buyerAddress: string;
23
+ fetcher?: typeof fetch;
24
+ signal?: AbortSignal;
25
+ }
26
+ export interface ProveMigrateResponse {
27
+ provenTxJson: string;
28
+ ancestorGen: number;
29
+ expirySlot: number;
30
+ licenseHash: string;
31
+ zkAppAddress: string;
32
+ }
33
+ /**
34
+ * Extends Error with the retryAfterMs hint the keeper returns on the
35
+ * "migration in flight" 409. Callers ignore it for other 4xx surfaces.
36
+ */
37
+ export declare class ProveMigrateError extends Error {
38
+ readonly status: number;
39
+ readonly retryAfterMs?: number;
40
+ constructor(message: string, status: number, retryAfterMs?: number);
41
+ }
42
+ export declare function proveMigrate(req: ProveMigrateRequest): Promise<ProveMigrateResponse>;
43
+ //# sourceMappingURL=migrateClient.d.ts.map
@@ -0,0 +1,47 @@
1
+ // Copyright (c) 2025-2026 zkLicensing project developers
2
+ // All rights reserved.
3
+ /**
4
+ * Extends Error with the retryAfterMs hint the keeper returns on the
5
+ * "migration in flight" 409. Callers ignore it for other 4xx surfaces.
6
+ */
7
+ export class ProveMigrateError extends Error {
8
+ status;
9
+ retryAfterMs;
10
+ constructor(message, status, retryAfterMs) {
11
+ super(message);
12
+ this.name = 'ProveMigrateError';
13
+ this.status = status;
14
+ this.retryAfterMs = retryAfterMs;
15
+ }
16
+ }
17
+ function baseUrl(url) {
18
+ return url.replace(/\/+$/, '');
19
+ }
20
+ export async function proveMigrate(req) {
21
+ const fetcher = req.fetcher ?? fetch;
22
+ const resp = await fetcher(`${baseUrl(req.keeperUrl)}/prove/migrate`, {
23
+ method: 'POST',
24
+ headers: { 'Content-Type': 'application/json' },
25
+ signal: req.signal,
26
+ body: JSON.stringify({
27
+ licenseHash: req.licenseHash,
28
+ appId: req.appId,
29
+ buyerAddress: req.buyerAddress,
30
+ }),
31
+ });
32
+ if (!resp.ok) {
33
+ let msg = `HTTP ${resp.status}`;
34
+ let retryAfterMs;
35
+ try {
36
+ const body = await resp.json();
37
+ if (typeof body.error === 'string')
38
+ msg = body.error;
39
+ if (typeof body.retryAfterMs === 'number')
40
+ retryAfterMs = body.retryAfterMs;
41
+ }
42
+ catch { /* body wasn't JSON */ }
43
+ throw new ProveMigrateError(msg, resp.status, retryAfterMs);
44
+ }
45
+ return await resp.json();
46
+ }
47
+ //# sourceMappingURL=migrateClient.js.map
@@ -0,0 +1,34 @@
1
+ export declare const OWNERSHIP_CHALLENGE_TTL_MS = 600000;
2
+ export declare const OWNERSHIP_TOKEN_TTL_MS: number;
3
+ export type OwnershipTokenPayload = {
4
+ z: string;
5
+ l: string;
6
+ e: number;
7
+ jti?: string;
8
+ };
9
+ export declare function newJti(): string;
10
+ export type ChallengeStore = {
11
+ issue(licenseHash: string): {
12
+ nonce: string;
13
+ ttl: number;
14
+ };
15
+ consume(licenseHash: string, nonce: string): 'ok' | 'unknown' | 'expired';
16
+ size(): number;
17
+ };
18
+ export declare function createChallengeStore(ttlMs?: number): ChallengeStore;
19
+ export declare function signOwnershipToken(key: Buffer, payload: OwnershipTokenPayload): string;
20
+ export declare function verifyOwnershipToken(key: Buffer, token: string): OwnershipTokenPayload | null;
21
+ export declare function newOwnershipKey(): Buffer;
22
+ export type SessionStore = {
23
+ insert(licenseHash: string, jti: string, exp: number, cap: number): {
24
+ state: 'ok' | 'at-cap';
25
+ active: number;
26
+ };
27
+ has(licenseHash: string, jti: string): boolean;
28
+ releaseSeat(licenseHash: string, jti: string): boolean;
29
+ reset(licenseHash: string): number;
30
+ activeCount(licenseHash: string): number;
31
+ size(): number;
32
+ };
33
+ export declare function createSessionStore(): SessionStore;
34
+ //# sourceMappingURL=ownership.d.ts.map