@rendobar/sdk 5.1.0 → 5.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rendobar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,110 +1,84 @@
1
- # @rendobar/sdk
1
+ <p align="center">
2
+ <a href="https://rendobar.com">
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset="https://cdn.rendobar.com/assets/brand/logo-mark.svg">
5
+ <img alt="Rendobar" src="https://cdn.rendobar.com/assets/brand/logo-mark-black.svg" width="80">
6
+ </picture>
7
+ </a>
8
+ </p>
2
9
 
3
- TypeScript client for the Rendobar media processing API.
10
+ <h1 align="center">@rendobar/sdk</h1>
4
11
 
5
- ## Install
12
+ <p align="center">
13
+ <strong>TypeScript client for the Rendobar media processing API.</strong>
14
+ </p>
6
15
 
7
- ```bash
8
- npm install @rendobar/sdk
9
- ```
16
+ <p align="center">
17
+ <a href="https://rendobar.com/docs/sdk">Docs</a> &nbsp;·&nbsp;
18
+ <a href="https://rendobar.com/docs/quickstart">Quickstart</a> &nbsp;·&nbsp;
19
+ <a href="https://www.npmjs.com/package/@rendobar/sdk">npm</a> &nbsp;·&nbsp;
20
+ <a href="https://discord.gg/kAGqjBzx8N">Discord</a>
21
+ </p>
10
22
 
11
- ## Quick Start
23
+ <p align="center">
24
+ <a href="https://www.npmjs.com/package/@rendobar/sdk"><img src="https://img.shields.io/npm/v/@rendobar/sdk?style=flat-square&color=059669&label=npm" alt="npm version"></a>
25
+ <a href="https://www.npmjs.com/package/@rendobar/sdk"><img src="https://img.shields.io/npm/dm/@rendobar/sdk?style=flat-square&color=059669" alt="npm downloads"></a>
26
+ <img src="https://img.shields.io/node/v/@rendobar/sdk?style=flat-square&color=059669" alt="Node version">
27
+ </p>
12
28
 
13
- ```typescript
14
- import { createClient } from "@rendobar/sdk";
29
+ `@rendobar/sdk` is the official TypeScript client for [Rendobar](https://rendobar.com), a serverless media processing API. You submit a job, the SDK polls it to completion, and you get back a hosted URL. It ships ESM and CJS builds with full types, retries 429 and 5xx automatically, auto-paginates, and accepts an `AbortSignal` on every method.
15
30
 
16
- const client = createClient({ apiKey: "rb_..." });
31
+ ## Requirements
17
32
 
18
- // Submit a job
19
- const job = await client.jobs.create({
20
- type: "watermark.apply",
21
- inputs: { source: "https://example.com/video.mp4" },
22
- params: { text: "PREVIEW", position: "center", opacity: 0.3 },
23
- });
33
+ Node 18 or later. Works in any runtime with `fetch`, including Cloudflare Workers, Deno, and Bun.
24
34
 
25
- // Wait for completion
26
- import { outputUrl } from "@rendobar/sdk";
35
+ ## Install
27
36
 
28
- const result = await client.jobs.wait(job.id);
29
- console.log(outputUrl(result)); // primary download/playable URL
37
+ ```bash
38
+ npm install @rendobar/sdk
30
39
  ```
31
40
 
32
- ## Authentication
33
-
34
- ```typescript
35
- // API key (external users, CLI, scripts)
36
- const client = createClient({ apiKey: "rb_..." });
37
-
38
- // Session cookie (internal dashboard)
39
- const client = createClient({
40
- baseUrl: "https://api.rendobar.com",
41
- credentials: "include",
42
- });
43
- ```
41
+ ## Quickstart
44
42
 
45
- ## Configuration
43
+ Run an FFmpeg command against a remote file and wait for the result. Put input
44
+ URLs directly in `-i` positions; the SDK stages them before execution.
46
45
 
47
46
  ```typescript
48
- const client = createClient({
49
- apiKey: "rb_...",
50
- baseUrl: "https://api.rendobar.com", // default
51
- timeout: 30_000, // default: 30s
52
- maxRetries: 2, // default: 2 (retries 429, 5xx)
53
- orgId: "org_abc", // X-Org-Id header
54
- debug: false, // log request metadata
55
- fetch: customFetch, // custom fetch for testing
56
- });
57
- ```
47
+ import { createClient, outputUrl } from "@rendobar/sdk";
58
48
 
59
- ## Jobs
49
+ const client = createClient({ apiKey: process.env.RENDOBAR_API_KEY });
60
50
 
61
- ```typescript
62
- // Create
63
51
  const job = await client.jobs.create({
64
- type: "watermark.apply",
65
- inputs: { source: "https://example.com/video.mp4" },
66
- params: { text: "PREVIEW", position: "center" },
67
- idempotencyKey: "unique-key-123",
52
+ type: "ffmpeg",
53
+ params: {
54
+ command:
55
+ "ffmpeg -i https://cdn.example.com/source.mp4 " +
56
+ "-vf scale=1280:720 -c:v libx264 -crf 23 output.mp4",
57
+ },
68
58
  });
69
59
 
70
- // Get
71
- const job = await client.jobs.get("job_abc");
72
-
73
- // List (paginated — works with React Query)
74
- const page = await client.jobs.list({ status: "complete", limit: 20 });
75
- // page.data: Job[], page.meta: { total, page, limit }
60
+ const done = await client.jobs.wait(job.id);
61
+ console.log(outputUrl(done));
62
+ ```
76
63
 
77
- // Auto-paginate all pages
78
- for await (const job of client.jobs.listAll({ status: "complete" })) {
79
- console.log(job.id);
80
- }
64
+ Prefer named inputs? Pass them in `inputs` and reference the keys in the command:
81
65
 
82
- // Wait for completion (polls until terminal status)
83
- const result = await client.jobs.wait("job_abc", {
84
- timeout: 300_000,
85
- interval: 2_000,
86
- onProgress: (job) => console.log(job.status),
87
- signal: abortController.signal,
66
+ ```typescript
67
+ await client.jobs.create({
68
+ type: "ffmpeg",
69
+ inputs: { source: "https://cdn.example.com/source.mp4" },
70
+ params: { command: "ffmpeg -i source -vf scale=1280:720 output.mp4" },
88
71
  });
89
-
90
- // Cancel
91
- await client.jobs.cancel("job_abc");
92
-
93
- // Download output (returns raw Response)
94
- const response = await client.jobs.download("job_abc");
95
- const blob = await response.blob();
96
-
97
- // Execution logs
98
- const logs = await client.jobs.logs("job_abc");
99
-
100
- // Available job types
101
- const types = await client.jobs.types();
102
72
  ```
103
73
 
104
- ### Job output
74
+ `ffmpeg` is one of several job types. Composition, compression to a size budget,
75
+ subtitle burn-in, animated captions, and media inspection each have their own.
76
+ Browse them at [rendobar.com/docs/jobs](https://rendobar.com/docs/jobs), or call
77
+ `client.jobs.types()` to read the live registry.
78
+
79
+ ## Reading a result
105
80
 
106
- Every job type returns the same output shape. A completed job carries an
107
- `output` with four fields:
81
+ Every job type returns the same output shape, so you write this handling once.
108
82
 
109
83
  ```typescript
110
84
  type Output = {
@@ -123,219 +97,177 @@ type OutputFile = {
123
97
  };
124
98
  ```
125
99
 
126
- `output` exists only on a completed job (`status === "complete"`), so narrow on
127
- status before reading it.
100
+ `output` exists only when `status === "complete"`, so narrow on status before
101
+ reading it.
128
102
 
129
103
  ```typescript
130
104
  import { outputUrl, jobData } from "@rendobar/sdk";
131
105
 
132
106
  const job = await client.jobs.wait("job_abc");
133
107
 
134
- // Headline URL: the single file, or a stream manifest (.m3u8/.mpd). Returns
135
- // undefined for data-only jobs and pure file sets (no single headline).
108
+ // Headline URL: the single file, or a stream manifest (.m3u8/.mpd). Undefined
109
+ // for data-only jobs and for pure file sets with no single headline.
136
110
  const url = outputUrl(job);
137
111
 
138
- // Or read it yourself after a status check
139
112
  if (job.status === "complete") {
140
- console.log(job.output.file?.url);
141
-
142
- // Iterate every produced file (frame extraction, HLS segments, sprite sets)
143
113
  for (const f of job.output.files) {
144
114
  console.log(f.type, f.path, f.size, f.url);
145
115
  }
146
116
  }
147
117
 
148
- // A failed job carries a structured error instead
149
118
  if (job.status === "failed") {
150
119
  console.log(job.error.code, job.error.message, job.error.detail);
151
120
  }
152
121
  ```
153
122
 
154
- For data jobs (`extract.metadata`, `caption.extract`, detections), the answer is
155
- in `output.data`. It is `unknown` at the contract level because its shape is
156
- job-type-specific. Use `jobData<T>(job)` to read it with your expected type, or
157
- read `job.output.data` and narrow it yourself:
123
+ Inspection jobs put their answer in `output.data`, which is `unknown` at the
124
+ contract level because its shape depends on the job type. `jobData<T>(job)`
125
+ reads it as your expected type:
158
126
 
159
127
  ```typescript
160
- import { jobData } from "@rendobar/sdk";
161
-
162
- type Metadata = { format: string; durationMs: number; width: number; height: number };
163
-
164
- const job = await client.jobs.wait("job_abc");
165
- const meta = jobData<Metadata>(job); // Metadata | null
128
+ type Probe = { format: string; durationMs: number; width: number; height: number };
166
129
 
167
- if (meta) {
168
- console.log(meta.format, meta.durationMs);
169
- }
130
+ const probe = jobData<Probe>(await client.jobs.wait("job_abc")); // Probe | null
170
131
  ```
171
132
 
172
- `T` is your claim about the shape for the job type you submitted. If the input is
173
- untrusted, validate `output.data` yourself (e.g. with Zod) instead of asserting a
174
- type.
175
-
176
- ## Raw FFmpeg
177
-
178
- Run arbitrary FFmpeg commands in a sandboxed container. Commands are parsed, sanitized (protocol whitelist/blacklist flags blocked), and executed with a clean process environment.
179
-
180
- Put input URLs directly in `-i` positions — they're extracted automatically and staged via presigned R2 URLs before execution:
133
+ `T` is your claim about the shape, not a guarantee. If the input is untrusted,
134
+ validate `output.data` with Zod rather than asserting.
181
135
 
182
- ```ts
183
- import { createClient } from "@rendobar/sdk";
136
+ ## Errors
184
137
 
185
- const client = createClient({ apiKey: process.env.RENDOBAR_API_KEY });
186
-
187
- const job = await client.jobs.create({
188
- type: "ffmpeg",
189
- params: {
190
- command:
191
- "ffmpeg -i https://cdn.example.com/source.mp4 " +
192
- "-vf scale=1280:720 -c:v libx264 -crf 23 output.mp4",
193
- },
194
- });
138
+ ```typescript
139
+ import { isApiError } from "@rendobar/sdk";
195
140
 
196
- const done = await client.jobs.wait(job.id);
197
- console.log(outputUrl(done));
141
+ try {
142
+ await client.jobs.create({ type: "ffmpeg", params: { command } });
143
+ } catch (err) {
144
+ if (isApiError(err)) {
145
+ console.log(err.code); // "INSUFFICIENT_CREDITS"
146
+ console.log(err.statusCode); // 402
147
+ console.log(err.retryAfter); // seconds, on 429s
148
+ }
149
+ }
198
150
  ```
199
151
 
200
- Prefer named inputs? Pass them in the top-level `inputs` map and reference the keys in the command:
152
+ Codes: `UNAUTHORIZED`, `FORBIDDEN`, `VALIDATION_ERROR`, `INSUFFICIENT_CREDITS`,
153
+ `RATE_LIMITED`, `NOT_FOUND`, `CONFLICT`, `INTERNAL_ERROR`. Full reference at
154
+ [rendobar.com/docs/support/errors](https://rendobar.com/docs/support/errors).
201
155
 
202
- ```ts
203
- await client.jobs.create({
204
- type: "ffmpeg",
205
- inputs: { source: "https://cdn.example.com/source.mp4" },
206
- params: { command: "ffmpeg -i source -vf scale=1280:720 output.mp4" },
207
- });
208
- ```
209
-
210
- Need a typed params object? Import `FfmpegParams` — optional fields (`timeout`, `compute`) have server-side defaults. The output format comes from the command's output filename.
156
+ 429 and 5xx retry automatically with exponential backoff (500ms, then 1s), twice
157
+ by default, respecting `Retry-After`. 400, 401, 403 and 404 throw immediately.
211
158
 
212
- See the [FFmpeg guide](https://rendobar.com/docs/guides/ffmpeg) for the full security model and supported flags.
159
+ ## The rest of the API
213
160
 
214
- ## Billing
161
+ <details>
162
+ <summary><strong>Jobs</strong></summary>
215
163
 
216
164
  ```typescript
217
- const state = await client.billing.state();
218
- const usage = await client.billing.usage({ start: "2026-01-01", end: "2026-03-29" });
219
- const txns = await client.billing.transactions({ page: 1, limit: 50 });
220
- ```
165
+ await client.jobs.create({ type, inputs, params, idempotencyKey });
166
+ await client.jobs.get("job_abc");
167
+ await client.jobs.cancel("job_abc");
168
+ await client.jobs.logs("job_abc");
169
+ await client.jobs.getTimings("job_abc");
170
+ await client.jobs.types(); // live job registry
221
171
 
222
- ## Uploads
172
+ const page = await client.jobs.list({ status: "complete", limit: 20 });
173
+ // page.data: Job[], page.meta: { total, page, limit }
223
174
 
224
- ```typescript
225
- // Upload a file, get a URL to use as job input
226
- const { downloadUrl } = await client.uploads.upload(file, { filename: "input.mp4" });
175
+ for await (const job of client.jobs.listAll({ status: "complete" })) { /* ... */ }
227
176
 
228
- const job = await client.jobs.create({
229
- type: "watermark.apply",
230
- inputs: { source: downloadUrl },
231
- params: { text: "PREVIEW" },
177
+ await client.jobs.wait("job_abc", {
178
+ timeout: 300_000,
179
+ interval: 2_000,
180
+ onProgress: (job) => console.log(job.status),
181
+ signal: controller.signal,
232
182
  });
183
+
184
+ const response = await client.jobs.download("job_abc"); // raw Response
233
185
  ```
186
+ </details>
234
187
 
235
- ## Batches
188
+ <details>
189
+ <summary><strong>Uploads, batches, billing</strong></summary>
236
190
 
237
191
  ```typescript
238
- const batch = await client.batches.create({
192
+ const { downloadUrl } = await client.uploads.upload(file, { filename: "input.mp4" });
193
+
194
+ await client.batches.create({
239
195
  jobs: [
240
- { type: "watermark.apply", inputs: { source: url1 }, params: { text: "1" } },
241
- { type: "watermark.apply", inputs: { source: url2 }, params: { text: "2" } },
196
+ { type: "ffmpeg", inputs: { source: url1 }, params: { command } },
197
+ { type: "ffmpeg", inputs: { source: url2 }, params: { command } },
242
198
  ],
243
199
  });
200
+
201
+ await client.billing.state();
202
+ await client.billing.usage({ start: "2026-01-01", end: "2026-03-29" });
203
+ await client.billing.transactions({ page: 1, limit: 50 });
244
204
  ```
205
+ </details>
245
206
 
246
- ## Webhooks
207
+ <details>
208
+ <summary><strong>Webhooks</strong></summary>
247
209
 
248
210
  ```typescript
249
- // Create endpoint
250
- const endpoint = await client.webhooks.create({
211
+ await client.webhooks.create({
251
212
  name: "My Webhook",
252
213
  url: "https://example.com/webhook",
253
214
  subscribedEvents: ["job.completed", "job.failed"],
254
215
  });
216
+ ```
217
+
218
+ Verify a delivery with the **raw body**, not parsed JSON. `verifyWebhook` reads
219
+ the signature and timestamp headers, rebuilds the signed string, checks the
220
+ HMAC, rejects stale deliveries, and handles secret rotation.
255
221
 
256
- // Verify a delivery in your webhook handler. Pass the raw body (a string, not
257
- // parsed JSON) and the request headers. verifyWebhook reads the signature and
258
- // timestamp headers, rebuilds the signed string, checks the HMAC, rejects stale
259
- // deliveries (replay protection), and handles secret rotation.
222
+ ```typescript
260
223
  import { verifyWebhook } from "@rendobar/sdk/webhooks";
261
224
 
262
- const ok = await verifyWebhook(rawBody, request.headers, signingSecret);
263
- if (!ok) throw new Error("invalid signature");
225
+ if (!(await verifyWebhook(rawBody, request.headers, signingSecret))) {
226
+ throw new Error("invalid signature");
227
+ }
264
228
  ```
265
229
 
266
- ## Realtime Events (WebSocket)
230
+ See the [webhooks guide](https://rendobar.com/docs/guides/webhooks).
231
+ </details>
232
+
233
+ <details>
234
+ <summary><strong>Realtime and client configuration</strong></summary>
235
+
236
+ Realtime needs session cookie auth. On an API key, use `jobs.wait()` instead.
267
237
 
268
238
  ```typescript
269
- // Org-wide event stream
270
239
  const connection = client.realtime.connect({
271
- onEvent: (event) => console.log(event.type, event),
272
- onLive: () => console.log("Replay complete, now live"),
273
- onResync: () => console.log("Buffer overflow refresh data"),
240
+ onEvent: (e) => console.log(e.type, e),
241
+ onLive: () => console.log("replay complete, now live"),
242
+ onResync: () => console.log("buffer overflow, refresh data"),
274
243
  });
275
244
 
276
- // Per-job subscription
277
245
  const sub = client.realtime.subscribeJob("job_abc", {
278
246
  onProgress: (e) => console.log(`${e.progress}%`),
279
- onStep: (e) => console.log(`Step: ${e.stepName}`),
280
- onComplete: (e) => console.log("Done!", e.status),
247
+ onComplete: (e) => console.log("done", e.status),
281
248
  });
282
249
 
283
- // Cleanup
284
250
  connection.disconnect();
285
251
  sub.unsubscribe();
286
252
  ```
287
253
 
288
- > **Note:** Realtime requires session cookie auth. API key users should use `jobs.wait()` for polling.
289
-
290
- ## Error Handling
291
-
292
254
  ```typescript
293
- import { createClient, ApiError, isApiError } from "@rendobar/sdk";
294
-
295
- try {
296
- await client.jobs.create({ type: "watermark.apply", ... });
297
- } catch (err) {
298
- if (isApiError(err)) {
299
- console.log(err.code); // "INSUFFICIENT_CREDITS"
300
- console.log(err.statusCode); // 402
301
- console.log(err.message); // "Not enough credits"
302
- console.log(err.retryAfter); // seconds (for 429s)
303
- }
304
- }
305
- ```
306
-
307
- Error codes: `UNAUTHORIZED`, `FORBIDDEN`, `VALIDATION_ERROR`, `INSUFFICIENT_CREDITS`, `RATE_LIMITED`, `NOT_FOUND`, `CONFLICT`, `INTERNAL_ERROR`.
308
-
309
- ## Retries
310
-
311
- The SDK automatically retries:
312
- - **429** (rate limited) — respects `Retry-After` header
313
- - **500, 502, 503, 504** — server errors
314
-
315
- Retries use exponential backoff (500ms, 1s). Max 2 retries by default. Non-retryable errors (400, 401, 403, 404) throw immediately.
316
-
317
- ## AbortSignal
318
-
319
- Every method accepts an optional `signal` for cancellation:
320
-
321
- ```typescript
322
- const controller = new AbortController();
323
- const job = await client.jobs.get("job_abc", { signal: controller.signal });
255
+ const client = createClient({
256
+ apiKey: "rb_...",
257
+ baseUrl: "https://api.rendobar.com", // default
258
+ timeout: 30_000, // default 30s
259
+ maxRetries: 2, // default 2
260
+ orgId: "org_abc", // X-Org-Id header
261
+ debug: false, // log request metadata
262
+ fetch: customFetch, // inject a fetch, for tests
263
+ });
324
264
 
325
- // Cancel the request
326
- controller.abort();
265
+ // Browser session cookie instead of a key
266
+ createClient({ baseUrl: "https://api.rendobar.com", credentials: "include" });
327
267
  ```
268
+ </details>
328
269
 
329
- ## Types
330
-
331
- All response types are exported:
332
-
333
- ```typescript
334
- import type {
335
- Job, JobStep, Output, OutputFile, FileType, JobError, Cost, JobType, JobCreatedResponse,
336
- BillingState, Transaction, UsageSummary,
337
- WebhookEndpoint, WebhookDelivery,
338
- Organization, ApiKey, PaginationMeta,
339
- BillingInvoice, PaymentMethod, LogEntryData,
340
- } from "@rendobar/sdk";
341
- ```
270
+ Response types are all exported: `Job`, `JobStep`, `Output`, `OutputFile`,
271
+ `FileType`, `JobError`, `Cost`, `JobType`, `BillingState`, `Transaction`,
272
+ `UsageSummary`, `WebhookEndpoint`, `WebhookDelivery`, `Organization`, `ApiKey`,
273
+ and `PaginationMeta`.
package/dist/index.cjs CHANGED
@@ -209,6 +209,25 @@ function createJobsResource(request) {
209
209
  signal: options?.signal
210
210
  });
211
211
  }
212
+ /**
213
+ * Create a job and wait for it to reach a terminal state, returning the
214
+ * finished job. A convenience wrapper over `create()` + `wait()` for
215
+ * callers who just want the result without juggling two calls — works for
216
+ * any job type, not just a specific product.
217
+ *
218
+ * Unlike `wait()` (which defaults `throwOnFailure` to `false` to preserve
219
+ * its existing behavior), `run()` defaults `throwOnFailure` to `true`: a
220
+ * job that ends "failed" or "cancelled" throws `JobFailedError` unless the
221
+ * caller explicitly opts out with `throwOnFailure: false`.
222
+ */
223
+ async function run(params, options = {}) {
224
+ const { signal, throwOnFailure = true, ...waitOptions } = options;
225
+ return wait((await create(params, { signal })).id, {
226
+ ...waitOptions,
227
+ signal,
228
+ throwOnFailure
229
+ });
230
+ }
212
231
  async function get(id, options) {
213
232
  return request(`/jobs/${id}`, { signal: options?.signal });
214
233
  }
@@ -265,6 +284,7 @@ function createJobsResource(request) {
265
284
  list,
266
285
  listAll,
267
286
  wait,
287
+ run,
268
288
  cancel,
269
289
  download,
270
290
  logs,