@nitida/sdk 0.25.2 → 0.26.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 CHANGED
@@ -24,6 +24,40 @@ import { getTransformUrl, setCdnBase, setTenantId } from "@nitida/asset-client";
24
24
  - **Video URLs** → `getAssetUrl({ sha }, "video")` after `setTenantId(id)`. The path segment is
25
25
  **base36** (`tenant 12 → /c/v/…`); a decimal prefix 404s.
26
26
 
27
+ ## Which entry point exports what — read this before your first import
28
+
29
+ **`@nitida/sdk`, `@nitida/sdk/server` and `@nitida/sdk/web` all carry the same
30
+ surface.** Pick the one that matches your runtime and import everything from it:
31
+
32
+ ```ts
33
+ import { NitidaClient, getTransformUrl, getHlsLadder } from "@nitida/sdk/server";
34
+ ```
35
+
36
+ The other three subpaths are **additive** — they carry only their own
37
+ runtime-specific symbols and you import a complete one alongside:
38
+
39
+ | subpath | carries | |
40
+ |---|---|---|
41
+ | `/react` | `NitidaProvider`, `useSlot`, `useSlots`, `useNitidaClient` | + `/web` or `/server` |
42
+ | `/native` | `compressImage`, `compressImages` for React Native | + `/web` |
43
+ | `/expo` | `createExpoUploader`, `listResumableSessions`, `cancelResumableSession` | + `/web` |
44
+
45
+ ⚠️ **React Native needs two imports.** `/web` for the client and every URL
46
+ builder — it has no top-level browser imports, its compressor is a dynamic
47
+ `import()` — plus `/native` and/or `/expo`. `/native` on its own gives you a
48
+ compressor and no way to build a URL.
49
+
50
+ The single deliberate omission: **`NitidaClientOptions` is not on `/web`**, because
51
+ it carries `apiKey` and this subpath exists so that shape is unreachable from
52
+ browser code. Use `WebClientOptions`.
53
+
54
+ ⚠️ **This was not true before 2026-08-21.** `/server` was short **28** of the
55
+ root's 72 exports and `/web` short **37** — the whole palette family, every slot
56
+ helper, the HLS ladder helpers, the preset constants, and on `/web` even
57
+ `setTenantId`. Importing `getHlsLadder` from `/server` failed at **runtime**, not
58
+ at compile time. It is now asserted in CI, so if you are reading an older
59
+ version of this file, verify against the `.d.ts` you actually installed.
60
+
27
61
  ## The four things that bite hardest
28
62
 
29
63
  1. **`presets` decides what exists forever.** Omit it and you get `original` only. Ask for
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # `@nitida/sdk`
2
2
 
3
- Universal SDK for the [aquienpz](https://aquienpz.com) multi-tenant asset platform.
3
+ Universal SDK for the [nitida](https://nitida.gofuture.space) multi-tenant media platform.
4
4
  A Cloudinary-style asset manager with deterministic CDN URLs, content-addressed
5
5
  dedup, server-side variants (thumb / sm / md / lg / poster / video), and **slot
6
6
  bindings** — admin-managed names that resolve to assets at runtime.
@@ -15,14 +15,45 @@ SDK**. Stripe/Cloudinary's split into two separate npm packages is the legacy
15
15
  shape — modern bundlers tree-shake subpaths perfectly and a single version
16
16
  eliminates type drift between server and client surfaces.
17
17
 
18
- | Where you run | Import | What it bundles |
18
+ ### Two kinds of subpath, and the difference matters
19
+
20
+ **COMPLETE entry points** carry the whole surface. Import one and you are done.
21
+
22
+ | Where you run | Import | |
23
+ |---|---|---|
24
+ | Node 20+, Bun, Cloud Run, Lambda, Edge, agents, cron | **`@nitida/sdk/server`** | `NitidaClient` + **every** export of the root. **No browser-only code.** |
25
+ | Browser, and React Native | **`@nitida/sdk/web`** | everything in `/server`, plus `compressImage` (HEIC→WebP). The one deliberate omission is `NitidaClientOptions`, the options type that carries `apiKey` — use `WebClientOptions`. |
26
+ | Anywhere (the root) | `@nitida/sdk` | the full surface, runtime-agnostic. Prefer an explicit subpath in new code. |
27
+
28
+ **ADDITIVE modules** are layered *on top* of one of the above. They carry only
29
+ their own runtime-specific symbols — **you import a complete entry point as
30
+ well**:
31
+
32
+ | For | Import | Carries |
19
33
  |---|---|---|
20
- | Node 20+, Bun, Cloud Run, Lambda, Edge, agents, cron | **`@nitida/sdk/server`** | `NitidaClient` + URL builders + `signTransformUrl`. **No browser-only code.** Hard guarantee against shipping `Blob`-workflow helpers to the wrong runtime. |
21
- | Browser (Next.js client, Vite, CRA, web workers) | **`@nitida/sdk/web`** | `NitidaClient` + everything in `/server` + `compressImage` (HEIC→WebP, compressorjs). Multipart is not exposed — see *Large uploads (web)*. |
22
- | React Native / Expo apps | `@nitida/sdk/native` | `compressImage` for RN (libjpeg-turbo via `react-native-compressor`). Pair with `/expo`. |
23
- | Expo background uploads | `@nitida/sdk/expo` | `createExpoUploader` — native `URLSession` (iOS) / `WorkManager` (Android) sessions that survive app backgrounding. |
24
- | Next.js / React (hooks) | `@nitida/sdk/react` | `NitidaProvider` + `useSlot` / `useSlots` / `useNitidaClient`. |
25
- | Backwards-compat (root) | `@nitida/sdk` | Same shape as today; equivalent to `/web` for the public symbols. **Prefer the explicit subpath in new code.** |
34
+ | React hooks | `@nitida/sdk/react` | `NitidaProvider`, `useSlot`, `useSlots`, `useNitidaClient` and nothing else |
35
+ | RN image compression | `@nitida/sdk/native` | `compressImage` / `compressImages` for RN (`react-native-compressor`) |
36
+ | Expo background uploads | `@nitida/sdk/expo` | `createExpoUploader` `URLSession` / `WorkManager` sessions that survive backgrounding. ⚠️ **Needs a runtime key on the device** — see below |
37
+
38
+ ⚠️ **`/expo` is the one place a runtime key lives on the device.**
39
+ `createExpoUploader` takes `apiKey` off the client and gives it to the native
40
+ session as its `authToken`, because the OS replays that upload from a background
41
+ task hours later and there is no BFF in that loop. It is the opposite call from
42
+ `@nitida/sdk/web`, which hit the same constraint and chose *not* to expose its
43
+ multipart uploader at all. An `amk_rt_*` in an IPA/APK is readable by anyone who
44
+ unzips it, and it is a write key to a paid platform. If background survival is
45
+ not worth that, build the client from `/web` against your own route and use
46
+ `aq.upload(file)` — no key on the device, and the upload dies with the JS thread.
47
+
48
+ ⚠️ **React Native needs two imports**, and this table used to imply otherwise:
49
+ `@nitida/sdk/web` for the client and every URL builder — it has no top-level
50
+ browser imports, the compressor behind it is a dynamic `import()` — **plus**
51
+ `/native` and/or `/expo` for the native pieces. `/native` alone gives you a
52
+ compressor and no way to build a URL.
53
+
54
+ That `/server` and `/web` really do mirror the root is **asserted in CI**, not
55
+ maintained by hand. It was not always true: before 2026-08-21 `/server` was
56
+ short 28 of the root's 72 exports and `/web` short 37.
26
57
 
27
58
  Uploaders + compressors are **optional** peer deps (`@aquienpz/asset-uploader-{web,expo}`,
28
59
  `@nitida/asset-compressor-{web,native}`). Skip them if your app only resolves
@@ -32,8 +63,9 @@ slots and reads assets — your bundle stays a few KB.
32
63
 
33
64
  - Server-side rendering, API routes, BFF, cron, agents → `@nitida/sdk/server`.
34
65
  - Browser components, build-time pre-resolution → `@nitida/sdk/web`.
35
- - Expo / React Native app → `@nitida/sdk/native` + `@nitida/sdk/expo`.
36
- - React hooks (any env) → `@nitida/sdk/react`.
66
+ - Expo / React Native app → `@nitida/sdk/web` (the client and the URL builders)
67
+ **plus** `@nitida/sdk/native` and/or `@nitida/sdk/expo` for the native pieces.
68
+ - React hooks (any env) → `@nitida/sdk/react` **plus** `/web` or `/server`.
37
69
 
38
70
  **Never ship the API key to the browser.** Whichever subpath you import,
39
71
  the long-lived `amk_rt_*` key lives on the server. Browser flows hit a BFF
@@ -45,8 +77,44 @@ route that proxies to aquienpz with the real key.
45
77
  |---|---|---|
46
78
  | Browser (Next.js client component, SPA, Worker, web extension) | `@nitida/sdk/web` | **No `apiKey`** — your BFF injects it. Type omits the field; build error if you try. |
47
79
  | Node/Bun server (API route, Server Action, Cloud Run, Fly, BFF) | `@nitida/sdk/server` | Requires `apiKey` at construction. |
48
- | React Native / Expo | `@nitida/sdk/native` or `@nitida/sdk/expo` | Same BFF model as `/web` — keep keys server-side. |
49
- | Universal React hook | `@nitida/sdk/react` | Wraps the right subpath for the runtime. |
80
+ | React Native / Expo | `@nitida/sdk/web` **plus** `/native` and/or `/expo` | Same BFF model as `/web` — keep keys server-side. `/native` and `/expo` are additive; on their own they carry no client. |
81
+ | Universal React hook | `@nitida/sdk/react` **plus** `/web` or `/server` | The hooks read a client from context; something has to construct it. |
82
+
83
+ ### …and where does each HELPER live?
84
+
85
+ The table above answers *"where do I get the client"*. It did **not** used to
86
+ answer *"where do I get `getHlsLadder`"*, and that is the question that cost the
87
+ most time — an agent evaluating this SDK followed `AGENTS.md` to `/server`,
88
+ imported `getHlsLadder` from there, and got a `SyntaxError` at runtime.
89
+
90
+ **Now there is a one-line answer, and CI keeps it true:**
91
+
92
+ > **`@nitida/sdk`, `/server` and `/web` all carry the same surface.**
93
+ > Import any helper from whichever of the three you already use.
94
+
95
+ | what you want | `@nitida/asset-client` | `@nitida/sdk` · `/server` · `/web` | elsewhere |
96
+ |---|---|---|---|
97
+ | URL builders — `getAssetUrl`, `getTransformUrl`, `getVideoTransformUrl`, `getAssetSrcSet`, `getTransformSrcSet`, `getHlsStreamingUrl`, `serializeTransform` | ✅ | ✅ | |
98
+ | Signing — `signTransformUrl`, `getSignedTransformUrl` | ✅ | ✅ | |
99
+ | Asset facts — `hasPreset`, `extractAssetSha`, `getAssetDimensions`, `computeVariantDimensions` | ✅ | ✅ | |
100
+ | HLS — `getHlsLadder`, `hlsLadderAlignment`, `HlsRung` | ✅ | ✅ | |
101
+ | Palette — `pickAmbientBackground`, `iteratePaletteSwatches`, `bestTextContrast`, `getAmbientGradient`, `getPaletteCssVars`, `getPaletteBlurBackground`, `getTextColorForBackground`, `contrastRatio`, `relativeLuminance` | ✅ | ✅ | |
102
+ | Slots — `resolveSlot`, `resolveSlots`, `configureSlotResolver`, `invalidateSlotCache` | ✅ | ✅ | |
103
+ | Constants — `TRANSFORM_WIDTHS`, `TransformWidth`, `PRESET_EXT`, `PRESET_LONG`, `PRESET_SHORT`, `PRESET_MAX_DIM` | ✅ | ✅ | |
104
+ | Config — `setCdnBase`/`getCdnBase`, `setTenantId`/`getTenantId` | ✅ | ✅ | |
105
+ | `NitidaClient` and its option/result types | | ✅ | |
106
+ | Client-side compression — `compressImage`, `compressImages` | | `/web` only | `/native` (React Native) |
107
+ | React hooks — `NitidaProvider`, `useNitidaClient`, `useSlot`, `useSlots` | | | `/react` only |
108
+ | Expo resumable upload — `createExpoUploader`, `listResumableSessions`, `cancelResumableSession` | | | `/expo` only |
109
+
110
+ The single deliberate exception: **`NitidaClientOptions` is not on `/web`.** That
111
+ is the options type that carries `apiKey`, and this subpath exists so the
112
+ apiKey-bearing shape is unreachable from browser code — use `WebClientOptions`.
113
+
114
+ This is not maintained by hand. `scripts/check-published-doc-symbols.ts` asserts
115
+ that `/server` and `/web` mirror the root, and that the one denial above still
116
+ has a written reason. Before 2026-08-21 it was not true: `/server` was short
117
+ **28** of the root's 72 and `/web` short **37**, `getHlsLadder` among them.
50
118
 
51
119
  ### Why subpaths, not a runtime flag
52
120
 
@@ -543,6 +611,39 @@ The preset is a **ceiling**, not a target.
543
611
  | `video` | `v` | original MP4 |
544
612
  | `aiproxy` | `a` | low-res proxy for AI captioning / search (opt-in) |
545
613
 
614
+ ### ⚠️ What you can ASK FOR is not what a variant can BE
615
+
616
+ Two different sets, two different types, and confusing them is the most
617
+ expensive type error this package has shipped — three unknown agents evaluating
618
+ the SDK hit it independently in one afternoon.
619
+
620
+ ```ts
621
+ aq.assets.regenerate(id, { presets: ["hls"] }); // ← used to compile. HTTP 400.
622
+ aq.upload(file, { presets: ["mp3"] }); // ← used to compile. HTTP 400.
623
+ ```
624
+
625
+ Both symbols are real. Neither is orderable.
626
+
627
+ | | `RequestablePreset` — you may ask | `VariantPreset` — a variant may be |
628
+ |---|---|---|
629
+ | `thumb` `sm` `md` `lg` `xl` `original` `poster` `video` `aiproxy` | ✅ | ✅ |
630
+ | `hls` — the adaptive ladder, built when a video transcodes | ❌ | ✅ |
631
+ | `mp3` — emitted automatically alongside any audio original | ❌ | ✅ |
632
+ | `probe` — indexed stills (`-pr0.jpg`), never on the compact `presets` string | ✅ | ❌ |
633
+
634
+ The write methods (`upload`, `presignUploadUrl`, `regenerate`) take
635
+ `RequestablePreset[]`, so all three lines above are now **compile errors**. The
636
+ read helpers (`hasPreset`, `variants[].preset`, `PRESET_SHORT`) keep
637
+ `VariantPreset`, because `hls` and `mp3` genuinely do exist on assets.
638
+
639
+ `probe` was the same bug mirrored: the server has always accepted it and the
640
+ type forbade it. It is requestable now.
641
+
642
+ This is checked in CI against the server itself —
643
+ `scripts/check-preset-contract.ts` reads the Elysia schemas of all four write
644
+ routes and asserts `RequestablePreset` still equals what they accept, so it
645
+ cannot go stale the way a hand-written list would.
646
+
546
647
  ### Per-upload preset selection
547
648
 
548
649
  **The SDK's default is `["original"]`** — calling `aq.upload(file)`
@@ -679,7 +780,23 @@ returned shape gives you `{ preset, url, width?, height?, bytes }[]`.
679
780
 
680
781
  Pre-generated presets (`thumb`/`sm`/`md`/`lg`/`xl`) cover the common cases.
681
782
  For everything else — exact CSS pixel widths, art-directed crops, devicePixelRatio
682
- ladders, square thumbs from rectangular sources — call `aq.transform()`:
783
+ ladders, square thumbs from rectangular sources — build a `/t/` URL.
784
+
785
+ ### `aq.transform()` or `getTransformUrl()`? Both, and the rule is which you have
786
+
787
+ Two agents evaluating this SDK picked different ones for the same task and both
788
+ were right, which is a documentation failure rather than a design one. They are
789
+ **not duplicates**: `aq.transform()` is `getTransformUrl()` plus the client's
790
+ context.
791
+
792
+ | you have | use | why |
793
+ |---|---|---|
794
+ | a client (`aq`) | **`aq.transform(asset, opts)`** | it already knows your CDN base and tenant, and it is the only one that can **sign** — `aq.transform(asset, opts, { sign: true })` returns a `?sig=` URL, which is the only way to an off-ladder width |
795
+ | only a DTO — a component, a Server Component, a worker | **`getTransformUrl(asset, opts)`** | no client needed. Call `setCdnBase()` / `setTenantId()` once at module load first |
796
+
797
+ Same builder underneath, same URL out. If you are holding a client, use its
798
+ method — reaching for the standalone one there means passing the tenant twice
799
+ and losing signing.
683
800
 
684
801
  ```tsx
685
802
  import { NitidaClient } from "@nitida/sdk";
@@ -809,16 +926,32 @@ hex-encoded. The server canonicalizes the URL the same way the SDK does
809
926
  (sort keys, lowercase strings), so two URLs with the same params in
810
927
  different order accept the same signature.
811
928
 
812
- **Admin operations** (system-scope admin key):
929
+ ### ⚠️ Admin operations need the SYSTEM key, not the admin key in your triplet
930
+
931
+ Both are `amk_ad_*`, and that is the whole trap. The `admin` key issued with
932
+ your tenant is **tenant-scoped**: it manages your assets and cannot touch
933
+ `/admin/projects/*`. Verified 2026-08-21:
934
+
935
+ ```
936
+ tenant admin key → GET /admin/projects → 403 SYSTEM_KEY_REQUIRED
937
+ "API key … is tenant-scoped; this endpoint requires a
938
+ system-scope key"
939
+ system admin key → GET /admin/projects → 200
940
+ ```
941
+
942
+ There is exactly one system-scope key and we hold it. **These calls are ours to
943
+ run, not yours** — ask, and we run them. They are documented here so you know
944
+ what exists and can name what you want done, not so you can copy the `amk_ad_`
945
+ out of your own triplet and get a 403 that reads like a broken credential.
813
946
 
814
947
  ```bash
815
948
  # Rotate the signing key — invalidates every URL signed with the old one.
816
- curl -X POST -H "Authorization: Bearer amk_ad_..." \
949
+ curl -X POST -H "Authorization: Bearer <SYSTEM amk_ad_...>" \
817
950
  https://api.nitida.gofuture.space/admin/projects/your-tenant/rotate-signing-key
818
951
  # → { ok: true, tenantId, code, signingKey: "<64-hex>" }
819
952
 
820
953
  # Flip strict mode on/off.
821
- curl -X PATCH -H "Authorization: Bearer amk_ad_..." -H "Content-Type: application/json" \
954
+ curl -X PATCH -H "Authorization: Bearer <SYSTEM amk_ad_...>" -H "Content-Type: application/json" \
822
955
  -d '{"enabled":true}' \
823
956
  https://api.nitida.gofuture.space/admin/projects/your-tenant/strict-transforms
824
957
  ```
package/dist/expo.d.ts CHANGED
@@ -31,6 +31,33 @@ import '@nitida/asset-client';
31
31
  * const { assetId } = await upload.start();
32
32
  * await aq.slots.bind("storefront.tour.video", { assetId, preset: "video" });
33
33
  *
34
+ * ⚠️ THIS PATH PUTS A RUNTIME KEY ON THE DEVICE. Read before shipping.
35
+ *
36
+ * `createExpoUploader` reads `client.opts.apiKey` and hands it to the native
37
+ * session as its `authToken` (see below — it is four lines, and they are the
38
+ * whole story). The native uploader talks to the API directly, so it needs a
39
+ * credential the OS can replay hours later from a background task. There is no
40
+ * BFF in that loop to inject one.
41
+ *
42
+ * That is the opposite of what every other surface here does, and the opposite
43
+ * of what `@nitida/sdk/web` chose when it hit the SAME constraint: `/web`
44
+ * deliberately does NOT expose the multipart uploader, because it needs a raw
45
+ * `authToken` a BFF cannot supply. `/expo` exposes it anyway, because
46
+ * background-surviving uploads are the entire reason a native session exists.
47
+ *
48
+ * So, concretely, an `amk_rt_*` key in your app bundle is readable by anyone
49
+ * who unzips the IPA/APK, and it is a WRITE key to a paid platform.
50
+ *
51
+ * - Uploading big video in the background is worth it to you ⇒ use this, and
52
+ * scope the key to one tenant so a leak is contained and revocable.
53
+ * - It is not ⇒ build the client from `@nitida/sdk/web` pointed at your own
54
+ * route and call `aq.upload(file)`. No key on the device. You lose survival
55
+ * across backgrounding and OS kill; the upload dies with the JS thread.
56
+ *
57
+ * The real fix — a BFF-minted short-lived token the native session can carry —
58
+ * is not built. It is the same gap `/web` documents. Ask; there is no public
59
+ * tracker.
60
+ *
34
61
  * Peer dep: `@aquienpz/asset-uploader-expo` (lazy — apps that don't
35
62
  * use the mobile SDK skip the install).
36
63
  * @module @nitida/sdk/expo
@@ -39,9 +66,11 @@ import '@nitida/asset-client';
39
66
  type ExpoUploadOptions = Omit<UploadTaskOptions, "tenantCode" | "endpoint" | "authToken">;
40
67
  /**
41
68
  * Spawn a native-backed `UploadTask` bound to a configured client.
42
- * Inherits the client's endpoint / api key / tenant scope; caller only
43
- * has to supply the file input + any per-upload tuning (partSize,
44
- * concurrency).
69
+ *
70
+ * ⚠️ It reaches into the client for `apiKey` and uses it as the session's
71
+ * `authToken`, so the client you pass MUST have been built with a real runtime
72
+ * key — which means that key is on the device. See the module header for what
73
+ * that costs and what the alternative is.
45
74
  */
46
75
  declare function createExpoUploader(client: NitidaClient, options: ExpoUploadOptions): UploadTask;
47
76
 
package/dist/expo.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/expo/index.ts"],"sourcesContent":["/**\n * @nitida/sdk/expo — native multipart uploads for React Native / Expo.\n *\n * Wraps `@aquienpz/asset-uploader-expo`'s `UploadTask`, which delegates\n * the actual byte transfer to a native background session (URLSession\n * on iOS, WorkManager on Android). The upload survives:\n * - JS thread freezing\n * - App backgrounding\n * - OS-initiated kill (low-memory, user swiping away)\n * - Network blips (retries with backoff)\n *\n * Usage:\n *\n * import { createExpoUploader, listResumableSessions } from \"@nitida/sdk/expo\";\n *\n * const aq = new NitidaClient({ ... });\n *\n * // Resume any uploads from a prior app launch on boot.\n * const resumable = await listResumableSessions();\n * // ...show a banner offering to resume them\n *\n * const upload = createExpoUploader(aq, {\n * file: { uri: assetUri, mime: \"video/mp4\", name: \"tour.mp4\" },\n * });\n * upload.on(\"progress\", ({ ratio }) => setProgress(ratio));\n * const { assetId } = await upload.start();\n * await aq.slots.bind(\"storefront.tour.video\", { assetId, preset: \"video\" });\n *\n * Peer dep: `@aquienpz/asset-uploader-expo` (lazy — apps that don't\n * use the mobile SDK skip the install).\n * @module @nitida/sdk/expo\n */\n\nimport {\n UploadTask,\n type UploadTaskOptions,\n} from \"@aquienpz/asset-uploader-expo\";\nimport type { NitidaClient } from \"..\";\n\nexport type ExpoUploadOptions = Omit<\n UploadTaskOptions,\n \"tenantCode\" | \"endpoint\" | \"authToken\"\n>;\n\n/**\n * Spawn a native-backed `UploadTask` bound to a configured client.\n * Inherits the client's endpoint / api key / tenant scope; caller only\n * has to supply the file input + any per-upload tuning (partSize,\n * concurrency).\n */\nexport function createExpoUploader(\n client: NitidaClient,\n options: ExpoUploadOptions,\n): UploadTask {\n const access = client as unknown as {\n opts: { endpoint: string; apiKey: string; tenantCode: string };\n };\n return new UploadTask({\n ...options,\n endpoint: access.opts.endpoint,\n authToken: access.opts.apiKey,\n tenantCode: access.opts.tenantCode,\n });\n}\n\nexport type {\n UploadEvents,\n UploadFileInput,\n UploadSessionState,\n UploadTaskOptions,\n} from \"@aquienpz/asset-uploader-expo\";\nexport {\n cancelResumableSession,\n listResumableSessions,\n UploadTask,\n} from \"@aquienpz/asset-uploader-expo\";\n"],"mappings":";AAiCA;AAAA,EACE;AAAA,OAEK;AAmCP;AAAA,EACE;AAAA,EACA;AAAA,EACA,cAAAA;AAAA,OACK;AAzBA,SAAS,mBACd,QACA,SACY;AACZ,QAAM,SAAS;AAGf,SAAO,IAAI,WAAW;AAAA,IACpB,GAAG;AAAA,IACH,UAAU,OAAO,KAAK;AAAA,IACtB,WAAW,OAAO,KAAK;AAAA,IACvB,YAAY,OAAO,KAAK;AAAA,EAC1B,CAAC;AACH;","names":["UploadTask"]}
1
+ {"version":3,"sources":["../src/expo/index.ts"],"sourcesContent":["/**\n * @nitida/sdk/expo — native multipart uploads for React Native / Expo.\n *\n * Wraps `@aquienpz/asset-uploader-expo`'s `UploadTask`, which delegates\n * the actual byte transfer to a native background session (URLSession\n * on iOS, WorkManager on Android). The upload survives:\n * - JS thread freezing\n * - App backgrounding\n * - OS-initiated kill (low-memory, user swiping away)\n * - Network blips (retries with backoff)\n *\n * Usage:\n *\n * import { createExpoUploader, listResumableSessions } from \"@nitida/sdk/expo\";\n *\n * const aq = new NitidaClient({ ... });\n *\n * // Resume any uploads from a prior app launch on boot.\n * const resumable = await listResumableSessions();\n * // ...show a banner offering to resume them\n *\n * const upload = createExpoUploader(aq, {\n * file: { uri: assetUri, mime: \"video/mp4\", name: \"tour.mp4\" },\n * });\n * upload.on(\"progress\", ({ ratio }) => setProgress(ratio));\n * const { assetId } = await upload.start();\n * await aq.slots.bind(\"storefront.tour.video\", { assetId, preset: \"video\" });\n *\n * ⚠️ THIS PATH PUTS A RUNTIME KEY ON THE DEVICE. Read before shipping.\n *\n * `createExpoUploader` reads `client.opts.apiKey` and hands it to the native\n * session as its `authToken` (see below — it is four lines, and they are the\n * whole story). The native uploader talks to the API directly, so it needs a\n * credential the OS can replay hours later from a background task. There is no\n * BFF in that loop to inject one.\n *\n * That is the opposite of what every other surface here does, and the opposite\n * of what `@nitida/sdk/web` chose when it hit the SAME constraint: `/web`\n * deliberately does NOT expose the multipart uploader, because it needs a raw\n * `authToken` a BFF cannot supply. `/expo` exposes it anyway, because\n * background-surviving uploads are the entire reason a native session exists.\n *\n * So, concretely, an `amk_rt_*` key in your app bundle is readable by anyone\n * who unzips the IPA/APK, and it is a WRITE key to a paid platform.\n *\n * - Uploading big video in the background is worth it to you ⇒ use this, and\n * scope the key to one tenant so a leak is contained and revocable.\n * - It is not ⇒ build the client from `@nitida/sdk/web` pointed at your own\n * route and call `aq.upload(file)`. No key on the device. You lose survival\n * across backgrounding and OS kill; the upload dies with the JS thread.\n *\n * The real fix — a BFF-minted short-lived token the native session can carry —\n * is not built. It is the same gap `/web` documents. Ask; there is no public\n * tracker.\n *\n * Peer dep: `@aquienpz/asset-uploader-expo` (lazy — apps that don't\n * use the mobile SDK skip the install).\n * @module @nitida/sdk/expo\n */\n\nimport {\n UploadTask,\n type UploadTaskOptions,\n} from \"@aquienpz/asset-uploader-expo\";\nimport type { NitidaClient } from \"..\";\n\nexport type ExpoUploadOptions = Omit<\n UploadTaskOptions,\n \"tenantCode\" | \"endpoint\" | \"authToken\"\n>;\n\n/**\n * Spawn a native-backed `UploadTask` bound to a configured client.\n *\n * ⚠️ It reaches into the client for `apiKey` and uses it as the session's\n * `authToken`, so the client you pass MUST have been built with a real runtime\n * key which means that key is on the device. See the module header for what\n * that costs and what the alternative is.\n */\nexport function createExpoUploader(\n client: NitidaClient,\n options: ExpoUploadOptions,\n): UploadTask {\n const access = client as unknown as {\n opts: { endpoint: string; apiKey: string; tenantCode: string };\n };\n return new UploadTask({\n ...options,\n endpoint: access.opts.endpoint,\n authToken: access.opts.apiKey,\n tenantCode: access.opts.tenantCode,\n });\n}\n\nexport type {\n UploadEvents,\n UploadFileInput,\n UploadSessionState,\n UploadTaskOptions,\n} from \"@aquienpz/asset-uploader-expo\";\nexport {\n cancelResumableSession,\n listResumableSessions,\n UploadTask,\n} from \"@aquienpz/asset-uploader-expo\";\n"],"mappings":";AA4DA;AAAA,EACE;AAAA,OAEK;AAqCP;AAAA,EACE;AAAA,EACA;AAAA,EACA,cAAAA;AAAA,OACK;AAzBA,SAAS,mBACd,QACA,SACY;AACZ,QAAM,SAAS;AAGf,SAAO,IAAI,WAAW;AAAA,IACpB,GAAG;AAAA,IACH,UAAU,OAAO,KAAK;AAAA,IACtB,WAAW,OAAO,KAAK;AAAA,IACvB,YAAY,OAAO,KAAK;AAAA,EAC1B,CAAC;AACH;","names":["UploadTask"]}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { ResolveSlotOptions, SlotResolution, SlotDTO, VariantPreset, AssetDTO, AssetVariant, TransformOptions, SignedTransformOptions } from '@nitida/asset-client';
2
- export { AssetDTO, AssetPalette, AssetVariant, HlsRung, PRESET_EXT, PRESET_LONG, PRESET_MAX_DIM, PRESET_SHORT, PaletteSwatch, ResolveSlotOptions, SignedTransformOptions, SlotDTO, SlotResolution, TRANSFORM_WIDTHS, TransformEffect, TransformFit, TransformFormat, TransformGravity, TransformOptions, TransformWidth, VariantPreset, bestTextContrast, computeVariantDimensions, configureSlotResolver, contrastRatio, extractAssetSha, getAmbientGradient, getAssetDimensions, getAssetSrcSet, getAssetUrl, getCdnBase, getHlsLadder, getHlsStreamingUrl, getPaletteBlurBackground, getPaletteCssVars, getSignedTransformUrl, getTenantId, getTextColorForBackground, getTransformSrcSet, getTransformUrl, getVideoTransformUrl, hasPreset, hlsLadderAlignment, invalidateSlotCache, iteratePaletteSwatches, pickAmbientBackground, relativeLuminance, resolveSlot, resolveSlots, serializeTransform, setCdnBase, setTenantId, signTransformUrl } from '@nitida/asset-client';
1
+ import { ResolveSlotOptions, SlotResolution, SlotDTO, VariantPreset, AssetDTO, AssetVariant, RequestablePreset, TransformOptions, SignedTransformOptions } from '@nitida/asset-client';
2
+ export { AssetDTO, AssetPalette, AssetVariant, HlsRung, PRESET_EXT, PRESET_LONG, PRESET_MAX_DIM, PRESET_SHORT, PaletteSwatch, RequestablePreset, ResolveSlotOptions, SignedTransformOptions, SlotDTO, SlotResolution, TRANSFORM_WIDTHS, TransformEffect, TransformFit, TransformFormat, TransformGravity, TransformOptions, TransformWidth, VariantEntryPreset, VariantPreset, bestTextContrast, computeVariantDimensions, configureSlotResolver, contrastRatio, extractAssetSha, getAmbientGradient, getAssetDimensions, getAssetSrcSet, getAssetUrl, getCdnBase, getHlsLadder, getHlsStreamingUrl, getPaletteBlurBackground, getPaletteCssVars, getSignedTransformUrl, getTenantId, getTextColorForBackground, getTransformSrcSet, getTransformUrl, getVideoTransformUrl, hasPreset, hlsLadderAlignment, invalidateSlotCache, iteratePaletteSwatches, pickAmbientBackground, relativeLuminance, resolveSlot, resolveSlots, serializeTransform, setCdnBase, setTenantId, signTransformUrl } from '@nitida/asset-client';
3
3
 
4
4
  /**
5
5
  * @nitida/sdk — universal client for the aquienpz multi-tenant asset
@@ -235,8 +235,11 @@ type PresignUploadUrlOptions = {
235
235
  /**
236
236
  * Variant ladder to generate after `/assets/process`. Defaults to
237
237
  * `["original"]` server-side when omitted — same contract as `aq.upload`.
238
+ *
239
+ * {@link RequestablePreset}, not {@link VariantPreset}: `hls` and `mp3` are
240
+ * things a variant can BE, never things you can ask for, and asking is a 400.
238
241
  */
239
- presets?: VariantPreset[];
242
+ presets?: RequestablePreset[];
240
243
  /**
241
244
  * Pre-compression size of the source (useful when the browser ran
242
245
  * compressorjs / heic2any before computing `bytes`). Recorded
@@ -353,7 +356,7 @@ declare class AssetsApi {
353
356
  * completion).
354
357
  */
355
358
  regenerate(assetId: string, opts?: {
356
- presets?: VariantPreset[];
359
+ presets?: RequestablePreset[];
357
360
  }): Promise<RegenerateResult>;
358
361
  /** Merge metadata into an asset (role / slot / description / tags). */
359
362
  patchMetadata(assetId: string, metadata: Record<string, unknown>): Promise<{
@@ -479,7 +482,7 @@ type UploadOptions = {
479
482
  * this option — those go to the upload pipeline raw.
480
483
  *
481
484
  * @see {@link CompressOptions}
482
- * @see https://github.com/espaciofuturoio/aquienpz/tree/main/packages/sdk#client-side-compression-browsers
485
+ * @see https://nitida.gofuture.space/guides/advanced/client-side compression
483
486
  */
484
487
  compress?: boolean | CompressOptions;
485
488
  /**
@@ -505,8 +508,12 @@ type UploadOptions = {
505
508
  * Idempotent: you can always add missing variants later via
506
509
  * `aq.assets.regenerate(id, { presets: [...] })`. The platform
507
510
  * stores the source so regeneration doesn't require re-uploading.
511
+ *
512
+ * {@link RequestablePreset}, not {@link VariantPreset}. `hls` and `mp3` are
513
+ * produced FOR you — the ladder when a video transcodes, the mp3 alongside
514
+ * any audio original — and asking for either is a 400.
508
515
  */
509
- presets?: VariantPreset[];
516
+ presets?: RequestablePreset[];
510
517
  /**
511
518
  * Max time to wait for the asset to transition to `ready` (or `failed`)
512
519
  * after dispatch. Default `5 * 60_000` (5 min). Bump higher for large