zklicensing 0.1.0-beta.2 → 0.1.0-beta.4

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 (173) hide show
  1. package/README.md +335 -97
  2. package/build/appsLookup.d.ts +14 -0
  3. package/build/appsLookup.d.ts.map +1 -0
  4. package/build/appsLookup.js +12 -0
  5. package/build/appsLookup.js.map +1 -0
  6. package/build/archiveBootstrap.d.ts +15 -1
  7. package/build/archiveBootstrap.d.ts.map +1 -0
  8. package/build/archiveBootstrap.js +62 -9
  9. package/build/archiveBootstrap.js.map +1 -0
  10. package/build/bindClient.d.ts +51 -0
  11. package/build/bindClient.d.ts.map +1 -0
  12. package/build/bindClient.js +115 -0
  13. package/build/bindClient.js.map +1 -0
  14. package/build/buyClient.d.ts +10 -3
  15. package/build/buyClient.d.ts.map +1 -0
  16. package/build/buyClient.js +33 -15
  17. package/build/buyClient.js.map +1 -0
  18. package/build/contractInterface.d.ts +11 -5
  19. package/build/contractInterface.d.ts.map +1 -0
  20. package/build/contractInterface.js +82 -17
  21. package/build/contractInterface.js.map +1 -0
  22. package/build/countryStats.d.ts +6 -1
  23. package/build/countryStats.d.ts.map +1 -0
  24. package/build/countryStats.js +33 -9
  25. package/build/countryStats.js.map +1 -0
  26. package/build/deployClient.d.ts.map +1 -0
  27. package/build/deployClient.js +1 -1
  28. package/build/deployClient.js.map +1 -0
  29. package/build/deviceIdentity.d.ts +68 -0
  30. package/build/deviceIdentity.d.ts.map +1 -0
  31. package/build/deviceIdentity.js +209 -0
  32. package/build/deviceIdentity.js.map +1 -0
  33. package/build/freezeClient.d.ts.map +1 -0
  34. package/build/freezeClient.js +1 -1
  35. package/build/freezeClient.js.map +1 -0
  36. package/build/generationClient.d.ts +1 -0
  37. package/build/generationClient.d.ts.map +1 -0
  38. package/build/generationClient.js +2 -2
  39. package/build/generationClient.js.map +1 -0
  40. package/build/index.d.ts +27 -37
  41. package/build/index.d.ts.map +1 -0
  42. package/build/index.js +85 -116
  43. package/build/index.js.map +1 -0
  44. package/build/licenseLeaf.d.ts +76 -0
  45. package/build/licenseLeaf.d.ts.map +1 -0
  46. package/build/licenseLeaf.js +189 -0
  47. package/build/licenseLeaf.js.map +1 -0
  48. package/build/licenseRecord.d.ts +20 -1
  49. package/build/licenseRecord.d.ts.map +1 -0
  50. package/build/licenseRecord.js +1 -1
  51. package/build/licenseRecord.js.map +1 -0
  52. package/build/licenseStore.d.ts +17 -2
  53. package/build/licenseStore.d.ts.map +1 -0
  54. package/build/licenseStore.js +167 -7
  55. package/build/licenseStore.js.map +1 -0
  56. package/build/manifestVerify.d.ts.map +1 -0
  57. package/build/manifestVerify.js +8 -3
  58. package/build/manifestVerify.js.map +1 -0
  59. package/build/migrateClient.d.ts +6 -21
  60. package/build/migrateClient.d.ts.map +1 -0
  61. package/build/migrateClient.js +30 -17
  62. package/build/migrateClient.js.map +1 -0
  63. package/build/onChainRoot.d.ts +67 -0
  64. package/build/onChainRoot.d.ts.map +1 -0
  65. package/build/onChainRoot.js +87 -0
  66. package/build/onChainRoot.js.map +1 -0
  67. package/build/ownershipLicenseHash.d.ts +25 -0
  68. package/build/ownershipLicenseHash.d.ts.map +1 -0
  69. package/build/ownershipLicenseHash.js +29 -0
  70. package/build/ownershipLicenseHash.js.map +1 -0
  71. package/build/ownershipSignature.d.ts +122 -0
  72. package/build/ownershipSignature.d.ts.map +1 -0
  73. package/build/ownershipSignature.js +244 -0
  74. package/build/ownershipSignature.js.map +1 -0
  75. package/build/proveErrors.d.ts +35 -0
  76. package/build/proveErrors.d.ts.map +1 -0
  77. package/build/proveErrors.js +49 -0
  78. package/build/proveErrors.js.map +1 -0
  79. package/build/refundClient.d.ts +2 -11
  80. package/build/refundClient.d.ts.map +1 -0
  81. package/build/refundClient.js +35 -17
  82. package/build/refundClient.js.map +1 -0
  83. package/build/renewClient.d.ts +4 -1
  84. package/build/renewClient.d.ts.map +1 -0
  85. package/build/renewClient.js +11 -4
  86. package/build/renewClient.js.map +1 -0
  87. package/build/seatName.d.ts +8 -0
  88. package/build/seatName.d.ts.map +1 -0
  89. package/build/seatName.js +136 -0
  90. package/build/seatName.js.map +1 -0
  91. package/build/statsClient.d.ts +47 -4
  92. package/build/statsClient.d.ts.map +1 -0
  93. package/build/statsClient.js +44 -5
  94. package/build/statsClient.js.map +1 -0
  95. package/build/trustAnchorVerify.d.ts +48 -0
  96. package/build/trustAnchorVerify.d.ts.map +1 -0
  97. package/build/trustAnchorVerify.js +431 -0
  98. package/build/trustAnchorVerify.js.map +1 -0
  99. package/build/trustAnchorVerifyCli.d.ts +35 -0
  100. package/build/trustAnchorVerifyCli.d.ts.map +1 -0
  101. package/build/trustAnchorVerifyCli.js +177 -0
  102. package/build/trustAnchorVerifyCli.js.map +1 -0
  103. package/build/txBroadcastClient.d.ts +26 -0
  104. package/build/txBroadcastClient.d.ts.map +1 -0
  105. package/build/txBroadcastClient.js +20 -0
  106. package/build/txBroadcastClient.js.map +1 -0
  107. package/build/unbindClient.d.ts +56 -0
  108. package/build/unbindClient.d.ts.map +1 -0
  109. package/build/unbindClient.js +113 -0
  110. package/build/unbindClient.js.map +1 -0
  111. package/build/vendorAccountClient.d.ts +56 -0
  112. package/build/vendorAccountClient.d.ts.map +1 -0
  113. package/build/vendorAccountClient.js +47 -0
  114. package/build/vendorAccountClient.js.map +1 -0
  115. package/build/verifyCore.d.ts +1 -1
  116. package/build/verifyCore.d.ts.map +1 -0
  117. package/build/verifyCore.js +11 -4
  118. package/build/verifyCore.js.map +1 -0
  119. package/build/verifyOffline.d.ts +85 -0
  120. package/build/verifyOffline.d.ts.map +1 -0
  121. package/build/verifyOffline.js +139 -0
  122. package/build/verifyOffline.js.map +1 -0
  123. package/build/verifyService.d.ts.map +1 -0
  124. package/build/verifyService.js +582 -455
  125. package/build/verifyService.js.map +1 -0
  126. package/build/verifyWithAncestors.d.ts +18 -0
  127. package/build/verifyWithAncestors.d.ts.map +1 -0
  128. package/build/verifyWithAncestors.js +119 -0
  129. package/build/verifyWithAncestors.js.map +1 -0
  130. package/build/zipToState.d.ts +2 -0
  131. package/build/zipToState.d.ts.map +1 -0
  132. package/build/zipToState.js +94 -0
  133. package/build/zipToState.js.map +1 -0
  134. package/package.json +57 -13
  135. package/src/appsLookup.ts +48 -0
  136. package/src/archiveBootstrap.ts +342 -0
  137. package/src/bindClient.ts +175 -0
  138. package/src/buyClient.ts +304 -0
  139. package/src/contractInterface.d.ts +61 -0
  140. package/src/contractInterface.ts +313 -0
  141. package/src/countryStats.ts +171 -0
  142. package/src/deployClient.ts +388 -0
  143. package/src/deviceIdentity.ts +249 -0
  144. package/src/freezeClient.ts +117 -0
  145. package/src/generationClient.ts +165 -0
  146. package/src/index.ts +329 -0
  147. package/src/licenseLeaf.ts +221 -0
  148. package/src/licenseRecord.ts +108 -0
  149. package/src/licenseStore.ts +538 -0
  150. package/src/manifestVerify.ts +111 -0
  151. package/src/migrateClient.ts +84 -0
  152. package/src/onChainRoot.ts +107 -0
  153. package/src/ownershipLicenseHash.ts +31 -0
  154. package/src/ownershipSignature.ts +354 -0
  155. package/src/proveErrors.ts +70 -0
  156. package/src/refundClient.ts +158 -0
  157. package/src/renewClient.ts +214 -0
  158. package/src/seatName.ts +165 -0
  159. package/src/statsClient.ts +235 -0
  160. package/src/trustAnchorVerify.ts +548 -0
  161. package/src/trustAnchorVerifyCli.ts +179 -0
  162. package/src/txBroadcastClient.ts +48 -0
  163. package/src/unbindClient.ts +149 -0
  164. package/src/vendorAccountClient.ts +110 -0
  165. package/src/verifyCore.ts +92 -0
  166. package/src/verifyOffline.ts +212 -0
  167. package/src/verifyService.ts +969 -0
  168. package/src/verifyWithAncestors.ts +153 -0
  169. package/src/zipToState.ts +94 -0
  170. package/build/licenseProof.d.ts +0 -26
  171. package/build/licenseProof.js +0 -41
  172. package/build/ownership.d.ts +0 -34
  173. package/build/ownership.js +0 -147
package/build/index.js CHANGED
@@ -1,54 +1,43 @@
1
1
  // Copyright (c) 2025-2026 zkLicensing project developers
2
- // All rights reserved.
2
+ // SPDX-License-Identifier: BUSL-1.1
3
3
  /**
4
4
  * verifyLicense — client SDK.
5
5
  *
6
- * Calls the keeper's GET /verify until the on-chain refund window for the
7
- * license has closed; from that point on returns from a local anchor and
8
- * never touches the network again. The refund-window decision is server-
9
- * side: the keeper returns `refundWindowClosed: boolean` on its /verify
10
- * response, computed from its own record of `purchaseSlot + REFUND_WINDOW_N`
11
- * against the on-chain `currentSlot`. The SDK gates anchor writes on that
12
- * boolean and never trusts a `purchaseSlot` supplied by the local proof
13
- * file, so a user who forges their proof metadata cannot force the offline
14
- * branch prematurely.
6
+ * Calls the licensing service's GET /verify until the on-chain refund
7
+ * window for the license has closed; from that point on returns from a
8
+ * local anchor and never touches the network again. The refund-window
9
+ * decision is server-side: the service returns `refundWindowClosed:
10
+ * boolean` on its /verify response, computed from its own record of
11
+ * `purchaseSlot + REFUND_WINDOW_N` against the on-chain `currentSlot`.
12
+ * The SDK gates anchor writes on that boolean and never trusts a
13
+ * `purchaseSlot` supplied by the local proof file, so a user who forges
14
+ * their proof metadata cannot force the offline branch prematurely.
15
15
  *
16
16
  * Anchor purity: every field persisted in the Anchor derives from the
17
- * keeper's /verify response — `anchoredSlot`, `expirySlot`, `ownership`.
18
- * The only client-supplied input to the offline branch is
19
- * `proof.expirySlot`, used solely as the loadAnchor binding key. A
20
- * tampered proof file therefore never gets a wrong answer; it gets no
21
- * offline branch at all and falls through to the keeper on every call.
17
+ * service's /verify response — `anchoredSlot`, `expirySlot`. The only
18
+ * client-supplied input to the offline branch is `proof.expirySlot`,
19
+ * used solely as the loadAnchor binding key. A tampered proof file
20
+ * therefore never gets a wrong answer; it gets no offline branch at all
21
+ * and falls through to the licensing service on every call.
22
22
  */
23
23
  import { REFUND_WINDOW_N, GRACE_PERIOD_N, MS_PER_SLOT_N, SLOTS_PER_DAY_N, } from './contractInterface.js';
24
24
  const MS_PER_SLOT = MS_PER_SLOT_N;
25
25
  const SLOTS_PER_DAY = SLOTS_PER_DAY_N;
26
+ // The one and only verify endpoint. Every issued license routes here
27
+ // regardless of network — the platform verifier resolves devnet vs
28
+ // mainnet from the zkAppAddress (each deployment is unique per network).
29
+ // Tests inject a mock via VerifyOptions.fetcher, not by rewriting this URL.
30
+ export const DEFAULT_VERIFIER_URL = 'https://zklicensing.com/api/verify';
31
+ // Anchor records are keyed by (zkAppAddress, licenseHash). zkAppAddress
32
+ // is generation-scoped by construction: each hardfork migration deploys
33
+ // the LicensingApp at a fresh address (see keeper's finalizeRedeploy
34
+ // and AppRecord.deploymentHistory[]), so keying on it implicitly
35
+ // captures generation. An anchor written pre-migration is orphaned by
36
+ // the new address on next verify and re-created cleanly — no
37
+ // cross-generation carry-over.
26
38
  const ANCHOR_KEY_PREFIX = 'zklic_anchor_';
27
- const ACTIVATION_KEY_PREFIX = 'zklic_activation_';
28
- function activationKey(zkAppAddress, licenseHash) {
29
- return `${ACTIVATION_KEY_PREFIX}${zkAppAddress}_${licenseHash}`;
30
- }
31
- export function loadActivation(storage, zkAppAddress, licenseHash, nowMs = Date.now()) {
32
- try {
33
- const raw = storage.getItem(activationKey(zkAppAddress, licenseHash));
34
- if (!raw)
35
- return null;
36
- const rec = JSON.parse(raw);
37
- if (nowMs > rec.expiresAt)
38
- return null;
39
- return rec;
40
- }
41
- catch {
42
- return null;
43
- }
44
- }
45
- export function saveActivation(storage, zkAppAddress, licenseHash, rec) {
46
- try {
47
- storage.setItem(activationKey(zkAppAddress, licenseHash), JSON.stringify(rec));
48
- }
49
- catch {
50
- /* quota / read-only — non-fatal */
51
- }
39
+ function anchorKey(zkAppAddress, licenseHash) {
40
+ return `${ANCHOR_KEY_PREFIX}${zkAppAddress}_${licenseHash}`;
52
41
  }
53
42
  function defaultStorage() {
54
43
  if (typeof globalThis !== 'undefined' && globalThis.localStorage) {
@@ -56,19 +45,20 @@ function defaultStorage() {
56
45
  }
57
46
  return null;
58
47
  }
59
- function loadAnchor(storage, licenseHash, expirySlot) {
48
+ function loadAnchor(storage, zkAppAddress, licenseHash, expirySlot) {
60
49
  try {
61
- const raw = storage.getItem(ANCHOR_KEY_PREFIX + licenseHash);
50
+ const raw = storage.getItem(anchorKey(zkAppAddress, licenseHash));
62
51
  if (!raw)
63
52
  return null;
64
53
  const a = JSON.parse(raw);
65
- // `a.expirySlot` is the KEEPER's value captured at anchor time; the
66
- // `expirySlot` argument is the LOCAL proof file's value. Match ⇒ local
67
- // file agrees with chain truth as of anchoring ⇒ offline is safe.
68
- // Mismatch has two honest meanings:
54
+ // `a.expirySlot` is the SERVICE's value captured at anchor time;
55
+ // the `expirySlot` argument is the LOCAL proof file's value. Match
56
+ // ⇒ local file agrees with chain truth as of anchoring ⇒ offline is
57
+ // safe. Mismatch has two honest meanings:
69
58
  // - renewal: chain moved, local file updated ⇒ re-anchor via chain check
70
- // - forgery: local file edited ⇒ every call falls through to the keeper,
71
- // which reports truth. The forger gets no offline branch at all.
59
+ // - forgery: local file edited ⇒ every call falls through to the
60
+ // licensing service, which reports truth. The forger gets no
61
+ // offline branch at all.
72
62
  if (a.expirySlot !== expirySlot)
73
63
  return null;
74
64
  return a;
@@ -77,9 +67,9 @@ function loadAnchor(storage, licenseHash, expirySlot) {
77
67
  return null;
78
68
  }
79
69
  }
80
- function saveAnchor(storage, licenseHash, anchor) {
70
+ function saveAnchor(storage, zkAppAddress, licenseHash, anchor) {
81
71
  try {
82
- storage.setItem(ANCHOR_KEY_PREFIX + licenseHash, JSON.stringify(anchor));
72
+ storage.setItem(anchorKey(zkAppAddress, licenseHash), JSON.stringify(anchor));
83
73
  }
84
74
  catch {
85
75
  // Storage may be full or read-only; the offline path simply won't activate.
@@ -90,39 +80,6 @@ function computeRemainingDays(expirySlot, currentSlot) {
90
80
  return 0;
91
81
  return Math.floor((expirySlot - currentSlot) / SLOTS_PER_DAY);
92
82
  }
93
- // Renew the activation token when within this window of expiry. Picked so
94
- // that a daily verify call has many chances to refresh before the token dies.
95
- const TOKEN_RENEW_THRESHOLD_MS = 24 * 60 * 60 * 1000;
96
- async function tryActivate(storage, zkAppAddress, licenseHash, current, bootstrapSecretHash, prover, verifierUrl, fetcher, nowMs) {
97
- const secretHash = current?.secretHash ?? bootstrapSecretHash;
98
- if (!secretHash)
99
- return current;
100
- // Existing token still fresh — nothing to do.
101
- if (current?.token && current.expiresAt - nowMs > TOKEN_RENEW_THRESHOLD_MS)
102
- return current;
103
- try {
104
- const base = verifierUrl.replace(/\/+$/, '');
105
- const challengeResp = await fetcher(`${base}/challenge?licenseHash=${encodeURIComponent(licenseHash)}`);
106
- if (!challengeResp.ok)
107
- return current;
108
- const { nonce } = (await challengeResp.json());
109
- const proofJson = await prover({ licenseHash, nonce, secretHash });
110
- const respondResp = await fetcher(`${base}/respond`, {
111
- method: 'POST',
112
- headers: { 'Content-Type': 'application/json' },
113
- body: JSON.stringify({ zkAppAddress, proof: proofJson }),
114
- });
115
- if (!respondResp.ok)
116
- return current;
117
- const { token, expiresAt } = (await respondResp.json());
118
- const next = { token, expiresAt, secretHash };
119
- saveActivation(storage, zkAppAddress, licenseHash, next);
120
- return next;
121
- }
122
- catch {
123
- return current;
124
- }
125
- }
126
83
  function offlineCheck(anchor, nowMs) {
127
84
  const elapsedSlots = Math.max(0, Math.floor((nowMs - anchor.anchoredAtMs) / MS_PER_SLOT));
128
85
  const estCurrentSlot = anchor.anchoredSlot + elapsedSlots;
@@ -142,20 +99,18 @@ function offlineCheck(anchor, nowMs) {
142
99
  ? 'License is in grace period — renew to avoid interruption'
143
100
  : null,
144
101
  source: 'offline',
145
- ownership: anchor.ownership,
146
102
  };
147
103
  }
148
104
  export async function verifyLicense(proof, options = {}) {
149
105
  const storage = options.storage === null ? null : options.storage ?? defaultStorage();
150
106
  const fetcher = options.fetcher ?? (typeof fetch !== 'undefined' ? fetch : undefined);
151
107
  const nowFn = options.now ?? (() => Date.now());
152
- const verifierUrl = options.verifierUrl ?? proof.verifierUrl;
153
108
  if (storage) {
154
- const anchor = loadAnchor(storage, proof.licenseHash, proof.expirySlot);
109
+ const anchor = loadAnchor(storage, proof.zkAppAddress, proof.licenseHash, proof.expirySlot);
155
110
  if (anchor)
156
111
  return offlineCheck(anchor, nowFn());
157
112
  }
158
- if (!verifierUrl || !fetcher) {
113
+ if (!fetcher) {
159
114
  return {
160
115
  valid: false,
161
116
  expirySlot: proof.expirySlot,
@@ -163,24 +118,13 @@ export async function verifyLicense(proof, options = {}) {
163
118
  currentSlot: 0,
164
119
  inGracePeriod: false,
165
120
  remainingDays: 0,
166
- reason: 'No verifier URL or fetcher available; cannot verify',
121
+ reason: 'No fetch implementation available; cannot verify',
167
122
  source: 'chain',
168
- ownership: 'unverified',
169
123
  };
170
124
  }
171
- let activation = storage
172
- ? loadActivation(storage, proof.zkAppAddress, proof.licenseHash, nowFn())
173
- : null;
174
- if (storage && options.prover && (activation || options.secretHash)) {
175
- activation = await tryActivate(storage, proof.zkAppAddress, proof.licenseHash, activation, options.secretHash, options.prover, verifierUrl, fetcher, nowFn());
176
- }
177
- const tokenParam = activation?.token
178
- ? `&token=${encodeURIComponent(activation.token)}`
179
- : '';
180
- const url = `${verifierUrl.replace(/\/+$/, '')}` +
125
+ const url = `${DEFAULT_VERIFIER_URL}` +
181
126
  `?licenseHash=${encodeURIComponent(proof.licenseHash)}` +
182
- `&zkAppAddress=${encodeURIComponent(proof.zkAppAddress)}` +
183
- tokenParam;
127
+ `&zkAppAddress=${encodeURIComponent(proof.zkAppAddress)}`;
184
128
  let data;
185
129
  try {
186
130
  const resp = await fetcher(url);
@@ -194,7 +138,6 @@ export async function verifyLicense(proof, options = {}) {
194
138
  remainingDays: 0,
195
139
  reason: `Verification request failed (HTTP ${resp.status})`,
196
140
  source: 'chain',
197
- ownership: 'unverified',
198
141
  };
199
142
  }
200
143
  data = await resp.json();
@@ -209,13 +152,12 @@ export async function verifyLicense(proof, options = {}) {
209
152
  remainingDays: 0,
210
153
  reason: err?.message ?? 'Verification request errored',
211
154
  source: 'chain',
212
- ownership: 'unverified',
213
155
  };
214
156
  }
215
- // Keeper's authoritative expiry, or null if the response omits it. Only
216
- // the null-safe form may be written into the anchor; the chain-result
217
- // display value below falls back to the local proof file for user-facing
218
- // fields, which is safe (display only).
157
+ // Service's authoritative expiry, or null if the response omits it.
158
+ // Only the null-safe form may be written into the anchor; the
159
+ // chain-result display value below falls back to the local proof file
160
+ // for user-facing fields, which is safe (display only).
219
161
  const respExpirySlotStrict = typeof data.expirySlot === 'number' ? data.expirySlot : null;
220
162
  const respExpirySlot = respExpirySlotStrict ?? proof.expirySlot;
221
163
  const respCurrentSlot = typeof data.currentSlot === 'number' ? data.currentSlot : 0;
@@ -230,24 +172,23 @@ export async function verifyLicense(proof, options = {}) {
230
172
  : computeRemainingDays(respExpirySlot, respCurrentSlot),
231
173
  reason: data.reason ?? null,
232
174
  source: 'chain',
233
- ownership: data.ownership === 'verified' ? 'verified' : 'unverified',
234
175
  };
235
- // Anchor gate uses the keeper's own refund-window decision. The keeper
236
- // knows the true purchaseSlot (stored in its LicenseRecord) and the true
237
- // currentSlot; the SDK never trusts a purchaseSlot or expirySlot supplied
238
- // by the local proof file, since that file is client-writable and could
239
- // be forged. If the keeper omits `expirySlot`, we fail-closed on the
240
- // offline path — no anchor written, subsequent calls stay online.
176
+ // Anchor gate uses the service's own refund-window decision. The
177
+ // service knows the true purchaseSlot (stored in its LicenseRecord)
178
+ // and the true currentSlot; the SDK never trusts a purchaseSlot or
179
+ // expirySlot supplied by the local proof file, since that file is
180
+ // client-writable and could be forged. If the service omits
181
+ // `expirySlot`, we fail-closed on the offline path — no anchor
182
+ // written, subsequent calls stay online.
241
183
  if (storage &&
242
184
  result.valid &&
243
185
  data.refundWindowClosed === true &&
244
186
  typeof data.currentSlot === 'number' &&
245
187
  respExpirySlotStrict !== null) {
246
- saveAnchor(storage, proof.licenseHash, {
188
+ saveAnchor(storage, proof.zkAppAddress, proof.licenseHash, {
247
189
  anchoredSlot: data.currentSlot,
248
190
  anchoredAtMs: nowFn(),
249
191
  expirySlot: respExpirySlotStrict,
250
- ownership: result.ownership,
251
192
  });
252
193
  }
253
194
  return result;
@@ -255,4 +196,32 @@ export async function verifyLicense(proof, options = {}) {
255
196
  // Re-exported so tests and downstream tooling can reference the same values
256
197
  // the SDK uses internally without re-deriving them.
257
198
  export { REFUND_WINDOW_N, GRACE_PERIOD_N, MS_PER_SLOT };
199
+ // Non-custodial device binding surface. Re-exported at the
200
+ // barrel alongside buy / renew / refund so vendor SDK integrators can
201
+ // pull the whole family from a single import path. Each also has its
202
+ // own package subpath (`zklicensing/bind`, `zklicensing/unbind`,
203
+ // `zklicensing/device-identity`) to match the buy / renew / refund
204
+ // convention.
205
+ export { proveBind, confirmBind } from './bindClient.js';
206
+ export { proveUnbind, confirmUnbind } from './unbindClient.js';
207
+ export { getOrCreateDeviceIdentity, computeDeviceCommitment, } from './deviceIdentity.js';
208
+ // Offline device-binding verifier. Vendors reconstruct the
209
+ // LicenseLeaf from a synced snapshot, verify the leaf hash against the
210
+ // last-known licensesRoot, and confirm the caller's device commitment
211
+ // appears in `occupied[]`. Also available at the `zklicensing/verify-offline`
212
+ // subpath for callers that want to minimize their imported surface.
213
+ export { verifyOffline, deviceCommitmentFromFields } from './verifyOffline.js';
214
+ // Non-custodial trust anchor for `verifyOffline`. Reads the live
215
+ // `licensesRoot` (zkapp.appState[0]) directly from Mina — pair it with
216
+ // `verifyOffline` when you do not want to trust the licensing service's
217
+ // reported root. Also available at the `zklicensing/on-chain-root`
218
+ // subpath. o1js is imported lazily so callers that skip this helper
219
+ // don't pay the o1js dep cost at load time.
220
+ export { fetchOnChainLicensesRoot, DEFAULT_MINA_GRAPHQL_URLS } from './onChainRoot.js';
221
+ // Human-readable seat names for the device-binding UI ("Silent Otter",
222
+ // "Curious Panda"). Deterministic in `licenseHash + slotIndex + boundAtSlot`,
223
+ // so every viewer of the same license computes the same name per slot
224
+ // without a naming sidecar. Also available at the `zklicensing/seat-name`
225
+ // subpath.
226
+ export { seatNameFor, SEAT_NAME_COMBINATIONS } from './seatName.js';
258
227
  //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,oCAAoC;AAEpC;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EACL,eAAe,EACf,cAAc,EACd,aAAa,EACb,eAAe,GAChB,MAAM,wBAAwB,CAAC;AAEhC,MAAM,WAAW,GAAG,aAAa,CAAC;AAClC,MAAM,aAAa,GAAG,eAAe,CAAC;AAEtC,qEAAqE;AACrE,mEAAmE;AACnE,yEAAyE;AACzE,4EAA4E;AAC5E,MAAM,CAAC,MAAM,oBAAoB,GAAG,oCAAoC,CAAC;AAEzE,wEAAwE;AACxE,wEAAwE;AACxE,qEAAqE;AACrE,iEAAiE;AACjE,sEAAsE;AACtE,6DAA6D;AAC7D,+BAA+B;AAC/B,MAAM,iBAAiB,GAAG,eAAe,CAAC;AAE1C,SAAS,SAAS,CAAC,YAAoB,EAAE,WAAmB;IAC1D,OAAO,GAAG,iBAAiB,GAAG,YAAY,IAAI,WAAW,EAAE,CAAC;AAC9D,CAAC;AA6CD,SAAS,cAAc;IACrB,IAAI,OAAO,UAAU,KAAK,WAAW,IAAK,UAAkB,CAAC,YAAY,EAAE,CAAC;QAC1E,OAAQ,UAAkB,CAAC,YAAuB,CAAC;IACrD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,UAAU,CACjB,OAAgB,EAChB,YAAoB,EACpB,WAAmB,EACnB,UAAkB;IAElB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,YAAY,EAAE,WAAW,CAAC,CAAC,CAAC;QAClE,IAAI,CAAC,GAAG;YAAE,OAAO,IAAI,CAAC;QACtB,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAW,CAAC;QACpC,iEAAiE;QACjE,mEAAmE;QACnE,oEAAoE;QACpE,0CAA0C;QAC1C,2EAA2E;QAC3E,mEAAmE;QACnE,iEAAiE;QACjE,6BAA6B;QAC7B,IAAI,CAAC,CAAC,UAAU,KAAK,UAAU;YAAE,OAAO,IAAI,CAAC;QAC7C,OAAO,CAAC,CAAC;IACX,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,SAAS,UAAU,CACjB,OAAgB,EAChB,YAAoB,EACpB,WAAmB,EACnB,MAAc;IAEd,IAAI,CAAC;QACH,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,YAAY,EAAE,WAAW,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;IAChF,CAAC;IAAC,MAAM,CAAC;QACP,4EAA4E;IAC9E,CAAC;AACH,CAAC;AAED,SAAS,oBAAoB,CAAC,UAAkB,EAAE,WAAmB;IACnE,IAAI,UAAU,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IAC/B,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,UAAU,GAAG,WAAW,CAAC,GAAG,aAAa,CAAC,CAAC;AAChE,CAAC;AAED,SAAS,YAAY,CAAC,MAAc,EAAE,KAAa;IACjD,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG,MAAM,CAAC,YAAY,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC;IAC1F,MAAM,cAAc,GAAG,MAAM,CAAC,YAAY,GAAG,YAAY,CAAC;IAC1D,MAAM,OAAO,GAAG,cAAc,GAAG,MAAM,CAAC,UAAU,GAAG,cAAc,CAAC;IACpE,MAAM,aAAa,GAAG,CAAC,OAAO,IAAI,cAAc,GAAG,MAAM,CAAC,UAAU,CAAC;IACrE,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,KAAK,GAAG,CAAC,MAAM,CAAC,UAAU,GAAG,cAAc,CAAC,GAAG,WAAW,CAAC,CAAC,WAAW,EAAE,CAAC;IACrG,OAAO;QACL,KAAK,EAAE,CAAC,OAAO;QACf,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,SAAS;QACT,WAAW,EAAE,cAAc;QAC3B,aAAa;QACb,aAAa,EAAE,oBAAoB,CAAC,MAAM,CAAC,UAAU,EAAE,cAAc,CAAC;QACtE,MAAM,EAAE,OAAO;YACb,CAAC,CAAC,qBAAqB;YACvB,CAAC,CAAC,aAAa;gBACf,CAAC,CAAC,0DAA0D;gBAC5D,CAAC,CAAC,IAAI;QACR,MAAM,EAAE,SAAS;KAClB,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,KAAiB,EACjB,UAAyB,EAAE;IAE3B,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,IAAI,cAAc,EAAE,CAAC;IACtF,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,CAAC,OAAO,KAAK,KAAK,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IACtF,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IAEhD,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,MAAM,GAAG,UAAU,CAAC,OAAO,EAAE,KAAK,CAAC,YAAY,EAAE,KAAK,CAAC,WAAW,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;QAC5F,IAAI,MAAM;YAAE,OAAO,YAAY,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;IACnD,CAAC;IAED,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO;YACL,KAAK,EAAE,KAAK;YACZ,UAAU,EAAE,KAAK,CAAC,UAAU;YAC5B,SAAS,EAAE,IAAI;YACf,WAAW,EAAE,CAAC;YACd,aAAa,EAAE,KAAK;YACpB,aAAa,EAAE,CAAC;YAChB,MAAM,EAAE,kDAAkD;YAC1D,MAAM,EAAE,OAAO;SAChB,CAAC;IACJ,CAAC;IAED,MAAM,GAAG,GACP,GAAG,oBAAoB,EAAE;QACzB,gBAAgB,kBAAkB,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE;QACvD,iBAAiB,kBAAkB,CAAC,KAAK,CAAC,YAAY,CAAC,EAAE,CAAC;IAE5D,IAAI,IAAS,CAAC;IACd,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC;QAChC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;YACb,OAAO;gBACL,KAAK,EAAE,KAAK;gBACZ,UAAU,EAAE,KAAK,CAAC,UAAU;gBAC5B,SAAS,EAAE,IAAI;gBACf,WAAW,EAAE,CAAC;gBACd,aAAa,EAAE,KAAK;gBACpB,aAAa,EAAE,CAAC;gBAChB,MAAM,EAAE,qCAAqC,IAAI,CAAC,MAAM,GAAG;gBAC3D,MAAM,EAAE,OAAO;aAChB,CAAC;QACJ,CAAC;QACD,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;IAC3B,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAClB,OAAO;YACL,KAAK,EAAE,KAAK;YACZ,UAAU,EAAE,KAAK,CAAC,UAAU;YAC5B,SAAS,EAAE,IAAI;YACf,WAAW,EAAE,CAAC;YACd,aAAa,EAAE,KAAK;YACpB,aAAa,EAAE,CAAC;YAChB,MAAM,EAAE,GAAG,EAAE,OAAO,IAAI,8BAA8B;YACtD,MAAM,EAAE,OAAO;SAChB,CAAC;IACJ,CAAC;IAED,oEAAoE;IACpE,8DAA8D;IAC9D,sEAAsE;IACtE,wDAAwD;IACxD,MAAM,oBAAoB,GACxB,OAAO,IAAI,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC;IAC/D,MAAM,cAAc,GAAW,oBAAoB,IAAI,KAAK,CAAC,UAAU,CAAC;IACxE,MAAM,eAAe,GAAW,OAAO,IAAI,CAAC,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5F,MAAM,MAAM,GAAiB;QAC3B,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK;QACnB,UAAU,EAAE,cAAc;QAC1B,SAAS,EAAE,OAAO,IAAI,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI;QACrE,WAAW,EAAE,eAAe;QAC5B,aAAa,EAAE,CAAC,CAAC,IAAI,CAAC,aAAa;QACnC,aAAa,EAAE,OAAO,IAAI,CAAC,aAAa,KAAK,QAAQ;YACnD,CAAC,CAAC,IAAI,CAAC,aAAa;YACpB,CAAC,CAAC,oBAAoB,CAAC,cAAc,EAAE,eAAe,CAAC;QACzD,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,IAAI;QAC3B,MAAM,EAAE,OAAO;KAChB,CAAC;IAEF,iEAAiE;IACjE,oEAAoE;IACpE,mEAAmE;IACnE,kEAAkE;IAClE,4DAA4D;IAC5D,+DAA+D;IAC/D,yCAAyC;IACzC,IACE,OAAO;QACP,MAAM,CAAC,KAAK;QACZ,IAAI,CAAC,kBAAkB,KAAK,IAAI;QAChC,OAAO,IAAI,CAAC,WAAW,KAAK,QAAQ;QACpC,oBAAoB,KAAK,IAAI,EAC7B,CAAC;QACD,UAAU,CAAC,OAAO,EAAE,KAAK,CAAC,YAAY,EAAE,KAAK,CAAC,WAAW,EAAE;YACzD,YAAY,EAAE,IAAI,CAAC,WAAW;YAC9B,YAAY,EAAE,KAAK,EAAE;YACrB,UAAU,EAAE,oBAAoB;SACjC,CAAC,CAAC;IACL,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,4EAA4E;AAC5E,oDAAoD;AACpD,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,WAAW,EAAE,CAAC;AAExD,2DAA2D;AAC3D,sEAAsE;AACtE,qEAAqE;AACrE,iEAAiE;AACjE,mEAAmE;AACnE,cAAc;AACd,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAMzD,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAM/D,OAAO,EACL,yBAAyB,EACzB,uBAAuB,GACxB,MAAM,qBAAqB,CAAC;AAG7B,2DAA2D;AAC3D,uEAAuE;AACvE,sEAAsE;AACtE,8EAA8E;AAC9E,oEAAoE;AACpE,OAAO,EAAE,aAAa,EAAE,0BAA0B,EAAE,MAAM,oBAAoB,CAAC;AAO/E,iEAAiE;AACjE,uEAAuE;AACvE,wEAAwE;AACxE,mEAAmE;AACnE,oEAAoE;AACpE,4CAA4C;AAC5C,OAAO,EAAE,wBAAwB,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAC;AAGvF,uEAAuE;AACvE,8EAA8E;AAC9E,sEAAsE;AACtE,0EAA0E;AAC1E,WAAW;AACX,OAAO,EAAE,WAAW,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * licenseLeaf.ts
3
+ *
4
+ * On-chain leaf value stored at every occupied `licensesRoot` slot. Bundles
5
+ * the license expiry with the device-binding state (cap, slot commitments,
6
+ * fill counter) so a single tree lookup returns everything a verifier or a
7
+ * `/prove/bind` call needs.
8
+ *
9
+ * Wire shape (in order of hash preimage):
10
+ * licenseHash Field the tree key, also carried in the leaf
11
+ * so a stolen witness can't be re-pointed
12
+ * at another entry. Doubles as the owner-
13
+ * pubkey commitment — buyLicense asserts
14
+ * licenseHash == Poseidon(pk.x, pk.isOdd)
15
+ * so bind/unbind's ownership check reads
16
+ * the same field as the tree key. Storing
17
+ * them separately would be pure redundancy.
18
+ * expirySlot UInt32→Field UInt32 packed into a Field
19
+ * cap UInt32→Field 0 disables /prove/bind; ≤DEVICE_SLOT_CAP
20
+ * (kept as UInt32 in-circuit because o1js's
21
+ * provable types don't include UInt8)
22
+ * deviceSlotsRoot Field Poseidon fold over the 10 slot fields
23
+ * below (2 fields × DEVICE_SLOT_CAP)
24
+ * deviceSlotsFilled UInt32→Field how many of the DEVICE_SLOT_CAP slots
25
+ * are currently non-empty — carried
26
+ * alongside the fold so `bind()` can do
27
+ * a cheap "cap - filled > 0" check
28
+ * without re-summing the array
29
+ *
30
+ * DEVICE_SLOT_CAP is fixed at 5 so the slot arrays have a size known to the
31
+ * circuit at compile time. `cap` in the leaf may be smaller — it's a
32
+ * per-license limit; slots [cap..DEVICE_SLOT_CAP) must always stay empty
33
+ * (Field(0)) and `bind()` rejects any write to them. `cap = 0` disables
34
+ * binding entirely; `bind()` rejects immediately without ever touching a
35
+ * slot.
36
+ *
37
+ * Slot encoding (fields carried in the fold, per slot i):
38
+ * occupied[i] Field 0 (empty) or Poseidon(devicePubKeyHash)
39
+ * freedAt[i] Field packed (boundAtSlot << 32) | freedAtSlot; both
40
+ * are UInt32-range Mina slot numbers. On a fresh
41
+ * empty slot both halves are 0. On bind, boundAt
42
+ * gets the current slot and freedAt stays 0. On
43
+ * unbind, freedAt gets the current slot (boundAt
44
+ * is preserved for audit / cooldown math).
45
+ *
46
+ * The struct + helpers live in the SDK (not the circuit package) so both
47
+ * the on-chain `@method` bodies AND the off-chain keeper / verifier /
48
+ * dashboards can share a single source of truth for the leaf hash. The
49
+ * circuit re-exports them from `LicensingApp.ts` so existing consumers
50
+ * that import from there keep working.
51
+ */
52
+ import { Field, PublicKey, UInt32 } from 'o1js';
53
+ import type { LicenseRecord } from './licenseRecord.js';
54
+ export declare const DEVICE_SLOT_CAP_N = 5;
55
+ export declare const DEVICE_SLOT_CAP: UInt32;
56
+ export declare const EMPTY_DEVICE_SLOTS_ROOT: Field;
57
+ export declare function ownerPubKeyHash(pub: PublicKey): Field;
58
+ export declare function packSlotTimes(boundAtSlot: UInt32, freedAtSlot: UInt32): Field;
59
+ export declare function unpackSlotTimes(packed: Field): {
60
+ boundAtSlot: UInt32;
61
+ freedAtSlot: UInt32;
62
+ };
63
+ export declare function computeDeviceSlotsRoot(occupied: Field[], freedAt: Field[]): Field;
64
+ export declare const DeviceSlot: any;
65
+ export type DeviceSlot = any;
66
+ export declare function emptyDeviceSlot(): DeviceSlot;
67
+ export declare const DeviceSlots: any;
68
+ export type DeviceSlots = any;
69
+ export declare function emptyDeviceSlots(): DeviceSlots;
70
+ export declare function deviceSlotsHash(ds: DeviceSlots): Field;
71
+ export declare const LicenseLeaf: any;
72
+ export type LicenseLeaf = any;
73
+ export declare function leafHash(leaf: LicenseLeaf): Field;
74
+ export declare function freshLeaf(licenseHash: Field, expirySlot: UInt32, cap: UInt32): LicenseLeaf;
75
+ export declare function leafFromRecord(record: LicenseRecord): LicenseLeaf;
76
+ //# sourceMappingURL=licenseLeaf.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"licenseLeaf.d.ts","sourceRoot":"","sources":["../src/licenseLeaf.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AAEH,OAAO,EAAE,KAAK,EAAsB,SAAS,EAAU,MAAM,EAAE,MAAM,MAAM,CAAC;AAC5E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAMxD,eAAO,MAAM,iBAAiB,IAAI,CAAC;AACnC,eAAO,MAAM,eAAe,QAAiC,CAAC;AAK9D,eAAO,MAAM,uBAAuB,EAAE,KAErC,CAAC;AAOF,wBAAgB,eAAe,CAAC,GAAG,EAAE,SAAS,GAAG,KAAK,CAErD;AASD,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,KAAK,CAE7E;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,KAAK,GAAG;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAQ3F;AAMD,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,KAAK,EAAE,EACjB,OAAO,EAAG,KAAK,EAAE,GAChB,KAAK,CAYP;AAYD,eAAO,MAAM,UAAU,EAAE,GAGvB,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,GAAG,CAAC;AAE7B,wBAAgB,eAAe,IAAI,UAAU,CAE5C;AAID,eAAO,MAAM,WAAW,EAAE,GAExB,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,GAAG,CAAC;AAE9B,wBAAgB,gBAAgB,IAAI,WAAW,CAI9C;AAKD,wBAAgB,eAAe,CAAC,EAAE,EAAE,WAAW,GAAG,KAAK,CAMtD;AAMD,eAAO,MAAM,WAAW,EAAE,GAMxB,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,GAAG,CAAC;AAE9B,wBAAgB,QAAQ,CAAC,IAAI,EAAE,WAAW,GAAG,KAAK,CAQjD;AAID,wBAAgB,SAAS,CACvB,WAAW,EAAE,KAAK,EAClB,UAAU,EAAG,MAAM,EACnB,GAAG,EAAU,MAAM,GAClB,WAAW,CAQb;AAMD,wBAAgB,cAAc,CAAC,MAAM,EAAE,aAAa,GAAG,WAAW,CAUjE"}
@@ -0,0 +1,189 @@
1
+ // Copyright (c) 2025-2026 zkLicensing project developers
2
+ // SPDX-License-Identifier: BUSL-1.1
3
+ /**
4
+ * licenseLeaf.ts
5
+ *
6
+ * On-chain leaf value stored at every occupied `licensesRoot` slot. Bundles
7
+ * the license expiry with the device-binding state (cap, slot commitments,
8
+ * fill counter) so a single tree lookup returns everything a verifier or a
9
+ * `/prove/bind` call needs.
10
+ *
11
+ * Wire shape (in order of hash preimage):
12
+ * licenseHash Field the tree key, also carried in the leaf
13
+ * so a stolen witness can't be re-pointed
14
+ * at another entry. Doubles as the owner-
15
+ * pubkey commitment — buyLicense asserts
16
+ * licenseHash == Poseidon(pk.x, pk.isOdd)
17
+ * so bind/unbind's ownership check reads
18
+ * the same field as the tree key. Storing
19
+ * them separately would be pure redundancy.
20
+ * expirySlot UInt32→Field UInt32 packed into a Field
21
+ * cap UInt32→Field 0 disables /prove/bind; ≤DEVICE_SLOT_CAP
22
+ * (kept as UInt32 in-circuit because o1js's
23
+ * provable types don't include UInt8)
24
+ * deviceSlotsRoot Field Poseidon fold over the 10 slot fields
25
+ * below (2 fields × DEVICE_SLOT_CAP)
26
+ * deviceSlotsFilled UInt32→Field how many of the DEVICE_SLOT_CAP slots
27
+ * are currently non-empty — carried
28
+ * alongside the fold so `bind()` can do
29
+ * a cheap "cap - filled > 0" check
30
+ * without re-summing the array
31
+ *
32
+ * DEVICE_SLOT_CAP is fixed at 5 so the slot arrays have a size known to the
33
+ * circuit at compile time. `cap` in the leaf may be smaller — it's a
34
+ * per-license limit; slots [cap..DEVICE_SLOT_CAP) must always stay empty
35
+ * (Field(0)) and `bind()` rejects any write to them. `cap = 0` disables
36
+ * binding entirely; `bind()` rejects immediately without ever touching a
37
+ * slot.
38
+ *
39
+ * Slot encoding (fields carried in the fold, per slot i):
40
+ * occupied[i] Field 0 (empty) or Poseidon(devicePubKeyHash)
41
+ * freedAt[i] Field packed (boundAtSlot << 32) | freedAtSlot; both
42
+ * are UInt32-range Mina slot numbers. On a fresh
43
+ * empty slot both halves are 0. On bind, boundAt
44
+ * gets the current slot and freedAt stays 0. On
45
+ * unbind, freedAt gets the current slot (boundAt
46
+ * is preserved for audit / cooldown math).
47
+ *
48
+ * The struct + helpers live in the SDK (not the circuit package) so both
49
+ * the on-chain `@method` bodies AND the off-chain keeper / verifier /
50
+ * dashboards can share a single source of truth for the leaf hash. The
51
+ * circuit re-exports them from `LicensingApp.ts` so existing consumers
52
+ * that import from there keep working.
53
+ */
54
+ import { Field, Poseidon, Provable, Struct, UInt32 } from 'o1js';
55
+ // Fixed slot-array width. Per the device-binding-noncustodial plan: 5 is
56
+ // the sweet spot between "enough seats for a normal user" and "witnesses
57
+ // stay small." Changing this is a hardfork migration (would flip every
58
+ // leaf hash) — not a runtime tunable.
59
+ export const DEVICE_SLOT_CAP_N = 5;
60
+ export const DEVICE_SLOT_CAP = UInt32.from(DEVICE_SLOT_CAP_N);
61
+ // Poseidon-fold of DEVICE_SLOT_CAP × 2 = 10 zero Fields, used as the
62
+ // initial deviceSlotsRoot on every freshly-minted leaf. Precomputed once
63
+ // at module load so `freshLeaf()` doesn't reconstruct it per-call.
64
+ export const EMPTY_DEVICE_SLOTS_ROOT = Poseidon.hash(Array.from({ length: DEVICE_SLOT_CAP_N * 2 }, () => Field(0)));
65
+ // Poseidon-bound identity of the passphrase-derived ownership pubkey.
66
+ // The full PublicKey is (x: Field, isOdd: Bool); Poseidon(x, isOdd.toField())
67
+ // gives a single Field commitment. The buy circuit asserts this equals
68
+ // `licenseHash`, so the leaf carries no separate owner-pubkey field —
69
+ // `licenseHash` IS the on-chain owner-pubkey commitment.
70
+ export function ownerPubKeyHash(pub) {
71
+ return Poseidon.hash([pub.x, pub.isOdd.toField()]);
72
+ }
73
+ // Pack (boundAtSlot, freedAtSlot) into a single Field. Both are Mina
74
+ // slot numbers (UInt32-range, ≤ 2^32-1), so the concatenation fits in 64
75
+ // bits. Layout mirrors packPrices in LicensingApp.ts — low bits first so
76
+ // the more frequently-read half (freedAt, checked by every bind cooldown)
77
+ // unpacks with a single mask.
78
+ const SHIFT_32 = Field(1n << 32n);
79
+ export function packSlotTimes(boundAtSlot, freedAtSlot) {
80
+ return freedAtSlot.value.add(boundAtSlot.value.mul(SHIFT_32));
81
+ }
82
+ export function unpackSlotTimes(packed) {
83
+ // toBits(64) enforces the high 190 bits are zero — belt-and-suspenders
84
+ // since we only ever write via packSlotTimes, but keeps the circuit
85
+ // sound against an adversarially-crafted slot in a future upgrade.
86
+ const bits = packed.toBits(64);
87
+ const freedAtSlot = UInt32.Unsafe.fromField(Field.fromBits(bits.slice(0, 32)));
88
+ const boundAtSlot = UInt32.Unsafe.fromField(Field.fromBits(bits.slice(32, 64)));
89
+ return { boundAtSlot, freedAtSlot };
90
+ }
91
+ // Fold the parallel slot arrays into `deviceSlotsRoot`. Same order every
92
+ // time — occupied[0], freedAt[0], occupied[1], freedAt[1], ... — so a
93
+ // buyer that reconstructs the fold off-chain (verifier, dashboard) gets
94
+ // the same commitment the circuit computed.
95
+ export function computeDeviceSlotsRoot(occupied, freedAt) {
96
+ if (occupied.length !== DEVICE_SLOT_CAP_N || freedAt.length !== DEVICE_SLOT_CAP_N) {
97
+ throw new Error(`computeDeviceSlotsRoot expects exactly ${DEVICE_SLOT_CAP_N} entries in each array ` +
98
+ `(got occupied=${occupied.length}, freedAt=${freedAt.length})`);
99
+ }
100
+ const fields = [];
101
+ for (let i = 0; i < DEVICE_SLOT_CAP_N; i++) {
102
+ fields.push(occupied[i], freedAt[i]);
103
+ }
104
+ return Poseidon.hash(fields);
105
+ }
106
+ // Per-slot payload. The Struct wrapper is what lets o1js serialize this
107
+ // inside larger provable structures — it also gives us a fixed
108
+ // two-Field width so DeviceSlots.slots can be a `Provable.Array` of a
109
+ // well-known type instead of a bag of loose Fields.
110
+ //
111
+ // Same `const X: any = Struct(...)` pattern as contractInterface.ts —
112
+ // annotating as `any` sidesteps TS2742 (o1js sits outside the package
113
+ // rootDir under npm hoisting, so tsc can't emit portable declarations
114
+ // for the inferred Struct class). Instance methods and `static empty()`
115
+ // therefore live as standalone helpers below rather than on the class.
116
+ export const DeviceSlot = Struct({
117
+ occupied: Field, // 0 or Poseidon(devicePubKeyHash)
118
+ freedAt: Field, // packSlotTimes(boundAtSlot, freedAtSlot)
119
+ });
120
+ export function emptyDeviceSlot() {
121
+ return new DeviceSlot({ occupied: Field(0), freedAt: Field(0) });
122
+ }
123
+ // Fixed-width slot array. `Provable.Array(DeviceSlot, N)` promises the
124
+ // circuit an exact length so witnesses can address slots by static index.
125
+ export const DeviceSlots = Struct({
126
+ slots: Provable.Array(DeviceSlot, DEVICE_SLOT_CAP_N),
127
+ });
128
+ export function emptyDeviceSlots() {
129
+ return new DeviceSlots({
130
+ slots: Array.from({ length: DEVICE_SLOT_CAP_N }, () => emptyDeviceSlot()),
131
+ });
132
+ }
133
+ // In-circuit fold — takes a DeviceSlots value so it composes cleanly
134
+ // inside `@method` bodies. The array length is fixed by the Struct's
135
+ // Provable.Array declaration, so no runtime length guard is needed.
136
+ export function deviceSlotsHash(ds) {
137
+ const fields = [];
138
+ for (const s of ds.slots) {
139
+ fields.push(s.occupied, s.freedAt);
140
+ }
141
+ return Poseidon.hash(fields);
142
+ }
143
+ // The leaf value itself. `LicensingApp.buyLicense` builds one of these
144
+ // with EMPTY_DEVICE_SLOTS_ROOT and writes leafHash(leaf) into licensesRoot;
145
+ // subsequent mutations (renew, refund, migrate, bind, unbind) each build
146
+ // a new leaf with the appropriate field(s) updated and write its hash.
147
+ export const LicenseLeaf = Struct({
148
+ licenseHash: Field,
149
+ expirySlot: UInt32,
150
+ cap: UInt32,
151
+ deviceSlotsRoot: Field,
152
+ deviceSlotsFilled: UInt32,
153
+ });
154
+ export function leafHash(leaf) {
155
+ return Poseidon.hash([
156
+ leaf.licenseHash,
157
+ leaf.expirySlot.value,
158
+ leaf.cap.value,
159
+ leaf.deviceSlotsRoot,
160
+ leaf.deviceSlotsFilled.value,
161
+ ]);
162
+ }
163
+ // Construct the leaf a fresh buy writes into the tree — no devices bound
164
+ // yet, filled counter at zero, slot-fold at the precomputed empty root.
165
+ export function freshLeaf(licenseHash, expirySlot, cap) {
166
+ return new LicenseLeaf({
167
+ licenseHash,
168
+ expirySlot,
169
+ cap,
170
+ deviceSlotsRoot: EMPTY_DEVICE_SLOTS_ROOT,
171
+ deviceSlotsFilled: UInt32.zero,
172
+ });
173
+ }
174
+ // Reconstruct the leaf that lives at `record.licenseHash` in the licensesRoot
175
+ // MerkleMap. `LicenseRecord` optionally persists the device-binding fields;
176
+ // anything missing falls back to the fresh-leaf defaults, matching the state
177
+ // a just-issued license was minted with.
178
+ export function leafFromRecord(record) {
179
+ return new LicenseLeaf({
180
+ licenseHash: Field(record.licenseHash),
181
+ expirySlot: UInt32.from(record.expirySlot),
182
+ cap: UInt32.from(record.deviceCap ?? 0),
183
+ deviceSlotsRoot: record.deviceSlotsRoot !== undefined
184
+ ? Field(record.deviceSlotsRoot)
185
+ : EMPTY_DEVICE_SLOTS_ROOT,
186
+ deviceSlotsFilled: UInt32.from(record.deviceSlotsFilled ?? 0),
187
+ });
188
+ }
189
+ //# sourceMappingURL=licenseLeaf.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"licenseLeaf.js","sourceRoot":"","sources":["../src/licenseLeaf.ts"],"names":[],"mappings":"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"}
@@ -7,6 +7,12 @@
7
7
  * the same type without dragging in filesystem code. licenseStore.ts
8
8
  * re-exports these to keep existing Node imports working.
9
9
  */
10
+ export type FiatRateSnapshot = {
11
+ minaEurRate: number;
12
+ minaUsdRate: number;
13
+ minaGbpRate: number;
14
+ recordedAt: string;
15
+ };
10
16
  export type LicenseRecord = {
11
17
  zkAppAddress: string;
12
18
  licenseHash: string;
@@ -15,22 +21,35 @@ export type LicenseRecord = {
15
21
  purchaseAmount: string;
16
22
  txHash: string;
17
23
  createdAt: string;
24
+ deviceCap?: number;
25
+ deviceSlotsRoot?: string;
26
+ deviceSlotsFilled?: number;
27
+ devices?: Array<{
28
+ slotIndex: number;
29
+ deviceCommitment: string;
30
+ boundAtSlot: number;
31
+ freedAtSlot: number;
32
+ }>;
18
33
  buyerAddress?: string;
19
34
  buyerCountry?: string;
35
+ buyerZip?: string;
20
36
  pending?: boolean;
21
37
  provedAtSlot?: number;
22
38
  initialExpirySlot?: number;
23
- tier?: 'monthly' | 'yearly' | 'fiveYear';
39
+ tier?: 'monthly' | 'yearly' | 'fiveYear' | 'perpetual';
24
40
  renewedAtLeastOnce?: boolean;
25
41
  refunded?: boolean;
26
42
  released?: boolean;
27
43
  releaseSlot?: number;
44
+ releaseTxHash?: string;
28
45
  renewals?: Array<{
29
46
  slot: number;
30
47
  amount: string;
31
48
  newExpiry: number;
32
49
  txHash?: string;
50
+ fiatSnapshot?: FiatRateSnapshot;
33
51
  }>;
52
+ fiatSnapshot?: FiatRateSnapshot;
34
53
  refundWaiverAcceptedAt?: string;
35
54
  escrowless?: boolean;
36
55
  };