@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.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