Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
40 lines
1.4 KiB
TypeScript
40 lines
1.4 KiB
TypeScript
/**
|
|
* 会话 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<AppEnv>, 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<AppEnv>): void {
|
|
c.header(
|
|
'Set-Cookie',
|
|
`${SESSION_COOKIE}=; Path=/; HttpOnly; SameSite=Lax${secureAttr()}; Max-Age=0`,
|
|
{ append: true },
|
|
);
|
|
}
|