@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.cjs
CHANGED
|
@@ -86,6 +86,7 @@ __export(index_exports, {
|
|
|
86
86
|
OAC_DEFAULT_CUMULATIVE_KOBO: () => OAC_DEFAULT_CUMULATIVE_KOBO,
|
|
87
87
|
OAC_DEFAULT_PER_TX_KOBO: () => OAC_DEFAULT_PER_TX_KOBO,
|
|
88
88
|
OAC_DEFAULT_VALIDITY_MS: () => OAC_DEFAULT_VALIDITY_MS,
|
|
89
|
+
OFFLINE_CLAIM_SMS_PREFIX: () => OFFLINE_CLAIM_SMS_PREFIX,
|
|
89
90
|
OfflineClaimArtifactSchema: () => OfflineClaimArtifactSchema,
|
|
90
91
|
OfflineHoldRecordSchema: () => OfflineHoldRecordSchema,
|
|
91
92
|
OfflinePaymentAuthorizationArtifactSchema: () => OfflinePaymentAuthorizationArtifactSchema,
|
|
@@ -181,20 +182,21 @@ __export(index_exports, {
|
|
|
181
182
|
createPassesClient: () => createPassesClient,
|
|
182
183
|
createReceiptArtifactUri: () => createReceiptArtifactUri,
|
|
183
184
|
createReceiptsClient: () => createReceiptsClient,
|
|
184
|
-
createSoftwareEd25519Signer: () => createSoftwareEd25519Signer,
|
|
185
185
|
createSoftwareP256Signer: () => createSoftwareP256Signer,
|
|
186
186
|
decodeArtifactUri: () => decodeArtifactUri,
|
|
187
187
|
decodeAuthorizationQR: () => decodeAuthorizationQR,
|
|
188
188
|
decodeBase45: () => decodeBase45,
|
|
189
|
+
decodeOfflineClaimSmsMessage: () => decodeOfflineClaimSmsMessage,
|
|
189
190
|
decodePaymentRequestQR: () => decodePaymentRequestQR,
|
|
190
191
|
encodeArtifactUri: () => encodeArtifactUri,
|
|
191
192
|
encodeAuthorizationQR: () => encodeAuthorizationQR,
|
|
192
193
|
encodeBase45: () => encodeBase45,
|
|
193
194
|
encodeNQR: () => encodeNQR,
|
|
195
|
+
encodeOfflineClaimSmsMessage: () => encodeOfflineClaimSmsMessage,
|
|
194
196
|
encodePaymentRequestQR: () => encodePaymentRequestQR,
|
|
197
|
+
extractOfflineClaimSmsToken: () => extractOfflineClaimSmsToken,
|
|
195
198
|
formatAmount: () => formatAmount,
|
|
196
199
|
generateDynamicQR: () => generateDynamicQR,
|
|
197
|
-
generateKeyPair: () => generateKeyPair,
|
|
198
200
|
generateStaticQR: () => generateStaticQR,
|
|
199
201
|
init: () => init,
|
|
200
202
|
isHardenedArtifactType: () => isHardenedArtifactType,
|
|
@@ -205,13 +207,10 @@ __export(index_exports, {
|
|
|
205
207
|
parseAmountInput: () => parseAmountInput,
|
|
206
208
|
parseNQR: () => parseNQR,
|
|
207
209
|
parseQR: () => parseQR,
|
|
208
|
-
publicKeyFromPrivate: () => publicKeyFromPrivate,
|
|
209
210
|
readTLV: () => readTLV,
|
|
210
211
|
routingHint: () => routingHint,
|
|
211
|
-
sign: () => sign,
|
|
212
212
|
signArtifact: () => signArtifact,
|
|
213
213
|
signAuthorization: () => signAuthorization,
|
|
214
|
-
signCanonical: () => signCanonical,
|
|
215
214
|
signOAC: () => signOAC,
|
|
216
215
|
signPartnerRequest: () => signPartnerRequest,
|
|
217
216
|
signPass: () => signPass,
|
|
@@ -219,11 +218,9 @@ __export(index_exports, {
|
|
|
219
218
|
signReceipt: () => signReceipt,
|
|
220
219
|
signRedemption: () => signRedemption,
|
|
221
220
|
signRequestHMAC: () => signRequestHMAC,
|
|
222
|
-
verify: () => verify,
|
|
223
221
|
verifyArtifactSignature: () => verifyArtifactSignature,
|
|
224
222
|
verifyArtifactUri: () => verifyArtifactUri,
|
|
225
223
|
verifyAuthorization: () => verifyAuthorization,
|
|
226
|
-
verifyCanonical: () => verifyCanonical,
|
|
227
224
|
verifyClaimSignature: () => verifyClaimSignature,
|
|
228
225
|
verifyOAC: () => verifyOAC,
|
|
229
226
|
verifyPass: () => verifyPass,
|
|
@@ -1800,64 +1797,113 @@ function constantTimeEqual(a, b) {
|
|
|
1800
1797
|
return diff === 0;
|
|
1801
1798
|
}
|
|
1802
1799
|
|
|
1803
|
-
// src/
|
|
1804
|
-
var
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1800
|
+
// src/offline/oac.ts
|
|
1801
|
+
var import_zod5 = require("zod");
|
|
1802
|
+
|
|
1803
|
+
// src/crypto/p256-issuer.ts
|
|
1804
|
+
var import_nist = require("@noble/curves/nist");
|
|
1805
|
+
function bytesToBase64(bytes) {
|
|
1806
|
+
if (typeof Buffer !== "undefined") {
|
|
1807
|
+
return Buffer.from(bytes).toString("base64");
|
|
1808
|
+
}
|
|
1809
|
+
let bin = "";
|
|
1810
|
+
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
1811
|
+
return btoa(bin);
|
|
1809
1812
|
}
|
|
1810
|
-
function
|
|
1811
|
-
|
|
1813
|
+
function base64ToBytes(b64) {
|
|
1814
|
+
if (typeof Buffer !== "undefined") {
|
|
1815
|
+
return new Uint8Array(Buffer.from(b64, "base64"));
|
|
1816
|
+
}
|
|
1817
|
+
const bin = atob(b64);
|
|
1818
|
+
const out = new Uint8Array(bin.length);
|
|
1819
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
1820
|
+
return out;
|
|
1812
1821
|
}
|
|
1813
|
-
|
|
1814
|
-
|
|
1822
|
+
var P256_SPKI_HEADER = new Uint8Array([
|
|
1823
|
+
48,
|
|
1824
|
+
89,
|
|
1825
|
+
48,
|
|
1826
|
+
19,
|
|
1827
|
+
6,
|
|
1828
|
+
7,
|
|
1829
|
+
42,
|
|
1830
|
+
134,
|
|
1831
|
+
72,
|
|
1832
|
+
206,
|
|
1833
|
+
61,
|
|
1834
|
+
2,
|
|
1835
|
+
1,
|
|
1836
|
+
6,
|
|
1837
|
+
8,
|
|
1838
|
+
42,
|
|
1839
|
+
134,
|
|
1840
|
+
72,
|
|
1841
|
+
206,
|
|
1842
|
+
61,
|
|
1843
|
+
3,
|
|
1844
|
+
1,
|
|
1845
|
+
7,
|
|
1846
|
+
3,
|
|
1847
|
+
66,
|
|
1848
|
+
0
|
|
1849
|
+
]);
|
|
1850
|
+
function p256SpkiB64ToRaw(spkiB64) {
|
|
1851
|
+
const spki = base64ToBytes(spkiB64);
|
|
1852
|
+
if (spki.length !== P256_SPKI_HEADER.length + 65) {
|
|
1853
|
+
throw new Error("p256: invalid SPKI length");
|
|
1854
|
+
}
|
|
1855
|
+
for (let i = 0; i < P256_SPKI_HEADER.length; i++) {
|
|
1856
|
+
if (spki[i] !== P256_SPKI_HEADER[i]) {
|
|
1857
|
+
throw new Error("p256: invalid SPKI header");
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
return spki.slice(P256_SPKI_HEADER.length);
|
|
1815
1861
|
}
|
|
1816
|
-
function
|
|
1862
|
+
function signIssuerP256(bytes, issuerPrivateKey) {
|
|
1863
|
+
const sig = import_nist.p256.sign(bytes, issuerPrivateKey, { prehash: true });
|
|
1864
|
+
return bytesToBase64(sig.toBytes("der"));
|
|
1865
|
+
}
|
|
1866
|
+
function verifyIssuerP256(bytes, signatureB64, issuerPublicKeySpkiB64) {
|
|
1817
1867
|
try {
|
|
1818
|
-
|
|
1868
|
+
const pubRaw = p256SpkiB64ToRaw(issuerPublicKeySpkiB64);
|
|
1869
|
+
const sigBytes = base64ToBytes(signatureB64);
|
|
1870
|
+
return import_nist.p256.verify(sigBytes, bytes, pubRaw, {
|
|
1871
|
+
prehash: true,
|
|
1872
|
+
format: "der"
|
|
1873
|
+
});
|
|
1819
1874
|
} catch {
|
|
1820
1875
|
return false;
|
|
1821
1876
|
}
|
|
1822
1877
|
}
|
|
1823
|
-
function signCanonical(value, privateKey) {
|
|
1824
|
-
return sign(canonicalJSONBytes(value), privateKey);
|
|
1825
|
-
}
|
|
1826
|
-
function verifyCanonical(value, signature, publicKey) {
|
|
1827
|
-
return verify(canonicalJSONBytes(value), signature, publicKey);
|
|
1828
|
-
}
|
|
1829
1878
|
|
|
1830
1879
|
// src/offline/oac.ts
|
|
1831
|
-
var import_zod5 = require("zod");
|
|
1832
1880
|
var OAC_DEFAULT_PER_TX_KOBO = 5e5;
|
|
1833
1881
|
var OAC_DEFAULT_CUMULATIVE_KOBO = 2e6;
|
|
1834
1882
|
var OAC_DEFAULT_VALIDITY_MS = 24 * 60 * 60 * 1e3;
|
|
1835
|
-
var
|
|
1836
|
-
new RegExp(`^[0-9a-fA-F]{${length * 2}}$`),
|
|
1837
|
-
`expected ${length}-byte hex string`
|
|
1838
|
-
);
|
|
1883
|
+
var Base64Std = import_zod5.z.string().min(16).max(4096).regex(/^[A-Za-z0-9+/]+={0,2}$/, "expected base64 (standard) string");
|
|
1839
1884
|
var OACSchema = import_zod5.z.object({
|
|
1840
1885
|
userId: import_zod5.z.string().min(1),
|
|
1841
1886
|
deviceId: import_zod5.z.string().min(1),
|
|
1842
|
-
|
|
1887
|
+
/** SubjectPublicKeyInfo DER, base64 (P-256). */
|
|
1888
|
+
devicePublicKey: Base64Std,
|
|
1843
1889
|
perTxCapKobo: import_zod5.z.number().int().nonnegative(),
|
|
1844
1890
|
cumulativeCapKobo: import_zod5.z.number().int().nonnegative(),
|
|
1845
1891
|
validFromMs: import_zod5.z.number().int().nonnegative(),
|
|
1846
1892
|
validUntilMs: import_zod5.z.number().int().positive(),
|
|
1847
1893
|
counterSeed: import_zod5.z.number().int().nonnegative(),
|
|
1848
1894
|
nonce: import_zod5.z.string().min(1),
|
|
1849
|
-
|
|
1895
|
+
/** ASN.1 DER ECDSA(SHA-256) signature, base64. */
|
|
1896
|
+
issuerSig: Base64Std
|
|
1850
1897
|
}).refine((v) => v.validUntilMs > v.validFromMs, {
|
|
1851
1898
|
message: "validUntilMs must be greater than validFromMs"
|
|
1852
1899
|
}).refine((v) => v.perTxCapKobo <= v.cumulativeCapKobo, {
|
|
1853
1900
|
message: "perTxCapKobo must not exceed cumulativeCapKobo"
|
|
1854
1901
|
});
|
|
1855
1902
|
function buildOAC(input) {
|
|
1856
|
-
const devicePublicKey = typeof input.devicePublicKey === "string" ? input.devicePublicKey : bytesToHex(input.devicePublicKey);
|
|
1857
1903
|
return {
|
|
1858
1904
|
userId: input.userId,
|
|
1859
1905
|
deviceId: input.deviceId,
|
|
1860
|
-
devicePublicKey,
|
|
1906
|
+
devicePublicKey: input.devicePublicKey,
|
|
1861
1907
|
perTxCapKobo: input.perTxCapKobo ?? OAC_DEFAULT_PER_TX_KOBO,
|
|
1862
1908
|
cumulativeCapKobo: input.cumulativeCapKobo ?? OAC_DEFAULT_CUMULATIVE_KOBO,
|
|
1863
1909
|
validFromMs: input.validFromMs,
|
|
@@ -1867,36 +1913,25 @@ function buildOAC(input) {
|
|
|
1867
1913
|
};
|
|
1868
1914
|
}
|
|
1869
1915
|
function signOAC(unsigned, issuerPrivateKey) {
|
|
1870
|
-
const issuerSig =
|
|
1871
|
-
|
|
1916
|
+
const issuerSig = signIssuerP256(
|
|
1917
|
+
canonicalJSONBytes(unsigned),
|
|
1918
|
+
issuerPrivateKey
|
|
1872
1919
|
);
|
|
1873
1920
|
return { ...unsigned, issuerSig };
|
|
1874
1921
|
}
|
|
1875
|
-
function verifyOAC(oac,
|
|
1922
|
+
function verifyOAC(oac, issuerPublicKeySpkiB64) {
|
|
1876
1923
|
try {
|
|
1877
1924
|
const parsed = OACSchema.parse(oac);
|
|
1878
1925
|
const { issuerSig, ...unsigned } = parsed;
|
|
1879
|
-
return
|
|
1926
|
+
return verifyIssuerP256(
|
|
1880
1927
|
canonicalJSONBytes(unsigned),
|
|
1881
|
-
|
|
1882
|
-
|
|
1928
|
+
issuerSig,
|
|
1929
|
+
issuerPublicKeySpkiB64
|
|
1883
1930
|
);
|
|
1884
1931
|
} catch {
|
|
1885
1932
|
return false;
|
|
1886
1933
|
}
|
|
1887
1934
|
}
|
|
1888
|
-
function bytesToHex(b) {
|
|
1889
|
-
let s = "";
|
|
1890
|
-
for (let i = 0; i < b.length; i++) s += b[i].toString(16).padStart(2, "0");
|
|
1891
|
-
return s;
|
|
1892
|
-
}
|
|
1893
|
-
function hexToBytes(s) {
|
|
1894
|
-
if (s.length % 2 !== 0) throw new Error("hex: odd length");
|
|
1895
|
-
const out = new Uint8Array(s.length / 2);
|
|
1896
|
-
for (let i = 0; i < out.length; i++)
|
|
1897
|
-
out[i] = parseInt(s.slice(i * 2, i * 2 + 2), 16);
|
|
1898
|
-
return out;
|
|
1899
|
-
}
|
|
1900
1935
|
|
|
1901
1936
|
// src/offline/codec.ts
|
|
1902
1937
|
var ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";
|
|
@@ -1953,19 +1988,19 @@ function decodeBase45(s) {
|
|
|
1953
1988
|
|
|
1954
1989
|
// src/offline/messages.ts
|
|
1955
1990
|
var import_zod6 = require("zod");
|
|
1956
|
-
var
|
|
1991
|
+
var Base64Sig = import_zod6.z.string().min(16).max(4096).regex(/^[A-Za-z0-9+/]+={0,2}$/, "expected base64 (standard) signature");
|
|
1957
1992
|
var OfflinePaymentRequestSchema = import_zod6.z.object({
|
|
1958
1993
|
reference: import_zod6.z.string().min(1),
|
|
1959
1994
|
amountKobo: import_zod6.z.number().int().positive(),
|
|
1960
1995
|
merchantOAC: OACSchema,
|
|
1961
1996
|
expiresAtMs: import_zod6.z.number().int().positive(),
|
|
1962
|
-
merchantSig:
|
|
1997
|
+
merchantSig: Base64Sig
|
|
1963
1998
|
});
|
|
1964
1999
|
var OfflinePaymentAuthorizationSchema = import_zod6.z.object({
|
|
1965
2000
|
request: OfflinePaymentRequestSchema,
|
|
1966
2001
|
payerOAC: OACSchema,
|
|
1967
2002
|
payerCounter: import_zod6.z.number().int().positive(),
|
|
1968
|
-
payerSig:
|
|
2003
|
+
payerSig: Base64Sig
|
|
1969
2004
|
});
|
|
1970
2005
|
function buildPaymentRequest(input) {
|
|
1971
2006
|
if (!Number.isInteger(input.amountKobo) || input.amountKobo <= 0) {
|
|
@@ -1982,27 +2017,28 @@ function buildPaymentRequest(input) {
|
|
|
1982
2017
|
};
|
|
1983
2018
|
}
|
|
1984
2019
|
function signPaymentRequest(unsigned, merchantDevicePrivateKey) {
|
|
1985
|
-
const merchantSig =
|
|
1986
|
-
|
|
2020
|
+
const merchantSig = signIssuerP256(
|
|
2021
|
+
canonicalJSONBytes(unsigned),
|
|
2022
|
+
merchantDevicePrivateKey
|
|
1987
2023
|
);
|
|
1988
2024
|
return { ...unsigned, merchantSig };
|
|
1989
2025
|
}
|
|
1990
|
-
function verifyPaymentRequest(req,
|
|
2026
|
+
function verifyPaymentRequest(req, issuerPublicKeySpkiB64) {
|
|
1991
2027
|
try {
|
|
1992
2028
|
const parsed = OfflinePaymentRequestSchema.parse(req);
|
|
1993
2029
|
const { issuerSig: merchantOacSig, ...merchantOacUnsigned } = parsed.merchantOAC;
|
|
1994
|
-
if (!
|
|
2030
|
+
if (!verifyIssuerP256(
|
|
1995
2031
|
canonicalJSONBytes(merchantOacUnsigned),
|
|
1996
|
-
|
|
1997
|
-
|
|
2032
|
+
merchantOacSig,
|
|
2033
|
+
issuerPublicKeySpkiB64
|
|
1998
2034
|
)) {
|
|
1999
2035
|
return false;
|
|
2000
2036
|
}
|
|
2001
2037
|
const { merchantSig, ...unsigned } = parsed;
|
|
2002
|
-
return
|
|
2038
|
+
return verifyIssuerP256(
|
|
2003
2039
|
canonicalJSONBytes(unsigned),
|
|
2004
|
-
|
|
2005
|
-
|
|
2040
|
+
merchantSig,
|
|
2041
|
+
parsed.merchantOAC.devicePublicKey
|
|
2006
2042
|
);
|
|
2007
2043
|
} catch {
|
|
2008
2044
|
return false;
|
|
@@ -2022,28 +2058,30 @@ function buildAuthorization(input) {
|
|
|
2022
2058
|
};
|
|
2023
2059
|
}
|
|
2024
2060
|
function signAuthorization(unsigned, payerDevicePrivateKey) {
|
|
2025
|
-
const payerSig =
|
|
2026
|
-
|
|
2061
|
+
const payerSig = signIssuerP256(
|
|
2062
|
+
canonicalJSONBytes(unsigned),
|
|
2063
|
+
payerDevicePrivateKey
|
|
2027
2064
|
);
|
|
2028
2065
|
return { ...unsigned, payerSig };
|
|
2029
2066
|
}
|
|
2030
|
-
function verifyAuthorization(auth,
|
|
2067
|
+
function verifyAuthorization(auth, issuerPublicKeySpkiB64) {
|
|
2031
2068
|
try {
|
|
2032
2069
|
const parsed = OfflinePaymentAuthorizationSchema.parse(auth);
|
|
2033
|
-
if (!verifyPaymentRequest(parsed.request,
|
|
2070
|
+
if (!verifyPaymentRequest(parsed.request, issuerPublicKeySpkiB64))
|
|
2071
|
+
return false;
|
|
2034
2072
|
const { issuerSig: payerOacSig, ...payerOacUnsigned } = parsed.payerOAC;
|
|
2035
|
-
if (!
|
|
2073
|
+
if (!verifyIssuerP256(
|
|
2036
2074
|
canonicalJSONBytes(payerOacUnsigned),
|
|
2037
|
-
|
|
2038
|
-
|
|
2075
|
+
payerOacSig,
|
|
2076
|
+
issuerPublicKeySpkiB64
|
|
2039
2077
|
)) {
|
|
2040
2078
|
return false;
|
|
2041
2079
|
}
|
|
2042
2080
|
const { payerSig, ...unsigned } = parsed;
|
|
2043
|
-
return
|
|
2081
|
+
return verifyIssuerP256(
|
|
2044
2082
|
canonicalJSONBytes(unsigned),
|
|
2045
|
-
|
|
2046
|
-
|
|
2083
|
+
payerSig,
|
|
2084
|
+
parsed.payerOAC.devicePublicKey
|
|
2047
2085
|
);
|
|
2048
2086
|
} catch {
|
|
2049
2087
|
return false;
|
|
@@ -2099,10 +2137,14 @@ var PaymentClaimSchema = import_zod7.z.object({
|
|
|
2099
2137
|
occurredAtMs: import_zod7.z.number().int().nonnegative(),
|
|
2100
2138
|
completedAtMs: import_zod7.z.number().int().nonnegative().optional(),
|
|
2101
2139
|
contextId: import_zod7.z.string().optional(),
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2140
|
+
// Stage 2c: P-256 device keys are now SubjectPublicKeyInfo DER, base64.
|
|
2141
|
+
// Signatures are ASN.1 DER ECDSA(SHA-256), base64. Backwards-incompatible
|
|
2142
|
+
// wire change; the backend has the matching widening in offline-settlements
|
|
2143
|
+
// service + zod schema.
|
|
2144
|
+
payerPubkey: import_zod7.z.string().min(16).max(4096).regex(/^[A-Za-z0-9+/]+={0,2}$/),
|
|
2145
|
+
payerSignature: import_zod7.z.string().min(16).max(4096).regex(/^[A-Za-z0-9+/]+={0,2}$/),
|
|
2146
|
+
payeePubkey: import_zod7.z.string().min(16).max(4096).regex(/^[A-Za-z0-9+/]+={0,2}$/).optional(),
|
|
2147
|
+
payeeSignature: import_zod7.z.string().min(16).max(4096).regex(/^[A-Za-z0-9+/]+={0,2}$/).optional()
|
|
2106
2148
|
});
|
|
2107
2149
|
var SettlementSchema = import_zod7.z.object({
|
|
2108
2150
|
settlementId: import_zod7.z.string().uuid(),
|
|
@@ -2530,10 +2572,6 @@ var PASS_STATES = [
|
|
|
2530
2572
|
"expired",
|
|
2531
2573
|
"revoked"
|
|
2532
2574
|
];
|
|
2533
|
-
var HexString2 = (length) => import_zod9.z.string().regex(
|
|
2534
|
-
new RegExp(`^[0-9a-fA-F]{${length * 2}}$`),
|
|
2535
|
-
`expected ${length}-byte hex string`
|
|
2536
|
-
);
|
|
2537
2575
|
var PassMetadataSchema = import_zod9.z.record(
|
|
2538
2576
|
import_zod9.z.union([import_zod9.z.string(), import_zod9.z.number(), import_zod9.z.boolean(), import_zod9.z.null()])
|
|
2539
2577
|
);
|
|
@@ -2554,9 +2592,9 @@ var PassSchema = import_zod9.z.object({
|
|
|
2554
2592
|
nonce: import_zod9.z.string().min(1),
|
|
2555
2593
|
/** Device id this pass is bound to (FK to backend `device_keys`). */
|
|
2556
2594
|
holderDeviceId: import_zod9.z.string().min(1),
|
|
2557
|
-
/**
|
|
2558
|
-
* is verified against this key — it is the security-critical binding. */
|
|
2559
|
-
holderDevicePubkey:
|
|
2595
|
+
/** SubjectPublicKeyInfo DER (P-256) of the bound device, base64. The redemption
|
|
2596
|
+
* signature is verified against this key — it is the security-critical binding. */
|
|
2597
|
+
holderDevicePubkey: import_zod9.z.string().min(64).max(4096).regex(/^[A-Za-z0-9+/]+={0,2}$/),
|
|
2560
2598
|
/** Optional fixed amount for monetary passes (vouchers, gift cards) in kobo. */
|
|
2561
2599
|
amountKobo: import_zod9.z.number().int().nonnegative().optional(),
|
|
2562
2600
|
/** ISO-4217-ish currency code; required on the wire. SDK builders default to NGN. */
|
|
@@ -2565,7 +2603,8 @@ var PassSchema = import_zod9.z.object({
|
|
|
2565
2603
|
counterSeed: import_zod9.z.number().int().nonnegative(),
|
|
2566
2604
|
/** Optional cumulative spend cap in kobo across all redemptions of this pass. */
|
|
2567
2605
|
cumulativeCapKobo: import_zod9.z.number().int().nonnegative().optional(),
|
|
2568
|
-
|
|
2606
|
+
/** ASN.1 DER ECDSA P-256 signature, base64. */
|
|
2607
|
+
issuerSig: import_zod9.z.string().min(64).max(2048).regex(/^[A-Za-z0-9+/]+={0,2}$/)
|
|
2569
2608
|
}).refine((v) => v.validUntilMs > v.validFromMs, {
|
|
2570
2609
|
message: "validUntilMs must be greater than validFromMs"
|
|
2571
2610
|
});
|
|
@@ -2598,19 +2637,20 @@ function buildPass(input) {
|
|
|
2598
2637
|
return out;
|
|
2599
2638
|
}
|
|
2600
2639
|
function signPass(unsigned, issuerPrivateKey) {
|
|
2601
|
-
const issuerSig =
|
|
2602
|
-
|
|
2640
|
+
const issuerSig = signIssuerP256(
|
|
2641
|
+
canonicalJSONBytes(unsigned),
|
|
2642
|
+
issuerPrivateKey
|
|
2603
2643
|
);
|
|
2604
2644
|
return { ...unsigned, issuerSig };
|
|
2605
2645
|
}
|
|
2606
|
-
function verifyPass(pass,
|
|
2646
|
+
function verifyPass(pass, issuerPublicKeySpkiB64) {
|
|
2607
2647
|
try {
|
|
2608
2648
|
const parsed = PassSchema.parse(pass);
|
|
2609
2649
|
const { issuerSig, ...unsigned } = parsed;
|
|
2610
|
-
return
|
|
2650
|
+
return verifyIssuerP256(
|
|
2611
2651
|
canonicalJSONBytes(unsigned),
|
|
2612
|
-
|
|
2613
|
-
|
|
2652
|
+
issuerSig,
|
|
2653
|
+
issuerPublicKeySpkiB64
|
|
2614
2654
|
);
|
|
2615
2655
|
} catch {
|
|
2616
2656
|
return false;
|
|
@@ -2622,7 +2662,7 @@ function isPassWithinValidity(pass, nowMs) {
|
|
|
2622
2662
|
|
|
2623
2663
|
// src/passes/redemption.ts
|
|
2624
2664
|
var import_zod10 = require("zod");
|
|
2625
|
-
var
|
|
2665
|
+
var Base64Std2 = import_zod10.z.string().min(16).max(2048).regex(/^[A-Za-z0-9+/]+={0,2}$/, "expected base64 (std)");
|
|
2626
2666
|
var RedemptionSchema = import_zod10.z.object({
|
|
2627
2667
|
pass: PassSchema,
|
|
2628
2668
|
redeemerId: import_zod10.z.string().min(1),
|
|
@@ -2633,7 +2673,8 @@ var RedemptionSchema = import_zod10.z.object({
|
|
|
2633
2673
|
/** Amount being redeemed in kobo (0 for non-monetary passes like ride tickets). */
|
|
2634
2674
|
amountKobo: import_zod10.z.number().int().nonnegative(),
|
|
2635
2675
|
nonce: import_zod10.z.string().min(1),
|
|
2636
|
-
|
|
2676
|
+
/** ASN.1 DER ECDSA P-256 signature over canonicalJSONBytes(unsigned), base64. */
|
|
2677
|
+
holderSig: Base64Std2
|
|
2637
2678
|
});
|
|
2638
2679
|
var REDEEMABLE_STATES = /* @__PURE__ */ new Set(["issued", "active"]);
|
|
2639
2680
|
function buildRedemption(input) {
|
|
@@ -2687,31 +2728,28 @@ function buildRedemption(input) {
|
|
|
2687
2728
|
};
|
|
2688
2729
|
}
|
|
2689
2730
|
function signRedemption(unsigned, holderDevicePrivateKey) {
|
|
2690
|
-
const holderSig =
|
|
2691
|
-
|
|
2731
|
+
const holderSig = signIssuerP256(
|
|
2732
|
+
canonicalJSONBytes(unsigned),
|
|
2733
|
+
holderDevicePrivateKey
|
|
2692
2734
|
);
|
|
2693
2735
|
return { ...unsigned, holderSig };
|
|
2694
2736
|
}
|
|
2695
|
-
function verifyRedemption(r,
|
|
2737
|
+
function verifyRedemption(r, issuerPublicKeySpkiB64) {
|
|
2696
2738
|
try {
|
|
2697
2739
|
const parsed = RedemptionSchema.parse(r);
|
|
2698
2740
|
if (parsed.counter <= parsed.pass.counterSeed) return false;
|
|
2699
2741
|
const { issuerSig, ...passUnsigned } = parsed.pass;
|
|
2700
|
-
if (!
|
|
2742
|
+
if (!verifyIssuerP256(
|
|
2701
2743
|
canonicalJSONBytes(passUnsigned),
|
|
2702
|
-
|
|
2703
|
-
|
|
2744
|
+
issuerSig,
|
|
2745
|
+
issuerPublicKeySpkiB64
|
|
2704
2746
|
)) {
|
|
2705
2747
|
return false;
|
|
2706
2748
|
}
|
|
2707
|
-
const
|
|
2708
|
-
if (typeof
|
|
2749
|
+
const holderPub = parsed.pass.holderDevicePubkey;
|
|
2750
|
+
if (typeof holderPub !== "string") return false;
|
|
2709
2751
|
const { holderSig, ...unsigned } = parsed;
|
|
2710
|
-
return
|
|
2711
|
-
canonicalJSONBytes(unsigned),
|
|
2712
|
-
hexToBytes(holderSig),
|
|
2713
|
-
hexToBytes(holderHex)
|
|
2714
|
-
);
|
|
2752
|
+
return verifyIssuerP256(canonicalJSONBytes(unsigned), holderSig, holderPub);
|
|
2715
2753
|
} catch {
|
|
2716
2754
|
return false;
|
|
2717
2755
|
}
|
|
@@ -2721,10 +2759,6 @@ function verifyRedemption(r, issuerPublicKey) {
|
|
|
2721
2759
|
var import_zod11 = require("zod");
|
|
2722
2760
|
var RECEIPT_CHANNELS = ["cash", "pass"];
|
|
2723
2761
|
var RECEIPT_KINDS = RECEIPT_CHANNELS;
|
|
2724
|
-
var HexString3 = (length) => import_zod11.z.string().regex(
|
|
2725
|
-
new RegExp(`^[0-9a-fA-F]{${length * 2}}$`),
|
|
2726
|
-
`expected ${length}-byte hex string`
|
|
2727
|
-
);
|
|
2728
2762
|
var ReceiptPayloadSchema = import_zod11.z.record(
|
|
2729
2763
|
import_zod11.z.union([import_zod11.z.string(), import_zod11.z.number(), import_zod11.z.boolean(), import_zod11.z.null()])
|
|
2730
2764
|
);
|
|
@@ -2742,7 +2776,8 @@ var ReceiptSchema = import_zod11.z.object({
|
|
|
2742
2776
|
issuedAtMs: import_zod11.z.number().int().nonnegative(),
|
|
2743
2777
|
issuerId: import_zod11.z.string().min(1),
|
|
2744
2778
|
payload: ReceiptPayloadSchema,
|
|
2745
|
-
|
|
2779
|
+
/** ASN.1 DER ECDSA P-256 signature, base64. */
|
|
2780
|
+
issuerSig: import_zod11.z.string().min(64).max(2048).regex(/^[A-Za-z0-9+/]+={0,2}$/)
|
|
2746
2781
|
}).superRefine((v, ctx) => {
|
|
2747
2782
|
if (v.channel === "cash") {
|
|
2748
2783
|
if (!v.intentId) {
|
|
@@ -2808,19 +2843,20 @@ function buildReceipt(input) {
|
|
|
2808
2843
|
return out;
|
|
2809
2844
|
}
|
|
2810
2845
|
function signReceipt(unsigned, issuerPrivateKey) {
|
|
2811
|
-
const issuerSig =
|
|
2812
|
-
|
|
2846
|
+
const issuerSig = signIssuerP256(
|
|
2847
|
+
canonicalJSONBytes(unsigned),
|
|
2848
|
+
issuerPrivateKey
|
|
2813
2849
|
);
|
|
2814
2850
|
return { ...unsigned, issuerSig };
|
|
2815
2851
|
}
|
|
2816
|
-
function verifyReceipt(r,
|
|
2852
|
+
function verifyReceipt(r, issuerPublicKeySpkiB64) {
|
|
2817
2853
|
try {
|
|
2818
2854
|
const parsed = ReceiptSchema.parse(r);
|
|
2819
2855
|
const { issuerSig, ...unsigned } = parsed;
|
|
2820
|
-
return
|
|
2856
|
+
return verifyIssuerP256(
|
|
2821
2857
|
canonicalJSONBytes(unsigned),
|
|
2822
|
-
|
|
2823
|
-
|
|
2858
|
+
issuerSig,
|
|
2859
|
+
issuerPublicKeySpkiB64
|
|
2824
2860
|
);
|
|
2825
2861
|
} catch {
|
|
2826
2862
|
return false;
|
|
@@ -3155,9 +3191,8 @@ function createAccountsClient(opts) {
|
|
|
3155
3191
|
// src/me-offline/client.ts
|
|
3156
3192
|
var import_zod13 = require("zod");
|
|
3157
3193
|
var Hex64 = import_zod13.z.string().regex(/^[0-9a-f]{64}$/i);
|
|
3158
|
-
var HexAny = import_zod13.z.string().regex(/^[0-9a-f]+$/i);
|
|
3159
3194
|
var Sha256Hex = import_zod13.z.string().regex(/^[0-9a-f]{64}$/i);
|
|
3160
|
-
var
|
|
3195
|
+
var Base64Std3 = import_zod13.z.string().regex(/^[A-Za-z0-9+/]+={0,2}$/);
|
|
3161
3196
|
var RegisterDeviceKeyInputSchema = import_zod13.z.object({
|
|
3162
3197
|
deviceId: import_zod13.z.string().min(1).max(128),
|
|
3163
3198
|
publicKeyHex: Hex64
|
|
@@ -3168,15 +3203,15 @@ var AttestationSecurityLevelSchema = import_zod13.z.enum([
|
|
|
3168
3203
|
"SECURE_ENCLAVE",
|
|
3169
3204
|
"SOFTWARE"
|
|
3170
3205
|
]);
|
|
3171
|
-
var DeviceKeyAlgSchema = import_zod13.z.
|
|
3206
|
+
var DeviceKeyAlgSchema = import_zod13.z.literal("p256");
|
|
3172
3207
|
var RegisterDeviceKeyP256InputSchema = import_zod13.z.object({
|
|
3173
3208
|
deviceId: import_zod13.z.string().min(1).max(128),
|
|
3174
3209
|
/** P-256 SubjectPublicKeyInfo DER, base64. */
|
|
3175
|
-
publicKeySpkiB64:
|
|
3210
|
+
publicKeySpkiB64: Base64Std3.min(64).max(4096),
|
|
3176
3211
|
/** Base64 of the server-issued enrollment challenge string. */
|
|
3177
|
-
challengeB64:
|
|
3212
|
+
challengeB64: Base64Std3.min(8).max(1024),
|
|
3178
3213
|
/** iOS App Attest payload or Android X.509 Key Attestation chain. */
|
|
3179
|
-
attestationChainB64: import_zod13.z.array(
|
|
3214
|
+
attestationChainB64: import_zod13.z.array(Base64Std3.min(16).max(16384)).min(1).max(16),
|
|
3180
3215
|
securityLevel: AttestationSecurityLevelSchema
|
|
3181
3216
|
});
|
|
3182
3217
|
var P256EnrollmentChallengeInputSchema = import_zod13.z.object({
|
|
@@ -3190,9 +3225,12 @@ var DeviceKeyRecordSchema = import_zod13.z.object({
|
|
|
3190
3225
|
id: import_zod13.z.string().uuid(),
|
|
3191
3226
|
userId: import_zod13.z.string().uuid(),
|
|
3192
3227
|
deviceId: import_zod13.z.string(),
|
|
3193
|
-
|
|
3228
|
+
/** Always 'p256' on the consumer offline rail. Field retained for forward-compat. */
|
|
3229
|
+
alg: DeviceKeyAlgSchema.default("p256"),
|
|
3230
|
+
/** Legacy ed25519 hex key. Always null on new records (kept for back-compat reads). */
|
|
3194
3231
|
publicKeyHex: Hex64.nullable().default(null),
|
|
3195
|
-
|
|
3232
|
+
/** P-256 SubjectPublicKeyInfo DER, base64. Required for new records. */
|
|
3233
|
+
publicKeySpkiB64: Base64Std3.nullable().default(null),
|
|
3196
3234
|
securityLevel: AttestationSecurityLevelSchema.nullable().default(null),
|
|
3197
3235
|
hardwareBacked: import_zod13.z.boolean().default(false),
|
|
3198
3236
|
attestedAtMs: import_zod13.z.number().int().nonnegative().nullable().default(null),
|
|
@@ -3204,9 +3242,10 @@ var ConsumerOACSchema = import_zod13.z.object({
|
|
|
3204
3242
|
issuerId: import_zod13.z.string().min(1).max(64),
|
|
3205
3243
|
userId: import_zod13.z.string().uuid(),
|
|
3206
3244
|
deviceId: import_zod13.z.string().min(1).max(128),
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3245
|
+
/** Always 'p256'. Field retained for forward-compat. */
|
|
3246
|
+
alg: import_zod13.z.literal("p256").default("p256"),
|
|
3247
|
+
/** P-256 SubjectPublicKeyInfo DER, base64. */
|
|
3248
|
+
devicePubkeySpkiB64: Base64Std3.min(64).max(4096),
|
|
3210
3249
|
perTxCapKobo: import_zod13.z.number().int().positive(),
|
|
3211
3250
|
cumulativeCapKobo: import_zod13.z.number().int().positive(),
|
|
3212
3251
|
currency: import_zod13.z.string().length(3),
|
|
@@ -3214,20 +3253,13 @@ var ConsumerOACSchema = import_zod13.z.object({
|
|
|
3214
3253
|
validUntilMs: import_zod13.z.number().int().nonnegative(),
|
|
3215
3254
|
counterSeed: import_zod13.z.number().int().nonnegative(),
|
|
3216
3255
|
issuedAtMs: import_zod13.z.number().int().nonnegative()
|
|
3217
|
-
})
|
|
3218
|
-
(o) => {
|
|
3219
|
-
const alg = o.alg ?? "ed25519";
|
|
3220
|
-
if (alg === "ed25519") {
|
|
3221
|
-
return Boolean(o.devicePubkeyHex) && !o.devicePubkeySpkiB64;
|
|
3222
|
-
}
|
|
3223
|
-
return Boolean(o.devicePubkeySpkiB64) && !o.devicePubkeyHex;
|
|
3224
|
-
},
|
|
3225
|
-
{ message: "OAC device pubkey shape must match alg" }
|
|
3226
|
-
);
|
|
3256
|
+
});
|
|
3227
3257
|
var SignedConsumerOACSchema = import_zod13.z.object({
|
|
3228
3258
|
oac: ConsumerOACSchema,
|
|
3229
|
-
|
|
3230
|
-
|
|
3259
|
+
/** ASN.1 DER ECDSA P-256 issuer signature, base64. */
|
|
3260
|
+
issuerSig: Base64Std3.min(16).max(2048),
|
|
3261
|
+
/** Issuer's P-256 public key as SubjectPublicKeyInfo DER, base64. */
|
|
3262
|
+
issuerPublicKeySpkiB64: Base64Std3.min(64).max(4096)
|
|
3231
3263
|
});
|
|
3232
3264
|
var OACRecordSchema = SignedConsumerOACSchema.extend({
|
|
3233
3265
|
currentOfflineSpentKobo: import_zod13.z.number().int().nonnegative(),
|
|
@@ -3310,10 +3342,9 @@ var OfflineStatusResultSchema = import_zod13.z.object({
|
|
|
3310
3342
|
var OfflineStateResultSchema = import_zod13.z.object({
|
|
3311
3343
|
active: OACRecordSchema.nullable()
|
|
3312
3344
|
});
|
|
3313
|
-
var ClaimAlgSchema = import_zod13.z.enum(["ed25519", "p256"]);
|
|
3314
3345
|
var ConsumerPaymentClaimSchema = import_zod13.z.object({
|
|
3315
|
-
/**
|
|
3316
|
-
alg:
|
|
3346
|
+
/** Always 'p256'. Retained for forward-compat and as an explicit domain marker. */
|
|
3347
|
+
alg: import_zod13.z.literal("p256").default("p256"),
|
|
3317
3348
|
oacId: import_zod13.z.string().uuid(),
|
|
3318
3349
|
encounterId: Sha256Hex.optional(),
|
|
3319
3350
|
payerUserId: import_zod13.z.string().uuid(),
|
|
@@ -3326,28 +3357,11 @@ var ConsumerPaymentClaimSchema = import_zod13.z.object({
|
|
|
3326
3357
|
occurredAtMs: import_zod13.z.number().int().nonnegative(),
|
|
3327
3358
|
completedAtMs: import_zod13.z.number().int().nonnegative().optional(),
|
|
3328
3359
|
contextId: import_zod13.z.string().max(128).optional(),
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
// p256 path
|
|
3335
|
-
payerPubkeySpkiB64: import_zod13.z.string().min(64).max(4096).optional(),
|
|
3336
|
-
payerSignatureDerB64: import_zod13.z.string().min(16).max(2048).optional(),
|
|
3337
|
-
payeePubkeySpkiB64: import_zod13.z.string().min(64).max(4096).optional(),
|
|
3338
|
-
payeeSignatureDerB64: import_zod13.z.string().min(16).max(2048).optional()
|
|
3339
|
-
}).refine(
|
|
3340
|
-
(c) => {
|
|
3341
|
-
const alg = c.alg ?? "ed25519";
|
|
3342
|
-
if (alg === "ed25519") {
|
|
3343
|
-
return Boolean(c.payerPubkeyHex) && Boolean(c.payerSignature);
|
|
3344
|
-
}
|
|
3345
|
-
return Boolean(c.payerPubkeySpkiB64) && Boolean(c.payerSignatureDerB64);
|
|
3346
|
-
},
|
|
3347
|
-
{
|
|
3348
|
-
message: "payer key/signature fields must match alg (ed25519: hex; p256: SPKI+DER b64)"
|
|
3349
|
-
}
|
|
3350
|
-
);
|
|
3360
|
+
payerPubkeySpkiB64: Base64Std3.min(64).max(4096),
|
|
3361
|
+
payerSignatureDerB64: Base64Std3.min(16).max(2048),
|
|
3362
|
+
payeePubkeySpkiB64: Base64Std3.min(64).max(4096).optional(),
|
|
3363
|
+
payeeSignatureDerB64: Base64Std3.min(16).max(2048).optional()
|
|
3364
|
+
});
|
|
3351
3365
|
var ConsumerSettlementSchema = import_zod13.z.object({
|
|
3352
3366
|
settlementId: import_zod13.z.string().uuid(),
|
|
3353
3367
|
settlementKey: Sha256Hex,
|
|
@@ -3360,7 +3374,8 @@ var ConsumerSettlementSchema = import_zod13.z.object({
|
|
|
3360
3374
|
status: import_zod13.z.enum(["SETTLED", "REVIEW"]),
|
|
3361
3375
|
reviewReason: import_zod13.z.string().nullable(),
|
|
3362
3376
|
ledgerRef: import_zod13.z.string().nullable(),
|
|
3363
|
-
|
|
3377
|
+
/** ASN.1 DER ECDSA P-256 issuer signature, base64. */
|
|
3378
|
+
issuerSig: Base64Std3.min(16).max(2048),
|
|
3364
3379
|
createdAtMs: import_zod13.z.number().int().nonnegative()
|
|
3365
3380
|
});
|
|
3366
3381
|
var ConsumerSettleResultSchema = import_zod13.z.object({
|
|
@@ -3488,9 +3503,7 @@ function createMeOfflineClient(opts) {
|
|
|
3488
3503
|
}
|
|
3489
3504
|
|
|
3490
3505
|
// src/me-offline/signer.ts
|
|
3491
|
-
var
|
|
3492
|
-
var import_nist = require("@noble/curves/nist");
|
|
3493
|
-
var import_utils3 = require("@noble/hashes/utils");
|
|
3506
|
+
var import_nist2 = require("@noble/curves/nist");
|
|
3494
3507
|
var CLAIM_DOMAIN_V2 = "flur:consumer-offline:v2:claim";
|
|
3495
3508
|
function canonicalClaimSigningPayload(claim) {
|
|
3496
3509
|
return {
|
|
@@ -3512,7 +3525,7 @@ function canonicalClaimSigningPayload(claim) {
|
|
|
3512
3525
|
function canonicalClaimSigningBytes(claim) {
|
|
3513
3526
|
return canonicalJSONBytes(canonicalClaimSigningPayload(claim));
|
|
3514
3527
|
}
|
|
3515
|
-
function
|
|
3528
|
+
function bytesToBase642(bytes) {
|
|
3516
3529
|
if (typeof Buffer !== "undefined") {
|
|
3517
3530
|
return Buffer.from(bytes).toString("base64");
|
|
3518
3531
|
}
|
|
@@ -3520,7 +3533,7 @@ function bytesToBase64(bytes) {
|
|
|
3520
3533
|
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
3521
3534
|
return btoa(bin);
|
|
3522
3535
|
}
|
|
3523
|
-
function
|
|
3536
|
+
function base64ToBytes2(b64) {
|
|
3524
3537
|
if (typeof Buffer !== "undefined") {
|
|
3525
3538
|
return new Uint8Array(Buffer.from(b64, "base64"));
|
|
3526
3539
|
}
|
|
@@ -3529,7 +3542,7 @@ function base64ToBytes(b64) {
|
|
|
3529
3542
|
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
3530
3543
|
return out;
|
|
3531
3544
|
}
|
|
3532
|
-
var
|
|
3545
|
+
var P256_SPKI_HEADER2 = new Uint8Array([
|
|
3533
3546
|
48,
|
|
3534
3547
|
89,
|
|
3535
3548
|
48,
|
|
@@ -3561,38 +3574,25 @@ function p256PublicKeyToSpkiB64(rawUncompressed) {
|
|
|
3561
3574
|
if (rawUncompressed.length !== 65 || rawUncompressed[0] !== 4) {
|
|
3562
3575
|
throw new Error("p256: expected 65-byte uncompressed point");
|
|
3563
3576
|
}
|
|
3564
|
-
const out = new Uint8Array(
|
|
3565
|
-
out.set(
|
|
3566
|
-
out.set(rawUncompressed,
|
|
3567
|
-
return
|
|
3577
|
+
const out = new Uint8Array(P256_SPKI_HEADER2.length + rawUncompressed.length);
|
|
3578
|
+
out.set(P256_SPKI_HEADER2, 0);
|
|
3579
|
+
out.set(rawUncompressed, P256_SPKI_HEADER2.length);
|
|
3580
|
+
return bytesToBase642(out);
|
|
3568
3581
|
}
|
|
3569
3582
|
function p256SpkiB64ToPublicKey(spkiB64) {
|
|
3570
|
-
const spki =
|
|
3571
|
-
if (spki.length !==
|
|
3583
|
+
const spki = base64ToBytes2(spkiB64);
|
|
3584
|
+
if (spki.length !== P256_SPKI_HEADER2.length + 65) {
|
|
3572
3585
|
throw new Error("p256: invalid SPKI length");
|
|
3573
3586
|
}
|
|
3574
|
-
for (let i = 0; i <
|
|
3575
|
-
if (spki[i] !==
|
|
3587
|
+
for (let i = 0; i < P256_SPKI_HEADER2.length; i++) {
|
|
3588
|
+
if (spki[i] !== P256_SPKI_HEADER2[i]) {
|
|
3576
3589
|
throw new Error("p256: invalid SPKI header");
|
|
3577
3590
|
}
|
|
3578
3591
|
}
|
|
3579
|
-
return spki.slice(
|
|
3580
|
-
}
|
|
3581
|
-
function createSoftwareEd25519Signer(privateKey) {
|
|
3582
|
-
const pub = import_ed255198.ed25519.getPublicKey(privateKey);
|
|
3583
|
-
return {
|
|
3584
|
-
alg: "ed25519",
|
|
3585
|
-
async getPublicKey() {
|
|
3586
|
-
return { alg: "ed25519", publicKey: (0, import_utils3.bytesToHex)(pub) };
|
|
3587
|
-
},
|
|
3588
|
-
async sign(bytes) {
|
|
3589
|
-
const sig = import_ed255198.ed25519.sign(bytes, privateKey);
|
|
3590
|
-
return { alg: "ed25519", signature: (0, import_utils3.bytesToHex)(sig) };
|
|
3591
|
-
}
|
|
3592
|
-
};
|
|
3592
|
+
return spki.slice(P256_SPKI_HEADER2.length);
|
|
3593
3593
|
}
|
|
3594
3594
|
function createSoftwareP256Signer(privateKey) {
|
|
3595
|
-
const raw =
|
|
3595
|
+
const raw = import_nist2.p256.getPublicKey(privateKey, false);
|
|
3596
3596
|
const spkiB64 = p256PublicKeyToSpkiB64(raw);
|
|
3597
3597
|
return {
|
|
3598
3598
|
alg: "p256",
|
|
@@ -3600,35 +3600,87 @@ function createSoftwareP256Signer(privateKey) {
|
|
|
3600
3600
|
return { alg: "p256", publicKey: spkiB64 };
|
|
3601
3601
|
},
|
|
3602
3602
|
async sign(bytes) {
|
|
3603
|
-
const sig =
|
|
3603
|
+
const sig = import_nist2.p256.sign(bytes, privateKey, { prehash: true });
|
|
3604
3604
|
const der = sig.toBytes("der");
|
|
3605
|
-
return { alg: "p256", signature:
|
|
3605
|
+
return { alg: "p256", signature: bytesToBase642(der) };
|
|
3606
3606
|
}
|
|
3607
3607
|
};
|
|
3608
3608
|
}
|
|
3609
3609
|
function verifyClaimSignature(input) {
|
|
3610
3610
|
try {
|
|
3611
|
-
if (input.alg
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
}
|
|
3618
|
-
if (input.alg === "p256") {
|
|
3619
|
-
const sigDer = base64ToBytes(input.signature);
|
|
3620
|
-
const pub = p256SpkiB64ToPublicKey(input.publicKey);
|
|
3621
|
-
return import_nist.p256.verify(sigDer, input.bytes, pub, {
|
|
3622
|
-
prehash: true,
|
|
3623
|
-
format: "der"
|
|
3624
|
-
});
|
|
3625
|
-
}
|
|
3626
|
-
return false;
|
|
3611
|
+
if (input.alg !== "p256") return false;
|
|
3612
|
+
const sigDer = base64ToBytes2(input.signature);
|
|
3613
|
+
const pub = p256SpkiB64ToPublicKey(input.publicKey);
|
|
3614
|
+
return import_nist2.p256.verify(sigDer, input.bytes, pub, {
|
|
3615
|
+
prehash: true,
|
|
3616
|
+
format: "der"
|
|
3617
|
+
});
|
|
3627
3618
|
} catch {
|
|
3628
3619
|
return false;
|
|
3629
3620
|
}
|
|
3630
3621
|
}
|
|
3631
3622
|
|
|
3623
|
+
// src/me-offline/sms.ts
|
|
3624
|
+
var OFFLINE_CLAIM_SMS_PREFIX = "FLURC1.";
|
|
3625
|
+
var TOKEN_RE = /(?:^|\s)(FLURC1\.[A-Za-z0-9_-]+={0,2})(?:\s|$)/;
|
|
3626
|
+
function encodeOfflineClaimSmsMessage(claim) {
|
|
3627
|
+
const parsed = ConsumerPaymentClaimSchema.parse(claim);
|
|
3628
|
+
const json = JSON.stringify(parsed);
|
|
3629
|
+
return `${OFFLINE_CLAIM_SMS_PREFIX}${base64UrlEncodeUtf8(json)}`;
|
|
3630
|
+
}
|
|
3631
|
+
function decodeOfflineClaimSmsMessage(message) {
|
|
3632
|
+
const token = extractOfflineClaimSmsToken(message);
|
|
3633
|
+
if (!token) {
|
|
3634
|
+
throw new Error("offline claim SMS token not found");
|
|
3635
|
+
}
|
|
3636
|
+
const encoded = token.slice(OFFLINE_CLAIM_SMS_PREFIX.length);
|
|
3637
|
+
let raw;
|
|
3638
|
+
try {
|
|
3639
|
+
raw = JSON.parse(base64UrlDecodeUtf8(encoded));
|
|
3640
|
+
} catch {
|
|
3641
|
+
throw new Error("offline claim SMS token is malformed");
|
|
3642
|
+
}
|
|
3643
|
+
const parsed = ConsumerPaymentClaimSchema.safeParse(raw);
|
|
3644
|
+
if (!parsed.success) {
|
|
3645
|
+
throw new Error("offline claim SMS token is invalid");
|
|
3646
|
+
}
|
|
3647
|
+
return parsed.data;
|
|
3648
|
+
}
|
|
3649
|
+
function extractOfflineClaimSmsToken(message) {
|
|
3650
|
+
const trimmed = message.trim();
|
|
3651
|
+
if (trimmed.startsWith(OFFLINE_CLAIM_SMS_PREFIX)) {
|
|
3652
|
+
return trimmed.split(/\s+/, 1)[0] ?? null;
|
|
3653
|
+
}
|
|
3654
|
+
return TOKEN_RE.exec(message)?.[1] ?? null;
|
|
3655
|
+
}
|
|
3656
|
+
function base64UrlEncodeUtf8(input) {
|
|
3657
|
+
const bytes = new TextEncoder().encode(input);
|
|
3658
|
+
let binary = "";
|
|
3659
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
3660
|
+
const base64 = typeof btoa === "function" ? btoa(binary) : typeof Buffer !== "undefined" ? Buffer.from(bytes).toString("base64") : void 0;
|
|
3661
|
+
if (!base64) throw new Error("base64 encoder unavailable");
|
|
3662
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
3663
|
+
}
|
|
3664
|
+
function base64UrlDecodeUtf8(input) {
|
|
3665
|
+
const base64 = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
3666
|
+
const padded = base64.padEnd(
|
|
3667
|
+
base64.length + (4 - base64.length % 4) % 4,
|
|
3668
|
+
"="
|
|
3669
|
+
);
|
|
3670
|
+
if (typeof atob === "function") {
|
|
3671
|
+
const binary = atob(padded);
|
|
3672
|
+
const bytes = new Uint8Array(binary.length);
|
|
3673
|
+
for (let index = 0; index < binary.length; index++) {
|
|
3674
|
+
bytes[index] = binary.charCodeAt(index);
|
|
3675
|
+
}
|
|
3676
|
+
return new TextDecoder().decode(bytes);
|
|
3677
|
+
}
|
|
3678
|
+
if (typeof Buffer !== "undefined") {
|
|
3679
|
+
return Buffer.from(padded, "base64").toString("utf8");
|
|
3680
|
+
}
|
|
3681
|
+
throw new Error("base64 decoder unavailable");
|
|
3682
|
+
}
|
|
3683
|
+
|
|
3632
3684
|
// src/partner-funding/client.ts
|
|
3633
3685
|
var import_zod14 = require("zod");
|
|
3634
3686
|
var MinorString = import_zod14.z.string().regex(/^-?\d+$/);
|
|
@@ -4004,14 +4056,15 @@ function buildArtifactBody(input) {
|
|
|
4004
4056
|
return { ...header, data: input.data };
|
|
4005
4057
|
}
|
|
4006
4058
|
function signArtifact(body, privateKey) {
|
|
4007
|
-
const sig =
|
|
4059
|
+
const sig = signIssuerP256(canonicalJSONBytes(body), privateKey);
|
|
4008
4060
|
return { body, sig };
|
|
4009
4061
|
}
|
|
4010
4062
|
function encodeArtifactUri(signed) {
|
|
4011
4063
|
const bodyBytes = canonicalJSONBytes(signed.body);
|
|
4012
4064
|
const bodyB64 = base64UrlEncode(bodyBytes);
|
|
4013
|
-
const
|
|
4014
|
-
|
|
4065
|
+
const sigBytes = base64UrlDecode(signed.sig);
|
|
4066
|
+
const sigB64Url = base64UrlEncode(sigBytes);
|
|
4067
|
+
return `${FLUR_ARTIFACT_URI_PREFIX}${signed.body.t}/${bodyB64}.${sigB64Url}`;
|
|
4015
4068
|
}
|
|
4016
4069
|
function decodeArtifactUri(uri) {
|
|
4017
4070
|
if (!uri.startsWith(FLUR_ARTIFACT_URI_PREFIX)) {
|
|
@@ -4042,9 +4095,9 @@ function decodeArtifactUri(uri) {
|
|
|
4042
4095
|
}
|
|
4043
4096
|
const bodyBytes = base64UrlDecode(payload.slice(0, dot));
|
|
4044
4097
|
const sigBytes = base64UrlDecode(payload.slice(dot + 1));
|
|
4045
|
-
if (sigBytes.length
|
|
4098
|
+
if (sigBytes.length < 64 || sigBytes.length > 80) {
|
|
4046
4099
|
throw new FlurArtifactError(
|
|
4047
|
-
`Signature
|
|
4100
|
+
`Signature length out of range: ${sigBytes.length}`,
|
|
4048
4101
|
"INVALID_SIGNATURE"
|
|
4049
4102
|
);
|
|
4050
4103
|
}
|
|
@@ -4071,17 +4124,26 @@ function decodeArtifactUri(uri) {
|
|
|
4071
4124
|
type,
|
|
4072
4125
|
bodyBytes,
|
|
4073
4126
|
body: bodyJson,
|
|
4074
|
-
|
|
4127
|
+
// Encode as standard base64 (not url-safe) for sig field consistency.
|
|
4128
|
+
sig: encodeStdBase64(sigBytes)
|
|
4075
4129
|
};
|
|
4076
4130
|
}
|
|
4077
|
-
function
|
|
4131
|
+
function encodeStdBase64(bytes) {
|
|
4132
|
+
if (typeof Buffer !== "undefined") {
|
|
4133
|
+
return Buffer.from(bytes).toString("base64");
|
|
4134
|
+
}
|
|
4135
|
+
let bin = "";
|
|
4136
|
+
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
4137
|
+
return typeof btoa === "function" ? btoa(bin) : "";
|
|
4138
|
+
}
|
|
4139
|
+
function verifyArtifactSignature(decoded, publicKeySpkiB64, options = {}) {
|
|
4078
4140
|
if (options.enforceExpiry !== false && decoded.body.exp !== void 0) {
|
|
4079
4141
|
const now = options.nowSeconds ?? Math.floor(Date.now() / 1e3);
|
|
4080
4142
|
if (decoded.body.exp < now) {
|
|
4081
4143
|
throw new FlurArtifactError("Artifact has expired", "EXPIRED");
|
|
4082
4144
|
}
|
|
4083
4145
|
}
|
|
4084
|
-
return
|
|
4146
|
+
return verifyIssuerP256(decoded.bodyBytes, decoded.sig, publicKeySpkiB64);
|
|
4085
4147
|
}
|
|
4086
4148
|
|
|
4087
4149
|
// src/artifacts/types.ts
|
|
@@ -4100,7 +4162,7 @@ var ARTIFACT_TYPES = {
|
|
|
4100
4162
|
PASS: "pass",
|
|
4101
4163
|
IDENTITY: "identity"
|
|
4102
4164
|
};
|
|
4103
|
-
var
|
|
4165
|
+
var HexString = (length) => import_zod16.z.string().regex(
|
|
4104
4166
|
new RegExp(`^[0-9a-fA-F]{${length * 2}}$`),
|
|
4105
4167
|
`expected ${length}-byte hex string`
|
|
4106
4168
|
);
|
|
@@ -4118,7 +4180,7 @@ var ReceiptArtifactSchema = import_zod16.z.object({
|
|
|
4118
4180
|
settledAtMs: import_zod16.z.number().int().positive(),
|
|
4119
4181
|
ledgerTxnId: import_zod16.z.string().min(1).max(64).optional(),
|
|
4120
4182
|
memo: import_zod16.z.string().max(140).optional(),
|
|
4121
|
-
hashChainPrev:
|
|
4183
|
+
hashChainPrev: HexString(32).optional()
|
|
4122
4184
|
});
|
|
4123
4185
|
var ShortId = import_zod16.z.string().min(1).max(64);
|
|
4124
4186
|
var PositiveInt = import_zod16.z.number().int().positive();
|
|
@@ -4200,7 +4262,7 @@ var StatementArtifactSchema = import_zod16.z.object({
|
|
|
4200
4262
|
closingBalanceKobo: import_zod16.z.number().int(),
|
|
4201
4263
|
transactionCount: NonNegativeInt,
|
|
4202
4264
|
currency: Currency2,
|
|
4203
|
-
hashChainPrev:
|
|
4265
|
+
hashChainPrev: HexString(32).optional()
|
|
4204
4266
|
}).refine((v) => v.periodEndMs > v.periodStartMs, {
|
|
4205
4267
|
message: "periodEndMs must be greater than periodStartMs",
|
|
4206
4268
|
path: ["periodEndMs"]
|
|
@@ -4233,7 +4295,7 @@ var IdentityArtifactSchema = import_zod16.z.object({
|
|
|
4233
4295
|
"kyc_tier",
|
|
4234
4296
|
"age_band"
|
|
4235
4297
|
]),
|
|
4236
|
-
claimValueHash:
|
|
4298
|
+
claimValueHash: HexString(32),
|
|
4237
4299
|
attestedAtMs: PositiveInt
|
|
4238
4300
|
});
|
|
4239
4301
|
var ARTIFACT_BODY_SCHEMAS = {
|
|
@@ -4297,7 +4359,7 @@ function createArtifactUri(input) {
|
|
|
4297
4359
|
const signed = signArtifact(body, input.privateKey);
|
|
4298
4360
|
return { uri: encodeArtifactUri(signed), signed };
|
|
4299
4361
|
}
|
|
4300
|
-
function verifyArtifactUri(uri,
|
|
4362
|
+
function verifyArtifactUri(uri, publicKeySpkiB64, options = {}) {
|
|
4301
4363
|
const decoded = decodeArtifactUri(uri);
|
|
4302
4364
|
if (!isKnownArtifactType(decoded.type)) {
|
|
4303
4365
|
throw new FlurArtifactError(
|
|
@@ -4319,7 +4381,7 @@ function verifyArtifactUri(uri, publicKey, options = {}) {
|
|
|
4319
4381
|
"INVALID_BODY"
|
|
4320
4382
|
);
|
|
4321
4383
|
}
|
|
4322
|
-
const ok = verifyArtifactSignature(decoded,
|
|
4384
|
+
const ok = verifyArtifactSignature(decoded, publicKeySpkiB64, options);
|
|
4323
4385
|
if (!ok) {
|
|
4324
4386
|
throw new FlurArtifactError(
|
|
4325
4387
|
"Artifact signature verification failed",
|
|
@@ -4413,6 +4475,7 @@ function createOfflinePaymentAuthorizationArtifactUri(input) {
|
|
|
4413
4475
|
OAC_DEFAULT_CUMULATIVE_KOBO,
|
|
4414
4476
|
OAC_DEFAULT_PER_TX_KOBO,
|
|
4415
4477
|
OAC_DEFAULT_VALIDITY_MS,
|
|
4478
|
+
OFFLINE_CLAIM_SMS_PREFIX,
|
|
4416
4479
|
OfflineClaimArtifactSchema,
|
|
4417
4480
|
OfflineHoldRecordSchema,
|
|
4418
4481
|
OfflinePaymentAuthorizationArtifactSchema,
|
|
@@ -4508,20 +4571,21 @@ function createOfflinePaymentAuthorizationArtifactUri(input) {
|
|
|
4508
4571
|
createPassesClient,
|
|
4509
4572
|
createReceiptArtifactUri,
|
|
4510
4573
|
createReceiptsClient,
|
|
4511
|
-
createSoftwareEd25519Signer,
|
|
4512
4574
|
createSoftwareP256Signer,
|
|
4513
4575
|
decodeArtifactUri,
|
|
4514
4576
|
decodeAuthorizationQR,
|
|
4515
4577
|
decodeBase45,
|
|
4578
|
+
decodeOfflineClaimSmsMessage,
|
|
4516
4579
|
decodePaymentRequestQR,
|
|
4517
4580
|
encodeArtifactUri,
|
|
4518
4581
|
encodeAuthorizationQR,
|
|
4519
4582
|
encodeBase45,
|
|
4520
4583
|
encodeNQR,
|
|
4584
|
+
encodeOfflineClaimSmsMessage,
|
|
4521
4585
|
encodePaymentRequestQR,
|
|
4586
|
+
extractOfflineClaimSmsToken,
|
|
4522
4587
|
formatAmount,
|
|
4523
4588
|
generateDynamicQR,
|
|
4524
|
-
generateKeyPair,
|
|
4525
4589
|
generateStaticQR,
|
|
4526
4590
|
init,
|
|
4527
4591
|
isHardenedArtifactType,
|
|
@@ -4532,13 +4596,10 @@ function createOfflinePaymentAuthorizationArtifactUri(input) {
|
|
|
4532
4596
|
parseAmountInput,
|
|
4533
4597
|
parseNQR,
|
|
4534
4598
|
parseQR,
|
|
4535
|
-
publicKeyFromPrivate,
|
|
4536
4599
|
readTLV,
|
|
4537
4600
|
routingHint,
|
|
4538
|
-
sign,
|
|
4539
4601
|
signArtifact,
|
|
4540
4602
|
signAuthorization,
|
|
4541
|
-
signCanonical,
|
|
4542
4603
|
signOAC,
|
|
4543
4604
|
signPartnerRequest,
|
|
4544
4605
|
signPass,
|
|
@@ -4546,11 +4607,9 @@ function createOfflinePaymentAuthorizationArtifactUri(input) {
|
|
|
4546
4607
|
signReceipt,
|
|
4547
4608
|
signRedemption,
|
|
4548
4609
|
signRequestHMAC,
|
|
4549
|
-
verify,
|
|
4550
4610
|
verifyArtifactSignature,
|
|
4551
4611
|
verifyArtifactUri,
|
|
4552
4612
|
verifyAuthorization,
|
|
4553
|
-
verifyCanonical,
|
|
4554
4613
|
verifyClaimSignature,
|
|
4555
4614
|
verifyOAC,
|
|
4556
4615
|
verifyPass,
|