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