@indreamai/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.
package/dist/index.js ADDED
@@ -0,0 +1,489 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
3
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
4
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
5
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
6
+ var __spreadValues = (a, b) => {
7
+ for (var prop in b || (b = {}))
8
+ if (__hasOwnProp.call(b, prop))
9
+ __defNormalProp(a, prop, b[prop]);
10
+ if (__getOwnPropSymbols)
11
+ for (var prop of __getOwnPropSymbols(b)) {
12
+ if (__propIsEnum.call(b, prop))
13
+ __defNormalProp(a, prop, b[prop]);
14
+ }
15
+ return a;
16
+ };
17
+
18
+ // src/errors.ts
19
+ var APIError = class extends Error {
20
+ constructor(problem) {
21
+ super(problem.detail || problem.title);
22
+ this.name = "APIError";
23
+ this.status = problem.status;
24
+ this.type = problem.type;
25
+ this.detail = problem.detail;
26
+ this.errorCode = problem.errorCode;
27
+ }
28
+ };
29
+ var AuthError = class extends APIError {
30
+ constructor(problem) {
31
+ super(problem);
32
+ this.name = "AuthError";
33
+ }
34
+ };
35
+ var ValidationError = class extends APIError {
36
+ constructor(problem) {
37
+ super(problem);
38
+ this.name = "ValidationError";
39
+ }
40
+ };
41
+ var RateLimitError = class extends APIError {
42
+ constructor(problem) {
43
+ super(problem);
44
+ this.name = "RateLimitError";
45
+ }
46
+ };
47
+ var toApiProblem = (status, payload) => {
48
+ if (payload && typeof payload === "object") {
49
+ const problem = payload;
50
+ if (typeof problem.type === "string" && typeof problem.title === "string") {
51
+ return {
52
+ type: problem.type,
53
+ title: problem.title,
54
+ status: Number(problem.status || status),
55
+ detail: String(problem.detail || problem.title),
56
+ errorCode: problem.errorCode
57
+ };
58
+ }
59
+ }
60
+ return {
61
+ type: "INTERNAL_ERROR",
62
+ title: "Internal server error",
63
+ status,
64
+ detail: "Unexpected API response",
65
+ errorCode: "SDK_UNEXPECTED_RESPONSE"
66
+ };
67
+ };
68
+ var createApiError = (status, payload) => {
69
+ const problem = toApiProblem(status, payload);
70
+ if (status === 401 || status === 403) {
71
+ return new AuthError(problem);
72
+ }
73
+ if (status === 422 || status === 400) {
74
+ return new ValidationError(problem);
75
+ }
76
+ if (status === 429) {
77
+ return new RateLimitError(problem);
78
+ }
79
+ return new APIError(problem);
80
+ };
81
+
82
+ // src/resources/exports.ts
83
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["COMPLETED", "FAILED", "CANCELED"]);
84
+ var sleep = async (ms) => {
85
+ await new Promise((resolve) => setTimeout(resolve, ms));
86
+ };
87
+ var ExportsResource = class {
88
+ constructor(client) {
89
+ this.client = client;
90
+ }
91
+ async create(payload, options = {}) {
92
+ var _a;
93
+ const idempotencyKey = (_a = options.idempotencyKey) == null ? void 0 : _a.trim();
94
+ return await this.client.request("/v1/exports", {
95
+ method: "POST",
96
+ body: payload,
97
+ idempotencyKey: idempotencyKey || void 0,
98
+ signal: options.signal
99
+ });
100
+ }
101
+ async get(taskId, options = {}) {
102
+ return await this.client.request(`/v1/exports/${taskId}`, {
103
+ method: "GET",
104
+ signal: options.signal
105
+ });
106
+ }
107
+ async list(params) {
108
+ var _a;
109
+ const search = new URLSearchParams();
110
+ if (params == null ? void 0 : params.pageSize) {
111
+ search.set("pageSize", String(params.pageSize));
112
+ }
113
+ if (params == null ? void 0 : params.pageCursor) {
114
+ search.set("pageCursor", params.pageCursor);
115
+ }
116
+ if (params == null ? void 0 : params.createdByApiKeyId) {
117
+ search.set("createdByApiKeyId", params.createdByApiKeyId);
118
+ }
119
+ const query = search.toString();
120
+ const envelope = await this.client.requestEnvelope(
121
+ `/v1/exports${query ? `?${query}` : ""}`,
122
+ {
123
+ method: "GET",
124
+ signal: params == null ? void 0 : params.signal
125
+ }
126
+ );
127
+ return {
128
+ items: envelope.data || [],
129
+ nextPageCursor: typeof ((_a = envelope.meta) == null ? void 0 : _a.nextPageCursor) === "string" ? envelope.meta.nextPageCursor : null
130
+ };
131
+ }
132
+ async wait(taskId, options = {}) {
133
+ var _a;
134
+ const timeoutMs = options.timeoutMs || 10 * 60 * 1e3;
135
+ const pollIntervalMs = options.pollIntervalMs || this.client.pollIntervalMs;
136
+ const startedAt = Date.now();
137
+ while (true) {
138
+ if ((_a = options.signal) == null ? void 0 : _a.aborted) {
139
+ throw new Error("wait aborted by caller signal");
140
+ }
141
+ if (Date.now() - startedAt > timeoutMs) {
142
+ throw new Error(`wait timeout after ${timeoutMs}ms`);
143
+ }
144
+ const task = await this.get(taskId, {
145
+ signal: options.signal
146
+ });
147
+ if (TERMINAL_STATUSES.has(task.status)) {
148
+ if (task.status === "FAILED" || task.status === "CANCELED") {
149
+ throw new APIError({
150
+ type: "TASK_TERMINAL_FAILURE",
151
+ title: "Task failed",
152
+ status: 422,
153
+ detail: task.error || `Task ended with status ${task.status}`,
154
+ errorCode: "TASK_TERMINAL_FAILURE"
155
+ });
156
+ }
157
+ return task;
158
+ }
159
+ await sleep(pollIntervalMs);
160
+ }
161
+ }
162
+ };
163
+
164
+ // src/resources/editor.ts
165
+ var EditorResource = class {
166
+ constructor(client) {
167
+ this.client = client;
168
+ }
169
+ async capabilities(options = {}) {
170
+ return await this.client.request("/v1/editor/capabilities", {
171
+ method: "GET",
172
+ signal: options.signal
173
+ });
174
+ }
175
+ async validate(editorState, options = {}) {
176
+ return await this.client.request("/v1/editor/validate", {
177
+ method: "POST",
178
+ body: {
179
+ editorState
180
+ },
181
+ signal: options.signal,
182
+ skipRetry: true
183
+ });
184
+ }
185
+ };
186
+
187
+ // src/retry.ts
188
+ var sleep2 = async (ms) => {
189
+ await new Promise((resolve) => setTimeout(resolve, ms));
190
+ };
191
+ var shouldRetryStatus = (status) => {
192
+ return status === 429 || status >= 500;
193
+ };
194
+ var computeRetryDelay = (attempt, baseDelayMs = 300, maxDelayMs = 3e3) => {
195
+ const exponential = Math.min(maxDelayMs, baseDelayMs * 2 ** attempt);
196
+ const jitter = Math.floor(Math.random() * 100);
197
+ return exponential + jitter;
198
+ };
199
+ var withRetry = async (execute, options) => {
200
+ let attempt = 0;
201
+ while (true) {
202
+ try {
203
+ return await execute(attempt);
204
+ } catch (error) {
205
+ if (error && typeof error === "object" && "noRetry" in error) {
206
+ throw error;
207
+ }
208
+ if (attempt >= options.maxRetries) {
209
+ throw error;
210
+ }
211
+ const delay = computeRetryDelay(attempt, options.baseDelayMs, options.maxDelayMs);
212
+ await sleep2(delay);
213
+ attempt += 1;
214
+ }
215
+ }
216
+ };
217
+
218
+ // src/client.ts
219
+ var IndreamClient = class {
220
+ constructor(options) {
221
+ if (!options.apiKey) {
222
+ throw new Error("apiKey is required");
223
+ }
224
+ this.apiKey = options.apiKey;
225
+ this.baseURL = (options.baseURL || "https://api.indream.ai").replace(/\/$/, "");
226
+ this.timeout = options.timeout || 6e4;
227
+ this.maxRetries = Number.isFinite(options.maxRetries) ? Number(options.maxRetries) : 2;
228
+ this.pollIntervalMs = options.pollIntervalMs || 2e3;
229
+ this.fetchImpl = options.fetch || fetch;
230
+ this.exports = new ExportsResource(this);
231
+ this.editor = new EditorResource(this);
232
+ }
233
+ async request(path, init) {
234
+ const envelope = await this.requestEnvelope(path, init);
235
+ return envelope.data;
236
+ }
237
+ async requestEnvelope(path, init) {
238
+ const url = `${this.baseURL}${path.startsWith("/") ? path : `/${path}`}`;
239
+ const payload = init.body === void 0 ? void 0 : JSON.stringify(init.body);
240
+ const execute = async () => {
241
+ const controller = new AbortController();
242
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
243
+ const externalSignal = init.signal;
244
+ const onExternalAbort = () => controller.abort();
245
+ let hasExternalAbortListener = false;
246
+ if (externalSignal) {
247
+ if (externalSignal.aborted) {
248
+ controller.abort();
249
+ } else {
250
+ externalSignal.addEventListener("abort", onExternalAbort, { once: true });
251
+ hasExternalAbortListener = true;
252
+ }
253
+ }
254
+ const headers = __spreadValues({
255
+ "x-api-key": this.apiKey,
256
+ Accept: "application/json"
257
+ }, init.headers);
258
+ if (payload !== void 0) {
259
+ headers["Content-Type"] = "application/json";
260
+ }
261
+ if (init.idempotencyKey) {
262
+ headers["Idempotency-Key"] = init.idempotencyKey;
263
+ }
264
+ try {
265
+ const response = await this.fetchImpl(url, {
266
+ method: init.method,
267
+ body: payload,
268
+ headers,
269
+ signal: controller.signal
270
+ });
271
+ const text = await response.text();
272
+ const parsed = text ? JSON.parse(text) : null;
273
+ if (!response.ok) {
274
+ throw createApiError(response.status, parsed);
275
+ }
276
+ if (!parsed || typeof parsed !== "object" || !("data" in parsed)) {
277
+ throw createApiError(response.status, parsed);
278
+ }
279
+ return parsed;
280
+ } finally {
281
+ clearTimeout(timeoutId);
282
+ if (hasExternalAbortListener) {
283
+ externalSignal == null ? void 0 : externalSignal.removeEventListener("abort", onExternalAbort);
284
+ }
285
+ }
286
+ };
287
+ if (init.skipRetry) {
288
+ return await execute();
289
+ }
290
+ return await withRetry(
291
+ async () => {
292
+ try {
293
+ return await execute();
294
+ } catch (error) {
295
+ const apiError = error;
296
+ if ((apiError == null ? void 0 : apiError.status) && shouldRetryStatus(apiError.status)) {
297
+ throw error;
298
+ }
299
+ if (error instanceof Error && error.name === "AbortError") {
300
+ throw {
301
+ noRetry: true,
302
+ error
303
+ };
304
+ }
305
+ if (apiError == null ? void 0 : apiError.status) {
306
+ throw {
307
+ noRetry: true,
308
+ error
309
+ };
310
+ }
311
+ throw error;
312
+ }
313
+ },
314
+ {
315
+ maxRetries: this.maxRetries
316
+ }
317
+ ).catch((error) => {
318
+ if (error && typeof error === "object" && "noRetry" in error) {
319
+ throw error.error;
320
+ }
321
+ throw error;
322
+ });
323
+ }
324
+ };
325
+
326
+ // src/webhooks.ts
327
+ var INDREAM_WEBHOOK_TIMESTAMP_HEADER = "X-Indream-Timestamp";
328
+ var INDREAM_WEBHOOK_SIGNATURE_HEADER = "X-Indream-Signature";
329
+ var DEFAULT_WEBHOOK_MAX_SKEW_SECONDS = 300;
330
+ var EXPORT_WEBHOOK_EVENT_TYPES = /* @__PURE__ */ new Set([
331
+ "EXPORT_STARTED",
332
+ "EXPORT_COMPLETED",
333
+ "EXPORT_FAILED"
334
+ ]);
335
+ var TASK_STATUSES = /* @__PURE__ */ new Set([
336
+ "PENDING",
337
+ "PROCESSING",
338
+ "COMPLETED",
339
+ "FAILED",
340
+ "PAUSED",
341
+ "CANCELED"
342
+ ]);
343
+ var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
344
+ var isNullableString = (value) => typeof value === "string" || value === null;
345
+ var toArrayBuffer = (bytes) => {
346
+ const copy = new Uint8Array(bytes.byteLength);
347
+ copy.set(bytes);
348
+ return copy.buffer;
349
+ };
350
+ var toUtf8ArrayBuffer = (value) => toArrayBuffer(new TextEncoder().encode(value));
351
+ var parseHexToBytes = (value) => {
352
+ const normalized = value.trim().toLowerCase();
353
+ if (!/^[0-9a-f]+$/.test(normalized) || normalized.length % 2 !== 0) {
354
+ return null;
355
+ }
356
+ const bytes = new Uint8Array(normalized.length / 2);
357
+ for (let index = 0; index < bytes.length; index += 1) {
358
+ const start = index * 2;
359
+ bytes[index] = Number.parseInt(normalized.slice(start, start + 2), 16);
360
+ }
361
+ return bytes;
362
+ };
363
+ var parseTimestampSeconds = (value) => {
364
+ if (!/^\d+$/.test(value)) {
365
+ return null;
366
+ }
367
+ const parsed = Number(value);
368
+ if (!Number.isSafeInteger(parsed)) {
369
+ return null;
370
+ }
371
+ return parsed;
372
+ };
373
+ var resolveHeaderValue = (headers, targetKey) => {
374
+ if (headers instanceof Headers) {
375
+ const value = headers.get(targetKey);
376
+ if (typeof value !== "string") {
377
+ return null;
378
+ }
379
+ const trimmed = value.trim();
380
+ return trimmed ? trimmed : null;
381
+ }
382
+ const lowerKey = targetKey.toLowerCase();
383
+ for (const [key, rawValue] of Object.entries(headers)) {
384
+ if (key.toLowerCase() !== lowerKey) {
385
+ continue;
386
+ }
387
+ const value = Array.isArray(rawValue) ? rawValue[0] : rawValue;
388
+ if (typeof value !== "string") {
389
+ return null;
390
+ }
391
+ const trimmed = value.trim();
392
+ return trimmed ? trimmed : null;
393
+ }
394
+ return null;
395
+ };
396
+ var isExportWebhookEventType = (value) => typeof value === "string" && EXPORT_WEBHOOK_EVENT_TYPES.has(value);
397
+ var isTaskStatus = (value) => typeof value === "string" && TASK_STATUSES.has(value);
398
+ var isExportTaskSnapshot = (value) => {
399
+ if (!isObject(value)) {
400
+ return false;
401
+ }
402
+ return typeof value.taskId === "string" && isNullableString(value.createdByApiKeyId) && isNullableString(value.clientTaskId) && isTaskStatus(value.status) && typeof value.progress === "number" && isNullableString(value.error) && isNullableString(value.outputUrl) && typeof value.durationSeconds === "number" && typeof value.billedStandardSeconds === "number" && typeof value.chargedCredits === "string" && isNullableString(value.callbackUrl) && typeof value.createdAt === "string" && isNullableString(value.completedAt);
403
+ };
404
+ var isExportWebhookEvent = (value) => {
405
+ if (!isObject(value)) {
406
+ return false;
407
+ }
408
+ return isExportWebhookEventType(value.eventType) && typeof value.occurredAt === "string" && isExportTaskSnapshot(value.task);
409
+ };
410
+ var parseExportWebhookEvent = (value) => {
411
+ if (!isExportWebhookEvent(value)) {
412
+ throw new TypeError("Invalid export webhook payload");
413
+ }
414
+ return value;
415
+ };
416
+ var verifyExportWebhookSignature = async ({
417
+ webhookSecret,
418
+ timestamp,
419
+ rawBody,
420
+ signature
421
+ }) => {
422
+ var _a;
423
+ if (!webhookSecret || !timestamp || !signature) {
424
+ return false;
425
+ }
426
+ const signatureBytes = parseHexToBytes(signature);
427
+ if (!signatureBytes) {
428
+ return false;
429
+ }
430
+ const subtle = (_a = globalThis.crypto) == null ? void 0 : _a.subtle;
431
+ if (!subtle) {
432
+ throw new Error("Web Crypto subtle API is not available in current runtime");
433
+ }
434
+ const key = await subtle.importKey(
435
+ "raw",
436
+ toUtf8ArrayBuffer(webhookSecret),
437
+ { name: "HMAC", hash: "SHA-256" },
438
+ false,
439
+ ["verify"]
440
+ );
441
+ return subtle.verify(
442
+ "HMAC",
443
+ key,
444
+ toArrayBuffer(signatureBytes),
445
+ toUtf8ArrayBuffer(`${timestamp}.${rawBody}`)
446
+ );
447
+ };
448
+ var verifyExportWebhookRequest = async ({
449
+ webhookSecret,
450
+ rawBody,
451
+ headers,
452
+ maxSkewSeconds = DEFAULT_WEBHOOK_MAX_SKEW_SECONDS,
453
+ nowTimestampSeconds = Math.floor(Date.now() / 1e3)
454
+ }) => {
455
+ const timestamp = resolveHeaderValue(headers, INDREAM_WEBHOOK_TIMESTAMP_HEADER);
456
+ const signature = resolveHeaderValue(headers, INDREAM_WEBHOOK_SIGNATURE_HEADER);
457
+ if (!timestamp || !signature) {
458
+ return false;
459
+ }
460
+ const parsedTimestamp = parseTimestampSeconds(timestamp);
461
+ if (parsedTimestamp === null || !Number.isFinite(maxSkewSeconds) || maxSkewSeconds < 0) {
462
+ return false;
463
+ }
464
+ if (Math.abs(nowTimestampSeconds - parsedTimestamp) > maxSkewSeconds) {
465
+ return false;
466
+ }
467
+ return verifyExportWebhookSignature({
468
+ webhookSecret,
469
+ timestamp,
470
+ rawBody,
471
+ signature
472
+ });
473
+ };
474
+ export {
475
+ APIError,
476
+ AuthError,
477
+ IndreamClient,
478
+ RateLimitError,
479
+ ValidationError,
480
+ createApiError,
481
+ isExportTaskSnapshot,
482
+ isExportWebhookEvent,
483
+ isExportWebhookEventType,
484
+ isTaskStatus,
485
+ parseExportWebhookEvent,
486
+ toApiProblem,
487
+ verifyExportWebhookRequest,
488
+ verifyExportWebhookSignature
489
+ };
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@indreamai/client",
3
+ "version": "0.1.0",
4
+ "description": "Official JavaScript client for Indream Open API (Node.js and Edge runtimes)",
5
+ "type": "module",
6
+ "main": "dist/index.cjs",
7
+ "module": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsup src/index.ts --format esm,cjs --dts",
21
+ "typecheck": "tsc --noEmit",
22
+ "lint": "eslint src tests",
23
+ "test": "vitest run",
24
+ "test:api": "vitest run tests/api",
25
+ "test:editor-state:core": "vitest run tests/editor-state/core",
26
+ "test:editor-state:full": "vitest run tests/editor-state/full",
27
+ "test:mock": "pnpm run test:api && pnpm run test:editor-state:core",
28
+ "test:ci": "pnpm run test:mock && pnpm run test:editor-state:full",
29
+ "test:live": "vitest run tests/integration",
30
+ "sync:spec": "node scripts/sync-spec.mjs",
31
+ "generate:types": "openapi-typescript openapi/openapi.yaml -o src/generated/openapi.ts"
32
+ },
33
+ "keywords": [
34
+ "indream",
35
+ "client",
36
+ "api",
37
+ "typescript",
38
+ "edge",
39
+ "workers"
40
+ ],
41
+ "license": "UNLICENSED",
42
+ "devDependencies": {
43
+ "@edge-runtime/vm": "5.0.0",
44
+ "@types/node": "24.10.0",
45
+ "@typescript-eslint/eslint-plugin": "8.46.3",
46
+ "@typescript-eslint/parser": "8.46.3",
47
+ "ajv": "8.17.1",
48
+ "eslint": "9.39.1",
49
+ "eslint-config-prettier": "10.1.8",
50
+ "eslint-plugin-prettier": "5.5.4",
51
+ "openapi-typescript": "7.10.1",
52
+ "prettier": "3.6.2",
53
+ "tsup": "8.5.0",
54
+ "typescript": "5.9.3",
55
+ "vitest": "3.2.4"
56
+ },
57
+ "engines": {
58
+ "node": ">=18"
59
+ },
60
+ "packageManager": "pnpm@10.26.1"
61
+ }