@docmana/sdk 0.24.0 → 0.26.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/CHANGELOG.md CHANGED
@@ -1,13 +1,24 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
4
-
5
- ## 0.24.0 - 2026-08-20
6
-
7
- - Preserve the exact numeric flow version when mapping raw execution results.
8
- - Expose each processing step's LLM model and immutable prompt identifier without
9
- reintroducing token or cost telemetry.
10
- - Update vulnerable HTTP and build-tool transitive dependencies to patched versions.
3
+ ## Unreleased
4
+
5
+ ## 0.26.0 - 2026-09-07
6
+
7
+ - Expose the exact numeric version that a new flow execution would use through
8
+ `getFlowMetadata`.
9
+
10
+ ## 0.25.0 - 2026-08-24
11
+
12
+ - Stream file-path inputs from disk instead of loading them completely into memory.
13
+ - Expose typed `413` and `503` upload errors, preserve `Retry-After`, and retry one
14
+ multipart admission `503` carrying that header with a fresh request body.
15
+
16
+ ## 0.24.0 - 2026-08-20
17
+
18
+ - Preserve the exact numeric flow version when mapping raw execution results.
19
+ - Expose each processing step's LLM model and immutable prompt identifier without
20
+ reintroducing token or cost telemetry.
21
+ - Update vulnerable HTTP and build-tool transitive dependencies to patched versions.
11
22
 
12
23
  ## 0.23.0 - 2026-08-07
13
24
 
package/README.md CHANGED
@@ -117,7 +117,9 @@ use `RunFlowParams` for request data:
117
117
  - `listOrganizations()` calls `GET /v1/organizations` and returns
118
118
  `{ id, name }[]` for the organizations available to the Bearer token.
119
119
  - `getFlowMetadata(flowId)` calls `GET /v1/flows/:flowId/metadata` and returns
120
- `{ flowId, name?, jsonContextSchema, languages, minScore?, documents }`.
120
+ `{ flowId, name?, version?, jsonContextSchema, languages, minScore?, documents }`.
121
+ `version` is the exact published or draft flow version that a new execution
122
+ would use.
121
123
  - `getFlowDefinition(flowId)` calls `GET /v1/flows/:flowId/definition` and
122
124
  returns the normalized flow definition `{ flowId, name?, languages, minScore?, nodes }`.
123
125
  - `listOrganizationDocuments({ prefix? })` calls
@@ -160,6 +162,12 @@ the SDK waits that many milliseconds before the next status call. If the header
160
162
  is absent, the SDK falls back to capped exponential full jitter between
161
163
  `pollIntervalMs` and `pollMaxIntervalMs`. `Retry-After` on `429` always wins.
162
164
 
165
+ Path inputs are streamed from disk instead of being loaded completely into memory.
166
+ For multipart submissions only, the SDK retries one `503 Service Unavailable`
167
+ response carrying `Retry-After` with a fresh request body, respecting that delay
168
+ up to 30 seconds. A downstream `503` without `Retry-After` is not retried.
169
+ Payloads rejected with `413 Payload Too Large` are never retried.
170
+
163
171
  The SDK does not log or return `documentUrl` or uploaded file contents. Signed
164
172
  query parameters are sent only in the JSON request body to Consumer Edge. Fraud
165
173
  jobs are not organization-scoped; the legacy `organization` inputs are accepted
@@ -202,7 +210,7 @@ console.log(organizations.map((org) => `${org.id}: ${org.name}`).join("\n"));
202
210
 
203
211
  ```ts
204
212
  const metadata = await docmana.getFlowMetadata("<flow-id>");
205
- console.log(metadata.jsonContextSchema);
213
+ console.log(metadata.version, metadata.jsonContextSchema);
206
214
 
207
215
  const definition = await docmana.getFlowDefinition("<flow-id>");
208
216
  console.log(definition.nodes.map((node) => node.label).join("\n"));
@@ -322,16 +330,18 @@ from the Bearer token.
322
330
  Every SDK error extends `DocmanaError`, which carries `.status`, `.code`, and an
323
331
  optional `.requestId` when available.
324
332
 
325
- | Error | Thrown when |
326
- | ---------------------------- | ------------------------------------------------------------------------------------------- |
327
- | `DocmanaRequestError` | `400` bad request. |
328
- | `DocmanaAuthError` | `401` rejected or expired Bearer token. |
329
- | `DocmanaPermissionError` | `403` missing permission. |
330
- | `DocmanaNotFoundError` | `404` flow or execution not found. |
331
- | `DocmanaExecutionError` | The flow finished with status `"FAILED"`. Carries `.errors` and `.result`. |
332
- | `DocmanaFraudDetectionError` | Fraud detection finished with status `"FAILED"`. Carries `.jobId`, `.error`, and `.result`. |
333
- | `DocmanaTimeoutError` | Polling exceeded `timeoutMs`. |
334
- | `DocmanaAbortError` | The provided `AbortSignal` fired. |
333
+ | Error | Thrown when |
334
+ | -------------------------------- | ----------------------------------------------------------------------------------------------- |
335
+ | `DocmanaRequestError` | `400` bad request. |
336
+ | `DocmanaPayloadTooLargeError` | `413` multipart payload exceeds a Consumer limit. |
337
+ | `DocmanaAuthError` | `401` rejected or expired Bearer token. |
338
+ | `DocmanaPermissionError` | `403` missing permission. |
339
+ | `DocmanaNotFoundError` | `404` flow or execution not found. |
340
+ | `DocmanaExecutionError` | The flow finished with status `"FAILED"`. Carries `.errors` and `.result`. |
341
+ | `DocmanaFraudDetectionError` | Fraud detection finished with status `"FAILED"`. Carries `.jobId`, `.error`, and `.result`. |
342
+ | `DocmanaTimeoutError` | Polling exceeded `timeoutMs`. |
343
+ | `DocmanaAbortError` | The provided `AbortSignal` fired. |
344
+ | `DocmanaServiceUnavailableError` | `503` Consumer upload capacity is unavailable after the bounded retry. Carries `.retryAfterMs`. |
335
345
 
336
346
  ## Local development with yalc
337
347
 
@@ -0,0 +1,2 @@
1
+ import type { HttpClient } from "../http/http-client.js";
2
+ export declare function requestMultipartJson<T>(http: HttpClient, path: string, createForm: () => FormData, signal?: AbortSignal): Promise<T>;
package/dist/cli.mjs CHANGED
@@ -70,6 +70,11 @@ var DocmanaRequestError = class extends DocmanaError {
70
70
  super(message, { ...opts, code: "bad_request" });
71
71
  }
72
72
  };
73
+ var DocmanaPayloadTooLargeError = class extends DocmanaError {
74
+ constructor(message, opts = {}) {
75
+ super(message, { ...opts, code: "payload_too_large" });
76
+ }
77
+ };
73
78
  var DocmanaExecutionError = class extends DocmanaError {
74
79
  errors;
75
80
  result;
@@ -107,6 +112,13 @@ var DocmanaRateLimitError = class extends DocmanaError {
107
112
  this.retryAfterMs = opts.retryAfterMs;
108
113
  }
109
114
  };
115
+ var DocmanaServiceUnavailableError = class extends DocmanaError {
116
+ retryAfterMs;
117
+ constructor(message, opts = {}) {
118
+ super(message, { ...opts, code: "service_unavailable" });
119
+ this.retryAfterMs = opts.retryAfterMs;
120
+ }
121
+ };
110
122
  function errorFromResponse(status, message, requestId, retryAfterMs) {
111
123
  const opts = { status, requestId };
112
124
  switch (status) {
@@ -118,8 +130,12 @@ function errorFromResponse(status, message, requestId, retryAfterMs) {
118
130
  return new DocmanaPermissionError(message, opts);
119
131
  case 404:
120
132
  return new DocmanaNotFoundError(message, opts);
133
+ case 413:
134
+ return new DocmanaPayloadTooLargeError(message, opts);
121
135
  case 429:
122
136
  return new DocmanaRateLimitError(message, { ...opts, retryAfterMs });
137
+ case 503:
138
+ return new DocmanaServiceUnavailableError(message, { ...opts, retryAfterMs });
123
139
  default:
124
140
  return new DocmanaError(message, opts);
125
141
  }
@@ -215,7 +231,7 @@ function parseRetryAfterMs(value) {
215
231
  }
216
232
 
217
233
  // src/sdk/files/resolve-input.ts
218
- import { readFile } from "fs/promises";
234
+ import { openAsBlob } from "fs";
219
235
  import { basename } from "path";
220
236
  var CONTENT_TYPES = {
221
237
  ".pdf": "application/pdf",
@@ -255,16 +271,18 @@ function toBlobBytes(bytes) {
255
271
  async function resolveOne(input) {
256
272
  if (typeof input === "string" || isPathInput(input)) {
257
273
  const path = typeof input === "string" ? input : input.path;
258
- const data = await readFile(path);
259
274
  const filename = basename(path);
260
- return { data: new Blob([toBlobBytes(data)]), filename, contentType: contentTypeFor(filename) };
275
+ const contentType = contentTypeFor(filename);
276
+ const data = await openAsBlob(path, { type: contentType });
277
+ return { data, filename, contentType };
261
278
  }
262
279
  if (isNamedFileInput(input)) {
263
280
  const bytes = input.data instanceof Uint8Array ? input.data : await streamToBuffer(input.data);
281
+ const contentType = input.contentType ?? contentTypeFor(input.filename);
264
282
  return {
265
- data: new Blob([toBlobBytes(bytes)]),
283
+ data: new Blob([toBlobBytes(bytes)], { type: contentType }),
266
284
  filename: input.filename,
267
- contentType: input.contentType ?? contentTypeFor(input.filename)
285
+ contentType
268
286
  };
269
287
  }
270
288
  throw new Error("Unsupported file input type");
@@ -275,55 +293,98 @@ async function resolveInputs(input) {
275
293
  return Promise.all(list.map((item) => resolveOne(item)));
276
294
  }
277
295
 
296
+ // src/sdk/api/multipart-request.ts
297
+ var MAX_ATTEMPTS = 2;
298
+ var MAX_RETRY_DELAY_MS = 3e4;
299
+ async function requestMultipartJson(http, path, createForm, signal) {
300
+ for (let attempt = 1; ; attempt += 1) {
301
+ try {
302
+ return await http.requestJson("POST", path, {
303
+ body: createForm(),
304
+ signal
305
+ });
306
+ } catch (error) {
307
+ if (!(error instanceof DocmanaServiceUnavailableError) || error.retryAfterMs === void 0 || attempt >= MAX_ATTEMPTS) {
308
+ throw error;
309
+ }
310
+ await waitForRetry(Math.min(error.retryAfterMs, MAX_RETRY_DELAY_MS), signal);
311
+ }
312
+ }
313
+ }
314
+ async function waitForRetry(delayMs, signal) {
315
+ if (signal?.aborted) throw new DocmanaAbortError("Upload retry aborted");
316
+ await new Promise((resolve3, reject) => {
317
+ let settled = false;
318
+ const finish = (error) => {
319
+ if (settled) return;
320
+ settled = true;
321
+ clearTimeout(timer);
322
+ signal?.removeEventListener("abort", onAbort);
323
+ if (error) reject(error);
324
+ else resolve3();
325
+ };
326
+ const onAbort = () => finish(new DocmanaAbortError("Upload retry aborted"));
327
+ const timer = setTimeout(() => finish(), delayMs);
328
+ signal?.addEventListener("abort", onAbort, { once: true });
329
+ });
330
+ }
331
+
278
332
  // src/sdk/api/run-flow.ts
279
333
  async function runFlow(http, flowId, parts, signal, useDraftVersion = false, context, callbackUrl, language, organization) {
280
- const form = new FormData();
281
- for (const part of parts) {
282
- form.append("files", new File([part.data], part.filename, { type: part.contentType }));
283
- }
284
- if (context !== void 0) form.append("jsonContext", JSON.stringify(context));
285
- if (callbackUrl !== void 0) form.append("callbackUrl", callbackUrl);
286
- if (language !== void 0) form.append("language", language);
287
- if (useDraftVersion) form.append("useDraftVersion", "true");
288
334
  const path = `/flows/${flowId}/execute-async`;
289
- return http.requestJson(
290
- "POST",
335
+ return requestMultipartJson(
336
+ http,
291
337
  organization ? `${path}?organization=${encodeURIComponent(organization)}` : path,
292
- { body: form, signal }
338
+ () => {
339
+ const form = new FormData();
340
+ for (const part of parts) form.append("files", part.data, part.filename);
341
+ if (context !== void 0) form.append("jsonContext", JSON.stringify(context));
342
+ if (callbackUrl !== void 0) form.append("callbackUrl", callbackUrl);
343
+ if (language !== void 0) form.append("language", language);
344
+ if (useDraftVersion) form.append("useDraftVersion", "true");
345
+ return form;
346
+ },
347
+ signal
293
348
  );
294
349
  }
295
350
 
296
351
  // src/sdk/api/run-partial-flow.ts
297
352
  function buildRequest(flowId, parts, params, asyncMode) {
298
- const form = new FormData();
299
- for (const part of parts) {
300
- form.append("files", new File([part.data], part.filename, { type: part.contentType }));
301
- }
302
- if (params.context !== void 0) form.append("jsonContext", JSON.stringify(params.context));
303
- form.append("executionScope", params.executionScope);
304
- if (params.flowVersion !== void 0) form.append("flowVersion", params.flowVersion);
305
- for (const name of params.classificationNodeNames ?? []) {
306
- form.append("classificationNodeNames", name);
307
- }
308
353
  const suffix = asyncMode ? "partial-execute-async" : "partial-execute";
309
354
  const path = `/flows/${flowId}/${suffix}`;
310
355
  return {
311
356
  path: params.organization ? `${path}?organization=${encodeURIComponent(params.organization)}` : path,
312
- form
357
+ createForm: () => {
358
+ const form = new FormData();
359
+ for (const part of parts) form.append("files", part.data, part.filename);
360
+ if (params.context !== void 0) form.append("jsonContext", JSON.stringify(params.context));
361
+ form.append("executionScope", params.executionScope);
362
+ if (params.flowVersion !== void 0) form.append("flowVersion", params.flowVersion);
363
+ for (const name of params.classificationNodeNames ?? []) {
364
+ form.append("classificationNodeNames", name);
365
+ }
366
+ if (asyncMode && "callbackUrl" in params && params.callbackUrl !== void 0) {
367
+ form.append("callbackUrl", params.callbackUrl);
368
+ }
369
+ if (asyncMode && "language" in params && params.language !== void 0) {
370
+ form.append("language", params.language);
371
+ }
372
+ return form;
373
+ }
313
374
  };
314
375
  }
315
376
  async function runPartialFlow(http, flowId, parts, params, signal) {
316
- const { path, form } = buildRequest(flowId, parts, params, false);
317
- return http.requestJson("POST", path, { body: form, signal });
377
+ const { path, createForm } = buildRequest(flowId, parts, params, false);
378
+ return requestMultipartJson(http, path, createForm, signal);
318
379
  }
319
380
  async function runPartialFlowAsync(http, flowId, parts, params, signal) {
320
- const { path, form } = buildRequest(flowId, parts, params, true);
321
- if (params.callbackUrl !== void 0) form.append("callbackUrl", params.callbackUrl);
322
- if (params.language !== void 0) form.append("language", params.language);
323
- return http.requestJson("POST", path, {
324
- body: form,
381
+ const { path, createForm } = buildRequest(flowId, parts, params, true);
382
+ return requestMultipartJson(
383
+ http,
384
+ path,
385
+ createForm,
325
386
  signal
326
- });
387
+ );
327
388
  }
328
389
 
329
390
  // src/sdk/status.ts
@@ -381,15 +442,19 @@ async function getResult(http, executionResultId, signal, language, organization
381
442
  async function startFraudDetection(http, params, signal) {
382
443
  if (params.file !== void 0) {
383
444
  const [part] = await resolveInputs({ files: [params.file] });
384
- const form = new FormData();
385
- form.append("file", new File([part.data], part.filename, { type: part.contentType }));
386
- if (params.analystContext !== void 0) {
387
- form.append("analystContext", params.analystContext);
388
- }
389
- return http.requestJson("POST", "/fraud-detections", {
390
- body: form,
445
+ return requestMultipartJson(
446
+ http,
447
+ "/fraud-detections",
448
+ () => {
449
+ const form = new FormData();
450
+ form.append("file", part.data, part.filename);
451
+ if (params.analystContext !== void 0) {
452
+ form.append("analystContext", params.analystContext);
453
+ }
454
+ return form;
455
+ },
391
456
  signal
392
- });
457
+ );
393
458
  }
394
459
  return http.requestJson("POST", "/fraud-detections", {
395
460
  headers: { "content-type": "application/json" },
@@ -992,7 +1057,7 @@ var HELP = {
992
1057
 
993
1058
  // src/cli/config-store.ts
994
1059
  import { existsSync } from "fs";
995
- import { mkdir, readFile as readFile2, rm, writeFile } from "fs/promises";
1060
+ import { mkdir, readFile, rm, writeFile } from "fs/promises";
996
1061
  import { dirname, resolve } from "path";
997
1062
  import { createInterface } from "readline/promises";
998
1063
 
@@ -1023,7 +1088,7 @@ async function loadCliConfig(configPathOption, cwd, requireExisting) {
1023
1088
  }
1024
1089
  let raw;
1025
1090
  try {
1026
- raw = await readFile2(configPath, "utf8");
1091
+ raw = await readFile(configPath, "utf8");
1027
1092
  } catch {
1028
1093
  throw new CliUsageError(`Unable to read config file: ${configPath}`);
1029
1094
  }
@@ -1057,7 +1122,7 @@ function createFileTokenCache(path) {
1057
1122
  async read() {
1058
1123
  if (!existsSync(path)) return null;
1059
1124
  try {
1060
- const parsed = JSON.parse(await readFile2(path, "utf8"));
1125
+ const parsed = JSON.parse(await readFile(path, "utf8"));
1061
1126
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
1062
1127
  const value = parsed;
1063
1128
  if (typeof value.accessToken !== "string" || typeof value.expiresAt !== "number" || typeof value.clientId !== "string" || typeof value.tokenEndpoint !== "string" || typeof value.scope !== "string") {