forked from ericc-ch/copilot-api
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsession.ts
More file actions
137 lines (112 loc) · 4.31 KB
/
session.ts
File metadata and controls
137 lines (112 loc) · 4.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
/**
* Server-side session management backed by the SQLite `sessions` table.
*
* Sessions have an 8-hour sliding window; each authenticated request extends
* the expiry. Session IDs are 32 random bytes (256-bit entropy) encoded as
* hex strings.
*/
import crypto from "node:crypto"
import { getDb } from "~/lib/db"
import { generateCsrfToken } from "./csrf"
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export interface SessionRow {
id: string
key_id: string
csrf_token: string
created_at: number
expires_at: number
}
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
/** Session lifetime: 8 hours in milliseconds */
export const SESSION_LIFETIME_MS = 8 * 60 * 60 * 1000
export const SESSION_COOKIE = "sid"
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function newSessionId(): string {
return crypto.randomBytes(32).toString("hex")
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/** Create a new session for the given key and return the session row. */
export function createSession(keyId: string): SessionRow {
const db = getDb()
const id = newSessionId()
const now = Date.now()
const expiresAt = now + SESSION_LIFETIME_MS
const csrfToken = generateCsrfToken(id)
db.run(
`INSERT INTO sessions (id, key_id, csrf_token, created_at, expires_at)
VALUES (?, ?, ?, ?, ?)`,
[id, keyId, csrfToken, now, expiresAt],
)
return {
id,
key_id: keyId,
csrf_token: csrfToken,
created_at: now,
expires_at: expiresAt,
}
}
/** Look up an active session by id, sliding its expiry. Returns null if not found or expired. */
export function getSession(sessionId: string): SessionRow | null {
const db = getDb()
const now = Date.now()
const row = db
.query<SessionRow, [string, number]>(
`SELECT id, key_id, csrf_token, created_at, expires_at
FROM sessions WHERE id = ? AND expires_at > ?`,
)
.get(sessionId, now)
if (!row) return null
// Slide the expiry window
const newExpiry = now + SESSION_LIFETIME_MS
db.run(`UPDATE sessions SET expires_at = ? WHERE id = ?`, [
newExpiry,
sessionId,
])
return { ...row, expires_at: newExpiry }
}
/** Destroy a session (logout). */
export function deleteSession(sessionId: string): void {
getDb().run(`DELETE FROM sessions WHERE id = ?`, [sessionId])
}
/** Sweep expired sessions (called on startup / periodically). */
export function purgeExpiredSessions(): void {
getDb().run(`DELETE FROM sessions WHERE expires_at <= ?`, [Date.now()])
}
/**
* Whether session cookies should be flagged `Secure` (HTTPS-only). Defaults
* to true. When ADMIN_INSECURE_HTTP=true is set, the operator has opted into
* plain-HTTP admin access on LAN, and `Secure` must be dropped — browsers
* silently discard Secure cookies received over HTTP, which would manifest
* as a "login loop" (server sets cookies, browser drops them, next request
* has no session → redirect back to login).
*/
function cookieSecureFlag(): string {
return process.env.ADMIN_INSECURE_HTTP === "true" ? "" : "; Secure"
}
/** Build the Set-Cookie header value for the session cookie. */
export function sessionCookieValue(sessionId: string): string {
return `${SESSION_COOKIE}=${sessionId}; HttpOnly${cookieSecureFlag()}; SameSite=Strict; Path=/admin; Max-Age=${SESSION_LIFETIME_MS / 1000}`
}
/** Build a Set-Cookie value that clears the session cookie. */
export function clearSessionCookieValue(): string {
return `${SESSION_COOKIE}=; HttpOnly${cookieSecureFlag()}; SameSite=Strict; Path=/admin; Max-Age=0`
}
/** Extract the session id from the Cookie header. */
export function extractSessionId(
cookieHeader: string | undefined,
): string | undefined {
if (!cookieHeader) return undefined
for (const part of cookieHeader.split(";")) {
const [name, ...rest] = part.trim().split("=")
if (name.trim() === SESSION_COOKIE) return rest.join("=").trim()
}
return undefined
}