| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { randomBytes } from "crypto";
|
|
|
| const TICKET_TTL_MS = 15 * 60 * 1000;
|
| const STORE_KEY = "__codexDeviceFlowTickets";
|
|
|
| export type DeviceFlowTicketStatus = "pending" | "claimed" | "completed";
|
|
|
| export interface DeviceFlowTicketResult {
|
| connectionId: string;
|
| email: string | null;
|
| }
|
|
|
| export interface DeviceFlowTicket {
|
| token: string;
|
| provider: string;
|
|
|
| connectionId?: string;
|
|
|
| expiresAt: number;
|
| status: DeviceFlowTicketStatus;
|
|
|
| result?: DeviceFlowTicketResult;
|
| }
|
|
|
| function store(): Map<string, DeviceFlowTicket> {
|
| const g = globalThis as unknown as { [STORE_KEY]?: Map<string, DeviceFlowTicket> };
|
| if (!g[STORE_KEY]) g[STORE_KEY] = new Map<string, DeviceFlowTicket>();
|
| return g[STORE_KEY]!;
|
| }
|
|
|
| function prune(): void {
|
| const now = Date.now();
|
| const map = store();
|
| for (const [token, ticket] of map) {
|
| if (ticket.expiresAt <= now) map.delete(token);
|
| }
|
| }
|
|
|
|
|
| export function createDeviceFlowTicket(
|
| provider: string,
|
| connectionId?: string
|
| ): { token: string; expiresAt: number } {
|
| prune();
|
| const token = randomBytes(32).toString("base64url");
|
| const expiresAt = Date.now() + TICKET_TTL_MS;
|
| store().set(token, { token, provider, connectionId, expiresAt, status: "pending" });
|
| return { token, expiresAt };
|
| }
|
|
|
|
|
| export function peekDeviceFlowTicket(token: string): DeviceFlowTicket | null {
|
| prune();
|
| const ticket = store().get(token);
|
| if (!ticket || ticket.expiresAt <= Date.now()) return null;
|
| return ticket;
|
| }
|
|
|
| |
| |
| |
| |
|
|
| export function claimDeviceFlowTicket(token: string, provider: string): DeviceFlowTicket | null {
|
| const ticket = peekDeviceFlowTicket(token);
|
| if (!ticket || ticket.provider !== provider || ticket.status !== "pending") return null;
|
| ticket.status = "claimed";
|
| return ticket;
|
| }
|
|
|
|
|
| export function completeDeviceFlowTicket(token: string, result: DeviceFlowTicketResult): void {
|
| const ticket = store().get(token);
|
| if (!ticket) return;
|
| ticket.status = "completed";
|
| ticket.result = result;
|
| }
|
|
|
|
|
| export function releaseDeviceFlowTicket(token: string): void {
|
| const ticket = store().get(token);
|
| if (ticket && ticket.status === "claimed") ticket.status = "pending";
|
| }
|
|
|
| |
| |
| |
|
|
| export function getDeviceFlowTicketStatus(
|
| token: string
|
| ): { status: DeviceFlowTicketStatus | "expired"; result: DeviceFlowTicketResult | null } {
|
| const ticket = peekDeviceFlowTicket(token);
|
| if (!ticket) return { status: "expired", result: null };
|
| return { status: ticket.status, result: ticket.result ?? null };
|
| }
|
|
|