/** * 会话 cookie 的读写(ADR-010)—— 只此一处拼 `Set-Cookie`,属性不许各写各的。 * * `HttpOnly`:脚本读不到 token,XSS 拿不走会话。 * `SameSite=Lax`:跨站表单 POST 不带 cookie,挡掉 CSRF 的常见形态; * 同站导航仍带,登录后跳转不受影响。 * 开发期走 http://localhost,故 `Secure` 随部署环境开关(生产必开)。 */ import type { Context } from 'hono'; import type { AppEnv } from './http-env.ts'; export const SESSION_COOKIE = 'jcai_session'; /** 生产环境必带 Secure;开发期 localhost 是 http,带了浏览器就不存 */ function secureAttr(): string { return process.env['NODE_ENV'] === 'production' ? '; Secure' : ''; } export function readSessionToken(cookieHeader: string | undefined): string | undefined { if (cookieHeader === undefined) return undefined; const match = new RegExp(`(?:^|;\\s*)${SESSION_COOKIE}=([^;]+)`).exec(cookieHeader); return match?.[1]; } export function setSessionCookie(c: Context, token: string, expiresAt: Date): void { c.header( 'Set-Cookie', `${SESSION_COOKIE}=${token}; Path=/; HttpOnly; SameSite=Lax${secureAttr()}; Expires=${expiresAt.toUTCString()}`, { append: true }, ); } export function clearSessionCookie(c: Context): void { c.header( 'Set-Cookie', `${SESSION_COOKIE}=; Path=/; HttpOnly; SameSite=Lax${secureAttr()}; Max-Age=0`, { append: true }, ); }