@attestry/sdk 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/README.md +1269 -0
- package/dist/client.d.ts +58 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +74 -0
- package/dist/client.js.map +1 -0
- package/dist/constants.d.ts +7 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +43 -0
- package/dist/constants.js.map +1 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +41 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/lines-parser.d.ts +50 -0
- package/dist/lines-parser.d.ts.map +1 -0
- package/dist/lines-parser.js +211 -0
- package/dist/lines-parser.js.map +1 -0
- package/dist/ndjson-parser.d.ts +57 -0
- package/dist/ndjson-parser.d.ts.map +1 -0
- package/dist/ndjson-parser.js +245 -0
- package/dist/ndjson-parser.js.map +1 -0
- package/dist/resources/abac-policies.d.ts +1034 -0
- package/dist/resources/abac-policies.d.ts.map +1 -0
- package/dist/resources/abac-policies.js +1519 -0
- package/dist/resources/abac-policies.js.map +1 -0
- package/dist/resources/audit-log.d.ts +588 -0
- package/dist/resources/audit-log.d.ts.map +1 -0
- package/dist/resources/audit-log.js +629 -0
- package/dist/resources/audit-log.js.map +1 -0
- package/dist/resources/batch.d.ts +845 -0
- package/dist/resources/batch.d.ts.map +1 -0
- package/dist/resources/batch.js +1074 -0
- package/dist/resources/batch.js.map +1 -0
- package/dist/resources/chat.d.ts +151 -0
- package/dist/resources/chat.d.ts.map +1 -0
- package/dist/resources/chat.js +124 -0
- package/dist/resources/chat.js.map +1 -0
- package/dist/resources/check.d.ts +348 -0
- package/dist/resources/check.d.ts.map +1 -0
- package/dist/resources/check.js +543 -0
- package/dist/resources/check.js.map +1 -0
- package/dist/resources/compliance-check.d.ts +330 -0
- package/dist/resources/compliance-check.d.ts.map +1 -0
- package/dist/resources/compliance-check.js +402 -0
- package/dist/resources/compliance-check.js.map +1 -0
- package/dist/resources/decisions.d.ts +1208 -0
- package/dist/resources/decisions.d.ts.map +1 -0
- package/dist/resources/decisions.js +1362 -0
- package/dist/resources/decisions.js.map +1 -0
- package/dist/resources/evidence-pack.d.ts +1080 -0
- package/dist/resources/evidence-pack.d.ts.map +1 -0
- package/dist/resources/evidence-pack.js +1789 -0
- package/dist/resources/evidence-pack.js.map +1 -0
- package/dist/resources/gate.d.ts +613 -0
- package/dist/resources/gate.d.ts.map +1 -0
- package/dist/resources/gate.js +737 -0
- package/dist/resources/gate.js.map +1 -0
- package/dist/resources/incidents.d.ts +136 -0
- package/dist/resources/incidents.d.ts.map +1 -0
- package/dist/resources/incidents.js +229 -0
- package/dist/resources/incidents.js.map +1 -0
- package/dist/resources/regulatory-changes.d.ts +307 -0
- package/dist/resources/regulatory-changes.d.ts.map +1 -0
- package/dist/resources/regulatory-changes.js +365 -0
- package/dist/resources/regulatory-changes.js.map +1 -0
- package/dist/resources/safe-input-read.d.ts +21 -0
- package/dist/resources/safe-input-read.d.ts.map +1 -0
- package/dist/resources/safe-input-read.js +57 -0
- package/dist/resources/safe-input-read.js.map +1 -0
- package/dist/resources/ship-gate.d.ts +475 -0
- package/dist/resources/ship-gate.d.ts.map +1 -0
- package/dist/resources/ship-gate.js +727 -0
- package/dist/resources/ship-gate.js.map +1 -0
- package/dist/resources/vision.d.ts +540 -0
- package/dist/resources/vision.d.ts.map +1 -0
- package/dist/resources/vision.js +1036 -0
- package/dist/resources/vision.js.map +1 -0
- package/dist/retry.d.ts +103 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/retry.js +224 -0
- package/dist/retry.js.map +1 -0
- package/dist/sse-parser.d.ts +64 -0
- package/dist/sse-parser.d.ts.map +1 -0
- package/dist/sse-parser.js +271 -0
- package/dist/sse-parser.js.map +1 -0
- package/dist/transport.d.ts +142 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +455 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +61 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +44 -0
|
@@ -0,0 +1,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
|