@convert-buddy/cloud-client 0.1.0-beta.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,64 @@
1
+ Convert Buddy Free Distribution License 1.0
2
+
3
+ Copyright (c) Convert Buddy copyright holders. All rights reserved, except as
4
+ expressly granted below or by an applicable separate license.
5
+
6
+ 1. Scope
7
+ This license applies to first-party material in official Convert Buddy package
8
+ distributions carrying this license (the Software), to the extent its copyright
9
+ holders have authority to license that material on these terms. It does not grant
10
+ access to the private development repository or license undistributed source code.
11
+ Previously licensed material and third-party components are addressed in section 5.
12
+
13
+ 2. Free application use
14
+ You may download, install, copy, run, and use the Software without a license fee
15
+ for personal, educational, nonprofit, internal business, and commercial purposes,
16
+ including commercial applications and hosted services. There are no per-user,
17
+ per-record, or revenue limits under this license.
18
+
19
+ You may bundle and redistribute the Software as part of your applications, SDK
20
+ integrations, websites, and hosted services, including through application stores
21
+ and CDNs. You may modify distributed JavaScript and other distributed integration
22
+ files as needed for those uses, including compilation, bundling, and minification.
23
+ You may mirror unmodified official packages for dependency installation and
24
+ archiving, including private registries and public package caches.
25
+
26
+ Keep this license and all applicable copyright and third-party notices with
27
+ redistributed copies, or in an accompanying, accessible legal-notices file or page
28
+ when bundling prevents retaining individual files. Identify your modifications
29
+ without implying that the modified distribution is an official Convert Buddy release.
30
+
31
+ 3. Reserved rights
32
+ This is a proprietary distribution license, not an open-source license. Except
33
+ as allowed above, you may not distribute or sublicense the Software as a standalone
34
+ library, engine, or replacement Convert Buddy package, or rebrand or sell it as
35
+ your own standalone software product. This does not restrict selling your own
36
+ applications or services that integrate the Software as permitted in section 2.
37
+ No rights to unpublished source code, trademarks, or an endorsement are granted.
38
+ Rights not expressly granted are reserved. Statutory rights that cannot lawfully
39
+ be excluded or limited by agreement remain unaffected.
40
+
41
+ 4. Hosted services
42
+ This license does not include Convert Buddy Cloud subscriptions, hosted quotas,
43
+ support commitments, or service-level guarantees. Those services have separate
44
+ terms. Their availability or pricing does not change the free-use grant for a
45
+ copy of the Software obtained under this license.
46
+
47
+ 5. Earlier releases and third-party material
48
+ This license does not revoke, replace, or restrict rights already granted under
49
+ MIT or any other separate license. Earlier Convert Buddy releases and portions
50
+ made available under MIT retain those permissions; see NOTICE and accompanying
51
+ license files. Where that material is included in this distribution, its separate
52
+ license continues to apply to that material. Third-party libraries, models,
53
+ datasets, generated bindings, and other separately licensed material retain their
54
+ own terms. These restrictions apply only to first-party material that is not
55
+ already available to you under a broader applicable license. You must comply with
56
+ the applicable notices and licenses for each included component.
57
+
58
+ 6. No warranty or liability
59
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
60
+ IMPLIED, INCLUDING MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND
61
+ NONINFRINGEMENT. TO THE MAXIMUM EXTENT PERMITTED BY LAW, THE COPYRIGHT HOLDERS
62
+ ARE NOT LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING FROM, OUT OF,
63
+ OR IN CONNECTION WITH THE SOFTWARE OR ITS USE, WHETHER IN CONTRACT, TORT, OR
64
+ OTHERWISE.
package/NOTICE ADDED
@@ -0,0 +1,33 @@
1
+ Convert Buddy distribution notices
2
+
3
+ Earlier Convert Buddy releases and portions licensed under MIT retain their
4
+ original permissions. The free distribution license does not revoke those rights.
5
+ The following is the original repository license text, preserved for that material.
6
+ It is not a grant of MIT rights for new proprietary material.
7
+
8
+ MIT License
9
+
10
+ Copyright (c) Convert Buddy contributors
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.
29
+
30
+ Third-party components retain their own licenses and copyright notices. Keep all
31
+ accompanying third-party license files when redistributing. OCR model weights and
32
+ other separately obtained assets require their own license review; this notice
33
+ is not a grant of rights to those assets.
package/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # Convert Buddy Cloud client
2
+
3
+ The browser client supports **Cloud API v2 only**. A publishable key alone cannot authorize a request.
4
+
5
+ Use `ConvertBuddyWidgetClient` with `apiUrl`, `publishableKey`, and a short-lived `sessionToken` supplied by your authorized backend. The backend mints a session with an environment server key at `POST /v2/server/widget-sessions`. Keep server credentials out of browser code; keep widget tokens in memory.
6
+
7
+ The client uses generated widget endpoint types, rejects redirects, bounds metadata responses, and validates batch receipts. Delivery retries keep the same body and identity. In-flight or uncertain delivery is not an acknowledgement. Cancellation and completion require an active authorized session.
8
+
9
+ The React importer exposes `getSession({ sessionId?, signal })`. For reload recovery, the host must verify the requesting user's ownership and renew the same session through the server-only renewal endpoint. Renewal preserves the version, destination, budgets, and receipts, and is limited to the original 24-hour horizon. A new session cannot recover an old import.
10
+
11
+ V1 exports, key-only calls, and AI-mapping endpoints have been removed. `TargetSchemaV1` and the signed delivery protocol retain their own independent version numbers.
12
+
13
+ See [the public guide](https://convert-buddy.app/docs/cloud/v2). Package publication and hosted production activation remain separate release gates.
@@ -0,0 +1,372 @@
1
+ // src/processing.ts
2
+ function blobFile(blob) {
3
+ return {
4
+ size: blob.size,
5
+ async read(offset, length, signal) {
6
+ signal?.throwIfAborted();
7
+ const bytes = new Uint8Array(
8
+ await blob.slice(offset, offset + length).arrayBuffer()
9
+ );
10
+ signal?.throwIfAborted();
11
+ return bytes;
12
+ }
13
+ };
14
+ }
15
+ var ProcessingCloudError = class extends Error {
16
+ constructor(code, status, requestId) {
17
+ super(code);
18
+ this.code = code;
19
+ this.status = status;
20
+ this.requestId = requestId;
21
+ this.name = "ProcessingCloudError";
22
+ }
23
+ code;
24
+ status;
25
+ requestId;
26
+ };
27
+ var uuid = /^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}$/i;
28
+ function identifier(value) {
29
+ if (!uuid.test(value)) throw new TypeError("Invalid resource ID");
30
+ return value;
31
+ }
32
+ function sleep(ms, signal) {
33
+ return new Promise((resolve, reject) => {
34
+ const abort = () => {
35
+ clearTimeout(timer);
36
+ reject(signal?.reason);
37
+ };
38
+ const timer = setTimeout(() => {
39
+ signal?.removeEventListener("abort", abort);
40
+ resolve();
41
+ }, ms);
42
+ if (signal?.aborted) abort();
43
+ else signal?.addEventListener("abort", abort, { once: true });
44
+ });
45
+ }
46
+ async function json(response) {
47
+ const reader = response.body?.getReader();
48
+ const chunks = [];
49
+ let length = 0;
50
+ try {
51
+ if (!reader) throw new ProcessingCloudError("INVALID_RESPONSE");
52
+ for (; ; ) {
53
+ const next = await reader.read();
54
+ if (next.done) break;
55
+ length += next.value.length;
56
+ if (length > 1024 * 1024)
57
+ throw new ProcessingCloudError("RESPONSE_LIMIT");
58
+ chunks.push(next.value);
59
+ }
60
+ const bytes = new Uint8Array(length);
61
+ let offset = 0;
62
+ for (const chunk of chunks) {
63
+ bytes.set(chunk, offset);
64
+ offset += chunk.length;
65
+ }
66
+ try {
67
+ return JSON.parse(new TextDecoder().decode(bytes));
68
+ } catch {
69
+ throw new ProcessingCloudError("INVALID_RESPONSE");
70
+ }
71
+ } finally {
72
+ await reader?.cancel().catch(() => void 0);
73
+ reader?.releaseLock();
74
+ }
75
+ }
76
+ function validJob(value) {
77
+ if (!value || !uuid.test(value.jobId) || ![
78
+ "queued",
79
+ "running",
80
+ "succeeded",
81
+ "failed",
82
+ "cancelling",
83
+ "cancelled"
84
+ ].includes(value.status) || !Array.isArray(value.artifacts) || value.artifacts.some((a) => !uuid.test(a.artifactId)))
85
+ throw new ProcessingCloudError("INVALID_RESPONSE");
86
+ return value;
87
+ }
88
+ var ProcessingClient = class {
89
+ constructor(options) {
90
+ this.options = options;
91
+ this.#origin = new URL(options.apiUrl);
92
+ const u = this.#origin;
93
+ if (u.username || u.password || u.search || u.hash || u.pathname !== "/" || u.protocol !== "https:" && !(u.protocol === "http:" && ["localhost", "127.0.0.1"].includes(u.hostname)))
94
+ throw new TypeError(
95
+ "API URL must be an HTTPS origin or local HTTP origin"
96
+ );
97
+ this.#fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
98
+ }
99
+ options;
100
+ #origin;
101
+ #fetch;
102
+ async #response(path, init) {
103
+ const headers = new Headers(await this.options.authorization());
104
+ new Headers(init.headers).forEach((value, key) => headers.set(key, value));
105
+ const response = await this.#fetch(
106
+ new URL(`/v2/processing${path}`, this.#origin),
107
+ {
108
+ ...init,
109
+ headers,
110
+ credentials: "omit",
111
+ redirect: "manual",
112
+ cache: "no-store"
113
+ }
114
+ );
115
+ if (response.type === "opaqueredirect" || response.status >= 300 && response.status < 400 && response.status !== 304) {
116
+ await response.body?.cancel();
117
+ throw new ProcessingCloudError("REDIRECT_DENIED", response.status);
118
+ }
119
+ return response;
120
+ }
121
+ async #metadata(path, method, body, signal, headers) {
122
+ const requestHeaders = new Headers(headers);
123
+ requestHeaders.set("content-type", "application/json");
124
+ const response = await this.#response(path, {
125
+ method,
126
+ body: body === void 0 ? void 0 : JSON.stringify(body),
127
+ signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(3e4)]) : AbortSignal.timeout(3e4),
128
+ headers: requestHeaders
129
+ });
130
+ const value = await json(response);
131
+ if (!response.ok) {
132
+ const code = value && typeof value === "object" && "error" in value && value.error && typeof value.error === "object" && "code" in value.error && typeof value.error.code === "string" ? value.error.code : "HTTP_ERROR";
133
+ throw new ProcessingCloudError(
134
+ code,
135
+ response.status,
136
+ response.headers.get("x-request-id") ?? void 0
137
+ );
138
+ }
139
+ return value;
140
+ }
141
+ getUpload(uploadId, after = 0, signal) {
142
+ return this.#metadata(
143
+ `/uploads/${identifier(uploadId)}?after=${after}`,
144
+ "GET",
145
+ void 0,
146
+ signal
147
+ );
148
+ }
149
+ createSession(grant, signal) {
150
+ return this.#metadata("/sessions", "POST", grant, signal);
151
+ }
152
+ renewSession(id, grant, signal) {
153
+ return this.#metadata(
154
+ `/sessions/${identifier(id)}/renew`,
155
+ "POST",
156
+ grant,
157
+ signal
158
+ );
159
+ }
160
+ revokeSession(id, signal) {
161
+ return this.#metadata(
162
+ `/sessions/${identifier(id)}/revoke`,
163
+ "POST",
164
+ {},
165
+ signal
166
+ );
167
+ }
168
+ abortUpload(uploadId, signal) {
169
+ return this.#metadata(
170
+ `/uploads/${identifier(uploadId)}`,
171
+ "DELETE",
172
+ {},
173
+ signal
174
+ );
175
+ }
176
+ async upload(source, options) {
177
+ const file = source instanceof Blob ? blobFile(source) : source;
178
+ if (!Number.isSafeInteger(file.size) || file.size <= 0)
179
+ throw new TypeError(
180
+ "A nonempty replayable file with a known size is required"
181
+ );
182
+ const upload = await this.#metadata(
183
+ "/uploads",
184
+ "POST",
185
+ {
186
+ clientKey: identifier(options.clientKey),
187
+ operation: options.operation,
188
+ bytes: file.size,
189
+ contentType: options.contentType ?? "application/octet-stream"
190
+ },
191
+ options.signal
192
+ );
193
+ if (!upload || !uuid.test(upload.uploadId) || upload.bytes !== file.size || !Number.isSafeInteger(upload.partBytes) || upload.partBytes < 1 || upload.partBytes > 32 * 1024 ** 2 || upload.partCount !== Math.ceil(file.size / upload.partBytes))
194
+ throw new ProcessingCloudError("INVALID_RESPONSE");
195
+ if (upload.jobId && uuid.test(upload.jobId)) return upload;
196
+ if (upload.status === "deleted" || upload.status === "deleting")
197
+ throw new ProcessingCloudError("UPLOAD_EXPIRED", 410);
198
+ let page;
199
+ for (let part = 1; part <= upload.partCount; part++) {
200
+ options.signal?.throwIfAborted();
201
+ if (!page || page.nextPart !== null && part > page.nextPart)
202
+ page = await this.getUpload(upload.uploadId, part - 1, options.signal);
203
+ const offset = (part - 1) * upload.partBytes, expected = Math.min(upload.partBytes, file.size - offset);
204
+ const bytes = new Uint8Array(
205
+ await file.read(offset, expected, options.signal)
206
+ );
207
+ if (bytes.length !== expected)
208
+ throw new TypeError("File reader returned an incorrect range");
209
+ const hash = Array.from(
210
+ new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)),
211
+ (value) => value.toString(16).padStart(2, "0")
212
+ ).join("");
213
+ const existing = page.parts.find((p) => p.partNumber === part);
214
+ if (existing && (existing.sha256 !== hash || existing.bytes !== bytes.length))
215
+ throw new ProcessingCloudError("LOCAL_FILE_CHANGED", 409);
216
+ if (!existing) {
217
+ if (upload.status !== "uploading")
218
+ throw new ProcessingCloudError("UPLOAD_CLOSED", 409);
219
+ for (let attempt = 0; ; attempt++) {
220
+ try {
221
+ const response = await this.#response(
222
+ `/uploads/${upload.uploadId}/parts/${part}`,
223
+ {
224
+ method: "PUT",
225
+ body: bytes,
226
+ signal: options.signal,
227
+ headers: {
228
+ "content-type": "application/octet-stream",
229
+ "X-Part-Sha256": hash
230
+ }
231
+ }
232
+ );
233
+ const receipt = await json(response);
234
+ if (!response.ok)
235
+ throw new ProcessingCloudError(
236
+ receipt.error?.code ?? "UPLOAD_FAILED",
237
+ response.status
238
+ );
239
+ if (receipt.bytes !== bytes.length || receipt.partNumber !== part)
240
+ throw new ProcessingCloudError("INVALID_RESPONSE");
241
+ break;
242
+ } catch (error) {
243
+ if (attempt >= 4 || options.signal?.aborted || error instanceof ProcessingCloudError && ![429, 500, 502, 503, 504].includes(error.status ?? 0) && !["PART_IN_FLIGHT", "PART_NOT_ACKNOWLEDGED"].includes(
244
+ error.code
245
+ ))
246
+ throw error;
247
+ await sleep(Math.min(3e4, 1e3 * 2 ** attempt), options.signal);
248
+ }
249
+ }
250
+ }
251
+ options.onProgress?.(offset + bytes.length, file.size);
252
+ }
253
+ return upload;
254
+ }
255
+ async submit(request, idempotencyKey, signal) {
256
+ if (!/^[A-Za-z0-9._:-]{1,128}$/.test(idempotencyKey))
257
+ throw new TypeError("Invalid idempotency key");
258
+ return validJob(
259
+ await this.#metadata("/jobs", "POST", request, signal, {
260
+ "Idempotency-Key": idempotencyKey
261
+ })
262
+ );
263
+ }
264
+ async getJob(id, signal) {
265
+ return validJob(
266
+ await this.#metadata(`/jobs/${identifier(id)}`, "GET", void 0, signal)
267
+ );
268
+ }
269
+ async cancel(id, signal) {
270
+ return validJob(
271
+ await this.#metadata(
272
+ `/jobs/${identifier(id)}/cancel`,
273
+ "POST",
274
+ {},
275
+ signal
276
+ )
277
+ );
278
+ }
279
+ async retry(id, expectedGeneration, signal) {
280
+ return validJob(
281
+ await this.#metadata(
282
+ `/jobs/${identifier(id)}/retry`,
283
+ "POST",
284
+ { expectedGeneration },
285
+ signal
286
+ )
287
+ );
288
+ }
289
+ async wait(id, options = {}) {
290
+ let delay = 1e3, etag = null;
291
+ for (; ; ) {
292
+ const response = await this.#response(`/jobs/${identifier(id)}`, {
293
+ method: "GET",
294
+ signal: options.signal,
295
+ headers: etag ? { "If-None-Match": etag } : {}
296
+ });
297
+ if (response.status === 304) await response.body?.cancel();
298
+ else {
299
+ if (!response.ok) {
300
+ await response.body?.cancel();
301
+ throw new ProcessingCloudError("STATUS_UNAVAILABLE", response.status);
302
+ }
303
+ const job = validJob(await json(response));
304
+ etag = response.headers.get("etag");
305
+ options.onProgress?.(job);
306
+ if (["succeeded", "failed", "cancelled"].includes(job.status))
307
+ return job;
308
+ }
309
+ await sleep(delay + Math.random() * 250, options.signal);
310
+ delay = Math.min(1e4, delay * 1.5);
311
+ }
312
+ }
313
+ async download(jobId, artifactId, options = {}) {
314
+ if (options.offset !== void 0 && (!Number.isSafeInteger(options.offset) || options.offset < 0 || !options.etag))
315
+ throw new TypeError(
316
+ "Resuming requires a nonnegative offset and the original ETag"
317
+ );
318
+ const headers = options.offset !== void 0 ? { Range: `bytes=${options.offset}-`, "If-Range": options.etag } : void 0;
319
+ const response = await this.#response(
320
+ `/jobs/${identifier(jobId)}/artifacts/${identifier(artifactId)}`,
321
+ { method: "GET", headers, signal: options.signal }
322
+ );
323
+ if (!response.ok || !response.body) {
324
+ await response.body?.cancel();
325
+ throw new ProcessingCloudError("DOWNLOAD_UNAVAILABLE", response.status);
326
+ }
327
+ if (options.offset !== void 0 && (response.status !== 206 || response.headers.get("etag") !== options.etag || !response.headers.get("content-range")?.startsWith(`bytes ${options.offset}-`))) {
328
+ await response.body.cancel();
329
+ throw new ProcessingCloudError("DOWNLOAD_CHANGED", 409);
330
+ }
331
+ return response;
332
+ }
333
+ };
334
+ function createBrowserProcessingClient(options) {
335
+ return new ProcessingClient({
336
+ ...options,
337
+ async authorization() {
338
+ const token = await options.sessionToken();
339
+ if (!/^cb_ps_[a-f0-9]{64}\.[A-Za-z0-9_-]{43}$/.test(token))
340
+ throw new TypeError("A short-lived processing session is required");
341
+ return {
342
+ authorization: `Bearer ${token}`,
343
+ "X-Processing-Identity": "session"
344
+ };
345
+ }
346
+ });
347
+ }
348
+ function createWidgetProcessingClient(options) {
349
+ return new ProcessingClient({
350
+ ...options,
351
+ async authorization() {
352
+ const token = await options.sessionToken(), route = /^cb_ws_([a-f0-9]{64})\.[A-Za-z0-9_-]{43}$/.exec(token), key = /^cb_pk_([a-f0-9]{64})\.[A-Za-z0-9_-]{43}$/.exec(
353
+ options.publishableKey
354
+ );
355
+ if (!route || !key || route[1] !== key[1])
356
+ throw new TypeError("Matching widget credentials are required");
357
+ return {
358
+ authorization: `Bearer ${token}`,
359
+ "X-Processing-Identity": "widget",
360
+ "X-Convert-Buddy-Key": options.publishableKey
361
+ };
362
+ }
363
+ });
364
+ }
365
+
366
+ export {
367
+ blobFile,
368
+ ProcessingCloudError,
369
+ ProcessingClient,
370
+ createBrowserProcessingClient,
371
+ createWidgetProcessingClient
372
+ };