@jsonstudio/rcc 0.89.1552 → 0.89.1800
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 +97 -13
- package/configsamples/config.json +8 -8
- package/configsamples/config.reference.json +1 -1
- package/configsamples/provider/crs/config.v1.json +1 -1
- package/configsamples/provider/glm/config.v1.json +1 -1
- package/configsamples/provider/glm-anthropic/config.v1.json +1 -1
- package/configsamples/provider/kimi/config.v1.json +1 -1
- package/configsamples/provider/lmstudio/config.v1.json +2 -1
- package/configsamples/provider/mimo/config.v1.json +1 -1
- package/configsamples/provider/modelscope/config.v1.json +1 -1
- package/configsamples/provider/qwen/config.v1.json +1 -1
- package/configsamples/provider/tab/config.v1.json +2 -1
- package/configsamples/provider/tabglm/config.v1.json +10 -15
- package/dist/build-info.js +2 -2
- package/dist/cli/commands/camoufox.d.ts +34 -0
- package/dist/cli/commands/camoufox.js +107 -0
- package/dist/cli/commands/camoufox.js.map +1 -0
- package/dist/cli/commands/config.js +8 -9
- package/dist/cli/commands/config.js.map +1 -1
- package/dist/cli/commands/restart.d.ts +4 -12
- package/dist/cli/commands/restart.js +226 -120
- package/dist/cli/commands/restart.js.map +1 -1
- package/dist/cli/commands/start.d.ts +1 -0
- package/dist/cli/commands/start.js +34 -6
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/cli/commands/status.js +12 -6
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/config/init-provider-catalog.js +12 -11
- package/dist/cli/config/init-provider-catalog.js.map +1 -1
- package/dist/cli/register/camoufox-command.d.ts +20 -0
- package/dist/cli/register/camoufox-command.js +22 -0
- package/dist/cli/register/camoufox-command.js.map +1 -0
- package/dist/cli.js +14 -14
- package/dist/cli.js.map +1 -1
- package/dist/client/anthropic/anthropic-protocol-client.d.ts +1 -0
- package/dist/client/anthropic/anthropic-protocol-client.js +25 -0
- package/dist/client/anthropic/anthropic-protocol-client.js.map +1 -1
- package/dist/commands/oauth.js +185 -9
- package/dist/commands/oauth.js.map +1 -1
- package/dist/commands/token-daemon.js +12 -2
- package/dist/commands/token-daemon.js.map +1 -1
- package/dist/commands/validate.js +1 -1
- package/dist/commands/validate.js.map +1 -1
- package/dist/docs/daemon-admin-ui.html +1355 -204
- package/dist/index.js +119 -0
- package/dist/index.js.map +1 -1
- package/dist/manager/index.d.ts +2 -0
- package/dist/manager/index.js +39 -2
- package/dist/manager/index.js.map +1 -1
- package/dist/manager/modules/quota/antigravity-quota-manager.d.ts +29 -5
- package/dist/manager/modules/quota/antigravity-quota-manager.js +369 -113
- package/dist/manager/modules/quota/antigravity-quota-manager.js.map +1 -1
- package/dist/manager/modules/quota/provider-quota-daemon.cooldown.d.ts +7 -0
- package/dist/manager/modules/quota/provider-quota-daemon.cooldown.js +61 -0
- package/dist/manager/modules/quota/provider-quota-daemon.cooldown.js.map +1 -1
- package/dist/manager/modules/quota/provider-quota-daemon.d.ts +1 -0
- package/dist/manager/modules/quota/provider-quota-daemon.events.js +245 -1
- package/dist/manager/modules/quota/provider-quota-daemon.events.js.map +1 -1
- package/dist/manager/modules/quota/provider-quota-daemon.js +20 -13
- package/dist/manager/modules/quota/provider-quota-daemon.js.map +1 -1
- package/dist/manager/modules/quota/provider-quota-daemon.model-backoff.d.ts +1 -0
- package/dist/manager/modules/quota/provider-quota-daemon.model-backoff.js +8 -3
- package/dist/manager/modules/quota/provider-quota-daemon.model-backoff.js.map +1 -1
- package/dist/manager/modules/token/index.js +2 -2
- package/dist/manager/modules/token/index.js.map +1 -1
- package/dist/manager/quota/provider-quota-center.d.ts +16 -1
- package/dist/manager/quota/provider-quota-center.js +24 -3
- package/dist/manager/quota/provider-quota-center.js.map +1 -1
- package/dist/modules/llmswitch/bridge.d.ts +33 -1
- package/dist/modules/llmswitch/bridge.js +170 -2
- package/dist/modules/llmswitch/bridge.js.map +1 -1
- package/dist/modules/llmswitch/core-loader.js +64 -11
- package/dist/modules/llmswitch/core-loader.js.map +1 -1
- package/dist/modules/pipeline/utils/debug-logger.d.ts +1 -0
- package/dist/modules/pipeline/utils/debug-logger.js +50 -3
- package/dist/modules/pipeline/utils/debug-logger.js.map +1 -1
- package/dist/providers/auth/apikey-auth.js +15 -3
- package/dist/providers/auth/apikey-auth.js.map +1 -1
- package/dist/providers/auth/oauth-lifecycle.d.ts +13 -1
- package/dist/providers/auth/oauth-lifecycle.js +346 -45
- package/dist/providers/auth/oauth-lifecycle.js.map +1 -1
- package/dist/providers/auth/oauth-repair-cooldown.d.ts +21 -0
- package/dist/providers/auth/oauth-repair-cooldown.js +100 -0
- package/dist/providers/auth/oauth-repair-cooldown.js.map +1 -0
- package/dist/providers/auth/oauth-repair-env.d.ts +1 -0
- package/dist/providers/auth/oauth-repair-env.js +79 -0
- package/dist/providers/auth/oauth-repair-env.js.map +1 -0
- package/dist/providers/auth/qwen-userinfo-helper.d.ts +2 -0
- package/dist/providers/auth/qwen-userinfo-helper.js +72 -40
- package/dist/providers/auth/qwen-userinfo-helper.js.map +1 -1
- package/dist/providers/auth/tokenfile-auth.js +148 -17
- package/dist/providers/auth/tokenfile-auth.js.map +1 -1
- package/dist/providers/core/api/provider-types.d.ts +10 -0
- package/dist/providers/core/config/camoufox-launcher.d.ts +3 -0
- package/dist/providers/core/config/camoufox-launcher.js +190 -3
- package/dist/providers/core/config/camoufox-launcher.js.map +1 -1
- package/dist/providers/core/config/oauth-flows.js +50 -19
- package/dist/providers/core/config/oauth-flows.js.map +1 -1
- package/dist/providers/core/config/provider-oauth-configs.js +1 -1
- package/dist/providers/core/config/provider-oauth-configs.js.map +1 -1
- package/dist/providers/core/runtime/gemini-cli-http-provider.d.ts +5 -0
- package/dist/providers/core/runtime/gemini-cli-http-provider.js +172 -15
- package/dist/providers/core/runtime/gemini-cli-http-provider.js.map +1 -1
- package/dist/providers/core/runtime/gemini-http-provider.d.ts +11 -0
- package/dist/providers/core/runtime/gemini-http-provider.js +281 -3
- package/dist/providers/core/runtime/gemini-http-provider.js.map +1 -1
- package/dist/providers/core/runtime/http-request-executor.js +55 -0
- package/dist/providers/core/runtime/http-request-executor.js.map +1 -1
- package/dist/providers/core/runtime/http-transport-provider.js +10 -14
- package/dist/providers/core/runtime/http-transport-provider.js.map +1 -1
- package/dist/providers/core/runtime/provider-factory.d.ts +1 -0
- package/dist/providers/core/runtime/provider-factory.js +40 -2
- package/dist/providers/core/runtime/provider-factory.js.map +1 -1
- package/dist/providers/core/strategies/oauth-auth-code-flow.js +45 -2
- package/dist/providers/core/strategies/oauth-auth-code-flow.js.map +1 -1
- package/dist/providers/core/strategies/oauth-device-flow.js +13 -2
- package/dist/providers/core/strategies/oauth-device-flow.js.map +1 -1
- package/dist/providers/core/strategies/oauth-refresh-errors.d.ts +1 -0
- package/dist/providers/core/strategies/oauth-refresh-errors.js +26 -0
- package/dist/providers/core/strategies/oauth-refresh-errors.js.map +1 -0
- package/dist/providers/core/utils/snapshot-writer.d.ts +4 -2
- package/dist/providers/core/utils/snapshot-writer.js +86 -23
- package/dist/providers/core/utils/snapshot-writer.js.map +1 -1
- package/dist/scripts/camoufox/launch-auth.mjs +545 -49
- package/dist/server/handlers/chat-handler.js +1 -1
- package/dist/server/handlers/chat-handler.js.map +1 -1
- package/dist/server/handlers/handler-utils.d.ts +1 -0
- package/dist/server/handlers/handler-utils.js +231 -3
- package/dist/server/handlers/handler-utils.js.map +1 -1
- package/dist/server/handlers/messages-handler.js +1 -1
- package/dist/server/handlers/messages-handler.js.map +1 -1
- package/dist/server/handlers/responses-handler.js +17 -5
- package/dist/server/handlers/responses-handler.js.map +1 -1
- package/dist/server/handlers/sse-dispatcher.js +10 -1
- package/dist/server/handlers/sse-dispatcher.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/control-handler.d.ts +3 -0
- package/dist/server/runtime/http-server/daemon-admin/control-handler.js +389 -0
- package/dist/server/runtime/http-server/daemon-admin/control-handler.js.map +1 -0
- package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js +190 -5
- package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/providers-handler.js +2 -1
- package/dist/server/runtime/http-server/daemon-admin/providers-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/quota-handler.js +117 -14
- package/dist/server/runtime/http-server/daemon-admin/quota-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/routing-policy.d.ts +30 -0
- package/dist/server/runtime/http-server/daemon-admin/routing-policy.js +133 -0
- package/dist/server/runtime/http-server/daemon-admin/routing-policy.js.map +1 -0
- package/dist/server/runtime/http-server/daemon-admin/status-handler.js +40 -1
- package/dist/server/runtime/http-server/daemon-admin/status-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin-routes.d.ts +5 -0
- package/dist/server/runtime/http-server/daemon-admin-routes.js +3 -0
- package/dist/server/runtime/http-server/daemon-admin-routes.js.map +1 -1
- package/dist/server/runtime/http-server/executor-pipeline.d.ts +10 -0
- package/dist/server/runtime/http-server/executor-pipeline.js +6 -0
- package/dist/server/runtime/http-server/executor-pipeline.js.map +1 -1
- package/dist/server/runtime/http-server/executor-response.js +26 -0
- package/dist/server/runtime/http-server/executor-response.js.map +1 -1
- package/dist/server/runtime/http-server/hub-shadow-compare.js +41 -3
- package/dist/server/runtime/http-server/hub-shadow-compare.js.map +1 -1
- package/dist/server/runtime/http-server/index.d.ts +9 -0
- package/dist/server/runtime/http-server/index.js +337 -91
- package/dist/server/runtime/http-server/index.js.map +1 -1
- package/dist/server/runtime/http-server/middleware.js +27 -1
- package/dist/server/runtime/http-server/middleware.js.map +1 -1
- package/dist/server/runtime/http-server/request-executor.js +199 -29
- package/dist/server/runtime/http-server/request-executor.js.map +1 -1
- package/dist/server/runtime/http-server/routes.d.ts +1 -0
- package/dist/server/runtime/http-server/routes.js +36 -3
- package/dist/server/runtime/http-server/routes.js.map +1 -1
- package/dist/server/runtime/http-server/server-id.d.ts +1 -0
- package/dist/server/runtime/http-server/server-id.js +18 -0
- package/dist/server/runtime/http-server/server-id.js.map +1 -0
- package/dist/server/runtime/http-server/stats-manager.d.ts +2 -0
- package/dist/server/runtime/http-server/stats-manager.js +63 -7
- package/dist/server/runtime/http-server/stats-manager.js.map +1 -1
- package/dist/server/runtime/http-server/types.d.ts +2 -0
- package/dist/server/utils/stage-logger.js +54 -9
- package/dist/server/utils/stage-logger.js.map +1 -1
- package/dist/token-daemon/history-store.d.ts +8 -3
- package/dist/token-daemon/history-store.js +41 -20
- package/dist/token-daemon/history-store.js.map +1 -1
- package/dist/token-daemon/index.d.ts +5 -1
- package/dist/token-daemon/index.js +191 -11
- package/dist/token-daemon/index.js.map +1 -1
- package/dist/token-daemon/quota-auth-issue.d.ts +7 -0
- package/dist/token-daemon/quota-auth-issue.js +231 -0
- package/dist/token-daemon/quota-auth-issue.js.map +1 -0
- package/dist/token-daemon/server-utils.js +1 -1
- package/dist/token-daemon/server-utils.js.map +1 -1
- package/dist/token-daemon/token-daemon.d.ts +2 -0
- package/dist/token-daemon/token-daemon.js +177 -14
- package/dist/token-daemon/token-daemon.js.map +1 -1
- package/dist/token-portal/local-token-portal.js +6 -0
- package/dist/token-portal/local-token-portal.js.map +1 -1
- package/dist/tools/provider-update/fetch-models.js +0 -1
- package/dist/tools/provider-update/fetch-models.js.map +1 -1
- package/dist/tools/provider-update/key-probe.js +0 -1
- package/dist/tools/provider-update/key-probe.js.map +1 -1
- package/docs/ANTIGRAVITY_IDE_FORWARD_PROXY.md +61 -0
- package/docs/ANTIGRAVITY_THOUGHT_SIGNATURE_BOOTSTRAP_429.md +80 -0
- package/docs/CLOCK.md +94 -0
- package/docs/DAEMON_CONTROL_PLANE.md +34 -0
- package/docs/OAUTH.md +172 -0
- package/docs/PROVIDERS_BUILTIN.md +8 -5
- package/docs/PROVIDER_TYPES.md +6 -4
- package/docs/QUOTA_MANAGER_V3.md +54 -0
- package/docs/ROUTING_POLICY_SCHEMA.md +47 -0
- package/docs/ROUTING_POLICY_UI.md +11 -0
- package/docs/SERVERTOOL_CLOCK_DESIGN.md +56 -25
- package/docs/antigravity-routing-contract.md +17 -11
- package/docs/config-secrets.md +49 -0
- package/docs/daemon-admin-ui.html +1355 -204
- package/docs/oauth-authentication-guide.md +4 -0
- package/docs/oauth-iflow-implementation.md +4 -0
- package/docs/provider-quota-design.md +11 -0
- package/docs/providers/antigravity-fingerprint-ua-warmup.md +25 -0
- package/docs/providers/antigravity-gemini-provider-compat.md +1 -0
- package/docs/providers/antigravity-thought-signature.md +127 -0
- package/docs/providers/tabglm-claude-code-compat.md +39 -0
- package/docs/refactoring/host-sharedmodule-safe-migration-plan.md +164 -0
- package/docs/stop-message-auto.md +1 -0
- package/docs/token-daemon-preview.html +2 -2
- package/docs/token-refresh-daemon-plan.md +6 -6
- package/package.json +5 -5
- package/scripts/antigravity-ide-forward-proxy.mjs +362 -0
- package/scripts/backfill-apply-patch-exec-errorsamples.mjs +19 -0
- package/scripts/camoufox/launch-auth.mjs +545 -49
- package/scripts/ci/repo-sanity.mjs +2 -0
- package/scripts/install-global.sh +46 -0
- package/scripts/migrate-antigravity-session-signatures-alias.mjs +193 -0
- package/scripts/migrate-antigravity-session-signatures.mjs +165 -0
- package/scripts/responses-compare-server.mjs +1 -1
- package/scripts/tests/blackbox-rcc-vs-routecodex-antigravity.mjs +44 -9
- package/scripts/tests/ci-jest.mjs +3 -0
- package/scripts/verify-client-headers.mjs +33 -5
- package/scripts/virtual-router-dryrun.mjs +333 -0
|
@@ -5,12 +5,15 @@ import fsSync from 'fs';
|
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import os from 'os';
|
|
7
7
|
import { fetchIFlowUserInfo, mergeIFlowTokenData } from './iflow-userinfo-helper.js';
|
|
8
|
-
import {} from './qwen-userinfo-helper.js';
|
|
8
|
+
import { fetchQwenUserInfo, mergeQwenTokenData } from './qwen-userinfo-helper.js';
|
|
9
9
|
import { fetchGeminiCLIUserInfo, fetchGeminiCLIProjects, mergeGeminiCLITokenData, getDefaultProjectId } from './gemini-cli-userinfo-helper.js';
|
|
10
10
|
import { parseTokenSequenceFromPath } from './token-scanner/index.js';
|
|
11
11
|
import { logOAuthDebug } from './oauth-logger.js';
|
|
12
12
|
import { fetchAntigravityProjectId } from './antigravity-userinfo-helper.js';
|
|
13
13
|
import { HTTP_PROTOCOLS, LOCAL_HOSTS } from '../../constants/index.js';
|
|
14
|
+
import { withOAuthRepairEnv } from './oauth-repair-env.js';
|
|
15
|
+
import { markInteractiveOAuthRepairAttempt, shouldSkipInteractiveOAuthRepair } from './oauth-repair-cooldown.js';
|
|
16
|
+
import { openAuthInCamoufox } from '../core/config/camoufox-launcher.js';
|
|
14
17
|
const TOKEN_REFRESH_SKEW_MS = 60_000;
|
|
15
18
|
const inFlight = new Map();
|
|
16
19
|
const lastRunAt = new Map();
|
|
@@ -31,7 +34,9 @@ function defaultTokenFile(providerType) {
|
|
|
31
34
|
return path.join(home, '.iflow', 'oauth_creds.json');
|
|
32
35
|
}
|
|
33
36
|
if (providerType === 'qwen') {
|
|
34
|
-
|
|
37
|
+
// Align with TokenFileAuthProvider + token-daemon defaults:
|
|
38
|
+
// keep a stable, well-known Qwen token file for alias="default".
|
|
39
|
+
return path.join(home, '.routecodex', 'auth', 'qwen-oauth-1-default.json');
|
|
35
40
|
}
|
|
36
41
|
if (isGeminiCliFamily(providerType)) {
|
|
37
42
|
const file = providerType.toLowerCase() === 'antigravity'
|
|
@@ -60,7 +65,22 @@ function resolveTokenFilePath(auth, providerType) {
|
|
|
60
65
|
const homeDir = process.env.HOME || os.homedir();
|
|
61
66
|
const authDir = path.join(homeDir, '.routecodex', 'auth');
|
|
62
67
|
const pattern = new RegExp(`^${providerType}-oauth-(\\d+)(?:-(.+))?\\.json$`, 'i');
|
|
68
|
+
const pt = providerType.toLowerCase();
|
|
69
|
+
// Qwen: keep a stable "default" file name whenever possible.
|
|
70
|
+
if (pt === 'qwen' && alias === 'default') {
|
|
71
|
+
const pinned = path.join(authDir, 'qwen-oauth-1-default.json');
|
|
72
|
+
try {
|
|
73
|
+
if (fsSync.existsSync(pinned)) {
|
|
74
|
+
auth.tokenFile = pinned;
|
|
75
|
+
return pinned;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// ignore and fall back to scanning
|
|
80
|
+
}
|
|
81
|
+
}
|
|
63
82
|
let existingPath = null;
|
|
83
|
+
let bestSeqForAlias = 0;
|
|
64
84
|
let maxSeq = 0;
|
|
65
85
|
try {
|
|
66
86
|
const entries = fsSync.readdirSync(authDir);
|
|
@@ -74,7 +94,8 @@ function resolveTokenFilePath(auth, providerType) {
|
|
|
74
94
|
continue;
|
|
75
95
|
}
|
|
76
96
|
const entryAlias = (match[2] || 'default');
|
|
77
|
-
if (entryAlias === alias &&
|
|
97
|
+
if (entryAlias === alias && seq >= bestSeqForAlias) {
|
|
98
|
+
bestSeqForAlias = seq;
|
|
78
99
|
existingPath = path.join(authDir, entry);
|
|
79
100
|
}
|
|
80
101
|
if (seq > maxSeq) {
|
|
@@ -89,7 +110,10 @@ function resolveTokenFilePath(auth, providerType) {
|
|
|
89
110
|
auth.tokenFile = existingPath;
|
|
90
111
|
return existingPath;
|
|
91
112
|
}
|
|
92
|
-
|
|
113
|
+
// When we don't have any existing token for this alias:
|
|
114
|
+
// - Qwen default alias should always map to seq=1 for stability.
|
|
115
|
+
// - Otherwise, allocate next seq to avoid collisions.
|
|
116
|
+
const nextSeq = (pt === 'qwen' && alias === 'default') ? 1 : (maxSeq + 1);
|
|
93
117
|
const fileName = `${providerType}-oauth-${nextSeq}-${alias}.json`;
|
|
94
118
|
const fullPath = path.join(authDir, fileName);
|
|
95
119
|
auth.tokenFile = fullPath;
|
|
@@ -102,6 +126,139 @@ function shouldThrottle(k, ms = 60_000) {
|
|
|
102
126
|
function updateThrottle(k) {
|
|
103
127
|
lastRunAt.set(k, Date.now());
|
|
104
128
|
}
|
|
129
|
+
function extractStatusCode(upstreamError) {
|
|
130
|
+
if (!upstreamError || typeof upstreamError !== 'object') {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
const anyErr = upstreamError;
|
|
134
|
+
const direct = anyErr.statusCode;
|
|
135
|
+
if (typeof direct === 'number' && Number.isFinite(direct)) {
|
|
136
|
+
return direct;
|
|
137
|
+
}
|
|
138
|
+
const status = anyErr.status;
|
|
139
|
+
if (typeof status === 'number' && Number.isFinite(status)) {
|
|
140
|
+
return status;
|
|
141
|
+
}
|
|
142
|
+
const response = anyErr.response;
|
|
143
|
+
if (response && typeof response === 'object') {
|
|
144
|
+
const respStatus = response.status;
|
|
145
|
+
if (typeof respStatus === 'number' && Number.isFinite(respStatus)) {
|
|
146
|
+
return respStatus;
|
|
147
|
+
}
|
|
148
|
+
const respStatusCode = response.statusCode;
|
|
149
|
+
if (typeof respStatusCode === 'number' && Number.isFinite(respStatusCode)) {
|
|
150
|
+
return respStatusCode;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
function isGoogleAccountVerificationRequiredMessage(lower) {
|
|
156
|
+
if (!lower) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
return (lower.includes('verify your account') ||
|
|
160
|
+
lower.includes('validation_required') ||
|
|
161
|
+
lower.includes('validation required') ||
|
|
162
|
+
lower.includes('validation_url') ||
|
|
163
|
+
lower.includes('validation url') ||
|
|
164
|
+
lower.includes('accounts.google.com/signin/continue') ||
|
|
165
|
+
lower.includes('support.google.com/accounts?p=al_alert'));
|
|
166
|
+
}
|
|
167
|
+
function extractGoogleAccountVerificationUrl(message) {
|
|
168
|
+
const msg = typeof message === 'string' ? message : '';
|
|
169
|
+
if (!msg) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
const normalized = msg
|
|
173
|
+
.replace(/\\\//g, '/')
|
|
174
|
+
.replace(/\\u0026/gi, '&')
|
|
175
|
+
.replace(/\\u003d/gi, '=')
|
|
176
|
+
.replace(/\\x26/gi, '&')
|
|
177
|
+
.replace(/\\x3d/gi, '=');
|
|
178
|
+
const patterns = [
|
|
179
|
+
/https:\/\/accounts\.google\.com\/signin\/continue[^\s"'\\<>)]*/i,
|
|
180
|
+
/https:\/\/accounts\.google\.com\/[^\s"'\\<>)]*/i,
|
|
181
|
+
/https:\/\/support\.google\.com\/accounts\?p=al_alert[^\s"'\\<>)]*/i
|
|
182
|
+
];
|
|
183
|
+
for (const re of patterns) {
|
|
184
|
+
const m = normalized.match(re);
|
|
185
|
+
if (m && m[0]) {
|
|
186
|
+
const url = String(m[0]).trim().replace(/[\\"']+$/g, '').replace(/[),.]+$/g, '');
|
|
187
|
+
if (url) {
|
|
188
|
+
return url;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
function resolveCamoufoxAliasForAuth(providerType, auth) {
|
|
195
|
+
const raw = typeof auth.tokenFile === 'string' ? auth.tokenFile.trim() : '';
|
|
196
|
+
if (raw && !raw.includes('/') && !raw.includes('\\') && !raw.endsWith('.json')) {
|
|
197
|
+
return raw;
|
|
198
|
+
}
|
|
199
|
+
const base = raw ? path.basename(raw) : '';
|
|
200
|
+
const pt = String(providerType || '').trim().toLowerCase();
|
|
201
|
+
if (base && pt) {
|
|
202
|
+
const re = new RegExp(`^${pt}-oauth-\\d+(?:-(.+))?\\.json$`, 'i');
|
|
203
|
+
const m = base.match(re);
|
|
204
|
+
const alias = m && m[1] ? String(m[1]).trim() : '';
|
|
205
|
+
if (alias) {
|
|
206
|
+
return alias;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return 'default';
|
|
210
|
+
}
|
|
211
|
+
async function openGoogleAccountVerificationInCamoufox(args) {
|
|
212
|
+
const providerType = args.providerType;
|
|
213
|
+
const url = args.url;
|
|
214
|
+
if (!url) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const alias = resolveCamoufoxAliasForAuth(providerType, args.auth);
|
|
218
|
+
const prevBrowser = process.env.ROUTECODEX_OAUTH_BROWSER;
|
|
219
|
+
const prevAutoMode = process.env.ROUTECODEX_CAMOUFOX_AUTO_MODE;
|
|
220
|
+
const prevDevMode = process.env.ROUTECODEX_CAMOUFOX_DEV_MODE;
|
|
221
|
+
const prevOpenOnly = process.env.ROUTECODEX_CAMOUFOX_OPEN_ONLY;
|
|
222
|
+
process.env.ROUTECODEX_OAUTH_BROWSER = 'camoufox';
|
|
223
|
+
delete process.env.ROUTECODEX_CAMOUFOX_AUTO_MODE;
|
|
224
|
+
process.env.ROUTECODEX_CAMOUFOX_DEV_MODE = '1';
|
|
225
|
+
process.env.ROUTECODEX_CAMOUFOX_OPEN_ONLY = '1';
|
|
226
|
+
try {
|
|
227
|
+
const ok = await openAuthInCamoufox({ url, provider: providerType, alias });
|
|
228
|
+
if (ok) {
|
|
229
|
+
console.warn(`[OAuth] Google account verification opened in Camoufox (provider=${providerType} alias=${alias}).`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
// best-effort; never block requests
|
|
234
|
+
}
|
|
235
|
+
finally {
|
|
236
|
+
if (prevBrowser === undefined) {
|
|
237
|
+
delete process.env.ROUTECODEX_OAUTH_BROWSER;
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
process.env.ROUTECODEX_OAUTH_BROWSER = prevBrowser;
|
|
241
|
+
}
|
|
242
|
+
if (prevAutoMode === undefined) {
|
|
243
|
+
delete process.env.ROUTECODEX_CAMOUFOX_AUTO_MODE;
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
process.env.ROUTECODEX_CAMOUFOX_AUTO_MODE = prevAutoMode;
|
|
247
|
+
}
|
|
248
|
+
if (prevDevMode === undefined) {
|
|
249
|
+
delete process.env.ROUTECODEX_CAMOUFOX_DEV_MODE;
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
process.env.ROUTECODEX_CAMOUFOX_DEV_MODE = prevDevMode;
|
|
253
|
+
}
|
|
254
|
+
if (prevOpenOnly === undefined) {
|
|
255
|
+
delete process.env.ROUTECODEX_CAMOUFOX_OPEN_ONLY;
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
process.env.ROUTECODEX_CAMOUFOX_OPEN_ONLY = prevOpenOnly;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
105
262
|
function isOAuthConfig(auth) {
|
|
106
263
|
return Boolean(auth && typeof auth.type === 'string' && auth.type.toLowerCase().includes('oauth'));
|
|
107
264
|
}
|
|
@@ -249,12 +406,30 @@ function extractAccessToken(token) {
|
|
|
249
406
|
}
|
|
250
407
|
return undefined;
|
|
251
408
|
}
|
|
409
|
+
function extractApiKey(token) {
|
|
410
|
+
if (!token) {
|
|
411
|
+
return undefined;
|
|
412
|
+
}
|
|
413
|
+
const candidate = token.apiKey ?? token.api_key;
|
|
414
|
+
return hasNonEmptyString(candidate) ? String(candidate) : undefined;
|
|
415
|
+
}
|
|
252
416
|
function hasApiKeyField(token) {
|
|
253
417
|
if (!token) {
|
|
254
418
|
return false;
|
|
255
419
|
}
|
|
256
|
-
|
|
257
|
-
|
|
420
|
+
return hasNonEmptyString(token.apiKey ?? token.api_key);
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Qwen: api_key 可能被降级为 access_token(userInfo 404 时的兼容写法),这种情况不应被视为“稳定 API Key”。
|
|
424
|
+
* 只有当 api_key 存在且与 access_token 不同(或缺失 access_token)时,才认为可以长期复用并跳过刷新。
|
|
425
|
+
*/
|
|
426
|
+
function hasStableQwenApiKey(token) {
|
|
427
|
+
const apiKey = extractApiKey(token);
|
|
428
|
+
if (!apiKey) {
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
const access = extractAccessToken(token);
|
|
432
|
+
return !access || apiKey !== access;
|
|
258
433
|
}
|
|
259
434
|
function hasAccessToken(token) {
|
|
260
435
|
return hasNonEmptyString(token?.access_token) || hasNonEmptyString(token?.AccessToken);
|
|
@@ -448,7 +623,8 @@ function evaluateTokenState(token, providerType) {
|
|
|
448
623
|
validAccess = hasApiKey || (!isExpiredOrNear && hasAccess);
|
|
449
624
|
}
|
|
450
625
|
else if (pt === 'qwen') {
|
|
451
|
-
|
|
626
|
+
// Qwen: 当获取到稳定 api_key 后,可跳过 refresh/reauth;否则仍依赖 access_token 的有效期。
|
|
627
|
+
validAccess = hasStableQwenApiKey(token) || (!isExpiredOrNear && (hasAccess || hasApiKey));
|
|
452
628
|
}
|
|
453
629
|
else {
|
|
454
630
|
validAccess = (hasApiKey || hasAccess) && !isExpiredOrNear;
|
|
@@ -669,6 +845,37 @@ async function finalizeTokenWrite(providerType, strategy, tokenFilePath, tokenDa
|
|
|
669
845
|
logOAuthDebug(`[OAuth] Token ${reason} saved: ${tokenFilePath}`);
|
|
670
846
|
}
|
|
671
847
|
async function maybeEnrichToken(providerType, tokenData, tokenFilePath) {
|
|
848
|
+
if (providerType === 'qwen') {
|
|
849
|
+
const sanitized = sanitizeToken(tokenData) ?? tokenData;
|
|
850
|
+
if (hasStableQwenApiKey(sanitized)) {
|
|
851
|
+
return tokenData;
|
|
852
|
+
}
|
|
853
|
+
const accessToken = extractAccessToken(sanitized);
|
|
854
|
+
if (!accessToken) {
|
|
855
|
+
logOAuthDebug('[OAuth] Qwen: no access_token found in auth result, skipping API Key fetch');
|
|
856
|
+
return tokenData;
|
|
857
|
+
}
|
|
858
|
+
try {
|
|
859
|
+
const userInfo = await fetchQwenUserInfo(accessToken);
|
|
860
|
+
if (userInfo.apiKey) {
|
|
861
|
+
logOAuthDebug(`[OAuth] Qwen: successfully fetched API Key${userInfo.email ? ` for ${userInfo.email}` : ''}`);
|
|
862
|
+
}
|
|
863
|
+
else {
|
|
864
|
+
logOAuthDebug('[OAuth] Qwen: user info fetched but apiKey missing; continuing with access_token only');
|
|
865
|
+
}
|
|
866
|
+
return mergeQwenTokenData(tokenData, userInfo);
|
|
867
|
+
}
|
|
868
|
+
catch (error) {
|
|
869
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
870
|
+
// If userInfo endpoint is unavailable (404), treat access_token as api_key to avoid repeated lookups.
|
|
871
|
+
if (/\bHTTP\s+404\b/i.test(msg) || /\bnot\s+found\b/i.test(msg)) {
|
|
872
|
+
logOAuthDebug('[OAuth] Qwen: userInfo endpoint unavailable (404); using access_token as api_key fallback');
|
|
873
|
+
return mergeQwenTokenData(tokenData, { apiKey: accessToken });
|
|
874
|
+
}
|
|
875
|
+
logOAuthDebug(`[OAuth] Qwen: failed to fetch user info - ${msg}`);
|
|
876
|
+
return tokenData;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
672
879
|
if (providerType === 'iflow') {
|
|
673
880
|
const accessToken = extractAccessToken(sanitizeToken(tokenData) ?? null);
|
|
674
881
|
if (!accessToken) {
|
|
@@ -847,9 +1054,14 @@ export async function ensureValidOAuthToken(providerType, auth, opts = {}) {
|
|
|
847
1054
|
await inFlight.get(cacheKey);
|
|
848
1055
|
return;
|
|
849
1056
|
}
|
|
1057
|
+
// Only treat "open browser" as explicit user intent when caller passed it explicitly.
|
|
1058
|
+
// This prevents background flows (daemon/provider init) from bypassing noRefresh due to env defaults.
|
|
1059
|
+
const openBrowserRequested = opts.openBrowser === true;
|
|
850
1060
|
// 当 opts.forceReauthorize 显式为 true 时,跳过节流检查,
|
|
851
1061
|
// 确保来自上游 401/406 等认证错误的修复请求不会被初始化阶段的调用吞掉。
|
|
852
|
-
|
|
1062
|
+
// Explicit user-triggered OAuth (openBrowser=true) must also bypass throttle,
|
|
1063
|
+
// otherwise repeated "Authorize" clicks in WebUI can become a silent no-op.
|
|
1064
|
+
if (!opts.forceReauthorize && !openBrowserRequested && shouldThrottle(cacheKey)) {
|
|
853
1065
|
return;
|
|
854
1066
|
}
|
|
855
1067
|
const aliasInfo = parseTokenSequenceFromPath(tokenFilePath);
|
|
@@ -869,6 +1081,22 @@ export async function ensureValidOAuthToken(providerType, auth, opts = {}) {
|
|
|
869
1081
|
const strategy = createStrategy(providerType, overrides, tokenFilePath);
|
|
870
1082
|
let token = await readTokenFromFile(tokenFilePath);
|
|
871
1083
|
const hadExistingTokenFile = token !== null;
|
|
1084
|
+
// Qwen: ensure api_key is present even when access_token is still valid.
|
|
1085
|
+
// Qwen OpenAI-compatible endpoints may require api_key (not access_token) for business requests.
|
|
1086
|
+
if (providerType === 'qwen' && token && !hasStableQwenApiKey(token)) {
|
|
1087
|
+
try {
|
|
1088
|
+
const enriched = await maybeEnrichToken(providerType, token);
|
|
1089
|
+
if (enriched && typeof strategy.saveToken === 'function') {
|
|
1090
|
+
const prepared = await prepareTokenForStorage(providerType, tokenFilePath, enriched);
|
|
1091
|
+
await strategy.saveToken(prepared);
|
|
1092
|
+
token = sanitizeToken(enriched) ?? enriched;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
catch (error) {
|
|
1096
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1097
|
+
console.error(`[OAuth] Qwen: failed to enrich existing token with api_key - ${msg}`);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
872
1100
|
// Gemini CLI family: if existing token lacks project metadata, try to enrich it without
|
|
873
1101
|
// forcing a full OAuth flow. Use current access_token to fetch userinfo/projects and write back.
|
|
874
1102
|
if (isGeminiCliFamily(providerType) && token) {
|
|
@@ -900,7 +1128,7 @@ export async function ensureValidOAuthToken(providerType, auth, opts = {}) {
|
|
|
900
1128
|
logTokenSnapshot(providerType, token, endpoints);
|
|
901
1129
|
const tokenState = evaluateTokenState(token, providerType);
|
|
902
1130
|
const noRefresh = hasNoRefreshFlag(token);
|
|
903
|
-
if (noRefresh) {
|
|
1131
|
+
if (noRefresh && !forceReauth && !openBrowserRequested) {
|
|
904
1132
|
logOAuthDebug(`[OAuth] norefresh flag set for provider=${providerType} tokenFile=${tokenFilePath} - skip auto-refresh and re-authorization.`);
|
|
905
1133
|
updateThrottle(cacheKey);
|
|
906
1134
|
return;
|
|
@@ -953,48 +1181,80 @@ export async function ensureValidOAuthToken(providerType, auth, opts = {}) {
|
|
|
953
1181
|
inFlight.delete(cacheKey);
|
|
954
1182
|
}
|
|
955
1183
|
}
|
|
956
|
-
export async function handleUpstreamInvalidOAuthToken(providerType, auth, upstreamError) {
|
|
1184
|
+
export async function handleUpstreamInvalidOAuthToken(providerType, auth, upstreamError, options) {
|
|
957
1185
|
const pt = providerType.toLowerCase();
|
|
1186
|
+
const allowBlocking = options?.allowBlocking !== false;
|
|
1187
|
+
const ensureValid = options?.ensureValidOAuthToken ?? ensureValidOAuthToken;
|
|
958
1188
|
try {
|
|
959
|
-
|
|
1189
|
+
if (!shouldTriggerInteractiveOAuthRepair(providerType, upstreamError)) {
|
|
1190
|
+
return false;
|
|
1191
|
+
}
|
|
1192
|
+
const msg = upstreamError instanceof Error
|
|
1193
|
+
? upstreamError.message
|
|
1194
|
+
: upstreamError && typeof upstreamError === 'object' && typeof upstreamError.message === 'string'
|
|
1195
|
+
? String(upstreamError.message)
|
|
1196
|
+
: String(upstreamError || '');
|
|
960
1197
|
const lower = msg.toLowerCase();
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
}
|
|
1198
|
+
const statusCode = extractStatusCode(upstreamError);
|
|
1199
|
+
const tokenFilePath = resolveTokenFilePath(auth, providerType);
|
|
1200
|
+
const cooldownReason = statusCode === 403 && isGoogleAccountVerificationRequiredMessage(lower) ? 'google_verify' : 'generic';
|
|
1201
|
+
const gate = await shouldSkipInteractiveOAuthRepair({
|
|
1202
|
+
providerType,
|
|
1203
|
+
tokenFile: tokenFilePath,
|
|
1204
|
+
reason: cooldownReason
|
|
1205
|
+
});
|
|
1206
|
+
if (gate.skip) {
|
|
1207
|
+
const msLeft = typeof gate.msLeft === 'number' ? gate.msLeft : 0;
|
|
1208
|
+
console.warn(`[OAuth] interactive repair skipped due to cooldown (provider=${providerType} status=${statusCode ?? 'unknown'} reason=${cooldownReason} msLeft=${msLeft} tokenFile=${tokenFilePath})`);
|
|
1209
|
+
return false;
|
|
972
1210
|
}
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1211
|
+
// Mark immediately so repeated auth failures don't cause infinite auth loops within a short window.
|
|
1212
|
+
await markInteractiveOAuthRepairAttempt({
|
|
1213
|
+
providerType,
|
|
1214
|
+
tokenFile: tokenFilePath,
|
|
1215
|
+
reason: cooldownReason
|
|
1216
|
+
});
|
|
1217
|
+
// Non-blocking server semantics:
|
|
1218
|
+
// - Try silent refresh first (fast path).
|
|
1219
|
+
// - If refresh fails or interactive is required (e.g. 403 verify), kick off interactive flow in background.
|
|
1220
|
+
// - Return false so Virtual Router can failover immediately.
|
|
1221
|
+
if (!allowBlocking) {
|
|
1222
|
+
if (statusCode === 403 && cooldownReason === 'google_verify') {
|
|
1223
|
+
const url = extractGoogleAccountVerificationUrl(msg);
|
|
1224
|
+
if (url) {
|
|
1225
|
+
void openGoogleAccountVerificationInCamoufox({
|
|
1226
|
+
providerType,
|
|
1227
|
+
auth: auth,
|
|
1228
|
+
url
|
|
1229
|
+
}).catch(() => { });
|
|
1230
|
+
}
|
|
1231
|
+
return false;
|
|
984
1232
|
}
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1233
|
+
try {
|
|
1234
|
+
await withOAuthRepairEnv(providerType, async () => {
|
|
1235
|
+
await ensureValid(providerType, auth, {
|
|
1236
|
+
forceReacquireIfRefreshFails: false,
|
|
1237
|
+
openBrowser: false,
|
|
1238
|
+
forceReauthorize: false
|
|
1239
|
+
});
|
|
1240
|
+
});
|
|
1241
|
+
return true;
|
|
991
1242
|
}
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
looksInvalid = true;
|
|
1243
|
+
catch {
|
|
1244
|
+
// ignore silent refresh errors; fall through to background interactive flow
|
|
995
1245
|
}
|
|
996
|
-
|
|
997
|
-
|
|
1246
|
+
const interactiveOpts = {
|
|
1247
|
+
forceReacquireIfRefreshFails: true,
|
|
1248
|
+
openBrowser: true,
|
|
1249
|
+
// 上游已经明确返回“认证失效”(包括 iflow 的 406/439),
|
|
1250
|
+
// 此时强制跳过节流并允许走完整 OAuth 流程。
|
|
1251
|
+
forceReauthorize: pt === 'gemini' || pt === 'gemini-cli' || pt === 'antigravity' || pt === 'iflow' || pt === 'qwen'
|
|
1252
|
+
};
|
|
1253
|
+
void withOAuthRepairEnv(providerType, async () => {
|
|
1254
|
+
await ensureValid(providerType, auth, interactiveOpts);
|
|
1255
|
+
}).catch(() => {
|
|
1256
|
+
// background repair failure must never block requests
|
|
1257
|
+
});
|
|
998
1258
|
return false;
|
|
999
1259
|
}
|
|
1000
1260
|
const opts = {
|
|
@@ -1004,13 +1264,54 @@ export async function handleUpstreamInvalidOAuthToken(providerType, auth, upstre
|
|
|
1004
1264
|
// 此时强制跳过节流并允许走完整 OAuth 流程。
|
|
1005
1265
|
forceReauthorize: pt === 'gemini' || pt === 'gemini-cli' || pt === 'antigravity' || pt === 'iflow' || pt === 'qwen'
|
|
1006
1266
|
};
|
|
1007
|
-
await
|
|
1267
|
+
await withOAuthRepairEnv(providerType, async () => {
|
|
1268
|
+
await ensureValid(providerType, auth, opts);
|
|
1269
|
+
});
|
|
1008
1270
|
return true;
|
|
1009
1271
|
}
|
|
1010
1272
|
catch {
|
|
1011
1273
|
return false;
|
|
1012
1274
|
}
|
|
1013
1275
|
}
|
|
1276
|
+
export function shouldTriggerInteractiveOAuthRepair(providerType, upstreamError) {
|
|
1277
|
+
const pt = providerType.toLowerCase();
|
|
1278
|
+
const msg = upstreamError instanceof Error
|
|
1279
|
+
? upstreamError.message
|
|
1280
|
+
: upstreamError && typeof upstreamError === 'object' && typeof upstreamError.message === 'string'
|
|
1281
|
+
? String(upstreamError.message)
|
|
1282
|
+
: String(upstreamError || '');
|
|
1283
|
+
const lower = msg.toLowerCase();
|
|
1284
|
+
const statusCode = extractStatusCode(upstreamError);
|
|
1285
|
+
// 基本令牌失效判定:只看典型 OAuth 文案
|
|
1286
|
+
let looksInvalid = /invalid[_-]?token|invalid[_-]?grant|unauthenticated|unauthorized|token has expired|access token expired/.test(lower);
|
|
1287
|
+
// 对于 iflow / qwen,保留基于 401/403 的宽松判定,避免破坏既有行为。
|
|
1288
|
+
if (!looksInvalid && (pt === 'iflow' || pt === 'qwen')) {
|
|
1289
|
+
if (statusCode === 401 ||
|
|
1290
|
+
statusCode === 403 ||
|
|
1291
|
+
/\b401\b|\b403\b|40308/.test(msg)) {
|
|
1292
|
+
looksInvalid = true;
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
// 对于 gemini / gemini-cli / antigravity,排除纯服务开关类错误,
|
|
1296
|
+
// 但如果明确提示缺少 project_id 或需要重新 OAuth,则视为令牌失效。
|
|
1297
|
+
if (pt === 'gemini' || pt === 'gemini-cli' || pt === 'antigravity') {
|
|
1298
|
+
if (/service_disabled/.test(lower) || lower.includes('has not been used in project')) {
|
|
1299
|
+
looksInvalid = false;
|
|
1300
|
+
}
|
|
1301
|
+
if (lower.includes('project_id not found in token') ||
|
|
1302
|
+
lower.includes('please authenticate with google oauth first')) {
|
|
1303
|
+
looksInvalid = true;
|
|
1304
|
+
}
|
|
1305
|
+
// Antigravity/Gemini may return 403 "verify your account" / validation_required.
|
|
1306
|
+
// This is not a token-expired case, but it still requires an interactive OAuth/browser flow
|
|
1307
|
+
// to unblock the account. Treat it as "needs interactive reauth".
|
|
1308
|
+
if (statusCode === 403 &&
|
|
1309
|
+
isGoogleAccountVerificationRequiredMessage(lower)) {
|
|
1310
|
+
looksInvalid = true;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
return looksInvalid;
|
|
1314
|
+
}
|
|
1014
1315
|
async function inferIflowClientCredsFromLog() {
|
|
1015
1316
|
try {
|
|
1016
1317
|
const home = process.env.HOME || '';
|