mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-07 08:27:12 +00:00
Replace cookie-based sessions with SQLite-backed storage using better-sqlite3 and React Router's createSessionStorage. Sessions are now stored in ~/.arc/arc-web.db with a session ID cookie, enabling larger payloads and server-side revocation. - Add db.server.ts (lazy singleton, WAL mode, web_sessions table) - Add session-storage.server.ts (CRUD ops, probabilistic cleanup) - Fetch primary verified email from /user/emails during OAuth - Add emails:read to GitHub App manifest default_permissions - Expand session data: userUrl, githubId, githubNodeId, email - Default ARC_API_BASE_URL to localhost:3000 - Whitelist better-sqlite3 in trustedDependencies - Externalize better-sqlite3 from Vite SSR bundling Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
78 lines
2.3 KiB
TypeScript
78 lines
2.3 KiB
TypeScript
import { createSessionStorage } from "react-router";
|
|
import crypto from "node:crypto";
|
|
import { getDatabase } from "./db.server";
|
|
|
|
interface SessionRow {
|
|
id: string;
|
|
user_url: string;
|
|
data: string;
|
|
expires_at: number | null;
|
|
}
|
|
|
|
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
|
|
|
|
function cleanupExpiredSessions() {
|
|
const db = getDatabase();
|
|
db.prepare("DELETE FROM web_sessions WHERE expires_at IS NOT NULL AND expires_at < ?").run(
|
|
Date.now(),
|
|
);
|
|
}
|
|
|
|
export function createSqliteSessionStorage(secret: string) {
|
|
return createSessionStorage({
|
|
cookie: {
|
|
name: "__arc_session",
|
|
httpOnly: true,
|
|
sameSite: "lax" as const,
|
|
secure: process.env.NODE_ENV === "production",
|
|
secrets: [secret],
|
|
path: "/",
|
|
maxAge: 30 * 24 * 60 * 60, // 30 days in seconds
|
|
},
|
|
async createData(data, expiresAt) {
|
|
const db = getDatabase();
|
|
const id = crypto.randomUUID();
|
|
const userUrl = (data.userUrl as string) ?? "";
|
|
const expiresAtMs = expiresAt ? expiresAt.getTime() : Date.now() + THIRTY_DAYS_MS;
|
|
|
|
db.prepare(
|
|
"INSERT INTO web_sessions (id, user_url, data, expires_at) VALUES (?, ?, ?, ?)",
|
|
).run(id, userUrl, JSON.stringify(data), expiresAtMs);
|
|
|
|
// Probabilistic cleanup (~1% of creates)
|
|
if (Math.random() < 0.01) {
|
|
cleanupExpiredSessions();
|
|
}
|
|
|
|
return id;
|
|
},
|
|
async readData(id) {
|
|
const db = getDatabase();
|
|
const row = db
|
|
.prepare("SELECT * FROM web_sessions WHERE id = ?")
|
|
.get(id) as SessionRow | undefined;
|
|
|
|
if (!row) return null;
|
|
|
|
if (row.expires_at && row.expires_at < Date.now()) {
|
|
db.prepare("DELETE FROM web_sessions WHERE id = ?").run(id);
|
|
return null;
|
|
}
|
|
|
|
return JSON.parse(row.data);
|
|
},
|
|
async updateData(id, data, expiresAt) {
|
|
const db = getDatabase();
|
|
const userUrl = (data.userUrl as string) ?? "";
|
|
const expiresAtMs = expiresAt ? expiresAt.getTime() : Date.now() + THIRTY_DAYS_MS;
|
|
|
|
db.prepare(
|
|
"UPDATE web_sessions SET user_url = ?, data = ?, expires_at = ? WHERE id = ?",
|
|
).run(userUrl, JSON.stringify(data), expiresAtMs, id);
|
|
},
|
|
async deleteData(id) {
|
|
const db = getDatabase();
|
|
db.prepare("DELETE FROM web_sessions WHERE id = ?").run(id);
|
|
},
|
|
});
|
|
}
|