@videxo/sdk 0.0.0-stage → 0.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.
@@ -0,0 +1,217 @@
1
+ import { type Client } from 'openapi-fetch';
2
+ import { DEFAULT_BASE_URL, DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT_MS, DEFAULT_WAIT } from './defaults.ts';
3
+ import type { paths } from './generated/openapi.ts';
4
+ import { type RetryPolicy, type Transport } from './transport.ts';
5
+ import type { AudioRequest, ClientToken, CompileRequest, CompileResult, CompletedUpload, ConcatRequest, CreateClientTokenRequest, CreateUploadRequest, GifRequest, Job, JobList, JobRequest, ListJobsParams, FontList, PresetList, ProbeRequest, ProbeResponse, ScenesRequest, SlideshowRequest, StoryboardRequest, ThumbnailsRequest, TranscodeRequest, Upload, WebhookEvent } from './types.ts';
6
+ import { type UploadData } from './uploads.ts';
7
+ import { type VerifyWebhookOptions, type WebhookHeaders } from './webhooks.ts';
8
+ export { DEFAULT_BASE_URL, DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT_MS, DEFAULT_WAIT };
9
+ export type VidexoOptions = {
10
+ /**
11
+ * An API key from the dashboard (`vdx_...`). Keep it on a server, never in browser code. Typed to
12
+ * take an unset environment variable (`process.env.VIDEXO_API_KEY`): the constructor throws then.
13
+ */
14
+ apiKey: string | undefined;
15
+ /** `https://api.videxo.io` by default. */
16
+ baseUrl?: string;
17
+ /** The `fetch` to send requests with; the runtime's own by default. */
18
+ fetch?(input: Request, init?: RequestInit): Promise<Response>;
19
+ /** Retries of a failed request (429, 502, 503, 504, a timeout, a network error); 2 by default. */
20
+ maxRetries?: number;
21
+ /** Per attempt, until the whole answer arrived (a download: its headers); 60 seconds by default. */
22
+ timeoutMs?: number;
23
+ };
24
+ export type RequestOptions = {
25
+ signal?: AbortSignal;
26
+ /** Overrides the client's `timeoutMs` for this call. */
27
+ timeoutMs?: number;
28
+ };
29
+ export type CreateOptions = RequestOptions & {
30
+ /**
31
+ * Makes the creation safe to retry for 24 hours. The SDK sends a new random key when you leave it
32
+ * out, and reuses it on its own retries; pass your own (an order id) to also cover your retries.
33
+ */
34
+ idempotencyKey?: string;
35
+ };
36
+ export type WaitOptions = {
37
+ /** How long to wait for the job to end; 10 minutes by default. */
38
+ timeoutMs?: number;
39
+ /** Between two reads of the job; 2 seconds by default. */
40
+ intervalMs?: number;
41
+ signal?: AbortSignal;
42
+ };
43
+ export type UploadOptions = RequestOptions & {
44
+ /** A plain file name: letters, digits, dots, underscores and hyphens. */
45
+ filename: string;
46
+ /** The type a job sees; the blob's own type or `application/octet-stream` by default. */
47
+ contentType?: string;
48
+ /** The exact byte length; needed for a stream, read from the data otherwise. */
49
+ size?: number;
50
+ /** For sending the bytes; no timeout by default, as a large file may take a while. */
51
+ uploadTimeoutMs?: number;
52
+ };
53
+ export type ClientTokenForUploadOptions = RequestOptions & {
54
+ /** Your own opaque id of the end user. */
55
+ subject: string;
56
+ /** The largest file the app may upload, in bytes. */
57
+ maxSizeBytes: number;
58
+ /** Media types the upload may declare, exact or `type/*`; video, audio and images by default. */
59
+ contentTypes?: string[];
60
+ /** Lifetime in seconds, 60 to 21600; 900 by default. */
61
+ ttlSeconds?: number;
62
+ };
63
+ export type ClientTokenForJobOptions = RequestOptions & {
64
+ /** Your own opaque id of the end user; a continuation keeps its chain's. */
65
+ subject?: string;
66
+ /** The app's token id: the new token continues its chain and adds the job. */
67
+ continues?: string;
68
+ /** Lifetime in seconds, 60 to 21600; 900 by default. */
69
+ ttlSeconds?: number;
70
+ };
71
+ type Result<T> = {
72
+ data?: T;
73
+ error?: unknown;
74
+ response: Response;
75
+ };
76
+ /**
77
+ * The Videxo API client. One instance per API key; it holds no connection and is cheap to create.
78
+ *
79
+ * ```ts
80
+ * const videxo = new Videxo({ apiKey: process.env.VIDEXO_API_KEY });
81
+ * const job = await videxo.transcode({ input: 'https://example.com/talk.mp4', preset: 'web-720p' });
82
+ * const done = await videxo.jobs.wait(job.id);
83
+ * ```
84
+ */
85
+ export declare class Videxo {
86
+ readonly baseUrl: string;
87
+ readonly jobs: Jobs;
88
+ readonly presets: Presets;
89
+ readonly fonts: Fonts;
90
+ readonly uploads: Uploads;
91
+ readonly outputs: Outputs;
92
+ readonly webhooks: Webhooks;
93
+ readonly clientTokens: ClientTokens;
94
+ /** @internal */
95
+ readonly transport: Transport;
96
+ /** @internal */
97
+ readonly client: Client<paths>;
98
+ constructor(options: VidexoOptions);
99
+ /** @internal The per-request `fetch` that openapi-fetch calls: the transport with this call's policy. */
100
+ fetchFor(retry: RetryPolicy, options?: RequestOptions): (request: Request) => Promise<Response>;
101
+ /** @internal The data of a 2xx answer, or the error of any other. */
102
+ unwrap<T>(pending: Promise<Result<T>>): Promise<T>;
103
+ /** @internal A job creation at `path`, with an Idempotency-Key that its retries reuse. */
104
+ createAt<T = Job>(path: CreatePath, body: unknown, options?: CreateOptions): Promise<T>;
105
+ /** `POST /v1/transcode`: one video output from `input`, from a preset or your own options. */
106
+ transcode(body: TranscodeRequest, options?: CreateOptions): Promise<Job>;
107
+ /** `POST /v1/thumbnails`: one image at `at`, or one per time in `times`. */
108
+ thumbnails(body: ThumbnailsRequest, options?: CreateOptions): Promise<Job>;
109
+ /** `POST /v1/storyboard`: frames at a fixed interval in a grid on one image. */
110
+ storyboard(body: StoryboardRequest, options?: CreateOptions): Promise<Job>;
111
+ /** `POST /v1/gif`: an animated GIF with its own palette. */
112
+ gif(body: GifRequest, options?: CreateOptions): Promise<Job>;
113
+ /** `POST /v1/audio`: the sound of `input` as an audio file. */
114
+ audio(body: AudioRequest, options?: CreateOptions): Promise<Job>;
115
+ /** `POST /v1/concat`: the `inputs` one after another, as one file. */
116
+ concat(body: ConcatRequest, options?: CreateOptions): Promise<Job>;
117
+ /** `POST /v1/slideshow`: the still `images` as one video, each held for a time, with an optional motion. */
118
+ slideshow(body: SlideshowRequest, options?: CreateOptions): Promise<Job>;
119
+ /**
120
+ * `POST /v1/probe`: the container, duration and streams of `input`, read with ffprobe. The api waits
121
+ * up to 25 seconds; a job still running then is waited for here (`wait`), and its document read from
122
+ * its output file. A job that failed is answered, not thrown: `probe` is null and `error` says why.
123
+ */
124
+ probe(body: ProbeRequest, options?: CreateOptions & {
125
+ wait?: Omit<WaitOptions, 'signal'>;
126
+ }): Promise<ProbeResponse>;
127
+ /** `POST /v1/scenes`: the scenes of `input`, with their times and scores, as a JSON file. */
128
+ scenes(body: ScenesRequest, options?: CreateOptions): Promise<Job>;
129
+ }
130
+ type CreatePath = '/v1/jobs' | '/v1/transcode' | '/v1/thumbnails' | '/v1/storyboard' | '/v1/gif' | '/v1/audio' | '/v1/concat' | '/v1/slideshow' | '/v1/probe' | '/v1/scenes';
131
+ export declare class Jobs {
132
+ #private;
133
+ constructor(videxo: Videxo);
134
+ /** `POST /v1/jobs`: a command body (`command`, `input_files`, `output_files`) or an operations body (`inputs`, `outputs`). */
135
+ create(body: JobRequest, options?: CreateOptions): Promise<Job>;
136
+ /** `GET /v1/jobs/{id}`. */
137
+ get(id: string, options?: RequestOptions): Promise<Job>;
138
+ /** `GET /v1/jobs`: one page, newest first; pass its `next_cursor` as `cursor` for the next. */
139
+ page(params?: ListJobsParams, options?: RequestOptions): Promise<JobList>;
140
+ /** Every job, newest first, reading page after page as you iterate (`for await (const job of videxo.jobs.list())`). */
141
+ list(params?: ListJobsParams, options?: RequestOptions): AsyncGenerator<Job, void, undefined>;
142
+ /** `POST /v1/jobs/{id}/cancel`: a queued or running job becomes cancelled; an ended one is answered as it is. */
143
+ cancel(id: string, options?: RequestOptions): Promise<Job>;
144
+ /** `DELETE /v1/jobs/{id}/files`: deletes an ended job's stored outputs now. */
145
+ deleteFiles(id: string, options?: RequestOptions): Promise<Job>;
146
+ /** `POST /v1/jobs/compile`: the spec, command and `ffmpeg_command` a job would get; no job is made. */
147
+ compile(body: CompileRequest, options?: RequestOptions): Promise<CompileResult>;
148
+ /**
149
+ * Reads the job until it is `succeeded`, `failed` or `cancelled` and answers it then (a failed job
150
+ * is answered, not thrown: see `job.error`). Waits out a 429 by its `Retry-After`. Throws
151
+ * `wait_timeout` when the job has not ended within `timeoutMs`, `aborted` when `signal` fires.
152
+ */
153
+ wait(id: string, options?: WaitOptions): Promise<Job>;
154
+ }
155
+ export declare class Presets {
156
+ #private;
157
+ constructor(videxo: Videxo);
158
+ /** `GET /v1/presets`: every preset with its type, its full options and whether it is deprecated. */
159
+ list(options?: RequestOptions): Promise<PresetList>;
160
+ }
161
+ export declare class Fonts {
162
+ #private;
163
+ constructor(videxo: Videxo);
164
+ /** `GET /v1/fonts`: the fonts a text layer can name, with their family, weight, scripts and licence. */
165
+ list(options?: RequestOptions): Promise<FontList>;
166
+ }
167
+ export declare class Uploads {
168
+ #private;
169
+ constructor(videxo: Videxo);
170
+ /**
171
+ * `POST /v1/uploads`: a one-time upload URL for a private input. The route takes no
172
+ * Idempotency-Key, so only a 429 is retried.
173
+ */
174
+ create(params: CreateUploadRequest, options?: RequestOptions): Promise<Upload>;
175
+ /**
176
+ * Creates an upload and sends the bytes to its URL; answers the completed upload, whose
177
+ * `input_url` (`videxo://uploads/<id>`) a job takes as an input.
178
+ */
179
+ upload(data: UploadData, options: UploadOptions): Promise<CompletedUpload>;
180
+ }
181
+ export declare class Outputs {
182
+ #private;
183
+ constructor(videxo: Videxo);
184
+ /**
185
+ * A fresh signed download URL of one output (`GET /v1/jobs/{id}/outputs/{alias}`). `output` is its
186
+ * alias (`out_video`), or its name or file name when only one file of the job has it.
187
+ */
188
+ url(jobId: string, output: string, options?: RequestOptions): Promise<string>;
189
+ /**
190
+ * The output's bytes as a `Response` (stream `response.body`, or read it with `arrayBuffer()`),
191
+ * fetched from a fresh signed URL. The API key is not sent to the file URL.
192
+ */
193
+ download(jobId: string, output: string, options?: RequestOptions): Promise<Response>;
194
+ }
195
+ /**
196
+ * Client tokens for apps and web pages (ADR 016): short-lived, narrowly scoped, minted here on your
197
+ * server. Hand the answer's `token` and `id` to the app, which uses `@videxo/sdk/client`. Minting
198
+ * supersedes nothing, so a lost answer is retried by minting again.
199
+ */
200
+ export declare class ClientTokens {
201
+ #private;
202
+ constructor(videxo: Videxo);
203
+ /** `POST /v1/client-tokens`: a token with the grant you describe; `continues` refreshes a chain. */
204
+ create(grant: CreateClientTokenRequest, options?: RequestOptions): Promise<ClientToken>;
205
+ /** A token for one upload by `subject`: what an app needs before it knows of any job. */
206
+ forUpload({ subject, maxSizeBytes, contentTypes, ttlSeconds, ...options }: ClientTokenForUploadOptions): Promise<ClientToken>;
207
+ /** A token that reads one job: a new one, or with `continues` the next of the app's chain, adding the job. */
208
+ forJob(jobId: string, { subject, continues, ttlSeconds, ...options }?: ClientTokenForJobOptions): Promise<ClientToken>;
209
+ /** `DELETE /v1/client-tokens/{id}`: ends the token and its whole chain. Revoking again is fine. */
210
+ revoke(id: string, options?: RequestOptions): Promise<void>;
211
+ /** `POST /v1/client-tokens/revoke`: ends every chain of the end user; answers how many tokens. */
212
+ revokeSubject(subject: string, options?: RequestOptions): Promise<number>;
213
+ }
214
+ export declare class Webhooks {
215
+ /** Checks a delivery's Standard Webhooks signature and answers its event (`verifyWebhook`). */
216
+ verify(body: string | Uint8Array | ArrayBuffer, headers: WebhookHeaders, secret: string, options?: VerifyWebhookOptions): Promise<WebhookEvent>;
217
+ }
package/dist/client.js ADDED
@@ -0,0 +1,403 @@
1
+ import createClient, {} from 'openapi-fetch';
2
+ import { DEFAULT_BASE_URL, DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT_MS, DEFAULT_WAIT } from "./defaults.js";
3
+ import { VidexoError, errorFromResponse, sdkError } from "./errors.js";
4
+ import { abortableSleep, redacted, runtimeFetch, send } from "./transport.js";
5
+ import { sendUpload, sizeOf } from "./uploads.js";
6
+ import { verifyWebhook } from "./webhooks.js";
7
+ export { DEFAULT_BASE_URL, DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT_MS, DEFAULT_WAIT };
8
+ const TERMINAL = new Set(['succeeded', 'failed', 'cancelled']);
9
+ const OUTPUT_ALIAS = /^out_[A-Za-z0-9_]{1,48}$/;
10
+ /** What an API key may hold: printable ASCII without spaces (the `Authorization` header carries it as is). */
11
+ const API_KEY_PATTERN = /^[\x21-\x7e]+$/;
12
+ /**
13
+ * The Videxo API client. One instance per API key; it holds no connection and is cheap to create.
14
+ *
15
+ * ```ts
16
+ * const videxo = new Videxo({ apiKey: process.env.VIDEXO_API_KEY });
17
+ * const job = await videxo.transcode({ input: 'https://example.com/talk.mp4', preset: 'web-720p' });
18
+ * const done = await videxo.jobs.wait(job.id);
19
+ * ```
20
+ */
21
+ export class Videxo {
22
+ baseUrl;
23
+ jobs;
24
+ presets;
25
+ fonts;
26
+ uploads;
27
+ outputs;
28
+ webhooks;
29
+ clientTokens;
30
+ /** @internal */
31
+ transport;
32
+ /** @internal */
33
+ client;
34
+ constructor(options) {
35
+ const apiKey = typeof options?.apiKey === 'string' ? options.apiKey.trim() : '';
36
+ if (!apiKey)
37
+ throw new TypeError('new Videxo() needs an apiKey: an API key from the dashboard, kept on your server.');
38
+ // Checked here so that no runtime refuses the Authorization header later with the key in its message.
39
+ if (!API_KEY_PATTERN.test(apiKey))
40
+ throw new TypeError('The apiKey is not valid: an API key has only printable ASCII characters, without spaces or line breaks.');
41
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
42
+ if (!/^https?:\/\//.test(baseUrl))
43
+ throw new TypeError('baseUrl must be an http or https URL.');
44
+ const userFetch = options.fetch;
45
+ this.baseUrl = baseUrl;
46
+ this.transport = {
47
+ fetch: userFetch ? (input, init) => userFetch(input, init) : runtimeFetch,
48
+ maxRetries: Math.max(0, Math.floor(options.maxRetries ?? DEFAULT_MAX_RETRIES)),
49
+ timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
50
+ redact: (text) => text.replaceAll(apiKey, '[api key]'),
51
+ };
52
+ this.client = createClient({ baseUrl, headers: { Authorization: `Bearer ${apiKey}` } });
53
+ this.jobs = new Jobs(this);
54
+ this.presets = new Presets(this);
55
+ this.fonts = new Fonts(this);
56
+ this.uploads = new Uploads(this);
57
+ this.outputs = new Outputs(this);
58
+ this.webhooks = new Webhooks();
59
+ this.clientTokens = new ClientTokens(this);
60
+ }
61
+ /** @internal The per-request `fetch` that openapi-fetch calls: the transport with this call's policy. */
62
+ fetchFor(retry, options = {}) {
63
+ return (request) => send(this.transport, request, { retry, signal: options.signal, timeoutMs: options.timeoutMs, buffer: true });
64
+ }
65
+ /** @internal The data of a 2xx answer, or the error of any other. */
66
+ async unwrap(pending) {
67
+ let result;
68
+ try {
69
+ result = await pending;
70
+ }
71
+ catch (error) {
72
+ if (error instanceof SyntaxError)
73
+ throw sdkError('invalid_response', 'The API answered with a body that is not JSON.', { cause: error });
74
+ throw redacted(this.transport, error);
75
+ }
76
+ if (!result.response.ok)
77
+ throw errorFromResponse(result.response, result.error);
78
+ if (result.data === undefined)
79
+ throw sdkError('invalid_response', 'The API answered without a body.', { status: result.response.status });
80
+ return result.data;
81
+ }
82
+ /** @internal A job creation at `path`, with an Idempotency-Key that its retries reuse. */
83
+ createAt(path, body, options = {}) {
84
+ const key = options.idempotencyKey ?? crypto.randomUUID();
85
+ return this.unwrap(this.client.POST(path, {
86
+ body: body,
87
+ params: { header: { 'idempotency-key': key } },
88
+ fetch: this.fetchFor('safe', options),
89
+ }));
90
+ }
91
+ /** `POST /v1/transcode`: one video output from `input`, from a preset or your own options. */
92
+ transcode(body, options) {
93
+ return this.createAt('/v1/transcode', body, options);
94
+ }
95
+ /** `POST /v1/thumbnails`: one image at `at`, or one per time in `times`. */
96
+ thumbnails(body, options) {
97
+ return this.createAt('/v1/thumbnails', body, options);
98
+ }
99
+ /** `POST /v1/storyboard`: frames at a fixed interval in a grid on one image. */
100
+ storyboard(body, options) {
101
+ return this.createAt('/v1/storyboard', body, options);
102
+ }
103
+ /** `POST /v1/gif`: an animated GIF with its own palette. */
104
+ gif(body, options) {
105
+ return this.createAt('/v1/gif', body, options);
106
+ }
107
+ /** `POST /v1/audio`: the sound of `input` as an audio file. */
108
+ audio(body, options) {
109
+ return this.createAt('/v1/audio', body, options);
110
+ }
111
+ /** `POST /v1/concat`: the `inputs` one after another, as one file. */
112
+ concat(body, options) {
113
+ return this.createAt('/v1/concat', body, options);
114
+ }
115
+ /** `POST /v1/slideshow`: the still `images` as one video, each held for a time, with an optional motion. */
116
+ slideshow(body, options) {
117
+ return this.createAt('/v1/slideshow', body, options);
118
+ }
119
+ /**
120
+ * `POST /v1/probe`: the container, duration and streams of `input`, read with ffprobe. The api waits
121
+ * up to 25 seconds; a job still running then is waited for here (`wait`), and its document read from
122
+ * its output file. A job that failed is answered, not thrown: `probe` is null and `error` says why.
123
+ */
124
+ async probe(body, options = {}) {
125
+ const answer = await this.createAt('/v1/probe', body, options);
126
+ if (TERMINAL.has(answer.state))
127
+ return answer;
128
+ const job = await this.jobs.wait(answer.id, { ...options.wait, signal: options.signal });
129
+ if (job.state !== 'succeeded')
130
+ return { ...job, probe: null };
131
+ const file = await this.outputs.download(job.id, 'out_metadata', { signal: options.signal });
132
+ return { ...job, probe: (await file.json()) };
133
+ }
134
+ /** `POST /v1/scenes`: the scenes of `input`, with their times and scores, as a JSON file. */
135
+ scenes(body, options) {
136
+ return this.createAt('/v1/scenes', body, options);
137
+ }
138
+ }
139
+ export class Jobs {
140
+ #videxo;
141
+ constructor(videxo) {
142
+ this.#videxo = videxo;
143
+ }
144
+ /** `POST /v1/jobs`: a command body (`command`, `input_files`, `output_files`) or an operations body (`inputs`, `outputs`). */
145
+ create(body, options) {
146
+ return this.#videxo.createAt('/v1/jobs', body, options);
147
+ }
148
+ /** `GET /v1/jobs/{id}`. */
149
+ get(id, options = {}) {
150
+ const videxo = this.#videxo;
151
+ return videxo.unwrap(videxo.client.GET('/v1/jobs/{id}', { params: { path: { id } }, fetch: videxo.fetchFor('safe', options) }));
152
+ }
153
+ /** `GET /v1/jobs`: one page, newest first; pass its `next_cursor` as `cursor` for the next. */
154
+ page(params = {}, options = {}) {
155
+ const videxo = this.#videxo;
156
+ return videxo.unwrap(videxo.client.GET('/v1/jobs', { params: { query: params }, fetch: videxo.fetchFor('safe', options) }));
157
+ }
158
+ /** Every job, newest first, reading page after page as you iterate (`for await (const job of videxo.jobs.list())`). */
159
+ async *list(params = {}, options = {}) {
160
+ let cursor = params.cursor;
161
+ for (;;) {
162
+ const page = await this.page({ ...params, ...(cursor === undefined ? {} : { cursor }) }, options);
163
+ yield* page.data;
164
+ if (!page.next_cursor)
165
+ return;
166
+ cursor = page.next_cursor;
167
+ }
168
+ }
169
+ /** `POST /v1/jobs/{id}/cancel`: a queued or running job becomes cancelled; an ended one is answered as it is. */
170
+ cancel(id, options = {}) {
171
+ const videxo = this.#videxo;
172
+ return videxo.unwrap(videxo.client.POST('/v1/jobs/{id}/cancel', { params: { path: { id } }, fetch: videxo.fetchFor('safe', options) }));
173
+ }
174
+ /** `DELETE /v1/jobs/{id}/files`: deletes an ended job's stored outputs now. */
175
+ deleteFiles(id, options = {}) {
176
+ const videxo = this.#videxo;
177
+ return videxo.unwrap(videxo.client.DELETE('/v1/jobs/{id}/files', { params: { path: { id } }, fetch: videxo.fetchFor('safe', options) }));
178
+ }
179
+ /** `POST /v1/jobs/compile`: the spec, command and `ffmpeg_command` a job would get; no job is made. */
180
+ compile(body, options = {}) {
181
+ const videxo = this.#videxo;
182
+ return videxo.unwrap(videxo.client.POST('/v1/jobs/compile', { body, fetch: videxo.fetchFor('safe', options) }));
183
+ }
184
+ /**
185
+ * Reads the job until it is `succeeded`, `failed` or `cancelled` and answers it then (a failed job
186
+ * is answered, not thrown: see `job.error`). Waits out a 429 by its `Retry-After`. Throws
187
+ * `wait_timeout` when the job has not ended within `timeoutMs`, `aborted` when `signal` fires.
188
+ */
189
+ async wait(id, options = {}) {
190
+ const timeoutMs = options.timeoutMs ?? DEFAULT_WAIT.timeoutMs;
191
+ const intervalMs = options.intervalMs ?? DEFAULT_WAIT.intervalMs;
192
+ const sleep = this.#videxo.transport.sleep ?? abortableSleep;
193
+ // The deadline also stops a read in flight and the retry pauses inside it (a 429's Retry-After).
194
+ const deadline = new AbortController();
195
+ const onAbort = () => deadline.abort(options.signal?.reason);
196
+ if (options.signal?.aborted)
197
+ onAbort();
198
+ options.signal?.addEventListener('abort', onAbort, { once: true });
199
+ const timer = setTimeout(() => deadline.abort(), timeoutMs);
200
+ let job;
201
+ const timedOut = () => sdkError('wait_timeout', `Job ${id} did not end within ${timeoutMs} ms.`, { body: job });
202
+ try {
203
+ for (;;) {
204
+ let pause = intervalMs;
205
+ try {
206
+ job = await this.get(id, { signal: deadline.signal });
207
+ if (TERMINAL.has(job.state))
208
+ return job;
209
+ }
210
+ catch (error) {
211
+ if (!(error instanceof VidexoError && error.status === 429))
212
+ throw error;
213
+ pause = Math.max(intervalMs, (error.retryAfter ?? 0) * 1000);
214
+ }
215
+ if (deadline.signal.aborted)
216
+ throw timedOut();
217
+ await sleep(pause, deadline.signal);
218
+ }
219
+ }
220
+ catch (error) {
221
+ if (error instanceof VidexoError && error.code === 'aborted' && !options.signal?.aborted)
222
+ throw timedOut();
223
+ throw error;
224
+ }
225
+ finally {
226
+ clearTimeout(timer);
227
+ options.signal?.removeEventListener('abort', onAbort);
228
+ }
229
+ }
230
+ }
231
+ export class Presets {
232
+ #videxo;
233
+ constructor(videxo) {
234
+ this.#videxo = videxo;
235
+ }
236
+ /** `GET /v1/presets`: every preset with its type, its full options and whether it is deprecated. */
237
+ list(options = {}) {
238
+ const videxo = this.#videxo;
239
+ return videxo.unwrap(videxo.client.GET('/v1/presets', { fetch: videxo.fetchFor('safe', options) }));
240
+ }
241
+ }
242
+ export class Fonts {
243
+ #videxo;
244
+ constructor(videxo) {
245
+ this.#videxo = videxo;
246
+ }
247
+ /** `GET /v1/fonts`: the fonts a text layer can name, with their family, weight, scripts and licence. */
248
+ list(options = {}) {
249
+ const videxo = this.#videxo;
250
+ return videxo.unwrap(videxo.client.GET('/v1/fonts', { fetch: videxo.fetchFor('safe', options) }));
251
+ }
252
+ }
253
+ export class Uploads {
254
+ #videxo;
255
+ constructor(videxo) {
256
+ this.#videxo = videxo;
257
+ }
258
+ /**
259
+ * `POST /v1/uploads`: a one-time upload URL for a private input. The route takes no
260
+ * Idempotency-Key, so only a 429 is retried.
261
+ */
262
+ create(params, options = {}) {
263
+ const videxo = this.#videxo;
264
+ return videxo.unwrap(videxo.client.POST('/v1/uploads', { body: params, fetch: videxo.fetchFor('rate_limit', options) }));
265
+ }
266
+ /**
267
+ * Creates an upload and sends the bytes to its URL; answers the completed upload, whose
268
+ * `input_url` (`videxo://uploads/<id>`) a job takes as an input.
269
+ */
270
+ async upload(data, options) {
271
+ const size = options.size ?? sizeOf(data);
272
+ if (size === null)
273
+ throw new TypeError('uploads.upload() needs options.size for a stream.');
274
+ const blobType = typeof Blob !== 'undefined' && data instanceof Blob ? data.type : '';
275
+ const contentType = options.contentType ?? (blobType || 'application/octet-stream');
276
+ const upload = await this.create({ filename: options.filename, size_bytes: size, content_type: contentType }, options);
277
+ return sendUpload(upload.upload_url, data, {
278
+ contentType,
279
+ fetch: this.#videxo.transport.fetch,
280
+ ...(options.signal ? { signal: options.signal } : {}),
281
+ ...(options.uploadTimeoutMs === undefined ? {} : { timeoutMs: options.uploadTimeoutMs }),
282
+ });
283
+ }
284
+ }
285
+ export class Outputs {
286
+ #videxo;
287
+ constructor(videxo) {
288
+ this.#videxo = videxo;
289
+ }
290
+ /**
291
+ * A fresh signed download URL of one output (`GET /v1/jobs/{id}/outputs/{alias}`). `output` is its
292
+ * alias (`out_video`), or its name or file name when only one file of the job has it.
293
+ */
294
+ async url(jobId, output, options = {}) {
295
+ const videxo = this.#videxo;
296
+ const alias = await this.#alias(jobId, output, options);
297
+ const pending = videxo.client.GET('/v1/jobs/{id}/outputs/{alias}', {
298
+ params: { path: { id: jobId, alias } },
299
+ redirect: 'manual',
300
+ parseAs: 'text',
301
+ fetch: videxo.fetchFor('safe', options),
302
+ });
303
+ const { response, error } = await pending.catch((failure) => {
304
+ throw redacted(videxo.transport, failure);
305
+ });
306
+ const location = response.headers.get('location');
307
+ if (response.status >= 300 && response.status < 400 && location)
308
+ return new URL(location, videxo.baseUrl).href;
309
+ if (!response.ok && response.type !== 'opaqueredirect')
310
+ throw errorFromResponse(response, error);
311
+ throw sdkError('invalid_response', 'The API did not answer with the download URL.', { status: response.status });
312
+ }
313
+ /**
314
+ * The output's bytes as a `Response` (stream `response.body`, or read it with `arrayBuffer()`),
315
+ * fetched from a fresh signed URL. The API key is not sent to the file URL.
316
+ */
317
+ async download(jobId, output, options = {}) {
318
+ const url = await this.url(jobId, output, options);
319
+ const response = await send(this.#videxo.transport, new Request(url), {
320
+ retry: 'safe',
321
+ signal: options.signal,
322
+ timeoutMs: options.timeoutMs,
323
+ buffer: false,
324
+ });
325
+ if (response.ok)
326
+ return response;
327
+ const text = await response.text();
328
+ let body = text;
329
+ try {
330
+ body = JSON.parse(text);
331
+ }
332
+ catch {
333
+ // Not JSON: kept as text.
334
+ }
335
+ throw errorFromResponse(response, body);
336
+ }
337
+ async #alias(jobId, output, options) {
338
+ if (OUTPUT_ALIAS.test(output))
339
+ return output;
340
+ const job = await this.#videxo.jobs.get(jobId, options);
341
+ const aliases = [...new Set(job.outputs.filter((file) => file.name === output || file.filename === output).map((file) => file.alias))];
342
+ if (aliases.length === 1)
343
+ return aliases[0];
344
+ if (aliases.length === 0)
345
+ throw sdkError('output_not_found', `Job ${jobId} has no output named ${output}.`);
346
+ throw sdkError('output_ambiguous', `Several files of job ${jobId} are named ${output}: ${aliases.join(', ')}.`);
347
+ }
348
+ }
349
+ /**
350
+ * Client tokens for apps and web pages (ADR 016): short-lived, narrowly scoped, minted here on your
351
+ * server. Hand the answer's `token` and `id` to the app, which uses `@videxo/sdk/client`. Minting
352
+ * supersedes nothing, so a lost answer is retried by minting again.
353
+ */
354
+ export class ClientTokens {
355
+ #videxo;
356
+ constructor(videxo) {
357
+ this.#videxo = videxo;
358
+ }
359
+ /** `POST /v1/client-tokens`: a token with the grant you describe; `continues` refreshes a chain. */
360
+ create(grant, options = {}) {
361
+ const videxo = this.#videxo;
362
+ return videxo.unwrap(videxo.client.POST('/v1/client-tokens', { body: grant, fetch: videxo.fetchFor('safe', options) }));
363
+ }
364
+ /** A token for one upload by `subject`: what an app needs before it knows of any job. */
365
+ forUpload({ subject, maxSizeBytes, contentTypes, ttlSeconds, ...options }) {
366
+ return this.create({
367
+ subject,
368
+ uploads: { max_count: 1, max_size_bytes: maxSizeBytes, ...(contentTypes ? { content_types: contentTypes } : {}) },
369
+ ...(ttlSeconds === undefined ? {} : { ttl_seconds: ttlSeconds }),
370
+ }, options);
371
+ }
372
+ /** A token that reads one job: a new one, or with `continues` the next of the app's chain, adding the job. */
373
+ forJob(jobId, { subject, continues, ttlSeconds, ...options } = {}) {
374
+ return this.create({
375
+ jobs: { read: [jobId] },
376
+ ...(subject === undefined ? {} : { subject }),
377
+ ...(continues === undefined ? {} : { continues }),
378
+ ...(ttlSeconds === undefined ? {} : { ttl_seconds: ttlSeconds }),
379
+ }, options);
380
+ }
381
+ /** `DELETE /v1/client-tokens/{id}`: ends the token and its whole chain. Revoking again is fine. */
382
+ async revoke(id, options = {}) {
383
+ const videxo = this.#videxo;
384
+ const pending = videxo.client.DELETE('/v1/client-tokens/{id}', { params: { path: { id } }, fetch: videxo.fetchFor('safe', options) });
385
+ const { response, error } = (await pending.catch((failure) => {
386
+ throw redacted(videxo.transport, failure);
387
+ }));
388
+ if (!response.ok)
389
+ throw errorFromResponse(response, error);
390
+ }
391
+ /** `POST /v1/client-tokens/revoke`: ends every chain of the end user; answers how many tokens. */
392
+ async revokeSubject(subject, options = {}) {
393
+ const videxo = this.#videxo;
394
+ const { revoked } = await videxo.unwrap(videxo.client.POST('/v1/client-tokens/revoke', { body: { subject }, fetch: videxo.fetchFor('safe', options) }));
395
+ return revoked;
396
+ }
397
+ }
398
+ export class Webhooks {
399
+ /** Checks a delivery's Standard Webhooks signature and answers its event (`verifyWebhook`). */
400
+ verify(body, headers, secret, options) {
401
+ return verifyWebhook(body, headers, secret, options);
402
+ }
403
+ }
@@ -0,0 +1,7 @@
1
+ export declare const DEFAULT_BASE_URL = "https://api.videxo.io";
2
+ export declare const DEFAULT_MAX_RETRIES = 2;
3
+ export declare const DEFAULT_TIMEOUT_MS = 60000;
4
+ export declare const DEFAULT_WAIT: Readonly<{
5
+ timeoutMs: number;
6
+ intervalMs: 2000;
7
+ }>;
@@ -0,0 +1,6 @@
1
+ // The defaults the keyed client and the app client share, apart from both so that one can be
2
+ // bundled without the other.
3
+ export const DEFAULT_BASE_URL = 'https://api.videxo.io';
4
+ export const DEFAULT_MAX_RETRIES = 2;
5
+ export const DEFAULT_TIMEOUT_MS = 60_000;
6
+ export const DEFAULT_WAIT = Object.freeze({ timeoutMs: 10 * 60_000, intervalMs: 2_000 });