@midhunxavier/code-flow 0.0.0-stage → 0.1.0-preview.3

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 (319) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +141 -2
  3. package/bin/codeflow.mjs +8 -0
  4. package/bin/launcher.mjs +127 -0
  5. package/dist/remote-host.json +1 -0
  6. package/licenses/inter.txt +92 -0
  7. package/licenses/preact.txt +21 -0
  8. package/licenses/signals-core.txt +21 -0
  9. package/licenses/signals.txt +21 -0
  10. package/licenses/typescript-notices.txt +193 -0
  11. package/licenses/typescript.txt +55 -0
  12. package/licenses/uqr.txt +22 -0
  13. package/licenses/xterm-fit.txt +19 -0
  14. package/licenses/xterm.txt +21 -0
  15. package/package.json +34 -4
  16. package/public/assets/changes-62ZIEQSN.js +1 -0
  17. package/public/assets/chunk-2PZVOKID.js +1 -0
  18. package/public/assets/chunk-4D5IENEB.js +1 -0
  19. package/public/assets/chunk-5SARP5MU.js +1 -0
  20. package/public/assets/chunk-63YLZ3PR.js +1 -0
  21. package/public/assets/chunk-7FUHKF2M.js +1 -0
  22. package/public/assets/chunk-APQEVJ2K.js +1 -0
  23. package/public/assets/chunk-CGOZQMEZ.js +2 -0
  24. package/public/assets/chunk-J4VYS3KV.js +2 -0
  25. package/public/assets/chunk-L7TUCIYF.js +1 -0
  26. package/public/assets/chunk-SIHDWM2Y.js +154 -0
  27. package/public/assets/chunk-TEJVYIKC.js +1 -0
  28. package/public/assets/code-browser-VB2HGZSG.js +4 -0
  29. package/public/assets/filtered-tasks-ES5CP4KV.js +1 -0
  30. package/public/assets/interactive-browser-7F5ZXRUV.js +7 -0
  31. package/public/assets/join-project-6RTFHNCA.js +1 -0
  32. package/public/assets/main-65EW73SB.js +1 -0
  33. package/public/assets/project-invitations-view-GMPWGWYM.js +1 -0
  34. package/public/assets/settings-UB5Q4AQM.js +9 -0
  35. package/public/assets/styles-18792BB4.css +32 -0
  36. package/public/assets/team-server-wizard-2GLDWS6D.js +8 -0
  37. package/public/assets/terminal-IWAIM4DE.js +62 -0
  38. package/public/assets.json +1 -0
  39. package/public/bundle-meta.json +1 -0
  40. package/public/docs/.nojekyll +0 -0
  41. package/public/docs/accessibility.html +19 -0
  42. package/public/docs/acp-agents.html +117 -0
  43. package/public/docs/agent-containers.html +79 -0
  44. package/public/docs/api.html +1130 -0
  45. package/public/docs/app.js +342 -0
  46. package/public/docs/application-improvements.html +275 -0
  47. package/public/docs/architecture-explorer.html +19 -0
  48. package/public/docs/architecture.html +149 -0
  49. package/public/docs/auto-team.html +148 -0
  50. package/public/docs/automations.html +109 -0
  51. package/public/docs/checkpoint-review.html +37 -0
  52. package/public/docs/code-hosts.html +35 -0
  53. package/public/docs/code-intelligence.html +35 -0
  54. package/public/docs/configuration.html +346 -0
  55. package/public/docs/context-inspection.html +23 -0
  56. package/public/docs/coordinated-changes.html +28 -0
  57. package/public/docs/deferred-runs.html +25 -0
  58. package/public/docs/dependency-discovery.html +19 -0
  59. package/public/docs/documentation.html +84 -0
  60. package/public/docs/dynamic-teams-evaluation.html +174 -0
  61. package/public/docs/dynamic-teams-implementation-plan.html +882 -0
  62. package/public/docs/dynamic-teams-implementation-review-1.html +141 -0
  63. package/public/docs/dynamic-teams-plan-review-0.html +204 -0
  64. package/public/docs/extensions.html +42 -0
  65. package/public/docs/external-sources.html +96 -0
  66. package/public/docs/filesystem-policy.html +26 -0
  67. package/public/docs/filtered-tasks.html +44 -0
  68. package/public/docs/font-license.txt +92 -0
  69. package/public/docs/getting-started.html +143 -0
  70. package/public/docs/github-review-threads.html +26 -0
  71. package/public/docs/github.html +157 -0
  72. package/public/docs/help.html +69 -0
  73. package/public/docs/index.html +88 -0
  74. package/public/docs/installation.html +80 -0
  75. package/public/docs/launch-profiles.html +48 -0
  76. package/public/docs/maintenance.html +60 -0
  77. package/public/docs/memory-claims.html +31 -0
  78. package/public/docs/memory-storage.html +58 -0
  79. package/public/docs/notifications.html +39 -0
  80. package/public/docs/patterns.html +195 -0
  81. package/public/docs/performance-results.html +210 -0
  82. package/public/docs/performance.html +166 -0
  83. package/public/docs/project-conventions.html +22 -0
  84. package/public/docs/project-first-navigation-plan.html +587 -0
  85. package/public/docs/project-teams-implementation-plan.html +772 -0
  86. package/public/docs/project-teams.html +129 -0
  87. package/public/docs/provider-compatibility.html +20 -0
  88. package/public/docs/releasing.html +65 -0
  89. package/public/docs/remote-workers.html +64 -0
  90. package/public/docs/review-inbox.html +26 -0
  91. package/public/docs/run-storage.html +23 -0
  92. package/public/docs/runs.html +141 -0
  93. package/public/docs/search.json +1 -0
  94. package/public/docs/security.html +133 -0
  95. package/public/docs/shared-projects.html +31 -0
  96. package/public/docs/ssh-projects.html +60 -0
  97. package/public/docs/style.css +768 -0
  98. package/public/docs/task-reruns.html +30 -0
  99. package/public/docs/team-collaboration.html +412 -0
  100. package/public/docs/verification.html +22 -0
  101. package/public/docs/vscode.html +53 -0
  102. package/public/docs/worker-permission-verification.html +131 -0
  103. package/public/index.html +17 -0
  104. package/scripts/check-node.mjs +7 -0
  105. package/scripts/codeflow.mjs +55 -0
  106. package/scripts/computer-use.swift +64 -0
  107. package/scripts/pty.py +273 -0
  108. package/src/acp-connections.js +110 -0
  109. package/src/adapters/acp-presets.js +37 -0
  110. package/src/adapters/acp-types.js +0 -0
  111. package/src/adapters/acp.js +328 -0
  112. package/src/adapters/agent-tools.js +28 -0
  113. package/src/adapters/agy-auth.js +67 -0
  114. package/src/adapters/agy-isolation.js +242 -0
  115. package/src/adapters/agy-tools.js +45 -0
  116. package/src/adapters/agy.js +326 -0
  117. package/src/adapters/claude-stream.js +58 -0
  118. package/src/adapters/claude.js +282 -0
  119. package/src/adapters/codex-app-server.js +555 -0
  120. package/src/adapters/codex.js +311 -0
  121. package/src/adapters/errors.js +57 -0
  122. package/src/adapters/index.js +91 -0
  123. package/src/adapters/managed-permissions.js +37 -0
  124. package/src/adapters/model-discovery.js +100 -0
  125. package/src/adapters/process.js +433 -0
  126. package/src/adapters/team-tools.js +103 -0
  127. package/src/adapters/types.js +0 -0
  128. package/src/agent-browser.js +688 -0
  129. package/src/agent-computer.js +88 -0
  130. package/src/agent-container.js +155 -0
  131. package/src/agent-team-definitions.js +131 -0
  132. package/src/agent-tool-client.mjs +79 -0
  133. package/src/agent-tools.js +296 -0
  134. package/src/approvals.js +134 -0
  135. package/src/architecture.js +456 -0
  136. package/src/assets.js +44 -0
  137. package/src/automation.js +901 -0
  138. package/src/browser-files.js +293 -0
  139. package/src/browser-ownership.js +150 -0
  140. package/src/certificate-renewal.js +75 -0
  141. package/src/chat-family.js +25 -0
  142. package/src/chat-handoff.js +87 -0
  143. package/src/checkpoint-review.js +72 -0
  144. package/src/checks.js +303 -0
  145. package/src/code-host-git.js +102 -0
  146. package/src/code-hosts-api.js +99 -0
  147. package/src/code-hosts.js +482 -0
  148. package/src/code-intelligence-worker.mjs +452 -0
  149. package/src/code-intelligence.js +141 -0
  150. package/src/collab/api.js +365 -0
  151. package/src/collab/checkpoints.js +198 -0
  152. package/src/collab/connections.js +139 -0
  153. package/src/collab/host-execution.js +347 -0
  154. package/src/collab/hub-client.js +210 -0
  155. package/src/collab/link-api.js +229 -0
  156. package/src/collab/link.js +595 -0
  157. package/src/collab/model.js +496 -0
  158. package/src/collab/runner.js +1014 -0
  159. package/src/collab/store.js +1452 -0
  160. package/src/collab/terminals.js +361 -0
  161. package/src/collaboration-reviews.js +116 -0
  162. package/src/config.js +48 -0
  163. package/src/context-budget.js +14 -0
  164. package/src/context-inspection.js +204 -0
  165. package/src/context.js +154 -0
  166. package/src/conventions.js +283 -0
  167. package/src/coordinated-api.js +247 -0
  168. package/src/deferred-launch.js +82 -0
  169. package/src/deferred-runs.js +515 -0
  170. package/src/dependency-health.js +35 -0
  171. package/src/diff.js +43 -0
  172. package/src/docs-help.js +316 -0
  173. package/src/doctor.js +232 -0
  174. package/src/documentation-changes.js +20 -0
  175. package/src/engine.js +5258 -0
  176. package/src/env.js +44 -0
  177. package/src/execution-budget.js +8 -0
  178. package/src/experience.js +50 -0
  179. package/src/extensions.js +144 -0
  180. package/src/filesystem-access.js +107 -0
  181. package/src/filesystem-policy.js +238 -0
  182. package/src/filtered-task-limits.js +93 -0
  183. package/src/filtered-tasks-api.js +53 -0
  184. package/src/filtered-tasks.js +576 -0
  185. package/src/frame-policy.js +66 -0
  186. package/src/fs.js +57 -0
  187. package/src/git.js +626 -0
  188. package/src/github-review-threads.js +181 -0
  189. package/src/github.js +538 -0
  190. package/src/http.js +69 -0
  191. package/src/improve-settings.js +38 -0
  192. package/src/improve.js +187 -0
  193. package/src/integrations.js +373 -0
  194. package/src/integrity.js +259 -0
  195. package/src/journey-api.js +77 -0
  196. package/src/journey-replay.js +65 -0
  197. package/src/journey-store.js +97 -0
  198. package/src/journeys.js +60 -0
  199. package/src/json.js +81 -0
  200. package/src/launch-profiles.js +292 -0
  201. package/src/learnings.js +80 -0
  202. package/src/local-app-url.js +19 -0
  203. package/src/log.js +39 -0
  204. package/src/maintenance.js +335 -0
  205. package/src/memory-analysis.js +182 -0
  206. package/src/memory-claims.js +153 -0
  207. package/src/memory-sqlite.js +236 -0
  208. package/src/memory.js +552 -0
  209. package/src/merge-validation.js +58 -0
  210. package/src/model-catalog.js +239 -0
  211. package/src/model-research.js +91 -0
  212. package/src/notes.js +176 -0
  213. package/src/notifications.js +413 -0
  214. package/src/patterns/auto-team-model.js +230 -0
  215. package/src/patterns/auto-team.js +816 -0
  216. package/src/patterns/council.js +199 -0
  217. package/src/patterns/debate.js +188 -0
  218. package/src/patterns/findings.js +141 -0
  219. package/src/patterns/index.js +21 -0
  220. package/src/patterns/loop.js +158 -0
  221. package/src/patterns/pipeline.js +125 -0
  222. package/src/patterns/prompts.js +181 -0
  223. package/src/patterns/race.js +147 -0
  224. package/src/patterns/schemas.js +40 -0
  225. package/src/patterns/solo.js +46 -0
  226. package/src/patterns/supervisor.js +235 -0
  227. package/src/patterns/task-draft.js +29 -0
  228. package/src/patterns/task-review.js +45 -0
  229. package/src/patterns/tdd.js +231 -0
  230. package/src/patterns/team-strategies.js +67 -0
  231. package/src/performance.js +43 -0
  232. package/src/persistence-status.js +35 -0
  233. package/src/phone-sessions.js +96 -0
  234. package/src/preview-overlay.js +392 -0
  235. package/src/preview.js +230 -0
  236. package/src/privacy.js +23 -0
  237. package/src/project-report.js +22 -0
  238. package/src/project-team-budget.js +22 -0
  239. package/src/project-team-model.js +225 -0
  240. package/src/project-team-policy.js +42 -0
  241. package/src/project-team-store.js +234 -0
  242. package/src/project-team-usage.js +24 -0
  243. package/src/project-teams-api.js +43 -0
  244. package/src/project-teams.js +644 -0
  245. package/src/projects.js +290 -0
  246. package/src/provider-eligibility.js +9 -0
  247. package/src/readiness.js +98 -0
  248. package/src/remote-tls.js +66 -0
  249. package/src/remote-workers/bundles.js +198 -0
  250. package/src/remote-workers/client.js +332 -0
  251. package/src/remote-workers/durability.js +37 -0
  252. package/src/remote-workers/protocol.js +68 -0
  253. package/src/remote-workers/stdio.js +138 -0
  254. package/src/remote-workers/worker.js +304 -0
  255. package/src/remote.js +204 -0
  256. package/src/repo-config.js +201 -0
  257. package/src/repo-coordination.js +725 -0
  258. package/src/repo-tree.js +106 -0
  259. package/src/repository-identity.js +13 -0
  260. package/src/retention-policy.js +466 -0
  261. package/src/review-inbox.js +38 -0
  262. package/src/routing.js +49 -0
  263. package/src/run-analysis.js +111 -0
  264. package/src/run-cold-store.js +231 -0
  265. package/src/run-environment.js +15 -0
  266. package/src/run-history.js +91 -0
  267. package/src/run-log.js +160 -0
  268. package/src/run-permissions.js +111 -0
  269. package/src/run-projection.js +77 -0
  270. package/src/run-recovery.js +53 -0
  271. package/src/run-status.js +56 -0
  272. package/src/serve.js +138 -0
  273. package/src/server.js +2871 -0
  274. package/src/shared-access.js +103 -0
  275. package/src/shared-accounts.js +381 -0
  276. package/src/shared-api.js +239 -0
  277. package/src/shell.js +346 -0
  278. package/src/ship.js +344 -0
  279. package/src/slots.js +74 -0
  280. package/src/snapshot.js +388 -0
  281. package/src/source-context.js +17 -0
  282. package/src/sse-writer.js +70 -0
  283. package/src/ssh-projects/bootstrap.mjs +95 -0
  284. package/src/ssh-projects/config.js +45 -0
  285. package/src/ssh-projects/gateway.js +143 -0
  286. package/src/ssh-projects/manager.js +320 -0
  287. package/src/ssh-projects/team-autostart.mjs +93 -0
  288. package/src/ssh-projects/team-remove.mjs +69 -0
  289. package/src/ssh-projects/team-status.mjs +39 -0
  290. package/src/ssh-projects/transport.js +133 -0
  291. package/src/stats.js +139 -0
  292. package/src/store.js +544 -0
  293. package/src/stream.js +91 -0
  294. package/src/sweeps.js +241 -0
  295. package/src/tailscale.js +101 -0
  296. package/src/task-draft.js +23 -0
  297. package/src/task-team.js +164 -0
  298. package/src/team-evaluation.js +243 -0
  299. package/src/team-mailbox.js +160 -0
  300. package/src/team-memory.js +84 -0
  301. package/src/team-rerun-graph.js +15 -0
  302. package/src/team-reruns.js +130 -0
  303. package/src/team-scheduler.js +121 -0
  304. package/src/team-steering.js +21 -0
  305. package/src/team-template-store.js +98 -0
  306. package/src/team-templates.js +117 -0
  307. package/src/team-tool-client.mjs +79 -0
  308. package/src/team-tools.js +235 -0
  309. package/src/teams.js +0 -0
  310. package/src/templates.js +62 -0
  311. package/src/transcript.js +134 -0
  312. package/src/verification-integrity.js +27 -0
  313. package/src/verification.js +183 -0
  314. package/src/watch.js +195 -0
  315. package/src/work-queue.js +47 -0
  316. package/src/workflow-control.js +83 -0
  317. package/src/workspace-api.js +207 -0
  318. package/src/workspace-changes.js +61 -0
  319. package/src/workspace.js +146 -0
@@ -0,0 +1,595 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { constants } from "node:fs";
3
+ import { chmod, lstat, mkdir, open, realpath, rename, rm } from "node:fs/promises";
4
+ import { hostname } from "node:os";
5
+ import { dirname, isAbsolute, join } from "node:path";
6
+ import { Readable } from "node:stream";
7
+ import { pipeline } from "node:stream/promises";
8
+ import { CODEFLOW_HOME } from "../config.js";
9
+ import { repoRoot } from "../git.js";
10
+ import { readJson, send } from "../http.js";
11
+ import { logError } from "../log.js";
12
+ import { readBounded, readError } from "./hub-client.js";
13
+ import { readBounded as readBounded2 } from "./hub-client.js";
14
+ import { CollabError, MAX, PATH_ID, REPO_KEY, RUNNER_ID, RUNNER_TOKEN, validatePathId, validateRepoKey, validateText } from "./model.js";
15
+ const READ_LIMIT = 1024 * 1024, JSON_LIMIT = 16 * 1024 * 1024, PROXY_BODY_LIMIT = 64 * 1024, DEFAULT_TIMEOUT_MS = 3e4, SSE_IDLE_MS = 6e4, BACKOFF_MIN_MS = 1e3, BACKOFF_MAX_MS = 3e4, MAX_MAPPINGS = 500, MAX_PATH = 4096;
16
+ const LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]"]), TRUSTS = ["safe", "standard", "trusted", "full"];
17
+ const PASSTHROUGH_HEADERS = ["content-type", "content-length", "x-codeflow-sha256", "content-disposition", "cache-control"];
18
+ const object = (v) => !!v && typeof v === "object" && !Array.isArray(v);
19
+ const UNSAFE_KEYS = ["__proto__", "constructor", "prototype"];
20
+ const empty = () => ({ version: 1, mappings: {} });
21
+ class InvalidLink extends Error {
22
+ }
23
+ function invalid(what) {
24
+ throw new InvalidLink(what);
25
+ }
26
+ const sleep = (ms, signal) => new Promise((resolve) => {
27
+ if (signal.aborted) return resolve();
28
+ const timer = setTimeout(done, ms);
29
+ function done() {
30
+ signal.removeEventListener("abort", done);
31
+ clearTimeout(timer);
32
+ resolve();
33
+ }
34
+ signal.addEventListener("abort", done, { once: true });
35
+ });
36
+ async function* tapped(source, onChunk) {
37
+ for await (const chunk of source) {
38
+ onChunk();
39
+ yield chunk;
40
+ }
41
+ }
42
+ const TLS_CODES = { DEPTH_ZERO_SELF_SIGNED_CERT: "a self-signed certificate", SELF_SIGNED_CERT_IN_CHAIN: "a self-signed certificate in its chain", UNABLE_TO_VERIFY_LEAF_SIGNATURE: "a certificate signed by an authority this computer does not know", UNABLE_TO_GET_ISSUER_CERT_LOCALLY: "a certificate signed by an authority this computer does not know", CERT_HAS_EXPIRED: "an expired certificate", ERR_TLS_CERT_ALTNAME_INVALID: "a certificate issued for another name" };
43
+ function validateHubUrl(value) {
44
+ const hint = "Enter the team server's address as https://host[:port] (http:// only for 127.0.0.1 or localhost)";
45
+ if (typeof value !== "string" || !value.trim() || value.length > 2048) throw new CollabError(hint);
46
+ let url;
47
+ try {
48
+ url = new URL(value.trim());
49
+ } catch {
50
+ throw new CollabError(hint);
51
+ }
52
+ if (url.username || url.password || url.search || url.hash || url.pathname !== "/" && url.pathname !== "") throw new CollabError(hint);
53
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname))) throw new CollabError(hint);
54
+ return url.origin;
55
+ }
56
+ function validateUser(v) {
57
+ if (!object(v) || typeof v.userId !== "string" || !PATH_ID.test(v.userId) || UNSAFE_KEYS.includes(v.userId) || typeof v.username !== "string" || !v.username || v.username.length > 64 || typeof v.displayName !== "string" || !v.displayName || v.displayName.length > MAX.displayName) invalid("link user has an unexpected format");
58
+ return { userId: v.userId, username: v.username, displayName: v.displayName };
59
+ }
60
+ function validateMappings(v) {
61
+ if (!object(v)) invalid("mappings is not an object");
62
+ const mappings = {};
63
+ let count = 0;
64
+ for (const [projectId, repos] of Object.entries(v)) {
65
+ if (!PATH_ID.test(projectId) || UNSAFE_KEYS.includes(projectId) || !object(repos)) invalid(`mapping for project ${projectId.slice(0, 40)} has an unexpected format`);
66
+ const entry = {};
67
+ for (const [key, path] of Object.entries(repos)) {
68
+ if (!REPO_KEY.test(key) || typeof path !== "string" || !isAbsolute(path) || path.length > MAX_PATH) invalid(`mapping ${key.slice(0, 40)} has an unexpected format`);
69
+ if (++count > MAX_MAPPINGS) invalid(`more than ${MAX_MAPPINGS} mappings`);
70
+ entry[key] = path;
71
+ }
72
+ mappings[projectId] = entry;
73
+ }
74
+ return mappings;
75
+ }
76
+ function validateDefaults(v) {
77
+ if (v === void 0) return void 0;
78
+ if (!object(v) || Object.keys(v).some((key) => !["pattern", "roles", "trust"].includes(key))) invalid("defaults has an unexpected format");
79
+ const defaults = {};
80
+ if (v.pattern !== void 0) {
81
+ if (typeof v.pattern !== "string" || !/^[a-z0-9-]{1,40}$/.test(v.pattern)) invalid("default pattern");
82
+ defaults.pattern = v.pattern;
83
+ }
84
+ if (v.trust !== void 0) {
85
+ if (typeof v.trust !== "string" || !TRUSTS.includes(v.trust)) invalid("default trust");
86
+ defaults.trust = v.trust;
87
+ }
88
+ if (v.roles !== void 0) {
89
+ if (!object(v.roles)) invalid("default roles");
90
+ const roles = {};
91
+ for (const [role, list] of Object.entries(v.roles)) {
92
+ if (!/^[\w-]{1,40}$/.test(role) || !Array.isArray(list) || list.length > 6 || list.some((id) => typeof id !== "string" || id.length > 80)) invalid("default roles");
93
+ roles[role] = list;
94
+ }
95
+ defaults.roles = roles;
96
+ }
97
+ return defaults;
98
+ }
99
+ function validateDocument(raw) {
100
+ if (!object(raw)) invalid("not an object");
101
+ const extra = Object.keys(raw).find((key) => !["version", "url", "runnerId", "token", "user", "hubName", "machineName", "mappings", "defaults"].includes(key));
102
+ if (extra !== void 0) invalid(`unknown field ${JSON.stringify(extra.slice(0, 40))}`);
103
+ if (raw.version !== 1) invalid("version is not 1");
104
+ const document = { version: 1, mappings: validateMappings(raw.mappings ?? {}) }, defaults = validateDefaults(raw.defaults);
105
+ if (defaults) document.defaults = defaults;
106
+ const link = [raw.url, raw.runnerId, raw.token, raw.user, raw.machineName].filter((value) => value !== void 0).length;
107
+ if (link === 0) return document;
108
+ if (link !== 5) invalid("the link is incomplete (url, runnerId, token, user and machineName belong together)");
109
+ try {
110
+ document.url = validateHubUrl(raw.url);
111
+ } catch {
112
+ return invalid("url is not a team server address");
113
+ }
114
+ if (typeof raw.runnerId !== "string" || !RUNNER_ID.test(raw.runnerId)) invalid("runnerId has an unexpected format");
115
+ if (typeof raw.token !== "string" || !RUNNER_TOKEN.test(raw.token)) invalid("token has an unexpected format");
116
+ document.runnerId = raw.runnerId;
117
+ document.token = raw.token;
118
+ document.user = validateUser(raw.user);
119
+ if (typeof raw.machineName !== "string" || !raw.machineName.trim() || raw.machineName.length > MAX.runnerName) invalid("machineName has an unexpected format");
120
+ document.machineName = raw.machineName;
121
+ if (raw.hubName !== void 0) {
122
+ if (typeof raw.hubName !== "string" || raw.hubName.length > MAX.displayName) invalid("hubName has an unexpected format");
123
+ if (raw.hubName) document.hubName = raw.hubName;
124
+ }
125
+ return document;
126
+ }
127
+ async function readDocument(file) {
128
+ let body;
129
+ try {
130
+ const handle = await open(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
131
+ try {
132
+ const info = await handle.stat();
133
+ if (!info.isFile()) throw new Error(`${file} is not a regular file`);
134
+ if (info.size > READ_LIMIT) throw new InvalidLink("larger than 1 MiB");
135
+ body = await handle.readFile("utf8");
136
+ } finally {
137
+ await handle.close();
138
+ }
139
+ } catch (error) {
140
+ if (error.code === "ENOENT") return void 0;
141
+ throw error;
142
+ }
143
+ try {
144
+ return JSON.parse(body);
145
+ } catch (error) {
146
+ throw new InvalidLink(`not valid JSON (${error.message})`);
147
+ }
148
+ }
149
+ async function* sseFrames(source) {
150
+ let buffer = "";
151
+ const frame = (block) => {
152
+ const data = [];
153
+ let id;
154
+ for (const line of block.split("\n")) {
155
+ if (line.startsWith("data:")) data.push(line.slice(5).replace(/^ /, ""));
156
+ else if (line.startsWith("id:")) id = line.slice(3).trim();
157
+ }
158
+ if (!data.length) return void 0;
159
+ const result = { data: data.join("\n") };
160
+ if (id !== void 0) result.id = id;
161
+ return result;
162
+ };
163
+ for await (const chunk of source) {
164
+ buffer += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
165
+ if (buffer.length > READ_LIMIT) throw new Error("The event stream sent a frame larger than 1 MiB");
166
+ let at;
167
+ while ((at = buffer.search(/\r?\n\r?\n/)) >= 0) {
168
+ const block = buffer.slice(0, at).replace(/\r\n/g, "\n");
169
+ buffer = buffer.slice(at).replace(/^\r?\n\r?\n/, "");
170
+ const parsed = frame(block);
171
+ if (parsed) yield parsed;
172
+ }
173
+ }
174
+ }
175
+ class TeamLink {
176
+ file;
177
+ document = empty();
178
+ loading;
179
+ connection = "offline";
180
+ connectionError;
181
+ lastEventAt;
182
+ stopEvents;
183
+ fetchImpl;
184
+ now;
185
+ defaultMachineName;
186
+ sseIdleMs;
187
+ constructor(options = {}) {
188
+ this.file = options.file ?? join(CODEFLOW_HOME, "team-link.json");
189
+ this.fetchImpl = options.fetch ?? ((input, init) => fetch(input, init));
190
+ this.now = options.now ?? Date.now;
191
+ this.defaultMachineName = (options.machineName ?? hostname() ?? "").trim().slice(0, MAX.runnerName) || "this computer";
192
+ this.sseIdleMs = options.sseIdleMs && options.sseIdleMs > 0 ? options.sseIdleMs : SSE_IDLE_MS;
193
+ }
194
+ load() {
195
+ return this.loading ??= (async () => {
196
+ let raw;
197
+ try {
198
+ raw = await readDocument(this.file);
199
+ } catch (error) {
200
+ if (error instanceof InvalidLink) {
201
+ await this.setAside(error.message);
202
+ return;
203
+ }
204
+ logError(`Couldn't read ${this.file}; this computer is treated as not linked until the file can be read`, error);
205
+ return;
206
+ }
207
+ if (raw === void 0) return;
208
+ try {
209
+ this.document = validateDocument(raw);
210
+ } catch (error) {
211
+ await this.setAside(error instanceof Error ? error.message : String(error));
212
+ }
213
+ })();
214
+ }
215
+ async setAside(why) {
216
+ const aside = `${this.file}.corrupt-${new Date(this.now()).toISOString().replace(/[:.]/g, "-")}`;
217
+ try {
218
+ await rename(this.file, aside);
219
+ logError(`${this.file} is unusable (${why}): moved it to ${aside}; connect this computer again in Settings \u2192 Team`);
220
+ } catch (error) {
221
+ logError(`${this.file} is unusable (${why}) and couldn't be moved aside; connect this computer again after fixing it`, error);
222
+ }
223
+ this.document = empty();
224
+ }
225
+ async save() {
226
+ validateDocument(this.document);
227
+ const body = JSON.stringify(this.document, null, 2);
228
+ await mkdir(dirname(this.file), { recursive: true, mode: 448 });
229
+ try {
230
+ const info = await lstat(this.file);
231
+ if (!info.isFile()) throw new CollabError(`Refusing to replace ${this.file}: it is not a regular file`, 503);
232
+ } catch (error) {
233
+ if (error.code !== "ENOENT") throw error;
234
+ }
235
+ const temp = `${this.file}.${randomBytes(6).toString("hex")}.tmp`;
236
+ try {
237
+ const handle = await open(temp, "wx", 384);
238
+ try {
239
+ await handle.writeFile(body);
240
+ await handle.sync();
241
+ } finally {
242
+ await handle.close();
243
+ }
244
+ await rename(temp, this.file);
245
+ await chmod(this.file, 384);
246
+ if (process.platform !== "win32") {
247
+ const dir = await open(dirname(this.file), "r");
248
+ try {
249
+ await dir.sync();
250
+ } finally {
251
+ await dir.close();
252
+ }
253
+ }
254
+ } finally {
255
+ await rm(temp, { force: true }).catch(() => {
256
+ });
257
+ }
258
+ }
259
+ get linked() {
260
+ return !!(this.document.url && this.document.token && this.document.runnerId && this.document.user && this.document.machineName);
261
+ }
262
+ /** Everything but the token. */
263
+ get config() {
264
+ const d = this.document, config = { linked: this.linked, mappings: structuredClone(d.mappings), connection: this.connection };
265
+ if (this.linked) {
266
+ config.url = d.url;
267
+ config.runnerId = d.runnerId;
268
+ config.user = { ...d.user };
269
+ config.machineName = d.machineName;
270
+ if (d.hubName) config.hubName = d.hubName;
271
+ } else if (!d.machineName) config.machineName = this.defaultMachineName;
272
+ if (d.defaults) config.defaults = structuredClone(d.defaults);
273
+ const { connection: _connection, ...rest } = config;
274
+ return rest;
275
+ }
276
+ state() {
277
+ const state = { ...this.config, connection: this.linked ? this.connection : "offline" };
278
+ if (this.lastEventAt !== void 0) {
279
+ state.lastEventAt = this.lastEventAt;
280
+ state.lastSync = this.lastEventAt;
281
+ }
282
+ if (this.connectionError && this.linked) state.error = this.connectionError;
283
+ return state;
284
+ }
285
+ requireLink() {
286
+ const d = this.document;
287
+ if (!this.linked) throw new CollabError("This computer is not connected to a team server; connect it in Settings \u2192 Team", 409);
288
+ return { url: d.url, token: d.token, runnerId: d.runnerId, user: d.user, machineName: d.machineName };
289
+ }
290
+ get url() {
291
+ return this.requireLink().url;
292
+ }
293
+ get runnerId() {
294
+ return this.requireLink().runnerId;
295
+ }
296
+ get runnerName() {
297
+ return this.requireLink().machineName;
298
+ }
299
+ /** Trades the account password for a runner credential once; the password is never stored. */
300
+ async connect(input) {
301
+ await this.load();
302
+ if (this.linked) throw new CollabError(`This computer is already connected to ${this.document.url}; disconnect it first`, 409);
303
+ const url = validateHubUrl(input.url), username = validateText(input.username, 64, "username", { required: true, singleLine: true }), name = validateText(input.name === void 0 || input.name === "" ? this.defaultMachineName : input.name, MAX.runnerName, "machine name", { required: true, singleLine: true });
304
+ if (typeof input.password !== "string" || !input.password || input.password.length > 1024) throw new CollabError("Enter the password of your shared account");
305
+ let response;
306
+ try {
307
+ response = await this.rawFetch(`${url}/api/team/runners/connect`, { method: "POST", headers: { accept: "application/json", "content-type": "application/json" }, body: JSON.stringify({ username, password: input.password, name }) }, DEFAULT_TIMEOUT_MS);
308
+ } catch (error) {
309
+ throw unreachable(error, url);
310
+ }
311
+ if (!response.ok) {
312
+ const error = await readError(response, true);
313
+ throw new CollabError(error.message, [400, 401, 403, 409, 429, 503].includes(response.status) ? response.status : 502);
314
+ }
315
+ const body = await readBounded(response, 64 * 1024).then((text) => JSON.parse(text)).catch(() => void 0);
316
+ if (!object(body) || !object(body.runner) || typeof body.runner.id !== "string" || !RUNNER_ID.test(body.runner.id) || typeof body.token !== "string" || !RUNNER_TOKEN.test(body.token)) throw new CollabError("The team server answered the connect request with an unexpected body; make sure it runs a code-flow with team collaboration", 502);
317
+ let user;
318
+ try {
319
+ user = validateUser(body.user);
320
+ } catch {
321
+ throw new CollabError("The team server answered the connect request without a usable account; make sure it runs a code-flow with team collaboration", 502);
322
+ }
323
+ const hubName = object(body.hub) && typeof body.hub.name === "string" && body.hub.name ? body.hub.name.slice(0, MAX.displayName) : void 0;
324
+ const document = { ...this.document, url, runnerId: body.runner.id, token: body.token, user, machineName: name };
325
+ if (hubName) document.hubName = hubName;
326
+ else delete document.hubName;
327
+ const previous = this.document;
328
+ this.document = document;
329
+ try {
330
+ await this.save();
331
+ } catch (error) {
332
+ this.document = previous;
333
+ throw error;
334
+ }
335
+ this.connection = "offline";
336
+ this.connectionError = void 0;
337
+ this.lastEventAt = void 0;
338
+ return this.state();
339
+ }
340
+ /** Revokes the credential on the hub when it can be reached, and forgets it here either way; mappings stay for the next link or for this instance's own board. */
341
+ async disconnect() {
342
+ await this.load();
343
+ this.stopEvents?.();
344
+ const d = this.document;
345
+ if (this.linked) {
346
+ try {
347
+ const response = await this.rawFetch(`${d.url}/api/team/runners/${encodeURIComponent(d.runnerId)}`, { method: "DELETE", headers: { accept: "application/json", authorization: `Bearer ${d.token}` } }, 5e3);
348
+ await response.body?.cancel().catch(() => void 0);
349
+ } catch (error) {
350
+ logError("The team server could not be told to revoke this computer's credential; revoke it there in Settings \u2192 Team", error);
351
+ }
352
+ }
353
+ this.document = { version: 1, mappings: d.mappings };
354
+ if (d.defaults) this.document.defaults = d.defaults;
355
+ this.connection = "offline";
356
+ this.connectionError = void 0;
357
+ this.lastEventAt = void 0;
358
+ await this.save();
359
+ }
360
+ /** Maps a project's repository key to a clone on this computer; an empty path removes the mapping. */
361
+ async setMapping(projectId, repoKey, path) {
362
+ await this.load();
363
+ const project = validatePathId(projectId, "project id"), key = validateRepoKey(repoKey);
364
+ if (path === "" || path === null || path === void 0) {
365
+ const repos2 = this.document.mappings[project];
366
+ if (repos2) {
367
+ delete repos2[key];
368
+ if (!Object.keys(repos2).length) delete this.document.mappings[project];
369
+ await this.save();
370
+ }
371
+ return structuredClone(this.document.mappings);
372
+ }
373
+ if (typeof path !== "string" || !isAbsolute(path) || path.length > MAX_PATH) throw new CollabError("Enter the absolute path of your clone of this repository");
374
+ let root;
375
+ try {
376
+ root = await realpath(await repoRoot(path));
377
+ } catch {
378
+ throw new CollabError(`${path} is not inside a Git repository; choose a clone of ${key}`);
379
+ }
380
+ const count = Object.values(this.document.mappings).reduce((sum, repos2) => sum + Object.keys(repos2).length, 0);
381
+ if (count >= MAX_MAPPINGS && !this.document.mappings[project]?.[key]) throw new CollabError(`At most ${MAX_MAPPINGS} repositories can be mapped on one computer`);
382
+ const repos = this.document.mappings[project] ?? (this.document.mappings[project] = {});
383
+ repos[key] = root;
384
+ await this.save();
385
+ return structuredClone(this.document.mappings);
386
+ }
387
+ resolveRepo(projectId, repoKey) {
388
+ return Object.hasOwn(this.document.mappings, projectId) ? this.document.mappings[projectId][repoKey] : void 0;
389
+ }
390
+ /** Every call goes to the validated hub origin and nowhere else: a redirect is an error (the password and the bearer-authenticated bodies would otherwise be re-sent to whatever address a 307 names, and that address's answers taken for the hub's), like every other credentialed outbound call in code-flow. */
391
+ async rawFetch(url, init, timeoutMs, signal) {
392
+ const controller = new AbortController(), timer = timeoutMs > 0 ? setTimeout(() => controller.abort(new CollabError(`The team server did not answer within ${Math.round(timeoutMs / 1e3)} s`, 504)), timeoutMs) : void 0;
393
+ timer?.unref?.();
394
+ try {
395
+ return await this.fetchImpl(url, { ...init, redirect: "error", signal: signal ? AbortSignal.any([signal, controller.signal]) : controller.signal });
396
+ } finally {
397
+ if (timer) clearTimeout(timer);
398
+ }
399
+ }
400
+ /** Every hub call carries the bearer; JSON bodies are tagged so the hub's `readJson` accepts them. */
401
+ async request(path, init = {}) {
402
+ const link = this.requireLink();
403
+ const headers = { accept: "application/json", ...init.headers, authorization: `Bearer ${link.token}` };
404
+ let body = init.body;
405
+ if (init.json !== void 0) {
406
+ body = JSON.stringify(init.json);
407
+ headers["content-type"] = "application/json";
408
+ }
409
+ try {
410
+ return await this.rawFetch(`${link.url}${path}`, { method: init.method ?? "GET", headers, body: body ?? null, ...init.duplex ? { duplex: init.duplex } : {} }, init.timeoutMs ?? DEFAULT_TIMEOUT_MS, init.signal);
411
+ } catch (error) {
412
+ throw unreachable(error, link.url);
413
+ }
414
+ }
415
+ async json(path, init = {}) {
416
+ const response = await this.request(path, init);
417
+ if (!response.ok) throw await readError(response);
418
+ if (response.status === 204) return void 0;
419
+ const timeoutMs = init.timeoutMs ?? DEFAULT_TIMEOUT_MS, timer = timeoutMs > 0 ? setTimeout(() => void response.body?.cancel(new CollabError(`The team server did not finish its answer within ${Math.round(timeoutMs / 1e3)} s`, 504)).catch(() => void 0), timeoutMs) : void 0;
420
+ timer?.unref?.();
421
+ let text;
422
+ try {
423
+ text = await readBounded(response, JSON_LIMIT);
424
+ } catch (error) {
425
+ throw unreachable(error, this.document.url ?? "");
426
+ } finally {
427
+ if (timer) clearTimeout(timer);
428
+ }
429
+ if (!text.trim()) return void 0;
430
+ try {
431
+ return JSON.parse(text);
432
+ } catch {
433
+ throw new CollabError("The team server answered with something other than JSON", 502);
434
+ }
435
+ }
436
+ /** Follows the hub's `/api/team/events` stream, reconnecting with 1→30 s backoff; the cursor `<epoch>:<seq>` resumes where the last frame left off. */
437
+ events(onEvent, onState) {
438
+ this.stopEvents?.();
439
+ const controller = new AbortController();
440
+ let stopped = false, delay = BACKOFF_MIN_MS, cursor, epoch;
441
+ const setState = (connection, error) => {
442
+ if (stopped && connection !== "offline") return;
443
+ this.connection = connection;
444
+ this.connectionError = error;
445
+ try {
446
+ onState?.(connection, error);
447
+ } catch {
448
+ }
449
+ };
450
+ const loop = async () => {
451
+ while (!stopped && this.linked) {
452
+ try {
453
+ const response = await this.request(`/api/team/events${cursor ? `?after=${encodeURIComponent(cursor)}` : ""}`, { headers: { accept: "text/event-stream" }, signal: controller.signal });
454
+ if (response.status === 401 || response.status === 403) {
455
+ const error = await readError(response);
456
+ setState("offline", error.message);
457
+ delay = BACKOFF_MAX_MS;
458
+ } else if (!response.ok || !response.body) {
459
+ await response.body?.cancel().catch(() => void 0);
460
+ setState("reconnecting", `The team server answered HTTP ${response.status} for its event stream`);
461
+ } else {
462
+ delay = BACKOFF_MIN_MS;
463
+ setState("connected");
464
+ const source = Readable.fromWeb(response.body);
465
+ const arm = () => {
466
+ const timer = setTimeout(() => source.destroy(new Error(`no event or ping for ${Math.round(this.sseIdleMs / 1e3)} s`)), this.sseIdleMs);
467
+ timer.unref?.();
468
+ return timer;
469
+ };
470
+ let idle = arm();
471
+ const abort = () => source.destroy();
472
+ controller.signal.addEventListener("abort", abort, { once: true });
473
+ try {
474
+ for await (const frame of sseFrames(tapped(source, () => {
475
+ clearTimeout(idle);
476
+ idle = arm();
477
+ }))) {
478
+ this.lastEventAt = this.now();
479
+ let event;
480
+ try {
481
+ event = JSON.parse(frame.data);
482
+ } catch {
483
+ continue;
484
+ }
485
+ if (!object(event)) continue;
486
+ if (event.type === "hello" && typeof event.epoch === "string") epoch = event.epoch;
487
+ if (event.type === "resync") {
488
+ cursor = void 0;
489
+ epoch = void 0;
490
+ }
491
+ if (typeof event.seq === "number" && Number.isSafeInteger(event.seq) && epoch) cursor = `${epoch}:${event.seq}`;
492
+ else if (frame.id && /^[a-f0-9]{1,32}:\d{1,15}$/.test(frame.id)) cursor = frame.id;
493
+ try {
494
+ onEvent(event);
495
+ } catch (error) {
496
+ logError("A team event listener failed", error);
497
+ }
498
+ }
499
+ if (!stopped) setState("reconnecting", "The team server closed its event stream");
500
+ } finally {
501
+ clearTimeout(idle);
502
+ controller.signal.removeEventListener("abort", abort);
503
+ }
504
+ }
505
+ } catch (error) {
506
+ if (!stopped) setState("reconnecting", error instanceof CollabError ? error.message : `The team server's event stream ended unexpectedly (${error instanceof Error ? error.message : String(error)}); reconnecting`);
507
+ }
508
+ if (stopped) break;
509
+ await sleep(delay, controller.signal);
510
+ delay = Math.min(BACKOFF_MAX_MS, delay * 2);
511
+ }
512
+ if (!stopped) {
513
+ stopped = true;
514
+ setState("offline", this.linked ? this.connectionError : void 0);
515
+ }
516
+ };
517
+ const stop = () => {
518
+ if (stopped) return;
519
+ stopped = true;
520
+ controller.abort();
521
+ if (this.stopEvents === stop) this.stopEvents = void 0;
522
+ setState("offline");
523
+ };
524
+ this.stopEvents = stop;
525
+ void loop();
526
+ return stop;
527
+ }
528
+ close() {
529
+ this.stopEvents?.();
530
+ }
531
+ /** Forwards an owner UI request to the hub under the runner credential: same method, path and query; a JSON body is re-read and re-sent, cookies never travel, and the hub's answer comes back with its status. */
532
+ async proxy(req, res, parts, url) {
533
+ const link = this.requireLink(), method = (req.method ?? "GET").toUpperCase(), path = `/${parts.map((part) => encodeURIComponent(part)).join("/")}${url.search}`;
534
+ const headers = { accept: typeof req.headers.accept === "string" && req.headers.accept ? req.headers.accept.slice(0, 200) : "application/json" };
535
+ let body;
536
+ if (method !== "GET" && method !== "HEAD") {
537
+ const type2 = req.headers["content-type"] ?? "", length = Number(req.headers["content-length"] ?? 0);
538
+ if (type2.startsWith("application/json")) {
539
+ body = JSON.stringify(await readJson(req, PROXY_BODY_LIMIT, { timeoutMs: 1e4 }));
540
+ headers["content-type"] = "application/json";
541
+ } else if (length > 0 || req.headers["transfer-encoding"]) return send(res, 415, { error: "Team requests are forwarded as JSON only; upload checkpoint bundles from the computer that runs the task" });
542
+ }
543
+ let upstream;
544
+ try {
545
+ upstream = await this.rawFetch(`${link.url}${path}`, { method, headers: { ...headers, authorization: `Bearer ${link.token}` }, body: body ?? null }, DEFAULT_TIMEOUT_MS);
546
+ } catch (error) {
547
+ const reason = unreachable(error, link.url);
548
+ return send(res, 502, { error: `The team server is unreachable at ${link.url}; check the connection and try again (${reason.message})` });
549
+ }
550
+ const type = upstream.headers.get("content-type") ?? "";
551
+ if (!upstream.body || type.startsWith("application/json") || upstream.status === 204 || upstream.status >= 400 && !type.startsWith("text/event-stream")) {
552
+ let text;
553
+ try {
554
+ text = upstream.body ? await readBounded(upstream, JSON_LIMIT) : "";
555
+ } catch (error) {
556
+ return send(res, 502, { error: `The team server's answer could not be read: ${error instanceof Error ? error.message : String(error)}` });
557
+ }
558
+ if (type.startsWith("application/json") || upstream.status === 204 || !text) {
559
+ res.writeHead(upstream.status, { "content-type": "application/json", "cache-control": "no-store" });
560
+ res.end(text || "{}");
561
+ return;
562
+ }
563
+ return send(res, upstream.status >= 400 ? upstream.status : 502, { error: `The team server answered with ${type.split(";")[0] || "an unexpected content type"}` });
564
+ }
565
+ const passthrough = { "cache-control": "no-store" };
566
+ for (const name of PASSTHROUGH_HEADERS) {
567
+ const value = upstream.headers.get(name);
568
+ if (value) passthrough[name] = value;
569
+ }
570
+ res.writeHead(upstream.status, passthrough);
571
+ const source = Readable.fromWeb(upstream.body);
572
+ res.once("close", () => source.destroy());
573
+ try {
574
+ await pipeline(source, res);
575
+ } catch (error) {
576
+ if (!res.destroyed) res.destroy();
577
+ logError("A proxied team response ended early", error);
578
+ }
579
+ }
580
+ }
581
+ function unreachable(error, url) {
582
+ if (error instanceof CollabError) return error;
583
+ const cause = error instanceof Error && error.cause instanceof Error ? error.cause : error instanceof Error ? error : void 0, code = cause ? cause.code : void 0, detail = cause ? `${code ?? cause.name}: ${cause.message}` : String(error);
584
+ if (error instanceof Error && error.name === "AbortError") return new CollabError(`The team server at ${url} did not answer in time`, 504);
585
+ if (cause?.message === "unexpected redirect") return new CollabError(`The team server at ${url} redirected the request; code-flow talks only to the address you entered, so enter the address the server itself answers at`, 502);
586
+ if (code !== void 0 && Object.hasOwn(TLS_CODES, code)) return new CollabError(`This computer does not trust the team server's certificate at ${url} (${TLS_CODES[code]}, ${code}): give the server a Tailscale HTTPS certificate for its tailnet name, or trust its certificate here with NODE_EXTRA_CA_CERTS`, 502);
587
+ return new CollabError(`The team server at ${url} is unreachable (${detail.slice(0, 300)}); check that it runs and that this computer can reach it`, 502);
588
+ }
589
+ export {
590
+ TeamLink,
591
+ readBounded2 as readBounded,
592
+ sseFrames,
593
+ unreachable,
594
+ validateHubUrl
595
+ };