@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.
@@ -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-Djiiu6YU.js';
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-OZNiZGga.js';
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
- }, "strip", z.ZodTypeAny, {
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
- }, "strip", z.ZodTypeAny, {
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
- }, "strip", z.ZodTypeAny, {
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
- }, "strip", z.ZodTypeAny, {
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
- }, "strip", z.ZodTypeAny, {
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
- }, "strip", z.ZodTypeAny, {
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
- }, "strip", z.ZodTypeAny, {
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
- }, "strip", z.ZodTypeAny, {
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
- }, "strip", z.ZodTypeAny, {
124
- query: string;
125
- parentId?: string | undefined;
126
- limit?: number | undefined;
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
- }, "strip", z.ZodTypeAny, {
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
- }, "strip", z.ZodTypeAny, {
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
- }, "strip", z.ZodTypeAny, {
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
- }, "strip", z.ZodTypeAny, {
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. 401 when it returns null. */
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
 
@@ -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: `${message}: ${e instanceof Error ? e.message : String(e)}`,
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: input.limit,
398
- cursor: input.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 matchesContentType = (pattern, actual) => {
505
- if (pattern === actual) return true;
506
- if (pattern.endsWith("/*")) {
507
- const prefix = pattern.slice(0, -1);
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 body;
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
- body = await req.json();
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 ${body.contentLength} exceeds maxBytes ${opts.maxBytes}`
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
- const allowed = opts.allowedContentTypes.some((p) => matchesContentType(p, body.contentType));
549
- if (!allowed) {
550
- return Response.json(
551
- {
552
- ok: false,
553
- error: {
554
- code: "UnsupportedMediaType",
555
- message: `contentType "${body.contentType}" is not in the allowed list`
556
- }
557
- },
558
- { status: 415 }
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: r.error.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
- let fs = opts.fs;
626
- if (opts.getAuth) {
627
- const auth = await opts.getAuth(req);
628
- if (!auth) {
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: "Unauthorized", message: "Not authenticated" } },
631
- { status: 401 }
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: r.error.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: resolved.error.code, message: resolved.error.message } },
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 rangeHeader = req.headers.get("range") ?? void 0;
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: asS3Key(node.s3Key),
702
- range: rangeHeader ?? void 0
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: read.error.code, message: read.error.message } },
815
+ { ok: false, error: { code: "NotFound", message: "Share not found" } },
707
816
  { status: 404 }
708
817
  );
709
818
  }
710
- const download = new URL(req.url).searchParams.get("download") === "1";
711
- const headers = new Headers();
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
- headers.set("Cache-Control", "private, no-store");
720
- headers.set("Content-Length", String(read.value.body.byteLength));
721
- return new Response(Buffer.from(read.value.body), { status: 200, headers });
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