@nokinc-flur/sdk 2.2.0 → 2.3.0
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 +145 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +265 -7
- package/dist/index.d.ts +265 -7
- package/dist/index.js +136 -8
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2961,7 +2961,17 @@ var Base64Std3 = z13.string().regex(/^[A-Za-z0-9+/]+={0,2}$/);
|
|
|
2961
2961
|
var ClaimNonce = z13.string().min(8).max(128).refine((value) => !value.includes("|"), {
|
|
2962
2962
|
message: "nonce must not contain |"
|
|
2963
2963
|
});
|
|
2964
|
-
var ACCOUNT_FUNDED_OAC_MAX_TTL_MS = 1e3 * 60 * 60 * 24
|
|
2964
|
+
var ACCOUNT_FUNDED_OAC_MAX_TTL_MS = 1e3 * 60 * 60 * 24;
|
|
2965
|
+
var IssuerTrustKeySchema = z13.object({
|
|
2966
|
+
issuerId: z13.string().min(1).max(128),
|
|
2967
|
+
alg: z13.literal("p256"),
|
|
2968
|
+
publicKeySpkiB64: Base64Std3.min(64).max(4096),
|
|
2969
|
+
notBeforeMs: z13.number().int().nonnegative().optional(),
|
|
2970
|
+
notAfterMs: z13.number().int().positive().optional()
|
|
2971
|
+
});
|
|
2972
|
+
var IssuerTrustBundleSchema = z13.object({
|
|
2973
|
+
keys: z13.array(IssuerTrustKeySchema).min(1)
|
|
2974
|
+
});
|
|
2965
2975
|
var CONSUMER_OFFLINE_CLAIM_SUBMIT_GRACE_MS = 1e3 * 60 * 60 * 24;
|
|
2966
2976
|
var AttestationSecurityLevelSchema = z13.enum([
|
|
2967
2977
|
"STRONGBOX",
|
|
@@ -3015,13 +3025,28 @@ var ConsumerOACSchema = z13.object({
|
|
|
3015
3025
|
alg: z13.literal("p256"),
|
|
3016
3026
|
/** P-256 SubjectPublicKeyInfo DER, base64. */
|
|
3017
3027
|
devicePubkeySpkiB64: Base64Std3.min(64).max(4096),
|
|
3018
|
-
|
|
3019
|
-
|
|
3028
|
+
/**
|
|
3029
|
+
* Per-transaction / cumulative offline spend ceilings. Zero is valid and
|
|
3030
|
+
* denotes an identity-only OAC: the credential proves who the holder is and
|
|
3031
|
+
* binds their device key for offline-verifiable first contact, but carries
|
|
3032
|
+
* no offline spend authority (every claim against it routes to REVIEW).
|
|
3033
|
+
* Issued to zero-balance wallets so they can still be paid offline.
|
|
3034
|
+
*/
|
|
3035
|
+
perTxCapKobo: z13.number().int().nonnegative(),
|
|
3036
|
+
cumulativeCapKobo: z13.number().int().nonnegative(),
|
|
3020
3037
|
currency: z13.string().length(3),
|
|
3021
3038
|
validFromMs: z13.number().int().nonnegative(),
|
|
3022
3039
|
validUntilMs: z13.number().int().nonnegative(),
|
|
3023
3040
|
counterSeed: z13.number().int().nonnegative(),
|
|
3024
|
-
issuedAtMs: z13.number().int().nonnegative()
|
|
3041
|
+
issuedAtMs: z13.number().int().nonnegative(),
|
|
3042
|
+
/**
|
|
3043
|
+
* Issuer-attested identity folded into the OAC so a single signed
|
|
3044
|
+
* credential serves both Tier B offline-verifiable identity and offline
|
|
3045
|
+
* spend authority. Verified offline against a pinned issuer key; the
|
|
3046
|
+
* backend remains authoritative at settlement.
|
|
3047
|
+
*/
|
|
3048
|
+
phoneE164: z13.string().regex(/^\+[1-9]\d{7,14}$/),
|
|
3049
|
+
displayName: z13.string().min(1).max(64)
|
|
3025
3050
|
});
|
|
3026
3051
|
var SignedConsumerOACSchema = z13.object({
|
|
3027
3052
|
oac: ConsumerOACSchema,
|
|
@@ -3189,6 +3214,12 @@ function createMeOfflineClient(opts) {
|
|
|
3189
3214
|
`/v1/me/offline/settlements/${encodeURIComponent(idOrKey)}`,
|
|
3190
3215
|
void 0,
|
|
3191
3216
|
(raw) => ConsumerSettlementSchema.parse(raw)
|
|
3217
|
+
),
|
|
3218
|
+
getIssuerKeys: () => call(
|
|
3219
|
+
"GET",
|
|
3220
|
+
"/v1/issuer/keys",
|
|
3221
|
+
void 0,
|
|
3222
|
+
(raw) => IssuerTrustBundleSchema.parse(raw)
|
|
3192
3223
|
)
|
|
3193
3224
|
};
|
|
3194
3225
|
}
|
|
@@ -3549,13 +3580,101 @@ function base64UrlDecodeUtf8(input) {
|
|
|
3549
3580
|
throw new Error("base64 decoder unavailable");
|
|
3550
3581
|
}
|
|
3551
3582
|
|
|
3583
|
+
// src/me-offline/oac.ts
|
|
3584
|
+
var CONSUMER_OAC_DOMAIN = "flur:consumer-offline:v1:oac";
|
|
3585
|
+
function consumerOacSigningPayload(oac) {
|
|
3586
|
+
return { domain: CONSUMER_OAC_DOMAIN, ...oac };
|
|
3587
|
+
}
|
|
3588
|
+
function verifyOacOffline(signed, trustedKeys, options = {}) {
|
|
3589
|
+
const parsed = SignedConsumerOACSchema.safeParse(signed);
|
|
3590
|
+
if (!parsed.success) return { ok: false, reason: "malformed" };
|
|
3591
|
+
const oacParsed = ConsumerOACSchema.safeParse(parsed.data.oac);
|
|
3592
|
+
if (!oacParsed.success) return { ok: false, reason: "malformed" };
|
|
3593
|
+
const oac = oacParsed.data;
|
|
3594
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
3595
|
+
const pinned = trustedKeys.filter(
|
|
3596
|
+
(k) => k.issuerId === oac.issuerId && (k.notBeforeMs === void 0 || nowMs >= k.notBeforeMs) && (k.notAfterMs === void 0 || nowMs <= k.notAfterMs)
|
|
3597
|
+
);
|
|
3598
|
+
if (pinned.length === 0) return { ok: false, reason: "untrusted_issuer" };
|
|
3599
|
+
const signingBytes = canonicalJSONBytes(consumerOacSigningPayload(oac));
|
|
3600
|
+
const signatureOk = pinned.some(
|
|
3601
|
+
(k) => verifyIssuerP256(signingBytes, parsed.data.issuerSig, k.publicKeySpkiB64)
|
|
3602
|
+
);
|
|
3603
|
+
if (!signatureOk) return { ok: false, reason: "signature_invalid" };
|
|
3604
|
+
if (oac.validUntilMs - oac.validFromMs > ACCOUNT_FUNDED_OAC_MAX_TTL_MS) {
|
|
3605
|
+
return { ok: false, reason: "window_too_long" };
|
|
3606
|
+
}
|
|
3607
|
+
if (nowMs < oac.validFromMs) return { ok: false, reason: "not_yet_valid" };
|
|
3608
|
+
if (nowMs >= oac.validUntilMs) return { ok: false, reason: "expired" };
|
|
3609
|
+
return {
|
|
3610
|
+
ok: true,
|
|
3611
|
+
oac,
|
|
3612
|
+
identity: {
|
|
3613
|
+
oacId: oac.oacId,
|
|
3614
|
+
issuerId: oac.issuerId,
|
|
3615
|
+
userId: oac.userId,
|
|
3616
|
+
phoneE164: oac.phoneE164,
|
|
3617
|
+
displayName: oac.displayName,
|
|
3618
|
+
devicePubkeySpkiB64: oac.devicePubkeySpkiB64
|
|
3619
|
+
}
|
|
3620
|
+
};
|
|
3621
|
+
}
|
|
3622
|
+
var CONSUMER_OAC_QR_PREFIX = "FLUROAC1.";
|
|
3623
|
+
function isConsumerOacQR(value) {
|
|
3624
|
+
return value.startsWith(CONSUMER_OAC_QR_PREFIX);
|
|
3625
|
+
}
|
|
3626
|
+
function encodeConsumerOacQR(signed) {
|
|
3627
|
+
const parsed = SignedConsumerOACSchema.parse(signed);
|
|
3628
|
+
return `${CONSUMER_OAC_QR_PREFIX}${base64UrlEncodeUtf82(JSON.stringify(parsed))}`;
|
|
3629
|
+
}
|
|
3630
|
+
function decodeUnverifiedConsumerOacQR(value) {
|
|
3631
|
+
if (!value.startsWith(CONSUMER_OAC_QR_PREFIX)) {
|
|
3632
|
+
throw new Error("not a Flur consumer OAC QR");
|
|
3633
|
+
}
|
|
3634
|
+
const encoded = value.slice(CONSUMER_OAC_QR_PREFIX.length);
|
|
3635
|
+
let raw;
|
|
3636
|
+
try {
|
|
3637
|
+
raw = JSON.parse(base64UrlDecodeUtf82(encoded));
|
|
3638
|
+
} catch {
|
|
3639
|
+
throw new Error("consumer OAC QR is malformed");
|
|
3640
|
+
}
|
|
3641
|
+
return SignedConsumerOACSchema.parse(raw);
|
|
3642
|
+
}
|
|
3643
|
+
function base64UrlEncodeUtf82(input) {
|
|
3644
|
+
const bytes = new TextEncoder().encode(input);
|
|
3645
|
+
let binary = "";
|
|
3646
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
3647
|
+
const base64 = typeof btoa === "function" ? btoa(binary) : typeof Buffer !== "undefined" ? Buffer.from(bytes).toString("base64") : void 0;
|
|
3648
|
+
if (!base64) throw new Error("base64 encoder unavailable");
|
|
3649
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
3650
|
+
}
|
|
3651
|
+
function base64UrlDecodeUtf82(input) {
|
|
3652
|
+
const base64 = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
3653
|
+
const padded = base64.padEnd(
|
|
3654
|
+
base64.length + (4 - base64.length % 4) % 4,
|
|
3655
|
+
"="
|
|
3656
|
+
);
|
|
3657
|
+
if (typeof atob === "function") {
|
|
3658
|
+
const binary = atob(padded);
|
|
3659
|
+
const bytes = new Uint8Array(binary.length);
|
|
3660
|
+
for (let index = 0; index < binary.length; index++) {
|
|
3661
|
+
bytes[index] = binary.charCodeAt(index);
|
|
3662
|
+
}
|
|
3663
|
+
return new TextDecoder().decode(bytes);
|
|
3664
|
+
}
|
|
3665
|
+
if (typeof Buffer !== "undefined") {
|
|
3666
|
+
return Buffer.from(padded, "base64").toString("utf8");
|
|
3667
|
+
}
|
|
3668
|
+
throw new Error("base64 decoder unavailable");
|
|
3669
|
+
}
|
|
3670
|
+
|
|
3552
3671
|
// src/me-offline/sms.ts
|
|
3553
3672
|
import { p256 as p2563 } from "@noble/curves/nist";
|
|
3554
3673
|
var OFFLINE_CLAIM_SMS_PREFIX = "FLURC1.";
|
|
3555
3674
|
var CLAIM_TOKEN_RE = /(?:^|\s)(FLURC1\.[A-Za-z0-9_-]+={0,2})(?:\s|$)/;
|
|
3556
3675
|
function encodeOfflineClaimSmsMessage(claim) {
|
|
3557
3676
|
const parsed = ConsumerPaymentClaimSchema.parse(claim);
|
|
3558
|
-
return `${OFFLINE_CLAIM_SMS_PREFIX}${
|
|
3677
|
+
return `${OFFLINE_CLAIM_SMS_PREFIX}${base64UrlEncodeUtf83(
|
|
3559
3678
|
JSON.stringify(parsed)
|
|
3560
3679
|
)}`;
|
|
3561
3680
|
}
|
|
@@ -3565,7 +3684,7 @@ function decodeOfflineClaimSmsMessage(message) {
|
|
|
3565
3684
|
const encoded = token.slice(OFFLINE_CLAIM_SMS_PREFIX.length);
|
|
3566
3685
|
let raw;
|
|
3567
3686
|
try {
|
|
3568
|
-
raw = JSON.parse(
|
|
3687
|
+
raw = JSON.parse(base64UrlDecodeUtf83(encoded));
|
|
3569
3688
|
} catch {
|
|
3570
3689
|
throw new Error("offline claim QR token is malformed");
|
|
3571
3690
|
}
|
|
@@ -3791,10 +3910,10 @@ function bytesToBase64Url(bytes) {
|
|
|
3791
3910
|
}
|
|
3792
3911
|
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
3793
3912
|
}
|
|
3794
|
-
function
|
|
3913
|
+
function base64UrlEncodeUtf83(input) {
|
|
3795
3914
|
return bytesToBase64Url(utf8(input));
|
|
3796
3915
|
}
|
|
3797
|
-
function
|
|
3916
|
+
function base64UrlDecodeUtf83(input) {
|
|
3798
3917
|
return new TextDecoder().decode(base64UrlToBytes(input));
|
|
3799
3918
|
}
|
|
3800
3919
|
function base64UrlToBytes(input) {
|
|
@@ -4691,6 +4810,8 @@ export {
|
|
|
4691
4810
|
CLAIM_DOMAIN_V2,
|
|
4692
4811
|
COLLECTION_INTENT_STATUSES,
|
|
4693
4812
|
COLLECTION_PAYMENT_STATUSES,
|
|
4813
|
+
CONSUMER_OAC_DOMAIN,
|
|
4814
|
+
CONSUMER_OAC_QR_PREFIX,
|
|
4694
4815
|
CONSUMER_OFFLINE_CLAIM_SUBMIT_GRACE_MS,
|
|
4695
4816
|
CONSUMER_PAYMENT_REQUEST_DOMAIN,
|
|
4696
4817
|
CONSUMER_SETTLEMENT_DOMAIN,
|
|
@@ -4729,6 +4850,8 @@ export {
|
|
|
4729
4850
|
IdentityArtifactSchema,
|
|
4730
4851
|
IngestFundingResultSchema,
|
|
4731
4852
|
IssueAccountOacInputSchema,
|
|
4853
|
+
IssuerTrustBundleSchema,
|
|
4854
|
+
IssuerTrustKeySchema,
|
|
4732
4855
|
LedgerJournalEntryArtifactSchema,
|
|
4733
4856
|
ListPayoutDestinationsResultSchema,
|
|
4734
4857
|
MEMBERSHIP_ROLES,
|
|
@@ -4832,6 +4955,7 @@ export {
|
|
|
4832
4955
|
computeConsumerClaimEncounterId,
|
|
4833
4956
|
computeEncounterId,
|
|
4834
4957
|
constantTimeEqual,
|
|
4958
|
+
consumerOacSigningPayload,
|
|
4835
4959
|
consumerPaymentRequestSigningBytes,
|
|
4836
4960
|
consumerPaymentRequestSigningPayload,
|
|
4837
4961
|
consumerSettlementSigningPayload,
|
|
@@ -4864,11 +4988,13 @@ export {
|
|
|
4864
4988
|
decodeOfflineSmsSettleToken,
|
|
4865
4989
|
decodePayCardArtifact,
|
|
4866
4990
|
decodePaymentRequestQR,
|
|
4991
|
+
decodeUnverifiedConsumerOacQR,
|
|
4867
4992
|
decodeUnverifiedConsumerSettlementReceiptQR,
|
|
4868
4993
|
derToRawP256Signature,
|
|
4869
4994
|
encodeArtifactUri,
|
|
4870
4995
|
encodeAuthorizationQR,
|
|
4871
4996
|
encodeBase45,
|
|
4997
|
+
encodeConsumerOacQR,
|
|
4872
4998
|
encodeConsumerSettlementReceiptQR,
|
|
4873
4999
|
encodeNQR,
|
|
4874
5000
|
encodeOfflineClaimSmsMessage,
|
|
@@ -4881,6 +5007,7 @@ export {
|
|
|
4881
5007
|
generateStaticQR,
|
|
4882
5008
|
init,
|
|
4883
5009
|
inspectPayCardFreshness,
|
|
5010
|
+
isConsumerOacQR,
|
|
4884
5011
|
isConsumerPaymentRequestExpired,
|
|
4885
5012
|
isHardenedArtifactType,
|
|
4886
5013
|
isKnownArtifactType,
|
|
@@ -4911,6 +5038,7 @@ export {
|
|
|
4911
5038
|
verifyConsumerSettlement,
|
|
4912
5039
|
verifyConsumerSettlementReceiptQR,
|
|
4913
5040
|
verifyOAC,
|
|
5041
|
+
verifyOacOffline,
|
|
4914
5042
|
verifyOfflineSmsSettleToken,
|
|
4915
5043
|
verifyPass,
|
|
4916
5044
|
verifyPayCardArtifact,
|