@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/LICENSE +26 -15
- package/README.md +229 -198
- package/dist/index.cjs +2294 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1593 -0
- package/dist/index.d.ts +1309 -2
- package/dist/index.js +1459 -7
- package/dist/index.js.map +1 -1
- package/package.json +76 -50
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2294 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
ADDITIONAL_DATA_SUBFIELD: () => ADDITIONAL_DATA_SUBFIELD,
|
|
24
|
+
FIELD: () => FIELD,
|
|
25
|
+
FlurApiError: () => FlurApiError,
|
|
26
|
+
FlurCapExceededError: () => FlurCapExceededError,
|
|
27
|
+
FlurClient: () => FlurClient,
|
|
28
|
+
FlurError: () => FlurError,
|
|
29
|
+
FlurExpiredError: () => FlurExpiredError,
|
|
30
|
+
FlurReplayError: () => FlurReplayError,
|
|
31
|
+
NGN_CURRENCY_CODE: () => NGN_CURRENCY_CODE,
|
|
32
|
+
NG_COUNTRY_CODE: () => NG_COUNTRY_CODE,
|
|
33
|
+
NQRParseError: () => NQRParseError,
|
|
34
|
+
OACSchema: () => OACSchema,
|
|
35
|
+
OAC_DEFAULT_CUMULATIVE_KOBO: () => OAC_DEFAULT_CUMULATIVE_KOBO,
|
|
36
|
+
OAC_DEFAULT_PER_TX_KOBO: () => OAC_DEFAULT_PER_TX_KOBO,
|
|
37
|
+
OAC_DEFAULT_VALIDITY_MS: () => OAC_DEFAULT_VALIDITY_MS,
|
|
38
|
+
OfflinePaymentAuthorizationSchema: () => OfflinePaymentAuthorizationSchema,
|
|
39
|
+
OfflinePaymentRequestSchema: () => OfflinePaymentRequestSchema,
|
|
40
|
+
PASS_KINDS: () => PASS_KINDS,
|
|
41
|
+
PASS_STATES: () => PASS_STATES,
|
|
42
|
+
PAYLOAD_FORMAT_INDICATOR_VALUE: () => PAYLOAD_FORMAT_INDICATOR_VALUE,
|
|
43
|
+
POINT_OF_INITIATION: () => POINT_OF_INITIATION,
|
|
44
|
+
PassMetadataSchema: () => PassMetadataSchema,
|
|
45
|
+
PassSchema: () => PassSchema,
|
|
46
|
+
RECEIPT_CHANNELS: () => RECEIPT_CHANNELS,
|
|
47
|
+
RECEIPT_KINDS: () => RECEIPT_KINDS,
|
|
48
|
+
REPLAY_WINDOW_MS: () => REPLAY_WINDOW_MS,
|
|
49
|
+
ReceiptPayloadSchema: () => ReceiptPayloadSchema,
|
|
50
|
+
ReceiptSchema: () => ReceiptSchema,
|
|
51
|
+
RedemptionSchema: () => RedemptionSchema,
|
|
52
|
+
bodySha256Hex: () => bodySha256Hex,
|
|
53
|
+
buildAuthorization: () => buildAuthorization,
|
|
54
|
+
buildOAC: () => buildOAC,
|
|
55
|
+
buildPass: () => buildPass,
|
|
56
|
+
buildPaymentRequest: () => buildPaymentRequest,
|
|
57
|
+
buildReceipt: () => buildReceipt,
|
|
58
|
+
buildRedemption: () => buildRedemption,
|
|
59
|
+
canonicalJSONBytes: () => canonicalJSONBytes,
|
|
60
|
+
canonicalJSONStringify: () => canonicalJSONStringify,
|
|
61
|
+
canonicalRequestString: () => canonicalRequestString,
|
|
62
|
+
constantTimeEqual: () => constantTimeEqual,
|
|
63
|
+
crc16ccitt: () => crc16ccitt,
|
|
64
|
+
crc16ccittHex: () => crc16ccittHex,
|
|
65
|
+
createHmacFetch: () => createHmacFetch,
|
|
66
|
+
createPassesClient: () => createPassesClient,
|
|
67
|
+
createReceiptsClient: () => createReceiptsClient,
|
|
68
|
+
decodeAuthorizationQR: () => decodeAuthorizationQR,
|
|
69
|
+
decodeBase45: () => decodeBase45,
|
|
70
|
+
decodePaymentRequestQR: () => decodePaymentRequestQR,
|
|
71
|
+
encodeAuthorizationQR: () => encodeAuthorizationQR,
|
|
72
|
+
encodeBase45: () => encodeBase45,
|
|
73
|
+
encodeNQR: () => encodeNQR,
|
|
74
|
+
encodePaymentRequestQR: () => encodePaymentRequestQR,
|
|
75
|
+
formatAmount: () => formatAmount,
|
|
76
|
+
generateDynamicQR: () => generateDynamicQR,
|
|
77
|
+
generateKeyPair: () => generateKeyPair,
|
|
78
|
+
generateStaticQR: () => generateStaticQR,
|
|
79
|
+
init: () => init,
|
|
80
|
+
isPassWithinValidity: () => isPassWithinValidity,
|
|
81
|
+
moneyMinorToNumber: () => moneyMinorToNumber,
|
|
82
|
+
normalizeE164: () => normalizeE164,
|
|
83
|
+
parseAmountInput: () => parseAmountInput,
|
|
84
|
+
parseNQR: () => parseNQR,
|
|
85
|
+
parseQR: () => parseQR,
|
|
86
|
+
publicKeyFromPrivate: () => publicKeyFromPrivate,
|
|
87
|
+
readTLV: () => readTLV,
|
|
88
|
+
routingHint: () => routingHint,
|
|
89
|
+
sign: () => sign,
|
|
90
|
+
signAuthorization: () => signAuthorization,
|
|
91
|
+
signCanonical: () => signCanonical,
|
|
92
|
+
signOAC: () => signOAC,
|
|
93
|
+
signPass: () => signPass,
|
|
94
|
+
signPaymentRequest: () => signPaymentRequest,
|
|
95
|
+
signReceipt: () => signReceipt,
|
|
96
|
+
signRedemption: () => signRedemption,
|
|
97
|
+
signRequestHMAC: () => signRequestHMAC,
|
|
98
|
+
verify: () => verify,
|
|
99
|
+
verifyAuthorization: () => verifyAuthorization,
|
|
100
|
+
verifyCanonical: () => verifyCanonical,
|
|
101
|
+
verifyOAC: () => verifyOAC,
|
|
102
|
+
verifyPass: () => verifyPass,
|
|
103
|
+
verifyPaymentRequest: () => verifyPaymentRequest,
|
|
104
|
+
verifyReceipt: () => verifyReceipt,
|
|
105
|
+
verifyRedemption: () => verifyRedemption,
|
|
106
|
+
verifyRequestHMAC: () => verifyRequestHMAC,
|
|
107
|
+
writeTLV: () => writeTLV
|
|
108
|
+
});
|
|
109
|
+
module.exports = __toCommonJS(index_exports);
|
|
110
|
+
|
|
111
|
+
// src/client.ts
|
|
112
|
+
var import_zod3 = require("zod");
|
|
113
|
+
|
|
114
|
+
// src/contracts.ts
|
|
115
|
+
var import_zod = require("zod");
|
|
116
|
+
var E164Regex = /^\+[1-9]\d{7,14}$/;
|
|
117
|
+
var UuidSchema = import_zod.z.string().uuid();
|
|
118
|
+
var IsoDateSchema = import_zod.z.string().datetime({ offset: true });
|
|
119
|
+
var CurrencySchema = import_zod.z.string().trim().length(3).transform((value) => value.toUpperCase());
|
|
120
|
+
var HealthResponseSchema = import_zod.z.object({
|
|
121
|
+
ok: import_zod.z.boolean()
|
|
122
|
+
});
|
|
123
|
+
var WelcomeResponseSchema = import_zod.z.object({
|
|
124
|
+
message: import_zod.z.string()
|
|
125
|
+
});
|
|
126
|
+
var OnboardingStartRequestSchema = import_zod.z.object({
|
|
127
|
+
phoneE164: import_zod.z.string().regex(E164Regex),
|
|
128
|
+
appInstanceId: import_zod.z.string().min(3),
|
|
129
|
+
platform: import_zod.z.enum(["android", "ios", "web"]),
|
|
130
|
+
turnstileToken: import_zod.z.string().min(20).optional(),
|
|
131
|
+
appAttestation: import_zod.z.object({
|
|
132
|
+
provider: import_zod.z.enum(["android", "ios", "web"]),
|
|
133
|
+
token: import_zod.z.string().min(24)
|
|
134
|
+
}).optional(),
|
|
135
|
+
firstName: import_zod.z.string().trim().min(1).max(80).optional(),
|
|
136
|
+
lastName: import_zod.z.string().trim().min(1).max(80).optional()
|
|
137
|
+
});
|
|
138
|
+
var OnboardingStartResponseSchema = import_zod.z.object({
|
|
139
|
+
requestId: import_zod.z.string().min(1),
|
|
140
|
+
checkUrl: import_zod.z.string().url().optional(),
|
|
141
|
+
expiresInSec: import_zod.z.number().int().positive(),
|
|
142
|
+
fallback: import_zod.z.enum(["SILENT_AUTH", "OTP"])
|
|
143
|
+
});
|
|
144
|
+
var OnboardingCompleteRequestSchema = import_zod.z.object({
|
|
145
|
+
requestId: import_zod.z.string().min(1),
|
|
146
|
+
code: import_zod.z.string().min(1).max(32),
|
|
147
|
+
appInstanceId: import_zod.z.string().min(3),
|
|
148
|
+
fingerprintHash: import_zod.z.string().min(3).optional()
|
|
149
|
+
});
|
|
150
|
+
var OnboardingCompleteResponseSchema = import_zod.z.object({
|
|
151
|
+
sessionToken: import_zod.z.string().min(1),
|
|
152
|
+
userId: UuidSchema,
|
|
153
|
+
restricted: import_zod.z.boolean(),
|
|
154
|
+
risk_reasons: import_zod.z.array(import_zod.z.enum(["SIM_SWAP_RECENT", "ROAMING", "CARRIER_CHANGED"])),
|
|
155
|
+
stepUpRequired: import_zod.z.boolean().optional(),
|
|
156
|
+
riskStatus: import_zod.z.enum(["ok", "unavailable"]).optional()
|
|
157
|
+
});
|
|
158
|
+
var RegisterDeviceRequestSchema = import_zod.z.object({
|
|
159
|
+
userId: UuidSchema,
|
|
160
|
+
appInstanceId: import_zod.z.string().min(3),
|
|
161
|
+
platform: import_zod.z.string().min(2),
|
|
162
|
+
model: import_zod.z.string().optional(),
|
|
163
|
+
networkSignals: import_zod.z.object({
|
|
164
|
+
ip: import_zod.z.string().min(3),
|
|
165
|
+
asn: import_zod.z.number().int().optional(),
|
|
166
|
+
country: import_zod.z.string().min(2).optional(),
|
|
167
|
+
carrier: import_zod.z.string().optional()
|
|
168
|
+
})
|
|
169
|
+
});
|
|
170
|
+
var RegisterDeviceResponseSchema = import_zod.z.object({
|
|
171
|
+
deviceId: import_zod.z.string().min(1),
|
|
172
|
+
fingerprintHash: import_zod.z.string().min(1),
|
|
173
|
+
driftScore: import_zod.z.number(),
|
|
174
|
+
trustState: import_zod.z.enum(["TRUSTED_PRIMARY", "TRUSTED_SECONDARY", "UNVERIFIED"]),
|
|
175
|
+
stepUpRequired: import_zod.z.boolean()
|
|
176
|
+
});
|
|
177
|
+
var AuthRefreshRequestSchema = import_zod.z.object({
|
|
178
|
+
userId: UuidSchema,
|
|
179
|
+
refreshToken: import_zod.z.string().min(8),
|
|
180
|
+
appInstanceId: import_zod.z.string().min(3),
|
|
181
|
+
fingerprintHash: import_zod.z.string().min(3)
|
|
182
|
+
});
|
|
183
|
+
var AuthRefreshResponseSchema = import_zod.z.object({
|
|
184
|
+
refreshToken: import_zod.z.string().min(8),
|
|
185
|
+
stepUpRequired: import_zod.z.boolean()
|
|
186
|
+
});
|
|
187
|
+
var AuthLogoutRequestSchema = import_zod.z.object({
|
|
188
|
+
userId: UuidSchema,
|
|
189
|
+
refreshToken: import_zod.z.string().min(8)
|
|
190
|
+
});
|
|
191
|
+
var PinSetRequestSchema = import_zod.z.object({
|
|
192
|
+
userId: UuidSchema,
|
|
193
|
+
pin: import_zod.z.string().regex(/^\d{6}$/)
|
|
194
|
+
});
|
|
195
|
+
var PinVerifyRequestSchema = import_zod.z.object({
|
|
196
|
+
userId: UuidSchema,
|
|
197
|
+
pin: import_zod.z.string().regex(/^\d{6}$/)
|
|
198
|
+
});
|
|
199
|
+
var OkResponseSchema = import_zod.z.object({
|
|
200
|
+
ok: import_zod.z.boolean()
|
|
201
|
+
});
|
|
202
|
+
var RegisterSendDeviceKeyRequestSchema = import_zod.z.object({
|
|
203
|
+
userId: UuidSchema,
|
|
204
|
+
deviceId: import_zod.z.string().min(3),
|
|
205
|
+
publicKey: import_zod.z.string().min(32)
|
|
206
|
+
});
|
|
207
|
+
var SendChallengeRequestSchema = import_zod.z.object({
|
|
208
|
+
userId: UuidSchema,
|
|
209
|
+
deviceId: import_zod.z.string().min(3)
|
|
210
|
+
});
|
|
211
|
+
var SendChallengeResponseSchema = import_zod.z.object({
|
|
212
|
+
challengeId: UuidSchema,
|
|
213
|
+
nonce: import_zod.z.string().min(1),
|
|
214
|
+
expiresAt: IsoDateSchema
|
|
215
|
+
});
|
|
216
|
+
var SendVerifyRequestSchema = import_zod.z.object({
|
|
217
|
+
userId: UuidSchema,
|
|
218
|
+
deviceId: import_zod.z.string().min(3),
|
|
219
|
+
challengeId: UuidSchema,
|
|
220
|
+
signature: import_zod.z.string().min(16)
|
|
221
|
+
});
|
|
222
|
+
var SendVerifyResponseSchema = import_zod.z.object({
|
|
223
|
+
sendAuthToken: import_zod.z.string().min(16)
|
|
224
|
+
});
|
|
225
|
+
var ResolveRecipientRequestSchema = import_zod.z.object({
|
|
226
|
+
identifier: import_zod.z.string().min(3)
|
|
227
|
+
});
|
|
228
|
+
var ResolveRecipientResponseSchema = import_zod.z.object({
|
|
229
|
+
recipientUserId: UuidSchema,
|
|
230
|
+
displayName: import_zod.z.string().min(1),
|
|
231
|
+
normalizedIdentifier: import_zod.z.string().regex(E164Regex),
|
|
232
|
+
isActive: import_zod.z.boolean()
|
|
233
|
+
});
|
|
234
|
+
var CreateTransferRequestSchema = import_zod.z.object({
|
|
235
|
+
recipientIdentifier: import_zod.z.string().min(3),
|
|
236
|
+
amountMinor: import_zod.z.number().int().positive(),
|
|
237
|
+
currency: CurrencySchema,
|
|
238
|
+
sendAuthToken: import_zod.z.string().min(16)
|
|
239
|
+
});
|
|
240
|
+
var TransferStatusSchema = import_zod.z.enum(["SETTLED", "PENDING_REVIEW", "DECLINED"]);
|
|
241
|
+
var TransferResponseSchema = import_zod.z.object({
|
|
242
|
+
transactionId: import_zod.z.string().min(1),
|
|
243
|
+
status: TransferStatusSchema,
|
|
244
|
+
userStatus: TransferStatusSchema,
|
|
245
|
+
recipientName: import_zod.z.string().min(1),
|
|
246
|
+
timestamp: IsoDateSchema
|
|
247
|
+
});
|
|
248
|
+
var DirectionSchema = import_zod.z.enum(["OUTGOING", "INCOMING"]);
|
|
249
|
+
var AccountActivityItemSchema = import_zod.z.object({
|
|
250
|
+
id: import_zod.z.string().min(1),
|
|
251
|
+
type: import_zod.z.string().min(1),
|
|
252
|
+
direction: DirectionSchema,
|
|
253
|
+
name: import_zod.z.string().min(1),
|
|
254
|
+
identifier: import_zod.z.string().min(1),
|
|
255
|
+
amountMinor: import_zod.z.number().int(),
|
|
256
|
+
currency: CurrencySchema,
|
|
257
|
+
status: import_zod.z.string().min(1),
|
|
258
|
+
timestamp: IsoDateSchema
|
|
259
|
+
});
|
|
260
|
+
var AccountSummaryResponseSchema = import_zod.z.object({
|
|
261
|
+
balance: import_zod.z.number().int(),
|
|
262
|
+
currency: CurrencySchema,
|
|
263
|
+
dailySendLimit: import_zod.z.number().int().nonnegative(),
|
|
264
|
+
dailySendRemaining: import_zod.z.number().int().nonnegative(),
|
|
265
|
+
kycTier: import_zod.z.string().min(1),
|
|
266
|
+
kycStatus: import_zod.z.string().min(1),
|
|
267
|
+
recentActivity: import_zod.z.array(AccountActivityItemSchema)
|
|
268
|
+
});
|
|
269
|
+
var TransactionsListResponseSchema = import_zod.z.object({
|
|
270
|
+
items: import_zod.z.array(AccountActivityItemSchema),
|
|
271
|
+
nextCursor: import_zod.z.string().nullable()
|
|
272
|
+
});
|
|
273
|
+
var TransactionDetailResponseSchema = import_zod.z.object({
|
|
274
|
+
transactionId: import_zod.z.string().min(1),
|
|
275
|
+
type: import_zod.z.string().min(1),
|
|
276
|
+
direction: DirectionSchema,
|
|
277
|
+
counterpartyName: import_zod.z.string().min(1),
|
|
278
|
+
counterpartyIdentifier: import_zod.z.string().min(1),
|
|
279
|
+
amountMinor: import_zod.z.number().int(),
|
|
280
|
+
currency: CurrencySchema,
|
|
281
|
+
status: import_zod.z.string().min(1),
|
|
282
|
+
timestamp: IsoDateSchema
|
|
283
|
+
});
|
|
284
|
+
var PushRegisterRequestSchema = import_zod.z.object({
|
|
285
|
+
deviceId: import_zod.z.string().min(3),
|
|
286
|
+
platform: import_zod.z.enum(["ios", "android", "web"]),
|
|
287
|
+
token: import_zod.z.string().min(16)
|
|
288
|
+
});
|
|
289
|
+
var CreatePayLinkResponseSchema = import_zod.z.object({
|
|
290
|
+
token: import_zod.z.string().min(1)
|
|
291
|
+
});
|
|
292
|
+
var ResolvePayLinkResponseSchema = import_zod.z.object({
|
|
293
|
+
recipientUserId: UuidSchema,
|
|
294
|
+
displayName: import_zod.z.string().min(1),
|
|
295
|
+
normalizedIdentifier: import_zod.z.string().regex(E164Regex),
|
|
296
|
+
isActive: import_zod.z.boolean()
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// src/errors.ts
|
|
300
|
+
var backendErrorCodeSet = /* @__PURE__ */ new Set([
|
|
301
|
+
"UNAUTHORIZED",
|
|
302
|
+
"INVALID_REQUEST",
|
|
303
|
+
"RATE_LIMITED",
|
|
304
|
+
"NOT_FOUND",
|
|
305
|
+
"USER_NOT_FOUND",
|
|
306
|
+
"TOKEN_REPLAYED",
|
|
307
|
+
"SESSION_MISMATCH",
|
|
308
|
+
"PIN_INVALID",
|
|
309
|
+
"PIN_LOCKED",
|
|
310
|
+
"PIN_NOT_SET",
|
|
311
|
+
"STEP_UP_REQUIRED",
|
|
312
|
+
"DEVICE_KEY_NOT_REGISTERED",
|
|
313
|
+
"CHALLENGE_INVALID",
|
|
314
|
+
"CHALLENGE_EXPIRED",
|
|
315
|
+
"DEVICE_KEY_REVOKED",
|
|
316
|
+
"SIGNATURE_INVALID",
|
|
317
|
+
"INVALID_RECIPIENT",
|
|
318
|
+
"SEND_AUTH_INVALID",
|
|
319
|
+
"IDEMPOTENCY_KEY_CONFLICT",
|
|
320
|
+
"IDEMPOTENCY_IN_PROGRESS",
|
|
321
|
+
"CANNOT_SEND_TO_SELF",
|
|
322
|
+
"INSUFFICIENT_FUNDS"
|
|
323
|
+
]);
|
|
324
|
+
var FlurError = class extends Error {
|
|
325
|
+
code;
|
|
326
|
+
status;
|
|
327
|
+
details;
|
|
328
|
+
reqId;
|
|
329
|
+
constructor(message, code, opts) {
|
|
330
|
+
super(message);
|
|
331
|
+
this.name = "FlurError";
|
|
332
|
+
this.code = code;
|
|
333
|
+
this.status = opts?.status;
|
|
334
|
+
this.details = opts?.details;
|
|
335
|
+
this.reqId = opts?.reqId;
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
var FlurApiError = class extends Error {
|
|
339
|
+
constructor(status, code, message, raw) {
|
|
340
|
+
super(message);
|
|
341
|
+
this.status = status;
|
|
342
|
+
this.code = code;
|
|
343
|
+
this.raw = raw;
|
|
344
|
+
this.name = "FlurApiError";
|
|
345
|
+
}
|
|
346
|
+
status;
|
|
347
|
+
code;
|
|
348
|
+
raw;
|
|
349
|
+
};
|
|
350
|
+
var FlurExpiredError = class extends Error {
|
|
351
|
+
code = "PASS_EXPIRED";
|
|
352
|
+
constructor(message = "pass is expired") {
|
|
353
|
+
super(message);
|
|
354
|
+
this.name = "FlurExpiredError";
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
var FlurReplayError = class extends Error {
|
|
358
|
+
code = "PASS_REPLAY";
|
|
359
|
+
constructor(message = "redemption counter is not strictly increasing") {
|
|
360
|
+
super(message);
|
|
361
|
+
this.name = "FlurReplayError";
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
var FlurCapExceededError = class extends Error {
|
|
365
|
+
code = "PASS_CAP_EXCEEDED";
|
|
366
|
+
constructor(message = "redemption exceeds cumulative cap") {
|
|
367
|
+
super(message);
|
|
368
|
+
this.name = "FlurCapExceededError";
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
async function mapToFlurError(res) {
|
|
372
|
+
const reqId = res.headers.get("x-request-id");
|
|
373
|
+
let details = void 0;
|
|
374
|
+
let mappedCode = "HTTP_ERROR";
|
|
375
|
+
let mappedMessage = `HTTP ${res.status}`;
|
|
376
|
+
try {
|
|
377
|
+
const ct = res.headers.get("content-type") ?? "";
|
|
378
|
+
details = ct.includes("application/json") ? await res.json() : await res.text();
|
|
379
|
+
if (details && typeof details === "object") {
|
|
380
|
+
const candidateCode = details.code;
|
|
381
|
+
const candidateMessage = details.message;
|
|
382
|
+
const fallbackMessage = details.error;
|
|
383
|
+
if (typeof candidateCode === "string" && backendErrorCodeSet.has(candidateCode)) {
|
|
384
|
+
mappedCode = candidateCode;
|
|
385
|
+
}
|
|
386
|
+
if (typeof candidateMessage === "string" && candidateMessage.length > 0) {
|
|
387
|
+
mappedMessage = candidateMessage;
|
|
388
|
+
} else if (typeof fallbackMessage === "string" && fallbackMessage.length > 0) {
|
|
389
|
+
mappedMessage = fallbackMessage;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
} catch {
|
|
393
|
+
}
|
|
394
|
+
return new FlurError(mappedMessage, mappedCode, {
|
|
395
|
+
status: res.status,
|
|
396
|
+
details,
|
|
397
|
+
reqId
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// src/primitives.ts
|
|
402
|
+
var import_zod2 = require("zod");
|
|
403
|
+
var CurrencyCodeSchema = import_zod2.z.string().trim().length(3).transform((value) => value.toUpperCase());
|
|
404
|
+
var currencyFractionDigits = {
|
|
405
|
+
NGN: 2,
|
|
406
|
+
USD: 2,
|
|
407
|
+
EUR: 2,
|
|
408
|
+
GBP: 2,
|
|
409
|
+
CAD: 2,
|
|
410
|
+
JPY: 0
|
|
411
|
+
};
|
|
412
|
+
function getFractionDigits(currency) {
|
|
413
|
+
return currencyFractionDigits[currency] ?? 2;
|
|
414
|
+
}
|
|
415
|
+
function parseAmountInput(value, currency) {
|
|
416
|
+
const normalizedCurrency = CurrencyCodeSchema.parse(currency);
|
|
417
|
+
const raw = value.trim();
|
|
418
|
+
if (!/^\d+(\.\d+)?$/.test(raw)) {
|
|
419
|
+
throw new FlurError("Invalid amount format", "INVALID_REQUEST");
|
|
420
|
+
}
|
|
421
|
+
const [whole, fraction = ""] = raw.split(".");
|
|
422
|
+
const fractionDigits = getFractionDigits(normalizedCurrency);
|
|
423
|
+
if (fraction.length > fractionDigits) {
|
|
424
|
+
throw new FlurError("Too many decimal places for currency", "INVALID_REQUEST");
|
|
425
|
+
}
|
|
426
|
+
const paddedFraction = fraction.padEnd(fractionDigits, "0");
|
|
427
|
+
const minorAsString = `${whole}${paddedFraction}`.replace(/^0+(\d)/, "$1") || "0";
|
|
428
|
+
const amountMinor = BigInt(minorAsString);
|
|
429
|
+
if (amountMinor < 0n) {
|
|
430
|
+
throw new FlurError("Amount cannot be negative", "INVALID_REQUEST");
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
amountMinor,
|
|
434
|
+
currency: normalizedCurrency
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
function formatAmount(amountMinor, currency) {
|
|
438
|
+
const normalizedCurrency = CurrencyCodeSchema.parse(currency);
|
|
439
|
+
if (amountMinor < 0n) {
|
|
440
|
+
throw new FlurError("Amount cannot be negative", "INVALID_REQUEST");
|
|
441
|
+
}
|
|
442
|
+
const fractionDigits = getFractionDigits(normalizedCurrency);
|
|
443
|
+
if (fractionDigits === 0) {
|
|
444
|
+
return amountMinor.toString();
|
|
445
|
+
}
|
|
446
|
+
const divisor = 10n ** BigInt(fractionDigits);
|
|
447
|
+
const whole = amountMinor / divisor;
|
|
448
|
+
const fraction = (amountMinor % divisor).toString().padStart(fractionDigits, "0");
|
|
449
|
+
return `${whole.toString()}.${fraction}`;
|
|
450
|
+
}
|
|
451
|
+
var defaultCountryDialingCode = {
|
|
452
|
+
NG: "234",
|
|
453
|
+
US: "1",
|
|
454
|
+
CA: "1",
|
|
455
|
+
GB: "44"
|
|
456
|
+
};
|
|
457
|
+
function normalizeE164(input, defaultCountry) {
|
|
458
|
+
const trimmed = input.trim();
|
|
459
|
+
if (trimmed.startsWith("+")) {
|
|
460
|
+
if (!E164Regex.test(trimmed)) {
|
|
461
|
+
throw new FlurError("Invalid phone number", "INVALID_REQUEST");
|
|
462
|
+
}
|
|
463
|
+
return trimmed;
|
|
464
|
+
}
|
|
465
|
+
const digits = trimmed.replace(/\D/g, "");
|
|
466
|
+
if (digits.length === 0) {
|
|
467
|
+
throw new FlurError("Invalid phone number", "INVALID_REQUEST");
|
|
468
|
+
}
|
|
469
|
+
if (digits.startsWith("00")) {
|
|
470
|
+
const candidate2 = `+${digits.slice(2)}`;
|
|
471
|
+
if (!E164Regex.test(candidate2)) {
|
|
472
|
+
throw new FlurError("Invalid phone number", "INVALID_REQUEST");
|
|
473
|
+
}
|
|
474
|
+
return candidate2;
|
|
475
|
+
}
|
|
476
|
+
const normalizedCountry = defaultCountry?.trim().toUpperCase();
|
|
477
|
+
const countryCode = normalizedCountry ? defaultCountryDialingCode[normalizedCountry] : void 0;
|
|
478
|
+
if (!countryCode) {
|
|
479
|
+
throw new FlurError("Invalid phone number", "INVALID_REQUEST");
|
|
480
|
+
}
|
|
481
|
+
const localDigits = digits.startsWith("0") ? digits.slice(1) : digits;
|
|
482
|
+
const candidate = `+${countryCode}${localDigits}`;
|
|
483
|
+
if (!E164Regex.test(candidate)) {
|
|
484
|
+
throw new FlurError("Invalid phone number", "INVALID_REQUEST");
|
|
485
|
+
}
|
|
486
|
+
return candidate;
|
|
487
|
+
}
|
|
488
|
+
function moneyMinorToNumber(amountMinor) {
|
|
489
|
+
const asNumber = Number(amountMinor);
|
|
490
|
+
if (!Number.isSafeInteger(asNumber)) {
|
|
491
|
+
throw new FlurError("Amount exceeds safe integer range", "INVALID_REQUEST");
|
|
492
|
+
}
|
|
493
|
+
return asNumber;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// src/client.ts
|
|
497
|
+
var FlurClient = class {
|
|
498
|
+
baseUrl;
|
|
499
|
+
fetchImpl;
|
|
500
|
+
timeoutMs;
|
|
501
|
+
getExtraHeaders;
|
|
502
|
+
constructor(opts) {
|
|
503
|
+
this.baseUrl = opts.baseUrl.replace(/\/$/, "");
|
|
504
|
+
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
505
|
+
this.timeoutMs = opts.timeoutMs ?? 1e4;
|
|
506
|
+
this.getExtraHeaders = opts.getExtraHeaders;
|
|
507
|
+
}
|
|
508
|
+
async health() {
|
|
509
|
+
return this.requestJson("/health", { method: "GET" }, void 0, HealthResponseSchema);
|
|
510
|
+
}
|
|
511
|
+
async welcome() {
|
|
512
|
+
return this.requestJson("/welcome", { method: "GET" }, void 0, WelcomeResponseSchema);
|
|
513
|
+
}
|
|
514
|
+
async onboardingStart(input) {
|
|
515
|
+
return this.requestJson(
|
|
516
|
+
"/v1/onboarding/start",
|
|
517
|
+
{
|
|
518
|
+
method: "POST",
|
|
519
|
+
headers: { "content-type": "application/json" }
|
|
520
|
+
},
|
|
521
|
+
OnboardingStartRequestSchema,
|
|
522
|
+
OnboardingStartResponseSchema,
|
|
523
|
+
input
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
async onboardingComplete(input) {
|
|
527
|
+
return this.requestJson(
|
|
528
|
+
"/v1/onboarding/complete",
|
|
529
|
+
{
|
|
530
|
+
method: "POST",
|
|
531
|
+
headers: { "content-type": "application/json" }
|
|
532
|
+
},
|
|
533
|
+
OnboardingCompleteRequestSchema,
|
|
534
|
+
OnboardingCompleteResponseSchema,
|
|
535
|
+
input
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
async registerDevice(input, options) {
|
|
539
|
+
return this.requestJson(
|
|
540
|
+
"/v1/devices/register",
|
|
541
|
+
{
|
|
542
|
+
method: "POST",
|
|
543
|
+
headers: {
|
|
544
|
+
"content-type": "application/json",
|
|
545
|
+
authorization: `Bearer ${options.accessToken}`
|
|
546
|
+
}
|
|
547
|
+
},
|
|
548
|
+
RegisterDeviceRequestSchema,
|
|
549
|
+
RegisterDeviceResponseSchema,
|
|
550
|
+
input
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
async authRefresh(input) {
|
|
554
|
+
return this.requestJson(
|
|
555
|
+
"/v1/auth/refresh",
|
|
556
|
+
{
|
|
557
|
+
method: "POST",
|
|
558
|
+
headers: { "content-type": "application/json" }
|
|
559
|
+
},
|
|
560
|
+
AuthRefreshRequestSchema,
|
|
561
|
+
AuthRefreshResponseSchema,
|
|
562
|
+
input
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
async authLogout(input, options) {
|
|
566
|
+
return this.requestJson(
|
|
567
|
+
"/v1/auth/logout",
|
|
568
|
+
{
|
|
569
|
+
method: "POST",
|
|
570
|
+
headers: {
|
|
571
|
+
"content-type": "application/json",
|
|
572
|
+
authorization: `Bearer ${options.accessToken}`
|
|
573
|
+
}
|
|
574
|
+
},
|
|
575
|
+
AuthLogoutRequestSchema,
|
|
576
|
+
OkResponseSchema,
|
|
577
|
+
input
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
async pinSet(input, options) {
|
|
581
|
+
return this.requestJson(
|
|
582
|
+
"/v1/auth/pin/set",
|
|
583
|
+
{
|
|
584
|
+
method: "POST",
|
|
585
|
+
headers: {
|
|
586
|
+
"content-type": "application/json",
|
|
587
|
+
authorization: `Bearer ${options.accessToken}`
|
|
588
|
+
}
|
|
589
|
+
},
|
|
590
|
+
PinSetRequestSchema,
|
|
591
|
+
OkResponseSchema,
|
|
592
|
+
input
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
async pinVerify(input, options) {
|
|
596
|
+
return this.requestJson(
|
|
597
|
+
"/v1/auth/pin/verify",
|
|
598
|
+
{
|
|
599
|
+
method: "POST",
|
|
600
|
+
headers: {
|
|
601
|
+
"content-type": "application/json",
|
|
602
|
+
authorization: `Bearer ${options.accessToken}`
|
|
603
|
+
}
|
|
604
|
+
},
|
|
605
|
+
PinVerifyRequestSchema,
|
|
606
|
+
OkResponseSchema,
|
|
607
|
+
input
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
async registerSendDeviceKey(input, options) {
|
|
611
|
+
return this.requestJson(
|
|
612
|
+
"/api/v1/auth/send/device-key",
|
|
613
|
+
{
|
|
614
|
+
method: "POST",
|
|
615
|
+
headers: {
|
|
616
|
+
"content-type": "application/json",
|
|
617
|
+
authorization: `Bearer ${options.accessToken}`
|
|
618
|
+
}
|
|
619
|
+
},
|
|
620
|
+
RegisterSendDeviceKeyRequestSchema,
|
|
621
|
+
OkResponseSchema,
|
|
622
|
+
input
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
async createSendChallenge(input, options) {
|
|
626
|
+
return this.requestJson(
|
|
627
|
+
"/api/v1/auth/send/challenge",
|
|
628
|
+
{
|
|
629
|
+
method: "POST",
|
|
630
|
+
headers: {
|
|
631
|
+
"content-type": "application/json",
|
|
632
|
+
authorization: `Bearer ${options.accessToken}`
|
|
633
|
+
}
|
|
634
|
+
},
|
|
635
|
+
SendChallengeRequestSchema,
|
|
636
|
+
SendChallengeResponseSchema,
|
|
637
|
+
input
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
async verifySendChallenge(input, options) {
|
|
641
|
+
return this.requestJson(
|
|
642
|
+
"/api/v1/auth/send/verify",
|
|
643
|
+
{
|
|
644
|
+
method: "POST",
|
|
645
|
+
headers: {
|
|
646
|
+
"content-type": "application/json",
|
|
647
|
+
authorization: `Bearer ${options.accessToken}`
|
|
648
|
+
}
|
|
649
|
+
},
|
|
650
|
+
SendVerifyRequestSchema,
|
|
651
|
+
SendVerifyResponseSchema,
|
|
652
|
+
input
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
async registerBiometricDeviceKey(input) {
|
|
656
|
+
const publicKey = await input.signer.getOrCreateKeyPair(input.deviceId);
|
|
657
|
+
return this.registerSendDeviceKey({
|
|
658
|
+
userId: input.userId,
|
|
659
|
+
deviceId: input.deviceId,
|
|
660
|
+
publicKey
|
|
661
|
+
}, { accessToken: input.accessToken });
|
|
662
|
+
}
|
|
663
|
+
async authorizeSendWithBiometric(input) {
|
|
664
|
+
const challenge = await this.createSendChallenge({
|
|
665
|
+
userId: input.userId,
|
|
666
|
+
deviceId: input.deviceId
|
|
667
|
+
}, { accessToken: input.accessToken });
|
|
668
|
+
const signature = await input.signer.sign(challenge.nonce);
|
|
669
|
+
return this.verifySendChallenge({
|
|
670
|
+
userId: input.userId,
|
|
671
|
+
deviceId: input.deviceId,
|
|
672
|
+
challengeId: challenge.challengeId,
|
|
673
|
+
signature
|
|
674
|
+
}, { accessToken: input.accessToken });
|
|
675
|
+
}
|
|
676
|
+
async resolveRecipient(input, options) {
|
|
677
|
+
return this.requestJson(
|
|
678
|
+
"/api/v1/recipients/resolve",
|
|
679
|
+
{
|
|
680
|
+
method: "POST",
|
|
681
|
+
headers: {
|
|
682
|
+
"content-type": "application/json",
|
|
683
|
+
authorization: `Bearer ${options.accessToken}`
|
|
684
|
+
}
|
|
685
|
+
},
|
|
686
|
+
ResolveRecipientRequestSchema,
|
|
687
|
+
ResolveRecipientResponseSchema,
|
|
688
|
+
input
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
async createTransfer(input, options) {
|
|
692
|
+
return this.requestJson(
|
|
693
|
+
"/api/v1/transfers",
|
|
694
|
+
{
|
|
695
|
+
method: "POST",
|
|
696
|
+
headers: {
|
|
697
|
+
"content-type": "application/json",
|
|
698
|
+
authorization: `Bearer ${options.accessToken}`,
|
|
699
|
+
"x-device-id": options.deviceId,
|
|
700
|
+
"x-idempotency-key": options.idempotencyKey
|
|
701
|
+
}
|
|
702
|
+
},
|
|
703
|
+
CreateTransferRequestSchema,
|
|
704
|
+
TransferResponseSchema,
|
|
705
|
+
input
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
async sendMoney(input, options) {
|
|
709
|
+
const normalizedRecipient = E164Regex.test(input.recipientIdentifier) ? input.recipientIdentifier : normalizeE164(input.recipientIdentifier, input.defaultCountry);
|
|
710
|
+
const idempotencyKey = input.idempotencyKey ?? getSecureRandomUuid();
|
|
711
|
+
return this.createTransfer(
|
|
712
|
+
{
|
|
713
|
+
recipientIdentifier: normalizedRecipient,
|
|
714
|
+
amountMinor: moneyMinorToNumber(input.money.amountMinor),
|
|
715
|
+
currency: input.money.currency,
|
|
716
|
+
sendAuthToken: input.sendAuthToken
|
|
717
|
+
},
|
|
718
|
+
{
|
|
719
|
+
accessToken: options.accessToken,
|
|
720
|
+
deviceId: options.deviceId,
|
|
721
|
+
idempotencyKey
|
|
722
|
+
}
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
async accountSummary(options) {
|
|
726
|
+
return this.requestJson(
|
|
727
|
+
"/api/v1/account/summary",
|
|
728
|
+
{
|
|
729
|
+
method: "GET",
|
|
730
|
+
headers: {
|
|
731
|
+
authorization: `Bearer ${options.accessToken}`
|
|
732
|
+
}
|
|
733
|
+
},
|
|
734
|
+
void 0,
|
|
735
|
+
AccountSummaryResponseSchema
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
async listTransactions(options) {
|
|
739
|
+
const query = new URLSearchParams();
|
|
740
|
+
if (typeof options.cursor === "string" && options.cursor.trim().length > 0) {
|
|
741
|
+
query.set("cursor", options.cursor);
|
|
742
|
+
}
|
|
743
|
+
if (typeof options.limit === "number") {
|
|
744
|
+
query.set("limit", String(options.limit));
|
|
745
|
+
}
|
|
746
|
+
const path = query.size > 0 ? `/api/v1/transactions?${query.toString()}` : "/api/v1/transactions";
|
|
747
|
+
return this.requestJson(
|
|
748
|
+
path,
|
|
749
|
+
{
|
|
750
|
+
method: "GET",
|
|
751
|
+
headers: {
|
|
752
|
+
authorization: `Bearer ${options.accessToken}`
|
|
753
|
+
}
|
|
754
|
+
},
|
|
755
|
+
void 0,
|
|
756
|
+
TransactionsListResponseSchema
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
async transactionDetail(transactionId, options) {
|
|
760
|
+
const encodedId = encodeURIComponent(transactionId);
|
|
761
|
+
return this.requestJson(
|
|
762
|
+
`/api/v1/transactions/${encodedId}`,
|
|
763
|
+
{
|
|
764
|
+
method: "GET",
|
|
765
|
+
headers: {
|
|
766
|
+
authorization: `Bearer ${options.accessToken}`
|
|
767
|
+
}
|
|
768
|
+
},
|
|
769
|
+
void 0,
|
|
770
|
+
TransactionDetailResponseSchema
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
async registerPushToken(input, options) {
|
|
774
|
+
return this.requestJson(
|
|
775
|
+
"/api/v1/push/register",
|
|
776
|
+
{
|
|
777
|
+
method: "POST",
|
|
778
|
+
headers: {
|
|
779
|
+
"content-type": "application/json",
|
|
780
|
+
authorization: `Bearer ${options.accessToken}`
|
|
781
|
+
}
|
|
782
|
+
},
|
|
783
|
+
PushRegisterRequestSchema,
|
|
784
|
+
OkResponseSchema,
|
|
785
|
+
input
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
async createPayLink(options) {
|
|
789
|
+
return this.requestJson(
|
|
790
|
+
"/api/v1/pay-links",
|
|
791
|
+
{
|
|
792
|
+
method: "POST",
|
|
793
|
+
headers: {
|
|
794
|
+
authorization: `Bearer ${options.accessToken}`
|
|
795
|
+
}
|
|
796
|
+
},
|
|
797
|
+
void 0,
|
|
798
|
+
CreatePayLinkResponseSchema
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
async resolvePayLink(token, options) {
|
|
802
|
+
const encodedToken = encodeURIComponent(token);
|
|
803
|
+
return this.requestJson(
|
|
804
|
+
`/api/v1/pay-links/${encodedToken}`,
|
|
805
|
+
{
|
|
806
|
+
method: "GET",
|
|
807
|
+
headers: {
|
|
808
|
+
authorization: `Bearer ${options.accessToken}`
|
|
809
|
+
}
|
|
810
|
+
},
|
|
811
|
+
void 0,
|
|
812
|
+
ResolvePayLinkResponseSchema
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
async requestJson(path, init2, requestSchema, responseSchema, input) {
|
|
816
|
+
const url = `${this.baseUrl}${path}`;
|
|
817
|
+
const controller = new AbortController();
|
|
818
|
+
const t = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
819
|
+
let body = init2.body;
|
|
820
|
+
try {
|
|
821
|
+
body = requestSchema ? JSON.stringify(requestSchema.parse(input)) : init2.body;
|
|
822
|
+
} catch (err) {
|
|
823
|
+
if (err instanceof import_zod3.z.ZodError) {
|
|
824
|
+
throw new FlurError("Invalid request payload", "INVALID_REQUEST", {
|
|
825
|
+
details: err.flatten()
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
throw err;
|
|
829
|
+
}
|
|
830
|
+
try {
|
|
831
|
+
let extraHeaders = {};
|
|
832
|
+
if (this.getExtraHeaders) {
|
|
833
|
+
extraHeaders = await this.getExtraHeaders();
|
|
834
|
+
}
|
|
835
|
+
const finalHeaders = {
|
|
836
|
+
...init2.headers,
|
|
837
|
+
...extraHeaders
|
|
838
|
+
};
|
|
839
|
+
const res = await this.fetchImpl(url, { ...init2, headers: finalHeaders, body, signal: controller.signal });
|
|
840
|
+
if (!res.ok) throw await mapToFlurError(res);
|
|
841
|
+
const payload = await res.json();
|
|
842
|
+
if (!responseSchema) return payload;
|
|
843
|
+
try {
|
|
844
|
+
return responseSchema.parse(payload);
|
|
845
|
+
} catch (err) {
|
|
846
|
+
if (err instanceof import_zod3.z.ZodError) {
|
|
847
|
+
throw new FlurError("SDK contract validation failed", "INVALID_REQUEST", {
|
|
848
|
+
details: err.flatten()
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
throw err;
|
|
852
|
+
}
|
|
853
|
+
} catch (err) {
|
|
854
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
855
|
+
throw new FlurError("Request timed out", "TIMEOUT");
|
|
856
|
+
}
|
|
857
|
+
if (err instanceof FlurError) throw err;
|
|
858
|
+
throw new FlurError("Network error", "NETWORK_ERROR", { details: String(err) });
|
|
859
|
+
} finally {
|
|
860
|
+
clearTimeout(t);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
function getSecureRandomUuid() {
|
|
865
|
+
if (typeof globalThis.crypto?.randomUUID === "function") {
|
|
866
|
+
return globalThis.crypto.randomUUID();
|
|
867
|
+
}
|
|
868
|
+
throw new FlurError("Secure UUID generator unavailable; provide idempotencyKey", "INVALID_REQUEST");
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// src/nqr/fields.ts
|
|
872
|
+
var FIELD = {
|
|
873
|
+
PAYLOAD_FORMAT_INDICATOR: "00",
|
|
874
|
+
POINT_OF_INITIATION: "01",
|
|
875
|
+
// 02..51 — Merchant Account Information templates (issuer-specific)
|
|
876
|
+
MERCHANT_CATEGORY_CODE: "52",
|
|
877
|
+
TRANSACTION_CURRENCY: "53",
|
|
878
|
+
TRANSACTION_AMOUNT: "54",
|
|
879
|
+
TIP_OR_CONVENIENCE_INDICATOR: "55",
|
|
880
|
+
VALUE_CONVENIENCE_FEE_FIXED: "56",
|
|
881
|
+
VALUE_CONVENIENCE_FEE_PERCENT: "57",
|
|
882
|
+
COUNTRY_CODE: "58",
|
|
883
|
+
MERCHANT_NAME: "59",
|
|
884
|
+
MERCHANT_CITY: "60",
|
|
885
|
+
POSTAL_CODE: "61",
|
|
886
|
+
ADDITIONAL_DATA_FIELD: "62",
|
|
887
|
+
CRC: "63",
|
|
888
|
+
MERCHANT_INFO_LANGUAGE: "64"
|
|
889
|
+
// 80..99 — Unreserved Templates
|
|
890
|
+
};
|
|
891
|
+
var ADDITIONAL_DATA_SUBFIELD = {
|
|
892
|
+
BILL_NUMBER: "01",
|
|
893
|
+
MOBILE_NUMBER: "02",
|
|
894
|
+
STORE_LABEL: "03",
|
|
895
|
+
LOYALTY_NUMBER: "04",
|
|
896
|
+
REFERENCE_LABEL: "05",
|
|
897
|
+
CUSTOMER_LABEL: "06",
|
|
898
|
+
TERMINAL_LABEL: "07",
|
|
899
|
+
PURPOSE_OF_TRANSACTION: "08"
|
|
900
|
+
};
|
|
901
|
+
var POINT_OF_INITIATION = {
|
|
902
|
+
STATIC: "11",
|
|
903
|
+
DYNAMIC: "12"
|
|
904
|
+
};
|
|
905
|
+
var PAYLOAD_FORMAT_INDICATOR_VALUE = "01";
|
|
906
|
+
var NGN_CURRENCY_CODE = "566";
|
|
907
|
+
var NG_COUNTRY_CODE = "NG";
|
|
908
|
+
var CRC_TAG_PREFIX = "6304";
|
|
909
|
+
|
|
910
|
+
// src/nqr/crc16.ts
|
|
911
|
+
function crc16ccitt(bytes) {
|
|
912
|
+
let crc = 65535;
|
|
913
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
914
|
+
crc ^= bytes[i] << 8;
|
|
915
|
+
for (let j = 0; j < 8; j++) {
|
|
916
|
+
if (crc & 32768) {
|
|
917
|
+
crc = crc << 1 ^ 4129;
|
|
918
|
+
} else {
|
|
919
|
+
crc <<= 1;
|
|
920
|
+
}
|
|
921
|
+
crc &= 65535;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
return crc;
|
|
925
|
+
}
|
|
926
|
+
function crc16ccittHex(bytes) {
|
|
927
|
+
return crc16ccitt(bytes).toString(16).toUpperCase().padStart(4, "0");
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// src/nqr/tlv.ts
|
|
931
|
+
function writeTLV(tag, value) {
|
|
932
|
+
if (!/^\d{2}$/.test(tag)) {
|
|
933
|
+
throw new Error(`TLV: tag must be 2 digits, got "${tag}"`);
|
|
934
|
+
}
|
|
935
|
+
if (value.length > 99) {
|
|
936
|
+
throw new Error(
|
|
937
|
+
`TLV: value for tag ${tag} exceeds 99 chars (${value.length})`
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
const len = value.length.toString().padStart(2, "0");
|
|
941
|
+
return `${tag}${len}${value}`;
|
|
942
|
+
}
|
|
943
|
+
function readTLV(buf) {
|
|
944
|
+
const out = [];
|
|
945
|
+
let i = 0;
|
|
946
|
+
while (i < buf.length) {
|
|
947
|
+
if (i + 4 > buf.length) {
|
|
948
|
+
throw new Error(`TLV: truncated header at offset ${i}`);
|
|
949
|
+
}
|
|
950
|
+
const tag = buf.slice(i, i + 2);
|
|
951
|
+
const lenStr = buf.slice(i + 2, i + 4);
|
|
952
|
+
if (!/^\d{2}$/.test(tag)) {
|
|
953
|
+
throw new Error(`TLV: bad tag "${tag}" at offset ${i}`);
|
|
954
|
+
}
|
|
955
|
+
if (!/^\d{2}$/.test(lenStr)) {
|
|
956
|
+
throw new Error(`TLV: bad length "${lenStr}" at offset ${i + 2}`);
|
|
957
|
+
}
|
|
958
|
+
const len = parseInt(lenStr, 10);
|
|
959
|
+
const valStart = i + 4;
|
|
960
|
+
const valEnd = valStart + len;
|
|
961
|
+
if (valEnd > buf.length) {
|
|
962
|
+
throw new Error(
|
|
963
|
+
`TLV: truncated value for tag ${tag} at offset ${valStart}`
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
out.push({ tag, value: buf.slice(valStart, valEnd) });
|
|
967
|
+
i = valEnd;
|
|
968
|
+
}
|
|
969
|
+
return out;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// src/nqr/encoder.ts
|
|
973
|
+
var ASCII_PRINTABLE = /^[\x20-\x7E]*$/;
|
|
974
|
+
function buildMAIValue(mai) {
|
|
975
|
+
if (!/^(0[2-9]|[1-4]\d|5[0-1])$/.test(mai.tag)) {
|
|
976
|
+
throw new Error(
|
|
977
|
+
`encodeNQR: merchantAccountInfo.tag must be in 02..51, got "${mai.tag}"`
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
let out = "";
|
|
981
|
+
for (const c of mai.children) {
|
|
982
|
+
out += writeTLV(c.tag, c.value);
|
|
983
|
+
}
|
|
984
|
+
return out;
|
|
985
|
+
}
|
|
986
|
+
function buildAdditionalDataValue(ad) {
|
|
987
|
+
let out = "";
|
|
988
|
+
const map = [
|
|
989
|
+
["billNumber", ADDITIONAL_DATA_SUBFIELD.BILL_NUMBER],
|
|
990
|
+
["mobileNumber", ADDITIONAL_DATA_SUBFIELD.MOBILE_NUMBER],
|
|
991
|
+
["storeLabel", ADDITIONAL_DATA_SUBFIELD.STORE_LABEL],
|
|
992
|
+
["loyaltyNumber", ADDITIONAL_DATA_SUBFIELD.LOYALTY_NUMBER],
|
|
993
|
+
["referenceLabel", ADDITIONAL_DATA_SUBFIELD.REFERENCE_LABEL],
|
|
994
|
+
["customerLabel", ADDITIONAL_DATA_SUBFIELD.CUSTOMER_LABEL],
|
|
995
|
+
["terminalLabel", ADDITIONAL_DATA_SUBFIELD.TERMINAL_LABEL],
|
|
996
|
+
["purposeOfTransaction", ADDITIONAL_DATA_SUBFIELD.PURPOSE_OF_TRANSACTION]
|
|
997
|
+
];
|
|
998
|
+
for (const [k, t] of map) {
|
|
999
|
+
const v = ad[k];
|
|
1000
|
+
if (v !== void 0) {
|
|
1001
|
+
out += writeTLV(t, v);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
return out;
|
|
1005
|
+
}
|
|
1006
|
+
function assertAscii(name, v) {
|
|
1007
|
+
if (!ASCII_PRINTABLE.test(v)) {
|
|
1008
|
+
throw new Error(
|
|
1009
|
+
`encodeNQR: ${name} contains non-printable-ASCII characters`
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
function assertMaxLen(name, v, max) {
|
|
1014
|
+
if (v.length > max) {
|
|
1015
|
+
throw new Error(`encodeNQR: ${name} length ${v.length} exceeds max ${max}`);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
function assertMCC(mcc) {
|
|
1019
|
+
if (!/^\d{4}$/.test(mcc)) {
|
|
1020
|
+
throw new Error(
|
|
1021
|
+
`encodeNQR: merchantCategoryCode must be 4 digits, got "${mcc}"`
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
function assertAmount(amt) {
|
|
1026
|
+
if (!/^\d{1,10}(\.\d{1,2})?$/.test(amt)) {
|
|
1027
|
+
throw new Error(
|
|
1028
|
+
`encodeNQR: transactionAmount must match /^\\d{1,10}(\\.\\d{1,2})?$/, got "${amt}"`
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
if (amt.length > 13) {
|
|
1032
|
+
throw new Error(
|
|
1033
|
+
`encodeNQR: transactionAmount length ${amt.length} exceeds 13`
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
function encodeNQR(input) {
|
|
1038
|
+
assertMCC(input.merchantCategoryCode);
|
|
1039
|
+
assertAscii("merchantName", input.merchantName);
|
|
1040
|
+
assertMaxLen("merchantName", input.merchantName, 25);
|
|
1041
|
+
assertAscii("merchantCity", input.merchantCity);
|
|
1042
|
+
assertMaxLen("merchantCity", input.merchantCity, 15);
|
|
1043
|
+
if (input.postalCode !== void 0) {
|
|
1044
|
+
assertAscii("postalCode", input.postalCode);
|
|
1045
|
+
assertMaxLen("postalCode", input.postalCode, 10);
|
|
1046
|
+
}
|
|
1047
|
+
if (input.transactionAmount !== void 0) {
|
|
1048
|
+
assertAmount(input.transactionAmount);
|
|
1049
|
+
}
|
|
1050
|
+
if (input.pointOfInitiation === "dynamic" && input.transactionAmount === void 0) {
|
|
1051
|
+
}
|
|
1052
|
+
let out = "";
|
|
1053
|
+
out += writeTLV(
|
|
1054
|
+
FIELD.PAYLOAD_FORMAT_INDICATOR,
|
|
1055
|
+
PAYLOAD_FORMAT_INDICATOR_VALUE
|
|
1056
|
+
);
|
|
1057
|
+
out += writeTLV(
|
|
1058
|
+
FIELD.POINT_OF_INITIATION,
|
|
1059
|
+
input.pointOfInitiation === "dynamic" ? POINT_OF_INITIATION.DYNAMIC : POINT_OF_INITIATION.STATIC
|
|
1060
|
+
);
|
|
1061
|
+
out += writeTLV(
|
|
1062
|
+
input.merchantAccountInfo.tag,
|
|
1063
|
+
buildMAIValue(input.merchantAccountInfo)
|
|
1064
|
+
);
|
|
1065
|
+
out += writeTLV(FIELD.MERCHANT_CATEGORY_CODE, input.merchantCategoryCode);
|
|
1066
|
+
out += writeTLV(FIELD.TRANSACTION_CURRENCY, NGN_CURRENCY_CODE);
|
|
1067
|
+
if (input.transactionAmount !== void 0) {
|
|
1068
|
+
out += writeTLV(FIELD.TRANSACTION_AMOUNT, input.transactionAmount);
|
|
1069
|
+
}
|
|
1070
|
+
out += writeTLV(FIELD.COUNTRY_CODE, NG_COUNTRY_CODE);
|
|
1071
|
+
out += writeTLV(FIELD.MERCHANT_NAME, input.merchantName);
|
|
1072
|
+
out += writeTLV(FIELD.MERCHANT_CITY, input.merchantCity);
|
|
1073
|
+
if (input.postalCode !== void 0) {
|
|
1074
|
+
out += writeTLV(FIELD.POSTAL_CODE, input.postalCode);
|
|
1075
|
+
}
|
|
1076
|
+
if (input.additionalData) {
|
|
1077
|
+
const adfValue = buildAdditionalDataValue(input.additionalData);
|
|
1078
|
+
if (adfValue.length > 0) {
|
|
1079
|
+
out += writeTLV(FIELD.ADDITIONAL_DATA_FIELD, adfValue);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
out += CRC_TAG_PREFIX;
|
|
1083
|
+
const crc = crc16ccittHex(new TextEncoder().encode(out));
|
|
1084
|
+
out += crc;
|
|
1085
|
+
return out;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// src/nqr/parser.ts
|
|
1089
|
+
var NQRParseError = class extends Error {
|
|
1090
|
+
path;
|
|
1091
|
+
constructor(message, path = "") {
|
|
1092
|
+
super(message);
|
|
1093
|
+
this.name = "NQRParseError";
|
|
1094
|
+
this.path = path;
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
function parseNQR(payload) {
|
|
1098
|
+
if (payload.length < 8) {
|
|
1099
|
+
throw new NQRParseError(`payload too short (${payload.length} chars)`);
|
|
1100
|
+
}
|
|
1101
|
+
const crcTagAndLen = payload.slice(-8, -4);
|
|
1102
|
+
const crcValue = payload.slice(-4);
|
|
1103
|
+
if (crcTagAndLen !== CRC_TAG_PREFIX) {
|
|
1104
|
+
throw new NQRParseError(
|
|
1105
|
+
`CRC tag prefix not found (expected "6304" at end)`
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
const beforeCrc = payload.slice(0, -4);
|
|
1109
|
+
const expectedCrc = crc16ccittHex(new TextEncoder().encode(beforeCrc));
|
|
1110
|
+
if (expectedCrc !== crcValue.toUpperCase()) {
|
|
1111
|
+
throw new NQRParseError(
|
|
1112
|
+
`CRC mismatch: payload says ${crcValue}, computed ${expectedCrc}`,
|
|
1113
|
+
"63"
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
const topBuf = payload.slice(0, -8);
|
|
1117
|
+
const top = readTLV(topBuf);
|
|
1118
|
+
const get = (tag) => top.find((f) => f.tag === tag)?.value;
|
|
1119
|
+
const pfi = get(FIELD.PAYLOAD_FORMAT_INDICATOR);
|
|
1120
|
+
if (pfi !== "01")
|
|
1121
|
+
throw new NQRParseError(
|
|
1122
|
+
`payloadFormatIndicator must be "01", got "${pfi}"`,
|
|
1123
|
+
"00"
|
|
1124
|
+
);
|
|
1125
|
+
const poiRaw = get(FIELD.POINT_OF_INITIATION);
|
|
1126
|
+
const pointOfInitiation = poiRaw === "11" ? "static" : poiRaw === "12" ? "dynamic" : "unknown";
|
|
1127
|
+
const currency = get(FIELD.TRANSACTION_CURRENCY);
|
|
1128
|
+
if (currency !== NGN_CURRENCY_CODE) {
|
|
1129
|
+
throw new NQRParseError(
|
|
1130
|
+
`unsupported currency "${currency}" (expected ${NGN_CURRENCY_CODE})`,
|
|
1131
|
+
"53"
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
const country = get(FIELD.COUNTRY_CODE);
|
|
1135
|
+
if (country !== NG_COUNTRY_CODE) {
|
|
1136
|
+
throw new NQRParseError(
|
|
1137
|
+
`unsupported country "${country}" (expected ${NG_COUNTRY_CODE})`,
|
|
1138
|
+
"58"
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
const mcc = get(FIELD.MERCHANT_CATEGORY_CODE);
|
|
1142
|
+
if (!mcc) throw new NQRParseError("merchantCategoryCode missing", "52");
|
|
1143
|
+
const merchantName = get(FIELD.MERCHANT_NAME) ?? "";
|
|
1144
|
+
const merchantCity = get(FIELD.MERCHANT_CITY) ?? "";
|
|
1145
|
+
const transactionAmount = get(FIELD.TRANSACTION_AMOUNT);
|
|
1146
|
+
const postalCode = get(FIELD.POSTAL_CODE);
|
|
1147
|
+
const mais = [];
|
|
1148
|
+
for (const f of top) {
|
|
1149
|
+
const n = parseInt(f.tag, 10);
|
|
1150
|
+
if (n >= 2 && n <= 51) {
|
|
1151
|
+
const children = readTLV(f.value).map((c) => ({
|
|
1152
|
+
tag: c.tag,
|
|
1153
|
+
value: c.value
|
|
1154
|
+
}));
|
|
1155
|
+
mais.push({ tag: f.tag, children });
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
let additionalData;
|
|
1159
|
+
const adfRaw = get(FIELD.ADDITIONAL_DATA_FIELD);
|
|
1160
|
+
if (adfRaw !== void 0) {
|
|
1161
|
+
additionalData = {};
|
|
1162
|
+
for (const c of readTLV(adfRaw)) {
|
|
1163
|
+
switch (c.tag) {
|
|
1164
|
+
case "01":
|
|
1165
|
+
additionalData.billNumber = c.value;
|
|
1166
|
+
break;
|
|
1167
|
+
case "02":
|
|
1168
|
+
additionalData.mobileNumber = c.value;
|
|
1169
|
+
break;
|
|
1170
|
+
case "03":
|
|
1171
|
+
additionalData.storeLabel = c.value;
|
|
1172
|
+
break;
|
|
1173
|
+
case "04":
|
|
1174
|
+
additionalData.loyaltyNumber = c.value;
|
|
1175
|
+
break;
|
|
1176
|
+
case "05":
|
|
1177
|
+
additionalData.referenceLabel = c.value;
|
|
1178
|
+
break;
|
|
1179
|
+
case "06":
|
|
1180
|
+
additionalData.customerLabel = c.value;
|
|
1181
|
+
break;
|
|
1182
|
+
case "07":
|
|
1183
|
+
additionalData.terminalLabel = c.value;
|
|
1184
|
+
break;
|
|
1185
|
+
case "08":
|
|
1186
|
+
additionalData.purposeOfTransaction = c.value;
|
|
1187
|
+
break;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
return {
|
|
1192
|
+
payloadFormatIndicator: pfi,
|
|
1193
|
+
pointOfInitiation,
|
|
1194
|
+
merchantAccountInfo: mais,
|
|
1195
|
+
merchantCategoryCode: mcc,
|
|
1196
|
+
transactionCurrency: currency,
|
|
1197
|
+
transactionAmount,
|
|
1198
|
+
countryCode: country,
|
|
1199
|
+
merchantName,
|
|
1200
|
+
merchantCity,
|
|
1201
|
+
postalCode,
|
|
1202
|
+
additionalData,
|
|
1203
|
+
flurReference: additionalData?.referenceLabel?.startsWith("FL-") ? additionalData.referenceLabel : void 0,
|
|
1204
|
+
raw: payload
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// src/nqr/routing.ts
|
|
1209
|
+
function routingHint(parsed) {
|
|
1210
|
+
if (parsed.flurReference && parsed.flurReference.startsWith("FL-")) {
|
|
1211
|
+
return "flur-onus";
|
|
1212
|
+
}
|
|
1213
|
+
if (parsed.merchantAccountInfo.length > 0) return "nibss";
|
|
1214
|
+
return "unknown";
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// src/crypto/canonical.ts
|
|
1218
|
+
function canonicalJSONStringify(value) {
|
|
1219
|
+
return stringify(value);
|
|
1220
|
+
}
|
|
1221
|
+
function canonicalJSONBytes(value) {
|
|
1222
|
+
return new TextEncoder().encode(canonicalJSONStringify(value));
|
|
1223
|
+
}
|
|
1224
|
+
function stringify(v) {
|
|
1225
|
+
if (v === null) return "null";
|
|
1226
|
+
const t = typeof v;
|
|
1227
|
+
if (t === "string") return JSON.stringify(v);
|
|
1228
|
+
if (t === "boolean") return v ? "true" : "false";
|
|
1229
|
+
if (t === "number") {
|
|
1230
|
+
if (!Number.isFinite(v)) {
|
|
1231
|
+
throw new Error("canonicalJSON: non-finite number not allowed");
|
|
1232
|
+
}
|
|
1233
|
+
return JSON.stringify(v);
|
|
1234
|
+
}
|
|
1235
|
+
if (t === "bigint" || t === "function" || t === "symbol" || t === "undefined") {
|
|
1236
|
+
throw new Error(`canonicalJSON: unsupported value type ${t}`);
|
|
1237
|
+
}
|
|
1238
|
+
if (Array.isArray(v)) {
|
|
1239
|
+
const parts = [];
|
|
1240
|
+
for (const item of v) parts.push(stringify(item));
|
|
1241
|
+
return "[" + parts.join(",") + "]";
|
|
1242
|
+
}
|
|
1243
|
+
if (t === "object") {
|
|
1244
|
+
const obj = v;
|
|
1245
|
+
const keys = Object.keys(obj).sort();
|
|
1246
|
+
const parts = [];
|
|
1247
|
+
for (const k of keys) {
|
|
1248
|
+
const val = obj[k];
|
|
1249
|
+
if (val === void 0) {
|
|
1250
|
+
throw new Error(`canonicalJSON: undefined value at key "${k}"`);
|
|
1251
|
+
}
|
|
1252
|
+
parts.push(JSON.stringify(k) + ":" + stringify(val));
|
|
1253
|
+
}
|
|
1254
|
+
return "{" + parts.join(",") + "}";
|
|
1255
|
+
}
|
|
1256
|
+
throw new Error(`canonicalJSON: unsupported value type ${t}`);
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
// src/crypto/ct.ts
|
|
1260
|
+
function constantTimeEqual(a, b) {
|
|
1261
|
+
if (a.length !== b.length) return false;
|
|
1262
|
+
let diff = 0;
|
|
1263
|
+
for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
|
|
1264
|
+
return diff === 0;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
// src/crypto/ed25519.ts
|
|
1268
|
+
var import_ed25519 = require("@noble/curves/ed25519");
|
|
1269
|
+
function generateKeyPair() {
|
|
1270
|
+
const privateKey = import_ed25519.ed25519.utils.randomPrivateKey();
|
|
1271
|
+
const publicKey = import_ed25519.ed25519.getPublicKey(privateKey);
|
|
1272
|
+
return { privateKey, publicKey };
|
|
1273
|
+
}
|
|
1274
|
+
function publicKeyFromPrivate(privateKey) {
|
|
1275
|
+
return import_ed25519.ed25519.getPublicKey(privateKey);
|
|
1276
|
+
}
|
|
1277
|
+
function sign(message, privateKey) {
|
|
1278
|
+
return import_ed25519.ed25519.sign(message, privateKey);
|
|
1279
|
+
}
|
|
1280
|
+
function verify(message, signature, publicKey) {
|
|
1281
|
+
try {
|
|
1282
|
+
return import_ed25519.ed25519.verify(signature, message, publicKey);
|
|
1283
|
+
} catch {
|
|
1284
|
+
return false;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
function signCanonical(value, privateKey) {
|
|
1288
|
+
return sign(canonicalJSONBytes(value), privateKey);
|
|
1289
|
+
}
|
|
1290
|
+
function verifyCanonical(value, signature, publicKey) {
|
|
1291
|
+
return verify(canonicalJSONBytes(value), signature, publicKey);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
// src/offline/oac.ts
|
|
1295
|
+
var import_zod4 = require("zod");
|
|
1296
|
+
var OAC_DEFAULT_PER_TX_KOBO = 5e5;
|
|
1297
|
+
var OAC_DEFAULT_CUMULATIVE_KOBO = 2e6;
|
|
1298
|
+
var OAC_DEFAULT_VALIDITY_MS = 24 * 60 * 60 * 1e3;
|
|
1299
|
+
var HexString = (length) => import_zod4.z.string().regex(
|
|
1300
|
+
new RegExp(`^[0-9a-fA-F]{${length * 2}}$`),
|
|
1301
|
+
`expected ${length}-byte hex string`
|
|
1302
|
+
);
|
|
1303
|
+
var OACSchema = import_zod4.z.object({
|
|
1304
|
+
userId: import_zod4.z.string().min(1),
|
|
1305
|
+
deviceId: import_zod4.z.string().min(1),
|
|
1306
|
+
devicePublicKey: HexString(32),
|
|
1307
|
+
perTxCapKobo: import_zod4.z.number().int().nonnegative(),
|
|
1308
|
+
cumulativeCapKobo: import_zod4.z.number().int().nonnegative(),
|
|
1309
|
+
validFromMs: import_zod4.z.number().int().nonnegative(),
|
|
1310
|
+
validUntilMs: import_zod4.z.number().int().positive(),
|
|
1311
|
+
counterSeed: import_zod4.z.number().int().nonnegative(),
|
|
1312
|
+
nonce: import_zod4.z.string().min(1),
|
|
1313
|
+
issuerSig: HexString(64)
|
|
1314
|
+
}).refine((v) => v.validUntilMs > v.validFromMs, {
|
|
1315
|
+
message: "validUntilMs must be greater than validFromMs"
|
|
1316
|
+
}).refine((v) => v.perTxCapKobo <= v.cumulativeCapKobo, {
|
|
1317
|
+
message: "perTxCapKobo must not exceed cumulativeCapKobo"
|
|
1318
|
+
});
|
|
1319
|
+
function buildOAC(input) {
|
|
1320
|
+
const devicePublicKey = typeof input.devicePublicKey === "string" ? input.devicePublicKey : bytesToHex(input.devicePublicKey);
|
|
1321
|
+
return {
|
|
1322
|
+
userId: input.userId,
|
|
1323
|
+
deviceId: input.deviceId,
|
|
1324
|
+
devicePublicKey,
|
|
1325
|
+
perTxCapKobo: input.perTxCapKobo ?? OAC_DEFAULT_PER_TX_KOBO,
|
|
1326
|
+
cumulativeCapKobo: input.cumulativeCapKobo ?? OAC_DEFAULT_CUMULATIVE_KOBO,
|
|
1327
|
+
validFromMs: input.validFromMs,
|
|
1328
|
+
validUntilMs: input.validUntilMs,
|
|
1329
|
+
counterSeed: input.counterSeed ?? 0,
|
|
1330
|
+
nonce: input.nonce
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
function signOAC(unsigned, issuerPrivateKey) {
|
|
1334
|
+
const issuerSig = bytesToHex(
|
|
1335
|
+
sign(canonicalJSONBytes(unsigned), issuerPrivateKey)
|
|
1336
|
+
);
|
|
1337
|
+
return { ...unsigned, issuerSig };
|
|
1338
|
+
}
|
|
1339
|
+
function verifyOAC(oac, issuerPublicKey) {
|
|
1340
|
+
try {
|
|
1341
|
+
const parsed = OACSchema.parse(oac);
|
|
1342
|
+
const { issuerSig, ...unsigned } = parsed;
|
|
1343
|
+
return verify(
|
|
1344
|
+
canonicalJSONBytes(unsigned),
|
|
1345
|
+
hexToBytes(issuerSig),
|
|
1346
|
+
issuerPublicKey
|
|
1347
|
+
);
|
|
1348
|
+
} catch {
|
|
1349
|
+
return false;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
function bytesToHex(b) {
|
|
1353
|
+
let s = "";
|
|
1354
|
+
for (let i = 0; i < b.length; i++) s += b[i].toString(16).padStart(2, "0");
|
|
1355
|
+
return s;
|
|
1356
|
+
}
|
|
1357
|
+
function hexToBytes(s) {
|
|
1358
|
+
if (s.length % 2 !== 0) throw new Error("hex: odd length");
|
|
1359
|
+
const out = new Uint8Array(s.length / 2);
|
|
1360
|
+
for (let i = 0; i < out.length; i++)
|
|
1361
|
+
out[i] = parseInt(s.slice(i * 2, i * 2 + 2), 16);
|
|
1362
|
+
return out;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
// src/offline/codec.ts
|
|
1366
|
+
var ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";
|
|
1367
|
+
var REVERSE = {};
|
|
1368
|
+
for (let i = 0; i < ALPHABET.length; i++) REVERSE[ALPHABET[i]] = i;
|
|
1369
|
+
function encodeBase45(bytes) {
|
|
1370
|
+
let out = "";
|
|
1371
|
+
let i = 0;
|
|
1372
|
+
for (; i + 1 < bytes.length; i += 2) {
|
|
1373
|
+
const x = bytes[i] << 8 | bytes[i + 1];
|
|
1374
|
+
const e = Math.floor(x / (45 * 45));
|
|
1375
|
+
const d = Math.floor(x % (45 * 45) / 45);
|
|
1376
|
+
const c = x % 45;
|
|
1377
|
+
out += ALPHABET[c] + ALPHABET[d] + ALPHABET[e];
|
|
1378
|
+
}
|
|
1379
|
+
if (i < bytes.length) {
|
|
1380
|
+
const x = bytes[i];
|
|
1381
|
+
const d = Math.floor(x / 45);
|
|
1382
|
+
const c = x % 45;
|
|
1383
|
+
out += ALPHABET[c] + ALPHABET[d];
|
|
1384
|
+
}
|
|
1385
|
+
return out;
|
|
1386
|
+
}
|
|
1387
|
+
function decodeBase45(s) {
|
|
1388
|
+
if (s.length === 0) return new Uint8Array(0);
|
|
1389
|
+
const fullChunks = Math.floor(s.length / 3);
|
|
1390
|
+
const tail = s.length - fullChunks * 3;
|
|
1391
|
+
if (tail === 1) throw new Error("base45: invalid length");
|
|
1392
|
+
const out = new Uint8Array(fullChunks * 2 + (tail === 2 ? 1 : 0));
|
|
1393
|
+
let oi = 0;
|
|
1394
|
+
for (let i = 0; i < fullChunks; i++) {
|
|
1395
|
+
const c = REVERSE[s[i * 3]];
|
|
1396
|
+
const d = REVERSE[s[i * 3 + 1]];
|
|
1397
|
+
const e = REVERSE[s[i * 3 + 2]];
|
|
1398
|
+
if (c === void 0 || d === void 0 || e === void 0) {
|
|
1399
|
+
throw new Error("base45: invalid character");
|
|
1400
|
+
}
|
|
1401
|
+
const x = c + 45 * d + 45 * 45 * e;
|
|
1402
|
+
if (x > 65535) throw new Error("base45: chunk overflow");
|
|
1403
|
+
out[oi++] = x >> 8 & 255;
|
|
1404
|
+
out[oi++] = x & 255;
|
|
1405
|
+
}
|
|
1406
|
+
if (tail === 2) {
|
|
1407
|
+
const c = REVERSE[s[fullChunks * 3]];
|
|
1408
|
+
const d = REVERSE[s[fullChunks * 3 + 1]];
|
|
1409
|
+
if (c === void 0 || d === void 0)
|
|
1410
|
+
throw new Error("base45: invalid character");
|
|
1411
|
+
const x = c + 45 * d;
|
|
1412
|
+
if (x > 255) throw new Error("base45: tail overflow");
|
|
1413
|
+
out[oi++] = x & 255;
|
|
1414
|
+
}
|
|
1415
|
+
return out;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// src/offline/messages.ts
|
|
1419
|
+
var import_zod5 = require("zod");
|
|
1420
|
+
var HexSig = import_zod5.z.string().regex(/^[0-9a-fA-F]{128}$/, "expected 64-byte hex signature");
|
|
1421
|
+
var OfflinePaymentRequestSchema = import_zod5.z.object({
|
|
1422
|
+
reference: import_zod5.z.string().min(1),
|
|
1423
|
+
amountKobo: import_zod5.z.number().int().positive(),
|
|
1424
|
+
merchantOAC: OACSchema,
|
|
1425
|
+
expiresAtMs: import_zod5.z.number().int().positive(),
|
|
1426
|
+
merchantSig: HexSig
|
|
1427
|
+
});
|
|
1428
|
+
var OfflinePaymentAuthorizationSchema = import_zod5.z.object({
|
|
1429
|
+
request: OfflinePaymentRequestSchema,
|
|
1430
|
+
payerOAC: OACSchema,
|
|
1431
|
+
payerCounter: import_zod5.z.number().int().positive(),
|
|
1432
|
+
payerSig: HexSig
|
|
1433
|
+
});
|
|
1434
|
+
function buildPaymentRequest(input) {
|
|
1435
|
+
if (!Number.isInteger(input.amountKobo) || input.amountKobo <= 0) {
|
|
1436
|
+
throw new Error("amountKobo must be a positive integer");
|
|
1437
|
+
}
|
|
1438
|
+
if (input.amountKobo > input.merchantOAC.perTxCapKobo) {
|
|
1439
|
+
throw new Error("amountKobo exceeds merchant OAC perTxCapKobo");
|
|
1440
|
+
}
|
|
1441
|
+
return {
|
|
1442
|
+
reference: input.reference,
|
|
1443
|
+
amountKobo: input.amountKobo,
|
|
1444
|
+
merchantOAC: input.merchantOAC,
|
|
1445
|
+
expiresAtMs: input.expiresAtMs
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
function signPaymentRequest(unsigned, merchantDevicePrivateKey) {
|
|
1449
|
+
const merchantSig = bytesToHex(
|
|
1450
|
+
sign(canonicalJSONBytes(unsigned), merchantDevicePrivateKey)
|
|
1451
|
+
);
|
|
1452
|
+
return { ...unsigned, merchantSig };
|
|
1453
|
+
}
|
|
1454
|
+
function verifyPaymentRequest(req, issuerPublicKey) {
|
|
1455
|
+
try {
|
|
1456
|
+
const parsed = OfflinePaymentRequestSchema.parse(req);
|
|
1457
|
+
const { issuerSig: merchantOacSig, ...merchantOacUnsigned } = parsed.merchantOAC;
|
|
1458
|
+
if (!verify(
|
|
1459
|
+
canonicalJSONBytes(merchantOacUnsigned),
|
|
1460
|
+
hexToBytes(merchantOacSig),
|
|
1461
|
+
issuerPublicKey
|
|
1462
|
+
)) {
|
|
1463
|
+
return false;
|
|
1464
|
+
}
|
|
1465
|
+
const { merchantSig, ...unsigned } = parsed;
|
|
1466
|
+
return verify(
|
|
1467
|
+
canonicalJSONBytes(unsigned),
|
|
1468
|
+
hexToBytes(merchantSig),
|
|
1469
|
+
hexToBytes(parsed.merchantOAC.devicePublicKey)
|
|
1470
|
+
);
|
|
1471
|
+
} catch {
|
|
1472
|
+
return false;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
function buildAuthorization(input) {
|
|
1476
|
+
if (!Number.isInteger(input.payerCounter) || input.payerCounter <= 0) {
|
|
1477
|
+
throw new Error("payerCounter must be a positive integer");
|
|
1478
|
+
}
|
|
1479
|
+
if (input.request.amountKobo > input.payerOAC.perTxCapKobo) {
|
|
1480
|
+
throw new Error("amountKobo exceeds payer OAC perTxCapKobo");
|
|
1481
|
+
}
|
|
1482
|
+
return {
|
|
1483
|
+
request: input.request,
|
|
1484
|
+
payerOAC: input.payerOAC,
|
|
1485
|
+
payerCounter: input.payerCounter
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
function signAuthorization(unsigned, payerDevicePrivateKey) {
|
|
1489
|
+
const payerSig = bytesToHex(
|
|
1490
|
+
sign(canonicalJSONBytes(unsigned), payerDevicePrivateKey)
|
|
1491
|
+
);
|
|
1492
|
+
return { ...unsigned, payerSig };
|
|
1493
|
+
}
|
|
1494
|
+
function verifyAuthorization(auth, issuerPublicKey) {
|
|
1495
|
+
try {
|
|
1496
|
+
const parsed = OfflinePaymentAuthorizationSchema.parse(auth);
|
|
1497
|
+
if (!verifyPaymentRequest(parsed.request, issuerPublicKey)) return false;
|
|
1498
|
+
const { issuerSig: payerOacSig, ...payerOacUnsigned } = parsed.payerOAC;
|
|
1499
|
+
if (!verify(
|
|
1500
|
+
canonicalJSONBytes(payerOacUnsigned),
|
|
1501
|
+
hexToBytes(payerOacSig),
|
|
1502
|
+
issuerPublicKey
|
|
1503
|
+
)) {
|
|
1504
|
+
return false;
|
|
1505
|
+
}
|
|
1506
|
+
const { payerSig, ...unsigned } = parsed;
|
|
1507
|
+
return verify(
|
|
1508
|
+
canonicalJSONBytes(unsigned),
|
|
1509
|
+
hexToBytes(payerSig),
|
|
1510
|
+
hexToBytes(parsed.payerOAC.devicePublicKey)
|
|
1511
|
+
);
|
|
1512
|
+
} catch {
|
|
1513
|
+
return false;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
function encodePaymentRequestQR(req) {
|
|
1517
|
+
const json = JSON.stringify(req);
|
|
1518
|
+
return "FLUR1:" + encodeBase45(new TextEncoder().encode(json));
|
|
1519
|
+
}
|
|
1520
|
+
function decodePaymentRequestQR(s) {
|
|
1521
|
+
if (!s.startsWith("FLUR1:"))
|
|
1522
|
+
throw new Error("not a Flur offline payment request QR");
|
|
1523
|
+
const bytes = decodeBase45(s.slice("FLUR1:".length));
|
|
1524
|
+
const json = new TextDecoder().decode(bytes);
|
|
1525
|
+
return OfflinePaymentRequestSchema.parse(JSON.parse(json));
|
|
1526
|
+
}
|
|
1527
|
+
function encodeAuthorizationQR(auth) {
|
|
1528
|
+
const json = JSON.stringify(auth);
|
|
1529
|
+
return "FLUR2:" + encodeBase45(new TextEncoder().encode(json));
|
|
1530
|
+
}
|
|
1531
|
+
function decodeAuthorizationQR(s) {
|
|
1532
|
+
if (!s.startsWith("FLUR2:"))
|
|
1533
|
+
throw new Error("not a Flur offline authorization QR");
|
|
1534
|
+
const bytes = decodeBase45(s.slice("FLUR2:".length));
|
|
1535
|
+
const json = new TextDecoder().decode(bytes);
|
|
1536
|
+
return OfflinePaymentAuthorizationSchema.parse(JSON.parse(json));
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
// src/auth/hmac.ts
|
|
1540
|
+
var import_hmac = require("@noble/hashes/hmac");
|
|
1541
|
+
var import_sha256 = require("@noble/hashes/sha256");
|
|
1542
|
+
function bytesToHex2(b) {
|
|
1543
|
+
let s = "";
|
|
1544
|
+
for (let i = 0; i < b.length; i++) s += b[i].toString(16).padStart(2, "0");
|
|
1545
|
+
return s;
|
|
1546
|
+
}
|
|
1547
|
+
function constantTimeStringEqual(a, b) {
|
|
1548
|
+
if (a.length !== b.length) return false;
|
|
1549
|
+
let diff = 0;
|
|
1550
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
1551
|
+
return diff === 0;
|
|
1552
|
+
}
|
|
1553
|
+
function bodySha256Hex(body) {
|
|
1554
|
+
return bytesToHex2((0, import_sha256.sha256)(new TextEncoder().encode(body)));
|
|
1555
|
+
}
|
|
1556
|
+
function canonicalRequestString(input) {
|
|
1557
|
+
return [
|
|
1558
|
+
input.method.toUpperCase(),
|
|
1559
|
+
input.path,
|
|
1560
|
+
input.timestamp,
|
|
1561
|
+
input.nonce,
|
|
1562
|
+
bodySha256Hex(input.body)
|
|
1563
|
+
].join("\n");
|
|
1564
|
+
}
|
|
1565
|
+
function signRequestHMAC(input) {
|
|
1566
|
+
return bytesToHex2(
|
|
1567
|
+
(0, import_hmac.hmac)(import_sha256.sha256, input.apiSecret, canonicalRequestString(input))
|
|
1568
|
+
);
|
|
1569
|
+
}
|
|
1570
|
+
function verifyRequestHMAC(input) {
|
|
1571
|
+
const expected = signRequestHMAC(input);
|
|
1572
|
+
return constantTimeStringEqual(expected, input.signature.toLowerCase());
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
// src/auth/middleware.ts
|
|
1576
|
+
var REPLAY_WINDOW_MS = 5 * 60 * 1e3;
|
|
1577
|
+
var SERVER_TIME_HEADER = "x-flur-server-time";
|
|
1578
|
+
function defaultNonce() {
|
|
1579
|
+
const c = globalThis.crypto;
|
|
1580
|
+
if (typeof c?.randomUUID === "function") return c.randomUUID();
|
|
1581
|
+
if (typeof c?.getRandomValues !== "function") {
|
|
1582
|
+
throw new Error(
|
|
1583
|
+
"Flur SDK: no CSPRNG available (globalThis.crypto.getRandomValues missing). Refusing to fall back to Math.random for HMAC nonce generation."
|
|
1584
|
+
);
|
|
1585
|
+
}
|
|
1586
|
+
const arr = new Uint8Array(16);
|
|
1587
|
+
c.getRandomValues(arr);
|
|
1588
|
+
let s = "";
|
|
1589
|
+
for (let i = 0; i < arr.length; i++)
|
|
1590
|
+
s += arr[i].toString(16).padStart(2, "0");
|
|
1591
|
+
return s;
|
|
1592
|
+
}
|
|
1593
|
+
function assertCSPRNG() {
|
|
1594
|
+
const c = globalThis.crypto;
|
|
1595
|
+
if (typeof c?.getRandomValues !== "function" && typeof c?.randomUUID !== "function") {
|
|
1596
|
+
throw new Error(
|
|
1597
|
+
"Flur SDK: no CSPRNG available (globalThis.crypto missing). Initialize on a runtime that provides Web Crypto (Node 19+, modern browsers, RN 0.74+)."
|
|
1598
|
+
);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
function parseServerTimeMs(value) {
|
|
1602
|
+
if (!value) return null;
|
|
1603
|
+
const trimmed = value.trim();
|
|
1604
|
+
if (/^\d+$/.test(trimmed)) {
|
|
1605
|
+
const n = Number(trimmed);
|
|
1606
|
+
return n < 1e12 ? n * 1e3 : n;
|
|
1607
|
+
}
|
|
1608
|
+
const t = Date.parse(trimmed);
|
|
1609
|
+
return Number.isFinite(t) ? t : null;
|
|
1610
|
+
}
|
|
1611
|
+
function createHmacFetch(opts) {
|
|
1612
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
1613
|
+
if (!fetchImpl)
|
|
1614
|
+
throw new Error("createHmacFetch: no fetch implementation available");
|
|
1615
|
+
if (!opts.nonceFn) assertCSPRNG();
|
|
1616
|
+
const nowMs = opts.nowMs ?? (() => Date.now());
|
|
1617
|
+
const nonceFn = opts.nonceFn ?? defaultNonce;
|
|
1618
|
+
const scopeHeader = opts.scope && opts.scope.length > 0 ? opts.scope.join(",") : void 0;
|
|
1619
|
+
async function signAndSend(req, skewOffsetMs) {
|
|
1620
|
+
const cloned = req.clone();
|
|
1621
|
+
const url = new URL(cloned.url);
|
|
1622
|
+
const path = url.pathname + url.search;
|
|
1623
|
+
const method = cloned.method.toUpperCase();
|
|
1624
|
+
const bodyText = method === "GET" || method === "HEAD" ? "" : await cloned.clone().text();
|
|
1625
|
+
const timestamp = Math.floor((nowMs() + skewOffsetMs) / 1e3).toString();
|
|
1626
|
+
const nonce = nonceFn();
|
|
1627
|
+
const signature = signRequestHMAC({
|
|
1628
|
+
method,
|
|
1629
|
+
path,
|
|
1630
|
+
timestamp,
|
|
1631
|
+
nonce,
|
|
1632
|
+
body: bodyText,
|
|
1633
|
+
apiSecret: opts.apiSecret
|
|
1634
|
+
});
|
|
1635
|
+
const headers = new Headers(cloned.headers);
|
|
1636
|
+
headers.set("x-flur-key", opts.apiKey);
|
|
1637
|
+
headers.set("x-flur-timestamp", timestamp);
|
|
1638
|
+
headers.set("x-flur-nonce", nonce);
|
|
1639
|
+
headers.set("x-flur-signature", signature);
|
|
1640
|
+
if (scopeHeader) headers.set("x-flur-scope", scopeHeader);
|
|
1641
|
+
const signed = new Request(cloned, { headers });
|
|
1642
|
+
return fetchImpl(signed);
|
|
1643
|
+
}
|
|
1644
|
+
return (async (input, init2) => {
|
|
1645
|
+
const req = input instanceof Request ? input : new Request(input, init2);
|
|
1646
|
+
let resp = await signAndSend(req, 0);
|
|
1647
|
+
if (resp.status === 401) {
|
|
1648
|
+
const serverTimeMs = parseServerTimeMs(
|
|
1649
|
+
resp.headers.get(SERVER_TIME_HEADER)
|
|
1650
|
+
);
|
|
1651
|
+
if (serverTimeMs !== null) {
|
|
1652
|
+
const skew = serverTimeMs - nowMs();
|
|
1653
|
+
if (Math.abs(skew) > 0) {
|
|
1654
|
+
resp = await signAndSend(req, skew);
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
return resp;
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// src/passes/pass.ts
|
|
1663
|
+
var import_zod6 = require("zod");
|
|
1664
|
+
var PASS_KINDS = [
|
|
1665
|
+
"ride-ticket",
|
|
1666
|
+
"transit-pass",
|
|
1667
|
+
"event-ticket",
|
|
1668
|
+
"voucher",
|
|
1669
|
+
"loyalty",
|
|
1670
|
+
"receipt-link"
|
|
1671
|
+
];
|
|
1672
|
+
var PASS_STATES = [
|
|
1673
|
+
"issued",
|
|
1674
|
+
"active",
|
|
1675
|
+
"redeemed",
|
|
1676
|
+
"expired",
|
|
1677
|
+
"revoked"
|
|
1678
|
+
];
|
|
1679
|
+
var HexString2 = (length) => import_zod6.z.string().regex(
|
|
1680
|
+
new RegExp(`^[0-9a-fA-F]{${length * 2}}$`),
|
|
1681
|
+
`expected ${length}-byte hex string`
|
|
1682
|
+
);
|
|
1683
|
+
var PassMetadataSchema = import_zod6.z.record(
|
|
1684
|
+
import_zod6.z.union([import_zod6.z.string(), import_zod6.z.number(), import_zod6.z.boolean(), import_zod6.z.null()])
|
|
1685
|
+
);
|
|
1686
|
+
var PassSchema = import_zod6.z.object({
|
|
1687
|
+
passId: import_zod6.z.string().min(1),
|
|
1688
|
+
/** Optional client/template grouping id (server may omit). */
|
|
1689
|
+
templateId: import_zod6.z.string().min(1).optional(),
|
|
1690
|
+
/** Optional human-facing holder identity (server may omit). The cryptographic binding
|
|
1691
|
+
* is `holderDevicePubkey` below. */
|
|
1692
|
+
holderUserId: import_zod6.z.string().min(1).optional(),
|
|
1693
|
+
kind: import_zod6.z.enum(PASS_KINDS),
|
|
1694
|
+
issuerId: import_zod6.z.string().min(1),
|
|
1695
|
+
issuedAtMs: import_zod6.z.number().int().nonnegative(),
|
|
1696
|
+
validFromMs: import_zod6.z.number().int().nonnegative(),
|
|
1697
|
+
validUntilMs: import_zod6.z.number().int().positive(),
|
|
1698
|
+
state: import_zod6.z.enum(PASS_STATES),
|
|
1699
|
+
metadata: PassMetadataSchema,
|
|
1700
|
+
nonce: import_zod6.z.string().min(1),
|
|
1701
|
+
/** Device id this pass is bound to (FK to backend `device_keys`). */
|
|
1702
|
+
holderDeviceId: import_zod6.z.string().min(1),
|
|
1703
|
+
/** 32-byte hex Ed25519 public key of the bound device. The redemption signature
|
|
1704
|
+
* is verified against this key — it is the security-critical binding. */
|
|
1705
|
+
holderDevicePubkey: HexString2(32),
|
|
1706
|
+
/** Optional fixed amount for monetary passes (vouchers, gift cards) in kobo. */
|
|
1707
|
+
amountKobo: import_zod6.z.number().int().nonnegative().optional(),
|
|
1708
|
+
/** ISO-4217-ish currency code; required on the wire. SDK builders default to NGN. */
|
|
1709
|
+
currency: import_zod6.z.string().min(3).max(8),
|
|
1710
|
+
/** Monotonic redemption counter floor. Redemption.counter MUST be > counterSeed. */
|
|
1711
|
+
counterSeed: import_zod6.z.number().int().nonnegative(),
|
|
1712
|
+
/** Optional cumulative spend cap in kobo across all redemptions of this pass. */
|
|
1713
|
+
cumulativeCapKobo: import_zod6.z.number().int().nonnegative().optional(),
|
|
1714
|
+
issuerSig: HexString2(64)
|
|
1715
|
+
}).refine((v) => v.validUntilMs > v.validFromMs, {
|
|
1716
|
+
message: "validUntilMs must be greater than validFromMs"
|
|
1717
|
+
});
|
|
1718
|
+
function buildPass(input) {
|
|
1719
|
+
if (input.validUntilMs <= input.validFromMs) {
|
|
1720
|
+
throw new Error("validUntilMs must be greater than validFromMs");
|
|
1721
|
+
}
|
|
1722
|
+
const out = {
|
|
1723
|
+
passId: input.passId,
|
|
1724
|
+
kind: input.kind,
|
|
1725
|
+
issuerId: input.issuerId,
|
|
1726
|
+
issuedAtMs: input.issuedAtMs,
|
|
1727
|
+
validFromMs: input.validFromMs,
|
|
1728
|
+
validUntilMs: input.validUntilMs,
|
|
1729
|
+
state: input.state,
|
|
1730
|
+
metadata: input.metadata,
|
|
1731
|
+
nonce: input.nonce,
|
|
1732
|
+
holderDeviceId: input.holderDeviceId,
|
|
1733
|
+
holderDevicePubkey: input.holderDevicePubkey,
|
|
1734
|
+
currency: input.currency ?? "NGN",
|
|
1735
|
+
counterSeed: input.counterSeed
|
|
1736
|
+
};
|
|
1737
|
+
if (typeof input.templateId === "string") out.templateId = input.templateId;
|
|
1738
|
+
if (typeof input.holderUserId === "string")
|
|
1739
|
+
out.holderUserId = input.holderUserId;
|
|
1740
|
+
if (typeof input.amountKobo === "number") out.amountKobo = input.amountKobo;
|
|
1741
|
+
if (typeof input.cumulativeCapKobo === "number") {
|
|
1742
|
+
out.cumulativeCapKobo = input.cumulativeCapKobo;
|
|
1743
|
+
}
|
|
1744
|
+
return out;
|
|
1745
|
+
}
|
|
1746
|
+
function signPass(unsigned, issuerPrivateKey) {
|
|
1747
|
+
const issuerSig = bytesToHex(
|
|
1748
|
+
sign(canonicalJSONBytes(unsigned), issuerPrivateKey)
|
|
1749
|
+
);
|
|
1750
|
+
return { ...unsigned, issuerSig };
|
|
1751
|
+
}
|
|
1752
|
+
function verifyPass(pass, issuerPublicKey) {
|
|
1753
|
+
try {
|
|
1754
|
+
const parsed = PassSchema.parse(pass);
|
|
1755
|
+
const { issuerSig, ...unsigned } = parsed;
|
|
1756
|
+
return verify(
|
|
1757
|
+
canonicalJSONBytes(unsigned),
|
|
1758
|
+
hexToBytes(issuerSig),
|
|
1759
|
+
issuerPublicKey
|
|
1760
|
+
);
|
|
1761
|
+
} catch {
|
|
1762
|
+
return false;
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
function isPassWithinValidity(pass, nowMs) {
|
|
1766
|
+
return nowMs >= pass.validFromMs && nowMs < pass.validUntilMs;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
// src/passes/redemption.ts
|
|
1770
|
+
var import_zod7 = require("zod");
|
|
1771
|
+
var HexSig2 = import_zod7.z.string().regex(/^[0-9a-fA-F]{128}$/, "expected 64-byte hex signature");
|
|
1772
|
+
var RedemptionSchema = import_zod7.z.object({
|
|
1773
|
+
pass: PassSchema,
|
|
1774
|
+
redeemerId: import_zod7.z.string().min(1),
|
|
1775
|
+
redeemedAtMs: import_zod7.z.number().int().nonnegative(),
|
|
1776
|
+
/** Strictly monotonic counter scoped to a single pass. Must be > pass.counterSeed
|
|
1777
|
+
* and > the redeemer's lastSeenCounter for this pass. */
|
|
1778
|
+
counter: import_zod7.z.number().int().positive(),
|
|
1779
|
+
/** Amount being redeemed in kobo (0 for non-monetary passes like ride tickets). */
|
|
1780
|
+
amountKobo: import_zod7.z.number().int().nonnegative(),
|
|
1781
|
+
nonce: import_zod7.z.string().min(1),
|
|
1782
|
+
holderSig: HexSig2
|
|
1783
|
+
});
|
|
1784
|
+
var REDEEMABLE_STATES = /* @__PURE__ */ new Set(["issued", "active"]);
|
|
1785
|
+
function buildRedemption(input) {
|
|
1786
|
+
if (!REDEEMABLE_STATES.has(input.pass.state)) {
|
|
1787
|
+
throw new Error(`pass not in redeemable state: ${input.pass.state}`);
|
|
1788
|
+
}
|
|
1789
|
+
if (input.redeemedAtMs < input.pass.validFromMs) {
|
|
1790
|
+
throw new Error("redeemedAtMs is before pass validFromMs");
|
|
1791
|
+
}
|
|
1792
|
+
if (input.redeemedAtMs >= input.pass.validUntilMs) {
|
|
1793
|
+
throw new FlurExpiredError(
|
|
1794
|
+
`pass ${input.pass.passId} expired at ${input.pass.validUntilMs}`
|
|
1795
|
+
);
|
|
1796
|
+
}
|
|
1797
|
+
const now = typeof input.nowMs === "number" ? input.nowMs : input.redeemedAtMs;
|
|
1798
|
+
if (now >= input.pass.validUntilMs) {
|
|
1799
|
+
throw new FlurExpiredError(
|
|
1800
|
+
`pass ${input.pass.passId} expired at ${input.pass.validUntilMs}`
|
|
1801
|
+
);
|
|
1802
|
+
}
|
|
1803
|
+
if (!input.pass.holderDevicePubkey) {
|
|
1804
|
+
throw new Error(
|
|
1805
|
+
"pass.holderDevicePubkey is required to build a redemption"
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
const lastSeen = Math.max(
|
|
1809
|
+
input.pass.counterSeed,
|
|
1810
|
+
typeof input.lastSeenCounter === "number" ? input.lastSeenCounter : 0
|
|
1811
|
+
);
|
|
1812
|
+
if (input.counter <= lastSeen) {
|
|
1813
|
+
throw new FlurReplayError(
|
|
1814
|
+
`redemption counter ${input.counter} must be > lastSeenCounter ${lastSeen}`
|
|
1815
|
+
);
|
|
1816
|
+
}
|
|
1817
|
+
const amount = input.amountKobo ?? 0;
|
|
1818
|
+
if (typeof input.pass.cumulativeCapKobo === "number") {
|
|
1819
|
+
const used = input.cumulativeUsedKobo ?? 0;
|
|
1820
|
+
if (used + amount > input.pass.cumulativeCapKobo) {
|
|
1821
|
+
throw new FlurCapExceededError(
|
|
1822
|
+
`pass ${input.pass.passId} cap ${input.pass.cumulativeCapKobo} would be exceeded (used ${used} + ${amount})`
|
|
1823
|
+
);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
return {
|
|
1827
|
+
pass: input.pass,
|
|
1828
|
+
redeemerId: input.redeemerId,
|
|
1829
|
+
redeemedAtMs: input.redeemedAtMs,
|
|
1830
|
+
counter: input.counter,
|
|
1831
|
+
amountKobo: amount,
|
|
1832
|
+
nonce: input.nonce
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1835
|
+
function signRedemption(unsigned, holderDevicePrivateKey) {
|
|
1836
|
+
const holderSig = bytesToHex(
|
|
1837
|
+
sign(canonicalJSONBytes(unsigned), holderDevicePrivateKey)
|
|
1838
|
+
);
|
|
1839
|
+
return { ...unsigned, holderSig };
|
|
1840
|
+
}
|
|
1841
|
+
function verifyRedemption(r, issuerPublicKey) {
|
|
1842
|
+
try {
|
|
1843
|
+
const parsed = RedemptionSchema.parse(r);
|
|
1844
|
+
if (parsed.counter <= parsed.pass.counterSeed) return false;
|
|
1845
|
+
const { issuerSig, ...passUnsigned } = parsed.pass;
|
|
1846
|
+
if (!verify(
|
|
1847
|
+
canonicalJSONBytes(passUnsigned),
|
|
1848
|
+
hexToBytes(issuerSig),
|
|
1849
|
+
issuerPublicKey
|
|
1850
|
+
)) {
|
|
1851
|
+
return false;
|
|
1852
|
+
}
|
|
1853
|
+
const holderHex = parsed.pass.holderDevicePubkey;
|
|
1854
|
+
if (typeof holderHex !== "string") return false;
|
|
1855
|
+
const { holderSig, ...unsigned } = parsed;
|
|
1856
|
+
return verify(
|
|
1857
|
+
canonicalJSONBytes(unsigned),
|
|
1858
|
+
hexToBytes(holderSig),
|
|
1859
|
+
hexToBytes(holderHex)
|
|
1860
|
+
);
|
|
1861
|
+
} catch {
|
|
1862
|
+
return false;
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
// src/passes/client.ts
|
|
1867
|
+
function createPassesClient(opts) {
|
|
1868
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
1869
|
+
if (!fetchImpl)
|
|
1870
|
+
throw new Error("createPassesClient: no fetch implementation available");
|
|
1871
|
+
const baseUrl = opts.baseUrl.replace(/\/$/, "");
|
|
1872
|
+
async function call(method, path, body, parser) {
|
|
1873
|
+
const init2 = {
|
|
1874
|
+
method,
|
|
1875
|
+
headers: {
|
|
1876
|
+
"content-type": "application/json",
|
|
1877
|
+
accept: "application/json"
|
|
1878
|
+
}
|
|
1879
|
+
};
|
|
1880
|
+
if (body !== void 0) init2.body = JSON.stringify(body);
|
|
1881
|
+
const resp = await fetchImpl(`${baseUrl}${path}`, init2);
|
|
1882
|
+
const text = await resp.text();
|
|
1883
|
+
let raw = void 0;
|
|
1884
|
+
if (text) {
|
|
1885
|
+
try {
|
|
1886
|
+
raw = JSON.parse(text);
|
|
1887
|
+
} catch {
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
if (!resp.ok) {
|
|
1891
|
+
const code = (raw && typeof raw === "object" && "code" in raw && typeof raw.code === "string" ? raw.code : void 0) ?? `http_${resp.status}`;
|
|
1892
|
+
const message = (raw && typeof raw === "object" && "message" in raw && typeof raw.message === "string" ? raw.message : void 0) ?? `request failed with status ${resp.status}`;
|
|
1893
|
+
throw new FlurApiError(resp.status, code, message, raw);
|
|
1894
|
+
}
|
|
1895
|
+
return parser(raw);
|
|
1896
|
+
}
|
|
1897
|
+
return {
|
|
1898
|
+
issuePass: (input) => call("POST", "/v1/passes", input, (raw) => PassSchema.parse(raw)),
|
|
1899
|
+
listPasses: (input) => {
|
|
1900
|
+
const qs = new URLSearchParams();
|
|
1901
|
+
if (input.holderDeviceId) qs.set("holderDeviceId", input.holderDeviceId);
|
|
1902
|
+
if (input.holderUserId) qs.set("holderUserId", input.holderUserId);
|
|
1903
|
+
if (input.state) qs.set("state", input.state);
|
|
1904
|
+
if (input.kind) qs.set("kind", input.kind);
|
|
1905
|
+
if (input.templateId) qs.set("templateId", input.templateId);
|
|
1906
|
+
if (typeof input.limit === "number") qs.set("limit", String(input.limit));
|
|
1907
|
+
if (input.cursor) qs.set("cursor", input.cursor);
|
|
1908
|
+
const path = `/v1/passes${qs.size > 0 ? `?${qs.toString()}` : ""}`;
|
|
1909
|
+
return call("GET", path, void 0, (raw) => {
|
|
1910
|
+
const obj = raw;
|
|
1911
|
+
const items = obj.items.map(
|
|
1912
|
+
(it) => PassSchema.parse(it)
|
|
1913
|
+
);
|
|
1914
|
+
const nextCursor = typeof obj.nextCursor === "string" ? obj.nextCursor : null;
|
|
1915
|
+
return { items, nextCursor };
|
|
1916
|
+
});
|
|
1917
|
+
},
|
|
1918
|
+
getPass: (passId) => call(
|
|
1919
|
+
"GET",
|
|
1920
|
+
`/v1/passes/${encodeURIComponent(passId)}`,
|
|
1921
|
+
void 0,
|
|
1922
|
+
(raw) => PassSchema.parse(raw)
|
|
1923
|
+
),
|
|
1924
|
+
redeemPass: (passId, redemption) => call(
|
|
1925
|
+
"POST",
|
|
1926
|
+
`/v1/passes/${encodeURIComponent(passId)}/redeem`,
|
|
1927
|
+
RedemptionSchema.parse(redemption),
|
|
1928
|
+
(raw) => PassSchema.parse(raw)
|
|
1929
|
+
),
|
|
1930
|
+
revokePass: (passId, input) => call(
|
|
1931
|
+
"POST",
|
|
1932
|
+
`/v1/passes/${encodeURIComponent(passId)}/revoke`,
|
|
1933
|
+
input,
|
|
1934
|
+
(raw) => PassSchema.parse(raw)
|
|
1935
|
+
),
|
|
1936
|
+
verifyPass: (pass, issuerPublicKey) => verifyPass(pass, issuerPublicKey)
|
|
1937
|
+
};
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
// src/receipts/receipt.ts
|
|
1941
|
+
var import_zod8 = require("zod");
|
|
1942
|
+
var RECEIPT_CHANNELS = ["cash", "pass"];
|
|
1943
|
+
var RECEIPT_KINDS = RECEIPT_CHANNELS;
|
|
1944
|
+
var HexString3 = (length) => import_zod8.z.string().regex(
|
|
1945
|
+
new RegExp(`^[0-9a-fA-F]{${length * 2}}$`),
|
|
1946
|
+
`expected ${length}-byte hex string`
|
|
1947
|
+
);
|
|
1948
|
+
var ReceiptPayloadSchema = import_zod8.z.record(
|
|
1949
|
+
import_zod8.z.union([import_zod8.z.string(), import_zod8.z.number(), import_zod8.z.boolean(), import_zod8.z.null()])
|
|
1950
|
+
);
|
|
1951
|
+
var ReceiptSchema = import_zod8.z.object({
|
|
1952
|
+
receiptId: import_zod8.z.string().min(1),
|
|
1953
|
+
channel: import_zod8.z.enum(RECEIPT_CHANNELS),
|
|
1954
|
+
/** Cash-channel: send_intents.id. Required when channel === 'cash'. */
|
|
1955
|
+
intentId: import_zod8.z.string().min(1).optional(),
|
|
1956
|
+
/** Pass-channel: pass_redemptions.id. Required when channel === 'pass'. */
|
|
1957
|
+
passRedemptionId: import_zod8.z.string().min(1).optional(),
|
|
1958
|
+
payerUserId: import_zod8.z.string().min(1),
|
|
1959
|
+
payeeUserId: import_zod8.z.string().min(1),
|
|
1960
|
+
amountKobo: import_zod8.z.number().int().nonnegative(),
|
|
1961
|
+
currency: import_zod8.z.string().min(3).max(8),
|
|
1962
|
+
issuedAtMs: import_zod8.z.number().int().nonnegative(),
|
|
1963
|
+
issuerId: import_zod8.z.string().min(1),
|
|
1964
|
+
payload: ReceiptPayloadSchema,
|
|
1965
|
+
issuerSig: HexString3(64)
|
|
1966
|
+
}).superRefine((v, ctx) => {
|
|
1967
|
+
if (v.channel === "cash") {
|
|
1968
|
+
if (!v.intentId) {
|
|
1969
|
+
ctx.addIssue({
|
|
1970
|
+
code: import_zod8.z.ZodIssueCode.custom,
|
|
1971
|
+
message: "cash receipts require intentId",
|
|
1972
|
+
path: ["intentId"]
|
|
1973
|
+
});
|
|
1974
|
+
}
|
|
1975
|
+
if (v.passRedemptionId) {
|
|
1976
|
+
ctx.addIssue({
|
|
1977
|
+
code: import_zod8.z.ZodIssueCode.custom,
|
|
1978
|
+
message: "cash receipts must not carry passRedemptionId",
|
|
1979
|
+
path: ["passRedemptionId"]
|
|
1980
|
+
});
|
|
1981
|
+
}
|
|
1982
|
+
} else if (v.channel === "pass") {
|
|
1983
|
+
if (!v.passRedemptionId) {
|
|
1984
|
+
ctx.addIssue({
|
|
1985
|
+
code: import_zod8.z.ZodIssueCode.custom,
|
|
1986
|
+
message: "pass receipts require passRedemptionId",
|
|
1987
|
+
path: ["passRedemptionId"]
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1990
|
+
if (v.intentId) {
|
|
1991
|
+
ctx.addIssue({
|
|
1992
|
+
code: import_zod8.z.ZodIssueCode.custom,
|
|
1993
|
+
message: "pass receipts must not carry intentId",
|
|
1994
|
+
path: ["intentId"]
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
});
|
|
1999
|
+
function buildReceipt(input) {
|
|
2000
|
+
if (input.channel === "cash") {
|
|
2001
|
+
if (!input.intentId) {
|
|
2002
|
+
throw new Error("cash receipts require intentId");
|
|
2003
|
+
}
|
|
2004
|
+
if (input.passRedemptionId) {
|
|
2005
|
+
throw new Error("cash receipts must not carry passRedemptionId");
|
|
2006
|
+
}
|
|
2007
|
+
} else {
|
|
2008
|
+
if (!input.passRedemptionId) {
|
|
2009
|
+
throw new Error("pass receipts require passRedemptionId");
|
|
2010
|
+
}
|
|
2011
|
+
if (input.intentId) {
|
|
2012
|
+
throw new Error("pass receipts must not carry intentId");
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
const out = {
|
|
2016
|
+
receiptId: input.receiptId,
|
|
2017
|
+
channel: input.channel,
|
|
2018
|
+
payerUserId: input.payerUserId,
|
|
2019
|
+
payeeUserId: input.payeeUserId,
|
|
2020
|
+
amountKobo: input.amountKobo,
|
|
2021
|
+
currency: input.currency,
|
|
2022
|
+
issuedAtMs: input.issuedAtMs,
|
|
2023
|
+
issuerId: input.issuerId,
|
|
2024
|
+
payload: input.payload ?? {}
|
|
2025
|
+
};
|
|
2026
|
+
if (input.intentId) out.intentId = input.intentId;
|
|
2027
|
+
if (input.passRedemptionId) out.passRedemptionId = input.passRedemptionId;
|
|
2028
|
+
return out;
|
|
2029
|
+
}
|
|
2030
|
+
function signReceipt(unsigned, issuerPrivateKey) {
|
|
2031
|
+
const issuerSig = bytesToHex(
|
|
2032
|
+
sign(canonicalJSONBytes(unsigned), issuerPrivateKey)
|
|
2033
|
+
);
|
|
2034
|
+
return { ...unsigned, issuerSig };
|
|
2035
|
+
}
|
|
2036
|
+
function verifyReceipt(r, issuerPublicKey) {
|
|
2037
|
+
try {
|
|
2038
|
+
const parsed = ReceiptSchema.parse(r);
|
|
2039
|
+
const { issuerSig, ...unsigned } = parsed;
|
|
2040
|
+
return verify(
|
|
2041
|
+
canonicalJSONBytes(unsigned),
|
|
2042
|
+
hexToBytes(issuerSig),
|
|
2043
|
+
issuerPublicKey
|
|
2044
|
+
);
|
|
2045
|
+
} catch {
|
|
2046
|
+
return false;
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
// src/receipts/client.ts
|
|
2051
|
+
function createReceiptsClient(opts) {
|
|
2052
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
2053
|
+
if (!fetchImpl)
|
|
2054
|
+
throw new Error("createReceiptsClient: no fetch implementation available");
|
|
2055
|
+
const baseUrl = opts.baseUrl.replace(/\/$/, "");
|
|
2056
|
+
async function call(method, path, body, parser) {
|
|
2057
|
+
const init2 = {
|
|
2058
|
+
method,
|
|
2059
|
+
headers: {
|
|
2060
|
+
"content-type": "application/json",
|
|
2061
|
+
accept: "application/json"
|
|
2062
|
+
}
|
|
2063
|
+
};
|
|
2064
|
+
if (body !== void 0) init2.body = JSON.stringify(body);
|
|
2065
|
+
const resp = await fetchImpl(`${baseUrl}${path}`, init2);
|
|
2066
|
+
const text = await resp.text();
|
|
2067
|
+
let raw = void 0;
|
|
2068
|
+
if (text) {
|
|
2069
|
+
try {
|
|
2070
|
+
raw = JSON.parse(text);
|
|
2071
|
+
} catch {
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
if (!resp.ok) {
|
|
2075
|
+
const code = raw && typeof raw === "object" && "code" in raw && typeof raw.code === "string" ? raw.code : `http_${resp.status}`;
|
|
2076
|
+
const message = raw && typeof raw === "object" && "message" in raw && typeof raw.message === "string" ? raw.message : `request failed with status ${resp.status}`;
|
|
2077
|
+
throw new FlurApiError(resp.status, code, message, raw);
|
|
2078
|
+
}
|
|
2079
|
+
return parser(raw);
|
|
2080
|
+
}
|
|
2081
|
+
const getById = (receiptId) => call(
|
|
2082
|
+
"GET",
|
|
2083
|
+
`/v1/receipts/${encodeURIComponent(receiptId)}`,
|
|
2084
|
+
void 0,
|
|
2085
|
+
(raw) => ReceiptSchema.parse(raw)
|
|
2086
|
+
);
|
|
2087
|
+
return {
|
|
2088
|
+
issueReceipt: (input) => call("POST", "/v1/receipts", input, (raw) => ReceiptSchema.parse(raw)),
|
|
2089
|
+
getReceipt: getById,
|
|
2090
|
+
getById,
|
|
2091
|
+
getByIntentId: (intentId) => call(
|
|
2092
|
+
"GET",
|
|
2093
|
+
`/v1/receipts/by-intent/${encodeURIComponent(intentId)}`,
|
|
2094
|
+
void 0,
|
|
2095
|
+
(raw) => ReceiptSchema.parse(raw)
|
|
2096
|
+
),
|
|
2097
|
+
getByPassRedemptionId: (passRedemptionId) => call(
|
|
2098
|
+
"GET",
|
|
2099
|
+
`/v1/receipts/by-pass-redemption/${encodeURIComponent(passRedemptionId)}`,
|
|
2100
|
+
void 0,
|
|
2101
|
+
(raw) => ReceiptSchema.parse(raw)
|
|
2102
|
+
),
|
|
2103
|
+
listForUser: (input) => {
|
|
2104
|
+
const qs = new URLSearchParams();
|
|
2105
|
+
if (input.payerUserId) qs.set("payerUserId", input.payerUserId);
|
|
2106
|
+
if (input.payeeUserId) qs.set("payeeUserId", input.payeeUserId);
|
|
2107
|
+
if (input.channel) qs.set("channel", input.channel);
|
|
2108
|
+
if (typeof input.limit === "number") qs.set("limit", String(input.limit));
|
|
2109
|
+
if (input.cursor) qs.set("cursor", input.cursor);
|
|
2110
|
+
const path = `/v1/receipts${qs.size > 0 ? `?${qs.toString()}` : ""}`;
|
|
2111
|
+
return call("GET", path, void 0, (raw) => {
|
|
2112
|
+
const obj = raw;
|
|
2113
|
+
const items = obj.items.map(
|
|
2114
|
+
(it) => ReceiptSchema.parse(it)
|
|
2115
|
+
);
|
|
2116
|
+
const nextCursor = typeof obj.nextCursor === "string" ? obj.nextCursor : null;
|
|
2117
|
+
return { items, nextCursor };
|
|
2118
|
+
});
|
|
2119
|
+
},
|
|
2120
|
+
verifyReceipt: (receipt, issuerPublicKey) => verifyReceipt(receipt, issuerPublicKey)
|
|
2121
|
+
};
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
// src/client/flur.ts
|
|
2125
|
+
function generateStaticQR(input) {
|
|
2126
|
+
return encodeNQR({ ...input, pointOfInitiation: "static" });
|
|
2127
|
+
}
|
|
2128
|
+
function generateDynamicQR(input) {
|
|
2129
|
+
return encodeNQR({ ...input, pointOfInitiation: "dynamic" });
|
|
2130
|
+
}
|
|
2131
|
+
function parseQR(payload) {
|
|
2132
|
+
return parseNQR(payload);
|
|
2133
|
+
}
|
|
2134
|
+
function init(opts) {
|
|
2135
|
+
const signedFetch = createHmacFetch({
|
|
2136
|
+
apiKey: opts.apiKey,
|
|
2137
|
+
apiSecret: opts.apiSecret,
|
|
2138
|
+
fetchImpl: opts.fetchImpl,
|
|
2139
|
+
scope: opts.scope
|
|
2140
|
+
});
|
|
2141
|
+
const baseUrl = opts.baseUrl.replace(/\/$/, "");
|
|
2142
|
+
function subscribeToPayments(s) {
|
|
2143
|
+
const controller = new AbortController();
|
|
2144
|
+
let cancelled = false;
|
|
2145
|
+
(async () => {
|
|
2146
|
+
try {
|
|
2147
|
+
const url = `${baseUrl}/v1/payments/subscribe?reference=${encodeURIComponent(s.reference)}`;
|
|
2148
|
+
const resp = await signedFetch(url, {
|
|
2149
|
+
method: "GET",
|
|
2150
|
+
headers: { accept: "text/event-stream" },
|
|
2151
|
+
signal: controller.signal
|
|
2152
|
+
});
|
|
2153
|
+
if (!resp.body) return;
|
|
2154
|
+
const reader = resp.body.getReader();
|
|
2155
|
+
const decoder = new TextDecoder();
|
|
2156
|
+
let buffer = "";
|
|
2157
|
+
while (!cancelled) {
|
|
2158
|
+
const { value, done } = await reader.read();
|
|
2159
|
+
if (done) return;
|
|
2160
|
+
buffer += decoder.decode(value, { stream: true });
|
|
2161
|
+
let idx;
|
|
2162
|
+
while ((idx = buffer.indexOf("\n\n")) >= 0) {
|
|
2163
|
+
const chunk = buffer.slice(0, idx);
|
|
2164
|
+
buffer = buffer.slice(idx + 2);
|
|
2165
|
+
for (const line of chunk.split("\n")) {
|
|
2166
|
+
if (!line.startsWith("data:")) continue;
|
|
2167
|
+
const json = line.slice(5).trim();
|
|
2168
|
+
if (!json) continue;
|
|
2169
|
+
try {
|
|
2170
|
+
s.onEvent(JSON.parse(json));
|
|
2171
|
+
} catch (err) {
|
|
2172
|
+
s.onError?.(err);
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
} catch (err) {
|
|
2178
|
+
if (!cancelled) s.onError?.(err);
|
|
2179
|
+
}
|
|
2180
|
+
})();
|
|
2181
|
+
return () => {
|
|
2182
|
+
cancelled = true;
|
|
2183
|
+
controller.abort();
|
|
2184
|
+
};
|
|
2185
|
+
}
|
|
2186
|
+
const cash = {
|
|
2187
|
+
generateStaticQR,
|
|
2188
|
+
generateDynamicQR,
|
|
2189
|
+
parseQR,
|
|
2190
|
+
subscribeToPayments
|
|
2191
|
+
};
|
|
2192
|
+
const passes = createPassesClient({ baseUrl, fetchImpl: signedFetch });
|
|
2193
|
+
const receipts = createReceiptsClient({ baseUrl, fetchImpl: signedFetch });
|
|
2194
|
+
return {
|
|
2195
|
+
// top-level back-compat surface
|
|
2196
|
+
generateStaticQR,
|
|
2197
|
+
generateDynamicQR,
|
|
2198
|
+
parseQR,
|
|
2199
|
+
subscribeToPayments,
|
|
2200
|
+
// namespaces
|
|
2201
|
+
cash,
|
|
2202
|
+
passes,
|
|
2203
|
+
receipts
|
|
2204
|
+
};
|
|
2205
|
+
}
|
|
2206
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2207
|
+
0 && (module.exports = {
|
|
2208
|
+
ADDITIONAL_DATA_SUBFIELD,
|
|
2209
|
+
FIELD,
|
|
2210
|
+
FlurApiError,
|
|
2211
|
+
FlurCapExceededError,
|
|
2212
|
+
FlurClient,
|
|
2213
|
+
FlurError,
|
|
2214
|
+
FlurExpiredError,
|
|
2215
|
+
FlurReplayError,
|
|
2216
|
+
NGN_CURRENCY_CODE,
|
|
2217
|
+
NG_COUNTRY_CODE,
|
|
2218
|
+
NQRParseError,
|
|
2219
|
+
OACSchema,
|
|
2220
|
+
OAC_DEFAULT_CUMULATIVE_KOBO,
|
|
2221
|
+
OAC_DEFAULT_PER_TX_KOBO,
|
|
2222
|
+
OAC_DEFAULT_VALIDITY_MS,
|
|
2223
|
+
OfflinePaymentAuthorizationSchema,
|
|
2224
|
+
OfflinePaymentRequestSchema,
|
|
2225
|
+
PASS_KINDS,
|
|
2226
|
+
PASS_STATES,
|
|
2227
|
+
PAYLOAD_FORMAT_INDICATOR_VALUE,
|
|
2228
|
+
POINT_OF_INITIATION,
|
|
2229
|
+
PassMetadataSchema,
|
|
2230
|
+
PassSchema,
|
|
2231
|
+
RECEIPT_CHANNELS,
|
|
2232
|
+
RECEIPT_KINDS,
|
|
2233
|
+
REPLAY_WINDOW_MS,
|
|
2234
|
+
ReceiptPayloadSchema,
|
|
2235
|
+
ReceiptSchema,
|
|
2236
|
+
RedemptionSchema,
|
|
2237
|
+
bodySha256Hex,
|
|
2238
|
+
buildAuthorization,
|
|
2239
|
+
buildOAC,
|
|
2240
|
+
buildPass,
|
|
2241
|
+
buildPaymentRequest,
|
|
2242
|
+
buildReceipt,
|
|
2243
|
+
buildRedemption,
|
|
2244
|
+
canonicalJSONBytes,
|
|
2245
|
+
canonicalJSONStringify,
|
|
2246
|
+
canonicalRequestString,
|
|
2247
|
+
constantTimeEqual,
|
|
2248
|
+
crc16ccitt,
|
|
2249
|
+
crc16ccittHex,
|
|
2250
|
+
createHmacFetch,
|
|
2251
|
+
createPassesClient,
|
|
2252
|
+
createReceiptsClient,
|
|
2253
|
+
decodeAuthorizationQR,
|
|
2254
|
+
decodeBase45,
|
|
2255
|
+
decodePaymentRequestQR,
|
|
2256
|
+
encodeAuthorizationQR,
|
|
2257
|
+
encodeBase45,
|
|
2258
|
+
encodeNQR,
|
|
2259
|
+
encodePaymentRequestQR,
|
|
2260
|
+
formatAmount,
|
|
2261
|
+
generateDynamicQR,
|
|
2262
|
+
generateKeyPair,
|
|
2263
|
+
generateStaticQR,
|
|
2264
|
+
init,
|
|
2265
|
+
isPassWithinValidity,
|
|
2266
|
+
moneyMinorToNumber,
|
|
2267
|
+
normalizeE164,
|
|
2268
|
+
parseAmountInput,
|
|
2269
|
+
parseNQR,
|
|
2270
|
+
parseQR,
|
|
2271
|
+
publicKeyFromPrivate,
|
|
2272
|
+
readTLV,
|
|
2273
|
+
routingHint,
|
|
2274
|
+
sign,
|
|
2275
|
+
signAuthorization,
|
|
2276
|
+
signCanonical,
|
|
2277
|
+
signOAC,
|
|
2278
|
+
signPass,
|
|
2279
|
+
signPaymentRequest,
|
|
2280
|
+
signReceipt,
|
|
2281
|
+
signRedemption,
|
|
2282
|
+
signRequestHMAC,
|
|
2283
|
+
verify,
|
|
2284
|
+
verifyAuthorization,
|
|
2285
|
+
verifyCanonical,
|
|
2286
|
+
verifyOAC,
|
|
2287
|
+
verifyPass,
|
|
2288
|
+
verifyPaymentRequest,
|
|
2289
|
+
verifyReceipt,
|
|
2290
|
+
verifyRedemption,
|
|
2291
|
+
verifyRequestHMAC,
|
|
2292
|
+
writeTLV
|
|
2293
|
+
});
|
|
2294
|
+
//# sourceMappingURL=index.cjs.map
|