contree-client 0.1.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.
@@ -0,0 +1,159 @@
1
+ /** Contree configuration profiles.
2
+ *
3
+ * The INI files under `$CONTREE_HOME`: `auth.ini`
4
+ * (secrets) merged over `$CONTREE_HOME/cli.ini` (non-secret defaults),
5
+ * where CONTREE_HOME defaults to `$XDG_CONFIG_HOME/contree` and
6
+ * finally `~/.config/contree`. Node-only: the filesystem modules are
7
+ * loaded lazily so browser bundlers never pull them in.
8
+ */
9
+
10
+ import { ContreeError } from "./errors.js";
11
+ import { DEFAULT_BASE_URL } from "./specInfo.js";
12
+
13
+ export const AUTH_TYPE_IAM = "iam";
14
+ export const AUTH_TYPE_JWT = "jwt";
15
+ export const PROFILE_PREFIX = "profile:";
16
+ export const DEFAULT_PROFILE = "default";
17
+
18
+ export class ProfileError extends ContreeError {}
19
+
20
+ function record() {
21
+ // a null-prototype object: keys come from an external file, and a
22
+ // key like "__proto__" must become plain data, never touch the
23
+ // prototype chain of anything process-wide
24
+ return Object.create(null);
25
+ }
26
+
27
+ /** A minimal INI parser covering the profile-file dialect: `[section]` headers, `key = value` pairs, `#`/`;` comments.
28
+ * `[DEFAULT]` and keys outside any section land in the "" (defaults)
29
+ * entry - exactly like ConfigParser's magic DEFAULT section. Returns
30
+ * a Map of section name to a null-prototype key/value object. */
31
+ export function parseIni(text) {
32
+ const sections = new Map([["", record()]]);
33
+ let current = "";
34
+ for (const rawLine of text.split(/\r?\n/)) {
35
+ const line = rawLine.trim();
36
+ if (!line || line.startsWith("#") || line.startsWith(";")) {
37
+ continue;
38
+ }
39
+ if (line.startsWith("[") && line.endsWith("]")) {
40
+ const name = line.slice(1, -1).trim();
41
+ current = name === "DEFAULT" ? "" : name;
42
+ if (!sections.has(current)) {
43
+ sections.set(current, record());
44
+ }
45
+ continue;
46
+ }
47
+ const eq = line.indexOf("=");
48
+ if (eq < 0) {
49
+ continue;
50
+ }
51
+ const key = line.slice(0, eq).trim().toLowerCase();
52
+ sections.get(current)[key] = line.slice(eq + 1).trim();
53
+ }
54
+ return sections;
55
+ }
56
+
57
+ async function contreeHome() {
58
+ const { homedir } = await import("node:os");
59
+ const { join } = await import("node:path");
60
+ const home = process.env.CONTREE_HOME;
61
+ if (home) {
62
+ return home;
63
+ }
64
+ const xdg = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
65
+ return join(xdg, "contree");
66
+ }
67
+
68
+ async function readIni(path) {
69
+ const { readFile } = await import("node:fs/promises");
70
+ try {
71
+ return parseIni(await readFile(path, "utf-8"));
72
+ } catch (error) {
73
+ if (error?.code === "ENOENT") {
74
+ return new Map([["", record()]]);
75
+ }
76
+ // a permission or I/O failure must not masquerade as a missing
77
+ // profile - surface it
78
+ throw error;
79
+ }
80
+ }
81
+
82
+ /** Read all profiles; resolves to `{profiles, active}`.
83
+ *
84
+ * `cli.ini` is read first, then `auth.ini` - values from `auth.ini`
85
+ * win on conflicts. `[DEFAULT]` values are inherited by every profile
86
+ * section (ConfigParser semantics, like the Python client).
87
+ */
88
+ export async function loadProfiles(path = null) {
89
+ const { dirname, join } = await import("node:path");
90
+ const authFile = path ?? join(await contreeHome(), "auth.ini");
91
+ const merged = new Map([["", record()]]);
92
+ for (const file of [join(dirname(authFile), "cli.ini"), authFile]) {
93
+ for (const [section, values] of await readIni(file)) {
94
+ const target = merged.get(section) ?? record();
95
+ Object.assign(target, values);
96
+ merged.set(section, target);
97
+ }
98
+ }
99
+ const defaults = merged.get("");
100
+ const active = defaults.profile ?? DEFAULT_PROFILE;
101
+ const profiles = record();
102
+ for (const [section, values] of merged) {
103
+ if (!section.startsWith(PROFILE_PREFIX)) {
104
+ continue;
105
+ }
106
+ const name = section.slice(PROFILE_PREFIX.length);
107
+ // ConfigParser exposes DEFAULT values through every section
108
+ const effective = record();
109
+ Object.assign(effective, defaults, values);
110
+ const authType = effective.type ?? AUTH_TYPE_JWT;
111
+ const defaultUrl = authType === AUTH_TYPE_IAM ? DEFAULT_BASE_URL : "";
112
+ profiles[name] = {
113
+ name,
114
+ token: effective.token ?? null,
115
+ url: (effective.url ?? defaultUrl).replace(/\/+$/, ""),
116
+ authType,
117
+ project: effective.project ?? null,
118
+ };
119
+ }
120
+ return { profiles, active };
121
+ }
122
+
123
+ /** Build a profile from the environment, bypassing config files.
124
+ *
125
+ * Reads the standard Contree variables: `CONTREE_TOKEN`
126
+ * (or `NEBIUS_API_KEY`), `CONTREE_URL` and optionally
127
+ * `CONTREE_PROJECT` (or `NEBIUS_AI_PROJECT`). Returns null unless
128
+ * both a token and a URL are present - a non-null result means the
129
+ * environment fully described a profile.
130
+ */
131
+ export function fromEnv() {
132
+ const env = globalThis.process?.env ?? {};
133
+ const token = env.CONTREE_TOKEN || env.NEBIUS_API_KEY || null;
134
+ const url = env.CONTREE_URL || null;
135
+ if (!token || !url) {
136
+ return null;
137
+ }
138
+ return {
139
+ name: "environment",
140
+ token,
141
+ url: url.replace(/\/+$/, ""),
142
+ authType: AUTH_TYPE_JWT,
143
+ project: env.CONTREE_PROJECT ?? env.NEBIUS_AI_PROJECT ?? null,
144
+ };
145
+ }
146
+
147
+ /** Resolve the active profile by name.
148
+ *
149
+ * Priority: explicit *name* > `CONTREE_PROFILE` environment variable
150
+ * > the active profile recorded in the config file (`[DEFAULT]`).
151
+ */
152
+ export async function resolveProfile(name = null, { path = null } = {}) {
153
+ const { profiles, active } = await loadProfiles(path);
154
+ const selected = name || process.env.CONTREE_PROFILE || active;
155
+ if (!(selected in profiles)) {
156
+ throw new ProfileError(`profile ${JSON.stringify(selected)} not found`);
157
+ }
158
+ return profiles[selected];
159
+ }
@@ -0,0 +1,93 @@
1
+ export declare const CHUNK_SIZE: number;
2
+ export declare const PACKAGE_VERSION: string;
3
+ export declare const UA_PRODUCT: string;
4
+ export declare const IS_NODE: boolean;
5
+ export declare const UA_RUNTIME: string;
6
+ export declare const UA_PLATFORM: string;
7
+ export declare const RETRY_DELAYS: number[];
8
+ export declare const TIGHT_LOOP_FLOOR: number;
9
+
10
+ export interface ResponseData {
11
+ status: number;
12
+ headers: Record<string, string>;
13
+ body: Uint8Array;
14
+ /** the final URL after redirects, when the transport reports it */
15
+ url?: string;
16
+ }
17
+
18
+ export type RequestBody =
19
+ Uint8Array | string | Blob | ReadableStream<Uint8Array> | null;
20
+
21
+ export interface RequestSpec {
22
+ method: string;
23
+ path: string;
24
+ query?: Record<string, string>;
25
+ headers?: Record<string, string>;
26
+ body?: RequestBody;
27
+ contentType?: string | null;
28
+ accept?: string | null;
29
+ idempotent?: boolean;
30
+ redirect?: "manual" | "follow";
31
+ /** absolute deadline in monotonic seconds; bounds SSE idle waits */
32
+ deadline?: number | null;
33
+ }
34
+
35
+ export declare function retryDelays(delays?: number[]): Generator<number>;
36
+
37
+ export declare class RetryPolicy {
38
+ statuses: number[];
39
+ serverErrors: boolean;
40
+ delays: number[];
41
+ maxAttempts: number | null;
42
+ retryUnsafe: boolean;
43
+ constructor(options?: {
44
+ statuses?: number[];
45
+ serverErrors?: boolean;
46
+ delays?: number[];
47
+ maxAttempts?: number | null;
48
+ retryUnsafe?: boolean;
49
+ });
50
+ retryableStatus(status: number): boolean;
51
+ }
52
+
53
+ export declare function parseRetryAfter(
54
+ value: string | null | undefined,
55
+ ): number | null;
56
+ export declare function retryAfterDelay(response: ResponseData): number | null;
57
+ export declare function sleep(seconds: number): Promise<void>;
58
+ export declare function monotonic(): number;
59
+ export declare function isUuid(ref: string): boolean;
60
+ export declare function quotePath(value: unknown): string;
61
+ export declare function encodeQuery(query: Record<string, string>): string;
62
+ export declare function formatTimeParam(value: string | number | Date): string;
63
+ export declare function parseDatetime(value: string): Date;
64
+ export declare function bytesToText(bytes: Uint8Array): string;
65
+ export declare function textToBytes(text: string): Uint8Array;
66
+ export declare function bytesToBase64(bytes: Uint8Array): string;
67
+ export declare function base64ToBytes(value: string): Uint8Array;
68
+ export declare function sha256(
69
+ content: Uint8Array | string | Blob | ReadableStream<Uint8Array>,
70
+ ): Promise<string | null>;
71
+ export declare function jsonBody(response: ResponseData): unknown;
72
+ export declare function jsonObject(
73
+ response: ResponseData,
74
+ ): Record<string, unknown>;
75
+ export declare function jsonArray(response: ResponseData): unknown[];
76
+ export declare function errorForResponse(response: ResponseData): Error;
77
+
78
+ export interface SSEFrame {
79
+ id: number | null;
80
+ event: string | null;
81
+ data: string;
82
+ }
83
+
84
+ export declare class SSEParser {
85
+ static MAX_BUFFER: number;
86
+ feed(chunk: Uint8Array): SSEFrame[];
87
+ flush(): SSEFrame | null;
88
+ }
89
+
90
+ export declare function decodeFramePayload(
91
+ frame: SSEFrame,
92
+ lastEventId?: number | null,
93
+ ): Record<string, unknown> | null;
package/lib/runtime.js ADDED
@@ -0,0 +1,415 @@
1
+ /** Transport-agnostic request/response plumbing and SSE parsing.
2
+ *
3
+ * A line-by-line port of contree_client/runtime.py adapted to the
4
+ * fetch platform: gzip decoding and connection pooling are handled by
5
+ * fetch itself, so only the protocol logic lives here.
6
+ */
7
+
8
+ import {
9
+ ContreeAPIError,
10
+ ERROR_CLASSES,
11
+ ServerError,
12
+ SSEStreamError,
13
+ } from "./errors.js";
14
+
15
+ export const CHUNK_SIZE = 65536;
16
+
17
+ export const PACKAGE_VERSION = "0.1.0";
18
+ export const UA_PRODUCT = `contree-client-js/${PACKAGE_VERSION}`;
19
+
20
+ const NODE_VERSION =
21
+ typeof process !== "undefined" && process.versions?.node
22
+ ? process.versions.node
23
+ : null;
24
+ // browsers refuse to set the User-Agent header (a forbidden header
25
+ // name): the composed string is only ATTACHED as a header in Node,
26
+ // though userAgent() reports it everywhere
27
+ export const IS_NODE = NODE_VERSION !== null;
28
+ export const UA_RUNTIME =
29
+ NODE_VERSION !== null ? `Node.js/${NODE_VERSION}` : "browser";
30
+ export const UA_PLATFORM =
31
+ NODE_VERSION !== null
32
+ ? `${process.platform}-${process.arch}`
33
+ : (globalThis.navigator?.userAgent ?? "");
34
+
35
+ export const RETRY_DELAYS = Object.freeze([0.1, 0.2, 0.5, 1.0, 2.0, 5.0]);
36
+
37
+ // floor for reconnect loops that made no forward progress, so a server
38
+ // returning immediate empty streams does not spin the client
39
+ export const TIGHT_LOOP_FLOOR = 0.5;
40
+
41
+ /** An endless ladder of backoff delays: the ladder is walked once and
42
+ * then the tail delay repeats forever. */
43
+ export function* retryDelays(delays = RETRY_DELAYS) {
44
+ yield* delays;
45
+ for (;;) {
46
+ yield delays[delays.length - 1];
47
+ }
48
+ }
49
+
50
+ /** Opt-in retries for transient failures of buffered requests. */
51
+ export class RetryPolicy {
52
+ constructor({
53
+ statuses = [410, 425],
54
+ serverErrors = true,
55
+ delays = RETRY_DELAYS,
56
+ maxAttempts = 10,
57
+ retryUnsafe = false,
58
+ } = {}) {
59
+ if (!delays.length) {
60
+ throw new RangeError("RetryPolicy delays must not be empty");
61
+ }
62
+ for (const delay of delays) {
63
+ if (!Number.isFinite(delay) || delay < 0) {
64
+ throw new RangeError(
65
+ `RetryPolicy delays must be finite and non-negative, got ${delay}`,
66
+ );
67
+ }
68
+ }
69
+ if (
70
+ maxAttempts !== null &&
71
+ (!Number.isInteger(maxAttempts) || maxAttempts < 1)
72
+ ) {
73
+ throw new RangeError("RetryPolicy maxAttempts must be an integer >= 1");
74
+ }
75
+ // defensive copies, frozen: a policy shared between clients must
76
+ // not be mutable from the outside after validation
77
+ this.statuses = Object.freeze([...statuses]);
78
+ this.serverErrors = serverErrors;
79
+ this.delays = Object.freeze([...delays]);
80
+ this.maxAttempts = maxAttempts;
81
+ this.retryUnsafe = retryUnsafe;
82
+ Object.freeze(this);
83
+ }
84
+
85
+ retryableStatus(status) {
86
+ if (this.statuses.includes(status)) {
87
+ return true;
88
+ }
89
+ return this.serverErrors && status >= 500 && status < 600;
90
+ }
91
+ }
92
+
93
+ /** Parse a Retry-After header: delta-seconds or an HTTP-date.
94
+ * Negative values clamp to 0; anything unparsable reports null. */
95
+ export function parseRetryAfter(value) {
96
+ if (value === null || value === undefined) {
97
+ return null;
98
+ }
99
+ const seconds = Number(value);
100
+ // Number("Infinity") parses: an infinite Retry-After would sleep
101
+ // forever (or collapse into an immediate retry storm)
102
+ if (Number.isFinite(seconds) && value.trim() !== "") {
103
+ return Math.max(0, seconds);
104
+ }
105
+ const moment = Date.parse(value);
106
+ if (Number.isNaN(moment)) {
107
+ return null;
108
+ }
109
+ return Math.max(0, (moment - Date.now()) / 1000);
110
+ }
111
+
112
+ export function retryAfterDelay(response) {
113
+ return parseRetryAfter(response.headers["retry-after"] ?? null);
114
+ }
115
+
116
+ export function sleep(seconds) {
117
+ return new Promise((resolve) => setTimeout(resolve, seconds * 1000));
118
+ }
119
+
120
+ export function monotonic() {
121
+ return performance.now() / 1000;
122
+ }
123
+
124
+ const UUID_RE =
125
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
126
+
127
+ /** True if *ref* looks like an image/operation UUID. */
128
+ export function isUuid(ref) {
129
+ return UUID_RE.test(ref);
130
+ }
131
+
132
+ export function quotePath(value) {
133
+ return encodeURIComponent(String(value));
134
+ }
135
+
136
+ export function encodeQuery(query) {
137
+ return Object.entries(query)
138
+ .map(
139
+ ([key, value]) =>
140
+ `${encodeURIComponent(key)}=${encodeURIComponent(value).replaceAll("%2F", "/")}`,
141
+ )
142
+ .join("&");
143
+ }
144
+
145
+ export function formatTimeParam(value) {
146
+ if (value instanceof Date) {
147
+ return value.toISOString();
148
+ }
149
+ return String(value);
150
+ }
151
+
152
+ /** Parse an ISO 8601 timestamp, tolerating nanosecond precision. */
153
+ export function parseDatetime(value) {
154
+ // Date.parse only accepts millisecond precision: trim the tail
155
+ const trimmed = value.replace(/(\.\d{3})\d+/, "$1");
156
+ const moment = new Date(trimmed);
157
+ if (Number.isNaN(moment.getTime())) {
158
+ throw new RangeError(`unparsable datetime: ${value}`);
159
+ }
160
+ return moment;
161
+ }
162
+
163
+ const textDecoder = new TextDecoder();
164
+ const textEncoder = new TextEncoder();
165
+
166
+ export function bytesToText(bytes) {
167
+ return textDecoder.decode(bytes);
168
+ }
169
+
170
+ export function textToBytes(text) {
171
+ return textEncoder.encode(text);
172
+ }
173
+
174
+ export function bytesToBase64(bytes) {
175
+ if (typeof Buffer !== "undefined") {
176
+ return Buffer.from(bytes).toString("base64");
177
+ }
178
+ let binary = "";
179
+ for (const byte of bytes) {
180
+ binary += String.fromCharCode(byte);
181
+ }
182
+ return btoa(binary);
183
+ }
184
+
185
+ export function base64ToBytes(value) {
186
+ if (typeof Buffer !== "undefined") {
187
+ return new Uint8Array(Buffer.from(value, "base64"));
188
+ }
189
+ const binary = atob(value);
190
+ const bytes = new Uint8Array(binary.length);
191
+ for (let index = 0; index < binary.length; index += 1) {
192
+ bytes[index] = binary.charCodeAt(index);
193
+ }
194
+ return bytes;
195
+ }
196
+
197
+ /** The sha256 hexdigest of an upload payload.
198
+ *
199
+ * Accepts Uint8Array, string or Blob; a ReadableStream cannot be
200
+ * re-read for the subsequent upload, so it reports null (the caller
201
+ * skips deduplication). Blobs hash chunk by chunk in Node (10 GiB
202
+ * must not materialize); browsers fall back to one arrayBuffer().
203
+ */
204
+ export async function sha256(content) {
205
+ if (typeof content === "string") {
206
+ content = textToBytes(content);
207
+ }
208
+ if (content instanceof Uint8Array) {
209
+ // the webcrypto global only appeared in Node 19: fall back to
210
+ // node:crypto on Node 18 (browsers always have globalThis.crypto)
211
+ const subtle = globalThis.crypto?.subtle;
212
+ if (subtle === undefined) {
213
+ const { createHash } = await import("node:crypto");
214
+ return createHash("sha256").update(content).digest("hex");
215
+ }
216
+ const digest = await subtle.digest("SHA-256", content);
217
+ return hex(new Uint8Array(digest));
218
+ }
219
+ if (typeof Blob !== "undefined" && content instanceof Blob) {
220
+ if (NODE_VERSION !== null) {
221
+ // node-only module, loaded lazily so browser bundlers never see it
222
+ const { createHash } = await import("node:crypto");
223
+ const digest = createHash("sha256");
224
+ for await (const chunk of content.stream()) {
225
+ digest.update(chunk);
226
+ }
227
+ return digest.digest("hex");
228
+ }
229
+ const digest = await globalThis.crypto.subtle.digest(
230
+ "SHA-256",
231
+ await content.arrayBuffer(),
232
+ );
233
+ return hex(new Uint8Array(digest));
234
+ }
235
+ return null;
236
+ }
237
+
238
+ function hex(bytes) {
239
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join(
240
+ "",
241
+ );
242
+ }
243
+
244
+ export function jsonBody(response) {
245
+ return JSON.parse(bytesToText(response.body));
246
+ }
247
+
248
+ export function jsonObject(response) {
249
+ const data = jsonBody(response);
250
+ if (data === null || typeof data !== "object" || Array.isArray(data)) {
251
+ throw new ContreeAPIError(response.status, "expected a JSON object");
252
+ }
253
+ return data;
254
+ }
255
+
256
+ export function jsonArray(response) {
257
+ const data = jsonBody(response);
258
+ if (!Array.isArray(data)) {
259
+ throw new ContreeAPIError(response.status, "expected a JSON array");
260
+ }
261
+ return data;
262
+ }
263
+
264
+ /** Build the exception matching an error response. */
265
+ export function errorForResponse(response) {
266
+ let error = bytesToText(response.body);
267
+ let traceback = null;
268
+ let payload = null;
269
+ try {
270
+ payload = JSON.parse(error);
271
+ } catch {
272
+ payload = null;
273
+ }
274
+ if (
275
+ payload !== null &&
276
+ typeof payload === "object" &&
277
+ !Array.isArray(payload)
278
+ ) {
279
+ error = payload.error ?? error;
280
+ if (Array.isArray(payload.traceback)) {
281
+ traceback = payload.traceback.map(String);
282
+ }
283
+ }
284
+ const parsedRetry = parseRetryAfter(response.headers["retry-after"] ?? null);
285
+ const retryAfter = parsedRetry === null ? null : Math.trunc(parsedRetry);
286
+ let cls = ERROR_CLASSES.get(response.status);
287
+ if (cls === undefined) {
288
+ cls = response.status >= 500 ? ServerError : ContreeAPIError;
289
+ }
290
+ return new cls(response.status, error, { traceback, retryAfter });
291
+ }
292
+
293
+ /** Incremental sans-io parser for `text/event-stream` bytes.
294
+ *
295
+ * Feed raw chunks, get complete frames `{id, event, data}` back.
296
+ * Comment lines (`: keepalive`) are discarded per the SSE spec.
297
+ */
298
+ export class SSEParser {
299
+ // a single SSE line/frame has no business being this large; the
300
+ // cap keeps a misbehaving peer from growing the buffer unbounded
301
+ static MAX_BUFFER = 4 * 1024 * 1024;
302
+
303
+ constructor() {
304
+ this.decoder = new TextDecoder();
305
+ this.buffer = "";
306
+ this.event = null;
307
+ this.eventId = null;
308
+ this.dataLines = [];
309
+ this.pendingSize = 0;
310
+ this.dirty = false;
311
+ }
312
+
313
+ feed(chunk) {
314
+ this.buffer += this.decoder.decode(chunk, { stream: true });
315
+ // the cap covers BOTH the unterminated line and the frame
316
+ // accumulated so far: many short data lines must not grow the
317
+ // pending event unboundedly
318
+ if (this.buffer.length + this.pendingSize > SSEParser.MAX_BUFFER) {
319
+ throw new SSEStreamError(
320
+ `SSE frame exceeds ${SSEParser.MAX_BUFFER} bytes before completion`,
321
+ );
322
+ }
323
+ const frames = [];
324
+ for (;;) {
325
+ const newline = this.buffer.indexOf("\n");
326
+ if (newline < 0) {
327
+ break;
328
+ }
329
+ let line = this.buffer.slice(0, newline);
330
+ this.buffer = this.buffer.slice(newline + 1);
331
+ if (line.endsWith("\r")) {
332
+ line = line.slice(0, -1);
333
+ }
334
+ if (!line) {
335
+ const frame = this.flush();
336
+ if (frame !== null) {
337
+ frames.push(frame);
338
+ }
339
+ continue;
340
+ }
341
+ if (line.startsWith(":")) {
342
+ continue;
343
+ }
344
+ const colon = line.indexOf(":");
345
+ const name = colon < 0 ? line : line.slice(0, colon);
346
+ let value = colon < 0 ? "" : line.slice(colon + 1);
347
+ if (value.startsWith(" ")) {
348
+ value = value.slice(1);
349
+ }
350
+ this.dirty = true;
351
+ if (name === "id") {
352
+ const parsed = Number.parseInt(value, 10);
353
+ this.eventId = Number.isNaN(parsed) ? null : parsed;
354
+ } else if (name === "event") {
355
+ this.event = value;
356
+ } else if (name === "data") {
357
+ this.dataLines.push(value);
358
+ this.pendingSize += value.length;
359
+ }
360
+ }
361
+ return frames;
362
+ }
363
+
364
+ /** Return the pending frame, if any, and reset the state. */
365
+ flush() {
366
+ if (!this.dirty) {
367
+ return null;
368
+ }
369
+ const frame = {
370
+ id: this.eventId,
371
+ event: this.event,
372
+ data: this.dataLines.join("\n"),
373
+ };
374
+ this.event = null;
375
+ this.eventId = null;
376
+ this.dataLines = [];
377
+ this.pendingSize = 0;
378
+ this.dirty = false;
379
+ return frame;
380
+ }
381
+ }
382
+
383
+ /** Decode an SSE frame's JSON payload.
384
+ *
385
+ * Raises SSEStreamError for in-band `sse_error` frames, carrying
386
+ * *lastEventId* so the caller can reconnect from that point; returns
387
+ * null for frames that carry no event payload. The caller turns the
388
+ * plain object into a typed OperationEvent.
389
+ */
390
+ export function decodeFramePayload(frame, lastEventId = null) {
391
+ if (frame.event === "sse_error") {
392
+ throw new SSEStreamError(frame.data, { lastEventId });
393
+ }
394
+ if (!frame.data) {
395
+ return null;
396
+ }
397
+ let payload;
398
+ try {
399
+ payload = JSON.parse(frame.data);
400
+ } catch (error) {
401
+ // surface protocol corruption through the SSE error channel so
402
+ // followOperationEvents reconnects instead of crashing
403
+ throw new SSEStreamError(`malformed SSE event payload: ${error}`, {
404
+ lastEventId,
405
+ });
406
+ }
407
+ if (
408
+ payload === null ||
409
+ typeof payload !== "object" ||
410
+ Array.isArray(payload)
411
+ ) {
412
+ return null;
413
+ }
414
+ return payload;
415
+ }
@@ -0,0 +1,4 @@
1
+ // Generated by codegen from the OpenAPI spec - do not edit.
2
+
3
+ export declare const DEFAULT_BASE_URL: string;
4
+ export declare const SPEC_SHA256: string;
@@ -0,0 +1,8 @@
1
+ // Generated by codegen from the OpenAPI spec - do not edit.
2
+
3
+ export const DEFAULT_BASE_URL = "https://api.tokenfactory.nebius.com/sandboxes";
4
+
5
+ // sha256 of the exact OpenAPI document this package was built
6
+ // from - the build input provenance
7
+ export const SPEC_SHA256 =
8
+ "3782df855d7ae14556e221f4f37a67fb6aeb65121396cb009a5e7a0995abd06b";