@convertrilo/sdk 0.0.1

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 Convertrilo
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 ADDED
@@ -0,0 +1,176 @@
1
+ # Convertrilo TypeScript SDK
2
+
3
+ Type-safe client for the Convertrilo API.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add @convertrilo/sdk
9
+ ```
10
+
11
+ The package currently targets modern Node.js runtimes with global `fetch`. If your runtime does
12
+ not provide `fetch`, pass `fetchImpl` to the client.
13
+
14
+ ## Create A Client
15
+
16
+ ```ts
17
+ import { ConvertriloClient } from "@convertrilo/sdk";
18
+
19
+ const client = new ConvertriloClient({
20
+ baseUrl: "https://api.convertrilo.com",
21
+ apiKey: process.env.CONVERTRILO_API_KEY,
22
+ });
23
+ ```
24
+
25
+ Use an API key for server-to-server integrations. Browser apps should call your own backend,
26
+ then your backend calls Convertrilo.
27
+
28
+ ## Examples
29
+
30
+ The `examples/` directory contains starter scripts for the main integration paths:
31
+
32
+ - `node-url-to-cdn.ts` - encode a public URL and receive a signed CDN download URL
33
+ - `node-url-to-s3.ts` - encode a public URL and upload the result to S3/S3-compatible storage
34
+ - `google-drive-byo-token.ts` - upload output to Google Drive using customer OAuth tokens from your app
35
+ - `folder-ingest-s3.ts` - queue one encode job per video in an S3 prefix
36
+
37
+ Local SDK smoke tests use `.env`, but published SDK users should provide credentials through their
38
+ own server environment. Do not put Convertrilo API keys or customer storage tokens in frontend code.
39
+
40
+ ## URL Source To CDN Output
41
+
42
+ ```ts
43
+ const job = await client.onDemandEncode({
44
+ sourceUrl: "https://example.com/input.mp4",
45
+ codec: "h264",
46
+ resolution: "1080p",
47
+ quality: "better",
48
+ });
49
+
50
+ let finalStatus;
51
+ while (true) {
52
+ finalStatus = await client.onDemandStatus(job.jobId);
53
+
54
+ if (finalStatus.status === "success") break;
55
+ if (finalStatus.status === "failed") {
56
+ throw new Error(finalStatus.failureMessage || "Encoding failed");
57
+ }
58
+
59
+ await new Promise((resolve) => setTimeout(resolve, 5000));
60
+ }
61
+
62
+ console.log(finalStatus.downloadUrl);
63
+ ```
64
+
65
+ ## URL Source To S3 Output
66
+
67
+ ```ts
68
+ const job = await client.onDemandEncode({
69
+ sourceUrl: "https://example.com/input.mp4",
70
+ codec: "h264",
71
+ resolution: "1080p",
72
+ outputS3: {
73
+ bucket: "customer-output-bucket",
74
+ key: "encoded/input-1080p.mp4",
75
+ region: "us-east-1",
76
+ accessKeyId: process.env.CUSTOMER_S3_ACCESS_KEY_ID,
77
+ secretAccessKey: process.env.CUSTOMER_S3_SECRET_ACCESS_KEY,
78
+ },
79
+ });
80
+
81
+ console.log(job.jobId);
82
+ ```
83
+
84
+ For S3-compatible services, pass `endpoint` and usually `forcePathStyle: true`.
85
+
86
+ ## URL Source To Google Drive Output
87
+
88
+ For API integrations, the customer should authorize Google Drive inside your application.
89
+ Then your backend passes the resulting Google token to Convertrilo.
90
+
91
+ ```ts
92
+ const job = await client.onDemandEncode({
93
+ sourceUrl: "https://example.com/input.mp4",
94
+ codec: "h264",
95
+ resolution: "1080p",
96
+ outputGoogleDrive: {
97
+ folderId: "GOOGLE_DRIVE_FOLDER_ID",
98
+ fileName: "input-1080p.mp4",
99
+ accessToken: customerGoogleAccessToken,
100
+ refreshToken: customerGoogleRefreshToken,
101
+ },
102
+ });
103
+
104
+ console.log(job.jobId);
105
+ ```
106
+
107
+ Dashboard Google OAuth is for dashboard workflows. API integrations should use this
108
+ bring-your-own-token pattern.
109
+
110
+ ## Folder Ingest
111
+
112
+ Queue one job per video in an S3 prefix:
113
+
114
+ ```ts
115
+ const batch = await client.onDemandIngestFolder({
116
+ sourceS3: {
117
+ bucket: "customer-source-bucket",
118
+ prefix: "incoming/",
119
+ region: "us-east-1",
120
+ accessKeyId: process.env.CUSTOMER_S3_ACCESS_KEY_ID,
121
+ secretAccessKey: process.env.CUSTOMER_S3_SECRET_ACCESS_KEY,
122
+ },
123
+ outputDestination: "s3",
124
+ outputS3: {
125
+ bucket: "customer-output-bucket",
126
+ prefix: "encoded/",
127
+ region: "us-east-1",
128
+ accessKeyId: process.env.CUSTOMER_S3_ACCESS_KEY_ID,
129
+ secretAccessKey: process.env.CUSTOMER_S3_SECRET_ACCESS_KEY,
130
+ },
131
+ codec: "h264",
132
+ resolution: "1080p",
133
+ });
134
+
135
+ for (const job of batch.jobs || []) {
136
+ console.log(job.jobId, job.fileName);
137
+ }
138
+ ```
139
+
140
+ Queue one job per video in a Google Drive folder:
141
+
142
+ ```ts
143
+ const batch = await client.onDemandIngestFolder({
144
+ sourceGoogleDrive: {
145
+ folderId: "SOURCE_FOLDER_ID",
146
+ accessToken: customerGoogleAccessToken,
147
+ refreshToken: customerGoogleRefreshToken,
148
+ },
149
+ outputDestination: "google-drive",
150
+ outputGoogleDrive: {
151
+ folderId: "OUTPUT_FOLDER_ID",
152
+ accessToken: customerGoogleAccessToken,
153
+ refreshToken: customerGoogleRefreshToken,
154
+ },
155
+ codec: "h264",
156
+ resolution: "1080p",
157
+ });
158
+
159
+ for (const job of batch.jobs || []) {
160
+ console.log(job.jobId, job.fileName);
161
+ }
162
+ ```
163
+
164
+ Poll each returned `jobId` with `client.onDemandStatus(jobId)`.
165
+
166
+ ## Regenerate Types
167
+
168
+ The SDK types are generated from `openapi.yaml`.
169
+
170
+ ```bash
171
+ pnpm run generate
172
+ pnpm run build
173
+ ```
174
+
175
+ The generate script uses `--default-non-nullable false` so OpenAPI defaults remain optional
176
+ in TypeScript request bodies.
@@ -0,0 +1,194 @@
1
+ import type { paths } from "./types";
2
+ type ApproveBody = paths["/jobs/{id}/approve-unified"]["post"]["requestBody"] extends {
3
+ content: {
4
+ "application/json": infer B;
5
+ };
6
+ } ? B : undefined;
7
+ type ConfirmBody = paths["/jobs/{id}/confirm"]["post"]["requestBody"] extends {
8
+ content: {
9
+ "application/json": infer B;
10
+ };
11
+ } ? B : undefined;
12
+ export type ClientConfig = {
13
+ baseUrl: string;
14
+ getToken?: () => Promise<string> | string;
15
+ apiKey?: string;
16
+ fetchImpl?: typeof fetch;
17
+ };
18
+ export declare class ConvertriloClient {
19
+ private baseUrl;
20
+ private getToken?;
21
+ private apiKey?;
22
+ private fetchImpl;
23
+ constructor(config: ClientConfig);
24
+ private request;
25
+ login(body: paths["/auth/login"]["post"]["requestBody"]["content"]["application/json"]): Promise<paths["/auth/login"]["post"]["responses"]["200"]["content"]["application/json"]>;
26
+ getBalance(): Promise<{
27
+ available?: number;
28
+ reserved?: number;
29
+ trialGrantRemaining?: number;
30
+ trialExpiresAt?: string | null;
31
+ }>;
32
+ reserveTokens(body: paths["/tokens/reserve"]["post"]["requestBody"]["content"]["application/json"]): Promise<unknown>;
33
+ releaseTokens(body: paths["/tokens/release"]["post"]["requestBody"]["content"]["application/json"]): Promise<unknown>;
34
+ deductTokens(body: paths["/tokens/deduct"]["post"]["requestBody"]["content"]["application/json"]): Promise<unknown>;
35
+ createJob(body: paths["/jobs"]["post"]["requestBody"]["content"]["application/json"]): Promise<{
36
+ jobId: string;
37
+ upload: {
38
+ url?: string;
39
+ key?: string;
40
+ } | null;
41
+ output: {
42
+ key?: string;
43
+ publicUrl?: string;
44
+ };
45
+ estimate?: {
46
+ neu?: number;
47
+ };
48
+ }>;
49
+ probeDuration(id: string): Promise<{
50
+ durationSec?: number;
51
+ durationMinutes?: number;
52
+ }>;
53
+ approveUnified(id: string, body?: ApproveBody): Promise<{
54
+ ok?: boolean;
55
+ estimate?: {
56
+ perMinuteNeu?: number;
57
+ durationSec?: number;
58
+ totalNeu?: number;
59
+ reserved?: number;
60
+ };
61
+ }>;
62
+ confirmJob(id: string, body?: ConfirmBody): Promise<{
63
+ ok?: boolean;
64
+ }>;
65
+ cancelJob(id: string): Promise<unknown>;
66
+ jobStatus(id: string): Promise<{
67
+ id?: string;
68
+ status?: string;
69
+ downloadUrl?: string | null;
70
+ encoder?: string | null;
71
+ pct?: number | null;
72
+ }>;
73
+ createJobsBulk(body: paths["/jobs/bulk"]["post"]["requestBody"]["content"]["application/json"]): Promise<{
74
+ totalJobs?: number;
75
+ totalEstimatedNeu?: number;
76
+ jobs?: {
77
+ index?: number;
78
+ jobId?: string;
79
+ status?: string;
80
+ error?: string;
81
+ }[];
82
+ }>;
83
+ bulkStatus(ids: string[]): Promise<{
84
+ jobs?: import("./types").components["schemas"]["StatusResponse"][];
85
+ missing?: string[];
86
+ }>;
87
+ bulkConfirm(body: paths["/jobs/bulk/confirm"]["post"]["requestBody"]["content"]["application/json"]): Promise<{
88
+ queued?: number;
89
+ jobs?: Record<string, never>[];
90
+ }>;
91
+ getApiKeys(): Promise<{
92
+ keys?: import("./types").components["schemas"]["ApiKeyResponse"][];
93
+ }>;
94
+ createApiKey(body: paths["/api-keys"]["post"]["requestBody"]["content"]["application/json"]): Promise<{
95
+ id?: string;
96
+ name?: string;
97
+ key?: string;
98
+ prefix?: string;
99
+ scopes?: string[];
100
+ expiresAt?: string | null;
101
+ createdAt?: string;
102
+ }>;
103
+ revokeApiKey(id: string): Promise<unknown>;
104
+ getWebhooks(): Promise<{
105
+ webhooks?: import("./types").components["schemas"]["WebhookResponse"][];
106
+ }>;
107
+ createWebhook(body: paths["/webhooks"]["post"]["requestBody"]["content"]["application/json"]): Promise<{
108
+ id?: string;
109
+ url?: string;
110
+ events?: string[];
111
+ secret?: string;
112
+ isActive?: boolean;
113
+ createdAt?: string;
114
+ }>;
115
+ deleteWebhook(id: string): Promise<unknown>;
116
+ testWebhook(id: string): Promise<unknown>;
117
+ initStream(id: string): Promise<{
118
+ ok?: boolean;
119
+ uploadId?: string;
120
+ inputKey?: string;
121
+ }>;
122
+ uploadChunk(id: string, index: number, data: Buffer | Blob | ArrayBuffer): Promise<unknown>;
123
+ finalizeStream(id: string, body?: paths["/jobs/{id}/stream/finalize"]["post"]["requestBody"] extends {
124
+ content: {
125
+ "application/json": infer B;
126
+ };
127
+ } ? B : any): Promise<unknown>;
128
+ abortStream(id: string): Promise<unknown>;
129
+ onDemandEncode(body: paths["/ondemand/encode"]["post"]["requestBody"]["content"]["application/json"]): Promise<{
130
+ jobId: string;
131
+ status: string;
132
+ estimatedDuration?: number;
133
+ pricing?: {
134
+ basePerMinute?: number;
135
+ onDemandPerMinute?: number;
136
+ multiplier?: number;
137
+ totalNeu?: number;
138
+ reserved?: number;
139
+ };
140
+ statusUrl: string;
141
+ webhook?: string | null;
142
+ }>;
143
+ onDemandIngestFolder(body: paths["/ondemand/ingest/folder"]["post"]["requestBody"]["content"]["application/json"]): Promise<{
144
+ message?: string;
145
+ jobs?: {
146
+ jobId?: string;
147
+ fileName?: string;
148
+ }[];
149
+ }>;
150
+ onDemandStatus(jobId: string): Promise<{
151
+ jobId?: string;
152
+ status?: string;
153
+ progress?: number | null;
154
+ encoder?: string | null;
155
+ downloadUrl?: string | null;
156
+ expiresIn?: number | null;
157
+ destination?: ({
158
+ type?: "cdn";
159
+ } | {
160
+ type?: "s3";
161
+ bucket?: string;
162
+ key?: string;
163
+ } | {
164
+ type?: "google-drive";
165
+ folderId?: string;
166
+ fileName?: string | null;
167
+ }) | null;
168
+ createdAt?: string;
169
+ startedAt?: string | null;
170
+ finishedAt?: string | null;
171
+ failureMessage?: string | null;
172
+ }>;
173
+ onDemandCancel(jobId: string): Promise<{
174
+ ok: boolean;
175
+ released: number;
176
+ }>;
177
+ /**
178
+ * Helper method to encode a video and wait for completion
179
+ * @param sourceUrl - URL to the video file
180
+ * @param options - Encoding options
181
+ * @param pollInterval - Polling interval in milliseconds (default: 5000)
182
+ * @param maxAttempts - Maximum polling attempts (default: 120)
183
+ * @returns Download URL when encoding is complete
184
+ */
185
+ onDemandEncodeAndWait(sourceUrl: string, options?: {
186
+ codec?: "h264" | "h265" | "av1";
187
+ resolution?: "480p" | "720p" | "1080p" | "1440p" | "2160p";
188
+ fps?: number;
189
+ quality?: "good" | "better" | "best";
190
+ priority?: "normal" | "high";
191
+ outputExpiry?: number;
192
+ }, pollInterval?: number, maxAttempts?: number): Promise<string>;
193
+ }
194
+ export {};
@@ -0,0 +1,215 @@
1
+ export class ConvertriloClient {
2
+ baseUrl;
3
+ getToken;
4
+ apiKey;
5
+ fetchImpl;
6
+ constructor(config) {
7
+ this.baseUrl = config.baseUrl.replace(/\/$/, "");
8
+ this.getToken = config.getToken;
9
+ this.apiKey = config.apiKey;
10
+ this.fetchImpl = config.fetchImpl || fetch;
11
+ }
12
+ async request(path, init = {}) {
13
+ const headers = {
14
+ ...init.headers,
15
+ };
16
+ if (this.apiKey) {
17
+ headers["X-API-Key"] = this.apiKey;
18
+ }
19
+ else if (this.getToken) {
20
+ const token = await this.getToken();
21
+ if (token)
22
+ headers["Authorization"] = `Bearer ${token}`;
23
+ }
24
+ const hasBody = init.body !== undefined;
25
+ if (hasBody && !headers["Content-Type"]) {
26
+ headers["Content-Type"] = "application/json";
27
+ }
28
+ const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
29
+ ...init,
30
+ headers,
31
+ });
32
+ if (!res.ok) {
33
+ const text = await res.text().catch(() => "");
34
+ throw new Error(`HTTP ${res.status} ${res.statusText}: ${text}`);
35
+ }
36
+ if (res.status === 204)
37
+ return undefined;
38
+ return (await res.json());
39
+ }
40
+ // Auth
41
+ async login(body) {
42
+ return this.request(`/auth/login`, {
43
+ method: "POST",
44
+ body: JSON.stringify(body),
45
+ headers: { Authorization: "" }, // no auth
46
+ });
47
+ }
48
+ // Tokens
49
+ async getBalance() {
50
+ return this.request(`/tokens/balance`);
51
+ }
52
+ async reserveTokens(body) {
53
+ return this.request(`/tokens/reserve`, {
54
+ method: "POST",
55
+ body: JSON.stringify(body),
56
+ });
57
+ }
58
+ async releaseTokens(body) {
59
+ return this.request(`/tokens/release`, {
60
+ method: "POST",
61
+ body: JSON.stringify(body),
62
+ });
63
+ }
64
+ async deductTokens(body) {
65
+ return this.request(`/tokens/deduct`, {
66
+ method: "POST",
67
+ body: JSON.stringify(body),
68
+ });
69
+ }
70
+ // Jobs
71
+ async createJob(body) {
72
+ return this.request(`/jobs`, {
73
+ method: "POST",
74
+ body: JSON.stringify(body),
75
+ });
76
+ }
77
+ async probeDuration(id) {
78
+ return this.request(`/jobs/${id}/probe-duration`);
79
+ }
80
+ async approveUnified(id, body) {
81
+ return this.request(`/jobs/${id}/approve-unified`, {
82
+ method: "POST",
83
+ body: body ? JSON.stringify(body) : undefined,
84
+ });
85
+ }
86
+ async confirmJob(id, body) {
87
+ return this.request(`/jobs/${id}/confirm`, {
88
+ method: "POST",
89
+ body: body ? JSON.stringify(body) : undefined,
90
+ });
91
+ }
92
+ async cancelJob(id) {
93
+ return this.request(`/jobs/${id}/cancel`, { method: "POST" });
94
+ }
95
+ async jobStatus(id) {
96
+ return this.request(`/jobs/${id}/status`);
97
+ }
98
+ // Bulk Jobs
99
+ async createJobsBulk(body) {
100
+ return this.request(`/jobs/bulk`, {
101
+ method: "POST",
102
+ body: JSON.stringify(body),
103
+ });
104
+ }
105
+ async bulkStatus(ids) {
106
+ return this.request(`/jobs/bulk/status?ids=${ids.join(",")}`);
107
+ }
108
+ async bulkConfirm(body) {
109
+ return this.request(`/jobs/bulk/confirm`, {
110
+ method: "POST",
111
+ body: JSON.stringify(body),
112
+ });
113
+ }
114
+ // API Keys
115
+ async getApiKeys() {
116
+ return this.request(`/api-keys`);
117
+ }
118
+ async createApiKey(body) {
119
+ return this.request(`/api-keys`, {
120
+ method: "POST",
121
+ body: JSON.stringify(body),
122
+ });
123
+ }
124
+ async revokeApiKey(id) {
125
+ return this.request(`/api-keys/${id}`, { method: "DELETE" });
126
+ }
127
+ // Webhooks
128
+ async getWebhooks() {
129
+ return this.request(`/webhooks`);
130
+ }
131
+ async createWebhook(body) {
132
+ return this.request(`/webhooks`, {
133
+ method: "POST",
134
+ body: JSON.stringify(body),
135
+ });
136
+ }
137
+ async deleteWebhook(id) {
138
+ return this.request(`/webhooks/${id}`, { method: "DELETE" });
139
+ }
140
+ async testWebhook(id) {
141
+ return this.request(`/webhooks/${id}/test`, { method: "POST" });
142
+ }
143
+ // Streaming
144
+ async initStream(id) {
145
+ return this.request(`/jobs/${id}/stream/init`, { method: "POST" });
146
+ }
147
+ async uploadChunk(id, index, data) {
148
+ return this.request(`/jobs/${id}/stream/chunk/${index}`, {
149
+ method: "PUT",
150
+ body: data,
151
+ headers: { "Content-Type": "application/octet-stream" },
152
+ });
153
+ }
154
+ async finalizeStream(id, body) {
155
+ return this.request(`/jobs/${id}/stream/finalize`, {
156
+ method: "POST",
157
+ body: body ? JSON.stringify(body) : undefined,
158
+ });
159
+ }
160
+ async abortStream(id) {
161
+ return this.request(`/jobs/${id}/stream/abort`, { method: "POST" });
162
+ }
163
+ // On-Demand Encoding
164
+ async onDemandEncode(body) {
165
+ return this.request(`/ondemand/encode`, {
166
+ method: "POST",
167
+ body: JSON.stringify(body),
168
+ });
169
+ }
170
+ async onDemandIngestFolder(body) {
171
+ return this.request(`/ondemand/ingest/folder`, {
172
+ method: "POST",
173
+ body: JSON.stringify(body),
174
+ });
175
+ }
176
+ async onDemandStatus(jobId) {
177
+ return this.request(`/ondemand/status/${jobId}`);
178
+ }
179
+ async onDemandCancel(jobId) {
180
+ return this.request(`/ondemand/${jobId}`, { method: "DELETE" });
181
+ }
182
+ /**
183
+ * Helper method to encode a video and wait for completion
184
+ * @param sourceUrl - URL to the video file
185
+ * @param options - Encoding options
186
+ * @param pollInterval - Polling interval in milliseconds (default: 5000)
187
+ * @param maxAttempts - Maximum polling attempts (default: 120)
188
+ * @returns Download URL when encoding is complete
189
+ */
190
+ async onDemandEncodeAndWait(sourceUrl, options = {}, pollInterval = 5000, maxAttempts = 120) {
191
+ // Submit for encoding
192
+ const job = await this.onDemandEncode({
193
+ sourceUrl,
194
+ ...options,
195
+ });
196
+ // Poll for completion
197
+ let attempts = 0;
198
+ while (attempts < maxAttempts) {
199
+ const status = await this.onDemandStatus(job.jobId);
200
+ if (status.status === "success" && status.downloadUrl) {
201
+ return status.downloadUrl;
202
+ }
203
+ if (status.status === "failed") {
204
+ throw new Error(status.failureMessage || "Encoding failed");
205
+ }
206
+ if (status.status === "canceled") {
207
+ throw new Error("Job was canceled");
208
+ }
209
+ // Wait before next poll
210
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
211
+ attempts++;
212
+ }
213
+ throw new Error("Polling timeout: job did not complete in time");
214
+ }
215
+ }