@nitida/sdk 0.24.0 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -3,7 +3,7 @@
3
3
  **Read `skills/nitida-sdk/SKILL.md` in this package first.** It is the canonical guide and it
4
4
  ships inside the tarball, so it is already on disk in `node_modules/@nitida/sdk/skills/`. It
5
5
  carries the things that cost real incidents to learn — the base36 tenant prefix, the width ladder,
6
- the `original`-preset trap, the R2 CORS boundary — not just the API shape.
6
+ the `original`-preset trap, the object-storage CORS boundary — not just the API shape.
7
7
 
8
8
  **Then read `src/`.** This package publishes its TypeScript sources (`files: ["dist/**", "src/**"]`),
9
9
  so the source *is* the reference: every public method carries TSDoc, and the ones worth copying
@@ -13,12 +13,12 @@ recalling an API from training data — this SDK moves.
13
13
  ## The five-minute version
14
14
 
15
15
  ```ts
16
- import { AquienpzClient } from "@nitida/sdk/server"; // uploads: server-only, holds the amk_rt_* key
16
+ import { NitidaClient } from "@nitida/sdk/server"; // uploads: server-only, holds the amk_rt_* key
17
17
  import { getTransformUrl, setCdnBase, setTenantId } from "@nitida/asset-client"; // URLs: anywhere
18
18
  ```
19
19
 
20
20
  - **Uploading** → `aq.upload(file, { fileName, contentType, presets })`. One call does compress →
21
- sha256 → presign → direct-to-R2 PUT → `/assets/process` → wait-until-ready.
21
+ sha256 → presign → direct-to-storage PUT → `/assets/process` → wait-until-ready.
22
22
  - **Image URLs** → `getTransformUrl({ sha }, { format: "webp", width })`. The width MUST be on
23
23
  `TRANSFORM_WIDTHS` or the edge answers **400**.
24
24
  - **Video URLs** → `getAssetUrl({ sha }, "video")` after `setTenantId(id)`. The path segment is
@@ -30,14 +30,15 @@ import { getTransformUrl, setCdnBase, setTenantId } from "@nitida/asset-client";
30
30
  `["thumb"]` and the bytes you PUT are *not* retrievable. A variant not requested at the FIRST
31
31
  ingest cannot be added later once the cleanup job reaps `raw/`.
32
32
  2. **The `amk_rt_*` key is server-only.** The browser talks to *your* route; your server talks to
33
- aquienpz. A runtime key in a client bundle is a write key to a paid platform.
34
- 3. **The PUT goes browser → R2 directly, so R2 answers the CORS preflight.** An origin missing from
35
- the bucket policy cannot be fixed in this SDK, in your app, or in `storefront_origins`. Symptom:
36
- `R2 PUT failed: network error` with every earlier step green.
33
+ the platform. A runtime key in a client bundle is a write key to a paid platform.
34
+ 3. **The PUT goes browser → object storage directly, so the STORAGE BUCKET answers the CORS
35
+ preflight.** An origin missing from the bucket policy cannot be fixed in this SDK, in your app,
36
+ or by the API's allowed-origins setting ask us to add it. Symptom: the direct PUT fails with a
37
+ bare network error while every earlier step is green.
37
38
  4. **Video is not image.** `/assets/process` filters video presets to
38
39
  `{poster, video, aiproxy, probe}` — `original` is silently dropped — and a video finalizes
39
- ASYNCHRONOUSLY: the call answers `{ assetId, status: "processing" }` and a Cloud Run Job flips it
40
- to `ready` 1–2 min later, so `processAndWait` needs a `timeoutMs` of at least `300_000`.
40
+ ASYNCHRONOUSLY: the call answers `{ assetId, status: "processing" }` and a background job flips
41
+ it to `ready` 1–2 min later, so `processAndWait` needs a `timeoutMs` of at least `300_000`.
41
42
 
42
43
  ## Related packages
43
44
 
package/README.md CHANGED
@@ -721,11 +721,11 @@ const asset = await aq.assets.byHash(sha256);
721
721
  > `2400`). `height` stays `number` — for the responsive path it's derived from `width`
722
722
  > by aspect ratio; only fixed-canvas crops / `genfill` set it explicitly.
723
723
 
724
- `format=auto` resolves to **WebP** by default. The bench in `apps/asset-manager/bench/avif-vs-webp-summary.md`
725
- (100 random realtyone-cr `lg.webp` samples, sharp 0.34.5) showed AVIF was 4.5–9.8% **larger**
726
- than WebP in every source-size bracket with virtually identical SSIM. The policy lives
727
- in a single function (`pickAutoFormat` in `transform.format-policy.ts`) so re-evaluation
728
- when libavif improves is a one-file swap.
724
+ `format=auto` resolves to **WebP** by default. Our bench over 100 random
725
+ production `lg.webp` samples showed AVIF was 4.5–9.8% **larger** than WebP in every
726
+ source-size bracket with virtually identical SSIM. The policy is re-evaluated as
727
+ libavif improves, so `auto` may resolve differently in the future — that is the
728
+ point of asking for `auto` instead of naming a format.
729
729
 
730
730
  `gravity=face` runs the source image through a ~1 MB Ultra-Light face
731
731
  detector (ultraface-RFB-320 via ONNX Runtime) and crops to the
@@ -746,12 +746,11 @@ instance boots.
746
746
  | normal (25–55) — most photos | 70 | 60 | 80 |
747
747
  | complex (≥ 55) — busy textures | 72 | 65 | 82 |
748
748
 
749
- Validated on 100 random realtyone-cr `lg.webp` samples: **+15.4 %
749
+ Validated on 100 random production `lg.webp` samples: **+15.4 %
750
750
  bytes saved vs fixed `quality=80` baseline, |ΔSSIM| 0.0011** (budget
751
- 0.005). See `bench/auto-quality-summary.md`. Both the bucket
752
- boundaries and per-format table live in
753
- `transform.quality-policy.ts` re-tune by editing the constant and
754
- re-running `bun run apps/asset-manager/scripts/bench-auto-quality.ts`.
751
+ 0.005). The bucket boundaries and the per-format table are server-side
752
+ policy and may be re-tuned; pass an explicit `quality=` when you need a
753
+ number that does not move.
755
754
 
756
755
  ### Canonicalization & caching
757
756
 
@@ -844,19 +843,15 @@ const jpegUrl = aq.transform(asset, { format: "jpeg" });
844
843
  // → https://8ok.uk/t/format=jpeg/<sha>.jpg
845
844
  ```
846
845
 
847
- `effect=removebg` is provider-pluggable via the
848
- `BG_REMOVAL_BACKEND` env on the asset-manager:
846
+ `effect=removebg` runs on one of two server-side matting backends, chosen
847
+ per deployment. What the caller sees:
849
848
 
850
- - **`local`** (default) — U²-Net ONNX inference, Apache 2.0 weights
851
- shipped under `/app/models/u2net.onnx` (~176 MB, fetched at
852
- Docker build time, sha256-verified). ~1-2 s warm CPU inference,
853
- ~5-7 s cold-start. Free runtime.
854
- - **`replicate`** — proxies to Replicate's `851-labs/background-remover`
855
- (BRIA-quality, commercially licensed via Replicate). ~3-8 s GPU,
856
- ~$0.001-0.005 per image. Requires `REPLICATE_API_TOKEN` in Secret
857
- Manager.
849
+ - **CPU matting** (default) — ~1-2 s warm, ~5-7 s cold. No per-image cost.
850
+ - **GPU matting** ~3-8 s, ~$0.001-0.005 per image, better edges on hair
851
+ and fine detail.
858
852
 
859
- In both cases the route caches the PNG in R2 under the standard
853
+ Either way the contract is the same, and the first request is the only one
854
+ that pays. In both cases the route caches the PNG in R2 under the standard
860
855
  `<sha>-t<dslHash>.png` key, so subsequent identical requests are 302
861
856
  redirects to the CDN — no inference, no per-image cost. **Always
862
857
  forces `format=png`** because the entire point is preserving alpha.
@@ -865,12 +860,9 @@ The output is the same dimensions as the source. Chain with `width`
865
860
  to resize the cutout in a single request (cached as one R2 entry per
866
861
  canonical DSL).
867
862
 
868
- To re-tune or swap the model:
869
- 1. Drop a new `.onnx` into `apps/asset-manager/src/features/assets/bg/`
870
- 2. Update `MODEL_PATH` + preprocessing constants in `bg/remove.ts`
871
- 3. Update the Dockerfile's `COPY src/features/assets/bg/*.onnx /app/models/`
872
- 4. Re-bench against a sample set (a CSV in `apps/asset-manager/bench/`
873
- makes sense once the comparison is non-trivial).
863
+ The matting model is server-side and may be swapped for a better one
864
+ without any change on your side — the URL, the PNG-with-alpha output and
865
+ the cache semantics are the contract.
874
866
 
875
867
  ### Generative fill / aspect outpaint (`effect=genfill`)
876
868
 
@@ -923,9 +915,8 @@ Powered by Replicate's `black-forest-labs/flux-fill-pro`. ~$0.05 per
923
915
  first request per (sha, dsl, format) tuple; subsequent identical
924
916
  requests are 302 redirects to the R2 cache — zero Replicate cost.
925
917
 
926
- Requires `REPLICATE_API_TOKEN` in the asset-manager's Secret Manager
927
- secrets. Same token as `BG_REMOVAL_BACKEND=replicate`; no separate
928
- provisioning needed.
918
+ Server-side credentials for the generative provider are part of the
919
+ deployment; nothing to provision on your side.
929
920
 
930
921
  **Short-circuit**: when the source already matches the target aspect
931
922
  exactly (resized to fill the canvas with zero padding), the server
@@ -1141,8 +1132,8 @@ const usage = await aq.usage.snapshot();
1141
1132
  // {
1142
1133
  // tenant: { id: 4, code: "realtyone-cr" },
1143
1134
  // storage: { totalBytes: 4_810_000_000, assetCount: 15_760 },
1144
- // today: { reads: 3201, writes: 18, processes: 6, bytesIn: 12_400_000, ... },
1145
- // last30Days: { reads: 86_400, writes: 412, ... }
1135
+ // today: { reads: 3201, writes: 18, processes: 6, videoMinutes: 4.5, ... },
1136
+ // last30Days: { reads: 86_400, writes: 412, videoMinutes: 312.75, ... }
1146
1137
  // }
1147
1138
 
1148
1139
  const chart = await aq.usage.timeseries(30); // for a 30-day line chart
@@ -1151,7 +1142,24 @@ const byKey = await aq.usage.keys(); // who's using the most quota
1151
1142
 
1152
1143
  Backed by the daily rollup of `assets.api_key_usage` →
1153
1144
  `assets.tenant_usage_daily`. The "today" window queries the raw
1154
- `api_key_usage` table since the rollup runs once per day at ~00:30 UTC.
1145
+ `api_key_usage` table since the rollup runs once per day at ~00:30 UTC
1146
+ except `videoMinutes`, which is accumulated live and so is real-time on
1147
+ every window.
1148
+
1149
+ **`videoMinutes` counts minutes actually TRANSCODED, not videos stored.**
1150
+ It is *source minutes × encode passes*, so a 7-rung HLS ladder over a
1151
+ 2-minute clip books 14. Re-encoding a video you already uploaded counts
1152
+ **again** — that is the point: the asset row is upserted on `sha256`, so
1153
+ nothing else in the system can see a re-encode. A passthrough stream copy
1154
+ or a cache hit runs no encoder and books nothing, so `0` means zero.
1155
+
1156
+ > ⚠️ **Removed 2026-08-17:** `bytesIn` (on the usage windows and daily
1157
+ > points) and `bytesTotal` (on the per-key rows). They were fed by
1158
+ > `api_key_usage.bytes`, which is structurally zero — measured at 440,820
1159
+ > rows, none above 0 — and could not be instrumented, because uploads go to
1160
+ > R2 through presigned URLs and never traverse the API. A field that always
1161
+ > reads `0` is worse than an absent one. The byte figure that is true,
1162
+ > `storage.totalBytes`, is unchanged.
1155
1163
 
1156
1164
  ## License
1157
1165
 
package/dist/index.d.ts CHANGED
@@ -52,7 +52,7 @@ export { AssetDTO, AssetVariant, ResolveSlotOptions, SignedTransformOptions, Slo
52
52
  */
53
53
  type NitidaClientOptions = {
54
54
  /**
55
- * Base URL of the aquienpz asset-manager (Cloud Run service URL).
55
+ * Base URL of the nitida API (e.g. `https://api.nitida.gofuture.space`).
56
56
  *
57
57
  * May be relative (e.g. `/api/am`) ONLY in browser contexts where the
58
58
  * SDK resolves it against `window.location.origin`. Node/Bun consumers
@@ -150,8 +150,8 @@ declare class SlotsApi {
150
150
  /**
151
151
  * Returned by `aq.assets.regenerate(...)`. The shape varies by kind —
152
152
  * images return immediately with the merged variant list; videos
153
- * return a dispatch handle (the actual transcode runs in a Cloud Run
154
- * Job and finishes async).
153
+ * return a dispatch handle (the actual transcode runs in a background
154
+ * job and finishes async).
155
155
  */
156
156
  type RegenerateResult = {
157
157
  ok: true;
@@ -182,7 +182,7 @@ type RegenerateResult = {
182
182
  /**
183
183
  * Wire shape returned by `POST /assets/upload-url`. Either the server
184
184
  * resolves the upload synchronously via dedup (`deduped: true` + existing
185
- * asset DTO) or it returns a presigned R2 PUT URL plus a `process` payload
185
+ * asset DTO) or it returns a presigned storage PUT URL plus a `process` payload
186
186
  * the caller must POST to `/assets/process` after the PUT lands.
187
187
  */
188
188
  type UploadUrlResult = {
@@ -208,7 +208,7 @@ type UploadVideoOptions = {
208
208
  /**
209
209
  * `false` → skip the auto-dispatched HLS adaptive ladder (240p–2160p). Use
210
210
  * for download-only assets served as a progressive `-v.mp4` and never
211
- * streamed (e.g. share-video reels) — it avoids a second Cloud Run Job no
211
+ * streamed (e.g. share-video reels) — it avoids a second background job no
212
212
  * one watches. Default/absent → the ladder is generated as before.
213
213
  */
214
214
  hls?: boolean;
@@ -224,7 +224,7 @@ type UploadVideoOptions = {
224
224
  };
225
225
  /** Input shape accepted by `aq.assets.presignUploadUrl(...)`. */
226
226
  type PresignUploadUrlOptions = {
227
- /** Full sha256 (64 hex) of the bytes that will be PUT to R2. */
227
+ /** Full sha256 (64 hex) of the bytes that will be PUT to storage. */
228
228
  sha256: string;
229
229
  /** MIME type of the bytes (e.g. `image/jpeg`, `video/mp4`). */
230
230
  mime: string;
@@ -318,7 +318,7 @@ declare class AssetsApi {
318
318
  * const v = await aq.assets.variants(logoId);
319
319
  * v.map((x) => x.preset); // → ("thumb" | "sm" | … | "original")[]
320
320
  *
321
- * ⚠️ Returns `[]` — not an error — against an `asset-manager` older than the
321
+ * ⚠️ Returns `[]` — not an error — against a server deploy older than the
322
322
  * 2026-08-17 deploy, which never sent the field (doc 240 §4.3). An empty
323
323
  * array is therefore "no variants OR old server". For a plain existence
324
324
  * check prefer `hasPreset(dto, preset)` on `dto.presets`, which every server
@@ -347,7 +347,7 @@ declare class AssetsApi {
347
347
  * bytes from `variants/o.<ext>` — no need to re-upload.
348
348
  *
349
349
  * Video presets are filtered to `["poster","video","aiproxy"]` and
350
- * dispatched to the Cloud Run Job (the call returns immediately
350
+ * dispatched to the background job (the call returns immediately
351
351
  * with a dispatch handle; poll `aq.assets.get(id).status` for
352
352
  * completion).
353
353
  */
@@ -360,13 +360,13 @@ declare class AssetsApi {
360
360
  metadata: Record<string, unknown>;
361
361
  }>;
362
362
  /**
363
- * Request a presigned R2 PUT URL for direct browser-side uploads.
363
+ * Request a presigned storage PUT URL for direct browser-side uploads.
364
364
  *
365
365
  * Mirrors the first half of `aq.upload()` — the caller (typically a
366
366
  * BFF / share-link dropzone) computes sha256 in the browser, then
367
- * uploads bytes straight to R2 with the returned `upload.url`, then
368
- * POSTs `process.body` to `/assets/process` (see {@link processAndWait})
369
- * once R2 has the bytes.
367
+ * uploads bytes straight to object storage with the returned `upload.url`,
368
+ * then POSTs `process.body` to `/assets/process` (see {@link processAndWait})
369
+ * once storage has the bytes.
370
370
  *
371
371
  * If the sha is already known to the tenant the server short-circuits
372
372
  * with `{ deduped: true, asset }` — no PUT needed.
@@ -378,16 +378,16 @@ declare class AssetsApi {
378
378
  * if (presign.deduped) return presign.asset; // those bytes already exist; none fly
379
379
  *
380
380
  * // BROWSER: PUT straight to presign.upload.url — the bytes never touch your server.
381
- * // ⚠️ R2 answers that preflight ITSELF, so your origin must be in the BUCKET's CORS policy.
382
- * // Symptom when it is not: "PUT failed: network error" with every earlier step green —
383
- * // and it cannot be fixed in this SDK, in your app, or in `storefront_origins`.
381
+ * // ⚠️ The STORAGE BUCKET answers that preflight itself, so your origin must be in its CORS
382
+ * // policy. Symptom when it is not: "PUT failed: network error" with every earlier step
383
+ * // green — and it cannot be fixed in this SDK, in your app, or by the API's allowed origins.
384
384
  *
385
385
  * // SERVER again, forwarding presign.process.body VERBATIM:
386
386
  * const asset = await aq.assets.processAndWait(presign.process.body, { timeoutMs: 300_000 });
387
387
  * ```
388
388
  *
389
389
  * Works for images AND video. A video answers immediately with
390
- * `{ assetId, status: "processing" }` while a Cloud Run Job transcodes, so
390
+ * `{ assetId, status: "processing" }` while a background job transcodes, so
391
391
  * give `processAndWait` a bigger `timeoutMs` (a transcode + HLS ladder runs
392
392
  * 1–2 min; 300_000 is a sane floor).
393
393
  */
@@ -397,7 +397,7 @@ declare class AssetsApi {
397
397
  * {@link presignUploadUrl} call, then poll until the asset transitions
398
398
  * to `ready` or `failed`. Throws on `failed` or timeout.
399
399
  *
400
- * Use this when bytes were uploaded directly from the browser to R2
400
+ * Use this when bytes were uploaded directly from the browser to storage
401
401
  * `aq.upload()` already does presign + PUT + process + wait in one
402
402
  * step when the server holds the bytes.
403
403
  */
@@ -420,7 +420,7 @@ declare class AssetsApi {
420
420
  * {@link waitReady} (typical timeout: 10 min for multi-segment kits).
421
421
  *
422
422
  * Tenant scope is inherited from the SDK client; `tenantCode` is added
423
- * to the request body so the Cloud Run Job can resolve it without
423
+ * to the request body so the background job can resolve it without
424
424
  * re-reading the header.
425
425
  */
426
426
  composeMarketing(opts: ComposeMarketingOptions): Promise<ComposeMarketingResult>;
@@ -451,7 +451,7 @@ type UploadOptions = {
451
451
  * MIME type of the bytes. **Only needed for a `Uint8Array` input** — a `File`/`Blob`
452
452
  * already carries its `.type`. Raw bytes have no inherent MIME, so without this (and
453
453
  * without an extension on `fileName` to infer from) they upload as
454
- * `application/octet-stream`, which the asset-manager classifies as `kind:"other"` —
454
+ * `application/octet-stream`, which the server classifies as `kind:"other"` —
455
455
  * meaning NO image/video variants are generated and `regenerate()` is unsupported.
456
456
  * Resolution order for the effective MIME: `Blob.type` → `contentType` →
457
457
  * inferred from `fileName`'s extension → `application/octet-stream`.
@@ -567,8 +567,35 @@ type UsageWindow = {
567
567
  admins: number;
568
568
  upscales: number;
569
569
  processes: number;
570
- bytesIn: number;
570
+ /**
571
+ * Minutes of video actually TRANSCODED in the window — source minutes ×
572
+ * encode passes, so a 7-rung HLS ladder over a 2-minute clip books 14.
573
+ *
574
+ * A re-encode of a video already ingested counts AGAIN. That is the point:
575
+ * `assets.assets` cannot report it, because its insert is
576
+ * `ON CONFLICT DO UPDATE` and a re-encode creates no row there.
577
+ *
578
+ * A stream copy (`-c copy` passthrough, an idempotent cache skip) books
579
+ * nothing, so a tenant that ran no encoder reads `0` — and that `0` is real.
580
+ */
581
+ videoMinutes: number;
582
+ /** Encode passes behind `videoMinutes`. 0 when nothing was encoded. */
583
+ videoEncodes: number;
571
584
  };
585
+ /**
586
+ * ⚠️ REMOVED 2026-08-17: `bytesIn` on `UsageWindow`/`UsageDailyPoint` and
587
+ * `bytesTotal` on `UsagePerKey`.
588
+ *
589
+ * All three were structurally zero: the field behind them was never populated,
590
+ * on any row. They could not be instrumented in place either — uploads go to
591
+ * object storage through presigned URLs, so the payload never passes through
592
+ * the API, and the only figure measurable there is a JSON envelope of a few
593
+ * hundred bytes.
594
+ *
595
+ * A field that always reads `0` is worse than an absent field, because absent
596
+ * is honest — a `0` next to a real `storage.totalBytes` reads as a measurement.
597
+ * The byte number that IS true is still served: `storage.totalBytes`.
598
+ */
572
599
  type UsageDailyPoint = {
573
600
  date: string;
574
601
  reads: number;
@@ -576,15 +603,16 @@ type UsageDailyPoint = {
576
603
  lists: number;
577
604
  deletes: number;
578
605
  processes: number;
579
- bytesIn: number;
580
606
  bytesStored: number;
607
+ /** See `UsageWindow.videoMinutes`. Minutes transcoded on this day. */
608
+ videoMinutes: number;
609
+ videoEncodes: number;
581
610
  };
582
611
  type UsagePerKey = {
583
612
  apiKeyId: string;
584
613
  prefix: string | null;
585
614
  name: string | null;
586
615
  opsTotal: number;
587
- bytesTotal: number;
588
616
  lastSeen: string;
589
617
  };
590
618
  declare class UsageApi {
@@ -644,7 +672,7 @@ declare class NitidaClient {
644
672
  * Returns the canonical `lg` variant URL when called with empty options,
645
673
  * so callers can swap `urlFor()` for `transform()` without thinking.
646
674
  *
647
- * URLs with the same params in different order produce the same R2
675
+ * URLs with the same params in different order produce the same
648
676
  * cache entry (the server canonicalizes both sides). Safe to use as
649
677
  * stable cache keys.
650
678
  *
@@ -675,8 +703,8 @@ declare class NitidaClient {
675
703
  * Build an on-the-fly VIDEO transform URL — Phase 4.
676
704
  *
677
705
  * Same DSL shape as `transform()` but the URL has a `.mp4` (default)
678
- * or `.webm` extension and the server routes the request to a Cloud
679
- * Run Job for ffmpeg encoding (vs the inline sharp pipeline for
706
+ * or `.webm` extension and the server routes the request to a
707
+ * background job for video encoding (vs the inline pipeline for
680
708
  * images).
681
709
  *
682
710
  * On the first request the route returns **202 Accepted** with
@@ -716,7 +744,7 @@ declare class NitidaClient {
716
744
  * />
717
745
  *
718
746
  * On the first request the server returns **202 Accepted** while a
719
- * Cloud Run Job builds the multi-rung ladder (typically 1-3 min for
747
+ * background job builds the multi-rung ladder (typically 1-3 min for
720
748
  * a 90 s source — five rungs of 240p/360p/480p/720p/1080p @ AAC).
721
749
  * Subsequent requests hit the cache → **302** to the master.m3u8.
722
750
  *
@@ -735,7 +763,7 @@ declare class NitidaClient {
735
763
  * but isn't always available depending on the runtime).
736
764
  */
737
765
  /**
738
- * Upload bytes end to end: optional client compression → sha256 → presign → **direct-to-R2 PUT**
766
+ * Upload bytes end to end: optional client compression → sha256 → presign → **direct-to-storage PUT**
739
767
  * → `/assets/process` → wait until the asset is ready.
740
768
  *
741
769
  * ⚠️ `presets` decides what exists FOREVER. Omit it and only `original` is written; ask for
@@ -775,8 +803,8 @@ declare class NitidaClient {
775
803
  * // {poster, video, aiproxy, probe} before dispatching the transcode Job.
776
804
  * await aq.upload(clip, { fileName: "tour.mp4", presets: ["poster", "video"] });
777
805
  *
778
- * // Omit `aiproxy`/`probe` unless the asset really goes to a vision model — they cost Job time
779
- * // and permanent R2 objects that nothing else reads.
806
+ * // Omit `aiproxy`/`probe` unless the asset really goes to a vision model — they cost encode
807
+ * // time and permanent stored objects that nothing else reads.
780
808
  * ```
781
809
  */
782
810
  upload(input: File | Blob | Uint8Array, opts?: UploadOptions): Promise<UploadResult>;
package/dist/index.js CHANGED
@@ -223,7 +223,7 @@ var AssetsApi = class {
223
223
  * const v = await aq.assets.variants(logoId);
224
224
  * v.map((x) => x.preset); // → ("thumb" | "sm" | … | "original")[]
225
225
  *
226
- * ⚠️ Returns `[]` — not an error — against an `asset-manager` older than the
226
+ * ⚠️ Returns `[]` — not an error — against a server deploy older than the
227
227
  * 2026-08-17 deploy, which never sent the field (doc 240 §4.3). An empty
228
228
  * array is therefore "no variants OR old server". For a plain existence
229
229
  * check prefer `hasPreset(dto, preset)` on `dto.presets`, which every server
@@ -255,7 +255,7 @@ var AssetsApi = class {
255
255
  * bytes from `variants/o.<ext>` — no need to re-upload.
256
256
  *
257
257
  * Video presets are filtered to `["poster","video","aiproxy"]` and
258
- * dispatched to the Cloud Run Job (the call returns immediately
258
+ * dispatched to the background job (the call returns immediately
259
259
  * with a dispatch handle; poll `aq.assets.get(id).status` for
260
260
  * completion).
261
261
  */
@@ -283,13 +283,13 @@ var AssetsApi = class {
283
283
  return await r.json();
284
284
  }
285
285
  /**
286
- * Request a presigned R2 PUT URL for direct browser-side uploads.
286
+ * Request a presigned storage PUT URL for direct browser-side uploads.
287
287
  *
288
288
  * Mirrors the first half of `aq.upload()` — the caller (typically a
289
289
  * BFF / share-link dropzone) computes sha256 in the browser, then
290
- * uploads bytes straight to R2 with the returned `upload.url`, then
291
- * POSTs `process.body` to `/assets/process` (see {@link processAndWait})
292
- * once R2 has the bytes.
290
+ * uploads bytes straight to object storage with the returned `upload.url`,
291
+ * then POSTs `process.body` to `/assets/process` (see {@link processAndWait})
292
+ * once storage has the bytes.
293
293
  *
294
294
  * If the sha is already known to the tenant the server short-circuits
295
295
  * with `{ deduped: true, asset }` — no PUT needed.
@@ -301,16 +301,16 @@ var AssetsApi = class {
301
301
  * if (presign.deduped) return presign.asset; // those bytes already exist; none fly
302
302
  *
303
303
  * // BROWSER: PUT straight to presign.upload.url — the bytes never touch your server.
304
- * // ⚠️ R2 answers that preflight ITSELF, so your origin must be in the BUCKET's CORS policy.
305
- * // Symptom when it is not: "PUT failed: network error" with every earlier step green —
306
- * // and it cannot be fixed in this SDK, in your app, or in `storefront_origins`.
304
+ * // ⚠️ The STORAGE BUCKET answers that preflight itself, so your origin must be in its CORS
305
+ * // policy. Symptom when it is not: "PUT failed: network error" with every earlier step
306
+ * // green — and it cannot be fixed in this SDK, in your app, or by the API's allowed origins.
307
307
  *
308
308
  * // SERVER again, forwarding presign.process.body VERBATIM:
309
309
  * const asset = await aq.assets.processAndWait(presign.process.body, { timeoutMs: 300_000 });
310
310
  * ```
311
311
  *
312
312
  * Works for images AND video. A video answers immediately with
313
- * `{ assetId, status: "processing" }` while a Cloud Run Job transcodes, so
313
+ * `{ assetId, status: "processing" }` while a background job transcodes, so
314
314
  * give `processAndWait` a bigger `timeoutMs` (a transcode + HLS ladder runs
315
315
  * 1–2 min; 300_000 is a sane floor).
316
316
  */
@@ -338,7 +338,7 @@ var AssetsApi = class {
338
338
  * {@link presignUploadUrl} call, then poll until the asset transitions
339
339
  * to `ready` or `failed`. Throws on `failed` or timeout.
340
340
  *
341
- * Use this when bytes were uploaded directly from the browser to R2
341
+ * Use this when bytes were uploaded directly from the browser to storage
342
342
  * `aq.upload()` already does presign + PUT + process + wait in one
343
343
  * step when the server holds the bytes.
344
344
  */
@@ -387,7 +387,7 @@ var AssetsApi = class {
387
387
  * {@link waitReady} (typical timeout: 10 min for multi-segment kits).
388
388
  *
389
389
  * Tenant scope is inherited from the SDK client; `tenantCode` is added
390
- * to the request body so the Cloud Run Job can resolve it without
390
+ * to the request body so the background job can resolve it without
391
391
  * re-reading the header.
392
392
  */
393
393
  async composeMarketing(opts) {
@@ -553,8 +553,8 @@ var NitidaClient = class {
553
553
  * Build an on-the-fly VIDEO transform URL — Phase 4.
554
554
  *
555
555
  * Same DSL shape as `transform()` but the URL has a `.mp4` (default)
556
- * or `.webm` extension and the server routes the request to a Cloud
557
- * Run Job for ffmpeg encoding (vs the inline sharp pipeline for
556
+ * or `.webm` extension and the server routes the request to a
557
+ * background job for video encoding (vs the inline pipeline for
558
558
  * images).
559
559
  *
560
560
  * On the first request the route returns **202 Accepted** with
@@ -596,7 +596,7 @@ var NitidaClient = class {
596
596
  * />
597
597
  *
598
598
  * On the first request the server returns **202 Accepted** while a
599
- * Cloud Run Job builds the multi-rung ladder (typically 1-3 min for
599
+ * background job builds the multi-rung ladder (typically 1-3 min for
600
600
  * a 90 s source — five rungs of 240p/360p/480p/720p/1080p @ AAC).
601
601
  * Subsequent requests hit the cache → **302** to the master.m3u8.
602
602
  *
@@ -617,7 +617,7 @@ var NitidaClient = class {
617
617
  * but isn't always available depending on the runtime).
618
618
  */
619
619
  /**
620
- * Upload bytes end to end: optional client compression → sha256 → presign → **direct-to-R2 PUT**
620
+ * Upload bytes end to end: optional client compression → sha256 → presign → **direct-to-storage PUT**
621
621
  * → `/assets/process` → wait until the asset is ready.
622
622
  *
623
623
  * ⚠️ `presets` decides what exists FOREVER. Omit it and only `original` is written; ask for
@@ -657,8 +657,8 @@ var NitidaClient = class {
657
657
  * // {poster, video, aiproxy, probe} before dispatching the transcode Job.
658
658
  * await aq.upload(clip, { fileName: "tour.mp4", presets: ["poster", "video"] });
659
659
  *
660
- * // Omit `aiproxy`/`probe` unless the asset really goes to a vision model — they cost Job time
661
- * // and permanent R2 objects that nothing else reads.
660
+ * // Omit `aiproxy`/`probe` unless the asset really goes to a vision model — they cost encode
661
+ * // time and permanent stored objects that nothing else reads.
662
662
  * ```
663
663
  */
664
664
  async upload(input, opts = {}) {
@@ -731,7 +731,7 @@ var NitidaClient = class {
731
731
  body: new Blob([bytes], { type: mime })
732
732
  });
733
733
  if (!putR.ok)
734
- throw new Error(`R2 PUT ${putR.status}: ${await putR.text()}`);
734
+ throw new Error(`Storage PUT ${putR.status}: ${await putR.text()}`);
735
735
  const procR = await fetch(endpointHref(this.opts, presign.process.url), {
736
736
  method: "POST",
737
737
  headers: {