Skip to content

Commit c8d3786

Browse files
feat(adapter-codex-local): add device-login output parser
Parse the Codex device-login output for the login URL and the one-time code. Accept only the exact origin and path https://auth.openai.com/codex/device and reject any query or fragment. Accept only the XXXX-XXXXX code structure. Keep the URL and the code out of every log and every thrown error. Add redacted, real sample fixtures from the capture step and table-driven tests. Co-authored-by: Paperclip <noreply@paperclip.ing>
1 parent 19be4cf commit c8d3786

5 files changed

Lines changed: 239 additions & 0 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Device-login sample fixtures
2+
3+
These fixtures hold redacted, real Codex device-login output. A capture step ran
4+
`codex login --device-auth` inside a Daytona sandbox and recorded the output. The
5+
capture step redacted every secret before it kept or posted the text. The parser
6+
tests read these fixtures. The tests never read a live secret.
7+
8+
## Source
9+
10+
- Capture date (UTC): `2026-08-08`.
11+
- Daytona image: `cr.app.daytona.io/sbox/daytona-6a8d60e245981dd72d0e647e6afae46e0fd7b8bdfdb29b08120747509a39dc33:daytona`.
12+
- Sandbox id: `3bd22e18-2103-4227-b918-6c375fd30b49` (region `us`).
13+
- Codex home: a new throwaway directory for each independent run.
14+
15+
## Files
16+
17+
| File | Row | Condition | Expected parse result |
18+
|---|---|---|---|
19+
| `device-login-sample.txt` | A — normal prompt | `timeout 60 codex login --device-auth` | a URL and a code |
20+
| `device-login-edge.txt` | D — error / retry | offline run with an unreachable local proxy | `null` |
21+
22+
Two more rows from the capture are not committed as fixtures. Row B (timeout
23+
tail) printed no extra Codex line; the external `timeout` process ended the
24+
command with exit status `124`. Row C (`codex login --help`) and Row E
25+
(`codex --version`) are not device-login prompts.
26+
27+
## Redaction
28+
29+
The capture step transformed the real one-time code to question marks and kept
30+
the shape. So `device-login-sample.txt` holds `????-?????` in the code position.
31+
The real code alphabet is a Codex detail and the capture did not keep it. The
32+
parser matches the grounded structure of the code — four characters, a hyphen,
33+
then five characters — and does not invent an alphabet. The token class is bound
34+
to alphanumerics and the redaction sentinel `?`, so the committed sample parses
35+
to a code without a real secret in the repository.
36+
37+
The capture step checked the final text for common credential field names and
38+
for an unredacted device-code pattern. It found no match.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Error logging in with device code: error sending request for url (https://auth.openai.com/api/accounts/deviceauth/<redacted-path>)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
1. Open this link in your browser and sign in to your account
2+
https://auth.openai.com/codex/device
3+
2. Enter this one-time code (expires in 15 minutes)
4+
????-?????
5+
Device codes are a common phishing target. Never share this code.
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { readFileSync } from "node:fs";
2+
import path from "node:path";
3+
import { fileURLToPath } from "node:url";
4+
import { describe, expect, it } from "vitest";
5+
import { parseDeviceLoginPrompt } from "./device-login-parse.js";
6+
7+
const fixturesDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "__fixtures__");
8+
9+
function readFixture(name: string): string {
10+
return readFileSync(path.join(fixturesDir, name), "utf8");
11+
}
12+
13+
const EXACT_URL = "https://auth.openai.com/codex/device";
14+
15+
describe("parseDeviceLoginPrompt", () => {
16+
it("parse_returns_url_and_code_from_sample", () => {
17+
const result = parseDeviceLoginPrompt(readFixture("device-login-sample.txt"));
18+
expect(result).not.toBeNull();
19+
expect(result?.url).toBe(EXACT_URL);
20+
// The committed sample keeps the capture-time redaction of the code. The
21+
// parser extracts the four-hyphen-five structure without a real secret.
22+
expect(result?.code).toBe("????-?????");
23+
});
24+
25+
it("parse_returns_null_when_prompt_absent", () => {
26+
const text = "Some unrelated log line\nNothing to see here\n";
27+
expect(parseDeviceLoginPrompt(text)).toBeNull();
28+
});
29+
30+
it("parse_returns_null_for_url_with_query_or_fragment", () => {
31+
const withQuery = [
32+
"Open this link",
33+
"https://auth.openai.com/codex/device?foo=bar",
34+
"ABCD-EFGHJ",
35+
].join("\n");
36+
const withFragment = [
37+
"Open this link",
38+
"https://auth.openai.com/codex/device#section",
39+
"ABCD-EFGHJ",
40+
].join("\n");
41+
expect(parseDeviceLoginPrompt(withQuery)).toBeNull();
42+
expect(parseDeviceLoginPrompt(withFragment)).toBeNull();
43+
});
44+
45+
it("parse_returns_null_for_wrong_origin_or_path", () => {
46+
const wrongOrigin = [
47+
"Open this link",
48+
"https://auth.example.com/codex/device",
49+
"ABCD-EFGHJ",
50+
].join("\n");
51+
const wrongPath = [
52+
"Open this link",
53+
"https://auth.openai.com/codex/device/extra",
54+
"ABCD-EFGHJ",
55+
].join("\n");
56+
const httpScheme = [
57+
"Open this link",
58+
"http://auth.openai.com/codex/device",
59+
"ABCD-EFGHJ",
60+
].join("\n");
61+
expect(parseDeviceLoginPrompt(wrongOrigin)).toBeNull();
62+
expect(parseDeviceLoginPrompt(wrongPath)).toBeNull();
63+
expect(parseDeviceLoginPrompt(httpScheme)).toBeNull();
64+
});
65+
66+
it("parse_returns_null_for_malformed_short_code", () => {
67+
const shortCode = [EXACT_URL, "ABC-EFGHJ"].join("\n"); // 3 then 5
68+
const longCode = [EXACT_URL, "ABCDE-EFGHJ"].join("\n"); // 5 then 5
69+
const noHyphen = [EXACT_URL, "ABCDEFGHJ"].join("\n");
70+
const noCode = [EXACT_URL, "no code on this line"].join("\n");
71+
expect(parseDeviceLoginPrompt(shortCode)).toBeNull();
72+
expect(parseDeviceLoginPrompt(longCode)).toBeNull();
73+
expect(parseDeviceLoginPrompt(noHyphen)).toBeNull();
74+
expect(parseDeviceLoginPrompt(noCode)).toBeNull();
75+
});
76+
77+
it("parse_ignores_token_like_text", () => {
78+
// Token-like noise without the exact device URL must not yield a prompt.
79+
const text = [
80+
"tokens received",
81+
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.payload.signature",
82+
"sk-proj-ABCD1234ABCD1234ABCD1234",
83+
"ABCD-EFGHJ",
84+
].join("\n");
85+
expect(parseDeviceLoginPrompt(text)).toBeNull();
86+
});
87+
88+
it("parse_returns_null_for_edge_sample", () => {
89+
// The grounded edge row carries a URL, but a wrong path and a wrong origin
90+
// segment, so the parser rejects it.
91+
expect(parseDeviceLoginPrompt(readFixture("device-login-edge.txt"))).toBeNull();
92+
});
93+
94+
it("keeps the url and the code out of a thrown error", () => {
95+
// A non-string input is a programming error, but the message must never
96+
// carry secret-bearing input. The parser returns null instead of throwing.
97+
// @ts-expect-error deliberate wrong type
98+
expect(parseDeviceLoginPrompt(undefined)).toBeNull();
99+
// @ts-expect-error deliberate wrong type
100+
expect(parseDeviceLoginPrompt(12345)).toBeNull();
101+
});
102+
});
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// The device-login output parser. It reads the Codex `login --device-auth`
2+
// output and returns the login URL and the one-time code, or null.
3+
//
4+
// Security (Control 1 — strict validation): the parser accepts only the exact
5+
// origin and path of the device-login URL. It rejects any query, any fragment, a
6+
// different origin, and a different path. It accepts only the short-code
7+
// structure `XXXX-XXXXX` (four characters, a hyphen, then five characters). The
8+
// parser never logs the URL, the code, or any input byte, and it keeps them out
9+
// of every thrown error. The parser is a pure function.
10+
11+
export interface DeviceLoginPrompt {
12+
url: string;
13+
code: string;
14+
}
15+
16+
// The one and only accepted device-login URL. The parser returns this exact
17+
// constant string on a match, so the output is never a caller-controlled value.
18+
export const DEVICE_LOGIN_URL = "https://auth.openai.com/codex/device";
19+
20+
// A candidate URL token is a run of non-space characters that starts with an
21+
// http or https scheme. The parser validates each candidate with the `URL`
22+
// class; the regular expression only splits tokens out of the text.
23+
const URL_TOKEN_RE = /https?:\/\/\S+/g;
24+
25+
// Trailing punctuation that prose commonly puts right after a URL. The parser
26+
// strips only these characters. It never strips `?` or `#`, so a URL with a
27+
// query or a fragment stays malformed and the parser rejects it.
28+
const TRAILING_PUNCTUATION_RE = /[)\].,;:!]+$/;
29+
30+
// The one-time code structure: four characters, a hyphen, then five characters.
31+
// The real code alphabet is a Codex detail that the grounded capture did not
32+
// keep (the capture redacted the code to `?`). So the parser matches the
33+
// grounded structure and binds the token class to alphanumerics and the
34+
// redaction sentinel `?`. The code sits on its own token, so the pattern anchors
35+
// on a word boundary at the start and requires a space or the end after it.
36+
const SHORT_CODE_RE = /(?:^|\s)([A-Za-z0-9?]{4}-[A-Za-z0-9?]{5})(?=\s|$)/m;
37+
38+
/**
39+
* Returns the exact device-login URL when `text` holds it as a standalone token
40+
* with the exact origin `https://auth.openai.com` and the exact path
41+
* `/codex/device` and no query, fragment, or credentials. Returns null
42+
* otherwise. Returns the canonical {@link DEVICE_LOGIN_URL} constant on a match.
43+
*/
44+
function findExactDeviceUrl(text: string): string | null {
45+
const tokens = text.match(URL_TOKEN_RE);
46+
if (!tokens) return null;
47+
for (const token of tokens) {
48+
const cleaned = token.replace(TRAILING_PUNCTUATION_RE, "");
49+
let parsed: URL;
50+
try {
51+
parsed = new URL(cleaned);
52+
} catch {
53+
continue;
54+
}
55+
if (
56+
parsed.protocol === "https:" &&
57+
parsed.host === "auth.openai.com" &&
58+
parsed.pathname === "/codex/device" &&
59+
parsed.search === "" &&
60+
parsed.hash === "" &&
61+
parsed.username === "" &&
62+
parsed.password === ""
63+
) {
64+
return DEVICE_LOGIN_URL;
65+
}
66+
}
67+
return null;
68+
}
69+
70+
/**
71+
* Returns the one-time code when `text` holds a token with the structure
72+
* `XXXX-XXXXX`. Returns null otherwise.
73+
*/
74+
function findShortCode(text: string): string | null {
75+
const match = SHORT_CODE_RE.exec(text);
76+
return match ? match[1] : null;
77+
}
78+
79+
/**
80+
* Parses Codex device-login output. Returns the login URL and the one-time code
81+
* when both are present and valid. Returns null for any other input, including a
82+
* non-string input, an absent prompt, a URL with a query or a fragment, a wrong
83+
* origin or path, and a malformed short code. Never throws on input, and never
84+
* puts the URL or the code into a log or an error.
85+
*/
86+
export function parseDeviceLoginPrompt(text: string): DeviceLoginPrompt | null {
87+
if (typeof text !== "string" || text.length === 0) return null;
88+
const url = findExactDeviceUrl(text);
89+
if (!url) return null;
90+
const code = findShortCode(text);
91+
if (!code) return null;
92+
return { url, code };
93+
}

0 commit comments

Comments
 (0)