@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.
@@ -13,8 +13,9 @@ description: How to consume the nitida media platform (@nitida/sdk + @nitida/ass
13
13
  > (2026-06-28) and 0.14.2 (2026-07-18). They receive nothing. If you are pinned to either, you are
14
14
  > on a line that predates the `variants`/`oext` fixes, the HLS ladder registration, and a
15
15
  > `hasPreset` that does not report an `original` that is not there. Move to `@nitida/*`.
16
- > `tenant-config`, `asset-uploader-web` and `asset-uploader-expo` are internal and not on npm at
17
- > all.
16
+ > `@nitida/asset-uploader-web` and `@nitida/asset-uploader-expo` ARE on npm since 2026-08-23
17
+ > they were workspace-only until another repo started consuming them as `workspace:*` copies, which
18
+ > is public in effect without being auditable. `@aquienpz/tenant-config` is still internal.
18
19
 
19
20
  > Durable source of truth: **the published docs at <https://nitida.gofuture.space>** — most of it
20
21
  > generated from the types, so the API reference cannot rot. Agents can download it: the site
@@ -506,6 +507,22 @@ await getPrivateTransformUrl(asset, { width: 1280 }, signingKey, { expiresInSeco
506
507
  - **`exp` is mandatory**; revocation is *"within a minute"* (60 s TTL at the edge).
507
508
  - **The signing key is a backend secret** — it mints URLs for every private
508
509
  asset the tenant owns.
510
+ - **⭐ Where the signing key comes from: the response that CREATED your
511
+ project, once.** `POST /admin/projects` returns `signingKey` next to the
512
+ three API keys, and the console shows it in the same panel. Nothing else
513
+ hands it out — `GET /admin/projects/:code` does **not** include it. If it is
514
+ lost, the only endpoint that returns a key is
515
+ `POST /admin/projects/:code/rotate-signing-key`, which **invalidates every
516
+ URL already signed** and needs the system-scope key the platform operator
517
+ holds. A tenant with nothing signed yet can rotate for free; a live one
518
+ cannot, which is why you save it at creation.
519
+ - **⭐ Already have a project and never saw a signing key?** Then you never got
520
+ one — projects created before 2026-08-23 were not handed it, and no endpoint
521
+ shows you the current one. **If you have not signed any URLs yet, ask us to
522
+ rotate: with nothing in flight, rotation invalidates nothing and is free.**
523
+ If you already have signed URLs circulating, ask us for the current key
524
+ instead. This is the wall the T5 agent hit, and the sentence that was
525
+ missing.
509
526
 
510
527
  ## Two ways an image gets smaller, and only one of them is yours to call
511
528
 
package/src/expo/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @nitida/sdk/expo — native multipart uploads for React Native / Expo.
3
3
  *
4
- * Wraps `@aquienpz/asset-uploader-expo`'s `UploadTask`, which delegates
4
+ * Wraps `@nitida/asset-uploader-expo`'s `UploadTask`, which delegates
5
5
  * the actual byte transfer to a native background session (URLSession
6
6
  * on iOS, WorkManager on Android). The upload survives:
7
7
  * - JS thread freezing
@@ -53,7 +53,7 @@
53
53
  * is not built. It is the same gap `/web` documents. Ask; there is no public
54
54
  * tracker.
55
55
  *
56
- * Peer dep: `@aquienpz/asset-uploader-expo` (lazy — apps that don't
56
+ * Peer dep: `@nitida/asset-uploader-expo` (lazy — apps that don't
57
57
  * use the mobile SDK skip the install).
58
58
  * @module @nitida/sdk/expo
59
59
  */
@@ -61,7 +61,7 @@
61
61
  import {
62
62
  UploadTask,
63
63
  type UploadTaskOptions,
64
- } from "@aquienpz/asset-uploader-expo";
64
+ } from "@nitida/asset-uploader-expo";
65
65
  import type { NitidaClient } from "..";
66
66
 
67
67
  export type ExpoUploadOptions = Omit<
@@ -97,9 +97,9 @@ export type {
97
97
  UploadFileInput,
98
98
  UploadSessionState,
99
99
  UploadTaskOptions,
100
- } from "@aquienpz/asset-uploader-expo";
100
+ } from "@nitida/asset-uploader-expo";
101
101
  export {
102
102
  cancelResumableSession,
103
103
  listResumableSessions,
104
104
  UploadTask,
105
- } from "@aquienpz/asset-uploader-expo";
105
+ } from "@nitida/asset-uploader-expo";
package/src/index.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * platform.
4
4
  *
5
5
  * One ergonomic facade over the underlying packages
6
- * (`@nitida/asset-client` URL builders + `@aquienpz/asset-uploader-web`
6
+ * (`@nitida/asset-client` URL builders + `@nitida/asset-uploader-web`
7
7
  * + the slot resolver). Auth is a bearer API key (`amk_rt_*`), issued
8
8
  * per tenant when the tenant is created; tenant scope comes from
9
9
  * the key's metadata (`X-Tenant-Code` is log-only).
@@ -48,6 +48,8 @@ import {
48
48
  getVideoTransformUrl,
49
49
  hasPreset,
50
50
  invalidateSlotCache,
51
+ isRequestablePreset,
52
+ REQUESTABLE_PRESETS,
51
53
  type RequestablePreset,
52
54
  type ResolveSlotOptions,
53
55
  resolveSlot,
@@ -58,6 +60,7 @@ import {
58
60
  setCdnBase,
59
61
  setTenantId,
60
62
  type TransformOptions,
63
+ toRequestablePresets,
61
64
  type VariantEntryPreset,
62
65
  type VariantPreset,
63
66
  } from "@nitida/asset-client";
@@ -114,12 +117,25 @@ export type NitidaClientOptions = {
114
117
  * Tenant's HMAC signing key for transform URLs (Phase 3). Required
115
118
  * only when calling `aq.transform(asset, opts, { sign: true })`.
116
119
  *
117
- * 32 random bytes, generated server-side on tenant creation; fetch
118
- * via `POST /admin/projects/:code/rotate-signing-key`, which needs a
119
- * SYSTEM-scope credential the platform operator holds your own admin key
120
- * answers `403 SYSTEM_KEY_REQUIRED`. Ask for it. **Keep it
121
- * server-side only** do not ship in `NEXT_PUBLIC_*` env vars. Sign
122
- * URLs from a BFF route handler, or pre-sign at build time.
120
+ * 32 random bytes, generated server-side on tenant creation.
121
+ *
122
+ * **Where you get it: the response to your project's creation, once.**
123
+ * `POST /admin/projects` returns `signingKey` next to the three API keys,
124
+ * and the console shows it in the same panel. It is shown there and nowhere
125
+ * else save it with the keys.
126
+ *
127
+ * **Never saw one?** Projects created before 2026-08-23 were not handed it,
128
+ * and no endpoint shows you the current one. If you have not signed any URLs
129
+ * yet — true of every project that has not shipped private assets — ask for
130
+ * a rotation: with nothing in flight it invalidates nothing and is free. If
131
+ * you already have signed URLs circulating, ask for the current key instead.
132
+ *
133
+ * Either way it is one request to the platform operator:
134
+ * `POST /admin/projects/:code/rotate-signing-key` needs a SYSTEM-scope
135
+ * credential, and your own admin key answers `403 SYSTEM_KEY_REQUIRED`.
136
+ *
137
+ * **Keep it server-side only** — do not ship in `NEXT_PUBLIC_*` env vars.
138
+ * Sign URLs from a BFF route handler, or pre-sign at build time.
123
139
  */
124
140
  signingKey?: string;
125
141
  };
@@ -254,12 +270,14 @@ export {
254
270
  hasPreset,
255
271
  hlsLadderAlignment,
256
272
  invalidateSlotCache,
273
+ isRequestablePreset,
257
274
  iteratePaletteSwatches,
258
275
  PRESET_EXT,
259
276
  PRESET_LONG,
260
277
  PRESET_MAX_DIM,
261
278
  PRESET_SHORT,
262
279
  pickAmbientBackground,
280
+ REQUESTABLE_PRESETS,
263
281
  relativeLuminance,
264
282
  resolveSlot,
265
283
  resolveSlots,
@@ -269,6 +287,7 @@ export {
269
287
  signAccessUrl,
270
288
  signTransformUrl,
271
289
  TRANSFORM_WIDTHS,
290
+ toRequestablePresets,
272
291
  } from "@nitida/asset-client";
273
292
 
274
293
  // ---------------------------------------------------------------------------
@@ -531,7 +550,12 @@ export type ComposeMarketingResult = {
531
550
  class AssetsApi {
532
551
  constructor(private readonly opts: NitidaClientOptions) {}
533
552
 
534
- /** Look up an asset by full sha256 (64 hex). Returns null on 404. */
553
+ /**
554
+ * Look up an asset by sha256. Accepts the full 64-hex digest that
555
+ * `upload()` returns as `sha256`, or the 16-char short prefix that appears
556
+ * in every CDN URL. Returns null on 404; throws with a message naming the
557
+ * expected shape if the string is neither form.
558
+ */
535
559
  async byHash(sha256: string): Promise<AssetDTO | null> {
536
560
  const r = await fetch(
537
561
  endpointHref(this.opts, `/assets/by-hash/${sha256}`),
@@ -796,7 +820,49 @@ class AssetsApi {
796
820
  * Default timeout is 5 minutes; videos / HLS ladders may need a
797
821
  * higher cap (pass `10 * 60_000` for compositions, transcodes).
798
822
  */
799
- async waitReady(assetId: string, timeoutMs = 5 * 60_000): Promise<AssetDTO> {
823
+ async waitReady(
824
+ assetId: string,
825
+ /**
826
+ * Milliseconds, OR `{ timeoutMs }`.
827
+ *
828
+ * ⭐ It accepts the object because that is what people write. Found
829
+ * 2026-08-23 while smoke-testing a brand-new tenant: I passed
830
+ * `{ timeoutMs: 90_000 }` — every other option-taking method in this SDK
831
+ * takes an object — and got
832
+ *
833
+ * Error: waitReady: no asset a379fbbb-…
834
+ *
835
+ * about an asset that existed, was `ready`, and whose variants were all
836
+ * serving 200. `Date.now() - start < {…}` compares against NaN, so the
837
+ * loop body never ran, `last` stayed null, and the message blamed the one
838
+ * thing that was fine.
839
+ *
840
+ * TypeScript catches the wrong shape. Running it through `bun` does not —
841
+ * the same gap that let `upload()`'s `sha256` reach a URL builder wanting
842
+ * `sha` and produce `.../undefined.webp`. The lesson there was the same as
843
+ * here: a guard with a good message is still a mistake the user has to
844
+ * make first. Make the obvious call correct instead.
845
+ */
846
+ timeout: number | { timeoutMs?: number } = 5 * 60_000,
847
+ ): Promise<AssetDTO> {
848
+ // A string, a Date, anything else: refuse it. Falling back to the default
849
+ // would be the same silent-success this whole method just stopped doing —
850
+ // `waitReady(id, "90s")` would wait five minutes and the caller would
851
+ // never learn why.
852
+ const timeoutMs =
853
+ typeof timeout === "number"
854
+ ? timeout
855
+ : timeout !== null && typeof timeout === "object"
856
+ ? (timeout.timeoutMs ?? 5 * 60_000)
857
+ : Number.NaN;
858
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
859
+ throw new Error(
860
+ "waitReady: timeout must be a positive number of milliseconds or " +
861
+ `{ timeoutMs }, got ${JSON.stringify(timeout)}. A non-numeric ` +
862
+ "timeout makes the poll loop exit before its first iteration, which " +
863
+ `used to surface as "no asset <id>" about an asset that was fine.`,
864
+ );
865
+ }
800
866
  const start = Date.now();
801
867
  let delay = 500;
802
868
  let last: AssetDTO | null = null;
@@ -806,7 +872,15 @@ class AssetsApi {
806
872
  await new Promise((r) => setTimeout(r, delay));
807
873
  delay = Math.min(delay * 1.5, 5_000);
808
874
  }
809
- if (!last) throw new Error(`waitReady: no asset ${assetId}`);
875
+ // Unreachable now that a non-numeric timeout is refused above: any
876
+ // positive timeout runs the loop at least once, so `last` is set. Kept as
877
+ // a belt, with a message that no longer accuses the asset of not existing.
878
+ if (!last) {
879
+ throw new Error(
880
+ `waitReady: polled ${assetId} zero times in ${timeoutMs}ms — this is a ` +
881
+ "bug in the SDK, not a missing asset.",
882
+ );
883
+ }
810
884
  throw new Error(`waitReady timeout for ${assetId}`);
811
885
  }
812
886
 
@@ -1296,7 +1370,7 @@ export class NitidaClient {
1296
1370
  if (!this.opts.signingKey) {
1297
1371
  throw new Error(
1298
1372
  "aq.transform({ sign: true }) requires `signingKey` in NitidaClientOptions. " +
1299
- "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 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.",
1373
+ "No signingKey on this client. It is returned ONCE, in the response that creates your project (POST /admin/projects → `signingKey`, next to the three API keys; the console shows it in the same panel). If you never saw one — projects created before 2026-08-23 were not handed it 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.",
1300
1374
  );
1301
1375
  }
1302
1376
  // Signed path — custom (off-ladder) widths allowed. Empty opts → no
@@ -1334,7 +1408,8 @@ export class NitidaClient {
1334
1408
  if (!signOpts?.sign) return getTransformSrcSet(asset, widths, extraOpts);
1335
1409
  if (!this.opts.signingKey) {
1336
1410
  throw new Error(
1337
- "aq.transformSrcSet({ sign: true }) requires `signingKey` in NitidaClientOptions.",
1411
+ "aq.transformSrcSet({ sign: true }) requires `signingKey` in NitidaClientOptions. " +
1412
+ "It was returned ONCE, by the response that created your project (POST /admin/projects → `signingKey`). See the `signingKey` docs on NitidaClientOptions.",
1338
1413
  );
1339
1414
  }
1340
1415
  const key = this.opts.signingKey;
@@ -1520,10 +1595,10 @@ export class NitidaClient {
1520
1595
  // was not enough: `bun build --compile` constant-folds simple
1521
1596
  // strings and still eagerly bundled `./web.js`, which
1522
1597
  // top-level-imports the browser-only peer deps
1523
- // `@aquienpz/asset-uploader-web` / `@nitida/asset-compressor-web`
1598
+ // `@nitida/asset-uploader-web` / `@nitida/asset-compressor-web`
1524
1599
  // — neither installed on server consumers — crashing the
1525
1600
  // single-binary on boot with `Cannot find module
1526
- // '@aquienpz/asset-uploader-web'`.
1601
+ // '@nitida/asset-uploader-web'`.
1527
1602
  //
1528
1603
  // The earlier `new URL("./web.js", import.meta.url)` + `await import(URL)`
1529
1604
  // dance survived bun-compile but Turbopack still tracks the URL literal
@@ -129,6 +129,7 @@ export {
129
129
  hasPreset,
130
130
  hlsLadderAlignment,
131
131
  invalidateSlotCache,
132
+ isRequestablePreset,
132
133
  isUniversallyPlayableAudio,
133
134
  iteratePaletteSwatches,
134
135
  mimeFromFileName,
@@ -140,6 +141,7 @@ export {
140
141
  PRESET_SHORT,
141
142
  type PresignUploadUrlOptions,
142
143
  pickAmbientBackground,
144
+ REQUESTABLE_PRESETS,
143
145
  type RegenerateResult,
144
146
  type RequestablePreset,
145
147
  type ResolveSlotOptions,
@@ -163,6 +165,7 @@ export {
163
165
  type TransformGravity,
164
166
  type TransformOptions,
165
167
  type TransformWidth,
168
+ toRequestablePresets,
166
169
  type UploadOptions,
167
170
  type UploadResult,
168
171
  type UploadUrlResult,
package/src/web/index.ts CHANGED
@@ -44,7 +44,7 @@
44
44
  * `apiKey` and bundles cleanly only in Node/Bun/edge runtimes.
45
45
  *
46
46
  * Peer deps (auto-installed via npm peer deps — declared optional):
47
- * - `@aquienpz/asset-uploader-web` for multipart uploads
47
+ * - `@nitida/asset-uploader-web` for multipart uploads
48
48
  * - `@nitida/asset-compressor-web` for client-side compression
49
49
  *
50
50
  * Both are lazy-imported; bundles that never call into them skip the cost.
@@ -146,6 +146,7 @@ export {
146
146
  hasPreset,
147
147
  hlsLadderAlignment,
148
148
  invalidateSlotCache,
149
+ isRequestablePreset,
149
150
  isUniversallyPlayableAudio,
150
151
  iteratePaletteSwatches,
151
152
  mimeFromFileName,
@@ -156,6 +157,7 @@ export {
156
157
  PRESET_SHORT,
157
158
  type PresignUploadUrlOptions,
158
159
  pickAmbientBackground,
160
+ REQUESTABLE_PRESETS,
159
161
  type RegenerateResult,
160
162
  type RequestablePreset,
161
163
  type ResolveSlotOptions,
@@ -179,6 +181,7 @@ export {
179
181
  type TransformGravity,
180
182
  type TransformOptions,
181
183
  type TransformWidth,
184
+ toRequestablePresets,
182
185
  type UploadOptions,
183
186
  type UploadResult,
184
187
  type UploadUrlResult,
@@ -194,7 +197,7 @@ export {
194
197
 
195
198
  // Multipart uploader helpers are NOT re-exported from this subpath.
196
199
  //
197
- // `@aquienpz/asset-uploader-web` is an optional peer-dep that isn't on
200
+ // `@nitida/asset-uploader-web` is an optional peer-dep that isn't on
198
201
  // npm yet, so a static `import` at module top broke every consumer of
199
202
  // `@nitida/sdk/web` (the bundler tries to resolve before the optional
200
203
  // peer check kicks in). The old `createWebUploader` + `UploadTask`
@@ -206,7 +209,7 @@ export {
206
209
  // token is not exposed yet. Ask us; there is no public tracker.
207
210
  //
208
211
  // Apps that need `UploadTask` directly today should depend on
209
- // `@aquienpz/asset-uploader-web` themselves once it's published.
212
+ // `@nitida/asset-uploader-web` themselves once it's published.
210
213
 
211
214
  // ---------------------------------------------------------------------------
212
215
  // Client-side compression (Phase 1.5)