@nitida/sdk 0.28.0 → 0.30.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
@@ -167,4 +167,4 @@ degrades to a wrong choice.
167
167
  |---|---|
168
168
  | `@nitida/asset-client` | URL builders + preset tables. No network, no key. |
169
169
  | `@nitida/asset-compressor-web` | Browser image compression (worker → OffscreenCanvas → WebP). |
170
- | `@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
 
@@ -434,7 +437,7 @@ export function UploadHeroScreen() {
434
437
  }
435
438
  ```
436
439
 
437
- ⚠️ The Expo background-upload path depends on `@aquienpz/asset-uploader-expo`,
440
+ ⚠️ The Expo background-upload path depends on `@nitida/asset-uploader-expo`,
438
441
  which is **not on npm**. `aq.upload()` works on Expo today without it; the
439
442
  background/resumable variant is not something an external consumer can install
440
443
  yet.
@@ -1383,4 +1386,10 @@ or a cache hit runs no encoder and books nothing, so `0` means zero.
1383
1386
 
1384
1387
  ## License
1385
1388
 
1386
- 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
@@ -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).
@@ -479,7 +479,31 @@ declare class AssetsApi {
479
479
  * Default timeout is 5 minutes; videos / HLS ladders may need a
480
480
  * higher cap (pass `10 * 60_000` for compositions, transcodes).
481
481
  */
482
- 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>;
483
507
  /**
484
508
  * Dispatch `POST /assets/compose-marketing` to stitch pre-uploaded clip
485
509
  * segments into a single MP4 composition. Returns the processing asset
@@ -621,6 +645,32 @@ type UploadResult = {
621
645
  * message about the wrong one.
622
646
  */
623
647
  sha: string;
648
+ /**
649
+ * The MIME the upload was stored under.
650
+ *
651
+ * ⭐ It exists because it did not, and that cost the THIRD 404 of this exact
652
+ * family. Found 2026-08-23 by an agent running the getting-started doc
653
+ * verbatim: `getAssetUrl(up, "original")` built `<sha>-o.bin` and 404'd,
654
+ * while the object served fine at `-o.jpg`.
655
+ *
656
+ * `original` is the one preset whose extension is not fixed — it is the
657
+ * bytes you uploaded, so the extension comes from the MIME (`ORIGINAL_EXT_BY_MIME`),
658
+ * and `PRESET_EXT.original` is only the `"bin"` fallback for when nothing
659
+ * says otherwise. `UploadResult` said nothing, so every caller handing an
660
+ * upload result straight to a URL builder got the fallback.
661
+ *
662
+ * The pattern is now three for three — `sha256` vs `sha`, and this:
663
+ * **a result type that omits what the next call needs turns the obvious
664
+ * call into a silent 404.** The fix is never a better error message.
665
+ */
666
+ mime: string;
667
+ /**
668
+ * The extension the server actually stored the original under, when it said
669
+ * so. Authoritative — it beats any client-side MIME table, because the
670
+ * server keys the object off the uploaded filename for the cases no table
671
+ * can close (`.mpga`, `.docx`, `.m4a` all arrive as octet-stream).
672
+ */
673
+ oext?: string | null;
624
674
  cdnUrl: string;
625
675
  };
626
676
  type SlotHistoryEntry = {
package/dist/index.js CHANGED
@@ -422,7 +422,13 @@ var AssetsApi = class {
422
422
  * Default timeout is 5 minutes; videos / HLS ladders may need a
423
423
  * higher cap (pass `10 * 60_000` for compositions, transcodes).
424
424
  */
425
- 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
+ }
426
432
  const start = Date.now();
427
433
  let delay = 500;
428
434
  let last = null;
@@ -432,7 +438,11 @@ var AssetsApi = class {
432
438
  await new Promise((r) => setTimeout(r, delay));
433
439
  delay = Math.min(delay * 1.5, 5e3);
434
440
  }
435
- 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
+ }
436
446
  throw new Error(`waitReady timeout for ${assetId}`);
437
447
  }
438
448
  /**
@@ -762,6 +772,8 @@ var NitidaClient = class {
762
772
  assetId: existing.id,
763
773
  sha256: sha,
764
774
  sha: sha.slice(0, 16),
775
+ mime: existing.mime ?? mime,
776
+ oext: existing.oext ?? null,
765
777
  cdnUrl: this.urlFor(existing, this.bestPresetForAsset(existing, mime))
766
778
  };
767
779
  }
@@ -779,6 +791,8 @@ var NitidaClient = class {
779
791
  assetId: presign.asset.id,
780
792
  sha256: sha,
781
793
  sha: sha.slice(0, 16),
794
+ mime: presign.asset.mime ?? mime,
795
+ oext: presign.asset.oext ?? null,
782
796
  cdnUrl: this.urlFor(presign.asset, this.defaultPresetForMime(mime))
783
797
  };
784
798
  }
@@ -822,6 +836,8 @@ var NitidaClient = class {
822
836
  assetId,
823
837
  sha256: sha,
824
838
  sha: sha.slice(0, 16),
839
+ mime: final.mime ?? mime,
840
+ oext: final.oext ?? null,
825
841
  cdnUrl: this.urlFor(final, this.bestPresetForAsset(final, mime))
826
842
  };
827
843
  }