@nokinc-flur/sdk 1.1.3 → 1.1.4
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/dist/index.cjs +294 -235
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +233 -498
- package/dist/index.d.ts +233 -498
- package/dist/index.js +292 -230
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1563,64 +1563,113 @@ function constantTimeEqual(a, b) {
|
|
|
1563
1563
|
return diff === 0;
|
|
1564
1564
|
}
|
|
1565
1565
|
|
|
1566
|
-
// src/
|
|
1567
|
-
import {
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1566
|
+
// src/offline/oac.ts
|
|
1567
|
+
import { z as z5 } from "zod";
|
|
1568
|
+
|
|
1569
|
+
// src/crypto/p256-issuer.ts
|
|
1570
|
+
import { p256 } from "@noble/curves/nist";
|
|
1571
|
+
function bytesToBase64(bytes) {
|
|
1572
|
+
if (typeof Buffer !== "undefined") {
|
|
1573
|
+
return Buffer.from(bytes).toString("base64");
|
|
1574
|
+
}
|
|
1575
|
+
let bin = "";
|
|
1576
|
+
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
1577
|
+
return btoa(bin);
|
|
1572
1578
|
}
|
|
1573
|
-
function
|
|
1574
|
-
|
|
1579
|
+
function base64ToBytes(b64) {
|
|
1580
|
+
if (typeof Buffer !== "undefined") {
|
|
1581
|
+
return new Uint8Array(Buffer.from(b64, "base64"));
|
|
1582
|
+
}
|
|
1583
|
+
const bin = atob(b64);
|
|
1584
|
+
const out = new Uint8Array(bin.length);
|
|
1585
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
1586
|
+
return out;
|
|
1575
1587
|
}
|
|
1576
|
-
|
|
1577
|
-
|
|
1588
|
+
var P256_SPKI_HEADER = new Uint8Array([
|
|
1589
|
+
48,
|
|
1590
|
+
89,
|
|
1591
|
+
48,
|
|
1592
|
+
19,
|
|
1593
|
+
6,
|
|
1594
|
+
7,
|
|
1595
|
+
42,
|
|
1596
|
+
134,
|
|
1597
|
+
72,
|
|
1598
|
+
206,
|
|
1599
|
+
61,
|
|
1600
|
+
2,
|
|
1601
|
+
1,
|
|
1602
|
+
6,
|
|
1603
|
+
8,
|
|
1604
|
+
42,
|
|
1605
|
+
134,
|
|
1606
|
+
72,
|
|
1607
|
+
206,
|
|
1608
|
+
61,
|
|
1609
|
+
3,
|
|
1610
|
+
1,
|
|
1611
|
+
7,
|
|
1612
|
+
3,
|
|
1613
|
+
66,
|
|
1614
|
+
0
|
|
1615
|
+
]);
|
|
1616
|
+
function p256SpkiB64ToRaw(spkiB64) {
|
|
1617
|
+
const spki = base64ToBytes(spkiB64);
|
|
1618
|
+
if (spki.length !== P256_SPKI_HEADER.length + 65) {
|
|
1619
|
+
throw new Error("p256: invalid SPKI length");
|
|
1620
|
+
}
|
|
1621
|
+
for (let i = 0; i < P256_SPKI_HEADER.length; i++) {
|
|
1622
|
+
if (spki[i] !== P256_SPKI_HEADER[i]) {
|
|
1623
|
+
throw new Error("p256: invalid SPKI header");
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
return spki.slice(P256_SPKI_HEADER.length);
|
|
1578
1627
|
}
|
|
1579
|
-
function
|
|
1628
|
+
function signIssuerP256(bytes, issuerPrivateKey) {
|
|
1629
|
+
const sig = p256.sign(bytes, issuerPrivateKey, { prehash: true });
|
|
1630
|
+
return bytesToBase64(sig.toBytes("der"));
|
|
1631
|
+
}
|
|
1632
|
+
function verifyIssuerP256(bytes, signatureB64, issuerPublicKeySpkiB64) {
|
|
1580
1633
|
try {
|
|
1581
|
-
|
|
1634
|
+
const pubRaw = p256SpkiB64ToRaw(issuerPublicKeySpkiB64);
|
|
1635
|
+
const sigBytes = base64ToBytes(signatureB64);
|
|
1636
|
+
return p256.verify(sigBytes, bytes, pubRaw, {
|
|
1637
|
+
prehash: true,
|
|
1638
|
+
format: "der"
|
|
1639
|
+
});
|
|
1582
1640
|
} catch {
|
|
1583
1641
|
return false;
|
|
1584
1642
|
}
|
|
1585
1643
|
}
|
|
1586
|
-
function signCanonical(value, privateKey) {
|
|
1587
|
-
return sign(canonicalJSONBytes(value), privateKey);
|
|
1588
|
-
}
|
|
1589
|
-
function verifyCanonical(value, signature, publicKey) {
|
|
1590
|
-
return verify(canonicalJSONBytes(value), signature, publicKey);
|
|
1591
|
-
}
|
|
1592
1644
|
|
|
1593
1645
|
// src/offline/oac.ts
|
|
1594
|
-
import { z as z5 } from "zod";
|
|
1595
1646
|
var OAC_DEFAULT_PER_TX_KOBO = 5e5;
|
|
1596
1647
|
var OAC_DEFAULT_CUMULATIVE_KOBO = 2e6;
|
|
1597
1648
|
var OAC_DEFAULT_VALIDITY_MS = 24 * 60 * 60 * 1e3;
|
|
1598
|
-
var
|
|
1599
|
-
new RegExp(`^[0-9a-fA-F]{${length * 2}}$`),
|
|
1600
|
-
`expected ${length}-byte hex string`
|
|
1601
|
-
);
|
|
1649
|
+
var Base64Std = z5.string().min(16).max(4096).regex(/^[A-Za-z0-9+/]+={0,2}$/, "expected base64 (standard) string");
|
|
1602
1650
|
var OACSchema = z5.object({
|
|
1603
1651
|
userId: z5.string().min(1),
|
|
1604
1652
|
deviceId: z5.string().min(1),
|
|
1605
|
-
|
|
1653
|
+
/** SubjectPublicKeyInfo DER, base64 (P-256). */
|
|
1654
|
+
devicePublicKey: Base64Std,
|
|
1606
1655
|
perTxCapKobo: z5.number().int().nonnegative(),
|
|
1607
1656
|
cumulativeCapKobo: z5.number().int().nonnegative(),
|
|
1608
1657
|
validFromMs: z5.number().int().nonnegative(),
|
|
1609
1658
|
validUntilMs: z5.number().int().positive(),
|
|
1610
1659
|
counterSeed: z5.number().int().nonnegative(),
|
|
1611
1660
|
nonce: z5.string().min(1),
|
|
1612
|
-
|
|
1661
|
+
/** ASN.1 DER ECDSA(SHA-256) signature, base64. */
|
|
1662
|
+
issuerSig: Base64Std
|
|
1613
1663
|
}).refine((v) => v.validUntilMs > v.validFromMs, {
|
|
1614
1664
|
message: "validUntilMs must be greater than validFromMs"
|
|
1615
1665
|
}).refine((v) => v.perTxCapKobo <= v.cumulativeCapKobo, {
|
|
1616
1666
|
message: "perTxCapKobo must not exceed cumulativeCapKobo"
|
|
1617
1667
|
});
|
|
1618
1668
|
function buildOAC(input) {
|
|
1619
|
-
const devicePublicKey = typeof input.devicePublicKey === "string" ? input.devicePublicKey : bytesToHex(input.devicePublicKey);
|
|
1620
1669
|
return {
|
|
1621
1670
|
userId: input.userId,
|
|
1622
1671
|
deviceId: input.deviceId,
|
|
1623
|
-
devicePublicKey,
|
|
1672
|
+
devicePublicKey: input.devicePublicKey,
|
|
1624
1673
|
perTxCapKobo: input.perTxCapKobo ?? OAC_DEFAULT_PER_TX_KOBO,
|
|
1625
1674
|
cumulativeCapKobo: input.cumulativeCapKobo ?? OAC_DEFAULT_CUMULATIVE_KOBO,
|
|
1626
1675
|
validFromMs: input.validFromMs,
|
|
@@ -1630,36 +1679,25 @@ function buildOAC(input) {
|
|
|
1630
1679
|
};
|
|
1631
1680
|
}
|
|
1632
1681
|
function signOAC(unsigned, issuerPrivateKey) {
|
|
1633
|
-
const issuerSig =
|
|
1634
|
-
|
|
1682
|
+
const issuerSig = signIssuerP256(
|
|
1683
|
+
canonicalJSONBytes(unsigned),
|
|
1684
|
+
issuerPrivateKey
|
|
1635
1685
|
);
|
|
1636
1686
|
return { ...unsigned, issuerSig };
|
|
1637
1687
|
}
|
|
1638
|
-
function verifyOAC(oac,
|
|
1688
|
+
function verifyOAC(oac, issuerPublicKeySpkiB64) {
|
|
1639
1689
|
try {
|
|
1640
1690
|
const parsed = OACSchema.parse(oac);
|
|
1641
1691
|
const { issuerSig, ...unsigned } = parsed;
|
|
1642
|
-
return
|
|
1692
|
+
return verifyIssuerP256(
|
|
1643
1693
|
canonicalJSONBytes(unsigned),
|
|
1644
|
-
|
|
1645
|
-
|
|
1694
|
+
issuerSig,
|
|
1695
|
+
issuerPublicKeySpkiB64
|
|
1646
1696
|
);
|
|
1647
1697
|
} catch {
|
|
1648
1698
|
return false;
|
|
1649
1699
|
}
|
|
1650
1700
|
}
|
|
1651
|
-
function bytesToHex(b) {
|
|
1652
|
-
let s = "";
|
|
1653
|
-
for (let i = 0; i < b.length; i++) s += b[i].toString(16).padStart(2, "0");
|
|
1654
|
-
return s;
|
|
1655
|
-
}
|
|
1656
|
-
function hexToBytes(s) {
|
|
1657
|
-
if (s.length % 2 !== 0) throw new Error("hex: odd length");
|
|
1658
|
-
const out = new Uint8Array(s.length / 2);
|
|
1659
|
-
for (let i = 0; i < out.length; i++)
|
|
1660
|
-
out[i] = parseInt(s.slice(i * 2, i * 2 + 2), 16);
|
|
1661
|
-
return out;
|
|
1662
|
-
}
|
|
1663
1701
|
|
|
1664
1702
|
// src/offline/codec.ts
|
|
1665
1703
|
var ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";
|
|
@@ -1716,19 +1754,19 @@ function decodeBase45(s) {
|
|
|
1716
1754
|
|
|
1717
1755
|
// src/offline/messages.ts
|
|
1718
1756
|
import { z as z6 } from "zod";
|
|
1719
|
-
var
|
|
1757
|
+
var Base64Sig = z6.string().min(16).max(4096).regex(/^[A-Za-z0-9+/]+={0,2}$/, "expected base64 (standard) signature");
|
|
1720
1758
|
var OfflinePaymentRequestSchema = z6.object({
|
|
1721
1759
|
reference: z6.string().min(1),
|
|
1722
1760
|
amountKobo: z6.number().int().positive(),
|
|
1723
1761
|
merchantOAC: OACSchema,
|
|
1724
1762
|
expiresAtMs: z6.number().int().positive(),
|
|
1725
|
-
merchantSig:
|
|
1763
|
+
merchantSig: Base64Sig
|
|
1726
1764
|
});
|
|
1727
1765
|
var OfflinePaymentAuthorizationSchema = z6.object({
|
|
1728
1766
|
request: OfflinePaymentRequestSchema,
|
|
1729
1767
|
payerOAC: OACSchema,
|
|
1730
1768
|
payerCounter: z6.number().int().positive(),
|
|
1731
|
-
payerSig:
|
|
1769
|
+
payerSig: Base64Sig
|
|
1732
1770
|
});
|
|
1733
1771
|
function buildPaymentRequest(input) {
|
|
1734
1772
|
if (!Number.isInteger(input.amountKobo) || input.amountKobo <= 0) {
|
|
@@ -1745,27 +1783,28 @@ function buildPaymentRequest(input) {
|
|
|
1745
1783
|
};
|
|
1746
1784
|
}
|
|
1747
1785
|
function signPaymentRequest(unsigned, merchantDevicePrivateKey) {
|
|
1748
|
-
const merchantSig =
|
|
1749
|
-
|
|
1786
|
+
const merchantSig = signIssuerP256(
|
|
1787
|
+
canonicalJSONBytes(unsigned),
|
|
1788
|
+
merchantDevicePrivateKey
|
|
1750
1789
|
);
|
|
1751
1790
|
return { ...unsigned, merchantSig };
|
|
1752
1791
|
}
|
|
1753
|
-
function verifyPaymentRequest(req,
|
|
1792
|
+
function verifyPaymentRequest(req, issuerPublicKeySpkiB64) {
|
|
1754
1793
|
try {
|
|
1755
1794
|
const parsed = OfflinePaymentRequestSchema.parse(req);
|
|
1756
1795
|
const { issuerSig: merchantOacSig, ...merchantOacUnsigned } = parsed.merchantOAC;
|
|
1757
|
-
if (!
|
|
1796
|
+
if (!verifyIssuerP256(
|
|
1758
1797
|
canonicalJSONBytes(merchantOacUnsigned),
|
|
1759
|
-
|
|
1760
|
-
|
|
1798
|
+
merchantOacSig,
|
|
1799
|
+
issuerPublicKeySpkiB64
|
|
1761
1800
|
)) {
|
|
1762
1801
|
return false;
|
|
1763
1802
|
}
|
|
1764
1803
|
const { merchantSig, ...unsigned } = parsed;
|
|
1765
|
-
return
|
|
1804
|
+
return verifyIssuerP256(
|
|
1766
1805
|
canonicalJSONBytes(unsigned),
|
|
1767
|
-
|
|
1768
|
-
|
|
1806
|
+
merchantSig,
|
|
1807
|
+
parsed.merchantOAC.devicePublicKey
|
|
1769
1808
|
);
|
|
1770
1809
|
} catch {
|
|
1771
1810
|
return false;
|
|
@@ -1785,28 +1824,30 @@ function buildAuthorization(input) {
|
|
|
1785
1824
|
};
|
|
1786
1825
|
}
|
|
1787
1826
|
function signAuthorization(unsigned, payerDevicePrivateKey) {
|
|
1788
|
-
const payerSig =
|
|
1789
|
-
|
|
1827
|
+
const payerSig = signIssuerP256(
|
|
1828
|
+
canonicalJSONBytes(unsigned),
|
|
1829
|
+
payerDevicePrivateKey
|
|
1790
1830
|
);
|
|
1791
1831
|
return { ...unsigned, payerSig };
|
|
1792
1832
|
}
|
|
1793
|
-
function verifyAuthorization(auth,
|
|
1833
|
+
function verifyAuthorization(auth, issuerPublicKeySpkiB64) {
|
|
1794
1834
|
try {
|
|
1795
1835
|
const parsed = OfflinePaymentAuthorizationSchema.parse(auth);
|
|
1796
|
-
if (!verifyPaymentRequest(parsed.request,
|
|
1836
|
+
if (!verifyPaymentRequest(parsed.request, issuerPublicKeySpkiB64))
|
|
1837
|
+
return false;
|
|
1797
1838
|
const { issuerSig: payerOacSig, ...payerOacUnsigned } = parsed.payerOAC;
|
|
1798
|
-
if (!
|
|
1839
|
+
if (!verifyIssuerP256(
|
|
1799
1840
|
canonicalJSONBytes(payerOacUnsigned),
|
|
1800
|
-
|
|
1801
|
-
|
|
1841
|
+
payerOacSig,
|
|
1842
|
+
issuerPublicKeySpkiB64
|
|
1802
1843
|
)) {
|
|
1803
1844
|
return false;
|
|
1804
1845
|
}
|
|
1805
1846
|
const { payerSig, ...unsigned } = parsed;
|
|
1806
|
-
return
|
|
1847
|
+
return verifyIssuerP256(
|
|
1807
1848
|
canonicalJSONBytes(unsigned),
|
|
1808
|
-
|
|
1809
|
-
|
|
1849
|
+
payerSig,
|
|
1850
|
+
parsed.payerOAC.devicePublicKey
|
|
1810
1851
|
);
|
|
1811
1852
|
} catch {
|
|
1812
1853
|
return false;
|
|
@@ -1838,7 +1879,7 @@ function decodeAuthorizationQR(s) {
|
|
|
1838
1879
|
// src/offline/settlements.ts
|
|
1839
1880
|
import { z as z7 } from "zod";
|
|
1840
1881
|
import { sha256 } from "@noble/hashes/sha256";
|
|
1841
|
-
import { bytesToHex
|
|
1882
|
+
import { bytesToHex } from "@noble/hashes/utils";
|
|
1842
1883
|
var OfflineTokenSchema = z7.object({
|
|
1843
1884
|
tokenId: z7.string().uuid(),
|
|
1844
1885
|
tokenSerial: z7.string(),
|
|
@@ -1862,10 +1903,14 @@ var PaymentClaimSchema = z7.object({
|
|
|
1862
1903
|
occurredAtMs: z7.number().int().nonnegative(),
|
|
1863
1904
|
completedAtMs: z7.number().int().nonnegative().optional(),
|
|
1864
1905
|
contextId: z7.string().optional(),
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1906
|
+
// Stage 2c: P-256 device keys are now SubjectPublicKeyInfo DER, base64.
|
|
1907
|
+
// Signatures are ASN.1 DER ECDSA(SHA-256), base64. Backwards-incompatible
|
|
1908
|
+
// wire change; the backend has the matching widening in offline-settlements
|
|
1909
|
+
// service + zod schema.
|
|
1910
|
+
payerPubkey: z7.string().min(16).max(4096).regex(/^[A-Za-z0-9+/]+={0,2}$/),
|
|
1911
|
+
payerSignature: z7.string().min(16).max(4096).regex(/^[A-Za-z0-9+/]+={0,2}$/),
|
|
1912
|
+
payeePubkey: z7.string().min(16).max(4096).regex(/^[A-Za-z0-9+/]+={0,2}$/).optional(),
|
|
1913
|
+
payeeSignature: z7.string().min(16).max(4096).regex(/^[A-Za-z0-9+/]+={0,2}$/).optional()
|
|
1869
1914
|
});
|
|
1870
1915
|
var SettlementSchema = z7.object({
|
|
1871
1916
|
settlementId: z7.string().uuid(),
|
|
@@ -1889,7 +1934,7 @@ var SettleResponseSchema = z7.object({
|
|
|
1889
1934
|
});
|
|
1890
1935
|
var ENCOUNTER_DOMAIN = "offline:v1:encounter";
|
|
1891
1936
|
async function sha256Hex(input) {
|
|
1892
|
-
return
|
|
1937
|
+
return bytesToHex(sha256(new TextEncoder().encode(input)));
|
|
1893
1938
|
}
|
|
1894
1939
|
async function computeEncounterId(input) {
|
|
1895
1940
|
return sha256Hex(
|
|
@@ -2293,10 +2338,6 @@ var PASS_STATES = [
|
|
|
2293
2338
|
"expired",
|
|
2294
2339
|
"revoked"
|
|
2295
2340
|
];
|
|
2296
|
-
var HexString2 = (length) => z9.string().regex(
|
|
2297
|
-
new RegExp(`^[0-9a-fA-F]{${length * 2}}$`),
|
|
2298
|
-
`expected ${length}-byte hex string`
|
|
2299
|
-
);
|
|
2300
2341
|
var PassMetadataSchema = z9.record(
|
|
2301
2342
|
z9.union([z9.string(), z9.number(), z9.boolean(), z9.null()])
|
|
2302
2343
|
);
|
|
@@ -2317,9 +2358,9 @@ var PassSchema = z9.object({
|
|
|
2317
2358
|
nonce: z9.string().min(1),
|
|
2318
2359
|
/** Device id this pass is bound to (FK to backend `device_keys`). */
|
|
2319
2360
|
holderDeviceId: z9.string().min(1),
|
|
2320
|
-
/**
|
|
2321
|
-
* is verified against this key — it is the security-critical binding. */
|
|
2322
|
-
holderDevicePubkey:
|
|
2361
|
+
/** SubjectPublicKeyInfo DER (P-256) of the bound device, base64. The redemption
|
|
2362
|
+
* signature is verified against this key — it is the security-critical binding. */
|
|
2363
|
+
holderDevicePubkey: z9.string().min(64).max(4096).regex(/^[A-Za-z0-9+/]+={0,2}$/),
|
|
2323
2364
|
/** Optional fixed amount for monetary passes (vouchers, gift cards) in kobo. */
|
|
2324
2365
|
amountKobo: z9.number().int().nonnegative().optional(),
|
|
2325
2366
|
/** ISO-4217-ish currency code; required on the wire. SDK builders default to NGN. */
|
|
@@ -2328,7 +2369,8 @@ var PassSchema = z9.object({
|
|
|
2328
2369
|
counterSeed: z9.number().int().nonnegative(),
|
|
2329
2370
|
/** Optional cumulative spend cap in kobo across all redemptions of this pass. */
|
|
2330
2371
|
cumulativeCapKobo: z9.number().int().nonnegative().optional(),
|
|
2331
|
-
|
|
2372
|
+
/** ASN.1 DER ECDSA P-256 signature, base64. */
|
|
2373
|
+
issuerSig: z9.string().min(64).max(2048).regex(/^[A-Za-z0-9+/]+={0,2}$/)
|
|
2332
2374
|
}).refine((v) => v.validUntilMs > v.validFromMs, {
|
|
2333
2375
|
message: "validUntilMs must be greater than validFromMs"
|
|
2334
2376
|
});
|
|
@@ -2361,19 +2403,20 @@ function buildPass(input) {
|
|
|
2361
2403
|
return out;
|
|
2362
2404
|
}
|
|
2363
2405
|
function signPass(unsigned, issuerPrivateKey) {
|
|
2364
|
-
const issuerSig =
|
|
2365
|
-
|
|
2406
|
+
const issuerSig = signIssuerP256(
|
|
2407
|
+
canonicalJSONBytes(unsigned),
|
|
2408
|
+
issuerPrivateKey
|
|
2366
2409
|
);
|
|
2367
2410
|
return { ...unsigned, issuerSig };
|
|
2368
2411
|
}
|
|
2369
|
-
function verifyPass(pass,
|
|
2412
|
+
function verifyPass(pass, issuerPublicKeySpkiB64) {
|
|
2370
2413
|
try {
|
|
2371
2414
|
const parsed = PassSchema.parse(pass);
|
|
2372
2415
|
const { issuerSig, ...unsigned } = parsed;
|
|
2373
|
-
return
|
|
2416
|
+
return verifyIssuerP256(
|
|
2374
2417
|
canonicalJSONBytes(unsigned),
|
|
2375
|
-
|
|
2376
|
-
|
|
2418
|
+
issuerSig,
|
|
2419
|
+
issuerPublicKeySpkiB64
|
|
2377
2420
|
);
|
|
2378
2421
|
} catch {
|
|
2379
2422
|
return false;
|
|
@@ -2385,7 +2428,7 @@ function isPassWithinValidity(pass, nowMs) {
|
|
|
2385
2428
|
|
|
2386
2429
|
// src/passes/redemption.ts
|
|
2387
2430
|
import { z as z10 } from "zod";
|
|
2388
|
-
var
|
|
2431
|
+
var Base64Std2 = z10.string().min(16).max(2048).regex(/^[A-Za-z0-9+/]+={0,2}$/, "expected base64 (std)");
|
|
2389
2432
|
var RedemptionSchema = z10.object({
|
|
2390
2433
|
pass: PassSchema,
|
|
2391
2434
|
redeemerId: z10.string().min(1),
|
|
@@ -2396,7 +2439,8 @@ var RedemptionSchema = z10.object({
|
|
|
2396
2439
|
/** Amount being redeemed in kobo (0 for non-monetary passes like ride tickets). */
|
|
2397
2440
|
amountKobo: z10.number().int().nonnegative(),
|
|
2398
2441
|
nonce: z10.string().min(1),
|
|
2399
|
-
|
|
2442
|
+
/** ASN.1 DER ECDSA P-256 signature over canonicalJSONBytes(unsigned), base64. */
|
|
2443
|
+
holderSig: Base64Std2
|
|
2400
2444
|
});
|
|
2401
2445
|
var REDEEMABLE_STATES = /* @__PURE__ */ new Set(["issued", "active"]);
|
|
2402
2446
|
function buildRedemption(input) {
|
|
@@ -2450,31 +2494,28 @@ function buildRedemption(input) {
|
|
|
2450
2494
|
};
|
|
2451
2495
|
}
|
|
2452
2496
|
function signRedemption(unsigned, holderDevicePrivateKey) {
|
|
2453
|
-
const holderSig =
|
|
2454
|
-
|
|
2497
|
+
const holderSig = signIssuerP256(
|
|
2498
|
+
canonicalJSONBytes(unsigned),
|
|
2499
|
+
holderDevicePrivateKey
|
|
2455
2500
|
);
|
|
2456
2501
|
return { ...unsigned, holderSig };
|
|
2457
2502
|
}
|
|
2458
|
-
function verifyRedemption(r,
|
|
2503
|
+
function verifyRedemption(r, issuerPublicKeySpkiB64) {
|
|
2459
2504
|
try {
|
|
2460
2505
|
const parsed = RedemptionSchema.parse(r);
|
|
2461
2506
|
if (parsed.counter <= parsed.pass.counterSeed) return false;
|
|
2462
2507
|
const { issuerSig, ...passUnsigned } = parsed.pass;
|
|
2463
|
-
if (!
|
|
2508
|
+
if (!verifyIssuerP256(
|
|
2464
2509
|
canonicalJSONBytes(passUnsigned),
|
|
2465
|
-
|
|
2466
|
-
|
|
2510
|
+
issuerSig,
|
|
2511
|
+
issuerPublicKeySpkiB64
|
|
2467
2512
|
)) {
|
|
2468
2513
|
return false;
|
|
2469
2514
|
}
|
|
2470
|
-
const
|
|
2471
|
-
if (typeof
|
|
2515
|
+
const holderPub = parsed.pass.holderDevicePubkey;
|
|
2516
|
+
if (typeof holderPub !== "string") return false;
|
|
2472
2517
|
const { holderSig, ...unsigned } = parsed;
|
|
2473
|
-
return
|
|
2474
|
-
canonicalJSONBytes(unsigned),
|
|
2475
|
-
hexToBytes(holderSig),
|
|
2476
|
-
hexToBytes(holderHex)
|
|
2477
|
-
);
|
|
2518
|
+
return verifyIssuerP256(canonicalJSONBytes(unsigned), holderSig, holderPub);
|
|
2478
2519
|
} catch {
|
|
2479
2520
|
return false;
|
|
2480
2521
|
}
|
|
@@ -2484,10 +2525,6 @@ function verifyRedemption(r, issuerPublicKey) {
|
|
|
2484
2525
|
import { z as z11 } from "zod";
|
|
2485
2526
|
var RECEIPT_CHANNELS = ["cash", "pass"];
|
|
2486
2527
|
var RECEIPT_KINDS = RECEIPT_CHANNELS;
|
|
2487
|
-
var HexString3 = (length) => z11.string().regex(
|
|
2488
|
-
new RegExp(`^[0-9a-fA-F]{${length * 2}}$`),
|
|
2489
|
-
`expected ${length}-byte hex string`
|
|
2490
|
-
);
|
|
2491
2528
|
var ReceiptPayloadSchema = z11.record(
|
|
2492
2529
|
z11.union([z11.string(), z11.number(), z11.boolean(), z11.null()])
|
|
2493
2530
|
);
|
|
@@ -2505,7 +2542,8 @@ var ReceiptSchema = z11.object({
|
|
|
2505
2542
|
issuedAtMs: z11.number().int().nonnegative(),
|
|
2506
2543
|
issuerId: z11.string().min(1),
|
|
2507
2544
|
payload: ReceiptPayloadSchema,
|
|
2508
|
-
|
|
2545
|
+
/** ASN.1 DER ECDSA P-256 signature, base64. */
|
|
2546
|
+
issuerSig: z11.string().min(64).max(2048).regex(/^[A-Za-z0-9+/]+={0,2}$/)
|
|
2509
2547
|
}).superRefine((v, ctx) => {
|
|
2510
2548
|
if (v.channel === "cash") {
|
|
2511
2549
|
if (!v.intentId) {
|
|
@@ -2571,19 +2609,20 @@ function buildReceipt(input) {
|
|
|
2571
2609
|
return out;
|
|
2572
2610
|
}
|
|
2573
2611
|
function signReceipt(unsigned, issuerPrivateKey) {
|
|
2574
|
-
const issuerSig =
|
|
2575
|
-
|
|
2612
|
+
const issuerSig = signIssuerP256(
|
|
2613
|
+
canonicalJSONBytes(unsigned),
|
|
2614
|
+
issuerPrivateKey
|
|
2576
2615
|
);
|
|
2577
2616
|
return { ...unsigned, issuerSig };
|
|
2578
2617
|
}
|
|
2579
|
-
function verifyReceipt(r,
|
|
2618
|
+
function verifyReceipt(r, issuerPublicKeySpkiB64) {
|
|
2580
2619
|
try {
|
|
2581
2620
|
const parsed = ReceiptSchema.parse(r);
|
|
2582
2621
|
const { issuerSig, ...unsigned } = parsed;
|
|
2583
|
-
return
|
|
2622
|
+
return verifyIssuerP256(
|
|
2584
2623
|
canonicalJSONBytes(unsigned),
|
|
2585
|
-
|
|
2586
|
-
|
|
2624
|
+
issuerSig,
|
|
2625
|
+
issuerPublicKeySpkiB64
|
|
2587
2626
|
);
|
|
2588
2627
|
} catch {
|
|
2589
2628
|
return false;
|
|
@@ -2918,9 +2957,8 @@ function createAccountsClient(opts) {
|
|
|
2918
2957
|
// src/me-offline/client.ts
|
|
2919
2958
|
import { z as z13 } from "zod";
|
|
2920
2959
|
var Hex64 = z13.string().regex(/^[0-9a-f]{64}$/i);
|
|
2921
|
-
var HexAny = z13.string().regex(/^[0-9a-f]+$/i);
|
|
2922
2960
|
var Sha256Hex = z13.string().regex(/^[0-9a-f]{64}$/i);
|
|
2923
|
-
var
|
|
2961
|
+
var Base64Std3 = z13.string().regex(/^[A-Za-z0-9+/]+={0,2}$/);
|
|
2924
2962
|
var RegisterDeviceKeyInputSchema = z13.object({
|
|
2925
2963
|
deviceId: z13.string().min(1).max(128),
|
|
2926
2964
|
publicKeyHex: Hex64
|
|
@@ -2931,15 +2969,15 @@ var AttestationSecurityLevelSchema = z13.enum([
|
|
|
2931
2969
|
"SECURE_ENCLAVE",
|
|
2932
2970
|
"SOFTWARE"
|
|
2933
2971
|
]);
|
|
2934
|
-
var DeviceKeyAlgSchema = z13.
|
|
2972
|
+
var DeviceKeyAlgSchema = z13.literal("p256");
|
|
2935
2973
|
var RegisterDeviceKeyP256InputSchema = z13.object({
|
|
2936
2974
|
deviceId: z13.string().min(1).max(128),
|
|
2937
2975
|
/** P-256 SubjectPublicKeyInfo DER, base64. */
|
|
2938
|
-
publicKeySpkiB64:
|
|
2976
|
+
publicKeySpkiB64: Base64Std3.min(64).max(4096),
|
|
2939
2977
|
/** Base64 of the server-issued enrollment challenge string. */
|
|
2940
|
-
challengeB64:
|
|
2978
|
+
challengeB64: Base64Std3.min(8).max(1024),
|
|
2941
2979
|
/** iOS App Attest payload or Android X.509 Key Attestation chain. */
|
|
2942
|
-
attestationChainB64: z13.array(
|
|
2980
|
+
attestationChainB64: z13.array(Base64Std3.min(16).max(16384)).min(1).max(16),
|
|
2943
2981
|
securityLevel: AttestationSecurityLevelSchema
|
|
2944
2982
|
});
|
|
2945
2983
|
var P256EnrollmentChallengeInputSchema = z13.object({
|
|
@@ -2953,9 +2991,12 @@ var DeviceKeyRecordSchema = z13.object({
|
|
|
2953
2991
|
id: z13.string().uuid(),
|
|
2954
2992
|
userId: z13.string().uuid(),
|
|
2955
2993
|
deviceId: z13.string(),
|
|
2956
|
-
|
|
2994
|
+
/** Always 'p256' on the consumer offline rail. Field retained for forward-compat. */
|
|
2995
|
+
alg: DeviceKeyAlgSchema.default("p256"),
|
|
2996
|
+
/** Legacy ed25519 hex key. Always null on new records (kept for back-compat reads). */
|
|
2957
2997
|
publicKeyHex: Hex64.nullable().default(null),
|
|
2958
|
-
|
|
2998
|
+
/** P-256 SubjectPublicKeyInfo DER, base64. Required for new records. */
|
|
2999
|
+
publicKeySpkiB64: Base64Std3.nullable().default(null),
|
|
2959
3000
|
securityLevel: AttestationSecurityLevelSchema.nullable().default(null),
|
|
2960
3001
|
hardwareBacked: z13.boolean().default(false),
|
|
2961
3002
|
attestedAtMs: z13.number().int().nonnegative().nullable().default(null),
|
|
@@ -2967,9 +3008,10 @@ var ConsumerOACSchema = z13.object({
|
|
|
2967
3008
|
issuerId: z13.string().min(1).max(64),
|
|
2968
3009
|
userId: z13.string().uuid(),
|
|
2969
3010
|
deviceId: z13.string().min(1).max(128),
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
3011
|
+
/** Always 'p256'. Field retained for forward-compat. */
|
|
3012
|
+
alg: z13.literal("p256").default("p256"),
|
|
3013
|
+
/** P-256 SubjectPublicKeyInfo DER, base64. */
|
|
3014
|
+
devicePubkeySpkiB64: Base64Std3.min(64).max(4096),
|
|
2973
3015
|
perTxCapKobo: z13.number().int().positive(),
|
|
2974
3016
|
cumulativeCapKobo: z13.number().int().positive(),
|
|
2975
3017
|
currency: z13.string().length(3),
|
|
@@ -2977,20 +3019,13 @@ var ConsumerOACSchema = z13.object({
|
|
|
2977
3019
|
validUntilMs: z13.number().int().nonnegative(),
|
|
2978
3020
|
counterSeed: z13.number().int().nonnegative(),
|
|
2979
3021
|
issuedAtMs: z13.number().int().nonnegative()
|
|
2980
|
-
})
|
|
2981
|
-
(o) => {
|
|
2982
|
-
const alg = o.alg ?? "ed25519";
|
|
2983
|
-
if (alg === "ed25519") {
|
|
2984
|
-
return Boolean(o.devicePubkeyHex) && !o.devicePubkeySpkiB64;
|
|
2985
|
-
}
|
|
2986
|
-
return Boolean(o.devicePubkeySpkiB64) && !o.devicePubkeyHex;
|
|
2987
|
-
},
|
|
2988
|
-
{ message: "OAC device pubkey shape must match alg" }
|
|
2989
|
-
);
|
|
3022
|
+
});
|
|
2990
3023
|
var SignedConsumerOACSchema = z13.object({
|
|
2991
3024
|
oac: ConsumerOACSchema,
|
|
2992
|
-
|
|
2993
|
-
|
|
3025
|
+
/** ASN.1 DER ECDSA P-256 issuer signature, base64. */
|
|
3026
|
+
issuerSig: Base64Std3.min(16).max(2048),
|
|
3027
|
+
/** Issuer's P-256 public key as SubjectPublicKeyInfo DER, base64. */
|
|
3028
|
+
issuerPublicKeySpkiB64: Base64Std3.min(64).max(4096)
|
|
2994
3029
|
});
|
|
2995
3030
|
var OACRecordSchema = SignedConsumerOACSchema.extend({
|
|
2996
3031
|
currentOfflineSpentKobo: z13.number().int().nonnegative(),
|
|
@@ -3073,10 +3108,9 @@ var OfflineStatusResultSchema = z13.object({
|
|
|
3073
3108
|
var OfflineStateResultSchema = z13.object({
|
|
3074
3109
|
active: OACRecordSchema.nullable()
|
|
3075
3110
|
});
|
|
3076
|
-
var ClaimAlgSchema = z13.enum(["ed25519", "p256"]);
|
|
3077
3111
|
var ConsumerPaymentClaimSchema = z13.object({
|
|
3078
|
-
/**
|
|
3079
|
-
alg:
|
|
3112
|
+
/** Always 'p256'. Retained for forward-compat and as an explicit domain marker. */
|
|
3113
|
+
alg: z13.literal("p256").default("p256"),
|
|
3080
3114
|
oacId: z13.string().uuid(),
|
|
3081
3115
|
encounterId: Sha256Hex.optional(),
|
|
3082
3116
|
payerUserId: z13.string().uuid(),
|
|
@@ -3089,28 +3123,11 @@ var ConsumerPaymentClaimSchema = z13.object({
|
|
|
3089
3123
|
occurredAtMs: z13.number().int().nonnegative(),
|
|
3090
3124
|
completedAtMs: z13.number().int().nonnegative().optional(),
|
|
3091
3125
|
contextId: z13.string().max(128).optional(),
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
// p256 path
|
|
3098
|
-
payerPubkeySpkiB64: z13.string().min(64).max(4096).optional(),
|
|
3099
|
-
payerSignatureDerB64: z13.string().min(16).max(2048).optional(),
|
|
3100
|
-
payeePubkeySpkiB64: z13.string().min(64).max(4096).optional(),
|
|
3101
|
-
payeeSignatureDerB64: z13.string().min(16).max(2048).optional()
|
|
3102
|
-
}).refine(
|
|
3103
|
-
(c) => {
|
|
3104
|
-
const alg = c.alg ?? "ed25519";
|
|
3105
|
-
if (alg === "ed25519") {
|
|
3106
|
-
return Boolean(c.payerPubkeyHex) && Boolean(c.payerSignature);
|
|
3107
|
-
}
|
|
3108
|
-
return Boolean(c.payerPubkeySpkiB64) && Boolean(c.payerSignatureDerB64);
|
|
3109
|
-
},
|
|
3110
|
-
{
|
|
3111
|
-
message: "payer key/signature fields must match alg (ed25519: hex; p256: SPKI+DER b64)"
|
|
3112
|
-
}
|
|
3113
|
-
);
|
|
3126
|
+
payerPubkeySpkiB64: Base64Std3.min(64).max(4096),
|
|
3127
|
+
payerSignatureDerB64: Base64Std3.min(16).max(2048),
|
|
3128
|
+
payeePubkeySpkiB64: Base64Std3.min(64).max(4096).optional(),
|
|
3129
|
+
payeeSignatureDerB64: Base64Std3.min(16).max(2048).optional()
|
|
3130
|
+
});
|
|
3114
3131
|
var ConsumerSettlementSchema = z13.object({
|
|
3115
3132
|
settlementId: z13.string().uuid(),
|
|
3116
3133
|
settlementKey: Sha256Hex,
|
|
@@ -3123,7 +3140,8 @@ var ConsumerSettlementSchema = z13.object({
|
|
|
3123
3140
|
status: z13.enum(["SETTLED", "REVIEW"]),
|
|
3124
3141
|
reviewReason: z13.string().nullable(),
|
|
3125
3142
|
ledgerRef: z13.string().nullable(),
|
|
3126
|
-
|
|
3143
|
+
/** ASN.1 DER ECDSA P-256 issuer signature, base64. */
|
|
3144
|
+
issuerSig: Base64Std3.min(16).max(2048),
|
|
3127
3145
|
createdAtMs: z13.number().int().nonnegative()
|
|
3128
3146
|
});
|
|
3129
3147
|
var ConsumerSettleResultSchema = z13.object({
|
|
@@ -3251,9 +3269,7 @@ function createMeOfflineClient(opts) {
|
|
|
3251
3269
|
}
|
|
3252
3270
|
|
|
3253
3271
|
// src/me-offline/signer.ts
|
|
3254
|
-
import {
|
|
3255
|
-
import { p256 } from "@noble/curves/nist";
|
|
3256
|
-
import { bytesToHex as bytesToHex5, hexToBytes as hexToBytes2 } from "@noble/hashes/utils";
|
|
3272
|
+
import { p256 as p2562 } from "@noble/curves/nist";
|
|
3257
3273
|
var CLAIM_DOMAIN_V2 = "flur:consumer-offline:v2:claim";
|
|
3258
3274
|
function canonicalClaimSigningPayload(claim) {
|
|
3259
3275
|
return {
|
|
@@ -3275,7 +3291,7 @@ function canonicalClaimSigningPayload(claim) {
|
|
|
3275
3291
|
function canonicalClaimSigningBytes(claim) {
|
|
3276
3292
|
return canonicalJSONBytes(canonicalClaimSigningPayload(claim));
|
|
3277
3293
|
}
|
|
3278
|
-
function
|
|
3294
|
+
function bytesToBase642(bytes) {
|
|
3279
3295
|
if (typeof Buffer !== "undefined") {
|
|
3280
3296
|
return Buffer.from(bytes).toString("base64");
|
|
3281
3297
|
}
|
|
@@ -3283,7 +3299,7 @@ function bytesToBase64(bytes) {
|
|
|
3283
3299
|
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
3284
3300
|
return btoa(bin);
|
|
3285
3301
|
}
|
|
3286
|
-
function
|
|
3302
|
+
function base64ToBytes2(b64) {
|
|
3287
3303
|
if (typeof Buffer !== "undefined") {
|
|
3288
3304
|
return new Uint8Array(Buffer.from(b64, "base64"));
|
|
3289
3305
|
}
|
|
@@ -3292,7 +3308,7 @@ function base64ToBytes(b64) {
|
|
|
3292
3308
|
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
3293
3309
|
return out;
|
|
3294
3310
|
}
|
|
3295
|
-
var
|
|
3311
|
+
var P256_SPKI_HEADER2 = new Uint8Array([
|
|
3296
3312
|
48,
|
|
3297
3313
|
89,
|
|
3298
3314
|
48,
|
|
@@ -3324,38 +3340,25 @@ function p256PublicKeyToSpkiB64(rawUncompressed) {
|
|
|
3324
3340
|
if (rawUncompressed.length !== 65 || rawUncompressed[0] !== 4) {
|
|
3325
3341
|
throw new Error("p256: expected 65-byte uncompressed point");
|
|
3326
3342
|
}
|
|
3327
|
-
const out = new Uint8Array(
|
|
3328
|
-
out.set(
|
|
3329
|
-
out.set(rawUncompressed,
|
|
3330
|
-
return
|
|
3343
|
+
const out = new Uint8Array(P256_SPKI_HEADER2.length + rawUncompressed.length);
|
|
3344
|
+
out.set(P256_SPKI_HEADER2, 0);
|
|
3345
|
+
out.set(rawUncompressed, P256_SPKI_HEADER2.length);
|
|
3346
|
+
return bytesToBase642(out);
|
|
3331
3347
|
}
|
|
3332
3348
|
function p256SpkiB64ToPublicKey(spkiB64) {
|
|
3333
|
-
const spki =
|
|
3334
|
-
if (spki.length !==
|
|
3349
|
+
const spki = base64ToBytes2(spkiB64);
|
|
3350
|
+
if (spki.length !== P256_SPKI_HEADER2.length + 65) {
|
|
3335
3351
|
throw new Error("p256: invalid SPKI length");
|
|
3336
3352
|
}
|
|
3337
|
-
for (let i = 0; i <
|
|
3338
|
-
if (spki[i] !==
|
|
3353
|
+
for (let i = 0; i < P256_SPKI_HEADER2.length; i++) {
|
|
3354
|
+
if (spki[i] !== P256_SPKI_HEADER2[i]) {
|
|
3339
3355
|
throw new Error("p256: invalid SPKI header");
|
|
3340
3356
|
}
|
|
3341
3357
|
}
|
|
3342
|
-
return spki.slice(
|
|
3343
|
-
}
|
|
3344
|
-
function createSoftwareEd25519Signer(privateKey) {
|
|
3345
|
-
const pub = ed255192.getPublicKey(privateKey);
|
|
3346
|
-
return {
|
|
3347
|
-
alg: "ed25519",
|
|
3348
|
-
async getPublicKey() {
|
|
3349
|
-
return { alg: "ed25519", publicKey: bytesToHex5(pub) };
|
|
3350
|
-
},
|
|
3351
|
-
async sign(bytes) {
|
|
3352
|
-
const sig = ed255192.sign(bytes, privateKey);
|
|
3353
|
-
return { alg: "ed25519", signature: bytesToHex5(sig) };
|
|
3354
|
-
}
|
|
3355
|
-
};
|
|
3358
|
+
return spki.slice(P256_SPKI_HEADER2.length);
|
|
3356
3359
|
}
|
|
3357
3360
|
function createSoftwareP256Signer(privateKey) {
|
|
3358
|
-
const raw =
|
|
3361
|
+
const raw = p2562.getPublicKey(privateKey, false);
|
|
3359
3362
|
const spkiB64 = p256PublicKeyToSpkiB64(raw);
|
|
3360
3363
|
return {
|
|
3361
3364
|
alg: "p256",
|
|
@@ -3363,35 +3366,87 @@ function createSoftwareP256Signer(privateKey) {
|
|
|
3363
3366
|
return { alg: "p256", publicKey: spkiB64 };
|
|
3364
3367
|
},
|
|
3365
3368
|
async sign(bytes) {
|
|
3366
|
-
const sig =
|
|
3369
|
+
const sig = p2562.sign(bytes, privateKey, { prehash: true });
|
|
3367
3370
|
const der = sig.toBytes("der");
|
|
3368
|
-
return { alg: "p256", signature:
|
|
3371
|
+
return { alg: "p256", signature: bytesToBase642(der) };
|
|
3369
3372
|
}
|
|
3370
3373
|
};
|
|
3371
3374
|
}
|
|
3372
3375
|
function verifyClaimSignature(input) {
|
|
3373
3376
|
try {
|
|
3374
|
-
if (input.alg
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
}
|
|
3381
|
-
if (input.alg === "p256") {
|
|
3382
|
-
const sigDer = base64ToBytes(input.signature);
|
|
3383
|
-
const pub = p256SpkiB64ToPublicKey(input.publicKey);
|
|
3384
|
-
return p256.verify(sigDer, input.bytes, pub, {
|
|
3385
|
-
prehash: true,
|
|
3386
|
-
format: "der"
|
|
3387
|
-
});
|
|
3388
|
-
}
|
|
3389
|
-
return false;
|
|
3377
|
+
if (input.alg !== "p256") return false;
|
|
3378
|
+
const sigDer = base64ToBytes2(input.signature);
|
|
3379
|
+
const pub = p256SpkiB64ToPublicKey(input.publicKey);
|
|
3380
|
+
return p2562.verify(sigDer, input.bytes, pub, {
|
|
3381
|
+
prehash: true,
|
|
3382
|
+
format: "der"
|
|
3383
|
+
});
|
|
3390
3384
|
} catch {
|
|
3391
3385
|
return false;
|
|
3392
3386
|
}
|
|
3393
3387
|
}
|
|
3394
3388
|
|
|
3389
|
+
// src/me-offline/sms.ts
|
|
3390
|
+
var OFFLINE_CLAIM_SMS_PREFIX = "FLURC1.";
|
|
3391
|
+
var TOKEN_RE = /(?:^|\s)(FLURC1\.[A-Za-z0-9_-]+={0,2})(?:\s|$)/;
|
|
3392
|
+
function encodeOfflineClaimSmsMessage(claim) {
|
|
3393
|
+
const parsed = ConsumerPaymentClaimSchema.parse(claim);
|
|
3394
|
+
const json = JSON.stringify(parsed);
|
|
3395
|
+
return `${OFFLINE_CLAIM_SMS_PREFIX}${base64UrlEncodeUtf8(json)}`;
|
|
3396
|
+
}
|
|
3397
|
+
function decodeOfflineClaimSmsMessage(message) {
|
|
3398
|
+
const token = extractOfflineClaimSmsToken(message);
|
|
3399
|
+
if (!token) {
|
|
3400
|
+
throw new Error("offline claim SMS token not found");
|
|
3401
|
+
}
|
|
3402
|
+
const encoded = token.slice(OFFLINE_CLAIM_SMS_PREFIX.length);
|
|
3403
|
+
let raw;
|
|
3404
|
+
try {
|
|
3405
|
+
raw = JSON.parse(base64UrlDecodeUtf8(encoded));
|
|
3406
|
+
} catch {
|
|
3407
|
+
throw new Error("offline claim SMS token is malformed");
|
|
3408
|
+
}
|
|
3409
|
+
const parsed = ConsumerPaymentClaimSchema.safeParse(raw);
|
|
3410
|
+
if (!parsed.success) {
|
|
3411
|
+
throw new Error("offline claim SMS token is invalid");
|
|
3412
|
+
}
|
|
3413
|
+
return parsed.data;
|
|
3414
|
+
}
|
|
3415
|
+
function extractOfflineClaimSmsToken(message) {
|
|
3416
|
+
const trimmed = message.trim();
|
|
3417
|
+
if (trimmed.startsWith(OFFLINE_CLAIM_SMS_PREFIX)) {
|
|
3418
|
+
return trimmed.split(/\s+/, 1)[0] ?? null;
|
|
3419
|
+
}
|
|
3420
|
+
return TOKEN_RE.exec(message)?.[1] ?? null;
|
|
3421
|
+
}
|
|
3422
|
+
function base64UrlEncodeUtf8(input) {
|
|
3423
|
+
const bytes = new TextEncoder().encode(input);
|
|
3424
|
+
let binary = "";
|
|
3425
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
3426
|
+
const base64 = typeof btoa === "function" ? btoa(binary) : typeof Buffer !== "undefined" ? Buffer.from(bytes).toString("base64") : void 0;
|
|
3427
|
+
if (!base64) throw new Error("base64 encoder unavailable");
|
|
3428
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
3429
|
+
}
|
|
3430
|
+
function base64UrlDecodeUtf8(input) {
|
|
3431
|
+
const base64 = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
3432
|
+
const padded = base64.padEnd(
|
|
3433
|
+
base64.length + (4 - base64.length % 4) % 4,
|
|
3434
|
+
"="
|
|
3435
|
+
);
|
|
3436
|
+
if (typeof atob === "function") {
|
|
3437
|
+
const binary = atob(padded);
|
|
3438
|
+
const bytes = new Uint8Array(binary.length);
|
|
3439
|
+
for (let index = 0; index < binary.length; index++) {
|
|
3440
|
+
bytes[index] = binary.charCodeAt(index);
|
|
3441
|
+
}
|
|
3442
|
+
return new TextDecoder().decode(bytes);
|
|
3443
|
+
}
|
|
3444
|
+
if (typeof Buffer !== "undefined") {
|
|
3445
|
+
return Buffer.from(padded, "base64").toString("utf8");
|
|
3446
|
+
}
|
|
3447
|
+
throw new Error("base64 decoder unavailable");
|
|
3448
|
+
}
|
|
3449
|
+
|
|
3395
3450
|
// src/partner-funding/client.ts
|
|
3396
3451
|
import { z as z14 } from "zod";
|
|
3397
3452
|
var MinorString = z14.string().regex(/^-?\d+$/);
|
|
@@ -3767,14 +3822,15 @@ function buildArtifactBody(input) {
|
|
|
3767
3822
|
return { ...header, data: input.data };
|
|
3768
3823
|
}
|
|
3769
3824
|
function signArtifact(body, privateKey) {
|
|
3770
|
-
const sig =
|
|
3825
|
+
const sig = signIssuerP256(canonicalJSONBytes(body), privateKey);
|
|
3771
3826
|
return { body, sig };
|
|
3772
3827
|
}
|
|
3773
3828
|
function encodeArtifactUri(signed) {
|
|
3774
3829
|
const bodyBytes = canonicalJSONBytes(signed.body);
|
|
3775
3830
|
const bodyB64 = base64UrlEncode(bodyBytes);
|
|
3776
|
-
const
|
|
3777
|
-
|
|
3831
|
+
const sigBytes = base64UrlDecode(signed.sig);
|
|
3832
|
+
const sigB64Url = base64UrlEncode(sigBytes);
|
|
3833
|
+
return `${FLUR_ARTIFACT_URI_PREFIX}${signed.body.t}/${bodyB64}.${sigB64Url}`;
|
|
3778
3834
|
}
|
|
3779
3835
|
function decodeArtifactUri(uri) {
|
|
3780
3836
|
if (!uri.startsWith(FLUR_ARTIFACT_URI_PREFIX)) {
|
|
@@ -3805,9 +3861,9 @@ function decodeArtifactUri(uri) {
|
|
|
3805
3861
|
}
|
|
3806
3862
|
const bodyBytes = base64UrlDecode(payload.slice(0, dot));
|
|
3807
3863
|
const sigBytes = base64UrlDecode(payload.slice(dot + 1));
|
|
3808
|
-
if (sigBytes.length
|
|
3864
|
+
if (sigBytes.length < 64 || sigBytes.length > 80) {
|
|
3809
3865
|
throw new FlurArtifactError(
|
|
3810
|
-
`Signature
|
|
3866
|
+
`Signature length out of range: ${sigBytes.length}`,
|
|
3811
3867
|
"INVALID_SIGNATURE"
|
|
3812
3868
|
);
|
|
3813
3869
|
}
|
|
@@ -3834,17 +3890,26 @@ function decodeArtifactUri(uri) {
|
|
|
3834
3890
|
type,
|
|
3835
3891
|
bodyBytes,
|
|
3836
3892
|
body: bodyJson,
|
|
3837
|
-
|
|
3893
|
+
// Encode as standard base64 (not url-safe) for sig field consistency.
|
|
3894
|
+
sig: encodeStdBase64(sigBytes)
|
|
3838
3895
|
};
|
|
3839
3896
|
}
|
|
3840
|
-
function
|
|
3897
|
+
function encodeStdBase64(bytes) {
|
|
3898
|
+
if (typeof Buffer !== "undefined") {
|
|
3899
|
+
return Buffer.from(bytes).toString("base64");
|
|
3900
|
+
}
|
|
3901
|
+
let bin = "";
|
|
3902
|
+
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
3903
|
+
return typeof btoa === "function" ? btoa(bin) : "";
|
|
3904
|
+
}
|
|
3905
|
+
function verifyArtifactSignature(decoded, publicKeySpkiB64, options = {}) {
|
|
3841
3906
|
if (options.enforceExpiry !== false && decoded.body.exp !== void 0) {
|
|
3842
3907
|
const now = options.nowSeconds ?? Math.floor(Date.now() / 1e3);
|
|
3843
3908
|
if (decoded.body.exp < now) {
|
|
3844
3909
|
throw new FlurArtifactError("Artifact has expired", "EXPIRED");
|
|
3845
3910
|
}
|
|
3846
3911
|
}
|
|
3847
|
-
return
|
|
3912
|
+
return verifyIssuerP256(decoded.bodyBytes, decoded.sig, publicKeySpkiB64);
|
|
3848
3913
|
}
|
|
3849
3914
|
|
|
3850
3915
|
// src/artifacts/types.ts
|
|
@@ -3863,7 +3928,7 @@ var ARTIFACT_TYPES = {
|
|
|
3863
3928
|
PASS: "pass",
|
|
3864
3929
|
IDENTITY: "identity"
|
|
3865
3930
|
};
|
|
3866
|
-
var
|
|
3931
|
+
var HexString = (length) => z16.string().regex(
|
|
3867
3932
|
new RegExp(`^[0-9a-fA-F]{${length * 2}}$`),
|
|
3868
3933
|
`expected ${length}-byte hex string`
|
|
3869
3934
|
);
|
|
@@ -3881,7 +3946,7 @@ var ReceiptArtifactSchema = z16.object({
|
|
|
3881
3946
|
settledAtMs: z16.number().int().positive(),
|
|
3882
3947
|
ledgerTxnId: z16.string().min(1).max(64).optional(),
|
|
3883
3948
|
memo: z16.string().max(140).optional(),
|
|
3884
|
-
hashChainPrev:
|
|
3949
|
+
hashChainPrev: HexString(32).optional()
|
|
3885
3950
|
});
|
|
3886
3951
|
var ShortId = z16.string().min(1).max(64);
|
|
3887
3952
|
var PositiveInt = z16.number().int().positive();
|
|
@@ -3963,7 +4028,7 @@ var StatementArtifactSchema = z16.object({
|
|
|
3963
4028
|
closingBalanceKobo: z16.number().int(),
|
|
3964
4029
|
transactionCount: NonNegativeInt,
|
|
3965
4030
|
currency: Currency2,
|
|
3966
|
-
hashChainPrev:
|
|
4031
|
+
hashChainPrev: HexString(32).optional()
|
|
3967
4032
|
}).refine((v) => v.periodEndMs > v.periodStartMs, {
|
|
3968
4033
|
message: "periodEndMs must be greater than periodStartMs",
|
|
3969
4034
|
path: ["periodEndMs"]
|
|
@@ -3996,7 +4061,7 @@ var IdentityArtifactSchema = z16.object({
|
|
|
3996
4061
|
"kyc_tier",
|
|
3997
4062
|
"age_band"
|
|
3998
4063
|
]),
|
|
3999
|
-
claimValueHash:
|
|
4064
|
+
claimValueHash: HexString(32),
|
|
4000
4065
|
attestedAtMs: PositiveInt
|
|
4001
4066
|
});
|
|
4002
4067
|
var ARTIFACT_BODY_SCHEMAS = {
|
|
@@ -4060,7 +4125,7 @@ function createArtifactUri(input) {
|
|
|
4060
4125
|
const signed = signArtifact(body, input.privateKey);
|
|
4061
4126
|
return { uri: encodeArtifactUri(signed), signed };
|
|
4062
4127
|
}
|
|
4063
|
-
function verifyArtifactUri(uri,
|
|
4128
|
+
function verifyArtifactUri(uri, publicKeySpkiB64, options = {}) {
|
|
4064
4129
|
const decoded = decodeArtifactUri(uri);
|
|
4065
4130
|
if (!isKnownArtifactType(decoded.type)) {
|
|
4066
4131
|
throw new FlurArtifactError(
|
|
@@ -4082,7 +4147,7 @@ function verifyArtifactUri(uri, publicKey, options = {}) {
|
|
|
4082
4147
|
"INVALID_BODY"
|
|
4083
4148
|
);
|
|
4084
4149
|
}
|
|
4085
|
-
const ok = verifyArtifactSignature(decoded,
|
|
4150
|
+
const ok = verifyArtifactSignature(decoded, publicKeySpkiB64, options);
|
|
4086
4151
|
if (!ok) {
|
|
4087
4152
|
throw new FlurArtifactError(
|
|
4088
4153
|
"Artifact signature verification failed",
|
|
@@ -4175,6 +4240,7 @@ export {
|
|
|
4175
4240
|
OAC_DEFAULT_CUMULATIVE_KOBO,
|
|
4176
4241
|
OAC_DEFAULT_PER_TX_KOBO,
|
|
4177
4242
|
OAC_DEFAULT_VALIDITY_MS,
|
|
4243
|
+
OFFLINE_CLAIM_SMS_PREFIX,
|
|
4178
4244
|
OfflineClaimArtifactSchema,
|
|
4179
4245
|
OfflineHoldRecordSchema,
|
|
4180
4246
|
OfflinePaymentAuthorizationArtifactSchema,
|
|
@@ -4270,20 +4336,21 @@ export {
|
|
|
4270
4336
|
createPassesClient,
|
|
4271
4337
|
createReceiptArtifactUri,
|
|
4272
4338
|
createReceiptsClient,
|
|
4273
|
-
createSoftwareEd25519Signer,
|
|
4274
4339
|
createSoftwareP256Signer,
|
|
4275
4340
|
decodeArtifactUri,
|
|
4276
4341
|
decodeAuthorizationQR,
|
|
4277
4342
|
decodeBase45,
|
|
4343
|
+
decodeOfflineClaimSmsMessage,
|
|
4278
4344
|
decodePaymentRequestQR,
|
|
4279
4345
|
encodeArtifactUri,
|
|
4280
4346
|
encodeAuthorizationQR,
|
|
4281
4347
|
encodeBase45,
|
|
4282
4348
|
encodeNQR,
|
|
4349
|
+
encodeOfflineClaimSmsMessage,
|
|
4283
4350
|
encodePaymentRequestQR,
|
|
4351
|
+
extractOfflineClaimSmsToken,
|
|
4284
4352
|
formatAmount,
|
|
4285
4353
|
generateDynamicQR,
|
|
4286
|
-
generateKeyPair,
|
|
4287
4354
|
generateStaticQR,
|
|
4288
4355
|
init,
|
|
4289
4356
|
isHardenedArtifactType,
|
|
@@ -4294,13 +4361,10 @@ export {
|
|
|
4294
4361
|
parseAmountInput,
|
|
4295
4362
|
parseNQR,
|
|
4296
4363
|
parseQR,
|
|
4297
|
-
publicKeyFromPrivate,
|
|
4298
4364
|
readTLV,
|
|
4299
4365
|
routingHint,
|
|
4300
|
-
sign,
|
|
4301
4366
|
signArtifact,
|
|
4302
4367
|
signAuthorization,
|
|
4303
|
-
signCanonical,
|
|
4304
4368
|
signOAC,
|
|
4305
4369
|
signPartnerRequest,
|
|
4306
4370
|
signPass,
|
|
@@ -4308,11 +4372,9 @@ export {
|
|
|
4308
4372
|
signReceipt,
|
|
4309
4373
|
signRedemption,
|
|
4310
4374
|
signRequestHMAC,
|
|
4311
|
-
verify,
|
|
4312
4375
|
verifyArtifactSignature,
|
|
4313
4376
|
verifyArtifactUri,
|
|
4314
4377
|
verifyAuthorization,
|
|
4315
|
-
verifyCanonical,
|
|
4316
4378
|
verifyClaimSignature,
|
|
4317
4379
|
verifyOAC,
|
|
4318
4380
|
verifyPass,
|