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.
Files changed (283) hide show
  1. package/README.md +129 -10
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/dist/cli/codex-app-command.js +1 -1
  5. package/dist/cli/command-manifest-lite.js +4 -1
  6. package/dist/cli/command-registry.js +2 -0
  7. package/dist/cli/install-helpers-codex-lb-chain.js +42 -0
  8. package/dist/cli/install-helpers-codex-lb-config.js +114 -9
  9. package/dist/cli/install-helpers-install-support.js +6 -16
  10. package/dist/cli/install-helpers.js +788 -600
  11. package/dist/cli/router.js +30 -2
  12. package/dist/commands/codex-app.js +37 -1
  13. package/dist/commands/codex-lb.js +491 -300
  14. package/dist/config/skills-manifest.json +57 -71
  15. package/dist/core/align/align-route.js +127 -0
  16. package/dist/core/codex-app/codex-app-fast-ui-repair.js +8 -4
  17. package/dist/core/codex-app/codex-app-restart.js +83 -0
  18. package/dist/core/codex-app/codex-app-ui-state-snapshot.js +8 -4
  19. package/dist/core/codex-app/desktop-routing-snapshot.js +1 -1
  20. package/dist/core/codex-app/menubar/constants.js +5 -0
  21. package/dist/core/codex-app/menubar/generation-transaction-journal.js +1 -3
  22. package/dist/core/codex-app/menubar/installer.js +18 -0
  23. package/dist/core/codex-app/multi-provider-router.js +22 -10
  24. package/dist/core/codex-app/openrouter-activate.js +26 -1
  25. package/dist/core/codex-app.js +118 -26
  26. package/dist/core/codex-control/codex-app-server-runtime-env.js +86 -0
  27. package/dist/core/codex-control/codex-app-server-v2-client.js +78 -8
  28. package/dist/core/codex-control/codex-task-runner.js +2 -2
  29. package/dist/core/codex-lb/capability-runner.js +204 -0
  30. package/dist/core/codex-lb/capability-types.js +1 -0
  31. package/dist/core/codex-lb/codex-lb-env.js +9 -13
  32. package/dist/core/codex-lb/codex-lb-setup.js +45 -8
  33. package/dist/core/codex-lb/codex-lb-tool-catalog.js +52 -74
  34. package/dist/core/codex-lb/codex-lb-tool-output-recovery.js +1 -1
  35. package/dist/core/codex-lb/desktop-auth-invariant.js +185 -0
  36. package/dist/core/codex-lb/desktop-bridge/client.js +34 -0
  37. package/dist/core/codex-lb/desktop-bridge/header-policy.js +106 -0
  38. package/dist/core/codex-lb/desktop-bridge/http-forward.js +101 -0
  39. package/dist/core/codex-lb/desktop-bridge/index.js +10 -0
  40. package/dist/core/codex-lb/desktop-bridge/launchd.js +81 -0
  41. package/dist/core/codex-lb/desktop-bridge/location-rewrite.js +27 -0
  42. package/dist/core/codex-lb/desktop-bridge/security.js +274 -0
  43. package/dist/core/codex-lb/desktop-bridge/server.js +179 -0
  44. package/dist/core/codex-lb/desktop-bridge/state.js +119 -0
  45. package/dist/core/codex-lb/desktop-bridge/types.js +16 -0
  46. package/dist/core/codex-lb/desktop-bridge/websocket-forward.js +151 -0
  47. package/dist/core/codex-lb/desktop-center-credentials.js +189 -0
  48. package/dist/core/codex-lb/desktop-controller.js +1410 -0
  49. package/dist/core/codex-lb/desktop-mode.js +32 -0
  50. package/dist/core/codex-lb/desktop-real-evidence.js +168 -0
  51. package/dist/core/codex-lb/desktop-service.js +613 -0
  52. package/dist/core/codex-lb/legacy-migration.js +575 -0
  53. package/dist/core/codex-lb/migration-receipt.js +246 -0
  54. package/dist/core/codex-lb/probes/auxiliary-surfaces-probe.js +60 -0
  55. package/dist/core/codex-lb/probes/bridge-probe.js +75 -0
  56. package/dist/core/codex-lb/probes/catalog-probe.js +128 -0
  57. package/dist/core/codex-lb/probes/computer-use-probe.js +50 -0
  58. package/dist/core/codex-lb/probes/image-generation-probe.js +47 -0
  59. package/dist/core/codex-lb/probes/probe-evidence.js +39 -0
  60. package/dist/core/codex-lb/probes/voice-realtime-probe.js +44 -0
  61. package/dist/core/codex-lb/trusted-deep-evidence.js +403 -0
  62. package/dist/core/codex-native/core-skill-manifest.js +22 -6
  63. package/dist/core/codex-runtime/codex-desktop-config-policy.js +2 -1
  64. package/dist/core/commands/align-command.js +220 -0
  65. package/dist/core/commands/image-ux-review-command.js +280 -68
  66. package/dist/core/commands/naruto-command.js +257 -8
  67. package/dist/core/commands/ppt-command.js +2 -2
  68. package/dist/core/commands/telegram-command.js +402 -34
  69. package/dist/core/commands/triwiki-command.js +11 -1
  70. package/dist/core/commands/triwiki-graph-command.js +160 -0
  71. package/dist/core/commands/wiki-command.js +100 -20
  72. package/dist/core/doctor/skill-legacy-surface.js +1 -3
  73. package/dist/core/errors/message.js +3 -0
  74. package/dist/core/evidence/evidence-freshness.js +14 -0
  75. package/dist/core/feature-fixtures.js +9 -1
  76. package/dist/core/feature-registry.js +5 -3
  77. package/dist/core/fsx.js +7 -3
  78. package/dist/core/hooks-runtime/codex-commit-hooks-selftest.js +82 -0
  79. package/dist/core/hooks-runtime/context-graph-freshness-preflight.js +113 -0
  80. package/dist/core/hooks-runtime/hook-context.js +15 -2
  81. package/dist/core/hooks-runtime/managed-skill-generation-heal.js +46 -0
  82. package/dist/core/hooks-runtime/naruto-terminal-finalization.js +1 -0
  83. package/dist/core/hooks-runtime/official-subagent-lifecycle.js +1 -1
  84. package/dist/core/hooks-runtime/tool-output-quarantine.js +1 -1
  85. package/dist/core/hooks-runtime.js +33 -89
  86. package/dist/core/image-ux-review/callout-extraction.js +6 -0
  87. package/dist/core/image-ux-review/imagegen-adapter.js +64 -23
  88. package/dist/core/image-ux-review/real-callout-extractor.js +1 -0
  89. package/dist/core/image-ux-review.js +230 -12
  90. package/dist/core/imagegen/codex-lb-imagegen-target.js +21 -7
  91. package/dist/core/imagegen/imagegen-capability.js +10 -4
  92. package/dist/core/imagegen/require-imagegen.js +29 -5
  93. package/dist/core/init/skills.js +25 -13
  94. package/dist/core/init.js +1 -1
  95. package/dist/core/install/installed-package-smoke.js +157 -4
  96. package/dist/core/managed-assets/managed-assets-manifest.js +2 -1
  97. package/dist/core/managed-path-safety.js +8 -10
  98. package/dist/core/mcp-config/backup.js +2 -4
  99. package/dist/core/mcp-config/config-reader.js +2 -4
  100. package/dist/core/mcp-config/scope.js +3 -5
  101. package/dist/core/naruto/context-graph-advisor-scope.js +242 -0
  102. package/dist/core/naruto/context-graph-advisor.js +0 -0
  103. package/dist/core/perf/release-latency-slo.js +12 -6
  104. package/dist/core/pipeline-internals/runtime-core.js +78 -0
  105. package/dist/core/pipeline-internals/runtime-gates.js +5 -2
  106. package/dist/core/policy/latest-version-guidance.js +113 -0
  107. package/dist/core/ppt.js +8 -8
  108. package/dist/core/preflight/parallel-preflight-engine.js +1 -1
  109. package/dist/core/proof/auto-finalize.js +98 -6
  110. package/dist/core/proof/route-proof-gate.js +12 -1
  111. package/dist/core/proof/route-proof-policy.js +8 -8
  112. package/dist/core/release/gate-manifest.js +32 -1
  113. package/dist/core/release/npm-stage-contract.js +21 -0
  114. package/dist/core/release/package-size-budget.js +2 -2
  115. package/dist/core/release/release-gate-contract.js +5 -0
  116. package/dist/core/release/release-proof-io.js +1 -3
  117. package/dist/core/release/release-real-contract.js +5 -4
  118. package/dist/core/release/release-stamp-proof.js +1 -3
  119. package/dist/core/release/stage-publish.js +27 -0
  120. package/dist/core/remote/audit-idempotency.js +50 -18
  121. package/dist/core/remote/codex-public-activity.js +283 -0
  122. package/dist/core/remote/index.js +1 -0
  123. package/dist/core/remote/ssh-worker-client.js +4 -1
  124. package/dist/core/remote/worker.js +206 -27
  125. package/dist/core/research/research-adversarial-review.js +1 -3
  126. package/dist/core/research/research-claim-synthesizer.js +1 -3
  127. package/dist/core/research/research-falsification-runner.js +1 -3
  128. package/dist/core/research/research-source-evidence.js +1 -3
  129. package/dist/core/responses-stream.js +65 -19
  130. package/dist/core/routes/constants.js +1 -1
  131. package/dist/core/routes/design-policy.js +7 -2
  132. package/dist/core/routes/dollar-manifest-lite.js +2 -4
  133. package/dist/core/routes/evidence.js +1 -1
  134. package/dist/core/routes.js +30 -10
  135. package/dist/core/safety/command-contract/contracts.js +2 -0
  136. package/dist/core/safety/command-contract/types.js +1 -1
  137. package/dist/core/search/context-graph-seeds.js +205 -0
  138. package/dist/core/search/context.js +0 -0
  139. package/dist/core/search/provider.js +2 -2
  140. package/dist/core/secret-redaction.js +6 -0
  141. package/dist/core/subagents/naruto-help-contract.js +2 -1
  142. package/dist/core/subagents/naruto-host-credentials.js +161 -0
  143. package/dist/core/subagents/official-subagent-preparation.js +4 -1
  144. package/dist/core/subagents/official-subagent-prompt.js +44 -2
  145. package/dist/core/subagents/official-subagent-runner.js +26 -8
  146. package/dist/core/subagents/triwiki-attention.js +135 -123
  147. package/dist/core/telegram/bot-api.js +85 -10
  148. package/dist/core/telegram/center-snapshot.js +718 -0
  149. package/dist/core/telegram/config.js +158 -31
  150. package/dist/core/telegram/hub.js +288 -33
  151. package/dist/core/telegram/index.js +2 -0
  152. package/dist/core/telegram/ingress-spool.js +419 -0
  153. package/dist/core/telegram/ledgers.js +188 -29
  154. package/dist/core/telegram/messages.js +26 -6
  155. package/dist/core/telegram/owner-lock.js +10 -7
  156. package/dist/core/telegram/runtime-projection.js +38 -3
  157. package/dist/core/telegram/runtime.js +298 -63
  158. package/dist/core/telegram/service.js +137 -8
  159. package/dist/core/telegram/setup.js +291 -73
  160. package/dist/core/text/strings.js +3 -0
  161. package/dist/core/triwiki/code-pack.js +12 -120
  162. package/dist/core/triwiki/context-graph/compiler/cache-key.js +277 -0
  163. package/dist/core/triwiki/context-graph/compiler/extract.js +104 -0
  164. package/dist/core/triwiki/context-graph/compiler/freshness.js +108 -0
  165. package/dist/core/triwiki/context-graph/compiler/index.js +237 -0
  166. package/dist/core/triwiki/context-graph/compiler/merge.js +0 -0
  167. package/dist/core/triwiki/context-graph/compiler/serialize.js +115 -0
  168. package/dist/core/triwiki/context-graph/contracts.js +236 -0
  169. package/dist/core/triwiki/context-graph/extractors/code/builders.js +150 -0
  170. package/dist/core/triwiki/context-graph/extractors/code/declarations.js +161 -0
  171. package/dist/core/triwiki/context-graph/extractors/code/index.js +265 -0
  172. package/dist/core/triwiki/context-graph/extractors/code/inventory.js +237 -0
  173. package/dist/core/triwiki/context-graph/extractors/code/module-graph.js +252 -0
  174. package/dist/core/triwiki/context-graph/extractors/code/modules.js +79 -0
  175. package/dist/core/triwiki/context-graph/extractors/code/references.js +184 -0
  176. package/dist/core/triwiki/context-graph/extractors/code/selection.js +100 -0
  177. package/dist/core/triwiki/context-graph/extractors/code/ts-config.js +161 -0
  178. package/dist/core/triwiki/context-graph/extractors/code/types.js +18 -0
  179. package/dist/core/triwiki/context-graph/extractors/evidence/claims.js +289 -0
  180. package/dist/core/triwiki/context-graph/extractors/evidence/index.js +54 -0
  181. package/dist/core/triwiki/context-graph/extractors/evidence/proof-index.js +213 -0
  182. package/dist/core/triwiki/context-graph/extractors/evidence/proofs.js +266 -0
  183. package/dist/core/triwiki/context-graph/extractors/evidence/redaction.js +177 -0
  184. package/dist/core/triwiki/context-graph/extractors/evidence/shared.js +0 -0
  185. package/dist/core/triwiki/context-graph/extractors/evidence/sources.js +154 -0
  186. package/dist/core/triwiki/context-graph/extractors/index.js +7 -0
  187. package/dist/core/triwiki/context-graph/extractors/topology/commands.js +207 -0
  188. package/dist/core/triwiki/context-graph/extractors/topology/gate-edges.js +201 -0
  189. package/dist/core/triwiki/context-graph/extractors/topology/gates.js +182 -0
  190. package/dist/core/triwiki/context-graph/extractors/topology/globs.js +124 -0
  191. package/dist/core/triwiki/context-graph/extractors/topology/index.js +92 -0
  192. package/dist/core/triwiki/context-graph/extractors/topology/routes.js +135 -0
  193. package/dist/core/triwiki/context-graph/extractors/topology/shared.js +215 -0
  194. package/dist/core/triwiki/context-graph/graph-index.js +143 -0
  195. package/dist/core/triwiki/context-graph/ids.js +0 -0
  196. package/dist/core/triwiki/context-graph/lint/index.js +0 -0
  197. package/dist/core/triwiki/context-graph/lint/rules.js +232 -0
  198. package/dist/core/triwiki/context-graph/lint/warnings.js +93 -0
  199. package/dist/core/triwiki/context-graph/paths.js +116 -0
  200. package/dist/core/triwiki/context-graph/profiles.js +78 -0
  201. package/dist/core/triwiki/context-graph/projections/anchors.js +91 -0
  202. package/dist/core/triwiki/context-graph/projections/attention.js +58 -0
  203. package/dist/core/triwiki/context-graph/projections/code-pack-workspace.js +62 -0
  204. package/dist/core/triwiki/context-graph/projections/code-pack.js +144 -0
  205. package/dist/core/triwiki/context-graph/projections/index.js +7 -0
  206. package/dist/core/triwiki/context-graph/projections/module-view.js +140 -0
  207. package/dist/core/triwiki/context-graph/projections/node-summary.js +118 -0
  208. package/dist/core/triwiki/context-graph/projections/pack-contract.js +6 -0
  209. package/dist/core/triwiki/context-graph/query/explain.js +0 -0
  210. package/dist/core/triwiki/context-graph/query/index.js +178 -0
  211. package/dist/core/triwiki/context-graph/query/load.js +116 -0
  212. package/dist/core/triwiki/context-graph/query/pack.js +117 -0
  213. package/dist/core/triwiki/context-graph/query/rank.js +142 -0
  214. package/dist/core/triwiki/context-graph/query/ranking-config.js +64 -0
  215. package/dist/core/triwiki/context-graph/query/seeds.js +241 -0
  216. package/dist/core/triwiki/context-graph/query/snapshot-cache.js +72 -0
  217. package/dist/core/triwiki/context-graph/query/traverse.js +154 -0
  218. package/dist/core/triwiki/context-graph/query-types.js +29 -0
  219. package/dist/core/triwiki/context-graph/store/compile-lock.js +21 -0
  220. package/dist/core/triwiki/context-graph/store/event-log.js +48 -0
  221. package/dist/core/triwiki/context-graph/store/fragment-cache.js +85 -0
  222. package/dist/core/triwiki/context-graph/store/graph-status.js +85 -0
  223. package/dist/core/triwiki/context-graph/store/snapshot-store.js +155 -0
  224. package/dist/core/triwiki/triwiki-proof-bank-index-repair.js +115 -0
  225. package/dist/core/triwiki/triwiki-proof-bank-index-store.js +290 -0
  226. package/dist/core/triwiki/triwiki-proof-bank-index.js +126 -0
  227. package/dist/core/triwiki/triwiki-proof-bank.js +7 -1
  228. package/dist/core/verification/context-graph-affected.js +271 -0
  229. package/dist/core/version.js +1 -1
  230. package/dist/native/sks-menubar/Sources/AlertFactory.swift +11 -3
  231. package/dist/native/sks-menubar/Sources/AppDelegate.swift +9 -0
  232. package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +25 -3
  233. package/dist/native/sks-menubar/Sources/ControlKit.swift +19 -8
  234. package/dist/native/sks-menubar/Sources/DiagnosticsViewController.swift +1 -2
  235. package/dist/native/sks-menubar/Sources/MCPServersViewController.swift +45 -17
  236. package/dist/native/sks-menubar/Sources/OverviewSummary.swift +162 -0
  237. package/dist/native/sks-menubar/Sources/OverviewViewController.swift +136 -190
  238. package/dist/native/sks-menubar/Sources/ProcessClient.swift +107 -11
  239. package/dist/native/sks-menubar/Sources/ProvidersMultiProvider.swift +5 -1
  240. package/dist/native/sks-menubar/Sources/ProvidersOpenRouter.swift +16 -4
  241. package/dist/native/sks-menubar/Sources/ProvidersRoleModels.swift +4 -1
  242. package/dist/native/sks-menubar/Sources/ProvidersRoutingTruth.swift +131 -0
  243. package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +288 -341
  244. package/dist/native/sks-menubar/Sources/RemoteTelegramCenterState.swift +447 -0
  245. package/dist/native/sks-menubar/Sources/RemoteTelegramRendering.swift +484 -0
  246. package/dist/native/sks-menubar/Sources/RemoteTelegramViewController.swift +364 -115
  247. package/dist/native/sks-menubar/Sources/SettingsViewController.swift +73 -12
  248. package/dist/native/sks-menubar/Sources/SidebarItem.swift +3 -3
  249. package/dist/native/sks-menubar/Sources/StatusItemController.swift +13 -13
  250. package/dist/native/sks-menubar/Sources/TelegramHubSupervisor.swift +469 -0
  251. package/dist/native/sks-menubar/Sources/UpdatesViewController.swift +1 -6
  252. package/dist/native/sks-menubar/Tests/AppRuntimeTestSupport.swift +12 -0
  253. package/dist/native/sks-menubar/Tests/NativeTelegramRegressionRunner.swift +55 -0
  254. package/dist/native/sks-menubar/Tests/ProcessClientTests.swift +33 -0
  255. package/dist/native/sks-menubar/Tests/ProvidersViewControllerTests.swift +142 -0
  256. package/dist/native/sks-menubar/Tests/RemoteTelegramCenterStateTests.swift +56 -0
  257. package/dist/native/sks-menubar/Tests/TelegramHubSupervisorTests.swift +55 -0
  258. package/dist/scripts/codex-lb-catalog-passthrough-check.js +120 -0
  259. package/dist/scripts/codex-lb-desktop-auth-invariant-check.js +169 -0
  260. package/dist/scripts/codex-lb-desktop-bridge-check.js +394 -0
  261. package/dist/scripts/codex-lb-desktop-bridge-latency-check.js +196 -0
  262. package/dist/scripts/codex-lb-desktop-capabilities-check.js +287 -0
  263. package/dist/scripts/codex-lb-desktop-real-evidence-check.js +123 -0
  264. package/dist/scripts/codex-lb-fast-mode-truth-check.js +10 -3
  265. package/dist/scripts/codex-lb-fast-ui-preservation-check.js +81 -40
  266. package/dist/scripts/codex-lb-gpt56-fast-profile-check.js +99 -37
  267. package/dist/scripts/context-graph-check.js +191 -0
  268. package/dist/scripts/doctor-fixes-codex-app-fast-ui-check.js +1 -1
  269. package/dist/scripts/gate-policy-audit-check.js +1 -1
  270. package/dist/scripts/helper-dedup-check.js +0 -2
  271. package/dist/scripts/installed-package-smoke-check.js +4 -1
  272. package/dist/scripts/latest-version-guidance-check.js +74 -0
  273. package/dist/scripts/release-metadata-check.js +21 -4
  274. package/dist/scripts/release-real-check.js +7 -0
  275. package/dist/scripts/ux-review-imagegen-blackbox-check.js +25 -1
  276. package/dist/scripts/ux-review-run-wires-imagegen-check.js +9 -1
  277. package/package.json +8 -3
  278. package/release-gates.v2.json +176 -1
  279. package/runtime-required-scripts.json +30 -0
  280. package/schemas/triwiki/context-graph.schema.json +119 -0
  281. package/dist/cli/install-helpers-codex-lb-selftest-chain.js +0 -112
  282. package/dist/cli/install-helpers-codex-lb-selftest.js +0 -481
  283. package/dist/core/triwiki/code-index-scanner.js +0 -309
@@ -0,0 +1,81 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import fsp from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { DesktopBridgeError } from './types.js';
6
+ export const DESKTOP_BRIDGE_LAUNCHD_LABEL = 'com.sneakoscope.codex-lb-desktop-bridge';
7
+ export function desktopBridgeLaunchdPlistPath(home = os.homedir()) {
8
+ return path.join(home, 'Library', 'LaunchAgents', `${DESKTOP_BRIDGE_LAUNCHD_LABEL}.plist`);
9
+ }
10
+ function xml(value) {
11
+ return value
12
+ .replaceAll('&', '&')
13
+ .replaceAll('<', '&lt;')
14
+ .replaceAll('>', '&gt;')
15
+ .replaceAll('"', '&quot;')
16
+ .replaceAll("'", '&apos;');
17
+ }
18
+ function assertSafeLaunchdValue(value) {
19
+ if (!value || /[\0\r\n]/.test(value))
20
+ throw new DesktopBridgeError('bridge_launchd_value_invalid');
21
+ }
22
+ export function renderDesktopBridgeLaunchdPlist(options) {
23
+ assertSafeLaunchdValue(options.executablePath);
24
+ if (!path.isAbsolute(options.executablePath))
25
+ throw new DesktopBridgeError('bridge_launchd_executable_not_absolute');
26
+ for (const argument of options.arguments) {
27
+ assertSafeLaunchdValue(argument);
28
+ if (/^(?:--?(?:(?:api|gateway|access|auth|bearer)[-_]?(?:key|token)|authorization|x-codex-lb-api-key)(?:=|$)|gatewayKey(?:=|$)|(?:CODEX_LB_API_KEY|SKS_CODEX_LB_API_KEY|AUTHORIZATION|ACCESS_TOKEN)=)/i
29
+ .test(argument)) {
30
+ throw new DesktopBridgeError('bridge_launchd_secret_argument_forbidden');
31
+ }
32
+ }
33
+ for (const logPath of [options.stdoutPath, options.stderrPath]) {
34
+ assertSafeLaunchdValue(logPath);
35
+ if (!path.isAbsolute(logPath))
36
+ throw new DesktopBridgeError('bridge_launchd_log_path_not_absolute');
37
+ }
38
+ const programArguments = [options.executablePath, ...options.arguments]
39
+ .map((value) => ` <string>${xml(value)}</string>`)
40
+ .join('\n');
41
+ return `<?xml version="1.0" encoding="UTF-8"?>
42
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
43
+ <plist version="1.0">
44
+ <dict>
45
+ <key>Label</key>
46
+ <string>${DESKTOP_BRIDGE_LAUNCHD_LABEL}</string>
47
+ <key>ProgramArguments</key>
48
+ <array>
49
+ ${programArguments}
50
+ </array>
51
+ <key>RunAtLoad</key>
52
+ <true/>
53
+ <key>KeepAlive</key>
54
+ <dict>
55
+ <key>SuccessfulExit</key>
56
+ <false/>
57
+ </dict>
58
+ <key>ProcessType</key>
59
+ <string>Interactive</string>
60
+ <key>StandardOutPath</key>
61
+ <string>${xml(options.stdoutPath)}</string>
62
+ <key>StandardErrorPath</key>
63
+ <string>${xml(options.stderrPath)}</string>
64
+ </dict>
65
+ </plist>
66
+ `;
67
+ }
68
+ export async function writeDesktopBridgeLaunchdPlist(file, options) {
69
+ const contents = renderDesktopBridgeLaunchdPlist(options);
70
+ const directory = path.dirname(file);
71
+ await fsp.mkdir(directory, { recursive: true, mode: 0o700 });
72
+ const temp = path.join(directory, `.${path.basename(file)}.${process.pid}.${randomUUID()}.tmp`);
73
+ try {
74
+ await fsp.writeFile(temp, contents, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
75
+ await fsp.rename(temp, file);
76
+ await fsp.chmod(file, 0o600);
77
+ }
78
+ finally {
79
+ await fsp.rm(temp, { force: true }).catch(() => undefined);
80
+ }
81
+ }
@@ -0,0 +1,27 @@
1
+ import { DesktopBridgeError } from './types.js';
2
+ export function rewriteLocationHeader(value, remoteBaseUrl, localOrigin) {
3
+ const remote = new URL(remoteBaseUrl);
4
+ const location = new URL(value, remote.origin);
5
+ const remoteWebSocketProtocol = remote.protocol === 'https:' ? 'wss:' : 'ws:';
6
+ const allowedProtocol = location.protocol === remote.protocol || location.protocol === remoteWebSocketProtocol;
7
+ const remotePort = remote.port || (remote.protocol === 'https:' ? '443' : '80');
8
+ const locationPort = location.port
9
+ || (location.protocol === 'https:' || location.protocol === 'wss:' ? '443' : '80');
10
+ if (!allowedProtocol
11
+ || location.hostname.toLowerCase() !== remote.hostname.toLowerCase()
12
+ || locationPort !== remotePort) {
13
+ throw new DesktopBridgeError('bridge_location_origin_forbidden');
14
+ }
15
+ const local = new URL(localOrigin);
16
+ local.pathname = location.pathname;
17
+ local.search = location.search;
18
+ local.hash = '';
19
+ if (location.protocol === 'wss:' || location.protocol === 'ws:')
20
+ local.protocol = 'ws:';
21
+ else if (location.protocol === 'https:' || location.protocol === 'http:')
22
+ local.protocol = 'http:';
23
+ else {
24
+ throw new DesktopBridgeError('bridge_location_protocol_forbidden');
25
+ }
26
+ return local.toString();
27
+ }
@@ -0,0 +1,274 @@
1
+ import dns from 'node:dns/promises';
2
+ import net from 'node:net';
3
+ import { DesktopBridgeError } from './types.js';
4
+ const MIN_HIGH_PORT = 49_152;
5
+ const MAX_PORT = 65_535;
6
+ const FORBIDDEN_REMOTE_ADDRESSES = new net.BlockList();
7
+ FORBIDDEN_REMOTE_ADDRESSES.addSubnet('0.0.0.0', 8, 'ipv4');
8
+ FORBIDDEN_REMOTE_ADDRESSES.addSubnet('10.0.0.0', 8, 'ipv4');
9
+ FORBIDDEN_REMOTE_ADDRESSES.addSubnet('100.64.0.0', 10, 'ipv4');
10
+ FORBIDDEN_REMOTE_ADDRESSES.addSubnet('127.0.0.0', 8, 'ipv4');
11
+ FORBIDDEN_REMOTE_ADDRESSES.addSubnet('169.254.0.0', 16, 'ipv4');
12
+ FORBIDDEN_REMOTE_ADDRESSES.addSubnet('172.16.0.0', 12, 'ipv4');
13
+ FORBIDDEN_REMOTE_ADDRESSES.addSubnet('192.0.0.0', 24, 'ipv4');
14
+ FORBIDDEN_REMOTE_ADDRESSES.addSubnet('192.0.2.0', 24, 'ipv4');
15
+ FORBIDDEN_REMOTE_ADDRESSES.addSubnet('192.168.0.0', 16, 'ipv4');
16
+ FORBIDDEN_REMOTE_ADDRESSES.addSubnet('198.18.0.0', 15, 'ipv4');
17
+ FORBIDDEN_REMOTE_ADDRESSES.addSubnet('198.51.100.0', 24, 'ipv4');
18
+ FORBIDDEN_REMOTE_ADDRESSES.addSubnet('203.0.113.0', 24, 'ipv4');
19
+ FORBIDDEN_REMOTE_ADDRESSES.addSubnet('224.0.0.0', 4, 'ipv4');
20
+ FORBIDDEN_REMOTE_ADDRESSES.addSubnet('240.0.0.0', 4, 'ipv4');
21
+ FORBIDDEN_REMOTE_ADDRESSES.addSubnet('::', 128, 'ipv6');
22
+ FORBIDDEN_REMOTE_ADDRESSES.addSubnet('::1', 128, 'ipv6');
23
+ FORBIDDEN_REMOTE_ADDRESSES.addSubnet('fc00::', 7, 'ipv6');
24
+ FORBIDDEN_REMOTE_ADDRESSES.addSubnet('fe80::', 10, 'ipv6');
25
+ FORBIDDEN_REMOTE_ADDRESSES.addSubnet('ff00::', 8, 'ipv6');
26
+ FORBIDDEN_REMOTE_ADDRESSES.addSubnet('2001:db8::', 32, 'ipv6');
27
+ export function assertLoopbackPeer(address) {
28
+ const normalized = String(address || '').replace(/^::ffff:/i, '');
29
+ if (!net.isIP(normalized))
30
+ throw new DesktopBridgeError('bridge_peer_not_ip');
31
+ if (normalized !== '127.0.0.1' && normalized !== '::1') {
32
+ throw new DesktopBridgeError('bridge_non_loopback_peer');
33
+ }
34
+ }
35
+ export function assertLoopbackListenHost(host) {
36
+ if (host !== '127.0.0.1' && host !== '::1') {
37
+ throw new DesktopBridgeError('bridge_listen_host_not_loopback');
38
+ }
39
+ }
40
+ export function assertAllowedPath(pathname, prefixes) {
41
+ if (!pathname.startsWith('/') || pathname.includes('\\') || /%(?:2f|5c)/i.test(pathname)) {
42
+ throw new DesktopBridgeError('bridge_path_invalid');
43
+ }
44
+ const allowed = prefixes.some((prefix) => {
45
+ if (!prefix.startsWith('/'))
46
+ return false;
47
+ if (prefix.endsWith('/'))
48
+ return pathname.startsWith(prefix);
49
+ return pathname === prefix || pathname.startsWith(`${prefix}/`);
50
+ });
51
+ if (!allowed)
52
+ throw new DesktopBridgeError('bridge_path_not_allowed');
53
+ }
54
+ function headerValues(value) {
55
+ return (Array.isArray(value) ? value : value === undefined ? [] : [value])
56
+ .map((entry) => entry.trim())
57
+ .filter(Boolean);
58
+ }
59
+ function comparableOrigin(value, referer) {
60
+ if (value === 'null')
61
+ return 'null';
62
+ try {
63
+ const parsed = new URL(value);
64
+ if (referer) {
65
+ return parsed.origin !== 'null' ? parsed.origin : `${parsed.protocol}//${parsed.host}`;
66
+ }
67
+ if ((parsed.pathname !== '/' && parsed.pathname !== '') || parsed.search || parsed.hash) {
68
+ throw new Error('origin_has_path');
69
+ }
70
+ return parsed.origin !== 'null' ? parsed.origin : `${parsed.protocol}//${parsed.host}`;
71
+ }
72
+ catch {
73
+ throw new DesktopBridgeError('bridge_origin_invalid');
74
+ }
75
+ }
76
+ export function normalizeAllowedOrigin(value) {
77
+ return comparableOrigin(String(value || '').trim(), false);
78
+ }
79
+ export function assertAllowedOrigin(headers, allowedOrigins) {
80
+ const origins = headerValues(headers.origin);
81
+ const referers = headerValues(headers.referer);
82
+ if (origins.length === 0 && referers.length === 0)
83
+ return;
84
+ if (origins.length > 1 || referers.length > 1)
85
+ throw new DesktopBridgeError('bridge_origin_forbidden');
86
+ const allowed = new Set(allowedOrigins.map(normalizeAllowedOrigin));
87
+ for (const origin of origins) {
88
+ if (!allowed.has(comparableOrigin(origin, false)))
89
+ throw new DesktopBridgeError('bridge_origin_forbidden');
90
+ }
91
+ for (const referer of referers) {
92
+ if (!allowed.has(comparableOrigin(referer, true)))
93
+ throw new DesktopBridgeError('bridge_origin_forbidden');
94
+ }
95
+ }
96
+ export function assertWebSocketUpgrade(headers, method) {
97
+ const connection = headerValues(headers.connection).join(',').toLowerCase().split(',').map((part) => part.trim());
98
+ const upgrade = headerValues(headers.upgrade);
99
+ const key = headerValues(headers['sec-websocket-key']);
100
+ const version = headerValues(headers['sec-websocket-version']);
101
+ if (method !== 'GET'
102
+ || !connection.includes('upgrade')
103
+ || upgrade.length !== 1
104
+ || upgrade[0]?.toLowerCase() !== 'websocket'
105
+ || key.length !== 1
106
+ || version.length !== 1) {
107
+ throw new DesktopBridgeError('bridge_websocket_upgrade_invalid');
108
+ }
109
+ }
110
+ function stripIpv6Brackets(hostname) {
111
+ return hostname.replace(/^\[/, '').replace(/\]$/, '');
112
+ }
113
+ function isLoopbackAddress(address) {
114
+ const normalized = address.replace(/^::ffff:/i, '');
115
+ if (normalized === '::1')
116
+ return true;
117
+ if (net.isIP(normalized) !== 4)
118
+ return false;
119
+ return Number(normalized.split('.')[0]) === 127;
120
+ }
121
+ function isExplicitLoopbackHostname(hostname) {
122
+ const normalized = stripIpv6Brackets(hostname).replace(/\.$/, '').toLowerCase();
123
+ return normalized === 'localhost' || isLoopbackAddress(normalized);
124
+ }
125
+ function isForbiddenRemoteAddress(address, family) {
126
+ const dottedMapped = address.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/i)?.[1];
127
+ const hexMapped = address.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i);
128
+ const mapped = dottedMapped || (hexMapped
129
+ ? [
130
+ Number.parseInt(hexMapped[1] || '0', 16) >> 8,
131
+ Number.parseInt(hexMapped[1] || '0', 16) & 0xff,
132
+ Number.parseInt(hexMapped[2] || '0', 16) >> 8,
133
+ Number.parseInt(hexMapped[2] || '0', 16) & 0xff,
134
+ ].join('.')
135
+ : undefined);
136
+ if (mapped)
137
+ return FORBIDDEN_REMOTE_ADDRESSES.check(mapped, 'ipv4');
138
+ return FORBIDDEN_REMOTE_ADDRESSES.check(address, family === 4 ? 'ipv4' : 'ipv6');
139
+ }
140
+ export function validateDesktopBridgeConfig(config) {
141
+ assertLoopbackListenHost(config.listenHost);
142
+ if (!Number.isInteger(config.listenPort) || config.listenPort < MIN_HIGH_PORT || config.listenPort > MAX_PORT) {
143
+ throw new DesktopBridgeError('bridge_listen_port_not_high');
144
+ }
145
+ if (!config.gatewayKey || /[\r\n\0]/.test(config.gatewayKey)) {
146
+ throw new DesktopBridgeError('bridge_gateway_key_invalid');
147
+ }
148
+ if (config.gatewayAuthTransport !== 'x-codex-lb-api-key' && config.gatewayAuthTransport !== 'authorization-bearer-compat') {
149
+ throw new DesktopBridgeError('bridge_gateway_auth_transport_invalid');
150
+ }
151
+ if (!Array.isArray(config.allowedPathPrefixes) || config.allowedPathPrefixes.length === 0) {
152
+ throw new DesktopBridgeError('bridge_path_allowlist_empty');
153
+ }
154
+ if (!Number.isFinite(config.connectTimeoutMs) || config.connectTimeoutMs < 100 || config.connectTimeoutMs > 120_000) {
155
+ throw new DesktopBridgeError('bridge_connect_timeout_invalid');
156
+ }
157
+ if (!Number.isFinite(config.idleTimeoutMs) || config.idleTimeoutMs < 1_000 || config.idleTimeoutMs > 86_400_000) {
158
+ throw new DesktopBridgeError('bridge_idle_timeout_invalid');
159
+ }
160
+ for (const prefix of config.allowedPathPrefixes) {
161
+ if (!prefix.startsWith('/') || prefix.includes('?') || prefix.includes('#') || prefix.includes('\\')) {
162
+ throw new DesktopBridgeError('bridge_path_allowlist_invalid');
163
+ }
164
+ }
165
+ for (const origin of config.allowedOrigins)
166
+ normalizeAllowedOrigin(origin);
167
+ let remote;
168
+ try {
169
+ remote = new URL(config.remoteBaseUrl);
170
+ }
171
+ catch {
172
+ throw new DesktopBridgeError('bridge_remote_url_invalid');
173
+ }
174
+ if (remote.username || remote.password)
175
+ throw new DesktopBridgeError('bridge_remote_userinfo_forbidden');
176
+ if (remote.hash || remote.search)
177
+ throw new DesktopBridgeError('bridge_remote_base_url_query_forbidden');
178
+ if (remote.protocol !== 'https:' && !(remote.protocol === 'http:' && isExplicitLoopbackHostname(remote.hostname))) {
179
+ throw new DesktopBridgeError('bridge_remote_transport_forbidden');
180
+ }
181
+ return remote;
182
+ }
183
+ const defaultLookup = async (hostname) => {
184
+ const rows = await dns.lookup(hostname, { all: true, verbatim: true });
185
+ return rows.map((row) => {
186
+ if (row.family !== 4 && row.family !== 6)
187
+ throw new DesktopBridgeError('bridge_remote_dns_invalid');
188
+ return { address: row.address, family: row.family };
189
+ });
190
+ };
191
+ export async function prepareDesktopBridgeConfig(config, lookup = defaultLookup) {
192
+ const remote = validateDesktopBridgeConfig(config);
193
+ const hostname = stripIpv6Brackets(remote.hostname);
194
+ const literalFamily = net.isIP(hostname);
195
+ let addresses;
196
+ try {
197
+ addresses = literalFamily
198
+ ? [{ address: hostname, family: literalFamily }]
199
+ : await lookup(hostname);
200
+ }
201
+ catch (error) {
202
+ throw new DesktopBridgeError('bridge_remote_dns_failed', { cause: error });
203
+ }
204
+ if (addresses.length === 0)
205
+ throw new DesktopBridgeError('bridge_remote_dns_empty');
206
+ if (addresses.some((row) => net.isIP(row.address) !== row.family)) {
207
+ throw new DesktopBridgeError('bridge_remote_dns_invalid');
208
+ }
209
+ const explicitLoopback = isExplicitLoopbackHostname(hostname);
210
+ if (explicitLoopback) {
211
+ if (addresses.some((row) => !isLoopbackAddress(row.address))) {
212
+ throw new DesktopBridgeError('bridge_remote_dns_rebinding_blocked');
213
+ }
214
+ }
215
+ else if (addresses.some((row) => isForbiddenRemoteAddress(row.address, row.family))) {
216
+ throw new DesktopBridgeError('bridge_remote_dns_private_address');
217
+ }
218
+ const selected = addresses[0];
219
+ if (!selected)
220
+ throw new DesktopBridgeError('bridge_remote_dns_empty');
221
+ const port = Number(remote.port || (remote.protocol === 'https:' ? 443 : 80));
222
+ const remoteTarget = {
223
+ baseUrl: remote.toString().replace(/\/$/, ''),
224
+ origin: remote.origin,
225
+ hostname,
226
+ port,
227
+ secure: remote.protocol === 'https:',
228
+ address: selected.address,
229
+ family: selected.family,
230
+ ...(literalFamily ? {} : { tlsServername: hostname }),
231
+ };
232
+ return { ...config, remoteBaseUrl: remoteTarget.baseUrl, remote: remoteTarget };
233
+ }
234
+ export function validatePreparedDesktopBridgeConfig(config) {
235
+ const remoteUrl = validateDesktopBridgeConfig(config);
236
+ const expectedHostname = stripIpv6Brackets(remoteUrl.hostname);
237
+ const expectedPort = Number(remoteUrl.port || (remoteUrl.protocol === 'https:' ? 443 : 80));
238
+ if (config.remote.origin !== remoteUrl.origin
239
+ || config.remote.hostname !== expectedHostname
240
+ || config.remote.port !== expectedPort
241
+ || config.remote.secure !== (remoteUrl.protocol === 'https:')
242
+ || net.isIP(config.remote.address) !== config.remote.family) {
243
+ throw new DesktopBridgeError('bridge_prepared_config_invalid');
244
+ }
245
+ if (isExplicitLoopbackHostname(expectedHostname)) {
246
+ if (!isLoopbackAddress(config.remote.address)) {
247
+ throw new DesktopBridgeError('bridge_remote_dns_rebinding_blocked');
248
+ }
249
+ }
250
+ else if (isForbiddenRemoteAddress(config.remote.address, config.remote.family)) {
251
+ throw new DesktopBridgeError('bridge_remote_dns_private_address');
252
+ }
253
+ }
254
+ export function resolveDesktopBridgeTarget(rawRequestUrl, remote) {
255
+ const raw = String(rawRequestUrl || '/');
256
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(raw) || raw.startsWith('//') || /[\r\n\0]/.test(raw)) {
257
+ throw new DesktopBridgeError('bridge_request_target_invalid');
258
+ }
259
+ let inbound;
260
+ try {
261
+ inbound = new URL(raw, 'http://bridge.invalid');
262
+ }
263
+ catch {
264
+ throw new DesktopBridgeError('bridge_request_target_invalid');
265
+ }
266
+ return new URL(`${inbound.pathname}${inbound.search}`, remote.origin);
267
+ }
268
+ export function safeBridgeErrorCode(error) {
269
+ if (error instanceof DesktopBridgeError)
270
+ return error.code;
271
+ if (error instanceof Error && /^bridge_[a-z0-9_]+$/.test(error.message))
272
+ return error.message;
273
+ return 'bridge_upstream_unavailable';
274
+ }
@@ -0,0 +1,179 @@
1
+ import { randomInt } from 'node:crypto';
2
+ import http, {} from 'node:http';
3
+ import net, {} from 'node:net';
4
+ import { forwardHttp } from './http-forward.js';
5
+ import { assertAllowedOrigin, assertAllowedPath, assertLoopbackPeer, assertLoopbackListenHost, assertWebSocketUpgrade, prepareDesktopBridgeConfig, safeBridgeErrorCode, validatePreparedDesktopBridgeConfig, } from './security.js';
6
+ import { createDesktopBridgePublicState, desktopBridgeStatePath, removeDesktopBridgeStateIfOwned, writeDesktopBridgeState, } from './state.js';
7
+ import { DesktopBridgeError } from './types.js';
8
+ import { forwardWebSocket } from './websocket-forward.js';
9
+ function pathnameFromRequest(req) {
10
+ try {
11
+ return new URL(req.url || '/', 'http://bridge.invalid').pathname;
12
+ }
13
+ catch {
14
+ throw new DesktopBridgeError('bridge_request_target_invalid');
15
+ }
16
+ }
17
+ function rejectionStatus(code) {
18
+ if (code === 'bridge_path_not_allowed')
19
+ return 404;
20
+ if (code.includes('origin') || code.includes('peer') || code.includes('loopback'))
21
+ return 403;
22
+ return 400;
23
+ }
24
+ function writeBridgeRejection(res, error) {
25
+ if (res.headersSent) {
26
+ res.destroy(error instanceof Error ? error : undefined);
27
+ return;
28
+ }
29
+ const code = safeBridgeErrorCode(error);
30
+ res.writeHead(rejectionStatus(code), {
31
+ 'content-type': 'application/json',
32
+ 'cache-control': 'no-store',
33
+ connection: 'close',
34
+ });
35
+ res.end(JSON.stringify({ error: { type: 'sks_bridge_rejection', code, message: code } }));
36
+ }
37
+ function writeUpgradeRejection(socket, error) {
38
+ if (socket.destroyed)
39
+ return;
40
+ const code = safeBridgeErrorCode(error);
41
+ const status = rejectionStatus(code);
42
+ socket.end(`HTTP/1.1 ${status} ${status === 404 ? 'Not Found' : status === 403 ? 'Forbidden' : 'Bad Request'}\r\n`
43
+ + 'Content-Type: application/json\r\n'
44
+ + 'Cache-Control: no-store\r\n'
45
+ + 'Connection: close\r\n'
46
+ + '\r\n'
47
+ + JSON.stringify({ error: { type: 'sks_bridge_rejection', code, message: code } }));
48
+ }
49
+ async function listenExact(server, host, port) {
50
+ await new Promise((resolve, reject) => {
51
+ const onError = (error) => {
52
+ server.off('listening', onListening);
53
+ if (error.code === 'EADDRINUSE')
54
+ reject(new DesktopBridgeError('bridge_port_conflict', { cause: error }));
55
+ else
56
+ reject(error);
57
+ };
58
+ const onListening = () => {
59
+ server.off('error', onError);
60
+ resolve();
61
+ };
62
+ server.once('error', onError);
63
+ server.once('listening', onListening);
64
+ server.listen({ host, port, exclusive: true });
65
+ });
66
+ }
67
+ async function closeServer(server, sockets) {
68
+ for (const socket of sockets)
69
+ socket.destroy();
70
+ if (!server.listening)
71
+ return;
72
+ await new Promise((resolve, reject) => {
73
+ server.close((error) => {
74
+ if (error)
75
+ reject(error);
76
+ else
77
+ resolve();
78
+ });
79
+ });
80
+ }
81
+ export async function selectAvailableDesktopBridgePort(host, attempts = 64) {
82
+ assertLoopbackListenHost(host);
83
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
84
+ const port = randomInt(49_152, 65_536);
85
+ const probe = net.createServer();
86
+ try {
87
+ await listenExact(probe, host, port);
88
+ await closeServer(probe, new Set());
89
+ return port;
90
+ }
91
+ catch (error) {
92
+ await closeServer(probe, new Set()).catch(() => undefined);
93
+ if (error instanceof DesktopBridgeError && error.code === 'bridge_port_conflict')
94
+ continue;
95
+ throw error;
96
+ }
97
+ }
98
+ throw new DesktopBridgeError('bridge_no_available_high_port');
99
+ }
100
+ export const preflightDesktopBridge = prepareDesktopBridgeConfig;
101
+ export async function startPreparedDesktopBridge(input, options = {}) {
102
+ validatePreparedDesktopBridgeConfig(input);
103
+ const sockets = new Set();
104
+ const server = http.createServer((req, res) => {
105
+ void (async () => {
106
+ try {
107
+ assertLoopbackPeer(req.socket.remoteAddress);
108
+ assertAllowedOrigin(req.headers, input.allowedOrigins);
109
+ assertAllowedPath(pathnameFromRequest(req), input.allowedPathPrefixes);
110
+ await forwardHttp(req, res, input);
111
+ }
112
+ catch (error) {
113
+ req.resume();
114
+ writeBridgeRejection(res, error);
115
+ }
116
+ })();
117
+ });
118
+ server.requestTimeout = 0;
119
+ server.headersTimeout = Math.max(5_000, input.connectTimeoutMs);
120
+ server.keepAliveTimeout = Math.min(input.idleTimeoutMs, 60_000);
121
+ server.on('connection', (socket) => {
122
+ sockets.add(socket);
123
+ socket.once('close', () => sockets.delete(socket));
124
+ });
125
+ server.on('clientError', (_error, socket) => {
126
+ if (!socket.destroyed)
127
+ socket.end('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
128
+ });
129
+ server.on('upgrade', (req, socket, head) => {
130
+ try {
131
+ assertLoopbackPeer(req.socket.remoteAddress);
132
+ assertAllowedOrigin(req.headers, input.allowedOrigins);
133
+ assertAllowedPath(pathnameFromRequest(req), input.allowedPathPrefixes);
134
+ assertWebSocketUpgrade(req.headers, req.method);
135
+ forwardWebSocket(req, socket, head, input);
136
+ }
137
+ catch (error) {
138
+ writeUpgradeRejection(socket, error);
139
+ }
140
+ });
141
+ await listenExact(server, input.listenHost, input.listenPort);
142
+ const state = createDesktopBridgePublicState(input, {
143
+ ...(options.pid === undefined ? {} : { pid: options.pid }),
144
+ ...(options.now === undefined ? {} : { now: options.now }),
145
+ });
146
+ const shouldWriteState = options.writeState !== false;
147
+ const statePath = shouldWriteState ? (options.statePath || desktopBridgeStatePath()) : null;
148
+ try {
149
+ if (statePath)
150
+ await writeDesktopBridgeState(statePath, state);
151
+ }
152
+ catch (error) {
153
+ await closeServer(server, sockets);
154
+ throw error;
155
+ }
156
+ let stopped = false;
157
+ const handle = {
158
+ server,
159
+ state,
160
+ statePath,
161
+ sockets,
162
+ async stop() {
163
+ if (stopped)
164
+ return;
165
+ stopped = true;
166
+ await closeServer(server, sockets);
167
+ if (statePath)
168
+ await removeDesktopBridgeStateIfOwned(statePath, state);
169
+ },
170
+ };
171
+ return handle;
172
+ }
173
+ export async function startDesktopBridge(input, options = {}) {
174
+ const prepared = await preflightDesktopBridge(input);
175
+ return startPreparedDesktopBridge(prepared, options);
176
+ }
177
+ export async function stopDesktopBridge(handle) {
178
+ await handle.stop();
179
+ }
@@ -0,0 +1,119 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import fsp from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { DESKTOP_BRIDGE_STATE_SCHEMA, DesktopBridgeError } from './types.js';
6
+ export function desktopBridgeStatePath(home = os.homedir()) {
7
+ return path.join(home, '.codex', 'sks', 'codex-lb-desktop-bridge.json');
8
+ }
9
+ export function sha256Hex(value) {
10
+ return createHash('sha256').update(value).digest('hex');
11
+ }
12
+ export function desktopBridgeListenOrigin(config) {
13
+ const host = config.listenHost === '::1' ? '[::1]' : config.listenHost;
14
+ return `http://${host}:${config.listenPort}`;
15
+ }
16
+ export function desktopBridgeConfigGeneration(config) {
17
+ const stable = JSON.stringify({
18
+ listenHost: config.listenHost,
19
+ listenPort: config.listenPort,
20
+ remoteBaseUrl: new URL(config.remoteBaseUrl).toString().replace(/\/$/, ''),
21
+ gatewayKeySha256: sha256Hex(config.gatewayKey),
22
+ gatewayAuthTransport: config.gatewayAuthTransport,
23
+ allowedPathPrefixes: [...config.allowedPathPrefixes],
24
+ allowedOrigins: [...config.allowedOrigins],
25
+ connectTimeoutMs: config.connectTimeoutMs,
26
+ idleTimeoutMs: config.idleTimeoutMs,
27
+ });
28
+ return sha256Hex(stable);
29
+ }
30
+ export function createDesktopBridgePublicState(config, options = {}) {
31
+ const listenOrigin = desktopBridgeListenOrigin(config);
32
+ const remote = new URL(config.remoteBaseUrl);
33
+ return {
34
+ schema: DESKTOP_BRIDGE_STATE_SCHEMA,
35
+ pid: options.pid ?? process.pid,
36
+ started_at: (options.now ?? new Date()).toISOString(),
37
+ listen_origin: listenOrigin,
38
+ codex_base_url: `${listenOrigin}/backend-api/codex`,
39
+ remote_origin_sha256: sha256Hex(remote.origin),
40
+ gateway_key_sha256: sha256Hex(config.gatewayKey),
41
+ gateway_auth_transport: config.gatewayAuthTransport,
42
+ config_generation: desktopBridgeConfigGeneration(config),
43
+ };
44
+ }
45
+ export function isDesktopBridgePublicState(value) {
46
+ if (!value || typeof value !== 'object')
47
+ return false;
48
+ const row = value;
49
+ return row.schema === DESKTOP_BRIDGE_STATE_SCHEMA
50
+ && Number.isInteger(row.pid)
51
+ && Number(row.pid) > 0
52
+ && typeof row.started_at === 'string'
53
+ && Number.isFinite(Date.parse(row.started_at))
54
+ && typeof row.listen_origin === 'string'
55
+ && /^http:\/\/(?:127\.0\.0\.1|\[::1\]):\d+$/.test(row.listen_origin)
56
+ && typeof row.codex_base_url === 'string'
57
+ && row.codex_base_url === `${row.listen_origin}/backend-api/codex`
58
+ && typeof row.remote_origin_sha256 === 'string'
59
+ && /^[a-f0-9]{64}$/.test(row.remote_origin_sha256)
60
+ && typeof row.gateway_key_sha256 === 'string'
61
+ && /^[a-f0-9]{64}$/.test(row.gateway_key_sha256)
62
+ && (row.gateway_auth_transport === 'x-codex-lb-api-key' || row.gateway_auth_transport === 'authorization-bearer-compat')
63
+ && typeof row.config_generation === 'string'
64
+ && /^[a-f0-9]{64}$/.test(row.config_generation);
65
+ }
66
+ export async function writeDesktopBridgeState(file, state) {
67
+ if (!isDesktopBridgePublicState(state))
68
+ throw new DesktopBridgeError('bridge_state_invalid');
69
+ const directory = path.dirname(file);
70
+ await fsp.mkdir(directory, { recursive: true, mode: 0o700 });
71
+ const temp = path.join(directory, `.${path.basename(file)}.${process.pid}.${randomUUID()}.tmp`);
72
+ try {
73
+ await fsp.writeFile(temp, `${JSON.stringify(state, null, 2)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
74
+ await fsp.chmod(temp, 0o600);
75
+ await fsp.rename(temp, file);
76
+ await fsp.chmod(file, 0o600);
77
+ }
78
+ finally {
79
+ await fsp.rm(temp, { force: true }).catch(() => undefined);
80
+ }
81
+ }
82
+ export async function readDesktopBridgeState(file) {
83
+ let stat;
84
+ try {
85
+ stat = await fsp.lstat(file);
86
+ }
87
+ catch (error) {
88
+ if (error.code === 'ENOENT')
89
+ return null;
90
+ throw error;
91
+ }
92
+ if (!stat.isFile())
93
+ throw new DesktopBridgeError('bridge_state_not_file');
94
+ if ((stat.mode & 0o077) !== 0)
95
+ throw new DesktopBridgeError('bridge_state_permissions_unsafe');
96
+ const raw = await fsp.readFile(file, 'utf8');
97
+ if (raw.length > 64 * 1024)
98
+ throw new DesktopBridgeError('bridge_state_too_large');
99
+ let value;
100
+ try {
101
+ value = JSON.parse(raw);
102
+ }
103
+ catch {
104
+ throw new DesktopBridgeError('bridge_state_invalid_json');
105
+ }
106
+ if (!isDesktopBridgePublicState(value))
107
+ throw new DesktopBridgeError('bridge_state_invalid');
108
+ return value;
109
+ }
110
+ export async function removeDesktopBridgeStateIfOwned(file, expected) {
111
+ const current = await readDesktopBridgeState(file).catch(() => null);
112
+ if (!current || current.pid !== expected.pid || current.config_generation !== expected.config_generation)
113
+ return false;
114
+ await fsp.unlink(file).catch((error) => {
115
+ if (error.code !== 'ENOENT')
116
+ throw error;
117
+ });
118
+ return true;
119
+ }