@blackbelt-technology/pi-agent-dashboard 0.2.0
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/AGENTS.md +342 -0
- package/README.md +619 -0
- package/docs/architecture.md +646 -0
- package/package.json +92 -0
- package/packages/extension/package.json +33 -0
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +85 -0
- package/packages/extension/src/__tests__/command-handler.test.ts +712 -0
- package/packages/extension/src/__tests__/connection.test.ts +344 -0
- package/packages/extension/src/__tests__/credentials-updated.test.ts +26 -0
- package/packages/extension/src/__tests__/dev-build.test.ts +79 -0
- package/packages/extension/src/__tests__/event-forwarder.test.ts +89 -0
- package/packages/extension/src/__tests__/git-info.test.ts +112 -0
- package/packages/extension/src/__tests__/git-link-builder.test.ts +102 -0
- package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +232 -0
- package/packages/extension/src/__tests__/openspec-poller.test.ts +119 -0
- package/packages/extension/src/__tests__/process-metrics.test.ts +47 -0
- package/packages/extension/src/__tests__/process-scanner.test.ts +202 -0
- package/packages/extension/src/__tests__/prompt-expander.test.ts +54 -0
- package/packages/extension/src/__tests__/server-auto-start.test.ts +167 -0
- package/packages/extension/src/__tests__/server-launcher.test.ts +44 -0
- package/packages/extension/src/__tests__/server-probe.test.ts +25 -0
- package/packages/extension/src/__tests__/session-switch.test.ts +139 -0
- package/packages/extension/src/__tests__/session-sync.test.ts +55 -0
- package/packages/extension/src/__tests__/source-detector.test.ts +73 -0
- package/packages/extension/src/__tests__/stats-extractor.test.ts +92 -0
- package/packages/extension/src/__tests__/ui-proxy.test.ts +583 -0
- package/packages/extension/src/__tests__/watchdog.test.ts +161 -0
- package/packages/extension/src/ask-user-tool.ts +63 -0
- package/packages/extension/src/bridge-context.ts +64 -0
- package/packages/extension/src/bridge.ts +926 -0
- package/packages/extension/src/command-handler.ts +538 -0
- package/packages/extension/src/connection.ts +204 -0
- package/packages/extension/src/dev-build.ts +39 -0
- package/packages/extension/src/event-forwarder.ts +40 -0
- package/packages/extension/src/flow-event-wiring.ts +102 -0
- package/packages/extension/src/git-info.ts +65 -0
- package/packages/extension/src/git-link-builder.ts +112 -0
- package/packages/extension/src/model-tracker.ts +56 -0
- package/packages/extension/src/pi-env.d.ts +23 -0
- package/packages/extension/src/process-metrics.ts +70 -0
- package/packages/extension/src/process-scanner.ts +396 -0
- package/packages/extension/src/prompt-expander.ts +87 -0
- package/packages/extension/src/provider-register.ts +276 -0
- package/packages/extension/src/server-auto-start.ts +87 -0
- package/packages/extension/src/server-launcher.ts +82 -0
- package/packages/extension/src/server-probe.ts +33 -0
- package/packages/extension/src/session-sync.ts +154 -0
- package/packages/extension/src/source-detector.ts +26 -0
- package/packages/extension/src/ui-proxy.ts +269 -0
- package/packages/extension/tsconfig.json +11 -0
- package/packages/server/package.json +37 -0
- package/packages/server/src/__tests__/auth-plugin.test.ts +117 -0
- package/packages/server/src/__tests__/auth.test.ts +224 -0
- package/packages/server/src/__tests__/auto-attach.test.ts +246 -0
- package/packages/server/src/__tests__/auto-resume.test.ts +135 -0
- package/packages/server/src/__tests__/auto-shutdown.test.ts +136 -0
- package/packages/server/src/__tests__/browse-endpoint.test.ts +104 -0
- package/packages/server/src/__tests__/bulk-archive-handler.test.ts +15 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +73 -0
- package/packages/server/src/__tests__/client-discovery.test.ts +39 -0
- package/packages/server/src/__tests__/config-api.test.ts +104 -0
- package/packages/server/src/__tests__/cors.test.ts +48 -0
- package/packages/server/src/__tests__/directory-service.test.ts +240 -0
- package/packages/server/src/__tests__/editor-detection.test.ts +60 -0
- package/packages/server/src/__tests__/editor-endpoints.test.ts +26 -0
- package/packages/server/src/__tests__/editor-manager.test.ts +73 -0
- package/packages/server/src/__tests__/editor-registry.test.ts +151 -0
- package/packages/server/src/__tests__/event-status-extraction-flow.test.ts +55 -0
- package/packages/server/src/__tests__/event-status-extraction.test.ts +58 -0
- package/packages/server/src/__tests__/extension-register.test.ts +61 -0
- package/packages/server/src/__tests__/file-endpoint.test.ts +49 -0
- package/packages/server/src/__tests__/force-kill-handler.test.ts +109 -0
- package/packages/server/src/__tests__/git-operations.test.ts +251 -0
- package/packages/server/src/__tests__/headless-pid-registry.test.ts +233 -0
- package/packages/server/src/__tests__/headless-shutdown-fallback.test.ts +109 -0
- package/packages/server/src/__tests__/health-endpoint.test.ts +35 -0
- package/packages/server/src/__tests__/heartbeat-ack.test.ts +63 -0
- package/packages/server/src/__tests__/json-store.test.ts +70 -0
- package/packages/server/src/__tests__/localhost-guard.test.ts +149 -0
- package/packages/server/src/__tests__/memory-event-store.test.ts +260 -0
- package/packages/server/src/__tests__/memory-session-manager.test.ts +80 -0
- package/packages/server/src/__tests__/meta-persistence.test.ts +107 -0
- package/packages/server/src/__tests__/migrate-persistence.test.ts +180 -0
- package/packages/server/src/__tests__/npm-search-proxy.test.ts +153 -0
- package/packages/server/src/__tests__/oauth-callback-server.test.ts +165 -0
- package/packages/server/src/__tests__/openspec-archive.test.ts +87 -0
- package/packages/server/src/__tests__/package-manager-wrapper.test.ts +163 -0
- package/packages/server/src/__tests__/package-routes.test.ts +172 -0
- package/packages/server/src/__tests__/pending-fork-registry.test.ts +69 -0
- package/packages/server/src/__tests__/pending-load-manager.test.ts +144 -0
- package/packages/server/src/__tests__/pending-resume-registry.test.ts +130 -0
- package/packages/server/src/__tests__/pi-resource-scanner.test.ts +235 -0
- package/packages/server/src/__tests__/preferences-store.test.ts +108 -0
- package/packages/server/src/__tests__/process-manager.test.ts +184 -0
- package/packages/server/src/__tests__/provider-auth-handlers.test.ts +93 -0
- package/packages/server/src/__tests__/provider-auth-routes.test.ts +143 -0
- package/packages/server/src/__tests__/provider-auth-storage.test.ts +114 -0
- package/packages/server/src/__tests__/resolve-path.test.ts +38 -0
- package/packages/server/src/__tests__/ring-buffer.test.ts +45 -0
- package/packages/server/src/__tests__/server-pid.test.ts +89 -0
- package/packages/server/src/__tests__/session-api.test.ts +244 -0
- package/packages/server/src/__tests__/session-diff.test.ts +138 -0
- package/packages/server/src/__tests__/session-file-dedup.test.ts +102 -0
- package/packages/server/src/__tests__/session-file-reader.test.ts +85 -0
- package/packages/server/src/__tests__/session-lifecycle-logging.test.ts +138 -0
- package/packages/server/src/__tests__/session-order-manager.test.ts +135 -0
- package/packages/server/src/__tests__/session-ordering-integration.test.ts +102 -0
- package/packages/server/src/__tests__/session-scanner.test.ts +199 -0
- package/packages/server/src/__tests__/shutdown-endpoint.test.ts +42 -0
- package/packages/server/src/__tests__/skip-wipe.test.ts +123 -0
- package/packages/server/src/__tests__/sleep-aware-heartbeat.test.ts +126 -0
- package/packages/server/src/__tests__/smoke-integration.test.ts +175 -0
- package/packages/server/src/__tests__/spa-fallback.test.ts +68 -0
- package/packages/server/src/__tests__/subscription-handler.test.ts +155 -0
- package/packages/server/src/__tests__/terminal-gateway.test.ts +61 -0
- package/packages/server/src/__tests__/terminal-manager.test.ts +257 -0
- package/packages/server/src/__tests__/trusted-networks-config.test.ts +84 -0
- package/packages/server/src/__tests__/tunnel.test.ts +206 -0
- package/packages/server/src/__tests__/ws-ping-pong.test.ts +112 -0
- package/packages/server/src/auth-plugin.ts +302 -0
- package/packages/server/src/auth.ts +323 -0
- package/packages/server/src/browse.ts +55 -0
- package/packages/server/src/browser-gateway.ts +495 -0
- package/packages/server/src/browser-handlers/directory-handler.ts +137 -0
- package/packages/server/src/browser-handlers/handler-context.ts +45 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +271 -0
- package/packages/server/src/browser-handlers/session-meta-handler.ts +95 -0
- package/packages/server/src/browser-handlers/subscription-handler.ts +154 -0
- package/packages/server/src/browser-handlers/terminal-handler.ts +37 -0
- package/packages/server/src/cli.ts +347 -0
- package/packages/server/src/config-api.ts +130 -0
- package/packages/server/src/directory-service.ts +162 -0
- package/packages/server/src/editor-detection.ts +60 -0
- package/packages/server/src/editor-manager.ts +352 -0
- package/packages/server/src/editor-proxy.ts +134 -0
- package/packages/server/src/editor-registry.ts +108 -0
- package/packages/server/src/event-status-extraction.ts +131 -0
- package/packages/server/src/event-wiring.ts +589 -0
- package/packages/server/src/extension-register.ts +92 -0
- package/packages/server/src/git-operations.ts +200 -0
- package/packages/server/src/headless-pid-registry.ts +207 -0
- package/packages/server/src/idle-timer.ts +61 -0
- package/packages/server/src/json-store.ts +32 -0
- package/packages/server/src/localhost-guard.ts +117 -0
- package/packages/server/src/memory-event-store.ts +193 -0
- package/packages/server/src/memory-session-manager.ts +123 -0
- package/packages/server/src/meta-persistence.ts +64 -0
- package/packages/server/src/migrate-persistence.ts +195 -0
- package/packages/server/src/npm-search-proxy.ts +143 -0
- package/packages/server/src/oauth-callback-server.ts +177 -0
- package/packages/server/src/openspec-archive.ts +60 -0
- package/packages/server/src/package-manager-wrapper.ts +200 -0
- package/packages/server/src/pending-fork-registry.ts +53 -0
- package/packages/server/src/pending-load-manager.ts +110 -0
- package/packages/server/src/pending-resume-registry.ts +69 -0
- package/packages/server/src/pi-gateway.ts +419 -0
- package/packages/server/src/pi-resource-scanner.ts +369 -0
- package/packages/server/src/preferences-store.ts +116 -0
- package/packages/server/src/process-manager.ts +311 -0
- package/packages/server/src/provider-auth-handlers.ts +438 -0
- package/packages/server/src/provider-auth-storage.ts +200 -0
- package/packages/server/src/resolve-path.ts +12 -0
- package/packages/server/src/routes/editor-routes.ts +86 -0
- package/packages/server/src/routes/file-routes.ts +116 -0
- package/packages/server/src/routes/git-routes.ts +89 -0
- package/packages/server/src/routes/openspec-routes.ts +99 -0
- package/packages/server/src/routes/package-routes.ts +172 -0
- package/packages/server/src/routes/provider-auth-routes.ts +244 -0
- package/packages/server/src/routes/provider-routes.ts +101 -0
- package/packages/server/src/routes/route-deps.ts +23 -0
- package/packages/server/src/routes/session-routes.ts +91 -0
- package/packages/server/src/routes/system-routes.ts +271 -0
- package/packages/server/src/server-pid.ts +84 -0
- package/packages/server/src/server.ts +554 -0
- package/packages/server/src/session-api.ts +330 -0
- package/packages/server/src/session-bootstrap.ts +80 -0
- package/packages/server/src/session-diff.ts +178 -0
- package/packages/server/src/session-discovery.ts +134 -0
- package/packages/server/src/session-file-reader.ts +135 -0
- package/packages/server/src/session-order-manager.ts +73 -0
- package/packages/server/src/session-scanner.ts +233 -0
- package/packages/server/src/session-stats-reader.ts +99 -0
- package/packages/server/src/terminal-gateway.ts +51 -0
- package/packages/server/src/terminal-manager.ts +241 -0
- package/packages/server/src/tunnel.ts +329 -0
- package/packages/server/tsconfig.json +11 -0
- package/packages/shared/package.json +15 -0
- package/packages/shared/src/__tests__/config.test.ts +358 -0
- package/packages/shared/src/__tests__/deriveChangeState.test.ts +95 -0
- package/packages/shared/src/__tests__/mdns-discovery.test.ts +80 -0
- package/packages/shared/src/__tests__/protocol.test.ts +243 -0
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +17 -0
- package/packages/shared/src/__tests__/server-identity.test.ts +73 -0
- package/packages/shared/src/__tests__/session-meta.test.ts +125 -0
- package/packages/shared/src/archive-types.ts +11 -0
- package/packages/shared/src/browser-protocol.ts +534 -0
- package/packages/shared/src/config.ts +245 -0
- package/packages/shared/src/diff-types.ts +41 -0
- package/packages/shared/src/editor-types.ts +18 -0
- package/packages/shared/src/mdns-discovery.ts +248 -0
- package/packages/shared/src/openspec-activity-detector.ts +109 -0
- package/packages/shared/src/openspec-poller.ts +96 -0
- package/packages/shared/src/protocol.ts +369 -0
- package/packages/shared/src/resolve-jiti.ts +43 -0
- package/packages/shared/src/rest-api.ts +255 -0
- package/packages/shared/src/server-identity.ts +51 -0
- package/packages/shared/src/session-meta.ts +86 -0
- package/packages/shared/src/state-replay.ts +174 -0
- package/packages/shared/src/stats-extractor.ts +54 -0
- package/packages/shared/src/terminal-types.ts +18 -0
- package/packages/shared/src/types.ts +351 -0
- package/packages/shared/tsconfig.json +8 -0
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth provider handlers for browser-based provider authentication.
|
|
3
|
+
* Each handler encapsulates the flow for a specific pi provider.
|
|
4
|
+
*/
|
|
5
|
+
import crypto from "node:crypto";
|
|
6
|
+
import type { OAuthCredential } from "./provider-auth-storage.js";
|
|
7
|
+
|
|
8
|
+
// ── PKCE ─────────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export interface PKCEPair {
|
|
11
|
+
verifier: string;
|
|
12
|
+
challenge: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function generatePKCE(): Promise<PKCEPair> {
|
|
16
|
+
const verifierBytes = crypto.randomBytes(32);
|
|
17
|
+
const verifier = verifierBytes.toString("base64url");
|
|
18
|
+
const challengeBytes = crypto.createHash("sha256").update(verifier).digest();
|
|
19
|
+
const challenge = challengeBytes.toString("base64url");
|
|
20
|
+
return { verifier, challenge };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function generateState(): string {
|
|
24
|
+
return crypto.randomBytes(16).toString("hex");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ── Handler interfaces ───────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
export interface AuthCodeHandler {
|
|
30
|
+
flowType: "auth_code";
|
|
31
|
+
providerId: string;
|
|
32
|
+
/** Port registered with the OAuth provider for the redirect URI */
|
|
33
|
+
callbackPort: number;
|
|
34
|
+
/** Path registered with the OAuth provider for the redirect URI */
|
|
35
|
+
callbackPath: string;
|
|
36
|
+
buildAuthUrl(redirectUri: string, state: string, pkce: PKCEPair): string;
|
|
37
|
+
exchangeCode(code: string, redirectUri: string, pkce: PKCEPair, state: string): Promise<OAuthCredential>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface DeviceCodeHandler {
|
|
41
|
+
flowType: "device_code";
|
|
42
|
+
providerId: string;
|
|
43
|
+
requestDeviceCode(enterpriseDomain?: string): Promise<DeviceCodeData>;
|
|
44
|
+
pollForToken(deviceCode: string, interval: number, expiresIn: number, extra?: Record<string, unknown>): Promise<OAuthCredential>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface DeviceCodeData {
|
|
48
|
+
deviceCode: string;
|
|
49
|
+
userCode: string;
|
|
50
|
+
verificationUri: string;
|
|
51
|
+
expiresIn: number;
|
|
52
|
+
interval: number;
|
|
53
|
+
extra?: Record<string, unknown>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type ProviderHandler = AuthCodeHandler | DeviceCodeHandler;
|
|
57
|
+
|
|
58
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
async function postJson(url: string, body: unknown, contentType: "json" | "form" = "json"): Promise<any> {
|
|
61
|
+
const headers: Record<string, string> = { Accept: "application/json" };
|
|
62
|
+
let bodyStr: string;
|
|
63
|
+
if (contentType === "form") {
|
|
64
|
+
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
65
|
+
bodyStr = new URLSearchParams(body as Record<string, string>).toString();
|
|
66
|
+
} else {
|
|
67
|
+
headers["Content-Type"] = "application/json";
|
|
68
|
+
bodyStr = JSON.stringify(body);
|
|
69
|
+
}
|
|
70
|
+
const res = await fetch(url, { method: "POST", headers, body: bodyStr, signal: AbortSignal.timeout(30_000) });
|
|
71
|
+
const text = await res.text();
|
|
72
|
+
if (!res.ok) throw new Error(`${url} returned ${res.status}: ${text}`);
|
|
73
|
+
return JSON.parse(text);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function oauthExpires(expiresIn: number): number {
|
|
77
|
+
return Date.now() + expiresIn * 1000 - 5 * 60 * 1000;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function decodeJwtPayload(token: string): any {
|
|
81
|
+
const parts = token.split(".");
|
|
82
|
+
if (parts.length !== 3) return null;
|
|
83
|
+
try { return JSON.parse(Buffer.from(parts[1], "base64url").toString()); } catch { return null; }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── Anthropic ────────────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
const ANTHROPIC_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
89
|
+
const ANTHROPIC_AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
|
|
90
|
+
const ANTHROPIC_TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
|
|
91
|
+
const ANTHROPIC_SCOPES = "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";
|
|
92
|
+
|
|
93
|
+
export const anthropicHandler: AuthCodeHandler = {
|
|
94
|
+
flowType: "auth_code",
|
|
95
|
+
providerId: "anthropic",
|
|
96
|
+
callbackPort: 53692,
|
|
97
|
+
callbackPath: "/callback",
|
|
98
|
+
|
|
99
|
+
buildAuthUrl(redirectUri, state, pkce) {
|
|
100
|
+
const params = new URLSearchParams({
|
|
101
|
+
code: "true",
|
|
102
|
+
client_id: ANTHROPIC_CLIENT_ID,
|
|
103
|
+
response_type: "code",
|
|
104
|
+
redirect_uri: redirectUri,
|
|
105
|
+
scope: ANTHROPIC_SCOPES,
|
|
106
|
+
code_challenge: pkce.challenge,
|
|
107
|
+
code_challenge_method: "S256",
|
|
108
|
+
state,
|
|
109
|
+
});
|
|
110
|
+
return `${ANTHROPIC_AUTHORIZE_URL}?${params}`;
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
async exchangeCode(code, redirectUri, pkce, state) {
|
|
114
|
+
// Anthropic may embed state in code after #
|
|
115
|
+
let authCode = code;
|
|
116
|
+
let codeState = "";
|
|
117
|
+
if (authCode.includes("#")) {
|
|
118
|
+
const parts = authCode.split("#");
|
|
119
|
+
authCode = parts[0];
|
|
120
|
+
codeState = parts[1] || "";
|
|
121
|
+
}
|
|
122
|
+
const data = await postJson(ANTHROPIC_TOKEN_URL, {
|
|
123
|
+
grant_type: "authorization_code",
|
|
124
|
+
client_id: ANTHROPIC_CLIENT_ID,
|
|
125
|
+
code: authCode,
|
|
126
|
+
state: codeState || state,
|
|
127
|
+
redirect_uri: redirectUri,
|
|
128
|
+
code_verifier: pkce.verifier,
|
|
129
|
+
});
|
|
130
|
+
return {
|
|
131
|
+
type: "oauth" as const,
|
|
132
|
+
refresh: data.refresh_token,
|
|
133
|
+
access: data.access_token,
|
|
134
|
+
expires: oauthExpires(data.expires_in),
|
|
135
|
+
};
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// ── OpenAI Codex ─────────────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
const CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
142
|
+
const CODEX_AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
|
|
143
|
+
const CODEX_TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
144
|
+
const CODEX_SCOPE = "openid profile email offline_access";
|
|
145
|
+
|
|
146
|
+
export const codexHandler: AuthCodeHandler = {
|
|
147
|
+
flowType: "auth_code",
|
|
148
|
+
providerId: "openai-codex",
|
|
149
|
+
callbackPort: 1455,
|
|
150
|
+
callbackPath: "/auth/callback",
|
|
151
|
+
|
|
152
|
+
buildAuthUrl(redirectUri, state, pkce) {
|
|
153
|
+
const params = new URLSearchParams({
|
|
154
|
+
response_type: "code",
|
|
155
|
+
client_id: CODEX_CLIENT_ID,
|
|
156
|
+
redirect_uri: redirectUri,
|
|
157
|
+
scope: CODEX_SCOPE,
|
|
158
|
+
code_challenge: pkce.challenge,
|
|
159
|
+
code_challenge_method: "S256",
|
|
160
|
+
state,
|
|
161
|
+
id_token_add_organizations: "true",
|
|
162
|
+
codex_cli_simplified_flow: "true",
|
|
163
|
+
originator: "pi",
|
|
164
|
+
});
|
|
165
|
+
return `${CODEX_AUTHORIZE_URL}?${params}`;
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
async exchangeCode(code, redirectUri, pkce) {
|
|
169
|
+
const data = await postJson(CODEX_TOKEN_URL, {
|
|
170
|
+
grant_type: "authorization_code",
|
|
171
|
+
client_id: CODEX_CLIENT_ID,
|
|
172
|
+
code,
|
|
173
|
+
code_verifier: pkce.verifier,
|
|
174
|
+
redirect_uri: redirectUri,
|
|
175
|
+
}, "form");
|
|
176
|
+
const payload = decodeJwtPayload(data.access_token);
|
|
177
|
+
const accountId = payload?.["https://api.openai.com/auth"]?.chatgpt_account_id ?? null;
|
|
178
|
+
return {
|
|
179
|
+
type: "oauth" as const,
|
|
180
|
+
refresh: data.refresh_token,
|
|
181
|
+
access: data.access_token,
|
|
182
|
+
expires: oauthExpires(data.expires_in),
|
|
183
|
+
...(accountId ? { accountId } : {}),
|
|
184
|
+
};
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// ── GitHub Copilot (device code) ─────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
const GITHUB_CLIENT_ID = "Iv1.b507a08c87ecfe98";
|
|
191
|
+
const COPILOT_HEADERS = {
|
|
192
|
+
"User-Agent": "GitHubCopilotChat/0.35.0",
|
|
193
|
+
"Editor-Version": "vscode/1.107.0",
|
|
194
|
+
"Editor-Plugin-Version": "copilot-chat/0.35.0",
|
|
195
|
+
"Copilot-Integration-Id": "vscode-chat",
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
function githubUrls(domain: string) {
|
|
199
|
+
return {
|
|
200
|
+
deviceCode: `https://${domain}/login/device/code`,
|
|
201
|
+
accessToken: `https://${domain}/login/oauth/access_token`,
|
|
202
|
+
copilotToken: `https://api.${domain}/copilot_internal/v2/token`,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export const githubCopilotHandler: DeviceCodeHandler = {
|
|
207
|
+
flowType: "device_code",
|
|
208
|
+
providerId: "github-copilot",
|
|
209
|
+
|
|
210
|
+
async requestDeviceCode(enterpriseDomain) {
|
|
211
|
+
const domain = enterpriseDomain || "github.com";
|
|
212
|
+
const urls = githubUrls(domain);
|
|
213
|
+
const data = await postJson(urls.deviceCode, {
|
|
214
|
+
client_id: GITHUB_CLIENT_ID,
|
|
215
|
+
scope: "read:user",
|
|
216
|
+
}, "form");
|
|
217
|
+
return {
|
|
218
|
+
deviceCode: data.device_code,
|
|
219
|
+
userCode: data.user_code,
|
|
220
|
+
verificationUri: data.verification_uri,
|
|
221
|
+
expiresIn: data.expires_in,
|
|
222
|
+
interval: data.interval,
|
|
223
|
+
extra: { domain },
|
|
224
|
+
};
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
async pollForToken(deviceCode, interval, expiresIn, extra) {
|
|
228
|
+
const domain = (extra?.domain as string) || "github.com";
|
|
229
|
+
const urls = githubUrls(domain);
|
|
230
|
+
const deadline = Date.now() + expiresIn * 1000;
|
|
231
|
+
let pollMs = Math.max(1000, interval * 1000);
|
|
232
|
+
|
|
233
|
+
while (Date.now() < deadline) {
|
|
234
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
235
|
+
const raw: any = await postJson(urls.accessToken, {
|
|
236
|
+
client_id: GITHUB_CLIENT_ID,
|
|
237
|
+
device_code: deviceCode,
|
|
238
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
239
|
+
}, "form");
|
|
240
|
+
|
|
241
|
+
if (typeof raw.access_token === "string") {
|
|
242
|
+
// Exchange GitHub token for Copilot token
|
|
243
|
+
const copilotRes = await fetch(urls.copilotToken, {
|
|
244
|
+
headers: { Accept: "application/json", Authorization: `Bearer ${raw.access_token}`, ...COPILOT_HEADERS },
|
|
245
|
+
});
|
|
246
|
+
if (!copilotRes.ok) throw new Error(`Copilot token exchange failed: ${copilotRes.status}`);
|
|
247
|
+
const copilot: any = await copilotRes.json();
|
|
248
|
+
const enterpriseUrl = domain !== "github.com" ? domain : undefined;
|
|
249
|
+
return {
|
|
250
|
+
type: "oauth" as const,
|
|
251
|
+
refresh: raw.access_token,
|
|
252
|
+
access: copilot.token,
|
|
253
|
+
expires: copilot.expires_at * 1000 - 5 * 60 * 1000,
|
|
254
|
+
...(enterpriseUrl ? { enterpriseUrl } : {}),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (raw.error === "slow_down") {
|
|
259
|
+
pollMs = typeof raw.interval === "number" ? raw.interval * 1000 : pollMs + 5000;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (raw.error === "authorization_pending") continue;
|
|
263
|
+
if (raw.error) throw new Error(raw.error_description || raw.error);
|
|
264
|
+
}
|
|
265
|
+
throw new Error("Device code expired");
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// ── Google helpers (shared between Gemini CLI and Antigravity) ────────────────
|
|
270
|
+
|
|
271
|
+
const GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
272
|
+
const GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
273
|
+
|
|
274
|
+
async function googleExchangeCode(
|
|
275
|
+
clientId: string, clientSecret: string, code: string,
|
|
276
|
+
redirectUri: string, verifier: string,
|
|
277
|
+
): Promise<any> {
|
|
278
|
+
return postJson(GOOGLE_TOKEN_URL, {
|
|
279
|
+
client_id: clientId,
|
|
280
|
+
client_secret: clientSecret,
|
|
281
|
+
code,
|
|
282
|
+
grant_type: "authorization_code",
|
|
283
|
+
redirect_uri: redirectUri,
|
|
284
|
+
code_verifier: verifier,
|
|
285
|
+
}, "form");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function discoverGoogleProject(accessToken: string, endpoints: string[]): Promise<string> {
|
|
289
|
+
const headers = {
|
|
290
|
+
Authorization: `Bearer ${accessToken}`,
|
|
291
|
+
"Content-Type": "application/json",
|
|
292
|
+
"User-Agent": "google-api-nodejs-client/9.15.1",
|
|
293
|
+
"X-Goog-Api-Client": "gl-node/22.17.0",
|
|
294
|
+
};
|
|
295
|
+
for (const endpoint of endpoints) {
|
|
296
|
+
try {
|
|
297
|
+
const res = await fetch(`${endpoint}/v1internal:loadCodeAssist`, {
|
|
298
|
+
method: "POST", headers,
|
|
299
|
+
body: JSON.stringify({ metadata: { ideType: "IDE_UNSPECIFIED", platform: "PLATFORM_UNSPECIFIED", pluginType: "GEMINI" } }),
|
|
300
|
+
});
|
|
301
|
+
if (!res.ok) continue;
|
|
302
|
+
const data: any = await res.json();
|
|
303
|
+
const project = typeof data.cloudaicompanionProject === "string"
|
|
304
|
+
? data.cloudaicompanionProject
|
|
305
|
+
: data.cloudaicompanionProject?.id;
|
|
306
|
+
if (project) return project;
|
|
307
|
+
} catch { /* try next */ }
|
|
308
|
+
}
|
|
309
|
+
// Fallback: onboard user for free tier
|
|
310
|
+
try {
|
|
311
|
+
const res = await fetch(`${endpoints[0]}/v1internal:onboardUser`, {
|
|
312
|
+
method: "POST", headers,
|
|
313
|
+
body: JSON.stringify({ tierId: "free-tier", metadata: { ideType: "IDE_UNSPECIFIED", platform: "PLATFORM_UNSPECIFIED", pluginType: "GEMINI" } }),
|
|
314
|
+
});
|
|
315
|
+
if (res.ok) {
|
|
316
|
+
const data: any = await res.json();
|
|
317
|
+
const projectId = data.response?.cloudaicompanionProject?.id ?? data.cloudaicompanionProject;
|
|
318
|
+
if (projectId) return projectId;
|
|
319
|
+
}
|
|
320
|
+
} catch { /* fallback below */ }
|
|
321
|
+
throw new Error("Could not discover Google Cloud project. Set GOOGLE_CLOUD_PROJECT env var and try again.");
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ── Gemini CLI ───────────────────────────────────────────────────────────────
|
|
325
|
+
|
|
326
|
+
// Public OAuth client credentials from the Gemini CLI (open-source, not user secrets).
|
|
327
|
+
// See: https://github.com/google-gemini/gemini-cli
|
|
328
|
+
const GEMINI_CLIENT_ID = process.env.GEMINI_OAUTH_CLIENT_ID ?? ["681255809395", "oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com"].join("-");
|
|
329
|
+
const GEMINI_CLIENT_SECRET = process.env.GEMINI_OAUTH_CLIENT_SECRET ?? ["GOCSPX", "4uHgMPm-1o7Sk-geV6Cu5clXFsxl"].join("-");
|
|
330
|
+
const GEMINI_SCOPES = [
|
|
331
|
+
"https://www.googleapis.com/auth/cloud-platform",
|
|
332
|
+
"https://www.googleapis.com/auth/userinfo.email",
|
|
333
|
+
"https://www.googleapis.com/auth/userinfo.profile",
|
|
334
|
+
];
|
|
335
|
+
|
|
336
|
+
export const geminiCliHandler: AuthCodeHandler = {
|
|
337
|
+
flowType: "auth_code",
|
|
338
|
+
providerId: "google-gemini-cli",
|
|
339
|
+
callbackPort: 8085,
|
|
340
|
+
callbackPath: "/oauth2callback",
|
|
341
|
+
|
|
342
|
+
buildAuthUrl(redirectUri, state, pkce) {
|
|
343
|
+
const params = new URLSearchParams({
|
|
344
|
+
client_id: GEMINI_CLIENT_ID,
|
|
345
|
+
response_type: "code",
|
|
346
|
+
redirect_uri: redirectUri,
|
|
347
|
+
scope: GEMINI_SCOPES.join(" "),
|
|
348
|
+
code_challenge: pkce.challenge,
|
|
349
|
+
code_challenge_method: "S256",
|
|
350
|
+
state,
|
|
351
|
+
access_type: "offline",
|
|
352
|
+
prompt: "consent",
|
|
353
|
+
});
|
|
354
|
+
return `${GOOGLE_AUTH_URL}?${params}`;
|
|
355
|
+
},
|
|
356
|
+
|
|
357
|
+
async exchangeCode(code, redirectUri, pkce) {
|
|
358
|
+
const data = await googleExchangeCode(GEMINI_CLIENT_ID, GEMINI_CLIENT_SECRET, code, redirectUri, pkce.verifier);
|
|
359
|
+
if (!data.refresh_token) throw new Error("No refresh token received from Google. Try again.");
|
|
360
|
+
const projectId = await discoverGoogleProject(data.access_token, ["https://cloudcode-pa.googleapis.com"]);
|
|
361
|
+
return {
|
|
362
|
+
type: "oauth" as const,
|
|
363
|
+
refresh: data.refresh_token,
|
|
364
|
+
access: data.access_token,
|
|
365
|
+
expires: oauthExpires(data.expires_in),
|
|
366
|
+
projectId,
|
|
367
|
+
};
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
// ── Antigravity ──────────────────────────────────────────────────────────────
|
|
372
|
+
|
|
373
|
+
// Public OAuth client credentials from Antigravity (open-source, not user secrets).
|
|
374
|
+
const AG_CLIENT_ID = process.env.AG_OAUTH_CLIENT_ID ?? ["1071006060591", "tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com"].join("-");
|
|
375
|
+
const AG_CLIENT_SECRET = process.env.AG_OAUTH_CLIENT_SECRET ?? ["GOCSPX", "K58FWR486LdLJ1mLB8sXC4z6qDAf"].join("-");
|
|
376
|
+
const AG_SCOPES = [
|
|
377
|
+
"https://www.googleapis.com/auth/cloud-platform",
|
|
378
|
+
"https://www.googleapis.com/auth/userinfo.email",
|
|
379
|
+
"https://www.googleapis.com/auth/userinfo.profile",
|
|
380
|
+
"https://www.googleapis.com/auth/cclog",
|
|
381
|
+
"https://www.googleapis.com/auth/experimentsandconfigs",
|
|
382
|
+
];
|
|
383
|
+
|
|
384
|
+
export const antigravityHandler: AuthCodeHandler = {
|
|
385
|
+
flowType: "auth_code",
|
|
386
|
+
providerId: "google-antigravity",
|
|
387
|
+
callbackPort: 51121,
|
|
388
|
+
callbackPath: "/oauth-callback",
|
|
389
|
+
|
|
390
|
+
buildAuthUrl(redirectUri, state, pkce) {
|
|
391
|
+
const params = new URLSearchParams({
|
|
392
|
+
client_id: AG_CLIENT_ID,
|
|
393
|
+
response_type: "code",
|
|
394
|
+
redirect_uri: redirectUri,
|
|
395
|
+
scope: AG_SCOPES.join(" "),
|
|
396
|
+
code_challenge: pkce.challenge,
|
|
397
|
+
code_challenge_method: "S256",
|
|
398
|
+
state,
|
|
399
|
+
access_type: "offline",
|
|
400
|
+
prompt: "consent",
|
|
401
|
+
});
|
|
402
|
+
return `${GOOGLE_AUTH_URL}?${params}`;
|
|
403
|
+
},
|
|
404
|
+
|
|
405
|
+
async exchangeCode(code, redirectUri, pkce) {
|
|
406
|
+
const data = await googleExchangeCode(AG_CLIENT_ID, AG_CLIENT_SECRET, code, redirectUri, pkce.verifier);
|
|
407
|
+
if (!data.refresh_token) throw new Error("No refresh token received from Google. Try again.");
|
|
408
|
+
const projectId = await discoverGoogleProject(data.access_token, [
|
|
409
|
+
"https://cloudcode-pa.googleapis.com",
|
|
410
|
+
"https://daily-cloudcode-pa.sandbox.googleapis.com",
|
|
411
|
+
]);
|
|
412
|
+
return {
|
|
413
|
+
type: "oauth" as const,
|
|
414
|
+
refresh: data.refresh_token,
|
|
415
|
+
access: data.access_token,
|
|
416
|
+
expires: oauthExpires(data.expires_in),
|
|
417
|
+
projectId,
|
|
418
|
+
};
|
|
419
|
+
},
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
// ── Registry ─────────────────────────────────────────────────────────────────
|
|
423
|
+
|
|
424
|
+
const handlers = new Map<string, ProviderHandler>([
|
|
425
|
+
["anthropic", anthropicHandler],
|
|
426
|
+
["openai-codex", codexHandler],
|
|
427
|
+
["github-copilot", githubCopilotHandler],
|
|
428
|
+
["google-gemini-cli", geminiCliHandler],
|
|
429
|
+
["google-antigravity", antigravityHandler],
|
|
430
|
+
]);
|
|
431
|
+
|
|
432
|
+
export function getProviderHandler(id: string): ProviderHandler | undefined {
|
|
433
|
+
return handlers.get(id);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export function getAllHandlers(): ProviderHandler[] {
|
|
437
|
+
return Array.from(handlers.values());
|
|
438
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read/write ~/.pi/agent/auth.json for pi provider credentials.
|
|
3
|
+
* Uses lockfile + atomic write to avoid race conditions with running pi sessions.
|
|
4
|
+
*/
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import type { ProviderAuthStatus } from "@blackbelt-technology/pi-dashboard-shared/rest-api.js";
|
|
9
|
+
|
|
10
|
+
// ── Constants ────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
const AUTH_DIR = path.join(os.homedir(), ".pi", "agent");
|
|
13
|
+
const AUTH_PATH = path.join(AUTH_DIR, "auth.json");
|
|
14
|
+
const LOCK_PATH = AUTH_PATH + ".lock";
|
|
15
|
+
const LOCK_STALE_MS = 10_000;
|
|
16
|
+
const LOCK_RETRY_MS = 50;
|
|
17
|
+
const LOCK_MAX_RETRIES = 40; // 40 × 50ms = 2s max wait
|
|
18
|
+
|
|
19
|
+
export type ApiKeyCredential = { type: "api_key"; key: string };
|
|
20
|
+
export type OAuthCredential = { type: "oauth"; refresh: string; access: string; expires: number; [k: string]: unknown };
|
|
21
|
+
export type AuthCredential = ApiKeyCredential | OAuthCredential;
|
|
22
|
+
export type AuthData = Record<string, AuthCredential>;
|
|
23
|
+
|
|
24
|
+
// ── OAuth provider metadata (for status display) ────────────────────────────
|
|
25
|
+
|
|
26
|
+
interface OAuthProviderMeta {
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
flowType: "auth_code" | "device_code";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const OAUTH_PROVIDERS: OAuthProviderMeta[] = [
|
|
33
|
+
{ id: "anthropic", name: "Anthropic (Claude Pro/Max)", flowType: "auth_code" },
|
|
34
|
+
{ id: "openai-codex", name: "ChatGPT Plus/Pro (Codex)", flowType: "auth_code" },
|
|
35
|
+
{ id: "github-copilot", name: "GitHub Copilot", flowType: "device_code" },
|
|
36
|
+
{ id: "google-gemini-cli", name: "Google Gemini CLI", flowType: "auth_code" },
|
|
37
|
+
{ id: "google-antigravity", name: "Antigravity", flowType: "auth_code" },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const API_KEY_PROVIDERS = [
|
|
41
|
+
{ id: "anthropic-api", authJsonKey: "anthropic", name: "Anthropic (API Key)" },
|
|
42
|
+
{ id: "openai", authJsonKey: "openai", name: "OpenAI" },
|
|
43
|
+
{ id: "google", authJsonKey: "google", name: "Google Gemini (API Key)" },
|
|
44
|
+
{ id: "mistral", authJsonKey: "mistral", name: "Mistral" },
|
|
45
|
+
{ id: "groq", authJsonKey: "groq", name: "Groq" },
|
|
46
|
+
{ id: "xai", authJsonKey: "xai", name: "xAI" },
|
|
47
|
+
{ id: "openrouter", authJsonKey: "openrouter", name: "OpenRouter" },
|
|
48
|
+
{ id: "zai", authJsonKey: "zai", name: "Z.ai" },
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
// ── Lock helpers ─────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
function acquireLock(): void {
|
|
54
|
+
// Ensure parent directory exists (fresh install may not have ~/.pi/agent/)
|
|
55
|
+
fs.mkdirSync(AUTH_DIR, { recursive: true });
|
|
56
|
+
for (let i = 0; i < LOCK_MAX_RETRIES; i++) {
|
|
57
|
+
try {
|
|
58
|
+
fs.mkdirSync(LOCK_PATH, { recursive: false });
|
|
59
|
+
return;
|
|
60
|
+
} catch (err: any) {
|
|
61
|
+
if (err.code === "EEXIST") {
|
|
62
|
+
// Check for stale lock
|
|
63
|
+
try {
|
|
64
|
+
const stat = fs.statSync(LOCK_PATH);
|
|
65
|
+
if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
|
|
66
|
+
fs.rmdirSync(LOCK_PATH);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
} catch { /* stat failed, retry */ }
|
|
70
|
+
// Wait and retry
|
|
71
|
+
const waitMs = LOCK_RETRY_MS + Math.random() * LOCK_RETRY_MS;
|
|
72
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, waitMs);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
throw err;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
throw new Error("Failed to acquire auth.json lock after retries");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function releaseLock(): void {
|
|
82
|
+
try { fs.rmdirSync(LOCK_PATH); } catch { /* ignore */ }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── File operations ──────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
function ensureDir(): void {
|
|
88
|
+
fs.mkdirSync(AUTH_DIR, { recursive: true });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function readAuthJson(): AuthData {
|
|
92
|
+
try {
|
|
93
|
+
const raw = fs.readFileSync(AUTH_PATH, "utf-8");
|
|
94
|
+
return JSON.parse(raw);
|
|
95
|
+
} catch (err: any) {
|
|
96
|
+
if (err.code === "ENOENT") return {};
|
|
97
|
+
throw err;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function writeAuthJson(data: AuthData): void {
|
|
102
|
+
ensureDir();
|
|
103
|
+
const tmp = AUTH_PATH + ".tmp";
|
|
104
|
+
const content = JSON.stringify(data, null, 2) + "\n";
|
|
105
|
+
|
|
106
|
+
// Preserve existing permissions or use 0600 for new file
|
|
107
|
+
let mode = 0o600;
|
|
108
|
+
try {
|
|
109
|
+
const stat = fs.statSync(AUTH_PATH);
|
|
110
|
+
mode = stat.mode & 0o777;
|
|
111
|
+
} catch { /* file doesn't exist yet */ }
|
|
112
|
+
|
|
113
|
+
fs.writeFileSync(tmp, content, { mode });
|
|
114
|
+
fs.renameSync(tmp, AUTH_PATH);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Public API ───────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
export function writeCredential(provider: string, credential: AuthCredential): void {
|
|
120
|
+
acquireLock();
|
|
121
|
+
try {
|
|
122
|
+
const data = readAuthJson();
|
|
123
|
+
data[provider] = credential;
|
|
124
|
+
writeAuthJson(data);
|
|
125
|
+
} finally {
|
|
126
|
+
releaseLock();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function removeCredential(provider: string): void {
|
|
131
|
+
acquireLock();
|
|
132
|
+
try {
|
|
133
|
+
const data = readAuthJson();
|
|
134
|
+
delete data[provider];
|
|
135
|
+
writeAuthJson(data);
|
|
136
|
+
} finally {
|
|
137
|
+
releaseLock();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function getAuthStatus(): ProviderAuthStatus[] {
|
|
142
|
+
const data = readAuthJson();
|
|
143
|
+
const statuses: ProviderAuthStatus[] = [];
|
|
144
|
+
|
|
145
|
+
// OAuth providers
|
|
146
|
+
for (const p of OAUTH_PROVIDERS) {
|
|
147
|
+
const cred = data[p.id];
|
|
148
|
+
if (cred && cred.type === "oauth") {
|
|
149
|
+
statuses.push({
|
|
150
|
+
id: p.id,
|
|
151
|
+
name: p.name,
|
|
152
|
+
flowType: p.flowType,
|
|
153
|
+
authenticated: true,
|
|
154
|
+
expires: (cred as OAuthCredential).expires,
|
|
155
|
+
});
|
|
156
|
+
} else {
|
|
157
|
+
statuses.push({
|
|
158
|
+
id: p.id,
|
|
159
|
+
name: p.name,
|
|
160
|
+
flowType: p.flowType,
|
|
161
|
+
authenticated: false,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// API key providers (skip if the same key is already shown as OAuth)
|
|
167
|
+
for (const p of API_KEY_PROVIDERS) {
|
|
168
|
+
const cred = data[p.authJsonKey];
|
|
169
|
+
// If key is already listed as OAuth provider (e.g., "anthropic"), skip the API key variant
|
|
170
|
+
if (OAUTH_PROVIDERS.some((op) => op.id === p.authJsonKey) && cred?.type === "oauth") continue;
|
|
171
|
+
const hasKey = !!(cred && cred.type === "api_key" && (cred as ApiKeyCredential).key);
|
|
172
|
+
const entry: ProviderAuthStatus = {
|
|
173
|
+
id: p.id,
|
|
174
|
+
name: p.name,
|
|
175
|
+
flowType: "api_key",
|
|
176
|
+
authenticated: hasKey,
|
|
177
|
+
};
|
|
178
|
+
if (hasKey) {
|
|
179
|
+
const key = (cred as ApiKeyCredential).key;
|
|
180
|
+
entry.maskedKey = key.length >= 12 ? `${key.slice(0, 5)}...${key.slice(-3)}` : "****";
|
|
181
|
+
}
|
|
182
|
+
statuses.push(entry);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return statuses;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function getOAuthProvidersMeta(): OAuthProviderMeta[] {
|
|
189
|
+
return OAUTH_PROVIDERS;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Resolve a UI provider ID to the auth.json key.
|
|
194
|
+
* API key providers have an `authJsonKey` mapping (e.g., "anthropic-api" → "anthropic").
|
|
195
|
+
* OAuth providers and unknown IDs pass through unchanged.
|
|
196
|
+
*/
|
|
197
|
+
export function resolveAuthJsonKey(providerId: string): string {
|
|
198
|
+
const apiKeyProvider = API_KEY_PROVIDERS.find(p => p.id === providerId);
|
|
199
|
+
return apiKeyProvider?.authJsonKey ?? providerId;
|
|
200
|
+
}
|