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,234 @@
1
+ #!/usr/bin/env node
2
+ // Copyright (c) 2025-2026 zkLicensing project developers
3
+ // SPDX-License-Identifier: BUSL-1.1
4
+
5
+ /**
6
+ * trustAnchorVerifyCli.ts
7
+ *
8
+ * Command-line front-end for verifyTrustAnchor(). Ships as the `zklicensing-verify-trust`
9
+ * bin — the sibling of `zklicensing-verify` (which is the verifyService HTTP daemon).
10
+ *
11
+ * Usage:
12
+ * zklicensing-verify-trust \
13
+ * --origin https://store.example.com \
14
+ * --address B62q... \
15
+ * --pinned-key <base64-ed25519-pubkey> \
16
+ * [--mina-graphql https://api.minascan.io/node/mainnet/v1/graphql] \
17
+ * [--mina-network-id mainnet|testnet|devnet] \
18
+ * [--skip-chain-checks] \
19
+ * [--json]
20
+ *
21
+ * The manifest lives at `<origin>/.well-known/zklicensing/<address>.json` —
22
+ * one file per generation address, all signed by the same platform key. The
23
+ * appId is read out of the signed payload; callers never need to know it
24
+ * out-of-band, which lets buyers verify from just the address their wallet
25
+ * shows them (including addresses for retired generations).
26
+ *
27
+ * Permit registries:
28
+ * zklicensing-verify-trust --registry B62q... [--origin https://authority.example]
29
+ * [--mina-network-id mainnet|devnet] [--keeper-url https://zklicensing.com/api/keeper]
30
+ * [--no-well-known] [--mina-graphql <url>] [--json]
31
+ * checks the registry's verification key against the published PermitRegistry
32
+ * hashes, its locked permissions and network binding, the authority website
33
+ * written into it at deploy (on the --origin host, when given) and that the
34
+ * website's /.well-known/zkpermits/<address>.json claims it, and (with
35
+ * --keeper-url) that the keeper's authority keys, payout and website match.
36
+ *
37
+ * Exit codes:
38
+ * 0 — every finding passed
39
+ * 1 — one or more findings failed
40
+ * 2 — invalid arguments
41
+ *
42
+ * --pinned-key is required and may be repeated; the CLI ships NO baked-in
43
+ * pin. Source it from https://zklicensing.com/trustanchor.html (published
44
+ * next to each generation's VK hash). See the SDK README for details.
45
+ */
46
+
47
+ import { verifyTrustAnchor, type Finding, type TrustAnchorReport } from './trustAnchorVerify.js';
48
+ import { verifyRegistryTrust } from './registryTrustVerify.js';
49
+
50
+ type Args = {
51
+ origin?: string;
52
+ address?: string;
53
+ registry?: string;
54
+ keeperUrl?: string;
55
+ pinnedKeys: string[];
56
+ // undefined = fall back to DEFAULT_GRAPHQL[minaNetworkId] at resolve time,
57
+ // so `--mina-network-id devnet` alone auto-picks the devnet endpoint even
58
+ // when the user doesn't pass --mina-graphql.
59
+ minaGraphql?: string;
60
+ minaNetworkId: 'mainnet' | 'devnet';
61
+ skipChainChecks: boolean;
62
+ crossCheck: boolean;
63
+ wellKnown: boolean;
64
+ json: boolean;
65
+ help: boolean;
66
+ };
67
+
68
+ const DEFAULT_GRAPHQL: Record<Args['minaNetworkId'], string> = {
69
+ mainnet: 'https://api.minascan.io/node/mainnet/v1/graphql',
70
+ // O1 Labs' devnet-plain node is the reliable public endpoint for freshly
71
+ // deployed accounts — public indexers like Minascan lag by several slots
72
+ // and often return null for a zkApp minutes after landing.
73
+ devnet: 'https://devnet-plain-1.gcp.o1test.net/graphql',
74
+ };
75
+
76
+ function usage(): string {
77
+ return [
78
+ 'zklicensing-verify-trust — verify a vendor page + on-chain zkApp mutually bind',
79
+ '',
80
+ 'Required:',
81
+ ' --origin <url> Vendor origin (https://...)',
82
+ ' --address <B62q...> zkApp address (the address your wallet shows you)',
83
+ ' --pinned-key <b64> Platform manifest pubkey (repeatable during rotation)',
84
+ '',
85
+ 'Or, for a permit registry:',
86
+ ' --registry <B62q...> Registry address; checks VK, permissions, network,',
87
+ ' the website bound at deploy and its .well-known claim',
88
+ ' --origin <url> The authority website you expect (compared by host)',
89
+ ' --keeper-url <url> Also compare the keeper\'s authority keys, payout, website',
90
+ ' --no-well-known Skip fetching <website>/.well-known/zkpermits/<registry>.json',
91
+ '',
92
+ 'Optional:',
93
+ ' --mina-graphql <url> Mina GraphQL endpoint (default depends on',
94
+ ' --mina-network-id: mainnet →',
95
+ ` ${DEFAULT_GRAPHQL.mainnet};`,
96
+ ` devnet → ${DEFAULT_GRAPHQL.devnet})`,
97
+ ' --mina-network-id <id> mainnet | devnet (default: devnet)',
98
+ ' --skip-chain-checks Manifest-only audit — no o1js chain reads',
99
+ ' --no-cross-check Skip the platform backend cross-check',
100
+ ' (default: on — verifies vendor manifest matches',
101
+ ' https://zklicensing.com/apps/by-address/<address>)',
102
+ ' --json Emit TrustAnchorReport as JSON on stdout',
103
+ '',
104
+ 'Exit: 0 all-ok · 1 any check failed · 2 bad args',
105
+ ].join('\n');
106
+ }
107
+
108
+ function parseArgs(argv: string[]): Args {
109
+ const args: Args = {
110
+ pinnedKeys: [],
111
+ minaNetworkId: 'devnet',
112
+ skipChainChecks: false,
113
+ // Default on — the platform backend cross-check is cheap and catches
114
+ // vendors whose local manifest has drifted from the signed platform
115
+ // record. Opt out with --no-cross-check for a pure origin+chain audit.
116
+ crossCheck: true,
117
+ wellKnown: true,
118
+ json: false,
119
+ help: false,
120
+ };
121
+ for (let i = 0; i < argv.length; i++) {
122
+ const a = argv[i];
123
+ const next = () => {
124
+ const v = argv[++i];
125
+ if (v === undefined) throw new Error(`missing value for ${a}`);
126
+ return v;
127
+ };
128
+ switch (a) {
129
+ case '--origin': args.origin = next(); break;
130
+ case '--address': args.address = next(); break;
131
+ case '--registry': args.registry = next(); break;
132
+ case '--keeper-url': args.keeperUrl = next(); break;
133
+ case '--pinned-key': args.pinnedKeys.push(next()); break;
134
+ case '--mina-graphql': args.minaGraphql = next(); break;
135
+ case '--mina-network-id': {
136
+ const v = next();
137
+ if (v !== 'mainnet' && v !== 'devnet') {
138
+ throw new Error(`--mina-network-id must be mainnet|devnet, got "${v}"`);
139
+ }
140
+ args.minaNetworkId = v;
141
+ break;
142
+ }
143
+ case '--skip-chain-checks': args.skipChainChecks = true; break;
144
+ case '--no-well-known': args.wellKnown = false; break;
145
+ // Kept as a no-op flag so any existing invocation that passed
146
+ // --cross-check keeps working. --no-cross-check opts out.
147
+ case '--cross-check': args.crossCheck = true; break;
148
+ case '--no-cross-check': args.crossCheck = false; break;
149
+ case '--json': args.json = true; break;
150
+ case '-h':
151
+ case '--help': args.help = true; break;
152
+ default: throw new Error(`unknown argument: ${a}`);
153
+ }
154
+ }
155
+ return args;
156
+ }
157
+
158
+ function formatHuman(report: TrustAnchorReport): string {
159
+ const lines: string[] = [];
160
+ const banner = report.ok ? 'TRUST ANCHOR VERIFIED' : 'TRUST ANCHOR FAILED';
161
+ lines.push(banner);
162
+ lines.push(` origin: ${report.origin}`);
163
+ lines.push(` address: ${report.address}`);
164
+ if (report.appId) lines.push(` appId: ${report.appId}`);
165
+ if (report.generation !== undefined) lines.push(` generation: ${report.generation}`);
166
+ lines.push('');
167
+ for (const f of report.findings) {
168
+ const mark = f.ok ? '[ok] ' : '[FAIL]';
169
+ lines.push(` ${mark} ${f.name} — ${f.detail}`);
170
+ }
171
+ return lines.join('\n');
172
+ }
173
+
174
+ async function main(): Promise<number> {
175
+ let args: Args;
176
+ try {
177
+ args = parseArgs(process.argv.slice(2));
178
+ } catch (err: any) {
179
+ process.stderr.write(`${err.message}\n\n${usage()}\n`);
180
+ return 2;
181
+ }
182
+ if (args.help) {
183
+ process.stdout.write(`${usage()}\n`);
184
+ return 0;
185
+ }
186
+ if (args.registry) {
187
+ const report = await verifyRegistryTrust({
188
+ address: args.registry, network: args.minaNetworkId, minaGraphqlUrl: args.minaGraphql, keeperUrl: args.keeperUrl,
189
+ website: args.origin, checkWellKnown: args.wellKnown,
190
+ });
191
+ if (args.json) process.stdout.write(JSON.stringify(report, null, 2) + '\n');
192
+ else {
193
+ process.stdout.write([report.ok ? 'REGISTRY VERIFIED' : 'REGISTRY FAILED', ` registry: ${report.address}`, ` network: ${report.network}`, '',
194
+ ...(report.findings.some(f => f.name === 'website_claims_registry' && !f.ok) ? [` The authority publishes the claim at <website>/.well-known/zkpermits/${report.address}.json`, ''] : []),
195
+ ...report.findings.map(f => ` ${f.ok ? '[ok] ' : '[FAIL]'} ${f.name} — ${f.detail}`)].join('\n') + '\n');
196
+ }
197
+ return report.ok ? 0 : 1;
198
+ }
199
+ const missing: string[] = [];
200
+ if (!args.origin) missing.push('--origin');
201
+ if (!args.address) missing.push('--address');
202
+ if (args.pinnedKeys.length === 0) missing.push('--pinned-key');
203
+ if (missing.length > 0) {
204
+ process.stderr.write(`missing required argument(s): ${missing.join(', ')}\n\n${usage()}\n`);
205
+ return 2;
206
+ }
207
+ const report = await verifyTrustAnchor({
208
+ origin: args.origin!,
209
+ address: args.address!,
210
+ pinnedPublicKeysBase64: args.pinnedKeys,
211
+ minaGraphqlUrl: args.minaGraphql ?? DEFAULT_GRAPHQL[args.minaNetworkId],
212
+ minaNetworkId: args.minaNetworkId,
213
+ skipChainChecks: args.skipChainChecks,
214
+ crossCheck: args.crossCheck,
215
+ });
216
+ if (args.json) {
217
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
218
+ } else {
219
+ process.stdout.write(`${formatHuman(report)}\n`);
220
+ }
221
+ return report.ok ? 0 : 1;
222
+ }
223
+
224
+ main().then(
225
+ (code) => process.exit(code),
226
+ (err) => {
227
+ process.stderr.write(`unexpected error: ${err?.stack ?? err}\n`);
228
+ process.exit(1);
229
+ },
230
+ );
231
+
232
+ // Silence unused-import warning when someone bundles the module (Finding is
233
+ // re-exported here for consumers that import from the CLI module directly).
234
+ export type { Finding };
@@ -0,0 +1,48 @@
1
+ // Copyright (c) 2025-2026 zkLicensing project developers
2
+ // SPDX-License-Identifier: BUSL-1.1
3
+
4
+ /**
5
+ * txBroadcastClient.ts
6
+ *
7
+ * Small helper for POST /tx-broadcast. Called by the buy/renew/migrate
8
+ * flows immediately after the buyer's wallet returns a txHash — signals
9
+ * the backend to extend the app's in-flight slot from the short signing-TTL
10
+ * to the longer landing-TTL, so concurrent buyers on the same app don't
11
+ * think the slot has expired while an honest signed tx is still in the
12
+ * mempool.
13
+ *
14
+ * Fire-and-forget by design: this is a tightening signal, not a
15
+ * correctness dependency. Land-clear in the buy-watcher and the
16
+ * signing-TTL both remain live regardless of whether this call succeeds.
17
+ * Failures are swallowed with an optional log callback so a network blip
18
+ * doesn't surface as a confusing error mid-checkout.
19
+ */
20
+
21
+ export type NotifyBroadcastRequest = {
22
+ keeperUrl: string;
23
+ appId: string;
24
+ txHash: string;
25
+ fetcher?: typeof fetch;
26
+ signal?: AbortSignal;
27
+ // Optional hook for surfacing swallowed failures to the caller's logger.
28
+ // Not called on the happy path.
29
+ onError?: (err: unknown) => void;
30
+ };
31
+
32
+ function baseUrl(url: string): string {
33
+ return url.replace(/\/+$/, '');
34
+ }
35
+
36
+ export async function notifyBroadcast(req: NotifyBroadcastRequest): Promise<void> {
37
+ const fetcher = req.fetcher ?? fetch;
38
+ try {
39
+ await fetcher(`${baseUrl(req.keeperUrl)}/tx-broadcast`, {
40
+ method: 'POST',
41
+ headers: { 'Content-Type': 'application/json' },
42
+ signal: req.signal,
43
+ body: JSON.stringify({ appId: req.appId, txHash: req.txHash }),
44
+ });
45
+ } catch (err) {
46
+ req.onError?.(err);
47
+ }
48
+ }
@@ -0,0 +1,233 @@
1
+ // Copyright (c) 2025-2026 zkLicensing project developers
2
+ // SPDX-License-Identifier: BUSL-1.1
3
+
4
+ /**
5
+ * unbindClient.ts
6
+ *
7
+ * Browser-safe client for the non-custodial device-unbinding flow.
8
+ * Mirrors `bindClient.ts` in shape: signs the unbind message with
9
+ * TAG_UNBINDING, POSTs to /prove/unbind, then /confirm/unbind after
10
+ * the wallet returns a txHash.
11
+ *
12
+ * Notable asymmetry with bind: the unbind message shape carries the
13
+ * deviceCommitment position too, but the backend and circuit both
14
+ * hard-code it to `Field(0)` (the "not about any specific new
15
+ * device" sentinel — the message is about freeing a slot, not
16
+ * introducing a new device). The SDK matches by omitting
17
+ * deviceCommitment from the request and using "0" when computing
18
+ * the signature preimage.
19
+ *
20
+ * Non-custodial invariant: the signature is re-verified in-circuit
21
+ * against the leaf's `licenseHash` (which IS the owner-pubkey
22
+ * commitment — the circuit asserts equality at buy time), so no
23
+ * backend / vendor can unilaterally kick a device — only the
24
+ * passphrase holder can.
25
+ * A user who lost the physical device but still has the passphrase
26
+ * can unbind the missing device's slot from any device that holds
27
+ * the ownership key (the "remote log-off" case).
28
+ */
29
+
30
+ import { Field, PrivateKey, PublicKey, Signature } from 'o1js';
31
+ import { readErrorBody, tryQueueBusyFromBody, keeperError } from './proveErrors.js';
32
+ import { encodePublicKey, encodeSignature, signUnbinding } from './ownershipSignature.js';
33
+ import { fetchBindingState } from './bindClient.js';
34
+
35
+ export type ProveUnbindRequest = {
36
+ keeperUrl: string;
37
+ network: 'devnet' | 'mainnet';
38
+ zkAppAddress: string;
39
+ licenseHash: string;
40
+ ownerPrivateKey: PrivateKey;
41
+ // Which slot (0..cap-1) to free.
42
+ slotIndex: number;
43
+ buyerAddress: string;
44
+ // Optional nonce override. Sampled fresh (128-bit random) when
45
+ // omitted — same shape as bindClient.
46
+ nonce?: string;
47
+ // Optional overrides for the state the signature commits to — same
48
+ // semantics as ProveBindRequest.slotState / expirySlot.
49
+ slotState?: string;
50
+ expirySlot?: number;
51
+ fetcher?: typeof fetch;
52
+ signal?: AbortSignal;
53
+ };
54
+
55
+ export type ProveUnbindResponse = {
56
+ provenTxJson: string;
57
+ licenseHash: string;
58
+ slotIndex: number;
59
+ };
60
+
61
+ export type ConfirmUnbindRequest = {
62
+ keeperUrl: string;
63
+ zkAppAddress: string;
64
+ licenseHash: string;
65
+ txHash: string;
66
+ fetcher?: typeof fetch;
67
+ signal?: AbortSignal;
68
+ };
69
+
70
+ function baseUrl(url: string): string {
71
+ return url.replace(/\/+$/, '');
72
+ }
73
+
74
+ async function readJsonError(resp: Response): Promise<string> {
75
+ try {
76
+ const body = await resp.json() as Record<string, unknown>;
77
+ if (typeof body.error === 'string') return body.error;
78
+ } catch { /* body wasn't JSON */ }
79
+ return `HTTP ${resp.status}`;
80
+ }
81
+
82
+ function freshNonce(): string {
83
+ const crypto = (globalThis as any).crypto as Crypto | undefined;
84
+ if (!crypto?.getRandomValues) {
85
+ throw new Error(
86
+ 'crypto.getRandomValues unavailable — unbindClient requires Node ≥ 20 or a modern browser',
87
+ );
88
+ }
89
+ const bytes = new Uint8Array(16); // 128 bits
90
+ crypto.getRandomValues(bytes);
91
+ let n = 0n;
92
+ for (const b of bytes) n = (n << 8n) | BigInt(b);
93
+ return Field(n).toString();
94
+ }
95
+
96
+ // POST /prove/unbind — same retry contract as /prove/bind. Returns
97
+ // 409 `ProveQueueBusyError` when another root-mutating tx for the
98
+ // same app is in flight; the caller retries with the server-supplied
99
+ // `retryAfterMs` cadence.
100
+ export async function proveUnbind(req: ProveUnbindRequest): Promise<ProveUnbindResponse> {
101
+ const fetcher = req.fetcher ?? fetch;
102
+ const nonce = req.nonce ?? freshNonce();
103
+ const { slotState, expirySlot } = (req.slotState !== undefined && req.expirySlot !== undefined)
104
+ ? { slotState: req.slotState, expirySlot: req.expirySlot }
105
+ : await fetchBindingState({ ...req, fetcher });
106
+
107
+ const ownerPubKey: PublicKey = req.ownerPrivateKey.toPublicKey();
108
+ // deviceCommitment=0 for the preimage — matches TAG_UNBINDING's
109
+ // circuit-side check (see verifyUnbinding / bindingLikeFields in
110
+ // ownershipSignature.ts).
111
+ const ownerSig: Signature = signUnbinding(req.ownerPrivateKey, {
112
+ licenseHash: req.licenseHash,
113
+ deviceCommitment: '0',
114
+ slotIndex: req.slotIndex,
115
+ nonce,
116
+ slotState,
117
+ expirySlot,
118
+ zkAppAddress: req.zkAppAddress,
119
+ });
120
+
121
+ const resp = await fetcher(`${baseUrl(req.keeperUrl)}/prove/unbind`, {
122
+ method: 'POST',
123
+ headers: { 'Content-Type': 'application/json' },
124
+ signal: req.signal,
125
+ body: JSON.stringify({
126
+ licenseHash: req.licenseHash,
127
+ buyerAddress: req.buyerAddress,
128
+ zkAppAddress: req.zkAppAddress,
129
+ network: req.network,
130
+ ownerPubKey: encodePublicKey(ownerPubKey),
131
+ ownerSig: encodeSignature(ownerSig),
132
+ slotIndex: req.slotIndex,
133
+ nonce,
134
+ }),
135
+ });
136
+ if (!resp.ok) {
137
+ const parsed = await readErrorBody(resp);
138
+ const queueBusy = tryQueueBusyFromBody(parsed);
139
+ if (queueBusy) throw queueBusy;
140
+ throw keeperError(parsed);
141
+ }
142
+ return await resp.json() as ProveUnbindResponse;
143
+ }
144
+
145
+ // POST /confirm/unbind — pass-through no-op mirror of /confirm/bind.
146
+ export async function confirmUnbind(req: ConfirmUnbindRequest): Promise<{ ok: true }> {
147
+ const fetcher = req.fetcher ?? fetch;
148
+ const resp = await fetcher(`${baseUrl(req.keeperUrl)}/confirm/unbind`, {
149
+ method: 'POST',
150
+ headers: { 'Content-Type': 'application/json' },
151
+ signal: req.signal,
152
+ body: JSON.stringify({
153
+ zkAppAddress: req.zkAppAddress,
154
+ licenseHash: req.licenseHash,
155
+ txHash: req.txHash,
156
+ }),
157
+ });
158
+ if (!resp.ok) throw new Error(await readJsonError(resp));
159
+ return { ok: true };
160
+ }
161
+
162
+ export type UnbindStatus = {
163
+ // 'confirmed' once eventSync has observed the DeviceUnbound event and
164
+ // the target slot's commitment has flipped back to '0'. 'pending' while
165
+ // the tx is in flight. 'unknown' if the license record hasn't been
166
+ // persisted yet.
167
+ status: 'confirmed' | 'pending' | 'unknown';
168
+ slotIndex?: number;
169
+ deviceCommitment?: string;
170
+ elapsedMs?: number;
171
+ };
172
+
173
+ export type PollUnbindStatusOptions = {
174
+ keeperUrl: string;
175
+ network: 'devnet' | 'mainnet';
176
+ zkAppAddress: string;
177
+ licenseHash: string;
178
+ slotIndex: number;
179
+ fetcher?: typeof fetch;
180
+ intervalMs?: number; // default 10 000
181
+ timeoutMs?: number; // default 20 min
182
+ signal?: AbortSignal;
183
+ onTick?: (status: UnbindStatus) => void;
184
+ };
185
+
186
+ // Polls GET /license/:zkAppAddress/:licenseHash until the unbind has
187
+ // landed on-chain (the target slot's commitment is '0') or the timeout
188
+ // fires. Mirrors pollBindStatus in shape.
189
+ export async function pollUnbindStatus(opts: PollUnbindStatusOptions): Promise<UnbindStatus> {
190
+ const fetcher = opts.fetcher ?? fetch;
191
+ const interval = opts.intervalMs ?? 10_000;
192
+ const timeout = opts.timeoutMs ?? 20 * 60 * 1000;
193
+ const startedAt = Date.now();
194
+ const deadline = startedAt + timeout;
195
+ const url = `${baseUrl(opts.keeperUrl)}/license/${encodeURIComponent(opts.zkAppAddress)}/${encodeURIComponent(opts.licenseHash)}?network=${opts.network}`;
196
+
197
+ while (true) {
198
+ if (opts.signal?.aborted) throw new Error('pollUnbindStatus aborted');
199
+ let status: UnbindStatus = { status: 'unknown', elapsedMs: Date.now() - startedAt };
200
+ try {
201
+ const resp = await fetcher(url, { signal: opts.signal });
202
+ if (resp.ok) {
203
+ const body = await resp.json() as {
204
+ found?: boolean;
205
+ devices?: Array<{ slotIndex: number; deviceCommitment: string }>;
206
+ };
207
+ if (body.found) {
208
+ const slot = (body.devices ?? []).find(d => d.slotIndex === opts.slotIndex);
209
+ const commitment = slot?.deviceCommitment ?? '0';
210
+ if (commitment === '0') {
211
+ status = {
212
+ status: 'confirmed',
213
+ slotIndex: opts.slotIndex,
214
+ deviceCommitment: commitment,
215
+ elapsedMs: Date.now() - startedAt,
216
+ };
217
+ } else {
218
+ status = {
219
+ status: 'pending',
220
+ slotIndex: opts.slotIndex,
221
+ deviceCommitment: commitment,
222
+ elapsedMs: Date.now() - startedAt,
223
+ };
224
+ }
225
+ }
226
+ }
227
+ } catch { /* transient network — try again on next tick */ }
228
+ opts.onTick?.(status);
229
+ if (status.status === 'confirmed') return status;
230
+ if (Date.now() >= deadline) return status;
231
+ await new Promise<void>((r) => setTimeout(r, interval));
232
+ }
233
+ }
package/src/units.ts ADDED
@@ -0,0 +1,74 @@
1
+ // Copyright (c) 2025-2026 zkLicensing project developers
2
+ // SPDX-License-Identifier: BUSL-1.1
3
+
4
+ /**
5
+ * units.ts — the unit amounts are shown in.
6
+ *
7
+ * Devnet amounts are test MINA (tMINA), which has no value; mainnet amounts
8
+ * are MINA. Only what people read changes: field names (`priceYearlyMina`,
9
+ * nanomina amounts) stay the same on both networks. The MINA price in euros
10
+ * or dollars is real and still shown on devnet, but only as a reference for
11
+ * mainnet MINA (`fiatReference`), never as the value of tMINA.
12
+ */
13
+
14
+ import { isEuCountry } from './taxId.js';
15
+
16
+ export type MinaUnit = 'MINA' | 'tMINA';
17
+
18
+ /** 'tMINA' on devnet (and any test network), 'MINA' on mainnet. */
19
+ export function minaUnit(network: string): MinaUnit {
20
+ return network === 'mainnet' ? 'MINA' : 'tMINA';
21
+ }
22
+
23
+ /** "12.5 tMINA" / "12.5 MINA" for an amount in MINA (number or decimal string). */
24
+ export function formatMina(amountMina: number | string, network: string): string {
25
+ return `${amountMina} ${minaUnit(network)}`;
26
+ }
27
+
28
+ /** "12.5 tMINA" for an amount in nanomina. */
29
+ export function formatNanomina(nano: bigint | string | number, network: string): string {
30
+ const n = BigInt(nano);
31
+ const neg = n < 0n;
32
+ const a = neg ? -n : n;
33
+ const frac = (a % 1_000_000_000n).toString().padStart(9, '0').replace(/0+$/, '');
34
+ return `${neg ? '-' : ''}${(a / 1_000_000_000n).toString()}${frac ? '.' + frac : ''} ${minaUnit(network)}`;
35
+ }
36
+
37
+ /**
38
+ * The words that go with a fiat value. Mainnet: "≈ €2.10". Devnet: the same
39
+ * amount of real MINA, said as such ("as MINA ≈ €2.10"), plus a note that
40
+ * tMINA itself has no value.
41
+ */
42
+ export function fiatReference(network: string): { prefix: string; note: string | null } {
43
+ return network === 'mainnet'
44
+ ? { prefix: '≈', note: null }
45
+ : { prefix: 'as MINA ≈', note: 'tMINA has no value; this is what the same amount of mainnet MINA is worth.' };
46
+ }
47
+
48
+ export type FiatCurrency = 'eur' | 'usd' | 'gbp';
49
+ const SYMBOL: Record<FiatCurrency, string> = { eur: '€', usd: '$', gbp: '£' };
50
+
51
+ /**
52
+ * Which money a person sees amounts in, by their country: euros in the EU,
53
+ * pounds and euros in the UK, dollars in the US, euros and dollars elsewhere
54
+ * (or when the country is unknown).
55
+ */
56
+ export function fiatCurrencies(country?: string | null): FiatCurrency[] {
57
+ const c = String(country ?? '').trim().toUpperCase();
58
+ if (isEuCountry(c)) return ['eur'];
59
+ if (c === 'GB' || c === 'UK') return ['gbp', 'eur'];
60
+ if (c === 'US') return ['usd'];
61
+ return ['eur', 'usd'];
62
+ }
63
+
64
+ /** "€2.10", "-€26.78" for an amount of money. */
65
+ export function formatFiat(amount: number | string, currency: FiatCurrency, digits = 2): string {
66
+ const n = typeof amount === 'string' ? Number(amount) : amount;
67
+ return `${n < 0 ? '-' : ''}${SYMBOL[currency]}${Math.abs(n).toFixed(digits)}`;
68
+ }
69
+
70
+ /** "€2.10 · $2.30" for the given currencies (a currency without a rate is left out). */
71
+ export function formatFiatList(values: Partial<Record<FiatCurrency, number | string>>, currencies: FiatCurrency[], digits = 2): string {
72
+ return currencies.filter(c => values[c] !== undefined && values[c] !== null && values[c] !== '')
73
+ .map(c => formatFiat(values[c]!, c, digits)).join(' · ');
74
+ }
@@ -0,0 +1,100 @@
1
+ // Copyright (c) 2025-2026 zkLicensing project developers
2
+ // SPDX-License-Identifier: BUSL-1.1
3
+
4
+ /**
5
+ * vatRates.ts — `zklicensing/vat`
6
+ *
7
+ * Standard VAT rates (EU member states and the UK, with their history) and
8
+ * the VAT included in a VAT-inclusive price. Prices on zkLicensing are fixed
9
+ * per tier on-chain and include VAT: a buyer pays the listed price, and the
10
+ * VAT in it is at the buyer's country's standard rate on the sale date.
11
+ *
12
+ * standardVatRate('FI', '2026-10-13') → 25.5
13
+ * vatIncludedNano(10_000_000_000n, 25.5) → { netNano: 7_968_127_490n, vatNano: 2_031_872_510n }
14
+ *
15
+ * For vendors' own reporting and buyers' information — an estimate for
16
+ * consumer sales, not tax advice: business buyers with a VAT number are
17
+ * usually reverse-charged, and the EU's €10,000 cross-border threshold is
18
+ * not applied. Countries without VAT in the table give null.
19
+ */
20
+
21
+ import { STANDARD_VAT_RATES, VAT_RATES_FETCHED, VAT_RATES_SOURCE } from './vatRatesData.js';
22
+
23
+ export { VAT_RATES_FETCHED, VAT_RATES_SOURCE };
24
+
25
+ function isoDay(date: Date | string | number): string {
26
+ const d = date instanceof Date ? date : new Date(date);
27
+ if (!Number.isFinite(d.getTime())) throw new Error('invalid date');
28
+ return d.toISOString().slice(0, 10);
29
+ }
30
+
31
+ /** The standard VAT rate (percent) in force in `country` on `date`, or null if the country isn't in the table. */
32
+ export function standardVatRate(country: string, date: Date | string | number = new Date()): number | null {
33
+ const c = String(country ?? '').toUpperCase();
34
+ const periods = Object.prototype.hasOwnProperty.call(STANDARD_VAT_RATES, c) ? STANDARD_VAT_RATES[c] : undefined;
35
+ if (!periods) return null;
36
+ const day = isoDay(date);
37
+ let rate: number | null = null;
38
+ for (const [from, r] of periods) if (from <= day) rate = r;
39
+ return rate;
40
+ }
41
+
42
+ /** Countries in the table (ISO codes). */
43
+ export function vatCountries(): string[] {
44
+ return Object.keys(STANDARD_VAT_RATES);
45
+ }
46
+
47
+ const bp = (ratePct: number) => {
48
+ if (!Number.isFinite(ratePct) || ratePct < 0 || ratePct >= 100) throw new Error('rate must be a percentage in [0, 100)');
49
+ return BigInt(Math.round(ratePct * 100));
50
+ };
51
+
52
+ /** The VAT included in a VAT-inclusive `totalNano` at `ratePct`: vat = total × r / (100 + r), rounded to the nearest nanomina. */
53
+ export function vatIncludedNano(totalNano: bigint, ratePct: number): { netNano: bigint; vatNano: bigint } {
54
+ if (totalNano < 0n) throw new Error('total must not be negative');
55
+ const r = bp(ratePct);
56
+ const den = 10_000n + r;
57
+ const vatNano = (totalNano * r * 2n + den) / (den * 2n);
58
+ return { netNano: totalNano - vatNano, vatNano };
59
+ }
60
+
61
+ /** Same, for a MINA amount (display only). */
62
+ export function vatIncludedMina(totalMina: number, ratePct: number): { net: number; vat: number } {
63
+ const { netNano, vatNano } = vatIncludedNano(BigInt(Math.round(totalMina * 1e9)), ratePct);
64
+ return { net: Number(netNano) / 1e9, vat: Number(vatNano) / 1e9 };
65
+ }
66
+
67
+ /** The price shown to a buyer in `country` on `date`: the total, and the VAT it includes. Null rate outside the table. */
68
+ export function priceWithVat(totalNano: bigint, country: string, date: Date | string | number = new Date()):
69
+ { totalNano: bigint; country: string; rate: number | null; netNano: bigint; vatNano: bigint } {
70
+ const rate = standardVatRate(country, date);
71
+ const split = rate === null ? { netNano: totalNano, vatNano: 0n } : vatIncludedNano(totalNano, rate);
72
+ return { totalNano, country: String(country).toUpperCase(), rate, ...split };
73
+ }
74
+
75
+ /** The VAT block of a license receipt (amounts as decimal MINA strings). */
76
+ export type VatReceipt = {
77
+ country: string;
78
+ /** Standard rate (percent) on the sale date; null when the country has no VAT in the table. */
79
+ ratePct: number | null;
80
+ totalMina: string;
81
+ vatMina: string;
82
+ netMina: string;
83
+ /** Which rates table was used. */
84
+ rates: string;
85
+ };
86
+
87
+ const minaString = (nano: bigint) => {
88
+ const s = nano.toString().padStart(10, '0');
89
+ return `${s.slice(0, -9)}.${s.slice(-9)}`.replace(/\.?0+$/, '') || '0';
90
+ };
91
+
92
+ /** The VAT included in a purchase of `totalNano` by a buyer in `country` on `date`, for the license file. */
93
+ export function vatReceipt(totalNano: bigint, country: string, date: Date | string | number = new Date()): VatReceipt {
94
+ const p = priceWithVat(totalNano, country, date);
95
+ return {
96
+ country: p.country, ratePct: p.rate,
97
+ totalMina: minaString(p.totalNano), vatMina: minaString(p.vatNano), netMina: minaString(p.netNano),
98
+ rates: `${VAT_RATES_SOURCE}, ${VAT_RATES_FETCHED}`,
99
+ };
100
+ }