@bitkyc08/opencodex 2.84.0 → 2.86.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -4
- package/bin/ocx.mjs +61 -3
- package/gui/dist/assets/App-BjlttVLy.js +58 -0
- package/gui/dist/assets/{Tray-C9TGtwad.js → Tray-DyR9yBee.js} +1 -1
- package/gui/dist/assets/index-BMssP4k3.js +94 -0
- package/gui/dist/assets/quota-summary-D8M1bc_R.js +1 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -2
- package/src/adapters/anthropic/beta-allowlist.ts +87 -8
- package/src/adapters/anthropic/client-identity.ts +1 -1
- package/src/adapters/anthropic/passthrough.ts +33 -4
- package/src/adapters/azure.ts +19 -1
- package/src/adapters/client-fingerprint.ts +24 -0
- package/src/adapters/coding-agent/turn.ts +22 -9
- package/src/adapters/cursor/arg-codec.ts +17 -5
- package/src/adapters/cursor/catalog.ts +8 -0
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/cursor/freeform-args.ts +75 -0
- package/src/adapters/cursor/protobuf-events.ts +36 -23
- package/src/adapters/google-antigravity-wire.ts +22 -1
- package/src/adapters/google.ts +2 -2
- package/src/adapters/openai-chat/response-events.ts +59 -1
- package/src/adapters/openai-chat.ts +8 -5
- package/src/adapters/qoder/adapter.ts +40 -1
- package/src/advisor/consult.ts +220 -0
- package/src/advisor/context.ts +226 -0
- package/src/advisor/disclosure.ts +20 -0
- package/src/advisor/runtime.ts +305 -0
- package/src/advisor/settings.ts +135 -0
- package/src/advisor/state.ts +369 -0
- package/src/advisor/synthetic-tool.ts +33 -0
- package/src/bridge/errors.ts +5 -0
- package/src/chat/inbound.ts +30 -6
- package/src/chat/outbound.ts +5 -0
- package/src/claude/desktop-3p-library.ts +43 -3
- package/src/claude/desktop-3p.ts +22 -8
- package/src/claude/desktop-picker-profile.ts +11 -4
- package/src/claude/desktop-profile.ts +10 -1
- package/src/claude/desktop-remote-store-io.ts +5 -3
- package/src/claude/inbound.ts +121 -50
- package/src/claude/intercept/local-ca.ts +8 -1
- package/src/claude/model-default-settings.ts +154 -0
- package/src/claude/model-info.ts +7 -7
- package/src/claude/outbound.ts +13 -6
- package/src/cli/advisor.ts +151 -0
- package/src/cli/auxiliary-listener-preflight.ts +37 -0
- package/src/cli/capabilities-agents-routing.ts +18 -0
- package/src/cli/capabilities-base.ts +1 -1
- package/src/cli/chatgpt-command.ts +35 -14
- package/src/cli/claude.ts +46 -6
- package/src/cli/dispatch.ts +5 -1
- package/src/cli/doctor.ts +9 -0
- package/src/cli/ensure-desktop-cold-start.ts +329 -0
- package/src/cli/export-command.ts +1 -1
- package/src/cli/help.ts +2 -1
- package/src/cli/index.ts +13 -4
- package/src/cli/integration-plan-dto.ts +2 -0
- package/src/cli/registry.ts +15 -3
- package/src/cli/restart-scope.ts +3 -1
- package/src/clients/config-export/contracts.ts +1 -0
- package/src/clients/config-export/model-metadata.ts +3 -2
- package/src/clients/config-export/qoder.ts +93 -0
- package/src/clients/config-export.ts +17 -4
- package/src/codex/auth-api/pool-mode-gate.ts +5 -2
- package/src/codex/auth-context.ts +79 -5
- package/src/codex/autostart-health.ts +4 -0
- package/src/codex/catalog/build-entries.ts +11 -2
- package/src/codex/catalog/configured-tiers.ts +50 -0
- package/src/codex/catalog/effort.ts +4 -1
- package/src/codex/catalog/model-hints.ts +21 -3
- package/src/codex/catalog/native-ultrafast.ts +106 -0
- package/src/codex/catalog/parsing.ts +5 -0
- package/src/codex/catalog/provider-models.ts +4 -0
- package/src/codex/catalog/retained-sync.ts +2 -0
- package/src/codex/catalog/routed-gather.ts +22 -4
- package/src/codex/catalog/vendor-context.ts +82 -0
- package/src/codex/config-write-lock.ts +40 -2
- package/src/codex/convergence.ts +2 -0
- package/src/codex/desktop-app/types.ts +2 -0
- package/src/codex/desktop-app/windows.ts +29 -16
- package/src/codex/desktop-app-restart.ts +4 -2
- package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
- package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
- package/src/codex/desktop-compatibility/connection-store.ts +87 -0
- package/src/codex/desktop-compatibility/installed-build.ts +55 -0
- package/src/codex/desktop-compatibility/json-body.ts +29 -0
- package/src/codex/desktop-compatibility/native-identity.ts +62 -0
- package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
- package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
- package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
- package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
- package/src/codex/desktop-compatibility/runtime.ts +173 -0
- package/src/codex/desktop-compatibility/service.ts +30 -0
- package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
- package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
- package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
- package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
- package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
- package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
- package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
- package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
- package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
- package/src/codex/desktop-compatibility/windows-key-protection.ts +59 -0
- package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
- package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
- package/src/codex/history-provider.ts +1 -1
- package/src/codex/inject/config-write-section.ts +4 -1
- package/src/codex/inject/remove.ts +18 -6
- package/src/codex/journal.ts +16 -1
- package/src/codex/main-account.ts +99 -15
- package/src/codex/native-main-refresh-mode.ts +217 -0
- package/src/codex/routing/health-store.ts +1 -1
- package/src/codex/routing/pool-recovery.ts +79 -0
- package/src/codex/shim-diagnostics.ts +6 -0
- package/src/codex/shim-overlay.ts +7 -2
- package/src/config/diagnostics.ts +2 -1
- package/src/config/live-reconcile.ts +13 -0
- package/src/config/load-degrade.ts +6 -1
- package/src/config/paths.ts +10 -3
- package/src/config/schema/config-schema.ts +17 -0
- package/src/config/schema/desktop-compatibility.ts +9 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +451 -163
- package/src/generated/model-metadata.ts +1 -1
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/config-io.ts +20 -2
- package/src/integrations/journal.ts +15 -1
- package/src/integrations/mutation-plan.ts +1 -0
- package/src/integrations/registry.ts +7 -0
- package/src/integrations/store.ts +3 -0
- package/src/integrations/writer.ts +10 -1
- package/src/lib/advisor-activation.ts +16 -0
- package/src/lib/background-work.ts +65 -0
- package/src/lib/bounded-file-read.ts +58 -0
- package/src/lib/bun-path-runtime.d.mts +10 -1
- package/src/lib/bun-path-runtime.mjs +62 -3
- package/src/lib/desktop-proxy-route.ts +30 -0
- package/src/lib/desktop-upstream-tunnel.ts +107 -0
- package/src/lib/errors.ts +7 -0
- package/src/lib/local-internal-call-capability.ts +55 -0
- package/src/lib/package-tree-integrity.ts +2 -2
- package/src/lib/socks5-fetch.ts +48 -71
- package/src/lib/socks5-handshake.ts +112 -0
- package/src/lib/translator-budget.ts +7 -1
- package/src/oauth/generic-account-failover.ts +29 -5
- package/src/oauth/index.ts +4 -2
- package/src/oauth/kiro-device-login.ts +2 -1
- package/src/oauth/kiro.ts +7 -1
- package/src/protocols/codecs/chat-ir.ts +467 -0
- package/src/protocols/codecs/chat.ts +10 -1
- package/src/protocols/codecs/messages-ir.ts +500 -0
- package/src/protocols/features.ts +10 -2
- package/src/protocols/path.ts +11 -1
- package/src/protocols/plan-snapshot.ts +39 -16
- package/src/protocols/plan.ts +9 -1
- package/src/protocols/settings.ts +8 -0
- package/src/providers/antigravity-account-roster.ts +102 -0
- package/src/providers/antigravity-models.ts +85 -7
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/model-rename-migration.ts +5 -3
- package/src/providers/registry/entries-core.ts +7 -6
- package/src/providers/registry/entries-extended.ts +19 -0
- package/src/responses/parser.ts +30 -19
- package/src/responses/state.ts +32 -29
- package/src/routing/history/build-file.ts +92 -0
- package/src/routing/history/indexer.ts +248 -216
- package/src/routing/history/ingest.ts +238 -0
- package/src/routing/history/rebuild-job.ts +111 -0
- package/src/routing/history/rebuild-protocol.ts +15 -0
- package/src/routing/history/rebuild-worker.ts +34 -0
- package/src/server/admission-model-scope.ts +4 -1
- package/src/server/antigravity-direct-eligibility.ts +208 -0
- package/src/server/auth-cors.ts +4 -0
- package/src/server/chat-completions.ts +75 -9
- package/src/server/claude-messages.ts +110 -20
- package/src/server/index/desktop-compatibility-startup.ts +56 -0
- package/src/server/index/serve-options.ts +2 -2
- package/src/server/index/startup-warnings.ts +9 -0
- package/src/server/index/websocket-handler.ts +9 -0
- package/src/server/index.ts +13 -10
- package/src/server/inference/client-encoder-delivery.ts +6 -5
- package/src/server/login-shell-path.ts +146 -0
- package/src/server/management/advisor-routes.ts +191 -0
- package/src/server/management/agent-settings-routes.ts +22 -4
- package/src/server/management/companion-routes.ts +4 -0
- package/src/server/management/config-routes.ts +2 -2
- package/src/server/management/context.ts +4 -0
- package/src/server/management/desktop-compatibility-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
- package/src/server/management/model-routes.ts +2 -2
- package/src/server/management/protocol-settings-patch.ts +1 -0
- package/src/server/management/route-registry.ts +11 -0
- package/src/server/management/sibling-guard.ts +1 -0
- package/src/server/management-api.ts +6 -0
- package/src/server/messages-antigravity-direct.ts +195 -0
- package/src/server/messages-native.ts +5 -1
- package/src/server/request-log.ts +7 -0
- package/src/server/responses/adapter-continuation.ts +35 -18
- package/src/server/responses/adapter-delivery.ts +24 -2
- package/src/server/responses/adapter-dispatch.ts +29 -2
- package/src/server/responses/advisor-plan-slot.ts +27 -0
- package/src/server/responses/advisor-slot.ts +336 -0
- package/src/server/responses/code-mode-collab-surface.ts +116 -8
- package/src/server/responses/core-combo-failure.ts +10 -1
- package/src/server/responses/core-combo.ts +21 -0
- package/src/server/responses/core-lifetime.ts +2 -0
- package/src/server/responses/core-normalize.ts +11 -0
- package/src/server/responses/core-options.ts +46 -0
- package/src/server/responses/core-replay.ts +7 -4
- package/src/server/responses/core.ts +1 -1
- package/src/server/responses/passthrough-delivery.ts +16 -10
- package/src/server/responses/passthrough-dispatch.ts +14 -3
- package/src/server/responses/request-prepare.ts +68 -5
- package/src/server/responses/request-transport.ts +16 -0
- package/src/server/responses/run-turn-execution.ts +70 -8
- package/src/server/responses/sidecar-execution.ts +78 -3
- package/src/server/responses/terminal-guard.ts +2 -1
- package/src/server/responses-undeclared-tool-guard.ts +30 -9
- package/src/server/startup-action-control.ts +16 -2
- package/src/server/startup-health-cache.ts +1 -0
- package/src/service/desktop-startup.ts +14 -0
- package/src/service/health-sentinel-main.ts +23 -4
- package/src/service/health-sentinel-probe.ts +24 -7
- package/src/service/logging.ts +72 -0
- package/src/tray/windows-tray.ps1 +43 -24
- package/src/types/config.ts +63 -6
- package/src/types/provider.ts +7 -0
- package/src/types/request.ts +15 -0
- package/src/types/tools.ts +2 -0
- package/src/update/async-check.ts +1 -1
- package/src/update/badge.ts +2 -2
- package/src/update/index.ts +88 -15
- package/src/update/job-supervised-restart.ts +107 -0
- package/src/update/job.ts +32 -32
- package/src/update/notify.ts +1 -1
- package/src/update/refresh-scheduler.ts +2 -2
- package/src/update/supervised-recovery.d.mts +27 -0
- package/src/update/supervised-recovery.mjs +157 -0
- package/src/usage/cache-diagnostic.ts +2 -0
- package/src/usage/expected-prices.ts +12 -0
- package/src/web-search/gemini-executor.ts +2 -2
- package/assets/sponsors/packycode.png +0 -0
- package/gui/dist/assets/App-CZtqiGcw.js +0 -56
- package/gui/dist/assets/index-CTm4l-HQ.js +0 -94
- package/gui/dist/assets/quota-summary-BbEeRfPY.js +0 -1
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-local internal-call capability.
|
|
3
|
+
*
|
|
4
|
+
* Some requests ARE the proxy itself talking to its own data plane: the advisor sidecar's
|
|
5
|
+
* loopback consultation is the first user. Such a request must be recognized as INTERNAL without
|
|
6
|
+
* trusting anything the client controls. A client-supplied header value is not evidence — any
|
|
7
|
+
* external caller can send it — and neither is the peer address: a public listener reached
|
|
8
|
+
* through Docker, WSL, a tunnel, or port forwarding can present as loopback on the last hop, as
|
|
9
|
+
* the local-management attestation notes.
|
|
10
|
+
*
|
|
11
|
+
* The authority is therefore process-owned: a 256-bit random value minted once per process, kept
|
|
12
|
+
* in memory only. It is never written to config or disk, never logged, never returned by the
|
|
13
|
+
* management API, never placed in usage or request metadata, and never forwarded upstream. A new
|
|
14
|
+
* process mints a new value, so a token captured from an older process is worthless. Comparison
|
|
15
|
+
* is timing-safe and shape-checked, reusing the same secret shape as the local attestation
|
|
16
|
+
* module.
|
|
17
|
+
*
|
|
18
|
+
* The same primitive is what the vision-describe fence would need for the same reason; wiring
|
|
19
|
+
* that surface is deliberately left out of this change (recorded as a pre-existing analogous
|
|
20
|
+
* issue) so the change stays scoped.
|
|
21
|
+
*/
|
|
22
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
23
|
+
import { isLocalAttestationSecret } from "./local-management-attestation";
|
|
24
|
+
|
|
25
|
+
/** Header an internal sidecar presents on its own loopback request. */
|
|
26
|
+
export const ADVISOR_INTERNAL_CAPABILITY_HEADER = "x-opencodex-advisor-internal";
|
|
27
|
+
|
|
28
|
+
let processCapability: string | null = null;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The current process's internal-call capability. Minted lazily on first use (one random draw,
|
|
32
|
+
* no I/O), so an install that never enables the advisor never generates one.
|
|
33
|
+
*/
|
|
34
|
+
export function internalCallCapability(): string {
|
|
35
|
+
if (processCapability === null) processCapability = randomBytes(32).toString("base64url");
|
|
36
|
+
return processCapability;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Test seam: install a deterministic value, or `null` to force a fresh mint. */
|
|
40
|
+
export function setInternalCallCapabilityForTests(value: string | null): void {
|
|
41
|
+
processCapability = value;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* True only when the supplied header value IS this process's capability. Shape-checked first so
|
|
46
|
+
* a malformed value cannot reach the comparison, then compared in constant time.
|
|
47
|
+
*/
|
|
48
|
+
export function isInternalCallCapability(supplied: string | null | undefined): boolean {
|
|
49
|
+
if (typeof supplied !== "string" || !isLocalAttestationSecret(supplied)) return false;
|
|
50
|
+
if (processCapability === null) return false;
|
|
51
|
+
const expected = processCapability;
|
|
52
|
+
const suppliedBytes = Buffer.from(supplied);
|
|
53
|
+
const expectedBytes = Buffer.from(expected);
|
|
54
|
+
return suppliedBytes.length === expectedBytes.length && timingSafeEqual(suppliedBytes, expectedBytes);
|
|
55
|
+
}
|
|
@@ -68,7 +68,7 @@ export interface PackageTreeIntegrityOptions {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
export type ObservePackageTree = () => PackageTreeObservation | null;
|
|
71
|
-
export type PackageTreeRuntimeInstall = "bun" | "mise" | "npm" | "pnpm" | "source";
|
|
71
|
+
export type PackageTreeRuntimeInstall = "bun" | "mise" | "npm" | "pnpm" | "source" | "standalone";
|
|
72
72
|
|
|
73
73
|
const packageManifestUrl = new URL("../../package.json", import.meta.url);
|
|
74
74
|
|
|
@@ -323,7 +323,7 @@ export function createRuntimePackageTreeIntegrityGuard(
|
|
|
323
323
|
now: () => number = Date.now,
|
|
324
324
|
options: PackageTreeIntegrityOptions = {},
|
|
325
325
|
): PackageTreeIntegrityGuard {
|
|
326
|
-
if (installer === "source" || isStandaloneBinary()) {
|
|
326
|
+
if (installer === "source" || installer === "standalone" || isStandaloneBinary()) {
|
|
327
327
|
return { status: () => ({ ok: true }), dispose: () => {} };
|
|
328
328
|
}
|
|
329
329
|
// The restart replacement spawns process.execPath, the bundled Bun in an npm install. Read it
|
package/src/lib/socks5-fetch.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import net, { type Socket } from "node:net";
|
|
2
2
|
import tls, { type TLSSocket } from "node:tls";
|
|
3
3
|
import { classifyContentCoding, isNullBodyStatus } from "./http-response-semantics";
|
|
4
|
+
import { socks5Credentials, socks5Handshake, Socks5HandshakeError } from "./socks5-handshake";
|
|
4
5
|
|
|
5
6
|
const DEFAULT_SOCKS5_PORT = 1080;
|
|
6
7
|
const SOCKS5_CONNECT_TIMEOUT_MS = 30_000;
|
|
@@ -10,11 +11,6 @@ const MAX_BODY_SLICE_BYTES = 64 * 1024;
|
|
|
10
11
|
const MAX_DECODED_BODY_BYTES = 32 * 1024 * 1024;
|
|
11
12
|
const MAX_STREAM_DECODE_EXPANSION_RATIO = 128;
|
|
12
13
|
const SOCKS5_VERSION = 0x05;
|
|
13
|
-
const SOCKS5_NO_AUTH = 0x00;
|
|
14
|
-
const SOCKS5_USER_PASS = 0x02;
|
|
15
|
-
const SOCKS5_CONNECT = 0x01;
|
|
16
|
-
const SOCKS5_DOMAIN = 0x03;
|
|
17
|
-
const SOCKS5_SUCCESS = 0x00;
|
|
18
14
|
const CRLF = Buffer.from("\r\n");
|
|
19
15
|
const HEADER_END = Buffer.from("\r\n\r\n");
|
|
20
16
|
|
|
@@ -22,24 +18,6 @@ export class Socks5FetchError extends Error {
|
|
|
22
18
|
override readonly name = "Socks5FetchError";
|
|
23
19
|
}
|
|
24
20
|
|
|
25
|
-
function proxyCredentials(proxy: URL): { username?: Uint8Array; password?: Uint8Array } {
|
|
26
|
-
if (!proxy.username && !proxy.password) return {};
|
|
27
|
-
let username: string;
|
|
28
|
-
let password: string;
|
|
29
|
-
try {
|
|
30
|
-
username = decodeURIComponent(proxy.username);
|
|
31
|
-
password = decodeURIComponent(proxy.password);
|
|
32
|
-
} catch {
|
|
33
|
-
throw new Socks5FetchError("SOCKS5 proxy credentials contain invalid percent encoding");
|
|
34
|
-
}
|
|
35
|
-
const usernameBytes = new TextEncoder().encode(username);
|
|
36
|
-
const passwordBytes = new TextEncoder().encode(password);
|
|
37
|
-
if (usernameBytes.byteLength > 255 || passwordBytes.byteLength > 255) {
|
|
38
|
-
throw new Socks5FetchError("SOCKS5 proxy credentials must each fit in 255 UTF-8 bytes");
|
|
39
|
-
}
|
|
40
|
-
return { username: usernameBytes, password: passwordBytes };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
21
|
function validateProxy(proxy: string): URL {
|
|
44
22
|
let parsed: URL;
|
|
45
23
|
try {
|
|
@@ -55,7 +33,10 @@ function validateProxy(proxy: string): URL {
|
|
|
55
33
|
throw new Socks5FetchError("SOCKS5 proxy port is invalid");
|
|
56
34
|
}
|
|
57
35
|
if (parsed.search || parsed.hash) throw new Socks5FetchError("SOCKS5 proxy URL must not contain a query or fragment");
|
|
58
|
-
|
|
36
|
+
try { socks5Credentials(parsed); } catch (error) {
|
|
37
|
+
if (error instanceof Socks5HandshakeError) throw new Socks5FetchError(error.message);
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
59
40
|
return parsed;
|
|
60
41
|
}
|
|
61
42
|
|
|
@@ -130,6 +111,14 @@ class SocketReader {
|
|
|
130
111
|
socket.resume();
|
|
131
112
|
}
|
|
132
113
|
|
|
114
|
+
write(bytes: Uint8Array): void {
|
|
115
|
+
this.socket.write(bytes);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
readExact(bytes: number, signal?: AbortSignal): Promise<Buffer> {
|
|
119
|
+
return this.read(bytes, signal);
|
|
120
|
+
}
|
|
121
|
+
|
|
133
122
|
private readonly onData = (chunk: Buffer | string): void => {
|
|
134
123
|
const value = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
135
124
|
if (this.anyWaiters.length > 0) {
|
|
@@ -263,7 +252,7 @@ class SocketReader {
|
|
|
263
252
|
|
|
264
253
|
async function socks5Connect(proxy: string, target: URL, signal?: AbortSignal): Promise<Socket> {
|
|
265
254
|
const parsedProxy = validateProxy(proxy);
|
|
266
|
-
const credentials =
|
|
255
|
+
const credentials = socks5Credentials(parsedProxy);
|
|
267
256
|
const proxyHost = parsedProxy.hostname.replace(/^\[|\]$/g, "");
|
|
268
257
|
const socket = await connectSocket(proxyHost, Number(parsedProxy.port) || DEFAULT_SOCKS5_PORT, signal);
|
|
269
258
|
socket.setTimeout(SOCKS5_CONNECT_TIMEOUT_MS, () => {
|
|
@@ -271,49 +260,12 @@ async function socks5Connect(proxy: string, target: URL, signal?: AbortSignal):
|
|
|
271
260
|
});
|
|
272
261
|
const reader = new SocketReader(socket);
|
|
273
262
|
try {
|
|
274
|
-
|
|
275
|
-
socket.write(Buffer.from([SOCKS5_VERSION, methods.byteLength, ...methods]));
|
|
276
|
-
const greeting = await reader.read(2, signal);
|
|
277
|
-
if (greeting[0] !== SOCKS5_VERSION) throw new Socks5FetchError("SOCKS5 proxy returned an invalid greeting");
|
|
278
|
-
if (greeting[1] === SOCKS5_USER_PASS && credentials.username && credentials.password) {
|
|
279
|
-
socket.write(Buffer.from([
|
|
280
|
-
0x01,
|
|
281
|
-
credentials.username.byteLength,
|
|
282
|
-
...credentials.username,
|
|
283
|
-
credentials.password.byteLength,
|
|
284
|
-
...credentials.password,
|
|
285
|
-
]));
|
|
286
|
-
const auth = await reader.read(2, signal);
|
|
287
|
-
if (auth[0] !== 0x01 || auth[1] !== 0x00) throw new Socks5FetchError("SOCKS5 proxy authentication failed");
|
|
288
|
-
} else if (greeting[1] !== SOCKS5_NO_AUTH) {
|
|
289
|
-
throw new Socks5FetchError("SOCKS5 proxy does not accept an offered authentication method");
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const hostname = new TextEncoder().encode(target.hostname);
|
|
293
|
-
if (hostname.byteLength > 255) throw new Socks5FetchError("SOCKS5 target hostname is too long");
|
|
294
|
-
const port = targetPort(target);
|
|
295
|
-
socket.write(Buffer.from([
|
|
296
|
-
SOCKS5_VERSION,
|
|
297
|
-
SOCKS5_CONNECT,
|
|
298
|
-
0x00,
|
|
299
|
-
SOCKS5_DOMAIN,
|
|
300
|
-
hostname.byteLength,
|
|
301
|
-
...hostname,
|
|
302
|
-
port >> 8,
|
|
303
|
-
port & 0xff,
|
|
304
|
-
]));
|
|
305
|
-
const reply = await reader.read(4, signal);
|
|
306
|
-
if (reply[0] !== SOCKS5_VERSION) throw new Socks5FetchError("SOCKS5 proxy returned an invalid connect response");
|
|
307
|
-
if (reply[1] !== SOCKS5_SUCCESS) throw new Socks5FetchError(`SOCKS5 proxy refused the connection (code ${reply[1]})`);
|
|
308
|
-
if (reply[2] !== 0x00 || ![0x01, SOCKS5_DOMAIN, 0x04].includes(reply[3]!)) {
|
|
309
|
-
throw new Socks5FetchError("SOCKS5 proxy returned an invalid address type or reserved byte");
|
|
310
|
-
}
|
|
311
|
-
const addressLength = reply[3] === 0x01 ? 4 : reply[3] === SOCKS5_DOMAIN ? (await reader.read(1, signal))[0]! : 16;
|
|
312
|
-
await reader.read(addressLength + 2, signal);
|
|
263
|
+
await socks5Handshake(reader, { host: target.hostname, port: targetPort(target) }, credentials, signal);
|
|
313
264
|
socket.setTimeout(0);
|
|
314
265
|
return socket;
|
|
315
266
|
} catch (error) {
|
|
316
267
|
socket.destroy();
|
|
268
|
+
if (error instanceof Socks5HandshakeError) throw new Socks5FetchError(error.message);
|
|
317
269
|
throw error;
|
|
318
270
|
} finally {
|
|
319
271
|
reader.dispose();
|
|
@@ -341,25 +293,50 @@ function requestHeaders(request: Request, target: URL): { text: string; chunked:
|
|
|
341
293
|
|
|
342
294
|
async function secureSocket(socket: Socket, target: URL, signal: AbortSignal): Promise<TLSSocket> {
|
|
343
295
|
return new Promise((resolve, reject) => {
|
|
296
|
+
if (signal.aborted) {
|
|
297
|
+
socket.destroy();
|
|
298
|
+
reject(signal.reason instanceof Error ? signal.reason : new Error("The operation was aborted"));
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
344
301
|
const tlsSocket = tls.connect({
|
|
345
302
|
socket,
|
|
346
303
|
servername: target.hostname,
|
|
347
304
|
rejectUnauthorized: true,
|
|
348
305
|
});
|
|
349
|
-
|
|
306
|
+
let settled = false;
|
|
307
|
+
// Absolute deadline: a peer sending occasional TLS bytes must not extend the handshake.
|
|
308
|
+
const timer = setTimeout(() => {
|
|
309
|
+
fail(new Socks5FetchError("SOCKS5 TLS handshake timed out"));
|
|
310
|
+
}, SOCKS5_CONNECT_TIMEOUT_MS);
|
|
311
|
+
const cleanup = () => {
|
|
312
|
+
clearTimeout(timer);
|
|
313
|
+
signal.removeEventListener("abort", onAbort);
|
|
314
|
+
tlsSocket.removeListener("secureConnect", onSecureConnect);
|
|
315
|
+
tlsSocket.removeListener("error", onError);
|
|
316
|
+
tlsSocket.removeListener("close", onClose);
|
|
317
|
+
};
|
|
318
|
+
const fail = (error: Error) => {
|
|
319
|
+
if (settled) return;
|
|
320
|
+
settled = true;
|
|
321
|
+
cleanup();
|
|
322
|
+
// No error argument: pending event observers must not receive a second error.
|
|
350
323
|
tlsSocket.destroy();
|
|
351
|
-
reject(
|
|
324
|
+
reject(error);
|
|
352
325
|
};
|
|
326
|
+
const onAbort = () => fail(signal.reason instanceof Error ? signal.reason : new Error("The operation was aborted"));
|
|
327
|
+
const onError = (error: Error) => fail(error);
|
|
328
|
+
const onClose = () => fail(new Socks5FetchError("SOCKS5 upstream closed during TLS handshake"));
|
|
353
329
|
const onSecureConnect = () => {
|
|
354
|
-
|
|
330
|
+
if (settled) return;
|
|
331
|
+
settled = true;
|
|
332
|
+
cleanup();
|
|
355
333
|
resolve(tlsSocket);
|
|
356
334
|
};
|
|
357
335
|
tlsSocket.once("secureConnect", onSecureConnect);
|
|
358
|
-
tlsSocket.once("error",
|
|
359
|
-
|
|
360
|
-
reject(error);
|
|
361
|
-
});
|
|
336
|
+
tlsSocket.once("error", onError);
|
|
337
|
+
tlsSocket.once("close", onClose);
|
|
362
338
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
339
|
+
if (signal.aborted) onAbort();
|
|
363
340
|
});
|
|
364
341
|
}
|
|
365
342
|
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SOCKS5 handshake both raw outbound transports share: method negotiation
|
|
3
|
+
* (RFC 1928), the optional RFC 1929 username/password subnegotiation, and the
|
|
4
|
+
* CONNECT exchange.
|
|
5
|
+
*
|
|
6
|
+
* `src/lib/socks5-fetch.ts` (the HTTP tunnel) and
|
|
7
|
+
* `src/lib/desktop-upstream-tunnel.ts` (the native desktop relay's raw
|
|
8
|
+
* dial) each own their socket, their timeouts, and what happens after CONNECT;
|
|
9
|
+
* this module only frames bytes through the reader each hands it, so a
|
|
10
|
+
* credentialed `socks5://` proxy URL authenticates identically whether the caller
|
|
11
|
+
* is a fetch or a raw WebSocket upgrade. When the URL carries credentials the
|
|
12
|
+
* greeting offers NO-AUTH alongside USER-PASS, which keeps an unauthenticated
|
|
13
|
+
* proxy working for a caller that configured more than it needed.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export class Socks5HandshakeError extends Error {
|
|
17
|
+
override readonly name = "Socks5HandshakeError";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const SOCKS5_VERSION = 0x05;
|
|
21
|
+
const SOCKS5_NO_AUTH = 0x00;
|
|
22
|
+
const SOCKS5_USER_PASS = 0x02;
|
|
23
|
+
const SOCKS5_CONNECT = 0x01;
|
|
24
|
+
const SOCKS5_DOMAIN = 0x03;
|
|
25
|
+
const SOCKS5_SUCCESS = 0x00;
|
|
26
|
+
|
|
27
|
+
/** RFC 1929 credentials decoded from the proxy URL, each bounded to one length byte. */
|
|
28
|
+
export interface Socks5Credentials {
|
|
29
|
+
username?: Uint8Array;
|
|
30
|
+
password?: Uint8Array;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function socks5Credentials(proxy: URL): Socks5Credentials {
|
|
34
|
+
if (!proxy.username && !proxy.password) return {};
|
|
35
|
+
let username: string;
|
|
36
|
+
let password: string;
|
|
37
|
+
try {
|
|
38
|
+
username = decodeURIComponent(proxy.username);
|
|
39
|
+
password = decodeURIComponent(proxy.password);
|
|
40
|
+
} catch {
|
|
41
|
+
throw new Socks5HandshakeError("SOCKS5 proxy credentials contain invalid percent encoding");
|
|
42
|
+
}
|
|
43
|
+
const usernameBytes = new TextEncoder().encode(username);
|
|
44
|
+
const passwordBytes = new TextEncoder().encode(password);
|
|
45
|
+
if (usernameBytes.byteLength > 255 || passwordBytes.byteLength > 255) {
|
|
46
|
+
throw new Socks5HandshakeError("SOCKS5 proxy credentials must each fit in 255 UTF-8 bytes");
|
|
47
|
+
}
|
|
48
|
+
return { username: usernameBytes, password: passwordBytes };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The slice of a transport's socket reader the handshake needs: exact-length reads
|
|
53
|
+
* that reject on close, on error, or on the transport's own timeout, plus writes.
|
|
54
|
+
* Bytes read past a step stay queued in the transport's reader; the handshake never
|
|
55
|
+
* touches the socket itself.
|
|
56
|
+
*/
|
|
57
|
+
export interface Socks5HandshakeReader {
|
|
58
|
+
write(bytes: Uint8Array): void;
|
|
59
|
+
readExact(bytes: number, signal?: AbortSignal): Promise<Uint8Array>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface Socks5Target {
|
|
63
|
+
host: string;
|
|
64
|
+
port: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Negotiate methods, authenticate when the proxy picks USER-PASS, and CONNECT to `target`. */
|
|
68
|
+
export async function socks5Handshake(
|
|
69
|
+
reader: Socks5HandshakeReader,
|
|
70
|
+
target: Socks5Target,
|
|
71
|
+
credentials: Socks5Credentials,
|
|
72
|
+
signal?: AbortSignal,
|
|
73
|
+
): Promise<void> {
|
|
74
|
+
const hostBytes = Buffer.from(target.host, "utf8");
|
|
75
|
+
if (hostBytes.byteLength > 255) throw new Socks5HandshakeError("SOCKS5 target hostname is too long");
|
|
76
|
+
const methods = credentials.username ? [SOCKS5_NO_AUTH, SOCKS5_USER_PASS] : [SOCKS5_NO_AUTH];
|
|
77
|
+
reader.write(Buffer.from([SOCKS5_VERSION, methods.length, ...methods]));
|
|
78
|
+
const greeting = await reader.readExact(2, signal);
|
|
79
|
+
if (greeting[0] !== SOCKS5_VERSION) throw new Socks5HandshakeError("SOCKS5 proxy returned an invalid greeting");
|
|
80
|
+
if (greeting[1] === SOCKS5_USER_PASS && credentials.username && credentials.password) {
|
|
81
|
+
reader.write(Buffer.from([
|
|
82
|
+
0x01,
|
|
83
|
+
credentials.username.byteLength,
|
|
84
|
+
...credentials.username,
|
|
85
|
+
credentials.password.byteLength,
|
|
86
|
+
...credentials.password,
|
|
87
|
+
]));
|
|
88
|
+
const auth = await reader.readExact(2, signal);
|
|
89
|
+
if (auth[0] !== 0x01 || auth[1] !== 0x00) throw new Socks5HandshakeError("SOCKS5 proxy authentication failed");
|
|
90
|
+
} else if (greeting[1] !== SOCKS5_NO_AUTH) {
|
|
91
|
+
throw new Socks5HandshakeError("SOCKS5 proxy does not accept an offered authentication method");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
reader.write(Buffer.from([
|
|
95
|
+
SOCKS5_VERSION,
|
|
96
|
+
SOCKS5_CONNECT,
|
|
97
|
+
0x00,
|
|
98
|
+
SOCKS5_DOMAIN,
|
|
99
|
+
hostBytes.byteLength,
|
|
100
|
+
...hostBytes,
|
|
101
|
+
target.port >> 8,
|
|
102
|
+
target.port & 0xff,
|
|
103
|
+
]));
|
|
104
|
+
const reply = await reader.readExact(4, signal);
|
|
105
|
+
if (reply[0] !== SOCKS5_VERSION) throw new Socks5HandshakeError("SOCKS5 proxy returned an invalid connect response");
|
|
106
|
+
if (reply[1] !== SOCKS5_SUCCESS) throw new Socks5HandshakeError(`SOCKS5 proxy refused the connection (code ${reply[1]})`);
|
|
107
|
+
if (reply[2] !== 0x00 || ![0x01, SOCKS5_DOMAIN, 0x04].includes(reply[3]!)) {
|
|
108
|
+
throw new Socks5HandshakeError("SOCKS5 proxy returned an invalid address type or reserved byte");
|
|
109
|
+
}
|
|
110
|
+
const addressLength = reply[3] === 0x01 ? 4 : reply[3] === SOCKS5_DOMAIN ? (await reader.readExact(1, signal))[0]! : 16;
|
|
111
|
+
await reader.readExact(addressLength + 2, signal);
|
|
112
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isNonReplayableResponse, markResponseNonReplayable } from "./upstream-retry";
|
|
2
|
+
|
|
1
3
|
export const TRANSLATOR_MAX_CALL_ARGUMENT_BYTES = 2 * 1024 * 1024;
|
|
2
4
|
export const TRANSLATOR_MAX_TURN_BYTES = 32 * 1024 * 1024;
|
|
3
5
|
export const TRANSLATOR_MAX_SSE_EVENT_BYTES = 32 * 1024 * 1024;
|
|
@@ -410,5 +412,9 @@ export function finalizeTranslatorBudgetResponse(response: Response, budget: Tra
|
|
|
410
412
|
});
|
|
411
413
|
if (signal?.aborted) finalize();
|
|
412
414
|
else signal?.addEventListener("abort", finalize, { once: true });
|
|
413
|
-
|
|
415
|
+
const finalizedResponse = new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
|
|
416
|
+
// The rebuild above drops identity markers. A refusal that must not be replayed
|
|
417
|
+
// stays non-replayable through accounting finalization.
|
|
418
|
+
if (isNonReplayableResponse(response)) markResponseNonReplayable(finalizedResponse);
|
|
419
|
+
return finalizedResponse;
|
|
414
420
|
}
|
|
@@ -41,6 +41,7 @@ import type { KiroRefusalKind } from "../adapters/kiro-refusal";
|
|
|
41
41
|
import { kiroAccountEvidence } from "../providers/kiro-usage";
|
|
42
42
|
import { kiroEvidenceIdentity } from "../providers/kiro-account-state-disk";
|
|
43
43
|
import { kiroAccountSupportsModel } from "../providers/kiro-model-catalog";
|
|
44
|
+
import { antigravityAccountSupportsModel } from "../providers/antigravity-account-roster";
|
|
44
45
|
import { ACCOUNT_QUOTA_TTL_MS } from "../providers/quota-wire";
|
|
45
46
|
import { sweepExpiredOnWrite } from "../lib/state-store-sweeper";
|
|
46
47
|
import { subscribeOAuthAccountPauseChanges } from "../lib/account-selection-events";
|
|
@@ -248,11 +249,31 @@ export function eligibleFailoverAccounts(providerName: string, now = Date.now(),
|
|
|
248
249
|
return eligibleIdsIn(getAccountSet(providerName), providerName, now, family);
|
|
249
250
|
}
|
|
250
251
|
|
|
251
|
-
/**
|
|
252
|
+
/** Whether an account's own model listing includes `model`; undefined when unknown. */
|
|
253
|
+
function accountListsModel(providerName: string, accountId: string, model: string): boolean | undefined {
|
|
254
|
+
if (providerName === "kiro") return kiroAccountSupportsModel(accountId, model);
|
|
255
|
+
if (providerName === "google-antigravity") return antigravityAccountSupportsModel(providerName, accountId, model);
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Model evidence only ranks accounts already eligible and below any configured cap.
|
|
261
|
+
*
|
|
262
|
+
* Kiro and Antigravity both gate models per account. For Antigravity (Claude 4.6 vs 5.5, Argon)
|
|
263
|
+
* an account whose discovered roster lists the model wins; an account whose roster omits it is
|
|
264
|
+
* skipped while any other remains; an account with no roster yet is unknown and stays usable.
|
|
265
|
+
*/
|
|
252
266
|
export function preferKiroModelSupport(
|
|
253
267
|
providerName: string, ids: string[], model?: string | null, config?: OcxConfig,
|
|
254
268
|
): string[] {
|
|
255
|
-
if (
|
|
269
|
+
if (!model) return ids;
|
|
270
|
+
if (providerName === "google-antigravity") {
|
|
271
|
+
const positive = ids.filter(id => accountListsModel(providerName, id, model) === true);
|
|
272
|
+
if (positive.length) return positive;
|
|
273
|
+
const notListedAgainst = ids.filter(id => accountListsModel(providerName, id, model) !== false);
|
|
274
|
+
return notListedAgainst.length ? notListedAgainst : ids;
|
|
275
|
+
}
|
|
276
|
+
if (providerName !== "kiro") return ids;
|
|
256
277
|
const cap = config?.providers?.kiro?.oauthAccountFailover?.maxConcurrentPerAccount;
|
|
257
278
|
const withRoom = typeof cap === "number" && Number.isInteger(cap) && cap >= 1 && cap <= 100
|
|
258
279
|
? ids.filter(id => accountInFlight("kiro", id) < cap) : ids;
|
|
@@ -631,10 +652,13 @@ export function preferredInitialAccount(
|
|
|
631
652
|
const modelEligible = preferKiroModelSupport(providerName,
|
|
632
653
|
eligibleFailoverAccounts(providerName, now, classifyModelFamilyForQuota(providerName, requestedModelId)),
|
|
633
654
|
requestedModelId, config);
|
|
634
|
-
const activeLacks = providerName === "kiro"
|
|
635
|
-
&&
|
|
655
|
+
const activeLacks = (providerName === "kiro" || providerName === "google-antigravity")
|
|
656
|
+
&& !!active && !!requestedModelId
|
|
657
|
+
&& accountListsModel(providerName, active, requestedModelId) === false;
|
|
636
658
|
const listingSibling = activeLacks
|
|
637
|
-
? modelEligible.find(id => id !== active &&
|
|
659
|
+
? modelEligible.find(id => id !== active && (providerName === "kiro"
|
|
660
|
+
? kiroModelListingHasRoom(config, id, requestedModelId!)
|
|
661
|
+
: accountListsModel(providerName, id, requestedModelId!) === true))
|
|
638
662
|
: undefined;
|
|
639
663
|
if (listingSibling) return listingSibling;
|
|
640
664
|
|
package/src/oauth/index.ts
CHANGED
|
@@ -49,7 +49,7 @@ import { loginGithubCopilot, refreshGithubCopilotToken, validateCopilotApiBaseUr
|
|
|
49
49
|
import { loginCommandCode, refreshCommandCodeToken } from "./command-code";
|
|
50
50
|
import { loginMetaMuse, refreshMetaMuseToken } from "./meta-muse";
|
|
51
51
|
import { loginOrcaRouter, orcaRouterInferenceBaseUrl, refreshOrcaRouterKey } from "./orcarouter";
|
|
52
|
-
import {
|
|
52
|
+
import { ANTIGRAVITY_DISCOVERY_UA } from "../adapters/google-antigravity-wire";
|
|
53
53
|
import { deriveOAuthDefaultModel, deriveOAuthProviderConfig } from "../providers/derive";
|
|
54
54
|
import { apiKeyPoolEntryId, sanitizeApiKeyValue } from "../providers/api-keys";
|
|
55
55
|
import { effectiveGoogleMode, getProviderRegistryEntry, mergeRegistryStaticHeaders, providerMatchesRegistryTransport } from "../providers/registry";
|
|
@@ -1161,7 +1161,9 @@ export function buildModelsRequest(
|
|
|
1161
1161
|
if (effectiveGoogleMode(providerName, effectiveProvider) === "cloud-code-assist") {
|
|
1162
1162
|
headers.Accept = "application/json";
|
|
1163
1163
|
headers["Content-Type"] = "application/json";
|
|
1164
|
-
|
|
1164
|
+
// CLI UA: CCA lists Gemini 4 Argon's wire ids only to the CLI client family. Inference keeps
|
|
1165
|
+
// choosing per wire model (antigravityUserAgentForWireModel).
|
|
1166
|
+
headers["User-Agent"] = ANTIGRAVITY_DISCOVERY_UA;
|
|
1165
1167
|
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
1166
1168
|
return guarded({
|
|
1167
1169
|
method: "POST" as const,
|
|
@@ -250,9 +250,10 @@ async function poll(flow: Flow, principal: KiroDevicePrincipal): Promise<KiroDev
|
|
|
250
250
|
if (data.status === "authorization_pending") return publicView(flow);
|
|
251
251
|
if (data.status === "expired_token" || data.status === "expired") return terminal(flow, "expired");
|
|
252
252
|
}
|
|
253
|
+
// The social poll reports a completed grant as status "authorized"; "approved" is kept as an alias.
|
|
253
254
|
if (result.status !== 200 || Object.hasOwn(data, "error") || error || (builder && (data.status !== undefined || !positive(data.expiresIn) || !isStorableKiroClientPart(flow.clientId)
|
|
254
255
|
|| !isStorableKiroClientPart(flow.clientSecret)))
|
|
255
|
-
|| (!builder && (data.status !== undefined && data.status !== "approved"))) return terminal(flow, "failed");
|
|
256
|
+
|| (!builder && (data.status !== undefined && data.status !== "authorized" && data.status !== "approved"))) return terminal(flow, "failed");
|
|
256
257
|
if (!code(data.accessToken) || !code(data.refreshToken)
|
|
257
258
|
|| (!builder && (typeof data.profileArn !== "string" || !PROFILE_ARN.test(data.profileArn)))) return terminal(flow, "failed");
|
|
258
259
|
const credential: OAuthCredentials = {
|
package/src/oauth/kiro.ts
CHANGED
|
@@ -45,6 +45,10 @@ const KIRO_TERMINAL_REFRESH_ERRORS = new Set([
|
|
|
45
45
|
"access_denied",
|
|
46
46
|
"expired_token",
|
|
47
47
|
]);
|
|
48
|
+
// The desktop refresh endpoint rejects a dead refresh token with HTTP 401 and a bare
|
|
49
|
+
// `{"message":"Bad credentials"}` body, without an OAuth `error` code. Only that exact
|
|
50
|
+
// pair is a terminal verdict; it is surfaced under this fixed code, never the raw text.
|
|
51
|
+
const KIRO_DESKTOP_BAD_CREDENTIALS = "bad_credentials";
|
|
48
52
|
|
|
49
53
|
interface ImportedKiroToken {
|
|
50
54
|
access: string;
|
|
@@ -545,12 +549,14 @@ async function kiroTokenRefreshError(response: Response, awsSsoRefresh = false):
|
|
|
545
549
|
let oauthError: string | undefined;
|
|
546
550
|
let refreshAttention = false;
|
|
547
551
|
try {
|
|
548
|
-
const payload = await response.json() as { error?: unknown };
|
|
552
|
+
const payload = await response.json() as { error?: unknown; message?: unknown };
|
|
549
553
|
// This input-error code is attention evidence only for our AWS SSO refresh request,
|
|
550
554
|
// never a terminal grant verdict. Do not surface arbitrary upstream descriptions.
|
|
551
555
|
refreshAttention = awsSsoRefresh && response.status === 400 && payload.error === "invalid_request";
|
|
552
556
|
if (typeof payload.error === "string" && KIRO_TERMINAL_REFRESH_ERRORS.has(payload.error)) {
|
|
553
557
|
oauthError = payload.error;
|
|
558
|
+
} else if (!awsSsoRefresh && response.status === 401 && payload.message === "Bad credentials") {
|
|
559
|
+
oauthError = KIRO_DESKTOP_BAD_CREDENTIALS;
|
|
554
560
|
}
|
|
555
561
|
} catch {
|
|
556
562
|
// Error bodies are untrusted and intentionally excluded from the surfaced message.
|