@hlix/sdk 0.2.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.
- package/LICENSE +201 -0
- package/README.md +144 -0
- package/dist/index.d.ts +9544 -0
- package/dist/index.js +693 -0
- package/package.json +54 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
// ../api-client/src/client.ts
|
|
2
|
+
import createClient from "openapi-fetch";
|
|
3
|
+
function createHlixClient(options) {
|
|
4
|
+
return createClient({
|
|
5
|
+
baseUrl: options.baseUrl,
|
|
6
|
+
headers: options.headers,
|
|
7
|
+
fetch: options.fetch
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// src/auth.ts
|
|
12
|
+
var apiKeyCredential = (apiKey) => ({
|
|
13
|
+
kind: "apiKey",
|
|
14
|
+
apiKey
|
|
15
|
+
});
|
|
16
|
+
var sessionCredential = (cookie) => ({
|
|
17
|
+
kind: "session",
|
|
18
|
+
cookie
|
|
19
|
+
});
|
|
20
|
+
function credentialHeaders(credential) {
|
|
21
|
+
return credential.kind === "apiKey" ? { "x-api-key": credential.apiKey } : { cookie: credential.cookie };
|
|
22
|
+
}
|
|
23
|
+
function organizationHeaders(organizationId) {
|
|
24
|
+
return organizationId ? { "X-Organization-Id": organizationId } : {};
|
|
25
|
+
}
|
|
26
|
+
function authHeaders(credential, organizationId) {
|
|
27
|
+
return {
|
|
28
|
+
...credentialHeaders(credential),
|
|
29
|
+
...organizationHeaders(organizationId)
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/errors.ts
|
|
34
|
+
function errorMessageOf(body) {
|
|
35
|
+
if (typeof body !== "object" || body === null) return null;
|
|
36
|
+
const { error } = body;
|
|
37
|
+
return typeof error === "string" ? error : null;
|
|
38
|
+
}
|
|
39
|
+
var HlixError = class extends Error {
|
|
40
|
+
status;
|
|
41
|
+
body;
|
|
42
|
+
requestId;
|
|
43
|
+
method;
|
|
44
|
+
url;
|
|
45
|
+
constructor(message, init) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.name = new.target.name;
|
|
48
|
+
this.status = init.status;
|
|
49
|
+
this.body = init.body;
|
|
50
|
+
this.requestId = init.requestId ?? null;
|
|
51
|
+
this.method = init.method;
|
|
52
|
+
this.url = init.url;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
var HlixValidationError = class extends HlixError {
|
|
56
|
+
/** The decoded `ZodIssue[]`; empty when the server sent a plain message. */
|
|
57
|
+
issues;
|
|
58
|
+
constructor(message, init, issues) {
|
|
59
|
+
super(message, init);
|
|
60
|
+
this.issues = issues;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
var HlixBadRequestError = class extends HlixError {
|
|
64
|
+
};
|
|
65
|
+
var HlixAuthenticationError = class extends HlixError {
|
|
66
|
+
};
|
|
67
|
+
var HlixPermissionError = class extends HlixError {
|
|
68
|
+
};
|
|
69
|
+
var HlixNotFoundError = class extends HlixError {
|
|
70
|
+
};
|
|
71
|
+
var HlixConflictError = class extends HlixError {
|
|
72
|
+
};
|
|
73
|
+
var HlixRateLimitError = class extends HlixError {
|
|
74
|
+
retryAfter;
|
|
75
|
+
constructor(message, init, retryAfter) {
|
|
76
|
+
super(message, init);
|
|
77
|
+
this.retryAfter = retryAfter;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
var HlixNotImplementedError = class extends HlixError {
|
|
81
|
+
};
|
|
82
|
+
var HlixServerError = class extends HlixError {
|
|
83
|
+
};
|
|
84
|
+
var HlixTransportError = class extends Error {
|
|
85
|
+
method;
|
|
86
|
+
url;
|
|
87
|
+
constructor(message, init) {
|
|
88
|
+
super(message, { cause: init.cause });
|
|
89
|
+
this.name = "HlixTransportError";
|
|
90
|
+
this.method = init.method;
|
|
91
|
+
this.url = init.url;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
function validationIssues(body) {
|
|
95
|
+
if (typeof body !== "object" || body === null) return null;
|
|
96
|
+
const candidate = body;
|
|
97
|
+
if (candidate.success !== false) return null;
|
|
98
|
+
const raw = candidate.error?.message;
|
|
99
|
+
if (typeof raw !== "string") return null;
|
|
100
|
+
try {
|
|
101
|
+
const parsed = JSON.parse(raw);
|
|
102
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
103
|
+
} catch {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function retryAfterSeconds(headers) {
|
|
108
|
+
const raw = headers.get("retry-after");
|
|
109
|
+
if (!raw) return null;
|
|
110
|
+
const seconds = Number(raw);
|
|
111
|
+
return Number.isFinite(seconds) && seconds >= 0 ? seconds : null;
|
|
112
|
+
}
|
|
113
|
+
function errorForResponse(response, body, request) {
|
|
114
|
+
const init = {
|
|
115
|
+
status: response.status,
|
|
116
|
+
body,
|
|
117
|
+
requestId: response.headers.get("x-request-id"),
|
|
118
|
+
method: request.method,
|
|
119
|
+
url: request.url
|
|
120
|
+
};
|
|
121
|
+
const stated = errorMessageOf(body);
|
|
122
|
+
const fallback = `${request.method} ${request.url} failed with ${response.status}`;
|
|
123
|
+
const issues = validationIssues(body);
|
|
124
|
+
if (issues) {
|
|
125
|
+
return new HlixValidationError(
|
|
126
|
+
`${request.method} ${request.url}: request body failed validation`,
|
|
127
|
+
init,
|
|
128
|
+
issues
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
const message = stated ?? fallback;
|
|
132
|
+
switch (response.status) {
|
|
133
|
+
case 400:
|
|
134
|
+
return new HlixBadRequestError(message, init);
|
|
135
|
+
case 401:
|
|
136
|
+
return new HlixAuthenticationError(message, init);
|
|
137
|
+
case 403:
|
|
138
|
+
return new HlixPermissionError(message, init);
|
|
139
|
+
case 404:
|
|
140
|
+
return new HlixNotFoundError(message, init);
|
|
141
|
+
case 409:
|
|
142
|
+
return new HlixConflictError(message, init);
|
|
143
|
+
case 429:
|
|
144
|
+
return new HlixRateLimitError(message, init, retryAfterSeconds(response.headers));
|
|
145
|
+
case 501:
|
|
146
|
+
return new HlixNotImplementedError(message, init);
|
|
147
|
+
default:
|
|
148
|
+
return response.status >= 500 ? new HlixServerError(message, init) : new HlixError(message, init);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/retry.ts
|
|
153
|
+
var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set([
|
|
154
|
+
"GET",
|
|
155
|
+
"HEAD",
|
|
156
|
+
"PUT",
|
|
157
|
+
"DELETE",
|
|
158
|
+
"OPTIONS",
|
|
159
|
+
"TRACE"
|
|
160
|
+
]);
|
|
161
|
+
function isIdempotent(method) {
|
|
162
|
+
return IDEMPOTENT_METHODS.has(method.toUpperCase());
|
|
163
|
+
}
|
|
164
|
+
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([
|
|
165
|
+
429,
|
|
166
|
+
500,
|
|
167
|
+
502,
|
|
168
|
+
503,
|
|
169
|
+
504
|
|
170
|
+
]);
|
|
171
|
+
function isRetryableStatus(status) {
|
|
172
|
+
return RETRYABLE_STATUSES.has(status);
|
|
173
|
+
}
|
|
174
|
+
var DEFAULT_RETRY = {
|
|
175
|
+
attempts: 3,
|
|
176
|
+
baseDelayMs: 200,
|
|
177
|
+
maxDelayMs: 5e3
|
|
178
|
+
};
|
|
179
|
+
function backoffDelayMs(attempt, policy, retryAfterSeconds3, random = Math.random) {
|
|
180
|
+
if (retryAfterSeconds3 !== null) {
|
|
181
|
+
return Math.min(retryAfterSeconds3 * 1e3, policy.maxDelayMs);
|
|
182
|
+
}
|
|
183
|
+
const ceiling = Math.min(policy.baseDelayMs * 2 ** attempt, policy.maxDelayMs);
|
|
184
|
+
return Math.round(random() * ceiling);
|
|
185
|
+
}
|
|
186
|
+
function shouldRetry(input) {
|
|
187
|
+
if (!isIdempotent(input.method)) return false;
|
|
188
|
+
if (input.attempt + 1 >= input.policy.attempts) return false;
|
|
189
|
+
return input.status === null || isRetryableStatus(input.status);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/http.ts
|
|
193
|
+
var wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
194
|
+
function retryAfterSeconds2(response) {
|
|
195
|
+
const raw = response.headers.get("retry-after");
|
|
196
|
+
if (!raw) return null;
|
|
197
|
+
const seconds = Number(raw);
|
|
198
|
+
return Number.isFinite(seconds) && seconds >= 0 ? seconds : null;
|
|
199
|
+
}
|
|
200
|
+
function retryingFetch(options) {
|
|
201
|
+
const { base, policy } = options;
|
|
202
|
+
const sleep = options.sleep ?? wait;
|
|
203
|
+
const random = options.random ?? Math.random;
|
|
204
|
+
return async (input) => {
|
|
205
|
+
const method = input.method.toUpperCase();
|
|
206
|
+
const replayable = isIdempotent(method) ? input.clone() : null;
|
|
207
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
208
|
+
const request = attempt === 0 ? input : replayable.clone();
|
|
209
|
+
let response;
|
|
210
|
+
try {
|
|
211
|
+
response = await base(request);
|
|
212
|
+
} catch (cause) {
|
|
213
|
+
if (!replayable || !shouldRetry({ method, attempt, policy, status: null })) {
|
|
214
|
+
throw new HlixTransportError(
|
|
215
|
+
`${method} ${input.url}: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
216
|
+
{ method, url: input.url, cause }
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
await sleep(backoffDelayMs(attempt, policy, null, random));
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (!shouldRetry({ method, attempt, policy, status: response.status })) {
|
|
223
|
+
return response;
|
|
224
|
+
}
|
|
225
|
+
if (!replayable) return response;
|
|
226
|
+
await sleep(backoffDelayMs(attempt, policy, retryAfterSeconds2(response), random));
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function unwrap(result, request) {
|
|
231
|
+
if (result.response.ok) return result.data;
|
|
232
|
+
throw errorForResponse(result.response, result.error ?? result.data, request);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// src/streaming.ts
|
|
236
|
+
var emptyFrame = () => ({ event: null, data: [], id: null, retry: null });
|
|
237
|
+
function applyField(frame, name, value) {
|
|
238
|
+
switch (name) {
|
|
239
|
+
case "event":
|
|
240
|
+
frame.event = value;
|
|
241
|
+
return;
|
|
242
|
+
case "data":
|
|
243
|
+
frame.data.push(value);
|
|
244
|
+
return;
|
|
245
|
+
case "id":
|
|
246
|
+
if (!value.includes("\0")) frame.id = value;
|
|
247
|
+
return;
|
|
248
|
+
case "retry": {
|
|
249
|
+
if (/^\d+$/.test(value)) frame.retry = Number(value);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
default:
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function dispatch(frame) {
|
|
257
|
+
const data = frame.data.join("\n");
|
|
258
|
+
const event = frame.event;
|
|
259
|
+
const id = frame.id;
|
|
260
|
+
frame.data = [];
|
|
261
|
+
frame.event = null;
|
|
262
|
+
if (data === "") return null;
|
|
263
|
+
return { event, data, id, retry: frame.retry };
|
|
264
|
+
}
|
|
265
|
+
function consumeLine(frame, line) {
|
|
266
|
+
if (line === "") return dispatch(frame);
|
|
267
|
+
if (line.startsWith(":")) return null;
|
|
268
|
+
const colon = line.indexOf(":");
|
|
269
|
+
if (colon === -1) {
|
|
270
|
+
applyField(frame, line, "");
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
const name = line.slice(0, colon);
|
|
274
|
+
let value = line.slice(colon + 1);
|
|
275
|
+
if (value.startsWith(" ")) value = value.slice(1);
|
|
276
|
+
applyField(frame, name, value);
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
function splitLines(buffer) {
|
|
280
|
+
const lines = [];
|
|
281
|
+
let start = 0;
|
|
282
|
+
for (let i = 0; i < buffer.length; i += 1) {
|
|
283
|
+
const ch = buffer[i];
|
|
284
|
+
if (ch !== "\n" && ch !== "\r") continue;
|
|
285
|
+
lines.push(buffer.slice(start, i));
|
|
286
|
+
if (ch === "\r" && buffer[i + 1] === "\n") i += 1;
|
|
287
|
+
start = i + 1;
|
|
288
|
+
}
|
|
289
|
+
return { lines, rest: buffer.slice(start) };
|
|
290
|
+
}
|
|
291
|
+
async function* parseSseStream(body, signal) {
|
|
292
|
+
const reader = body.getReader();
|
|
293
|
+
const decoder = new TextDecoder();
|
|
294
|
+
const frame = emptyFrame();
|
|
295
|
+
let buffer = "";
|
|
296
|
+
const abort = () => void reader.cancel().catch(() => {
|
|
297
|
+
});
|
|
298
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
299
|
+
try {
|
|
300
|
+
for (; ; ) {
|
|
301
|
+
const { done, value } = await reader.read();
|
|
302
|
+
if (done) break;
|
|
303
|
+
buffer += decoder.decode(value, { stream: true });
|
|
304
|
+
const { lines, rest } = splitLines(buffer);
|
|
305
|
+
buffer = rest;
|
|
306
|
+
for (const line of lines) {
|
|
307
|
+
const event = consumeLine(frame, line);
|
|
308
|
+
if (event) yield event;
|
|
309
|
+
}
|
|
310
|
+
if (signal?.aborted) return;
|
|
311
|
+
}
|
|
312
|
+
buffer += decoder.decode();
|
|
313
|
+
if (buffer !== "") {
|
|
314
|
+
const event = consumeLine(frame, buffer);
|
|
315
|
+
if (event) yield event;
|
|
316
|
+
}
|
|
317
|
+
const trailing = dispatch(frame);
|
|
318
|
+
if (trailing) yield trailing;
|
|
319
|
+
} finally {
|
|
320
|
+
signal?.removeEventListener("abort", abort);
|
|
321
|
+
reader.releaseLock();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// src/index.ts
|
|
326
|
+
var Hlix = class {
|
|
327
|
+
/** The un-opinionated transport, for anything this facade does not wrap. */
|
|
328
|
+
raw;
|
|
329
|
+
baseUrl;
|
|
330
|
+
headers;
|
|
331
|
+
fetcher;
|
|
332
|
+
constructor(options) {
|
|
333
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
334
|
+
this.headers = authHeaders(options.credential, options.organizationId);
|
|
335
|
+
const policy = { ...DEFAULT_RETRY, ...options.retry };
|
|
336
|
+
const base = options.fetch ?? ((input) => globalThis.fetch(input));
|
|
337
|
+
this.fetcher = retryingFetch({ base, policy });
|
|
338
|
+
this.raw = createHlixClient({
|
|
339
|
+
baseUrl: this.baseUrl,
|
|
340
|
+
headers: this.headers,
|
|
341
|
+
fetch: this.fetcher
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
at(method, path) {
|
|
345
|
+
return { method, url: `${this.baseUrl}${path}` };
|
|
346
|
+
}
|
|
347
|
+
projects = {
|
|
348
|
+
list: async () => unwrap(
|
|
349
|
+
await this.raw.GET("/v1/api/projects"),
|
|
350
|
+
this.at("GET", "/v1/api/projects")
|
|
351
|
+
),
|
|
352
|
+
get: async (id) => unwrap(
|
|
353
|
+
await this.raw.GET("/v1/api/projects/{id}", { params: { path: { id } } }),
|
|
354
|
+
this.at("GET", `/v1/api/projects/${id}`)
|
|
355
|
+
),
|
|
356
|
+
create: async (body) => unwrap(
|
|
357
|
+
await this.raw.POST("/v1/api/projects", { body }),
|
|
358
|
+
this.at("POST", "/v1/api/projects")
|
|
359
|
+
),
|
|
360
|
+
delete: async (id) => unwrap(
|
|
361
|
+
await this.raw.DELETE("/v1/api/projects/{id}", {
|
|
362
|
+
params: { path: { id } }
|
|
363
|
+
}),
|
|
364
|
+
this.at("DELETE", `/v1/api/projects/${id}`)
|
|
365
|
+
),
|
|
366
|
+
start: async (id, body) => unwrap(
|
|
367
|
+
await this.raw.POST("/v1/api/projects/{id}/start", {
|
|
368
|
+
params: { path: { id } },
|
|
369
|
+
body
|
|
370
|
+
}),
|
|
371
|
+
this.at("POST", `/v1/api/projects/${id}/start`)
|
|
372
|
+
)
|
|
373
|
+
};
|
|
374
|
+
imports = {
|
|
375
|
+
create: async (body) => {
|
|
376
|
+
const result = await this.raw.POST("/v1/api/imports", { body });
|
|
377
|
+
return unwrap(result, this.at("POST", "/v1/api/imports"));
|
|
378
|
+
},
|
|
379
|
+
get: async (id) => unwrap(
|
|
380
|
+
await this.raw.GET("/v1/api/imports/{id}", {
|
|
381
|
+
params: { path: { id } }
|
|
382
|
+
}),
|
|
383
|
+
this.at("GET", `/v1/api/imports/${id}`)
|
|
384
|
+
),
|
|
385
|
+
finalize: async (id, body) => {
|
|
386
|
+
const result = await this.raw.POST("/v1/api/imports/{id}/finalize", {
|
|
387
|
+
params: { path: { id } },
|
|
388
|
+
body
|
|
389
|
+
});
|
|
390
|
+
return unwrap(
|
|
391
|
+
result,
|
|
392
|
+
this.at("POST", `/v1/api/imports/${id}/finalize`)
|
|
393
|
+
);
|
|
394
|
+
},
|
|
395
|
+
uploadBundle: async (upload, bundle, options = {}) => {
|
|
396
|
+
const chunkSize = options.chunkSize ?? 8 * 1024 * 1024;
|
|
397
|
+
if (chunkSize <= 0 || chunkSize % (256 * 1024) !== 0) {
|
|
398
|
+
throw new Error("Import upload chunkSize must be a positive multiple of 256 KiB");
|
|
399
|
+
}
|
|
400
|
+
const url = upload.url.startsWith("/") ? `${this.baseUrl}${upload.url}` : upload.url;
|
|
401
|
+
if (upload.kind === "hlix-stream") {
|
|
402
|
+
const request = new Request(url, {
|
|
403
|
+
method: "PUT",
|
|
404
|
+
headers: {
|
|
405
|
+
...this.headers,
|
|
406
|
+
"content-type": "application/x-git-bundle",
|
|
407
|
+
"content-length": String(bundle.size)
|
|
408
|
+
},
|
|
409
|
+
body: bundle,
|
|
410
|
+
signal: options.signal
|
|
411
|
+
});
|
|
412
|
+
const response = await this.fetcher(request);
|
|
413
|
+
if (!response.ok) await this.throwUploadError(response, url);
|
|
414
|
+
options.onProgress?.(bundle.size, bundle.size);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
let offset = 0;
|
|
418
|
+
while (offset < bundle.size) {
|
|
419
|
+
const end = Math.min(offset + chunkSize, bundle.size);
|
|
420
|
+
const request = new Request(url, {
|
|
421
|
+
method: "PUT",
|
|
422
|
+
headers: {
|
|
423
|
+
"content-type": "application/x-git-bundle",
|
|
424
|
+
"content-length": String(end - offset),
|
|
425
|
+
"content-range": `bytes ${offset}-${end - 1}/${bundle.size}`
|
|
426
|
+
},
|
|
427
|
+
body: bundle.slice(offset, end),
|
|
428
|
+
signal: options.signal
|
|
429
|
+
});
|
|
430
|
+
const response = await this.fetcher(request);
|
|
431
|
+
if (response.status !== 308 && !response.ok) {
|
|
432
|
+
await this.throwUploadError(response, url);
|
|
433
|
+
}
|
|
434
|
+
const acknowledged = response.headers.get("range")?.match(/bytes=0-(\d+)/)?.[1];
|
|
435
|
+
offset = acknowledged ? Number(acknowledged) + 1 : end;
|
|
436
|
+
if (!Number.isSafeInteger(offset) || offset < end || offset > bundle.size) {
|
|
437
|
+
throw new Error("Import upload server returned an invalid acknowledged range");
|
|
438
|
+
}
|
|
439
|
+
options.onProgress?.(offset, bundle.size);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
revisions = {
|
|
444
|
+
head: async (projectId) => unwrap(
|
|
445
|
+
await this.raw.GET("/v1/api/projects/{id}/revisions/head", {
|
|
446
|
+
params: { path: { id: projectId } }
|
|
447
|
+
}),
|
|
448
|
+
this.at("GET", `/v1/api/projects/${projectId}/revisions/head`)
|
|
449
|
+
),
|
|
450
|
+
protectedFiles: async (projectId) => unwrap(
|
|
451
|
+
await this.raw.GET("/v1/api/projects/{id}/protected-files", {
|
|
452
|
+
params: { path: { id: projectId } }
|
|
453
|
+
}),
|
|
454
|
+
this.at("GET", `/v1/api/projects/${projectId}/protected-files`)
|
|
455
|
+
),
|
|
456
|
+
createUpload: async (projectId, body) => {
|
|
457
|
+
const result = await this.raw.POST("/v1/api/projects/{id}/revisions/uploads", {
|
|
458
|
+
params: { path: { id: projectId } },
|
|
459
|
+
body
|
|
460
|
+
});
|
|
461
|
+
return unwrap(result, this.at("POST", `/v1/api/projects/${projectId}/revisions/uploads`));
|
|
462
|
+
},
|
|
463
|
+
finalize: async (projectId, uploadId, body) => {
|
|
464
|
+
const result = await this.raw.POST(
|
|
465
|
+
"/v1/api/projects/{id}/revisions/uploads/{uploadId}/finalize",
|
|
466
|
+
{ params: { path: { id: projectId, uploadId } }, body }
|
|
467
|
+
);
|
|
468
|
+
return unwrap(
|
|
469
|
+
result,
|
|
470
|
+
this.at("POST", `/v1/api/projects/${projectId}/revisions/uploads/${uploadId}/finalize`)
|
|
471
|
+
);
|
|
472
|
+
},
|
|
473
|
+
uploadBundle: async (upload, bundle, options = {}) => this.imports.uploadBundle(upload, bundle, options),
|
|
474
|
+
downloadBundle: async (projectId, revisionId) => {
|
|
475
|
+
const path = `/v1/api/projects/${projectId}/revisions/${revisionId}/bundle`;
|
|
476
|
+
const request = new Request(`${this.baseUrl}${path}`, {
|
|
477
|
+
method: "GET",
|
|
478
|
+
headers: this.headers
|
|
479
|
+
});
|
|
480
|
+
const response = await this.fetcher(request);
|
|
481
|
+
if (!response.ok) await this.throwUploadError(response, `${this.baseUrl}${path}`);
|
|
482
|
+
return response.blob();
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
resources = {
|
|
486
|
+
importSkill: async (projectId, body) => unwrap(
|
|
487
|
+
await this.raw.POST("/v1/api/projects/{id}/resources/skill", {
|
|
488
|
+
params: { path: { id: projectId } },
|
|
489
|
+
body
|
|
490
|
+
}),
|
|
491
|
+
this.at("POST", `/v1/api/projects/${projectId}/resources/skill`)
|
|
492
|
+
),
|
|
493
|
+
importAgent: async (projectId, body) => unwrap(
|
|
494
|
+
await this.raw.POST("/v1/api/projects/{id}/resources/agent", {
|
|
495
|
+
params: { path: { id: projectId } },
|
|
496
|
+
body
|
|
497
|
+
}),
|
|
498
|
+
this.at("POST", `/v1/api/projects/${projectId}/resources/agent`)
|
|
499
|
+
),
|
|
500
|
+
importMcp: async (projectId, body) => unwrap(
|
|
501
|
+
await this.raw.POST("/v1/api/projects/{id}/resources/mcp", {
|
|
502
|
+
params: { path: { id: projectId } },
|
|
503
|
+
body
|
|
504
|
+
}),
|
|
505
|
+
this.at("POST", `/v1/api/projects/${projectId}/resources/mcp`)
|
|
506
|
+
)
|
|
507
|
+
};
|
|
508
|
+
async throwUploadError(response, url) {
|
|
509
|
+
let body = null;
|
|
510
|
+
try {
|
|
511
|
+
body = await response.json();
|
|
512
|
+
} catch {
|
|
513
|
+
body = await response.text().catch(() => null);
|
|
514
|
+
}
|
|
515
|
+
throw errorForResponse(response, body, { method: "PUT", url });
|
|
516
|
+
}
|
|
517
|
+
tasks = {
|
|
518
|
+
list: async (query) => unwrap(
|
|
519
|
+
await this.raw.GET("/v1/api/tasks", { params: { query: query ?? {} } }),
|
|
520
|
+
this.at("GET", "/v1/api/tasks")
|
|
521
|
+
),
|
|
522
|
+
get: async (id) => unwrap(
|
|
523
|
+
await this.raw.GET("/v1/api/tasks/{id}", { params: { path: { id } } }),
|
|
524
|
+
this.at("GET", `/v1/api/tasks/${id}`)
|
|
525
|
+
),
|
|
526
|
+
create: async (body) => unwrap(
|
|
527
|
+
await this.raw.POST("/v1/api/tasks", { body }),
|
|
528
|
+
this.at("POST", "/v1/api/tasks")
|
|
529
|
+
),
|
|
530
|
+
update: async (id, body) => unwrap(
|
|
531
|
+
await this.raw.PATCH("/v1/api/tasks/{id}", {
|
|
532
|
+
params: { path: { id } },
|
|
533
|
+
body
|
|
534
|
+
}),
|
|
535
|
+
this.at("PATCH", `/v1/api/tasks/${id}`)
|
|
536
|
+
),
|
|
537
|
+
listComments: async (id) => unwrap(
|
|
538
|
+
await this.raw.GET("/v1/api/tasks/{id}/comments", {
|
|
539
|
+
params: { path: { id } }
|
|
540
|
+
}),
|
|
541
|
+
this.at("GET", `/v1/api/tasks/${id}/comments`)
|
|
542
|
+
),
|
|
543
|
+
addComment: async (id, body) => unwrap(
|
|
544
|
+
await this.raw.POST("/v1/api/tasks/{id}/comments", {
|
|
545
|
+
params: { path: { id } },
|
|
546
|
+
body
|
|
547
|
+
}),
|
|
548
|
+
this.at("POST", `/v1/api/tasks/${id}/comments`)
|
|
549
|
+
),
|
|
550
|
+
/** The verified review evidence — the same observation QA reviews. */
|
|
551
|
+
review: async (id) => unwrap(
|
|
552
|
+
await this.raw.GET("/v1/api/tasks/{id}/review", {
|
|
553
|
+
params: { path: { id } }
|
|
554
|
+
}),
|
|
555
|
+
this.at("GET", `/v1/api/tasks/${id}/review`)
|
|
556
|
+
),
|
|
557
|
+
/** Live status frames. A terminal status closes the stream server-side. */
|
|
558
|
+
stream: (id, init) => this.stream(`/v1/api/tasks/${id}/stream`, init)
|
|
559
|
+
};
|
|
560
|
+
cycles = {
|
|
561
|
+
list: async (query) => unwrap(
|
|
562
|
+
await this.raw.GET("/v1/api/cycles", {
|
|
563
|
+
params: { query: query ?? {} }
|
|
564
|
+
}),
|
|
565
|
+
this.at("GET", "/v1/api/cycles")
|
|
566
|
+
),
|
|
567
|
+
get: async (id) => unwrap(
|
|
568
|
+
await this.raw.GET("/v1/api/cycles/{id}", {
|
|
569
|
+
params: { path: { id } }
|
|
570
|
+
}),
|
|
571
|
+
this.at("GET", `/v1/api/cycles/${id}`)
|
|
572
|
+
),
|
|
573
|
+
/** Creation is a PROPOSAL — the orchestrator owns the roadmap. */
|
|
574
|
+
propose: async (body) => unwrap(
|
|
575
|
+
await this.raw.POST("/v1/api/cycles", { body }),
|
|
576
|
+
this.at("POST", "/v1/api/cycles")
|
|
577
|
+
),
|
|
578
|
+
stream: (id, init) => this.stream(`/v1/api/cycles/${id}/stream`, init)
|
|
579
|
+
};
|
|
580
|
+
/**
|
|
581
|
+
* Invoices the agency issues to its own customer.
|
|
582
|
+
*
|
|
583
|
+
* NOT the hlix subscription — that is Polar's, and the two never meet.
|
|
584
|
+
*
|
|
585
|
+
* There is no `update`, because there is no route that could edit an issued
|
|
586
|
+
* invoice: a correction is `void` plus a new draft naming it through
|
|
587
|
+
* `replacesInvoiceId`. And `create` names no customer — the customer is
|
|
588
|
+
* derived from the billables being invoiced.
|
|
589
|
+
*/
|
|
590
|
+
clientInvoices = {
|
|
591
|
+
list: async (query) => unwrap(
|
|
592
|
+
await this.raw.GET("/v1/api/client-billing/invoices", {
|
|
593
|
+
params: { query: query ?? {} }
|
|
594
|
+
}),
|
|
595
|
+
this.at("GET", "/v1/api/client-billing/invoices")
|
|
596
|
+
),
|
|
597
|
+
get: async (id) => unwrap(
|
|
598
|
+
await this.raw.GET("/v1/api/client-billing/invoices/{id}", {
|
|
599
|
+
params: { path: { id } }
|
|
600
|
+
}),
|
|
601
|
+
this.at("GET", `/v1/api/client-billing/invoices/${id}`)
|
|
602
|
+
),
|
|
603
|
+
createDraft: async (body) => unwrap(
|
|
604
|
+
await this.raw.POST("/v1/api/client-billing/invoices", { body }),
|
|
605
|
+
this.at("POST", "/v1/api/client-billing/invoices")
|
|
606
|
+
),
|
|
607
|
+
/** The number is assigned by the server, gapless per workspace. */
|
|
608
|
+
issue: async (id) => unwrap(
|
|
609
|
+
await this.raw.POST("/v1/api/client-billing/invoices/{id}/issue", {
|
|
610
|
+
params: { path: { id } },
|
|
611
|
+
body: {}
|
|
612
|
+
}),
|
|
613
|
+
this.at("POST", `/v1/api/client-billing/invoices/${id}/issue`)
|
|
614
|
+
),
|
|
615
|
+
void: async (id, reason) => unwrap(
|
|
616
|
+
await this.raw.POST("/v1/api/client-billing/invoices/{id}/void", {
|
|
617
|
+
params: { path: { id } },
|
|
618
|
+
body: { reason }
|
|
619
|
+
}),
|
|
620
|
+
this.at("POST", `/v1/api/client-billing/invoices/${id}/void`)
|
|
621
|
+
),
|
|
622
|
+
pay: async (id) => unwrap(
|
|
623
|
+
await this.raw.POST("/v1/api/client-billing/invoices/{id}/pay", {
|
|
624
|
+
params: { path: { id } },
|
|
625
|
+
body: {}
|
|
626
|
+
}),
|
|
627
|
+
this.at("POST", `/v1/api/client-billing/invoices/${id}/pay`)
|
|
628
|
+
),
|
|
629
|
+
/**
|
|
630
|
+
* Open a PROVIDER-HOSTED checkout. Returns a URL and nothing else — no card
|
|
631
|
+
* field and no payment credential ever passes through hlix or this client.
|
|
632
|
+
* It does not mark the invoice paid: that happens only when the provider's
|
|
633
|
+
* signed webhook reconciles.
|
|
634
|
+
*/
|
|
635
|
+
paymentLink: async (id, body = {}) => unwrap(
|
|
636
|
+
await this.raw.POST(
|
|
637
|
+
"/v1/api/client-billing/invoices/{id}/payment-link",
|
|
638
|
+
{ params: { path: { id } }, body }
|
|
639
|
+
),
|
|
640
|
+
this.at("POST", `/v1/api/client-billing/invoices/${id}/payment-link`)
|
|
641
|
+
)
|
|
642
|
+
};
|
|
643
|
+
/**
|
|
644
|
+
* Consume any `text/event-stream` route.
|
|
645
|
+
*
|
|
646
|
+
* Deliberately NOT routed through the typed client: `openapi-fetch` parses
|
|
647
|
+
* the body, and a stream must not be parsed — it has to be read as it
|
|
648
|
+
* arrives. Retry does not apply either; a stream that drops mid-flight has
|
|
649
|
+
* already delivered events, and silently restarting it would replay them.
|
|
650
|
+
*/
|
|
651
|
+
async *stream(path, init) {
|
|
652
|
+
const url = `${this.baseUrl}${path}`;
|
|
653
|
+
const request = new Request(url, {
|
|
654
|
+
headers: { ...this.headers, accept: "text/event-stream" },
|
|
655
|
+
signal: init?.signal
|
|
656
|
+
});
|
|
657
|
+
const response = await globalThis.fetch(request);
|
|
658
|
+
if (!response.ok || !response.body) {
|
|
659
|
+
let body = null;
|
|
660
|
+
try {
|
|
661
|
+
body = await response.json();
|
|
662
|
+
} catch {
|
|
663
|
+
body = null;
|
|
664
|
+
}
|
|
665
|
+
throw errorForResponse(response, body, { method: "GET", url });
|
|
666
|
+
}
|
|
667
|
+
yield* parseSseStream(response.body, init?.signal);
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
function createHlix(options) {
|
|
671
|
+
return new Hlix(options);
|
|
672
|
+
}
|
|
673
|
+
export {
|
|
674
|
+
DEFAULT_RETRY,
|
|
675
|
+
Hlix,
|
|
676
|
+
HlixAuthenticationError,
|
|
677
|
+
HlixBadRequestError,
|
|
678
|
+
HlixConflictError,
|
|
679
|
+
HlixError,
|
|
680
|
+
HlixNotFoundError,
|
|
681
|
+
HlixNotImplementedError,
|
|
682
|
+
HlixPermissionError,
|
|
683
|
+
HlixRateLimitError,
|
|
684
|
+
HlixServerError,
|
|
685
|
+
HlixTransportError,
|
|
686
|
+
HlixValidationError,
|
|
687
|
+
IDEMPOTENT_METHODS,
|
|
688
|
+
apiKeyCredential,
|
|
689
|
+
createHlix,
|
|
690
|
+
isIdempotent,
|
|
691
|
+
parseSseStream,
|
|
692
|
+
sessionCredential
|
|
693
|
+
};
|