@nitida/sdk 0.27.0 → 0.28.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/src/index.ts CHANGED
@@ -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
  };
@@ -227,6 +243,7 @@ export type {
227
243
  export {
228
244
  accessMessage,
229
245
  assertPublic,
246
+ assertSha,
230
247
  bestTextContrast,
231
248
  computeVariantDimensions,
232
249
  configureSlotResolver,
@@ -253,12 +270,14 @@ export {
253
270
  hasPreset,
254
271
  hlsLadderAlignment,
255
272
  invalidateSlotCache,
273
+ isRequestablePreset,
256
274
  iteratePaletteSwatches,
257
275
  PRESET_EXT,
258
276
  PRESET_LONG,
259
277
  PRESET_MAX_DIM,
260
278
  PRESET_SHORT,
261
279
  pickAmbientBackground,
280
+ REQUESTABLE_PRESETS,
262
281
  relativeLuminance,
263
282
  resolveSlot,
264
283
  resolveSlots,
@@ -268,6 +287,7 @@ export {
268
287
  signAccessUrl,
269
288
  signTransformUrl,
270
289
  TRANSFORM_WIDTHS,
290
+ toRequestablePresets,
271
291
  } from "@nitida/asset-client";
272
292
 
273
293
  // ---------------------------------------------------------------------------
@@ -530,7 +550,12 @@ export type ComposeMarketingResult = {
530
550
  class AssetsApi {
531
551
  constructor(private readonly opts: NitidaClientOptions) {}
532
552
 
533
- /** 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
+ */
534
559
  async byHash(sha256: string): Promise<AssetDTO | null> {
535
560
  const r = await fetch(
536
561
  endpointHref(this.opts, `/assets/by-hash/${sha256}`),
@@ -1018,6 +1043,22 @@ const DEFAULT_UPLOAD_PRESETS: RequestablePreset[] = ["original"];
1018
1043
  export type UploadResult = {
1019
1044
  assetId: string;
1020
1045
  sha256: string;
1046
+ /**
1047
+ * The SAME 16-hex prefix an `AssetDTO` carries, so the result of an upload can
1048
+ * be handed straight to any URL builder.
1049
+ *
1050
+ * ⭐ It exists because it did not, and that cost a real 404. A Haiku agent
1051
+ * evaluating the SDK on 2026-08-23 did the most natural thing there is —
1052
+ * `transform(await upload(file), { width: 1280 })` — and got
1053
+ * `https://8ok.uk/t/width=1280/undefined.webp`. The builders read `sha`; this
1054
+ * type only had `sha256`. TypeScript caught it; running through `bun`, or in
1055
+ * plain JS, nothing did.
1056
+ *
1057
+ * The guard (`assertSha`) is the backstop. This field is the actual fix: the
1058
+ * obvious call is now the correct one, which is worth more than a good error
1059
+ * message about the wrong one.
1060
+ */
1061
+ sha: string;
1021
1062
  cdnUrl: string;
1022
1063
  };
1023
1064
 
@@ -1279,7 +1320,7 @@ export class NitidaClient {
1279
1320
  if (!this.opts.signingKey) {
1280
1321
  throw new Error(
1281
1322
  "aq.transform({ sign: true }) requires `signingKey` in NitidaClientOptions. " +
1282
- "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.",
1323
+ "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.",
1283
1324
  );
1284
1325
  }
1285
1326
  // Signed path — custom (off-ladder) widths allowed. Empty opts → no
@@ -1317,7 +1358,8 @@ export class NitidaClient {
1317
1358
  if (!signOpts?.sign) return getTransformSrcSet(asset, widths, extraOpts);
1318
1359
  if (!this.opts.signingKey) {
1319
1360
  throw new Error(
1320
- "aq.transformSrcSet({ sign: true }) requires `signingKey` in NitidaClientOptions.",
1361
+ "aq.transformSrcSet({ sign: true }) requires `signingKey` in NitidaClientOptions. " +
1362
+ "It was returned ONCE, by the response that created your project (POST /admin/projects → `signingKey`). See the `signingKey` docs on NitidaClientOptions.",
1321
1363
  );
1322
1364
  }
1323
1365
  const key = this.opts.signingKey;
@@ -1563,6 +1605,7 @@ export class NitidaClient {
1563
1605
  return {
1564
1606
  assetId: existing.id,
1565
1607
  sha256: sha,
1608
+ sha: sha.slice(0, 16),
1566
1609
  cdnUrl: this.urlFor(existing, this.bestPresetForAsset(existing, mime)),
1567
1610
  };
1568
1611
  }
@@ -1588,6 +1631,7 @@ export class NitidaClient {
1588
1631
  return {
1589
1632
  assetId: presign.asset.id,
1590
1633
  sha256: sha,
1634
+ sha: sha.slice(0, 16),
1591
1635
  cdnUrl: this.urlFor(presign.asset, this.defaultPresetForMime(mime)),
1592
1636
  };
1593
1637
  }
@@ -1638,6 +1682,7 @@ export class NitidaClient {
1638
1682
  return {
1639
1683
  assetId,
1640
1684
  sha256: sha,
1685
+ sha: sha.slice(0, 16),
1641
1686
  cdnUrl: this.urlFor(final, this.bestPresetForAsset(final, mime)),
1642
1687
  };
1643
1688
  }
@@ -96,6 +96,7 @@ export {
96
96
  type AssetVariant,
97
97
  accessMessage,
98
98
  assertPublic,
99
+ assertSha,
99
100
  bestTextContrast,
100
101
  type ComposeMarketingComposition,
101
102
  type ComposeMarketingOptions,
@@ -128,6 +129,7 @@ export {
128
129
  hasPreset,
129
130
  hlsLadderAlignment,
130
131
  invalidateSlotCache,
132
+ isRequestablePreset,
131
133
  isUniversallyPlayableAudio,
132
134
  iteratePaletteSwatches,
133
135
  mimeFromFileName,
@@ -139,6 +141,7 @@ export {
139
141
  PRESET_SHORT,
140
142
  type PresignUploadUrlOptions,
141
143
  pickAmbientBackground,
144
+ REQUESTABLE_PRESETS,
142
145
  type RegenerateResult,
143
146
  type RequestablePreset,
144
147
  type ResolveSlotOptions,
@@ -162,6 +165,7 @@ export {
162
165
  type TransformGravity,
163
166
  type TransformOptions,
164
167
  type TransformWidth,
168
+ toRequestablePresets,
165
169
  type UploadOptions,
166
170
  type UploadResult,
167
171
  type UploadUrlResult,
package/src/web/index.ts CHANGED
@@ -113,6 +113,7 @@ export {
113
113
  type AssetVariant,
114
114
  accessMessage,
115
115
  assertPublic,
116
+ assertSha,
116
117
  bestTextContrast,
117
118
  type ComposeMarketingComposition,
118
119
  type ComposeMarketingOptions,
@@ -145,6 +146,7 @@ export {
145
146
  hasPreset,
146
147
  hlsLadderAlignment,
147
148
  invalidateSlotCache,
149
+ isRequestablePreset,
148
150
  isUniversallyPlayableAudio,
149
151
  iteratePaletteSwatches,
150
152
  mimeFromFileName,
@@ -155,6 +157,7 @@ export {
155
157
  PRESET_SHORT,
156
158
  type PresignUploadUrlOptions,
157
159
  pickAmbientBackground,
160
+ REQUESTABLE_PRESETS,
158
161
  type RegenerateResult,
159
162
  type RequestablePreset,
160
163
  type ResolveSlotOptions,
@@ -178,6 +181,7 @@ export {
178
181
  type TransformGravity,
179
182
  type TransformOptions,
180
183
  type TransformWidth,
184
+ toRequestablePresets,
181
185
  type UploadOptions,
182
186
  type UploadResult,
183
187
  type UploadUrlResult,