@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,737 @@
|
|
|
1
|
+
// ─── Gate resource ──────────────────────────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Wraps the CI/CD compliance gate surface (session 17):
|
|
4
|
+
//
|
|
5
|
+
// - POST /api/v1/gate Body: {systemId: <UUID>, minScore?: int 0-100,
|
|
6
|
+
// frameworks?: string[], failOnMissingAssessment?: boolean}
|
|
7
|
+
//
|
|
8
|
+
// Fifth non-decisions resource on `@attestry/sdk`. Sibling to
|
|
9
|
+
// `IncidentsResource`, `DecisionsResource`, `ChatResource`,
|
|
10
|
+
// `AuditLogResource`, `RegulatoryChangesResource`,
|
|
11
|
+
// `ComplianceCheckResource`, `CheckResource`. Single public method today
|
|
12
|
+
// (`evaluate`); the resource class exists as the landing pad for future
|
|
13
|
+
// gate methods if/when the kernel adds them (resource-class-per-kernel-
|
|
14
|
+
// resource convention, carry-forward invariant #43).
|
|
15
|
+
//
|
|
16
|
+
// Method name `evaluate` — matches the verb-method convention of
|
|
17
|
+
// `decisions.ingest`, `chat.send`, `auditLog.export`, `check.run`,
|
|
18
|
+
// `complianceCheck.check`. Pass/fail evaluation reads naturally as
|
|
19
|
+
// "evaluate the gate"; alternatives `run` / `check` / `execute` were
|
|
20
|
+
// considered and rejected (the first two clash naming-wise with sibling
|
|
21
|
+
// resources, the third is less idiomatic in the SDK). User-confirmed at
|
|
22
|
+
// session start.
|
|
23
|
+
//
|
|
24
|
+
// **Multi-permission UNION auth scope**: the kernel route gates on
|
|
25
|
+
// `requireApiKeyWithPermission(request, READ_ASSESSMENTS, READ_SYSTEMS)`
|
|
26
|
+
// which is OR semantics — `permissions.ts:53-55` uses `Array.some()`,
|
|
27
|
+
// NOT `.every()`. A key with EITHER permission (or `ADMIN`, or empty
|
|
28
|
+
// permissions for backwards-compat) succeeds. **HTTP 401** for
|
|
29
|
+
// no/invalid API key; **HTTP 403** for an authenticated key that has
|
|
30
|
+
// NEITHER required permission. Pin BOTH branches separately.
|
|
31
|
+
// Carry-forward invariant #45 (same shape as `check.run` —
|
|
32
|
+
// `READ_ASSESSMENTS` listed first in BOTH routes; `Array.some()` is
|
|
33
|
+
// order-insensitive but the kernel error message would echo the order
|
|
34
|
+
// declared).
|
|
35
|
+
//
|
|
36
|
+
// **Second SDK route to PRE-VALIDATE every Zod closed-spec rule
|
|
37
|
+
// synchronously** (first was `check.run`). The kernel uses
|
|
38
|
+
// `parseBody(request, gateSchema)` where `gateSchema` is
|
|
39
|
+
// `z.object({systemId: z.string().uuid(), minScore: z.number().int()
|
|
40
|
+
// .min(0).max(100).default(70), frameworks: z.array(z.string().min(1)
|
|
41
|
+
// .max(100)).max(20).optional(), failOnMissingAssessment: z.boolean()
|
|
42
|
+
// .default(true)})`. The SDK pre-validates EVERY closed-spec rule
|
|
43
|
+
// synchronously (UUID format on systemId, integer + range [0, 100] on
|
|
44
|
+
// minScore, boolean type on failOnMissingAssessment, array length cap
|
|
45
|
+
// + per-element string length on frameworks). The SDK's runtime
|
|
46
|
+
// checks always run regardless of TypeScript types — `as any` casts
|
|
47
|
+
// do NOT bypass them. So 422 from this route reaches consumers ONLY
|
|
48
|
+
// via kernel-side rule changes the SDK hasn't synced to. Codifies
|
|
49
|
+
// invariant #49 (carry-forward) and new invariant candidate #52
|
|
50
|
+
// (closed-default field pre-validation — when the schema has
|
|
51
|
+
// `.default(<value>)`, pre-validate AND omit the field from the body
|
|
52
|
+
// when the consumer omits it so the kernel applies its default).
|
|
53
|
+
//
|
|
54
|
+
// **Asymmetric cross-org error code**: cross-org `systemId` returns
|
|
55
|
+
// **404** (kernel's `and(eq id, eq orgId)` at route.ts:62-75 followed
|
|
56
|
+
// by "System not found or access denied" — mirror of `check.run`,
|
|
57
|
+
// `decisions.retrieve`, and `complianceCheck.check`'s systemId
|
|
58
|
+
// branch). Partial carry-forward of #47 (no orgName twin here, so
|
|
59
|
+
// only the 404 half applies). Note kernel emits the literal string
|
|
60
|
+
// `"System not found or access denied"` (longer than `check.run`'s
|
|
61
|
+
// `"System not found"`) — pin the exact string in the spec-diff drift
|
|
62
|
+
// suite.
|
|
63
|
+
//
|
|
64
|
+
// **Two silent kernel-side truncations** (faithful courier — SDK does
|
|
65
|
+
// NOT mask, new invariant candidate #50 carry-forward):
|
|
66
|
+
// 1. `assessments` row-population capped at 10 (`.limit(10)` at
|
|
67
|
+
// route.ts:85). If the system has >10 assessment rows, the kernel
|
|
68
|
+
// considers only the 10 most recent by `completedAt` DESC. A
|
|
69
|
+
// system with the most-recent completed assessment in position
|
|
70
|
+
// 11+ would be misclassified as "no assessment found". **Tighter
|
|
71
|
+
// cap than `check.run`'s `.limit(100)`** — gate is strictly less
|
|
72
|
+
// defensive against many-assessment systems.
|
|
73
|
+
// 2. `remediationTasks` row-population capped at 100 (`.limit(100)`
|
|
74
|
+
// at route.ts:154). If the assessment has >100 unresolved
|
|
75
|
+
// remediation tasks, the 101st+ are invisible (cap is on
|
|
76
|
+
// row-population BEFORE the filter-to-unresolved step). No
|
|
77
|
+
// `total` field, no `hasMore` cursor.
|
|
78
|
+
// Each documented in JSDoc + README + drift-pinned with ANCHORED
|
|
79
|
+
// regex per session-16 second-review MEDIUM #4 (`.from(schema.X)
|
|
80
|
+
// [\s\S]*?.limit(N)`).
|
|
81
|
+
//
|
|
82
|
+
// **`score` defaults to `null` (NOT 0) when no completed assessment
|
|
83
|
+
// exists** (route.ts:118 + 131 — both no-assessment emit paths emit
|
|
84
|
+
// `score: null`). **Asymmetric with `check.run`** which used `0` as
|
|
85
|
+
// the default for "no completed assessment". Gate's `null` preserves
|
|
86
|
+
// the distinction at the type level and is more consumer-friendly
|
|
87
|
+
// for the CI/CD pipeline use case. Consumers should use `score ===
|
|
88
|
+
// null` (NOT `score === 0`) to detect the no-assessment branch.
|
|
89
|
+
//
|
|
90
|
+
// **`gate` is a STRING ENUM ("pass" | "fail"), NOT a boolean**
|
|
91
|
+
// (route.ts:114, 127, 181). The kernel uses string-enum form; the
|
|
92
|
+
// pre-build session-17 handoff predicted `passed: boolean` but route
|
|
93
|
+
// source contradicts that. The SDK contract uses the string-enum
|
|
94
|
+
// form to match the kernel emit.
|
|
95
|
+
//
|
|
96
|
+
// **`frameworks` filter is substring + case-insensitive
|
|
97
|
+
// (`.toLowerCase().includes()`), NOT exact-equality, NOT OR-overlap**
|
|
98
|
+
// (route.ts:94-96). **Asymmetric with `check.run`'s OR-overlap exact-
|
|
99
|
+
// equality** (route.ts:67-71 there). Consumer passing `["GDPR"]`
|
|
100
|
+
// matches an assessment with frameworks `["EU_GDPR_2024"]`,
|
|
101
|
+
// `["gdpr_compliance_v2"]`, etc. Documented as a kernel surface
|
|
102
|
+
// behavior; faithful courier.
|
|
103
|
+
//
|
|
104
|
+
// **`writeAuditLog` side effect** — gate WRITES one `gate.checked`
|
|
105
|
+
// audit log entry per call (route.ts:104-111 for the no-assessment
|
|
106
|
+
// emit + route.ts:165-178 for the normal emit). **NEW for a read-
|
|
107
|
+
// shaped SDK route**; new invariant candidate #53. Consumers should
|
|
108
|
+
// know each `gate.evaluate(...)` call leaves an auditable trail.
|
|
109
|
+
// Properties of the write: org-scoped, hash-chained (per
|
|
110
|
+
// `src/lib/api.ts:writeAuditLog`); **time-blocking but error-tolerant
|
|
111
|
+
// (NOT fire-and-forget)** — the kernel uses `await writeAuditLog(...)`
|
|
112
|
+
// (route.ts:104, 165) which awaits TWO DB ops inside the function
|
|
113
|
+
// (a SELECT to fetch the previous hash + an INSERT for the new entry,
|
|
114
|
+
// at `src/lib/api.ts:130-159`). The gate request's response latency
|
|
115
|
+
// includes the audit-log write time. **Error semantics are
|
|
116
|
+
// non-blocking**: `writeAuditLog` wraps its body in a try/catch that
|
|
117
|
+
// swallows errors and logs them, so a write FAILURE does NOT fail
|
|
118
|
+
// the gate request. Audit log writes are NOT counted against
|
|
119
|
+
// `decisionsPerMonth` (read-shaped from a quota perspective).
|
|
120
|
+
//
|
|
121
|
+
// **NO URIError defense on body fields** — POST body uses
|
|
122
|
+
// `JSON.stringify`, which handles lone UTF-16 surrogates by emitting
|
|
123
|
+
// them as literal `\uDxxx` escapes. The URIError defect class
|
|
124
|
+
// (carry-forward invariant #32) applies only to query-string paths
|
|
125
|
+
// (`encodeURIComponent`); this route has no query string and a fixed
|
|
126
|
+
// path. `assertEncodableQueryString` is NOT invoked here — explicit
|
|
127
|
+
// asymmetry vs `complianceCheck.check` / decisions / incidents /
|
|
128
|
+
// audit-log / regulatory-changes. Same asymmetry as `check.run`.
|
|
129
|
+
// Documented as D6.
|
|
130
|
+
//
|
|
131
|
+
// **Symmetric prototype-pollution defense** — module-load snapshot of
|
|
132
|
+
// `Object.hasOwn` applied to BOTH input AND response sides
|
|
133
|
+
// (carry-forward of session 16's second-hostile-review MEDIUM #3
|
|
134
|
+
// generalization). Without the response-side defense, a kernel
|
|
135
|
+
// regression that drops a response field combined with a hostile npm
|
|
136
|
+
// dep polluting `Object.prototype.<field>` would let the polluted
|
|
137
|
+
// value pass typeof-check via prototype walk. With the defense,
|
|
138
|
+
// missing own-property → describeType(undefined) → AttestryError. See
|
|
139
|
+
// build-round audit doc D7.
|
|
140
|
+
//
|
|
141
|
+
// Sync JSON request/response: reuses `client._request` and the
|
|
142
|
+
// existing `{success:true, data}` envelope-unwrap (carry-forward
|
|
143
|
+
// invariant #9). NO new SDK primitive needed. Returns
|
|
144
|
+
// `Promise<GateResponse>`.
|
|
145
|
+
import { AttestryError } from "../errors.js";
|
|
146
|
+
import { readInputField } from "./safe-input-read.js";
|
|
147
|
+
// Module-load snapshot of `Object.hasOwn` — defends against a
|
|
148
|
+
// late-loading hostile/buggy npm dependency that overrides the global
|
|
149
|
+
// (e.g., `Object.hasOwn = () => true`). Without the snapshot, the
|
|
150
|
+
// prototype-pollution defenses below use whatever Object.hasOwn the
|
|
151
|
+
// dependency replaced it with at request time. Snapshotting at module
|
|
152
|
+
// load captures the original implementation BEFORE most consumer
|
|
153
|
+
// code has a chance to monkey-patch.
|
|
154
|
+
//
|
|
155
|
+
// Caveat: this is partial. If the hostile dependency is imported
|
|
156
|
+
// BEFORE @attestry/sdk in the consumer's load graph, the snapshot
|
|
157
|
+
// captures the bad version. Consumers ordering imports
|
|
158
|
+
// SDK-then-untrusted-deps benefit; the reverse ordering does not.
|
|
159
|
+
// Combined with `Object.hasOwn` itself being immune to
|
|
160
|
+
// `obj.hasOwnProperty = ...` overrides (per MDN), this gives a
|
|
161
|
+
// layered defense.
|
|
162
|
+
//
|
|
163
|
+
// Mirror of `check.run` / `complianceCheck.check`'s pattern. Used
|
|
164
|
+
// symmetrically on input AND response sides (session-16 second-
|
|
165
|
+
// hostile-review MEDIUM #3 carry-forward — defense on both
|
|
166
|
+
// boundaries).
|
|
167
|
+
const objectHasOwn = Object.hasOwn;
|
|
168
|
+
// UUID format regex — RFC 4122 hyphenated form (8-4-4-4-12 hex,
|
|
169
|
+
// case-insensitive). Matches Zod's `z.string().uuid()` regex
|
|
170
|
+
// effectively. Mirror of `check.run`'s UUID_REGEX. Drift-pinned in
|
|
171
|
+
// `sdk-drift.test.ts` spec-diff round so a kernel-side switch to a
|
|
172
|
+
// different UUID flavor (ULID, KSUID, etc.) fires before consumer
|
|
173
|
+
// regressions.
|
|
174
|
+
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}$/;
|
|
175
|
+
/**
|
|
176
|
+
* `gate` resource — sibling to `IncidentsResource`,
|
|
177
|
+
* `DecisionsResource`, `ChatResource`, `AuditLogResource`,
|
|
178
|
+
* `RegulatoryChangesResource`, `ComplianceCheckResource`,
|
|
179
|
+
* `CheckResource`. Today wraps a single endpoint (`evaluate`); the
|
|
180
|
+
* class is the landing pad for future gate methods if the kernel
|
|
181
|
+
* adds them (resource-class-per-kernel-resource convention, invariant
|
|
182
|
+
* #43).
|
|
183
|
+
*/
|
|
184
|
+
export class GateResource {
|
|
185
|
+
client;
|
|
186
|
+
constructor(client) {
|
|
187
|
+
this.client = client;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Evaluate a CI/CD compliance gate for a single system. Returns a
|
|
191
|
+
* structured pass/fail verdict (string enum `"pass"`/`"fail"`),
|
|
192
|
+
* the score, the threshold, and a list of unresolved compliance
|
|
193
|
+
* gaps. Designed for pipeline integration (CI build logs / GitHub
|
|
194
|
+
* Actions / GitLab CI).
|
|
195
|
+
*
|
|
196
|
+
* **Three emit paths** — the response shape varies by whether a
|
|
197
|
+
* completed assessment was found and the value of
|
|
198
|
+
* `failOnMissingAssessment`:
|
|
199
|
+
* - **Path 1 (normal pass/fail)**: assessment found; `score: number`;
|
|
200
|
+
* all 14 fields present (including `assessmentId`,
|
|
201
|
+
* `assessmentDate`, `gapCount`, `criticalGaps`, `highGaps`).
|
|
202
|
+
* - **Path 2 (fail-on-missing)**: `failOnMissingAssessment=true`
|
|
203
|
+
* (the default) AND no completed assessment; `gate: "fail"`;
|
|
204
|
+
* `score: null`; `gaps: []`; emit-only fields ABSENT.
|
|
205
|
+
* - **Path 3 (pass-on-missing)**: `failOnMissingAssessment=false`
|
|
206
|
+
* AND no completed assessment; `gate: "pass"`; `score: null`;
|
|
207
|
+
* `gaps: []`; emit-only fields ABSENT.
|
|
208
|
+
*
|
|
209
|
+
* **Multi-permission UNION auth scope**: kernel uses
|
|
210
|
+
* `requireApiKeyWithPermission(req, READ_ASSESSMENTS, READ_SYSTEMS)`
|
|
211
|
+
* which is OR semantics (`Array.some()` at
|
|
212
|
+
* `permissions.ts:53-55`). A key with EITHER permission (or
|
|
213
|
+
* `ADMIN`, or null/empty permissions for backwards-compat)
|
|
214
|
+
* succeeds. **HTTP 401** for no/invalid API key, **HTTP 403** for
|
|
215
|
+
* an authenticated key that has NEITHER required permission. Pin
|
|
216
|
+
* BOTH branches separately. Carry-forward invariant #45 (same
|
|
217
|
+
* shape as `check.run`).
|
|
218
|
+
*
|
|
219
|
+
* **Asymmetric cross-org error code** (carry-forward #47, partial):
|
|
220
|
+
* cross-org `systemId` returns **404** — the kernel's
|
|
221
|
+
* `and(eq id, eq orgId)` at route.ts:62-75 collapses cross-org
|
|
222
|
+
* to "System not found or access denied" (mirror of
|
|
223
|
+
* `check.run`'s 404 surface; note kernel emits a LONGER literal
|
|
224
|
+
* string than `check.run`'s `"System not found"`). Consumers
|
|
225
|
+
* writing defensive error-handling logic must recognize: a 404
|
|
226
|
+
* may be "not your org" OR "genuine missing UUID". No 403-via-
|
|
227
|
+
* orgName twin here (no orgName input mode).
|
|
228
|
+
*
|
|
229
|
+
* **Two silent kernel-side truncations** (faithful courier;
|
|
230
|
+
* documented as kernel surface gaps — JSDoc + README + drift
|
|
231
|
+
* pinned with ANCHORED regex per session-16 second-review
|
|
232
|
+
* MEDIUM #4). Invariant candidate #50:
|
|
233
|
+
* 1. `assessments` row-population — `.limit(10)` at route.ts:85.
|
|
234
|
+
* If the system has >10 assessment rows, the kernel only
|
|
235
|
+
* considers the 10 most recent by `completedAt` DESC. The
|
|
236
|
+
* "relevant" completed assessment is found by `.find()` over
|
|
237
|
+
* those 10 — a system with the most-recent completed
|
|
238
|
+
* assessment in position 11+ would be misclassified as "no
|
|
239
|
+
* assessment found" (falling into Paths 2 or 3). **Tighter
|
|
240
|
+
* cap than `check.run`'s `.limit(100)`** — gate is strictly
|
|
241
|
+
* less defensive against many-assessment systems.
|
|
242
|
+
* 2. `remediationTasks` row-population — `.limit(100)` at
|
|
243
|
+
* route.ts:154. If the assessment has >100 unresolved
|
|
244
|
+
* remediation tasks, the 101st+ are invisible. The cap
|
|
245
|
+
* applies BEFORE the filter-to-unresolved step
|
|
246
|
+
* (`status !== "resolved" && status !== "wont_fix"`), so the
|
|
247
|
+
* final `gaps.length` may be less than 100 even at the cap.
|
|
248
|
+
*
|
|
249
|
+
* **`score` defaults to `null` in the no-assessment paths**
|
|
250
|
+
* (route.ts:118 + 131). **Asymmetric with `check.run` which used
|
|
251
|
+
* `0`** — gate's `null` preserves the distinction at the type
|
|
252
|
+
* level. Consumers should use `score === null` (NOT `score === 0`)
|
|
253
|
+
* to detect Paths 2 or 3.
|
|
254
|
+
*
|
|
255
|
+
* **`gate` is a STRING ENUM, NOT a boolean** — kernel emits the
|
|
256
|
+
* literal strings `"pass"` and `"fail"` (route.ts:114, 127, 181).
|
|
257
|
+
* Type-narrowing via equality check: `if (result.gate === "pass")`.
|
|
258
|
+
*
|
|
259
|
+
* **`frameworks` filter is substring + case-insensitive** — kernel
|
|
260
|
+
* uses `.toLowerCase().includes()` at route.ts:94-96. **Asymmetric
|
|
261
|
+
* with `check.run`'s exact-equality OR-overlap**. Consumer passing
|
|
262
|
+
* `["GDPR"]` matches an assessment with `["EU_GDPR_2024"]`,
|
|
263
|
+
* `["gdpr_compliance_v2"]`, etc. Looser semantics may surprise.
|
|
264
|
+
*
|
|
265
|
+
* **`writeAuditLog` side effect** — every `gate.evaluate(...)`
|
|
266
|
+
* call writes one `gate.checked` entry to the org's audit log
|
|
267
|
+
* (route.ts:104-111 for the no-assessment paths, route.ts:165-178
|
|
268
|
+
* for the normal path). Properties of the write:
|
|
269
|
+
* - Org-scoped, hash-chained (per `writeAuditLog` at
|
|
270
|
+
* `src/lib/api.ts:125-`).
|
|
271
|
+
* - **Time-blocking** but error-tolerant: the kernel uses
|
|
272
|
+
* `await writeAuditLog(...)`, which awaits two DB ops (SELECT
|
|
273
|
+
* previous-hash + INSERT new entry). The gate response latency
|
|
274
|
+
* INCLUDES the audit-log write time — a slow audit-log DB will
|
|
275
|
+
* delay every gate.evaluate() response. Error semantics ARE
|
|
276
|
+
* non-blocking: `writeAuditLog` wraps its body in a try/catch
|
|
277
|
+
* that swallows errors and logs them, so a write FAILURE does
|
|
278
|
+
* NOT fail the gate request.
|
|
279
|
+
* - NOT counted against `decisionsPerMonth` quota (gate is read-
|
|
280
|
+
* shaped from a quota perspective).
|
|
281
|
+
*
|
|
282
|
+
* **Defaults applied by the kernel when fields are omitted**
|
|
283
|
+
* (carry-forward #44, non-obvious-default-filter pattern):
|
|
284
|
+
* - `minScore` defaults to **70** (Zod `.default(70)` at
|
|
285
|
+
* route.ts:33). Consumers who omit this field get the implicit
|
|
286
|
+
* threshold of 70.
|
|
287
|
+
* - `failOnMissingAssessment` defaults to **true** (Zod
|
|
288
|
+
* `.default(true)` at route.ts:35). Consumers who omit this
|
|
289
|
+
* get strict behavior.
|
|
290
|
+
* The SDK omits these fields from the request body when the
|
|
291
|
+
* consumer omits them, so the kernel applies its defaults
|
|
292
|
+
* (invariant candidate #52).
|
|
293
|
+
*
|
|
294
|
+
* Errors — ordered by kernel firing precedence (rate-limit → auth
|
|
295
|
+
* → Zod body validation → DB lookup → internal). A request with
|
|
296
|
+
* multiple problems surfaces ONLY the highest-precedence one. For
|
|
297
|
+
* example: a request with bad auth AND a malformed body surfaces
|
|
298
|
+
* 401, not 422; a request with valid auth + bad body AND a cross-
|
|
299
|
+
* org systemId surfaces 422, not 404.
|
|
300
|
+
* - `AttestryAPIError` (status 429) — rate limit FIRES FIRST
|
|
301
|
+
* (auto-retried by default — invariant #18; per-IP rate-limit
|
|
302
|
+
* key `v1-gate:${ip}`).
|
|
303
|
+
* - `AttestryAPIError` (status 401) — no API key OR invalid key.
|
|
304
|
+
* Fires AFTER rate-limit but BEFORE input validation.
|
|
305
|
+
* - `AttestryAPIError` (status 403) — authenticated key has
|
|
306
|
+
* NEITHER `READ_ASSESSMENTS` nor `READ_SYSTEMS` (the
|
|
307
|
+
* permission-check branch). Single test case — the union-auth
|
|
308
|
+
* pattern collapses three intuition-suggesting cases to one.
|
|
309
|
+
* - `AttestryAPIError` (status 422) — Zod schema rejection
|
|
310
|
+
* (kernel's `BodyParseError` surface — `parseBody(request,
|
|
311
|
+
* gateSchema)` failed). **Fires BEFORE the systemId/cross-
|
|
312
|
+
* org 404 lookup**, so a request with bad UUID format AND
|
|
313
|
+
* cross-org-correct UUID surfaces 422 (the kernel's Zod
|
|
314
|
+
* `.uuid()` reject), not 404. `apiErr.details` carries the
|
|
315
|
+
* full kernel error body verbatim (the transport does NOT
|
|
316
|
+
* strip the `{success:false, ...}` envelope on error responses
|
|
317
|
+
* — only the `{success:true, data}` envelope on success). The
|
|
318
|
+
* wire shape is: `{success: false, error: "Validation failed.",
|
|
319
|
+
* details: Array<{path: string; message: string}>}` — `error`
|
|
320
|
+
* is the literal string
|
|
321
|
+
* "Validation failed." (with trailing period), `details` is
|
|
322
|
+
* an array (NOT a keyed map) of `{path, message}` pairs
|
|
323
|
+
* derived from Zod's `result.error.errors`. Consumers
|
|
324
|
+
* reading field-by-field errors should iterate
|
|
325
|
+
* `apiErr.details.details` (the kernel's `details` array
|
|
326
|
+
* nested under the SDK's parsed-body wrapper). **The SDK
|
|
327
|
+
* pre-validates all closed-spec rules** (UUID format,
|
|
328
|
+
* minScore int + range, failOnMissingAssessment boolean,
|
|
329
|
+
* framework element length 1-100, array length ≤20) AND the
|
|
330
|
+
* runtime checks always run regardless of TypeScript types —
|
|
331
|
+
* `as any` casts do NOT bypass them. So 422 reaches consumers
|
|
332
|
+
* ONLY via kernel rule changes the SDK hasn't synced to.
|
|
333
|
+
* Invariant candidate #51.
|
|
334
|
+
* - `AttestryAPIError` (status 404) — system not found OR
|
|
335
|
+
* cross-org systemId (kernel collapses to "System not found
|
|
336
|
+
* or access denied", route.ts:74). Fires AFTER Zod validation
|
|
337
|
+
* (422).
|
|
338
|
+
* - `AttestryAPIError` (status 500) — internal kernel error
|
|
339
|
+
* (scrubbed message via `internalErrorResponse`).
|
|
340
|
+
* - `AttestryError` ("request aborted by caller") — caller-
|
|
341
|
+
* supplied `options.signal` fired (pre-aborted or mid-flight).
|
|
342
|
+
* - `AttestryError` (P2 hardening) — kernel response failed
|
|
343
|
+
* SDK-side shape validation (not an object, wrong type on any
|
|
344
|
+
* field).
|
|
345
|
+
* - `AttestryAPIError` (P3 hardening) — kernel response had a
|
|
346
|
+
* wrong Content-Type (transport-level guard before body
|
|
347
|
+
* parsing).
|
|
348
|
+
* - `TypeError` (synchronous, no fetch issued) — input failed
|
|
349
|
+
* SDK-side validation (null / array / non-object input,
|
|
350
|
+
* missing systemId, invalid UUID format, non-integer minScore,
|
|
351
|
+
* out-of-range minScore, non-boolean failOnMissingAssessment,
|
|
352
|
+
* frameworks array too long, frameworks element wrong type or
|
|
353
|
+
* length).
|
|
354
|
+
*
|
|
355
|
+
* **Notably ABSENT**:
|
|
356
|
+
* - **No 400** — all input validation is Zod (422).
|
|
357
|
+
* - **No 413** — body size limit not explicit.
|
|
358
|
+
* - **No 402** — read-shaped, doesn't count against
|
|
359
|
+
* decisionsPerMonth quota (despite the audit-log side effect).
|
|
360
|
+
*
|
|
361
|
+
* **SDK-side validation** (synchronous `TypeError`, no fetch
|
|
362
|
+
* issued):
|
|
363
|
+
* - `input` itself: required; must be a non-null, non-array
|
|
364
|
+
* object.
|
|
365
|
+
* - `input.systemId`: required own-property (Object.hasOwn
|
|
366
|
+
* defends against prototype pollution lying about presence —
|
|
367
|
+
* generalization of invariant #48); must be a non-empty
|
|
368
|
+
* string; must match the RFC 4122 hyphenated UUID format
|
|
369
|
+
* (D2 — SDK pre-validates closed-spec rule). No
|
|
370
|
+
* lone-surrogate URIError defense (D6 — POST body uses
|
|
371
|
+
* JSON.stringify).
|
|
372
|
+
* - `input.minScore` (when own-property present, value not
|
|
373
|
+
* undefined): must be a `number`, an integer (`Number.isInteger`,
|
|
374
|
+
* which excludes NaN / ±Infinity automatically), and within
|
|
375
|
+
* `[0, 100]` inclusive. Mirrors Zod's
|
|
376
|
+
* `z.number().int().min(0).max(100)` exactly (D3).
|
|
377
|
+
* - `input.failOnMissingAssessment` (when own-property present,
|
|
378
|
+
* value not undefined): must be a `boolean` (`typeof ===
|
|
379
|
+
* "boolean"`). Mirrors Zod's `z.boolean()` exactly (D4).
|
|
380
|
+
* - `input.frameworks` (when own-property present, value not
|
|
381
|
+
* undefined): must be an array of ≤20 strings, each of length
|
|
382
|
+
* 1-100. SDK pre-validates each rule (D5). Array is
|
|
383
|
+
* snapshotted via `Array.from` for TOCTOU defense.
|
|
384
|
+
*
|
|
385
|
+
* **Response-shape validation** (P2 hardening — D8, symmetric
|
|
386
|
+
* defense on response side per D7):
|
|
387
|
+
* - Rejects with `AttestryError` if the kernel response isn't a
|
|
388
|
+
* non-null, non-array object.
|
|
389
|
+
* - Rejects if `gate` isn't a string.
|
|
390
|
+
* - Rejects if `systemId` / `systemName` / `reason` / `timestamp`
|
|
391
|
+
* aren't strings.
|
|
392
|
+
* - Rejects if `score` isn't a number OR null.
|
|
393
|
+
* - Rejects if `minScore` isn't a number.
|
|
394
|
+
* - Rejects if `frameworks` / `gaps` aren't arrays.
|
|
395
|
+
* - Rejects if `assessmentId` (when own-present) isn't a string.
|
|
396
|
+
* - Rejects if `assessmentDate` (when own-present) isn't a
|
|
397
|
+
* string or null.
|
|
398
|
+
* - Rejects if `gapCount` / `criticalGaps` / `highGaps` (when
|
|
399
|
+
* own-present) aren't numbers.
|
|
400
|
+
* - Each response field read goes through the module-load
|
|
401
|
+
* `objectHasOwn` snapshot (symmetric to the input-side
|
|
402
|
+
* prototype-pollution defense — D7 generalized to the response
|
|
403
|
+
* boundary). A hostile npm dep that pollutes
|
|
404
|
+
* `Object.prototype.<field>` cannot mask a kernel regression
|
|
405
|
+
* where the field is missing — the SDK requires the field to
|
|
406
|
+
* be a kernel-emitted own property.
|
|
407
|
+
* - Per-gap-element shape (open-spec strings) is faithful-
|
|
408
|
+
* courier — NOT validated.
|
|
409
|
+
*
|
|
410
|
+
* **Transport-shape validation** (P3 hardening):
|
|
411
|
+
* - Rejects with `AttestryAPIError` if the kernel responds with
|
|
412
|
+
* a non-`application/json` Content-Type.
|
|
413
|
+
*
|
|
414
|
+
* @example Basic gate evaluation (defaults: minScore=70, failOnMissingAssessment=true)
|
|
415
|
+
* ```ts
|
|
416
|
+
* const result = await client.gate.evaluate({
|
|
417
|
+
* systemId: "11111111-1111-1111-1111-111111111111",
|
|
418
|
+
* });
|
|
419
|
+
* if (result.gate === "pass") {
|
|
420
|
+
* console.log("OK to deploy — score:", result.score);
|
|
421
|
+
* } else if (result.score === null) {
|
|
422
|
+
* console.warn("No completed assessment — failing strict-mode gate");
|
|
423
|
+
* } else {
|
|
424
|
+
* // Path 1 fail: emit-only fields are present at runtime, but
|
|
425
|
+
* // typed as optional. Use `??` (or a Path-1 narrowing check on
|
|
426
|
+
* // `assessmentId`) so the example compiles without `!` or `as`.
|
|
427
|
+
* console.warn(
|
|
428
|
+
* `Score ${result.score} below threshold ${result.minScore};`,
|
|
429
|
+
* `${result.gapCount ?? 0} unresolved gaps (${result.criticalGaps ?? 0} critical)`
|
|
430
|
+
* );
|
|
431
|
+
* }
|
|
432
|
+
* ```
|
|
433
|
+
*
|
|
434
|
+
* @example Strict threshold + framework filter
|
|
435
|
+
* ```ts
|
|
436
|
+
* const euOnly = await client.gate.evaluate({
|
|
437
|
+
* systemId: "11111111-1111-1111-1111-111111111111",
|
|
438
|
+
* minScore: 85,
|
|
439
|
+
* frameworks: ["EU_AI_ACT", "ISO_42001"],
|
|
440
|
+
* });
|
|
441
|
+
* ```
|
|
442
|
+
*
|
|
443
|
+
* @example Pre-launch / staging — allow missing assessments
|
|
444
|
+
* ```ts
|
|
445
|
+
* const lenient = await client.gate.evaluate({
|
|
446
|
+
* systemId: "11111111-1111-1111-1111-111111111111",
|
|
447
|
+
* failOnMissingAssessment: false,
|
|
448
|
+
* });
|
|
449
|
+
* // `lenient.gate === "pass"` even without a completed assessment.
|
|
450
|
+
* ```
|
|
451
|
+
*/
|
|
452
|
+
evaluate(input, options) {
|
|
453
|
+
// Top-level shape — input is REQUIRED. typeof null === "object"
|
|
454
|
+
// and typeof [] === "object", so guard both explicitly.
|
|
455
|
+
if (input === null ||
|
|
456
|
+
typeof input !== "object" ||
|
|
457
|
+
Array.isArray(input)) {
|
|
458
|
+
throw new TypeError("gate.evaluate: `input` must be a non-null object with `systemId`");
|
|
459
|
+
}
|
|
460
|
+
// Snapshot each field's value EXACTLY ONCE up front via the
|
|
461
|
+
// own-property indexer, then operate only on the locals
|
|
462
|
+
// downstream. Three motivations:
|
|
463
|
+
// 1. **Prototype-pollution defense (generalization of #48)**:
|
|
464
|
+
// `Object.prototype.systemId = "<some-uuid>"` (set somewhere
|
|
465
|
+
// else in the consumer's process) does NOT trick the SDK
|
|
466
|
+
// into silently sending the polluted value when the user
|
|
467
|
+
// passes `{}`. Use the module-load snapshot (`objectHasOwn`)
|
|
468
|
+
// so a late-loading dep that overrides the global doesn't
|
|
469
|
+
// defeat the defense.
|
|
470
|
+
// 2. **TOCTOU defense**: a Proxy or getter-defining input could
|
|
471
|
+
// yield DIFFERENT values across multiple reads. Snapshotting
|
|
472
|
+
// once collapses validate-then-send to a single read per
|
|
473
|
+
// field; the validated value is provably the value sent.
|
|
474
|
+
// 3. An explicit `{systemId: "..."}` (no other fields) is
|
|
475
|
+
// treated as those-fields-omitted — `objectHasOwn` correctly
|
|
476
|
+
// returns false on missing keys.
|
|
477
|
+
const hasSystemId = objectHasOwn(input, "systemId");
|
|
478
|
+
const systemIdRaw = hasSystemId
|
|
479
|
+
? readInputField(input, "systemId", "gate.evaluate")
|
|
480
|
+
: undefined;
|
|
481
|
+
const hasMinScore = objectHasOwn(input, "minScore");
|
|
482
|
+
const minScoreRaw = hasMinScore
|
|
483
|
+
? readInputField(input, "minScore", "gate.evaluate")
|
|
484
|
+
: undefined;
|
|
485
|
+
const hasFrameworks = objectHasOwn(input, "frameworks");
|
|
486
|
+
const frameworksRaw = hasFrameworks
|
|
487
|
+
? readInputField(input, "frameworks", "gate.evaluate")
|
|
488
|
+
: undefined;
|
|
489
|
+
const hasFailOnMissing = objectHasOwn(input, "failOnMissingAssessment");
|
|
490
|
+
const failOnMissingRaw = hasFailOnMissing
|
|
491
|
+
? readInputField(input, "failOnMissingAssessment", "gate.evaluate")
|
|
492
|
+
: undefined;
|
|
493
|
+
// systemId is REQUIRED. Reject missing-or-undefined first with a
|
|
494
|
+
// clear "required" message; subsequent checks assume present.
|
|
495
|
+
if (!hasSystemId || systemIdRaw === undefined) {
|
|
496
|
+
throw new TypeError("gate.evaluate: `systemId` is required");
|
|
497
|
+
}
|
|
498
|
+
if (typeof systemIdRaw !== "string" || systemIdRaw.length === 0) {
|
|
499
|
+
throw new TypeError("gate.evaluate: `systemId` must be a non-empty string");
|
|
500
|
+
}
|
|
501
|
+
// UUID format pre-validation (D2 — SDK matches kernel's Zod
|
|
502
|
+
// `z.string().uuid()` closed-spec rule). Mirror of `check.run`.
|
|
503
|
+
// Drift-pinned in spec-diff round.
|
|
504
|
+
if (!UUID_REGEX.test(systemIdRaw)) {
|
|
505
|
+
throw new TypeError("gate.evaluate: `systemId` must be an RFC 4122 hyphenated UUID " +
|
|
506
|
+
"(matched regex: /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-" +
|
|
507
|
+
"[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/)");
|
|
508
|
+
}
|
|
509
|
+
// minScore — optional. When provided, MUST mirror Zod's
|
|
510
|
+
// `z.number().int().min(0).max(100)` exactly:
|
|
511
|
+
// - typeof === "number"
|
|
512
|
+
// - Number.isInteger (which already rejects NaN / ±Infinity,
|
|
513
|
+
// since neither is an integer per ECMA-262 spec)
|
|
514
|
+
// - within [0, 100] inclusive
|
|
515
|
+
// (D3 — SDK pre-validates closed-spec rule + closed-default
|
|
516
|
+
// candidate #52.)
|
|
517
|
+
let validatedMinScore;
|
|
518
|
+
if (hasMinScore && minScoreRaw !== undefined) {
|
|
519
|
+
if (typeof minScoreRaw !== "number") {
|
|
520
|
+
throw new TypeError(`gate.evaluate: \`minScore\` must be a number when provided ` +
|
|
521
|
+
`(got ${describeType(minScoreRaw)})`);
|
|
522
|
+
}
|
|
523
|
+
if (!Number.isInteger(minScoreRaw)) {
|
|
524
|
+
throw new TypeError(`gate.evaluate: \`minScore\` must be a finite integer ` +
|
|
525
|
+
`(got ${minScoreRaw})`);
|
|
526
|
+
}
|
|
527
|
+
if (minScoreRaw < 0 || minScoreRaw > 100) {
|
|
528
|
+
throw new TypeError(`gate.evaluate: \`minScore\` must be in the range [0, 100] ` +
|
|
529
|
+
`(got ${minScoreRaw})`);
|
|
530
|
+
}
|
|
531
|
+
validatedMinScore = minScoreRaw;
|
|
532
|
+
}
|
|
533
|
+
// failOnMissingAssessment — optional boolean. When provided, MUST
|
|
534
|
+
// mirror Zod's `z.boolean()` exactly:
|
|
535
|
+
// - typeof === "boolean"
|
|
536
|
+
// (D4 — SDK pre-validates closed-spec rule + closed-default
|
|
537
|
+
// candidate #52. Truthy/falsy non-booleans like 0 / 1 / "true" /
|
|
538
|
+
// null are rejected.)
|
|
539
|
+
let validatedFailOnMissing;
|
|
540
|
+
if (hasFailOnMissing && failOnMissingRaw !== undefined) {
|
|
541
|
+
if (typeof failOnMissingRaw !== "boolean") {
|
|
542
|
+
throw new TypeError(`gate.evaluate: \`failOnMissingAssessment\` must be a boolean ` +
|
|
543
|
+
`when provided (got ${describeType(failOnMissingRaw)})`);
|
|
544
|
+
}
|
|
545
|
+
validatedFailOnMissing = failOnMissingRaw;
|
|
546
|
+
}
|
|
547
|
+
// frameworks — optional. Carry-forward from check.run exactly:
|
|
548
|
+
// - Array (not other iterable).
|
|
549
|
+
// - Length ≤20.
|
|
550
|
+
// - Each element a string of length 1-100.
|
|
551
|
+
// Snapshot via Array.from up front so a Proxy whose `.length` or
|
|
552
|
+
// `[i]` changes between reads can't slip past validation.
|
|
553
|
+
let validatedFrameworks;
|
|
554
|
+
if (hasFrameworks && frameworksRaw !== undefined) {
|
|
555
|
+
if (!Array.isArray(frameworksRaw)) {
|
|
556
|
+
throw new TypeError(`gate.evaluate: \`frameworks\` must be an array when provided ` +
|
|
557
|
+
`(got ${describeType(frameworksRaw)})`);
|
|
558
|
+
}
|
|
559
|
+
const snapshot = Array.from(frameworksRaw);
|
|
560
|
+
if (snapshot.length > 20) {
|
|
561
|
+
throw new TypeError(`gate.evaluate: \`frameworks\` array exceeds the kernel's max ` +
|
|
562
|
+
`length of 20 (got ${snapshot.length})`);
|
|
563
|
+
}
|
|
564
|
+
for (let i = 0; i < snapshot.length; i++) {
|
|
565
|
+
const elem = snapshot[i];
|
|
566
|
+
if (typeof elem !== "string") {
|
|
567
|
+
throw new TypeError(`gate.evaluate: \`frameworks[${i}]\` must be a string ` +
|
|
568
|
+
`(got ${describeType(elem)})`);
|
|
569
|
+
}
|
|
570
|
+
if (elem.length === 0) {
|
|
571
|
+
throw new TypeError(`gate.evaluate: \`frameworks[${i}]\` must be a non-empty string`);
|
|
572
|
+
}
|
|
573
|
+
if (elem.length > 100) {
|
|
574
|
+
throw new TypeError(`gate.evaluate: \`frameworks[${i}]\` exceeds the kernel's max ` +
|
|
575
|
+
`length of 100 chars (got ${elem.length})`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
validatedFrameworks = snapshot;
|
|
579
|
+
}
|
|
580
|
+
// Construct the body. Omit any optional field the consumer
|
|
581
|
+
// omitted — the kernel's Zod schema applies defaults
|
|
582
|
+
// (minScore=70, failOnMissingAssessment=true) when fields are
|
|
583
|
+
// absent. Closed-default invariant candidate #52: consumer
|
|
584
|
+
// omission → kernel default applied.
|
|
585
|
+
const body = {
|
|
586
|
+
systemId: systemIdRaw,
|
|
587
|
+
};
|
|
588
|
+
if (validatedMinScore !== undefined) {
|
|
589
|
+
body.minScore = validatedMinScore;
|
|
590
|
+
}
|
|
591
|
+
if (validatedFrameworks !== undefined) {
|
|
592
|
+
body.frameworks = validatedFrameworks;
|
|
593
|
+
}
|
|
594
|
+
if (validatedFailOnMissing !== undefined) {
|
|
595
|
+
body.failOnMissingAssessment = validatedFailOnMissing;
|
|
596
|
+
}
|
|
597
|
+
return this.client
|
|
598
|
+
._request({
|
|
599
|
+
method: "POST",
|
|
600
|
+
path: "/api/v1/gate",
|
|
601
|
+
body,
|
|
602
|
+
options,
|
|
603
|
+
})
|
|
604
|
+
.then((result) => {
|
|
605
|
+
// P2 hardening: validate every documented field type.
|
|
606
|
+
// Symmetric prototype-pollution defense — read EACH field
|
|
607
|
+
// via `objectHasOwn` so a hostile npm dep polluting
|
|
608
|
+
// `Object.prototype.<field>` cannot mask a kernel regression
|
|
609
|
+
// that drops the field (per session-16 second-hostile-review
|
|
610
|
+
// MEDIUM #3 carry-forward — defense applied on both input AND
|
|
611
|
+
// response boundaries).
|
|
612
|
+
if (result === null ||
|
|
613
|
+
typeof result !== "object" ||
|
|
614
|
+
Array.isArray(result)) {
|
|
615
|
+
throw new AttestryError(`gate.evaluate: expected an object response from the kernel ` +
|
|
616
|
+
`(got ${describeType(result)})`);
|
|
617
|
+
}
|
|
618
|
+
const obj = result;
|
|
619
|
+
// Always-present fields (9 in all 3 emit paths).
|
|
620
|
+
const gate = objectHasOwn(obj, "gate") ? obj.gate : undefined;
|
|
621
|
+
if (typeof gate !== "string") {
|
|
622
|
+
throw new AttestryError(`gate.evaluate: expected response.gate to be a string ` +
|
|
623
|
+
`(got ${describeType(gate)})`);
|
|
624
|
+
}
|
|
625
|
+
const systemId = objectHasOwn(obj, "systemId")
|
|
626
|
+
? obj.systemId
|
|
627
|
+
: undefined;
|
|
628
|
+
if (typeof systemId !== "string") {
|
|
629
|
+
throw new AttestryError(`gate.evaluate: expected response.systemId to be a string ` +
|
|
630
|
+
`(got ${describeType(systemId)})`);
|
|
631
|
+
}
|
|
632
|
+
const systemName = objectHasOwn(obj, "systemName")
|
|
633
|
+
? obj.systemName
|
|
634
|
+
: undefined;
|
|
635
|
+
if (typeof systemName !== "string") {
|
|
636
|
+
throw new AttestryError(`gate.evaluate: expected response.systemName to be a string ` +
|
|
637
|
+
`(got ${describeType(systemName)})`);
|
|
638
|
+
}
|
|
639
|
+
const score = objectHasOwn(obj, "score") ? obj.score : undefined;
|
|
640
|
+
if (score !== null && typeof score !== "number") {
|
|
641
|
+
throw new AttestryError(`gate.evaluate: expected response.score to be a number or null ` +
|
|
642
|
+
`(got ${describeType(score)})`);
|
|
643
|
+
}
|
|
644
|
+
const minScore = objectHasOwn(obj, "minScore")
|
|
645
|
+
? obj.minScore
|
|
646
|
+
: undefined;
|
|
647
|
+
if (typeof minScore !== "number") {
|
|
648
|
+
throw new AttestryError(`gate.evaluate: expected response.minScore to be a number ` +
|
|
649
|
+
`(got ${describeType(minScore)})`);
|
|
650
|
+
}
|
|
651
|
+
const frameworks = objectHasOwn(obj, "frameworks")
|
|
652
|
+
? obj.frameworks
|
|
653
|
+
: undefined;
|
|
654
|
+
if (!Array.isArray(frameworks)) {
|
|
655
|
+
throw new AttestryError(`gate.evaluate: expected response.frameworks to be an array ` +
|
|
656
|
+
`(got ${describeType(frameworks)})`);
|
|
657
|
+
}
|
|
658
|
+
const gaps = objectHasOwn(obj, "gaps") ? obj.gaps : undefined;
|
|
659
|
+
if (!Array.isArray(gaps)) {
|
|
660
|
+
throw new AttestryError(`gate.evaluate: expected response.gaps to be an array ` +
|
|
661
|
+
`(got ${describeType(gaps)})`);
|
|
662
|
+
}
|
|
663
|
+
const reason = objectHasOwn(obj, "reason") ? obj.reason : undefined;
|
|
664
|
+
if (typeof reason !== "string") {
|
|
665
|
+
throw new AttestryError(`gate.evaluate: expected response.reason to be a string ` +
|
|
666
|
+
`(got ${describeType(reason)})`);
|
|
667
|
+
}
|
|
668
|
+
const timestamp = objectHasOwn(obj, "timestamp")
|
|
669
|
+
? obj.timestamp
|
|
670
|
+
: undefined;
|
|
671
|
+
if (typeof timestamp !== "string") {
|
|
672
|
+
throw new AttestryError(`gate.evaluate: expected response.timestamp to be a string ` +
|
|
673
|
+
`(got ${describeType(timestamp)})`);
|
|
674
|
+
}
|
|
675
|
+
// Emit-only fields (5 in Path 1 only). Validate ONLY when
|
|
676
|
+
// own-present; absence is the correct "no assessment" shape
|
|
677
|
+
// (Paths 2 + 3), not an error. Per-field own-property check
|
|
678
|
+
// mirrors the always-present fields' defense pattern.
|
|
679
|
+
if (objectHasOwn(obj, "assessmentId")) {
|
|
680
|
+
const assessmentId = obj.assessmentId;
|
|
681
|
+
if (typeof assessmentId !== "string") {
|
|
682
|
+
throw new AttestryError(`gate.evaluate: expected response.assessmentId to be a string ` +
|
|
683
|
+
`when present (got ${describeType(assessmentId)})`);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
if (objectHasOwn(obj, "assessmentDate")) {
|
|
687
|
+
const assessmentDate = obj.assessmentDate;
|
|
688
|
+
if (assessmentDate !== null &&
|
|
689
|
+
typeof assessmentDate !== "string") {
|
|
690
|
+
throw new AttestryError(`gate.evaluate: expected response.assessmentDate to be a string or null ` +
|
|
691
|
+
`when present (got ${describeType(assessmentDate)})`);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
if (objectHasOwn(obj, "gapCount")) {
|
|
695
|
+
const gapCount = obj.gapCount;
|
|
696
|
+
if (typeof gapCount !== "number") {
|
|
697
|
+
throw new AttestryError(`gate.evaluate: expected response.gapCount to be a number ` +
|
|
698
|
+
`when present (got ${describeType(gapCount)})`);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
if (objectHasOwn(obj, "criticalGaps")) {
|
|
702
|
+
const criticalGaps = obj.criticalGaps;
|
|
703
|
+
if (typeof criticalGaps !== "number") {
|
|
704
|
+
throw new AttestryError(`gate.evaluate: expected response.criticalGaps to be a number ` +
|
|
705
|
+
`when present (got ${describeType(criticalGaps)})`);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
if (objectHasOwn(obj, "highGaps")) {
|
|
709
|
+
const highGaps = obj.highGaps;
|
|
710
|
+
if (typeof highGaps !== "number") {
|
|
711
|
+
throw new AttestryError(`gate.evaluate: expected response.highGaps to be a number ` +
|
|
712
|
+
`when present (got ${describeType(highGaps)})`);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
return result;
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Human-readable type description for error messages. Distinguishes
|
|
721
|
+
* `null` and `array` from generic `object`. Duplicated in
|
|
722
|
+
* `decisions.ts`, `incidents.ts`, `regulatory-changes.ts`,
|
|
723
|
+
* `compliance-check.ts`, `check.ts` per project pattern (small
|
|
724
|
+
* helper, leaf-resource modules, no shared module yet).
|
|
725
|
+
*
|
|
726
|
+
* Every branch is reachable in this file through the multiple call
|
|
727
|
+
* sites (top-level shape, each field type guard, frameworks
|
|
728
|
+
* non-array, frameworks element non-string).
|
|
729
|
+
*/
|
|
730
|
+
function describeType(value) {
|
|
731
|
+
if (value === null)
|
|
732
|
+
return "null";
|
|
733
|
+
if (Array.isArray(value))
|
|
734
|
+
return "array";
|
|
735
|
+
return typeof value;
|
|
736
|
+
}
|
|
737
|
+
//# sourceMappingURL=gate.js.map
|