1export const MAX_BYTES = 10 * 1024 * 1024;
2export const UUID =
3 /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
4
5export interface UploadInput {
6 filename: string;
7 contentType: string;
8 size: number;
9}
10
11export interface UploadRow {
12 id: string;
13 filename: string;
14 object_key: string;
15 content_type: string;
16 expected_bytes: number | string;
17 actual_bytes: number | string | null;
18 status: "awaiting_upload" | "ready" | "rejected";
19 created_at: string;
20}
21
22// Effect Postgres decodes int8 as bigint and timestamps as epoch milliseconds.
23export interface UploadRecord extends Omit<
24 UploadRow,
25 "expected_bytes" | "actual_bytes" | "created_at"
26> {
27 expected_bytes: bigint;
28 actual_bytes: bigint | null;
29 created_at: number;
30}
31
32export function serializeUploadRow(row: UploadRecord): UploadRow {
33 return {
34 ...row,
35 expected_bytes: row.expected_bytes.toString(),
36 actual_bytes: row.actual_bytes?.toString() ?? null,
37 created_at: new Date(row.created_at).toISOString(),
38 };
39}
40
41export function parseUpload(value: unknown): UploadInput | undefined {
42 if (typeof value !== "object" || value === null) return;
43 const { filename, contentType, size } = value as Record<string, unknown>;
44 if (
45 typeof filename !== "string" ||
46 !filename.trim() ||
47 filename.length > 200 ||
48 /[\u0000-\u001f\u007f]/.test(filename)
49 )
50 return;
51 if (
52 typeof contentType !== "string" ||
53 !/^[\w.+-]+\/[\w.+-]+$/.test(contentType)
54 )
55 return;
56 if (
57 typeof size !== "number" ||
58 !Number.isSafeInteger(size) ||
59 size < 1 ||
60 size > MAX_BYTES
61 )
62 return;
63 return { filename, contentType, size };
64}
65
66export function objectKey(owner: string, id: string) {
67 return `incoming/${encodeURIComponent(owner)}/${id}`;
68}
69
70export function corsHeaders(
71 origin: string | null,
72 allowed: string | undefined,
73): Record<string, string> | undefined {
74 if (origin && allowed !== "*") {
75 if (!allowed) return;
76 try {
77 if (origin !== new URL(allowed).origin) return;
78 } catch {
79 return;
80 }
81 }
82 return {
83 ...(origin
84 ? { "access-control-allow-origin": allowed === "*" ? "*" : origin }
85 : {}),
86 "access-control-allow-methods": "GET, POST, OPTIONS",
87 "access-control-allow-headers": "authorization, content-type",
88 "cache-control": "no-store",
89 vary: "Origin",
90 };
91}