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.
Files changed (142) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/LICENSE +21 -0
  3. package/README.md +295 -0
  4. package/dist/cjs/core/config.js +25 -0
  5. package/dist/cjs/core/config.js.map +1 -0
  6. package/dist/cjs/core/crypto.js +110 -0
  7. package/dist/cjs/core/crypto.js.map +1 -0
  8. package/dist/cjs/core/http.js +191 -0
  9. package/dist/cjs/core/http.js.map +1 -0
  10. package/dist/cjs/core/index.js +14 -0
  11. package/dist/cjs/core/index.js.map +1 -0
  12. package/dist/cjs/core/retry.js +42 -0
  13. package/dist/cjs/core/retry.js.map +1 -0
  14. package/dist/cjs/domains/accounts/index.js +6 -0
  15. package/dist/cjs/domains/accounts/index.js.map +1 -0
  16. package/dist/cjs/domains/accounts/service.js +216 -0
  17. package/dist/cjs/domains/accounts/service.js.map +1 -0
  18. package/dist/cjs/domains/accounts/types.js +3 -0
  19. package/dist/cjs/domains/accounts/types.js.map +1 -0
  20. package/dist/cjs/domains/embedded/index.js +6 -0
  21. package/dist/cjs/domains/embedded/index.js.map +1 -0
  22. package/dist/cjs/domains/embedded/service.js +215 -0
  23. package/dist/cjs/domains/embedded/service.js.map +1 -0
  24. package/dist/cjs/domains/embedded/types.js +3 -0
  25. package/dist/cjs/domains/embedded/types.js.map +1 -0
  26. package/dist/cjs/domains/multicurrency/index.js +6 -0
  27. package/dist/cjs/domains/multicurrency/index.js.map +1 -0
  28. package/dist/cjs/domains/multicurrency/service.js +202 -0
  29. package/dist/cjs/domains/multicurrency/service.js.map +1 -0
  30. package/dist/cjs/domains/multicurrency/types.js +3 -0
  31. package/dist/cjs/domains/multicurrency/types.js.map +1 -0
  32. package/dist/cjs/domains/payments/index.js +6 -0
  33. package/dist/cjs/domains/payments/index.js.map +1 -0
  34. package/dist/cjs/domains/payments/service.js +255 -0
  35. package/dist/cjs/domains/payments/service.js.map +1 -0
  36. package/dist/cjs/domains/payments/types.js +3 -0
  37. package/dist/cjs/domains/payments/types.js.map +1 -0
  38. package/dist/cjs/domains/webhooks/index.js +7 -0
  39. package/dist/cjs/domains/webhooks/index.js.map +1 -0
  40. package/dist/cjs/domains/webhooks/service.js +173 -0
  41. package/dist/cjs/domains/webhooks/service.js.map +1 -0
  42. package/dist/cjs/domains/webhooks/types.js +8 -0
  43. package/dist/cjs/domains/webhooks/types.js.map +1 -0
  44. package/dist/cjs/index.js +73 -0
  45. package/dist/cjs/index.js.map +1 -0
  46. package/dist/cjs/types/index.js +45 -0
  47. package/dist/cjs/types/index.js.map +1 -0
  48. package/dist/cjs/utils/index.js +46 -0
  49. package/dist/cjs/utils/index.js.map +1 -0
  50. package/dist/esm/core/config.js +22 -0
  51. package/dist/esm/core/config.js.map +1 -0
  52. package/dist/esm/core/crypto.js +105 -0
  53. package/dist/esm/core/crypto.js.map +1 -0
  54. package/dist/esm/core/http.js +187 -0
  55. package/dist/esm/core/http.js.map +1 -0
  56. package/dist/esm/core/index.js +5 -0
  57. package/dist/esm/core/index.js.map +1 -0
  58. package/dist/esm/core/retry.js +39 -0
  59. package/dist/esm/core/retry.js.map +1 -0
  60. package/dist/esm/domains/accounts/index.js +2 -0
  61. package/dist/esm/domains/accounts/index.js.map +1 -0
  62. package/dist/esm/domains/accounts/service.js +212 -0
  63. package/dist/esm/domains/accounts/service.js.map +1 -0
  64. package/dist/esm/domains/accounts/types.js +2 -0
  65. package/dist/esm/domains/accounts/types.js.map +1 -0
  66. package/dist/esm/domains/embedded/index.js +2 -0
  67. package/dist/esm/domains/embedded/index.js.map +1 -0
  68. package/dist/esm/domains/embedded/service.js +211 -0
  69. package/dist/esm/domains/embedded/service.js.map +1 -0
  70. package/dist/esm/domains/embedded/types.js +2 -0
  71. package/dist/esm/domains/embedded/types.js.map +1 -0
  72. package/dist/esm/domains/multicurrency/index.js +2 -0
  73. package/dist/esm/domains/multicurrency/index.js.map +1 -0
  74. package/dist/esm/domains/multicurrency/service.js +198 -0
  75. package/dist/esm/domains/multicurrency/service.js.map +1 -0
  76. package/dist/esm/domains/multicurrency/types.js +2 -0
  77. package/dist/esm/domains/multicurrency/types.js.map +1 -0
  78. package/dist/esm/domains/payments/index.js +2 -0
  79. package/dist/esm/domains/payments/index.js.map +1 -0
  80. package/dist/esm/domains/payments/service.js +251 -0
  81. package/dist/esm/domains/payments/service.js.map +1 -0
  82. package/dist/esm/domains/payments/types.js +2 -0
  83. package/dist/esm/domains/payments/types.js.map +1 -0
  84. package/dist/esm/domains/webhooks/index.js +2 -0
  85. package/dist/esm/domains/webhooks/index.js.map +1 -0
  86. package/dist/esm/domains/webhooks/service.js +168 -0
  87. package/dist/esm/domains/webhooks/service.js.map +1 -0
  88. package/dist/esm/domains/webhooks/types.js +5 -0
  89. package/dist/esm/domains/webhooks/types.js.map +1 -0
  90. package/dist/esm/index.js +56 -0
  91. package/dist/esm/index.js.map +1 -0
  92. package/dist/esm/types/index.js +41 -0
  93. package/dist/esm/types/index.js.map +1 -0
  94. package/dist/esm/utils/index.js +35 -0
  95. package/dist/esm/utils/index.js.map +1 -0
  96. package/dist/types/core/config.d.ts +34 -0
  97. package/dist/types/core/config.d.ts.map +1 -0
  98. package/dist/types/core/crypto.d.ts +20 -0
  99. package/dist/types/core/crypto.d.ts.map +1 -0
  100. package/dist/types/core/http.d.ts +34 -0
  101. package/dist/types/core/http.d.ts.map +1 -0
  102. package/dist/types/core/index.d.ts +7 -0
  103. package/dist/types/core/index.d.ts.map +1 -0
  104. package/dist/types/core/retry.d.ts +14 -0
  105. package/dist/types/core/retry.d.ts.map +1 -0
  106. package/dist/types/domains/accounts/index.d.ts +3 -0
  107. package/dist/types/domains/accounts/index.d.ts.map +1 -0
  108. package/dist/types/domains/accounts/service.d.ts +61 -0
  109. package/dist/types/domains/accounts/service.d.ts.map +1 -0
  110. package/dist/types/domains/accounts/types.d.ts +90 -0
  111. package/dist/types/domains/accounts/types.d.ts.map +1 -0
  112. package/dist/types/domains/embedded/index.d.ts +3 -0
  113. package/dist/types/domains/embedded/index.d.ts.map +1 -0
  114. package/dist/types/domains/embedded/service.d.ts +63 -0
  115. package/dist/types/domains/embedded/service.d.ts.map +1 -0
  116. package/dist/types/domains/embedded/types.d.ts +148 -0
  117. package/dist/types/domains/embedded/types.d.ts.map +1 -0
  118. package/dist/types/domains/multicurrency/index.d.ts +3 -0
  119. package/dist/types/domains/multicurrency/index.d.ts.map +1 -0
  120. package/dist/types/domains/multicurrency/service.d.ts +50 -0
  121. package/dist/types/domains/multicurrency/service.d.ts.map +1 -0
  122. package/dist/types/domains/multicurrency/types.d.ts +84 -0
  123. package/dist/types/domains/multicurrency/types.d.ts.map +1 -0
  124. package/dist/types/domains/payments/index.d.ts +3 -0
  125. package/dist/types/domains/payments/index.d.ts.map +1 -0
  126. package/dist/types/domains/payments/service.d.ts +65 -0
  127. package/dist/types/domains/payments/service.d.ts.map +1 -0
  128. package/dist/types/domains/payments/types.d.ts +107 -0
  129. package/dist/types/domains/payments/types.d.ts.map +1 -0
  130. package/dist/types/domains/webhooks/index.d.ts +3 -0
  131. package/dist/types/domains/webhooks/index.d.ts.map +1 -0
  132. package/dist/types/domains/webhooks/service.d.ts +66 -0
  133. package/dist/types/domains/webhooks/service.d.ts.map +1 -0
  134. package/dist/types/domains/webhooks/types.d.ts +215 -0
  135. package/dist/types/domains/webhooks/types.d.ts.map +1 -0
  136. package/dist/types/index.d.ts +54 -0
  137. package/dist/types/index.d.ts.map +1 -0
  138. package/dist/types/types/index.d.ts +77 -0
  139. package/dist/types/types/index.d.ts.map +1 -0
  140. package/dist/types/utils/index.d.ts +22 -0
  141. package/dist/types/utils/index.d.ts.map +1 -0
  142. 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
+ [![npm version](https://img.shields.io/npm/v/clearbank-sdk)](https://www.npmjs.com/package/clearbank-sdk)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.4-blue)](https://www.typescriptlang.org/)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](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