@glboost/sdk 0.0.0-stage → 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,366 @@
1
+ import {
2
+ PRESET_NAMES,
3
+ canonicalSpec,
4
+ isPresetName,
5
+ parseCanonicalSpec,
6
+ presetSpec,
7
+ specDiffFromCompat
8
+ } from "./chunk-YL4SZ5Y4.js";
9
+
10
+ // ../core/src/share.ts
11
+ function deliveryQueryForSpec(canonical, preset) {
12
+ const q = new URLSearchParams();
13
+ if (canonical === "original") {
14
+ q.set("p", "original");
15
+ return q;
16
+ }
17
+ if (preset && preset !== "original") {
18
+ q.set("p", preset);
19
+ return q;
20
+ }
21
+ q.set("p", "compat");
22
+ for (const [key, value] of specDiffFromCompat(parseCanonicalSpec(canonical))) q.set(key, value);
23
+ return q;
24
+ }
25
+
26
+ // src/client.ts
27
+ import createOpenApiClient from "openapi-fetch";
28
+
29
+ // src/errors.ts
30
+ var GLBoostApiError = class extends Error {
31
+ name = "GLBoostApiError";
32
+ status;
33
+ /** The API's `error` string, e.g. `asset not found` or `optimization limit reached`. */
34
+ code;
35
+ body;
36
+ url;
37
+ constructor(status, body, url = null) {
38
+ const code = body.error ?? `HTTP ${status}`;
39
+ const planLimit = status === 402 || status === 413;
40
+ super(planLimit && body.hint ? `${code}: ${body.hint}` : code);
41
+ this.status = status;
42
+ this.code = code;
43
+ this.body = body;
44
+ this.url = url;
45
+ }
46
+ /** 402 (plan limit) or 413 (file too large): `plan`, `used`, `limit`, and `upgradeUrl` are set. */
47
+ get isPlanLimit() {
48
+ return this.status === 402 || this.status === 413;
49
+ }
50
+ get plan() {
51
+ return this.body.plan ?? null;
52
+ }
53
+ get used() {
54
+ return this.body.used ?? null;
55
+ }
56
+ get limit() {
57
+ return this.body.limit ?? null;
58
+ }
59
+ get hint() {
60
+ return this.body.hint ?? null;
61
+ }
62
+ /** Relative to the app (`/app/settings#billing`). */
63
+ get upgradeUrl() {
64
+ return this.body.upgradeUrl ?? null;
65
+ }
66
+ get issues() {
67
+ return this.body.issues ?? null;
68
+ }
69
+ };
70
+ async function apiErrorFromResponse(response, parsedBody) {
71
+ let body;
72
+ if (parsedBody !== void 0 && parsedBody !== null) {
73
+ body = typeof parsedBody === "object" ? parsedBody : { error: String(parsedBody) };
74
+ } else {
75
+ const text = await response.text().catch(() => "");
76
+ try {
77
+ const parsed = text ? JSON.parse(text) : {};
78
+ body = parsed && typeof parsed === "object" ? parsed : { error: text };
79
+ } catch {
80
+ body = text ? { error: text.slice(0, 200) } : {};
81
+ }
82
+ }
83
+ return new GLBoostApiError(response.status, body, response.url || null);
84
+ }
85
+ var GLBoostTimeoutError = class extends Error {
86
+ constructor(assetId, versionId, timeoutMs) {
87
+ super(`asset ${assetId} was not ready after ${Math.round(timeoutMs / 1e3)} s`);
88
+ this.assetId = assetId;
89
+ this.versionId = versionId;
90
+ this.timeoutMs = timeoutMs;
91
+ }
92
+ assetId;
93
+ versionId;
94
+ timeoutMs;
95
+ name = "GLBoostTimeoutError";
96
+ };
97
+
98
+ // src/client.ts
99
+ var DEFAULT_BASE_URL = "https://glboost.com";
100
+ function createClient(options = {}) {
101
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
102
+ const impl = options.fetch;
103
+ const doFetch = (input, init) => (impl ?? globalThis.fetch)(input, init);
104
+ const headers = { ...options.headers };
105
+ if (options.apiKey) headers.authorization = `Bearer ${options.apiKey}`;
106
+ if (options.project) headers["x-glboost-project"] = options.project;
107
+ const api = createOpenApiClient({
108
+ baseUrl,
109
+ headers,
110
+ fetch: (request2) => doFetch(request2)
111
+ });
112
+ const request = (path, init = {}) => {
113
+ const url = /^https?:\/\//i.test(path) ? path : `${baseUrl}${path.startsWith("/") ? "" : "/"}${path}`;
114
+ const h = new Headers(init.headers);
115
+ for (const [key, value] of Object.entries(headers)) if (!h.has(key)) h.set(key, value);
116
+ if (typeof init.body === "string" && !h.has("content-type")) {
117
+ h.set("content-type", "application/json");
118
+ }
119
+ return doFetch(url, { ...init, headers: h });
120
+ };
121
+ let mePromise = null;
122
+ const me = () => {
123
+ mePromise ??= unwrap(api.GET("/api/v1/me")).catch((e) => {
124
+ mePromise = null;
125
+ throw e;
126
+ });
127
+ return mePromise;
128
+ };
129
+ return { baseUrl, api, fetch: doFetch, request, me };
130
+ }
131
+ async function unwrap(result) {
132
+ const { data, error, response } = await result;
133
+ if (!response.ok) throw await apiErrorFromResponse(response, error);
134
+ return data;
135
+ }
136
+
137
+ // src/sign.ts
138
+ async function signUrl(client, assetId, options = {}) {
139
+ const spec = typeof options.spec === "string" ? parseCanonicalSpec(options.spec) : options.spec;
140
+ const body = {
141
+ ...options.preset ? { preset: options.preset } : {},
142
+ ...spec ? { spec } : {},
143
+ ...options.versionId ? { versionId: options.versionId } : {},
144
+ ...options.ttlSeconds !== void 0 ? { ttlSeconds: options.ttlSeconds } : {},
145
+ ...options.dl !== void 0 ? { dl: options.dl } : {}
146
+ };
147
+ return unwrap(
148
+ client.api.POST("/api/v1/assets/{assetId}/sign", { params: { path: { assetId } }, body })
149
+ );
150
+ }
151
+
152
+ // src/upload.ts
153
+ var DEFAULT_UPLOAD_ENDPOINTS = {
154
+ init: "/api/v1/uploads",
155
+ finish: (versionId) => `/api/v1/uploads/${versionId}/complete`,
156
+ abort: (versionId) => `/api/v1/uploads/${versionId}/abort`
157
+ };
158
+ async function uploadModel(client, input, options = {}) {
159
+ const { name, blob } = toNamedBlob(input);
160
+ const endpoints = options.endpoints ?? DEFAULT_UPLOAD_ENDPOINTS;
161
+ const projectId = options.projectId ?? (options.endpoints ? void 0 : (await client.me()).project.id);
162
+ const initRes = await client.request(endpoints.init, {
163
+ method: "POST",
164
+ signal: options.signal,
165
+ body: JSON.stringify({
166
+ ...projectId ? { projectId } : {},
167
+ filename: name,
168
+ contentType: blob.type || contentTypeFor(name),
169
+ bytes: blob.size,
170
+ ...options.assetId ? { assetId: options.assetId } : {},
171
+ ...options.note?.trim() ? { note: options.note.trim() } : {},
172
+ ...endpoints.extra
173
+ })
174
+ });
175
+ if (!initRes.ok) throw await apiErrorFromResponse(initRes);
176
+ const init = await initRes.json();
177
+ try {
178
+ return await transfer(client, blob, init, options, endpoints);
179
+ } catch (e) {
180
+ await abortUpload(client, init, endpoints);
181
+ throw e;
182
+ }
183
+ }
184
+ async function transfer(client, blob, init, options, endpoints) {
185
+ const total = blob.size;
186
+ const count = Math.max(1, Math.ceil(total / init.partSize));
187
+ const transport = options.transport ?? (typeof XMLHttpRequest === "function" && options.onProgress ? "xhr" : "fetch");
188
+ const token = encodeURIComponent(init.token);
189
+ const parts = [];
190
+ let completed = 0;
191
+ options.onProgress?.({ loaded: 0, total });
192
+ for (let i = 0; i < count; i++) {
193
+ const chunk = blob.slice(i * init.partSize, Math.min(total, (i + 1) * init.partSize));
194
+ const url = `${init.uploadUrl}/part?n=${i + 1}&token=${token}`;
195
+ const report = (loaded) => options.onProgress?.({ loaded: Math.min(total, completed + loaded), total });
196
+ const part = transport === "xhr" ? await putPartXhr(url, chunk, report) : await putPartFetch(client, url, chunk, options.signal);
197
+ completed += chunk.size;
198
+ options.onProgress?.({ loaded: completed, total });
199
+ parts.push(part);
200
+ }
201
+ const done = await client.fetch(`${init.uploadUrl}/complete?token=${token}`, {
202
+ method: "POST",
203
+ headers: { "content-type": "application/json" },
204
+ body: JSON.stringify({ parts }),
205
+ signal: options.signal
206
+ });
207
+ if (!done.ok) throw new Error(`complete failed: ${await readError(done)}`);
208
+ const finish = await client.request(endpoints.finish(init.versionId), {
209
+ method: "POST",
210
+ signal: options.signal
211
+ });
212
+ if (!finish.ok) throw await apiErrorFromResponse(finish);
213
+ return {
214
+ assetId: init.assetId,
215
+ versionId: init.versionId,
216
+ finish: await finish.json().catch(() => null)
217
+ };
218
+ }
219
+ async function putPartFetch(client, url, chunk, signal) {
220
+ const res = await client.fetch(url, { method: "PUT", body: chunk, signal });
221
+ if (!res.ok) throw new Error(`part upload failed: ${res.status} ${await readError(res)}`);
222
+ return await res.json();
223
+ }
224
+ function putPartXhr(url, chunk, onProgress) {
225
+ return new Promise((resolve, reject) => {
226
+ const xhr = new XMLHttpRequest();
227
+ xhr.open("PUT", url);
228
+ xhr.upload.onprogress = (e) => onProgress(e.loaded);
229
+ xhr.onload = () => {
230
+ if (xhr.status >= 200 && xhr.status < 300) {
231
+ resolve(JSON.parse(xhr.responseText));
232
+ } else reject(new Error(`part upload failed: ${xhr.status} ${xhr.responseText}`));
233
+ };
234
+ xhr.onerror = () => reject(new Error("part upload failed: network error"));
235
+ xhr.send(chunk);
236
+ });
237
+ }
238
+ async function abortUpload(client, init, endpoints) {
239
+ await Promise.allSettled([
240
+ client.fetch(`${init.uploadUrl}/abort?token=${encodeURIComponent(init.token)}`, {
241
+ method: "POST"
242
+ }),
243
+ endpoints.abort ? client.request(endpoints.abort(init.versionId), { method: "POST" }) : null
244
+ ]);
245
+ }
246
+ async function readError(res) {
247
+ try {
248
+ const body = await res.json();
249
+ return body.error ?? `${res.status}`;
250
+ } catch {
251
+ return `${res.status}`;
252
+ }
253
+ }
254
+ function toNamedBlob(input) {
255
+ if (input instanceof Blob) {
256
+ const name2 = input.name;
257
+ if (!name2) throw new TypeError("uploadModel: a Blob needs a name; pass { name, data }");
258
+ return { name: name2, blob: input };
259
+ }
260
+ if (!input.name) throw new TypeError("uploadModel: name is required");
261
+ const { name, data } = input;
262
+ const type = input.type ?? (data instanceof Blob ? data.type : "") ?? "";
263
+ if (data instanceof Blob) return { name, blob: input.type ? new Blob([data], { type }) : data };
264
+ return { name, blob: new Blob([data], { type }) };
265
+ }
266
+ function contentTypeFor(filename) {
267
+ const lower = filename.toLowerCase();
268
+ if (lower.endsWith(".glb")) return "model/gltf-binary";
269
+ if (lower.endsWith(".gltf")) return "model/gltf+json";
270
+ if (lower.endsWith(".zip")) return "application/zip";
271
+ return "application/octet-stream";
272
+ }
273
+
274
+ // src/urls.ts
275
+ var DEFAULT_CDN_BASE = "https://cdn.glboost.com";
276
+ function assetUrl(assetId, options = {}) {
277
+ const base = (options.base ?? DEFAULT_CDN_BASE).replace(/\/+$/, "");
278
+ const query = options.spec ? deliveryQueryForSpec(options.spec, null) : presetQuery(options.preset ?? "compat");
279
+ if (options.v) query.set("v", options.v);
280
+ if (options.dl) query.set("dl", "1");
281
+ return `${base}/${assetId}.glb?${query.toString()}`;
282
+ }
283
+ function configUrl(assetId, base = DEFAULT_CDN_BASE) {
284
+ return `${base.replace(/\/+$/, "")}/${assetId}/config.json`;
285
+ }
286
+ function presetQuery(preset) {
287
+ if (!isPresetName(preset)) throw new Error(`unknown preset "${preset}"`);
288
+ const spec = presetSpec(preset);
289
+ return deliveryQueryForSpec(spec ? canonicalSpec(spec) : "original", preset);
290
+ }
291
+
292
+ // src/wait.ts
293
+ var PENDING = /* @__PURE__ */ new Set(["queued", "processing"]);
294
+ async function waitForReady(client, assetId, options = {}) {
295
+ const timeoutMs = options.timeoutMs ?? 6e5;
296
+ const intervalMs = options.intervalMs ?? 5e3;
297
+ const deadline = Date.now() + timeoutMs;
298
+ for (; ; ) {
299
+ const data = await unwrap(
300
+ client.api.GET("/api/v1/assets/{assetId}/variants", {
301
+ params: { path: { assetId } },
302
+ signal: options.signal
303
+ })
304
+ );
305
+ const version = options.versionId ? data.versions.find((v) => v.id === options.versionId) : data.versions.reduce(
306
+ (best, v) => best === void 0 || v.number > best.number ? v : best,
307
+ void 0
308
+ );
309
+ if (!version) {
310
+ throw new Error(
311
+ options.versionId ? `version ${options.versionId} not found on asset ${assetId}` : `asset ${assetId} has no versions`
312
+ );
313
+ }
314
+ const state = {
315
+ versionId: version.id,
316
+ number: version.number,
317
+ variants: version.variants,
318
+ pending: version.variants.filter((v) => PENDING.has(v.status)),
319
+ failed: version.variants.filter((v) => v.status === "failed")
320
+ };
321
+ options.onPoll?.(state);
322
+ if (state.pending.length === 0) return state;
323
+ if (Date.now() + intervalMs > deadline) {
324
+ throw new GLBoostTimeoutError(assetId, version.id, timeoutMs);
325
+ }
326
+ await sleep(intervalMs, options.signal);
327
+ }
328
+ }
329
+ function sleep(ms, signal) {
330
+ return new Promise((resolve, reject) => {
331
+ if (signal?.aborted) {
332
+ reject(signal.reason instanceof Error ? signal.reason : new Error("aborted"));
333
+ return;
334
+ }
335
+ const onAbort = () => {
336
+ clearTimeout(timer);
337
+ reject(signal?.reason instanceof Error ? signal.reason : new Error("aborted"));
338
+ };
339
+ const timer = setTimeout(() => {
340
+ signal?.removeEventListener("abort", onAbort);
341
+ resolve();
342
+ }, ms);
343
+ signal?.addEventListener("abort", onAbort, { once: true });
344
+ });
345
+ }
346
+ export {
347
+ DEFAULT_BASE_URL,
348
+ DEFAULT_CDN_BASE,
349
+ DEFAULT_UPLOAD_ENDPOINTS,
350
+ GLBoostApiError,
351
+ GLBoostTimeoutError,
352
+ PRESET_NAMES,
353
+ apiErrorFromResponse,
354
+ assetUrl,
355
+ canonicalSpec,
356
+ configUrl,
357
+ contentTypeFor,
358
+ createClient,
359
+ isPresetName,
360
+ parseCanonicalSpec,
361
+ signUrl,
362
+ unwrap,
363
+ uploadModel,
364
+ waitForReady
365
+ };
366
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../core/src/share.ts","../src/client.ts","../src/errors.ts","../src/sign.ts","../src/upload.ts","../src/urls.ts","../src/wait.ts"],"sourcesContent":["import { parseCanonicalSpec, specDiffFromCompat, type PresetName } from './spec';\nimport { signDelivery } from './tokens';\n\nconst SHARE_TOKEN_LENGTH = 22;\nconst URL_SAFE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';\n\n/** 22 URL-safe chars from 64 symbols = 132 bits of entropy. */\nexport function newShareToken(random: (n: number) => Uint8Array = defaultRandom): string {\n const bytes = random(SHARE_TOKEN_LENGTH);\n let out = '';\n for (let i = 0; i < SHARE_TOKEN_LENGTH; i++) out += URL_SAFE[(bytes[i] ?? 0) & 63];\n return out;\n}\n\nfunction defaultRandom(n: number): Uint8Array {\n const bytes = new Uint8Array(n);\n crypto.getRandomValues(bytes);\n return bytes;\n}\n\n/**\n * Query parameters that select a variant: `p=<preset>` for named presets, `p=original` for the\n * original, or `p=compat` plus the spec keys that differ from compat for ad-hoc specs. The preset\n * is always spelled out because the edge seeds a bare URL from the *project's* default preset,\n * which need not be compat; a URL that relied on that default would resolve (and sign) a different\n * spec on a project whose default is, say, `gpu`.\n */\nexport function deliveryQueryForSpec(\n canonical: string,\n preset: PresetName | null,\n): URLSearchParams {\n const q = new URLSearchParams();\n if (canonical === 'original') {\n q.set('p', 'original');\n return q;\n }\n if (preset && preset !== 'original') {\n q.set('p', preset);\n return q;\n }\n q.set('p', 'compat');\n for (const [key, value] of specDiffFromCompat(parseCanonicalSpec(canonical))) q.set(key, value);\n return q;\n}\n\nexport interface SignedUrlInput {\n base: string;\n assetId: string;\n /** Canonical spec string or `original`; must match what `query` selects. */\n canonical: string;\n query: URLSearchParams;\n versionId?: string;\n dl?: boolean;\n signingKey: string;\n /** Unix seconds. */\n exp: number;\n}\n\n/** Builds `https://<base>/<assetId>.glb?<query>&v=&dl=1&exp=&sig=` with a signature that verifies. */\nexport async function buildSignedUrl(input: SignedUrlInput): Promise<{ url: string; sig: string }> {\n const path = `/${input.assetId}.glb`;\n const sig = await signDelivery({\n signingKey: input.signingKey,\n path,\n canonical: input.canonical,\n exp: input.exp,\n });\n const q = new URLSearchParams(input.query);\n if (input.versionId) q.set('v', input.versionId);\n if (input.dl) q.set('dl', '1');\n q.set('exp', String(input.exp));\n q.set('sig', sig);\n return { url: `${input.base}${path}?${q.toString()}`, sig };\n}\n","import createOpenApiClient, { type Client } from 'openapi-fetch';\nimport { apiErrorFromResponse } from './errors';\nimport type { paths } from './openapi';\n\nexport const DEFAULT_BASE_URL = 'https://glboost.com';\n\nexport interface ClientOptions {\n /** `glb_live_…` from Settings → API keys. Leave unset for a same-origin dashboard session (cookies). */\n apiKey?: string;\n /** The app's origin (default `https://glboost.com`); `''` makes relative, same-origin requests in a browser. */\n baseUrl?: string;\n /** Project an org-wide key should work in (sent as `X-GLBoost-Project`). */\n project?: string;\n /** Replaces the global `fetch` (tests, custom dispatchers). */\n fetch?: typeof fetch;\n /** Extra headers on every API request. */\n headers?: Record<string, string>;\n}\n\n/** `GET /api/v1/me`: the caller's org, project, role, and how it authenticated. */\nexport type Me = paths['/api/v1/me']['get']['responses'][200]['content']['application/json'];\n\nexport interface GLBoostClient {\n readonly baseUrl: string;\n /** The typed openapi-fetch client: `client.api.GET('/api/v1/assets')`, `client.api.POST(…)`. */\n readonly api: Client<paths>;\n /** The fetch in use, with no GLBoost headers (CDN upload endpoints take their token in the URL). */\n readonly fetch: typeof fetch;\n /**\n * A raw request with the auth headers; `path` is absolute or relative to `baseUrl`. String bodies\n * are sent as JSON unless a content type is given.\n */\n request(path: string, init?: RequestInit): Promise<Response>;\n /** `GET /api/v1/me`, fetched once per client. */\n me(): Promise<Me>;\n}\n\nexport function createClient(options: ClientOptions = {}): GLBoostClient {\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, '');\n const impl = options.fetch;\n // Browsers throw \"Illegal invocation\" for an unbound `window.fetch`, hence the wrapper.\n const doFetch: typeof fetch = (input, init) => (impl ?? globalThis.fetch)(input, init);\n const headers: Record<string, string> = { ...options.headers };\n if (options.apiKey) headers.authorization = `Bearer ${options.apiKey}`;\n if (options.project) headers['x-glboost-project'] = options.project;\n const api = createOpenApiClient<paths>({\n baseUrl,\n headers,\n fetch: (request) => doFetch(request),\n });\n\n const request: GLBoostClient['request'] = (path, init = {}) => {\n const url = /^https?:\\/\\//i.test(path)\n ? path\n : `${baseUrl}${path.startsWith('/') ? '' : '/'}${path}`;\n const h = new Headers(init.headers);\n for (const [key, value] of Object.entries(headers)) if (!h.has(key)) h.set(key, value);\n if (typeof init.body === 'string' && !h.has('content-type')) {\n h.set('content-type', 'application/json');\n }\n return doFetch(url, { ...init, headers: h });\n };\n\n let mePromise: Promise<Me> | null = null;\n const me = () => {\n mePromise ??= unwrap(api.GET('/api/v1/me')).catch((e: unknown) => {\n mePromise = null;\n throw e;\n });\n return mePromise;\n };\n\n return { baseUrl, api, fetch: doFetch, request, me };\n}\n\ntype ApiResult<T> = { data?: T; error?: unknown; response: Response };\n\n/** `data` of an openapi-fetch result, or a `GLBoostApiError` built from the response and its body. */\nexport async function unwrap<T>(result: ApiResult<T> | PromiseLike<ApiResult<T>>): Promise<T> {\n const { data, error, response } = await result;\n if (!response.ok) throw await apiErrorFromResponse(response, error);\n return data as T;\n}\n","/** The body the API answers errors with: `{ error, issues? }`, plus the plan fields on 402 and 413. */\nexport interface ApiErrorBody {\n error?: string;\n issues?: unknown[];\n plan?: string;\n used?: number;\n limit?: number;\n hint?: string;\n upgradeUrl?: string;\n}\n\n/** A non-2xx answer from the API. `message` is the API's `error` (plus the hint on plan limits). */\nexport class GLBoostApiError extends Error {\n override readonly name = 'GLBoostApiError';\n readonly status: number;\n /** The API's `error` string, e.g. `asset not found` or `optimization limit reached`. */\n readonly code: string;\n readonly body: ApiErrorBody;\n readonly url: string | null;\n\n constructor(status: number, body: ApiErrorBody, url: string | null = null) {\n const code = body.error ?? `HTTP ${status}`;\n const planLimit = status === 402 || status === 413;\n super(planLimit && body.hint ? `${code}: ${body.hint}` : code);\n this.status = status;\n this.code = code;\n this.body = body;\n this.url = url;\n }\n\n /** 402 (plan limit) or 413 (file too large): `plan`, `used`, `limit`, and `upgradeUrl` are set. */\n get isPlanLimit(): boolean {\n return this.status === 402 || this.status === 413;\n }\n get plan(): string | null {\n return this.body.plan ?? null;\n }\n get used(): number | null {\n return this.body.used ?? null;\n }\n get limit(): number | null {\n return this.body.limit ?? null;\n }\n get hint(): string | null {\n return this.body.hint ?? null;\n }\n /** Relative to the app (`/app/settings#billing`). */\n get upgradeUrl(): string | null {\n return this.body.upgradeUrl ?? null;\n }\n get issues(): unknown[] | null {\n return this.body.issues ?? null;\n }\n}\n\n/**\n * Builds the error for a failed response. Pass `parsedBody` when the body was already read (the\n * openapi-fetch client parses it); otherwise the body is read here.\n */\nexport async function apiErrorFromResponse(\n response: Response,\n parsedBody?: unknown,\n): Promise<GLBoostApiError> {\n let body: ApiErrorBody;\n if (parsedBody !== undefined && parsedBody !== null) {\n body =\n typeof parsedBody === 'object' ? (parsedBody as ApiErrorBody) : { error: String(parsedBody) };\n } else {\n const text = await response.text().catch(() => '');\n try {\n const parsed: unknown = text ? JSON.parse(text) : {};\n body = parsed && typeof parsed === 'object' ? (parsed as ApiErrorBody) : { error: text };\n } catch {\n body = text ? { error: text.slice(0, 200) } : {};\n }\n }\n return new GLBoostApiError(response.status, body, response.url || null);\n}\n\n/** `waitForReady` gave up before every variant settled. */\nexport class GLBoostTimeoutError extends Error {\n override readonly name = 'GLBoostTimeoutError';\n constructor(\n readonly assetId: string,\n readonly versionId: string | null,\n readonly timeoutMs: number,\n ) {\n super(`asset ${assetId} was not ready after ${Math.round(timeoutMs / 1000)} s`);\n }\n}\n","import { parseCanonicalSpec, type OptimizeSpec } from '@glboost/core';\nimport { unwrap, type GLBoostClient } from './client';\nimport type { paths } from './openapi';\n\nexport type SignResult =\n paths['/api/v1/assets/{assetId}/sign']['post']['responses'][200]['content']['application/json'];\ntype SignBody = NonNullable<\n paths['/api/v1/assets/{assetId}/sign']['post']['requestBody']\n>['content']['application/json'];\n\nexport interface SignOptions {\n /** Named preset; default: the project's default preset. */\n preset?: string;\n /** A spec object or canonical string instead of a preset. */\n spec?: OptimizeSpec | string;\n versionId?: string;\n /** 60 s to 7 days; default 10 minutes. */\n ttlSeconds?: number;\n /** Sign a download URL (`dl=1`). */\n dl?: boolean;\n}\n\n/** `POST /api/v1/assets/{assetId}/sign`: a signed CDN URL for a private asset or a signing-required project. */\nexport async function signUrl(\n client: GLBoostClient,\n assetId: string,\n options: SignOptions = {},\n): Promise<SignResult> {\n const spec = typeof options.spec === 'string' ? parseCanonicalSpec(options.spec) : options.spec;\n const body: SignBody = {\n ...(options.preset ? { preset: options.preset } : {}),\n ...(spec ? { spec: spec as SignBody['spec'] } : {}),\n ...(options.versionId ? { versionId: options.versionId } : {}),\n ...(options.ttlSeconds !== undefined ? { ttlSeconds: options.ttlSeconds } : {}),\n ...(options.dl !== undefined ? { dl: options.dl } : {}),\n };\n return unwrap(\n client.api.POST('/api/v1/assets/{assetId}/sign', { params: { path: { assetId } }, body }),\n );\n}\n","import type { GLBoostClient } from './client';\nimport { apiErrorFromResponse } from './errors';\nimport type { paths } from './openapi';\n\n/** What `POST /api/v1/uploads` answers: the version ids and the CDN upload target. */\nexport type UploadInit =\n paths['/api/v1/uploads']['post']['responses'][200]['content']['application/json'];\n/** What `POST /api/v1/uploads/{versionId}/complete` answers. */\nexport type UploadFinish =\n paths['/api/v1/uploads/{versionId}/complete']['post']['responses'][200]['content']['application/json'];\n\nexport interface UploadProgress {\n loaded: number;\n total: number;\n}\n\n/** A `File`, or a named blob or byte buffer (`.glb`, self-contained `.gltf`, or `.zip`). */\nexport type UploadInput =\n Blob | { name: string; data: Blob | ArrayBuffer | ArrayBufferView; type?: string };\n\n/** Where the init, finish, and abort calls go; the defaults are the public API. */\nexport interface UploadEndpoints {\n init: string;\n finish: (versionId: string) => string;\n /** Tells the app the upload was given up on, so the version does not sit at `uploading`. */\n abort?: (versionId: string) => string;\n /** Extra fields for the init body (GLBoost's demo sends its Turnstile token). */\n extra?: Record<string, unknown>;\n}\n\nexport const DEFAULT_UPLOAD_ENDPOINTS: UploadEndpoints = {\n init: '/api/v1/uploads',\n finish: (versionId) => `/api/v1/uploads/${versionId}/complete`,\n abort: (versionId) => `/api/v1/uploads/${versionId}/abort`,\n};\n\nexport interface UploadOptions {\n /** Defaults to the caller's project (`GET /api/v1/me`) when the default endpoints are used. */\n projectId?: string;\n /** Replace an existing asset with a new version. */\n assetId?: string;\n /** What changed in this version; shown on the dashboard and share pages. */\n note?: string | null;\n onProgress?: (progress: UploadProgress) => void;\n endpoints?: UploadEndpoints;\n signal?: AbortSignal;\n /**\n * How parts reach the CDN: `xhr` reports progress per byte and is the default in browsers when\n * `onProgress` is set; `fetch` (the default elsewhere) reports progress per part.\n */\n transport?: 'fetch' | 'xhr';\n}\n\nexport interface UploadResult<F = UploadFinish> {\n assetId: string;\n versionId: string;\n /** The finish call's body (`{ assetId, versionId, queued }` on the public API). */\n finish: F | null;\n}\n\n/**\n * Uploads a model the way the dashboard does: init with the app, PUT the parts to the CDN,\n * complete the multipart upload there, then tell the app to start optimizing. On any failure after\n * init the upload is aborted on both sides (best effort) and the error is rethrown.\n */\nexport async function uploadModel<F = UploadFinish>(\n client: GLBoostClient,\n input: UploadInput,\n options: UploadOptions = {},\n): Promise<UploadResult<F>> {\n const { name, blob } = toNamedBlob(input);\n const endpoints = options.endpoints ?? DEFAULT_UPLOAD_ENDPOINTS;\n // Custom endpoints (the demo) decide their own project; the public API needs one.\n const projectId =\n options.projectId ?? (options.endpoints ? undefined : (await client.me()).project.id);\n const initRes = await client.request(endpoints.init, {\n method: 'POST',\n signal: options.signal,\n body: JSON.stringify({\n ...(projectId ? { projectId } : {}),\n filename: name,\n contentType: blob.type || contentTypeFor(name),\n bytes: blob.size,\n ...(options.assetId ? { assetId: options.assetId } : {}),\n ...(options.note?.trim() ? { note: options.note.trim() } : {}),\n ...endpoints.extra,\n }),\n });\n if (!initRes.ok) throw await apiErrorFromResponse(initRes);\n const init = (await initRes.json()) as UploadInit;\n try {\n return await transfer<F>(client, blob, init, options, endpoints);\n } catch (e) {\n await abortUpload(client, init, endpoints);\n throw e;\n }\n}\n\nasync function transfer<F>(\n client: GLBoostClient,\n blob: Blob,\n init: UploadInit,\n options: UploadOptions,\n endpoints: UploadEndpoints,\n): Promise<UploadResult<F>> {\n const total = blob.size;\n const count = Math.max(1, Math.ceil(total / init.partSize));\n const transport =\n options.transport ??\n (typeof XMLHttpRequest === 'function' && options.onProgress ? 'xhr' : 'fetch');\n const token = encodeURIComponent(init.token);\n const parts: { partNumber: number; etag: string }[] = [];\n let completed = 0;\n options.onProgress?.({ loaded: 0, total });\n for (let i = 0; i < count; i++) {\n const chunk = blob.slice(i * init.partSize, Math.min(total, (i + 1) * init.partSize));\n const url = `${init.uploadUrl}/part?n=${i + 1}&token=${token}`;\n const report = (loaded: number) =>\n options.onProgress?.({ loaded: Math.min(total, completed + loaded), total });\n const part =\n transport === 'xhr'\n ? await putPartXhr(url, chunk, report)\n : await putPartFetch(client, url, chunk, options.signal);\n completed += chunk.size;\n options.onProgress?.({ loaded: completed, total });\n parts.push(part);\n }\n\n const done = await client.fetch(`${init.uploadUrl}/complete?token=${token}`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ parts }),\n signal: options.signal,\n });\n if (!done.ok) throw new Error(`complete failed: ${await readError(done)}`);\n\n const finish = await client.request(endpoints.finish(init.versionId), {\n method: 'POST',\n signal: options.signal,\n });\n if (!finish.ok) throw await apiErrorFromResponse(finish);\n return {\n assetId: init.assetId,\n versionId: init.versionId,\n finish: (await finish.json().catch(() => null)) as F | null,\n };\n}\n\nasync function putPartFetch(\n client: GLBoostClient,\n url: string,\n chunk: Blob,\n signal?: AbortSignal,\n): Promise<{ partNumber: number; etag: string }> {\n const res = await client.fetch(url, { method: 'PUT', body: chunk, signal });\n if (!res.ok) throw new Error(`part upload failed: ${res.status} ${await readError(res)}`);\n return (await res.json()) as { partNumber: number; etag: string };\n}\n\nfunction putPartXhr(\n url: string,\n chunk: Blob,\n onProgress: (loaded: number) => void,\n): Promise<{ partNumber: number; etag: string }> {\n return new Promise((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhr.open('PUT', url);\n xhr.upload.onprogress = (e) => onProgress(e.loaded);\n xhr.onload = () => {\n if (xhr.status >= 200 && xhr.status < 300) {\n resolve(JSON.parse(xhr.responseText) as { partNumber: number; etag: string });\n } else reject(new Error(`part upload failed: ${xhr.status} ${xhr.responseText}`));\n };\n xhr.onerror = () => reject(new Error('part upload failed: network error'));\n xhr.send(chunk);\n });\n}\n\n/** Drops the multipart upload on the CDN and marks the version failed in the app; failures are swallowed. */\nasync function abortUpload(\n client: GLBoostClient,\n init: UploadInit,\n endpoints: UploadEndpoints,\n): Promise<void> {\n await Promise.allSettled([\n client.fetch(`${init.uploadUrl}/abort?token=${encodeURIComponent(init.token)}`, {\n method: 'POST',\n }),\n endpoints.abort ? client.request(endpoints.abort(init.versionId), { method: 'POST' }) : null,\n ]);\n}\n\nasync function readError(res: Response): Promise<string> {\n try {\n const body = (await res.json()) as { error?: string };\n return body.error ?? `${res.status}`;\n } catch {\n return `${res.status}`;\n }\n}\n\nfunction toNamedBlob(input: UploadInput): { name: string; blob: Blob } {\n if (input instanceof Blob) {\n const name = (input as Partial<File>).name;\n if (!name) throw new TypeError('uploadModel: a Blob needs a name; pass { name, data }');\n return { name, blob: input };\n }\n if (!input.name) throw new TypeError('uploadModel: name is required');\n const { name, data } = input;\n const type = input.type ?? (data instanceof Blob ? data.type : '') ?? '';\n if (data instanceof Blob) return { name, blob: input.type ? new Blob([data], { type }) : data };\n return { name, blob: new Blob([data as unknown as BlobPart], { type }) };\n}\n\n/** By extension; the app decides the format from the filename anyway. */\nexport function contentTypeFor(filename: string): string {\n const lower = filename.toLowerCase();\n if (lower.endsWith('.glb')) return 'model/gltf-binary';\n if (lower.endsWith('.gltf')) return 'model/gltf+json';\n if (lower.endsWith('.zip')) return 'application/zip';\n return 'application/octet-stream';\n}\n","import {\n canonicalSpec,\n deliveryQueryForSpec,\n isPresetName,\n presetSpec,\n type PresetName,\n} from '@glboost/core';\n\nexport const DEFAULT_CDN_BASE = 'https://cdn.glboost.com';\n\nexport interface AssetUrlOptions {\n /** CDN origin (default `https://cdn.glboost.com`). */\n base?: string;\n /** Named preset (default `compat`); ignored when `spec` is given. */\n preset?: PresetName | string;\n /** Canonical spec string, e.g. `anim=keep,geo=meshopt,maxtex=1024,q=low,tex=ktx2`. */\n spec?: string;\n /** Pin a version id (`v=`). */\n v?: string;\n /** Ask for a download (`dl=1`). */\n dl?: boolean;\n}\n\n/** The public delivery URL of an asset variant: `https://cdn.glboost.com/<id>.glb?p=gpu`. */\nexport function assetUrl(assetId: string, options: AssetUrlOptions = {}): string {\n const base = (options.base ?? DEFAULT_CDN_BASE).replace(/\\/+$/, '');\n const query = options.spec\n ? deliveryQueryForSpec(options.spec, null)\n : presetQuery(options.preset ?? 'compat');\n if (options.v) query.set('v', options.v);\n if (options.dl) query.set('dl', '1');\n return `${base}/${assetId}.glb?${query.toString()}`;\n}\n\n/** `https://cdn.glboost.com/<id>/config.json`: what the viewer reads before loading. */\nexport function configUrl(assetId: string, base: string = DEFAULT_CDN_BASE): string {\n return `${base.replace(/\\/+$/, '')}/${assetId}/config.json`;\n}\n\nfunction presetQuery(preset: string): URLSearchParams {\n if (!isPresetName(preset)) throw new Error(`unknown preset \"${preset}\"`);\n const spec = presetSpec(preset);\n return deliveryQueryForSpec(spec ? canonicalSpec(spec) : 'original', preset);\n}\n","import { unwrap, type GLBoostClient } from './client';\nimport { GLBoostTimeoutError } from './errors';\nimport type { paths } from './openapi';\n\nexport type VariantsResponse =\n paths['/api/v1/assets/{assetId}/variants']['get']['responses'][200]['content']['application/json'];\nexport type VariantRow = VariantsResponse['versions'][number]['variants'][number];\n\nexport interface WaitOptions {\n /** A version to watch; default: the newest. */\n versionId?: string;\n /** Give up after this long (default 10 minutes). */\n timeoutMs?: number;\n /** Poll interval (default 5 s). */\n intervalMs?: number;\n signal?: AbortSignal;\n /** Called after every poll with the state so far. */\n onPoll?: (state: ReadyState) => void;\n}\n\nexport interface ReadyState {\n versionId: string;\n number: number;\n variants: VariantRow[];\n /** Still `queued` or `processing`. */\n pending: VariantRow[];\n failed: VariantRow[];\n}\n\nconst PENDING = new Set(['queued', 'processing']);\n\n/**\n * Polls `GET /api/v1/assets/{assetId}/variants` until no variant of the version is queued or\n * processing. Failed variants do not throw: they come back under `failed`.\n */\nexport async function waitForReady(\n client: GLBoostClient,\n assetId: string,\n options: WaitOptions = {},\n): Promise<ReadyState> {\n const timeoutMs = options.timeoutMs ?? 600_000;\n const intervalMs = options.intervalMs ?? 5_000;\n const deadline = Date.now() + timeoutMs;\n for (;;) {\n const data = await unwrap(\n client.api.GET('/api/v1/assets/{assetId}/variants', {\n params: { path: { assetId } },\n signal: options.signal,\n }),\n );\n const version = options.versionId\n ? data.versions.find((v) => v.id === options.versionId)\n : data.versions.reduce<VariantsResponse['versions'][number] | undefined>(\n (best, v) => (best === undefined || v.number > best.number ? v : best),\n undefined,\n );\n if (!version) {\n throw new Error(\n options.versionId\n ? `version ${options.versionId} not found on asset ${assetId}`\n : `asset ${assetId} has no versions`,\n );\n }\n const state: ReadyState = {\n versionId: version.id,\n number: version.number,\n variants: version.variants,\n pending: version.variants.filter((v) => PENDING.has(v.status)),\n failed: version.variants.filter((v) => v.status === 'failed'),\n };\n options.onPoll?.(state);\n if (state.pending.length === 0) return state;\n if (Date.now() + intervalMs > deadline) {\n throw new GLBoostTimeoutError(assetId, version.id, timeoutMs);\n }\n await sleep(intervalMs, options.signal);\n }\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(signal.reason instanceof Error ? signal.reason : new Error('aborted'));\n return;\n }\n const onAbort = () => {\n clearTimeout(timer);\n reject(signal?.reason instanceof Error ? signal.reason : new Error('aborted'));\n };\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n"],"mappings":"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","names":["request","name"]}
@@ -0,0 +1,126 @@
1
+ /**
2
+ * An in-memory stand-in for the GLBoost API and the CDN's upload endpoints, for tests of code built
3
+ * on `@glboost/sdk` (the SDK's and the CLI's own tests run against it). Pass `fetch` to
4
+ * `createClient({ fetch })`. It speaks the same routes and shapes as the real API for: `/me`,
5
+ * `/projects`, `/assets` (list, get, delete), `/assets/:id/variants`, `/assets/:id/sign`,
6
+ * `/assets/:id/exports`, `/shares`, the upload flow, and `${cdnBase}/_/upload/*`.
7
+ */
8
+ interface FakeApiOptions {
9
+ /** API origin (default `https://api.test`). */
10
+ baseUrl?: string;
11
+ /** CDN origin the upload target points at (default `https://cdn.test`). */
12
+ cdnBase?: string;
13
+ plan?: 'free' | 'pro' | 'business';
14
+ project?: {
15
+ id: string;
16
+ name: string;
17
+ slug?: string;
18
+ };
19
+ /** Part size the upload target advertises (default 25 MiB, like the real edge). */
20
+ partSize?: number;
21
+ /** Variants turn `ready` once a version was polled this many times (default 1). */
22
+ readyAfterPolls?: number;
23
+ /** A preset whose variant fails instead of becoming ready. */
24
+ failPreset?: string | null;
25
+ /** Make `POST /uploads` answer a plan limit (402) or size limit (413) with the real body shape. */
26
+ refuse?: {
27
+ status: 402 | 413;
28
+ error: string;
29
+ hint?: string;
30
+ used?: number;
31
+ limit?: number;
32
+ } | null;
33
+ /** Require `Authorization: Bearer glb_…` on API routes (default: when `baseUrl` is set). */
34
+ requireAuth?: boolean;
35
+ }
36
+ interface FakeVariant {
37
+ id: string;
38
+ versionId: string;
39
+ spec: string;
40
+ preset: string | null;
41
+ requested: null;
42
+ status: 'queued' | 'processing' | 'ready' | 'failed';
43
+ bytes: number | null;
44
+ stats: null;
45
+ error: string | null;
46
+ createdAt: string;
47
+ readyAt: string | null;
48
+ }
49
+ interface FakeVersion {
50
+ id: string;
51
+ assetId: string;
52
+ number: number;
53
+ status: 'uploading' | 'uploaded' | 'processing' | 'ready' | 'failed';
54
+ originalBytes: number | null;
55
+ sourceFormat: 'glb' | 'gltf' | 'zip';
56
+ note: string | null;
57
+ createdAt: string;
58
+ variants: FakeVariant[];
59
+ /** Internal: how often this version's variants were read. */
60
+ polls: number;
61
+ }
62
+ interface FakeAsset {
63
+ id: string;
64
+ projectId: string;
65
+ name: string;
66
+ description: string | null;
67
+ visibility: 'public' | 'private';
68
+ tags: string[];
69
+ createdAt: string;
70
+ deletedAt: string | null;
71
+ versions: FakeVersion[];
72
+ }
73
+ interface FakeUpload {
74
+ uploadId: string;
75
+ token: string;
76
+ assetId: string;
77
+ versionId: string;
78
+ maxBytes: number;
79
+ parts: Map<number, {
80
+ etag: string;
81
+ size: number;
82
+ }>;
83
+ completed: number | null;
84
+ aborted: boolean;
85
+ }
86
+ interface FakeApiState {
87
+ assets: Map<string, FakeAsset>;
88
+ uploads: Map<string, FakeUpload>;
89
+ shares: Record<string, unknown>[];
90
+ exports: Record<string, unknown>[];
91
+ signed: Record<string, unknown>[];
92
+ /** Every request seen, oldest first. */
93
+ requests: {
94
+ method: string;
95
+ path: string;
96
+ }[];
97
+ }
98
+ interface SeedAssetInput {
99
+ id?: string;
100
+ name: string;
101
+ description?: string | null;
102
+ tags?: string[];
103
+ /** Original size of every version (default 1 MB). */
104
+ bytes?: number;
105
+ /** How many versions to create (default 1). */
106
+ versions?: number;
107
+ /** Variants start ready (default) or queued, to exercise polling. */
108
+ ready?: boolean;
109
+ deleted?: boolean;
110
+ }
111
+ interface FakeApi {
112
+ fetch: typeof fetch;
113
+ state: FakeApiState;
114
+ readonly baseUrl: string;
115
+ readonly cdnBase: string;
116
+ readonly project: {
117
+ id: string;
118
+ name: string;
119
+ slug: string;
120
+ };
121
+ seedAsset(input: SeedAssetInput): FakeAsset;
122
+ }
123
+ declare function createFakeApi(options?: FakeApiOptions): FakeApi;
124
+
125
+ export { createFakeApi };
126
+ export type { FakeApi, FakeApiOptions, FakeApiState, FakeAsset, FakeVariant, FakeVersion, SeedAssetInput };