zklicensing 0.1.0-beta.2 → 0.1.0-beta.21

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 (317) hide show
  1. package/README.md +417 -201
  2. package/build/agent.d.ts +371 -0
  3. package/build/agent.d.ts.map +1 -0
  4. package/build/agent.js +553 -0
  5. package/build/agent.js.map +1 -0
  6. package/build/agentActions.d.ts +841 -0
  7. package/build/agentActions.d.ts.map +1 -0
  8. package/build/agentActions.js +581 -0
  9. package/build/agentActions.js.map +1 -0
  10. package/build/agentRecord.d.ts +83 -0
  11. package/build/agentRecord.d.ts.map +1 -0
  12. package/build/agentRecord.js +69 -0
  13. package/build/agentRecord.js.map +1 -0
  14. package/build/agentVendor.d.ts +220 -0
  15. package/build/agentVendor.d.ts.map +1 -0
  16. package/build/agentVendor.js +306 -0
  17. package/build/agentVendor.js.map +1 -0
  18. package/build/appsLookup.d.ts +14 -0
  19. package/build/appsLookup.d.ts.map +1 -0
  20. package/build/appsLookup.js +12 -0
  21. package/build/appsLookup.js.map +1 -0
  22. package/build/archiveBootstrap.d.ts +31 -5
  23. package/build/archiveBootstrap.d.ts.map +1 -0
  24. package/build/archiveBootstrap.js +140 -47
  25. package/build/archiveBootstrap.js.map +1 -0
  26. package/build/atomicJson.d.ts +5 -0
  27. package/build/atomicJson.d.ts.map +1 -0
  28. package/build/atomicJson.js +55 -0
  29. package/build/atomicJson.js.map +1 -0
  30. package/build/bindClient.d.ts +85 -0
  31. package/build/bindClient.d.ts.map +1 -0
  32. package/build/bindClient.js +187 -0
  33. package/build/bindClient.js.map +1 -0
  34. package/build/buyClient.d.ts +27 -4
  35. package/build/buyClient.d.ts.map +1 -0
  36. package/build/buyClient.js +72 -26
  37. package/build/buyClient.js.map +1 -0
  38. package/build/chainHead.d.ts +61 -0
  39. package/build/chainHead.d.ts.map +1 -0
  40. package/build/chainHead.js +88 -0
  41. package/build/chainHead.js.map +1 -0
  42. package/build/compactMerkleMap.d.ts +72 -0
  43. package/build/compactMerkleMap.d.ts.map +1 -0
  44. package/build/compactMerkleMap.js +224 -0
  45. package/build/compactMerkleMap.js.map +1 -0
  46. package/build/contractInterface.d.ts +191 -51
  47. package/build/contractInterface.d.ts.map +1 -0
  48. package/build/contractInterface.js +234 -43
  49. package/build/contractInterface.js.map +1 -0
  50. package/build/contractTypes.d.ts +9 -0
  51. package/build/contractTypes.d.ts.map +1 -0
  52. package/build/contractTypes.js +28 -0
  53. package/build/contractTypes.js.map +1 -0
  54. package/build/countryStats.d.ts +12 -3
  55. package/build/countryStats.d.ts.map +1 -0
  56. package/build/countryStats.js +87 -23
  57. package/build/countryStats.js.map +1 -0
  58. package/build/deployClient.d.ts +138 -24
  59. package/build/deployClient.d.ts.map +1 -0
  60. package/build/deployClient.js +91 -31
  61. package/build/deployClient.js.map +1 -0
  62. package/build/deviceIdentity.d.ts +68 -0
  63. package/build/deviceIdentity.d.ts.map +1 -0
  64. package/build/deviceIdentity.js +209 -0
  65. package/build/deviceIdentity.js.map +1 -0
  66. package/build/freezeClient.d.ts +26 -6
  67. package/build/freezeClient.d.ts.map +1 -0
  68. package/build/freezeClient.js +30 -6
  69. package/build/freezeClient.js.map +1 -0
  70. package/build/generationClient.d.ts +6 -3
  71. package/build/generationClient.d.ts.map +1 -0
  72. package/build/generationClient.js +25 -38
  73. package/build/generationClient.js.map +1 -0
  74. package/build/holderSecret.d.ts +70 -0
  75. package/build/holderSecret.d.ts.map +1 -0
  76. package/build/holderSecret.js +76 -0
  77. package/build/holderSecret.js.map +1 -0
  78. package/build/index.d.ts +38 -38
  79. package/build/index.d.ts.map +1 -0
  80. package/build/index.js +97 -121
  81. package/build/index.js.map +1 -0
  82. package/build/keysClient.d.ts +142 -0
  83. package/build/keysClient.d.ts.map +1 -0
  84. package/build/keysClient.js +109 -0
  85. package/build/keysClient.js.map +1 -0
  86. package/build/licenseLeaf.d.ts +76 -0
  87. package/build/licenseLeaf.d.ts.map +1 -0
  88. package/build/licenseLeaf.js +189 -0
  89. package/build/licenseLeaf.js.map +1 -0
  90. package/build/licenseRecord.d.ts +68 -5
  91. package/build/licenseRecord.d.ts.map +1 -0
  92. package/build/licenseRecord.js +26 -1
  93. package/build/licenseRecord.js.map +1 -0
  94. package/build/licenseStore.d.ts +81 -4
  95. package/build/licenseStore.d.ts.map +1 -0
  96. package/build/licenseStore.js +400 -30
  97. package/build/licenseStore.js.map +1 -0
  98. package/build/licenseTrustless.d.ts +43 -0
  99. package/build/licenseTrustless.d.ts.map +1 -0
  100. package/build/licenseTrustless.js +180 -0
  101. package/build/licenseTrustless.js.map +1 -0
  102. package/build/manifestVerify.d.ts +29 -1
  103. package/build/manifestVerify.d.ts.map +1 -0
  104. package/build/manifestVerify.js +46 -4
  105. package/build/manifestVerify.js.map +1 -0
  106. package/build/minaPriceHistory.d.ts +9 -0
  107. package/build/minaPriceHistory.d.ts.map +1 -0
  108. package/build/minaPriceHistory.js +152 -0
  109. package/build/minaPriceHistory.js.map +1 -0
  110. package/build/onChainRoot.d.ts +60 -0
  111. package/build/onChainRoot.d.ts.map +1 -0
  112. package/build/onChainRoot.js +74 -0
  113. package/build/onChainRoot.js.map +1 -0
  114. package/build/ownershipLicenseHash.d.ts +25 -0
  115. package/build/ownershipLicenseHash.d.ts.map +1 -0
  116. package/build/ownershipLicenseHash.js +29 -0
  117. package/build/ownershipLicenseHash.js.map +1 -0
  118. package/build/ownershipSignature.d.ts +153 -0
  119. package/build/ownershipSignature.d.ts.map +1 -0
  120. package/build/ownershipSignature.js +297 -0
  121. package/build/ownershipSignature.js.map +1 -0
  122. package/build/permitCheckFlow.d.ts +155 -0
  123. package/build/permitCheckFlow.d.ts.map +1 -0
  124. package/build/permitCheckFlow.js +196 -0
  125. package/build/permitCheckFlow.js.map +1 -0
  126. package/build/permitDescribe.d.ts +124 -0
  127. package/build/permitDescribe.d.ts.map +1 -0
  128. package/build/permitDescribe.js +324 -0
  129. package/build/permitDescribe.js.map +1 -0
  130. package/build/permitInterface.d.ts +85 -0
  131. package/build/permitInterface.d.ts.map +1 -0
  132. package/build/permitInterface.js +121 -0
  133. package/build/permitInterface.js.map +1 -0
  134. package/build/permitMessages.d.ts +313 -0
  135. package/build/permitMessages.d.ts.map +1 -0
  136. package/build/permitMessages.js +462 -0
  137. package/build/permitMessages.js.map +1 -0
  138. package/build/permitProof.d.ts +108 -0
  139. package/build/permitProof.d.ts.map +1 -0
  140. package/build/permitProof.js +202 -0
  141. package/build/permitProof.js.map +1 -0
  142. package/build/permitRegistryRebuild.d.ts +166 -0
  143. package/build/permitRegistryRebuild.d.ts.map +1 -0
  144. package/build/permitRegistryRebuild.js +347 -0
  145. package/build/permitRegistryRebuild.js.map +1 -0
  146. package/build/permitTree.d.ts +88 -0
  147. package/build/permitTree.d.ts.map +1 -0
  148. package/build/permitTree.js +230 -0
  149. package/build/permitTree.js.map +1 -0
  150. package/build/permitsClient.d.ts +680 -0
  151. package/build/permitsClient.d.ts.map +1 -0
  152. package/build/permitsClient.js +597 -0
  153. package/build/permitsClient.js.map +1 -0
  154. package/build/proveErrors.d.ts +50 -0
  155. package/build/proveErrors.d.ts.map +1 -0
  156. package/build/proveErrors.js +76 -0
  157. package/build/proveErrors.js.map +1 -0
  158. package/build/receipt.d.ts +95 -0
  159. package/build/receipt.d.ts.map +1 -0
  160. package/build/receipt.js +84 -0
  161. package/build/receipt.js.map +1 -0
  162. package/build/refundClient.d.ts +7 -12
  163. package/build/refundClient.d.ts.map +1 -0
  164. package/build/refundClient.js +52 -21
  165. package/build/refundClient.js.map +1 -0
  166. package/build/registryTrustVerify.d.ts +78 -0
  167. package/build/registryTrustVerify.d.ts.map +1 -0
  168. package/build/registryTrustVerify.js +129 -0
  169. package/build/registryTrustVerify.js.map +1 -0
  170. package/build/releaseClient.d.ts +26 -0
  171. package/build/releaseClient.d.ts.map +1 -0
  172. package/build/releaseClient.js +65 -0
  173. package/build/releaseClient.js.map +1 -0
  174. package/build/renewClient.d.ts +28 -2
  175. package/build/renewClient.d.ts.map +1 -0
  176. package/build/renewClient.js +31 -27
  177. package/build/renewClient.js.map +1 -0
  178. package/build/seatName.d.ts +8 -0
  179. package/build/seatName.d.ts.map +1 -0
  180. package/build/seatName.js +136 -0
  181. package/build/seatName.js.map +1 -0
  182. package/build/slotConstants.d.ts +15 -0
  183. package/build/slotConstants.d.ts.map +1 -0
  184. package/build/slotConstants.js +17 -0
  185. package/build/slotConstants.js.map +1 -0
  186. package/build/statsClient.d.ts +61 -7
  187. package/build/statsClient.d.ts.map +1 -0
  188. package/build/statsClient.js +44 -5
  189. package/build/statsClient.js.map +1 -0
  190. package/build/successorChain.d.ts +35 -0
  191. package/build/successorChain.d.ts.map +1 -0
  192. package/build/successorChain.js +69 -0
  193. package/build/successorChain.js.map +1 -0
  194. package/build/taxId.d.ts +44 -0
  195. package/build/taxId.d.ts.map +1 -0
  196. package/build/taxId.js +212 -0
  197. package/build/taxId.js.map +1 -0
  198. package/build/trustAnchorVerify.d.ts +49 -0
  199. package/build/trustAnchorVerify.d.ts.map +1 -0
  200. package/build/trustAnchorVerify.js +432 -0
  201. package/build/trustAnchorVerify.js.map +1 -0
  202. package/build/trustAnchorVerifyCli.d.ts +45 -0
  203. package/build/trustAnchorVerifyCli.d.ts.map +1 -0
  204. package/build/trustAnchorVerifyCli.js +235 -0
  205. package/build/trustAnchorVerifyCli.js.map +1 -0
  206. package/build/txBroadcastClient.d.ts +26 -0
  207. package/build/txBroadcastClient.d.ts.map +1 -0
  208. package/build/txBroadcastClient.js +20 -0
  209. package/build/txBroadcastClient.js.map +1 -0
  210. package/build/unbindClient.d.ts +77 -0
  211. package/build/unbindClient.d.ts.map +1 -0
  212. package/build/unbindClient.js +169 -0
  213. package/build/unbindClient.js.map +1 -0
  214. package/build/units.d.ts +28 -0
  215. package/build/units.d.ts.map +1 -0
  216. package/build/units.js +65 -0
  217. package/build/units.js.map +1 -0
  218. package/build/vatRates.d.ts +54 -0
  219. package/build/vatRates.d.ts.map +1 -0
  220. package/build/vatRates.js +82 -0
  221. package/build/vatRates.js.map +1 -0
  222. package/build/vatRatesData.d.ts +5 -0
  223. package/build/vatRatesData.d.ts.map +1 -0
  224. package/build/vatRatesData.js +42 -0
  225. package/build/vatRatesData.js.map +1 -0
  226. package/build/vendorAccountClient.d.ts +66 -0
  227. package/build/vendorAccountClient.d.ts.map +1 -0
  228. package/build/vendorAccountClient.js +47 -0
  229. package/build/vendorAccountClient.js.map +1 -0
  230. package/build/verifyCore.d.ts +4 -1
  231. package/build/verifyCore.d.ts.map +1 -0
  232. package/build/verifyCore.js +21 -10
  233. package/build/verifyCore.js.map +1 -0
  234. package/build/verifyOffline.d.ts +85 -0
  235. package/build/verifyOffline.d.ts.map +1 -0
  236. package/build/verifyOffline.js +139 -0
  237. package/build/verifyOffline.js.map +1 -0
  238. package/build/verifyService.d.ts.map +1 -0
  239. package/build/verifyService.js +571 -501
  240. package/build/verifyService.js.map +1 -0
  241. package/build/verifyWithAncestors.d.ts +19 -0
  242. package/build/verifyWithAncestors.d.ts.map +1 -0
  243. package/build/verifyWithAncestors.js +127 -0
  244. package/build/verifyWithAncestors.js.map +1 -0
  245. package/build/zipToState.d.ts +2 -0
  246. package/build/zipToState.d.ts.map +1 -0
  247. package/build/zipToState.js +94 -0
  248. package/build/zipToState.js.map +1 -0
  249. package/package.json +141 -16
  250. package/src/agent.ts +682 -0
  251. package/src/agentActions.ts +637 -0
  252. package/src/agentRecord.ts +121 -0
  253. package/src/agentVendor.ts +356 -0
  254. package/src/appsLookup.ts +48 -0
  255. package/src/archiveBootstrap.ts +386 -0
  256. package/src/atomicJson.ts +53 -0
  257. package/src/bindClient.ts +297 -0
  258. package/src/buyClient.ts +344 -0
  259. package/src/chainHead.ts +106 -0
  260. package/src/compactMerkleMap.ts +222 -0
  261. package/src/contractInterface.ts +465 -0
  262. package/src/contractTypes.ts +31 -0
  263. package/src/countryStats.ts +200 -0
  264. package/src/deployClient.ts +517 -0
  265. package/src/deviceIdentity.ts +249 -0
  266. package/src/freezeClient.ts +162 -0
  267. package/src/generationClient.ts +145 -0
  268. package/src/holderSecret.ts +91 -0
  269. package/src/index.ts +362 -0
  270. package/src/keysClient.ts +135 -0
  271. package/src/licenseLeaf.ts +221 -0
  272. package/src/licenseRecord.ts +190 -0
  273. package/src/licenseStore.ts +767 -0
  274. package/src/licenseTrustless.ts +207 -0
  275. package/src/manifestVerify.ts +164 -0
  276. package/src/minaPriceHistory.ts +166 -0
  277. package/src/onChainRoot.ts +90 -0
  278. package/src/ownershipLicenseHash.ts +31 -0
  279. package/src/ownershipSignature.ts +421 -0
  280. package/src/permitCheckFlow.ts +227 -0
  281. package/src/permitDescribe.ts +362 -0
  282. package/src/permitInterface.ts +146 -0
  283. package/src/permitMessages.ts +572 -0
  284. package/src/permitProof.ts +230 -0
  285. package/src/permitRegistryRebuild.ts +423 -0
  286. package/src/permitTree.ts +232 -0
  287. package/src/permitsClient.ts +746 -0
  288. package/src/proveErrors.ts +98 -0
  289. package/src/receipt.ts +156 -0
  290. package/src/refundClient.ts +177 -0
  291. package/src/registryTrustVerify.ts +192 -0
  292. package/src/releaseClient.ts +101 -0
  293. package/src/renewClient.ts +234 -0
  294. package/src/seatName.ts +165 -0
  295. package/src/slotConstants.ts +18 -0
  296. package/src/statsClient.ts +249 -0
  297. package/src/successorChain.ts +87 -0
  298. package/src/taxId.ts +227 -0
  299. package/src/trustAnchorVerify.ts +550 -0
  300. package/src/trustAnchorVerifyCli.ts +234 -0
  301. package/src/txBroadcastClient.ts +48 -0
  302. package/src/unbindClient.ts +233 -0
  303. package/src/units.ts +74 -0
  304. package/src/vatRates.ts +100 -0
  305. package/src/vatRatesData.ts +43 -0
  306. package/src/vendorAccountClient.ts +120 -0
  307. package/src/verifyCore.ts +100 -0
  308. package/src/verifyOffline.ts +212 -0
  309. package/src/verifyService.ts +982 -0
  310. package/src/verifyWithAncestors.ts +163 -0
  311. package/src/zipToState.ts +94 -0
  312. package/build/licenseProof.d.ts +0 -26
  313. package/build/licenseProof.js +0 -41
  314. package/build/migrateClient.d.ts +0 -43
  315. package/build/migrateClient.js +0 -47
  316. package/build/ownership.d.ts +0 -34
  317. package/build/ownership.js +0 -147
@@ -0,0 +1,421 @@
1
+ // Copyright (c) 2025-2026 zkLicensing project developers
2
+ // SPDX-License-Identifier: BUSL-1.1
3
+
4
+ /**
5
+ * ownershipSignature.ts
6
+ *
7
+ * Schnorr-over-Pallas signing primitive for the two on-chain flows that
8
+ * need the passphrase-derived owner key:
9
+ *
10
+ * 1. The /prove/bind and /prove/unbind flows — the passphrase-derived
11
+ * keypair authorizes device slot mutations, and the signature is
12
+ * re-verified INSIDE the circuit by `Signature.verify()` so even a
13
+ * compromised licensing service cannot forge a bind.
14
+ * 2. The retained `ActivationMessage` shape (see below) — kept so external
15
+ * tooling can still sign an "I hold the passphrase for this license"
16
+ * assertion off-band. The platform no longer runs a challenge/respond
17
+ * endpoint; the shape is purely an SDK primitive now.
18
+ *
19
+ * Deterministic HKDF-SHA256 derivation of a seed from the passphrase,
20
+ * reduced to a Pallas scalar → o1js `PrivateKey`. The buyer can
21
+ * re-derive the same keypair anywhere from just the passphrase.
22
+ *
23
+ * o1js-dependent by design: the whole point of switching from Ed25519 to
24
+ * Pallas Schnorr is native in-circuit verification. Any client that
25
+ * signs an activation or binding challenge already ships o1js for the
26
+ * downstream buy/verify surface, so the "keep this module o1js-free"
27
+ * constraint that motivated the Ed25519 version no longer applies.
28
+ *
29
+ * HKDF still needs WebCrypto (Node ≥ 20, Chromium 137+, Safari 17+,
30
+ * Firefox 130+). The module throws at derive time if
31
+ * `globalThis.crypto.subtle` is unavailable — callers cannot silently
32
+ * fall back to a weaker path.
33
+ */
34
+
35
+ import { Field, PrivateKey, PublicKey, Signature, UInt32 } from 'o1js';
36
+ import { packSlotTimes } from './licenseLeaf.js';
37
+ import { licenseHashFromPubkey } from './ownershipLicenseHash.js';
38
+
39
+ // HKDF info binds the derivation to a specific zkApp address, so the
40
+ // same passphrase yields different keypairs for different apps. This
41
+ // makes credentials per-app: a leaked passphrase for app A cannot be
42
+ // replayed as an activation for app B. RFC 5869 designates `info` as
43
+ // the domain-separation channel (salt is for randomness, which we don't
44
+ // have here — the passphrase IS the secret input).
45
+ //
46
+ // The `v1` prefix reserves room to rotate the KDF or the target key type
47
+ // later without a silent collision — a future scheme bumps to `v2` and
48
+ // derives an unrelated key from the same passphrase.
49
+ const HKDF_INFO_PREFIX = 'zkLicensing/v1/ownership|';
50
+ const HKDF_SALT = new Uint8Array(0);
51
+
52
+ // Domain-separation tags baked into every signed message. Prevents a
53
+ // signature captured on one flow (activation) from being replayed on
54
+ // another (binding) — the first Field the signer hashes is the flow ID,
55
+ // so the two signatures live in disjoint spaces even at identical
56
+ // (licenseHash, nonce).
57
+ //
58
+ // Exported so `LicensingApp.bind()` / `unbind()` can rebuild the exact
59
+ // same Field[] preimage inside the circuit for `Signature.verify()`.
60
+ // Bumping these values is a hardfork — every off-chain signer and every
61
+ // deployed circuit would need to agree.
62
+ export const TAG_ACTIVATION = 1n;
63
+ export const TAG_BINDING = 2n;
64
+ export const TAG_UNBINDING = 3n;
65
+
66
+ function buildHkdfInfo(zkAppAddress: string): Uint8Array {
67
+ return new TextEncoder().encode(HKDF_INFO_PREFIX + zkAppAddress);
68
+ }
69
+
70
+ function utf8Bytes(s: string): Uint8Array {
71
+ return new TextEncoder().encode(s);
72
+ }
73
+
74
+ function bytesToBase64(bytes: Uint8Array): string {
75
+ let s = '';
76
+ for (const b of bytes) s += String.fromCharCode(b);
77
+ return btoa(s);
78
+ }
79
+
80
+ function base64ToBytes(b64: string): Uint8Array {
81
+ const bin = atob(b64);
82
+ const out = new Uint8Array(bin.length);
83
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
84
+ return out;
85
+ }
86
+
87
+ function subtle(): SubtleCrypto {
88
+ const s = (globalThis.crypto as Crypto | undefined)?.subtle;
89
+ if (!s) {
90
+ throw new Error(
91
+ 'WebCrypto unavailable — ownershipSignature requires Node ≥ 20 or a modern browser (Chromium 137+, Safari 17+, Firefox 130+).',
92
+ );
93
+ }
94
+ return s;
95
+ }
96
+
97
+ function bytesToBigInt(bytes: Uint8Array): bigint {
98
+ let n = 0n;
99
+ for (const b of bytes) n = (n << 8n) | BigInt(b);
100
+ return n;
101
+ }
102
+
103
+ /**
104
+ * HKDF-SHA256 with empty salt and a per-app domain-separation info
105
+ * string (`zkLicensing/v1/ownership|<zkAppAddress>`). Deterministic:
106
+ * same (passphrase, zkAppAddress) → same 32-byte seed. Different
107
+ * zkAppAddress → cryptographically unrelated seed even with the same
108
+ * passphrase.
109
+ */
110
+ export async function deriveSeed(
111
+ passphrase: string,
112
+ zkAppAddress: string,
113
+ ): Promise<Uint8Array> {
114
+ const baseKey = await subtle().importKey(
115
+ 'raw',
116
+ utf8Bytes(passphrase) as unknown as ArrayBuffer,
117
+ 'HKDF',
118
+ false,
119
+ ['deriveBits'],
120
+ );
121
+ const info = buildHkdfInfo(zkAppAddress);
122
+ const bits = await subtle().deriveBits(
123
+ {
124
+ name: 'HKDF',
125
+ hash: 'SHA-256',
126
+ salt: HKDF_SALT as unknown as ArrayBuffer,
127
+ info: info as unknown as ArrayBuffer,
128
+ },
129
+ baseKey,
130
+ 256,
131
+ );
132
+ return new Uint8Array(bits);
133
+ }
134
+
135
+ export type OwnershipKeypair = {
136
+ privateKey: PrivateKey;
137
+ publicKey: PublicKey;
138
+ publicKeyBase58: string;
139
+ };
140
+
141
+ /**
142
+ * Derive a Pallas Schnorr keypair deterministically from (passphrase,
143
+ * zkAppAddress). The keypair is bound to the specific app: the same
144
+ * passphrase used against a different app yields a different keypair,
145
+ * so a leaked passphrase cannot be replayed as an activation for any
146
+ * other app.
147
+ *
148
+ * The 32-byte HKDF output is interpreted as a big-endian integer and
149
+ * passed to `PrivateKey.fromBigInt`, which reduces it modulo the Pallas
150
+ * scalar field order internally. All 2^256 possible HKDF outputs map to
151
+ * valid private keys with (256 - log2(Fq_order))-bit modular bias — the
152
+ * scalar field is ~254 bits, so bias is <2^-253 and lies well below any
153
+ * practical distinguisher.
154
+ */
155
+ export async function deriveOwnershipKeypair(
156
+ passphrase: string,
157
+ zkAppAddress: string,
158
+ ): Promise<OwnershipKeypair> {
159
+ const seed = await deriveSeed(passphrase, zkAppAddress);
160
+ const privateKey = PrivateKey.fromBigInt(bytesToBigInt(seed));
161
+ const publicKey = privateKey.toPublicKey();
162
+ return { privateKey, publicKey, publicKeyBase58: publicKey.toBase58() };
163
+ }
164
+
165
+ /**
166
+ * The ownership keypair of license `licenseHash`, trying each candidate app
167
+ * address in turn. The key is derived from the address the license was
168
+ * issued on; after a redeploy the license carries over to the successor
169
+ * unchanged, so its key is the earlier generation's. Pass the app's
170
+ * generation addresses (any order; the live one first is cheapest for new
171
+ * licenses). A wrong address only gives a key that doesn't match, so the
172
+ * list needn't be authenticated. Null when none matches (another passphrase).
173
+ */
174
+ export async function findOwnershipKeypair(
175
+ passphrase: string,
176
+ licenseHash: string,
177
+ candidateAddresses: readonly string[],
178
+ ): Promise<(OwnershipKeypair & { zkAppAddress: string }) | null> {
179
+ for (const zkAppAddress of [...new Set(candidateAddresses)]) {
180
+ const kp = await deriveOwnershipKeypair(passphrase, zkAppAddress);
181
+ if (licenseHashFromPubkey(kp.publicKey) === licenseHash) return { ...kp, zkAppAddress };
182
+ }
183
+ return null;
184
+ }
185
+
186
+ /**
187
+ * The addresses of every generation of the app at `zkAppAddress` (any of
188
+ * them), live first, from the keeper's generation list; just
189
+ * `zkAppAddress` when the keeper doesn't know it. For findOwnershipKeypair.
190
+ */
191
+ export async function ownershipCandidateAddresses(keeperUrl: string, zkAppAddress: string, fetcher: typeof fetch = fetch): Promise<string[]> {
192
+ try {
193
+ const r = await fetcher(`${keeperUrl.replace(/\/+$/, '')}/apps/by-address/${encodeURIComponent(zkAppAddress)}`);
194
+ if (!r.ok) return [zkAppAddress];
195
+ const body = await r.json() as { generations?: Array<{ gen: number; zkAppAddress: string }> };
196
+ const gens = Array.isArray(body.generations) ? [...body.generations].sort((a, b) => b.gen - a.gen).map(g => g.zkAppAddress) : [];
197
+ return [...new Set([zkAppAddress, ...gens])];
198
+ } catch {
199
+ return [zkAppAddress];
200
+ }
201
+ }
202
+
203
+ // ── Message shapes ────────────────────────────────────────────────
204
+
205
+ /**
206
+ * Activation assertion shape. Proves the caller holds the passphrase
207
+ * that derived the license's ownerPubKey. Retained as an SDK primitive
208
+ * so external tooling can still request / verify an off-band "I own
209
+ * this license" signature — the on-chain flows themselves do not use
210
+ * it.
211
+ */
212
+ export type ActivationMessage = {
213
+ licenseHash: string; // Field as decimal string
214
+ nonce: string; // Field as decimal string — replay defense
215
+ zkAppAddress: string; // base58
216
+ };
217
+
218
+ /**
219
+ * Binding / unbinding message — the /prove/bind and /prove/unbind
220
+ * flows. Authorizes a specific device commitment being placed into or
221
+ * cleared from a specific slot on a specific license.
222
+ */
223
+ export type BindingMessage = {
224
+ licenseHash: string; // Field as decimal string
225
+ deviceCommitment: string; // Field as decimal string (Poseidon(devicePubKeyHash))
226
+ slotIndex: number; // 0..cap-1
227
+ nonce: string; // Field as decimal string
228
+ // Target slot's current packed (boundAt, freedAt) — see slotStateOf().
229
+ // Every bind/unbind changes it, so a signature authorizes exactly one
230
+ // transition and cannot be replayed once the slot has moved on.
231
+ slotState: string; // Field as decimal string
232
+ // The license leaf's current expirySlot. A refund + re-buy with the same
233
+ // passphrase resets every slot to pristine; the new leaf's expiry differs,
234
+ // so signatures from the earlier purchase stay dead.
235
+ expirySlot: number;
236
+ zkAppAddress: string; // base58 — stops cross-deployment replay
237
+ };
238
+
239
+ /**
240
+ * Packed (boundAt, freedAt) of one slot as the circuit sees it — the
241
+ * `slotState` a BindingMessage must commit to. Takes the slot entry from a
242
+ * LicenseRecord's `devices[]` (as served by GET /license/:zkApp/:hash);
243
+ * an absent entry is a pristine slot (0).
244
+ */
245
+ export function slotStateOf(
246
+ device?: { boundAtSlot: number; freedAtSlot: number } | null,
247
+ ): string {
248
+ if (!device) return '0';
249
+ return packSlotTimes(UInt32.from(device.boundAtSlot), UInt32.from(device.freedAtSlot)).toString();
250
+ }
251
+
252
+ // Retained for back-compat with callers that imported ChallengeMessage
253
+ // from this module before the v2 rename. New code should use
254
+ // ActivationMessage directly.
255
+ export type ChallengeMessage = ActivationMessage;
256
+
257
+ /**
258
+ * Canonicalize the activation message into the Field[] that
259
+ * `Signature.create` / `Signature.verify` hash. The domain-separation
260
+ * tag prefix means no captured activation signature can be replayed as
261
+ * a binding signature, even if all other fields collide.
262
+ */
263
+ export function activationMessageFields(msg: ActivationMessage): Field[] {
264
+ const addr = PublicKey.fromBase58(msg.zkAppAddress);
265
+ return [
266
+ Field(TAG_ACTIVATION),
267
+ Field(msg.licenseHash),
268
+ Field(msg.nonce),
269
+ addr.x,
270
+ addr.isOdd.toField(),
271
+ ];
272
+ }
273
+
274
+ /**
275
+ * Canonicalize a binding or unbinding message. The `tag` argument
276
+ * distinguishes bind (TAG_BINDING) from unbind (TAG_UNBINDING) so a
277
+ * bind signature captured off-chain cannot be replayed as an unbind
278
+ * even at identical (licenseHash, deviceCommitment, slotIndex, nonce).
279
+ */
280
+ function bindingLikeFields(tag: bigint, msg: BindingMessage): Field[] {
281
+ const addr = PublicKey.fromBase58(msg.zkAppAddress);
282
+ return [
283
+ Field(tag),
284
+ Field(msg.licenseHash),
285
+ Field(msg.deviceCommitment),
286
+ Field(msg.slotIndex),
287
+ Field(msg.nonce),
288
+ Field(msg.slotState),
289
+ Field(msg.expirySlot),
290
+ addr.x,
291
+ addr.isOdd.toField(),
292
+ ];
293
+ }
294
+
295
+ export function bindingMessageFields(msg: BindingMessage): Field[] {
296
+ return bindingLikeFields(TAG_BINDING, msg);
297
+ }
298
+
299
+ export function unbindingMessageFields(msg: BindingMessage): Field[] {
300
+ return bindingLikeFields(TAG_UNBINDING, msg);
301
+ }
302
+
303
+ // ── Signing ───────────────────────────────────────────────────────
304
+
305
+ export function signActivation(
306
+ privateKey: PrivateKey,
307
+ msg: ActivationMessage,
308
+ ): Signature {
309
+ return Signature.create(privateKey, activationMessageFields(msg));
310
+ }
311
+
312
+ export function signBinding(
313
+ privateKey: PrivateKey,
314
+ msg: BindingMessage,
315
+ ): Signature {
316
+ return Signature.create(privateKey, bindingMessageFields(msg));
317
+ }
318
+
319
+ export function signUnbinding(
320
+ privateKey: PrivateKey,
321
+ msg: BindingMessage,
322
+ ): Signature {
323
+ return Signature.create(privateKey, unbindingMessageFields(msg));
324
+ }
325
+
326
+ // Retained for back-compat with the Ed25519-era name. New code should
327
+ // call signActivation directly.
328
+ export function signChallenge(
329
+ privateKey: PrivateKey,
330
+ msg: ChallengeMessage,
331
+ ): Signature {
332
+ return signActivation(privateKey, msg);
333
+ }
334
+
335
+ // ── Verification ──────────────────────────────────────────────────
336
+
337
+ /**
338
+ * Verify an activation signature. Returns false (never throws) for any
339
+ * failure — malformed pubkey, malformed signature, actual signature
340
+ * mismatch. Callers get a single go/no-go decision without having to
341
+ * distinguish "invalid" from "unparseable."
342
+ */
343
+ export function verifyActivation(
344
+ publicKey: PublicKey,
345
+ msg: ActivationMessage,
346
+ signature: Signature,
347
+ ): boolean {
348
+ try {
349
+ return signature.verify(publicKey, activationMessageFields(msg)).toBoolean();
350
+ } catch {
351
+ return false;
352
+ }
353
+ }
354
+
355
+ export function verifyBinding(
356
+ publicKey: PublicKey,
357
+ msg: BindingMessage,
358
+ signature: Signature,
359
+ ): boolean {
360
+ try {
361
+ return signature.verify(publicKey, bindingMessageFields(msg)).toBoolean();
362
+ } catch {
363
+ return false;
364
+ }
365
+ }
366
+
367
+ export function verifyUnbinding(
368
+ publicKey: PublicKey,
369
+ msg: BindingMessage,
370
+ signature: Signature,
371
+ ): boolean {
372
+ try {
373
+ return signature.verify(publicKey, unbindingMessageFields(msg)).toBoolean();
374
+ } catch {
375
+ return false;
376
+ }
377
+ }
378
+
379
+ // Back-compat alias, see signChallenge.
380
+ export function verifyChallenge(
381
+ publicKey: PublicKey,
382
+ msg: ChallengeMessage,
383
+ signature: Signature,
384
+ ): boolean {
385
+ return verifyActivation(publicKey, msg, signature);
386
+ }
387
+
388
+ // ── Wire encoding helpers ─────────────────────────────────────────
389
+
390
+ /**
391
+ * Encode a signature for the wire (POST body, ProofFile JSON, etc).
392
+ * Base58 matches Mina's canonical serialization for both PublicKey and
393
+ * Signature, keeping the same encoding shape across everything the
394
+ * buyer's SDK produces.
395
+ */
396
+ export function encodeSignature(sig: Signature): string {
397
+ return sig.toBase58();
398
+ }
399
+
400
+ export function decodeSignature(base58: string): Signature {
401
+ return Signature.fromBase58(base58);
402
+ }
403
+
404
+ export function encodePublicKey(pk: PublicKey): string {
405
+ return pk.toBase58();
406
+ }
407
+
408
+ export function decodePublicKey(base58: string): PublicKey {
409
+ return PublicKey.fromBase58(base58);
410
+ }
411
+
412
+ // Retained so callers that already do `encodeBase64(seed)` for logging
413
+ // or persistence keep working — this module no longer produces raw byte
414
+ // pubkeys or signatures, only o1js objects with .toBase58().
415
+ export function encodeBase64(bytes: Uint8Array): string {
416
+ return bytesToBase64(bytes);
417
+ }
418
+
419
+ export function decodeBase64(b64: string): Uint8Array {
420
+ return base64ToBytes(b64);
421
+ }
@@ -0,0 +1,227 @@
1
+ // Copyright (c) 2025-2026 zkLicensing project developers
2
+ // SPDX-License-Identifier: BUSL-1.1
3
+
4
+ /**
5
+ * permitCheckFlow.ts — `zklicensing/permits/check`
6
+ *
7
+ * Anonymous permit checks through the keeper's relay (verify.html's
8
+ * Anonymous mode and prove.html use this; so can any checker or holder app).
9
+ *
10
+ * checker: openCheck (or createCheckEvent + openCheck in it) → show the
11
+ * check's link → verifyCheck once the holder has proved
12
+ * holder: proveForCheck(checkId, permit file, wallet)
13
+ *
14
+ * The challenge is bound to the site both sides are on: context =
15
+ * verifierContext(origin, check nonce). The holder builds its Merkle witness
16
+ * from a compact copy of the whole registry (zklicensing/permits/tree), so
17
+ * the keeper never learns which permit is being proved; the proof itself
18
+ * reveals nothing about it. The checker verifies the proof itself, against roots from the keeper or, with
19
+ * `trustless`, from a Mina node.
20
+ */
21
+
22
+ import { Field, MerkleMap, PublicKey } from 'o1js';
23
+ import { permitLeafHash } from './permitMessages.js';
24
+ import { recordToLeaf, permitStatusAt, type PermitRecord } from './permitRegistryRebuild.js';
25
+ import { syncRegistryTree, witnessFromCompactTree, type TreeCache } from './permitTree.js';
26
+ import type { PermitMessageDescription } from './permitDescribe.js';
27
+
28
+ export type Network = 'devnet' | 'mainnet';
29
+
30
+ export type CheckView = {
31
+ id: string; network: Network; registry: string; requiredScope: string; label: string; nonce: string;
32
+ createdAt: string; expiresAt: string; status: 'waiting' | 'proved' | 'expired';
33
+ event?: { id: string; label: string; expiresAt: string };
34
+ };
35
+ export type CheckEventView = { id: string; network: Network; registry: string; requiredScope: string; label: string; createdAt: string; expiresAt: string };
36
+
37
+ /**
38
+ * The holder's permit file (from the apply page): no secret in it. `registry`
39
+ * is where it was issued; `lineage`, that registry's first generation's
40
+ * address (what its secret is derived against; `registry` when absent).
41
+ */
42
+ export type PermitFileRef = { registry: string; subject: string; holderKey: string; network?: string; lineage?: string };
43
+
44
+ export class CheckError extends Error {
45
+ constructor(message: string, readonly code: string, readonly status = 0) { super(message); this.name = 'CheckError'; }
46
+ }
47
+
48
+ /** The keeper's check relay. */
49
+ export function createCheckRelay(keeperUrl: string, fetcher: typeof fetch = fetch) {
50
+ const base = keeperUrl.replace(/\/+$/, '');
51
+ async function call<T>(method: string, path: string, body?: unknown, headers: Record<string, string> = {}): Promise<T> {
52
+ const r = await fetcher(base + path, {
53
+ method, cache: 'no-store',
54
+ headers: { Accept: 'application/json', ...(body === undefined ? {} : { 'Content-Type': 'application/json' }), ...headers },
55
+ body: body === undefined ? undefined : JSON.stringify(body),
56
+ });
57
+ const j: any = await r.json().catch(() => null);
58
+ if (!r.ok) throw new CheckError(j?.error ?? `HTTP ${r.status}`, j?.code ?? 'HTTP_ERROR', r.status);
59
+ return j as T;
60
+ }
61
+ return {
62
+ /** A single check: its own nonce, so its nullifier never repeats. */
63
+ openCheck: (a: { registry: string; network?: Network; requiredScope?: string; label?: string }) =>
64
+ call<{ check: CheckView }>('POST', '/permits/checks', a).then(r => r.check),
65
+ /** A check in an event (the event's nonce), opened with the event key. */
66
+ openEventCheck: (eventId: string, eventKey: string) =>
67
+ call<{ check: CheckView }>('POST', '/permits/checks', { eventId }, { 'X-Event-Key': eventKey }).then(r => r.check),
68
+ getCheck: (id: string) => call<{ check: CheckView; proof?: unknown; provedAt?: string }>('GET', `/permits/checks/${encodeURIComponent(id)}`),
69
+ submitProof: (id: string, proof: unknown) => call<{ ok: true }>('POST', `/permits/checks/${encodeURIComponent(id)}/proof`, { proof }),
70
+ createEvent: (a: { registry: string; network?: Network; requiredScope?: string; label: string; days?: number }) =>
71
+ call<{ event: CheckEventView; eventKey: string }>('POST', '/permits/check-events', a),
72
+ getEvent: (id: string) => call<{ event: CheckEventView }>('GET', `/permits/check-events/${encodeURIComponent(id)}`).then(r => r.event),
73
+ /** Records a verified nullifier at the event: first use there, or when it was first seen. */
74
+ markSeen: (eventId: string, eventKey: string, nullifier: string) =>
75
+ call<{ firstUse: boolean; firstSeenAt: string; count: number }>('POST', `/permits/check-events/${encodeURIComponent(eventId)}/seen`, { nullifier }, { 'X-Event-Key': eventKey }),
76
+ seenCount: (eventId: string, eventKey: string) =>
77
+ call<{ count: number }>('GET', `/permits/check-events/${encodeURIComponent(eventId)}/seen`, undefined, { 'X-Event-Key': eventKey }).then(r => r.count),
78
+ /** The registry's compact tree (zklicensing/permits/tree), or with `since` the changes since that root. */
79
+ getTree: (registry: string, since?: string) =>
80
+ call<any>('GET', `/permits/registries/${encodeURIComponent(registry)}/tree${since ? `?since=${encodeURIComponent(since)}` : ''}`),
81
+ getRoots: (registry: string) => call<{ roots: Array<{ root: string; slot: number }>; current: string; slot: number }>('GET', `/permits/registries/${encodeURIComponent(registry)}/roots`),
82
+ };
83
+ }
84
+
85
+ /** The Merkle witness for `subject`, built locally from all of a registry's permits. */
86
+ export function witnessFromPermits(permits: PermitRecord[], subject: string): { root: string; witness: { isLefts: boolean[]; siblings: string[] } } {
87
+ const map = new MerkleMap();
88
+ for (const p of permits) map.set(Field(p.subject), permitLeafHash(recordToLeaf(p)));
89
+ const w = map.getWitness(Field(subject));
90
+ return { root: map.getRoot().toString(), witness: { isLefts: w.isLefts.map(b => b.toBoolean()), siblings: w.siblings.map(s => s.toString()) } };
91
+ }
92
+
93
+ export type ProveStep = 'check' | 'registry' | 'sign-secret' | 'sign-proof' | 'compile-prove' | 'send';
94
+
95
+ /**
96
+ * Holder: proves to a check that this wallet holds an active permit covering
97
+ * the check's activities, and sends the proof. `origin` is the site the
98
+ * holder is on (the check's verifier). Two wallet signatures: one re-derives
99
+ * the permit's secret, one authorises the proof.
100
+ */
101
+ export async function proveForCheck(opts: {
102
+ keeperUrl: string;
103
+ network: Network;
104
+ checkId: string;
105
+ permit: PermitFileRef;
106
+ wallet: { address: string; signFields(fields: string[]): Promise<string>; signMessage(message: string): Promise<{ field: string; scalar: string }> };
107
+ origin: string;
108
+ graphqlUrl?: string;
109
+ fetcher?: typeof fetch;
110
+ /** Where to keep registry copies between checks (IndexedDB in a browser); without one, each check downloads the registry. */
111
+ treeCache?: TreeCache;
112
+ onStep?: (step: ProveStep) => void;
113
+ /** Shown before the proof's field signature (as PermitsClientOptions.confirmSignature); false cancels. */
114
+ confirmSignature?: (d: PermitMessageDescription) => Promise<boolean | void> | boolean | void;
115
+ }): Promise<{ nullifier: string }> {
116
+ const step = opts.onStep ?? (() => {});
117
+ const relay = createCheckRelay(opts.keeperUrl, opts.fetcher);
118
+ step('check');
119
+ const { check } = await relay.getCheck(opts.checkId);
120
+ if (check.status !== 'waiting') throw new CheckError(check.status === 'proved' ? 'This check already has a proof.' : 'This check has expired; ask for a new one.', 'CHECK_CLOSED');
121
+ if (check.network !== opts.network) throw new CheckError(`This check is on ${check.network}.`, 'WRONG_NETWORK');
122
+ const p = opts.permit;
123
+ if (p.network && p.network !== opts.network) throw new CheckError(`Your permit is on ${p.network}; this check is on ${opts.network}.`, 'WRONG_NETWORK');
124
+ if (opts.wallet.address !== p.holderKey) throw new CheckError(`Connect the permit's holder key ${p.holderKey}.`, 'WRONG_KEY');
125
+ if (p.registry !== check.registry) {
126
+ // Issued by an earlier generation: fine when, on chain, its successors lead to the registry checked.
127
+ const [{ currentOnChain, PERMIT_LINEAGE }, { CANONICAL_PERMIT_VK_HASHES }, { DEFAULT_MINA_GRAPHQL_URLS }] = await Promise.all([
128
+ import('./successorChain.js'), import('./permitInterface.js'), import('./onChainRoot.js'),
129
+ ]);
130
+ const current = await currentOnChain({
131
+ start: p.registry, layout: PERMIT_LINEAGE, canonicalVks: Object.values(CANONICAL_PERMIT_VK_HASHES),
132
+ graphqlUrl: opts.graphqlUrl ?? DEFAULT_MINA_GRAPHQL_URLS[opts.network], fetcher: opts.fetcher,
133
+ }).then(r => r.address, () => null);
134
+ if (current !== check.registry) throw new CheckError('Your permit is from another registry than the one this check asks for.', 'WRONG_REGISTRY');
135
+ }
136
+
137
+ step('registry');
138
+ // The whole registry (a compact copy, kept between checks), so the keeper
139
+ // can't tell which permit is proved; the root and slot from a Mina node,
140
+ // so the proof is for the chain's current state.
141
+ const { readChainHead } = await import('./chainHead.js');
142
+ const [tree, head] = await Promise.all([
143
+ syncRegistryTree({ registry: check.registry, fetchTree: since => relay.getTree(check.registry, since), cache: opts.treeCache }),
144
+ readChainHead({ address: check.registry, network: opts.network, graphqlUrl: opts.graphqlUrl, fetcher: opts.fetcher }),
145
+ ]);
146
+ if (tree.root !== head.root) throw new CheckError('zkLicensing\'s copy of the registry is behind the blockchain; try again in a minute.', 'ROOT_BEHIND');
147
+ let found;
148
+ try { found = witnessFromCompactTree(tree, p.subject); }
149
+ catch (e: any) { throw new CheckError(e?.message ?? String(e), /no such permit/.test(e?.message) ? 'NOT_FOUND' : 'ROOT_MISMATCH'); }
150
+ const { witness, permit: rec } = found;
151
+ const status = permitStatusAt(rec, head.slot);
152
+ if (status !== 'active') throw new CheckError(`Your permit is ${status}.`, 'NOT_ACTIVE');
153
+ const required = BigInt(check.requiredScope);
154
+ if ((BigInt(rec.scope) & required) !== required) throw new CheckError('Your permit doesn\'t cover the activities this check asks for.', 'SCOPE');
155
+
156
+ step('sign-secret');
157
+ // Re-derived with the wallet against the permit's lineage: nothing from the keeper.
158
+ const { findHolderSecret } = await import('./holderSecret.js');
159
+ let secret: string;
160
+ try { ({ secret } = await findHolderSecret(opts.wallet, { lineage: p.lineage ?? p.registry, network: opts.network, subject: p.subject })); }
161
+ catch (e: any) { throw new CheckError(e?.message ?? String(e), 'WRONG_SECRET'); }
162
+
163
+ step('sign-proof');
164
+ const { holderProofMessage, verifierContext, proveHolder } = await import('./permitProof.js');
165
+ const context = verifierContext(opts.origin, check.nonce);
166
+ const msg = holderProofMessage(PublicKey.fromBase58(check.registry), Field(head.root), Field(context));
167
+ const fields = msg.map(f => f.toString());
168
+ if (opts.confirmSignature) {
169
+ const { fingerprint } = await import('./permitDescribe.js');
170
+ const d: PermitMessageDescription = {
171
+ kind: 'holderProof', title: `Prove to ${new URL(opts.origin).host} that you hold an active permit, without saying which`,
172
+ facts: [['Checker', opts.origin], ['Registry', check.registry], ['Network', opts.network], ['Check', check.label || check.id]],
173
+ network: opts.network, registry: check.registry, verified: true, fingerprint: fingerprint(fields),
174
+ };
175
+ if ((await opts.confirmSignature(d)) === false) throw new CheckError('Cancelled', 'CANCELLED');
176
+ }
177
+ const holderSignature = await opts.wallet.signFields(fields);
178
+
179
+ step('compile-prove');
180
+ const { proof, nullifier } = await proveHolder({
181
+ registry: check.registry, root: head.root, witness, leaf: recordToLeaf(rec), holderSecret: secret,
182
+ slot: head.slot, requiredScope: required, context, holderSignature,
183
+ });
184
+
185
+ step('send');
186
+ await relay.submitProof(check.id, proof);
187
+ return { nullifier };
188
+ }
189
+
190
+ /**
191
+ * Checker: verifies the proof sent to a check, in this process. Roots from
192
+ * the keeper's recent roots, or with `trustless` only the registry's current
193
+ * root read from a Mina node. `origin` is the site the check was opened on.
194
+ */
195
+ export async function verifyCheck(opts: {
196
+ keeperUrl: string;
197
+ network: Network;
198
+ checkId: string;
199
+ origin: string;
200
+ trustless?: boolean;
201
+ maxSlotSkew?: number;
202
+ graphqlUrl?: string;
203
+ fetcher?: typeof fetch;
204
+ }): Promise<{ status: CheckView['status']; check: CheckView; valid: boolean; nullifier: string | null; reason: string | null }> {
205
+ const relay = createCheckRelay(opts.keeperUrl, opts.fetcher);
206
+ const { check, proof } = await relay.getCheck(opts.checkId);
207
+ if (!proof) return { status: check.status, check, valid: false, nullifier: null, reason: null };
208
+ let acceptedRoots: string[];
209
+ let currentSlot: number;
210
+ if (opts.trustless) {
211
+ const { readChainHead } = await import('./chainHead.js');
212
+ const head = await readChainHead({ address: check.registry, network: opts.network, graphqlUrl: opts.graphqlUrl, fetcher: opts.fetcher });
213
+ acceptedRoots = [head.root];
214
+ currentSlot = head.slot;
215
+ } else {
216
+ const r = await relay.getRoots(check.registry);
217
+ const { freshRoots } = await import('./agent.js');
218
+ acceptedRoots = freshRoots(r.roots, r.current, r.slot, opts.maxSlotSkew ?? 10);
219
+ currentSlot = r.slot;
220
+ }
221
+ const { verifyHolderProof } = await import('./permitProof.js');
222
+ const res = await verifyHolderProof(proof, {
223
+ registry: check.registry, acceptedRoots, currentSlot, maxSlotSkew: opts.maxSlotSkew,
224
+ requiredScope: BigInt(check.requiredScope), verifier: opts.origin, nonce: check.nonce,
225
+ });
226
+ return { status: 'proved', check, ...res };
227
+ }