@attestry/sdk 0.6.0

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