@nitida/sdk 0.24.0 → 0.25.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 +10 -9
- package/README.md +161 -154
- package/dist/index.d.ts +79 -50
- package/dist/index.js +73 -28
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +9 -7
- package/dist/server.js +51 -28
- package/dist/server.js.map +1 -1
- package/dist/web.d.ts +11 -12
- package/dist/web.js +51 -28
- package/dist/web.js.map +1 -1
- package/package.json +2 -2
- package/skills/nitida-sdk/SKILL.md +215 -91
- package/src/index.ts +114 -51
- package/src/server/index.ts +9 -7
- package/src/web/index.ts +16 -17
package/src/index.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
* One ergonomic facade over the underlying packages
|
|
6
6
|
* (`@nitida/asset-client` URL builders + `@aquienpz/asset-uploader-web`
|
|
7
|
-
* + the slot resolver). Auth is a
|
|
8
|
-
*
|
|
7
|
+
* + the slot resolver). Auth is a bearer API key (`amk_rt_*`), issued
|
|
8
|
+
* per tenant when the tenant is created; tenant scope comes from
|
|
9
9
|
* the key's metadata (`X-Tenant-Code` is log-only).
|
|
10
10
|
*
|
|
11
11
|
* Usage:
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
*
|
|
15
15
|
* const aq = new NitidaClient({
|
|
16
16
|
* endpoint: "https://api.nitida.gofuture.space",
|
|
17
|
-
* apiKey: process.env.
|
|
18
|
-
* tenantCode: "
|
|
17
|
+
* apiKey: process.env.AQUIENPZ_API_KEY!, // amk_rt_* — server-only
|
|
18
|
+
* tenantCode: "acme-co",
|
|
19
19
|
* cdnBase: "https://8ok.uk", // optional override
|
|
20
20
|
* tenantId: 4, // required for tenant-prefixed URLs
|
|
21
21
|
* });
|
|
@@ -78,7 +78,7 @@ import {
|
|
|
78
78
|
*/
|
|
79
79
|
export type NitidaClientOptions = {
|
|
80
80
|
/**
|
|
81
|
-
* Base URL of the
|
|
81
|
+
* Base URL of the nitida API (e.g. `https://api.nitida.gofuture.space`).
|
|
82
82
|
*
|
|
83
83
|
* May be relative (e.g. `/api/am`) ONLY in browser contexts where the
|
|
84
84
|
* SDK resolves it against `window.location.origin`. Node/Bun consumers
|
|
@@ -86,7 +86,7 @@ export type NitidaClientOptions = {
|
|
|
86
86
|
*/
|
|
87
87
|
endpoint: string;
|
|
88
88
|
/**
|
|
89
|
-
*
|
|
89
|
+
* Bearer API key with the `amk_rt_*` prefix.
|
|
90
90
|
*
|
|
91
91
|
* **Server-only.** Omit when constructing from `@nitida/sdk/web` —
|
|
92
92
|
* your BFF / route handler injects the bearer header in proxy mode.
|
|
@@ -111,8 +111,8 @@ export type NitidaClientOptions = {
|
|
|
111
111
|
* Tenant's HMAC signing key for transform URLs (Phase 3). Required
|
|
112
112
|
* only when calling `aq.transform(asset, opts, { sign: true })`.
|
|
113
113
|
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
114
|
+
* 32 random bytes, generated server-side on tenant creation; fetch
|
|
115
|
+
* via `GET /admin/tenants/:id` with an admin key. **Keep it
|
|
116
116
|
* server-side only** — do not ship in `NEXT_PUBLIC_*` env vars. Sign
|
|
117
117
|
* URLs from a BFF route handler, or pre-sign at build time.
|
|
118
118
|
*/
|
|
@@ -190,9 +190,19 @@ function authHeaders(opts: NitidaClientOptions): Record<string, string> {
|
|
|
190
190
|
// Re-exports (so consumers don't double-import from asset-client)
|
|
191
191
|
// ---------------------------------------------------------------------------
|
|
192
192
|
|
|
193
|
+
// This barrel is COMPLETE on purpose: every public symbol of
|
|
194
|
+
// `@nitida/asset-client` is reachable from `@nitida/sdk`, so a consumer never
|
|
195
|
+
// has to know which of the two packages a helper happens to live in. It had
|
|
196
|
+
// drifted to 30 of 53 — the gap silently included `TRANSFORM_WIDTHS` and
|
|
197
|
+
// `TransformWidth`, which both READMEs tell you to import precisely so an
|
|
198
|
+
// off-ladder width fails at compile time instead of returning 400, and the
|
|
199
|
+
// palette helpers the README imports from here by name.
|
|
193
200
|
export type {
|
|
194
201
|
AssetDTO,
|
|
202
|
+
AssetPalette,
|
|
195
203
|
AssetVariant,
|
|
204
|
+
HlsRung,
|
|
205
|
+
PaletteSwatch,
|
|
196
206
|
ResolveSlotOptions,
|
|
197
207
|
SignedTransformOptions,
|
|
198
208
|
SlotDTO,
|
|
@@ -202,24 +212,47 @@ export type {
|
|
|
202
212
|
TransformFormat,
|
|
203
213
|
TransformGravity,
|
|
204
214
|
TransformOptions,
|
|
215
|
+
TransformWidth,
|
|
205
216
|
VariantPreset,
|
|
206
217
|
} from "@nitida/asset-client";
|
|
207
218
|
export {
|
|
219
|
+
bestTextContrast,
|
|
208
220
|
computeVariantDimensions,
|
|
221
|
+
configureSlotResolver,
|
|
222
|
+
contrastRatio,
|
|
209
223
|
extractAssetSha,
|
|
224
|
+
getAmbientGradient,
|
|
210
225
|
getAssetDimensions,
|
|
211
226
|
getAssetSrcSet,
|
|
212
227
|
getAssetUrl,
|
|
228
|
+
getCdnBase,
|
|
229
|
+
getHlsLadder,
|
|
213
230
|
getHlsStreamingUrl,
|
|
231
|
+
getPaletteBlurBackground,
|
|
232
|
+
getPaletteCssVars,
|
|
214
233
|
getSignedTransformUrl,
|
|
215
234
|
getTenantId,
|
|
235
|
+
getTextColorForBackground,
|
|
216
236
|
getTransformSrcSet,
|
|
217
237
|
getTransformUrl,
|
|
218
238
|
getVideoTransformUrl,
|
|
219
239
|
hasPreset,
|
|
240
|
+
hlsLadderAlignment,
|
|
241
|
+
invalidateSlotCache,
|
|
242
|
+
iteratePaletteSwatches,
|
|
243
|
+
PRESET_EXT,
|
|
244
|
+
PRESET_LONG,
|
|
245
|
+
PRESET_MAX_DIM,
|
|
246
|
+
PRESET_SHORT,
|
|
247
|
+
pickAmbientBackground,
|
|
248
|
+
relativeLuminance,
|
|
249
|
+
resolveSlot,
|
|
250
|
+
resolveSlots,
|
|
220
251
|
serializeTransform,
|
|
252
|
+
setCdnBase,
|
|
221
253
|
setTenantId,
|
|
222
254
|
signTransformUrl,
|
|
255
|
+
TRANSFORM_WIDTHS,
|
|
223
256
|
} from "@nitida/asset-client";
|
|
224
257
|
|
|
225
258
|
// ---------------------------------------------------------------------------
|
|
@@ -348,8 +381,8 @@ class SlotsApi {
|
|
|
348
381
|
/**
|
|
349
382
|
* Returned by `aq.assets.regenerate(...)`. The shape varies by kind —
|
|
350
383
|
* images return immediately with the merged variant list; videos
|
|
351
|
-
* return a dispatch handle (the actual transcode runs in a
|
|
352
|
-
*
|
|
384
|
+
* return a dispatch handle (the actual transcode runs in a background
|
|
385
|
+
* job and finishes async).
|
|
353
386
|
*/
|
|
354
387
|
export type RegenerateResult =
|
|
355
388
|
| {
|
|
@@ -383,7 +416,7 @@ export type RegenerateResult =
|
|
|
383
416
|
/**
|
|
384
417
|
* Wire shape returned by `POST /assets/upload-url`. Either the server
|
|
385
418
|
* resolves the upload synchronously via dedup (`deduped: true` + existing
|
|
386
|
-
* asset DTO) or it returns a presigned
|
|
419
|
+
* asset DTO) or it returns a presigned storage PUT URL plus a `process` payload
|
|
387
420
|
* the caller must POST to `/assets/process` after the PUT lands.
|
|
388
421
|
*/
|
|
389
422
|
export type UploadUrlResult =
|
|
@@ -403,13 +436,13 @@ export type UploadVideoOptions = {
|
|
|
403
436
|
/**
|
|
404
437
|
* `false` → skip the auto-dispatched HLS adaptive ladder (240p–2160p). Use
|
|
405
438
|
* for download-only assets served as a progressive `-v.mp4` and never
|
|
406
|
-
* streamed (e.g. share-video reels) — it avoids a second
|
|
439
|
+
* streamed (e.g. share-video reels) — it avoids a second background job no
|
|
407
440
|
* one watches. Default/absent → the ladder is generated as before.
|
|
408
441
|
*/
|
|
409
442
|
hls?: boolean;
|
|
410
443
|
/**
|
|
411
444
|
* `true` → when the uploaded MP4 is ALREADY web-safe (H.264 + yuv420p),
|
|
412
|
-
* re-mux the `video` variant
|
|
445
|
+
* re-mux the `video` variant instead of re-encoding it. Use for
|
|
413
446
|
* delivery-ready uploads (the bytes are already H.264 High / yuv420p /
|
|
414
447
|
* +faststart / capped bitrate) to skip a wasteful re-encode + generational
|
|
415
448
|
* quality loss. Falls back to a full re-encode automatically when the source
|
|
@@ -420,7 +453,7 @@ export type UploadVideoOptions = {
|
|
|
420
453
|
|
|
421
454
|
/** Input shape accepted by `aq.assets.presignUploadUrl(...)`. */
|
|
422
455
|
export type PresignUploadUrlOptions = {
|
|
423
|
-
/** Full sha256 (64 hex) of the bytes that will be PUT to
|
|
456
|
+
/** Full sha256 (64 hex) of the bytes that will be PUT to storage. */
|
|
424
457
|
sha256: string;
|
|
425
458
|
/** MIME type of the bytes (e.g. `image/jpeg`, `video/mp4`). */
|
|
426
459
|
mime: string;
|
|
@@ -435,8 +468,8 @@ export type PresignUploadUrlOptions = {
|
|
|
435
468
|
presets?: VariantPreset[];
|
|
436
469
|
/**
|
|
437
470
|
* Pre-compression size of the source (useful when the browser ran
|
|
438
|
-
* compressorjs / heic2any before computing `bytes`).
|
|
439
|
-
*
|
|
471
|
+
* compressorjs / heic2any before computing `bytes`). Recorded
|
|
472
|
+
* server-side, so the savings show up in the admin usage dashboards.
|
|
440
473
|
*/
|
|
441
474
|
clientOriginalBytes?: number;
|
|
442
475
|
/** VIDEO-only delivery knobs forwarded into `/assets/process`. See {@link UploadVideoOptions}. */
|
|
@@ -571,8 +604,8 @@ class AssetsApi {
|
|
|
571
604
|
* const v = await aq.assets.variants(logoId);
|
|
572
605
|
* v.map((x) => x.preset); // → ("thumb" | "sm" | … | "original")[]
|
|
573
606
|
*
|
|
574
|
-
* ⚠️ Returns `[]` — not an error — against
|
|
575
|
-
* 2026-08-17
|
|
607
|
+
* ⚠️ Returns `[]` — not an error — against a server older than the
|
|
608
|
+
* 2026-08-17 release, which never sent the field at all. An empty
|
|
576
609
|
* array is therefore "no variants OR old server". For a plain existence
|
|
577
610
|
* check prefer `hasPreset(dto, preset)` on `dto.presets`, which every server
|
|
578
611
|
* version sends; use this when you need the URLs and sizes.
|
|
@@ -601,12 +634,13 @@ class AssetsApi {
|
|
|
601
634
|
* Passing no presets re-runs the FULL default pipeline for that
|
|
602
635
|
* asset's kind (thumb+sm+md+lg for images, poster+video for video).
|
|
603
636
|
*
|
|
604
|
-
* If the asset was uploaded original-only and the
|
|
605
|
-
* already
|
|
606
|
-
* bytes from
|
|
637
|
+
* If the asset was uploaded original-only and the ~24 h grace window
|
|
638
|
+
* on the uploaded bytes has already closed, the route falls back to
|
|
639
|
+
* reading the source bytes from the permanent `original` variant —
|
|
640
|
+
* no need to re-upload.
|
|
607
641
|
*
|
|
608
642
|
* Video presets are filtered to `["poster","video","aiproxy"]` and
|
|
609
|
-
* dispatched to
|
|
643
|
+
* dispatched to a background job (the call returns immediately
|
|
610
644
|
* with a dispatch handle; poll `aq.assets.get(id).status` for
|
|
611
645
|
* completion).
|
|
612
646
|
*/
|
|
@@ -642,13 +676,13 @@ class AssetsApi {
|
|
|
642
676
|
}
|
|
643
677
|
|
|
644
678
|
/**
|
|
645
|
-
* Request a presigned
|
|
679
|
+
* Request a presigned storage PUT URL for direct browser-side uploads.
|
|
646
680
|
*
|
|
647
681
|
* Mirrors the first half of `aq.upload()` — the caller (typically a
|
|
648
682
|
* BFF / share-link dropzone) computes sha256 in the browser, then
|
|
649
|
-
* uploads bytes straight to
|
|
650
|
-
* POSTs `process.body` to `/assets/process` (see {@link processAndWait})
|
|
651
|
-
* once
|
|
683
|
+
* uploads bytes straight to object storage with the returned `upload.url`,
|
|
684
|
+
* then POSTs `process.body` to `/assets/process` (see {@link processAndWait})
|
|
685
|
+
* once storage has the bytes.
|
|
652
686
|
*
|
|
653
687
|
* If the sha is already known to the tenant the server short-circuits
|
|
654
688
|
* with `{ deduped: true, asset }` — no PUT needed.
|
|
@@ -660,16 +694,16 @@ class AssetsApi {
|
|
|
660
694
|
* if (presign.deduped) return presign.asset; // those bytes already exist; none fly
|
|
661
695
|
*
|
|
662
696
|
* // BROWSER: PUT straight to presign.upload.url — the bytes never touch your server.
|
|
663
|
-
* // ⚠️
|
|
664
|
-
* // Symptom when it is not: "PUT failed: network error" with every earlier step
|
|
665
|
-
* // and it cannot be fixed in this SDK, in your app, or
|
|
697
|
+
* // ⚠️ The STORAGE BUCKET answers that preflight itself, so your origin must be in its CORS
|
|
698
|
+
* // policy. Symptom when it is not: "PUT failed: network error" with every earlier step
|
|
699
|
+
* // green — and it cannot be fixed in this SDK, in your app, or by the API's allowed origins.
|
|
666
700
|
*
|
|
667
701
|
* // SERVER again, forwarding presign.process.body VERBATIM:
|
|
668
702
|
* const asset = await aq.assets.processAndWait(presign.process.body, { timeoutMs: 300_000 });
|
|
669
703
|
* ```
|
|
670
704
|
*
|
|
671
705
|
* Works for images AND video. A video answers immediately with
|
|
672
|
-
* `{ assetId, status: "processing" }` while a
|
|
706
|
+
* `{ assetId, status: "processing" }` while a background job transcodes, so
|
|
673
707
|
* give `processAndWait` a bigger `timeoutMs` (a transcode + HLS ladder runs
|
|
674
708
|
* 1–2 min; 300_000 is a sane floor).
|
|
675
709
|
*/
|
|
@@ -701,7 +735,7 @@ class AssetsApi {
|
|
|
701
735
|
* {@link presignUploadUrl} call, then poll until the asset transitions
|
|
702
736
|
* to `ready` or `failed`. Throws on `failed` or timeout.
|
|
703
737
|
*
|
|
704
|
-
* Use this when bytes were uploaded directly from the browser to
|
|
738
|
+
* Use this when bytes were uploaded directly from the browser to storage —
|
|
705
739
|
* `aq.upload()` already does presign + PUT + process + wait in one
|
|
706
740
|
* step when the server holds the bytes.
|
|
707
741
|
*/
|
|
@@ -755,7 +789,7 @@ class AssetsApi {
|
|
|
755
789
|
* {@link waitReady} (typical timeout: 10 min for multi-segment kits).
|
|
756
790
|
*
|
|
757
791
|
* Tenant scope is inherited from the SDK client; `tenantCode` is added
|
|
758
|
-
* to the request body so the
|
|
792
|
+
* to the request body so the background job can resolve it without
|
|
759
793
|
* re-reading the header.
|
|
760
794
|
*/
|
|
761
795
|
async composeMarketing(
|
|
@@ -858,7 +892,7 @@ export type UploadOptions = {
|
|
|
858
892
|
* MIME type of the bytes. **Only needed for a `Uint8Array` input** — a `File`/`Blob`
|
|
859
893
|
* already carries its `.type`. Raw bytes have no inherent MIME, so without this (and
|
|
860
894
|
* without an extension on `fileName` to infer from) they upload as
|
|
861
|
-
* `application/octet-stream`, which the
|
|
895
|
+
* `application/octet-stream`, which the server classifies as `kind:"other"` —
|
|
862
896
|
* meaning NO image/video variants are generated and `regenerate()` is unsupported.
|
|
863
897
|
* Resolution order for the effective MIME: `Blob.type` → `contentType` →
|
|
864
898
|
* inferred from `fileName`'s extension → `application/octet-stream`.
|
|
@@ -916,8 +950,8 @@ export type UploadOptions = {
|
|
|
916
950
|
/**
|
|
917
951
|
* Max time to wait for the asset to transition to `ready` (or `failed`)
|
|
918
952
|
* after dispatch. Default `5 * 60_000` (5 min). Bump higher for large
|
|
919
|
-
* videos / HLS transcodes —
|
|
920
|
-
*
|
|
953
|
+
* videos / HLS transcodes — processing time scales with input size,
|
|
954
|
+
* and with how much other work the platform is doing at that moment.
|
|
921
955
|
*
|
|
922
956
|
* Throws `Error("waitReady timeout for <id>")` if the deadline passes
|
|
923
957
|
* without the asset transitioning. The asset row stays in aquienpz
|
|
@@ -1002,9 +1036,37 @@ export type UsageWindow = {
|
|
|
1002
1036
|
admins: number;
|
|
1003
1037
|
upscales: number;
|
|
1004
1038
|
processes: number;
|
|
1005
|
-
|
|
1039
|
+
/**
|
|
1040
|
+
* Minutes of video actually TRANSCODED in the window — source minutes ×
|
|
1041
|
+
* encode passes, so a 7-rung HLS ladder over a 2-minute clip books 14.
|
|
1042
|
+
*
|
|
1043
|
+
* A re-encode of a video already ingested counts AGAIN. That is the point:
|
|
1044
|
+
* an asset count cannot report it, because re-encoding an asset that already
|
|
1045
|
+
* exists creates no new asset.
|
|
1046
|
+
*
|
|
1047
|
+
* A stream copy (`video: { passthrough: true }`, or an idempotent cache skip)
|
|
1048
|
+
* books nothing, so a tenant that ran no encoder reads `0` — and that `0` is real.
|
|
1049
|
+
*/
|
|
1050
|
+
videoMinutes: number;
|
|
1051
|
+
/** Encode passes behind `videoMinutes`. 0 when nothing was encoded. */
|
|
1052
|
+
videoEncodes: number;
|
|
1006
1053
|
};
|
|
1007
1054
|
|
|
1055
|
+
/**
|
|
1056
|
+
* ⚠️ REMOVED 2026-08-17: `bytesIn` on `UsageWindow`/`UsageDailyPoint` and
|
|
1057
|
+
* `bytesTotal` on `UsagePerKey`.
|
|
1058
|
+
*
|
|
1059
|
+
* All three were structurally zero: the field behind them was never populated,
|
|
1060
|
+
* on any row. They could not be instrumented in place either — uploads go to
|
|
1061
|
+
* object storage through presigned URLs, so the payload never passes through
|
|
1062
|
+
* the API, and the only figure measurable there is a JSON envelope of a few
|
|
1063
|
+
* hundred bytes.
|
|
1064
|
+
*
|
|
1065
|
+
* A field that always reads `0` is worse than an absent field, because absent
|
|
1066
|
+
* is honest — a `0` next to a real `storage.totalBytes` reads as a measurement.
|
|
1067
|
+
* The byte number that IS true is still served: `storage.totalBytes`.
|
|
1068
|
+
*/
|
|
1069
|
+
|
|
1008
1070
|
export type UsageDailyPoint = {
|
|
1009
1071
|
date: string;
|
|
1010
1072
|
reads: number;
|
|
@@ -1012,8 +1074,10 @@ export type UsageDailyPoint = {
|
|
|
1012
1074
|
lists: number;
|
|
1013
1075
|
deletes: number;
|
|
1014
1076
|
processes: number;
|
|
1015
|
-
bytesIn: number;
|
|
1016
1077
|
bytesStored: number;
|
|
1078
|
+
/** See `UsageWindow.videoMinutes`. Minutes transcoded on this day. */
|
|
1079
|
+
videoMinutes: number;
|
|
1080
|
+
videoEncodes: number;
|
|
1017
1081
|
};
|
|
1018
1082
|
|
|
1019
1083
|
export type UsagePerKey = {
|
|
@@ -1021,7 +1085,6 @@ export type UsagePerKey = {
|
|
|
1021
1085
|
prefix: string | null;
|
|
1022
1086
|
name: string | null;
|
|
1023
1087
|
opsTotal: number;
|
|
1024
|
-
bytesTotal: number;
|
|
1025
1088
|
lastSeen: string;
|
|
1026
1089
|
};
|
|
1027
1090
|
|
|
@@ -1136,7 +1199,7 @@ export class NitidaClient {
|
|
|
1136
1199
|
* Returns the canonical `lg` variant URL when called with empty options,
|
|
1137
1200
|
* so callers can swap `urlFor()` for `transform()` without thinking.
|
|
1138
1201
|
*
|
|
1139
|
-
* URLs with the same params in different order produce the same
|
|
1202
|
+
* URLs with the same params in different order produce the same
|
|
1140
1203
|
* cache entry (the server canonicalizes both sides). Safe to use as
|
|
1141
1204
|
* stable cache keys.
|
|
1142
1205
|
*
|
|
@@ -1236,12 +1299,12 @@ export class NitidaClient {
|
|
|
1236
1299
|
* Build an on-the-fly VIDEO transform URL — Phase 4.
|
|
1237
1300
|
*
|
|
1238
1301
|
* Same DSL shape as `transform()` but the URL has a `.mp4` (default)
|
|
1239
|
-
* or `.webm` extension and the server routes the request to a
|
|
1240
|
-
*
|
|
1302
|
+
* or `.webm` extension and the server routes the request to a
|
|
1303
|
+
* background job for video encoding (vs the inline pipeline for
|
|
1241
1304
|
* images).
|
|
1242
1305
|
*
|
|
1243
1306
|
* On the first request the route returns **202 Accepted** with
|
|
1244
|
-
* `Retry-After: 10` while the
|
|
1307
|
+
* `Retry-After: 10` while the encode runs (typically 5-30 s for a
|
|
1245
1308
|
* short clip). The response body includes `outputUrl` which is the
|
|
1246
1309
|
* eventual CDN URL — poll the same transform URL after the
|
|
1247
1310
|
* retry-after window to get a 302 redirect to it.
|
|
@@ -1283,7 +1346,7 @@ export class NitidaClient {
|
|
|
1283
1346
|
* />
|
|
1284
1347
|
*
|
|
1285
1348
|
* On the first request the server returns **202 Accepted** while a
|
|
1286
|
-
*
|
|
1349
|
+
* background job builds the multi-rung ladder (typically 1-3 min for
|
|
1287
1350
|
* a 90 s source — five rungs of 240p/360p/480p/720p/1080p @ AAC).
|
|
1288
1351
|
* Subsequent requests hit the cache → **302** to the master.m3u8.
|
|
1289
1352
|
*
|
|
@@ -1308,13 +1371,13 @@ export class NitidaClient {
|
|
|
1308
1371
|
* but isn't always available depending on the runtime).
|
|
1309
1372
|
*/
|
|
1310
1373
|
/**
|
|
1311
|
-
* Upload bytes end to end: optional client compression → sha256 → presign → **direct-to-
|
|
1374
|
+
* Upload bytes end to end: optional client compression → sha256 → presign → **direct-to-storage PUT**
|
|
1312
1375
|
* → `/assets/process` → wait until the asset is ready.
|
|
1313
1376
|
*
|
|
1314
1377
|
* ⚠️ `presets` decides what exists FOREVER. Omit it and only `original` is written; ask for
|
|
1315
1378
|
* `["thumb"]` and the bytes you just uploaded are **not retrievable**. A variant not requested in
|
|
1316
|
-
* this first ingest cannot be added later once the
|
|
1317
|
-
* "97 files archived successfully, zero recoverable".
|
|
1379
|
+
* this first ingest cannot be added later once the ~24 h grace window on the uploaded bytes
|
|
1380
|
+
* closes — measured once as "97 files archived successfully, zero recoverable".
|
|
1318
1381
|
*
|
|
1319
1382
|
* @example Deliver an image on a site (the responsive ladder)
|
|
1320
1383
|
* ```ts
|
|
@@ -1345,11 +1408,11 @@ export class NitidaClient {
|
|
|
1345
1408
|
* @example Video — and what does NOT work there
|
|
1346
1409
|
* ```ts
|
|
1347
1410
|
* // `original` is accepted and then silently DROPPED: /assets/process filters video presets to
|
|
1348
|
-
* // {poster, video, aiproxy, probe} before dispatching the transcode
|
|
1411
|
+
* // {poster, video, aiproxy, probe} before dispatching the background transcode.
|
|
1349
1412
|
* await aq.upload(clip, { fileName: "tour.mp4", presets: ["poster", "video"] });
|
|
1350
1413
|
*
|
|
1351
|
-
* // Omit `aiproxy`/`probe` unless the asset really goes to a vision model — they cost
|
|
1352
|
-
* // and permanent
|
|
1414
|
+
* // Omit `aiproxy`/`probe` unless the asset really goes to a vision model — they cost encode
|
|
1415
|
+
* // time and permanent stored objects that nothing else reads.
|
|
1353
1416
|
* ```
|
|
1354
1417
|
*/
|
|
1355
1418
|
async upload(
|
|
@@ -1360,7 +1423,7 @@ export class NitidaClient {
|
|
|
1360
1423
|
// "should we compress this?")
|
|
1361
1424
|
const sourceIsBlob = input instanceof File || input instanceof Blob;
|
|
1362
1425
|
// A Uint8Array has no inherent MIME. Fall back to an explicit `contentType`, then to
|
|
1363
|
-
// the file extension, before octet-stream (which the
|
|
1426
|
+
// the file extension, before octet-stream (which the server files as
|
|
1364
1427
|
// `kind:"other"` — no variants). A File/Blob's own `.type` always wins when present.
|
|
1365
1428
|
const sourceMime =
|
|
1366
1429
|
(sourceIsBlob ? input.type : "") ||
|
|
@@ -1494,7 +1557,7 @@ export class NitidaClient {
|
|
|
1494
1557
|
body: new Blob([bytes as unknown as ArrayBuffer], { type: mime }),
|
|
1495
1558
|
});
|
|
1496
1559
|
if (!putR.ok)
|
|
1497
|
-
throw new Error(`
|
|
1560
|
+
throw new Error(`Storage PUT ${putR.status}: ${await putR.text()}`);
|
|
1498
1561
|
|
|
1499
1562
|
const procR = await fetch(endpointHref(this.opts, presign.process.url), {
|
|
1500
1563
|
method: "POST",
|
package/src/server/index.ts
CHANGED
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
* import { NitidaClient } from "@nitida/sdk/server";
|
|
11
11
|
*
|
|
12
12
|
* const aq = new NitidaClient({
|
|
13
|
-
* endpoint: process.env.
|
|
14
|
-
* apiKey: process.env.
|
|
15
|
-
* tenantCode: "
|
|
13
|
+
* endpoint: process.env.AQUIENPZ_URL!,
|
|
14
|
+
* apiKey: process.env.AQUIENPZ_API_KEY!, // <- required
|
|
15
|
+
* tenantCode: "acme-co",
|
|
16
16
|
* tenantId: 1,
|
|
17
17
|
* // signingKey: optional, only for `aq.transform(..., { sign: true })`
|
|
18
18
|
* });
|
|
@@ -32,9 +32,11 @@
|
|
|
32
32
|
* globally; File-API workflows are documented on the /web subpath instead)
|
|
33
33
|
*
|
|
34
34
|
* What's NOT here (use `@nitida/sdk/web` instead):
|
|
35
|
-
* - `createWebUploader` (multipart UploadTask with IndexedDB resume)
|
|
36
35
|
* - `compressImage` (browser-side compressorjs + heic2any)
|
|
37
36
|
*
|
|
37
|
+
* And what exists nowhere: there is no `createWebUploader`. Multipart is not
|
|
38
|
+
* exposed from any subpath — `aq.upload()` is the supported path.
|
|
39
|
+
*
|
|
38
40
|
* Stripe/Cloudinary historically shipped two separate packages
|
|
39
41
|
* (`stripe` vs `@stripe/stripe-js`, `cloudinary` vs `@cloudinary/url-gen`)
|
|
40
42
|
* for this split. Modern providers (Vercel Blob, Uploadthing, Better
|
|
@@ -51,9 +53,9 @@ import { NitidaClient as BaseNitidaClient, type NitidaClientOptions } from "..";
|
|
|
51
53
|
* Cloud Run, Vercel Functions, edge runtimes, BFFs).
|
|
52
54
|
*
|
|
53
55
|
* const aq = new NitidaClient({
|
|
54
|
-
* endpoint: process.env.
|
|
55
|
-
* apiKey: process.env.
|
|
56
|
-
* tenantCode: "
|
|
56
|
+
* endpoint: process.env.AQUIENPZ_URL!,
|
|
57
|
+
* apiKey: process.env.AQUIENPZ_API_KEY!,
|
|
58
|
+
* tenantCode: "acme-co",
|
|
57
59
|
* tenantId: 1,
|
|
58
60
|
* });
|
|
59
61
|
*/
|
package/src/web/index.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* // Point at your BFF route — the SDK calls
|
|
14
14
|
* // `${endpoint}/assets/by-hash/...`, `${endpoint}/slots/...`, etc.
|
|
15
15
|
* endpoint: "/api/am", // relative ⇒ same-origin proxy
|
|
16
|
-
* tenantCode: "
|
|
16
|
+
* tenantCode: "acme-co",
|
|
17
17
|
* tenantId: 1,
|
|
18
18
|
* // NO apiKey — the type strips it. Your BFF injects the bearer.
|
|
19
19
|
* });
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* return fetch(url, {
|
|
29
29
|
* headers: {
|
|
30
30
|
* Authorization: `Bearer ${process.env.AQUIENPZ_API_KEY!}`,
|
|
31
|
-
* "X-Tenant-Code": "
|
|
31
|
+
* "X-Tenant-Code": "acme-co",
|
|
32
32
|
* },
|
|
33
33
|
* });
|
|
34
34
|
* }
|
|
@@ -64,7 +64,7 @@ import { NitidaClient as BaseNitidaClient, type NitidaClientOptions } from "..";
|
|
|
64
64
|
*
|
|
65
65
|
* const aq = new NitidaClient({
|
|
66
66
|
* endpoint: "/api/am", // OK: relative → same-origin BFF
|
|
67
|
-
* tenantCode: "
|
|
67
|
+
* tenantCode: "acme-co",
|
|
68
68
|
* tenantId: 1,
|
|
69
69
|
* // apiKey: "amk_rt_...", // ERROR: TS error: not assignable
|
|
70
70
|
* });
|
|
@@ -150,9 +150,9 @@ export {
|
|
|
150
150
|
// ---------------------------------------------------------------------------
|
|
151
151
|
//
|
|
152
152
|
// Wraps `@nitida/asset-compressor-web` with the SDK's
|
|
153
|
-
// `DEFAULT_COMPRESSION_OPTIONS` — values
|
|
154
|
-
//
|
|
155
|
-
//
|
|
153
|
+
// `DEFAULT_COMPRESSION_OPTIONS` — values tuned against a production
|
|
154
|
+
// photo-upload workload (quality 0.80, max-edge 3840px, WebP output,
|
|
155
|
+
// 5MB PNG→JPEG threshold, strict mode). Those defaults
|
|
156
156
|
// differ from `@nitida/asset-compressor-web`'s own defaults (0.85 / 2880 /
|
|
157
157
|
// convertSize=0) which target a slightly different audience; SDK callers
|
|
158
158
|
// get the webapp-tuned values, package-direct callers keep theirs.
|
|
@@ -184,17 +184,16 @@ export type CompressStage =
|
|
|
184
184
|
|
|
185
185
|
/**
|
|
186
186
|
* Subset of compressorjs options exposed by the SDK. Matches the
|
|
187
|
-
*
|
|
188
|
-
* production back-office uploader.
|
|
187
|
+
* `DEFAULT_COMPRESSION_OPTIONS` shape below.
|
|
189
188
|
*/
|
|
190
189
|
export type CompressOptions = {
|
|
191
|
-
/** 0..1. Default 0.80
|
|
190
|
+
/** 0..1. Default 0.80. */
|
|
192
191
|
quality?: number;
|
|
193
|
-
/** Output format. Default `"image/webp"
|
|
192
|
+
/** Output format. Default `"image/webp"`. */
|
|
194
193
|
mimeType?: "image/jpeg" | "image/webp";
|
|
195
|
-
/** Max edge in pixels. Default 3840
|
|
194
|
+
/** Max edge in pixels. Default 3840. */
|
|
196
195
|
maxWidth?: number;
|
|
197
|
-
/** Default 3840
|
|
196
|
+
/** Max edge in pixels. Default 3840. */
|
|
198
197
|
maxHeight?: number;
|
|
199
198
|
/**
|
|
200
199
|
* compressorjs `convertSize`: PNG > this byte count auto-converts to
|
|
@@ -215,9 +214,9 @@ export type CompressOptions = {
|
|
|
215
214
|
};
|
|
216
215
|
|
|
217
216
|
/**
|
|
218
|
-
*
|
|
219
|
-
* from `@nitida/asset-compressor-web`'s
|
|
220
|
-
* SDK overrides at call time.
|
|
217
|
+
* Defaults tuned against a production photo-upload workload. These
|
|
218
|
+
* intentionally differ from `@nitida/asset-compressor-web`'s
|
|
219
|
+
* package-level defaults — the SDK overrides at call time.
|
|
221
220
|
*/
|
|
222
221
|
export const DEFAULT_COMPRESSION_OPTIONS: Required<
|
|
223
222
|
Omit<CompressOptions, "keepOriginalDimensions" | "convertHeic" | "onProgress">
|
|
@@ -274,8 +273,8 @@ export async function compressImage(
|
|
|
274
273
|
// String indirection defeats bundler static-analysis of dynamic
|
|
275
274
|
// imports — Turbopack/webpack will leave this for the runtime to
|
|
276
275
|
// resolve instead of failing the build when the optional peer dep
|
|
277
|
-
// isn't installed.
|
|
278
|
-
//
|
|
276
|
+
// isn't installed. (The same trick is used in reverse elsewhere in
|
|
277
|
+
// this SDK to opt OUT of bundling.)
|
|
279
278
|
const compressorPkg = "@nitida/asset-compressor-web";
|
|
280
279
|
const { compressImage: doCompress } = (await import(
|
|
281
280
|
/* @vite-ignore */ /* webpackIgnore: true */ compressorPkg
|