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