mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-24 21:41:08 +02:00
feat: Sign in with ChatGPT — OAuth sign-in, token storage, and settings UI
Lets users connect their ChatGPT Plus/Pro subscription instead of pasting
an API key. Sign-in half only: the codex model client consumes the session
via getChatGPTAccessToken().
- PKCE OAuth against auth.openai.com using the official Codex CLI public
client id (constants verified against the openai/codex sources); system
browser + loopback callback server on the pre-registered fixed port
127.0.0.1:1455, state validated, stale callbacks rejected
- token store (core): single config/chatgpt-auth.json holding non-secret
display fields (email, accountId, expiresAt) plus the token material
encrypted via the safeStorage cipher bridge (plaintext-with-marker
fallback when no keychain, matching the GitHub token path); no token
value ever written in the clear or logged
- getChatGPTAccessToken(): single-flight refresh within 5 min of expiry;
refresh rejection (400/401) clears to a clean signed-out state and
throws typed ChatGPTAuthRequiredError; 5xx/network kept as transient
- sign-out with best-effort token revocation
- IPC chatgpt:getStatus/signIn/cancelSignIn/signOut — raw tokens never
cross to the renderer; real cancellation settles the in-flight attempt,
frees the port, and a retry starts a fresh attempt (new PKCE/state)
- settings UI: ChatGPT Subscription section on the OpenAI card
(sign in / waiting + cancel / connected as {email} + sign out) via a
useChatGPT hook
Vitest covers the token store, single-flight refresh, and
transient-vs-terminal refresh handling.
This commit is contained in:
parent
826bce90ad
commit
a1db0e395a
10 changed files with 1382 additions and 23 deletions
|
|
@ -783,6 +783,44 @@ const ipcSchemas = {
|
|||
}),
|
||||
res: z.null(),
|
||||
},
|
||||
// --- "Sign in with ChatGPT" (subscription OAuth via the Codex CLI client) ---
|
||||
// Raw tokens are never exposed over IPC — the renderer only sees identity
|
||||
// and connection state.
|
||||
'chatgpt:getStatus': {
|
||||
req: z.null(),
|
||||
res: z.object({
|
||||
signedIn: z.boolean(),
|
||||
email: z.string().optional(),
|
||||
accountId: z.string().optional(),
|
||||
}),
|
||||
},
|
||||
// Resolves when the browser flow settles (success, denial, timeout, port
|
||||
// busy, exchange failure, cancellation) — same shape as getStatus plus an
|
||||
// error string; `cancelled` marks expected teardown (no error toast).
|
||||
'chatgpt:signIn': {
|
||||
req: z.null(),
|
||||
res: z.object({
|
||||
signedIn: z.boolean(),
|
||||
email: z.string().optional(),
|
||||
accountId: z.string().optional(),
|
||||
cancelled: z.boolean().optional(),
|
||||
error: z.string().optional(),
|
||||
}),
|
||||
},
|
||||
// Abort the pending sign-in attempt: stops the loopback server and settles
|
||||
// the in-flight chatgpt:signIn with a cancelled outcome. No-op when idle.
|
||||
'chatgpt:cancelSignIn': {
|
||||
req: z.null(),
|
||||
res: z.object({
|
||||
success: z.boolean(),
|
||||
}),
|
||||
},
|
||||
'chatgpt:signOut': {
|
||||
req: z.null(),
|
||||
res: z.object({
|
||||
success: z.boolean(),
|
||||
}),
|
||||
},
|
||||
'app:openUrl': {
|
||||
req: z.object({
|
||||
url: z.string(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue