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.
- package/LICENSE +104 -0
- package/README.md +603 -0
- package/build/archiveBootstrap.d.ts +60 -0
- package/build/archiveBootstrap.js +234 -0
- package/build/buyClient.d.ts +58 -0
- package/build/buyClient.js +162 -0
- package/build/contractInterface.d.ts +55 -0
- package/build/contractInterface.js +213 -0
- package/build/countryStats.d.ts +37 -0
- package/build/countryStats.js +89 -0
- package/build/deployClient.d.ts +168 -0
- package/build/deployClient.js +172 -0
- package/build/freezeClient.d.ts +44 -0
- package/build/freezeClient.js +59 -0
- package/build/generationClient.d.ts +54 -0
- package/build/generationClient.js +113 -0
- package/build/index.d.ts +72 -0
- package/build/index.js +258 -0
- package/build/licenseProof.d.ts +26 -0
- package/build/licenseProof.js +41 -0
- package/build/licenseRecord.d.ts +39 -0
- package/build/licenseRecord.js +6 -0
- package/build/licenseStore.d.ts +49 -0
- package/build/licenseStore.js +303 -0
- package/build/manifestVerify.d.ts +30 -0
- package/build/manifestVerify.js +95 -0
- package/build/migrateClient.d.ts +43 -0
- package/build/migrateClient.js +47 -0
- package/build/ownership.d.ts +34 -0
- package/build/ownership.js +147 -0
- package/build/refundClient.d.ts +52 -0
- package/build/refundClient.js +92 -0
- package/build/renewClient.d.ts +49 -0
- package/build/renewClient.js +135 -0
- package/build/statsClient.d.ts +96 -0
- package/build/statsClient.js +28 -0
- package/build/verifyCore.d.ts +26 -0
- package/build/verifyCore.js +57 -0
- package/build/verifyService.d.ts +3 -0
- package/build/verifyService.js +690 -0
- package/package.json +118 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Copyright (c) 2025-2026 zkLicensing project developers
|
|
2
|
+
// All rights reserved.
|
|
3
|
+
/**
|
|
4
|
+
* ownership.ts
|
|
5
|
+
*
|
|
6
|
+
* Ownership-token helpers and a small in-memory nonce store for the
|
|
7
|
+
* challenge-response /verify flow.
|
|
8
|
+
*
|
|
9
|
+
* Replay-resistance: the LicenseProof's public input binds (licenseHash,
|
|
10
|
+
* nonce). The keeper hands out a nonce, the client proves against it, the
|
|
11
|
+
* keeper consumes the nonce on the way through. Tokens are HMAC-SHA256 of
|
|
12
|
+
* the (zkAppAddress, licenseHash, exp, jti?) tuple so signature validation
|
|
13
|
+
* is purely cryptographic — no server-side lookup, restart-safe as long as
|
|
14
|
+
* the HMAC key persists.
|
|
15
|
+
*
|
|
16
|
+
* Session tracking: the optional `jti` field lets the caller run a
|
|
17
|
+
* revocation / concurrency layer on top of the stateless HMAC — the token
|
|
18
|
+
* is still validated cryptographically, but the caller can also check that
|
|
19
|
+
* the jti is present in a per-license active-session set. Absent jti keeps
|
|
20
|
+
* old callers working unchanged (there is no session tracking to begin with,
|
|
21
|
+
* so nothing needs to migrate).
|
|
22
|
+
*/
|
|
23
|
+
import { createHmac, randomBytes, timingSafeEqual } from 'crypto';
|
|
24
|
+
import { Field } from 'o1js';
|
|
25
|
+
export const OWNERSHIP_CHALLENGE_TTL_MS = 600_000;
|
|
26
|
+
// A device that stays active refreshes via a fresh /challenge + /respond;
|
|
27
|
+
// a lost/uninstalled device burns its slot for at most this long before
|
|
28
|
+
// the server prunes it (see verifyService session store). 7 days keeps
|
|
29
|
+
// re-prove pressure low on daily-use apps while still bounding the recovery
|
|
30
|
+
// window for owners hitting the concurrent-device cap.
|
|
31
|
+
export const OWNERSHIP_TOKEN_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
32
|
+
// Generate a 128-bit random jti. base64url so it drops into token payloads
|
|
33
|
+
// and query strings without escaping.
|
|
34
|
+
export function newJti() {
|
|
35
|
+
return randomBytes(16).toString('base64url');
|
|
36
|
+
}
|
|
37
|
+
export function createChallengeStore(ttlMs = OWNERSHIP_CHALLENGE_TTL_MS) {
|
|
38
|
+
const issued = new Map(); // `${lh}:${nonce}` → expiresAt
|
|
39
|
+
return {
|
|
40
|
+
issue(licenseHash) {
|
|
41
|
+
const nonce = Field.random().toString();
|
|
42
|
+
issued.set(`${licenseHash}:${nonce}`, Date.now() + ttlMs);
|
|
43
|
+
return { nonce, ttl: ttlMs };
|
|
44
|
+
},
|
|
45
|
+
consume(licenseHash, nonce) {
|
|
46
|
+
const key = `${licenseHash}:${nonce}`;
|
|
47
|
+
const expiresAt = issued.get(key);
|
|
48
|
+
if (expiresAt === undefined)
|
|
49
|
+
return 'unknown';
|
|
50
|
+
issued.delete(key);
|
|
51
|
+
if (Date.now() > expiresAt)
|
|
52
|
+
return 'expired';
|
|
53
|
+
return 'ok';
|
|
54
|
+
},
|
|
55
|
+
size: () => issued.size,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// --- token signing -----------------------------------------------------------
|
|
59
|
+
export function signOwnershipToken(key, payload) {
|
|
60
|
+
const body = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
61
|
+
const mac = createHmac('sha256', key).update(body).digest('base64url');
|
|
62
|
+
return `${body}.${mac}`;
|
|
63
|
+
}
|
|
64
|
+
export function verifyOwnershipToken(key, token) {
|
|
65
|
+
const dot = token.indexOf('.');
|
|
66
|
+
if (dot < 0)
|
|
67
|
+
return null;
|
|
68
|
+
const body = token.slice(0, dot);
|
|
69
|
+
const mac = token.slice(dot + 1);
|
|
70
|
+
const expected = createHmac('sha256', key).update(body).digest('base64url');
|
|
71
|
+
const a = Buffer.from(mac);
|
|
72
|
+
const b = Buffer.from(expected);
|
|
73
|
+
if (a.length !== b.length || !timingSafeEqual(a, b))
|
|
74
|
+
return null;
|
|
75
|
+
try {
|
|
76
|
+
const payload = JSON.parse(Buffer.from(body, 'base64url').toString('utf8'));
|
|
77
|
+
if (Date.now() > payload.e)
|
|
78
|
+
return null;
|
|
79
|
+
return payload;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export function newOwnershipKey() {
|
|
86
|
+
return randomBytes(32);
|
|
87
|
+
}
|
|
88
|
+
export function createSessionStore() {
|
|
89
|
+
// licenseHash → jti → expiresAt
|
|
90
|
+
const sessions = new Map();
|
|
91
|
+
const pruneLicense = (licenseHash) => {
|
|
92
|
+
const perLic = sessions.get(licenseHash);
|
|
93
|
+
if (!perLic)
|
|
94
|
+
return undefined;
|
|
95
|
+
const now = Date.now();
|
|
96
|
+
for (const [jti, exp] of perLic)
|
|
97
|
+
if (exp <= now)
|
|
98
|
+
perLic.delete(jti);
|
|
99
|
+
if (perLic.size === 0) {
|
|
100
|
+
sessions.delete(licenseHash);
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
return perLic;
|
|
104
|
+
};
|
|
105
|
+
return {
|
|
106
|
+
insert(licenseHash, jti, exp, cap) {
|
|
107
|
+
const perLic = pruneLicense(licenseHash) ?? new Map();
|
|
108
|
+
if (cap > 0 && perLic.size >= cap && !perLic.has(jti)) {
|
|
109
|
+
return { state: 'at-cap', active: perLic.size };
|
|
110
|
+
}
|
|
111
|
+
perLic.set(jti, exp);
|
|
112
|
+
sessions.set(licenseHash, perLic);
|
|
113
|
+
return { state: 'ok', active: perLic.size };
|
|
114
|
+
},
|
|
115
|
+
has(licenseHash, jti) {
|
|
116
|
+
const perLic = pruneLicense(licenseHash);
|
|
117
|
+
return perLic?.has(jti) ?? false;
|
|
118
|
+
},
|
|
119
|
+
releaseSeat(licenseHash, jti) {
|
|
120
|
+
const perLic = sessions.get(licenseHash);
|
|
121
|
+
if (!perLic)
|
|
122
|
+
return false;
|
|
123
|
+
const had = perLic.delete(jti);
|
|
124
|
+
if (perLic.size === 0)
|
|
125
|
+
sessions.delete(licenseHash);
|
|
126
|
+
return had;
|
|
127
|
+
},
|
|
128
|
+
reset(licenseHash) {
|
|
129
|
+
const perLic = sessions.get(licenseHash);
|
|
130
|
+
if (!perLic)
|
|
131
|
+
return 0;
|
|
132
|
+
const n = perLic.size;
|
|
133
|
+
sessions.delete(licenseHash);
|
|
134
|
+
return n;
|
|
135
|
+
},
|
|
136
|
+
activeCount(licenseHash) {
|
|
137
|
+
return pruneLicense(licenseHash)?.size ?? 0;
|
|
138
|
+
},
|
|
139
|
+
size() {
|
|
140
|
+
let n = 0;
|
|
141
|
+
for (const perLic of sessions.values())
|
|
142
|
+
n += perLic.size;
|
|
143
|
+
return n;
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=ownership.js.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* refundClient.ts
|
|
3
|
+
*
|
|
4
|
+
* Browser-safe client for the refund flow. Mirrors `buyClient.ts` in shape:
|
|
5
|
+
* the caller drives the wallet step between `proveRefund` and
|
|
6
|
+
* `confirmRefund` so the SDK stays wallet-agnostic.
|
|
7
|
+
*
|
|
8
|
+
* The 14-day refund window is enforced on the keeper side (returns HTTP 409
|
|
9
|
+
* when closed) and cross-checked in the circuit; the SDK doesn't second-guess
|
|
10
|
+
* it locally so a single source of truth stays authoritative.
|
|
11
|
+
*/
|
|
12
|
+
export type ProveRefundRequest = {
|
|
13
|
+
keeperUrl: string;
|
|
14
|
+
licenseHash: string;
|
|
15
|
+
buyerAddress: string;
|
|
16
|
+
zkAppAddress: string;
|
|
17
|
+
fetcher?: typeof fetch;
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
};
|
|
20
|
+
export type ProveRefundResponse = {
|
|
21
|
+
provenTxJson: string;
|
|
22
|
+
licenseHash: string;
|
|
23
|
+
};
|
|
24
|
+
export type ConfirmRefundRequest = {
|
|
25
|
+
keeperUrl: string;
|
|
26
|
+
zkAppAddress: string;
|
|
27
|
+
licenseHash: string;
|
|
28
|
+
txHash: string;
|
|
29
|
+
fetcher?: typeof fetch;
|
|
30
|
+
signal?: AbortSignal;
|
|
31
|
+
};
|
|
32
|
+
export declare function proveRefund(req: ProveRefundRequest): Promise<ProveRefundResponse>;
|
|
33
|
+
export declare function confirmRefund(req: ConfirmRefundRequest): Promise<{
|
|
34
|
+
ok: true;
|
|
35
|
+
}>;
|
|
36
|
+
export type RefundStatus = {
|
|
37
|
+
status: 'confirmed' | 'pending' | 'unknown';
|
|
38
|
+
txHash?: string | null;
|
|
39
|
+
elapsedMs?: number;
|
|
40
|
+
};
|
|
41
|
+
export type PollRefundStatusOptions = {
|
|
42
|
+
keeperUrl: string;
|
|
43
|
+
zkAppAddress: string;
|
|
44
|
+
licenseHash: string;
|
|
45
|
+
fetcher?: typeof fetch;
|
|
46
|
+
intervalMs?: number;
|
|
47
|
+
timeoutMs?: number;
|
|
48
|
+
signal?: AbortSignal;
|
|
49
|
+
onTick?: (status: RefundStatus) => void;
|
|
50
|
+
};
|
|
51
|
+
export declare function pollRefundStatus(opts: PollRefundStatusOptions): Promise<RefundStatus>;
|
|
52
|
+
//# sourceMappingURL=refundClient.d.ts.map
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Copyright (c) 2025-2026 zkLicensing project developers
|
|
2
|
+
// All rights reserved.
|
|
3
|
+
function baseUrl(url) {
|
|
4
|
+
return url.replace(/\/+$/, '');
|
|
5
|
+
}
|
|
6
|
+
async function readJsonError(resp) {
|
|
7
|
+
try {
|
|
8
|
+
const body = await resp.json();
|
|
9
|
+
if (typeof body.error === 'string')
|
|
10
|
+
return body.error;
|
|
11
|
+
}
|
|
12
|
+
catch { /* body wasn't JSON */ }
|
|
13
|
+
return `HTTP ${resp.status}`;
|
|
14
|
+
}
|
|
15
|
+
// POST /prove/refund — keeper compiles + proves the refund tx.
|
|
16
|
+
// The keeper 409s when the 14-day window has closed; that error surfaces
|
|
17
|
+
// through the thrown `Error` message.
|
|
18
|
+
export async function proveRefund(req) {
|
|
19
|
+
const fetcher = req.fetcher ?? fetch;
|
|
20
|
+
const resp = await fetcher(`${baseUrl(req.keeperUrl)}/prove/refund`, {
|
|
21
|
+
method: 'POST',
|
|
22
|
+
headers: { 'Content-Type': 'application/json' },
|
|
23
|
+
signal: req.signal,
|
|
24
|
+
body: JSON.stringify({
|
|
25
|
+
licenseHash: req.licenseHash,
|
|
26
|
+
buyerAddress: req.buyerAddress,
|
|
27
|
+
zkAppAddress: req.zkAppAddress,
|
|
28
|
+
}),
|
|
29
|
+
});
|
|
30
|
+
if (!resp.ok)
|
|
31
|
+
throw new Error(await readJsonError(resp));
|
|
32
|
+
return await resp.json();
|
|
33
|
+
}
|
|
34
|
+
// POST /confirm/refund — promotes the in-memory refund intent to a
|
|
35
|
+
// pending (refunded: true) LicenseRecord. Best-effort: if the caller
|
|
36
|
+
// swallows the failure, eventSyncLoop will still pick up the on-chain
|
|
37
|
+
// refund event.
|
|
38
|
+
export async function confirmRefund(req) {
|
|
39
|
+
const fetcher = req.fetcher ?? fetch;
|
|
40
|
+
const resp = await fetcher(`${baseUrl(req.keeperUrl)}/confirm/refund`, {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: { 'Content-Type': 'application/json' },
|
|
43
|
+
signal: req.signal,
|
|
44
|
+
body: JSON.stringify({
|
|
45
|
+
zkAppAddress: req.zkAppAddress,
|
|
46
|
+
licenseHash: req.licenseHash,
|
|
47
|
+
txHash: req.txHash,
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
if (!resp.ok)
|
|
51
|
+
throw new Error(await readJsonError(resp));
|
|
52
|
+
return { ok: true };
|
|
53
|
+
}
|
|
54
|
+
// Polls GET /license/:zkAppAddress/:licenseHash until the refund has landed
|
|
55
|
+
// on-chain (record.status === 'refunded') or the timeout fires. Mirrors
|
|
56
|
+
// pollBuyStatus / pollRenewStatus.
|
|
57
|
+
export async function pollRefundStatus(opts) {
|
|
58
|
+
const fetcher = opts.fetcher ?? fetch;
|
|
59
|
+
const interval = opts.intervalMs ?? 10_000;
|
|
60
|
+
const timeout = opts.timeoutMs ?? 20 * 60 * 1000;
|
|
61
|
+
const startedAt = Date.now();
|
|
62
|
+
const deadline = startedAt + timeout;
|
|
63
|
+
const url = `${baseUrl(opts.keeperUrl)}/license/${encodeURIComponent(opts.zkAppAddress)}/${encodeURIComponent(opts.licenseHash)}`;
|
|
64
|
+
while (true) {
|
|
65
|
+
if (opts.signal?.aborted)
|
|
66
|
+
throw new Error('pollRefundStatus aborted');
|
|
67
|
+
let status = { status: 'unknown', elapsedMs: Date.now() - startedAt };
|
|
68
|
+
try {
|
|
69
|
+
const resp = await fetcher(url, { signal: opts.signal });
|
|
70
|
+
if (resp.ok) {
|
|
71
|
+
const body = await resp.json();
|
|
72
|
+
if (!body.found) {
|
|
73
|
+
status = { status: 'unknown', elapsedMs: Date.now() - startedAt };
|
|
74
|
+
}
|
|
75
|
+
else if (body.status === 'refunded') {
|
|
76
|
+
status = { status: 'confirmed', txHash: body.txHash ?? null, elapsedMs: Date.now() - startedAt };
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
status = { status: 'pending', txHash: body.txHash ?? null, elapsedMs: Date.now() - startedAt };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch { /* transient network — try again on next tick */ }
|
|
84
|
+
opts.onTick?.(status);
|
|
85
|
+
if (status.status === 'confirmed')
|
|
86
|
+
return status;
|
|
87
|
+
if (Date.now() >= deadline)
|
|
88
|
+
return status;
|
|
89
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=refundClient.js.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export type ProveRenewRequest = {
|
|
2
|
+
keeperUrl: string;
|
|
3
|
+
licenseHash: string;
|
|
4
|
+
buyerAddress: string;
|
|
5
|
+
zkAppAddress: string;
|
|
6
|
+
duration: 0 | 1 | 2;
|
|
7
|
+
waiverAccepted: boolean;
|
|
8
|
+
freezeAfterSlot?: number | null;
|
|
9
|
+
fetcher?: typeof fetch;
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
};
|
|
12
|
+
export type ProveRenewResponse = {
|
|
13
|
+
provenTxJson: string;
|
|
14
|
+
newExpirySlot: number;
|
|
15
|
+
licenseHash: string;
|
|
16
|
+
renewAmount: string;
|
|
17
|
+
generation: number;
|
|
18
|
+
};
|
|
19
|
+
export type ConfirmRenewRequest = {
|
|
20
|
+
keeperUrl: string;
|
|
21
|
+
zkAppAddress: string;
|
|
22
|
+
licenseHash: string;
|
|
23
|
+
txHash: string;
|
|
24
|
+
fetcher?: typeof fetch;
|
|
25
|
+
signal?: AbortSignal;
|
|
26
|
+
};
|
|
27
|
+
export declare function proveRenew(req: ProveRenewRequest): Promise<ProveRenewResponse>;
|
|
28
|
+
export declare function confirmRenew(req: ConfirmRenewRequest): Promise<{
|
|
29
|
+
ok: true;
|
|
30
|
+
}>;
|
|
31
|
+
export type RenewStatus = {
|
|
32
|
+
status: 'confirmed' | 'pending' | 'unknown';
|
|
33
|
+
expirySlot?: number;
|
|
34
|
+
txHash?: string | null;
|
|
35
|
+
elapsedMs?: number;
|
|
36
|
+
};
|
|
37
|
+
export type PollRenewStatusOptions = {
|
|
38
|
+
keeperUrl: string;
|
|
39
|
+
zkAppAddress: string;
|
|
40
|
+
licenseHash: string;
|
|
41
|
+
newExpirySlot: number;
|
|
42
|
+
fetcher?: typeof fetch;
|
|
43
|
+
intervalMs?: number;
|
|
44
|
+
timeoutMs?: number;
|
|
45
|
+
signal?: AbortSignal;
|
|
46
|
+
onTick?: (status: RenewStatus) => void;
|
|
47
|
+
};
|
|
48
|
+
export declare function pollRenewStatus(opts: PollRenewStatusOptions): Promise<RenewStatus>;
|
|
49
|
+
//# sourceMappingURL=renewClient.d.ts.map
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Copyright (c) 2025-2026 zkLicensing project developers
|
|
2
|
+
// All rights reserved.
|
|
3
|
+
/**
|
|
4
|
+
* renewClient.ts
|
|
5
|
+
*
|
|
6
|
+
* Browser-safe client for the renew flow. Mirrors `buyClient.ts` in shape:
|
|
7
|
+
* the caller drives the wallet step between `proveRenew` and `confirmRenew`
|
|
8
|
+
* so the SDK stays wallet-agnostic.
|
|
9
|
+
*
|
|
10
|
+
* Unlike buy, renew carries a legally load-bearing `waiverAccepted` flag —
|
|
11
|
+
* EU/UK CRD Article 16(m) extinguishes the 14-day right of withdrawal only
|
|
12
|
+
* on the buyer's express affirmative act (consent to immediate delivery +
|
|
13
|
+
* acknowledgment of loss of withdrawal). We refuse the /prove/renew call
|
|
14
|
+
* when the flag is false to keep third-party integrators from silently
|
|
15
|
+
* shipping a UI that omits the waiver gate.
|
|
16
|
+
*
|
|
17
|
+
* Also refused pre-network when the keeper's sales-freeze schedule has
|
|
18
|
+
* fired (salesFrozen === true) — the keeper would 423 the request anyway,
|
|
19
|
+
* so surfacing the specific message client-side avoids a wasted round-trip.
|
|
20
|
+
*/
|
|
21
|
+
import { resolveFreezeState } from './freezeClient.js';
|
|
22
|
+
function baseUrl(url) {
|
|
23
|
+
return url.replace(/\/+$/, '');
|
|
24
|
+
}
|
|
25
|
+
async function readJsonError(resp) {
|
|
26
|
+
try {
|
|
27
|
+
const body = await resp.json();
|
|
28
|
+
if (typeof body.error === 'string')
|
|
29
|
+
return body.error;
|
|
30
|
+
}
|
|
31
|
+
catch { /* body wasn't JSON */ }
|
|
32
|
+
return `HTTP ${resp.status}`;
|
|
33
|
+
}
|
|
34
|
+
// POST /prove/renew — keeper compiles + proves the renew tx.
|
|
35
|
+
// Rejects locally when `waiverAccepted` is false; a network call in that
|
|
36
|
+
// state would be wasted work and would give the caller no signal that the
|
|
37
|
+
// legal precondition was missing. Also rejects when the keeper's freeze
|
|
38
|
+
// has fired — same reasoning.
|
|
39
|
+
export async function proveRenew(req) {
|
|
40
|
+
if (req.waiverAccepted !== true) {
|
|
41
|
+
throw new Error('waiverAccepted must be true — Article 16(m) waiver required to renew');
|
|
42
|
+
}
|
|
43
|
+
const fetcher = req.fetcher ?? fetch;
|
|
44
|
+
const freeze = await resolveFreezeState({
|
|
45
|
+
keeperUrl: req.keeperUrl,
|
|
46
|
+
freezeAfterSlot: req.freezeAfterSlot,
|
|
47
|
+
fetcher,
|
|
48
|
+
signal: req.signal,
|
|
49
|
+
});
|
|
50
|
+
if (freeze.salesFrozen) {
|
|
51
|
+
throw new Error('Sales frozen — new renewals refused ahead of scheduled hardfork migration');
|
|
52
|
+
}
|
|
53
|
+
const resp = await fetcher(`${baseUrl(req.keeperUrl)}/prove/renew`, {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: { 'Content-Type': 'application/json' },
|
|
56
|
+
signal: req.signal,
|
|
57
|
+
body: JSON.stringify({
|
|
58
|
+
licenseHash: req.licenseHash,
|
|
59
|
+
buyerAddress: req.buyerAddress,
|
|
60
|
+
zkAppAddress: req.zkAppAddress,
|
|
61
|
+
duration: req.duration,
|
|
62
|
+
}),
|
|
63
|
+
});
|
|
64
|
+
if (!resp.ok)
|
|
65
|
+
throw new Error(await readJsonError(resp));
|
|
66
|
+
return await resp.json();
|
|
67
|
+
}
|
|
68
|
+
// POST /confirm/renew — promotes the in-memory renew intent to a pending
|
|
69
|
+
// LicenseRecord. Call this only after the wallet returns a txHash.
|
|
70
|
+
export async function confirmRenew(req) {
|
|
71
|
+
const fetcher = req.fetcher ?? fetch;
|
|
72
|
+
const resp = await fetcher(`${baseUrl(req.keeperUrl)}/confirm/renew`, {
|
|
73
|
+
method: 'POST',
|
|
74
|
+
headers: { 'Content-Type': 'application/json' },
|
|
75
|
+
signal: req.signal,
|
|
76
|
+
body: JSON.stringify({
|
|
77
|
+
zkAppAddress: req.zkAppAddress,
|
|
78
|
+
licenseHash: req.licenseHash,
|
|
79
|
+
txHash: req.txHash,
|
|
80
|
+
}),
|
|
81
|
+
});
|
|
82
|
+
if (!resp.ok)
|
|
83
|
+
throw new Error(await readJsonError(resp));
|
|
84
|
+
return { ok: true };
|
|
85
|
+
}
|
|
86
|
+
// Polls GET /license/:zkAppAddress/:licenseHash until the renew has landed
|
|
87
|
+
// on-chain (pending flipped and new expiry recorded) or the timeout fires.
|
|
88
|
+
// Mirrors pollBuyStatus, except the terminal condition is Mina-side rather
|
|
89
|
+
// than keeper-side buy-watch — renew has no dedicated watch map.
|
|
90
|
+
export async function pollRenewStatus(opts) {
|
|
91
|
+
const fetcher = opts.fetcher ?? fetch;
|
|
92
|
+
const interval = opts.intervalMs ?? 10_000;
|
|
93
|
+
const timeout = opts.timeoutMs ?? 20 * 60 * 1000;
|
|
94
|
+
const startedAt = Date.now();
|
|
95
|
+
const deadline = startedAt + timeout;
|
|
96
|
+
const url = `${baseUrl(opts.keeperUrl)}/license/${encodeURIComponent(opts.zkAppAddress)}/${encodeURIComponent(opts.licenseHash)}`;
|
|
97
|
+
while (true) {
|
|
98
|
+
if (opts.signal?.aborted)
|
|
99
|
+
throw new Error('pollRenewStatus aborted');
|
|
100
|
+
let status = { status: 'unknown', elapsedMs: Date.now() - startedAt };
|
|
101
|
+
try {
|
|
102
|
+
const resp = await fetcher(url, { signal: opts.signal });
|
|
103
|
+
if (resp.ok) {
|
|
104
|
+
const body = await resp.json();
|
|
105
|
+
if (!body.found) {
|
|
106
|
+
status = { status: 'unknown', elapsedMs: Date.now() - startedAt };
|
|
107
|
+
}
|
|
108
|
+
else if (body.pending === false && (body.expirySlot ?? 0) >= opts.newExpirySlot) {
|
|
109
|
+
status = {
|
|
110
|
+
status: 'confirmed',
|
|
111
|
+
expirySlot: body.expirySlot,
|
|
112
|
+
txHash: body.txHash ?? null,
|
|
113
|
+
elapsedMs: Date.now() - startedAt,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
status = {
|
|
118
|
+
status: 'pending',
|
|
119
|
+
expirySlot: body.expirySlot,
|
|
120
|
+
txHash: body.txHash ?? null,
|
|
121
|
+
elapsedMs: Date.now() - startedAt,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch { /* transient network — try again on next tick */ }
|
|
127
|
+
opts.onTick?.(status);
|
|
128
|
+
if (status.status === 'confirmed')
|
|
129
|
+
return status;
|
|
130
|
+
if (Date.now() >= deadline)
|
|
131
|
+
return status;
|
|
132
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=renewClient.js.map
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* statsClient.ts
|
|
3
|
+
*
|
|
4
|
+
* Browser-safe HTTP client for the vendor-facing keeper endpoints that back
|
|
5
|
+
* the dashboard: GET /stats, GET /transactions, and GET /apps/vendor/:addr.
|
|
6
|
+
*
|
|
7
|
+
* Types mirror the keeper's response shapes so a dashboard implementation
|
|
8
|
+
* (this repo's zklicensing.com dashboard or a third-party one) doesn't have
|
|
9
|
+
* to re-derive them. New optional fields on the server should be added here
|
|
10
|
+
* as `?:` so older clients still typecheck against a newer keeper.
|
|
11
|
+
*/
|
|
12
|
+
import type { CountryBreakdown } from './countryStats.js';
|
|
13
|
+
export type Tier = 'monthly' | 'yearly' | 'fiveYear';
|
|
14
|
+
export type AppRecord = {
|
|
15
|
+
appId: string;
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
longDescription?: string;
|
|
19
|
+
category: string;
|
|
20
|
+
website: string;
|
|
21
|
+
zkAppAddress: string;
|
|
22
|
+
vendorAddress: string;
|
|
23
|
+
priceMonthlyMina: number;
|
|
24
|
+
priceYearlyMina: number;
|
|
25
|
+
priceFiveYearMina: number;
|
|
26
|
+
txHash: string;
|
|
27
|
+
verificationKeyHash?: string;
|
|
28
|
+
logo?: string;
|
|
29
|
+
iconUrl?: string;
|
|
30
|
+
tags?: string[];
|
|
31
|
+
contactEmail?: string;
|
|
32
|
+
country?: string;
|
|
33
|
+
status?: 'pending' | 'active' | 'inactive' | 'rejected';
|
|
34
|
+
rejectionReason?: string;
|
|
35
|
+
createdAt: string;
|
|
36
|
+
approvedAt?: string;
|
|
37
|
+
pendingEdit?: Partial<AppRecord> & {
|
|
38
|
+
editedAt: string;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
export type VendorStatsResponse = {
|
|
42
|
+
apps: AppRecord[];
|
|
43
|
+
activeLicenses: number;
|
|
44
|
+
totalLicenses: number;
|
|
45
|
+
totalRevenueMina: number;
|
|
46
|
+
pendingRevenueMina?: number;
|
|
47
|
+
renewalRate: number | null;
|
|
48
|
+
monthlyRevenueMina: {
|
|
49
|
+
month: string;
|
|
50
|
+
mina: number;
|
|
51
|
+
}[];
|
|
52
|
+
licensesByTier?: {
|
|
53
|
+
monthly: number;
|
|
54
|
+
yearly: number;
|
|
55
|
+
fiveYear: number;
|
|
56
|
+
};
|
|
57
|
+
deltas: {
|
|
58
|
+
revenuePct: number | null;
|
|
59
|
+
};
|
|
60
|
+
countryBreakdown?: CountryBreakdown;
|
|
61
|
+
};
|
|
62
|
+
export type Transaction = {
|
|
63
|
+
type: 'purchase' | 'renewal' | 'release' | 'refund';
|
|
64
|
+
appId: string;
|
|
65
|
+
appName: string;
|
|
66
|
+
tier: Tier | null;
|
|
67
|
+
amountMina: number;
|
|
68
|
+
txHash: string;
|
|
69
|
+
date: string;
|
|
70
|
+
};
|
|
71
|
+
export type TransactionsResponse = {
|
|
72
|
+
transactions: Transaction[];
|
|
73
|
+
};
|
|
74
|
+
export type VendorAppsResponse = {
|
|
75
|
+
apps: AppRecord[];
|
|
76
|
+
};
|
|
77
|
+
export declare function fetchVendorStats(opts: {
|
|
78
|
+
keeperUrl: string;
|
|
79
|
+
vendorAddress: string;
|
|
80
|
+
fetcher?: typeof fetch;
|
|
81
|
+
signal?: AbortSignal;
|
|
82
|
+
}): Promise<VendorStatsResponse | null>;
|
|
83
|
+
export declare function fetchTransactions(opts: {
|
|
84
|
+
keeperUrl: string;
|
|
85
|
+
vendorAddress: string;
|
|
86
|
+
days?: number;
|
|
87
|
+
fetcher?: typeof fetch;
|
|
88
|
+
signal?: AbortSignal;
|
|
89
|
+
}): Promise<TransactionsResponse | null>;
|
|
90
|
+
export declare function fetchVendorApps(opts: {
|
|
91
|
+
keeperUrl: string;
|
|
92
|
+
vendorAddress: string;
|
|
93
|
+
fetcher?: typeof fetch;
|
|
94
|
+
signal?: AbortSignal;
|
|
95
|
+
}): Promise<VendorAppsResponse | null>;
|
|
96
|
+
//# sourceMappingURL=statsClient.d.ts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Copyright (c) 2025-2026 zkLicensing project developers
|
|
2
|
+
// All rights reserved.
|
|
3
|
+
function baseUrl(url) {
|
|
4
|
+
return url.replace(/\/+$/, '');
|
|
5
|
+
}
|
|
6
|
+
export async function fetchVendorStats(opts) {
|
|
7
|
+
const fetcher = opts.fetcher ?? fetch;
|
|
8
|
+
const resp = await fetcher(`${baseUrl(opts.keeperUrl)}/stats?vendor=${encodeURIComponent(opts.vendorAddress)}`, { signal: opts.signal });
|
|
9
|
+
if (!resp.ok)
|
|
10
|
+
return null;
|
|
11
|
+
return await resp.json();
|
|
12
|
+
}
|
|
13
|
+
export async function fetchTransactions(opts) {
|
|
14
|
+
const fetcher = opts.fetcher ?? fetch;
|
|
15
|
+
const days = opts.days ?? 30;
|
|
16
|
+
const resp = await fetcher(`${baseUrl(opts.keeperUrl)}/transactions?vendor=${encodeURIComponent(opts.vendorAddress)}&days=${days}`, { signal: opts.signal });
|
|
17
|
+
if (!resp.ok)
|
|
18
|
+
return null;
|
|
19
|
+
return await resp.json();
|
|
20
|
+
}
|
|
21
|
+
export async function fetchVendorApps(opts) {
|
|
22
|
+
const fetcher = opts.fetcher ?? fetch;
|
|
23
|
+
const resp = await fetcher(`${baseUrl(opts.keeperUrl)}/apps/vendor/${encodeURIComponent(opts.vendorAddress)}`, { signal: opts.signal });
|
|
24
|
+
if (!resp.ok)
|
|
25
|
+
return null;
|
|
26
|
+
return await resp.json();
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=statsClient.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type LicenseRecord } from './licenseStore.js';
|
|
2
|
+
export type VerifyResponse = {
|
|
3
|
+
valid: boolean;
|
|
4
|
+
expiresAt: string | null;
|
|
5
|
+
expirySlot: number;
|
|
6
|
+
inGracePeriod: boolean;
|
|
7
|
+
remainingDays: number;
|
|
8
|
+
reason: string | null;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Core license verification.
|
|
12
|
+
*
|
|
13
|
+
* @param zkAppAddress - app address (composite key with licenseHash)
|
|
14
|
+
* @param licenseHash - Poseidon.hash([secretHash]) as a field string
|
|
15
|
+
* @param records - current off-chain license records (from licenseStore)
|
|
16
|
+
* @param onChainRoot - zkApp appState[0] fetched from Mina
|
|
17
|
+
* @param currentSlot - current blockchain slot (globalSlotSinceGenesis)
|
|
18
|
+
*/
|
|
19
|
+
export declare function verifyLicenseCore(params: {
|
|
20
|
+
zkAppAddress: string;
|
|
21
|
+
licenseHash: string;
|
|
22
|
+
records: LicenseRecord[];
|
|
23
|
+
onChainRoot: string;
|
|
24
|
+
currentSlot: number;
|
|
25
|
+
}): VerifyResponse;
|
|
26
|
+
//# sourceMappingURL=verifyCore.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Copyright (c) 2025-2026 zkLicensing project developers
|
|
2
|
+
// All rights reserved.
|
|
3
|
+
/**
|
|
4
|
+
* verifyCore.ts
|
|
5
|
+
*
|
|
6
|
+
* Pure verification logic shared by keeperService and verifyService.
|
|
7
|
+
* No HTTP, no network — takes already-fetched on-chain state and
|
|
8
|
+
* performs Merkle inclusion check + expiry logic.
|
|
9
|
+
*/
|
|
10
|
+
import { Field } from 'o1js';
|
|
11
|
+
import { buildMap } from './licenseStore.js';
|
|
12
|
+
import { GRACE_PERIOD_N, MS_PER_SLOT_N, SLOTS_PER_DAY_N } from './contractInterface.js';
|
|
13
|
+
function computeRemainingDays(expirySlot, currentSlot) {
|
|
14
|
+
if (expirySlot === 0)
|
|
15
|
+
return 0;
|
|
16
|
+
return Math.floor((expirySlot - currentSlot) / SLOTS_PER_DAY_N);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Core license verification.
|
|
20
|
+
*
|
|
21
|
+
* @param zkAppAddress - app address (composite key with licenseHash)
|
|
22
|
+
* @param licenseHash - Poseidon.hash([secretHash]) as a field string
|
|
23
|
+
* @param records - current off-chain license records (from licenseStore)
|
|
24
|
+
* @param onChainRoot - zkApp appState[0] fetched from Mina
|
|
25
|
+
* @param currentSlot - current blockchain slot (globalSlotSinceGenesis)
|
|
26
|
+
*/
|
|
27
|
+
export function verifyLicenseCore(params) {
|
|
28
|
+
const { zkAppAddress, licenseHash, records, onChainRoot, currentSlot } = params;
|
|
29
|
+
const map = buildMap(records, zkAppAddress);
|
|
30
|
+
const key = Field(licenseHash);
|
|
31
|
+
const currentValue = map.get(key).toString();
|
|
32
|
+
const witness = map.getWitness(key);
|
|
33
|
+
const [computedRoot] = witness.computeRootAndKey(Field(currentValue));
|
|
34
|
+
if (computedRoot.toString() !== onChainRoot) {
|
|
35
|
+
return { valid: false, expiresAt: null, expirySlot: 0, inGracePeriod: false, remainingDays: 0, reason: 'License state does not match on-chain root' };
|
|
36
|
+
}
|
|
37
|
+
if (currentValue === '0') {
|
|
38
|
+
return { valid: false, expiresAt: null, expirySlot: 0, inGracePeriod: false, remainingDays: 0, reason: 'License has been refunded' };
|
|
39
|
+
}
|
|
40
|
+
const expirySlot = Number(currentValue);
|
|
41
|
+
const inGracePeriod = currentSlot > expirySlot && currentSlot <= expirySlot + GRACE_PERIOD_N;
|
|
42
|
+
const expired = currentSlot > expirySlot + GRACE_PERIOD_N;
|
|
43
|
+
const expiresAt = new Date(Date.now() + (expirySlot - currentSlot) * MS_PER_SLOT_N).toISOString();
|
|
44
|
+
const remainingDays = computeRemainingDays(expirySlot, currentSlot);
|
|
45
|
+
if (expired) {
|
|
46
|
+
return { valid: false, expiresAt, expirySlot, inGracePeriod: false, remainingDays, reason: 'License has expired' };
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
valid: true,
|
|
50
|
+
expiresAt,
|
|
51
|
+
expirySlot,
|
|
52
|
+
inGracePeriod,
|
|
53
|
+
remainingDays,
|
|
54
|
+
reason: inGracePeriod ? 'License is in grace period — renew to avoid interruption' : null,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=verifyCore.js.map
|