@rendobar/sdk 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.mjs ADDED
@@ -0,0 +1,640 @@
1
+ import { WebSocket } from "partysocket";
2
+ //#region src/errors.ts
3
+ /**
4
+ * SDK error class. All API errors are thrown as ApiError.
5
+ * Use isApiError() to narrow in catch blocks.
6
+ */
7
+ var ApiError = class extends Error {
8
+ constructor(code, statusCode, message, details, retryAfter) {
9
+ super(message);
10
+ this.code = code;
11
+ this.statusCode = statusCode;
12
+ this.details = details;
13
+ this.retryAfter = retryAfter;
14
+ this.name = "ApiError";
15
+ }
16
+ };
17
+ function isApiError(error) {
18
+ return error instanceof ApiError;
19
+ }
20
+ //#endregion
21
+ //#region src/lib/request.ts
22
+ /**
23
+ * Core HTTP request layer. Handles auth, retries, timeouts, envelope
24
+ * unwrapping, and error mapping. No external dependencies beyond fetch.
25
+ */
26
+ const DEFAULT_TIMEOUT = 3e4;
27
+ const DEFAULT_MAX_RETRIES = 2;
28
+ const RETRY_BASE_DELAY = 500;
29
+ const RETRYABLE_STATUS_CODES = new Set([
30
+ 429,
31
+ 500,
32
+ 502,
33
+ 503,
34
+ 504
35
+ ]);
36
+ function createRequestFn(config) {
37
+ const { baseUrl = "https://api.rendobar.com", apiKey, accessToken, credentials, orgId, timeout = DEFAULT_TIMEOUT, maxRetries = DEFAULT_MAX_RETRIES, debug = false, fetch: fetchFn = globalThis.fetch } = config;
38
+ return async function request(path, options = {}) {
39
+ const url = buildUrl(baseUrl, path, options.query);
40
+ const init = buildInit(options, apiKey, accessToken, credentials, orgId);
41
+ const combinedSignal = combineSignals(options.signal, timeout);
42
+ let lastError;
43
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
44
+ if (attempt > 0) await sleep$1(calcRetryDelay(attempt, lastError));
45
+ const start = Date.now();
46
+ try {
47
+ const response = await fetchFn(url, {
48
+ ...init,
49
+ signal: combinedSignal
50
+ });
51
+ const duration = Date.now() - start;
52
+ if (debug) console.debug(`[sdk] ${init.method ?? "GET"} ${path} → ${response.status} (${duration}ms)`);
53
+ if (response.ok) {
54
+ if (response.status === 204) return void 0;
55
+ if (options.raw) return response;
56
+ return parseResponse(await response.json());
57
+ }
58
+ const apiError = await parseErrorResponse(response);
59
+ lastError = apiError;
60
+ if (!RETRYABLE_STATUS_CODES.has(response.status) || attempt === maxRetries) throw apiError;
61
+ } catch (error) {
62
+ if (error instanceof ApiError) {
63
+ if (!RETRYABLE_STATUS_CODES.has(error.statusCode) || attempt === maxRetries) throw error;
64
+ lastError = error;
65
+ continue;
66
+ }
67
+ if (isAbortError(error)) throw error;
68
+ lastError = error instanceof Error ? error : new Error(String(error));
69
+ if (attempt === maxRetries) throw lastError;
70
+ }
71
+ }
72
+ throw lastError;
73
+ };
74
+ }
75
+ function buildUrl(baseUrl, path, query) {
76
+ let url = `${baseUrl}${path}`;
77
+ if (query) {
78
+ const params = new URLSearchParams();
79
+ for (const [k, v] of Object.entries(query)) if (v != null) params.set(k, String(v));
80
+ const qs = params.toString();
81
+ if (qs) url += `?${qs}`;
82
+ }
83
+ return url;
84
+ }
85
+ function buildInit(options, apiKey, accessToken, credentials, orgId) {
86
+ const headers = { "Content-Type": "application/json" };
87
+ if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
88
+ else if (accessToken) headers["Authorization"] = `Bearer ${accessToken}`;
89
+ if (orgId) headers["X-Org-Id"] = orgId;
90
+ const init = {
91
+ method: options.method ?? "GET",
92
+ headers
93
+ };
94
+ if (credentials) init.credentials = credentials;
95
+ if (options.bodyRaw !== void 0) {
96
+ init.body = options.bodyRaw;
97
+ delete headers["Content-Type"];
98
+ } else if (options.body !== void 0) init.body = JSON.stringify(options.body);
99
+ return init;
100
+ }
101
+ function combineSignals(userSignal, timeout) {
102
+ const timeoutSignal = AbortSignal.timeout(timeout);
103
+ if (!userSignal) return timeoutSignal;
104
+ return AbortSignal.any([userSignal, timeoutSignal]);
105
+ }
106
+ function isDataEnvelope(json) {
107
+ return json !== null && typeof json === "object" && "data" in json;
108
+ }
109
+ function parseResponse(json) {
110
+ if (isDataEnvelope(json)) {
111
+ if (json.meta !== void 0) return json;
112
+ return json.data;
113
+ }
114
+ return json;
115
+ }
116
+ async function parseErrorResponse(response) {
117
+ let code = "UNKNOWN_ERROR";
118
+ let message = `Request failed with status ${response.status}`;
119
+ let details;
120
+ let retryAfter;
121
+ try {
122
+ const body = await response.json();
123
+ if (body.error) {
124
+ code = body.error.code ?? code;
125
+ message = body.error.message ?? message;
126
+ details = body.error.details;
127
+ }
128
+ } catch {}
129
+ if (response.status === 429) {
130
+ const header = response.headers.get("Retry-After");
131
+ if (header) retryAfter = Number(header);
132
+ }
133
+ return new ApiError(code, response.status, message, details, retryAfter);
134
+ }
135
+ function calcRetryDelay(attempt, lastError) {
136
+ if (lastError instanceof ApiError && lastError.retryAfter) return lastError.retryAfter * 1e3;
137
+ return RETRY_BASE_DELAY * 2 ** (attempt - 1);
138
+ }
139
+ function sleep$1(ms) {
140
+ return new Promise((resolve) => setTimeout(resolve, ms));
141
+ }
142
+ function isAbortError(error) {
143
+ return error instanceof DOMException && error.name === "AbortError";
144
+ }
145
+ //#endregion
146
+ //#region src/resources/jobs.ts
147
+ const TERMINAL_STATUSES$1 = new Set([
148
+ "complete",
149
+ "failed",
150
+ "cancelled"
151
+ ]);
152
+ function createJobsResource(request) {
153
+ return {
154
+ async create(params, options) {
155
+ return request("/jobs", {
156
+ method: "POST",
157
+ body: params,
158
+ signal: options?.signal
159
+ });
160
+ },
161
+ async get(id, options) {
162
+ return request(`/jobs/${id}`, { signal: options?.signal });
163
+ },
164
+ async list(params, options) {
165
+ return request("/jobs", {
166
+ query: params,
167
+ signal: options?.signal
168
+ });
169
+ },
170
+ async *listAll(params) {
171
+ let offset = 0;
172
+ const limit = params?.limit ?? 50;
173
+ while (true) {
174
+ const page = await request("/jobs", { query: {
175
+ ...params,
176
+ limit,
177
+ offset
178
+ } });
179
+ for (const job of page.data) yield job;
180
+ if (offset + page.data.length >= page.meta.total) break;
181
+ offset += limit;
182
+ }
183
+ },
184
+ async wait(id, options = {}) {
185
+ const { timeout = 3e5, interval = 2e3, onProgress, signal } = options;
186
+ const deadline = Date.now() + timeout;
187
+ while (Date.now() < deadline) {
188
+ if (signal?.aborted) throw new DOMException("Wait aborted", "AbortError");
189
+ const job = await request(`/jobs/${id}`, { signal });
190
+ onProgress?.(job);
191
+ if (TERMINAL_STATUSES$1.has(job.status)) return job;
192
+ const remaining = deadline - Date.now();
193
+ const delay = Math.min(interval, remaining);
194
+ if (delay <= 0) break;
195
+ await sleep(delay, signal);
196
+ }
197
+ const job = await request(`/jobs/${id}`, { signal });
198
+ if (TERMINAL_STATUSES$1.has(job.status)) return job;
199
+ throw new Error(`Job ${id} did not complete within ${timeout}ms (status: ${job.status})`);
200
+ },
201
+ async cancel(id, options) {
202
+ return request(`/jobs/${id}/cancel`, {
203
+ method: "POST",
204
+ signal: options?.signal
205
+ });
206
+ },
207
+ async download(id, options) {
208
+ return request(`/jobs/${id}/download`, {
209
+ raw: true,
210
+ signal: options?.signal
211
+ });
212
+ },
213
+ async logs(id, options) {
214
+ return request(`/jobs/${id}/logs`, { signal: options?.signal });
215
+ },
216
+ async types(options) {
217
+ return request("/jobs/types", { signal: options?.signal });
218
+ }
219
+ };
220
+ }
221
+ function sleep(ms, signal) {
222
+ return new Promise((resolve, reject) => {
223
+ if (signal?.aborted) {
224
+ reject(new DOMException("Wait aborted", "AbortError"));
225
+ return;
226
+ }
227
+ const timer = setTimeout(resolve, ms);
228
+ signal?.addEventListener("abort", () => {
229
+ clearTimeout(timer);
230
+ reject(new DOMException("Wait aborted", "AbortError"));
231
+ }, { once: true });
232
+ });
233
+ }
234
+ //#endregion
235
+ //#region src/resources/billing.ts
236
+ function createBillingResource(request) {
237
+ return {
238
+ async state(options) {
239
+ return request("/billing/state", { signal: options?.signal });
240
+ },
241
+ async usage(params, options) {
242
+ return request("/billing/usage", {
243
+ query: params,
244
+ signal: options?.signal
245
+ });
246
+ },
247
+ async transactions(params, options) {
248
+ return request("/billing/transactions", {
249
+ query: params,
250
+ signal: options?.signal
251
+ });
252
+ },
253
+ async checkoutCredits(amount, options) {
254
+ return request("/billing/checkout/credits", {
255
+ method: "POST",
256
+ body: { amount },
257
+ signal: options?.signal
258
+ });
259
+ },
260
+ async checkoutPro(options) {
261
+ return request("/billing/checkout/pro", {
262
+ method: "POST",
263
+ signal: options?.signal
264
+ });
265
+ },
266
+ async cancelSubscription(options) {
267
+ await request("/billing/subscription", {
268
+ method: "PATCH",
269
+ body: { cancelAtPeriodEnd: true },
270
+ signal: options?.signal
271
+ });
272
+ },
273
+ async reactivateSubscription(options) {
274
+ await request("/billing/subscription", {
275
+ method: "PATCH",
276
+ body: { cancelAtPeriodEnd: false },
277
+ signal: options?.signal
278
+ });
279
+ },
280
+ async history(params, options) {
281
+ return request("/billing/history", {
282
+ query: params,
283
+ signal: options?.signal
284
+ });
285
+ },
286
+ async paymentMethods(options) {
287
+ return request("/billing/payment-methods", { signal: options?.signal });
288
+ },
289
+ async deletePaymentMethod(id, options) {
290
+ await request(`/billing/payment-methods/${id}`, {
291
+ method: "DELETE",
292
+ signal: options?.signal
293
+ });
294
+ },
295
+ async portalUrl(options) {
296
+ return request("/billing/payment-methods/portal-url", { signal: options?.signal });
297
+ }
298
+ };
299
+ }
300
+ //#endregion
301
+ //#region src/resources/uploads.ts
302
+ function createUploadsResource(request) {
303
+ return { async upload(file, options) {
304
+ return request("/uploads", {
305
+ method: "POST",
306
+ bodyRaw: file,
307
+ query: options?.filename ? { filename: options.filename } : void 0,
308
+ signal: options?.signal
309
+ });
310
+ } };
311
+ }
312
+ //#endregion
313
+ //#region src/resources/webhooks.ts
314
+ function createWebhooksResource(request) {
315
+ return {
316
+ async create(params, options) {
317
+ return request("/webhooks/endpoints", {
318
+ method: "POST",
319
+ body: params,
320
+ signal: options?.signal
321
+ });
322
+ },
323
+ async list(options) {
324
+ return request("/webhooks/endpoints", { signal: options?.signal });
325
+ },
326
+ async get(id, options) {
327
+ return request(`/webhooks/endpoints/${id}`, { signal: options?.signal });
328
+ },
329
+ async update(id, params, options) {
330
+ return request(`/webhooks/endpoints/${id}`, {
331
+ method: "PATCH",
332
+ body: params,
333
+ signal: options?.signal
334
+ });
335
+ },
336
+ async delete(id, options) {
337
+ await request(`/webhooks/endpoints/${id}`, {
338
+ method: "DELETE",
339
+ signal: options?.signal
340
+ });
341
+ },
342
+ async rotateSecret(id, options) {
343
+ return request(`/webhooks/endpoints/${id}/secret`, {
344
+ method: "POST",
345
+ signal: options?.signal
346
+ });
347
+ },
348
+ async test(id, options) {
349
+ return request(`/webhooks/endpoints/${id}/test`, {
350
+ method: "POST",
351
+ signal: options?.signal
352
+ });
353
+ },
354
+ async listDeliveries(params, options) {
355
+ return request("/webhooks/deliveries", {
356
+ query: params,
357
+ signal: options?.signal
358
+ });
359
+ },
360
+ async retryDelivery(id, options) {
361
+ return request(`/webhooks/deliveries/${id}/retry`, {
362
+ method: "POST",
363
+ signal: options?.signal
364
+ });
365
+ }
366
+ };
367
+ }
368
+ //#endregion
369
+ //#region src/resources/api-keys.ts
370
+ function createApiKeysResource(request) {
371
+ return {
372
+ async create(params, options) {
373
+ return request("/api-keys", {
374
+ method: "POST",
375
+ body: params,
376
+ signal: options?.signal
377
+ });
378
+ },
379
+ async list(options) {
380
+ return request("/api-keys", { signal: options?.signal });
381
+ },
382
+ async revoke(id, options) {
383
+ return request(`/api-keys/${id}`, {
384
+ method: "DELETE",
385
+ signal: options?.signal
386
+ });
387
+ }
388
+ };
389
+ }
390
+ //#endregion
391
+ //#region src/resources/orgs.ts
392
+ function createOrgsResource(request) {
393
+ return {
394
+ async list(options) {
395
+ return request("/orgs", { signal: options?.signal });
396
+ },
397
+ async current(options) {
398
+ return request("/orgs/current", { signal: options?.signal });
399
+ },
400
+ async create(params, options) {
401
+ return request("/orgs", {
402
+ method: "POST",
403
+ body: params,
404
+ signal: options?.signal
405
+ });
406
+ },
407
+ async update(params, options) {
408
+ return request("/orgs/current", {
409
+ method: "PATCH",
410
+ body: params,
411
+ signal: options?.signal
412
+ });
413
+ },
414
+ async delete(options) {
415
+ await request("/orgs/current", {
416
+ method: "DELETE",
417
+ signal: options?.signal
418
+ });
419
+ }
420
+ };
421
+ }
422
+ //#endregion
423
+ //#region src/resources/batches.ts
424
+ function createBatchesResource(request) {
425
+ return {
426
+ async create(params, options) {
427
+ return request("/batches", {
428
+ method: "POST",
429
+ body: params,
430
+ signal: options?.signal
431
+ });
432
+ },
433
+ async get(id, options) {
434
+ return request(`/batches/${id}`, { signal: options?.signal });
435
+ }
436
+ };
437
+ }
438
+ //#endregion
439
+ //#region src/resources/assets.ts
440
+ function createAssetsResource(request) {
441
+ return { async templates(params, options) {
442
+ return request("/assets/templates", {
443
+ query: params ? { ...params } : void 0,
444
+ signal: options?.signal
445
+ });
446
+ } };
447
+ }
448
+ //#endregion
449
+ //#region src/resources/team.ts
450
+ function createTeamResource(request) {
451
+ return {
452
+ async invite(params, options) {
453
+ return request("/team/invite", {
454
+ method: "POST",
455
+ body: params,
456
+ signal: options?.signal
457
+ });
458
+ },
459
+ async changeRole(params, options) {
460
+ await request("/team/role", {
461
+ method: "POST",
462
+ body: params,
463
+ signal: options?.signal
464
+ });
465
+ },
466
+ async remove(params, options) {
467
+ await request("/team/remove", {
468
+ method: "POST",
469
+ body: params,
470
+ signal: options?.signal
471
+ });
472
+ },
473
+ async revokeInvitation(params, options) {
474
+ await request("/team/revoke-invitation", {
475
+ method: "POST",
476
+ body: params,
477
+ signal: options?.signal
478
+ });
479
+ }
480
+ };
481
+ }
482
+ //#endregion
483
+ //#region src/realtime/client.ts
484
+ /**
485
+ * Realtime event client using partysocket for reconnection
486
+ * and the Rendobar OrgHub protocol for event replay + dedup.
487
+ *
488
+ * Protocol:
489
+ * 1. On connect: send { type: "init", lastEventId }
490
+ * 2. Server replays buffered events (each has monotonic `id` field)
491
+ * 3. Server sends { type: "replay.done" } — switch to live mode
492
+ * 4. Server sends { type: "resync" } — buffer overflow, caller should full-refresh
493
+ * 5. Dedup: skip events where id <= lastEventId
494
+ */
495
+ function parseMessage(raw) {
496
+ try {
497
+ const data = JSON.parse(raw);
498
+ if (data && typeof data === "object" && typeof data.type === "string") return data;
499
+ return null;
500
+ } catch {
501
+ return null;
502
+ }
503
+ }
504
+ const WS_OPTIONS = {
505
+ maxReconnectionDelay: 3e4,
506
+ minReconnectionDelay: 1e3,
507
+ reconnectionDelayGrowFactor: 1.5,
508
+ maxRetries: Infinity
509
+ };
510
+ const TERMINAL_STATUSES = new Set([
511
+ "complete",
512
+ "failed",
513
+ "cancelled"
514
+ ]);
515
+ /** Known OrgEvent types — only dispatch events with recognized types. */
516
+ const KNOWN_EVENT_TYPES = new Set([
517
+ "job.created",
518
+ "job.status",
519
+ "job.result",
520
+ "job.progress",
521
+ "job.step",
522
+ "job.log",
523
+ "balance.updated",
524
+ "subscription.updated",
525
+ "notification",
526
+ "org.updated"
527
+ ]);
528
+ /**
529
+ * Create a realtime client for WebSocket event streaming.
530
+ *
531
+ * Auth: session cookie (credentials: "include") OR API key via ?token= query param.
532
+ * The client appends the token automatically when apiKey is provided.
533
+ */
534
+ function createRealtimeClient(baseUrl, options) {
535
+ const wsUrl = baseUrl.replace("https://", "wss://").replace("http://", "ws://");
536
+ const token = options?.apiKey ?? options?.accessToken;
537
+ const tokenSuffix = token ? `?token=${encodeURIComponent(token)}` : "";
538
+ return {
539
+ connect(options) {
540
+ let lastEventId = options.lastEventId ?? 0;
541
+ const ws = new WebSocket(`${wsUrl}/events/ws${tokenSuffix}`, void 0, WS_OPTIONS);
542
+ ws.addEventListener("open", () => {
543
+ options.onReconnect?.();
544
+ ws.send(JSON.stringify({
545
+ type: "init",
546
+ lastEventId
547
+ }));
548
+ });
549
+ ws.addEventListener("message", (evt) => {
550
+ if (typeof evt.data !== "string") return;
551
+ const data = parseMessage(evt.data);
552
+ if (!data) return;
553
+ if (typeof data.id === "number") {
554
+ if (data.id <= lastEventId) return;
555
+ lastEventId = data.id;
556
+ }
557
+ if (data.type === "replay.done") {
558
+ options.onLive?.();
559
+ return;
560
+ }
561
+ if (data.type === "resync") {
562
+ options.onResync?.();
563
+ return;
564
+ }
565
+ if (KNOWN_EVENT_TYPES.has(data.type)) options.onEvent(data);
566
+ });
567
+ return { disconnect: () => ws.close() };
568
+ },
569
+ subscribeJob(jobId, options) {
570
+ const ws = new WebSocket(`${wsUrl}/events/ws${tokenSuffix}${tokenSuffix ? "&" : "?"}jobId=${jobId}`, void 0, WS_OPTIONS);
571
+ ws.addEventListener("open", () => {
572
+ ws.send(JSON.stringify({
573
+ type: "init",
574
+ lastEventId: 0
575
+ }));
576
+ });
577
+ ws.addEventListener("message", (evt) => {
578
+ if (typeof evt.data !== "string") return;
579
+ const data = parseMessage(evt.data);
580
+ if (!data || data.type === "replay.done" || data.type === "resync") return;
581
+ try {
582
+ dispatchJobEvent(data, options);
583
+ } catch (err) {
584
+ options.onError?.(err instanceof Error ? err : new Error(String(err)));
585
+ }
586
+ });
587
+ return { unsubscribe: () => ws.close() };
588
+ }
589
+ };
590
+ }
591
+ function dispatchJobEvent(event, options) {
592
+ switch (event.type) {
593
+ case "job.progress":
594
+ options.onProgress?.(event);
595
+ break;
596
+ case "job.step":
597
+ options.onStep?.(event);
598
+ break;
599
+ case "job.log":
600
+ options.onLog?.(event);
601
+ break;
602
+ case "job.status": {
603
+ const statusEvent = event;
604
+ options.onStatus?.(statusEvent);
605
+ if (TERMINAL_STATUSES.has(statusEvent.status)) options.onComplete?.(statusEvent);
606
+ break;
607
+ }
608
+ case "job.result":
609
+ options.onResult?.(event);
610
+ break;
611
+ }
612
+ }
613
+ //#endregion
614
+ //#region src/client.ts
615
+ /**
616
+ * SDK client factory. Creates a configured client with resource namespaces.
617
+ */
618
+ function createClient(config = {}) {
619
+ const baseUrl = config.baseUrl ?? "https://api.rendobar.com";
620
+ const request = createRequestFn(config);
621
+ return {
622
+ jobs: createJobsResource(request),
623
+ billing: createBillingResource(request),
624
+ uploads: createUploadsResource(request),
625
+ webhooks: createWebhooksResource(request),
626
+ apiKeys: createApiKeysResource(request),
627
+ orgs: createOrgsResource(request),
628
+ batches: createBatchesResource(request),
629
+ assets: createAssetsResource(request),
630
+ team: createTeamResource(request),
631
+ realtime: createRealtimeClient(baseUrl, {
632
+ apiKey: config.apiKey,
633
+ accessToken: config.accessToken
634
+ })
635
+ };
636
+ }
637
+ //#endregion
638
+ export { ApiError, createClient, isApiError };
639
+
640
+ //# sourceMappingURL=index.mjs.map