@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.
Files changed (99) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +1269 -0
  3. package/dist/client.d.ts +58 -0
  4. package/dist/client.d.ts.map +1 -0
  5. package/dist/client.js +74 -0
  6. package/dist/client.js.map +1 -0
  7. package/dist/constants.d.ts +7 -0
  8. package/dist/constants.d.ts.map +1 -0
  9. package/dist/constants.js +43 -0
  10. package/dist/constants.js.map +1 -0
  11. package/dist/errors.d.ts +16 -0
  12. package/dist/errors.d.ts.map +1 -0
  13. package/dist/errors.js +41 -0
  14. package/dist/errors.js.map +1 -0
  15. package/dist/index.d.ts +17 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +20 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/lines-parser.d.ts +50 -0
  20. package/dist/lines-parser.d.ts.map +1 -0
  21. package/dist/lines-parser.js +211 -0
  22. package/dist/lines-parser.js.map +1 -0
  23. package/dist/ndjson-parser.d.ts +57 -0
  24. package/dist/ndjson-parser.d.ts.map +1 -0
  25. package/dist/ndjson-parser.js +245 -0
  26. package/dist/ndjson-parser.js.map +1 -0
  27. package/dist/resources/abac-policies.d.ts +1034 -0
  28. package/dist/resources/abac-policies.d.ts.map +1 -0
  29. package/dist/resources/abac-policies.js +1519 -0
  30. package/dist/resources/abac-policies.js.map +1 -0
  31. package/dist/resources/audit-log.d.ts +588 -0
  32. package/dist/resources/audit-log.d.ts.map +1 -0
  33. package/dist/resources/audit-log.js +629 -0
  34. package/dist/resources/audit-log.js.map +1 -0
  35. package/dist/resources/batch.d.ts +845 -0
  36. package/dist/resources/batch.d.ts.map +1 -0
  37. package/dist/resources/batch.js +1074 -0
  38. package/dist/resources/batch.js.map +1 -0
  39. package/dist/resources/chat.d.ts +151 -0
  40. package/dist/resources/chat.d.ts.map +1 -0
  41. package/dist/resources/chat.js +124 -0
  42. package/dist/resources/chat.js.map +1 -0
  43. package/dist/resources/check.d.ts +348 -0
  44. package/dist/resources/check.d.ts.map +1 -0
  45. package/dist/resources/check.js +543 -0
  46. package/dist/resources/check.js.map +1 -0
  47. package/dist/resources/compliance-check.d.ts +330 -0
  48. package/dist/resources/compliance-check.d.ts.map +1 -0
  49. package/dist/resources/compliance-check.js +402 -0
  50. package/dist/resources/compliance-check.js.map +1 -0
  51. package/dist/resources/decisions.d.ts +1208 -0
  52. package/dist/resources/decisions.d.ts.map +1 -0
  53. package/dist/resources/decisions.js +1362 -0
  54. package/dist/resources/decisions.js.map +1 -0
  55. package/dist/resources/evidence-pack.d.ts +1080 -0
  56. package/dist/resources/evidence-pack.d.ts.map +1 -0
  57. package/dist/resources/evidence-pack.js +1789 -0
  58. package/dist/resources/evidence-pack.js.map +1 -0
  59. package/dist/resources/gate.d.ts +613 -0
  60. package/dist/resources/gate.d.ts.map +1 -0
  61. package/dist/resources/gate.js +737 -0
  62. package/dist/resources/gate.js.map +1 -0
  63. package/dist/resources/incidents.d.ts +136 -0
  64. package/dist/resources/incidents.d.ts.map +1 -0
  65. package/dist/resources/incidents.js +229 -0
  66. package/dist/resources/incidents.js.map +1 -0
  67. package/dist/resources/regulatory-changes.d.ts +307 -0
  68. package/dist/resources/regulatory-changes.d.ts.map +1 -0
  69. package/dist/resources/regulatory-changes.js +365 -0
  70. package/dist/resources/regulatory-changes.js.map +1 -0
  71. package/dist/resources/safe-input-read.d.ts +21 -0
  72. package/dist/resources/safe-input-read.d.ts.map +1 -0
  73. package/dist/resources/safe-input-read.js +57 -0
  74. package/dist/resources/safe-input-read.js.map +1 -0
  75. package/dist/resources/ship-gate.d.ts +475 -0
  76. package/dist/resources/ship-gate.d.ts.map +1 -0
  77. package/dist/resources/ship-gate.js +727 -0
  78. package/dist/resources/ship-gate.js.map +1 -0
  79. package/dist/resources/vision.d.ts +540 -0
  80. package/dist/resources/vision.d.ts.map +1 -0
  81. package/dist/resources/vision.js +1036 -0
  82. package/dist/resources/vision.js.map +1 -0
  83. package/dist/retry.d.ts +103 -0
  84. package/dist/retry.d.ts.map +1 -0
  85. package/dist/retry.js +224 -0
  86. package/dist/retry.js.map +1 -0
  87. package/dist/sse-parser.d.ts +64 -0
  88. package/dist/sse-parser.d.ts.map +1 -0
  89. package/dist/sse-parser.js +271 -0
  90. package/dist/sse-parser.js.map +1 -0
  91. package/dist/transport.d.ts +142 -0
  92. package/dist/transport.d.ts.map +1 -0
  93. package/dist/transport.js +455 -0
  94. package/dist/transport.js.map +1 -0
  95. package/dist/types.d.ts +61 -0
  96. package/dist/types.d.ts.map +1 -0
  97. package/dist/types.js +3 -0
  98. package/dist/types.js.map +1 -0
  99. 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