@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,1519 @@
1
+ // ─── AbacPolicies resource ──────────────────────────────────────────────────
2
+ //
3
+ // Wraps the ABAC (Attribute-Based Access Control) policies surface
4
+ // (Prompt C.3 — session 21):
5
+ //
6
+ // - GET /api/v1/abac-policies list (session 21)
7
+ // - POST /api/v1/abac-policies create (session 21)
8
+ // - GET /api/v1/abac-policies/[id] retrieve (session 22)
9
+ // - PATCH /api/v1/abac-policies/[id] update (session 22)
10
+ // - DELETE /api/v1/abac-policies/[id] delete (session 22)
11
+ //
12
+ // Eighth non-decisions resource on `@attestry/sdk` (after `auditLog`,
13
+ // `regulatoryChanges`, `complianceCheck`, `check`, `gate`, `batch`,
14
+ // `shipGate`). Sibling to all 10 existing resource classes on the SDK.
15
+ // FIRST 5-method CRUD cluster on the SDK — prior multi-method clusters
16
+ // either grew an existing class (`decisions` reached 7 methods over
17
+ // many sessions; `auditLog` reached 2; `batch` shipped with 2) OR
18
+ // shipped a smaller surface. `.list()` and `.create()` shipped in
19
+ // session 21; `.retrieve()`, `.update()`, and `.delete()` complete
20
+ // the cluster in session 22.
21
+ //
22
+ // **Dual-auth admin scope** — the kernel route gates on
23
+ // `requireSessionOrApiKey(request, { sessionRoles: ["admin"],
24
+ // apiKeyPermissions: [API_KEY_PERMISSIONS.ADMIN] })`. The dual-auth
25
+ // helper routes by request header presence: an `x-api-key` header
26
+ // (even empty-string) takes the api-key path; absent header takes
27
+ // the session path. The SDK's transport ALWAYS sends `x-api-key`
28
+ // (constructed at `transport.ts:headers.set("x-api-key", apiKey)`),
29
+ // so the api-key path is the ONLY one reachable from SDK consumers.
30
+ // The session path is exercised by the dashboard, not by the SDK.
31
+ //
32
+ // **NOT the first SDK use of dual-auth** — `auditLog.export`
33
+ // (session 12) and `decisions.verifyChain` (session 19) already use
34
+ // `requireSessionOrApiKey` middleware on the kernel side. The novelty
35
+ // for `abacPolicies` is that it's the first SDK CRUD cluster (5
36
+ // methods) under dual-auth admin, not the first dual-auth use.
37
+ //
38
+ // **Status-code surface — 401 vs 403 distinguished** (asymmetric with
39
+ // the ADMIN-only-route convention from carry-forward invariant #42).
40
+ // The kernel returns:
41
+ // - **HTTP 401** for: no `x-api-key` header (when session is also
42
+ // absent — SDK path is api-key-only, so this is the "no key"
43
+ // case); empty `x-api-key` header (`""`); invalid key (no matching
44
+ // keyHash row in the `apiKeys` table); expired key (`expiresAt <
45
+ // now`).
46
+ // - **HTTP 403** for: a VALID api-key in the org whose `permissions`
47
+ // column does NOT include `ADMIN`. Error message:
48
+ // `"API key lacks required permission. Required: admin. Key has:
49
+ // ..."`. Source: `src/lib/middleware/permissions.ts:57-62`.
50
+ // Pin BOTH branches separately. **`auditLog.export` is the SAME
51
+ // dual-auth surface** — its kernel route uses the identical
52
+ // `requireSessionOrApiKey(request, { sessionRoles: ["admin"],
53
+ // apiKeyPermissions: [API_KEY_PERMISSIONS.ADMIN] })` gate (verified at
54
+ // `src/app/api/v1/audit-log/export/route.ts:66-68`), so it too
55
+ // returns 401 for no/invalid/expired key and 403 for a valid key
56
+ // lacking `ADMIN`. The `audit-log.ts` JSDoc previously claimed "HTTP
57
+ // 401 for both" — a mis-read of the kernel test, which MOCKS
58
+ // `AuthError(401)` and never exercises the real middleware;
59
+ // `audit-log.ts` was corrected in session-22 hostile review #2.
60
+ // Carry-forward invariant #42's "auditLog.export collapses both to
61
+ // 401" premise is corrected by the same review.
62
+ // Established invariant: **dual-auth admin routes surface BOTH 401 AND
63
+ // 403** (verified by reading `requireRole` at `auth.ts:96-110` +
64
+ // `requireApiKeyWithPermission` at `permissions.ts:35-66`).
65
+ //
66
+ // **No pagination on `.list()`** — the response is `{items: AbacPolicy[],
67
+ // count: number}` with NO cursor / nextCursor field. Server-side
68
+ // `listAbacPolicies` caps at `MAX_POLICIES_PER_ORG_FETCH = 200` rows
69
+ // (`src/lib/auth/abac-policies.ts:113`). Consumers calling `.list()`
70
+ // get up to 200 rows verbatim. If an org has >200 policies (rare —
71
+ // the policy table is intended for ~tens of rules), only the lowest
72
+ // 200 by `priority` ASC are returned. Documented kernel surface gap
73
+ // — invariant #50 (silent kernel-side truncation enumeration; the
74
+ // 200 cap is the ONLY truncation on this method).
75
+ //
76
+ // **Audit log side effect — NONE on `.list()`** — read-shaped routes
77
+ // without `writeAuditLog` calls (asymmetric with `gate.evaluate`,
78
+ // `batch.submit`, `shipGate.check` which all write). `.create()` /
79
+ // `.update()` / `.delete()` will write entries (`abac_policy.create`
80
+ // / `.update` / `.delete`) but `.list()` is quiet. The `.retrieve()`
81
+ // method (session 22) will also be quiet.
82
+ //
83
+ // **Symmetric prototype-pollution defense — RESPONSE side only on
84
+ // `.list()`** — module-load snapshot of `Object.hasOwn` applied to
85
+ // the response shape. The input side is N/A (`.list()` takes no
86
+ // input — only `options?: RequestOptions`).
87
+ //
88
+ // Sync JSON request/response: reuses `client._request` and the
89
+ // existing `{success:true, data}` envelope-unwrap (carry-forward
90
+ // invariant #9). NO new SDK primitive needed.
91
+ import { AttestryError } from "../errors.js";
92
+ import { readInputField } from "./safe-input-read.js";
93
+ // ─── Public closed-enum runtime arrays ──────────────────────────────────────
94
+ //
95
+ // Mirrored from kernel `src/lib/auth.ts` (RESOURCES + ACTIONS) and the
96
+ // kernel's Effect type alias. **Object.freeze**'d (P1 hardening — mirror
97
+ // of constants.ts pattern) so a hostile/buggy npm dep cannot mutate the
98
+ // validation arrays at runtime, bypassing the `.includes()` checks in
99
+ // `.create()` pre-validation.
100
+ //
101
+ // Drift-pinned in `src/lib/incidents/__tests__/sdk-drift.test.ts` so any
102
+ // kernel-side addition / rename / reorder lands at the SDK before
103
+ // consumer regressions.
104
+ //
105
+ // Used by `.create()` pre-validation to reject unknown resource /
106
+ // action / effect values synchronously (closed-enum SDK fields always
107
+ // pre-reject — carry-forward invariant #41).
108
+ /**
109
+ * Closed-enum of resources an ABAC policy can target. Runtime mirror
110
+ * of the kernel's `RESOURCES` const at `src/lib/auth.ts:29-40`.
111
+ * Object.freeze'd to prevent runtime mutation.
112
+ */
113
+ export const ABAC_POLICY_RESOURCES = Object.freeze([
114
+ "systems",
115
+ "assessments",
116
+ "documents",
117
+ "attestations",
118
+ "evidence",
119
+ "users",
120
+ "api_keys",
121
+ "audit_log",
122
+ "organization",
123
+ "regulations",
124
+ ]);
125
+ /**
126
+ * Closed-enum of actions an ABAC policy can gate. Runtime mirror of
127
+ * the kernel's `ACTIONS` const at `src/lib/auth.ts:42-48`.
128
+ */
129
+ export const ABAC_POLICY_ACTIONS = Object.freeze([
130
+ "create",
131
+ "read",
132
+ "update",
133
+ "delete",
134
+ "manage",
135
+ ]);
136
+ /**
137
+ * Closed-enum of effects. Runtime mirror of the kernel's `Effect` type
138
+ * alias at `src/lib/auth/abac-policies.ts:77` + the Zod
139
+ * `z.enum(["allow", "deny"]).default("allow")` at
140
+ * `src/lib/auth/abac-policies.ts:756`.
141
+ */
142
+ export const ABAC_POLICY_EFFECTS = Object.freeze(["allow", "deny"]);
143
+ // ─── Public closed-spec bounds (mirror kernel constants) ────────────────────
144
+ //
145
+ // Drift-pinned in `sdk-drift.test.ts` so kernel-side changes surface
146
+ // before consumer regressions. Used by `.create()` for synchronous
147
+ // pre-validation per invariant #49.
148
+ /** Mirror of kernel `MAX_POLICY_NAME_LENGTH = 128` at `abac-policies.ts:115`. */
149
+ const MAX_POLICY_NAME_LENGTH = 128;
150
+ /** Mirror of kernel `description.max(2000)` at `abac-policies.ts:753`. */
151
+ const MAX_POLICY_DESCRIPTION_LENGTH = 2000;
152
+ /** Mirror of kernel `MIN_PRIORITY = 0` at `abac-policies.ts:117`. */
153
+ const MIN_POLICY_PRIORITY = 0;
154
+ /** Mirror of kernel `MAX_PRIORITY = 1000` at `abac-policies.ts:118`. */
155
+ const MAX_POLICY_PRIORITY = 1000;
156
+ // ─── UUID path-segment validation (mirror kernel `badId`) ───────────────────
157
+ //
158
+ // RFC 4122 hyphenated form (8-4-4-4-12 hex, case-insensitive). Mirror
159
+ // of `batch.get` / `gate.evaluate` / `check.run` / `shipGate.check`'s
160
+ // `UUID_REGEX`, AND a runtime mirror of the kernel's `UUID_RE` at
161
+ // `src/app/api/v1/abac-policies/[id]/route.ts:32-33`. The kernel's
162
+ // `badId` helper rejects a malformed id with HTTP 400 "Invalid policy
163
+ // id." BEFORE rate-limit + auth. The SDK pre-validates `id` for
164
+ // `.retrieve()` / `.update()` / `.delete()` (the three id-path
165
+ // methods) so that 400 is reachable only via an `as any` cast or a
166
+ // kernel-side id-flavor change (ULID, KSUID, etc.).
167
+ //
168
+ // **No `encodeURIComponent` on the path segment** — a string matching
169
+ // this regex is ASCII hex digits + hyphens only: every code point is
170
+ // URL-safe AND none can form a lone UTF-16 surrogate, so
171
+ // `encodeURIComponent` could neither throw a `URIError` nor alter the
172
+ // string. The validated `id` is interpolated into the request path
173
+ // raw. Mirror of `batch.get` (asymmetric with `decisions.retrieve`,
174
+ // whose free-form `id` needs `encodePathSegment` + path-traversal +
175
+ // URIError defenses — carry-forward invariant #32 applies only where
176
+ // a segment can actually reach `encodeURIComponent`; a pre-validated
177
+ // UUID cannot). Drift-pinned in `sdk-drift.test.ts`.
178
+ 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}$/;
179
+ // Module-load snapshot of `Object.hasOwn` — defends against a
180
+ // late-loading hostile/buggy npm dependency that overrides the global
181
+ // (e.g., `Object.hasOwn = () => true`). Without the snapshot, the
182
+ // response-side prototype-pollution defense below would use whatever
183
+ // `Object.hasOwn` the dependency replaced at request time.
184
+ //
185
+ // Mirror of `audit-log.ts` / `batch.ts` / `gate.ts` / `check.ts` /
186
+ // `compliance-check.ts` / `ship-gate.ts` pattern. Used on the response
187
+ // side; the input side is N/A for `.list()` (no input fields).
188
+ const objectHasOwn = Object.hasOwn;
189
+ // ─── Resource class ─────────────────────────────────────────────────────────
190
+ /**
191
+ * `abacPolicies` resource — sibling to all 10 existing resource classes
192
+ * on the SDK. The class is the landing pad for the 5-method CRUD
193
+ * cluster; `.list()` is the first method to ship (session 21), with
194
+ * `.create()` / `.retrieve()` / `.update()` / `.delete()` arriving in
195
+ * session 22. Resource-class-per-kernel-resource convention,
196
+ * invariant #43.
197
+ */
198
+ export class AbacPoliciesResource {
199
+ client;
200
+ constructor(client) {
201
+ this.client = client;
202
+ }
203
+ /**
204
+ * List ABAC policies for the caller's org. Returns up to 200 rows
205
+ * ordered by `priority` ASC. No pagination; the cap is a documented
206
+ * kernel surface gap.
207
+ *
208
+ * **Dual-auth admin scope** — the kernel route gates on
209
+ * `requireSessionOrApiKey(request, { sessionRoles: ["admin"],
210
+ * apiKeyPermissions: [API_KEY_PERMISSIONS.ADMIN] })`. The SDK's
211
+ * transport always sends `x-api-key`, so the api-key path is the
212
+ * only one reachable from SDK consumers. An api-key without the
213
+ * `ADMIN` permission returns 403 (NOT 401 — see status code
214
+ * surface below).
215
+ *
216
+ * **Status-code surface — 401 AND 403 distinguished** (verified
217
+ * by reading `src/lib/middleware/auth.ts:96-110` and
218
+ * `src/lib/middleware/permissions.ts:35-66`):
219
+ * - **HTTP 401**: no `x-api-key` header, empty `x-api-key`
220
+ * header (`""`), invalid key (no matching row), expired key.
221
+ * - **HTTP 403**: valid api-key in the org whose `permissions`
222
+ * column does NOT include `ADMIN`. Error message: `"API key
223
+ * lacks required permission. Required: admin. Key has: ..."`.
224
+ * `auditLog.export` shares this EXACT dual-auth surface — its
225
+ * kernel route uses the identical `requireSessionOrApiKey(...
226
+ * sessionRoles:["admin"], apiKeyPermissions:[ADMIN] ...)` gate, so
227
+ * it too returns 401 vs 403 distinctly. (The `audit-log.ts` JSDoc's
228
+ * prior "HTTP 401 for both" claim was a mis-read of the kernel
229
+ * test's mocked `AuthError(401)` — corrected in session-22 hostile
230
+ * review #2.)
231
+ *
232
+ * **No pagination** — `count` is `items.length`, NOT a total org
233
+ * count. Server-side cap is 200 rows by priority ASC. Orgs with
234
+ * >200 policies are silently truncated (documented invariant #50
235
+ * gap; the SDK does NOT auto-paginate this method because the
236
+ * kernel emits no cursor — there's no next-page anchor to follow).
237
+ *
238
+ * **No `writeAuditLog` side effect** — `.list()` is quiet
239
+ * (asymmetric with `gate.evaluate` / `batch.submit` /
240
+ * `shipGate.check` which all write entries).
241
+ *
242
+ * **Symmetric prototype-pollution defense — RESPONSE side only**:
243
+ * the P2 validator uses the module-load `objectHasOwn` snapshot on
244
+ * each response field read. Input side is N/A (`.list()` has no
245
+ * input fields). Mirror of `audit-log.verifyChain` /
246
+ * `regulatoryChanges.list` pattern.
247
+ *
248
+ * Errors — **happy-path precedence ordering** is rate-limit → auth
249
+ * → DB lookup → successResponse. **The 500-catchall is a SEPARATE
250
+ * DIMENSION** — any throwable not matched by the named `instanceof`
251
+ * arms (only `AuthError` here) falls to 500, regardless of where
252
+ * in the happy-path it fired.
253
+ * - `AttestryAPIError` (status 429) — rate limit FIRES FIRST
254
+ * (auto-retried by default — invariant #18; per-IP rate-limit
255
+ * key `abac-policies-list:${ip}` against the
256
+ * `assessmentLimiter` — 30 req / 1-min sliding window).
257
+ * - `AttestryAPIError` (status 401) — no API key OR invalid key
258
+ * OR expired key. Fires AFTER rate-limit.
259
+ * - `AttestryAPIError` (status 403) — valid api-key in the org
260
+ * whose permissions do NOT include `ADMIN`. Distinct branch
261
+ * from 401 — pin BOTH separately per the dual-auth status-
262
+ * code surface.
263
+ * - `AttestryAPIError` (status 500) — internal kernel error
264
+ * (scrubbed message via `internalErrorResponse`). **The 500
265
+ * surface is orthogonal to the precedence list above**: ANY
266
+ * throwable not matched by the route's single `instanceof
267
+ * AuthError` arm falls to 500.
268
+ * - `AttestryError` ("request aborted by caller") — caller-
269
+ * supplied `options.signal` fired.
270
+ * - `AttestryError` (P2 hardening) — kernel response failed
271
+ * SDK-side shape validation (not an object, `items` not an
272
+ * array, `count` not a number).
273
+ * - `AttestryAPIError` (P3 hardening) — kernel response had a
274
+ * wrong Content-Type (transport-level guard before body
275
+ * parsing).
276
+ *
277
+ * **Notably ABSENT**:
278
+ * - **No 400** — no input → nothing to validate.
279
+ * - **No 404** — `.list()` returns `{items: [], count: 0}` on
280
+ * empty result (NOT 404). 404 is only on `.retrieve()` /
281
+ * `.update()` / `.delete()` (session 22).
282
+ *
283
+ * **Response-shape validation** (P2 hardening — symmetric defense
284
+ * on response side per the module-load `objectHasOwn` snapshot;
285
+ * mirror of `regulatoryChanges.list` / `incidents.list` patterns):
286
+ * - Rejects with `AttestryError` if the kernel response isn't
287
+ * a non-null, non-array object.
288
+ * - Rejects if `items` isn't an array.
289
+ * - Rejects if `count` isn't a number.
290
+ * - Per-row item shape NOT validated (faithful courier — P4
291
+ * candidate; matches incidents.list / regulatoryChanges.list).
292
+ *
293
+ * **Transport-shape validation** (P3 hardening):
294
+ * - Rejects with `AttestryAPIError` if the kernel responds with
295
+ * a non-`application/json` Content-Type.
296
+ *
297
+ * @example List all ABAC policies for the caller's org
298
+ * ```ts
299
+ * const { items, count } = await client.abacPolicies.list();
300
+ * console.log(`${count} policies in this org:`);
301
+ * for (const policy of items) {
302
+ * console.log(` ${policy.priority} ${policy.effect} ${policy.action} ${policy.resource}: ${policy.name}`);
303
+ * }
304
+ * ```
305
+ *
306
+ * @example Inspect a policy's condition AST
307
+ * ```ts
308
+ * const { items } = await client.abacPolicies.list();
309
+ * const ownerOnly = items.find((p) => p.name === "owner-only");
310
+ * if (ownerOnly && ownerOnly.condition.op === "attrEq") {
311
+ * console.log(`Compares ${ownerOnly.condition.left} === ${ownerOnly.condition.right}`);
312
+ * }
313
+ * ```
314
+ */
315
+ list(options) {
316
+ return this.client
317
+ ._request({
318
+ method: "GET",
319
+ path: "/api/v1/abac-policies",
320
+ options,
321
+ })
322
+ .then((result) => validateAbacPoliciesListResponse(result));
323
+ }
324
+ /**
325
+ * Create a new ABAC policy in the caller's org. Returns the
326
+ * inserted row on success (HTTP 201).
327
+ *
328
+ * **Dual-auth admin scope** — same as `.list()`: kernel uses
329
+ * `requireSessionOrApiKey(request, { sessionRoles: ["admin"],
330
+ * apiKeyPermissions: [API_KEY_PERMISSIONS.ADMIN] })`. The SDK's
331
+ * transport always sends `x-api-key`, so the api-key path is the
332
+ * only one reachable from SDK consumers. **HTTP 401** for no/
333
+ * invalid/expired key, **HTTP 403** for valid-key-without-ADMIN
334
+ * permission. Pin BOTH branches separately.
335
+ *
336
+ * **FIRST SDK route to return HTTP 201 on success** (NOT 200).
337
+ * The transport unwraps the `{success:true, data}` envelope and
338
+ * returns the body on any 2xx status — consumers receive the
339
+ * created row directly. To inspect the literal HTTP status,
340
+ * consumers must inspect via fetch-instrumented middleware
341
+ * (not exposed by the SDK today; P4 candidate).
342
+ *
343
+ * **FIRST SDK route with HTTP 409 Conflict surface**. The
344
+ * `(org_id, name)` unique constraint trips `AbacPolicyNameConflictError`
345
+ * at the DB layer; the kernel maps to 409 with error message
346
+ * `An ABAC policy named "<name>" already exists in this organization.`.
347
+ * Consumers should branch on `err.status === 409` to render a
348
+ * specific "name taken" UX.
349
+ *
350
+ * **Three-way 422 fan-out — distinct wire shapes per error class**:
351
+ * The kernel's POST handler catch block has THREE 422-mapping arms:
352
+ *
353
+ * 1. **`BodyParseError`** (most common — from `parseBody(request,
354
+ * schema)`) → `{ success: false, error: "Validation failed.",
355
+ * details: Array<{ path: string, message: string }> }`. Raised
356
+ * when Zod's `.strict()` schema rejects (e.g., extra field,
357
+ * wrong type, out-of-range value).
358
+ * 2. **`ZodError`** (DEFENSIVE — DEAD on happy path; `parseBody`
359
+ * catches Zod and converts to `BodyParseError`. The catch arm
360
+ * exists as defense-in-depth if some other code path throws
361
+ * raw `ZodError`) → `{ success: false, error: "Validation
362
+ * failed.", details: ZodIssue[] }` (richer than BodyParseError's
363
+ * mapped form — includes `code`, `expected`, `received`).
364
+ * 3. **`AbacPolicyValidationError`** (REACHABLE — from server-side
365
+ * canonical AST validation in `createAbacPolicy`) → `{ success:
366
+ * false, error: "ABAC policy validation failed: <messages>",
367
+ * details: { errors: string[] } }`. Raised when the condition
368
+ * AST violates depth / clause / value-list / total-node budgets,
369
+ * or has unknown ops / malformed attr paths.
370
+ *
371
+ * SDK surfaces all three uniformly as `AttestryAPIError(422)` —
372
+ * consumers inspect `err.details` to discriminate:
373
+ * - `Array.isArray(err.details)` → BodyParseError OR ZodError
374
+ * (per-field validation failure; iterate for `{path, message}`).
375
+ * - `err.details && Array.isArray(err.details.errors)` →
376
+ * AbacPolicyValidationError (AST violation; iterate `details.errors`
377
+ * for descriptive strings).
378
+ *
379
+ * Drift-pinned in spec-diff round (both wire shapes + the DEAD
380
+ * ZodError catch arm).
381
+ *
382
+ * **Fifth SDK route to PRE-VALIDATE every Zod closed-spec rule
383
+ * synchronously** (after `check.run`, `gate.evaluate`,
384
+ * `batch.submit`, `shipGate.check`). **FIRST SDK route with
385
+ * PARTIAL pre-validation** — the SDK pre-validates all 7 closed-
386
+ * spec fields synchronously (name length, description length-or-
387
+ * null, resource/action/effect closed-enums, priority int +
388
+ * bounds, enabled boolean) AND defers the recursive AST validation
389
+ * on `condition` to the server canonical validator. The condition
390
+ * field is `z.record(z.string(), z.unknown())` at the schema level
391
+ * (OPEN-spec, not pre-validatable without shipping the full
392
+ * recursive validator). Invariant #49 calibration: closed-spec
393
+ * rules ARE pre-validatable; recursive grammar rules are NOT
394
+ * (too expensive to mirror).
395
+ *
396
+ * **`writeAuditLog` side effect — every successful `.create()` call
397
+ * writes one audit entry** with `action: "abac_policy.create"` and
398
+ * `resourceType: "abac_policy"` (kernel route.ts:105-120; both
399
+ * strings drift-pinned). Properties:
400
+ * - Org-scoped, hash-chained.
401
+ * - **Time-blocking** but error-tolerant: kernel uses
402
+ * `await writeAuditLog(...)`; check response latency INCLUDES
403
+ * the audit-log write time.
404
+ * - Write FAILURE does NOT fail the request (try/catch swallows).
405
+ * - NOT counted against `decisionsPerMonth` quota.
406
+ * - **Audit log is NOT written on failed create** (Zod / canonical
407
+ * validation / name conflict all surface BEFORE writeAuditLog).
408
+ *
409
+ * **Kernel-side 30-second timeout** (`maxDuration = 30`). Same as
410
+ * `.list()` and `auditLog.export`; looser than `gate.evaluate` /
411
+ * `shipGate.check`'s 15s. ABAC policy creation does NO heavy
412
+ * computation — the 30s budget is for DB I/O headroom.
413
+ *
414
+ * **Default-applied fields**: `effect` defaults to `"allow"`,
415
+ * `priority` defaults to `100`, `enabled` defaults to `true`. The
416
+ * SDK OMITS these fields from the request body when the consumer
417
+ * omits them (so the kernel applies its default). Per invariant #52.
418
+ *
419
+ * Errors:
420
+ * - `AttestryAPIError` (429) — rate limit (auto-retried; per-IP
421
+ * key `abac-policies-create:${ip}` against `assessmentLimiter`).
422
+ * - `AttestryAPIError` (401) — no/invalid/expired api-key.
423
+ * - `AttestryAPIError` (403) — valid api-key without ADMIN permission.
424
+ * - `AttestryAPIError` (422) — Zod-schema validation OR canonical-
425
+ * validator AST failure. Discriminate via `err.details` shape
426
+ * (see "Three-way 422 fan-out" above).
427
+ * - `AttestryAPIError` (409) — `(org_id, name)` uniqueness
428
+ * conflict.
429
+ * - `AttestryAPIError` (500) — internal kernel error (scrubbed).
430
+ * - `AttestryError` ("request aborted by caller") — `options.signal`
431
+ * fired.
432
+ * - `AttestryError` (P2 hardening) — kernel response failed
433
+ * SDK-side shape validation. Pre-validated per-row shape:
434
+ * non-null object + all 13 fields present and correctly typed.
435
+ * - `AttestryAPIError` (P3 hardening) — wrong Content-Type on
436
+ * response.
437
+ * - `TypeError` (synchronous, no fetch issued) — SDK-side input
438
+ * validation: missing required field, wrong type, out-of-range
439
+ * value, unknown closed-enum value, malformed input.
440
+ *
441
+ * @example Create a simple "owner can edit own assessments" policy
442
+ * ```ts
443
+ * const policy = await client.abacPolicies.create({
444
+ * name: "owner-can-edit-own",
445
+ * description: "Owners can edit their own assessments.",
446
+ * resource: "assessments",
447
+ * action: "update",
448
+ * effect: "allow",
449
+ * condition: {
450
+ * op: "attrEq",
451
+ * left: "principal.id",
452
+ * right: "resource.ownerId",
453
+ * },
454
+ * priority: 100,
455
+ * enabled: true,
456
+ * });
457
+ * console.log(`Created policy ${policy.id}`);
458
+ * ```
459
+ *
460
+ * @example Catch name-conflict (HTTP 409)
461
+ * ```ts
462
+ * try {
463
+ * await client.abacPolicies.create({...});
464
+ * } catch (err) {
465
+ * if (err instanceof AttestryAPIError && err.status === 409) {
466
+ * // Show "name taken" UX
467
+ * }
468
+ * }
469
+ * ```
470
+ *
471
+ * @example Discriminate 422 wire-shape (Zod vs canonical-validator)
472
+ * ```ts
473
+ * try {
474
+ * await client.abacPolicies.create({...});
475
+ * } catch (err) {
476
+ * if (err instanceof AttestryAPIError && err.status === 422) {
477
+ * const details = err.details as unknown;
478
+ * if (Array.isArray(details)) {
479
+ * // Zod field-level failures: [{path, message}, ...]
480
+ * } else if (details && typeof details === "object" &&
481
+ * Array.isArray((details as {errors?: unknown}).errors)) {
482
+ * // Canonical-validator AST failures: {errors: string[]}
483
+ * }
484
+ * }
485
+ * }
486
+ * ```
487
+ */
488
+ create(input, options) {
489
+ // Top-level shape — input is REQUIRED.
490
+ if (input === null ||
491
+ typeof input !== "object" ||
492
+ Array.isArray(input)) {
493
+ throw new TypeError("abacPolicies.create: `input` must be a non-null object with " +
494
+ "`name`, `resource`, `action`, `condition` (and optional " +
495
+ "`description`, `effect`, `priority`, `enabled`)");
496
+ }
497
+ // Snapshot each field's value EXACTLY ONCE up front via the
498
+ // own-property indexer, then operate only on the locals
499
+ // downstream. Four motivations (mirror of ship-gate.ts):
500
+ // 1. Prototype-pollution defense (generalization of #48).
501
+ // 2. TOCTOU defense — Proxy / getter inputs read once.
502
+ // 3. Missing-key shape: objectHasOwn correctly returns false.
503
+ // 4. Throwing-getter defense — `readInputField` converts a
504
+ // throwing accessor's exception into the documented
505
+ // synchronous `TypeError` input contract (session-22
506
+ // hostile review #1; the SDK-wide MEDIUM-1 fix).
507
+ const hasName = objectHasOwn(input, "name");
508
+ const nameRaw = hasName
509
+ ? readInputField(input, "name", "abacPolicies.create")
510
+ : undefined;
511
+ const hasDescription = objectHasOwn(input, "description");
512
+ const descriptionRaw = hasDescription
513
+ ? readInputField(input, "description", "abacPolicies.create")
514
+ : undefined;
515
+ const hasResource = objectHasOwn(input, "resource");
516
+ const resourceRaw = hasResource
517
+ ? readInputField(input, "resource", "abacPolicies.create")
518
+ : undefined;
519
+ const hasAction = objectHasOwn(input, "action");
520
+ const actionRaw = hasAction
521
+ ? readInputField(input, "action", "abacPolicies.create")
522
+ : undefined;
523
+ const hasEffect = objectHasOwn(input, "effect");
524
+ const effectRaw = hasEffect
525
+ ? readInputField(input, "effect", "abacPolicies.create")
526
+ : undefined;
527
+ const hasCondition = objectHasOwn(input, "condition");
528
+ const conditionRaw = hasCondition
529
+ ? readInputField(input, "condition", "abacPolicies.create")
530
+ : undefined;
531
+ const hasPriority = objectHasOwn(input, "priority");
532
+ const priorityRaw = hasPriority
533
+ ? readInputField(input, "priority", "abacPolicies.create")
534
+ : undefined;
535
+ const hasEnabled = objectHasOwn(input, "enabled");
536
+ const enabledRaw = hasEnabled
537
+ ? readInputField(input, "enabled", "abacPolicies.create")
538
+ : undefined;
539
+ // ─── Required field validation ──────────────────────────────────────────
540
+ // name — REQUIRED non-empty string, length 1-128.
541
+ if (!hasName || nameRaw === undefined) {
542
+ throw new TypeError("abacPolicies.create: `name` is required");
543
+ }
544
+ if (typeof nameRaw !== "string") {
545
+ throw new TypeError(`abacPolicies.create: \`name\` must be a string ` +
546
+ `(got ${describeType(nameRaw)})`);
547
+ }
548
+ if (nameRaw.length === 0) {
549
+ throw new TypeError("abacPolicies.create: `name` must be a non-empty string");
550
+ }
551
+ if (nameRaw.length > MAX_POLICY_NAME_LENGTH) {
552
+ throw new TypeError(`abacPolicies.create: \`name\` exceeds the kernel's max length of ` +
553
+ `${MAX_POLICY_NAME_LENGTH} chars (got ${nameRaw.length})`);
554
+ }
555
+ // resource — REQUIRED closed-enum.
556
+ if (!hasResource || resourceRaw === undefined) {
557
+ throw new TypeError("abacPolicies.create: `resource` is required");
558
+ }
559
+ if (typeof resourceRaw !== "string") {
560
+ throw new TypeError(`abacPolicies.create: \`resource\` must be a string ` +
561
+ `(got ${describeType(resourceRaw)})`);
562
+ }
563
+ if (!ABAC_POLICY_RESOURCES.includes(resourceRaw)) {
564
+ throw new TypeError(`abacPolicies.create: \`resource\` must be one of ` +
565
+ `[${ABAC_POLICY_RESOURCES.join(", ")}] (got "${resourceRaw}")`);
566
+ }
567
+ // action — REQUIRED closed-enum.
568
+ if (!hasAction || actionRaw === undefined) {
569
+ throw new TypeError("abacPolicies.create: `action` is required");
570
+ }
571
+ if (typeof actionRaw !== "string") {
572
+ throw new TypeError(`abacPolicies.create: \`action\` must be a string ` +
573
+ `(got ${describeType(actionRaw)})`);
574
+ }
575
+ if (!ABAC_POLICY_ACTIONS.includes(actionRaw)) {
576
+ throw new TypeError(`abacPolicies.create: \`action\` must be one of ` +
577
+ `[${ABAC_POLICY_ACTIONS.join(", ")}] (got "${actionRaw}")`);
578
+ }
579
+ // condition — REQUIRED non-null object (AST validation defers to server).
580
+ if (!hasCondition || conditionRaw === undefined) {
581
+ throw new TypeError("abacPolicies.create: `condition` is required");
582
+ }
583
+ if (conditionRaw === null ||
584
+ typeof conditionRaw !== "object" ||
585
+ Array.isArray(conditionRaw)) {
586
+ throw new TypeError(`abacPolicies.create: \`condition\` must be a non-null object ` +
587
+ `(got ${describeType(conditionRaw)}). The recursive AST grammar ` +
588
+ `is validated server-side by the canonical validator.`);
589
+ }
590
+ // ─── Optional field validation (per invariant #52 — omit when missing) ─
591
+ // description — OPTIONAL string OR null. An explicit `undefined`
592
+ // own-property is treated as omission (consistent with the JSDoc
593
+ // claim "Accepts `string`, `null`, or `undefined` (omitted)" AND
594
+ // with the symmetric pattern used by `effect` / `priority` /
595
+ // `enabled` below). The kernel's Zod is `.optional().nullable()`
596
+ // so `undefined` AND missing-key are both accepted server-side;
597
+ // JSON.stringify drops undefined fields anyway. Pre-rejecting
598
+ // undefined here was a HIGH false-positive: a consumer doing
599
+ // `{...form, description: form.maybeStr}` where `maybeStr: string |
600
+ // undefined` would hit a TypeError despite the JSDoc claim
601
+ // (hostile-review HIGH-1).
602
+ if (hasDescription && descriptionRaw !== undefined) {
603
+ if (descriptionRaw !== null && typeof descriptionRaw !== "string") {
604
+ throw new TypeError(`abacPolicies.create: \`description\` must be a string or null ` +
605
+ `when present (got ${describeType(descriptionRaw)})`);
606
+ }
607
+ if (typeof descriptionRaw === "string" &&
608
+ descriptionRaw.length > MAX_POLICY_DESCRIPTION_LENGTH) {
609
+ throw new TypeError(`abacPolicies.create: \`description\` exceeds the kernel's max ` +
610
+ `length of ${MAX_POLICY_DESCRIPTION_LENGTH} chars ` +
611
+ `(got ${descriptionRaw.length})`);
612
+ }
613
+ }
614
+ // effect — OPTIONAL closed-enum. Default "allow" applied server-side.
615
+ if (hasEffect && effectRaw !== undefined) {
616
+ if (typeof effectRaw !== "string") {
617
+ throw new TypeError(`abacPolicies.create: \`effect\` must be a string when present ` +
618
+ `(got ${describeType(effectRaw)})`);
619
+ }
620
+ if (!ABAC_POLICY_EFFECTS.includes(effectRaw)) {
621
+ throw new TypeError(`abacPolicies.create: \`effect\` must be one of ` +
622
+ `[${ABAC_POLICY_EFFECTS.join(", ")}] (got "${effectRaw}")`);
623
+ }
624
+ }
625
+ // priority — OPTIONAL int [0, 1000]. Default 100 applied server-side.
626
+ if (hasPriority && priorityRaw !== undefined) {
627
+ if (typeof priorityRaw !== "number" || !Number.isFinite(priorityRaw)) {
628
+ throw new TypeError(`abacPolicies.create: \`priority\` must be a finite number when ` +
629
+ `present (got ${describeType(priorityRaw)})`);
630
+ }
631
+ if (!Number.isInteger(priorityRaw)) {
632
+ throw new TypeError(`abacPolicies.create: \`priority\` must be an integer when ` +
633
+ `present (got ${priorityRaw})`);
634
+ }
635
+ if (priorityRaw < MIN_POLICY_PRIORITY ||
636
+ priorityRaw > MAX_POLICY_PRIORITY) {
637
+ throw new TypeError(`abacPolicies.create: \`priority\` must be in range ` +
638
+ `[${MIN_POLICY_PRIORITY}, ${MAX_POLICY_PRIORITY}] ` +
639
+ `(got ${priorityRaw})`);
640
+ }
641
+ }
642
+ // enabled — OPTIONAL boolean. Default true applied server-side.
643
+ if (hasEnabled && enabledRaw !== undefined) {
644
+ if (typeof enabledRaw !== "boolean") {
645
+ throw new TypeError(`abacPolicies.create: \`enabled\` must be a boolean when present ` +
646
+ `(got ${describeType(enabledRaw)})`);
647
+ }
648
+ }
649
+ // ─── Body construction (omit defaults so kernel applies them — #52) ────
650
+ const body = {
651
+ name: nameRaw,
652
+ resource: resourceRaw,
653
+ action: actionRaw,
654
+ condition: conditionRaw,
655
+ };
656
+ if (hasDescription && descriptionRaw !== undefined) {
657
+ // Pass through both string and null verbatim (caller's intent).
658
+ // Explicit `undefined` is treated as omission per the JSDoc
659
+ // contract — JSON.stringify would drop the key anyway, but the
660
+ // symmetric guard with effect/priority/enabled keeps the body
661
+ // construction predictable (hostile-review HIGH-1).
662
+ body.description = descriptionRaw;
663
+ }
664
+ if (hasEffect && effectRaw !== undefined) {
665
+ body.effect = effectRaw;
666
+ }
667
+ if (hasPriority && priorityRaw !== undefined) {
668
+ body.priority = priorityRaw;
669
+ }
670
+ if (hasEnabled && enabledRaw !== undefined) {
671
+ body.enabled = enabledRaw;
672
+ }
673
+ return this.client
674
+ ._request({
675
+ method: "POST",
676
+ path: "/api/v1/abac-policies",
677
+ body,
678
+ options,
679
+ })
680
+ .then((result) => validateAbacPolicy(result, "abacPolicies.create"));
681
+ }
682
+ /**
683
+ * Retrieve one ABAC policy by id. Returns the policy row.
684
+ *
685
+ * **FIRST `abacPolicies` method with a UUID path segment** — `id`
686
+ * is interpolated into the request path
687
+ * (`/api/v1/abac-policies/<id>`). `.list()` / `.create()` hit the
688
+ * collection path with no segment; `.update()` / `.delete()` share
689
+ * this id-path shape.
690
+ *
691
+ * **Dual-auth admin scope** — same as `.list()` / `.create()`: the
692
+ * kernel route gates on `requireSessionOrApiKey(request, {
693
+ * sessionRoles: ["admin"], apiKeyPermissions:
694
+ * [API_KEY_PERMISSIONS.ADMIN] })`. The SDK transport always sends
695
+ * `x-api-key`, so the api-key path is the only one reachable from
696
+ * SDK consumers. **HTTP 401** for no/invalid/expired key, **HTTP
697
+ * 403** for a valid key whose permissions do NOT include `ADMIN`.
698
+ * Pin BOTH branches separately (asymmetric with carry-forward
699
+ * invariant #42's ADMIN-only 401-collapse).
700
+ *
701
+ * **UUID pre-validation** — the SDK pre-validates `id` against
702
+ * `UUID_REGEX` synchronously (`TypeError`, NO fetch issued) before
703
+ * constructing the URL. The kernel's own `badId` check would
704
+ * return HTTP 400 "Invalid policy id." on a malformed id, but the
705
+ * SDK pre-empts it — that 400 is reachable only via an `as any`
706
+ * cast or a kernel-side id-flavor change. Mirror of `batch.get`.
707
+ *
708
+ * **No `encodeURIComponent` / URIError defense on the path
709
+ * segment** — a string matching `UUID_REGEX` is ASCII hex digits +
710
+ * hyphens, so it is URL-safe verbatim and cannot trigger a
711
+ * `URIError`. The validated `id` is interpolated raw. Asymmetric
712
+ * with `decisions.retrieve` (free-form id → `encodePathSegment`
713
+ * with path-traversal + URIError defenses).
714
+ *
715
+ * **404 surface** — the kernel's `getAbacPolicyById(orgId, id)`
716
+ * returns `null` for a missing id OR a cross-org id (the
717
+ * `eq(orgId)` clause silently filters policies in other orgs), and
718
+ * the GET handler maps that to `errorResponse("ABAC policy not
719
+ * found.", 404)`. **Inline literal message** — distinct from
720
+ * `.update()` / `.delete()`'s 404, which is raised by
721
+ * `AbacPolicyNotFoundError` with the id-embedded message `"ABAC
722
+ * policy <id> not found in this organization."`.
723
+ *
724
+ * **No `writeAuditLog` side effect** — `.retrieve()` is a quiet
725
+ * read (same as `.list()`; asymmetric with `.create()` /
726
+ * `.update()` / `.delete()`, which each write an `abac_policy.*`
727
+ * audit-log entry).
728
+ *
729
+ * **Kernel-side 30-second timeout** (`maxDuration = 30`). Same as
730
+ * `.list()` / `.create()` / `auditLog.export`; looser than
731
+ * `gate.evaluate` / `shipGate.check`'s 15s.
732
+ *
733
+ * Errors — **happy-path precedence ordering**: UUID format (kernel
734
+ * `badId` — SDK-pre-empted) → rate-limit → auth → DB lookup →
735
+ * successResponse. **The 500-catchall is a SEPARATE DIMENSION** —
736
+ * any throwable not matched by the GET handler's single
737
+ * `instanceof AuthError` arm falls to 500.
738
+ * - `AttestryAPIError` (status 429) — rate limit FIRES FIRST
739
+ * among the network surfaces (auto-retried by default —
740
+ * invariant #18; per-IP key `abac-policies-get:${ip}` against
741
+ * `assessmentLimiter` — 30 req / 1-min sliding window).
742
+ * - `AttestryAPIError` (status 401) — no/invalid/expired api-key.
743
+ * - `AttestryAPIError` (status 403) — valid api-key whose
744
+ * permissions do NOT include `ADMIN`. Distinct branch from
745
+ * 401 — pin BOTH separately.
746
+ * - `AttestryAPIError` (status 404) — policy not found OR a
747
+ * cross-org id (kernel collapses both to "ABAC policy not
748
+ * found.").
749
+ * - `AttestryAPIError` (status 400) — kernel `badId` rejected
750
+ * the id. **SDK-pre-empted** — reachable from a consumer only
751
+ * via an `as any` cast or a kernel-side id-flavor change.
752
+ * - `AttestryAPIError` (status 500) — internal kernel error
753
+ * (scrubbed message via `internalErrorResponse`). **Orthogonal
754
+ * to the precedence list** — ANY throwable not matched by the
755
+ * route's single `instanceof AuthError` arm falls here.
756
+ * - `AttestryError` ("request aborted by caller") — caller-
757
+ * supplied `options.signal` fired.
758
+ * - `AttestryError` (P2 hardening) — kernel response failed
759
+ * SDK-side shape validation (non-object, or any of the 13
760
+ * `AbacPolicy` fields missing / wrong-typed).
761
+ * - `AttestryAPIError` (P3 hardening) — kernel response had a
762
+ * wrong Content-Type (transport-level guard before body
763
+ * parsing).
764
+ * - `TypeError` (synchronous, NO fetch issued) — `id` is missing,
765
+ * a non-string, an empty string, or not an RFC 4122 UUID.
766
+ *
767
+ * **SDK-side validation** (synchronous `TypeError`, no fetch
768
+ * issued):
769
+ * - `id`: required; must be a non-empty string matching
770
+ * `UUID_REGEX` (RFC 4122 hyphenated, case-insensitive).
771
+ *
772
+ * **Response-shape validation** (P2 hardening) — the shared
773
+ * `validateAbacPolicy` validator checks all 13 `AbacPolicy` fields
774
+ * via the module-load `objectHasOwn` snapshot (symmetric
775
+ * prototype-pollution defense). `condition` is validated as a
776
+ * non-null object only — the recursive AST is faithful-courier.
777
+ *
778
+ * **Transport-shape validation** (P3 hardening) — rejects with
779
+ * `AttestryAPIError` if the kernel responds with a
780
+ * non-`application/json` Content-Type.
781
+ *
782
+ * @example Retrieve a policy by id
783
+ * ```ts
784
+ * const policy = await client.abacPolicies.retrieve(
785
+ * "550e8400-e29b-41d4-a716-446655440000",
786
+ * );
787
+ * console.log(`${policy.effect} ${policy.action} ${policy.resource}`);
788
+ * ```
789
+ *
790
+ * @example Handle not-found (HTTP 404)
791
+ * ```ts
792
+ * try {
793
+ * return await client.abacPolicies.retrieve(id);
794
+ * } catch (err) {
795
+ * if (err instanceof AttestryAPIError && err.status === 404) {
796
+ * return null; // policy doesn't exist (or belongs to another org)
797
+ * }
798
+ * throw err;
799
+ * }
800
+ * ```
801
+ */
802
+ retrieve(id, options) {
803
+ assertValidPolicyId(id, "abacPolicies.retrieve");
804
+ return this.client
805
+ ._request({
806
+ method: "GET",
807
+ path: `/api/v1/abac-policies/${id}`,
808
+ options,
809
+ })
810
+ .then((result) => validateAbacPolicy(result, "abacPolicies.retrieve"));
811
+ }
812
+ /**
813
+ * Update one ABAC policy by id (PARTIAL update). Returns the
814
+ * **updated row** — the policy as it exists AFTER the patch is
815
+ * applied — on HTTP 200.
816
+ *
817
+ * **SECOND SDK method using the HTTP `PATCH` verb** (`incidents.update`
818
+ * is the first). The kernel route is `PATCH /api/v1/abac-policies/[id]`.
819
+ *
820
+ * **Partial update — every input field is optional.** A consumer
821
+ * patches only the fields they want to change; omitted fields keep
822
+ * their current value. The SDK builds the request body from the
823
+ * present-and-not-`undefined` fields only — an omitted field (or an
824
+ * explicit `field: undefined`) is left out of the body so the kernel
825
+ * leaves that column untouched.
826
+ *
827
+ * **Empty-patch pre-validation.** The kernel's `updateAbacPolicySchema`
828
+ * ends in a `.refine()` rejecting a body with NO updatable field
829
+ * (`"PATCH body must include at least one updatable field"`). The
830
+ * SDK pre-rejects an empty patch — `update(id, {})`, an
831
+ * all-`undefined` patch, or a patch carrying ONLY unknown keys —
832
+ * synchronously with a `TypeError` (NO fetch issued) so the consumer
833
+ * never burns a round-trip on a guaranteed 422.
834
+ *
835
+ * **Dual-auth admin scope** — same as `.list()` / `.create()` /
836
+ * `.retrieve()` / `.delete()`: `requireSessionOrApiKey(request, {
837
+ * sessionRoles: ["admin"], apiKeyPermissions:
838
+ * [API_KEY_PERMISSIONS.ADMIN] })`. **HTTP 401** for no/invalid/
839
+ * expired key, **HTTP 403** for a valid key whose permissions do NOT
840
+ * include `ADMIN`. Pin BOTH branches separately.
841
+ *
842
+ * **UUID pre-validation** — `id` is pre-validated against
843
+ * `UUID_REGEX` synchronously via the shared `assertValidPolicyId`
844
+ * helper (`TypeError`, NO fetch issued). The kernel `badId` 400
845
+ * ("Invalid policy id.") is SDK-pre-empted. **No `encodeURIComponent`
846
+ * / URIError defense** — a validated UUID is ASCII hex + hyphens and
847
+ * is interpolated into the path raw (mirror of `batch.get`).
848
+ *
849
+ * **Partial Zod pre-validation** — the closed-spec fields that ARE
850
+ * present are pre-validated synchronously (name length, description
851
+ * length-or-null, resource/action/effect closed-enums, priority
852
+ * int+bounds, enabled boolean); a present `condition` is checked
853
+ * only as a non-null object, deferring the recursive AST grammar to
854
+ * the kernel's canonical validator. Mirror of `.create()`'s partial
855
+ * pre-validation — but here EVERY field is optional.
856
+ *
857
+ * **`description: null` CLEARS the description.** Passing
858
+ * `description: null` is a valid non-empty patch — the kernel
859
+ * persists `null`. An explicit `description: undefined` is treated
860
+ * as omission (symmetric with `.create()`).
861
+ *
862
+ * **Three-way 422 fan-out — same distinct wire shapes as `.create()`:**
863
+ * 1. **`BodyParseError`** (Zod `.strict()` rejection via
864
+ * `parseBody`) → `details: Array<{ path, message }>`.
865
+ * 2. **`ZodError`** (DEFENSIVE — DEAD on the happy path;
866
+ * `parseBody` catches Zod and converts to `BodyParseError`) →
867
+ * `details: ZodIssue[]`.
868
+ * 3. **`AbacPolicyValidationError`** (server-side canonical AST
869
+ * validation) → `details: { errors: string[] }`.
870
+ * SDK surfaces all three uniformly as `AttestryAPIError(422)` —
871
+ * discriminate via `err.details` (see the `.create()` examples).
872
+ *
873
+ * **HTTP 409 Conflict** — patching `name` to a value already used
874
+ * by a sibling policy in the org trips the `(orgId, name)` unique
875
+ * constraint → `AbacPolicyNameConflictError` → 409.
876
+ *
877
+ * **HTTP 404** — the kernel's `updateAbacPolicy` throws
878
+ * `AbacPolicyNotFoundError` when the `(id, orgId)`-scoped lookup
879
+ * misses (a missing id OR a cross-org id). **The message is
880
+ * id-embedded** — `"ABAC policy <id> not found in this
881
+ * organization."` — same shape as `.delete()`'s 404 (distinct from
882
+ * `.retrieve()`'s INLINE `"ABAC policy not found."`).
883
+ *
884
+ * **6 named-error catch arms — the LARGEST on the SDK**, in order:
885
+ * `AuthError`, `BodyParseError`, `ZodError`,
886
+ * `AbacPolicyValidationError`, `AbacPolicyNameConflictError`,
887
+ * `AbacPolicyNotFoundError`. Everything else falls to the 500
888
+ * `internalErrorResponse` catchall.
889
+ *
890
+ * **`writeAuditLog` side effect** — every successful `.update()`
891
+ * writes one audit-log entry with `action: "abac_policy.update"`
892
+ * and `resourceType: "abac_policy"`; the entry's `details` records
893
+ * the changed field names plus a structured `before`/`after` diff.
894
+ * The write is org-scoped + hash-chained, `await`-ed (so `.update()`
895
+ * latency includes the audit write) but error-tolerant (a write
896
+ * failure does NOT fail the request) and is NOT counted against any
897
+ * quota. The audit log is NOT written on a failed update — 404 /
898
+ * 409 / 422 all surface BEFORE the `writeAuditLog` call.
899
+ *
900
+ * **Kernel-side 30-second timeout** (`maxDuration = 30`). Same as
901
+ * `.list()` / `.create()` / `.retrieve()` / `.delete()`.
902
+ *
903
+ * Errors — **happy-path precedence ordering**: UUID format (kernel
904
+ * `badId` — SDK-pre-empted) → rate-limit → auth → body parse → DB
905
+ * lookup/update → audit write → successResponse.
906
+ * - `AttestryAPIError` (429) — rate limit FIRES FIRST among the
907
+ * network surfaces (auto-retried; per-IP key
908
+ * `abac-policies-patch:${ip}` against `assessmentLimiter`).
909
+ * - `AttestryAPIError` (401) — no/invalid/expired api-key.
910
+ * - `AttestryAPIError` (403) — valid api-key without `ADMIN`.
911
+ * Distinct branch from 401 — pin BOTH separately.
912
+ * - `AttestryAPIError` (422) — Zod-schema validation OR canonical-
913
+ * validator AST failure. Discriminate via `err.details` shape.
914
+ * - `AttestryAPIError` (409) — `(orgId, name)` uniqueness conflict.
915
+ * - `AttestryAPIError` (404) — policy not found OR a cross-org id
916
+ * (`AbacPolicyNotFoundError`; id-embedded message).
917
+ * - `AttestryAPIError` (400) — kernel `badId` rejected the id.
918
+ * **SDK-pre-empted**.
919
+ * - `AttestryAPIError` (500) — internal kernel error (scrubbed).
920
+ * - `AttestryError` ("request aborted by caller") — `options.signal`
921
+ * fired.
922
+ * - `AttestryError` (P2 hardening) — the updated row failed
923
+ * SDK-side shape validation (non-object, or any of the 13
924
+ * `AbacPolicy` fields missing / wrong-typed).
925
+ * - `AttestryAPIError` (P3 hardening) — wrong Content-Type on the
926
+ * response.
927
+ * - `TypeError` (synchronous, NO fetch issued) — invalid `id`;
928
+ * `input` not a non-null object; a present field is the wrong
929
+ * type / out of range / an unknown closed-enum value; OR the
930
+ * patch is empty (no updatable field).
931
+ *
932
+ * @example Patch a single field (the rest of the policy is unchanged)
933
+ * ```ts
934
+ * const updated = await client.abacPolicies.update(
935
+ * "550e8400-e29b-41d4-a716-446655440000",
936
+ * { enabled: false },
937
+ * );
938
+ * console.log(`Policy "${updated.name}" is now ${updated.enabled ? "on" : "off"}`);
939
+ * ```
940
+ *
941
+ * @example Clear a description (pass null) and re-prioritize
942
+ * ```ts
943
+ * await client.abacPolicies.update(id, { description: null, priority: 10 });
944
+ * ```
945
+ *
946
+ * @example Catch a name-conflict (HTTP 409)
947
+ * ```ts
948
+ * try {
949
+ * await client.abacPolicies.update(id, { name: "taken-name" });
950
+ * } catch (err) {
951
+ * if (err instanceof AttestryAPIError && err.status === 409) {
952
+ * // another policy in the org already uses that name
953
+ * }
954
+ * }
955
+ * ```
956
+ */
957
+ update(id, input, options) {
958
+ // `id` first — the path segment is validated before the body,
959
+ // mirror of the kernel PATCH handler (`badId` runs before
960
+ // `parseBody`).
961
+ assertValidPolicyId(id, "abacPolicies.update");
962
+ // Top-level shape — `input` is REQUIRED (a non-null, non-array
963
+ // object). `update(id)` with no second argument lands here too
964
+ // (`undefined` is not an object).
965
+ if (input === null ||
966
+ typeof input !== "object" ||
967
+ Array.isArray(input)) {
968
+ throw new TypeError("abacPolicies.update: `input` must be a non-null object with at " +
969
+ "least one updatable field (`name`, `description`, `resource`, " +
970
+ "`action`, `effect`, `condition`, `priority`, `enabled`)");
971
+ }
972
+ // Snapshot each field's value EXACTLY ONCE up front via the
973
+ // own-property indexer, then operate only on the locals downstream
974
+ // (mirror of `.create()`): prototype-pollution defense, TOCTOU
975
+ // defense (Proxy / getter inputs read once), a correct missing-key
976
+ // shape (objectHasOwn returns false), and the throwing-getter
977
+ // defense (`readInputField` converts a throwing accessor into the
978
+ // documented synchronous `TypeError` — session-22 hostile review
979
+ // #1, the SDK-wide MEDIUM-1 fix).
980
+ const hasName = objectHasOwn(input, "name");
981
+ const nameRaw = hasName
982
+ ? readInputField(input, "name", "abacPolicies.update")
983
+ : undefined;
984
+ const hasDescription = objectHasOwn(input, "description");
985
+ const descriptionRaw = hasDescription
986
+ ? readInputField(input, "description", "abacPolicies.update")
987
+ : undefined;
988
+ const hasResource = objectHasOwn(input, "resource");
989
+ const resourceRaw = hasResource
990
+ ? readInputField(input, "resource", "abacPolicies.update")
991
+ : undefined;
992
+ const hasAction = objectHasOwn(input, "action");
993
+ const actionRaw = hasAction
994
+ ? readInputField(input, "action", "abacPolicies.update")
995
+ : undefined;
996
+ const hasEffect = objectHasOwn(input, "effect");
997
+ const effectRaw = hasEffect
998
+ ? readInputField(input, "effect", "abacPolicies.update")
999
+ : undefined;
1000
+ const hasCondition = objectHasOwn(input, "condition");
1001
+ const conditionRaw = hasCondition
1002
+ ? readInputField(input, "condition", "abacPolicies.update")
1003
+ : undefined;
1004
+ const hasPriority = objectHasOwn(input, "priority");
1005
+ const priorityRaw = hasPriority
1006
+ ? readInputField(input, "priority", "abacPolicies.update")
1007
+ : undefined;
1008
+ const hasEnabled = objectHasOwn(input, "enabled");
1009
+ const enabledRaw = hasEnabled
1010
+ ? readInputField(input, "enabled", "abacPolicies.update")
1011
+ : undefined;
1012
+ // ─── Per-field validation (EVERY field optional — PATCH semantics) ─────
1013
+ // Each block fires only when the field is present AND not
1014
+ // `undefined` — an explicit `undefined` own-property is treated as
1015
+ // omission, the same convention `.create()` applies to its optional
1016
+ // fields.
1017
+ // name — when present: non-empty string, length 1-128.
1018
+ if (hasName && nameRaw !== undefined) {
1019
+ if (typeof nameRaw !== "string") {
1020
+ throw new TypeError(`abacPolicies.update: \`name\` must be a string when present ` +
1021
+ `(got ${describeType(nameRaw)})`);
1022
+ }
1023
+ if (nameRaw.length === 0) {
1024
+ throw new TypeError("abacPolicies.update: `name` must be a non-empty string when present");
1025
+ }
1026
+ if (nameRaw.length > MAX_POLICY_NAME_LENGTH) {
1027
+ throw new TypeError(`abacPolicies.update: \`name\` exceeds the kernel's max length ` +
1028
+ `of ${MAX_POLICY_NAME_LENGTH} chars (got ${nameRaw.length})`);
1029
+ }
1030
+ }
1031
+ // description — when present: string OR null. An explicit
1032
+ // `undefined` is treated as omission; `null` CLEARS the field.
1033
+ if (hasDescription && descriptionRaw !== undefined) {
1034
+ if (descriptionRaw !== null && typeof descriptionRaw !== "string") {
1035
+ throw new TypeError(`abacPolicies.update: \`description\` must be a string or null ` +
1036
+ `when present (got ${describeType(descriptionRaw)})`);
1037
+ }
1038
+ if (typeof descriptionRaw === "string" &&
1039
+ descriptionRaw.length > MAX_POLICY_DESCRIPTION_LENGTH) {
1040
+ throw new TypeError(`abacPolicies.update: \`description\` exceeds the kernel's max ` +
1041
+ `length of ${MAX_POLICY_DESCRIPTION_LENGTH} chars ` +
1042
+ `(got ${descriptionRaw.length})`);
1043
+ }
1044
+ }
1045
+ // resource — when present: closed-enum.
1046
+ if (hasResource && resourceRaw !== undefined) {
1047
+ if (typeof resourceRaw !== "string") {
1048
+ throw new TypeError(`abacPolicies.update: \`resource\` must be a string when present ` +
1049
+ `(got ${describeType(resourceRaw)})`);
1050
+ }
1051
+ if (!ABAC_POLICY_RESOURCES.includes(resourceRaw)) {
1052
+ throw new TypeError(`abacPolicies.update: \`resource\` must be one of ` +
1053
+ `[${ABAC_POLICY_RESOURCES.join(", ")}] (got "${resourceRaw}")`);
1054
+ }
1055
+ }
1056
+ // action — when present: closed-enum.
1057
+ if (hasAction && actionRaw !== undefined) {
1058
+ if (typeof actionRaw !== "string") {
1059
+ throw new TypeError(`abacPolicies.update: \`action\` must be a string when present ` +
1060
+ `(got ${describeType(actionRaw)})`);
1061
+ }
1062
+ if (!ABAC_POLICY_ACTIONS.includes(actionRaw)) {
1063
+ throw new TypeError(`abacPolicies.update: \`action\` must be one of ` +
1064
+ `[${ABAC_POLICY_ACTIONS.join(", ")}] (got "${actionRaw}")`);
1065
+ }
1066
+ }
1067
+ // effect — when present: closed-enum.
1068
+ if (hasEffect && effectRaw !== undefined) {
1069
+ if (typeof effectRaw !== "string") {
1070
+ throw new TypeError(`abacPolicies.update: \`effect\` must be a string when present ` +
1071
+ `(got ${describeType(effectRaw)})`);
1072
+ }
1073
+ if (!ABAC_POLICY_EFFECTS.includes(effectRaw)) {
1074
+ throw new TypeError(`abacPolicies.update: \`effect\` must be one of ` +
1075
+ `[${ABAC_POLICY_EFFECTS.join(", ")}] (got "${effectRaw}")`);
1076
+ }
1077
+ }
1078
+ // condition — when present: non-null object (AST defers to server).
1079
+ if (hasCondition && conditionRaw !== undefined) {
1080
+ if (conditionRaw === null ||
1081
+ typeof conditionRaw !== "object" ||
1082
+ Array.isArray(conditionRaw)) {
1083
+ throw new TypeError(`abacPolicies.update: \`condition\` must be a non-null object ` +
1084
+ `when present (got ${describeType(conditionRaw)}). The ` +
1085
+ `recursive AST grammar is validated server-side by the ` +
1086
+ `canonical validator.`);
1087
+ }
1088
+ }
1089
+ // priority — when present: integer [0, 1000].
1090
+ if (hasPriority && priorityRaw !== undefined) {
1091
+ if (typeof priorityRaw !== "number" || !Number.isFinite(priorityRaw)) {
1092
+ throw new TypeError(`abacPolicies.update: \`priority\` must be a finite number when ` +
1093
+ `present (got ${describeType(priorityRaw)})`);
1094
+ }
1095
+ if (!Number.isInteger(priorityRaw)) {
1096
+ throw new TypeError(`abacPolicies.update: \`priority\` must be an integer when ` +
1097
+ `present (got ${priorityRaw})`);
1098
+ }
1099
+ if (priorityRaw < MIN_POLICY_PRIORITY ||
1100
+ priorityRaw > MAX_POLICY_PRIORITY) {
1101
+ throw new TypeError(`abacPolicies.update: \`priority\` must be in range ` +
1102
+ `[${MIN_POLICY_PRIORITY}, ${MAX_POLICY_PRIORITY}] ` +
1103
+ `(got ${priorityRaw})`);
1104
+ }
1105
+ }
1106
+ // enabled — when present: boolean.
1107
+ if (hasEnabled && enabledRaw !== undefined) {
1108
+ if (typeof enabledRaw !== "boolean") {
1109
+ throw new TypeError(`abacPolicies.update: \`enabled\` must be a boolean when present ` +
1110
+ `(got ${describeType(enabledRaw)})`);
1111
+ }
1112
+ }
1113
+ // ─── Body construction (only present-and-not-undefined fields) ─────────
1114
+ const body = {};
1115
+ if (hasName && nameRaw !== undefined) {
1116
+ body.name = nameRaw;
1117
+ }
1118
+ if (hasDescription && descriptionRaw !== undefined) {
1119
+ // `null` rides through verbatim — it CLEARS the description.
1120
+ body.description = descriptionRaw;
1121
+ }
1122
+ if (hasResource && resourceRaw !== undefined) {
1123
+ body.resource = resourceRaw;
1124
+ }
1125
+ if (hasAction && actionRaw !== undefined) {
1126
+ body.action = actionRaw;
1127
+ }
1128
+ if (hasEffect && effectRaw !== undefined) {
1129
+ body.effect = effectRaw;
1130
+ }
1131
+ if (hasCondition && conditionRaw !== undefined) {
1132
+ body.condition = conditionRaw;
1133
+ }
1134
+ if (hasPriority && priorityRaw !== undefined) {
1135
+ body.priority = priorityRaw;
1136
+ }
1137
+ if (hasEnabled && enabledRaw !== undefined) {
1138
+ body.enabled = enabledRaw;
1139
+ }
1140
+ // ─── Empty-patch pre-validation ────────────────────────────────────────
1141
+ // The kernel `updateAbacPolicySchema` ends in a `.refine()` that
1142
+ // rejects a body carrying no updatable field. Pre-reject the empty
1143
+ // patch synchronously so the consumer gets a `TypeError` (NO fetch
1144
+ // issued) instead of burning a round-trip on a guaranteed 422. An
1145
+ // input of `{}`, an all-`undefined` patch, or a patch carrying only
1146
+ // unknown keys all produce a zero-key `body`.
1147
+ if (Object.keys(body).length === 0) {
1148
+ throw new TypeError("abacPolicies.update: `input` must include at least one updatable " +
1149
+ "field (`name`, `description`, `resource`, `action`, `effect`, " +
1150
+ "`condition`, `priority`, `enabled`) — the kernel rejects an " +
1151
+ "empty patch");
1152
+ }
1153
+ return this.client
1154
+ ._request({
1155
+ method: "PATCH",
1156
+ path: `/api/v1/abac-policies/${id}`,
1157
+ body,
1158
+ options,
1159
+ })
1160
+ .then((result) => validateAbacPolicy(result, "abacPolicies.update"));
1161
+ }
1162
+ /**
1163
+ * Delete one ABAC policy by id. Returns the **deleted row** — the
1164
+ * policy as it existed immediately before deletion — on HTTP 200.
1165
+ *
1166
+ * **Returns the deleted row, NOT `void`.** The kernel's DELETE
1167
+ * handler emits `successResponse(row, 200)` carrying the
1168
+ * just-deleted `AbacPolicy`, so a caller can log / audit / render
1169
+ * an undo affordance with the full prior state. Consumers MUST NOT
1170
+ * expect `Promise<void>` or a `{ deleted: true }` envelope — the
1171
+ * resolved value is a complete `AbacPolicy`.
1172
+ *
1173
+ * **FIRST SDK method using the HTTP `DELETE` verb.** Every prior
1174
+ * SDK route is GET / POST / PATCH. The transport's
1175
+ * `InternalRequestArgs.method` union already includes `"DELETE"`,
1176
+ * so no new transport primitive is needed.
1177
+ *
1178
+ * **Dual-auth admin scope** — same as `.list()` / `.create()` /
1179
+ * `.retrieve()`: `requireSessionOrApiKey(request, { sessionRoles:
1180
+ * ["admin"], apiKeyPermissions: [API_KEY_PERMISSIONS.ADMIN] })`.
1181
+ * **HTTP 401** for no/invalid/expired key, **HTTP 403** for a valid
1182
+ * key whose permissions do NOT include `ADMIN`. Pin BOTH branches.
1183
+ *
1184
+ * **UUID pre-validation** — `id` is pre-validated against
1185
+ * `UUID_REGEX` synchronously (`TypeError`, NO fetch issued) via the
1186
+ * shared `assertValidPolicyId` helper. The kernel `badId` 400
1187
+ * ("Invalid policy id.") is SDK-pre-empted. **No `encodeURIComponent`
1188
+ * / URIError defense** — a validated UUID is ASCII hex + hyphens and
1189
+ * is interpolated into the path raw (see `UUID_REGEX`; mirror of
1190
+ * `batch.get`).
1191
+ *
1192
+ * **404 surface** — the kernel's `deleteAbacPolicy(orgId, id)`
1193
+ * throws `AbacPolicyNotFoundError` when the `(id, orgId)`-scoped
1194
+ * delete matches zero rows (a missing id OR a cross-org id — the
1195
+ * `eq(orgId)` clause scopes the delete). The DELETE handler maps it
1196
+ * to `errorResponse(error.message, 404)`. **The message is
1197
+ * id-embedded** — `"ABAC policy <id> not found in this
1198
+ * organization."` — distinct from `.retrieve()`'s INLINE
1199
+ * `"ABAC policy not found."`. (`.retrieve()` uses an inline string;
1200
+ * `.update()` / `.delete()` raise `AbacPolicyNotFoundError`.)
1201
+ *
1202
+ * **`writeAuditLog` side effect — every successful `.delete()` call
1203
+ * writes one audit-log entry** with `action: "abac_policy.delete"`
1204
+ * and `resourceType: "abac_policy"`. The entry's `details` records
1205
+ * the deleted policy's `name` / `resource` / `action` / `effect`
1206
+ * for forensics. The write is org-scoped + hash-chained; it is
1207
+ * `await`-ed (so `.delete()` latency includes the audit write) but
1208
+ * error-tolerant (a write failure does NOT fail the request); it is
1209
+ * NOT counted against any quota. The audit log is NOT written on a
1210
+ * failed delete — a 404 surfaces BEFORE the `writeAuditLog` call.
1211
+ *
1212
+ * **Kernel-side 30-second timeout** (`maxDuration = 30`). Same as
1213
+ * `.list()` / `.create()` / `.retrieve()`.
1214
+ *
1215
+ * Errors — **happy-path precedence ordering**: UUID format (kernel
1216
+ * `badId` — SDK-pre-empted) → rate-limit → auth → DB delete → audit
1217
+ * write → successResponse. **The 500-catchall is a SEPARATE
1218
+ * DIMENSION** — any throwable not matched by the DELETE handler's 2
1219
+ * `instanceof` arms (`AuthError`, `AbacPolicyNotFoundError`) falls
1220
+ * to 500.
1221
+ * - `AttestryAPIError` (status 429) — rate limit FIRES FIRST
1222
+ * among the network surfaces (auto-retried by default —
1223
+ * invariant #18; per-IP key `abac-policies-delete:${ip}`
1224
+ * against `assessmentLimiter` — 30 req / 1-min sliding window).
1225
+ * - `AttestryAPIError` (status 401) — no/invalid/expired api-key.
1226
+ * - `AttestryAPIError` (status 403) — valid api-key without
1227
+ * `ADMIN`. Distinct branch from 401 — pin BOTH separately.
1228
+ * - `AttestryAPIError` (status 404) — policy not found OR a
1229
+ * cross-org id (`AbacPolicyNotFoundError`; id-embedded message).
1230
+ * - `AttestryAPIError` (status 400) — kernel `badId` rejected the
1231
+ * id. **SDK-pre-empted** — reachable from a consumer only via
1232
+ * an `as any` cast or a kernel-side id-flavor change.
1233
+ * - `AttestryAPIError` (status 500) — internal kernel error
1234
+ * (scrubbed message via `internalErrorResponse`). **Orthogonal
1235
+ * to the precedence list** — ANY throwable not matched by the
1236
+ * route's 2 `instanceof` arms falls here.
1237
+ * - `AttestryError` ("request aborted by caller") — caller-
1238
+ * supplied `options.signal` fired.
1239
+ * - `AttestryError` (P2 hardening) — the deleted row failed
1240
+ * SDK-side shape validation (non-object, or any of the 13
1241
+ * `AbacPolicy` fields missing / wrong-typed).
1242
+ * - `AttestryAPIError` (P3 hardening) — kernel response had a
1243
+ * wrong Content-Type (transport-level guard before body
1244
+ * parsing).
1245
+ * - `TypeError` (synchronous, NO fetch issued) — `id` is missing,
1246
+ * a non-string, an empty string, or not an RFC 4122 UUID.
1247
+ *
1248
+ * **SDK-side validation** (synchronous `TypeError`, no fetch
1249
+ * issued):
1250
+ * - `id`: required; must be a non-empty string matching
1251
+ * `UUID_REGEX` (RFC 4122 hyphenated, case-insensitive).
1252
+ *
1253
+ * **Response-shape validation** (P2 hardening) — the shared
1254
+ * `validateAbacPolicy` validator checks all 13 `AbacPolicy` fields
1255
+ * of the deleted row via the module-load `objectHasOwn` snapshot.
1256
+ * `condition` is validated as a non-null object only — the
1257
+ * recursive AST is faithful-courier.
1258
+ *
1259
+ * **Transport-shape validation** (P3 hardening) — rejects with
1260
+ * `AttestryAPIError` if the kernel responds with a
1261
+ * non-`application/json` Content-Type.
1262
+ *
1263
+ * @example Delete a policy and log the prior state
1264
+ * ```ts
1265
+ * const deleted = await client.abacPolicies.delete(
1266
+ * "550e8400-e29b-41d4-a716-446655440000",
1267
+ * );
1268
+ * console.log(`Deleted "${deleted.name}" (${deleted.effect} ${deleted.action})`);
1269
+ * ```
1270
+ *
1271
+ * @example Treat a not-found delete as idempotent success
1272
+ * ```ts
1273
+ * try {
1274
+ * await client.abacPolicies.delete(id);
1275
+ * } catch (err) {
1276
+ * if (err instanceof AttestryAPIError && err.status === 404) {
1277
+ * // already gone — fine for an idempotent caller
1278
+ * } else {
1279
+ * throw err;
1280
+ * }
1281
+ * }
1282
+ * ```
1283
+ */
1284
+ delete(id, options) {
1285
+ assertValidPolicyId(id, "abacPolicies.delete");
1286
+ return this.client
1287
+ ._request({
1288
+ method: "DELETE",
1289
+ path: `/api/v1/abac-policies/${id}`,
1290
+ options,
1291
+ })
1292
+ .then((result) => validateAbacPolicy(result, "abacPolicies.delete"));
1293
+ }
1294
+ }
1295
+ // ─── UUID path-segment pre-validation ───────────────────────────────────────
1296
+ /**
1297
+ * Pre-validate an ABAC policy `id` path segment. Shared by
1298
+ * `.retrieve()` / `.update()` / `.delete()` — the three methods that
1299
+ * interpolate `id` into the request path `/api/v1/abac-policies/<id>`.
1300
+ *
1301
+ * The kernel validates `id` with a strict RFC 4122 UUID regex
1302
+ * (`badId` at `src/app/api/v1/abac-policies/[id]/route.ts:35-37`) and
1303
+ * returns HTTP 400 "Invalid policy id." on a mismatch — BEFORE
1304
+ * rate-limit + auth. The SDK pre-validates synchronously (mirror of
1305
+ * `batch.get`) so that 400 is reachable from a consumer only via an
1306
+ * `as any` cast or a kernel-side id-flavor change (ULID, KSUID, ...).
1307
+ *
1308
+ * **No `encodeURIComponent` / URIError defense** — see `UUID_REGEX`
1309
+ * above: a validated UUID is ASCII hex + hyphens, so the caller
1310
+ * interpolates it into the path raw. Carry-forward invariant #32
1311
+ * (URIError → TypeError conversion) applies only where a path segment
1312
+ * can actually reach `encodeURIComponent` with potentially-malformed
1313
+ * input (`decisions.retrieve`'s free-form id); a pre-validated UUID
1314
+ * cannot, so there is no `URIError` surface to convert.
1315
+ *
1316
+ * Throws `TypeError` synchronously (NO fetch issued) when `id` is a
1317
+ * non-string, an empty string, or not an RFC 4122 hyphenated UUID.
1318
+ */
1319
+ function assertValidPolicyId(id, methodName) {
1320
+ if (typeof id !== "string" || id.length === 0) {
1321
+ throw new TypeError(`${methodName}: \`id\` must be a non-empty string`);
1322
+ }
1323
+ if (!UUID_REGEX.test(id)) {
1324
+ throw new TypeError(`${methodName}: \`id\` must be an RFC 4122 hyphenated UUID ` +
1325
+ `(matched regex: /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-` +
1326
+ `[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/, got ${JSON.stringify(id)})`);
1327
+ }
1328
+ }
1329
+ // ─── Response-shape validation (P2 hardening) ───────────────────────────────
1330
+ /**
1331
+ * P2 hardening: validate the `.list()` response shape. Mirror of
1332
+ * `assertIncidentsListResponse` in `incidents.ts` and the
1333
+ * `regulatoryChanges.list` validator pattern.
1334
+ *
1335
+ * The kernel emits
1336
+ * `{success: true, data: {items: AbacPolicy[], count: number}}`. After
1337
+ * the transport's envelope-unwrap, this validator sees `{items,
1338
+ * count}`. Asserts:
1339
+ * - non-null, non-array object
1340
+ * - `items` is an array
1341
+ * - `count` is a number
1342
+ *
1343
+ * **Per-row item shape NOT validated** (faithful courier — P4
1344
+ * candidate; consistent with `incidents.list` /
1345
+ * `regulatoryChanges.list`).
1346
+ *
1347
+ * **`objectHasOwn` is the module-load snapshot** (set at module-load
1348
+ * via `const objectHasOwn = Object.hasOwn`). A hostile dep that
1349
+ * monkey-patches `Object.hasOwn` AFTER SDK import time does NOT
1350
+ * affect this validator. Symmetric prototype-pollution defense on
1351
+ * the response side; input side is N/A for `.list()`.
1352
+ */
1353
+ function validateAbacPoliciesListResponse(result) {
1354
+ if (result === null ||
1355
+ typeof result !== "object" ||
1356
+ Array.isArray(result)) {
1357
+ throw new AttestryError(`abacPolicies.list: expected an object response from the kernel ` +
1358
+ `(got ${describeType(result)})`);
1359
+ }
1360
+ const obj = result;
1361
+ // items — ALWAYS-PRESENT array (empty when org has no policies).
1362
+ // UNCONDITIONAL own-property check (the validator's items branch
1363
+ // is unconditional).
1364
+ const items = objectHasOwn(obj, "items") ? obj.items : undefined;
1365
+ if (!Array.isArray(items)) {
1366
+ throw new AttestryError(`abacPolicies.list: expected response.items to be an array ` +
1367
+ `(got ${describeType(items)})`);
1368
+ }
1369
+ // count — ALWAYS-PRESENT number.
1370
+ const count = objectHasOwn(obj, "count") ? obj.count : undefined;
1371
+ if (typeof count !== "number") {
1372
+ throw new AttestryError(`abacPolicies.list: expected response.count to be a number ` +
1373
+ `(got ${describeType(count)})`);
1374
+ }
1375
+ return result;
1376
+ }
1377
+ /**
1378
+ * P2 hardening: validate a single `AbacPolicy` row response. **Shared
1379
+ * by `.create()` / `.retrieve()` / `.update()` / `.delete()`** — all
1380
+ * four kernel handlers emit `{success: true, data: <row>}` (HTTP 201
1381
+ * for `.create()`, 200 for the other three) and the transport
1382
+ * unwraps `data`, so this validator sees the row directly. The
1383
+ * `methodName` argument (`"abacPolicies.create"` / `".retrieve"` /
1384
+ * `".update"` / `".delete"`) prefixes every thrown `AttestryError`
1385
+ * message so a malformed response names the method the consumer
1386
+ * actually called — mirror of the shared `encodePathSegment` /
1387
+ * `assertNonNullObjectResponse` `methodName`-parameter convention.
1388
+ *
1389
+ * Symmetric prototype-pollution defense — read EACH field via the
1390
+ * module-load `objectHasOwn` snapshot so a hostile npm dep polluting
1391
+ * `Object.prototype.<field>` cannot mask a kernel regression that
1392
+ * drops a field. Mirror of `ship-gate.ts` / `gate.ts` patterns.
1393
+ *
1394
+ * All 13 fields are ALWAYS-PRESENT on the wire (`description` and
1395
+ * `createdByUserId` are `string | null`, NOT optional; kernel uses
1396
+ * `?? null` coalesce at `rowToPolicy`). Validator's branches for
1397
+ * each field are UNCONDITIONAL.
1398
+ *
1399
+ * **`condition` is validated as `non-null object`** only — the
1400
+ * recursive AST grammar is the kernel's source of truth. SDK
1401
+ * passes the AST through verbatim once it's confirmed to be an
1402
+ * object (faithful courier on the recursive structure).
1403
+ *
1404
+ * **Single-field rejection semantics** (mirror of ship-gate.ts /
1405
+ * gate.ts / batch.ts / audit-log.ts) — validator checks fields in
1406
+ * declaration order and throws on the FIRST failing field. Project
1407
+ * convention.
1408
+ */
1409
+ function validateAbacPolicy(result, methodName) {
1410
+ if (result === null ||
1411
+ typeof result !== "object" ||
1412
+ Array.isArray(result)) {
1413
+ throw new AttestryError(`${methodName}: expected an object response from the kernel ` +
1414
+ `(got ${describeType(result)})`);
1415
+ }
1416
+ const obj = result;
1417
+ // id — string (UUID).
1418
+ const id = objectHasOwn(obj, "id") ? obj.id : undefined;
1419
+ if (typeof id !== "string") {
1420
+ throw new AttestryError(`${methodName}: expected response.id to be a string ` +
1421
+ `(got ${describeType(id)})`);
1422
+ }
1423
+ // orgId — string (UUID).
1424
+ const orgId = objectHasOwn(obj, "orgId") ? obj.orgId : undefined;
1425
+ if (typeof orgId !== "string") {
1426
+ throw new AttestryError(`${methodName}: expected response.orgId to be a string ` +
1427
+ `(got ${describeType(orgId)})`);
1428
+ }
1429
+ // name — string.
1430
+ const name = objectHasOwn(obj, "name") ? obj.name : undefined;
1431
+ if (typeof name !== "string") {
1432
+ throw new AttestryError(`${methodName}: expected response.name to be a string ` +
1433
+ `(got ${describeType(name)})`);
1434
+ }
1435
+ // description — string OR null (ALWAYS-PRESENT — kernel uses ?? null).
1436
+ const description = objectHasOwn(obj, "description")
1437
+ ? obj.description
1438
+ : undefined;
1439
+ if (description !== null && typeof description !== "string") {
1440
+ throw new AttestryError(`${methodName}: expected response.description to be a string ` +
1441
+ `or null (got ${describeType(description)})`);
1442
+ }
1443
+ // resource — string (closed-enum, but P2 is faithful courier).
1444
+ const resource = objectHasOwn(obj, "resource") ? obj.resource : undefined;
1445
+ if (typeof resource !== "string") {
1446
+ throw new AttestryError(`${methodName}: expected response.resource to be a string ` +
1447
+ `(got ${describeType(resource)})`);
1448
+ }
1449
+ // action — string (closed-enum, faithful courier).
1450
+ const action = objectHasOwn(obj, "action") ? obj.action : undefined;
1451
+ if (typeof action !== "string") {
1452
+ throw new AttestryError(`${methodName}: expected response.action to be a string ` +
1453
+ `(got ${describeType(action)})`);
1454
+ }
1455
+ // effect — string (closed-enum, faithful courier).
1456
+ const effect = objectHasOwn(obj, "effect") ? obj.effect : undefined;
1457
+ if (typeof effect !== "string") {
1458
+ throw new AttestryError(`${methodName}: expected response.effect to be a string ` +
1459
+ `(got ${describeType(effect)})`);
1460
+ }
1461
+ // condition — non-null object (AST recursive shape NOT validated;
1462
+ // faithful courier on the recursive grammar).
1463
+ const condition = objectHasOwn(obj, "condition") ? obj.condition : undefined;
1464
+ if (condition === null ||
1465
+ typeof condition !== "object" ||
1466
+ Array.isArray(condition)) {
1467
+ throw new AttestryError(`${methodName}: expected response.condition to be a non-null ` +
1468
+ `object (got ${describeType(condition)})`);
1469
+ }
1470
+ // priority — number.
1471
+ const priority = objectHasOwn(obj, "priority") ? obj.priority : undefined;
1472
+ if (typeof priority !== "number") {
1473
+ throw new AttestryError(`${methodName}: expected response.priority to be a number ` +
1474
+ `(got ${describeType(priority)})`);
1475
+ }
1476
+ // enabled — boolean.
1477
+ const enabled = objectHasOwn(obj, "enabled") ? obj.enabled : undefined;
1478
+ if (typeof enabled !== "boolean") {
1479
+ throw new AttestryError(`${methodName}: expected response.enabled to be a boolean ` +
1480
+ `(got ${describeType(enabled)})`);
1481
+ }
1482
+ // createdByUserId — string OR null.
1483
+ const createdByUserId = objectHasOwn(obj, "createdByUserId")
1484
+ ? obj.createdByUserId
1485
+ : undefined;
1486
+ if (createdByUserId !== null && typeof createdByUserId !== "string") {
1487
+ throw new AttestryError(`${methodName}: expected response.createdByUserId to be a ` +
1488
+ `string or null (got ${describeType(createdByUserId)})`);
1489
+ }
1490
+ // createdAt — string (ISO-8601; NOT Date — wire shape).
1491
+ const createdAt = objectHasOwn(obj, "createdAt") ? obj.createdAt : undefined;
1492
+ if (typeof createdAt !== "string") {
1493
+ throw new AttestryError(`${methodName}: expected response.createdAt to be a string ` +
1494
+ `(got ${describeType(createdAt)})`);
1495
+ }
1496
+ // updatedAt — string (ISO-8601).
1497
+ const updatedAt = objectHasOwn(obj, "updatedAt") ? obj.updatedAt : undefined;
1498
+ if (typeof updatedAt !== "string") {
1499
+ throw new AttestryError(`${methodName}: expected response.updatedAt to be a string ` +
1500
+ `(got ${describeType(updatedAt)})`);
1501
+ }
1502
+ return result;
1503
+ }
1504
+ /**
1505
+ * Human-readable type description for error messages. Distinguishes
1506
+ * `null` and `array` from generic `object`. Duplicated in
1507
+ * `decisions.ts` / `incidents.ts` / `regulatory-changes.ts` /
1508
+ * `compliance-check.ts` / `check.ts` / `gate.ts` / `batch.ts` /
1509
+ * `audit-log.ts` / `ship-gate.ts` per project pattern (small helper,
1510
+ * leaf-resource modules, no shared module yet).
1511
+ */
1512
+ function describeType(value) {
1513
+ if (value === null)
1514
+ return "null";
1515
+ if (Array.isArray(value))
1516
+ return "array";
1517
+ return typeof value;
1518
+ }
1519
+ //# sourceMappingURL=abac-policies.js.map