@codemation/core-nodes 0.9.0 → 0.10.1
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 +45 -0
- package/dist/index.cjs +140 -48
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +55 -16
- package/dist/index.d.ts +55 -16
- package/dist/index.js +140 -49
- package/dist/index.js.map +1 -1
- package/dist/metadata.json +13 -1
- package/package.json +2 -2
- package/src/chatModels/CodemationChatModelFactory.ts +2 -61
- package/src/chatModels/ManagedHmacSignerFactory.types.ts +88 -0
- package/src/index.ts +1 -0
- package/src/nodes/AIAgentNode.ts +14 -4
- package/src/nodes/codemationDocumentScannerNode.ts +131 -0
package/dist/metadata.json
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"packageName": "@codemation/core-nodes",
|
|
4
|
-
"packageVersion": "0.
|
|
4
|
+
"packageVersion": "0.10.1",
|
|
5
5
|
"description": "",
|
|
6
6
|
"kind": "nodes",
|
|
7
7
|
"nodes": [
|
|
8
|
+
{
|
|
9
|
+
"name": "Codemation Document Scanner",
|
|
10
|
+
"kind": "node",
|
|
11
|
+
"description": "",
|
|
12
|
+
"inputPorts": [
|
|
13
|
+
"main"
|
|
14
|
+
],
|
|
15
|
+
"outputPorts": [
|
|
16
|
+
"main"
|
|
17
|
+
],
|
|
18
|
+
"sourcePath": "src/nodes/codemationDocumentScannerNode.ts"
|
|
19
|
+
},
|
|
8
20
|
{
|
|
9
21
|
"name": "Collection: Delete",
|
|
10
22
|
"kind": "node",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codemation/core-nodes",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"@ai-sdk/provider": "^3.0.8",
|
|
33
33
|
"ai": "^6.0.168",
|
|
34
34
|
"croner": "^10.0.1",
|
|
35
|
-
"@codemation/core": "0.
|
|
35
|
+
"@codemation/core": "0.13.1"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/node": "^25.3.5",
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { createHmac, createHash, randomBytes } from "node:crypto";
|
|
2
1
|
import type { ChatLanguageModel, ChatModelFactory, NodeExecutionContext } from "@codemation/core";
|
|
3
2
|
import { chatModel } from "@codemation/core";
|
|
4
3
|
|
|
5
4
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
6
5
|
|
|
7
6
|
import type { CodemationChatModelConfig } from "./CodemationChatModelConfig";
|
|
7
|
+
import { managedHmacFetchFactory } from "./ManagedHmacSignerFactory.types";
|
|
8
8
|
|
|
9
9
|
@chatModel({ packageName: "@codemation/core-nodes" })
|
|
10
10
|
export class CodemationChatModelFactory implements ChatModelFactory<CodemationChatModelConfig> {
|
|
@@ -26,7 +26,7 @@ export class CodemationChatModelFactory implements ChatModelFactory<CodemationCh
|
|
|
26
26
|
throw new Error("Codemation managed AI not available in this environment (workspace pairing is not configured).");
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const hmacFetch =
|
|
29
|
+
const hmacFetch = managedHmacFetchFactory(workspaceId, pairingSecret);
|
|
30
30
|
// apiKey is required by the AI SDK but unused — authentication is handled by the HMAC-signed fetch wrapper.
|
|
31
31
|
const provider = createOpenAI({ baseURL: `${gatewayUrl}/v1`, apiKey: "codemation-managed", fetch: hmacFetch });
|
|
32
32
|
const languageModel = provider.chat(args.config.model);
|
|
@@ -41,63 +41,4 @@ export class CodemationChatModelFactory implements ChatModelFactory<CodemationCh
|
|
|
41
41
|
},
|
|
42
42
|
});
|
|
43
43
|
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Creates an HMAC-signed fetch wrapper for use with AI SDK's createOpenAI.
|
|
47
|
-
* Each call signs the request body with the workspace pairing secret so the
|
|
48
|
-
* LLM broker can authenticate the workspace without a user-managed API key.
|
|
49
|
-
*
|
|
50
|
-
* Mirrors HmacRequestSigner from @codemation/host/pairing without importing
|
|
51
|
-
* that package (which would create a circular dependency since @codemation/host
|
|
52
|
-
* depends on @codemation/core-nodes).
|
|
53
|
-
*/
|
|
54
|
-
private buildHmacSignedFetch(workspaceId: string, pairingSecret: string): typeof fetch {
|
|
55
|
-
return async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
|
|
56
|
-
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
57
|
-
const method = init?.method ?? "POST";
|
|
58
|
-
|
|
59
|
-
// Normalise body to a string for signing. Chat completions are always JSON strings
|
|
60
|
-
// but the fetch spec allows other BodyInit types — handle those defensively.
|
|
61
|
-
let bodyString = "";
|
|
62
|
-
if (init?.body !== undefined && init.body !== null) {
|
|
63
|
-
if (typeof init.body === "string") {
|
|
64
|
-
bodyString = init.body;
|
|
65
|
-
} else {
|
|
66
|
-
bodyString = await new Response(init.body).text();
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const authHeader = this.buildHmacAuthHeader(workspaceId, pairingSecret, method, url, bodyString);
|
|
71
|
-
|
|
72
|
-
const headers = new Headers(init?.headers as Record<string, string> | undefined);
|
|
73
|
-
headers.set("Authorization", authHeader);
|
|
74
|
-
|
|
75
|
-
// Use the same (possibly normalised) body string that was signed.
|
|
76
|
-
const effectiveBody = bodyString || init?.body;
|
|
77
|
-
return fetch(input, { ...init, body: effectiveBody, headers });
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Produces a Codemation-Hmac v1 Authorization header value.
|
|
83
|
-
* The algorithm must match HmacVerifier.computeSignature() in the control-plane.
|
|
84
|
-
*/
|
|
85
|
-
private buildHmacAuthHeader(
|
|
86
|
-
workspaceId: string,
|
|
87
|
-
pairingSecret: string,
|
|
88
|
-
method: string,
|
|
89
|
-
url: string,
|
|
90
|
-
body: string,
|
|
91
|
-
): string {
|
|
92
|
-
const ts = Math.floor(Date.now() / 1000);
|
|
93
|
-
const nonce = randomBytes(16).toString("base64");
|
|
94
|
-
const parsed = new URL(url);
|
|
95
|
-
const path = (parsed.pathname + parsed.search).toLowerCase();
|
|
96
|
-
const bodyHash = createHash("sha256").update(body, "utf8").digest("hex");
|
|
97
|
-
const baseString = [method.toUpperCase(), path, ts, nonce, bodyHash].join("\n");
|
|
98
|
-
// eslint-disable-next-line codemation/no-buffer-everything -- pairing secret is a fixed 32-byte value, never large
|
|
99
|
-
const secretBytes = Buffer.from(pairingSecret, "base64");
|
|
100
|
-
const sig = createHmac("sha256", secretBytes).update(baseString, "utf8").digest("base64");
|
|
101
|
-
return `Codemation-Hmac v=1,workspaceId=${workspaceId},ts=${ts},nonce=${nonce},sig=${sig}`;
|
|
102
|
-
}
|
|
103
44
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { createHmac, createHash, randomBytes } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export interface ManagedHmacSignerOptions {
|
|
4
|
+
/**
|
|
5
|
+
* When true (the default), the signer hashes the request body and includes
|
|
6
|
+
* the hash in the HMAC base string. Use for small JSON payloads (LLM chat).
|
|
7
|
+
*
|
|
8
|
+
* When false (doc-scanner / LD11 mode), the signer computes the signature
|
|
9
|
+
* over sha256("") regardless of the actual body bytes, and forwards those
|
|
10
|
+
* bytes to the upstream fetch untouched. The HMAC binds the workspace
|
|
11
|
+
* identity, not the file content — enabling streaming without buffering.
|
|
12
|
+
*/
|
|
13
|
+
signBody?: boolean;
|
|
14
|
+
/** Override wall-clock seconds for deterministic testing. */
|
|
15
|
+
now?: () => number;
|
|
16
|
+
/** Override nonce generation for deterministic testing. */
|
|
17
|
+
nonce?: () => string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Creates an HMAC-signing fetch wrapper that authenticates requests to
|
|
22
|
+
* Codemation managed services (LLM broker, doc-scanner) with the
|
|
23
|
+
* Codemation-Hmac v=1 scheme.
|
|
24
|
+
*
|
|
25
|
+
* Mirrors HmacRequestSigner from @codemation/host/pairing without importing
|
|
26
|
+
* that package (which would create a circular dependency since @codemation/host
|
|
27
|
+
* depends on @codemation/core-nodes).
|
|
28
|
+
*
|
|
29
|
+
* @param workspaceId - Workspace identifier injected by the CP provisioner.
|
|
30
|
+
* @param pairingSecret - Base64-encoded 32-byte HMAC key injected by the provisioner.
|
|
31
|
+
* @param options - Optional behaviour flags and test seams.
|
|
32
|
+
*/
|
|
33
|
+
export function managedHmacFetchFactory(
|
|
34
|
+
workspaceId: string,
|
|
35
|
+
pairingSecret: string,
|
|
36
|
+
options?: ManagedHmacSignerOptions,
|
|
37
|
+
): typeof fetch {
|
|
38
|
+
// LLM chat (the existing caller) signs its small JSON body → signBody defaults true.
|
|
39
|
+
// The doc-scanner node passes signBody:false (LD11): the HMAC binds the WORKSPACE,
|
|
40
|
+
// not the file, so we never read/normalise the (possibly large, binary) body —
|
|
41
|
+
// it is forwarded untouched and the signature covers an empty body hash.
|
|
42
|
+
const signBody = options?.signBody ?? true;
|
|
43
|
+
|
|
44
|
+
return async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
|
|
45
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
46
|
+
const method = init?.method ?? "POST";
|
|
47
|
+
|
|
48
|
+
let bodyForSigning = "";
|
|
49
|
+
if (signBody && init?.body !== undefined && init.body !== null) {
|
|
50
|
+
bodyForSigning = typeof init.body === "string" ? init.body : await new Response(init.body).text();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const authHeader = buildHmacAuthHeader(workspaceId, pairingSecret, method, url, bodyForSigning, options);
|
|
54
|
+
|
|
55
|
+
const headers = new Headers(init?.headers as Record<string, string> | undefined);
|
|
56
|
+
headers.set("Authorization", authHeader);
|
|
57
|
+
|
|
58
|
+
// When signing the body, forward the (possibly normalised) string.
|
|
59
|
+
// When not signing (signBody:false), forward the ORIGINAL body untouched
|
|
60
|
+
// so binary/streamed payloads are never buffered or re-encoded.
|
|
61
|
+
const outgoingBody = signBody ? bodyForSigning || init?.body : init?.body;
|
|
62
|
+
return fetch(input, { ...init, body: outgoingBody, headers });
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Produces a Codemation-Hmac v=1 Authorization header value.
|
|
68
|
+
* Algorithm must match HmacVerifier.computeSignature() in the control-plane.
|
|
69
|
+
*/
|
|
70
|
+
function buildHmacAuthHeader(
|
|
71
|
+
workspaceId: string,
|
|
72
|
+
pairingSecret: string,
|
|
73
|
+
method: string,
|
|
74
|
+
url: string,
|
|
75
|
+
body: string,
|
|
76
|
+
overrides?: Pick<ManagedHmacSignerOptions, "now" | "nonce">,
|
|
77
|
+
): string {
|
|
78
|
+
const ts = overrides?.now ? overrides.now() : Math.floor(Date.now() / 1000);
|
|
79
|
+
const nonce = overrides?.nonce ? overrides.nonce() : randomBytes(16).toString("base64");
|
|
80
|
+
const parsed = new URL(url);
|
|
81
|
+
const path = (parsed.pathname + parsed.search).toLowerCase();
|
|
82
|
+
const bodyHash = createHash("sha256").update(body, "utf8").digest("hex");
|
|
83
|
+
const baseString = [method.toUpperCase(), path, ts, nonce, bodyHash].join("\n");
|
|
84
|
+
// eslint-disable-next-line codemation/no-buffer-everything -- pairing secret is a fixed 32-byte value, never large
|
|
85
|
+
const secretBytes = Buffer.from(pairingSecret, "base64");
|
|
86
|
+
const sig = createHmac("sha256", secretBytes).update(baseString, "utf8").digest("base64");
|
|
87
|
+
return `Codemation-Hmac v=1,workspaceId=${workspaceId},ts=${ts},nonce=${nonce},sig=${sig}`;
|
|
88
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -44,3 +44,4 @@ export * from "./nodes/ConnectionCredentialNodeConfigFactory";
|
|
|
44
44
|
export * from "./nodes/ConnectionCredentialExecutionContextFactory";
|
|
45
45
|
export * from "./nodes/collections";
|
|
46
46
|
export * from "./nodes/InboxApprovalNode.types";
|
|
47
|
+
export * from "./nodes/codemationDocumentScannerNode";
|
package/src/nodes/AIAgentNode.ts
CHANGED
|
@@ -867,10 +867,20 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
867
867
|
});
|
|
868
868
|
try {
|
|
869
869
|
const callOptions = this.resolveCallOptions(model, guardrails.modelInvocationOptions);
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
870
|
+
// Always feed the AI SDK a plain JSON Schema, never a raw Zod schema. A consumer
|
|
871
|
+
// workflow's outputSchema is created with the consumer's tsx-loaded Zod — a different
|
|
872
|
+
// runtime copy than the framework's Zod — so handing that object to `Output.object`
|
|
873
|
+
// throws "schema is not a function". Convert via the schema's own instance method
|
|
874
|
+
// (dual-zod safe; see AIAgentExecutionHelpersFactory) before wrapping with jsonSchema().
|
|
875
|
+
const schemaRecord = this.isZodSchema(schema)
|
|
876
|
+
? this.executionHelpers.createJsonSchemaRecord(schema, {
|
|
877
|
+
schemaName: structuredOptions?.schemaName ?? "structured_output",
|
|
878
|
+
requireObjectRoot: true,
|
|
879
|
+
})
|
|
880
|
+
: schema;
|
|
881
|
+
const outputSchema = Output.object({
|
|
882
|
+
schema: jsonSchema(schemaRecord as Parameters<typeof jsonSchema>[0]) as never,
|
|
883
|
+
});
|
|
874
884
|
const result = await generateText({
|
|
875
885
|
model: model.languageModel as LanguageModel,
|
|
876
886
|
messages: [...messages],
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { defineNode } from "@codemation/core";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { managedHmacFetchFactory } from "../chatModels/ManagedHmacSignerFactory.types";
|
|
4
|
+
|
|
5
|
+
const ANALYZER_TYPES = ["document", "invoice", "image", "auto"] as const;
|
|
6
|
+
export type DocScannerAnalyzerType = (typeof ANALYZER_TYPES)[number];
|
|
7
|
+
|
|
8
|
+
/** Per-field value/confidence shape as returned by apps/doc-scanner. */
|
|
9
|
+
export type DocScannerField = Readonly<{
|
|
10
|
+
value: unknown;
|
|
11
|
+
confidence: number | null;
|
|
12
|
+
}>;
|
|
13
|
+
|
|
14
|
+
/** Output shape of CodemationDocumentScanner — identical to the service wire response. */
|
|
15
|
+
export type DocScannerOutput = Readonly<{
|
|
16
|
+
markdown: string;
|
|
17
|
+
fields: Readonly<Record<string, DocScannerField>>;
|
|
18
|
+
}>;
|
|
19
|
+
|
|
20
|
+
export type CodemationDocumentScannerConfig = Readonly<{
|
|
21
|
+
/** Key on `item.binary` that holds the document. Default: "data". */
|
|
22
|
+
binaryField?: string;
|
|
23
|
+
/** Analyzer type. Default: "auto" (routes on mime type on the service side). */
|
|
24
|
+
analyzerType?: DocScannerAnalyzerType;
|
|
25
|
+
/** MIME type override. Falls back to attachment.mimeType. */
|
|
26
|
+
contentType?: string;
|
|
27
|
+
/** Include per-field confidence scores (0–1). Default: false.
|
|
28
|
+
* Enabling this roughly doubles contextualization tokens for document analyzers.
|
|
29
|
+
* Image and auto-to-image requests silently ignore this flag (confidence stays null). */
|
|
30
|
+
includeConfidence?: boolean;
|
|
31
|
+
/** Max bytes checked before any read. Default: 50 MiB (same cap as OCR nodes, LD10). */
|
|
32
|
+
maxBytes?: number;
|
|
33
|
+
}>;
|
|
34
|
+
|
|
35
|
+
export const codemationDocumentScannerNode = defineNode({
|
|
36
|
+
key: "codemation.document-scanner",
|
|
37
|
+
title: "Codemation Document Scanner",
|
|
38
|
+
description:
|
|
39
|
+
"Analyzes a binary attachment (document or image) via the managed Codemation document-scanning service " +
|
|
40
|
+
"and returns markdown text plus structured fields. No Azure credential required — auth uses the workspace " +
|
|
41
|
+
"pairing secret. Enable includeConfidence to get per-field confidence scores (0–1).",
|
|
42
|
+
icon: "lucide:scan-text",
|
|
43
|
+
input: {
|
|
44
|
+
binaryField: "data",
|
|
45
|
+
analyzerType: "auto" as DocScannerAnalyzerType,
|
|
46
|
+
contentType: undefined as string | undefined,
|
|
47
|
+
includeConfidence: false,
|
|
48
|
+
maxBytes: undefined as number | undefined,
|
|
49
|
+
},
|
|
50
|
+
configSchema: z.object({
|
|
51
|
+
binaryField: z.string().optional(),
|
|
52
|
+
analyzerType: z.enum(ANALYZER_TYPES).optional(),
|
|
53
|
+
contentType: z.string().optional(),
|
|
54
|
+
includeConfidence: z.boolean().optional(),
|
|
55
|
+
maxBytes: z.number().int().positive().optional(),
|
|
56
|
+
}),
|
|
57
|
+
// keepBinaries is omitted (false) — the output replaces the item payload.
|
|
58
|
+
// To carry the source binary forward, set keepBinaries: true at the call site
|
|
59
|
+
// or use a downstream step that reads item.binary.
|
|
60
|
+
inspectorSummary({ config }) {
|
|
61
|
+
const cfg = config as unknown as CodemationDocumentScannerConfig;
|
|
62
|
+
const rows: Array<{ label: string; value: string }> = [
|
|
63
|
+
{ label: "Analyzer type", value: cfg.analyzerType ?? "auto" },
|
|
64
|
+
];
|
|
65
|
+
const binaryField = cfg.binaryField ?? "data";
|
|
66
|
+
if (binaryField !== "data") rows.push({ label: "Binary field", value: binaryField });
|
|
67
|
+
if (cfg.includeConfidence) rows.push({ label: "Confidence", value: "enabled" });
|
|
68
|
+
if (cfg.contentType) rows.push({ label: "Content type", value: cfg.contentType });
|
|
69
|
+
return rows;
|
|
70
|
+
},
|
|
71
|
+
async execute({ item, ctx }, { config: rawConfig }) {
|
|
72
|
+
const config = rawConfig as unknown as CodemationDocumentScannerConfig;
|
|
73
|
+
|
|
74
|
+
// eslint-disable-next-line no-restricted-properties -- DOC_SCANNER_GATEWAY_URL is injected by the control-plane provisioner into the workspace process env; reading it at execute time is the justified boundary.
|
|
75
|
+
const gatewayUrl = process.env["DOC_SCANNER_GATEWAY_URL"];
|
|
76
|
+
if (!gatewayUrl) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
"Codemation Document Scanner not available in this environment (DOC_SCANNER_GATEWAY_URL is not set).",
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Workspace pairing identity — injected by the CP provisioner (mirrors
|
|
83
|
+
// CodemationChatModelFactory). Passed to the signer; the HMAC binds THIS
|
|
84
|
+
// workspace (LD4) and does not sign the file body (LD11).
|
|
85
|
+
// eslint-disable-next-line no-restricted-properties -- WORKSPACE_ID is injected by the provisioner; read at execute time.
|
|
86
|
+
const workspaceId = process.env["WORKSPACE_ID"];
|
|
87
|
+
// eslint-disable-next-line no-restricted-properties -- WORKSPACE_PAIRING_SECRET is injected by the provisioner; read at execute time.
|
|
88
|
+
const pairingSecret = process.env["WORKSPACE_PAIRING_SECRET"];
|
|
89
|
+
if (!workspaceId || !pairingSecret) {
|
|
90
|
+
throw new Error("Codemation Document Scanner not available (workspace pairing is not configured).");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const binaryField = config.binaryField ?? "data";
|
|
94
|
+
const attachment = item.binary?.[binaryField];
|
|
95
|
+
if (!attachment) {
|
|
96
|
+
throw new Error(`Codemation Document Scanner: no binary attachment at key "${binaryField}".`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const body = await ctx.binary.getBytes(attachment, config.maxBytes);
|
|
100
|
+
const contentType = config.contentType ?? attachment.mimeType ?? "application/octet-stream";
|
|
101
|
+
const analyzerType = config.analyzerType ?? "auto";
|
|
102
|
+
const includeConfidence = config.includeConfidence ?? false;
|
|
103
|
+
|
|
104
|
+
const confidenceSuffix = includeConfidence ? "&confidence=true" : "";
|
|
105
|
+
const url = `${gatewayUrl}/analyze?type=${encodeURIComponent(analyzerType)}${confidenceSuffix}`;
|
|
106
|
+
|
|
107
|
+
// signBody: false (LD11): the HMAC binds the workspace, not the file body.
|
|
108
|
+
// The signer forwards the bytes untouched and signs an empty-body hash, so
|
|
109
|
+
// we never re-read or normalise the binary payload.
|
|
110
|
+
const hmacFetch = managedHmacFetchFactory(workspaceId, pairingSecret, { signBody: false });
|
|
111
|
+
|
|
112
|
+
const response = await hmacFetch(url, {
|
|
113
|
+
method: "POST",
|
|
114
|
+
body: body.buffer as ArrayBuffer,
|
|
115
|
+
headers: {
|
|
116
|
+
"Content-Type": contentType,
|
|
117
|
+
"Content-Length": String(body.byteLength),
|
|
118
|
+
"X-Codemation-Caller": "workflow-node",
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (!response.ok) {
|
|
123
|
+
const text = await response.text().catch(() => "(unreadable)");
|
|
124
|
+
throw new Error(
|
|
125
|
+
`Codemation Document Scanner: service responded ${response.status} ${response.statusText} — ${text}`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return (await response.json()) as DocScannerOutput;
|
|
130
|
+
},
|
|
131
|
+
});
|