@spreadspace/embed 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +128 -0
- package/dist/index.cjs +1155 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +682 -0
- package/dist/index.d.ts +682 -0
- package/dist/index.js +1131 -0
- package/dist/index.js.map +1 -0
- package/dist/webhooks-D6PW9fcE.d.cts +189 -0
- package/dist/webhooks-D6PW9fcE.d.ts +189 -0
- package/dist/webhooks.cjs +141 -0
- package/dist/webhooks.cjs.map +1 -0
- package/dist/webhooks.d.cts +1 -0
- package/dist/webhooks.d.ts +1 -0
- package/dist/webhooks.js +137 -0
- package/dist/webhooks.js.map +1 -0
- package/package.json +65 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1155 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
// src/client.ts
|
|
6
|
+
|
|
7
|
+
// src/errors.ts
|
|
8
|
+
var SpreadSpaceError = class extends Error {
|
|
9
|
+
/**
|
|
10
|
+
* The canonical `error.type` string from the API envelope, e.g.
|
|
11
|
+
* `invalid_request`, `rate_limited`, `idempotency_request_mismatch`.
|
|
12
|
+
* Stable across versions — clients pattern-match on this.
|
|
13
|
+
*/
|
|
14
|
+
type;
|
|
15
|
+
/** HTTP status code. */
|
|
16
|
+
statusCode;
|
|
17
|
+
/** `X-Request-ID` echoed from the server. Quote in support tickets. */
|
|
18
|
+
requestId;
|
|
19
|
+
/** Raw response body for debugging. May be the parsed JSON or a string. */
|
|
20
|
+
rawBody;
|
|
21
|
+
/** Optional structured details (e.g. `borrower_id` on `pii_claim_required`). */
|
|
22
|
+
details;
|
|
23
|
+
constructor(params) {
|
|
24
|
+
super(params.message);
|
|
25
|
+
this.name = new.target.name;
|
|
26
|
+
this.type = params.type;
|
|
27
|
+
this.statusCode = params.statusCode;
|
|
28
|
+
this.requestId = params.requestId;
|
|
29
|
+
this.rawBody = params.rawBody;
|
|
30
|
+
this.details = params.details;
|
|
31
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
var InvalidRequestError = class extends SpreadSpaceError {
|
|
35
|
+
};
|
|
36
|
+
var AuthenticationError = class extends SpreadSpaceError {
|
|
37
|
+
};
|
|
38
|
+
var PermissionError = class extends SpreadSpaceError {
|
|
39
|
+
};
|
|
40
|
+
var NotFoundError = class extends SpreadSpaceError {
|
|
41
|
+
};
|
|
42
|
+
var ConflictError = class extends SpreadSpaceError {
|
|
43
|
+
};
|
|
44
|
+
var RateLimitError = class extends SpreadSpaceError {
|
|
45
|
+
};
|
|
46
|
+
var ServerError = class extends SpreadSpaceError {
|
|
47
|
+
};
|
|
48
|
+
var NetworkError = class _NetworkError extends Error {
|
|
49
|
+
cause;
|
|
50
|
+
constructor(message, cause) {
|
|
51
|
+
super(message);
|
|
52
|
+
this.name = "NetworkError";
|
|
53
|
+
this.cause = cause;
|
|
54
|
+
Object.setPrototypeOf(this, _NetworkError.prototype);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
function classifyError(statusCode, _type) {
|
|
58
|
+
if (statusCode === 400) return InvalidRequestError;
|
|
59
|
+
if (statusCode === 401) return AuthenticationError;
|
|
60
|
+
if (statusCode === 403) return PermissionError;
|
|
61
|
+
if (statusCode === 404) return NotFoundError;
|
|
62
|
+
if (statusCode === 409) return ConflictError;
|
|
63
|
+
if (statusCode === 429) return RateLimitError;
|
|
64
|
+
if (statusCode >= 500) return ServerError;
|
|
65
|
+
return InvalidRequestError;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/pagination.ts
|
|
69
|
+
function paginate(client, path, params, options) {
|
|
70
|
+
let cursor;
|
|
71
|
+
let exhausted = false;
|
|
72
|
+
async function fetchPage() {
|
|
73
|
+
if (exhausted) return null;
|
|
74
|
+
const query = { ...params ?? {} };
|
|
75
|
+
if (cursor !== void 0) query.cursor = cursor;
|
|
76
|
+
const response = await client.request("GET", path, {
|
|
77
|
+
query,
|
|
78
|
+
...options
|
|
79
|
+
});
|
|
80
|
+
const next = response.next_cursor ?? null;
|
|
81
|
+
if (!next) {
|
|
82
|
+
exhausted = true;
|
|
83
|
+
} else {
|
|
84
|
+
cursor = next;
|
|
85
|
+
}
|
|
86
|
+
return response;
|
|
87
|
+
}
|
|
88
|
+
async function* itemIterator() {
|
|
89
|
+
while (true) {
|
|
90
|
+
const page = await fetchPage();
|
|
91
|
+
if (!page) return;
|
|
92
|
+
for (const item of page.data) {
|
|
93
|
+
yield item;
|
|
94
|
+
}
|
|
95
|
+
if (exhausted) return;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async function* pageIterator() {
|
|
99
|
+
while (true) {
|
|
100
|
+
const page = await fetchPage();
|
|
101
|
+
if (!page) return;
|
|
102
|
+
yield page;
|
|
103
|
+
if (exhausted) return;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const iter = itemIterator();
|
|
107
|
+
const pager = {
|
|
108
|
+
async next() {
|
|
109
|
+
return iter.next();
|
|
110
|
+
},
|
|
111
|
+
async return(value) {
|
|
112
|
+
exhausted = true;
|
|
113
|
+
return { value, done: true };
|
|
114
|
+
},
|
|
115
|
+
async throw(err) {
|
|
116
|
+
exhausted = true;
|
|
117
|
+
throw err;
|
|
118
|
+
},
|
|
119
|
+
[Symbol.asyncIterator]() {
|
|
120
|
+
return pager;
|
|
121
|
+
},
|
|
122
|
+
async toArray() {
|
|
123
|
+
const out = [];
|
|
124
|
+
for await (const item of pager) {
|
|
125
|
+
out.push(item);
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
},
|
|
129
|
+
pages() {
|
|
130
|
+
return pageIterator();
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
return pager;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/resources/borrowers.ts
|
|
137
|
+
var BorrowersResource = class {
|
|
138
|
+
constructor(client) {
|
|
139
|
+
this.client = client;
|
|
140
|
+
}
|
|
141
|
+
client;
|
|
142
|
+
/**
|
|
143
|
+
* Retrieve a borrower by id.
|
|
144
|
+
*
|
|
145
|
+
* GET /api/borrowers/{id}
|
|
146
|
+
*/
|
|
147
|
+
async retrieve(borrowerId, options) {
|
|
148
|
+
return this.client.request(
|
|
149
|
+
"GET",
|
|
150
|
+
`/api/borrowers/${encodeURIComponent(borrowerId)}`,
|
|
151
|
+
options
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Create a new borrower.
|
|
156
|
+
*
|
|
157
|
+
* POST /api/borrowers
|
|
158
|
+
*/
|
|
159
|
+
async create(params, options) {
|
|
160
|
+
return this.client.request("POST", "/api/borrowers", { body: params, ...options });
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Iterate every borrower visible to the calling principal.
|
|
164
|
+
*
|
|
165
|
+
* GET /api/borrowers
|
|
166
|
+
*/
|
|
167
|
+
list(params, options) {
|
|
168
|
+
return this.client.paginate(
|
|
169
|
+
"/api/borrowers",
|
|
170
|
+
params,
|
|
171
|
+
options
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Assign a lender to this borrower.
|
|
176
|
+
*
|
|
177
|
+
* POST /api/borrowers/{id}/assign
|
|
178
|
+
*/
|
|
179
|
+
async assign(borrowerId, params, options) {
|
|
180
|
+
return this.client.request(
|
|
181
|
+
"POST",
|
|
182
|
+
`/api/borrowers/${encodeURIComponent(borrowerId)}/assign`,
|
|
183
|
+
{ body: params, ...options }
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Claim PII access on a borrower. Required by SOC 2 / GLBA before an
|
|
188
|
+
* admin can view the borrower's extracted financials. Surfaced as a verb
|
|
189
|
+
* method (rather than `update()`) because the audit log records this as
|
|
190
|
+
* a distinct event and integrators reading the SDK want to see the flow
|
|
191
|
+
* by name.
|
|
192
|
+
*
|
|
193
|
+
* POST /api/borrowers/{id}/claim
|
|
194
|
+
*/
|
|
195
|
+
async claim(borrowerId, params, options) {
|
|
196
|
+
return this.client.request(
|
|
197
|
+
"POST",
|
|
198
|
+
`/api/borrowers/${encodeURIComponent(borrowerId)}/claim`,
|
|
199
|
+
{ body: params, ...options }
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Fetch the audit log for a borrower (PII access events, claim grants,
|
|
204
|
+
* etc.).
|
|
205
|
+
*
|
|
206
|
+
* GET /api/borrowers/{id}/audit-log
|
|
207
|
+
*/
|
|
208
|
+
async auditLog(borrowerId, options) {
|
|
209
|
+
return this.client.request(
|
|
210
|
+
"GET",
|
|
211
|
+
`/api/borrowers/${encodeURIComponent(borrowerId)}/audit-log`,
|
|
212
|
+
options
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* List loans associated with a borrower.
|
|
217
|
+
*
|
|
218
|
+
* GET /api/borrowers/{borrowerId}/loans
|
|
219
|
+
*/
|
|
220
|
+
loans(borrowerId, params, options) {
|
|
221
|
+
return this.client.paginate(
|
|
222
|
+
`/api/borrowers/${encodeURIComponent(borrowerId)}/loans`,
|
|
223
|
+
params,
|
|
224
|
+
options
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* List jobs (document packages) for a borrower.
|
|
229
|
+
*
|
|
230
|
+
* GET /api/borrowers/{id}/jobs
|
|
231
|
+
*/
|
|
232
|
+
jobs(borrowerId, params, options) {
|
|
233
|
+
return this.client.paginate(
|
|
234
|
+
`/api/borrowers/${encodeURIComponent(borrowerId)}/jobs`,
|
|
235
|
+
params,
|
|
236
|
+
options
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Fetch results for a specific job under a borrower.
|
|
241
|
+
*
|
|
242
|
+
* GET /api/borrowers/{id}/jobs/{jobId}/results
|
|
243
|
+
*/
|
|
244
|
+
async jobResults(borrowerId, jobId, options) {
|
|
245
|
+
return this.client.request(
|
|
246
|
+
"GET",
|
|
247
|
+
`/api/borrowers/${encodeURIComponent(borrowerId)}/jobs/${encodeURIComponent(jobId)}/results`,
|
|
248
|
+
options
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// src/resources/documents.ts
|
|
254
|
+
var DocumentsResource = class {
|
|
255
|
+
constructor(client) {
|
|
256
|
+
this.client = client;
|
|
257
|
+
}
|
|
258
|
+
client;
|
|
259
|
+
/**
|
|
260
|
+
* Request a presigned S3 PUT URL for a single document upload.
|
|
261
|
+
*
|
|
262
|
+
* POST /api/documents/presigned-url
|
|
263
|
+
*/
|
|
264
|
+
async presignedUrl(params, options) {
|
|
265
|
+
return this.client.request("POST", "/api/documents/presigned-url", {
|
|
266
|
+
body: params,
|
|
267
|
+
...options
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Confirm a single uploaded document, kicking off the extraction
|
|
272
|
+
* pipeline.
|
|
273
|
+
*
|
|
274
|
+
* POST /api/documents/{jobId}/confirm-upload
|
|
275
|
+
*/
|
|
276
|
+
async confirmUpload(jobId, params = {}, options) {
|
|
277
|
+
return this.client.request(
|
|
278
|
+
"POST",
|
|
279
|
+
`/api/documents/${encodeURIComponent(jobId)}/confirm-upload`,
|
|
280
|
+
{ body: params, ...options }
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Confirm a batch of uploaded documents in a single request. Faster than
|
|
285
|
+
* looping `confirmUpload()` when the integrator has many files in one
|
|
286
|
+
* package.
|
|
287
|
+
*
|
|
288
|
+
* POST /api/documents/confirm-uploads
|
|
289
|
+
*/
|
|
290
|
+
async batchConfirm(params, options) {
|
|
291
|
+
return this.client.request("POST", "/api/documents/confirm-uploads", {
|
|
292
|
+
body: params,
|
|
293
|
+
...options
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Get a presigned S3 GET URL to download a previously-uploaded document.
|
|
298
|
+
*
|
|
299
|
+
* GET /api/documents/{jobId}/download-url
|
|
300
|
+
*/
|
|
301
|
+
async downloadUrl(jobId, options) {
|
|
302
|
+
return this.client.request(
|
|
303
|
+
"GET",
|
|
304
|
+
`/api/documents/${encodeURIComponent(jobId)}/download-url`,
|
|
305
|
+
options
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Fetch processing status for a document upload.
|
|
310
|
+
*
|
|
311
|
+
* GET /api/documents/{jobId}/status
|
|
312
|
+
*/
|
|
313
|
+
async status(jobId, options) {
|
|
314
|
+
return this.client.request(
|
|
315
|
+
"GET",
|
|
316
|
+
`/api/documents/${encodeURIComponent(jobId)}/status`,
|
|
317
|
+
options
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// src/resources/embed.ts
|
|
323
|
+
var EmbedSessionsResource = class {
|
|
324
|
+
constructor(client) {
|
|
325
|
+
this.client = client;
|
|
326
|
+
}
|
|
327
|
+
client;
|
|
328
|
+
/**
|
|
329
|
+
* Mint an embed-session token for a loan. The minted token has a strict
|
|
330
|
+
* subset of the calling API key's scopes and is locked to the supplied
|
|
331
|
+
* `loan_id` — it cannot be used to access other loans or to mint further
|
|
332
|
+
* tokens.
|
|
333
|
+
*
|
|
334
|
+
* POST /api/embed/sessions
|
|
335
|
+
*
|
|
336
|
+
* Body: { loan_id, scopes?, expires_in_seconds? }
|
|
337
|
+
* Returns: { embed_token, expires_at, ... }
|
|
338
|
+
*/
|
|
339
|
+
async create(params, options) {
|
|
340
|
+
return this.client.request("POST", "/api/embed/sessions", { body: params, ...options });
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Revoke an embed session early — useful when the integrator's UI flow
|
|
344
|
+
* ends before the natural expiry, or when reissuing after a logout.
|
|
345
|
+
*
|
|
346
|
+
* DELETE /api/embed/sessions/{sessionId}
|
|
347
|
+
*/
|
|
348
|
+
async revoke(sessionId, options) {
|
|
349
|
+
return this.client.request(
|
|
350
|
+
"DELETE",
|
|
351
|
+
`/api/embed/sessions/${encodeURIComponent(sessionId)}`,
|
|
352
|
+
options
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
var EmbedResource = class {
|
|
357
|
+
sessions;
|
|
358
|
+
constructor(client) {
|
|
359
|
+
this.sessions = new EmbedSessionsResource(client);
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
// src/resources/extractions.ts
|
|
364
|
+
var ExtractionsResource = class {
|
|
365
|
+
constructor(client) {
|
|
366
|
+
this.client = client;
|
|
367
|
+
}
|
|
368
|
+
client;
|
|
369
|
+
/**
|
|
370
|
+
* Iterate every extraction for a borrower.
|
|
371
|
+
*
|
|
372
|
+
* GET /api/borrowers/{borrowerId}/extractions
|
|
373
|
+
*/
|
|
374
|
+
list(borrowerId, params, options) {
|
|
375
|
+
return this.client.paginate(
|
|
376
|
+
`/api/borrowers/${encodeURIComponent(borrowerId)}/extractions`,
|
|
377
|
+
params,
|
|
378
|
+
options
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Structured query across a borrower's extractions. Filter by document
|
|
383
|
+
* type, period, and other metadata.
|
|
384
|
+
*
|
|
385
|
+
* POST /api/borrowers/{borrowerId}/extractions/query
|
|
386
|
+
*/
|
|
387
|
+
async query(borrowerId, params, options) {
|
|
388
|
+
return this.client.request(
|
|
389
|
+
"POST",
|
|
390
|
+
`/api/borrowers/${encodeURIComponent(borrowerId)}/extractions/query`,
|
|
391
|
+
{ body: params, ...options }
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Move an extracted document to a different loan under the same
|
|
396
|
+
* borrower. Use when the user uploaded a doc to the wrong package.
|
|
397
|
+
*
|
|
398
|
+
* POST /api/borrowers/{borrowerId}/extractions/{docId}/move
|
|
399
|
+
*/
|
|
400
|
+
async move(borrowerId, docId, params, options) {
|
|
401
|
+
return this.client.request(
|
|
402
|
+
"POST",
|
|
403
|
+
`/api/borrowers/${encodeURIComponent(borrowerId)}/extractions/${encodeURIComponent(docId)}/move`,
|
|
404
|
+
{ body: params, ...options }
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Roll-up of the borrower's bank-statement analyses across all
|
|
409
|
+
* uploaded statements.
|
|
410
|
+
*
|
|
411
|
+
* GET /api/borrowers/{borrowerId}/extractions/bank-statement-analysis
|
|
412
|
+
*/
|
|
413
|
+
async bankStatementAnalysis(borrowerId, options) {
|
|
414
|
+
return this.client.request(
|
|
415
|
+
"GET",
|
|
416
|
+
`/api/borrowers/${encodeURIComponent(borrowerId)}/extractions/bank-statement-analysis`,
|
|
417
|
+
options
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Roll-up of the borrower's balance sheet across all uploaded
|
|
422
|
+
* balance-sheet documents.
|
|
423
|
+
*/
|
|
424
|
+
async balanceSheet(borrowerId, options) {
|
|
425
|
+
return this.client.request(
|
|
426
|
+
"GET",
|
|
427
|
+
`/api/borrowers/${encodeURIComponent(borrowerId)}/extractions/balance-sheet`,
|
|
428
|
+
options
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
/** P&L roll-up across uploaded P&L documents. */
|
|
432
|
+
async pl(borrowerId, options) {
|
|
433
|
+
return this.client.request(
|
|
434
|
+
"GET",
|
|
435
|
+
`/api/borrowers/${encodeURIComponent(borrowerId)}/extractions/pl`,
|
|
436
|
+
options
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
/** Cash-flow roll-up. */
|
|
440
|
+
async cashFlow(borrowerId, options) {
|
|
441
|
+
return this.client.request(
|
|
442
|
+
"GET",
|
|
443
|
+
`/api/borrowers/${encodeURIComponent(borrowerId)}/extractions/cash-flow`,
|
|
444
|
+
options
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
/** Accounts payable roll-up. */
|
|
448
|
+
async accountsPayable(borrowerId, options) {
|
|
449
|
+
return this.client.request(
|
|
450
|
+
"GET",
|
|
451
|
+
`/api/borrowers/${encodeURIComponent(borrowerId)}/extractions/accounts-payable`,
|
|
452
|
+
options
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
/** Accounts receivable roll-up. */
|
|
456
|
+
async accountsReceivable(borrowerId, options) {
|
|
457
|
+
return this.client.request(
|
|
458
|
+
"GET",
|
|
459
|
+
`/api/borrowers/${encodeURIComponent(borrowerId)}/extractions/accounts-receivable`,
|
|
460
|
+
options
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Generic report endpoint — pass the report type as a path segment.
|
|
465
|
+
* Useful for report types added after this SDK release.
|
|
466
|
+
*
|
|
467
|
+
* GET /api/borrowers/{borrowerId}/extractions/reports/{reportType}
|
|
468
|
+
*/
|
|
469
|
+
async report(borrowerId, reportType, options) {
|
|
470
|
+
return this.client.request(
|
|
471
|
+
"GET",
|
|
472
|
+
`/api/borrowers/${encodeURIComponent(borrowerId)}/extractions/reports/${encodeURIComponent(reportType)}`,
|
|
473
|
+
options
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
// src/resources/jobs.ts
|
|
479
|
+
var JobsResource = class {
|
|
480
|
+
constructor(client) {
|
|
481
|
+
this.client = client;
|
|
482
|
+
}
|
|
483
|
+
client;
|
|
484
|
+
/**
|
|
485
|
+
* Iterate jobs visible to the calling principal.
|
|
486
|
+
*
|
|
487
|
+
* GET /api/jobs
|
|
488
|
+
*/
|
|
489
|
+
list(params, options) {
|
|
490
|
+
return this.client.paginate(
|
|
491
|
+
"/api/jobs",
|
|
492
|
+
params,
|
|
493
|
+
options
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Fetch processing status for a job.
|
|
498
|
+
*
|
|
499
|
+
* GET /api/jobs/{jobId}/status
|
|
500
|
+
*/
|
|
501
|
+
async status(jobId, options) {
|
|
502
|
+
return this.client.request(
|
|
503
|
+
"GET",
|
|
504
|
+
`/api/jobs/${encodeURIComponent(jobId)}/status`,
|
|
505
|
+
options
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Fetch results for a job — the rolled-up extraction outcomes.
|
|
510
|
+
*
|
|
511
|
+
* GET /api/jobs/{jobId}/results
|
|
512
|
+
*/
|
|
513
|
+
async results(jobId, options) {
|
|
514
|
+
return this.client.request(
|
|
515
|
+
"GET",
|
|
516
|
+
`/api/jobs/${encodeURIComponent(jobId)}/results`,
|
|
517
|
+
options
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
// src/resources/loans.ts
|
|
523
|
+
var LoansResource = class {
|
|
524
|
+
constructor(client) {
|
|
525
|
+
this.client = client;
|
|
526
|
+
}
|
|
527
|
+
client;
|
|
528
|
+
/**
|
|
529
|
+
* Retrieve a loan by id.
|
|
530
|
+
*
|
|
531
|
+
* GET /api/loans/{loanId}
|
|
532
|
+
*/
|
|
533
|
+
async retrieve(loanId, options) {
|
|
534
|
+
return this.client.request("GET", `/api/loans/${encodeURIComponent(loanId)}`, options);
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Create a new loan.
|
|
538
|
+
*
|
|
539
|
+
* POST /api/loans
|
|
540
|
+
*/
|
|
541
|
+
async create(params, options) {
|
|
542
|
+
return this.client.request("POST", "/api/loans", { body: params, ...options });
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Update an existing loan.
|
|
546
|
+
*
|
|
547
|
+
* PATCH /api/loans/{loanId}
|
|
548
|
+
*/
|
|
549
|
+
async update(loanId, params, options) {
|
|
550
|
+
return this.client.request("PATCH", `/api/loans/${encodeURIComponent(loanId)}`, {
|
|
551
|
+
body: params,
|
|
552
|
+
...options
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Iterate every loan visible to the calling principal. Returns an
|
|
557
|
+
* `AsyncPager<T>` — consume with `for await`, `.toArray()`, or
|
|
558
|
+
* `.pages()`.
|
|
559
|
+
*
|
|
560
|
+
* GET /api/loans
|
|
561
|
+
*/
|
|
562
|
+
list(params, options) {
|
|
563
|
+
return this.client.paginate(
|
|
564
|
+
"/api/loans",
|
|
565
|
+
params,
|
|
566
|
+
options
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Assign a lender to this loan. Calls `POST /api/loans/{id}/assign`.
|
|
571
|
+
* Surfaced as a verb method rather than forcing it through `update()`
|
|
572
|
+
* because the assignment flow is semantically distinct from a generic
|
|
573
|
+
* field patch (it touches the lender-access table, not the loan row).
|
|
574
|
+
*/
|
|
575
|
+
async assign(loanId, params, options) {
|
|
576
|
+
return this.client.request("POST", `/api/loans/${encodeURIComponent(loanId)}/assign`, {
|
|
577
|
+
body: params,
|
|
578
|
+
...options
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Fetch the audit log for a loan.
|
|
583
|
+
*
|
|
584
|
+
* GET /api/loans/{loanId}/audit-log
|
|
585
|
+
*/
|
|
586
|
+
async auditLog(loanId, options) {
|
|
587
|
+
return this.client.request(
|
|
588
|
+
"GET",
|
|
589
|
+
`/api/loans/${encodeURIComponent(loanId)}/audit-log`,
|
|
590
|
+
options
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
var DEFAULT_FRESHNESS_TOLERANCE_SECONDS = 5 * 60;
|
|
595
|
+
var VERSION_1_PREFIX = "v1";
|
|
596
|
+
function verifyWebhookSignature(rawBody, signature, secret, options) {
|
|
597
|
+
if (typeof signature !== "string" || signature.length === 0) {
|
|
598
|
+
throw new WebhookSignatureError("SpreadSpace-Signature header is missing or empty.");
|
|
599
|
+
}
|
|
600
|
+
if (typeof secret !== "string" || secret.length === 0) {
|
|
601
|
+
throw new WebhookSignatureError("Webhook signing secret is missing or empty.");
|
|
602
|
+
}
|
|
603
|
+
if (rawBody === null || rawBody === void 0) {
|
|
604
|
+
throw new WebhookSignatureError("Webhook raw body is missing.");
|
|
605
|
+
}
|
|
606
|
+
const parsed = parseSignatureHeader(signature);
|
|
607
|
+
if (!parsed) {
|
|
608
|
+
throw new WebhookSignatureError("Webhook signature header is malformed.");
|
|
609
|
+
}
|
|
610
|
+
const { timestamp, hex } = parsed;
|
|
611
|
+
const bodyString = bodyToString(rawBody);
|
|
612
|
+
const expectedHex = computeHmacHex(secret, `${timestamp}.${bodyString}`);
|
|
613
|
+
if (!hexEquals(expectedHex, hex)) {
|
|
614
|
+
throw new WebhookSignatureError("Webhook signature does not match.");
|
|
615
|
+
}
|
|
616
|
+
const tolerance = options?.freshnessTolerance ?? DEFAULT_FRESHNESS_TOLERANCE_SECONDS;
|
|
617
|
+
const now = options?.currentTimestamp ?? Math.floor(Date.now() / 1e3);
|
|
618
|
+
const age = now - timestamp;
|
|
619
|
+
if (age > tolerance || age < -tolerance) {
|
|
620
|
+
throw new WebhookSignatureError(
|
|
621
|
+
`Webhook timestamp is outside the freshness window of ${tolerance}s.`
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
return true;
|
|
625
|
+
}
|
|
626
|
+
function verifyAndParseWebhook(rawBody, signature, secret, options) {
|
|
627
|
+
verifyWebhookSignature(rawBody, signature, secret, options);
|
|
628
|
+
const bodyString = bodyToString(rawBody);
|
|
629
|
+
let parsed;
|
|
630
|
+
try {
|
|
631
|
+
parsed = JSON.parse(bodyString);
|
|
632
|
+
} catch (cause) {
|
|
633
|
+
throw new WebhookSignatureError("Webhook body is not valid JSON.", cause);
|
|
634
|
+
}
|
|
635
|
+
if (!isPlainObject(parsed)) {
|
|
636
|
+
throw new WebhookSignatureError("Webhook body is not a JSON object.");
|
|
637
|
+
}
|
|
638
|
+
const candidate = parsed;
|
|
639
|
+
if (typeof candidate.type !== "string" || candidate.type.length === 0) {
|
|
640
|
+
throw new WebhookSignatureError("Webhook body is missing a `type` field.");
|
|
641
|
+
}
|
|
642
|
+
if (typeof candidate.id !== "string" || candidate.id.length === 0) {
|
|
643
|
+
throw new WebhookSignatureError("Webhook body is missing an `id` field.");
|
|
644
|
+
}
|
|
645
|
+
if (typeof candidate.created !== "number") {
|
|
646
|
+
throw new WebhookSignatureError("Webhook body is missing a numeric `created` field.");
|
|
647
|
+
}
|
|
648
|
+
if (typeof candidate.tenant_id !== "string" || candidate.tenant_id.length === 0) {
|
|
649
|
+
throw new WebhookSignatureError("Webhook body is missing a `tenant_id` field.");
|
|
650
|
+
}
|
|
651
|
+
if (!isPlainObject(candidate.data)) {
|
|
652
|
+
throw new WebhookSignatureError("Webhook body is missing a `data` object.");
|
|
653
|
+
}
|
|
654
|
+
return parsed;
|
|
655
|
+
}
|
|
656
|
+
var WebhookSignatureError = class _WebhookSignatureError extends Error {
|
|
657
|
+
cause;
|
|
658
|
+
constructor(message, cause) {
|
|
659
|
+
super(message);
|
|
660
|
+
this.name = "WebhookSignatureError";
|
|
661
|
+
this.cause = cause;
|
|
662
|
+
Object.setPrototypeOf(this, _WebhookSignatureError.prototype);
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
function parseSignatureHeader(header) {
|
|
666
|
+
const segments = header.split(",").filter((s) => s.length > 0);
|
|
667
|
+
if (segments.length < 2) return null;
|
|
668
|
+
let parsedTs = null;
|
|
669
|
+
let parsedV1 = null;
|
|
670
|
+
for (const raw of segments) {
|
|
671
|
+
const segment = raw.trim();
|
|
672
|
+
const eq = segment.indexOf("=");
|
|
673
|
+
if (eq <= 0 || eq === segment.length - 1) {
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
const key = segment.slice(0, eq).trim();
|
|
677
|
+
const value = segment.slice(eq + 1).trim();
|
|
678
|
+
if (key === "t") {
|
|
679
|
+
if (parsedTs !== null) return null;
|
|
680
|
+
if (!/^-?\d+$/.test(value)) return null;
|
|
681
|
+
const ts = Number(value);
|
|
682
|
+
if (!Number.isFinite(ts) || !Number.isInteger(ts)) return null;
|
|
683
|
+
parsedTs = ts;
|
|
684
|
+
} else if (key === VERSION_1_PREFIX) {
|
|
685
|
+
if (parsedV1 !== null) return null;
|
|
686
|
+
if (value.length === 0) return null;
|
|
687
|
+
parsedV1 = value;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
if (parsedTs === null || parsedV1 === null) return null;
|
|
691
|
+
return { timestamp: parsedTs, hex: parsedV1 };
|
|
692
|
+
}
|
|
693
|
+
function computeHmacHex(secret, signedPayload) {
|
|
694
|
+
const hmac = crypto.createHmac("sha256", Buffer.from(secret, "utf-8"));
|
|
695
|
+
hmac.update(Buffer.from(signedPayload, "utf-8"));
|
|
696
|
+
return hmac.digest("hex");
|
|
697
|
+
}
|
|
698
|
+
function hexEquals(expectedHex, providedHex) {
|
|
699
|
+
if (expectedHex.length !== providedHex.length) return false;
|
|
700
|
+
let expectedBytes;
|
|
701
|
+
let providedBytes;
|
|
702
|
+
try {
|
|
703
|
+
expectedBytes = Buffer.from(expectedHex, "hex");
|
|
704
|
+
providedBytes = Buffer.from(providedHex, "hex");
|
|
705
|
+
} catch {
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
if (expectedBytes.length !== providedBytes.length) return false;
|
|
709
|
+
if (expectedBytes.length * 2 !== expectedHex.length) return false;
|
|
710
|
+
if (providedBytes.length * 2 !== providedHex.length) return false;
|
|
711
|
+
return crypto.timingSafeEqual(
|
|
712
|
+
new Uint8Array(expectedBytes.buffer, expectedBytes.byteOffset, expectedBytes.byteLength),
|
|
713
|
+
new Uint8Array(providedBytes.buffer, providedBytes.byteOffset, providedBytes.byteLength)
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
function bodyToString(body) {
|
|
717
|
+
if (typeof body === "string") return body;
|
|
718
|
+
if (Buffer.isBuffer(body)) return body.toString("utf-8");
|
|
719
|
+
return Buffer.from(body).toString("utf-8");
|
|
720
|
+
}
|
|
721
|
+
function isPlainObject(v) {
|
|
722
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// src/resources/webhooks.ts
|
|
726
|
+
var WebhooksResource = class {
|
|
727
|
+
constructor(client) {
|
|
728
|
+
this.client = client;
|
|
729
|
+
}
|
|
730
|
+
client;
|
|
731
|
+
/**
|
|
732
|
+
* Verify a `SpreadSpace-Signature` header against a body and signing
|
|
733
|
+
* secret. Throws `WebhookSignatureError` on any failure.
|
|
734
|
+
*/
|
|
735
|
+
static verifySignature = verifyWebhookSignature;
|
|
736
|
+
/**
|
|
737
|
+
* Verify a webhook signature and JSON-parse the body into a typed
|
|
738
|
+
* `WebhookEvent`. Throws `WebhookSignatureError` on signature failure or
|
|
739
|
+
* invalid JSON.
|
|
740
|
+
*/
|
|
741
|
+
static verifyAndParse = verifyAndParseWebhook;
|
|
742
|
+
/**
|
|
743
|
+
* List configured webhook endpoints for this organization.
|
|
744
|
+
*
|
|
745
|
+
* GET /api/webhooks
|
|
746
|
+
*/
|
|
747
|
+
list(params, options) {
|
|
748
|
+
return this.client.paginate(
|
|
749
|
+
"/api/webhooks",
|
|
750
|
+
params,
|
|
751
|
+
options
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Retrieve a single webhook endpoint.
|
|
756
|
+
*
|
|
757
|
+
* GET /api/webhooks/{id}
|
|
758
|
+
*/
|
|
759
|
+
async retrieve(endpointId, options) {
|
|
760
|
+
return this.client.request(
|
|
761
|
+
"GET",
|
|
762
|
+
`/api/webhooks/${encodeURIComponent(endpointId)}`,
|
|
763
|
+
options
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Create a new webhook endpoint. The response carries the plaintext
|
|
768
|
+
* signing secret exactly once — store it server-side, do not log it.
|
|
769
|
+
*
|
|
770
|
+
* POST /api/webhooks
|
|
771
|
+
*/
|
|
772
|
+
async create(params, options) {
|
|
773
|
+
return this.client.request("POST", "/api/webhooks", { body: params, ...options });
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Update endpoint metadata (URL, subscribed event types, enabled flag).
|
|
777
|
+
*
|
|
778
|
+
* PATCH /api/webhooks/{id}
|
|
779
|
+
*/
|
|
780
|
+
async update(endpointId, params, options) {
|
|
781
|
+
return this.client.request(
|
|
782
|
+
"PATCH",
|
|
783
|
+
`/api/webhooks/${encodeURIComponent(endpointId)}`,
|
|
784
|
+
{ body: params, ...options }
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Delete a webhook endpoint.
|
|
789
|
+
*
|
|
790
|
+
* DELETE /api/webhooks/{id}
|
|
791
|
+
*/
|
|
792
|
+
async delete(endpointId, options) {
|
|
793
|
+
return this.client.request(
|
|
794
|
+
"DELETE",
|
|
795
|
+
`/api/webhooks/${encodeURIComponent(endpointId)}`,
|
|
796
|
+
options
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Rotate an endpoint's signing secret. The old secret remains valid for
|
|
801
|
+
* a grace window so deliveries in flight aren't dropped. Returns the new
|
|
802
|
+
* plaintext secret exactly once.
|
|
803
|
+
*
|
|
804
|
+
* POST /api/webhooks/{id}/rotate
|
|
805
|
+
*/
|
|
806
|
+
async rotateSecret(endpointId, params = {}, options) {
|
|
807
|
+
return this.client.request(
|
|
808
|
+
"POST",
|
|
809
|
+
`/api/webhooks/${encodeURIComponent(endpointId)}/rotate`,
|
|
810
|
+
{ body: params, ...options }
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Iterate delivery attempts for an endpoint.
|
|
815
|
+
*
|
|
816
|
+
* GET /api/webhooks/{id}/deliveries
|
|
817
|
+
*/
|
|
818
|
+
deliveries(endpointId, params, options) {
|
|
819
|
+
return this.client.paginate(
|
|
820
|
+
`/api/webhooks/${encodeURIComponent(endpointId)}/deliveries`,
|
|
821
|
+
params,
|
|
822
|
+
options
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Manually replay a delivery (e.g. after fixing a downstream outage).
|
|
827
|
+
*
|
|
828
|
+
* POST /api/webhooks/{id}/deliveries/{deliveryId}/replay
|
|
829
|
+
*/
|
|
830
|
+
async replayDelivery(endpointId, deliveryId, params = {}, options) {
|
|
831
|
+
return this.client.request(
|
|
832
|
+
"POST",
|
|
833
|
+
`/api/webhooks/${encodeURIComponent(endpointId)}/deliveries/${encodeURIComponent(deliveryId)}/replay`,
|
|
834
|
+
{ body: params, ...options }
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
|
|
839
|
+
// src/version.ts
|
|
840
|
+
var SDK_VERSION = "0.1.0";
|
|
841
|
+
var DEFAULT_API_VERSION = "2026-05-03";
|
|
842
|
+
|
|
843
|
+
// src/client.ts
|
|
844
|
+
var DEFAULT_BASE_URL = "https://api.spreadspace.app";
|
|
845
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
846
|
+
var DEFAULT_MAX_RETRIES = 3;
|
|
847
|
+
var RETRY_BASE_DELAY_MS = 500;
|
|
848
|
+
var RETRY_MAX_DELAY_MS = 3e4;
|
|
849
|
+
var SpreadSpaceClient = class {
|
|
850
|
+
/** Resolved base URL (no trailing slash). */
|
|
851
|
+
baseUrl;
|
|
852
|
+
/** Resolved API version string (the `SpreadSpace-Version` header value). */
|
|
853
|
+
apiVersion;
|
|
854
|
+
/** Public so resource files can pull it for `paginate()` URL construction. */
|
|
855
|
+
maxRetries;
|
|
856
|
+
// ── Resources ────────────────────────────────────────────────────────────
|
|
857
|
+
loans;
|
|
858
|
+
borrowers;
|
|
859
|
+
documents;
|
|
860
|
+
extractions;
|
|
861
|
+
jobs;
|
|
862
|
+
webhooks;
|
|
863
|
+
embed;
|
|
864
|
+
// ── Internals ────────────────────────────────────────────────────────────
|
|
865
|
+
apiKey;
|
|
866
|
+
timeoutMs;
|
|
867
|
+
fetchImpl;
|
|
868
|
+
idempotencyKeyGenerator;
|
|
869
|
+
constructor(options) {
|
|
870
|
+
if (!options || typeof options.apiKey !== "string" || options.apiKey.length === 0) {
|
|
871
|
+
throw new TypeError(
|
|
872
|
+
"SpreadSpaceClient requires an apiKey. Issue one from the dashboard at /settings/api-keys."
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
this.apiKey = options.apiKey;
|
|
876
|
+
let baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
|
|
877
|
+
while (baseUrl.endsWith("/")) baseUrl = baseUrl.slice(0, -1);
|
|
878
|
+
this.baseUrl = baseUrl;
|
|
879
|
+
this.apiVersion = options.apiVersion ?? DEFAULT_API_VERSION;
|
|
880
|
+
this.timeoutMs = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
881
|
+
this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
882
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
883
|
+
this.idempotencyKeyGenerator = options.idempotencyKeyGenerator ?? crypto.randomUUID;
|
|
884
|
+
if (typeof this.fetchImpl !== "function") {
|
|
885
|
+
throw new TypeError(
|
|
886
|
+
"SpreadSpaceClient requires a fetch implementation. Node 18+ provides one globally; pass a polyfill via options.fetch otherwise."
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
this.loans = new LoansResource(this);
|
|
890
|
+
this.borrowers = new BorrowersResource(this);
|
|
891
|
+
this.documents = new DocumentsResource(this);
|
|
892
|
+
this.extractions = new ExtractionsResource(this);
|
|
893
|
+
this.jobs = new JobsResource(this);
|
|
894
|
+
this.webhooks = new WebhooksResource(this);
|
|
895
|
+
this.embed = new EmbedResource(this);
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Low-level request method. Resource layer wraps this with ergonomic
|
|
899
|
+
* method names; integrators can call it directly to hit endpoints not yet
|
|
900
|
+
* surfaced via a typed resource.
|
|
901
|
+
*
|
|
902
|
+
* Returns the parsed JSON body. For 204 No Content responses (currently
|
|
903
|
+
* none in the API but reserved), returns `undefined as T`.
|
|
904
|
+
*/
|
|
905
|
+
async request(method, path, options = {}) {
|
|
906
|
+
const url = this.buildUrl(path, options.query);
|
|
907
|
+
const headers = this.buildHeaders(method, options);
|
|
908
|
+
const body = options.body !== void 0 ? JSON.stringify(options.body) : void 0;
|
|
909
|
+
const maxRetries = options.maxRetries ?? this.maxRetries;
|
|
910
|
+
let attempt = 0;
|
|
911
|
+
let lastError;
|
|
912
|
+
while (attempt <= maxRetries) {
|
|
913
|
+
const controller = new AbortController();
|
|
914
|
+
const timeoutHandle = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
915
|
+
const signal = options.signal ? composeAbortSignals(controller.signal, options.signal) : controller.signal;
|
|
916
|
+
let response;
|
|
917
|
+
try {
|
|
918
|
+
response = await this.fetchImpl(url, {
|
|
919
|
+
method,
|
|
920
|
+
headers,
|
|
921
|
+
body,
|
|
922
|
+
signal
|
|
923
|
+
});
|
|
924
|
+
} catch (err) {
|
|
925
|
+
clearTimeout(timeoutHandle);
|
|
926
|
+
lastError = err;
|
|
927
|
+
if (attempt < maxRetries) {
|
|
928
|
+
await sleep(this.computeRetryDelay(attempt, void 0));
|
|
929
|
+
attempt += 1;
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
932
|
+
throw new NetworkError(
|
|
933
|
+
`SpreadSpace request to ${method} ${url} failed: ${describeError(err)}`,
|
|
934
|
+
err
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
clearTimeout(timeoutHandle);
|
|
938
|
+
const requestId = response.headers.get("X-Request-ID") ?? void 0;
|
|
939
|
+
if (response.ok) {
|
|
940
|
+
return await this.parseSuccessBody(response);
|
|
941
|
+
}
|
|
942
|
+
const status = response.status;
|
|
943
|
+
const retryable = status === 429 || status >= 500 && status <= 599;
|
|
944
|
+
if (retryable && attempt < maxRetries) {
|
|
945
|
+
const retryAfterSec = parseRetryAfter(response.headers.get("Retry-After"));
|
|
946
|
+
await sleep(this.computeRetryDelay(attempt, retryAfterSec));
|
|
947
|
+
attempt += 1;
|
|
948
|
+
try {
|
|
949
|
+
await response.body?.cancel();
|
|
950
|
+
} catch {
|
|
951
|
+
}
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
throw await this.buildErrorFromResponse(response, requestId);
|
|
955
|
+
}
|
|
956
|
+
throw new NetworkError(
|
|
957
|
+
`SpreadSpace request to ${method} ${url} exhausted retries (last error: ${describeError(lastError)}).`,
|
|
958
|
+
lastError
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* Convenience wrapper around `paginate()` for resource list methods. Lets
|
|
963
|
+
* each resource simply do `return this.client.paginate(...)` rather than
|
|
964
|
+
* importing the helper directly.
|
|
965
|
+
*/
|
|
966
|
+
paginate(path, params, options) {
|
|
967
|
+
return paginate(this, path, params, options);
|
|
968
|
+
}
|
|
969
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
970
|
+
// Internals
|
|
971
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
972
|
+
buildUrl(path, query) {
|
|
973
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
974
|
+
const url = new URL(this.baseUrl + normalizedPath);
|
|
975
|
+
if (query) {
|
|
976
|
+
for (const [k, v] of Object.entries(query)) {
|
|
977
|
+
if (v === void 0) continue;
|
|
978
|
+
url.searchParams.set(k, String(v));
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
return url.toString();
|
|
982
|
+
}
|
|
983
|
+
buildHeaders(method, options) {
|
|
984
|
+
const headers = new Headers();
|
|
985
|
+
headers.set("Authorization", `Bearer ${this.apiKey}`);
|
|
986
|
+
headers.set("SpreadSpace-Version", options.apiVersion ?? this.apiVersion);
|
|
987
|
+
headers.set("Accept", "application/json");
|
|
988
|
+
headers.set("User-Agent", this.buildUserAgent());
|
|
989
|
+
if (options.body !== void 0) {
|
|
990
|
+
headers.set("Content-Type", "application/json; charset=utf-8");
|
|
991
|
+
}
|
|
992
|
+
if (method !== "GET") {
|
|
993
|
+
const explicit = options.idempotencyKey;
|
|
994
|
+
if (explicit === null) ; else if (typeof explicit === "string" && explicit.length > 0) {
|
|
995
|
+
headers.set("Idempotency-Key", explicit);
|
|
996
|
+
} else {
|
|
997
|
+
headers.set("Idempotency-Key", this.idempotencyKeyGenerator());
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
return headers;
|
|
1001
|
+
}
|
|
1002
|
+
buildUserAgent() {
|
|
1003
|
+
const nodeVersion = typeof process !== "undefined" ? process.version : "unknown";
|
|
1004
|
+
return `spreadspace-node/${SDK_VERSION} node/${nodeVersion}`;
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Compute the next retry delay. Exponential backoff with full jitter,
|
|
1008
|
+
* floored by `Retry-After` if the server provided one (we never retry
|
|
1009
|
+
* faster than the server asked us to).
|
|
1010
|
+
*
|
|
1011
|
+
* base = min(maxDelay, baseDelay * 2^attempt)
|
|
1012
|
+
* delay = random(0, base)
|
|
1013
|
+
*
|
|
1014
|
+
* Full jitter (vs. equal jitter) is the AWS-recommended default — it
|
|
1015
|
+
* minimizes thundering-herd across many concurrent retrying clients.
|
|
1016
|
+
*/
|
|
1017
|
+
computeRetryDelay(attempt, retryAfterSec) {
|
|
1018
|
+
const expBackoff = Math.min(RETRY_MAX_DELAY_MS, RETRY_BASE_DELAY_MS * 2 ** attempt);
|
|
1019
|
+
const jittered = Math.floor(Math.random() * expBackoff);
|
|
1020
|
+
if (retryAfterSec !== void 0 && retryAfterSec > 0) {
|
|
1021
|
+
const retryAfterMs = retryAfterSec * 1e3;
|
|
1022
|
+
return Math.max(jittered, retryAfterMs);
|
|
1023
|
+
}
|
|
1024
|
+
return jittered;
|
|
1025
|
+
}
|
|
1026
|
+
async parseSuccessBody(response) {
|
|
1027
|
+
if (response.status === 204) {
|
|
1028
|
+
return void 0;
|
|
1029
|
+
}
|
|
1030
|
+
const text = await response.text();
|
|
1031
|
+
if (text.length === 0) {
|
|
1032
|
+
return void 0;
|
|
1033
|
+
}
|
|
1034
|
+
try {
|
|
1035
|
+
return JSON.parse(text);
|
|
1036
|
+
} catch (err) {
|
|
1037
|
+
throw new NetworkError(
|
|
1038
|
+
`Failed to parse SpreadSpace response as JSON (status=${response.status}): ${describeError(err)}`,
|
|
1039
|
+
err
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
async buildErrorFromResponse(response, requestId) {
|
|
1044
|
+
const status = response.status;
|
|
1045
|
+
let rawBody;
|
|
1046
|
+
let type = "unknown";
|
|
1047
|
+
let message = `SpreadSpace API request failed with status ${status}.`;
|
|
1048
|
+
let details;
|
|
1049
|
+
let resolvedRequestId = requestId;
|
|
1050
|
+
try {
|
|
1051
|
+
const text = await response.text();
|
|
1052
|
+
if (text.length > 0) {
|
|
1053
|
+
try {
|
|
1054
|
+
const parsed = JSON.parse(text);
|
|
1055
|
+
rawBody = parsed;
|
|
1056
|
+
if (typeof parsed === "object" && parsed !== null && "error" in parsed && typeof parsed.error === "object" && parsed.error !== null) {
|
|
1057
|
+
const errBody = parsed.error;
|
|
1058
|
+
if (typeof errBody.type === "string") type = errBody.type;
|
|
1059
|
+
if (typeof errBody.message === "string") message = errBody.message;
|
|
1060
|
+
if (errBody.details && typeof errBody.details === "object" && !Array.isArray(errBody.details)) {
|
|
1061
|
+
details = errBody.details;
|
|
1062
|
+
}
|
|
1063
|
+
if (resolvedRequestId === void 0 && typeof errBody.request_id === "string") {
|
|
1064
|
+
resolvedRequestId = errBody.request_id;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
} catch {
|
|
1068
|
+
rawBody = text;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
} catch {
|
|
1072
|
+
}
|
|
1073
|
+
const ErrorClass = classifyError(status);
|
|
1074
|
+
const ctorParams = {
|
|
1075
|
+
type,
|
|
1076
|
+
message,
|
|
1077
|
+
statusCode: status,
|
|
1078
|
+
requestId: resolvedRequestId,
|
|
1079
|
+
rawBody,
|
|
1080
|
+
details
|
|
1081
|
+
};
|
|
1082
|
+
return new ErrorClass(ctorParams);
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
function sleep(ms) {
|
|
1086
|
+
if (ms <= 0) return Promise.resolve();
|
|
1087
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1088
|
+
}
|
|
1089
|
+
function parseRetryAfter(value) {
|
|
1090
|
+
if (value === null || value.length === 0) return void 0;
|
|
1091
|
+
const asInt = Number(value);
|
|
1092
|
+
if (Number.isFinite(asInt) && Number.isInteger(asInt) && asInt >= 0) {
|
|
1093
|
+
return asInt;
|
|
1094
|
+
}
|
|
1095
|
+
const asDate = Date.parse(value);
|
|
1096
|
+
if (!Number.isNaN(asDate)) {
|
|
1097
|
+
const deltaMs = asDate - Date.now();
|
|
1098
|
+
if (deltaMs > 0) return Math.ceil(deltaMs / 1e3);
|
|
1099
|
+
}
|
|
1100
|
+
return void 0;
|
|
1101
|
+
}
|
|
1102
|
+
function describeError(err) {
|
|
1103
|
+
if (err instanceof Error) return err.message;
|
|
1104
|
+
if (typeof err === "string") return err;
|
|
1105
|
+
return String(err);
|
|
1106
|
+
}
|
|
1107
|
+
function composeAbortSignals(a, b) {
|
|
1108
|
+
const maybeAny = AbortSignal.any;
|
|
1109
|
+
if (typeof maybeAny === "function") {
|
|
1110
|
+
return maybeAny([a, b]);
|
|
1111
|
+
}
|
|
1112
|
+
const controller = new AbortController();
|
|
1113
|
+
const onAbort = (source) => {
|
|
1114
|
+
if (controller.signal.aborted) return;
|
|
1115
|
+
const reason = source.reason;
|
|
1116
|
+
controller.abort(reason);
|
|
1117
|
+
};
|
|
1118
|
+
if (a.aborted) {
|
|
1119
|
+
onAbort(a);
|
|
1120
|
+
} else {
|
|
1121
|
+
a.addEventListener("abort", () => onAbort(a), { once: true });
|
|
1122
|
+
}
|
|
1123
|
+
if (b.aborted) {
|
|
1124
|
+
onAbort(b);
|
|
1125
|
+
} else {
|
|
1126
|
+
b.addEventListener("abort", () => onAbort(b), { once: true });
|
|
1127
|
+
}
|
|
1128
|
+
return controller.signal;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
exports.AuthenticationError = AuthenticationError;
|
|
1132
|
+
exports.BorrowersResource = BorrowersResource;
|
|
1133
|
+
exports.ConflictError = ConflictError;
|
|
1134
|
+
exports.DEFAULT_API_VERSION = DEFAULT_API_VERSION;
|
|
1135
|
+
exports.DocumentsResource = DocumentsResource;
|
|
1136
|
+
exports.EmbedResource = EmbedResource;
|
|
1137
|
+
exports.EmbedSessionsResource = EmbedSessionsResource;
|
|
1138
|
+
exports.ExtractionsResource = ExtractionsResource;
|
|
1139
|
+
exports.InvalidRequestError = InvalidRequestError;
|
|
1140
|
+
exports.JobsResource = JobsResource;
|
|
1141
|
+
exports.LoansResource = LoansResource;
|
|
1142
|
+
exports.NetworkError = NetworkError;
|
|
1143
|
+
exports.NotFoundError = NotFoundError;
|
|
1144
|
+
exports.PermissionError = PermissionError;
|
|
1145
|
+
exports.RateLimitError = RateLimitError;
|
|
1146
|
+
exports.SDK_VERSION = SDK_VERSION;
|
|
1147
|
+
exports.ServerError = ServerError;
|
|
1148
|
+
exports.SpreadSpaceClient = SpreadSpaceClient;
|
|
1149
|
+
exports.SpreadSpaceError = SpreadSpaceError;
|
|
1150
|
+
exports.WebhookSignatureError = WebhookSignatureError;
|
|
1151
|
+
exports.WebhooksResource = WebhooksResource;
|
|
1152
|
+
exports.verifyAndParseWebhook = verifyAndParseWebhook;
|
|
1153
|
+
exports.verifyWebhookSignature = verifyWebhookSignature;
|
|
1154
|
+
//# sourceMappingURL=index.cjs.map
|
|
1155
|
+
//# sourceMappingURL=index.cjs.map
|