@nokinc-flur/sdk 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -44,6 +44,8 @@ __export(index_exports, {
44
44
  CLAIM_DOMAIN_V2: () => CLAIM_DOMAIN_V2,
45
45
  COLLECTION_INTENT_STATUSES: () => COLLECTION_INTENT_STATUSES,
46
46
  COLLECTION_PAYMENT_STATUSES: () => COLLECTION_PAYMENT_STATUSES,
47
+ CONSUMER_OAC_DOMAIN: () => CONSUMER_OAC_DOMAIN,
48
+ CONSUMER_OAC_QR_PREFIX: () => CONSUMER_OAC_QR_PREFIX,
47
49
  CONSUMER_OFFLINE_CLAIM_SUBMIT_GRACE_MS: () => CONSUMER_OFFLINE_CLAIM_SUBMIT_GRACE_MS,
48
50
  CONSUMER_PAYMENT_REQUEST_DOMAIN: () => CONSUMER_PAYMENT_REQUEST_DOMAIN,
49
51
  CONSUMER_SETTLEMENT_DOMAIN: () => CONSUMER_SETTLEMENT_DOMAIN,
@@ -82,6 +84,8 @@ __export(index_exports, {
82
84
  IdentityArtifactSchema: () => IdentityArtifactSchema,
83
85
  IngestFundingResultSchema: () => IngestFundingResultSchema,
84
86
  IssueAccountOacInputSchema: () => IssueAccountOacInputSchema,
87
+ IssuerTrustBundleSchema: () => IssuerTrustBundleSchema,
88
+ IssuerTrustKeySchema: () => IssuerTrustKeySchema,
85
89
  LedgerJournalEntryArtifactSchema: () => LedgerJournalEntryArtifactSchema,
86
90
  ListPayoutDestinationsResultSchema: () => ListPayoutDestinationsResultSchema,
87
91
  MEMBERSHIP_ROLES: () => MEMBERSHIP_ROLES,
@@ -185,6 +189,7 @@ __export(index_exports, {
185
189
  computeConsumerClaimEncounterId: () => computeConsumerClaimEncounterId,
186
190
  computeEncounterId: () => computeEncounterId,
187
191
  constantTimeEqual: () => constantTimeEqual,
192
+ consumerOacSigningPayload: () => consumerOacSigningPayload,
188
193
  consumerPaymentRequestSigningBytes: () => consumerPaymentRequestSigningBytes,
189
194
  consumerPaymentRequestSigningPayload: () => consumerPaymentRequestSigningPayload,
190
195
  consumerSettlementSigningPayload: () => consumerSettlementSigningPayload,
@@ -217,11 +222,13 @@ __export(index_exports, {
217
222
  decodeOfflineSmsSettleToken: () => decodeOfflineSmsSettleToken,
218
223
  decodePayCardArtifact: () => decodePayCardArtifact,
219
224
  decodePaymentRequestQR: () => decodePaymentRequestQR,
225
+ decodeUnverifiedConsumerOacQR: () => decodeUnverifiedConsumerOacQR,
220
226
  decodeUnverifiedConsumerSettlementReceiptQR: () => decodeUnverifiedConsumerSettlementReceiptQR,
221
227
  derToRawP256Signature: () => derToRawP256Signature,
222
228
  encodeArtifactUri: () => encodeArtifactUri,
223
229
  encodeAuthorizationQR: () => encodeAuthorizationQR,
224
230
  encodeBase45: () => encodeBase45,
231
+ encodeConsumerOacQR: () => encodeConsumerOacQR,
225
232
  encodeConsumerSettlementReceiptQR: () => encodeConsumerSettlementReceiptQR,
226
233
  encodeNQR: () => encodeNQR,
227
234
  encodeOfflineClaimSmsMessage: () => encodeOfflineClaimSmsMessage,
@@ -234,6 +241,7 @@ __export(index_exports, {
234
241
  generateStaticQR: () => generateStaticQR,
235
242
  init: () => init,
236
243
  inspectPayCardFreshness: () => inspectPayCardFreshness,
244
+ isConsumerOacQR: () => isConsumerOacQR,
237
245
  isConsumerPaymentRequestExpired: () => isConsumerPaymentRequestExpired,
238
246
  isHardenedArtifactType: () => isHardenedArtifactType,
239
247
  isKnownArtifactType: () => isKnownArtifactType,
@@ -264,6 +272,7 @@ __export(index_exports, {
264
272
  verifyConsumerSettlement: () => verifyConsumerSettlement,
265
273
  verifyConsumerSettlementReceiptQR: () => verifyConsumerSettlementReceiptQR,
266
274
  verifyOAC: () => verifyOAC,
275
+ verifyOacOffline: () => verifyOacOffline,
267
276
  verifyOfflineSmsSettleToken: () => verifyOfflineSmsSettleToken,
268
277
  verifyPass: () => verifyPass,
269
278
  verifyPayCardArtifact: () => verifyPayCardArtifact,
@@ -3238,7 +3247,17 @@ var Base64Std3 = import_zod13.z.string().regex(/^[A-Za-z0-9+/]+={0,2}$/);
3238
3247
  var ClaimNonce = import_zod13.z.string().min(8).max(128).refine((value) => !value.includes("|"), {
3239
3248
  message: "nonce must not contain |"
3240
3249
  });
3241
- var ACCOUNT_FUNDED_OAC_MAX_TTL_MS = 1e3 * 60 * 60 * 24 * 7;
3250
+ var ACCOUNT_FUNDED_OAC_MAX_TTL_MS = 1e3 * 60 * 60 * 24;
3251
+ var IssuerTrustKeySchema = import_zod13.z.object({
3252
+ issuerId: import_zod13.z.string().min(1).max(128),
3253
+ alg: import_zod13.z.literal("p256"),
3254
+ publicKeySpkiB64: Base64Std3.min(64).max(4096),
3255
+ notBeforeMs: import_zod13.z.number().int().nonnegative().optional(),
3256
+ notAfterMs: import_zod13.z.number().int().positive().optional()
3257
+ });
3258
+ var IssuerTrustBundleSchema = import_zod13.z.object({
3259
+ keys: import_zod13.z.array(IssuerTrustKeySchema).min(1)
3260
+ });
3242
3261
  var CONSUMER_OFFLINE_CLAIM_SUBMIT_GRACE_MS = 1e3 * 60 * 60 * 24;
3243
3262
  var AttestationSecurityLevelSchema = import_zod13.z.enum([
3244
3263
  "STRONGBOX",
@@ -3292,13 +3311,28 @@ var ConsumerOACSchema = import_zod13.z.object({
3292
3311
  alg: import_zod13.z.literal("p256"),
3293
3312
  /** P-256 SubjectPublicKeyInfo DER, base64. */
3294
3313
  devicePubkeySpkiB64: Base64Std3.min(64).max(4096),
3295
- perTxCapKobo: import_zod13.z.number().int().positive(),
3296
- cumulativeCapKobo: import_zod13.z.number().int().positive(),
3314
+ /**
3315
+ * Per-transaction / cumulative offline spend ceilings. Zero is valid and
3316
+ * denotes an identity-only OAC: the credential proves who the holder is and
3317
+ * binds their device key for offline-verifiable first contact, but carries
3318
+ * no offline spend authority (every claim against it routes to REVIEW).
3319
+ * Issued to zero-balance wallets so they can still be paid offline.
3320
+ */
3321
+ perTxCapKobo: import_zod13.z.number().int().nonnegative(),
3322
+ cumulativeCapKobo: import_zod13.z.number().int().nonnegative(),
3297
3323
  currency: import_zod13.z.string().length(3),
3298
3324
  validFromMs: import_zod13.z.number().int().nonnegative(),
3299
3325
  validUntilMs: import_zod13.z.number().int().nonnegative(),
3300
3326
  counterSeed: import_zod13.z.number().int().nonnegative(),
3301
- issuedAtMs: import_zod13.z.number().int().nonnegative()
3327
+ issuedAtMs: import_zod13.z.number().int().nonnegative(),
3328
+ /**
3329
+ * Issuer-attested identity folded into the OAC so a single signed
3330
+ * credential serves both Tier B offline-verifiable identity and offline
3331
+ * spend authority. Verified offline against a pinned issuer key; the
3332
+ * backend remains authoritative at settlement.
3333
+ */
3334
+ phoneE164: import_zod13.z.string().regex(/^\+[1-9]\d{7,14}$/),
3335
+ displayName: import_zod13.z.string().min(1).max(64)
3302
3336
  });
3303
3337
  var SignedConsumerOACSchema = import_zod13.z.object({
3304
3338
  oac: ConsumerOACSchema,
@@ -3466,6 +3500,12 @@ function createMeOfflineClient(opts) {
3466
3500
  `/v1/me/offline/settlements/${encodeURIComponent(idOrKey)}`,
3467
3501
  void 0,
3468
3502
  (raw) => ConsumerSettlementSchema.parse(raw)
3503
+ ),
3504
+ getIssuerKeys: () => call(
3505
+ "GET",
3506
+ "/v1/issuer/keys",
3507
+ void 0,
3508
+ (raw) => IssuerTrustBundleSchema.parse(raw)
3469
3509
  )
3470
3510
  };
3471
3511
  }
@@ -3826,13 +3866,101 @@ function base64UrlDecodeUtf8(input) {
3826
3866
  throw new Error("base64 decoder unavailable");
3827
3867
  }
3828
3868
 
3869
+ // src/me-offline/oac.ts
3870
+ var CONSUMER_OAC_DOMAIN = "flur:consumer-offline:v1:oac";
3871
+ function consumerOacSigningPayload(oac) {
3872
+ return { domain: CONSUMER_OAC_DOMAIN, ...oac };
3873
+ }
3874
+ function verifyOacOffline(signed, trustedKeys, options = {}) {
3875
+ const parsed = SignedConsumerOACSchema.safeParse(signed);
3876
+ if (!parsed.success) return { ok: false, reason: "malformed" };
3877
+ const oacParsed = ConsumerOACSchema.safeParse(parsed.data.oac);
3878
+ if (!oacParsed.success) return { ok: false, reason: "malformed" };
3879
+ const oac = oacParsed.data;
3880
+ const nowMs = options.nowMs ?? Date.now();
3881
+ const pinned = trustedKeys.filter(
3882
+ (k) => k.issuerId === oac.issuerId && (k.notBeforeMs === void 0 || nowMs >= k.notBeforeMs) && (k.notAfterMs === void 0 || nowMs <= k.notAfterMs)
3883
+ );
3884
+ if (pinned.length === 0) return { ok: false, reason: "untrusted_issuer" };
3885
+ const signingBytes = canonicalJSONBytes(consumerOacSigningPayload(oac));
3886
+ const signatureOk = pinned.some(
3887
+ (k) => verifyIssuerP256(signingBytes, parsed.data.issuerSig, k.publicKeySpkiB64)
3888
+ );
3889
+ if (!signatureOk) return { ok: false, reason: "signature_invalid" };
3890
+ if (oac.validUntilMs - oac.validFromMs > ACCOUNT_FUNDED_OAC_MAX_TTL_MS) {
3891
+ return { ok: false, reason: "window_too_long" };
3892
+ }
3893
+ if (nowMs < oac.validFromMs) return { ok: false, reason: "not_yet_valid" };
3894
+ if (nowMs >= oac.validUntilMs) return { ok: false, reason: "expired" };
3895
+ return {
3896
+ ok: true,
3897
+ oac,
3898
+ identity: {
3899
+ oacId: oac.oacId,
3900
+ issuerId: oac.issuerId,
3901
+ userId: oac.userId,
3902
+ phoneE164: oac.phoneE164,
3903
+ displayName: oac.displayName,
3904
+ devicePubkeySpkiB64: oac.devicePubkeySpkiB64
3905
+ }
3906
+ };
3907
+ }
3908
+ var CONSUMER_OAC_QR_PREFIX = "FLUROAC1.";
3909
+ function isConsumerOacQR(value) {
3910
+ return value.startsWith(CONSUMER_OAC_QR_PREFIX);
3911
+ }
3912
+ function encodeConsumerOacQR(signed) {
3913
+ const parsed = SignedConsumerOACSchema.parse(signed);
3914
+ return `${CONSUMER_OAC_QR_PREFIX}${base64UrlEncodeUtf82(JSON.stringify(parsed))}`;
3915
+ }
3916
+ function decodeUnverifiedConsumerOacQR(value) {
3917
+ if (!value.startsWith(CONSUMER_OAC_QR_PREFIX)) {
3918
+ throw new Error("not a Flur consumer OAC QR");
3919
+ }
3920
+ const encoded = value.slice(CONSUMER_OAC_QR_PREFIX.length);
3921
+ let raw;
3922
+ try {
3923
+ raw = JSON.parse(base64UrlDecodeUtf82(encoded));
3924
+ } catch {
3925
+ throw new Error("consumer OAC QR is malformed");
3926
+ }
3927
+ return SignedConsumerOACSchema.parse(raw);
3928
+ }
3929
+ function base64UrlEncodeUtf82(input) {
3930
+ const bytes = new TextEncoder().encode(input);
3931
+ let binary = "";
3932
+ for (const byte of bytes) binary += String.fromCharCode(byte);
3933
+ const base64 = typeof btoa === "function" ? btoa(binary) : typeof Buffer !== "undefined" ? Buffer.from(bytes).toString("base64") : void 0;
3934
+ if (!base64) throw new Error("base64 encoder unavailable");
3935
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
3936
+ }
3937
+ function base64UrlDecodeUtf82(input) {
3938
+ const base64 = input.replace(/-/g, "+").replace(/_/g, "/");
3939
+ const padded = base64.padEnd(
3940
+ base64.length + (4 - base64.length % 4) % 4,
3941
+ "="
3942
+ );
3943
+ if (typeof atob === "function") {
3944
+ const binary = atob(padded);
3945
+ const bytes = new Uint8Array(binary.length);
3946
+ for (let index = 0; index < binary.length; index++) {
3947
+ bytes[index] = binary.charCodeAt(index);
3948
+ }
3949
+ return new TextDecoder().decode(bytes);
3950
+ }
3951
+ if (typeof Buffer !== "undefined") {
3952
+ return Buffer.from(padded, "base64").toString("utf8");
3953
+ }
3954
+ throw new Error("base64 decoder unavailable");
3955
+ }
3956
+
3829
3957
  // src/me-offline/sms.ts
3830
3958
  var import_nist3 = require("@noble/curves/nist");
3831
3959
  var OFFLINE_CLAIM_SMS_PREFIX = "FLURC1.";
3832
3960
  var CLAIM_TOKEN_RE = /(?:^|\s)(FLURC1\.[A-Za-z0-9_-]+={0,2})(?:\s|$)/;
3833
3961
  function encodeOfflineClaimSmsMessage(claim) {
3834
3962
  const parsed = ConsumerPaymentClaimSchema.parse(claim);
3835
- return `${OFFLINE_CLAIM_SMS_PREFIX}${base64UrlEncodeUtf82(
3963
+ return `${OFFLINE_CLAIM_SMS_PREFIX}${base64UrlEncodeUtf83(
3836
3964
  JSON.stringify(parsed)
3837
3965
  )}`;
3838
3966
  }
@@ -3842,7 +3970,7 @@ function decodeOfflineClaimSmsMessage(message) {
3842
3970
  const encoded = token.slice(OFFLINE_CLAIM_SMS_PREFIX.length);
3843
3971
  let raw;
3844
3972
  try {
3845
- raw = JSON.parse(base64UrlDecodeUtf82(encoded));
3973
+ raw = JSON.parse(base64UrlDecodeUtf83(encoded));
3846
3974
  } catch {
3847
3975
  throw new Error("offline claim QR token is malformed");
3848
3976
  }
@@ -4068,10 +4196,10 @@ function bytesToBase64Url(bytes) {
4068
4196
  }
4069
4197
  return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
4070
4198
  }
4071
- function base64UrlEncodeUtf82(input) {
4199
+ function base64UrlEncodeUtf83(input) {
4072
4200
  return bytesToBase64Url(utf8(input));
4073
4201
  }
4074
- function base64UrlDecodeUtf82(input) {
4202
+ function base64UrlDecodeUtf83(input) {
4075
4203
  return new TextDecoder().decode(base64UrlToBytes(input));
4076
4204
  }
4077
4205
  function base64UrlToBytes(input) {
@@ -4969,6 +5097,8 @@ function buildPayCardSigningInput(input) {
4969
5097
  CLAIM_DOMAIN_V2,
4970
5098
  COLLECTION_INTENT_STATUSES,
4971
5099
  COLLECTION_PAYMENT_STATUSES,
5100
+ CONSUMER_OAC_DOMAIN,
5101
+ CONSUMER_OAC_QR_PREFIX,
4972
5102
  CONSUMER_OFFLINE_CLAIM_SUBMIT_GRACE_MS,
4973
5103
  CONSUMER_PAYMENT_REQUEST_DOMAIN,
4974
5104
  CONSUMER_SETTLEMENT_DOMAIN,
@@ -5007,6 +5137,8 @@ function buildPayCardSigningInput(input) {
5007
5137
  IdentityArtifactSchema,
5008
5138
  IngestFundingResultSchema,
5009
5139
  IssueAccountOacInputSchema,
5140
+ IssuerTrustBundleSchema,
5141
+ IssuerTrustKeySchema,
5010
5142
  LedgerJournalEntryArtifactSchema,
5011
5143
  ListPayoutDestinationsResultSchema,
5012
5144
  MEMBERSHIP_ROLES,
@@ -5110,6 +5242,7 @@ function buildPayCardSigningInput(input) {
5110
5242
  computeConsumerClaimEncounterId,
5111
5243
  computeEncounterId,
5112
5244
  constantTimeEqual,
5245
+ consumerOacSigningPayload,
5113
5246
  consumerPaymentRequestSigningBytes,
5114
5247
  consumerPaymentRequestSigningPayload,
5115
5248
  consumerSettlementSigningPayload,
@@ -5142,11 +5275,13 @@ function buildPayCardSigningInput(input) {
5142
5275
  decodeOfflineSmsSettleToken,
5143
5276
  decodePayCardArtifact,
5144
5277
  decodePaymentRequestQR,
5278
+ decodeUnverifiedConsumerOacQR,
5145
5279
  decodeUnverifiedConsumerSettlementReceiptQR,
5146
5280
  derToRawP256Signature,
5147
5281
  encodeArtifactUri,
5148
5282
  encodeAuthorizationQR,
5149
5283
  encodeBase45,
5284
+ encodeConsumerOacQR,
5150
5285
  encodeConsumerSettlementReceiptQR,
5151
5286
  encodeNQR,
5152
5287
  encodeOfflineClaimSmsMessage,
@@ -5159,6 +5294,7 @@ function buildPayCardSigningInput(input) {
5159
5294
  generateStaticQR,
5160
5295
  init,
5161
5296
  inspectPayCardFreshness,
5297
+ isConsumerOacQR,
5162
5298
  isConsumerPaymentRequestExpired,
5163
5299
  isHardenedArtifactType,
5164
5300
  isKnownArtifactType,
@@ -5189,6 +5325,7 @@ function buildPayCardSigningInput(input) {
5189
5325
  verifyConsumerSettlement,
5190
5326
  verifyConsumerSettlementReceiptQR,
5191
5327
  verifyOAC,
5328
+ verifyOacOffline,
5192
5329
  verifyOfflineSmsSettleToken,
5193
5330
  verifyPass,
5194
5331
  verifyPayCardArtifact,