@attestry/sdk 0.6.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/LICENSE +190 -0
- package/README.md +1269 -0
- package/dist/client.d.ts +58 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +74 -0
- package/dist/client.js.map +1 -0
- package/dist/constants.d.ts +7 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +43 -0
- package/dist/constants.js.map +1 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +41 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/lines-parser.d.ts +50 -0
- package/dist/lines-parser.d.ts.map +1 -0
- package/dist/lines-parser.js +211 -0
- package/dist/lines-parser.js.map +1 -0
- package/dist/ndjson-parser.d.ts +57 -0
- package/dist/ndjson-parser.d.ts.map +1 -0
- package/dist/ndjson-parser.js +245 -0
- package/dist/ndjson-parser.js.map +1 -0
- package/dist/resources/abac-policies.d.ts +1034 -0
- package/dist/resources/abac-policies.d.ts.map +1 -0
- package/dist/resources/abac-policies.js +1519 -0
- package/dist/resources/abac-policies.js.map +1 -0
- package/dist/resources/audit-log.d.ts +588 -0
- package/dist/resources/audit-log.d.ts.map +1 -0
- package/dist/resources/audit-log.js +629 -0
- package/dist/resources/audit-log.js.map +1 -0
- package/dist/resources/batch.d.ts +845 -0
- package/dist/resources/batch.d.ts.map +1 -0
- package/dist/resources/batch.js +1074 -0
- package/dist/resources/batch.js.map +1 -0
- package/dist/resources/chat.d.ts +151 -0
- package/dist/resources/chat.d.ts.map +1 -0
- package/dist/resources/chat.js +124 -0
- package/dist/resources/chat.js.map +1 -0
- package/dist/resources/check.d.ts +348 -0
- package/dist/resources/check.d.ts.map +1 -0
- package/dist/resources/check.js +543 -0
- package/dist/resources/check.js.map +1 -0
- package/dist/resources/compliance-check.d.ts +330 -0
- package/dist/resources/compliance-check.d.ts.map +1 -0
- package/dist/resources/compliance-check.js +402 -0
- package/dist/resources/compliance-check.js.map +1 -0
- package/dist/resources/decisions.d.ts +1208 -0
- package/dist/resources/decisions.d.ts.map +1 -0
- package/dist/resources/decisions.js +1362 -0
- package/dist/resources/decisions.js.map +1 -0
- package/dist/resources/evidence-pack.d.ts +1080 -0
- package/dist/resources/evidence-pack.d.ts.map +1 -0
- package/dist/resources/evidence-pack.js +1789 -0
- package/dist/resources/evidence-pack.js.map +1 -0
- package/dist/resources/gate.d.ts +613 -0
- package/dist/resources/gate.d.ts.map +1 -0
- package/dist/resources/gate.js +737 -0
- package/dist/resources/gate.js.map +1 -0
- package/dist/resources/incidents.d.ts +136 -0
- package/dist/resources/incidents.d.ts.map +1 -0
- package/dist/resources/incidents.js +229 -0
- package/dist/resources/incidents.js.map +1 -0
- package/dist/resources/regulatory-changes.d.ts +307 -0
- package/dist/resources/regulatory-changes.d.ts.map +1 -0
- package/dist/resources/regulatory-changes.js +365 -0
- package/dist/resources/regulatory-changes.js.map +1 -0
- package/dist/resources/safe-input-read.d.ts +21 -0
- package/dist/resources/safe-input-read.d.ts.map +1 -0
- package/dist/resources/safe-input-read.js +57 -0
- package/dist/resources/safe-input-read.js.map +1 -0
- package/dist/resources/ship-gate.d.ts +475 -0
- package/dist/resources/ship-gate.d.ts.map +1 -0
- package/dist/resources/ship-gate.js +727 -0
- package/dist/resources/ship-gate.js.map +1 -0
- package/dist/resources/vision.d.ts +540 -0
- package/dist/resources/vision.d.ts.map +1 -0
- package/dist/resources/vision.js +1036 -0
- package/dist/resources/vision.js.map +1 -0
- package/dist/retry.d.ts +103 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/retry.js +224 -0
- package/dist/retry.js.map +1 -0
- package/dist/sse-parser.d.ts +64 -0
- package/dist/sse-parser.d.ts.map +1 -0
- package/dist/sse-parser.js +271 -0
- package/dist/sse-parser.js.map +1 -0
- package/dist/transport.d.ts +142 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +455 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +61 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +44 -0
|
@@ -0,0 +1,1036 @@
|
|
|
1
|
+
// ─── Vision resource ────────────────────────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Wraps the KE2 P5 vision extraction surface (P5.1–P5.5):
|
|
4
|
+
//
|
|
5
|
+
// - POST /api/v1/vision/extract sync single-document extraction
|
|
6
|
+
// - POST /api/v1/vision/extract/batch async multi-document submission
|
|
7
|
+
// - GET /api/v1/vision/extract/jobs/:id poll an async job
|
|
8
|
+
//
|
|
9
|
+
// Ninth resource on `@attestry/sdk`. Sibling to `IncidentsResource`,
|
|
10
|
+
// `DecisionsResource`, `ChatResource`, `AuditLogResource`,
|
|
11
|
+
// `RegulatoryChangesResource`, `ComplianceCheckResource`, `CheckResource`,
|
|
12
|
+
// `GateResource`.
|
|
13
|
+
//
|
|
14
|
+
// Resource-class-per-kernel-resource convention (carry-forward invariant
|
|
15
|
+
// #43). Three methods today (`extract`, `extractBatch`, `getJobStatus`).
|
|
16
|
+
// All three are JSON request/response; the sync `extract` runs ~25s p50
|
|
17
|
+
// (Opus 4.7 vision tail) so the SDK does NOT lower the default 30s
|
|
18
|
+
// timeout — consumers extending it pass `{timeoutMs: 60_000}` via
|
|
19
|
+
// `RequestOptions` per call.
|
|
20
|
+
//
|
|
21
|
+
// **`mediaType` is REQUIRED** on both `extract` and `extractBatch` (P5.4
|
|
22
|
+
// DEV-10; REQ-04 spec-amendment open in COORDINATION_REQUESTS.md). The
|
|
23
|
+
// kernel route's Zod schema requires it; the SDK pre-validates against
|
|
24
|
+
// the local frozen `SUPPORTED_MEDIA_TYPES` tuple so a wrong value fails
|
|
25
|
+
// synchronously with a `TypeError` rather than a billed 422.
|
|
26
|
+
//
|
|
27
|
+
// **`base64` XOR `imageUri`** — exactly one of the two image-source
|
|
28
|
+
// fields must be supplied on each request (and each batch document).
|
|
29
|
+
// The kernel's `.refine` would 422 either way; the SDK pre-validates so
|
|
30
|
+
// the request fails BEFORE the network round-trip.
|
|
31
|
+
//
|
|
32
|
+
// **`packId` is the P5.5 evidence-pack wrap target** — optional UUID on
|
|
33
|
+
// `extract`. When supplied, the response carries an additive
|
|
34
|
+
// `packIntegration` field describing the wrap outcome. When omitted, the
|
|
35
|
+
// response is byte-identical to P5.4 (no `packIntegration` own-property).
|
|
36
|
+
// Drift-pinned in the spec-diff round.
|
|
37
|
+
//
|
|
38
|
+
// **Idempotency-Key header is NOT exposed** in P5.6 (carry-forward).
|
|
39
|
+
// The kernel accepts `Idempotency-Key` on `POST /extract/batch`; the
|
|
40
|
+
// SDK's `RequestOptions` does not currently surface extra headers for
|
|
41
|
+
// JSON POSTs (only `streamRequest` accepts a `headers` parameter).
|
|
42
|
+
// Adding it is a clean 5-line forward-compat — see the P5.6 audit doc
|
|
43
|
+
// carry-forward section. Consumers who need batch idempotency today
|
|
44
|
+
// should retry with their own client-side dedupe.
|
|
45
|
+
//
|
|
46
|
+
// **Symmetric prototype-pollution defense** — module-load snapshot of
|
|
47
|
+
// `Object.hasOwn` applied to BOTH input AND response sides (carry-
|
|
48
|
+
// forward of session-16 second-hostile-review MEDIUM #3 generalization;
|
|
49
|
+
// freshest implementation in `gate.ts`). Without the snapshot, a late-
|
|
50
|
+
// loading hostile/buggy npm dep that overrides the global (e.g.
|
|
51
|
+
// `Object.hasOwn = () => true`) would defeat the defense.
|
|
52
|
+
//
|
|
53
|
+
// **No URIError defense on body fields** — both POSTs use
|
|
54
|
+
// `JSON.stringify` (not `encodeURIComponent`), which handles lone UTF-16
|
|
55
|
+
// surrogates by emitting them as literal `\uDxxx` escapes. The URIError
|
|
56
|
+
// defect class (carry-forward invariant #32) applies only to query-
|
|
57
|
+
// string paths. `getJobStatus`'s path segment IS encoded via
|
|
58
|
+
// `encodeURIComponent`, but the SDK pre-validates `jobId` as a hyphen-
|
|
59
|
+
// only RFC 4122 UUID first, so a malformed input rejects with
|
|
60
|
+
// `TypeError` BEFORE the encoder runs.
|
|
61
|
+
//
|
|
62
|
+
// **P3 content-type guard** — already present in the SDK transport
|
|
63
|
+
// (`packages/attestry-sdk/src/transport.ts:271-291` + `readBody` at
|
|
64
|
+
// `:543-561`). A non-JSON 200 response (LB HTML error page, plain-text
|
|
65
|
+
// proxy body) surfaces as `AttestryAPIError`, NOT an opaque
|
|
66
|
+
// `SyntaxError`. P5.6 confirms this; HR-4(b) carry-forward applies to
|
|
67
|
+
// `mcp-server/src/client.ts`, NOT to the SDK transport.
|
|
68
|
+
import { AttestryError } from "../errors.js";
|
|
69
|
+
import { readInputField } from "./safe-input-read.js";
|
|
70
|
+
// Module-load snapshot of `Object.hasOwn`. Mirror of `gate.ts`'s
|
|
71
|
+
// pattern. Used symmetrically on input AND response sides — defense on
|
|
72
|
+
// both boundaries.
|
|
73
|
+
const objectHasOwn = Object.hasOwn;
|
|
74
|
+
// RFC 4122 hyphenated UUID (8-4-4-4-12 hex, case-insensitive). Matches
|
|
75
|
+
// Zod's `z.string().uuid()` regex effectively. Mirror of `gate.ts`'s
|
|
76
|
+
// `UUID_REGEX`; drift-pinned in `sdk-drift.test.ts` (Round 2) so a
|
|
77
|
+
// kernel-side switch to a different UUID flavor (ULID, KSUID) fires
|
|
78
|
+
// before consumer regressions.
|
|
79
|
+
const UUID_REGEX = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
80
|
+
/**
|
|
81
|
+
* Maximum length of an `imageUri` string (kernel `z.string().url().max(2048)`).
|
|
82
|
+
* Mirrored locally as the closed-spec rule the SDK pre-validates.
|
|
83
|
+
*/
|
|
84
|
+
const MAX_IMAGE_URI_LENGTH = 2048;
|
|
85
|
+
/**
|
|
86
|
+
* Maximum length of a base64-encoded image (kernel `ANTHROPIC_IMAGE_MAX_BASE64`
|
|
87
|
+
* in `src/lib/vision/types.ts` — `ceil((5 * 1024 * 1024 * 4) / 3) = 6_990_507`).
|
|
88
|
+
* The kernel rejects oversize with 422; the SDK fails synchronously to save the
|
|
89
|
+
* billed network round-trip. Drift-pinned in `sdk-drift.test.ts` so a kernel-
|
|
90
|
+
* side adjustment surfaces before consumer regressions.
|
|
91
|
+
*/
|
|
92
|
+
const ANTHROPIC_IMAGE_MAX_BASE64 = 6_990_507;
|
|
93
|
+
// ─── Closed-enum frozen tuples (drift-pinned in Round 2) ────────────────────
|
|
94
|
+
/**
|
|
95
|
+
* The four image MIME types the kernel accepts via `mediaType`. Mirrors
|
|
96
|
+
* `src/lib/vision/types.ts:23-28` (kernel side). Frozen so consumer code can
|
|
97
|
+
* safely use `SUPPORTED_MEDIA_TYPES.includes(...)` without mutation risk.
|
|
98
|
+
*
|
|
99
|
+
* Drift-pinned in the spec-diff round (`sdk-drift.test.ts`) by text-comparing
|
|
100
|
+
* this declaration with the kernel's. An addition/removal on either side trips
|
|
101
|
+
* the test.
|
|
102
|
+
*/
|
|
103
|
+
export const SUPPORTED_MEDIA_TYPES = Object.freeze([
|
|
104
|
+
"image/jpeg",
|
|
105
|
+
"image/png",
|
|
106
|
+
"image/webp",
|
|
107
|
+
"image/gif",
|
|
108
|
+
]);
|
|
109
|
+
/**
|
|
110
|
+
* The five document-type keys registered in the kernel schema library. Mirrors
|
|
111
|
+
* `src/lib/vision/schemas/index.ts:140-146` (kernel side). Frozen; drift-
|
|
112
|
+
* pinned identically to `SUPPORTED_MEDIA_TYPES`.
|
|
113
|
+
*
|
|
114
|
+
* Schema additions (e.g. a new Annex IV section schema) bump the kernel tuple
|
|
115
|
+
* AND require a new `chore(sdk): bump` to keep the SDK in sync — the drift pin
|
|
116
|
+
* is the trip-wire.
|
|
117
|
+
*/
|
|
118
|
+
export const SUPPORTED_DOCUMENT_TYPES = Object.freeze([
|
|
119
|
+
"model-card",
|
|
120
|
+
"validation-report",
|
|
121
|
+
"certification-label",
|
|
122
|
+
"schematic-extraction",
|
|
123
|
+
"generic-tabular",
|
|
124
|
+
]);
|
|
125
|
+
/**
|
|
126
|
+
* Closed enum for the optional `model` field. Mirrors kernel
|
|
127
|
+
* `z.enum(["opus", "sonnet"])` in `src/app/api/v1/vision/extract/route.ts`.
|
|
128
|
+
*/
|
|
129
|
+
export const VISION_MODELS = Object.freeze(["opus", "sonnet"]);
|
|
130
|
+
/**
|
|
131
|
+
* `packIntegration.status` closed enum. Mirrors `PackIntegrationResult.status`
|
|
132
|
+
* in kernel `src/lib/vision/pack-integration.ts:315`.
|
|
133
|
+
*
|
|
134
|
+
* Typed as a closed union at compile time; runtime check is `typeof ===
|
|
135
|
+
* "string"` only (faithful courier — same convention as
|
|
136
|
+
* `BulkFailedSummary.code` / `DecisionStreamEvent.eventType`). A future
|
|
137
|
+
* kernel-side `"partial"` would round-trip via the type system; consumers
|
|
138
|
+
* doing exhaustive narrowing should update the SDK type then.
|
|
139
|
+
*/
|
|
140
|
+
export const PACK_INTEGRATION_STATUSES = Object.freeze([
|
|
141
|
+
"wrapped",
|
|
142
|
+
"failed",
|
|
143
|
+
]);
|
|
144
|
+
// ─── Resource class ─────────────────────────────────────────────────────────
|
|
145
|
+
/**
|
|
146
|
+
* `vision` resource — sibling to `IncidentsResource`, `DecisionsResource`,
|
|
147
|
+
* `ChatResource`, `AuditLogResource`, `RegulatoryChangesResource`,
|
|
148
|
+
* `ComplianceCheckResource`, `CheckResource`, `GateResource`.
|
|
149
|
+
*
|
|
150
|
+
* Three methods today (`extract`, `extractBatch`, `getJobStatus`). All three
|
|
151
|
+
* are JSON request/response (no SSE / NDJSON). The class is also the landing
|
|
152
|
+
* pad for future vision methods (e.g. a `listSupportedSchemas` once a kernel
|
|
153
|
+
* route surfaces it — currently exposed only as an MCP tool).
|
|
154
|
+
*/
|
|
155
|
+
export class VisionResource {
|
|
156
|
+
client;
|
|
157
|
+
constructor(client) {
|
|
158
|
+
this.client = client;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Synchronously extract structured data from a single regulatory document
|
|
162
|
+
* image. Wraps `POST /api/v1/vision/extract`.
|
|
163
|
+
*
|
|
164
|
+
* **Latency**: ~25.5s p50 with Opus 4.7 (`KE2-P5-VISION.md` §"Empirical
|
|
165
|
+
* findings"); tail latency approaches the kernel `maxDuration: 60s`. The
|
|
166
|
+
* SDK does NOT lower the default `timeoutMs: 30_000` for this method
|
|
167
|
+
* (resource methods do not override per-method timeouts in `@attestry/sdk`).
|
|
168
|
+
* Latency-sensitive consumers MAY raise it via `{timeoutMs: 60_000}` per
|
|
169
|
+
* call.
|
|
170
|
+
*
|
|
171
|
+
* **Cost**: ~$0.22 per Opus 4.7 call; Sonnet 4 is ~5–6× cheaper. The cost
|
|
172
|
+
* is returned in `response.costUsdCents` (integer).
|
|
173
|
+
*
|
|
174
|
+
* **`mediaType` is REQUIRED** (P5.4 DEV-10). The SDK rejects requests
|
|
175
|
+
* without it synchronously (`TypeError`), saving the network round-trip
|
|
176
|
+
* the kernel would otherwise 422.
|
|
177
|
+
*
|
|
178
|
+
* **`packId` is the P5.5 evidence-pack wrap target** — optional UUID.
|
|
179
|
+
* When supplied, the response carries `packIntegration` describing the
|
|
180
|
+
* wrap outcome:
|
|
181
|
+
* - success → `{status: "wrapped", packId, bundleId, packContentHash,
|
|
182
|
+
* inputsHash, outputsHash, hashCollision?, schemaCompatibility}`
|
|
183
|
+
* - post-extraction wrap failure (race / transient DB fault) →
|
|
184
|
+
* `{status: "failed", packId, schemaCompatibility, error}` —
|
|
185
|
+
* the extraction itself succeeded and is returned in the same response.
|
|
186
|
+
* - caller-error wrap target (unknown / cross-org / non-draft pack) →
|
|
187
|
+
* fails as `AttestryAPIError` BEFORE the billed extraction (the
|
|
188
|
+
* kernel's pre-flight catches these and returns 4xx; nothing is
|
|
189
|
+
* billed). The SDK does NOT pre-validate pack state; the kernel is
|
|
190
|
+
* the authority.
|
|
191
|
+
*
|
|
192
|
+
* Errors — ordered by kernel firing precedence (rate-limit → auth → body
|
|
193
|
+
* parse → pre-flight pack → vision extraction → wrap). A request with
|
|
194
|
+
* multiple problems surfaces ONLY the highest-precedence one.
|
|
195
|
+
*
|
|
196
|
+
* - `AttestryAPIError` (status 429) — rate limit FIRES FIRST (auto-
|
|
197
|
+
* retried by default — invariant #18; per-IP rate-limit key
|
|
198
|
+
* `vision-extract:${ip}`).
|
|
199
|
+
* - `AttestryAPIError` (status 401) — no API key OR invalid key.
|
|
200
|
+
* - `AttestryAPIError` (status 403) — authenticated key lacks
|
|
201
|
+
* `WRITE_ASSESSMENTS` permission.
|
|
202
|
+
* - `AttestryAPIError` (status 400) — JSON parse failure on the body
|
|
203
|
+
* OR a kernel-side vision-validation rejection (`vision.<code>`).
|
|
204
|
+
* - `AttestryAPIError` (status 422) — Zod validation failed (`details.code`
|
|
205
|
+
* === `"vision.validation_failed"`; `details.issues` carries the
|
|
206
|
+
* field paths).
|
|
207
|
+
* - `AttestryAPIError` (status 404) — `packId` does not exist OR
|
|
208
|
+
* belongs to another org (anti-enumeration collapse; per
|
|
209
|
+
* `mapEvidencePackError`).
|
|
210
|
+
* - `AttestryAPIError` (status 409) — `packId` is not in `draft`
|
|
211
|
+
* state (signed / superseded / revoked / expired).
|
|
212
|
+
* - `AttestryAPIError` (status 502) — upstream Anthropic / DNS /
|
|
213
|
+
* gateway fault (`vision.<code>`).
|
|
214
|
+
* - `AttestryAPIError` (status 503) — extraction completed but the
|
|
215
|
+
* call row failed to persist (`vision.persist_failed`).
|
|
216
|
+
* - `AttestryAPIError` (status 500) — internal kernel error.
|
|
217
|
+
* - `AttestryError` ("request aborted by caller") — caller-supplied
|
|
218
|
+
* `options.signal` fired (pre-aborted or mid-flight).
|
|
219
|
+
* - `AttestryError` (P2 hardening) — kernel response failed SDK-side
|
|
220
|
+
* shape validation (not an object, wrong type on any field).
|
|
221
|
+
* - `AttestryAPIError` (P3 hardening) — kernel response had a wrong
|
|
222
|
+
* `Content-Type` (transport-level guard at `transport.ts:271-291`,
|
|
223
|
+
* before body parsing).
|
|
224
|
+
* - `TypeError` (synchronous, no fetch issued) — input failed SDK-side
|
|
225
|
+
* validation (null / array / non-object input; missing or bad
|
|
226
|
+
* `mediaType` / `documentType`; bad `base64` / `imageUri` XOR;
|
|
227
|
+
* oversized `imageUri` / `base64`; bad `model` / `extractionSchema`
|
|
228
|
+
* enum; bad `packId` UUID).
|
|
229
|
+
*
|
|
230
|
+
* **SDK-side validation** (synchronous `TypeError`, no fetch issued):
|
|
231
|
+
* - `input` itself: required; must be a non-null, non-array object.
|
|
232
|
+
* - `input.mediaType`: required own-property; must be a member of
|
|
233
|
+
* `SUPPORTED_MEDIA_TYPES`.
|
|
234
|
+
* - `input.documentType`: required own-property; must be a member of
|
|
235
|
+
* `SUPPORTED_DOCUMENT_TYPES`.
|
|
236
|
+
* - `input.base64` XOR `input.imageUri`: exactly one own-property must
|
|
237
|
+
* be present with a non-empty string value.
|
|
238
|
+
* - `input.base64` (when present): non-empty string, length ≤
|
|
239
|
+
* ANTHROPIC_IMAGE_MAX_BASE64.
|
|
240
|
+
* - `input.imageUri` (when present): non-empty string, length ≤
|
|
241
|
+
* 2048.
|
|
242
|
+
* - `input.extractionSchema` (when own-present, value not undefined):
|
|
243
|
+
* must be a member of `SUPPORTED_DOCUMENT_TYPES`.
|
|
244
|
+
* - `input.model` (when own-present, value not undefined): must be a
|
|
245
|
+
* member of `VISION_MODELS`.
|
|
246
|
+
* - `input.packId` (when own-present, value not undefined): must be a
|
|
247
|
+
* non-empty string matching `UUID_REGEX`.
|
|
248
|
+
*
|
|
249
|
+
* **Response-shape validation** (P2 hardening; symmetric to input-side
|
|
250
|
+
* prototype-pollution defense): every documented response field is
|
|
251
|
+
* type-checked via the `objectHasOwn` snapshot. A hostile npm dep that
|
|
252
|
+
* pollutes `Object.prototype.<field>` cannot mask a kernel regression
|
|
253
|
+
* where the field is missing.
|
|
254
|
+
*
|
|
255
|
+
* @example Basic single-image extraction
|
|
256
|
+
* ```ts
|
|
257
|
+
* const result = await client.vision.extract({
|
|
258
|
+
* base64: "iVBORw0KGgoAAAANSUhEUgAA...",
|
|
259
|
+
* mediaType: "image/png",
|
|
260
|
+
* documentType: "model-card",
|
|
261
|
+
* });
|
|
262
|
+
* console.log(result.structuredExtraction);
|
|
263
|
+
* console.log(`cost: ${result.costUsdCents / 100} USD`);
|
|
264
|
+
* ```
|
|
265
|
+
*
|
|
266
|
+
* @example Extraction wrapped into an evidence pack (P5.5)
|
|
267
|
+
* ```ts
|
|
268
|
+
* const result = await client.vision.extract({
|
|
269
|
+
* imageUri: "https://example.com/cert.png",
|
|
270
|
+
* mediaType: "image/png",
|
|
271
|
+
* documentType: "certification-label",
|
|
272
|
+
* model: "sonnet", // cheaper tier for high-volume
|
|
273
|
+
* packId: "11111111-1111-1111-1111-111111111111",
|
|
274
|
+
* });
|
|
275
|
+
* if (result.packIntegration?.status === "wrapped") {
|
|
276
|
+
* console.log("bundle:", result.packIntegration.bundleId);
|
|
277
|
+
* }
|
|
278
|
+
* ```
|
|
279
|
+
*/
|
|
280
|
+
extract(input, options) {
|
|
281
|
+
// Top-level shape — input is REQUIRED. typeof null === "object" and
|
|
282
|
+
// typeof [] === "object", so guard both explicitly.
|
|
283
|
+
if (input === null ||
|
|
284
|
+
typeof input !== "object" ||
|
|
285
|
+
Array.isArray(input)) {
|
|
286
|
+
throw new TypeError("vision.extract: `input` must be a non-null object");
|
|
287
|
+
}
|
|
288
|
+
// Snapshot each field's value EXACTLY ONCE up front via the own-property
|
|
289
|
+
// indexer. Three motivations (same as `gate.ts`):
|
|
290
|
+
// 1. Prototype-pollution defense (generalization of invariant #48): a
|
|
291
|
+
// late-set `Object.prototype.documentType` cannot trick the SDK
|
|
292
|
+
// into silently sending a polluted value when the caller passes
|
|
293
|
+
// `{}`. The module-load `objectHasOwn` snapshot defends against a
|
|
294
|
+
// late-loading hostile dep too.
|
|
295
|
+
// 2. TOCTOU defense: a Proxy / getter-defining input could yield
|
|
296
|
+
// different values across multiple reads. Snapshotting once
|
|
297
|
+
// collapses validate-then-send to a single read per field.
|
|
298
|
+
// 3. Explicit `{}` (no other fields) is treated as those-fields-omitted
|
|
299
|
+
// — `objectHasOwn` correctly returns false on missing keys.
|
|
300
|
+
// 4. Throwing-getter defense — each read goes through
|
|
301
|
+
// `readInputField`, converting a throwing accessor's exception
|
|
302
|
+
// into the documented synchronous `TypeError` input contract
|
|
303
|
+
// (session-22 hostile MEDIUM-1).
|
|
304
|
+
const hasBase64 = objectHasOwn(input, "base64");
|
|
305
|
+
const base64Raw = hasBase64
|
|
306
|
+
? readInputField(input, "base64", "vision.extract")
|
|
307
|
+
: undefined;
|
|
308
|
+
const hasImageUri = objectHasOwn(input, "imageUri");
|
|
309
|
+
const imageUriRaw = hasImageUri
|
|
310
|
+
? readInputField(input, "imageUri", "vision.extract")
|
|
311
|
+
: undefined;
|
|
312
|
+
const hasMediaType = objectHasOwn(input, "mediaType");
|
|
313
|
+
const mediaTypeRaw = hasMediaType
|
|
314
|
+
? readInputField(input, "mediaType", "vision.extract")
|
|
315
|
+
: undefined;
|
|
316
|
+
const hasDocumentType = objectHasOwn(input, "documentType");
|
|
317
|
+
const documentTypeRaw = hasDocumentType
|
|
318
|
+
? readInputField(input, "documentType", "vision.extract")
|
|
319
|
+
: undefined;
|
|
320
|
+
const hasExtractionSchema = objectHasOwn(input, "extractionSchema");
|
|
321
|
+
const extractionSchemaRaw = hasExtractionSchema
|
|
322
|
+
? readInputField(input, "extractionSchema", "vision.extract")
|
|
323
|
+
: undefined;
|
|
324
|
+
const hasModel = objectHasOwn(input, "model");
|
|
325
|
+
const modelRaw = hasModel
|
|
326
|
+
? readInputField(input, "model", "vision.extract")
|
|
327
|
+
: undefined;
|
|
328
|
+
const hasPackId = objectHasOwn(input, "packId");
|
|
329
|
+
const packIdRaw = hasPackId
|
|
330
|
+
? readInputField(input, "packId", "vision.extract")
|
|
331
|
+
: undefined;
|
|
332
|
+
// mediaType REQUIRED + closed-enum membership.
|
|
333
|
+
if (!hasMediaType || mediaTypeRaw === undefined) {
|
|
334
|
+
throw new TypeError("vision.extract: `mediaType` is required");
|
|
335
|
+
}
|
|
336
|
+
if (typeof mediaTypeRaw !== "string") {
|
|
337
|
+
throw new TypeError(`vision.extract: \`mediaType\` must be a string ` +
|
|
338
|
+
`(got ${describeType(mediaTypeRaw)})`);
|
|
339
|
+
}
|
|
340
|
+
if (!SUPPORTED_MEDIA_TYPES.includes(mediaTypeRaw)) {
|
|
341
|
+
throw new TypeError(`vision.extract: \`mediaType\` must be one of ` +
|
|
342
|
+
`${JSON.stringify(SUPPORTED_MEDIA_TYPES)} (got ` +
|
|
343
|
+
`${JSON.stringify(mediaTypeRaw)})`);
|
|
344
|
+
}
|
|
345
|
+
const validatedMediaType = mediaTypeRaw;
|
|
346
|
+
// documentType REQUIRED + closed-enum membership.
|
|
347
|
+
if (!hasDocumentType || documentTypeRaw === undefined) {
|
|
348
|
+
throw new TypeError("vision.extract: `documentType` is required");
|
|
349
|
+
}
|
|
350
|
+
if (typeof documentTypeRaw !== "string") {
|
|
351
|
+
throw new TypeError(`vision.extract: \`documentType\` must be a string ` +
|
|
352
|
+
`(got ${describeType(documentTypeRaw)})`);
|
|
353
|
+
}
|
|
354
|
+
if (!SUPPORTED_DOCUMENT_TYPES.includes(documentTypeRaw)) {
|
|
355
|
+
throw new TypeError(`vision.extract: \`documentType\` must be one of ` +
|
|
356
|
+
`${JSON.stringify(SUPPORTED_DOCUMENT_TYPES)} (got ` +
|
|
357
|
+
`${JSON.stringify(documentTypeRaw)})`);
|
|
358
|
+
}
|
|
359
|
+
const validatedDocumentType = documentTypeRaw;
|
|
360
|
+
// base64 XOR imageUri — exactly one own-present + non-empty string.
|
|
361
|
+
// Empty strings reject in the per-field branches below; the XOR test
|
|
362
|
+
// operates on "has-and-non-undefined" so {base64: undefined, imageUri:
|
|
363
|
+
// "..."} is still treated as imageUri-only.
|
|
364
|
+
const presentBase64 = hasBase64 && base64Raw !== undefined;
|
|
365
|
+
const presentImageUri = hasImageUri && imageUriRaw !== undefined;
|
|
366
|
+
if (presentBase64 && presentImageUri) {
|
|
367
|
+
throw new TypeError("vision.extract: `base64` and `imageUri` are mutually exclusive — " +
|
|
368
|
+
"supply exactly one");
|
|
369
|
+
}
|
|
370
|
+
if (!presentBase64 && !presentImageUri) {
|
|
371
|
+
throw new TypeError("vision.extract: exactly one of `base64` or `imageUri` is required");
|
|
372
|
+
}
|
|
373
|
+
let validatedBase64;
|
|
374
|
+
if (presentBase64) {
|
|
375
|
+
if (typeof base64Raw !== "string") {
|
|
376
|
+
throw new TypeError(`vision.extract: \`base64\` must be a string ` +
|
|
377
|
+
`(got ${describeType(base64Raw)})`);
|
|
378
|
+
}
|
|
379
|
+
if (base64Raw.length === 0) {
|
|
380
|
+
throw new TypeError("vision.extract: `base64` must be a non-empty string");
|
|
381
|
+
}
|
|
382
|
+
if (base64Raw.length > ANTHROPIC_IMAGE_MAX_BASE64) {
|
|
383
|
+
throw new TypeError(`vision.extract: \`base64\` exceeds the maximum length of ` +
|
|
384
|
+
`${ANTHROPIC_IMAGE_MAX_BASE64} characters (got ${base64Raw.length})`);
|
|
385
|
+
}
|
|
386
|
+
validatedBase64 = base64Raw;
|
|
387
|
+
}
|
|
388
|
+
let validatedImageUri;
|
|
389
|
+
if (presentImageUri) {
|
|
390
|
+
if (typeof imageUriRaw !== "string") {
|
|
391
|
+
throw new TypeError(`vision.extract: \`imageUri\` must be a string ` +
|
|
392
|
+
`(got ${describeType(imageUriRaw)})`);
|
|
393
|
+
}
|
|
394
|
+
if (imageUriRaw.length === 0) {
|
|
395
|
+
throw new TypeError("vision.extract: `imageUri` must be a non-empty string");
|
|
396
|
+
}
|
|
397
|
+
if (imageUriRaw.length > MAX_IMAGE_URI_LENGTH) {
|
|
398
|
+
throw new TypeError(`vision.extract: \`imageUri\` exceeds the maximum length of ` +
|
|
399
|
+
`${MAX_IMAGE_URI_LENGTH} characters (got ${imageUriRaw.length})`);
|
|
400
|
+
}
|
|
401
|
+
validatedImageUri = imageUriRaw;
|
|
402
|
+
}
|
|
403
|
+
// Optional extractionSchema — closed-enum membership when own-present.
|
|
404
|
+
let validatedExtractionSchema;
|
|
405
|
+
if (hasExtractionSchema && extractionSchemaRaw !== undefined) {
|
|
406
|
+
if (typeof extractionSchemaRaw !== "string") {
|
|
407
|
+
throw new TypeError(`vision.extract: \`extractionSchema\` must be a string when provided ` +
|
|
408
|
+
`(got ${describeType(extractionSchemaRaw)})`);
|
|
409
|
+
}
|
|
410
|
+
if (!SUPPORTED_DOCUMENT_TYPES.includes(extractionSchemaRaw)) {
|
|
411
|
+
throw new TypeError(`vision.extract: \`extractionSchema\` must be one of ` +
|
|
412
|
+
`${JSON.stringify(SUPPORTED_DOCUMENT_TYPES)} (got ` +
|
|
413
|
+
`${JSON.stringify(extractionSchemaRaw)})`);
|
|
414
|
+
}
|
|
415
|
+
validatedExtractionSchema =
|
|
416
|
+
extractionSchemaRaw;
|
|
417
|
+
}
|
|
418
|
+
// Optional model — closed-enum membership when own-present.
|
|
419
|
+
let validatedModel;
|
|
420
|
+
if (hasModel && modelRaw !== undefined) {
|
|
421
|
+
if (typeof modelRaw !== "string") {
|
|
422
|
+
throw new TypeError(`vision.extract: \`model\` must be a string when provided ` +
|
|
423
|
+
`(got ${describeType(modelRaw)})`);
|
|
424
|
+
}
|
|
425
|
+
if (!VISION_MODELS.includes(modelRaw)) {
|
|
426
|
+
throw new TypeError(`vision.extract: \`model\` must be one of ` +
|
|
427
|
+
`${JSON.stringify(VISION_MODELS)} (got ` +
|
|
428
|
+
`${JSON.stringify(modelRaw)})`);
|
|
429
|
+
}
|
|
430
|
+
validatedModel = modelRaw;
|
|
431
|
+
}
|
|
432
|
+
// Optional packId — UUID pre-validation when own-present.
|
|
433
|
+
let validatedPackId;
|
|
434
|
+
if (hasPackId && packIdRaw !== undefined) {
|
|
435
|
+
if (typeof packIdRaw !== "string") {
|
|
436
|
+
throw new TypeError(`vision.extract: \`packId\` must be a string when provided ` +
|
|
437
|
+
`(got ${describeType(packIdRaw)})`);
|
|
438
|
+
}
|
|
439
|
+
if (packIdRaw.length === 0) {
|
|
440
|
+
throw new TypeError("vision.extract: `packId` must be a non-empty string when provided");
|
|
441
|
+
}
|
|
442
|
+
if (!UUID_REGEX.test(packIdRaw)) {
|
|
443
|
+
throw new TypeError("vision.extract: `packId` must be an RFC 4122 hyphenated UUID " +
|
|
444
|
+
"(matched regex: /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-" +
|
|
445
|
+
"[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/)");
|
|
446
|
+
}
|
|
447
|
+
validatedPackId = packIdRaw;
|
|
448
|
+
}
|
|
449
|
+
// Construct the body. Omit any optional field the consumer omitted —
|
|
450
|
+
// the kernel `z.optional()` is the schema authority on the kernel side.
|
|
451
|
+
const body = {
|
|
452
|
+
mediaType: validatedMediaType,
|
|
453
|
+
documentType: validatedDocumentType,
|
|
454
|
+
};
|
|
455
|
+
if (validatedBase64 !== undefined)
|
|
456
|
+
body.base64 = validatedBase64;
|
|
457
|
+
if (validatedImageUri !== undefined)
|
|
458
|
+
body.imageUri = validatedImageUri;
|
|
459
|
+
if (validatedExtractionSchema !== undefined) {
|
|
460
|
+
body.extractionSchema = validatedExtractionSchema;
|
|
461
|
+
}
|
|
462
|
+
if (validatedModel !== undefined)
|
|
463
|
+
body.model = validatedModel;
|
|
464
|
+
if (validatedPackId !== undefined)
|
|
465
|
+
body.packId = validatedPackId;
|
|
466
|
+
return this.client
|
|
467
|
+
._request({
|
|
468
|
+
method: "POST",
|
|
469
|
+
path: "/api/v1/vision/extract",
|
|
470
|
+
body,
|
|
471
|
+
options,
|
|
472
|
+
})
|
|
473
|
+
.then((result) => {
|
|
474
|
+
// P2 hardening: validate every documented field type. Symmetric
|
|
475
|
+
// prototype-pollution defense — read each field via `objectHasOwn`
|
|
476
|
+
// so a hostile npm dep polluting `Object.prototype.<field>` cannot
|
|
477
|
+
// mask a kernel regression that drops the field.
|
|
478
|
+
if (result === null ||
|
|
479
|
+
typeof result !== "object" ||
|
|
480
|
+
Array.isArray(result)) {
|
|
481
|
+
throw new AttestryError(`vision.extract: expected an object response from the kernel ` +
|
|
482
|
+
`(got ${describeType(result)})`);
|
|
483
|
+
}
|
|
484
|
+
const obj = result;
|
|
485
|
+
const callId = objectHasOwn(obj, "callId") ? obj.callId : undefined;
|
|
486
|
+
if (typeof callId !== "string") {
|
|
487
|
+
throw new AttestryError(`vision.extract: expected response.callId to be a string ` +
|
|
488
|
+
`(got ${describeType(callId)})`);
|
|
489
|
+
}
|
|
490
|
+
// structuredExtraction: object | null (parse_failed = null).
|
|
491
|
+
const structuredExtraction = objectHasOwn(obj, "structuredExtraction")
|
|
492
|
+
? obj.structuredExtraction
|
|
493
|
+
: undefined;
|
|
494
|
+
if (structuredExtraction !== null &&
|
|
495
|
+
(typeof structuredExtraction !== "object" ||
|
|
496
|
+
Array.isArray(structuredExtraction))) {
|
|
497
|
+
throw new AttestryError(`vision.extract: expected response.structuredExtraction to be ` +
|
|
498
|
+
`an object or null (got ` +
|
|
499
|
+
`${describeType(structuredExtraction)})`);
|
|
500
|
+
}
|
|
501
|
+
const confidencePerField = objectHasOwn(obj, "confidencePerField")
|
|
502
|
+
? obj.confidencePerField
|
|
503
|
+
: undefined;
|
|
504
|
+
if (confidencePerField === null ||
|
|
505
|
+
typeof confidencePerField !== "object" ||
|
|
506
|
+
Array.isArray(confidencePerField)) {
|
|
507
|
+
throw new AttestryError(`vision.extract: expected response.confidencePerField to be ` +
|
|
508
|
+
`an object (got ${describeType(confidencePerField)})`);
|
|
509
|
+
}
|
|
510
|
+
const sourceRegions = objectHasOwn(obj, "sourceRegions")
|
|
511
|
+
? obj.sourceRegions
|
|
512
|
+
: undefined;
|
|
513
|
+
if (sourceRegions === null ||
|
|
514
|
+
typeof sourceRegions !== "object" ||
|
|
515
|
+
Array.isArray(sourceRegions)) {
|
|
516
|
+
throw new AttestryError(`vision.extract: expected response.sourceRegions to be ` +
|
|
517
|
+
`an object (got ${describeType(sourceRegions)})`);
|
|
518
|
+
}
|
|
519
|
+
const tokensUsed = objectHasOwn(obj, "tokensUsed")
|
|
520
|
+
? obj.tokensUsed
|
|
521
|
+
: undefined;
|
|
522
|
+
if (tokensUsed === null ||
|
|
523
|
+
typeof tokensUsed !== "object" ||
|
|
524
|
+
Array.isArray(tokensUsed)) {
|
|
525
|
+
throw new AttestryError(`vision.extract: expected response.tokensUsed to be ` +
|
|
526
|
+
`an object (got ${describeType(tokensUsed)})`);
|
|
527
|
+
}
|
|
528
|
+
// `tokensUsed` is a FIXED, always-present, closed 4-number shape
|
|
529
|
+
// (kernel `TokensUsed` in src/lib/vision/types.ts:50-55 — input,
|
|
530
|
+
// output, cacheCreation, cacheRead all `number`). Unlike the
|
|
531
|
+
// optional / conditional / open-spec `packIntegration` sub-fields
|
|
532
|
+
// (faithful courier) and the open-spec `confidencePerField` /
|
|
533
|
+
// `sourceRegions` map VALUES, this shape is fully determined, so
|
|
534
|
+
// the P2 validator enforces each inner field is a number — a kernel
|
|
535
|
+
// regression that mistyped one (e.g. `input: "lots"`) would
|
|
536
|
+
// otherwise round-trip a string typed as `number` to the consumer.
|
|
537
|
+
// (Founder hostile-review F3.) Per-field own-property reads via the
|
|
538
|
+
// module-load `objectHasOwn` snapshot — symmetric with the rest of
|
|
539
|
+
// the response-side prototype-pollution defense.
|
|
540
|
+
const tokensUsedObj = tokensUsed;
|
|
541
|
+
for (const tf of [
|
|
542
|
+
"input",
|
|
543
|
+
"output",
|
|
544
|
+
"cacheCreation",
|
|
545
|
+
"cacheRead",
|
|
546
|
+
]) {
|
|
547
|
+
const tv = objectHasOwn(tokensUsedObj, tf)
|
|
548
|
+
? tokensUsedObj[tf]
|
|
549
|
+
: undefined;
|
|
550
|
+
if (typeof tv !== "number") {
|
|
551
|
+
throw new AttestryError(`vision.extract: expected response.tokensUsed.${tf} to be a ` +
|
|
552
|
+
`number (got ${describeType(tv)})`);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
const costUsdCents = objectHasOwn(obj, "costUsdCents")
|
|
556
|
+
? obj.costUsdCents
|
|
557
|
+
: undefined;
|
|
558
|
+
if (typeof costUsdCents !== "number") {
|
|
559
|
+
throw new AttestryError(`vision.extract: expected response.costUsdCents to be a number ` +
|
|
560
|
+
`(got ${describeType(costUsdCents)})`);
|
|
561
|
+
}
|
|
562
|
+
const latencyMs = objectHasOwn(obj, "latencyMs")
|
|
563
|
+
? obj.latencyMs
|
|
564
|
+
: undefined;
|
|
565
|
+
if (typeof latencyMs !== "number") {
|
|
566
|
+
throw new AttestryError(`vision.extract: expected response.latencyMs to be a number ` +
|
|
567
|
+
`(got ${describeType(latencyMs)})`);
|
|
568
|
+
}
|
|
569
|
+
// packIntegration — present ONLY when the request supplied packId.
|
|
570
|
+
// The no-packId response has NO own-property here.
|
|
571
|
+
if (objectHasOwn(obj, "packIntegration")) {
|
|
572
|
+
const pi = obj.packIntegration;
|
|
573
|
+
if (pi === null || typeof pi !== "object" || Array.isArray(pi)) {
|
|
574
|
+
throw new AttestryError(`vision.extract: expected response.packIntegration to be ` +
|
|
575
|
+
`an object when present (got ${describeType(pi)})`);
|
|
576
|
+
}
|
|
577
|
+
const piObj = pi;
|
|
578
|
+
const piStatus = objectHasOwn(piObj, "status")
|
|
579
|
+
? piObj.status
|
|
580
|
+
: undefined;
|
|
581
|
+
if (typeof piStatus !== "string") {
|
|
582
|
+
throw new AttestryError(`vision.extract: expected response.packIntegration.status ` +
|
|
583
|
+
`to be a string (got ${describeType(piStatus)})`);
|
|
584
|
+
}
|
|
585
|
+
const piPackId = objectHasOwn(piObj, "packId")
|
|
586
|
+
? piObj.packId
|
|
587
|
+
: undefined;
|
|
588
|
+
if (typeof piPackId !== "string") {
|
|
589
|
+
throw new AttestryError(`vision.extract: expected response.packIntegration.packId ` +
|
|
590
|
+
`to be a string (got ${describeType(piPackId)})`);
|
|
591
|
+
}
|
|
592
|
+
const piSchemaCompat = objectHasOwn(piObj, "schemaCompatibility")
|
|
593
|
+
? piObj.schemaCompatibility
|
|
594
|
+
: undefined;
|
|
595
|
+
if (piSchemaCompat === null ||
|
|
596
|
+
typeof piSchemaCompat !== "object" ||
|
|
597
|
+
Array.isArray(piSchemaCompat)) {
|
|
598
|
+
throw new AttestryError(`vision.extract: expected response.packIntegration.` +
|
|
599
|
+
`schemaCompatibility to be an object ` +
|
|
600
|
+
`(got ${describeType(piSchemaCompat)})`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return result;
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Submit a multi-document batch for asynchronous extraction. Wraps
|
|
608
|
+
* `POST /api/v1/vision/extract/batch`.
|
|
609
|
+
*
|
|
610
|
+
* Returns immediately with the new `jobId` and `status: "queued"`. The
|
|
611
|
+
* Vercel cron `vision-process-batch` (P5.3) drains the job over multiple
|
|
612
|
+
* 5-minute ticks; consumers poll progress via `vision.getJobStatus(jobId)`.
|
|
613
|
+
*
|
|
614
|
+
* **Enterprise plan-gate** — the kernel `requirePlan(org, 'hasBatchVision')`
|
|
615
|
+
* returns 403 with `details.code === "vision.batch.plan_required"` for
|
|
616
|
+
* non-entitled orgs. The SDK forwards the error faithfully.
|
|
617
|
+
*
|
|
618
|
+
* **Per-org active-job ceiling** — the kernel rejects with 429 + `details.
|
|
619
|
+
* code === "vision.batch.queue_limit"` when the org already has 5 jobs in
|
|
620
|
+
* `queued` or `processing` state. The SDK forwards (no client-side count).
|
|
621
|
+
*
|
|
622
|
+
* **`Idempotency-Key` HTTP header is NOT exposed in P5.6** (concern #9 in
|
|
623
|
+
* the audit doc; carry-forward documented). The kernel accepts the header
|
|
624
|
+
* for safe replay, but the SDK transport does not currently surface a
|
|
625
|
+
* way to thread arbitrary headers through JSON POSTs. A future small
|
|
626
|
+
* `chore(sdk):` will add `headers?: Record<string, string>` to
|
|
627
|
+
* `RequestOptions` and a corresponding method-arg here. Consumers needing
|
|
628
|
+
* idempotency today should retry with their own client-side dedupe.
|
|
629
|
+
*
|
|
630
|
+
* Errors — ordered by kernel firing precedence:
|
|
631
|
+
* - `AttestryAPIError` (status 429) — rate limit OR queue-limit
|
|
632
|
+
* (`details.code === "vision.batch.queue_limit"`). The rate-limit
|
|
633
|
+
* variant is auto-retried; the queue-limit variant is NOT (it
|
|
634
|
+
* indicates persistent backpressure).
|
|
635
|
+
* - `AttestryAPIError` (status 401) — no API key OR invalid key.
|
|
636
|
+
* - `AttestryAPIError` (status 403) — plan-gate denial
|
|
637
|
+
* (`details.code === "vision.batch.plan_required"`).
|
|
638
|
+
* - `AttestryAPIError` (status 400) — JSON parse failure on body.
|
|
639
|
+
* - `AttestryAPIError` (status 422) — Zod validation failed
|
|
640
|
+
* (`details.code === "vision.validation_failed"`); OR custom
|
|
641
|
+
* `BatchValidationError` (`details.code === "vision.batch.invalid"`).
|
|
642
|
+
* - `AttestryAPIError` (status 500) — internal kernel error
|
|
643
|
+
* (e.g. `vision_extraction_jobs` INSERT returned no id).
|
|
644
|
+
* - `AttestryError` — request abort / response shape failure / P3 guard.
|
|
645
|
+
* - `TypeError` (synchronous, no fetch issued) — input failed SDK-side
|
|
646
|
+
* validation.
|
|
647
|
+
*
|
|
648
|
+
* **SDK-side validation** (synchronous `TypeError`, no fetch issued):
|
|
649
|
+
* - `input` itself: required; non-null, non-array object.
|
|
650
|
+
* - `input.documents`: required; non-empty array (SDK enforces
|
|
651
|
+
* `length >= 1`; kernel's upper bound is NOT duplicated).
|
|
652
|
+
* - Each document mirrors `extract` validation: required `mediaType`
|
|
653
|
+
* (enum) + `documentType` (enum); `base64` XOR `imageUri`;
|
|
654
|
+
* `extractionSchema` / `sourceImageUri` optional with the same caps.
|
|
655
|
+
* - `input.model` (when own-present, value not undefined): must be a
|
|
656
|
+
* member of `VISION_MODELS`.
|
|
657
|
+
*
|
|
658
|
+
* @example
|
|
659
|
+
* ```ts
|
|
660
|
+
* const { jobId } = await client.vision.extractBatch({
|
|
661
|
+
* documents: [
|
|
662
|
+
* {
|
|
663
|
+
* imageUri: "https://example.com/cert1.png",
|
|
664
|
+
* mediaType: "image/png",
|
|
665
|
+
* documentType: "certification-label",
|
|
666
|
+
* },
|
|
667
|
+
* {
|
|
668
|
+
* imageUri: "https://example.com/cert2.png",
|
|
669
|
+
* mediaType: "image/png",
|
|
670
|
+
* documentType: "certification-label",
|
|
671
|
+
* },
|
|
672
|
+
* ],
|
|
673
|
+
* model: "sonnet",
|
|
674
|
+
* });
|
|
675
|
+
* // Poll: while (status !== "completed") { status = (await client.vision
|
|
676
|
+
* // .getJobStatus(jobId)).status; await new Promise(r => setTimeout(r, 5000)); }
|
|
677
|
+
* ```
|
|
678
|
+
*/
|
|
679
|
+
extractBatch(input, options) {
|
|
680
|
+
if (input === null ||
|
|
681
|
+
typeof input !== "object" ||
|
|
682
|
+
Array.isArray(input)) {
|
|
683
|
+
throw new TypeError("vision.extractBatch: `input` must be a non-null object");
|
|
684
|
+
}
|
|
685
|
+
// Snapshot via `readInputField` so a throwing accessor on the
|
|
686
|
+
// consumer-supplied input surfaces as the documented synchronous
|
|
687
|
+
// `TypeError` input contract (session-22 hostile MEDIUM-1); the
|
|
688
|
+
// `objectHasOwn` presence check is a separate pollution defense.
|
|
689
|
+
const hasDocuments = objectHasOwn(input, "documents");
|
|
690
|
+
const documentsRaw = hasDocuments
|
|
691
|
+
? readInputField(input, "documents", "vision.extractBatch")
|
|
692
|
+
: undefined;
|
|
693
|
+
const hasModel = objectHasOwn(input, "model");
|
|
694
|
+
const modelRaw = hasModel
|
|
695
|
+
? readInputField(input, "model", "vision.extractBatch")
|
|
696
|
+
: undefined;
|
|
697
|
+
if (!hasDocuments || documentsRaw === undefined) {
|
|
698
|
+
throw new TypeError("vision.extractBatch: `documents` is required");
|
|
699
|
+
}
|
|
700
|
+
if (!Array.isArray(documentsRaw)) {
|
|
701
|
+
throw new TypeError(`vision.extractBatch: \`documents\` must be an array ` +
|
|
702
|
+
`(got ${describeType(documentsRaw)})`);
|
|
703
|
+
}
|
|
704
|
+
// Snapshot via Array.from up front so a Proxy whose `.length` or `[i]`
|
|
705
|
+
// changes between reads can't slip past validation.
|
|
706
|
+
const docsSnapshot = Array.from(documentsRaw);
|
|
707
|
+
if (docsSnapshot.length === 0) {
|
|
708
|
+
throw new TypeError("vision.extractBatch: `documents` must contain at least one entry");
|
|
709
|
+
}
|
|
710
|
+
// Validate each document; collect a parallel array of validated payloads
|
|
711
|
+
// to forward.
|
|
712
|
+
const validatedDocuments = [];
|
|
713
|
+
for (let i = 0; i < docsSnapshot.length; i++) {
|
|
714
|
+
const doc = docsSnapshot[i];
|
|
715
|
+
if (doc === null || typeof doc !== "object" || Array.isArray(doc)) {
|
|
716
|
+
throw new TypeError(`vision.extractBatch: \`documents[${i}]\` must be a non-null object ` +
|
|
717
|
+
`(got ${describeType(doc)})`);
|
|
718
|
+
}
|
|
719
|
+
validatedDocuments.push(validateBatchDocument(doc, i));
|
|
720
|
+
}
|
|
721
|
+
let validatedModel;
|
|
722
|
+
if (hasModel && modelRaw !== undefined) {
|
|
723
|
+
if (typeof modelRaw !== "string") {
|
|
724
|
+
throw new TypeError(`vision.extractBatch: \`model\` must be a string when provided ` +
|
|
725
|
+
`(got ${describeType(modelRaw)})`);
|
|
726
|
+
}
|
|
727
|
+
if (!VISION_MODELS.includes(modelRaw)) {
|
|
728
|
+
throw new TypeError(`vision.extractBatch: \`model\` must be one of ` +
|
|
729
|
+
`${JSON.stringify(VISION_MODELS)} (got ` +
|
|
730
|
+
`${JSON.stringify(modelRaw)})`);
|
|
731
|
+
}
|
|
732
|
+
validatedModel = modelRaw;
|
|
733
|
+
}
|
|
734
|
+
const body = { documents: validatedDocuments };
|
|
735
|
+
if (validatedModel !== undefined)
|
|
736
|
+
body.model = validatedModel;
|
|
737
|
+
return this.client
|
|
738
|
+
._request({
|
|
739
|
+
method: "POST",
|
|
740
|
+
path: "/api/v1/vision/extract/batch",
|
|
741
|
+
body,
|
|
742
|
+
options,
|
|
743
|
+
})
|
|
744
|
+
.then((result) => {
|
|
745
|
+
if (result === null ||
|
|
746
|
+
typeof result !== "object" ||
|
|
747
|
+
Array.isArray(result)) {
|
|
748
|
+
throw new AttestryError(`vision.extractBatch: expected an object response from the kernel ` +
|
|
749
|
+
`(got ${describeType(result)})`);
|
|
750
|
+
}
|
|
751
|
+
const obj = result;
|
|
752
|
+
const jobId = objectHasOwn(obj, "jobId") ? obj.jobId : undefined;
|
|
753
|
+
if (typeof jobId !== "string") {
|
|
754
|
+
throw new AttestryError(`vision.extractBatch: expected response.jobId to be a string ` +
|
|
755
|
+
`(got ${describeType(jobId)})`);
|
|
756
|
+
}
|
|
757
|
+
const status = objectHasOwn(obj, "status") ? obj.status : undefined;
|
|
758
|
+
if (typeof status !== "string") {
|
|
759
|
+
throw new AttestryError(`vision.extractBatch: expected response.status to be a string ` +
|
|
760
|
+
`(got ${describeType(status)})`);
|
|
761
|
+
}
|
|
762
|
+
return result;
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Poll the status + cost rollup of an async batch job. Wraps
|
|
767
|
+
* `GET /api/v1/vision/extract/jobs/{jobId}`.
|
|
768
|
+
*
|
|
769
|
+
* **Anti-enumeration 404 collapse** — a job belonging to another org
|
|
770
|
+
* returns 404, identical to an unknown id. Consumers writing defensive
|
|
771
|
+
* error-handling logic must NOT use a 404 to infer "this job ID never
|
|
772
|
+
* existed". The raw `config` jsonb (which can hold base64 image
|
|
773
|
+
* payloads) is INTENTIONALLY NOT echoed back; only status/cost/error
|
|
774
|
+
* columns are projected.
|
|
775
|
+
*
|
|
776
|
+
* Errors:
|
|
777
|
+
* - `AttestryAPIError` (status 429) — rate limit (auto-retried).
|
|
778
|
+
* - `AttestryAPIError` (status 401) — no API key OR invalid key.
|
|
779
|
+
* - `AttestryAPIError` (status 403) — authenticated key lacks
|
|
780
|
+
* `READ_ASSESSMENTS`.
|
|
781
|
+
* - `AttestryAPIError` (status 400) — kernel-side malformed UUID
|
|
782
|
+
* ("Invalid job id."). Pre-validated by the SDK first, so reaches
|
|
783
|
+
* consumers only via UUID regex drift.
|
|
784
|
+
* - `AttestryAPIError` (status 404) — not found OR cross-org
|
|
785
|
+
* (deliberate conflation).
|
|
786
|
+
* - `AttestryAPIError` (status 500) — internal kernel error.
|
|
787
|
+
* - `AttestryError` — request abort / response shape failure / P3.
|
|
788
|
+
* - `TypeError` (synchronous, no fetch issued) — `jobId` empty,
|
|
789
|
+
* non-string, or not an RFC 4122 hyphenated UUID.
|
|
790
|
+
*/
|
|
791
|
+
getJobStatus(jobId, options) {
|
|
792
|
+
if (typeof jobId !== "string") {
|
|
793
|
+
throw new TypeError(`vision.getJobStatus: \`jobId\` must be a string ` +
|
|
794
|
+
`(got ${describeType(jobId)})`);
|
|
795
|
+
}
|
|
796
|
+
if (jobId.length === 0) {
|
|
797
|
+
throw new TypeError("vision.getJobStatus: `jobId` must be a non-empty string");
|
|
798
|
+
}
|
|
799
|
+
// UUID regex pre-validation. Hyphen-only characters: a UUID cannot
|
|
800
|
+
// contain `.`/`..`/NUL/slashes/UTF-16 surrogates, so the path-traversal
|
|
801
|
+
// and encodeURIComponent-URIError guards inherent to other resources
|
|
802
|
+
// are automatically satisfied here.
|
|
803
|
+
if (!UUID_REGEX.test(jobId)) {
|
|
804
|
+
throw new TypeError("vision.getJobStatus: `jobId` must be an RFC 4122 hyphenated UUID " +
|
|
805
|
+
"(matched regex: /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-" +
|
|
806
|
+
"[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/)");
|
|
807
|
+
}
|
|
808
|
+
const encoded = encodeURIComponent(jobId);
|
|
809
|
+
return this.client
|
|
810
|
+
._request({
|
|
811
|
+
method: "GET",
|
|
812
|
+
path: `/api/v1/vision/extract/jobs/${encoded}`,
|
|
813
|
+
options,
|
|
814
|
+
})
|
|
815
|
+
.then((result) => {
|
|
816
|
+
if (result === null ||
|
|
817
|
+
typeof result !== "object" ||
|
|
818
|
+
Array.isArray(result)) {
|
|
819
|
+
throw new AttestryError(`vision.getJobStatus: expected an object response from the kernel ` +
|
|
820
|
+
`(got ${describeType(result)})`);
|
|
821
|
+
}
|
|
822
|
+
const obj = result;
|
|
823
|
+
// String fields.
|
|
824
|
+
const stringFields = ["jobId", "status", "modelTier", "createdAt"];
|
|
825
|
+
for (const field of stringFields) {
|
|
826
|
+
const v = objectHasOwn(obj, field) ? obj[field] : undefined;
|
|
827
|
+
if (typeof v !== "string") {
|
|
828
|
+
throw new AttestryError(`vision.getJobStatus: expected response.${field} to be a string ` +
|
|
829
|
+
`(got ${describeType(v)})`);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
// Number fields (always present).
|
|
833
|
+
const numberFields = [
|
|
834
|
+
"documentCount",
|
|
835
|
+
"documentsProcessed",
|
|
836
|
+
"costUsdCents",
|
|
837
|
+
];
|
|
838
|
+
for (const field of numberFields) {
|
|
839
|
+
const v = objectHasOwn(obj, field) ? obj[field] : undefined;
|
|
840
|
+
if (typeof v !== "number") {
|
|
841
|
+
throw new AttestryError(`vision.getJobStatus: expected response.${field} to be a number ` +
|
|
842
|
+
`(got ${describeType(v)})`);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
// bigint columns: number OR string.
|
|
846
|
+
const bigintFields = ["costTokensInput", "costTokensOutput"];
|
|
847
|
+
for (const field of bigintFields) {
|
|
848
|
+
const v = objectHasOwn(obj, field) ? obj[field] : undefined;
|
|
849
|
+
if (typeof v !== "number" && typeof v !== "string") {
|
|
850
|
+
throw new AttestryError(`vision.getJobStatus: expected response.${field} to be a number ` +
|
|
851
|
+
`or string (got ${describeType(v)})`);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
// Nullable fields: errorLog (array | null); resultPackId, startedAt,
|
|
855
|
+
// completedAt (string | null).
|
|
856
|
+
const errorLog = objectHasOwn(obj, "errorLog") ? obj.errorLog : undefined;
|
|
857
|
+
if (errorLog !== null && !Array.isArray(errorLog)) {
|
|
858
|
+
throw new AttestryError(`vision.getJobStatus: expected response.errorLog to be an array ` +
|
|
859
|
+
`or null (got ${describeType(errorLog)})`);
|
|
860
|
+
}
|
|
861
|
+
const nullableStringFields = [
|
|
862
|
+
"resultPackId",
|
|
863
|
+
"startedAt",
|
|
864
|
+
"completedAt",
|
|
865
|
+
];
|
|
866
|
+
for (const field of nullableStringFields) {
|
|
867
|
+
const v = objectHasOwn(obj, field) ? obj[field] : undefined;
|
|
868
|
+
if (v !== null && typeof v !== "string") {
|
|
869
|
+
throw new AttestryError(`vision.getJobStatus: expected response.${field} to be a string ` +
|
|
870
|
+
`or null (got ${describeType(v)})`);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
return result;
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
// ─── Internal helpers ───────────────────────────────────────────────────────
|
|
878
|
+
/**
|
|
879
|
+
* Validate one batch-document object. Used by `extractBatch`. Returns a
|
|
880
|
+
* frozen payload object with only the validated fields populated (mirrors
|
|
881
|
+
* the per-document body kernel `batchDocumentSchema` accepts).
|
|
882
|
+
*
|
|
883
|
+
* Throws `TypeError` on any rule violation — same contract as `extract`'s
|
|
884
|
+
* top-level validation.
|
|
885
|
+
*/
|
|
886
|
+
function validateBatchDocument(doc, index) {
|
|
887
|
+
const prefix = `vision.extractBatch: \`documents[${index}]\``;
|
|
888
|
+
const d = doc;
|
|
889
|
+
// Each per-document field read goes through `readInputField` so a
|
|
890
|
+
// throwing accessor on a consumer-supplied `documents[i]` object
|
|
891
|
+
// surfaces as the documented synchronous `TypeError` input contract
|
|
892
|
+
// (session-22 hostile MEDIUM-1); the `objectHasOwn` presence check is
|
|
893
|
+
// a separate pollution defense.
|
|
894
|
+
const hasBase64 = objectHasOwn(d, "base64");
|
|
895
|
+
const base64Raw = hasBase64
|
|
896
|
+
? readInputField(d, "base64", "vision.extractBatch")
|
|
897
|
+
: undefined;
|
|
898
|
+
const hasImageUri = objectHasOwn(d, "imageUri");
|
|
899
|
+
const imageUriRaw = hasImageUri
|
|
900
|
+
? readInputField(d, "imageUri", "vision.extractBatch")
|
|
901
|
+
: undefined;
|
|
902
|
+
const hasMediaType = objectHasOwn(d, "mediaType");
|
|
903
|
+
const mediaTypeRaw = hasMediaType
|
|
904
|
+
? readInputField(d, "mediaType", "vision.extractBatch")
|
|
905
|
+
: undefined;
|
|
906
|
+
const hasDocumentType = objectHasOwn(d, "documentType");
|
|
907
|
+
const documentTypeRaw = hasDocumentType
|
|
908
|
+
? readInputField(d, "documentType", "vision.extractBatch")
|
|
909
|
+
: undefined;
|
|
910
|
+
const hasExtractionSchema = objectHasOwn(d, "extractionSchema");
|
|
911
|
+
const extractionSchemaRaw = hasExtractionSchema
|
|
912
|
+
? readInputField(d, "extractionSchema", "vision.extractBatch")
|
|
913
|
+
: undefined;
|
|
914
|
+
const hasSourceImageUri = objectHasOwn(d, "sourceImageUri");
|
|
915
|
+
const sourceImageUriRaw = hasSourceImageUri
|
|
916
|
+
? readInputField(d, "sourceImageUri", "vision.extractBatch")
|
|
917
|
+
: undefined;
|
|
918
|
+
if (!hasMediaType || mediaTypeRaw === undefined) {
|
|
919
|
+
throw new TypeError(`${prefix}.mediaType is required`);
|
|
920
|
+
}
|
|
921
|
+
if (typeof mediaTypeRaw !== "string") {
|
|
922
|
+
throw new TypeError(`${prefix}.mediaType must be a string (got ${describeType(mediaTypeRaw)})`);
|
|
923
|
+
}
|
|
924
|
+
if (!SUPPORTED_MEDIA_TYPES.includes(mediaTypeRaw)) {
|
|
925
|
+
throw new TypeError(`${prefix}.mediaType must be one of ${JSON.stringify(SUPPORTED_MEDIA_TYPES)} ` +
|
|
926
|
+
`(got ${JSON.stringify(mediaTypeRaw)})`);
|
|
927
|
+
}
|
|
928
|
+
if (!hasDocumentType || documentTypeRaw === undefined) {
|
|
929
|
+
throw new TypeError(`${prefix}.documentType is required`);
|
|
930
|
+
}
|
|
931
|
+
if (typeof documentTypeRaw !== "string") {
|
|
932
|
+
throw new TypeError(`${prefix}.documentType must be a string ` +
|
|
933
|
+
`(got ${describeType(documentTypeRaw)})`);
|
|
934
|
+
}
|
|
935
|
+
if (!SUPPORTED_DOCUMENT_TYPES.includes(documentTypeRaw)) {
|
|
936
|
+
throw new TypeError(`${prefix}.documentType must be one of ` +
|
|
937
|
+
`${JSON.stringify(SUPPORTED_DOCUMENT_TYPES)} ` +
|
|
938
|
+
`(got ${JSON.stringify(documentTypeRaw)})`);
|
|
939
|
+
}
|
|
940
|
+
// base64 XOR imageUri.
|
|
941
|
+
const presentBase64 = hasBase64 && base64Raw !== undefined;
|
|
942
|
+
const presentImageUri = hasImageUri && imageUriRaw !== undefined;
|
|
943
|
+
if (presentBase64 && presentImageUri) {
|
|
944
|
+
throw new TypeError(`${prefix}: \`base64\` and \`imageUri\` are mutually exclusive — supply exactly one`);
|
|
945
|
+
}
|
|
946
|
+
if (!presentBase64 && !presentImageUri) {
|
|
947
|
+
throw new TypeError(`${prefix}: exactly one of \`base64\` or \`imageUri\` is required`);
|
|
948
|
+
}
|
|
949
|
+
let validatedBase64;
|
|
950
|
+
if (presentBase64) {
|
|
951
|
+
if (typeof base64Raw !== "string") {
|
|
952
|
+
throw new TypeError(`${prefix}.base64 must be a string (got ${describeType(base64Raw)})`);
|
|
953
|
+
}
|
|
954
|
+
if (base64Raw.length === 0) {
|
|
955
|
+
throw new TypeError(`${prefix}.base64 must be a non-empty string`);
|
|
956
|
+
}
|
|
957
|
+
if (base64Raw.length > ANTHROPIC_IMAGE_MAX_BASE64) {
|
|
958
|
+
throw new TypeError(`${prefix}.base64 exceeds the maximum length of ` +
|
|
959
|
+
`${ANTHROPIC_IMAGE_MAX_BASE64} characters (got ${base64Raw.length})`);
|
|
960
|
+
}
|
|
961
|
+
validatedBase64 = base64Raw;
|
|
962
|
+
}
|
|
963
|
+
let validatedImageUri;
|
|
964
|
+
if (presentImageUri) {
|
|
965
|
+
if (typeof imageUriRaw !== "string") {
|
|
966
|
+
throw new TypeError(`${prefix}.imageUri must be a string (got ${describeType(imageUriRaw)})`);
|
|
967
|
+
}
|
|
968
|
+
if (imageUriRaw.length === 0) {
|
|
969
|
+
throw new TypeError(`${prefix}.imageUri must be a non-empty string`);
|
|
970
|
+
}
|
|
971
|
+
if (imageUriRaw.length > MAX_IMAGE_URI_LENGTH) {
|
|
972
|
+
throw new TypeError(`${prefix}.imageUri exceeds the maximum length of ` +
|
|
973
|
+
`${MAX_IMAGE_URI_LENGTH} characters (got ${imageUriRaw.length})`);
|
|
974
|
+
}
|
|
975
|
+
validatedImageUri = imageUriRaw;
|
|
976
|
+
}
|
|
977
|
+
let validatedExtractionSchema;
|
|
978
|
+
if (hasExtractionSchema && extractionSchemaRaw !== undefined) {
|
|
979
|
+
if (typeof extractionSchemaRaw !== "string") {
|
|
980
|
+
throw new TypeError(`${prefix}.extractionSchema must be a string when provided ` +
|
|
981
|
+
`(got ${describeType(extractionSchemaRaw)})`);
|
|
982
|
+
}
|
|
983
|
+
if (!SUPPORTED_DOCUMENT_TYPES.includes(extractionSchemaRaw)) {
|
|
984
|
+
throw new TypeError(`${prefix}.extractionSchema must be one of ` +
|
|
985
|
+
`${JSON.stringify(SUPPORTED_DOCUMENT_TYPES)} ` +
|
|
986
|
+
`(got ${JSON.stringify(extractionSchemaRaw)})`);
|
|
987
|
+
}
|
|
988
|
+
validatedExtractionSchema =
|
|
989
|
+
extractionSchemaRaw;
|
|
990
|
+
}
|
|
991
|
+
let validatedSourceImageUri;
|
|
992
|
+
if (hasSourceImageUri && sourceImageUriRaw !== undefined) {
|
|
993
|
+
if (typeof sourceImageUriRaw !== "string") {
|
|
994
|
+
throw new TypeError(`${prefix}.sourceImageUri must be a string when provided ` +
|
|
995
|
+
`(got ${describeType(sourceImageUriRaw)})`);
|
|
996
|
+
}
|
|
997
|
+
if (sourceImageUriRaw.length === 0) {
|
|
998
|
+
throw new TypeError(`${prefix}.sourceImageUri must be a non-empty string when provided`);
|
|
999
|
+
}
|
|
1000
|
+
if (sourceImageUriRaw.length > MAX_IMAGE_URI_LENGTH) {
|
|
1001
|
+
throw new TypeError(`${prefix}.sourceImageUri exceeds the maximum length of ` +
|
|
1002
|
+
`${MAX_IMAGE_URI_LENGTH} characters (got ${sourceImageUriRaw.length})`);
|
|
1003
|
+
}
|
|
1004
|
+
validatedSourceImageUri = sourceImageUriRaw;
|
|
1005
|
+
}
|
|
1006
|
+
const out = {
|
|
1007
|
+
mediaType: mediaTypeRaw,
|
|
1008
|
+
documentType: documentTypeRaw,
|
|
1009
|
+
};
|
|
1010
|
+
if (validatedBase64 !== undefined)
|
|
1011
|
+
out.base64 = validatedBase64;
|
|
1012
|
+
if (validatedImageUri !== undefined)
|
|
1013
|
+
out.imageUri = validatedImageUri;
|
|
1014
|
+
if (validatedExtractionSchema !== undefined) {
|
|
1015
|
+
out.extractionSchema = validatedExtractionSchema;
|
|
1016
|
+
}
|
|
1017
|
+
if (validatedSourceImageUri !== undefined) {
|
|
1018
|
+
out.sourceImageUri = validatedSourceImageUri;
|
|
1019
|
+
}
|
|
1020
|
+
return out;
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Human-readable type description for error messages. Distinguishes `null`
|
|
1024
|
+
* and `array` from generic `object`. Duplicated per project pattern in
|
|
1025
|
+
* `decisions.ts` / `incidents.ts` / `gate.ts` / `check.ts` /
|
|
1026
|
+
* `compliance-check.ts` / `regulatory-changes.ts` (small helper, leaf-
|
|
1027
|
+
* resource modules, no shared module yet).
|
|
1028
|
+
*/
|
|
1029
|
+
function describeType(value) {
|
|
1030
|
+
if (value === null)
|
|
1031
|
+
return "null";
|
|
1032
|
+
if (Array.isArray(value))
|
|
1033
|
+
return "array";
|
|
1034
|
+
return typeof value;
|
|
1035
|
+
}
|
|
1036
|
+
//# sourceMappingURL=vision.js.map
|