@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,307 @@
1
+ import type { AttestryClient } from "../client.js";
2
+ import type { RequestOptions } from "../types.js";
3
+ /**
4
+ * Public closed-enum of supported severity values. Mirrors the kernel's
5
+ * route-local `validSeverities` const at
6
+ * `src/app/api/v1/regulatory-changes/route.ts:39`. Drift-pinned in
7
+ * `src/lib/incidents/__tests__/sdk-drift.test.ts`.
8
+ *
9
+ * Forward-compat: when a future severity value is added kernel-side,
10
+ * bump the SDK minor version and extend this array. The kernel returns
11
+ * 400 for any value outside this set; the SDK pre-rejects invalid
12
+ * values synchronously as `TypeError` (build-round D5 — closed-enum
13
+ * input validates at the SDK boundary so the failure is faster +
14
+ * clearer than waiting for the server's 400).
15
+ */
16
+ export declare const REGULATORY_CHANGE_SEVERITIES: readonly ["critical", "high", "medium", "low"];
17
+ export type RegulatoryChangeSeverity = (typeof REGULATORY_CHANGE_SEVERITIES)[number];
18
+ /**
19
+ * Public closed-enum of supported status values. Mirrors the kernel's
20
+ * route-local `VALID_STATUSES` const at
21
+ * `src/app/api/v1/regulatory-changes/route.ts:62`. Drift-pinned in
22
+ * `src/lib/incidents/__tests__/sdk-drift.test.ts`.
23
+ *
24
+ * Same forward-compat trade-off as `REGULATORY_CHANGE_SEVERITIES`
25
+ * (build-round D5).
26
+ */
27
+ export declare const REGULATORY_CHANGE_STATUSES: readonly ["new", "reviewed", "actioned", "dismissed"];
28
+ export type RegulatoryChangeStatus = (typeof REGULATORY_CHANGE_STATUSES)[number];
29
+ /**
30
+ * Wire shape for a single regulatory-change row. Source-of-truth at
31
+ * kernel `src/lib/db/schema.ts:1082-1111` (`regulatoryChanges` pgTable).
32
+ *
33
+ * The route returns raw Drizzle rows (no `rowToWireJson` mapper today —
34
+ * unlike `auditLog.export`'s stable wire shape). Drizzle serializes
35
+ * timestamp columns as ISO-8601 strings via `JSON.stringify(Date)`.
36
+ * jsonb columns (`affectedRequirements`, `aiAnalysis`,
37
+ * `statusTransitions`) are typed as `unknown` — the schema comment hints
38
+ * at concrete shapes (`string[]`, Reggie analysis, `[{status, date,
39
+ * source}]`) but they're not enforced server-side. Consumers parse via
40
+ * their own validators (build-round D3).
41
+ *
42
+ * `severity` and `status` are typed as `string` (NOT typed unions) —
43
+ * forward-compat for kernel-side enum additions that haven't landed in
44
+ * the SDK yet. The write-side `RegulatoryChangesListInput.severity` /
45
+ * `.status` use the literal-union for IDE auto-completion. Same
46
+ * asymmetry as `humanOversightState`/`policyOutcome` on
47
+ * `decisions.ingest` (build-round D2).
48
+ *
49
+ * **Field stability**: this wire shape IS the Drizzle row shape today.
50
+ * A future row-to-wire mapper (parallel to `rowToWireJson`) would
51
+ * stabilize it; until then a kernel-side column rename ripples directly
52
+ * to the SDK. The drift pin in `sdk-drift.test.ts` trip-wires that
53
+ * change at the kernel-source level.
54
+ */
55
+ export interface RegulatoryChange {
56
+ /** UUID. */
57
+ id: string;
58
+ /** Framework code: `"EU_AI_ACT"` / `"COLORADO_AI_ACT"` / etc. — open string. */
59
+ framework: string;
60
+ title: string;
61
+ description: string | null;
62
+ /**
63
+ * `'amendment' | 'clarification' | 'enforcement' | 'guidance'` per
64
+ * the schema comment. Not enforced server-side — typed as `string`
65
+ * for forward-compat.
66
+ */
67
+ changeType: string;
68
+ /**
69
+ * `'critical' | 'high' | 'medium' | 'low'` (closed enum, see
70
+ * `REGULATORY_CHANGE_SEVERITIES`). Typed as `string` for forward-compat
71
+ * (build-round D2).
72
+ */
73
+ severity: string;
74
+ /** ISO-8601, nullable. */
75
+ effectiveDate: string | null;
76
+ /**
77
+ * jsonb — typed as `string[]` in the schema comment, but defensively
78
+ * `unknown` at the SDK boundary (build-round D3).
79
+ */
80
+ affectedRequirements: unknown;
81
+ sourceUrl: string | null;
82
+ /** ISO-8601, nullable; kernel sorts the response DESC by this column. */
83
+ publishedAt: string | null;
84
+ /**
85
+ * `'eur_lex' | 'federal_register' | 'uk_legislation' | 'colorado_leg'
86
+ * | 'nist_gov' | 'rss_custom'` per schema comment. Open string —
87
+ * source pipeline IDs.
88
+ */
89
+ sourceId: string | null;
90
+ /** External document ID from the source system. */
91
+ sourceReferenceId: string | null;
92
+ /** ISO-8601, nullable; kernel records when the row was scraped. */
93
+ ingestedAt: string | null;
94
+ /** E.g., `"European Commission"`, `"US Congress"`. */
95
+ authorityPublisher: string | null;
96
+ /**
97
+ * jsonb — Reggie's cached analysis result. Defensively `unknown`
98
+ * (build-round D3).
99
+ */
100
+ aiAnalysis: unknown;
101
+ /** ISO-8601, nullable; kernel records when alerts were sent. */
102
+ notifiedAt: string | null;
103
+ /**
104
+ * `'draft' | 'introduced' | 'committee' | 'passed_one_chamber' |
105
+ * 'passed_both' | 'signed' | 'enacted' | 'vetoed' | 'withdrawn'` per
106
+ * schema comment. Open string — bill lifecycle state.
107
+ */
108
+ billStatus: string | null;
109
+ /**
110
+ * jsonb — array of `{status, date, source}` per schema comment.
111
+ * Defensively `unknown` (build-round D3).
112
+ */
113
+ statusTransitions: unknown;
114
+ /**
115
+ * `'new' | 'reviewed' | 'actioned' | 'dismissed'` (closed enum, see
116
+ * `REGULATORY_CHANGE_STATUSES`). Defaults `"new"` server-side. Typed
117
+ * as `string` for forward-compat (build-round D2).
118
+ */
119
+ status: string;
120
+ /** `'high' | 'medium' | 'low'` per schema comment; defaults `"medium"` server-side. */
121
+ relevance: string | null;
122
+ /** ISO-8601, NOT NULL (server `defaultNow()`). */
123
+ createdAt: string;
124
+ }
125
+ /**
126
+ * Input shape for `regulatoryChanges.list(input?)`. All fields
127
+ * optional; a bare `regulatoryChanges.list()` call returns the most
128
+ * recent 200 non-dismissed regulatory-change rows (kernel default —
129
+ * see "Default-excludes-dismissed" in JSDoc on `list()`).
130
+ *
131
+ * Filters are AND-combined server-side. Date range filters apply to
132
+ * `publishedAt` (kernel `gte` / `lte`).
133
+ */
134
+ export interface RegulatoryChangesListInput {
135
+ /**
136
+ * Open string — kernel forwards verbatim to a DB filter
137
+ * (`WHERE framework = ?`). The SDK does NOT pre-validate (forward-
138
+ * compat: new framework codes added kernel-side don't require an
139
+ * SDK bump). URIError defense via `assertEncodableQueryString`
140
+ * (carry-forward invariant #32).
141
+ */
142
+ framework?: string;
143
+ /**
144
+ * Closed enum. Pre-validated against
145
+ * `REGULATORY_CHANGE_SEVERITIES`; SDK throws `TypeError`
146
+ * synchronously for unknown values (build-round D5; carry-forward
147
+ * invariant #41). Drift-pinned.
148
+ */
149
+ severity?: RegulatoryChangeSeverity;
150
+ /**
151
+ * Closed enum. Pre-validated against
152
+ * `REGULATORY_CHANGE_STATUSES`; SDK throws `TypeError` synchronously
153
+ * for unknown values (build-round D5).
154
+ *
155
+ * **Default-excludes-dismissed**: when omitted, the kernel filters
156
+ * dismissed rows OUT (`WHERE status != 'dismissed'`). To include
157
+ * dismissed rows, pass `status: "dismissed"` (returns ONLY
158
+ * dismissed) or one of `"new"` / `"reviewed"` / `"actioned"` for an
159
+ * exact match. There is currently NO way to retrieve "everything
160
+ * including dismissed" via this endpoint.
161
+ */
162
+ status?: RegulatoryChangeStatus;
163
+ /**
164
+ * Date-string lower bound on `publishedAt`. Kernel parses via
165
+ * `new Date(value)` and returns 400 with
166
+ * `error: "Invalid 'from' date format"` if `isNaN(parsed.getTime())`.
167
+ * The SDK does NOT pre-validate ISO-8601 format (build-round D6 —
168
+ * kernel's `new Date(...)` is lenient: `"May 7 2026"`, `"2026/05/07"`,
169
+ * and `"2026-05-07T00:00:00Z"` all parse; pre-validating to strict
170
+ * ISO would reject valid kernel inputs).
171
+ */
172
+ from?: string;
173
+ /** Same semantics as `from`. */
174
+ to?: string;
175
+ /**
176
+ * Page size, 1..200; default 200 server-side. SDK pre-rejects
177
+ * `NaN` / `Infinity` / `<= 0` / non-integer as `TypeError`
178
+ * synchronously. Values > 200 are forwarded verbatim — server
179
+ * returns 400 (`"Invalid limit. Must be between 1 and 200."`).
180
+ * Build-round D4: kernel's MAX_LIMIT is the authority; pre-capping
181
+ * SDK-side would silently mask future kernel raises.
182
+ */
183
+ limit?: number;
184
+ }
185
+ /**
186
+ * RegulatoryChanges resource — sibling to `IncidentsResource`,
187
+ * `DecisionsResource`, `ChatResource`, `AuditLogResource`. Today wraps
188
+ * a single endpoint (`list`); the class is the landing pad for future
189
+ * regulatory-changes methods (mark-as-read, mark-as-actioned,
190
+ * subscribe-to-framework, etc.).
191
+ */
192
+ export declare class RegulatoryChangesResource {
193
+ private readonly client;
194
+ constructor(client: AttestryClient);
195
+ /**
196
+ * List regulatory-change rows filtered by framework / severity /
197
+ * status / date range. Returns all matching rows up to `limit`
198
+ * (default 200 server-side, max 200). Rows arrive DESC by
199
+ * `publishedAt`.
200
+ *
201
+ * **Default-excludes-dismissed** (the non-obvious gotcha — read
202
+ * carefully): when `status` is OMITTED from the input, the kernel
203
+ * filters dismissed rows OUT (`WHERE status != 'dismissed'`). To
204
+ * retrieve dismissed rows, pass `status: "dismissed"` (returns ONLY
205
+ * dismissed). There is currently NO way to retrieve "everything
206
+ * including dismissed" via this endpoint — the kernel route hardcodes
207
+ * the exclusion at `route.ts:78-79`.
208
+ *
209
+ * **READ_SYSTEMS auth scope**: returns HTTP 401 for no/invalid API
210
+ * key, HTTP **403** for an authenticated key that lacks the
211
+ * READ_SYSTEMS permission. `auditLog.export` (ADMIN-only dual-auth)
212
+ * surfaces the SAME 401-vs-403 split — the auth models differ, the
213
+ * status surface does not (corrected session-22 hostile review #2).
214
+ * Consumers must distinguish 401 (re-authenticate) from 403 (request
215
+ * a different API key) at the call site.
216
+ *
217
+ * **Sync JSON list**: returns `Promise<RegulatoryChange[]>`. No
218
+ * pagination cursor — caller adjusts `limit` or filters to narrow the
219
+ * result set. An empty match returns `[]` (no 404).
220
+ *
221
+ * Errors:
222
+ * - `AttestryAPIError` (status 400) — invalid `from`/`to` date
223
+ * format (kernel's `new Date(...)` returned `NaN`), or
224
+ * `limit > 200` / `limit < 1` (server-side range check). Closed-
225
+ * enum 400s (`severity` / `status` not in enum) are
226
+ * UNREACHABLE through the SDK — pre-rejected as `TypeError`.
227
+ * - `AttestryAPIError` (status 401) — no API key OR invalid key
228
+ * (the `requireApiKey` branch).
229
+ * - `AttestryAPIError` (status 403) — authenticated key lacks
230
+ * `READ_SYSTEMS` permission (the `requireApiKeyWithPermission`
231
+ * branch).
232
+ * - `AttestryAPIError` (status 429) — rate limit (auto-retried by
233
+ * default — invariant #18; per-IP rate-limit key
234
+ * `v1-reg-changes:${ip}`).
235
+ * - `AttestryAPIError` (status 500) — internal kernel error (scrubbed
236
+ * message via `internalErrorResponse`).
237
+ * - `AttestryError` ("request aborted by caller") — caller-supplied
238
+ * `options.signal` fired (pre-aborted or mid-flight).
239
+ * - `TypeError` (synchronous, no fetch issued) — input failed
240
+ * SDK-side validation.
241
+ *
242
+ * **Notably ABSENT**:
243
+ * - **No 404** — empty filter set returns 200 with `data: []`. Do
244
+ * NOT special-case 404 in error handling.
245
+ * - **No 422** — closed enums return 400 (kernel uses inline
246
+ * string-includes parsing, not Zod for these query params).
247
+ * - **No 413** — limit is enforced by `parseInt` + range check.
248
+ *
249
+ * **SDK-side validation** (synchronous `TypeError`, no fetch
250
+ * issued):
251
+ * - `input` itself: optional; when provided, must be a non-null,
252
+ * non-array object.
253
+ * - `input.framework`: optional; non-empty string when provided.
254
+ * Lone-surrogate guard via `assertEncodableQueryString`
255
+ * (carry-forward invariant #32). NOT pre-validated as a known
256
+ * framework code (forward-compat).
257
+ * - `input.severity`: optional; one of `"critical"`/`"high"`/
258
+ * `"medium"`/`"low"` when provided. Pre-validated against the
259
+ * closed enum (`REGULATORY_CHANGE_SEVERITIES`).
260
+ * - `input.status`: optional; one of `"new"`/`"reviewed"`/
261
+ * `"actioned"`/`"dismissed"` when provided. Pre-validated against
262
+ * the closed enum (`REGULATORY_CHANGE_STATUSES`).
263
+ * - `input.from` / `input.to`: optional; non-empty string when
264
+ * provided. Lone-surrogate guard. **NOT pre-validated as ISO-8601**
265
+ * (build-round D6 — kernel's `new Date(...)` is lenient).
266
+ * - `input.limit`: optional; positive finite integer when provided.
267
+ * `NaN` / `Infinity` / `<= 0` / non-integer rejected. Values
268
+ * `> 200` forwarded verbatim (build-round D4).
269
+ *
270
+ * **Response-shape validation** (P2 hardening):
271
+ * - Rejects with `AttestryError` if the kernel response isn't an
272
+ * array. Per-row shape (the 21-field `RegulatoryChange`) is
273
+ * faithful-courier — NOT validated (P4 candidate).
274
+ *
275
+ * **Transport-shape validation** (P3 hardening):
276
+ * - Rejects with `AttestryAPIError` if the kernel responds with a
277
+ * non-`application/json` Content-Type — protects against
278
+ * proxy-injected HTML 200 pages parsing into junk consumer state.
279
+ *
280
+ * @example List the most recent 200 non-dismissed regulatory updates
281
+ * ```ts
282
+ * const changes = await client.regulatoryChanges.list();
283
+ * for (const change of changes) {
284
+ * console.log(change.framework, change.severity, change.title);
285
+ * }
286
+ * ```
287
+ *
288
+ * @example Filter to critical EU AI Act updates from last week
289
+ * ```ts
290
+ * const changes = await client.regulatoryChanges.list({
291
+ * framework: "EU_AI_ACT",
292
+ * severity: "critical",
293
+ * from: "2026-04-30T00:00:00Z",
294
+ * limit: 50,
295
+ * });
296
+ * ```
297
+ *
298
+ * @example Retrieve only dismissed rows (default omits them)
299
+ * ```ts
300
+ * const dismissed = await client.regulatoryChanges.list({
301
+ * status: "dismissed",
302
+ * });
303
+ * ```
304
+ */
305
+ list(input?: RegulatoryChangesListInput, options?: RequestOptions): Promise<RegulatoryChange[]>;
306
+ }
307
+ //# sourceMappingURL=regulatory-changes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"regulatory-changes.d.ts","sourceRoot":"","sources":["../../src/resources/regulatory-changes.ts"],"names":[],"mappings":"AAsCA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAGlD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,4BAA4B,gDAK9B,CAAC;AAEZ,MAAM,MAAM,wBAAwB,GAClC,CAAC,OAAO,4BAA4B,CAAC,CAAC,MAAM,CAAC,CAAC;AAEhD;;;;;;;;GAQG;AACH,eAAO,MAAM,0BAA0B,uDAK5B,CAAC;AAEZ,MAAM,MAAM,sBAAsB,GAChC,CAAC,OAAO,0BAA0B,CAAC,CAAC,MAAM,CAAC,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,WAAW,gBAAgB;IAC/B,YAAY;IACZ,EAAE,EAAE,MAAM,CAAC;IACX,gFAAgF;IAChF,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB,0BAA0B;IAC1B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B;;;OAGG;IACH,oBAAoB,EAAE,OAAO,CAAC;IAC9B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,yEAAyE;IACzE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,mDAAmD;IACnD,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,mEAAmE;IACnE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,sDAAsD;IACtD,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC;;;OAGG;IACH,UAAU,EAAE,OAAO,CAAC;IACpB,gEAAgE;IAChE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B;;;;OAIG;IACH,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B;;;OAGG;IACH,iBAAiB,EAAE,OAAO,CAAC;IAC3B;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,uFAAuF;IACvF,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,kDAAkD;IAClD,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,0BAA0B;IACzC;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,wBAAwB,CAAC;IACpC;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,sBAAsB,CAAC;IAChC;;;;;;;;OAQG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gCAAgC;IAChC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;GAMG;AACH,qBAAa,yBAAyB;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,cAAc;IAEnD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6GG;IACH,IAAI,CACF,KAAK,CAAC,EAAE,0BAA0B,EAClC,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,gBAAgB,EAAE,CAAC;CAwK/B"}
@@ -0,0 +1,365 @@
1
+ // ─── RegulatoryChanges resource ─────────────────────────────────────────────
2
+ //
3
+ // Wraps the regulatory-changes feed surface (Prompt R12):
4
+ //
5
+ // - GET /api/v1/regulatory-changes sync JSON list of regulatory updates
6
+ //
7
+ // Second non-decisions resource on `@attestry/sdk`. Sibling to
8
+ // `IncidentsResource`, `DecisionsResource`, `ChatResource`,
9
+ // `AuditLogResource`. Single public method today (`list`); the resource
10
+ // class exists as the landing pad for future regulatory-changes methods
11
+ // if/when the kernel adds them.
12
+ //
13
+ // READ_SYSTEMS auth scope: the kernel route gates on
14
+ // `apiKeyPermissions:[READ_SYSTEMS]`. Returns **HTTP 401** for no/invalid
15
+ // API key (via `requireApiKey` first), and **HTTP 403** for an authenticated
16
+ // key that lacks the READ_SYSTEMS permission. `auditLog.export` (ADMIN-only
17
+ // dual-auth) surfaces the SAME 401-vs-403 split — the auth MODELS differ
18
+ // (single READ_SYSTEMS permission vs ADMIN-only dual-auth) but the status
19
+ // surface does not (corrected session-22 hostile review #2; the prior
20
+ // "auditLog.export returns 401 for both" framing of invariant #42 was
21
+ // wrong). Consumers must pin both 401 and 403 branches separately.
22
+ //
23
+ // Sync JSON list response: reuses `client._request` and the existing
24
+ // `{success:true, data}` envelope-unwrap (carry-forward invariant #9).
25
+ // NO new SDK primitive needed — smaller blast radius than `auditLog.export`.
26
+ // Returns `Promise<RegulatoryChange[]>`.
27
+ //
28
+ // **Default-excludes-dismissed semantics** (the non-obvious gotcha):
29
+ // - `status` omitted → kernel filters dismissed rows OUT
30
+ // (`WHERE status != 'dismissed'`)
31
+ // - `status: "dismissed"` → only dismissed rows
32
+ // - `status: "new"|"reviewed"|"actioned"` → only that exact status
33
+ // There is currently NO way to retrieve "everything including dismissed"
34
+ // via this endpoint. Documented prominently in JSDoc on `list()` and
35
+ // `RegulatoryChangesListInput.status`. Pinned via URL test
36
+ // (omitted-status URL omits the param entirely; explicit-`"dismissed"`
37
+ // URL contains `status=dismissed`).
38
+ import { AttestryError } from "../errors.js";
39
+ import { readInputField } from "./safe-input-read.js";
40
+ /**
41
+ * Public closed-enum of supported severity values. Mirrors the kernel's
42
+ * route-local `validSeverities` const at
43
+ * `src/app/api/v1/regulatory-changes/route.ts:39`. Drift-pinned in
44
+ * `src/lib/incidents/__tests__/sdk-drift.test.ts`.
45
+ *
46
+ * Forward-compat: when a future severity value is added kernel-side,
47
+ * bump the SDK minor version and extend this array. The kernel returns
48
+ * 400 for any value outside this set; the SDK pre-rejects invalid
49
+ * values synchronously as `TypeError` (build-round D5 — closed-enum
50
+ * input validates at the SDK boundary so the failure is faster +
51
+ * clearer than waiting for the server's 400).
52
+ */
53
+ export const REGULATORY_CHANGE_SEVERITIES = Object.freeze([
54
+ "critical",
55
+ "high",
56
+ "medium",
57
+ "low",
58
+ ]);
59
+ /**
60
+ * Public closed-enum of supported status values. Mirrors the kernel's
61
+ * route-local `VALID_STATUSES` const at
62
+ * `src/app/api/v1/regulatory-changes/route.ts:62`. Drift-pinned in
63
+ * `src/lib/incidents/__tests__/sdk-drift.test.ts`.
64
+ *
65
+ * Same forward-compat trade-off as `REGULATORY_CHANGE_SEVERITIES`
66
+ * (build-round D5).
67
+ */
68
+ export const REGULATORY_CHANGE_STATUSES = Object.freeze([
69
+ "new",
70
+ "reviewed",
71
+ "actioned",
72
+ "dismissed",
73
+ ]);
74
+ /**
75
+ * RegulatoryChanges resource — sibling to `IncidentsResource`,
76
+ * `DecisionsResource`, `ChatResource`, `AuditLogResource`. Today wraps
77
+ * a single endpoint (`list`); the class is the landing pad for future
78
+ * regulatory-changes methods (mark-as-read, mark-as-actioned,
79
+ * subscribe-to-framework, etc.).
80
+ */
81
+ export class RegulatoryChangesResource {
82
+ client;
83
+ constructor(client) {
84
+ this.client = client;
85
+ }
86
+ /**
87
+ * List regulatory-change rows filtered by framework / severity /
88
+ * status / date range. Returns all matching rows up to `limit`
89
+ * (default 200 server-side, max 200). Rows arrive DESC by
90
+ * `publishedAt`.
91
+ *
92
+ * **Default-excludes-dismissed** (the non-obvious gotcha — read
93
+ * carefully): when `status` is OMITTED from the input, the kernel
94
+ * filters dismissed rows OUT (`WHERE status != 'dismissed'`). To
95
+ * retrieve dismissed rows, pass `status: "dismissed"` (returns ONLY
96
+ * dismissed). There is currently NO way to retrieve "everything
97
+ * including dismissed" via this endpoint — the kernel route hardcodes
98
+ * the exclusion at `route.ts:78-79`.
99
+ *
100
+ * **READ_SYSTEMS auth scope**: returns HTTP 401 for no/invalid API
101
+ * key, HTTP **403** for an authenticated key that lacks the
102
+ * READ_SYSTEMS permission. `auditLog.export` (ADMIN-only dual-auth)
103
+ * surfaces the SAME 401-vs-403 split — the auth models differ, the
104
+ * status surface does not (corrected session-22 hostile review #2).
105
+ * Consumers must distinguish 401 (re-authenticate) from 403 (request
106
+ * a different API key) at the call site.
107
+ *
108
+ * **Sync JSON list**: returns `Promise<RegulatoryChange[]>`. No
109
+ * pagination cursor — caller adjusts `limit` or filters to narrow the
110
+ * result set. An empty match returns `[]` (no 404).
111
+ *
112
+ * Errors:
113
+ * - `AttestryAPIError` (status 400) — invalid `from`/`to` date
114
+ * format (kernel's `new Date(...)` returned `NaN`), or
115
+ * `limit > 200` / `limit < 1` (server-side range check). Closed-
116
+ * enum 400s (`severity` / `status` not in enum) are
117
+ * UNREACHABLE through the SDK — pre-rejected as `TypeError`.
118
+ * - `AttestryAPIError` (status 401) — no API key OR invalid key
119
+ * (the `requireApiKey` branch).
120
+ * - `AttestryAPIError` (status 403) — authenticated key lacks
121
+ * `READ_SYSTEMS` permission (the `requireApiKeyWithPermission`
122
+ * branch).
123
+ * - `AttestryAPIError` (status 429) — rate limit (auto-retried by
124
+ * default — invariant #18; per-IP rate-limit key
125
+ * `v1-reg-changes:${ip}`).
126
+ * - `AttestryAPIError` (status 500) — internal kernel error (scrubbed
127
+ * message via `internalErrorResponse`).
128
+ * - `AttestryError` ("request aborted by caller") — caller-supplied
129
+ * `options.signal` fired (pre-aborted or mid-flight).
130
+ * - `TypeError` (synchronous, no fetch issued) — input failed
131
+ * SDK-side validation.
132
+ *
133
+ * **Notably ABSENT**:
134
+ * - **No 404** — empty filter set returns 200 with `data: []`. Do
135
+ * NOT special-case 404 in error handling.
136
+ * - **No 422** — closed enums return 400 (kernel uses inline
137
+ * string-includes parsing, not Zod for these query params).
138
+ * - **No 413** — limit is enforced by `parseInt` + range check.
139
+ *
140
+ * **SDK-side validation** (synchronous `TypeError`, no fetch
141
+ * issued):
142
+ * - `input` itself: optional; when provided, must be a non-null,
143
+ * non-array object.
144
+ * - `input.framework`: optional; non-empty string when provided.
145
+ * Lone-surrogate guard via `assertEncodableQueryString`
146
+ * (carry-forward invariant #32). NOT pre-validated as a known
147
+ * framework code (forward-compat).
148
+ * - `input.severity`: optional; one of `"critical"`/`"high"`/
149
+ * `"medium"`/`"low"` when provided. Pre-validated against the
150
+ * closed enum (`REGULATORY_CHANGE_SEVERITIES`).
151
+ * - `input.status`: optional; one of `"new"`/`"reviewed"`/
152
+ * `"actioned"`/`"dismissed"` when provided. Pre-validated against
153
+ * the closed enum (`REGULATORY_CHANGE_STATUSES`).
154
+ * - `input.from` / `input.to`: optional; non-empty string when
155
+ * provided. Lone-surrogate guard. **NOT pre-validated as ISO-8601**
156
+ * (build-round D6 — kernel's `new Date(...)` is lenient).
157
+ * - `input.limit`: optional; positive finite integer when provided.
158
+ * `NaN` / `Infinity` / `<= 0` / non-integer rejected. Values
159
+ * `> 200` forwarded verbatim (build-round D4).
160
+ *
161
+ * **Response-shape validation** (P2 hardening):
162
+ * - Rejects with `AttestryError` if the kernel response isn't an
163
+ * array. Per-row shape (the 21-field `RegulatoryChange`) is
164
+ * faithful-courier — NOT validated (P4 candidate).
165
+ *
166
+ * **Transport-shape validation** (P3 hardening):
167
+ * - Rejects with `AttestryAPIError` if the kernel responds with a
168
+ * non-`application/json` Content-Type — protects against
169
+ * proxy-injected HTML 200 pages parsing into junk consumer state.
170
+ *
171
+ * @example List the most recent 200 non-dismissed regulatory updates
172
+ * ```ts
173
+ * const changes = await client.regulatoryChanges.list();
174
+ * for (const change of changes) {
175
+ * console.log(change.framework, change.severity, change.title);
176
+ * }
177
+ * ```
178
+ *
179
+ * @example Filter to critical EU AI Act updates from last week
180
+ * ```ts
181
+ * const changes = await client.regulatoryChanges.list({
182
+ * framework: "EU_AI_ACT",
183
+ * severity: "critical",
184
+ * from: "2026-04-30T00:00:00Z",
185
+ * limit: 50,
186
+ * });
187
+ * ```
188
+ *
189
+ * @example Retrieve only dismissed rows (default omits them)
190
+ * ```ts
191
+ * const dismissed = await client.regulatoryChanges.list({
192
+ * status: "dismissed",
193
+ * });
194
+ * ```
195
+ */
196
+ list(input, options) {
197
+ // Top-level shape — when provided, must be a non-null, non-array
198
+ // object. typeof null === "object" and typeof [] === "object", so
199
+ // guard both explicitly. Like auditLog.export, input is OPTIONAL —
200
+ // `()` and `(undefined)` are both valid.
201
+ //
202
+ // The six query fields are snapshotted into locals (declared here
203
+ // so they stay visible for the query construction below — `input`
204
+ // is optional, and when omitted the locals stay `undefined`). Each
205
+ // read goes through `readInputField`, which converts a throwing
206
+ // accessor's exception into the documented synchronous `TypeError`
207
+ // input contract (session-22 hostile review #1 — the SDK-wide
208
+ // MEDIUM-1 getter-throws fix). The `as` cast re-asserts only what
209
+ // the consumer's own `RegulatoryChangesListInput` type claims.
210
+ let framework;
211
+ let severity;
212
+ let status;
213
+ let from;
214
+ let to;
215
+ let limit;
216
+ if (input !== undefined) {
217
+ if (input === null ||
218
+ typeof input !== "object" ||
219
+ Array.isArray(input)) {
220
+ throw new TypeError("regulatoryChanges.list: `input` must be an object when provided");
221
+ }
222
+ framework = readInputField(input, "framework", "regulatoryChanges.list");
223
+ severity = readInputField(input, "severity", "regulatoryChanges.list");
224
+ status = readInputField(input, "status", "regulatoryChanges.list");
225
+ from = readInputField(input, "from", "regulatoryChanges.list");
226
+ to = readInputField(input, "to", "regulatoryChanges.list");
227
+ limit = readInputField(input, "limit", "regulatoryChanges.list");
228
+ // framework: open string. Non-empty when provided. Lone-surrogate
229
+ // guard (#32). NOT pre-validated as a known code.
230
+ if (framework !== undefined) {
231
+ if (typeof framework !== "string" || framework.length === 0) {
232
+ throw new TypeError("regulatoryChanges.list: `framework` must be a non-empty string when provided");
233
+ }
234
+ assertEncodableQueryString(framework, "framework", "regulatoryChanges.list");
235
+ }
236
+ // severity: closed enum. Pre-reject invalid values synchronously
237
+ // (#41 / build-round D5).
238
+ if (severity !== undefined) {
239
+ if (typeof severity !== "string" ||
240
+ !REGULATORY_CHANGE_SEVERITIES.includes(severity)) {
241
+ throw new TypeError(`regulatoryChanges.list: \`severity\` must be one of ${REGULATORY_CHANGE_SEVERITIES.join(", ")} when provided`);
242
+ }
243
+ }
244
+ // status: closed enum. Same treatment as severity.
245
+ if (status !== undefined) {
246
+ if (typeof status !== "string" ||
247
+ !REGULATORY_CHANGE_STATUSES.includes(status)) {
248
+ throw new TypeError(`regulatoryChanges.list: \`status\` must be one of ${REGULATORY_CHANGE_STATUSES.join(", ")} when provided`);
249
+ }
250
+ }
251
+ // from / to: open date strings. Non-empty when provided. NOT
252
+ // pre-validated as ISO-8601 (build-round D6 — kernel's
253
+ // `new Date(...)` is lenient). Lone-surrogate guard (#32).
254
+ if (from !== undefined) {
255
+ if (typeof from !== "string" || from.length === 0) {
256
+ throw new TypeError("regulatoryChanges.list: `from` must be a non-empty string when provided");
257
+ }
258
+ assertEncodableQueryString(from, "from", "regulatoryChanges.list");
259
+ }
260
+ if (to !== undefined) {
261
+ if (typeof to !== "string" || to.length === 0) {
262
+ throw new TypeError("regulatoryChanges.list: `to` must be a non-empty string when provided");
263
+ }
264
+ assertEncodableQueryString(to, "to", "regulatoryChanges.list");
265
+ }
266
+ // limit: positive finite integer. NaN / Infinity / fractional /
267
+ // <= 0 rejected. Stricter than kernel's 400 (build-round D4 —
268
+ // fail-loud-and-synchronous; mirrors auditLog.export's limit
269
+ // policy).
270
+ if (limit !== undefined) {
271
+ if (typeof limit !== "number" ||
272
+ !Number.isInteger(limit) ||
273
+ limit <= 0) {
274
+ throw new TypeError("regulatoryChanges.list: `limit` must be a positive integer when provided");
275
+ }
276
+ }
277
+ }
278
+ return this.client
279
+ ._request({
280
+ method: "GET",
281
+ path: "/api/v1/regulatory-changes",
282
+ query: {
283
+ framework,
284
+ severity,
285
+ status,
286
+ from,
287
+ to,
288
+ limit,
289
+ },
290
+ options,
291
+ })
292
+ .then((result) => {
293
+ // P2 hardening: validate the kernel returned an array. The
294
+ // route emits `successResponse(changes)` where `changes` comes
295
+ // from Drizzle's `db.select()...limit(N)` which always returns
296
+ // Array — but a kernel-side regression to scalar/null/undefined
297
+ // would let `null as RegulatoryChange[]` reach consumers, who
298
+ // would crash on `out.length` with a confusing TypeError.
299
+ // Catch it at the SDK boundary with a clear AttestryError.
300
+ // NOTE: G6 documented behavior (wrong content-type with
301
+ // unparseable body resolves to null) is changed in P3 — see
302
+ // the P3 audit-prompt for the transport-level content-type
303
+ // guard that subsumes this null-case.
304
+ if (!Array.isArray(result)) {
305
+ throw new AttestryError(`regulatoryChanges.list: expected an array response from the kernel (got ${describeType(result)})`);
306
+ }
307
+ return result;
308
+ });
309
+ }
310
+ }
311
+ /**
312
+ * Synchronously verify a query-string value is encodable via
313
+ * `encodeURIComponent`. Mirrors the helper in `decisions.ts` and
314
+ * `audit-log.ts` (carry-forward invariant #32 — URIError defect-class
315
+ * is uniformly handled).
316
+ *
317
+ * Duplicated rather than shared because cross-resource imports between
318
+ * `regulatory-changes.ts`, `audit-log.ts`, and `decisions.ts` would
319
+ * create graph-cycle hazards — all three want to remain leaf-resource
320
+ * modules. A future SDK refactor may extract validation helpers to a
321
+ * shared module (e.g., `src/validate.ts`) when a fourth caller shows
322
+ * up; for now the duplication is intentional and documented.
323
+ */
324
+ function assertEncodableQueryString(value, fieldName, methodName) {
325
+ try {
326
+ encodeURIComponent(value);
327
+ }
328
+ catch (err) {
329
+ throw new TypeError(`${methodName}: \`${fieldName}\` contains invalid UTF-16 sequences (${
330
+ // encodeURIComponent always throws URIError (an Error subclass),
331
+ // so the String(err) branch is unreachable. Defense-in-depth
332
+ // marker for the v8 coverage tool.
333
+ /* v8 ignore next */
334
+ err instanceof Error ? err.message : String(err)})`, { cause: err });
335
+ }
336
+ }
337
+ /**
338
+ * Human-readable type description for response-shape error messages.
339
+ * Distinguishes `null` and `array` from generic `object`.
340
+ *
341
+ * Duplicated in `decisions.ts` and `incidents.ts` per project pattern
342
+ * (small helper, leaf-resource modules, no shared module yet).
343
+ *
344
+ * In regulatoryChanges.list, the validator's outer check is
345
+ * `!Array.isArray(result)` — describeType is only invoked when the
346
+ * result is NOT an array. The Array.isArray branch below is therefore
347
+ * structurally unreachable in this file's call site (kept for helper
348
+ * symmetry with the sibling files where the branch IS reachable —
349
+ * decisions.ts and incidents.ts both call describeType from
350
+ * "expected object, got X" contexts where X may be an array).
351
+ */
352
+ function describeType(value) {
353
+ if (value === null)
354
+ return "null";
355
+ // The validator above guarantees `value` is not an array when this
356
+ // helper is invoked from regulatoryChanges.list. Branch retained
357
+ // for helper symmetry with decisions.ts / incidents.ts (where it
358
+ // IS reachable) and as defense-in-depth if the validator's outer
359
+ // check is ever changed. Coverage marker:
360
+ /* v8 ignore next */
361
+ if (Array.isArray(value))
362
+ return "array";
363
+ return typeof value;
364
+ }
365
+ //# sourceMappingURL=regulatory-changes.js.map