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,230 @@
1
+ // Copyright (c) 2025-2026 zkLicensing project developers
2
+ // SPDX-License-Identifier: BUSL-1.1
3
+
4
+ /**
5
+ * permitProof.ts — `zklicensing/permits/proof`
6
+ *
7
+ * Zero-knowledge proof that the prover holds an active permit in a registry
8
+ * (PERMITS_PLAN.md §7.2), without revealing which permit or which key:
9
+ *
10
+ * public: registry, root, slot, requiredScope, context
11
+ * output: nullifier = Poseidon(holder secret, context)
12
+ * private: the permit leaf, its Merkle witness, the holder secret, and the
13
+ * holder's signature over (registry, root, context)
14
+ *
15
+ * The verifier checks the proof, that `root` is a recent root of the
16
+ * registry (from a Mina node, or the keeper's /roots), that `slot` is close
17
+ * to now, and — if it wants one use per holder per context — that the
18
+ * nullifier is new. Proving runs on the holder's device: it needs the
19
+ * holder's signature and secret, which never leave it.
20
+ *
21
+ * The holder secret: permit subjects are public (each permit is published
22
+ * with its subject), so a nullifier computed from the subject could be
23
+ * recomputed by anyone for every permit and matched. The nullifier is
24
+ * computed from a secret only the holder's wallet can produce instead: the
25
+ * hash of its text signature over (registry, network, a counter), which is
26
+ * deterministic, so the same wallet re-derives it anywhere from the chain
27
+ * alone (holderSecret.ts). The permit's
28
+ * salt is the hash of that secret, so the keeper and the authority, who see
29
+ * the salt, still cannot compute nullifiers; and one secret per permit keeps
30
+ * one nullifier per permit and context.
31
+ */
32
+
33
+ import { Field, MerkleMapWitness, Poseidon, PublicKey, Signature, Struct, UInt32, ZkProgram } from 'o1js';
34
+ import {
35
+ PermitLeaf, permitLeafHash, permitSubject, PERMIT_STATUS_ACTIVE,
36
+ } from './permitMessages.js';
37
+ import { saltFromHolderSecret } from './holderSecret.js';
38
+
39
+ /** Tag for the holder's proof signature (distinct from every registry message). */
40
+ export const PERMIT_PROOF_TAG: Field = Field([...'zkPermits/v1/proof'].reduce((v, ch) => (v << 8n) | BigInt(ch.charCodeAt(0)), 0n));
41
+
42
+ export const PermitProofInput: any = Struct({
43
+ registry: PublicKey,
44
+ root: Field,
45
+ slot: UInt32,
46
+ requiredScope: Field,
47
+ context: Field,
48
+ });
49
+ export type PermitProofInput = any;
50
+
51
+ /** The message the holder signs to prove control of the permit's key. */
52
+ export function holderProofMessage(registry: PublicKey, root: Field, context: Field): Field[] {
53
+ return [PERMIT_PROOF_TAG, ...registry.toFields(), root, context];
54
+ }
55
+
56
+ /** Nullifier: stable per (permit, context), unlinkable across contexts; needs the holder secret. */
57
+ export function permitNullifier(holderSecret: Field, context: Field): Field {
58
+ return Poseidon.hash([PERMIT_PROOF_TAG, holderSecret, context]);
59
+ }
60
+
61
+ // ── Holder secret ─── (holderSecret.ts; re-exported here)
62
+
63
+ export {
64
+ HOLDER_SECRET_TAG, HOLDER_SALT_TAG, holderSecretText, holderSecretFromSignature, saltFromHolderSecret, deriveHolderSecret,
65
+ findHolderSecret, holderSecretMatches, MAX_HOLDER_SECRETS,
66
+ } from './holderSecret.js';
67
+
68
+ // ── Verifier-bound challenges ───────────────────────────────────────────────
69
+ //
70
+ // The proof's `context` is a public input the verifier chooses, and the
71
+ // nullifier is Poseidon(tag, subject, context). If two verifiers handed out
72
+ // the same raw context, the same holder would give both the same nullifier,
73
+ // and comparing lists would link the two uses. So the context is derived
74
+ // from the verifier's identity and its nonce: verifiers with different
75
+ // identities always get different contexts, whatever nonces they pick. The
76
+ // holder derives it from the identity it actually talks to (a site's
77
+ // origin, a service's address), never from a context the verifier supplies.
78
+
79
+ /** Tag for verifier-bound contexts (distinct from PERMIT_PROOF_TAG). */
80
+ export const VERIFIER_CONTEXT_TAG: Field = Field([...'zkPermits/v1/verifier'].reduce((v, ch) => (v << 8n) | BigInt(ch.charCodeAt(0)), 0n));
81
+
82
+ /**
83
+ * The canonical form of a verifier identity: a URL becomes its origin
84
+ * (scheme, lower-case host, non-default port; path and query dropped), so
85
+ * `https://Shop.Example/verify?x=1` and `https://shop.example` are the same
86
+ * verifier. Anything else is a service name, trimmed and lower-cased with
87
+ * runs of whitespace collapsed.
88
+ */
89
+ export function normalizeVerifier(verifier: string): string {
90
+ const raw = String(verifier ?? '').trim();
91
+ let id: string;
92
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) {
93
+ let origin: string;
94
+ try { origin = new URL(raw).origin; } catch { throw new Error(`verifier is not a valid URL: ${raw}`); }
95
+ if (!origin || origin === 'null') throw new Error(`verifier URL has no origin: ${raw}`);
96
+ id = origin;
97
+ } else {
98
+ id = raw.toLowerCase().replace(/\s+/g, ' ');
99
+ }
100
+ if (!id) throw new Error('verifier identity is empty');
101
+ if (id.length > 256) throw new Error('verifier identity is longer than 256 characters');
102
+ if (/[\u0000-\u001f\u007f]/.test(id)) throw new Error('verifier identity contains control characters');
103
+ return id;
104
+ }
105
+
106
+ /** A fresh 128-bit nonce for a verifier to hand to a holder. */
107
+ export function randomNonce(): bigint {
108
+ const b = new Uint8Array(16);
109
+ globalThis.crypto.getRandomValues(b);
110
+ return b.reduce((v, x) => (v << 8n) | BigInt(x), 0n);
111
+ }
112
+
113
+ function parseNonce(nonce: bigint | string): bigint {
114
+ const n = typeof nonce === 'bigint' ? nonce : /^\d{1,80}$/.test(String(nonce).trim()) ? BigInt(String(nonce).trim()) : -1n;
115
+ if (n < 0n || n >= 1n << 253n) throw new Error('nonce must be a decimal integer below 2^253');
116
+ return n;
117
+ }
118
+
119
+ /**
120
+ * The proof context for `verifier` (see normalizeVerifier) and its `nonce`:
121
+ * Poseidon(VERIFIER_CONTEXT_TAG, byteLength, identity in 31-byte chunks, nonce).
122
+ * Holder and verifier compute it the same way.
123
+ */
124
+ export function verifierContext(verifier: string, nonce: bigint | string): bigint {
125
+ const bytes = new TextEncoder().encode(normalizeVerifier(verifier));
126
+ const chunks: Field[] = [];
127
+ for (let i = 0; i < bytes.length; i += 31) {
128
+ chunks.push(Field(bytes.slice(i, i + 31).reduce((v, x) => (v << 8n) | BigInt(x), 0n)));
129
+ }
130
+ return Poseidon.hash([VERIFIER_CONTEXT_TAG, Field(bytes.length), ...chunks, Field(parseNonce(nonce))]).toBigInt();
131
+ }
132
+
133
+ /** In-circuit: every bit set in `required` is set in `scope` (254-bit masks). */
134
+ function assertScopeCovers(scope: Field, required: Field): void {
135
+ const s = scope.toBits(254);
136
+ const r = required.toBits(254);
137
+ for (let i = 0; i < 254; i++) r[i].not().or(s[i]).assertTrue();
138
+ }
139
+
140
+ export const PermitProof: any = ZkProgram({
141
+ name: 'zkPermits-holder-proof',
142
+ publicInput: PermitProofInput,
143
+ publicOutput: Field,
144
+ methods: {
145
+ prove: {
146
+ privateInputs: [PermitLeaf, MerkleMapWitness, Field, Signature],
147
+ async method(input: PermitProofInput, leaf: PermitLeaf, witness: MerkleMapWitness, holderSecret: Field, holderSig: Signature) {
148
+ const [root, key] = witness.computeRootAndKey(permitLeafHash(leaf));
149
+ root.assertEquals(input.root);
150
+ key.assertEquals(leaf.subject);
151
+ permitSubject(leaf.holderKey, saltFromHolderSecret(holderSecret)).assertEquals(leaf.subject);
152
+ holderSig.verify(leaf.holderKey, holderProofMessage(input.registry, input.root, input.context)).assertTrue();
153
+ leaf.status.assertEquals(PERMIT_STATUS_ACTIVE);
154
+ leaf.validFrom.assertLessThanOrEqual(input.slot);
155
+ input.slot.assertLessThanOrEqual(leaf.expirySlot);
156
+ assertScopeCovers(leaf.scope, input.requiredScope);
157
+ return { publicOutput: permitNullifier(holderSecret, input.context) };
158
+ },
159
+ },
160
+ },
161
+ });
162
+
163
+ export type HolderProofRequest = {
164
+ registry: string;
165
+ /** Root the witness is for (the keeper's GET …/permits/:subject returns both). */
166
+ root: string;
167
+ witness: { isLefts: boolean[]; siblings: string[] };
168
+ leaf: PermitLeaf;
169
+ /** The holder secret (deriveHolderSecret). Never leaves the holder's device. */
170
+ holderSecret: string;
171
+ slot: number;
172
+ requiredScope: bigint;
173
+ context: bigint;
174
+ /** Signature over holderProofMessage(registry, root, context) by the holder key (wallet signFields). */
175
+ holderSignature: string;
176
+ };
177
+
178
+ /** Holder side: compiles (once) and proves. Returns the proof as JSON. */
179
+ export async function proveHolder(r: HolderProofRequest): Promise<{ proof: unknown; nullifier: string }> {
180
+ const { Bool } = await import('o1js');
181
+ await PermitProof.compile();
182
+ const input = new PermitProofInput({
183
+ registry: PublicKey.fromBase58(r.registry),
184
+ root: Field(r.root),
185
+ slot: UInt32.from(r.slot),
186
+ requiredScope: Field(r.requiredScope),
187
+ context: Field(r.context),
188
+ });
189
+ const witness = new MerkleMapWitness(r.witness.isLefts.map(b => Bool(b)), r.witness.siblings.map(s => Field(s)));
190
+ const { proof } = await PermitProof.prove(input, r.leaf, witness, Field(r.holderSecret), Signature.fromBase58(r.holderSignature));
191
+ return { proof: proof.toJSON(), nullifier: proof.publicOutput.toString() };
192
+ }
193
+
194
+ export type HolderProofCheck = {
195
+ registry: string;
196
+ /** Roots accepted as current (e.g. from a Mina node, or the keeper's recent /roots). */
197
+ acceptedRoots: string[];
198
+ currentSlot: number;
199
+ /** How far the proof's slot may be from `currentSlot`. Default 10 slots. */
200
+ maxSlotSkew?: number;
201
+ requiredScope?: bigint;
202
+ } & (
203
+ /** Your identity as the holder saw it and the nonce you issued: the expected context is verifierContext(verifier, nonce). */
204
+ | { verifier: string; nonce: bigint | string; context?: never }
205
+ /** A raw expected context, for schemes that derive it otherwise. */
206
+ | { context: bigint; verifier?: never; nonce?: never }
207
+ );
208
+
209
+ /** Verifier side: checks the proof and its public inputs. Returns the nullifier. */
210
+ export async function verifyHolderProof(proofJson: unknown, check: HolderProofCheck): Promise<{ valid: boolean; nullifier: string | null; reason: string | null }> {
211
+ const { verificationKey } = await PermitProof.compile();
212
+ const ProofClass = ZkProgram.Proof(PermitProof);
213
+ let proof: any;
214
+ try { proof = await ProofClass.fromJSON(proofJson as any); } catch (e: any) {
215
+ return { valid: false, nullifier: null, reason: `malformed proof: ${e?.message ?? e}` };
216
+ }
217
+ const pub = proof.publicInput;
218
+ const fail = (reason: string) => ({ valid: false, nullifier: null, reason });
219
+ if (pub.registry.toBase58() !== check.registry) return fail('proof is for another registry');
220
+ if (!check.acceptedRoots.includes(pub.root.toString())) return fail('root is not a current root of the registry');
221
+ if (Math.abs(Number(pub.slot.toString()) - check.currentSlot) > (check.maxSlotSkew ?? 10)) return fail('slot is too far from now');
222
+ if (pub.requiredScope.toBigInt() !== (check.requiredScope ?? 0n)) return fail('proof is for other scope bits');
223
+ let expected: bigint;
224
+ try { expected = check.verifier !== undefined ? verifierContext(check.verifier, check.nonce) : check.context; }
225
+ catch (e: any) { return fail(e?.message ?? String(e)); }
226
+ if (pub.context.toBigInt() !== expected) return fail('proof is for another context');
227
+ const { verify } = await import('o1js');
228
+ if (!(await verify(proof, verificationKey))) return fail('proof does not verify');
229
+ return { valid: true, nullifier: proof.publicOutput.toString(), reason: null };
230
+ }
@@ -0,0 +1,423 @@
1
+ // Copyright (c) 2025-2026 zkLicensing project developers
2
+ // SPDX-License-Identifier: BUSL-1.1
3
+
4
+ /**
5
+ * permitRegistryRebuild.ts — `zklicensing/permits/rebuild`
6
+ *
7
+ * Rebuilds a PermitRegistry's permits tree from its events alone. Every
8
+ * event that changes a permit carries the full new leaf, so the tree is
9
+ * recoverable from the chain without the Keeper (PERMITS_PLAN.md §8.2).
10
+ * The Keeper's indexer uses this same code. Open applications (fees in
11
+ * escrow) are rebuilt the same way: filed minus settled.
12
+ */
13
+
14
+ import { Field, PublicKey, UInt32, UInt64 } from 'o1js';
15
+ import { CompactMerkleMap, encodeFields, decodeFields } from './compactMerkleMap.js';
16
+ import { Application, PermitLeaf, applicationHash, permitLeafHash, unpackFees, unpackPolicy } from './permitMessages.js';
17
+ import { PermitRegistryEvents } from './permitInterface.js';
18
+ import { forkBumpedExpiry } from './contractInterface.js';
19
+
20
+ /** A decoded event, as returned by `fetchEvents` (fields we use). */
21
+ export type RegistryEvent = {
22
+ type: string;
23
+ event: { data: any };
24
+ blockHeight?: UInt32 | { toString(): string };
25
+ globalSlot?: UInt32 | { toString(): string };
26
+ };
27
+
28
+ export type PermitRecord = {
29
+ subject: string;
30
+ holderKey: string;
31
+ scope: string;
32
+ validFrom: number;
33
+ expirySlot: number;
34
+ status: number;
35
+ seq: number;
36
+ docHash: string;
37
+ };
38
+
39
+ /** An application with its fee in escrow, as JSON (amounts in nanomina, slots as numbers). */
40
+ export type ApplicationRecord = {
41
+ subject: string;
42
+ holderKey: string;
43
+ payer: string;
44
+ baseSeq: number;
45
+ scope: string;
46
+ validFrom: number;
47
+ expirySlot: number;
48
+ docHash: string;
49
+ fee: string;
50
+ processingFee: string;
51
+ deadline: number;
52
+ /** The registry's filing number. */
53
+ filing: number;
54
+ };
55
+
56
+ /** How an application ended (APPLICATION_OUTCOME), amounts in nanomina. */
57
+ export type Settlement = { subject: string; outcome: number; kept: string; refunded: string };
58
+
59
+ export type RegistryView = {
60
+ /** The permits tree (a MerkleMap drop-in that can be saved and loaded without re-hashing). */
61
+ tree: CompactMerkleMap;
62
+ root: string;
63
+ permits: Map<string, PermitRecord>;
64
+ keysHash: string | null;
65
+ epoch: number;
66
+ feesPacked: string | null;
67
+ /** The escrow tree (subject → applicationHash of the open application) and its root. */
68
+ escrow: CompactMerkleMap;
69
+ escrowRoot: string;
70
+ /** Open applications by subject. */
71
+ applications: Map<string, ApplicationRecord>;
72
+ /** Settled applications by application hash: how each ended. */
73
+ settlements: Map<string, Settlement>;
74
+ eventsApplied: number;
75
+ /** Set once the registry was retired (redeployed): its frozen permits root. */
76
+ retiredRoot?: string;
77
+ /** Whether the platform key retired it (retireByPlatform) rather than the authority. */
78
+ retiredByPlatform?: boolean;
79
+ /** Whether it was retired with application fees in escrow (they stay on it). */
80
+ retiredForced?: boolean;
81
+ /**
82
+ * A redeployed registry starts with its predecessor's permits
83
+ * (seededRegistryView): the tree it started with, and each carried-over
84
+ * permit's seq then, so its next change is seq + 1.
85
+ */
86
+ start?: { root: string; seqs: Map<string, number> };
87
+ };
88
+
89
+ export function leafToRecord(leaf: PermitLeaf): PermitRecord {
90
+ return {
91
+ subject: leaf.subject.toString(),
92
+ // The never-granted leaf's key is PublicKey.empty() (not a curve point): ''.
93
+ holderKey: leaf.holderKey.equals(PublicKey.empty()).toBoolean() ? '' : leaf.holderKey.toBase58(),
94
+ scope: leaf.scope.toString(),
95
+ validFrom: Number(leaf.validFrom.toString()),
96
+ expirySlot: Number(leaf.expirySlot.toString()),
97
+ status: Number(leaf.status.toString()),
98
+ seq: Number(leaf.seq.toString()),
99
+ docHash: leaf.docHash.toString(),
100
+ };
101
+ }
102
+
103
+ export function applicationToRecord(app: Application): ApplicationRecord {
104
+ return {
105
+ subject: app.subject.toString(),
106
+ holderKey: app.holderKey.toBase58(),
107
+ payer: app.payer.toBase58(),
108
+ baseSeq: Number(app.baseSeq.toString()),
109
+ scope: app.scope.toString(),
110
+ validFrom: Number(app.validFrom.toString()),
111
+ expirySlot: Number(app.expirySlot.toString()),
112
+ docHash: app.docHash.toString(),
113
+ fee: app.fee.toString(),
114
+ processingFee: app.processingFee.toString(),
115
+ deadline: Number(app.deadline.toString()),
116
+ filing: Number(app.filing.toString()),
117
+ };
118
+ }
119
+
120
+ export function recordToApplication(r: ApplicationRecord): Application {
121
+ return new Application({
122
+ subject: Field(r.subject),
123
+ holderKey: PublicKey.fromBase58(r.holderKey),
124
+ payer: PublicKey.fromBase58(r.payer),
125
+ baseSeq: UInt32.from(r.baseSeq),
126
+ scope: Field(r.scope),
127
+ validFrom: UInt32.from(r.validFrom),
128
+ expirySlot: UInt32.from(r.expirySlot),
129
+ docHash: Field(r.docHash),
130
+ fee: UInt64.from(r.fee),
131
+ processingFee: UInt64.from(r.processingFee),
132
+ deadline: UInt32.from(r.deadline),
133
+ filing: UInt32.from(r.filing),
134
+ });
135
+ }
136
+
137
+ export function recordToLeaf(r: PermitRecord): PermitLeaf {
138
+ return new PermitLeaf({
139
+ subject: Field(r.subject),
140
+ holderKey: r.holderKey === '' ? PublicKey.empty() : PublicKey.fromBase58(r.holderKey),
141
+ scope: Field(r.scope),
142
+ validFrom: UInt32.from(r.validFrom),
143
+ expirySlot: UInt32.from(r.expirySlot),
144
+ status: UInt32.from(r.status),
145
+ seq: UInt32.from(r.seq),
146
+ docHash: Field(r.docHash),
147
+ });
148
+ }
149
+
150
+ /**
151
+ * The view a redeployed registry starts with: a copy of its retired
152
+ * predecessor's permits and tree (the initialPermitsRoot its keys signed).
153
+ * Its own events then apply on top; `registryCreated` checks the root.
154
+ */
155
+ export function seededRegistryView(predecessor: RegistryView): RegistryView {
156
+ const view = registryViewFromStored({ ...registryViewToStored(predecessor), applications: [] });
157
+ view.start = { root: view.root, seqs: new Map([...view.permits.values()].map(p => [p.subject, p.seq] as const)) };
158
+ return view;
159
+ }
160
+
161
+ /**
162
+ * The retired registry a registry continues, from its `registryCreated`
163
+ * event (base58), or null for a first generation. With it a registry's
164
+ * lineage can be walked back on chain events alone; the successor's start
165
+ * was checked against the predecessor's retired root when it was deployed.
166
+ */
167
+ export function registryPredecessor(events: RegistryEvent[]): string | null {
168
+ const created = events.find(e => e.type === 'registryCreated');
169
+ const p = created?.event.data.predecessor as PublicKey | undefined;
170
+ if (!p || p.equals(PublicKey.empty()).toBoolean()) return null;
171
+ return p.toBase58();
172
+ }
173
+
174
+ export function emptyRegistryView(): RegistryView {
175
+ const tree = new CompactMerkleMap();
176
+ return { tree, root: tree.getRoot().toString(), permits: new Map(), keysHash: null, epoch: 0, feesPacked: null, ...emptyEscrow(), eventsApplied: 0 };
177
+ }
178
+
179
+ function emptyEscrow() {
180
+ const escrow = new CompactMerkleMap();
181
+ return { escrow, escrowRoot: escrow.getRoot().toString(), applications: new Map<string, ApplicationRecord>(), settlements: new Map<string, Settlement>() };
182
+ }
183
+
184
+ /**
185
+ * Open applications from filed and settled events. A settled event names the
186
+ * application by hash; the same hash can be filed again after it settles, so
187
+ * filings and settlements are counted. Events of a transaction the chain
188
+ * refused can show up in some sources (the local test chain records them):
189
+ * a settlement with no filing is ignored, and of two open filings for one
190
+ * subject the later filing wins, since a subject can only be filed again
191
+ * once its previous application settled. The view's escrowRoot against the
192
+ * contract's is the check that the result is right.
193
+ */
194
+ function openApplications(filed: Application[], settled: string[]): Map<string, ApplicationRecord> {
195
+ const closed = new Map<string, number>();
196
+ for (const h of settled) closed.set(h, (closed.get(h) ?? 0) + 1);
197
+ const open = new Map<string, ApplicationRecord>();
198
+ for (const app of filed) {
199
+ const h = applicationHash(app).toString();
200
+ const n = closed.get(h) ?? 0;
201
+ if (n > 0) { closed.set(h, n - 1); continue; }
202
+ const rec = applicationToRecord(app);
203
+ const other = open.get(rec.subject);
204
+ if (!other || rec.filing > other.filing) open.set(rec.subject, rec);
205
+ }
206
+ return open;
207
+ }
208
+
209
+ /** Replaces the view's open applications and rebuilds the (small) escrow tree. */
210
+ function setApplications(view: RegistryView, open: Map<string, ApplicationRecord>) {
211
+ const escrow = new CompactMerkleMap();
212
+ escrow.setMany([...open.values()].map(r => [Field(r.subject), applicationHash(recordToApplication(r))] as [Field, Field]));
213
+ view.escrow = escrow;
214
+ view.escrowRoot = escrow.getRoot().toString();
215
+ view.applications = open;
216
+ }
217
+
218
+ /**
219
+ * Applies events in any order. Archive nodes, local chains and paginated
220
+ * fetches don't agree on ordering, so none is assumed: each subject's leaves
221
+ * are ordered by `seq` (the contract advances it by exactly one per change),
222
+ * key rotations by `epoch` and fee changes by `actionNonce`. A gap in a
223
+ * subject's sequence means events are missing, and throws rather than build
224
+ * a wrong tree. Applying to a non-empty view continues from its state.
225
+ */
226
+ export function applyRegistryEvents(view: RegistryView, events: RegistryEvent[]): RegistryView {
227
+ const updates: Array<[Field, Field]> = [];
228
+ const esc = { filed: [...view.applications.values()].map(recordToApplication), settled: [] as string[] };
229
+ for (const [subject, last] of collectRegistryEvents(view, events, esc)) {
230
+ updates.push([last.leaf.subject, permitLeafHash(last.leaf)]);
231
+ view.permits.set(subject, last.rec);
232
+ }
233
+ view.tree.setMany(updates);
234
+ view.root = view.tree.getRoot().toString();
235
+ setApplications(view, openApplications(esc.filed, esc.settled));
236
+ return view;
237
+ }
238
+
239
+ /**
240
+ * Brings `view` up to date with a registry's full event list, re-hashing only
241
+ * the permits whose latest leaf changed: a full rebuild costs a 256-level tree
242
+ * path per permit, a refresh one per changed permit. Returns the subjects
243
+ * that changed (added or updated).
244
+ */
245
+ export function refreshRegistryView(view: RegistryView, events: RegistryEvent[]): string[] {
246
+ const fresh: RegistryView = { tree: view.tree, root: view.root, permits: new Map(), keysHash: null, epoch: 0, feesPacked: null, ...emptyEscrow(), eventsApplied: 0, ...(view.start ? { start: view.start } : {}) };
247
+ const esc = { filed: [] as Application[], settled: [] as string[] };
248
+ const latest = collectRegistryEvents(fresh, events, esc);
249
+ const changed: string[] = [];
250
+ const updates: Array<[Field, Field]> = [];
251
+ for (const [subject, last] of latest) {
252
+ const cur = view.permits.get(subject);
253
+ if (cur && sameRecord(cur, last.rec)) continue;
254
+ updates.push([last.leaf.subject, permitLeafHash(last.leaf)]);
255
+ view.permits.set(subject, last.rec);
256
+ changed.push(subject);
257
+ }
258
+ view.tree.setMany(updates);
259
+ view.keysHash = fresh.keysHash; view.epoch = fresh.epoch; view.feesPacked = fresh.feesPacked; view.eventsApplied = fresh.eventsApplied;
260
+ view.settlements = fresh.settlements;
261
+ view.root = view.tree.getRoot().toString();
262
+ setApplications(view, openApplications(esc.filed, esc.settled));
263
+ return changed;
264
+ }
265
+
266
+ /** Same permit leaf (field by field: cheaper than hashing both). */
267
+ function sameRecord(a: PermitRecord, b: PermitRecord): boolean {
268
+ return a.seq === b.seq && a.status === b.status && a.scope === b.scope && a.validFrom === b.validFrom
269
+ && a.expirySlot === b.expirySlot && a.holderKey === b.holderKey && a.docHash === b.docHash && a.subject === b.subject;
270
+ }
271
+
272
+ /**
273
+ * Validates the events and returns each subject's latest leaf; updates the
274
+ * view's keys, epoch and fees, and collects escrow events into `esc`.
275
+ */
276
+ function collectRegistryEvents(view: RegistryView, events: RegistryEvent[], esc: { filed: Application[]; settled: string[] }): Map<string, { type: string; leaf: PermitLeaf; rec: PermitRecord }> {
277
+ const bySubject = new Map<string, Array<{ type: string; leaf: PermitLeaf; rec: PermitRecord }>>();
278
+ let lastEpoch = view.epoch;
279
+ let lastFeeNonce = -1;
280
+ for (const e of events) {
281
+ const d = e.event.data;
282
+ switch (e.type) {
283
+ case 'registryCreated': {
284
+ if (view.keysHash === null) view.keysHash = d.keysHash.toString();
285
+ const startRoot = view.start?.root ?? emptyRegistryView().root;
286
+ if (d.permitsRoot !== undefined && d.permitsRoot.toString() !== startRoot) {
287
+ throw new Error('the registry started from another permits tree: seed the view with its predecessor (seededRegistryView)');
288
+ }
289
+ break;
290
+ }
291
+ case 'authorityRotated': {
292
+ const epoch = Number(d.epoch.toString());
293
+ if (epoch > lastEpoch || view.keysHash === null) { lastEpoch = epoch; view.keysHash = d.keysHash.toString(); view.epoch = epoch; }
294
+ break;
295
+ }
296
+ case 'feesUpdated': {
297
+ const nonce = Number(d.actionNonce.toString());
298
+ if (nonce > lastFeeNonce) { lastFeeNonce = nonce; view.feesPacked = d.feesPacked.toString(); }
299
+ break;
300
+ }
301
+ // A permit moved to a new holder key: its new leaf (seq 1, a new subject);
302
+ // the old one closes as MOVED in a permitStatusChanged event.
303
+ case 'permitMoved':
304
+ case 'permitGranted':
305
+ case 'permitStatusChanged': {
306
+ const leaf = d.leaf as PermitLeaf;
307
+ const rec = leafToRecord(leaf);
308
+ const list = bySubject.get(rec.subject) ?? [];
309
+ list.push({ type: e.type, leaf, rec });
310
+ bySubject.set(rec.subject, list);
311
+ break;
312
+ }
313
+ case 'applicationFiled':
314
+ esc.filed.push(d.application as Application);
315
+ break;
316
+ case 'applicationSettled': {
317
+ const hash = d.applicationHash.toString();
318
+ esc.settled.push(hash);
319
+ view.settlements.set(hash, { subject: d.subject.toString(), outcome: Number(d.outcome.toString()), kept: d.kept.toString(), refunded: d.refunded.toString() });
320
+ break;
321
+ }
322
+ // Payout and platform-key changes touch no permit (the account state has them).
323
+ case 'payoutProposed':
324
+ case 'payoutChanged':
325
+ case 'platformChanged':
326
+ break;
327
+ case 'registryRetired':
328
+ view.retiredRoot = d.permitsRoot.toString();
329
+ view.retiredByPlatform = d.byPlatform.toBoolean();
330
+ view.retiredForced = d.forced.toBoolean();
331
+ break;
332
+ default:
333
+ throw new Error(`unknown registry event type: ${e.type}`);
334
+ }
335
+ view.eventsApplied++;
336
+ }
337
+ // A rotation to epoch N implies the registryCreated keys were replaced.
338
+ const out = new Map<string, { type: string; leaf: PermitLeaf; rec: PermitRecord }>();
339
+ for (const [subject, list] of bySubject) {
340
+ list.sort((a, b) => a.rec.seq - b.rec.seq);
341
+ // Each change is the previous seq + 1, from the start: 0, or the seq a
342
+ // carried-over permit had when the registry started.
343
+ let prevSeq = view.permits.get(subject)?.seq ?? view.start?.seqs.get(subject) ?? 0;
344
+ for (const item of list) {
345
+ if (item.rec.seq !== prevSeq + 1) {
346
+ throw new Error(`event out of order for ${subject}: seq ${item.rec.seq} after ${prevSeq || 'none'}`);
347
+ }
348
+ prevSeq = item.rec.seq;
349
+ }
350
+ out.set(subject, list[list.length - 1]);
351
+ }
352
+ return out;
353
+ }
354
+
355
+ /** Fetches a registry's events (active Mina instance) and rebuilds its tree. */
356
+ export async function rebuildRegistry(address: PublicKey | string): Promise<RegistryView> {
357
+ const pk = typeof address === 'string' ? PublicKey.fromBase58(address) : address;
358
+ const events = await new PermitRegistryEvents(pk).fetchEvents();
359
+ return applyRegistryEvents(emptyRegistryView(), events as RegistryEvent[]);
360
+ }
361
+
362
+ /** 'moved': the holder moved it to a new key (a new permit, under a new subject); this one is closed. */
363
+ export type PermitStatusName = 'active' | 'suspended' | 'revoked' | 'moved' | 'expired' | 'not-yet-valid' | 'in-grace' | 'unknown';
364
+
365
+ /**
366
+ * Effective status of a permit at `slot`: revoked and suspended win, then the
367
+ * dates. `gracePeriod` comes from the registry policy.
368
+ */
369
+ /**
370
+ * A permit's status at `slot`. With the registry's `forkActivationSlot`
371
+ * (a successor deployed after a Mina hard fork): a permit that lapsed
372
+ * shortly before the fork, while its registry was retired and couldn't
373
+ * renew it, counts as expiring at the fork slot, so its grace runs from the
374
+ * fork (as licensing's effectiveExpirySlot). Anonymous holder proofs check
375
+ * the expiry as granted.
376
+ */
377
+ export function permitStatusAt(rec: PermitRecord | undefined, slot: number, gracePeriod = 0, forkActivationSlot = 0): PermitStatusName {
378
+ if (!rec) return 'unknown';
379
+ if (rec.status === 2) return 'revoked';
380
+ if (rec.status === 3) return 'moved';
381
+ if (rec.status === 1) return 'suspended';
382
+ if (slot < rec.validFrom) return 'not-yet-valid';
383
+ const expiry = forkBumpedExpiry(rec.expirySlot, forkActivationSlot, gracePeriod);
384
+ if (slot <= expiry) return 'active';
385
+ if (slot <= expiry + gracePeriod) return 'in-grace';
386
+ return 'expired';
387
+ }
388
+
389
+ export { unpackFees, unpackPolicy };
390
+
391
+ /** A registry view saved without its tree's hashes recomputed (see CompactMerkleMap). */
392
+ export type StoredRegistryView = {
393
+ root: string;
394
+ permits: PermitRecord[];
395
+ tree: { indices: string; leaves: string; splits: string };
396
+ /** Open applications (absent in views saved before escrow). */
397
+ applications?: ApplicationRecord[];
398
+ /** A redeployed registry's start (RegistryView.start): its root and the carried-over seqs. */
399
+ start?: { root: string; seqs: Array<[string, number]> };
400
+ };
401
+
402
+ export function registryViewToStored(view: RegistryView): StoredRegistryView {
403
+ const d = view.tree.toCompact();
404
+ return {
405
+ root: view.root, permits: [...view.permits.values()], applications: [...view.applications.values()],
406
+ tree: { indices: encodeFields(d.indices), leaves: encodeFields(d.leaves), splits: encodeFields(d.splits) },
407
+ ...(view.start ? { start: { root: view.start.root, seqs: [...view.start.seqs] } } : {}),
408
+ };
409
+ }
410
+
411
+ /**
412
+ * Loads a saved view without re-hashing; its keys, epoch and fees come back
413
+ * with the next refreshRegistryView. Throws if the copy is inconsistent.
414
+ */
415
+ export function registryViewFromStored(s: StoredRegistryView): RegistryView {
416
+ const tree = CompactMerkleMap.fromCompact({ indices: decodeFields(s.tree.indices), leaves: decodeFields(s.tree.leaves), splits: decodeFields(s.tree.splits) }, s.root);
417
+ if (tree.size !== s.permits.length) throw new Error('saved registry view: the permits and the tree disagree');
418
+ const permits = new Map(s.permits.map(p => [p.subject, p] as const));
419
+ const view: RegistryView = { tree, root: s.root, permits, keysHash: null, epoch: 0, feesPacked: null, ...emptyEscrow(), eventsApplied: 0 };
420
+ setApplications(view, new Map((s.applications ?? []).map(a => [a.subject, a] as const)));
421
+ if (s.start) view.start = { root: s.start.root, seqs: new Map(s.start.seqs) };
422
+ return view;
423
+ }