File size: 4,124 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
/**
 * Single-use, short-lived tickets for the public external Codex connection link.
 *
 * The dashboard generates a ticket and shares a public URL
 * (`/connect/codex/{token}`). A third party opens it and completes the Codex
 * device flow in their own browser; the public completion endpoint claims +
 * completes the ticket before persisting the connection. The dashboard polls the
 * ticket status so it can notify + refresh once the remote login finishes.
 *
 * Backed by an in-memory `globalThis` map, mirroring the existing
 * `__codexCallbackState` pattern in the OAuth route. Trade-off: tickets do not
 * survive a restart and are not shared across instances — acceptable for a
 * short-lived (15 min), single-use link.
 */
import { randomBytes } from "crypto";

const TICKET_TTL_MS = 15 * 60 * 1000; // matches OpenAI's device code expiry
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;
  /** Optional target connection to update instead of creating a new one. */
  connectionId?: string;
  /** Epoch ms. */
  expiresAt: number;
  status: DeviceFlowTicketStatus;
  /** Populated once the device flow completes and the connection is persisted. */
  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);
  }
}

/** Create a ticket and return its opaque token + expiry. */
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 };
}

/** Return a ticket if it exists and has not expired (any status). */
export function peekDeviceFlowTicket(token: string): DeviceFlowTicket | null {
  prune();
  const ticket = store().get(token);
  if (!ticket || ticket.expiresAt <= Date.now()) return null;
  return ticket;
}

/**
 * Claim a still-pending ticket for the given provider so the device flow can be
 * completed exactly once. Marks it "claimed" to reject concurrent/duplicate
 * submissions. Returns the ticket, or null if invalid/expired/used/wrong-provider.
 */
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;
}

/** Mark a claimed ticket completed and record the resulting connection. */
export function completeDeviceFlowTicket(token: string, result: DeviceFlowTicketResult): void {
  const ticket = store().get(token);
  if (!ticket) return;
  ticket.status = "completed";
  ticket.result = result;
}

/** Revert a claimed ticket back to pending so the visitor can retry after a failure. */
export function releaseDeviceFlowTicket(token: string): void {
  const ticket = store().get(token);
  if (ticket && ticket.status === "claimed") ticket.status = "pending";
}

/**
 * Status for the dashboard poll. Returns "expired" when the ticket is gone
 * (missing or past its TTL), otherwise the live status + result if completed.
 */
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 };
}