@nokinc-flur/sdk 0.1.7 → 1.0.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
@@ -225,6 +225,39 @@ var FlurError = class extends Error {
225
225
  this.reqId = opts?.reqId;
226
226
  }
227
227
  };
228
+ var FlurApiError = class extends Error {
229
+ constructor(status, code, message, raw) {
230
+ super(message);
231
+ this.status = status;
232
+ this.code = code;
233
+ this.raw = raw;
234
+ this.name = "FlurApiError";
235
+ }
236
+ status;
237
+ code;
238
+ raw;
239
+ };
240
+ var FlurExpiredError = class extends Error {
241
+ code = "PASS_EXPIRED";
242
+ constructor(message = "pass is expired") {
243
+ super(message);
244
+ this.name = "FlurExpiredError";
245
+ }
246
+ };
247
+ var FlurReplayError = class extends Error {
248
+ code = "PASS_REPLAY";
249
+ constructor(message = "redemption counter is not strictly increasing") {
250
+ super(message);
251
+ this.name = "FlurReplayError";
252
+ }
253
+ };
254
+ var FlurCapExceededError = class extends Error {
255
+ code = "PASS_CAP_EXCEEDED";
256
+ constructor(message = "redemption exceeds cumulative cap") {
257
+ super(message);
258
+ this.name = "FlurCapExceededError";
259
+ }
260
+ };
228
261
  async function mapToFlurError(res) {
229
262
  const reqId = res.headers.get("x-request-id");
230
263
  let details = void 0;
@@ -248,7 +281,11 @@ async function mapToFlurError(res) {
248
281
  }
249
282
  } catch {
250
283
  }
251
- return new FlurError(mappedMessage, mappedCode, { status: res.status, details, reqId });
284
+ return new FlurError(mappedMessage, mappedCode, {
285
+ status: res.status,
286
+ details,
287
+ reqId
288
+ });
252
289
  }
253
290
 
254
291
  // src/primitives.ts
@@ -665,13 +702,13 @@ var FlurClient = class {
665
702
  ResolvePayLinkResponseSchema
666
703
  );
667
704
  }
668
- async requestJson(path, init, requestSchema, responseSchema, input) {
705
+ async requestJson(path, init2, requestSchema, responseSchema, input) {
669
706
  const url = `${this.baseUrl}${path}`;
670
707
  const controller = new AbortController();
671
708
  const t = setTimeout(() => controller.abort(), this.timeoutMs);
672
- let body = init.body;
709
+ let body = init2.body;
673
710
  try {
674
- body = requestSchema ? JSON.stringify(requestSchema.parse(input)) : init.body;
711
+ body = requestSchema ? JSON.stringify(requestSchema.parse(input)) : init2.body;
675
712
  } catch (err) {
676
713
  if (err instanceof z3.ZodError) {
677
714
  throw new FlurError("Invalid request payload", "INVALID_REQUEST", {
@@ -686,10 +723,10 @@ var FlurClient = class {
686
723
  extraHeaders = await this.getExtraHeaders();
687
724
  }
688
725
  const finalHeaders = {
689
- ...init.headers,
726
+ ...init2.headers,
690
727
  ...extraHeaders
691
728
  };
692
- const res = await this.fetchImpl(url, { ...init, headers: finalHeaders, body, signal: controller.signal });
729
+ const res = await this.fetchImpl(url, { ...init2, headers: finalHeaders, body, signal: controller.signal });
693
730
  if (!res.ok) throw await mapToFlurError(res);
694
731
  const payload = await res.json();
695
732
  if (!responseSchema) return payload;
@@ -720,12 +757,1427 @@ function getSecureRandomUuid() {
720
757
  }
721
758
  throw new FlurError("Secure UUID generator unavailable; provide idempotencyKey", "INVALID_REQUEST");
722
759
  }
760
+
761
+ // src/nqr/fields.ts
762
+ var FIELD = {
763
+ PAYLOAD_FORMAT_INDICATOR: "00",
764
+ POINT_OF_INITIATION: "01",
765
+ // 02..51 — Merchant Account Information templates (issuer-specific)
766
+ MERCHANT_CATEGORY_CODE: "52",
767
+ TRANSACTION_CURRENCY: "53",
768
+ TRANSACTION_AMOUNT: "54",
769
+ TIP_OR_CONVENIENCE_INDICATOR: "55",
770
+ VALUE_CONVENIENCE_FEE_FIXED: "56",
771
+ VALUE_CONVENIENCE_FEE_PERCENT: "57",
772
+ COUNTRY_CODE: "58",
773
+ MERCHANT_NAME: "59",
774
+ MERCHANT_CITY: "60",
775
+ POSTAL_CODE: "61",
776
+ ADDITIONAL_DATA_FIELD: "62",
777
+ CRC: "63",
778
+ MERCHANT_INFO_LANGUAGE: "64"
779
+ // 80..99 — Unreserved Templates
780
+ };
781
+ var ADDITIONAL_DATA_SUBFIELD = {
782
+ BILL_NUMBER: "01",
783
+ MOBILE_NUMBER: "02",
784
+ STORE_LABEL: "03",
785
+ LOYALTY_NUMBER: "04",
786
+ REFERENCE_LABEL: "05",
787
+ CUSTOMER_LABEL: "06",
788
+ TERMINAL_LABEL: "07",
789
+ PURPOSE_OF_TRANSACTION: "08"
790
+ };
791
+ var POINT_OF_INITIATION = {
792
+ STATIC: "11",
793
+ DYNAMIC: "12"
794
+ };
795
+ var PAYLOAD_FORMAT_INDICATOR_VALUE = "01";
796
+ var NGN_CURRENCY_CODE = "566";
797
+ var NG_COUNTRY_CODE = "NG";
798
+ var CRC_TAG_PREFIX = "6304";
799
+
800
+ // src/nqr/crc16.ts
801
+ function crc16ccitt(bytes) {
802
+ let crc = 65535;
803
+ for (let i = 0; i < bytes.length; i++) {
804
+ crc ^= bytes[i] << 8;
805
+ for (let j = 0; j < 8; j++) {
806
+ if (crc & 32768) {
807
+ crc = crc << 1 ^ 4129;
808
+ } else {
809
+ crc <<= 1;
810
+ }
811
+ crc &= 65535;
812
+ }
813
+ }
814
+ return crc;
815
+ }
816
+ function crc16ccittHex(bytes) {
817
+ return crc16ccitt(bytes).toString(16).toUpperCase().padStart(4, "0");
818
+ }
819
+
820
+ // src/nqr/tlv.ts
821
+ function writeTLV(tag, value) {
822
+ if (!/^\d{2}$/.test(tag)) {
823
+ throw new Error(`TLV: tag must be 2 digits, got "${tag}"`);
824
+ }
825
+ if (value.length > 99) {
826
+ throw new Error(
827
+ `TLV: value for tag ${tag} exceeds 99 chars (${value.length})`
828
+ );
829
+ }
830
+ const len = value.length.toString().padStart(2, "0");
831
+ return `${tag}${len}${value}`;
832
+ }
833
+ function readTLV(buf) {
834
+ const out = [];
835
+ let i = 0;
836
+ while (i < buf.length) {
837
+ if (i + 4 > buf.length) {
838
+ throw new Error(`TLV: truncated header at offset ${i}`);
839
+ }
840
+ const tag = buf.slice(i, i + 2);
841
+ const lenStr = buf.slice(i + 2, i + 4);
842
+ if (!/^\d{2}$/.test(tag)) {
843
+ throw new Error(`TLV: bad tag "${tag}" at offset ${i}`);
844
+ }
845
+ if (!/^\d{2}$/.test(lenStr)) {
846
+ throw new Error(`TLV: bad length "${lenStr}" at offset ${i + 2}`);
847
+ }
848
+ const len = parseInt(lenStr, 10);
849
+ const valStart = i + 4;
850
+ const valEnd = valStart + len;
851
+ if (valEnd > buf.length) {
852
+ throw new Error(
853
+ `TLV: truncated value for tag ${tag} at offset ${valStart}`
854
+ );
855
+ }
856
+ out.push({ tag, value: buf.slice(valStart, valEnd) });
857
+ i = valEnd;
858
+ }
859
+ return out;
860
+ }
861
+
862
+ // src/nqr/encoder.ts
863
+ var ASCII_PRINTABLE = /^[\x20-\x7E]*$/;
864
+ function buildMAIValue(mai) {
865
+ if (!/^(0[2-9]|[1-4]\d|5[0-1])$/.test(mai.tag)) {
866
+ throw new Error(
867
+ `encodeNQR: merchantAccountInfo.tag must be in 02..51, got "${mai.tag}"`
868
+ );
869
+ }
870
+ let out = "";
871
+ for (const c of mai.children) {
872
+ out += writeTLV(c.tag, c.value);
873
+ }
874
+ return out;
875
+ }
876
+ function buildAdditionalDataValue(ad) {
877
+ let out = "";
878
+ const map = [
879
+ ["billNumber", ADDITIONAL_DATA_SUBFIELD.BILL_NUMBER],
880
+ ["mobileNumber", ADDITIONAL_DATA_SUBFIELD.MOBILE_NUMBER],
881
+ ["storeLabel", ADDITIONAL_DATA_SUBFIELD.STORE_LABEL],
882
+ ["loyaltyNumber", ADDITIONAL_DATA_SUBFIELD.LOYALTY_NUMBER],
883
+ ["referenceLabel", ADDITIONAL_DATA_SUBFIELD.REFERENCE_LABEL],
884
+ ["customerLabel", ADDITIONAL_DATA_SUBFIELD.CUSTOMER_LABEL],
885
+ ["terminalLabel", ADDITIONAL_DATA_SUBFIELD.TERMINAL_LABEL],
886
+ ["purposeOfTransaction", ADDITIONAL_DATA_SUBFIELD.PURPOSE_OF_TRANSACTION]
887
+ ];
888
+ for (const [k, t] of map) {
889
+ const v = ad[k];
890
+ if (v !== void 0) {
891
+ out += writeTLV(t, v);
892
+ }
893
+ }
894
+ return out;
895
+ }
896
+ function assertAscii(name, v) {
897
+ if (!ASCII_PRINTABLE.test(v)) {
898
+ throw new Error(
899
+ `encodeNQR: ${name} contains non-printable-ASCII characters`
900
+ );
901
+ }
902
+ }
903
+ function assertMaxLen(name, v, max) {
904
+ if (v.length > max) {
905
+ throw new Error(`encodeNQR: ${name} length ${v.length} exceeds max ${max}`);
906
+ }
907
+ }
908
+ function assertMCC(mcc) {
909
+ if (!/^\d{4}$/.test(mcc)) {
910
+ throw new Error(
911
+ `encodeNQR: merchantCategoryCode must be 4 digits, got "${mcc}"`
912
+ );
913
+ }
914
+ }
915
+ function assertAmount(amt) {
916
+ if (!/^\d{1,10}(\.\d{1,2})?$/.test(amt)) {
917
+ throw new Error(
918
+ `encodeNQR: transactionAmount must match /^\\d{1,10}(\\.\\d{1,2})?$/, got "${amt}"`
919
+ );
920
+ }
921
+ if (amt.length > 13) {
922
+ throw new Error(
923
+ `encodeNQR: transactionAmount length ${amt.length} exceeds 13`
924
+ );
925
+ }
926
+ }
927
+ function encodeNQR(input) {
928
+ assertMCC(input.merchantCategoryCode);
929
+ assertAscii("merchantName", input.merchantName);
930
+ assertMaxLen("merchantName", input.merchantName, 25);
931
+ assertAscii("merchantCity", input.merchantCity);
932
+ assertMaxLen("merchantCity", input.merchantCity, 15);
933
+ if (input.postalCode !== void 0) {
934
+ assertAscii("postalCode", input.postalCode);
935
+ assertMaxLen("postalCode", input.postalCode, 10);
936
+ }
937
+ if (input.transactionAmount !== void 0) {
938
+ assertAmount(input.transactionAmount);
939
+ }
940
+ if (input.pointOfInitiation === "dynamic" && input.transactionAmount === void 0) {
941
+ }
942
+ let out = "";
943
+ out += writeTLV(
944
+ FIELD.PAYLOAD_FORMAT_INDICATOR,
945
+ PAYLOAD_FORMAT_INDICATOR_VALUE
946
+ );
947
+ out += writeTLV(
948
+ FIELD.POINT_OF_INITIATION,
949
+ input.pointOfInitiation === "dynamic" ? POINT_OF_INITIATION.DYNAMIC : POINT_OF_INITIATION.STATIC
950
+ );
951
+ out += writeTLV(
952
+ input.merchantAccountInfo.tag,
953
+ buildMAIValue(input.merchantAccountInfo)
954
+ );
955
+ out += writeTLV(FIELD.MERCHANT_CATEGORY_CODE, input.merchantCategoryCode);
956
+ out += writeTLV(FIELD.TRANSACTION_CURRENCY, NGN_CURRENCY_CODE);
957
+ if (input.transactionAmount !== void 0) {
958
+ out += writeTLV(FIELD.TRANSACTION_AMOUNT, input.transactionAmount);
959
+ }
960
+ out += writeTLV(FIELD.COUNTRY_CODE, NG_COUNTRY_CODE);
961
+ out += writeTLV(FIELD.MERCHANT_NAME, input.merchantName);
962
+ out += writeTLV(FIELD.MERCHANT_CITY, input.merchantCity);
963
+ if (input.postalCode !== void 0) {
964
+ out += writeTLV(FIELD.POSTAL_CODE, input.postalCode);
965
+ }
966
+ if (input.additionalData) {
967
+ const adfValue = buildAdditionalDataValue(input.additionalData);
968
+ if (adfValue.length > 0) {
969
+ out += writeTLV(FIELD.ADDITIONAL_DATA_FIELD, adfValue);
970
+ }
971
+ }
972
+ out += CRC_TAG_PREFIX;
973
+ const crc = crc16ccittHex(new TextEncoder().encode(out));
974
+ out += crc;
975
+ return out;
976
+ }
977
+
978
+ // src/nqr/parser.ts
979
+ var NQRParseError = class extends Error {
980
+ path;
981
+ constructor(message, path = "") {
982
+ super(message);
983
+ this.name = "NQRParseError";
984
+ this.path = path;
985
+ }
986
+ };
987
+ function parseNQR(payload) {
988
+ if (payload.length < 8) {
989
+ throw new NQRParseError(`payload too short (${payload.length} chars)`);
990
+ }
991
+ const crcTagAndLen = payload.slice(-8, -4);
992
+ const crcValue = payload.slice(-4);
993
+ if (crcTagAndLen !== CRC_TAG_PREFIX) {
994
+ throw new NQRParseError(
995
+ `CRC tag prefix not found (expected "6304" at end)`
996
+ );
997
+ }
998
+ const beforeCrc = payload.slice(0, -4);
999
+ const expectedCrc = crc16ccittHex(new TextEncoder().encode(beforeCrc));
1000
+ if (expectedCrc !== crcValue.toUpperCase()) {
1001
+ throw new NQRParseError(
1002
+ `CRC mismatch: payload says ${crcValue}, computed ${expectedCrc}`,
1003
+ "63"
1004
+ );
1005
+ }
1006
+ const topBuf = payload.slice(0, -8);
1007
+ const top = readTLV(topBuf);
1008
+ const get = (tag) => top.find((f) => f.tag === tag)?.value;
1009
+ const pfi = get(FIELD.PAYLOAD_FORMAT_INDICATOR);
1010
+ if (pfi !== "01")
1011
+ throw new NQRParseError(
1012
+ `payloadFormatIndicator must be "01", got "${pfi}"`,
1013
+ "00"
1014
+ );
1015
+ const poiRaw = get(FIELD.POINT_OF_INITIATION);
1016
+ const pointOfInitiation = poiRaw === "11" ? "static" : poiRaw === "12" ? "dynamic" : "unknown";
1017
+ const currency = get(FIELD.TRANSACTION_CURRENCY);
1018
+ if (currency !== NGN_CURRENCY_CODE) {
1019
+ throw new NQRParseError(
1020
+ `unsupported currency "${currency}" (expected ${NGN_CURRENCY_CODE})`,
1021
+ "53"
1022
+ );
1023
+ }
1024
+ const country = get(FIELD.COUNTRY_CODE);
1025
+ if (country !== NG_COUNTRY_CODE) {
1026
+ throw new NQRParseError(
1027
+ `unsupported country "${country}" (expected ${NG_COUNTRY_CODE})`,
1028
+ "58"
1029
+ );
1030
+ }
1031
+ const mcc = get(FIELD.MERCHANT_CATEGORY_CODE);
1032
+ if (!mcc) throw new NQRParseError("merchantCategoryCode missing", "52");
1033
+ const merchantName = get(FIELD.MERCHANT_NAME) ?? "";
1034
+ const merchantCity = get(FIELD.MERCHANT_CITY) ?? "";
1035
+ const transactionAmount = get(FIELD.TRANSACTION_AMOUNT);
1036
+ const postalCode = get(FIELD.POSTAL_CODE);
1037
+ const mais = [];
1038
+ for (const f of top) {
1039
+ const n = parseInt(f.tag, 10);
1040
+ if (n >= 2 && n <= 51) {
1041
+ const children = readTLV(f.value).map((c) => ({
1042
+ tag: c.tag,
1043
+ value: c.value
1044
+ }));
1045
+ mais.push({ tag: f.tag, children });
1046
+ }
1047
+ }
1048
+ let additionalData;
1049
+ const adfRaw = get(FIELD.ADDITIONAL_DATA_FIELD);
1050
+ if (adfRaw !== void 0) {
1051
+ additionalData = {};
1052
+ for (const c of readTLV(adfRaw)) {
1053
+ switch (c.tag) {
1054
+ case "01":
1055
+ additionalData.billNumber = c.value;
1056
+ break;
1057
+ case "02":
1058
+ additionalData.mobileNumber = c.value;
1059
+ break;
1060
+ case "03":
1061
+ additionalData.storeLabel = c.value;
1062
+ break;
1063
+ case "04":
1064
+ additionalData.loyaltyNumber = c.value;
1065
+ break;
1066
+ case "05":
1067
+ additionalData.referenceLabel = c.value;
1068
+ break;
1069
+ case "06":
1070
+ additionalData.customerLabel = c.value;
1071
+ break;
1072
+ case "07":
1073
+ additionalData.terminalLabel = c.value;
1074
+ break;
1075
+ case "08":
1076
+ additionalData.purposeOfTransaction = c.value;
1077
+ break;
1078
+ }
1079
+ }
1080
+ }
1081
+ return {
1082
+ payloadFormatIndicator: pfi,
1083
+ pointOfInitiation,
1084
+ merchantAccountInfo: mais,
1085
+ merchantCategoryCode: mcc,
1086
+ transactionCurrency: currency,
1087
+ transactionAmount,
1088
+ countryCode: country,
1089
+ merchantName,
1090
+ merchantCity,
1091
+ postalCode,
1092
+ additionalData,
1093
+ flurReference: additionalData?.referenceLabel?.startsWith("FL-") ? additionalData.referenceLabel : void 0,
1094
+ raw: payload
1095
+ };
1096
+ }
1097
+
1098
+ // src/nqr/routing.ts
1099
+ function routingHint(parsed) {
1100
+ if (parsed.flurReference && parsed.flurReference.startsWith("FL-")) {
1101
+ return "flur-onus";
1102
+ }
1103
+ if (parsed.merchantAccountInfo.length > 0) return "nibss";
1104
+ return "unknown";
1105
+ }
1106
+
1107
+ // src/crypto/canonical.ts
1108
+ function canonicalJSONStringify(value) {
1109
+ return stringify(value);
1110
+ }
1111
+ function canonicalJSONBytes(value) {
1112
+ return new TextEncoder().encode(canonicalJSONStringify(value));
1113
+ }
1114
+ function stringify(v) {
1115
+ if (v === null) return "null";
1116
+ const t = typeof v;
1117
+ if (t === "string") return JSON.stringify(v);
1118
+ if (t === "boolean") return v ? "true" : "false";
1119
+ if (t === "number") {
1120
+ if (!Number.isFinite(v)) {
1121
+ throw new Error("canonicalJSON: non-finite number not allowed");
1122
+ }
1123
+ return JSON.stringify(v);
1124
+ }
1125
+ if (t === "bigint" || t === "function" || t === "symbol" || t === "undefined") {
1126
+ throw new Error(`canonicalJSON: unsupported value type ${t}`);
1127
+ }
1128
+ if (Array.isArray(v)) {
1129
+ const parts = [];
1130
+ for (const item of v) parts.push(stringify(item));
1131
+ return "[" + parts.join(",") + "]";
1132
+ }
1133
+ if (t === "object") {
1134
+ const obj = v;
1135
+ const keys = Object.keys(obj).sort();
1136
+ const parts = [];
1137
+ for (const k of keys) {
1138
+ const val = obj[k];
1139
+ if (val === void 0) {
1140
+ throw new Error(`canonicalJSON: undefined value at key "${k}"`);
1141
+ }
1142
+ parts.push(JSON.stringify(k) + ":" + stringify(val));
1143
+ }
1144
+ return "{" + parts.join(",") + "}";
1145
+ }
1146
+ throw new Error(`canonicalJSON: unsupported value type ${t}`);
1147
+ }
1148
+
1149
+ // src/crypto/ct.ts
1150
+ function constantTimeEqual(a, b) {
1151
+ if (a.length !== b.length) return false;
1152
+ let diff = 0;
1153
+ for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
1154
+ return diff === 0;
1155
+ }
1156
+
1157
+ // src/crypto/ed25519.ts
1158
+ import { ed25519 } from "@noble/curves/ed25519";
1159
+ function generateKeyPair() {
1160
+ const privateKey = ed25519.utils.randomPrivateKey();
1161
+ const publicKey = ed25519.getPublicKey(privateKey);
1162
+ return { privateKey, publicKey };
1163
+ }
1164
+ function publicKeyFromPrivate(privateKey) {
1165
+ return ed25519.getPublicKey(privateKey);
1166
+ }
1167
+ function sign(message, privateKey) {
1168
+ return ed25519.sign(message, privateKey);
1169
+ }
1170
+ function verify(message, signature, publicKey) {
1171
+ try {
1172
+ return ed25519.verify(signature, message, publicKey);
1173
+ } catch {
1174
+ return false;
1175
+ }
1176
+ }
1177
+ function signCanonical(value, privateKey) {
1178
+ return sign(canonicalJSONBytes(value), privateKey);
1179
+ }
1180
+ function verifyCanonical(value, signature, publicKey) {
1181
+ return verify(canonicalJSONBytes(value), signature, publicKey);
1182
+ }
1183
+
1184
+ // src/offline/oac.ts
1185
+ import { z as z4 } from "zod";
1186
+ var OAC_DEFAULT_PER_TX_KOBO = 5e5;
1187
+ var OAC_DEFAULT_CUMULATIVE_KOBO = 2e6;
1188
+ var OAC_DEFAULT_VALIDITY_MS = 24 * 60 * 60 * 1e3;
1189
+ var HexString = (length) => z4.string().regex(
1190
+ new RegExp(`^[0-9a-fA-F]{${length * 2}}$`),
1191
+ `expected ${length}-byte hex string`
1192
+ );
1193
+ var OACSchema = z4.object({
1194
+ userId: z4.string().min(1),
1195
+ deviceId: z4.string().min(1),
1196
+ devicePublicKey: HexString(32),
1197
+ perTxCapKobo: z4.number().int().nonnegative(),
1198
+ cumulativeCapKobo: z4.number().int().nonnegative(),
1199
+ validFromMs: z4.number().int().nonnegative(),
1200
+ validUntilMs: z4.number().int().positive(),
1201
+ counterSeed: z4.number().int().nonnegative(),
1202
+ nonce: z4.string().min(1),
1203
+ issuerSig: HexString(64)
1204
+ }).refine((v) => v.validUntilMs > v.validFromMs, {
1205
+ message: "validUntilMs must be greater than validFromMs"
1206
+ }).refine((v) => v.perTxCapKobo <= v.cumulativeCapKobo, {
1207
+ message: "perTxCapKobo must not exceed cumulativeCapKobo"
1208
+ });
1209
+ function buildOAC(input) {
1210
+ const devicePublicKey = typeof input.devicePublicKey === "string" ? input.devicePublicKey : bytesToHex(input.devicePublicKey);
1211
+ return {
1212
+ userId: input.userId,
1213
+ deviceId: input.deviceId,
1214
+ devicePublicKey,
1215
+ perTxCapKobo: input.perTxCapKobo ?? OAC_DEFAULT_PER_TX_KOBO,
1216
+ cumulativeCapKobo: input.cumulativeCapKobo ?? OAC_DEFAULT_CUMULATIVE_KOBO,
1217
+ validFromMs: input.validFromMs,
1218
+ validUntilMs: input.validUntilMs,
1219
+ counterSeed: input.counterSeed ?? 0,
1220
+ nonce: input.nonce
1221
+ };
1222
+ }
1223
+ function signOAC(unsigned, issuerPrivateKey) {
1224
+ const issuerSig = bytesToHex(
1225
+ sign(canonicalJSONBytes(unsigned), issuerPrivateKey)
1226
+ );
1227
+ return { ...unsigned, issuerSig };
1228
+ }
1229
+ function verifyOAC(oac, issuerPublicKey) {
1230
+ try {
1231
+ const parsed = OACSchema.parse(oac);
1232
+ const { issuerSig, ...unsigned } = parsed;
1233
+ return verify(
1234
+ canonicalJSONBytes(unsigned),
1235
+ hexToBytes(issuerSig),
1236
+ issuerPublicKey
1237
+ );
1238
+ } catch {
1239
+ return false;
1240
+ }
1241
+ }
1242
+ function bytesToHex(b) {
1243
+ let s = "";
1244
+ for (let i = 0; i < b.length; i++) s += b[i].toString(16).padStart(2, "0");
1245
+ return s;
1246
+ }
1247
+ function hexToBytes(s) {
1248
+ if (s.length % 2 !== 0) throw new Error("hex: odd length");
1249
+ const out = new Uint8Array(s.length / 2);
1250
+ for (let i = 0; i < out.length; i++)
1251
+ out[i] = parseInt(s.slice(i * 2, i * 2 + 2), 16);
1252
+ return out;
1253
+ }
1254
+
1255
+ // src/offline/codec.ts
1256
+ var ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";
1257
+ var REVERSE = {};
1258
+ for (let i = 0; i < ALPHABET.length; i++) REVERSE[ALPHABET[i]] = i;
1259
+ function encodeBase45(bytes) {
1260
+ let out = "";
1261
+ let i = 0;
1262
+ for (; i + 1 < bytes.length; i += 2) {
1263
+ const x = bytes[i] << 8 | bytes[i + 1];
1264
+ const e = Math.floor(x / (45 * 45));
1265
+ const d = Math.floor(x % (45 * 45) / 45);
1266
+ const c = x % 45;
1267
+ out += ALPHABET[c] + ALPHABET[d] + ALPHABET[e];
1268
+ }
1269
+ if (i < bytes.length) {
1270
+ const x = bytes[i];
1271
+ const d = Math.floor(x / 45);
1272
+ const c = x % 45;
1273
+ out += ALPHABET[c] + ALPHABET[d];
1274
+ }
1275
+ return out;
1276
+ }
1277
+ function decodeBase45(s) {
1278
+ if (s.length === 0) return new Uint8Array(0);
1279
+ const fullChunks = Math.floor(s.length / 3);
1280
+ const tail = s.length - fullChunks * 3;
1281
+ if (tail === 1) throw new Error("base45: invalid length");
1282
+ const out = new Uint8Array(fullChunks * 2 + (tail === 2 ? 1 : 0));
1283
+ let oi = 0;
1284
+ for (let i = 0; i < fullChunks; i++) {
1285
+ const c = REVERSE[s[i * 3]];
1286
+ const d = REVERSE[s[i * 3 + 1]];
1287
+ const e = REVERSE[s[i * 3 + 2]];
1288
+ if (c === void 0 || d === void 0 || e === void 0) {
1289
+ throw new Error("base45: invalid character");
1290
+ }
1291
+ const x = c + 45 * d + 45 * 45 * e;
1292
+ if (x > 65535) throw new Error("base45: chunk overflow");
1293
+ out[oi++] = x >> 8 & 255;
1294
+ out[oi++] = x & 255;
1295
+ }
1296
+ if (tail === 2) {
1297
+ const c = REVERSE[s[fullChunks * 3]];
1298
+ const d = REVERSE[s[fullChunks * 3 + 1]];
1299
+ if (c === void 0 || d === void 0)
1300
+ throw new Error("base45: invalid character");
1301
+ const x = c + 45 * d;
1302
+ if (x > 255) throw new Error("base45: tail overflow");
1303
+ out[oi++] = x & 255;
1304
+ }
1305
+ return out;
1306
+ }
1307
+
1308
+ // src/offline/messages.ts
1309
+ import { z as z5 } from "zod";
1310
+ var HexSig = z5.string().regex(/^[0-9a-fA-F]{128}$/, "expected 64-byte hex signature");
1311
+ var OfflinePaymentRequestSchema = z5.object({
1312
+ reference: z5.string().min(1),
1313
+ amountKobo: z5.number().int().positive(),
1314
+ merchantOAC: OACSchema,
1315
+ expiresAtMs: z5.number().int().positive(),
1316
+ merchantSig: HexSig
1317
+ });
1318
+ var OfflinePaymentAuthorizationSchema = z5.object({
1319
+ request: OfflinePaymentRequestSchema,
1320
+ payerOAC: OACSchema,
1321
+ payerCounter: z5.number().int().positive(),
1322
+ payerSig: HexSig
1323
+ });
1324
+ function buildPaymentRequest(input) {
1325
+ if (!Number.isInteger(input.amountKobo) || input.amountKobo <= 0) {
1326
+ throw new Error("amountKobo must be a positive integer");
1327
+ }
1328
+ if (input.amountKobo > input.merchantOAC.perTxCapKobo) {
1329
+ throw new Error("amountKobo exceeds merchant OAC perTxCapKobo");
1330
+ }
1331
+ return {
1332
+ reference: input.reference,
1333
+ amountKobo: input.amountKobo,
1334
+ merchantOAC: input.merchantOAC,
1335
+ expiresAtMs: input.expiresAtMs
1336
+ };
1337
+ }
1338
+ function signPaymentRequest(unsigned, merchantDevicePrivateKey) {
1339
+ const merchantSig = bytesToHex(
1340
+ sign(canonicalJSONBytes(unsigned), merchantDevicePrivateKey)
1341
+ );
1342
+ return { ...unsigned, merchantSig };
1343
+ }
1344
+ function verifyPaymentRequest(req, issuerPublicKey) {
1345
+ try {
1346
+ const parsed = OfflinePaymentRequestSchema.parse(req);
1347
+ const { issuerSig: merchantOacSig, ...merchantOacUnsigned } = parsed.merchantOAC;
1348
+ if (!verify(
1349
+ canonicalJSONBytes(merchantOacUnsigned),
1350
+ hexToBytes(merchantOacSig),
1351
+ issuerPublicKey
1352
+ )) {
1353
+ return false;
1354
+ }
1355
+ const { merchantSig, ...unsigned } = parsed;
1356
+ return verify(
1357
+ canonicalJSONBytes(unsigned),
1358
+ hexToBytes(merchantSig),
1359
+ hexToBytes(parsed.merchantOAC.devicePublicKey)
1360
+ );
1361
+ } catch {
1362
+ return false;
1363
+ }
1364
+ }
1365
+ function buildAuthorization(input) {
1366
+ if (!Number.isInteger(input.payerCounter) || input.payerCounter <= 0) {
1367
+ throw new Error("payerCounter must be a positive integer");
1368
+ }
1369
+ if (input.request.amountKobo > input.payerOAC.perTxCapKobo) {
1370
+ throw new Error("amountKobo exceeds payer OAC perTxCapKobo");
1371
+ }
1372
+ return {
1373
+ request: input.request,
1374
+ payerOAC: input.payerOAC,
1375
+ payerCounter: input.payerCounter
1376
+ };
1377
+ }
1378
+ function signAuthorization(unsigned, payerDevicePrivateKey) {
1379
+ const payerSig = bytesToHex(
1380
+ sign(canonicalJSONBytes(unsigned), payerDevicePrivateKey)
1381
+ );
1382
+ return { ...unsigned, payerSig };
1383
+ }
1384
+ function verifyAuthorization(auth, issuerPublicKey) {
1385
+ try {
1386
+ const parsed = OfflinePaymentAuthorizationSchema.parse(auth);
1387
+ if (!verifyPaymentRequest(parsed.request, issuerPublicKey)) return false;
1388
+ const { issuerSig: payerOacSig, ...payerOacUnsigned } = parsed.payerOAC;
1389
+ if (!verify(
1390
+ canonicalJSONBytes(payerOacUnsigned),
1391
+ hexToBytes(payerOacSig),
1392
+ issuerPublicKey
1393
+ )) {
1394
+ return false;
1395
+ }
1396
+ const { payerSig, ...unsigned } = parsed;
1397
+ return verify(
1398
+ canonicalJSONBytes(unsigned),
1399
+ hexToBytes(payerSig),
1400
+ hexToBytes(parsed.payerOAC.devicePublicKey)
1401
+ );
1402
+ } catch {
1403
+ return false;
1404
+ }
1405
+ }
1406
+ function encodePaymentRequestQR(req) {
1407
+ const json = JSON.stringify(req);
1408
+ return "FLUR1:" + encodeBase45(new TextEncoder().encode(json));
1409
+ }
1410
+ function decodePaymentRequestQR(s) {
1411
+ if (!s.startsWith("FLUR1:"))
1412
+ throw new Error("not a Flur offline payment request QR");
1413
+ const bytes = decodeBase45(s.slice("FLUR1:".length));
1414
+ const json = new TextDecoder().decode(bytes);
1415
+ return OfflinePaymentRequestSchema.parse(JSON.parse(json));
1416
+ }
1417
+ function encodeAuthorizationQR(auth) {
1418
+ const json = JSON.stringify(auth);
1419
+ return "FLUR2:" + encodeBase45(new TextEncoder().encode(json));
1420
+ }
1421
+ function decodeAuthorizationQR(s) {
1422
+ if (!s.startsWith("FLUR2:"))
1423
+ throw new Error("not a Flur offline authorization QR");
1424
+ const bytes = decodeBase45(s.slice("FLUR2:".length));
1425
+ const json = new TextDecoder().decode(bytes);
1426
+ return OfflinePaymentAuthorizationSchema.parse(JSON.parse(json));
1427
+ }
1428
+
1429
+ // src/auth/hmac.ts
1430
+ import { hmac } from "@noble/hashes/hmac";
1431
+ import { sha256 } from "@noble/hashes/sha256";
1432
+ function bytesToHex2(b) {
1433
+ let s = "";
1434
+ for (let i = 0; i < b.length; i++) s += b[i].toString(16).padStart(2, "0");
1435
+ return s;
1436
+ }
1437
+ function constantTimeStringEqual(a, b) {
1438
+ if (a.length !== b.length) return false;
1439
+ let diff = 0;
1440
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
1441
+ return diff === 0;
1442
+ }
1443
+ function bodySha256Hex(body) {
1444
+ return bytesToHex2(sha256(new TextEncoder().encode(body)));
1445
+ }
1446
+ function canonicalRequestString(input) {
1447
+ return [
1448
+ input.method.toUpperCase(),
1449
+ input.path,
1450
+ input.timestamp,
1451
+ input.nonce,
1452
+ bodySha256Hex(input.body)
1453
+ ].join("\n");
1454
+ }
1455
+ function signRequestHMAC(input) {
1456
+ return bytesToHex2(
1457
+ hmac(sha256, input.apiSecret, canonicalRequestString(input))
1458
+ );
1459
+ }
1460
+ function verifyRequestHMAC(input) {
1461
+ const expected = signRequestHMAC(input);
1462
+ return constantTimeStringEqual(expected, input.signature.toLowerCase());
1463
+ }
1464
+
1465
+ // src/auth/middleware.ts
1466
+ var REPLAY_WINDOW_MS = 5 * 60 * 1e3;
1467
+ var SERVER_TIME_HEADER = "x-flur-server-time";
1468
+ function defaultNonce() {
1469
+ const c = globalThis.crypto;
1470
+ if (typeof c?.randomUUID === "function") return c.randomUUID();
1471
+ if (typeof c?.getRandomValues !== "function") {
1472
+ throw new Error(
1473
+ "Flur SDK: no CSPRNG available (globalThis.crypto.getRandomValues missing). Refusing to fall back to Math.random for HMAC nonce generation."
1474
+ );
1475
+ }
1476
+ const arr = new Uint8Array(16);
1477
+ c.getRandomValues(arr);
1478
+ let s = "";
1479
+ for (let i = 0; i < arr.length; i++)
1480
+ s += arr[i].toString(16).padStart(2, "0");
1481
+ return s;
1482
+ }
1483
+ function assertCSPRNG() {
1484
+ const c = globalThis.crypto;
1485
+ if (typeof c?.getRandomValues !== "function" && typeof c?.randomUUID !== "function") {
1486
+ throw new Error(
1487
+ "Flur SDK: no CSPRNG available (globalThis.crypto missing). Initialize on a runtime that provides Web Crypto (Node 19+, modern browsers, RN 0.74+)."
1488
+ );
1489
+ }
1490
+ }
1491
+ function parseServerTimeMs(value) {
1492
+ if (!value) return null;
1493
+ const trimmed = value.trim();
1494
+ if (/^\d+$/.test(trimmed)) {
1495
+ const n = Number(trimmed);
1496
+ return n < 1e12 ? n * 1e3 : n;
1497
+ }
1498
+ const t = Date.parse(trimmed);
1499
+ return Number.isFinite(t) ? t : null;
1500
+ }
1501
+ function createHmacFetch(opts) {
1502
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
1503
+ if (!fetchImpl)
1504
+ throw new Error("createHmacFetch: no fetch implementation available");
1505
+ if (!opts.nonceFn) assertCSPRNG();
1506
+ const nowMs = opts.nowMs ?? (() => Date.now());
1507
+ const nonceFn = opts.nonceFn ?? defaultNonce;
1508
+ const scopeHeader = opts.scope && opts.scope.length > 0 ? opts.scope.join(",") : void 0;
1509
+ async function signAndSend(req, skewOffsetMs) {
1510
+ const cloned = req.clone();
1511
+ const url = new URL(cloned.url);
1512
+ const path = url.pathname + url.search;
1513
+ const method = cloned.method.toUpperCase();
1514
+ const bodyText = method === "GET" || method === "HEAD" ? "" : await cloned.clone().text();
1515
+ const timestamp = Math.floor((nowMs() + skewOffsetMs) / 1e3).toString();
1516
+ const nonce = nonceFn();
1517
+ const signature = signRequestHMAC({
1518
+ method,
1519
+ path,
1520
+ timestamp,
1521
+ nonce,
1522
+ body: bodyText,
1523
+ apiSecret: opts.apiSecret
1524
+ });
1525
+ const headers = new Headers(cloned.headers);
1526
+ headers.set("x-flur-key", opts.apiKey);
1527
+ headers.set("x-flur-timestamp", timestamp);
1528
+ headers.set("x-flur-nonce", nonce);
1529
+ headers.set("x-flur-signature", signature);
1530
+ if (scopeHeader) headers.set("x-flur-scope", scopeHeader);
1531
+ const signed = new Request(cloned, { headers });
1532
+ return fetchImpl(signed);
1533
+ }
1534
+ return (async (input, init2) => {
1535
+ const req = input instanceof Request ? input : new Request(input, init2);
1536
+ let resp = await signAndSend(req, 0);
1537
+ if (resp.status === 401) {
1538
+ const serverTimeMs = parseServerTimeMs(
1539
+ resp.headers.get(SERVER_TIME_HEADER)
1540
+ );
1541
+ if (serverTimeMs !== null) {
1542
+ const skew = serverTimeMs - nowMs();
1543
+ if (Math.abs(skew) > 0) {
1544
+ resp = await signAndSend(req, skew);
1545
+ }
1546
+ }
1547
+ }
1548
+ return resp;
1549
+ });
1550
+ }
1551
+
1552
+ // src/passes/pass.ts
1553
+ import { z as z6 } from "zod";
1554
+ var PASS_KINDS = [
1555
+ "ride-ticket",
1556
+ "transit-pass",
1557
+ "event-ticket",
1558
+ "voucher",
1559
+ "loyalty",
1560
+ "receipt-link"
1561
+ ];
1562
+ var PASS_STATES = [
1563
+ "issued",
1564
+ "active",
1565
+ "redeemed",
1566
+ "expired",
1567
+ "revoked"
1568
+ ];
1569
+ var HexString2 = (length) => z6.string().regex(
1570
+ new RegExp(`^[0-9a-fA-F]{${length * 2}}$`),
1571
+ `expected ${length}-byte hex string`
1572
+ );
1573
+ var PassMetadataSchema = z6.record(
1574
+ z6.union([z6.string(), z6.number(), z6.boolean(), z6.null()])
1575
+ );
1576
+ var PassSchema = z6.object({
1577
+ passId: z6.string().min(1),
1578
+ /** Optional client/template grouping id (server may omit). */
1579
+ templateId: z6.string().min(1).optional(),
1580
+ /** Optional human-facing holder identity (server may omit). The cryptographic binding
1581
+ * is `holderDevicePubkey` below. */
1582
+ holderUserId: z6.string().min(1).optional(),
1583
+ kind: z6.enum(PASS_KINDS),
1584
+ issuerId: z6.string().min(1),
1585
+ issuedAtMs: z6.number().int().nonnegative(),
1586
+ validFromMs: z6.number().int().nonnegative(),
1587
+ validUntilMs: z6.number().int().positive(),
1588
+ state: z6.enum(PASS_STATES),
1589
+ metadata: PassMetadataSchema,
1590
+ nonce: z6.string().min(1),
1591
+ /** Device id this pass is bound to (FK to backend `device_keys`). */
1592
+ holderDeviceId: z6.string().min(1),
1593
+ /** 32-byte hex Ed25519 public key of the bound device. The redemption signature
1594
+ * is verified against this key — it is the security-critical binding. */
1595
+ holderDevicePubkey: HexString2(32),
1596
+ /** Optional fixed amount for monetary passes (vouchers, gift cards) in kobo. */
1597
+ amountKobo: z6.number().int().nonnegative().optional(),
1598
+ /** ISO-4217-ish currency code; required on the wire. SDK builders default to NGN. */
1599
+ currency: z6.string().min(3).max(8),
1600
+ /** Monotonic redemption counter floor. Redemption.counter MUST be > counterSeed. */
1601
+ counterSeed: z6.number().int().nonnegative(),
1602
+ /** Optional cumulative spend cap in kobo across all redemptions of this pass. */
1603
+ cumulativeCapKobo: z6.number().int().nonnegative().optional(),
1604
+ issuerSig: HexString2(64)
1605
+ }).refine((v) => v.validUntilMs > v.validFromMs, {
1606
+ message: "validUntilMs must be greater than validFromMs"
1607
+ });
1608
+ function buildPass(input) {
1609
+ if (input.validUntilMs <= input.validFromMs) {
1610
+ throw new Error("validUntilMs must be greater than validFromMs");
1611
+ }
1612
+ const out = {
1613
+ passId: input.passId,
1614
+ kind: input.kind,
1615
+ issuerId: input.issuerId,
1616
+ issuedAtMs: input.issuedAtMs,
1617
+ validFromMs: input.validFromMs,
1618
+ validUntilMs: input.validUntilMs,
1619
+ state: input.state,
1620
+ metadata: input.metadata,
1621
+ nonce: input.nonce,
1622
+ holderDeviceId: input.holderDeviceId,
1623
+ holderDevicePubkey: input.holderDevicePubkey,
1624
+ currency: input.currency ?? "NGN",
1625
+ counterSeed: input.counterSeed
1626
+ };
1627
+ if (typeof input.templateId === "string") out.templateId = input.templateId;
1628
+ if (typeof input.holderUserId === "string")
1629
+ out.holderUserId = input.holderUserId;
1630
+ if (typeof input.amountKobo === "number") out.amountKobo = input.amountKobo;
1631
+ if (typeof input.cumulativeCapKobo === "number") {
1632
+ out.cumulativeCapKobo = input.cumulativeCapKobo;
1633
+ }
1634
+ return out;
1635
+ }
1636
+ function signPass(unsigned, issuerPrivateKey) {
1637
+ const issuerSig = bytesToHex(
1638
+ sign(canonicalJSONBytes(unsigned), issuerPrivateKey)
1639
+ );
1640
+ return { ...unsigned, issuerSig };
1641
+ }
1642
+ function verifyPass(pass, issuerPublicKey) {
1643
+ try {
1644
+ const parsed = PassSchema.parse(pass);
1645
+ const { issuerSig, ...unsigned } = parsed;
1646
+ return verify(
1647
+ canonicalJSONBytes(unsigned),
1648
+ hexToBytes(issuerSig),
1649
+ issuerPublicKey
1650
+ );
1651
+ } catch {
1652
+ return false;
1653
+ }
1654
+ }
1655
+ function isPassWithinValidity(pass, nowMs) {
1656
+ return nowMs >= pass.validFromMs && nowMs < pass.validUntilMs;
1657
+ }
1658
+
1659
+ // src/passes/redemption.ts
1660
+ import { z as z7 } from "zod";
1661
+ var HexSig2 = z7.string().regex(/^[0-9a-fA-F]{128}$/, "expected 64-byte hex signature");
1662
+ var RedemptionSchema = z7.object({
1663
+ pass: PassSchema,
1664
+ redeemerId: z7.string().min(1),
1665
+ redeemedAtMs: z7.number().int().nonnegative(),
1666
+ /** Strictly monotonic counter scoped to a single pass. Must be > pass.counterSeed
1667
+ * and > the redeemer's lastSeenCounter for this pass. */
1668
+ counter: z7.number().int().positive(),
1669
+ /** Amount being redeemed in kobo (0 for non-monetary passes like ride tickets). */
1670
+ amountKobo: z7.number().int().nonnegative(),
1671
+ nonce: z7.string().min(1),
1672
+ holderSig: HexSig2
1673
+ });
1674
+ var REDEEMABLE_STATES = /* @__PURE__ */ new Set(["issued", "active"]);
1675
+ function buildRedemption(input) {
1676
+ if (!REDEEMABLE_STATES.has(input.pass.state)) {
1677
+ throw new Error(`pass not in redeemable state: ${input.pass.state}`);
1678
+ }
1679
+ if (input.redeemedAtMs < input.pass.validFromMs) {
1680
+ throw new Error("redeemedAtMs is before pass validFromMs");
1681
+ }
1682
+ if (input.redeemedAtMs >= input.pass.validUntilMs) {
1683
+ throw new FlurExpiredError(
1684
+ `pass ${input.pass.passId} expired at ${input.pass.validUntilMs}`
1685
+ );
1686
+ }
1687
+ const now = typeof input.nowMs === "number" ? input.nowMs : input.redeemedAtMs;
1688
+ if (now >= input.pass.validUntilMs) {
1689
+ throw new FlurExpiredError(
1690
+ `pass ${input.pass.passId} expired at ${input.pass.validUntilMs}`
1691
+ );
1692
+ }
1693
+ if (!input.pass.holderDevicePubkey) {
1694
+ throw new Error(
1695
+ "pass.holderDevicePubkey is required to build a redemption"
1696
+ );
1697
+ }
1698
+ const lastSeen = Math.max(
1699
+ input.pass.counterSeed,
1700
+ typeof input.lastSeenCounter === "number" ? input.lastSeenCounter : 0
1701
+ );
1702
+ if (input.counter <= lastSeen) {
1703
+ throw new FlurReplayError(
1704
+ `redemption counter ${input.counter} must be > lastSeenCounter ${lastSeen}`
1705
+ );
1706
+ }
1707
+ const amount = input.amountKobo ?? 0;
1708
+ if (typeof input.pass.cumulativeCapKobo === "number") {
1709
+ const used = input.cumulativeUsedKobo ?? 0;
1710
+ if (used + amount > input.pass.cumulativeCapKobo) {
1711
+ throw new FlurCapExceededError(
1712
+ `pass ${input.pass.passId} cap ${input.pass.cumulativeCapKobo} would be exceeded (used ${used} + ${amount})`
1713
+ );
1714
+ }
1715
+ }
1716
+ return {
1717
+ pass: input.pass,
1718
+ redeemerId: input.redeemerId,
1719
+ redeemedAtMs: input.redeemedAtMs,
1720
+ counter: input.counter,
1721
+ amountKobo: amount,
1722
+ nonce: input.nonce
1723
+ };
1724
+ }
1725
+ function signRedemption(unsigned, holderDevicePrivateKey) {
1726
+ const holderSig = bytesToHex(
1727
+ sign(canonicalJSONBytes(unsigned), holderDevicePrivateKey)
1728
+ );
1729
+ return { ...unsigned, holderSig };
1730
+ }
1731
+ function verifyRedemption(r, issuerPublicKey) {
1732
+ try {
1733
+ const parsed = RedemptionSchema.parse(r);
1734
+ if (parsed.counter <= parsed.pass.counterSeed) return false;
1735
+ const { issuerSig, ...passUnsigned } = parsed.pass;
1736
+ if (!verify(
1737
+ canonicalJSONBytes(passUnsigned),
1738
+ hexToBytes(issuerSig),
1739
+ issuerPublicKey
1740
+ )) {
1741
+ return false;
1742
+ }
1743
+ const holderHex = parsed.pass.holderDevicePubkey;
1744
+ if (typeof holderHex !== "string") return false;
1745
+ const { holderSig, ...unsigned } = parsed;
1746
+ return verify(
1747
+ canonicalJSONBytes(unsigned),
1748
+ hexToBytes(holderSig),
1749
+ hexToBytes(holderHex)
1750
+ );
1751
+ } catch {
1752
+ return false;
1753
+ }
1754
+ }
1755
+
1756
+ // src/passes/client.ts
1757
+ function createPassesClient(opts) {
1758
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
1759
+ if (!fetchImpl)
1760
+ throw new Error("createPassesClient: no fetch implementation available");
1761
+ const baseUrl = opts.baseUrl.replace(/\/$/, "");
1762
+ async function call(method, path, body, parser) {
1763
+ const init2 = {
1764
+ method,
1765
+ headers: {
1766
+ "content-type": "application/json",
1767
+ accept: "application/json"
1768
+ }
1769
+ };
1770
+ if (body !== void 0) init2.body = JSON.stringify(body);
1771
+ const resp = await fetchImpl(`${baseUrl}${path}`, init2);
1772
+ const text = await resp.text();
1773
+ let raw = void 0;
1774
+ if (text) {
1775
+ try {
1776
+ raw = JSON.parse(text);
1777
+ } catch {
1778
+ }
1779
+ }
1780
+ if (!resp.ok) {
1781
+ const code = (raw && typeof raw === "object" && "code" in raw && typeof raw.code === "string" ? raw.code : void 0) ?? `http_${resp.status}`;
1782
+ const message = (raw && typeof raw === "object" && "message" in raw && typeof raw.message === "string" ? raw.message : void 0) ?? `request failed with status ${resp.status}`;
1783
+ throw new FlurApiError(resp.status, code, message, raw);
1784
+ }
1785
+ return parser(raw);
1786
+ }
1787
+ return {
1788
+ issuePass: (input) => call("POST", "/v1/passes", input, (raw) => PassSchema.parse(raw)),
1789
+ listPasses: (input) => {
1790
+ const qs = new URLSearchParams();
1791
+ if (input.holderDeviceId) qs.set("holderDeviceId", input.holderDeviceId);
1792
+ if (input.holderUserId) qs.set("holderUserId", input.holderUserId);
1793
+ if (input.state) qs.set("state", input.state);
1794
+ if (input.kind) qs.set("kind", input.kind);
1795
+ if (input.templateId) qs.set("templateId", input.templateId);
1796
+ if (typeof input.limit === "number") qs.set("limit", String(input.limit));
1797
+ if (input.cursor) qs.set("cursor", input.cursor);
1798
+ const path = `/v1/passes${qs.size > 0 ? `?${qs.toString()}` : ""}`;
1799
+ return call("GET", path, void 0, (raw) => {
1800
+ const obj = raw;
1801
+ const items = obj.items.map(
1802
+ (it) => PassSchema.parse(it)
1803
+ );
1804
+ const nextCursor = typeof obj.nextCursor === "string" ? obj.nextCursor : null;
1805
+ return { items, nextCursor };
1806
+ });
1807
+ },
1808
+ getPass: (passId) => call(
1809
+ "GET",
1810
+ `/v1/passes/${encodeURIComponent(passId)}`,
1811
+ void 0,
1812
+ (raw) => PassSchema.parse(raw)
1813
+ ),
1814
+ redeemPass: (passId, redemption) => call(
1815
+ "POST",
1816
+ `/v1/passes/${encodeURIComponent(passId)}/redeem`,
1817
+ RedemptionSchema.parse(redemption),
1818
+ (raw) => PassSchema.parse(raw)
1819
+ ),
1820
+ revokePass: (passId, input) => call(
1821
+ "POST",
1822
+ `/v1/passes/${encodeURIComponent(passId)}/revoke`,
1823
+ input,
1824
+ (raw) => PassSchema.parse(raw)
1825
+ ),
1826
+ verifyPass: (pass, issuerPublicKey) => verifyPass(pass, issuerPublicKey)
1827
+ };
1828
+ }
1829
+
1830
+ // src/receipts/receipt.ts
1831
+ import { z as z8 } from "zod";
1832
+ var RECEIPT_CHANNELS = ["cash", "pass"];
1833
+ var RECEIPT_KINDS = RECEIPT_CHANNELS;
1834
+ var HexString3 = (length) => z8.string().regex(
1835
+ new RegExp(`^[0-9a-fA-F]{${length * 2}}$`),
1836
+ `expected ${length}-byte hex string`
1837
+ );
1838
+ var ReceiptPayloadSchema = z8.record(
1839
+ z8.union([z8.string(), z8.number(), z8.boolean(), z8.null()])
1840
+ );
1841
+ var ReceiptSchema = z8.object({
1842
+ receiptId: z8.string().min(1),
1843
+ channel: z8.enum(RECEIPT_CHANNELS),
1844
+ /** Cash-channel: send_intents.id. Required when channel === 'cash'. */
1845
+ intentId: z8.string().min(1).optional(),
1846
+ /** Pass-channel: pass_redemptions.id. Required when channel === 'pass'. */
1847
+ passRedemptionId: z8.string().min(1).optional(),
1848
+ payerUserId: z8.string().min(1),
1849
+ payeeUserId: z8.string().min(1),
1850
+ amountKobo: z8.number().int().nonnegative(),
1851
+ currency: z8.string().min(3).max(8),
1852
+ issuedAtMs: z8.number().int().nonnegative(),
1853
+ issuerId: z8.string().min(1),
1854
+ payload: ReceiptPayloadSchema,
1855
+ issuerSig: HexString3(64)
1856
+ }).superRefine((v, ctx) => {
1857
+ if (v.channel === "cash") {
1858
+ if (!v.intentId) {
1859
+ ctx.addIssue({
1860
+ code: z8.ZodIssueCode.custom,
1861
+ message: "cash receipts require intentId",
1862
+ path: ["intentId"]
1863
+ });
1864
+ }
1865
+ if (v.passRedemptionId) {
1866
+ ctx.addIssue({
1867
+ code: z8.ZodIssueCode.custom,
1868
+ message: "cash receipts must not carry passRedemptionId",
1869
+ path: ["passRedemptionId"]
1870
+ });
1871
+ }
1872
+ } else if (v.channel === "pass") {
1873
+ if (!v.passRedemptionId) {
1874
+ ctx.addIssue({
1875
+ code: z8.ZodIssueCode.custom,
1876
+ message: "pass receipts require passRedemptionId",
1877
+ path: ["passRedemptionId"]
1878
+ });
1879
+ }
1880
+ if (v.intentId) {
1881
+ ctx.addIssue({
1882
+ code: z8.ZodIssueCode.custom,
1883
+ message: "pass receipts must not carry intentId",
1884
+ path: ["intentId"]
1885
+ });
1886
+ }
1887
+ }
1888
+ });
1889
+ function buildReceipt(input) {
1890
+ if (input.channel === "cash") {
1891
+ if (!input.intentId) {
1892
+ throw new Error("cash receipts require intentId");
1893
+ }
1894
+ if (input.passRedemptionId) {
1895
+ throw new Error("cash receipts must not carry passRedemptionId");
1896
+ }
1897
+ } else {
1898
+ if (!input.passRedemptionId) {
1899
+ throw new Error("pass receipts require passRedemptionId");
1900
+ }
1901
+ if (input.intentId) {
1902
+ throw new Error("pass receipts must not carry intentId");
1903
+ }
1904
+ }
1905
+ const out = {
1906
+ receiptId: input.receiptId,
1907
+ channel: input.channel,
1908
+ payerUserId: input.payerUserId,
1909
+ payeeUserId: input.payeeUserId,
1910
+ amountKobo: input.amountKobo,
1911
+ currency: input.currency,
1912
+ issuedAtMs: input.issuedAtMs,
1913
+ issuerId: input.issuerId,
1914
+ payload: input.payload ?? {}
1915
+ };
1916
+ if (input.intentId) out.intentId = input.intentId;
1917
+ if (input.passRedemptionId) out.passRedemptionId = input.passRedemptionId;
1918
+ return out;
1919
+ }
1920
+ function signReceipt(unsigned, issuerPrivateKey) {
1921
+ const issuerSig = bytesToHex(
1922
+ sign(canonicalJSONBytes(unsigned), issuerPrivateKey)
1923
+ );
1924
+ return { ...unsigned, issuerSig };
1925
+ }
1926
+ function verifyReceipt(r, issuerPublicKey) {
1927
+ try {
1928
+ const parsed = ReceiptSchema.parse(r);
1929
+ const { issuerSig, ...unsigned } = parsed;
1930
+ return verify(
1931
+ canonicalJSONBytes(unsigned),
1932
+ hexToBytes(issuerSig),
1933
+ issuerPublicKey
1934
+ );
1935
+ } catch {
1936
+ return false;
1937
+ }
1938
+ }
1939
+
1940
+ // src/receipts/client.ts
1941
+ function createReceiptsClient(opts) {
1942
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
1943
+ if (!fetchImpl)
1944
+ throw new Error("createReceiptsClient: no fetch implementation available");
1945
+ const baseUrl = opts.baseUrl.replace(/\/$/, "");
1946
+ async function call(method, path, body, parser) {
1947
+ const init2 = {
1948
+ method,
1949
+ headers: {
1950
+ "content-type": "application/json",
1951
+ accept: "application/json"
1952
+ }
1953
+ };
1954
+ if (body !== void 0) init2.body = JSON.stringify(body);
1955
+ const resp = await fetchImpl(`${baseUrl}${path}`, init2);
1956
+ const text = await resp.text();
1957
+ let raw = void 0;
1958
+ if (text) {
1959
+ try {
1960
+ raw = JSON.parse(text);
1961
+ } catch {
1962
+ }
1963
+ }
1964
+ if (!resp.ok) {
1965
+ const code = raw && typeof raw === "object" && "code" in raw && typeof raw.code === "string" ? raw.code : `http_${resp.status}`;
1966
+ const message = raw && typeof raw === "object" && "message" in raw && typeof raw.message === "string" ? raw.message : `request failed with status ${resp.status}`;
1967
+ throw new FlurApiError(resp.status, code, message, raw);
1968
+ }
1969
+ return parser(raw);
1970
+ }
1971
+ const getById = (receiptId) => call(
1972
+ "GET",
1973
+ `/v1/receipts/${encodeURIComponent(receiptId)}`,
1974
+ void 0,
1975
+ (raw) => ReceiptSchema.parse(raw)
1976
+ );
1977
+ return {
1978
+ issueReceipt: (input) => call("POST", "/v1/receipts", input, (raw) => ReceiptSchema.parse(raw)),
1979
+ getReceipt: getById,
1980
+ getById,
1981
+ getByIntentId: (intentId) => call(
1982
+ "GET",
1983
+ `/v1/receipts/by-intent/${encodeURIComponent(intentId)}`,
1984
+ void 0,
1985
+ (raw) => ReceiptSchema.parse(raw)
1986
+ ),
1987
+ getByPassRedemptionId: (passRedemptionId) => call(
1988
+ "GET",
1989
+ `/v1/receipts/by-pass-redemption/${encodeURIComponent(passRedemptionId)}`,
1990
+ void 0,
1991
+ (raw) => ReceiptSchema.parse(raw)
1992
+ ),
1993
+ listForUser: (input) => {
1994
+ const qs = new URLSearchParams();
1995
+ if (input.payerUserId) qs.set("payerUserId", input.payerUserId);
1996
+ if (input.payeeUserId) qs.set("payeeUserId", input.payeeUserId);
1997
+ if (input.channel) qs.set("channel", input.channel);
1998
+ if (typeof input.limit === "number") qs.set("limit", String(input.limit));
1999
+ if (input.cursor) qs.set("cursor", input.cursor);
2000
+ const path = `/v1/receipts${qs.size > 0 ? `?${qs.toString()}` : ""}`;
2001
+ return call("GET", path, void 0, (raw) => {
2002
+ const obj = raw;
2003
+ const items = obj.items.map(
2004
+ (it) => ReceiptSchema.parse(it)
2005
+ );
2006
+ const nextCursor = typeof obj.nextCursor === "string" ? obj.nextCursor : null;
2007
+ return { items, nextCursor };
2008
+ });
2009
+ },
2010
+ verifyReceipt: (receipt, issuerPublicKey) => verifyReceipt(receipt, issuerPublicKey)
2011
+ };
2012
+ }
2013
+
2014
+ // src/client/flur.ts
2015
+ function generateStaticQR(input) {
2016
+ return encodeNQR({ ...input, pointOfInitiation: "static" });
2017
+ }
2018
+ function generateDynamicQR(input) {
2019
+ return encodeNQR({ ...input, pointOfInitiation: "dynamic" });
2020
+ }
2021
+ function parseQR(payload) {
2022
+ return parseNQR(payload);
2023
+ }
2024
+ function init(opts) {
2025
+ const signedFetch = createHmacFetch({
2026
+ apiKey: opts.apiKey,
2027
+ apiSecret: opts.apiSecret,
2028
+ fetchImpl: opts.fetchImpl,
2029
+ scope: opts.scope
2030
+ });
2031
+ const baseUrl = opts.baseUrl.replace(/\/$/, "");
2032
+ function subscribeToPayments(s) {
2033
+ const controller = new AbortController();
2034
+ let cancelled = false;
2035
+ (async () => {
2036
+ try {
2037
+ const url = `${baseUrl}/v1/payments/subscribe?reference=${encodeURIComponent(s.reference)}`;
2038
+ const resp = await signedFetch(url, {
2039
+ method: "GET",
2040
+ headers: { accept: "text/event-stream" },
2041
+ signal: controller.signal
2042
+ });
2043
+ if (!resp.body) return;
2044
+ const reader = resp.body.getReader();
2045
+ const decoder = new TextDecoder();
2046
+ let buffer = "";
2047
+ while (!cancelled) {
2048
+ const { value, done } = await reader.read();
2049
+ if (done) return;
2050
+ buffer += decoder.decode(value, { stream: true });
2051
+ let idx;
2052
+ while ((idx = buffer.indexOf("\n\n")) >= 0) {
2053
+ const chunk = buffer.slice(0, idx);
2054
+ buffer = buffer.slice(idx + 2);
2055
+ for (const line of chunk.split("\n")) {
2056
+ if (!line.startsWith("data:")) continue;
2057
+ const json = line.slice(5).trim();
2058
+ if (!json) continue;
2059
+ try {
2060
+ s.onEvent(JSON.parse(json));
2061
+ } catch (err) {
2062
+ s.onError?.(err);
2063
+ }
2064
+ }
2065
+ }
2066
+ }
2067
+ } catch (err) {
2068
+ if (!cancelled) s.onError?.(err);
2069
+ }
2070
+ })();
2071
+ return () => {
2072
+ cancelled = true;
2073
+ controller.abort();
2074
+ };
2075
+ }
2076
+ const cash = {
2077
+ generateStaticQR,
2078
+ generateDynamicQR,
2079
+ parseQR,
2080
+ subscribeToPayments
2081
+ };
2082
+ const passes = createPassesClient({ baseUrl, fetchImpl: signedFetch });
2083
+ const receipts = createReceiptsClient({ baseUrl, fetchImpl: signedFetch });
2084
+ return {
2085
+ // top-level back-compat surface
2086
+ generateStaticQR,
2087
+ generateDynamicQR,
2088
+ parseQR,
2089
+ subscribeToPayments,
2090
+ // namespaces
2091
+ cash,
2092
+ passes,
2093
+ receipts
2094
+ };
2095
+ }
723
2096
  export {
2097
+ ADDITIONAL_DATA_SUBFIELD,
2098
+ FIELD,
2099
+ FlurApiError,
2100
+ FlurCapExceededError,
724
2101
  FlurClient,
725
2102
  FlurError,
2103
+ FlurExpiredError,
2104
+ FlurReplayError,
2105
+ NGN_CURRENCY_CODE,
2106
+ NG_COUNTRY_CODE,
2107
+ NQRParseError,
2108
+ OACSchema,
2109
+ OAC_DEFAULT_CUMULATIVE_KOBO,
2110
+ OAC_DEFAULT_PER_TX_KOBO,
2111
+ OAC_DEFAULT_VALIDITY_MS,
2112
+ OfflinePaymentAuthorizationSchema,
2113
+ OfflinePaymentRequestSchema,
2114
+ PASS_KINDS,
2115
+ PASS_STATES,
2116
+ PAYLOAD_FORMAT_INDICATOR_VALUE,
2117
+ POINT_OF_INITIATION,
2118
+ PassMetadataSchema,
2119
+ PassSchema,
2120
+ RECEIPT_CHANNELS,
2121
+ RECEIPT_KINDS,
2122
+ REPLAY_WINDOW_MS,
2123
+ ReceiptPayloadSchema,
2124
+ ReceiptSchema,
2125
+ RedemptionSchema,
2126
+ bodySha256Hex,
2127
+ buildAuthorization,
2128
+ buildOAC,
2129
+ buildPass,
2130
+ buildPaymentRequest,
2131
+ buildReceipt,
2132
+ buildRedemption,
2133
+ canonicalJSONBytes,
2134
+ canonicalJSONStringify,
2135
+ canonicalRequestString,
2136
+ constantTimeEqual,
2137
+ crc16ccitt,
2138
+ crc16ccittHex,
2139
+ createHmacFetch,
2140
+ createPassesClient,
2141
+ createReceiptsClient,
2142
+ decodeAuthorizationQR,
2143
+ decodeBase45,
2144
+ decodePaymentRequestQR,
2145
+ encodeAuthorizationQR,
2146
+ encodeBase45,
2147
+ encodeNQR,
2148
+ encodePaymentRequestQR,
726
2149
  formatAmount,
2150
+ generateDynamicQR,
2151
+ generateKeyPair,
2152
+ generateStaticQR,
2153
+ init,
2154
+ isPassWithinValidity,
727
2155
  moneyMinorToNumber,
728
2156
  normalizeE164,
729
- parseAmountInput
2157
+ parseAmountInput,
2158
+ parseNQR,
2159
+ parseQR,
2160
+ publicKeyFromPrivate,
2161
+ readTLV,
2162
+ routingHint,
2163
+ sign,
2164
+ signAuthorization,
2165
+ signCanonical,
2166
+ signOAC,
2167
+ signPass,
2168
+ signPaymentRequest,
2169
+ signReceipt,
2170
+ signRedemption,
2171
+ signRequestHMAC,
2172
+ verify,
2173
+ verifyAuthorization,
2174
+ verifyCanonical,
2175
+ verifyOAC,
2176
+ verifyPass,
2177
+ verifyPaymentRequest,
2178
+ verifyReceipt,
2179
+ verifyRedemption,
2180
+ verifyRequestHMAC,
2181
+ writeTLV
730
2182
  };
731
2183
  //# sourceMappingURL=index.js.map