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.
- package/README.md +417 -201
- package/build/agent.d.ts +371 -0
- package/build/agent.d.ts.map +1 -0
- package/build/agent.js +553 -0
- package/build/agent.js.map +1 -0
- package/build/agentActions.d.ts +841 -0
- package/build/agentActions.d.ts.map +1 -0
- package/build/agentActions.js +581 -0
- package/build/agentActions.js.map +1 -0
- package/build/agentRecord.d.ts +83 -0
- package/build/agentRecord.d.ts.map +1 -0
- package/build/agentRecord.js +69 -0
- package/build/agentRecord.js.map +1 -0
- package/build/agentVendor.d.ts +220 -0
- package/build/agentVendor.d.ts.map +1 -0
- package/build/agentVendor.js +306 -0
- package/build/agentVendor.js.map +1 -0
- package/build/appsLookup.d.ts +14 -0
- package/build/appsLookup.d.ts.map +1 -0
- package/build/appsLookup.js +12 -0
- package/build/appsLookup.js.map +1 -0
- package/build/archiveBootstrap.d.ts +31 -5
- package/build/archiveBootstrap.d.ts.map +1 -0
- package/build/archiveBootstrap.js +140 -47
- package/build/archiveBootstrap.js.map +1 -0
- package/build/atomicJson.d.ts +5 -0
- package/build/atomicJson.d.ts.map +1 -0
- package/build/atomicJson.js +55 -0
- package/build/atomicJson.js.map +1 -0
- package/build/bindClient.d.ts +85 -0
- package/build/bindClient.d.ts.map +1 -0
- package/build/bindClient.js +187 -0
- package/build/bindClient.js.map +1 -0
- package/build/buyClient.d.ts +27 -4
- package/build/buyClient.d.ts.map +1 -0
- package/build/buyClient.js +72 -26
- package/build/buyClient.js.map +1 -0
- package/build/chainHead.d.ts +61 -0
- package/build/chainHead.d.ts.map +1 -0
- package/build/chainHead.js +88 -0
- package/build/chainHead.js.map +1 -0
- package/build/compactMerkleMap.d.ts +72 -0
- package/build/compactMerkleMap.d.ts.map +1 -0
- package/build/compactMerkleMap.js +224 -0
- package/build/compactMerkleMap.js.map +1 -0
- package/build/contractInterface.d.ts +191 -51
- package/build/contractInterface.d.ts.map +1 -0
- package/build/contractInterface.js +234 -43
- package/build/contractInterface.js.map +1 -0
- package/build/contractTypes.d.ts +9 -0
- package/build/contractTypes.d.ts.map +1 -0
- package/build/contractTypes.js +28 -0
- package/build/contractTypes.js.map +1 -0
- package/build/countryStats.d.ts +12 -3
- package/build/countryStats.d.ts.map +1 -0
- package/build/countryStats.js +87 -23
- package/build/countryStats.js.map +1 -0
- package/build/deployClient.d.ts +138 -24
- package/build/deployClient.d.ts.map +1 -0
- package/build/deployClient.js +91 -31
- package/build/deployClient.js.map +1 -0
- package/build/deviceIdentity.d.ts +68 -0
- package/build/deviceIdentity.d.ts.map +1 -0
- package/build/deviceIdentity.js +209 -0
- package/build/deviceIdentity.js.map +1 -0
- package/build/freezeClient.d.ts +26 -6
- package/build/freezeClient.d.ts.map +1 -0
- package/build/freezeClient.js +30 -6
- package/build/freezeClient.js.map +1 -0
- package/build/generationClient.d.ts +6 -3
- package/build/generationClient.d.ts.map +1 -0
- package/build/generationClient.js +25 -38
- package/build/generationClient.js.map +1 -0
- package/build/holderSecret.d.ts +70 -0
- package/build/holderSecret.d.ts.map +1 -0
- package/build/holderSecret.js +76 -0
- package/build/holderSecret.js.map +1 -0
- package/build/index.d.ts +38 -38
- package/build/index.d.ts.map +1 -0
- package/build/index.js +97 -121
- package/build/index.js.map +1 -0
- package/build/keysClient.d.ts +142 -0
- package/build/keysClient.d.ts.map +1 -0
- package/build/keysClient.js +109 -0
- package/build/keysClient.js.map +1 -0
- package/build/licenseLeaf.d.ts +76 -0
- package/build/licenseLeaf.d.ts.map +1 -0
- package/build/licenseLeaf.js +189 -0
- package/build/licenseLeaf.js.map +1 -0
- package/build/licenseRecord.d.ts +68 -5
- package/build/licenseRecord.d.ts.map +1 -0
- package/build/licenseRecord.js +26 -1
- package/build/licenseRecord.js.map +1 -0
- package/build/licenseStore.d.ts +81 -4
- package/build/licenseStore.d.ts.map +1 -0
- package/build/licenseStore.js +400 -30
- package/build/licenseStore.js.map +1 -0
- package/build/licenseTrustless.d.ts +43 -0
- package/build/licenseTrustless.d.ts.map +1 -0
- package/build/licenseTrustless.js +180 -0
- package/build/licenseTrustless.js.map +1 -0
- package/build/manifestVerify.d.ts +29 -1
- package/build/manifestVerify.d.ts.map +1 -0
- package/build/manifestVerify.js +46 -4
- package/build/manifestVerify.js.map +1 -0
- package/build/minaPriceHistory.d.ts +9 -0
- package/build/minaPriceHistory.d.ts.map +1 -0
- package/build/minaPriceHistory.js +152 -0
- package/build/minaPriceHistory.js.map +1 -0
- package/build/onChainRoot.d.ts +60 -0
- package/build/onChainRoot.d.ts.map +1 -0
- package/build/onChainRoot.js +74 -0
- package/build/onChainRoot.js.map +1 -0
- package/build/ownershipLicenseHash.d.ts +25 -0
- package/build/ownershipLicenseHash.d.ts.map +1 -0
- package/build/ownershipLicenseHash.js +29 -0
- package/build/ownershipLicenseHash.js.map +1 -0
- package/build/ownershipSignature.d.ts +153 -0
- package/build/ownershipSignature.d.ts.map +1 -0
- package/build/ownershipSignature.js +297 -0
- package/build/ownershipSignature.js.map +1 -0
- package/build/permitCheckFlow.d.ts +155 -0
- package/build/permitCheckFlow.d.ts.map +1 -0
- package/build/permitCheckFlow.js +196 -0
- package/build/permitCheckFlow.js.map +1 -0
- package/build/permitDescribe.d.ts +124 -0
- package/build/permitDescribe.d.ts.map +1 -0
- package/build/permitDescribe.js +324 -0
- package/build/permitDescribe.js.map +1 -0
- package/build/permitInterface.d.ts +85 -0
- package/build/permitInterface.d.ts.map +1 -0
- package/build/permitInterface.js +121 -0
- package/build/permitInterface.js.map +1 -0
- package/build/permitMessages.d.ts +313 -0
- package/build/permitMessages.d.ts.map +1 -0
- package/build/permitMessages.js +462 -0
- package/build/permitMessages.js.map +1 -0
- package/build/permitProof.d.ts +108 -0
- package/build/permitProof.d.ts.map +1 -0
- package/build/permitProof.js +202 -0
- package/build/permitProof.js.map +1 -0
- package/build/permitRegistryRebuild.d.ts +166 -0
- package/build/permitRegistryRebuild.d.ts.map +1 -0
- package/build/permitRegistryRebuild.js +347 -0
- package/build/permitRegistryRebuild.js.map +1 -0
- package/build/permitTree.d.ts +88 -0
- package/build/permitTree.d.ts.map +1 -0
- package/build/permitTree.js +230 -0
- package/build/permitTree.js.map +1 -0
- package/build/permitsClient.d.ts +680 -0
- package/build/permitsClient.d.ts.map +1 -0
- package/build/permitsClient.js +597 -0
- package/build/permitsClient.js.map +1 -0
- package/build/proveErrors.d.ts +50 -0
- package/build/proveErrors.d.ts.map +1 -0
- package/build/proveErrors.js +76 -0
- package/build/proveErrors.js.map +1 -0
- package/build/receipt.d.ts +95 -0
- package/build/receipt.d.ts.map +1 -0
- package/build/receipt.js +84 -0
- package/build/receipt.js.map +1 -0
- package/build/refundClient.d.ts +7 -12
- package/build/refundClient.d.ts.map +1 -0
- package/build/refundClient.js +52 -21
- package/build/refundClient.js.map +1 -0
- package/build/registryTrustVerify.d.ts +78 -0
- package/build/registryTrustVerify.d.ts.map +1 -0
- package/build/registryTrustVerify.js +129 -0
- package/build/registryTrustVerify.js.map +1 -0
- package/build/releaseClient.d.ts +26 -0
- package/build/releaseClient.d.ts.map +1 -0
- package/build/releaseClient.js +65 -0
- package/build/releaseClient.js.map +1 -0
- package/build/renewClient.d.ts +28 -2
- package/build/renewClient.d.ts.map +1 -0
- package/build/renewClient.js +31 -27
- package/build/renewClient.js.map +1 -0
- package/build/seatName.d.ts +8 -0
- package/build/seatName.d.ts.map +1 -0
- package/build/seatName.js +136 -0
- package/build/seatName.js.map +1 -0
- package/build/slotConstants.d.ts +15 -0
- package/build/slotConstants.d.ts.map +1 -0
- package/build/slotConstants.js +17 -0
- package/build/slotConstants.js.map +1 -0
- package/build/statsClient.d.ts +61 -7
- package/build/statsClient.d.ts.map +1 -0
- package/build/statsClient.js +44 -5
- package/build/statsClient.js.map +1 -0
- package/build/successorChain.d.ts +35 -0
- package/build/successorChain.d.ts.map +1 -0
- package/build/successorChain.js +69 -0
- package/build/successorChain.js.map +1 -0
- package/build/taxId.d.ts +44 -0
- package/build/taxId.d.ts.map +1 -0
- package/build/taxId.js +212 -0
- package/build/taxId.js.map +1 -0
- package/build/trustAnchorVerify.d.ts +49 -0
- package/build/trustAnchorVerify.d.ts.map +1 -0
- package/build/trustAnchorVerify.js +432 -0
- package/build/trustAnchorVerify.js.map +1 -0
- package/build/trustAnchorVerifyCli.d.ts +45 -0
- package/build/trustAnchorVerifyCli.d.ts.map +1 -0
- package/build/trustAnchorVerifyCli.js +235 -0
- package/build/trustAnchorVerifyCli.js.map +1 -0
- package/build/txBroadcastClient.d.ts +26 -0
- package/build/txBroadcastClient.d.ts.map +1 -0
- package/build/txBroadcastClient.js +20 -0
- package/build/txBroadcastClient.js.map +1 -0
- package/build/unbindClient.d.ts +77 -0
- package/build/unbindClient.d.ts.map +1 -0
- package/build/unbindClient.js +169 -0
- package/build/unbindClient.js.map +1 -0
- package/build/units.d.ts +28 -0
- package/build/units.d.ts.map +1 -0
- package/build/units.js +65 -0
- package/build/units.js.map +1 -0
- package/build/vatRates.d.ts +54 -0
- package/build/vatRates.d.ts.map +1 -0
- package/build/vatRates.js +82 -0
- package/build/vatRates.js.map +1 -0
- package/build/vatRatesData.d.ts +5 -0
- package/build/vatRatesData.d.ts.map +1 -0
- package/build/vatRatesData.js +42 -0
- package/build/vatRatesData.js.map +1 -0
- package/build/vendorAccountClient.d.ts +66 -0
- package/build/vendorAccountClient.d.ts.map +1 -0
- package/build/vendorAccountClient.js +47 -0
- package/build/vendorAccountClient.js.map +1 -0
- package/build/verifyCore.d.ts +4 -1
- package/build/verifyCore.d.ts.map +1 -0
- package/build/verifyCore.js +21 -10
- package/build/verifyCore.js.map +1 -0
- package/build/verifyOffline.d.ts +85 -0
- package/build/verifyOffline.d.ts.map +1 -0
- package/build/verifyOffline.js +139 -0
- package/build/verifyOffline.js.map +1 -0
- package/build/verifyService.d.ts.map +1 -0
- package/build/verifyService.js +571 -501
- package/build/verifyService.js.map +1 -0
- package/build/verifyWithAncestors.d.ts +19 -0
- package/build/verifyWithAncestors.d.ts.map +1 -0
- package/build/verifyWithAncestors.js +127 -0
- package/build/verifyWithAncestors.js.map +1 -0
- package/build/zipToState.d.ts +2 -0
- package/build/zipToState.d.ts.map +1 -0
- package/build/zipToState.js +94 -0
- package/build/zipToState.js.map +1 -0
- package/package.json +141 -16
- package/src/agent.ts +682 -0
- package/src/agentActions.ts +637 -0
- package/src/agentRecord.ts +121 -0
- package/src/agentVendor.ts +356 -0
- package/src/appsLookup.ts +48 -0
- package/src/archiveBootstrap.ts +386 -0
- package/src/atomicJson.ts +53 -0
- package/src/bindClient.ts +297 -0
- package/src/buyClient.ts +344 -0
- package/src/chainHead.ts +106 -0
- package/src/compactMerkleMap.ts +222 -0
- package/src/contractInterface.ts +465 -0
- package/src/contractTypes.ts +31 -0
- package/src/countryStats.ts +200 -0
- package/src/deployClient.ts +517 -0
- package/src/deviceIdentity.ts +249 -0
- package/src/freezeClient.ts +162 -0
- package/src/generationClient.ts +145 -0
- package/src/holderSecret.ts +91 -0
- package/src/index.ts +362 -0
- package/src/keysClient.ts +135 -0
- package/src/licenseLeaf.ts +221 -0
- package/src/licenseRecord.ts +190 -0
- package/src/licenseStore.ts +767 -0
- package/src/licenseTrustless.ts +207 -0
- package/src/manifestVerify.ts +164 -0
- package/src/minaPriceHistory.ts +166 -0
- package/src/onChainRoot.ts +90 -0
- package/src/ownershipLicenseHash.ts +31 -0
- package/src/ownershipSignature.ts +421 -0
- package/src/permitCheckFlow.ts +227 -0
- package/src/permitDescribe.ts +362 -0
- package/src/permitInterface.ts +146 -0
- package/src/permitMessages.ts +572 -0
- package/src/permitProof.ts +230 -0
- package/src/permitRegistryRebuild.ts +423 -0
- package/src/permitTree.ts +232 -0
- package/src/permitsClient.ts +746 -0
- package/src/proveErrors.ts +98 -0
- package/src/receipt.ts +156 -0
- package/src/refundClient.ts +177 -0
- package/src/registryTrustVerify.ts +192 -0
- package/src/releaseClient.ts +101 -0
- package/src/renewClient.ts +234 -0
- package/src/seatName.ts +165 -0
- package/src/slotConstants.ts +18 -0
- package/src/statsClient.ts +249 -0
- package/src/successorChain.ts +87 -0
- package/src/taxId.ts +227 -0
- package/src/trustAnchorVerify.ts +550 -0
- package/src/trustAnchorVerifyCli.ts +234 -0
- package/src/txBroadcastClient.ts +48 -0
- package/src/unbindClient.ts +233 -0
- package/src/units.ts +74 -0
- package/src/vatRates.ts +100 -0
- package/src/vatRatesData.ts +43 -0
- package/src/vendorAccountClient.ts +120 -0
- package/src/verifyCore.ts +100 -0
- package/src/verifyOffline.ts +212 -0
- package/src/verifyService.ts +982 -0
- package/src/verifyWithAncestors.ts +163 -0
- package/src/zipToState.ts +94 -0
- package/build/licenseProof.d.ts +0 -26
- package/build/licenseProof.js +0 -41
- package/build/migrateClient.d.ts +0 -43
- package/build/migrateClient.js +0 -47
- package/build/ownership.d.ts +0 -34
- 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
|
+
}
|