@mastra/factory 0.0.0-esbuild-bundle-worker-20260807173433

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 (329) hide show
  1. package/CHANGELOG.md +1008 -0
  2. package/LICENSE.md +30 -0
  3. package/README.md +27 -0
  4. package/dist/auth.d.ts +183 -0
  5. package/dist/auth.d.ts.map +1 -0
  6. package/dist/auth.js +507 -0
  7. package/dist/auth.js.map +1 -0
  8. package/dist/capabilities/connection.d.ts +9 -0
  9. package/dist/capabilities/connection.d.ts.map +1 -0
  10. package/dist/capabilities/connection.js +1 -0
  11. package/dist/capabilities/intake.d.ts +141 -0
  12. package/dist/capabilities/intake.d.ts.map +1 -0
  13. package/dist/capabilities/intake.js +1 -0
  14. package/dist/capabilities/version-control.d.ts +247 -0
  15. package/dist/capabilities/version-control.d.ts.map +1 -0
  16. package/dist/capabilities/version-control.js +1 -0
  17. package/dist/factory.d.ts +159 -0
  18. package/dist/factory.d.ts.map +1 -0
  19. package/dist/factory.js +491 -0
  20. package/dist/factory.js.map +1 -0
  21. package/dist/index.d.ts +14 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +7 -0
  24. package/dist/integrations/base.d.ts +254 -0
  25. package/dist/integrations/base.d.ts.map +1 -0
  26. package/dist/integrations/base.js +1 -0
  27. package/dist/integrations/github/config.d.ts +60 -0
  28. package/dist/integrations/github/config.d.ts.map +1 -0
  29. package/dist/integrations/github/config.js +42 -0
  30. package/dist/integrations/github/config.js.map +1 -0
  31. package/dist/integrations/github/integration.d.ts +220 -0
  32. package/dist/integrations/github/integration.d.ts.map +1 -0
  33. package/dist/integrations/github/integration.js +1162 -0
  34. package/dist/integrations/github/integration.js.map +1 -0
  35. package/dist/integrations/github/issue-triage.d.ts +27 -0
  36. package/dist/integrations/github/issue-triage.d.ts.map +1 -0
  37. package/dist/integrations/github/issue-triage.js +82 -0
  38. package/dist/integrations/github/issue-triage.js.map +1 -0
  39. package/dist/integrations/github/pat.d.ts +38 -0
  40. package/dist/integrations/github/pat.d.ts.map +1 -0
  41. package/dist/integrations/github/pat.js +59 -0
  42. package/dist/integrations/github/pat.js.map +1 -0
  43. package/dist/integrations/github/provenance.d.ts +27 -0
  44. package/dist/integrations/github/provenance.d.ts.map +1 -0
  45. package/dist/integrations/github/provenance.js +75 -0
  46. package/dist/integrations/github/provenance.js.map +1 -0
  47. package/dist/integrations/github/reconcile-worker.d.ts +41 -0
  48. package/dist/integrations/github/reconcile-worker.d.ts.map +1 -0
  49. package/dist/integrations/github/reconcile-worker.js +125 -0
  50. package/dist/integrations/github/reconcile-worker.js.map +1 -0
  51. package/dist/integrations/github/routes.d.ts +69 -0
  52. package/dist/integrations/github/routes.d.ts.map +1 -0
  53. package/dist/integrations/github/routes.js +1335 -0
  54. package/dist/integrations/github/routes.js.map +1 -0
  55. package/dist/integrations/github/rules.d.ts +105 -0
  56. package/dist/integrations/github/rules.d.ts.map +1 -0
  57. package/dist/integrations/github/rules.js +551 -0
  58. package/dist/integrations/github/rules.js.map +1 -0
  59. package/dist/integrations/github/sandbox-release.d.ts +53 -0
  60. package/dist/integrations/github/sandbox-release.d.ts.map +1 -0
  61. package/dist/integrations/github/sandbox-release.js +115 -0
  62. package/dist/integrations/github/sandbox-release.js.map +1 -0
  63. package/dist/integrations/github/sandbox.d.ts +281 -0
  64. package/dist/integrations/github/sandbox.d.ts.map +1 -0
  65. package/dist/integrations/github/sandbox.js +547 -0
  66. package/dist/integrations/github/sandbox.js.map +1 -0
  67. package/dist/integrations/github/session-subscriptions.d.ts +26 -0
  68. package/dist/integrations/github/session-subscriptions.d.ts.map +1 -0
  69. package/dist/integrations/github/session-subscriptions.js +192 -0
  70. package/dist/integrations/github/session-subscriptions.js.map +1 -0
  71. package/dist/integrations/github/subscriptions.d.ts +54 -0
  72. package/dist/integrations/github/subscriptions.d.ts.map +1 -0
  73. package/dist/integrations/github/subscriptions.js +62 -0
  74. package/dist/integrations/github/subscriptions.js.map +1 -0
  75. package/dist/integrations/github/token-refresh.d.ts +12 -0
  76. package/dist/integrations/github/token-refresh.d.ts.map +1 -0
  77. package/dist/integrations/github/token-refresh.js +24 -0
  78. package/dist/integrations/github/token-refresh.js.map +1 -0
  79. package/dist/integrations/github/webhook.d.ts +107 -0
  80. package/dist/integrations/github/webhook.d.ts.map +1 -0
  81. package/dist/integrations/github/webhook.js +371 -0
  82. package/dist/integrations/github/webhook.js.map +1 -0
  83. package/dist/integrations/linear/agent-tools.d.ts +34 -0
  84. package/dist/integrations/linear/agent-tools.d.ts.map +1 -0
  85. package/dist/integrations/linear/agent-tools.js +85 -0
  86. package/dist/integrations/linear/agent-tools.js.map +1 -0
  87. package/dist/integrations/linear/integration.d.ts +235 -0
  88. package/dist/integrations/linear/integration.d.ts.map +1 -0
  89. package/dist/integrations/linear/integration.js +676 -0
  90. package/dist/integrations/linear/integration.js.map +1 -0
  91. package/dist/integrations/linear/routes.d.ts +61 -0
  92. package/dist/integrations/linear/routes.d.ts.map +1 -0
  93. package/dist/integrations/linear/routes.js +240 -0
  94. package/dist/integrations/linear/routes.js.map +1 -0
  95. package/dist/integrations/linear/rules.d.ts +44 -0
  96. package/dist/integrations/linear/rules.d.ts.map +1 -0
  97. package/dist/integrations/linear/rules.js +133 -0
  98. package/dist/integrations/linear/rules.js.map +1 -0
  99. package/dist/integrations/linear/storage.d.ts +65 -0
  100. package/dist/integrations/linear/storage.d.ts.map +1 -0
  101. package/dist/integrations/linear/storage.js +1 -0
  102. package/dist/integrations/platform/api-client.d.ts +26 -0
  103. package/dist/integrations/platform/api-client.d.ts.map +1 -0
  104. package/dist/integrations/platform/api-client.js +156 -0
  105. package/dist/integrations/platform/api-client.js.map +1 -0
  106. package/dist/integrations/platform/github/event-worker.d.ts +44 -0
  107. package/dist/integrations/platform/github/event-worker.d.ts.map +1 -0
  108. package/dist/integrations/platform/github/event-worker.js +349 -0
  109. package/dist/integrations/platform/github/event-worker.js.map +1 -0
  110. package/dist/integrations/platform/github/integration.d.ts +64 -0
  111. package/dist/integrations/platform/github/integration.d.ts.map +1 -0
  112. package/dist/integrations/platform/github/integration.js +1006 -0
  113. package/dist/integrations/platform/github/integration.js.map +1 -0
  114. package/dist/integrations/platform/linear/integration.d.ts +46 -0
  115. package/dist/integrations/platform/linear/integration.d.ts.map +1 -0
  116. package/dist/integrations/platform/linear/integration.js +483 -0
  117. package/dist/integrations/platform/linear/integration.js.map +1 -0
  118. package/dist/integrations/slack/connect-route.d.ts +46 -0
  119. package/dist/integrations/slack/connect-route.d.ts.map +1 -0
  120. package/dist/integrations/slack/connect-route.js +186 -0
  121. package/dist/integrations/slack/connect-route.js.map +1 -0
  122. package/dist/integrations/slack/integration.d.ts +62 -0
  123. package/dist/integrations/slack/integration.d.ts.map +1 -0
  124. package/dist/integrations/slack/integration.js +68 -0
  125. package/dist/integrations/slack/integration.js.map +1 -0
  126. package/dist/integrations/slack/slack.d.ts +200 -0
  127. package/dist/integrations/slack/slack.d.ts.map +1 -0
  128. package/dist/integrations/slack/slack.js +411 -0
  129. package/dist/integrations/slack/slack.js.map +1 -0
  130. package/dist/integrations/workos/integration.d.ts +39 -0
  131. package/dist/integrations/workos/integration.d.ts.map +1 -0
  132. package/dist/integrations/workos/integration.js +91 -0
  133. package/dist/integrations/workos/integration.js.map +1 -0
  134. package/dist/routes/config.d.ts +235 -0
  135. package/dist/routes/config.d.ts.map +1 -0
  136. package/dist/routes/config.js +972 -0
  137. package/dist/routes/config.js.map +1 -0
  138. package/dist/routes/custom-provider-source.d.ts +52 -0
  139. package/dist/routes/custom-provider-source.d.ts.map +1 -0
  140. package/dist/routes/custom-provider-source.js +124 -0
  141. package/dist/routes/custom-provider-source.js.map +1 -0
  142. package/dist/routes/fs.d.ts +151 -0
  143. package/dist/routes/fs.d.ts.map +1 -0
  144. package/dist/routes/fs.js +708 -0
  145. package/dist/routes/fs.js.map +1 -0
  146. package/dist/routes/intake.d.ts +23 -0
  147. package/dist/routes/intake.d.ts.map +1 -0
  148. package/dist/routes/intake.js +179 -0
  149. package/dist/routes/intake.js.map +1 -0
  150. package/dist/routes/oauth.d.ts +43 -0
  151. package/dist/routes/oauth.d.ts.map +1 -0
  152. package/dist/routes/oauth.js +463 -0
  153. package/dist/routes/oauth.js.map +1 -0
  154. package/dist/routes/projects.d.ts +19 -0
  155. package/dist/routes/projects.d.ts.map +1 -0
  156. package/dist/routes/projects.js +461 -0
  157. package/dist/routes/projects.js.map +1 -0
  158. package/dist/routes/provider-credentials.d.ts +81 -0
  159. package/dist/routes/provider-credentials.d.ts.map +1 -0
  160. package/dist/routes/provider-credentials.js +86 -0
  161. package/dist/routes/provider-credentials.js.map +1 -0
  162. package/dist/routes/route.d.ts +48 -0
  163. package/dist/routes/route.d.ts.map +1 -0
  164. package/dist/routes/route.js +15 -0
  165. package/dist/routes/route.js.map +1 -0
  166. package/dist/routes/skills.d.ts +30 -0
  167. package/dist/routes/skills.d.ts.map +1 -0
  168. package/dist/routes/skills.js +168 -0
  169. package/dist/routes/skills.js.map +1 -0
  170. package/dist/routes/surface.d.ts +107 -0
  171. package/dist/routes/surface.d.ts.map +1 -0
  172. package/dist/routes/surface.js +297 -0
  173. package/dist/routes/surface.js.map +1 -0
  174. package/dist/routes/tenant-credentials.d.ts +74 -0
  175. package/dist/routes/tenant-credentials.d.ts.map +1 -0
  176. package/dist/routes/tenant-credentials.js +156 -0
  177. package/dist/routes/tenant-credentials.js.map +1 -0
  178. package/dist/routes/work-items.d.ts +40 -0
  179. package/dist/routes/work-items.d.ts.map +1 -0
  180. package/dist/routes/work-items.js +695 -0
  181. package/dist/routes/work-items.js.map +1 -0
  182. package/dist/rules/binding-context.d.ts +5 -0
  183. package/dist/rules/binding-context.d.ts.map +1 -0
  184. package/dist/rules/binding-context.js +28 -0
  185. package/dist/rules/binding-context.js.map +1 -0
  186. package/dist/rules/defaults.d.ts +9 -0
  187. package/dist/rules/defaults.d.ts.map +1 -0
  188. package/dist/rules/defaults.js +322 -0
  189. package/dist/rules/defaults.js.map +1 -0
  190. package/dist/rules/dispatcher.d.ts +43 -0
  191. package/dist/rules/dispatcher.d.ts.map +1 -0
  192. package/dist/rules/dispatcher.js +547 -0
  193. package/dist/rules/dispatcher.js.map +1 -0
  194. package/dist/rules/index.d.ts +7 -0
  195. package/dist/rules/index.d.ts.map +1 -0
  196. package/dist/rules/index.js +5 -0
  197. package/dist/rules/processor.d.ts +54 -0
  198. package/dist/rules/processor.d.ts.map +1 -0
  199. package/dist/rules/processor.js +378 -0
  200. package/dist/rules/processor.js.map +1 -0
  201. package/dist/rules/resolve.d.ts +17 -0
  202. package/dist/rules/resolve.d.ts.map +1 -0
  203. package/dist/rules/resolve.js +30 -0
  204. package/dist/rules/resolve.js.map +1 -0
  205. package/dist/rules/start-coordinator.d.ts +60 -0
  206. package/dist/rules/start-coordinator.d.ts.map +1 -0
  207. package/dist/rules/start-coordinator.js +175 -0
  208. package/dist/rules/start-coordinator.js.map +1 -0
  209. package/dist/rules/tools.d.ts +10 -0
  210. package/dist/rules/tools.d.ts.map +1 -0
  211. package/dist/rules/tools.js +59 -0
  212. package/dist/rules/tools.js.map +1 -0
  213. package/dist/rules/transition-service.d.ts +49 -0
  214. package/dist/rules/transition-service.d.ts.map +1 -0
  215. package/dist/rules/transition-service.js +232 -0
  216. package/dist/rules/transition-service.js.map +1 -0
  217. package/dist/rules/types.d.ts +261 -0
  218. package/dist/rules/types.d.ts.map +1 -0
  219. package/dist/rules/types.js +43 -0
  220. package/dist/rules/types.js.map +1 -0
  221. package/dist/rules/validation.d.ts +11 -0
  222. package/dist/rules/validation.d.ts.map +1 -0
  223. package/dist/rules/validation.js +308 -0
  224. package/dist/rules/validation.js.map +1 -0
  225. package/dist/sandbox/fleet.d.ts +222 -0
  226. package/dist/sandbox/fleet.d.ts.map +1 -0
  227. package/dist/sandbox/fleet.js +322 -0
  228. package/dist/sandbox/fleet.js.map +1 -0
  229. package/dist/sandbox/reattach.d.ts +3 -0
  230. package/dist/sandbox/reattach.d.ts.map +1 -0
  231. package/dist/sandbox/reattach.js +16 -0
  232. package/dist/sandbox/reattach.js.map +1 -0
  233. package/dist/server-error.d.ts +12 -0
  234. package/dist/server-error.d.ts.map +1 -0
  235. package/dist/server-error.js +16 -0
  236. package/dist/server-error.js.map +1 -0
  237. package/dist/session/factory-session.d.ts +116 -0
  238. package/dist/session/factory-session.d.ts.map +1 -0
  239. package/dist/session/factory-session.js +162 -0
  240. package/dist/session/factory-session.js.map +1 -0
  241. package/dist/skills/service.d.ts +48 -0
  242. package/dist/skills/service.d.ts.map +1 -0
  243. package/dist/skills/service.js +40 -0
  244. package/dist/skills/service.js.map +1 -0
  245. package/dist/spa-static.d.ts +33 -0
  246. package/dist/spa-static.d.ts.map +1 -0
  247. package/dist/spa-static.js +104 -0
  248. package/dist/spa-static.js.map +1 -0
  249. package/dist/state-signing.d.ts +59 -0
  250. package/dist/state-signing.d.ts.map +1 -0
  251. package/dist/state-signing.js +83 -0
  252. package/dist/state-signing.js.map +1 -0
  253. package/dist/storage/domains/audit/agent-audit.d.ts +27 -0
  254. package/dist/storage/domains/audit/agent-audit.d.ts.map +1 -0
  255. package/dist/storage/domains/audit/agent-audit.js +66 -0
  256. package/dist/storage/domains/audit/agent-audit.js.map +1 -0
  257. package/dist/storage/domains/audit/base.d.ts +140 -0
  258. package/dist/storage/domains/audit/base.d.ts.map +1 -0
  259. package/dist/storage/domains/audit/base.js +184 -0
  260. package/dist/storage/domains/audit/base.js.map +1 -0
  261. package/dist/storage/domains/audit/domain.d.ts +76 -0
  262. package/dist/storage/domains/audit/domain.d.ts.map +1 -0
  263. package/dist/storage/domains/audit/domain.js +269 -0
  264. package/dist/storage/domains/audit/domain.js.map +1 -0
  265. package/dist/storage/domains/channel-identity/base.d.ts +96 -0
  266. package/dist/storage/domains/channel-identity/base.d.ts.map +1 -0
  267. package/dist/storage/domains/channel-identity/base.js +149 -0
  268. package/dist/storage/domains/channel-identity/base.js.map +1 -0
  269. package/dist/storage/domains/credentials/base.d.ts +134 -0
  270. package/dist/storage/domains/credentials/base.d.ts.map +1 -0
  271. package/dist/storage/domains/credentials/base.js +273 -0
  272. package/dist/storage/domains/credentials/base.js.map +1 -0
  273. package/dist/storage/domains/custom-providers/base.d.ts +57 -0
  274. package/dist/storage/domains/custom-providers/base.d.ts.map +1 -0
  275. package/dist/storage/domains/custom-providers/base.js +145 -0
  276. package/dist/storage/domains/custom-providers/base.js.map +1 -0
  277. package/dist/storage/domains/intake/base.d.ts +38 -0
  278. package/dist/storage/domains/intake/base.d.ts.map +1 -0
  279. package/dist/storage/domains/intake/base.js +80 -0
  280. package/dist/storage/domains/intake/base.js.map +1 -0
  281. package/dist/storage/domains/integrations/base.d.ts +126 -0
  282. package/dist/storage/domains/integrations/base.d.ts.map +1 -0
  283. package/dist/storage/domains/integrations/base.js +317 -0
  284. package/dist/storage/domains/integrations/base.js.map +1 -0
  285. package/dist/storage/domains/memory-settings/base.d.ts +62 -0
  286. package/dist/storage/domains/memory-settings/base.d.ts.map +1 -0
  287. package/dist/storage/domains/memory-settings/base.js +126 -0
  288. package/dist/storage/domains/memory-settings/base.js.map +1 -0
  289. package/dist/storage/domains/model-packs/base.d.ts +49 -0
  290. package/dist/storage/domains/model-packs/base.d.ts.map +1 -0
  291. package/dist/storage/domains/model-packs/base.js +96 -0
  292. package/dist/storage/domains/model-packs/base.js.map +1 -0
  293. package/dist/storage/domains/projects/base.d.ts +58 -0
  294. package/dist/storage/domains/projects/base.d.ts.map +1 -0
  295. package/dist/storage/domains/projects/base.js +111 -0
  296. package/dist/storage/domains/projects/base.js.map +1 -0
  297. package/dist/storage/domains/queue-health/base.d.ts +52 -0
  298. package/dist/storage/domains/queue-health/base.d.ts.map +1 -0
  299. package/dist/storage/domains/queue-health/base.js +122 -0
  300. package/dist/storage/domains/queue-health/base.js.map +1 -0
  301. package/dist/storage/domains/source-control/base.d.ts +360 -0
  302. package/dist/storage/domains/source-control/base.d.ts.map +1 -0
  303. package/dist/storage/domains/source-control/base.js +914 -0
  304. package/dist/storage/domains/source-control/base.js.map +1 -0
  305. package/dist/storage/domains/source-control/inmemory.d.ts +175 -0
  306. package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -0
  307. package/dist/storage/domains/source-control/inmemory.js +380 -0
  308. package/dist/storage/domains/source-control/inmemory.js.map +1 -0
  309. package/dist/storage/domains/work-items/base.d.ts +412 -0
  310. package/dist/storage/domains/work-items/base.d.ts.map +1 -0
  311. package/dist/storage/domains/work-items/base.js +1320 -0
  312. package/dist/storage/domains/work-items/base.js.map +1 -0
  313. package/dist/storage/domains/work-items/metrics.d.ts +101 -0
  314. package/dist/storage/domains/work-items/metrics.d.ts.map +1 -0
  315. package/dist/storage/domains/work-items/metrics.js +221 -0
  316. package/dist/storage/domains/work-items/metrics.js.map +1 -0
  317. package/dist/timing.d.ts +15 -0
  318. package/dist/timing.d.ts.map +1 -0
  319. package/dist/timing.js +34 -0
  320. package/dist/timing.js.map +1 -0
  321. package/dist/workspace.d.ts +24 -0
  322. package/dist/workspace.d.ts.map +1 -0
  323. package/dist/workspace.js +287 -0
  324. package/dist/workspace.js.map +1 -0
  325. package/factory-skills/configure-factory-rules/SKILL.md +67 -0
  326. package/factory-skills/factory-plan/SKILL.md +61 -0
  327. package/factory-skills/factory-review/SKILL.md +157 -0
  328. package/factory-skills/factory-triage/SKILL.md +99 -0
  329. package/package.json +83 -0
@@ -0,0 +1,1335 @@
1
+ import { clearGithubPat, getGithubPat, getGithubPatStatus, setGithubPat } from "./pat.js";
2
+ import { listPullRequestSubscriptionsForThread, subscribeToPullRequest } from "./subscriptions.js";
3
+ import { SandboxBudgetError } from "../../sandbox/fleet.js";
4
+ import { MaterializeError, WorktreeError, commitAll, computeWorktreePath, ensureProjectSandbox, isValidGitRef as isValidGitRef$1, materializeRepo, pushBranch, teardownProjectSandbox } from "./sandbox.js";
5
+ import { reclaimDeletedSessionSandbox } from "./sandbox-release.js";
6
+ import { resolveFactoryDefaultModelId } from "../../session/factory-session.js";
7
+ import { getGithubFeatureDiagnostics, isGithubFeatureEnabled } from "./config.js";
8
+ import { handleGithubWebhook } from "./webhook.js";
9
+ import { registerApiRoute } from "@mastra/core/server";
10
+ import { randomUUID } from "crypto";
11
+ import { UniqueViolationError } from "@mastra/core/storage";
12
+ import { streamSSE } from "hono/streaming";
13
+ //#region src/integrations/github/routes.ts
14
+ /**
15
+ * Mastra `apiRoutes` for the GitHub App project feature.
16
+ *
17
+ * Registered alongside the other `/web/*` routes, behind the host auth gate.
18
+ * Every route additionally re-checks the authenticated user via the injected
19
+ * `RouteAuth` seam and scopes all rows by that user's stable id, so a user can
20
+ * only ever see and operate on their own installations and projects.
21
+ *
22
+ * When the feature is disabled (`isGithubFeatureEnabled()` false), `buildGithubRoutes`
23
+ * returns only `GET /web/github/status`, which reports `enabled:false`
24
+ * so the SPA can cleanly hide all GitHub UI.
25
+ */
26
+ const sessionOperationLocks = /* @__PURE__ */ new Map();
27
+ const MAX_SESSION_TITLE_LENGTH = 80;
28
+ const USER_SESSION_BRANCH_PREFIX = "user/session-";
29
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
30
+ /**
31
+ * Serialize same-session mutations within one Factory process. Factory sessions
32
+ * normally issue these operations sequentially, so this lock is probably not
33
+ * necessary; keep the cheap local guard until that invariant is enforced by
34
+ * the request protocol. It intentionally does not consume a database connection.
35
+ */
36
+ function withSessionOperationLock(sessionId, fn) {
37
+ const next = (sessionOperationLocks.get(sessionId) ?? Promise.resolve()).then(fn, fn);
38
+ const tail = next.then(() => void 0, () => void 0);
39
+ sessionOperationLocks.set(sessionId, tail);
40
+ tail.then(() => {
41
+ if (sessionOperationLocks.get(sessionId) === tail) sessionOperationLocks.delete(sessionId);
42
+ });
43
+ return next;
44
+ }
45
+ /** Erase a route handler's path-parameterized context to a plain `Context`. */
46
+ function loose(c) {
47
+ return c;
48
+ }
49
+ function pullRequestNumberFromUrl(value, expectedRepo) {
50
+ try {
51
+ const url = new URL(value);
52
+ const match = url.pathname.match(/^\/([^/]+\/[^/]+)\/pull\/(\d+)\/?$/);
53
+ if (url.protocol !== "https:" || url.hostname !== "github.com" || match?.[1]?.toLowerCase() !== expectedRepo.toLowerCase()) return;
54
+ const number = Number(match[2]);
55
+ return Number.isInteger(number) && number > 0 ? number : void 0;
56
+ } catch {
57
+ return;
58
+ }
59
+ }
60
+ function isCanonicalGithubIssueUrl(value, repoFullName, issueNumber) {
61
+ try {
62
+ const url = new URL(value);
63
+ const [owner, repo] = repoFullName.split("/");
64
+ return url.protocol === "https:" && url.hostname === "github.com" && url.pathname === `/${owner}/${repo}/issues/${issueNumber}` && url.search === "" && url.hash === "";
65
+ } catch {
66
+ return false;
67
+ }
68
+ }
69
+ /**
70
+ * Validate a git branch/ref name against a strict whitelist. The value is later
71
+ * interpolated into a shell `git clone --branch` command, so it must never
72
+ * contain shell metacharacters. We accept only git-ref-safe characters and
73
+ * reject anything else rather than relying on shell quoting alone.
74
+ */
75
+ function isValidGitRef(value) {
76
+ return typeof value === "string" && value.length > 0 && value.length <= 255 && /^[A-Za-z0-9_./-]+$/.test(value);
77
+ }
78
+ function isJsonObject(value) {
79
+ return typeof value === "object" && value !== null && !Array.isArray(value);
80
+ }
81
+ function normalizeSessionTitle(title) {
82
+ return [...title.replace(/\s+/g, " ").trim()].slice(0, MAX_SESSION_TITLE_LENGTH).join("").trimEnd() || null;
83
+ }
84
+ /**
85
+ * Resolve the org-scoped tenant for a GitHub request. GitHub project features
86
+ * are org-owned, so they require both a signed-in user and a WorkOS
87
+ * organization. Returns the `(orgId, userId)` tenant (with `orgId` narrowed to a
88
+ * non-null string) or a ready-to-return error response: 401 when unauthenticated,
89
+ * 403 when the user has no organization (personal account).
90
+ *
91
+ * Resolves the session from the request cookie itself (via `auth.ensureUser`)
92
+ * instead of relying on the auth gate's context stash: on platform deploys
93
+ * custom `apiRoutes` run on an isolated sub-app context where the gate's
94
+ * `c.set(...)` is invisible. When the gate stash IS visible (local Hono
95
+ * server), `auth.ensureUser` returns the cached user and this is a no-op.
96
+ */
97
+ async function resolveOrgTenant(c, auth) {
98
+ await auth.ensureUser(c);
99
+ const tenant = auth.tenant(c);
100
+ if (!tenant) return { response: c.json({ error: "unauthorized" }, 401) };
101
+ if (!tenant.orgId) return { response: c.json({
102
+ error: "organization_required",
103
+ message: "GitHub projects require a WorkOS organization. Personal accounts cannot connect repositories."
104
+ }, 403) };
105
+ return { tenant: {
106
+ orgId: tenant.orgId,
107
+ userId: tenant.userId
108
+ } };
109
+ }
110
+ /**
111
+ * Parse a 1-based `page` query param. Missing means page 1; anything that is
112
+ * not a small positive integer is rejected (`null`).
113
+ */
114
+ function parseListPage(raw) {
115
+ if (raw === void 0) return 1;
116
+ if (!/^\d{1,5}$/.test(raw)) return null;
117
+ const page = Number(raw);
118
+ return page >= 1 ? page : null;
119
+ }
120
+ const VALID_ISSUE_LABEL_FILTERS = /* @__PURE__ */ new Set(["auto-triaged", "needs-approval"]);
121
+ function parseIssueLabelFilter(raw) {
122
+ if (raw === void 0 || raw === "") return void 0;
123
+ if (VALID_ISSUE_LABEL_FILTERS.has(raw)) return raw;
124
+ return null;
125
+ }
126
+ function parseIssueNumberParam(raw) {
127
+ if (!raw || !/^\d{1,10}$/.test(raw)) return null;
128
+ const issueNumber = Number(raw);
129
+ return Number.isSafeInteger(issueNumber) && issueNumber > 0 ? issueNumber : null;
130
+ }
131
+ function parseStringList(value) {
132
+ if (!Array.isArray(value)) return [];
133
+ return value.filter((item) => typeof item === "string" && item.length > 0);
134
+ }
135
+ async function resolveProjectRepository(args) {
136
+ const projectRepository = await args.github.sourceControlStorage.projectRepositories.get({
137
+ orgId: args.orgId,
138
+ id: args.projectRepositoryId
139
+ });
140
+ if (!projectRepository) return null;
141
+ const connection = await args.github.sourceControlStorage.connections.get({
142
+ orgId: args.orgId,
143
+ id: projectRepository.connectionId
144
+ });
145
+ if (!connection) return null;
146
+ const repository = await args.github.sourceControlStorage.repositories.get({
147
+ orgId: args.orgId,
148
+ id: projectRepository.repositoryId
149
+ });
150
+ if (!repository) return null;
151
+ const installation = await args.github.sourceControlStorage.installations.get({
152
+ orgId: args.orgId,
153
+ id: connection.installationId
154
+ });
155
+ if (!installation) return null;
156
+ return {
157
+ ...projectRepository,
158
+ connection,
159
+ installation,
160
+ repository,
161
+ factoryProjectId: connection.factoryProjectId,
162
+ defaultBranch: projectRepository.branch ?? repository.defaultBranch
163
+ };
164
+ }
165
+ function polledIssueEvent(project, issue) {
166
+ const repositoryId = Number(project.repository.externalId);
167
+ const assigneeLogins = issue.assignees ?? (issue.assignee ? [issue.assignee] : []);
168
+ return {
169
+ event: "issues",
170
+ deliveryId: `poll:${repositoryId}:issue:${issue.number}:${issue.createdAt}`,
171
+ payload: {
172
+ action: "opened",
173
+ installation: { id: Number(project.installation.externalId) },
174
+ repository: {
175
+ id: repositoryId,
176
+ full_name: project.repository.slug
177
+ },
178
+ sender: { login: issue.author ?? "__unknown__" },
179
+ issue: {
180
+ number: issue.number,
181
+ title: issue.title,
182
+ html_url: issue.url,
183
+ created_at: issue.createdAt,
184
+ assignees: assigneeLogins.map((login) => ({ login })),
185
+ labels: issue.labels.map((name) => ({ name }))
186
+ }
187
+ }
188
+ };
189
+ }
190
+ function polledPullRequestEvent(project, pullRequest) {
191
+ const repositoryId = Number(project.repository.externalId);
192
+ return {
193
+ event: "pull_request",
194
+ deliveryId: `poll:${repositoryId}:pull-request:${pullRequest.number}:${pullRequest.createdAt}`,
195
+ payload: {
196
+ action: "opened",
197
+ installation: { id: Number(project.installation.externalId) },
198
+ repository: {
199
+ id: repositoryId,
200
+ full_name: project.repository.slug
201
+ },
202
+ sender: { login: pullRequest.author ?? "__unknown__" },
203
+ pull_request: {
204
+ number: pullRequest.number,
205
+ title: pullRequest.title,
206
+ html_url: pullRequest.url,
207
+ created_at: pullRequest.createdAt,
208
+ state: "open",
209
+ merged: false,
210
+ assignees: pullRequest.assignees.map((login) => ({ login })),
211
+ requested_reviewers: pullRequest.requestedReviewers.map((login) => ({ login })),
212
+ head: { ref: pullRequest.headBranch },
213
+ base: { ref: pullRequest.baseBranch }
214
+ }
215
+ }
216
+ };
217
+ }
218
+ async function ingestPolledEvents(events, ingestFactoryEvent) {
219
+ if (!ingestFactoryEvent) return;
220
+ const rejected = (await Promise.allSettled(events.map((event) => ingestFactoryEvent(event)))).find((result) => result.status === "rejected");
221
+ if (rejected) throw rejected.reason;
222
+ }
223
+ /**
224
+ * Build the GitHub routes as Mastra `apiRoutes`. When the feature is disabled,
225
+ * returns only the `status` route so the SPA can detect the disabled state.
226
+ */
227
+ function buildGithubRoutes(options) {
228
+ const routes = [];
229
+ const { auth, fleet, storage, github, stateSigner, emitAudit } = options;
230
+ const diagnostics = () => getGithubFeatureDiagnostics({
231
+ github,
232
+ auth,
233
+ appDbConfigured: storage !== void 0,
234
+ stateSigner,
235
+ fleet
236
+ });
237
+ routes.push(registerApiRoute("/web/github/status", {
238
+ method: "GET",
239
+ requiresAuth: false,
240
+ handler: async (c) => {
241
+ if (!isGithubFeatureEnabled({
242
+ github,
243
+ auth
244
+ }) || !github || !stateSigner) return c.json({
245
+ enabled: false,
246
+ connected: false,
247
+ installations: [],
248
+ reason: "missing_config",
249
+ diagnostics: diagnostics()
250
+ });
251
+ await auth.ensureUser(loose(c));
252
+ const tenant = auth.tenant(loose(c));
253
+ if (!tenant) return c.json({
254
+ error: "unauthorized",
255
+ reason: "auth_required"
256
+ }, 401);
257
+ if (!tenant.orgId) return c.json({
258
+ enabled: true,
259
+ sandboxEnabled: fleet.enabled,
260
+ organizationRequired: true,
261
+ connected: false,
262
+ installations: [],
263
+ reason: "organization_required",
264
+ diagnostics: diagnostics()
265
+ });
266
+ const rows = options.github ? await options.github.sourceControlStorage.installations.list({ orgId: tenant.orgId }) : [];
267
+ const connected = rows.length > 0;
268
+ return c.json({
269
+ enabled: true,
270
+ sandboxEnabled: fleet.enabled,
271
+ connected,
272
+ installations: rows.map((r) => ({
273
+ installationId: Number(r.externalId),
274
+ accountLogin: r.accountName,
275
+ accountType: r.accountType
276
+ })),
277
+ reason: connected ? "ready" : "not_connected",
278
+ diagnostics: diagnostics()
279
+ });
280
+ }
281
+ }));
282
+ if (!isGithubFeatureEnabled({
283
+ github,
284
+ auth
285
+ }) || !github || !stateSigner) return routes;
286
+ const signState = (orgId, userId) => stateSigner.sign(orgId, userId);
287
+ const verifyState = (state) => stateSigner.verify(state);
288
+ const { runIssueTriage } = options;
289
+ const runBoardIssueTriage = runIssueTriage ? async (input) => {
290
+ if (!input.resourceId || !input.projectPath) throw new Error("GitHub issue triage requires an explicit Factory project repository");
291
+ await github.addIssueLabels(input.installationId, input.repository, input.issueNumber, ["auto-triaged"]);
292
+ return runIssueTriage({
293
+ ...input,
294
+ defaultModelId: input.defaultModelId ?? await resolveFactoryDefaultModelId(options.projects, input.resourceId),
295
+ labels: input.labels.includes("auto-triaged") ? input.labels : [...input.labels, "auto-triaged"]
296
+ });
297
+ } : void 0;
298
+ routes.push(registerApiRoute("/web/github/subscriptions", {
299
+ method: "GET",
300
+ handler: async (c) => {
301
+ await auth.ensureUser(loose(c));
302
+ const tenant = auth.tenant(loose(c));
303
+ if (!tenant?.orgId) return c.json({ error: "unauthorized" }, 401);
304
+ const resourceId = c.req.query("resourceId");
305
+ const threadId = c.req.query("threadId");
306
+ const sessionScope = c.req.query("scope");
307
+ if (!resourceId || !threadId) return c.json({ error: "resourceId and threadId are required" }, 400);
308
+ const subscriptions = await listPullRequestSubscriptionsForThread({
309
+ orgId: tenant.orgId,
310
+ resourceId,
311
+ threadId,
312
+ sessionScope
313
+ }, github.integrationStorage);
314
+ return c.json({ subscriptions: subscriptions.map((subscription) => ({
315
+ id: subscription.id,
316
+ repoFullName: subscription.data.repositorySlug,
317
+ pullRequestNumber: Number(subscription.data.changeRequestId),
318
+ status: subscription.status,
319
+ url: `https://github.com/${subscription.data.repositorySlug}/pull/${subscription.data.changeRequestId}`
320
+ })) });
321
+ }
322
+ }), registerApiRoute("/web/github/webhook", {
323
+ method: "POST",
324
+ requiresAuth: false,
325
+ handler: async (c) => {
326
+ const result = await handleGithubWebhook(loose(c), {
327
+ github,
328
+ runIssueTriage: runBoardIssueTriage,
329
+ ingestFactoryEvent: options.ingestFactoryEvent,
330
+ ...options.controller ? {
331
+ controller: options.controller,
332
+ onTargetError: (subscription, error) => {
333
+ console.warn(`[GitHub Webhook] Delivery failed for subscription ${subscription.id} (${subscription.resourceId}/${subscription.threadId}).`, error);
334
+ }
335
+ } : {}
336
+ });
337
+ return c.json(result.body, result.status);
338
+ }
339
+ }));
340
+ const redirectUri = options.redirectUri ?? `${(options.baseUrl ?? "").replace(/\/$/, "")}/auth/github/callback`;
341
+ routes.push(registerApiRoute("/auth/github/connect", {
342
+ method: "GET",
343
+ requiresAuth: false,
344
+ handler: async (c) => {
345
+ const resolved = await resolveOrgTenant(loose(c), auth);
346
+ if ("response" in resolved) return resolved.response;
347
+ const state = signState(resolved.tenant.orgId, resolved.tenant.userId);
348
+ if (c.req.query("manage")) return c.redirect(github.buildInstallUrl(state));
349
+ return c.redirect(github.buildOAuthIdentifyUrl(state, redirectUri));
350
+ }
351
+ }));
352
+ routes.push(registerApiRoute("/auth/github/callback", {
353
+ method: "GET",
354
+ requiresAuth: false,
355
+ handler: async (c) => {
356
+ const resolved = await resolveOrgTenant(loose(c), auth);
357
+ if ("response" in resolved) return resolved.response;
358
+ const { orgId, userId } = resolved.tenant;
359
+ const state = c.req.query("state");
360
+ if (!state) return c.redirect(github.buildOAuthIdentifyUrl(signState(orgId, userId), redirectUri));
361
+ const stateTenant = verifyState(state);
362
+ if (!stateTenant || stateTenant.userId !== userId || stateTenant.orgId !== orgId) {
363
+ console.warn("[GitHub] Install callback rejected: state/tenant mismatch.", JSON.stringify({
364
+ stateValid: Boolean(stateTenant),
365
+ stateOrgId: stateTenant?.orgId,
366
+ stateUserId: stateTenant?.userId,
367
+ sessionOrgId: orgId,
368
+ sessionUserId: userId
369
+ }));
370
+ return c.redirect("/?github=error");
371
+ }
372
+ const code = c.req.query("code");
373
+ if (!code) return c.redirect(github.buildOAuthIdentifyUrl(signState(orgId, userId), redirectUri));
374
+ try {
375
+ const userToken = await github.exchangeOAuthCode(code, redirectUri);
376
+ const installations = await github.listUserInstallations(userToken);
377
+ if (installations.length === 0) return c.redirect(github.buildInstallUrl(signState(orgId, userId)));
378
+ for (const inst of installations) await github.sourceControlStorage.installations.upsert({
379
+ orgId,
380
+ connectedByUserId: userId,
381
+ externalId: inst.installationId.toString(),
382
+ accountName: inst.accountLogin,
383
+ accountType: inst.accountType
384
+ });
385
+ } catch (error) {
386
+ console.warn(`[GitHub] Install callback failed to persist installations for org ${orgId} / user ${userId}.`, error);
387
+ return c.redirect("/?github=error");
388
+ }
389
+ return c.redirect("/?github=connected");
390
+ }
391
+ }));
392
+ routes.push(registerApiRoute("/web/github/repos", {
393
+ method: "GET",
394
+ requiresAuth: false,
395
+ handler: async (c) => {
396
+ const resolved = await resolveOrgTenant(loose(c), auth);
397
+ if ("response" in resolved) return resolved.response;
398
+ const orgId = resolved.tenant.orgId;
399
+ const installs = await github.sourceControlStorage.installations.list({ orgId });
400
+ const query = (c.req.query("q") ?? "").toLowerCase();
401
+ const listed = await Promise.all(installs.map(async (inst) => {
402
+ try {
403
+ return {
404
+ inst,
405
+ list: await github.listInstallationRepos(Number(inst.externalId))
406
+ };
407
+ } catch (err) {
408
+ if (err.status !== 404) throw err;
409
+ console.error(`[Mastra Factory] pruning stale GitHub installation ${inst.externalId} (404 from GitHub)`);
410
+ await github.sourceControlStorage.installations.delete({
411
+ orgId,
412
+ id: inst.id
413
+ });
414
+ return {
415
+ inst,
416
+ list: []
417
+ };
418
+ }
419
+ }));
420
+ const matches = [];
421
+ const seenRepositoryIds = /* @__PURE__ */ new Set();
422
+ for (const { inst, list } of listed) for (const repo of list) {
423
+ if (query && !repo.fullName.toLowerCase().includes(query)) continue;
424
+ if (seenRepositoryIds.has(repo.id)) continue;
425
+ seenRepositoryIds.add(repo.id);
426
+ matches.push({
427
+ inst,
428
+ repo
429
+ });
430
+ }
431
+ const repos = new Array(matches.length);
432
+ const upsertConcurrency = 10;
433
+ for (let start = 0; start < matches.length; start += upsertConcurrency) await Promise.all(matches.slice(start, start + upsertConcurrency).map(async ({ inst, repo }, offset) => {
434
+ const repository = await github.sourceControlStorage.repositories.upsert({
435
+ orgId,
436
+ input: {
437
+ installationId: inst.id,
438
+ externalId: repo.id.toString(),
439
+ slug: repo.fullName,
440
+ defaultBranch: isValidGitRef(repo.defaultBranch) ? repo.defaultBranch : "main",
441
+ providerMetadata: {
442
+ private: repo.private,
443
+ owner: repo.owner
444
+ }
445
+ }
446
+ });
447
+ repos[start + offset] = {
448
+ ...repo,
449
+ installationStorageId: inst.id,
450
+ repositoryStorageId: repository.id,
451
+ sandboxProvider: fleet.provider,
452
+ sandboxWorkdir: fleet.computeWorkdir(repo.fullName)
453
+ };
454
+ }));
455
+ return c.json({ repos });
456
+ }
457
+ }));
458
+ routes.push(registerApiRoute("/web/github/projects/:id/ensure", {
459
+ method: "POST",
460
+ requiresAuth: false,
461
+ handler: async (c) => {
462
+ const resolved = await resolveOrgTenant(loose(c), auth);
463
+ if ("response" in resolved) return resolved.response;
464
+ const { orgId, userId } = resolved.tenant;
465
+ if (!fleet.enabled) return c.json({
466
+ error: "sandbox_not_configured",
467
+ message: "No sandbox provider is configured."
468
+ }, 503);
469
+ const projectRepositoryId = c.req.param("id");
470
+ if (!projectRepositoryId) return c.json({ error: "Project repository not found" }, 404);
471
+ const project = await resolveProjectRepository({
472
+ github,
473
+ orgId,
474
+ projectRepositoryId
475
+ });
476
+ if (!project) return c.json({ error: "Project repository not found" }, 404);
477
+ if ((c.req.header("accept") ?? "").includes("text/event-stream")) return streamSSE(loose(c), async (stream) => {
478
+ try {
479
+ const result = await prepareProject({
480
+ github,
481
+ fleet,
482
+ project,
483
+ userId,
484
+ onProgress: (ev) => void stream.writeSSE({
485
+ event: "progress",
486
+ data: JSON.stringify(ev)
487
+ })
488
+ });
489
+ await stream.writeSSE({
490
+ event: "done",
491
+ data: JSON.stringify(result)
492
+ });
493
+ } catch (err) {
494
+ await stream.writeSSE({
495
+ event: "error",
496
+ data: JSON.stringify(ensureErrorPayload(err).body)
497
+ });
498
+ }
499
+ });
500
+ try {
501
+ const result = await prepareProject({
502
+ github,
503
+ fleet,
504
+ project,
505
+ userId
506
+ });
507
+ return c.json(result);
508
+ } catch (err) {
509
+ const { status, body } = ensureErrorPayload(err);
510
+ return c.json(body, status);
511
+ }
512
+ }
513
+ }));
514
+ routes.push(registerApiRoute("/web/github/projects/:id/issues", {
515
+ method: "GET",
516
+ requiresAuth: false,
517
+ handler: async (c) => {
518
+ const loaded = await loadOrgProject({
519
+ github,
520
+ auth,
521
+ c: loose(c)
522
+ });
523
+ if ("response" in loaded) return loaded.response;
524
+ const page = parseListPage(c.req.query("page"));
525
+ if (page === null) return c.json({ error: "invalid_page" }, 400);
526
+ const label = parseIssueLabelFilter(c.req.query("label"));
527
+ if (label === null) return c.json({ error: "invalid_label" }, 400);
528
+ try {
529
+ const { issues, nextCursor } = await github.intake.listIssues({
530
+ connection: {
531
+ type: "app-installation",
532
+ installationId: Number(loaded.project.installation.externalId)
533
+ },
534
+ sourceIds: [loaded.project.repository.slug],
535
+ labels: label ? [label] : void 0,
536
+ cursor: String(page)
537
+ });
538
+ const responseIssues = issues.map((issue) => ({
539
+ number: Number(issue.id),
540
+ title: issue.title,
541
+ url: issue.url,
542
+ author: issue.author,
543
+ assignee: issue.assignee,
544
+ assignees: issue.assignees,
545
+ labels: issue.labels,
546
+ comments: issue.commentCount ?? 0,
547
+ createdAt: issue.createdAt,
548
+ updatedAt: issue.updatedAt
549
+ }));
550
+ await ingestPolledEvents(responseIssues.map((issue) => polledIssueEvent(loaded.project, issue)), options.ingestFactoryEvent);
551
+ return c.json({
552
+ issues: responseIssues,
553
+ nextPage: nextCursor === null ? null : Number(nextCursor)
554
+ });
555
+ } catch (err) {
556
+ return c.json({
557
+ error: "github_fetch_failed",
558
+ message: err instanceof Error ? err.message : String(err)
559
+ }, 502);
560
+ }
561
+ }
562
+ }));
563
+ routes.push(registerApiRoute("/web/github/projects/:id/issues/:number/triage", {
564
+ method: "POST",
565
+ requiresAuth: false,
566
+ handler: async (c) => {
567
+ const owned = await loadOwnedProject({
568
+ github,
569
+ auth,
570
+ fleet,
571
+ c: loose(c)
572
+ });
573
+ if ("response" in owned) return owned.response;
574
+ const { project, sandboxRow } = owned;
575
+ const issueNumber = parseIssueNumberParam(c.req.param("number"));
576
+ if (issueNumber === null) return c.json({ error: "invalid_issue_number" }, 400);
577
+ let body;
578
+ try {
579
+ body = await c.req.json();
580
+ } catch {
581
+ return c.json({ error: "Invalid JSON body" }, 400);
582
+ }
583
+ if (typeof body.title !== "string" || body.title.trim().length === 0 || body.title.length > 5e3) return c.json({ error: "invalid_title" }, 400);
584
+ if (typeof body.url !== "string" || body.url.trim().length === 0 || body.url.length > 2048 || !isCanonicalGithubIssueUrl(body.url, project.repository.slug, issueNumber)) return c.json({ error: "invalid_url" }, 400);
585
+ if (!runBoardIssueTriage) return c.json({ error: "triage_unavailable" }, 503);
586
+ const branch = `factory/issue-${issueNumber}`;
587
+ const projectPath = computeWorktreePath(sandboxRow.sandboxWorkdir, branch);
588
+ const result = await runBoardIssueTriage({
589
+ repository: project.repository.slug,
590
+ issueNumber,
591
+ issueTitle: body.title,
592
+ issueUrl: body.url,
593
+ labels: parseStringList(body.labels),
594
+ installationId: Number(project.installation.externalId),
595
+ resourceId: project.factoryProjectId,
596
+ projectPath,
597
+ branch
598
+ });
599
+ await emitAudit?.({
600
+ context: loose(c),
601
+ input: {
602
+ action: "factory.triage.started",
603
+ factoryProjectId: project.factoryProjectId,
604
+ projectRepositoryId: project.id,
605
+ targets: [{
606
+ type: "issue",
607
+ id: String(issueNumber),
608
+ name: body.title
609
+ }],
610
+ metadata: {
611
+ issueNumber,
612
+ branch,
613
+ threadId: result.threadId
614
+ }
615
+ }
616
+ });
617
+ return c.json({
618
+ ok: true,
619
+ threadId: result.threadId,
620
+ projectPath: result.projectPath ?? projectPath,
621
+ branch: result.branch ?? branch
622
+ }, 202);
623
+ }
624
+ }));
625
+ routes.push(registerApiRoute("/web/github/projects/:id/prs", {
626
+ method: "GET",
627
+ requiresAuth: false,
628
+ handler: async (c) => {
629
+ const loaded = await loadOrgProject({
630
+ github,
631
+ auth,
632
+ c: loose(c)
633
+ });
634
+ if ("response" in loaded) return loaded.response;
635
+ const page = parseListPage(c.req.query("page"));
636
+ if (page === null) return c.json({ error: "invalid_page" }, 400);
637
+ try {
638
+ const { pullRequests, nextCursor } = await github.versionControl.listPullRequests({
639
+ connection: {
640
+ type: "app-installation",
641
+ installationId: Number(loaded.project.installation.externalId)
642
+ },
643
+ sourceId: loaded.project.repository.slug,
644
+ includeDrafts: false,
645
+ cursor: String(page)
646
+ });
647
+ const responsePullRequests = pullRequests.map((pr) => ({
648
+ number: Number(pr.id),
649
+ title: pr.title,
650
+ url: pr.url,
651
+ author: pr.author,
652
+ assignees: pr.assignees ?? [],
653
+ requestedReviewers: pr.requestedReviewers ?? [],
654
+ baseBranch: pr.baseBranch,
655
+ headBranch: pr.headBranch,
656
+ createdAt: pr.createdAt,
657
+ updatedAt: pr.updatedAt
658
+ }));
659
+ await ingestPolledEvents(responsePullRequests.map((pullRequest) => polledPullRequestEvent(loaded.project, pullRequest)), options.ingestFactoryEvent);
660
+ return c.json({
661
+ pullRequests: responsePullRequests,
662
+ nextPage: nextCursor === null ? null : Number(nextCursor)
663
+ });
664
+ } catch (err) {
665
+ return c.json({
666
+ error: "github_fetch_failed",
667
+ message: err instanceof Error ? err.message : String(err)
668
+ }, 502);
669
+ }
670
+ }
671
+ }));
672
+ routes.push(registerApiRoute("/web/github/projects/:id/settings", {
673
+ method: "GET",
674
+ requiresAuth: false,
675
+ handler: async (c) => {
676
+ const loaded = await loadOrgProject({
677
+ github,
678
+ auth,
679
+ c: loose(c)
680
+ });
681
+ if ("response" in loaded) return loaded.response;
682
+ return c.json({ setupCommand: loaded.project.setupCommand });
683
+ }
684
+ }));
685
+ routes.push(registerApiRoute("/web/github/projects/:id/settings", {
686
+ method: "POST",
687
+ requiresAuth: false,
688
+ handler: async (c) => {
689
+ const loaded = await loadOrgProject({
690
+ github,
691
+ auth,
692
+ c: loose(c)
693
+ });
694
+ if ("response" in loaded) return loaded.response;
695
+ let body;
696
+ try {
697
+ body = await c.req.json();
698
+ } catch {
699
+ return c.json({ error: "Invalid JSON body" }, 400);
700
+ }
701
+ if (body.setupCommand !== null && typeof body.setupCommand !== "string") return c.json({ error: "Invalid setupCommand" }, 400);
702
+ if (typeof body.setupCommand === "string" && body.setupCommand.length > 2e3) return c.json({ error: "setupCommand too long (max 2000 characters)" }, 400);
703
+ if (typeof body.setupCommand === "string" && /[\0-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(body.setupCommand)) return c.json({ error: "setupCommand contains control characters" }, 400);
704
+ const setupCommand = typeof body.setupCommand === "string" && body.setupCommand.trim().length > 0 ? body.setupCommand.trim() : null;
705
+ await github.sourceControlStorage.projectRepositories.update({
706
+ orgId: loaded.project.installation.orgId,
707
+ id: loaded.project.id,
708
+ input: { setupCommand }
709
+ });
710
+ return c.json({ setupCommand });
711
+ }
712
+ }));
713
+ const parsePatKind = (value) => {
714
+ if (value === void 0 || value === null || value === "default") return "default";
715
+ if (value === "reviewer") return "reviewer";
716
+ return null;
717
+ };
718
+ routes.push(registerApiRoute("/web/github/pat", {
719
+ method: "GET",
720
+ requiresAuth: false,
721
+ handler: async (c) => {
722
+ const resolved = await resolveOrgTenant(loose(c), auth);
723
+ if ("response" in resolved) return resolved.response;
724
+ return c.json(await getGithubPatStatus(() => github.integrationStorage, resolved.tenant.orgId));
725
+ }
726
+ }), registerApiRoute("/web/github/pat", {
727
+ method: "POST",
728
+ requiresAuth: false,
729
+ handler: async (c) => {
730
+ const resolved = await resolveOrgTenant(loose(c), auth);
731
+ if ("response" in resolved) return resolved.response;
732
+ let body;
733
+ try {
734
+ body = await c.req.json();
735
+ } catch {
736
+ return c.json({ error: "Invalid JSON body" }, 400);
737
+ }
738
+ const kind = parsePatKind(body.kind);
739
+ if (!kind) return c.json({ error: "kind must be 'default' or 'reviewer'" }, 400);
740
+ const token = typeof body.token === "string" ? body.token.trim() : "";
741
+ if (!token) return c.json({ error: "A token is required" }, 400);
742
+ if (token.length > 500) return c.json({ error: "Token too long (max 500 characters)" }, 400);
743
+ if (/\s/.test(token)) return c.json({ error: "Token must not contain whitespace" }, 400);
744
+ await setGithubPat(github.integrationStorage, resolved.tenant.orgId, token, kind);
745
+ return c.json(await getGithubPatStatus(() => github.integrationStorage, resolved.tenant.orgId));
746
+ }
747
+ }), registerApiRoute("/web/github/pat", {
748
+ method: "DELETE",
749
+ requiresAuth: false,
750
+ handler: async (c) => {
751
+ const resolved = await resolveOrgTenant(loose(c), auth);
752
+ if ("response" in resolved) return resolved.response;
753
+ const kind = parsePatKind(c.req.query("kind"));
754
+ if (!kind) return c.json({ error: "kind must be 'default' or 'reviewer'" }, 400);
755
+ await clearGithubPat(github.integrationStorage, resolved.tenant.orgId, kind);
756
+ return c.json(await getGithubPatStatus(() => github.integrationStorage, resolved.tenant.orgId));
757
+ }
758
+ }));
759
+ routes.push(...buildProjectGitRoutes({
760
+ github,
761
+ auth,
762
+ fleet,
763
+ emitAudit
764
+ }));
765
+ return routes;
766
+ }
767
+ /**
768
+ * Load the org-owned project for a read-only GitHub API route. Unlike
769
+ * `loadOwnedProject`, this never touches sandbox state — the issues/PR list
770
+ * routes only need the repo + installation, so they work before a sandbox is
771
+ * ever provisioned.
772
+ */
773
+ async function loadOrgProject(options) {
774
+ const { github, auth, c } = options;
775
+ const resolved = await resolveOrgTenant(c, auth);
776
+ if ("response" in resolved) return { response: resolved.response };
777
+ const { orgId, userId } = resolved.tenant;
778
+ const projectRepositoryId = c.req.param("id");
779
+ if (!projectRepositoryId) return { response: c.json({ error: "Project repository not found" }, 404) };
780
+ const project = await resolveProjectRepository({
781
+ github,
782
+ orgId,
783
+ projectRepositoryId
784
+ });
785
+ if (!project) return { response: c.json({ error: "Project repository not found" }, 404) };
786
+ return {
787
+ project,
788
+ userId
789
+ };
790
+ }
791
+ /** Derive a commit/author identity from the authenticated host user. */
792
+ function identityFromUser(user) {
793
+ const u = user;
794
+ return {
795
+ name: u?.name ?? null,
796
+ email: u?.email ?? null
797
+ };
798
+ }
799
+ /**
800
+ * Resolve a live, started sandbox for the caller's per-user sandbox binding. The
801
+ * sandbox must already have been provisioned (`sandboxId` set) — the git write
802
+ * routes never clone, they operate on the existing checkout.
803
+ */
804
+ async function resolveProjectSandbox(options) {
805
+ const { fleet, sandboxRow } = options;
806
+ if (!sandboxRow.sandboxId) throw new MaterializeError("Project sandbox is not provisioned. Open the project first.", "clone-failed");
807
+ return fleet.reattachSandbox(sandboxRow.sandboxId);
808
+ }
809
+ /**
810
+ * Load (or create) the caller's per-(project,user) sandbox binding row. The
811
+ * binding inherits its workdir from the org-owned project, but `sandboxId` /
812
+ * `materializedAt` stay null until the user first opens the project.
813
+ */
814
+ async function loadOrCreateSandboxRow(github, project, userId) {
815
+ return github.sourceControlStorage.sandboxes.getOrCreate({
816
+ projectRepository: project,
817
+ userId
818
+ });
819
+ }
820
+ /**
821
+ * Provision/reattach the caller's sandbox and materialize the repo into it,
822
+ * emitting coarse progress events as each server step happens. Shared by both
823
+ * the JSON and SSE variants of the `/ensure` route. Throws on failure so the
824
+ * caller can shape the response (HTTP status vs SSE `error` event).
825
+ */
826
+ async function prepareProject(options) {
827
+ const { github, fleet, userId, onProgress } = options;
828
+ let project = options.project;
829
+ if (project.sandboxProvider !== fleet.provider) {
830
+ const sandboxWorkdir = fleet.computeWorkdir(project.repository.slug);
831
+ await github.sourceControlStorage.projectRepositories.update({
832
+ orgId: project.installation.orgId,
833
+ id: project.id,
834
+ input: {
835
+ sandboxProvider: fleet.provider,
836
+ sandboxWorkdir
837
+ }
838
+ });
839
+ project = {
840
+ ...project,
841
+ sandboxProvider: fleet.provider,
842
+ sandboxWorkdir
843
+ };
844
+ }
845
+ let sandboxRow = await loadOrCreateSandboxRow(github, project, userId);
846
+ if (sandboxRow.sandboxWorkdir !== project.sandboxWorkdir) {
847
+ await github.sourceControlStorage.sandboxes.setWorkdir({
848
+ id: sandboxRow.id,
849
+ sandboxWorkdir: project.sandboxWorkdir
850
+ });
851
+ sandboxRow = {
852
+ ...sandboxRow,
853
+ sandboxWorkdir: project.sandboxWorkdir,
854
+ materializedAt: null
855
+ };
856
+ }
857
+ const access = await github.versionControl.getRepositoryAccess({
858
+ orgId: project.installation.orgId,
859
+ repositoryId: project.repository.id
860
+ });
861
+ if (!access.authorization) throw new MaterializeError("Repository access did not include a bearer token.", "clone-failed");
862
+ const ghCliToken = await getGithubPat(() => github.integrationStorage, project.installation.orgId) ?? access.authorization.token;
863
+ const sandbox = await ensureProjectSandbox({
864
+ fleet,
865
+ row: sandboxRow,
866
+ storage: github.sourceControlStorage.sandboxes,
867
+ token: ghCliToken,
868
+ onProgress
869
+ });
870
+ const finalRow = await github.sourceControlStorage.sandboxes.getById({ id: sandboxRow.id }) ?? sandboxRow;
871
+ await materializeRepo({
872
+ row: finalRow,
873
+ repoInfo: {
874
+ repoFullName: project.repository.slug,
875
+ defaultBranch: project.defaultBranch
876
+ },
877
+ sandbox,
878
+ token: access.authorization.token,
879
+ storage: github.sourceControlStorage.sandboxes,
880
+ onProgress
881
+ });
882
+ const result = {
883
+ resourceId: project.factoryProjectId,
884
+ factoryProjectId: project.factoryProjectId,
885
+ projectRepositoryId: project.id,
886
+ sandboxId: finalRow.sandboxId,
887
+ sandboxWorkdir: finalRow.sandboxWorkdir
888
+ };
889
+ onProgress?.({
890
+ phase: "done",
891
+ message: "Workspace ready."
892
+ });
893
+ return result;
894
+ }
895
+ /** Shape an /ensure failure into an HTTP status + JSON body (also used as the SSE error payload). */
896
+ function ensureErrorPayload(err) {
897
+ if (err instanceof SandboxBudgetError) return {
898
+ status: 429,
899
+ body: {
900
+ error: err.code,
901
+ message: err.message
902
+ }
903
+ };
904
+ if (err instanceof MaterializeError) return {
905
+ status: 502,
906
+ body: {
907
+ error: err.code,
908
+ message: err.message
909
+ }
910
+ };
911
+ return {
912
+ status: 500,
913
+ body: {
914
+ error: "materialize_failed",
915
+ message: err instanceof Error ? err.message : String(err)
916
+ }
917
+ };
918
+ }
919
+ /** Map a sandbox/worktree error to an actionable HTTP response. */
920
+ function gitErrorResponse(c, err) {
921
+ if (err instanceof WorktreeError) return c.json({
922
+ error: err.code,
923
+ message: err.message
924
+ }, err.code === "invalid-branch" ? 400 : 502);
925
+ if (err instanceof MaterializeError) return c.json({
926
+ error: err.code,
927
+ message: err.message
928
+ }, 502);
929
+ return c.json({
930
+ error: "git_failed",
931
+ message: err instanceof Error ? err.message : String(err)
932
+ }, 500);
933
+ }
934
+ /**
935
+ * Load the org-owned project and the caller's per-user sandbox binding for a git
936
+ * route. Centralizes the auth + org/ownership checks every git route shares:
937
+ * the project is scoped by `(id, orgId)`, the sandbox binding by
938
+ * `(projectRepositoryId, userId)`. Returns the tenant, project, and sandbox row, or
939
+ * a ready-to-return error response.
940
+ */
941
+ async function loadOwnedProject(options) {
942
+ const { github, auth, fleet, c } = options;
943
+ const resolved = await resolveOrgTenant(c, auth);
944
+ if ("response" in resolved) return { response: resolved.response };
945
+ const { orgId, userId } = resolved.tenant;
946
+ if (!fleet.enabled) return { response: c.json({
947
+ error: "sandbox_not_configured",
948
+ message: "No sandbox provider is configured."
949
+ }, 503) };
950
+ const projectRepositoryId = c.req.param("id");
951
+ if (!projectRepositoryId) return { response: c.json({ error: "Project repository not found" }, 404) };
952
+ const project = await resolveProjectRepository({
953
+ github,
954
+ orgId,
955
+ projectRepositoryId
956
+ });
957
+ if (!project) return { response: c.json({ error: "Project repository not found" }, 404) };
958
+ return {
959
+ orgId,
960
+ userId,
961
+ project,
962
+ sandboxRow: await loadOrCreateSandboxRow(github, project, userId)
963
+ };
964
+ }
965
+ function buildProjectGitRoutes({ github, auth, fleet, emitAudit }) {
966
+ return [
967
+ registerApiRoute("/web/github/projects/:id/sessions", {
968
+ method: "GET",
969
+ requiresAuth: false,
970
+ handler: async (c) => {
971
+ const resolved = await resolveOrgTenant(loose(c), auth);
972
+ if ("response" in resolved) return resolved.response;
973
+ const { orgId, userId } = resolved.tenant;
974
+ const projectRepositoryId = c.req.param("id");
975
+ const project = projectRepositoryId ? await resolveProjectRepository({
976
+ github,
977
+ orgId,
978
+ projectRepositoryId
979
+ }) : null;
980
+ if (!project) return c.json({ error: "Project repository not found" }, 404);
981
+ const sessions = await github.sourceControlStorage.sessions.list({
982
+ projectRepositoryId: project.id,
983
+ userId
984
+ });
985
+ return c.json({ sessions });
986
+ }
987
+ }),
988
+ registerApiRoute("/web/github/projects/:id/sessions", {
989
+ method: "POST",
990
+ requiresAuth: false,
991
+ handler: async (c) => {
992
+ const resolved = await resolveOrgTenant(loose(c), auth);
993
+ if ("response" in resolved) return resolved.response;
994
+ const { orgId, userId } = resolved.tenant;
995
+ const projectRepositoryId = c.req.param("id");
996
+ const project = projectRepositoryId ? await resolveProjectRepository({
997
+ github,
998
+ orgId,
999
+ projectRepositoryId
1000
+ }) : null;
1001
+ if (!project) return c.json({ error: "Project repository not found" }, 404);
1002
+ let body;
1003
+ try {
1004
+ body = await c.req.json();
1005
+ } catch {
1006
+ return c.json({ error: "Invalid JSON body" }, 400);
1007
+ }
1008
+ if (!isJsonObject(body)) return c.json({ error: "Invalid JSON body" }, 400);
1009
+ const requestedBaseBranch = body.baseBranch;
1010
+ if (requestedBaseBranch !== void 0 && typeof requestedBaseBranch !== "string") return c.json({ error: "Invalid baseBranch" }, 400);
1011
+ const baseBranch = requestedBaseBranch ?? project.defaultBranch;
1012
+ if (!isValidGitRef$1(baseBranch)) return c.json({ error: "Invalid baseBranch" }, 400);
1013
+ const requestedSessionId = body.sessionId;
1014
+ if (requestedSessionId !== void 0 && (typeof requestedSessionId !== "string" || !UUID_PATTERN.test(requestedSessionId))) return c.json({ error: "Invalid sessionId" }, 400);
1015
+ const sessionId = requestedSessionId ?? randomUUID();
1016
+ const requestedTitle = body.title;
1017
+ if (requestedTitle !== void 0 && typeof requestedTitle !== "string") return c.json({ error: "Invalid title" }, 400);
1018
+ const normalizedTitle = requestedTitle === void 0 ? null : normalizeSessionTitle(requestedTitle);
1019
+ const requestedBranch = body.branch;
1020
+ let branch;
1021
+ if (requestedBranch === void 0) branch = `${USER_SESSION_BRANCH_PREFIX}${sessionId}`;
1022
+ else if (typeof requestedBranch === "string" && isValidGitRef$1(requestedBranch)) branch = requestedBranch;
1023
+ else return c.json({ error: "Invalid branch" }, 400);
1024
+ if (requestedSessionId !== void 0) {
1025
+ const existing = await github.sourceControlStorage.sessions.getBySessionId(sessionId);
1026
+ if (existing) {
1027
+ if (existing.projectRepositoryId !== project.id || existing.orgId !== orgId || existing.userId !== userId || existing.branch !== branch) return c.json({ error: "Session ID conflict" }, 409);
1028
+ return c.json({ session: existing });
1029
+ }
1030
+ }
1031
+ const session = await github.sourceControlStorage.sessions.create({
1032
+ sessionId,
1033
+ projectRepositoryId: project.id,
1034
+ orgId,
1035
+ userId,
1036
+ branch,
1037
+ baseBranch,
1038
+ title: normalizedTitle
1039
+ }).catch(async (error) => {
1040
+ if (!(error instanceof UniqueViolationError) || requestedSessionId === void 0) throw error;
1041
+ const conflict = await github.sourceControlStorage.sessions.getBySessionId(sessionId);
1042
+ if (!conflict) throw error;
1043
+ return conflict;
1044
+ });
1045
+ if (requestedSessionId !== void 0 && (session.sessionId !== sessionId || session.projectRepositoryId !== project.id || session.orgId !== orgId || session.userId !== userId || session.branch !== branch)) return c.json({ error: "Session ID conflict" }, 409);
1046
+ return c.json({ session });
1047
+ }
1048
+ }),
1049
+ registerApiRoute("/web/user-sessions/:sessionId", {
1050
+ method: "GET",
1051
+ requiresAuth: false,
1052
+ handler: async (c) => {
1053
+ const resolved = await resolveOrgTenant(loose(c), auth);
1054
+ if ("response" in resolved) return resolved.response;
1055
+ const session = await github.sourceControlStorage.sessions.getBySessionId(c.req.param("sessionId"));
1056
+ if (!session || session.orgId !== resolved.tenant.orgId || session.userId !== resolved.tenant.userId) return c.json({ error: "Session not found" }, 404);
1057
+ return c.json({ session });
1058
+ }
1059
+ }),
1060
+ registerApiRoute("/web/user-sessions/:sessionId", {
1061
+ method: "DELETE",
1062
+ requiresAuth: false,
1063
+ handler: async (c) => {
1064
+ const resolved = await resolveOrgTenant(loose(c), auth);
1065
+ if ("response" in resolved) return resolved.response;
1066
+ const session = await github.sourceControlStorage.sessions.getBySessionId(c.req.param("sessionId"));
1067
+ if (!session || session.orgId !== resolved.tenant.orgId || session.userId !== resolved.tenant.userId) return c.json({ error: "Session not found" }, 404);
1068
+ await github.sourceControlStorage.sessions.delete(session.id);
1069
+ reclaimDeletedSessionSandbox({
1070
+ fleet,
1071
+ sourceControl: github.sourceControlStorage,
1072
+ session
1073
+ }).catch((error) => {
1074
+ console.error("[GitHub Sessions] Failed to reclaim sandbox for deleted session", {
1075
+ sessionId: session.sessionId,
1076
+ sandboxId: session.sandboxId,
1077
+ error
1078
+ });
1079
+ });
1080
+ return c.json({ removed: true });
1081
+ }
1082
+ }),
1083
+ registerApiRoute("/web/github/projects/:id/commit", {
1084
+ method: "POST",
1085
+ requiresAuth: false,
1086
+ handler: async (c) => {
1087
+ const owned = await loadOwnedProject({
1088
+ github,
1089
+ auth,
1090
+ fleet,
1091
+ c: loose(c)
1092
+ });
1093
+ if ("response" in owned) return owned.response;
1094
+ const { userId, project } = owned;
1095
+ let body;
1096
+ try {
1097
+ body = await c.req.json();
1098
+ } catch {
1099
+ return c.json({ error: "Invalid JSON body" }, 400);
1100
+ }
1101
+ if (typeof body.message !== "string" || body.message.trim().length === 0 || body.message.length > 5e3) return c.json({ error: "Invalid message" }, 400);
1102
+ const sessionWorkspace = await resolveSessionWorkspace(github, project.id, userId, body.sessionId);
1103
+ if (!sessionWorkspace) return c.json({ error: "Invalid sessionId" }, 400);
1104
+ const { workdir, sandboxBinding } = sessionWorkspace;
1105
+ try {
1106
+ return await withSessionOperationLock(sessionWorkspace.session.sessionId, async () => {
1107
+ const result = await commitAll(await resolveProjectSandbox({
1108
+ fleet,
1109
+ sandboxRow: sandboxBinding
1110
+ }), workdir, body.message, identityFromUser(await auth.ensureUser(loose(c))));
1111
+ if (result.committed) await emitAudit?.({
1112
+ context: loose(c),
1113
+ input: {
1114
+ action: "factory.git.commit",
1115
+ factoryProjectId: project.factoryProjectId,
1116
+ projectRepositoryId: project.id,
1117
+ targets: [{
1118
+ type: "session",
1119
+ id: sessionWorkspace.session.sessionId
1120
+ }],
1121
+ metadata: { sessionId: sessionWorkspace.session.sessionId }
1122
+ }
1123
+ });
1124
+ return c.json({ committed: result.committed });
1125
+ });
1126
+ } catch (err) {
1127
+ return gitErrorResponse(loose(c), err);
1128
+ }
1129
+ }
1130
+ }),
1131
+ registerApiRoute("/web/github/projects/:id/push", {
1132
+ method: "POST",
1133
+ requiresAuth: false,
1134
+ handler: async (c) => {
1135
+ const owned = await loadOwnedProject({
1136
+ github,
1137
+ auth,
1138
+ fleet,
1139
+ c: loose(c)
1140
+ });
1141
+ if ("response" in owned) return owned.response;
1142
+ const { orgId, userId, project } = owned;
1143
+ let body;
1144
+ try {
1145
+ body = await c.req.json();
1146
+ } catch {
1147
+ return c.json({ error: "Invalid JSON body" }, 400);
1148
+ }
1149
+ if (!isValidGitRef$1(body.branch)) return c.json({ error: "Invalid branch" }, 400);
1150
+ const branch = body.branch;
1151
+ const sessionWorkspace = await resolveSessionWorkspace(github, project.id, userId, body.sessionId);
1152
+ if (!sessionWorkspace) return c.json({ error: "Invalid sessionId" }, 400);
1153
+ const { workdir, sandboxBinding } = sessionWorkspace;
1154
+ try {
1155
+ return await withSessionOperationLock(sessionWorkspace.session.sessionId, async () => {
1156
+ const sandbox = await resolveProjectSandbox({
1157
+ fleet,
1158
+ sandboxRow: sandboxBinding
1159
+ });
1160
+ const access = await github.versionControl.getRepositoryAccess({
1161
+ orgId,
1162
+ repositoryId: project.repository.id
1163
+ });
1164
+ if (!access.authorization) throw new Error("Repository access did not include a bearer token.");
1165
+ await pushBranch(sandbox, workdir, branch, access.authorization.token, project.repository.slug);
1166
+ await emitAudit?.({
1167
+ context: loose(c),
1168
+ input: {
1169
+ action: "factory.git.push",
1170
+ factoryProjectId: project.factoryProjectId,
1171
+ projectRepositoryId: project.id,
1172
+ targets: [{
1173
+ type: "branch",
1174
+ id: branch
1175
+ }],
1176
+ metadata: {
1177
+ branch,
1178
+ sessionId: sessionWorkspace.session.sessionId
1179
+ }
1180
+ }
1181
+ });
1182
+ return c.json({
1183
+ pushed: true,
1184
+ branch
1185
+ });
1186
+ });
1187
+ } catch (err) {
1188
+ return gitErrorResponse(loose(c), err);
1189
+ }
1190
+ }
1191
+ }),
1192
+ registerApiRoute("/web/github/projects/:id/pr", {
1193
+ method: "POST",
1194
+ requiresAuth: false,
1195
+ handler: async (c) => {
1196
+ const owned = await loadOwnedProject({
1197
+ github,
1198
+ auth,
1199
+ fleet,
1200
+ c: loose(c)
1201
+ });
1202
+ if ("response" in owned) return owned.response;
1203
+ const { orgId, userId, project } = owned;
1204
+ let body;
1205
+ try {
1206
+ body = await c.req.json();
1207
+ } catch {
1208
+ return c.json({ error: "Invalid JSON body" }, 400);
1209
+ }
1210
+ if (!isValidGitRef$1(body.branch)) return c.json({ error: "Invalid branch" }, 400);
1211
+ const base = body.base === void 0 ? project.defaultBranch : body.base;
1212
+ if (!isValidGitRef$1(base)) return c.json({ error: "Invalid base" }, 400);
1213
+ if (typeof body.title !== "string" || body.title.trim().length === 0 || body.title.length > 256) return c.json({ error: "Invalid title" }, 400);
1214
+ if (body.body !== void 0 && (typeof body.body !== "string" || body.body.length > 65536)) return c.json({ error: "Invalid body" }, 400);
1215
+ const head = body.branch;
1216
+ const title = body.title;
1217
+ const prBody = body.body;
1218
+ const sessionWorkspace = await resolveSessionWorkspace(github, project.id, userId, body.sessionId);
1219
+ if (!sessionWorkspace) return c.json({ error: "Invalid sessionId" }, 400);
1220
+ try {
1221
+ return await withSessionOperationLock(sessionWorkspace.session.sessionId, async () => {
1222
+ const result = await github.versionControl.createPullRequest({
1223
+ connection: {
1224
+ type: "app-installation",
1225
+ installationId: Number(project.installation.externalId)
1226
+ },
1227
+ sourceId: project.repository.slug,
1228
+ baseBranch: base,
1229
+ headBranch: head,
1230
+ title,
1231
+ body: prBody,
1232
+ actingUserId: userId
1233
+ });
1234
+ await emitAudit?.({
1235
+ context: loose(c),
1236
+ input: {
1237
+ action: "factory.git.pr_opened",
1238
+ factoryProjectId: project.factoryProjectId,
1239
+ projectRepositoryId: project.id,
1240
+ targets: [{
1241
+ type: "pull_request",
1242
+ id: result.url,
1243
+ name: title
1244
+ }],
1245
+ metadata: {
1246
+ branch: head,
1247
+ base,
1248
+ url: result.url
1249
+ }
1250
+ }
1251
+ });
1252
+ const pullRequestNumber = pullRequestNumberFromUrl(result.url, project.repository.slug);
1253
+ if (pullRequestNumber) {
1254
+ const sessionId = sessionWorkspace.session.sessionId;
1255
+ await subscribeToPullRequest({
1256
+ orgId,
1257
+ installationExternalId: project.installation.externalId,
1258
+ projectRepositoryId: project.id,
1259
+ repositoryExternalId: project.repository.externalId,
1260
+ repositorySlug: project.repository.slug,
1261
+ changeRequestId: pullRequestNumber.toString(),
1262
+ sessionId,
1263
+ ownerId: userId,
1264
+ resourceId: sessionId,
1265
+ threadId: sessionId,
1266
+ source: "factory-pr-create",
1267
+ subscribedByUserId: userId
1268
+ }, github.integrationStorage).catch((error) => {
1269
+ console.warn(`[GitHub] Pull request ${result.url} was created but automatic subscription failed.`, error);
1270
+ });
1271
+ }
1272
+ return c.json({ url: result.url });
1273
+ });
1274
+ } catch (err) {
1275
+ return c.json({
1276
+ error: "github_pr_create_failed",
1277
+ message: err instanceof Error ? err.message : String(err)
1278
+ }, 502);
1279
+ }
1280
+ }
1281
+ }),
1282
+ registerApiRoute("/web/github/projects/:id/sandbox", {
1283
+ method: "DELETE",
1284
+ requiresAuth: false,
1285
+ handler: async (c) => {
1286
+ const owned = await loadOwnedProject({
1287
+ github,
1288
+ auth,
1289
+ fleet,
1290
+ c: loose(c)
1291
+ });
1292
+ if ("response" in owned) return owned.response;
1293
+ const { sandboxRow } = owned;
1294
+ if (!sandboxRow.sandboxId) return c.json({ tornDown: false });
1295
+ try {
1296
+ return await withSessionOperationLock(`sandbox:${sandboxRow.id}`, async () => {
1297
+ const sandbox = await fleet.reattachSandbox(sandboxRow.sandboxId);
1298
+ await teardownProjectSandbox({
1299
+ fleet,
1300
+ row: sandboxRow,
1301
+ storage: github.sourceControlStorage.sandboxes,
1302
+ sandbox
1303
+ });
1304
+ return c.json({ tornDown: true });
1305
+ });
1306
+ } catch (err) {
1307
+ return gitErrorResponse(loose(c), err);
1308
+ }
1309
+ }
1310
+ })
1311
+ ];
1312
+ }
1313
+ /** Resolve the materialized workspace owned by a Factory session. */
1314
+ async function resolveSessionWorkspace(github, projectId, userId, sessionId) {
1315
+ if (typeof sessionId !== "string") return;
1316
+ const session = await github.sourceControlStorage.sessions.getBySessionId(sessionId);
1317
+ if (session?.projectRepositoryId !== projectId || session.userId !== userId || !session.sandboxId || !session.sandboxWorkdir) return;
1318
+ return {
1319
+ session,
1320
+ workdir: session.sandboxWorkdir,
1321
+ sandboxBinding: {
1322
+ id: session.id,
1323
+ projectRepositoryId: session.projectRepositoryId,
1324
+ userId: session.userId,
1325
+ sandboxId: session.sandboxId,
1326
+ sandboxWorkdir: session.sandboxWorkdir,
1327
+ materializedAt: session.materializedAt,
1328
+ createdAt: session.createdAt
1329
+ }
1330
+ };
1331
+ }
1332
+ //#endregion
1333
+ export { buildGithubRoutes };
1334
+
1335
+ //# sourceMappingURL=routes.js.map