@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.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: z13.string().min(8).max(128),
3054
- payeeNonce: z13.string().min(8).max(128),
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
- createdAtMs: z13.number().int().nonnegative()
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/sms.ts
3291
- var OFFLINE_CLAIM_SMS_PREFIX = "FLURC1.";
3292
- var TOKEN_RE = /(?:^|\s)(FLURC1\.[A-Za-z0-9_-]+={0,2})(?:\s|$)/;
3293
- function encodeOfflineClaimSmsMessage(claim) {
3294
- const parsed = ConsumerPaymentClaimSchema.parse(claim);
3295
- const json = JSON.stringify(parsed);
3296
- return `${OFFLINE_CLAIM_SMS_PREFIX}${base64UrlEncodeUtf8(json)}`;
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 decodeOfflineClaimSmsMessage(message) {
3299
- const token = extractOfflineClaimSmsToken(message);
3300
- if (!token) {
3301
- throw new Error("offline claim SMS token not found");
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 encoded = token.slice(OFFLINE_CLAIM_SMS_PREFIX.length);
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("offline claim SMS token is malformed");
3507
+ throw new Error("consumer settlement receipt QR is malformed");
3309
3508
  }
3310
- const parsed = ConsumerPaymentClaimSchema.safeParse(raw);
3311
- if (!parsed.success) {
3312
- throw new Error("offline claim SMS token is invalid");
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 parsed.data;
3516
+ return settlement;
3315
3517
  }
3316
- function extractOfflineClaimSmsToken(message) {
3317
- const trimmed = message.trim();
3318
- if (trimmed.startsWith(OFFLINE_CLAIM_SMS_PREFIX)) {
3319
- return trimmed.split(/\s+/, 1)[0] ?? null;
3518
+ function decodeConsumerSettlementReceiptQR(value, issuerPublicKeySpkiB64) {
3519
+ if (!issuerPublicKeySpkiB64) {
3520
+ return decodeUnverifiedConsumerSettlementReceiptQR(value);
3320
3521
  }
3321
- return TOKEN_RE.exec(message)?.[1] ?? null;
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 z14 } from "zod";
3353
- var MinorString = z14.string().regex(/^-?\d+$/);
3354
- var PositiveMinor = z14.union([
3355
- z14.number().int().positive(),
3356
- z14.string().regex(/^[1-9]\d{0,18}$/)
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 = z14.string().trim().length(3).transform((v) => v.toUpperCase());
3359
- var Metadata = z14.record(z14.unknown());
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 = z14.object({
3387
- partnerAccountId: z14.string().uuid(),
3388
- kind: z14.enum(PARTNER_KINDS),
3389
- custodialMode: z14.enum(CUSTODIAL_MODES),
3390
- displayName: z14.string(),
3391
- bankCode: z14.string().nullable(),
3392
- poolAccountNumber: z14.string().nullable(),
3393
- status: z14.enum(PARTNER_PROFILE_STATUSES),
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: z14.number().int().nonnegative(),
3396
- updatedAtMs: z14.number().int().nonnegative()
3862
+ createdAtMs: z15.number().int().nonnegative(),
3863
+ updatedAtMs: z15.number().int().nonnegative()
3397
3864
  });
3398
- var UpsertPartnerProfileInputSchema = z14.object({
3399
- kind: z14.enum(PARTNER_KINDS),
3400
- custodialMode: z14.enum(CUSTODIAL_MODES),
3401
- displayName: z14.string().trim().min(1).max(200),
3402
- bankCode: z14.string().trim().min(1).max(64).optional(),
3403
- poolAccountNumber: z14.string().trim().min(1).max(64).optional(),
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 = z14.object({
3407
- externalRef: z14.string().trim().min(8).max(128),
3408
- direction: z14.enum(PARTNER_FUNDING_DIRECTIONS).optional(),
3409
- userId: z14.string().uuid().optional(),
3410
- accountId: z14.string().uuid().optional(),
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: z14.string().trim().min(1).max(64).optional(),
3880
+ fundingSource: z15.string().trim().min(1).max(64).optional(),
3414
3881
  providerMetadata: Metadata.optional()
3415
3882
  });
3416
- var PartnerFundingSchema = z14.object({
3417
- fundingId: z14.string().uuid(),
3418
- partnerId: z14.string().uuid(),
3419
- accountId: z14.string().uuid(),
3420
- userId: z14.string().uuid().nullable(),
3421
- direction: z14.enum(PARTNER_FUNDING_DIRECTIONS),
3422
- currency: z14.string(),
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: z14.string(),
3425
- status: z14.enum(PARTNER_FUNDING_STATUSES),
3426
- fundingSource: z14.string(),
3427
- ledgerRef: z14.string(),
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: z14.number().int().nonnegative(),
3430
- updatedAtMs: z14.number().int().nonnegative()
3896
+ createdAtMs: z15.number().int().nonnegative(),
3897
+ updatedAtMs: z15.number().int().nonnegative()
3431
3898
  });
3432
- var IngestFundingResultSchema = z14.object({
3899
+ var IngestFundingResultSchema = z15.object({
3433
3900
  funding: PartnerFundingSchema,
3434
- replayed: z14.boolean()
3901
+ replayed: z15.boolean()
3435
3902
  });
3436
- var PayoutDestinationSchema = z14.object({
3437
- destinationId: z14.string().uuid(),
3438
- accountId: z14.string().uuid(),
3439
- partnerId: z14.string().uuid(),
3440
- bankCode: z14.string(),
3441
- accountNumber: z14.string(),
3442
- accountName: z14.string(),
3443
- status: z14.enum(PAYOUT_DESTINATION_STATUSES),
3444
- verifiedAtMs: z14.number().int().nonnegative().nullable(),
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: z14.number().int().nonnegative(),
3447
- updatedAtMs: z14.number().int().nonnegative()
3913
+ createdAtMs: z15.number().int().nonnegative(),
3914
+ updatedAtMs: z15.number().int().nonnegative()
3448
3915
  });
3449
- var CreatePayoutDestinationInputSchema = z14.object({
3450
- partnerId: z14.string().uuid(),
3451
- bankCode: z14.string().trim().min(1).max(32),
3452
- accountNumber: z14.string().trim().min(4).max(64),
3453
- accountName: z14.string().trim().min(1).max(200),
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 = z14.object({
3457
- items: z14.array(PayoutDestinationSchema)
3923
+ var ListPayoutDestinationsResultSchema = z15.object({
3924
+ items: z15.array(PayoutDestinationSchema)
3458
3925
  });
3459
- var WithdrawalSchema = z14.object({
3460
- withdrawalId: z14.string().uuid(),
3461
- accountId: z14.string().uuid(),
3462
- userId: z14.string().uuid(),
3463
- partnerId: z14.string().uuid(),
3464
- destinationId: z14.string().uuid(),
3465
- currency: z14.string(),
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: z14.enum(WITHDRAWAL_STATES),
3468
- idempotencyKey: z14.string(),
3469
- providerRef: z14.string().nullable(),
3470
- lastError: z14.string().nullable(),
3471
- ledgerRef: z14.string(),
3472
- reverseLedgerRef: z14.string().nullable(),
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: z14.number().int().nonnegative(),
3475
- updatedAtMs: z14.number().int().nonnegative()
3941
+ createdAtMs: z15.number().int().nonnegative(),
3942
+ updatedAtMs: z15.number().int().nonnegative()
3476
3943
  });
3477
- var CreateWithdrawalInputSchema = z14.object({
3478
- destinationId: z14.string().uuid(),
3944
+ var CreateWithdrawalInputSchema = z15.object({
3945
+ destinationId: z15.string().uuid(),
3479
3946
  amountMinor: PositiveMinor,
3480
3947
  currency: Currency,
3481
- idempotencyKey: z14.string().trim().min(8).max(128),
3948
+ idempotencyKey: z15.string().trim().min(8).max(128),
3482
3949
  metadata: Metadata.optional()
3483
3950
  });
3484
- var CreateWithdrawalResultSchema = z14.object({
3951
+ var CreateWithdrawalResultSchema = z15.object({
3485
3952
  withdrawal: WithdrawalSchema,
3486
- replayed: z14.boolean()
3953
+ replayed: z15.boolean()
3487
3954
  });
3488
- var PayoutEventInputSchema = z14.object({
3489
- externalRef: z14.string().trim().min(8).max(128),
3490
- withdrawalId: z14.string().uuid().optional(),
3491
- state: z14.enum(["submitted", "processing", "paid", "failed"]),
3492
- providerRef: z14.string().trim().min(1).max(128).optional(),
3493
- failureCode: z14.string().trim().max(64).optional(),
3494
- failureMessage: z14.string().trim().max(512).optional(),
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 = z14.object({
3964
+ var RecordPayoutEventResultSchema = z15.object({
3498
3965
  withdrawal: WithdrawalSchema,
3499
- replayed: z14.boolean()
3966
+ replayed: z15.boolean()
3500
3967
  });
3501
- var ReconciliationReportSchema = z14.object({
3502
- partnerId: z14.string().uuid(),
3503
- currency: z14.string(),
3504
- fromMs: z14.number().int().nonnegative(),
3505
- toMs: z14.number().int().nonnegative(),
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: z14.number().int().nonnegative()
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 z15 } from "zod";
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 = z15.object({
3672
- v: z15.literal(FLUR_ARTIFACT_VERSION),
3673
- t: z15.string().regex(ArtifactTypeRe, "invalid artifact type"),
3674
- iss: z15.string().min(1).max(128),
3675
- kid: z15.string().min(1).max(128),
3676
- iat: z15.number().int().nonnegative(),
3677
- exp: z15.number().int().positive().optional(),
3678
- nonce: z15.string().min(8).max(64).regex(/^[A-Za-z0-9_-]+$/, "nonce must be url-safe")
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 z16 } from "zod";
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) => z16.string().regex(
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 = z16.object({
4306
+ var OfflinePaymentAuthorizationArtifactSchema = z17.object({
3837
4307
  authorization: OfflinePaymentAuthorizationSchema
3838
4308
  });
3839
- var ReceiptArtifactSchema = z16.object({
3840
- receiptId: z16.string().min(1).max(64),
3841
- paymentReference: z16.string().min(1).max(64),
3842
- payerUserId: z16.string().min(1).max(64).optional(),
3843
- payeeUserId: z16.string().min(1).max(64),
3844
- amountKobo: z16.number().int().positive(),
3845
- currency: z16.literal("NGN"),
3846
- channel: z16.enum(["online", "offline_reconciled", "pay_link", "nqr"]),
3847
- settledAtMs: z16.number().int().positive(),
3848
- ledgerTxnId: z16.string().min(1).max(64).optional(),
3849
- memo: z16.string().max(140).optional(),
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 = z16.string().min(1).max(64);
3853
- var PositiveInt = z16.number().int().positive();
3854
- var NonNegativeInt = z16.number().int().nonnegative();
3855
- var Currency2 = z16.literal("NGN");
3856
- var Memo = z16.string().max(140);
3857
- var NqrPaymentRequestArtifactSchema = z16.object({
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 = z16.object({
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 = z16.object({
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 = z16.object({
4353
+ var SettlementRecordArtifactSchema = z17.object({
3884
4354
  settlementId: ShortId,
3885
4355
  ledgerTxnId: ShortId,
3886
- sourceRefType: z16.enum([
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 = z16.object({
4367
+ var ReversalRecordArtifactSchema = z17.object({
3898
4368
  reversalId: ShortId,
3899
4369
  originalTxnId: ShortId,
3900
4370
  amountKobo: PositiveInt,
3901
4371
  currency: Currency2,
3902
- reason: z16.enum([
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 = z16.object({
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 = z16.object({
4393
+ var StatementArtifactSchema = z17.object({
3924
4394
  statementId: ShortId,
3925
4395
  userId: ShortId,
3926
4396
  periodStartMs: PositiveInt,
3927
4397
  periodEndMs: PositiveInt,
3928
- openingBalanceKobo: z16.number().int(),
3929
- closingBalanceKobo: z16.number().int(),
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 = z16.object({
4407
+ var PassArtifactSchema = z17.object({
3938
4408
  passId: ShortId,
3939
4409
  holderId: ShortId,
3940
- category: z16.enum(["membership", "ticket", "loyalty", "access", "voucher"]),
3941
- title: z16.string().min(1).max(120),
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: z16.record(
3945
- z16.string().min(1).max(64),
3946
- z16.union([z16.string().max(280), z16.number(), z16.boolean()])
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 = z16.object({
4425
+ var IdentityArtifactSchema = z17.object({
3956
4426
  attestationId: ShortId,
3957
4427
  subjectId: ShortId,
3958
- claimType: z16.enum([
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,