@nokinc-flur/sdk 1.1.2 → 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 +455 -147
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +659 -110
- package/dist/index.d.ts +659 -110
- package/dist/index.js +441 -143
- 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;
|
|
1587
|
+
}
|
|
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);
|
|
1575
1627
|
}
|
|
1576
|
-
function
|
|
1577
|
-
|
|
1628
|
+
function signIssuerP256(bytes, issuerPrivateKey) {
|
|
1629
|
+
const sig = p256.sign(bytes, issuerPrivateKey, { prehash: true });
|
|
1630
|
+
return bytesToBase64(sig.toBytes("der"));
|
|
1578
1631
|
}
|
|
1579
|
-
function
|
|
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,17 +2957,49 @@ 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);
|
|
2961
|
+
var Base64Std3 = z13.string().regex(/^[A-Za-z0-9+/]+={0,2}$/);
|
|
2923
2962
|
var RegisterDeviceKeyInputSchema = z13.object({
|
|
2924
2963
|
deviceId: z13.string().min(1).max(128),
|
|
2925
2964
|
publicKeyHex: Hex64
|
|
2926
2965
|
});
|
|
2966
|
+
var AttestationSecurityLevelSchema = z13.enum([
|
|
2967
|
+
"STRONGBOX",
|
|
2968
|
+
"TEE",
|
|
2969
|
+
"SECURE_ENCLAVE",
|
|
2970
|
+
"SOFTWARE"
|
|
2971
|
+
]);
|
|
2972
|
+
var DeviceKeyAlgSchema = z13.literal("p256");
|
|
2973
|
+
var RegisterDeviceKeyP256InputSchema = z13.object({
|
|
2974
|
+
deviceId: z13.string().min(1).max(128),
|
|
2975
|
+
/** P-256 SubjectPublicKeyInfo DER, base64. */
|
|
2976
|
+
publicKeySpkiB64: Base64Std3.min(64).max(4096),
|
|
2977
|
+
/** Base64 of the server-issued enrollment challenge string. */
|
|
2978
|
+
challengeB64: Base64Std3.min(8).max(1024),
|
|
2979
|
+
/** iOS App Attest payload or Android X.509 Key Attestation chain. */
|
|
2980
|
+
attestationChainB64: z13.array(Base64Std3.min(16).max(16384)).min(1).max(16),
|
|
2981
|
+
securityLevel: AttestationSecurityLevelSchema
|
|
2982
|
+
});
|
|
2983
|
+
var P256EnrollmentChallengeInputSchema = z13.object({
|
|
2984
|
+
deviceId: z13.string().min(1).max(128)
|
|
2985
|
+
});
|
|
2986
|
+
var P256EnrollmentChallengeResultSchema = z13.object({
|
|
2987
|
+
challenge: z13.string().min(16),
|
|
2988
|
+
expiresAtMs: z13.number().int().positive()
|
|
2989
|
+
});
|
|
2927
2990
|
var DeviceKeyRecordSchema = z13.object({
|
|
2928
2991
|
id: z13.string().uuid(),
|
|
2929
2992
|
userId: z13.string().uuid(),
|
|
2930
2993
|
deviceId: z13.string(),
|
|
2931
|
-
|
|
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). */
|
|
2997
|
+
publicKeyHex: Hex64.nullable().default(null),
|
|
2998
|
+
/** P-256 SubjectPublicKeyInfo DER, base64. Required for new records. */
|
|
2999
|
+
publicKeySpkiB64: Base64Std3.nullable().default(null),
|
|
3000
|
+
securityLevel: AttestationSecurityLevelSchema.nullable().default(null),
|
|
3001
|
+
hardwareBacked: z13.boolean().default(false),
|
|
3002
|
+
attestedAtMs: z13.number().int().nonnegative().nullable().default(null),
|
|
2932
3003
|
createdAtMs: z13.number().int().nonnegative(),
|
|
2933
3004
|
revokedAtMs: z13.number().int().nonnegative().nullable()
|
|
2934
3005
|
});
|
|
@@ -2937,7 +3008,10 @@ var ConsumerOACSchema = z13.object({
|
|
|
2937
3008
|
issuerId: z13.string().min(1).max(64),
|
|
2938
3009
|
userId: z13.string().uuid(),
|
|
2939
3010
|
deviceId: z13.string().min(1).max(128),
|
|
2940
|
-
|
|
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),
|
|
2941
3015
|
perTxCapKobo: z13.number().int().positive(),
|
|
2942
3016
|
cumulativeCapKobo: z13.number().int().positive(),
|
|
2943
3017
|
currency: z13.string().length(3),
|
|
@@ -2948,8 +3022,10 @@ var ConsumerOACSchema = z13.object({
|
|
|
2948
3022
|
});
|
|
2949
3023
|
var SignedConsumerOACSchema = z13.object({
|
|
2950
3024
|
oac: ConsumerOACSchema,
|
|
2951
|
-
|
|
2952
|
-
|
|
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)
|
|
2953
3029
|
});
|
|
2954
3030
|
var OACRecordSchema = SignedConsumerOACSchema.extend({
|
|
2955
3031
|
currentOfflineSpentKobo: z13.number().int().nonnegative(),
|
|
@@ -2981,6 +3057,7 @@ var EnableOfflineInputSchema = z13.object({
|
|
|
2981
3057
|
installId: z13.string().min(1).max(128),
|
|
2982
3058
|
partnerId: z13.string().min(1).max(64).optional()
|
|
2983
3059
|
});
|
|
3060
|
+
var ProvisionOfflineAllowanceInputSchema = EnableOfflineInputSchema;
|
|
2984
3061
|
var DisableOfflineInputSchema = z13.object({
|
|
2985
3062
|
deviceId: z13.string().min(1).max(128),
|
|
2986
3063
|
installId: z13.string().min(1).max(128).optional(),
|
|
@@ -3018,6 +3095,7 @@ var EnableOfflineResultSchema = z13.object({
|
|
|
3018
3095
|
hold: OfflineHoldRecordSchema,
|
|
3019
3096
|
oac: OACRecordSchema
|
|
3020
3097
|
});
|
|
3098
|
+
var ProvisionOfflineAllowanceResultSchema = EnableOfflineResultSchema;
|
|
3021
3099
|
var DisableOfflineResultSchema = z13.object({
|
|
3022
3100
|
hold: OfflineHoldRecordSchema,
|
|
3023
3101
|
trusted: z13.boolean(),
|
|
@@ -3031,6 +3109,8 @@ var OfflineStateResultSchema = z13.object({
|
|
|
3031
3109
|
active: OACRecordSchema.nullable()
|
|
3032
3110
|
});
|
|
3033
3111
|
var ConsumerPaymentClaimSchema = z13.object({
|
|
3112
|
+
/** Always 'p256'. Retained for forward-compat and as an explicit domain marker. */
|
|
3113
|
+
alg: z13.literal("p256").default("p256"),
|
|
3034
3114
|
oacId: z13.string().uuid(),
|
|
3035
3115
|
encounterId: Sha256Hex.optional(),
|
|
3036
3116
|
payerUserId: z13.string().uuid(),
|
|
@@ -3043,10 +3123,10 @@ var ConsumerPaymentClaimSchema = z13.object({
|
|
|
3043
3123
|
occurredAtMs: z13.number().int().nonnegative(),
|
|
3044
3124
|
completedAtMs: z13.number().int().nonnegative().optional(),
|
|
3045
3125
|
contextId: z13.string().max(128).optional(),
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
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()
|
|
3050
3130
|
});
|
|
3051
3131
|
var ConsumerSettlementSchema = z13.object({
|
|
3052
3132
|
settlementId: z13.string().uuid(),
|
|
@@ -3060,7 +3140,8 @@ var ConsumerSettlementSchema = z13.object({
|
|
|
3060
3140
|
status: z13.enum(["SETTLED", "REVIEW"]),
|
|
3061
3141
|
reviewReason: z13.string().nullable(),
|
|
3062
3142
|
ledgerRef: z13.string().nullable(),
|
|
3063
|
-
|
|
3143
|
+
/** ASN.1 DER ECDSA P-256 issuer signature, base64. */
|
|
3144
|
+
issuerSig: Base64Std3.min(16).max(2048),
|
|
3064
3145
|
createdAtMs: z13.number().int().nonnegative()
|
|
3065
3146
|
});
|
|
3066
3147
|
var ConsumerSettleResultSchema = z13.object({
|
|
@@ -3112,6 +3193,18 @@ function createMeOfflineClient(opts) {
|
|
|
3112
3193
|
RegisterDeviceKeyInputSchema.parse(input),
|
|
3113
3194
|
(raw) => DeviceKeyRecordSchema.parse(raw)
|
|
3114
3195
|
),
|
|
3196
|
+
issueP256EnrollmentChallenge: (input) => call(
|
|
3197
|
+
"POST",
|
|
3198
|
+
"/v1/me/offline/keys/p256/challenge",
|
|
3199
|
+
P256EnrollmentChallengeInputSchema.parse(input),
|
|
3200
|
+
(raw) => P256EnrollmentChallengeResultSchema.parse(raw)
|
|
3201
|
+
),
|
|
3202
|
+
registerDeviceKeyP256: (input) => call(
|
|
3203
|
+
"POST",
|
|
3204
|
+
"/v1/me/offline/keys/p256",
|
|
3205
|
+
RegisterDeviceKeyP256InputSchema.parse(input),
|
|
3206
|
+
(raw) => DeviceKeyRecordSchema.parse(raw)
|
|
3207
|
+
),
|
|
3115
3208
|
listDeviceKeys: () => call(
|
|
3116
3209
|
"GET",
|
|
3117
3210
|
"/v1/me/offline/keys",
|
|
@@ -3124,6 +3217,12 @@ function createMeOfflineClient(opts) {
|
|
|
3124
3217
|
RevokeDeviceKeyInputSchema.parse(input),
|
|
3125
3218
|
() => void 0
|
|
3126
3219
|
),
|
|
3220
|
+
provisionAllowance: (input) => call(
|
|
3221
|
+
"POST",
|
|
3222
|
+
"/v1/me/offline/allowance",
|
|
3223
|
+
ProvisionOfflineAllowanceInputSchema.parse(input),
|
|
3224
|
+
(raw) => ProvisionOfflineAllowanceResultSchema.parse(raw)
|
|
3225
|
+
),
|
|
3127
3226
|
enable: (input) => call(
|
|
3128
3227
|
"POST",
|
|
3129
3228
|
"/v1/me/offline/enable",
|
|
@@ -3169,6 +3268,185 @@ function createMeOfflineClient(opts) {
|
|
|
3169
3268
|
};
|
|
3170
3269
|
}
|
|
3171
3270
|
|
|
3271
|
+
// src/me-offline/signer.ts
|
|
3272
|
+
import { p256 as p2562 } from "@noble/curves/nist";
|
|
3273
|
+
var CLAIM_DOMAIN_V2 = "flur:consumer-offline:v2:claim";
|
|
3274
|
+
function canonicalClaimSigningPayload(claim) {
|
|
3275
|
+
return {
|
|
3276
|
+
domain: CLAIM_DOMAIN_V2,
|
|
3277
|
+
alg: claim.alg,
|
|
3278
|
+
oacId: claim.oacId,
|
|
3279
|
+
payerUserId: claim.payerUserId,
|
|
3280
|
+
payeeUserId: claim.payeeUserId,
|
|
3281
|
+
payerDeviceId: claim.payerDeviceId,
|
|
3282
|
+
payerNonce: claim.payerNonce,
|
|
3283
|
+
payeeNonce: claim.payeeNonce,
|
|
3284
|
+
amountKobo: claim.amountKobo,
|
|
3285
|
+
currency: claim.currency,
|
|
3286
|
+
occurredAtMs: claim.occurredAtMs,
|
|
3287
|
+
completedAtMs: claim.completedAtMs ?? null,
|
|
3288
|
+
contextId: claim.contextId ?? null
|
|
3289
|
+
};
|
|
3290
|
+
}
|
|
3291
|
+
function canonicalClaimSigningBytes(claim) {
|
|
3292
|
+
return canonicalJSONBytes(canonicalClaimSigningPayload(claim));
|
|
3293
|
+
}
|
|
3294
|
+
function bytesToBase642(bytes) {
|
|
3295
|
+
if (typeof Buffer !== "undefined") {
|
|
3296
|
+
return Buffer.from(bytes).toString("base64");
|
|
3297
|
+
}
|
|
3298
|
+
let bin = "";
|
|
3299
|
+
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
3300
|
+
return btoa(bin);
|
|
3301
|
+
}
|
|
3302
|
+
function base64ToBytes2(b64) {
|
|
3303
|
+
if (typeof Buffer !== "undefined") {
|
|
3304
|
+
return new Uint8Array(Buffer.from(b64, "base64"));
|
|
3305
|
+
}
|
|
3306
|
+
const bin = atob(b64);
|
|
3307
|
+
const out = new Uint8Array(bin.length);
|
|
3308
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
3309
|
+
return out;
|
|
3310
|
+
}
|
|
3311
|
+
var P256_SPKI_HEADER2 = new Uint8Array([
|
|
3312
|
+
48,
|
|
3313
|
+
89,
|
|
3314
|
+
48,
|
|
3315
|
+
19,
|
|
3316
|
+
6,
|
|
3317
|
+
7,
|
|
3318
|
+
42,
|
|
3319
|
+
134,
|
|
3320
|
+
72,
|
|
3321
|
+
206,
|
|
3322
|
+
61,
|
|
3323
|
+
2,
|
|
3324
|
+
1,
|
|
3325
|
+
6,
|
|
3326
|
+
8,
|
|
3327
|
+
42,
|
|
3328
|
+
134,
|
|
3329
|
+
72,
|
|
3330
|
+
206,
|
|
3331
|
+
61,
|
|
3332
|
+
3,
|
|
3333
|
+
1,
|
|
3334
|
+
7,
|
|
3335
|
+
3,
|
|
3336
|
+
66,
|
|
3337
|
+
0
|
|
3338
|
+
]);
|
|
3339
|
+
function p256PublicKeyToSpkiB64(rawUncompressed) {
|
|
3340
|
+
if (rawUncompressed.length !== 65 || rawUncompressed[0] !== 4) {
|
|
3341
|
+
throw new Error("p256: expected 65-byte uncompressed point");
|
|
3342
|
+
}
|
|
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);
|
|
3347
|
+
}
|
|
3348
|
+
function p256SpkiB64ToPublicKey(spkiB64) {
|
|
3349
|
+
const spki = base64ToBytes2(spkiB64);
|
|
3350
|
+
if (spki.length !== P256_SPKI_HEADER2.length + 65) {
|
|
3351
|
+
throw new Error("p256: invalid SPKI length");
|
|
3352
|
+
}
|
|
3353
|
+
for (let i = 0; i < P256_SPKI_HEADER2.length; i++) {
|
|
3354
|
+
if (spki[i] !== P256_SPKI_HEADER2[i]) {
|
|
3355
|
+
throw new Error("p256: invalid SPKI header");
|
|
3356
|
+
}
|
|
3357
|
+
}
|
|
3358
|
+
return spki.slice(P256_SPKI_HEADER2.length);
|
|
3359
|
+
}
|
|
3360
|
+
function createSoftwareP256Signer(privateKey) {
|
|
3361
|
+
const raw = p2562.getPublicKey(privateKey, false);
|
|
3362
|
+
const spkiB64 = p256PublicKeyToSpkiB64(raw);
|
|
3363
|
+
return {
|
|
3364
|
+
alg: "p256",
|
|
3365
|
+
async getPublicKey() {
|
|
3366
|
+
return { alg: "p256", publicKey: spkiB64 };
|
|
3367
|
+
},
|
|
3368
|
+
async sign(bytes) {
|
|
3369
|
+
const sig = p2562.sign(bytes, privateKey, { prehash: true });
|
|
3370
|
+
const der = sig.toBytes("der");
|
|
3371
|
+
return { alg: "p256", signature: bytesToBase642(der) };
|
|
3372
|
+
}
|
|
3373
|
+
};
|
|
3374
|
+
}
|
|
3375
|
+
function verifyClaimSignature(input) {
|
|
3376
|
+
try {
|
|
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
|
+
});
|
|
3384
|
+
} catch {
|
|
3385
|
+
return false;
|
|
3386
|
+
}
|
|
3387
|
+
}
|
|
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
|
+
|
|
3172
3450
|
// src/partner-funding/client.ts
|
|
3173
3451
|
import { z as z14 } from "zod";
|
|
3174
3452
|
var MinorString = z14.string().regex(/^-?\d+$/);
|
|
@@ -3544,14 +3822,15 @@ function buildArtifactBody(input) {
|
|
|
3544
3822
|
return { ...header, data: input.data };
|
|
3545
3823
|
}
|
|
3546
3824
|
function signArtifact(body, privateKey) {
|
|
3547
|
-
const sig =
|
|
3825
|
+
const sig = signIssuerP256(canonicalJSONBytes(body), privateKey);
|
|
3548
3826
|
return { body, sig };
|
|
3549
3827
|
}
|
|
3550
3828
|
function encodeArtifactUri(signed) {
|
|
3551
3829
|
const bodyBytes = canonicalJSONBytes(signed.body);
|
|
3552
3830
|
const bodyB64 = base64UrlEncode(bodyBytes);
|
|
3553
|
-
const
|
|
3554
|
-
|
|
3831
|
+
const sigBytes = base64UrlDecode(signed.sig);
|
|
3832
|
+
const sigB64Url = base64UrlEncode(sigBytes);
|
|
3833
|
+
return `${FLUR_ARTIFACT_URI_PREFIX}${signed.body.t}/${bodyB64}.${sigB64Url}`;
|
|
3555
3834
|
}
|
|
3556
3835
|
function decodeArtifactUri(uri) {
|
|
3557
3836
|
if (!uri.startsWith(FLUR_ARTIFACT_URI_PREFIX)) {
|
|
@@ -3582,9 +3861,9 @@ function decodeArtifactUri(uri) {
|
|
|
3582
3861
|
}
|
|
3583
3862
|
const bodyBytes = base64UrlDecode(payload.slice(0, dot));
|
|
3584
3863
|
const sigBytes = base64UrlDecode(payload.slice(dot + 1));
|
|
3585
|
-
if (sigBytes.length
|
|
3864
|
+
if (sigBytes.length < 64 || sigBytes.length > 80) {
|
|
3586
3865
|
throw new FlurArtifactError(
|
|
3587
|
-
`Signature
|
|
3866
|
+
`Signature length out of range: ${sigBytes.length}`,
|
|
3588
3867
|
"INVALID_SIGNATURE"
|
|
3589
3868
|
);
|
|
3590
3869
|
}
|
|
@@ -3611,17 +3890,26 @@ function decodeArtifactUri(uri) {
|
|
|
3611
3890
|
type,
|
|
3612
3891
|
bodyBytes,
|
|
3613
3892
|
body: bodyJson,
|
|
3614
|
-
|
|
3893
|
+
// Encode as standard base64 (not url-safe) for sig field consistency.
|
|
3894
|
+
sig: encodeStdBase64(sigBytes)
|
|
3615
3895
|
};
|
|
3616
3896
|
}
|
|
3617
|
-
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 = {}) {
|
|
3618
3906
|
if (options.enforceExpiry !== false && decoded.body.exp !== void 0) {
|
|
3619
3907
|
const now = options.nowSeconds ?? Math.floor(Date.now() / 1e3);
|
|
3620
3908
|
if (decoded.body.exp < now) {
|
|
3621
3909
|
throw new FlurArtifactError("Artifact has expired", "EXPIRED");
|
|
3622
3910
|
}
|
|
3623
3911
|
}
|
|
3624
|
-
return
|
|
3912
|
+
return verifyIssuerP256(decoded.bodyBytes, decoded.sig, publicKeySpkiB64);
|
|
3625
3913
|
}
|
|
3626
3914
|
|
|
3627
3915
|
// src/artifacts/types.ts
|
|
@@ -3640,7 +3928,7 @@ var ARTIFACT_TYPES = {
|
|
|
3640
3928
|
PASS: "pass",
|
|
3641
3929
|
IDENTITY: "identity"
|
|
3642
3930
|
};
|
|
3643
|
-
var
|
|
3931
|
+
var HexString = (length) => z16.string().regex(
|
|
3644
3932
|
new RegExp(`^[0-9a-fA-F]{${length * 2}}$`),
|
|
3645
3933
|
`expected ${length}-byte hex string`
|
|
3646
3934
|
);
|
|
@@ -3658,7 +3946,7 @@ var ReceiptArtifactSchema = z16.object({
|
|
|
3658
3946
|
settledAtMs: z16.number().int().positive(),
|
|
3659
3947
|
ledgerTxnId: z16.string().min(1).max(64).optional(),
|
|
3660
3948
|
memo: z16.string().max(140).optional(),
|
|
3661
|
-
hashChainPrev:
|
|
3949
|
+
hashChainPrev: HexString(32).optional()
|
|
3662
3950
|
});
|
|
3663
3951
|
var ShortId = z16.string().min(1).max(64);
|
|
3664
3952
|
var PositiveInt = z16.number().int().positive();
|
|
@@ -3740,7 +4028,7 @@ var StatementArtifactSchema = z16.object({
|
|
|
3740
4028
|
closingBalanceKobo: z16.number().int(),
|
|
3741
4029
|
transactionCount: NonNegativeInt,
|
|
3742
4030
|
currency: Currency2,
|
|
3743
|
-
hashChainPrev:
|
|
4031
|
+
hashChainPrev: HexString(32).optional()
|
|
3744
4032
|
}).refine((v) => v.periodEndMs > v.periodStartMs, {
|
|
3745
4033
|
message: "periodEndMs must be greater than periodStartMs",
|
|
3746
4034
|
path: ["periodEndMs"]
|
|
@@ -3773,7 +4061,7 @@ var IdentityArtifactSchema = z16.object({
|
|
|
3773
4061
|
"kyc_tier",
|
|
3774
4062
|
"age_band"
|
|
3775
4063
|
]),
|
|
3776
|
-
claimValueHash:
|
|
4064
|
+
claimValueHash: HexString(32),
|
|
3777
4065
|
attestedAtMs: PositiveInt
|
|
3778
4066
|
});
|
|
3779
4067
|
var ARTIFACT_BODY_SCHEMAS = {
|
|
@@ -3837,7 +4125,7 @@ function createArtifactUri(input) {
|
|
|
3837
4125
|
const signed = signArtifact(body, input.privateKey);
|
|
3838
4126
|
return { uri: encodeArtifactUri(signed), signed };
|
|
3839
4127
|
}
|
|
3840
|
-
function verifyArtifactUri(uri,
|
|
4128
|
+
function verifyArtifactUri(uri, publicKeySpkiB64, options = {}) {
|
|
3841
4129
|
const decoded = decodeArtifactUri(uri);
|
|
3842
4130
|
if (!isKnownArtifactType(decoded.type)) {
|
|
3843
4131
|
throw new FlurArtifactError(
|
|
@@ -3859,7 +4147,7 @@ function verifyArtifactUri(uri, publicKey, options = {}) {
|
|
|
3859
4147
|
"INVALID_BODY"
|
|
3860
4148
|
);
|
|
3861
4149
|
}
|
|
3862
|
-
const ok = verifyArtifactSignature(decoded,
|
|
4150
|
+
const ok = verifyArtifactSignature(decoded, publicKeySpkiB64, options);
|
|
3863
4151
|
if (!ok) {
|
|
3864
4152
|
throw new FlurArtifactError(
|
|
3865
4153
|
"Artifact signature verification failed",
|
|
@@ -3895,6 +4183,8 @@ export {
|
|
|
3895
4183
|
AccountSchema,
|
|
3896
4184
|
ApiCredentialPublicSchema,
|
|
3897
4185
|
ArtifactHeaderSchema,
|
|
4186
|
+
AttestationSecurityLevelSchema,
|
|
4187
|
+
CLAIM_DOMAIN_V2,
|
|
3898
4188
|
COLLECTION_INTENT_STATUSES,
|
|
3899
4189
|
COLLECTION_PAYMENT_STATUSES,
|
|
3900
4190
|
CUSTODIAL_MODES,
|
|
@@ -3913,6 +4203,7 @@ export {
|
|
|
3913
4203
|
CreatePayoutInputSchema,
|
|
3914
4204
|
CreateWithdrawalInputSchema,
|
|
3915
4205
|
CreateWithdrawalResultSchema,
|
|
4206
|
+
DeviceKeyAlgSchema,
|
|
3916
4207
|
DeviceKeyRecordSchema,
|
|
3917
4208
|
DisableOfflineInputSchema,
|
|
3918
4209
|
DisableOfflineResultSchema,
|
|
@@ -3949,6 +4240,7 @@ export {
|
|
|
3949
4240
|
OAC_DEFAULT_CUMULATIVE_KOBO,
|
|
3950
4241
|
OAC_DEFAULT_PER_TX_KOBO,
|
|
3951
4242
|
OAC_DEFAULT_VALIDITY_MS,
|
|
4243
|
+
OFFLINE_CLAIM_SMS_PREFIX,
|
|
3952
4244
|
OfflineClaimArtifactSchema,
|
|
3953
4245
|
OfflineHoldRecordSchema,
|
|
3954
4246
|
OfflinePaymentAuthorizationArtifactSchema,
|
|
@@ -3957,6 +4249,8 @@ export {
|
|
|
3957
4249
|
OfflineStateResultSchema,
|
|
3958
4250
|
OfflineStatusResultSchema,
|
|
3959
4251
|
OfflineTokenSchema,
|
|
4252
|
+
P256EnrollmentChallengeInputSchema,
|
|
4253
|
+
P256EnrollmentChallengeResultSchema,
|
|
3960
4254
|
PARTNER_FUNDING_DIRECTIONS,
|
|
3961
4255
|
PARTNER_FUNDING_STATUSES,
|
|
3962
4256
|
PARTNER_KINDS,
|
|
@@ -3980,6 +4274,8 @@ export {
|
|
|
3980
4274
|
PayoutEventInputSchema,
|
|
3981
4275
|
ProviderEventInputSchema,
|
|
3982
4276
|
ProviderEventRecordSchema,
|
|
4277
|
+
ProvisionOfflineAllowanceInputSchema,
|
|
4278
|
+
ProvisionOfflineAllowanceResultSchema,
|
|
3983
4279
|
PublicCollectionIntentSchema,
|
|
3984
4280
|
RECEIPT_CHANNELS,
|
|
3985
4281
|
RECEIPT_KINDS,
|
|
@@ -3991,6 +4287,7 @@ export {
|
|
|
3991
4287
|
RecordPayoutEventResultSchema,
|
|
3992
4288
|
RedemptionSchema,
|
|
3993
4289
|
RegisterDeviceKeyInputSchema,
|
|
4290
|
+
RegisterDeviceKeyP256InputSchema,
|
|
3994
4291
|
ReversalRecordArtifactSchema,
|
|
3995
4292
|
RevokeDeviceKeyInputSchema,
|
|
3996
4293
|
SETTLEMENT_SCHEDULES,
|
|
@@ -4013,6 +4310,8 @@ export {
|
|
|
4013
4310
|
buildPaymentRequest,
|
|
4014
4311
|
buildReceipt,
|
|
4015
4312
|
buildRedemption,
|
|
4313
|
+
canonicalClaimSigningBytes,
|
|
4314
|
+
canonicalClaimSigningPayload,
|
|
4016
4315
|
canonicalJSONBytes,
|
|
4017
4316
|
canonicalJSONStringify,
|
|
4018
4317
|
canonicalRequestString,
|
|
@@ -4037,18 +4336,21 @@ export {
|
|
|
4037
4336
|
createPassesClient,
|
|
4038
4337
|
createReceiptArtifactUri,
|
|
4039
4338
|
createReceiptsClient,
|
|
4339
|
+
createSoftwareP256Signer,
|
|
4040
4340
|
decodeArtifactUri,
|
|
4041
4341
|
decodeAuthorizationQR,
|
|
4042
4342
|
decodeBase45,
|
|
4343
|
+
decodeOfflineClaimSmsMessage,
|
|
4043
4344
|
decodePaymentRequestQR,
|
|
4044
4345
|
encodeArtifactUri,
|
|
4045
4346
|
encodeAuthorizationQR,
|
|
4046
4347
|
encodeBase45,
|
|
4047
4348
|
encodeNQR,
|
|
4349
|
+
encodeOfflineClaimSmsMessage,
|
|
4048
4350
|
encodePaymentRequestQR,
|
|
4351
|
+
extractOfflineClaimSmsToken,
|
|
4049
4352
|
formatAmount,
|
|
4050
4353
|
generateDynamicQR,
|
|
4051
|
-
generateKeyPair,
|
|
4052
4354
|
generateStaticQR,
|
|
4053
4355
|
init,
|
|
4054
4356
|
isHardenedArtifactType,
|
|
@@ -4059,13 +4361,10 @@ export {
|
|
|
4059
4361
|
parseAmountInput,
|
|
4060
4362
|
parseNQR,
|
|
4061
4363
|
parseQR,
|
|
4062
|
-
publicKeyFromPrivate,
|
|
4063
4364
|
readTLV,
|
|
4064
4365
|
routingHint,
|
|
4065
|
-
sign,
|
|
4066
4366
|
signArtifact,
|
|
4067
4367
|
signAuthorization,
|
|
4068
|
-
signCanonical,
|
|
4069
4368
|
signOAC,
|
|
4070
4369
|
signPartnerRequest,
|
|
4071
4370
|
signPass,
|
|
@@ -4073,11 +4372,10 @@ export {
|
|
|
4073
4372
|
signReceipt,
|
|
4074
4373
|
signRedemption,
|
|
4075
4374
|
signRequestHMAC,
|
|
4076
|
-
verify,
|
|
4077
4375
|
verifyArtifactSignature,
|
|
4078
4376
|
verifyArtifactUri,
|
|
4079
4377
|
verifyAuthorization,
|
|
4080
|
-
|
|
4378
|
+
verifyClaimSignature,
|
|
4081
4379
|
verifyOAC,
|
|
4082
4380
|
verifyPass,
|
|
4083
4381
|
verifyPaymentRequest,
|