@docmana/sdk 0.32.0 → 0.36.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,5 +1,61 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.36.0 - 2026-10-30
4
+
5
+ - Promote the additive review and execution-event client APIs to a stable release.
6
+ Unused review APIs require no additional backend configuration.
7
+ - Align native pending-list envelopes (documents are read on task detail), and
8
+ support confirmations, correction reasons and human review duration.
9
+ - Preserve DOC-2558 review conflict metadata through Consumer Edge: `decidedBy`,
10
+ `decidedAt` and `currentVersion`, with the exported `ReviewConflictDetails` type.
11
+ Keep deprecated legacy aliases and bounded metadata; decision conflicts are
12
+ never retried automatically. Normal executions and final callbacks are unchanged.
13
+
14
+ ## 0.36.0-hitl.2 - 2026-10-23 (HITL prerelease)
15
+
16
+ - Add `executionEvents.list()` and typed, non-consuming sequence-paged node
17
+ history for formulas with or without human review. Add the expanded
18
+ `verifyExecutionEventCallback` / `parseExecutionEventCallback` while retaining
19
+ the existing strict review-only helpers and final callback behavior.
20
+ - Publish pilot versions under the explicit `hitl` npm distribution tag;
21
+ other prereleases use `next` and stable releases use `latest`.
22
+
23
+ ## 0.36.0-hitl.1 - 2026-10-23 (local POC)
24
+
25
+ - Add `reviews.calculation()` and exported `ReviewCalculation` for non-consuming
26
+ provisional calculation state, coherent/target revisions, review counts and
27
+ per-revision node reuse. Preserve legacy null metadata, organization scope,
28
+ versioned decisions, assignment and final callback behavior.
29
+
30
+ ## 0.35.0-hitl.3 - 2026-10-22 (local POC)
31
+
32
+ - Add optional `ReviewTask.findings` and exported `ReviewFinding` for explicit,
33
+ review-scoped field signals. Preserve missing/empty findings and existing
34
+ corrections, assignment, global confirmation and final callbacks.
35
+
36
+ ## 0.35.0-hitl.2 - 2026-10-22 (local POC)
37
+
38
+ - Add optional read-only frozen review assessments (node score, description, feedback, explanation and reasoning). No per-field findings or decision/callback change.
39
+
40
+ ## 0.35.0-hitl.1 - 2026-10-22 (local POC)
41
+
42
+ - Preserve optional native review lifecycle timestamps for Live's human-wait and activity display. No final result fetch, pipeline or callback contract change.
43
+
44
+ ## 0.34.0 - 2026-10-22
45
+
46
+ - Add `reviews.document(id, docKey, "preview", options)` and exported
47
+ `DocumentPreviewManifest` for progressive import-time WebP preparation.
48
+ Existing document streams, review decisions and final callbacks are unchanged.
49
+
50
+ ## 0.33.0 - 2026-10-21
51
+
52
+ - Add typed review event callbacks and raw-body HMAC signature verification for
53
+ Consumer's separate review event relay. Existing execution callbacks are unchanged.
54
+ - Add `Docmana.reviews` for organization-scoped queues, task details, versioned
55
+ decisions and streaming page/content/location reads. Preserve range/cache
56
+ statuses and safe conflict metadata; never automatically retry decisions.
57
+ - Expose `AWAITING_REVIEW` as a non-terminal execution status.
58
+
3
59
  ## Unreleased
4
60
 
5
61
  ## 0.32.0 - 2026-10-19
@@ -257,3 +313,7 @@ consu## 0.16.0
257
313
  - Typed error hierarchy (`DocmanaError`, `DocmanaExecutionError`, …).
258
314
  - Flexible file inputs: path, `Buffer`, `Stream`, and multi-file uploads.
259
315
  - Dual ESM + CJS distribution with bundled type definitions.
316
+
317
+ ### Local HITL outline addition
318
+
319
+ - Add reviews.outline with typed published formula steps, edges and review marks. Existing execution, decision and final callback contracts are unchanged.
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Docmana
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Docmana
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,5 +1,68 @@
1
1
  # @docmana/sdk
2
2
 
3
+ The HITL pilot uses the explicit prerelease `0.36.0-hitl.2` and the npm `hitl`
4
+ distribution tag. Install the exact version for reproducible pilot builds:
5
+
6
+ ```sh
7
+ npm install --save-exact @docmana/sdk@0.36.0-hitl.2
8
+ ```
9
+
10
+ The stable default remains on `latest`; the prerelease does not select a native
11
+ API. Configure the SDK's Consumer base URL on the application server.
12
+
13
+ ## Real formula node progress (local HITL pilot)
14
+
15
+ `sdk.executionEvents.list(executionId, { organization, after: 0, limit: 100, signal })`
16
+ reads a non-consuming metadata journal, including formulas without human review.
17
+ Follow `nextAfter` while non-null; order by `sequence`, deduplicate `eventId`, and
18
+ honor `truncated` when the cursor predates retained history. The native journal
19
+ keeps at most 2,000 events with the execution. No PDF or final-result retention
20
+ is added. Older APIs can return 404; show confirmed status without inventing
21
+ progress. Never turn a journal failure into a call to another backend.
22
+
23
+ The four new names are `execution.node.started`, `execution.node.completed`,
24
+ `execution.node.failed`, and `execution.node.reused`. Events identify a node,
25
+ document keys, revision and attempt. A reused output does not imply a new LLM
26
+ call. Completion of a node is technical completion, not a positive business
27
+ verdict or final execution completion. `occurredAt` is the real action time;
28
+ `publishedAt` is optional broker time, present on signed callbacks.
29
+
30
+ `verifyExecutionEventCallback` and `parseExecutionEventCallback` accept both the
31
+ new node metadata and the four existing review events. Existing review-only
32
+ verification helpers remain strict and unchanged. Final execution callbacks and
33
+ result ownership keep their existing contract; there are no pipeline events.
34
+
35
+ ## Review event callbacks
36
+
37
+ SDK 0.33.0 adds server-side `verifyReviewEventCallback`, `parseReviewEventCallback`
38
+ and the `ReviewEventCallback` type for Consumer's opt-in review event relay.
39
+ This is independent from `runFlowAsync({ callbackUrl })`, which still receives only
40
+ the final result. No Service Bus connection is required in your application.
41
+
42
+ ```ts
43
+ import { verifyReviewEventCallback } from "@docmana/sdk";
44
+ const event = verifyReviewEventCallback({
45
+ body: rawUtf8Body,
46
+ signature: request.headers.get("x-docmana-event-signature"),
47
+ timestamp: request.headers.get("x-docmana-event-timestamp"),
48
+ secret: serverConfiguredSecret, // at least 32 characters; never sent to a browser
49
+ organization: expectedOrganization,
50
+ });
51
+ await persistOnce(event.organization, event.eventId, event);
52
+ // Only now return 2xx. Never finalize an execution from a review notification.
53
+ ```
54
+
55
+ Supported types are `execution.awaiting_review`, `execution.review_done`,
56
+ `execution.review_closed` and `execution.review_expired`. `review_done` means the
57
+ decision is recorded, not that the execution is finished. `review_closed` also
58
+ describes the review rather than the execution. `publishedAt` is the broker's
59
+ publication time. The metadata excludes document names, values, notes and results.
60
+ Signatures use HMAC-SHA256 over `timestamp + "." + rawBody` with header `v1=<hex>`;
61
+ verification accepts a five-minute clock difference. Broker retries are re-signed
62
+ but retain the event ID; store it durably for deduplication. An event notification
63
+ never consumes the final result. Source publication is best-effort: reconcile
64
+ current review state separately after a missed notification.
65
+
3
66
  Official Docmana SDK for calling `docmana-consumer-edge-api`.
4
67
 
5
68
  SDK 0.32.0 adds native formula authoring, assisted conversations and revision-pinned
@@ -18,6 +81,17 @@ status, and returns the mapped Consumer execution DTO.
18
81
  npm install @docmana/sdk
19
82
  ```
20
83
 
84
+ ### GitHub release recovery
85
+
86
+ The Release workflow publishes an existing `v<version>` tag. GitHub Actions are
87
+ pinned to full commit SHAs to satisfy the enterprise policy. If a tag-triggered
88
+ run fails before starting, merge the workflow correction first, then manually
89
+ run **Release** from `main` with that existing tag. The workflow checks out the
90
+ tag and verifies its package version before publishing; do not move a release
91
+ tag to repair a pipeline. Publication requires the existing npm publisher
92
+ credential or trusted-publishing configuration. Verify the version is available
93
+ in npm before updating a consumer's lockfile.
94
+
21
95
  Requires Node.js 20+.
22
96
 
23
97
  ## Quickstart
@@ -450,6 +524,98 @@ user confirmation before submitting the same file with `confirmOverwrite: true`.
450
524
  `remove(id, options)` permanently deletes the formula and its authoring data;
451
525
  confirm that action with the user. Import and deletion do not use operation IDs.
452
526
 
527
+ ## Human review of formula executions
528
+
529
+ The native pending queue omits documents: load the selected task with `reviews.get()`
530
+ before displaying its values and documents. Decided tasks leave this queue; resume
531
+ progress belongs to the execution. `ReviewDecision` also supports `confirmations`,
532
+ per-correction `reason`, and `durationSeconds`. Conflict details identify an already
533
+ submitted decision or the current version; no decision is retried automatically.
534
+
535
+ Read calculation metadata repeatedly with
536
+ `client.reviews.calculation(executionId, { organization })`. It returns
537
+ `executionResultId`, normalized `executionStatus` and `calculation` (null for
538
+ legacy/unmarked runs). Calculation includes `status`, coherent `revision`
539
+ (0 before the first calculation), optional `targetRevision`, `provisional`,
540
+ review counts and computed/reused node counts for that revision. Node counts
541
+ are not individual model-call counts. Calculation revision is distinct from
542
+ `ReviewTask.version`, which remains required for decisions. A completed
543
+ calculation stays provisional until the persisted execution is terminal.
544
+ This read is available before the first review and after final callback delivery;
545
+ it never consumes the single-use final result or triggers pipelines/callbacks.
546
+
547
+ Requires Consumer's review routes and the compatible HITL Document API branch.
548
+ No pipelines are required. Start published manual formulas with `runFlowAsync`;
549
+ `AWAITING_REVIEW` is non-terminal. `runFlow` still waits for final completion and
550
+ retains its configured timeout, so do not hold an HTTP request open for human work.
551
+
552
+ ```ts
553
+ const scope = { organization: "your-organization", signal: controller.signal };
554
+ const { reviews, nextCursor } = await docmana.reviews.list({
555
+ ...scope,
556
+ status: "pending",
557
+ limit: 50,
558
+ });
559
+ const task = await docmana.reviews.get(reviews[0].executionResultId, scope);
560
+ await docmana.reviews.decide(
561
+ task.executionResultId,
562
+ {
563
+ decision: "continue",
564
+ version: task.version,
565
+ reviewer: authenticatedServerActor, // Never trust an identity from the browser.
566
+ corrections: [{ itemId: task.documents[0].items![0].itemId, value: false }],
567
+ },
568
+ scope,
569
+ );
570
+ // Preparation starts at import. Poll queued/processing; other states are terminal.
571
+ const preparation = await docmana.reviews.document(
572
+ task.executionResultId,
573
+ task.documents[0].docKey,
574
+ "preview",
575
+ {
576
+ ...scope,
577
+ version: task.reviewId,
578
+ },
579
+ );
580
+ const manifest = await preparation.json(); // DocumentPreviewManifest
581
+ // Only fetch numbers present in manifest.pages; page 1 precedes later batches.
582
+ // Original content remains available as the PDF fallback.
583
+ const page = await docmana.reviews.document(
584
+ task.executionResultId,
585
+ "doc01",
586
+ { page: 1 },
587
+ {
588
+ ...scope,
589
+ version: task.reviewId,
590
+ size: "thumb",
591
+ },
592
+ );
593
+ // page is a streaming Fetch Response. Consume or cancel its body; do not buffer a large PDF.
594
+ ```
595
+
596
+ `list` accepts `pending`, `submitted`, `resuming` and an opaque `cursor`. It lists
597
+ open tasks only. The `"*"` dossier entry is not a file. Use the returned item IDs
598
+ unchanged; JSON values preserve `null`, `false`, `0`, objects and arrays.
599
+ `document` also accepts `"content"` or `"locations"`, Range/If-Range and ETag
600
+ options. It returns 200/206/304/416 without consuming the body.
601
+
602
+ A decision receipt means **recorded and resume queued**, never final completion.
603
+ On 409 inspect `DocmanaError.reviewConflict` and `apiCode`, then re-read the task.
604
+ `REVIEW_ALREADY_SUBMITTED` can include `reviewConflict.decidedBy` (`id`, `name`,
605
+ `source: "integrator" | "token"`) and `decidedAt`.
606
+ `REVIEW_VERSION_STALE` can include `reviewConflict.currentVersion`.
607
+ Missing metadata remains `null`; only bounded, whitelisted fields are retained.
608
+ The previous `version`, `submittedBy` and `submittedAt` properties remain as
609
+ deprecated compatibility aliases. Never replace the submitted version with the
610
+ new version and retry automatically: let the user inspect the refreshed task.
611
+ On 503 re-read first: the decision may already be recorded. The SDK does not retry
612
+ a decision after a conflict, 503 or uncertain transport outcome. The standard one-time
613
+ 401 token-refresh retry still applies. Keep the existing final callback/polling result owner;
614
+ opening a review must not consume the single-use execution result.
615
+
616
+ Release order: compatible Document API, Consumer, published SDK, then Arcana's
617
+ exact pinned dependency. Preparing this change does not publish or deploy it.
618
+
453
619
  ## License
454
620
 
455
621
  MIT
@@ -476,3 +642,20 @@ Use `authoring.trialDocument(fileBlob, filename, spec, { organization, signal })
476
642
  For assisted Mongo nodes, call `authoring.documentTrialDefinitions({ flowId, expectedRevisions, node | allNodes }, { organization, signal })`. This read-only preparation pins active node-owned rules and settings to the coherent native snapshot, checking revisions again after resolution. Pass the returned definition and uploaded file to the server-side `runAssistedDocumentTrial` library in `@docmana/docmana-flows`. Git callers use Flows' `prepareDocumentTrial` directly. Neither path requires `/extract/test-assisted` or a modified DocMana API. Calling the old native trial method with assisted nodes now fails before upload with HTTP 422.
477
643
 
478
644
  Flows owns assisted IDP/model calls and requires its server-side provider configuration. It analyzes the file once and tries up to 100 compatible classification/extraction nodes independently, retaining evidence envelopes. Files are nonempty PDF/images up to 20 MiB. Definitions and uploads use disposable local storage, cleaned after completion; no formula, rule, reference expectation or publication is saved. These trials do not execute production routing, translate results, compare expectations or produce release approval evidence. Missing upstream context is rejected. Native API accounting and PromptShield are not implied by the Flows executor. No new CLI command is introduced.
645
+
646
+ ### Formula steps during human review
647
+
648
+ Task detail also preserves optional native `createdAt`, `submittedAt`, `resumingAt` and `closedAt` timestamps. Human waiting is bounded by task creation and native decision acceptance; review closure does not prove final execution completion. Older backends can omit these fields; clients must show an unavailable duration rather than infer it from callback delivery time.
649
+
650
+ Use client.reviews.outline(flowId, { organization }) to read the published nodes, edges, ordering and human-review marks through Consumer. Compare outline.version with the review flowVersion before displaying progress. This read never consumes an execution result and does not introduce pipelines.
651
+
652
+ ### Frozen review evidence (local HITL pilot)
653
+
654
+ `reviews.get()` optionally returns `documents[].assessments`: the stored node score (0–100), `scoreDescription`, `feedback`, `explanation` and `reasoning` from the same frozen snapshot as the items. These are read-only node-level evidence, not uncertain-field findings. Older backends may omit assessments or scores; do not infer missing scores. Decisions still send only editable item corrections through Consumer.
655
+
656
+ `ReviewTask.findings?: ReviewFinding[]` separately carries explicit field signals:
657
+ stable review-scoped `id`, `code`, `message`, resolved `itemIds`, and optional
658
+ `source` (`model` or `rule`). Consumers must not infer fields from node scores or
659
+ explanation text. Missing or empty findings preserve the existing presentation.
660
+ These are frozen evidence, not per-field decisions; editing a value does not
661
+ erase its finding. Continue using the same global, versioned decision call.
package/dist/cli.mjs CHANGED
@@ -171,6 +171,234 @@ var FormulaAuthoring = class {
171
171
  }
172
172
  };
173
173
 
174
+ // src/sdk/reviews.ts
175
+ function segment(value) {
176
+ if (!value || value === "." || value === ".." || /[\\/]/.test(value) || [...value].some((char) => char.charCodeAt(0) < 32))
177
+ throw new Error("Invalid review identifier");
178
+ return encodeURIComponent(value);
179
+ }
180
+ var Reviews = class {
181
+ constructor(http) {
182
+ this.http = http;
183
+ }
184
+ http;
185
+ /** Repeatable metadata read: never consumes the final execution result. */
186
+ calculation(executionId, options) {
187
+ return this.http.requestJson("GET", `/executions/${segment(executionId)}/review/calculation`, {
188
+ signal: options.signal,
189
+ query: { organization: options.organization }
190
+ });
191
+ }
192
+ /** Published formula structure only. Compare its version with the review's flowVersion. */
193
+ outline(flowId, options) {
194
+ return this.http.requestJson("GET", `/flows/${segment(flowId)}/outline`, {
195
+ signal: options.signal,
196
+ query: { organization: options.organization }
197
+ });
198
+ }
199
+ list(options) {
200
+ return this.http.requestJson("GET", "/reviews", {
201
+ signal: options.signal,
202
+ query: {
203
+ organization: options.organization,
204
+ ...options.status ? { status: options.status } : {},
205
+ ...options.cursor ? { cursor: options.cursor } : {},
206
+ ...options.limit ? { limit: String(options.limit) } : {}
207
+ }
208
+ });
209
+ }
210
+ get(executionId, options) {
211
+ return this.http.requestJson("GET", `/executions/${segment(executionId)}/review`, {
212
+ signal: options.signal,
213
+ query: { organization: options.organization }
214
+ });
215
+ }
216
+ decide(executionId, decision, options) {
217
+ return this.http.requestJson("POST", `/executions/${segment(executionId)}/review`, {
218
+ signal: options.signal,
219
+ query: { organization: options.organization },
220
+ headers: { "content-type": "application/json" },
221
+ body: JSON.stringify(decision)
222
+ });
223
+ }
224
+ /** Streaming Response: callers own consumption/cancellation. Never buffers the PDF. */
225
+ document(executionId, docKey, resource, options) {
226
+ if (typeof resource === "object" && (!Number.isSafeInteger(resource.page) || resource.page < 1 || resource.page > 999999))
227
+ throw new Error("Invalid page number");
228
+ const path = typeof resource === "object" ? `pages/${resource.page}` : resource;
229
+ const headers = Object.fromEntries(
230
+ Object.entries({
231
+ range: options.range,
232
+ "if-range": options.ifRange,
233
+ "if-none-match": options.ifNoneMatch,
234
+ "if-modified-since": options.ifModifiedSince
235
+ }).filter((entry) => typeof entry[1] === "string")
236
+ );
237
+ return this.http.requestRaw(
238
+ "GET",
239
+ `/executions/${segment(executionId)}/review/documents/${segment(docKey)}/${path}`,
240
+ {
241
+ signal: options.signal,
242
+ headers,
243
+ acceptStatuses: [304, 416],
244
+ query: {
245
+ organization: options.organization,
246
+ ...options.version ? { v: options.version } : {},
247
+ ...options.size ? { size: options.size } : {}
248
+ }
249
+ }
250
+ );
251
+ }
252
+ };
253
+
254
+ // src/sdk/execution-events.ts
255
+ import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
256
+
257
+ // src/sdk/review-events.ts
258
+ import { createHmac, timingSafeEqual } from "crypto";
259
+ var REVIEW_EVENT_TYPES = [
260
+ "execution.awaiting_review",
261
+ "execution.review_done",
262
+ "execution.review_closed",
263
+ "execution.review_expired"
264
+ ];
265
+ var ReviewEventVerificationError = class extends Error {
266
+ constructor(reason) {
267
+ super(`Invalid review event ${reason}`);
268
+ this.reason = reason;
269
+ this.name = "ReviewEventVerificationError";
270
+ }
271
+ reason;
272
+ };
273
+ function parseReviewEventCallback(value) {
274
+ const fail = () => {
275
+ throw new ReviewEventVerificationError("payload");
276
+ };
277
+ if (!value || typeof value !== "object" || Array.isArray(value)) return fail();
278
+ const raw = value;
279
+ const text = (key) => {
280
+ const v = raw[key];
281
+ if (typeof v !== "string" || !v.trim() || v.length > 512 || [...v].some((char) => char.charCodeAt(0) < 32))
282
+ return fail();
283
+ return v;
284
+ };
285
+ if (raw.schemaVersion !== 1 || !REVIEW_EVENT_TYPES.includes(raw.type))
286
+ return fail();
287
+ const event = {
288
+ schemaVersion: 1,
289
+ eventId: text("eventId"),
290
+ type: raw.type,
291
+ organization: text("organization"),
292
+ executionResultId: text("executionResultId"),
293
+ reviewId: text("reviewId"),
294
+ publishedAt: text("publishedAt")
295
+ };
296
+ if (!Number.isFinite(Date.parse(event.publishedAt))) return fail();
297
+ for (const key of ["flowId", "nodeId"])
298
+ if (raw[key] !== void 0) event[key] = text(key);
299
+ for (const key of ["flowVersion", "reviewVersion"]) {
300
+ if (raw[key] === void 0) continue;
301
+ if (!Number.isSafeInteger(raw[key]) || Number(raw[key]) < 1) return fail();
302
+ event[key] = Number(raw[key]);
303
+ }
304
+ if (raw.documentKeys !== void 0) {
305
+ if (!Array.isArray(raw.documentKeys) || raw.documentKeys.length > 1e3 || raw.documentKeys.some((v) => typeof v !== "string" || !v || v.length > 512))
306
+ return fail();
307
+ event.documentKeys = raw.documentKeys;
308
+ }
309
+ if (raw.decision !== void 0) {
310
+ if (!["continue", "exclude", "rerun", "reject"].includes(String(raw.decision))) return fail();
311
+ event.decision = raw.decision;
312
+ }
313
+ if (raw.reviewStatus !== void 0) {
314
+ if (!["completed", "rejected", "failed"].includes(String(raw.reviewStatus))) return fail();
315
+ event.reviewStatus = raw.reviewStatus;
316
+ }
317
+ if (event.type === "execution.review_done" && (!event.decision || !event.reviewVersion))
318
+ return fail();
319
+ if (event.type === "execution.review_closed" && !event.reviewStatus) return fail();
320
+ return event;
321
+ }
322
+
323
+ // src/sdk/execution-events.ts
324
+ var EXECUTION_NODE_EVENT_TYPES = [
325
+ "execution.node.started",
326
+ "execution.node.completed",
327
+ "execution.node.failed",
328
+ "execution.node.reused"
329
+ ];
330
+ function parseExecutionEventCallback(value) {
331
+ const raw = value;
332
+ if (!raw || !EXECUTION_NODE_EVENT_TYPES.includes(raw.type))
333
+ return parseReviewEventCallback(value);
334
+ const fail = () => {
335
+ throw new ReviewEventVerificationError("payload");
336
+ };
337
+ const text = (key) => {
338
+ const v = raw[key];
339
+ return typeof v === "string" && v.length > 0 && v.length <= 512 && [...v].every((c) => c.charCodeAt(0) >= 32) ? v : fail();
340
+ };
341
+ const integer = (key) => Number.isSafeInteger(raw[key]) && Number(raw[key]) >= 1 ? Number(raw[key]) : fail();
342
+ if (raw.schemaVersion !== 1) return fail();
343
+ const event = {
344
+ schemaVersion: 1,
345
+ eventId: text("eventId"),
346
+ type: raw.type,
347
+ organization: text("organization"),
348
+ executionResultId: text("executionResultId"),
349
+ sequence: integer("sequence"),
350
+ occurredAt: text("occurredAt"),
351
+ nodeId: text("nodeId"),
352
+ nodeType: text("nodeType"),
353
+ revision: integer("revision"),
354
+ attemptId: text("attemptId"),
355
+ documentKeys: []
356
+ };
357
+ if (!Number.isFinite(Date.parse(event.occurredAt))) return fail();
358
+ if (!Array.isArray(raw.documentKeys) || !raw.documentKeys.length || raw.documentKeys.length > 1e3 || raw.documentKeys.some(
359
+ (v) => typeof v !== "string" || !v || v.length > 512 || [...v].some((c) => c.charCodeAt(0) < 32)
360
+ ))
361
+ return fail();
362
+ event.documentKeys = raw.documentKeys;
363
+ if (raw.flowId !== void 0) event.flowId = text("flowId");
364
+ if (raw.flowVersion !== void 0) event.flowVersion = integer("flowVersion");
365
+ if (raw.publishedAt !== void 0) {
366
+ event.publishedAt = text("publishedAt");
367
+ if (!Number.isFinite(Date.parse(event.publishedAt))) return fail();
368
+ }
369
+ return event;
370
+ }
371
+ var ExecutionEvents = class {
372
+ constructor(http) {
373
+ this.http = http;
374
+ }
375
+ http;
376
+ /** One bounded page; never polls for completion or consumes a final result. */
377
+ async list(id, options) {
378
+ const after = options.after ?? 0, limit = options.limit ?? 100;
379
+ if (!id || id.length > 512 || id === "." || id === ".." || /[\\/]/.test(id) || [...id].some((c) => c.charCodeAt(0) < 32) || !options.organization?.trim() || !Number.isSafeInteger(after) || after < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 100)
380
+ throw new Error("Invalid execution event query");
381
+ const page = await this.http.requestJson(
382
+ "GET",
383
+ `/executions/${encodeURIComponent(id)}/events`,
384
+ {
385
+ signal: options.signal,
386
+ query: { organization: options.organization, after: String(after), limit: String(limit) }
387
+ }
388
+ );
389
+ if (page.executionResultId !== id || !Array.isArray(page.events) || page.events.length > limit || !Number.isSafeInteger(page.lastSequence) || page.lastSequence < 0 || typeof page.truncated !== "boolean")
390
+ throw new ReviewEventVerificationError("payload");
391
+ const events = page.events.map((value) => parseExecutionEventCallback(value));
392
+ if (events.some(
393
+ (event, i) => !("sequence" in event) || event.executionResultId !== id || event.organization !== options.organization || event.sequence <= (i ? events[i - 1].sequence : after) || event.sequence > page.lastSequence
394
+ ))
395
+ throw new ReviewEventVerificationError("payload");
396
+ if (page.nextAfter !== null && (!Number.isSafeInteger(page.nextAfter) || page.nextAfter <= after || page.nextAfter !== events.at(-1)?.sequence))
397
+ throw new ReviewEventVerificationError("payload");
398
+ return { ...page, events };
399
+ }
400
+ };
401
+
174
402
  // src/sdk/errors.ts
175
403
  var DocmanaError = class extends Error {
176
404
  status;
@@ -178,6 +406,7 @@ var DocmanaError = class extends Error {
178
406
  code;
179
407
  /** Public API business code; `code` retains the SDK's existing error category. */
180
408
  apiCode;
409
+ reviewConflict;
181
410
  constructor(message, opts = {}) {
182
411
  super(message);
183
412
  this.name = new.target.name;
@@ -303,7 +532,7 @@ var HttpClient = class {
303
532
  if (res.status === 401 && this.opts.getAccessToken) {
304
533
  res = await this.send(method, path, options, true);
305
534
  }
306
- if (!res.ok) {
535
+ if (!res.ok && !options.acceptStatuses?.includes(res.status)) {
307
536
  const requestId = res.headers.get("x-request-id") ?? void 0;
308
537
  const message = await this.extractMessage(res);
309
538
  const error = errorFromResponse(
@@ -315,6 +544,11 @@ var HttpClient = class {
315
544
  const data = await res.clone().json().catch(() => null);
316
545
  if (typeof data?.code === "string" && /^[A-Za-z][A-Za-z0-9_]{0,99}$/.test(data.code))
317
546
  Object.defineProperty(error, "apiCode", { value: data.code, enumerable: true });
547
+ if (res.status === 409 && isRecord(data?.details))
548
+ Object.defineProperty(error, "reviewConflict", {
549
+ value: reviewConflictDetails(data.details),
550
+ enumerable: true
551
+ });
318
552
  throw error;
319
553
  }
320
554
  return res;
@@ -358,6 +592,32 @@ var HttpClient = class {
358
592
  }
359
593
  }
360
594
  };
595
+ function isRecord(value) {
596
+ return typeof value === "object" && value !== null && !Array.isArray(value);
597
+ }
598
+ function reviewConflictDetails(raw) {
599
+ const text = (value, limit) => typeof value === "string" ? value.slice(0, limit) : null;
600
+ const version = (value) => Number.isSafeInteger(value) && Number(value) >= 1 ? value : null;
601
+ const actor = isRecord(raw.decidedBy) ? raw.decidedBy : {};
602
+ const id = text(actor.id, 200);
603
+ const name = text(actor.name, 200);
604
+ const source = actor.source === "integrator" || actor.source === "token" ? actor.source : null;
605
+ const decidedBy = id !== null || name !== null || source !== null ? {
606
+ ...id !== null ? { id } : {},
607
+ ...name !== null ? { name } : {},
608
+ ...source ? { source } : {}
609
+ } : null;
610
+ const currentVersion = version(raw.currentVersion) ?? version(raw.version);
611
+ const decidedAt = text(raw.decidedAt, 40) ?? text(raw.submittedAt, 40);
612
+ return {
613
+ currentVersion,
614
+ decidedBy,
615
+ decidedAt,
616
+ version: currentVersion,
617
+ submittedBy: id ?? name ?? text(raw.submittedBy, 200),
618
+ submittedAt: decidedAt
619
+ };
620
+ }
361
621
  function parseRetryAfterMs(value) {
362
622
  if (!value) return void 0;
363
623
  const seconds = Number(value);
@@ -816,6 +1076,8 @@ function fullJitterDelay(intervalMs, maxIntervalMs, attempt, random) {
816
1076
  // src/sdk/client.ts
817
1077
  var Docmana = class {
818
1078
  authoring;
1079
+ reviews;
1080
+ executionEvents;
819
1081
  config;
820
1082
  http;
821
1083
  constructor(config) {
@@ -828,6 +1090,8 @@ var Docmana = class {
828
1090
  headers: this.config.headers
829
1091
  });
830
1092
  this.authoring = new FormulaAuthoring(this.http);
1093
+ this.reviews = new Reviews(this.http);
1094
+ this.executionEvents = new ExecutionEvents(this.http);
831
1095
  }
832
1096
  async runFlowAsync(flowId, params, options = {}) {
833
1097
  const parts = await resolveInputs(params);