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