@oar-sh/oar 0.9.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 (293) hide show
  1. package/.github/extensions/web-relay/README.md +190 -0
  2. package/.github/extensions/web-relay/extension.mjs +972 -0
  3. package/.github/extensions/web-relay/model-api/model-switching.mjs +338 -0
  4. package/.github/extensions/web-relay/polling/heartbeat.mjs +55 -0
  5. package/.github/extensions/web-relay/polling/polling-loop.mjs +1105 -0
  6. package/.github/extensions/web-relay/runtime/api-client.mjs +34 -0
  7. package/.github/extensions/web-relay/runtime/banner-state.mjs +126 -0
  8. package/.github/extensions/web-relay/runtime/config-loader.mjs +104 -0
  9. package/.github/extensions/web-relay/runtime/debug-log.mjs +27 -0
  10. package/.github/extensions/web-relay/runtime/send-and-wait-errors.mjs +170 -0
  11. package/.github/extensions/web-relay/runtime/session-binding.mjs +78 -0
  12. package/.github/extensions/web-relay/runtime/session-io.mjs +349 -0
  13. package/.github/extensions/web-relay/runtime/session-join-retry.mjs +44 -0
  14. package/.github/extensions/web-relay/runtime/session-registry.mjs +29 -0
  15. package/.github/extensions/web-relay/runtime/session-runtime-manager.mjs +156 -0
  16. package/.github/extensions/web-relay/runtime/session-sync-bridge.mjs +74 -0
  17. package/.github/extensions/web-relay/runtime/worker-websocket-link.mjs +325 -0
  18. package/.github/extensions/web-relay/runtime/workspace-root.mjs +55 -0
  19. package/.github/extensions/web-relay/server-lifecycle/managed-server.mjs +386 -0
  20. package/.github/extensions/web-relay/skills/prompt-context.mjs +160 -0
  21. package/.github/extensions/web-relay/skills/question-bridge.mjs +141 -0
  22. package/.github/extensions/web-relay/skills/question-routing-hooks.mjs +463 -0
  23. package/.github/extensions/web-relay/skills/reasoning-stream.mjs +285 -0
  24. package/.github/extensions/web-relay/skills/subagent-lifecycle.mjs +345 -0
  25. package/.github/extensions/web-relay/skills/tool-activity.mjs +243 -0
  26. package/.github/extensions/web-relay/utils/process-utils.mjs +25 -0
  27. package/.github/extensions/web-relay/utils/token-generation.mjs +58 -0
  28. package/CHANGELOG.md +25 -0
  29. package/LICENSE +21 -0
  30. package/README.md +717 -0
  31. package/bin/bat/oar.bat +46 -0
  32. package/bin/oar.js +665 -0
  33. package/package.json +68 -0
  34. package/server/README.md +1379 -0
  35. package/server/claude-worker/claude-attachments.mjs +134 -0
  36. package/server/claude-worker/claude-preview-tool.mjs +57 -0
  37. package/server/claude-worker/claude-sdk-adapter.mjs +420 -0
  38. package/server/claude-worker/claude-session-process.mjs +2391 -0
  39. package/server/claude-worker/claude-session-test-harness.mjs +236 -0
  40. package/server/claude-worker/claude-session-worker.mjs +186 -0
  41. package/server/claude-worker/claude-transcript-relocator.mjs +189 -0
  42. package/server/claude-worker/claude-turn-publisher.mjs +291 -0
  43. package/server/claude-worker/fixtures/workflow-run-record.json +1 -0
  44. package/server/claude-worker/sdk-message-normalizer.mjs +629 -0
  45. package/server/claude-worker/workflow-progress-digest.mjs +269 -0
  46. package/server/copilot-sdk-runtime.mjs +126 -0
  47. package/server/copilot-worker/copilot-attachments.mjs +97 -0
  48. package/server/copilot-worker/copilot-byok-provider.mjs +121 -0
  49. package/server/copilot-worker/copilot-continuation-signals.mjs +349 -0
  50. package/server/copilot-worker/copilot-plan-board.mjs +110 -0
  51. package/server/copilot-worker/copilot-prompt-context.mjs +86 -0
  52. package/server/copilot-worker/copilot-question-bridge.mjs +216 -0
  53. package/server/copilot-worker/copilot-sdk-adapter.mjs +494 -0
  54. package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +800 -0
  55. package/server/copilot-worker/copilot-sdk-session-process.mjs +1729 -0
  56. package/server/copilot-worker/copilot-sdk-session-worker.mjs +167 -0
  57. package/server/copilot-worker/copilot-sdk-test-harness.mjs +329 -0
  58. package/server/copilot-worker/fixtures/abort-turn.json +588 -0
  59. package/server/copilot-worker/fixtures/ask-user-turn.json +1154 -0
  60. package/server/copilot-worker/fixtures/background-timer-continuation.json +445 -0
  61. package/server/copilot-worker/fixtures/background-timer-turn.json +497 -0
  62. package/server/copilot-worker/fixtures/happy-turn.json +566 -0
  63. package/server/copilot-worker/fixtures/quota-turn.json +647 -0
  64. package/server/copilot-worker/fixtures/reasoning-turn.json +625 -0
  65. package/server/copilot-worker/fixtures/resume-turn.json +700 -0
  66. package/server/copilot-worker/fixtures/subagent-turn.json +1378 -0
  67. package/server/copilot-worker/fixtures/tool-permission-turn.json +1293 -0
  68. package/server/cursor-worker/cursor-ask-user-tool.mjs +83 -0
  69. package/server/cursor-worker/cursor-attachments.mjs +88 -0
  70. package/server/cursor-worker/cursor-context-usage.mjs +85 -0
  71. package/server/cursor-worker/cursor-preview-tool.mjs +44 -0
  72. package/server/cursor-worker/cursor-sdk-adapter.mjs +554 -0
  73. package/server/cursor-worker/cursor-session-worker.mjs +145 -0
  74. package/server/cursor-worker/cursor-subagent-roster.mjs +81 -0
  75. package/server/cursor-worker/cursor-turn-runner.mjs +717 -0
  76. package/server/cursor-worker/sdk-message-normalizer.mjs +485 -0
  77. package/server/db-schema.mjs +932 -0
  78. package/server/features.mjs +106 -0
  79. package/server/grok-worker/acp-client.mjs +434 -0
  80. package/server/grok-worker/acp-host-services.mjs +277 -0
  81. package/server/grok-worker/grok-context-usage.mjs +77 -0
  82. package/server/grok-worker/grok-sdk-adapter.mjs +334 -0
  83. package/server/grok-worker/grok-session-worker.mjs +145 -0
  84. package/server/grok-worker/grok-turn-runner.mjs +523 -0
  85. package/server/grok-worker/sdk-message-normalizer.mjs +311 -0
  86. package/server/migrations/0001-add-structured-answer.mjs +80 -0
  87. package/server/migrations/0002-recent-workspace-roots-path-key.mjs +121 -0
  88. package/server/migrations/0003-push-subscriptions.mjs +87 -0
  89. package/server/public/app/action-confirmations.js +267 -0
  90. package/server/public/app/activity-replay-state.mjs +149 -0
  91. package/server/public/app/api-client.js +1082 -0
  92. package/server/public/app/ask-user-view.js +545 -0
  93. package/server/public/app/attachments-view.js +2112 -0
  94. package/server/public/app/auto-compact-window-options.mjs +59 -0
  95. package/server/public/app/background-tasks-view.mjs +494 -0
  96. package/server/public/app/bootstrap.js +4347 -0
  97. package/server/public/app/claude-auth-ui.js +394 -0
  98. package/server/public/app/claude-thinking-options.mjs +38 -0
  99. package/server/public/app/cli-install-ui.js +455 -0
  100. package/server/public/app/code-copy.mjs +85 -0
  101. package/server/public/app/composer-attachment-cache.mjs +151 -0
  102. package/server/public/app/composer-control-state.mjs +107 -0
  103. package/server/public/app/composer-paste.mjs +132 -0
  104. package/server/public/app/context-tier-options.mjs +68 -0
  105. package/server/public/app/context-usage-view.mjs +338 -0
  106. package/server/public/app/conversation-draft-timestamp-utils.mjs +11 -0
  107. package/server/public/app/conversation-preferences.mjs +91 -0
  108. package/server/public/app/conversation-provider-indicator.mjs +88 -0
  109. package/server/public/app/conversation-view.js +2898 -0
  110. package/server/public/app/cwd-menu-placement.mjs +85 -0
  111. package/server/public/app/cwd-picker.js +605 -0
  112. package/server/public/app/emoji-view.js +101 -0
  113. package/server/public/app/external-link-policy.mjs +80 -0
  114. package/server/public/app/file-preview-navigation.mjs +51 -0
  115. package/server/public/app/font-scaling.js +262 -0
  116. package/server/public/app/git-changes-view.js +339 -0
  117. package/server/public/app/git-diff-model.mjs +109 -0
  118. package/server/public/app/grok-auth-ui.js +387 -0
  119. package/server/public/app/image-downscale.mjs +174 -0
  120. package/server/public/app/in-flight-snapshot.mjs +40 -0
  121. package/server/public/app/infinite-loader.js +164 -0
  122. package/server/public/app/journal-view.js +1141 -0
  123. package/server/public/app/known-cwd-options.mjs +41 -0
  124. package/server/public/app/launch-cwd-path.mjs +23 -0
  125. package/server/public/app/live-message-dedupe.mjs +57 -0
  126. package/server/public/app/message-search-view.js +242 -0
  127. package/server/public/app/model-selector-options.mjs +31 -0
  128. package/server/public/app/model-vision-support.mjs +56 -0
  129. package/server/public/app/new-conversation-model-choice.mjs +67 -0
  130. package/server/public/app/plan-usage-view.mjs +307 -0
  131. package/server/public/app/preview-cards.mjs +273 -0
  132. package/server/public/app/preview-command.mjs +103 -0
  133. package/server/public/app/push-settings.js +370 -0
  134. package/server/public/app/pwa-install.js +456 -0
  135. package/server/public/app/question-schema-view.mjs +71 -0
  136. package/server/public/app/reasoning-effort-labels.mjs +42 -0
  137. package/server/public/app/relay-board-view.js +117 -0
  138. package/server/public/app/relay-dot-state.mjs +69 -0
  139. package/server/public/app/relay-error-ctas.mjs +52 -0
  140. package/server/public/app/relay-prompt-sanitizer.mjs +114 -0
  141. package/server/public/app/relay-thoughts.mjs +98 -0
  142. package/server/public/app/relay-watchdog-policy.mjs +29 -0
  143. package/server/public/app/repo-browser-preferences.mjs +79 -0
  144. package/server/public/app/repo-browser-tree-state.mjs +97 -0
  145. package/server/public/app/router.js +657 -0
  146. package/server/public/app/selection-guard.mjs +93 -0
  147. package/server/public/app/settings-modal.js +1484 -0
  148. package/server/public/app/settings-tabs.js +129 -0
  149. package/server/public/app/slash-autocomplete.mjs +188 -0
  150. package/server/public/app/slash-commands.mjs +183 -0
  151. package/server/public/app/socket-handlers.js +703 -0
  152. package/server/public/app/status-store.mjs +242 -0
  153. package/server/public/app/status-view.mjs +315 -0
  154. package/server/public/app/store.js +1454 -0
  155. package/server/public/app/stream-state.mjs +73 -0
  156. package/server/public/app/streaming-dom-patch.mjs +23 -0
  157. package/server/public/app/sync-outbox.mjs +208 -0
  158. package/server/public/app/tap-actions.js +62 -0
  159. package/server/public/app/thread-order.mjs +54 -0
  160. package/server/public/app/tmux-inspector-view.js +422 -0
  161. package/server/public/app/transcript-separators.mjs +282 -0
  162. package/server/public/app-icon-192.png +0 -0
  163. package/server/public/app-icon-512.png +0 -0
  164. package/server/public/app-icon.svg +4 -0
  165. package/server/public/favicon.ico +0 -0
  166. package/server/public/index.html +5743 -0
  167. package/server/public/manifest.webmanifest +32 -0
  168. package/server/public/sw.js +307 -0
  169. package/server/relay-exit-codes.mjs +4 -0
  170. package/server/relay-self-restart.mjs +170 -0
  171. package/server/relay-tools.md +27 -0
  172. package/server/relay.mjs +1026 -0
  173. package/server/repositories/image-conversation-repository.mjs +317 -0
  174. package/server/repositories/message-repository.mjs +434 -0
  175. package/server/repositories/question-repository.mjs +161 -0
  176. package/server/repositories/session-repository.mjs +320 -0
  177. package/server/repositories/share-visibility-statements.mjs +24 -0
  178. package/server/routes/ask-user-routes.mjs +292 -0
  179. package/server/routes/cache-routes.mjs +44 -0
  180. package/server/routes/git-routes.mjs +92 -0
  181. package/server/routes/messages-routes-test-harness.mjs +111 -0
  182. package/server/routes/messages-routes.mjs +7107 -0
  183. package/server/routes/preview-routes.mjs +108 -0
  184. package/server/routes/push-routes.mjs +59 -0
  185. package/server/routes/relay-board-routes.mjs +268 -0
  186. package/server/routes/sessions-routes.mjs +6150 -0
  187. package/server/server-runtime.mjs +7463 -0
  188. package/server/server.js +24 -0
  189. package/server/services/ask-user-routing-service.mjs +117 -0
  190. package/server/services/cache-rebuild-service.mjs +236 -0
  191. package/server/services/claude-auth-service.mjs +578 -0
  192. package/server/services/claude-context-usage.mjs +240 -0
  193. package/server/services/claude-session-root-service.mjs +228 -0
  194. package/server/services/cli-install-service.mjs +1152 -0
  195. package/server/services/cli-process-runner.mjs +181 -0
  196. package/server/services/cloudflared-tunnel-service.mjs +358 -0
  197. package/server/services/context-snapshot-service.mjs +499 -0
  198. package/server/services/context-usage-view.mjs +160 -0
  199. package/server/services/conversation-preferences-service.mjs +208 -0
  200. package/server/services/cursor-dashboard-usage.mjs +217 -0
  201. package/server/services/cursor-session-root-service.mjs +62 -0
  202. package/server/services/delete-archive-service.mjs +154 -0
  203. package/server/services/drives-path-helpers.mjs +66 -0
  204. package/server/services/fixtures/claude-auth-stub.sh +62 -0
  205. package/server/services/fixtures/cli-install-stub.sh +75 -0
  206. package/server/services/fixtures/grok-stub.sh +86 -0
  207. package/server/services/generated-image-cleanup-service.mjs +114 -0
  208. package/server/services/git-changes-service.mjs +195 -0
  209. package/server/services/github-billing-usage.mjs +133 -0
  210. package/server/services/grok-auth-service.mjs +593 -0
  211. package/server/services/grok-billing-usage.mjs +120 -0
  212. package/server/services/image-context-service.mjs +106 -0
  213. package/server/services/image-operation-service.mjs +135 -0
  214. package/server/services/keyed-mutex-service.mjs +82 -0
  215. package/server/services/mime-sniffer.mjs +118 -0
  216. package/server/services/oar-cli-helpers.mjs +109 -0
  217. package/server/services/oar-state-migration-service.mjs +243 -0
  218. package/server/services/plan-usage-claude.mjs +387 -0
  219. package/server/services/plan-usage-contract.mjs +271 -0
  220. package/server/services/plan-usage-copilot.mjs +421 -0
  221. package/server/services/plan-usage-cursor.mjs +566 -0
  222. package/server/services/plan-usage-grok.mjs +436 -0
  223. package/server/services/plan-usage-service.mjs +487 -0
  224. package/server/services/preview-health-probe.mjs +82 -0
  225. package/server/services/preview-proxy-server.mjs +498 -0
  226. package/server/services/preview-registry-service.mjs +457 -0
  227. package/server/services/preview-static-handler.mjs +237 -0
  228. package/server/services/provider-model-selection.mjs +91 -0
  229. package/server/services/provider-reasoning-effort.mjs +94 -0
  230. package/server/services/push-active-device-service.mjs +57 -0
  231. package/server/services/push-dispatch-service.mjs +459 -0
  232. package/server/services/rate-limit-service.mjs +63 -0
  233. package/server/services/relay-bridge-owner-service.mjs +84 -0
  234. package/server/services/relay-cli-launcher-service.mjs +209 -0
  235. package/server/services/relay-prompt-sanitizer.mjs +137 -0
  236. package/server/services/relay-queue-gate-service.mjs +38 -0
  237. package/server/services/relay-restart-orchestrator-service.mjs +935 -0
  238. package/server/services/relay-singleton-guard.mjs +124 -0
  239. package/server/services/safe-served-content.mjs +57 -0
  240. package/server/services/sdk-session-import-service.mjs +271 -0
  241. package/server/services/sdk-session-sync-service.mjs +207 -0
  242. package/server/services/session-history-refresh-service.mjs +497 -0
  243. package/server/services/session-transcript-service.mjs +419 -0
  244. package/server/services/session-worker-launch-service.mjs +786 -0
  245. package/server/services/session-worker-process-service.mjs +319 -0
  246. package/server/services/session-worker-registry-service.mjs +194 -0
  247. package/server/services/session-worker-stop-service.mjs +141 -0
  248. package/server/services/session-worker-supervisor-service.mjs +898 -0
  249. package/server/services/session-worker-websocket-service.mjs +483 -0
  250. package/server/services/ssh-tunnel-manager-service.mjs +423 -0
  251. package/server/services/status-event-service.mjs +158 -0
  252. package/server/services/tmux-inspector-access-policy.mjs +77 -0
  253. package/server/services/tmux-inspector-socket-service.mjs +191 -0
  254. package/server/services/tmux-inspector-stream-service.mjs +552 -0
  255. package/server/services/tunnel-worker-path-guard.mjs +107 -0
  256. package/server/services/upload-sweep.mjs +60 -0
  257. package/server/services/usage-snapshot-helpers.mjs +209 -0
  258. package/server/services/windows-autostart-service.mjs +115 -0
  259. package/server/services/workspace-root-defaults-service.mjs +34 -0
  260. package/server/services/workspace-root-path-policy.mjs +196 -0
  261. package/server/services/workspace-root-relaunch-service.mjs +132 -0
  262. package/server/services/workspace-symlink-guard.mjs +54 -0
  263. package/server/tools/capture-proxy.mjs +173 -0
  264. package/server/tty-console-bootstrap.mjs +75 -0
  265. package/server/uploads/README.md +36 -0
  266. package/server/uploads/image_input.md +540 -0
  267. package/server/windows-terminal-launcher.mjs +29 -0
  268. package/server/workspace-root.mjs +87 -0
  269. package/shared/ask-user-bridge.mjs +131 -0
  270. package/shared/auto-compact-window.mjs +116 -0
  271. package/shared/background-task-timeout.mjs +52 -0
  272. package/shared/claude-thinking.mjs +101 -0
  273. package/shared/context-window-fallbacks.mjs +148 -0
  274. package/shared/control-poller.mjs +115 -0
  275. package/shared/empty-turn-completion.mjs +20 -0
  276. package/shared/model-catalog-freshness.mjs +11 -0
  277. package/shared/model-descriptors.mjs +118 -0
  278. package/shared/model-id.mjs +147 -0
  279. package/shared/model-refresh.mjs +24 -0
  280. package/shared/openai-reasoning.mjs +13 -0
  281. package/shared/plan-lines.mjs +13 -0
  282. package/shared/preview-instructions.mjs +77 -0
  283. package/shared/preview-tool-core.mjs +191 -0
  284. package/shared/provider-routing.mjs +29 -0
  285. package/shared/question-schema.mjs +315 -0
  286. package/shared/question-timeout.mjs +18 -0
  287. package/shared/stream-emit-gating.mjs +28 -0
  288. package/shared/subagent-run-id.mjs +17 -0
  289. package/shared/thought-cap.mjs +13 -0
  290. package/shared/tool-activity.mjs +98 -0
  291. package/shared/turn-ceiling.mjs +53 -0
  292. package/shared/worker-bootstrap.mjs +44 -0
  293. package/shared/worker-crash-guard.mjs +60 -0
@@ -0,0 +1,181 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Mechanical helpers for driving a provider CLI as a child process.
5
+ *
6
+ * Lifted verbatim out of claude-auth-service.mjs, which held the only copy:
7
+ * the CLI install service and the Grok auth service need the same escape
8
+ * stripping, secret scrubbing, process-group kill and run-to-completion
9
+ * wrapper, and a second hand-rolled copy of any of them is exactly how two
10
+ * spawn paths drift apart.
11
+ *
12
+ * Only the pieces with no state of their own moved. The Claude auth state
13
+ * machine is live-verified and stayed where it is; claude-auth-service.mjs
14
+ * re-exports the helpers it used to export, so every importer and test of it
15
+ * is untouched.
16
+ */
17
+
18
+ /** Refusal used by every spawn path when the relay runs with CLI spawns off. */
19
+ export const CLI_SPAWN_DISABLED_ERROR = 'cli spawns disabled';
20
+
21
+ /** Retained-output ceiling shared by every capture path (auth, install). */
22
+ export const MAX_CAPTURED_OUTPUT_CHARS = 16_000;
23
+ const MAX_ERROR_TAIL_CHARS = 600;
24
+
25
+ // Any OSC sequence (hyperlinks, window-title sets, clipboard writes): ESC ]
26
+ // <payload> (BEL | ST). Stripped whole so no `0;title` residue reaches the
27
+ // prompt/JSON/error parsers.
28
+ const OSC_PATTERN = /\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
29
+ // Any CSI sequence (SGR colours, cursor moves, erases).
30
+ const CSI_PATTERN = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
31
+ // Remaining single-character / other escape sequences.
32
+ const OTHER_ESCAPE_PATTERN = /\x1b[@-Z\\-_]/g;
33
+ // `script` gives a CLI an 80-column PTY, and CLIs hard-wrap their own output at
34
+ // that width. Only a line that is *exactly* that wide is treated as continued:
35
+ // a line that merely ends in a long token ended because the CLI printed a
36
+ // newline, and gluing the next line onto it would corrupt the match. On a
37
+ // differently-sized terminal this degrades to the pre-wrap behaviour.
38
+ const PTY_WRAP_COLUMNS = 80;
39
+
40
+ function normalizeText(value) {
41
+ return String(value == null ? '' : value).trim();
42
+ }
43
+
44
+ /**
45
+ * Removes OSC sequences (including OSC-8 hyperlink wrappers), ANSI CSI
46
+ * sequences and the CR the PTY adds to every line, leaving the human-visible
47
+ * text.
48
+ */
49
+ export function stripTerminalEscapes(value) {
50
+ return String(value == null ? '' : value)
51
+ .replace(OSC_PATTERN, '')
52
+ .replace(CSI_PATTERN, '')
53
+ .replace(OTHER_ESCAPE_PATTERN, '')
54
+ .replace(/\r/g, '');
55
+ }
56
+
57
+ /**
58
+ * Rejoins lines the PTY hard-wrapped mid-token: a wrap fills the terminal width
59
+ * and leaves no trailing space, so a full-width line is stitched onto the line
60
+ * that follows it (repeatedly, for a URL spanning three or more rows).
61
+ */
62
+ export function joinWrappedLines(value) {
63
+ const lines = String(value == null ? '' : value).split('\n');
64
+ const joined = [];
65
+ let previousWrapped = false;
66
+ for (const line of lines) {
67
+ if (previousWrapped && joined.length && /^\S/.test(line)) {
68
+ joined[joined.length - 1] = `${joined[joined.length - 1]}${line}`;
69
+ } else {
70
+ joined.push(line);
71
+ }
72
+ previousWrapped = line.length === PTY_WRAP_COLUMNS && /\S$/.test(line);
73
+ }
74
+ return joined.join('\n');
75
+ }
76
+
77
+ /**
78
+ * Redacts anything token-shaped plus an exact secret the caller knows it
79
+ * handed over (the pasted Claude login code), so an error tail can be surfaced
80
+ * to the UI without leaking a credential. Long PKCE segments in an authorize
81
+ * URL get caught by the generic rule too, which is fine: the URL is already
82
+ * carried in the login payload.
83
+ */
84
+ export function scrubSecrets(text, submittedCode = '') {
85
+ let output = String(text == null ? '' : text);
86
+ const code = normalizeText(submittedCode);
87
+ if (code.length >= 4) {
88
+ output = output.split(code).join('[redacted]');
89
+ }
90
+ return output
91
+ .replace(/sk-ant-[A-Za-z0-9_-]+/g, '[redacted]')
92
+ .replace(/[A-Za-z0-9_-]{32,}/g, '[redacted]');
93
+ }
94
+
95
+ /** Collapses captured output into one short single-line tail for an error. */
96
+ export function tailOf(text, limit = MAX_ERROR_TAIL_CHARS) {
97
+ const lines = String(text || '')
98
+ .split('\n')
99
+ .map((line) => line.trimEnd())
100
+ .filter((line) => line.trim());
101
+ const joined = lines.join(' · ');
102
+ return joined.length > limit ? `…${joined.slice(-limit)}` : joined;
103
+ }
104
+
105
+ /**
106
+ * Signals the child's whole process group where the platform has one, so a
107
+ * `script`/`bash -lc` wrapper cannot leave the real CLI running behind it.
108
+ * A child that has already exited is left alone.
109
+ */
110
+ export function killTree(child, signal, {
111
+ platform = process.platform,
112
+ processKillImpl = process.kill.bind(process),
113
+ } = {}) {
114
+ if (!child || child.exitCode !== null) return;
115
+ const pid = Number(child.pid);
116
+ if (platform !== 'win32' && Number.isInteger(pid) && pid > 0) {
117
+ try {
118
+ processKillImpl(-pid, signal);
119
+ return;
120
+ } catch {}
121
+ }
122
+ try { child.kill(signal); } catch {}
123
+ }
124
+
125
+ /**
126
+ * Runs one already-configured child to completion and resolves (never rejects)
127
+ * with `{ok, code, output, error}`.
128
+ *
129
+ * `spawnChild` is a thunk so the caller keeps ownership of how the process is
130
+ * built — PTY or not, which stdio, which environment — and so a refused spawn
131
+ * (the CLI kill switch) surfaces as a result rather than a throw. `onChild`
132
+ * hands the live handle back for cancellation, and `onOutput` sees each raw
133
+ * chunk for callers that stream a log while it runs.
134
+ */
135
+ export function runToCompletion(spawnChild, {
136
+ timeoutMs,
137
+ setTimeoutImpl = setTimeout,
138
+ clearTimeoutImpl = clearTimeout,
139
+ killChild = (child, signal) => killTree(child, signal),
140
+ maxOutputChars = MAX_CAPTURED_OUTPUT_CHARS,
141
+ onChild = null,
142
+ onOutput = null,
143
+ } = {}) {
144
+ return new Promise((resolve) => {
145
+ let child = null;
146
+ try {
147
+ child = spawnChild();
148
+ } catch (error) {
149
+ resolve({ ok: false, code: null, output: '', error: error?.message || String(error) });
150
+ return;
151
+ }
152
+ try { onChild?.(child); } catch {}
153
+ let output = '';
154
+ let settled = false;
155
+ const append = (chunk) => {
156
+ const text = chunk.toString();
157
+ output = `${output}${text}`.slice(-maxOutputChars);
158
+ try { onOutput?.(text); } catch {}
159
+ };
160
+ const finish = (result) => {
161
+ if (settled) return;
162
+ settled = true;
163
+ if (timer) clearTimeoutImpl(timer);
164
+ resolve(result);
165
+ };
166
+ const timer = setTimeoutImpl(() => {
167
+ killChild(child, 'SIGTERM');
168
+ finish({ ok: false, code: null, output, error: `timed out after ${timeoutMs}ms` });
169
+ }, timeoutMs);
170
+ timer?.unref?.();
171
+ child.stdout?.on?.('data', append);
172
+ child.stderr?.on?.('data', append);
173
+ child.stdin?.end?.();
174
+ child.on('error', (error) => finish({
175
+ ok: false, code: null, output, error: error?.message || String(error),
176
+ }));
177
+ child.on('close', (code) => finish({
178
+ ok: code === 0, code, output, error: null,
179
+ }));
180
+ });
181
+ }
@@ -0,0 +1,358 @@
1
+ 'use strict';
2
+
3
+ import path from 'path';
4
+ import fs from 'fs';
5
+ import { createRequire } from 'module';
6
+ import { spawn } from 'child_process';
7
+
8
+ const requireFromHere = createRequire(import.meta.url);
9
+
10
+ const BACKOFF_STEPS = [5_000, 10_000, 20_000, 40_000, 60_000];
11
+ const READINESS_WINDOW_MS = 5_000;
12
+ const STABLE_CONNECTION_MS = 30_000;
13
+ const FAST_EXIT_MS = 10_000;
14
+ const MAX_CONSECUTIVE_FAST_EXITS = 3;
15
+
16
+ function toText(value) {
17
+ return String(value || '').trim();
18
+ }
19
+
20
+ function normalizeTunnelMode(raw = {}) {
21
+ const mode = toText(raw.mode).toLowerCase();
22
+ if (mode === 'disabled' || mode === 'managed') return mode;
23
+ if (raw.enabled === true) return 'managed';
24
+ return 'disabled';
25
+ }
26
+
27
+ function normalizeExtraArgs(rawValue) {
28
+ if (!Array.isArray(rawValue)) return [];
29
+ return rawValue.map((entry) => toText(entry)).filter(Boolean);
30
+ }
31
+
32
+ // The npm `cloudflared` package downloads the official binary on demand; it is an
33
+ // optional dependency, so absence must degrade to a config error, never a crash.
34
+ // `requireImpl` is a seam: the package is optional, so a suite running under
35
+ // `npm ci --omit=optional` (or after a failed postinstall download) must still be
36
+ // able to exercise this logic instead of silently testing the host's install state.
37
+ export function resolveCloudflaredBinaryFromPackage({
38
+ existsSync = fs.existsSync,
39
+ requireImpl = requireFromHere,
40
+ } = {}) {
41
+ try {
42
+ const mod = requireImpl('cloudflared');
43
+ const bin = toText(mod?.bin);
44
+ // `bin` is where the package *will* place the binary; it only exists after
45
+ // the on-demand download has run, so an un-downloaded path is not usable.
46
+ if (bin && existsSync(bin)) return bin;
47
+ } catch {}
48
+ return null;
49
+ }
50
+
51
+ export function normalizeCloudflaredTunnelConfig(rawConfig = {}, {
52
+ env = process.env,
53
+ resolveBinary = resolveCloudflaredBinaryFromPackage,
54
+ configBaseDir = process.cwd(),
55
+ pathImpl = path,
56
+ } = {}) {
57
+ const raw = rawConfig && typeof rawConfig === 'object' ? rawConfig : {};
58
+ const envMode = toText(env?.COPILOT_CLOUDFLARED_MODE).toLowerCase();
59
+ const mode = envMode === 'disabled' || envMode === 'managed'
60
+ ? envMode
61
+ : normalizeTunnelMode(raw);
62
+ const required = raw.required === true;
63
+ const token = toText(env?.COPILOT_CLOUDFLARED_TOKEN) || toText(raw.token);
64
+ const binaryInput = toText(env?.COPILOT_CLOUDFLARED_BINARY) || toText(raw.binary);
65
+ const extraArgs = normalizeExtraArgs(raw.extraArgs);
66
+
67
+ let binary = '';
68
+ let binarySource = null;
69
+ if (binaryInput) {
70
+ binary = binaryInput.includes('/') || binaryInput.includes('\\')
71
+ ? pathImpl.resolve(configBaseDir, binaryInput)
72
+ : binaryInput;
73
+ binarySource = 'config';
74
+ } else {
75
+ let packageBinary = null;
76
+ try {
77
+ packageBinary = toText(typeof resolveBinary === 'function' ? resolveBinary() : '');
78
+ } catch {
79
+ packageBinary = null;
80
+ }
81
+ if (packageBinary) {
82
+ binary = packageBinary;
83
+ binarySource = 'package';
84
+ } else {
85
+ binary = 'cloudflared';
86
+ binarySource = 'path';
87
+ }
88
+ }
89
+
90
+ const errors = [];
91
+ if (mode === 'managed') {
92
+ if (!token) errors.push('cloudflaredTunnel.token is required when cloudflaredTunnel.mode is "managed"');
93
+ if (!binary) errors.push('cloudflaredTunnel.binary could not be resolved (install the "cloudflared" package or set cloudflaredTunnel.binary)');
94
+ }
95
+
96
+ return {
97
+ mode,
98
+ enabled: mode === 'managed',
99
+ valid: errors.length === 0,
100
+ errors,
101
+ required,
102
+ token,
103
+ binary,
104
+ binarySource,
105
+ extraArgs,
106
+ };
107
+ }
108
+
109
+ export function buildCloudflaredArgs(tunnelConfig) {
110
+ return ['tunnel', 'run', '--token', tunnelConfig.token, ...tunnelConfig.extraArgs];
111
+ }
112
+
113
+ export function redactCloudflaredArgs(args = []) {
114
+ const out = [];
115
+ for (let i = 0; i < args.length; i += 1) {
116
+ if (args[i] === '--token') {
117
+ out.push('--token', '<redacted>');
118
+ i += 1;
119
+ continue;
120
+ }
121
+ out.push(args[i]);
122
+ }
123
+ return out;
124
+ }
125
+
126
+ function buildSpawnOptions(platform, stdio) {
127
+ if (platform === 'win32') return { stdio, windowsHide: true };
128
+ return { stdio };
129
+ }
130
+
131
+ function isRegistrationLine(text) {
132
+ return /registered tunnel connection|connection .* registered|registered connection/i.test(text);
133
+ }
134
+
135
+ export function createCloudflaredTunnelManager({
136
+ tunnelConfig: rawTunnelConfig = {},
137
+ runtimeLogPrefix = () => '',
138
+ io = null,
139
+ logger = console,
140
+ runtimeShutdownRef = () => false,
141
+ platform = process.platform,
142
+ spawnImpl = spawn,
143
+ nowIso = () => new Date().toISOString(),
144
+ setTimeoutImpl = setTimeout,
145
+ clearTimeoutImpl = clearTimeout,
146
+ configBaseDir = process.cwd(),
147
+ env = process.env,
148
+ resolveBinary = resolveCloudflaredBinaryFromPackage,
149
+ pathImpl = path,
150
+ } = {}) {
151
+ const tunnelConfig = normalizeCloudflaredTunnelConfig(rawTunnelConfig, {
152
+ env,
153
+ resolveBinary,
154
+ configBaseDir,
155
+ pathImpl,
156
+ });
157
+ const log = (msg) => logger.log(`${runtimeLogPrefix()}[cloudflared-tunnel] ${msg}`);
158
+ const warn = (msg) => logger.warn(`${runtimeLogPrefix()}[cloudflared-tunnel] ${msg}`);
159
+
160
+ for (const error of tunnelConfig.errors) {
161
+ warn(error);
162
+ }
163
+
164
+ const state = {
165
+ mode: tunnelConfig.mode,
166
+ enabled: tunnelConfig.enabled && tunnelConfig.valid,
167
+ valid: tunnelConfig.valid,
168
+ required: tunnelConfig.required,
169
+ connected: false,
170
+ reconnectAttempts: 0,
171
+ fastExits: 0,
172
+ connectedSince: null,
173
+ blocking: tunnelConfig.required && tunnelConfig.mode === 'managed',
174
+ lastError: tunnelConfig.errors[0] || null,
175
+ lastEventAt: nowIso(),
176
+ binary: tunnelConfig.binary || null,
177
+ proc: null,
178
+ backoffTimer: null,
179
+ };
180
+
181
+ const emitStatus = () => {
182
+ state.lastEventAt = nowIso();
183
+ io?.emit?.('cloudflared_tunnel_status', {
184
+ connected: state.connected,
185
+ mode: state.mode,
186
+ enabled: state.enabled,
187
+ required: state.required,
188
+ blocking: state.blocking,
189
+ reconnectAttempts: state.reconnectAttempts,
190
+ connectedSince: state.connectedSince,
191
+ lastError: state.lastError,
192
+ });
193
+ };
194
+
195
+ const updateBlockingState = () => {
196
+ state.blocking = state.required && state.mode === 'managed' && !state.connected;
197
+ };
198
+
199
+ const scheduleReconnect = (spawnTunnel, { slowest = false } = {}) => {
200
+ if (runtimeShutdownRef()) return;
201
+ if (state.backoffTimer) {
202
+ clearTimeoutImpl(state.backoffTimer);
203
+ state.backoffTimer = null;
204
+ }
205
+ const stepIndex = slowest
206
+ ? BACKOFF_STEPS.length - 1
207
+ : Math.min(state.reconnectAttempts, BACKOFF_STEPS.length - 1);
208
+ const base = BACKOFF_STEPS[stepIndex];
209
+ const delay = Math.round(base + (Math.random() * base * 0.2));
210
+ state.reconnectAttempts += 1;
211
+ log(`Reconnecting in ${Math.round(delay / 1000)}s (attempt ${state.reconnectAttempts})...`);
212
+ state.backoffTimer = setTimeoutImpl(spawnTunnel, delay);
213
+ if (typeof state.backoffTimer?.unref === 'function') state.backoffTimer.unref();
214
+ };
215
+
216
+ const spawnTunnel = () => {
217
+ if (!state.enabled || runtimeShutdownRef()) return;
218
+ if (state.proc && state.proc.exitCode === null) {
219
+ log('Spawn skipped: existing cloudflared process is still running.');
220
+ return;
221
+ }
222
+
223
+ const args = buildCloudflaredArgs(tunnelConfig);
224
+ log(`Spawning: ${tunnelConfig.binary} ${redactCloudflaredArgs(args).join(' ')}`);
225
+ const proc = spawnImpl(tunnelConfig.binary, args, buildSpawnOptions(platform, ['ignore', 'pipe', 'pipe']));
226
+ state.proc = proc;
227
+ state.lastError = null;
228
+ updateBlockingState();
229
+ emitStatus();
230
+
231
+ const startedAt = Date.now();
232
+ let readinessTimer = null;
233
+
234
+ const clearReadinessTimer = () => {
235
+ if (!readinessTimer) return;
236
+ clearTimeoutImpl(readinessTimer);
237
+ readinessTimer = null;
238
+ };
239
+
240
+ const markConnected = (reason) => {
241
+ if (state.connected) return;
242
+ state.connected = true;
243
+ state.connectedSince = nowIso();
244
+ state.lastError = null;
245
+ state.fastExits = 0;
246
+ updateBlockingState();
247
+ log(`Tunnel connected (${reason}).`);
248
+ emitStatus();
249
+ };
250
+
251
+ const armReadinessFallback = () => {
252
+ clearReadinessTimer();
253
+ readinessTimer = setTimeoutImpl(() => {
254
+ readinessTimer = null;
255
+ if (runtimeShutdownRef()) return;
256
+ if (state.proc !== proc) return;
257
+ if (proc.exitCode !== null) return;
258
+ markConnected('readiness-window');
259
+ }, READINESS_WINDOW_MS);
260
+ if (typeof readinessTimer?.unref === 'function') readinessTimer.unref();
261
+ };
262
+
263
+ const handleOutput = (text) => {
264
+ if (!text) return;
265
+ log(`stderr: ${text}`);
266
+ if (isRegistrationLine(text)) {
267
+ clearReadinessTimer();
268
+ markConnected('registration');
269
+ }
270
+ };
271
+
272
+ proc.stdout?.on?.('data', (d) => handleOutput(d.toString().trim()));
273
+ proc.stderr?.on?.('data', (d) => handleOutput(d.toString().trim()));
274
+ proc.on('spawn', armReadinessFallback);
275
+ proc.on('error', (error) => {
276
+ clearReadinessTimer();
277
+ state.lastError = error?.message || String(error);
278
+ updateBlockingState();
279
+ log(`Error: ${state.lastError}`);
280
+ emitStatus();
281
+ });
282
+ proc.on('close', (code) => {
283
+ clearReadinessTimer();
284
+ const wasConnected = state.connected;
285
+ const uptime = Date.now() - startedAt;
286
+ state.connected = false;
287
+ state.connectedSince = null;
288
+ state.proc = null;
289
+ if (wasConnected && uptime > STABLE_CONNECTION_MS) {
290
+ state.reconnectAttempts = 0;
291
+ }
292
+ updateBlockingState();
293
+
294
+ if (runtimeShutdownRef()) {
295
+ log(`Process exited (code=${code}) during shutdown.`);
296
+ emitStatus();
297
+ return;
298
+ }
299
+
300
+ if (uptime < FAST_EXIT_MS) {
301
+ state.fastExits += 1;
302
+ } else {
303
+ state.fastExits = 0;
304
+ }
305
+
306
+ if (state.fastExits >= MAX_CONSECUTIVE_FAST_EXITS) {
307
+ state.lastError = 'auth-or-config';
308
+ log(`Process exited (code=${code}) after ${state.fastExits} fast exits; treating as auth/config failure.`);
309
+ emitStatus();
310
+ scheduleReconnect(spawnTunnel, { slowest: true });
311
+ return;
312
+ }
313
+
314
+ state.lastError = `exit:${code ?? 'null'}`;
315
+ log(`Process exited (code=${code}). Scheduling reconnect...`);
316
+ emitStatus();
317
+ scheduleReconnect(spawnTunnel);
318
+ });
319
+ };
320
+
321
+ const start = () => {
322
+ if (state.mode === 'disabled') {
323
+ state.lastError = null;
324
+ state.blocking = false;
325
+ emitStatus();
326
+ log('Cloudflare tunnel mode disabled.');
327
+ return;
328
+ }
329
+ if (!state.enabled) {
330
+ state.lastError = state.lastError || 'invalid-config';
331
+ updateBlockingState();
332
+ emitStatus();
333
+ log('Managed cloudflared mode requested but configuration is invalid; tunnel not started.');
334
+ return;
335
+ }
336
+ log(`Cloudflare tunnel enabled (binary source: ${tunnelConfig.binarySource}).`);
337
+ spawnTunnel();
338
+ };
339
+
340
+ const stop = () => {
341
+ if (state.backoffTimer) {
342
+ clearTimeoutImpl(state.backoffTimer);
343
+ state.backoffTimer = null;
344
+ }
345
+ if (state.proc) {
346
+ try { state.proc.kill('SIGTERM'); } catch {}
347
+ state.proc = null;
348
+ }
349
+ };
350
+
351
+ return {
352
+ state,
353
+ config: tunnelConfig,
354
+ start,
355
+ stop,
356
+ emitStatus,
357
+ };
358
+ }