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,213 @@
1
+ // Copyright (c) 2025-2026 zkLicensing project developers
2
+ // All rights reserved.
3
+ /**
4
+ * contractInterface.ts
5
+ *
6
+ * Public contract interface: event Structs, slot/grace constants, duration
7
+ * helpers, and a minimal `LicensingAppEventsContract` SmartContract subclass
8
+ * that has the events declaration but no @method bodies.
9
+ *
10
+ * The full `LicensingApp` (with provable method bodies) lives in the private
11
+ * zklicensing-app package; only the things needed to decode events and reason
12
+ * about durations are published here so a public verify service can replay
13
+ * the archive without pulling in the contract source.
14
+ *
15
+ * `CANONICAL_VK_HASH` and `CANONICAL_LICENSE_PROOF_VK_HASH` are the
16
+ * verification-key hashes of the two published circuits — `LicensingApp`
17
+ * (the on-chain zkApp) and `LicenseOwnershipProgram` (the ownership
18
+ * challenge/response `ZkProgram`), respectively. Both are hand-maintained
19
+ * constants — bump them whenever the corresponding circuit changes and
20
+ * rerun the keeper's boot assertion (see zklicensing-app/src/keeperService.ts)
21
+ * / the verify service's `ensureCompiled()` check to catch drift.
22
+ */
23
+ import { declareState, fetchAccount, Field, PublicKey, SmartContract, Struct, UInt32, UInt64, MerkleMap } from 'o1js';
24
+ // ------------------------------------------------------------
25
+ // EVENT STRUCTS
26
+ // ------------------------------------------------------------
27
+ // Struct() returns a class whose type references o1js internals that fall
28
+ // outside this package's rootDir (npm hoists o1js to the workspace root).
29
+ // Annotating each export as `any` keeps tsc from inferring (and therefore
30
+ // emitting) those internal type references. Consumers get full typing through
31
+ // the hand-written contractInterface.d.ts shim.
32
+ //
33
+ // The clean alternative — `export class X extends Struct({...}) {}` as the
34
+ // mina-fungible-token repo does — only works when o1js sits inside rootDir,
35
+ // which a workspace member can't guarantee.
36
+ export const LicenseIssuedEvent = Struct({
37
+ licenseHash: Field,
38
+ buyer: PublicKey,
39
+ amount: UInt64,
40
+ slot: UInt32,
41
+ expirySlot: UInt32,
42
+ });
43
+ export const ExpiryUpdateEvent = Struct({
44
+ licenseHash: Field,
45
+ newExpiry: UInt32,
46
+ });
47
+ export const PriceUpdatedEvent = Struct({
48
+ priceMonthly: UInt64,
49
+ priceYearly: UInt64,
50
+ priceFiveYear: UInt64,
51
+ });
52
+ export const AppCreatedEvent = Struct({
53
+ vendor: PublicKey,
54
+ });
55
+ // Emitted by `migrateLicense` when a license issued under an ancestor
56
+ // deployment is re-minted into this deployment at zero payment. `licenseHash`
57
+ // is stable across all deployments because the buyer's derived hash is what
58
+ // identifies the license; `expirySlot` is copied verbatim from the ancestor's
59
+ // licensesRoot entry; `ancestorGen` is the generation index the migration
60
+ // resolved against (needed so archive replay and auditors can trace which
61
+ // frozen root supplied the entry — skip-generation migrations are one-hop).
62
+ export const LicenseMigratedEvent = Struct({
63
+ licenseHash: Field,
64
+ expirySlot: UInt32,
65
+ ancestorGen: UInt32,
66
+ });
67
+ // ------------------------------------------------------------
68
+ // CONSTANTS (slot math — 90 s/slot after Mesa upgrade)
69
+ // ------------------------------------------------------------
70
+ export const MONTHLY_SLOTS_N = 28_800; // 30 days × 24 × 40
71
+ export const ONE_YEAR_SLOTS_N = 350_640; // 365.25 days × 24 × 40 (Julian year)
72
+ export const FIVE_YEAR_SLOTS_N = 1_753_200; // 5 × ONE_YEAR_SLOTS_N
73
+ export const REFUND_WINDOW_N = 13_440;
74
+ export const GRACE_PERIOD_N = 6_720;
75
+ export const MS_PER_SLOT_N = 90_000; // Mesa slot duration in ms
76
+ export const SLOTS_PER_DAY_N = 960; // 86_400_000 / MS_PER_SLOT_N
77
+ // Slot tolerance for the globalSlotSinceGenesis precondition. Applied uniformly to
78
+ // every method that reads currentSlot. 40 slots × 90 s = 1 hour — sized to
79
+ // clear the honest-user prove+wallet+mempool latency envelope. The refund vs
80
+ // release windows stay disjoint by construction: requestRefund asserts against
81
+ // the lower bound (currentSlot ≤ purchaseSlot + REFUND_WINDOW), releaseFunds
82
+ // asserts against a threshold shifted by +TOLERANCE_WINDOW so neither can
83
+ // land on the same real slot as the other. See AUDIT_BRIEF.md §7.
84
+ export const TOLERANCE_WINDOW_N = 40;
85
+ export const EMPTY = Field(0);
86
+ export const EMPTY_ROOT = new MerkleMap().getRoot();
87
+ // ------------------------------------------------------------
88
+ // DURATION HELPERS
89
+ // ------------------------------------------------------------
90
+ export const DURATION_LABELS = ['1 Month', '1 Year', '5 Years'];
91
+ export const DURATION_INDEX = { '1 Month': 0, '1 Year': 1, '5 Years': 2 };
92
+ export function durationSlotsFromLabel(label) {
93
+ if (label === '1 Month')
94
+ return MONTHLY_SLOTS_N;
95
+ if (label === '1 Year')
96
+ return ONE_YEAR_SLOTS_N;
97
+ if (label === '5 Years')
98
+ return FIVE_YEAR_SLOTS_N;
99
+ return null;
100
+ }
101
+ export function durationIndexFromLabel(label) {
102
+ if (label === '1 Month')
103
+ return 0;
104
+ if (label === '1 Year')
105
+ return 1;
106
+ if (label === '5 Years')
107
+ return 2;
108
+ return null;
109
+ }
110
+ export function durationLabelFromIndex(idx) {
111
+ return DURATION_LABELS[idx] ?? null;
112
+ }
113
+ // ------------------------------------------------------------
114
+ // EVENTS-ONLY CONTRACT
115
+ //
116
+ // A SmartContract subclass with the same `events: { ... }` declaration as
117
+ // LicensingApp but no `@method` bodies. Used by archive-replay code to call
118
+ // `.fetchEvents()` without depending on the private circuit source.
119
+ // ------------------------------------------------------------
120
+ export class LicensingAppEventsContract extends SmartContract {
121
+ events = {
122
+ appCreated: AppCreatedEvent,
123
+ licenseIssued: LicenseIssuedEvent,
124
+ licenseIssuedNoEscrow: LicenseIssuedEvent,
125
+ licenseRenewed: ExpiryUpdateEvent,
126
+ licenseMigrated: LicenseMigratedEvent,
127
+ refundIssued: Field,
128
+ fundsReleased: Field,
129
+ priceUpdated: PriceUpdatedEvent,
130
+ };
131
+ }
132
+ // State-slot layout mirrors LicensingApp's declareState(). Read-only from here
133
+ // — no @method bodies exist. Consumers use `.version.get()`, `.licensesRoot.get()`
134
+ // etc. after fetchAccount(). `pricesPacked` is a single Field with the three
135
+ // UInt64 tier prices packed as [pM | pY<<64 | pF<<128]; unpack with
136
+ // `unpackPrices` if you need the individual tiers off-chain.
137
+ declareState(LicensingAppEventsContract, {
138
+ licensesRoot: Field,
139
+ vendor: PublicKey,
140
+ escrowRoot: Field,
141
+ pricesPacked: Field,
142
+ version: UInt32,
143
+ ancestorsRoot: Field,
144
+ });
145
+ // Off-chain mirror of the in-circuit price packing (see LicensingApp.ts). Kept
146
+ // here so the events service can decode `pricesPacked` slots without pulling
147
+ // the private contract source.
148
+ export function unpackPrices(packed) {
149
+ const raw = packed.toBigInt();
150
+ const mask = (1n << 64n) - 1n;
151
+ return {
152
+ priceMonthly: UInt64.from(raw & mask),
153
+ priceYearly: UInt64.from((raw >> 64n) & mask),
154
+ priceFiveYear: UInt64.from((raw >> 128n) & mask),
155
+ };
156
+ }
157
+ // ------------------------------------------------------------
158
+ // SUPPORTED SOURCE VERSIONS
159
+ //
160
+ // On-chain `version` slot is set at init() and can never change (setPermissions
161
+ // is impossible; setVerificationKey is impossibleDuringCurrentVersion). A
162
+ // keeper / verifier that only understands v1 state decodes must refuse to serve
163
+ // a zkApp deployed from a different source revision — otherwise it could
164
+ // mis-decode events or feed a stale state format to the verifier.
165
+ //
166
+ // Add new versions here once the corresponding decoder / verifier code path
167
+ // lands. Never remove: dropping a version is a support-removal decision that
168
+ // belongs in a release note, not silent breakage.
169
+ // ------------------------------------------------------------
170
+ export const SUPPORTED_LICENSING_APP_VERSIONS = [1];
171
+ // Read the immutable `version` slot from a deployed LicensingApp. Returns null
172
+ // when the account is not (yet) a zkApp — caller decides whether to retry
173
+ // (deploy still pending) or reject (wrong address).
174
+ export async function fetchLicensingAppVersion(zkAppAddress) {
175
+ const pub = PublicKey.fromBase58(zkAppAddress);
176
+ await fetchAccount({ publicKey: pub });
177
+ const contract = new LicensingAppEventsContract(pub);
178
+ try {
179
+ const v = contract.version.get();
180
+ return Number(v.toString());
181
+ }
182
+ catch {
183
+ return null;
184
+ }
185
+ }
186
+ // ------------------------------------------------------------
187
+ // CANONICAL VK HASHES
188
+ //
189
+ // Per-generation, append-only. The keeper boots with `LicensingApp.compile()`
190
+ // and asserts the resulting VK hash matches `CANONICAL_VK_HASH` (the current
191
+ // generation's row); the verify service resolves an address's generation via
192
+ // AppRecord.deploymentHistory and asserts the on-chain VK hash matches
193
+ // `CANONICAL_VK_HASHES[generation]`. Retired-generation licenses (e.g. a
194
+ // 5-year buyer whose license lives on v1 after the platform bumped to v2/v3)
195
+ // continue to verify because the historical row is still present. A genuine
196
+ // old circuit deployed at a fresh address fails: the address is not in any
197
+ // `deploymentHistory[]`, so the resolver returns null and verify rejects.
198
+ //
199
+ // Rules:
200
+ // - Append the new generation's row at each hardfork migration; never
201
+ // delete historical rows.
202
+ // - Bump `CURRENT_GENERATION` to the new key alongside the row.
203
+ // - `CANONICAL_VK_HASH` is derived from the record — drift test and keeper
204
+ // boot-assert are unchanged.
205
+ // ------------------------------------------------------------
206
+ export const CANONICAL_VK_HASHES = {
207
+ 1: '25510637873522061606403949338800808972643356911784243535869508122414992770753',
208
+ // 2: '…' — append at each hardfork migration, never delete.
209
+ };
210
+ export const CURRENT_GENERATION = 1;
211
+ export const CANONICAL_VK_HASH = CANONICAL_VK_HASHES[CURRENT_GENERATION];
212
+ export const CANONICAL_LICENSE_PROOF_VK_HASH = '6592142093294558010952934797827640785154417146015648013584661926355710165692';
213
+ //# sourceMappingURL=contractInterface.js.map
@@ -0,0 +1,37 @@
1
+ /**
2
+ * countryStats.ts
3
+ *
4
+ * Buyer-country breakdown of a vendor's revenue, bucketed by UTC calendar year.
5
+ * Structured for tax reporting: one row per (year, appId, countryCode) with
6
+ * license count + the vendor's cut in MINA. Both the keeper (source of
7
+ * truth for /stats) and the dashboard UI import from here so the numbers can
8
+ * never drift between them.
9
+ *
10
+ * Purchases attribute to the year the buyer confirmed on-chain (LicenseRecord
11
+ * `createdAt` ISO timestamp). Legacy records that predate `createdAt` fall
12
+ * back to slot-derived time. Renewals attribute to the renewal year via
13
+ * `renewals[i].slot → time`. Refunded purchases contribute zero revenue but
14
+ * still count the license (a purchase happened even if the money was
15
+ * returned) — remove `count` from refunded rows client-side if you want a
16
+ * strictly net view.
17
+ *
18
+ * Browser-safe: pure computation, no fs, no fetch.
19
+ */
20
+ import type { LicenseRecord } from './licenseRecord.js';
21
+ export type CountryBucket = {
22
+ count: number;
23
+ revenueMina: number;
24
+ };
25
+ export type CountryYearBreakdown = {
26
+ aggregate: Record<string, CountryBucket>;
27
+ byApp: Record<string, Record<string, CountryBucket>>;
28
+ };
29
+ export type CountryBreakdown = {
30
+ years: number[];
31
+ byYear: Record<number, CountryYearBreakdown>;
32
+ };
33
+ export declare function computeCountryBreakdown(apps: {
34
+ appId: string;
35
+ zkAppAddress: string;
36
+ }[], records: LicenseRecord[], currentSlot: number, nowMs: number): CountryBreakdown;
37
+ //# sourceMappingURL=countryStats.d.ts.map
@@ -0,0 +1,89 @@
1
+ // Copyright (c) 2025-2026 zkLicensing project developers
2
+ // All rights reserved.
3
+ // LicensingApp charges a 2 % platform fee (200 basis points); the vendor
4
+ // receives the remaining 9 800 bps. Keep this in sync with the on-chain
5
+ // constant in LicensingApp.
6
+ const PLATFORM_FEE_BASIS_POINTS = 200n;
7
+ const VENDOR_BPS = 10000n - PLATFORM_FEE_BASIS_POINTS;
8
+ // 90 s/slot post-Mesa. Used only for slot→time fallback attribution.
9
+ const SLOT_DURATION_MS = 90_000;
10
+ export function computeCountryBreakdown(apps, records, currentSlot, nowMs) {
11
+ const appByAddress = new Map(apps.map(a => [a.zkAppAddress, a.appId]));
12
+ const byYear = {};
13
+ const yearSet = new Set();
14
+ const ensureYear = (year) => {
15
+ let yb = byYear[year];
16
+ if (!yb) {
17
+ yb = { aggregate: {}, byApp: {} };
18
+ byYear[year] = yb;
19
+ yearSet.add(year);
20
+ }
21
+ return yb;
22
+ };
23
+ const ensureAppBucket = (yb, appId) => {
24
+ let ab = yb.byApp[appId];
25
+ if (!ab) {
26
+ ab = {};
27
+ yb.byApp[appId] = ab;
28
+ }
29
+ return ab;
30
+ };
31
+ const add = (bucket, code, count, revenueNano) => {
32
+ let b = bucket[code];
33
+ if (!b) {
34
+ b = { count: 0, revenueMina: 0 };
35
+ bucket[code] = b;
36
+ }
37
+ b.count += count;
38
+ b.revenueMina += Number(revenueNano) / 1e9;
39
+ };
40
+ const slotToYear = (slot) => {
41
+ if (currentSlot <= 0)
42
+ return null;
43
+ const ms = nowMs - (currentSlot - slot) * SLOT_DURATION_MS;
44
+ const y = new Date(ms).getUTCFullYear();
45
+ return Number.isFinite(y) ? y : null;
46
+ };
47
+ for (const r of records) {
48
+ const appId = appByAddress.get(r.zkAppAddress);
49
+ if (!appId)
50
+ continue;
51
+ const country = r.buyerCountry && /^[A-Z]{2}$/.test(r.buyerCountry) ? r.buyerCountry : '??';
52
+ // Purchase — attribute to the year of confirm-on-chain (createdAt).
53
+ // Fallback: slot-derived time for legacy records that lack createdAt.
54
+ let purchaseYear = null;
55
+ if (r.createdAt) {
56
+ const y = new Date(r.createdAt).getUTCFullYear();
57
+ if (Number.isFinite(y))
58
+ purchaseYear = y;
59
+ }
60
+ if (purchaseYear === null && r.purchaseSlot > 0) {
61
+ purchaseYear = slotToYear(r.purchaseSlot);
62
+ }
63
+ if (purchaseYear !== null) {
64
+ const yb = ensureYear(purchaseYear);
65
+ const ab = ensureAppBucket(yb, appId);
66
+ const purchaseCut = (!r.refunded && r.purchaseAmount && r.purchaseAmount !== '0')
67
+ ? (BigInt(r.purchaseAmount) * VENDOR_BPS) / 10000n
68
+ : 0n;
69
+ add(ab, country, 1, purchaseCut);
70
+ add(yb.aggregate, country, 1, purchaseCut);
71
+ }
72
+ // Renewals — revenue only (no license count), attributed to renewal year.
73
+ for (const rn of r.renewals ?? []) {
74
+ const y = slotToYear(rn.slot);
75
+ if (y === null)
76
+ continue;
77
+ const yb = ensureYear(y);
78
+ const ab = ensureAppBucket(yb, appId);
79
+ const cut = (BigInt(rn.amount) * VENDOR_BPS) / 10000n;
80
+ add(ab, country, 0, cut);
81
+ add(yb.aggregate, country, 0, cut);
82
+ }
83
+ }
84
+ return {
85
+ years: Array.from(yearSet).sort((a, b) => b - a),
86
+ byYear,
87
+ };
88
+ }
89
+ //# sourceMappingURL=countryStats.js.map
@@ -0,0 +1,168 @@
1
+ /**
2
+ * deployClient.ts
3
+ *
4
+ * Browser-safe client for the deploy flow. Mirrors the buy / renew / refund
5
+ * client shape: `proveDeploy` posts to the keeper's `/prove/deploy` endpoint
6
+ * and returns the atomic deploy+initialize tx already signed with the
7
+ * throwaway zkApp key. The vendor's wallet then adds the fee-payer signature
8
+ * and broadcasts.
9
+ *
10
+ * The atomic bundling is what makes single-shot initialization safe:
11
+ * `provenTxJson` holds both `zkApp.deploy()` and `zkApp.initialize()`
12
+ * AccountUpdates in the same transaction, and the keeper generates a
13
+ * throwaway zkApp keypair per request that is discarded before the tx is
14
+ * returned — so the target address is not observable to an attacker until
15
+ * the initialize AU inside is already signed and locked to `(vendor,
16
+ * prices)`. Combined with the in-circuit `vendor.requireEquals(empty())`
17
+ * precondition on `initialize()`, this yields a one-shot init per account.
18
+ *
19
+ * Also exports admin-side `redeployApp` / `redeployAll` for the platform
20
+ * operator to trigger post-hardfork v1→v2 migrations via the keeper's
21
+ * `/admin/redeploy*` endpoints (bearer-token auth).
22
+ */
23
+ export type ProveDeployRequest = {
24
+ keeperUrl: string;
25
+ senderPublicKey: string;
26
+ vendorAddress: string;
27
+ priceMonthlyMina: number;
28
+ priceYearlyMina: number;
29
+ priceFiveYearMina: number;
30
+ fetcher?: typeof fetch;
31
+ signal?: AbortSignal;
32
+ };
33
+ export type ProveDeployResponse = {
34
+ provenTxJson: string;
35
+ verificationKeyHash: string;
36
+ zkAppAddress: string;
37
+ };
38
+ export type DeployStatus = {
39
+ status: 'unknown' | 'pending' | 'confirmed' | 'failed';
40
+ landedAtSlot?: number;
41
+ elapsedMs?: number;
42
+ };
43
+ export declare function proveDeploy(req: ProveDeployRequest): Promise<ProveDeployResponse>;
44
+ export type PollDeployStatusOptions = {
45
+ keeperUrl: string;
46
+ zkAppAddress: string;
47
+ fetcher?: typeof fetch;
48
+ intervalMs?: number;
49
+ timeoutMs?: number;
50
+ signal?: AbortSignal;
51
+ onTick?: (status: DeployStatus) => void;
52
+ };
53
+ export declare function pollDeployStatus(opts: PollDeployStatusOptions): Promise<DeployStatus>;
54
+ export type RedeployAppRequest = {
55
+ keeperUrl: string;
56
+ appId: string;
57
+ adminToken: string;
58
+ force?: boolean;
59
+ fetcher?: typeof fetch;
60
+ signal?: AbortSignal;
61
+ };
62
+ export type RedeployAppResponse = {
63
+ ok: true;
64
+ zkAppAddress: string;
65
+ txHash: string;
66
+ verificationKeyHash: string;
67
+ onChainVersion: number;
68
+ forcedEscrowNonEmpty?: true;
69
+ };
70
+ export type RedeployAllRequest = {
71
+ keeperUrl: string;
72
+ adminToken: string;
73
+ force?: boolean;
74
+ fetcher?: typeof fetch;
75
+ signal?: AbortSignal;
76
+ };
77
+ export type RedeployAllResponse = {
78
+ ok: true;
79
+ count: number;
80
+ succeeded: number;
81
+ results: Array<{
82
+ appId: string;
83
+ ok: true;
84
+ zkAppAddress: string;
85
+ txHash: string;
86
+ verificationKeyHash: string;
87
+ onChainVersion: number;
88
+ forcedEscrowNonEmpty?: true;
89
+ } | {
90
+ appId: string;
91
+ ok: false;
92
+ error: string;
93
+ }>;
94
+ };
95
+ export declare function redeployApp(req: RedeployAppRequest): Promise<RedeployAppResponse>;
96
+ export declare function redeployAll(req: RedeployAllRequest): Promise<RedeployAllResponse>;
97
+ export type VendorRedeployPreflight = {
98
+ ok: boolean;
99
+ freezeSlotConfigured: boolean;
100
+ appExists: boolean;
101
+ status?: 'active' | 'inactive' | 'pending';
102
+ refundWindow: {
103
+ closed: boolean;
104
+ opensAtSlot: number;
105
+ currentSlot: number;
106
+ };
107
+ onChainAccount: {
108
+ exists: boolean;
109
+ };
110
+ escrow: {
111
+ empty: boolean;
112
+ };
113
+ version: {
114
+ current: number;
115
+ supported: boolean;
116
+ supportedList: readonly number[];
117
+ };
118
+ reasons: string[];
119
+ };
120
+ export declare function fetchVendorRedeployPreflight(opts: {
121
+ keeperUrl: string;
122
+ appId: string;
123
+ fetcher?: typeof fetch;
124
+ signal?: AbortSignal;
125
+ }): Promise<VendorRedeployPreflight>;
126
+ export type VendorRedeployChallenge = {
127
+ nonce: string;
128
+ expiresAt: number;
129
+ };
130
+ export declare function getVendorRedeployChallenge(opts: {
131
+ keeperUrl: string;
132
+ appId: string;
133
+ fetcher?: typeof fetch;
134
+ signal?: AbortSignal;
135
+ }): Promise<VendorRedeployChallenge>;
136
+ export type ProveVendorRedeployRequest = {
137
+ keeperUrl: string;
138
+ appId: string;
139
+ nonce: string;
140
+ signature: {
141
+ field: string;
142
+ scalar: string;
143
+ };
144
+ senderPublicKey: string;
145
+ force?: boolean;
146
+ fetcher?: typeof fetch;
147
+ signal?: AbortSignal;
148
+ };
149
+ export type ProveVendorRedeployResponse = {
150
+ provenTxJson: string;
151
+ zkAppAddress: string;
152
+ verificationKeyHash: string;
153
+ redeployId: string;
154
+ forcedEscrowNonEmpty?: true;
155
+ };
156
+ export declare function proveVendorRedeploy(req: ProveVendorRedeployRequest): Promise<ProveVendorRedeployResponse>;
157
+ export type RegisterVendorRedeployRequest = {
158
+ keeperUrl: string;
159
+ appId: string;
160
+ redeployId: string;
161
+ txHash: string;
162
+ fetcher?: typeof fetch;
163
+ signal?: AbortSignal;
164
+ };
165
+ export declare function registerVendorRedeploy(req: RegisterVendorRedeployRequest): Promise<{
166
+ ok: true;
167
+ }>;
168
+ //# sourceMappingURL=deployClient.d.ts.map
@@ -0,0 +1,172 @@
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/deploy — keeper generates the throwaway zkApp keypair, assembles
16
+ // the deploy+initialize tx, proves it, and signs the zkApp AccountUpdate with
17
+ // the throwaway key. The caller forwards `provenTxJson` to the vendor's wallet
18
+ // for the fee-payer signature.
19
+ //
20
+ // At least one of the three prices must be > 0 (enforced by the keeper *and*
21
+ // by the circuit's `initialize()`).
22
+ export async function proveDeploy(req) {
23
+ const fetcher = req.fetcher ?? fetch;
24
+ const resp = await fetcher(`${baseUrl(req.keeperUrl)}/prove/deploy`, {
25
+ method: 'POST',
26
+ headers: { 'Content-Type': 'application/json' },
27
+ signal: req.signal,
28
+ body: JSON.stringify({
29
+ senderPublicKey: req.senderPublicKey,
30
+ vendorAddress: req.vendorAddress,
31
+ priceMonthlyMina: req.priceMonthlyMina,
32
+ priceYearlyMina: req.priceYearlyMina,
33
+ priceFiveYearMina: req.priceFiveYearMina,
34
+ }),
35
+ });
36
+ if (!resp.ok)
37
+ throw new Error(await readJsonError(resp));
38
+ return await resp.json();
39
+ }
40
+ // Polls GET /deploy/status until it returns a terminal state ('confirmed' or
41
+ // 'failed') or the timeout fires. 'unknown' means the keeper hasn't seen the
42
+ // tx yet (register step not called or watch not armed).
43
+ export async function pollDeployStatus(opts) {
44
+ const fetcher = opts.fetcher ?? fetch;
45
+ const interval = opts.intervalMs ?? 3_000;
46
+ const timeout = opts.timeoutMs ?? 10 * 60 * 1000;
47
+ const deadline = Date.now() + timeout;
48
+ const url = `${baseUrl(opts.keeperUrl)}/deploy/status?zkAppAddress=${encodeURIComponent(opts.zkAppAddress)}`;
49
+ while (true) {
50
+ if (opts.signal?.aborted)
51
+ throw new Error('pollDeployStatus aborted');
52
+ let status = { status: 'unknown' };
53
+ try {
54
+ const resp = await fetcher(url, { signal: opts.signal });
55
+ if (resp.ok)
56
+ status = await resp.json();
57
+ }
58
+ catch { /* transient network — try again on next tick */ }
59
+ opts.onTick?.(status);
60
+ if (status.status === 'confirmed' || status.status === 'failed')
61
+ return status;
62
+ if (Date.now() >= deadline)
63
+ return status;
64
+ await new Promise((r) => setTimeout(r, interval));
65
+ }
66
+ }
67
+ function adminHeaders(token) {
68
+ return {
69
+ 'Content-Type': 'application/json',
70
+ 'Authorization': `Bearer ${token}`,
71
+ };
72
+ }
73
+ // POST /admin/redeploy/:appId — one-shot migration for a single app. Rejects
74
+ // with a descriptive error if any keeper-side guard fails (409) or the deploy
75
+ // itself throws (500). The returned `zkAppAddress` is the *new* v2 address; the
76
+ // old one stays on chain but is no longer referenced by keeper records.
77
+ export async function redeployApp(req) {
78
+ const fetcher = req.fetcher ?? fetch;
79
+ const resp = await fetcher(`${baseUrl(req.keeperUrl)}/admin/redeploy/${encodeURIComponent(req.appId)}`, {
80
+ method: 'POST',
81
+ headers: adminHeaders(req.adminToken),
82
+ signal: req.signal,
83
+ body: JSON.stringify(req.force ? { force: true } : {}),
84
+ });
85
+ if (!resp.ok)
86
+ throw new Error(await readJsonError(resp));
87
+ return await resp.json();
88
+ }
89
+ // POST /admin/redeploy-all — iterate every registered app and attempt migration.
90
+ // Idempotent: apps already migrated surface as ok:false with a clear error, so
91
+ // a rerun after a partial failure retries only the remaining ones. Callers
92
+ // should inspect `results` for per-app outcomes rather than relying on the
93
+ // top-level `ok`, which is always true unless the whole request failed.
94
+ export async function redeployAll(req) {
95
+ const fetcher = req.fetcher ?? fetch;
96
+ const resp = await fetcher(`${baseUrl(req.keeperUrl)}/admin/redeploy-all`, {
97
+ method: 'POST',
98
+ headers: adminHeaders(req.adminToken),
99
+ signal: req.signal,
100
+ body: JSON.stringify(req.force ? { force: true } : {}),
101
+ });
102
+ if (!resp.ok)
103
+ throw new Error(await readJsonError(resp));
104
+ return await resp.json();
105
+ }
106
+ // GET /vendor/redeploy/:appId/preflight — read-only guard status. No nonce
107
+ // consumed, no signature required. UI decides button state from the returned
108
+ // fields; 404 means the app itself is unknown (a distinct failure mode from a
109
+ // known app failing individual guards).
110
+ export async function fetchVendorRedeployPreflight(opts) {
111
+ const fetcher = opts.fetcher ?? fetch;
112
+ const resp = await fetcher(`${baseUrl(opts.keeperUrl)}/vendor/redeploy/${encodeURIComponent(opts.appId)}/preflight`, {
113
+ signal: opts.signal,
114
+ });
115
+ if (!resp.ok)
116
+ throw new Error(await readJsonError(resp));
117
+ return await resp.json();
118
+ }
119
+ // GET /vendor/redeploy/:appId/challenge — mint a one-shot nonce that the
120
+ // vendor's wallet signs. The nonce is bound to the appId and expires in 60s;
121
+ // consumed on /prove.
122
+ export async function getVendorRedeployChallenge(opts) {
123
+ const fetcher = opts.fetcher ?? fetch;
124
+ const resp = await fetcher(`${baseUrl(opts.keeperUrl)}/vendor/redeploy/${encodeURIComponent(opts.appId)}/challenge`, {
125
+ signal: opts.signal,
126
+ });
127
+ if (!resp.ok)
128
+ throw new Error(await readJsonError(resp));
129
+ return await resp.json();
130
+ }
131
+ // POST /vendor/redeploy/:appId/prove — keeper verifies the nonce + signature
132
+ // against the on-chain vendor of the app, runs all redeploy guards, generates
133
+ // a throwaway zkApp keypair, builds and proves the deploy+initialize tx, and
134
+ // signs the zkApp AU with the throwaway key. Returns the tx JSON for the
135
+ // vendor's wallet to add the fee-payer signature and broadcast.
136
+ export async function proveVendorRedeploy(req) {
137
+ const fetcher = req.fetcher ?? fetch;
138
+ const resp = await fetcher(`${baseUrl(req.keeperUrl)}/vendor/redeploy/${encodeURIComponent(req.appId)}/prove`, {
139
+ method: 'POST',
140
+ headers: { 'Content-Type': 'application/json' },
141
+ signal: req.signal,
142
+ body: JSON.stringify({
143
+ nonce: req.nonce,
144
+ signature: req.signature,
145
+ senderPublicKey: req.senderPublicKey,
146
+ ...(req.force ? { force: true } : {}),
147
+ }),
148
+ });
149
+ if (!resp.ok)
150
+ throw new Error(await readJsonError(resp));
151
+ return await resp.json();
152
+ }
153
+ // POST /vendor/redeploy/:appId/register — arm the keeper's deploy-watcher on
154
+ // the new address. On confirmation the keeper mutates AppRecord to promote
155
+ // the new deployment (push old to deploymentHistory[], overwrite live fields,
156
+ // clear event-sync cursors). Idempotent to duplicate calls (the watcher
157
+ // checks whether one is already armed for the address). Poll GET
158
+ // /deploy/status?zkAppAddress=<new> — or use pollDeployStatus — for landing
159
+ // progress, same as first-time deploys.
160
+ export async function registerVendorRedeploy(req) {
161
+ const fetcher = req.fetcher ?? fetch;
162
+ const resp = await fetcher(`${baseUrl(req.keeperUrl)}/vendor/redeploy/${encodeURIComponent(req.appId)}/register`, {
163
+ method: 'POST',
164
+ headers: { 'Content-Type': 'application/json' },
165
+ signal: req.signal,
166
+ body: JSON.stringify({ redeployId: req.redeployId, txHash: req.txHash }),
167
+ });
168
+ if (!resp.ok)
169
+ throw new Error(await readJsonError(resp));
170
+ return await resp.json();
171
+ }
172
+ //# sourceMappingURL=deployClient.js.map