@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,271 @@
1
+ // ─── Server-Sent Events parser ──────────────────────────────────────────────
2
+ //
3
+ // Generic SSE frame parser used by streaming resources (today: decisions.stream;
4
+ // future: any other text/event-stream endpoint). Reads from a
5
+ // `ReadableStreamDefaultReader<Uint8Array>` and yields parsed frames.
6
+ //
7
+ // Spec: https://html.spec.whatwg.org/multipage/server-sent-events.html
8
+ // Subset implemented:
9
+ // - `id:`, `event:`, `data:` field lines (the three the kernel emits today)
10
+ // - Multi-`data:` line concatenation (joined with `\n` per spec)
11
+ // - Comment lines (`:` prefix) — silently dropped (heartbeat handling)
12
+ // - Frames separated by blank line (`\n\n`, `\r\n\r\n`, or `\r\r`)
13
+ // - UTF-8 decoded with `{stream: true}` so multibyte chars split across
14
+ // reads recombine correctly
15
+ // Subset deliberately NOT implemented:
16
+ // - `retry:` field (would map to a reconnect-delay hint; we don't
17
+ // auto-reconnect today — caller manages reconnection by passing the
18
+ // last seen event id back as `lastEventId`)
19
+ // - `BOM` stripping at the very first byte (the kernel's text/event-stream
20
+ // never emits a BOM; defensive at the parser level would be over-spec)
21
+ //
22
+ // The parser is INTENTIONALLY lax about the optional space after the colon:
23
+ // `data:foo` and `data: foo` both yield `foo`. The W3C spec (§ 9.2.6) says
24
+ // "If value starts with a U+0020 SPACE character, remove it." — implemented.
25
+ import { AttestryError } from "./errors.js";
26
+ /**
27
+ * Maximum buffer size before the parser bails. Defense against a server
28
+ * that emits an unbounded frame (no `\n\n` boundary) — without a cap, the
29
+ * parser would buffer until the consumer's process runs out of memory.
30
+ * 1 MiB is generous: the kernel's frame payload is ~500 bytes today, so
31
+ * 1 MiB is ~2000× headroom. A real-world SSE event larger than this is
32
+ * either a server bug or a hostile peer.
33
+ *
34
+ * Configurable via `parseSSE`'s `maxBufferBytes` option for callers
35
+ * with legitimate large-frame use cases (none today). Hostile-review H3.
36
+ */
37
+ const DEFAULT_MAX_BUFFER_BYTES = 1024 * 1024;
38
+ /**
39
+ * Async generator: reads from a stream of UTF-8-encoded SSE bytes and
40
+ * yields each complete frame. Ends when the underlying reader signals
41
+ * `done`. Defensive: a caller that breaks early out of the for-await
42
+ * loop leaves the reader open — call `reader.cancel()` from your finally
43
+ * block (or use the `parseSSEResponse` wrapper below which handles it).
44
+ *
45
+ * @throws AttestryError if a frame line is malformed past recovery (no
46
+ * such case in the kernel's emitter today; future-proofing); OR
47
+ * if the buffer exceeds `maxBufferBytes` (defense against
48
+ * unbounded-frame DoS — hostile-review H3).
49
+ */
50
+ export async function* parseSSE(reader, options = {}) {
51
+ const maxBufferBytes = options.maxBufferBytes ?? DEFAULT_MAX_BUFFER_BYTES;
52
+ const decoder = new TextDecoder("utf-8");
53
+ let buffer = "";
54
+ let bomStripped = false;
55
+ while (true) {
56
+ const { value, done } = await reader.read();
57
+ if (done) {
58
+ // Flush any final bytes (no-op if value was always complete).
59
+ buffer += decoder.decode();
60
+ // A final unterminated frame is dropped per SSE spec § 9.2.4
61
+ // ("If the end of the file is reached, then dispatch the event and
62
+ // proceed."). Be permissive and emit the trailing block if it has
63
+ // content — an HTTP/1.1 connection-close mid-frame is the most
64
+ // common failure mode and dropping the partial event silently is
65
+ // worse than emitting an under-defined one.
66
+ if (buffer.length > 0) {
67
+ const frame = parseFrameBlock(buffer);
68
+ if (frame !== null)
69
+ yield frame;
70
+ }
71
+ return;
72
+ }
73
+ if (value === undefined)
74
+ continue;
75
+ // `{stream: true}` carries multi-byte boundaries across chunks.
76
+ buffer += decoder.decode(value, { stream: true });
77
+ // Strip a leading BOM (U+FEFF) if present. Kernel doesn't emit BOM
78
+ // today, but a misconfigured proxy / character-set transcoder
79
+ // could prepend one. Without this strip, the first frame's first
80
+ // field would be e.g. `"id"` instead of `"id"` and silently
81
+ // drop. Hostile-review H4. One-shot — only at the very first
82
+ // decoded codepoint of the stream.
83
+ //
84
+ // Hostile-review fix (parallel to ndjson-parser): `bomStripped` is
85
+ // only flipped once we've actually decoded at least one character.
86
+ // Otherwise an empty / partial-multibyte first read would set the
87
+ // flag prematurely; if the BOM bytes arrived in a later read, the
88
+ // strip-attempt would be skipped. Defense-in-depth — String.prototype.trim
89
+ // and TextDecoder("utf-8")'s default stream-start BOM handling
90
+ // mask the consumer-visible bug today, but a future TextDecoder
91
+ // change (e.g. `ignoreBOM: true`) would unmask it.
92
+ if (!bomStripped && buffer.length > 0) {
93
+ bomStripped = true;
94
+ // Defense-in-depth dead path: same rationale as
95
+ // ndjson-parser.ts — TextDecoder strips stream-start BOM
96
+ // internally, so the slice below is unreachable in Node 18+.
97
+ // Marked for v8 coverage to make the intentional defense visible;
98
+ // a future `{ignoreBOM: true}` decoder OR a buffer path that
99
+ // bypasses TextDecoder would unmask it.
100
+ /* v8 ignore next 3 */
101
+ if (buffer.charCodeAt(0) === 0xfeff) {
102
+ buffer = buffer.slice(1);
103
+ }
104
+ }
105
+ // Defense in depth: if the buffer grew past the cap WITHOUT
106
+ // a frame boundary, abort rather than spin until OOM. Hostile-
107
+ // review H3.
108
+ if (buffer.length > maxBufferBytes) {
109
+ throw new AttestryError(`SSE frame exceeded maximum buffer size (${maxBufferBytes} bytes) — server emitted an unbounded frame or omitted the boundary delimiter`);
110
+ }
111
+ // Find frame boundaries. Spec allows `\r\n\r\n`, `\n\n`, or `\r\r`
112
+ // as the separator. Scan with a regex that accepts any.
113
+ while (true) {
114
+ const match = FRAME_BOUNDARY.exec(buffer);
115
+ if (match === null)
116
+ break;
117
+ const block = buffer.slice(0, match.index);
118
+ buffer = buffer.slice(match.index + match[0].length);
119
+ if (block.length === 0)
120
+ continue; // back-to-back blank lines
121
+ const frame = parseFrameBlock(block);
122
+ if (frame !== null)
123
+ yield frame;
124
+ }
125
+ }
126
+ }
127
+ /**
128
+ * Convenience wrapper around `parseSSE` that accepts a `Response`,
129
+ * derives the reader, and ALWAYS calls `reader.cancel()` in a `finally`
130
+ * block — including when a caller breaks out of the for-await loop
131
+ * early. Without this, an early `break` leaks the underlying connection.
132
+ *
133
+ * Resources should call this rather than `parseSSE` directly.
134
+ */
135
+ export async function* parseSSEResponse(response, options = {}) {
136
+ if (response.body === null) {
137
+ // 204 No Content / empty body — iterator ends with zero frames.
138
+ // Consumer's for-await loop runs zero times. (Throwing here would
139
+ // be over-strict; the server may legitimately close before any data.)
140
+ return;
141
+ }
142
+ const reader = response.body.getReader();
143
+ try {
144
+ yield* parseSSE(reader, options);
145
+ }
146
+ finally {
147
+ // Release the lock + signal cancellation upstream. Wrapped in
148
+ // try/catch because cancel() on an already-closed reader rejects.
149
+ try {
150
+ await reader.cancel();
151
+ }
152
+ catch {
153
+ // Already canceled / closed — nothing to do.
154
+ }
155
+ }
156
+ }
157
+ // ─── Internals ──────────────────────────────────────────────────────────────
158
+ /**
159
+ * One block of SSE text (delimited by blank line) → a parsed frame, or
160
+ * `null` if the block was nothing but comments / whitespace.
161
+ *
162
+ * Per spec:
163
+ * - Lines beginning with `:` are comments, ignored.
164
+ * - Otherwise split on first `:` into `field` + `value`.
165
+ * - If `value` starts with U+0020 SPACE, remove ONE leading space.
166
+ * - Multiple `data:` lines accumulate, joined by `\n`.
167
+ * - Field names other than `id`, `event`, `data`, `retry` are ignored.
168
+ */
169
+ function parseFrameBlock(block) {
170
+ let id;
171
+ let eventType;
172
+ let dataLines = null;
173
+ let sawNonComment = false;
174
+ // Split on any of `\r\n`, `\n`, or `\r` per spec. Use a character
175
+ // class regex so we don't hit the trailing-empty-string edge case
176
+ // that String.split with a multi-char separator can produce.
177
+ for (const line of block.split(/\r\n|\n|\r/)) {
178
+ if (line.length === 0)
179
+ continue;
180
+ if (line[0] === ":")
181
+ continue; // comment
182
+ sawNonComment = true;
183
+ const colonIdx = line.indexOf(":");
184
+ let field;
185
+ let value;
186
+ if (colonIdx === -1) {
187
+ // Per spec: "If line contains no U+003A COLON character, set field to
188
+ // line and value to the empty string." — we don't recognize any
189
+ // value-less fields today, so silently drop these.
190
+ field = line;
191
+ value = "";
192
+ }
193
+ else {
194
+ field = line.slice(0, colonIdx);
195
+ value = line.slice(colonIdx + 1);
196
+ if (value.length > 0 && value[0] === " ")
197
+ value = value.slice(1);
198
+ }
199
+ switch (field) {
200
+ case "id":
201
+ // Per spec: "If the field value does not contain U+0000 NULL,
202
+ // then set the last event ID buffer to the field value."
203
+ if (!value.includes(""))
204
+ id = value;
205
+ break;
206
+ case "event":
207
+ eventType = value;
208
+ break;
209
+ case "data":
210
+ if (dataLines === null)
211
+ dataLines = [];
212
+ dataLines.push(value);
213
+ break;
214
+ case "retry":
215
+ // Reconnect-delay hint. Not implemented — see file header.
216
+ break;
217
+ default:
218
+ // Unknown field — silently ignored per spec. Don't throw; future
219
+ // server-side fields shouldn't break consumers.
220
+ break;
221
+ }
222
+ }
223
+ if (!sawNonComment)
224
+ return null;
225
+ // Defense in depth: if EVERY non-comment line was an unrecognized
226
+ // field, we still produced no data. Emitting an empty-data event is
227
+ // confusing — drop. Consumer never has to handle a frame with no
228
+ // content. (SSE spec strictly says emit anyway, but our wire shape
229
+ // always carries data.)
230
+ if (dataLines === null) {
231
+ if (id === undefined && eventType === undefined)
232
+ return null;
233
+ // id-only or event-only is unusual but valid — the kernel doesn't
234
+ // emit these today, but defensively pass them through with empty data
235
+ // rather than swallowing the metadata entirely.
236
+ return { id, event: eventType, data: "" };
237
+ }
238
+ return {
239
+ id,
240
+ event: eventType,
241
+ data: dataLines.join("\n"),
242
+ };
243
+ }
244
+ /**
245
+ * Frame-boundary regex — matches `\r\n\r\n`, `\n\n`, or `\r\r` per spec.
246
+ * Exported only for tests.
247
+ */
248
+ const FRAME_BOUNDARY = /\r\n\r\n|\n\n|\r\r/g;
249
+ /**
250
+ * Parse a `data:` payload as JSON. Throws `AttestryError` (with the
251
+ * underlying error as `cause`) if the payload isn't valid JSON. Resources
252
+ * call this when the wire shape is documented as JSON-encoded.
253
+ */
254
+ export function parseSSEData(data) {
255
+ try {
256
+ return JSON.parse(data);
257
+ }
258
+ catch (err) {
259
+ throw new AttestryError(`SSE frame data was not valid JSON: ${
260
+ // JSON.parse always throws SyntaxError (an Error subclass), so
261
+ // the String(err) branch is unreachable. Defense-in-depth.
262
+ /* v8 ignore next */
263
+ err instanceof Error ? err.message : String(err)}`, { cause: err });
264
+ }
265
+ }
266
+ // Test-only handles for direct pinning of internals.
267
+ export const __test__ = {
268
+ FRAME_BOUNDARY,
269
+ parseFrameBlock,
270
+ };
271
+ //# sourceMappingURL=sse-parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sse-parser.js","sourceRoot":"","sources":["../src/sse-parser.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,142 @@
1
+ import { type RetryOptions } from "./retry.js";
2
+ import type { AttestryClientOptions, FetchLike, RequestOptions } from "./types.js";
3
+ interface ResolvedClientConfig {
4
+ apiKey: string;
5
+ baseUrl: string;
6
+ fetchImpl: FetchLike;
7
+ timeoutMs: number;
8
+ retry: Partial<RetryOptions> | undefined;
9
+ }
10
+ /**
11
+ * Validates options at construction time. Surfaces every misconfiguration
12
+ * as `AttestryError` BEFORE any request is made — fail-fast beats failing
13
+ * deep in the resource methods. Used by `AttestryClient` constructor.
14
+ */
15
+ export declare function resolveClientConfig(options: AttestryClientOptions): ResolvedClientConfig;
16
+ /**
17
+ * Internal — exported for tests. Composes a URL safely from baseUrl + path.
18
+ * Caller-supplied `path` may or may not start with `/`; either way works.
19
+ */
20
+ export declare function composeUrl(baseUrl: string, path: string): string;
21
+ interface InternalRequestArgs {
22
+ config: ResolvedClientConfig;
23
+ method: "GET" | "POST" | "PATCH" | "DELETE";
24
+ path: string;
25
+ body?: unknown;
26
+ query?: Record<string, string | number | boolean | undefined | null>;
27
+ options?: RequestOptions;
28
+ /**
29
+ * P3 hardening: expected response Content-Type for the 2xx success
30
+ * path. Defaults to `"application/json"`. The transport's success
31
+ * path checks the response's Content-Type header against this value
32
+ * (MIME-type prefix only, parameter-tolerant for `; charset=utf-8`)
33
+ * and throws `AttestryAPIError` on mismatch — fail-fast against a
34
+ * misbehaving proxy / load balancer that returns 200 OK with an
35
+ * HTML error page or text/plain body. Pre-P3 the SDK silently
36
+ * resolved with `null` (or whatever JSON.parse returned) for
37
+ * wrong-content-type responses; consumers crashed downstream with
38
+ * a confusing TypeError. Now consumers receive a clear
39
+ * AttestryAPIError naming the expected and actual content-type.
40
+ *
41
+ * Skipped for 204 No Content and 304 Not Modified (no body to
42
+ * validate). Error path (non-2xx) is unchanged — the existing
43
+ * error-body parser still falls back to raw text for non-JSON
44
+ * error bodies (see hostile-review H1 pin in transport.test.ts).
45
+ *
46
+ * Symmetric with `streamRequestOnce`'s `expectedContentType` param
47
+ * (which defaults to `text/event-stream` for SSE). Both use the
48
+ * same MIME-type-extraction algorithm to defend against superset /
49
+ * parameter-injection / structured-suffix attacks.
50
+ */
51
+ expectedContentType?: string;
52
+ }
53
+ /**
54
+ * Public request runner. Wraps the single-attempt `requestOnce` with the
55
+ * retry loop. Resources should NOT import this directly — they go
56
+ * through `AttestryClient._request`.
57
+ *
58
+ * Retry behavior: on 429 only (today). The retry loop honors the caller's
59
+ * abort signal — `signal.abort()` during the backoff sleep rejects with
60
+ * `AttestryError("request aborted by caller")` rather than completing
61
+ * the wait.
62
+ */
63
+ export declare function request<T>(args: InternalRequestArgs): Promise<T>;
64
+ /**
65
+ * Internal single-attempt request runner. Exposed under `__test__` for
66
+ * unit pinning.
67
+ */
68
+ export declare function requestOnce<T>(args: InternalRequestArgs): Promise<T>;
69
+ /**
70
+ * Streaming-response request — used for any long-lived streaming endpoint
71
+ * (SSE, NDJSON). Mirrors `request` for header / signal / URL composition
72
+ * but:
73
+ *
74
+ * - Hardcodes `method: "GET"` (streaming endpoints today are read-only).
75
+ * - Sends `Accept: <expectedContentType>` (default `text/event-stream`
76
+ * for SSE backward-compat; NDJSON callers pass
77
+ * `application/x-ndjson`).
78
+ * - Does NOT arm an internal timeout — streams are long-lived; the
79
+ * 30s default would kill them after half a minute. Caller controls
80
+ * duration via `options.signal`. (`decisions.export` runs up to
81
+ * 5 minutes server-side.)
82
+ * - Returns the un-consumed `Response` so the resource can read
83
+ * `response.body` as a stream of bytes (for an SSE / NDJSON parser).
84
+ * - On non-2xx, drains the body and throws `AttestryAPIError` exactly
85
+ * like the JSON path so consumers see the same error shape.
86
+ * - On a 2xx with the WRONG content-type, throws with a clear message —
87
+ * defensive against a misconfigured proxy returning `text/html` (LB
88
+ * error page) or `text/plain`. The kernel always sets the right
89
+ * content-type for each route, so this is a fail-fast guard. The
90
+ * `expectedContentType` parameter (default `text/event-stream`)
91
+ * drives both the `Accept:` request header AND this guard, so SSE
92
+ * callers reject NDJSON responses and vice versa — a single source
93
+ * of truth.
94
+ *
95
+ * Retry semantics: the INITIAL fetch goes through the retry wrapper
96
+ * (same 429 logic as `request<T>`). Once the response is open,
97
+ * mid-iteration errors do NOT retry — events would be lost or
98
+ * duplicated without per-event idempotency. Caller manages reconnection
99
+ * by passing the last seen cursor back (e.g., SSE `lastEventId`,
100
+ * NDJSON: re-issue with adjusted filters).
101
+ */
102
+ export declare function streamRequest(args: {
103
+ config: ResolvedClientConfig;
104
+ path: string;
105
+ query?: Record<string, string | number | boolean | undefined | null>;
106
+ headers?: Record<string, string>;
107
+ options?: RequestOptions;
108
+ /**
109
+ * Substring (case-insensitive) the response's `Content-Type` header
110
+ * must contain to pass the fail-fast guard. ALSO drives the request's
111
+ * `Accept:` header. Defaults to `"text/event-stream"` for SSE
112
+ * backward-compat. NDJSON callers (`decisions.export`) pass
113
+ * `"application/x-ndjson"`. Single source of truth — same value
114
+ * everywhere keeps SSE callers from accidentally accepting NDJSON
115
+ * responses and vice versa.
116
+ */
117
+ expectedContentType?: string;
118
+ }): Promise<Response>;
119
+ declare function encodeQuery(q: Record<string, string | number | boolean | undefined | null> | undefined): string;
120
+ /**
121
+ * Reads the response body and tries to parse as JSON. Returns BOTH the
122
+ * parsed value (or null on parse failure / empty body) AND the raw text.
123
+ * Callers on the success path use only `parsed`; callers on the error
124
+ * path fall back to `raw` when `parsed` is null so non-JSON error bodies
125
+ * (LB 502 HTML, plain-text proxy errors) are surfaced as
126
+ * `AttestryAPIError.details` rather than silently lost. (Hostile-review
127
+ * H1.)
128
+ */
129
+ declare function readBody(response: Response): Promise<{
130
+ parsed: unknown;
131
+ raw: string;
132
+ }>;
133
+ declare function extractMessage(detail: unknown): string | null;
134
+ export declare const __test__: {
135
+ DEFAULT_BASE_URL: string;
136
+ DEFAULT_TIMEOUT_MS: number;
137
+ encodeQuery: typeof encodeQuery;
138
+ readBody: typeof readBody;
139
+ extractMessage: typeof extractMessage;
140
+ };
141
+ export {};
142
+ //# sourceMappingURL=transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AA6BA,OAAO,EAML,KAAK,YAAY,EAClB,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EACV,qBAAqB,EACrB,SAAS,EACT,cAAc,EACf,MAAM,YAAY,CAAC;AAKpB,UAAU,oBAAoB;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,SAAS,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,SAAS,CAAC;CAC1C;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,qBAAqB,GAC7B,oBAAoB,CAwCtB;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAGhE;AAED,UAAU,mBAAmB;IAC3B,MAAM,EAAE,oBAAoB,CAAC;IAC7B,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,IAAI,CAAC,CAAC;IACrE,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;;;;GASG;AACH,wBAAsB,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,CAAC,CAAC,CAsBtE;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAAC,CAAC,EAAE,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,CAAC,CAAC,CA8I1E;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE;IACxC,MAAM,EAAE,oBAAoB,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,IAAI,CAAC,CAAC;IACrE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB;;;;;;;;OAQG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B,GAAG,OAAO,CAAC,QAAQ,CAAC,CAyBpB;AA+HD,iBAAS,WAAW,CAClB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,IAAI,CAAC,GAAG,SAAS,GAC1E,MAAM,CAQR;AAED;;;;;;;;GAQG;AACH,iBAAe,QAAQ,CACrB,QAAQ,EAAE,QAAQ,GACjB,OAAO,CAAC;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAS3C;AAED,iBAAS,cAAc,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CActD;AAcD,eAAO,MAAM,QAAQ;;;;;;CAMpB,CAAC"}