sneakoscope 7.3.0 → 7.6.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 +129 -10
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/dist/cli/codex-app-command.js +1 -1
- package/dist/cli/command-manifest-lite.js +4 -1
- package/dist/cli/command-registry.js +2 -0
- package/dist/cli/install-helpers-codex-lb-chain.js +42 -0
- package/dist/cli/install-helpers-codex-lb-config.js +114 -9
- package/dist/cli/install-helpers-install-support.js +6 -16
- package/dist/cli/install-helpers.js +788 -600
- package/dist/cli/router.js +30 -2
- package/dist/commands/codex-app.js +37 -1
- package/dist/commands/codex-lb.js +491 -300
- package/dist/config/skills-manifest.json +57 -71
- package/dist/core/align/align-route.js +127 -0
- package/dist/core/codex-app/codex-app-fast-ui-repair.js +8 -4
- package/dist/core/codex-app/codex-app-restart.js +83 -0
- package/dist/core/codex-app/codex-app-ui-state-snapshot.js +8 -4
- package/dist/core/codex-app/desktop-routing-snapshot.js +1 -1
- package/dist/core/codex-app/menubar/constants.js +5 -0
- package/dist/core/codex-app/menubar/generation-transaction-journal.js +1 -3
- package/dist/core/codex-app/menubar/installer.js +18 -0
- package/dist/core/codex-app/multi-provider-router.js +22 -10
- package/dist/core/codex-app/openrouter-activate.js +26 -1
- package/dist/core/codex-app.js +118 -26
- package/dist/core/codex-control/codex-app-server-runtime-env.js +86 -0
- package/dist/core/codex-control/codex-app-server-v2-client.js +78 -8
- package/dist/core/codex-control/codex-task-runner.js +2 -2
- package/dist/core/codex-lb/capability-runner.js +204 -0
- package/dist/core/codex-lb/capability-types.js +1 -0
- package/dist/core/codex-lb/codex-lb-env.js +9 -13
- package/dist/core/codex-lb/codex-lb-setup.js +45 -8
- package/dist/core/codex-lb/codex-lb-tool-catalog.js +52 -74
- package/dist/core/codex-lb/codex-lb-tool-output-recovery.js +1 -1
- package/dist/core/codex-lb/desktop-auth-invariant.js +185 -0
- package/dist/core/codex-lb/desktop-bridge/client.js +34 -0
- package/dist/core/codex-lb/desktop-bridge/header-policy.js +106 -0
- package/dist/core/codex-lb/desktop-bridge/http-forward.js +101 -0
- package/dist/core/codex-lb/desktop-bridge/index.js +10 -0
- package/dist/core/codex-lb/desktop-bridge/launchd.js +81 -0
- package/dist/core/codex-lb/desktop-bridge/location-rewrite.js +27 -0
- package/dist/core/codex-lb/desktop-bridge/security.js +274 -0
- package/dist/core/codex-lb/desktop-bridge/server.js +179 -0
- package/dist/core/codex-lb/desktop-bridge/state.js +119 -0
- package/dist/core/codex-lb/desktop-bridge/types.js +16 -0
- package/dist/core/codex-lb/desktop-bridge/websocket-forward.js +151 -0
- package/dist/core/codex-lb/desktop-center-credentials.js +189 -0
- package/dist/core/codex-lb/desktop-controller.js +1410 -0
- package/dist/core/codex-lb/desktop-mode.js +32 -0
- package/dist/core/codex-lb/desktop-real-evidence.js +168 -0
- package/dist/core/codex-lb/desktop-service.js +613 -0
- package/dist/core/codex-lb/legacy-migration.js +575 -0
- package/dist/core/codex-lb/migration-receipt.js +246 -0
- package/dist/core/codex-lb/probes/auxiliary-surfaces-probe.js +60 -0
- package/dist/core/codex-lb/probes/bridge-probe.js +75 -0
- package/dist/core/codex-lb/probes/catalog-probe.js +128 -0
- package/dist/core/codex-lb/probes/computer-use-probe.js +50 -0
- package/dist/core/codex-lb/probes/image-generation-probe.js +47 -0
- package/dist/core/codex-lb/probes/probe-evidence.js +39 -0
- package/dist/core/codex-lb/probes/voice-realtime-probe.js +44 -0
- package/dist/core/codex-lb/trusted-deep-evidence.js +403 -0
- package/dist/core/codex-native/core-skill-manifest.js +22 -6
- package/dist/core/codex-runtime/codex-desktop-config-policy.js +2 -1
- package/dist/core/commands/align-command.js +220 -0
- package/dist/core/commands/image-ux-review-command.js +280 -68
- package/dist/core/commands/naruto-command.js +257 -8
- package/dist/core/commands/ppt-command.js +2 -2
- package/dist/core/commands/telegram-command.js +402 -34
- package/dist/core/commands/triwiki-command.js +11 -1
- package/dist/core/commands/triwiki-graph-command.js +160 -0
- package/dist/core/commands/wiki-command.js +100 -20
- package/dist/core/doctor/skill-legacy-surface.js +1 -3
- package/dist/core/errors/message.js +3 -0
- package/dist/core/evidence/evidence-freshness.js +14 -0
- package/dist/core/feature-fixtures.js +9 -1
- package/dist/core/feature-registry.js +5 -3
- package/dist/core/fsx.js +7 -3
- package/dist/core/hooks-runtime/codex-commit-hooks-selftest.js +82 -0
- package/dist/core/hooks-runtime/context-graph-freshness-preflight.js +113 -0
- package/dist/core/hooks-runtime/hook-context.js +15 -2
- package/dist/core/hooks-runtime/managed-skill-generation-heal.js +46 -0
- package/dist/core/hooks-runtime/naruto-terminal-finalization.js +1 -0
- package/dist/core/hooks-runtime/official-subagent-lifecycle.js +1 -1
- package/dist/core/hooks-runtime/tool-output-quarantine.js +1 -1
- package/dist/core/hooks-runtime.js +33 -89
- package/dist/core/image-ux-review/callout-extraction.js +6 -0
- package/dist/core/image-ux-review/imagegen-adapter.js +64 -23
- package/dist/core/image-ux-review/real-callout-extractor.js +1 -0
- package/dist/core/image-ux-review.js +230 -12
- package/dist/core/imagegen/codex-lb-imagegen-target.js +21 -7
- package/dist/core/imagegen/imagegen-capability.js +10 -4
- package/dist/core/imagegen/require-imagegen.js +29 -5
- package/dist/core/init/skills.js +25 -13
- package/dist/core/init.js +1 -1
- package/dist/core/install/installed-package-smoke.js +157 -4
- package/dist/core/managed-assets/managed-assets-manifest.js +2 -1
- package/dist/core/managed-path-safety.js +8 -10
- package/dist/core/mcp-config/backup.js +2 -4
- package/dist/core/mcp-config/config-reader.js +2 -4
- package/dist/core/mcp-config/scope.js +3 -5
- package/dist/core/naruto/context-graph-advisor-scope.js +242 -0
- package/dist/core/naruto/context-graph-advisor.js +0 -0
- package/dist/core/perf/release-latency-slo.js +12 -6
- package/dist/core/pipeline-internals/runtime-core.js +78 -0
- package/dist/core/pipeline-internals/runtime-gates.js +5 -2
- package/dist/core/policy/latest-version-guidance.js +113 -0
- package/dist/core/ppt.js +8 -8
- package/dist/core/preflight/parallel-preflight-engine.js +1 -1
- package/dist/core/proof/auto-finalize.js +98 -6
- package/dist/core/proof/route-proof-gate.js +12 -1
- package/dist/core/proof/route-proof-policy.js +8 -8
- package/dist/core/release/gate-manifest.js +32 -1
- package/dist/core/release/npm-stage-contract.js +21 -0
- package/dist/core/release/package-size-budget.js +2 -2
- package/dist/core/release/release-gate-contract.js +5 -0
- package/dist/core/release/release-proof-io.js +1 -3
- package/dist/core/release/release-real-contract.js +5 -4
- package/dist/core/release/release-stamp-proof.js +1 -3
- package/dist/core/release/stage-publish.js +27 -0
- package/dist/core/remote/audit-idempotency.js +50 -18
- package/dist/core/remote/codex-public-activity.js +283 -0
- package/dist/core/remote/index.js +1 -0
- package/dist/core/remote/ssh-worker-client.js +4 -1
- package/dist/core/remote/worker.js +206 -27
- package/dist/core/research/research-adversarial-review.js +1 -3
- package/dist/core/research/research-claim-synthesizer.js +1 -3
- package/dist/core/research/research-falsification-runner.js +1 -3
- package/dist/core/research/research-source-evidence.js +1 -3
- package/dist/core/responses-stream.js +65 -19
- package/dist/core/routes/constants.js +1 -1
- package/dist/core/routes/design-policy.js +7 -2
- package/dist/core/routes/dollar-manifest-lite.js +2 -4
- package/dist/core/routes/evidence.js +1 -1
- package/dist/core/routes.js +30 -10
- package/dist/core/safety/command-contract/contracts.js +2 -0
- package/dist/core/safety/command-contract/types.js +1 -1
- package/dist/core/search/context-graph-seeds.js +205 -0
- package/dist/core/search/context.js +0 -0
- package/dist/core/search/provider.js +2 -2
- package/dist/core/secret-redaction.js +6 -0
- package/dist/core/subagents/naruto-help-contract.js +2 -1
- package/dist/core/subagents/naruto-host-credentials.js +161 -0
- package/dist/core/subagents/official-subagent-preparation.js +4 -1
- package/dist/core/subagents/official-subagent-prompt.js +44 -2
- package/dist/core/subagents/official-subagent-runner.js +26 -8
- package/dist/core/subagents/triwiki-attention.js +135 -123
- package/dist/core/telegram/bot-api.js +85 -10
- package/dist/core/telegram/center-snapshot.js +718 -0
- package/dist/core/telegram/config.js +158 -31
- package/dist/core/telegram/hub.js +288 -33
- package/dist/core/telegram/index.js +2 -0
- package/dist/core/telegram/ingress-spool.js +419 -0
- package/dist/core/telegram/ledgers.js +188 -29
- package/dist/core/telegram/messages.js +26 -6
- package/dist/core/telegram/owner-lock.js +10 -7
- package/dist/core/telegram/runtime-projection.js +38 -3
- package/dist/core/telegram/runtime.js +298 -63
- package/dist/core/telegram/service.js +137 -8
- package/dist/core/telegram/setup.js +291 -73
- package/dist/core/text/strings.js +3 -0
- package/dist/core/triwiki/code-pack.js +12 -120
- package/dist/core/triwiki/context-graph/compiler/cache-key.js +277 -0
- package/dist/core/triwiki/context-graph/compiler/extract.js +104 -0
- package/dist/core/triwiki/context-graph/compiler/freshness.js +108 -0
- package/dist/core/triwiki/context-graph/compiler/index.js +237 -0
- package/dist/core/triwiki/context-graph/compiler/merge.js +0 -0
- package/dist/core/triwiki/context-graph/compiler/serialize.js +115 -0
- package/dist/core/triwiki/context-graph/contracts.js +236 -0
- package/dist/core/triwiki/context-graph/extractors/code/builders.js +150 -0
- package/dist/core/triwiki/context-graph/extractors/code/declarations.js +161 -0
- package/dist/core/triwiki/context-graph/extractors/code/index.js +265 -0
- package/dist/core/triwiki/context-graph/extractors/code/inventory.js +237 -0
- package/dist/core/triwiki/context-graph/extractors/code/module-graph.js +252 -0
- package/dist/core/triwiki/context-graph/extractors/code/modules.js +79 -0
- package/dist/core/triwiki/context-graph/extractors/code/references.js +184 -0
- package/dist/core/triwiki/context-graph/extractors/code/selection.js +100 -0
- package/dist/core/triwiki/context-graph/extractors/code/ts-config.js +161 -0
- package/dist/core/triwiki/context-graph/extractors/code/types.js +18 -0
- package/dist/core/triwiki/context-graph/extractors/evidence/claims.js +289 -0
- package/dist/core/triwiki/context-graph/extractors/evidence/index.js +54 -0
- package/dist/core/triwiki/context-graph/extractors/evidence/proof-index.js +213 -0
- package/dist/core/triwiki/context-graph/extractors/evidence/proofs.js +266 -0
- package/dist/core/triwiki/context-graph/extractors/evidence/redaction.js +177 -0
- package/dist/core/triwiki/context-graph/extractors/evidence/shared.js +0 -0
- package/dist/core/triwiki/context-graph/extractors/evidence/sources.js +154 -0
- package/dist/core/triwiki/context-graph/extractors/index.js +7 -0
- package/dist/core/triwiki/context-graph/extractors/topology/commands.js +207 -0
- package/dist/core/triwiki/context-graph/extractors/topology/gate-edges.js +201 -0
- package/dist/core/triwiki/context-graph/extractors/topology/gates.js +182 -0
- package/dist/core/triwiki/context-graph/extractors/topology/globs.js +124 -0
- package/dist/core/triwiki/context-graph/extractors/topology/index.js +92 -0
- package/dist/core/triwiki/context-graph/extractors/topology/routes.js +135 -0
- package/dist/core/triwiki/context-graph/extractors/topology/shared.js +215 -0
- package/dist/core/triwiki/context-graph/graph-index.js +143 -0
- package/dist/core/triwiki/context-graph/ids.js +0 -0
- package/dist/core/triwiki/context-graph/lint/index.js +0 -0
- package/dist/core/triwiki/context-graph/lint/rules.js +232 -0
- package/dist/core/triwiki/context-graph/lint/warnings.js +93 -0
- package/dist/core/triwiki/context-graph/paths.js +116 -0
- package/dist/core/triwiki/context-graph/profiles.js +78 -0
- package/dist/core/triwiki/context-graph/projections/anchors.js +91 -0
- package/dist/core/triwiki/context-graph/projections/attention.js +58 -0
- package/dist/core/triwiki/context-graph/projections/code-pack-workspace.js +62 -0
- package/dist/core/triwiki/context-graph/projections/code-pack.js +144 -0
- package/dist/core/triwiki/context-graph/projections/index.js +7 -0
- package/dist/core/triwiki/context-graph/projections/module-view.js +140 -0
- package/dist/core/triwiki/context-graph/projections/node-summary.js +118 -0
- package/dist/core/triwiki/context-graph/projections/pack-contract.js +6 -0
- package/dist/core/triwiki/context-graph/query/explain.js +0 -0
- package/dist/core/triwiki/context-graph/query/index.js +178 -0
- package/dist/core/triwiki/context-graph/query/load.js +116 -0
- package/dist/core/triwiki/context-graph/query/pack.js +117 -0
- package/dist/core/triwiki/context-graph/query/rank.js +142 -0
- package/dist/core/triwiki/context-graph/query/ranking-config.js +64 -0
- package/dist/core/triwiki/context-graph/query/seeds.js +241 -0
- package/dist/core/triwiki/context-graph/query/snapshot-cache.js +72 -0
- package/dist/core/triwiki/context-graph/query/traverse.js +154 -0
- package/dist/core/triwiki/context-graph/query-types.js +29 -0
- package/dist/core/triwiki/context-graph/store/compile-lock.js +21 -0
- package/dist/core/triwiki/context-graph/store/event-log.js +48 -0
- package/dist/core/triwiki/context-graph/store/fragment-cache.js +85 -0
- package/dist/core/triwiki/context-graph/store/graph-status.js +85 -0
- package/dist/core/triwiki/context-graph/store/snapshot-store.js +155 -0
- package/dist/core/triwiki/triwiki-proof-bank-index-repair.js +115 -0
- package/dist/core/triwiki/triwiki-proof-bank-index-store.js +290 -0
- package/dist/core/triwiki/triwiki-proof-bank-index.js +126 -0
- package/dist/core/triwiki/triwiki-proof-bank.js +7 -1
- package/dist/core/verification/context-graph-affected.js +271 -0
- package/dist/core/version.js +1 -1
- package/dist/native/sks-menubar/Sources/AlertFactory.swift +11 -3
- package/dist/native/sks-menubar/Sources/AppDelegate.swift +9 -0
- package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +25 -3
- package/dist/native/sks-menubar/Sources/ControlKit.swift +19 -8
- package/dist/native/sks-menubar/Sources/DiagnosticsViewController.swift +1 -2
- package/dist/native/sks-menubar/Sources/MCPServersViewController.swift +45 -17
- package/dist/native/sks-menubar/Sources/OverviewSummary.swift +162 -0
- package/dist/native/sks-menubar/Sources/OverviewViewController.swift +136 -190
- package/dist/native/sks-menubar/Sources/ProcessClient.swift +107 -11
- package/dist/native/sks-menubar/Sources/ProvidersMultiProvider.swift +5 -1
- package/dist/native/sks-menubar/Sources/ProvidersOpenRouter.swift +16 -4
- package/dist/native/sks-menubar/Sources/ProvidersRoleModels.swift +4 -1
- package/dist/native/sks-menubar/Sources/ProvidersRoutingTruth.swift +131 -0
- package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +288 -341
- package/dist/native/sks-menubar/Sources/RemoteTelegramCenterState.swift +447 -0
- package/dist/native/sks-menubar/Sources/RemoteTelegramRendering.swift +484 -0
- package/dist/native/sks-menubar/Sources/RemoteTelegramViewController.swift +364 -115
- package/dist/native/sks-menubar/Sources/SettingsViewController.swift +73 -12
- package/dist/native/sks-menubar/Sources/SidebarItem.swift +3 -3
- package/dist/native/sks-menubar/Sources/StatusItemController.swift +13 -13
- package/dist/native/sks-menubar/Sources/TelegramHubSupervisor.swift +469 -0
- package/dist/native/sks-menubar/Sources/UpdatesViewController.swift +1 -6
- package/dist/native/sks-menubar/Tests/AppRuntimeTestSupport.swift +12 -0
- package/dist/native/sks-menubar/Tests/NativeTelegramRegressionRunner.swift +55 -0
- package/dist/native/sks-menubar/Tests/ProcessClientTests.swift +33 -0
- package/dist/native/sks-menubar/Tests/ProvidersViewControllerTests.swift +142 -0
- package/dist/native/sks-menubar/Tests/RemoteTelegramCenterStateTests.swift +56 -0
- package/dist/native/sks-menubar/Tests/TelegramHubSupervisorTests.swift +55 -0
- package/dist/scripts/codex-lb-catalog-passthrough-check.js +120 -0
- package/dist/scripts/codex-lb-desktop-auth-invariant-check.js +169 -0
- package/dist/scripts/codex-lb-desktop-bridge-check.js +394 -0
- package/dist/scripts/codex-lb-desktop-bridge-latency-check.js +196 -0
- package/dist/scripts/codex-lb-desktop-capabilities-check.js +287 -0
- package/dist/scripts/codex-lb-desktop-real-evidence-check.js +123 -0
- package/dist/scripts/codex-lb-fast-mode-truth-check.js +10 -3
- package/dist/scripts/codex-lb-fast-ui-preservation-check.js +81 -40
- package/dist/scripts/codex-lb-gpt56-fast-profile-check.js +99 -37
- package/dist/scripts/context-graph-check.js +191 -0
- package/dist/scripts/doctor-fixes-codex-app-fast-ui-check.js +1 -1
- package/dist/scripts/gate-policy-audit-check.js +1 -1
- package/dist/scripts/helper-dedup-check.js +0 -2
- package/dist/scripts/installed-package-smoke-check.js +4 -1
- package/dist/scripts/latest-version-guidance-check.js +74 -0
- package/dist/scripts/release-metadata-check.js +21 -4
- package/dist/scripts/release-real-check.js +7 -0
- package/dist/scripts/ux-review-imagegen-blackbox-check.js +25 -1
- package/dist/scripts/ux-review-run-wires-imagegen-check.js +9 -1
- package/package.json +8 -3
- package/release-gates.v2.json +176 -1
- package/runtime-required-scripts.json +30 -0
- package/schemas/triwiki/context-graph.schema.json +119 -0
- package/dist/cli/install-helpers-codex-lb-selftest-chain.js +0 -112
- package/dist/cli/install-helpers-codex-lb-selftest.js +0 -481
- package/dist/core/triwiki/code-index-scanner.js +0 -309
|
@@ -11,58 +11,18 @@ const CODEX_LB_TOOL_CATALOG_MAX_METADATA_BYTES = 16 * 1024;
|
|
|
11
11
|
const DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
12
12
|
const GPT56_MODEL_RE = /^gpt-5\.6-(?:sol|terra|luna)$/;
|
|
13
13
|
const ensureInflight = new Map();
|
|
14
|
-
const
|
|
14
|
+
const REQUIRED_MODEL_FIELDS = {
|
|
15
15
|
slug: ['string'],
|
|
16
16
|
display_name: ['string'],
|
|
17
17
|
supported_reasoning_levels: ['array'],
|
|
18
|
-
|
|
19
|
-
visibility: ['string'],
|
|
20
|
-
supported_in_api: ['boolean'],
|
|
21
|
-
priority: ['number'],
|
|
22
|
-
base_instructions: ['string'],
|
|
23
|
-
support_verbosity: ['boolean'],
|
|
24
|
-
truncation_policy: ['object'],
|
|
25
|
-
supports_parallel_tool_calls: ['boolean'],
|
|
26
|
-
experimental_supported_tools: ['array']
|
|
27
|
-
};
|
|
28
|
-
const REASONING_SUMMARY_FIELD_NAMES = ['supports_reasoning_summaries', 'supports_reasoning_summary_parameter'];
|
|
29
|
-
const CODEX_0144_MODEL_FIELD_TYPES = {
|
|
30
|
-
...REQUIRED_CODEX_0144_MODEL_FIELDS,
|
|
31
|
-
supports_reasoning_summaries: ['boolean'],
|
|
32
|
-
supports_reasoning_summary_parameter: ['boolean'],
|
|
33
|
-
description: ['string'],
|
|
34
|
-
default_reasoning_level: ['string'],
|
|
35
|
-
additional_speed_tiers: ['array'],
|
|
36
|
-
service_tiers: ['array'],
|
|
37
|
-
availability_nux: ['object', 'null'],
|
|
38
|
-
upgrade: ['object', 'null'],
|
|
39
|
-
model_messages: ['object'],
|
|
40
|
-
include_skills_usage_instructions: ['boolean'],
|
|
41
|
-
default_reasoning_summary: ['string'],
|
|
42
|
-
default_verbosity: ['string'],
|
|
43
|
-
apply_patch_tool_type: ['string'],
|
|
44
|
-
web_search_tool_type: ['string'],
|
|
45
|
-
supports_image_detail_original: ['boolean'],
|
|
46
|
-
context_window: ['number'],
|
|
47
|
-
max_context_window: ['number'],
|
|
48
|
-
comp_hash: ['string'],
|
|
49
|
-
effective_context_window_percent: ['number'],
|
|
50
|
-
input_modalities: ['array'],
|
|
51
|
-
supports_search_tool: ['boolean'],
|
|
52
|
-
use_responses_lite: ['boolean'],
|
|
53
|
-
tool_mode: ['string', 'null'],
|
|
54
|
-
multi_agent_version: ['string', 'null'],
|
|
55
|
-
minimal_client_version: ['string'],
|
|
56
|
-
available_in_plans: ['array'],
|
|
57
|
-
prefer_websockets: ['boolean'],
|
|
58
|
-
auto_review_model_override: ['string', 'null'],
|
|
59
|
-
auto_compact_token_limit: ['number', 'null'],
|
|
60
|
-
reasoning_summary_format: ['string'],
|
|
61
|
-
default_service_tier: ['string', 'null']
|
|
18
|
+
truncation_policy: ['object']
|
|
62
19
|
};
|
|
63
20
|
export function isCodexLbGpt56Model(model) {
|
|
64
21
|
return GPT56_MODEL_RE.test(String(model || '').trim());
|
|
65
22
|
}
|
|
23
|
+
export function shouldBindLocalModelCatalog(mode) {
|
|
24
|
+
return mode === 'desktop-dual-auth-compat' || mode === 'cli-provider';
|
|
25
|
+
}
|
|
66
26
|
export function codexLbToolCatalogPath(codexHome) {
|
|
67
27
|
return path.join(path.resolve(codexHome), CODEX_LB_TOOL_CATALOG_FILENAME);
|
|
68
28
|
}
|
|
@@ -85,30 +45,34 @@ export function normalizeCodexLbToolCatalog(payload, opts = {}) {
|
|
|
85
45
|
validationIssues.push(`codex_lb_model_catalog_row_invalid:${index}:object`);
|
|
86
46
|
return {};
|
|
87
47
|
}
|
|
88
|
-
|
|
48
|
+
let normalized;
|
|
49
|
+
try {
|
|
50
|
+
normalized = normalizeCodexLbModelRow(row, index);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
validationIssues.push(error instanceof Error ? error.message : `codex_lb_model_catalog_row_invalid:${index}`);
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
const model = String(normalized.slug || '').trim();
|
|
89
57
|
if (isCodexLbGpt56Model(model))
|
|
90
58
|
gpt56Models.push(model);
|
|
91
|
-
const sanitized = sanitizeCodex0144Model(row);
|
|
92
|
-
validationIssues.push(...validateCodex0144Model(sanitized, index));
|
|
93
|
-
if (!isCodexLbGpt56Model(model))
|
|
94
|
-
return sanitized;
|
|
95
59
|
if (row.use_responses_lite !== false)
|
|
96
60
|
patchedModels.push(model);
|
|
97
|
-
|
|
61
|
+
validationIssues.push(...validateKnownModelFields(normalized, index));
|
|
62
|
+
return normalized;
|
|
98
63
|
});
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
&&
|
|
102
|
-
&& compatibleRows.every((row) => row.use_responses_lite === false);
|
|
64
|
+
const compatible = models.length > 0
|
|
65
|
+
&& validationIssues.length === 0
|
|
66
|
+
&& models.every((row) => row.use_responses_lite === false);
|
|
103
67
|
const blockers = uniqueBounded([
|
|
104
68
|
...(rows.length ? [] : ['codex_lb_model_catalog_empty']),
|
|
105
69
|
...(rows.length > maxModels ? [`codex_lb_model_catalog_model_limit_exceeded:${rows.length}:${maxModels}`] : []),
|
|
106
70
|
...validationIssues,
|
|
107
|
-
...(
|
|
108
|
-
...(compatible ? [] : ['codex_lb_gpt56_tools_transport_incompatible'])
|
|
71
|
+
...(compatible ? [] : ['codex_lb_model_catalog_tools_transport_incompatible'])
|
|
109
72
|
]);
|
|
110
73
|
return {
|
|
111
74
|
schema: CODEX_LB_TOOL_CATALOG_SCHEMA,
|
|
75
|
+
contract: 'codex-model-catalog-pass-through.v2',
|
|
112
76
|
ok: blockers.length === 0,
|
|
113
77
|
catalog: { models },
|
|
114
78
|
model_count: models.length,
|
|
@@ -230,7 +194,11 @@ async function ensureValidatedCodexLbToolCatalog(input, outputPath, baseUrl, api
|
|
|
230
194
|
const { catalog: _catalog, ...status } = normalized;
|
|
231
195
|
return { ...status, generated_at: nowIso(), status: 'blocked', path: outputPath, exists: await exists(outputPath), fetched: true, required: true };
|
|
232
196
|
}
|
|
233
|
-
await writeSecureCatalog(outputPath, normalized.catalog, identity
|
|
197
|
+
await writeSecureCatalog(outputPath, normalized.catalog, identity, {
|
|
198
|
+
upstream_etag: response.headers.get('etag'),
|
|
199
|
+
upstream_last_modified: response.headers.get('last-modified'),
|
|
200
|
+
client_version: String(input.clientVersion || 'runtime-discovered')
|
|
201
|
+
});
|
|
234
202
|
const verified = await inspectCodexLbToolCatalog(outputPath, inspectOpts);
|
|
235
203
|
return {
|
|
236
204
|
...verified,
|
|
@@ -305,7 +273,7 @@ function parseCatalogJson(text) {
|
|
|
305
273
|
throw new CatalogFetchError('codex_lb_tool_catalog_response_json_invalid');
|
|
306
274
|
}
|
|
307
275
|
}
|
|
308
|
-
async function writeSecureCatalog(file, catalog, identity) {
|
|
276
|
+
async function writeSecureCatalog(file, catalog, identity, upstream) {
|
|
309
277
|
const text = `${JSON.stringify(catalog, null, 2)}\n`;
|
|
310
278
|
const metadataPath = codexLbToolCatalogMetadataPath(file);
|
|
311
279
|
const metadata = {
|
|
@@ -314,7 +282,12 @@ async function writeSecureCatalog(file, catalog, identity) {
|
|
|
314
282
|
catalog_schema: CODEX_LB_TOOL_CATALOG_SCHEMA,
|
|
315
283
|
catalog_sha256: sha256(text),
|
|
316
284
|
identity,
|
|
317
|
-
model_count: Array.isArray(catalog?.models) ? catalog.models.length : 0
|
|
285
|
+
model_count: Array.isArray(catalog?.models) ? catalog.models.length : 0,
|
|
286
|
+
upstream_etag: upstream.upstream_etag,
|
|
287
|
+
upstream_last_modified: upstream.upstream_last_modified,
|
|
288
|
+
client_version: upstream.client_version,
|
|
289
|
+
unknown_fields_preserved: true,
|
|
290
|
+
normalizations: ['desktop_full_responses_required']
|
|
318
291
|
};
|
|
319
292
|
await ensureDir(path.dirname(file));
|
|
320
293
|
await writeTextAtomic(file, text, { mode: 0o600 });
|
|
@@ -344,14 +317,26 @@ async function inspectSecureRegularFile(file, maxBytes, prefix) {
|
|
|
344
317
|
blockers.push(`${prefix}_mode_insecure:${mode.toString(8)}`);
|
|
345
318
|
return { ok: blockers.length === 0, exists: true, updated_at_ms: stat.mtimeMs, size_bytes: stat.size, mode: mode.toString(8).padStart(3, '0'), owner_uid: ownerUid, blockers };
|
|
346
319
|
}
|
|
347
|
-
function
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
320
|
+
export function normalizeCodexLbModelRow(row, index = 0) {
|
|
321
|
+
const cloned = structuredClone(row);
|
|
322
|
+
for (const key of Object.keys(cloned)) {
|
|
323
|
+
if (isSensitiveCatalogField(key))
|
|
324
|
+
delete cloned[key];
|
|
325
|
+
}
|
|
326
|
+
const slug = String(cloned.slug ?? cloned.id ?? cloned.model ?? cloned.name ?? '').trim();
|
|
327
|
+
if (!slug)
|
|
328
|
+
throw new Error(`codex_lb_model_catalog_slug_missing:${index}`);
|
|
329
|
+
if (!Object.prototype.hasOwnProperty.call(cloned, 'slug'))
|
|
330
|
+
cloned.slug = slug;
|
|
331
|
+
cloned.use_responses_lite = false;
|
|
332
|
+
return cloned;
|
|
333
|
+
}
|
|
334
|
+
function isSensitiveCatalogField(field) {
|
|
335
|
+
return /(?:^|[_-])(?:authorization|api[_-]?key|access[_-]?token|refresh[_-]?token|cookie|set[_-]?cookie)(?:$|[_-])/i.test(field);
|
|
351
336
|
}
|
|
352
|
-
function
|
|
337
|
+
function validateKnownModelFields(row, index) {
|
|
353
338
|
const issues = [];
|
|
354
|
-
for (const [field, allowedTypes] of Object.entries(
|
|
339
|
+
for (const [field, allowedTypes] of Object.entries(REQUIRED_MODEL_FIELDS)) {
|
|
355
340
|
if (!Object.prototype.hasOwnProperty.call(row, field)) {
|
|
356
341
|
issues.push(`codex_lb_model_catalog_required_field_missing:${index}:${field}`);
|
|
357
342
|
continue;
|
|
@@ -359,13 +344,6 @@ function validateCodex0144Model(row, index) {
|
|
|
359
344
|
if (!allowedTypes.includes(valueType(row[field])))
|
|
360
345
|
issues.push(`codex_lb_model_catalog_field_type_invalid:${index}:${field}`);
|
|
361
346
|
}
|
|
362
|
-
if (!REASONING_SUMMARY_FIELD_NAMES.some((field) => Object.prototype.hasOwnProperty.call(row, field))) {
|
|
363
|
-
issues.push(`codex_lb_model_catalog_required_field_missing:${index}:supports_reasoning_summaries`);
|
|
364
|
-
}
|
|
365
|
-
for (const [field, value] of Object.entries(row)) {
|
|
366
|
-
if (!(CODEX_0144_MODEL_FIELD_TYPES[field] || []).includes(valueType(value)))
|
|
367
|
-
issues.push(`codex_lb_model_catalog_field_type_invalid:${index}:${field}`);
|
|
368
|
-
}
|
|
369
347
|
if (typeof row.slug === 'string' && !row.slug.trim())
|
|
370
348
|
issues.push(`codex_lb_model_catalog_field_empty:${index}:slug`);
|
|
371
349
|
if (typeof row.display_name === 'string' && !row.display_name.trim())
|
|
@@ -394,7 +372,7 @@ function catalogIdentity(baseUrl, apiKey) {
|
|
|
394
372
|
origin: new URL(baseUrl).origin,
|
|
395
373
|
base_url_sha256: sha256(baseUrl),
|
|
396
374
|
api_key_sha256: sha256(apiKey),
|
|
397
|
-
contract: 'codex-
|
|
375
|
+
contract: 'codex-model-catalog-pass-through.v2'
|
|
398
376
|
};
|
|
399
377
|
}
|
|
400
378
|
function sameCatalogIdentity(actual, expected) {
|
|
@@ -5,7 +5,7 @@ export const CODEX_LB_TOOL_OUTPUT_RECOVERY_MIN_VERSION = '1.21.0-beta.3';
|
|
|
5
5
|
export const CODEX_LB_TOOL_OUTPUT_RECOVERY_OVERRIDE_ENV = 'SKS_ALLOW_UNVERIFIED_CODEX_LB_RECOVERY';
|
|
6
6
|
export const CODEX_LB_TOOL_OUTPUT_RECOVERY_OVERRIDE_FLAG = '--allow-unverified-codex-lb-recovery';
|
|
7
7
|
const UPGRADE_ACTION = `Upgrade codex-lb to ${CODEX_LB_TOOL_OUTPUT_RECOVERY_MIN_VERSION} or later, then rerun \`sks codex-lb status\`.`;
|
|
8
|
-
const OAUTH_ACTION = 'To
|
|
8
|
+
const OAUTH_ACTION = 'To bypass codex-lb routing while preserving ChatGPT OAuth, run `sks codex-lb disable`; SKS will not change routing silently.';
|
|
9
9
|
const FRESH_THREAD_ACTION = 'After upgrading or switching providers, open a fresh Codex thread and continue the persisted mission; do not replay the corrupted turn.';
|
|
10
10
|
export async function probeCodexLbToolOutputRecovery(opts = {}) {
|
|
11
11
|
const baseUrl = normalizeCodexLbBaseUrl(opts.baseUrl);
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { createHash, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import fsp from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
export async function captureCodexAuthSnapshot(input = {}) {
|
|
6
|
+
const home = input.home || process.env.HOME || os.homedir();
|
|
7
|
+
const authPath = input.authPath || path.join(home, '.codex', 'auth.json');
|
|
8
|
+
let bytes;
|
|
9
|
+
try {
|
|
10
|
+
const stat = await fsp.lstat(authPath);
|
|
11
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
12
|
+
throw new Error('desktop_auth_not_regular_file');
|
|
13
|
+
bytes = await fsp.readFile(authPath);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
if (isMissingFileError(error)) {
|
|
17
|
+
return {
|
|
18
|
+
path: authPath,
|
|
19
|
+
exists: false,
|
|
20
|
+
sha256: null,
|
|
21
|
+
semantic_fingerprint: null,
|
|
22
|
+
mode: 'missing',
|
|
23
|
+
has_refresh_token: false,
|
|
24
|
+
has_access_token: false,
|
|
25
|
+
has_api_key: false
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
const parsed = parseJsonObject(bytes.toString('utf8'));
|
|
31
|
+
const hasRefreshToken = containsNamedSecret(parsed, /^(?:refresh_token|refreshToken)$/);
|
|
32
|
+
const hasAccessToken = containsNamedSecret(parsed, /^(?:access_token|accessToken)$/);
|
|
33
|
+
const hasIdToken = containsNamedSecret(parsed, /^(?:id_token|idToken)$/);
|
|
34
|
+
const apiKey = findNamedSecret(parsed, /^(?:key|api_key|apiKey|openai_api_key|OPENAI_API_KEY)$/);
|
|
35
|
+
const hasApiKey = Boolean(apiKey);
|
|
36
|
+
const authMode = stringField(parsed, ['auth_mode', 'authMode', 'mode']).toLowerCase();
|
|
37
|
+
const oauth = hasRefreshToken || hasAccessToken || hasIdToken || /chatgpt|oauth|browser/.test(authMode);
|
|
38
|
+
const mode = oauth && hasApiKey
|
|
39
|
+
? 'mixed'
|
|
40
|
+
: oauth
|
|
41
|
+
? 'chatgpt_oauth'
|
|
42
|
+
: hasApiKey
|
|
43
|
+
? 'openai_api_key'
|
|
44
|
+
: 'unknown';
|
|
45
|
+
return {
|
|
46
|
+
path: authPath,
|
|
47
|
+
exists: true,
|
|
48
|
+
sha256: sha256(bytes),
|
|
49
|
+
semantic_fingerprint: oauth ? oauthSemanticFingerprint(parsed) : null,
|
|
50
|
+
mode,
|
|
51
|
+
has_refresh_token: hasRefreshToken,
|
|
52
|
+
has_access_token: hasAccessToken,
|
|
53
|
+
has_api_key: hasApiKey
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export async function codexAuthApiKeyMatches(input) {
|
|
57
|
+
const expectedApiKey = String(input.expectedApiKey || '');
|
|
58
|
+
if (!expectedApiKey)
|
|
59
|
+
return false;
|
|
60
|
+
const home = input.home || process.env.HOME || os.homedir();
|
|
61
|
+
const authPath = input.authPath || path.join(home, '.codex', 'auth.json');
|
|
62
|
+
try {
|
|
63
|
+
const stat = await fsp.lstat(authPath);
|
|
64
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
65
|
+
return false;
|
|
66
|
+
const parsed = parseJsonObject(await fsp.readFile(authPath, 'utf8'));
|
|
67
|
+
const apiKey = findNamedSecret(parsed, /^(?:key|api_key|apiKey|openai_api_key|OPENAI_API_KEY)$/);
|
|
68
|
+
if (!apiKey)
|
|
69
|
+
return false;
|
|
70
|
+
const actualDigest = Buffer.from(sha256(apiKey), 'hex');
|
|
71
|
+
const expectedDigest = Buffer.from(sha256(expectedApiKey), 'hex');
|
|
72
|
+
return timingSafeEqual(actualDigest, expectedDigest);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export async function assertDesktopAuthUnchangedBySks(before, afterConfigCommitBeforeRestart) {
|
|
79
|
+
if (before.path !== afterConfigCommitBeforeRestart.path
|
|
80
|
+
|| before.exists !== afterConfigCommitBeforeRestart.exists
|
|
81
|
+
|| before.sha256 !== afterConfigCommitBeforeRestart.sha256) {
|
|
82
|
+
throw new Error('desktop_auth_mutated_by_sks');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export function assertDesktopOAuthSemanticIdentity(before, afterAppRestart) {
|
|
86
|
+
if (before.mode !== 'chatgpt_oauth' && before.mode !== 'mixed') {
|
|
87
|
+
throw new Error(`desktop_oauth_missing_before_restart:${before.mode}`);
|
|
88
|
+
}
|
|
89
|
+
if (afterAppRestart.mode !== 'chatgpt_oauth' && afterAppRestart.mode !== 'mixed') {
|
|
90
|
+
throw new Error(`desktop_oauth_missing_after_restart:${afterAppRestart.mode}`);
|
|
91
|
+
}
|
|
92
|
+
if (before.semantic_fingerprint === null || afterAppRestart.semantic_fingerprint === null) {
|
|
93
|
+
throw new Error('desktop_oauth_identity_unverifiable');
|
|
94
|
+
}
|
|
95
|
+
if (before.semantic_fingerprint !== afterAppRestart.semantic_fingerprint) {
|
|
96
|
+
throw new Error('desktop_oauth_identity_changed');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function parseJsonObject(text) {
|
|
100
|
+
try {
|
|
101
|
+
const parsed = JSON.parse(text);
|
|
102
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
103
|
+
? parsed
|
|
104
|
+
: null;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function containsNamedSecret(value, keyPattern) {
|
|
111
|
+
return Boolean(findNamedSecret(value, keyPattern));
|
|
112
|
+
}
|
|
113
|
+
function findNamedSecret(value, keyPattern) {
|
|
114
|
+
if (!value || typeof value !== 'object')
|
|
115
|
+
return null;
|
|
116
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
117
|
+
if (keyPattern.test(key) && typeof entry === 'string' && entry.trim())
|
|
118
|
+
return entry.trim();
|
|
119
|
+
const nested = findNamedSecret(entry, keyPattern);
|
|
120
|
+
if (nested)
|
|
121
|
+
return nested;
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
function stringField(value, keys) {
|
|
126
|
+
if (!value)
|
|
127
|
+
return '';
|
|
128
|
+
for (const key of keys) {
|
|
129
|
+
const entry = value[key];
|
|
130
|
+
if (typeof entry === 'string')
|
|
131
|
+
return entry;
|
|
132
|
+
}
|
|
133
|
+
return '';
|
|
134
|
+
}
|
|
135
|
+
function oauthSemanticFingerprint(parsed) {
|
|
136
|
+
const identity = {};
|
|
137
|
+
collectIdentityFields(parsed, identity);
|
|
138
|
+
for (const token of collectTokenValues(parsed, /^(?:id_token|idToken|access_token|accessToken)$/)) {
|
|
139
|
+
collectIdentityFields(decodeJwtClaims(token), identity);
|
|
140
|
+
}
|
|
141
|
+
if (Object.keys(identity).length === 0)
|
|
142
|
+
return null;
|
|
143
|
+
const sortedIdentity = Object.fromEntries(Object.entries(identity).sort(([left], [right]) => left.localeCompare(right)));
|
|
144
|
+
return sha256(JSON.stringify(sortedIdentity));
|
|
145
|
+
}
|
|
146
|
+
function collectIdentityFields(value, output) {
|
|
147
|
+
if (!value || typeof value !== 'object')
|
|
148
|
+
return;
|
|
149
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
150
|
+
if (/^(?:sub|email|account_id|accountId|chatgpt_account_id|chatgptAccountId|user_id|userId|organization_id|organizationId)$/i.test(key)
|
|
151
|
+
&& (typeof entry === 'string' || typeof entry === 'number')) {
|
|
152
|
+
output[key.toLowerCase()] = String(entry);
|
|
153
|
+
}
|
|
154
|
+
if (typeof entry === 'object')
|
|
155
|
+
collectIdentityFields(entry, output);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function collectTokenValues(value, keyPattern, output = []) {
|
|
159
|
+
if (!value || typeof value !== 'object')
|
|
160
|
+
return output;
|
|
161
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
162
|
+
if (keyPattern.test(key) && typeof entry === 'string' && entry.trim())
|
|
163
|
+
output.push(entry);
|
|
164
|
+
else if (typeof entry === 'object')
|
|
165
|
+
collectTokenValues(entry, keyPattern, output);
|
|
166
|
+
}
|
|
167
|
+
return output;
|
|
168
|
+
}
|
|
169
|
+
function decodeJwtClaims(token) {
|
|
170
|
+
const payload = token.split('.')[1];
|
|
171
|
+
if (!payload)
|
|
172
|
+
return null;
|
|
173
|
+
try {
|
|
174
|
+
return parseJsonObject(Buffer.from(payload, 'base64url').toString('utf8'));
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function sha256(value) {
|
|
181
|
+
return createHash('sha256').update(value).digest('hex');
|
|
182
|
+
}
|
|
183
|
+
function isMissingFileError(error) {
|
|
184
|
+
return Boolean(error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT');
|
|
185
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { readDesktopBridgeState } from './state.js';
|
|
2
|
+
import { safeBridgeErrorCode } from './security.js';
|
|
3
|
+
export const desktopBridgeProcessExists = (pid) => {
|
|
4
|
+
try {
|
|
5
|
+
process.kill(pid, 0);
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
return error.code === 'EPERM';
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
export async function getDesktopBridgeStatus(input) {
|
|
13
|
+
let state;
|
|
14
|
+
try {
|
|
15
|
+
state = await readDesktopBridgeState(input.statePath);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
return { status: 'invalid', state: null, blocker: safeBridgeErrorCode(error) };
|
|
19
|
+
}
|
|
20
|
+
if (!state)
|
|
21
|
+
return { status: 'missing', state: null };
|
|
22
|
+
if (input.expectedConfigGeneration && state.config_generation !== input.expectedConfigGeneration) {
|
|
23
|
+
return {
|
|
24
|
+
status: 'configuration_mismatch',
|
|
25
|
+
state,
|
|
26
|
+
blocker: 'bridge_config_generation_mismatch',
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const processExists = input.processExists || desktopBridgeProcessExists;
|
|
30
|
+
if (!processExists(state.pid)) {
|
|
31
|
+
return { status: 'stale', state, blocker: 'bridge_process_not_running' };
|
|
32
|
+
}
|
|
33
|
+
return { status: 'running', state };
|
|
34
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { rewriteLocationHeader } from './location-rewrite.js';
|
|
2
|
+
import { DesktopBridgeError } from './types.js';
|
|
3
|
+
const HOP_BY_HOP = new Set([
|
|
4
|
+
'connection',
|
|
5
|
+
'keep-alive',
|
|
6
|
+
'proxy-authenticate',
|
|
7
|
+
'proxy-authorization',
|
|
8
|
+
'te',
|
|
9
|
+
'trailer',
|
|
10
|
+
'transfer-encoding',
|
|
11
|
+
'upgrade',
|
|
12
|
+
]);
|
|
13
|
+
const NEVER_FORWARD_FROM_CLIENT = new Set([
|
|
14
|
+
'authorization',
|
|
15
|
+
'cookie',
|
|
16
|
+
'forwarded',
|
|
17
|
+
'proxy-authorization',
|
|
18
|
+
'x-codex-lb-api-key',
|
|
19
|
+
'x-forwarded-for',
|
|
20
|
+
'x-forwarded-host',
|
|
21
|
+
'x-forwarded-port',
|
|
22
|
+
'x-forwarded-proto',
|
|
23
|
+
'x-real-ip',
|
|
24
|
+
]);
|
|
25
|
+
const NEVER_FORWARD_TO_CLIENT = new Set([
|
|
26
|
+
'authorization',
|
|
27
|
+
'cookie',
|
|
28
|
+
'proxy-authenticate',
|
|
29
|
+
'proxy-authorization',
|
|
30
|
+
'set-cookie',
|
|
31
|
+
'x-codex-lb-api-key',
|
|
32
|
+
]);
|
|
33
|
+
function connectionTokens(inbound) {
|
|
34
|
+
const raw = inbound.connection;
|
|
35
|
+
const values = Array.isArray(raw) ? raw : raw ? [raw] : [];
|
|
36
|
+
return new Set(values.flatMap((value) => value.split(',')).map((value) => value.trim().toLowerCase()).filter(Boolean));
|
|
37
|
+
}
|
|
38
|
+
function injectGatewayCredential(result, gatewayKey, transport) {
|
|
39
|
+
if (transport === 'x-codex-lb-api-key') {
|
|
40
|
+
result['x-codex-lb-api-key'] = gatewayKey;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
result.authorization = `Bearer ${gatewayKey}`;
|
|
44
|
+
}
|
|
45
|
+
export function buildUpstreamHeaders(inbound, config, upstreamHost) {
|
|
46
|
+
const result = {};
|
|
47
|
+
const dynamicHopByHop = connectionTokens(inbound);
|
|
48
|
+
for (const [rawName, rawValue] of Object.entries(inbound)) {
|
|
49
|
+
if (rawValue === undefined)
|
|
50
|
+
continue;
|
|
51
|
+
const name = rawName.toLowerCase();
|
|
52
|
+
if (HOP_BY_HOP.has(name) || dynamicHopByHop.has(name))
|
|
53
|
+
continue;
|
|
54
|
+
if (NEVER_FORWARD_FROM_CLIENT.has(name) || name === 'host')
|
|
55
|
+
continue;
|
|
56
|
+
result[name] = rawValue;
|
|
57
|
+
}
|
|
58
|
+
result.host = upstreamHost;
|
|
59
|
+
injectGatewayCredential(result, config.gatewayKey, config.gatewayAuthTransport);
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
export function buildWebSocketHeaders(inbound, config, upstreamHost) {
|
|
63
|
+
const result = buildUpstreamHeaders(inbound, config, upstreamHost);
|
|
64
|
+
for (const [rawName, rawValue] of Object.entries(inbound)) {
|
|
65
|
+
if (rawValue === undefined)
|
|
66
|
+
continue;
|
|
67
|
+
const name = rawName.toLowerCase();
|
|
68
|
+
if (name === 'connection' || name === 'upgrade' || name.startsWith('sec-websocket-')) {
|
|
69
|
+
result[name] = rawValue;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
result.connection = 'Upgrade';
|
|
73
|
+
result.upgrade = 'websocket';
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
export function rewriteResponseHeaders(inbound, config, localOrigin) {
|
|
77
|
+
const result = {};
|
|
78
|
+
const dynamicHopByHop = connectionTokens(inbound);
|
|
79
|
+
for (const [rawName, rawValue] of Object.entries(inbound)) {
|
|
80
|
+
if (rawValue === undefined)
|
|
81
|
+
continue;
|
|
82
|
+
const name = rawName.toLowerCase();
|
|
83
|
+
if (HOP_BY_HOP.has(name) || dynamicHopByHop.has(name) || NEVER_FORWARD_TO_CLIENT.has(name))
|
|
84
|
+
continue;
|
|
85
|
+
if (name.startsWith('access-control-'))
|
|
86
|
+
continue;
|
|
87
|
+
if (name === 'location') {
|
|
88
|
+
if (Array.isArray(rawValue) && rawValue.length !== 1) {
|
|
89
|
+
throw new DesktopBridgeError('bridge_location_header_invalid');
|
|
90
|
+
}
|
|
91
|
+
const location = Array.isArray(rawValue) ? rawValue[0] : rawValue;
|
|
92
|
+
if (!location)
|
|
93
|
+
throw new DesktopBridgeError('bridge_location_header_invalid');
|
|
94
|
+
result.location = rewriteLocationHeader(location, config.remoteBaseUrl, localOrigin);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
result[name] = rawValue;
|
|
98
|
+
}
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
export function isRedactedHeaderName(name) {
|
|
102
|
+
const normalized = name.toLowerCase();
|
|
103
|
+
return NEVER_FORWARD_FROM_CLIENT.has(normalized)
|
|
104
|
+
|| NEVER_FORWARD_TO_CLIENT.has(normalized)
|
|
105
|
+
|| normalized === 'set-cookie';
|
|
106
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import http, {} from 'node:http';
|
|
2
|
+
import https from 'node:https';
|
|
3
|
+
import { pipeline } from 'node:stream/promises';
|
|
4
|
+
import { buildUpstreamHeaders, rewriteResponseHeaders } from './header-policy.js';
|
|
5
|
+
import { resolveDesktopBridgeTarget, safeBridgeErrorCode } from './security.js';
|
|
6
|
+
import { desktopBridgeListenOrigin } from './state.js';
|
|
7
|
+
import { DesktopBridgeError } from './types.js';
|
|
8
|
+
function connectTimeout(request, config) {
|
|
9
|
+
request.once('socket', (socket) => {
|
|
10
|
+
if (!socket.connecting)
|
|
11
|
+
return;
|
|
12
|
+
const timer = setTimeout(() => {
|
|
13
|
+
request.destroy(new DesktopBridgeError('bridge_upstream_connect_timeout'));
|
|
14
|
+
}, config.connectTimeoutMs);
|
|
15
|
+
timer.unref();
|
|
16
|
+
const event = config.remote.secure ? 'secureConnect' : 'connect';
|
|
17
|
+
socket.once(event, () => clearTimeout(timer));
|
|
18
|
+
socket.once('close', () => clearTimeout(timer));
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function writeHttpBridgeError(res, error) {
|
|
22
|
+
const code = safeBridgeErrorCode(error);
|
|
23
|
+
if (res.headersSent) {
|
|
24
|
+
res.destroy(error instanceof Error ? error : undefined);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
res.writeHead(502, {
|
|
28
|
+
'content-type': 'application/json',
|
|
29
|
+
'cache-control': 'no-store',
|
|
30
|
+
connection: 'close',
|
|
31
|
+
});
|
|
32
|
+
res.end(JSON.stringify({
|
|
33
|
+
error: {
|
|
34
|
+
type: 'sks_bridge_error',
|
|
35
|
+
code,
|
|
36
|
+
message: code,
|
|
37
|
+
},
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
export async function forwardHttp(req, res, config) {
|
|
41
|
+
const target = resolveDesktopBridgeTarget(req.url, config.remote);
|
|
42
|
+
const transport = config.remote.secure ? https : http;
|
|
43
|
+
const headers = buildUpstreamHeaders(req.headers, config, target.host);
|
|
44
|
+
const localOrigin = desktopBridgeListenOrigin(config);
|
|
45
|
+
try {
|
|
46
|
+
await new Promise((resolve, reject) => {
|
|
47
|
+
let responseStarted = false;
|
|
48
|
+
let settled = false;
|
|
49
|
+
const finish = (error) => {
|
|
50
|
+
if (settled)
|
|
51
|
+
return;
|
|
52
|
+
settled = true;
|
|
53
|
+
if (error)
|
|
54
|
+
reject(error);
|
|
55
|
+
else
|
|
56
|
+
resolve();
|
|
57
|
+
};
|
|
58
|
+
const upstream = transport.request({
|
|
59
|
+
protocol: target.protocol,
|
|
60
|
+
hostname: config.remote.address,
|
|
61
|
+
family: config.remote.family,
|
|
62
|
+
port: config.remote.port,
|
|
63
|
+
method: req.method,
|
|
64
|
+
path: `${target.pathname}${target.search}`,
|
|
65
|
+
headers,
|
|
66
|
+
...(config.remote.tlsServername ? { servername: config.remote.tlsServername } : {}),
|
|
67
|
+
});
|
|
68
|
+
connectTimeout(upstream, config);
|
|
69
|
+
upstream.setTimeout(config.idleTimeoutMs, () => {
|
|
70
|
+
upstream.destroy(new DesktopBridgeError('bridge_upstream_idle_timeout'));
|
|
71
|
+
});
|
|
72
|
+
const abortUpstream = () => {
|
|
73
|
+
if (!res.writableEnded)
|
|
74
|
+
upstream.destroy(new DesktopBridgeError('bridge_client_disconnected'));
|
|
75
|
+
};
|
|
76
|
+
req.once('aborted', abortUpstream);
|
|
77
|
+
res.once('close', abortUpstream);
|
|
78
|
+
upstream.once('error', finish);
|
|
79
|
+
upstream.once('response', (upstreamResponse) => {
|
|
80
|
+
responseStarted = true;
|
|
81
|
+
try {
|
|
82
|
+
const responseHeaders = rewriteResponseHeaders(upstreamResponse.headers, config, localOrigin);
|
|
83
|
+
res.writeHead(upstreamResponse.statusCode || 502, responseHeaders);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
upstreamResponse.destroy(error instanceof Error ? error : undefined);
|
|
87
|
+
finish(error);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
void pipeline(upstreamResponse, res).then(() => finish(), finish);
|
|
91
|
+
});
|
|
92
|
+
void pipeline(req, upstream).catch((error) => {
|
|
93
|
+
if (!responseStarted)
|
|
94
|
+
finish(error);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
writeHttpBridgeError(res, error);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export * from './security.js';
|
|
3
|
+
export * from './header-policy.js';
|
|
4
|
+
export * from './location-rewrite.js';
|
|
5
|
+
export * from './state.js';
|
|
6
|
+
export * from './http-forward.js';
|
|
7
|
+
export * from './websocket-forward.js';
|
|
8
|
+
export * from './server.js';
|
|
9
|
+
export * from './client.js';
|
|
10
|
+
export * from './launchd.js';
|