@jsonstudio/rcc 0.89.2202 → 0.90.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/README.md +27 -0
- package/dist/build-info.js +2 -2
- package/dist/build-info.js.map +1 -1
- package/dist/cli/commands/claude.js +1 -0
- package/dist/cli/commands/claude.js.map +1 -1
- package/dist/cli/commands/codex.js +1 -0
- package/dist/cli/commands/codex.js.map +1 -1
- package/dist/cli/commands/guardian-daemon.d.ts +2 -0
- package/dist/cli/commands/guardian-daemon.js +299 -0
- package/dist/cli/commands/guardian-daemon.js.map +1 -0
- package/dist/cli/commands/init/camoufox.js +1 -1
- package/dist/cli/commands/init/camoufox.js.map +1 -1
- package/dist/cli/commands/init.js +15 -1
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/launcher/types.d.ts +6 -0
- package/dist/cli/commands/launcher-kernel.js +456 -109
- package/dist/cli/commands/launcher-kernel.js.map +1 -1
- package/dist/cli/commands/port.js +28 -8
- package/dist/cli/commands/port.js.map +1 -1
- package/dist/cli/commands/restart.d.ts +4 -0
- package/dist/cli/commands/restart.js +91 -42
- package/dist/cli/commands/restart.js.map +1 -1
- package/dist/cli/commands/start-types.d.ts +4 -0
- package/dist/cli/commands/start.js +112 -68
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/cli/commands/stop.d.ts +3 -0
- package/dist/cli/commands/stop.js +30 -63
- package/dist/cli/commands/stop.js.map +1 -1
- package/dist/cli/config/init-config.js +15 -1
- package/dist/cli/config/init-config.js.map +1 -1
- package/dist/cli/config/init-provider-catalog.js +13 -5
- package/dist/cli/config/init-provider-catalog.js.map +1 -1
- package/dist/cli/guardian/client.d.ts +38 -0
- package/dist/cli/guardian/client.js +237 -0
- package/dist/cli/guardian/client.js.map +1 -0
- package/dist/cli/guardian/paths.d.ts +7 -0
- package/dist/cli/guardian/paths.js +13 -0
- package/dist/cli/guardian/paths.js.map +1 -0
- package/dist/cli/guardian/types.d.ts +30 -0
- package/dist/cli/guardian/types.js +2 -0
- package/dist/cli/guardian/types.js.map +1 -0
- package/dist/cli/register/guardian-daemon-command.d.ts +2 -0
- package/dist/cli/register/guardian-daemon-command.js +5 -0
- package/dist/cli/register/guardian-daemon-command.js.map +1 -0
- package/dist/cli/server/port-utils.js +57 -1
- package/dist/cli/server/port-utils.js.map +1 -1
- package/dist/cli.js +48 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/oauth.js +6 -6
- package/dist/commands/oauth.js.map +1 -1
- package/dist/commands/provider-update.js +12 -0
- package/dist/commands/provider-update.js.map +1 -1
- package/dist/config/routecodex-config-loader.js +66 -1
- package/dist/config/routecodex-config-loader.js.map +1 -1
- package/dist/config/virtual-router-builder.js +18 -0
- package/dist/config/virtual-router-builder.js.map +1 -1
- package/dist/config/virtual-router-types.js +20 -5
- package/dist/config/virtual-router-types.js.map +1 -1
- package/dist/daemon-admin-ui/assets/index-C8vP_c5E.js +15 -0
- package/dist/daemon-admin-ui/assets/index-DjIoHmNv.css +1 -0
- package/dist/daemon-admin-ui/index.html +13 -0
- package/dist/docs/daemon-admin-ui.html +328 -57
- package/dist/index.d.ts +9 -0
- package/dist/index.js +268 -10
- package/dist/index.js.map +1 -1
- package/dist/manager/modules/quota/provider-quota-daemon.error-helpers.d.ts +1 -0
- package/dist/manager/modules/quota/provider-quota-daemon.error-helpers.js +36 -0
- package/dist/manager/modules/quota/provider-quota-daemon.error-helpers.js.map +1 -1
- package/dist/manager/modules/quota/provider-quota-daemon.events.js +50 -1
- package/dist/manager/modules/quota/provider-quota-daemon.events.js.map +1 -1
- package/dist/providers/auth/antigravity-user-agent.js +78 -31
- package/dist/providers/auth/antigravity-user-agent.js.map +1 -1
- package/dist/providers/auth/gemini-cli-userinfo-helper.js +94 -63
- package/dist/providers/auth/gemini-cli-userinfo-helper.js.map +1 -1
- package/dist/providers/auth/iflow-userinfo-helper.js +1 -1
- package/dist/providers/auth/iflow-userinfo-helper.js.map +1 -1
- package/dist/providers/auth/oauth-error-message.d.ts +1 -0
- package/dist/providers/auth/oauth-error-message.js +44 -0
- package/dist/providers/auth/oauth-error-message.js.map +1 -0
- package/dist/providers/auth/oauth-lifecycle/error-detection.js +42 -8
- package/dist/providers/auth/oauth-lifecycle/error-detection.js.map +1 -1
- package/dist/providers/auth/oauth-lifecycle/token-io.d.ts +1 -0
- package/dist/providers/auth/oauth-lifecycle/token-io.js +12 -0
- package/dist/providers/auth/oauth-lifecycle/token-io.js.map +1 -1
- package/dist/providers/auth/oauth-lifecycle.js +502 -87
- package/dist/providers/auth/oauth-lifecycle.js.map +1 -1
- package/dist/providers/auth/oauth-repair-cooldown.js +2 -7
- package/dist/providers/auth/oauth-repair-cooldown.js.map +1 -1
- package/dist/providers/auth/oauth-repair-env.js +3 -5
- package/dist/providers/auth/oauth-repair-env.js.map +1 -1
- package/dist/providers/auth/oauth-utils/error-extraction.js +42 -8
- package/dist/providers/auth/oauth-utils/error-extraction.js.map +1 -1
- package/dist/providers/auth/qwen-userinfo-helper.js +18 -3
- package/dist/providers/auth/qwen-userinfo-helper.js.map +1 -1
- package/dist/providers/auth/tokenfile-auth.d.ts +1 -0
- package/dist/providers/auth/tokenfile-auth.js +15 -9
- package/dist/providers/auth/tokenfile-auth.js.map +1 -1
- package/dist/providers/core/config/camoufox-actions.d.ts +31 -0
- package/dist/providers/core/config/camoufox-actions.js +461 -0
- package/dist/providers/core/config/camoufox-actions.js.map +1 -0
- package/dist/providers/core/config/camoufox-launcher.d.ts +3 -0
- package/dist/providers/core/config/camoufox-launcher.js +518 -160
- package/dist/providers/core/config/camoufox-launcher.js.map +1 -1
- package/dist/providers/core/config/oauth-flows.js +6 -44
- package/dist/providers/core/config/oauth-flows.js.map +1 -1
- package/dist/providers/core/config/provider-oauth-configs.js +51 -7
- package/dist/providers/core/config/provider-oauth-configs.js.map +1 -1
- package/dist/providers/core/config/service-profiles.js +13 -4
- package/dist/providers/core/config/service-profiles.js.map +1 -1
- package/dist/providers/core/runtime/http-transport-provider.d.ts +1 -0
- package/dist/providers/core/runtime/http-transport-provider.js +60 -1
- package/dist/providers/core/runtime/http-transport-provider.js.map +1 -1
- package/dist/providers/core/runtime/provider-error-classifier.js +32 -15
- package/dist/providers/core/runtime/provider-error-classifier.js.map +1 -1
- package/dist/providers/core/runtime/provider-family-profile-utils.js +1 -1
- package/dist/providers/core/runtime/provider-family-profile-utils.js.map +1 -1
- package/dist/providers/core/runtime/provider-response-postprocessor.js +61 -14
- package/dist/providers/core/runtime/provider-response-postprocessor.js.map +1 -1
- package/dist/providers/core/strategies/oauth-auth-code-flow.d.ts +1 -0
- package/dist/providers/core/strategies/oauth-auth-code-flow.js +124 -19
- package/dist/providers/core/strategies/oauth-auth-code-flow.js.map +1 -1
- package/dist/providers/core/strategies/oauth-device-flow.js +6 -3
- package/dist/providers/core/strategies/oauth-device-flow.js.map +1 -1
- package/dist/providers/profile/families/iflow-profile.js +83 -10
- package/dist/providers/profile/families/iflow-profile.js.map +1 -1
- package/dist/providers/profile/families/qwen-profile.js +203 -0
- package/dist/providers/profile/families/qwen-profile.js.map +1 -1
- package/dist/scripts/camoufox/launch-auth.mjs +112 -5
- package/dist/server/handlers/config-admin-handler.js +9 -2
- package/dist/server/handlers/config-admin-handler.js.map +1 -1
- package/dist/server/handlers/handler-utils.js +3 -14
- package/dist/server/handlers/handler-utils.js.map +1 -1
- package/dist/server/handlers/logging.js +3 -4
- package/dist/server/handlers/logging.js.map +1 -1
- package/dist/server/runtime/http-server/clock-client-reaper.d.ts +1 -0
- package/dist/server/runtime/http-server/clock-client-reaper.js +21 -15
- package/dist/server/runtime/http-server/clock-client-reaper.js.map +1 -1
- package/dist/server/runtime/http-server/clock-client-registry-utils.d.ts +4 -0
- package/dist/server/runtime/http-server/clock-client-registry-utils.js +74 -16
- package/dist/server/runtime/http-server/clock-client-registry-utils.js.map +1 -1
- package/dist/server/runtime/http-server/clock-client-registry.d.ts +15 -0
- package/dist/server/runtime/http-server/clock-client-registry.js +300 -6
- package/dist/server/runtime/http-server/clock-client-registry.js.map +1 -1
- package/dist/server/runtime/http-server/clock-client-routes.js +49 -19
- package/dist/server/runtime/http-server/clock-client-routes.js.map +1 -1
- package/dist/server/runtime/http-server/clock-daemon-log-throttle.d.ts +16 -0
- package/dist/server/runtime/http-server/clock-daemon-log-throttle.js +49 -0
- package/dist/server/runtime/http-server/clock-daemon-log-throttle.js.map +1 -1
- package/dist/server/runtime/http-server/clock-scope-resolution.d.ts +14 -0
- package/dist/server/runtime/http-server/clock-scope-resolution.js +212 -0
- package/dist/server/runtime/http-server/clock-scope-resolution.js.map +1 -0
- package/dist/server/runtime/http-server/daemon-admin/auth-handler.js +5 -3
- package/dist/server/runtime/http-server/daemon-admin/auth-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/control-handler.js +104 -15
- package/dist/server/runtime/http-server/daemon-admin/control-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js +2 -2
- package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/providers-handler-routing-utils.d.ts +24 -0
- package/dist/server/runtime/http-server/daemon-admin/providers-handler-routing-utils.js +316 -70
- package/dist/server/runtime/http-server/daemon-admin/providers-handler-routing-utils.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/providers-handler.js +190 -1
- package/dist/server/runtime/http-server/daemon-admin/providers-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/routing-policy.js +18 -29
- package/dist/server/runtime/http-server/daemon-admin/routing-policy.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/stats-handler.js +2 -0
- package/dist/server/runtime/http-server/daemon-admin/stats-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin-routes.d.ts +8 -1
- package/dist/server/runtime/http-server/daemon-admin-routes.js +30 -0
- package/dist/server/runtime/http-server/daemon-admin-routes.js.map +1 -1
- package/dist/server/runtime/http-server/executor/client-injection-flow.d.ts +14 -0
- package/dist/server/runtime/http-server/executor/client-injection-flow.js +287 -0
- package/dist/server/runtime/http-server/executor/client-injection-flow.js.map +1 -0
- package/dist/server/runtime/http-server/executor/index.d.ts +1 -1
- package/dist/server/runtime/http-server/executor/index.js +1 -1
- package/dist/server/runtime/http-server/executor/index.js.map +1 -1
- package/dist/server/runtime/http-server/executor/provider-response-converter.js +236 -62
- package/dist/server/runtime/http-server/executor/provider-response-converter.js.map +1 -1
- package/dist/server/runtime/http-server/executor/provider-response-utils.js +5 -0
- package/dist/server/runtime/http-server/executor/provider-response-utils.js.map +1 -1
- package/dist/server/runtime/http-server/executor/request-executor-core-utils.d.ts +2 -0
- package/dist/server/runtime/http-server/executor/request-executor-core-utils.js +60 -0
- package/dist/server/runtime/http-server/executor/request-executor-core-utils.js.map +1 -1
- package/dist/server/runtime/http-server/executor/request-retry-helpers.js +20 -8
- package/dist/server/runtime/http-server/executor/request-retry-helpers.js.map +1 -1
- package/dist/server/runtime/http-server/executor/sse-error-handler.d.ts +1 -0
- package/dist/server/runtime/http-server/executor/sse-error-handler.js +13 -2
- package/dist/server/runtime/http-server/executor/sse-error-handler.js.map +1 -1
- package/dist/server/runtime/http-server/executor/usage-aggregator.d.ts +0 -12
- package/dist/server/runtime/http-server/executor/usage-aggregator.js +84 -88
- package/dist/server/runtime/http-server/executor/usage-aggregator.js.map +1 -1
- package/dist/server/runtime/http-server/executor-metadata.js +328 -7
- package/dist/server/runtime/http-server/executor-metadata.js.map +1 -1
- package/dist/server/runtime/http-server/executor-response.d.ts +1 -0
- package/dist/server/runtime/http-server/executor-response.js +52 -50
- package/dist/server/runtime/http-server/executor-response.js.map +1 -1
- package/dist/server/runtime/http-server/http-server-bootstrap.js +55 -6
- package/dist/server/runtime/http-server/http-server-bootstrap.js.map +1 -1
- package/dist/server/runtime/http-server/http-server-clock-daemon.d.ts +1 -0
- package/dist/server/runtime/http-server/http-server-clock-daemon.js +199 -44
- package/dist/server/runtime/http-server/http-server-clock-daemon.js.map +1 -1
- package/dist/server/runtime/http-server/http-server-lifecycle.js +4 -4
- package/dist/server/runtime/http-server/http-server-lifecycle.js.map +1 -1
- package/dist/server/runtime/http-server/hub-shadow-compare.js +1 -1
- package/dist/server/runtime/http-server/hub-shadow-compare.js.map +1 -1
- package/dist/server/runtime/http-server/index.d.ts +1 -0
- package/dist/server/runtime/http-server/index.js +1 -0
- package/dist/server/runtime/http-server/index.js.map +1 -1
- package/dist/server/runtime/http-server/middleware.js +82 -4
- package/dist/server/runtime/http-server/middleware.js.map +1 -1
- package/dist/server/runtime/http-server/request-executor.js +26 -7
- package/dist/server/runtime/http-server/request-executor.js.map +1 -1
- package/dist/server/runtime/http-server/routes.d.ts +2 -1
- package/dist/server/runtime/http-server/routes.js +4 -2
- package/dist/server/runtime/http-server/routes.js.map +1 -1
- package/dist/server/runtime/http-server/session-dir.js +12 -1
- package/dist/server/runtime/http-server/session-dir.js.map +1 -1
- package/dist/server/runtime/http-server/stats-manager.d.ts +35 -0
- package/dist/server/runtime/http-server/stats-manager.js +269 -21
- package/dist/server/runtime/http-server/stats-manager.js.map +1 -1
- package/dist/server/runtime/http-server/stopmessage-scope-rebind.d.ts +13 -0
- package/dist/server/runtime/http-server/stopmessage-scope-rebind.js +168 -0
- package/dist/server/runtime/http-server/stopmessage-scope-rebind.js.map +1 -0
- package/dist/server/runtime/http-server/tmux-session-probe.d.ts +10 -0
- package/dist/server/runtime/http-server/tmux-session-probe.js +97 -0
- package/dist/server/runtime/http-server/tmux-session-probe.js.map +1 -1
- package/dist/server-lifecycle/port-utils.d.ts +2 -1
- package/dist/server-lifecycle/port-utils.js +84 -4
- package/dist/server-lifecycle/port-utils.js.map +1 -1
- package/dist/token-daemon/index.d.ts +1 -0
- package/dist/token-daemon/index.js +17 -12
- package/dist/token-daemon/index.js.map +1 -1
- package/dist/utils/clock-client-token.d.ts +2 -1
- package/dist/utils/clock-client-token.js +52 -8
- package/dist/utils/clock-client-token.js.map +1 -1
- package/dist/utils/clock-scope-trace.d.ts +11 -0
- package/dist/utils/clock-scope-trace.js +41 -0
- package/dist/utils/clock-scope-trace.js.map +1 -0
- package/dist/utils/llms-engine-shadow.js +1 -1
- package/dist/utils/llms-engine-shadow.js.map +1 -1
- package/docs/DAEMON_CONTROL_PLANE.md +1 -0
- package/docs/ROUTING_POLICY_SCHEMA.md +4 -2
- package/docs/daemon-admin-ui.html +328 -57
- package/docs/design/servertool-stopmessage-lifecycle.md +109 -0
- package/docs/exec-command-guard-policy.example.v1.json +7 -1
- package/docs/providers/antigravity-gemini-provider-compat.md +2 -2
- package/package.json +23 -6
- package/scripts/build-core.mjs +12 -0
- package/scripts/camoufox/launch-auth.mjs +112 -5
- package/scripts/ci/repo-sanity.mjs +1 -0
- package/scripts/cleanup-stale-server-pids.mjs +142 -0
- package/scripts/install-global.sh +8 -0
- package/scripts/install-verify.mjs +33 -16
- package/scripts/run-bg.sh +226 -43
- package/scripts/run-fg-gtimeout.sh +158 -14
- package/scripts/tests/blackbox-rcc-vs-routecodex-antigravity.mjs +3 -3
- package/scripts/tests/ci-jest.mjs +9 -1
- package/scripts/triage-errorsamples.mjs +216 -0
- package/scripts/verify-codex-error-samples.mjs +92 -15
- package/scripts/verify-e2e-toolcall.mjs +12 -1
- package/scripts/verify-install-e2e.mjs +69 -28
|
@@ -4,77 +4,28 @@ import fs from 'fs/promises';
|
|
|
4
4
|
import fsSync from 'fs';
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import os from 'os';
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
7
8
|
import { fetchIFlowUserInfo, mergeIFlowTokenData } from './iflow-userinfo-helper.js';
|
|
8
9
|
import { fetchQwenUserInfo, mergeQwenTokenData } from './qwen-userinfo-helper.js';
|
|
9
10
|
import { fetchGeminiCLIUserInfo, fetchGeminiCLIProjects, mergeGeminiCLITokenData, getDefaultProjectId } from './gemini-cli-userinfo-helper.js';
|
|
10
11
|
import { parseTokenSequenceFromPath } from './token-scanner/index.js';
|
|
11
12
|
import { logOAuthDebug } from './oauth-logger.js';
|
|
13
|
+
import { formatOAuthErrorMessage } from './oauth-error-message.js';
|
|
12
14
|
import { fetchAntigravityProjectId } from './antigravity-userinfo-helper.js';
|
|
13
15
|
import { HTTP_PROTOCOLS, LOCAL_HOSTS } from '../../constants/index.js';
|
|
14
16
|
import { withOAuthRepairEnv } from './oauth-repair-env.js';
|
|
15
17
|
import { markInteractiveOAuthRepairAttempt, markInteractiveOAuthRepairSuccess, shouldSkipInteractiveOAuthRepair } from './oauth-repair-cooldown.js';
|
|
16
18
|
import { openAuthInCamoufox } from '../core/config/camoufox-launcher.js';
|
|
17
|
-
import { isGeminiCliFamily, resolveTokenFilePath, resolveCamoufoxAliasForAuth
|
|
18
|
-
import { keyFor, shouldThrottle, updateThrottle, inFlight, interactiveTail } from './oauth-lifecycle/throttle.js';
|
|
19
|
+
import { isGeminiCliFamily, resolveTokenFilePath, resolveCamoufoxAliasForAuth } from './oauth-lifecycle/path-resolver.js';
|
|
20
|
+
import { keyFor, shouldThrottle, updateThrottle, lastRunAt, inFlight, interactiveTail } from './oauth-lifecycle/throttle.js';
|
|
19
21
|
import { extractStatusCode, isGoogleAccountVerificationRequiredMessage, extractGoogleAccountVerificationUrl } from './oauth-lifecycle/error-detection.js';
|
|
20
22
|
import { hasNonEmptyString, extractAccessToken, extractApiKey, hasApiKeyField, hasStableQwenApiKey, hasAccessToken, getExpiresAt, resolveProjectId, coerceExpiryTimestampSeconds, hasNoRefreshFlag, evaluateTokenState } from './oauth-lifecycle/token-helpers.js';
|
|
21
|
-
import { normalizeGeminiCliAccountToken, sanitizeToken, readTokenFromFile, backupTokenFile, restoreTokenFileFromBackup, discardBackupFile, readRawTokenFile } from './oauth-lifecycle/token-io.js';
|
|
23
|
+
import { normalizeGeminiCliAccountToken, sanitizeToken, readTokenFromFile, backupTokenFile, restoreTokenFileFromBackup, discardBackupFile, clearTokenFile, readRawTokenFile } from './oauth-lifecycle/token-io.js';
|
|
24
|
+
const OAUTH_INTERACTIVE_LOCK_FILE = path.join(os.homedir(), '.routecodex', 'auth', '.oauth-interactive.lock.json');
|
|
25
|
+
const IFLOW_AUTO_FAILURE_FILE = path.join(os.homedir(), '.routecodex', 'auth', '.iflow-auto-failures.json');
|
|
26
|
+
const OAUTH_THROTTLE_WINDOW_MS = 60_000;
|
|
27
|
+
const IFLOW_REFRESH_FAILURE_BACKOFF_MS = 5 * 60_000;
|
|
22
28
|
const TOKEN_REFRESH_SKEW_MS = 60_000;
|
|
23
|
-
async function selectBestIflowTokenCandidate(targetTokenFilePath) {
|
|
24
|
-
const targetResolved = path.resolve(targetTokenFilePath);
|
|
25
|
-
const candidates = resolveIflowCredentialCandidates();
|
|
26
|
-
let best = null;
|
|
27
|
-
for (const candidatePath of candidates) {
|
|
28
|
-
if (!candidatePath) {
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
const resolved = path.resolve(candidatePath);
|
|
32
|
-
if (resolved === targetResolved) {
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
const token = await readTokenFromFile(candidatePath);
|
|
36
|
-
if (!token) {
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
const state = evaluateTokenState(token, 'iflow');
|
|
40
|
-
if (!state.validAccess) {
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
const expiresAt = state.expiresAt ?? null;
|
|
44
|
-
if (!best) {
|
|
45
|
-
best = { token, sourcePath: candidatePath, expiresAt };
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
const bestExpiry = best.expiresAt ?? -1;
|
|
49
|
-
const currentExpiry = expiresAt ?? -1;
|
|
50
|
-
if (currentExpiry > bestExpiry) {
|
|
51
|
-
best = { token, sourcePath: candidatePath, expiresAt };
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return best;
|
|
55
|
-
}
|
|
56
|
-
async function maybeAdoptIflowExternalToken(strategy, tokenFilePath, existingToken) {
|
|
57
|
-
const currentState = evaluateTokenState(existingToken, 'iflow');
|
|
58
|
-
if (currentState.validAccess) {
|
|
59
|
-
return existingToken;
|
|
60
|
-
}
|
|
61
|
-
const bestCandidate = await selectBestIflowTokenCandidate(tokenFilePath);
|
|
62
|
-
if (!bestCandidate) {
|
|
63
|
-
return existingToken;
|
|
64
|
-
}
|
|
65
|
-
// Keep per-alias token files in RouteCodex, but adopt fresh credentials from iFlow-native stores when available.
|
|
66
|
-
const prepared = await prepareTokenForStorage('iflow', tokenFilePath, bestCandidate.token);
|
|
67
|
-
if (typeof strategy.saveToken === 'function') {
|
|
68
|
-
await strategy.saveToken(prepared);
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
await fs.mkdir(path.dirname(tokenFilePath), { recursive: true });
|
|
72
|
-
await fs.writeFile(tokenFilePath, `${JSON.stringify(prepared, null, 2)}\n`, 'utf8');
|
|
73
|
-
}
|
|
74
|
-
const normalized = sanitizeToken(prepared) ?? bestCandidate.token;
|
|
75
|
-
logOAuthDebug(`[OAuth] iflow token adopted from ${bestCandidate.sourcePath} -> ${tokenFilePath}`);
|
|
76
|
-
return normalized;
|
|
77
|
-
}
|
|
78
29
|
async function openGoogleAccountVerificationInCamoufox(args) {
|
|
79
30
|
const providerType = args.providerType;
|
|
80
31
|
const url = args.url;
|
|
@@ -126,6 +77,129 @@ async function openGoogleAccountVerificationInCamoufox(args) {
|
|
|
126
77
|
}
|
|
127
78
|
}
|
|
128
79
|
}
|
|
80
|
+
function isIflowRefreshEndpointRejectionMessage(message) {
|
|
81
|
+
const normalized = (message || '').toLowerCase();
|
|
82
|
+
if (!normalized) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
return (normalized.includes('oauth token endpoint rejected request') ||
|
|
86
|
+
(normalized.includes('token refresh failed') && normalized.includes('iflow.cn/oauth/token')));
|
|
87
|
+
}
|
|
88
|
+
function isIflowAkBlockedMessage(message) {
|
|
89
|
+
const normalized = (message || '').toLowerCase();
|
|
90
|
+
if (!normalized) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
return normalized.includes('access to the current ak has been blocked due to unauthorized requests');
|
|
94
|
+
}
|
|
95
|
+
function shouldClearIflowTokenOnRefreshFailure(message) {
|
|
96
|
+
const normalized = (message || '').toLowerCase();
|
|
97
|
+
if (!normalized) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
if (isIflowAkBlockedMessage(normalized)) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
if (normalized.includes('oauth error: invalid_grant')) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
if (normalized.includes('oauth error: invalid_client')) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
if (normalized.includes('oauth error: unauthorized_client')) {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
if (normalized.includes('oauth error: invalid_request') &&
|
|
113
|
+
(normalized.includes('refresh token') ||
|
|
114
|
+
normalized.includes('refresh_token') ||
|
|
115
|
+
normalized.includes('client_id'))) {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
function applyRefreshFailureBackoff(cacheKey, providerType, message) {
|
|
121
|
+
if (providerType !== 'iflow' || !isIflowRefreshEndpointRejectionMessage(message)) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
// For iFlow refresh endpoint 500/generic failures, avoid hammering token endpoint
|
|
125
|
+
// from preflight/retry loops. Keep a longer cooldown before next refresh attempt.
|
|
126
|
+
lastRunAt.set(cacheKey, Date.now() + IFLOW_REFRESH_FAILURE_BACKOFF_MS - OAUTH_THROTTLE_WINDOW_MS);
|
|
127
|
+
}
|
|
128
|
+
function isElementMissingAutomationFailure(message) {
|
|
129
|
+
const normalized = String(message || '').toLowerCase();
|
|
130
|
+
if (!normalized) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
return (normalized.includes('element not found') ||
|
|
134
|
+
normalized.includes('element_not_found') ||
|
|
135
|
+
normalized.includes('required but not matched'));
|
|
136
|
+
}
|
|
137
|
+
async function runInteractiveRepairWithAutoFallback(args) {
|
|
138
|
+
const { providerType, auth, ensureValid, opts } = args;
|
|
139
|
+
const autoModeAtStart = String(process.env.ROUTECODEX_CAMOUFOX_AUTO_MODE || '').trim();
|
|
140
|
+
try {
|
|
141
|
+
await ensureValid(providerType, auth, opts);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
if (!autoModeAtStart) {
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
const msg = error instanceof Error ? error.message : String(error || '');
|
|
149
|
+
const selectorFailure = isElementMissingAutomationFailure(msg);
|
|
150
|
+
let tokenFilePath = '';
|
|
151
|
+
try {
|
|
152
|
+
tokenFilePath = resolveTokenFilePath(auth, providerType);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
tokenFilePath = '';
|
|
156
|
+
}
|
|
157
|
+
if (tokenFilePath) {
|
|
158
|
+
closeOAuthAuthResources(providerType, tokenFilePath);
|
|
159
|
+
}
|
|
160
|
+
console.warn(`[OAuth] Camoufox auto OAuth failed (${providerType}, autoMode=${autoModeAtStart}): ${msg}. Falling back to headful manual mode once.`);
|
|
161
|
+
if (selectorFailure) {
|
|
162
|
+
console.warn(`[OAuth] Camoufox auto selector step failed; switched to headful manual mode (provider=${providerType}${tokenFilePath ? ` tokenFile=${tokenFilePath}` : ''}).`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const prevAutoMode = process.env.ROUTECODEX_CAMOUFOX_AUTO_MODE;
|
|
166
|
+
const prevAutoConfirm = process.env.ROUTECODEX_OAUTH_AUTO_CONFIRM;
|
|
167
|
+
const prevDevMode = process.env.ROUTECODEX_CAMOUFOX_DEV_MODE;
|
|
168
|
+
const prevOpenOnly = process.env.ROUTECODEX_CAMOUFOX_OPEN_ONLY;
|
|
169
|
+
try {
|
|
170
|
+
delete process.env.ROUTECODEX_CAMOUFOX_AUTO_MODE;
|
|
171
|
+
delete process.env.ROUTECODEX_OAUTH_AUTO_CONFIRM;
|
|
172
|
+
process.env.ROUTECODEX_CAMOUFOX_DEV_MODE = '1';
|
|
173
|
+
process.env.ROUTECODEX_CAMOUFOX_OPEN_ONLY = '1';
|
|
174
|
+
await ensureValid(providerType, auth, opts);
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
if (prevAutoMode === undefined) {
|
|
178
|
+
delete process.env.ROUTECODEX_CAMOUFOX_AUTO_MODE;
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
process.env.ROUTECODEX_CAMOUFOX_AUTO_MODE = prevAutoMode;
|
|
182
|
+
}
|
|
183
|
+
if (prevAutoConfirm === undefined) {
|
|
184
|
+
delete process.env.ROUTECODEX_OAUTH_AUTO_CONFIRM;
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
process.env.ROUTECODEX_OAUTH_AUTO_CONFIRM = prevAutoConfirm;
|
|
188
|
+
}
|
|
189
|
+
if (prevDevMode === undefined) {
|
|
190
|
+
delete process.env.ROUTECODEX_CAMOUFOX_DEV_MODE;
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
process.env.ROUTECODEX_CAMOUFOX_DEV_MODE = prevDevMode;
|
|
194
|
+
}
|
|
195
|
+
if (prevOpenOnly === undefined) {
|
|
196
|
+
delete process.env.ROUTECODEX_CAMOUFOX_OPEN_ONLY;
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
process.env.ROUTECODEX_CAMOUFOX_OPEN_ONLY = prevOpenOnly;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
129
203
|
function isOAuthConfig(auth) {
|
|
130
204
|
return Boolean(auth && typeof auth.type === 'string' && auth.type.toLowerCase().includes('oauth'));
|
|
131
205
|
}
|
|
@@ -396,7 +470,7 @@ function buildHeaderOverrides(defaults, providerType) {
|
|
|
396
470
|
if (providerType === 'iflow') {
|
|
397
471
|
return {
|
|
398
472
|
...baseHeaders,
|
|
399
|
-
'User-Agent': '
|
|
473
|
+
'User-Agent': 'iFlow-Cli',
|
|
400
474
|
'X-Requested-With': 'XMLHttpRequest',
|
|
401
475
|
'Origin': 'https://iflow.cn',
|
|
402
476
|
'Referer': 'https://iflow.cn/oauth',
|
|
@@ -489,7 +563,7 @@ async function maybeEnrichToken(providerType, tokenData, tokenFilePath) {
|
|
|
489
563
|
return mergeQwenTokenData(tokenData, userInfo);
|
|
490
564
|
}
|
|
491
565
|
catch (error) {
|
|
492
|
-
const msg =
|
|
566
|
+
const msg = formatOAuthErrorMessage(error);
|
|
493
567
|
// If userInfo endpoint is unavailable (404), treat access_token as api_key to avoid repeated lookups.
|
|
494
568
|
if (/\bHTTP\s+404\b/i.test(msg) || /\bnot\s+found\b/i.test(msg)) {
|
|
495
569
|
logOAuthDebug('[OAuth] Qwen: userInfo endpoint unavailable (404); using access_token as api_key fallback');
|
|
@@ -511,7 +585,7 @@ async function maybeEnrichToken(providerType, tokenData, tokenFilePath) {
|
|
|
511
585
|
return mergeIFlowTokenData(tokenData, userInfo);
|
|
512
586
|
}
|
|
513
587
|
catch (error) {
|
|
514
|
-
console.error(`[OAuth] iFlow: failed to fetch API Key - ${
|
|
588
|
+
console.error(`[OAuth] iFlow: failed to fetch API Key - ${formatOAuthErrorMessage(error)}`);
|
|
515
589
|
return tokenData;
|
|
516
590
|
}
|
|
517
591
|
}
|
|
@@ -532,7 +606,7 @@ async function maybeEnrichToken(providerType, tokenData, tokenFilePath) {
|
|
|
532
606
|
return merged;
|
|
533
607
|
}
|
|
534
608
|
catch (error) {
|
|
535
|
-
const msg =
|
|
609
|
+
const msg = formatOAuthErrorMessage(error);
|
|
536
610
|
console.error(`[OAuth] Antigravity: failed to fetch metadata - ${msg}`);
|
|
537
611
|
const normalized = msg.toLowerCase();
|
|
538
612
|
const isAuthError = normalized.includes('401') ||
|
|
@@ -560,7 +634,7 @@ async function maybeEnrichToken(providerType, tokenData, tokenFilePath) {
|
|
|
560
634
|
projects = await fetchGeminiCLIProjects(accessToken);
|
|
561
635
|
}
|
|
562
636
|
catch (projectsError) {
|
|
563
|
-
const msg =
|
|
637
|
+
const msg = formatOAuthErrorMessage(projectsError);
|
|
564
638
|
console.error(`[OAuth] ${label}: failed to fetch Projects - ${msg}`);
|
|
565
639
|
projects = [];
|
|
566
640
|
}
|
|
@@ -580,7 +654,7 @@ async function maybeEnrichToken(providerType, tokenData, tokenFilePath) {
|
|
|
580
654
|
return merged;
|
|
581
655
|
}
|
|
582
656
|
catch (error) {
|
|
583
|
-
const msg =
|
|
657
|
+
const msg = formatOAuthErrorMessage(error);
|
|
584
658
|
console.error(`[OAuth] ${label}: failed to fetch UserInfo - ${msg}`);
|
|
585
659
|
// 将明确的 401/invalid token 视为凭证失效,由调用方决定是否触发重新授权。
|
|
586
660
|
const normalized = msg.toLowerCase();
|
|
@@ -615,6 +689,255 @@ function logOAuthSetup(providerType, defaults, overrides, endpoints, client, tok
|
|
|
615
689
|
function createStrategy(providerType, overrides, tokenFilePath) {
|
|
616
690
|
return createProviderOAuthStrategy(providerType, overrides, tokenFilePath);
|
|
617
691
|
}
|
|
692
|
+
function resolveCamoCommand() {
|
|
693
|
+
const configured = String(process.env.ROUTECODEX_CAMO_CLI_PATH || process.env.RCC_CAMO_CLI_PATH || '').trim();
|
|
694
|
+
return configured || 'camo';
|
|
695
|
+
}
|
|
696
|
+
function isTruthyFlag(value) {
|
|
697
|
+
const raw = String(value || '').trim().toLowerCase();
|
|
698
|
+
return raw === '1' || raw === 'true' || raw === 'yes' || raw === 'on';
|
|
699
|
+
}
|
|
700
|
+
function resolveOAuthProfileId(providerType, tokenFilePath) {
|
|
701
|
+
const parsed = parseTokenSequenceFromPath(tokenFilePath);
|
|
702
|
+
const alias = String(parsed?.alias || 'default').trim().toLowerCase();
|
|
703
|
+
const normalizedAlias = alias.replace(/[^a-z0-9._-]+/gi, '-');
|
|
704
|
+
const normalizedProvider = String(providerType || '').trim().toLowerCase();
|
|
705
|
+
const providerFamily = normalizedProvider === 'gemini-cli' || normalizedProvider === 'antigravity'
|
|
706
|
+
? 'gemini'
|
|
707
|
+
: normalizedProvider;
|
|
708
|
+
const base = providerFamily ? `${providerFamily}.${normalizedAlias || 'default'}` : (normalizedAlias || 'default');
|
|
709
|
+
const profile = `rc-${base}`;
|
|
710
|
+
return profile.length > 64 ? profile.slice(0, 64) : profile;
|
|
711
|
+
}
|
|
712
|
+
function closeOAuthAuthResources(providerType, tokenFilePath) {
|
|
713
|
+
const profileId = resolveOAuthProfileId(providerType, tokenFilePath);
|
|
714
|
+
try {
|
|
715
|
+
const result = spawnSync(resolveCamoCommand(), ['stop', profileId], {
|
|
716
|
+
stdio: 'ignore',
|
|
717
|
+
env: process.env
|
|
718
|
+
});
|
|
719
|
+
if (result.status === 0) {
|
|
720
|
+
logOAuthDebug(`[OAuth] auth cleanup: stopped camo profile=${profileId}`);
|
|
721
|
+
}
|
|
722
|
+
else {
|
|
723
|
+
logOAuthDebug(`[OAuth] auth cleanup: camo stop profile=${profileId} status=${result.status ?? 'n/a'}`);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
catch (error) {
|
|
727
|
+
logOAuthDebug(`[OAuth] auth cleanup failed profile=${profileId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
function shouldAutoCloseOAuthBrowserSession() {
|
|
731
|
+
const raw = String(process.env.ROUTECODEX_OAUTH_AUTO_CLOSE_BROWSER ??
|
|
732
|
+
process.env.RCC_OAUTH_AUTO_CLOSE_BROWSER ??
|
|
733
|
+
'').trim().toLowerCase();
|
|
734
|
+
if (!raw) {
|
|
735
|
+
// Default: keep browser session alive and rely on camo idle-timeout cleanup.
|
|
736
|
+
return false;
|
|
737
|
+
}
|
|
738
|
+
return raw === '1' || raw === 'true' || raw === 'yes' || raw === 'on';
|
|
739
|
+
}
|
|
740
|
+
function readInteractiveOAuthLock() {
|
|
741
|
+
try {
|
|
742
|
+
if (!fsSync.existsSync(OAUTH_INTERACTIVE_LOCK_FILE)) {
|
|
743
|
+
return null;
|
|
744
|
+
}
|
|
745
|
+
const raw = fsSync.readFileSync(OAUTH_INTERACTIVE_LOCK_FILE, 'utf8');
|
|
746
|
+
const parsed = JSON.parse(raw);
|
|
747
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
748
|
+
return null;
|
|
749
|
+
}
|
|
750
|
+
const node = parsed;
|
|
751
|
+
if (typeof node.pid !== 'number' || typeof node.tokenFile !== 'string' || typeof node.providerType !== 'string') {
|
|
752
|
+
return null;
|
|
753
|
+
}
|
|
754
|
+
return {
|
|
755
|
+
pid: node.pid,
|
|
756
|
+
tokenFile: node.tokenFile,
|
|
757
|
+
providerType: node.providerType,
|
|
758
|
+
startedAt: typeof node.startedAt === 'number' ? node.startedAt : Date.now(),
|
|
759
|
+
callbackPort: typeof node.callbackPort === 'number' ? node.callbackPort : undefined
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
catch {
|
|
763
|
+
return null;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
function isSameInteractiveOAuthLock(left, right) {
|
|
767
|
+
return (left.pid === right.pid &&
|
|
768
|
+
left.providerType === right.providerType &&
|
|
769
|
+
path.resolve(left.tokenFile) === path.resolve(right.tokenFile));
|
|
770
|
+
}
|
|
771
|
+
function isProcessAlive(pid) {
|
|
772
|
+
if (!Number.isFinite(pid) || pid <= 0) {
|
|
773
|
+
return false;
|
|
774
|
+
}
|
|
775
|
+
try {
|
|
776
|
+
process.kill(pid, 0);
|
|
777
|
+
return true;
|
|
778
|
+
}
|
|
779
|
+
catch {
|
|
780
|
+
return false;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
async function forceReclaimInteractiveOAuthLock(lock) {
|
|
784
|
+
try {
|
|
785
|
+
const existing = readInteractiveOAuthLock();
|
|
786
|
+
if (!existing || !isSameInteractiveOAuthLock(existing, lock)) {
|
|
787
|
+
return false;
|
|
788
|
+
}
|
|
789
|
+
await fs.unlink(OAUTH_INTERACTIVE_LOCK_FILE);
|
|
790
|
+
logOAuthDebug(`[OAuth] interactive lock reclaimed pid=${lock.pid} token=${lock.tokenFile} provider=${lock.providerType}`);
|
|
791
|
+
return true;
|
|
792
|
+
}
|
|
793
|
+
catch {
|
|
794
|
+
return false;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
async function notifyOAuthLockCancel(lock) {
|
|
798
|
+
if (!lock.callbackPort || !Number.isFinite(lock.callbackPort) || lock.callbackPort <= 0) {
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
const url = `http://127.0.0.1:${lock.callbackPort}/oauth2callback?error=cancelled_by_new_auth`;
|
|
802
|
+
try {
|
|
803
|
+
await fetch(url, { method: 'GET' });
|
|
804
|
+
logOAuthDebug(`[OAuth] interactive lock cancel signal sent port=${lock.callbackPort}`);
|
|
805
|
+
}
|
|
806
|
+
catch (error) {
|
|
807
|
+
logOAuthDebug(`[OAuth] interactive lock cancel signal failed port=${lock.callbackPort}: ${error instanceof Error ? error.message : String(error)}`);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
async function acquireInteractiveOAuthLock(providerType, tokenFilePath) {
|
|
811
|
+
await fs.mkdir(path.dirname(OAUTH_INTERACTIVE_LOCK_FILE), { recursive: true });
|
|
812
|
+
const current = {
|
|
813
|
+
pid: process.pid,
|
|
814
|
+
providerType,
|
|
815
|
+
tokenFile: path.resolve(tokenFilePath),
|
|
816
|
+
startedAt: Date.now()
|
|
817
|
+
};
|
|
818
|
+
const maxAttempts = 20;
|
|
819
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
820
|
+
try {
|
|
821
|
+
await fs.writeFile(OAUTH_INTERACTIVE_LOCK_FILE, `${JSON.stringify(current, null, 2)}\n`, { flag: 'wx' });
|
|
822
|
+
process.env.ROUTECODEX_OAUTH_INTERACTIVE_LOCK_FILE = OAUTH_INTERACTIVE_LOCK_FILE;
|
|
823
|
+
return () => {
|
|
824
|
+
try {
|
|
825
|
+
const lock = readInteractiveOAuthLock();
|
|
826
|
+
if (lock && lock.pid === process.pid && path.resolve(lock.tokenFile) === current.tokenFile) {
|
|
827
|
+
fsSync.unlinkSync(OAUTH_INTERACTIVE_LOCK_FILE);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
catch {
|
|
831
|
+
// ignore release errors
|
|
832
|
+
}
|
|
833
|
+
finally {
|
|
834
|
+
if (process.env.ROUTECODEX_OAUTH_INTERACTIVE_LOCK_FILE === OAUTH_INTERACTIVE_LOCK_FILE) {
|
|
835
|
+
delete process.env.ROUTECODEX_OAUTH_INTERACTIVE_LOCK_FILE;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
catch (error) {
|
|
841
|
+
const code = error?.code || '';
|
|
842
|
+
if (code !== 'EEXIST') {
|
|
843
|
+
throw error;
|
|
844
|
+
}
|
|
845
|
+
const existing = readInteractiveOAuthLock();
|
|
846
|
+
if (!existing) {
|
|
847
|
+
try {
|
|
848
|
+
await fs.unlink(OAUTH_INTERACTIVE_LOCK_FILE);
|
|
849
|
+
}
|
|
850
|
+
catch {
|
|
851
|
+
// ignore
|
|
852
|
+
}
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
if (!isProcessAlive(existing.pid)) {
|
|
856
|
+
try {
|
|
857
|
+
await fs.unlink(OAUTH_INTERACTIVE_LOCK_FILE);
|
|
858
|
+
}
|
|
859
|
+
catch {
|
|
860
|
+
// ignore
|
|
861
|
+
}
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
const sameToken = path.resolve(existing.tokenFile) === current.tokenFile;
|
|
865
|
+
if (sameToken) {
|
|
866
|
+
await notifyOAuthLockCancel(existing);
|
|
867
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
868
|
+
const afterCancel = readInteractiveOAuthLock();
|
|
869
|
+
const stuckOnSameLock = !!afterCancel && isSameInteractiveOAuthLock(afterCancel, existing);
|
|
870
|
+
if (stuckOnSameLock) {
|
|
871
|
+
const lockAgeMs = Math.max(0, Date.now() - (afterCancel.startedAt || Date.now()));
|
|
872
|
+
const shouldForceReclaim = attempt >= 3 || lockAgeMs >= 15_000;
|
|
873
|
+
if (shouldForceReclaim) {
|
|
874
|
+
await forceReclaimInteractiveOAuthLock(afterCancel);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
throw new Error(`Interactive OAuth is already running for token=${existing.tokenFile}. Concurrent auth is disabled.`);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
throw new Error('Failed to acquire interactive OAuth lock after multiple attempts');
|
|
883
|
+
}
|
|
884
|
+
function readIflowAutoFailureState() {
|
|
885
|
+
try {
|
|
886
|
+
if (!fsSync.existsSync(IFLOW_AUTO_FAILURE_FILE)) {
|
|
887
|
+
return {};
|
|
888
|
+
}
|
|
889
|
+
const raw = fsSync.readFileSync(IFLOW_AUTO_FAILURE_FILE, 'utf8');
|
|
890
|
+
const parsed = JSON.parse(raw);
|
|
891
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
892
|
+
return {};
|
|
893
|
+
}
|
|
894
|
+
return parsed;
|
|
895
|
+
}
|
|
896
|
+
catch {
|
|
897
|
+
return {};
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
function writeIflowAutoFailureState(state) {
|
|
901
|
+
try {
|
|
902
|
+
fsSync.mkdirSync(path.dirname(IFLOW_AUTO_FAILURE_FILE), { recursive: true });
|
|
903
|
+
fsSync.writeFileSync(IFLOW_AUTO_FAILURE_FILE, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
|
|
904
|
+
}
|
|
905
|
+
catch {
|
|
906
|
+
// ignore persistence failures
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
function resolveIflowFailureKey(tokenFilePath) {
|
|
910
|
+
return path.resolve(tokenFilePath);
|
|
911
|
+
}
|
|
912
|
+
function clearIflowAutoFailureState(tokenFilePath) {
|
|
913
|
+
const state = readIflowAutoFailureState();
|
|
914
|
+
const key = resolveIflowFailureKey(tokenFilePath);
|
|
915
|
+
if (!state[key]) {
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
delete state[key];
|
|
919
|
+
writeIflowAutoFailureState(state);
|
|
920
|
+
}
|
|
921
|
+
function markIflowAutoFailureState(tokenFilePath, maxAttempts, errorText) {
|
|
922
|
+
const state = readIflowAutoFailureState();
|
|
923
|
+
const key = resolveIflowFailureKey(tokenFilePath);
|
|
924
|
+
const previous = state[key];
|
|
925
|
+
const nextCount = (previous?.count || 0) + 1;
|
|
926
|
+
const record = {
|
|
927
|
+
count: nextCount,
|
|
928
|
+
manualRequired: nextCount >= maxAttempts,
|
|
929
|
+
updatedAt: Date.now(),
|
|
930
|
+
lastError: errorText
|
|
931
|
+
};
|
|
932
|
+
state[key] = record;
|
|
933
|
+
writeIflowAutoFailureState(state);
|
|
934
|
+
return record;
|
|
935
|
+
}
|
|
936
|
+
function getIflowAutoFailureState(tokenFilePath) {
|
|
937
|
+
const state = readIflowAutoFailureState();
|
|
938
|
+
const key = resolveIflowFailureKey(tokenFilePath);
|
|
939
|
+
return state[key] || null;
|
|
940
|
+
}
|
|
618
941
|
async function runInteractiveAuthorizationFlow(providerType, overrides, tokenFilePath, openBrowser, forceTokenReset, forceReauth) {
|
|
619
942
|
const execute = async () => {
|
|
620
943
|
let backupFile = null;
|
|
@@ -633,6 +956,10 @@ async function runInteractiveAuthorizationFlow(providerType, overrides, tokenFil
|
|
|
633
956
|
await finalizeTokenWrite(providerType, strategy, tokenFilePath, authed, 'acquired');
|
|
634
957
|
}
|
|
635
958
|
await discardBackupFile(backupFile);
|
|
959
|
+
if (openBrowser && shouldAutoCloseOAuthBrowserSession()) {
|
|
960
|
+
// Optional: close only after token is fully written; never close browser on failed auth.
|
|
961
|
+
closeOAuthAuthResources(providerType, tokenFilePath);
|
|
962
|
+
}
|
|
636
963
|
}
|
|
637
964
|
catch (error) {
|
|
638
965
|
await restoreTokenFileFromBackup(backupFile, tokenFilePath);
|
|
@@ -650,10 +977,12 @@ async function runInteractiveAuthorizationFlow(providerType, overrides, tokenFil
|
|
|
650
977
|
})
|
|
651
978
|
.then(async () => {
|
|
652
979
|
logOAuthDebug(`[OAuth] interactive queue enter ${label}`);
|
|
980
|
+
const releaseLock = await acquireInteractiveOAuthLock(providerType, tokenFilePath);
|
|
653
981
|
try {
|
|
654
982
|
await execute();
|
|
655
983
|
}
|
|
656
984
|
finally {
|
|
985
|
+
releaseLock();
|
|
657
986
|
logOAuthDebug(`[OAuth] interactive queue leave ${label}`);
|
|
658
987
|
}
|
|
659
988
|
});
|
|
@@ -662,6 +991,12 @@ async function runInteractiveAuthorizationFlow(providerType, overrides, tokenFil
|
|
|
662
991
|
}
|
|
663
992
|
async function runIflowAuthorizationSequence(providerType, overrides, tokenFilePath, forceReauth) {
|
|
664
993
|
const authCodeOverrides = { ...overrides, flowType: OAuthFlowType.AUTHORIZATION_CODE };
|
|
994
|
+
const autoMode = String(process.env.ROUTECODEX_CAMOUFOX_AUTO_MODE || '').trim().toLowerCase();
|
|
995
|
+
if (autoMode === 'iflow') {
|
|
996
|
+
// Auto mode should stay single-path to keep retry lifecycle deterministic.
|
|
997
|
+
await executeAuthFlow(providerType, authCodeOverrides, tokenFilePath, forceReauth);
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
665
1000
|
try {
|
|
666
1001
|
await executeAuthFlow(providerType, authCodeOverrides, tokenFilePath, forceReauth);
|
|
667
1002
|
return;
|
|
@@ -673,9 +1008,58 @@ async function runIflowAuthorizationSequence(providerType, overrides, tokenFileP
|
|
|
673
1008
|
await executeAuthFlow(providerType, deviceOverrides, tokenFilePath, forceReauth);
|
|
674
1009
|
}
|
|
675
1010
|
async function executeAuthFlow(providerType, overrides, tokenFilePath, forceReauth) {
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
|
|
1011
|
+
const runOnce = async () => {
|
|
1012
|
+
const strategy = createStrategy(providerType, overrides, tokenFilePath);
|
|
1013
|
+
const authed = await strategy.authenticate?.({ openBrowser: true, forceReauthorize: forceReauth });
|
|
1014
|
+
await finalizeTokenWrite(providerType, strategy, tokenFilePath, authed, overrides.flowType ? `acquired (${String(overrides.flowType)})` : 'acquired');
|
|
1015
|
+
};
|
|
1016
|
+
const autoMode = String(process.env.ROUTECODEX_CAMOUFOX_AUTO_MODE || '').trim().toLowerCase();
|
|
1017
|
+
const iflowAutoEnabled = providerType === 'iflow' && autoMode === 'iflow';
|
|
1018
|
+
if (!iflowAutoEnabled) {
|
|
1019
|
+
await runOnce();
|
|
1020
|
+
if (providerType === 'iflow') {
|
|
1021
|
+
clearIflowAutoFailureState(tokenFilePath);
|
|
1022
|
+
}
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
const headfulMode = isTruthyFlag(process.env.ROUTECODEX_CAMOUFOX_DEV_MODE);
|
|
1026
|
+
const maxAutoAttemptsRaw = Number.parseInt(String(process.env.ROUTECODEX_IFLOW_AUTO_MAX_ATTEMPTS || '').trim(), 10);
|
|
1027
|
+
const maxAutoAttempts = Number.isFinite(maxAutoAttemptsRaw) && maxAutoAttemptsRaw > 0 ? maxAutoAttemptsRaw : 3;
|
|
1028
|
+
const retryDelayRaw = Number.parseInt(String(process.env.ROUTECODEX_IFLOW_AUTO_RETRY_DELAY_MS || '').trim(), 10);
|
|
1029
|
+
const retryDelayMs = Number.isFinite(retryDelayRaw) && retryDelayRaw >= 0 ? retryDelayRaw : 1000;
|
|
1030
|
+
// Headful run is considered manual trigger; successful manual run clears auto failure gate.
|
|
1031
|
+
if (headfulMode) {
|
|
1032
|
+
await runOnce();
|
|
1033
|
+
clearIflowAutoFailureState(tokenFilePath);
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
const existingFailure = getIflowAutoFailureState(tokenFilePath);
|
|
1037
|
+
if (existingFailure?.manualRequired) {
|
|
1038
|
+
throw new Error(`[OAuth] iflow auto auth is disabled for token=${tokenFilePath} after ${existingFailure.count} failures. Manual trigger required.`);
|
|
1039
|
+
}
|
|
1040
|
+
let lastError = null;
|
|
1041
|
+
for (let attempt = 1; attempt <= maxAutoAttempts; attempt += 1) {
|
|
1042
|
+
try {
|
|
1043
|
+
await runOnce();
|
|
1044
|
+
clearIflowAutoFailureState(tokenFilePath);
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
catch (error) {
|
|
1048
|
+
lastError = error;
|
|
1049
|
+
const msg = error instanceof Error ? error.message : String(error || '');
|
|
1050
|
+
const record = markIflowAutoFailureState(tokenFilePath, maxAutoAttempts, msg);
|
|
1051
|
+
logOAuthDebug(`[OAuth] iflow auto auth attempt ${attempt}/${maxAutoAttempts} failed: ${msg} ` +
|
|
1052
|
+
`(failureCount=${record.count} manualRequired=${record.manualRequired ? '1' : '0'})`);
|
|
1053
|
+
if (attempt < maxAutoAttempts) {
|
|
1054
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
const finalRecord = getIflowAutoFailureState(tokenFilePath);
|
|
1059
|
+
if (finalRecord?.manualRequired) {
|
|
1060
|
+
throw new Error(`[OAuth] iflow auto auth failed ${finalRecord.count} times; manual trigger is required and auto retries are suspended.`);
|
|
1061
|
+
}
|
|
1062
|
+
throw (lastError instanceof Error ? lastError : new Error(String(lastError || 'iflow auto auth failed')));
|
|
679
1063
|
}
|
|
680
1064
|
export async function ensureValidOAuthToken(providerType, auth, opts = {}) {
|
|
681
1065
|
if (!isOAuthConfig(auth)) {
|
|
@@ -714,9 +1098,6 @@ export async function ensureValidOAuthToken(providerType, auth, opts = {}) {
|
|
|
714
1098
|
logOAuthSetup(providerType, defaults, overrides, endpoints, client, tokenFilePath, openBrowser, forceReauth);
|
|
715
1099
|
const strategy = createStrategy(providerType, overrides, tokenFilePath);
|
|
716
1100
|
let token = await readTokenFromFile(tokenFilePath);
|
|
717
|
-
if (providerType === 'iflow') {
|
|
718
|
-
token = await maybeAdoptIflowExternalToken(strategy, tokenFilePath, token);
|
|
719
|
-
}
|
|
720
1101
|
const hadExistingTokenFile = token !== null;
|
|
721
1102
|
// Qwen: ensure api_key is present even when access_token is still valid.
|
|
722
1103
|
// Qwen OpenAI-compatible endpoints may require api_key (not access_token) for business requests.
|
|
@@ -787,13 +1168,27 @@ export async function ensureValidOAuthToken(providerType, auth, opts = {}) {
|
|
|
787
1168
|
return;
|
|
788
1169
|
}
|
|
789
1170
|
catch (error) {
|
|
1171
|
+
const message = error instanceof Error ? error.message : String(error || '');
|
|
1172
|
+
applyRefreshFailureBackoff(cacheKey, providerType, message);
|
|
1173
|
+
if (providerType === 'iflow' && shouldClearIflowTokenOnRefreshFailure(message)) {
|
|
1174
|
+
// Only clear token file for permanent refresh-credential failures.
|
|
1175
|
+
await clearTokenFile(tokenFilePath);
|
|
1176
|
+
}
|
|
790
1177
|
if (!opts.forceReacquireIfRefreshFails) {
|
|
791
1178
|
throw error;
|
|
792
1179
|
}
|
|
1180
|
+
logOAuthDebug(`[OAuth] refresh failed (${providerType}): ${message}`);
|
|
793
1181
|
logOAuthDebug('[OAuth] refresh failed, attempting interactive authorization...');
|
|
794
1182
|
}
|
|
795
1183
|
}
|
|
796
1184
|
try {
|
|
1185
|
+
const flowTypeRaw = String(overrides.flowType || defaults.flowType || '').trim().toLowerCase();
|
|
1186
|
+
const authorizationCodeFlow = flowTypeRaw === String(OAuthFlowType.AUTHORIZATION_CODE).trim().toLowerCase();
|
|
1187
|
+
if (!openBrowser && authorizationCodeFlow) {
|
|
1188
|
+
// Non-interactive contexts must never enter auth-code callback/manual prompts.
|
|
1189
|
+
// Let callers decide whether to retry in explicit interactive mode.
|
|
1190
|
+
throw new Error(`[OAuth] interactive authorization requires openBrowser=true for ${providerType} (flow=${flowTypeRaw || 'authorization_code'})`);
|
|
1191
|
+
}
|
|
797
1192
|
await runInteractiveAuthorizationFlow(providerType, overrides, tokenFilePath, openBrowser, forceReauth || hadExistingTokenFile, forceReauth);
|
|
798
1193
|
updateThrottle(cacheKey);
|
|
799
1194
|
}
|
|
@@ -868,22 +1263,25 @@ export async function handleUpstreamInvalidOAuthToken(providerType, auth, upstre
|
|
|
868
1263
|
}
|
|
869
1264
|
return false;
|
|
870
1265
|
}
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1266
|
+
const refreshRejectedForIflow = pt === 'iflow' && isIflowRefreshEndpointRejectionMessage(lower);
|
|
1267
|
+
if (!refreshRejectedForIflow) {
|
|
1268
|
+
try {
|
|
1269
|
+
await withOAuthRepairEnv(providerType, async () => {
|
|
1270
|
+
await ensureValid(providerType, auth, {
|
|
1271
|
+
forceReacquireIfRefreshFails: false,
|
|
1272
|
+
openBrowser: false,
|
|
1273
|
+
forceReauthorize: false
|
|
1274
|
+
});
|
|
877
1275
|
});
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
1276
|
+
await markInteractiveOAuthRepairSuccess({
|
|
1277
|
+
providerType,
|
|
1278
|
+
tokenFile: tokenFilePath
|
|
1279
|
+
});
|
|
1280
|
+
return true;
|
|
1281
|
+
}
|
|
1282
|
+
catch {
|
|
1283
|
+
// ignore silent refresh errors; fall through to background interactive flow
|
|
1284
|
+
}
|
|
887
1285
|
}
|
|
888
1286
|
const interactiveOpts = {
|
|
889
1287
|
forceReacquireIfRefreshFails: true,
|
|
@@ -893,7 +1291,12 @@ export async function handleUpstreamInvalidOAuthToken(providerType, auth, upstre
|
|
|
893
1291
|
forceReauthorize: pt === 'gemini' || pt === 'gemini-cli' || pt === 'antigravity' || pt === 'iflow' || pt === 'qwen'
|
|
894
1292
|
};
|
|
895
1293
|
void withOAuthRepairEnv(providerType, async () => {
|
|
896
|
-
await
|
|
1294
|
+
await runInteractiveRepairWithAutoFallback({
|
|
1295
|
+
providerType,
|
|
1296
|
+
auth,
|
|
1297
|
+
ensureValid,
|
|
1298
|
+
opts: interactiveOpts
|
|
1299
|
+
});
|
|
897
1300
|
}).catch(() => {
|
|
898
1301
|
// background repair failure must never block requests
|
|
899
1302
|
});
|
|
@@ -907,7 +1310,12 @@ export async function handleUpstreamInvalidOAuthToken(providerType, auth, upstre
|
|
|
907
1310
|
forceReauthorize: pt === 'gemini' || pt === 'gemini-cli' || pt === 'antigravity' || pt === 'iflow' || pt === 'qwen'
|
|
908
1311
|
};
|
|
909
1312
|
await withOAuthRepairEnv(providerType, async () => {
|
|
910
|
-
await
|
|
1313
|
+
await runInteractiveRepairWithAutoFallback({
|
|
1314
|
+
providerType,
|
|
1315
|
+
auth,
|
|
1316
|
+
ensureValid,
|
|
1317
|
+
opts
|
|
1318
|
+
});
|
|
911
1319
|
});
|
|
912
1320
|
await markInteractiveOAuthRepairSuccess({
|
|
913
1321
|
providerType,
|
|
@@ -928,6 +1336,10 @@ export function shouldTriggerInteractiveOAuthRepair(providerType, upstreamError)
|
|
|
928
1336
|
: String(upstreamError || '');
|
|
929
1337
|
const lower = msg.toLowerCase();
|
|
930
1338
|
const statusCode = extractStatusCode(upstreamError);
|
|
1339
|
+
if (pt === 'iflow' && (statusCode === 434 || isIflowAkBlockedMessage(lower))) {
|
|
1340
|
+
// iFlow 434 是账号级封禁,必须人工恢复,不走自动修复。
|
|
1341
|
+
return false;
|
|
1342
|
+
}
|
|
931
1343
|
// 基本令牌失效判定:只看典型 OAuth 文案
|
|
932
1344
|
let looksInvalid = /invalid[_-]?token|invalid[_-]?grant|unauthenticated|unauthorized|token has expired|access token expired/.test(lower);
|
|
933
1345
|
// 对于 iflow / qwen,保留基于 401/403 的宽松判定,避免破坏既有行为。
|
|
@@ -938,6 +1350,9 @@ export function shouldTriggerInteractiveOAuthRepair(providerType, upstreamError)
|
|
|
938
1350
|
looksInvalid = true;
|
|
939
1351
|
}
|
|
940
1352
|
}
|
|
1353
|
+
if (!looksInvalid && pt === 'iflow' && isIflowRefreshEndpointRejectionMessage(lower)) {
|
|
1354
|
+
looksInvalid = true;
|
|
1355
|
+
}
|
|
941
1356
|
// 对于 gemini / gemini-cli / antigravity,排除纯服务开关类错误,
|
|
942
1357
|
// 但如果明确提示缺少 project_id 或需要重新 OAuth,则视为令牌失效。
|
|
943
1358
|
if (pt === 'gemini' || pt === 'gemini-cli' || pt === 'antigravity') {
|