mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-16 08:14:45 +00:00
feat(codex-auth): add profile registry + storage foundation
Adds the storage substrate for ccsx auth profile isolation. Each Codex profile gets its own CODEX_HOME dir under ~/.ccs/codex-instances/<name>/ with isolated auth.json and history.jsonl; config.toml is shared via symlink to ~/.codex/config.toml so two terminals can run two Codex accounts simultaneously without duplicating user config. - CodexProfileRegistry (YAML, atomic write tmp.<pid>.<rand> + rename, orphan cleanup, full CRUD + default pointer) - decode-id-token: pure base64 JWT decoder for OpenAI id_token, reads nested https://api.openai.com/auth claims (chatgpt_plan_type, chatgpt_account_id) and dual-path email - ensureSharedConfigSymlink: self-healing, idempotent, overwrites stale entries with stderr warning - 45 unit tests, all green Foundation only — no CLI, no runtime injection, no dashboard. Subsequent commits wire those in.
This commit is contained in:
1 parent
d2f7d3f407
commit
358b703d98
12 files changed
+1006
No files matched your search
@@ -0,0 +1,124 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
|
||||
// Lazy import so tests can run before implementation
|
||||
let decodeIdToken: (idToken: string) => { email?: string; plan_type?: string; account_id?: string };
|
||||
|
||||
// Fixture: real-shape JWT with nested claims
|
||||
// Payload (base64url): {"email":"user@example.com","https://api.openai.com/auth":{"chatgpt_plan_type":"pro","chatgpt_account_id":"4b0448c0-e4a2-4cc0-a70d-77065d613553"}}
|
||||
const VALID_NESTED_TOKEN = buildToken({
|
||||
email: 'user@example.com',
|
||||
'https://api.openai.com/auth': {
|
||||
chatgpt_plan_type: 'pro',
|
||||
chatgpt_account_id: '4b0448c0-e4a2-4cc0-a70d-77065d613553',
|
||||
},
|
||||
});
|
||||
|
||||
// Fixture: email only via profile fallback path
|
||||
const PROFILE_EMAIL_TOKEN = buildToken({
|
||||
'https://api.openai.com/profile': { email: 'profile@example.com' },
|
||||
'https://api.openai.com/auth': { chatgpt_plan_type: 'plus', chatgpt_account_id: 'acct-xyz' },
|
||||
});
|
||||
|
||||
// Fixture: neither top-level email nor profile email — both absent
|
||||
const NO_EMAIL_TOKEN = buildToken({
|
||||
sub: 'user-abc',
|
||||
'https://api.openai.com/auth': {
|
||||
chatgpt_plan_type: 'free',
|
||||
chatgpt_account_id: 'acct-123',
|
||||
},
|
||||
});
|
||||
|
||||
// Fixture: missing plan_type in auth claim
|
||||
const MISSING_PLAN_TOKEN = buildToken({
|
||||
email: 'user@example.com',
|
||||
'https://api.openai.com/auth': {
|
||||
chatgpt_account_id: 'acct-no-plan',
|
||||
},
|
||||
});
|
||||
|
||||
// Fixture: missing account_id in auth claim
|
||||
const MISSING_ACCOUNT_ID_TOKEN = buildToken({
|
||||
email: 'user@example.com',
|
||||
'https://api.openai.com/auth': {
|
||||
chatgpt_plan_type: 'pro',
|
||||
},
|
||||
});
|
||||
|
||||
// Fixture: no https://api.openai.com/auth claim at all
|
||||
const NO_AUTH_CLAIM_TOKEN = buildToken({
|
||||
email: 'user@example.com',
|
||||
sub: 'user-abc',
|
||||
});
|
||||
|
||||
function buildToken(payload: Record<string, unknown>): string {
|
||||
const header = Buffer.from(JSON.stringify({ alg: 'RS256', typ: 'JWT' })).toString('base64url');
|
||||
const body = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
||||
return `${header}.${body}.fakesig`;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
const mod = await import('../../../src/codex-auth/decode-id-token');
|
||||
decodeIdToken = mod.decodeIdToken;
|
||||
});
|
||||
|
||||
describe('decodeIdToken', () => {
|
||||
it('extracts email, plan_type, account_id from standard nested JWT', () => {
|
||||
const result = decodeIdToken(VALID_NESTED_TOKEN);
|
||||
expect(result.email).toBe('user@example.com');
|
||||
expect(result.plan_type).toBe('pro');
|
||||
expect(result.account_id).toBe('4b0448c0-e4a2-4cc0-a70d-77065d613553');
|
||||
});
|
||||
|
||||
it('falls back to profile email when top-level email is absent', () => {
|
||||
const result = decodeIdToken(PROFILE_EMAIL_TOKEN);
|
||||
expect(result.email).toBe('profile@example.com');
|
||||
expect(result.plan_type).toBe('plus');
|
||||
expect(result.account_id).toBe('acct-xyz');
|
||||
});
|
||||
|
||||
it('returns {} for token with only 2 segments (malformed)', () => {
|
||||
const result = decodeIdToken('header.payload');
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('returns {} for non-base64 garbage input', () => {
|
||||
const result = decodeIdToken('!!!.%%%.$$$');
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('returns empty object when email is absent in both paths', () => {
|
||||
const result = decodeIdToken(NO_EMAIL_TOKEN);
|
||||
expect(result.email).toBeUndefined();
|
||||
expect(result.plan_type).toBe('free');
|
||||
expect(result.account_id).toBe('acct-123');
|
||||
});
|
||||
|
||||
it('returns undefined plan_type when chatgpt_plan_type is absent', () => {
|
||||
const result = decodeIdToken(MISSING_PLAN_TOKEN);
|
||||
expect(result.email).toBe('user@example.com');
|
||||
expect(result.plan_type).toBeUndefined();
|
||||
expect(result.account_id).toBe('acct-no-plan');
|
||||
});
|
||||
|
||||
it('returns undefined account_id when chatgpt_account_id is absent', () => {
|
||||
const result = decodeIdToken(MISSING_ACCOUNT_ID_TOKEN);
|
||||
expect(result.email).toBe('user@example.com');
|
||||
expect(result.plan_type).toBe('pro');
|
||||
expect(result.account_id).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns partial result when https://api.openai.com/auth claim is absent', () => {
|
||||
const result = decodeIdToken(NO_AUTH_CLAIM_TOKEN);
|
||||
expect(result.email).toBe('user@example.com');
|
||||
expect(result.plan_type).toBeUndefined();
|
||||
expect(result.account_id).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not throw on empty string input', () => {
|
||||
expect(() => decodeIdToken('')).not.toThrow();
|
||||
expect(decodeIdToken('')).toEqual({});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user