@howells/stow-server 0.1.1 → 0.2.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/LICENSE +21 -0
- package/dist/index.d.mts +514 -45
- package/dist/index.d.ts +514 -45
- package/dist/index.js +642 -63
- package/dist/index.mjs +642 -63
- package/package.json +13 -13
package/dist/index.js
CHANGED
|
@@ -24,6 +24,7 @@ __export(index_exports, {
|
|
|
24
24
|
StowServer: () => StowServer
|
|
25
25
|
});
|
|
26
26
|
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
var import_node_crypto = require("crypto");
|
|
27
28
|
var import_zod = require("zod");
|
|
28
29
|
var StowError = class extends Error {
|
|
29
30
|
status;
|
|
@@ -35,12 +36,74 @@ var StowError = class extends Error {
|
|
|
35
36
|
this.code = code;
|
|
36
37
|
}
|
|
37
38
|
};
|
|
39
|
+
var fileColorSchema = import_zod.z.object({
|
|
40
|
+
position: import_zod.z.number().int(),
|
|
41
|
+
proportion: import_zod.z.number(),
|
|
42
|
+
hex: import_zod.z.string(),
|
|
43
|
+
name: import_zod.z.string().nullable(),
|
|
44
|
+
hsl: import_zod.z.object({ h: import_zod.z.number(), s: import_zod.z.number(), l: import_zod.z.number() }),
|
|
45
|
+
oklab: import_zod.z.object({ L: import_zod.z.number(), a: import_zod.z.number(), b: import_zod.z.number() }).nullable(),
|
|
46
|
+
oklch: import_zod.z.object({ l: import_zod.z.number(), c: import_zod.z.number(), h: import_zod.z.number() }).nullable()
|
|
47
|
+
});
|
|
48
|
+
var fileColorProfileSchema = import_zod.z.object({
|
|
49
|
+
palette: import_zod.z.object({
|
|
50
|
+
mood: import_zod.z.string(),
|
|
51
|
+
brightness: import_zod.z.number(),
|
|
52
|
+
temperature: import_zod.z.number(),
|
|
53
|
+
vibrancy: import_zod.z.number(),
|
|
54
|
+
complexity: import_zod.z.number(),
|
|
55
|
+
dominantFamily: import_zod.z.string().nullable()
|
|
56
|
+
}),
|
|
57
|
+
backgroundHex: import_zod.z.string().nullable(),
|
|
58
|
+
accent: import_zod.z.object({
|
|
59
|
+
hex: import_zod.z.string(),
|
|
60
|
+
name: import_zod.z.string().nullable(),
|
|
61
|
+
oklab: import_zod.z.object({ L: import_zod.z.number(), a: import_zod.z.number(), b: import_zod.z.number() }).nullable(),
|
|
62
|
+
oklch: import_zod.z.object({ l: import_zod.z.number(), c: import_zod.z.number(), h: import_zod.z.number() }).nullable()
|
|
63
|
+
}).nullable(),
|
|
64
|
+
extractedAt: import_zod.z.string(),
|
|
65
|
+
colorCount: import_zod.z.number().int()
|
|
66
|
+
});
|
|
38
67
|
var uploadResultSchema = import_zod.z.object({
|
|
39
68
|
key: import_zod.z.string(),
|
|
40
69
|
url: import_zod.z.string().nullable(),
|
|
41
70
|
size: import_zod.z.number(),
|
|
42
71
|
contentType: import_zod.z.string().optional(),
|
|
43
|
-
metadata: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional()
|
|
72
|
+
metadata: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional(),
|
|
73
|
+
deduped: import_zod.z.boolean().optional()
|
|
74
|
+
});
|
|
75
|
+
var bucketSchema = import_zod.z.object({
|
|
76
|
+
id: import_zod.z.string(),
|
|
77
|
+
name: import_zod.z.string(),
|
|
78
|
+
description: import_zod.z.string().nullable().optional(),
|
|
79
|
+
isPublic: import_zod.z.boolean().optional(),
|
|
80
|
+
searchable: import_zod.z.boolean().optional(),
|
|
81
|
+
allowedTypes: import_zod.z.array(import_zod.z.string()).nullable().optional(),
|
|
82
|
+
maxFileSize: import_zod.z.coerce.number().nullable().optional(),
|
|
83
|
+
storageQuota: import_zod.z.coerce.number().nullable().optional(),
|
|
84
|
+
fileCountLimit: import_zod.z.coerce.number().nullable().optional(),
|
|
85
|
+
fileCount: import_zod.z.coerce.number().optional(),
|
|
86
|
+
usageBytes: import_zod.z.coerce.number().optional(),
|
|
87
|
+
createdAt: import_zod.z.string().optional()
|
|
88
|
+
});
|
|
89
|
+
var listBucketsSchema = import_zod.z.object({
|
|
90
|
+
buckets: import_zod.z.array(bucketSchema)
|
|
91
|
+
});
|
|
92
|
+
var bucketResponseSchema = import_zod.z.object({
|
|
93
|
+
bucket: bucketSchema
|
|
94
|
+
});
|
|
95
|
+
var whoamiSchema = import_zod.z.object({
|
|
96
|
+
user: import_zod.z.object({ email: import_zod.z.string() }),
|
|
97
|
+
stats: import_zod.z.object({
|
|
98
|
+
totalBytes: import_zod.z.coerce.number(),
|
|
99
|
+
totalFiles: import_zod.z.coerce.number(),
|
|
100
|
+
bucketCount: import_zod.z.coerce.number()
|
|
101
|
+
}),
|
|
102
|
+
key: import_zod.z.object({
|
|
103
|
+
name: import_zod.z.string(),
|
|
104
|
+
scope: import_zod.z.string(),
|
|
105
|
+
permissions: import_zod.z.record(import_zod.z.string(), import_zod.z.boolean())
|
|
106
|
+
}).optional()
|
|
44
107
|
});
|
|
45
108
|
var listFilesSchema = import_zod.z.object({
|
|
46
109
|
files: import_zod.z.array(
|
|
@@ -49,11 +112,26 @@ var listFilesSchema = import_zod.z.object({
|
|
|
49
112
|
size: import_zod.z.number(),
|
|
50
113
|
lastModified: import_zod.z.string(),
|
|
51
114
|
url: import_zod.z.string().nullable(),
|
|
52
|
-
|
|
115
|
+
width: import_zod.z.number().nullable().optional(),
|
|
116
|
+
height: import_zod.z.number().nullable().optional(),
|
|
117
|
+
duration: import_zod.z.number().nullable().optional(),
|
|
118
|
+
metadata: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional(),
|
|
119
|
+
colorProfile: fileColorProfileSchema.nullable().optional(),
|
|
120
|
+
colors: import_zod.z.array(fileColorSchema).optional()
|
|
53
121
|
})
|
|
54
122
|
),
|
|
55
123
|
nextCursor: import_zod.z.string().nullable()
|
|
56
124
|
});
|
|
125
|
+
var reprocessResultSchema = import_zod.z.object({
|
|
126
|
+
key: import_zod.z.string(),
|
|
127
|
+
triggered: import_zod.z.array(import_zod.z.string())
|
|
128
|
+
});
|
|
129
|
+
var replaceResultSchema = import_zod.z.object({
|
|
130
|
+
key: import_zod.z.string(),
|
|
131
|
+
size: import_zod.z.number(),
|
|
132
|
+
contentType: import_zod.z.string(),
|
|
133
|
+
triggered: import_zod.z.array(import_zod.z.string())
|
|
134
|
+
});
|
|
57
135
|
var errorSchema = import_zod.z.object({
|
|
58
136
|
error: import_zod.z.string(),
|
|
59
137
|
code: import_zod.z.string().optional()
|
|
@@ -78,12 +156,23 @@ var listDropsSchema = import_zod.z.object({
|
|
|
78
156
|
drops: import_zod.z.array(dropSchema),
|
|
79
157
|
usage: import_zod.z.object({ bytes: import_zod.z.number(), limit: import_zod.z.number() })
|
|
80
158
|
});
|
|
81
|
-
var
|
|
159
|
+
var presignNewResultSchema = import_zod.z.object({
|
|
82
160
|
fileKey: import_zod.z.string(),
|
|
83
161
|
uploadUrl: import_zod.z.string(),
|
|
84
|
-
|
|
85
|
-
|
|
162
|
+
confirmUrl: import_zod.z.string(),
|
|
163
|
+
dedupe: import_zod.z.literal(false).optional()
|
|
86
164
|
});
|
|
165
|
+
var presignDedupeResultSchema = import_zod.z.object({
|
|
166
|
+
dedupe: import_zod.z.literal(true),
|
|
167
|
+
key: import_zod.z.string(),
|
|
168
|
+
url: import_zod.z.string().nullable(),
|
|
169
|
+
size: import_zod.z.number(),
|
|
170
|
+
contentType: import_zod.z.string()
|
|
171
|
+
});
|
|
172
|
+
var presignResultSchema = import_zod.z.union([
|
|
173
|
+
presignDedupeResultSchema,
|
|
174
|
+
presignNewResultSchema
|
|
175
|
+
]);
|
|
87
176
|
var confirmResultSchema = import_zod.z.object({
|
|
88
177
|
key: import_zod.z.string(),
|
|
89
178
|
url: import_zod.z.string().nullable(),
|
|
@@ -91,18 +180,93 @@ var confirmResultSchema = import_zod.z.object({
|
|
|
91
180
|
contentType: import_zod.z.string(),
|
|
92
181
|
metadata: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional()
|
|
93
182
|
});
|
|
183
|
+
var fileResultSchema = import_zod.z.object({
|
|
184
|
+
key: import_zod.z.string(),
|
|
185
|
+
size: import_zod.z.number(),
|
|
186
|
+
contentType: import_zod.z.string(),
|
|
187
|
+
url: import_zod.z.string().nullable(),
|
|
188
|
+
width: import_zod.z.number().nullable(),
|
|
189
|
+
height: import_zod.z.number().nullable(),
|
|
190
|
+
duration: import_zod.z.number().nullable(),
|
|
191
|
+
metadata: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).nullable(),
|
|
192
|
+
colorProfile: fileColorProfileSchema.nullable(),
|
|
193
|
+
colors: import_zod.z.array(fileColorSchema),
|
|
194
|
+
embeddingStatus: import_zod.z.string().nullable(),
|
|
195
|
+
createdAt: import_zod.z.string()
|
|
196
|
+
});
|
|
197
|
+
var profileClusterResultSchema = import_zod.z.object({
|
|
198
|
+
id: import_zod.z.string(),
|
|
199
|
+
index: import_zod.z.number().int(),
|
|
200
|
+
name: import_zod.z.string().nullable(),
|
|
201
|
+
description: import_zod.z.string().nullable(),
|
|
202
|
+
signalCount: import_zod.z.number().int(),
|
|
203
|
+
totalWeight: import_zod.z.number(),
|
|
204
|
+
nameGeneratedAt: import_zod.z.string().nullable()
|
|
205
|
+
});
|
|
206
|
+
var profileResultSchema = import_zod.z.object({
|
|
207
|
+
id: import_zod.z.string(),
|
|
208
|
+
name: import_zod.z.string().nullable(),
|
|
209
|
+
fileCount: import_zod.z.number(),
|
|
210
|
+
signalCount: import_zod.z.number(),
|
|
211
|
+
vector: import_zod.z.array(import_zod.z.number()).nullable(),
|
|
212
|
+
clusters: import_zod.z.array(profileClusterResultSchema).optional(),
|
|
213
|
+
createdAt: import_zod.z.string(),
|
|
214
|
+
updatedAt: import_zod.z.string()
|
|
215
|
+
});
|
|
216
|
+
var profileFilesResultSchema = import_zod.z.object({
|
|
217
|
+
id: import_zod.z.string(),
|
|
218
|
+
fileCount: import_zod.z.number()
|
|
219
|
+
});
|
|
220
|
+
var profileSignalResultSchema = import_zod.z.object({
|
|
221
|
+
id: import_zod.z.string(),
|
|
222
|
+
fileKey: import_zod.z.string(),
|
|
223
|
+
type: import_zod.z.enum([
|
|
224
|
+
"view",
|
|
225
|
+
"view_long",
|
|
226
|
+
"click",
|
|
227
|
+
"like",
|
|
228
|
+
"save",
|
|
229
|
+
"choose",
|
|
230
|
+
"purchase",
|
|
231
|
+
"share",
|
|
232
|
+
"dismiss",
|
|
233
|
+
"skip",
|
|
234
|
+
"reject",
|
|
235
|
+
"report",
|
|
236
|
+
"custom"
|
|
237
|
+
]),
|
|
238
|
+
weight: import_zod.z.number()
|
|
239
|
+
});
|
|
240
|
+
var profileSignalsResponseSchema = import_zod.z.object({
|
|
241
|
+
profileId: import_zod.z.string(),
|
|
242
|
+
signals: import_zod.z.array(profileSignalResultSchema),
|
|
243
|
+
totalSignals: import_zod.z.number(),
|
|
244
|
+
vectorUpdated: import_zod.z.boolean()
|
|
245
|
+
});
|
|
246
|
+
var deleteProfileSignalsResponseSchema = import_zod.z.object({
|
|
247
|
+
profileId: import_zod.z.string(),
|
|
248
|
+
removed: import_zod.z.number(),
|
|
249
|
+
totalSignals: import_zod.z.number(),
|
|
250
|
+
vectorUpdated: import_zod.z.boolean()
|
|
251
|
+
});
|
|
94
252
|
var StowServer = class {
|
|
95
253
|
apiKey;
|
|
96
254
|
baseUrl;
|
|
97
255
|
bucket;
|
|
256
|
+
timeout;
|
|
257
|
+
retries;
|
|
98
258
|
constructor(config) {
|
|
99
259
|
if (typeof config === "string") {
|
|
100
260
|
this.apiKey = config;
|
|
101
261
|
this.baseUrl = "https://app.stow.sh";
|
|
262
|
+
this.timeout = 3e4;
|
|
263
|
+
this.retries = 3;
|
|
102
264
|
} else {
|
|
103
265
|
this.apiKey = config.apiKey;
|
|
104
266
|
this.baseUrl = config.baseUrl || "https://app.stow.sh";
|
|
105
267
|
this.bucket = config.bucket;
|
|
268
|
+
this.timeout = config.timeout ?? 3e4;
|
|
269
|
+
this.retries = config.retries ?? 3;
|
|
106
270
|
}
|
|
107
271
|
}
|
|
108
272
|
/**
|
|
@@ -111,6 +275,88 @@ var StowServer = class {
|
|
|
111
275
|
getBaseUrl() {
|
|
112
276
|
return this.baseUrl;
|
|
113
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* Return account usage and API key info for the current credential.
|
|
280
|
+
*/
|
|
281
|
+
whoami() {
|
|
282
|
+
return this.request("/api/whoami", { method: "GET" }, whoamiSchema);
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* List all buckets available to the current organization.
|
|
286
|
+
*/
|
|
287
|
+
listBuckets() {
|
|
288
|
+
return this.request("/api/buckets", { method: "GET" }, listBucketsSchema);
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Create a new bucket.
|
|
292
|
+
*/
|
|
293
|
+
async createBucket(request) {
|
|
294
|
+
const result = await this.request(
|
|
295
|
+
"/api/buckets",
|
|
296
|
+
{
|
|
297
|
+
method: "POST",
|
|
298
|
+
headers: { "Content-Type": "application/json" },
|
|
299
|
+
body: JSON.stringify(request)
|
|
300
|
+
},
|
|
301
|
+
bucketResponseSchema
|
|
302
|
+
);
|
|
303
|
+
return result.bucket;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Get bucket details by id.
|
|
307
|
+
*/
|
|
308
|
+
async getBucket(id) {
|
|
309
|
+
const result = await this.request(
|
|
310
|
+
`/api/buckets/${encodeURIComponent(id)}`,
|
|
311
|
+
{ method: "GET" },
|
|
312
|
+
bucketResponseSchema
|
|
313
|
+
);
|
|
314
|
+
return result.bucket;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Update bucket settings by id.
|
|
318
|
+
*/
|
|
319
|
+
async updateBucket(id, updates) {
|
|
320
|
+
const result = await this.request(
|
|
321
|
+
`/api/buckets/${encodeURIComponent(id)}`,
|
|
322
|
+
{
|
|
323
|
+
method: "PATCH",
|
|
324
|
+
headers: { "Content-Type": "application/json" },
|
|
325
|
+
body: JSON.stringify(updates)
|
|
326
|
+
},
|
|
327
|
+
bucketResponseSchema
|
|
328
|
+
);
|
|
329
|
+
return result.bucket;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Rename/update a bucket by current bucket name.
|
|
333
|
+
*/
|
|
334
|
+
async updateBucketByName(name, updates) {
|
|
335
|
+
const result = await this.request(
|
|
336
|
+
`/api/buckets/_?bucket=${encodeURIComponent(name)}`,
|
|
337
|
+
{
|
|
338
|
+
method: "PATCH",
|
|
339
|
+
headers: { "Content-Type": "application/json" },
|
|
340
|
+
body: JSON.stringify(updates)
|
|
341
|
+
},
|
|
342
|
+
bucketResponseSchema
|
|
343
|
+
);
|
|
344
|
+
return result.bucket;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Rename a bucket by current bucket name.
|
|
348
|
+
*/
|
|
349
|
+
renameBucket(name, newName) {
|
|
350
|
+
return this.updateBucketByName(name, { name: newName });
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Delete a bucket by id.
|
|
354
|
+
*/
|
|
355
|
+
async deleteBucket(id) {
|
|
356
|
+
await this.request(`/api/buckets/${encodeURIComponent(id)}`, {
|
|
357
|
+
method: "DELETE"
|
|
358
|
+
});
|
|
359
|
+
}
|
|
114
360
|
/**
|
|
115
361
|
* Resolve the effective bucket for this request.
|
|
116
362
|
* Per-call override > constructor default.
|
|
@@ -130,55 +376,131 @@ var StowServer = class {
|
|
|
130
376
|
return `${path}${sep}bucket=${encodeURIComponent(b)}`;
|
|
131
377
|
}
|
|
132
378
|
/**
|
|
133
|
-
* Make an API request with
|
|
379
|
+
* Make an API request with retry, timeout, and error handling.
|
|
380
|
+
*
|
|
381
|
+
* - Retries on 429 (rate limit) and 5xx with exponential backoff (1s, 2s, 4s).
|
|
382
|
+
* - AbortController timeout (default 30s).
|
|
383
|
+
* - Consumer can pass `signal` in options to cancel.
|
|
134
384
|
*/
|
|
385
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: retry + timeout + error normalization intentionally handled in one request pipeline
|
|
135
386
|
async request(path, options, schema) {
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
387
|
+
const maxAttempts = this.retries + 1;
|
|
388
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
389
|
+
const controller = new AbortController();
|
|
390
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
391
|
+
if (options.signal) {
|
|
392
|
+
options.signal.addEventListener("abort", () => controller.abort());
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
396
|
+
...options,
|
|
397
|
+
signal: controller.signal,
|
|
398
|
+
headers: {
|
|
399
|
+
...options.headers,
|
|
400
|
+
"x-api-key": this.apiKey
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
const data = await response.json();
|
|
404
|
+
if (!response.ok) {
|
|
405
|
+
const error = errorSchema.safeParse(data);
|
|
406
|
+
const message = error.success ? error.data.error : "Request failed";
|
|
407
|
+
const code = error.success ? error.data.code : void 0;
|
|
408
|
+
const isRetryable = response.status === 429 || response.status >= 500;
|
|
409
|
+
if (isRetryable && attempt < maxAttempts - 1) {
|
|
410
|
+
await this.sleep(1e3 * 2 ** attempt);
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
throw new StowError(message, response.status, code);
|
|
414
|
+
}
|
|
415
|
+
return schema ? schema.parse(data) : data;
|
|
416
|
+
} catch (err) {
|
|
417
|
+
if (err instanceof StowError) {
|
|
418
|
+
throw err;
|
|
419
|
+
}
|
|
420
|
+
if (err instanceof import_zod.z.ZodError) {
|
|
421
|
+
throw new StowError(
|
|
422
|
+
"Invalid response format",
|
|
423
|
+
500,
|
|
424
|
+
"INVALID_RESPONSE"
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
if (err instanceof DOMException || err instanceof Error && err.name === "AbortError") {
|
|
428
|
+
throw new StowError("Request timed out", 408, "TIMEOUT");
|
|
429
|
+
}
|
|
430
|
+
if (attempt < maxAttempts - 1) {
|
|
431
|
+
await this.sleep(1e3 * 2 ** attempt);
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
throw new StowError(
|
|
435
|
+
err instanceof Error ? err.message : "Network error",
|
|
436
|
+
0,
|
|
437
|
+
"NETWORK_ERROR"
|
|
438
|
+
);
|
|
439
|
+
} finally {
|
|
440
|
+
clearTimeout(timeoutId);
|
|
141
441
|
}
|
|
142
|
-
});
|
|
143
|
-
const data = await response.json();
|
|
144
|
-
if (!response.ok) {
|
|
145
|
-
const error = errorSchema.safeParse(data);
|
|
146
|
-
const message = error.success ? error.data.error : "Request failed";
|
|
147
|
-
const code = error.success ? error.data.code : void 0;
|
|
148
|
-
throw new StowError(message, response.status, code);
|
|
149
442
|
}
|
|
150
|
-
|
|
443
|
+
throw new StowError("Max retries exceeded", 0, "MAX_RETRIES");
|
|
444
|
+
}
|
|
445
|
+
sleep(ms) {
|
|
446
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
151
447
|
}
|
|
152
448
|
/**
|
|
153
449
|
* Upload a file directly from the server
|
|
154
450
|
*/
|
|
155
451
|
async uploadFile(file, options) {
|
|
156
|
-
const
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
452
|
+
const filename = options?.filename || "file";
|
|
453
|
+
const buffer = Buffer.isBuffer(file) ? file : Buffer.from(await file.arrayBuffer());
|
|
454
|
+
const contentType = options?.contentType || (!Buffer.isBuffer(file) && file.type ? file.type : "application/octet-stream");
|
|
455
|
+
const contentHash = (0, import_node_crypto.createHash)("sha256").update(buffer).digest("hex");
|
|
456
|
+
const presign = await this.getPresignedUrl({
|
|
457
|
+
filename,
|
|
458
|
+
contentType,
|
|
459
|
+
size: buffer.length,
|
|
460
|
+
...options?.route ? { route: options.route } : {},
|
|
461
|
+
...options?.bucket ? { bucket: options.bucket } : {},
|
|
462
|
+
...options?.metadata ? { metadata: options.metadata } : {},
|
|
463
|
+
contentHash
|
|
464
|
+
});
|
|
465
|
+
if (presign.dedupe) {
|
|
466
|
+
return {
|
|
467
|
+
key: presign.key,
|
|
468
|
+
url: presign.url,
|
|
469
|
+
size: presign.size,
|
|
470
|
+
contentType: presign.contentType,
|
|
471
|
+
deduped: true
|
|
472
|
+
};
|
|
163
473
|
}
|
|
164
|
-
|
|
165
|
-
|
|
474
|
+
const uploadRes = await fetch(presign.uploadUrl, {
|
|
475
|
+
method: "PUT",
|
|
476
|
+
headers: { "Content-Type": contentType },
|
|
477
|
+
body: new Uint8Array(buffer)
|
|
478
|
+
});
|
|
479
|
+
if (!uploadRes.ok) {
|
|
480
|
+
throw new StowError("Failed to upload to storage", uploadRes.status);
|
|
166
481
|
}
|
|
167
|
-
|
|
168
|
-
this.withBucket(
|
|
482
|
+
return this.request(
|
|
483
|
+
this.withBucket(
|
|
484
|
+
presign.confirmUrl || "/api/presign/confirm",
|
|
485
|
+
options?.bucket
|
|
486
|
+
),
|
|
169
487
|
{
|
|
170
488
|
method: "POST",
|
|
171
|
-
|
|
489
|
+
headers: { "Content-Type": "application/json" },
|
|
490
|
+
body: JSON.stringify({
|
|
491
|
+
fileKey: presign.fileKey,
|
|
492
|
+
size: buffer.length,
|
|
493
|
+
contentType,
|
|
494
|
+
...options?.metadata ? { metadata: options.metadata } : {},
|
|
495
|
+
contentHash,
|
|
496
|
+
skipVerify: true,
|
|
497
|
+
...options?.title ? { title: true } : {},
|
|
498
|
+
...options?.describe ? { describe: true } : {},
|
|
499
|
+
...options?.altText ? { altText: true } : {}
|
|
500
|
+
})
|
|
172
501
|
},
|
|
173
|
-
|
|
502
|
+
confirmResultSchema
|
|
174
503
|
);
|
|
175
|
-
return {
|
|
176
|
-
key: result.key,
|
|
177
|
-
url: result.url,
|
|
178
|
-
size: result.size,
|
|
179
|
-
contentType: result.contentType || options?.contentType || "application/octet-stream",
|
|
180
|
-
...result.metadata ? { metadata: result.metadata } : {}
|
|
181
|
-
};
|
|
182
504
|
}
|
|
183
505
|
/**
|
|
184
506
|
* Upload a file from a URL (server-side fetch + upload)
|
|
@@ -192,7 +514,11 @@ var StowServer = class {
|
|
|
192
514
|
body: JSON.stringify({
|
|
193
515
|
url,
|
|
194
516
|
filename,
|
|
195
|
-
...options?.metadata ? { metadata: options.metadata } : {}
|
|
517
|
+
...options?.metadata ? { metadata: options.metadata } : {},
|
|
518
|
+
...options?.headers ? { headers: options.headers } : {},
|
|
519
|
+
...options?.title ? { title: true } : {},
|
|
520
|
+
...options?.describe ? { describe: true } : {},
|
|
521
|
+
...options?.altText ? { altText: true } : {}
|
|
196
522
|
})
|
|
197
523
|
},
|
|
198
524
|
uploadResultSchema
|
|
@@ -215,7 +541,15 @@ var StowServer = class {
|
|
|
215
541
|
* 4. Client calls confirmUpload to finalize
|
|
216
542
|
*/
|
|
217
543
|
getPresignedUrl(request) {
|
|
218
|
-
const {
|
|
544
|
+
const {
|
|
545
|
+
filename,
|
|
546
|
+
contentType,
|
|
547
|
+
size,
|
|
548
|
+
route,
|
|
549
|
+
bucket,
|
|
550
|
+
metadata,
|
|
551
|
+
contentHash
|
|
552
|
+
} = request;
|
|
219
553
|
return this.request(
|
|
220
554
|
this.withBucket("/api/presign", bucket),
|
|
221
555
|
{
|
|
@@ -226,7 +560,8 @@ var StowServer = class {
|
|
|
226
560
|
contentType,
|
|
227
561
|
size,
|
|
228
562
|
route,
|
|
229
|
-
...metadata ? { metadata } : {}
|
|
563
|
+
...metadata ? { metadata } : {},
|
|
564
|
+
...contentHash ? { contentHash } : {}
|
|
230
565
|
})
|
|
231
566
|
},
|
|
232
567
|
presignResultSchema
|
|
@@ -237,7 +572,19 @@ var StowServer = class {
|
|
|
237
572
|
* This creates the file record in the database.
|
|
238
573
|
*/
|
|
239
574
|
confirmUpload(request) {
|
|
240
|
-
const {
|
|
575
|
+
const {
|
|
576
|
+
fileKey,
|
|
577
|
+
size,
|
|
578
|
+
contentType,
|
|
579
|
+
bucket,
|
|
580
|
+
metadata,
|
|
581
|
+
skipVerify,
|
|
582
|
+
deferKvSync,
|
|
583
|
+
contentHash,
|
|
584
|
+
title,
|
|
585
|
+
describe,
|
|
586
|
+
altText
|
|
587
|
+
} = request;
|
|
241
588
|
return this.request(
|
|
242
589
|
this.withBucket("/api/presign/confirm", bucket),
|
|
243
590
|
{
|
|
@@ -247,7 +594,13 @@ var StowServer = class {
|
|
|
247
594
|
fileKey,
|
|
248
595
|
size,
|
|
249
596
|
contentType,
|
|
250
|
-
...metadata ? { metadata } : {}
|
|
597
|
+
...metadata ? { metadata } : {},
|
|
598
|
+
...skipVerify ? { skipVerify } : {},
|
|
599
|
+
...deferKvSync ? { deferKvSync } : {},
|
|
600
|
+
...contentHash ? { contentHash } : {},
|
|
601
|
+
...title ? { title } : {},
|
|
602
|
+
...describe ? { describe } : {},
|
|
603
|
+
...altText ? { altText } : {}
|
|
251
604
|
})
|
|
252
605
|
},
|
|
253
606
|
confirmResultSchema
|
|
@@ -289,7 +642,7 @@ var StowServer = class {
|
|
|
289
642
|
/**
|
|
290
643
|
* Update metadata on an existing file
|
|
291
644
|
*/
|
|
292
|
-
|
|
645
|
+
updateFileMetadata(key, metadata, options) {
|
|
293
646
|
const path = `/api/files/${encodeURIComponent(key)}`;
|
|
294
647
|
return this.request(this.withBucket(path, options?.bucket), {
|
|
295
648
|
method: "PATCH",
|
|
@@ -297,6 +650,51 @@ var StowServer = class {
|
|
|
297
650
|
body: JSON.stringify({ metadata })
|
|
298
651
|
});
|
|
299
652
|
}
|
|
653
|
+
/**
|
|
654
|
+
* Get a single file by key
|
|
655
|
+
*/
|
|
656
|
+
getFile(key, options) {
|
|
657
|
+
const path = `/api/files/${encodeURIComponent(key)}`;
|
|
658
|
+
return this.request(
|
|
659
|
+
this.withBucket(path, options?.bucket),
|
|
660
|
+
{ method: "GET" },
|
|
661
|
+
fileResultSchema
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Reprocess a file: reset all derived data (embeddings, colors, dimensions,
|
|
666
|
+
* AI metadata, taxonomies) and re-trigger processing tasks.
|
|
667
|
+
*/
|
|
668
|
+
reprocessFile(key, options) {
|
|
669
|
+
const path = `/api/files/${encodeURIComponent(key)}/reprocess`;
|
|
670
|
+
return this.request(
|
|
671
|
+
this.withBucket(path, options?.bucket),
|
|
672
|
+
{ method: "POST" },
|
|
673
|
+
reprocessResultSchema
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Replace a file's content by fetching from a new URL.
|
|
678
|
+
*
|
|
679
|
+
* Keeps the same file key but replaces the stored object and resets all
|
|
680
|
+
* derived data (dimensions, embeddings, colors, AI metadata). Processing
|
|
681
|
+
* tasks are re-dispatched as if the file were newly uploaded.
|
|
682
|
+
*/
|
|
683
|
+
replaceFile(key, url, options) {
|
|
684
|
+
const path = `/api/files/${encodeURIComponent(key)}/replace`;
|
|
685
|
+
return this.request(
|
|
686
|
+
this.withBucket(path, options?.bucket),
|
|
687
|
+
{
|
|
688
|
+
method: "PUT",
|
|
689
|
+
headers: { "Content-Type": "application/json" },
|
|
690
|
+
body: JSON.stringify({
|
|
691
|
+
url,
|
|
692
|
+
...options?.headers ? { headers: options.headers } : {}
|
|
693
|
+
})
|
|
694
|
+
},
|
|
695
|
+
replaceResultSchema
|
|
696
|
+
);
|
|
697
|
+
}
|
|
300
698
|
/**
|
|
301
699
|
* Get a transform URL for an image.
|
|
302
700
|
*
|
|
@@ -308,25 +706,23 @@ var StowServer = class {
|
|
|
308
706
|
* @param options - Transform options (width, height, quality, format)
|
|
309
707
|
*/
|
|
310
708
|
getTransformUrl(url, options) {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
params.set("w", options.width.toString());
|
|
709
|
+
if (!(options && (options.width || options.height || options.quality || options.format))) {
|
|
710
|
+
return url;
|
|
314
711
|
}
|
|
315
|
-
|
|
316
|
-
|
|
712
|
+
const parsed = new URL(url);
|
|
713
|
+
if (options.width) {
|
|
714
|
+
parsed.searchParams.set("w", String(options.width));
|
|
317
715
|
}
|
|
318
|
-
if (options
|
|
319
|
-
|
|
716
|
+
if (options.height) {
|
|
717
|
+
parsed.searchParams.set("h", String(options.height));
|
|
320
718
|
}
|
|
321
|
-
if (options
|
|
322
|
-
|
|
719
|
+
if (options.quality) {
|
|
720
|
+
parsed.searchParams.set("q", String(options.quality));
|
|
323
721
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
return url;
|
|
722
|
+
if (options.format) {
|
|
723
|
+
parsed.searchParams.set("f", options.format);
|
|
327
724
|
}
|
|
328
|
-
|
|
329
|
-
return `${url}${separator}${query}`;
|
|
725
|
+
return parsed.toString();
|
|
330
726
|
}
|
|
331
727
|
// ============================================================
|
|
332
728
|
// TAGS - Org-scoped labels for file organization
|
|
@@ -387,18 +783,77 @@ var StowServer = class {
|
|
|
387
783
|
*/
|
|
388
784
|
get search() {
|
|
389
785
|
return {
|
|
390
|
-
similar: (params) => this.searchSimilar(params)
|
|
786
|
+
similar: (params) => this.searchSimilar(params),
|
|
787
|
+
diverse: (params) => this.searchDiverse(params ?? {}),
|
|
788
|
+
text: (params) => this.searchText(params),
|
|
789
|
+
color: (params) => this.searchColor(params)
|
|
391
790
|
};
|
|
392
791
|
}
|
|
393
792
|
searchSimilar(params) {
|
|
793
|
+
const bucket = this.resolveBucket(params.bucket);
|
|
394
794
|
return this.request("/api/search/similar", {
|
|
395
795
|
method: "POST",
|
|
396
796
|
headers: { "Content-Type": "application/json" },
|
|
397
797
|
body: JSON.stringify({
|
|
398
798
|
...params.fileKey ? { fileKey: params.fileKey } : {},
|
|
399
799
|
...params.vector ? { vector: params.vector } : {},
|
|
400
|
-
...params.
|
|
401
|
-
...params.
|
|
800
|
+
...params.profileId ? { profileId: params.profileId } : {},
|
|
801
|
+
...params.clusterId ? { clusterId: params.clusterId } : {},
|
|
802
|
+
...params.clusterIds?.length ? { clusterIds: params.clusterIds } : {},
|
|
803
|
+
...bucket ? { bucket } : {},
|
|
804
|
+
...params.limit ? { limit: params.limit } : {},
|
|
805
|
+
...params.excludeKeys?.length ? { excludeKeys: params.excludeKeys } : {},
|
|
806
|
+
...params.filters ? { filters: params.filters } : {},
|
|
807
|
+
...params.include?.length ? { include: params.include } : {}
|
|
808
|
+
})
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
searchDiverse(params) {
|
|
812
|
+
const bucket = this.resolveBucket(params.bucket);
|
|
813
|
+
return this.request("/api/search/diverse", {
|
|
814
|
+
method: "POST",
|
|
815
|
+
headers: { "Content-Type": "application/json" },
|
|
816
|
+
body: JSON.stringify({
|
|
817
|
+
...params.fileKey ? { fileKey: params.fileKey } : {},
|
|
818
|
+
...params.vector ? { vector: params.vector } : {},
|
|
819
|
+
...params.profileId ? { profileId: params.profileId } : {},
|
|
820
|
+
...params.clusterId ? { clusterId: params.clusterId } : {},
|
|
821
|
+
...params.clusterIds?.length ? { clusterIds: params.clusterIds } : {},
|
|
822
|
+
...bucket ? { bucket } : {},
|
|
823
|
+
...params.limit ? { limit: params.limit } : {},
|
|
824
|
+
...params.lambda !== void 0 ? { lambda: params.lambda } : {},
|
|
825
|
+
...params.excludeKeys?.length ? { excludeKeys: params.excludeKeys } : {},
|
|
826
|
+
...params.filters ? { filters: params.filters } : {},
|
|
827
|
+
...params.include?.length ? { include: params.include } : {}
|
|
828
|
+
})
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
searchText(params) {
|
|
832
|
+
const bucket = this.resolveBucket(params.bucket);
|
|
833
|
+
return this.request("/api/search/text", {
|
|
834
|
+
method: "POST",
|
|
835
|
+
headers: { "Content-Type": "application/json" },
|
|
836
|
+
body: JSON.stringify({
|
|
837
|
+
query: params.query,
|
|
838
|
+
...bucket ? { bucket } : {},
|
|
839
|
+
...params.limit ? { limit: params.limit } : {},
|
|
840
|
+
...params.filters ? { filters: params.filters } : {},
|
|
841
|
+
...params.include?.length ? { include: params.include } : {}
|
|
842
|
+
})
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
searchColor(params) {
|
|
846
|
+
const bucket = this.resolveBucket(params.bucket);
|
|
847
|
+
return this.request("/api/search/color", {
|
|
848
|
+
method: "POST",
|
|
849
|
+
headers: { "Content-Type": "application/json" },
|
|
850
|
+
body: JSON.stringify({
|
|
851
|
+
...params.hex ? { hex: params.hex } : {},
|
|
852
|
+
...params.oklab ? { oklab: params.oklab } : {},
|
|
853
|
+
...bucket ? { bucket } : {},
|
|
854
|
+
...params.limit ? { limit: params.limit } : {},
|
|
855
|
+
...params.minProportion !== void 0 ? { minProportion: params.minProportion } : {},
|
|
856
|
+
...params.dominantOnly ? { dominantOnly: params.dominantOnly } : {}
|
|
402
857
|
})
|
|
403
858
|
});
|
|
404
859
|
}
|
|
@@ -439,7 +894,7 @@ var StowServer = class {
|
|
|
439
894
|
throw new StowError("Failed to upload to storage", putRes.status);
|
|
440
895
|
}
|
|
441
896
|
return this.request(
|
|
442
|
-
"/api/drops/presign/confirm",
|
|
897
|
+
presign.confirmUrl || "/api/drops/presign/confirm",
|
|
443
898
|
{
|
|
444
899
|
method: "POST",
|
|
445
900
|
headers: { "Content-Type": "application/json" },
|
|
@@ -469,6 +924,130 @@ var StowServer = class {
|
|
|
469
924
|
method: "DELETE"
|
|
470
925
|
});
|
|
471
926
|
}
|
|
927
|
+
// ============================================================
|
|
928
|
+
// PROFILES - Taste/preference profiles from file collections
|
|
929
|
+
// ============================================================
|
|
930
|
+
/**
|
|
931
|
+
* Profiles namespace for managing taste profiles
|
|
932
|
+
*/
|
|
933
|
+
get profiles() {
|
|
934
|
+
return {
|
|
935
|
+
create: (params) => this.createProfile(params),
|
|
936
|
+
get: (id) => this.getProfile(id),
|
|
937
|
+
delete: (id) => this.deleteProfile(id),
|
|
938
|
+
addFiles: (id, fileKeys, bucket) => this.addProfileFiles(id, fileKeys, bucket),
|
|
939
|
+
removeFiles: (id, fileKeys, bucket) => this.removeProfileFiles(id, fileKeys, bucket),
|
|
940
|
+
signal: (id, signals, bucket) => this.signalProfile(id, signals, bucket),
|
|
941
|
+
deleteSignals: (id, signalIds) => this.deleteProfileSignals(id, signalIds),
|
|
942
|
+
clusters: (id) => this.getProfileClusters(id),
|
|
943
|
+
recluster: (id, params) => this.reclusterProfile(id, params),
|
|
944
|
+
renameCluster: (profileId, clusterId, params) => this.renameProfileCluster(profileId, clusterId, params)
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
createProfile(params) {
|
|
948
|
+
return this.request(
|
|
949
|
+
"/api/profiles",
|
|
950
|
+
{
|
|
951
|
+
method: "POST",
|
|
952
|
+
headers: { "Content-Type": "application/json" },
|
|
953
|
+
body: JSON.stringify({
|
|
954
|
+
...params?.name ? { name: params.name } : {},
|
|
955
|
+
...params?.fileKeys ? { fileKeys: params.fileKeys } : {},
|
|
956
|
+
...params?.bucket ? { bucket: params.bucket } : {}
|
|
957
|
+
})
|
|
958
|
+
},
|
|
959
|
+
profileResultSchema
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
getProfile(id) {
|
|
963
|
+
return this.request(
|
|
964
|
+
`/api/profiles/${encodeURIComponent(id)}`,
|
|
965
|
+
{ method: "GET" },
|
|
966
|
+
profileResultSchema
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
async deleteProfile(id) {
|
|
970
|
+
await this.request(`/api/profiles/${encodeURIComponent(id)}`, {
|
|
971
|
+
method: "DELETE"
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
addProfileFiles(id, fileKeys, bucket) {
|
|
975
|
+
return this.request(
|
|
976
|
+
`/api/profiles/${encodeURIComponent(id)}/files`,
|
|
977
|
+
{
|
|
978
|
+
method: "POST",
|
|
979
|
+
headers: { "Content-Type": "application/json" },
|
|
980
|
+
body: JSON.stringify({
|
|
981
|
+
fileKeys,
|
|
982
|
+
...bucket ? { bucket } : {}
|
|
983
|
+
})
|
|
984
|
+
},
|
|
985
|
+
profileFilesResultSchema
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
removeProfileFiles(id, fileKeys, bucket) {
|
|
989
|
+
return this.request(
|
|
990
|
+
`/api/profiles/${encodeURIComponent(id)}/files`,
|
|
991
|
+
{
|
|
992
|
+
method: "DELETE",
|
|
993
|
+
headers: { "Content-Type": "application/json" },
|
|
994
|
+
body: JSON.stringify({
|
|
995
|
+
fileKeys,
|
|
996
|
+
...bucket ? { bucket } : {}
|
|
997
|
+
})
|
|
998
|
+
},
|
|
999
|
+
profileFilesResultSchema
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
signalProfile(id, signals, bucket) {
|
|
1003
|
+
return this.request(
|
|
1004
|
+
`/api/profiles/${encodeURIComponent(id)}/signals`,
|
|
1005
|
+
{
|
|
1006
|
+
method: "POST",
|
|
1007
|
+
headers: { "Content-Type": "application/json" },
|
|
1008
|
+
body: JSON.stringify({
|
|
1009
|
+
signals,
|
|
1010
|
+
...bucket ? { bucket } : {}
|
|
1011
|
+
})
|
|
1012
|
+
},
|
|
1013
|
+
profileSignalsResponseSchema
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
deleteProfileSignals(id, signalIds) {
|
|
1017
|
+
return this.request(
|
|
1018
|
+
`/api/profiles/${encodeURIComponent(id)}/signals`,
|
|
1019
|
+
{
|
|
1020
|
+
method: "DELETE",
|
|
1021
|
+
headers: { "Content-Type": "application/json" },
|
|
1022
|
+
body: JSON.stringify({ signalIds })
|
|
1023
|
+
},
|
|
1024
|
+
deleteProfileSignalsResponseSchema
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
1027
|
+
getProfileClusters(id) {
|
|
1028
|
+
return this.request(`/api/profiles/${encodeURIComponent(id)}/clusters`, {
|
|
1029
|
+
method: "GET"
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
reclusterProfile(id, params) {
|
|
1033
|
+
return this.request(`/api/profiles/${encodeURIComponent(id)}/clusters`, {
|
|
1034
|
+
method: "POST",
|
|
1035
|
+
headers: { "Content-Type": "application/json" },
|
|
1036
|
+
body: JSON.stringify({
|
|
1037
|
+
...params?.clusterCount !== void 0 ? { clusterCount: params.clusterCount } : {}
|
|
1038
|
+
})
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
renameProfileCluster(profileId, clusterId, params) {
|
|
1042
|
+
return this.request(
|
|
1043
|
+
`/api/profiles/${encodeURIComponent(profileId)}/clusters/${encodeURIComponent(clusterId)}`,
|
|
1044
|
+
{
|
|
1045
|
+
method: "PUT",
|
|
1046
|
+
headers: { "Content-Type": "application/json" },
|
|
1047
|
+
body: JSON.stringify(params)
|
|
1048
|
+
}
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
472
1051
|
};
|
|
473
1052
|
// Annotate the CommonJS export names for ESM import in node:
|
|
474
1053
|
0 && (module.exports = {
|