mirror your GitHub repos to tangled.org automatically
1import type { H3Event, SessionConfig } from 'h3'
2
3/**
4 * Cookie-backed session for the dashboard. The OAuth callback writes the
5 * session after a successful AT Proto login; `requireSession()` reads it on
6 * every dashboard / `/api/me/*` / `/api/repos/*` request.
7 *
8 * Iron-session-style sealed cookie via h3's built-in `useSession`. The
9 * `password` comes from `NUXT_SESSION_PASSWORD` (32+ chars). We read
10 * `process.env` directly so the helper is callable outside a request context
11 * for tests, mirroring `encryption.ts`.
12 *
13 * v1 stores `installationId` alongside `did`: a user may have installed the
14 * GitHub App on more than one account (personal + an org), but the session
15 * pins us to the one they last authenticated against. See the dashboard
16 * "Connect a different installation?" link for the workaround.
17 */
18export interface SynchubSessionData {
19 did: string
20 installationId: number
21 handle?: string
22}
23
24const COOKIE_NAME = 'synchub-session'
25const COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 30 // 30 days
26
27function sessionPassword(): string {
28 const raw = process.env.NUXT_SESSION_PASSWORD
29 if (!raw || raw.length < 32) {
30 throw new Error('NUXT_SESSION_PASSWORD is not set (need 32+ characters of entropy)')
31 }
32 return raw
33}
34
35export function sessionConfig(): SessionConfig {
36 return {
37 name: COOKIE_NAME,
38 password: sessionPassword(),
39 maxAge: COOKIE_MAX_AGE_SECONDS,
40 cookie: {
41 httpOnly: true,
42 sameSite: 'lax',
43 secure: !(process.env.NUXT_PUBLIC_URL?.startsWith('http://127.0.0.1')
44 || process.env.NUXT_PUBLIC_URL?.startsWith('http://localhost')),
45 path: '/',
46 },
47 }
48}
49
50export async function getSessionData(event: H3Event): Promise<SynchubSessionData | null> {
51 const session = await useSession<SynchubSessionData>(event, sessionConfig())
52 const { did, installationId } = session.data
53 if (typeof did !== 'string' || typeof installationId !== 'number') return null
54 return { did, installationId, handle: session.data.handle }
55}
56
57/**
58 * Return the current session or throw a 401. Use from any handler that needs
59 * an authenticated user. The thrown error is consumed by Nitro and rendered
60 * as `{ statusCode: 401, statusMessage: 'unauthenticated' }`.
61 */
62export async function requireSession(event: H3Event): Promise<SynchubSessionData> {
63 const data = await getSessionData(event)
64 if (!data) {
65 throw createError({ statusCode: 401, statusMessage: 'unauthenticated' })
66 }
67 return data
68}
69
70export async function writeSession(event: H3Event, data: SynchubSessionData): Promise<void> {
71 const session = await useSession<SynchubSessionData>(event, sessionConfig())
72 await session.update(data)
73}