@vryzel/file-next 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/{filesystem-Djiiu6YU.d.ts → filesystem-CF4TFF58.d.ts} +18 -122
- package/dist/{filesystem-C0vVDtg4.d.cts → filesystem-CFRC-gky.d.cts} +18 -122
- package/dist/index.cjs +52 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +52 -35
- package/dist/index.js.map +1 -1
- package/dist/server/index.cjs +186 -78
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +34 -110
- package/dist/server/index.d.ts +34 -110
- package/dist/server/index.js +186 -78
- package/dist/server/index.js.map +1 -1
- package/dist/sync/index.cjs +33 -10
- package/dist/sync/index.cjs.map +1 -1
- package/dist/sync/index.d.cts +1 -1
- package/dist/sync/index.d.ts +1 -1
- package/dist/sync/index.js +33 -10
- package/dist/sync/index.js.map +1 -1
- package/dist/{with-auth-OZNiZGga.d.ts → with-auth-BvFHyioA.d.ts} +1 -1
- package/dist/{with-auth-Dtn57B_S.d.cts → with-auth-UBz99KZJ.d.cts} +1 -1
- package/package.json +5 -5
package/dist/server/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { F as FileNode, M as MetadataStore, a as FileSystem, R as Result } from '../filesystem-
|
|
2
|
+
import { F as FileNode, M as MetadataStore, a as FileSystem, R as Result } from '../filesystem-CF4TFF58.js';
|
|
3
3
|
import { FileSystemError } from '../errors/index.js';
|
|
4
|
-
import { A as AuthContext } from '../with-auth-
|
|
4
|
+
import { A as AuthContext } from '../with-auth-BvFHyioA.js';
|
|
5
5
|
import { createWriteThrough } from '../sync/index.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -13,150 +13,64 @@ declare const ListFilesInputSchema: z.ZodObject<{
|
|
|
13
13
|
parentId: z.ZodNullable<z.ZodString>;
|
|
14
14
|
limit: z.ZodOptional<z.ZodNumber>;
|
|
15
15
|
cursor: z.ZodOptional<z.ZodString>;
|
|
16
|
-
},
|
|
17
|
-
parentId: string | null;
|
|
18
|
-
limit?: number | undefined;
|
|
19
|
-
cursor?: string | undefined;
|
|
20
|
-
}, {
|
|
21
|
-
parentId: string | null;
|
|
22
|
-
limit?: number | undefined;
|
|
23
|
-
cursor?: string | undefined;
|
|
24
|
-
}>;
|
|
16
|
+
}, z.core.$strip>;
|
|
25
17
|
declare const DeleteFileInputSchema: z.ZodObject<{
|
|
26
18
|
id: z.ZodString;
|
|
27
19
|
recursive: z.ZodOptional<z.ZodBoolean>;
|
|
28
|
-
},
|
|
29
|
-
id: string;
|
|
30
|
-
recursive?: boolean | undefined;
|
|
31
|
-
}, {
|
|
32
|
-
id: string;
|
|
33
|
-
recursive?: boolean | undefined;
|
|
34
|
-
}>;
|
|
20
|
+
}, z.core.$strip>;
|
|
35
21
|
declare const MoveFileInputSchema: z.ZodObject<{
|
|
36
22
|
id: z.ZodString;
|
|
37
23
|
newParentId: z.ZodNullable<z.ZodString>;
|
|
38
24
|
newName: z.ZodOptional<z.ZodString>;
|
|
39
|
-
},
|
|
40
|
-
id: string;
|
|
41
|
-
newParentId: string | null;
|
|
42
|
-
newName?: string | undefined;
|
|
43
|
-
}, {
|
|
44
|
-
id: string;
|
|
45
|
-
newParentId: string | null;
|
|
46
|
-
newName?: string | undefined;
|
|
47
|
-
}>;
|
|
25
|
+
}, z.core.$strip>;
|
|
48
26
|
declare const CopyFileInputSchema: z.ZodObject<{
|
|
49
27
|
id: z.ZodString;
|
|
50
28
|
newParentId: z.ZodNullable<z.ZodString>;
|
|
51
29
|
newName: z.ZodOptional<z.ZodString>;
|
|
52
|
-
},
|
|
53
|
-
id: string;
|
|
54
|
-
newParentId: string | null;
|
|
55
|
-
newName?: string | undefined;
|
|
56
|
-
}, {
|
|
57
|
-
id: string;
|
|
58
|
-
newParentId: string | null;
|
|
59
|
-
newName?: string | undefined;
|
|
60
|
-
}>;
|
|
30
|
+
}, z.core.$strip>;
|
|
61
31
|
declare const SetMetadataInputSchema: z.ZodObject<{
|
|
62
32
|
id: z.ZodString;
|
|
63
33
|
metadata: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
64
34
|
replace: z.ZodOptional<z.ZodBoolean>;
|
|
65
|
-
},
|
|
66
|
-
id: string;
|
|
67
|
-
metadata: Record<string, string>;
|
|
68
|
-
replace?: boolean | undefined;
|
|
69
|
-
}, {
|
|
70
|
-
id: string;
|
|
71
|
-
metadata: Record<string, string>;
|
|
72
|
-
replace?: boolean | undefined;
|
|
73
|
-
}>;
|
|
35
|
+
}, z.core.$strip>;
|
|
74
36
|
declare const CreateFolderInputSchema: z.ZodObject<{
|
|
75
37
|
parentId: z.ZodNullable<z.ZodString>;
|
|
76
38
|
name: z.ZodString;
|
|
77
|
-
},
|
|
78
|
-
parentId: string | null;
|
|
79
|
-
name: string;
|
|
80
|
-
}, {
|
|
81
|
-
parentId: string | null;
|
|
82
|
-
name: string;
|
|
83
|
-
}>;
|
|
39
|
+
}, z.core.$strip>;
|
|
84
40
|
declare const PrepareUploadInputSchema: z.ZodObject<{
|
|
85
41
|
parentId: z.ZodNullable<z.ZodString>;
|
|
86
42
|
name: z.ZodString;
|
|
87
43
|
contentType: z.ZodString;
|
|
88
44
|
contentLength: z.ZodNumber;
|
|
89
|
-
},
|
|
90
|
-
parentId: string | null;
|
|
91
|
-
name: string;
|
|
92
|
-
contentType: string;
|
|
93
|
-
contentLength: number;
|
|
94
|
-
}, {
|
|
95
|
-
parentId: string | null;
|
|
96
|
-
name: string;
|
|
97
|
-
contentType: string;
|
|
98
|
-
contentLength: number;
|
|
99
|
-
}>;
|
|
45
|
+
}, z.core.$strip>;
|
|
100
46
|
declare const ConfirmUploadInputSchema: z.ZodObject<{
|
|
101
47
|
id: z.ZodString;
|
|
102
48
|
parentId: z.ZodNullable<z.ZodString>;
|
|
103
49
|
name: z.ZodString;
|
|
104
50
|
contentType: z.ZodOptional<z.ZodString>;
|
|
105
51
|
size: z.ZodOptional<z.ZodNumber>;
|
|
106
|
-
},
|
|
107
|
-
parentId: string | null;
|
|
108
|
-
id: string;
|
|
109
|
-
name: string;
|
|
110
|
-
contentType?: string | undefined;
|
|
111
|
-
size?: number | undefined;
|
|
112
|
-
}, {
|
|
113
|
-
parentId: string | null;
|
|
114
|
-
id: string;
|
|
115
|
-
name: string;
|
|
116
|
-
contentType?: string | undefined;
|
|
117
|
-
size?: number | undefined;
|
|
118
|
-
}>;
|
|
52
|
+
}, z.core.$strip>;
|
|
119
53
|
declare const SearchFilesInputSchema: z.ZodObject<{
|
|
120
54
|
query: z.ZodString;
|
|
121
55
|
parentId: z.ZodOptional<z.ZodString>;
|
|
122
56
|
limit: z.ZodOptional<z.ZodNumber>;
|
|
123
|
-
},
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
},
|
|
128
|
-
query: string;
|
|
129
|
-
parentId?: string | undefined;
|
|
130
|
-
limit?: number | undefined;
|
|
131
|
-
}>;
|
|
57
|
+
}, z.core.$strip>;
|
|
58
|
+
declare const ListTrashInputSchema: z.ZodObject<{
|
|
59
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
60
|
+
cursor: z.ZodOptional<z.ZodString>;
|
|
61
|
+
}, z.core.$strip>;
|
|
132
62
|
declare const RestoreNodeInputSchema: z.ZodObject<{
|
|
133
63
|
id: z.ZodString;
|
|
134
|
-
},
|
|
135
|
-
id: string;
|
|
136
|
-
}, {
|
|
137
|
-
id: string;
|
|
138
|
-
}>;
|
|
64
|
+
}, z.core.$strip>;
|
|
139
65
|
declare const CreateShareInputSchema: z.ZodObject<{
|
|
140
66
|
id: z.ZodString;
|
|
141
|
-
},
|
|
142
|
-
id: string;
|
|
143
|
-
}, {
|
|
144
|
-
id: string;
|
|
145
|
-
}>;
|
|
67
|
+
}, z.core.$strip>;
|
|
146
68
|
declare const ResolveShareInputSchema: z.ZodObject<{
|
|
147
69
|
token: z.ZodString;
|
|
148
|
-
},
|
|
149
|
-
token: string;
|
|
150
|
-
}, {
|
|
151
|
-
token: string;
|
|
152
|
-
}>;
|
|
70
|
+
}, z.core.$strip>;
|
|
153
71
|
declare const RevokeShareInputSchema: z.ZodObject<{
|
|
154
72
|
token: z.ZodString;
|
|
155
|
-
},
|
|
156
|
-
token: string;
|
|
157
|
-
}, {
|
|
158
|
-
token: string;
|
|
159
|
-
}>;
|
|
73
|
+
}, z.core.$strip>;
|
|
160
74
|
interface ServerActionsDeps {
|
|
161
75
|
readonly store: MetadataStore;
|
|
162
76
|
readonly writeThrough: CreateWriteThrough;
|
|
@@ -164,6 +78,10 @@ interface ServerActionsDeps {
|
|
|
164
78
|
readonly getAuth: () => AuthContext | Promise<AuthContext>;
|
|
165
79
|
/** App path prefix for share links. Default `/api/share`. */
|
|
166
80
|
readonly sharePathPrefix?: string;
|
|
81
|
+
/** Per-object cap for `prepareUpload`. Falls back to `fs.maxBytes`. */
|
|
82
|
+
readonly maxBytes?: number;
|
|
83
|
+
/** Literals (`image/png`) or wildcards (`image/*`). SVG is never allowed. */
|
|
84
|
+
readonly allowedContentTypes?: ReadonlyArray<string>;
|
|
167
85
|
}
|
|
168
86
|
declare const createServerActions: (deps: ServerActionsDeps) => {
|
|
169
87
|
listFiles: (input: z.infer<typeof ListFilesInputSchema>) => Promise<Result<ListFilesOutput, FileSystemError>>;
|
|
@@ -175,10 +93,7 @@ declare const createServerActions: (deps: ServerActionsDeps) => {
|
|
|
175
93
|
prepareUpload: (input: z.infer<typeof PrepareUploadInputSchema>) => Promise<Result<PrepareUploadOutput, FileSystemError>>;
|
|
176
94
|
confirmUpload: (input: z.infer<typeof ConfirmUploadInputSchema>) => Promise<Result<FileNode, FileSystemError>>;
|
|
177
95
|
searchFiles: (input: z.infer<typeof SearchFilesInputSchema>) => Promise<Result<ListFilesOutput, FileSystemError>>;
|
|
178
|
-
listTrash: (input?:
|
|
179
|
-
limit?: number;
|
|
180
|
-
cursor?: string;
|
|
181
|
-
}) => Promise<Result<ListFilesOutput, FileSystemError>>;
|
|
96
|
+
listTrash: (input?: z.infer<typeof ListTrashInputSchema>) => Promise<Result<ListFilesOutput, FileSystemError>>;
|
|
182
97
|
restoreNode: (input: z.infer<typeof RestoreNodeInputSchema>) => Promise<Result<FileNode, FileSystemError>>;
|
|
183
98
|
purgeNode: (input: z.infer<typeof RestoreNodeInputSchema>) => Promise<Result<void, FileSystemError>>;
|
|
184
99
|
createShare: (input: z.infer<typeof CreateShareInputSchema>) => Promise<Result<{
|
|
@@ -203,8 +118,10 @@ interface PrepareUploadOutput {
|
|
|
203
118
|
|
|
204
119
|
interface CreateUploadRouteHandlerOptions {
|
|
205
120
|
readonly fs: FileSystem;
|
|
206
|
-
/** Resolve the caller.
|
|
121
|
+
/** Resolve the caller. Required unless `unsafeNoAuth` is set. */
|
|
207
122
|
readonly getAuth?: (req: Request) => Promise<AuthContext | null>;
|
|
123
|
+
/** Explicit opt-in for a handler that signs without a caller. */
|
|
124
|
+
readonly unsafeNoAuth?: boolean;
|
|
208
125
|
/** Reject requests whose `contentLength` exceeds this. Omit for no cap. */
|
|
209
126
|
readonly maxBytes?: number;
|
|
210
127
|
/**
|
|
@@ -234,7 +151,12 @@ declare const createUploadRouteHandler: (opts: CreateUploadRouteHandlerOptions)
|
|
|
234
151
|
|
|
235
152
|
interface CreateDownloadRouteHandlerOptions {
|
|
236
153
|
readonly fs: FileSystem;
|
|
154
|
+
/** Required unless `unsafeNoAuth` is set. */
|
|
237
155
|
readonly getAuth?: (req: Request) => Promise<AuthContext | null>;
|
|
156
|
+
/** Explicit opt-in for a handler that signs without a caller. */
|
|
157
|
+
readonly unsafeNoAuth?: boolean;
|
|
158
|
+
/** Required with `getAuth`. Signs only a live node key for that tenant. */
|
|
159
|
+
readonly store?: MetadataStore;
|
|
238
160
|
/** Presigned URL lifetime in seconds. Default 900, max 7d (S3 SigV4). */
|
|
239
161
|
readonly expiresIn?: number;
|
|
240
162
|
}
|
|
@@ -256,6 +178,8 @@ declare const createDownloadRouteHandler: (opts: CreateDownloadRouteHandlerOptio
|
|
|
256
178
|
interface CreateShareRouteHandlerOptions {
|
|
257
179
|
readonly store: MetadataStore;
|
|
258
180
|
readonly fs: FileSystem;
|
|
181
|
+
/** Bytes buffered through the app. Larger objects redirect to a 60s signed URL. */
|
|
182
|
+
readonly maxBytes?: number;
|
|
259
183
|
}
|
|
260
184
|
declare const createShareRouteHandler: (opts: CreateShareRouteHandlerOptions) => ((req: Request) => Promise<Response>);
|
|
261
185
|
|
package/dist/server/index.js
CHANGED
|
@@ -158,13 +158,44 @@ var FileSystemError = class extends Error {
|
|
|
158
158
|
var asS3Key = (s) => s;
|
|
159
159
|
var asTenantId = (s) => s;
|
|
160
160
|
|
|
161
|
+
// src/storage/content-disposition.ts
|
|
162
|
+
var NEVER_INLINE = /* @__PURE__ */ new Set(["text/html", "application/xhtml+xml", "image/svg+xml"]);
|
|
163
|
+
var DENIED_UPLOAD = /* @__PURE__ */ new Set([
|
|
164
|
+
"text/html",
|
|
165
|
+
"application/xhtml+xml",
|
|
166
|
+
"image/svg+xml",
|
|
167
|
+
"text/xml",
|
|
168
|
+
"application/xml"
|
|
169
|
+
]);
|
|
170
|
+
function mimeBase(contentType) {
|
|
171
|
+
return (contentType ?? "").split(";")[0]?.trim().toLowerCase() ?? "";
|
|
172
|
+
}
|
|
173
|
+
function isDeniedUploadType(contentType) {
|
|
174
|
+
return DENIED_UPLOAD.has(mimeBase(contentType));
|
|
175
|
+
}
|
|
176
|
+
function matchesAllowedContentType(pattern, actual) {
|
|
177
|
+
const mime = mimeBase(actual);
|
|
178
|
+
if (!mime || isDeniedUploadType(mime)) return false;
|
|
179
|
+
if (pattern === mime) return true;
|
|
180
|
+
if (pattern.endsWith("/*")) return mime.startsWith(pattern.slice(0, -1));
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
function signedContentDisposition(requested, contentType) {
|
|
184
|
+
const mime = (contentType ?? "").split(";")[0]?.trim().toLowerCase() ?? "";
|
|
185
|
+
if (!mime || NEVER_INLINE.has(mime)) return "attachment";
|
|
186
|
+
if (mime === "application/pdf" || mime === "text/plain" || mime === "text/csv" || mime === "text/markdown" || mime.startsWith("image/") || mime.startsWith("video/") || mime.startsWith("audio/")) {
|
|
187
|
+
return "inline";
|
|
188
|
+
}
|
|
189
|
+
return "attachment";
|
|
190
|
+
}
|
|
191
|
+
|
|
161
192
|
// src/server/actions.ts
|
|
162
193
|
var NodeIdSchema = z.string().min(1, "id is required");
|
|
163
194
|
var PathSchema = z.string().nullable();
|
|
164
195
|
var ListFilesInputSchema = z.object({
|
|
165
196
|
parentId: PathSchema,
|
|
166
|
-
limit: z.number().int().positive().optional(),
|
|
167
|
-
cursor: z.string().optional()
|
|
197
|
+
limit: z.number().int().positive().max(500).optional(),
|
|
198
|
+
cursor: z.string().max(256).optional()
|
|
168
199
|
});
|
|
169
200
|
var DeleteFileInputSchema = z.object({
|
|
170
201
|
id: NodeIdSchema,
|
|
@@ -182,7 +213,7 @@ var CopyFileInputSchema = z.object({
|
|
|
182
213
|
});
|
|
183
214
|
var SetMetadataInputSchema = z.object({
|
|
184
215
|
id: NodeIdSchema,
|
|
185
|
-
metadata: z.record(z.string()),
|
|
216
|
+
metadata: z.record(z.string(), z.string()),
|
|
186
217
|
replace: z.boolean().optional()
|
|
187
218
|
});
|
|
188
219
|
var CreateFolderInputSchema = z.object({
|
|
@@ -203,9 +234,13 @@ var ConfirmUploadInputSchema = z.object({
|
|
|
203
234
|
size: z.number().int().nonnegative().optional()
|
|
204
235
|
});
|
|
205
236
|
var SearchFilesInputSchema = z.object({
|
|
206
|
-
query: z.string(),
|
|
207
|
-
parentId: z.string().optional(),
|
|
208
|
-
limit: z.number().int().positive().optional()
|
|
237
|
+
query: z.string().max(256),
|
|
238
|
+
parentId: z.string().max(256).optional(),
|
|
239
|
+
limit: z.number().int().positive().max(500).optional()
|
|
240
|
+
});
|
|
241
|
+
var ListTrashInputSchema = z.object({
|
|
242
|
+
limit: z.number().int().positive().max(500).optional(),
|
|
243
|
+
cursor: z.string().max(256).optional()
|
|
209
244
|
});
|
|
210
245
|
var RestoreNodeInputSchema = z.object({
|
|
211
246
|
id: NodeIdSchema
|
|
@@ -234,7 +269,7 @@ var createServerActions = (deps) => {
|
|
|
234
269
|
if (e instanceof FileSystemError) return e;
|
|
235
270
|
return new FileSystemError({
|
|
236
271
|
code,
|
|
237
|
-
message
|
|
272
|
+
message,
|
|
238
273
|
retryable: true
|
|
239
274
|
});
|
|
240
275
|
};
|
|
@@ -335,12 +370,34 @@ var createServerActions = (deps) => {
|
|
|
335
370
|
const prepareUpload = async (input) => {
|
|
336
371
|
const parsed = PrepareUploadInputSchema.safeParse(input);
|
|
337
372
|
if (!parsed.success) return zodErr("Invalid prepareUpload input", parsed);
|
|
373
|
+
const maxBytes = deps.maxBytes ?? fs.maxBytes;
|
|
374
|
+
if (maxBytes != null && parsed.data.contentLength > maxBytes) {
|
|
375
|
+
return err(
|
|
376
|
+
new FileSystemError({
|
|
377
|
+
code: "PayloadTooLarge",
|
|
378
|
+
message: `contentLength exceeds maxBytes ${maxBytes}`,
|
|
379
|
+
retryable: false
|
|
380
|
+
})
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
if (isDeniedUploadType(parsed.data.contentType) || deps.allowedContentTypes != null && !deps.allowedContentTypes.some(
|
|
384
|
+
(pattern) => matchesAllowedContentType(pattern, parsed.data.contentType)
|
|
385
|
+
)) {
|
|
386
|
+
return err(
|
|
387
|
+
new FileSystemError({
|
|
388
|
+
code: "UnsupportedMediaType",
|
|
389
|
+
message: "Content type is not allowed",
|
|
390
|
+
retryable: false
|
|
391
|
+
})
|
|
392
|
+
);
|
|
393
|
+
}
|
|
338
394
|
try {
|
|
339
395
|
const auth = await authTenant();
|
|
340
396
|
const id = typeof globalThis.crypto?.randomUUID === "function" ? globalThis.crypto.randomUUID() : `upl-${Date.now()}`;
|
|
341
397
|
const signed = await fs.forTenant(auth.tenantId).adapter.createPresignedUploadUrl({
|
|
342
398
|
key: asS3Key(id),
|
|
343
399
|
contentType: parsed.data.contentType,
|
|
400
|
+
contentLength: parsed.data.contentLength,
|
|
344
401
|
expiresIn: 900
|
|
345
402
|
});
|
|
346
403
|
if (!signed.ok) return signed;
|
|
@@ -390,12 +447,14 @@ var createServerActions = (deps) => {
|
|
|
390
447
|
}
|
|
391
448
|
};
|
|
392
449
|
const listTrash = async (input = {}) => {
|
|
450
|
+
const parsed = ListTrashInputSchema.safeParse(input);
|
|
451
|
+
if (!parsed.success) return zodErr("Invalid listTrash input", parsed);
|
|
393
452
|
try {
|
|
394
453
|
const auth = await authTenant();
|
|
395
454
|
return store.listTrash({
|
|
396
455
|
tenantId: asTenantId(auth.tenantId),
|
|
397
|
-
limit:
|
|
398
|
-
cursor:
|
|
456
|
+
limit: parsed.data.limit,
|
|
457
|
+
cursor: parsed.data.cursor
|
|
399
458
|
});
|
|
400
459
|
} catch (e) {
|
|
401
460
|
return err(wrap(e, "InternalError", "listTrash failed"));
|
|
@@ -497,18 +556,12 @@ var createServerActions = (deps) => {
|
|
|
497
556
|
revokeShare
|
|
498
557
|
};
|
|
499
558
|
};
|
|
500
|
-
|
|
501
|
-
// src/server/route-handlers/upload.ts
|
|
502
559
|
var MAX_EXPIRES_IN_SECONDS = 7 * 24 * 60 * 60;
|
|
503
560
|
var DEFAULT_EXPIRES_IN_SECONDS = 900;
|
|
504
|
-
var
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
return actual.startsWith(prefix);
|
|
509
|
-
}
|
|
510
|
-
return false;
|
|
511
|
-
};
|
|
561
|
+
var UploadBody = z.object({
|
|
562
|
+
contentType: z.string().min(1).max(255),
|
|
563
|
+
contentLength: z.number().int().nonnegative()
|
|
564
|
+
});
|
|
512
565
|
var assertExpiresIn = (expiresIn) => {
|
|
513
566
|
if (!Number.isFinite(expiresIn) || expiresIn < 1 || expiresIn > MAX_EXPIRES_IN_SECONDS) {
|
|
514
567
|
throw new FileSystemError({
|
|
@@ -520,65 +573,76 @@ var assertExpiresIn = (expiresIn) => {
|
|
|
520
573
|
}
|
|
521
574
|
};
|
|
522
575
|
var createUploadRouteHandler = (opts) => {
|
|
576
|
+
if (!opts.getAuth && opts.unsafeNoAuth !== true) {
|
|
577
|
+
throw new Error("file-next: getAuth is required");
|
|
578
|
+
}
|
|
523
579
|
const expiresIn = opts.expiresIn ?? DEFAULT_EXPIRES_IN_SECONDS;
|
|
524
580
|
assertExpiresIn(expiresIn);
|
|
525
581
|
return async (req) => {
|
|
526
|
-
let
|
|
582
|
+
let fs = opts.fs;
|
|
583
|
+
if (opts.getAuth) {
|
|
584
|
+
const auth = await opts.getAuth(req);
|
|
585
|
+
if (!auth) {
|
|
586
|
+
return Response.json(
|
|
587
|
+
{ ok: false, error: { code: "Unauthorized", message: "Not authenticated" } },
|
|
588
|
+
{ status: 401 }
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
fs = opts.fs.forTenant(auth.tenantId);
|
|
592
|
+
}
|
|
593
|
+
let raw;
|
|
527
594
|
try {
|
|
528
|
-
|
|
595
|
+
raw = await req.json();
|
|
529
596
|
} catch {
|
|
530
597
|
return Response.json(
|
|
531
598
|
{ ok: false, error: { code: "InternalError", message: "Request body must be valid JSON" } },
|
|
532
599
|
{ status: 400 }
|
|
533
600
|
);
|
|
534
601
|
}
|
|
602
|
+
const parsed = UploadBody.safeParse(raw);
|
|
603
|
+
if (!parsed.success) {
|
|
604
|
+
return Response.json(
|
|
605
|
+
{ ok: false, error: { code: "InternalError", message: "Invalid upload request" } },
|
|
606
|
+
{ status: 400 }
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
const body = parsed.data;
|
|
535
610
|
if (typeof opts.maxBytes === "number" && body.contentLength > opts.maxBytes) {
|
|
536
611
|
return Response.json(
|
|
537
612
|
{
|
|
538
613
|
ok: false,
|
|
539
614
|
error: {
|
|
540
615
|
code: "PayloadTooLarge",
|
|
541
|
-
message: `contentLength
|
|
616
|
+
message: `contentLength exceeds maxBytes ${opts.maxBytes}`
|
|
542
617
|
}
|
|
543
618
|
},
|
|
544
619
|
{ status: 413 }
|
|
545
620
|
);
|
|
546
621
|
}
|
|
547
|
-
if (opts.allowedContentTypes && opts.allowedContentTypes.length > 0
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
let fs = opts.fs;
|
|
563
|
-
if (opts.getAuth) {
|
|
564
|
-
const auth = await opts.getAuth(req);
|
|
565
|
-
if (!auth) {
|
|
566
|
-
return Response.json(
|
|
567
|
-
{ ok: false, error: { code: "Unauthorized", message: "Not authenticated" } },
|
|
568
|
-
{ status: 401 }
|
|
569
|
-
);
|
|
570
|
-
}
|
|
571
|
-
fs = opts.fs.forTenant(auth.tenantId);
|
|
622
|
+
if (isDeniedUploadType(body.contentType) || opts.allowedContentTypes && opts.allowedContentTypes.length > 0 && !opts.allowedContentTypes.some(
|
|
623
|
+
(pattern) => matchesAllowedContentType(pattern, body.contentType)
|
|
624
|
+
)) {
|
|
625
|
+
return Response.json(
|
|
626
|
+
{
|
|
627
|
+
ok: false,
|
|
628
|
+
error: {
|
|
629
|
+
code: "UnsupportedMediaType",
|
|
630
|
+
message: "Content type is not allowed"
|
|
631
|
+
}
|
|
632
|
+
},
|
|
633
|
+
{ status: 415 }
|
|
634
|
+
);
|
|
572
635
|
}
|
|
573
636
|
const id = typeof globalThis.crypto?.randomUUID === "function" ? globalThis.crypto.randomUUID() : `upl-${Date.now()}`;
|
|
574
637
|
const r = await fs.adapter.createPresignedUploadUrl({
|
|
575
638
|
key: asS3Key(id),
|
|
576
639
|
contentType: body.contentType,
|
|
640
|
+
contentLength: body.contentLength,
|
|
577
641
|
expiresIn
|
|
578
642
|
});
|
|
579
643
|
if (!r.ok) {
|
|
580
644
|
return Response.json(
|
|
581
|
-
{ ok: false, error: { code: r.error.code, message:
|
|
645
|
+
{ ok: false, error: { code: r.error.code, message: "Upload failed" } },
|
|
582
646
|
{ status: 500 }
|
|
583
647
|
);
|
|
584
648
|
}
|
|
@@ -611,9 +675,28 @@ var assertExpiresIn2 = (expiresIn) => {
|
|
|
611
675
|
}
|
|
612
676
|
};
|
|
613
677
|
var createDownloadRouteHandler = (opts) => {
|
|
678
|
+
if (!opts.getAuth && opts.unsafeNoAuth !== true) {
|
|
679
|
+
throw new Error("file-next: getAuth is required");
|
|
680
|
+
}
|
|
681
|
+
if (opts.getAuth && !opts.store) {
|
|
682
|
+
throw new Error("file-next: store is required when getAuth is set");
|
|
683
|
+
}
|
|
614
684
|
const expiresIn = opts.expiresIn ?? DEFAULT_EXPIRES_IN_SECONDS2;
|
|
615
685
|
assertExpiresIn2(expiresIn);
|
|
616
686
|
return async (req) => {
|
|
687
|
+
let fs = opts.fs;
|
|
688
|
+
let tenantId;
|
|
689
|
+
if (opts.getAuth) {
|
|
690
|
+
const auth = await opts.getAuth(req);
|
|
691
|
+
if (!auth) {
|
|
692
|
+
return Response.json(
|
|
693
|
+
{ ok: false, error: { code: "Unauthorized", message: "Not authenticated" } },
|
|
694
|
+
{ status: 401 }
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
tenantId = auth.tenantId;
|
|
698
|
+
fs = opts.fs.forTenant(auth.tenantId);
|
|
699
|
+
}
|
|
617
700
|
const url = new URL(req.url);
|
|
618
701
|
const key = url.searchParams.get("key");
|
|
619
702
|
if (!key) {
|
|
@@ -622,16 +705,17 @@ var createDownloadRouteHandler = (opts) => {
|
|
|
622
705
|
{ status: 400 }
|
|
623
706
|
);
|
|
624
707
|
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
708
|
+
if (opts.store && tenantId) {
|
|
709
|
+
const node = await opts.store.findByS3Key({
|
|
710
|
+
tenantId: asTenantId(tenantId),
|
|
711
|
+
s3Key: key
|
|
712
|
+
});
|
|
713
|
+
if (!node.ok || !node.value) {
|
|
629
714
|
return Response.json(
|
|
630
|
-
{ ok: false, error: { code: "
|
|
631
|
-
{ status:
|
|
715
|
+
{ ok: false, error: { code: "NotFound", message: "Not found" } },
|
|
716
|
+
{ status: 404 }
|
|
632
717
|
);
|
|
633
718
|
}
|
|
634
|
-
fs = opts.fs.forTenant(auth.tenantId);
|
|
635
719
|
}
|
|
636
720
|
const r = await fs.adapter.createPresignedDownloadUrl({
|
|
637
721
|
key: asS3Key(key),
|
|
@@ -639,7 +723,7 @@ var createDownloadRouteHandler = (opts) => {
|
|
|
639
723
|
});
|
|
640
724
|
if (!r.ok) {
|
|
641
725
|
return Response.json(
|
|
642
|
-
{ ok: false, error: { code: r.error.code, message:
|
|
726
|
+
{ ok: false, error: { code: r.error.code, message: "Download failed" } },
|
|
643
727
|
{ status: 500 }
|
|
644
728
|
);
|
|
645
729
|
}
|
|
@@ -651,18 +735,17 @@ var createDownloadRouteHandler = (opts) => {
|
|
|
651
735
|
};
|
|
652
736
|
};
|
|
653
737
|
|
|
654
|
-
// src/storage/content-disposition.ts
|
|
655
|
-
var NEVER_INLINE = /* @__PURE__ */ new Set(["text/html", "application/xhtml+xml", "image/svg+xml"]);
|
|
656
|
-
function signedContentDisposition(requested, contentType) {
|
|
657
|
-
const mime = (contentType ?? "").split(";")[0]?.trim().toLowerCase() ?? "";
|
|
658
|
-
if (!mime || NEVER_INLINE.has(mime)) return "attachment";
|
|
659
|
-
if (mime === "application/pdf" || mime === "text/plain" || mime === "text/csv" || mime === "text/markdown" || mime.startsWith("image/") || mime.startsWith("video/") || mime.startsWith("audio/")) {
|
|
660
|
-
return "inline";
|
|
661
|
-
}
|
|
662
|
-
return "attachment";
|
|
663
|
-
}
|
|
664
|
-
|
|
665
738
|
// src/server/route-handlers/share.ts
|
|
739
|
+
var DEFAULT_MAX_SHARE_BYTES = 32 * 1024 * 1024;
|
|
740
|
+
var shareHeaders = () => {
|
|
741
|
+
const headers = new Headers();
|
|
742
|
+
headers.set("X-Content-Type-Options", "nosniff");
|
|
743
|
+
headers.set("Content-Security-Policy", "sandbox; default-src 'none'");
|
|
744
|
+
headers.set("Cross-Origin-Resource-Policy", "same-origin");
|
|
745
|
+
headers.set("Cache-Control", "private, no-store");
|
|
746
|
+
headers.set("Referrer-Policy", "no-referrer");
|
|
747
|
+
return headers;
|
|
748
|
+
};
|
|
666
749
|
var tokenFromRequest = (req) => {
|
|
667
750
|
const url = new URL(req.url);
|
|
668
751
|
const query = url.searchParams.get("token");
|
|
@@ -684,7 +767,7 @@ var createShareRouteHandler = (opts) => {
|
|
|
684
767
|
const resolved = await opts.store.resolveShare({ token });
|
|
685
768
|
if (!resolved.ok) {
|
|
686
769
|
return Response.json(
|
|
687
|
-
{ ok: false, error: { code:
|
|
770
|
+
{ ok: false, error: { code: "InternalError", message: "Share lookup failed" } },
|
|
688
771
|
{ status: 500 }
|
|
689
772
|
);
|
|
690
773
|
}
|
|
@@ -696,29 +779,54 @@ var createShareRouteHandler = (opts) => {
|
|
|
696
779
|
);
|
|
697
780
|
}
|
|
698
781
|
const adapter = opts.fs.forTenant(node.tenantId).adapter;
|
|
699
|
-
const
|
|
782
|
+
const download = new URL(req.url).searchParams.get("download") === "1";
|
|
783
|
+
const key = asS3Key(node.s3Key);
|
|
784
|
+
const st = await adapter.stat({ key });
|
|
785
|
+
if (!st.ok) {
|
|
786
|
+
return Response.json(
|
|
787
|
+
{ ok: false, error: { code: "NotFound", message: "Share not found" } },
|
|
788
|
+
{ status: 404 }
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
const maxBytes = opts.maxBytes ?? DEFAULT_MAX_SHARE_BYTES;
|
|
792
|
+
if (st.value.size > maxBytes) {
|
|
793
|
+
const signed = await adapter.createPresignedDownloadUrl({
|
|
794
|
+
key,
|
|
795
|
+
expiresIn: 60,
|
|
796
|
+
disposition: download ? "attachment" : "inline",
|
|
797
|
+
contentType: st.value.contentType || node.mimeType
|
|
798
|
+
});
|
|
799
|
+
if (!signed.ok) {
|
|
800
|
+
return Response.json(
|
|
801
|
+
{ ok: false, error: { code: "InternalError", message: "Share failed" } },
|
|
802
|
+
{ status: 500 }
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
const headers2 = shareHeaders();
|
|
806
|
+
headers2.set("Location", signed.value.url);
|
|
807
|
+
return new Response(null, { status: 302, headers: headers2 });
|
|
808
|
+
}
|
|
700
809
|
const read = await adapter.read({
|
|
701
|
-
key
|
|
702
|
-
range:
|
|
810
|
+
key,
|
|
811
|
+
range: req.headers.get("range") ?? void 0
|
|
703
812
|
});
|
|
704
813
|
if (!read.ok) {
|
|
705
814
|
return Response.json(
|
|
706
|
-
{ ok: false, error: { code:
|
|
815
|
+
{ ok: false, error: { code: "NotFound", message: "Share not found" } },
|
|
707
816
|
{ status: 404 }
|
|
708
817
|
);
|
|
709
818
|
}
|
|
710
|
-
const
|
|
711
|
-
const
|
|
712
|
-
const mime = node.mimeType || read.value.contentType || "application/octet-stream";
|
|
819
|
+
const headers = shareHeaders();
|
|
820
|
+
const mime = st.value.contentType || node.mimeType || read.value.contentType || "application/octet-stream";
|
|
713
821
|
headers.set("Content-Type", mime);
|
|
714
822
|
const disposition = download ? "attachment" : signedContentDisposition("inline", mime);
|
|
715
823
|
headers.set(
|
|
716
824
|
"Content-Disposition",
|
|
717
825
|
`${disposition}; filename*=UTF-8''${encodeURIComponent(node.name)}`
|
|
718
826
|
);
|
|
719
|
-
|
|
720
|
-
headers.set("Content-Length", String(
|
|
721
|
-
return new Response(
|
|
827
|
+
const bytes = Uint8Array.from(read.value.body);
|
|
828
|
+
headers.set("Content-Length", String(bytes.byteLength));
|
|
829
|
+
return new Response(bytes.buffer, { status: 200, headers });
|
|
722
830
|
};
|
|
723
831
|
};
|
|
724
832
|
|