@nokinc-flur/sdk 2.0.0 → 2.2.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 +864 -172
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +510 -11
- package/dist/index.d.ts +510 -11
- package/dist/index.js +814 -172
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2958,6 +2958,9 @@ function createAccountsClient(opts) {
|
|
|
2958
2958
|
import { z as z13 } from "zod";
|
|
2959
2959
|
var Sha256Hex = z13.string().regex(/^[0-9a-f]{64}$/);
|
|
2960
2960
|
var Base64Std3 = z13.string().regex(/^[A-Za-z0-9+/]+={0,2}$/);
|
|
2961
|
+
var ClaimNonce = z13.string().min(8).max(128).refine((value) => !value.includes("|"), {
|
|
2962
|
+
message: "nonce must not contain |"
|
|
2963
|
+
});
|
|
2961
2964
|
var ACCOUNT_FUNDED_OAC_MAX_TTL_MS = 1e3 * 60 * 60 * 24 * 7;
|
|
2962
2965
|
var CONSUMER_OFFLINE_CLAIM_SUBMIT_GRACE_MS = 1e3 * 60 * 60 * 24;
|
|
2963
2966
|
var AttestationSecurityLevelSchema = z13.enum([
|
|
@@ -3050,13 +3053,25 @@ var ConsumerPaymentClaimSchema = z13.object({
|
|
|
3050
3053
|
payerUserId: z13.string().uuid(),
|
|
3051
3054
|
payeeUserId: z13.string().uuid(),
|
|
3052
3055
|
payerDeviceId: z13.string().min(1).max(128),
|
|
3053
|
-
payerNonce:
|
|
3054
|
-
payeeNonce:
|
|
3056
|
+
payerNonce: ClaimNonce,
|
|
3057
|
+
payeeNonce: ClaimNonce,
|
|
3055
3058
|
amountKobo: z13.number().int().positive(),
|
|
3056
3059
|
currency: z13.string().length(3).default("NGN"),
|
|
3057
3060
|
occurredAtMs: z13.number().int().nonnegative(),
|
|
3058
3061
|
completedAtMs: z13.number().int().nonnegative().optional(),
|
|
3059
3062
|
contextId: z13.string().max(128).optional(),
|
|
3063
|
+
requestId: z13.string().uuid().optional(),
|
|
3064
|
+
requestMode: z13.enum(["fixed", "editable"]).optional(),
|
|
3065
|
+
requestTakerUserId: z13.string().uuid().optional(),
|
|
3066
|
+
requestAmountKobo: z13.number().int().positive().optional(),
|
|
3067
|
+
requestCurrency: z13.string().length(3).optional(),
|
|
3068
|
+
requestReference: z13.string().max(128).nullable().optional(),
|
|
3069
|
+
requestCreatedAtMs: z13.number().int().nonnegative().optional(),
|
|
3070
|
+
requestExpiresAtMs: z13.number().int().positive().optional(),
|
|
3071
|
+
requestNonce: z13.string().min(8).max(128).optional(),
|
|
3072
|
+
requestTakerDeviceId: z13.string().min(1).max(128).nullable().optional(),
|
|
3073
|
+
requestTakerPubkeySpkiB64: Base64Std3.min(64).max(4096).optional(),
|
|
3074
|
+
requestTakerSignatureDerB64: Base64Std3.min(16).max(2048).optional(),
|
|
3060
3075
|
payerPubkeySpkiB64: Base64Std3.min(64).max(4096),
|
|
3061
3076
|
payerSignatureDerB64: Base64Std3.min(16).max(2048),
|
|
3062
3077
|
payeePubkeySpkiB64: Base64Std3.min(64).max(4096).optional(),
|
|
@@ -3076,7 +3091,10 @@ var ConsumerSettlementSchema = z13.object({
|
|
|
3076
3091
|
ledgerRef: z13.string().nullable(),
|
|
3077
3092
|
/** ASN.1 DER ECDSA P-256 issuer signature, base64. */
|
|
3078
3093
|
issuerSig: Base64Std3.min(16).max(2048),
|
|
3079
|
-
|
|
3094
|
+
/** Canonical millisecond timestamp signed into the settlement receipt. */
|
|
3095
|
+
issuedAtMs: z13.number().int().nonnegative(),
|
|
3096
|
+
/** Compatibility alias for API consumers that predate issuedAtMs. */
|
|
3097
|
+
createdAtMs: z13.number().int().nonnegative().optional()
|
|
3080
3098
|
});
|
|
3081
3099
|
var ConsumerSettleResultSchema = z13.object({
|
|
3082
3100
|
settlement: ConsumerSettlementSchema,
|
|
@@ -3165,13 +3183,21 @@ function createMeOfflineClient(opts) {
|
|
|
3165
3183
|
"/v1/me/offline/claims",
|
|
3166
3184
|
ConsumerPaymentClaimSchema.parse(claim),
|
|
3167
3185
|
(raw) => ConsumerSettleResultSchema.parse(raw)
|
|
3186
|
+
),
|
|
3187
|
+
getSettlement: (idOrKey) => call(
|
|
3188
|
+
"GET",
|
|
3189
|
+
`/v1/me/offline/settlements/${encodeURIComponent(idOrKey)}`,
|
|
3190
|
+
void 0,
|
|
3191
|
+
(raw) => ConsumerSettlementSchema.parse(raw)
|
|
3168
3192
|
)
|
|
3169
3193
|
};
|
|
3170
3194
|
}
|
|
3171
3195
|
|
|
3172
3196
|
// src/me-offline/signer.ts
|
|
3173
3197
|
import { p256 as p2562 } from "@noble/curves/nist";
|
|
3198
|
+
import { sha256 as sha2564 } from "@noble/hashes/sha2";
|
|
3174
3199
|
var CLAIM_DOMAIN_V2 = "flur:consumer-offline:v2:claim";
|
|
3200
|
+
var ENCOUNTER_DOMAIN2 = "flur:consumer-offline:v1:encounter";
|
|
3175
3201
|
function canonicalClaimSigningPayload(claim) {
|
|
3176
3202
|
return {
|
|
3177
3203
|
domain: CLAIM_DOMAIN_V2,
|
|
@@ -3192,6 +3218,27 @@ function canonicalClaimSigningPayload(claim) {
|
|
|
3192
3218
|
function canonicalClaimSigningBytes(claim) {
|
|
3193
3219
|
return canonicalJSONBytes(canonicalClaimSigningPayload(claim));
|
|
3194
3220
|
}
|
|
3221
|
+
function computeConsumerClaimEncounterId(input) {
|
|
3222
|
+
const material = `${ENCOUNTER_DOMAIN2}|${[
|
|
3223
|
+
assertEncounterPart("oacId", input.oacId),
|
|
3224
|
+
assertEncounterPart("payerUserId", input.payerUserId),
|
|
3225
|
+
assertEncounterPart("payeeUserId", input.payeeUserId),
|
|
3226
|
+
assertEncounterPart("payerNonce", input.payerNonce),
|
|
3227
|
+
assertEncounterPart("payeeNonce", input.payeeNonce)
|
|
3228
|
+
].join("|")}`;
|
|
3229
|
+
return bytesToHex5(sha2564(new TextEncoder().encode(material)));
|
|
3230
|
+
}
|
|
3231
|
+
function assertEncounterPart(field, value) {
|
|
3232
|
+
if (value.includes("|")) {
|
|
3233
|
+
throw new Error(`consumer encounter id ${field} must not contain |`);
|
|
3234
|
+
}
|
|
3235
|
+
return value;
|
|
3236
|
+
}
|
|
3237
|
+
function bytesToHex5(bytes) {
|
|
3238
|
+
let out = "";
|
|
3239
|
+
for (const byte of bytes) out += byte.toString(16).padStart(2, "0");
|
|
3240
|
+
return out;
|
|
3241
|
+
}
|
|
3195
3242
|
function bytesToBase642(bytes) {
|
|
3196
3243
|
if (typeof Buffer !== "undefined") {
|
|
3197
3244
|
return Buffer.from(bytes).toString("base64");
|
|
@@ -3287,38 +3334,192 @@ function verifyClaimSignature(input) {
|
|
|
3287
3334
|
}
|
|
3288
3335
|
}
|
|
3289
3336
|
|
|
3290
|
-
// src/me-offline/
|
|
3291
|
-
|
|
3292
|
-
var
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3337
|
+
// src/me-offline/request.ts
|
|
3338
|
+
import { z as z14 } from "zod";
|
|
3339
|
+
var Base64Std4 = z14.string().regex(/^[A-Za-z0-9+/]+={0,2}$/);
|
|
3340
|
+
var CONSUMER_PAYMENT_REQUEST_DOMAIN = "flur:consumer-offline:v1:request";
|
|
3341
|
+
var ConsumerPaymentRequestEnvelopeSchema = z14.object({
|
|
3342
|
+
requestId: z14.string().uuid(),
|
|
3343
|
+
mode: z14.enum(["fixed", "editable"]),
|
|
3344
|
+
takerUserId: z14.string().uuid(),
|
|
3345
|
+
amountKobo: z14.number().int().positive(),
|
|
3346
|
+
currency: z14.string().length(3).default("NGN"),
|
|
3347
|
+
reference: z14.string().max(128).nullable().default(null),
|
|
3348
|
+
createdAtMs: z14.number().int().nonnegative(),
|
|
3349
|
+
expiresAtMs: z14.number().int().positive(),
|
|
3350
|
+
nonce: z14.string().min(8).max(128),
|
|
3351
|
+
takerDeviceId: z14.string().min(1).max(128).nullable().default(null),
|
|
3352
|
+
takerPubkeySpkiB64: Base64Std4.min(64).max(4096).optional(),
|
|
3353
|
+
takerSignatureDerB64: Base64Std4.min(16).max(2048).optional()
|
|
3354
|
+
}).superRefine((value, ctx) => {
|
|
3355
|
+
if (value.expiresAtMs <= value.createdAtMs) {
|
|
3356
|
+
ctx.addIssue({
|
|
3357
|
+
code: z14.ZodIssueCode.custom,
|
|
3358
|
+
path: ["expiresAtMs"],
|
|
3359
|
+
message: "expiresAtMs must be greater than createdAtMs"
|
|
3360
|
+
});
|
|
3361
|
+
}
|
|
3362
|
+
const hasSignature = Boolean(
|
|
3363
|
+
value.takerPubkeySpkiB64 || value.takerSignatureDerB64
|
|
3364
|
+
);
|
|
3365
|
+
if (value.mode === "fixed" || hasSignature) {
|
|
3366
|
+
if (!value.takerDeviceId) {
|
|
3367
|
+
ctx.addIssue({
|
|
3368
|
+
code: z14.ZodIssueCode.custom,
|
|
3369
|
+
path: ["takerDeviceId"],
|
|
3370
|
+
message: "signed requests require takerDeviceId"
|
|
3371
|
+
});
|
|
3372
|
+
}
|
|
3373
|
+
if (!value.takerPubkeySpkiB64) {
|
|
3374
|
+
ctx.addIssue({
|
|
3375
|
+
code: z14.ZodIssueCode.custom,
|
|
3376
|
+
path: ["takerPubkeySpkiB64"],
|
|
3377
|
+
message: "signed requests require takerPubkeySpkiB64"
|
|
3378
|
+
});
|
|
3379
|
+
}
|
|
3380
|
+
if (!value.takerSignatureDerB64) {
|
|
3381
|
+
ctx.addIssue({
|
|
3382
|
+
code: z14.ZodIssueCode.custom,
|
|
3383
|
+
path: ["takerSignatureDerB64"],
|
|
3384
|
+
message: "signed requests require takerSignatureDerB64"
|
|
3385
|
+
});
|
|
3386
|
+
}
|
|
3387
|
+
}
|
|
3388
|
+
});
|
|
3389
|
+
function buildConsumerPaymentRequest(input) {
|
|
3390
|
+
const unsigned = {
|
|
3391
|
+
requestId: input.requestId,
|
|
3392
|
+
mode: input.mode,
|
|
3393
|
+
takerUserId: input.takerUserId,
|
|
3394
|
+
amountKobo: input.amountKobo,
|
|
3395
|
+
currency: input.currency ?? "NGN",
|
|
3396
|
+
reference: input.reference ?? null,
|
|
3397
|
+
createdAtMs: input.createdAtMs,
|
|
3398
|
+
expiresAtMs: input.expiresAtMs,
|
|
3399
|
+
nonce: input.nonce,
|
|
3400
|
+
takerDeviceId: input.takerDeviceId ?? null
|
|
3401
|
+
};
|
|
3402
|
+
if (unsigned.mode === "fixed" && !unsigned.takerDeviceId) {
|
|
3403
|
+
throw new Error("fixed requests require takerDeviceId");
|
|
3404
|
+
}
|
|
3405
|
+
if (unsigned.expiresAtMs <= unsigned.createdAtMs) {
|
|
3406
|
+
throw new Error("expiresAtMs must be greater than createdAtMs");
|
|
3407
|
+
}
|
|
3408
|
+
return unsigned;
|
|
3297
3409
|
}
|
|
3298
|
-
function
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3410
|
+
function consumerPaymentRequestSigningPayload(request) {
|
|
3411
|
+
return {
|
|
3412
|
+
domain: CONSUMER_PAYMENT_REQUEST_DOMAIN,
|
|
3413
|
+
version: 1,
|
|
3414
|
+
requestId: request.requestId,
|
|
3415
|
+
mode: request.mode,
|
|
3416
|
+
takerUserId: request.takerUserId,
|
|
3417
|
+
amountKobo: request.amountKobo,
|
|
3418
|
+
currency: request.currency,
|
|
3419
|
+
reference: request.reference ?? null,
|
|
3420
|
+
createdAtMs: request.createdAtMs,
|
|
3421
|
+
expiresAtMs: request.expiresAtMs,
|
|
3422
|
+
nonce: request.nonce,
|
|
3423
|
+
takerDeviceId: request.takerDeviceId ?? null
|
|
3424
|
+
};
|
|
3425
|
+
}
|
|
3426
|
+
function consumerPaymentRequestSigningBytes(request) {
|
|
3427
|
+
return canonicalJSONBytes(consumerPaymentRequestSigningPayload(request));
|
|
3428
|
+
}
|
|
3429
|
+
async function signConsumerPaymentRequest(unsigned, signer) {
|
|
3430
|
+
if (signer.alg !== "p256") {
|
|
3431
|
+
throw new Error("consumer payment requests require p256 signer");
|
|
3302
3432
|
}
|
|
3303
|
-
const
|
|
3433
|
+
const publicKey = await signer.getPublicKey();
|
|
3434
|
+
if (publicKey.alg !== "p256") {
|
|
3435
|
+
throw new Error("consumer payment requests require p256 public key");
|
|
3436
|
+
}
|
|
3437
|
+
const signature = await signer.sign(
|
|
3438
|
+
consumerPaymentRequestSigningBytes(unsigned)
|
|
3439
|
+
);
|
|
3440
|
+
return ConsumerPaymentRequestEnvelopeSchema.parse({
|
|
3441
|
+
...unsigned,
|
|
3442
|
+
takerPubkeySpkiB64: publicKey.publicKey,
|
|
3443
|
+
takerSignatureDerB64: signature.signature
|
|
3444
|
+
});
|
|
3445
|
+
}
|
|
3446
|
+
function verifyConsumerPaymentRequest(request) {
|
|
3447
|
+
const parsed = ConsumerPaymentRequestEnvelopeSchema.safeParse(request);
|
|
3448
|
+
if (!parsed.success) return false;
|
|
3449
|
+
const value = parsed.data;
|
|
3450
|
+
if (!value.takerPubkeySpkiB64 || !value.takerSignatureDerB64) return false;
|
|
3451
|
+
return verifyClaimSignature({
|
|
3452
|
+
alg: "p256",
|
|
3453
|
+
bytes: consumerPaymentRequestSigningBytes(value),
|
|
3454
|
+
signature: value.takerSignatureDerB64,
|
|
3455
|
+
publicKey: value.takerPubkeySpkiB64
|
|
3456
|
+
});
|
|
3457
|
+
}
|
|
3458
|
+
function isConsumerPaymentRequestExpired(request, nowMs = Date.now()) {
|
|
3459
|
+
const parsed = ConsumerPaymentRequestEnvelopeSchema.parse(request);
|
|
3460
|
+
return parsed.expiresAtMs <= nowMs;
|
|
3461
|
+
}
|
|
3462
|
+
|
|
3463
|
+
// src/me-offline/settlement.ts
|
|
3464
|
+
var CONSUMER_SETTLEMENT_DOMAIN = "flur:consumer-offline:v1:settlement";
|
|
3465
|
+
var CONSUMER_SETTLEMENT_RECEIPT_QR_PREFIX = "FLURSR1.";
|
|
3466
|
+
function consumerSettlementSigningPayload(settlement) {
|
|
3467
|
+
return {
|
|
3468
|
+
domain: CONSUMER_SETTLEMENT_DOMAIN,
|
|
3469
|
+
settlementId: settlement.settlementId,
|
|
3470
|
+
settlementKey: settlement.settlementKey,
|
|
3471
|
+
encounterId: settlement.encounterId,
|
|
3472
|
+
oacId: settlement.oacId,
|
|
3473
|
+
payerUserId: settlement.payerUserId,
|
|
3474
|
+
payeeUserId: settlement.payeeUserId,
|
|
3475
|
+
amountKobo: settlement.amountKobo,
|
|
3476
|
+
currency: settlement.currency,
|
|
3477
|
+
status: settlement.status,
|
|
3478
|
+
reviewReason: settlement.reviewReason,
|
|
3479
|
+
ledgerRef: settlement.ledgerRef,
|
|
3480
|
+
issuedAtMs: settlement.issuedAtMs
|
|
3481
|
+
};
|
|
3482
|
+
}
|
|
3483
|
+
function verifyConsumerSettlement(settlement, issuerPublicKeySpkiB64) {
|
|
3484
|
+
const parsed = ConsumerSettlementSchema.safeParse(settlement);
|
|
3485
|
+
if (!parsed.success) return false;
|
|
3486
|
+
return verifyIssuerP256(
|
|
3487
|
+
canonicalJSONBytes(consumerSettlementSigningPayload(parsed.data)),
|
|
3488
|
+
parsed.data.issuerSig,
|
|
3489
|
+
issuerPublicKeySpkiB64
|
|
3490
|
+
);
|
|
3491
|
+
}
|
|
3492
|
+
function encodeConsumerSettlementReceiptQR(settlement) {
|
|
3493
|
+
const parsed = ConsumerSettlementSchema.parse(settlement);
|
|
3494
|
+
return `${CONSUMER_SETTLEMENT_RECEIPT_QR_PREFIX}${base64UrlEncodeUtf8(
|
|
3495
|
+
JSON.stringify(parsed)
|
|
3496
|
+
)}`;
|
|
3497
|
+
}
|
|
3498
|
+
function decodeUnverifiedConsumerSettlementReceiptQR(value) {
|
|
3499
|
+
if (!value.startsWith(CONSUMER_SETTLEMENT_RECEIPT_QR_PREFIX)) {
|
|
3500
|
+
throw new Error("not a Flur consumer settlement receipt QR");
|
|
3501
|
+
}
|
|
3502
|
+
const encoded = value.slice(CONSUMER_SETTLEMENT_RECEIPT_QR_PREFIX.length);
|
|
3304
3503
|
let raw;
|
|
3305
3504
|
try {
|
|
3306
3505
|
raw = JSON.parse(base64UrlDecodeUtf8(encoded));
|
|
3307
3506
|
} catch {
|
|
3308
|
-
throw new Error("
|
|
3507
|
+
throw new Error("consumer settlement receipt QR is malformed");
|
|
3309
3508
|
}
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3509
|
+
return ConsumerSettlementSchema.parse(raw);
|
|
3510
|
+
}
|
|
3511
|
+
function verifyConsumerSettlementReceiptQR(value, issuerPublicKeySpkiB64) {
|
|
3512
|
+
const settlement = decodeUnverifiedConsumerSettlementReceiptQR(value);
|
|
3513
|
+
if (!verifyConsumerSettlement(settlement, issuerPublicKeySpkiB64)) {
|
|
3514
|
+
throw new Error("consumer settlement receipt QR signature invalid");
|
|
3313
3515
|
}
|
|
3314
|
-
return
|
|
3516
|
+
return settlement;
|
|
3315
3517
|
}
|
|
3316
|
-
function
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
return trimmed.split(/\s+/, 1)[0] ?? null;
|
|
3518
|
+
function decodeConsumerSettlementReceiptQR(value, issuerPublicKeySpkiB64) {
|
|
3519
|
+
if (!issuerPublicKeySpkiB64) {
|
|
3520
|
+
return decodeUnverifiedConsumerSettlementReceiptQR(value);
|
|
3320
3521
|
}
|
|
3321
|
-
return
|
|
3522
|
+
return verifyConsumerSettlementReceiptQR(value, issuerPublicKeySpkiB64);
|
|
3322
3523
|
}
|
|
3323
3524
|
function base64UrlEncodeUtf8(input) {
|
|
3324
3525
|
const bytes = new TextEncoder().encode(input);
|
|
@@ -3348,15 +3549,281 @@ function base64UrlDecodeUtf8(input) {
|
|
|
3348
3549
|
throw new Error("base64 decoder unavailable");
|
|
3349
3550
|
}
|
|
3350
3551
|
|
|
3552
|
+
// src/me-offline/sms.ts
|
|
3553
|
+
import { p256 as p2563 } from "@noble/curves/nist";
|
|
3554
|
+
var OFFLINE_CLAIM_SMS_PREFIX = "FLURC1.";
|
|
3555
|
+
var CLAIM_TOKEN_RE = /(?:^|\s)(FLURC1\.[A-Za-z0-9_-]+={0,2})(?:\s|$)/;
|
|
3556
|
+
function encodeOfflineClaimSmsMessage(claim) {
|
|
3557
|
+
const parsed = ConsumerPaymentClaimSchema.parse(claim);
|
|
3558
|
+
return `${OFFLINE_CLAIM_SMS_PREFIX}${base64UrlEncodeUtf82(
|
|
3559
|
+
JSON.stringify(parsed)
|
|
3560
|
+
)}`;
|
|
3561
|
+
}
|
|
3562
|
+
function decodeOfflineClaimSmsMessage(message) {
|
|
3563
|
+
const token = extractOfflineClaimSmsToken(message);
|
|
3564
|
+
if (!token) throw new Error("offline claim QR token not found");
|
|
3565
|
+
const encoded = token.slice(OFFLINE_CLAIM_SMS_PREFIX.length);
|
|
3566
|
+
let raw;
|
|
3567
|
+
try {
|
|
3568
|
+
raw = JSON.parse(base64UrlDecodeUtf82(encoded));
|
|
3569
|
+
} catch {
|
|
3570
|
+
throw new Error("offline claim QR token is malformed");
|
|
3571
|
+
}
|
|
3572
|
+
const parsed = ConsumerPaymentClaimSchema.safeParse(raw);
|
|
3573
|
+
if (!parsed.success) throw new Error("offline claim QR token is invalid");
|
|
3574
|
+
return parsed.data;
|
|
3575
|
+
}
|
|
3576
|
+
function extractOfflineClaimSmsToken(message) {
|
|
3577
|
+
const trimmed = message.trim();
|
|
3578
|
+
if (trimmed.startsWith(OFFLINE_CLAIM_SMS_PREFIX)) {
|
|
3579
|
+
return trimmed.split(/\s+/, 1)[0] ?? null;
|
|
3580
|
+
}
|
|
3581
|
+
return CLAIM_TOKEN_RE.exec(message)?.[1] ?? null;
|
|
3582
|
+
}
|
|
3583
|
+
var OFFLINE_SMS_SETTLE_PREFIX = "FLURA1.";
|
|
3584
|
+
var OFFLINE_SMS_SETTLE_DOMAIN = "flur:consumer-offline:v1:attest";
|
|
3585
|
+
var OFFLINE_SMS_SETTLE_TOKEN_BYTES = 112;
|
|
3586
|
+
var OFFLINE_SMS_SETTLE_HEADER_BYTES = 48;
|
|
3587
|
+
var OFFLINE_SMS_SETTLE_SIGNATURE_BYTES = 64;
|
|
3588
|
+
var OFFLINE_SMS_SETTLE_VERSION = 1;
|
|
3589
|
+
var TOKEN_RE = /(?:^|[\s,;:()<>"'])(FLURA1\.[A-Za-z0-9_-]{150})/;
|
|
3590
|
+
async function encodeOfflineSmsSettleToken(input, signer) {
|
|
3591
|
+
const header = await buildSmsSettleHeader(input);
|
|
3592
|
+
const sig = await signer.signRaw(domainTag(header));
|
|
3593
|
+
if (sig.length !== OFFLINE_SMS_SETTLE_SIGNATURE_BYTES) {
|
|
3594
|
+
throw new Error(
|
|
3595
|
+
`FLURA1: signer returned ${sig.length}-byte sig; expected ${OFFLINE_SMS_SETTLE_SIGNATURE_BYTES}`
|
|
3596
|
+
);
|
|
3597
|
+
}
|
|
3598
|
+
const out = new Uint8Array(OFFLINE_SMS_SETTLE_TOKEN_BYTES);
|
|
3599
|
+
out.set(header, 0);
|
|
3600
|
+
out.set(sig, OFFLINE_SMS_SETTLE_HEADER_BYTES);
|
|
3601
|
+
return `${OFFLINE_SMS_SETTLE_PREFIX}${bytesToBase64Url(out)}`;
|
|
3602
|
+
}
|
|
3603
|
+
async function buildSmsSettleHeader(input) {
|
|
3604
|
+
assertSafeUint64(input.amountKobo, "amountKobo");
|
|
3605
|
+
if (input.amountKobo <= 0) {
|
|
3606
|
+
throw new Error("FLURA1: amountKobo must be greater than zero");
|
|
3607
|
+
}
|
|
3608
|
+
assertSafeUint48(input.occurredAtMs, "occurredAtMs");
|
|
3609
|
+
const encounterPrefix = (await sha2565(utf8(input.encounterId))).slice(0, 16);
|
|
3610
|
+
const payerPrefix = uuidToBytes(input.payerUserId).slice(0, 8);
|
|
3611
|
+
const payeePrefix = uuidToBytes(input.payeeUserId).slice(0, 8);
|
|
3612
|
+
const header = new Uint8Array(OFFLINE_SMS_SETTLE_HEADER_BYTES);
|
|
3613
|
+
const dv = new DataView(header.buffer);
|
|
3614
|
+
header[0] = OFFLINE_SMS_SETTLE_VERSION;
|
|
3615
|
+
header[1] = 0;
|
|
3616
|
+
header.set(encounterPrefix, 2);
|
|
3617
|
+
header.set(payerPrefix, 18);
|
|
3618
|
+
header.set(payeePrefix, 26);
|
|
3619
|
+
writeUint64BE(dv, 34, input.amountKobo);
|
|
3620
|
+
writeUint48BE(dv, 42, input.occurredAtMs);
|
|
3621
|
+
return header;
|
|
3622
|
+
}
|
|
3623
|
+
function domainTag(header) {
|
|
3624
|
+
if (header.length !== OFFLINE_SMS_SETTLE_HEADER_BYTES) {
|
|
3625
|
+
throw new Error(
|
|
3626
|
+
`FLURA1: header must be ${OFFLINE_SMS_SETTLE_HEADER_BYTES} bytes`
|
|
3627
|
+
);
|
|
3628
|
+
}
|
|
3629
|
+
const domain = utf8(OFFLINE_SMS_SETTLE_DOMAIN);
|
|
3630
|
+
const out = new Uint8Array(domain.length + header.length);
|
|
3631
|
+
out.set(domain, 0);
|
|
3632
|
+
out.set(header, domain.length);
|
|
3633
|
+
return out;
|
|
3634
|
+
}
|
|
3635
|
+
function decodeOfflineSmsSettleToken(message) {
|
|
3636
|
+
const token = extractOfflineSmsSettleToken(message);
|
|
3637
|
+
if (!token) throw new Error("FLURA1: token not found");
|
|
3638
|
+
const encoded = token.slice(OFFLINE_SMS_SETTLE_PREFIX.length);
|
|
3639
|
+
let bytes;
|
|
3640
|
+
try {
|
|
3641
|
+
bytes = base64UrlToBytes(encoded);
|
|
3642
|
+
} catch {
|
|
3643
|
+
throw new Error("FLURA1: token base64url is malformed");
|
|
3644
|
+
}
|
|
3645
|
+
if (bytesToBase64Url(bytes) !== encoded) {
|
|
3646
|
+
throw new Error("FLURA1: token base64url is malformed");
|
|
3647
|
+
}
|
|
3648
|
+
if (bytes.length !== OFFLINE_SMS_SETTLE_TOKEN_BYTES) {
|
|
3649
|
+
throw new Error(
|
|
3650
|
+
`FLURA1: expected ${OFFLINE_SMS_SETTLE_TOKEN_BYTES} bytes, got ${bytes.length}`
|
|
3651
|
+
);
|
|
3652
|
+
}
|
|
3653
|
+
const version = bytes[0];
|
|
3654
|
+
const flags = bytes[1];
|
|
3655
|
+
if (version !== OFFLINE_SMS_SETTLE_VERSION) {
|
|
3656
|
+
throw new Error(`FLURA1: unsupported version ${version}`);
|
|
3657
|
+
}
|
|
3658
|
+
if (flags !== 0) {
|
|
3659
|
+
throw new Error(`FLURA1: reserved flags must be 0, got ${flags}`);
|
|
3660
|
+
}
|
|
3661
|
+
const header = bytes.slice(0, OFFLINE_SMS_SETTLE_HEADER_BYTES);
|
|
3662
|
+
const signature = bytes.slice(OFFLINE_SMS_SETTLE_HEADER_BYTES);
|
|
3663
|
+
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
3664
|
+
const amountKobo = readUint64BE(dv, 34);
|
|
3665
|
+
if (amountKobo <= 0) {
|
|
3666
|
+
throw new Error("FLURA1: amountKobo must be greater than zero");
|
|
3667
|
+
}
|
|
3668
|
+
return {
|
|
3669
|
+
version,
|
|
3670
|
+
flags,
|
|
3671
|
+
encounterIdPrefixHex: bytesToHex6(bytes.slice(2, 18)),
|
|
3672
|
+
payerUserIdPrefixHex: bytesToHex6(bytes.slice(18, 26)),
|
|
3673
|
+
payeeUserIdPrefixHex: bytesToHex6(bytes.slice(26, 34)),
|
|
3674
|
+
amountKobo,
|
|
3675
|
+
occurredAtMs: readUint48BE(dv, 42),
|
|
3676
|
+
signature,
|
|
3677
|
+
header,
|
|
3678
|
+
signedBytes: domainTag(header)
|
|
3679
|
+
};
|
|
3680
|
+
}
|
|
3681
|
+
function extractOfflineSmsSettleToken(message) {
|
|
3682
|
+
const trimmed = message.trim();
|
|
3683
|
+
if (trimmed.startsWith(OFFLINE_SMS_SETTLE_PREFIX)) {
|
|
3684
|
+
return trimmed.split(/\s+/, 1)[0] ?? null;
|
|
3685
|
+
}
|
|
3686
|
+
return TOKEN_RE.exec(message)?.[1] ?? null;
|
|
3687
|
+
}
|
|
3688
|
+
function verifyOfflineSmsSettleToken(decoded, payerPubkeySpkiB64) {
|
|
3689
|
+
try {
|
|
3690
|
+
const pubRaw = p256SpkiB64ToRaw(payerPubkeySpkiB64);
|
|
3691
|
+
return p2563.verify(decoded.signature, decoded.signedBytes, pubRaw, {
|
|
3692
|
+
prehash: true,
|
|
3693
|
+
format: "compact"
|
|
3694
|
+
});
|
|
3695
|
+
} catch {
|
|
3696
|
+
return false;
|
|
3697
|
+
}
|
|
3698
|
+
}
|
|
3699
|
+
function derToRawP256Signature(derBytes) {
|
|
3700
|
+
const sig = p2563.Signature.fromBytes(derBytes, "der");
|
|
3701
|
+
const raw = sig.toBytes("compact");
|
|
3702
|
+
if (raw.length !== OFFLINE_SMS_SETTLE_SIGNATURE_BYTES) {
|
|
3703
|
+
throw new Error(
|
|
3704
|
+
`FLURA1: DER\u2192raw produced ${raw.length} bytes; expected ${OFFLINE_SMS_SETTLE_SIGNATURE_BYTES}`
|
|
3705
|
+
);
|
|
3706
|
+
}
|
|
3707
|
+
return raw;
|
|
3708
|
+
}
|
|
3709
|
+
function utf8(s) {
|
|
3710
|
+
return new TextEncoder().encode(s);
|
|
3711
|
+
}
|
|
3712
|
+
async function sha2565(bytes) {
|
|
3713
|
+
const subtle = typeof globalThis !== "undefined" && globalThis.crypto?.subtle || void 0;
|
|
3714
|
+
if (subtle) {
|
|
3715
|
+
const digest = await subtle.digest("SHA-256", bytes);
|
|
3716
|
+
return new Uint8Array(digest);
|
|
3717
|
+
}
|
|
3718
|
+
const { sha256: nobleSha256 } = await import("@noble/hashes/sha2");
|
|
3719
|
+
return nobleSha256(bytes);
|
|
3720
|
+
}
|
|
3721
|
+
function uuidToBytes(uuid) {
|
|
3722
|
+
const hex = uuid.replace(/-/g, "").toLowerCase();
|
|
3723
|
+
if (hex.length !== 32 || !/^[0-9a-f]{32}$/.test(hex)) {
|
|
3724
|
+
throw new Error(`FLURA1: invalid UUID: ${uuid}`);
|
|
3725
|
+
}
|
|
3726
|
+
const out = new Uint8Array(16);
|
|
3727
|
+
for (let i = 0; i < 16; i++) {
|
|
3728
|
+
out[i] = parseInt(hex.substring(i * 2, i * 2 + 2), 16);
|
|
3729
|
+
}
|
|
3730
|
+
return out;
|
|
3731
|
+
}
|
|
3732
|
+
function assertSafeUint64(value, field) {
|
|
3733
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
3734
|
+
throw new Error(`FLURA1: ${field} must be a non-negative integer`);
|
|
3735
|
+
}
|
|
3736
|
+
if (value > Number.MAX_SAFE_INTEGER) {
|
|
3737
|
+
throw new Error(`FLURA1: ${field} exceeds Number.MAX_SAFE_INTEGER`);
|
|
3738
|
+
}
|
|
3739
|
+
}
|
|
3740
|
+
function assertSafeUint48(value, field) {
|
|
3741
|
+
assertSafeUint64(value, field);
|
|
3742
|
+
if (value > 281474976710655) {
|
|
3743
|
+
throw new Error(`FLURA1: ${field} exceeds uint48 range`);
|
|
3744
|
+
}
|
|
3745
|
+
}
|
|
3746
|
+
function writeUint64BE(dv, offset, value) {
|
|
3747
|
+
const high = Math.floor(value / 4294967296);
|
|
3748
|
+
const low = value >>> 0;
|
|
3749
|
+
dv.setUint32(offset, high, false);
|
|
3750
|
+
dv.setUint32(offset + 4, low, false);
|
|
3751
|
+
}
|
|
3752
|
+
function readUint64BE(dv, offset) {
|
|
3753
|
+
const high = dv.getUint32(offset, false);
|
|
3754
|
+
const low = dv.getUint32(offset + 4, false);
|
|
3755
|
+
if (high > 2097151) {
|
|
3756
|
+
throw new Error("FLURA1: amountKobo exceeds Number.MAX_SAFE_INTEGER");
|
|
3757
|
+
}
|
|
3758
|
+
return high * 4294967296 + low;
|
|
3759
|
+
}
|
|
3760
|
+
function writeUint48BE(dv, offset, value) {
|
|
3761
|
+
const high = Math.floor(value / 65536);
|
|
3762
|
+
const low = value & 65535;
|
|
3763
|
+
dv.setUint32(offset, high, false);
|
|
3764
|
+
dv.setUint16(offset + 4, low, false);
|
|
3765
|
+
}
|
|
3766
|
+
function readUint48BE(dv, offset) {
|
|
3767
|
+
const high = dv.getUint32(offset, false);
|
|
3768
|
+
const low = dv.getUint16(offset + 4, false);
|
|
3769
|
+
return high * 65536 + low;
|
|
3770
|
+
}
|
|
3771
|
+
function bytesToHex6(bytes) {
|
|
3772
|
+
let out = "";
|
|
3773
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
3774
|
+
out += bytes[i].toString(16).padStart(2, "0");
|
|
3775
|
+
}
|
|
3776
|
+
return out;
|
|
3777
|
+
}
|
|
3778
|
+
function bytesToBase64Url(bytes) {
|
|
3779
|
+
let base64;
|
|
3780
|
+
if (typeof Buffer !== "undefined") {
|
|
3781
|
+
base64 = Buffer.from(bytes).toString("base64");
|
|
3782
|
+
} else {
|
|
3783
|
+
let binary = "";
|
|
3784
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
3785
|
+
binary += String.fromCharCode(bytes[i]);
|
|
3786
|
+
}
|
|
3787
|
+
if (typeof btoa !== "function") {
|
|
3788
|
+
throw new Error("FLURA1: base64 encoder unavailable");
|
|
3789
|
+
}
|
|
3790
|
+
base64 = btoa(binary);
|
|
3791
|
+
}
|
|
3792
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
3793
|
+
}
|
|
3794
|
+
function base64UrlEncodeUtf82(input) {
|
|
3795
|
+
return bytesToBase64Url(utf8(input));
|
|
3796
|
+
}
|
|
3797
|
+
function base64UrlDecodeUtf82(input) {
|
|
3798
|
+
return new TextDecoder().decode(base64UrlToBytes(input));
|
|
3799
|
+
}
|
|
3800
|
+
function base64UrlToBytes(input) {
|
|
3801
|
+
const base64 = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
3802
|
+
const padded = base64.padEnd(
|
|
3803
|
+
base64.length + (4 - base64.length % 4) % 4,
|
|
3804
|
+
"="
|
|
3805
|
+
);
|
|
3806
|
+
if (typeof Buffer !== "undefined") {
|
|
3807
|
+
return new Uint8Array(Buffer.from(padded, "base64"));
|
|
3808
|
+
}
|
|
3809
|
+
if (typeof atob !== "function") {
|
|
3810
|
+
throw new Error("FLURA1: base64 decoder unavailable");
|
|
3811
|
+
}
|
|
3812
|
+
const binary = atob(padded);
|
|
3813
|
+
const out = new Uint8Array(binary.length);
|
|
3814
|
+
for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
|
|
3815
|
+
return out;
|
|
3816
|
+
}
|
|
3817
|
+
|
|
3351
3818
|
// src/partner-funding/client.ts
|
|
3352
|
-
import { z as
|
|
3353
|
-
var MinorString =
|
|
3354
|
-
var PositiveMinor =
|
|
3355
|
-
|
|
3356
|
-
|
|
3819
|
+
import { z as z15 } from "zod";
|
|
3820
|
+
var MinorString = z15.string().regex(/^-?\d+$/);
|
|
3821
|
+
var PositiveMinor = z15.union([
|
|
3822
|
+
z15.number().int().positive(),
|
|
3823
|
+
z15.string().regex(/^[1-9]\d{0,18}$/)
|
|
3357
3824
|
]);
|
|
3358
|
-
var Currency =
|
|
3359
|
-
var Metadata =
|
|
3825
|
+
var Currency = z15.string().trim().length(3).transform((v) => v.toUpperCase());
|
|
3826
|
+
var Metadata = z15.record(z15.unknown());
|
|
3360
3827
|
var PARTNER_KINDS = ["bank", "merchant"];
|
|
3361
3828
|
var CUSTODIAL_MODES = ["agent_of_bank", "flur_virtual_pool"];
|
|
3362
3829
|
var PARTNER_PROFILE_STATUSES = [
|
|
@@ -3383,126 +3850,126 @@ var WITHDRAWAL_STATES = [
|
|
|
3383
3850
|
"failed",
|
|
3384
3851
|
"reversed"
|
|
3385
3852
|
];
|
|
3386
|
-
var PartnerProfileSchema =
|
|
3387
|
-
partnerAccountId:
|
|
3388
|
-
kind:
|
|
3389
|
-
custodialMode:
|
|
3390
|
-
displayName:
|
|
3391
|
-
bankCode:
|
|
3392
|
-
poolAccountNumber:
|
|
3393
|
-
status:
|
|
3853
|
+
var PartnerProfileSchema = z15.object({
|
|
3854
|
+
partnerAccountId: z15.string().uuid(),
|
|
3855
|
+
kind: z15.enum(PARTNER_KINDS),
|
|
3856
|
+
custodialMode: z15.enum(CUSTODIAL_MODES),
|
|
3857
|
+
displayName: z15.string(),
|
|
3858
|
+
bankCode: z15.string().nullable(),
|
|
3859
|
+
poolAccountNumber: z15.string().nullable(),
|
|
3860
|
+
status: z15.enum(PARTNER_PROFILE_STATUSES),
|
|
3394
3861
|
metadata: Metadata,
|
|
3395
|
-
createdAtMs:
|
|
3396
|
-
updatedAtMs:
|
|
3862
|
+
createdAtMs: z15.number().int().nonnegative(),
|
|
3863
|
+
updatedAtMs: z15.number().int().nonnegative()
|
|
3397
3864
|
});
|
|
3398
|
-
var UpsertPartnerProfileInputSchema =
|
|
3399
|
-
kind:
|
|
3400
|
-
custodialMode:
|
|
3401
|
-
displayName:
|
|
3402
|
-
bankCode:
|
|
3403
|
-
poolAccountNumber:
|
|
3865
|
+
var UpsertPartnerProfileInputSchema = z15.object({
|
|
3866
|
+
kind: z15.enum(PARTNER_KINDS),
|
|
3867
|
+
custodialMode: z15.enum(CUSTODIAL_MODES),
|
|
3868
|
+
displayName: z15.string().trim().min(1).max(200),
|
|
3869
|
+
bankCode: z15.string().trim().min(1).max(64).optional(),
|
|
3870
|
+
poolAccountNumber: z15.string().trim().min(1).max(64).optional(),
|
|
3404
3871
|
metadata: Metadata.optional()
|
|
3405
3872
|
});
|
|
3406
|
-
var PartnerFundingEventInputSchema =
|
|
3407
|
-
externalRef:
|
|
3408
|
-
direction:
|
|
3409
|
-
userId:
|
|
3410
|
-
accountId:
|
|
3873
|
+
var PartnerFundingEventInputSchema = z15.object({
|
|
3874
|
+
externalRef: z15.string().trim().min(8).max(128),
|
|
3875
|
+
direction: z15.enum(PARTNER_FUNDING_DIRECTIONS).optional(),
|
|
3876
|
+
userId: z15.string().uuid().optional(),
|
|
3877
|
+
accountId: z15.string().uuid().optional(),
|
|
3411
3878
|
amountMinor: PositiveMinor,
|
|
3412
3879
|
currency: Currency,
|
|
3413
|
-
fundingSource:
|
|
3880
|
+
fundingSource: z15.string().trim().min(1).max(64).optional(),
|
|
3414
3881
|
providerMetadata: Metadata.optional()
|
|
3415
3882
|
});
|
|
3416
|
-
var PartnerFundingSchema =
|
|
3417
|
-
fundingId:
|
|
3418
|
-
partnerId:
|
|
3419
|
-
accountId:
|
|
3420
|
-
userId:
|
|
3421
|
-
direction:
|
|
3422
|
-
currency:
|
|
3883
|
+
var PartnerFundingSchema = z15.object({
|
|
3884
|
+
fundingId: z15.string().uuid(),
|
|
3885
|
+
partnerId: z15.string().uuid(),
|
|
3886
|
+
accountId: z15.string().uuid(),
|
|
3887
|
+
userId: z15.string().uuid().nullable(),
|
|
3888
|
+
direction: z15.enum(PARTNER_FUNDING_DIRECTIONS),
|
|
3889
|
+
currency: z15.string(),
|
|
3423
3890
|
amountMinor: MinorString,
|
|
3424
|
-
externalRef:
|
|
3425
|
-
status:
|
|
3426
|
-
fundingSource:
|
|
3427
|
-
ledgerRef:
|
|
3891
|
+
externalRef: z15.string(),
|
|
3892
|
+
status: z15.enum(PARTNER_FUNDING_STATUSES),
|
|
3893
|
+
fundingSource: z15.string(),
|
|
3894
|
+
ledgerRef: z15.string(),
|
|
3428
3895
|
providerMetadata: Metadata,
|
|
3429
|
-
createdAtMs:
|
|
3430
|
-
updatedAtMs:
|
|
3896
|
+
createdAtMs: z15.number().int().nonnegative(),
|
|
3897
|
+
updatedAtMs: z15.number().int().nonnegative()
|
|
3431
3898
|
});
|
|
3432
|
-
var IngestFundingResultSchema =
|
|
3899
|
+
var IngestFundingResultSchema = z15.object({
|
|
3433
3900
|
funding: PartnerFundingSchema,
|
|
3434
|
-
replayed:
|
|
3901
|
+
replayed: z15.boolean()
|
|
3435
3902
|
});
|
|
3436
|
-
var PayoutDestinationSchema =
|
|
3437
|
-
destinationId:
|
|
3438
|
-
accountId:
|
|
3439
|
-
partnerId:
|
|
3440
|
-
bankCode:
|
|
3441
|
-
accountNumber:
|
|
3442
|
-
accountName:
|
|
3443
|
-
status:
|
|
3444
|
-
verifiedAtMs:
|
|
3903
|
+
var PayoutDestinationSchema = z15.object({
|
|
3904
|
+
destinationId: z15.string().uuid(),
|
|
3905
|
+
accountId: z15.string().uuid(),
|
|
3906
|
+
partnerId: z15.string().uuid(),
|
|
3907
|
+
bankCode: z15.string(),
|
|
3908
|
+
accountNumber: z15.string(),
|
|
3909
|
+
accountName: z15.string(),
|
|
3910
|
+
status: z15.enum(PAYOUT_DESTINATION_STATUSES),
|
|
3911
|
+
verifiedAtMs: z15.number().int().nonnegative().nullable(),
|
|
3445
3912
|
metadata: Metadata,
|
|
3446
|
-
createdAtMs:
|
|
3447
|
-
updatedAtMs:
|
|
3913
|
+
createdAtMs: z15.number().int().nonnegative(),
|
|
3914
|
+
updatedAtMs: z15.number().int().nonnegative()
|
|
3448
3915
|
});
|
|
3449
|
-
var CreatePayoutDestinationInputSchema =
|
|
3450
|
-
partnerId:
|
|
3451
|
-
bankCode:
|
|
3452
|
-
accountNumber:
|
|
3453
|
-
accountName:
|
|
3916
|
+
var CreatePayoutDestinationInputSchema = z15.object({
|
|
3917
|
+
partnerId: z15.string().uuid(),
|
|
3918
|
+
bankCode: z15.string().trim().min(1).max(32),
|
|
3919
|
+
accountNumber: z15.string().trim().min(4).max(64),
|
|
3920
|
+
accountName: z15.string().trim().min(1).max(200),
|
|
3454
3921
|
metadata: Metadata.optional()
|
|
3455
3922
|
});
|
|
3456
|
-
var ListPayoutDestinationsResultSchema =
|
|
3457
|
-
items:
|
|
3923
|
+
var ListPayoutDestinationsResultSchema = z15.object({
|
|
3924
|
+
items: z15.array(PayoutDestinationSchema)
|
|
3458
3925
|
});
|
|
3459
|
-
var WithdrawalSchema =
|
|
3460
|
-
withdrawalId:
|
|
3461
|
-
accountId:
|
|
3462
|
-
userId:
|
|
3463
|
-
partnerId:
|
|
3464
|
-
destinationId:
|
|
3465
|
-
currency:
|
|
3926
|
+
var WithdrawalSchema = z15.object({
|
|
3927
|
+
withdrawalId: z15.string().uuid(),
|
|
3928
|
+
accountId: z15.string().uuid(),
|
|
3929
|
+
userId: z15.string().uuid(),
|
|
3930
|
+
partnerId: z15.string().uuid(),
|
|
3931
|
+
destinationId: z15.string().uuid(),
|
|
3932
|
+
currency: z15.string(),
|
|
3466
3933
|
amountMinor: MinorString,
|
|
3467
|
-
state:
|
|
3468
|
-
idempotencyKey:
|
|
3469
|
-
providerRef:
|
|
3470
|
-
lastError:
|
|
3471
|
-
ledgerRef:
|
|
3472
|
-
reverseLedgerRef:
|
|
3934
|
+
state: z15.enum(WITHDRAWAL_STATES),
|
|
3935
|
+
idempotencyKey: z15.string(),
|
|
3936
|
+
providerRef: z15.string().nullable(),
|
|
3937
|
+
lastError: z15.string().nullable(),
|
|
3938
|
+
ledgerRef: z15.string(),
|
|
3939
|
+
reverseLedgerRef: z15.string().nullable(),
|
|
3473
3940
|
metadata: Metadata,
|
|
3474
|
-
createdAtMs:
|
|
3475
|
-
updatedAtMs:
|
|
3941
|
+
createdAtMs: z15.number().int().nonnegative(),
|
|
3942
|
+
updatedAtMs: z15.number().int().nonnegative()
|
|
3476
3943
|
});
|
|
3477
|
-
var CreateWithdrawalInputSchema =
|
|
3478
|
-
destinationId:
|
|
3944
|
+
var CreateWithdrawalInputSchema = z15.object({
|
|
3945
|
+
destinationId: z15.string().uuid(),
|
|
3479
3946
|
amountMinor: PositiveMinor,
|
|
3480
3947
|
currency: Currency,
|
|
3481
|
-
idempotencyKey:
|
|
3948
|
+
idempotencyKey: z15.string().trim().min(8).max(128),
|
|
3482
3949
|
metadata: Metadata.optional()
|
|
3483
3950
|
});
|
|
3484
|
-
var CreateWithdrawalResultSchema =
|
|
3951
|
+
var CreateWithdrawalResultSchema = z15.object({
|
|
3485
3952
|
withdrawal: WithdrawalSchema,
|
|
3486
|
-
replayed:
|
|
3953
|
+
replayed: z15.boolean()
|
|
3487
3954
|
});
|
|
3488
|
-
var PayoutEventInputSchema =
|
|
3489
|
-
externalRef:
|
|
3490
|
-
withdrawalId:
|
|
3491
|
-
state:
|
|
3492
|
-
providerRef:
|
|
3493
|
-
failureCode:
|
|
3494
|
-
failureMessage:
|
|
3955
|
+
var PayoutEventInputSchema = z15.object({
|
|
3956
|
+
externalRef: z15.string().trim().min(8).max(128),
|
|
3957
|
+
withdrawalId: z15.string().uuid().optional(),
|
|
3958
|
+
state: z15.enum(["submitted", "processing", "paid", "failed"]),
|
|
3959
|
+
providerRef: z15.string().trim().min(1).max(128).optional(),
|
|
3960
|
+
failureCode: z15.string().trim().max(64).optional(),
|
|
3961
|
+
failureMessage: z15.string().trim().max(512).optional(),
|
|
3495
3962
|
providerMetadata: Metadata.optional()
|
|
3496
3963
|
});
|
|
3497
|
-
var RecordPayoutEventResultSchema =
|
|
3964
|
+
var RecordPayoutEventResultSchema = z15.object({
|
|
3498
3965
|
withdrawal: WithdrawalSchema,
|
|
3499
|
-
replayed:
|
|
3966
|
+
replayed: z15.boolean()
|
|
3500
3967
|
});
|
|
3501
|
-
var ReconciliationReportSchema =
|
|
3502
|
-
partnerId:
|
|
3503
|
-
currency:
|
|
3504
|
-
fromMs:
|
|
3505
|
-
toMs:
|
|
3968
|
+
var ReconciliationReportSchema = z15.object({
|
|
3969
|
+
partnerId: z15.string().uuid(),
|
|
3970
|
+
currency: z15.string(),
|
|
3971
|
+
fromMs: z15.number().int().nonnegative(),
|
|
3972
|
+
toMs: z15.number().int().nonnegative(),
|
|
3506
3973
|
fundingsCreditMinor: MinorString,
|
|
3507
3974
|
fundingsDebitMinor: MinorString,
|
|
3508
3975
|
withdrawalsPaidMinor: MinorString,
|
|
@@ -3511,7 +3978,7 @@ var ReconciliationReportSchema = z14.object({
|
|
|
3511
3978
|
expectedReserveBalanceMinor: MinorString,
|
|
3512
3979
|
actualReserveBalanceMinor: MinorString,
|
|
3513
3980
|
imbalanceMinor: MinorString,
|
|
3514
|
-
generatedAtMs:
|
|
3981
|
+
generatedAtMs: z15.number().int().nonnegative()
|
|
3515
3982
|
});
|
|
3516
3983
|
function createPartnerFundingClient(partner) {
|
|
3517
3984
|
return {
|
|
@@ -3663,19 +4130,19 @@ function createPartnerProfileAdminClient(opts) {
|
|
|
3663
4130
|
}
|
|
3664
4131
|
|
|
3665
4132
|
// src/artifacts/envelope.ts
|
|
3666
|
-
import { z as
|
|
4133
|
+
import { z as z16 } from "zod";
|
|
3667
4134
|
var FLUR_ARTIFACT_URI_SCHEME = "flur";
|
|
3668
4135
|
var FLUR_ARTIFACT_VERSION = 1;
|
|
3669
4136
|
var FLUR_ARTIFACT_URI_PREFIX = `${FLUR_ARTIFACT_URI_SCHEME}://v${FLUR_ARTIFACT_VERSION}/`;
|
|
3670
4137
|
var ArtifactTypeRe = /^[a-z][a-z0-9_]{1,63}$/;
|
|
3671
|
-
var ArtifactHeaderSchema =
|
|
3672
|
-
v:
|
|
3673
|
-
t:
|
|
3674
|
-
iss:
|
|
3675
|
-
kid:
|
|
3676
|
-
iat:
|
|
3677
|
-
exp:
|
|
3678
|
-
nonce:
|
|
4138
|
+
var ArtifactHeaderSchema = z16.object({
|
|
4139
|
+
v: z16.literal(FLUR_ARTIFACT_VERSION),
|
|
4140
|
+
t: z16.string().regex(ArtifactTypeRe, "invalid artifact type"),
|
|
4141
|
+
iss: z16.string().min(1).max(128),
|
|
4142
|
+
kid: z16.string().min(1).max(128),
|
|
4143
|
+
iat: z16.number().int().nonnegative(),
|
|
4144
|
+
exp: z16.number().int().positive().optional(),
|
|
4145
|
+
nonce: z16.string().min(8).max(64).regex(/^[A-Za-z0-9_-]+$/, "nonce must be url-safe")
|
|
3679
4146
|
});
|
|
3680
4147
|
var FlurArtifactError = class extends Error {
|
|
3681
4148
|
constructor(message, code) {
|
|
@@ -3814,7 +4281,7 @@ function verifyArtifactSignature(decoded, publicKeySpkiB64, options = {}) {
|
|
|
3814
4281
|
}
|
|
3815
4282
|
|
|
3816
4283
|
// src/artifacts/types.ts
|
|
3817
|
-
import { z as
|
|
4284
|
+
import { z as z17 } from "zod";
|
|
3818
4285
|
var ARTIFACT_TYPES = {
|
|
3819
4286
|
OFFLINE_PAYMENT_AUTHORIZATION: "offline_payment_authorization",
|
|
3820
4287
|
RECEIPT: "receipt",
|
|
@@ -3827,34 +4294,37 @@ var ARTIFACT_TYPES = {
|
|
|
3827
4294
|
LEDGER_JOURNAL_ENTRY: "ledger_journal_entry",
|
|
3828
4295
|
STATEMENT: "statement",
|
|
3829
4296
|
PASS: "pass",
|
|
3830
|
-
IDENTITY: "identity"
|
|
4297
|
+
IDENTITY: "identity",
|
|
4298
|
+
// Tier B: holder-signed identity attestation for offline trust. The
|
|
4299
|
+
// envelope.iat / envelope.exp express the card's canonical lifetime.
|
|
4300
|
+
PAY_CARD: "pay_card"
|
|
3831
4301
|
};
|
|
3832
|
-
var HexString = (length) =>
|
|
4302
|
+
var HexString = (length) => z17.string().regex(
|
|
3833
4303
|
new RegExp(`^[0-9a-fA-F]{${length * 2}}$`),
|
|
3834
4304
|
`expected ${length}-byte hex string`
|
|
3835
4305
|
);
|
|
3836
|
-
var OfflinePaymentAuthorizationArtifactSchema =
|
|
4306
|
+
var OfflinePaymentAuthorizationArtifactSchema = z17.object({
|
|
3837
4307
|
authorization: OfflinePaymentAuthorizationSchema
|
|
3838
4308
|
});
|
|
3839
|
-
var ReceiptArtifactSchema =
|
|
3840
|
-
receiptId:
|
|
3841
|
-
paymentReference:
|
|
3842
|
-
payerUserId:
|
|
3843
|
-
payeeUserId:
|
|
3844
|
-
amountKobo:
|
|
3845
|
-
currency:
|
|
3846
|
-
channel:
|
|
3847
|
-
settledAtMs:
|
|
3848
|
-
ledgerTxnId:
|
|
3849
|
-
memo:
|
|
4309
|
+
var ReceiptArtifactSchema = z17.object({
|
|
4310
|
+
receiptId: z17.string().min(1).max(64),
|
|
4311
|
+
paymentReference: z17.string().min(1).max(64),
|
|
4312
|
+
payerUserId: z17.string().min(1).max(64).optional(),
|
|
4313
|
+
payeeUserId: z17.string().min(1).max(64),
|
|
4314
|
+
amountKobo: z17.number().int().positive(),
|
|
4315
|
+
currency: z17.literal("NGN"),
|
|
4316
|
+
channel: z17.enum(["online", "offline_reconciled", "pay_link", "nqr"]),
|
|
4317
|
+
settledAtMs: z17.number().int().positive(),
|
|
4318
|
+
ledgerTxnId: z17.string().min(1).max(64).optional(),
|
|
4319
|
+
memo: z17.string().max(140).optional(),
|
|
3850
4320
|
hashChainPrev: HexString(32).optional()
|
|
3851
4321
|
});
|
|
3852
|
-
var ShortId =
|
|
3853
|
-
var PositiveInt =
|
|
3854
|
-
var NonNegativeInt =
|
|
3855
|
-
var Currency2 =
|
|
3856
|
-
var Memo =
|
|
3857
|
-
var NqrPaymentRequestArtifactSchema =
|
|
4322
|
+
var ShortId = z17.string().min(1).max(64);
|
|
4323
|
+
var PositiveInt = z17.number().int().positive();
|
|
4324
|
+
var NonNegativeInt = z17.number().int().nonnegative();
|
|
4325
|
+
var Currency2 = z17.literal("NGN");
|
|
4326
|
+
var Memo = z17.string().max(140);
|
|
4327
|
+
var NqrPaymentRequestArtifactSchema = z17.object({
|
|
3858
4328
|
requestId: ShortId,
|
|
3859
4329
|
payeeUserId: ShortId,
|
|
3860
4330
|
amountKobo: PositiveInt.optional(),
|
|
@@ -3862,7 +4332,7 @@ var NqrPaymentRequestArtifactSchema = z16.object({
|
|
|
3862
4332
|
memo: Memo.optional(),
|
|
3863
4333
|
expiresAtMs: PositiveInt.optional()
|
|
3864
4334
|
});
|
|
3865
|
-
var PaymentIntentArtifactSchema =
|
|
4335
|
+
var PaymentIntentArtifactSchema = z17.object({
|
|
3866
4336
|
intentId: ShortId,
|
|
3867
4337
|
payerUserId: ShortId,
|
|
3868
4338
|
payeeUserId: ShortId,
|
|
@@ -3871,7 +4341,7 @@ var PaymentIntentArtifactSchema = z16.object({
|
|
|
3871
4341
|
idempotencyKey: ShortId,
|
|
3872
4342
|
createdAtMs: PositiveInt
|
|
3873
4343
|
});
|
|
3874
|
-
var OfflineClaimArtifactSchema =
|
|
4344
|
+
var OfflineClaimArtifactSchema = z17.object({
|
|
3875
4345
|
claimId: ShortId,
|
|
3876
4346
|
authorizationId: ShortId,
|
|
3877
4347
|
payeeUserId: ShortId,
|
|
@@ -3880,10 +4350,10 @@ var OfflineClaimArtifactSchema = z16.object({
|
|
|
3880
4350
|
claimedAtMs: PositiveInt,
|
|
3881
4351
|
paymentReference: ShortId.optional()
|
|
3882
4352
|
});
|
|
3883
|
-
var SettlementRecordArtifactSchema =
|
|
4353
|
+
var SettlementRecordArtifactSchema = z17.object({
|
|
3884
4354
|
settlementId: ShortId,
|
|
3885
4355
|
ledgerTxnId: ShortId,
|
|
3886
|
-
sourceRefType:
|
|
4356
|
+
sourceRefType: z17.enum([
|
|
3887
4357
|
"offline_authorization",
|
|
3888
4358
|
"offline_claim",
|
|
3889
4359
|
"transfer",
|
|
@@ -3894,12 +4364,12 @@ var SettlementRecordArtifactSchema = z16.object({
|
|
|
3894
4364
|
currency: Currency2,
|
|
3895
4365
|
settledAtMs: PositiveInt
|
|
3896
4366
|
});
|
|
3897
|
-
var ReversalRecordArtifactSchema =
|
|
4367
|
+
var ReversalRecordArtifactSchema = z17.object({
|
|
3898
4368
|
reversalId: ShortId,
|
|
3899
4369
|
originalTxnId: ShortId,
|
|
3900
4370
|
amountKobo: PositiveInt,
|
|
3901
4371
|
currency: Currency2,
|
|
3902
|
-
reason:
|
|
4372
|
+
reason: z17.enum([
|
|
3903
4373
|
"user_dispute",
|
|
3904
4374
|
"fraud",
|
|
3905
4375
|
"duplicate",
|
|
@@ -3909,7 +4379,7 @@ var ReversalRecordArtifactSchema = z16.object({
|
|
|
3909
4379
|
reversedAtMs: PositiveInt,
|
|
3910
4380
|
memo: Memo.optional()
|
|
3911
4381
|
});
|
|
3912
|
-
var LedgerJournalEntryArtifactSchema =
|
|
4382
|
+
var LedgerJournalEntryArtifactSchema = z17.object({
|
|
3913
4383
|
entryId: ShortId,
|
|
3914
4384
|
journalId: ShortId,
|
|
3915
4385
|
debitAccountId: ShortId,
|
|
@@ -3920,13 +4390,13 @@ var LedgerJournalEntryArtifactSchema = z16.object({
|
|
|
3920
4390
|
refType: ShortId.optional(),
|
|
3921
4391
|
refId: ShortId.optional()
|
|
3922
4392
|
});
|
|
3923
|
-
var StatementArtifactSchema =
|
|
4393
|
+
var StatementArtifactSchema = z17.object({
|
|
3924
4394
|
statementId: ShortId,
|
|
3925
4395
|
userId: ShortId,
|
|
3926
4396
|
periodStartMs: PositiveInt,
|
|
3927
4397
|
periodEndMs: PositiveInt,
|
|
3928
|
-
openingBalanceKobo:
|
|
3929
|
-
closingBalanceKobo:
|
|
4398
|
+
openingBalanceKobo: z17.number().int(),
|
|
4399
|
+
closingBalanceKobo: z17.number().int(),
|
|
3930
4400
|
transactionCount: NonNegativeInt,
|
|
3931
4401
|
currency: Currency2,
|
|
3932
4402
|
hashChainPrev: HexString(32).optional()
|
|
@@ -3934,16 +4404,16 @@ var StatementArtifactSchema = z16.object({
|
|
|
3934
4404
|
message: "periodEndMs must be greater than periodStartMs",
|
|
3935
4405
|
path: ["periodEndMs"]
|
|
3936
4406
|
});
|
|
3937
|
-
var PassArtifactSchema =
|
|
4407
|
+
var PassArtifactSchema = z17.object({
|
|
3938
4408
|
passId: ShortId,
|
|
3939
4409
|
holderId: ShortId,
|
|
3940
|
-
category:
|
|
3941
|
-
title:
|
|
4410
|
+
category: z17.enum(["membership", "ticket", "loyalty", "access", "voucher"]),
|
|
4411
|
+
title: z17.string().min(1).max(120),
|
|
3942
4412
|
validFromMs: PositiveInt,
|
|
3943
4413
|
validUntilMs: PositiveInt.optional(),
|
|
3944
|
-
metadata:
|
|
3945
|
-
|
|
3946
|
-
|
|
4414
|
+
metadata: z17.record(
|
|
4415
|
+
z17.string().min(1).max(64),
|
|
4416
|
+
z17.union([z17.string().max(280), z17.number(), z17.boolean()])
|
|
3947
4417
|
).optional()
|
|
3948
4418
|
}).refine(
|
|
3949
4419
|
(v) => v.validUntilMs === void 0 || v.validUntilMs > v.validFromMs,
|
|
@@ -3952,10 +4422,10 @@ var PassArtifactSchema = z16.object({
|
|
|
3952
4422
|
path: ["validUntilMs"]
|
|
3953
4423
|
}
|
|
3954
4424
|
);
|
|
3955
|
-
var IdentityArtifactSchema =
|
|
4425
|
+
var IdentityArtifactSchema = z17.object({
|
|
3956
4426
|
attestationId: ShortId,
|
|
3957
4427
|
subjectId: ShortId,
|
|
3958
|
-
claimType:
|
|
4428
|
+
claimType: z17.enum([
|
|
3959
4429
|
"phone_verified",
|
|
3960
4430
|
"email_verified",
|
|
3961
4431
|
"bvn_verified",
|
|
@@ -3965,6 +4435,12 @@ var IdentityArtifactSchema = z16.object({
|
|
|
3965
4435
|
claimValueHash: HexString(32),
|
|
3966
4436
|
attestedAtMs: PositiveInt
|
|
3967
4437
|
});
|
|
4438
|
+
var PayCardArtifactSchema = z17.object({
|
|
4439
|
+
userId: ShortId,
|
|
4440
|
+
phoneE164: z17.string().regex(/^\+[1-9]\d{7,14}$/, "phoneE164 must be normalised E.164"),
|
|
4441
|
+
displayName: z17.string().min(1).max(64),
|
|
4442
|
+
devicePubKeySpkiB64: z17.string().min(64).max(256).regex(/^[A-Za-z0-9+/]+=*$/, "devicePubKeySpkiB64 must be standard base64")
|
|
4443
|
+
});
|
|
3968
4444
|
var ARTIFACT_BODY_SCHEMAS = {
|
|
3969
4445
|
[ARTIFACT_TYPES.OFFLINE_PAYMENT_AUTHORIZATION]: OfflinePaymentAuthorizationArtifactSchema,
|
|
3970
4446
|
[ARTIFACT_TYPES.RECEIPT]: ReceiptArtifactSchema,
|
|
@@ -3976,7 +4452,8 @@ var ARTIFACT_BODY_SCHEMAS = {
|
|
|
3976
4452
|
[ARTIFACT_TYPES.LEDGER_JOURNAL_ENTRY]: LedgerJournalEntryArtifactSchema,
|
|
3977
4453
|
[ARTIFACT_TYPES.STATEMENT]: StatementArtifactSchema,
|
|
3978
4454
|
[ARTIFACT_TYPES.PASS]: PassArtifactSchema,
|
|
3979
|
-
[ARTIFACT_TYPES.IDENTITY]: IdentityArtifactSchema
|
|
4455
|
+
[ARTIFACT_TYPES.IDENTITY]: IdentityArtifactSchema,
|
|
4456
|
+
[ARTIFACT_TYPES.PAY_CARD]: PayCardArtifactSchema
|
|
3980
4457
|
};
|
|
3981
4458
|
var HARDENED_ARTIFACT_TYPES = /* @__PURE__ */ new Set([
|
|
3982
4459
|
ARTIFACT_TYPES.OFFLINE_PAYMENT_AUTHORIZATION,
|
|
@@ -3989,7 +4466,8 @@ var HARDENED_ARTIFACT_TYPES = /* @__PURE__ */ new Set([
|
|
|
3989
4466
|
ARTIFACT_TYPES.LEDGER_JOURNAL_ENTRY,
|
|
3990
4467
|
ARTIFACT_TYPES.STATEMENT,
|
|
3991
4468
|
ARTIFACT_TYPES.PASS,
|
|
3992
|
-
ARTIFACT_TYPES.IDENTITY
|
|
4469
|
+
ARTIFACT_TYPES.IDENTITY,
|
|
4470
|
+
ARTIFACT_TYPES.PAY_CARD
|
|
3993
4471
|
]);
|
|
3994
4472
|
function isKnownArtifactType(t) {
|
|
3995
4473
|
return Object.values(ARTIFACT_TYPES).includes(t);
|
|
@@ -4074,6 +4552,130 @@ function createOfflinePaymentAuthorizationArtifactUri(input) {
|
|
|
4074
4552
|
type: ARTIFACT_TYPES.OFFLINE_PAYMENT_AUTHORIZATION
|
|
4075
4553
|
});
|
|
4076
4554
|
}
|
|
4555
|
+
|
|
4556
|
+
// src/artifacts/paycard.ts
|
|
4557
|
+
var PAY_CARD_DEFAULT_TTL_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
4558
|
+
var PAY_CARD_REFRESH_THRESHOLD_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
4559
|
+
var PAY_CARD_URI_PREFIX = `${FLUR_ARTIFACT_URI_PREFIX}${ARTIFACT_TYPES.PAY_CARD}/`;
|
|
4560
|
+
function createPayCardArtifactUri(input) {
|
|
4561
|
+
if (input.data.userId !== input.issuer) {
|
|
4562
|
+
throw new FlurArtifactError(
|
|
4563
|
+
"pay_card.data.userId must equal envelope issuer",
|
|
4564
|
+
"INVALID_BODY"
|
|
4565
|
+
);
|
|
4566
|
+
}
|
|
4567
|
+
const iat = input.issuedAtSeconds ?? Math.floor(Date.now() / 1e3);
|
|
4568
|
+
const exp = input.expiresAtSeconds ?? iat + Math.floor(PAY_CARD_DEFAULT_TTL_MS / 1e3);
|
|
4569
|
+
return createArtifactUri({
|
|
4570
|
+
type: ARTIFACT_TYPES.PAY_CARD,
|
|
4571
|
+
issuer: input.issuer,
|
|
4572
|
+
keyId: input.keyId,
|
|
4573
|
+
privateKey: input.privateKey,
|
|
4574
|
+
nonce: input.nonce,
|
|
4575
|
+
issuedAtSeconds: iat,
|
|
4576
|
+
expiresAtSeconds: exp,
|
|
4577
|
+
data: input.data
|
|
4578
|
+
});
|
|
4579
|
+
}
|
|
4580
|
+
function isPayCardArtifactUri(uri) {
|
|
4581
|
+
return typeof uri === "string" && uri.startsWith(PAY_CARD_URI_PREFIX);
|
|
4582
|
+
}
|
|
4583
|
+
function decodePayCardArtifact(uri) {
|
|
4584
|
+
if (!isPayCardArtifactUri(uri)) {
|
|
4585
|
+
throw new FlurArtifactError(
|
|
4586
|
+
`URI does not start with ${PAY_CARD_URI_PREFIX}`,
|
|
4587
|
+
"INVALID_URI"
|
|
4588
|
+
);
|
|
4589
|
+
}
|
|
4590
|
+
const decoded = decodeArtifactUri(uri);
|
|
4591
|
+
if (decoded.type !== ARTIFACT_TYPES.PAY_CARD) {
|
|
4592
|
+
throw new FlurArtifactError(
|
|
4593
|
+
`Expected pay_card, got ${decoded.type}`,
|
|
4594
|
+
"TYPE_MISMATCH"
|
|
4595
|
+
);
|
|
4596
|
+
}
|
|
4597
|
+
const parsed = PayCardArtifactSchema.safeParse(decoded.body.data);
|
|
4598
|
+
if (!parsed.success) {
|
|
4599
|
+
throw new FlurArtifactError(
|
|
4600
|
+
`pay_card body invalid: ${parsed.error.message}`,
|
|
4601
|
+
"INVALID_BODY"
|
|
4602
|
+
);
|
|
4603
|
+
}
|
|
4604
|
+
if (parsed.data.userId !== decoded.body.iss) {
|
|
4605
|
+
throw new FlurArtifactError(
|
|
4606
|
+
"pay_card.data.userId must equal envelope issuer",
|
|
4607
|
+
"INVALID_BODY"
|
|
4608
|
+
);
|
|
4609
|
+
}
|
|
4610
|
+
return {
|
|
4611
|
+
body: {
|
|
4612
|
+
...decoded.body,
|
|
4613
|
+
data: parsed.data
|
|
4614
|
+
},
|
|
4615
|
+
sig: decoded.sig,
|
|
4616
|
+
decoded
|
|
4617
|
+
};
|
|
4618
|
+
}
|
|
4619
|
+
function verifyPayCardArtifact(uri, publicKeySpkiB64, options = {}) {
|
|
4620
|
+
if (!isPayCardArtifactUri(uri)) {
|
|
4621
|
+
throw new FlurArtifactError(
|
|
4622
|
+
`URI does not start with ${PAY_CARD_URI_PREFIX}`,
|
|
4623
|
+
"INVALID_URI"
|
|
4624
|
+
);
|
|
4625
|
+
}
|
|
4626
|
+
const verified = verifyArtifactUri(
|
|
4627
|
+
uri,
|
|
4628
|
+
publicKeySpkiB64,
|
|
4629
|
+
options
|
|
4630
|
+
);
|
|
4631
|
+
if (verified.decoded.type !== ARTIFACT_TYPES.PAY_CARD) {
|
|
4632
|
+
throw new FlurArtifactError(
|
|
4633
|
+
`Expected pay_card, got ${verified.decoded.type}`,
|
|
4634
|
+
"TYPE_MISMATCH"
|
|
4635
|
+
);
|
|
4636
|
+
}
|
|
4637
|
+
if (verified.body.data.userId !== verified.body.iss) {
|
|
4638
|
+
throw new FlurArtifactError(
|
|
4639
|
+
"pay_card.data.userId must equal envelope issuer",
|
|
4640
|
+
"INVALID_BODY"
|
|
4641
|
+
);
|
|
4642
|
+
}
|
|
4643
|
+
return {
|
|
4644
|
+
body: verified.body,
|
|
4645
|
+
sig: verified.sig,
|
|
4646
|
+
decoded: verified.decoded
|
|
4647
|
+
};
|
|
4648
|
+
}
|
|
4649
|
+
function inspectPayCardFreshness(decoded, nowMs = Date.now()) {
|
|
4650
|
+
const exp = decoded.body.exp;
|
|
4651
|
+
if (exp === void 0) return "no_expiry";
|
|
4652
|
+
const remainingMs = exp * 1e3 - nowMs;
|
|
4653
|
+
if (remainingMs <= 0) return "expired";
|
|
4654
|
+
if (remainingMs <= PAY_CARD_REFRESH_THRESHOLD_MS)
|
|
4655
|
+
return "refresh_recommended";
|
|
4656
|
+
return "fresh";
|
|
4657
|
+
}
|
|
4658
|
+
function buildPayCardSigningInput(input) {
|
|
4659
|
+
if (input.data.userId !== input.issuer) {
|
|
4660
|
+
throw new FlurArtifactError(
|
|
4661
|
+
"pay_card.data.userId must equal envelope issuer",
|
|
4662
|
+
"INVALID_BODY"
|
|
4663
|
+
);
|
|
4664
|
+
}
|
|
4665
|
+
const parsedData = PayCardArtifactSchema.parse(input.data);
|
|
4666
|
+
const iat = input.issuedAtSeconds ?? Math.floor(Date.now() / 1e3);
|
|
4667
|
+
const exp = input.expiresAtSeconds ?? iat + Math.floor(PAY_CARD_DEFAULT_TTL_MS / 1e3);
|
|
4668
|
+
const body = buildArtifactBody({
|
|
4669
|
+
type: ARTIFACT_TYPES.PAY_CARD,
|
|
4670
|
+
issuer: input.issuer,
|
|
4671
|
+
keyId: input.keyId,
|
|
4672
|
+
data: parsedData,
|
|
4673
|
+
nonce: input.nonce,
|
|
4674
|
+
issuedAtSeconds: iat,
|
|
4675
|
+
expiresAtSeconds: exp
|
|
4676
|
+
});
|
|
4677
|
+
return { body, bodyBytes: canonicalJSONBytes(body) };
|
|
4678
|
+
}
|
|
4077
4679
|
export {
|
|
4078
4680
|
ACCOUNT_FUNDED_OAC_MAX_TTL_MS,
|
|
4079
4681
|
ACCOUNT_STATUSES,
|
|
@@ -4090,6 +4692,9 @@ export {
|
|
|
4090
4692
|
COLLECTION_INTENT_STATUSES,
|
|
4091
4693
|
COLLECTION_PAYMENT_STATUSES,
|
|
4092
4694
|
CONSUMER_OFFLINE_CLAIM_SUBMIT_GRACE_MS,
|
|
4695
|
+
CONSUMER_PAYMENT_REQUEST_DOMAIN,
|
|
4696
|
+
CONSUMER_SETTLEMENT_DOMAIN,
|
|
4697
|
+
CONSUMER_SETTLEMENT_RECEIPT_QR_PREFIX,
|
|
4093
4698
|
CUSTODIAL_MODES,
|
|
4094
4699
|
CollectionIntentSchema,
|
|
4095
4700
|
CollectionPaymentResultSchema,
|
|
@@ -4099,6 +4704,7 @@ export {
|
|
|
4099
4704
|
OACRecordSchema as ConsumerOACRecordSchema,
|
|
4100
4705
|
ConsumerOACSchema,
|
|
4101
4706
|
ConsumerPaymentClaimSchema,
|
|
4707
|
+
ConsumerPaymentRequestEnvelopeSchema,
|
|
4102
4708
|
ConsumerSettleResultSchema,
|
|
4103
4709
|
ConsumerSettlementSchema,
|
|
4104
4710
|
CreateCollectionIntentInputSchema,
|
|
@@ -4140,6 +4746,12 @@ export {
|
|
|
4140
4746
|
OAC_DEFAULT_PER_TX_KOBO,
|
|
4141
4747
|
OAC_DEFAULT_VALIDITY_MS,
|
|
4142
4748
|
OFFLINE_CLAIM_SMS_PREFIX,
|
|
4749
|
+
OFFLINE_SMS_SETTLE_DOMAIN,
|
|
4750
|
+
OFFLINE_SMS_SETTLE_HEADER_BYTES,
|
|
4751
|
+
OFFLINE_SMS_SETTLE_PREFIX,
|
|
4752
|
+
OFFLINE_SMS_SETTLE_SIGNATURE_BYTES,
|
|
4753
|
+
OFFLINE_SMS_SETTLE_TOKEN_BYTES,
|
|
4754
|
+
OFFLINE_SMS_SETTLE_VERSION,
|
|
4143
4755
|
OfflineClaimArtifactSchema,
|
|
4144
4756
|
OfflinePaymentAuthorizationArtifactSchema,
|
|
4145
4757
|
OfflinePaymentAuthorizationSchema,
|
|
@@ -4157,6 +4769,9 @@ export {
|
|
|
4157
4769
|
PASS_STATES,
|
|
4158
4770
|
PAYLOAD_FORMAT_INDICATOR_VALUE,
|
|
4159
4771
|
PAYOUT_DESTINATION_STATUSES,
|
|
4772
|
+
PAY_CARD_DEFAULT_TTL_MS,
|
|
4773
|
+
PAY_CARD_REFRESH_THRESHOLD_MS,
|
|
4774
|
+
PAY_CARD_URI_PREFIX,
|
|
4160
4775
|
POINT_OF_INITIATION,
|
|
4161
4776
|
PartnerFundingEventInputSchema,
|
|
4162
4777
|
PartnerFundingSchema,
|
|
@@ -4164,6 +4779,7 @@ export {
|
|
|
4164
4779
|
PassArtifactSchema,
|
|
4165
4780
|
PassMetadataSchema,
|
|
4166
4781
|
PassSchema,
|
|
4782
|
+
PayCardArtifactSchema,
|
|
4167
4783
|
PayCollectionInputSchema,
|
|
4168
4784
|
PaymentClaimSchema,
|
|
4169
4785
|
PaymentIntentArtifactSchema,
|
|
@@ -4199,18 +4815,26 @@ export {
|
|
|
4199
4815
|
bodySha256Hex,
|
|
4200
4816
|
buildArtifactBody,
|
|
4201
4817
|
buildAuthorization,
|
|
4818
|
+
buildConsumerPaymentRequest,
|
|
4202
4819
|
buildOAC,
|
|
4203
4820
|
buildPass,
|
|
4821
|
+
buildPayCardSigningInput,
|
|
4204
4822
|
buildPaymentRequest,
|
|
4205
4823
|
buildReceipt,
|
|
4206
4824
|
buildRedemption,
|
|
4825
|
+
buildSmsSettleHeader,
|
|
4826
|
+
domainTag as buildSmsSettleSignedBytes,
|
|
4207
4827
|
canonicalClaimSigningBytes,
|
|
4208
4828
|
canonicalClaimSigningPayload,
|
|
4209
4829
|
canonicalJSONBytes,
|
|
4210
4830
|
canonicalJSONStringify,
|
|
4211
4831
|
canonicalRequestString,
|
|
4832
|
+
computeConsumerClaimEncounterId,
|
|
4212
4833
|
computeEncounterId,
|
|
4213
4834
|
constantTimeEqual,
|
|
4835
|
+
consumerPaymentRequestSigningBytes,
|
|
4836
|
+
consumerPaymentRequestSigningPayload,
|
|
4837
|
+
consumerSettlementSigningPayload,
|
|
4214
4838
|
crc16ccitt,
|
|
4215
4839
|
crc16ccittHex,
|
|
4216
4840
|
createAccountsClient,
|
|
@@ -4228,28 +4852,40 @@ export {
|
|
|
4228
4852
|
createPartnerFundingClient,
|
|
4229
4853
|
createPartnerProfileAdminClient,
|
|
4230
4854
|
createPassesClient,
|
|
4855
|
+
createPayCardArtifactUri,
|
|
4231
4856
|
createReceiptArtifactUri,
|
|
4232
4857
|
createReceiptsClient,
|
|
4233
4858
|
createSoftwareP256Signer,
|
|
4234
4859
|
decodeArtifactUri,
|
|
4235
4860
|
decodeAuthorizationQR,
|
|
4236
4861
|
decodeBase45,
|
|
4862
|
+
decodeConsumerSettlementReceiptQR,
|
|
4237
4863
|
decodeOfflineClaimSmsMessage,
|
|
4864
|
+
decodeOfflineSmsSettleToken,
|
|
4865
|
+
decodePayCardArtifact,
|
|
4238
4866
|
decodePaymentRequestQR,
|
|
4867
|
+
decodeUnverifiedConsumerSettlementReceiptQR,
|
|
4868
|
+
derToRawP256Signature,
|
|
4239
4869
|
encodeArtifactUri,
|
|
4240
4870
|
encodeAuthorizationQR,
|
|
4241
4871
|
encodeBase45,
|
|
4872
|
+
encodeConsumerSettlementReceiptQR,
|
|
4242
4873
|
encodeNQR,
|
|
4243
4874
|
encodeOfflineClaimSmsMessage,
|
|
4875
|
+
encodeOfflineSmsSettleToken,
|
|
4244
4876
|
encodePaymentRequestQR,
|
|
4245
4877
|
extractOfflineClaimSmsToken,
|
|
4878
|
+
extractOfflineSmsSettleToken,
|
|
4246
4879
|
formatAmount,
|
|
4247
4880
|
generateDynamicQR,
|
|
4248
4881
|
generateStaticQR,
|
|
4249
4882
|
init,
|
|
4883
|
+
inspectPayCardFreshness,
|
|
4884
|
+
isConsumerPaymentRequestExpired,
|
|
4250
4885
|
isHardenedArtifactType,
|
|
4251
4886
|
isKnownArtifactType,
|
|
4252
4887
|
isPassWithinValidity,
|
|
4888
|
+
isPayCardArtifactUri,
|
|
4253
4889
|
moneyMinorToNumber,
|
|
4254
4890
|
normalizeE164,
|
|
4255
4891
|
parseAmountInput,
|
|
@@ -4259,6 +4895,7 @@ export {
|
|
|
4259
4895
|
routingHint,
|
|
4260
4896
|
signArtifact,
|
|
4261
4897
|
signAuthorization,
|
|
4898
|
+
signConsumerPaymentRequest,
|
|
4262
4899
|
signOAC,
|
|
4263
4900
|
signPartnerRequest,
|
|
4264
4901
|
signPass,
|
|
@@ -4270,8 +4907,13 @@ export {
|
|
|
4270
4907
|
verifyArtifactUri,
|
|
4271
4908
|
verifyAuthorization,
|
|
4272
4909
|
verifyClaimSignature,
|
|
4910
|
+
verifyConsumerPaymentRequest,
|
|
4911
|
+
verifyConsumerSettlement,
|
|
4912
|
+
verifyConsumerSettlementReceiptQR,
|
|
4273
4913
|
verifyOAC,
|
|
4914
|
+
verifyOfflineSmsSettleToken,
|
|
4274
4915
|
verifyPass,
|
|
4916
|
+
verifyPayCardArtifact,
|
|
4275
4917
|
verifyPaymentRequest,
|
|
4276
4918
|
verifyReceipt,
|
|
4277
4919
|
verifyRedemption,
|