@howells/stow-server 2.2.1 → 2.3.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/dist/index.mjs CHANGED
@@ -1,6 +1,9 @@
1
1
  // src/index.ts
2
2
  import { createHash } from "crypto";
3
+ import { setTimeout as delay } from "timers/promises";
3
4
  import { z } from "zod";
5
+
6
+ // src/stow-error.ts
4
7
  var StowError = class extends Error {
5
8
  status;
6
9
  code;
@@ -11,6 +14,8 @@ var StowError = class extends Error {
11
14
  this.code = code;
12
15
  }
13
16
  };
17
+
18
+ // src/index.ts
14
19
  var fileColorSchema = z.object({
15
20
  position: z.number().int(),
16
21
  proportion: z.number(),
@@ -169,10 +174,7 @@ var presignDedupeResultSchema = z.object({
169
174
  size: z.number(),
170
175
  contentType: z.string()
171
176
  });
172
- var presignResultSchema = z.union([
173
- presignDedupeResultSchema,
174
- presignNewResultSchema
175
- ]);
177
+ var presignResultSchema = z.union([presignDedupeResultSchema, presignNewResultSchema]);
176
178
  var confirmResultSchema = z.object({
177
179
  key: z.string(),
178
180
  url: z.string().nullable(),
@@ -224,6 +226,42 @@ var profileResultSchema = z.object({
224
226
  createdAt: z.string(),
225
227
  updatedAt: z.string()
226
228
  });
229
+ var clusterGroupResultSchema = z.object({
230
+ id: z.string(),
231
+ index: z.number().int(),
232
+ name: z.string().nullable(),
233
+ description: z.string().nullable(),
234
+ fileCount: z.number().int(),
235
+ nameGeneratedAt: z.string().nullable()
236
+ });
237
+ var clusterResourceResultSchema = z.object({
238
+ id: z.string(),
239
+ name: z.string().nullable(),
240
+ clusterCount: z.number().int(),
241
+ fileCount: z.number().int(),
242
+ clusteredAt: z.string().nullable(),
243
+ createdAt: z.string(),
244
+ updatedAt: z.string(),
245
+ clusters: z.array(clusterGroupResultSchema)
246
+ });
247
+ var clusterFileResultSchema = z.object({
248
+ id: z.string(),
249
+ key: z.string(),
250
+ bucketId: z.string(),
251
+ originalFilename: z.string().nullable(),
252
+ size: z.number(),
253
+ contentType: z.string(),
254
+ metadata: z.record(z.string(), z.string()).nullable(),
255
+ createdAt: z.string(),
256
+ distance: z.number().nullable()
257
+ });
258
+ var clusterFilesResultSchema = z.object({
259
+ clusterId: z.string(),
260
+ files: z.array(clusterFileResultSchema),
261
+ limit: z.number().int(),
262
+ offset: z.number().int(),
263
+ total: z.number().int()
264
+ });
227
265
  var profileFilesResultSchema = z.object({
228
266
  id: z.string(),
229
267
  fileCount: z.number()
@@ -271,12 +309,48 @@ var anchorResponseSchema = z.object({
271
309
  var anchorListResponseSchema = z.object({
272
310
  anchors: z.array(anchorResponseSchema)
273
311
  });
312
+ function sleep(ms) {
313
+ return delay(ms);
314
+ }
315
+ function buildTransformUrl(url, options) {
316
+ if (!(options && (options.width || options.height || options.quality || options.format))) {
317
+ return url;
318
+ }
319
+ const parsed = new URL(url);
320
+ if (options.width) {
321
+ parsed.searchParams.set("w", String(options.width));
322
+ }
323
+ if (options.height) {
324
+ parsed.searchParams.set("h", String(options.height));
325
+ }
326
+ if (options.quality) {
327
+ parsed.searchParams.set("q", String(options.quality));
328
+ }
329
+ if (options.format) {
330
+ parsed.searchParams.set("f", options.format);
331
+ }
332
+ return parsed.toString();
333
+ }
274
334
  var StowServer = class {
275
335
  apiKey;
276
336
  baseUrl;
277
337
  bucket;
278
338
  timeout;
279
339
  retries;
340
+ /**
341
+ * Pure helper for building signed transform URLs from an existing public file URL.
342
+ *
343
+ * This does not perform any network I/O and is safe to pass around as a plain
344
+ * function, for example to view-layer code that needs responsive image URLs.
345
+ */
346
+ getTransformUrl;
347
+ /**
348
+ * Create a server SDK instance.
349
+ *
350
+ * Pass a bare string when you only need the API key and want default values
351
+ * for `baseUrl`, `timeout`, and retries. Pass an object when you want a
352
+ * default bucket, a non-production API origin, or custom transport settings.
353
+ */
280
354
  constructor(config) {
281
355
  if (typeof config === "string") {
282
356
  this.apiKey = config;
@@ -290,10 +364,9 @@ var StowServer = class {
290
364
  this.timeout = config.timeout ?? 3e4;
291
365
  this.retries = config.retries ?? 3;
292
366
  }
367
+ this.getTransformUrl = buildTransformUrl;
293
368
  }
294
- /**
295
- * Get the base URL for this instance (used by client SDK)
296
- */
369
+ /** Return the configured API origin, mainly for adapter packages such as `stow-next`. */
297
370
  getBaseUrl() {
298
371
  return this.baseUrl;
299
372
  }
@@ -407,7 +480,7 @@ var StowServer = class {
407
480
  // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: retry + timeout + error normalization intentionally handled in one request pipeline
408
481
  async request(path, options, schema) {
409
482
  const maxAttempts = this.retries + 1;
410
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
483
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
411
484
  const controller = new AbortController();
412
485
  const timeoutId = setTimeout(() => controller.abort(), this.timeout);
413
486
  if (options.signal) {
@@ -429,32 +502,28 @@ var StowServer = class {
429
502
  const code = error.success ? error.data.code : void 0;
430
503
  const isRetryable = response.status === 429 || response.status >= 500;
431
504
  if (isRetryable && attempt < maxAttempts - 1) {
432
- await this.sleep(1e3 * 2 ** attempt);
505
+ await sleep(1e3 * 2 ** attempt);
433
506
  continue;
434
507
  }
435
508
  throw new StowError(message, response.status, code);
436
509
  }
437
510
  return schema ? schema.parse(data) : data;
438
- } catch (err) {
439
- if (err instanceof StowError) {
440
- throw err;
511
+ } catch (error) {
512
+ if (error instanceof StowError) {
513
+ throw error;
441
514
  }
442
- if (err instanceof z.ZodError) {
443
- throw new StowError(
444
- "Invalid response format",
445
- 500,
446
- "INVALID_RESPONSE"
447
- );
515
+ if (error instanceof z.ZodError) {
516
+ throw new StowError("Invalid response format", 500, "INVALID_RESPONSE");
448
517
  }
449
- if (err instanceof DOMException || err instanceof Error && err.name === "AbortError") {
518
+ if (error instanceof DOMException || error instanceof Error && error.name === "AbortError") {
450
519
  throw new StowError("Request timed out", 408, "TIMEOUT");
451
520
  }
452
521
  if (attempt < maxAttempts - 1) {
453
- await this.sleep(1e3 * 2 ** attempt);
522
+ await sleep(1e3 * 2 ** attempt);
454
523
  continue;
455
524
  }
456
525
  throw new StowError(
457
- err instanceof Error ? err.message : "Network error",
526
+ error instanceof Error ? error.message : "Network error",
458
527
  0,
459
528
  "NETWORK_ERROR"
460
529
  );
@@ -464,11 +533,30 @@ var StowServer = class {
464
533
  }
465
534
  throw new StowError("Max retries exceeded", 0, "MAX_RETRIES");
466
535
  }
467
- sleep(ms) {
468
- return new Promise((resolve) => setTimeout(resolve, ms));
469
- }
470
536
  /**
471
- * Upload a file directly from the server
537
+ * Upload bytes from a trusted server environment.
538
+ *
539
+ * This is the highest-level server upload helper:
540
+ * 1. compute a SHA-256 hash for dedupe
541
+ * 2. request a presigned upload URL
542
+ * 3. PUT bytes to storage
543
+ * 4. confirm the upload with optional AI metadata generation
544
+ *
545
+ * Prefer this method when your code already has the file bytes in memory.
546
+ * Use `getPresignedUrl()` + `confirmUpload()` for direct browser uploads
547
+ * instead, and `uploadFromUrl()` when the source is an external URL.
548
+ *
549
+ * @example
550
+ * ```typescript
551
+ * await stow.uploadFile(buffer, {
552
+ * filename: "product.jpg",
553
+ * contentType: "image/jpeg",
554
+ * bucket: "catalog",
555
+ * metadata: { sku: "SKU-123" },
556
+ * title: true,
557
+ * altText: true,
558
+ * });
559
+ * ```
472
560
  */
473
561
  async uploadFile(file, options) {
474
562
  const filename = options?.filename || "file";
@@ -502,10 +590,7 @@ var StowServer = class {
502
590
  throw new StowError("Failed to upload to storage", uploadRes.status);
503
591
  }
504
592
  return this.request(
505
- this.withBucket(
506
- presign.confirmUrl || "/presign/confirm",
507
- options?.bucket
508
- ),
593
+ this.withBucket(presign.confirmUrl || "/presign/confirm", options?.bucket),
509
594
  {
510
595
  method: "POST",
511
596
  headers: { "Content-Type": "application/json" },
@@ -525,7 +610,12 @@ var StowServer = class {
525
610
  );
526
611
  }
527
612
  /**
528
- * Upload a file from a URL (server-side fetch + upload)
613
+ * Import a remote asset by URL.
614
+ *
615
+ * Stow fetches the remote URL server-side, stores the resulting bytes, and
616
+ * persists the file as if it had been uploaded normally. This is useful for
617
+ * migrations, ingestion pipelines, or bringing third-party assets into Stow
618
+ * without downloading them into your own process first.
529
619
  */
530
620
  async uploadFromUrl(url, filename, options) {
531
621
  const result = await this.request(
@@ -560,7 +650,7 @@ var StowServer = class {
560
650
  * The actual fetch, validation, and upload happen in a background worker.
561
651
  * Use this for bulk imports where you don't need immediate confirmation.
562
652
  */
563
- async queueUploadFromUrl(url, filename, options) {
653
+ queueUploadFromUrl(url, filename, options) {
564
654
  return this.request(
565
655
  this.withBucket("/upload", options?.bucket),
566
656
  {
@@ -581,24 +671,20 @@ var StowServer = class {
581
671
  );
582
672
  }
583
673
  /**
584
- * Get a presigned URL for direct client-side upload.
674
+ * Get a presigned URL for a direct client upload.
585
675
  *
586
- * This enables uploads that bypass your server entirely:
587
- * 1. Client calls your endpoint
588
- * 2. Your endpoint calls this method
589
- * 3. Client PUTs directly to the returned uploadUrl
590
- * 4. Client calls confirmUpload to finalize
676
+ * This is the server-side half of the browser upload flow used by
677
+ * `@howells/stow-client` and `@howells/stow-next`:
678
+ * 1. browser calls your app
679
+ * 2. your app calls `getPresignedUrl()`
680
+ * 3. browser PUTs bytes to `uploadUrl`
681
+ * 4. browser or your app calls `confirmUpload()`
682
+ *
683
+ * If `contentHash` matches an existing file in the target bucket, the API
684
+ * short-circuits with `{ dedupe: true, ... }` and no upload is required.
591
685
  */
592
686
  getPresignedUrl(request) {
593
- const {
594
- filename,
595
- contentType,
596
- size,
597
- route,
598
- bucket,
599
- metadata,
600
- contentHash
601
- } = request;
687
+ const { filename, contentType, size, route, bucket, metadata, contentHash } = request;
602
688
  return this.request(
603
689
  this.withBucket("/presign", bucket),
604
690
  {
@@ -617,8 +703,12 @@ var StowServer = class {
617
703
  );
618
704
  }
619
705
  /**
620
- * Confirm a presigned upload after the client has uploaded to R2.
621
- * This creates the file record in the database.
706
+ * Confirm a direct upload after the client has finished the storage PUT.
707
+ *
708
+ * This finalizes the file record in the database and optionally triggers
709
+ * post-processing such as AI-generated title/description/alt text.
710
+ *
711
+ * Call this exactly once per successful presigned upload.
622
712
  */
623
713
  confirmUpload(request) {
624
714
  const {
@@ -654,9 +744,13 @@ var StowServer = class {
654
744
  );
655
745
  }
656
746
  /**
657
- * List files in the bucket
747
+ * List files in a bucket with optional prefix filtering and enrichment blocks.
658
748
  *
659
- * @param options.include - Optional enrichment fields: `"tags"`, `"taxonomies"`
749
+ * Use `cursor` to continue pagination from a previous page. When requesting
750
+ * `include`, Stow expands those relationships inline so you can avoid follow-up
751
+ * per-file lookups.
752
+ *
753
+ * @param options.include Optional enrichment fields: `"tags"`, `"taxonomies"`
660
754
  */
661
755
  listFiles(options) {
662
756
  const params = new URLSearchParams();
@@ -676,11 +770,7 @@ var StowServer = class {
676
770
  params.set("include", options.include.join(","));
677
771
  }
678
772
  const path = `/files?${params}`;
679
- return this.request(
680
- this.withBucket(path, options?.bucket),
681
- { method: "GET" },
682
- listFilesSchema
683
- );
773
+ return this.request(this.withBucket(path, options?.bucket), { method: "GET" }, listFilesSchema);
684
774
  }
685
775
  /**
686
776
  * Delete a file by key
@@ -703,9 +793,12 @@ var StowServer = class {
703
793
  });
704
794
  }
705
795
  /**
706
- * Get a single file by key
796
+ * Get one file by key.
707
797
  *
708
- * @param options.include - Optional enrichment fields: `"tags"`, `"taxonomies"`
798
+ * This is the detailed file view and includes dimensions, embeddings status,
799
+ * extracted colors, and AI metadata fields when available.
800
+ *
801
+ * @param options.include Optional enrichment fields: `"tags"`, `"taxonomies"`
709
802
  */
710
803
  getFile(key, options) {
711
804
  const params = new URLSearchParams();
@@ -813,35 +906,6 @@ var StowServer = class {
813
906
  replaceResultSchema
814
907
  );
815
908
  }
816
- /**
817
- * Get a transform URL for an image.
818
- *
819
- * Appends transform query params (?w=, ?h=, ?q=, ?f=) to a file URL.
820
- * Transforms are applied at the edge by the Cloudflare Worker — no
821
- * server round-trip needed.
822
- *
823
- * @param url - Full file URL (e.g. from upload result's fileUrl)
824
- * @param options - Transform options (width, height, quality, format)
825
- */
826
- getTransformUrl(url, options) {
827
- if (!(options && (options.width || options.height || options.quality || options.format))) {
828
- return url;
829
- }
830
- const parsed = new URL(url);
831
- if (options.width) {
832
- parsed.searchParams.set("w", String(options.width));
833
- }
834
- if (options.height) {
835
- parsed.searchParams.set("h", String(options.height));
836
- }
837
- if (options.quality) {
838
- parsed.searchParams.set("q", String(options.quality));
839
- }
840
- if (options.format) {
841
- parsed.searchParams.set("f", options.format);
842
- }
843
- return parsed.toString();
844
- }
845
909
  // ============================================================
846
910
  // TAGS - Org-scoped labels for file organization
847
911
  // ============================================================
@@ -919,7 +983,34 @@ var StowServer = class {
919
983
  // SEARCH - Vector similarity search
920
984
  // ============================================================
921
985
  /**
922
- * Search namespace for vector similarity search
986
+ * Semantic search namespace.
987
+ *
988
+ * Methods:
989
+ * - `text(...)` embeds text and finds matching files
990
+ * - `similar(...)` finds nearest neighbors for a file, anchor, profile, or cluster
991
+ * - `diverse(...)` balances similarity against result spread
992
+ * - `color(...)` performs palette similarity search
993
+ * - `image(...)` searches using an existing file or an external image URL
994
+ *
995
+ * Cluster-aware search examples:
996
+ *
997
+ * @example
998
+ * ```typescript
999
+ * const cluster = await stow.clusters.create({
1000
+ * bucket: "inspiration",
1001
+ * fileKeys,
1002
+ * clusterCount: 12,
1003
+ * });
1004
+ *
1005
+ * const firstGroup = cluster.clusters[0];
1006
+ * if (firstGroup) {
1007
+ * const related = await stow.search.similar({
1008
+ * bucket: "inspiration",
1009
+ * clusterId: firstGroup.id,
1010
+ * limit: 20,
1011
+ * });
1012
+ * }
1013
+ * ```
923
1014
  */
924
1015
  get search() {
925
1016
  return {
@@ -1091,7 +1182,11 @@ var StowServer = class {
1091
1182
  // PROFILES - Taste/preference profiles from file collections
1092
1183
  // ============================================================
1093
1184
  /**
1094
- * Profiles namespace for managing taste profiles
1185
+ * Taste-profile namespace.
1186
+ *
1187
+ * Profiles are long-lived preference objects. They can be seeded from files,
1188
+ * updated through weighted signals, clustered into interpretable segments, and
1189
+ * then reused as semantic search seeds.
1095
1190
  */
1096
1191
  get profiles() {
1097
1192
  return {
@@ -1107,6 +1202,49 @@ var StowServer = class {
1107
1202
  renameCluster: (profileId, clusterId, params, bucket) => this.renameProfileCluster(profileId, clusterId, params, bucket)
1108
1203
  };
1109
1204
  }
1205
+ /**
1206
+ * Curated cluster namespace.
1207
+ *
1208
+ * Use this when you have an explicit file set that should be grouped by visual
1209
+ * similarity, but should not be modeled as a behavioral profile.
1210
+ *
1211
+ * Typical workflow:
1212
+ * 1. `create({ fileKeys, clusterCount })`
1213
+ * 2. poll `get(id)` until `clusteredAt` is non-null
1214
+ * 3. inspect `clusters`
1215
+ * 4. fetch representative files with `files(id, clusterId)`
1216
+ * 5. optionally `renameCluster(...)`
1217
+ * 6. use `clusterId` with `search.similar(...)` or `search.diverse(...)`
1218
+ *
1219
+ * @example
1220
+ * ```typescript
1221
+ * const resource = await stow.clusters.create({
1222
+ * bucket: "featured-products",
1223
+ * fileKeys: featuredKeys,
1224
+ * clusterCount: 12,
1225
+ * name: "Navigator groups",
1226
+ * });
1227
+ *
1228
+ * const latest = await stow.clusters.get(resource.id, "featured-products");
1229
+ * const group = latest.clusters[0];
1230
+ * if (group) {
1231
+ * const representatives = await stow.clusters.files(resource.id, group.id, {
1232
+ * limit: 12,
1233
+ * offset: 0,
1234
+ * });
1235
+ * }
1236
+ * ```
1237
+ */
1238
+ get clusters() {
1239
+ return {
1240
+ create: (params) => this.createClustersResource(params),
1241
+ get: (id, bucket) => this.getClustersResource(id, bucket),
1242
+ recluster: (id, params, bucket) => this.reclusterClustersResource(id, params, bucket),
1243
+ files: (id, clusterId, params, bucket) => this.getClusterFiles(id, clusterId, params, bucket),
1244
+ renameCluster: (id, clusterId, params, bucket) => this.renameClusterGroup(id, clusterId, params, bucket),
1245
+ delete: (id, bucket) => this.deleteClustersResource(id, bucket)
1246
+ };
1247
+ }
1110
1248
  createProfile(params) {
1111
1249
  return this.request(
1112
1250
  this.withBucket("/profiles", params?.bucket),
@@ -1129,10 +1267,9 @@ var StowServer = class {
1129
1267
  );
1130
1268
  }
1131
1269
  async deleteProfile(id, bucket) {
1132
- await this.request(
1133
- this.withBucket(`/profiles/${encodeURIComponent(id)}`, bucket),
1134
- { method: "DELETE" }
1135
- );
1270
+ await this.request(this.withBucket(`/profiles/${encodeURIComponent(id)}`, bucket), {
1271
+ method: "DELETE"
1272
+ });
1136
1273
  }
1137
1274
  addProfileFiles(id, fileKeys, bucket) {
1138
1275
  return this.request(
@@ -1179,36 +1316,99 @@ var StowServer = class {
1179
1316
  );
1180
1317
  }
1181
1318
  getProfileClusters(id, bucket) {
1319
+ return this.request(this.withBucket(`/profiles/${encodeURIComponent(id)}/clusters`, bucket), {
1320
+ method: "GET"
1321
+ });
1322
+ }
1323
+ reclusterProfile(id, params, bucket) {
1324
+ return this.request(this.withBucket(`/profiles/${encodeURIComponent(id)}/clusters`, bucket), {
1325
+ method: "POST",
1326
+ headers: { "Content-Type": "application/json" },
1327
+ body: JSON.stringify(
1328
+ params?.clusterCount === void 0 ? {} : { clusterCount: params.clusterCount }
1329
+ )
1330
+ });
1331
+ }
1332
+ renameProfileCluster(profileId, clusterId, params, bucket) {
1182
1333
  return this.request(
1183
- this.withBucket(`/profiles/${encodeURIComponent(id)}/clusters`, bucket),
1184
- { method: "GET" }
1334
+ this.withBucket(
1335
+ `/profiles/${encodeURIComponent(profileId)}/clusters/${encodeURIComponent(clusterId)}`,
1336
+ bucket
1337
+ ),
1338
+ {
1339
+ method: "PUT",
1340
+ headers: { "Content-Type": "application/json" },
1341
+ body: JSON.stringify(params)
1342
+ }
1185
1343
  );
1186
1344
  }
1187
- reclusterProfile(id, params, bucket) {
1345
+ createClustersResource(params) {
1188
1346
  return this.request(
1189
- this.withBucket(`/profiles/${encodeURIComponent(id)}/clusters`, bucket),
1347
+ "/clusters",
1190
1348
  {
1191
1349
  method: "POST",
1192
1350
  headers: { "Content-Type": "application/json" },
1193
1351
  body: JSON.stringify({
1194
- ...params?.clusterCount === void 0 ? {} : { clusterCount: params.clusterCount }
1352
+ fileKeys: params.fileKeys,
1353
+ ...params.clusterCount === void 0 ? {} : { clusterCount: params.clusterCount },
1354
+ ...params.name ? { name: params.name } : {},
1355
+ ...params.bucket ? { bucket: params.bucket } : {}
1195
1356
  })
1196
- }
1357
+ },
1358
+ clusterResourceResultSchema
1197
1359
  );
1198
1360
  }
1199
- renameProfileCluster(profileId, clusterId, params, bucket) {
1361
+ getClustersResource(id, bucket) {
1362
+ return this.request(
1363
+ this.withBucket(`/clusters/${encodeURIComponent(id)}`, bucket),
1364
+ { method: "GET" },
1365
+ clusterResourceResultSchema
1366
+ );
1367
+ }
1368
+ reclusterClustersResource(id, params, bucket) {
1369
+ return this.request(
1370
+ this.withBucket(`/clusters/${encodeURIComponent(id)}/recluster`, bucket),
1371
+ {
1372
+ method: "POST",
1373
+ headers: { "Content-Type": "application/json" },
1374
+ body: JSON.stringify(
1375
+ params?.clusterCount === void 0 ? {} : { clusterCount: params.clusterCount }
1376
+ )
1377
+ },
1378
+ clusterResourceResultSchema
1379
+ );
1380
+ }
1381
+ getClusterFiles(id, clusterId, params, bucket) {
1382
+ const searchParams = new URLSearchParams();
1383
+ if (params?.limit !== void 0) {
1384
+ searchParams.set("limit", String(params.limit));
1385
+ }
1386
+ if (params?.offset !== void 0) {
1387
+ searchParams.set("offset", String(params.offset));
1388
+ }
1389
+ const qs = searchParams.toString();
1390
+ const path = `/clusters/${encodeURIComponent(id)}/clusters/${encodeURIComponent(clusterId)}/files${qs ? `?${qs}` : ""}`;
1391
+ return this.request(this.withBucket(path, bucket), { method: "GET" }, clusterFilesResultSchema);
1392
+ }
1393
+ renameClusterGroup(id, clusterId, params, bucket) {
1200
1394
  return this.request(
1201
1395
  this.withBucket(
1202
- `/profiles/${encodeURIComponent(profileId)}/clusters/${encodeURIComponent(clusterId)}`,
1396
+ `/clusters/${encodeURIComponent(id)}/clusters/${encodeURIComponent(clusterId)}`,
1203
1397
  bucket
1204
1398
  ),
1205
1399
  {
1206
1400
  method: "PUT",
1207
1401
  headers: { "Content-Type": "application/json" },
1208
1402
  body: JSON.stringify(params)
1209
- }
1403
+ },
1404
+ clusterGroupResultSchema
1210
1405
  );
1211
1406
  }
1407
+ async deleteClustersResource(id, bucket) {
1408
+ await this.request(this.withBucket(`/clusters/${encodeURIComponent(id)}`, bucket), {
1409
+ method: "DELETE"
1410
+ });
1411
+ }
1212
1412
  // ============================================================
1213
1413
  // ANCHORS - Named semantic reference points in vector space
1214
1414
  // ============================================================
@@ -1270,10 +1470,9 @@ var StowServer = class {
1270
1470
  );
1271
1471
  }
1272
1472
  async deleteAnchor(id, options) {
1273
- await this.request(
1274
- this.withBucket(`/anchors/${encodeURIComponent(id)}`, options?.bucket),
1275
- { method: "DELETE" }
1276
- );
1473
+ await this.request(this.withBucket(`/anchors/${encodeURIComponent(id)}`, options?.bucket), {
1474
+ method: "DELETE"
1475
+ });
1277
1476
  }
1278
1477
  };
1279
1478
  export {
package/package.json CHANGED
@@ -1,20 +1,23 @@
1
1
  {
2
2
  "name": "@howells/stow-server",
3
- "version": "2.2.1",
3
+ "version": "2.3.0",
4
4
  "description": "Server-side SDK for Stow file storage",
5
+ "keywords": [
6
+ "file-storage",
7
+ "s3",
8
+ "sdk",
9
+ "stow",
10
+ "upload"
11
+ ],
12
+ "homepage": "https://stow.sh",
5
13
  "license": "MIT",
6
14
  "repository": {
7
15
  "type": "git",
8
16
  "url": "git+https://github.com/howells/stow.git",
9
17
  "directory": "packages/stow-server"
10
18
  },
11
- "homepage": "https://stow.sh",
12
- "keywords": [
13
- "stow",
14
- "file-storage",
15
- "s3",
16
- "upload",
17
- "sdk"
19
+ "files": [
20
+ "dist"
18
21
  ],
19
22
  "main": "./dist/index.js",
20
23
  "module": "./dist/index.mjs",
@@ -26,24 +29,21 @@
26
29
  "require": "./dist/index.js"
27
30
  }
28
31
  },
29
- "files": [
30
- "dist"
31
- ],
32
- "scripts": {
33
- "build": "tsup src/index.ts --format cjs,esm --dts",
34
- "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
35
- "test": "vitest run",
36
- "test:watch": "vitest"
37
- },
38
- "peerDependencies": {
39
- "zod": "^3.0.0 || ^4.0.0"
40
- },
41
32
  "devDependencies": {
42
- "@stow/typescript-config": "workspace:*",
43
33
  "@types/node": "^25.5.0",
44
34
  "tsup": "^8.5.1",
45
35
  "typescript": "^5.9.3",
46
- "vitest": "^4.1.0",
47
- "zod": "^4.3.6"
36
+ "vite-plus": "latest",
37
+ "zod": "^4.3.6",
38
+ "@stow/typescript-config": "0.0.0"
39
+ },
40
+ "peerDependencies": {
41
+ "zod": "^3.0.0 || ^4.0.0"
42
+ },
43
+ "scripts": {
44
+ "build": "tsup src/index.ts --format cjs,esm --dts",
45
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
46
+ "test": "vp test run",
47
+ "test:watch": "vp test"
48
48
  }
49
- }
49
+ }