@nokinc-flur/sdk 1.1.3 → 1.1.5
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/README.md +229 -229
- package/dist/index.cjs +1085 -981
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1428 -1434
- package/dist/index.d.ts +1428 -1434
- package/dist/index.js +1082 -976
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,201 +1,11 @@
|
|
|
1
1
|
// src/client.ts
|
|
2
|
-
import { z as
|
|
2
|
+
import { z as z5 } from "zod";
|
|
3
3
|
|
|
4
4
|
// src/contracts.ts
|
|
5
|
+
import { z as z2 } from "zod";
|
|
6
|
+
|
|
7
|
+
// src/me-offline/client.ts
|
|
5
8
|
import { z } from "zod";
|
|
6
|
-
var E164Regex = /^\+[1-9]\d{7,14}$/;
|
|
7
|
-
var UuidSchema = z.string().uuid();
|
|
8
|
-
var IsoDateSchema = z.string().datetime({ offset: true });
|
|
9
|
-
var CurrencySchema = z.string().trim().length(3).transform((value) => value.toUpperCase());
|
|
10
|
-
var HealthResponseSchema = z.object({
|
|
11
|
-
ok: z.boolean()
|
|
12
|
-
});
|
|
13
|
-
var WelcomeResponseSchema = z.object({
|
|
14
|
-
message: z.string()
|
|
15
|
-
});
|
|
16
|
-
var OnboardingStartRequestSchema = z.object({
|
|
17
|
-
phoneE164: z.string().regex(E164Regex),
|
|
18
|
-
appInstanceId: z.string().min(3),
|
|
19
|
-
platform: z.enum(["android", "ios", "web"]),
|
|
20
|
-
turnstileToken: z.string().min(20).optional(),
|
|
21
|
-
appAttestation: z.object({
|
|
22
|
-
provider: z.enum(["android", "ios", "web"]),
|
|
23
|
-
token: z.string().min(24)
|
|
24
|
-
}).optional(),
|
|
25
|
-
firstName: z.string().trim().min(1).max(80).optional(),
|
|
26
|
-
lastName: z.string().trim().min(1).max(80).optional()
|
|
27
|
-
});
|
|
28
|
-
var OnboardingStartResponseSchema = z.object({
|
|
29
|
-
requestId: z.string().min(1),
|
|
30
|
-
checkUrl: z.string().url().optional(),
|
|
31
|
-
expiresInSec: z.number().int().positive(),
|
|
32
|
-
fallback: z.enum(["SILENT_AUTH", "OTP"])
|
|
33
|
-
});
|
|
34
|
-
var OnboardingCompleteRequestSchema = z.object({
|
|
35
|
-
requestId: z.string().min(1),
|
|
36
|
-
code: z.string().min(1).max(32),
|
|
37
|
-
appInstanceId: z.string().min(3),
|
|
38
|
-
fingerprintHash: z.string().min(3).optional()
|
|
39
|
-
});
|
|
40
|
-
var OnboardingCompleteResponseSchema = z.object({
|
|
41
|
-
sessionToken: z.string().min(1),
|
|
42
|
-
userId: UuidSchema,
|
|
43
|
-
restricted: z.boolean(),
|
|
44
|
-
risk_reasons: z.array(
|
|
45
|
-
z.enum(["SIM_SWAP_RECENT", "ROAMING", "CARRIER_CHANGED"])
|
|
46
|
-
),
|
|
47
|
-
stepUpRequired: z.boolean().optional(),
|
|
48
|
-
riskStatus: z.enum(["ok", "unavailable"]).optional()
|
|
49
|
-
});
|
|
50
|
-
var RegisterDeviceRequestSchema = z.object({
|
|
51
|
-
userId: UuidSchema,
|
|
52
|
-
appInstanceId: z.string().min(3),
|
|
53
|
-
platform: z.string().min(2),
|
|
54
|
-
model: z.string().optional(),
|
|
55
|
-
networkSignals: z.object({
|
|
56
|
-
ip: z.string().min(3),
|
|
57
|
-
asn: z.number().int().optional(),
|
|
58
|
-
country: z.string().min(2).optional(),
|
|
59
|
-
carrier: z.string().optional()
|
|
60
|
-
})
|
|
61
|
-
});
|
|
62
|
-
var RegisterDeviceResponseSchema = z.object({
|
|
63
|
-
deviceId: z.string().min(1),
|
|
64
|
-
fingerprintHash: z.string().min(1),
|
|
65
|
-
driftScore: z.number(),
|
|
66
|
-
trustState: z.enum(["TRUSTED_PRIMARY", "TRUSTED_SECONDARY", "UNVERIFIED"]),
|
|
67
|
-
stepUpRequired: z.boolean()
|
|
68
|
-
});
|
|
69
|
-
var AuthRefreshRequestSchema = z.object({
|
|
70
|
-
userId: UuidSchema,
|
|
71
|
-
refreshToken: z.string().min(8),
|
|
72
|
-
appInstanceId: z.string().min(3),
|
|
73
|
-
fingerprintHash: z.string().min(3)
|
|
74
|
-
});
|
|
75
|
-
var AuthRefreshResponseSchema = z.object({
|
|
76
|
-
refreshToken: z.string().min(8),
|
|
77
|
-
stepUpRequired: z.boolean()
|
|
78
|
-
});
|
|
79
|
-
var AuthLogoutRequestSchema = z.object({
|
|
80
|
-
userId: UuidSchema,
|
|
81
|
-
refreshToken: z.string().min(8)
|
|
82
|
-
});
|
|
83
|
-
var PinSetRequestSchema = z.object({
|
|
84
|
-
userId: UuidSchema,
|
|
85
|
-
pin: z.string().regex(/^\d{6}$/)
|
|
86
|
-
});
|
|
87
|
-
var PinVerifyRequestSchema = z.object({
|
|
88
|
-
userId: UuidSchema,
|
|
89
|
-
pin: z.string().regex(/^\d{6}$/)
|
|
90
|
-
});
|
|
91
|
-
var OkResponseSchema = z.object({
|
|
92
|
-
ok: z.boolean()
|
|
93
|
-
});
|
|
94
|
-
var RegisterSendDeviceKeyRequestSchema = z.object({
|
|
95
|
-
userId: UuidSchema,
|
|
96
|
-
deviceId: z.string().min(3),
|
|
97
|
-
publicKey: z.string().min(32)
|
|
98
|
-
});
|
|
99
|
-
var SEND_AUTH_PURPOSES = ["send_money", "offline_revoke"];
|
|
100
|
-
var SendChallengeRequestSchema = z.object({
|
|
101
|
-
userId: UuidSchema,
|
|
102
|
-
deviceId: z.string().min(3),
|
|
103
|
-
purpose: z.enum(SEND_AUTH_PURPOSES).optional()
|
|
104
|
-
});
|
|
105
|
-
var SendChallengeResponseSchema = z.object({
|
|
106
|
-
challengeId: UuidSchema,
|
|
107
|
-
nonce: z.string().min(1),
|
|
108
|
-
expiresAt: IsoDateSchema
|
|
109
|
-
});
|
|
110
|
-
var SendVerifyRequestSchema = z.object({
|
|
111
|
-
userId: UuidSchema,
|
|
112
|
-
deviceId: z.string().min(3),
|
|
113
|
-
challengeId: UuidSchema,
|
|
114
|
-
signature: z.string().min(16)
|
|
115
|
-
});
|
|
116
|
-
var SendVerifyResponseSchema = z.object({
|
|
117
|
-
sendAuthToken: z.string().min(16)
|
|
118
|
-
});
|
|
119
|
-
var ResolveRecipientRequestSchema = z.object({
|
|
120
|
-
identifier: z.string().min(3)
|
|
121
|
-
});
|
|
122
|
-
var ResolveRecipientResponseSchema = z.object({
|
|
123
|
-
recipientUserId: UuidSchema,
|
|
124
|
-
displayName: z.string().min(1),
|
|
125
|
-
normalizedIdentifier: z.string().regex(E164Regex),
|
|
126
|
-
isActive: z.boolean()
|
|
127
|
-
});
|
|
128
|
-
var CreateTransferRequestSchema = z.object({
|
|
129
|
-
recipientIdentifier: z.string().min(3),
|
|
130
|
-
amountMinor: z.number().int().positive(),
|
|
131
|
-
currency: CurrencySchema,
|
|
132
|
-
sendAuthToken: z.string().min(16)
|
|
133
|
-
});
|
|
134
|
-
var TransferStatusSchema = z.enum(["SETTLED", "PENDING_REVIEW", "DECLINED"]);
|
|
135
|
-
var TransferResponseSchema = z.object({
|
|
136
|
-
transactionId: z.string().min(1),
|
|
137
|
-
status: TransferStatusSchema,
|
|
138
|
-
userStatus: TransferStatusSchema,
|
|
139
|
-
recipientName: z.string().min(1),
|
|
140
|
-
timestamp: IsoDateSchema
|
|
141
|
-
});
|
|
142
|
-
var DirectionSchema = z.enum(["OUTGOING", "INCOMING"]);
|
|
143
|
-
var AccountActivityItemSchema = z.object({
|
|
144
|
-
id: z.string().min(1),
|
|
145
|
-
type: z.string().min(1),
|
|
146
|
-
direction: DirectionSchema,
|
|
147
|
-
name: z.string().min(1),
|
|
148
|
-
identifier: z.string().min(1),
|
|
149
|
-
amountMinor: z.number().int(),
|
|
150
|
-
currency: CurrencySchema,
|
|
151
|
-
status: z.string().min(1),
|
|
152
|
-
timestamp: IsoDateSchema
|
|
153
|
-
});
|
|
154
|
-
var AccountSummaryResponseSchema = z.object({
|
|
155
|
-
/** Authenticated user's stable internal id. */
|
|
156
|
-
userId: UuidSchema,
|
|
157
|
-
/**
|
|
158
|
-
* 10-digit Nigeria Uniform Bank Account Number (NUBAN) allocated by the
|
|
159
|
-
* bank partner. `null` when the user has no partner-allocated account yet.
|
|
160
|
-
*/
|
|
161
|
-
nuban: z.string().regex(/^[0-9]{10}$/).nullable(),
|
|
162
|
-
balance: z.number().int(),
|
|
163
|
-
currency: CurrencySchema,
|
|
164
|
-
dailySendLimit: z.number().int().nonnegative(),
|
|
165
|
-
dailySendRemaining: z.number().int().nonnegative(),
|
|
166
|
-
kycTier: z.string().min(1),
|
|
167
|
-
kycStatus: z.string().min(1),
|
|
168
|
-
recentActivity: z.array(AccountActivityItemSchema)
|
|
169
|
-
});
|
|
170
|
-
var TransactionsListResponseSchema = z.object({
|
|
171
|
-
items: z.array(AccountActivityItemSchema),
|
|
172
|
-
nextCursor: z.string().nullable()
|
|
173
|
-
});
|
|
174
|
-
var TransactionDetailResponseSchema = z.object({
|
|
175
|
-
transactionId: z.string().min(1),
|
|
176
|
-
type: z.string().min(1),
|
|
177
|
-
direction: DirectionSchema,
|
|
178
|
-
counterpartyName: z.string().min(1),
|
|
179
|
-
counterpartyIdentifier: z.string().min(1),
|
|
180
|
-
amountMinor: z.number().int(),
|
|
181
|
-
currency: CurrencySchema,
|
|
182
|
-
status: z.string().min(1),
|
|
183
|
-
timestamp: IsoDateSchema
|
|
184
|
-
});
|
|
185
|
-
var PushRegisterRequestSchema = z.object({
|
|
186
|
-
deviceId: z.string().min(3),
|
|
187
|
-
platform: z.enum(["ios", "android", "web"]),
|
|
188
|
-
token: z.string().min(16)
|
|
189
|
-
});
|
|
190
|
-
var CreatePayLinkResponseSchema = z.object({
|
|
191
|
-
token: z.string().min(1)
|
|
192
|
-
});
|
|
193
|
-
var ResolvePayLinkResponseSchema = z.object({
|
|
194
|
-
recipientUserId: UuidSchema,
|
|
195
|
-
displayName: z.string().min(1),
|
|
196
|
-
normalizedIdentifier: z.string().regex(E164Regex),
|
|
197
|
-
isActive: z.boolean()
|
|
198
|
-
});
|
|
199
9
|
|
|
200
10
|
// src/errors.ts
|
|
201
11
|
var backendErrorCodeSet = /* @__PURE__ */ new Set([
|
|
@@ -299,9 +109,556 @@ async function mapToFlurError(res) {
|
|
|
299
109
|
});
|
|
300
110
|
}
|
|
301
111
|
|
|
112
|
+
// src/me-offline/client.ts
|
|
113
|
+
var Hex64 = z.string().regex(/^[0-9a-f]{64}$/i);
|
|
114
|
+
var Sha256Hex = z.string().regex(/^[0-9a-f]{64}$/i);
|
|
115
|
+
var Base64Std = z.string().regex(/^[A-Za-z0-9+/]+={0,2}$/);
|
|
116
|
+
var RegisterDeviceKeyInputSchema = z.object({
|
|
117
|
+
deviceId: z.string().min(1).max(128),
|
|
118
|
+
publicKeyHex: Hex64
|
|
119
|
+
});
|
|
120
|
+
var AttestationSecurityLevelSchema = z.enum([
|
|
121
|
+
"STRONGBOX",
|
|
122
|
+
"TEE",
|
|
123
|
+
"SECURE_ENCLAVE",
|
|
124
|
+
"SOFTWARE"
|
|
125
|
+
]);
|
|
126
|
+
var DeviceKeyAlgSchema = z.literal("p256");
|
|
127
|
+
var RegisterDeviceKeyP256InputSchema = z.object({
|
|
128
|
+
deviceId: z.string().min(1).max(128),
|
|
129
|
+
/** P-256 SubjectPublicKeyInfo DER, base64. */
|
|
130
|
+
publicKeySpkiB64: Base64Std.min(64).max(4096),
|
|
131
|
+
/** Base64 of the server-issued enrollment challenge string. */
|
|
132
|
+
challengeB64: Base64Std.min(8).max(1024),
|
|
133
|
+
/** iOS App Attest payload or Android X.509 Key Attestation chain. */
|
|
134
|
+
attestationChainB64: z.array(Base64Std.min(16).max(16384)).min(1).max(16),
|
|
135
|
+
securityLevel: AttestationSecurityLevelSchema
|
|
136
|
+
});
|
|
137
|
+
var P256EnrollmentChallengeInputSchema = z.object({
|
|
138
|
+
deviceId: z.string().min(1).max(128)
|
|
139
|
+
});
|
|
140
|
+
var P256EnrollmentChallengeResultSchema = z.object({
|
|
141
|
+
challenge: z.string().min(16),
|
|
142
|
+
expiresAtMs: z.number().int().positive()
|
|
143
|
+
});
|
|
144
|
+
var DeviceKeyRecordSchema = z.object({
|
|
145
|
+
id: z.string().uuid(),
|
|
146
|
+
userId: z.string().uuid(),
|
|
147
|
+
deviceId: z.string(),
|
|
148
|
+
/** Always 'p256' on the consumer offline rail. Field retained for forward-compat. */
|
|
149
|
+
alg: DeviceKeyAlgSchema.default("p256"),
|
|
150
|
+
/** Legacy ed25519 hex key. Always null on new records (kept for back-compat reads). */
|
|
151
|
+
publicKeyHex: Hex64.nullable().default(null),
|
|
152
|
+
/** P-256 SubjectPublicKeyInfo DER, base64. Required for new records. */
|
|
153
|
+
publicKeySpkiB64: Base64Std.nullable().default(null),
|
|
154
|
+
securityLevel: AttestationSecurityLevelSchema.nullable().default(null),
|
|
155
|
+
hardwareBacked: z.boolean().default(false),
|
|
156
|
+
attestedAtMs: z.number().int().nonnegative().nullable().default(null),
|
|
157
|
+
createdAtMs: z.number().int().nonnegative(),
|
|
158
|
+
revokedAtMs: z.number().int().nonnegative().nullable()
|
|
159
|
+
});
|
|
160
|
+
var ConsumerOACSchema = z.object({
|
|
161
|
+
oacId: z.string().uuid(),
|
|
162
|
+
issuerId: z.string().min(1).max(64),
|
|
163
|
+
userId: z.string().uuid(),
|
|
164
|
+
deviceId: z.string().min(1).max(128),
|
|
165
|
+
/**
|
|
166
|
+
* Always 'p256'. Required on the wire (backend always emits it).
|
|
167
|
+
* Kept as a literal so input/output infer identically and the schema
|
|
168
|
+
* can be nested inside other response shapes without Zod input/output
|
|
169
|
+
* divergence under tsup DTS bundling.
|
|
170
|
+
*/
|
|
171
|
+
alg: z.literal("p256"),
|
|
172
|
+
/** P-256 SubjectPublicKeyInfo DER, base64. */
|
|
173
|
+
devicePubkeySpkiB64: Base64Std.min(64).max(4096),
|
|
174
|
+
perTxCapKobo: z.number().int().positive(),
|
|
175
|
+
cumulativeCapKobo: z.number().int().positive(),
|
|
176
|
+
currency: z.string().length(3),
|
|
177
|
+
validFromMs: z.number().int().nonnegative(),
|
|
178
|
+
validUntilMs: z.number().int().nonnegative(),
|
|
179
|
+
counterSeed: z.number().int().nonnegative(),
|
|
180
|
+
issuedAtMs: z.number().int().nonnegative()
|
|
181
|
+
});
|
|
182
|
+
var SignedConsumerOACSchema = z.object({
|
|
183
|
+
oac: ConsumerOACSchema,
|
|
184
|
+
/** ASN.1 DER ECDSA P-256 issuer signature, base64. */
|
|
185
|
+
issuerSig: Base64Std.min(16).max(2048),
|
|
186
|
+
/** Issuer's P-256 public key as SubjectPublicKeyInfo DER, base64. */
|
|
187
|
+
issuerPublicKeySpkiB64: Base64Std.min(64).max(4096)
|
|
188
|
+
});
|
|
189
|
+
var OACRecordSchema = SignedConsumerOACSchema.extend({
|
|
190
|
+
currentOfflineSpentKobo: z.number().int().nonnegative(),
|
|
191
|
+
status: z.enum([
|
|
192
|
+
"active",
|
|
193
|
+
"superseded",
|
|
194
|
+
"expired",
|
|
195
|
+
"revoked",
|
|
196
|
+
"disabling",
|
|
197
|
+
"draining",
|
|
198
|
+
"closed"
|
|
199
|
+
]),
|
|
200
|
+
supersededAtMs: z.number().int().nonnegative().nullable(),
|
|
201
|
+
revokedAtMs: z.number().int().nonnegative().nullable(),
|
|
202
|
+
holdId: z.string().uuid().nullable().optional()
|
|
203
|
+
});
|
|
204
|
+
var IssueOACInputSchema = z.object({
|
|
205
|
+
deviceId: z.string().min(1).max(128),
|
|
206
|
+
perTxCapKobo: z.number().int().positive().optional(),
|
|
207
|
+
cumulativeCapKobo: z.number().int().positive().optional(),
|
|
208
|
+
ttlMs: z.number().int().min(6e4).max(1e3 * 60 * 60 * 24 * 7).optional(),
|
|
209
|
+
spendableOnlineKobo: z.number().int().nonnegative().optional()
|
|
210
|
+
});
|
|
211
|
+
var IssueAccountOacInputSchema = z.object({
|
|
212
|
+
deviceId: z.string().min(1).max(128),
|
|
213
|
+
perTxCapKobo: z.number().int().positive().optional(),
|
|
214
|
+
cumulativeCapKobo: z.number().int().positive().optional(),
|
|
215
|
+
ttlMs: z.number().int().min(6e4).max(1e3 * 60 * 60 * 24 * 7).optional()
|
|
216
|
+
});
|
|
217
|
+
var EnableOfflineInputSchema = z.object({
|
|
218
|
+
deviceId: z.string().min(1).max(128),
|
|
219
|
+
amountKobo: z.number().int().positive(),
|
|
220
|
+
perTxCapKobo: z.number().int().positive().optional(),
|
|
221
|
+
ttlMs: z.number().int().min(6e4).max(1e3 * 60 * 60 * 24 * 7).optional(),
|
|
222
|
+
installId: z.string().min(1).max(128),
|
|
223
|
+
partnerId: z.string().min(1).max(64).optional()
|
|
224
|
+
});
|
|
225
|
+
var ProvisionOfflineAllowanceInputSchema = EnableOfflineInputSchema;
|
|
226
|
+
var DisableOfflineInputSchema = z.object({
|
|
227
|
+
deviceId: z.string().min(1).max(128),
|
|
228
|
+
installId: z.string().min(1).max(128).optional(),
|
|
229
|
+
claims: z.array(z.unknown()).max(256).optional()
|
|
230
|
+
});
|
|
231
|
+
var OfflineHoldRecordSchema = z.object({
|
|
232
|
+
holdId: z.string().uuid(),
|
|
233
|
+
userId: z.string().uuid(),
|
|
234
|
+
deviceId: z.string(),
|
|
235
|
+
partnerId: z.string(),
|
|
236
|
+
adapterKind: z.string(),
|
|
237
|
+
externalHoldRef: z.string().nullable(),
|
|
238
|
+
amountKobo: z.number().int().nonnegative(),
|
|
239
|
+
capturedKobo: z.number().int().nonnegative(),
|
|
240
|
+
releasedKobo: z.number().int().nonnegative(),
|
|
241
|
+
remainingKobo: z.number().int().nonnegative(),
|
|
242
|
+
currency: z.string().length(3),
|
|
243
|
+
status: z.enum([
|
|
244
|
+
"placing",
|
|
245
|
+
"active",
|
|
246
|
+
"disabling",
|
|
247
|
+
"draining",
|
|
248
|
+
"closed",
|
|
249
|
+
"revoked",
|
|
250
|
+
"failed"
|
|
251
|
+
]),
|
|
252
|
+
installId: z.string().nullable(),
|
|
253
|
+
drainDeadlineMs: z.number().int().nonnegative(),
|
|
254
|
+
disableRequestedAtMs: z.number().int().nonnegative().nullable(),
|
|
255
|
+
createdAtMs: z.number().int().nonnegative(),
|
|
256
|
+
closedAtMs: z.number().int().nonnegative().nullable(),
|
|
257
|
+
isTrusted: z.boolean().optional()
|
|
258
|
+
});
|
|
259
|
+
var EnableOfflineResultSchema = z.object({
|
|
260
|
+
hold: OfflineHoldRecordSchema,
|
|
261
|
+
oac: OACRecordSchema
|
|
262
|
+
});
|
|
263
|
+
var ProvisionOfflineAllowanceResultSchema = EnableOfflineResultSchema;
|
|
264
|
+
var DisableOfflineResultSchema = z.object({
|
|
265
|
+
hold: OfflineHoldRecordSchema,
|
|
266
|
+
trusted: z.boolean(),
|
|
267
|
+
settledClaims: z.number().int().nonnegative()
|
|
268
|
+
});
|
|
269
|
+
var OfflineStatusResultSchema = z.object({
|
|
270
|
+
hold: OfflineHoldRecordSchema.nullable(),
|
|
271
|
+
active: OACRecordSchema.nullable()
|
|
272
|
+
});
|
|
273
|
+
var OfflineStateResultSchema = z.object({
|
|
274
|
+
active: OACRecordSchema.nullable()
|
|
275
|
+
});
|
|
276
|
+
var ConsumerPaymentClaimSchema = z.object({
|
|
277
|
+
/** Always 'p256'. Retained for forward-compat and as an explicit domain marker. */
|
|
278
|
+
alg: z.literal("p256").default("p256"),
|
|
279
|
+
oacId: z.string().uuid(),
|
|
280
|
+
encounterId: Sha256Hex.optional(),
|
|
281
|
+
payerUserId: z.string().uuid(),
|
|
282
|
+
payeeUserId: z.string().uuid(),
|
|
283
|
+
payerDeviceId: z.string().min(1).max(128),
|
|
284
|
+
payerNonce: z.string().min(8).max(128),
|
|
285
|
+
payeeNonce: z.string().min(8).max(128),
|
|
286
|
+
amountKobo: z.number().int().positive(),
|
|
287
|
+
currency: z.string().length(3).default("NGN"),
|
|
288
|
+
occurredAtMs: z.number().int().nonnegative(),
|
|
289
|
+
completedAtMs: z.number().int().nonnegative().optional(),
|
|
290
|
+
contextId: z.string().max(128).optional(),
|
|
291
|
+
payerPubkeySpkiB64: Base64Std.min(64).max(4096),
|
|
292
|
+
payerSignatureDerB64: Base64Std.min(16).max(2048),
|
|
293
|
+
payeePubkeySpkiB64: Base64Std.min(64).max(4096).optional(),
|
|
294
|
+
payeeSignatureDerB64: Base64Std.min(16).max(2048).optional()
|
|
295
|
+
});
|
|
296
|
+
var ConsumerSettlementSchema = z.object({
|
|
297
|
+
settlementId: z.string().uuid(),
|
|
298
|
+
settlementKey: Sha256Hex,
|
|
299
|
+
encounterId: Sha256Hex,
|
|
300
|
+
oacId: z.string().uuid(),
|
|
301
|
+
payerUserId: z.string().uuid(),
|
|
302
|
+
payeeUserId: z.string().uuid(),
|
|
303
|
+
amountKobo: z.number().int().positive(),
|
|
304
|
+
currency: z.string().length(3),
|
|
305
|
+
status: z.enum(["SETTLED", "REVIEW"]),
|
|
306
|
+
reviewReason: z.string().nullable(),
|
|
307
|
+
ledgerRef: z.string().nullable(),
|
|
308
|
+
/** ASN.1 DER ECDSA P-256 issuer signature, base64. */
|
|
309
|
+
issuerSig: Base64Std.min(16).max(2048),
|
|
310
|
+
createdAtMs: z.number().int().nonnegative()
|
|
311
|
+
});
|
|
312
|
+
var ConsumerSettleResultSchema = z.object({
|
|
313
|
+
settlement: ConsumerSettlementSchema,
|
|
314
|
+
encounterId: Sha256Hex,
|
|
315
|
+
replayed: z.boolean()
|
|
316
|
+
});
|
|
317
|
+
var RevokeDeviceKeyInputSchema = z.object({
|
|
318
|
+
deviceId: z.string().min(1).max(128),
|
|
319
|
+
/** Step-up token from /api/v1/auth/send/verify with purpose='offline_revoke'. */
|
|
320
|
+
sendAuthToken: z.string().min(16)
|
|
321
|
+
});
|
|
322
|
+
function createMeOfflineClient(opts) {
|
|
323
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
324
|
+
if (!fetchImpl) {
|
|
325
|
+
throw new Error("createMeOfflineClient: no fetch implementation available");
|
|
326
|
+
}
|
|
327
|
+
const baseUrl = opts.baseUrl.replace(/\/$/, "");
|
|
328
|
+
async function call(method, path, body, parser) {
|
|
329
|
+
const init2 = {
|
|
330
|
+
method,
|
|
331
|
+
headers: {
|
|
332
|
+
"content-type": "application/json",
|
|
333
|
+
accept: "application/json"
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
if (body !== void 0) init2.body = JSON.stringify(body);
|
|
337
|
+
const resp = await fetchImpl(`${baseUrl}${path}`, init2);
|
|
338
|
+
const text = await resp.text();
|
|
339
|
+
let raw = void 0;
|
|
340
|
+
if (text) {
|
|
341
|
+
try {
|
|
342
|
+
raw = JSON.parse(text);
|
|
343
|
+
} catch {
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (!resp.ok) {
|
|
347
|
+
const code = raw && typeof raw === "object" && "code" in raw && typeof raw.code === "string" ? raw.code : `http_${resp.status}`;
|
|
348
|
+
const message = raw && typeof raw === "object" && "message" in raw && typeof raw.message === "string" ? raw.message : `request failed with status ${resp.status}`;
|
|
349
|
+
throw new FlurApiError(resp.status, code, message, raw);
|
|
350
|
+
}
|
|
351
|
+
return parser(raw);
|
|
352
|
+
}
|
|
353
|
+
const deviceKeyItems = z.object({ items: z.array(DeviceKeyRecordSchema) });
|
|
354
|
+
return {
|
|
355
|
+
registerDeviceKey: (input) => call(
|
|
356
|
+
"POST",
|
|
357
|
+
"/v1/me/offline/keys",
|
|
358
|
+
RegisterDeviceKeyInputSchema.parse(input),
|
|
359
|
+
(raw) => DeviceKeyRecordSchema.parse(raw)
|
|
360
|
+
),
|
|
361
|
+
issueP256EnrollmentChallenge: (input) => call(
|
|
362
|
+
"POST",
|
|
363
|
+
"/v1/me/offline/keys/p256/challenge",
|
|
364
|
+
P256EnrollmentChallengeInputSchema.parse(input),
|
|
365
|
+
(raw) => P256EnrollmentChallengeResultSchema.parse(raw)
|
|
366
|
+
),
|
|
367
|
+
registerDeviceKeyP256: (input) => call(
|
|
368
|
+
"POST",
|
|
369
|
+
"/v1/me/offline/keys/p256",
|
|
370
|
+
RegisterDeviceKeyP256InputSchema.parse(input),
|
|
371
|
+
(raw) => DeviceKeyRecordSchema.parse(raw)
|
|
372
|
+
),
|
|
373
|
+
listDeviceKeys: () => call(
|
|
374
|
+
"GET",
|
|
375
|
+
"/v1/me/offline/keys",
|
|
376
|
+
void 0,
|
|
377
|
+
(raw) => deviceKeyItems.parse(raw)
|
|
378
|
+
),
|
|
379
|
+
revokeDeviceKey: (input) => call(
|
|
380
|
+
"POST",
|
|
381
|
+
"/v1/me/offline/keys/revoke",
|
|
382
|
+
RevokeDeviceKeyInputSchema.parse(input),
|
|
383
|
+
() => void 0
|
|
384
|
+
),
|
|
385
|
+
issueAccountOac: (input) => call(
|
|
386
|
+
"POST",
|
|
387
|
+
"/v1/me/offline/oac",
|
|
388
|
+
IssueAccountOacInputSchema.parse(input),
|
|
389
|
+
(raw) => OACRecordSchema.parse(raw)
|
|
390
|
+
),
|
|
391
|
+
provisionAllowance: (input) => call(
|
|
392
|
+
"POST",
|
|
393
|
+
"/v1/me/offline/allowance",
|
|
394
|
+
ProvisionOfflineAllowanceInputSchema.parse(input),
|
|
395
|
+
(raw) => ProvisionOfflineAllowanceResultSchema.parse(raw)
|
|
396
|
+
),
|
|
397
|
+
enable: (input) => call(
|
|
398
|
+
"POST",
|
|
399
|
+
"/v1/me/offline/enable",
|
|
400
|
+
EnableOfflineInputSchema.parse(input),
|
|
401
|
+
(raw) => EnableOfflineResultSchema.parse(raw)
|
|
402
|
+
),
|
|
403
|
+
refresh: (input) => call(
|
|
404
|
+
"POST",
|
|
405
|
+
"/v1/me/offline/refresh",
|
|
406
|
+
IssueOACInputSchema.parse(input),
|
|
407
|
+
(raw) => OACRecordSchema.parse(raw)
|
|
408
|
+
),
|
|
409
|
+
disable: (input) => call(
|
|
410
|
+
"POST",
|
|
411
|
+
"/v1/me/offline/disable",
|
|
412
|
+
DisableOfflineInputSchema.parse(input),
|
|
413
|
+
(raw) => DisableOfflineResultSchema.parse(raw)
|
|
414
|
+
),
|
|
415
|
+
getStatus: (deviceId) => {
|
|
416
|
+
const qs = deviceId ? `?deviceId=${encodeURIComponent(deviceId)}` : "";
|
|
417
|
+
return call(
|
|
418
|
+
"GET",
|
|
419
|
+
`/v1/me/offline/status${qs}`,
|
|
420
|
+
void 0,
|
|
421
|
+
(raw) => OfflineStatusResultSchema.parse(raw)
|
|
422
|
+
);
|
|
423
|
+
},
|
|
424
|
+
getState: (deviceId) => {
|
|
425
|
+
const qs = deviceId ? `?deviceId=${encodeURIComponent(deviceId)}` : "";
|
|
426
|
+
return call(
|
|
427
|
+
"GET",
|
|
428
|
+
`/v1/me/offline/state${qs}`,
|
|
429
|
+
void 0,
|
|
430
|
+
(raw) => OfflineStateResultSchema.parse(raw)
|
|
431
|
+
);
|
|
432
|
+
},
|
|
433
|
+
submitClaim: (claim) => call(
|
|
434
|
+
"POST",
|
|
435
|
+
"/v1/me/offline/claims",
|
|
436
|
+
ConsumerPaymentClaimSchema.parse(claim),
|
|
437
|
+
(raw) => ConsumerSettleResultSchema.parse(raw)
|
|
438
|
+
)
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// src/contracts.ts
|
|
443
|
+
var E164Regex = /^\+[1-9]\d{7,14}$/;
|
|
444
|
+
var UuidSchema = z2.string().uuid();
|
|
445
|
+
var IsoDateSchema = z2.string().datetime({ offset: true });
|
|
446
|
+
var CurrencySchema = z2.string().trim().length(3).transform((value) => value.toUpperCase());
|
|
447
|
+
var HealthResponseSchema = z2.object({
|
|
448
|
+
ok: z2.boolean()
|
|
449
|
+
});
|
|
450
|
+
var WelcomeResponseSchema = z2.object({
|
|
451
|
+
message: z2.string()
|
|
452
|
+
});
|
|
453
|
+
var OnboardingStartRequestSchema = z2.object({
|
|
454
|
+
phoneE164: z2.string().regex(E164Regex),
|
|
455
|
+
appInstanceId: z2.string().min(3),
|
|
456
|
+
platform: z2.enum(["android", "ios", "web"]),
|
|
457
|
+
turnstileToken: z2.string().min(20).optional(),
|
|
458
|
+
appAttestation: z2.object({
|
|
459
|
+
provider: z2.enum(["android", "ios", "web"]),
|
|
460
|
+
token: z2.string().min(24)
|
|
461
|
+
}).optional(),
|
|
462
|
+
firstName: z2.string().trim().min(1).max(80).optional(),
|
|
463
|
+
lastName: z2.string().trim().min(1).max(80).optional()
|
|
464
|
+
});
|
|
465
|
+
var OnboardingStartResponseSchema = z2.object({
|
|
466
|
+
requestId: z2.string().min(1),
|
|
467
|
+
checkUrl: z2.string().url().optional(),
|
|
468
|
+
expiresInSec: z2.number().int().positive(),
|
|
469
|
+
fallback: z2.enum(["SILENT_AUTH", "OTP"])
|
|
470
|
+
});
|
|
471
|
+
var OnboardingCompleteRequestSchema = z2.object({
|
|
472
|
+
requestId: z2.string().min(1),
|
|
473
|
+
code: z2.string().min(1).max(32),
|
|
474
|
+
appInstanceId: z2.string().min(3),
|
|
475
|
+
fingerprintHash: z2.string().min(3).optional()
|
|
476
|
+
});
|
|
477
|
+
var OnboardingCompleteResponseSchema = z2.object({
|
|
478
|
+
sessionToken: z2.string().min(1),
|
|
479
|
+
userId: UuidSchema,
|
|
480
|
+
restricted: z2.boolean(),
|
|
481
|
+
risk_reasons: z2.array(
|
|
482
|
+
z2.enum(["SIM_SWAP_RECENT", "ROAMING", "CARRIER_CHANGED"])
|
|
483
|
+
),
|
|
484
|
+
stepUpRequired: z2.boolean().optional(),
|
|
485
|
+
riskStatus: z2.enum(["ok", "unavailable"]).optional()
|
|
486
|
+
});
|
|
487
|
+
var RegisterDeviceRequestSchema = z2.object({
|
|
488
|
+
userId: UuidSchema,
|
|
489
|
+
appInstanceId: z2.string().min(3),
|
|
490
|
+
platform: z2.string().min(2),
|
|
491
|
+
model: z2.string().optional(),
|
|
492
|
+
networkSignals: z2.object({
|
|
493
|
+
ip: z2.string().min(3),
|
|
494
|
+
asn: z2.number().int().optional(),
|
|
495
|
+
country: z2.string().min(2).optional(),
|
|
496
|
+
carrier: z2.string().optional()
|
|
497
|
+
})
|
|
498
|
+
});
|
|
499
|
+
var RegisterDeviceResponseSchema = z2.object({
|
|
500
|
+
deviceId: z2.string().min(1),
|
|
501
|
+
fingerprintHash: z2.string().min(1),
|
|
502
|
+
driftScore: z2.number(),
|
|
503
|
+
trustState: z2.enum(["TRUSTED_PRIMARY", "TRUSTED_SECONDARY", "UNVERIFIED"]),
|
|
504
|
+
stepUpRequired: z2.boolean()
|
|
505
|
+
});
|
|
506
|
+
var AuthRefreshRequestSchema = z2.object({
|
|
507
|
+
userId: UuidSchema,
|
|
508
|
+
refreshToken: z2.string().min(8),
|
|
509
|
+
appInstanceId: z2.string().min(3),
|
|
510
|
+
fingerprintHash: z2.string().min(3)
|
|
511
|
+
});
|
|
512
|
+
var AuthRefreshResponseSchema = z2.object({
|
|
513
|
+
refreshToken: z2.string().min(8),
|
|
514
|
+
stepUpRequired: z2.boolean()
|
|
515
|
+
});
|
|
516
|
+
var AuthLogoutRequestSchema = z2.object({
|
|
517
|
+
userId: UuidSchema,
|
|
518
|
+
refreshToken: z2.string().min(8)
|
|
519
|
+
});
|
|
520
|
+
var PinSetRequestSchema = z2.object({
|
|
521
|
+
userId: UuidSchema,
|
|
522
|
+
pin: z2.string().regex(/^\d{6}$/)
|
|
523
|
+
});
|
|
524
|
+
var PinVerifyRequestSchema = z2.object({
|
|
525
|
+
userId: UuidSchema,
|
|
526
|
+
pin: z2.string().regex(/^\d{6}$/)
|
|
527
|
+
});
|
|
528
|
+
var OkResponseSchema = z2.object({
|
|
529
|
+
ok: z2.boolean()
|
|
530
|
+
});
|
|
531
|
+
var RegisterSendDeviceKeyRequestSchema = z2.object({
|
|
532
|
+
userId: UuidSchema,
|
|
533
|
+
deviceId: z2.string().min(3),
|
|
534
|
+
publicKey: z2.string().min(32)
|
|
535
|
+
});
|
|
536
|
+
var SEND_AUTH_PURPOSES = ["send_money", "offline_revoke"];
|
|
537
|
+
var SendChallengeRequestSchema = z2.object({
|
|
538
|
+
userId: UuidSchema,
|
|
539
|
+
deviceId: z2.string().min(3),
|
|
540
|
+
purpose: z2.enum(SEND_AUTH_PURPOSES).optional()
|
|
541
|
+
});
|
|
542
|
+
var SendChallengeResponseSchema = z2.object({
|
|
543
|
+
challengeId: UuidSchema,
|
|
544
|
+
nonce: z2.string().min(1),
|
|
545
|
+
expiresAt: IsoDateSchema
|
|
546
|
+
});
|
|
547
|
+
var SendVerifyRequestSchema = z2.object({
|
|
548
|
+
userId: UuidSchema,
|
|
549
|
+
deviceId: z2.string().min(3),
|
|
550
|
+
challengeId: UuidSchema,
|
|
551
|
+
signature: z2.string().min(16)
|
|
552
|
+
});
|
|
553
|
+
var SendVerifyResponseSchema = z2.object({
|
|
554
|
+
sendAuthToken: z2.string().min(16)
|
|
555
|
+
});
|
|
556
|
+
var ResolveRecipientRequestSchema = z2.object({
|
|
557
|
+
identifier: z2.string().min(3)
|
|
558
|
+
});
|
|
559
|
+
var ResolveRecipientResponseSchema = z2.object({
|
|
560
|
+
recipientUserId: UuidSchema,
|
|
561
|
+
displayName: z2.string().min(1),
|
|
562
|
+
normalizedIdentifier: z2.string().regex(E164Regex),
|
|
563
|
+
isActive: z2.boolean()
|
|
564
|
+
});
|
|
565
|
+
var CreateTransferRequestSchema = z2.object({
|
|
566
|
+
recipientIdentifier: z2.string().min(3),
|
|
567
|
+
amountMinor: z2.number().int().positive(),
|
|
568
|
+
currency: CurrencySchema,
|
|
569
|
+
sendAuthToken: z2.string().min(16)
|
|
570
|
+
});
|
|
571
|
+
var TransferStatusSchema = z2.enum(["SETTLED", "PENDING_REVIEW", "DECLINED"]);
|
|
572
|
+
var TransferResponseSchema = z2.object({
|
|
573
|
+
transactionId: z2.string().min(1),
|
|
574
|
+
status: TransferStatusSchema,
|
|
575
|
+
userStatus: TransferStatusSchema,
|
|
576
|
+
recipientName: z2.string().min(1),
|
|
577
|
+
timestamp: IsoDateSchema,
|
|
578
|
+
/**
|
|
579
|
+
* Fresh issuer-signed OACs returned by the backend's post-commit
|
|
580
|
+
* rotation hook on `SETTLED` transfers.
|
|
581
|
+
*
|
|
582
|
+
* In the current backend implementation this hook rotates LEGACY
|
|
583
|
+
* hold-backed OACs only. Account-funded (no-hold) OACs introduced by
|
|
584
|
+
* the unified-pay-rails refactor are NOT rotated here — their
|
|
585
|
+
* cumulative spend counter and main-balance check are enforced at
|
|
586
|
+
* claim submission time via `createTransfer` under the per-sender
|
|
587
|
+
* advisory lock, so a stale no-hold OAC cannot authorise overspending
|
|
588
|
+
* (it can only resolve to REVIEW on submission).
|
|
589
|
+
*
|
|
590
|
+
* Optional: omitted when the sender has no registered hold-backed
|
|
591
|
+
* devices, when the refresh failed (best-effort), or on retry replays
|
|
592
|
+
* that pre-date the refresh. When present, the entries supersede any
|
|
593
|
+
* locally cached OACs for the listed devices; when absent, clients
|
|
594
|
+
* should fall back to `/v1/me/offline/refresh`.
|
|
595
|
+
*
|
|
596
|
+
* Each entry is validated against the canonical `OACRecordSchema`,
|
|
597
|
+
* so consumers can trust the runtime shape matches the static type.
|
|
598
|
+
*/
|
|
599
|
+
offlineOacs: OACRecordSchema.array().optional()
|
|
600
|
+
});
|
|
601
|
+
var DirectionSchema = z2.enum(["OUTGOING", "INCOMING"]);
|
|
602
|
+
var AccountActivityItemSchema = z2.object({
|
|
603
|
+
id: z2.string().min(1),
|
|
604
|
+
type: z2.string().min(1),
|
|
605
|
+
direction: DirectionSchema,
|
|
606
|
+
name: z2.string().min(1),
|
|
607
|
+
identifier: z2.string().min(1),
|
|
608
|
+
amountMinor: z2.number().int(),
|
|
609
|
+
currency: CurrencySchema,
|
|
610
|
+
status: z2.string().min(1),
|
|
611
|
+
timestamp: IsoDateSchema
|
|
612
|
+
});
|
|
613
|
+
var AccountSummaryResponseSchema = z2.object({
|
|
614
|
+
/** Authenticated user's stable internal id. */
|
|
615
|
+
userId: UuidSchema,
|
|
616
|
+
/**
|
|
617
|
+
* 10-digit Nigeria Uniform Bank Account Number (NUBAN) allocated by the
|
|
618
|
+
* bank partner. `null` when the user has no partner-allocated account yet.
|
|
619
|
+
*/
|
|
620
|
+
nuban: z2.string().regex(/^[0-9]{10}$/).nullable(),
|
|
621
|
+
balance: z2.number().int(),
|
|
622
|
+
currency: CurrencySchema,
|
|
623
|
+
dailySendLimit: z2.number().int().nonnegative(),
|
|
624
|
+
dailySendRemaining: z2.number().int().nonnegative(),
|
|
625
|
+
kycTier: z2.string().min(1),
|
|
626
|
+
kycStatus: z2.string().min(1),
|
|
627
|
+
recentActivity: z2.array(AccountActivityItemSchema)
|
|
628
|
+
});
|
|
629
|
+
var TransactionsListResponseSchema = z2.object({
|
|
630
|
+
items: z2.array(AccountActivityItemSchema),
|
|
631
|
+
nextCursor: z2.string().nullable()
|
|
632
|
+
});
|
|
633
|
+
var TransactionDetailResponseSchema = z2.object({
|
|
634
|
+
transactionId: z2.string().min(1),
|
|
635
|
+
type: z2.string().min(1),
|
|
636
|
+
direction: DirectionSchema,
|
|
637
|
+
counterpartyName: z2.string().min(1),
|
|
638
|
+
counterpartyIdentifier: z2.string().min(1),
|
|
639
|
+
amountMinor: z2.number().int(),
|
|
640
|
+
currency: CurrencySchema,
|
|
641
|
+
status: z2.string().min(1),
|
|
642
|
+
timestamp: IsoDateSchema
|
|
643
|
+
});
|
|
644
|
+
var PushRegisterRequestSchema = z2.object({
|
|
645
|
+
deviceId: z2.string().min(3),
|
|
646
|
+
platform: z2.enum(["ios", "android", "web"]),
|
|
647
|
+
token: z2.string().min(16)
|
|
648
|
+
});
|
|
649
|
+
var CreatePayLinkResponseSchema = z2.object({
|
|
650
|
+
token: z2.string().min(1)
|
|
651
|
+
});
|
|
652
|
+
var ResolvePayLinkResponseSchema = z2.object({
|
|
653
|
+
recipientUserId: UuidSchema,
|
|
654
|
+
displayName: z2.string().min(1),
|
|
655
|
+
normalizedIdentifier: z2.string().regex(E164Regex),
|
|
656
|
+
isActive: z2.boolean()
|
|
657
|
+
});
|
|
658
|
+
|
|
302
659
|
// src/primitives.ts
|
|
303
|
-
import { z as
|
|
304
|
-
var CurrencyCodeSchema =
|
|
660
|
+
import { z as z3 } from "zod";
|
|
661
|
+
var CurrencyCodeSchema = z3.string().trim().length(3).transform((value) => value.toUpperCase());
|
|
305
662
|
var currencyFractionDigits = {
|
|
306
663
|
NGN: 2,
|
|
307
664
|
USD: 2,
|
|
@@ -395,7 +752,7 @@ function moneyMinorToNumber(amountMinor) {
|
|
|
395
752
|
}
|
|
396
753
|
|
|
397
754
|
// src/collections/client.ts
|
|
398
|
-
import { z as
|
|
755
|
+
import { z as z4 } from "zod";
|
|
399
756
|
var MERCHANT_PROFILE_STATUSES = [
|
|
400
757
|
"pending",
|
|
401
758
|
"active",
|
|
@@ -425,172 +782,172 @@ var MERCHANT_PAYOUT_STATUSES = [
|
|
|
425
782
|
"failed",
|
|
426
783
|
"cancelled"
|
|
427
784
|
];
|
|
428
|
-
var MoneyKoboSchema =
|
|
429
|
-
var MetadataSchema =
|
|
430
|
-
|
|
785
|
+
var MoneyKoboSchema = z4.number().int().positive().max(Number.MAX_SAFE_INTEGER);
|
|
786
|
+
var MetadataSchema = z4.record(
|
|
787
|
+
z4.union([z4.string(), z4.number(), z4.boolean(), z4.null()])
|
|
431
788
|
);
|
|
432
|
-
var CurrencySchema2 =
|
|
433
|
-
var ReferenceSchema =
|
|
434
|
-
var MerchantProfileSchema =
|
|
435
|
-
accountId:
|
|
436
|
-
legalName:
|
|
437
|
-
tradingName:
|
|
438
|
-
merchantCategoryCode:
|
|
439
|
-
nqrMerchantId:
|
|
440
|
-
settlementBankCode:
|
|
441
|
-
settlementAccountNumber:
|
|
442
|
-
settlementAccountName:
|
|
443
|
-
settlementSchedule:
|
|
444
|
-
status:
|
|
445
|
-
offlineEnabled:
|
|
789
|
+
var CurrencySchema2 = z4.string().trim().length(3).transform((value) => value.toUpperCase());
|
|
790
|
+
var ReferenceSchema = z4.string().trim().min(6).max(128).regex(/^[A-Za-z0-9._:-]+$/);
|
|
791
|
+
var MerchantProfileSchema = z4.object({
|
|
792
|
+
accountId: z4.string().uuid(),
|
|
793
|
+
legalName: z4.string(),
|
|
794
|
+
tradingName: z4.string(),
|
|
795
|
+
merchantCategoryCode: z4.string().regex(/^\d{4}$/),
|
|
796
|
+
nqrMerchantId: z4.string(),
|
|
797
|
+
settlementBankCode: z4.string(),
|
|
798
|
+
settlementAccountNumber: z4.string(),
|
|
799
|
+
settlementAccountName: z4.string(),
|
|
800
|
+
settlementSchedule: z4.enum(SETTLEMENT_SCHEDULES),
|
|
801
|
+
status: z4.enum(MERCHANT_PROFILE_STATUSES),
|
|
802
|
+
offlineEnabled: z4.boolean(),
|
|
446
803
|
perTxLimitKobo: MoneyKoboSchema,
|
|
447
804
|
dailyLimitKobo: MoneyKoboSchema,
|
|
448
805
|
metadata: MetadataSchema,
|
|
449
|
-
createdAtMs:
|
|
450
|
-
updatedAtMs:
|
|
806
|
+
createdAtMs: z4.number().int().nonnegative(),
|
|
807
|
+
updatedAtMs: z4.number().int().nonnegative()
|
|
451
808
|
});
|
|
452
|
-
var UpsertMerchantProfileInputSchema =
|
|
453
|
-
legalName:
|
|
454
|
-
tradingName:
|
|
455
|
-
merchantCategoryCode:
|
|
456
|
-
nqrMerchantId:
|
|
457
|
-
settlementBankCode:
|
|
458
|
-
settlementAccountNumber:
|
|
459
|
-
settlementAccountName:
|
|
460
|
-
settlementSchedule:
|
|
461
|
-
status:
|
|
462
|
-
offlineEnabled:
|
|
809
|
+
var UpsertMerchantProfileInputSchema = z4.object({
|
|
810
|
+
legalName: z4.string().trim().min(1).max(200),
|
|
811
|
+
tradingName: z4.string().trim().min(1).max(25),
|
|
812
|
+
merchantCategoryCode: z4.string().trim().regex(/^\d{4}$/),
|
|
813
|
+
nqrMerchantId: z4.string().trim().min(3).max(64),
|
|
814
|
+
settlementBankCode: z4.string().trim().min(2).max(16),
|
|
815
|
+
settlementAccountNumber: z4.string().trim().min(5).max(32),
|
|
816
|
+
settlementAccountName: z4.string().trim().min(1).max(200),
|
|
817
|
+
settlementSchedule: z4.enum(SETTLEMENT_SCHEDULES).optional(),
|
|
818
|
+
status: z4.enum(MERCHANT_PROFILE_STATUSES).optional(),
|
|
819
|
+
offlineEnabled: z4.boolean().optional(),
|
|
463
820
|
perTxLimitKobo: MoneyKoboSchema.optional(),
|
|
464
821
|
dailyLimitKobo: MoneyKoboSchema.optional(),
|
|
465
822
|
metadata: MetadataSchema.optional()
|
|
466
823
|
});
|
|
467
|
-
var CollectionIntentSchema =
|
|
468
|
-
intentId:
|
|
469
|
-
accountId:
|
|
470
|
-
terminalId:
|
|
471
|
-
reference:
|
|
472
|
-
amountKobo:
|
|
473
|
-
currency:
|
|
474
|
-
status:
|
|
475
|
-
description:
|
|
476
|
-
nqrPayload:
|
|
477
|
-
provider:
|
|
478
|
-
providerReference:
|
|
824
|
+
var CollectionIntentSchema = z4.object({
|
|
825
|
+
intentId: z4.string().uuid(),
|
|
826
|
+
accountId: z4.string().uuid(),
|
|
827
|
+
terminalId: z4.string().uuid().nullable(),
|
|
828
|
+
reference: z4.string(),
|
|
829
|
+
amountKobo: z4.number().int().positive().nullable(),
|
|
830
|
+
currency: z4.string().length(3),
|
|
831
|
+
status: z4.enum(COLLECTION_INTENT_STATUSES),
|
|
832
|
+
description: z4.string().nullable(),
|
|
833
|
+
nqrPayload: z4.string(),
|
|
834
|
+
provider: z4.string(),
|
|
835
|
+
providerReference: z4.string().nullable(),
|
|
479
836
|
metadata: MetadataSchema,
|
|
480
|
-
expiresAtMs:
|
|
481
|
-
paidAtMs:
|
|
482
|
-
createdAtMs:
|
|
483
|
-
updatedAtMs:
|
|
837
|
+
expiresAtMs: z4.number().int().nonnegative().nullable(),
|
|
838
|
+
paidAtMs: z4.number().int().nonnegative().nullable(),
|
|
839
|
+
createdAtMs: z4.number().int().nonnegative(),
|
|
840
|
+
updatedAtMs: z4.number().int().nonnegative()
|
|
484
841
|
});
|
|
485
|
-
var CreateCollectionIntentInputSchema =
|
|
842
|
+
var CreateCollectionIntentInputSchema = z4.object({
|
|
486
843
|
reference: ReferenceSchema.optional(),
|
|
487
844
|
amountKobo: MoneyKoboSchema.optional(),
|
|
488
845
|
currency: CurrencySchema2.optional(),
|
|
489
|
-
terminalId:
|
|
490
|
-
terminalLabel:
|
|
491
|
-
merchantCity:
|
|
492
|
-
description:
|
|
493
|
-
expiresAtMs:
|
|
494
|
-
provider:
|
|
846
|
+
terminalId: z4.string().uuid().optional(),
|
|
847
|
+
terminalLabel: z4.string().trim().min(1).max(25).optional(),
|
|
848
|
+
merchantCity: z4.string().trim().min(1).max(15).optional(),
|
|
849
|
+
description: z4.string().trim().min(1).max(280).optional(),
|
|
850
|
+
expiresAtMs: z4.number().int().positive().optional(),
|
|
851
|
+
provider: z4.string().trim().min(1).max(40).optional(),
|
|
495
852
|
metadata: MetadataSchema.optional()
|
|
496
853
|
});
|
|
497
|
-
var PublicCollectionIntentSchema =
|
|
498
|
-
intentId:
|
|
499
|
-
reference:
|
|
500
|
-
amountKobo:
|
|
501
|
-
currency:
|
|
502
|
-
status:
|
|
503
|
-
merchantAccountId:
|
|
504
|
-
merchantName:
|
|
505
|
-
merchantCategoryCode:
|
|
506
|
-
description:
|
|
507
|
-
expiresAtMs:
|
|
854
|
+
var PublicCollectionIntentSchema = z4.object({
|
|
855
|
+
intentId: z4.string().uuid(),
|
|
856
|
+
reference: z4.string(),
|
|
857
|
+
amountKobo: z4.number().int().positive().nullable(),
|
|
858
|
+
currency: z4.string().length(3),
|
|
859
|
+
status: z4.enum(COLLECTION_INTENT_STATUSES),
|
|
860
|
+
merchantAccountId: z4.string().uuid(),
|
|
861
|
+
merchantName: z4.string(),
|
|
862
|
+
merchantCategoryCode: z4.string(),
|
|
863
|
+
description: z4.string().nullable(),
|
|
864
|
+
expiresAtMs: z4.number().int().nonnegative().nullable()
|
|
508
865
|
});
|
|
509
|
-
var PayCollectionInputSchema =
|
|
866
|
+
var PayCollectionInputSchema = z4.object({
|
|
510
867
|
reference: ReferenceSchema,
|
|
511
868
|
amountKobo: MoneyKoboSchema.optional(),
|
|
512
869
|
currency: CurrencySchema2.optional(),
|
|
513
|
-
idempotencyKey:
|
|
870
|
+
idempotencyKey: z4.string().trim().min(8).max(160).optional()
|
|
514
871
|
});
|
|
515
|
-
var CollectionPaymentSchema =
|
|
516
|
-
paymentId:
|
|
517
|
-
intentId:
|
|
518
|
-
accountId:
|
|
519
|
-
payerUserId:
|
|
520
|
-
merchantOwnerUserId:
|
|
872
|
+
var CollectionPaymentSchema = z4.object({
|
|
873
|
+
paymentId: z4.string().uuid(),
|
|
874
|
+
intentId: z4.string().uuid(),
|
|
875
|
+
accountId: z4.string().uuid(),
|
|
876
|
+
payerUserId: z4.string().uuid().nullable(),
|
|
877
|
+
merchantOwnerUserId: z4.string().uuid(),
|
|
521
878
|
amountKobo: MoneyKoboSchema,
|
|
522
|
-
currency:
|
|
523
|
-
status:
|
|
524
|
-
provider:
|
|
525
|
-
providerReference:
|
|
526
|
-
idempotencyKey:
|
|
527
|
-
ledgerRef:
|
|
528
|
-
failureCode:
|
|
529
|
-
failureMessage:
|
|
530
|
-
paidAtMs:
|
|
531
|
-
createdAtMs:
|
|
532
|
-
updatedAtMs:
|
|
879
|
+
currency: z4.string().length(3),
|
|
880
|
+
status: z4.enum(COLLECTION_PAYMENT_STATUSES),
|
|
881
|
+
provider: z4.string(),
|
|
882
|
+
providerReference: z4.string().nullable(),
|
|
883
|
+
idempotencyKey: z4.string().nullable(),
|
|
884
|
+
ledgerRef: z4.string(),
|
|
885
|
+
failureCode: z4.string().nullable(),
|
|
886
|
+
failureMessage: z4.string().nullable(),
|
|
887
|
+
paidAtMs: z4.number().int().nonnegative().nullable(),
|
|
888
|
+
createdAtMs: z4.number().int().nonnegative(),
|
|
889
|
+
updatedAtMs: z4.number().int().nonnegative()
|
|
533
890
|
});
|
|
534
|
-
var CollectionPaymentResultSchema =
|
|
891
|
+
var CollectionPaymentResultSchema = z4.object({
|
|
535
892
|
payment: CollectionPaymentSchema,
|
|
536
893
|
intent: CollectionIntentSchema,
|
|
537
|
-
receipt:
|
|
538
|
-
replayed:
|
|
894
|
+
receipt: z4.unknown().optional(),
|
|
895
|
+
replayed: z4.boolean()
|
|
539
896
|
});
|
|
540
|
-
var CollectionReportSummarySchema =
|
|
541
|
-
accountId:
|
|
542
|
-
fromMs:
|
|
543
|
-
toMs:
|
|
544
|
-
currency:
|
|
545
|
-
paidCount:
|
|
546
|
-
paidAmountKobo:
|
|
547
|
-
pendingCount:
|
|
548
|
-
failedCount:
|
|
549
|
-
reversedCount:
|
|
550
|
-
availableBalanceKobo:
|
|
897
|
+
var CollectionReportSummarySchema = z4.object({
|
|
898
|
+
accountId: z4.string().uuid(),
|
|
899
|
+
fromMs: z4.number().int().nonnegative(),
|
|
900
|
+
toMs: z4.number().int().nonnegative(),
|
|
901
|
+
currency: z4.string().length(3),
|
|
902
|
+
paidCount: z4.number().int().nonnegative(),
|
|
903
|
+
paidAmountKobo: z4.number().int().nonnegative(),
|
|
904
|
+
pendingCount: z4.number().int().nonnegative(),
|
|
905
|
+
failedCount: z4.number().int().nonnegative(),
|
|
906
|
+
reversedCount: z4.number().int().nonnegative(),
|
|
907
|
+
availableBalanceKobo: z4.number().int().nonnegative()
|
|
551
908
|
});
|
|
552
|
-
var CollectionStatementSchema =
|
|
553
|
-
accountId:
|
|
554
|
-
year:
|
|
555
|
-
month:
|
|
556
|
-
currency:
|
|
557
|
-
totalPaidKobo:
|
|
558
|
-
items:
|
|
909
|
+
var CollectionStatementSchema = z4.object({
|
|
910
|
+
accountId: z4.string().uuid(),
|
|
911
|
+
year: z4.number().int(),
|
|
912
|
+
month: z4.number().int().min(1).max(12),
|
|
913
|
+
currency: z4.string().length(3),
|
|
914
|
+
totalPaidKobo: z4.number().int().nonnegative(),
|
|
915
|
+
items: z4.array(CollectionPaymentSchema)
|
|
559
916
|
});
|
|
560
|
-
var CreatePayoutInputSchema =
|
|
917
|
+
var CreatePayoutInputSchema = z4.object({
|
|
561
918
|
amountKobo: MoneyKoboSchema,
|
|
562
919
|
currency: CurrencySchema2.optional(),
|
|
563
|
-
idempotencyKey:
|
|
920
|
+
idempotencyKey: z4.string().trim().min(8).max(160)
|
|
564
921
|
});
|
|
565
|
-
var MerchantPayoutSchema =
|
|
566
|
-
payoutId:
|
|
567
|
-
accountId:
|
|
922
|
+
var MerchantPayoutSchema = z4.object({
|
|
923
|
+
payoutId: z4.string().uuid(),
|
|
924
|
+
accountId: z4.string().uuid(),
|
|
568
925
|
amountKobo: MoneyKoboSchema,
|
|
569
|
-
currency:
|
|
570
|
-
status:
|
|
571
|
-
idempotencyKey:
|
|
572
|
-
ledgerRef:
|
|
573
|
-
providerReference:
|
|
574
|
-
requestedByUserId:
|
|
575
|
-
failureCode:
|
|
576
|
-
failureMessage:
|
|
577
|
-
createdAtMs:
|
|
578
|
-
updatedAtMs:
|
|
926
|
+
currency: z4.string().length(3),
|
|
927
|
+
status: z4.enum(MERCHANT_PAYOUT_STATUSES),
|
|
928
|
+
idempotencyKey: z4.string().nullable(),
|
|
929
|
+
ledgerRef: z4.string(),
|
|
930
|
+
providerReference: z4.string().nullable(),
|
|
931
|
+
requestedByUserId: z4.string().uuid().nullable(),
|
|
932
|
+
failureCode: z4.string().nullable(),
|
|
933
|
+
failureMessage: z4.string().nullable(),
|
|
934
|
+
createdAtMs: z4.number().int().nonnegative(),
|
|
935
|
+
updatedAtMs: z4.number().int().nonnegative()
|
|
579
936
|
});
|
|
580
|
-
var ProviderEventInputSchema =
|
|
581
|
-
provider:
|
|
582
|
-
eventId:
|
|
583
|
-
eventType:
|
|
584
|
-
payload:
|
|
937
|
+
var ProviderEventInputSchema = z4.object({
|
|
938
|
+
provider: z4.string().trim().min(1).max(80),
|
|
939
|
+
eventId: z4.string().trim().min(1).max(160),
|
|
940
|
+
eventType: z4.string().trim().min(1).max(120),
|
|
941
|
+
payload: z4.record(z4.unknown()).optional()
|
|
585
942
|
});
|
|
586
|
-
var ProviderEventRecordSchema =
|
|
587
|
-
id:
|
|
588
|
-
provider:
|
|
589
|
-
eventId:
|
|
590
|
-
eventType:
|
|
591
|
-
signatureVerified:
|
|
592
|
-
receivedAtMs:
|
|
593
|
-
processedAtMs:
|
|
943
|
+
var ProviderEventRecordSchema = z4.object({
|
|
944
|
+
id: z4.string().uuid(),
|
|
945
|
+
provider: z4.string(),
|
|
946
|
+
eventId: z4.string(),
|
|
947
|
+
eventType: z4.string(),
|
|
948
|
+
signatureVerified: z4.boolean(),
|
|
949
|
+
receivedAtMs: z4.number().int().nonnegative(),
|
|
950
|
+
processedAtMs: z4.number().int().nonnegative().nullable()
|
|
594
951
|
});
|
|
595
952
|
function createCollectionsClient(opts) {
|
|
596
953
|
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
@@ -1105,7 +1462,7 @@ var FlurClient = class {
|
|
|
1105
1462
|
try {
|
|
1106
1463
|
body = requestSchema ? JSON.stringify(requestSchema.parse(input)) : init2.body;
|
|
1107
1464
|
} catch (err) {
|
|
1108
|
-
if (err instanceof
|
|
1465
|
+
if (err instanceof z5.ZodError) {
|
|
1109
1466
|
throw new FlurError("Invalid request payload", "INVALID_REQUEST", {
|
|
1110
1467
|
details: err.flatten()
|
|
1111
1468
|
});
|
|
@@ -1133,7 +1490,7 @@ var FlurClient = class {
|
|
|
1133
1490
|
try {
|
|
1134
1491
|
return responseSchema.parse(payload);
|
|
1135
1492
|
} catch (err) {
|
|
1136
|
-
if (err instanceof
|
|
1493
|
+
if (err instanceof z5.ZodError) {
|
|
1137
1494
|
throw new FlurError(
|
|
1138
1495
|
"SDK contract validation failed",
|
|
1139
1496
|
"INVALID_REQUEST",
|
|
@@ -1563,64 +1920,113 @@ function constantTimeEqual(a, b) {
|
|
|
1563
1920
|
return diff === 0;
|
|
1564
1921
|
}
|
|
1565
1922
|
|
|
1566
|
-
// src/
|
|
1567
|
-
import {
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1923
|
+
// src/offline/oac.ts
|
|
1924
|
+
import { z as z6 } from "zod";
|
|
1925
|
+
|
|
1926
|
+
// src/crypto/p256-issuer.ts
|
|
1927
|
+
import { p256 } from "@noble/curves/nist";
|
|
1928
|
+
function bytesToBase64(bytes) {
|
|
1929
|
+
if (typeof Buffer !== "undefined") {
|
|
1930
|
+
return Buffer.from(bytes).toString("base64");
|
|
1931
|
+
}
|
|
1932
|
+
let bin = "";
|
|
1933
|
+
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
1934
|
+
return btoa(bin);
|
|
1572
1935
|
}
|
|
1573
|
-
function
|
|
1574
|
-
|
|
1936
|
+
function base64ToBytes(b64) {
|
|
1937
|
+
if (typeof Buffer !== "undefined") {
|
|
1938
|
+
return new Uint8Array(Buffer.from(b64, "base64"));
|
|
1939
|
+
}
|
|
1940
|
+
const bin = atob(b64);
|
|
1941
|
+
const out = new Uint8Array(bin.length);
|
|
1942
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
1943
|
+
return out;
|
|
1575
1944
|
}
|
|
1576
|
-
|
|
1577
|
-
|
|
1945
|
+
var P256_SPKI_HEADER = new Uint8Array([
|
|
1946
|
+
48,
|
|
1947
|
+
89,
|
|
1948
|
+
48,
|
|
1949
|
+
19,
|
|
1950
|
+
6,
|
|
1951
|
+
7,
|
|
1952
|
+
42,
|
|
1953
|
+
134,
|
|
1954
|
+
72,
|
|
1955
|
+
206,
|
|
1956
|
+
61,
|
|
1957
|
+
2,
|
|
1958
|
+
1,
|
|
1959
|
+
6,
|
|
1960
|
+
8,
|
|
1961
|
+
42,
|
|
1962
|
+
134,
|
|
1963
|
+
72,
|
|
1964
|
+
206,
|
|
1965
|
+
61,
|
|
1966
|
+
3,
|
|
1967
|
+
1,
|
|
1968
|
+
7,
|
|
1969
|
+
3,
|
|
1970
|
+
66,
|
|
1971
|
+
0
|
|
1972
|
+
]);
|
|
1973
|
+
function p256SpkiB64ToRaw(spkiB64) {
|
|
1974
|
+
const spki = base64ToBytes(spkiB64);
|
|
1975
|
+
if (spki.length !== P256_SPKI_HEADER.length + 65) {
|
|
1976
|
+
throw new Error("p256: invalid SPKI length");
|
|
1977
|
+
}
|
|
1978
|
+
for (let i = 0; i < P256_SPKI_HEADER.length; i++) {
|
|
1979
|
+
if (spki[i] !== P256_SPKI_HEADER[i]) {
|
|
1980
|
+
throw new Error("p256: invalid SPKI header");
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
return spki.slice(P256_SPKI_HEADER.length);
|
|
1984
|
+
}
|
|
1985
|
+
function signIssuerP256(bytes, issuerPrivateKey) {
|
|
1986
|
+
const sig = p256.sign(bytes, issuerPrivateKey, { prehash: true });
|
|
1987
|
+
return bytesToBase64(sig.toBytes("der"));
|
|
1578
1988
|
}
|
|
1579
|
-
function
|
|
1989
|
+
function verifyIssuerP256(bytes, signatureB64, issuerPublicKeySpkiB64) {
|
|
1580
1990
|
try {
|
|
1581
|
-
|
|
1991
|
+
const pubRaw = p256SpkiB64ToRaw(issuerPublicKeySpkiB64);
|
|
1992
|
+
const sigBytes = base64ToBytes(signatureB64);
|
|
1993
|
+
return p256.verify(sigBytes, bytes, pubRaw, {
|
|
1994
|
+
prehash: true,
|
|
1995
|
+
format: "der"
|
|
1996
|
+
});
|
|
1582
1997
|
} catch {
|
|
1583
1998
|
return false;
|
|
1584
1999
|
}
|
|
1585
2000
|
}
|
|
1586
|
-
function signCanonical(value, privateKey) {
|
|
1587
|
-
return sign(canonicalJSONBytes(value), privateKey);
|
|
1588
|
-
}
|
|
1589
|
-
function verifyCanonical(value, signature, publicKey) {
|
|
1590
|
-
return verify(canonicalJSONBytes(value), signature, publicKey);
|
|
1591
|
-
}
|
|
1592
2001
|
|
|
1593
2002
|
// src/offline/oac.ts
|
|
1594
|
-
import { z as z5 } from "zod";
|
|
1595
2003
|
var OAC_DEFAULT_PER_TX_KOBO = 5e5;
|
|
1596
2004
|
var OAC_DEFAULT_CUMULATIVE_KOBO = 2e6;
|
|
1597
2005
|
var OAC_DEFAULT_VALIDITY_MS = 24 * 60 * 60 * 1e3;
|
|
1598
|
-
var
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
)
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
issuerSig: HexString(64)
|
|
2006
|
+
var Base64Std2 = z6.string().min(16).max(4096).regex(/^[A-Za-z0-9+/]+={0,2}$/, "expected base64 (standard) string");
|
|
2007
|
+
var OACSchema = z6.object({
|
|
2008
|
+
userId: z6.string().min(1),
|
|
2009
|
+
deviceId: z6.string().min(1),
|
|
2010
|
+
/** SubjectPublicKeyInfo DER, base64 (P-256). */
|
|
2011
|
+
devicePublicKey: Base64Std2,
|
|
2012
|
+
perTxCapKobo: z6.number().int().nonnegative(),
|
|
2013
|
+
cumulativeCapKobo: z6.number().int().nonnegative(),
|
|
2014
|
+
validFromMs: z6.number().int().nonnegative(),
|
|
2015
|
+
validUntilMs: z6.number().int().positive(),
|
|
2016
|
+
counterSeed: z6.number().int().nonnegative(),
|
|
2017
|
+
nonce: z6.string().min(1),
|
|
2018
|
+
/** ASN.1 DER ECDSA(SHA-256) signature, base64. */
|
|
2019
|
+
issuerSig: Base64Std2
|
|
1613
2020
|
}).refine((v) => v.validUntilMs > v.validFromMs, {
|
|
1614
2021
|
message: "validUntilMs must be greater than validFromMs"
|
|
1615
2022
|
}).refine((v) => v.perTxCapKobo <= v.cumulativeCapKobo, {
|
|
1616
2023
|
message: "perTxCapKobo must not exceed cumulativeCapKobo"
|
|
1617
2024
|
});
|
|
1618
2025
|
function buildOAC(input) {
|
|
1619
|
-
const devicePublicKey = typeof input.devicePublicKey === "string" ? input.devicePublicKey : bytesToHex(input.devicePublicKey);
|
|
1620
2026
|
return {
|
|
1621
2027
|
userId: input.userId,
|
|
1622
2028
|
deviceId: input.deviceId,
|
|
1623
|
-
devicePublicKey,
|
|
2029
|
+
devicePublicKey: input.devicePublicKey,
|
|
1624
2030
|
perTxCapKobo: input.perTxCapKobo ?? OAC_DEFAULT_PER_TX_KOBO,
|
|
1625
2031
|
cumulativeCapKobo: input.cumulativeCapKobo ?? OAC_DEFAULT_CUMULATIVE_KOBO,
|
|
1626
2032
|
validFromMs: input.validFromMs,
|
|
@@ -1630,36 +2036,25 @@ function buildOAC(input) {
|
|
|
1630
2036
|
};
|
|
1631
2037
|
}
|
|
1632
2038
|
function signOAC(unsigned, issuerPrivateKey) {
|
|
1633
|
-
const issuerSig =
|
|
1634
|
-
|
|
2039
|
+
const issuerSig = signIssuerP256(
|
|
2040
|
+
canonicalJSONBytes(unsigned),
|
|
2041
|
+
issuerPrivateKey
|
|
1635
2042
|
);
|
|
1636
2043
|
return { ...unsigned, issuerSig };
|
|
1637
2044
|
}
|
|
1638
|
-
function verifyOAC(oac,
|
|
2045
|
+
function verifyOAC(oac, issuerPublicKeySpkiB64) {
|
|
1639
2046
|
try {
|
|
1640
2047
|
const parsed = OACSchema.parse(oac);
|
|
1641
2048
|
const { issuerSig, ...unsigned } = parsed;
|
|
1642
|
-
return
|
|
2049
|
+
return verifyIssuerP256(
|
|
1643
2050
|
canonicalJSONBytes(unsigned),
|
|
1644
|
-
|
|
1645
|
-
|
|
2051
|
+
issuerSig,
|
|
2052
|
+
issuerPublicKeySpkiB64
|
|
1646
2053
|
);
|
|
1647
2054
|
} catch {
|
|
1648
2055
|
return false;
|
|
1649
2056
|
}
|
|
1650
2057
|
}
|
|
1651
|
-
function bytesToHex(b) {
|
|
1652
|
-
let s = "";
|
|
1653
|
-
for (let i = 0; i < b.length; i++) s += b[i].toString(16).padStart(2, "0");
|
|
1654
|
-
return s;
|
|
1655
|
-
}
|
|
1656
|
-
function hexToBytes(s) {
|
|
1657
|
-
if (s.length % 2 !== 0) throw new Error("hex: odd length");
|
|
1658
|
-
const out = new Uint8Array(s.length / 2);
|
|
1659
|
-
for (let i = 0; i < out.length; i++)
|
|
1660
|
-
out[i] = parseInt(s.slice(i * 2, i * 2 + 2), 16);
|
|
1661
|
-
return out;
|
|
1662
|
-
}
|
|
1663
2058
|
|
|
1664
2059
|
// src/offline/codec.ts
|
|
1665
2060
|
var ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";
|
|
@@ -1715,20 +2110,20 @@ function decodeBase45(s) {
|
|
|
1715
2110
|
}
|
|
1716
2111
|
|
|
1717
2112
|
// src/offline/messages.ts
|
|
1718
|
-
import { z as
|
|
1719
|
-
var
|
|
1720
|
-
var OfflinePaymentRequestSchema =
|
|
1721
|
-
reference:
|
|
1722
|
-
amountKobo:
|
|
2113
|
+
import { z as z7 } from "zod";
|
|
2114
|
+
var Base64Sig = z7.string().min(16).max(4096).regex(/^[A-Za-z0-9+/]+={0,2}$/, "expected base64 (standard) signature");
|
|
2115
|
+
var OfflinePaymentRequestSchema = z7.object({
|
|
2116
|
+
reference: z7.string().min(1),
|
|
2117
|
+
amountKobo: z7.number().int().positive(),
|
|
1723
2118
|
merchantOAC: OACSchema,
|
|
1724
|
-
expiresAtMs:
|
|
1725
|
-
merchantSig:
|
|
2119
|
+
expiresAtMs: z7.number().int().positive(),
|
|
2120
|
+
merchantSig: Base64Sig
|
|
1726
2121
|
});
|
|
1727
|
-
var OfflinePaymentAuthorizationSchema =
|
|
2122
|
+
var OfflinePaymentAuthorizationSchema = z7.object({
|
|
1728
2123
|
request: OfflinePaymentRequestSchema,
|
|
1729
2124
|
payerOAC: OACSchema,
|
|
1730
|
-
payerCounter:
|
|
1731
|
-
payerSig:
|
|
2125
|
+
payerCounter: z7.number().int().positive(),
|
|
2126
|
+
payerSig: Base64Sig
|
|
1732
2127
|
});
|
|
1733
2128
|
function buildPaymentRequest(input) {
|
|
1734
2129
|
if (!Number.isInteger(input.amountKobo) || input.amountKobo <= 0) {
|
|
@@ -1745,27 +2140,28 @@ function buildPaymentRequest(input) {
|
|
|
1745
2140
|
};
|
|
1746
2141
|
}
|
|
1747
2142
|
function signPaymentRequest(unsigned, merchantDevicePrivateKey) {
|
|
1748
|
-
const merchantSig =
|
|
1749
|
-
|
|
2143
|
+
const merchantSig = signIssuerP256(
|
|
2144
|
+
canonicalJSONBytes(unsigned),
|
|
2145
|
+
merchantDevicePrivateKey
|
|
1750
2146
|
);
|
|
1751
2147
|
return { ...unsigned, merchantSig };
|
|
1752
2148
|
}
|
|
1753
|
-
function verifyPaymentRequest(req,
|
|
2149
|
+
function verifyPaymentRequest(req, issuerPublicKeySpkiB64) {
|
|
1754
2150
|
try {
|
|
1755
2151
|
const parsed = OfflinePaymentRequestSchema.parse(req);
|
|
1756
2152
|
const { issuerSig: merchantOacSig, ...merchantOacUnsigned } = parsed.merchantOAC;
|
|
1757
|
-
if (!
|
|
2153
|
+
if (!verifyIssuerP256(
|
|
1758
2154
|
canonicalJSONBytes(merchantOacUnsigned),
|
|
1759
|
-
|
|
1760
|
-
|
|
2155
|
+
merchantOacSig,
|
|
2156
|
+
issuerPublicKeySpkiB64
|
|
1761
2157
|
)) {
|
|
1762
2158
|
return false;
|
|
1763
2159
|
}
|
|
1764
2160
|
const { merchantSig, ...unsigned } = parsed;
|
|
1765
|
-
return
|
|
2161
|
+
return verifyIssuerP256(
|
|
1766
2162
|
canonicalJSONBytes(unsigned),
|
|
1767
|
-
|
|
1768
|
-
|
|
2163
|
+
merchantSig,
|
|
2164
|
+
parsed.merchantOAC.devicePublicKey
|
|
1769
2165
|
);
|
|
1770
2166
|
} catch {
|
|
1771
2167
|
return false;
|
|
@@ -1785,28 +2181,30 @@ function buildAuthorization(input) {
|
|
|
1785
2181
|
};
|
|
1786
2182
|
}
|
|
1787
2183
|
function signAuthorization(unsigned, payerDevicePrivateKey) {
|
|
1788
|
-
const payerSig =
|
|
1789
|
-
|
|
2184
|
+
const payerSig = signIssuerP256(
|
|
2185
|
+
canonicalJSONBytes(unsigned),
|
|
2186
|
+
payerDevicePrivateKey
|
|
1790
2187
|
);
|
|
1791
2188
|
return { ...unsigned, payerSig };
|
|
1792
2189
|
}
|
|
1793
|
-
function verifyAuthorization(auth,
|
|
2190
|
+
function verifyAuthorization(auth, issuerPublicKeySpkiB64) {
|
|
1794
2191
|
try {
|
|
1795
2192
|
const parsed = OfflinePaymentAuthorizationSchema.parse(auth);
|
|
1796
|
-
if (!verifyPaymentRequest(parsed.request,
|
|
2193
|
+
if (!verifyPaymentRequest(parsed.request, issuerPublicKeySpkiB64))
|
|
2194
|
+
return false;
|
|
1797
2195
|
const { issuerSig: payerOacSig, ...payerOacUnsigned } = parsed.payerOAC;
|
|
1798
|
-
if (!
|
|
2196
|
+
if (!verifyIssuerP256(
|
|
1799
2197
|
canonicalJSONBytes(payerOacUnsigned),
|
|
1800
|
-
|
|
1801
|
-
|
|
2198
|
+
payerOacSig,
|
|
2199
|
+
issuerPublicKeySpkiB64
|
|
1802
2200
|
)) {
|
|
1803
2201
|
return false;
|
|
1804
2202
|
}
|
|
1805
2203
|
const { payerSig, ...unsigned } = parsed;
|
|
1806
|
-
return
|
|
2204
|
+
return verifyIssuerP256(
|
|
1807
2205
|
canonicalJSONBytes(unsigned),
|
|
1808
|
-
|
|
1809
|
-
|
|
2206
|
+
payerSig,
|
|
2207
|
+
parsed.payerOAC.devicePublicKey
|
|
1810
2208
|
);
|
|
1811
2209
|
} catch {
|
|
1812
2210
|
return false;
|
|
@@ -1836,60 +2234,64 @@ function decodeAuthorizationQR(s) {
|
|
|
1836
2234
|
}
|
|
1837
2235
|
|
|
1838
2236
|
// src/offline/settlements.ts
|
|
1839
|
-
import { z as
|
|
2237
|
+
import { z as z8 } from "zod";
|
|
1840
2238
|
import { sha256 } from "@noble/hashes/sha256";
|
|
1841
|
-
import { bytesToHex
|
|
1842
|
-
var OfflineTokenSchema =
|
|
1843
|
-
tokenId:
|
|
1844
|
-
tokenSerial:
|
|
1845
|
-
issuerAccountId:
|
|
1846
|
-
payerUserId:
|
|
1847
|
-
maxAmountKobo:
|
|
1848
|
-
currency:
|
|
1849
|
-
issuedAtMs:
|
|
1850
|
-
expiresAtMs:
|
|
1851
|
-
issuerSig:
|
|
2239
|
+
import { bytesToHex } from "@noble/hashes/utils";
|
|
2240
|
+
var OfflineTokenSchema = z8.object({
|
|
2241
|
+
tokenId: z8.string().uuid(),
|
|
2242
|
+
tokenSerial: z8.string(),
|
|
2243
|
+
issuerAccountId: z8.string().uuid(),
|
|
2244
|
+
payerUserId: z8.string().uuid(),
|
|
2245
|
+
maxAmountKobo: z8.number().int().positive(),
|
|
2246
|
+
currency: z8.string().length(3),
|
|
2247
|
+
issuedAtMs: z8.number().int().nonnegative(),
|
|
2248
|
+
expiresAtMs: z8.number().int().nonnegative(),
|
|
2249
|
+
issuerSig: z8.string()
|
|
1852
2250
|
});
|
|
1853
|
-
var PaymentClaimSchema =
|
|
1854
|
-
encounterId:
|
|
1855
|
-
tokenSerial:
|
|
1856
|
-
payerUserId:
|
|
1857
|
-
payeeUserId:
|
|
1858
|
-
payerNonce:
|
|
1859
|
-
payeeNonce:
|
|
1860
|
-
amountKobo:
|
|
1861
|
-
currency:
|
|
1862
|
-
occurredAtMs:
|
|
1863
|
-
completedAtMs:
|
|
1864
|
-
contextId:
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
2251
|
+
var PaymentClaimSchema = z8.object({
|
|
2252
|
+
encounterId: z8.string().regex(/^[0-9a-f]{64}$/i).optional(),
|
|
2253
|
+
tokenSerial: z8.string(),
|
|
2254
|
+
payerUserId: z8.string().uuid(),
|
|
2255
|
+
payeeUserId: z8.string().uuid(),
|
|
2256
|
+
payerNonce: z8.string(),
|
|
2257
|
+
payeeNonce: z8.string(),
|
|
2258
|
+
amountKobo: z8.number().int().positive(),
|
|
2259
|
+
currency: z8.string().length(3).default("NGN"),
|
|
2260
|
+
occurredAtMs: z8.number().int().nonnegative(),
|
|
2261
|
+
completedAtMs: z8.number().int().nonnegative().optional(),
|
|
2262
|
+
contextId: z8.string().optional(),
|
|
2263
|
+
// Stage 2c: P-256 device keys are now SubjectPublicKeyInfo DER, base64.
|
|
2264
|
+
// Signatures are ASN.1 DER ECDSA(SHA-256), base64. Backwards-incompatible
|
|
2265
|
+
// wire change; the backend has the matching widening in offline-settlements
|
|
2266
|
+
// service + zod schema.
|
|
2267
|
+
payerPubkey: z8.string().min(16).max(4096).regex(/^[A-Za-z0-9+/]+={0,2}$/),
|
|
2268
|
+
payerSignature: z8.string().min(16).max(4096).regex(/^[A-Za-z0-9+/]+={0,2}$/),
|
|
2269
|
+
payeePubkey: z8.string().min(16).max(4096).regex(/^[A-Za-z0-9+/]+={0,2}$/).optional(),
|
|
2270
|
+
payeeSignature: z8.string().min(16).max(4096).regex(/^[A-Za-z0-9+/]+={0,2}$/).optional()
|
|
1869
2271
|
});
|
|
1870
|
-
var SettlementSchema =
|
|
1871
|
-
settlementId:
|
|
1872
|
-
settlementKey:
|
|
1873
|
-
encounterId:
|
|
1874
|
-
issuerAccountId:
|
|
1875
|
-
tokenSerial:
|
|
1876
|
-
payerUserId:
|
|
1877
|
-
payeeUserId:
|
|
1878
|
-
amountKobo:
|
|
1879
|
-
currency:
|
|
1880
|
-
receiptId:
|
|
1881
|
-
status:
|
|
1882
|
-
issuerSig:
|
|
1883
|
-
createdAtMs:
|
|
2272
|
+
var SettlementSchema = z8.object({
|
|
2273
|
+
settlementId: z8.string().uuid(),
|
|
2274
|
+
settlementKey: z8.string().regex(/^[0-9a-f]{64}$/i),
|
|
2275
|
+
encounterId: z8.string().regex(/^[0-9a-f]{64}$/i),
|
|
2276
|
+
issuerAccountId: z8.string().uuid(),
|
|
2277
|
+
tokenSerial: z8.string(),
|
|
2278
|
+
payerUserId: z8.string().uuid(),
|
|
2279
|
+
payeeUserId: z8.string().uuid(),
|
|
2280
|
+
amountKobo: z8.number().int().nonnegative(),
|
|
2281
|
+
currency: z8.string().length(3),
|
|
2282
|
+
receiptId: z8.string().nullable(),
|
|
2283
|
+
status: z8.enum(["SETTLED", "REVIEW", "REJECTED"]),
|
|
2284
|
+
issuerSig: z8.string(),
|
|
2285
|
+
createdAtMs: z8.number().int().nonnegative()
|
|
1884
2286
|
});
|
|
1885
|
-
var SettleResponseSchema =
|
|
2287
|
+
var SettleResponseSchema = z8.object({
|
|
1886
2288
|
settlement: SettlementSchema,
|
|
1887
|
-
encounterId:
|
|
1888
|
-
replayed:
|
|
2289
|
+
encounterId: z8.string().regex(/^[0-9a-f]{64}$/i),
|
|
2290
|
+
replayed: z8.boolean()
|
|
1889
2291
|
});
|
|
1890
2292
|
var ENCOUNTER_DOMAIN = "offline:v1:encounter";
|
|
1891
2293
|
async function sha256Hex(input) {
|
|
1892
|
-
return
|
|
2294
|
+
return bytesToHex(sha256(new TextEncoder().encode(input)));
|
|
1893
2295
|
}
|
|
1894
2296
|
async function computeEncounterId(input) {
|
|
1895
2297
|
return sha256Hex(
|
|
@@ -2059,7 +2461,7 @@ function createHmacFetch(opts) {
|
|
|
2059
2461
|
}
|
|
2060
2462
|
|
|
2061
2463
|
// src/partner/client.ts
|
|
2062
|
-
import { z as
|
|
2464
|
+
import { z as z9 } from "zod";
|
|
2063
2465
|
import { sha256 as sha2563 } from "@noble/hashes/sha256";
|
|
2064
2466
|
import { hmac as hmac2 } from "@noble/hashes/hmac";
|
|
2065
2467
|
import { bytesToHex as bytesToHex4 } from "@noble/hashes/utils";
|
|
@@ -2083,18 +2485,18 @@ var PARTNER_SCOPES = [
|
|
|
2083
2485
|
"partner:payout:write",
|
|
2084
2486
|
"partner:reconciliation:read"
|
|
2085
2487
|
];
|
|
2086
|
-
var ApiCredentialPublicSchema =
|
|
2087
|
-
id:
|
|
2088
|
-
accountId:
|
|
2089
|
-
keyId:
|
|
2090
|
-
scopes:
|
|
2091
|
-
label:
|
|
2092
|
-
createdAtMs:
|
|
2093
|
-
lastUsedAtMs:
|
|
2094
|
-
revokedAtMs:
|
|
2488
|
+
var ApiCredentialPublicSchema = z9.object({
|
|
2489
|
+
id: z9.string().uuid(),
|
|
2490
|
+
accountId: z9.string().uuid(),
|
|
2491
|
+
keyId: z9.string(),
|
|
2492
|
+
scopes: z9.array(z9.enum(PARTNER_SCOPES)),
|
|
2493
|
+
label: z9.string().nullable(),
|
|
2494
|
+
createdAtMs: z9.number().int().nonnegative(),
|
|
2495
|
+
lastUsedAtMs: z9.number().int().nonnegative().nullable(),
|
|
2496
|
+
revokedAtMs: z9.number().int().nonnegative().nullable()
|
|
2095
2497
|
});
|
|
2096
2498
|
var MintedApiCredentialSchema = ApiCredentialPublicSchema.extend({
|
|
2097
|
-
secret:
|
|
2499
|
+
secret: z9.string().min(1)
|
|
2098
2500
|
});
|
|
2099
2501
|
var enc = new TextEncoder();
|
|
2100
2502
|
async function sha256Hex2(input) {
|
|
@@ -2251,8 +2653,8 @@ function createApiCredentialsAdminClient(opts) {
|
|
|
2251
2653
|
}
|
|
2252
2654
|
return parser(raw);
|
|
2253
2655
|
}
|
|
2254
|
-
const listSchema =
|
|
2255
|
-
items:
|
|
2656
|
+
const listSchema = z9.object({
|
|
2657
|
+
items: z9.array(ApiCredentialPublicSchema)
|
|
2256
2658
|
});
|
|
2257
2659
|
return {
|
|
2258
2660
|
list: (accountId) => call(
|
|
@@ -2277,7 +2679,7 @@ function createApiCredentialsAdminClient(opts) {
|
|
|
2277
2679
|
}
|
|
2278
2680
|
|
|
2279
2681
|
// src/passes/pass.ts
|
|
2280
|
-
import { z as
|
|
2682
|
+
import { z as z10 } from "zod";
|
|
2281
2683
|
var PASS_KINDS = [
|
|
2282
2684
|
"ride-ticket",
|
|
2283
2685
|
"transit-pass",
|
|
@@ -2293,42 +2695,39 @@ var PASS_STATES = [
|
|
|
2293
2695
|
"expired",
|
|
2294
2696
|
"revoked"
|
|
2295
2697
|
];
|
|
2296
|
-
var
|
|
2297
|
-
|
|
2298
|
-
`expected ${length}-byte hex string`
|
|
2299
|
-
);
|
|
2300
|
-
var PassMetadataSchema = z9.record(
|
|
2301
|
-
z9.union([z9.string(), z9.number(), z9.boolean(), z9.null()])
|
|
2698
|
+
var PassMetadataSchema = z10.record(
|
|
2699
|
+
z10.union([z10.string(), z10.number(), z10.boolean(), z10.null()])
|
|
2302
2700
|
);
|
|
2303
|
-
var PassSchema =
|
|
2304
|
-
passId:
|
|
2701
|
+
var PassSchema = z10.object({
|
|
2702
|
+
passId: z10.string().min(1),
|
|
2305
2703
|
/** Optional client/template grouping id (server may omit). */
|
|
2306
|
-
templateId:
|
|
2704
|
+
templateId: z10.string().min(1).optional(),
|
|
2307
2705
|
/** Optional human-facing holder identity (server may omit). The cryptographic binding
|
|
2308
2706
|
* is `holderDevicePubkey` below. */
|
|
2309
|
-
holderUserId:
|
|
2310
|
-
kind:
|
|
2311
|
-
issuerId:
|
|
2312
|
-
issuedAtMs:
|
|
2313
|
-
validFromMs:
|
|
2314
|
-
validUntilMs:
|
|
2315
|
-
state:
|
|
2707
|
+
holderUserId: z10.string().min(1).optional(),
|
|
2708
|
+
kind: z10.enum(PASS_KINDS),
|
|
2709
|
+
issuerId: z10.string().min(1),
|
|
2710
|
+
issuedAtMs: z10.number().int().nonnegative(),
|
|
2711
|
+
validFromMs: z10.number().int().nonnegative(),
|
|
2712
|
+
validUntilMs: z10.number().int().positive(),
|
|
2713
|
+
state: z10.enum(PASS_STATES),
|
|
2316
2714
|
metadata: PassMetadataSchema,
|
|
2317
|
-
nonce:
|
|
2715
|
+
nonce: z10.string().min(1),
|
|
2318
2716
|
/** Device id this pass is bound to (FK to backend `device_keys`). */
|
|
2319
|
-
holderDeviceId:
|
|
2320
|
-
/**
|
|
2321
|
-
* is verified against this key — it is the security-critical binding. */
|
|
2322
|
-
holderDevicePubkey:
|
|
2717
|
+
holderDeviceId: z10.string().min(1),
|
|
2718
|
+
/** SubjectPublicKeyInfo DER (P-256) of the bound device, base64. The redemption
|
|
2719
|
+
* signature is verified against this key — it is the security-critical binding. */
|
|
2720
|
+
holderDevicePubkey: z10.string().min(64).max(4096).regex(/^[A-Za-z0-9+/]+={0,2}$/),
|
|
2323
2721
|
/** Optional fixed amount for monetary passes (vouchers, gift cards) in kobo. */
|
|
2324
|
-
amountKobo:
|
|
2722
|
+
amountKobo: z10.number().int().nonnegative().optional(),
|
|
2325
2723
|
/** ISO-4217-ish currency code; required on the wire. SDK builders default to NGN. */
|
|
2326
|
-
currency:
|
|
2724
|
+
currency: z10.string().min(3).max(8),
|
|
2327
2725
|
/** Monotonic redemption counter floor. Redemption.counter MUST be > counterSeed. */
|
|
2328
|
-
counterSeed:
|
|
2726
|
+
counterSeed: z10.number().int().nonnegative(),
|
|
2329
2727
|
/** Optional cumulative spend cap in kobo across all redemptions of this pass. */
|
|
2330
|
-
cumulativeCapKobo:
|
|
2331
|
-
|
|
2728
|
+
cumulativeCapKobo: z10.number().int().nonnegative().optional(),
|
|
2729
|
+
/** ASN.1 DER ECDSA P-256 signature, base64. */
|
|
2730
|
+
issuerSig: z10.string().min(64).max(2048).regex(/^[A-Za-z0-9+/]+={0,2}$/)
|
|
2332
2731
|
}).refine((v) => v.validUntilMs > v.validFromMs, {
|
|
2333
2732
|
message: "validUntilMs must be greater than validFromMs"
|
|
2334
2733
|
});
|
|
@@ -2361,19 +2760,20 @@ function buildPass(input) {
|
|
|
2361
2760
|
return out;
|
|
2362
2761
|
}
|
|
2363
2762
|
function signPass(unsigned, issuerPrivateKey) {
|
|
2364
|
-
const issuerSig =
|
|
2365
|
-
|
|
2763
|
+
const issuerSig = signIssuerP256(
|
|
2764
|
+
canonicalJSONBytes(unsigned),
|
|
2765
|
+
issuerPrivateKey
|
|
2366
2766
|
);
|
|
2367
2767
|
return { ...unsigned, issuerSig };
|
|
2368
2768
|
}
|
|
2369
|
-
function verifyPass(pass,
|
|
2769
|
+
function verifyPass(pass, issuerPublicKeySpkiB64) {
|
|
2370
2770
|
try {
|
|
2371
2771
|
const parsed = PassSchema.parse(pass);
|
|
2372
2772
|
const { issuerSig, ...unsigned } = parsed;
|
|
2373
|
-
return
|
|
2773
|
+
return verifyIssuerP256(
|
|
2374
2774
|
canonicalJSONBytes(unsigned),
|
|
2375
|
-
|
|
2376
|
-
|
|
2775
|
+
issuerSig,
|
|
2776
|
+
issuerPublicKeySpkiB64
|
|
2377
2777
|
);
|
|
2378
2778
|
} catch {
|
|
2379
2779
|
return false;
|
|
@@ -2384,19 +2784,20 @@ function isPassWithinValidity(pass, nowMs) {
|
|
|
2384
2784
|
}
|
|
2385
2785
|
|
|
2386
2786
|
// src/passes/redemption.ts
|
|
2387
|
-
import { z as
|
|
2388
|
-
var
|
|
2389
|
-
var RedemptionSchema =
|
|
2787
|
+
import { z as z11 } from "zod";
|
|
2788
|
+
var Base64Std3 = z11.string().min(16).max(2048).regex(/^[A-Za-z0-9+/]+={0,2}$/, "expected base64 (std)");
|
|
2789
|
+
var RedemptionSchema = z11.object({
|
|
2390
2790
|
pass: PassSchema,
|
|
2391
|
-
redeemerId:
|
|
2392
|
-
redeemedAtMs:
|
|
2791
|
+
redeemerId: z11.string().min(1),
|
|
2792
|
+
redeemedAtMs: z11.number().int().nonnegative(),
|
|
2393
2793
|
/** Strictly monotonic counter scoped to a single pass. Must be > pass.counterSeed
|
|
2394
2794
|
* and > the redeemer's lastSeenCounter for this pass. */
|
|
2395
|
-
counter:
|
|
2795
|
+
counter: z11.number().int().positive(),
|
|
2396
2796
|
/** Amount being redeemed in kobo (0 for non-monetary passes like ride tickets). */
|
|
2397
|
-
amountKobo:
|
|
2398
|
-
nonce:
|
|
2399
|
-
|
|
2797
|
+
amountKobo: z11.number().int().nonnegative(),
|
|
2798
|
+
nonce: z11.string().min(1),
|
|
2799
|
+
/** ASN.1 DER ECDSA P-256 signature over canonicalJSONBytes(unsigned), base64. */
|
|
2800
|
+
holderSig: Base64Std3
|
|
2400
2801
|
});
|
|
2401
2802
|
var REDEEMABLE_STATES = /* @__PURE__ */ new Set(["issued", "active"]);
|
|
2402
2803
|
function buildRedemption(input) {
|
|
@@ -2450,74 +2851,68 @@ function buildRedemption(input) {
|
|
|
2450
2851
|
};
|
|
2451
2852
|
}
|
|
2452
2853
|
function signRedemption(unsigned, holderDevicePrivateKey) {
|
|
2453
|
-
const holderSig =
|
|
2454
|
-
|
|
2854
|
+
const holderSig = signIssuerP256(
|
|
2855
|
+
canonicalJSONBytes(unsigned),
|
|
2856
|
+
holderDevicePrivateKey
|
|
2455
2857
|
);
|
|
2456
2858
|
return { ...unsigned, holderSig };
|
|
2457
2859
|
}
|
|
2458
|
-
function verifyRedemption(r,
|
|
2860
|
+
function verifyRedemption(r, issuerPublicKeySpkiB64) {
|
|
2459
2861
|
try {
|
|
2460
2862
|
const parsed = RedemptionSchema.parse(r);
|
|
2461
2863
|
if (parsed.counter <= parsed.pass.counterSeed) return false;
|
|
2462
2864
|
const { issuerSig, ...passUnsigned } = parsed.pass;
|
|
2463
|
-
if (!
|
|
2865
|
+
if (!verifyIssuerP256(
|
|
2464
2866
|
canonicalJSONBytes(passUnsigned),
|
|
2465
|
-
|
|
2466
|
-
|
|
2867
|
+
issuerSig,
|
|
2868
|
+
issuerPublicKeySpkiB64
|
|
2467
2869
|
)) {
|
|
2468
2870
|
return false;
|
|
2469
2871
|
}
|
|
2470
|
-
const
|
|
2471
|
-
if (typeof
|
|
2872
|
+
const holderPub = parsed.pass.holderDevicePubkey;
|
|
2873
|
+
if (typeof holderPub !== "string") return false;
|
|
2472
2874
|
const { holderSig, ...unsigned } = parsed;
|
|
2473
|
-
return
|
|
2474
|
-
canonicalJSONBytes(unsigned),
|
|
2475
|
-
hexToBytes(holderSig),
|
|
2476
|
-
hexToBytes(holderHex)
|
|
2477
|
-
);
|
|
2875
|
+
return verifyIssuerP256(canonicalJSONBytes(unsigned), holderSig, holderPub);
|
|
2478
2876
|
} catch {
|
|
2479
2877
|
return false;
|
|
2480
2878
|
}
|
|
2481
2879
|
}
|
|
2482
2880
|
|
|
2483
2881
|
// src/receipts/receipt.ts
|
|
2484
|
-
import { z as
|
|
2882
|
+
import { z as z12 } from "zod";
|
|
2485
2883
|
var RECEIPT_CHANNELS = ["cash", "pass"];
|
|
2486
2884
|
var RECEIPT_KINDS = RECEIPT_CHANNELS;
|
|
2487
|
-
var
|
|
2488
|
-
|
|
2489
|
-
`expected ${length}-byte hex string`
|
|
2490
|
-
);
|
|
2491
|
-
var ReceiptPayloadSchema = z11.record(
|
|
2492
|
-
z11.union([z11.string(), z11.number(), z11.boolean(), z11.null()])
|
|
2885
|
+
var ReceiptPayloadSchema = z12.record(
|
|
2886
|
+
z12.union([z12.string(), z12.number(), z12.boolean(), z12.null()])
|
|
2493
2887
|
);
|
|
2494
|
-
var ReceiptSchema =
|
|
2495
|
-
receiptId:
|
|
2496
|
-
channel:
|
|
2888
|
+
var ReceiptSchema = z12.object({
|
|
2889
|
+
receiptId: z12.string().min(1),
|
|
2890
|
+
channel: z12.enum(RECEIPT_CHANNELS),
|
|
2497
2891
|
/** Cash-channel: send_intents.id. Required when channel === 'cash'. */
|
|
2498
|
-
intentId:
|
|
2892
|
+
intentId: z12.string().min(1).optional(),
|
|
2499
2893
|
/** Pass-channel: pass_redemptions.id. Required when channel === 'pass'. */
|
|
2500
|
-
passRedemptionId:
|
|
2501
|
-
payerUserId:
|
|
2502
|
-
payeeUserId:
|
|
2503
|
-
amountKobo:
|
|
2504
|
-
currency:
|
|
2505
|
-
issuedAtMs:
|
|
2506
|
-
issuerId:
|
|
2894
|
+
passRedemptionId: z12.string().min(1).optional(),
|
|
2895
|
+
payerUserId: z12.string().min(1),
|
|
2896
|
+
payeeUserId: z12.string().min(1),
|
|
2897
|
+
amountKobo: z12.number().int().nonnegative(),
|
|
2898
|
+
currency: z12.string().min(3).max(8),
|
|
2899
|
+
issuedAtMs: z12.number().int().nonnegative(),
|
|
2900
|
+
issuerId: z12.string().min(1),
|
|
2507
2901
|
payload: ReceiptPayloadSchema,
|
|
2508
|
-
|
|
2902
|
+
/** ASN.1 DER ECDSA P-256 signature, base64. */
|
|
2903
|
+
issuerSig: z12.string().min(64).max(2048).regex(/^[A-Za-z0-9+/]+={0,2}$/)
|
|
2509
2904
|
}).superRefine((v, ctx) => {
|
|
2510
2905
|
if (v.channel === "cash") {
|
|
2511
2906
|
if (!v.intentId) {
|
|
2512
2907
|
ctx.addIssue({
|
|
2513
|
-
code:
|
|
2908
|
+
code: z12.ZodIssueCode.custom,
|
|
2514
2909
|
message: "cash receipts require intentId",
|
|
2515
2910
|
path: ["intentId"]
|
|
2516
2911
|
});
|
|
2517
2912
|
}
|
|
2518
2913
|
if (v.passRedemptionId) {
|
|
2519
2914
|
ctx.addIssue({
|
|
2520
|
-
code:
|
|
2915
|
+
code: z12.ZodIssueCode.custom,
|
|
2521
2916
|
message: "cash receipts must not carry passRedemptionId",
|
|
2522
2917
|
path: ["passRedemptionId"]
|
|
2523
2918
|
});
|
|
@@ -2525,14 +2920,14 @@ var ReceiptSchema = z11.object({
|
|
|
2525
2920
|
} else if (v.channel === "pass") {
|
|
2526
2921
|
if (!v.passRedemptionId) {
|
|
2527
2922
|
ctx.addIssue({
|
|
2528
|
-
code:
|
|
2923
|
+
code: z12.ZodIssueCode.custom,
|
|
2529
2924
|
message: "pass receipts require passRedemptionId",
|
|
2530
2925
|
path: ["passRedemptionId"]
|
|
2531
2926
|
});
|
|
2532
2927
|
}
|
|
2533
2928
|
if (v.intentId) {
|
|
2534
2929
|
ctx.addIssue({
|
|
2535
|
-
code:
|
|
2930
|
+
code: z12.ZodIssueCode.custom,
|
|
2536
2931
|
message: "pass receipts must not carry intentId",
|
|
2537
2932
|
path: ["intentId"]
|
|
2538
2933
|
});
|
|
@@ -2571,19 +2966,20 @@ function buildReceipt(input) {
|
|
|
2571
2966
|
return out;
|
|
2572
2967
|
}
|
|
2573
2968
|
function signReceipt(unsigned, issuerPrivateKey) {
|
|
2574
|
-
const issuerSig =
|
|
2575
|
-
|
|
2969
|
+
const issuerSig = signIssuerP256(
|
|
2970
|
+
canonicalJSONBytes(unsigned),
|
|
2971
|
+
issuerPrivateKey
|
|
2576
2972
|
);
|
|
2577
2973
|
return { ...unsigned, issuerSig };
|
|
2578
2974
|
}
|
|
2579
|
-
function verifyReceipt(r,
|
|
2975
|
+
function verifyReceipt(r, issuerPublicKeySpkiB64) {
|
|
2580
2976
|
try {
|
|
2581
2977
|
const parsed = ReceiptSchema.parse(r);
|
|
2582
2978
|
const { issuerSig, ...unsigned } = parsed;
|
|
2583
|
-
return
|
|
2979
|
+
return verifyIssuerP256(
|
|
2584
2980
|
canonicalJSONBytes(unsigned),
|
|
2585
|
-
|
|
2586
|
-
|
|
2981
|
+
issuerSig,
|
|
2982
|
+
issuerPublicKeySpkiB64
|
|
2587
2983
|
);
|
|
2588
2984
|
} catch {
|
|
2589
2985
|
return false;
|
|
@@ -2833,23 +3229,23 @@ function init(opts) {
|
|
|
2833
3229
|
}
|
|
2834
3230
|
|
|
2835
3231
|
// src/accounts/client.ts
|
|
2836
|
-
import { z as
|
|
3232
|
+
import { z as z13 } from "zod";
|
|
2837
3233
|
var ACCOUNT_TYPES = ["personal", "business", "partner"];
|
|
2838
3234
|
var ACCOUNT_STATUSES = ["active", "suspended", "closed"];
|
|
2839
3235
|
var MEMBERSHIP_ROLES = ["owner", "admin", "driver", "staff"];
|
|
2840
|
-
var AccountSchema =
|
|
2841
|
-
accountId:
|
|
2842
|
-
type:
|
|
2843
|
-
displayName:
|
|
2844
|
-
status:
|
|
2845
|
-
ownerUserId:
|
|
2846
|
-
createdAtMs:
|
|
3236
|
+
var AccountSchema = z13.object({
|
|
3237
|
+
accountId: z13.string().uuid(),
|
|
3238
|
+
type: z13.enum(ACCOUNT_TYPES),
|
|
3239
|
+
displayName: z13.string().min(1),
|
|
3240
|
+
status: z13.enum(ACCOUNT_STATUSES),
|
|
3241
|
+
ownerUserId: z13.string().uuid().nullable(),
|
|
3242
|
+
createdAtMs: z13.number().int().nonnegative()
|
|
2847
3243
|
});
|
|
2848
|
-
var AccountMembershipSchema =
|
|
2849
|
-
accountId:
|
|
2850
|
-
userId:
|
|
2851
|
-
role:
|
|
2852
|
-
createdAtMs:
|
|
3244
|
+
var AccountMembershipSchema = z13.object({
|
|
3245
|
+
accountId: z13.string().uuid(),
|
|
3246
|
+
userId: z13.string().uuid(),
|
|
3247
|
+
role: z13.enum(MEMBERSHIP_ROLES),
|
|
3248
|
+
createdAtMs: z13.number().int().nonnegative()
|
|
2853
3249
|
});
|
|
2854
3250
|
function createAccountsClient(opts) {
|
|
2855
3251
|
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
@@ -2882,9 +3278,9 @@ function createAccountsClient(opts) {
|
|
|
2882
3278
|
}
|
|
2883
3279
|
return parser(raw);
|
|
2884
3280
|
}
|
|
2885
|
-
const itemsSchema =
|
|
2886
|
-
const memberItemsSchema =
|
|
2887
|
-
items:
|
|
3281
|
+
const itemsSchema = z13.object({ items: z13.array(AccountSchema) });
|
|
3282
|
+
const memberItemsSchema = z13.object({
|
|
3283
|
+
items: z13.array(AccountMembershipSchema)
|
|
2888
3284
|
});
|
|
2889
3285
|
return {
|
|
2890
3286
|
listMyAccounts: () => call(
|
|
@@ -2915,345 +3311,8 @@ function createAccountsClient(opts) {
|
|
|
2915
3311
|
};
|
|
2916
3312
|
}
|
|
2917
3313
|
|
|
2918
|
-
// src/me-offline/client.ts
|
|
2919
|
-
import { z as z13 } from "zod";
|
|
2920
|
-
var Hex64 = z13.string().regex(/^[0-9a-f]{64}$/i);
|
|
2921
|
-
var HexAny = z13.string().regex(/^[0-9a-f]+$/i);
|
|
2922
|
-
var Sha256Hex = z13.string().regex(/^[0-9a-f]{64}$/i);
|
|
2923
|
-
var Base64Std = z13.string().regex(/^[A-Za-z0-9+/]+={0,2}$/);
|
|
2924
|
-
var RegisterDeviceKeyInputSchema = z13.object({
|
|
2925
|
-
deviceId: z13.string().min(1).max(128),
|
|
2926
|
-
publicKeyHex: Hex64
|
|
2927
|
-
});
|
|
2928
|
-
var AttestationSecurityLevelSchema = z13.enum([
|
|
2929
|
-
"STRONGBOX",
|
|
2930
|
-
"TEE",
|
|
2931
|
-
"SECURE_ENCLAVE",
|
|
2932
|
-
"SOFTWARE"
|
|
2933
|
-
]);
|
|
2934
|
-
var DeviceKeyAlgSchema = z13.enum(["ed25519", "p256"]);
|
|
2935
|
-
var RegisterDeviceKeyP256InputSchema = z13.object({
|
|
2936
|
-
deviceId: z13.string().min(1).max(128),
|
|
2937
|
-
/** P-256 SubjectPublicKeyInfo DER, base64. */
|
|
2938
|
-
publicKeySpkiB64: Base64Std.min(64).max(4096),
|
|
2939
|
-
/** Base64 of the server-issued enrollment challenge string. */
|
|
2940
|
-
challengeB64: Base64Std.min(8).max(1024),
|
|
2941
|
-
/** iOS App Attest payload or Android X.509 Key Attestation chain. */
|
|
2942
|
-
attestationChainB64: z13.array(Base64Std.min(16).max(16384)).min(1).max(16),
|
|
2943
|
-
securityLevel: AttestationSecurityLevelSchema
|
|
2944
|
-
});
|
|
2945
|
-
var P256EnrollmentChallengeInputSchema = z13.object({
|
|
2946
|
-
deviceId: z13.string().min(1).max(128)
|
|
2947
|
-
});
|
|
2948
|
-
var P256EnrollmentChallengeResultSchema = z13.object({
|
|
2949
|
-
challenge: z13.string().min(16),
|
|
2950
|
-
expiresAtMs: z13.number().int().positive()
|
|
2951
|
-
});
|
|
2952
|
-
var DeviceKeyRecordSchema = z13.object({
|
|
2953
|
-
id: z13.string().uuid(),
|
|
2954
|
-
userId: z13.string().uuid(),
|
|
2955
|
-
deviceId: z13.string(),
|
|
2956
|
-
alg: DeviceKeyAlgSchema.default("ed25519"),
|
|
2957
|
-
publicKeyHex: Hex64.nullable().default(null),
|
|
2958
|
-
publicKeySpkiB64: Base64Std.nullable().default(null),
|
|
2959
|
-
securityLevel: AttestationSecurityLevelSchema.nullable().default(null),
|
|
2960
|
-
hardwareBacked: z13.boolean().default(false),
|
|
2961
|
-
attestedAtMs: z13.number().int().nonnegative().nullable().default(null),
|
|
2962
|
-
createdAtMs: z13.number().int().nonnegative(),
|
|
2963
|
-
revokedAtMs: z13.number().int().nonnegative().nullable()
|
|
2964
|
-
});
|
|
2965
|
-
var ConsumerOACSchema = z13.object({
|
|
2966
|
-
oacId: z13.string().uuid(),
|
|
2967
|
-
issuerId: z13.string().min(1).max(64),
|
|
2968
|
-
userId: z13.string().uuid(),
|
|
2969
|
-
deviceId: z13.string().min(1).max(128),
|
|
2970
|
-
alg: z13.enum(["ed25519", "p256"]).optional(),
|
|
2971
|
-
devicePubkeyHex: Hex64.optional(),
|
|
2972
|
-
devicePubkeySpkiB64: Base64Std.min(64).max(4096).optional(),
|
|
2973
|
-
perTxCapKobo: z13.number().int().positive(),
|
|
2974
|
-
cumulativeCapKobo: z13.number().int().positive(),
|
|
2975
|
-
currency: z13.string().length(3),
|
|
2976
|
-
validFromMs: z13.number().int().nonnegative(),
|
|
2977
|
-
validUntilMs: z13.number().int().nonnegative(),
|
|
2978
|
-
counterSeed: z13.number().int().nonnegative(),
|
|
2979
|
-
issuedAtMs: z13.number().int().nonnegative()
|
|
2980
|
-
}).refine(
|
|
2981
|
-
(o) => {
|
|
2982
|
-
const alg = o.alg ?? "ed25519";
|
|
2983
|
-
if (alg === "ed25519") {
|
|
2984
|
-
return Boolean(o.devicePubkeyHex) && !o.devicePubkeySpkiB64;
|
|
2985
|
-
}
|
|
2986
|
-
return Boolean(o.devicePubkeySpkiB64) && !o.devicePubkeyHex;
|
|
2987
|
-
},
|
|
2988
|
-
{ message: "OAC device pubkey shape must match alg" }
|
|
2989
|
-
);
|
|
2990
|
-
var SignedConsumerOACSchema = z13.object({
|
|
2991
|
-
oac: ConsumerOACSchema,
|
|
2992
|
-
issuerSig: HexAny,
|
|
2993
|
-
issuerPublicKeyHex: Hex64
|
|
2994
|
-
});
|
|
2995
|
-
var OACRecordSchema = SignedConsumerOACSchema.extend({
|
|
2996
|
-
currentOfflineSpentKobo: z13.number().int().nonnegative(),
|
|
2997
|
-
status: z13.enum([
|
|
2998
|
-
"active",
|
|
2999
|
-
"superseded",
|
|
3000
|
-
"expired",
|
|
3001
|
-
"revoked",
|
|
3002
|
-
"disabling",
|
|
3003
|
-
"draining",
|
|
3004
|
-
"closed"
|
|
3005
|
-
]),
|
|
3006
|
-
supersededAtMs: z13.number().int().nonnegative().nullable(),
|
|
3007
|
-
revokedAtMs: z13.number().int().nonnegative().nullable(),
|
|
3008
|
-
holdId: z13.string().uuid().nullable().optional()
|
|
3009
|
-
});
|
|
3010
|
-
var IssueOACInputSchema = z13.object({
|
|
3011
|
-
deviceId: z13.string().min(1).max(128),
|
|
3012
|
-
perTxCapKobo: z13.number().int().positive().optional(),
|
|
3013
|
-
cumulativeCapKobo: z13.number().int().positive().optional(),
|
|
3014
|
-
ttlMs: z13.number().int().min(6e4).max(1e3 * 60 * 60 * 24 * 7).optional(),
|
|
3015
|
-
spendableOnlineKobo: z13.number().int().nonnegative().optional()
|
|
3016
|
-
});
|
|
3017
|
-
var EnableOfflineInputSchema = z13.object({
|
|
3018
|
-
deviceId: z13.string().min(1).max(128),
|
|
3019
|
-
amountKobo: z13.number().int().positive(),
|
|
3020
|
-
perTxCapKobo: z13.number().int().positive().optional(),
|
|
3021
|
-
ttlMs: z13.number().int().min(6e4).max(1e3 * 60 * 60 * 24 * 7).optional(),
|
|
3022
|
-
installId: z13.string().min(1).max(128),
|
|
3023
|
-
partnerId: z13.string().min(1).max(64).optional()
|
|
3024
|
-
});
|
|
3025
|
-
var ProvisionOfflineAllowanceInputSchema = EnableOfflineInputSchema;
|
|
3026
|
-
var DisableOfflineInputSchema = z13.object({
|
|
3027
|
-
deviceId: z13.string().min(1).max(128),
|
|
3028
|
-
installId: z13.string().min(1).max(128).optional(),
|
|
3029
|
-
claims: z13.array(z13.unknown()).max(256).optional()
|
|
3030
|
-
});
|
|
3031
|
-
var OfflineHoldRecordSchema = z13.object({
|
|
3032
|
-
holdId: z13.string().uuid(),
|
|
3033
|
-
userId: z13.string().uuid(),
|
|
3034
|
-
deviceId: z13.string(),
|
|
3035
|
-
partnerId: z13.string(),
|
|
3036
|
-
adapterKind: z13.string(),
|
|
3037
|
-
externalHoldRef: z13.string().nullable(),
|
|
3038
|
-
amountKobo: z13.number().int().nonnegative(),
|
|
3039
|
-
capturedKobo: z13.number().int().nonnegative(),
|
|
3040
|
-
releasedKobo: z13.number().int().nonnegative(),
|
|
3041
|
-
remainingKobo: z13.number().int().nonnegative(),
|
|
3042
|
-
currency: z13.string().length(3),
|
|
3043
|
-
status: z13.enum([
|
|
3044
|
-
"placing",
|
|
3045
|
-
"active",
|
|
3046
|
-
"disabling",
|
|
3047
|
-
"draining",
|
|
3048
|
-
"closed",
|
|
3049
|
-
"revoked",
|
|
3050
|
-
"failed"
|
|
3051
|
-
]),
|
|
3052
|
-
installId: z13.string().nullable(),
|
|
3053
|
-
drainDeadlineMs: z13.number().int().nonnegative(),
|
|
3054
|
-
disableRequestedAtMs: z13.number().int().nonnegative().nullable(),
|
|
3055
|
-
createdAtMs: z13.number().int().nonnegative(),
|
|
3056
|
-
closedAtMs: z13.number().int().nonnegative().nullable(),
|
|
3057
|
-
isTrusted: z13.boolean().optional()
|
|
3058
|
-
});
|
|
3059
|
-
var EnableOfflineResultSchema = z13.object({
|
|
3060
|
-
hold: OfflineHoldRecordSchema,
|
|
3061
|
-
oac: OACRecordSchema
|
|
3062
|
-
});
|
|
3063
|
-
var ProvisionOfflineAllowanceResultSchema = EnableOfflineResultSchema;
|
|
3064
|
-
var DisableOfflineResultSchema = z13.object({
|
|
3065
|
-
hold: OfflineHoldRecordSchema,
|
|
3066
|
-
trusted: z13.boolean(),
|
|
3067
|
-
settledClaims: z13.number().int().nonnegative()
|
|
3068
|
-
});
|
|
3069
|
-
var OfflineStatusResultSchema = z13.object({
|
|
3070
|
-
hold: OfflineHoldRecordSchema.nullable(),
|
|
3071
|
-
active: OACRecordSchema.nullable()
|
|
3072
|
-
});
|
|
3073
|
-
var OfflineStateResultSchema = z13.object({
|
|
3074
|
-
active: OACRecordSchema.nullable()
|
|
3075
|
-
});
|
|
3076
|
-
var ClaimAlgSchema = z13.enum(["ed25519", "p256"]);
|
|
3077
|
-
var ConsumerPaymentClaimSchema = z13.object({
|
|
3078
|
-
/** Algorithm discriminator. Omit / 'ed25519' for V1 clients. */
|
|
3079
|
-
alg: ClaimAlgSchema.optional(),
|
|
3080
|
-
oacId: z13.string().uuid(),
|
|
3081
|
-
encounterId: Sha256Hex.optional(),
|
|
3082
|
-
payerUserId: z13.string().uuid(),
|
|
3083
|
-
payeeUserId: z13.string().uuid(),
|
|
3084
|
-
payerDeviceId: z13.string().min(1).max(128),
|
|
3085
|
-
payerNonce: z13.string().min(8).max(128),
|
|
3086
|
-
payeeNonce: z13.string().min(8).max(128),
|
|
3087
|
-
amountKobo: z13.number().int().positive(),
|
|
3088
|
-
currency: z13.string().length(3).default("NGN"),
|
|
3089
|
-
occurredAtMs: z13.number().int().nonnegative(),
|
|
3090
|
-
completedAtMs: z13.number().int().nonnegative().optional(),
|
|
3091
|
-
contextId: z13.string().max(128).optional(),
|
|
3092
|
-
// ed25519 path
|
|
3093
|
-
payerPubkeyHex: Hex64.optional(),
|
|
3094
|
-
payerSignature: HexAny.optional(),
|
|
3095
|
-
payeePubkeyHex: Hex64.optional(),
|
|
3096
|
-
payeeSignature: HexAny.optional(),
|
|
3097
|
-
// p256 path
|
|
3098
|
-
payerPubkeySpkiB64: z13.string().min(64).max(4096).optional(),
|
|
3099
|
-
payerSignatureDerB64: z13.string().min(16).max(2048).optional(),
|
|
3100
|
-
payeePubkeySpkiB64: z13.string().min(64).max(4096).optional(),
|
|
3101
|
-
payeeSignatureDerB64: z13.string().min(16).max(2048).optional()
|
|
3102
|
-
}).refine(
|
|
3103
|
-
(c) => {
|
|
3104
|
-
const alg = c.alg ?? "ed25519";
|
|
3105
|
-
if (alg === "ed25519") {
|
|
3106
|
-
return Boolean(c.payerPubkeyHex) && Boolean(c.payerSignature);
|
|
3107
|
-
}
|
|
3108
|
-
return Boolean(c.payerPubkeySpkiB64) && Boolean(c.payerSignatureDerB64);
|
|
3109
|
-
},
|
|
3110
|
-
{
|
|
3111
|
-
message: "payer key/signature fields must match alg (ed25519: hex; p256: SPKI+DER b64)"
|
|
3112
|
-
}
|
|
3113
|
-
);
|
|
3114
|
-
var ConsumerSettlementSchema = z13.object({
|
|
3115
|
-
settlementId: z13.string().uuid(),
|
|
3116
|
-
settlementKey: Sha256Hex,
|
|
3117
|
-
encounterId: Sha256Hex,
|
|
3118
|
-
oacId: z13.string().uuid(),
|
|
3119
|
-
payerUserId: z13.string().uuid(),
|
|
3120
|
-
payeeUserId: z13.string().uuid(),
|
|
3121
|
-
amountKobo: z13.number().int().positive(),
|
|
3122
|
-
currency: z13.string().length(3),
|
|
3123
|
-
status: z13.enum(["SETTLED", "REVIEW"]),
|
|
3124
|
-
reviewReason: z13.string().nullable(),
|
|
3125
|
-
ledgerRef: z13.string().nullable(),
|
|
3126
|
-
issuerSig: HexAny,
|
|
3127
|
-
createdAtMs: z13.number().int().nonnegative()
|
|
3128
|
-
});
|
|
3129
|
-
var ConsumerSettleResultSchema = z13.object({
|
|
3130
|
-
settlement: ConsumerSettlementSchema,
|
|
3131
|
-
encounterId: Sha256Hex,
|
|
3132
|
-
replayed: z13.boolean()
|
|
3133
|
-
});
|
|
3134
|
-
var RevokeDeviceKeyInputSchema = z13.object({
|
|
3135
|
-
deviceId: z13.string().min(1).max(128),
|
|
3136
|
-
/** Step-up token from /api/v1/auth/send/verify with purpose='offline_revoke'. */
|
|
3137
|
-
sendAuthToken: z13.string().min(16)
|
|
3138
|
-
});
|
|
3139
|
-
function createMeOfflineClient(opts) {
|
|
3140
|
-
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
3141
|
-
if (!fetchImpl) {
|
|
3142
|
-
throw new Error("createMeOfflineClient: no fetch implementation available");
|
|
3143
|
-
}
|
|
3144
|
-
const baseUrl = opts.baseUrl.replace(/\/$/, "");
|
|
3145
|
-
async function call(method, path, body, parser) {
|
|
3146
|
-
const init2 = {
|
|
3147
|
-
method,
|
|
3148
|
-
headers: {
|
|
3149
|
-
"content-type": "application/json",
|
|
3150
|
-
accept: "application/json"
|
|
3151
|
-
}
|
|
3152
|
-
};
|
|
3153
|
-
if (body !== void 0) init2.body = JSON.stringify(body);
|
|
3154
|
-
const resp = await fetchImpl(`${baseUrl}${path}`, init2);
|
|
3155
|
-
const text = await resp.text();
|
|
3156
|
-
let raw = void 0;
|
|
3157
|
-
if (text) {
|
|
3158
|
-
try {
|
|
3159
|
-
raw = JSON.parse(text);
|
|
3160
|
-
} catch {
|
|
3161
|
-
}
|
|
3162
|
-
}
|
|
3163
|
-
if (!resp.ok) {
|
|
3164
|
-
const code = raw && typeof raw === "object" && "code" in raw && typeof raw.code === "string" ? raw.code : `http_${resp.status}`;
|
|
3165
|
-
const message = raw && typeof raw === "object" && "message" in raw && typeof raw.message === "string" ? raw.message : `request failed with status ${resp.status}`;
|
|
3166
|
-
throw new FlurApiError(resp.status, code, message, raw);
|
|
3167
|
-
}
|
|
3168
|
-
return parser(raw);
|
|
3169
|
-
}
|
|
3170
|
-
const deviceKeyItems = z13.object({ items: z13.array(DeviceKeyRecordSchema) });
|
|
3171
|
-
return {
|
|
3172
|
-
registerDeviceKey: (input) => call(
|
|
3173
|
-
"POST",
|
|
3174
|
-
"/v1/me/offline/keys",
|
|
3175
|
-
RegisterDeviceKeyInputSchema.parse(input),
|
|
3176
|
-
(raw) => DeviceKeyRecordSchema.parse(raw)
|
|
3177
|
-
),
|
|
3178
|
-
issueP256EnrollmentChallenge: (input) => call(
|
|
3179
|
-
"POST",
|
|
3180
|
-
"/v1/me/offline/keys/p256/challenge",
|
|
3181
|
-
P256EnrollmentChallengeInputSchema.parse(input),
|
|
3182
|
-
(raw) => P256EnrollmentChallengeResultSchema.parse(raw)
|
|
3183
|
-
),
|
|
3184
|
-
registerDeviceKeyP256: (input) => call(
|
|
3185
|
-
"POST",
|
|
3186
|
-
"/v1/me/offline/keys/p256",
|
|
3187
|
-
RegisterDeviceKeyP256InputSchema.parse(input),
|
|
3188
|
-
(raw) => DeviceKeyRecordSchema.parse(raw)
|
|
3189
|
-
),
|
|
3190
|
-
listDeviceKeys: () => call(
|
|
3191
|
-
"GET",
|
|
3192
|
-
"/v1/me/offline/keys",
|
|
3193
|
-
void 0,
|
|
3194
|
-
(raw) => deviceKeyItems.parse(raw)
|
|
3195
|
-
),
|
|
3196
|
-
revokeDeviceKey: (input) => call(
|
|
3197
|
-
"POST",
|
|
3198
|
-
"/v1/me/offline/keys/revoke",
|
|
3199
|
-
RevokeDeviceKeyInputSchema.parse(input),
|
|
3200
|
-
() => void 0
|
|
3201
|
-
),
|
|
3202
|
-
provisionAllowance: (input) => call(
|
|
3203
|
-
"POST",
|
|
3204
|
-
"/v1/me/offline/allowance",
|
|
3205
|
-
ProvisionOfflineAllowanceInputSchema.parse(input),
|
|
3206
|
-
(raw) => ProvisionOfflineAllowanceResultSchema.parse(raw)
|
|
3207
|
-
),
|
|
3208
|
-
enable: (input) => call(
|
|
3209
|
-
"POST",
|
|
3210
|
-
"/v1/me/offline/enable",
|
|
3211
|
-
EnableOfflineInputSchema.parse(input),
|
|
3212
|
-
(raw) => EnableOfflineResultSchema.parse(raw)
|
|
3213
|
-
),
|
|
3214
|
-
refresh: (input) => call(
|
|
3215
|
-
"POST",
|
|
3216
|
-
"/v1/me/offline/refresh",
|
|
3217
|
-
IssueOACInputSchema.parse(input),
|
|
3218
|
-
(raw) => OACRecordSchema.parse(raw)
|
|
3219
|
-
),
|
|
3220
|
-
disable: (input) => call(
|
|
3221
|
-
"POST",
|
|
3222
|
-
"/v1/me/offline/disable",
|
|
3223
|
-
DisableOfflineInputSchema.parse(input),
|
|
3224
|
-
(raw) => DisableOfflineResultSchema.parse(raw)
|
|
3225
|
-
),
|
|
3226
|
-
getStatus: (deviceId) => {
|
|
3227
|
-
const qs = deviceId ? `?deviceId=${encodeURIComponent(deviceId)}` : "";
|
|
3228
|
-
return call(
|
|
3229
|
-
"GET",
|
|
3230
|
-
`/v1/me/offline/status${qs}`,
|
|
3231
|
-
void 0,
|
|
3232
|
-
(raw) => OfflineStatusResultSchema.parse(raw)
|
|
3233
|
-
);
|
|
3234
|
-
},
|
|
3235
|
-
getState: (deviceId) => {
|
|
3236
|
-
const qs = deviceId ? `?deviceId=${encodeURIComponent(deviceId)}` : "";
|
|
3237
|
-
return call(
|
|
3238
|
-
"GET",
|
|
3239
|
-
`/v1/me/offline/state${qs}`,
|
|
3240
|
-
void 0,
|
|
3241
|
-
(raw) => OfflineStateResultSchema.parse(raw)
|
|
3242
|
-
);
|
|
3243
|
-
},
|
|
3244
|
-
submitClaim: (claim) => call(
|
|
3245
|
-
"POST",
|
|
3246
|
-
"/v1/me/offline/claims",
|
|
3247
|
-
ConsumerPaymentClaimSchema.parse(claim),
|
|
3248
|
-
(raw) => ConsumerSettleResultSchema.parse(raw)
|
|
3249
|
-
)
|
|
3250
|
-
};
|
|
3251
|
-
}
|
|
3252
|
-
|
|
3253
3314
|
// src/me-offline/signer.ts
|
|
3254
|
-
import {
|
|
3255
|
-
import { p256 } from "@noble/curves/nist";
|
|
3256
|
-
import { bytesToHex as bytesToHex5, hexToBytes as hexToBytes2 } from "@noble/hashes/utils";
|
|
3315
|
+
import { p256 as p2562 } from "@noble/curves/nist";
|
|
3257
3316
|
var CLAIM_DOMAIN_V2 = "flur:consumer-offline:v2:claim";
|
|
3258
3317
|
function canonicalClaimSigningPayload(claim) {
|
|
3259
3318
|
return {
|
|
@@ -3275,7 +3334,7 @@ function canonicalClaimSigningPayload(claim) {
|
|
|
3275
3334
|
function canonicalClaimSigningBytes(claim) {
|
|
3276
3335
|
return canonicalJSONBytes(canonicalClaimSigningPayload(claim));
|
|
3277
3336
|
}
|
|
3278
|
-
function
|
|
3337
|
+
function bytesToBase642(bytes) {
|
|
3279
3338
|
if (typeof Buffer !== "undefined") {
|
|
3280
3339
|
return Buffer.from(bytes).toString("base64");
|
|
3281
3340
|
}
|
|
@@ -3283,7 +3342,7 @@ function bytesToBase64(bytes) {
|
|
|
3283
3342
|
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
3284
3343
|
return btoa(bin);
|
|
3285
3344
|
}
|
|
3286
|
-
function
|
|
3345
|
+
function base64ToBytes2(b64) {
|
|
3287
3346
|
if (typeof Buffer !== "undefined") {
|
|
3288
3347
|
return new Uint8Array(Buffer.from(b64, "base64"));
|
|
3289
3348
|
}
|
|
@@ -3292,7 +3351,7 @@ function base64ToBytes(b64) {
|
|
|
3292
3351
|
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
3293
3352
|
return out;
|
|
3294
3353
|
}
|
|
3295
|
-
var
|
|
3354
|
+
var P256_SPKI_HEADER2 = new Uint8Array([
|
|
3296
3355
|
48,
|
|
3297
3356
|
89,
|
|
3298
3357
|
48,
|
|
@@ -3324,38 +3383,25 @@ function p256PublicKeyToSpkiB64(rawUncompressed) {
|
|
|
3324
3383
|
if (rawUncompressed.length !== 65 || rawUncompressed[0] !== 4) {
|
|
3325
3384
|
throw new Error("p256: expected 65-byte uncompressed point");
|
|
3326
3385
|
}
|
|
3327
|
-
const out = new Uint8Array(
|
|
3328
|
-
out.set(
|
|
3329
|
-
out.set(rawUncompressed,
|
|
3330
|
-
return
|
|
3386
|
+
const out = new Uint8Array(P256_SPKI_HEADER2.length + rawUncompressed.length);
|
|
3387
|
+
out.set(P256_SPKI_HEADER2, 0);
|
|
3388
|
+
out.set(rawUncompressed, P256_SPKI_HEADER2.length);
|
|
3389
|
+
return bytesToBase642(out);
|
|
3331
3390
|
}
|
|
3332
3391
|
function p256SpkiB64ToPublicKey(spkiB64) {
|
|
3333
|
-
const spki =
|
|
3334
|
-
if (spki.length !==
|
|
3392
|
+
const spki = base64ToBytes2(spkiB64);
|
|
3393
|
+
if (spki.length !== P256_SPKI_HEADER2.length + 65) {
|
|
3335
3394
|
throw new Error("p256: invalid SPKI length");
|
|
3336
3395
|
}
|
|
3337
|
-
for (let i = 0; i <
|
|
3338
|
-
if (spki[i] !==
|
|
3396
|
+
for (let i = 0; i < P256_SPKI_HEADER2.length; i++) {
|
|
3397
|
+
if (spki[i] !== P256_SPKI_HEADER2[i]) {
|
|
3339
3398
|
throw new Error("p256: invalid SPKI header");
|
|
3340
3399
|
}
|
|
3341
3400
|
}
|
|
3342
|
-
return spki.slice(
|
|
3343
|
-
}
|
|
3344
|
-
function createSoftwareEd25519Signer(privateKey) {
|
|
3345
|
-
const pub = ed255192.getPublicKey(privateKey);
|
|
3346
|
-
return {
|
|
3347
|
-
alg: "ed25519",
|
|
3348
|
-
async getPublicKey() {
|
|
3349
|
-
return { alg: "ed25519", publicKey: bytesToHex5(pub) };
|
|
3350
|
-
},
|
|
3351
|
-
async sign(bytes) {
|
|
3352
|
-
const sig = ed255192.sign(bytes, privateKey);
|
|
3353
|
-
return { alg: "ed25519", signature: bytesToHex5(sig) };
|
|
3354
|
-
}
|
|
3355
|
-
};
|
|
3401
|
+
return spki.slice(P256_SPKI_HEADER2.length);
|
|
3356
3402
|
}
|
|
3357
3403
|
function createSoftwareP256Signer(privateKey) {
|
|
3358
|
-
const raw =
|
|
3404
|
+
const raw = p2562.getPublicKey(privateKey, false);
|
|
3359
3405
|
const spkiB64 = p256PublicKeyToSpkiB64(raw);
|
|
3360
3406
|
return {
|
|
3361
3407
|
alg: "p256",
|
|
@@ -3363,35 +3409,87 @@ function createSoftwareP256Signer(privateKey) {
|
|
|
3363
3409
|
return { alg: "p256", publicKey: spkiB64 };
|
|
3364
3410
|
},
|
|
3365
3411
|
async sign(bytes) {
|
|
3366
|
-
const sig =
|
|
3412
|
+
const sig = p2562.sign(bytes, privateKey, { prehash: true });
|
|
3367
3413
|
const der = sig.toBytes("der");
|
|
3368
|
-
return { alg: "p256", signature:
|
|
3414
|
+
return { alg: "p256", signature: bytesToBase642(der) };
|
|
3369
3415
|
}
|
|
3370
3416
|
};
|
|
3371
3417
|
}
|
|
3372
3418
|
function verifyClaimSignature(input) {
|
|
3373
3419
|
try {
|
|
3374
|
-
if (input.alg
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
}
|
|
3381
|
-
if (input.alg === "p256") {
|
|
3382
|
-
const sigDer = base64ToBytes(input.signature);
|
|
3383
|
-
const pub = p256SpkiB64ToPublicKey(input.publicKey);
|
|
3384
|
-
return p256.verify(sigDer, input.bytes, pub, {
|
|
3385
|
-
prehash: true,
|
|
3386
|
-
format: "der"
|
|
3387
|
-
});
|
|
3388
|
-
}
|
|
3389
|
-
return false;
|
|
3420
|
+
if (input.alg !== "p256") return false;
|
|
3421
|
+
const sigDer = base64ToBytes2(input.signature);
|
|
3422
|
+
const pub = p256SpkiB64ToPublicKey(input.publicKey);
|
|
3423
|
+
return p2562.verify(sigDer, input.bytes, pub, {
|
|
3424
|
+
prehash: true,
|
|
3425
|
+
format: "der"
|
|
3426
|
+
});
|
|
3390
3427
|
} catch {
|
|
3391
3428
|
return false;
|
|
3392
3429
|
}
|
|
3393
3430
|
}
|
|
3394
3431
|
|
|
3432
|
+
// src/me-offline/sms.ts
|
|
3433
|
+
var OFFLINE_CLAIM_SMS_PREFIX = "FLURC1.";
|
|
3434
|
+
var TOKEN_RE = /(?:^|\s)(FLURC1\.[A-Za-z0-9_-]+={0,2})(?:\s|$)/;
|
|
3435
|
+
function encodeOfflineClaimSmsMessage(claim) {
|
|
3436
|
+
const parsed = ConsumerPaymentClaimSchema.parse(claim);
|
|
3437
|
+
const json = JSON.stringify(parsed);
|
|
3438
|
+
return `${OFFLINE_CLAIM_SMS_PREFIX}${base64UrlEncodeUtf8(json)}`;
|
|
3439
|
+
}
|
|
3440
|
+
function decodeOfflineClaimSmsMessage(message) {
|
|
3441
|
+
const token = extractOfflineClaimSmsToken(message);
|
|
3442
|
+
if (!token) {
|
|
3443
|
+
throw new Error("offline claim SMS token not found");
|
|
3444
|
+
}
|
|
3445
|
+
const encoded = token.slice(OFFLINE_CLAIM_SMS_PREFIX.length);
|
|
3446
|
+
let raw;
|
|
3447
|
+
try {
|
|
3448
|
+
raw = JSON.parse(base64UrlDecodeUtf8(encoded));
|
|
3449
|
+
} catch {
|
|
3450
|
+
throw new Error("offline claim SMS token is malformed");
|
|
3451
|
+
}
|
|
3452
|
+
const parsed = ConsumerPaymentClaimSchema.safeParse(raw);
|
|
3453
|
+
if (!parsed.success) {
|
|
3454
|
+
throw new Error("offline claim SMS token is invalid");
|
|
3455
|
+
}
|
|
3456
|
+
return parsed.data;
|
|
3457
|
+
}
|
|
3458
|
+
function extractOfflineClaimSmsToken(message) {
|
|
3459
|
+
const trimmed = message.trim();
|
|
3460
|
+
if (trimmed.startsWith(OFFLINE_CLAIM_SMS_PREFIX)) {
|
|
3461
|
+
return trimmed.split(/\s+/, 1)[0] ?? null;
|
|
3462
|
+
}
|
|
3463
|
+
return TOKEN_RE.exec(message)?.[1] ?? null;
|
|
3464
|
+
}
|
|
3465
|
+
function base64UrlEncodeUtf8(input) {
|
|
3466
|
+
const bytes = new TextEncoder().encode(input);
|
|
3467
|
+
let binary = "";
|
|
3468
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
3469
|
+
const base64 = typeof btoa === "function" ? btoa(binary) : typeof Buffer !== "undefined" ? Buffer.from(bytes).toString("base64") : void 0;
|
|
3470
|
+
if (!base64) throw new Error("base64 encoder unavailable");
|
|
3471
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
3472
|
+
}
|
|
3473
|
+
function base64UrlDecodeUtf8(input) {
|
|
3474
|
+
const base64 = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
3475
|
+
const padded = base64.padEnd(
|
|
3476
|
+
base64.length + (4 - base64.length % 4) % 4,
|
|
3477
|
+
"="
|
|
3478
|
+
);
|
|
3479
|
+
if (typeof atob === "function") {
|
|
3480
|
+
const binary = atob(padded);
|
|
3481
|
+
const bytes = new Uint8Array(binary.length);
|
|
3482
|
+
for (let index = 0; index < binary.length; index++) {
|
|
3483
|
+
bytes[index] = binary.charCodeAt(index);
|
|
3484
|
+
}
|
|
3485
|
+
return new TextDecoder().decode(bytes);
|
|
3486
|
+
}
|
|
3487
|
+
if (typeof Buffer !== "undefined") {
|
|
3488
|
+
return Buffer.from(padded, "base64").toString("utf8");
|
|
3489
|
+
}
|
|
3490
|
+
throw new Error("base64 decoder unavailable");
|
|
3491
|
+
}
|
|
3492
|
+
|
|
3395
3493
|
// src/partner-funding/client.ts
|
|
3396
3494
|
import { z as z14 } from "zod";
|
|
3397
3495
|
var MinorString = z14.string().regex(/^-?\d+$/);
|
|
@@ -3767,14 +3865,15 @@ function buildArtifactBody(input) {
|
|
|
3767
3865
|
return { ...header, data: input.data };
|
|
3768
3866
|
}
|
|
3769
3867
|
function signArtifact(body, privateKey) {
|
|
3770
|
-
const sig =
|
|
3868
|
+
const sig = signIssuerP256(canonicalJSONBytes(body), privateKey);
|
|
3771
3869
|
return { body, sig };
|
|
3772
3870
|
}
|
|
3773
3871
|
function encodeArtifactUri(signed) {
|
|
3774
3872
|
const bodyBytes = canonicalJSONBytes(signed.body);
|
|
3775
3873
|
const bodyB64 = base64UrlEncode(bodyBytes);
|
|
3776
|
-
const
|
|
3777
|
-
|
|
3874
|
+
const sigBytes = base64UrlDecode(signed.sig);
|
|
3875
|
+
const sigB64Url = base64UrlEncode(sigBytes);
|
|
3876
|
+
return `${FLUR_ARTIFACT_URI_PREFIX}${signed.body.t}/${bodyB64}.${sigB64Url}`;
|
|
3778
3877
|
}
|
|
3779
3878
|
function decodeArtifactUri(uri) {
|
|
3780
3879
|
if (!uri.startsWith(FLUR_ARTIFACT_URI_PREFIX)) {
|
|
@@ -3805,9 +3904,9 @@ function decodeArtifactUri(uri) {
|
|
|
3805
3904
|
}
|
|
3806
3905
|
const bodyBytes = base64UrlDecode(payload.slice(0, dot));
|
|
3807
3906
|
const sigBytes = base64UrlDecode(payload.slice(dot + 1));
|
|
3808
|
-
if (sigBytes.length
|
|
3907
|
+
if (sigBytes.length < 64 || sigBytes.length > 80) {
|
|
3809
3908
|
throw new FlurArtifactError(
|
|
3810
|
-
`Signature
|
|
3909
|
+
`Signature length out of range: ${sigBytes.length}`,
|
|
3811
3910
|
"INVALID_SIGNATURE"
|
|
3812
3911
|
);
|
|
3813
3912
|
}
|
|
@@ -3834,17 +3933,26 @@ function decodeArtifactUri(uri) {
|
|
|
3834
3933
|
type,
|
|
3835
3934
|
bodyBytes,
|
|
3836
3935
|
body: bodyJson,
|
|
3837
|
-
|
|
3936
|
+
// Encode as standard base64 (not url-safe) for sig field consistency.
|
|
3937
|
+
sig: encodeStdBase64(sigBytes)
|
|
3838
3938
|
};
|
|
3839
3939
|
}
|
|
3840
|
-
function
|
|
3940
|
+
function encodeStdBase64(bytes) {
|
|
3941
|
+
if (typeof Buffer !== "undefined") {
|
|
3942
|
+
return Buffer.from(bytes).toString("base64");
|
|
3943
|
+
}
|
|
3944
|
+
let bin = "";
|
|
3945
|
+
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
3946
|
+
return typeof btoa === "function" ? btoa(bin) : "";
|
|
3947
|
+
}
|
|
3948
|
+
function verifyArtifactSignature(decoded, publicKeySpkiB64, options = {}) {
|
|
3841
3949
|
if (options.enforceExpiry !== false && decoded.body.exp !== void 0) {
|
|
3842
3950
|
const now = options.nowSeconds ?? Math.floor(Date.now() / 1e3);
|
|
3843
3951
|
if (decoded.body.exp < now) {
|
|
3844
3952
|
throw new FlurArtifactError("Artifact has expired", "EXPIRED");
|
|
3845
3953
|
}
|
|
3846
3954
|
}
|
|
3847
|
-
return
|
|
3955
|
+
return verifyIssuerP256(decoded.bodyBytes, decoded.sig, publicKeySpkiB64);
|
|
3848
3956
|
}
|
|
3849
3957
|
|
|
3850
3958
|
// src/artifacts/types.ts
|
|
@@ -3863,7 +3971,7 @@ var ARTIFACT_TYPES = {
|
|
|
3863
3971
|
PASS: "pass",
|
|
3864
3972
|
IDENTITY: "identity"
|
|
3865
3973
|
};
|
|
3866
|
-
var
|
|
3974
|
+
var HexString = (length) => z16.string().regex(
|
|
3867
3975
|
new RegExp(`^[0-9a-fA-F]{${length * 2}}$`),
|
|
3868
3976
|
`expected ${length}-byte hex string`
|
|
3869
3977
|
);
|
|
@@ -3881,7 +3989,7 @@ var ReceiptArtifactSchema = z16.object({
|
|
|
3881
3989
|
settledAtMs: z16.number().int().positive(),
|
|
3882
3990
|
ledgerTxnId: z16.string().min(1).max(64).optional(),
|
|
3883
3991
|
memo: z16.string().max(140).optional(),
|
|
3884
|
-
hashChainPrev:
|
|
3992
|
+
hashChainPrev: HexString(32).optional()
|
|
3885
3993
|
});
|
|
3886
3994
|
var ShortId = z16.string().min(1).max(64);
|
|
3887
3995
|
var PositiveInt = z16.number().int().positive();
|
|
@@ -3963,7 +4071,7 @@ var StatementArtifactSchema = z16.object({
|
|
|
3963
4071
|
closingBalanceKobo: z16.number().int(),
|
|
3964
4072
|
transactionCount: NonNegativeInt,
|
|
3965
4073
|
currency: Currency2,
|
|
3966
|
-
hashChainPrev:
|
|
4074
|
+
hashChainPrev: HexString(32).optional()
|
|
3967
4075
|
}).refine((v) => v.periodEndMs > v.periodStartMs, {
|
|
3968
4076
|
message: "periodEndMs must be greater than periodStartMs",
|
|
3969
4077
|
path: ["periodEndMs"]
|
|
@@ -3996,7 +4104,7 @@ var IdentityArtifactSchema = z16.object({
|
|
|
3996
4104
|
"kyc_tier",
|
|
3997
4105
|
"age_band"
|
|
3998
4106
|
]),
|
|
3999
|
-
claimValueHash:
|
|
4107
|
+
claimValueHash: HexString(32),
|
|
4000
4108
|
attestedAtMs: PositiveInt
|
|
4001
4109
|
});
|
|
4002
4110
|
var ARTIFACT_BODY_SCHEMAS = {
|
|
@@ -4060,7 +4168,7 @@ function createArtifactUri(input) {
|
|
|
4060
4168
|
const signed = signArtifact(body, input.privateKey);
|
|
4061
4169
|
return { uri: encodeArtifactUri(signed), signed };
|
|
4062
4170
|
}
|
|
4063
|
-
function verifyArtifactUri(uri,
|
|
4171
|
+
function verifyArtifactUri(uri, publicKeySpkiB64, options = {}) {
|
|
4064
4172
|
const decoded = decodeArtifactUri(uri);
|
|
4065
4173
|
if (!isKnownArtifactType(decoded.type)) {
|
|
4066
4174
|
throw new FlurArtifactError(
|
|
@@ -4082,7 +4190,7 @@ function verifyArtifactUri(uri, publicKey, options = {}) {
|
|
|
4082
4190
|
"INVALID_BODY"
|
|
4083
4191
|
);
|
|
4084
4192
|
}
|
|
4085
|
-
const ok = verifyArtifactSignature(decoded,
|
|
4193
|
+
const ok = verifyArtifactSignature(decoded, publicKeySpkiB64, options);
|
|
4086
4194
|
if (!ok) {
|
|
4087
4195
|
throw new FlurArtifactError(
|
|
4088
4196
|
"Artifact signature verification failed",
|
|
@@ -4158,6 +4266,7 @@ export {
|
|
|
4158
4266
|
HARDENED_ARTIFACT_TYPES,
|
|
4159
4267
|
IdentityArtifactSchema,
|
|
4160
4268
|
IngestFundingResultSchema,
|
|
4269
|
+
IssueAccountOacInputSchema,
|
|
4161
4270
|
IssueOACInputSchema,
|
|
4162
4271
|
LedgerJournalEntryArtifactSchema,
|
|
4163
4272
|
ListPayoutDestinationsResultSchema,
|
|
@@ -4175,6 +4284,7 @@ export {
|
|
|
4175
4284
|
OAC_DEFAULT_CUMULATIVE_KOBO,
|
|
4176
4285
|
OAC_DEFAULT_PER_TX_KOBO,
|
|
4177
4286
|
OAC_DEFAULT_VALIDITY_MS,
|
|
4287
|
+
OFFLINE_CLAIM_SMS_PREFIX,
|
|
4178
4288
|
OfflineClaimArtifactSchema,
|
|
4179
4289
|
OfflineHoldRecordSchema,
|
|
4180
4290
|
OfflinePaymentAuthorizationArtifactSchema,
|
|
@@ -4270,20 +4380,21 @@ export {
|
|
|
4270
4380
|
createPassesClient,
|
|
4271
4381
|
createReceiptArtifactUri,
|
|
4272
4382
|
createReceiptsClient,
|
|
4273
|
-
createSoftwareEd25519Signer,
|
|
4274
4383
|
createSoftwareP256Signer,
|
|
4275
4384
|
decodeArtifactUri,
|
|
4276
4385
|
decodeAuthorizationQR,
|
|
4277
4386
|
decodeBase45,
|
|
4387
|
+
decodeOfflineClaimSmsMessage,
|
|
4278
4388
|
decodePaymentRequestQR,
|
|
4279
4389
|
encodeArtifactUri,
|
|
4280
4390
|
encodeAuthorizationQR,
|
|
4281
4391
|
encodeBase45,
|
|
4282
4392
|
encodeNQR,
|
|
4393
|
+
encodeOfflineClaimSmsMessage,
|
|
4283
4394
|
encodePaymentRequestQR,
|
|
4395
|
+
extractOfflineClaimSmsToken,
|
|
4284
4396
|
formatAmount,
|
|
4285
4397
|
generateDynamicQR,
|
|
4286
|
-
generateKeyPair,
|
|
4287
4398
|
generateStaticQR,
|
|
4288
4399
|
init,
|
|
4289
4400
|
isHardenedArtifactType,
|
|
@@ -4294,13 +4405,10 @@ export {
|
|
|
4294
4405
|
parseAmountInput,
|
|
4295
4406
|
parseNQR,
|
|
4296
4407
|
parseQR,
|
|
4297
|
-
publicKeyFromPrivate,
|
|
4298
4408
|
readTLV,
|
|
4299
4409
|
routingHint,
|
|
4300
|
-
sign,
|
|
4301
4410
|
signArtifact,
|
|
4302
4411
|
signAuthorization,
|
|
4303
|
-
signCanonical,
|
|
4304
4412
|
signOAC,
|
|
4305
4413
|
signPartnerRequest,
|
|
4306
4414
|
signPass,
|
|
@@ -4308,11 +4416,9 @@ export {
|
|
|
4308
4416
|
signReceipt,
|
|
4309
4417
|
signRedemption,
|
|
4310
4418
|
signRequestHMAC,
|
|
4311
|
-
verify,
|
|
4312
4419
|
verifyArtifactSignature,
|
|
4313
4420
|
verifyArtifactUri,
|
|
4314
4421
|
verifyAuthorization,
|
|
4315
|
-
verifyCanonical,
|
|
4316
4422
|
verifyClaimSignature,
|
|
4317
4423
|
verifyOAC,
|
|
4318
4424
|
verifyPass,
|