@nokinc-flur/sdk 2.2.0 → 2.3.0

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