@nitida/sdk 0.20.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/AGENTS.md +48 -0
- package/README.md +1158 -0
- package/dist/expo.d.ts +48 -0
- package/dist/expo.js +25 -0
- package/dist/expo.js.map +1 -0
- package/dist/index.d.ts +790 -0
- package/dist/index.js +802 -0
- package/dist/index.js.map +1 -0
- package/dist/native.d.ts +1 -0
- package/dist/native.js +16 -0
- package/dist/native.js.map +1 -0
- package/dist/react.d.ts +47 -0
- package/dist/react.js +96 -0
- package/dist/react.js.map +1 -0
- package/dist/server.d.ts +72 -0
- package/dist/server.js +808 -0
- package/dist/server.js.map +1 -0
- package/dist/web.d.ts +147 -0
- package/dist/web.js +864 -0
- package/dist/web.js.map +1 -0
- package/package.json +122 -0
- package/skills/nitida-sdk/SKILL.md +306 -0
- package/src/expo/index.ts +76 -0
- package/src/index.ts +1562 -0
- package/src/native/index.ts +48 -0
- package/src/react/index.ts +169 -0
- package/src/server/index.ts +124 -0
- package/src/web/index.ts +334 -0
package/dist/web.js
ADDED
|
@@ -0,0 +1,864 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import {
|
|
3
|
+
configureSlotResolver,
|
|
4
|
+
getAssetSrcSet,
|
|
5
|
+
getAssetUrl,
|
|
6
|
+
getHlsStreamingUrl,
|
|
7
|
+
getSignedTransformUrl,
|
|
8
|
+
getTransformSrcSet,
|
|
9
|
+
getTransformUrl,
|
|
10
|
+
getVideoTransformUrl,
|
|
11
|
+
hasPreset,
|
|
12
|
+
invalidateSlotCache,
|
|
13
|
+
resolveSlot,
|
|
14
|
+
resolveSlots,
|
|
15
|
+
setCdnBase,
|
|
16
|
+
setTenantId
|
|
17
|
+
} from "@nitida/asset-client";
|
|
18
|
+
import {
|
|
19
|
+
computeVariantDimensions,
|
|
20
|
+
extractAssetSha,
|
|
21
|
+
getAssetDimensions,
|
|
22
|
+
getAssetSrcSet as getAssetSrcSet2,
|
|
23
|
+
getAssetUrl as getAssetUrl2,
|
|
24
|
+
getHlsStreamingUrl as getHlsStreamingUrl2,
|
|
25
|
+
getSignedTransformUrl as getSignedTransformUrl2,
|
|
26
|
+
getTenantId,
|
|
27
|
+
getTransformSrcSet as getTransformSrcSet2,
|
|
28
|
+
getTransformUrl as getTransformUrl2,
|
|
29
|
+
getVideoTransformUrl as getVideoTransformUrl2,
|
|
30
|
+
hasPreset as hasPreset2,
|
|
31
|
+
serializeTransform,
|
|
32
|
+
setTenantId as setTenantId2,
|
|
33
|
+
signTransformUrl
|
|
34
|
+
} from "@nitida/asset-client";
|
|
35
|
+
function endpointUrl(opts, path, searchParams) {
|
|
36
|
+
const endpoint = opts.endpoint.replace(/\/+$/, "");
|
|
37
|
+
const isAbsolute = /^https?:\/\//i.test(endpoint);
|
|
38
|
+
let base;
|
|
39
|
+
if (isAbsolute) {
|
|
40
|
+
base = `${endpoint}${path}`;
|
|
41
|
+
} else if (typeof window !== "undefined" && window.location?.origin) {
|
|
42
|
+
base = `${window.location.origin}${endpoint}${path}`;
|
|
43
|
+
} else {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`[@nitida/sdk] relative endpoint "${endpoint}" requires a browser; pass an absolute URL when using the SDK from Node/Bun.`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
const u = new URL(base);
|
|
49
|
+
if (searchParams) {
|
|
50
|
+
for (const [k, v] of Object.entries(searchParams)) {
|
|
51
|
+
if (v !== void 0 && v !== null && v !== "") {
|
|
52
|
+
u.searchParams.set(k, String(v));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return u;
|
|
57
|
+
}
|
|
58
|
+
function endpointHref(opts, path) {
|
|
59
|
+
return endpointUrl(opts, path).toString();
|
|
60
|
+
}
|
|
61
|
+
function authHeaders(opts) {
|
|
62
|
+
const h = {
|
|
63
|
+
// Caller-supplied headers first; `X-Tenant-Code` stays authoritative below.
|
|
64
|
+
...opts.headers,
|
|
65
|
+
"X-Tenant-Code": opts.tenantCode
|
|
66
|
+
};
|
|
67
|
+
if (opts.apiKey) h.Authorization = `Bearer ${opts.apiKey}`;
|
|
68
|
+
return h;
|
|
69
|
+
}
|
|
70
|
+
var SlotsApi = class {
|
|
71
|
+
constructor(opts) {
|
|
72
|
+
this.opts = opts;
|
|
73
|
+
}
|
|
74
|
+
opts;
|
|
75
|
+
/** Resolve one slot — returns `{slot, preset, url}` or `{slot: null, url: null}` when unbound. */
|
|
76
|
+
resolve(slotKey, options = {}) {
|
|
77
|
+
return resolveSlot(slotKey, options);
|
|
78
|
+
}
|
|
79
|
+
/** Bulk-resolve N slots in one HTTP round-trip. */
|
|
80
|
+
resolveMany(slotKeys, options = {}) {
|
|
81
|
+
return resolveSlots(slotKeys, options);
|
|
82
|
+
}
|
|
83
|
+
/** List slots for the tenant (admin). Optional prefix filter for tree views. */
|
|
84
|
+
async list(opts = {}) {
|
|
85
|
+
const u = endpointUrl(this.opts, "/slots", {
|
|
86
|
+
prefix: opts.prefix,
|
|
87
|
+
limit: opts.limit
|
|
88
|
+
});
|
|
89
|
+
const r = await fetch(u, { headers: this.headers() });
|
|
90
|
+
if (!r.ok) throw new Error(`slots list ${r.status}: ${await r.text()}`);
|
|
91
|
+
const body = await r.json();
|
|
92
|
+
return body.slots;
|
|
93
|
+
}
|
|
94
|
+
/** Bind / rebind a slot to an asset. Admin-only operation. */
|
|
95
|
+
async bind(slotKey, body) {
|
|
96
|
+
const r = await fetch(
|
|
97
|
+
endpointHref(this.opts, `/slots/${encodeURIComponent(slotKey)}`),
|
|
98
|
+
{
|
|
99
|
+
method: "PUT",
|
|
100
|
+
headers: { ...this.headers(), "Content-Type": "application/json" },
|
|
101
|
+
body: JSON.stringify(body)
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
if (!r.ok) throw new Error(`slot bind ${r.status}: ${await r.text()}`);
|
|
105
|
+
return await r.json();
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Recent bindings for a slot. Lets the admin audit who changed
|
|
109
|
+
* what and restore a previous binding without remembering the
|
|
110
|
+
* asset id. Default limit 20, max 100.
|
|
111
|
+
*/
|
|
112
|
+
async history(slotKey, opts = {}) {
|
|
113
|
+
const u = endpointUrl(
|
|
114
|
+
this.opts,
|
|
115
|
+
`/slots/${encodeURIComponent(slotKey)}/history`,
|
|
116
|
+
{ limit: opts.limit }
|
|
117
|
+
);
|
|
118
|
+
const r = await fetch(u, { headers: this.headers() });
|
|
119
|
+
if (!r.ok) throw new Error(`slots history ${r.status}: ${await r.text()}`);
|
|
120
|
+
const body = await r.json();
|
|
121
|
+
return body.history;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Restore the slot to a previous binding. Equivalent to
|
|
125
|
+
* `bind(key, { assetId: previous.assetId, action: "restore" })`
|
|
126
|
+
* — the audit row is tagged `restore` instead of `bind`.
|
|
127
|
+
*/
|
|
128
|
+
async restore(slotKey, args) {
|
|
129
|
+
const r = await fetch(
|
|
130
|
+
endpointHref(this.opts, `/slots/${encodeURIComponent(slotKey)}`),
|
|
131
|
+
{
|
|
132
|
+
method: "PUT",
|
|
133
|
+
headers: { ...this.headers(), "Content-Type": "application/json" },
|
|
134
|
+
body: JSON.stringify({ ...args, action: "restore" })
|
|
135
|
+
}
|
|
136
|
+
);
|
|
137
|
+
if (!r.ok) throw new Error(`slot restore ${r.status}: ${await r.text()}`);
|
|
138
|
+
return await r.json();
|
|
139
|
+
}
|
|
140
|
+
/** Remove a slot binding. The asset itself is left alone. */
|
|
141
|
+
async unbind(slotKey) {
|
|
142
|
+
const r = await fetch(
|
|
143
|
+
endpointHref(this.opts, `/slots/${encodeURIComponent(slotKey)}`),
|
|
144
|
+
{
|
|
145
|
+
method: "DELETE",
|
|
146
|
+
headers: this.headers()
|
|
147
|
+
}
|
|
148
|
+
);
|
|
149
|
+
if (!r.ok) throw new Error(`slot unbind ${r.status}: ${await r.text()}`);
|
|
150
|
+
return await r.json();
|
|
151
|
+
}
|
|
152
|
+
/** Invalidate the in-process cache after a slot rebind. */
|
|
153
|
+
invalidateCache(slotKey) {
|
|
154
|
+
invalidateSlotCache(slotKey);
|
|
155
|
+
}
|
|
156
|
+
headers() {
|
|
157
|
+
return authHeaders(this.opts);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
var AssetsApi = class {
|
|
161
|
+
constructor(opts) {
|
|
162
|
+
this.opts = opts;
|
|
163
|
+
}
|
|
164
|
+
opts;
|
|
165
|
+
/** Look up an asset by full sha256 (64 hex). Returns null on 404. */
|
|
166
|
+
async byHash(sha256) {
|
|
167
|
+
const r = await fetch(
|
|
168
|
+
endpointHref(this.opts, `/assets/by-hash/${sha256}`),
|
|
169
|
+
{ headers: this.headers() }
|
|
170
|
+
);
|
|
171
|
+
if (r.status === 404) return null;
|
|
172
|
+
if (!r.ok) throw new Error(`assets byHash ${r.status}: ${await r.text()}`);
|
|
173
|
+
return await r.json();
|
|
174
|
+
}
|
|
175
|
+
/** Bulk lookup by sha256s. */
|
|
176
|
+
async byHashes(hashes) {
|
|
177
|
+
const r = await fetch(endpointHref(this.opts, "/assets/by-hashes"), {
|
|
178
|
+
method: "POST",
|
|
179
|
+
headers: { ...this.headers(), "Content-Type": "application/json" },
|
|
180
|
+
body: JSON.stringify({ hashes })
|
|
181
|
+
});
|
|
182
|
+
if (!r.ok)
|
|
183
|
+
throw new Error(`assets byHashes ${r.status}: ${await r.text()}`);
|
|
184
|
+
return await r.json();
|
|
185
|
+
}
|
|
186
|
+
/** Paginated list of recent assets for the tenant. */
|
|
187
|
+
async list(opts = {}) {
|
|
188
|
+
const u = endpointUrl(this.opts, "/assets", {
|
|
189
|
+
limit: opts.limit,
|
|
190
|
+
cursor: opts.cursor,
|
|
191
|
+
include_deleted: opts.includeDeleted ? "true" : void 0
|
|
192
|
+
});
|
|
193
|
+
const r = await fetch(u, { headers: this.headers() });
|
|
194
|
+
if (!r.ok) throw new Error(`assets list ${r.status}: ${await r.text()}`);
|
|
195
|
+
return await r.json();
|
|
196
|
+
}
|
|
197
|
+
/** Full DTO for an asset (admin view — includes audit-only fields). */
|
|
198
|
+
async get(assetId) {
|
|
199
|
+
const r = await fetch(endpointHref(this.opts, `/assets/${assetId}`), {
|
|
200
|
+
headers: this.headers()
|
|
201
|
+
});
|
|
202
|
+
if (!r.ok) throw new Error(`asset get ${r.status}: ${await r.text()}`);
|
|
203
|
+
return await r.json();
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Slot bindings pointing at an asset. Use this before deleting an
|
|
207
|
+
* asset so the admin sees which storefront slots would suddenly
|
|
208
|
+
* resolve to nothing.
|
|
209
|
+
*/
|
|
210
|
+
async bindings(assetId) {
|
|
211
|
+
const r = await fetch(endpointHref(this.opts, `/assets/${assetId}/slots`), {
|
|
212
|
+
headers: this.headers()
|
|
213
|
+
});
|
|
214
|
+
if (!r.ok) throw new Error(`asset bindings ${r.status}: ${await r.text()}`);
|
|
215
|
+
const body = await r.json();
|
|
216
|
+
return body.slots;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Full variant list for an asset — preset, URL, dimensions, bytes.
|
|
220
|
+
* Stronger-typed wrapper around `get()` that exposes only the
|
|
221
|
+
* `variants` field with the proper `AssetVariant[]` shape.
|
|
222
|
+
*
|
|
223
|
+
* const v = await aq.assets.variants(logoId);
|
|
224
|
+
* v.map((x) => x.preset); // → ("thumb" | "sm" | … | "original")[]
|
|
225
|
+
*/
|
|
226
|
+
async variants(assetId) {
|
|
227
|
+
const dto = await this.get(assetId);
|
|
228
|
+
return Array.isArray(dto.variants) ? dto.variants : [];
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Add or rebuild variants on an existing asset. Image presets are
|
|
232
|
+
* MERGED with what's there — passing `{ presets: ["thumb"] }` adds
|
|
233
|
+
* the thumb variant without touching `lg`, `sm`, `original`, etc.
|
|
234
|
+
*
|
|
235
|
+
* // Day 0: upload original-only logo
|
|
236
|
+
* const { assetId } = await aq.upload(logoFile); // defaults to ["original"]
|
|
237
|
+
*
|
|
238
|
+
* // Day 7: need a thumb without re-uploading
|
|
239
|
+
* await aq.assets.regenerate(assetId, { presets: ["thumb"] });
|
|
240
|
+
*
|
|
241
|
+
* const after = await aq.assets.variants(assetId);
|
|
242
|
+
* after.map((v) => v.preset); // → ["original", "thumb"]
|
|
243
|
+
*
|
|
244
|
+
* Passing no presets re-runs the FULL default pipeline for that
|
|
245
|
+
* asset's kind (thumb+sm+md+lg for images, poster+video for video).
|
|
246
|
+
*
|
|
247
|
+
* If the asset was uploaded original-only and the cleanup job has
|
|
248
|
+
* already reaped `raw/`, the route falls back to reading the source
|
|
249
|
+
* bytes from `variants/o.<ext>` — no need to re-upload.
|
|
250
|
+
*
|
|
251
|
+
* Video presets are filtered to `["poster","video","aiproxy"]` and
|
|
252
|
+
* dispatched to the Cloud Run Job (the call returns immediately
|
|
253
|
+
* with a dispatch handle; poll `aq.assets.get(id).status` for
|
|
254
|
+
* completion).
|
|
255
|
+
*/
|
|
256
|
+
async regenerate(assetId, opts = {}) {
|
|
257
|
+
const r = await fetch(
|
|
258
|
+
endpointHref(this.opts, `/assets/${assetId}/regenerate`),
|
|
259
|
+
{
|
|
260
|
+
method: "POST",
|
|
261
|
+
headers: { ...this.headers(), "Content-Type": "application/json" },
|
|
262
|
+
body: JSON.stringify(opts.presets ? { presets: opts.presets } : {})
|
|
263
|
+
}
|
|
264
|
+
);
|
|
265
|
+
if (!r.ok)
|
|
266
|
+
throw new Error(`asset regenerate ${r.status}: ${await r.text()}`);
|
|
267
|
+
return await r.json();
|
|
268
|
+
}
|
|
269
|
+
/** Merge metadata into an asset (role / slot / description / tags). */
|
|
270
|
+
async patchMetadata(assetId, metadata) {
|
|
271
|
+
const r = await fetch(endpointHref(this.opts, `/assets/${assetId}`), {
|
|
272
|
+
method: "PATCH",
|
|
273
|
+
headers: { ...this.headers(), "Content-Type": "application/json" },
|
|
274
|
+
body: JSON.stringify({ metadata })
|
|
275
|
+
});
|
|
276
|
+
if (!r.ok) throw new Error(`asset patch ${r.status}: ${await r.text()}`);
|
|
277
|
+
return await r.json();
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Request a presigned R2 PUT URL for direct browser-side uploads.
|
|
281
|
+
*
|
|
282
|
+
* Mirrors the first half of `aq.upload()` — the caller (typically a
|
|
283
|
+
* BFF / share-link dropzone) computes sha256 in the browser, then
|
|
284
|
+
* uploads bytes straight to R2 with the returned `upload.url`, then
|
|
285
|
+
* POSTs `process.body` to `/assets/process` (see {@link processAndWait})
|
|
286
|
+
* once R2 has the bytes.
|
|
287
|
+
*
|
|
288
|
+
* If the sha is already known to the tenant the server short-circuits
|
|
289
|
+
* with `{ deduped: true, asset }` — no PUT needed.
|
|
290
|
+
*
|
|
291
|
+
* @example The browser-direct flow, in full
|
|
292
|
+
* ```ts
|
|
293
|
+
* // SERVER (holds the amk_rt_* key — never the browser):
|
|
294
|
+
* const presign = await aq.assets.presignUploadUrl({ sha256, mime, bytes, fileName, presets });
|
|
295
|
+
* if (presign.deduped) return presign.asset; // those bytes already exist; none fly
|
|
296
|
+
*
|
|
297
|
+
* // BROWSER: PUT straight to presign.upload.url — the bytes never touch your server.
|
|
298
|
+
* // ⚠️ R2 answers that preflight ITSELF, so your origin must be in the BUCKET's CORS policy.
|
|
299
|
+
* // Symptom when it is not: "PUT failed: network error" with every earlier step green —
|
|
300
|
+
* // and it cannot be fixed in this SDK, in your app, or in `storefront_origins`.
|
|
301
|
+
*
|
|
302
|
+
* // SERVER again, forwarding presign.process.body VERBATIM:
|
|
303
|
+
* const asset = await aq.assets.processAndWait(presign.process.body, { timeoutMs: 300_000 });
|
|
304
|
+
* ```
|
|
305
|
+
*
|
|
306
|
+
* Works for images AND video. A video answers immediately with
|
|
307
|
+
* `{ assetId, status: "processing" }` while a Cloud Run Job transcodes, so
|
|
308
|
+
* give `processAndWait` a bigger `timeoutMs` (a transcode + HLS ladder runs
|
|
309
|
+
* 1–2 min; 300_000 is a sane floor).
|
|
310
|
+
*/
|
|
311
|
+
async presignUploadUrl(opts) {
|
|
312
|
+
const body = {
|
|
313
|
+
sha256: opts.sha256,
|
|
314
|
+
mime: opts.mime,
|
|
315
|
+
bytes: opts.bytes,
|
|
316
|
+
fileName: opts.fileName
|
|
317
|
+
};
|
|
318
|
+
if (opts.presets && opts.presets.length > 0) body.presets = opts.presets;
|
|
319
|
+
if (opts.clientOriginalBytes != null)
|
|
320
|
+
body.clientOriginalBytes = opts.clientOriginalBytes;
|
|
321
|
+
if (opts.video != null) body.video = opts.video;
|
|
322
|
+
const r = await fetch(endpointHref(this.opts, "/assets/upload-url"), {
|
|
323
|
+
method: "POST",
|
|
324
|
+
headers: { ...this.headers(), "Content-Type": "application/json" },
|
|
325
|
+
body: JSON.stringify(body)
|
|
326
|
+
});
|
|
327
|
+
if (!r.ok) throw new Error(`upload-url ${r.status}: ${await r.text()}`);
|
|
328
|
+
return await r.json();
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Dispatch `/assets/process` with the body returned by a prior
|
|
332
|
+
* {@link presignUploadUrl} call, then poll until the asset transitions
|
|
333
|
+
* to `ready` or `failed`. Throws on `failed` or timeout.
|
|
334
|
+
*
|
|
335
|
+
* Use this when bytes were uploaded directly from the browser to R2 —
|
|
336
|
+
* `aq.upload()` already does presign + PUT + process + wait in one
|
|
337
|
+
* step when the server holds the bytes.
|
|
338
|
+
*/
|
|
339
|
+
async processAndWait(processBody, opts = {}) {
|
|
340
|
+
const r = await fetch(endpointHref(this.opts, "/assets/process"), {
|
|
341
|
+
method: "POST",
|
|
342
|
+
headers: { ...this.headers(), "Content-Type": "application/json" },
|
|
343
|
+
body: JSON.stringify(processBody)
|
|
344
|
+
});
|
|
345
|
+
if (!r.ok) throw new Error(`process ${r.status}: ${await r.text()}`);
|
|
346
|
+
const proc = await r.json();
|
|
347
|
+
if (!proc.assetId) throw new Error("process returned no assetId");
|
|
348
|
+
const final = await this.waitReady(proc.assetId, opts.timeoutMs);
|
|
349
|
+
if (final.status === "failed") {
|
|
350
|
+
throw new Error(
|
|
351
|
+
`processAndWait: asset ${proc.assetId} ended status=failed`
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
return final;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Poll `GET /assets/:id` until the asset transitions to `ready` or
|
|
358
|
+
* `failed`. Returns the final DTO (whether ready OR failed — callers
|
|
359
|
+
* decide whether to throw on `failed`). Throws on timeout.
|
|
360
|
+
*
|
|
361
|
+
* Default timeout is 5 minutes; videos / HLS ladders may need a
|
|
362
|
+
* higher cap (pass `10 * 60_000` for compositions, transcodes).
|
|
363
|
+
*/
|
|
364
|
+
async waitReady(assetId, timeoutMs = 5 * 6e4) {
|
|
365
|
+
const start = Date.now();
|
|
366
|
+
let delay = 500;
|
|
367
|
+
let last = null;
|
|
368
|
+
while (Date.now() - start < timeoutMs) {
|
|
369
|
+
last = await this.get(assetId);
|
|
370
|
+
if (last.status === "ready" || last.status === "failed") return last;
|
|
371
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
372
|
+
delay = Math.min(delay * 1.5, 5e3);
|
|
373
|
+
}
|
|
374
|
+
if (!last) throw new Error(`waitReady: no asset ${assetId}`);
|
|
375
|
+
throw new Error(`waitReady timeout for ${assetId}`);
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Dispatch `POST /assets/compose-marketing` to stitch pre-uploaded clip
|
|
379
|
+
* segments into a single MP4 composition. Returns the processing asset
|
|
380
|
+
* id immediately — does NOT block on completion. Callers poll via
|
|
381
|
+
* {@link waitReady} (typical timeout: 10 min for multi-segment kits).
|
|
382
|
+
*
|
|
383
|
+
* Tenant scope is inherited from the SDK client; `tenantCode` is added
|
|
384
|
+
* to the request body so the Cloud Run Job can resolve it without
|
|
385
|
+
* re-reading the header.
|
|
386
|
+
*/
|
|
387
|
+
async composeMarketing(opts) {
|
|
388
|
+
const r = await fetch(
|
|
389
|
+
endpointHref(this.opts, "/assets/compose-marketing"),
|
|
390
|
+
{
|
|
391
|
+
method: "POST",
|
|
392
|
+
headers: { ...this.headers(), "Content-Type": "application/json" },
|
|
393
|
+
body: JSON.stringify({
|
|
394
|
+
tenantCode: this.opts.tenantCode,
|
|
395
|
+
marketingKitId: opts.marketingKitId,
|
|
396
|
+
segments: opts.segments,
|
|
397
|
+
composition: opts.composition ?? {}
|
|
398
|
+
})
|
|
399
|
+
}
|
|
400
|
+
);
|
|
401
|
+
if (!r.ok) {
|
|
402
|
+
throw new Error(`compose-marketing ${r.status}: ${await r.text()}`);
|
|
403
|
+
}
|
|
404
|
+
const body = await r.json();
|
|
405
|
+
const assetId = body.asset?.id ?? body.assetId;
|
|
406
|
+
const status = body.asset?.status ?? body.status ?? "processing";
|
|
407
|
+
if (!assetId) {
|
|
408
|
+
throw new Error("compose-marketing: response missing assetId");
|
|
409
|
+
}
|
|
410
|
+
return { assetId, status };
|
|
411
|
+
}
|
|
412
|
+
headers() {
|
|
413
|
+
return authHeaders(this.opts);
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
var MIME_BY_EXT = {
|
|
417
|
+
webp: "image/webp",
|
|
418
|
+
jpg: "image/jpeg",
|
|
419
|
+
jpeg: "image/jpeg",
|
|
420
|
+
png: "image/png",
|
|
421
|
+
gif: "image/gif",
|
|
422
|
+
avif: "image/avif",
|
|
423
|
+
heic: "image/heic",
|
|
424
|
+
heif: "image/heif",
|
|
425
|
+
svg: "image/svg+xml",
|
|
426
|
+
bmp: "image/bmp",
|
|
427
|
+
tiff: "image/tiff",
|
|
428
|
+
mp4: "video/mp4",
|
|
429
|
+
webm: "video/webm",
|
|
430
|
+
mov: "video/quicktime",
|
|
431
|
+
m4v: "video/x-m4v",
|
|
432
|
+
pdf: "application/pdf"
|
|
433
|
+
};
|
|
434
|
+
function mimeFromFileName(fileName) {
|
|
435
|
+
if (!fileName) return null;
|
|
436
|
+
const ext = fileName.split(".").pop()?.toLowerCase();
|
|
437
|
+
return ext ? MIME_BY_EXT[ext] ?? null : null;
|
|
438
|
+
}
|
|
439
|
+
var DEFAULT_UPLOAD_PRESETS = ["original"];
|
|
440
|
+
async function computeSha256(bytes) {
|
|
441
|
+
const buf = bytes instanceof Blob ? await bytes.arrayBuffer() : bytes instanceof Uint8Array ? bytes.buffer : bytes;
|
|
442
|
+
const digest = await crypto.subtle.digest("SHA-256", buf);
|
|
443
|
+
return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
444
|
+
}
|
|
445
|
+
var UsageApi = class {
|
|
446
|
+
constructor(opts) {
|
|
447
|
+
this.opts = opts;
|
|
448
|
+
}
|
|
449
|
+
opts;
|
|
450
|
+
/** Snapshot for the active tenant — storage + today + last 30 days totals. */
|
|
451
|
+
async snapshot() {
|
|
452
|
+
const r = await fetch(endpointHref(this.opts, "/usage"), {
|
|
453
|
+
headers: this.headers()
|
|
454
|
+
});
|
|
455
|
+
if (!r.ok) throw new Error(`usage snapshot ${r.status}: ${await r.text()}`);
|
|
456
|
+
return await r.json();
|
|
457
|
+
}
|
|
458
|
+
/** Daily rollup for charts — 1..365 days, default 30. */
|
|
459
|
+
async timeseries(days = 30) {
|
|
460
|
+
const r = await fetch(
|
|
461
|
+
endpointUrl(this.opts, "/usage/timeseries", { days }),
|
|
462
|
+
{ headers: this.headers() }
|
|
463
|
+
);
|
|
464
|
+
if (!r.ok)
|
|
465
|
+
throw new Error(`usage timeseries ${r.status}: ${await r.text()}`);
|
|
466
|
+
return await r.json();
|
|
467
|
+
}
|
|
468
|
+
/** Per-API-key breakdown for the current month. */
|
|
469
|
+
async keys() {
|
|
470
|
+
const r = await fetch(endpointHref(this.opts, "/usage/keys"), {
|
|
471
|
+
headers: this.headers()
|
|
472
|
+
});
|
|
473
|
+
if (!r.ok) throw new Error(`usage keys ${r.status}: ${await r.text()}`);
|
|
474
|
+
return await r.json();
|
|
475
|
+
}
|
|
476
|
+
headers() {
|
|
477
|
+
return authHeaders(this.opts);
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
var AquienpzClient = class {
|
|
481
|
+
slots;
|
|
482
|
+
assets;
|
|
483
|
+
usage;
|
|
484
|
+
/**
|
|
485
|
+
* Effective options — read-only. Exposed so the `/web` and `/expo`
|
|
486
|
+
* subpaths can inherit endpoint / apiKey / tenant scope from the
|
|
487
|
+
* configured client without re-passing them per call site.
|
|
488
|
+
*/
|
|
489
|
+
opts;
|
|
490
|
+
constructor(opts) {
|
|
491
|
+
this.opts = opts;
|
|
492
|
+
const cdn = opts.cdnBase ?? "https://8ok.uk";
|
|
493
|
+
setCdnBase(cdn);
|
|
494
|
+
setTenantId(opts.tenantId);
|
|
495
|
+
configureSlotResolver({
|
|
496
|
+
endpoint: opts.endpoint,
|
|
497
|
+
apiKey: opts.apiKey,
|
|
498
|
+
tenantCode: opts.tenantCode
|
|
499
|
+
});
|
|
500
|
+
this.slots = new SlotsApi(opts);
|
|
501
|
+
this.assets = new AssetsApi(opts);
|
|
502
|
+
this.usage = new UsageApi(opts);
|
|
503
|
+
}
|
|
504
|
+
/** Tenant id as base36 path segment (e.g. tenantId=4 → "4/v/"). */
|
|
505
|
+
get tenantSegment() {
|
|
506
|
+
return `${this.opts.tenantId.toString(36)}/v/`;
|
|
507
|
+
}
|
|
508
|
+
/** Build the canonical CDN URL deterministically from sha + preset. */
|
|
509
|
+
urlFor(asset, preset = "lg") {
|
|
510
|
+
return getAssetUrl(asset, preset);
|
|
511
|
+
}
|
|
512
|
+
/** Build a responsive srcSet across the available image presets. */
|
|
513
|
+
srcSetFor(asset) {
|
|
514
|
+
return getAssetSrcSet(asset);
|
|
515
|
+
}
|
|
516
|
+
transform(asset, opts = {}, signOpts) {
|
|
517
|
+
if (!signOpts?.sign) {
|
|
518
|
+
return getTransformUrl(asset, opts) ?? this.urlFor(asset, "lg");
|
|
519
|
+
}
|
|
520
|
+
if (!this.opts.signingKey) {
|
|
521
|
+
throw new Error(
|
|
522
|
+
"aq.transform({ sign: true }) requires `signingKey` in AquienpzClientOptions. Pull the tenant's signing key from /admin/tenants/:id and pass it to the SDK constructor on a SERVER-side instance only."
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
return getSignedTransformUrl(asset, opts, this.opts.signingKey) ?? Promise.resolve(this.urlFor(asset, "lg"));
|
|
526
|
+
}
|
|
527
|
+
transformSrcSet(asset, widths, extraOpts = {}, signOpts) {
|
|
528
|
+
if (!signOpts?.sign) return getTransformSrcSet(asset, widths, extraOpts);
|
|
529
|
+
if (!this.opts.signingKey) {
|
|
530
|
+
throw new Error(
|
|
531
|
+
"aq.transformSrcSet({ sign: true }) requires `signingKey` in AquienpzClientOptions."
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
const key = this.opts.signingKey;
|
|
535
|
+
return Promise.all(
|
|
536
|
+
widths.map(async (w) => {
|
|
537
|
+
const signed = await getSignedTransformUrl(
|
|
538
|
+
asset,
|
|
539
|
+
{ ...extraOpts, width: w },
|
|
540
|
+
key
|
|
541
|
+
);
|
|
542
|
+
return signed ? `${signed} ${w}w` : null;
|
|
543
|
+
})
|
|
544
|
+
).then((parts) => parts.filter((s) => s != null).join(", "));
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Build an on-the-fly VIDEO transform URL — Phase 4.
|
|
548
|
+
*
|
|
549
|
+
* Same DSL shape as `transform()` but the URL has a `.mp4` (default)
|
|
550
|
+
* or `.webm` extension and the server routes the request to a Cloud
|
|
551
|
+
* Run Job for ffmpeg encoding (vs the inline sharp pipeline for
|
|
552
|
+
* images).
|
|
553
|
+
*
|
|
554
|
+
* On the first request the route returns **202 Accepted** with
|
|
555
|
+
* `Retry-After: 10` while the Job runs (typically 5-30 s for a
|
|
556
|
+
* short clip). The response body includes `outputUrl` which is the
|
|
557
|
+
* eventual CDN URL — poll the same transform URL after the
|
|
558
|
+
* retry-after window to get a 302 redirect to it.
|
|
559
|
+
*
|
|
560
|
+
* const url = aq.transformVideo(asset, {
|
|
561
|
+
* width: 1080, height: 1920, fit: "cover",
|
|
562
|
+
* start: 0, duration: 15,
|
|
563
|
+
* });
|
|
564
|
+
* // Pass to Video.js / <video src={url}>; on the first load it
|
|
565
|
+
* // gets 202 + body.outputUrl; subsequent loads hit cache → 302.
|
|
566
|
+
*
|
|
567
|
+
* Video-specific DSL params:
|
|
568
|
+
* - `start` (seconds, decimal OK)
|
|
569
|
+
* - `duration` (seconds, 1..300)
|
|
570
|
+
* - `format`: "mp4" (default) or "webm"
|
|
571
|
+
*
|
|
572
|
+
* The other params (`width`, `height`, `fit`) work identically to
|
|
573
|
+
* image transforms. `gravity`, `quality`, `effect`, `dpr` are
|
|
574
|
+
* accepted by the DSL but currently ignored on the video path.
|
|
575
|
+
*/
|
|
576
|
+
transformVideo(asset, opts = {}) {
|
|
577
|
+
return getVideoTransformUrl(asset, opts) ?? this.urlFor(asset, "lg");
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Build the HLS master playlist URL for a VIDEO asset (Phase 5).
|
|
581
|
+
*
|
|
582
|
+
* Returns `<cdn>/t/format=hls(,start=…,duration=…)/<sha>.m3u8`. Pass
|
|
583
|
+
* to an HLS-aware player:
|
|
584
|
+
*
|
|
585
|
+
* <video
|
|
586
|
+
* src={aq.streamingUrl(asset)}
|
|
587
|
+
* controls playsInline
|
|
588
|
+
* // Video.js v10's @videojs/http-streaming ships native HLS —
|
|
589
|
+
* // no plugin needed.
|
|
590
|
+
* />
|
|
591
|
+
*
|
|
592
|
+
* On the first request the server returns **202 Accepted** while a
|
|
593
|
+
* Cloud Run Job builds the multi-rung ladder (typically 1-3 min for
|
|
594
|
+
* a 90 s source — five rungs of 240p/360p/480p/720p/1080p @ AAC).
|
|
595
|
+
* Subsequent requests hit the cache → **302** to the master.m3u8.
|
|
596
|
+
*
|
|
597
|
+
* Supports `start` + `duration` to ladder a sub-clip. Other DSL
|
|
598
|
+
* params (width, height, fit) are ignored on the HLS path because
|
|
599
|
+
* the rungs determine resolution.
|
|
600
|
+
*/
|
|
601
|
+
streamingUrl(asset, opts = {}) {
|
|
602
|
+
return getHlsStreamingUrl(asset, opts);
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Upload a file or raw bytes. Returns the new asset id + canonical
|
|
606
|
+
* URL. Hash-deduped — uploading the same bytes twice returns the
|
|
607
|
+
* existing asset.
|
|
608
|
+
*
|
|
609
|
+
* Browser-first: uses `Blob` + WebCrypto. For Node 20+, pass a
|
|
610
|
+
* Uint8Array and a precomputed `sha256` (since `crypto.subtle` works
|
|
611
|
+
* but isn't always available depending on the runtime).
|
|
612
|
+
*/
|
|
613
|
+
/**
|
|
614
|
+
* Upload bytes end to end: optional client compression → sha256 → presign → **direct-to-R2 PUT**
|
|
615
|
+
* → `/assets/process` → wait until the asset is ready.
|
|
616
|
+
*
|
|
617
|
+
* ⚠️ `presets` decides what exists FOREVER. Omit it and only `original` is written; ask for
|
|
618
|
+
* `["thumb"]` and the bytes you just uploaded are **not retrievable**. A variant not requested in
|
|
619
|
+
* this first ingest cannot be added later once the cleanup job reaps `raw/` — measured once as
|
|
620
|
+
* "97 files archived successfully, zero recoverable".
|
|
621
|
+
*
|
|
622
|
+
* @example Deliver an image on a site (the responsive ladder)
|
|
623
|
+
* ```ts
|
|
624
|
+
* import { AquienpzClient } from "@nitida/sdk/server";
|
|
625
|
+
*
|
|
626
|
+
* const aq = new AquienpzClient({ endpoint, apiKey, tenantCode, tenantId });
|
|
627
|
+
* const { assetId, sha256 } = await aq.upload(file, {
|
|
628
|
+
* fileName: file.name,
|
|
629
|
+
* presets: ["thumb", "sm", "md", "lg"],
|
|
630
|
+
* });
|
|
631
|
+
* ```
|
|
632
|
+
*
|
|
633
|
+
* @example ARCHIVE a file — you must ask for `original`
|
|
634
|
+
* ```ts
|
|
635
|
+
* await aq.upload(bytes, {
|
|
636
|
+
* fileName: "contrato.pdf",
|
|
637
|
+
* contentType: "application/pdf",
|
|
638
|
+
* presets: ["original"], // without this the bytes are unrecoverable
|
|
639
|
+
* });
|
|
640
|
+
* ```
|
|
641
|
+
*
|
|
642
|
+
* @example Raw bytes need an explicit MIME
|
|
643
|
+
* ```ts
|
|
644
|
+
* await aq.upload(bytes, { fileName: "track.mp3", contentType: "audio/mpeg" });
|
|
645
|
+
* // Without either, it stores as kind:"other" — no variants, and regenerate() is unsupported.
|
|
646
|
+
* ```
|
|
647
|
+
*
|
|
648
|
+
* @example Video — and what does NOT work there
|
|
649
|
+
* ```ts
|
|
650
|
+
* // `original` is accepted and then silently DROPPED: /assets/process filters video presets to
|
|
651
|
+
* // {poster, video, aiproxy, probe} before dispatching the transcode Job.
|
|
652
|
+
* await aq.upload(clip, { fileName: "tour.mp4", presets: ["poster", "video"] });
|
|
653
|
+
*
|
|
654
|
+
* // Omit `aiproxy`/`probe` unless the asset really goes to a vision model — they cost Job time
|
|
655
|
+
* // and permanent R2 objects that nothing else reads.
|
|
656
|
+
* ```
|
|
657
|
+
*/
|
|
658
|
+
async upload(input, opts = {}) {
|
|
659
|
+
const sourceIsBlob = input instanceof File || input instanceof Blob;
|
|
660
|
+
const sourceMime = (sourceIsBlob ? input.type : "") || opts.contentType || mimeFromFileName(opts.fileName) || "application/octet-stream";
|
|
661
|
+
if (!sourceIsBlob && sourceMime === "application/octet-stream") {
|
|
662
|
+
console.warn(
|
|
663
|
+
'[@nitida/sdk] upload(Uint8Array): no MIME resolved (no `contentType`, no recognizable `fileName` extension) \u2014 the asset will be stored as kind:"other" with NO image/video variants and regenerate() unsupported. Pass `contentType` or a `fileName` with an extension.'
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
let bytes;
|
|
667
|
+
let effectiveMime;
|
|
668
|
+
let clientOriginalBytes;
|
|
669
|
+
const wantCompression = !!opts.compress && sourceIsBlob && typeof window !== "undefined" && sourceMime.startsWith("image/");
|
|
670
|
+
if (wantCompression) {
|
|
671
|
+
const compressOpts = opts.compress === true ? {} : opts.compress;
|
|
672
|
+
const dynImport = new Function(
|
|
673
|
+
"base",
|
|
674
|
+
"return import(new URL('./' + 'web' + '.js', base).href)"
|
|
675
|
+
);
|
|
676
|
+
const { compressImage: compressImage2 } = await dynImport(import.meta.url);
|
|
677
|
+
const result = await compressImage2(input, compressOpts);
|
|
678
|
+
bytes = new Uint8Array(await result.blob.arrayBuffer());
|
|
679
|
+
clientOriginalBytes = result.originalBytes;
|
|
680
|
+
effectiveMime = result.blob.type || sourceMime;
|
|
681
|
+
} else {
|
|
682
|
+
if (opts.compress && !sourceIsBlob) {
|
|
683
|
+
console.warn(
|
|
684
|
+
"[@nitida/sdk] compress: true requires a File or Blob input; got Uint8Array \u2014 uploading raw bytes."
|
|
685
|
+
);
|
|
686
|
+
} else if (opts.compress && typeof window === "undefined") {
|
|
687
|
+
console.warn(
|
|
688
|
+
"[@nitida/sdk] compress: true is browser-only \u2014 uploading raw bytes."
|
|
689
|
+
);
|
|
690
|
+
} else if (opts.compress && !sourceMime.startsWith("image/")) {
|
|
691
|
+
}
|
|
692
|
+
bytes = input instanceof Uint8Array ? input : new Uint8Array(await input.arrayBuffer());
|
|
693
|
+
effectiveMime = sourceMime;
|
|
694
|
+
}
|
|
695
|
+
const sha = opts.sha256 ?? await computeSha256(bytes);
|
|
696
|
+
const mime = effectiveMime;
|
|
697
|
+
const fileName = opts.fileName ?? (input instanceof File ? input.name : `upload-${sha.slice(0, 8)}.bin`);
|
|
698
|
+
const existing = await this.assets.byHash(sha);
|
|
699
|
+
if (existing && existing.status === "ready") {
|
|
700
|
+
return {
|
|
701
|
+
assetId: existing.id,
|
|
702
|
+
sha256: sha,
|
|
703
|
+
cdnUrl: this.urlFor(existing, this.bestPresetForAsset(existing, mime))
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
const presign = await this.assets.presignUploadUrl({
|
|
707
|
+
sha256: sha,
|
|
708
|
+
mime,
|
|
709
|
+
bytes: bytes.byteLength,
|
|
710
|
+
fileName,
|
|
711
|
+
presets: opts.presets && opts.presets.length > 0 ? opts.presets : DEFAULT_UPLOAD_PRESETS,
|
|
712
|
+
...clientOriginalBytes != null && { clientOriginalBytes },
|
|
713
|
+
...opts.video != null && { video: opts.video }
|
|
714
|
+
});
|
|
715
|
+
if (presign.deduped) {
|
|
716
|
+
return {
|
|
717
|
+
assetId: presign.asset.id,
|
|
718
|
+
sha256: sha,
|
|
719
|
+
cdnUrl: this.urlFor(presign.asset, this.defaultPresetForMime(mime))
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
const putR = await fetch(presign.upload.url, {
|
|
723
|
+
method: "PUT",
|
|
724
|
+
headers: { "Content-Type": mime, ...presign.upload.headers ?? {} },
|
|
725
|
+
body: new Blob([bytes], { type: mime })
|
|
726
|
+
});
|
|
727
|
+
if (!putR.ok)
|
|
728
|
+
throw new Error(`R2 PUT ${putR.status}: ${await putR.text()}`);
|
|
729
|
+
const procR = await fetch(endpointHref(this.opts, presign.process.url), {
|
|
730
|
+
method: "POST",
|
|
731
|
+
headers: {
|
|
732
|
+
...authHeaders(this.opts),
|
|
733
|
+
"Content-Type": "application/json"
|
|
734
|
+
},
|
|
735
|
+
body: JSON.stringify(presign.process.body)
|
|
736
|
+
});
|
|
737
|
+
if (!procR.ok)
|
|
738
|
+
throw new Error(`process ${procR.status}: ${await procR.text()}`);
|
|
739
|
+
const proc = await procR.json();
|
|
740
|
+
let assetId = proc.assetId;
|
|
741
|
+
if (!assetId && proc.kind === "video") {
|
|
742
|
+
const start = Date.now();
|
|
743
|
+
let delay = 1e3;
|
|
744
|
+
while (Date.now() - start < 6e4) {
|
|
745
|
+
const dto = await this.assets.byHash(sha);
|
|
746
|
+
if (dto?.id) {
|
|
747
|
+
assetId = dto.id;
|
|
748
|
+
break;
|
|
749
|
+
}
|
|
750
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
751
|
+
delay = Math.min(delay * 1.5, 5e3);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
if (!assetId) throw new Error("upload: process returned no assetId");
|
|
755
|
+
const final = await this.assets.waitReady(assetId, opts.timeoutMs);
|
|
756
|
+
if (final.status !== "ready")
|
|
757
|
+
throw new Error(`upload: asset ended status=${final.status}`);
|
|
758
|
+
return {
|
|
759
|
+
assetId,
|
|
760
|
+
sha256: sha,
|
|
761
|
+
cdnUrl: this.urlFor(final, this.bestPresetForAsset(final, mime))
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
defaultPresetForMime(mime) {
|
|
765
|
+
if (mime.startsWith("video/")) return "video";
|
|
766
|
+
if (mime.startsWith("audio/")) return "original";
|
|
767
|
+
return "lg";
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Pick a sensible preset to build a URL for, given the asset's actual
|
|
771
|
+
* `presets` string. Falls back through the preference order
|
|
772
|
+
* lg → md → sm → thumb → original (for images)
|
|
773
|
+
* video → poster (for videos)
|
|
774
|
+
* mp3 → original (for audio)
|
|
775
|
+
* so an upload that was processed with e.g. `["original"]` still
|
|
776
|
+
* returns a non-404 URL in `aq.upload`'s result.
|
|
777
|
+
*/
|
|
778
|
+
bestPresetForAsset(asset, mime) {
|
|
779
|
+
const order = mime.startsWith("video/") ? ["video", "poster"] : mime.startsWith("audio/") ? ["mp3", "original"] : ["lg", "md", "sm", "thumb", "xl", "original"];
|
|
780
|
+
return order.find((p) => hasPreset(asset, p)) ?? this.defaultPresetForMime(mime);
|
|
781
|
+
}
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
// src/web/index.ts
|
|
785
|
+
var AquienpzClient2 = class extends AquienpzClient {
|
|
786
|
+
constructor(opts) {
|
|
787
|
+
super(opts);
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
var DEFAULT_COMPRESSION_OPTIONS = {
|
|
791
|
+
quality: 0.8,
|
|
792
|
+
mimeType: "image/webp",
|
|
793
|
+
maxWidth: 3840,
|
|
794
|
+
maxHeight: 3840,
|
|
795
|
+
convertSize: 5 * 1024 * 1024,
|
|
796
|
+
strict: true
|
|
797
|
+
};
|
|
798
|
+
async function compressImage(file, opts = {}) {
|
|
799
|
+
if (typeof window === "undefined") {
|
|
800
|
+
console.warn(
|
|
801
|
+
"[@nitida/sdk/web] compressImage called outside a browser \u2014 returning input unchanged."
|
|
802
|
+
);
|
|
803
|
+
return { blob: file, originalBytes: file.size };
|
|
804
|
+
}
|
|
805
|
+
const finalOpts = { ...DEFAULT_COMPRESSION_OPTIONS, ...opts };
|
|
806
|
+
const packageOpts = {
|
|
807
|
+
quality: finalOpts.quality,
|
|
808
|
+
mimeType: finalOpts.mimeType,
|
|
809
|
+
maxWidth: finalOpts.maxWidth,
|
|
810
|
+
maxHeight: finalOpts.maxHeight,
|
|
811
|
+
convertSize: finalOpts.convertSize,
|
|
812
|
+
keepOriginalDimensions: opts.keepOriginalDimensions ?? false
|
|
813
|
+
};
|
|
814
|
+
const compressorPkg = "@nitida/asset-compressor-web";
|
|
815
|
+
const { compressImage: doCompress } = await import(
|
|
816
|
+
/* @vite-ignore */
|
|
817
|
+
/* webpackIgnore: true */
|
|
818
|
+
compressorPkg
|
|
819
|
+
);
|
|
820
|
+
const r = await doCompress({
|
|
821
|
+
blob: file,
|
|
822
|
+
filename: file instanceof File ? file.name : "blob",
|
|
823
|
+
options: packageOpts,
|
|
824
|
+
statusCallback: opts.onProgress ? (status) => {
|
|
825
|
+
if (status === "convertingHeic" || status === "compressing" || status === "compressingKeepingDimensions") {
|
|
826
|
+
opts.onProgress?.(status);
|
|
827
|
+
}
|
|
828
|
+
} : void 0
|
|
829
|
+
});
|
|
830
|
+
return { blob: r.blob, originalBytes: r.originalSize };
|
|
831
|
+
}
|
|
832
|
+
async function compressImages(files, opts = {}) {
|
|
833
|
+
if (typeof window === "undefined") {
|
|
834
|
+
console.warn(
|
|
835
|
+
"[@nitida/sdk/web] compressImages called outside a browser \u2014 returning inputs unchanged."
|
|
836
|
+
);
|
|
837
|
+
return files.map((f) => ({ blob: f, originalBytes: f.size }));
|
|
838
|
+
}
|
|
839
|
+
const results = [];
|
|
840
|
+
for (const f of files) {
|
|
841
|
+
results.push(await compressImage(f, opts));
|
|
842
|
+
}
|
|
843
|
+
return results;
|
|
844
|
+
}
|
|
845
|
+
export {
|
|
846
|
+
AquienpzClient2 as AquienpzClient,
|
|
847
|
+
DEFAULT_COMPRESSION_OPTIONS,
|
|
848
|
+
compressImage,
|
|
849
|
+
compressImages,
|
|
850
|
+
computeVariantDimensions,
|
|
851
|
+
extractAssetSha,
|
|
852
|
+
getAssetDimensions,
|
|
853
|
+
getAssetSrcSet2 as getAssetSrcSet,
|
|
854
|
+
getAssetUrl2 as getAssetUrl,
|
|
855
|
+
getHlsStreamingUrl2 as getHlsStreamingUrl,
|
|
856
|
+
getSignedTransformUrl2 as getSignedTransformUrl,
|
|
857
|
+
getTransformSrcSet2 as getTransformSrcSet,
|
|
858
|
+
getTransformUrl2 as getTransformUrl,
|
|
859
|
+
getVideoTransformUrl2 as getVideoTransformUrl,
|
|
860
|
+
hasPreset2 as hasPreset,
|
|
861
|
+
serializeTransform,
|
|
862
|
+
signTransformUrl
|
|
863
|
+
};
|
|
864
|
+
//# sourceMappingURL=web.js.map
|