@spreadspace/sdk 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,1284 @@
1
+ import {
2
+ WebhookSignatureError,
3
+ verifyAndParseWebhook,
4
+ verifyWebhook
5
+ } from "./chunk-2JW6MGIK.js";
6
+
7
+ // src/transport.ts
8
+ import { randomUUID } from "crypto";
9
+
10
+ // src/errors.ts
11
+ var SpreadSpaceError = class extends Error {
12
+ /** Stable machine code from `error.type` (match on this, never `message`). */
13
+ type;
14
+ /** HTTP status code. */
15
+ statusCode;
16
+ /** `X-Request-ID` echoed from the server. Quote in support tickets. */
17
+ requestId;
18
+ /** Raw response body for debugging — parsed JSON or a string. */
19
+ rawBody;
20
+ /** Optional structured details (e.g. a field name on a validation error). */
21
+ details;
22
+ /** `Retry-After` seconds, when the server provided one. */
23
+ retryAfter;
24
+ constructor(params) {
25
+ super(params.message);
26
+ this.name = new.target.name;
27
+ this.type = params.type;
28
+ this.statusCode = params.statusCode;
29
+ this.requestId = params.requestId;
30
+ this.rawBody = params.rawBody;
31
+ this.details = params.details;
32
+ this.retryAfter = params.retryAfter;
33
+ Object.setPrototypeOf(this, new.target.prototype);
34
+ }
35
+ };
36
+ var InvalidRequestError = class extends SpreadSpaceError {
37
+ };
38
+ var AuthenticationError = class extends SpreadSpaceError {
39
+ };
40
+ var PermissionError = class extends SpreadSpaceError {
41
+ };
42
+ var NotFoundError = class extends SpreadSpaceError {
43
+ };
44
+ var ConflictError = class extends SpreadSpaceError {
45
+ };
46
+ var RateLimitError = class extends SpreadSpaceError {
47
+ };
48
+ var ServerError = class extends SpreadSpaceError {
49
+ };
50
+ var NetworkError = class _NetworkError extends Error {
51
+ cause;
52
+ constructor(message, cause) {
53
+ super(message);
54
+ this.name = "NetworkError";
55
+ this.cause = cause;
56
+ Object.setPrototypeOf(this, _NetworkError.prototype);
57
+ }
58
+ };
59
+ function classifyError(statusCode, _type) {
60
+ if (statusCode === 400) return InvalidRequestError;
61
+ if (statusCode === 401) return AuthenticationError;
62
+ if (statusCode === 403) return PermissionError;
63
+ if (statusCode === 404) return NotFoundError;
64
+ if (statusCode === 409) return ConflictError;
65
+ if (statusCode === 429) return RateLimitError;
66
+ if (statusCode >= 500) return ServerError;
67
+ return SpreadSpaceError;
68
+ }
69
+
70
+ // src/money.ts
71
+ import { Decimal } from "decimal.js";
72
+ import { isLosslessNumber, parse } from "lossless-json";
73
+ var SCALAR_MONEY_KEYS = /* @__PURE__ */ new Set([
74
+ // BankCounterparty* (direction splits, rollups, relationship groups, review
75
+ // items, merge/split responses).
76
+ "net_amount",
77
+ "inflow_amount",
78
+ "outflow_amount",
79
+ "amount",
80
+ "advance_amount",
81
+ "remittance_amount",
82
+ // BankCounterpartyRelationshipGroup.cost_of_capital — a dollar figure
83
+ // (`net < 0 ? -net : 0`), NOT a percentage. Money.
84
+ "cost_of_capital",
85
+ // Loan create/update/response.
86
+ "requested_amount",
87
+ // BorrowerResponse facility-size aggregates.
88
+ "mean_facility_size",
89
+ "median_facility_size",
90
+ // DocumentLogEntry / usage / job-status billing figures (USD).
91
+ "billed_amount_usd",
92
+ "estimated_cost_usd"
93
+ ]);
94
+ var PAYLOAD_MONEY_KEYS = /* @__PURE__ */ new Set([
95
+ // P&L / cash-flow / balance-sheet rows (List<decimal>).
96
+ "values",
97
+ "total",
98
+ // AR/AP aging rows (List<decimal>).
99
+ "vals",
100
+ "total_vals",
101
+ // Bank-statement period summary (scalar decimal each).
102
+ "deposits",
103
+ "withdrawals",
104
+ "fees",
105
+ "interest",
106
+ "net",
107
+ // Generic leaf amount inside the payload tree.
108
+ "amount",
109
+ // Bank TTM combined month / summary tiles (Spreading/PayloadBank.cs).
110
+ "total_deposits",
111
+ "total_withdrawals",
112
+ "net_change",
113
+ "total_deposits_ex_internal_transfers",
114
+ "total_withdrawals_ex_internal_transfers",
115
+ "net_change_ex_internal_transfers",
116
+ "ending_balance",
117
+ "value"
118
+ ]);
119
+ var PAYLOAD_SCOPE_KEYS = /* @__PURE__ */ new Set(["payload"]);
120
+ function parseBodyWithExactMoney(text) {
121
+ const tree = parse(text);
122
+ return convertDeep(tree, false);
123
+ }
124
+ function convertDeep(value, inPayload, key, inMoneyArray = false) {
125
+ if (isLosslessNumber(value)) {
126
+ if (inMoneyArray || isMoneyKey(key, inPayload)) {
127
+ return losslessToDecimal(value);
128
+ }
129
+ return losslessToNumber(value);
130
+ }
131
+ if (Array.isArray(value)) {
132
+ const elementIsMoney = isMoneyKey(key, inPayload);
133
+ return value.map((item) => convertDeep(item, inPayload, void 0, elementIsMoney));
134
+ }
135
+ if (isPlainObject(value)) {
136
+ const out = {};
137
+ for (const [childKey, childValue] of Object.entries(value)) {
138
+ const childInPayload = inPayload || PAYLOAD_SCOPE_KEYS.has(childKey);
139
+ out[childKey] = convertDeep(childValue, childInPayload, childKey, false);
140
+ }
141
+ return out;
142
+ }
143
+ return value;
144
+ }
145
+ function isMoneyKey(key, inPayload) {
146
+ if (key === void 0) return false;
147
+ if (SCALAR_MONEY_KEYS.has(key)) return true;
148
+ return inPayload && PAYLOAD_MONEY_KEYS.has(key);
149
+ }
150
+ function losslessToDecimal(value) {
151
+ return new Decimal(value.toString());
152
+ }
153
+ function losslessToNumber(value) {
154
+ const v = value.valueOf();
155
+ return typeof v === "bigint" ? Number(v) : v;
156
+ }
157
+ function isPlainObject(value) {
158
+ return typeof value === "object" && value !== null && !Array.isArray(value);
159
+ }
160
+
161
+ // src/version.ts
162
+ var SDK_VERSION = "0.1.0";
163
+ var DEFAULT_API_VERSION = "2026-05-03";
164
+
165
+ // src/transport.ts
166
+ var DEFAULT_BASE_URL = "https://api.spreadspace.ai";
167
+ var DEFAULT_TIMEOUT_MS = 6e4;
168
+ var DEFAULT_MAX_RETRIES = 2;
169
+ var RETRY_BASE_DELAY_MS = 500;
170
+ var RETRY_MAX_DELAY_MS = 3e4;
171
+ var SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
172
+ var Transport = class {
173
+ /** Resolved base URL (no trailing slash). */
174
+ baseUrl;
175
+ /** Resolved API version (the `SpreadSpace-Version` header value). */
176
+ apiVersion;
177
+ /** Default retry ceiling. Public so helpers can construct URLs / pagers off it. */
178
+ maxRetries;
179
+ apiKey;
180
+ timeoutMs;
181
+ fetchImpl;
182
+ sleepImpl;
183
+ idempotencyKeyGenerator;
184
+ userAgent;
185
+ constructor(options) {
186
+ if (!options || typeof options.apiKey !== "string" || options.apiKey.length === 0) {
187
+ throw new TypeError(
188
+ "Transport requires an apiKey. Issue one from the dashboard at /settings/api-keys."
189
+ );
190
+ }
191
+ this.apiKey = options.apiKey;
192
+ let baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
193
+ while (baseUrl.endsWith("/")) baseUrl = baseUrl.slice(0, -1);
194
+ this.baseUrl = baseUrl;
195
+ this.apiVersion = options.apiVersion ?? DEFAULT_API_VERSION;
196
+ this.timeoutMs = options.timeout ?? DEFAULT_TIMEOUT_MS;
197
+ this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
198
+ this.fetchImpl = options.fetch ?? globalThis.fetch;
199
+ this.sleepImpl = options.sleep ?? defaultSleep;
200
+ this.idempotencyKeyGenerator = options.idempotencyKeyGenerator ?? randomUUID;
201
+ this.userAgent = `spreadspace-sdk/${SDK_VERSION} node/${nodeVersion()}`;
202
+ if (typeof this.fetchImpl !== "function") {
203
+ throw new TypeError(
204
+ "Transport requires a fetch implementation. Node 18+ provides one globally; pass a polyfill via options.fetch otherwise."
205
+ );
206
+ }
207
+ }
208
+ /**
209
+ * Issue an API request and return the decoded JSON body (or `undefined` for
210
+ * an empty / 204 response). Integrators can call this directly to hit
211
+ * endpoints not yet surfaced via a typed resource.
212
+ */
213
+ async request(method, path, options = {}) {
214
+ const url = this.buildUrl(path, options.query);
215
+ const headers = this.buildHeaders(method, options);
216
+ const body = options.body !== void 0 ? JSON.stringify(options.body) : void 0;
217
+ const response = await this.send(url, { method, headers, body }, {
218
+ maxRetries: options.maxRetries ?? this.maxRetries,
219
+ signal: options.signal,
220
+ describe: `${method} ${url}`
221
+ });
222
+ if (response.ok) {
223
+ return await this.parseSuccessBody(response);
224
+ }
225
+ const requestId = response.headers.get("X-Request-ID") ?? void 0;
226
+ throw await this.buildErrorFromResponse(response, requestId);
227
+ }
228
+ /**
229
+ * Single retry path shared by `request()` and `putPresigned()` (mirrors the
230
+ * Python `_send`). Issues `fetch` with a per-attempt timeout, retrying 429 +
231
+ * 5xx + transport failures on the full-jitter backoff curve, honoring
232
+ * `Retry-After`. Returns the final `Response` (success OR a non-retryable /
233
+ * retries-exhausted error status — callers decide what to do with it); throws
234
+ * `NetworkError` only when transport failures exhaust retries.
235
+ */
236
+ async send(url, init, opts) {
237
+ const { maxRetries, signal: callerSignal, describe } = opts;
238
+ let attempt = 0;
239
+ let lastError;
240
+ while (attempt <= maxRetries) {
241
+ const controller = new AbortController();
242
+ const timeoutHandle = setTimeout(() => controller.abort(), this.timeoutMs);
243
+ const signal = callerSignal ? composeAbortSignals(controller.signal, callerSignal) : controller.signal;
244
+ let response;
245
+ try {
246
+ response = await this.fetchImpl(url, { ...init, signal });
247
+ } catch (err) {
248
+ clearTimeout(timeoutHandle);
249
+ lastError = err;
250
+ if (attempt < maxRetries) {
251
+ await this.sleepImpl(this.computeRetryDelay(attempt, void 0));
252
+ attempt += 1;
253
+ continue;
254
+ }
255
+ throw new NetworkError(`SpreadSpace request to ${describe} failed: ${describeError(err)}`, err);
256
+ }
257
+ clearTimeout(timeoutHandle);
258
+ const status = response.status;
259
+ const retryable = status === 429 || status >= 500 && status <= 599;
260
+ if (retryable && attempt < maxRetries) {
261
+ const retryAfterSec = parseRetryAfter(response.headers.get("Retry-After"));
262
+ await this.sleepImpl(this.computeRetryDelay(attempt, retryAfterSec));
263
+ attempt += 1;
264
+ try {
265
+ await response.body?.cancel();
266
+ } catch {
267
+ }
268
+ continue;
269
+ }
270
+ return response;
271
+ }
272
+ throw new NetworkError(
273
+ `SpreadSpace request to ${describe} exhausted retries (last error: ${describeError(lastError)}).`,
274
+ lastError
275
+ );
276
+ }
277
+ /**
278
+ * PUT raw bytes straight to a presigned S3 URL (out-of-band, no auth).
279
+ *
280
+ * `contentType` MUST equal the value sent when minting the URL — it is part
281
+ * of the V4 signature. No SSE headers (bucket-default KMS applies). No
282
+ * Authorization / version headers; this does not hit a SpreadSpace endpoint.
283
+ * Retries on transport/5xx like `request()`; throws `SpreadSpaceError` on a
284
+ * non-2xx S3 status.
285
+ */
286
+ async putPresigned(url, data, contentType, options = {}) {
287
+ const response = await this.send(
288
+ url,
289
+ { method: "PUT", headers: { "Content-Type": contentType }, body: data },
290
+ {
291
+ maxRetries: options.maxRetries ?? this.maxRetries,
292
+ signal: options.signal,
293
+ describe: `PUT ${url}`
294
+ }
295
+ );
296
+ if (response.ok) {
297
+ return response;
298
+ }
299
+ const text = await safeReadText(response);
300
+ throw new SpreadSpaceError({
301
+ type: "presigned_upload_failed",
302
+ message: `Presigned upload failed: HTTP ${response.status}`,
303
+ statusCode: response.status,
304
+ rawBody: text
305
+ });
306
+ }
307
+ // -- internals ----------------------------------------------------------
308
+ buildUrl(path, query) {
309
+ const normalizedPath = path.startsWith("/") ? path : `/${path}`;
310
+ const url = new URL(this.baseUrl + normalizedPath);
311
+ if (query) {
312
+ for (const [k, v] of Object.entries(query)) {
313
+ if (v === void 0) continue;
314
+ url.searchParams.set(k, String(v));
315
+ }
316
+ }
317
+ return url.toString();
318
+ }
319
+ buildHeaders(method, options) {
320
+ const headers = new Headers();
321
+ headers.set("Authorization", `Bearer ${this.apiKey}`);
322
+ headers.set("SpreadSpace-Version", options.apiVersion ?? this.apiVersion);
323
+ headers.set("Accept", "application/json");
324
+ headers.set("User-Agent", this.userAgent);
325
+ if (options.body !== void 0) {
326
+ headers.set("Content-Type", "application/json; charset=utf-8");
327
+ }
328
+ if (!SAFE_METHODS.has(method)) {
329
+ const explicit = options.idempotencyKey;
330
+ if (explicit === null) {
331
+ } else if (typeof explicit === "string" && explicit.length > 0) {
332
+ headers.set("Idempotency-Key", explicit);
333
+ } else {
334
+ headers.set("Idempotency-Key", this.idempotencyKeyGenerator());
335
+ }
336
+ }
337
+ return headers;
338
+ }
339
+ /**
340
+ * Next retry delay. Exponential backoff with full jitter, floored by
341
+ * `Retry-After` when the server provided one (never retry faster than asked).
342
+ *
343
+ * base = min(maxDelay, baseDelay * 2^attempt)
344
+ * delay = random(0, base)
345
+ *
346
+ * Full jitter (AWS-recommended) minimizes thundering-herd across clients.
347
+ */
348
+ computeRetryDelay(attempt, retryAfterSec) {
349
+ const expBackoff = Math.min(RETRY_MAX_DELAY_MS, RETRY_BASE_DELAY_MS * 2 ** attempt);
350
+ const jittered = Math.floor(Math.random() * expBackoff);
351
+ if (retryAfterSec !== void 0 && retryAfterSec > 0) {
352
+ return Math.max(jittered, retryAfterSec * 1e3);
353
+ }
354
+ return jittered;
355
+ }
356
+ async parseSuccessBody(response) {
357
+ if (response.status === 204) {
358
+ return void 0;
359
+ }
360
+ const text = await response.text();
361
+ if (text.length === 0) {
362
+ return void 0;
363
+ }
364
+ try {
365
+ return parseBodyWithExactMoney(text);
366
+ } catch (err) {
367
+ throw new NetworkError(
368
+ `Failed to parse SpreadSpace response as JSON (status=${response.status}): ${describeError(err)}`,
369
+ err
370
+ );
371
+ }
372
+ }
373
+ async buildErrorFromResponse(response, requestId) {
374
+ const status = response.status;
375
+ let rawBody;
376
+ let type = "unknown";
377
+ let message = `SpreadSpace API request failed with status ${status}.`;
378
+ let details;
379
+ let resolvedRequestId = requestId;
380
+ try {
381
+ const text = await response.text();
382
+ if (text.length > 0) {
383
+ try {
384
+ const parsed = JSON.parse(text);
385
+ rawBody = parsed;
386
+ if (typeof parsed === "object" && parsed !== null && "error" in parsed && typeof parsed.error === "object" && parsed.error !== null) {
387
+ const errBody = parsed.error;
388
+ if (typeof errBody.type === "string") type = errBody.type;
389
+ if (typeof errBody.message === "string") message = errBody.message;
390
+ if (errBody.details && typeof errBody.details === "object" && !Array.isArray(errBody.details)) {
391
+ details = errBody.details;
392
+ }
393
+ if (resolvedRequestId === void 0 && typeof errBody.request_id === "string") {
394
+ resolvedRequestId = errBody.request_id;
395
+ }
396
+ }
397
+ } catch {
398
+ rawBody = text;
399
+ }
400
+ }
401
+ } catch {
402
+ }
403
+ const retryAfter = parseRetryAfter(response.headers.get("Retry-After"));
404
+ const ErrorClass = classifyError(status, type);
405
+ return new ErrorClass({
406
+ type,
407
+ message,
408
+ statusCode: status,
409
+ requestId: resolvedRequestId,
410
+ rawBody,
411
+ details,
412
+ retryAfter
413
+ });
414
+ }
415
+ };
416
+ function defaultSleep(ms) {
417
+ if (ms <= 0) return Promise.resolve();
418
+ return new Promise((resolve) => setTimeout(resolve, ms));
419
+ }
420
+ function nodeVersion() {
421
+ return typeof process !== "undefined" && process.version ? process.version : "unknown";
422
+ }
423
+ function parseRetryAfter(value) {
424
+ if (value === null || value.length === 0) return void 0;
425
+ const asInt = Number(value);
426
+ if (Number.isFinite(asInt) && Number.isInteger(asInt) && asInt >= 0) {
427
+ return asInt;
428
+ }
429
+ const asDate = Date.parse(value);
430
+ if (!Number.isNaN(asDate)) {
431
+ const deltaMs = asDate - Date.now();
432
+ if (deltaMs > 0) return Math.ceil(deltaMs / 1e3);
433
+ }
434
+ return void 0;
435
+ }
436
+ function describeError(err) {
437
+ if (err instanceof Error) return err.message;
438
+ if (typeof err === "string") return err;
439
+ return String(err);
440
+ }
441
+ async function safeReadText(response) {
442
+ try {
443
+ return await response.text();
444
+ } catch {
445
+ return void 0;
446
+ }
447
+ }
448
+ function composeAbortSignals(a, b) {
449
+ const maybeAny = AbortSignal.any;
450
+ if (typeof maybeAny === "function") {
451
+ return maybeAny([a, b]);
452
+ }
453
+ const controller = new AbortController();
454
+ const onAbort = (source) => {
455
+ if (controller.signal.aborted) return;
456
+ const reason = source.reason;
457
+ controller.abort(reason);
458
+ };
459
+ if (a.aborted) onAbort(a);
460
+ else a.addEventListener("abort", () => onAbort(a), { once: true });
461
+ if (b.aborted) onAbort(b);
462
+ else b.addEventListener("abort", () => onAbort(b), { once: true });
463
+ return controller.signal;
464
+ }
465
+
466
+ // src/helpers/pagination.ts
467
+ function paginate(transport, path, options = {}) {
468
+ const itemsKey = options.itemsKey ?? "data";
469
+ const cursorParam = options.cursorParam ?? "cursor";
470
+ const apiVersion = options.apiVersion;
471
+ const baseParams = { ...options.params ?? {} };
472
+ async function* pageIterator() {
473
+ let cursor;
474
+ while (true) {
475
+ const query = { ...baseParams };
476
+ if (cursor !== void 0) query[cursorParam] = cursor;
477
+ const page = await transport.request("GET", path, {
478
+ query,
479
+ apiVersion
480
+ });
481
+ if (page === null || typeof page !== "object") return;
482
+ yield page;
483
+ const next = page.next_cursor ?? null;
484
+ if (!next) return;
485
+ if (next === cursor) return;
486
+ cursor = next;
487
+ }
488
+ }
489
+ async function* itemIterator() {
490
+ for await (const page of pageIterator()) {
491
+ const items = page[itemsKey];
492
+ if (!items) continue;
493
+ yield* items;
494
+ }
495
+ }
496
+ return {
497
+ [Symbol.asyncIterator]() {
498
+ return itemIterator();
499
+ },
500
+ pages() {
501
+ return pageIterator();
502
+ },
503
+ async toArray() {
504
+ const out = [];
505
+ for await (const item of itemIterator()) out.push(item);
506
+ return out;
507
+ }
508
+ };
509
+ }
510
+
511
+ // src/helpers/operations.ts
512
+ var OPERATIONS_PATH = "/api/async-operations";
513
+ var BACKOFF_CAP_MS = 1e4;
514
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["succeeded", "failed", "cancelled"]);
515
+ function toAsyncOperation(body) {
516
+ const b = body ?? {};
517
+ return {
518
+ operationId: b["operation_id"],
519
+ kind: b["kind"],
520
+ status: b["status"],
521
+ progress: b["progress"] ?? {},
522
+ result: b["result"],
523
+ resultUrl: b["result_url"],
524
+ resultExpiresAt: b["result_expires_at"],
525
+ errorCode: b["error_code"],
526
+ errorMessage: b["error_message"],
527
+ createdAt: b["created_at"],
528
+ startedAt: b["started_at"],
529
+ completedAt: b["completed_at"],
530
+ expiresAt: b["expires_at"],
531
+ links: b["links"] ?? {},
532
+ raw: b
533
+ };
534
+ }
535
+ function isTerminal(operation) {
536
+ return TERMINAL_STATUSES.has(operation.status);
537
+ }
538
+ var AsyncOperationError = class _AsyncOperationError extends SpreadSpaceError {
539
+ operation;
540
+ constructor(operation) {
541
+ super({
542
+ type: operation.errorCode ?? "operation_failed",
543
+ message: operation.errorMessage ?? "operation failed",
544
+ statusCode: 0
545
+ });
546
+ this.operation = operation;
547
+ Object.setPrototypeOf(this, _AsyncOperationError.prototype);
548
+ }
549
+ };
550
+ var AsyncOperationTimeout = class _AsyncOperationTimeout extends SpreadSpaceError {
551
+ operation;
552
+ constructor(message, operation) {
553
+ super({ type: "operation_timeout", message, statusCode: 0 });
554
+ this.operation = operation;
555
+ Object.setPrototypeOf(this, _AsyncOperationTimeout.prototype);
556
+ }
557
+ };
558
+ var AsyncOperationHandle = class {
559
+ constructor(transport, operation) {
560
+ this.transport = transport;
561
+ this.op = operation;
562
+ }
563
+ transport;
564
+ op;
565
+ get id() {
566
+ return this.op.operationId;
567
+ }
568
+ /** Last-known state — does not hit the network. */
569
+ get operation() {
570
+ return this.op;
571
+ }
572
+ /** GET the operation once and cache the result. */
573
+ async refresh(options) {
574
+ this.op = await getOperation(this.transport, this.id, options);
575
+ return this.op;
576
+ }
577
+ /** POST the cancel route and cache the returned state. */
578
+ async cancel(options) {
579
+ this.op = await cancelOperation(this.transport, this.id, options);
580
+ return this.op;
581
+ }
582
+ /**
583
+ * Poll until terminal. Resolves the `AsyncOperation` on `succeeded` /
584
+ * `cancelled`; rejects with `AsyncOperationError` on `failed`, or
585
+ * `AsyncOperationTimeout` if `timeoutMs` elapses while still non-terminal.
586
+ * `sleep` and `now` are injectable so tests never actually wait.
587
+ */
588
+ async wait(options = {}) {
589
+ const timeoutMs = options.timeoutMs === void 0 ? 3e5 : options.timeoutMs;
590
+ const sleep = options.sleep ?? defaultSleep2;
591
+ const now = options.now ?? Date.now;
592
+ const deadline = timeoutMs === null ? null : now() + timeoutMs;
593
+ let delay = options.pollIntervalMs ?? 2e3;
594
+ for (; ; ) {
595
+ const op = await this.refresh();
596
+ if (isTerminal(op)) {
597
+ if (op.status === "failed") {
598
+ throw new AsyncOperationError(op);
599
+ }
600
+ return op;
601
+ }
602
+ if (deadline !== null && now() >= deadline) {
603
+ throw new AsyncOperationTimeout(
604
+ `operation ${this.id} did not finish within ${timeoutMs}ms (last status ${JSON.stringify(op.status)})`,
605
+ op
606
+ );
607
+ }
608
+ await sleep(delay);
609
+ delay = Math.min(delay * 2, BACKOFF_CAP_MS);
610
+ }
611
+ }
612
+ };
613
+ async function createExtractionExport(transport, params = {}, options) {
614
+ const body = {};
615
+ if (params.borrowerId !== void 0) body["borrower_id"] = params.borrowerId;
616
+ if (params.loanId !== void 0) body["loan_id"] = params.loanId;
617
+ if (params.documentIds !== void 0) body["document_ids"] = params.documentIds;
618
+ if (params.format !== void 0) body["format"] = params.format;
619
+ if (params.deliveryMode !== void 0) body["delivery_mode"] = params.deliveryMode;
620
+ const resp = await transport.request(
621
+ "POST",
622
+ `${OPERATIONS_PATH}/extraction_export`,
623
+ { body, ...options }
624
+ );
625
+ return new AsyncOperationHandle(transport, toAsyncOperation(resp));
626
+ }
627
+ async function getOperation(transport, operationId, options) {
628
+ const resp = await transport.request(
629
+ "GET",
630
+ `${OPERATIONS_PATH}/${encodeURIComponent(operationId)}`,
631
+ options
632
+ );
633
+ return toAsyncOperation(resp);
634
+ }
635
+ async function cancelOperation(transport, operationId, options) {
636
+ const resp = await transport.request(
637
+ "POST",
638
+ `${OPERATIONS_PATH}/${encodeURIComponent(operationId)}/cancel`,
639
+ options
640
+ );
641
+ return toAsyncOperation(resp);
642
+ }
643
+ function defaultSleep2(ms) {
644
+ if (ms <= 0) return Promise.resolve();
645
+ return new Promise((resolve) => setTimeout(resolve, ms));
646
+ }
647
+
648
+ // src/helpers/upload.ts
649
+ import { createHash } from "crypto";
650
+ import { readFile } from "fs/promises";
651
+ import { basename } from "path";
652
+ var TERMINAL_JOB_STATUSES = /* @__PURE__ */ new Set(["COMPLETED", "FAILED"]);
653
+ var FAILURE_JOB_STATUSES = /* @__PURE__ */ new Set(["FAILED"]);
654
+ var DOCUMENTS_PATH = "/api/documents";
655
+ var DEFAULT_CONTENT_TYPE = "application/octet-stream";
656
+ function parsePresignedUrl(body) {
657
+ const pick = (camel, snake) => {
658
+ const value = body[camel];
659
+ return value === void 0 || value === null ? body[snake] : value;
660
+ };
661
+ return {
662
+ jobId: pick("jobId", "job_id"),
663
+ uploadUrl: pick("uploadUrl", "upload_url"),
664
+ s3Key: pick("s3Key", "s3_key"),
665
+ expiresInSeconds: pick("expiresInSeconds", "expires_in_seconds")
666
+ };
667
+ }
668
+ var UploadError = class _UploadError extends SpreadSpaceError {
669
+ statusBody;
670
+ constructor(message, statusBody) {
671
+ super({ type: "upload_failed", message, statusCode: 0 });
672
+ this.statusBody = statusBody;
673
+ Object.setPrototypeOf(this, _UploadError.prototype);
674
+ }
675
+ };
676
+ var UploadTimeout = class _UploadTimeout extends SpreadSpaceError {
677
+ statusBody;
678
+ constructor(message, statusBody) {
679
+ super({ type: "upload_timeout", message, statusCode: 0 });
680
+ this.statusBody = statusBody;
681
+ Object.setPrototypeOf(this, _UploadTimeout.prototype);
682
+ }
683
+ };
684
+ var JobHandle = class {
685
+ constructor(transport, id, status, raw) {
686
+ this.transport = transport;
687
+ this.id = id;
688
+ this.status_ = status;
689
+ this.raw = raw ?? {};
690
+ }
691
+ transport;
692
+ id;
693
+ status_;
694
+ raw;
695
+ /** GET the status route once; cache and return the decoded body. */
696
+ async status() {
697
+ const body = await this.transport.request(
698
+ "GET",
699
+ `${DOCUMENTS_PATH}/${encodeURIComponent(this.id)}/status`
700
+ ) ?? {};
701
+ this.raw = body;
702
+ if (typeof body.status === "string") this.status_ = body.status;
703
+ return body;
704
+ }
705
+ /**
706
+ * Poll status until terminal; return the final status body.
707
+ *
708
+ * Raises {@link UploadError} if the job ends in a failure status (`FAILED`),
709
+ * {@link UploadTimeout} if `timeoutMs` elapses while the job is still
710
+ * non-terminal. Clock and sleep are injectable so tests never actually wait.
711
+ */
712
+ async wait(options = {}) {
713
+ const {
714
+ timeoutMs = 6e5,
715
+ pollIntervalMs = 3e3,
716
+ terminalStatuses = TERMINAL_JOB_STATUSES,
717
+ sleep = defaultSleep3,
718
+ now = defaultNow
719
+ } = options;
720
+ const deadline = timeoutMs === null ? null : now() + timeoutMs;
721
+ for (; ; ) {
722
+ const body = await this.status();
723
+ const current = typeof body.status === "string" ? body.status : void 0;
724
+ if (current !== void 0 && terminalStatuses.has(current)) {
725
+ if (FAILURE_JOB_STATUSES.has(current)) {
726
+ throw new UploadError(`job ${this.id} ended in status ${current}`, body);
727
+ }
728
+ return body;
729
+ }
730
+ if (deadline !== null && now() >= deadline) {
731
+ throw new UploadTimeout(
732
+ `job ${this.id} did not finish within ${timeoutMs}ms (last status ${String(current)})`,
733
+ body
734
+ );
735
+ }
736
+ await sleep(pollIntervalMs);
737
+ }
738
+ }
739
+ };
740
+ async function uploadDocument(transport, file, options = {}) {
741
+ const {
742
+ fileName,
743
+ contentType,
744
+ fileSize,
745
+ borrowerId,
746
+ loanId,
747
+ contentHash,
748
+ computeHash = true,
749
+ sampleDocument,
750
+ wait = false,
751
+ waitOptions,
752
+ signal
753
+ } = options;
754
+ const payload = await resolvePayload(file, { fileName, contentType });
755
+ if (payload.fileName.length > 255) {
756
+ throw new Error("fileName must be <= 255 characters.");
757
+ }
758
+ const size = fileSize ?? payload.bytes.length;
759
+ let digest = contentHash;
760
+ if (digest === void 0 && computeHash) {
761
+ digest = createHash("sha256").update(payload.bytes).digest("hex");
762
+ }
763
+ const presignBody = { file_name: payload.fileName, content_type: payload.contentType };
764
+ if (size !== void 0) presignBody.file_size = size;
765
+ if (borrowerId !== void 0) presignBody.borrower_id = borrowerId;
766
+ if (loanId !== void 0) presignBody.loan_id = loanId;
767
+ if (digest !== void 0) presignBody.content_hash = digest;
768
+ const presignResponse = await transport.request(
769
+ "POST",
770
+ `${DOCUMENTS_PATH}/presigned-url`,
771
+ { body: presignBody, signal }
772
+ );
773
+ const presigned = parsePresignedUrl(presignResponse ?? {});
774
+ await transport.putPresigned(presigned.uploadUrl, payload.bytes, payload.contentType, { signal });
775
+ const confirmBody = sampleDocument !== void 0 ? { sample_document: sampleDocument } : {};
776
+ const confirmed = await transport.request(
777
+ "POST",
778
+ `${DOCUMENTS_PATH}/${encodeURIComponent(presigned.jobId)}/confirm-upload`,
779
+ { body: confirmBody, signal }
780
+ ) ?? {};
781
+ const handle = new JobHandle(
782
+ transport,
783
+ typeof confirmed.jobId === "string" ? confirmed.jobId : presigned.jobId,
784
+ typeof confirmed.status === "string" ? confirmed.status : void 0,
785
+ confirmed
786
+ );
787
+ if (wait) {
788
+ await handle.wait(waitOptions);
789
+ }
790
+ return handle;
791
+ }
792
+ function defaultSleep3(ms) {
793
+ if (ms <= 0) return Promise.resolve();
794
+ return new Promise((resolve) => setTimeout(resolve, ms));
795
+ }
796
+ function defaultNow() {
797
+ return Date.now();
798
+ }
799
+ async function resolvePayload(file, opts) {
800
+ if (typeof file === "string") {
801
+ const bytes = toOwnedBytes(await readFile(file));
802
+ const name = opts.fileName ?? basename(file);
803
+ const ctype = opts.contentType ?? guessContentType(name) ?? DEFAULT_CONTENT_TYPE;
804
+ return { bytes, fileName: name, contentType: ctype };
805
+ }
806
+ if (isBlob(file)) {
807
+ const bytes = toOwnedBytes(new Uint8Array(await file.arrayBuffer()));
808
+ const blobName = typeof file.name === "string" ? file.name : "";
809
+ const name = opts.fileName ?? blobName;
810
+ if (!name) {
811
+ throw new Error("fileName is required for a Blob without a name.");
812
+ }
813
+ const ctype = opts.contentType ?? (file.type || void 0) ?? guessContentType(name) ?? DEFAULT_CONTENT_TYPE;
814
+ return { bytes, fileName: name, contentType: ctype };
815
+ }
816
+ if (isBinaryData(file)) {
817
+ if (!opts.fileName || !opts.contentType) {
818
+ throw new Error("fileName and contentType are required when uploading raw bytes.");
819
+ }
820
+ return { bytes: toOwnedBytes(file), fileName: opts.fileName, contentType: opts.contentType };
821
+ }
822
+ if (isAsyncIterable(file) || isIterable(file)) {
823
+ if (!opts.fileName || !opts.contentType) {
824
+ throw new Error("fileName and contentType are required when uploading a stream.");
825
+ }
826
+ const bytes = await collectChunks(file);
827
+ return { bytes, fileName: opts.fileName, contentType: opts.contentType };
828
+ }
829
+ throw new TypeError(
830
+ "file must be a path string, Uint8Array/Buffer, Blob/File, or a Readable/iterable of chunks."
831
+ );
832
+ }
833
+ function isBlob(value) {
834
+ return typeof Blob !== "undefined" && value instanceof Blob && typeof value.arrayBuffer === "function";
835
+ }
836
+ function isBinaryData(value) {
837
+ return value instanceof Uint8Array || value instanceof ArrayBuffer || ArrayBuffer.isView(value);
838
+ }
839
+ function toOwnedBytes(value) {
840
+ const src = value instanceof Uint8Array ? value : value instanceof ArrayBuffer ? new Uint8Array(value) : new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
841
+ const out = new Uint8Array(new ArrayBuffer(src.byteLength));
842
+ out.set(src);
843
+ return out;
844
+ }
845
+ function isAsyncIterable(value) {
846
+ return typeof value === "object" && value !== null && typeof value[Symbol.asyncIterator] === "function";
847
+ }
848
+ function isIterable(value) {
849
+ return typeof value === "object" && value !== null && typeof value[Symbol.iterator] === "function";
850
+ }
851
+ async function collectChunks(source) {
852
+ const parts = [];
853
+ let total = 0;
854
+ for await (const chunk of source) {
855
+ const part = typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk;
856
+ parts.push(part);
857
+ total += part.byteLength;
858
+ }
859
+ const out = new Uint8Array(new ArrayBuffer(total));
860
+ let offset = 0;
861
+ for (const part of parts) {
862
+ out.set(part, offset);
863
+ offset += part.byteLength;
864
+ }
865
+ return out;
866
+ }
867
+ var CONTENT_TYPE_BY_EXT = {
868
+ pdf: "application/pdf",
869
+ png: "image/png",
870
+ jpg: "image/jpeg",
871
+ jpeg: "image/jpeg",
872
+ tif: "image/tiff",
873
+ tiff: "image/tiff",
874
+ csv: "text/csv",
875
+ txt: "text/plain",
876
+ xls: "application/vnd.ms-excel",
877
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
878
+ doc: "application/msword",
879
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
880
+ };
881
+ function guessContentType(name) {
882
+ const dot = name.lastIndexOf(".");
883
+ if (dot < 0 || dot === name.length - 1) return void 0;
884
+ return CONTENT_TYPE_BY_EXT[name.slice(dot + 1).toLowerCase()];
885
+ }
886
+
887
+ // src/resources/webhooks.ts
888
+ var WebhooksResource = class {
889
+ constructor(transport) {
890
+ this.transport = transport;
891
+ }
892
+ transport;
893
+ /**
894
+ * Verify a `SpreadSpace-Signature` header against a body and signing secret.
895
+ * Throws `WebhookSignatureError` on any failure.
896
+ */
897
+ static verifySignature = verifyWebhook;
898
+ /**
899
+ * Verify a webhook signature and JSON-parse the body into a typed
900
+ * `WebhookEvent`. Throws `WebhookSignatureError` on signature failure or
901
+ * invalid JSON.
902
+ */
903
+ static verifyAndParse = verifyAndParseWebhook;
904
+ /** Low-level request shim onto the shared transport. */
905
+ request(method, path, options) {
906
+ return this.transport.request(method, path, options ?? {});
907
+ }
908
+ /**
909
+ * List configured webhook endpoints for this organization.
910
+ *
911
+ * GET /api/admin/webhooks
912
+ */
913
+ list(params = {}) {
914
+ return paginate(this.transport, "/api/admin/webhooks", {
915
+ params: { limit: params.limit },
916
+ apiVersion: params.apiVersion
917
+ });
918
+ }
919
+ /**
920
+ * Retrieve a single webhook endpoint.
921
+ *
922
+ * GET /api/admin/webhooks/{id}
923
+ */
924
+ retrieve(endpointId, options) {
925
+ return this.request(
926
+ "GET",
927
+ `/api/admin/webhooks/${encodeURIComponent(endpointId)}`,
928
+ options
929
+ );
930
+ }
931
+ /**
932
+ * Create a new webhook endpoint. The response carries the plaintext signing
933
+ * secret exactly once — store it server-side, do not log it.
934
+ *
935
+ * POST /api/admin/webhooks
936
+ */
937
+ create(params, options) {
938
+ return this.request("POST", "/api/admin/webhooks", { body: params, ...options });
939
+ }
940
+ /**
941
+ * Update endpoint metadata (URL, subscribed event types, enabled flag).
942
+ *
943
+ * PATCH /api/admin/webhooks/{id}
944
+ */
945
+ update(endpointId, params, options) {
946
+ return this.request(
947
+ "PATCH",
948
+ `/api/admin/webhooks/${encodeURIComponent(endpointId)}`,
949
+ { body: params, ...options }
950
+ );
951
+ }
952
+ /**
953
+ * Delete a webhook endpoint.
954
+ *
955
+ * DELETE /api/admin/webhooks/{id}
956
+ */
957
+ delete(endpointId, options) {
958
+ return this.request(
959
+ "DELETE",
960
+ `/api/admin/webhooks/${encodeURIComponent(endpointId)}`,
961
+ options
962
+ );
963
+ }
964
+ /**
965
+ * Rotate an endpoint's signing secret. The old secret stays valid for a grace
966
+ * window so in-flight deliveries aren't dropped. Returns the new plaintext
967
+ * secret exactly once.
968
+ *
969
+ * POST /api/admin/webhooks/{id}/rotate
970
+ */
971
+ rotateSecret(endpointId, params = {}, options) {
972
+ return this.request(
973
+ "POST",
974
+ `/api/admin/webhooks/${encodeURIComponent(endpointId)}/rotate`,
975
+ { body: params, ...options }
976
+ );
977
+ }
978
+ /**
979
+ * Iterate delivery attempts for an endpoint.
980
+ *
981
+ * GET /api/admin/webhooks/{id}/deliveries
982
+ */
983
+ deliveries(endpointId, params = {}) {
984
+ return paginate(
985
+ this.transport,
986
+ `/api/admin/webhooks/${encodeURIComponent(endpointId)}/deliveries`,
987
+ { params: { limit: params.limit }, apiVersion: params.apiVersion }
988
+ );
989
+ }
990
+ /**
991
+ * Manually replay a delivery (e.g. after fixing a downstream outage).
992
+ *
993
+ * POST /api/admin/webhooks/{id}/deliveries/{deliveryId}/replay
994
+ */
995
+ replayDelivery(endpointId, deliveryId, params = {}, options) {
996
+ return this.request(
997
+ "POST",
998
+ `/api/admin/webhooks/${encodeURIComponent(endpointId)}/deliveries/${encodeURIComponent(deliveryId)}/replay`,
999
+ { body: params, ...options }
1000
+ );
1001
+ }
1002
+ };
1003
+
1004
+ // src/resources/embed.ts
1005
+ var EmbedSessionsResource = class {
1006
+ constructor(transport) {
1007
+ this.transport = transport;
1008
+ }
1009
+ transport;
1010
+ request(method, path, options) {
1011
+ return this.transport.request(method, path, options ?? {});
1012
+ }
1013
+ /**
1014
+ * Mint an embed-session token for a loan.
1015
+ *
1016
+ * POST /api/embed/sessions
1017
+ *
1018
+ * Server skips idempotency (`[SkipIdempotency]`); the SDK still sends the
1019
+ * auto-generated `Idempotency-Key` header (harmless — the server ignores it).
1020
+ * Returns the `ss_embed_*` token plus session metadata (shown once).
1021
+ */
1022
+ create(params, options) {
1023
+ return this.request("POST", "/api/embed/sessions", { body: params, ...options });
1024
+ }
1025
+ /**
1026
+ * Revoke an embed session early — useful when the integrator's UI flow ends
1027
+ * before the natural expiry, or when reissuing after a logout. Honors
1028
+ * `Idempotency-Key`. Resolves to `void` on the `204 No Content`.
1029
+ *
1030
+ * DELETE /api/embed/sessions/{sessionId}
1031
+ */
1032
+ revoke(sessionId, options) {
1033
+ return this.request(
1034
+ "DELETE",
1035
+ `/api/embed/sessions/${encodeURIComponent(sessionId)}`,
1036
+ options
1037
+ );
1038
+ }
1039
+ };
1040
+ var EmbedIframeUrlsResource = class {
1041
+ constructor(transport) {
1042
+ this.transport = transport;
1043
+ }
1044
+ transport;
1045
+ request(method, path, options) {
1046
+ return this.transport.request(method, path, options ?? {});
1047
+ }
1048
+ /**
1049
+ * Mint a signed-URL handle to drop into an `<iframe src>`. The browser then
1050
+ * exchanges the handle (single-use) for an embed token at the iframe origin.
1051
+ *
1052
+ * POST /api/embed/iframe-urls
1053
+ */
1054
+ create(params, options) {
1055
+ return this.request("POST", "/api/embed/iframe-urls", { body: params, ...options });
1056
+ }
1057
+ };
1058
+ var EmbedResource = class {
1059
+ /** `embed.sessions` — mint/revoke embed-session tokens. */
1060
+ sessions;
1061
+ /** `embed.iframeUrls` — mint signed iframe-URL handles. */
1062
+ iframeUrls;
1063
+ constructor(transport) {
1064
+ this.sessions = new EmbedSessionsResource(transport);
1065
+ this.iframeUrls = new EmbedIframeUrlsResource(transport);
1066
+ }
1067
+ };
1068
+
1069
+ // src/client.ts
1070
+ function envApiKey() {
1071
+ if (typeof process === "undefined" || !process.env) return void 0;
1072
+ const value = process.env.SPREADSPACE_API_KEY;
1073
+ return value && value.length > 0 ? value : void 0;
1074
+ }
1075
+ var BorrowersResource = class {
1076
+ constructor(transport) {
1077
+ this.transport = transport;
1078
+ }
1079
+ transport;
1080
+ /** Paginate `GET /api/borrowers`. Lazy async-iterable of borrower records. */
1081
+ list(params = {}) {
1082
+ return paginate(this.transport, "/api/borrowers", {
1083
+ params: { limit: params.limit, intake: params.intake },
1084
+ apiVersion: params.apiVersion
1085
+ });
1086
+ }
1087
+ };
1088
+ var LoansResource = class {
1089
+ constructor(transport) {
1090
+ this.transport = transport;
1091
+ }
1092
+ transport;
1093
+ /**
1094
+ * Paginate loans. With `borrowerId`, hits `GET /api/borrowers/{id}/loans`;
1095
+ * otherwise the org-wide `GET /api/loans`.
1096
+ */
1097
+ list(params = {}) {
1098
+ const path = params.borrowerId ? `/api/borrowers/${encodeURIComponent(params.borrowerId)}/loans` : "/api/loans";
1099
+ return paginate(this.transport, path, {
1100
+ params: { limit: params.limit },
1101
+ apiVersion: params.apiVersion
1102
+ });
1103
+ }
1104
+ };
1105
+ var JobsResource = class {
1106
+ constructor(transport) {
1107
+ this.transport = transport;
1108
+ }
1109
+ transport;
1110
+ /** Paginate `GET /api/jobs`. */
1111
+ list(params = {}) {
1112
+ return paginate(this.transport, "/api/jobs", {
1113
+ params: { limit: params.limit },
1114
+ apiVersion: params.apiVersion
1115
+ });
1116
+ }
1117
+ };
1118
+ var AsyncOperationsResource = class {
1119
+ constructor(transport) {
1120
+ this.transport = transport;
1121
+ }
1122
+ transport;
1123
+ /**
1124
+ * Paginate `GET /api/async-operations`. Divergent envelope: items live under
1125
+ * `operations` (no `data`/`limit`), so the items key is overridden here.
1126
+ */
1127
+ list(params = {}) {
1128
+ return paginate(this.transport, "/api/async-operations", {
1129
+ params: { limit: params.limit, kind: params.kind, status: params.status },
1130
+ itemsKey: "operations",
1131
+ apiVersion: params.apiVersion
1132
+ });
1133
+ }
1134
+ /** `GET /api/async-operations/{id}` — fetch one operation's current state. */
1135
+ get(operationId, options) {
1136
+ return getOperation(this.transport, operationId, options);
1137
+ }
1138
+ /**
1139
+ * `POST /api/async-operations/{id}/cancel`. Cancelling a terminal
1140
+ * (non-cancelled) op -> 409 (`ConflictError`); cancelling an already-cancelled
1141
+ * op is idempotent (200).
1142
+ */
1143
+ cancel(operationId, options) {
1144
+ return cancelOperation(this.transport, operationId, options);
1145
+ }
1146
+ };
1147
+ var ExportsResource = class {
1148
+ constructor(transport) {
1149
+ this.transport = transport;
1150
+ }
1151
+ transport;
1152
+ /**
1153
+ * `POST /api/async-operations/extraction_export`. All fields optional; `null`/
1154
+ * `undefined` are omitted from the body. Returns a handle whose `.wait()` polls
1155
+ * the operation to a terminal state.
1156
+ */
1157
+ create(params = {}, options) {
1158
+ return createExtractionExport(this.transport, params, options);
1159
+ }
1160
+ /** `GET /api/async-operations/{id}` — fetch the export operation's state. */
1161
+ get(operationId, options) {
1162
+ return getOperation(this.transport, operationId, options);
1163
+ }
1164
+ };
1165
+ var DocumentsResource = class {
1166
+ constructor(transport) {
1167
+ this.transport = transport;
1168
+ }
1169
+ transport;
1170
+ /**
1171
+ * Upload a document: presigned-url -> S3 PUT -> confirm-upload. `file` may be a
1172
+ * path string, raw bytes, a `Blob`/`File`, or a `Readable`/iterable of chunks.
1173
+ * With `wait: true`, polls to a terminal job status before resolving.
1174
+ */
1175
+ upload(file, options) {
1176
+ return uploadDocument(this.transport, file, options);
1177
+ }
1178
+ /** `GET /api/documents/{jobId}/status` — fetch one job's current status body. */
1179
+ status(jobId) {
1180
+ return new JobHandle(this.transport, jobId).status();
1181
+ }
1182
+ };
1183
+ var SpreadSpace = class {
1184
+ /** Borrowers: `list()`. */
1185
+ borrowers;
1186
+ /** Loans: `list({ borrowerId?, limit? })`. */
1187
+ loans;
1188
+ /** Jobs: `list()`. */
1189
+ jobs;
1190
+ /** Async operations: `list({ kind?, status? })` / `get(id)` / `cancel(id)`. */
1191
+ asyncOperations;
1192
+ /** Extraction exports: `create(...)` / `get(id)`. */
1193
+ exports;
1194
+ /** Documents: `upload(file, opts)` / `status(jobId)`. */
1195
+ documents;
1196
+ /** Webhooks: `create/list/retrieve/update/delete` endpoints + static `verifySignature`/`verifyAndParse`. */
1197
+ webhooks;
1198
+ /** Embed: `sessions.create(...)` mints a loan-scoped `ss_embed_` token. */
1199
+ embed;
1200
+ _transport;
1201
+ constructor(options = {}) {
1202
+ if (options.transport) {
1203
+ this._transport = options.transport;
1204
+ } else {
1205
+ const apiKey = options.apiKey ?? envApiKey();
1206
+ if (!apiKey) {
1207
+ throw new TypeError(
1208
+ "SpreadSpace requires an apiKey. Pass { apiKey } or set SPREADSPACE_API_KEY. Issue a key from the dashboard at /settings/api-keys."
1209
+ );
1210
+ }
1211
+ this._transport = new Transport({
1212
+ apiKey,
1213
+ baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
1214
+ apiVersion: options.apiVersion ?? DEFAULT_API_VERSION,
1215
+ timeout: options.timeoutMs,
1216
+ maxRetries: options.maxRetries,
1217
+ fetch: options.fetch
1218
+ });
1219
+ }
1220
+ this.borrowers = new BorrowersResource(this._transport);
1221
+ this.loans = new LoansResource(this._transport);
1222
+ this.jobs = new JobsResource(this._transport);
1223
+ this.asyncOperations = new AsyncOperationsResource(this._transport);
1224
+ this.exports = new ExportsResource(this._transport);
1225
+ this.documents = new DocumentsResource(this._transport);
1226
+ this.webhooks = new WebhooksResource(this._transport);
1227
+ this.embed = new EmbedResource(this._transport);
1228
+ }
1229
+ /** The underlying configured transport. */
1230
+ get transport() {
1231
+ return this._transport;
1232
+ }
1233
+ /**
1234
+ * Low-level escape hatch to any endpoint the resource helpers don't wrap.
1235
+ * Returns the decoded JSON body (or `undefined` for an empty / 204 response).
1236
+ */
1237
+ request(method, path, options) {
1238
+ return this._transport.request(method, path, options);
1239
+ }
1240
+ };
1241
+ export {
1242
+ AsyncOperationError,
1243
+ AsyncOperationHandle,
1244
+ AsyncOperationTimeout,
1245
+ AuthenticationError,
1246
+ ConflictError,
1247
+ DEFAULT_API_VERSION,
1248
+ DEFAULT_BASE_URL,
1249
+ Decimal,
1250
+ EmbedIframeUrlsResource,
1251
+ EmbedResource,
1252
+ EmbedSessionsResource,
1253
+ InvalidRequestError,
1254
+ JobHandle,
1255
+ NetworkError,
1256
+ NotFoundError,
1257
+ PAYLOAD_MONEY_KEYS,
1258
+ PermissionError,
1259
+ RateLimitError,
1260
+ SCALAR_MONEY_KEYS,
1261
+ SDK_VERSION,
1262
+ ServerError,
1263
+ SpreadSpace,
1264
+ SpreadSpaceError,
1265
+ TERMINAL_JOB_STATUSES,
1266
+ TERMINAL_STATUSES,
1267
+ Transport,
1268
+ UploadError,
1269
+ UploadTimeout,
1270
+ WebhookSignatureError,
1271
+ WebhooksResource,
1272
+ cancelOperation,
1273
+ classifyError,
1274
+ createExtractionExport,
1275
+ getOperation,
1276
+ isTerminal,
1277
+ paginate,
1278
+ parseBodyWithExactMoney,
1279
+ parsePresignedUrl,
1280
+ uploadDocument,
1281
+ verifyAndParseWebhook,
1282
+ verifyWebhook
1283
+ };
1284
+ //# sourceMappingURL=index.js.map