@agile-vibe-coding/avc 0.2.3 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/agents/agent-selector.md +23 -0
- package/cli/agents/code-implementer.md +117 -0
- package/cli/agents/code-validator.md +80 -0
- package/cli/agents/context-reviewer-epic.md +101 -0
- package/cli/agents/context-reviewer-story.md +92 -0
- package/cli/agents/context-writer-epic.md +145 -0
- package/cli/agents/context-writer-story.md +111 -0
- package/cli/agents/doc-writer-epic.md +42 -0
- package/cli/agents/doc-writer-story.md +43 -0
- package/cli/agents/duplicate-detector.md +110 -0
- package/cli/agents/epic-story-decomposer.md +318 -39
- package/cli/agents/mission-scope-generator.md +68 -4
- package/cli/agents/mission-scope-validator.md +40 -6
- package/cli/agents/project-context-extractor.md +21 -6
- package/cli/agents/scaffolding-generator.md +99 -0
- package/cli/agents/seed-validator.md +71 -0
- package/cli/agents/story-scope-reviewer.md +147 -0
- package/cli/agents/story-splitter.md +83 -0
- package/cli/agents/validator-documentation.json +31 -0
- package/cli/agents/validator-documentation.md +3 -1
- package/cli/api-reference-tool.js +368 -0
- package/cli/checks/catalog.json +76 -0
- package/cli/checks/code/quality.json +26 -0
- package/cli/checks/code/testing.json +14 -0
- package/cli/checks/code/traceability.json +26 -0
- package/cli/checks/cross-refs/epic.json +171 -0
- package/cli/checks/cross-refs/story.json +149 -0
- package/cli/checks/epic/api.json +114 -0
- package/cli/checks/epic/backend.json +126 -0
- package/cli/checks/epic/cloud.json +126 -0
- package/cli/checks/epic/data.json +102 -0
- package/cli/checks/epic/database.json +114 -0
- package/cli/checks/epic/developer.json +182 -0
- package/cli/checks/epic/devops.json +174 -0
- package/cli/checks/epic/frontend.json +162 -0
- package/cli/checks/epic/mobile.json +102 -0
- package/cli/checks/epic/qa.json +90 -0
- package/cli/checks/epic/security.json +184 -0
- package/cli/checks/epic/solution-architect.json +192 -0
- package/cli/checks/epic/test-architect.json +90 -0
- package/cli/checks/epic/ui.json +102 -0
- package/cli/checks/epic/ux.json +90 -0
- package/cli/checks/fixes/epic-fix-template.md +10 -0
- package/cli/checks/fixes/story-fix-template.md +10 -0
- package/cli/checks/story/api.json +186 -0
- package/cli/checks/story/backend.json +102 -0
- package/cli/checks/story/cloud.json +102 -0
- package/cli/checks/story/data.json +210 -0
- package/cli/checks/story/database.json +102 -0
- package/cli/checks/story/developer.json +168 -0
- package/cli/checks/story/devops.json +102 -0
- package/cli/checks/story/frontend.json +174 -0
- package/cli/checks/story/mobile.json +102 -0
- package/cli/checks/story/qa.json +210 -0
- package/cli/checks/story/security.json +198 -0
- package/cli/checks/story/solution-architect.json +230 -0
- package/cli/checks/story/test-architect.json +210 -0
- package/cli/checks/story/ui.json +102 -0
- package/cli/checks/story/ux.json +102 -0
- package/cli/coding-order.js +401 -0
- package/cli/dependency-checker.js +72 -0
- package/cli/epic-story-validator.js +284 -799
- package/cli/index.js +0 -0
- package/cli/init-model-config.js +17 -10
- package/cli/init.js +514 -92
- package/cli/kanban-server-manager.js +1 -2
- package/cli/llm-claude.js +98 -31
- package/cli/llm-gemini.js +29 -5
- package/cli/llm-local.js +493 -0
- package/cli/llm-openai.js +262 -41
- package/cli/llm-provider.js +147 -8
- package/cli/llm-token-limits.js +113 -4
- package/cli/llm-verifier.js +209 -1
- package/cli/llm-xiaomi.js +143 -0
- package/cli/message-constants.js +3 -12
- package/cli/messaging-api.js +6 -12
- package/cli/micro-check-fixer.js +335 -0
- package/cli/micro-check-runner.js +449 -0
- package/cli/micro-check-scorer.js +148 -0
- package/cli/micro-check-validator.js +538 -0
- package/cli/model-pricing.js +23 -0
- package/cli/model-selector.js +3 -2
- package/cli/prompt-logger.js +57 -0
- package/cli/repl-ink.js +106 -346
- package/cli/repl-old.js +1 -2
- package/cli/seed-processor.js +194 -24
- package/cli/sprint-planning-processor.js +2638 -289
- package/cli/template-processor.js +50 -3
- package/cli/token-tracker.js +50 -23
- package/cli/tools/generate-story-validators.js +1 -1
- package/cli/validation-router.js +70 -8
- package/cli/worktree-runner.js +654 -0
- package/kanban/client/dist/assets/index-D_KC5EQT.css +1 -0
- package/kanban/client/dist/assets/index-DjY5zqW7.js +351 -0
- package/kanban/client/dist/index.html +2 -2
- package/kanban/client/src/App.jsx +43 -14
- package/kanban/client/src/components/ceremony/AskArchPopup.jsx +7 -3
- package/kanban/client/src/components/ceremony/AskModelPopup.jsx +23 -10
- package/kanban/client/src/components/ceremony/CeremonyWorkflowModal.jsx +320 -133
- package/kanban/client/src/components/ceremony/ProviderSwitcherButton.jsx +290 -0
- package/kanban/client/src/components/ceremony/SponsorCallModal.jsx +80 -13
- package/kanban/client/src/components/ceremony/SprintPlanningModal.jsx +156 -22
- package/kanban/client/src/components/ceremony/steps/ArchitectureStep.jsx +11 -11
- package/kanban/client/src/components/ceremony/steps/CompleteStep.jsx +3 -21
- package/kanban/client/src/components/ceremony/steps/ReviewAnswersStep.jsx +214 -10
- package/kanban/client/src/components/ceremony/steps/RunningStep.jsx +23 -2
- package/kanban/client/src/components/kanban/CardDetailModal.jsx +97 -10
- package/kanban/client/src/components/kanban/GroupingSelector.jsx +7 -1
- package/kanban/client/src/components/kanban/KanbanCard.jsx +23 -14
- package/kanban/client/src/components/kanban/RefineWorkItemPopup.jsx +9 -14
- package/kanban/client/src/components/kanban/RunButton.jsx +162 -0
- package/kanban/client/src/components/kanban/SeedButton.jsx +176 -0
- package/kanban/client/src/components/settings/AgentsTab.jsx +103 -75
- package/kanban/client/src/components/settings/ApiKeysTab.jsx +31 -2
- package/kanban/client/src/components/settings/CeremonyModelsTab.jsx +9 -2
- package/kanban/client/src/components/settings/CheckEditorPopup.jsx +507 -0
- package/kanban/client/src/components/settings/CostThresholdsTab.jsx +3 -2
- package/kanban/client/src/components/settings/ModelPricingTab.jsx +72 -7
- package/kanban/client/src/components/settings/OpenAIAuthSection.jsx +412 -0
- package/kanban/client/src/components/settings/SettingsModal.jsx +4 -4
- package/kanban/client/src/components/stats/CostModal.jsx +34 -3
- package/kanban/client/src/hooks/useGrouping.js +59 -0
- package/kanban/client/src/lib/api.js +118 -4
- package/kanban/client/src/lib/status-grouping.js +10 -0
- package/kanban/client/src/store/kanbanStore.js +8 -0
- package/kanban/server/index.js +23 -2
- package/kanban/server/routes/ceremony.js +153 -4
- package/kanban/server/routes/costs.js +9 -3
- package/kanban/server/routes/openai-oauth.js +366 -0
- package/kanban/server/routes/settings.js +447 -14
- package/kanban/server/routes/websocket.js +7 -2
- package/kanban/server/routes/work-items.js +141 -1
- package/kanban/server/services/CeremonyService.js +275 -24
- package/kanban/server/services/TaskRunnerService.js +261 -0
- package/kanban/server/workers/run-task-worker.js +121 -0
- package/kanban/server/workers/seed-worker.js +94 -0
- package/kanban/server/workers/sponsor-call-worker.js +14 -6
- package/kanban/server/workers/sprint-planning-worker.js +94 -12
- package/package.json +2 -3
- package/cli/agents/solver-epic-api.json +0 -15
- package/cli/agents/solver-epic-api.md +0 -39
- package/cli/agents/solver-epic-backend.json +0 -15
- package/cli/agents/solver-epic-backend.md +0 -39
- package/cli/agents/solver-epic-cloud.json +0 -15
- package/cli/agents/solver-epic-cloud.md +0 -39
- package/cli/agents/solver-epic-data.json +0 -15
- package/cli/agents/solver-epic-data.md +0 -39
- package/cli/agents/solver-epic-database.json +0 -15
- package/cli/agents/solver-epic-database.md +0 -39
- package/cli/agents/solver-epic-developer.json +0 -15
- package/cli/agents/solver-epic-developer.md +0 -39
- package/cli/agents/solver-epic-devops.json +0 -15
- package/cli/agents/solver-epic-devops.md +0 -39
- package/cli/agents/solver-epic-frontend.json +0 -15
- package/cli/agents/solver-epic-frontend.md +0 -39
- package/cli/agents/solver-epic-mobile.json +0 -15
- package/cli/agents/solver-epic-mobile.md +0 -39
- package/cli/agents/solver-epic-qa.json +0 -15
- package/cli/agents/solver-epic-qa.md +0 -39
- package/cli/agents/solver-epic-security.json +0 -15
- package/cli/agents/solver-epic-security.md +0 -39
- package/cli/agents/solver-epic-solution-architect.json +0 -15
- package/cli/agents/solver-epic-solution-architect.md +0 -39
- package/cli/agents/solver-epic-test-architect.json +0 -15
- package/cli/agents/solver-epic-test-architect.md +0 -39
- package/cli/agents/solver-epic-ui.json +0 -15
- package/cli/agents/solver-epic-ui.md +0 -39
- package/cli/agents/solver-epic-ux.json +0 -15
- package/cli/agents/solver-epic-ux.md +0 -39
- package/cli/agents/solver-story-api.json +0 -15
- package/cli/agents/solver-story-api.md +0 -39
- package/cli/agents/solver-story-backend.json +0 -15
- package/cli/agents/solver-story-backend.md +0 -39
- package/cli/agents/solver-story-cloud.json +0 -15
- package/cli/agents/solver-story-cloud.md +0 -39
- package/cli/agents/solver-story-data.json +0 -15
- package/cli/agents/solver-story-data.md +0 -39
- package/cli/agents/solver-story-database.json +0 -15
- package/cli/agents/solver-story-database.md +0 -39
- package/cli/agents/solver-story-developer.json +0 -15
- package/cli/agents/solver-story-developer.md +0 -39
- package/cli/agents/solver-story-devops.json +0 -15
- package/cli/agents/solver-story-devops.md +0 -39
- package/cli/agents/solver-story-frontend.json +0 -15
- package/cli/agents/solver-story-frontend.md +0 -39
- package/cli/agents/solver-story-mobile.json +0 -15
- package/cli/agents/solver-story-mobile.md +0 -39
- package/cli/agents/solver-story-qa.json +0 -15
- package/cli/agents/solver-story-qa.md +0 -39
- package/cli/agents/solver-story-security.json +0 -15
- package/cli/agents/solver-story-security.md +0 -39
- package/cli/agents/solver-story-solution-architect.json +0 -15
- package/cli/agents/solver-story-solution-architect.md +0 -39
- package/cli/agents/solver-story-test-architect.json +0 -15
- package/cli/agents/solver-story-test-architect.md +0 -39
- package/cli/agents/solver-story-ui.json +0 -15
- package/cli/agents/solver-story-ui.md +0 -39
- package/cli/agents/solver-story-ux.json +0 -15
- package/cli/agents/solver-story-ux.md +0 -39
- package/cli/agents/validator-epic-api.json +0 -93
- package/cli/agents/validator-epic-api.md +0 -137
- package/cli/agents/validator-epic-backend.json +0 -93
- package/cli/agents/validator-epic-backend.md +0 -130
- package/cli/agents/validator-epic-cloud.json +0 -93
- package/cli/agents/validator-epic-cloud.md +0 -137
- package/cli/agents/validator-epic-data.json +0 -93
- package/cli/agents/validator-epic-data.md +0 -130
- package/cli/agents/validator-epic-database.json +0 -93
- package/cli/agents/validator-epic-database.md +0 -137
- package/cli/agents/validator-epic-developer.json +0 -74
- package/cli/agents/validator-epic-developer.md +0 -153
- package/cli/agents/validator-epic-devops.json +0 -74
- package/cli/agents/validator-epic-devops.md +0 -153
- package/cli/agents/validator-epic-frontend.json +0 -74
- package/cli/agents/validator-epic-frontend.md +0 -153
- package/cli/agents/validator-epic-mobile.json +0 -93
- package/cli/agents/validator-epic-mobile.md +0 -130
- package/cli/agents/validator-epic-qa.json +0 -93
- package/cli/agents/validator-epic-qa.md +0 -130
- package/cli/agents/validator-epic-security.json +0 -74
- package/cli/agents/validator-epic-security.md +0 -154
- package/cli/agents/validator-epic-solution-architect.json +0 -74
- package/cli/agents/validator-epic-solution-architect.md +0 -156
- package/cli/agents/validator-epic-test-architect.json +0 -93
- package/cli/agents/validator-epic-test-architect.md +0 -130
- package/cli/agents/validator-epic-ui.json +0 -93
- package/cli/agents/validator-epic-ui.md +0 -130
- package/cli/agents/validator-epic-ux.json +0 -93
- package/cli/agents/validator-epic-ux.md +0 -130
- package/cli/agents/validator-story-api.json +0 -104
- package/cli/agents/validator-story-api.md +0 -152
- package/cli/agents/validator-story-backend.json +0 -104
- package/cli/agents/validator-story-backend.md +0 -152
- package/cli/agents/validator-story-cloud.json +0 -104
- package/cli/agents/validator-story-cloud.md +0 -152
- package/cli/agents/validator-story-data.json +0 -104
- package/cli/agents/validator-story-data.md +0 -152
- package/cli/agents/validator-story-database.json +0 -104
- package/cli/agents/validator-story-database.md +0 -152
- package/cli/agents/validator-story-developer.json +0 -104
- package/cli/agents/validator-story-developer.md +0 -152
- package/cli/agents/validator-story-devops.json +0 -104
- package/cli/agents/validator-story-devops.md +0 -152
- package/cli/agents/validator-story-frontend.json +0 -104
- package/cli/agents/validator-story-frontend.md +0 -152
- package/cli/agents/validator-story-mobile.json +0 -104
- package/cli/agents/validator-story-mobile.md +0 -152
- package/cli/agents/validator-story-qa.json +0 -104
- package/cli/agents/validator-story-qa.md +0 -152
- package/cli/agents/validator-story-security.json +0 -104
- package/cli/agents/validator-story-security.md +0 -152
- package/cli/agents/validator-story-solution-architect.json +0 -104
- package/cli/agents/validator-story-solution-architect.md +0 -152
- package/cli/agents/validator-story-test-architect.json +0 -104
- package/cli/agents/validator-story-test-architect.md +0 -152
- package/cli/agents/validator-story-ui.json +0 -104
- package/cli/agents/validator-story-ui.md +0 -152
- package/cli/agents/validator-story-ux.json +0 -104
- package/cli/agents/validator-story-ux.md +0 -152
- package/kanban/client/dist/assets/index-CiD8PS2e.js +0 -306
- package/kanban/client/dist/assets/index-nLh0m82Q.css +0 -1
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
import http from 'node:http';
|
|
6
|
+
import { exec } from 'node:child_process';
|
|
7
|
+
|
|
8
|
+
const CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
|
|
9
|
+
const AUTHORIZE_URL = 'https://auth.openai.com/oauth/authorize';
|
|
10
|
+
const TOKEN_URL = 'https://auth.openai.com/oauth/token';
|
|
11
|
+
const REDIRECT_URI = 'http://localhost:1455/auth/callback';
|
|
12
|
+
const SCOPE = 'openid profile email offline_access';
|
|
13
|
+
|
|
14
|
+
const oauthFilePath = (projectRoot) => path.join(projectRoot, '.avc', 'openai-oauth.json');
|
|
15
|
+
|
|
16
|
+
// Parse .env into key→value map
|
|
17
|
+
async function readEnv(envPath) {
|
|
18
|
+
try {
|
|
19
|
+
const lines = (await fs.readFile(envPath, 'utf8')).split('\n');
|
|
20
|
+
const map = {};
|
|
21
|
+
for (const line of lines) {
|
|
22
|
+
const m = line.match(/^([A-Z_]+)\s*=\s*(.*)$/);
|
|
23
|
+
if (m) map[m[1]] = m[2].replace(/^["']|["']$/g, '');
|
|
24
|
+
}
|
|
25
|
+
return map;
|
|
26
|
+
} catch {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Update or insert a single key in .env, preserving all other lines
|
|
32
|
+
async function upsertEnvKey(envPath, key, value) {
|
|
33
|
+
let content = '';
|
|
34
|
+
try { content = await fs.readFile(envPath, 'utf8'); } catch {}
|
|
35
|
+
const lines = content.split('\n');
|
|
36
|
+
const idx = lines.findIndex(l => l.match(new RegExp(`^${key}\\s*=`)));
|
|
37
|
+
const newLine = value ? `${key}=${value}` : '';
|
|
38
|
+
if (idx >= 0) {
|
|
39
|
+
if (newLine) {
|
|
40
|
+
lines[idx] = newLine;
|
|
41
|
+
} else {
|
|
42
|
+
lines.splice(idx, 1);
|
|
43
|
+
}
|
|
44
|
+
} else if (newLine) {
|
|
45
|
+
lines.push(newLine);
|
|
46
|
+
}
|
|
47
|
+
await fs.writeFile(envPath, lines.join('\n'), 'utf8');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Decode a JWT payload (no verification needed — we trust our own callback)
|
|
51
|
+
function decodeJwtPayload(token) {
|
|
52
|
+
try {
|
|
53
|
+
const [, payloadB64] = token.split('.');
|
|
54
|
+
const padded = payloadB64.replace(/-/g, '+').replace(/_/g, '/');
|
|
55
|
+
return JSON.parse(Buffer.from(padded, 'base64').toString('utf8'));
|
|
56
|
+
} catch {
|
|
57
|
+
return {};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Open URL in the system default browser
|
|
62
|
+
function openBrowser(url) {
|
|
63
|
+
const platform = process.platform;
|
|
64
|
+
const cmd = platform === 'darwin' ? `open "${url}"`
|
|
65
|
+
: platform === 'win32' ? `start "" "${url}"`
|
|
66
|
+
: `xdg-open "${url}"`;
|
|
67
|
+
exec(cmd, (err) => {
|
|
68
|
+
if (err) console.error('[openai-oauth] Browser open failed:', err.message);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Spin up a temporary HTTP server on port 1455 to catch the OAuth callback.
|
|
74
|
+
* Returns a Promise that resolves with the authorization code, or rejects on timeout/error.
|
|
75
|
+
*/
|
|
76
|
+
function waitForOAuthCallback(expectedState, timeoutMs = 300_000) {
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
const server = http.createServer((req, res) => {
|
|
79
|
+
const parsedUrl = new URL(req.url, 'http://localhost:1455');
|
|
80
|
+
if (parsedUrl.pathname !== '/auth/callback') {
|
|
81
|
+
res.writeHead(404);
|
|
82
|
+
res.end('Not found');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const code = parsedUrl.searchParams.get('code');
|
|
87
|
+
const state = parsedUrl.searchParams.get('state');
|
|
88
|
+
const error = parsedUrl.searchParams.get('error');
|
|
89
|
+
|
|
90
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
91
|
+
res.end('<html><body><h2>Authentication complete. You can close this tab.</h2></body></html>');
|
|
92
|
+
server.close();
|
|
93
|
+
|
|
94
|
+
if (error) return reject(new Error(`OAuth error: ${error}`));
|
|
95
|
+
if (state !== expectedState) return reject(new Error('State mismatch — possible CSRF'));
|
|
96
|
+
if (!code) return reject(new Error('No code returned from OAuth server'));
|
|
97
|
+
resolve(code);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
server.on('error', (err) => reject(err));
|
|
101
|
+
|
|
102
|
+
server.listen(1455, () => {
|
|
103
|
+
console.log('[openai-oauth] Callback server listening on http://localhost:1455');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const timer = setTimeout(() => {
|
|
107
|
+
server.close();
|
|
108
|
+
reject(new Error('OAuth login timed out after 5 minutes'));
|
|
109
|
+
}, timeoutMs);
|
|
110
|
+
|
|
111
|
+
server.on('close', () => clearTimeout(timer));
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Exchange the authorization code for tokens.
|
|
117
|
+
*/
|
|
118
|
+
async function exchangeCodeForTokens(code, codeVerifier) {
|
|
119
|
+
const body = new URLSearchParams({
|
|
120
|
+
grant_type: 'authorization_code',
|
|
121
|
+
client_id: CLIENT_ID,
|
|
122
|
+
code,
|
|
123
|
+
code_verifier: codeVerifier,
|
|
124
|
+
redirect_uri: REDIRECT_URI,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const resp = await fetch(TOKEN_URL, {
|
|
128
|
+
method: 'POST',
|
|
129
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
130
|
+
body: body.toString(),
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if (!resp.ok) {
|
|
134
|
+
const text = await resp.text();
|
|
135
|
+
throw new Error(`Token exchange failed (${resp.status}): ${text}`);
|
|
136
|
+
}
|
|
137
|
+
return resp.json();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @param {string} projectRoot
|
|
142
|
+
* @param {() => object|null} getWebSocket - lazy getter; returns the websocket object or null
|
|
143
|
+
*/
|
|
144
|
+
export function createOpenAIOAuthRouter(projectRoot, getWebSocket) {
|
|
145
|
+
const router = express.Router();
|
|
146
|
+
const envPath = path.join(projectRoot, '.env');
|
|
147
|
+
|
|
148
|
+
// POST /login — start PKCE flow
|
|
149
|
+
router.post('/login', async (req, res) => {
|
|
150
|
+
try {
|
|
151
|
+
// Generate PKCE
|
|
152
|
+
const verifier = crypto.randomBytes(32).toString('hex');
|
|
153
|
+
const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
|
|
154
|
+
const state = crypto.randomBytes(16).toString('hex');
|
|
155
|
+
|
|
156
|
+
// Build authorize URL
|
|
157
|
+
const params = new URLSearchParams({
|
|
158
|
+
response_type: 'code',
|
|
159
|
+
client_id: CLIENT_ID,
|
|
160
|
+
redirect_uri: REDIRECT_URI,
|
|
161
|
+
scope: SCOPE,
|
|
162
|
+
state,
|
|
163
|
+
code_challenge: challenge,
|
|
164
|
+
code_challenge_method: 'S256',
|
|
165
|
+
id_token_add_organizations: 'true',
|
|
166
|
+
codex_cli_simplified_flow: 'true',
|
|
167
|
+
});
|
|
168
|
+
const authorizeUrl = `${AUTHORIZE_URL}?${params.toString()}`;
|
|
169
|
+
|
|
170
|
+
// Open browser (best-effort)
|
|
171
|
+
openBrowser(authorizeUrl);
|
|
172
|
+
|
|
173
|
+
// Return immediately so the UI can show the URL
|
|
174
|
+
res.json({ status: 'pending', authorizeUrl });
|
|
175
|
+
|
|
176
|
+
// Handle callback asynchronously
|
|
177
|
+
(async () => {
|
|
178
|
+
try {
|
|
179
|
+
const code = await waitForOAuthCallback(state);
|
|
180
|
+
const tokens = await exchangeCodeForTokens(code, verifier);
|
|
181
|
+
|
|
182
|
+
// Extract accountId from JWT payload
|
|
183
|
+
const payload = decodeJwtPayload(tokens.access_token);
|
|
184
|
+
const accountId = payload['https://api.openai.com/auth']?.chatgpt_account_id ?? '';
|
|
185
|
+
|
|
186
|
+
// Persist tokens
|
|
187
|
+
const oauthData = {
|
|
188
|
+
access: tokens.access_token,
|
|
189
|
+
refresh: tokens.refresh_token,
|
|
190
|
+
expires: Date.now() + (tokens.expires_in || 3600) * 1000,
|
|
191
|
+
accountId,
|
|
192
|
+
};
|
|
193
|
+
await fs.writeFile(oauthFilePath(projectRoot), JSON.stringify(oauthData, null, 2), 'utf8');
|
|
194
|
+
|
|
195
|
+
// Set auth mode in .env
|
|
196
|
+
await upsertEnvKey(envPath, 'OPENAI_AUTH_MODE', 'oauth');
|
|
197
|
+
|
|
198
|
+
// Notify UI via WebSocket
|
|
199
|
+
const ws = getWebSocket();
|
|
200
|
+
if (ws?.broadcast) {
|
|
201
|
+
ws.broadcast({ type: 'openai-oauth-connected', accountId });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
console.log(`[openai-oauth] Connected successfully (accountId: ${accountId})`);
|
|
205
|
+
} catch (err) {
|
|
206
|
+
console.error('[openai-oauth] Login flow error:', err.message);
|
|
207
|
+
const ws = getWebSocket();
|
|
208
|
+
if (ws?.broadcast) {
|
|
209
|
+
ws.broadcast({ type: 'openai-oauth-error', error: err.message });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
})();
|
|
213
|
+
} catch (err) {
|
|
214
|
+
res.status(500).json({ error: err.message });
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// GET /status
|
|
219
|
+
router.get('/status', async (req, res) => {
|
|
220
|
+
try {
|
|
221
|
+
const [env, oauthRaw] = await Promise.all([
|
|
222
|
+
readEnv(envPath),
|
|
223
|
+
fs.readFile(oauthFilePath(projectRoot), 'utf8').catch(() => null),
|
|
224
|
+
]);
|
|
225
|
+
|
|
226
|
+
if (!oauthRaw || env.OPENAI_AUTH_MODE !== 'oauth') {
|
|
227
|
+
return res.json({ connected: false });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const { accountId, expires } = JSON.parse(oauthRaw);
|
|
231
|
+
res.json({
|
|
232
|
+
connected: true,
|
|
233
|
+
accountId,
|
|
234
|
+
expiresAt: expires,
|
|
235
|
+
expiresIn: Math.max(0, Math.round((expires - Date.now()) / 1000)),
|
|
236
|
+
fallback: env.OPENAI_OAUTH_FALLBACK === 'true',
|
|
237
|
+
});
|
|
238
|
+
} catch (err) {
|
|
239
|
+
res.status(500).json({ error: err.message });
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// POST /test — fire a minimal prompt to verify the OAuth tokens work
|
|
244
|
+
router.post('/test', async (req, res) => {
|
|
245
|
+
try {
|
|
246
|
+
const oauthRaw = await fs.readFile(oauthFilePath(projectRoot), 'utf8').catch(() => null);
|
|
247
|
+
if (!oauthRaw) return res.status(400).json({ error: 'Not connected' });
|
|
248
|
+
|
|
249
|
+
let { access, accountId, expires, refresh } = JSON.parse(oauthRaw);
|
|
250
|
+
|
|
251
|
+
// Refresh if within 60s of expiry
|
|
252
|
+
if (expires - Date.now() < 60_000) {
|
|
253
|
+
const body = new URLSearchParams({
|
|
254
|
+
grant_type: 'refresh_token',
|
|
255
|
+
client_id: CLIENT_ID,
|
|
256
|
+
refresh_token: refresh,
|
|
257
|
+
});
|
|
258
|
+
const refreshResp = await fetch(TOKEN_URL, {
|
|
259
|
+
method: 'POST',
|
|
260
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
261
|
+
body: body.toString(),
|
|
262
|
+
});
|
|
263
|
+
if (!refreshResp.ok) return res.status(401).json({ error: 'Token refresh failed' });
|
|
264
|
+
const refreshed = await refreshResp.json();
|
|
265
|
+
access = refreshed.access_token;
|
|
266
|
+
refresh = refreshed.refresh_token || refresh;
|
|
267
|
+
expires = Date.now() + (refreshed.expires_in || 3600) * 1000;
|
|
268
|
+
await fs.writeFile(oauthFilePath(projectRoot), JSON.stringify({ access, refresh, expires, accountId }, null, 2), 'utf8');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const t0 = Date.now();
|
|
272
|
+
const apiResp = await fetch('https://chatgpt.com/backend-api/codex/responses', {
|
|
273
|
+
method: 'POST',
|
|
274
|
+
headers: {
|
|
275
|
+
'Authorization': `Bearer ${access}`,
|
|
276
|
+
'chatgpt-account-id': accountId,
|
|
277
|
+
'Content-Type': 'application/json',
|
|
278
|
+
'OpenAI-Beta': 'responses=experimental',
|
|
279
|
+
'accept': 'application/json',
|
|
280
|
+
},
|
|
281
|
+
body: JSON.stringify({
|
|
282
|
+
model: 'gpt-5.2-codex',
|
|
283
|
+
instructions: 'You are a helpful assistant.',
|
|
284
|
+
input: [{ role: 'user', content: 'Reply with exactly: "AVC OAuth test OK"' }],
|
|
285
|
+
store: false,
|
|
286
|
+
stream: true,
|
|
287
|
+
}),
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
if (!apiResp.ok) {
|
|
291
|
+
const raw = await apiResp.text();
|
|
292
|
+
return res.status(502).json({ error: `API error ${apiResp.status}: ${raw}` });
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Parse SSE stream — accumulate deltas; use response.done for final text
|
|
296
|
+
const body = await apiResp.text();
|
|
297
|
+
let text = '';
|
|
298
|
+
let finalEvent = null;
|
|
299
|
+
for (const line of body.split('\n')) {
|
|
300
|
+
if (!line.startsWith('data: ')) continue;
|
|
301
|
+
const chunk = line.slice(6).trim();
|
|
302
|
+
if (chunk === '[DONE]') break;
|
|
303
|
+
try {
|
|
304
|
+
const event = JSON.parse(chunk);
|
|
305
|
+
if (event.type === 'response.output_text.delta') {
|
|
306
|
+
text += event.delta ?? '';
|
|
307
|
+
} else if (event.type === 'response.output_text.done') {
|
|
308
|
+
text = event.text ?? text;
|
|
309
|
+
} else if (event.type === 'response.done' || event.type === 'response.completed') {
|
|
310
|
+
finalEvent = event.response ?? event;
|
|
311
|
+
if (!text) {
|
|
312
|
+
text = finalEvent?.output_text ?? finalEvent?.output?.[0]?.content?.[0]?.text ?? '';
|
|
313
|
+
}
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
} catch { /* skip malformed lines */ }
|
|
317
|
+
}
|
|
318
|
+
res.json({ ok: true, response: text.trim(), model: 'gpt-5.2-codex', elapsed: Date.now() - t0 });
|
|
319
|
+
} catch (err) {
|
|
320
|
+
res.status(500).json({ error: err.message });
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// POST /fallback — enable or disable API-key fallback
|
|
325
|
+
router.post('/fallback', async (req, res) => {
|
|
326
|
+
try {
|
|
327
|
+
const { enabled } = req.body;
|
|
328
|
+
if (typeof enabled !== 'boolean') return res.status(400).json({ error: 'enabled must be boolean' });
|
|
329
|
+
|
|
330
|
+
if (enabled) {
|
|
331
|
+
// Verify the API key exists and can reach OpenAI before enabling
|
|
332
|
+
const env = await readEnv(envPath);
|
|
333
|
+
if (!env.OPENAI_API_KEY) {
|
|
334
|
+
return res.status(400).json({ error: 'OPENAI_API_KEY is not set. Add it first.' });
|
|
335
|
+
}
|
|
336
|
+
// Quick validation call
|
|
337
|
+
const verifyResp = await fetch('https://api.openai.com/v1/models', {
|
|
338
|
+
headers: { 'Authorization': `Bearer ${env.OPENAI_API_KEY}` },
|
|
339
|
+
});
|
|
340
|
+
if (!verifyResp.ok) {
|
|
341
|
+
return res.status(400).json({ error: `API key verification failed (${verifyResp.status}). Check the key.` });
|
|
342
|
+
}
|
|
343
|
+
await upsertEnvKey(envPath, 'OPENAI_OAUTH_FALLBACK', 'true');
|
|
344
|
+
} else {
|
|
345
|
+
await upsertEnvKey(envPath, 'OPENAI_OAUTH_FALLBACK', '');
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
res.json({ status: 'ok', enabled });
|
|
349
|
+
} catch (err) {
|
|
350
|
+
res.status(500).json({ error: err.message });
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// POST /logout
|
|
355
|
+
router.post('/logout', async (req, res) => {
|
|
356
|
+
try {
|
|
357
|
+
try { await fs.unlink(oauthFilePath(projectRoot)); } catch {}
|
|
358
|
+
await upsertEnvKey(envPath, 'OPENAI_AUTH_MODE', '');
|
|
359
|
+
res.json({ status: 'ok' });
|
|
360
|
+
} catch (err) {
|
|
361
|
+
res.status(500).json({ error: err.message });
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
return router;
|
|
366
|
+
}
|