@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 +2 -0
- package/dist/src/index.d.ts +13 -0
- package/dist/src/types.d.ts +27 -1
- package/docs/API-INTEGRATION-GUIDE.md +4 -0
- package/examples/idempotency.ts +60 -0
- package/examples/webhook-receiver-hmac.ts +54 -0
- package/openapi.yaml +56 -0
- package/package.json +1 -1
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.
|
package/dist/src/index.d.ts
CHANGED
|
@@ -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;
|
package/dist/src/types.d.ts
CHANGED
|
@@ -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
|
-
|
|
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:
|