Initial commit

This commit is contained in:
Zoe
2026-08-07 22:13:25 -05:00
commit 8006fd67f3
40 changed files with 11017 additions and 0 deletions

104
server/utils/auth.ts Normal file
View File

@@ -0,0 +1,104 @@
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
import type { H3Event } from "h3";
import {
createError,
deleteCookie,
getCookie,
getRequestHeader,
setCookie,
} from "h3";
const COOKIE = "quickdrop_auth";
const DAY = 86400000;
function config() {
const runtime = useRuntimeConfig();
const configuredTtl = Number(
process.env.AUTH_TOKEN_TTL_DAYS || runtime.authTokenTtlDays || 7,
);
const ttlDays =
Number.isFinite(configuredTtl) && configuredTtl > 0 ? configuredTtl : 7;
return {
passcode: String(process.env.AUTH_PASSCODE || runtime.authPasscode || ""),
secret: String(process.env.AUTH_SECRET || runtime.authSecret || ""),
ttl: ttlDays * DAY,
};
}
function mac(value: string, secret: string) {
return createHmac("sha256", secret).update(value).digest("base64url");
}
export function authConfigured() {
const c = config();
return Boolean(c.passcode && c.secret);
}
export function issueSession(event: H3Event) {
const c = config();
if (!c.secret) throw new Error("Authentication is not configured.");
const payload = `${Date.now() + c.ttl}.${randomBytes(24).toString("base64url")}`;
setCookie(event, COOKIE, `${payload}.${mac(payload, c.secret)}`, {
httpOnly: true,
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
path: "/",
maxAge: Math.floor(c.ttl / 1000),
});
}
export function clearAuthCookie(event: H3Event) {
deleteCookie(event, COOKIE, { path: "/" });
}
export function isAuthenticated(event: H3Event): boolean {
const c = config();
const token = getCookie(event, COOKIE);
if (!token || !c.secret) return false;
const split = token.lastIndexOf(".");
if (split < 1) return false;
const payload = token.slice(0, split);
const supplied = Buffer.from(token.slice(split + 1));
const expected = Buffer.from(mac(payload, c.secret));
if (
supplied.length !== expected.length ||
!timingSafeEqual(supplied, expected)
)
return false;
const expiry = Number(payload.slice(0, payload.indexOf(".")));
return Number.isSafeInteger(expiry) && expiry > Date.now();
}
export function requireAuth(event: H3Event) {
if (!isAuthenticated(event))
throw createError({
statusCode: 401,
statusMessage: "Authentication required.",
});
requireSameOrigin(event);
}
export function verifyPasscode(value: unknown): boolean {
const c = config();
if (!c.passcode || typeof value !== "string") return false;
const supplied = Buffer.from(value);
const expected = Buffer.from(c.passcode);
return (
supplied.length === expected.length && timingSafeEqual(supplied, expected)
);
}
export function requireSameOrigin(event: H3Event) {
const origin = getRequestHeader(event, "origin");
const site = getRequestHeader(event, "sec-fetch-site");
if (
site &&
["cross-site", "same-site"].includes(site) &&
site === "cross-site"
)
throw createError({
statusCode: 403,
statusMessage: "Cross-site state change rejected.",
});
if (origin) {
const host = getRequestHeader(event, "host");
const proto = getRequestHeader(event, "x-forwarded-proto") || "http";
try {
if (new URL(origin).host !== host || !["http", "https"].includes(proto))
throw new Error();
} catch {
throw createError({ statusCode: 403, statusMessage: "Origin rejected." });
}
}
}
export { COOKIE };

580
server/utils/drop-store.ts Normal file
View File

@@ -0,0 +1,580 @@
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
import {
mkdir,
readFile,
readdir,
rename,
rm,
stat,
writeFile,
open as openFile,
} from "node:fs/promises";
import { resolve } from "node:path";
import {
ROOM_ID_PATTERN,
expiresInMs,
validateReservation,
validateRoomId,
} from "../../shared/utils/drop-contract";
import {
CHUNK_SIZE,
LEASE_IDLE_MS,
UPLOAD_RESERVATION_MS,
type PublicDescriptor,
type ReserveRequest,
type StoredDrop,
type StoredLease,
} from "../../shared/types/drop";
const ALPHABET = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
const productionRoot = resolve(
process.env.QUICKDROP_STORAGE_DIR || ".data/quickdrop",
);
export class DropStoreError extends Error {
constructor(
public code:
| "not-found"
| "expired"
| "conflict"
| "incomplete"
| "exhausted"
| "forbidden",
message?: string,
) {
super(message || code);
}
}
interface LockEntry {
tail: Promise<void>;
waiters: number;
}
const locks = new Map<string, LockEntry>();
async function withLock<T>(
key: string,
operation: () => Promise<T>,
): Promise<T> {
let entry = locks.get(key);
if (!entry) {
entry = { tail: Promise.resolve(), waiters: 0 };
locks.set(key, entry);
}
entry.waiters++;
const previous = entry.tail;
let release!: () => void;
entry.tail = new Promise<void>((resolve) => {
release = resolve;
});
await previous;
try {
return await operation();
} finally {
release();
entry.waiters--;
if (entry.waiters === 0 && locks.get(key) === entry) locks.delete(key);
}
}
function hash(value: string): string {
return createHash("sha256").update(value).digest("hex");
}
function sameHash(left: string, right: string): boolean {
const a = Buffer.from(left, "hex");
const b = Buffer.from(right, "hex");
return a.length === b.length && timingSafeEqual(a, b);
}
function bearer(): string {
return randomBytes(32).toString("base64url");
}
function nowValue(clock: (() => number) | { now(): number }): number {
return typeof clock === "function" ? clock() : clock.now();
}
function ensureId(id: string) {
try {
validateRoomId(id);
} catch {
throw new DropStoreError("not-found", "Drop not found.");
}
return id;
}
export interface DropStoreOptions {
root?: string;
clock?: (() => number) | { now(): number };
}
const TEMP_FILE_GRACE_MS = 60 * 60 * 1000;
const METADATALESS_GRACE_MS = 60 * 60 * 1000;
export function pathsFor(id: string, root = productionRoot) {
ensureId(id);
const directory = resolve(root, id);
return {
directory,
metadata: resolve(directory, "metadata.json"),
manifest: resolve(directory, "manifest.bin"),
chunks: resolve(directory, "chunks"),
};
}
async function atomicWrite(path: string, data: Uint8Array | string) {
const temp = `${path}.tmp-${process.pid}-${randomBytes(6).toString("hex")}`;
await writeFile(temp, data, { mode: 0o600 });
try {
const handle = await openFile(temp, "r");
await handle.sync();
await handle.close();
} catch {
/* fsync is best effort on unusual filesystems */
}
await rename(temp, path);
}
function chunkPath(paths: ReturnType<typeof pathsFor>, index: number) {
return resolve(paths.chunks, `${String(index).padStart(10, "0")}.bin`);
}
export class DropStore {
readonly root: string;
readonly clock: (() => number) | { now(): number };
constructor(options: DropStoreOptions = {}) {
this.root = resolve(options.root || productionRoot);
this.clock = options.clock || (() => Date.now());
}
async ensure() {
await mkdir(this.root, { recursive: true, mode: 0o700 });
}
async reserve(
input: ReserveRequest,
): Promise<{ id: string; uploadExpiresAt: number; access: string }> {
validateReservation(input);
await this.ensure();
for (let attempt = 0; attempt < 100; attempt++) {
const id = Array.from(
randomBytes(32),
(byte) => ALPHABET[byte % ALPHABET.length],
).join("");
const paths = pathsFor(id, this.root);
try {
await mkdir(paths.directory, { recursive: false, mode: 0o700 });
await mkdir(paths.chunks, { mode: 0o700 });
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "EEXIST") continue;
throw error;
}
const createdAt = nowValue(this.clock);
const access = bearer();
const metadata: StoredDrop = {
version: 2,
id,
state: "uploading",
createdAt,
uploadExpiresAt: createdAt + UPLOAD_RESERVATION_MS,
expiresInMs: expiresInMs(input.expiresIn),
expiresAt: null,
maxReads: input.maxReads,
readsClaimed: 0,
chunkSize: input.chunkSize,
chunkCount: input.chunkCount,
noncePrefix: input.noncePrefix,
leases: {},
accessHash: hash(access),
};
try {
await atomicWrite(paths.metadata, JSON.stringify(metadata));
return { id, uploadExpiresAt: metadata.uploadExpiresAt, access };
} catch (error) {
await rm(paths.directory, { recursive: true, force: true });
throw error;
}
}
throw new Error("Could not allocate a drop id.");
}
async readMetadata(id: string): Promise<StoredDrop | null> {
const paths = pathsFor(id, this.root);
try {
return JSON.parse(await readFile(paths.metadata, "utf8")) as StoredDrop;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
throw error;
}
}
private async metadataLocked(id: string): Promise<StoredDrop> {
const metadata = await this.readMetadata(id);
if (!metadata || metadata.version !== 2 || metadata.id !== id)
throw new DropStoreError("not-found", "Drop not found.");
const now = nowValue(this.clock);
if (
(metadata.state === "uploading" && metadata.uploadExpiresAt <= now) ||
(metadata.state === "ready" &&
metadata.expiresAt !== null &&
metadata.expiresAt <= now)
) {
await rm(pathsFor(id, this.root).directory, {
recursive: true,
force: true,
});
throw new DropStoreError("expired", "Drop expired.");
}
return metadata;
}
private async save(id: string, metadata: StoredDrop) {
await atomicWrite(
pathsFor(id, this.root).metadata,
JSON.stringify(metadata),
);
}
async putChunk(
id: string,
index: number,
body: Uint8Array,
): Promise<"created" | "same"> {
ensureId(id);
return withLock(`${this.root}\0${id}`, async () => {
const metadata = await this.metadataLocked(id);
if (metadata.state !== "uploading")
throw new DropStoreError("conflict", "Drop is already finalized.");
if (
!Number.isSafeInteger(index) ||
index < 0 ||
index >= metadata.chunkCount
)
throw new DropStoreError("conflict", "Invalid chunk index.");
const paths = pathsFor(id, this.root);
const target = chunkPath(paths, index);
const expectedFull = metadata.chunkSize + 16;
if (
body.byteLength < 16 ||
body.byteLength > expectedFull ||
(index < metadata.chunkCount - 1 && body.byteLength !== expectedFull)
)
throw new DropStoreError("conflict", "Invalid chunk length.");
try {
const existing = await readFile(target);
if (
existing.byteLength === body.byteLength &&
sameHash(
hash(existing.toString("base64")),
hash(Buffer.from(body).toString("base64")),
)
)
return "same";
throw new DropStoreError(
"conflict",
"Chunk already exists with different contents.",
);
} catch (error) {
if (error instanceof DropStoreError) throw error;
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
}
const temp = resolve(
paths.directory,
`.chunk-${index}-${randomBytes(5).toString("hex")}`,
);
await writeFile(temp, body, { mode: 0o600 });
try {
const handle = await openFile(temp, "r");
await handle.sync();
await handle.close();
} catch {}
await rename(temp, target);
return "created";
});
}
async finalize(id: string, manifest: Uint8Array): Promise<StoredDrop> {
ensureId(id);
if (manifest.byteLength <= 16)
throw new DropStoreError("conflict", "Invalid manifest.");
return withLock(`${this.root}\0${id}`, async () => {
const metadata = await this.metadataLocked(id);
const paths = pathsFor(id, this.root);
if (metadata.state === "ready") {
const current = await readFile(paths.manifest);
if (Buffer.compare(current, Buffer.from(manifest)) !== 0)
throw new DropStoreError(
"conflict",
"Manifest conflicts with finalized drop.",
);
return metadata;
}
for (let index = 0; index < metadata.chunkCount; index++) {
let info;
try {
info = await stat(chunkPath(paths, index));
} catch {
throw new DropStoreError("incomplete", "Some chunks are missing.");
}
if (
info.size < 16 ||
(index < metadata.chunkCount - 1 &&
info.size !== metadata.chunkSize + 16)
)
throw new DropStoreError("incomplete", "Chunk geometry is invalid.");
}
let previous: Buffer | null = null;
try {
previous = await readFile(paths.manifest);
} catch {}
if (previous && Buffer.compare(previous, Buffer.from(manifest)) !== 0)
throw new DropStoreError(
"conflict",
"Manifest conflicts with staged data.",
);
if (!previous) await atomicWrite(paths.manifest, manifest);
metadata.state = "ready";
metadata.expiresAt = nowValue(this.clock) + metadata.expiresInMs;
metadata.manifestBytes = manifest.byteLength;
metadata.chunkCipherBytes = [];
for (let index = 0; index < metadata.chunkCount; index++)
metadata.chunkCipherBytes.push(
(await stat(chunkPath(paths, index))).size,
);
await this.save(id, metadata);
return metadata;
});
}
async descriptor(id: string, access: string): Promise<PublicDescriptor> {
ensureId(id);
return withLock(`${this.root}\0${id}`, async () => {
const metadata = await this.metadataLocked(id);
if (metadata.state !== "ready")
throw new DropStoreError("not-found", "Drop is not ready.");
if (!sameHash(hash(access), metadata.accessHash))
throw new DropStoreError("forbidden", "Invalid access capability.");
const manifest = await readFile(pathsFor(id, this.root).manifest);
return {
version: 2,
id,
chunkSize: metadata.chunkSize,
chunkCount: metadata.chunkCount,
noncePrefix: metadata.noncePrefix,
chunkCipherBytes: metadata.chunkCipherBytes || [],
manifest: manifest.toString("base64url"),
expiresAt: new Date(metadata.expiresAt!).toISOString(),
canClaim:
metadata.maxReads === null ||
metadata.readsClaimed < metadata.maxReads,
maxReads: metadata.maxReads,
};
});
}
async claim(
id: string,
access: string,
existing?: string,
): Promise<{
token: string;
leaseExpiresAt: number;
expiresAt: number;
reused: boolean;
}> {
ensureId(id);
return withLock(`${this.root}\0${id}`, async () => {
const metadata = await this.metadataLocked(id);
if (metadata.state !== "ready")
throw new DropStoreError("not-found", "Drop is not ready.");
if (!sameHash(hash(access), metadata.accessHash))
throw new DropStoreError("forbidden", "Invalid access capability.");
const now = nowValue(this.clock);
for (const [tokenHash, lease] of Object.entries(metadata.leases)) {
if (lease.expiresAt <= now) delete metadata.leases[tokenHash];
}
if (existing && metadata.leases[hash(existing)]) {
const lease = metadata.leases[hash(existing)]!;
lease.lastSeenAt = now;
lease.expiresAt = Math.min(now + LEASE_IDLE_MS, metadata.expiresAt!);
await this.save(id, metadata);
return {
token: existing,
leaseExpiresAt: lease.expiresAt,
expiresAt: metadata.expiresAt!,
reused: true,
};
}
if (
metadata.maxReads !== null &&
metadata.readsClaimed >= metadata.maxReads
) {
await this.save(id, metadata);
throw new DropStoreError("exhausted", "No new reads remain.");
}
const token = bearer();
const lease: StoredLease = {
createdAt: now,
lastSeenAt: now,
expiresAt: Math.min(now + LEASE_IDLE_MS, metadata.expiresAt!),
};
metadata.leases[hash(token)] = lease;
metadata.readsClaimed++;
await this.save(id, metadata);
return {
token,
leaseExpiresAt: lease.expiresAt,
expiresAt: metadata.expiresAt!,
reused: false,
};
});
}
async readChunk(
id: string,
index: number,
token: string,
): Promise<{ handle: Awaited<ReturnType<typeof openFile>>; size: number }> {
ensureId(id);
return withLock(`${this.root}\0${id}`, async () => {
const metadata = await this.metadataLocked(id);
const tokenHash = hash(token);
if (metadata.state !== "ready" || !metadata.leases[tokenHash])
throw new DropStoreError("forbidden", "Invalid lease.");
const lease = metadata.leases[tokenHash]!;
const now = nowValue(this.clock);
if (lease.expiresAt <= now) {
delete metadata.leases[hash(token)];
await this.save(id, metadata);
throw new DropStoreError("forbidden", "Lease expired.");
}
if (
!Number.isSafeInteger(index) ||
index < 0 ||
index >= metadata.chunkCount
)
throw new DropStoreError("not-found", "Chunk not found.");
lease.lastSeenAt = now;
lease.expiresAt = Math.min(now + LEASE_IDLE_MS, metadata.expiresAt!);
await this.save(id, metadata);
const handle = await openFile(
chunkPath(pathsFor(id, this.root), index),
"r",
);
try {
const info = await handle.stat();
return { handle, size: info.size };
} catch (error) {
await handle.close().catch(() => undefined);
throw error;
}
});
}
async complete(id: string, token: string): Promise<void> {
ensureId(id);
return withLock(`${this.root}\0${id}`, async () => {
const existing = await this.readMetadata(id);
if (!existing) return;
const now = nowValue(this.clock);
if (existing.expiresAt !== null && existing.expiresAt <= now) {
await rm(pathsFor(id, this.root).directory, {
recursive: true,
force: true,
});
return;
}
const metadata = await this.metadataLocked(id);
const key = hash(token);
if (!metadata.leases[key])
throw new DropStoreError("forbidden", "Invalid lease.");
delete metadata.leases[key];
if (
metadata.maxReads !== null &&
metadata.readsClaimed >= metadata.maxReads &&
Object.keys(metadata.leases).length === 0
)
await rm(pathsFor(id, this.root).directory, {
recursive: true,
force: true,
});
else await this.save(id, metadata);
});
}
async delete(id: string) {
ensureId(id);
await withLock(`${this.root}\0${id}`, () =>
rm(pathsFor(id, this.root).directory, { recursive: true, force: true }),
);
}
async cleanup() {
await this.ensure();
const entries = await readdir(this.root, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory() || !ROOM_ID_PATTERN.test(entry.name)) continue;
await withLock(`${this.root}\0${entry.name}`, async () => {
try {
const dropDirectory = pathsFor(entry.name, this.root).directory;
const now = nowValue(this.clock);
for (const temporary of await readdir(dropDirectory)) {
if (
!temporary.startsWith(".chunk-") &&
!temporary.includes(".tmp-")
)
continue;
const temporaryPath = resolve(dropDirectory, temporary);
const temporaryInfo = await stat(temporaryPath).catch(() => null);
if (
temporaryInfo &&
now - temporaryInfo.mtimeMs >= TEMP_FILE_GRACE_MS
)
await rm(temporaryPath, { force: true });
}
const metadata = await this.readMetadata(entry.name);
if (!metadata) {
const directoryInfo = await stat(dropDirectory);
if (now - directoryInfo.mtimeMs >= METADATALESS_GRACE_MS)
await rm(dropDirectory, { recursive: true, force: true });
return;
}
if (
(metadata.state === "uploading" &&
metadata.uploadExpiresAt <= now) ||
(metadata.state === "ready" &&
metadata.expiresAt !== null &&
metadata.expiresAt <= now)
) {
await rm(dropDirectory, { recursive: true, force: true });
return;
}
let changed = false;
for (const [key, lease] of Object.entries(metadata.leases))
if (
lease.expiresAt <= now ||
now - lease.lastSeenAt >= LEASE_IDLE_MS
) {
delete metadata.leases[key];
changed = true;
}
if (
metadata.maxReads !== null &&
metadata.readsClaimed >= metadata.maxReads &&
Object.keys(metadata.leases).length === 0
) {
await rm(dropDirectory, { recursive: true, force: true });
return;
}
if (changed) await this.save(entry.name, metadata);
} catch (error) {
console.error(
`[quickdrop] ignoring malformed drop ${entry.name}`,
error,
);
await rm(resolve(this.root, entry.name), {
recursive: true,
force: true,
});
}
});
}
}
}
export const dropStore = new DropStore();
export async function ensureStore() {
return dropStore.ensure();
}
export async function cleanupExpiredDrops() {
return dropStore.cleanup();
}
export async function deleteDrop(id: string) {
return dropStore.delete(id);
}
export async function createRoomId() {
return (
await dropStore.reserve({
version: 2,
chunkSize: CHUNK_SIZE,
chunkCount: 1,
noncePrefix: randomBytes(8).toString("base64url"),
expiresIn: "24h",
maxReads: 1,
})
).id;
}

55
server/utils/http.ts Normal file
View File

@@ -0,0 +1,55 @@
import type { H3Event } from "h3";
import { createError, getRequestHeader } from "h3";
export async function readBounded(
event: H3Event,
max: number,
): Promise<Buffer> {
const declared = getRequestHeader(event, "content-length");
if (declared && (!/^\d+$/u.test(declared) || Number(declared) > max))
throw createError({
statusCode: 413,
statusMessage: "Request body exceeds the limit.",
});
const chunks: Buffer[] = [];
let total = 0;
for await (const value of event.node.req) {
const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
total += chunk.byteLength;
if (total > max)
throw createError({
statusCode: 413,
statusMessage: "Request body exceeds the limit.",
});
chunks.push(chunk);
}
return Buffer.concat(chunks, total);
}
export async function readJson<T>(event: H3Event, max: number): Promise<T> {
const body = await readBounded(event, max);
try {
return JSON.parse(body.toString("utf8")) as T;
} catch {
throw createError({ statusCode: 400, statusMessage: "Malformed JSON." });
}
}
export function requireContentType(event: H3Event, expected: string) {
const type = (
(getRequestHeader(event, "content-type") || "").split(";", 1)[0] || ""
)
.trim()
.toLowerCase();
if (type !== expected)
throw createError({
statusCode: 415,
statusMessage: `Content-Type must be ${expected}.`,
});
}
export function bearerToken(event: H3Event): string | undefined {
const value = getRequestHeader(event, "authorization") || "";
return /^Bearer ([A-Za-z0-9_-]{20,})$/u.exec(value)?.[1];
}
export function publicHeaders(event: H3Event) {
event.node.res.setHeader("cache-control", "no-store");
event.node.res.setHeader("referrer-policy", "no-referrer");
event.node.res.setHeader("x-content-type-options", "nosniff");
}