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,550 @@
1
+ // Copyright (c) 2025-2026 zkLicensing project developers
2
+ // SPDX-License-Identifier: BUSL-1.1
3
+
4
+ /**
5
+ * trustAnchorVerify.ts
6
+ *
7
+ * End-to-end trust-anchor verification for a single zkLicensing app.
8
+ *
9
+ * Given a vendor origin + a zkApp address + one or more pinned platform
10
+ * manifest public keys, this module performs the full "does this vendor
11
+ * page point at a real zkLicensing deployment for this vendor?" pipeline
12
+ * that the trust-anchor page walks a human buyer through — mechanically,
13
+ * with structured findings suitable for CI, monitoring, or a
14
+ * wallet/extension consumer.
15
+ *
16
+ * The verifier is address-keyed: buyers see zkApp addresses in their
17
+ * wallet popup, not opaque platform appIds, so the primary lookup is by
18
+ * address. The signed manifest served at
19
+ * `<origin>/.well-known/zklicensing/<zkAppAddress>.json` carries the
20
+ * appId inside its signed payload, so callers never need to know the
21
+ * appId out-of-band.
22
+ *
23
+ * Pipeline (each step yields a discrete Finding — no boolean collapse):
24
+ *
25
+ * 1. manifest_fetch — GET https://<origin>/.well-known/zklicensing/<address>.json
26
+ * 2. manifest_signature — Ed25519 verify against caller-provided pinned keys
27
+ * (any of the manifest's signatures: during a platform
28
+ * key rotation it carries the old and the new key's)
29
+ * 2b. manifest_network — signed for the network being checked (minaNetworkId)
30
+ * 3. manifest_address_in_generations — opts.address appears in the signed generations[]
31
+ * 3b. manifest_matches_backend — (opt-in, opts.crossCheck) vendor payload equals
32
+ * zklicensing.com/apps/by-address/<address> after
33
+ * independent signature-verify against same pins
34
+ * 4. chain_account_exists — fetchAccount succeeds at the address
35
+ * 5. chain_zkapp_uri_match — on-chain zkappUri equals opts.origin
36
+ * 6. chain_vk_hash_pinned — on-chain verification-key hash equals CANONICAL_VK_HASHES[generation]
37
+ * 7. chain_set_zkapp_uri_impossible — permissions.setZkappUri === Permissions.impossible()
38
+ * 8. chain_set_permissions_impossible — permissions.setPermissions === Permissions.impossible() (meta-freeze)
39
+ * 9. chain_set_verification_key_locked — permissions.setVerificationKey.auth === Permissions.impossible()
40
+ * 10. chain_send_proof_only — permissions.send === Permissions.proof()
41
+ * 11. chain_edit_state_proof_only — permissions.editState === Permissions.proof()
42
+ *
43
+ * Steps 5 + 7 close the URI-binding loop: (5) proves the contract claims
44
+ * this origin, (7) proves nobody can rewrite that claim. Steps 8–11 verify
45
+ * the rest of LicensingApp.init()'s permission matrix — catching the case
46
+ * where a deployer used the correct VK but skipped/altered init(), leaving
47
+ * default `signature()` permissions that would still let the deploy key
48
+ * drain escrow, rewrite state, or swap the circuit.
49
+ *
50
+ * Chain reads go to the Mina node's GraphQL API directly (chainHead.ts):
51
+ * no o1js is needed, and o1js's global Mina instance is never switched, so
52
+ * long-lived services can run the check concurrently.
53
+ *
54
+ * The pinned pubkey list is caller-supplied — the SDK ships NO baked-in
55
+ * pin. Source it from https://zklicensing.com/trustanchor.html (published
56
+ * next to each generation's VK hash). See the SDK README for details.
57
+ */
58
+
59
+ import { canonicalize, manifestSignaturesOf, signedManifestOf, verifyManifestSignature } from './manifestVerify.js';
60
+
61
+ export type Finding = {
62
+ /** Machine-readable check identifier, stable across releases. */
63
+ name: string;
64
+ /** True iff the check passed. */
65
+ ok: boolean;
66
+ /** Human-readable one-line summary or reason. */
67
+ detail: string;
68
+ };
69
+
70
+ export type TrustAnchorReport = {
71
+ /** True iff every finding is ok. */
72
+ ok: boolean;
73
+ origin: string;
74
+ /** Address that was checked. Echoed from opts.address. */
75
+ address: string;
76
+ /** appId as declared in the signed manifest payload. Undefined if the fetch/signature failed. */
77
+ appId?: string;
78
+ /** Generation index the address maps to in the signed manifest (1-based). */
79
+ generation?: number;
80
+ findings: Finding[];
81
+ };
82
+
83
+ export interface VerifyTrustAnchorOpts {
84
+ /** Vendor origin, e.g. `https://store.example.com`. HTTPS required. */
85
+ origin: string;
86
+ /** zkApp address (base58 PublicKey) — the manifest lives at `/.well-known/zklicensing/<address>.json`. */
87
+ address: string;
88
+ /** Platform manifest Ed25519 public keys (base64). Comma-list from `VITE_MANIFEST_PUBKEYS`. */
89
+ pinnedPublicKeysBase64: readonly string[];
90
+ /** Mina GraphQL endpoint for on-chain reads. Ignored when `skipChainChecks` is true. */
91
+ minaGraphqlUrl?: string;
92
+ /** Optional Mina networkId — 'mainnet' or 'testnet'. Defaults to 'mainnet'. */
93
+ minaNetworkId?: 'mainnet' | 'testnet' | 'devnet';
94
+ /** Fetch timeout in ms for the manifest fetch. Default 15000. */
95
+ fetchTimeoutMs?: number;
96
+ /** When true, skips all `chain_*` checks. Useful for manifest-only audits. */
97
+ skipChainChecks?: boolean;
98
+ /**
99
+ * When true, runs an extra `manifest_matches_backend` finding: fetch the
100
+ * same signed payload from `https://zklicensing.com/apps/by-address/{address}`,
101
+ * verify it against the same pinned keys, then byte-compare canonicalized
102
+ * payloads (minus `signature`, `publicKey`, `issuedAt`, `app`). Catches a
103
+ * vendor serving a stale/rolled-back manifest even when its own signature
104
+ * is still valid. Not on by default — adds a second network fetch and
105
+ * couples the verifier to zklicensing.com being reachable.
106
+ */
107
+ crossCheck?: boolean;
108
+ }
109
+
110
+ // The zkLicensing backend is mounted under /api/keeper on zklicensing.com —
111
+ // the bare /apps/... paths return 404 (they're not the app route, they're an
112
+ // unrelated marketing route). This is the concrete URL prefix, not just an
113
+ // origin. The `/api/keeper` path is a stable public route and must not be
114
+ // renamed here.
115
+ const BACKEND_URL_PREFIX = 'https://zklicensing.com/api/keeper';
116
+
117
+ const DEFAULT_FETCH_TIMEOUT_MS = 15000;
118
+
119
+ function normalizeOrigin(origin: string): string {
120
+ return origin.replace(/\/+$/, '');
121
+ }
122
+
123
+ // Strip trailing slash / path from a URL so we can compare against zkappUri,
124
+ // which vendors typically set to their bare origin.
125
+ function urlOrigin(url: string): string | null {
126
+ try {
127
+ const u = new URL(url);
128
+ return `${u.protocol}//${u.host}`;
129
+ } catch {
130
+ return null;
131
+ }
132
+ }
133
+
134
+ type Generation = {
135
+ gen: number;
136
+ zkAppAddress: string;
137
+ status: 'retired' | 'live';
138
+ frozenRoot?: string;
139
+ deployedAt?: string;
140
+ retiredAt?: string;
141
+ // On-chain permissions.setVerificationKey.txnVersion recorded by the backend
142
+ // at manifest-assembly time. Optional for backwards compatibility with
143
+ // older manifests written before the field existed.
144
+ deployTxnVersion?: number;
145
+ };
146
+
147
+ type ManifestPayload = {
148
+ appId: string;
149
+ network: 'devnet' | 'mainnet';
150
+ current: number;
151
+ generations: Generation[];
152
+ issuedAt: string;
153
+ };
154
+
155
+ async function fetchManifest(
156
+ origin: string,
157
+ address: string,
158
+ timeoutMs: number,
159
+ ): Promise<Finding & { body?: unknown }> {
160
+ const originOk = origin.startsWith('https://');
161
+ if (!originOk) {
162
+ return { name: 'manifest_fetch', ok: false, detail: 'origin must be https://' };
163
+ }
164
+ let manifestUrl: string;
165
+ try {
166
+ manifestUrl = new URL(`/.well-known/zklicensing/${encodeURIComponent(address)}.json`, origin).href;
167
+ } catch {
168
+ return { name: 'manifest_fetch', ok: false, detail: 'origin is not a valid URL' };
169
+ }
170
+ const controller = new AbortController();
171
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
172
+ try {
173
+ const resp = await fetch(manifestUrl, { signal: controller.signal, redirect: 'follow' });
174
+ if (!resp.ok) {
175
+ return { name: 'manifest_fetch', ok: false, detail: `HTTP ${resp.status} from ${manifestUrl}` };
176
+ }
177
+ const text = await resp.text();
178
+ let parsed: unknown;
179
+ try { parsed = JSON.parse(text); }
180
+ catch { return { name: 'manifest_fetch', ok: false, detail: 'response is not valid JSON' }; }
181
+ return { name: 'manifest_fetch', ok: true, detail: `fetched ${manifestUrl}`, body: parsed };
182
+ } catch (err: any) {
183
+ return { name: 'manifest_fetch', ok: false, detail: `fetch failed: ${err?.message ?? String(err)}` };
184
+ } finally {
185
+ clearTimeout(timer);
186
+ }
187
+ }
188
+
189
+ // Split a raw manifest JSON into { signature, payload }. The backend writes the
190
+ // payload fields at the top level plus { signature, publicKey } — we ignore
191
+ // publicKey (never trust the payload's self-declared key; only the
192
+ // caller-pinned list is authoritative). The same signed bytes are served at
193
+ // every /.well-known/zklicensing/<address>.json — one file per generation
194
+ // address, all carrying the same appId in the payload.
195
+ //
196
+ // We whitelist the signed fields (appId, current, generations, issuedAt)
197
+ // rather than blacklist non-signed ones. The backend appends advisory
198
+ // fields (e.g. `forkActivationSlot`) to the response AFTER signing, so a
199
+ // blacklist misses them and canonicalize() then hashes bytes the signer
200
+ // never saw — the signature never verifies.
201
+ function splitManifest(raw: unknown): { signatures: string[]; payload: ManifestPayload | null } {
202
+ const payload = signedManifestOf(raw) as ManifestPayload | null;
203
+ return { signatures: manifestSignaturesOf(raw), payload };
204
+ }
205
+
206
+ /** True when one of `signatures` verifies over `payload` against a pinned key. */
207
+ async function anySignatureVerifies(payload: unknown, signatures: readonly string[], pinned: readonly string[]): Promise<boolean> {
208
+ for (const sig of signatures) if (await verifyManifestSignature(payload, sig, pinned)) return true;
209
+ return false;
210
+ }
211
+
212
+ // Optional cross-check against the zkLicensing backend's own copy of the
213
+ // signed generation list. Fetches `https://zklicensing.com/apps/by-address/
214
+ // {address}`, verifies its signature against the same caller-pinned keys (so
215
+ // a compromised backend URL cannot lie), strips signature/publicKey/issuedAt/
216
+ // app/network, and byte-compares canonicalized payloads against the vendor's
217
+ // `.well-known` copy. `issuedAt` is stripped because the backend re-stamps on
218
+ // every call; `app` and `network` are stripped because they are unsigned
219
+ // envelope fields the by-address endpoint tacks on.
220
+ async function checkBackendMatch(args: {
221
+ address: string;
222
+ vendorPayload: ManifestPayload;
223
+ pinnedPublicKeysBase64: readonly string[];
224
+ timeoutMs: number;
225
+ }): Promise<Finding> {
226
+ const name = 'manifest_matches_backend';
227
+ const url = `${BACKEND_URL_PREFIX}/apps/by-address/${encodeURIComponent(args.address)}`;
228
+ const controller = new AbortController();
229
+ const timer = setTimeout(() => controller.abort(), args.timeoutMs);
230
+ let body: any;
231
+ try {
232
+ const resp = await fetch(url, { signal: controller.signal, redirect: 'follow' });
233
+ if (!resp.ok) return { name, ok: false, detail: `backend HTTP ${resp.status} from ${url}` };
234
+ body = await resp.json();
235
+ } catch (err: any) {
236
+ return { name, ok: false, detail: `backend fetch failed: ${err?.message ?? String(err)}` };
237
+ } finally {
238
+ clearTimeout(timer);
239
+ }
240
+ if (!body || typeof body !== 'object') {
241
+ return { name, ok: false, detail: 'backend response is not a JSON object' };
242
+ }
243
+ // Only the signed fields (same reason as splitManifest above): the backend
244
+ // appends envelope fields (publicKey, app, forkActivationSlot) after signing.
245
+ const backendSignatures = manifestSignaturesOf(body);
246
+ if (!backendSignatures.length) {
247
+ return { name, ok: false, detail: 'backend response missing signature' };
248
+ }
249
+ const backendPayload = signedManifestOf(body);
250
+ if (!backendPayload) {
251
+ return { name, ok: false, detail: 'backend response missing signed fields' };
252
+ }
253
+ if (!await anySignatureVerifies(backendPayload, backendSignatures, args.pinnedPublicKeysBase64)) {
254
+ return { name, ok: false, detail: 'backend response signature does not verify against pinned keys' };
255
+ }
256
+ const { issuedAt: _kIA, ...kRest } = backendPayload as Record<string, unknown>;
257
+ const { issuedAt: _vIA, ...vRest } = args.vendorPayload as unknown as Record<string, unknown>;
258
+ if (canonicalize(kRest) !== canonicalize(vRest)) {
259
+ return { name, ok: false, detail: 'vendor manifest content diverges from backend record (stale or drifted)' };
260
+ }
261
+ return { name, ok: true, detail: `vendor payload matches backend /apps/by-address/${args.address}` };
262
+ }
263
+
264
+ type ChainState = {
265
+ zkappUri?: string;
266
+ vkHash?: string;
267
+ // Each flag mirrors one line of LicensingApp.init()'s permissions matrix.
268
+ // undefined means the field could not be read from chain.
269
+ setZkappUriIsImpossible?: boolean;
270
+ setPermissionsIsImpossible?: boolean;
271
+ // setVerificationKey uses VerificationKey.impossibleDuringCurrentVersion(),
272
+ // which stores {auth: Impossible, txnVersion: currentChainTxnVersion} on the
273
+ // account. After a Mina hardfork the chain's current txnVersion advances
274
+ // past the stored value and the effective enforcement falls back to
275
+ // signature() by design — but the deploy key was discarded at deploy time
276
+ // (see LicensingApp.ts:546-547), so no one can sign the fallback update.
277
+ //
278
+ // We read both the stored auth string AND the stored txnVersion so the
279
+ // verifier can bind on-chain state to the manifest-recorded deployTxnVersion.
280
+ setVerificationKeyAuth?: string;
281
+ setVerificationKeyTxnVersion?: number;
282
+ sendIsProof?: boolean;
283
+ editStateIsProof?: boolean;
284
+ accountFound: boolean;
285
+ fetchError?: string;
286
+ };
287
+
288
+ // Converts an o1js-decoded Permission (three booleans) back to its canonical
289
+ // enum string. Mirrors the switch inside o1js's own AuthRequired.toJSON — kept
290
+ // inline because o1js doesn't publicly export it.
291
+ export function permToString(p: any): string | undefined {
292
+ const b = (v: any) => (typeof v?.toBoolean === 'function' ? v.toBoolean() : Boolean(v)) ? 1 : 0;
293
+ const key = `${b(p?.constant)}${b(p?.signatureNecessary)}${b(p?.signatureSufficient)}`;
294
+ switch (key) {
295
+ case '110': return 'Impossible';
296
+ case '101': return 'None';
297
+ case '000': return 'Proof';
298
+ case '011': return 'Signature';
299
+ case '001': return 'Either';
300
+ default: return undefined;
301
+ }
302
+ }
303
+
304
+ // Reads the on-chain facts needed to close the trust loop for a single
305
+ // zkApp address, over the node's GraphQL API (no o1js, and no switch of
306
+ // o1js's global Mina instance, so long-lived hosts can call it safely).
307
+ async function readChainState(
308
+ address: string,
309
+ minaGraphqlUrl: string,
310
+ _minaNetworkId: 'mainnet' | 'testnet' | 'devnet',
311
+ fetcher?: typeof fetch,
312
+ ): Promise<ChainState> {
313
+ try {
314
+ const { readZkappAccount } = await import('./chainHead.js');
315
+ const a = await readZkappAccount({ address, graphqlUrl: minaGraphqlUrl, fetcher });
316
+ if (!a.found) return { accountFound: false, fetchError: `no account at ${address} via ${minaGraphqlUrl}` };
317
+ const p = a.permissions ?? {};
318
+ return {
319
+ accountFound: true,
320
+ zkappUri: a.zkappUri,
321
+ vkHash: a.vkHash,
322
+ setZkappUriIsImpossible: p.setZkappUri === 'Impossible',
323
+ setPermissionsIsImpossible: p.setPermissions === 'Impossible',
324
+ setVerificationKeyAuth: p.setVerificationKey?.auth,
325
+ setVerificationKeyTxnVersion: p.setVerificationKey?.txnVersion,
326
+ sendIsProof: p.send === 'Proof',
327
+ editStateIsProof: p.editState === 'Proof',
328
+ };
329
+ } catch (err: any) {
330
+ return { accountFound: false, fetchError: err?.message ?? String(err) };
331
+ }
332
+ }
333
+
334
+ async function loadCanonicalVkHashes(): Promise<Readonly<Record<number, string>> | null> {
335
+ try {
336
+ const m = await import('./contractInterface.js');
337
+ return m.CANONICAL_VK_HASHES ?? null;
338
+ } catch {
339
+ return null;
340
+ }
341
+ }
342
+
343
+ export async function verifyTrustAnchor(opts: VerifyTrustAnchorOpts): Promise<TrustAnchorReport> {
344
+ const origin = normalizeOrigin(opts.origin);
345
+ const address = opts.address;
346
+ const findings: Finding[] = [];
347
+ const timeoutMs = opts.fetchTimeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
348
+
349
+ // 1. manifest_fetch
350
+ const fetchResult = await fetchManifest(origin, address, timeoutMs);
351
+ findings.push({ name: fetchResult.name, ok: fetchResult.ok, detail: fetchResult.detail });
352
+ if (!fetchResult.ok) {
353
+ return { ok: false, origin, address, findings };
354
+ }
355
+ const { signatures, payload } = splitManifest(fetchResult.body);
356
+
357
+ // 2. manifest_signature
358
+ if (!signatures.length || !payload) {
359
+ findings.push({ name: 'manifest_signature', ok: false, detail: 'manifest missing signature or payload fields (appId, network, current, generations, issuedAt)' });
360
+ return { ok: false, origin, address, findings };
361
+ }
362
+ const sigOk = await anySignatureVerifies(payload, signatures, opts.pinnedPublicKeysBase64);
363
+ findings.push({
364
+ name: 'manifest_signature',
365
+ ok: sigOk,
366
+ detail: sigOk
367
+ ? `Ed25519 verified against pinned key (1 of ${opts.pinnedPublicKeysBase64.length})`
368
+ : 'signature does not verify against any pinned key',
369
+ });
370
+ if (!sigOk) {
371
+ return { ok: false, origin, address, findings };
372
+ }
373
+
374
+ // appId is trusted only after signature verifies — it comes from the signed
375
+ // payload, not the caller.
376
+ const appId = payload.appId;
377
+
378
+ // 2b. manifest_network: signed for the network being checked (a devnet
379
+ // manifest never vouches for a mainnet contract, nor the other way round).
380
+ const expectedNetwork = (opts.minaNetworkId ?? 'mainnet') === 'mainnet' ? 'mainnet' : 'devnet';
381
+ findings.push({
382
+ name: 'manifest_network',
383
+ ok: payload.network === expectedNetwork,
384
+ detail: payload.network === expectedNetwork ? `signed for ${payload.network}` : `signed for ${payload.network}, checking ${expectedNetwork}`,
385
+ });
386
+ if (payload.network !== expectedNetwork) {
387
+ return { ok: false, origin, address, findings };
388
+ }
389
+
390
+ // 3. manifest_address_in_generations
391
+ const entry = payload.generations.find((g) => g.zkAppAddress === address);
392
+ findings.push({
393
+ name: 'manifest_address_in_generations',
394
+ ok: !!entry,
395
+ detail: entry
396
+ ? `address maps to gen ${entry.gen} (${entry.status})`
397
+ : `address ${address} not in manifest generations for app "${appId}"`,
398
+ });
399
+ const generation = entry?.gen;
400
+
401
+ // 3b. manifest_matches_backend (opt-in)
402
+ if (opts.crossCheck) {
403
+ findings.push(await checkBackendMatch({
404
+ address,
405
+ vendorPayload: payload,
406
+ pinnedPublicKeysBase64: opts.pinnedPublicKeysBase64,
407
+ timeoutMs,
408
+ }));
409
+ }
410
+
411
+ if (opts.skipChainChecks) {
412
+ const allOk = findings.every((f) => f.ok);
413
+ return { ok: allOk, origin, appId, address, generation, findings };
414
+ }
415
+
416
+ if (!opts.minaGraphqlUrl) {
417
+ findings.push({ name: 'chain_account_exists', ok: false, detail: 'minaGraphqlUrl is required for chain checks' });
418
+ return { ok: false, origin, appId, address, generation, findings };
419
+ }
420
+
421
+ // 4-11: chain reads
422
+ const state = await readChainState(address, opts.minaGraphqlUrl, opts.minaNetworkId ?? 'mainnet');
423
+ findings.push({
424
+ name: 'chain_account_exists',
425
+ ok: state.accountFound,
426
+ detail: state.accountFound ? `account ${address.slice(0, 12)}… found` : (state.fetchError ?? 'account not found'),
427
+ });
428
+ if (!state.accountFound) {
429
+ return { ok: false, origin, appId, address, generation, findings };
430
+ }
431
+
432
+ const onChainOrigin = state.zkappUri ? urlOrigin(state.zkappUri) : null;
433
+ const originClean = urlOrigin(origin);
434
+ const uriOk = !!state.zkappUri && !!onChainOrigin && !!originClean && onChainOrigin === originClean;
435
+ findings.push({
436
+ name: 'chain_zkapp_uri_match',
437
+ ok: uriOk,
438
+ detail: state.zkappUri
439
+ ? (uriOk ? `zkappUri "${state.zkappUri}" origin matches` : `zkappUri "${state.zkappUri}" origin ≠ "${originClean ?? origin}"`)
440
+ : 'zkappUri is empty on chain',
441
+ });
442
+
443
+ // Pinned by circuit: the on-chain VK must be one of the published circuit
444
+ // hashes. The manifest's `gen` counts this app's redeploys, which is not
445
+ // the circuit generation (a redeploy can keep the circuit, and an app
446
+ // deployed after a circuit bump starts at gen 1 on the newer circuit).
447
+ const canonicalVks = await loadCanonicalVkHashes();
448
+ const matchedCircuit = state.vkHash && canonicalVks
449
+ ? Object.entries(canonicalVks).find(([, h]) => h === state.vkHash)?.[0]
450
+ : undefined;
451
+ const vkOk = matchedCircuit !== undefined;
452
+ findings.push({
453
+ name: 'chain_vk_hash_pinned',
454
+ ok: vkOk,
455
+ detail: vkOk
456
+ ? `on-chain vkHash matches CANONICAL_VK_HASHES[${matchedCircuit}] (app gen ${generation ?? '?'})`
457
+ : !canonicalVks
458
+ ? 'CANONICAL_VK_HASHES unavailable'
459
+ : `on-chain vkHash "${state.vkHash?.slice(0, 16) ?? 'missing'}…" is not a published circuit hash`,
460
+ });
461
+
462
+ findings.push({
463
+ name: 'chain_set_zkapp_uri_impossible',
464
+ ok: !!state.setZkappUriIsImpossible,
465
+ detail: state.setZkappUriIsImpossible
466
+ ? 'permissions.setZkappUri = impossible (zkappUri is frozen)'
467
+ : 'permissions.setZkappUri ≠ impossible — vendor could rewrite the URI',
468
+ });
469
+
470
+ // The meta-freeze: without this, every other permission lock below is
471
+ // reversible with a single signature. Priority #1 among the extras.
472
+ findings.push({
473
+ name: 'chain_set_permissions_impossible',
474
+ ok: !!state.setPermissionsIsImpossible,
475
+ detail: state.setPermissionsIsImpossible
476
+ ? 'permissions.setPermissions = impossible (permission matrix is meta-frozen)'
477
+ : 'permissions.setPermissions ≠ impossible — every other lock could be relaxed later',
478
+ });
479
+
480
+ // setVerificationKey lock, with hardfork-aware softening.
481
+ //
482
+ // The contract deploys with VerificationKey.impossibleDuringCurrentVersion(),
483
+ // which stores {auth: Impossible, txnVersion: <chain's txnVersion at deploy
484
+ // time>}. After a Mina hardfork the chain's current txnVersion advances
485
+ // past the stored value and the effective enforcement falls back to
486
+ // signature() — but the deploy key was discarded (LicensingApp.ts:546-547),
487
+ // so no one can sign the fallback update.
488
+ //
489
+ // The backend records the deploy-time txnVersion in the signed manifest as
490
+ // deployTxnVersion. We accept two states as OK, provided the on-chain
491
+ // stored txnVersion still matches the manifest-recorded value (which proves
492
+ // no one has re-set the permission since deploy):
493
+ //
494
+ // - auth = Impossible → normal, pre-hardfork state
495
+ // - auth = Signature → post-hardfork fallback (see above)
496
+ //
497
+ // Any mismatch — different txnVersion, or auth is some other string, or
498
+ // deployTxnVersion missing from an old manifest — falls back to the strict
499
+ // rule (auth === 'Impossible').
500
+ const onChainAuth = state.setVerificationKeyAuth;
501
+ const onChainTxnVersion = state.setVerificationKeyTxnVersion;
502
+ const manifestDeployTxnVersion = entry?.deployTxnVersion;
503
+ let vkLockOk = false;
504
+ let vkLockDetail: string;
505
+ if (manifestDeployTxnVersion !== undefined && onChainTxnVersion !== undefined && onChainTxnVersion === manifestDeployTxnVersion) {
506
+ if (onChainAuth === 'Impossible') {
507
+ vkLockOk = true;
508
+ vkLockDetail = `permissions.setVerificationKey.auth = impossible (circuit cannot be swapped; txnVersion ${onChainTxnVersion} matches deploy)`;
509
+ } else if (onChainAuth === 'Signature') {
510
+ vkLockOk = true;
511
+ vkLockDetail = `permissions.setVerificationKey.auth = signature (post-hardfork fallback; deploy key was destroyed at deploy time; txnVersion ${onChainTxnVersion} matches deploy)`;
512
+ } else {
513
+ vkLockDetail = `permissions.setVerificationKey.auth = "${onChainAuth ?? '?'}" — expected impossible or signature`;
514
+ }
515
+ } else if (manifestDeployTxnVersion === undefined) {
516
+ // Older manifest without deployTxnVersion — fall back to strict.
517
+ vkLockOk = onChainAuth === 'Impossible';
518
+ vkLockDetail = vkLockOk
519
+ ? 'permissions.setVerificationKey.auth = impossible (circuit cannot be swapped; manifest omits deployTxnVersion — strict rule applied)'
520
+ : `permissions.setVerificationKey.auth = "${onChainAuth ?? '?'}" ≠ impossible — vendor could swap the circuit (manifest omits deployTxnVersion — strict rule applied)`;
521
+ } else {
522
+ vkLockDetail = `on-chain setVerificationKey.txnVersion (${onChainTxnVersion ?? 'unknown'}) ≠ manifest deployTxnVersion (${manifestDeployTxnVersion}) — permission may have been re-set since deploy`;
523
+ }
524
+ findings.push({
525
+ name: 'chain_set_verification_key_locked',
526
+ ok: vkLockOk,
527
+ detail: vkLockDetail,
528
+ });
529
+
530
+ // send + editState must be proof() (not signature()): withdrawals and Merkle
531
+ // root advances only happen via a valid LicensingApp proof.
532
+ findings.push({
533
+ name: 'chain_send_proof_only',
534
+ ok: !!state.sendIsProof,
535
+ detail: state.sendIsProof
536
+ ? 'permissions.send = proof() (escrow only leaves via a refund proof)'
537
+ : 'permissions.send ≠ proof() — vendor could drain escrow with a signature',
538
+ });
539
+
540
+ findings.push({
541
+ name: 'chain_edit_state_proof_only',
542
+ ok: !!state.editStateIsProof,
543
+ detail: state.editStateIsProof
544
+ ? 'permissions.editState = proof() (Merkle root advances only via a proof)'
545
+ : 'permissions.editState ≠ proof() — vendor could rewrite the license root',
546
+ });
547
+
548
+ const allOk = findings.every((f) => f.ok);
549
+ return { ok: allOk, origin, appId, address, generation, findings };
550
+ }