@nokinc-flur/sdk 0.1.2 → 0.1.4
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 +86 -5
- package/dist/index.d.ts +167 -7
- package/dist/index.js +598 -51
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -23,6 +23,31 @@ npm run gen
|
|
|
23
23
|
FLUR_BASE_URL=http://localhost:8080 RUN_E2E=1 npm test
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
Contract-focused E2E only:
|
|
27
|
+
```bash
|
|
28
|
+
FLUR_BASE_URL=https://your-oci-gateway.example.com RUN_E2E=1 npm run test:contract
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Optional envs for full send-money E2E coverage:
|
|
32
|
+
- `FLUR_E2E_SESSION_TOKEN` (required for account/transactions/recipient)
|
|
33
|
+
- `FLUR_E2E_RECIPIENT_IDENTIFIER` (required for recipient resolve / transfer)
|
|
34
|
+
- `FLUR_E2E_SEND_AUTH_TOKEN` (required for transfer create)
|
|
35
|
+
- `FLUR_E2E_DEVICE_ID` (required for transfer create)
|
|
36
|
+
- `FLUR_E2E_AMOUNT_MINOR` (optional, default `100`)
|
|
37
|
+
- `FLUR_E2E_CURRENCY` (optional, default `NGN`)
|
|
38
|
+
|
|
39
|
+
Generate real E2E auth inputs against deployed backend:
|
|
40
|
+
```bash
|
|
41
|
+
# Step 1: start onboarding and get requestId
|
|
42
|
+
npm run e2e:bootstrap -- --base-url https://your-oci-gateway --phone +23480xxxxxxx
|
|
43
|
+
|
|
44
|
+
# Step 2: rerun with OTP/silent-auth code (and optional recipient)
|
|
45
|
+
npm run e2e:bootstrap -- --base-url https://your-oci-gateway --phone +23480xxxxxxx --code 123456 --recipient +23481xxxxxxx
|
|
46
|
+
|
|
47
|
+
# Script prints PowerShell env exports, then run:
|
|
48
|
+
npm run test:contract
|
|
49
|
+
```
|
|
50
|
+
|
|
26
51
|
## Onboarding flow methods
|
|
27
52
|
|
|
28
53
|
```ts
|
|
@@ -30,12 +55,17 @@ import { FlurClient } from "@nokinc-flur/sdk";
|
|
|
30
55
|
|
|
31
56
|
const client = new FlurClient({ baseUrl: "https://api.example.com" });
|
|
32
57
|
|
|
33
|
-
const started = await client.onboardingStart({
|
|
58
|
+
const started = await client.onboardingStart({
|
|
59
|
+
phoneE164: "+14155550123",
|
|
60
|
+
appInstanceId: "app-instance-1",
|
|
61
|
+
platform: "ios",
|
|
62
|
+
});
|
|
34
63
|
// started: { requestId, checkUrl?, expiresInSec, fallback }
|
|
35
64
|
|
|
36
65
|
const completed = await client.onboardingComplete({
|
|
37
66
|
requestId: started.requestId,
|
|
38
67
|
code: "123456",
|
|
68
|
+
appInstanceId: "app-instance-1",
|
|
39
69
|
});
|
|
40
70
|
// completed: { sessionToken, userId, restricted, risk_reasons }
|
|
41
71
|
```
|
|
@@ -48,7 +78,7 @@ const registered = await client.registerDevice({
|
|
|
48
78
|
appInstanceId: "app-instance-1",
|
|
49
79
|
platform: "ios",
|
|
50
80
|
networkSignals: { ip: "1.2.3.4" },
|
|
51
|
-
});
|
|
81
|
+
}, { accessToken: completed.sessionToken });
|
|
52
82
|
// { deviceId, fingerprintHash, driftScore, trustState, stepUpRequired }
|
|
53
83
|
|
|
54
84
|
const refreshed = await client.authRefresh({
|
|
@@ -59,9 +89,60 @@ const refreshed = await client.authRefresh({
|
|
|
59
89
|
});
|
|
60
90
|
// { refreshToken, stepUpRequired }
|
|
61
91
|
|
|
62
|
-
await client.pinSet({ userId: completed.userId, pin: "123456" });
|
|
63
|
-
await client.pinVerify({ userId: completed.userId, pin: "123456" });
|
|
64
|
-
await client.authLogout(
|
|
92
|
+
await client.pinSet({ userId: completed.userId, pin: "123456" }, { accessToken: completed.sessionToken });
|
|
93
|
+
await client.pinVerify({ userId: completed.userId, pin: "123456" }, { accessToken: completed.sessionToken });
|
|
94
|
+
await client.authLogout(
|
|
95
|
+
{ userId: completed.userId, refreshToken: refreshed.refreshToken },
|
|
96
|
+
{ accessToken: refreshed.refreshToken }
|
|
97
|
+
);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Send-money methods
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
// Register per-device signing key for send authorization
|
|
104
|
+
await client.registerSendDeviceKey({
|
|
105
|
+
userId: completed.userId,
|
|
106
|
+
deviceId: registered.deviceId,
|
|
107
|
+
publicKey: "-----BEGIN PUBLIC KEY-----...",
|
|
108
|
+
}, { accessToken: completed.sessionToken });
|
|
109
|
+
|
|
110
|
+
// Obtain challenge and verify signature to get short-lived send token
|
|
111
|
+
const challenge = await client.createSendChallenge({
|
|
112
|
+
userId: completed.userId,
|
|
113
|
+
deviceId: registered.deviceId,
|
|
114
|
+
}, { accessToken: completed.sessionToken });
|
|
115
|
+
|
|
116
|
+
const send = await client.verifySendChallenge({
|
|
117
|
+
userId: completed.userId,
|
|
118
|
+
deviceId: registered.deviceId,
|
|
119
|
+
challengeId: challenge.challengeId,
|
|
120
|
+
signature: "base64-signature",
|
|
121
|
+
}, { accessToken: completed.sessionToken });
|
|
122
|
+
|
|
123
|
+
// Resolve recipient and create transfer
|
|
124
|
+
const recipient = await client.resolveRecipient(
|
|
125
|
+
{ identifier: "+14155550123" },
|
|
126
|
+
{ accessToken: completed.sessionToken }
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
await client.createTransfer(
|
|
130
|
+
{
|
|
131
|
+
recipientIdentifier: recipient.normalizedIdentifier,
|
|
132
|
+
amountMinor: 5000,
|
|
133
|
+
currency: "NGN",
|
|
134
|
+
sendAuthToken: send.sendAuthToken,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
accessToken: completed.sessionToken,
|
|
138
|
+
deviceId: registered.deviceId,
|
|
139
|
+
idempotencyKey: crypto.randomUUID(),
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Account + transaction endpoints
|
|
144
|
+
await client.accountSummary({ accessToken: completed.sessionToken });
|
|
145
|
+
await client.listTransactions({ accessToken: completed.sessionToken, limit: 20 });
|
|
65
146
|
```
|
|
66
147
|
|
|
67
148
|
## Error code mapping
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
type Money = {
|
|
2
|
+
amountMinor: bigint;
|
|
3
|
+
currency: string;
|
|
4
|
+
};
|
|
5
|
+
declare function parseAmountInput(value: string, currency: string): Money;
|
|
6
|
+
declare function formatAmount(amountMinor: bigint, currency: string): string;
|
|
7
|
+
declare function normalizeE164(input: string, defaultCountry?: string): string;
|
|
8
|
+
declare function moneyMinorToNumber(amountMinor: bigint): number;
|
|
9
|
+
|
|
1
10
|
type FlurClientOptions = {
|
|
2
11
|
baseUrl: string;
|
|
3
12
|
fetchImpl?: typeof fetch;
|
|
@@ -6,6 +15,15 @@ type FlurClientOptions = {
|
|
|
6
15
|
type OnboardingFallback = "SILENT_AUTH" | "OTP";
|
|
7
16
|
type OnboardingStartInput = {
|
|
8
17
|
phoneE164: string;
|
|
18
|
+
appInstanceId: string;
|
|
19
|
+
platform: "android" | "ios" | "web";
|
|
20
|
+
turnstileToken?: string;
|
|
21
|
+
appAttestation?: {
|
|
22
|
+
provider: "android" | "ios" | "web";
|
|
23
|
+
token: string;
|
|
24
|
+
};
|
|
25
|
+
firstName?: string;
|
|
26
|
+
lastName?: string;
|
|
9
27
|
};
|
|
10
28
|
type OnboardingStartResponse = {
|
|
11
29
|
requestId: string;
|
|
@@ -17,7 +35,7 @@ type OnboardingRiskReason = "SIM_SWAP_RECENT" | "ROAMING" | "CARRIER_CHANGED";
|
|
|
17
35
|
type OnboardingCompleteInput = {
|
|
18
36
|
requestId: string;
|
|
19
37
|
code: string;
|
|
20
|
-
appInstanceId
|
|
38
|
+
appInstanceId: string;
|
|
21
39
|
fingerprintHash?: string;
|
|
22
40
|
};
|
|
23
41
|
type OnboardingCompleteResponse = {
|
|
@@ -70,6 +88,125 @@ type PinVerifyInput = {
|
|
|
70
88
|
userId: string;
|
|
71
89
|
pin: string;
|
|
72
90
|
};
|
|
91
|
+
type RegisterSendDeviceKeyInput = {
|
|
92
|
+
userId: string;
|
|
93
|
+
deviceId: string;
|
|
94
|
+
publicKey: string;
|
|
95
|
+
};
|
|
96
|
+
type SendChallengeInput = {
|
|
97
|
+
userId: string;
|
|
98
|
+
deviceId: string;
|
|
99
|
+
};
|
|
100
|
+
type SendChallengeResponse = {
|
|
101
|
+
challengeId: string;
|
|
102
|
+
nonce: string;
|
|
103
|
+
expiresAt: string;
|
|
104
|
+
};
|
|
105
|
+
type SendVerifyInput = {
|
|
106
|
+
userId: string;
|
|
107
|
+
deviceId: string;
|
|
108
|
+
challengeId: string;
|
|
109
|
+
signature: string;
|
|
110
|
+
};
|
|
111
|
+
type SendVerifyResponse = {
|
|
112
|
+
sendAuthToken: string;
|
|
113
|
+
};
|
|
114
|
+
type RecipientResolveInput = {
|
|
115
|
+
identifier: string;
|
|
116
|
+
};
|
|
117
|
+
type RecipientResolveResponse = {
|
|
118
|
+
recipientUserId: string;
|
|
119
|
+
displayName: string;
|
|
120
|
+
normalizedIdentifier: string;
|
|
121
|
+
isActive: boolean;
|
|
122
|
+
};
|
|
123
|
+
type TransferInput = {
|
|
124
|
+
recipientIdentifier: string;
|
|
125
|
+
amountMinor: number;
|
|
126
|
+
currency: string;
|
|
127
|
+
sendAuthToken: string;
|
|
128
|
+
};
|
|
129
|
+
type TransferStatus = "SETTLED" | "PENDING_REVIEW" | "DECLINED";
|
|
130
|
+
type TransferResponse = {
|
|
131
|
+
transactionId: string;
|
|
132
|
+
status: TransferStatus;
|
|
133
|
+
userStatus: TransferStatus;
|
|
134
|
+
recipientName: string;
|
|
135
|
+
timestamp: string;
|
|
136
|
+
};
|
|
137
|
+
type TransactionDirection = "OUTGOING" | "INCOMING";
|
|
138
|
+
type AccountActivityItem = {
|
|
139
|
+
id: string;
|
|
140
|
+
type: string;
|
|
141
|
+
direction: TransactionDirection;
|
|
142
|
+
name: string;
|
|
143
|
+
identifier: string;
|
|
144
|
+
amountMinor: number;
|
|
145
|
+
currency: string;
|
|
146
|
+
status: string;
|
|
147
|
+
timestamp: string;
|
|
148
|
+
};
|
|
149
|
+
type AccountSummaryResponse = {
|
|
150
|
+
balance: number;
|
|
151
|
+
currency: string;
|
|
152
|
+
dailySendLimit: number;
|
|
153
|
+
dailySendRemaining: number;
|
|
154
|
+
kycTier: string;
|
|
155
|
+
kycStatus: string;
|
|
156
|
+
recentActivity: AccountActivityItem[];
|
|
157
|
+
};
|
|
158
|
+
type TransactionsListResponse = {
|
|
159
|
+
items: AccountActivityItem[];
|
|
160
|
+
nextCursor: string | null;
|
|
161
|
+
};
|
|
162
|
+
type TransactionDetailResponse = {
|
|
163
|
+
transactionId: string;
|
|
164
|
+
type: string;
|
|
165
|
+
direction: TransactionDirection;
|
|
166
|
+
counterpartyName: string;
|
|
167
|
+
counterpartyIdentifier: string;
|
|
168
|
+
amountMinor: number;
|
|
169
|
+
currency: string;
|
|
170
|
+
status: string;
|
|
171
|
+
timestamp: string;
|
|
172
|
+
};
|
|
173
|
+
type PushPlatform = "ios" | "android" | "web";
|
|
174
|
+
type PushRegisterInput = {
|
|
175
|
+
deviceId: string;
|
|
176
|
+
platform: PushPlatform;
|
|
177
|
+
token: string;
|
|
178
|
+
};
|
|
179
|
+
type AuthorizedOptions = {
|
|
180
|
+
accessToken: string;
|
|
181
|
+
};
|
|
182
|
+
type CreateTransferOptions = AuthorizedOptions & {
|
|
183
|
+
deviceId: string;
|
|
184
|
+
idempotencyKey: string;
|
|
185
|
+
};
|
|
186
|
+
type ListTransactionsOptions = AuthorizedOptions & {
|
|
187
|
+
cursor?: string;
|
|
188
|
+
limit?: number;
|
|
189
|
+
};
|
|
190
|
+
type BiometricSigner = {
|
|
191
|
+
getOrCreateKeyPair(deviceId: string): Promise<string>;
|
|
192
|
+
sign(payload: string): Promise<string>;
|
|
193
|
+
};
|
|
194
|
+
type SendMoneyInput = {
|
|
195
|
+
recipientIdentifier: string;
|
|
196
|
+
money: Money;
|
|
197
|
+
sendAuthToken: string;
|
|
198
|
+
idempotencyKey?: string;
|
|
199
|
+
defaultCountry?: string;
|
|
200
|
+
};
|
|
201
|
+
type SendMoneyOptions = AuthorizedOptions & {
|
|
202
|
+
deviceId: string;
|
|
203
|
+
};
|
|
204
|
+
type AuthorizeSendWithBiometricInput = {
|
|
205
|
+
userId: string;
|
|
206
|
+
deviceId: string;
|
|
207
|
+
accessToken: string;
|
|
208
|
+
signer: BiometricSigner;
|
|
209
|
+
};
|
|
73
210
|
declare class FlurClient {
|
|
74
211
|
private readonly baseUrl;
|
|
75
212
|
private readonly fetchImpl;
|
|
@@ -83,21 +220,44 @@ declare class FlurClient {
|
|
|
83
220
|
}>;
|
|
84
221
|
onboardingStart(input: OnboardingStartInput): Promise<OnboardingStartResponse>;
|
|
85
222
|
onboardingComplete(input: OnboardingCompleteInput): Promise<OnboardingCompleteResponse>;
|
|
86
|
-
registerDevice(input: RegisterDeviceInput): Promise<RegisterDeviceResponse>;
|
|
223
|
+
registerDevice(input: RegisterDeviceInput, options: AuthorizedOptions): Promise<RegisterDeviceResponse>;
|
|
87
224
|
authRefresh(input: AuthRefreshInput): Promise<AuthRefreshResponse>;
|
|
88
|
-
authLogout(input: AuthLogoutInput): Promise<{
|
|
225
|
+
authLogout(input: AuthLogoutInput, options: AuthorizedOptions): Promise<{
|
|
226
|
+
ok: boolean;
|
|
227
|
+
}>;
|
|
228
|
+
pinSet(input: PinSetInput, options: AuthorizedOptions): Promise<{
|
|
229
|
+
ok: boolean;
|
|
230
|
+
}>;
|
|
231
|
+
pinVerify(input: PinVerifyInput, options: AuthorizedOptions): Promise<{
|
|
232
|
+
ok: boolean;
|
|
233
|
+
}>;
|
|
234
|
+
registerSendDeviceKey(input: RegisterSendDeviceKeyInput, options: AuthorizedOptions): Promise<{
|
|
89
235
|
ok: boolean;
|
|
90
236
|
}>;
|
|
91
|
-
|
|
237
|
+
createSendChallenge(input: SendChallengeInput, options: AuthorizedOptions): Promise<SendChallengeResponse>;
|
|
238
|
+
verifySendChallenge(input: SendVerifyInput, options: AuthorizedOptions): Promise<SendVerifyResponse>;
|
|
239
|
+
registerBiometricDeviceKey(input: {
|
|
240
|
+
userId: string;
|
|
241
|
+
deviceId: string;
|
|
242
|
+
accessToken: string;
|
|
243
|
+
signer: BiometricSigner;
|
|
244
|
+
}): Promise<{
|
|
92
245
|
ok: boolean;
|
|
93
246
|
}>;
|
|
94
|
-
|
|
247
|
+
authorizeSendWithBiometric(input: AuthorizeSendWithBiometricInput): Promise<SendVerifyResponse>;
|
|
248
|
+
resolveRecipient(input: RecipientResolveInput, options: AuthorizedOptions): Promise<RecipientResolveResponse>;
|
|
249
|
+
createTransfer(input: TransferInput, options: CreateTransferOptions): Promise<TransferResponse>;
|
|
250
|
+
sendMoney(input: SendMoneyInput, options: SendMoneyOptions): Promise<TransferResponse>;
|
|
251
|
+
accountSummary(options: AuthorizedOptions): Promise<AccountSummaryResponse>;
|
|
252
|
+
listTransactions(options: ListTransactionsOptions): Promise<TransactionsListResponse>;
|
|
253
|
+
transactionDetail(transactionId: string, options: AuthorizedOptions): Promise<TransactionDetailResponse>;
|
|
254
|
+
registerPushToken(input: PushRegisterInput, options: AuthorizedOptions): Promise<{
|
|
95
255
|
ok: boolean;
|
|
96
256
|
}>;
|
|
97
257
|
private requestJson;
|
|
98
258
|
}
|
|
99
259
|
|
|
100
|
-
type FlurErrorCode = "NETWORK_ERROR" | "HTTP_ERROR" | "TIMEOUT" | "UNKNOWN" | "TOKEN_REPLAYED" | "SESSION_MISMATCH" | "PIN_INVALID" | "PIN_LOCKED" | "PIN_NOT_SET" | "STEP_UP_REQUIRED";
|
|
260
|
+
type FlurErrorCode = "NETWORK_ERROR" | "HTTP_ERROR" | "TIMEOUT" | "UNKNOWN" | "UNAUTHORIZED" | "INVALID_REQUEST" | "RATE_LIMITED" | "NOT_FOUND" | "USER_NOT_FOUND" | "TOKEN_REPLAYED" | "SESSION_MISMATCH" | "PIN_INVALID" | "PIN_LOCKED" | "PIN_NOT_SET" | "STEP_UP_REQUIRED" | "DEVICE_KEY_NOT_REGISTERED" | "CHALLENGE_INVALID" | "CHALLENGE_EXPIRED" | "DEVICE_KEY_REVOKED" | "SIGNATURE_INVALID" | "INVALID_RECIPIENT" | "SEND_AUTH_INVALID" | "IDEMPOTENCY_KEY_CONFLICT" | "IDEMPOTENCY_IN_PROGRESS" | "CANNOT_SEND_TO_SELF" | "INSUFFICIENT_FUNDS";
|
|
101
261
|
declare class FlurError extends Error {
|
|
102
262
|
readonly code: FlurErrorCode;
|
|
103
263
|
readonly status?: number;
|
|
@@ -108,4 +268,4 @@ declare class FlurError extends Error {
|
|
|
108
268
|
});
|
|
109
269
|
}
|
|
110
270
|
|
|
111
|
-
export { type AuthLogoutInput, type AuthRefreshInput, type AuthRefreshResponse, type DeviceTrustState, FlurClient, type FlurClientOptions, FlurError, type OnboardingCompleteInput, type OnboardingCompleteResponse, type OnboardingFallback, type OnboardingRiskReason, type OnboardingStartInput, type OnboardingStartResponse, type PinSetInput, type PinVerifyInput, type RegisterDeviceInput, type RegisterDeviceResponse };
|
|
271
|
+
export { type AccountActivityItem, type AccountSummaryResponse, type AuthLogoutInput, type AuthRefreshInput, type AuthRefreshResponse, type AuthorizeSendWithBiometricInput, type AuthorizedOptions, type BiometricSigner, type CreateTransferOptions, type DeviceTrustState, FlurClient, type FlurClientOptions, FlurError, type ListTransactionsOptions, type Money, type OnboardingCompleteInput, type OnboardingCompleteResponse, type OnboardingFallback, type OnboardingRiskReason, type OnboardingStartInput, type OnboardingStartResponse, type PinSetInput, type PinVerifyInput, type PushPlatform, type PushRegisterInput, type RecipientResolveInput, type RecipientResolveResponse, type RegisterDeviceInput, type RegisterDeviceResponse, type RegisterSendDeviceKeyInput, type SendChallengeInput, type SendChallengeResponse, type SendMoneyInput, type SendMoneyOptions, type SendVerifyInput, type SendVerifyResponse, type TransactionDetailResponse, type TransactionDirection, type TransactionsListResponse, type TransferInput, type TransferResponse, type TransferStatus, formatAmount, moneyMinorToNumber, normalizeE164, parseAmountInput };
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,206 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
import { z as z3 } from "zod";
|
|
3
|
+
|
|
4
|
+
// src/contracts.ts
|
|
5
|
+
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(z.enum(["SIM_SWAP_RECENT", "ROAMING", "CARRIER_CHANGED"])),
|
|
45
|
+
stepUpRequired: z.boolean().optional(),
|
|
46
|
+
riskStatus: z.enum(["ok", "unavailable"]).optional()
|
|
47
|
+
});
|
|
48
|
+
var RegisterDeviceRequestSchema = z.object({
|
|
49
|
+
userId: UuidSchema,
|
|
50
|
+
appInstanceId: z.string().min(3),
|
|
51
|
+
platform: z.string().min(2),
|
|
52
|
+
model: z.string().optional(),
|
|
53
|
+
networkSignals: z.object({
|
|
54
|
+
ip: z.string().min(3),
|
|
55
|
+
asn: z.number().int().optional(),
|
|
56
|
+
country: z.string().min(2).optional(),
|
|
57
|
+
carrier: z.string().optional()
|
|
58
|
+
})
|
|
59
|
+
});
|
|
60
|
+
var RegisterDeviceResponseSchema = z.object({
|
|
61
|
+
deviceId: z.string().min(1),
|
|
62
|
+
fingerprintHash: z.string().min(1),
|
|
63
|
+
driftScore: z.number(),
|
|
64
|
+
trustState: z.enum(["TRUSTED_PRIMARY", "TRUSTED_SECONDARY", "UNVERIFIED"]),
|
|
65
|
+
stepUpRequired: z.boolean()
|
|
66
|
+
});
|
|
67
|
+
var AuthRefreshRequestSchema = z.object({
|
|
68
|
+
userId: UuidSchema,
|
|
69
|
+
refreshToken: z.string().min(8),
|
|
70
|
+
appInstanceId: z.string().min(3),
|
|
71
|
+
fingerprintHash: z.string().min(3)
|
|
72
|
+
});
|
|
73
|
+
var AuthRefreshResponseSchema = z.object({
|
|
74
|
+
refreshToken: z.string().min(8),
|
|
75
|
+
stepUpRequired: z.boolean()
|
|
76
|
+
});
|
|
77
|
+
var AuthLogoutRequestSchema = z.object({
|
|
78
|
+
userId: UuidSchema,
|
|
79
|
+
refreshToken: z.string().min(8)
|
|
80
|
+
});
|
|
81
|
+
var PinSetRequestSchema = z.object({
|
|
82
|
+
userId: UuidSchema,
|
|
83
|
+
pin: z.string().regex(/^\d{6}$/)
|
|
84
|
+
});
|
|
85
|
+
var PinVerifyRequestSchema = z.object({
|
|
86
|
+
userId: UuidSchema,
|
|
87
|
+
pin: z.string().regex(/^\d{6}$/)
|
|
88
|
+
});
|
|
89
|
+
var OkResponseSchema = z.object({
|
|
90
|
+
ok: z.boolean()
|
|
91
|
+
});
|
|
92
|
+
var RegisterSendDeviceKeyRequestSchema = z.object({
|
|
93
|
+
userId: UuidSchema,
|
|
94
|
+
deviceId: z.string().min(3),
|
|
95
|
+
publicKey: z.string().min(32)
|
|
96
|
+
});
|
|
97
|
+
var SendChallengeRequestSchema = z.object({
|
|
98
|
+
userId: UuidSchema,
|
|
99
|
+
deviceId: z.string().min(3)
|
|
100
|
+
});
|
|
101
|
+
var SendChallengeResponseSchema = z.object({
|
|
102
|
+
challengeId: UuidSchema,
|
|
103
|
+
nonce: z.string().min(1),
|
|
104
|
+
expiresAt: IsoDateSchema
|
|
105
|
+
});
|
|
106
|
+
var SendVerifyRequestSchema = z.object({
|
|
107
|
+
userId: UuidSchema,
|
|
108
|
+
deviceId: z.string().min(3),
|
|
109
|
+
challengeId: UuidSchema,
|
|
110
|
+
signature: z.string().min(16)
|
|
111
|
+
});
|
|
112
|
+
var SendVerifyResponseSchema = z.object({
|
|
113
|
+
sendAuthToken: z.string().min(16)
|
|
114
|
+
});
|
|
115
|
+
var ResolveRecipientRequestSchema = z.object({
|
|
116
|
+
identifier: z.string().min(3)
|
|
117
|
+
});
|
|
118
|
+
var ResolveRecipientResponseSchema = z.object({
|
|
119
|
+
recipientUserId: UuidSchema,
|
|
120
|
+
displayName: z.string().min(1),
|
|
121
|
+
normalizedIdentifier: z.string().regex(E164Regex),
|
|
122
|
+
isActive: z.boolean()
|
|
123
|
+
});
|
|
124
|
+
var CreateTransferRequestSchema = z.object({
|
|
125
|
+
recipientIdentifier: z.string().min(3),
|
|
126
|
+
amountMinor: z.number().int().positive(),
|
|
127
|
+
currency: CurrencySchema,
|
|
128
|
+
sendAuthToken: z.string().min(16)
|
|
129
|
+
});
|
|
130
|
+
var TransferStatusSchema = z.enum(["SETTLED", "PENDING_REVIEW", "DECLINED"]);
|
|
131
|
+
var TransferResponseSchema = z.object({
|
|
132
|
+
transactionId: z.string().min(1),
|
|
133
|
+
status: TransferStatusSchema,
|
|
134
|
+
userStatus: TransferStatusSchema,
|
|
135
|
+
recipientName: z.string().min(1),
|
|
136
|
+
timestamp: IsoDateSchema
|
|
137
|
+
});
|
|
138
|
+
var DirectionSchema = z.enum(["OUTGOING", "INCOMING"]);
|
|
139
|
+
var AccountActivityItemSchema = z.object({
|
|
140
|
+
id: z.string().min(1),
|
|
141
|
+
type: z.string().min(1),
|
|
142
|
+
direction: DirectionSchema,
|
|
143
|
+
name: z.string().min(1),
|
|
144
|
+
identifier: z.string().min(1),
|
|
145
|
+
amountMinor: z.number().int(),
|
|
146
|
+
currency: CurrencySchema,
|
|
147
|
+
status: z.string().min(1),
|
|
148
|
+
timestamp: IsoDateSchema
|
|
149
|
+
});
|
|
150
|
+
var AccountSummaryResponseSchema = z.object({
|
|
151
|
+
balance: z.number().int(),
|
|
152
|
+
currency: CurrencySchema,
|
|
153
|
+
dailySendLimit: z.number().int().nonnegative(),
|
|
154
|
+
dailySendRemaining: z.number().int().nonnegative(),
|
|
155
|
+
kycTier: z.string().min(1),
|
|
156
|
+
kycStatus: z.string().min(1),
|
|
157
|
+
recentActivity: z.array(AccountActivityItemSchema)
|
|
158
|
+
});
|
|
159
|
+
var TransactionsListResponseSchema = z.object({
|
|
160
|
+
items: z.array(AccountActivityItemSchema),
|
|
161
|
+
nextCursor: z.string().nullable()
|
|
162
|
+
});
|
|
163
|
+
var TransactionDetailResponseSchema = z.object({
|
|
164
|
+
transactionId: z.string().min(1),
|
|
165
|
+
type: z.string().min(1),
|
|
166
|
+
direction: DirectionSchema,
|
|
167
|
+
counterpartyName: z.string().min(1),
|
|
168
|
+
counterpartyIdentifier: z.string().min(1),
|
|
169
|
+
amountMinor: z.number().int(),
|
|
170
|
+
currency: CurrencySchema,
|
|
171
|
+
status: z.string().min(1),
|
|
172
|
+
timestamp: IsoDateSchema
|
|
173
|
+
});
|
|
174
|
+
var PushRegisterRequestSchema = z.object({
|
|
175
|
+
deviceId: z.string().min(3),
|
|
176
|
+
platform: z.enum(["ios", "android", "web"]),
|
|
177
|
+
token: z.string().min(16)
|
|
178
|
+
});
|
|
179
|
+
|
|
1
180
|
// src/errors.ts
|
|
2
181
|
var backendErrorCodeSet = /* @__PURE__ */ new Set([
|
|
182
|
+
"UNAUTHORIZED",
|
|
183
|
+
"INVALID_REQUEST",
|
|
184
|
+
"RATE_LIMITED",
|
|
185
|
+
"NOT_FOUND",
|
|
186
|
+
"USER_NOT_FOUND",
|
|
3
187
|
"TOKEN_REPLAYED",
|
|
4
188
|
"SESSION_MISMATCH",
|
|
5
189
|
"PIN_INVALID",
|
|
6
190
|
"PIN_LOCKED",
|
|
7
191
|
"PIN_NOT_SET",
|
|
8
|
-
"STEP_UP_REQUIRED"
|
|
192
|
+
"STEP_UP_REQUIRED",
|
|
193
|
+
"DEVICE_KEY_NOT_REGISTERED",
|
|
194
|
+
"CHALLENGE_INVALID",
|
|
195
|
+
"CHALLENGE_EXPIRED",
|
|
196
|
+
"DEVICE_KEY_REVOKED",
|
|
197
|
+
"SIGNATURE_INVALID",
|
|
198
|
+
"INVALID_RECIPIENT",
|
|
199
|
+
"SEND_AUTH_INVALID",
|
|
200
|
+
"IDEMPOTENCY_KEY_CONFLICT",
|
|
201
|
+
"IDEMPOTENCY_IN_PROGRESS",
|
|
202
|
+
"CANNOT_SEND_TO_SELF",
|
|
203
|
+
"INSUFFICIENT_FUNDS"
|
|
9
204
|
]);
|
|
10
205
|
var FlurError = class extends Error {
|
|
11
206
|
code;
|
|
@@ -44,6 +239,101 @@ async function mapToFlurError(res) {
|
|
|
44
239
|
return new FlurError(mappedMessage, mappedCode, { status: res.status, details });
|
|
45
240
|
}
|
|
46
241
|
|
|
242
|
+
// src/primitives.ts
|
|
243
|
+
import { z as z2 } from "zod";
|
|
244
|
+
var CurrencyCodeSchema = z2.string().trim().length(3).transform((value) => value.toUpperCase());
|
|
245
|
+
var currencyFractionDigits = {
|
|
246
|
+
NGN: 2,
|
|
247
|
+
USD: 2,
|
|
248
|
+
EUR: 2,
|
|
249
|
+
GBP: 2,
|
|
250
|
+
CAD: 2,
|
|
251
|
+
JPY: 0
|
|
252
|
+
};
|
|
253
|
+
function getFractionDigits(currency) {
|
|
254
|
+
return currencyFractionDigits[currency] ?? 2;
|
|
255
|
+
}
|
|
256
|
+
function parseAmountInput(value, currency) {
|
|
257
|
+
const normalizedCurrency = CurrencyCodeSchema.parse(currency);
|
|
258
|
+
const raw = value.trim();
|
|
259
|
+
if (!/^\d+(\.\d+)?$/.test(raw)) {
|
|
260
|
+
throw new FlurError("Invalid amount format", "INVALID_REQUEST");
|
|
261
|
+
}
|
|
262
|
+
const [whole, fraction = ""] = raw.split(".");
|
|
263
|
+
const fractionDigits = getFractionDigits(normalizedCurrency);
|
|
264
|
+
if (fraction.length > fractionDigits) {
|
|
265
|
+
throw new FlurError("Too many decimal places for currency", "INVALID_REQUEST");
|
|
266
|
+
}
|
|
267
|
+
const paddedFraction = fraction.padEnd(fractionDigits, "0");
|
|
268
|
+
const minorAsString = `${whole}${paddedFraction}`.replace(/^0+(\d)/, "$1") || "0";
|
|
269
|
+
const amountMinor = BigInt(minorAsString);
|
|
270
|
+
if (amountMinor < 0n) {
|
|
271
|
+
throw new FlurError("Amount cannot be negative", "INVALID_REQUEST");
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
amountMinor,
|
|
275
|
+
currency: normalizedCurrency
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
function formatAmount(amountMinor, currency) {
|
|
279
|
+
const normalizedCurrency = CurrencyCodeSchema.parse(currency);
|
|
280
|
+
if (amountMinor < 0n) {
|
|
281
|
+
throw new FlurError("Amount cannot be negative", "INVALID_REQUEST");
|
|
282
|
+
}
|
|
283
|
+
const fractionDigits = getFractionDigits(normalizedCurrency);
|
|
284
|
+
if (fractionDigits === 0) {
|
|
285
|
+
return amountMinor.toString();
|
|
286
|
+
}
|
|
287
|
+
const divisor = 10n ** BigInt(fractionDigits);
|
|
288
|
+
const whole = amountMinor / divisor;
|
|
289
|
+
const fraction = (amountMinor % divisor).toString().padStart(fractionDigits, "0");
|
|
290
|
+
return `${whole.toString()}.${fraction}`;
|
|
291
|
+
}
|
|
292
|
+
var defaultCountryDialingCode = {
|
|
293
|
+
NG: "234",
|
|
294
|
+
US: "1",
|
|
295
|
+
CA: "1",
|
|
296
|
+
GB: "44"
|
|
297
|
+
};
|
|
298
|
+
function normalizeE164(input, defaultCountry) {
|
|
299
|
+
const trimmed = input.trim();
|
|
300
|
+
if (trimmed.startsWith("+")) {
|
|
301
|
+
if (!E164Regex.test(trimmed)) {
|
|
302
|
+
throw new FlurError("Invalid phone number", "INVALID_REQUEST");
|
|
303
|
+
}
|
|
304
|
+
return trimmed;
|
|
305
|
+
}
|
|
306
|
+
const digits = trimmed.replace(/\D/g, "");
|
|
307
|
+
if (digits.length === 0) {
|
|
308
|
+
throw new FlurError("Invalid phone number", "INVALID_REQUEST");
|
|
309
|
+
}
|
|
310
|
+
if (digits.startsWith("00")) {
|
|
311
|
+
const candidate2 = `+${digits.slice(2)}`;
|
|
312
|
+
if (!E164Regex.test(candidate2)) {
|
|
313
|
+
throw new FlurError("Invalid phone number", "INVALID_REQUEST");
|
|
314
|
+
}
|
|
315
|
+
return candidate2;
|
|
316
|
+
}
|
|
317
|
+
const normalizedCountry = defaultCountry?.trim().toUpperCase();
|
|
318
|
+
const countryCode = normalizedCountry ? defaultCountryDialingCode[normalizedCountry] : void 0;
|
|
319
|
+
if (!countryCode) {
|
|
320
|
+
throw new FlurError("Invalid phone number", "INVALID_REQUEST");
|
|
321
|
+
}
|
|
322
|
+
const localDigits = digits.startsWith("0") ? digits.slice(1) : digits;
|
|
323
|
+
const candidate = `+${countryCode}${localDigits}`;
|
|
324
|
+
if (!E164Regex.test(candidate)) {
|
|
325
|
+
throw new FlurError("Invalid phone number", "INVALID_REQUEST");
|
|
326
|
+
}
|
|
327
|
+
return candidate;
|
|
328
|
+
}
|
|
329
|
+
function moneyMinorToNumber(amountMinor) {
|
|
330
|
+
const asNumber = Number(amountMinor);
|
|
331
|
+
if (!Number.isSafeInteger(asNumber)) {
|
|
332
|
+
throw new FlurError("Amount exceeds safe integer range", "INVALID_REQUEST");
|
|
333
|
+
}
|
|
334
|
+
return asNumber;
|
|
335
|
+
}
|
|
336
|
+
|
|
47
337
|
// src/client.ts
|
|
48
338
|
var FlurClient = class {
|
|
49
339
|
baseUrl;
|
|
@@ -55,68 +345,315 @@ var FlurClient = class {
|
|
|
55
345
|
this.timeoutMs = opts.timeoutMs ?? 1e4;
|
|
56
346
|
}
|
|
57
347
|
async health() {
|
|
58
|
-
return this.requestJson("/health", { method: "GET" });
|
|
348
|
+
return this.requestJson("/health", { method: "GET" }, void 0, HealthResponseSchema);
|
|
59
349
|
}
|
|
60
350
|
async welcome() {
|
|
61
|
-
return this.requestJson("/welcome", { method: "GET" });
|
|
351
|
+
return this.requestJson("/welcome", { method: "GET" }, void 0, WelcomeResponseSchema);
|
|
62
352
|
}
|
|
63
353
|
async onboardingStart(input) {
|
|
64
|
-
return this.requestJson(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
354
|
+
return this.requestJson(
|
|
355
|
+
"/v1/onboarding/start",
|
|
356
|
+
{
|
|
357
|
+
method: "POST",
|
|
358
|
+
headers: { "content-type": "application/json" }
|
|
359
|
+
},
|
|
360
|
+
OnboardingStartRequestSchema,
|
|
361
|
+
OnboardingStartResponseSchema,
|
|
362
|
+
input
|
|
363
|
+
);
|
|
69
364
|
}
|
|
70
365
|
async onboardingComplete(input) {
|
|
71
|
-
return this.requestJson(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
366
|
+
return this.requestJson(
|
|
367
|
+
"/v1/onboarding/complete",
|
|
368
|
+
{
|
|
369
|
+
method: "POST",
|
|
370
|
+
headers: { "content-type": "application/json" }
|
|
371
|
+
},
|
|
372
|
+
OnboardingCompleteRequestSchema,
|
|
373
|
+
OnboardingCompleteResponseSchema,
|
|
374
|
+
input
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
async registerDevice(input, options) {
|
|
378
|
+
return this.requestJson(
|
|
379
|
+
"/v1/devices/register",
|
|
380
|
+
{
|
|
381
|
+
method: "POST",
|
|
382
|
+
headers: {
|
|
383
|
+
"content-type": "application/json",
|
|
384
|
+
authorization: `Bearer ${options.accessToken}`
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
RegisterDeviceRequestSchema,
|
|
388
|
+
RegisterDeviceResponseSchema,
|
|
389
|
+
input
|
|
390
|
+
);
|
|
83
391
|
}
|
|
84
392
|
async authRefresh(input) {
|
|
85
|
-
return this.requestJson(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
393
|
+
return this.requestJson(
|
|
394
|
+
"/v1/auth/refresh",
|
|
395
|
+
{
|
|
396
|
+
method: "POST",
|
|
397
|
+
headers: { "content-type": "application/json" }
|
|
398
|
+
},
|
|
399
|
+
AuthRefreshRequestSchema,
|
|
400
|
+
AuthRefreshResponseSchema,
|
|
401
|
+
input
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
async authLogout(input, options) {
|
|
405
|
+
return this.requestJson(
|
|
406
|
+
"/v1/auth/logout",
|
|
407
|
+
{
|
|
408
|
+
method: "POST",
|
|
409
|
+
headers: {
|
|
410
|
+
"content-type": "application/json",
|
|
411
|
+
authorization: `Bearer ${options.accessToken}`
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
AuthLogoutRequestSchema,
|
|
415
|
+
OkResponseSchema,
|
|
416
|
+
input
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
async pinSet(input, options) {
|
|
420
|
+
return this.requestJson(
|
|
421
|
+
"/v1/auth/pin/set",
|
|
422
|
+
{
|
|
423
|
+
method: "POST",
|
|
424
|
+
headers: {
|
|
425
|
+
"content-type": "application/json",
|
|
426
|
+
authorization: `Bearer ${options.accessToken}`
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
PinSetRequestSchema,
|
|
430
|
+
OkResponseSchema,
|
|
431
|
+
input
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
async pinVerify(input, options) {
|
|
435
|
+
return this.requestJson(
|
|
436
|
+
"/v1/auth/pin/verify",
|
|
437
|
+
{
|
|
438
|
+
method: "POST",
|
|
439
|
+
headers: {
|
|
440
|
+
"content-type": "application/json",
|
|
441
|
+
authorization: `Bearer ${options.accessToken}`
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
PinVerifyRequestSchema,
|
|
445
|
+
OkResponseSchema,
|
|
446
|
+
input
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
async registerSendDeviceKey(input, options) {
|
|
450
|
+
return this.requestJson(
|
|
451
|
+
"/api/v1/auth/send/device-key",
|
|
452
|
+
{
|
|
453
|
+
method: "POST",
|
|
454
|
+
headers: {
|
|
455
|
+
"content-type": "application/json",
|
|
456
|
+
authorization: `Bearer ${options.accessToken}`
|
|
457
|
+
}
|
|
458
|
+
},
|
|
459
|
+
RegisterSendDeviceKeyRequestSchema,
|
|
460
|
+
OkResponseSchema,
|
|
461
|
+
input
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
async createSendChallenge(input, options) {
|
|
465
|
+
return this.requestJson(
|
|
466
|
+
"/api/v1/auth/send/challenge",
|
|
467
|
+
{
|
|
468
|
+
method: "POST",
|
|
469
|
+
headers: {
|
|
470
|
+
"content-type": "application/json",
|
|
471
|
+
authorization: `Bearer ${options.accessToken}`
|
|
472
|
+
}
|
|
473
|
+
},
|
|
474
|
+
SendChallengeRequestSchema,
|
|
475
|
+
SendChallengeResponseSchema,
|
|
476
|
+
input
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
async verifySendChallenge(input, options) {
|
|
480
|
+
return this.requestJson(
|
|
481
|
+
"/api/v1/auth/send/verify",
|
|
482
|
+
{
|
|
483
|
+
method: "POST",
|
|
484
|
+
headers: {
|
|
485
|
+
"content-type": "application/json",
|
|
486
|
+
authorization: `Bearer ${options.accessToken}`
|
|
487
|
+
}
|
|
488
|
+
},
|
|
489
|
+
SendVerifyRequestSchema,
|
|
490
|
+
SendVerifyResponseSchema,
|
|
491
|
+
input
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
async registerBiometricDeviceKey(input) {
|
|
495
|
+
const publicKey = await input.signer.getOrCreateKeyPair(input.deviceId);
|
|
496
|
+
return this.registerSendDeviceKey({
|
|
497
|
+
userId: input.userId,
|
|
498
|
+
deviceId: input.deviceId,
|
|
499
|
+
publicKey
|
|
500
|
+
}, { accessToken: input.accessToken });
|
|
501
|
+
}
|
|
502
|
+
async authorizeSendWithBiometric(input) {
|
|
503
|
+
const challenge = await this.createSendChallenge({
|
|
504
|
+
userId: input.userId,
|
|
505
|
+
deviceId: input.deviceId
|
|
506
|
+
}, { accessToken: input.accessToken });
|
|
507
|
+
const signature = await input.signer.sign(challenge.nonce);
|
|
508
|
+
return this.verifySendChallenge({
|
|
509
|
+
userId: input.userId,
|
|
510
|
+
deviceId: input.deviceId,
|
|
511
|
+
challengeId: challenge.challengeId,
|
|
512
|
+
signature
|
|
513
|
+
}, { accessToken: input.accessToken });
|
|
514
|
+
}
|
|
515
|
+
async resolveRecipient(input, options) {
|
|
516
|
+
return this.requestJson(
|
|
517
|
+
"/api/v1/recipients/resolve",
|
|
518
|
+
{
|
|
519
|
+
method: "POST",
|
|
520
|
+
headers: {
|
|
521
|
+
"content-type": "application/json",
|
|
522
|
+
authorization: `Bearer ${options.accessToken}`
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
ResolveRecipientRequestSchema,
|
|
526
|
+
ResolveRecipientResponseSchema,
|
|
527
|
+
input
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
async createTransfer(input, options) {
|
|
531
|
+
return this.requestJson(
|
|
532
|
+
"/api/v1/transfers",
|
|
533
|
+
{
|
|
534
|
+
method: "POST",
|
|
535
|
+
headers: {
|
|
536
|
+
"content-type": "application/json",
|
|
537
|
+
authorization: `Bearer ${options.accessToken}`,
|
|
538
|
+
"x-device-id": options.deviceId,
|
|
539
|
+
"x-idempotency-key": options.idempotencyKey
|
|
540
|
+
}
|
|
541
|
+
},
|
|
542
|
+
CreateTransferRequestSchema,
|
|
543
|
+
TransferResponseSchema,
|
|
544
|
+
input
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
async sendMoney(input, options) {
|
|
548
|
+
const normalizedRecipient = E164Regex.test(input.recipientIdentifier) ? input.recipientIdentifier : normalizeE164(input.recipientIdentifier, input.defaultCountry);
|
|
549
|
+
const idempotencyKey = input.idempotencyKey ?? getSecureRandomUuid();
|
|
550
|
+
return this.createTransfer(
|
|
551
|
+
{
|
|
552
|
+
recipientIdentifier: normalizedRecipient,
|
|
553
|
+
amountMinor: moneyMinorToNumber(input.money.amountMinor),
|
|
554
|
+
currency: input.money.currency,
|
|
555
|
+
sendAuthToken: input.sendAuthToken
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
accessToken: options.accessToken,
|
|
559
|
+
deviceId: options.deviceId,
|
|
560
|
+
idempotencyKey
|
|
561
|
+
}
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
async accountSummary(options) {
|
|
565
|
+
return this.requestJson(
|
|
566
|
+
"/api/v1/account/summary",
|
|
567
|
+
{
|
|
568
|
+
method: "GET",
|
|
569
|
+
headers: {
|
|
570
|
+
authorization: `Bearer ${options.accessToken}`
|
|
571
|
+
}
|
|
572
|
+
},
|
|
573
|
+
void 0,
|
|
574
|
+
AccountSummaryResponseSchema
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
async listTransactions(options) {
|
|
578
|
+
const query = new URLSearchParams();
|
|
579
|
+
if (typeof options.cursor === "string" && options.cursor.trim().length > 0) {
|
|
580
|
+
query.set("cursor", options.cursor);
|
|
581
|
+
}
|
|
582
|
+
if (typeof options.limit === "number") {
|
|
583
|
+
query.set("limit", String(options.limit));
|
|
584
|
+
}
|
|
585
|
+
const path = query.size > 0 ? `/api/v1/transactions?${query.toString()}` : "/api/v1/transactions";
|
|
586
|
+
return this.requestJson(
|
|
587
|
+
path,
|
|
588
|
+
{
|
|
589
|
+
method: "GET",
|
|
590
|
+
headers: {
|
|
591
|
+
authorization: `Bearer ${options.accessToken}`
|
|
592
|
+
}
|
|
593
|
+
},
|
|
594
|
+
void 0,
|
|
595
|
+
TransactionsListResponseSchema
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
async transactionDetail(transactionId, options) {
|
|
599
|
+
const encodedId = encodeURIComponent(transactionId);
|
|
600
|
+
return this.requestJson(
|
|
601
|
+
`/api/v1/transactions/${encodedId}`,
|
|
602
|
+
{
|
|
603
|
+
method: "GET",
|
|
604
|
+
headers: {
|
|
605
|
+
authorization: `Bearer ${options.accessToken}`
|
|
606
|
+
}
|
|
607
|
+
},
|
|
608
|
+
void 0,
|
|
609
|
+
TransactionDetailResponseSchema
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
async registerPushToken(input, options) {
|
|
613
|
+
return this.requestJson(
|
|
614
|
+
"/api/v1/push/register",
|
|
615
|
+
{
|
|
616
|
+
method: "POST",
|
|
617
|
+
headers: {
|
|
618
|
+
"content-type": "application/json",
|
|
619
|
+
authorization: `Bearer ${options.accessToken}`
|
|
620
|
+
}
|
|
621
|
+
},
|
|
622
|
+
PushRegisterRequestSchema,
|
|
623
|
+
OkResponseSchema,
|
|
624
|
+
input
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
async requestJson(path, init, requestSchema, responseSchema, input) {
|
|
113
628
|
const url = `${this.baseUrl}${path}`;
|
|
114
629
|
const controller = new AbortController();
|
|
115
630
|
const t = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
631
|
+
let body = init.body;
|
|
632
|
+
try {
|
|
633
|
+
body = requestSchema ? JSON.stringify(requestSchema.parse(input)) : init.body;
|
|
634
|
+
} catch (err) {
|
|
635
|
+
if (err instanceof z3.ZodError) {
|
|
636
|
+
throw new FlurError("Invalid request payload", "INVALID_REQUEST", {
|
|
637
|
+
details: err.flatten()
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
throw err;
|
|
641
|
+
}
|
|
116
642
|
try {
|
|
117
|
-
const res = await this.fetchImpl(url, { ...init, signal: controller.signal });
|
|
643
|
+
const res = await this.fetchImpl(url, { ...init, body, signal: controller.signal });
|
|
118
644
|
if (!res.ok) throw await mapToFlurError(res);
|
|
119
|
-
|
|
645
|
+
const payload = await res.json();
|
|
646
|
+
if (!responseSchema) return payload;
|
|
647
|
+
try {
|
|
648
|
+
return responseSchema.parse(payload);
|
|
649
|
+
} catch (err) {
|
|
650
|
+
if (err instanceof z3.ZodError) {
|
|
651
|
+
throw new FlurError("SDK contract validation failed", "INVALID_REQUEST", {
|
|
652
|
+
details: err.flatten()
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
throw err;
|
|
656
|
+
}
|
|
120
657
|
} catch (err) {
|
|
121
658
|
if (err instanceof Error && err.name === "AbortError") {
|
|
122
659
|
throw new FlurError("Request timed out", "TIMEOUT");
|
|
@@ -128,7 +665,17 @@ var FlurClient = class {
|
|
|
128
665
|
}
|
|
129
666
|
}
|
|
130
667
|
};
|
|
668
|
+
function getSecureRandomUuid() {
|
|
669
|
+
if (typeof globalThis.crypto?.randomUUID === "function") {
|
|
670
|
+
return globalThis.crypto.randomUUID();
|
|
671
|
+
}
|
|
672
|
+
throw new FlurError("Secure UUID generator unavailable; provide idempotencyKey", "INVALID_REQUEST");
|
|
673
|
+
}
|
|
131
674
|
export {
|
|
132
675
|
FlurClient,
|
|
133
|
-
FlurError
|
|
676
|
+
FlurError,
|
|
677
|
+
formatAmount,
|
|
678
|
+
moneyMinorToNumber,
|
|
679
|
+
normalizeE164,
|
|
680
|
+
parseAmountInput
|
|
134
681
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nokinc-flur/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Flur Wallet SDK (sprint 1 scaffold)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -22,8 +22,12 @@
|
|
|
22
22
|
"lint": "eslint .",
|
|
23
23
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
24
24
|
"test": "vitest run",
|
|
25
|
+
"test:contract": "vitest run test/e2e.health.test.ts test/e2e.send-money.test.ts",
|
|
26
|
+
"e2e:bootstrap": "node scripts/bootstrap-e2e-sendmoney.mjs",
|
|
25
27
|
"build": "tsup src/index.ts --format esm --dts",
|
|
26
28
|
"gen": "openapi-typescript openapi/flur.openapi.json -o src/gen/openapi-types.ts",
|
|
29
|
+
"prod:release": "pwsh -NoProfile -ExecutionPolicy Bypass -File ./scripts/prod-build-publish-npm.ps1",
|
|
30
|
+
"prod:release:publish": "pwsh -NoProfile -ExecutionPolicy Bypass -File ./scripts/prod-build-publish-npm.ps1 -Publish",
|
|
27
31
|
"release:patch": "pwsh -NoProfile -ExecutionPolicy Bypass -File ./scripts/release.ps1 -Bump patch",
|
|
28
32
|
"release:minor": "pwsh -NoProfile -ExecutionPolicy Bypass -File ./scripts/release.ps1 -Bump minor",
|
|
29
33
|
"release:major": "pwsh -NoProfile -ExecutionPolicy Bypass -File ./scripts/release.ps1 -Bump major"
|