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