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
+ * statsClient.ts
6
+ *
7
+ * Browser-safe HTTP client for the vendor-facing backend endpoints that back
8
+ * the dashboard: GET /stats, GET /transactions, and GET /apps/vendor/:addr.
9
+ *
10
+ * Types mirror the backend's response shapes so a dashboard implementation
11
+ * (this repo's zklicensing.com dashboard or a third-party one) doesn't have
12
+ * to re-derive them. New optional fields on the server should be added here
13
+ * as `?:` so older clients still typecheck against a newer backend.
14
+ */
15
+
16
+ import type { CountryBreakdown } from './countryStats.js';
17
+ import type { FiatRateSnapshot } from './licenseRecord.js';
18
+
19
+ export type Tier = 'monthly' | 'yearly' | 'fiveYear' | 'perpetual';
20
+
21
+ export type AppRecord = {
22
+ appId: string;
23
+ name: string;
24
+ description: string;
25
+ longDescription?: string;
26
+ category: string;
27
+ website: string;
28
+ // Absent on `status: 'not-deployed'` stub rows (returned by
29
+ // GET /apps/vendor/:addr?network=… for apps the vendor owns but hasn't
30
+ // deployed on the queried network). Present on every other projection.
31
+ zkAppAddress?: string;
32
+ vendorAddress: string;
33
+ priceMonthlyMina: number;
34
+ priceYearlyMina: number;
35
+ priceFiveYearMina: number;
36
+ pricePerpetualMina: number;
37
+ // Also absent on `status: 'not-deployed'` stub rows. See zkAppAddress note.
38
+ txHash?: string;
39
+ verificationKeyHash?: string;
40
+ logo?: string;
41
+ // Best favicon URL for the vendor's website, resolved server-side by the
42
+ // backend (parses `<link rel="icon">` tags on the website). Undefined until
43
+ // the vendor's next edit for grandfathered apps — clients should fall back
44
+ // to `${host}/favicon.ico` in that case.
45
+ iconUrl?: string;
46
+ tags?: string[];
47
+ contactEmail?: string;
48
+ supportEmail?: string;
49
+ country?: string;
50
+ status?: 'pending' | 'active' | 'inactive' | 'rejected' | 'not-deployed';
51
+ rejectionReason?: string;
52
+ createdAt: string;
53
+ approvedAt?: string;
54
+ // Which network this record projection is scoped to. Present on responses
55
+ // from GET /apps?network=… and GET /apps/vendor/:addr?network=… — the backend
56
+ // stamps it so a dashboard rendering multiple projections doesn't have to
57
+ // track the query it made.
58
+ network?: 'devnet' | 'mainnet';
59
+ // Sibling networks this app is also deployed to. Empty or omitted when the
60
+ // app only exists on `network`. Dashboards use this to render "Also deploy
61
+ // to X →" / "Promote to X →" cross-origin CTAs.
62
+ alsoOnNetworks?: ('devnet' | 'mainnet')[];
63
+ // Per-deployment sync bookkeeping, flattened from AppRecord.deployments[network]
64
+ // by the backend's network-scoped list handlers.
65
+ firstEventSlot?: number;
66
+ lastSyncedSlot?: number;
67
+ onChainVersion?: number;
68
+ deploymentHistory?: unknown[];
69
+ pendingEdit?: Partial<AppRecord> & { editedAt: string };
70
+ // Append-only audit trail of status transitions. Optional so older backends
71
+ // that don't yet emit it still typecheck.
72
+ statusHistory?: { status: string; at: string; actor: string; note?: string }[];
73
+ // Vendor legal / billing block — collected at registration for compliance
74
+ // (CRD Art. 6, e-Commerce Directive Art. 5) and reused in the vendor's
75
+ // tax-year CSV export. All optional here so older backends that don't yet
76
+ // return these fields still typecheck. `country` (top-level above) is the
77
+ // billing country and pairs with `billingAddress` — the address struct
78
+ // intentionally omits a country field to avoid drift.
79
+ legalName?: string;
80
+ billingAddress?: { line1: string; line2?: string; city: string; postalCode: string };
81
+ invoiceEmail?: string;
82
+ vatId?: string;
83
+ companyRegistrationRegistry?: string;
84
+ companyRegistrationNumber?: string;
85
+ // Consumer-facing policy URLs displayed in SDK buy/renew/refund flows
86
+ // before the buyer signs (CRD Art. 6(1)(g)/(h) pre-contract disclosures).
87
+ // Vendors set them at registration; may be omitted on legacy records that
88
+ // pre-date the field.
89
+ termsOfServiceUrl?: string;
90
+ privacyPolicyUrl?: string;
91
+ refundPolicyUrl?: string;
92
+ // Registration fee (in MINA) actually paid at deploy time. Stamped from
93
+ // the backend's REGISTRATION_FEE constant when the record was created.
94
+ // Absent on pre-persistence records — consumers should fall back to the
95
+ // legacy flat 100 MINA fee for records without this field.
96
+ registrationFeeMina?: number;
97
+ };
98
+
99
+ export type VendorStatsResponse = {
100
+ apps: AppRecord[];
101
+ activeLicenses: number;
102
+ totalLicenses: number;
103
+ totalRevenueMina: number;
104
+ pendingRevenueMina?: number;
105
+ // Vendor's post-fee share of refunded purchases, in MINA. Optional so older
106
+ // backends (pre-refund-tracking) still typecheck against this SDK.
107
+ refundedRevenueMina?: number;
108
+ renewalRate: number | null;
109
+ monthlyRevenueMina: { month: string; mina: number }[];
110
+ licensesByTier?: { monthly: number; yearly: number; fiveYear: number; perpetual: number };
111
+ deltas: { revenuePct: number | null };
112
+ countryBreakdown?: CountryBreakdown;
113
+ };
114
+
115
+ export type Transaction = {
116
+ type: 'purchase' | 'renewal' | 'release' | 'refund';
117
+ appId: string;
118
+ appName: string;
119
+ // Only populated on the admin __all response — vendor-scoped queries omit it
120
+ // since the caller already knows the vendor from the query itself.
121
+ vendorAddress?: string;
122
+ tier: Tier | null;
123
+ amountMina: number;
124
+ txHash: string;
125
+ date: string; // ISO
126
+ // Buyer's ISO-3166 alpha-2 country at purchase (present on purchase rows and
127
+ // any downstream renewal/refund/release rows for the same record). Optional
128
+ // for pre-country-capture legacy rows.
129
+ buyerCountry?: string;
130
+ buyerZip?: string; // US buyers only (state-level sales-tax attribution)
131
+ // VAT evidence: the country of the buyer's IP address at the sale (null:
132
+ // unknown or private address); absent when the keeper made no lookup.
133
+ ipCountry?: string | null;
134
+ // MINA→fiat rates snapshotted at the moment the underlying purchase/renewal
135
+ // confirmed — same fields as the receipt email. Missing on archive-replayed
136
+ // records and on rows created before the first successful CoinGecko fetch.
137
+ fiatSnapshot?: FiatRateSnapshot;
138
+ // Present when an AI agent gave the buyer's withdrawal waiver under a
139
+ // standing consent the buyer signed (Terms §7.8).
140
+ waiverGivenBy?: { agent: string; owner: string; statementSha256: string; issuedAt: string };
141
+ };
142
+
143
+ export type TransactionsResponse = { transactions: Transaction[] };
144
+
145
+ export type VendorAppsResponse = { apps: AppRecord[] };
146
+
147
+ function baseUrl(url: string): string {
148
+ return url.replace(/\/+$/, '');
149
+ }
150
+
151
+ // Admin __all mode: pass `adminToken` and omit `vendorAddress` (or pass '__all').
152
+ // Fetches aggregate over every registered app. Server gates with Bearer auth.
153
+ // tzOffsetMinutes: signed minutes east of UTC — shifts tax-year cutovers on the
154
+ // countryBreakdown so a UK vendor's 2026 doesn't include a Dec 31 23:30 PST
155
+ // purchase. Default omitted → UTC bucketing preserved for existing callers.
156
+ export async function fetchVendorStats(opts: {
157
+ keeperUrl: string;
158
+ vendorAddress: string;
159
+ network: 'devnet' | 'mainnet';
160
+ adminToken?: string;
161
+ // Vendor dashboard-session bearer for the per-vendor read path. Mutually
162
+ // exclusive with adminToken (adminToken wins if both set — the '__all'
163
+ // path never has a session).
164
+ vendorSessionToken?: string;
165
+ tzOffsetMinutes?: number;
166
+ fetcher?: typeof fetch;
167
+ signal?: AbortSignal;
168
+ }): Promise<VendorStatsResponse | null> {
169
+ const fetcher = opts.fetcher ?? fetch;
170
+ const headers: Record<string, string> = {};
171
+ if (opts.adminToken) headers.Authorization = `Bearer ${opts.adminToken}`;
172
+ else if (opts.vendorSessionToken) headers.Authorization = `VendorSession ${opts.vendorSessionToken}`;
173
+ const tzQ = typeof opts.tzOffsetMinutes === 'number' ? `&tz=${opts.tzOffsetMinutes}` : '';
174
+ const resp = await fetcher(
175
+ `${baseUrl(opts.keeperUrl)}/stats?vendor=${encodeURIComponent(opts.vendorAddress)}&network=${opts.network}${tzQ}`,
176
+ { signal: opts.signal, headers },
177
+ );
178
+ if (!resp.ok) return null;
179
+ return await resp.json() as VendorStatsResponse;
180
+ }
181
+
182
+ export async function fetchTransactions(opts: {
183
+ keeperUrl: string;
184
+ vendorAddress: string;
185
+ network: 'devnet' | 'mainnet';
186
+ adminToken?: string;
187
+ vendorSessionToken?: string;
188
+ // Rolling window in days. Default 30. Ignored when both `from` and `to` are
189
+ // set (explicit range wins) — used for the dashboard's "last N days" view.
190
+ days?: number;
191
+ // Explicit inclusive-start / exclusive-end ISO dates (YYYY-MM-DD). When both
192
+ // are set the server filters to [from, to) instead of the rolling window.
193
+ // Used by the CSV export to fetch an exact tax year without over-fetching.
194
+ from?: string;
195
+ to?: string;
196
+ fetcher?: typeof fetch;
197
+ signal?: AbortSignal;
198
+ }): Promise<TransactionsResponse | null> {
199
+ const fetcher = opts.fetcher ?? fetch;
200
+ const headers: Record<string, string> = {};
201
+ if (opts.adminToken) headers.Authorization = `Bearer ${opts.adminToken}`;
202
+ else if (opts.vendorSessionToken) headers.Authorization = `VendorSession ${opts.vendorSessionToken}`;
203
+ const qs = new URLSearchParams({ vendor: opts.vendorAddress, network: opts.network });
204
+ if (opts.from && opts.to) {
205
+ qs.set('from', opts.from);
206
+ qs.set('to', opts.to);
207
+ } else {
208
+ qs.set('days', String(opts.days ?? 30));
209
+ }
210
+ const resp = await fetcher(
211
+ `${baseUrl(opts.keeperUrl)}/transactions?${qs.toString()}`,
212
+ { signal: opts.signal, headers },
213
+ );
214
+ if (!resp.ok) return null;
215
+ return await resp.json() as TransactionsResponse;
216
+ }
217
+
218
+ // Admin all-apps mode: pass `adminToken` and set `vendorAddress` to '__all'.
219
+ // Hits GET /apps (no auth needed today; the admin overload just uses a
220
+ // different URL) so vendors visiting their own dashboard still hit the same
221
+ // per-vendor endpoint.
222
+ export async function fetchVendorApps(opts: {
223
+ keeperUrl: string;
224
+ vendorAddress: string;
225
+ adminToken?: string;
226
+ vendorSessionToken?: string;
227
+ // Network scope for the vendor projection. Required for the per-vendor path
228
+ // — the backend returns records projected against `deployments[network]` plus
229
+ // stub rows for apps the vendor owns but hasn't deployed on this network
230
+ // (status: 'not-deployed'). Omitted only in admin __all mode.
231
+ network?: 'devnet' | 'mainnet';
232
+ fetcher?: typeof fetch;
233
+ signal?: AbortSignal;
234
+ }): Promise<VendorAppsResponse | null> {
235
+ const fetcher = opts.fetcher ?? fetch;
236
+ const headers: Record<string, string> = {};
237
+ if (opts.adminToken) headers.Authorization = `Bearer ${opts.adminToken}`;
238
+ else if (opts.vendorSessionToken) headers.Authorization = `VendorSession ${opts.vendorSessionToken}`;
239
+ const isAdminAll = opts.adminToken && opts.vendorAddress === '__all';
240
+ if (!isAdminAll && !opts.network) {
241
+ throw new Error("fetchVendorApps: `network` is required for the per-vendor path");
242
+ }
243
+ const url = isAdminAll
244
+ ? `${baseUrl(opts.keeperUrl)}/apps?all=1`
245
+ : `${baseUrl(opts.keeperUrl)}/apps/vendor/${encodeURIComponent(opts.vendorAddress)}?network=${opts.network}`;
246
+ const resp = await fetcher(url, { signal: opts.signal, headers });
247
+ if (!resp.ok) return null;
248
+ return await resp.json() as VendorAppsResponse;
249
+ }
@@ -0,0 +1,87 @@
1
+ // Copyright (c) 2025-2026 zkLicensing project developers
2
+ // SPDX-License-Identifier: BUSL-1.1
3
+
4
+ /**
5
+ * successorChain.ts — the current contract of a lineage (licensing app
6
+ * generations, permit registry generations), from chain state alone.
7
+ *
8
+ * A retired contract names its successor in its state (its retirement,
9
+ * which the vendor or authority signed, stored it), and the successor's
10
+ * initialization checked on chain that the predecessor named it. Each hop
11
+ * here also checks the successor runs a published release and started
12
+ * from the predecessor's frozen root. A successor not deployed yet
13
+ * (between retirement and deployment, e.g. across a hard fork) leaves the
14
+ * retired contract current.
15
+ */
16
+
17
+ import { Field, PublicKey } from 'o1js';
18
+ import { readZkappAccount, type ZkappAccount } from './chainHead.js';
19
+ import { LICENSING_APP_STATE } from './contractInterface.js';
20
+ import { PERMIT_REGISTRY_STATE } from './permitInterface.js';
21
+
22
+ /** Where a contract kind keeps its lineage in state. */
23
+ export type LineageLayout = {
24
+ /** Contract name for error messages. */
25
+ name: string;
26
+ /** State index and value that mark the contract retired. */
27
+ retiredAt: number;
28
+ retiredValue: string;
29
+ /** The tree root a retirement freezes, and where a successor keeps the root it started from. */
30
+ rootAt: number;
31
+ ancestorsRootAt: number;
32
+ successorXAt: number;
33
+ successorIsOddAt: number;
34
+ };
35
+
36
+ export const LICENSING_LINEAGE: LineageLayout = {
37
+ name: 'LicensingApp',
38
+ retiredAt: LICENSING_APP_STATE.retired, retiredValue: '1',
39
+ rootAt: LICENSING_APP_STATE.licensesRoot, ancestorsRootAt: LICENSING_APP_STATE.ancestorsRoot,
40
+ successorXAt: LICENSING_APP_STATE.successorX, successorIsOddAt: LICENSING_APP_STATE.successorIsOdd,
41
+ };
42
+
43
+ /** A retired registry's authority-keys hash is RETIRED_KEYS_HASH (1). */
44
+ export const PERMIT_LINEAGE: LineageLayout = {
45
+ name: 'PermitRegistry',
46
+ retiredAt: PERMIT_REGISTRY_STATE.authorityKeysHash, retiredValue: '1',
47
+ rootAt: PERMIT_REGISTRY_STATE.permitsRoot, ancestorsRootAt: PERMIT_REGISTRY_STATE.ancestorsRoot,
48
+ successorXAt: PERMIT_REGISTRY_STATE.successorX, successorIsOddAt: PERMIT_REGISTRY_STATE.successorIsOdd,
49
+ };
50
+
51
+ export const MAX_LINEAGE_HOPS = 32;
52
+
53
+ /**
54
+ * Follows `start`'s successors to the current contract. Returns it with
55
+ * its account (`found: false` when `start` isn't on chain). Throws when a
56
+ * named successor is deployed but isn't a published release (`canonicalVks`)
57
+ * or didn't start from the frozen root.
58
+ */
59
+ export async function currentOnChain(args: {
60
+ start: string;
61
+ layout: LineageLayout;
62
+ canonicalVks: readonly string[];
63
+ graphqlUrl: string;
64
+ fetcher?: typeof fetch;
65
+ }): Promise<{ address: string; account: ZkappAccount }> {
66
+ const { layout: l } = args;
67
+ const read = (address: string) => readZkappAccount({ address, graphqlUrl: args.graphqlUrl, fetcher: args.fetcher });
68
+ let address = args.start;
69
+ let account = await read(address);
70
+ for (let hop = 0; hop < MAX_LINEAGE_HOPS; hop++) {
71
+ const st = account.appState;
72
+ if (!account.found || !st || st[l.retiredAt] !== l.retiredValue) return { address, account };
73
+ let next: string;
74
+ try { next = PublicKey.fromFields([Field(st[l.successorXAt]), Field(st[l.successorIsOddAt])]).toBase58(); } catch { return { address, account }; }
75
+ const succ = await read(next);
76
+ if (!succ.found || !succ.appState) return { address, account }; // not deployed yet
77
+ if (!succ.vkHash || !args.canonicalVks.includes(succ.vkHash)) {
78
+ throw new Error(`The successor ${next} of ${address} is not a published ${l.name} release`);
79
+ }
80
+ if (succ.appState[l.ancestorsRootAt] !== st[l.rootAt]) {
81
+ throw new Error(`The successor ${next} of ${address} did not start from its frozen root`);
82
+ }
83
+ address = next;
84
+ account = succ;
85
+ }
86
+ throw new Error(`${l.name} lineage of ${args.start} is longer than ${MAX_LINEAGE_HOPS} generations`);
87
+ }
package/src/taxId.ts ADDED
@@ -0,0 +1,227 @@
1
+ // Copyright (c) 2025-2026 zkLicensing project developers
2
+ // SPDX-License-Identifier: BUSL-1.1
3
+
4
+ /**
5
+ * taxId.ts — `zklicensing/tax-id`
6
+ *
7
+ * Vendor tax identifiers: every vendor must have one. This module checks
8
+ * the format (and, where the scheme has one, the check digit) for the
9
+ * vendor's country, offline. EU VAT numbers are additionally looked up in
10
+ * VIES by the keeper; this module only says which numbers go to VIES.
11
+ *
12
+ * checkTaxId('FI', 'FI 1234 5678') → { ok, scheme: 'eu-vat', normalized: 'FI12345678', vies: { countryCode: 'FI', number: '12345678' } }
13
+ * checkTaxId('NO', '974 760 673 MVA') → { ok, scheme: 'no-mva', normalized: 'NO974760673MVA' }
14
+ * checkTaxId('BR', '…') → { ok, scheme: 'other', review: true } (no known format: admin review)
15
+ *
16
+ * Pure and browser-safe: the site uses it for live form feedback.
17
+ */
18
+
19
+ export type TaxIdScheme =
20
+ | 'eu-vat' | 'gb-vat' | 'no-mva' | 'ch-uid' | 'au-abn' | 'nz-gst' | 'ca-bn'
21
+ | 'us-ein' | 'jp-invoice' | 'in-gstin' | 'sg-gst' | 'other';
22
+
23
+ export type TaxIdResult =
24
+ | {
25
+ ok: true;
26
+ scheme: TaxIdScheme;
27
+ /** Canonical form to store and show. */
28
+ normalized: string;
29
+ /** EU numbers only: what to look up in VIES (Greece is `EL`, Northern Ireland `XI`). */
30
+ vies?: { countryCode: string; number: string };
31
+ /** True when the country has no known format: accepted for an admin to review. */
32
+ review?: boolean;
33
+ }
34
+ | { ok: false; scheme: TaxIdScheme; error: string; example?: string };
35
+
36
+ /** EU member states, by ISO 3166-1 alpha-2 country code, with their VAT prefix and number format. */
37
+ const EU: Record<string, { prefix: string; re: RegExp; example: string }> = {
38
+ AT: { prefix: 'AT', re: /^U\d{8}$/, example: 'ATU12345678' },
39
+ BE: { prefix: 'BE', re: /^[01]\d{9}$/, example: 'BE0123456789' },
40
+ BG: { prefix: 'BG', re: /^\d{9,10}$/, example: 'BG123456789' },
41
+ CY: { prefix: 'CY', re: /^\d{8}[A-Z]$/, example: 'CY12345678X' },
42
+ CZ: { prefix: 'CZ', re: /^\d{8,10}$/, example: 'CZ12345678' },
43
+ DE: { prefix: 'DE', re: /^\d{9}$/, example: 'DE123456789' },
44
+ DK: { prefix: 'DK', re: /^\d{8}$/, example: 'DK12345678' },
45
+ EE: { prefix: 'EE', re: /^\d{9}$/, example: 'EE123456789' },
46
+ GR: { prefix: 'EL', re: /^\d{9}$/, example: 'EL123456789' },
47
+ ES: { prefix: 'ES', re: /^[A-Z0-9]\d{7}[A-Z0-9]$/, example: 'ESX1234567X' },
48
+ FI: { prefix: 'FI', re: /^\d{8}$/, example: 'FI12345678' },
49
+ FR: { prefix: 'FR', re: /^[A-HJ-NP-Z0-9]{2}\d{9}$/, example: 'FRXX123456789' },
50
+ HR: { prefix: 'HR', re: /^\d{11}$/, example: 'HR12345678901' },
51
+ HU: { prefix: 'HU', re: /^\d{8}$/, example: 'HU12345678' },
52
+ IE: { prefix: 'IE', re: /^(\d{7}[A-W][A-I]?|\d[A-Z+*]\d{5}[A-W])$/, example: 'IE1234567WA' },
53
+ IT: { prefix: 'IT', re: /^\d{11}$/, example: 'IT12345678901' },
54
+ LT: { prefix: 'LT', re: /^(\d{9}|\d{12})$/, example: 'LT123456789' },
55
+ LU: { prefix: 'LU', re: /^\d{8}$/, example: 'LU12345678' },
56
+ LV: { prefix: 'LV', re: /^\d{11}$/, example: 'LV12345678901' },
57
+ MT: { prefix: 'MT', re: /^\d{8}$/, example: 'MT12345678' },
58
+ NL: { prefix: 'NL', re: /^\d{9}B\d{2}$/, example: 'NL123456789B01' },
59
+ PL: { prefix: 'PL', re: /^\d{10}$/, example: 'PL1234567890' },
60
+ PT: { prefix: 'PT', re: /^\d{9}$/, example: 'PT123456789' },
61
+ RO: { prefix: 'RO', re: /^\d{2,10}$/, example: 'RO1234567890' },
62
+ SE: { prefix: 'SE', re: /^\d{12}$/, example: 'SE123456789012' },
63
+ SI: { prefix: 'SI', re: /^\d{8}$/, example: 'SI12345678' },
64
+ SK: { prefix: 'SK', re: /^\d{10}$/, example: 'SK1234567890' },
65
+ };
66
+
67
+ /** True for the 27 EU member states (ISO codes; Greece is GR here). */
68
+ export function isEuCountry(country: string): boolean {
69
+ return Object.prototype.hasOwnProperty.call(EU, String(country).toUpperCase());
70
+ }
71
+
72
+ /** An example of the expected format for `country`, for form placeholders. */
73
+ export function taxIdExample(country: string): string {
74
+ const c = String(country).toUpperCase();
75
+ if (isEuCountry(c)) return EU[c].example;
76
+ return ({
77
+ GB: 'GB123456789', NO: 'NO123456789MVA', CH: 'CHE-123.456.789 MWST', LI: 'CHE-123.456.789 MWST',
78
+ AU: '12 345 678 901', NZ: '123-456-789', CA: '123456789RT0001', US: '12-3456789',
79
+ JP: 'T1234567890123', IN: '22AAAAA0000A1Z5', SG: 'M2-1234567-8',
80
+ } as Record<string, string>)[c] ?? 'Your national tax or VAT number';
81
+ }
82
+
83
+ const digits = (s: string) => [...s].map(Number);
84
+
85
+ function gbCheck(n: string): boolean {
86
+ const x = digits(n.slice(0, 9));
87
+ const sum = x.slice(0, 7).reduce((a, v, i) => a + v * (8 - i), 0) + Number(n.slice(7, 9));
88
+ return sum % 97 === 0 || (sum + 55) % 97 === 0;
89
+ }
90
+
91
+ /** Weighted mod-11 check used by Norway (org. number) and Switzerland (UID). */
92
+ function mod11(n: string, weights: number[]): boolean {
93
+ const x = digits(n);
94
+ let c = 11 - (weights.reduce((a, w, i) => a + w * x[i], 0) % 11);
95
+ if (c === 11) c = 0;
96
+ return c !== 10 && c === x[weights.length];
97
+ }
98
+
99
+ function abnCheck(n: string): boolean {
100
+ const x = digits(n);
101
+ x[0] -= 1;
102
+ return [10, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19].reduce((a, w, i) => a + w * x[i], 0) % 89 === 0;
103
+ }
104
+
105
+ function nzCheck(raw: string): boolean {
106
+ const n = raw.padStart(9, '0');
107
+ const v = Number(n);
108
+ if (v < 10_000_000 || v > 150_000_000) return false;
109
+ const x = digits(n);
110
+ const calc = (w: number[]) => { const r = w.reduce((a, k, i) => a + k * x[i], 0) % 11; return r === 0 ? 0 : 11 - r; };
111
+ let c = calc([3, 2, 7, 6, 5, 4, 3, 2]);
112
+ if (c === 10) c = calc([7, 4, 3, 2, 5, 2, 7, 6]);
113
+ return c !== 10 && c === x[8];
114
+ }
115
+
116
+ function luhn(n: string): boolean {
117
+ let s = 0;
118
+ [...n].reverse().forEach((ch, i) => { let v = Number(ch); if (i % 2) { v *= 2; if (v > 9) v -= 9; } s += v; });
119
+ return s % 10 === 0;
120
+ }
121
+
122
+ const B36 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
123
+ function gstinCheck(g: string): boolean {
124
+ let s = 0;
125
+ for (let i = 0; i < 14; i++) {
126
+ const p = B36.indexOf(g[i]) * (i % 2 === 0 ? 1 : 2);
127
+ s += Math.floor(p / 36) + (p % 36);
128
+ }
129
+ return B36[(36 - (s % 36)) % 36] === g[14];
130
+ }
131
+
132
+ // IRS campus prefixes never issued for an EIN.
133
+ const EIN_INVALID_PREFIXES = new Set(['00', '07', '08', '09', '17', '18', '19', '28', '29', '49', '69', '70', '78', '79', '89', '96', '97']);
134
+
135
+ /**
136
+ * Checks `taxId` for a vendor in `country` (ISO 3166-1 alpha-2): the format
137
+ * and, where the scheme has one, the check digit. Spaces, dots and dashes
138
+ * are ignored; letters are upper-cased.
139
+ */
140
+ export function checkTaxId(country: string, taxId: string): TaxIdResult {
141
+ const c = String(country ?? '').trim().toUpperCase();
142
+ const raw = String(taxId ?? '').trim().toUpperCase();
143
+ const compact = raw.replace(/[\s.\-]/g, '');
144
+ const fail = (scheme: TaxIdScheme, error: string): TaxIdResult => ({ ok: false, scheme, error, example: taxIdExample(c) });
145
+ if (!compact) return fail('other', 'A tax ID is required');
146
+ if (compact.length > 30) return fail('other', 'The tax ID is too long');
147
+
148
+ if (isEuCountry(c)) {
149
+ const { prefix, re } = EU[c];
150
+ // The country prefix is optional on input (some numbers, e.g. Spanish,
151
+ // may themselves start with a letter, so test the body as given too).
152
+ const body = compact.startsWith(prefix) && re.test(compact.slice(prefix.length)) ? compact.slice(prefix.length) : compact;
153
+ if (!re.test(body)) {
154
+ return fail('eu-vat', /^[A-Z]{2}/.test(compact) && !compact.startsWith(prefix)
155
+ ? `A VAT number for ${c} starts with ${prefix}` : `Not a valid ${c} VAT number`);
156
+ }
157
+ return { ok: true, scheme: 'eu-vat', normalized: prefix + body, vies: { countryCode: prefix, number: body } };
158
+ }
159
+
160
+ switch (c) {
161
+ case 'GB': {
162
+ const b = compact.replace(/^GB/, '');
163
+ if (/^(GD|HA)\d{3}$/.test(b)) return { ok: true, scheme: 'gb-vat', normalized: 'GB' + b };
164
+ if (!/^(\d{9}|\d{12})$/.test(b)) return fail('gb-vat', 'A UK VAT number is GB and 9 or 12 digits');
165
+ if (!gbCheck(b)) return fail('gb-vat', 'The UK VAT number\'s check digits are wrong');
166
+ return { ok: true, scheme: 'gb-vat', normalized: 'GB' + b };
167
+ }
168
+ case 'NO': {
169
+ const b = compact.replace(/^NO/, '').replace(/MVA$/, '');
170
+ if (!/^\d{9}$/.test(b)) return fail('no-mva', 'A Norwegian VAT number is 9 digits followed by MVA');
171
+ if (!mod11(b, [3, 2, 7, 6, 5, 4, 3, 2])) return fail('no-mva', 'The organisation number\'s check digit is wrong');
172
+ return { ok: true, scheme: 'no-mva', normalized: `NO${b}MVA` };
173
+ }
174
+ case 'CH': case 'LI': {
175
+ const m = /^CHE(\d{9})(MWST|TVA|IVA|MVA)?$/.exec(compact);
176
+ if (!m) return fail('ch-uid', 'A Swiss VAT number is CHE-123.456.789 MWST');
177
+ if (!mod11(m[1], [5, 4, 3, 2, 7, 6, 5, 4])) return fail('ch-uid', 'The UID\'s check digit is wrong');
178
+ const d = m[1];
179
+ return { ok: true, scheme: 'ch-uid', normalized: `CHE-${d.slice(0, 3)}.${d.slice(3, 6)}.${d.slice(6)} ${m[2] ?? 'MWST'}` };
180
+ }
181
+ case 'AU': {
182
+ const b = compact.replace(/^ABN/, '');
183
+ if (!/^\d{11}$/.test(b)) return fail('au-abn', 'An ABN is 11 digits');
184
+ if (!abnCheck(b)) return fail('au-abn', 'The ABN\'s check digits are wrong');
185
+ return { ok: true, scheme: 'au-abn', normalized: `${b.slice(0, 2)} ${b.slice(2, 5)} ${b.slice(5, 8)} ${b.slice(8)}` };
186
+ }
187
+ case 'NZ': {
188
+ if (!/^\d{8,9}$/.test(compact)) return fail('nz-gst', 'A New Zealand GST number is 8 or 9 digits');
189
+ if (!nzCheck(compact)) return fail('nz-gst', 'The GST number\'s check digit is wrong');
190
+ const n = compact.padStart(9, '0');
191
+ return { ok: true, scheme: 'nz-gst', normalized: `${n.slice(0, 3)}-${n.slice(3, 6)}-${n.slice(6)}` };
192
+ }
193
+ case 'CA': {
194
+ const m = /^(\d{9})(RT\d{4})?$/.exec(compact);
195
+ if (!m) return fail('ca-bn', 'A Canadian business number is 9 digits, optionally followed by RT0001');
196
+ if (!luhn(m[1])) return fail('ca-bn', 'The business number\'s check digit is wrong');
197
+ return { ok: true, scheme: 'ca-bn', normalized: m[1] + (m[2] ?? '') };
198
+ }
199
+ case 'US': {
200
+ if (!/^\d{9}$/.test(compact)) return fail('us-ein', 'An EIN is 9 digits (12-3456789)');
201
+ if (EIN_INVALID_PREFIXES.has(compact.slice(0, 2))) return fail('us-ein', 'No EIN starts with ' + compact.slice(0, 2));
202
+ return { ok: true, scheme: 'us-ein', normalized: `${compact.slice(0, 2)}-${compact.slice(2)}` };
203
+ }
204
+ case 'JP': {
205
+ // Format only: T and the 13-digit corporate number.
206
+ const b = compact.replace(/^T/, '');
207
+ if (!/^\d{13}$/.test(b)) return fail('jp-invoice', 'A Japanese registration number is T and 13 digits');
208
+ return { ok: true, scheme: 'jp-invoice', normalized: 'T' + b };
209
+ }
210
+ case 'IN': {
211
+ if (!/^\d{2}[A-Z]{5}\d{4}[A-Z][1-9A-Z]Z[0-9A-Z]$/.test(compact)) return fail('in-gstin', 'A GSTIN is 15 characters, like 22AAAAA0000A1Z5');
212
+ if (!gstinCheck(compact)) return fail('in-gstin', 'The GSTIN\'s check character is wrong');
213
+ return { ok: true, scheme: 'in-gstin', normalized: compact };
214
+ }
215
+ case 'SG': {
216
+ if (!/^(M[2-9A-Z]\d{7}[0-9A-Z]|\d{8,9}[A-Z]|[TSR]\d{2}[A-Z]{2}\d{4}[A-Z])$/.test(compact)) {
217
+ return fail('sg-gst', 'Not a Singapore GST registration number');
218
+ }
219
+ return { ok: true, scheme: 'sg-gst', normalized: compact };
220
+ }
221
+ default: {
222
+ // No known format: accept sensible input for an admin to review.
223
+ if (!/^[A-Z0-9]{4,30}$/.test(compact)) return fail('other', 'Use letters and digits only');
224
+ return { ok: true, scheme: 'other', normalized: raw.replace(/\s+/g, ' '), review: true };
225
+ }
226
+ }
227
+ }