import { SignJWT, jwtVerify, type JWTPayload } from "jose"; import { JWT_EXPIRY_SECONDS } from "@/lib/constants/auth"; import { unauthorized } from "@/modules/auth/errors"; export const EMBED_DEMO_ROLE = "embed_demo" as const; export const EMBED_DEMO_COOKIE = "embed_demo_session"; export const EMBED_DEMO_DEFAULT_PASSWORD = "123456"; /** * 嵌入 Cookie 选项:生产环境 iframe(跨站宿主如 CC)须 SameSite=None + Secure, * 否则第三方 iframe 内登录态无法写入/回传。 */ export function embedDemoCookieOptions(maxAgeSeconds?: number): { httpOnly: boolean; secure: boolean; sameSite: "lax" | "none"; path: string; maxAge?: number; } { const isProd = process.env.NODE_ENV === "production"; const forceNone = process.env.EMBED_COOKIE_SAMESITE?.trim().toLowerCase() === "none"; const useNone = isProd || forceNone; return { httpOnly: true, secure: useNone ? true : isProd, sameSite: useNone ? "none" : "lax", path: "/", ...(typeof maxAgeSeconds === "number" ? { maxAge: maxAgeSeconds } : {}), }; } /** 登录类型:账号密码 / API Key */ export type EmbedDemoLoginType = "password" | "api_key"; export type EmbedDemoJwtPayload = { sub: string; role: typeof EMBED_DEMO_ROLE; loginType: EmbedDemoLoginType; }; function normalizeLoginType(raw: unknown): EmbedDemoLoginType { // 兼容旧 Cookie(无 lt 声明):默认按账号密码处理 return raw === "api_key" ? "api_key" : "password"; } function getJwtSecret(): Uint8Array { const secret = process.env.JWT_SECRET; if (!secret) { throw new Error("JWT_SECRET 未配置"); } return new TextEncoder().encode(secret); } export function getEmbedDemoPassword(): string { return process.env.EMBED_DEMO_PASSWORD?.trim() || EMBED_DEMO_DEFAULT_PASSWORD; } function toEmbedPayload(payload: JWTPayload): EmbedDemoJwtPayload { if (typeof payload.sub !== "string" || payload.role !== EMBED_DEMO_ROLE) { throw unauthorized("无效的演示会话"); } return { sub: payload.sub, role: EMBED_DEMO_ROLE, loginType: normalizeLoginType(payload.lt), }; } export async function signEmbedDemoToken( customerId: string, loginType: EmbedDemoLoginType = "password", ): Promise { return new SignJWT({ role: EMBED_DEMO_ROLE, lt: loginType }) .setProtectedHeader({ alg: "HS256" }) .setSubject(customerId) .setIssuedAt() .setExpirationTime(`${JWT_EXPIRY_SECONDS}s`) .sign(getJwtSecret()); } export async function verifyEmbedDemoToken( token: string, ): Promise { try { const { payload } = await jwtVerify(token, getJwtSecret(), { algorithms: ["HS256"], }); return toEmbedPayload(payload); } catch { throw unauthorized("演示会话无效或已过期"); } }