@convertrilo/sdk 0.0.11 → 0.0.12

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/README.md CHANGED
@@ -33,6 +33,8 @@ The `examples/` directory contains starter scripts for the main integration path
33
33
  - `node-url-to-s3.ts` - encode a public URL and upload the result to S3/S3-compatible storage
34
34
  - `google-drive-byo-token.ts` - upload output to Google Drive using customer OAuth tokens from your app
35
35
  - `folder-ingest-s3.ts` - queue one encode job per video in an S3 prefix
36
+ - `idempotency.ts` - safely retry `createJob` and `createJobsBulk`
37
+ - `webhook-receiver-hmac.ts` - verify managed webhook HMAC signatures from a Node receiver
36
38
 
37
39
  Local SDK smoke tests use `.env`, but published SDK users should provide credentials through their
38
40
  own server environment. Do not put Convertrilo API keys or customer storage tokens in frontend code.
@@ -75,10 +75,23 @@ export declare class ConvertriloClient {
75
75
  cancelJob(id: string): Promise<unknown>;
76
76
  jobStatus(id: string): Promise<{
77
77
  id?: string;
78
+ externalId?: string | null;
79
+ metadata?: {
80
+ [key: string]: unknown;
81
+ } | null;
78
82
  status?: string;
83
+ ingest?: string | null;
84
+ output?: {
85
+ key?: string;
86
+ publicUrl?: string;
87
+ } | null;
79
88
  downloadUrl?: string | null;
80
89
  encoder?: string | null;
81
90
  pct?: number | null;
91
+ createdAt?: string;
92
+ startedAt?: string | null;
93
+ finishedAt?: string | null;
94
+ failureMessage?: string | null;
82
95
  }>;
83
96
  createJobsBulk(body: paths["/jobs/bulk"]["post"]["requestBody"]["content"]["application/json"], options?: RequestOptions): Promise<{
84
97
  totalJobs?: number;
@@ -1642,13 +1642,39 @@ export interface components {
1642
1642
  };
1643
1643
  StatusResponse: {
1644
1644
  id?: string;
1645
+ externalId?: string | null;
1646
+ metadata?: {
1647
+ [key: string]: unknown;
1648
+ } | null;
1645
1649
  status?: string;
1650
+ ingest?: string | null;
1651
+ output?: {
1652
+ key?: string;
1653
+ publicUrl?: string;
1654
+ } | null;
1646
1655
  downloadUrl?: string | null;
1647
1656
  encoder?: string | null;
1648
1657
  pct?: number | null;
1658
+ /** Format: date-time */
1659
+ createdAt?: string;
1660
+ /** Format: date-time */
1661
+ startedAt?: string | null;
1662
+ /** Format: date-time */
1663
+ finishedAt?: string | null;
1664
+ failureMessage?: string | null;
1649
1665
  };
1650
1666
  ErrorResponse: {
1651
- message?: string;
1667
+ /**
1668
+ * @description Stable machine-readable error code. Branch on this instead of parsing `message`.
1669
+ * @enum {string}
1670
+ */
1671
+ code: "invalid_api_key" | "api_key_expired" | "user_not_found" | "account_banned" | "missing_required_scope" | "invalid_request" | "invalid_body" | "invalid_url" | "url_ingest_disabled" | "url_host_blocked" | "signed_url_required" | "host_not_resolvable" | "private_ip_blocked" | "missing_content_length" | "file_too_large" | "unsupported_content_type" | "probe_failed" | "insufficient_tokens" | "idempotency_conflict" | "idempotency_in_progress" | "not_found" | "forbidden" | "deprecated_dropbox" | "google_drive_not_connected" | "dropbox_not_connected" | "no_video_files_found" | "webhook_limit_exceeded" | "webhook_not_found" | "no_updates_provided" | "output_not_available";
1672
+ message: string;
1673
+ /** @description Validation details for `invalid_body`. */
1674
+ issues?: unknown;
1675
+ requiredScope?: string;
1676
+ required?: number;
1677
+ available?: number;
1652
1678
  };
1653
1679
  BulkCreateRequest: {
1654
1680
  jobs: components["schemas"]["JobCreateRequest"][];
@@ -79,6 +79,10 @@ const batch = await client.createJobsBulk({
79
79
  });
80
80
  ```
81
81
 
82
+ API errors include a stable `code` field plus a human-readable `message`. Branch on
83
+ `code` in your backend, for example `idempotency_conflict`, `insufficient_tokens`,
84
+ or `missing_required_scope`.
85
+
82
86
  ## Flow 1: URL Source To CDN Output
83
87
 
84
88
  Use this when the source video is already available over HTTP(S), and you want Convertrilo to return a signed CDN download URL.
@@ -0,0 +1,60 @@
1
+ import { ConvertriloClient } from "@convertrilo/sdk";
2
+
3
+ const apiKey = process.env.CONVERTRILO_API_KEY;
4
+ if (!apiKey) throw new Error("Set CONVERTRILO_API_KEY");
5
+
6
+ const client = new ConvertriloClient({
7
+ baseUrl: process.env.CONVERTRILO_API_URL || "https://api.convertrilo.com",
8
+ apiKey,
9
+ });
10
+
11
+ async function main() {
12
+ const externalId = `example-upload-${Date.now()}`;
13
+
14
+ const job = await client.createJob(
15
+ {
16
+ externalId,
17
+ metadata: {
18
+ customerId: "cus_123",
19
+ workflow: "sdk-idempotency-example",
20
+ },
21
+ codec: "h264",
22
+ resolution: "1080p",
23
+ fps: 30,
24
+ },
25
+ {
26
+ idempotencyKey: `job-${externalId}`,
27
+ },
28
+ );
29
+
30
+ console.log(`Created or replayed job ${job.jobId}`);
31
+
32
+ const batch = await client.createJobsBulk(
33
+ {
34
+ jobs: [
35
+ {
36
+ externalId: `${externalId}:clip-1`,
37
+ metadata: { customerId: "cus_123" },
38
+ codec: "h264",
39
+ resolution: "1080p",
40
+ fps: 30,
41
+ sourceS3: {
42
+ bucket: "customer-source-bucket",
43
+ key: "incoming/clip-1.mp4",
44
+ },
45
+ },
46
+ ],
47
+ settings: { dryRun: true },
48
+ },
49
+ {
50
+ idempotencyKey: `bulk-${externalId}`,
51
+ },
52
+ );
53
+
54
+ console.log(`Created or replayed bulk response with ${batch.totalJobs} job(s)`);
55
+ }
56
+
57
+ main().catch((error) => {
58
+ console.error(error);
59
+ process.exit(1);
60
+ });
@@ -0,0 +1,54 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ import http from "node:http";
3
+
4
+ const secret = process.env.CONVERTRILO_WEBHOOK_SECRET;
5
+ if (!secret) throw new Error("Set CONVERTRILO_WEBHOOK_SECRET");
6
+
7
+ function verifySignature(rawBody: Buffer, signature: string) {
8
+ const expected = createHmac("sha256", secret!).update(rawBody).digest("hex");
9
+ const expectedBuffer = Buffer.from(expected, "hex");
10
+ const signatureBuffer = Buffer.from(signature, "hex");
11
+
12
+ return (
13
+ expectedBuffer.length === signatureBuffer.length &&
14
+ timingSafeEqual(expectedBuffer, signatureBuffer)
15
+ );
16
+ }
17
+
18
+ const server = http.createServer((request, response) => {
19
+ if (request.method !== "POST" || request.url !== "/webhooks/convertrilo") {
20
+ response.writeHead(404);
21
+ response.end("Not found");
22
+ return;
23
+ }
24
+
25
+ const chunks: Buffer[] = [];
26
+
27
+ request.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
28
+ request.on("end", () => {
29
+ const rawBody = Buffer.concat(chunks);
30
+ const signature = String(request.headers["x-webhook-signature"] || "");
31
+
32
+ if (!signature || !verifySignature(rawBody, signature)) {
33
+ response.writeHead(401);
34
+ response.end("Invalid signature");
35
+ return;
36
+ }
37
+
38
+ const payload = JSON.parse(rawBody.toString("utf8"));
39
+ console.log("Verified Convertrilo webhook", {
40
+ event: payload.event,
41
+ jobId: payload.data?.jobId,
42
+ status: payload.data?.status,
43
+ externalId: payload.data?.externalId,
44
+ });
45
+
46
+ response.writeHead(204);
47
+ response.end();
48
+ });
49
+ });
50
+
51
+ const port = Number(process.env.PORT || 3000);
52
+ server.listen(port, () => {
53
+ console.log(`Webhook receiver listening on http://localhost:${port}/webhooks/convertrilo`);
54
+ });
package/openapi.yaml CHANGED
@@ -214,14 +214,70 @@ components:
214
214
  type: object
215
215
  properties:
216
216
  id: { type: string }
217
+ externalId: { type: string, nullable: true }
218
+ metadata:
219
+ type: object
220
+ additionalProperties: true
221
+ nullable: true
217
222
  status: { type: string }
223
+ ingest: { type: string, nullable: true }
224
+ output:
225
+ nullable: true
226
+ type: object
227
+ properties:
228
+ key: { type: string }
229
+ publicUrl: { type: string }
218
230
  downloadUrl: { type: string, nullable: true }
219
231
  encoder: { type: string, nullable: true }
220
232
  pct: { type: number, nullable: true }
233
+ createdAt: { type: string, format: date-time }
234
+ startedAt: { type: string, format: date-time, nullable: true }
235
+ finishedAt: { type: string, format: date-time, nullable: true }
236
+ failureMessage: { type: string, nullable: true }
221
237
  ErrorResponse:
222
238
  type: object
239
+ required: [code, message]
223
240
  properties:
241
+ code:
242
+ type: string
243
+ description: Stable machine-readable error code. Branch on this instead of parsing `message`.
244
+ enum:
245
+ - invalid_api_key
246
+ - api_key_expired
247
+ - user_not_found
248
+ - account_banned
249
+ - missing_required_scope
250
+ - invalid_request
251
+ - invalid_body
252
+ - invalid_url
253
+ - url_ingest_disabled
254
+ - url_host_blocked
255
+ - signed_url_required
256
+ - host_not_resolvable
257
+ - private_ip_blocked
258
+ - missing_content_length
259
+ - file_too_large
260
+ - unsupported_content_type
261
+ - probe_failed
262
+ - insufficient_tokens
263
+ - idempotency_conflict
264
+ - idempotency_in_progress
265
+ - not_found
266
+ - forbidden
267
+ - deprecated_dropbox
268
+ - google_drive_not_connected
269
+ - dropbox_not_connected
270
+ - no_video_files_found
271
+ - webhook_limit_exceeded
272
+ - webhook_not_found
273
+ - no_updates_provided
274
+ - output_not_available
224
275
  message: { type: string }
276
+ issues:
277
+ description: Validation details for `invalid_body`.
278
+ requiredScope: { type: string }
279
+ required: { type: number }
280
+ available: { type: number }
225
281
 
226
282
  # Bulk Jobs
227
283
  BulkCreateRequest:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@convertrilo/sdk",
3
- "version": "0.0.11",
3
+ "version": "0.0.12",
4
4
  "description": "TypeScript client for the Convertrilo video encoding API",
5
5
  "private": false,
6
6
  "license": "MIT",