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,249 @@
1
+ // Copyright (c) 2025-2026 zkLicensing project developers
2
+ // SPDX-License-Identifier: BUSL-1.1
3
+
4
+ /**
5
+ * deviceIdentity.ts
6
+ *
7
+ * Per-install Pallas Schnorr keypair used to identify a device to the
8
+ * non-custodial binding flow. `getOrCreateDeviceIdentity(storageKey)`
9
+ * returns a stable keypair for the caller's storage key: created on
10
+ * first call, loaded verbatim on every subsequent call.
11
+ *
12
+ * The `deviceCommitment` this module exposes matches the on-chain
13
+ * `occupied[i]` field shape written by `LicensingApp.bind()` — it's
14
+ * `Poseidon([pk.x, pk.isOdd.toField()])` over the device's Pallas
15
+ * pubkey (see `ownerPubKeyHash` in ./licenseLeaf.ts for the identical
16
+ * shape applied to the ownership pubkey). Callers pass the commitment
17
+ * to `proveBind` and to the ownership signature; the raw device pubkey
18
+ * itself never crosses the wire.
19
+ *
20
+ * Storage tradeoffs (documented up front — the plan's original
21
+ * assumption was WebCrypto extractable=false, which only applies to
22
+ * WebCrypto-native primitives):
23
+ *
24
+ * Browser path — IndexedDB.
25
+ * Pallas is not a WebCrypto primitive, so we cannot store the
26
+ * private key as a non-extractable CryptoKey. The private key is
27
+ * persisted as its Mina canonical base58 encoding. An adversary
28
+ * with access to the browser profile (malware running as the user,
29
+ * evil browser extension) can read it. This matches the trust
30
+ * model of every other browser-persisted secret today (bearer
31
+ * cookies, IndexedDB tokens); binding to a fresh device after
32
+ * compromise is a keypair regen + one on-chain bind — no funds
33
+ * at risk, only slot churn. Users who want stronger storage can
34
+ * wipe the storageKey between sessions and re-bind on demand.
35
+ *
36
+ * Node / Electron path — keytar (OS keychain).
37
+ * `keytar` is loaded lazily and marked as an optional peer
38
+ * dependency in package.json so the browser bundle isn't dragged
39
+ * into installing native bindings it can't use. If keytar is
40
+ * unavailable at runtime, the SDK throws with a clear message
41
+ * rather than falling back to plaintext-on-disk.
42
+ *
43
+ * The keypair is Pallas-scalar-random, NOT passphrase-derived — the
44
+ * device identity is meant to survive a passphrase reset (the user
45
+ * can re-derive their ownership key from the passphrase, then unbind
46
+ * / rebind existing device slots without re-installing the app).
47
+ */
48
+
49
+ import { PrivateKey, PublicKey, Poseidon } from 'o1js';
50
+
51
+ // ── Storage key namespacing ───────────────────────────────────────
52
+
53
+ // All device identities live under this shared prefix in whatever
54
+ // backing store is chosen. Callers pass a per-app / per-installation
55
+ // suffix (e.g. the app's short name) so a single browser profile can
56
+ // hold distinct device identities for distinct vendors' apps.
57
+ const STORAGE_PREFIX = 'zklic_device_';
58
+
59
+ function namespaced(storageKey: string): string {
60
+ return `${STORAGE_PREFIX}${storageKey}`;
61
+ }
62
+
63
+ // ── The identity shape callers get back ──────────────────────────
64
+
65
+ export type DeviceIdentity = {
66
+ devicePrivateKey: PrivateKey;
67
+ devicePubKey: PublicKey;
68
+ // Poseidon([pk.x, pk.isOdd.toField()]) as a decimal Field string.
69
+ // Same shape the on-chain `occupied[i]` slot stores, and the value
70
+ // /prove/bind expects in its request body. Callers pass this
71
+ // string directly — no further hashing at the call site.
72
+ deviceCommitment: string;
73
+ };
74
+
75
+ /**
76
+ * Compute the on-chain-shaped device commitment from a Pallas pubkey.
77
+ * Exported so tests and downstream tooling can re-derive it without
78
+ * re-instantiating the whole DeviceIdentity.
79
+ */
80
+ export function computeDeviceCommitment(devicePubKey: PublicKey): string {
81
+ return Poseidon.hash([devicePubKey.x, devicePubKey.isOdd.toField()]).toString();
82
+ }
83
+
84
+ function buildIdentity(privateKey: PrivateKey): DeviceIdentity {
85
+ const devicePubKey = privateKey.toPublicKey();
86
+ return {
87
+ devicePrivateKey: privateKey,
88
+ devicePubKey,
89
+ deviceCommitment: computeDeviceCommitment(devicePubKey),
90
+ };
91
+ }
92
+
93
+ // ── Environment detection ─────────────────────────────────────────
94
+
95
+ function hasIndexedDB(): boolean {
96
+ return typeof globalThis !== 'undefined'
97
+ && typeof (globalThis as any).indexedDB !== 'undefined';
98
+ }
99
+
100
+ function isNodeLike(): boolean {
101
+ // Node exposes `process.versions.node`; Deno/Bun don't (their
102
+ // process shims omit this key). Browsers have no `process` global
103
+ // unless something's polyfilled it, but the indexedDB branch runs
104
+ // first so that ordering is fine.
105
+ return typeof (globalThis as any).process !== 'undefined'
106
+ && typeof (globalThis as any).process?.versions?.node === 'string';
107
+ }
108
+
109
+ // ── IndexedDB backing store (browser) ─────────────────────────────
110
+
111
+ // Small promisified wrapper — no third-party idb library, to keep the
112
+ // SDK dependency surface minimal. Single key/value store; the whole
113
+ // database is one object store with `storageKey` as the key.
114
+ const IDB_DB_NAME = 'zklicensing';
115
+ const IDB_STORE_NAME = 'device_identities';
116
+ const IDB_VERSION = 1;
117
+
118
+ function openIdb(): Promise<IDBDatabase> {
119
+ return new Promise((resolve, reject) => {
120
+ const req = (globalThis as any).indexedDB.open(IDB_DB_NAME, IDB_VERSION);
121
+ req.onupgradeneeded = () => {
122
+ const db = req.result as IDBDatabase;
123
+ if (!db.objectStoreNames.contains(IDB_STORE_NAME)) {
124
+ db.createObjectStore(IDB_STORE_NAME);
125
+ }
126
+ };
127
+ req.onsuccess = () => resolve(req.result);
128
+ req.onerror = () => reject(req.error ?? new Error('IndexedDB open failed'));
129
+ });
130
+ }
131
+
132
+ async function idbGet(key: string): Promise<string | null> {
133
+ const db = await openIdb();
134
+ try {
135
+ return await new Promise<string | null>((resolve, reject) => {
136
+ const tx = db.transaction(IDB_STORE_NAME, 'readonly');
137
+ const store = tx.objectStore(IDB_STORE_NAME);
138
+ const req = store.get(key);
139
+ req.onsuccess = () => resolve((req.result as string | undefined) ?? null);
140
+ req.onerror = () => reject(req.error ?? new Error('IndexedDB get failed'));
141
+ });
142
+ } finally {
143
+ db.close();
144
+ }
145
+ }
146
+
147
+ async function idbPut(key: string, value: string): Promise<void> {
148
+ const db = await openIdb();
149
+ try {
150
+ await new Promise<void>((resolve, reject) => {
151
+ const tx = db.transaction(IDB_STORE_NAME, 'readwrite');
152
+ const store = tx.objectStore(IDB_STORE_NAME);
153
+ const req = store.put(value, key);
154
+ req.onsuccess = () => resolve();
155
+ req.onerror = () => reject(req.error ?? new Error('IndexedDB put failed'));
156
+ });
157
+ } finally {
158
+ db.close();
159
+ }
160
+ }
161
+
162
+ // ── keytar backing store (Node / Electron) ────────────────────────
163
+
164
+ // Loaded via dynamic import so a browser bundler doesn't try to
165
+ // resolve the native module. If keytar isn't installed we throw with
166
+ // a message that tells the caller which optional peer dep to add,
167
+ // rather than silently persisting to plaintext.
168
+ async function loadKeytar(): Promise<any> {
169
+ try {
170
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
171
+ return await (Function('return import("keytar")')() as Promise<any>);
172
+ } catch {
173
+ throw new Error(
174
+ 'deviceIdentity requires the optional peer dependency `keytar` in Node/Electron. ' +
175
+ 'Install it with `npm i keytar` alongside the zklicensing SDK.',
176
+ );
177
+ }
178
+ }
179
+
180
+ // keytar's shape: getPassword(service, account) / setPassword(service, account, value).
181
+ // We use a single service name and let `storageKey` be the account, so
182
+ // multiple apps under the same node install can coexist under distinct
183
+ // storage keys without stomping each other.
184
+ const KEYTAR_SERVICE = 'zklicensing-device-identity';
185
+
186
+ async function keytarGet(key: string): Promise<string | null> {
187
+ const keytar = await loadKeytar();
188
+ const mod = (keytar.default ?? keytar) as { getPassword: (s: string, a: string) => Promise<string | null> };
189
+ return mod.getPassword(KEYTAR_SERVICE, key);
190
+ }
191
+
192
+ async function keytarPut(key: string, value: string): Promise<void> {
193
+ const keytar = await loadKeytar();
194
+ const mod = (keytar.default ?? keytar) as { setPassword: (s: string, a: string, p: string) => Promise<void> };
195
+ await mod.setPassword(KEYTAR_SERVICE, key, value);
196
+ }
197
+
198
+ // ── Backend selection ────────────────────────────────────────────
199
+
200
+ type Backend = {
201
+ get: (key: string) => Promise<string | null>;
202
+ put: (key: string, value: string) => Promise<void>;
203
+ };
204
+
205
+ function pickBackend(): Backend {
206
+ if (hasIndexedDB()) return { get: idbGet, put: idbPut };
207
+ if (isNodeLike()) return { get: keytarGet, put: keytarPut };
208
+ throw new Error(
209
+ 'deviceIdentity: no suitable storage backend detected. ' +
210
+ 'Expected IndexedDB in the browser or a Node/Electron runtime with `keytar` installed.',
211
+ );
212
+ }
213
+
214
+ // ── Public API ────────────────────────────────────────────────────
215
+
216
+ /**
217
+ * Load an existing device keypair for `storageKey`, or create + persist
218
+ * a fresh one on first call. Idempotent across every subsequent call
219
+ * for the same key on the same install.
220
+ *
221
+ * The `storageKey` should be per-app (e.g. the vendor's app short
222
+ * name or its `appId`), so a single browser profile / OS user account
223
+ * can hold distinct device identities for distinct vendors without
224
+ * cross-contamination.
225
+ */
226
+ export async function getOrCreateDeviceIdentity(storageKey: string): Promise<DeviceIdentity> {
227
+ if (!storageKey || typeof storageKey !== 'string') {
228
+ throw new Error('getOrCreateDeviceIdentity: storageKey must be a non-empty string');
229
+ }
230
+ const backend = pickBackend();
231
+ const key = namespaced(storageKey);
232
+
233
+ const existing = await backend.get(key);
234
+ if (existing) {
235
+ try {
236
+ const privateKey = PrivateKey.fromBase58(existing);
237
+ return buildIdentity(privateKey);
238
+ } catch {
239
+ // Corrupt persisted value — fall through and mint a fresh key,
240
+ // overwriting the bad blob. Losing a device identity here is
241
+ // recoverable: the caller re-binds the new keypair and unbinds
242
+ // the old slot from any other device that still has the
243
+ // ownership key.
244
+ }
245
+ }
246
+ const privateKey = PrivateKey.random();
247
+ await backend.put(key, privateKey.toBase58());
248
+ return buildIdentity(privateKey);
249
+ }
@@ -0,0 +1,162 @@
1
+ // Copyright (c) 2025-2026 zkLicensing project developers
2
+ // SPDX-License-Identifier: BUSL-1.1
3
+
4
+ /**
5
+ * freezeClient.ts
6
+ *
7
+ * Client-side view of the backend's sales-freeze schedule. A freeze is set
8
+ * ahead of a hardfork so every open escrow window can drain (refund or
9
+ * release) before v1 → v2 migration. Once past the freeze slot the backend
10
+ * refuses new buys and renewals; refunds and verify remain open.
11
+ *
12
+ * Resolution order for buy/renew clients that want to know the current
13
+ * freeze state:
14
+ * 1. Caller-supplied `freezeAfterSlot` (explicit override — build-time
15
+ * constant, or pulled from the vendor's own env at boot).
16
+ * 2. `process.env.FREEZE_AFTER_SLOT` when running under Node (server-side
17
+ * integrators / CLIs).
18
+ * 3. Backend's own `/health` payload — the authoritative source of truth.
19
+ *
20
+ * A `freezeAfterSlot` of 0 / null / undefined means "no freeze scheduled".
21
+ */
22
+
23
+ export type NetworkHealth = {
24
+ graphqlUrl?: string;
25
+ archiveUrl?: string;
26
+ slot: number;
27
+ salesFrozen: boolean;
28
+ freezeAfterSlot: number | null;
29
+ };
30
+
31
+ // GET /health returns one block per configured network:
32
+ // { status: 'ok', devnet?: NetworkHealth, mainnet?: NetworkHealth }
33
+ // Older keepers returned a single flat block; those fields stay optional here
34
+ // so both shapes typecheck. Use pickNetworkHealth() rather than reading
35
+ // either shape directly.
36
+ export type KeeperHealth = {
37
+ status: string;
38
+ devnet?: NetworkHealth;
39
+ mainnet?: NetworkHealth;
40
+ /** @deprecated legacy flat shape */
41
+ network?: string;
42
+ /** @deprecated legacy flat shape */
43
+ slot?: number;
44
+ /** @deprecated legacy flat shape */
45
+ salesFrozen?: boolean;
46
+ /** @deprecated legacy flat shape */
47
+ freezeAfterSlot?: number | null;
48
+ };
49
+
50
+ /**
51
+ * The health block for `network`. Without a network: the only configured
52
+ * one, or null when several are configured (ambiguous). Falls back to the
53
+ * legacy flat shape.
54
+ */
55
+ export function pickNetworkHealth(health: KeeperHealth, network?: 'devnet' | 'mainnet'): NetworkHealth | null {
56
+ const nested = (['devnet', 'mainnet'] as const).filter(n => health[n] && typeof health[n]!.slot === 'number');
57
+ if (nested.length > 0) {
58
+ if (network) return health[network] ?? null;
59
+ return nested.length === 1 ? health[nested[0]]! : null;
60
+ }
61
+ if (typeof health.slot === 'number' || typeof health.salesFrozen === 'boolean') {
62
+ return {
63
+ slot: typeof health.slot === 'number' ? health.slot : 0,
64
+ salesFrozen: health.salesFrozen === true,
65
+ freezeAfterSlot: typeof health.freezeAfterSlot === 'number' ? health.freezeAfterSlot : null,
66
+ };
67
+ }
68
+ return null;
69
+ }
70
+
71
+ export type FreezeState = {
72
+ // True when the current slot is past freezeAfterSlot — new buys and
73
+ // renewals are refused backend-side.
74
+ salesFrozen: boolean;
75
+ // Scheduled freeze slot (announced ahead of a hardfork). Null when no
76
+ // freeze is configured. Buy clients should require an explicit
77
+ // refund-window waiver only when the buyer's 14-day refund window would
78
+ // extend past this slot (i.e. currentSlot + REFUND_WINDOW_N > freezeAfterSlot).
79
+ freezeAfterSlot: number | null;
80
+ // Chain slot at resolution time. Populated from /health; null on the
81
+ // caller-override / env paths where the slot is not known client-side.
82
+ // Callers computing refund-window overlap should treat null
83
+ // conservatively (require the waiver) — the backend's own guard is still
84
+ // the last line of defence.
85
+ currentSlot: number | null;
86
+ };
87
+
88
+ function baseUrl(url: string): string {
89
+ return url.replace(/\/+$/, '');
90
+ }
91
+
92
+ function envFreezeSlot(): number | null {
93
+ // Runs both in Node (process.env) and browser (no `process`). The typeof
94
+ // guard keeps bundlers from choking on the reference.
95
+ if (typeof process === 'undefined' || !process.env) return null;
96
+ const raw = process.env.FREEZE_AFTER_SLOT;
97
+ if (!raw) return null;
98
+ const n = parseInt(raw, 10);
99
+ return Number.isFinite(n) && n > 0 ? n : null;
100
+ }
101
+
102
+ export type GetKeeperHealthOptions = {
103
+ keeperUrl: string;
104
+ fetcher?: typeof fetch;
105
+ signal?: AbortSignal;
106
+ };
107
+
108
+ // GET /health — raw payload. Callers that only need the freeze state
109
+ // should use resolveFreezeState() instead; this is exposed for UIs that
110
+ // surface slot / network info alongside freeze.
111
+ export async function getKeeperHealth(opts: GetKeeperHealthOptions): Promise<KeeperHealth> {
112
+ const fetcher = opts.fetcher ?? fetch;
113
+ const resp = await fetcher(`${baseUrl(opts.keeperUrl)}/health`, { signal: opts.signal });
114
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
115
+ return await resp.json() as KeeperHealth;
116
+ }
117
+
118
+ export type ResolveFreezeStateOptions = {
119
+ keeperUrl: string;
120
+ // Network the action targets. Needed to read the right block of a
121
+ // multi-network /health response.
122
+ network?: 'devnet' | 'mainnet';
123
+ // Explicit override (highest precedence). Null/undefined falls through
124
+ // to env, then /health.
125
+ freezeAfterSlot?: number | null;
126
+ fetcher?: typeof fetch;
127
+ signal?: AbortSignal;
128
+ };
129
+
130
+ // Resolves the effective freeze state for the caller. When the source is
131
+ // the caller override or process.env, the current slot is not known
132
+ // client-side, so salesFrozen is filled from /health only — the override
133
+ // path returns { freezeAfterSlot: N, salesFrozen: false, currentSlot: null }
134
+ // and lets the backend reject the actual prove call if the freeze has fired.
135
+ export async function resolveFreezeState(opts: ResolveFreezeStateOptions): Promise<FreezeState> {
136
+ if (typeof opts.freezeAfterSlot === 'number' && opts.freezeAfterSlot > 0) {
137
+ return { salesFrozen: false, freezeAfterSlot: opts.freezeAfterSlot, currentSlot: null };
138
+ }
139
+ const envSlot = envFreezeSlot();
140
+ if (envSlot !== null) {
141
+ return { salesFrozen: false, freezeAfterSlot: envSlot, currentSlot: null };
142
+ }
143
+ try {
144
+ const health = await getKeeperHealth({
145
+ keeperUrl: opts.keeperUrl,
146
+ fetcher: opts.fetcher,
147
+ signal: opts.signal,
148
+ });
149
+ const net = pickNetworkHealth(health, opts.network);
150
+ if (!net) return { salesFrozen: false, freezeAfterSlot: null, currentSlot: null };
151
+ return {
152
+ salesFrozen: net.salesFrozen === true,
153
+ freezeAfterSlot: typeof net.freezeAfterSlot === 'number' && net.freezeAfterSlot > 0 ? net.freezeAfterSlot : null,
154
+ currentSlot: typeof net.slot === 'number' && net.slot > 0 ? net.slot : null,
155
+ };
156
+ } catch {
157
+ // /health unreachable — treat as no freeze so we don't block buys on a
158
+ // transient network hiccup. The backend's own guard is still the last
159
+ // line of defense.
160
+ return { salesFrozen: false, freezeAfterSlot: null, currentSlot: null };
161
+ }
162
+ }
@@ -0,0 +1,145 @@
1
+ // Copyright (c) 2025-2026 zkLicensing project developers
2
+ // SPDX-License-Identifier: BUSL-1.1
3
+
4
+ /**
5
+ * generationClient.ts
6
+ *
7
+ * Browser-safe fetcher for the backend's signed generation-list. Returns the
8
+ * app's deployment history — every past address plus the current live one —
9
+ * verified against a caller-supplied pin list of platform manifest public
10
+ * keys.
11
+ *
12
+ * Trust model: the backend is not the trust anchor for this data — it can
13
+ * be re-hosted, mirrored, cached, or (worst case) impersonated. The pin
14
+ * list is. Any response that fails signature verification is surfaced as
15
+ * `verified: false` with `generations: []` so callers cannot accidentally
16
+ * route a wallet prompt to an unverified address.
17
+ *
18
+ * Optional corroboration channel: an operator that publishes
19
+ * https://<vendor-host>/.well-known/zklicensing/<zkAppAddress>.json
20
+ * (using the same backend-signed payload) can be cross-checked from the
21
+ * on-chain zkappUri. One file per deployed address (so per generation),
22
+ * because a vendor may host several apps at one origin. The cross-check is opt-in per-app — a 404 at that
23
+ * URL is not a failure signal, just "no corroboration published for this
24
+ * app." That's the caller's job, not this module's.
25
+ */
26
+
27
+ import { verifyManifest } from './manifestVerify.js';
28
+
29
+ export interface GenerationEntry {
30
+ gen: number;
31
+ zkAppAddress: string;
32
+ status: 'retired' | 'live';
33
+ frozenRoot?: string;
34
+ deployedAt?: string;
35
+ retiredAt?: string;
36
+ }
37
+
38
+ export interface GenerationInfo {
39
+ /** Live-generation index (matches the entry with status: 'live'). Null when unverified or missing. */
40
+ current: number | null;
41
+ generations: GenerationEntry[];
42
+ /** True iff signature verified against one of the pinned pubkeys. */
43
+ verified: boolean;
44
+ }
45
+
46
+ export interface FetchGenerationRequest {
47
+ keeperUrl: string;
48
+ network: 'devnet' | 'mainnet';
49
+ appId: string;
50
+ /** Base64-encoded Ed25519 public keys pinned by the caller. Empty ⇒ verified:false. */
51
+ pinnedPublicKeysBase64: readonly string[];
52
+ fetcher?: typeof fetch;
53
+ signal?: AbortSignal;
54
+ }
55
+
56
+ export interface FetchGenerationByAddressRequest {
57
+ keeperUrl: string;
58
+ /** When given, a list signed for the other network is refused. */
59
+ network?: 'devnet' | 'mainnet';
60
+ /** Any zkApp address in the app's generation chain — current live or any retired. */
61
+ zkAppAddress: string;
62
+ /** Base64-encoded Ed25519 public keys pinned by the caller. Empty ⇒ verified:false. */
63
+ pinnedPublicKeysBase64: readonly string[];
64
+ fetcher?: typeof fetch;
65
+ signal?: AbortSignal;
66
+ }
67
+
68
+ function baseUrl(url: string): string {
69
+ return url.replace(/\/+$/, '');
70
+ }
71
+
72
+ /**
73
+ * The verified generation list in a keeper response: a platform signature
74
+ * from a pinned key (current or, during a key rotation, the previous one)
75
+ * over the signed fields, for `network` when given. Unverified data is
76
+ * never surfaced: a caller must not route a wallet prompt to an address we
77
+ * couldn't authenticate.
78
+ */
79
+ async function verifyAndProject(
80
+ body: unknown,
81
+ pinnedPublicKeysBase64: readonly string[],
82
+ network?: 'devnet' | 'mainnet',
83
+ ): Promise<GenerationInfo> {
84
+ const signed = await verifyManifest(body, pinnedPublicKeysBase64, network ? { network } : {});
85
+ if (!signed) return { current: null, generations: [], verified: false };
86
+ return { current: signed.current, generations: signed.generations as GenerationEntry[], verified: true };
87
+ }
88
+
89
+ /**
90
+ * GET /apps/:appId — returns { appId, current, generations, issuedAt, publicKey, signature }.
91
+ * We reconstruct the canonical payload (everything except `publicKey` +
92
+ * `signature`) and verify against the pin list. `publicKey` in the response
93
+ * is advisory only — it's the key the backend claims to have signed with;
94
+ * the SDK never trusts it in place of a pin.
95
+ */
96
+ export async function fetchGenerationInfo(req: FetchGenerationRequest): Promise<GenerationInfo> {
97
+ const fetcher = req.fetcher ?? fetch;
98
+ const resp = await fetcher(`${baseUrl(req.keeperUrl)}/apps/${encodeURIComponent(req.appId)}?network=${req.network}`, {
99
+ method: 'GET',
100
+ signal: req.signal,
101
+ });
102
+ if (!resp.ok) {
103
+ return { current: null, generations: [], verified: false };
104
+ }
105
+ let body: Record<string, unknown>;
106
+ try { body = await resp.json() as Record<string, unknown>; }
107
+ catch { return { current: null, generations: [], verified: false }; }
108
+
109
+ return verifyAndProject(body, req.pinnedPublicKeysBase64, req.network);
110
+ }
111
+
112
+ /**
113
+ * GET /apps/by-address/:zkAppAddress — same signed payload as fetchGenerationInfo,
114
+ * but keyed by an address the caller already has (direct-paste buys, renew on a
115
+ * since-retired address). Buyers arriving without an appId would otherwise have
116
+ * to trust the backend's unsigned `/apps` list to bind their address to an appId
117
+ * first — defeating the pin.
118
+ *
119
+ * Extra assertion beyond the appId flow: the queried address MUST appear in the
120
+ * returned `generations[]`. Without this, a malicious backend could return a
121
+ * validly-signed payload for a DIFFERENT app in response to the query, and the
122
+ * signature check alone wouldn't catch it.
123
+ */
124
+ export async function fetchGenerationInfoByAddress(
125
+ req: FetchGenerationByAddressRequest,
126
+ ): Promise<GenerationInfo> {
127
+ const fetcher = req.fetcher ?? fetch;
128
+ const resp = await fetcher(
129
+ `${baseUrl(req.keeperUrl)}/apps/by-address/${encodeURIComponent(req.zkAppAddress)}`,
130
+ { method: 'GET', signal: req.signal },
131
+ );
132
+ if (!resp.ok) {
133
+ return { current: null, generations: [], verified: false };
134
+ }
135
+ let body: Record<string, unknown>;
136
+ try { body = await resp.json() as Record<string, unknown>; }
137
+ catch { return { current: null, generations: [], verified: false }; }
138
+
139
+ const projected = await verifyAndProject(body, req.pinnedPublicKeysBase64, req.network);
140
+ if (!projected.verified) return projected;
141
+ if (!projected.generations.some(g => g.zkAppAddress === req.zkAppAddress)) {
142
+ return { current: null, generations: [], verified: false };
143
+ }
144
+ return projected;
145
+ }
@@ -0,0 +1,91 @@
1
+ // Copyright (c) 2025-2026 zkLicensing project developers
2
+ // SPDX-License-Identifier: BUSL-1.1
3
+
4
+ /**
5
+ * holderSecret.ts — the permit holder's secret for anonymous proofs
6
+ * (see permitProof.ts). Light: no ZkProgram, so clients can derive it when
7
+ * applying without loading the proof.
8
+ */
9
+
10
+ import { Field, Poseidon, PublicKey } from 'o1js';
11
+ import { networkField, permitSubject, type PermitNetwork } from './permitMessages.js';
12
+
13
+ /** Tags for the holder secret's signed message and for the salt derived from it. */
14
+ export const HOLDER_SECRET_TAG: Field = Field([...'zkPermits/v1/holder-secret'].reduce((v, ch) => (v << 8n) | BigInt(ch.charCodeAt(0)), 0n));
15
+ export const HOLDER_SALT_TAG: Field = Field([...'zkPermits/v1/holder-salt'].reduce((v, ch) => (v << 8n) | BigInt(ch.charCodeAt(0)), 0n));
16
+
17
+ /** How many counter values recovery tries: one key rarely holds more permits in one registry. */
18
+ export const MAX_HOLDER_SECRETS = 16;
19
+
20
+ /**
21
+ * What the holder's wallet signs (a text signature, signMessage) to derive a
22
+ * secret: the registry's lineage, its network and a counter: 0 for the
23
+ * key's first permit in the registry, 1 for the next, and so on. The
24
+ * lineage is the registry's first generation's address (the keeper's
25
+ * `registry.lineage`; the address itself for a registry never redeployed),
26
+ * so a permit's secret stays the same when the registry moves to a new
27
+ * address. Readable in the wallet, and nothing needs to be kept: the holder
28
+ * re-derives the secret from the chain and the wallet alone (findHolderSecret).
29
+ */
30
+ export function holderSecretText(lineage: string, network: PermitNetwork, index: number): string {
31
+ if (!Number.isSafeInteger(index) || index < 0) throw new Error('holder secret index must be a non-negative integer');
32
+ PublicKey.fromBase58(lineage);
33
+ networkField(network);
34
+ return [
35
+ 'zkPermits: recover my permit secret',
36
+ `registry: ${lineage}`,
37
+ `network: ${network}`,
38
+ `permit number: ${index}`,
39
+ 'Sign this only on zkLicensing pages: it lets the signer recognise this permit\'s anonymous proofs.',
40
+ ].join('\n');
41
+ }
42
+
43
+ /** The holder secret from the wallet's (deterministic) text signature over holderSecretText. */
44
+ export function holderSecretFromSignature(sig: { field: string; scalar: string }): Field {
45
+ const scalar = BigInt(sig.scalar);
46
+ const low = scalar & ((1n << 128n) - 1n);
47
+ return Poseidon.hash([HOLDER_SECRET_TAG, Field(BigInt(sig.field)), Field(scalar >> 128n), Field(low)]);
48
+ }
49
+
50
+ /** The permit's salt: public to the keeper and the authority, and reveals nothing about the secret. */
51
+ export function saltFromHolderSecret(holderSecret: Field): Field {
52
+ return Poseidon.hash([HOLDER_SALT_TAG, holderSecret]);
53
+ }
54
+
55
+ type TextWallet = { address: string; signMessage(message: string): Promise<{ field: string; scalar: string }> };
56
+
57
+ /** Derives the secret, salt and permit subject for counter `index` (one wallet signature). */
58
+ export async function deriveHolderSecret(
59
+ wallet: TextWallet,
60
+ args: { lineage: string; network: PermitNetwork; index: number },
61
+ ): Promise<{ secret: string; salt: string; subject: string; index: number }> {
62
+ const signature = await wallet.signMessage(holderSecretText(args.lineage, args.network, args.index));
63
+ const secret = holderSecretFromSignature(signature);
64
+ const salt = saltFromHolderSecret(secret);
65
+ return { secret: secret.toString(), salt: salt.toString(), subject: permitSubject(PublicKey.fromBase58(wallet.address), salt).toString(), index: args.index };
66
+ }
67
+
68
+ /**
69
+ * Recovers the secret of the permit `subject` held by this wallet: signs for
70
+ * counter 0, 1, 2… until one gives that subject (usually the first). Throws
71
+ * when none does: another wallet, or a permit issued before secrets were
72
+ * derived this way.
73
+ */
74
+ export async function findHolderSecret(
75
+ wallet: TextWallet,
76
+ args: { lineage: string; network: PermitNetwork; subject: string; maxTries?: number },
77
+ ): Promise<{ secret: string; index: number }> {
78
+ for (let index = 0; index < (args.maxTries ?? MAX_HOLDER_SECRETS); index++) {
79
+ const d = await deriveHolderSecret(wallet, { lineage: args.lineage, network: args.network, index });
80
+ if (d.subject === args.subject) return { secret: d.secret, index };
81
+ }
82
+ throw new Error('This wallet derives no secret for this permit: is it the wallet the permit was applied for with, on this network?');
83
+ }
84
+
85
+ /**
86
+ * True when `holderSecret` belongs to the permit (subject = H(holderKey, salt(secret))).
87
+ * Check before proving, for a clear error instead of a failed proof.
88
+ */
89
+ export function holderSecretMatches(holderKey: string, subject: string, holderSecret: string): boolean {
90
+ return permitSubject(PublicKey.fromBase58(holderKey), saltFromHolderSecret(Field(holderSecret))).toString() === subject;
91
+ }