@nitida/sdk 0.27.1 → 0.29.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
@@ -101,6 +101,22 @@ await getPrivateTransformUrl(asset, { width: 1280 }, signingKey, { expiresInSeco
101
101
  - **`exp` is mandatory**; revocation is *"within a minute"* (60 s TTL at the edge).
102
102
  - **The signing key is a backend secret** — it mints URLs for every private
103
103
  asset the tenant owns.
104
+ - **⭐ Where the signing key comes from: the response that CREATED your
105
+ project, once.** `POST /admin/projects` returns `signingKey` next to the
106
+ three API keys, and the console shows it in the same panel. Nothing else
107
+ hands it out — `GET /admin/projects/:code` does **not** include it. If it is
108
+ lost, the only endpoint that returns a key is
109
+ `POST /admin/projects/:code/rotate-signing-key`, which **invalidates every
110
+ URL already signed** and needs the system-scope key the platform operator
111
+ holds. A tenant with nothing signed yet can rotate for free; a live one
112
+ cannot, which is why you save it at creation.
113
+ - **⭐ Already have a project and never saw a signing key?** Then you never got
114
+ one — projects created before 2026-08-23 were not handed it, and no endpoint
115
+ shows you the current one. **If you have not signed any URLs yet, ask us to
116
+ rotate: with nothing in flight, rotation invalidates nothing and is free.**
117
+ If you already have signed URLs circulating, ask us for the current key
118
+ instead. This is the wall the T5 agent hit, and the sentence that was
119
+ missing.
104
120
 
105
121
  ## Two ways an image gets smaller, and only one of them is yours to call
106
122
 
@@ -151,4 +167,4 @@ degrades to a wrong choice.
151
167
  |---|---|
152
168
  | `@nitida/asset-client` | URL builders + preset tables. No network, no key. |
153
169
  | `@nitida/asset-compressor-web` | Browser image compression (worker → OffscreenCanvas → WebP). |
154
- | `@aquienpz/asset-uploader-web` | Per-file transport: multipart, resume via IndexedDB, progress. |
170
+ | `@nitida/asset-uploader-web` | Per-file transport: multipart, resume via IndexedDB, progress. |
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Espacio Futuro LTD
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -55,7 +55,7 @@ That `/server` and `/web` really do mirror the root is **asserted in CI**, not
55
55
  maintained by hand. It was not always true: before 2026-08-21 `/server` was
56
56
  short 28 of the root's 72 exports and `/web` short 37.
57
57
 
58
- Uploaders + compressors are **optional** peer deps (`@aquienpz/asset-uploader-{web,expo}`,
58
+ Uploaders + compressors are **optional** peer deps (`@nitida/asset-uploader-{web,expo}`,
59
59
  `@nitida/asset-compressor-{web,native}`). Skip them if your app only resolves
60
60
  slots and reads assets — your bundle stays a few KB.
61
61
 
@@ -229,10 +229,13 @@ Peer deps: `@nitida/asset-client` (URL builders + types) and `react` (only if
229
229
  you use the `/react` subpath). Both compressors are optional and lazy — the
230
230
  SDK never imports them unless you call a compress path.
231
231
 
232
- > ⚠️ The multipart uploader packages (`@aquienpz/asset-uploader-web` /
233
- > `-expo`) are **not published on npm**. `aq.upload()` covers files in a
234
- > single PUT and is what every consumer uses today; see *Large uploads* below
235
- > for where the ceiling actually is.
232
+ > The multipart uploader packages (`@nitida/asset-uploader-web` / `-expo`) are
233
+ > on npm since 2026-08-23. You do not need them for ordinary uploads:
234
+ > `aq.upload()` covers a file in a single PUT and is what every consumer uses
235
+ > today — see *Large uploads* below for where that ceiling actually is. Reach
236
+ > for an uploader when you need **resume**: a 2 GB upload that must survive a
237
+ > reload (web, IndexedDB) or the app being backgrounded (Expo, iOS URLSession
238
+ > / Android WorkManager).
236
239
 
237
240
  ## Quick start
238
241
 
@@ -259,8 +262,11 @@ const heroes = await aq.slots.resolveMany([
259
262
  "storefront.home.tile-2",
260
263
  ]);
261
264
 
262
- // Lower-level operations.
265
+ // Lower-level operations. `byHash` takes the full 64-hex sha256 that
266
+ // `upload()` returns as `.sha256`, OR its 16-char prefix — the short form
267
+ // that appears inside every CDN URL. Both resolve to the same asset.
263
268
  const asset = await aq.assets.byHash("3c…<64 hex>…");
269
+ const same = await aq.assets.byHash("3c8f1a20b7d94e05"); // 16-char prefix ✓
264
270
  const { assets, nextCursor } = await aq.assets.list({ limit: 50 });
265
271
 
266
272
  // Uploads — hash-deduped; returns the canonical v2 URL immediately.
@@ -431,7 +437,7 @@ export function UploadHeroScreen() {
431
437
  }
432
438
  ```
433
439
 
434
- ⚠️ The Expo background-upload path depends on `@aquienpz/asset-uploader-expo`,
440
+ ⚠️ The Expo background-upload path depends on `@nitida/asset-uploader-expo`,
435
441
  which is **not on npm**. `aq.upload()` works on Expo today without it; the
436
442
  background/resumable variant is not something an external consumer can install
437
443
  yet.
@@ -894,6 +900,29 @@ as they specify the same params.
894
900
 
895
901
  Every tenant has an HMAC-SHA256 signing key — 32 random bytes, generated
896
902
  on tenant creation.
903
+
904
+ > **Where you get it: the response that created your project, once.**
905
+ > `POST /admin/projects` returns `signingKey` next to the three API keys, and
906
+ > the console shows it in the same panel. Save it with the keys — nothing else
907
+ > hands it out. In particular `GET /admin/projects/:code` does **not** return
908
+ > it, and neither does any tenant-scoped endpoint.
909
+ >
910
+ > **Your project already exists and you never saw a signing key?** Then you
911
+ > never got one: projects created before 2026-08-23 were not handed it, and
912
+ > there is no endpoint that shows you the current one. This is the exact wall
913
+ > to hit, so here is the way through it:
914
+ >
915
+ > - **If you have not signed any URLs yet** — which is true of every project
916
+ > that has not shipped private assets — ask us to rotate. Rotation returns a
917
+ > key, and with nothing in flight it invalidates nothing. It is free.
918
+ > - **If you already have signed URLs in circulation**, rotation kills them.
919
+ > Ask us for the current key instead; we can read it.
920
+ >
921
+ > Either way it is one request to us, because
922
+ > `POST /admin/projects/:code/rotate-signing-key` needs the system-scope key
923
+ > the platform operator holds — your own admin key answers
924
+ > `403 SYSTEM_KEY_REQUIRED`.
925
+
897
926
  Optionally enable `strict_transforms = true` to reject unsigned URLs
898
927
  with a 401 — useful when transform URLs leak from a private surface
899
928
  (internal admin, b2b portal) and you don't want third parties
@@ -905,7 +934,8 @@ const aq = new NitidaClient({
905
934
  apiKey: process.env.AQUIENPZ_API_KEY!,
906
935
  tenantCode: "your-tenant",
907
936
  tenantId: 42,
908
- // Fetch via GET /admin/projects/your-tenant; do NOT ship to the browser.
937
+ // Handed to you once, by the response that created the project.
938
+ // do NOT ship to the browser.
909
939
  signingKey: process.env.AQUIENPZ_SIGNING_KEY!,
910
940
  });
911
941
 
@@ -1304,7 +1334,7 @@ API key tiers:
1304
1334
  | `aq.slots` | `bind(key, {assetId, preset})` | Admin rebind |
1305
1335
  | `aq.slots` | `unbind(key)` | Remove binding |
1306
1336
  | `aq.slots` | `invalidateCache(key?)` | After admin rebind |
1307
- | `aq.assets` | `byHash(sha)` / `byHashes([])` | Lookup |
1337
+ | `aq.assets` | `byHash(sha)` / `byHashes([])` | Lookup — full 64-hex sha **or** its 16-char prefix |
1308
1338
  | `aq.assets` | `list({limit, cursor})` | Paginated |
1309
1339
  | `aq.assets` | `get(id)` | Full DTO |
1310
1340
  | `aq.assets` | `patchMetadata(id, {…})` | Merge JSON |
@@ -1356,4 +1386,10 @@ or a cache hit runs no encoder and books nothing, so `0` means zero.
1356
1386
 
1357
1387
  ## License
1358
1388
 
1359
- UNLICENSEDinternal use only.
1389
+ MITsee [LICENSE](./LICENSE).
1390
+
1391
+ ⚠️ **The licence covers this client code, not the service.** MIT lets you use,
1392
+ modify and redistribute the SDK; it does not grant access to nitida. The
1393
+ service is governed by its own terms and plan tiers, and every call still needs
1394
+ credentials issued to your project. Same shape as every other API client you
1395
+ already depend on.
package/SECURITY.md ADDED
@@ -0,0 +1,56 @@
1
+ # Security
2
+
3
+ ## Reporting a vulnerability
4
+
5
+ Email **security@espaciofuturo.io**. Please do not open a public issue — a
6
+ public report is a disclosure, and it reaches attackers at the same moment it
7
+ reaches us.
8
+
9
+ Include what you have: the affected package and version, what you did, and what
10
+ happened. A URL that reproduces it is worth more than a paragraph describing
11
+ one. We will confirm receipt and tell you what we found.
12
+
13
+ ## What is in scope
14
+
15
+ The published client packages and the service they talk to:
16
+
17
+ - `@nitida/sdk`, `@nitida/asset-client`
18
+ - `@nitida/asset-compressor-web`, `@nitida/asset-compressor-native`
19
+ - `@nitida/asset-uploader-web`, `@nitida/asset-uploader-expo`
20
+ - the delivery edge (`8ok.uk`) and the API (`api.nitida.gofuture.space`)
21
+
22
+ ## Two behaviours that look like bugs and are not
23
+
24
+ Reported often enough to state up front, so a real report is not lost among
25
+ them:
26
+
27
+ **A 404 on a private asset is the feature.** An asset with
28
+ `visibility: "private"` answers 404 on every public URL — the raw path, every
29
+ stored variant, every `/t/` transform and every HLS segment. It is not a
30
+ missing file. Signed access lives at `/a/{tenant}/…?exp&sig`, minted on your
31
+ backend. See the SDK's `getPrivateAssetUrl`.
32
+
33
+ **A presigned upload URL is a bearer credential, on purpose.** The browser
34
+ uploader persists presigned R2 PUT URLs in IndexedDB, because that is what
35
+ resuming after a reload means. They are write-only, scoped to the parts of one
36
+ upload the holder started, expire in 60 minutes, and are deleted on complete
37
+ and on abort.
38
+
39
+ ## Credentials, and the one mistake that matters
40
+
41
+ An API key (`amk_rt_*`, `amk_ad_*`, `amk_ci_*`) is a **server-side** credential.
42
+ It must never reach a browser bundle, an `NEXT_PUBLIC_*` variable, or a mobile
43
+ app binary.
44
+
45
+ The same goes for a tenant's **URL signing key**: it mints signed URLs for
46
+ every private asset that tenant owns. Sign on your backend, or pre-sign at
47
+ build time.
48
+
49
+ The client packages are built so this is hard to get wrong — the web entrypoint
50
+ `@nitida/sdk/web` does not accept `apiKey` or `signingKey` at all; the types
51
+ reject them. If you find a path that leaks either one, that is exactly the
52
+ report we want.
53
+
54
+ If you believe a key has been exposed, email the address above and we will
55
+ rotate it. Rotating a signing key invalidates every URL already signed with it,
56
+ so tell us whether you have signed URLs in circulation.
package/dist/expo.d.ts CHANGED
@@ -1,12 +1,12 @@
1
- import { UploadTaskOptions, UploadTask } from '@aquienpz/asset-uploader-expo';
2
- export { UploadEvents, UploadFileInput, UploadSessionState, UploadTask, UploadTaskOptions, cancelResumableSession, listResumableSessions } from '@aquienpz/asset-uploader-expo';
1
+ import { UploadTaskOptions, UploadTask } from '@nitida/asset-uploader-expo';
2
+ export { UploadEvents, UploadFileInput, UploadSessionState, UploadTask, UploadTaskOptions, cancelResumableSession, listResumableSessions } from '@nitida/asset-uploader-expo';
3
3
  import { NitidaClient } from './index.js';
4
4
  import '@nitida/asset-client';
5
5
 
6
6
  /**
7
7
  * @nitida/sdk/expo — native multipart uploads for React Native / Expo.
8
8
  *
9
- * Wraps `@aquienpz/asset-uploader-expo`'s `UploadTask`, which delegates
9
+ * Wraps `@nitida/asset-uploader-expo`'s `UploadTask`, which delegates
10
10
  * the actual byte transfer to a native background session (URLSession
11
11
  * on iOS, WorkManager on Android). The upload survives:
12
12
  * - JS thread freezing
@@ -58,7 +58,7 @@ import '@nitida/asset-client';
58
58
  * is not built. It is the same gap `/web` documents. Ask; there is no public
59
59
  * tracker.
60
60
  *
61
- * Peer dep: `@aquienpz/asset-uploader-expo` (lazy — apps that don't
61
+ * Peer dep: `@nitida/asset-uploader-expo` (lazy — apps that don't
62
62
  * use the mobile SDK skip the install).
63
63
  * @module @nitida/sdk/expo
64
64
  */
package/dist/expo.js CHANGED
@@ -1,12 +1,12 @@
1
1
  // src/expo/index.ts
2
2
  import {
3
3
  UploadTask
4
- } from "@aquienpz/asset-uploader-expo";
4
+ } from "@nitida/asset-uploader-expo";
5
5
  import {
6
6
  cancelResumableSession,
7
7
  listResumableSessions,
8
8
  UploadTask as UploadTask2
9
- } from "@aquienpz/asset-uploader-expo";
9
+ } from "@nitida/asset-uploader-expo";
10
10
  function createExpoUploader(client, options) {
11
11
  const access = client;
12
12
  return new UploadTask({
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 * ⚠️ 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"]}
1
+ {"version":3,"sources":["../src/expo/index.ts"],"sourcesContent":["/**\n * @nitida/sdk/expo — native multipart uploads for React Native / Expo.\n *\n * Wraps `@nitida/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: `@nitida/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 \"@nitida/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 \"@nitida/asset-uploader-expo\";\nexport {\n cancelResumableSession,\n listResumableSessions,\n UploadTask,\n} from \"@nitida/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
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, SignAccessOptions, SignedTransformOptions, SlotDTO, SlotResolution, TRANSFORM_WIDTHS, TransformEffect, TransformFit, TransformFormat, TransformGravity, TransformOptions, TransformWidth, VariantEntryPreset, VariantPreset, VisibilityHint, accessMessage, assertPublic, assertSha, bestTextContrast, computeVariantDimensions, configureSlotResolver, contrastRatio, deriveAccessKey, extractAssetSha, getAmbientGradient, getAssetDimensions, getAssetSrcSet, getAssetUrl, getCdnBase, getHlsLadder, getHlsStreamingUrl, getPaletteBlurBackground, getPaletteCssVars, getPrivateAssetUrl, getPrivateTransformUrl, getSignedTransformUrl, getTenantId, getTextColorForBackground, getTransformSrcSet, getTransformUrl, getVideoTransformUrl, hasPreset, hlsLadderAlignment, invalidateSlotCache, iteratePaletteSwatches, pickAmbientBackground, relativeLuminance, resolveSlot, resolveSlots, serializeTransform, setCdnBase, setTenantId, signAccessUrl, signTransformUrl } from '@nitida/asset-client';
2
+ export { AssetDTO, AssetPalette, AssetVariant, HlsRung, PRESET_EXT, PRESET_LONG, PRESET_MAX_DIM, PRESET_SHORT, PaletteSwatch, REQUESTABLE_PRESETS, RequestablePreset, ResolveSlotOptions, SignAccessOptions, SignedTransformOptions, SlotDTO, SlotResolution, TRANSFORM_WIDTHS, TransformEffect, TransformFit, TransformFormat, TransformGravity, TransformOptions, TransformWidth, VariantEntryPreset, VariantPreset, VisibilityHint, accessMessage, assertPublic, assertSha, bestTextContrast, computeVariantDimensions, configureSlotResolver, contrastRatio, deriveAccessKey, extractAssetSha, getAmbientGradient, getAssetDimensions, getAssetSrcSet, getAssetUrl, getCdnBase, getHlsLadder, getHlsStreamingUrl, getPaletteBlurBackground, getPaletteCssVars, getPrivateAssetUrl, getPrivateTransformUrl, getSignedTransformUrl, getTenantId, getTextColorForBackground, getTransformSrcSet, getTransformUrl, getVideoTransformUrl, hasPreset, hlsLadderAlignment, invalidateSlotCache, isRequestablePreset, iteratePaletteSwatches, pickAmbientBackground, relativeLuminance, resolveSlot, resolveSlots, serializeTransform, setCdnBase, setTenantId, signAccessUrl, signTransformUrl, toRequestablePresets } from '@nitida/asset-client';
3
3
 
4
4
  /**
5
5
  * Which audio uploads already play everywhere — the single source of truth for
@@ -40,7 +40,7 @@ declare function isUniversallyPlayableAudio(mime: string): boolean;
40
40
  * platform.
41
41
  *
42
42
  * One ergonomic facade over the underlying packages
43
- * (`@nitida/asset-client` URL builders + `@aquienpz/asset-uploader-web`
43
+ * (`@nitida/asset-client` URL builders + `@nitida/asset-uploader-web`
44
44
  * + the slot resolver). Auth is a bearer API key (`amk_rt_*`), issued
45
45
  * per tenant when the tenant is created; tenant scope comes from
46
46
  * the key's metadata (`X-Tenant-Code` is log-only).
@@ -119,12 +119,25 @@ type NitidaClientOptions = {
119
119
  * Tenant's HMAC signing key for transform URLs (Phase 3). Required
120
120
  * only when calling `aq.transform(asset, opts, { sign: true })`.
121
121
  *
122
- * 32 random bytes, generated server-side on tenant creation; fetch
123
- * via `POST /admin/projects/:code/rotate-signing-key`, which needs a
124
- * SYSTEM-scope credential the platform operator holds your own admin key
125
- * answers `403 SYSTEM_KEY_REQUIRED`. Ask for it. **Keep it
126
- * server-side only** do not ship in `NEXT_PUBLIC_*` env vars. Sign
127
- * URLs from a BFF route handler, or pre-sign at build time.
122
+ * 32 random bytes, generated server-side on tenant creation.
123
+ *
124
+ * **Where you get it: the response to your project's creation, once.**
125
+ * `POST /admin/projects` returns `signingKey` next to the three API keys,
126
+ * and the console shows it in the same panel. It is shown there and nowhere
127
+ * else save it with the keys.
128
+ *
129
+ * **Never saw one?** Projects created before 2026-08-23 were not handed it,
130
+ * and no endpoint shows you the current one. If you have not signed any URLs
131
+ * yet — true of every project that has not shipped private assets — ask for
132
+ * a rotation: with nothing in flight it invalidates nothing and is free. If
133
+ * you already have signed URLs circulating, ask for the current key instead.
134
+ *
135
+ * Either way it is one request to the platform operator:
136
+ * `POST /admin/projects/:code/rotate-signing-key` needs a SYSTEM-scope
137
+ * credential, and your own admin key answers `403 SYSTEM_KEY_REQUIRED`.
138
+ *
139
+ * **Keep it server-side only** — do not ship in `NEXT_PUBLIC_*` env vars.
140
+ * Sign URLs from a BFF route handler, or pre-sign at build time.
128
141
  */
129
142
  signingKey?: string;
130
143
  };
@@ -319,7 +332,12 @@ type ComposeMarketingResult = {
319
332
  declare class AssetsApi {
320
333
  private readonly opts;
321
334
  constructor(opts: NitidaClientOptions);
322
- /** Look up an asset by full sha256 (64 hex). Returns null on 404. */
335
+ /**
336
+ * Look up an asset by sha256. Accepts the full 64-hex digest that
337
+ * `upload()` returns as `sha256`, or the 16-char short prefix that appears
338
+ * in every CDN URL. Returns null on 404; throws with a message naming the
339
+ * expected shape if the string is neither form.
340
+ */
323
341
  byHash(sha256: string): Promise<AssetDTO | null>;
324
342
  /** Bulk lookup by sha256s. */
325
343
  byHashes(hashes: string[]): Promise<{
@@ -461,7 +479,31 @@ declare class AssetsApi {
461
479
  * Default timeout is 5 minutes; videos / HLS ladders may need a
462
480
  * higher cap (pass `10 * 60_000` for compositions, transcodes).
463
481
  */
464
- waitReady(assetId: string, timeoutMs?: number): Promise<AssetDTO>;
482
+ waitReady(assetId: string,
483
+ /**
484
+ * Milliseconds, OR `{ timeoutMs }`.
485
+ *
486
+ * ⭐ It accepts the object because that is what people write. Found
487
+ * 2026-08-23 while smoke-testing a brand-new tenant: I passed
488
+ * `{ timeoutMs: 90_000 }` — every other option-taking method in this SDK
489
+ * takes an object — and got
490
+ *
491
+ * Error: waitReady: no asset a379fbbb-…
492
+ *
493
+ * about an asset that existed, was `ready`, and whose variants were all
494
+ * serving 200. `Date.now() - start < {…}` compares against NaN, so the
495
+ * loop body never ran, `last` stayed null, and the message blamed the one
496
+ * thing that was fine.
497
+ *
498
+ * TypeScript catches the wrong shape. Running it through `bun` does not —
499
+ * the same gap that let `upload()`'s `sha256` reach a URL builder wanting
500
+ * `sha` and produce `.../undefined.webp`. The lesson there was the same as
501
+ * here: a guard with a good message is still a mistake the user has to
502
+ * make first. Make the obvious call correct instead.
503
+ */
504
+ timeout?: number | {
505
+ timeoutMs?: number;
506
+ }): Promise<AssetDTO>;
465
507
  /**
466
508
  * Dispatch `POST /assets/compose-marketing` to stitch pre-uploaded clip
467
509
  * segments into a single MP4 composition. Returns the processing asset
package/dist/index.js CHANGED
@@ -53,12 +53,14 @@ import {
53
53
  hasPreset as hasPreset2,
54
54
  hlsLadderAlignment,
55
55
  invalidateSlotCache as invalidateSlotCache2,
56
+ isRequestablePreset as isRequestablePreset2,
56
57
  iteratePaletteSwatches,
57
58
  PRESET_EXT,
58
59
  PRESET_LONG,
59
60
  PRESET_MAX_DIM,
60
61
  PRESET_SHORT,
61
62
  pickAmbientBackground,
63
+ REQUESTABLE_PRESETS as REQUESTABLE_PRESETS2,
62
64
  relativeLuminance,
63
65
  resolveSlot as resolveSlot2,
64
66
  resolveSlots as resolveSlots2,
@@ -67,7 +69,8 @@ import {
67
69
  setTenantId as setTenantId2,
68
70
  signAccessUrl,
69
71
  signTransformUrl,
70
- TRANSFORM_WIDTHS
72
+ TRANSFORM_WIDTHS,
73
+ toRequestablePresets as toRequestablePresets2
71
74
  } from "@nitida/asset-client";
72
75
  function endpointUrl(opts, path, searchParams) {
73
76
  const endpoint = opts.endpoint.replace(/\/+$/, "");
@@ -199,7 +202,12 @@ var AssetsApi = class {
199
202
  this.opts = opts;
200
203
  }
201
204
  opts;
202
- /** Look up an asset by full sha256 (64 hex). Returns null on 404. */
205
+ /**
206
+ * Look up an asset by sha256. Accepts the full 64-hex digest that
207
+ * `upload()` returns as `sha256`, or the 16-char short prefix that appears
208
+ * in every CDN URL. Returns null on 404; throws with a message naming the
209
+ * expected shape if the string is neither form.
210
+ */
203
211
  async byHash(sha256) {
204
212
  const r = await fetch(
205
213
  endpointHref(this.opts, `/assets/by-hash/${sha256}`),
@@ -414,7 +422,13 @@ var AssetsApi = class {
414
422
  * Default timeout is 5 minutes; videos / HLS ladders may need a
415
423
  * higher cap (pass `10 * 60_000` for compositions, transcodes).
416
424
  */
417
- async waitReady(assetId, timeoutMs = 5 * 6e4) {
425
+ async waitReady(assetId, timeout = 5 * 6e4) {
426
+ const timeoutMs = typeof timeout === "number" ? timeout : timeout !== null && typeof timeout === "object" ? timeout.timeoutMs ?? 5 * 6e4 : Number.NaN;
427
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
428
+ throw new Error(
429
+ `waitReady: timeout must be a positive number of milliseconds or { timeoutMs }, got ${JSON.stringify(timeout)}. A non-numeric timeout makes the poll loop exit before its first iteration, which used to surface as "no asset <id>" about an asset that was fine.`
430
+ );
431
+ }
418
432
  const start = Date.now();
419
433
  let delay = 500;
420
434
  let last = null;
@@ -424,7 +438,11 @@ var AssetsApi = class {
424
438
  await new Promise((r) => setTimeout(r, delay));
425
439
  delay = Math.min(delay * 1.5, 5e3);
426
440
  }
427
- if (!last) throw new Error(`waitReady: no asset ${assetId}`);
441
+ if (!last) {
442
+ throw new Error(
443
+ `waitReady: polled ${assetId} zero times in ${timeoutMs}ms \u2014 this is a bug in the SDK, not a missing asset.`
444
+ );
445
+ }
428
446
  throw new Error(`waitReady timeout for ${assetId}`);
429
447
  }
430
448
  /**
@@ -572,7 +590,7 @@ var NitidaClient = class {
572
590
  }
573
591
  if (!this.opts.signingKey) {
574
592
  throw new Error(
575
- "aq.transform({ sign: true }) requires `signingKey` in NitidaClientOptions. No signingKey on this client. The key is minted by POST /admin/projects/:code/rotate-signing-key, which needs a system-scope credential the platform operator holds \u2014 your own admin key gets 403 SYSTEM_KEY_REQUIRED, so ask for it. Then pass it to the SDK constructor on a SERVER-side instance only."
593
+ "aq.transform({ sign: true }) requires `signingKey` in NitidaClientOptions. No signingKey on this client. It is returned ONCE, in the response that creates your project (POST /admin/projects \u2192 `signingKey`, next to the three API keys; the console shows it in the same panel). If you never saw one \u2014 projects created before 2026-08-23 were not handed it \u2014 ask the platform operator to rotate: with no signed URLs in flight that invalidates nothing and is free. If you DO have signed URLs circulating, ask for the current key instead, because rotating would kill them. Then pass it to the SDK constructor on a SERVER-side instance only."
576
594
  );
577
595
  }
578
596
  return getSignedTransformUrl(asset, opts, this.opts.signingKey) ?? Promise.resolve(this.urlFor(asset, "lg"));
@@ -581,7 +599,7 @@ var NitidaClient = class {
581
599
  if (!signOpts?.sign) return getTransformSrcSet(asset, widths, extraOpts);
582
600
  if (!this.opts.signingKey) {
583
601
  throw new Error(
584
- "aq.transformSrcSet({ sign: true }) requires `signingKey` in NitidaClientOptions."
602
+ "aq.transformSrcSet({ sign: true }) requires `signingKey` in NitidaClientOptions. It was returned ONCE, by the response that created your project (POST /admin/projects \u2192 `signingKey`). See the `signingKey` docs on NitidaClientOptions."
585
603
  );
586
604
  }
587
605
  const key = this.opts.signingKey;
@@ -860,6 +878,7 @@ export {
860
878
  PRESET_LONG,
861
879
  PRESET_MAX_DIM,
862
880
  PRESET_SHORT,
881
+ REQUESTABLE_PRESETS2 as REQUESTABLE_PRESETS,
863
882
  TRANSFORM_WIDTHS,
864
883
  accessMessage,
865
884
  assertPublic,
@@ -890,6 +909,7 @@ export {
890
909
  hasPreset2 as hasPreset,
891
910
  hlsLadderAlignment,
892
911
  invalidateSlotCache2 as invalidateSlotCache,
912
+ isRequestablePreset2 as isRequestablePreset,
893
913
  isUniversallyPlayableAudio,
894
914
  iteratePaletteSwatches,
895
915
  mimeFromFileName,
@@ -901,6 +921,7 @@ export {
901
921
  setCdnBase2 as setCdnBase,
902
922
  setTenantId2 as setTenantId,
903
923
  signAccessUrl,
904
- signTransformUrl
924
+ signTransformUrl,
925
+ toRequestablePresets2 as toRequestablePresets
905
926
  };
906
927
  //# sourceMappingURL=index.js.map