clearbank-sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/LICENSE +21 -0
- package/README.md +295 -0
- package/dist/cjs/core/config.js +25 -0
- package/dist/cjs/core/config.js.map +1 -0
- package/dist/cjs/core/crypto.js +110 -0
- package/dist/cjs/core/crypto.js.map +1 -0
- package/dist/cjs/core/http.js +191 -0
- package/dist/cjs/core/http.js.map +1 -0
- package/dist/cjs/core/index.js +14 -0
- package/dist/cjs/core/index.js.map +1 -0
- package/dist/cjs/core/retry.js +42 -0
- package/dist/cjs/core/retry.js.map +1 -0
- package/dist/cjs/domains/accounts/index.js +6 -0
- package/dist/cjs/domains/accounts/index.js.map +1 -0
- package/dist/cjs/domains/accounts/service.js +216 -0
- package/dist/cjs/domains/accounts/service.js.map +1 -0
- package/dist/cjs/domains/accounts/types.js +3 -0
- package/dist/cjs/domains/accounts/types.js.map +1 -0
- package/dist/cjs/domains/embedded/index.js +6 -0
- package/dist/cjs/domains/embedded/index.js.map +1 -0
- package/dist/cjs/domains/embedded/service.js +215 -0
- package/dist/cjs/domains/embedded/service.js.map +1 -0
- package/dist/cjs/domains/embedded/types.js +3 -0
- package/dist/cjs/domains/embedded/types.js.map +1 -0
- package/dist/cjs/domains/multicurrency/index.js +6 -0
- package/dist/cjs/domains/multicurrency/index.js.map +1 -0
- package/dist/cjs/domains/multicurrency/service.js +202 -0
- package/dist/cjs/domains/multicurrency/service.js.map +1 -0
- package/dist/cjs/domains/multicurrency/types.js +3 -0
- package/dist/cjs/domains/multicurrency/types.js.map +1 -0
- package/dist/cjs/domains/payments/index.js +6 -0
- package/dist/cjs/domains/payments/index.js.map +1 -0
- package/dist/cjs/domains/payments/service.js +255 -0
- package/dist/cjs/domains/payments/service.js.map +1 -0
- package/dist/cjs/domains/payments/types.js +3 -0
- package/dist/cjs/domains/payments/types.js.map +1 -0
- package/dist/cjs/domains/webhooks/index.js +7 -0
- package/dist/cjs/domains/webhooks/index.js.map +1 -0
- package/dist/cjs/domains/webhooks/service.js +173 -0
- package/dist/cjs/domains/webhooks/service.js.map +1 -0
- package/dist/cjs/domains/webhooks/types.js +8 -0
- package/dist/cjs/domains/webhooks/types.js.map +1 -0
- package/dist/cjs/index.js +73 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/types/index.js +45 -0
- package/dist/cjs/types/index.js.map +1 -0
- package/dist/cjs/utils/index.js +46 -0
- package/dist/cjs/utils/index.js.map +1 -0
- package/dist/esm/core/config.js +22 -0
- package/dist/esm/core/config.js.map +1 -0
- package/dist/esm/core/crypto.js +105 -0
- package/dist/esm/core/crypto.js.map +1 -0
- package/dist/esm/core/http.js +187 -0
- package/dist/esm/core/http.js.map +1 -0
- package/dist/esm/core/index.js +5 -0
- package/dist/esm/core/index.js.map +1 -0
- package/dist/esm/core/retry.js +39 -0
- package/dist/esm/core/retry.js.map +1 -0
- package/dist/esm/domains/accounts/index.js +2 -0
- package/dist/esm/domains/accounts/index.js.map +1 -0
- package/dist/esm/domains/accounts/service.js +212 -0
- package/dist/esm/domains/accounts/service.js.map +1 -0
- package/dist/esm/domains/accounts/types.js +2 -0
- package/dist/esm/domains/accounts/types.js.map +1 -0
- package/dist/esm/domains/embedded/index.js +2 -0
- package/dist/esm/domains/embedded/index.js.map +1 -0
- package/dist/esm/domains/embedded/service.js +211 -0
- package/dist/esm/domains/embedded/service.js.map +1 -0
- package/dist/esm/domains/embedded/types.js +2 -0
- package/dist/esm/domains/embedded/types.js.map +1 -0
- package/dist/esm/domains/multicurrency/index.js +2 -0
- package/dist/esm/domains/multicurrency/index.js.map +1 -0
- package/dist/esm/domains/multicurrency/service.js +198 -0
- package/dist/esm/domains/multicurrency/service.js.map +1 -0
- package/dist/esm/domains/multicurrency/types.js +2 -0
- package/dist/esm/domains/multicurrency/types.js.map +1 -0
- package/dist/esm/domains/payments/index.js +2 -0
- package/dist/esm/domains/payments/index.js.map +1 -0
- package/dist/esm/domains/payments/service.js +251 -0
- package/dist/esm/domains/payments/service.js.map +1 -0
- package/dist/esm/domains/payments/types.js +2 -0
- package/dist/esm/domains/payments/types.js.map +1 -0
- package/dist/esm/domains/webhooks/index.js +2 -0
- package/dist/esm/domains/webhooks/index.js.map +1 -0
- package/dist/esm/domains/webhooks/service.js +168 -0
- package/dist/esm/domains/webhooks/service.js.map +1 -0
- package/dist/esm/domains/webhooks/types.js +5 -0
- package/dist/esm/domains/webhooks/types.js.map +1 -0
- package/dist/esm/index.js +56 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/types/index.js +41 -0
- package/dist/esm/types/index.js.map +1 -0
- package/dist/esm/utils/index.js +35 -0
- package/dist/esm/utils/index.js.map +1 -0
- package/dist/types/core/config.d.ts +34 -0
- package/dist/types/core/config.d.ts.map +1 -0
- package/dist/types/core/crypto.d.ts +20 -0
- package/dist/types/core/crypto.d.ts.map +1 -0
- package/dist/types/core/http.d.ts +34 -0
- package/dist/types/core/http.d.ts.map +1 -0
- package/dist/types/core/index.d.ts +7 -0
- package/dist/types/core/index.d.ts.map +1 -0
- package/dist/types/core/retry.d.ts +14 -0
- package/dist/types/core/retry.d.ts.map +1 -0
- package/dist/types/domains/accounts/index.d.ts +3 -0
- package/dist/types/domains/accounts/index.d.ts.map +1 -0
- package/dist/types/domains/accounts/service.d.ts +61 -0
- package/dist/types/domains/accounts/service.d.ts.map +1 -0
- package/dist/types/domains/accounts/types.d.ts +90 -0
- package/dist/types/domains/accounts/types.d.ts.map +1 -0
- package/dist/types/domains/embedded/index.d.ts +3 -0
- package/dist/types/domains/embedded/index.d.ts.map +1 -0
- package/dist/types/domains/embedded/service.d.ts +63 -0
- package/dist/types/domains/embedded/service.d.ts.map +1 -0
- package/dist/types/domains/embedded/types.d.ts +148 -0
- package/dist/types/domains/embedded/types.d.ts.map +1 -0
- package/dist/types/domains/multicurrency/index.d.ts +3 -0
- package/dist/types/domains/multicurrency/index.d.ts.map +1 -0
- package/dist/types/domains/multicurrency/service.d.ts +50 -0
- package/dist/types/domains/multicurrency/service.d.ts.map +1 -0
- package/dist/types/domains/multicurrency/types.d.ts +84 -0
- package/dist/types/domains/multicurrency/types.d.ts.map +1 -0
- package/dist/types/domains/payments/index.d.ts +3 -0
- package/dist/types/domains/payments/index.d.ts.map +1 -0
- package/dist/types/domains/payments/service.d.ts +65 -0
- package/dist/types/domains/payments/service.d.ts.map +1 -0
- package/dist/types/domains/payments/types.d.ts +107 -0
- package/dist/types/domains/payments/types.d.ts.map +1 -0
- package/dist/types/domains/webhooks/index.d.ts +3 -0
- package/dist/types/domains/webhooks/index.d.ts.map +1 -0
- package/dist/types/domains/webhooks/service.d.ts +66 -0
- package/dist/types/domains/webhooks/service.d.ts.map +1 -0
- package/dist/types/domains/webhooks/types.d.ts +215 -0
- package/dist/types/domains/webhooks/types.d.ts.map +1 -0
- package/dist/types/index.d.ts +54 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/types/index.d.ts +77 -0
- package/dist/types/types/index.d.ts.map +1 -0
- package/dist/types/utils/index.d.ts +22 -0
- package/dist/types/utils/index.d.ts.map +1 -0
- package/package.json +96 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `clearbank-sdk` are documented here.
|
|
4
|
+
|
|
5
|
+
## [1.0.0] - 2026-06-09
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Initial release
|
|
9
|
+
- **GBP Accounts** — real and virtual account management, transactions, Bacs DDIs, camt.053 statements
|
|
10
|
+
- **Payments** — FPS (single + bulk), CHAPS (customer credit + return), Bacs returns, cheque deposits (ICS), internal transfers (single + bulk), CoP outbound name verification, SEPA SCT UK, GBP Cross-Border (deprecated)
|
|
11
|
+
- **Multi-currency & FX** — MCCY account management, virtual accounts, international payment batches, FX spot trading, FX RFQ (request-for-quote), SEPA SCT UK
|
|
12
|
+
- **Embedded Banking** — retail/sole-trader/legal-entity customer management, payment/savings/hub/ISA accounts, embedded FPS, ISA transfer-in, interest products, KYC submission and status
|
|
13
|
+
- **Webhooks** — RSA-SHA256 signature verification, typed event parsing, `dispatch()` router for all 19 event types, `buildAck()` helper
|
|
14
|
+
- RSA-SHA256 `DigitalSignature` signing via Web Crypto API (PKCS#1 v1.5 + SHA-256)
|
|
15
|
+
- Full-jitter exponential backoff retries for 429/500/503
|
|
16
|
+
- Pinnable `X-Request-Id` for idempotent mutation retries
|
|
17
|
+
- Telemetry hook for structured observability events
|
|
18
|
+
- Dual CJS/ESM output with TypeScript declaration files
|
|
19
|
+
- 100+ unit tests using Vitest + MSW
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kanishka Naik
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# clearbank-sdk
|
|
2
|
+
|
|
3
|
+
> Production-grade TypeScript SDK for the [ClearBank UK API](https://docs.clear.bank/)
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/clearbank-sdk)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Full API coverage** — GBP Accounts, FPS/CHAPS/Bacs/Cheques/CoP Payments, Multi-currency & FX, Embedded Banking, Webhooks
|
|
12
|
+
- **TypeScript-first** — Complete type definitions for every request, response, and webhook event
|
|
13
|
+
- **Dual CJS/ESM output** — Works in Node.js, Bun, Deno, and modern bundlers
|
|
14
|
+
- **RSA-SHA256 signing** — Automatic `DigitalSignature` header using Web Crypto API (Node 18+)
|
|
15
|
+
- **Idempotent retries** — Exponential backoff with full jitter; pinnable `X-Request-Id` for safe mutation retries
|
|
16
|
+
- **Telemetry hooks** — Emit structured events to any observability provider
|
|
17
|
+
- **Zero mandatory dependencies** — Uses native `fetch` and Web Crypto
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install clearbank-sdk
|
|
23
|
+
# or
|
|
24
|
+
pnpm add clearbank-sdk
|
|
25
|
+
# or
|
|
26
|
+
yarn add clearbank-sdk
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Requires Node.js ≥ 18** (for native `fetch` and `crypto.subtle`).
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { ClearBankClient } from 'clearbank-sdk';
|
|
35
|
+
|
|
36
|
+
const client = new ClearBankClient({
|
|
37
|
+
apiToken: process.env.CLEARBANK_API_TOKEN!,
|
|
38
|
+
privateKey: process.env.CLEARBANK_PRIVATE_KEY!, // PEM string
|
|
39
|
+
environment: 'simulation', // or 'production'
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// List GBP accounts
|
|
43
|
+
const { data: accounts } = await client.accounts.list({ pageNumber: 1, pageSize: 20 });
|
|
44
|
+
|
|
45
|
+
// Send a Faster Payment
|
|
46
|
+
await client.payments.sendFPS({
|
|
47
|
+
accountId: 'your-account-id',
|
|
48
|
+
payment: {
|
|
49
|
+
amount: '250.00',
|
|
50
|
+
destinationSortCode: '040004',
|
|
51
|
+
destinationAccountNumber: '12345678',
|
|
52
|
+
destinationAccountName: 'Jane Smith',
|
|
53
|
+
reference: 'Invoice 001',
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Execute an FX trade
|
|
58
|
+
const trade = await client.multiCurrency.executeFXTrade({
|
|
59
|
+
sellAccountId: 'gbp-account-id',
|
|
60
|
+
buyAccountId: 'eur-account-id',
|
|
61
|
+
sellCurrency: 'GBP',
|
|
62
|
+
buyCurrency: 'EUR',
|
|
63
|
+
sellAmount: '10000.00',
|
|
64
|
+
});
|
|
65
|
+
console.log(`Traded at rate: ${trade.rate}`);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Configuration
|
|
69
|
+
|
|
70
|
+
| Option | Type | Default | Description |
|
|
71
|
+
|---|---|---|---|
|
|
72
|
+
| `apiToken` | `string` | **required** | Bearer token from ClearBank Portal |
|
|
73
|
+
| `privateKey` | `string` | — | RSA private key PEM (required for POST/PUT/PATCH) |
|
|
74
|
+
| `environment` | `'simulation' \| 'production'` | `'simulation'` | API environment |
|
|
75
|
+
| `baseUrl` | `string` | — | Override base URL (useful for testing) |
|
|
76
|
+
| `timeoutMs` | `number` | `30000` | Request timeout in milliseconds |
|
|
77
|
+
| `maxRetries` | `number` | `3` | Max retry attempts on 429/500/503 |
|
|
78
|
+
| `retryBaseDelayMs` | `number` | `1000` | Backoff base delay in milliseconds |
|
|
79
|
+
| `telemetryHook` | `TelemetryHook` | — | Called after every HTTP request |
|
|
80
|
+
|
|
81
|
+
## Error Handling
|
|
82
|
+
|
|
83
|
+
All API errors are thrown as `ClearBankError`:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { ClearBankError } from 'clearbank-sdk';
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
await client.accounts.get('nonexistent-id');
|
|
90
|
+
} catch (err) {
|
|
91
|
+
if (err instanceof ClearBankError) {
|
|
92
|
+
console.log(err.statusCode); // 404
|
|
93
|
+
console.log(err.message); // "Not Found"
|
|
94
|
+
console.log(err.correlationId); // X-Correlation-Id for support
|
|
95
|
+
console.log(err.isNotFound()); // true
|
|
96
|
+
console.log(err.isRetryable()); // false
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### ClearBankError methods
|
|
102
|
+
|
|
103
|
+
| Method | Returns | Description |
|
|
104
|
+
|---|---|---|
|
|
105
|
+
| `isNotFound()` | `boolean` | HTTP 404 |
|
|
106
|
+
| `isConflict()` | `boolean` | HTTP 409 (duplicate X-Request-Id) |
|
|
107
|
+
| `isRateLimited()` | `boolean` | HTTP 429 |
|
|
108
|
+
| `isUnprocessable()` | `boolean` | HTTP 422 |
|
|
109
|
+
| `isRetryable()` | `boolean` | HTTP 429, 500, or 503 |
|
|
110
|
+
|
|
111
|
+
## Idempotent Retries
|
|
112
|
+
|
|
113
|
+
ClearBank requires mutating requests (POST/PUT/PATCH) that fail with 5XX to be retried with the **same** `X-Request-Id`:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
const requestId = ClearBankClient.generateRequestId();
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
await client.payments.sendFPS(params, { requestId });
|
|
120
|
+
} catch (err) {
|
|
121
|
+
if (err instanceof ClearBankError && err.isRetryable()) {
|
|
122
|
+
// Retry with identical requestId — ClearBank deduplicates on their side
|
|
123
|
+
await client.payments.sendFPS(params, { requestId });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Webhooks
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import express from 'express';
|
|
132
|
+
|
|
133
|
+
const app = express();
|
|
134
|
+
app.use(express.json());
|
|
135
|
+
|
|
136
|
+
app.post('/webhooks', async (req, res) => {
|
|
137
|
+
// 1. Verify signature (using ClearBank's public key from Portal)
|
|
138
|
+
const rawBody = JSON.stringify(req.body);
|
|
139
|
+
const isValid = await client.webhooks.verifySignature(
|
|
140
|
+
rawBody,
|
|
141
|
+
req.headers['digitalsignature'] as string,
|
|
142
|
+
process.env.CLEARBANK_PUBLIC_KEY!,
|
|
143
|
+
);
|
|
144
|
+
if (!isValid) return res.status(401).json({ error: 'Invalid signature' });
|
|
145
|
+
|
|
146
|
+
// 2. Parse and dispatch
|
|
147
|
+
const envelope = client.webhooks.parseEnvelope(req.body);
|
|
148
|
+
await client.webhooks.dispatch(envelope, {
|
|
149
|
+
TransactionSettled: async (event) => {
|
|
150
|
+
console.log(`Payment settled: £${event.amount} on account ${event.accountId}`);
|
|
151
|
+
await yourDatabase.recordSettlement(event);
|
|
152
|
+
},
|
|
153
|
+
FxTradeSettled: async (event) => {
|
|
154
|
+
console.log(`FX trade ${event.tradeId} settled`);
|
|
155
|
+
},
|
|
156
|
+
CustomerKycStatusChanged: async (event) => {
|
|
157
|
+
if (event.newStatus === 'Approved') {
|
|
158
|
+
await activateCustomerAccount(event.customerId);
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
onUnknown: (envelope) => {
|
|
162
|
+
console.log('Unknown webhook event:', envelope.Type);
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// 3. Acknowledge
|
|
167
|
+
res.json(client.webhooks.buildAck(envelope));
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Domain Services
|
|
172
|
+
|
|
173
|
+
### `client.accounts` — GBP Accounts
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
// Real accounts
|
|
177
|
+
await client.accounts.list({ pageNumber: 1, pageSize: 50 });
|
|
178
|
+
await client.accounts.get(accountId);
|
|
179
|
+
await client.accounts.create({ accountName: 'Segregated Pool', accountType: 'SegregatedPooled' });
|
|
180
|
+
await client.accounts.update(accountId, { accountName: 'Renamed', copEnabled: true });
|
|
181
|
+
|
|
182
|
+
// Virtual accounts
|
|
183
|
+
await client.accounts.listVirtual(accountId);
|
|
184
|
+
await client.accounts.createVirtual(accountId, { accountName: 'Customer 1', owner: customerId });
|
|
185
|
+
await client.accounts.getVirtual(accountId, virtualAccountId);
|
|
186
|
+
|
|
187
|
+
// Transactions
|
|
188
|
+
await client.accounts.listAllTransactions({ startDate: '2024-01-01', endDate: '2024-01-31' });
|
|
189
|
+
await client.accounts.listTransactions(accountId, { pageSize: 100 });
|
|
190
|
+
await client.accounts.getTransaction(accountId, transactionId);
|
|
191
|
+
|
|
192
|
+
// Bacs DDIs
|
|
193
|
+
await client.accounts.createDDI(accountId, { serviceUserNumber, reference, payerName, payerSortCode, payerAccountNumber });
|
|
194
|
+
await client.accounts.listDDIs(accountId);
|
|
195
|
+
await client.accounts.cancelDDI(accountId, mandateId);
|
|
196
|
+
|
|
197
|
+
// camt.053 statements
|
|
198
|
+
const messageId = await client.accounts.requestStatement({ accountId, startDate, endDate });
|
|
199
|
+
const allPages = await client.accounts.getAllStatementPages(messageId);
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### `client.payments` — GBP Payments
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
// Faster Payments
|
|
206
|
+
await client.payments.sendFPS({ accountId, payment: { amount, destinationSortCode, ... } });
|
|
207
|
+
await client.payments.sendFPSBulk({ accountId, payments: [...] });
|
|
208
|
+
|
|
209
|
+
// CHAPS
|
|
210
|
+
await client.payments.sendCHAPS({ debtorAccountId, amount, creditorName, creditorAddress: { country: 'GB' }, ... });
|
|
211
|
+
await client.payments.returnCHAPS({ originalInstructionId, debtorAccountId, returnReasonCode, amount });
|
|
212
|
+
|
|
213
|
+
// Internal transfers
|
|
214
|
+
await client.payments.sendInternalTransfer({ debtorAccountId, creditorAccountId, amount });
|
|
215
|
+
await client.payments.sendBulkInternalTransfer([...transfers]);
|
|
216
|
+
|
|
217
|
+
// Bacs returns
|
|
218
|
+
await client.payments.returnBacs(accountId, { transactionId, reasonCode });
|
|
219
|
+
|
|
220
|
+
// Cheques
|
|
221
|
+
await client.payments.submitChequeDeposit({ accountId, amount, chequeImageFront, chequeImageBack, micrLine });
|
|
222
|
+
|
|
223
|
+
// Confirmation of Payee
|
|
224
|
+
const result = await client.payments.checkCoP({ accountName, sortCode, accountNumber });
|
|
225
|
+
// result.matchResult: 'MATC' | 'CLOSE' | 'NOMATCH' | 'INAM' | 'PANM'
|
|
226
|
+
await client.payments.optOutAccountCoP(accountId);
|
|
227
|
+
|
|
228
|
+
// SEPA SCT UK
|
|
229
|
+
await client.payments.sendSEPA({ debtorAccountId, amount, creditorName, creditorIban, creditorBic });
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### `client.multiCurrency` — Multi-currency & FX
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
// Accounts
|
|
236
|
+
await client.multiCurrency.listAccounts('EUR');
|
|
237
|
+
await client.multiCurrency.createAccount({ accountName, accountType: 'YourFunds', currency: 'EUR' });
|
|
238
|
+
|
|
239
|
+
// International payments
|
|
240
|
+
await client.multiCurrency.sendPayment({ accountId, amount, currency, creditorName, creditorIban, creditorBic });
|
|
241
|
+
await client.multiCurrency.sendBulkPayments([...payments]);
|
|
242
|
+
|
|
243
|
+
// FX Spot
|
|
244
|
+
const trade = await client.multiCurrency.executeFXTrade({ sellAccountId, buyAccountId, sellCurrency, buyCurrency, sellAmount });
|
|
245
|
+
|
|
246
|
+
// FX RFQ
|
|
247
|
+
const quote = await client.multiCurrency.requestFXQuote({ sellAccountId, buyAccountId, ... });
|
|
248
|
+
if (!quote.isExpired()) {
|
|
249
|
+
const trade = await client.multiCurrency.executeFXQuote(quote.quoteId);
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### `client.embedded` — Embedded Banking
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
// Customers
|
|
257
|
+
await client.embedded.createRetailCustomer({ firstName, lastName, dateOfBirth });
|
|
258
|
+
await client.embedded.createSoleTraderCustomer({ firstName, lastName, dateOfBirth, tradingName });
|
|
259
|
+
await client.embedded.createLegalEntityCustomer({ companyName, registrationNumber, registeredCountry, companyType });
|
|
260
|
+
|
|
261
|
+
// Accounts
|
|
262
|
+
await client.embedded.createPaymentAccount({ customerId, accountName });
|
|
263
|
+
await client.embedded.createSavingsAccount({ customerId, accountName });
|
|
264
|
+
await client.embedded.createISA({ customerId, accountName });
|
|
265
|
+
await client.embedded.sendFPS({ accountId, amount, destinationSortCode, destinationAccountNumber, destinationName });
|
|
266
|
+
|
|
267
|
+
// KYC
|
|
268
|
+
await client.embedded.submitKYC(customerId, { idDocumentType, idDocumentNumber, idDocumentExpiry, idDocumentCountry });
|
|
269
|
+
const status = await client.embedded.getKYCStatus(customerId);
|
|
270
|
+
|
|
271
|
+
// Interest
|
|
272
|
+
const products = await client.embedded.listInterestProducts();
|
|
273
|
+
await client.embedded.configureInterest(accountId, { productId });
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Telemetry
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
const client = new ClearBankClient({
|
|
280
|
+
apiToken: '...',
|
|
281
|
+
telemetryHook: (event) => {
|
|
282
|
+
yourMetrics.histogram('clearbank.request.duration', event.durationMs, {
|
|
283
|
+
method: event.method,
|
|
284
|
+
statusCode: String(event.statusCode ?? 'network_error'),
|
|
285
|
+
});
|
|
286
|
+
if (event.error) {
|
|
287
|
+
yourLogger.error('ClearBank request failed', { error: event.error, requestId: event.requestId });
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## License
|
|
294
|
+
|
|
295
|
+
MIT © [Kanishka Naik](https://github.com/iamkanishka)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveConfig = resolveConfig;
|
|
4
|
+
const BASE_URLS = {
|
|
5
|
+
simulation: 'https://institution-api-sim.clearbank.co.uk',
|
|
6
|
+
production: 'https://institution-api.clearbank.co.uk',
|
|
7
|
+
};
|
|
8
|
+
function resolveConfig(config) {
|
|
9
|
+
if (!config.apiToken || config.apiToken.trim() === '') {
|
|
10
|
+
throw new Error('ClearBank SDK: apiToken is required and must not be empty.');
|
|
11
|
+
}
|
|
12
|
+
const environment = config.environment ?? 'simulation';
|
|
13
|
+
const baseUrl = config.baseUrl ?? BASE_URLS[environment];
|
|
14
|
+
return {
|
|
15
|
+
apiToken: config.apiToken,
|
|
16
|
+
privateKey: config.privateKey ?? null,
|
|
17
|
+
baseUrl,
|
|
18
|
+
timeoutMs: config.timeoutMs ?? 30000,
|
|
19
|
+
maxRetries: config.maxRetries ?? 3,
|
|
20
|
+
retryBaseDelayMs: config.retryBaseDelayMs ?? 1000,
|
|
21
|
+
retryMaxDelayMs: config.retryMaxDelayMs ?? 30000,
|
|
22
|
+
telemetryHook: config.telemetryHook ?? null,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../../src/core/config.ts"],"names":[],"mappings":";;AAyCA,sCAkBC;AAvDD,MAAM,SAAS,GAAgC;IAC7C,UAAU,EAAE,6CAA6C;IACzD,UAAU,EAAE,yCAAyC;CAC7C,CAAC;AAkCX,SAAgB,aAAa,CAAC,MAAuB;IACnD,IAAI,CAAC,MAAM,CAAC,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QACtD,MAAM,IAAI,KAAK,CAAC,4DAA4D,CAAC,CAAC;IAChF,CAAC;IAED,MAAM,WAAW,GAAgB,MAAM,CAAC,WAAW,IAAI,YAAY,CAAC;IACpE,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,IAAI,SAAS,CAAC,WAAW,CAAC,CAAC;IAEzD,OAAO;QACL,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,UAAU,EAAE,MAAM,CAAC,UAAU,IAAI,IAAI;QACrC,OAAO;QACP,SAAS,EAAE,MAAM,CAAC,SAAS,IAAI,KAAM;QACrC,UAAU,EAAE,MAAM,CAAC,UAAU,IAAI,CAAC;QAClC,gBAAgB,EAAE,MAAM,CAAC,gBAAgB,IAAI,IAAK;QAClD,eAAe,EAAE,MAAM,CAAC,eAAe,IAAI,KAAM;QACjD,aAAa,EAAE,MAAM,CAAC,aAAa,IAAI,IAAI;KAC5C,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Cryptographic utilities for the ClearBank SDK.
|
|
4
|
+
*
|
|
5
|
+
* Uses the Web Crypto API (globally available in Node.js ≥ 18 and all modern browsers).
|
|
6
|
+
* A Node.js `crypto` module fallback handles PKCS#1 RSA private keys.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.generateUUID = generateUUID;
|
|
10
|
+
exports.signRequestBody = signRequestBody;
|
|
11
|
+
exports.verifySignature = verifySignature;
|
|
12
|
+
/**
|
|
13
|
+
* Generates a UUID v4 string using cryptographically secure random bytes.
|
|
14
|
+
*/
|
|
15
|
+
function generateUUID() {
|
|
16
|
+
if (typeof globalThis.crypto?.randomUUID === 'function') {
|
|
17
|
+
return globalThis.crypto.randomUUID();
|
|
18
|
+
}
|
|
19
|
+
const bytes = new Uint8Array(16);
|
|
20
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
22
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
24
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
25
|
+
const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, '0'));
|
|
26
|
+
return [
|
|
27
|
+
hex.slice(0, 4).join(''),
|
|
28
|
+
hex.slice(4, 6).join(''),
|
|
29
|
+
hex.slice(6, 8).join(''),
|
|
30
|
+
hex.slice(8, 10).join(''),
|
|
31
|
+
hex.slice(10, 16).join(''),
|
|
32
|
+
].join('-');
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Signs a request body with an RSA private key (PKCS#1 v1.5 + SHA-256).
|
|
36
|
+
* Returns Base64-encoded signature for the `DigitalSignature` header.
|
|
37
|
+
*/
|
|
38
|
+
async function signRequestBody(body, privateKeyPem) {
|
|
39
|
+
const cryptoKey = await importRsaPrivateKey(privateKeyPem);
|
|
40
|
+
const bodyBytes = toArrayBuffer(new TextEncoder().encode(body));
|
|
41
|
+
const signature = await globalThis.crypto.subtle.sign({ name: 'RSASSA-PKCS1-v1_5' }, cryptoKey, bodyBytes);
|
|
42
|
+
return bytesToBase64(new Uint8Array(signature));
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Verifies a ClearBank `DigitalSignature` against the raw body using their RSA public key.
|
|
46
|
+
*/
|
|
47
|
+
async function verifySignature(body, signatureBase64, publicKeyPem) {
|
|
48
|
+
const cryptoKey = await importRsaPublicKey(publicKeyPem);
|
|
49
|
+
const bodyBytes = typeof body === 'string' ? toArrayBuffer(new TextEncoder().encode(body)) : toArrayBuffer(body);
|
|
50
|
+
const sigBytes = toArrayBuffer(base64ToUint8Array(signatureBase64));
|
|
51
|
+
return globalThis.crypto.subtle.verify({ name: 'RSASSA-PKCS1-v1_5' }, cryptoKey, sigBytes, bodyBytes);
|
|
52
|
+
}
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Private helpers
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
async function importRsaPrivateKey(pem) {
|
|
57
|
+
try {
|
|
58
|
+
const der = pemToDer(pem);
|
|
59
|
+
return await globalThis.crypto.subtle.importKey('pkcs8', der, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, false, ['sign']);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Fall back to Node.js crypto for PKCS#1 (BEGIN RSA PRIVATE KEY)
|
|
63
|
+
return importPkcs1ViaNode(pem);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function importRsaPublicKey(pem) {
|
|
67
|
+
const der = pemToDer(pem);
|
|
68
|
+
return globalThis.crypto.subtle.importKey('spki', der, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, false, ['verify']);
|
|
69
|
+
}
|
|
70
|
+
/** Strips PEM headers and returns a plain ArrayBuffer. */
|
|
71
|
+
function pemToDer(pem) {
|
|
72
|
+
const stripped = pem
|
|
73
|
+
.split('\n')
|
|
74
|
+
.filter((line) => !line.startsWith('-----'))
|
|
75
|
+
.join('');
|
|
76
|
+
return toArrayBuffer(base64ToUint8Array(stripped));
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Copies a Uint8Array into a fresh ArrayBuffer.
|
|
80
|
+
* Required because TypeScript's Web Crypto types expect a plain ArrayBuffer,
|
|
81
|
+
* not an ArrayBufferLike (which includes SharedArrayBuffer).
|
|
82
|
+
*/
|
|
83
|
+
function toArrayBuffer(bytes) {
|
|
84
|
+
const buf = new ArrayBuffer(bytes.byteLength);
|
|
85
|
+
new Uint8Array(buf).set(bytes);
|
|
86
|
+
return buf;
|
|
87
|
+
}
|
|
88
|
+
/** Decodes a Base64 string to a Uint8Array. */
|
|
89
|
+
function base64ToUint8Array(b64) {
|
|
90
|
+
const binary = atob(b64);
|
|
91
|
+
const bytes = new Uint8Array(binary.length);
|
|
92
|
+
for (let i = 0; i < binary.length; i++) {
|
|
93
|
+
bytes[i] = binary.charCodeAt(i);
|
|
94
|
+
}
|
|
95
|
+
return bytes;
|
|
96
|
+
}
|
|
97
|
+
/** Encodes a Uint8Array to a Base64 string. */
|
|
98
|
+
function bytesToBase64(bytes) {
|
|
99
|
+
return btoa(Array.from(bytes, (b) => String.fromCharCode(b)).join(''));
|
|
100
|
+
}
|
|
101
|
+
/** Node.js fallback: convert PKCS#1 PEM to Web CryptoKey via the `crypto` built-in. */
|
|
102
|
+
async function importPkcs1ViaNode(pem) {
|
|
103
|
+
const { createPrivateKey } = await Promise.resolve().then(() => require('node:crypto'));
|
|
104
|
+
const nodeKey = createPrivateKey(pem);
|
|
105
|
+
const pkcs8Buffer = nodeKey.export({ type: 'pkcs8', format: 'der' });
|
|
106
|
+
// Slice into a plain ArrayBuffer to satisfy Web Crypto's strict BufferSource constraint.
|
|
107
|
+
const der = pkcs8Buffer.buffer.slice(pkcs8Buffer.byteOffset, pkcs8Buffer.byteOffset + pkcs8Buffer.byteLength);
|
|
108
|
+
return globalThis.crypto.subtle.importKey('pkcs8', der, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, false, ['sign']);
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=crypto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../../../src/core/crypto.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HttpClient = void 0;
|
|
4
|
+
const index_js_1 = require("../types/index.js");
|
|
5
|
+
const crypto_js_1 = require("./crypto.js");
|
|
6
|
+
const retry_js_1 = require("./retry.js");
|
|
7
|
+
const MUTATING_METHODS = new Set(['POST', 'PUT', 'PATCH']);
|
|
8
|
+
/**
|
|
9
|
+
* Core HTTP client. All domain services use this.
|
|
10
|
+
*/
|
|
11
|
+
class HttpClient {
|
|
12
|
+
constructor(config) {
|
|
13
|
+
this.config = config;
|
|
14
|
+
}
|
|
15
|
+
async get(path, params, opts) {
|
|
16
|
+
const url = buildUrl(this.config.baseUrl, path, params);
|
|
17
|
+
return this.request('GET', url, undefined, opts);
|
|
18
|
+
}
|
|
19
|
+
async post(path, body, opts) {
|
|
20
|
+
return this.request('POST', buildUrl(this.config.baseUrl, path), body, opts);
|
|
21
|
+
}
|
|
22
|
+
async put(path, body, opts) {
|
|
23
|
+
return this.request('PUT', buildUrl(this.config.baseUrl, path), body, opts);
|
|
24
|
+
}
|
|
25
|
+
async patch(path, body, opts) {
|
|
26
|
+
return this.request('PATCH', buildUrl(this.config.baseUrl, path), body, opts);
|
|
27
|
+
}
|
|
28
|
+
async delete(path, opts) {
|
|
29
|
+
return this.request('DELETE', buildUrl(this.config.baseUrl, path), undefined, opts);
|
|
30
|
+
}
|
|
31
|
+
static generateRequestId() {
|
|
32
|
+
return (0, crypto_js_1.generateUUID)();
|
|
33
|
+
}
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Private
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
async request(method, url, body, opts) {
|
|
38
|
+
const requestId = opts?.requestId ?? (0, crypto_js_1.generateUUID)();
|
|
39
|
+
const maxRetries = opts?.maxRetries ?? this.config.maxRetries;
|
|
40
|
+
const execute = () => this.executeOnce(method, url, body, requestId, opts?.headers);
|
|
41
|
+
return (0, retry_js_1.withRetry)(execute, { ...this.config, maxRetries }, (e) => e instanceof index_js_1.ClearBankError);
|
|
42
|
+
}
|
|
43
|
+
async executeOnce(method, url, body, requestId, extraHeaders) {
|
|
44
|
+
const bodyStr = body !== undefined ? JSON.stringify(body) : null;
|
|
45
|
+
const headers = await this.buildHeaders(method, bodyStr, requestId, extraHeaders);
|
|
46
|
+
const startMs = Date.now();
|
|
47
|
+
let response;
|
|
48
|
+
try {
|
|
49
|
+
const controller = new AbortController();
|
|
50
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.timeoutMs);
|
|
51
|
+
try {
|
|
52
|
+
const init = {
|
|
53
|
+
method,
|
|
54
|
+
headers,
|
|
55
|
+
signal: controller.signal,
|
|
56
|
+
};
|
|
57
|
+
if (bodyStr !== null) {
|
|
58
|
+
init.body = bodyStr;
|
|
59
|
+
}
|
|
60
|
+
response = await fetch(url, init);
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
clearTimeout(timeoutId);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
const durationMs = Date.now() - startMs;
|
|
68
|
+
const error = new index_js_1.ClearBankError({
|
|
69
|
+
message: err instanceof Error ? err.message : 'Network request failed',
|
|
70
|
+
requestId,
|
|
71
|
+
details: { cause: String(err) },
|
|
72
|
+
});
|
|
73
|
+
this.emitTelemetry({ method, url, statusCode: null, durationMs, requestId, error });
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
const durationMs = Date.now() - startMs;
|
|
77
|
+
const correlationId = response.headers.get('x-correlation-id');
|
|
78
|
+
this.emitTelemetry({
|
|
79
|
+
method,
|
|
80
|
+
url,
|
|
81
|
+
statusCode: response.status,
|
|
82
|
+
durationMs,
|
|
83
|
+
requestId,
|
|
84
|
+
correlationId,
|
|
85
|
+
error: null,
|
|
86
|
+
});
|
|
87
|
+
return this.handleResponse(response, requestId, correlationId);
|
|
88
|
+
}
|
|
89
|
+
async buildHeaders(method, bodyStr, requestId, extraHeaders) {
|
|
90
|
+
const headers = {
|
|
91
|
+
Authorization: `Bearer ${this.config.apiToken}`,
|
|
92
|
+
'X-Request-Id': requestId,
|
|
93
|
+
Accept: 'application/json',
|
|
94
|
+
};
|
|
95
|
+
if (bodyStr !== null) {
|
|
96
|
+
headers['Content-Type'] = 'application/json';
|
|
97
|
+
}
|
|
98
|
+
if (MUTATING_METHODS.has(method) && bodyStr !== null && this.config.privateKey) {
|
|
99
|
+
headers['DigitalSignature'] = await (0, crypto_js_1.signRequestBody)(bodyStr, this.config.privateKey);
|
|
100
|
+
}
|
|
101
|
+
if (extraHeaders) {
|
|
102
|
+
Object.assign(headers, extraHeaders);
|
|
103
|
+
}
|
|
104
|
+
return headers;
|
|
105
|
+
}
|
|
106
|
+
async handleResponse(response, requestId, correlationId) {
|
|
107
|
+
if (response.status >= 200 && response.status < 300) {
|
|
108
|
+
const data = response.status === 204 ? undefined : (await parseJsonBody(response));
|
|
109
|
+
return { data, requestId, correlationId };
|
|
110
|
+
}
|
|
111
|
+
const rawBody = await parseJsonBody(response);
|
|
112
|
+
const retryAfter = parseRetryAfter(response.headers.get('retry-after'));
|
|
113
|
+
throw buildApiError(response.status, rawBody, requestId, correlationId, retryAfter);
|
|
114
|
+
}
|
|
115
|
+
emitTelemetry(event) {
|
|
116
|
+
if (this.config.telemetryHook) {
|
|
117
|
+
this.config.telemetryHook({
|
|
118
|
+
method: event.method,
|
|
119
|
+
url: event.url,
|
|
120
|
+
statusCode: event.statusCode,
|
|
121
|
+
durationMs: event.durationMs,
|
|
122
|
+
requestId: event.requestId,
|
|
123
|
+
correlationId: event.correlationId ?? null,
|
|
124
|
+
error: event.error,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
exports.HttpClient = HttpClient;
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// Helpers
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
function buildUrl(base, path, params) {
|
|
134
|
+
const url = base + path;
|
|
135
|
+
if (!params)
|
|
136
|
+
return url;
|
|
137
|
+
const qs = Object.entries(params)
|
|
138
|
+
.filter(([, v]) => v !== undefined && v !== null)
|
|
139
|
+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)
|
|
140
|
+
.join('&');
|
|
141
|
+
return qs ? `${url}?${qs}` : url;
|
|
142
|
+
}
|
|
143
|
+
async function parseJsonBody(response) {
|
|
144
|
+
const text = await response.text();
|
|
145
|
+
if (!text)
|
|
146
|
+
return null;
|
|
147
|
+
try {
|
|
148
|
+
return JSON.parse(text);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return text;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function parseRetryAfter(value) {
|
|
155
|
+
if (!value)
|
|
156
|
+
return null;
|
|
157
|
+
const n = parseInt(value, 10);
|
|
158
|
+
return isNaN(n) ? null : n;
|
|
159
|
+
}
|
|
160
|
+
function buildApiError(status, body, requestId, correlationId, retryAfterSeconds) {
|
|
161
|
+
let message = `HTTP ${status}`;
|
|
162
|
+
let code = null;
|
|
163
|
+
let details = null;
|
|
164
|
+
if (body && typeof body === 'object') {
|
|
165
|
+
const b = body;
|
|
166
|
+
if (typeof b['title'] === 'string' && typeof b['detail'] === 'string') {
|
|
167
|
+
message = `${b['title']}: ${b['detail']}`;
|
|
168
|
+
}
|
|
169
|
+
else if (typeof b['title'] === 'string') {
|
|
170
|
+
message = b['title'];
|
|
171
|
+
}
|
|
172
|
+
else if (typeof b['message'] === 'string') {
|
|
173
|
+
message = b['message'];
|
|
174
|
+
}
|
|
175
|
+
else if (typeof b['error'] === 'string') {
|
|
176
|
+
message = b['error'];
|
|
177
|
+
code = b['error'];
|
|
178
|
+
}
|
|
179
|
+
details = b;
|
|
180
|
+
}
|
|
181
|
+
return new index_js_1.ClearBankError({
|
|
182
|
+
message,
|
|
183
|
+
statusCode: status,
|
|
184
|
+
code,
|
|
185
|
+
correlationId,
|
|
186
|
+
requestId,
|
|
187
|
+
details,
|
|
188
|
+
retryAfterSeconds,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=http.js.map
|