import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { classifyToken, isValidEmail, looksLikeToken, parseAccountText, unwrapPastedSecret, } from '../src/dashboard/account-text-parser.js'; // WHY THIS FILE EXISTS. account-text-parser.js was one of four src/ files never named in // test/, and the only one where that was a real gap: 440 lines of pure string parsing, no // network and no side effects, reached directly from the dashboard's account-import path // (src/dashboard/api.js). It is also the code that decides what counts as a credential — // `devin-session-token$…`, `auth1_…`, and bare Firebase refresh JWTs each take a different // path downstream, and it holds two silent `catch {}` blocks, so a misparse shows up as // "the import did nothing" rather than as an error. // // Assertions are on behaviour with realistic-shaped fixtures. Every credential-looking value // here is a `ws-fixture-*` literal or an obviously fake padded string: nothing that resembles // a live token, and deliberately not the `sk-*` shape, which secret-scan flags. const LONG = (prefix, n = 140) => prefix + 'x'.repeat(Math.max(0, n - prefix.length)); describe('classifyToken', () => { it('recognises the session-token prefix this repo actually issues', () => { // The `$` in `devin-session-token$` is the character that once slipped past // secret-scan's own character class, so it is worth pinning that it classifies here. assert.equal(classifyToken('devin-session-token$ws-fixture-abc.def.ghi'), 'session'); assert.equal(classifyToken(' devin-session-token$ws-fixture-abc '), 'session', 'trims'); }); it('requires auth1_ tokens to reach the length floor', () => { assert.equal(classifyToken(`auth1_${'y'.repeat(30)}`), 'auth1'); // 19 chars total: prefix present but under the >= 20 floor, so NOT auth1. It is also // under the refresh floor, so it lands on unknown rather than silently becoming refresh. assert.equal(classifyToken('auth1_yyyyyyyyyyyyy'), 'unknown'); }); it('treats a long opaque blob as a refresh token', () => { assert.equal(classifyToken(LONG('ws-fixture-refresh-')), 'refresh'); }); it('holds the refresh length floor at >100, so short opaque strings stay unknown', () => { // Without this the floor is unpinned: every other fixture here is far past 100 chars, so // relaxing `length > 100` to `> 5` survived a mutation run. A low floor would promote // ordinary short identifiers to credentials. assert.equal(classifyToken('x'.repeat(101)), 'refresh', '101 chars is over the floor'); assert.equal(classifyToken('x'.repeat(100)), 'unknown', 'exactly 100 must not qualify'); assert.equal(classifyToken('ws-fixture-short'), 'unknown'); }); it('classifies a long three-part JWT as refresh', () => { const jwt = `${'a'.repeat(40)}.${'b'.repeat(40)}.${'c'.repeat(40)}`; assert.equal(classifyToken(jwt), 'refresh'); }); it('rejects the shapes that must NOT be mistaken for credentials', () => { assert.equal(classifyToken(''), 'unknown'); assert.equal(classifyToken(' '), 'unknown'); assert.equal(classifyToken(undefined), 'unknown'); // An email is long enough to trip a naive length check; the @ exclusion is what stops it. assert.equal(classifyToken(`${'ws-fixture-'.repeat(12)}@example.test`), 'unknown'); // Contains a space, so it is prose, not a token. assert.equal(classifyToken(LONG('this is a sentence ')), 'unknown'); // `devin-` prefixed but not the session form: explicitly excluded from refresh. assert.equal(classifyToken(LONG('devin-something-else-')), 'unknown'); }); }); describe('unwrapPastedSecret', () => { it('passes a bare session token through', () => { assert.equal(unwrapPastedSecret(' devin-session-token$ws-fixture-abc '), 'devin-session-token$ws-fixture-abc'); }); it('pulls token= out of a show-auth-token URL', () => { const url = 'https://windsurf.com/show-auth-token?token=devin-session-token$ws-fixture-from-url'; assert.equal(unwrapPastedSecret(url), 'devin-session-token$ws-fixture-from-url'); }); it('pulls access_token out of a hash fragment', () => { const url = 'https://windsurf.com/windsurf/signin#access_token=devin-session-token$ws-fixture-hash'; assert.equal(unwrapPastedSecret(url), 'devin-session-token$ws-fixture-hash'); }); it('throws ERR_NO_TOKEN_IN_INPUT when the URL has no token param', () => { assert.throws( () => unwrapPastedSecret('https://windsurf.com/show-auth-token?state=abc'), /ERR_NO_TOKEN_IN_INPUT/, ); }); }); describe('isValidEmail', () => { it('accepts ordinary addresses', () => { for (const e of [ 'ws-fixture@example.test', 'ws.fixture+tag@sub.example.test', 'ws-fixture_1@example.co.uk', ]) { assert.equal(isValidEmail(e), true, e); } }); it('rejects non-strings and out-of-range lengths', () => { assert.equal(isValidEmail(null), false); assert.equal(isValidEmail(undefined), false); assert.equal(isValidEmail(12345), false); assert.equal(isValidEmail('a@b'), false, 'under the 5-char floor'); assert.equal(isValidEmail(`${'a'.repeat(250)}@example.test`), false, 'over 254'); }); it('rejects separators, which is what keeps pasted lists from parsing as one address', () => { // The dashboard accepts pasted blobs, so a value carrying a delimiter means the caller // split on the wrong thing — accepting it would attach one account to another's token. for (const bad of [ 'a@example.test b@example.test', 'a@example.test,b@example.test', 'a@example.test;b@example.test', 'a@example.test,b@example.test', 'a@example.test;b@example.test', 'a@example.test\tb@example.test', ]) { assert.equal(isValidEmail(bad), false, bad); } }); it('rejects malformed addresses', () => { for (const bad of ['noatsign.example.test', '@example.test', 'a@', 'a@example', 'a@.test']) { assert.equal(isValidEmail(bad), false, bad); } }); }); describe('looksLikeToken', () => { it('is a coarse gate, so only assert what the parser relies on', () => { assert.equal(looksLikeToken('devin-session-token$ws-fixture-abc'), true); assert.equal(looksLikeToken(''), false); assert.equal(looksLikeToken(null), false); }); }); describe('parseAccountText', () => { it('pairs an email with the token that follows it', () => { const out = parseAccountText( `ws-fixture-1@example.test\ndevin-session-token$${'a'.repeat(30)}\n`, ); assert.equal(out.tokenPairs.length, 1); assert.equal(out.tokenPairs[0].email, 'ws-fixture-1@example.test'); assert.match(out.tokenPairs[0].token, /^devin-session-token\$/); }); it('pairs an email with a LABELLED password', () => { // A bare line after an email is NOT treated as a password — passwords are only // recognised behind a label (RE_LABEL_PASS) or across a ---- delimiter. That is the // safe direction: guessing would turn any stray line into a credential. const out = parseAccountText( 'email: ws-fixture-2@example.test\npassword: ws-fixture-pass-9\n', ); assert.equal(out.accounts.length, 1); assert.equal(out.accounts[0].email, 'ws-fixture-2@example.test'); assert.equal(out.accounts[0].password, 'ws-fixture-pass-9'); }); it('accepts the Chinese labels too, since the dashboard is bilingual', () => { const out = parseAccountText('邮箱:ws-fixture-3@example.test\n密码:ws-fixture-pass-7\n'); assert.equal(out.accounts.length, 1); assert.equal(out.accounts[0].email, 'ws-fixture-3@example.test'); assert.equal(out.accounts[0].password, 'ws-fixture-pass-7'); }); it('parses the ----separated form', () => { const out = parseAccountText(`ws-fixture-4@example.test----ws-fixture-pass-4\n`); assert.equal(out.accounts.length, 1, JSON.stringify(out)); assert.equal(out.accounts[0].email, 'ws-fixture-4@example.test'); assert.equal(out.accounts[0].password, 'ws-fixture-pass-4'); }); it('does not treat a bare line after an email as a password', () => { const out = parseAccountText('ws-fixture-5@example.test\njust some note\n'); assert.equal(out.accounts.length, 0, 'an unlabelled line must not become a credential'); }); it('unwraps a show-auth-token URL line into the tokens bucket', () => { const inner = `devin-session-token$${'u'.repeat(30)}`; const out = parseAccountText(`https://windsurf.com/show-auth-token?token=${encodeURIComponent(inner)}\n`); assert.equal(out.tokens.length, 1); assert.equal(out.tokens[0], inner); }); it('unwraps a labeled Token: show-auth-token URL', () => { const inner = `devin-session-token$${'L'.repeat(30)}`; const out = parseAccountText(`Token: https://windsurf.com/show-auth-token?token=${encodeURIComponent(inner)}\n`); assert.equal(out.tokens.length, 1, JSON.stringify(out)); assert.equal(out.tokens[0], inner); }); it('keeps a token-less show-auth-token URL in the tokens bucket so import can fail visibly', () => { const out = parseAccountText('https://windsurf.com/show-auth-token?state=abc\n'); assert.equal(out.tokens.length, 1, JSON.stringify(out)); assert.match(out.tokens[0], /^https:\/\/windsurf\.com\/show-auth-token/); }); it('keeps an unpaired token as a bare token rather than dropping it', () => { // Silently discarding a credential is the failure mode that reads as "import did // nothing", so pin that a token with no preceding email still comes out. const out = parseAccountText(`devin-session-token$${'b'.repeat(30)}\n`); assert.equal(out.tokenPairs.length, 0); assert.equal(out.tokens.length, 1); assert.match(out.tokens[0], /^devin-session-token\$/); }); it('does not merge two accounts when a token is missing between them', () => { const out = parseAccountText( [ 'ws-fixture-a@example.test', `devin-session-token$${'c'.repeat(30)}`, 'ws-fixture-b@example.test', `devin-session-token$${'d'.repeat(30)}`, ].join('\n'), ); assert.equal(out.tokenPairs.length, 2); assert.deepEqual( out.tokenPairs.map((p) => p.email), ['ws-fixture-a@example.test', 'ws-fixture-b@example.test'], ); assert.notEqual(out.tokenPairs[0].token, out.tokenPairs[1].token, 'no cross-wiring'); }); it('returns the four buckets even for input it cannot use', () => { for (const input of ['', ' \n\n ', 'just some prose with no credentials at all']) { const out = parseAccountText(input); assert.deepEqual(Object.keys(out).sort(), [ 'accounts', 'githubAccounts', 'tokenPairs', 'tokens', ]); for (const k of Object.keys(out)) assert.ok(Array.isArray(out[k]), `${k} is an array`); } }); it('tolerates non-string input instead of throwing into the dashboard', () => { for (const bad of [null, undefined, 42, {}]) { assert.doesNotThrow(() => parseAccountText(bad), String(bad)); } }); });