@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 +17 -1
- package/LICENSE +21 -0
- package/README.md +46 -10
- package/SECURITY.md +56 -0
- package/dist/expo.d.ts +4 -4
- package/dist/expo.js +2 -2
- package/dist/expo.js.map +1 -1
- package/dist/index.d.ts +52 -10
- package/dist/index.js +28 -7
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +28 -7
- package/dist/server.js.map +1 -1
- package/dist/web.d.ts +2 -2
- package/dist/web.js +28 -7
- package/dist/web.js.map +1 -1
- package/package.json +7 -5
- package/skills/nitida-sdk/SKILL.md +19 -2
- package/src/expo/index.ts +5 -5
- package/src/index.ts +89 -14
- package/src/server/index.ts +3 -0
- package/src/web/index.ts +6 -3
|
@@ -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
|
-
>
|
|
17
|
-
>
|
|
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 `@
|
|
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: `@
|
|
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 "@
|
|
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 "@
|
|
100
|
+
} from "@nitida/asset-uploader-expo";
|
|
101
101
|
export {
|
|
102
102
|
cancelResumableSession,
|
|
103
103
|
listResumableSessions,
|
|
104
104
|
UploadTask,
|
|
105
|
-
} from "@
|
|
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 + `@
|
|
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
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
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
|
-
/**
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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
|
-
// `@
|
|
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
|
-
// '@
|
|
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
|
package/src/server/index.ts
CHANGED
|
@@ -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
|
-
* - `@
|
|
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
|
-
// `@
|
|
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
|
-
// `@
|
|
212
|
+
// `@nitida/asset-uploader-web` themselves once it's published.
|
|
210
213
|
|
|
211
214
|
// ---------------------------------------------------------------------------
|
|
212
215
|
// Client-side compression (Phase 1.5)
|