@nitida/sdk 0.36.0 → 0.36.2
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/README.md +19 -9
- package/dist/index.d.ts +3 -0
- package/dist/index.js +16 -13
- package/dist/index.js.map +1 -1
- package/dist/server.js +16 -13
- package/dist/server.js.map +1 -1
- package/dist/web.js +23 -13
- package/dist/web.js.map +1 -1
- package/package.json +2 -2
- package/skills/nitida-sdk/SKILL.md +18 -5
- package/src/index.ts +23 -12
- package/src/web/index.ts +10 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nitida/sdk",
|
|
3
|
-
"version": "0.36.
|
|
3
|
+
"version": "0.36.2",
|
|
4
4
|
"description": "nitida — the media SDK: browser and mobile upload with resume, client-side compression, on-the-fly transforms behind a CDN, video transcode, HLS ladders and AI proxies. Multi-tenant.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
"@types/bun": "^1.3.14",
|
|
94
94
|
"react": "^19.2.6",
|
|
95
95
|
"tsup": "^8.5.1",
|
|
96
|
-
"typescript": "
|
|
96
|
+
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
97
97
|
"@nitida/asset-client": "workspace:*",
|
|
98
98
|
"@nitida/asset-compressor-web": "workspace:*",
|
|
99
99
|
"@nitida/asset-compressor-native": "workspace:*",
|
|
@@ -190,11 +190,24 @@ everyone, and the ladder buys you nothing.
|
|
|
190
190
|
|
|
191
191
|
> Full measurements, with the method: <https://nitida.gofuture.space/guides/transform-benchmark/>
|
|
192
192
|
|
|
193
|
-
`/t/` short-
|
|
194
|
-
— and that variant is already a WebP
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
193
|
+
`/t/` can short-circuit to the smallest **stored variant** that covers the request
|
|
194
|
+
— and that variant is already a WebP that went through one lossy pass.
|
|
195
|
+
|
|
196
|
+
🔴 **Until 2026-08-31 that shortcut fired on `typeof quality === "number"`**, so
|
|
197
|
+
the parameter named "quality" was the one that chose WHICH BYTES to decode, and
|
|
198
|
+
pinning it *lowered* quality. **That is fixed at the server.** `quality` now only
|
|
199
|
+
sets the encoder; the cheap source has to be asked for by name, `source=nearest`
|
|
200
|
+
(default `master`), and a bogus value is a 400.
|
|
201
|
+
|
|
202
|
+
Verified against production the day it shipped, same asset, cold DSLs:
|
|
203
|
+
`quality=61` → `x-transform-source: lg` before · `quality=62` → `original` after ·
|
|
204
|
+
`quality=64,source=nearest` → `lg`, deliberately.
|
|
205
|
+
|
|
206
|
+
⇒ **You cannot trigger a second lossy pass by accident any more.** The numbers
|
|
207
|
+
below are the honest cost of one, i.e. what `source=nearest` buys today. And
|
|
208
|
+
`source` is deliberately NOT in `TransformOptions`: the only legitimate consumer
|
|
209
|
+
is a bulk pipeline that hand-builds URLs, and keeping it off the typed surface is
|
|
210
|
+
what stops an ordinary caller from landing there.
|
|
198
211
|
|
|
199
212
|
Measured at `width=800` on five photographs, same photo uploaded twice (full
|
|
200
213
|
ladder vs `["original"]` only):
|
package/src/index.ts
CHANGED
|
@@ -309,12 +309,22 @@ export {
|
|
|
309
309
|
class SlotsApi {
|
|
310
310
|
constructor(private readonly opts: NitidaClientOptions) {}
|
|
311
311
|
|
|
312
|
+
/** This client's own auth/endpoint scope, threaded into every resolve so it
|
|
313
|
+
* never uses another client's key (audit N15). */
|
|
314
|
+
private resolverConfig() {
|
|
315
|
+
return {
|
|
316
|
+
endpoint: this.opts.endpoint,
|
|
317
|
+
apiKey: this.opts.apiKey ?? null,
|
|
318
|
+
tenantCode: this.opts.tenantCode,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
312
322
|
/** Resolve one slot — returns `{slot, preset, url}` or `{slot: null, url: null}` when unbound. */
|
|
313
323
|
resolve(
|
|
314
324
|
slotKey: string,
|
|
315
325
|
options: ResolveSlotOptions = {},
|
|
316
326
|
): Promise<SlotResolution> {
|
|
317
|
-
return resolveSlot(slotKey, options);
|
|
327
|
+
return resolveSlot(slotKey, { config: this.resolverConfig(), ...options });
|
|
318
328
|
}
|
|
319
329
|
|
|
320
330
|
/** Bulk-resolve N slots in one HTTP round-trip. */
|
|
@@ -322,7 +332,7 @@ class SlotsApi {
|
|
|
322
332
|
slotKeys: string[],
|
|
323
333
|
options: ResolveSlotOptions = {},
|
|
324
334
|
): Promise<Record<string, SlotResolution>> {
|
|
325
|
-
return resolveSlots(slotKeys, options);
|
|
335
|
+
return resolveSlots(slotKeys, { config: this.resolverConfig(), ...options });
|
|
326
336
|
}
|
|
327
337
|
|
|
328
338
|
/** List slots for the tenant (admin). Optional prefix filter for tree views. */
|
|
@@ -570,7 +580,7 @@ class AssetsApi {
|
|
|
570
580
|
*/
|
|
571
581
|
async byHash(sha256: string): Promise<AssetDTO | null> {
|
|
572
582
|
const r = await fetch(
|
|
573
|
-
endpointHref(this.opts, `/assets/by-hash/${sha256}`),
|
|
583
|
+
endpointHref(this.opts, `/assets/by-hash/${encodeURIComponent(sha256)}`),
|
|
574
584
|
{ headers: this.headers() },
|
|
575
585
|
);
|
|
576
586
|
if (r.status === 404) return null;
|
|
@@ -614,7 +624,7 @@ class AssetsApi {
|
|
|
614
624
|
|
|
615
625
|
/** Full DTO for an asset (admin view — includes audit-only fields). */
|
|
616
626
|
async get(assetId: string): Promise<AssetDTO & Record<string, unknown>> {
|
|
617
|
-
const r = await fetch(endpointHref(this.opts, `/assets/${assetId}`), {
|
|
627
|
+
const r = await fetch(endpointHref(this.opts, `/assets/${encodeURIComponent(assetId)}`), {
|
|
618
628
|
headers: this.headers(),
|
|
619
629
|
});
|
|
620
630
|
if (!r.ok) throw new Error(`asset get ${r.status}: ${await r.text()}`);
|
|
@@ -635,7 +645,7 @@ class AssetsApi {
|
|
|
635
645
|
updatedBy: string | null;
|
|
636
646
|
}>
|
|
637
647
|
> {
|
|
638
|
-
const r = await fetch(endpointHref(this.opts, `/assets/${assetId}/slots`), {
|
|
648
|
+
const r = await fetch(endpointHref(this.opts, `/assets/${encodeURIComponent(assetId)}/slots`), {
|
|
639
649
|
headers: this.headers(),
|
|
640
650
|
});
|
|
641
651
|
if (!r.ok) throw new Error(`asset bindings ${r.status}: ${await r.text()}`);
|
|
@@ -713,7 +723,7 @@ class AssetsApi {
|
|
|
713
723
|
opts: { presets?: RequestablePreset[] } = {},
|
|
714
724
|
): Promise<RegenerateResult> {
|
|
715
725
|
const r = await fetch(
|
|
716
|
-
endpointHref(this.opts, `/assets/${assetId}/regenerate`),
|
|
726
|
+
endpointHref(this.opts, `/assets/${encodeURIComponent(assetId)}/regenerate`),
|
|
717
727
|
{
|
|
718
728
|
method: "POST",
|
|
719
729
|
headers: { ...this.headers(), "Content-Type": "application/json" },
|
|
@@ -730,7 +740,7 @@ class AssetsApi {
|
|
|
730
740
|
assetId: string,
|
|
731
741
|
metadata: Record<string, unknown>,
|
|
732
742
|
): Promise<{ ok: true; metadata: Record<string, unknown> }> {
|
|
733
|
-
const r = await fetch(endpointHref(this.opts, `/assets/${assetId}`), {
|
|
743
|
+
const r = await fetch(endpointHref(this.opts, `/assets/${encodeURIComponent(assetId)}`), {
|
|
734
744
|
method: "PATCH",
|
|
735
745
|
headers: { ...this.headers(), "Content-Type": "application/json" },
|
|
736
746
|
body: JSON.stringify({ metadata }),
|
|
@@ -1434,11 +1444,12 @@ export class NitidaClient {
|
|
|
1434
1444
|
// (`getAssetUrl`, `urlFor`, `srcSetFor`, upload `cdnUrl`) emits the
|
|
1435
1445
|
// tenant-prefixed path `<cdn>/<tid b36>/v/<sha>-<preset>.<ext>`.
|
|
1436
1446
|
setTenantId(opts.tenantId);
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1447
|
+
// NOTE: do NOT configure the process-global slot resolver here. A second
|
|
1448
|
+
// client for another tenant would overwrite the first client's apiKey /
|
|
1449
|
+
// tenantCode and make its slots.resolve() read cross-tenant (audit N15).
|
|
1450
|
+
// Slot scope is threaded per-client via SlotsApi.resolve → resolveSlot({config}).
|
|
1451
|
+
// `configureSlotResolver` stays exported for standalone resolveSlot() users
|
|
1452
|
+
// (a browser/edge that boots one resolver).
|
|
1442
1453
|
this.slots = new SlotsApi(opts);
|
|
1443
1454
|
this.assets = new AssetsApi(opts);
|
|
1444
1455
|
this.usage = new UsageApi(opts);
|
package/src/web/index.ts
CHANGED
|
@@ -84,6 +84,16 @@ export type WebClientOptions = Omit<
|
|
|
84
84
|
*/
|
|
85
85
|
export class NitidaClient extends BaseNitidaClient {
|
|
86
86
|
constructor(opts: WebClientOptions) {
|
|
87
|
+
// "Keys can't reach a browser" was a TYPESCRIPT-only property: WebClientOptions
|
|
88
|
+
// omits apiKey/signingKey, but a JS consumer (or `as any`) could still pass them
|
|
89
|
+
// and this bundle would use them (audit N14). Refuse at runtime too.
|
|
90
|
+
for (const k of ["apiKey", "signingKey"] as const) {
|
|
91
|
+
if (k in (opts as object)) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`@nitida/sdk/web: ${k} must never reach a browser bundle. Import @nitida/sdk/server on the server, and use a BFF route from the browser.`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
87
97
|
super(opts as NitidaClientOptions);
|
|
88
98
|
}
|
|
89
99
|
}
|