@nitida/sdk 0.36.1 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitida/sdk",
3
- "version": "0.36.1",
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": "^6.0.3",
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:*",
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
- configureSlotResolver({
1438
- endpoint: opts.endpoint,
1439
- apiKey: opts.apiKey,
1440
- tenantCode: opts.tenantCode,
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
  }