@nokinc-flur/sdk 2.0.0 → 2.1.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 +706 -169
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +326 -11
- package/dist/index.d.ts +326 -11
- package/dist/index.js +666 -169
- 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",
|
|
@@ -3829,32 +4296,32 @@ var ARTIFACT_TYPES = {
|
|
|
3829
4296
|
PASS: "pass",
|
|
3830
4297
|
IDENTITY: "identity"
|
|
3831
4298
|
};
|
|
3832
|
-
var HexString = (length) =>
|
|
4299
|
+
var HexString = (length) => z17.string().regex(
|
|
3833
4300
|
new RegExp(`^[0-9a-fA-F]{${length * 2}}$`),
|
|
3834
4301
|
`expected ${length}-byte hex string`
|
|
3835
4302
|
);
|
|
3836
|
-
var OfflinePaymentAuthorizationArtifactSchema =
|
|
4303
|
+
var OfflinePaymentAuthorizationArtifactSchema = z17.object({
|
|
3837
4304
|
authorization: OfflinePaymentAuthorizationSchema
|
|
3838
4305
|
});
|
|
3839
|
-
var ReceiptArtifactSchema =
|
|
3840
|
-
receiptId:
|
|
3841
|
-
paymentReference:
|
|
3842
|
-
payerUserId:
|
|
3843
|
-
payeeUserId:
|
|
3844
|
-
amountKobo:
|
|
3845
|
-
currency:
|
|
3846
|
-
channel:
|
|
3847
|
-
settledAtMs:
|
|
3848
|
-
ledgerTxnId:
|
|
3849
|
-
memo:
|
|
4306
|
+
var ReceiptArtifactSchema = z17.object({
|
|
4307
|
+
receiptId: z17.string().min(1).max(64),
|
|
4308
|
+
paymentReference: z17.string().min(1).max(64),
|
|
4309
|
+
payerUserId: z17.string().min(1).max(64).optional(),
|
|
4310
|
+
payeeUserId: z17.string().min(1).max(64),
|
|
4311
|
+
amountKobo: z17.number().int().positive(),
|
|
4312
|
+
currency: z17.literal("NGN"),
|
|
4313
|
+
channel: z17.enum(["online", "offline_reconciled", "pay_link", "nqr"]),
|
|
4314
|
+
settledAtMs: z17.number().int().positive(),
|
|
4315
|
+
ledgerTxnId: z17.string().min(1).max(64).optional(),
|
|
4316
|
+
memo: z17.string().max(140).optional(),
|
|
3850
4317
|
hashChainPrev: HexString(32).optional()
|
|
3851
4318
|
});
|
|
3852
|
-
var ShortId =
|
|
3853
|
-
var PositiveInt =
|
|
3854
|
-
var NonNegativeInt =
|
|
3855
|
-
var Currency2 =
|
|
3856
|
-
var Memo =
|
|
3857
|
-
var NqrPaymentRequestArtifactSchema =
|
|
4319
|
+
var ShortId = z17.string().min(1).max(64);
|
|
4320
|
+
var PositiveInt = z17.number().int().positive();
|
|
4321
|
+
var NonNegativeInt = z17.number().int().nonnegative();
|
|
4322
|
+
var Currency2 = z17.literal("NGN");
|
|
4323
|
+
var Memo = z17.string().max(140);
|
|
4324
|
+
var NqrPaymentRequestArtifactSchema = z17.object({
|
|
3858
4325
|
requestId: ShortId,
|
|
3859
4326
|
payeeUserId: ShortId,
|
|
3860
4327
|
amountKobo: PositiveInt.optional(),
|
|
@@ -3862,7 +4329,7 @@ var NqrPaymentRequestArtifactSchema = z16.object({
|
|
|
3862
4329
|
memo: Memo.optional(),
|
|
3863
4330
|
expiresAtMs: PositiveInt.optional()
|
|
3864
4331
|
});
|
|
3865
|
-
var PaymentIntentArtifactSchema =
|
|
4332
|
+
var PaymentIntentArtifactSchema = z17.object({
|
|
3866
4333
|
intentId: ShortId,
|
|
3867
4334
|
payerUserId: ShortId,
|
|
3868
4335
|
payeeUserId: ShortId,
|
|
@@ -3871,7 +4338,7 @@ var PaymentIntentArtifactSchema = z16.object({
|
|
|
3871
4338
|
idempotencyKey: ShortId,
|
|
3872
4339
|
createdAtMs: PositiveInt
|
|
3873
4340
|
});
|
|
3874
|
-
var OfflineClaimArtifactSchema =
|
|
4341
|
+
var OfflineClaimArtifactSchema = z17.object({
|
|
3875
4342
|
claimId: ShortId,
|
|
3876
4343
|
authorizationId: ShortId,
|
|
3877
4344
|
payeeUserId: ShortId,
|
|
@@ -3880,10 +4347,10 @@ var OfflineClaimArtifactSchema = z16.object({
|
|
|
3880
4347
|
claimedAtMs: PositiveInt,
|
|
3881
4348
|
paymentReference: ShortId.optional()
|
|
3882
4349
|
});
|
|
3883
|
-
var SettlementRecordArtifactSchema =
|
|
4350
|
+
var SettlementRecordArtifactSchema = z17.object({
|
|
3884
4351
|
settlementId: ShortId,
|
|
3885
4352
|
ledgerTxnId: ShortId,
|
|
3886
|
-
sourceRefType:
|
|
4353
|
+
sourceRefType: z17.enum([
|
|
3887
4354
|
"offline_authorization",
|
|
3888
4355
|
"offline_claim",
|
|
3889
4356
|
"transfer",
|
|
@@ -3894,12 +4361,12 @@ var SettlementRecordArtifactSchema = z16.object({
|
|
|
3894
4361
|
currency: Currency2,
|
|
3895
4362
|
settledAtMs: PositiveInt
|
|
3896
4363
|
});
|
|
3897
|
-
var ReversalRecordArtifactSchema =
|
|
4364
|
+
var ReversalRecordArtifactSchema = z17.object({
|
|
3898
4365
|
reversalId: ShortId,
|
|
3899
4366
|
originalTxnId: ShortId,
|
|
3900
4367
|
amountKobo: PositiveInt,
|
|
3901
4368
|
currency: Currency2,
|
|
3902
|
-
reason:
|
|
4369
|
+
reason: z17.enum([
|
|
3903
4370
|
"user_dispute",
|
|
3904
4371
|
"fraud",
|
|
3905
4372
|
"duplicate",
|
|
@@ -3909,7 +4376,7 @@ var ReversalRecordArtifactSchema = z16.object({
|
|
|
3909
4376
|
reversedAtMs: PositiveInt,
|
|
3910
4377
|
memo: Memo.optional()
|
|
3911
4378
|
});
|
|
3912
|
-
var LedgerJournalEntryArtifactSchema =
|
|
4379
|
+
var LedgerJournalEntryArtifactSchema = z17.object({
|
|
3913
4380
|
entryId: ShortId,
|
|
3914
4381
|
journalId: ShortId,
|
|
3915
4382
|
debitAccountId: ShortId,
|
|
@@ -3920,13 +4387,13 @@ var LedgerJournalEntryArtifactSchema = z16.object({
|
|
|
3920
4387
|
refType: ShortId.optional(),
|
|
3921
4388
|
refId: ShortId.optional()
|
|
3922
4389
|
});
|
|
3923
|
-
var StatementArtifactSchema =
|
|
4390
|
+
var StatementArtifactSchema = z17.object({
|
|
3924
4391
|
statementId: ShortId,
|
|
3925
4392
|
userId: ShortId,
|
|
3926
4393
|
periodStartMs: PositiveInt,
|
|
3927
4394
|
periodEndMs: PositiveInt,
|
|
3928
|
-
openingBalanceKobo:
|
|
3929
|
-
closingBalanceKobo:
|
|
4395
|
+
openingBalanceKobo: z17.number().int(),
|
|
4396
|
+
closingBalanceKobo: z17.number().int(),
|
|
3930
4397
|
transactionCount: NonNegativeInt,
|
|
3931
4398
|
currency: Currency2,
|
|
3932
4399
|
hashChainPrev: HexString(32).optional()
|
|
@@ -3934,16 +4401,16 @@ var StatementArtifactSchema = z16.object({
|
|
|
3934
4401
|
message: "periodEndMs must be greater than periodStartMs",
|
|
3935
4402
|
path: ["periodEndMs"]
|
|
3936
4403
|
});
|
|
3937
|
-
var PassArtifactSchema =
|
|
4404
|
+
var PassArtifactSchema = z17.object({
|
|
3938
4405
|
passId: ShortId,
|
|
3939
4406
|
holderId: ShortId,
|
|
3940
|
-
category:
|
|
3941
|
-
title:
|
|
4407
|
+
category: z17.enum(["membership", "ticket", "loyalty", "access", "voucher"]),
|
|
4408
|
+
title: z17.string().min(1).max(120),
|
|
3942
4409
|
validFromMs: PositiveInt,
|
|
3943
4410
|
validUntilMs: PositiveInt.optional(),
|
|
3944
|
-
metadata:
|
|
3945
|
-
|
|
3946
|
-
|
|
4411
|
+
metadata: z17.record(
|
|
4412
|
+
z17.string().min(1).max(64),
|
|
4413
|
+
z17.union([z17.string().max(280), z17.number(), z17.boolean()])
|
|
3947
4414
|
).optional()
|
|
3948
4415
|
}).refine(
|
|
3949
4416
|
(v) => v.validUntilMs === void 0 || v.validUntilMs > v.validFromMs,
|
|
@@ -3952,10 +4419,10 @@ var PassArtifactSchema = z16.object({
|
|
|
3952
4419
|
path: ["validUntilMs"]
|
|
3953
4420
|
}
|
|
3954
4421
|
);
|
|
3955
|
-
var IdentityArtifactSchema =
|
|
4422
|
+
var IdentityArtifactSchema = z17.object({
|
|
3956
4423
|
attestationId: ShortId,
|
|
3957
4424
|
subjectId: ShortId,
|
|
3958
|
-
claimType:
|
|
4425
|
+
claimType: z17.enum([
|
|
3959
4426
|
"phone_verified",
|
|
3960
4427
|
"email_verified",
|
|
3961
4428
|
"bvn_verified",
|
|
@@ -4090,6 +4557,9 @@ export {
|
|
|
4090
4557
|
COLLECTION_INTENT_STATUSES,
|
|
4091
4558
|
COLLECTION_PAYMENT_STATUSES,
|
|
4092
4559
|
CONSUMER_OFFLINE_CLAIM_SUBMIT_GRACE_MS,
|
|
4560
|
+
CONSUMER_PAYMENT_REQUEST_DOMAIN,
|
|
4561
|
+
CONSUMER_SETTLEMENT_DOMAIN,
|
|
4562
|
+
CONSUMER_SETTLEMENT_RECEIPT_QR_PREFIX,
|
|
4093
4563
|
CUSTODIAL_MODES,
|
|
4094
4564
|
CollectionIntentSchema,
|
|
4095
4565
|
CollectionPaymentResultSchema,
|
|
@@ -4099,6 +4569,7 @@ export {
|
|
|
4099
4569
|
OACRecordSchema as ConsumerOACRecordSchema,
|
|
4100
4570
|
ConsumerOACSchema,
|
|
4101
4571
|
ConsumerPaymentClaimSchema,
|
|
4572
|
+
ConsumerPaymentRequestEnvelopeSchema,
|
|
4102
4573
|
ConsumerSettleResultSchema,
|
|
4103
4574
|
ConsumerSettlementSchema,
|
|
4104
4575
|
CreateCollectionIntentInputSchema,
|
|
@@ -4140,6 +4611,12 @@ export {
|
|
|
4140
4611
|
OAC_DEFAULT_PER_TX_KOBO,
|
|
4141
4612
|
OAC_DEFAULT_VALIDITY_MS,
|
|
4142
4613
|
OFFLINE_CLAIM_SMS_PREFIX,
|
|
4614
|
+
OFFLINE_SMS_SETTLE_DOMAIN,
|
|
4615
|
+
OFFLINE_SMS_SETTLE_HEADER_BYTES,
|
|
4616
|
+
OFFLINE_SMS_SETTLE_PREFIX,
|
|
4617
|
+
OFFLINE_SMS_SETTLE_SIGNATURE_BYTES,
|
|
4618
|
+
OFFLINE_SMS_SETTLE_TOKEN_BYTES,
|
|
4619
|
+
OFFLINE_SMS_SETTLE_VERSION,
|
|
4143
4620
|
OfflineClaimArtifactSchema,
|
|
4144
4621
|
OfflinePaymentAuthorizationArtifactSchema,
|
|
4145
4622
|
OfflinePaymentAuthorizationSchema,
|
|
@@ -4199,18 +4676,25 @@ export {
|
|
|
4199
4676
|
bodySha256Hex,
|
|
4200
4677
|
buildArtifactBody,
|
|
4201
4678
|
buildAuthorization,
|
|
4679
|
+
buildConsumerPaymentRequest,
|
|
4202
4680
|
buildOAC,
|
|
4203
4681
|
buildPass,
|
|
4204
4682
|
buildPaymentRequest,
|
|
4205
4683
|
buildReceipt,
|
|
4206
4684
|
buildRedemption,
|
|
4685
|
+
buildSmsSettleHeader,
|
|
4686
|
+
domainTag as buildSmsSettleSignedBytes,
|
|
4207
4687
|
canonicalClaimSigningBytes,
|
|
4208
4688
|
canonicalClaimSigningPayload,
|
|
4209
4689
|
canonicalJSONBytes,
|
|
4210
4690
|
canonicalJSONStringify,
|
|
4211
4691
|
canonicalRequestString,
|
|
4692
|
+
computeConsumerClaimEncounterId,
|
|
4212
4693
|
computeEncounterId,
|
|
4213
4694
|
constantTimeEqual,
|
|
4695
|
+
consumerPaymentRequestSigningBytes,
|
|
4696
|
+
consumerPaymentRequestSigningPayload,
|
|
4697
|
+
consumerSettlementSigningPayload,
|
|
4214
4698
|
crc16ccitt,
|
|
4215
4699
|
crc16ccittHex,
|
|
4216
4700
|
createAccountsClient,
|
|
@@ -4234,19 +4718,27 @@ export {
|
|
|
4234
4718
|
decodeArtifactUri,
|
|
4235
4719
|
decodeAuthorizationQR,
|
|
4236
4720
|
decodeBase45,
|
|
4721
|
+
decodeConsumerSettlementReceiptQR,
|
|
4237
4722
|
decodeOfflineClaimSmsMessage,
|
|
4723
|
+
decodeOfflineSmsSettleToken,
|
|
4238
4724
|
decodePaymentRequestQR,
|
|
4725
|
+
decodeUnverifiedConsumerSettlementReceiptQR,
|
|
4726
|
+
derToRawP256Signature,
|
|
4239
4727
|
encodeArtifactUri,
|
|
4240
4728
|
encodeAuthorizationQR,
|
|
4241
4729
|
encodeBase45,
|
|
4730
|
+
encodeConsumerSettlementReceiptQR,
|
|
4242
4731
|
encodeNQR,
|
|
4243
4732
|
encodeOfflineClaimSmsMessage,
|
|
4733
|
+
encodeOfflineSmsSettleToken,
|
|
4244
4734
|
encodePaymentRequestQR,
|
|
4245
4735
|
extractOfflineClaimSmsToken,
|
|
4736
|
+
extractOfflineSmsSettleToken,
|
|
4246
4737
|
formatAmount,
|
|
4247
4738
|
generateDynamicQR,
|
|
4248
4739
|
generateStaticQR,
|
|
4249
4740
|
init,
|
|
4741
|
+
isConsumerPaymentRequestExpired,
|
|
4250
4742
|
isHardenedArtifactType,
|
|
4251
4743
|
isKnownArtifactType,
|
|
4252
4744
|
isPassWithinValidity,
|
|
@@ -4259,6 +4751,7 @@ export {
|
|
|
4259
4751
|
routingHint,
|
|
4260
4752
|
signArtifact,
|
|
4261
4753
|
signAuthorization,
|
|
4754
|
+
signConsumerPaymentRequest,
|
|
4262
4755
|
signOAC,
|
|
4263
4756
|
signPartnerRequest,
|
|
4264
4757
|
signPass,
|
|
@@ -4270,7 +4763,11 @@ export {
|
|
|
4270
4763
|
verifyArtifactUri,
|
|
4271
4764
|
verifyAuthorization,
|
|
4272
4765
|
verifyClaimSignature,
|
|
4766
|
+
verifyConsumerPaymentRequest,
|
|
4767
|
+
verifyConsumerSettlement,
|
|
4768
|
+
verifyConsumerSettlementReceiptQR,
|
|
4273
4769
|
verifyOAC,
|
|
4770
|
+
verifyOfflineSmsSettleToken,
|
|
4274
4771
|
verifyPass,
|
|
4275
4772
|
verifyPaymentRequest,
|
|
4276
4773
|
verifyReceipt,
|