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,207 @@
1
+ // Copyright (c) 2025-2026 zkLicensing project developers
2
+ // SPDX-License-Identifier: BUSL-1.1
3
+
4
+ /**
5
+ * licenseTrustless.ts — a license check that doesn't trust the keeper.
6
+ *
7
+ * The keeper's `/verify` reads the chain for you; here the caller reads it:
8
+ *
9
+ * 1. From a Mina node: the contract account at each address checked (its
10
+ * license root, verification key and permissions) and the current slot.
11
+ * The contract must run a published LicensingApp release (the pinned
12
+ * CANONICAL_VK_HASHES) and be locked: state changes only by proof, no
13
+ * new verification key, no new permissions.
14
+ * 2. From the keeper: only the license's leaf fields and Merkle path
15
+ * (`/witness`). They are hashed and folded up to the root read in 1, so
16
+ * a keeper that lies, or is behind, can't make the check pass; it can
17
+ * only make it fail. An absent license is proved the same way (an
18
+ * empty leaf folds to the root).
19
+ * 3. Which addresses belong to the app: the platform-signed generation
20
+ * list (`/apps/by-address`), checked against the pinned manifest keys.
21
+ * As the keeper does, the current generation is tried first, then the
22
+ * address given, then the other retired ones (licenses refunded or
23
+ * expired before a redeploy stay there). Without a valid signed list the
24
+ * given address and its on-chain successors are checked: a retired
25
+ * contract names its successor in its state (see successorChain.ts).
26
+ *
27
+ * Whether the address is the app you think it is (the seller's website) is
28
+ * a separate check: verifyTrustAnchor. Loads o1js (for Poseidon).
29
+ */
30
+
31
+ import { AgentError, PLATFORM_MANIFEST_PUBKEYS } from './agent.js';
32
+ import { CANONICAL_VK_HASHES, GRACE_PERIOD_N, LICENSING_APP_STATE, MS_PER_SLOT_N, SLOTS_PER_DAY_N, effectiveExpirySlot } from './contractInterface.js';
33
+ import { DEFAULT_MINA_GRAPHQL_URLS } from './onChainRoot.js';
34
+ import { MinaNodeError, readZkappHead } from './chainHead.js';
35
+ import { LICENSING_LINEAGE, currentOnChain } from './successorChain.js';
36
+ import { verifyManifest } from './manifestVerify.js';
37
+
38
+ export type TrustlessLicenseVerification = {
39
+ valid: boolean;
40
+ expiresAt: string | null;
41
+ expirySlot: number;
42
+ inGracePeriod: boolean;
43
+ remainingDays: number;
44
+ reason: string | null;
45
+ trustless: true;
46
+ /** The contract address the license was checked at (another generation of the app's contract, if that's where it is). */
47
+ resolvedAddress: string;
48
+ /** LicensingApp release (generation) of that contract, from its verification key. */
49
+ generation: number | null;
50
+ /** Current slot, as the Mina node reported it. */
51
+ slot: number;
52
+ /** Whether the app's generation list came signed by the platform ("verified") or only the given address and its on-chain successors were checked. */
53
+ generations: 'verified' | 'given-address-only';
54
+ };
55
+
56
+ type ContractHead = { found: boolean; root?: string; forkSlot?: number; vkHash?: string; editState?: string; setPermissions?: string; setVerificationKey?: string; slot: number };
57
+
58
+ async function readContract(address: string, graphqlUrl: string, f: typeof fetch): Promise<ContractHead> {
59
+ let head: Awaited<ReturnType<typeof readZkappHead>>;
60
+ try {
61
+ head = await readZkappHead({ address, graphqlUrl, fetcher: f });
62
+ } catch (e: any) {
63
+ throw new AgentError(e?.message ?? String(e), 'UPSTREAM_UNAVAILABLE', e instanceof MinaNodeError ? e.status : 0, true);
64
+ }
65
+ const { account: a, slot } = head;
66
+ if (slot === undefined) throw new AgentError(`Mina node ${graphqlUrl} did not report the current slot`, 'UPSTREAM_UNAVAILABLE', 0, true);
67
+ if (!a.found) return { found: false, slot };
68
+ return {
69
+ found: true, slot,
70
+ root: a.appState?.[0],
71
+ forkSlot: Number(a.appState?.[LICENSING_APP_STATE.forkActivationSlot] ?? 0),
72
+ vkHash: a.vkHash,
73
+ editState: a.permissions?.editState, setPermissions: a.permissions?.setPermissions, setVerificationKey: a.permissions?.setVerificationKey?.auth,
74
+ };
75
+ }
76
+
77
+ /** Why a contract isn't a genuine, locked LicensingApp, or null. */
78
+ export function contractProblem(c: ContractHead): string | null {
79
+ if (!c.found || !c.root) return 'no zkApp at this address on this network';
80
+ if (!c.vkHash || !Object.values(CANONICAL_VK_HASHES).includes(c.vkHash)) return 'the contract is not a published LicensingApp release (verification key)';
81
+ if (c.editState !== 'Proof') return 'the contract state can be changed without a proof';
82
+ if (c.setVerificationKey !== 'Impossible') return 'the contract\'s verification key can be replaced';
83
+ if (c.setPermissions !== 'Impossible') return 'the contract\'s permissions can be changed';
84
+ return null;
85
+ }
86
+
87
+ /** Addresses to try, current generation first; null when no valid signed list. */
88
+ async function signedGenerations(base: string, address: string, f: typeof fetch, pinned: readonly string[], network: 'devnet' | 'mainnet'): Promise<string[] | null> {
89
+ try {
90
+ const r = await f(`${base}/apps/by-address/${encodeURIComponent(address)}`, { headers: { Accept: 'application/json' } });
91
+ if (!r.ok) return null;
92
+ // Only the signed fields count (the keeper adds unsigned envelope fields after signing).
93
+ const b = await verifyManifest(await r.json(), pinned, { network });
94
+ if (!b) return null;
95
+ const gens = (b.generations as Array<{ gen?: number; zkAppAddress?: string }>).filter(g => typeof g.zkAppAddress === 'string');
96
+ if (!gens.some(g => g.zkAppAddress === address)) return null;
97
+ const current = gens.find(g => g.gen === b.current)?.zkAppAddress;
98
+ const others = gens.map(g => g.zkAppAddress!).filter(a => a !== current && a !== address).reverse();
99
+ return [...new Set([current, address, ...others].filter((a): a is string => !!a))];
100
+ } catch {
101
+ return null;
102
+ }
103
+ }
104
+
105
+ type Witness = { root?: string; currentValue: string; isLefts: boolean[]; siblings: string[]; leaf: { expirySlot: number; deviceCap: number; deviceSlotsRoot: string; deviceSlotsFilled: number } };
106
+
107
+ /** Folds the keeper's leaf fields and path; the on-chain root decides. */
108
+ async function foldsTo(w: Witness, licenseHash: string, root: string): Promise<boolean> {
109
+ const { Field, UInt32 } = await import('o1js');
110
+ const { LicenseLeaf, leafHash } = await import('./licenseLeaf.js');
111
+ const { deserializeWitness } = await import('./licenseStore.js');
112
+ const leafValue = w.currentValue === '0'
113
+ ? Field(0)
114
+ : leafHash(new LicenseLeaf({
115
+ licenseHash: Field(licenseHash), expirySlot: UInt32.from(w.leaf.expirySlot), cap: UInt32.from(w.leaf.deviceCap),
116
+ deviceSlotsRoot: Field(w.leaf.deviceSlotsRoot), deviceSlotsFilled: UInt32.from(w.leaf.deviceSlotsFilled),
117
+ }));
118
+ const [computedRoot, key] = deserializeWitness({ isLefts: w.isLefts, siblings: w.siblings }).computeRootAndKey(leafValue);
119
+ return key.toString() === Field(licenseHash).toString() && computedRoot.toString() === root;
120
+ }
121
+
122
+ export async function verifyLicenseTrustless(opts: {
123
+ keeperUrl: string;
124
+ network: 'devnet' | 'mainnet';
125
+ zkAppAddress: string;
126
+ licenseHash: string;
127
+ /** Your own Mina node (default: a public one per network). */
128
+ graphqlUrl?: string;
129
+ fetcher?: typeof fetch;
130
+ pinnedPublicKeysBase64?: readonly string[];
131
+ /** Wait between the two witness reads when the keeper is behind the chain (ms). */
132
+ retryDelayMs?: number;
133
+ }): Promise<TrustlessLicenseVerification> {
134
+ const f = opts.fetcher ?? fetch;
135
+ const base = opts.keeperUrl.replace(/\/+$/, '');
136
+ const graphqlUrl = opts.graphqlUrl ?? DEFAULT_MINA_GRAPHQL_URLS[opts.network];
137
+ if (!/^\d{1,80}$/.test(opts.licenseHash)) throw new AgentError('licenseHash must be a decimal field element', 'INVALID_REQUEST', 0, false);
138
+
139
+ const signed = await signedGenerations(base, opts.zkAppAddress, f, opts.pinnedPublicKeysBase64 ?? PLATFORM_MANIFEST_PUBKEYS, opts.network);
140
+ // The chain names the current generation too: a retired contract stores
141
+ // its successor. One found there goes first, so a missing or stale list
142
+ // still finds it.
143
+ let current: string;
144
+ try {
145
+ current = (await currentOnChain({ start: opts.zkAppAddress, layout: LICENSING_LINEAGE, canonicalVks: Object.values(CANONICAL_VK_HASHES), graphqlUrl, fetcher: f })).address;
146
+ } catch (e: any) {
147
+ if (e instanceof MinaNodeError) throw new AgentError(e.message, 'UPSTREAM_UNAVAILABLE', e.status, true);
148
+ return result({ valid: false, reason: `Not a genuine licensing lineage: ${e?.message ?? e}` }, opts.zkAppAddress, null, 0, signed);
149
+ }
150
+ const listed = signed ?? [opts.zkAppAddress];
151
+ const addresses = current === opts.zkAppAddress ? listed : [...new Set([current, ...listed])];
152
+
153
+ let last: { address: string; generation: number | null; slot: number } | null = null;
154
+ for (const address of addresses) {
155
+ const c = await readContract(address, graphqlUrl, f);
156
+ const problem = contractProblem(c);
157
+ if (problem) {
158
+ // The address asked about must be genuine; a broken other generation is skipped.
159
+ if (address === opts.zkAppAddress) {
160
+ return result({ valid: false, reason: `Not a genuine licensing contract: ${problem}` }, address, null, c.slot, signed);
161
+ }
162
+ continue;
163
+ }
164
+ const generation = Number(Object.entries(CANONICAL_VK_HASHES).find(([, h]) => h === c.vkHash)![0]);
165
+ let w: Witness | null = null;
166
+ let ok = false;
167
+ // The keeper may be a block or two behind a fresh purchase: one more read.
168
+ for (let attempt = 0; attempt < 2 && !ok; attempt++) {
169
+ if (attempt > 0) await new Promise(r => setTimeout(r, opts.retryDelayMs ?? 3000));
170
+ const r = await f(`${base}/witness/${encodeURIComponent(address)}/${encodeURIComponent(opts.licenseHash)}?network=${opts.network}`, { headers: { Accept: 'application/json' } }).catch(() => null);
171
+ if (!r?.ok) continue;
172
+ w = await r.json().catch(() => null) as Witness | null;
173
+ if (w && Array.isArray(w.isLefts) && Array.isArray(w.siblings) && w.leaf) ok = await foldsTo(w, opts.licenseHash, c.root!);
174
+ }
175
+ if (!ok || !w) {
176
+ throw new AgentError(`The keeper's copy of ${address} does not match the chain (it may be catching up); retry, or check without trustless`, 'STATE_MISMATCH', 0, true, 10_000);
177
+ }
178
+ last = { address, generation, slot: c.slot };
179
+ if (w.currentValue === '0') continue; // proved absent here: try the next generation
180
+ const expirySlot = w.leaf.expirySlot;
181
+ // A license that lapsed during the pre-fork sales freeze counts from the fork slot.
182
+ const until = effectiveExpirySlot(expirySlot, c.forkSlot ?? 0);
183
+ const inGracePeriod = c.slot > until && c.slot <= until + GRACE_PERIOD_N;
184
+ const expired = c.slot > until + GRACE_PERIOD_N;
185
+ return result({
186
+ valid: !expired,
187
+ expirySlot,
188
+ inGracePeriod: !expired && inGracePeriod,
189
+ expiresAt: new Date(Date.now() + (until - c.slot) * MS_PER_SLOT_N).toISOString(),
190
+ remainingDays: Math.floor((until - c.slot) / SLOTS_PER_DAY_N),
191
+ reason: expired ? 'License has expired' : inGracePeriod ? 'License is in grace period — renew to avoid interruption' : null,
192
+ }, address, generation, c.slot, signed);
193
+ }
194
+ return result({ valid: false, reason: 'License not found on chain (never bought at this app, or refunded)' },
195
+ last?.address ?? opts.zkAppAddress, last?.generation ?? null, last?.slot ?? 0, signed);
196
+ }
197
+
198
+ function result(
199
+ r: Partial<TrustlessLicenseVerification> & { valid: boolean; reason: string | null },
200
+ address: string, generation: number | null, slot: number, signed: string[] | null,
201
+ ): TrustlessLicenseVerification {
202
+ return {
203
+ valid: r.valid, expiresAt: r.expiresAt ?? null, expirySlot: r.expirySlot ?? 0, inGracePeriod: r.inGracePeriod ?? false,
204
+ remainingDays: r.remainingDays ?? 0, reason: r.reason, trustless: true,
205
+ resolvedAddress: address, generation, slot, generations: signed ? 'verified' : 'given-address-only',
206
+ };
207
+ }
@@ -0,0 +1,164 @@
1
+ // Copyright (c) 2025-2026 zkLicensing project developers
2
+ // SPDX-License-Identifier: BUSL-1.1
3
+
4
+ /**
5
+ * manifestVerify.ts
6
+ *
7
+ * Browser + Node Ed25519 signature verification for platform-signed manifests
8
+ * (currently: the generation-list served at GET /apps/:appId, next: the
9
+ * sales-freeze manifest). The backend signs with the platform manifest key
10
+ * loaded via PLATFORM_MANIFEST_KEY_FILE; the SDK pins the public key against
11
+ * a caller-supplied allow-list and verifies before trusting anything.
12
+ *
13
+ * Runtime: browsers and Node ≥ 20 expose `globalThis.crypto.subtle`. The
14
+ * SDK's `engines.node` is `>=20` for this reason — Node 18 child workers
15
+ * (spawned by `node --test`) do not populate `globalThis.crypto`.
16
+ *
17
+ * Canonicalization must match `src/manifestSign.ts` byte-for-byte:
18
+ * - sorted object keys
19
+ * - no whitespace
20
+ * - arrays preserved in order
21
+ * Any divergence silently breaks verification, so keep the algorithm dead
22
+ * simple and mirrored.
23
+ */
24
+
25
+ // Canonical JSON: sorted keys, no whitespace, arrays preserved in order.
26
+ // Undefined values are skipped, mirroring JSON.stringify — otherwise the
27
+ // signed bytes would contain `"key":undefined` for optional fields left
28
+ // unset, while `res.json()` on the wire would strip them, and the signature
29
+ // would never verify.
30
+ // Mirror of src/manifestSign.ts:canonicalize. Divergence here silently
31
+ // breaks verification.
32
+ export function canonicalize(value: unknown): string {
33
+ if (value === null || typeof value !== 'object') return JSON.stringify(value);
34
+ if (Array.isArray(value)) return '[' + value.map(canonicalize).join(',') + ']';
35
+ const obj = value as Record<string, unknown>;
36
+ const keys = Object.keys(obj).filter(k => obj[k] !== undefined).sort();
37
+ return '{' + keys.map(k =>
38
+ JSON.stringify(k) + ':' + canonicalize(obj[k])
39
+ ).join(',') + '}';
40
+ }
41
+
42
+ function base64ToBytes(b64: string): Uint8Array {
43
+ // atob is present in every modern JS runtime we target (browser + Node ≥ 16).
44
+ const bin = atob(b64);
45
+ const out = new Uint8Array(bin.length);
46
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
47
+ return out;
48
+ }
49
+
50
+ function utf8Bytes(s: string): Uint8Array {
51
+ return new TextEncoder().encode(s);
52
+ }
53
+
54
+ // SPKI DER wrapper around a raw 32-byte Ed25519 public key.
55
+ // Same prefix used by src/manifestSign.ts:verifyPayload — WebCrypto needs
56
+ // SPKI to import; the wire format is raw base64 for space.
57
+ const ED25519_SPKI_PREFIX = new Uint8Array([
58
+ 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00,
59
+ ]);
60
+
61
+ function rawEd25519ToSpki(raw: Uint8Array): Uint8Array {
62
+ if (raw.length !== 32) throw new Error(`Ed25519 pubkey must be 32 bytes, got ${raw.length}`);
63
+ const out = new Uint8Array(ED25519_SPKI_PREFIX.length + raw.length);
64
+ out.set(ED25519_SPKI_PREFIX, 0);
65
+ out.set(raw, ED25519_SPKI_PREFIX.length);
66
+ return out;
67
+ }
68
+
69
+ /**
70
+ * Verify a base64 Ed25519 signature over a canonicalized payload against
71
+ * one of the pinned base64 public keys. Returns true iff at least one
72
+ * pinned key verifies. Fails closed on any error (unknown key format,
73
+ * WebCrypto rejection, missing subtle) — callers must interpret `false`
74
+ * as "do not trust."
75
+ */
76
+ export async function verifyManifestSignature(
77
+ payload: unknown,
78
+ signatureBase64: string,
79
+ pinnedPublicKeysBase64: readonly string[],
80
+ ): Promise<boolean> {
81
+ if (!pinnedPublicKeysBase64.length) return false;
82
+ const subtle = (globalThis.crypto as Crypto | undefined)?.subtle;
83
+ if (!subtle) return false;
84
+ const messageBytes = utf8Bytes(canonicalize(payload));
85
+ let signatureBytes: Uint8Array;
86
+ try { signatureBytes = base64ToBytes(signatureBase64); }
87
+ catch { return false; }
88
+ for (const pinB64 of pinnedPublicKeysBase64) {
89
+ try {
90
+ const spki = rawEd25519ToSpki(base64ToBytes(pinB64));
91
+ const key = await subtle.importKey(
92
+ 'spki',
93
+ spki as unknown as ArrayBuffer,
94
+ { name: 'Ed25519' },
95
+ false,
96
+ ['verify'],
97
+ );
98
+ const ok = await subtle.verify(
99
+ { name: 'Ed25519' },
100
+ key,
101
+ signatureBytes as unknown as ArrayBuffer,
102
+ messageBytes as unknown as ArrayBuffer,
103
+ );
104
+ if (ok) return true;
105
+ } catch {
106
+ // Try the next pin — one bad entry in the pin list must not mask
107
+ // a valid signature under a different pin.
108
+ }
109
+ }
110
+ return false;
111
+ }
112
+
113
+ /**
114
+ * The fields the platform signs in an app's generation manifest (GET
115
+ * /apps/:appId, and the vendor's /.well-known/zklicensing/<address>.json).
116
+ * The keeper adds envelope fields after signing (signature, publicKey, app,
117
+ * forkActivationSlot…), so verifiers rebuild exactly these. `network` keeps a
118
+ * devnet manifest from passing for mainnet and the other way round.
119
+ */
120
+ export type SignedManifest = {
121
+ appId: string;
122
+ network: 'devnet' | 'mainnet';
123
+ current: number;
124
+ generations: unknown[];
125
+ issuedAt: string;
126
+ };
127
+
128
+ /** The signed fields of a manifest body, or null when any is missing. */
129
+ export function signedManifestOf(body: unknown): SignedManifest | null {
130
+ if (!body || typeof body !== 'object') return null;
131
+ const b = body as Record<string, unknown>;
132
+ if (typeof b.appId !== 'string' || (b.network !== 'devnet' && b.network !== 'mainnet') || typeof b.current !== 'number'
133
+ || !Array.isArray(b.generations) || typeof b.issuedAt !== 'string') return null;
134
+ return { appId: b.appId, network: b.network, current: b.current, generations: b.generations, issuedAt: b.issuedAt };
135
+ }
136
+
137
+ /**
138
+ * Every signature a manifest carries: `signature`, and `signatures` (one per
139
+ * platform key; during a key rotation both the old and the new key sign).
140
+ */
141
+ export function manifestSignaturesOf(body: unknown): string[] {
142
+ if (!body || typeof body !== 'object') return [];
143
+ const b = body as { signature?: unknown; signatures?: unknown };
144
+ const list = Array.isArray(b.signatures) ? b.signatures.map((s: any) => s?.signature).filter((s: unknown): s is string => typeof s === 'string') : [];
145
+ return [...new Set([...(typeof b.signature === 'string' ? [b.signature] : []), ...list])];
146
+ }
147
+
148
+ /**
149
+ * The signed manifest in `body` when one of its signatures verifies against
150
+ * a pinned key (and it is for `expect.network`, when given); else null.
151
+ */
152
+ export async function verifyManifest(
153
+ body: unknown,
154
+ pinnedPublicKeysBase64: readonly string[],
155
+ expect: { network?: 'devnet' | 'mainnet' } = {},
156
+ ): Promise<SignedManifest | null> {
157
+ const payload = signedManifestOf(body);
158
+ if (!payload) return null;
159
+ if (expect.network && payload.network !== expect.network) return null;
160
+ for (const sig of manifestSignaturesOf(body)) {
161
+ if (await verifyManifestSignature(payload, sig, pinnedPublicKeysBase64)) return payload;
162
+ }
163
+ return null;
164
+ }
@@ -0,0 +1,166 @@
1
+ // Copyright (c) 2025-2026 zkLicensing project developers
2
+ // SPDX-License-Identifier: BUSL-1.1
3
+
4
+ /**
5
+ * minaPriceHistory.ts
6
+ *
7
+ * Historical MINA→fiat lookups for archive-replayed license records that were
8
+ * created before the backend had a live CoinGecko cache. Uses CoinGecko's
9
+ * free-tier `/coins/mina-protocol/history?date=DD-MM-YYYY` endpoint, which
10
+ * returns a UTC end-of-day snapshot in a single call. Rates for a past day
11
+ * never change, so results are cached permanently to a JSON file — a repeat
12
+ * bootstrap replays without another HTTP round-trip.
13
+ *
14
+ * Not used by live purchase / renew flows — those keep hitting the 5-minute
15
+ * `/simple/price` cache in keeperService.ts. Only invoked by the backfill
16
+ * pass that stamps historical `fiatSnapshot` after archive replay.
17
+ */
18
+
19
+ import fs from 'fs/promises';
20
+
21
+ // Mina consensus slot is 3 min in theory; on-chain block time targets 90s.
22
+ // The stats module uses the same constant for its monthly aggregation
23
+ // (stats.ts:25). Any drift here shows up as records stamped with the wrong
24
+ // day's rate for events near the UTC midnight boundary.
25
+ const SLOT_DURATION_MS = 90 * 1000;
26
+
27
+ // CoinGecko free-tier is ~30 req/min. A 1.5s spacing keeps us comfortably
28
+ // under that for a single-tenant backend backfill; if a very large replay ever
29
+ // hits the limit CoinGecko returns 429 and getMinaFiatForDate returns null,
30
+ // leaving that day's records unstamped (dashboards render fiat as `—`).
31
+ const THROTTLE_MS = 1500;
32
+
33
+ // Days for which CoinGecko returned no usable data. Kept in-memory only —
34
+ // they don't get written to the on-disk cache so a follow-up bootstrap after
35
+ // e.g. a CoinGecko outage will retry them. Guards against re-hitting the
36
+ // endpoint for the same missing date within one process lifetime.
37
+ const missingDates = new Set<string>();
38
+
39
+ export type HistoricalRate = {
40
+ eur: number;
41
+ usd: number;
42
+ gbp: number;
43
+ };
44
+
45
+ // Convert a Mina global slot to a UTC calendar date (YYYY-MM-DD). Uses the
46
+ // per-network (currentSlot, nowMs) pair as the anchor so caller can pass the
47
+ // clock captured at bootstrap time. Same trick stats.ts:slotToMonthKey uses.
48
+ export function slotToUtcDate(slot: number, currentSlot: number, nowMs: number): string {
49
+ const ms = nowMs - (currentSlot - slot) * SLOT_DURATION_MS;
50
+ const d = new Date(ms);
51
+ return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}-${String(d.getUTCDate()).padStart(2, '0')}`;
52
+ }
53
+
54
+ function isoToCoinGeckoDate(iso: string): string {
55
+ const [y, m, d] = iso.split('-');
56
+ return `${d}-${m}-${y}`;
57
+ }
58
+
59
+ function cachePath(): string {
60
+ return process.env.MINA_PRICE_CACHE_PATH ?? 'mina-prices.json';
61
+ }
62
+
63
+ type CacheFile = Record<string, HistoricalRate>;
64
+
65
+ let cache: CacheFile | null = null;
66
+ let cacheLoaded = false;
67
+
68
+ async function loadCache(): Promise<CacheFile> {
69
+ if (cacheLoaded && cache) return cache;
70
+ try {
71
+ const raw = await fs.readFile(cachePath(), 'utf8');
72
+ const parsed = JSON.parse(raw);
73
+ if (parsed && typeof parsed === 'object') {
74
+ cache = parsed as CacheFile;
75
+ } else {
76
+ cache = {};
77
+ }
78
+ } catch {
79
+ cache = {};
80
+ }
81
+ cacheLoaded = true;
82
+ return cache;
83
+ }
84
+
85
+ // Serialize cache writes so concurrent stamping calls don't clobber each
86
+ // other's entries. Every write flushes the whole file — tiny (< 10KB even
87
+ // for years of dates) so no need for anything fancier.
88
+ let writeChain: Promise<void> = Promise.resolve();
89
+ function withWriteLock<T>(fn: () => Promise<T>): Promise<T> {
90
+ const result = writeChain.then(fn, fn);
91
+ writeChain = result.then(() => {}, () => {});
92
+ return result;
93
+ }
94
+
95
+ async function persistCache(): Promise<void> {
96
+ if (!cache) return;
97
+ await fs.writeFile(cachePath(), JSON.stringify(cache, null, 2));
98
+ }
99
+
100
+ // Serialize the outbound HTTP calls so we respect CoinGecko's rate limit
101
+ // without a global scheduler. Concurrent callers of getMinaFiatForDate for
102
+ // distinct dates queue up 1.5s apart.
103
+ let lastFetchAt = 0;
104
+ let fetchChain: Promise<void> = Promise.resolve();
105
+ function throttled<T>(fn: () => Promise<T>): Promise<T> {
106
+ const next = fetchChain.then(async () => {
107
+ const wait = Math.max(0, THROTTLE_MS - (Date.now() - lastFetchAt));
108
+ if (wait > 0) await new Promise(r => setTimeout(r, wait));
109
+ lastFetchAt = Date.now();
110
+ });
111
+ fetchChain = next.then(() => {}, () => {});
112
+ return next.then(fn);
113
+ }
114
+
115
+ // Return { eur, usd, gbp } for the given UTC calendar date, or null if the
116
+ // endpoint fails or omits any of the three currencies. Cached results are
117
+ // returned immediately. Fail-soft: no exceptions, just null → caller leaves
118
+ // the record unstamped.
119
+ export async function getMinaFiatForDate(iso: string): Promise<HistoricalRate | null> {
120
+ const c = await loadCache();
121
+ if (c[iso]) return c[iso];
122
+ if (missingDates.has(iso)) return null;
123
+ const cgDate = isoToCoinGeckoDate(iso);
124
+ const url = `https://api.coingecko.com/api/v3/coins/mina-protocol/history?date=${cgDate}&localization=false`;
125
+ try {
126
+ const r = await throttled(() => fetch(url));
127
+ if (!r.ok) {
128
+ missingDates.add(iso);
129
+ return null;
130
+ }
131
+ const d = await r.json() as { market_data?: { current_price?: { eur?: number; usd?: number; gbp?: number } } };
132
+ const px = d?.market_data?.current_price;
133
+ const eur = px?.eur;
134
+ const usd = px?.usd;
135
+ const gbp = px?.gbp;
136
+ const eurOk = typeof eur === 'number' && Number.isFinite(eur) && eur > 0;
137
+ const usdOk = typeof usd === 'number' && Number.isFinite(usd) && usd > 0;
138
+ const gbpOk = typeof gbp === 'number' && Number.isFinite(gbp) && gbp > 0;
139
+ if (!eurOk || !usdOk || !gbpOk) {
140
+ missingDates.add(iso);
141
+ return null;
142
+ }
143
+ const rate: HistoricalRate = { eur: eur!, usd: usd!, gbp: gbp! };
144
+ await withWriteLock(async () => {
145
+ const cc = await loadCache();
146
+ cc[iso] = rate;
147
+ await persistCache();
148
+ });
149
+ return rate;
150
+ } catch {
151
+ missingDates.add(iso);
152
+ return null;
153
+ }
154
+ }
155
+
156
+ // Test-only: reset in-process state so unit tests can exercise fresh cache
157
+ // misses without cross-contamination. Not exported through the SDK's public
158
+ // entry points.
159
+ export function _resetForTests(): void {
160
+ cache = null;
161
+ cacheLoaded = false;
162
+ missingDates.clear();
163
+ lastFetchAt = 0;
164
+ fetchChain = Promise.resolve();
165
+ writeChain = Promise.resolve();
166
+ }
@@ -0,0 +1,90 @@
1
+ // Copyright (c) 2025-2026 zkLicensing project developers
2
+ // SPDX-License-Identifier: BUSL-1.1
3
+
4
+ /**
5
+ * onChainRoot.ts
6
+ *
7
+ * Non-custodial trust anchor for `verifyOffline`. Reads the live
8
+ * `licensesRoot` (`zkapp.appState[0]`) directly from Mina — no
9
+ * licensing-service round-trip, no trust assumption about whoever
10
+ * served the witness / license record.
11
+ *
12
+ * Pair it with `verifyOffline` to close the trust loop:
13
+ *
14
+ * ```ts
15
+ * import { fetchOnChainLicensesRoot } from 'zklicensing/on-chain-root';
16
+ * import { verifyOffline } from 'zklicensing/verify-offline';
17
+ *
18
+ * // 1. Ask the licensing service for a witness + slot snapshot.
19
+ * const snap = await fetch(`${service}/witness?licenseHash=${h}&zkAppAddress=${a}`).then(r => r.json());
20
+ *
21
+ * // 2. Fetch the on-chain root INDEPENDENTLY, from Mina.
22
+ * const onChainRoot = await fetchOnChainLicensesRoot({ zkAppAddress: a, network: 'devnet' });
23
+ *
24
+ * // 3. Cross-check — if they disagree, the service is stale/wrong; bail.
25
+ * if (snap.root !== onChainRoot) throw new Error('service root disagrees with chain');
26
+ *
27
+ * // 4. Fold-verify the snapshot against the on-chain root.
28
+ * const res = verifyOffline({ licensesRoot: onChainRoot, ...snap });
29
+ * ```
30
+ *
31
+ * The read goes to the node's GraphQL API directly: no o1js is needed
32
+ * and o1js's global Mina instance is never touched, so it is safe in
33
+ * long-lived hosts that talk to several networks.
34
+ */
35
+
36
+ // Default read-only public Mina GraphQL endpoints. Devnet points at
37
+ // o1Labs' Mesa cluster (the current stable devnet node published at
38
+ // devnet-plain-1.gcp.o1test.net); mainnet uses Minascan's public node.
39
+ // Neither requires auth or a rate-limit key. Vendors who prefer their
40
+ // own Mina node can override via `graphqlUrl` on the input.
41
+ export const DEFAULT_MINA_GRAPHQL_URLS = {
42
+ mainnet: 'https://api.minascan.io/node/mainnet/v1/graphql',
43
+ devnet: 'https://devnet-plain-1.gcp.o1test.net/graphql',
44
+ } as const;
45
+
46
+ export type NetworkName = 'mainnet' | 'devnet';
47
+
48
+ export type FetchOnChainLicensesRootInput = {
49
+ /** B62q… address of the deployed LicensingApp for this app. */
50
+ zkAppAddress: string;
51
+ } & (
52
+ | { network: NetworkName; graphqlUrl?: string }
53
+ | { graphqlUrl: string; network?: NetworkName }
54
+ );
55
+
56
+ /**
57
+ * Read the live `licensesRoot` from the deployed LicensingApp's
58
+ * on-chain state. Returns the field value as a decimal string,
59
+ * matching the shape `verifyOffline` and the licensing service both
60
+ * consume.
61
+ *
62
+ * Throws on any of:
63
+ * - `fetchAccount` returned an error (network down, wrong endpoint,
64
+ * account doesn't exist on this chain);
65
+ * - account exists but has no zkApp state (not a zkApp address, or
66
+ * contract deployed but `initialize()` never ran).
67
+ */
68
+ export async function fetchOnChainLicensesRoot(
69
+ input: FetchOnChainLicensesRootInput,
70
+ ): Promise<string> {
71
+ const { zkAppAddress } = input;
72
+ const network: NetworkName = input.network ?? 'devnet';
73
+ const graphqlUrl = input.graphqlUrl ?? DEFAULT_MINA_GRAPHQL_URLS[network];
74
+
75
+ // Over the node's GraphQL API: no o1js, no global Mina instance.
76
+ const { readZkappAccount } = await import('./chainHead.js');
77
+ let account;
78
+ try {
79
+ account = await readZkappAccount({ address: zkAppAddress, graphqlUrl });
80
+ } catch (e: any) {
81
+ throw new Error(`fetchAccount(${zkAppAddress}) via ${graphqlUrl} failed: ${e?.message ?? e}`);
82
+ }
83
+ const root = account.found ? account.appState?.[0] : undefined;
84
+ if (!root) {
85
+ throw new Error(
86
+ `zkApp ${zkAppAddress} has no appState — is the LicensingApp deployed and initialized on ${network}?`,
87
+ );
88
+ }
89
+ return root;
90
+ }
@@ -0,0 +1,31 @@
1
+ // Copyright (c) 2025-2026 zkLicensing project developers
2
+ // SPDX-License-Identifier: BUSL-1.1
3
+
4
+ /**
5
+ * ownershipLicenseHash.ts
6
+ *
7
+ * Field-side binding between the buyer's Pallas Schnorr pubkey and the
8
+ * `licenseHash` Field stored on-chain. The buy circuit and the buy client
9
+ * both compute this from the pubkey and assert equality — that mutual
10
+ * assertion is what proves knowledge of the passphrase without ever
11
+ * showing the passphrase to the network.
12
+ *
13
+ * A Pallas `PublicKey` is `(x: Field, isOdd: Bool)` — the two Fields that
14
+ * uniquely identify the compressed point. Poseidon-hashing them yields a
15
+ * single Field that matches the on-chain shape of every other `licenseHash`
16
+ * used by LicensingApp (which treats licenseHash as an opaque Field
17
+ * commitment, not a specific preimage).
18
+ */
19
+
20
+ import { Poseidon, PublicKey } from 'o1js';
21
+
22
+ /**
23
+ * Derive the Field-side `licenseHash` from a Pallas Schnorr pubkey. The
24
+ * hash is domain-free because the whole platform treats `licenseHash` as
25
+ * an opaque commitment — the on-chain contract never re-derives it from
26
+ * a preimage, so there's no cross-preimage collision surface to guard
27
+ * against here.
28
+ */
29
+ export function licenseHashFromPubkey(publicKey: PublicKey): string {
30
+ return Poseidon.hash([publicKey.x, publicKey.isOdd.toField()]).toString();
31
+ }