sneakoscope 8.1.2 → 8.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -58
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/dist/bin/fast-inline.js +5 -51
- package/dist/cli/command-manifest-lite.js +2 -1
- package/dist/cli/command-registry.js +2 -1
- package/dist/cli/global-mode-router.js +1 -1
- package/dist/cli/install-helpers-codex-lb-config.js +112 -914
- package/dist/cli/install-helpers-codex-lb-shared.js +3 -44
- package/dist/cli/install-helpers-install-support.js +4 -20
- package/dist/cli/install-helpers.js +36 -2716
- package/dist/cli/router.js +9 -9
- package/dist/commands/bridge-command-output.js +111 -0
- package/dist/commands/bridge.js +320 -0
- package/dist/commands/codex-app.js +39 -123
- package/dist/commands/doctor-helpers.js +380 -0
- package/dist/commands/doctor-profile.js +3 -0
- package/dist/commands/doctor.js +114 -873
- package/dist/commands/mad-sks.js +0 -4
- package/dist/config/skills-manifest.json +1 -1
- package/dist/core/agents/agent-effort-policy.js +11 -21
- package/dist/core/agents/agent-recursion-guard.js +26 -0
- package/dist/core/agents/agent-runner-codex-exec.js +7 -9
- package/dist/core/architecture-hardening/contracts/contracts.js +225 -161
- package/dist/core/architecture-hardening/state/state-service.js +49 -13
- package/dist/core/auto-review.js +15 -9
- package/dist/core/codex/codex-config-guard.js +8 -0
- package/dist/core/codex-adapter.js +8 -10
- package/dist/core/codex-app/codex-app-fast-ui-repair.js +7 -29
- package/dist/core/codex-app/codex-app-launcher.js +10 -12
- package/dist/core/codex-app/codex-app-restart.js +10 -12
- package/dist/core/codex-app/menubar/constants.js +2 -5
- package/dist/core/codex-app/menubar/global-install.js +3 -2
- package/dist/core/codex-app/menubar/installer/result.js +23 -0
- package/dist/core/codex-app/menubar/installer/runtime.js +28 -0
- package/dist/core/codex-app/menubar/installer.js +5 -96
- package/dist/core/codex-app/menubar/migration.js +1 -42
- package/dist/core/codex-app/openrouter-provider.js +0 -78
- package/dist/core/codex-app.js +97 -360
- package/dist/core/codex-control/codex-app-server-runtime-env.js +8 -28
- package/dist/core/codex-control/codex-current-image-path-real-probe.js +2 -3
- package/dist/core/codex-control/codex-current-web-search-probe.js +2 -3
- package/dist/core/codex-control/codex-sdk-adapter.js +5 -48
- package/dist/core/codex-control/codex-task-runner.js +16 -60
- package/dist/core/codex-control/desktop-bridge-launch-guard.js +227 -0
- package/dist/core/codex-exec-output-schema.js +11 -13
- package/dist/core/codex-lb/bridge-contracts.js +2 -0
- package/dist/core/codex-lb/bridge-runtime-validation/capability.js +104 -0
- package/dist/core/codex-lb/bridge-runtime-validation/catalog.js +52 -0
- package/dist/core/codex-lb/bridge-runtime-validation/shared.js +109 -0
- package/dist/core/codex-lb/bridge-runtime-validation/status.js +250 -0
- package/dist/core/codex-lb/bridge-runtime-validation.js +106 -0
- package/dist/core/codex-lb/capability-runner.js +505 -201
- package/dist/core/codex-lb/codex-lb-env.js +179 -12
- package/dist/core/codex-lb/combined-catalog/builder.js +86 -0
- package/dist/core/codex-lb/combined-catalog/contracts.js +5 -0
- package/dist/core/codex-lb/combined-catalog/normalize.js +170 -0
- package/dist/core/codex-lb/combined-catalog/persistence.js +207 -0
- package/dist/core/codex-lb/combined-catalog/shared.js +12 -0
- package/dist/core/codex-lb/combined-catalog/storage.js +171 -0
- package/dist/core/codex-lb/combined-catalog.js +12 -0
- package/dist/core/codex-lb/desktop-bridge/client.js +4 -1
- package/dist/core/codex-lb/desktop-bridge/header-policy.js +48 -63
- package/dist/core/codex-lb/desktop-bridge/http-forward.js +115 -83
- package/dist/core/codex-lb/desktop-bridge/launchd.js +20 -13
- package/dist/core/codex-lb/desktop-bridge/location-rewrite.js +4 -1
- package/dist/core/codex-lb/desktop-bridge/security.js +381 -267
- package/dist/core/codex-lb/desktop-bridge/server.js +148 -19
- package/dist/core/codex-lb/desktop-bridge/state.js +56 -70
- package/dist/core/codex-lb/desktop-bridge/types.js +6 -1
- package/dist/core/codex-lb/desktop-bridge/websocket-forward.js +281 -106
- package/dist/core/codex-lb/desktop-bridge-migration/historical-config.js +245 -0
- package/dist/core/codex-lb/desktop-bridge-migration/historical-toml.js +56 -0
- package/dist/core/codex-lb/desktop-bridge-migration/metadata.js +73 -0
- package/dist/core/codex-lb/desktop-bridge-migration/receipt.js +52 -0
- package/dist/core/codex-lb/desktop-bridge-migration/retired-runtime-cleanup.js +106 -0
- package/dist/core/codex-lb/desktop-bridge-migration/types.js +1 -0
- package/dist/core/codex-lb/desktop-bridge-migration.js +238 -0
- package/dist/core/codex-lb/desktop-controller-v3/catalog.js +257 -0
- package/dist/core/codex-lb/desktop-controller-v3/diagnostics.js +62 -0
- package/dist/core/codex-lb/desktop-controller-v3/lifecycle-commands.js +203 -0
- package/dist/core/codex-lb/desktop-controller-v3/live-probes.js +293 -0
- package/dist/core/codex-lb/desktop-controller-v3/preflight-probes.js +144 -0
- package/dist/core/codex-lb/desktop-controller-v3/provider-commands.js +182 -0
- package/dist/core/codex-lb/desktop-controller-v3/shared.js +279 -0
- package/dist/core/codex-lb/desktop-controller-v3/status.js +269 -0
- package/dist/core/codex-lb/desktop-controller-v3/types.js +1 -0
- package/dist/core/codex-lb/desktop-controller-v3/verification.js +89 -0
- package/dist/core/codex-lb/desktop-controller-v3.js +67 -0
- package/dist/core/codex-lb/desktop-controller.js +2 -1730
- package/dist/core/codex-lb/desktop-service.js +462 -627
- package/dist/core/codex-lb/migration-receipt.js +560 -118
- package/dist/core/codex-lb/probes/auxiliary-surfaces-probe.js +37 -53
- package/dist/core/codex-lb/probes/bridge-probe.js +125 -64
- package/dist/core/codex-lb/probes/catalog-probe.js +36 -136
- package/dist/core/codex-lb/probes/computer-use-probe.js +63 -39
- package/dist/core/codex-lb/probes/image-generation-probe.js +98 -41
- package/dist/core/codex-lb/probes/probe-evidence.js +32 -33
- package/dist/core/codex-lb/probes/voice-realtime-probe.js +69 -43
- package/dist/core/codex-lb/provider-credentials/configuration.js +127 -0
- package/dist/core/codex-lb/provider-credentials/locks.js +11 -0
- package/dist/core/codex-lb/provider-credentials/private-snapshot.js +28 -0
- package/dist/core/codex-lb/provider-credentials/removal.js +33 -0
- package/dist/core/codex-lb/provider-credentials/resolution.js +59 -0
- package/dist/core/codex-lb/provider-credentials/runtime.js +55 -0
- package/dist/core/codex-lb/provider-credentials/types.js +1 -0
- package/dist/core/codex-lb/provider-credentials/validation-store.js +137 -0
- package/dist/core/codex-lb/provider-credentials.js +5 -0
- package/dist/core/codex-lb/provider-registry.js +323 -0
- package/dist/core/codex-lb/provider-route-policy.js +118 -0
- package/dist/core/codex-lb/request-route-resolver.js +172 -0
- package/dist/core/codex-lb/route-index.js +148 -0
- package/dist/core/codex-lb/trusted-deep-evidence.js +158 -286
- package/dist/core/commands/basic-cli.js +1 -1
- package/dist/core/commands/check-command.js +3 -10
- package/dist/core/commands/gate-result-contract.js +10 -14
- package/dist/core/commands/intent-normalization/normalizer.js +1 -1
- package/dist/core/commands/mad-sks-command.js +2 -6
- package/dist/core/commands/release-command.js +11 -1
- package/dist/core/commands/telegram-command.js +46 -6
- package/dist/core/doctor/doctor-readiness-matrix.js +9 -7
- package/dist/core/doctor/skill-legacy-surface.js +5 -5
- package/dist/core/evidence/v2/evidence-key.js +30 -3
- package/dist/core/feature-fixtures.js +8 -1
- package/dist/core/feature-registry.js +3 -2
- package/dist/core/hooks-runtime/stop-finalization.js +11 -1
- package/dist/core/hooks-runtime/subagent-context.js +15 -1
- package/dist/core/hooks-runtime/subagent-skill-availability-contract.js +1 -1
- package/dist/core/hooks-runtime/tool-output-quarantine.js +1 -1
- package/dist/core/hooks-runtime.js +73 -87
- package/dist/core/image-ux-review/imagegen-adapter.js +108 -94
- package/dist/core/image-ux-review/real-callout-extractor.js +144 -14
- package/dist/core/imagegen/desktop-bridge-imagegen-target.js +77 -0
- package/dist/core/imagegen/imagegen-capability.js +43 -151
- package/dist/core/init/skills/filesystem.js +89 -0
- package/dist/core/init/skills/inventory.js +97 -0
- package/dist/core/init/skills/manifest.js +151 -0
- package/dist/core/init/skills.js +5 -325
- package/dist/core/init.js +0 -63
- package/dist/core/install/installed-package-smoke.js +1 -1
- package/dist/core/managed-assets/managed-assets-manifest.js +0 -1
- package/dist/core/policy/latest-version-guidance.js +15 -2
- package/dist/core/preflight/parallel-preflight-engine.js +27 -51
- package/dist/core/provider/provider-badge.js +9 -4
- package/dist/core/provider/provider-context.js +57 -107
- package/dist/core/providers/openrouter/openrouter-account.js +3 -1
- package/dist/core/providers/openrouter/openrouter-secret-store.js +91 -22
- package/dist/core/release/desktop-bridge-release-evidence.js +457 -0
- package/dist/core/release/gate-manifest.js +0 -1
- package/dist/core/release/main-push-guard.js +13 -0
- package/dist/core/release/main-push-receipt.js +11 -0
- package/dist/core/release/physical-release-gates.js +664 -61
- package/dist/core/release/publish-preflight.js +3 -3
- package/dist/core/release/release-gate-batch-runner.js +19 -6
- package/dist/core/release/release-gate-dag.js +33 -6
- package/dist/core/release/release-gate-node.js +1 -3
- package/dist/core/release/release-gate-output-contract.js +55 -0
- package/dist/core/release/release-pack-contract.js +1 -1
- package/dist/core/release/release-pack-receipt.js +209 -8
- package/dist/core/release/release-real-contract.js +1 -1
- package/dist/core/release/stage-publish.js +106 -28
- package/dist/core/retention/temp-lease.js +204 -0
- package/dist/core/retention.js +18 -195
- package/dist/core/routes.js +1 -2
- package/dist/core/safety/intent-contract/intent-contract.js +3 -3
- package/dist/core/subagents/naruto-host-credentials.js +6 -1
- package/dist/core/subagents/official-subagent-config.js +27 -9
- package/dist/core/subagents/official-subagent-runner.js +14 -31
- package/dist/core/subagents/wave-lifecycle.js +36 -8
- package/dist/core/subagents/wave-parent-guidance.js +2 -1
- package/dist/core/telegram/doctor.js +2 -2
- package/dist/core/telegram/liveness.js +10 -4
- package/dist/core/triwiki/context-graph/compiler/index.js +18 -18
- package/dist/core/update/update-migration-state/fast-mode-config.js +172 -0
- package/dist/core/update/update-migration-state/session-state-split.js +247 -0
- package/dist/core/update/update-migration-state/simple-stages.js +46 -0
- package/dist/core/update/update-migration-state.js +4 -481
- package/dist/core/version.js +1 -1
- package/dist/native/sks-menubar/Sources/OperationCoordinator.swift +28 -4
- package/dist/native/sks-menubar/Sources/OperationModels.swift +112 -2
- package/dist/native/sks-menubar/Sources/ProviderRouteExplanation.swift +21 -0
- package/dist/native/sks-menubar/Sources/ProvidersBridgeCatalog.swift +140 -0
- package/dist/native/sks-menubar/Sources/ProvidersOpenRouter.swift +107 -470
- package/dist/native/sks-menubar/Sources/ProvidersReliability.swift +27 -197
- package/dist/native/sks-menubar/Sources/ProvidersRoutingTruth.swift +335 -269
- package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +233 -437
- package/dist/native/sks-menubar/Sources/RemoteCodingViewController.swift +16 -7
- package/dist/native/sks-menubar/Sources/SecureProcessEnvelope.swift +39 -9
- package/dist/native/sks-menubar/Sources/TelegramSupport.swift +56 -0
- package/dist/scripts/canonical-test-runner.js +7 -1
- package/dist/scripts/check-runtime-schemas.js +7 -2
- package/dist/scripts/codex-sdk-real-smoke-check.js +3 -1
- package/dist/scripts/desktop-bridge-capabilities-check.js +123 -0
- package/dist/scripts/{codex-lb-desktop-bridge-latency-check.js → desktop-bridge-latency-check.js} +61 -16
- package/dist/scripts/desktop-bridge-real-evidence-check.js +64 -0
- package/dist/scripts/{codex-lb-desktop-bridge-check.js → desktop-bridge-transport-check.js} +92 -155
- package/dist/scripts/desktop-bridge-transport-client.js +116 -0
- package/dist/scripts/desktop-bridge-unification-check.js +276 -0
- package/dist/scripts/doctor-fixes-codex-app-fast-ui-check.js +12 -8
- package/dist/scripts/gate-policy-audit-check.js +52 -2
- package/dist/scripts/provider-badge-context-check.js +55 -12
- package/dist/scripts/publish-preflight.js +1 -2
- package/dist/scripts/release-affected-selector-check.js +1 -2
- package/dist/scripts/release-aggressive-resource-governor-check.js +1 -1
- package/dist/scripts/release-cache-glob-hashing-check.js +1 -0
- package/dist/scripts/release-cache-version-neutral-fixture-check.js +1 -0
- package/dist/scripts/release-full-parallelism-blackbox.js +1 -1
- package/dist/scripts/release-gate-batch-runner-check.js +9 -0
- package/dist/scripts/release-gate-dag-runner-check.js +6 -0
- package/dist/scripts/release-gate-dag-runner.js +4 -2
- package/dist/scripts/release-metadata-check.js +16 -3
- package/dist/scripts/release-pack-receipt.js +29 -6
- package/dist/scripts/release-physical-gates-check.js +39 -0
- package/dist/scripts/release-real-check.js +7 -5
- package/package.json +6 -8
- package/release-gates.v2.json +156 -157
- package/runtime-required-scripts.json +11 -11
- package/schemas/codex/desktop-bridge-status-v3.schema.json +286 -0
- package/schemas/codex/desktop-capabilities-v3.schema.json +198 -0
- package/schemas/release/release-gate-node.schema.json +2 -3
- package/dist/cli/codex-lb-setup-warning-output.js +0 -17
- package/dist/cli/install-helpers-codex-lb-chain.js +0 -529
- package/dist/commands/codex-lb.js +0 -1232
- package/dist/core/architecture-hardening/migration/migration.js +0 -111
- package/dist/core/codex-app/auxiliary-oauth-policy.js +0 -36
- package/dist/core/codex-app/catalog-compat/catalog-service.js +0 -104
- package/dist/core/codex-app/child-policy/child-policy.js +0 -35
- package/dist/core/codex-app/desktop-routing-snapshot.js +0 -233
- package/dist/core/codex-app/glm-model-profile.js +0 -1
- package/dist/core/codex-app/glm-profile-installer.js +0 -191
- package/dist/core/codex-app/glm-profile-schema.js +0 -42
- package/dist/core/codex-app/multi-provider-router.js +0 -391
- package/dist/core/codex-app/openrouter-activate.js +0 -405
- package/dist/core/codex-app/openrouter-model-catalog.js +0 -217
- package/dist/core/codex-app/provider-child-policy.js +0 -64
- package/dist/core/codex-app/provider-mode.js +0 -91
- package/dist/core/codex-app/provider-session-policy.js +0 -85
- package/dist/core/codex-app/session-policy/session-pinning.js +0 -68
- package/dist/core/codex-control/codex-lb-launch-recovery.js +0 -319
- package/dist/core/codex-lb/codex-lb-setup.js +0 -293
- package/dist/core/codex-lb/codex-lb-tool-catalog.js +0 -456
- package/dist/core/codex-lb/codex-lb-tool-output-recovery.js +0 -290
- package/dist/core/codex-lb/desktop-center-credentials.js +0 -1083
- package/dist/core/codex-lb/desktop-mode.js +0 -32
- package/dist/core/codex-lb/desktop-real-evidence.js +0 -168
- package/dist/core/codex-lb/legacy-migration.js +0 -576
- package/dist/core/codex-lb/native-openai-transport/transport.js +0 -68
- package/dist/core/codex-lb/provider-routing/provider-router.js +0 -58
- package/dist/core/codex-lb/routing-truth.js +0 -327
- package/dist/core/imagegen/codex-lb-imagegen-target.js +0 -92
- package/dist/native/sks-menubar/Sources/ProvidersConnectTest.swift +0 -118
- package/dist/native/sks-menubar/Sources/ProvidersFastMode.swift +0 -17
- package/dist/native/sks-menubar/Sources/ProvidersMultiProvider.swift +0 -331
- package/dist/native/sks-menubar/Sources/ProvidersRoleModels.swift +0 -260
- package/dist/scripts/codex-lb-catalog-passthrough-check.js +0 -120
- package/dist/scripts/codex-lb-desktop-auth-invariant-check.js +0 -169
- package/dist/scripts/codex-lb-desktop-capabilities-check.js +0 -287
- package/dist/scripts/codex-lb-desktop-real-evidence-check.js +0 -123
- package/dist/scripts/codex-lb-fast-mode-truth-check.js +0 -114
- package/dist/scripts/codex-lb-fast-ui-preservation-check.js +0 -152
- package/dist/scripts/codex-lb-gpt56-fast-profile-check.js +0 -231
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import dns from 'node:dns/promises';
|
|
2
2
|
import net from 'node:net';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { parseProviderPolicySnapshot, stableArchitectureHash, } from '../../architecture-hardening/contracts/contracts.js';
|
|
6
|
-
import { decideProviderRoute } from '../provider-routing/provider-router.js';
|
|
7
|
-
import { decideChildSelection } from '../../codex-app/child-policy/child-policy.js';
|
|
8
|
-
import { assertSessionRequest, resumeSessionPin, sessionPinHash, } from '../../codex-app/session-policy/session-pinning.js';
|
|
3
|
+
import { canonicalizeBridgeModelId, normalizeBridgeUpstreamModelId, } from '../route-index.js';
|
|
4
|
+
import { DESKTOP_BRIDGE_CLIENT_PATH_PREFIX, DesktopBridgeError } from './types.js';
|
|
9
5
|
const MIN_HIGH_PORT = 49_152;
|
|
10
6
|
const MAX_PORT = 65_535;
|
|
7
|
+
const MAX_SESSION_PINS = 10_000;
|
|
8
|
+
const sessionPinMutationQueues = new WeakMap();
|
|
11
9
|
const FORBIDDEN_REMOTE_ADDRESSES = new net.BlockList();
|
|
12
10
|
FORBIDDEN_REMOTE_ADDRESSES.addSubnet('0.0.0.0', 8, 'ipv4');
|
|
13
11
|
FORBIDDEN_REMOTE_ADDRESSES.addSubnet('10.0.0.0', 8, 'ipv4');
|
|
@@ -15,12 +13,7 @@ FORBIDDEN_REMOTE_ADDRESSES.addSubnet('100.64.0.0', 10, 'ipv4');
|
|
|
15
13
|
FORBIDDEN_REMOTE_ADDRESSES.addSubnet('127.0.0.0', 8, 'ipv4');
|
|
16
14
|
FORBIDDEN_REMOTE_ADDRESSES.addSubnet('169.254.0.0', 16, 'ipv4');
|
|
17
15
|
FORBIDDEN_REMOTE_ADDRESSES.addSubnet('172.16.0.0', 12, 'ipv4');
|
|
18
|
-
FORBIDDEN_REMOTE_ADDRESSES.addSubnet('192.0.0.0', 24, 'ipv4');
|
|
19
|
-
FORBIDDEN_REMOTE_ADDRESSES.addSubnet('192.0.2.0', 24, 'ipv4');
|
|
20
16
|
FORBIDDEN_REMOTE_ADDRESSES.addSubnet('192.168.0.0', 16, 'ipv4');
|
|
21
|
-
FORBIDDEN_REMOTE_ADDRESSES.addSubnet('198.18.0.0', 15, 'ipv4');
|
|
22
|
-
FORBIDDEN_REMOTE_ADDRESSES.addSubnet('198.51.100.0', 24, 'ipv4');
|
|
23
|
-
FORBIDDEN_REMOTE_ADDRESSES.addSubnet('203.0.113.0', 24, 'ipv4');
|
|
24
17
|
FORBIDDEN_REMOTE_ADDRESSES.addSubnet('224.0.0.0', 4, 'ipv4');
|
|
25
18
|
FORBIDDEN_REMOTE_ADDRESSES.addSubnet('240.0.0.0', 4, 'ipv4');
|
|
26
19
|
FORBIDDEN_REMOTE_ADDRESSES.addSubnet('::', 128, 'ipv6');
|
|
@@ -28,133 +21,103 @@ FORBIDDEN_REMOTE_ADDRESSES.addSubnet('::1', 128, 'ipv6');
|
|
|
28
21
|
FORBIDDEN_REMOTE_ADDRESSES.addSubnet('fc00::', 7, 'ipv6');
|
|
29
22
|
FORBIDDEN_REMOTE_ADDRESSES.addSubnet('fe80::', 10, 'ipv6');
|
|
30
23
|
FORBIDDEN_REMOTE_ADDRESSES.addSubnet('ff00::', 8, 'ipv6');
|
|
31
|
-
FORBIDDEN_REMOTE_ADDRESSES.addSubnet('2001:db8::', 32, 'ipv6');
|
|
32
24
|
export function assertLoopbackPeer(address) {
|
|
33
25
|
const normalized = String(address || '').replace(/^::ffff:/i, '');
|
|
34
26
|
if (!net.isIP(normalized))
|
|
35
27
|
throw new DesktopBridgeError('bridge_peer_not_ip');
|
|
36
|
-
if (normalized !== '127.0.0.1' && normalized !== '::1')
|
|
28
|
+
if (normalized !== '127.0.0.1' && normalized !== '::1')
|
|
37
29
|
throw new DesktopBridgeError('bridge_non_loopback_peer');
|
|
38
|
-
}
|
|
39
30
|
}
|
|
40
31
|
export function assertLoopbackListenHost(host) {
|
|
41
|
-
if (host !== '127.0.0.1' && host !== '::1')
|
|
32
|
+
if (host !== '127.0.0.1' && host !== '::1')
|
|
42
33
|
throw new DesktopBridgeError('bridge_listen_host_not_loopback');
|
|
43
|
-
}
|
|
44
34
|
}
|
|
45
35
|
export function assertAllowedPath(pathname, prefixes) {
|
|
46
36
|
if (!pathname.startsWith('/') || pathname.includes('\\') || /%(?:2f|5c)/i.test(pathname)) {
|
|
47
37
|
throw new DesktopBridgeError('bridge_path_invalid');
|
|
48
38
|
}
|
|
49
|
-
const allowed = prefixes.some((prefix) =>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (prefix.endsWith('/'))
|
|
53
|
-
return pathname.startsWith(prefix);
|
|
54
|
-
return pathname === prefix || pathname.startsWith(`${prefix}/`);
|
|
55
|
-
});
|
|
39
|
+
const allowed = prefixes.some((prefix) => prefix.endsWith('/')
|
|
40
|
+
? pathname.startsWith(prefix)
|
|
41
|
+
: pathname === prefix || pathname.startsWith(`${prefix}/`));
|
|
56
42
|
if (!allowed)
|
|
57
43
|
throw new DesktopBridgeError('bridge_path_not_allowed');
|
|
58
44
|
}
|
|
45
|
+
export function desktopBridgeClientPath(capability, canonicalPath) {
|
|
46
|
+
if (!/^[A-Za-z0-9_-]{43}$/.test(capability))
|
|
47
|
+
throw new DesktopBridgeError('bridge_client_capability_invalid');
|
|
48
|
+
if (!canonicalPath.startsWith('/') || canonicalPath.includes('\\') || /[\r\n\0]/.test(canonicalPath)) {
|
|
49
|
+
throw new DesktopBridgeError('bridge_request_target_invalid');
|
|
50
|
+
}
|
|
51
|
+
return `${DESKTOP_BRIDGE_CLIENT_PATH_PREFIX}/${capability}${canonicalPath}`;
|
|
52
|
+
}
|
|
59
53
|
function headerValues(value) {
|
|
60
|
-
return (Array.isArray(value) ? value : value === undefined ? [] : [value])
|
|
61
|
-
.map((entry) => entry.trim())
|
|
62
|
-
.filter(Boolean);
|
|
54
|
+
return (Array.isArray(value) ? value : value === undefined ? [] : [value]).map((v) => v.trim()).filter(Boolean);
|
|
63
55
|
}
|
|
64
|
-
function
|
|
65
|
-
const values = headerValues(headers[name]);
|
|
56
|
+
export function singleBridgeHeader(headers, name) {
|
|
57
|
+
const values = headerValues(headers[name.toLowerCase()]);
|
|
66
58
|
if (values.length > 1)
|
|
67
59
|
throw new DesktopBridgeError('bridge_policy_header_ambiguous');
|
|
68
60
|
return values[0] || null;
|
|
69
61
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
catch (error) {
|
|
85
|
-
throw new DesktopBridgeError(`bridge_${error.message}`);
|
|
86
|
-
}
|
|
62
|
+
function codexMetadataObject(value, source) {
|
|
63
|
+
if (value === undefined || value === null)
|
|
64
|
+
return null;
|
|
65
|
+
let parsed = value;
|
|
66
|
+
if (source === 'header') {
|
|
67
|
+
if (typeof value !== 'string' || Buffer.byteLength(value) > 16 * 1024) {
|
|
68
|
+
throw new DesktopBridgeError('bridge_codex_turn_metadata_invalid');
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
parsed = JSON.parse(value);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
throw new DesktopBridgeError('bridge_codex_turn_metadata_invalid');
|
|
87
75
|
}
|
|
88
|
-
if (config.requireSessionPin)
|
|
89
|
-
throw new DesktopBridgeError('bridge_session_policy_missing');
|
|
90
|
-
return;
|
|
91
76
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (input.model !== undefined) {
|
|
97
|
-
const route = decideProviderRoute({
|
|
98
|
-
policy,
|
|
99
|
-
credential,
|
|
100
|
-
requestedMode: (requestedMode || config.providerMode),
|
|
101
|
-
model: String(input.model || ''),
|
|
102
|
-
});
|
|
103
|
-
if (!route.ok)
|
|
104
|
-
throw new DesktopBridgeError(`bridge_${route.blockers[0] || 'provider_route_blocked'}`);
|
|
105
|
-
}
|
|
106
|
-
const sessionId = singlePolicyHeader(input.headers, 'x-sks-session-id');
|
|
107
|
-
if (!sessionId) {
|
|
108
|
-
if (config.requireSessionPin)
|
|
109
|
-
throw new DesktopBridgeError('bridge_session_pin_required');
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
const pin = (config.sessionPins || []).find((entry) => entry.session_id === sessionId);
|
|
113
|
-
if (!pin)
|
|
114
|
-
throw new DesktopBridgeError('bridge_session_pin_unknown');
|
|
115
|
-
const resume = resumeSessionPin(pin, policy);
|
|
116
|
-
if (!resume.ok)
|
|
117
|
-
throw new DesktopBridgeError(`bridge_${resume.blocker || 'session_pin_blocked'}`);
|
|
118
|
-
const childFlag = singlePolicyHeader(input.headers, 'x-sks-child-request');
|
|
119
|
-
if (childFlag && childFlag !== '1')
|
|
120
|
-
throw new DesktopBridgeError('bridge_child_request_header_invalid');
|
|
121
|
-
const childHash = singlePolicyHeader(input.headers, 'x-sks-child-policy-hash') || policy.child_policy_hash;
|
|
122
|
-
const requestModel = String(input.model === undefined ? pin.model : input.model || '');
|
|
123
|
-
try {
|
|
124
|
-
assertSessionRequest(pin, {
|
|
125
|
-
mode: config.providerMode,
|
|
126
|
-
model: childFlag === '1' ? pin.model : requestModel,
|
|
127
|
-
childPolicyHash: childHash,
|
|
128
|
-
});
|
|
77
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
78
|
+
throw new DesktopBridgeError(source === 'header'
|
|
79
|
+
? 'bridge_codex_turn_metadata_invalid'
|
|
80
|
+
: 'bridge_codex_client_metadata_invalid');
|
|
129
81
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (
|
|
137
|
-
throw new DesktopBridgeError('
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
82
|
+
return parsed;
|
|
83
|
+
}
|
|
84
|
+
function oneCodexIdentity(values, kind) {
|
|
85
|
+
const canonical = values
|
|
86
|
+
.filter((value) => value !== undefined && value !== null && value !== '')
|
|
87
|
+
.map(canonicalSessionId);
|
|
88
|
+
if (new Set(canonical).size > 1)
|
|
89
|
+
throw new DesktopBridgeError('bridge_codex_session_identity_conflict');
|
|
90
|
+
return canonical[0] || null;
|
|
91
|
+
}
|
|
92
|
+
export function resolveCodexSessionIdentity(headers, payload = null) {
|
|
93
|
+
const turnMetadataValue = singleBridgeHeader(headers, 'x-codex-turn-metadata');
|
|
94
|
+
const turnMetadata = turnMetadataValue === null ? null : codexMetadataObject(turnMetadataValue, 'header');
|
|
95
|
+
const clientMetadataValue = payload?.client_metadata;
|
|
96
|
+
const clientMetadata = clientMetadataValue === undefined ? null : codexMetadataObject(clientMetadataValue, 'body');
|
|
97
|
+
const threadId = oneCodexIdentity([
|
|
98
|
+
singleBridgeHeader(headers, 'thread-id'),
|
|
99
|
+
turnMetadata?.thread_id,
|
|
100
|
+
clientMetadata?.thread_id,
|
|
101
|
+
], 'thread');
|
|
102
|
+
const sessionId = oneCodexIdentity([
|
|
103
|
+
singleBridgeHeader(headers, 'session-id'),
|
|
104
|
+
turnMetadata?.session_id,
|
|
105
|
+
clientMetadata?.session_id,
|
|
106
|
+
], 'session');
|
|
107
|
+
if (sessionId && !threadId)
|
|
108
|
+
throw new DesktopBridgeError('bridge_codex_thread_id_missing');
|
|
109
|
+
if (threadId && sessionId && threadId !== sessionId) {
|
|
110
|
+
throw new DesktopBridgeError('bridge_codex_session_identity_mismatch');
|
|
111
|
+
}
|
|
112
|
+
return { thread_id: threadId, session_id: sessionId };
|
|
146
113
|
}
|
|
147
114
|
function comparableOrigin(value, referer) {
|
|
148
115
|
if (value === 'null')
|
|
149
|
-
return
|
|
116
|
+
return value;
|
|
150
117
|
try {
|
|
151
118
|
const parsed = new URL(value);
|
|
152
|
-
if (referer)
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
if ((parsed.pathname !== '/' && parsed.pathname !== '') || parsed.search || parsed.hash) {
|
|
156
|
-
throw new Error('origin_has_path');
|
|
157
|
-
}
|
|
119
|
+
if (!referer && ((parsed.pathname !== '/' && parsed.pathname !== '') || parsed.search || parsed.hash))
|
|
120
|
+
throw new Error();
|
|
158
121
|
return parsed.origin !== 'null' ? parsed.origin : `${parsed.protocol}//${parsed.host}`;
|
|
159
122
|
}
|
|
160
123
|
catch {
|
|
@@ -167,140 +130,180 @@ export function normalizeAllowedOrigin(value) {
|
|
|
167
130
|
export function assertAllowedOrigin(headers, allowedOrigins) {
|
|
168
131
|
const origins = headerValues(headers.origin);
|
|
169
132
|
const referers = headerValues(headers.referer);
|
|
170
|
-
if (origins.length === 0 && referers.length === 0)
|
|
171
|
-
return;
|
|
172
133
|
if (origins.length > 1 || referers.length > 1)
|
|
173
134
|
throw new DesktopBridgeError('bridge_origin_forbidden');
|
|
174
135
|
const allowed = new Set(allowedOrigins.map(normalizeAllowedOrigin));
|
|
175
|
-
for (const
|
|
176
|
-
if (!allowed.has(comparableOrigin(
|
|
136
|
+
for (const value of origins)
|
|
137
|
+
if (!allowed.has(comparableOrigin(value, false)))
|
|
177
138
|
throw new DesktopBridgeError('bridge_origin_forbidden');
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if (!allowed.has(comparableOrigin(referer, true)))
|
|
139
|
+
for (const value of referers)
|
|
140
|
+
if (!allowed.has(comparableOrigin(value, true)))
|
|
181
141
|
throw new DesktopBridgeError('bridge_origin_forbidden');
|
|
182
|
-
}
|
|
183
142
|
}
|
|
184
143
|
export function assertWebSocketUpgrade(headers, method) {
|
|
185
|
-
const connection = headerValues(headers.connection).join(',').toLowerCase().split(',').map((
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const version = headerValues(headers['sec-websocket-version']);
|
|
189
|
-
if (method !== 'GET'
|
|
190
|
-
|| !connection.includes('upgrade')
|
|
191
|
-
|| upgrade.length !== 1
|
|
192
|
-
|| upgrade[0]?.toLowerCase() !== 'websocket'
|
|
193
|
-
|| key.length !== 1
|
|
194
|
-
|| version.length !== 1) {
|
|
144
|
+
const connection = headerValues(headers.connection).join(',').toLowerCase().split(',').map((v) => v.trim());
|
|
145
|
+
if (method !== 'GET' || !connection.includes('upgrade') || headerValues(headers.upgrade)[0]?.toLowerCase() !== 'websocket'
|
|
146
|
+
|| headerValues(headers['sec-websocket-key']).length !== 1 || headerValues(headers['sec-websocket-version']).length !== 1) {
|
|
195
147
|
throw new DesktopBridgeError('bridge_websocket_upgrade_invalid');
|
|
196
148
|
}
|
|
197
149
|
}
|
|
198
|
-
function
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if (normalized === '::1')
|
|
204
|
-
return true;
|
|
205
|
-
if (net.isIP(normalized) !== 4)
|
|
206
|
-
return false;
|
|
207
|
-
return Number(normalized.split('.')[0]) === 127;
|
|
208
|
-
}
|
|
209
|
-
function isExplicitLoopbackHostname(hostname) {
|
|
210
|
-
const normalized = stripIpv6Brackets(hostname).replace(/\.$/, '').toLowerCase();
|
|
211
|
-
return normalized === 'localhost' || isLoopbackAddress(normalized);
|
|
212
|
-
}
|
|
213
|
-
function isForbiddenRemoteAddress(address, family) {
|
|
214
|
-
const dottedMapped = address.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/i)?.[1];
|
|
215
|
-
const hexMapped = address.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i);
|
|
216
|
-
const mapped = dottedMapped || (hexMapped
|
|
217
|
-
? [
|
|
218
|
-
Number.parseInt(hexMapped[1] || '0', 16) >> 8,
|
|
219
|
-
Number.parseInt(hexMapped[1] || '0', 16) & 0xff,
|
|
220
|
-
Number.parseInt(hexMapped[2] || '0', 16) >> 8,
|
|
221
|
-
Number.parseInt(hexMapped[2] || '0', 16) & 0xff,
|
|
222
|
-
].join('.')
|
|
223
|
-
: undefined);
|
|
224
|
-
if (mapped)
|
|
225
|
-
return FORBIDDEN_REMOTE_ADDRESSES.check(mapped, 'ipv4');
|
|
226
|
-
return FORBIDDEN_REMOTE_ADDRESSES.check(address, family === 4 ? 'ipv4' : 'ipv6');
|
|
150
|
+
function canonicalPublicModel(value) {
|
|
151
|
+
const model = canonicalizeBridgeModelId(value);
|
|
152
|
+
if (!model)
|
|
153
|
+
throw new DesktopBridgeError('catalog_model_route_missing');
|
|
154
|
+
return model;
|
|
227
155
|
}
|
|
228
|
-
export function
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if (config.providerMode !== 'codex-lb' && config.providerMode !== 'openrouter') {
|
|
238
|
-
throw new DesktopBridgeError('bridge_provider_mode_invalid');
|
|
156
|
+
export function resolveBridgeRequestRoute(request, policy, pins) {
|
|
157
|
+
const publicModel = canonicalPublicModel(request.public_model);
|
|
158
|
+
const sessionId = request.session_id ? canonicalSessionId(request.session_id) : null;
|
|
159
|
+
const pin = sessionId ? pins.find((entry) => entry.thread_id === sessionId) || null : null;
|
|
160
|
+
if (pin && pin.public_model === publicModel) {
|
|
161
|
+
if (pin.public_model !== publicModel
|
|
162
|
+
|| pin.catalog_generation !== policy.catalog_generation
|
|
163
|
+
|| pin.route_policy_generation !== policy.policy_generation) {
|
|
164
|
+
throw new DesktopBridgeError('session_pin_route_unavailable');
|
|
239
165
|
}
|
|
240
|
-
|
|
241
|
-
|
|
166
|
+
const current = policy.model_routes[publicModel];
|
|
167
|
+
if (!current || current.provider_id !== pin.provider_id || current.upstream_model !== pin.upstream_model) {
|
|
168
|
+
throw new DesktopBridgeError('session_pin_route_unavailable');
|
|
242
169
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
170
|
+
return {
|
|
171
|
+
provider_id: pin.provider_id,
|
|
172
|
+
public_model: publicModel,
|
|
173
|
+
upstream_model: pin.upstream_model,
|
|
174
|
+
catalog_generation: pin.catalog_generation,
|
|
175
|
+
route_policy_generation: pin.route_policy_generation,
|
|
176
|
+
session_pin: pin,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const route = policy.model_routes[publicModel];
|
|
180
|
+
if (!route)
|
|
181
|
+
throw new DesktopBridgeError('catalog_model_route_missing');
|
|
182
|
+
const nextPin = sessionId ? {
|
|
183
|
+
thread_id: sessionId,
|
|
184
|
+
provider_id: route.provider_id,
|
|
185
|
+
public_model: publicModel,
|
|
186
|
+
upstream_model: route.upstream_model,
|
|
187
|
+
catalog_generation: policy.catalog_generation,
|
|
188
|
+
route_policy_generation: policy.policy_generation,
|
|
189
|
+
created_at: new Date().toISOString(),
|
|
190
|
+
} : null;
|
|
191
|
+
return {
|
|
192
|
+
provider_id: route.provider_id,
|
|
193
|
+
public_model: publicModel,
|
|
194
|
+
upstream_model: route.upstream_model,
|
|
195
|
+
catalog_generation: policy.catalog_generation,
|
|
196
|
+
route_policy_generation: policy.policy_generation,
|
|
197
|
+
session_pin: nextPin,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
export function assertDesktopBridgeRouteContext(request, config) {
|
|
201
|
+
const resolver = config.resolveRequestRoute || resolveBridgeRequestRoute;
|
|
202
|
+
const route = resolver(request, config.routePolicy, config.providerSessionPins);
|
|
203
|
+
const policy = config.routePolicy;
|
|
204
|
+
const expected = policy.model_routes[route.public_model];
|
|
205
|
+
if (!expected || expected.provider_id !== route.provider_id || expected.upstream_model !== route.upstream_model) {
|
|
206
|
+
throw new DesktopBridgeError('catalog_model_route_missing');
|
|
207
|
+
}
|
|
208
|
+
if (route.catalog_generation !== policy.catalog_generation || route.route_policy_generation !== policy.policy_generation) {
|
|
209
|
+
throw new DesktopBridgeError('session_pin_route_unavailable');
|
|
210
|
+
}
|
|
211
|
+
if (request.session_id) {
|
|
212
|
+
const sessionId = canonicalSessionId(request.session_id);
|
|
213
|
+
if (!route.session_pin
|
|
214
|
+
|| route.session_pin.thread_id !== sessionId
|
|
215
|
+
|| route.session_pin.provider_id !== route.provider_id
|
|
216
|
+
|| route.session_pin.public_model !== route.public_model
|
|
217
|
+
|| route.session_pin.upstream_model !== route.upstream_model
|
|
218
|
+
|| route.session_pin.catalog_generation !== route.catalog_generation
|
|
219
|
+
|| route.session_pin.route_policy_generation !== route.route_policy_generation) {
|
|
220
|
+
throw new DesktopBridgeError('bridge_session_pin_invalid');
|
|
280
221
|
}
|
|
281
222
|
}
|
|
282
|
-
if (
|
|
283
|
-
throw new DesktopBridgeError('
|
|
284
|
-
}
|
|
285
|
-
if (!Array.isArray(config.allowedPathPrefixes) || config.allowedPathPrefixes.length === 0) {
|
|
286
|
-
throw new DesktopBridgeError('bridge_path_allowlist_empty');
|
|
223
|
+
else if (route.session_pin) {
|
|
224
|
+
throw new DesktopBridgeError('bridge_session_pin_invalid');
|
|
287
225
|
}
|
|
288
|
-
|
|
289
|
-
|
|
226
|
+
const provider = config.providers[route.provider_id];
|
|
227
|
+
if (!provider || !provider.enabled)
|
|
228
|
+
throw new DesktopBridgeError('bridge_provider_route_unavailable');
|
|
229
|
+
if (provider.credential_state !== 'ready')
|
|
230
|
+
throw new DesktopBridgeError(`${route.provider_id.replace('-', '_')}_credential_unavailable`);
|
|
231
|
+
if (provider.source_catalog_generation !== null && !provider.source_catalog_generation.trim()) {
|
|
232
|
+
throw new DesktopBridgeError('bridge_provider_source_catalog_generation_invalid');
|
|
290
233
|
}
|
|
291
|
-
if (!
|
|
292
|
-
throw new DesktopBridgeError('
|
|
234
|
+
if (!provider.allowed_origins.map(normalizeAllowedOrigin).includes(provider.remote.origin)) {
|
|
235
|
+
throw new DesktopBridgeError('bridge_provider_origin_forbidden');
|
|
293
236
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
237
|
+
return route;
|
|
238
|
+
}
|
|
239
|
+
export async function resolveAndBindDesktopBridgeRouteContext(request, config) {
|
|
240
|
+
return withSessionPinMutation(config, async () => {
|
|
241
|
+
const route = assertDesktopBridgeRouteContext(request, config);
|
|
242
|
+
const pin = route.session_pin;
|
|
243
|
+
if (!pin)
|
|
244
|
+
return route;
|
|
245
|
+
const existing = config.providerSessionPins.find((entry) => entry.thread_id === pin.thread_id) || null;
|
|
246
|
+
if (existing && sameSessionPin(existing, pin))
|
|
247
|
+
return route;
|
|
248
|
+
const nextPins = [
|
|
249
|
+
...config.providerSessionPins.filter((entry) => entry.thread_id !== pin.thread_id),
|
|
250
|
+
pin,
|
|
251
|
+
];
|
|
252
|
+
const retainedPins = nextPins.length > MAX_SESSION_PINS
|
|
253
|
+
? nextPins
|
|
254
|
+
.sort((left, right) => Date.parse(left.created_at) - Date.parse(right.created_at)
|
|
255
|
+
|| left.thread_id.localeCompare(right.thread_id))
|
|
256
|
+
.slice(nextPins.length - MAX_SESSION_PINS)
|
|
257
|
+
: nextPins;
|
|
258
|
+
const persistedPins = retainedPins.sort((left, right) => left.thread_id.localeCompare(right.thread_id));
|
|
259
|
+
if (config.persistProviderSessionPins) {
|
|
260
|
+
await config.persistProviderSessionPins(persistedPins);
|
|
297
261
|
}
|
|
262
|
+
config.providerSessionPins = persistedPins;
|
|
263
|
+
return route;
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
async function withSessionPinMutation(config, action) {
|
|
267
|
+
const previous = sessionPinMutationQueues.get(config) || Promise.resolve();
|
|
268
|
+
const run = previous.catch(() => undefined).then(action);
|
|
269
|
+
const tail = run.then(() => undefined, () => undefined);
|
|
270
|
+
sessionPinMutationQueues.set(config, tail);
|
|
271
|
+
try {
|
|
272
|
+
return await run;
|
|
298
273
|
}
|
|
299
|
-
|
|
300
|
-
|
|
274
|
+
finally {
|
|
275
|
+
if (sessionPinMutationQueues.get(config) === tail)
|
|
276
|
+
sessionPinMutationQueues.delete(config);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function sameSessionPin(left, right) {
|
|
280
|
+
return left.thread_id === right.thread_id
|
|
281
|
+
&& left.provider_id === right.provider_id
|
|
282
|
+
&& left.public_model === right.public_model
|
|
283
|
+
&& left.upstream_model === right.upstream_model
|
|
284
|
+
&& left.catalog_generation === right.catalog_generation
|
|
285
|
+
&& left.route_policy_generation === right.route_policy_generation;
|
|
286
|
+
}
|
|
287
|
+
function canonicalSessionId(value) {
|
|
288
|
+
const sessionId = typeof value === 'string' ? value.trim() : '';
|
|
289
|
+
if (!sessionId || sessionId.length > 256 || !/^[A-Za-z0-9._:/-]+$/.test(sessionId)) {
|
|
290
|
+
throw new DesktopBridgeError('bridge_session_id_invalid');
|
|
291
|
+
}
|
|
292
|
+
return sessionId;
|
|
293
|
+
}
|
|
294
|
+
function stripIpv6Brackets(hostname) { return hostname.replace(/^\[/, '').replace(/\]$/, ''); }
|
|
295
|
+
function isLoopbackAddress(address) {
|
|
296
|
+
const value = address.replace(/^::ffff:/i, '');
|
|
297
|
+
return value === '::1' || (net.isIP(value) === 4 && Number(value.split('.')[0]) === 127);
|
|
298
|
+
}
|
|
299
|
+
function isExplicitLoopbackHostname(hostname) {
|
|
300
|
+
const value = stripIpv6Brackets(hostname).replace(/\.$/, '').toLowerCase();
|
|
301
|
+
return value === 'localhost' || isLoopbackAddress(value);
|
|
302
|
+
}
|
|
303
|
+
function validateRemoteUrl(raw) {
|
|
301
304
|
let remote;
|
|
302
305
|
try {
|
|
303
|
-
remote = new URL(
|
|
306
|
+
remote = new URL(raw);
|
|
304
307
|
}
|
|
305
308
|
catch {
|
|
306
309
|
throw new DesktopBridgeError('bridge_remote_url_invalid');
|
|
@@ -322,74 +325,170 @@ const defaultLookup = async (hostname) => {
|
|
|
322
325
|
return { address: row.address, family: row.family };
|
|
323
326
|
});
|
|
324
327
|
};
|
|
325
|
-
export async function
|
|
326
|
-
const remote =
|
|
328
|
+
export async function resolveDesktopBridgeRemoteTarget(raw, lookup = defaultLookup) {
|
|
329
|
+
const remote = validateRemoteUrl(raw);
|
|
327
330
|
const hostname = stripIpv6Brackets(remote.hostname);
|
|
328
|
-
const
|
|
331
|
+
const family = net.isIP(hostname);
|
|
329
332
|
let addresses;
|
|
330
333
|
try {
|
|
331
|
-
addresses =
|
|
332
|
-
? [{ address: hostname, family: literalFamily }]
|
|
333
|
-
: await lookup(hostname);
|
|
334
|
+
addresses = family ? [{ address: hostname, family: family }] : await lookup(hostname);
|
|
334
335
|
}
|
|
335
336
|
catch (error) {
|
|
336
337
|
throw new DesktopBridgeError('bridge_remote_dns_failed', { cause: error });
|
|
337
338
|
}
|
|
338
|
-
if (addresses.length
|
|
339
|
-
throw new DesktopBridgeError('bridge_remote_dns_empty');
|
|
340
|
-
if (addresses.some((row) => net.isIP(row.address) !== row.family)) {
|
|
339
|
+
if (!addresses.length || addresses.some((row) => net.isIP(row.address) !== row.family))
|
|
341
340
|
throw new DesktopBridgeError('bridge_remote_dns_invalid');
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
throw new DesktopBridgeError('bridge_remote_dns_rebinding_blocked');
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
else if (addresses.some((row) => isForbiddenRemoteAddress(row.address, row.family))) {
|
|
341
|
+
const loopback = isExplicitLoopbackHostname(hostname);
|
|
342
|
+
if (loopback && addresses.some((row) => !isLoopbackAddress(row.address)))
|
|
343
|
+
throw new DesktopBridgeError('bridge_remote_dns_rebinding_blocked');
|
|
344
|
+
if (!loopback && addresses.some((row) => FORBIDDEN_REMOTE_ADDRESSES.check(row.address, row.family === 4 ? 'ipv4' : 'ipv6'))) {
|
|
350
345
|
throw new DesktopBridgeError('bridge_remote_dns_private_address');
|
|
351
346
|
}
|
|
352
347
|
const selected = addresses[0];
|
|
353
348
|
if (!selected)
|
|
354
349
|
throw new DesktopBridgeError('bridge_remote_dns_empty');
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
hostname,
|
|
360
|
-
port,
|
|
361
|
-
secure: remote.protocol === 'https:',
|
|
362
|
-
address: selected.address,
|
|
363
|
-
family: selected.family,
|
|
364
|
-
...(literalFamily ? {} : { tlsServername: hostname }),
|
|
350
|
+
const target = {
|
|
351
|
+
baseUrl: remote.toString().replace(/\/$/, ''), origin: remote.origin, hostname,
|
|
352
|
+
port: Number(remote.port || (remote.protocol === 'https:' ? 443 : 80)), secure: remote.protocol === 'https:',
|
|
353
|
+
address: selected.address, family: selected.family, ...(family ? {} : { tlsServername: hostname }),
|
|
365
354
|
};
|
|
366
|
-
return
|
|
355
|
+
return target;
|
|
367
356
|
}
|
|
368
|
-
|
|
369
|
-
const
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
357
|
+
async function prepareProvider(provider, lookup) {
|
|
358
|
+
const remote = validateRemoteUrl(provider.base_url);
|
|
359
|
+
const hostname = stripIpv6Brackets(remote.hostname);
|
|
360
|
+
if (!provider.allowed_origins.map(normalizeAllowedOrigin).includes(remote.origin))
|
|
361
|
+
throw new DesktopBridgeError('bridge_provider_origin_forbidden');
|
|
362
|
+
if (!provider.enabled) {
|
|
363
|
+
return {
|
|
364
|
+
...provider,
|
|
365
|
+
base_url: remote.toString().replace(/\/$/, ''),
|
|
366
|
+
remote: {
|
|
367
|
+
baseUrl: remote.toString().replace(/\/$/, ''), origin: remote.origin, hostname,
|
|
368
|
+
port: Number(remote.port || (remote.protocol === 'https:' ? 443 : 80)), secure: remote.protocol === 'https:',
|
|
369
|
+
address: '0.0.0.0', family: 4, ...(net.isIP(hostname) ? {} : { tlsServername: hostname }),
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
const target = await resolveDesktopBridgeRemoteTarget(provider.base_url, lookup);
|
|
374
|
+
return { ...provider, base_url: target.baseUrl, remote: target };
|
|
375
|
+
}
|
|
376
|
+
function assertRegistryAndPolicy(config, registry) {
|
|
377
|
+
if (registry.schema !== 'sks.desktop-bridge-provider-registry.v1' || !registry.generation || !Number.isFinite(Date.parse(registry.created_at))) {
|
|
378
|
+
throw new DesktopBridgeError('bridge_provider_registry_invalid');
|
|
379
|
+
}
|
|
380
|
+
const ids = ['codex-lb', 'openrouter'];
|
|
381
|
+
if (Object.keys(registry.providers).length !== ids.length)
|
|
382
|
+
throw new DesktopBridgeError('bridge_provider_registry_invalid');
|
|
383
|
+
for (const id of ids) {
|
|
384
|
+
const provider = registry.providers[id];
|
|
385
|
+
if (!provider || provider.provider_id !== id || !provider.credential_generation || !Array.isArray(provider.allowed_origins) || !provider.allowed_origins.length) {
|
|
386
|
+
throw new DesktopBridgeError('bridge_provider_registry_invalid');
|
|
387
|
+
}
|
|
388
|
+
if (provider.source_catalog_generation !== null
|
|
389
|
+
&& (typeof provider.source_catalog_generation !== 'string' || !provider.source_catalog_generation.trim())) {
|
|
390
|
+
throw new DesktopBridgeError('bridge_provider_source_catalog_generation_invalid');
|
|
391
|
+
}
|
|
392
|
+
if (id === 'openrouter' && provider.auth_transport !== 'openrouter-bearer')
|
|
393
|
+
throw new DesktopBridgeError('bridge_provider_auth_transport_mismatch');
|
|
394
|
+
if (id === 'codex-lb' && provider.auth_transport === 'openrouter-bearer')
|
|
395
|
+
throw new DesktopBridgeError('bridge_provider_auth_transport_mismatch');
|
|
396
|
+
validateRemoteUrl(provider.base_url);
|
|
397
|
+
}
|
|
398
|
+
const policy = config.routePolicy;
|
|
399
|
+
if (policy.schema !== 'sks.bridge-routing-policy.v1' || policy.fallback !== 'none' || !policy.catalog_generation || !policy.policy_generation) {
|
|
400
|
+
throw new DesktopBridgeError('bridge_route_policy_invalid');
|
|
401
|
+
}
|
|
402
|
+
for (const [model, route] of Object.entries(policy.model_routes)) {
|
|
403
|
+
if (canonicalizeBridgeModelId(model) !== model
|
|
404
|
+
|| !ids.includes(route.provider_id)
|
|
405
|
+
|| normalizeBridgeUpstreamModelId(route.upstream_model) !== route.upstream_model) {
|
|
406
|
+
throw new DesktopBridgeError('bridge_route_policy_invalid');
|
|
382
407
|
}
|
|
383
408
|
}
|
|
384
|
-
|
|
385
|
-
|
|
409
|
+
const pinIds = new Set();
|
|
410
|
+
for (const pin of config.providerSessionPins) {
|
|
411
|
+
const keys = Object.keys(pin);
|
|
412
|
+
if (keys.some((key) => ![
|
|
413
|
+
'thread_id', 'provider_id', 'public_model', 'upstream_model',
|
|
414
|
+
'catalog_generation', 'route_policy_generation', 'created_at',
|
|
415
|
+
].includes(key))
|
|
416
|
+
|| canonicalSessionId(pin.thread_id) !== pin.thread_id
|
|
417
|
+
|| pinIds.has(pin.thread_id)
|
|
418
|
+
|| !ids.includes(pin.provider_id)
|
|
419
|
+
|| canonicalizeBridgeModelId(pin.public_model) !== pin.public_model
|
|
420
|
+
|| normalizeBridgeUpstreamModelId(pin.upstream_model) !== pin.upstream_model
|
|
421
|
+
|| !pin.catalog_generation
|
|
422
|
+
|| !pin.route_policy_generation
|
|
423
|
+
|| !Number.isFinite(Date.parse(pin.created_at))) {
|
|
424
|
+
throw new DesktopBridgeError('bridge_session_pin_invalid');
|
|
425
|
+
}
|
|
426
|
+
pinIds.add(pin.thread_id);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
export function validateDesktopBridgeConfig(config) {
|
|
430
|
+
assertLoopbackListenHost(config.listenHost);
|
|
431
|
+
if (!Number.isInteger(config.listenPort) || config.listenPort < MIN_HIGH_PORT || config.listenPort > MAX_PORT)
|
|
432
|
+
throw new DesktopBridgeError('bridge_listen_port_not_high');
|
|
433
|
+
if (!config.allowedPathPrefixes.length)
|
|
434
|
+
throw new DesktopBridgeError('bridge_path_allowlist_empty');
|
|
435
|
+
if (!Number.isFinite(config.connectTimeoutMs) || config.connectTimeoutMs < 100 || config.connectTimeoutMs > 120_000)
|
|
436
|
+
throw new DesktopBridgeError('bridge_connect_timeout_invalid');
|
|
437
|
+
if (!Number.isFinite(config.idleTimeoutMs) || config.idleTimeoutMs < 1_000 || config.idleTimeoutMs > 86_400_000)
|
|
438
|
+
throw new DesktopBridgeError('bridge_idle_timeout_invalid');
|
|
439
|
+
if (config.requestTimeoutMs !== undefined
|
|
440
|
+
&& (!Number.isFinite(config.requestTimeoutMs) || config.requestTimeoutMs < 1_000 || config.requestTimeoutMs > 120_000)) {
|
|
441
|
+
throw new DesktopBridgeError('bridge_request_timeout_invalid');
|
|
442
|
+
}
|
|
443
|
+
if (config.maxConcurrentRequests !== undefined
|
|
444
|
+
&& (!Number.isInteger(config.maxConcurrentRequests) || config.maxConcurrentRequests < 1 || config.maxConcurrentRequests > 1_024)) {
|
|
445
|
+
throw new DesktopBridgeError('bridge_concurrent_request_limit_invalid');
|
|
446
|
+
}
|
|
447
|
+
if (config.maxConnections !== undefined
|
|
448
|
+
&& (!Number.isInteger(config.maxConnections) || config.maxConnections < 1 || config.maxConnections > 2_048)) {
|
|
449
|
+
throw new DesktopBridgeError('bridge_connection_limit_invalid');
|
|
450
|
+
}
|
|
451
|
+
for (const origin of config.allowedOrigins)
|
|
452
|
+
normalizeAllowedOrigin(origin);
|
|
453
|
+
if (!config.providerRegistry)
|
|
454
|
+
throw new DesktopBridgeError('bridge_provider_registry_missing');
|
|
455
|
+
if (!config.routePolicy)
|
|
456
|
+
throw new DesktopBridgeError('bridge_route_policy_invalid');
|
|
457
|
+
if (!Array.isArray(config.providerSessionPins))
|
|
458
|
+
throw new DesktopBridgeError('bridge_session_pin_invalid');
|
|
459
|
+
if (!/^[a-f0-9]{64}$/.test(config.clientCapabilitySha256))
|
|
460
|
+
throw new DesktopBridgeError('bridge_client_capability_invalid');
|
|
461
|
+
if (typeof config.resolveProviderCredential !== 'function')
|
|
462
|
+
throw new DesktopBridgeError('bridge_provider_credential_resolver_missing');
|
|
463
|
+
assertRegistryAndPolicy(config, config.providerRegistry);
|
|
464
|
+
if (!Object.values(config.providerRegistry.providers).some((provider) => provider.enabled)) {
|
|
465
|
+
throw new DesktopBridgeError('bridge_provider_registry_no_enabled_provider');
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
export async function prepareDesktopBridgeConfig(config, lookup = defaultLookup) {
|
|
469
|
+
validateDesktopBridgeConfig(config);
|
|
470
|
+
const entries = await Promise.all(Object.keys(config.providerRegistry.providers).map(async (id) => {
|
|
471
|
+
const provider = config.providerRegistry.providers[id];
|
|
472
|
+
if (!provider)
|
|
473
|
+
throw new DesktopBridgeError('bridge_provider_registry_invalid');
|
|
474
|
+
const prepared = await prepareProvider({ ...provider, remote: {} }, lookup);
|
|
475
|
+
return [id, prepared];
|
|
476
|
+
}));
|
|
477
|
+
const providers = Object.fromEntries(entries);
|
|
478
|
+
return { ...config, providers };
|
|
479
|
+
}
|
|
480
|
+
export function validatePreparedDesktopBridgeConfig(config) {
|
|
481
|
+
validateDesktopBridgeConfig(config);
|
|
482
|
+
for (const id of Object.keys(config.providers)) {
|
|
483
|
+
const provider = config.providers[id];
|
|
484
|
+
if (!provider || provider.provider_id !== id || provider.remote.origin !== new URL(provider.base_url).origin)
|
|
485
|
+
throw new DesktopBridgeError('bridge_prepared_config_invalid');
|
|
386
486
|
}
|
|
387
487
|
}
|
|
388
488
|
export function resolveDesktopBridgeTarget(rawRequestUrl, remote) {
|
|
389
489
|
const raw = String(rawRequestUrl || '/');
|
|
390
|
-
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(raw) || raw.startsWith('//') || /[\r\n\0]/.test(raw))
|
|
490
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(raw) || raw.startsWith('//') || /[\r\n\0]/.test(raw))
|
|
391
491
|
throw new DesktopBridgeError('bridge_request_target_invalid');
|
|
392
|
-
}
|
|
393
492
|
let inbound;
|
|
394
493
|
try {
|
|
395
494
|
inbound = new URL(raw, 'http://bridge.invalid');
|
|
@@ -397,7 +496,22 @@ export function resolveDesktopBridgeTarget(rawRequestUrl, remote) {
|
|
|
397
496
|
catch {
|
|
398
497
|
throw new DesktopBridgeError('bridge_request_target_invalid');
|
|
399
498
|
}
|
|
400
|
-
|
|
499
|
+
const base = new URL(remote.baseUrl);
|
|
500
|
+
const basePath = base.pathname.replace(/\/+$/, '');
|
|
501
|
+
const providerRelative = providerRelativePath(inbound.pathname);
|
|
502
|
+
const pathname = providerRelative === null
|
|
503
|
+
? inbound.pathname
|
|
504
|
+
: `${basePath}${providerRelative}` || '/';
|
|
505
|
+
return new URL(`${pathname}${inbound.search}`, remote.origin);
|
|
506
|
+
}
|
|
507
|
+
function providerRelativePath(pathname) {
|
|
508
|
+
for (const prefix of ['/backend-api/codex', '/api/v1', '/v1']) {
|
|
509
|
+
if (pathname === prefix)
|
|
510
|
+
return '';
|
|
511
|
+
if (pathname.startsWith(`${prefix}/`))
|
|
512
|
+
return pathname.slice(prefix.length);
|
|
513
|
+
}
|
|
514
|
+
return null;
|
|
401
515
|
}
|
|
402
516
|
export function safeBridgeErrorCode(error) {
|
|
403
517
|
if (error instanceof DesktopBridgeError)
|