@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,727 @@
1
+ // ─── ShipGate resource ──────────────────────────────────────────────────────
2
+ //
3
+ // Wraps the CI/CD ship-gate check surface (Prompt E.2 — session 20):
4
+ //
5
+ // - POST /api/v1/ship-gate/check Body: {systemId: <UUID>, attestationId: <string 1-256>}
6
+ //
7
+ // Seventh non-decisions resource on `@attestry/sdk`. Sibling to
8
+ // `IncidentsResource`, `DecisionsResource`, `ChatResource`,
9
+ // `AuditLogResource`, `RegulatoryChangesResource`,
10
+ // `ComplianceCheckResource`, `CheckResource`, `GateResource`,
11
+ // `BatchResource`. Single public method today (`check`); the resource
12
+ // class is the landing pad for future ship-gate methods if/when the
13
+ // kernel adds them (resource-class-per-kernel-resource convention,
14
+ // invariant #43).
15
+ //
16
+ // Method name `check` — matches the kernel endpoint name (POST
17
+ // /api/v1/ship-gate/check). Alternative `run` was considered for
18
+ // symmetry with `check.run` and rejected because the kernel endpoint
19
+ // is named `/check` and the existing `check.run` already occupies the
20
+ // `.run` verb at the SDK level. User-confirmed at session 20 start.
21
+ //
22
+ // **Distinct from `gate.evaluate`** — `gate.evaluate` is a synchronous
23
+ // compliance-score gate (pass/fail on assessment scores); `shipGate.check`
24
+ // is a multi-approver workflow gate that asks "is an in-flight approval
25
+ // chain blocking THIS build?". Different lifecycle (gate.evaluate has
26
+ // no state; shipGate has the gated → released/rejected/timed_out
27
+ // state machine bound to an approval chain execution). Different
28
+ // kernel routes and different consumer audience (gate.evaluate for
29
+ // CI score gates; shipGate.check for human-approver gates).
30
+ //
31
+ // **Multi-permission UNION auth scope**: the kernel route gates on
32
+ // `requireApiKeyWithPermission(request, READ_SYSTEMS, READ_ASSESSMENTS)`
33
+ // which is OR semantics — `permissions.ts:53-55` uses `Array.some()`.
34
+ // A key with EITHER permission (or `ADMIN`, or empty permissions for
35
+ // backwards-compat) succeeds. **HTTP 401** for no/invalid API key;
36
+ // **HTTP 403** for an authenticated key that has NEITHER required
37
+ // permission. Pin BOTH branches separately. Carry-forward invariant
38
+ // #45. **NOTE — argument order is READ_SYSTEMS FIRST** (asymmetric
39
+ // with `check.run` and `gate.evaluate` which list READ_ASSESSMENTS
40
+ // first); `Array.some()` is order-insensitive, but the kernel error
41
+ // message would echo the order declared. Drift-pinned exact-arglist
42
+ // in the spec-diff round.
43
+ //
44
+ // **Fourth SDK route to PRE-VALIDATE every Zod closed-spec rule
45
+ // synchronously** (after `check.run`, `gate.evaluate`, and
46
+ // `batch.submit`). The kernel
47
+ // uses `parseBody(request, checkSchema)` where `checkSchema` is
48
+ // `z.object({systemId: z.string().uuid(), attestationId: z.string()
49
+ // .min(1).max(MAX_ATTESTATION_ID_LENGTH)})`. The SDK pre-validates BOTH
50
+ // closed-spec rules synchronously (UUID format on systemId, string
51
+ // length bounds [1, 256] on attestationId). The SDK's runtime checks
52
+ // always run regardless of TypeScript types — `as any` casts do NOT
53
+ // bypass them. So 422 from this route reaches consumers ONLY via
54
+ // kernel-side rule changes the SDK hasn't synced to. Codifies
55
+ // invariants #49 + #51.
56
+ //
57
+ // **Variadic four-shape response** — SDK exposes a single
58
+ // `ShipGateCheckResponse` type with `gated: boolean` as the ALWAYS-
59
+ // present anchor field and 5 OPTIONAL own-property fields. The 4
60
+ // emit shapes (kernel `formatShipGateCheckResult` at
61
+ // `ship-gates.ts:249-312` + the default-permissive `{ gated: false }`
62
+ // short-circuit at `ship-gates.ts:544`):
63
+ // - Shape A (no gate exists): `{ gated: false }` — 1 field
64
+ // (default-permissive — the gate is opt-in; missing
65
+ // `(system_id, attestation_id)` row → SDK returns this shape).
66
+ // - Shape B (released): `{ gated: false, state: "released",
67
+ // executionId, chainId }` — 4 fields. The approval chain approved
68
+ // the deployment; the build proceeds.
69
+ // - Shape C (rejected/timed_out): `{ gated: true, reason: "rejected"
70
+ // | "timed_out", approvers_pending: [], state: "rejected" |
71
+ // "timed_out", executionId, chainId }` — 6 fields. The approval
72
+ // chain went terminal in a build-blocking state. `approvers_pending`
73
+ // is always `[]` (nobody is pending on a closed chain).
74
+ // - Shape D (gated): `{ gated: true, reason: "awaiting_approvers",
75
+ // approvers_pending: [<UUIDs>], state: "gated", executionId,
76
+ // chainId }` — 6 fields. The approval chain is in-flight;
77
+ // `approvers_pending` lists the userIds still owed a decision
78
+ // (pool-order, post-decided filtering — see kernel
79
+ // `computeApproversPending` for the full algorithm).
80
+ // Discriminate via `gated === true` (closed-enum boolean), NOT
81
+ // `reason === undefined` (prototype-pollution-unsafe — see D7).
82
+ //
83
+ // **`approvers_pending` is SNAKE_CASE on the wire** — the kernel emits
84
+ // the literal field name `approvers_pending` (NOT `approversPending`),
85
+ // asymmetric with the rest of the SDK's camelCase response surface.
86
+ // This matches the master-plan spec contract (line 5369) verbatim and
87
+ // is preserved here as-is. Consumers must use the snake_case spelling
88
+ // to read the field. Drift-pinned in the spec-diff round.
89
+ //
90
+ // **`reason` is a closed-string-enum** (`"awaiting_approvers" |
91
+ // "rejected" | "timed_out"`) at the TYPE level; the P2 runtime
92
+ // validator checks `typeof === "string"` only (faithful courier —
93
+ // mirror of gate.evaluate's `gate: "pass" | "fail"` pattern). If a
94
+ // future kernel emits a new reason code (e.g., `"escalated"`) before
95
+ // the SDK is bumped, the value round-trips at runtime. Consumers
96
+ // using exhaustive type-narrowing would misclassify the new value.
97
+ // The kernel emit sites are drift-pinned via the wire-shape build-
98
+ // round pin so a kernel extension surfaces in the drift suite before
99
+ // consumer regressions.
100
+ //
101
+ // **`state` is also a closed-string-enum** (`"gated" | "released" |
102
+ // "rejected" | "timed_out"`) — 4 values. Same faithful-courier
103
+ // treatment as `reason`. Drift-pinned in build-round wire pin.
104
+ //
105
+ // **Reconciliation-on-read inside transaction** — when the linked
106
+ // `approval_chain_executions` row has gone terminal (approved /
107
+ // rejected / timed_out) but the ship_gate row still says `gated`,
108
+ // the kernel's `checkShipGate` advances the gate to the corresponding
109
+ // terminal state inside `SELECT … FOR UPDATE` (see
110
+ // `ship-gates.ts:567-584`). The SDK does NOT observe the
111
+ // reconciliation step — only the post-reconciliation shape. A
112
+ // consumer calling `check()` twice in quick succession on a chain
113
+ // that just completed sees the gated-state shape on call 1 (if
114
+ // reconciliation hadn't fired yet) and the terminal shape on call 2.
115
+ // **Documented kernel surface behavior, faithful courier**.
116
+ //
117
+ // **`writeAuditLog` side effect** — every `shipGate.check(...)` call
118
+ // writes one `ship_gate.checked` entry to the org's audit log
119
+ // (route.ts:73-87). Properties of the write:
120
+ // - Org-scoped, hash-chained (per `writeAuditLog`).
121
+ // - **Time-blocking** but error-tolerant: the kernel uses
122
+ // `await writeAuditLog(...)`, which awaits two DB ops (SELECT
123
+ // previous-hash + INSERT new entry, at `src/lib/api.ts:130-159`).
124
+ // The check response latency INCLUDES the audit-log write time —
125
+ // a slow audit-log DB will delay every shipGate.check() response.
126
+ // Error semantics ARE non-blocking: `writeAuditLog` wraps its
127
+ // body in a try/catch that swallows + logs errors, so a write
128
+ // FAILURE does NOT fail the check request.
129
+ // - NOT counted against `decisionsPerMonth` quota (read-shaped from
130
+ // a quota perspective). Invariant candidate #53 carry-forward
131
+ // (matches `gate.evaluate`'s pattern).
132
+ //
133
+ // **Kernel-side 15-second timeout** (`maxDuration = 15` at
134
+ // `route.ts:24`). **Same as `gate.evaluate`'s 15s; tighter than
135
+ // `auditLog.verifyChain`'s 30s.** Ship-gate's transaction has a
136
+ // SELECT FOR UPDATE + up to 4 follow-up reads + an optional UPDATE
137
+ // on the reconcile path, and the kernel team budgeted 15s as
138
+ // sufficient for the worst case. The SDK does NOT enforce a client-
139
+ // side timeout (consumers manage via `options.signal`), but the
140
+ // kernel's function-runtime cap bounds the request latency on the
141
+ // server side. A future kernel raise (e.g., 15 → 30s) would relax
142
+ // this; CI pipeline timeout settings should be revisited. Drift-
143
+ // pinned in build-round Pin 7.
144
+ //
145
+ // **Documented kernel-side cascade-gap surfaces — TWO distinct paths**:
146
+ // 1. **Execution-missing → HTTP 404** (named-error path). The kernel
147
+ // route maps `ShipGateExecutionNotFoundError` to 404 at
148
+ // route.ts:97-99. This error is thrown by `checkShipGate` ONLY
149
+ // when the inner `executionRows.length === 0` defensive branch
150
+ // fires (`ship-gates.ts:559-564`) — i.e., when a `ship_gates`
151
+ // row references an `executionId` whose row is missing in
152
+ // `approval_chain_executions`.
153
+ // 2. **Chain-missing → HTTP 500 (scrubbed)** (plain-Error path).
154
+ // A SEPARATE defensive branch in `checkShipGate` at
155
+ // `ship-gates.ts:610-617` throws a PLAIN `Error` (NOT a named
156
+ // ship-gate error class) when a `ship_gates` row → execution
157
+ // → chain reference is broken on the LAST hop (execution row
158
+ // exists but its `chainId` doesn't resolve to an
159
+ // `approval_chains` row in the same org). The route's catch
160
+ // block has only three `instanceof` arms (`AuthError`,
161
+ // `BodyParseError`, `ShipGateExecutionNotFoundError`); a plain
162
+ // `Error` falls through to `internalErrorResponse → 500` with
163
+ // the scrubbed message "An internal error occurred. Please
164
+ // try again later." Defense-in-depth refusal posture per the
165
+ // kernel comment; the caller can't observe the chain-missing
166
+ // condition distinctly from any other internal error.
167
+ // Both branches should be unreachable via the RESTRICT FK + filter-
168
+ // by-orgId pattern in normal operation; both are documented as
169
+ // "only reachable via direct DB intervention or a cascade-behavior
170
+ // gap." Faithful courier: the SDK surfaces whichever status the
171
+ // kernel chose (404 vs 500), but consumers running SIEM /
172
+ // observability filters on cascade-gap-404 events should know the
173
+ // second branch hides as 500 (NOT 404).
174
+ //
175
+ // **No path-segment URIError defense** — POST body uses
176
+ // `JSON.stringify`, which handles lone UTF-16 surrogates by emitting
177
+ // them as literal `\uDxxx` escapes. The URIError defect class
178
+ // (carry-forward invariant #32) applies only to query-string paths
179
+ // (`encodeURIComponent`); this route has no query string and a fixed
180
+ // path. `assertEncodableQueryString` is NOT invoked here — explicit
181
+ // asymmetry vs `complianceCheck.check` / decisions / incidents /
182
+ // audit-log / regulatory-changes. Same asymmetry as `check.run` /
183
+ // `gate.evaluate`.
184
+ //
185
+ // **Symmetric prototype-pollution defense** — module-load snapshot of
186
+ // `Object.hasOwn` applied to BOTH input AND response sides. Mirror of
187
+ // `gate.ts` / `audit-log.ts` / `check.ts` / `batch.ts` pattern.
188
+ // Without the response-side defense, a kernel regression that drops
189
+ // a response field combined with a hostile npm dep polluting
190
+ // `Object.prototype.<field>` would let the polluted value pass
191
+ // typeof-check via prototype walk. With the defense, missing own-
192
+ // property → describeType(undefined) → AttestryError.
193
+ //
194
+ // Sync JSON request/response: reuses `client._request` and the
195
+ // existing `{success:true, data}` envelope-unwrap (carry-forward
196
+ // invariant #9). NO new SDK primitive needed. Returns
197
+ // `Promise<ShipGateCheckResponse>`.
198
+ import { AttestryError } from "../errors.js";
199
+ import { readInputField } from "./safe-input-read.js";
200
+ // Module-load snapshot of `Object.hasOwn` — defends against a
201
+ // late-loading hostile/buggy npm dependency that overrides the global
202
+ // (e.g., `Object.hasOwn = () => true`). Without the snapshot, the
203
+ // prototype-pollution defenses below use whatever Object.hasOwn the
204
+ // dependency replaced it with at request time. Snapshotting at module
205
+ // load captures the original implementation BEFORE most consumer
206
+ // code has a chance to monkey-patch.
207
+ //
208
+ // Mirror of `audit-log.ts` / `batch.ts` / `gate.ts` / `check.ts` /
209
+ // `compliance-check.ts` pattern. Used symmetrically on input AND
210
+ // response sides (input boundary is non-empty here — 2 fields:
211
+ // systemId + attestationId).
212
+ const objectHasOwn = Object.hasOwn;
213
+ // UUID format regex — RFC 4122 hyphenated form (8-4-4-4-12 hex,
214
+ // case-insensitive). Matches Zod's `z.string().uuid()` regex
215
+ // effectively. Mirror of `check.ts` / `gate.ts` UUID_REGEX. Drift-
216
+ // pinned in `sdk-drift.test.ts` spec-diff round so a kernel-side
217
+ // switch to a different UUID flavor (ULID, KSUID, etc.) fires before
218
+ // consumer regressions.
219
+ 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}$/;
220
+ // Max attestation-id length — mirrors the kernel constant
221
+ // `MAX_ATTESTATION_ID_LENGTH = 256` at `src/lib/workflow/ship-gates.ts:106`.
222
+ // Pre-validated SDK-side (D4); drift-pinned in spec-diff round so a
223
+ // kernel-side relax/tighten surfaces before consumer regressions.
224
+ const MAX_ATTESTATION_ID_LENGTH = 256;
225
+ /**
226
+ * `shipGate` resource — sibling to `IncidentsResource`,
227
+ * `DecisionsResource`, `ChatResource`, `AuditLogResource`,
228
+ * `RegulatoryChangesResource`, `ComplianceCheckResource`,
229
+ * `CheckResource`, `GateResource`, `BatchResource`. Today wraps a
230
+ * single endpoint (`check`); the class is the landing pad for future
231
+ * ship-gate methods if the kernel adds them (resource-class-per-
232
+ * kernel-resource convention, invariant #43).
233
+ */
234
+ export class ShipGateResource {
235
+ client;
236
+ constructor(client) {
237
+ this.client = client;
238
+ }
239
+ /**
240
+ * Check whether a CI/CD build is gated by an in-flight approval
241
+ * chain. Returns a four-shape verdict keyed by the gate's
242
+ * existence + state. Designed for pipeline integration (GitHub
243
+ * Actions / GitLab CI / Buildkite).
244
+ *
245
+ * **Four emit shapes** — the response shape varies by the gate's
246
+ * existence + state:
247
+ * - **Shape A — no gate exists**: `{ gated: false }` (1 field —
248
+ * default-permissive, opt-in gate semantics).
249
+ * - **Shape B — released**: `{ gated: false, state: "released",
250
+ * executionId, chainId }` (4 fields).
251
+ * - **Shape C — rejected / timed_out**: `{ gated: true, reason:
252
+ * "rejected" | "timed_out", approvers_pending: [], state, ... }`
253
+ * (6 fields; `approvers_pending` always `[]` on closed chain).
254
+ * - **Shape D — gated awaiting approvers**: `{ gated: true,
255
+ * reason: "awaiting_approvers", approvers_pending: [<UUIDs>],
256
+ * state: "gated", ... }` (6 fields; `approvers_pending` lists
257
+ * pending userIds).
258
+ *
259
+ * **`approvers_pending` is SNAKE_CASE on the wire** — the kernel
260
+ * emits the literal field name `approvers_pending` (asymmetric
261
+ * with the rest of the SDK's camelCase response surface). The SDK
262
+ * preserves this verbatim; consumers must use the snake_case
263
+ * spelling.
264
+ *
265
+ * **Multi-permission UNION auth scope**: kernel uses
266
+ * `requireApiKeyWithPermission(req, READ_SYSTEMS, READ_ASSESSMENTS)`
267
+ * which is OR semantics (`Array.some()` at
268
+ * `permissions.ts:53-55`). A key with EITHER permission (or
269
+ * `ADMIN`, or null/empty permissions for backwards-compat)
270
+ * succeeds. **HTTP 401** for no/invalid API key, **HTTP 403** for
271
+ * an authenticated key that has NEITHER required permission. Pin
272
+ * BOTH branches separately. Carry-forward invariant #45.
273
+ * **NOTE — argument order is READ_SYSTEMS FIRST** (asymmetric
274
+ * with `check.run` and `gate.evaluate` which list READ_ASSESSMENTS
275
+ * first); `Array.some()` is order-insensitive, but drift-pinned
276
+ * exact-arglist in the spec-diff round catches a kernel-side
277
+ * rename or reordering.
278
+ *
279
+ * **Discriminator pattern**: branch on `result.gated === true`
280
+ * (closed-enum boolean) to detect "build must block". The SDK's
281
+ * P2 validator guarantees `gated` is ALWAYS an own-property of
282
+ * the returned object — that's the only consumer-side discriminator
283
+ * that is genuinely pollution-safe (no prototype-walk hazard).
284
+ *
285
+ * **Do NOT use `result.reason === undefined`** as a discriminator
286
+ * — a hostile dep polluting `Object.prototype.reason` makes the
287
+ * `=== undefined` check return false (reads via prototype walk),
288
+ * silently misclassifying Shape A / B as Shape C / D.
289
+ *
290
+ * **Consumer-side `Object.hasOwn(result, "state")` is NOT a fully
291
+ * safe alternative** — it relies on the LIVE global `Object.hasOwn`,
292
+ * which is itself subject to override by a hostile dep
293
+ * (`Object.hasOwn = () => true`). The SDK's own response-side
294
+ * validator uses a module-load snapshot of `Object.hasOwn` (taken
295
+ * at SDK import time, before consumer-graph deps load) so the
296
+ * SDK-internal validation is hardened; consumer code calling the
297
+ * live `Object.hasOwn` after the SDK resolves is not. For
298
+ * consumer-side defense-in-depth, branch on `result.gated` first
299
+ * (the safe boolean), and only inspect `result.state` after.
300
+ *
301
+ * **Reconciliation-on-read inside transaction** — when the linked
302
+ * `approval_chain_executions` row has gone terminal but the
303
+ * `ship_gates` row still says `gated`, the kernel's `checkShipGate`
304
+ * advances the gate to the corresponding terminal state inside
305
+ * `SELECT … FOR UPDATE` (`ship-gates.ts:567-584`). The SDK does
306
+ * NOT observe the reconciliation step — only the post-reconciliation
307
+ * shape. A consumer calling `check()` twice in quick succession
308
+ * on a chain that just completed sees the gated-state shape on
309
+ * call 1 (if reconciliation hadn't fired yet) and the terminal
310
+ * shape on call 2. Faithful courier; documented kernel behavior.
311
+ *
312
+ * **`writeAuditLog` side effect** — every `shipGate.check(...)`
313
+ * call writes one audit-log entry with `action: "ship_gate.checked"`
314
+ * and `resourceType: "ship_gate"` (route.ts:73-87; both strings
315
+ * drift-pinned). SIEM / observability consumers keying off either
316
+ * field for filter setup should depend on both staying stable.
317
+ * Properties of the write:
318
+ * - Org-scoped, hash-chained (per `writeAuditLog`).
319
+ * - **Time-blocking** but error-tolerant: the kernel uses
320
+ * `await writeAuditLog(...)`, which awaits two DB ops (SELECT
321
+ * previous-hash + INSERT new entry). The check response
322
+ * latency INCLUDES the audit-log write time — a slow audit-log
323
+ * DB will delay every `shipGate.check()` response. Error
324
+ * semantics ARE non-blocking: `writeAuditLog` wraps its body
325
+ * in a try/catch that swallows + logs errors, so a write
326
+ * FAILURE does NOT fail the check request.
327
+ * - NOT counted against `decisionsPerMonth` quota (read-shaped).
328
+ * Invariant #53 carry-forward (matches `gate.evaluate`'s pattern).
329
+ *
330
+ * **Kernel-side 15-second timeout** (`maxDuration = 15` at
331
+ * `route.ts:24`). **Same as `gate.evaluate`'s 15s; tighter than
332
+ * `auditLog.verifyChain`'s 30s.** The SDK does NOT enforce a
333
+ * client-side timeout (consumers manage via `options.signal`),
334
+ * but the kernel's function-runtime cap bounds the request
335
+ * latency on the server side. CI pipeline timeouts should budget
336
+ * relative to this cap.
337
+ *
338
+ * **Documented kernel-side cascade-gap surfaces — TWO distinct
339
+ * paths**:
340
+ * 1. **Execution-missing → HTTP 404** (named-error path). The
341
+ * kernel maps `ShipGateExecutionNotFoundError` to 404 at
342
+ * route.ts:97-99. Thrown by `checkShipGate` only when the
343
+ * inner `executionRows.length === 0` defensive branch fires
344
+ * (`ship-gates.ts:559-564`).
345
+ * 2. **Chain-missing → HTTP 500 (scrubbed)** (plain-Error path).
346
+ * A SEPARATE defensive branch at `ship-gates.ts:610-617`
347
+ * throws a PLAIN `Error` (NOT a named class) when a
348
+ * ship_gate → execution → chain reference is broken on the
349
+ * LAST hop. The route's catch block has only three
350
+ * `instanceof` arms (`AuthError`, `BodyParseError`,
351
+ * `ShipGateExecutionNotFoundError`); a plain `Error` falls
352
+ * through to `internalErrorResponse → 500` with the scrubbed
353
+ * message "An internal error occurred. Please try again
354
+ * later." The caller cannot distinguish this cascade-gap
355
+ * from any other internal error via the HTTP status alone.
356
+ * Both branches are unreachable in normal operation (RESTRICT FK
357
+ * + filter-by-orgId); both documented as "only reachable via
358
+ * direct DB intervention or a cascade-behavior gap." Faithful
359
+ * courier: the SDK surfaces whichever status the kernel chose
360
+ * (404 vs 500), but SIEM consumers running cascade-gap-404
361
+ * filters should know the second branch hides as 500.
362
+ *
363
+ * Errors — **happy-path precedence ordering** is rate-limit → auth
364
+ * → Zod body validation → DB lookup → successResponse. A request
365
+ * with multiple happy-path problems surfaces ONLY the highest-
366
+ * precedence one. **The 500-catchall is a SEPARATE DIMENSION** —
367
+ * any throwable not matched by the named `instanceof` arms (see
368
+ * the 500 bullet below) falls to 500, regardless of where in the
369
+ * happy-path it fired. So e.g. a Zod-library crash during body
370
+ * parsing surfaces as 500 (NOT 422).
371
+ * - `AttestryAPIError` (status 429) — rate limit FIRES FIRST
372
+ * (auto-retried by default — invariant #18; per-IP rate-limit
373
+ * key `v1-ship-gate-check:${ip}` against the standard
374
+ * `apiLimiter`).
375
+ * - `AttestryAPIError` (status 401) — no API key OR invalid key.
376
+ * Fires AFTER rate-limit but BEFORE input validation.
377
+ * - `AttestryAPIError` (status 403) — authenticated key has
378
+ * NEITHER `READ_SYSTEMS` nor `READ_ASSESSMENTS`. Single test
379
+ * case — the union-auth pattern collapses three intuition-
380
+ * suggesting cases to one.
381
+ * - `AttestryAPIError` (status 422) — Zod schema rejection
382
+ * (kernel's `BodyParseError` surface — `parseBody(request,
383
+ * checkSchema)` failed). **Fires BEFORE the cascade-gap 404
384
+ * lookup**. `apiErr.details` carries the full kernel error
385
+ * body verbatim (the transport does NOT strip the
386
+ * `{success:false, ...}` envelope on error responses — only
387
+ * the `{success:true, data}` envelope on success). The wire
388
+ * shape is: `{success: false, error: "Validation failed.",
389
+ * details: Array<{path: string; message: string}>}` — `error`
390
+ * is the literal string "Validation failed." (with trailing
391
+ * period), `details` is an array (NOT a keyed map) of `{path,
392
+ * message}` pairs derived from Zod's `result.error.errors`.
393
+ * **The SDK pre-validates both closed-spec rules** (UUID
394
+ * format on systemId, length 1-256 on attestationId) AND the
395
+ * runtime checks always run regardless of TypeScript types —
396
+ * `as any` casts do NOT bypass them. So 422 reaches consumers
397
+ * ONLY via kernel rule changes the SDK hasn't synced to.
398
+ * Invariant #51.
399
+ * - `AttestryAPIError` (status 404) — cascade-gap rare path:
400
+ * kernel threw `ShipGateExecutionNotFoundError` because a
401
+ * `ship_gates` row references an `executionId` whose row is
402
+ * missing in `approval_chain_executions`. Documented as "only
403
+ * reachable via direct DB intervention".
404
+ * - `AttestryAPIError` (status 500) — internal kernel error
405
+ * (scrubbed message via `internalErrorResponse`). **The 500
406
+ * surface is orthogonal to the precedence list above**: ANY
407
+ * throwable not matched by the three named `instanceof` arms
408
+ * (`AuthError` 401/403, `BodyParseError` 422,
409
+ * `ShipGateExecutionNotFoundError` 404) — INCLUDING throwables
410
+ * that fire DURING any of the happy-path steps — falls to
411
+ * 500. **Includes the chain-missing cascade-gap** (the second
412
+ * defensive branch in `checkShipGate` at `ship-gates.ts:
413
+ * 610-617` throws a plain `Error` that hides as 500, NOT as a
414
+ * named 404 surface).
415
+ * - `AttestryError` ("request aborted by caller") — caller-
416
+ * supplied `options.signal` fired (pre-aborted or mid-flight).
417
+ * - `AttestryError` (P2 hardening) — kernel response failed
418
+ * SDK-side shape validation (not an object, wrong type on
419
+ * `gated`, wrong type on any optional own-property field).
420
+ * - `AttestryAPIError` (P3 hardening) — kernel response had a
421
+ * wrong Content-Type (transport-level guard before body
422
+ * parsing).
423
+ * - `TypeError` (synchronous, no fetch issued) — input failed
424
+ * SDK-side validation (null / array / non-object input,
425
+ * missing systemId, invalid UUID format, missing attestationId,
426
+ * non-string attestationId, attestationId length out of
427
+ * range [1, 256]).
428
+ *
429
+ * **Notably ABSENT**:
430
+ * - **No 400** — all input validation is Zod (422).
431
+ * - **No 402** — read-shaped, doesn't count against
432
+ * decisionsPerMonth quota (despite the audit-log side effect).
433
+ * - **No 413** — body size limit not explicit.
434
+ *
435
+ * **SDK-side validation** (synchronous `TypeError`, no fetch
436
+ * issued):
437
+ * - `input` itself: required; must be a non-null, non-array
438
+ * object.
439
+ * - `input.systemId`: required own-property (Object.hasOwn
440
+ * defends against prototype pollution lying about presence —
441
+ * generalization of invariant #48); must be a non-empty
442
+ * string; must match the RFC 4122 hyphenated UUID format
443
+ * (D2 — SDK pre-validates closed-spec rule). No
444
+ * lone-surrogate URIError defense (POST body uses
445
+ * JSON.stringify).
446
+ * - `input.attestationId`: required own-property; must be a
447
+ * non-empty string; length 1-256 (matches kernel constant
448
+ * `MAX_ATTESTATION_ID_LENGTH = 256` at
449
+ * `src/lib/workflow/ship-gates.ts:106`).
450
+ *
451
+ * **Response-shape validation** (P2 hardening — symmetric defense
452
+ * on response side per the module-load `objectHasOwn` snapshot;
453
+ * mirror of `gate.ts` / `audit-log.ts` patterns):
454
+ * - Rejects with `AttestryError` if the kernel response isn't
455
+ * a non-null, non-array object.
456
+ * - Rejects if `gated` isn't a boolean (ALWAYS-present anchor
457
+ * field — the validator's `gated` branch is UNCONDITIONAL).
458
+ * - Rejects if `reason` (when own-present) isn't a string.
459
+ * - Rejects if `approvers_pending` (when own-present) isn't an
460
+ * array.
461
+ * - Rejects if `state` (when own-present) isn't a string.
462
+ * - Rejects if `executionId` / `chainId` (when own-present)
463
+ * aren't strings.
464
+ * - Each response field read goes through the module-load
465
+ * `objectHasOwn` snapshot — defends against
466
+ * `Object.prototype.<field>` pollution masking a missing field.
467
+ * - Per-element shape on `approvers_pending` (each element must
468
+ * be a string) is validated when the field is own-present.
469
+ *
470
+ * **Transport-shape validation** (P3 hardening):
471
+ * - Rejects with `AttestryAPIError` if the kernel responds with
472
+ * a non-`application/json` Content-Type.
473
+ *
474
+ * @example Basic ship-gate check (typical CI usage)
475
+ * ```ts
476
+ * const verdict = await client.shipGate.check({
477
+ * systemId: "11111111-1111-1111-1111-111111111111",
478
+ * attestationId: "build-1234",
479
+ * });
480
+ * if (verdict.gated) {
481
+ * // Shape C or D — build must block.
482
+ * if (verdict.reason === "awaiting_approvers") {
483
+ * // Shape D — list pending approvers in PR comment.
484
+ * console.error(
485
+ * `Awaiting approval from ${verdict.approvers_pending?.join(", ")}`,
486
+ * );
487
+ * } else {
488
+ * // Shape C — rejected or timed_out.
489
+ * console.error(`Build blocked: ${verdict.reason}`);
490
+ * }
491
+ * process.exit(1);
492
+ * }
493
+ * // Shape A (no gate) or Shape B (released) — build proceeds.
494
+ * console.log("OK to deploy.");
495
+ * ```
496
+ *
497
+ * @example Discriminate Shape A vs Shape B (no-gate vs released)
498
+ * ```ts
499
+ * const verdict = await client.shipGate.check({
500
+ * systemId: "11111111-1111-1111-1111-111111111111",
501
+ * attestationId: "build-1234",
502
+ * });
503
+ * if (!verdict.gated) {
504
+ * if (verdict.state === "released") {
505
+ * console.log(`Approved (execution: ${verdict.executionId})`);
506
+ * } else {
507
+ * // No state field → Shape A (no gate exists).
508
+ * console.log("No gate configured for this build.");
509
+ * }
510
+ * }
511
+ * ```
512
+ */
513
+ check(input, options) {
514
+ // Top-level shape — input is REQUIRED. typeof null === "object"
515
+ // and typeof [] === "object", so guard both explicitly.
516
+ if (input === null ||
517
+ typeof input !== "object" ||
518
+ Array.isArray(input)) {
519
+ throw new TypeError("shipGate.check: `input` must be a non-null object with `systemId` and `attestationId`");
520
+ }
521
+ // Snapshot each field's value EXACTLY ONCE up front via the
522
+ // own-property indexer, then operate only on the locals
523
+ // downstream. Three motivations (mirror of gate.ts / check.ts):
524
+ // 1. **Prototype-pollution defense (generalization of #48)**:
525
+ // `Object.prototype.systemId = "<some-uuid>"` (set somewhere
526
+ // else in the consumer's process) does NOT trick the SDK
527
+ // into silently sending the polluted value when the user
528
+ // passes `{}`. Use the module-load snapshot (`objectHasOwn`)
529
+ // so a late-loading dep that overrides the global doesn't
530
+ // defeat the defense.
531
+ // 2. **TOCTOU defense**: a Proxy or getter-defining input could
532
+ // yield DIFFERENT values across multiple reads. Snapshotting
533
+ // once collapses validate-then-send to a single read per
534
+ // field; the validated value is provably the value sent.
535
+ // 3. An explicit `{systemId: "..."}` (no attestationId) is
536
+ // treated as attestationId-omitted — `objectHasOwn` correctly
537
+ // returns false on missing keys.
538
+ const hasSystemId = objectHasOwn(input, "systemId");
539
+ const systemIdRaw = hasSystemId
540
+ ? readInputField(input, "systemId", "shipGate.check")
541
+ : undefined;
542
+ const hasAttestationId = objectHasOwn(input, "attestationId");
543
+ const attestationIdRaw = hasAttestationId
544
+ ? readInputField(input, "attestationId", "shipGate.check")
545
+ : undefined;
546
+ // systemId is REQUIRED. Reject missing-or-undefined first with a
547
+ // clear "required" message; subsequent checks assume present.
548
+ if (!hasSystemId || systemIdRaw === undefined) {
549
+ throw new TypeError("shipGate.check: `systemId` is required");
550
+ }
551
+ if (typeof systemIdRaw !== "string" || systemIdRaw.length === 0) {
552
+ throw new TypeError("shipGate.check: `systemId` must be a non-empty string");
553
+ }
554
+ // UUID format pre-validation (D2 — SDK matches kernel's Zod
555
+ // `z.string().uuid()` closed-spec rule). Mirror of `check.run` /
556
+ // `gate.evaluate`. Drift-pinned in spec-diff round.
557
+ if (!UUID_REGEX.test(systemIdRaw)) {
558
+ throw new TypeError("shipGate.check: `systemId` must be an RFC 4122 hyphenated UUID " +
559
+ "(matched regex: /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-" +
560
+ "[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/)");
561
+ }
562
+ // attestationId is REQUIRED. Length 1-256 inclusive (matches
563
+ // kernel constant MAX_ATTESTATION_ID_LENGTH at
564
+ // src/lib/workflow/ship-gates.ts:106). Drift-pinned in spec-diff.
565
+ if (!hasAttestationId || attestationIdRaw === undefined) {
566
+ throw new TypeError("shipGate.check: `attestationId` is required");
567
+ }
568
+ if (typeof attestationIdRaw !== "string") {
569
+ throw new TypeError(`shipGate.check: \`attestationId\` must be a string ` +
570
+ `(got ${describeType(attestationIdRaw)})`);
571
+ }
572
+ if (attestationIdRaw.length === 0) {
573
+ throw new TypeError("shipGate.check: `attestationId` must be a non-empty string");
574
+ }
575
+ if (attestationIdRaw.length > MAX_ATTESTATION_ID_LENGTH) {
576
+ throw new TypeError(`shipGate.check: \`attestationId\` exceeds the kernel's max ` +
577
+ `length of ${MAX_ATTESTATION_ID_LENGTH} chars ` +
578
+ `(got ${attestationIdRaw.length})`);
579
+ }
580
+ const body = {
581
+ systemId: systemIdRaw,
582
+ attestationId: attestationIdRaw,
583
+ };
584
+ return this.client
585
+ ._request({
586
+ method: "POST",
587
+ path: "/api/v1/ship-gate/check",
588
+ body,
589
+ options,
590
+ })
591
+ .then((result) => validateShipGateCheckResponse(result));
592
+ }
593
+ }
594
+ /**
595
+ * P2 hardening: validate the `check()` response's anchor field
596
+ * (`gated`) + each of the 5 optional own-property fields (`reason`,
597
+ * `approvers_pending`, `state`, `executionId`, `chainId`). Symmetric
598
+ * prototype-pollution defense — read EACH field via the module-load
599
+ * `objectHasOwn` snapshot so a hostile npm dep polluting
600
+ * `Object.prototype.<field>` cannot mask a kernel regression that
601
+ * drops the field OR silently inject a polluted value through a
602
+ * present-but-missing-own-property read path.
603
+ *
604
+ * Returns the validated `result` (typed `ShipGateCheckResponse`) on
605
+ * success; throws `AttestryError` on any shape violation. Extracted
606
+ * as a free function so the resource method body stays focused on
607
+ * request construction.
608
+ *
609
+ * **`gated` is the ALWAYS-PRESENT anchor field** — UNCONDITIONAL
610
+ * own-property check. Every response code path (Shapes A, B, C, D)
611
+ * emits `gated`. A missing `gated` is a hard regression signal.
612
+ *
613
+ * **The other 5 fields are OPTIONAL own-properties** — kernel omits
614
+ * them entirely in Shape A (`{gated: false}` 1-field); kernel omits
615
+ * `reason` + `approvers_pending` in Shape B (4 fields); kernel
616
+ * includes all 5 in Shapes C + D (6 fields). Validator checks
617
+ * `objectHasOwn` BEFORE type-checking, so absent-and-untyped is
618
+ * forward-compatible — present-but-wrong-type is the actual
619
+ * regression signal.
620
+ *
621
+ * **Number-field validation is N/A here** — this resource has NO
622
+ * numeric response fields; the validator's footprint is smaller
623
+ * than `audit-log.ts` / `gate.ts`. If a future field is added
624
+ * (e.g., a `slaHoursRemaining: number`), revisit the
625
+ * faithful-courier-on-numbers asymmetry documented in
626
+ * `audit-log.ts:920-940` (typeof === "number" accepts NaN /
627
+ * Infinity, which JSON.parse never produces for JSON wire formats).
628
+ *
629
+ * **Single-field rejection semantics** (carry-forward from
630
+ * audit-log.ts L1 / session-19 review-3 M1 — project convention).
631
+ * The validator checks fields SEQUENTIALLY in declaration order
632
+ * (gated → reason → approvers_pending → state → executionId →
633
+ * chainId) and throws on the FIRST failing field. If a kernel
634
+ * regression drops MULTIPLE fields at once, the consumer sees ONLY
635
+ * the first failing field's diagnostic — they must fix the fixture
636
+ * and re-run to surface the next failure. This matches batch.ts /
637
+ * gate.ts / check.ts / audit-log.ts patterns (project convention
638
+ * for response-shape validators); accumulating into a multi-field
639
+ * message would diverge from the rest of the SDK. Trade-off
640
+ * accepted: consistency-with-project-pattern wins over single-cycle
641
+ * full-diagnostic.
642
+ */
643
+ function validateShipGateCheckResponse(result) {
644
+ if (result === null ||
645
+ typeof result !== "object" ||
646
+ Array.isArray(result)) {
647
+ throw new AttestryError(`shipGate.check: expected an object response from the kernel ` +
648
+ `(got ${describeType(result)})`);
649
+ }
650
+ const obj = result;
651
+ // gated — ALWAYS-PRESENT anchor. UNCONDITIONAL own-property check.
652
+ const gated = objectHasOwn(obj, "gated") ? obj.gated : undefined;
653
+ if (typeof gated !== "boolean") {
654
+ throw new AttestryError(`shipGate.check: expected response.gated to be a boolean ` +
655
+ `(got ${describeType(gated)})`);
656
+ }
657
+ // reason — OPTIONAL own-property. PRESENT only in Shapes C + D.
658
+ if (objectHasOwn(obj, "reason")) {
659
+ const reason = obj.reason;
660
+ if (typeof reason !== "string") {
661
+ throw new AttestryError(`shipGate.check: expected response.reason to be a string ` +
662
+ `when present (got ${describeType(reason)})`);
663
+ }
664
+ }
665
+ // approvers_pending — OPTIONAL own-property (SNAKE_CASE wire name).
666
+ // PRESENT only in Shapes C + D. Each element must be a string.
667
+ if (objectHasOwn(obj, "approvers_pending")) {
668
+ const approversPending = obj.approvers_pending;
669
+ if (!Array.isArray(approversPending)) {
670
+ throw new AttestryError(`shipGate.check: expected response.approvers_pending to be an array ` +
671
+ `when present (got ${describeType(approversPending)})`);
672
+ }
673
+ for (let i = 0; i < approversPending.length; i++) {
674
+ const elem = approversPending[i];
675
+ if (typeof elem !== "string") {
676
+ throw new AttestryError(`shipGate.check: expected response.approvers_pending[${i}] to be a string ` +
677
+ `(got ${describeType(elem)})`);
678
+ }
679
+ }
680
+ }
681
+ // state — OPTIONAL own-property. PRESENT in Shapes B + C + D.
682
+ if (objectHasOwn(obj, "state")) {
683
+ const state = obj.state;
684
+ if (typeof state !== "string") {
685
+ throw new AttestryError(`shipGate.check: expected response.state to be a string ` +
686
+ `when present (got ${describeType(state)})`);
687
+ }
688
+ }
689
+ // executionId — OPTIONAL own-property. PRESENT in Shapes B + C + D.
690
+ if (objectHasOwn(obj, "executionId")) {
691
+ const executionId = obj.executionId;
692
+ if (typeof executionId !== "string") {
693
+ throw new AttestryError(`shipGate.check: expected response.executionId to be a string ` +
694
+ `when present (got ${describeType(executionId)})`);
695
+ }
696
+ }
697
+ // chainId — OPTIONAL own-property. PRESENT in Shapes B + C + D.
698
+ if (objectHasOwn(obj, "chainId")) {
699
+ const chainId = obj.chainId;
700
+ if (typeof chainId !== "string") {
701
+ throw new AttestryError(`shipGate.check: expected response.chainId to be a string ` +
702
+ `when present (got ${describeType(chainId)})`);
703
+ }
704
+ }
705
+ return result;
706
+ }
707
+ /**
708
+ * Human-readable type description for error messages. Distinguishes
709
+ * `null` and `array` from generic `object`. Duplicated in
710
+ * `decisions.ts`, `incidents.ts`, `regulatory-changes.ts`,
711
+ * `compliance-check.ts`, `check.ts`, `gate.ts`, `batch.ts`,
712
+ * `audit-log.ts` per project pattern (small helper, leaf-resource
713
+ * modules, no shared module yet).
714
+ *
715
+ * All branches are reachable through this file's call sites: top-
716
+ * level shape check (null + array + non-object scalar), per-field
717
+ * type guards (each field's `describeType(<wrong type>)` exercised
718
+ * by tests in the build round).
719
+ */
720
+ function describeType(value) {
721
+ if (value === null)
722
+ return "null";
723
+ if (Array.isArray(value))
724
+ return "array";
725
+ return typeof value;
726
+ }
727
+ //# sourceMappingURL=ship-gate.js.map