mirror your GitHub repos to tangled.org automatically
1import type { H3Event } from 'h3'
2import { sessionConfig } from './server-session'
3
4/**
5 * Short-lived sealed cookie proving the current browser completed GitHub
6 * user-OAuth and was confirmed as an administrator of a specific installation.
7 *
8 * The atproto callback checks this before binding a DID to an installation, so
9 * a connecting user can't claim an installation they don't actually control by
10 * crafting `/connect?installation_id=<victim>`. Reuses the session password
11 * for sealing; a distinct cookie name and a 15-minute TTL keep it scoped to a
12 * single connect flow.
13 */
14interface InstallOwnershipData {
15 installationId: number
16 verifiedAt: number
17}
18
19const COOKIE_NAME = 'synchub-install-ownership'
20const TTL_SECONDS = 15 * 60
21
22function ownershipConfig() {
23 return { ...sessionConfig(), name: COOKIE_NAME, maxAge: TTL_SECONDS }
24}
25
26export async function markInstallOwned(event: H3Event, installationId: number): Promise<void> {
27 const session = await useSession<InstallOwnershipData>(event, ownershipConfig())
28 await session.update({ installationId, verifiedAt: Date.now() })
29}
30
31/**
32 * Return true if the current browser proved ownership of `installationId`
33 * within the TTL. Does not clear the cookie; the caller clears it after a
34 * successful bind so it can't be replayed.
35 */
36export async function hasVerifiedInstall(event: H3Event, installationId: number): Promise<boolean> {
37 const session = await useSession<InstallOwnershipData>(event, ownershipConfig())
38 const { installationId: owned, verifiedAt } = session.data
39 if (typeof owned !== 'number' || typeof verifiedAt !== 'number') return false
40 if (owned !== installationId) return false
41 if (Date.now() - verifiedAt > TTL_SECONDS * 1000) return false
42 return true
43}
44
45export async function clearInstallOwnership(event: H3Event): Promise<void> {
46 const session = await useSession<InstallOwnershipData>(event, ownershipConfig())
47 await session.clear()
48}