@org-quicko/silo-client 1.0.1 → 1.1.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/README.md CHANGED
@@ -208,6 +208,7 @@ value you store. Storing the URL instead is what a later rename breaks.
208
208
  await poster.rename("arrival-2016.jpg")
209
209
  await poster.moveTo("posters/2016")
210
210
  await poster.setTags(["poster"]) // replaces the whole list
211
+ await poster.replace(file) // new bytes, same id, name and URL
211
212
  await poster.delete() // refused while an entry refers to it
212
213
  await poster.delete({ force: true })
213
214
 
@@ -217,6 +218,12 @@ usage.total // the true count
217
218
  usage.visible // how many of them this key may see
218
219
  ```
219
220
 
221
+ `replace` swaps the file behind an asset. The id, the reference, the name and
222
+ the URL all stay, so every entry that refers to it shows the new file and none
223
+ of them is rewritten. The new file must keep the same extension. It needs the
224
+ `media:replace` claim, and `entries:update` on each scope that refers to the
225
+ asset.
226
+
220
227
  Folders, and a bulk delete that takes up to 100 ids:
221
228
 
222
229
  ```ts
@@ -8,4 +8,9 @@ export declare class NetworkError extends Error {
8
8
  readonly method: string;
9
9
  readonly path: string;
10
10
  constructor(method: string, path: string, cause: unknown);
11
+ /** What `fetch` rejected with, in the message itself: a `cause` chain is
12
+ * printed by some consoles and by no log line, and "never reached the
13
+ * server" alone reads as a verdict on the network when the fault can be the
14
+ * call. */
15
+ private static reason;
11
16
  }
@@ -8,4 +8,9 @@ export declare class NetworkError extends Error {
8
8
  readonly method: string;
9
9
  readonly path: string;
10
10
  constructor(method: string, path: string, cause: unknown);
11
+ /** What `fetch` rejected with, in the message itself: a `cause` chain is
12
+ * printed by some consoles and by no log line, and "never reached the
13
+ * server" alone reads as a verdict on the network when the fault can be the
14
+ * call. */
15
+ private static reason;
11
16
  }
package/dist/index.cjs CHANGED
@@ -181,6 +181,9 @@ class ApiPath {
181
181
  static mediaAssetUsages(id) {
182
182
  return `${ApiPath.mediaAsset(id)}/usages`;
183
183
  }
184
+ static mediaAssetContent(id) {
185
+ return `${ApiPath.mediaAsset(id)}/content`;
186
+ }
184
187
  static mediaBulkDelete() {
185
188
  return "/api/media/delete";
186
189
  }
@@ -227,6 +230,22 @@ class MediaAssetMapper {
227
230
  }
228
231
  }
229
232
 
233
+ // src/media/media-file.ts
234
+ class MediaFile {
235
+ static toBlob(bytes, contentType) {
236
+ if (bytes instanceof Blob)
237
+ return bytes;
238
+ return new Blob([bytes], contentType ? { type: contentType } : undefined);
239
+ }
240
+ static nameOf(input, explicit) {
241
+ if (explicit)
242
+ return explicit;
243
+ if (input instanceof File && input.name)
244
+ return input.name;
245
+ throw new Error("media: a Blob has no filename — pass { filename } explicitly");
246
+ }
247
+ }
248
+
230
249
  // src/media/media-reference.ts
231
250
  class MediaReference {
232
251
  static Scheme = "silo://media/";
@@ -388,6 +407,22 @@ class MediaAsset {
388
407
  async setTags(tags, options) {
389
408
  return this.patch({ tags: [...tags] }, options);
390
409
  }
410
+ async replace(input, options) {
411
+ const form = new FormData;
412
+ if (input instanceof Blob) {
413
+ form.set("file", input, MediaFile.nameOf(input, options?.filename));
414
+ } else {
415
+ form.set("file", MediaFile.toBlob(input.bytes, input.contentType), input.filename);
416
+ }
417
+ const payload = await this.transport.upload({
418
+ method: "POST",
419
+ path: ApiPath.mediaAssetContent(this.id),
420
+ signal: options?.signal,
421
+ timeoutMilliseconds: options?.timeoutMilliseconds
422
+ }, form);
423
+ this.record = MediaAssetMapper.toRecord(payload);
424
+ return this;
425
+ }
391
426
  async delete(options) {
392
427
  await this.transport.empty({
393
428
  method: "DELETE",
@@ -621,11 +656,11 @@ class Media {
621
656
  let options;
622
657
  if (input instanceof Blob) {
623
658
  const fileOptions = second ?? {};
624
- form.set("file", input, Media.filenameOf(input, fileOptions.filename));
659
+ form.set("file", input, MediaFile.nameOf(input, fileOptions.filename));
625
660
  folder = fileOptions.folder;
626
661
  options = third ?? fileOptions;
627
662
  } else {
628
- form.set("file", Media.toBlob(input.bytes, input.contentType), input.filename);
663
+ form.set("file", MediaFile.toBlob(input.bytes, input.contentType), input.filename);
629
664
  folder = input.folder;
630
665
  options = second;
631
666
  }
@@ -693,18 +728,6 @@ class Media {
693
728
  sort: query.sort
694
729
  };
695
730
  }
696
- static toBlob(bytes, contentType) {
697
- if (bytes instanceof Blob)
698
- return bytes;
699
- return new Blob([bytes], contentType ? { type: contentType } : undefined);
700
- }
701
- static filenameOf(input, explicit) {
702
- if (explicit)
703
- return explicit;
704
- if (input instanceof File && input.name)
705
- return input.name;
706
- throw new Error("media upload: a Blob has no filename — pass { filename } explicitly");
707
- }
708
731
  }
709
732
 
710
733
  // src/variables/variable.ts
@@ -1568,12 +1591,19 @@ class NetworkError extends Error {
1568
1591
  method;
1569
1592
  path;
1570
1593
  constructor(method, path, cause) {
1571
- super(`network error on ${method} ${path}: the request never reached the server`, { cause });
1594
+ super(`network error on ${method} ${path}: the request never reached the server${NetworkError.reason(cause)}`, { cause });
1572
1595
  this.name = "NetworkError";
1573
1596
  this.method = method;
1574
1597
  this.path = path;
1575
1598
  Object.setPrototypeOf(this, NetworkError.prototype);
1576
1599
  }
1600
+ static reason(cause) {
1601
+ if (cause instanceof Error && cause.message)
1602
+ return ` (${cause.message})`;
1603
+ if (typeof cause === "string" && cause)
1604
+ return ` (${cause})`;
1605
+ return "";
1606
+ }
1577
1607
  }
1578
1608
 
1579
1609
  // src/errors/timeout-error.ts
@@ -1687,7 +1717,7 @@ class Transport {
1687
1717
  this.key = options.key;
1688
1718
  this.headers = options.headers ?? {};
1689
1719
  this.timeoutMilliseconds = options.timeoutMilliseconds;
1690
- this.fetchFunction = options.fetch ?? fetch;
1720
+ this.fetchFunction = Transport.resolveFetch(options.fetch);
1691
1721
  }
1692
1722
  async json(request) {
1693
1723
  const response = await this.execute(request);
@@ -1723,9 +1753,10 @@ class Transport {
1723
1753
  async execute(request, form) {
1724
1754
  const abortSignals = new AbortSignals(request.signal, request.timeoutMilliseconds ?? this.timeoutMilliseconds);
1725
1755
  const url = `${this.url}${request.path}${QueryString.build(request.query)}`;
1756
+ const sendRequest = this.fetchFunction;
1726
1757
  let response;
1727
1758
  try {
1728
- response = await this.fetchFunction(url, {
1759
+ response = await sendRequest(url, {
1729
1760
  method: request.method,
1730
1761
  headers: this.buildHeaders(request, Boolean(form)),
1731
1762
  body: form ?? (request.body === undefined ? undefined : JSON.stringify(request.body)),
@@ -1763,6 +1794,14 @@ class Transport {
1763
1794
  static normalizeUrl(url) {
1764
1795
  return url.replace(/\/+$/, "");
1765
1796
  }
1797
+ static resolveFetch(fetchFunction) {
1798
+ if (fetchFunction)
1799
+ return fetchFunction;
1800
+ if (typeof globalThis.fetch !== "function") {
1801
+ throw new Error("this runtime has no global fetch: pass one as SiloOptions.fetch");
1802
+ }
1803
+ return globalThis.fetch.bind(globalThis);
1804
+ }
1766
1805
  }
1767
1806
 
1768
1807
  // src/silo.ts
@@ -1847,6 +1886,7 @@ class RouteInventory {
1847
1886
  "PATCH /api/media/:id",
1848
1887
  "DELETE /api/media/:id",
1849
1888
  "GET /api/media/:id/usages",
1889
+ "POST /api/media/:id/content",
1850
1890
  "POST /api/media/delete",
1851
1891
  "GET /api/media/folders",
1852
1892
  "POST /api/media/folders",
@@ -1857,6 +1897,7 @@ class RouteInventory {
1857
1897
  "GET /api/session": "key introspection, an operator surface",
1858
1898
  "GET /api/keys": "keys and claims are an operator surface",
1859
1899
  "POST /api/keys": "keys and claims are an operator surface",
1900
+ "PATCH /api/keys/:id": "keys and claims are an operator surface",
1860
1901
  "DELETE /api/keys/:id": "keys and claims are an operator surface",
1861
1902
  "GET /api/audit": "an operator surface",
1862
1903
  "GET /api/observability": "an operator surface",
package/dist/index.d.cts CHANGED
@@ -71,6 +71,7 @@ export type { MediaAssetRecord } from "./media/media-asset.cjs";
71
71
  export type { MediaQuery } from "./media/media-query.cjs";
72
72
  export type { MediaUsage } from "./media/media-usage.cjs";
73
73
  export type { MediaUsageQuery } from "./media/media-usage-page.cjs";
74
+ export type { MediaReplace, MediaReplaceBytes, MediaReplaceOptions } from "./media/media-replace.cjs";
74
75
  export type { MediaUpload, MediaUploadBytes, MediaUploadFileOptions } from "./media/media-upload.cjs";
75
76
  export type { MediaDeleteFailure } from "./media/media-delete-failure.cjs";
76
77
  export type { MediaDeleteOptions } from "./media/media-delete-options.cjs";
package/dist/index.d.ts CHANGED
@@ -71,6 +71,7 @@ export type { MediaAssetRecord } from "./media/media-asset.js";
71
71
  export type { MediaQuery } from "./media/media-query.js";
72
72
  export type { MediaUsage } from "./media/media-usage.js";
73
73
  export type { MediaUsageQuery } from "./media/media-usage-page.js";
74
+ export type { MediaReplace, MediaReplaceBytes, MediaReplaceOptions } from "./media/media-replace.js";
74
75
  export type { MediaUpload, MediaUploadBytes, MediaUploadFileOptions } from "./media/media-upload.js";
75
76
  export type { MediaDeleteFailure } from "./media/media-delete-failure.js";
76
77
  export type { MediaDeleteOptions } from "./media/media-delete-options.js";
package/dist/index.js CHANGED
@@ -84,6 +84,9 @@ class ApiPath {
84
84
  static mediaAssetUsages(id) {
85
85
  return `${ApiPath.mediaAsset(id)}/usages`;
86
86
  }
87
+ static mediaAssetContent(id) {
88
+ return `${ApiPath.mediaAsset(id)}/content`;
89
+ }
87
90
  static mediaBulkDelete() {
88
91
  return "/api/media/delete";
89
92
  }
@@ -130,6 +133,22 @@ class MediaAssetMapper {
130
133
  }
131
134
  }
132
135
 
136
+ // src/media/media-file.ts
137
+ class MediaFile {
138
+ static toBlob(bytes, contentType) {
139
+ if (bytes instanceof Blob)
140
+ return bytes;
141
+ return new Blob([bytes], contentType ? { type: contentType } : undefined);
142
+ }
143
+ static nameOf(input, explicit) {
144
+ if (explicit)
145
+ return explicit;
146
+ if (input instanceof File && input.name)
147
+ return input.name;
148
+ throw new Error("media: a Blob has no filename — pass { filename } explicitly");
149
+ }
150
+ }
151
+
133
152
  // src/media/media-reference.ts
134
153
  class MediaReference {
135
154
  static Scheme = "silo://media/";
@@ -291,6 +310,22 @@ class MediaAsset {
291
310
  async setTags(tags, options) {
292
311
  return this.patch({ tags: [...tags] }, options);
293
312
  }
313
+ async replace(input, options) {
314
+ const form = new FormData;
315
+ if (input instanceof Blob) {
316
+ form.set("file", input, MediaFile.nameOf(input, options?.filename));
317
+ } else {
318
+ form.set("file", MediaFile.toBlob(input.bytes, input.contentType), input.filename);
319
+ }
320
+ const payload = await this.transport.upload({
321
+ method: "POST",
322
+ path: ApiPath.mediaAssetContent(this.id),
323
+ signal: options?.signal,
324
+ timeoutMilliseconds: options?.timeoutMilliseconds
325
+ }, form);
326
+ this.record = MediaAssetMapper.toRecord(payload);
327
+ return this;
328
+ }
294
329
  async delete(options) {
295
330
  await this.transport.empty({
296
331
  method: "DELETE",
@@ -524,11 +559,11 @@ class Media {
524
559
  let options;
525
560
  if (input instanceof Blob) {
526
561
  const fileOptions = second ?? {};
527
- form.set("file", input, Media.filenameOf(input, fileOptions.filename));
562
+ form.set("file", input, MediaFile.nameOf(input, fileOptions.filename));
528
563
  folder = fileOptions.folder;
529
564
  options = third ?? fileOptions;
530
565
  } else {
531
- form.set("file", Media.toBlob(input.bytes, input.contentType), input.filename);
566
+ form.set("file", MediaFile.toBlob(input.bytes, input.contentType), input.filename);
532
567
  folder = input.folder;
533
568
  options = second;
534
569
  }
@@ -596,18 +631,6 @@ class Media {
596
631
  sort: query.sort
597
632
  };
598
633
  }
599
- static toBlob(bytes, contentType) {
600
- if (bytes instanceof Blob)
601
- return bytes;
602
- return new Blob([bytes], contentType ? { type: contentType } : undefined);
603
- }
604
- static filenameOf(input, explicit) {
605
- if (explicit)
606
- return explicit;
607
- if (input instanceof File && input.name)
608
- return input.name;
609
- throw new Error("media upload: a Blob has no filename — pass { filename } explicitly");
610
- }
611
634
  }
612
635
 
613
636
  // src/variables/variable.ts
@@ -1471,12 +1494,19 @@ class NetworkError extends Error {
1471
1494
  method;
1472
1495
  path;
1473
1496
  constructor(method, path, cause) {
1474
- super(`network error on ${method} ${path}: the request never reached the server`, { cause });
1497
+ super(`network error on ${method} ${path}: the request never reached the server${NetworkError.reason(cause)}`, { cause });
1475
1498
  this.name = "NetworkError";
1476
1499
  this.method = method;
1477
1500
  this.path = path;
1478
1501
  Object.setPrototypeOf(this, NetworkError.prototype);
1479
1502
  }
1503
+ static reason(cause) {
1504
+ if (cause instanceof Error && cause.message)
1505
+ return ` (${cause.message})`;
1506
+ if (typeof cause === "string" && cause)
1507
+ return ` (${cause})`;
1508
+ return "";
1509
+ }
1480
1510
  }
1481
1511
 
1482
1512
  // src/errors/timeout-error.ts
@@ -1590,7 +1620,7 @@ class Transport {
1590
1620
  this.key = options.key;
1591
1621
  this.headers = options.headers ?? {};
1592
1622
  this.timeoutMilliseconds = options.timeoutMilliseconds;
1593
- this.fetchFunction = options.fetch ?? fetch;
1623
+ this.fetchFunction = Transport.resolveFetch(options.fetch);
1594
1624
  }
1595
1625
  async json(request) {
1596
1626
  const response = await this.execute(request);
@@ -1626,9 +1656,10 @@ class Transport {
1626
1656
  async execute(request, form) {
1627
1657
  const abortSignals = new AbortSignals(request.signal, request.timeoutMilliseconds ?? this.timeoutMilliseconds);
1628
1658
  const url = `${this.url}${request.path}${QueryString.build(request.query)}`;
1659
+ const sendRequest = this.fetchFunction;
1629
1660
  let response;
1630
1661
  try {
1631
- response = await this.fetchFunction(url, {
1662
+ response = await sendRequest(url, {
1632
1663
  method: request.method,
1633
1664
  headers: this.buildHeaders(request, Boolean(form)),
1634
1665
  body: form ?? (request.body === undefined ? undefined : JSON.stringify(request.body)),
@@ -1666,6 +1697,14 @@ class Transport {
1666
1697
  static normalizeUrl(url) {
1667
1698
  return url.replace(/\/+$/, "");
1668
1699
  }
1700
+ static resolveFetch(fetchFunction) {
1701
+ if (fetchFunction)
1702
+ return fetchFunction;
1703
+ if (typeof globalThis.fetch !== "function") {
1704
+ throw new Error("this runtime has no global fetch: pass one as SiloOptions.fetch");
1705
+ }
1706
+ return globalThis.fetch.bind(globalThis);
1707
+ }
1669
1708
  }
1670
1709
 
1671
1710
  // src/silo.ts
@@ -1750,6 +1789,7 @@ class RouteInventory {
1750
1789
  "PATCH /api/media/:id",
1751
1790
  "DELETE /api/media/:id",
1752
1791
  "GET /api/media/:id/usages",
1792
+ "POST /api/media/:id/content",
1753
1793
  "POST /api/media/delete",
1754
1794
  "GET /api/media/folders",
1755
1795
  "POST /api/media/folders",
@@ -1760,6 +1800,7 @@ class RouteInventory {
1760
1800
  "GET /api/session": "key introspection, an operator surface",
1761
1801
  "GET /api/keys": "keys and claims are an operator surface",
1762
1802
  "POST /api/keys": "keys and claims are an operator surface",
1803
+ "PATCH /api/keys/:id": "keys and claims are an operator surface",
1763
1804
  "DELETE /api/keys/:id": "keys and claims are an operator surface",
1764
1805
  "GET /api/audit": "an operator surface",
1765
1806
  "GET /api/observability": "an operator surface",
@@ -1,6 +1,7 @@
1
1
  import type { RequestOptions } from "../request-options.cjs";
2
2
  import type { Transport } from "../transport/transport.cjs";
3
3
  import type { MediaDeleteOptions } from "./media-delete-options.cjs";
4
+ import type { MediaReplace, MediaReplaceOptions } from "./media-replace.cjs";
4
5
  import type { MediaUsageQuery } from "./media-usage-page.cjs";
5
6
  import { MediaUsagePage } from "./media-usage-page.cjs";
6
7
  /** `MediaAsset`'s mapped state — `MediaAssetMapper.toRecord`'s output. */
@@ -53,6 +54,22 @@ export declare class MediaAsset {
53
54
  moveTo(folder: string, options?: RequestOptions): Promise<this>;
54
55
  /** REPLACES the tag list — `PATCH` replaces, it does not append. */
55
56
  setTags(tags: readonly string[], options?: RequestOptions): Promise<this>;
57
+ /**
58
+ * Swaps the bytes behind this asset (D67). `id`, `reference`, `url`,
59
+ * `filename` and `folder` all survive; `hash`, `sizeInBytes` and
60
+ * `contentType` are re-read from the server's answer.
61
+ *
62
+ * Every entry referencing it now resolves to the new file, without any of
63
+ * them being rewritten — which is the point, and the reason the server asks
64
+ * for `media:replace` **and** `entries:update` at every scope that refers
65
+ * to it.
66
+ *
67
+ * The new file must keep the current extension: the blob key's suffix is
68
+ * derived from the filename at upload and is the visible tail of the URL on
69
+ * a bucket-backed instance, so a `.png` asset takes a `.png` replacement.
70
+ * Converting a file is a new upload, not a replacement of this one.
71
+ */
72
+ replace(input: MediaReplace, options?: MediaReplaceOptions): Promise<this>;
56
73
  /** Refused while an entry references it; `{ force: true }` also needs
57
74
  * `entries:update` on every scope this asset reaches. */
58
75
  delete(options?: MediaDeleteOptions): Promise<void>;
@@ -1,6 +1,7 @@
1
1
  import type { RequestOptions } from "../request-options.js";
2
2
  import type { Transport } from "../transport/transport.js";
3
3
  import type { MediaDeleteOptions } from "./media-delete-options.js";
4
+ import type { MediaReplace, MediaReplaceOptions } from "./media-replace.js";
4
5
  import type { MediaUsageQuery } from "./media-usage-page.js";
5
6
  import { MediaUsagePage } from "./media-usage-page.js";
6
7
  /** `MediaAsset`'s mapped state — `MediaAssetMapper.toRecord`'s output. */
@@ -53,6 +54,22 @@ export declare class MediaAsset {
53
54
  moveTo(folder: string, options?: RequestOptions): Promise<this>;
54
55
  /** REPLACES the tag list — `PATCH` replaces, it does not append. */
55
56
  setTags(tags: readonly string[], options?: RequestOptions): Promise<this>;
57
+ /**
58
+ * Swaps the bytes behind this asset (D67). `id`, `reference`, `url`,
59
+ * `filename` and `folder` all survive; `hash`, `sizeInBytes` and
60
+ * `contentType` are re-read from the server's answer.
61
+ *
62
+ * Every entry referencing it now resolves to the new file, without any of
63
+ * them being rewritten — which is the point, and the reason the server asks
64
+ * for `media:replace` **and** `entries:update` at every scope that refers
65
+ * to it.
66
+ *
67
+ * The new file must keep the current extension: the blob key's suffix is
68
+ * derived from the filename at upload and is the visible tail of the URL on
69
+ * a bucket-backed instance, so a `.png` asset takes a `.png` replacement.
70
+ * Converting a file is a new upload, not a replacement of this one.
71
+ */
72
+ replace(input: MediaReplace, options?: MediaReplaceOptions): Promise<this>;
56
73
  /** Refused while an entry references it; `{ force: true }` also needs
57
74
  * `entries:update` on every scope this asset reaches. */
58
75
  delete(options?: MediaDeleteOptions): Promise<void>;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Turning what a caller passed into the two things a multipart part needs: a
3
+ * `Blob` and a filename.
4
+ *
5
+ * Shared by `Media.upload` and `MediaAsset.replace` (D67) rather than copied
6
+ * into the second. The filename is not cosmetic on either path — the server
7
+ * reads the extension off it to decide what the library accepts, and on a
8
+ * replace to decide whether the file's type is changing at all — so the two
9
+ * entry points must derive it the same way.
10
+ */
11
+ export declare class MediaFile {
12
+ static toBlob(bytes: Uint8Array | ArrayBuffer | Blob, contentType?: string): Blob;
13
+ /** A `File` carries its own name; a bare `Blob` does not, and sending it
14
+ * as "blob" is worse than refusing outright. */
15
+ static nameOf(input: Blob, explicit?: string): string;
16
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Turning what a caller passed into the two things a multipart part needs: a
3
+ * `Blob` and a filename.
4
+ *
5
+ * Shared by `Media.upload` and `MediaAsset.replace` (D67) rather than copied
6
+ * into the second. The filename is not cosmetic on either path — the server
7
+ * reads the extension off it to decide what the library accepts, and on a
8
+ * replace to decide whether the file's type is changing at all — so the two
9
+ * entry points must derive it the same way.
10
+ */
11
+ export declare class MediaFile {
12
+ static toBlob(bytes: Uint8Array | ArrayBuffer | Blob, contentType?: string): Blob;
13
+ /** A `File` carries its own name; a bare `Blob` does not, and sending it
14
+ * as "blob" is worse than refusing outright. */
15
+ static nameOf(input: Blob, explicit?: string): string;
16
+ }
@@ -0,0 +1,17 @@
1
+ import type { RequestOptions } from "../request-options.cjs";
2
+ /** The fully-described replacement: raw bytes plus the name whose extension
3
+ * the server checks against the one the asset already has. No `folder` — a
4
+ * replace changes bytes and nothing else about where the asset sits. */
5
+ export interface MediaReplaceBytes {
6
+ bytes: Uint8Array | ArrayBuffer | Blob;
7
+ filename: string;
8
+ contentType?: string;
9
+ }
10
+ /** The filename to read when the input carries none — a `File` has `.name`,
11
+ * a bare `Blob` does not, and the extension is what the server checks. */
12
+ export interface MediaReplaceOptions extends RequestOptions {
13
+ filename?: string;
14
+ }
15
+ /** `MediaAsset.replace()`'s input: a fully-described replacement, or a
16
+ * runtime `File`/`Blob`. */
17
+ export type MediaReplace = MediaReplaceBytes | File | Blob;
@@ -0,0 +1,17 @@
1
+ import type { RequestOptions } from "../request-options.js";
2
+ /** The fully-described replacement: raw bytes plus the name whose extension
3
+ * the server checks against the one the asset already has. No `folder` — a
4
+ * replace changes bytes and nothing else about where the asset sits. */
5
+ export interface MediaReplaceBytes {
6
+ bytes: Uint8Array | ArrayBuffer | Blob;
7
+ filename: string;
8
+ contentType?: string;
9
+ }
10
+ /** The filename to read when the input carries none — a `File` has `.name`,
11
+ * a bare `Blob` does not, and the extension is what the server checks. */
12
+ export interface MediaReplaceOptions extends RequestOptions {
13
+ filename?: string;
14
+ }
15
+ /** `MediaAsset.replace()`'s input: a fully-described replacement, or a
16
+ * runtime `File`/`Blob`. */
17
+ export type MediaReplace = MediaReplaceBytes | File | Blob;
@@ -30,8 +30,4 @@ export declare class Media {
30
30
  * per-id outcomes live in the report, not in a thrown error. */
31
31
  deleteMany(ids: readonly string[], options?: MediaDeleteOptions): Promise<MediaDeleteReport>;
32
32
  private static toWireQuery;
33
- private static toBlob;
34
- /** A `File` carries its own name; a bare `Blob` does not, and uploading it
35
- * as "blob" is worse than refusing outright. */
36
- private static filenameOf;
37
33
  }
@@ -30,8 +30,4 @@ export declare class Media {
30
30
  * per-id outcomes live in the report, not in a thrown error. */
31
31
  deleteMany(ids: readonly string[], options?: MediaDeleteOptions): Promise<MediaDeleteReport>;
32
32
  private static toWireQuery;
33
- private static toBlob;
34
- /** A `File` carries its own name; a bare `Blob` does not, and uploading it
35
- * as "blob" is worse than refusing outright. */
36
- private static filenameOf;
37
33
  }
@@ -31,6 +31,7 @@ export declare class ApiPath {
31
31
  static mediaExtensions(): string;
32
32
  static mediaAsset(id: string): string;
33
33
  static mediaAssetUsages(id: string): string;
34
+ static mediaAssetContent(id: string): string;
34
35
  static mediaBulkDelete(): string;
35
36
  static mediaFolders(): string;
36
37
  }
@@ -31,6 +31,7 @@ export declare class ApiPath {
31
31
  static mediaExtensions(): string;
32
32
  static mediaAsset(id: string): string;
33
33
  static mediaAssetUsages(id: string): string;
34
+ static mediaAssetContent(id: string): string;
34
35
  static mediaBulkDelete(): string;
35
36
  static mediaFolders(): string;
36
37
  }
@@ -42,4 +42,8 @@ export declare class Transport {
42
42
  private transportFailure;
43
43
  private buildHeaders;
44
44
  private static normalizeUrl;
45
+ /** The caller's own `fetch`, or the runtime's bound to the global. A
46
+ * browser's `fetch` is a `Window` method and refuses every other receiver,
47
+ * so the default is stored already bound rather than bare. */
48
+ private static resolveFetch;
45
49
  }
@@ -42,4 +42,8 @@ export declare class Transport {
42
42
  private transportFailure;
43
43
  private buildHeaders;
44
44
  private static normalizeUrl;
45
+ /** The caller's own `fetch`, or the runtime's bound to the global. A
46
+ * browser's `fetch` is a `Window` method and refuses every other receiver,
47
+ * so the default is stored already bound rather than bare. */
48
+ private static resolveFetch;
45
49
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@org-quicko/silo-client",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "A typed, object-oriented client for silo's HTTP API",
5
5
  "keywords": [
6
6
  "silo",