@mastra/factory 0.0.0-fix-thread-subscription-multi-instance-redis-streams-20260730204150

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 (313) hide show
  1. package/CHANGELOG.md +695 -0
  2. package/LICENSE.md +30 -0
  3. package/README.md +27 -0
  4. package/dist/auth.d.ts +181 -0
  5. package/dist/auth.d.ts.map +1 -0
  6. package/dist/auth.js +505 -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 +140 -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 +245 -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 +156 -0
  18. package/dist/factory.d.ts.map +1 -0
  19. package/dist/factory.js +483 -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 +210 -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/factory-session.d.ts +12 -0
  32. package/dist/integrations/github/factory-session.d.ts.map +1 -0
  33. package/dist/integrations/github/factory-session.js +37 -0
  34. package/dist/integrations/github/factory-session.js.map +1 -0
  35. package/dist/integrations/github/integration.d.ts +191 -0
  36. package/dist/integrations/github/integration.d.ts.map +1 -0
  37. package/dist/integrations/github/integration.js +1077 -0
  38. package/dist/integrations/github/integration.js.map +1 -0
  39. package/dist/integrations/github/issue-triage.d.ts +27 -0
  40. package/dist/integrations/github/issue-triage.d.ts.map +1 -0
  41. package/dist/integrations/github/issue-triage.js +82 -0
  42. package/dist/integrations/github/issue-triage.js.map +1 -0
  43. package/dist/integrations/github/pat.d.ts +38 -0
  44. package/dist/integrations/github/pat.d.ts.map +1 -0
  45. package/dist/integrations/github/pat.js +59 -0
  46. package/dist/integrations/github/pat.js.map +1 -0
  47. package/dist/integrations/github/provenance.d.ts +27 -0
  48. package/dist/integrations/github/provenance.d.ts.map +1 -0
  49. package/dist/integrations/github/provenance.js +75 -0
  50. package/dist/integrations/github/provenance.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 +1324 -0
  54. package/dist/integrations/github/routes.js.map +1 -0
  55. package/dist/integrations/github/rules.d.ts +78 -0
  56. package/dist/integrations/github/rules.d.ts.map +1 -0
  57. package/dist/integrations/github/rules.js +362 -0
  58. package/dist/integrations/github/rules.js.map +1 -0
  59. package/dist/integrations/github/sandbox-release.d.ts +38 -0
  60. package/dist/integrations/github/sandbox-release.d.ts.map +1 -0
  61. package/dist/integrations/github/sandbox-release.js +74 -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 +87 -0
  80. package/dist/integrations/github/webhook.d.ts.map +1 -0
  81. package/dist/integrations/github/webhook.js +397 -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 +233 -0
  88. package/dist/integrations/linear/integration.d.ts.map +1 -0
  89. package/dist/integrations/linear/integration.js +672 -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 +239 -0
  94. package/dist/integrations/linear/routes.js.map +1 -0
  95. package/dist/integrations/linear/rules.d.ts +42 -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 +49 -0
  107. package/dist/integrations/platform/github/event-worker.d.ts.map +1 -0
  108. package/dist/integrations/platform/github/event-worker.js +348 -0
  109. package/dist/integrations/platform/github/event-worker.js.map +1 -0
  110. package/dist/integrations/platform/github/integration.d.ts +50 -0
  111. package/dist/integrations/platform/github/integration.d.ts.map +1 -0
  112. package/dist/integrations/platform/github/integration.js +961 -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/workos/integration.d.ts +39 -0
  119. package/dist/integrations/workos/integration.d.ts.map +1 -0
  120. package/dist/integrations/workos/integration.js +91 -0
  121. package/dist/integrations/workos/integration.js.map +1 -0
  122. package/dist/routes/config.d.ts +235 -0
  123. package/dist/routes/config.d.ts.map +1 -0
  124. package/dist/routes/config.js +972 -0
  125. package/dist/routes/config.js.map +1 -0
  126. package/dist/routes/custom-provider-source.d.ts +52 -0
  127. package/dist/routes/custom-provider-source.d.ts.map +1 -0
  128. package/dist/routes/custom-provider-source.js +124 -0
  129. package/dist/routes/custom-provider-source.js.map +1 -0
  130. package/dist/routes/fs.d.ts +151 -0
  131. package/dist/routes/fs.d.ts.map +1 -0
  132. package/dist/routes/fs.js +708 -0
  133. package/dist/routes/fs.js.map +1 -0
  134. package/dist/routes/intake.d.ts +23 -0
  135. package/dist/routes/intake.d.ts.map +1 -0
  136. package/dist/routes/intake.js +179 -0
  137. package/dist/routes/intake.js.map +1 -0
  138. package/dist/routes/oauth.d.ts +43 -0
  139. package/dist/routes/oauth.d.ts.map +1 -0
  140. package/dist/routes/oauth.js +463 -0
  141. package/dist/routes/oauth.js.map +1 -0
  142. package/dist/routes/projects.d.ts +19 -0
  143. package/dist/routes/projects.d.ts.map +1 -0
  144. package/dist/routes/projects.js +461 -0
  145. package/dist/routes/projects.js.map +1 -0
  146. package/dist/routes/provider-credentials.d.ts +81 -0
  147. package/dist/routes/provider-credentials.d.ts.map +1 -0
  148. package/dist/routes/provider-credentials.js +86 -0
  149. package/dist/routes/provider-credentials.js.map +1 -0
  150. package/dist/routes/route.d.ts +48 -0
  151. package/dist/routes/route.d.ts.map +1 -0
  152. package/dist/routes/route.js +15 -0
  153. package/dist/routes/route.js.map +1 -0
  154. package/dist/routes/skills.d.ts +30 -0
  155. package/dist/routes/skills.d.ts.map +1 -0
  156. package/dist/routes/skills.js +168 -0
  157. package/dist/routes/skills.js.map +1 -0
  158. package/dist/routes/surface.d.ts +91 -0
  159. package/dist/routes/surface.d.ts.map +1 -0
  160. package/dist/routes/surface.js +260 -0
  161. package/dist/routes/surface.js.map +1 -0
  162. package/dist/routes/tenant-credentials.d.ts +74 -0
  163. package/dist/routes/tenant-credentials.d.ts.map +1 -0
  164. package/dist/routes/tenant-credentials.js +156 -0
  165. package/dist/routes/tenant-credentials.js.map +1 -0
  166. package/dist/routes/work-items.d.ts +40 -0
  167. package/dist/routes/work-items.d.ts.map +1 -0
  168. package/dist/routes/work-items.js +695 -0
  169. package/dist/routes/work-items.js.map +1 -0
  170. package/dist/rules/binding-context.d.ts +5 -0
  171. package/dist/rules/binding-context.d.ts.map +1 -0
  172. package/dist/rules/binding-context.js +28 -0
  173. package/dist/rules/binding-context.js.map +1 -0
  174. package/dist/rules/defaults.d.ts +9 -0
  175. package/dist/rules/defaults.d.ts.map +1 -0
  176. package/dist/rules/defaults.js +260 -0
  177. package/dist/rules/defaults.js.map +1 -0
  178. package/dist/rules/dispatcher.d.ts +42 -0
  179. package/dist/rules/dispatcher.d.ts.map +1 -0
  180. package/dist/rules/dispatcher.js +518 -0
  181. package/dist/rules/dispatcher.js.map +1 -0
  182. package/dist/rules/index.d.ts +7 -0
  183. package/dist/rules/index.d.ts.map +1 -0
  184. package/dist/rules/index.js +5 -0
  185. package/dist/rules/processor.d.ts +54 -0
  186. package/dist/rules/processor.d.ts.map +1 -0
  187. package/dist/rules/processor.js +378 -0
  188. package/dist/rules/processor.js.map +1 -0
  189. package/dist/rules/resolve.d.ts +17 -0
  190. package/dist/rules/resolve.d.ts.map +1 -0
  191. package/dist/rules/resolve.js +30 -0
  192. package/dist/rules/resolve.js.map +1 -0
  193. package/dist/rules/start-coordinator.d.ts +60 -0
  194. package/dist/rules/start-coordinator.d.ts.map +1 -0
  195. package/dist/rules/start-coordinator.js +194 -0
  196. package/dist/rules/start-coordinator.js.map +1 -0
  197. package/dist/rules/tools.d.ts +10 -0
  198. package/dist/rules/tools.d.ts.map +1 -0
  199. package/dist/rules/tools.js +58 -0
  200. package/dist/rules/tools.js.map +1 -0
  201. package/dist/rules/transition-service.d.ts +49 -0
  202. package/dist/rules/transition-service.d.ts.map +1 -0
  203. package/dist/rules/transition-service.js +232 -0
  204. package/dist/rules/transition-service.js.map +1 -0
  205. package/dist/rules/types.d.ts +234 -0
  206. package/dist/rules/types.d.ts.map +1 -0
  207. package/dist/rules/types.js +38 -0
  208. package/dist/rules/types.js.map +1 -0
  209. package/dist/rules/validation.d.ts +11 -0
  210. package/dist/rules/validation.d.ts.map +1 -0
  211. package/dist/rules/validation.js +308 -0
  212. package/dist/rules/validation.js.map +1 -0
  213. package/dist/sandbox/fleet.d.ts +222 -0
  214. package/dist/sandbox/fleet.d.ts.map +1 -0
  215. package/dist/sandbox/fleet.js +322 -0
  216. package/dist/sandbox/fleet.js.map +1 -0
  217. package/dist/sandbox/reattach.d.ts +3 -0
  218. package/dist/sandbox/reattach.d.ts.map +1 -0
  219. package/dist/sandbox/reattach.js +16 -0
  220. package/dist/sandbox/reattach.js.map +1 -0
  221. package/dist/server-error.d.ts +12 -0
  222. package/dist/server-error.d.ts.map +1 -0
  223. package/dist/server-error.js +16 -0
  224. package/dist/server-error.js.map +1 -0
  225. package/dist/skills/service.d.ts +48 -0
  226. package/dist/skills/service.d.ts.map +1 -0
  227. package/dist/skills/service.js +40 -0
  228. package/dist/skills/service.js.map +1 -0
  229. package/dist/spa-static.d.ts +33 -0
  230. package/dist/spa-static.d.ts.map +1 -0
  231. package/dist/spa-static.js +104 -0
  232. package/dist/spa-static.js.map +1 -0
  233. package/dist/state-signing.d.ts +59 -0
  234. package/dist/state-signing.d.ts.map +1 -0
  235. package/dist/state-signing.js +83 -0
  236. package/dist/state-signing.js.map +1 -0
  237. package/dist/storage/domains/audit/agent-audit.d.ts +27 -0
  238. package/dist/storage/domains/audit/agent-audit.d.ts.map +1 -0
  239. package/dist/storage/domains/audit/agent-audit.js +66 -0
  240. package/dist/storage/domains/audit/agent-audit.js.map +1 -0
  241. package/dist/storage/domains/audit/base.d.ts +140 -0
  242. package/dist/storage/domains/audit/base.d.ts.map +1 -0
  243. package/dist/storage/domains/audit/base.js +184 -0
  244. package/dist/storage/domains/audit/base.js.map +1 -0
  245. package/dist/storage/domains/audit/domain.d.ts +69 -0
  246. package/dist/storage/domains/audit/domain.d.ts.map +1 -0
  247. package/dist/storage/domains/audit/domain.js +167 -0
  248. package/dist/storage/domains/audit/domain.js.map +1 -0
  249. package/dist/storage/domains/channel-identity/base.d.ts +96 -0
  250. package/dist/storage/domains/channel-identity/base.d.ts.map +1 -0
  251. package/dist/storage/domains/channel-identity/base.js +149 -0
  252. package/dist/storage/domains/channel-identity/base.js.map +1 -0
  253. package/dist/storage/domains/credentials/base.d.ts +134 -0
  254. package/dist/storage/domains/credentials/base.d.ts.map +1 -0
  255. package/dist/storage/domains/credentials/base.js +273 -0
  256. package/dist/storage/domains/credentials/base.js.map +1 -0
  257. package/dist/storage/domains/custom-providers/base.d.ts +57 -0
  258. package/dist/storage/domains/custom-providers/base.d.ts.map +1 -0
  259. package/dist/storage/domains/custom-providers/base.js +145 -0
  260. package/dist/storage/domains/custom-providers/base.js.map +1 -0
  261. package/dist/storage/domains/intake/base.d.ts +38 -0
  262. package/dist/storage/domains/intake/base.d.ts.map +1 -0
  263. package/dist/storage/domains/intake/base.js +80 -0
  264. package/dist/storage/domains/intake/base.js.map +1 -0
  265. package/dist/storage/domains/integrations/base.d.ts +126 -0
  266. package/dist/storage/domains/integrations/base.d.ts.map +1 -0
  267. package/dist/storage/domains/integrations/base.js +317 -0
  268. package/dist/storage/domains/integrations/base.js.map +1 -0
  269. package/dist/storage/domains/memory-settings/base.d.ts +62 -0
  270. package/dist/storage/domains/memory-settings/base.d.ts.map +1 -0
  271. package/dist/storage/domains/memory-settings/base.js +126 -0
  272. package/dist/storage/domains/memory-settings/base.js.map +1 -0
  273. package/dist/storage/domains/model-packs/base.d.ts +49 -0
  274. package/dist/storage/domains/model-packs/base.d.ts.map +1 -0
  275. package/dist/storage/domains/model-packs/base.js +96 -0
  276. package/dist/storage/domains/model-packs/base.js.map +1 -0
  277. package/dist/storage/domains/projects/base.d.ts +58 -0
  278. package/dist/storage/domains/projects/base.d.ts.map +1 -0
  279. package/dist/storage/domains/projects/base.js +111 -0
  280. package/dist/storage/domains/projects/base.js.map +1 -0
  281. package/dist/storage/domains/queue-health/base.d.ts +52 -0
  282. package/dist/storage/domains/queue-health/base.d.ts.map +1 -0
  283. package/dist/storage/domains/queue-health/base.js +122 -0
  284. package/dist/storage/domains/queue-health/base.js.map +1 -0
  285. package/dist/storage/domains/source-control/base.d.ts +358 -0
  286. package/dist/storage/domains/source-control/base.d.ts.map +1 -0
  287. package/dist/storage/domains/source-control/base.js +908 -0
  288. package/dist/storage/domains/source-control/base.js.map +1 -0
  289. package/dist/storage/domains/source-control/inmemory.d.ts +175 -0
  290. package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -0
  291. package/dist/storage/domains/source-control/inmemory.js +377 -0
  292. package/dist/storage/domains/source-control/inmemory.js.map +1 -0
  293. package/dist/storage/domains/work-items/base.d.ts +412 -0
  294. package/dist/storage/domains/work-items/base.d.ts.map +1 -0
  295. package/dist/storage/domains/work-items/base.js +1309 -0
  296. package/dist/storage/domains/work-items/base.js.map +1 -0
  297. package/dist/storage/domains/work-items/metrics.d.ts +101 -0
  298. package/dist/storage/domains/work-items/metrics.d.ts.map +1 -0
  299. package/dist/storage/domains/work-items/metrics.js +221 -0
  300. package/dist/storage/domains/work-items/metrics.js.map +1 -0
  301. package/dist/timing.d.ts +15 -0
  302. package/dist/timing.d.ts.map +1 -0
  303. package/dist/timing.js +34 -0
  304. package/dist/timing.js.map +1 -0
  305. package/dist/workspace.d.ts +24 -0
  306. package/dist/workspace.d.ts.map +1 -0
  307. package/dist/workspace.js +286 -0
  308. package/dist/workspace.js.map +1 -0
  309. package/factory-skills/configure-factory-rules/SKILL.md +67 -0
  310. package/factory-skills/factory-plan/SKILL.md +61 -0
  311. package/factory-skills/factory-review/SKILL.md +155 -0
  312. package/factory-skills/factory-triage/SKILL.md +77 -0
  313. package/package.json +81 -0
@@ -0,0 +1,1077 @@
1
+ import { createGithubSubscriptionTools, parseCreatedPullRequest, subscribeCurrentSessionToPullRequest } from "./session-subscriptions.js";
2
+ import { runGithubIssueTriage } from "./issue-triage.js";
3
+ import { buildGithubRoutes } from "./routes.js";
4
+ import { attachGithubRules } from "./rules.js";
5
+ import { createAppAuth } from "@octokit/auth-app";
6
+ import { Octokit } from "@octokit/rest";
7
+ //#region src/integrations/github/integration.ts
8
+ /**
9
+ * Normalize a PEM private key supplied via env. Env tooling tends to mangle
10
+ * multi-line PEMs, so two single-line forms are supported:
11
+ * - `\n`-escaped: literal `\n` sequences become real newlines
12
+ * - fully flattened: newlines stripped entirely — the PEM is rebuilt by
13
+ * re-wrapping the base64 body (Node's decoder rejects header/body/footer
14
+ * on one line with `error:1E08010C:DECODER routines::unsupported`)
15
+ */
16
+ function normalizePrivateKey(raw) {
17
+ const key = raw.replace(/\\n/g, "\n");
18
+ if (key.includes("\n")) return key;
19
+ const flattened = key.trim().match(/^(-----BEGIN [A-Z0-9 ]+-----)\s*(.+?)\s*(-----END [A-Z0-9 ]+-----)$/);
20
+ if (!flattened) return key;
21
+ const body = flattened[2].replace(/\s+/g, "");
22
+ return `${flattened[1]}\n${body.match(/.{1,64}/g).join("\n")}\n${flattened[3]}\n`;
23
+ }
24
+ /** Page size for issue/PR listings; one GitHub API call per page. */
25
+ const LIST_PAGE_SIZE = 30;
26
+ /** Human-readable names of the required config fields, for construction errors. */
27
+ const REQUIRED_FIELDS = [
28
+ "appId",
29
+ "privateKey",
30
+ "clientId",
31
+ "clientSecret",
32
+ "slug"
33
+ ];
34
+ var GithubIntegration = class {
35
+ /** Stable integration identifier (see `../factory-integration.ts`). */
36
+ id = "github";
37
+ intake = {
38
+ resolveIntakeDispatch: (input) => this.#resolveIntakeDispatch(input),
39
+ listSources: async ({ orgId }) => {
40
+ const installations = await this.sourceControlStorage.installations.list({ orgId });
41
+ return (await Promise.all(installations.map((installation) => this.sourceControlStorage.repositories.list({
42
+ orgId,
43
+ installationId: installation.id
44
+ })))).flat().map((repository) => ({
45
+ id: repository.id,
46
+ name: repository.slug,
47
+ type: "repository",
48
+ metadata: { defaultBranch: repository.defaultBranch }
49
+ }));
50
+ },
51
+ listItems: async ({ orgId, sourceIds, cursor }) => {
52
+ const page = cursor ? Number.parseInt(cursor, 10) : 1;
53
+ const requestedPage = Number.isSafeInteger(page) && page > 0 ? page : 1;
54
+ const pages = await Promise.all(sourceIds.map(async (sourceId) => {
55
+ const repository = await this.sourceControlStorage.repositories.get({
56
+ orgId,
57
+ id: sourceId
58
+ });
59
+ if (!repository) return {
60
+ items: [],
61
+ hasNextPage: false
62
+ };
63
+ const installation = await this.sourceControlStorage.installations.get({
64
+ orgId,
65
+ id: repository.installationId
66
+ });
67
+ if (!installation) return {
68
+ items: [],
69
+ hasNextPage: false
70
+ };
71
+ const installationId = Number.parseInt(installation.externalId, 10);
72
+ if (!Number.isSafeInteger(installationId)) return {
73
+ items: [],
74
+ hasNextPage: false
75
+ };
76
+ const [issues, pullRequests] = await Promise.all([this.listRepoOpenIssues(installationId, repository.slug, requestedPage), this.versionControl.listPullRequests({
77
+ connection: {
78
+ type: "app-installation",
79
+ installationId
80
+ },
81
+ sourceId: repository.slug,
82
+ includeDrafts: false,
83
+ cursor: String(requestedPage)
84
+ })]);
85
+ return {
86
+ items: [...issues.issues.map((issue) => ({
87
+ source: {
88
+ type: "issue",
89
+ externalId: `${repository.externalId}:${issue.number}`,
90
+ url: issue.url
91
+ },
92
+ sourceId: repository.id,
93
+ title: issue.title,
94
+ status: "open",
95
+ labels: issue.labels,
96
+ createdAt: issue.createdAt,
97
+ updatedAt: issue.updatedAt,
98
+ metadata: {
99
+ repository: repository.slug,
100
+ number: issue.number,
101
+ author: issue.author
102
+ }
103
+ })), ...pullRequests.pullRequests.map((pullRequest) => ({
104
+ source: {
105
+ type: "pull-request",
106
+ externalId: `${repository.externalId}:${pullRequest.id}`,
107
+ url: pullRequest.url
108
+ },
109
+ sourceId: repository.id,
110
+ title: pullRequest.title,
111
+ status: "open",
112
+ createdAt: pullRequest.createdAt,
113
+ updatedAt: pullRequest.updatedAt,
114
+ metadata: {
115
+ repository: repository.slug,
116
+ number: Number(pullRequest.id),
117
+ author: pullRequest.author,
118
+ baseBranch: pullRequest.baseBranch,
119
+ headBranch: pullRequest.headBranch
120
+ }
121
+ }))],
122
+ hasNextPage: issues.nextPage !== null || pullRequests.nextCursor !== null
123
+ };
124
+ }));
125
+ return {
126
+ items: pages.flatMap((result) => result.items),
127
+ nextCursor: pages.some((result) => result.hasNextPage) ? String(requestedPage + 1) : null
128
+ };
129
+ },
130
+ listIssues: (input) => this.#listIntakeIssues(input),
131
+ getIssue: (input) => this.#getIntakeIssue(input),
132
+ createComment: (input) => this.#createIntakeComment(input),
133
+ updateIssue: (input) => this.#updateIntakeIssue(input)
134
+ };
135
+ versionControl = {
136
+ initialize: ({ storage }) => {
137
+ this.#sourceControlStorage = storage;
138
+ },
139
+ registerInstallation: ({ orgId, userId, installation }) => this.sourceControlStorage.installations.upsert({
140
+ orgId,
141
+ connectedByUserId: userId,
142
+ externalId: installation.externalId,
143
+ accountName: installation.accountName,
144
+ accountType: installation.accountType,
145
+ providerMetadata: installation.metadata
146
+ }),
147
+ registerRepositories: ({ orgId, installationId, repositories }) => Promise.all(repositories.map((repository) => this.sourceControlStorage.repositories.upsert({
148
+ orgId,
149
+ input: {
150
+ installationId,
151
+ externalId: repository.externalId,
152
+ slug: repository.slug,
153
+ defaultBranch: repository.defaultBranch,
154
+ providerMetadata: repository.metadata
155
+ }
156
+ }))),
157
+ getRepositoryAccess: async ({ orgId, repositoryId }) => {
158
+ const repository = await this.sourceControlStorage.repositories.get({
159
+ orgId,
160
+ id: repositoryId
161
+ });
162
+ if (!repository) throw new Error("Version-control repository not found.");
163
+ const installation = await this.sourceControlStorage.installations.get({
164
+ orgId,
165
+ id: repository.installationId
166
+ });
167
+ if (!installation) throw new Error("Version-control installation not found.");
168
+ const installationId = Number.parseInt(installation.externalId, 10);
169
+ if (!Number.isSafeInteger(installationId)) throw new Error("GitHub installation id is invalid.");
170
+ return {
171
+ cloneUrl: `https://github.com/${repository.slug}.git`,
172
+ authorization: {
173
+ scheme: "bearer",
174
+ token: await this.mintInstallationToken(installationId)
175
+ }
176
+ };
177
+ },
178
+ listPullRequests: (input) => this.#listPullRequests(input),
179
+ getPullRequest: (input) => this.#getPullRequest(input),
180
+ createPullRequest: (input) => this.#createPullRequest(input),
181
+ updatePullRequest: (input) => this.#updatePullRequest(input),
182
+ closePullRequest: (input) => this.#closePullRequest(input),
183
+ mergePullRequest: (input) => this.#mergePullRequest(input),
184
+ listComments: (input) => this.#listComments(input),
185
+ createComment: (input) => this.#createComment(input),
186
+ updateComment: (input) => this.#updateComment(input),
187
+ deleteComment: (input) => this.#deleteComment(input),
188
+ listReviews: (input) => this.#listReviews(input),
189
+ getReview: (input) => this.#getReview(input),
190
+ createReview: (input) => this.#createReview(input),
191
+ updateReview: (input) => this.#updateReview(input),
192
+ submitReview: (input) => this.#submitReview(input),
193
+ dismissReview: (input) => this.#dismissReview(input),
194
+ deletePendingReview: (input) => this.#deletePendingReview(input),
195
+ listReviewComments: (input) => this.#listReviewComments(input),
196
+ createReviewComment: (input) => this.#createReviewComment(input),
197
+ updateReviewComment: (input) => this.#updateReviewComment(input),
198
+ deleteReviewComment: (input) => this.#deleteReviewComment(input),
199
+ listRequestedReviewers: (input) => this.#listRequestedReviewers(input),
200
+ requestReviewers: (input) => this.#requestReviewers(input),
201
+ removeRequestedReviewers: (input) => this.#removeReviewers(input)
202
+ };
203
+ /**
204
+ * The OAuth/install flow round-trips a signed `state` through GitHub, so a
205
+ * multi-replica deploy needs a deployment-stable state secret.
206
+ */
207
+ requiresStableStateSigner = true;
208
+ #appId;
209
+ #privateKey;
210
+ #clientId;
211
+ #clientSecret;
212
+ #slug;
213
+ #webhookSecret;
214
+ #storage;
215
+ #sourceControlStorage;
216
+ constructor(config) {
217
+ const missing = REQUIRED_FIELDS.filter((field) => !config[field]);
218
+ if (missing.length > 0) throw new Error(`GithubIntegration: missing required config field(s): ${missing.join(", ")}. Provide the full GitHub App credentials (appId, privateKey, clientId, clientSecret, slug) or omit the integration to disable GitHub-backed repositories.`);
219
+ this.#appId = config.appId;
220
+ this.#privateKey = normalizePrivateKey(config.privateKey);
221
+ this.#clientId = config.clientId;
222
+ this.#clientSecret = config.clientSecret;
223
+ this.#slug = config.slug;
224
+ this.#webhookSecret = config.webhookSecret || void 0;
225
+ }
226
+ /** App slug — the URL name used to build the install URL. */
227
+ get slug() {
228
+ return this.#slug;
229
+ }
230
+ get genericStorage() {
231
+ if (!this.#storage) throw new Error("GithubIntegration storage has not been initialized.");
232
+ return this.#storage.generic;
233
+ }
234
+ get sourceControlStorage() {
235
+ const storage = this.#sourceControlStorage ?? this.#storage?.sourceControl;
236
+ if (!storage) throw new Error("GithubIntegration source-control storage has not been initialized.");
237
+ return storage;
238
+ }
239
+ get integrationStorage() {
240
+ if (!this.#storage) throw new Error("GithubIntegration storage has not been initialized.");
241
+ return this.#storage.generic;
242
+ }
243
+ /** Resolve a stored GitHub locator without scanning installations or repositories. */
244
+ async #resolveIntakeDispatch({ orgId, externalSource }) {
245
+ const target = parseGithubExternalTarget(externalSource.externalId);
246
+ if (!target) return null;
247
+ const repository = await this.sourceControlStorage.repositories.findByExternalId({
248
+ orgId,
249
+ externalId: target.repositoryId
250
+ });
251
+ if (!repository) return null;
252
+ const installation = await this.sourceControlStorage.installations.get({
253
+ orgId,
254
+ id: repository.installationId
255
+ });
256
+ if (!installation) return null;
257
+ const installationId = Number.parseInt(installation.externalId, 10);
258
+ if (!Number.isSafeInteger(installationId)) return null;
259
+ return {
260
+ connection: {
261
+ type: "app-installation",
262
+ installationId
263
+ },
264
+ sourceId: repository.slug,
265
+ issueId: target.issueId
266
+ };
267
+ }
268
+ /** Secret GitHub uses to sign webhook deliveries, when configured. */
269
+ get webhookSecret() {
270
+ return this.#webhookSecret;
271
+ }
272
+ #appAuth() {
273
+ return {
274
+ appId: this.#appId,
275
+ privateKey: this.#privateKey,
276
+ clientId: this.#clientId,
277
+ clientSecret: this.#clientSecret
278
+ };
279
+ }
280
+ /**
281
+ * Octokit authenticated as the GitHub App itself (app JWT). Used for
282
+ * app-level operations and to mint installation tokens.
283
+ */
284
+ getAppOctokit() {
285
+ return new Octokit({
286
+ authStrategy: createAppAuth,
287
+ auth: this.#appAuth()
288
+ });
289
+ }
290
+ /**
291
+ * Octokit authenticated as a specific installation (installation access
292
+ * token). Used to list repos and to operate on a repo on the user's behalf.
293
+ */
294
+ getInstallationOctokit(installationId) {
295
+ return new Octokit({
296
+ authStrategy: createAppAuth,
297
+ auth: {
298
+ ...this.#appAuth(),
299
+ installationId
300
+ }
301
+ });
302
+ }
303
+ /** Octokit authenticated as a user via their OAuth token (the identify step). */
304
+ getUserOctokit(userToken) {
305
+ return new Octokit({ auth: userToken });
306
+ }
307
+ /**
308
+ * Mint a short-lived installation access token. Returned token is used only
309
+ * server-side / inside the sandbox clone URL and never sent to the browser.
310
+ */
311
+ async mintInstallationToken(installationId) {
312
+ return (await createAppAuth(this.#appAuth())({
313
+ type: "installation",
314
+ installationId
315
+ })).token;
316
+ }
317
+ /**
318
+ * List the installations the authenticated user can access, via their OAuth
319
+ * token (`GET /user/installations`).
320
+ */
321
+ async listUserInstallations(userToken) {
322
+ const octokit = this.getUserOctokit(userToken);
323
+ return (await octokit.paginate(octokit.apps.listInstallationsForAuthenticatedUser, { per_page: 100 })).map((inst) => ({
324
+ installationId: inst.id,
325
+ accountLogin: inst.account && "login" in inst.account ? inst.account.login : null,
326
+ accountType: inst.account && "type" in inst.account ? inst.account.type : null
327
+ }));
328
+ }
329
+ /** List repos accessible to an installation (paginated). */
330
+ async listInstallationRepos(installationId) {
331
+ const octokit = this.getInstallationOctokit(installationId);
332
+ return (await octokit.paginate(octokit.apps.listReposAccessibleToInstallation, { per_page: 100 })).map((repo) => ({
333
+ id: repo.id,
334
+ fullName: repo.full_name,
335
+ name: repo.name,
336
+ owner: repo.owner.login,
337
+ defaultBranch: repo.default_branch,
338
+ private: repo.private,
339
+ installationId
340
+ }));
341
+ }
342
+ /**
343
+ * The authenticated user's permission level on a repo, through the
344
+ * installation. `undefined` when the repo/user is not accessible.
345
+ */
346
+ async getRepositoryCollaboratorPermission(installationId, repoFullName, username, signal) {
347
+ const parts = splitRepoFullName(repoFullName);
348
+ if (!parts) return void 0;
349
+ try {
350
+ const { data } = await this.getInstallationOctokit(installationId).repos.getCollaboratorPermissionLevel({
351
+ ...parts,
352
+ username,
353
+ request: { signal }
354
+ });
355
+ return data.permission;
356
+ } catch {
357
+ return;
358
+ }
359
+ }
360
+ /**
361
+ * Fetch a single repo's metadata through an installation token and confirm
362
+ * the installation actually has access to it. Returns `null` when the repo
363
+ * is not accessible to the installation (so a client can't create a project
364
+ * for an arbitrary repo under an installation id it merely owns).
365
+ */
366
+ async getInstallationRepo(installationId, repoFullName) {
367
+ const parts = splitRepoFullName(repoFullName);
368
+ if (!parts) return null;
369
+ const octokit = this.getInstallationOctokit(installationId);
370
+ try {
371
+ const { data } = await octokit.repos.get(parts);
372
+ return {
373
+ id: data.id,
374
+ fullName: data.full_name,
375
+ name: data.name,
376
+ owner: data.owner.login,
377
+ defaultBranch: data.default_branch,
378
+ private: data.private,
379
+ installationId
380
+ };
381
+ } catch {
382
+ return null;
383
+ }
384
+ }
385
+ async #listIntakeIssues(input) {
386
+ const installationId = getGithubInstallationId(input.connection);
387
+ const repoFullName = getSingleSourceId(input.sourceIds, "GitHub Intake requires exactly one repository source.");
388
+ const page = parsePositiveCursor(input.cursor);
389
+ const labels = normalizeLabels(input.labels);
390
+ const result = await this.listRepoOpenIssues(installationId, repoFullName, page, { label: labels.length > 0 ? labels.join(",") : void 0 });
391
+ return {
392
+ issues: result.issues.map((issue) => ({
393
+ id: String(issue.number),
394
+ identifier: `#${issue.number}`,
395
+ title: issue.title,
396
+ url: issue.url,
397
+ author: issue.author,
398
+ state: "open",
399
+ stateType: "open",
400
+ priority: null,
401
+ assignee: null,
402
+ source: repoFullName,
403
+ labels: issue.labels,
404
+ commentCount: issue.comments,
405
+ createdAt: issue.createdAt,
406
+ updatedAt: issue.updatedAt
407
+ })),
408
+ nextCursor: result.nextPage === null ? null : String(result.nextPage)
409
+ };
410
+ }
411
+ async #getIntakeIssue(input) {
412
+ const installationId = getGithubInstallationId(input.connection);
413
+ const repoFullName = requireSourceId(input.sourceId, "GitHub Intake requires a repository source.");
414
+ const parts = splitRepoFullName(repoFullName);
415
+ const issueNumber = parsePositiveInteger(input.issueId);
416
+ if (!parts || issueNumber === null) return null;
417
+ const octokit = this.getInstallationOctokit(installationId);
418
+ try {
419
+ const [{ data: issue }, comments] = await Promise.all([octokit.issues.get({
420
+ owner: parts.owner,
421
+ repo: parts.repo,
422
+ issue_number: issueNumber
423
+ }), octokit.paginate(octokit.issues.listComments, {
424
+ owner: parts.owner,
425
+ repo: parts.repo,
426
+ issue_number: issueNumber,
427
+ per_page: 100
428
+ })]);
429
+ if (issue.pull_request) return null;
430
+ return {
431
+ id: String(issue.number),
432
+ identifier: `#${issue.number}`,
433
+ title: issue.title,
434
+ url: issue.html_url,
435
+ author: issue.user?.login ?? null,
436
+ state: issue.state,
437
+ stateType: issue.state,
438
+ priority: null,
439
+ assignee: issue.assignee?.login ?? null,
440
+ source: repoFullName,
441
+ labels: issue.labels.map((label) => typeof label === "string" ? label : label.name ?? "").filter(Boolean),
442
+ commentCount: issue.comments,
443
+ createdAt: issue.created_at,
444
+ updatedAt: issue.updated_at,
445
+ description: issue.body?.trim() ? issue.body : null,
446
+ comments: comments.map((comment) => ({
447
+ author: comment.user?.login ?? null,
448
+ body: comment.body ?? "",
449
+ createdAt: comment.created_at
450
+ }))
451
+ };
452
+ } catch (err) {
453
+ if (isNotFoundError(err)) return null;
454
+ throw err;
455
+ }
456
+ }
457
+ async #updateIntakeIssue(input) {
458
+ const installationId = getGithubInstallationId(input.connection);
459
+ const repoFullName = requireSourceId(input.sourceId, "GitHub Intake requires a repository source.");
460
+ const parts = splitRepoFullName(repoFullName);
461
+ const issueNumber = parsePositiveInteger(input.issueId);
462
+ if (!parts || issueNumber === null) return null;
463
+ if (input.state.kind === "byName") {
464
+ console.warn(`GitHub integration: updateIssue byName is not supported (name=${input.state.name}); ignoring.`);
465
+ return null;
466
+ }
467
+ const targetState = input.state.stateType === "unstarted" || input.state.stateType === "started" ? "open" : "closed";
468
+ const stateReason = targetState === "closed" ? input.state.stateType === "canceled" ? "not_planned" : "completed" : null;
469
+ const octokit = this.getInstallationOctokit(installationId);
470
+ try {
471
+ const { data: current } = await octokit.issues.get({
472
+ owner: parts.owner,
473
+ repo: parts.repo,
474
+ issue_number: issueNumber
475
+ });
476
+ if (current.pull_request) {
477
+ console.warn(`GitHub integration: updateIssue rejected — target ${repoFullName}#${issueNumber} is a pull request.`);
478
+ return null;
479
+ }
480
+ const { data: issue } = await octokit.issues.update({
481
+ owner: parts.owner,
482
+ repo: parts.repo,
483
+ issue_number: issueNumber,
484
+ state: targetState,
485
+ ...stateReason ? { state_reason: stateReason } : {}
486
+ });
487
+ return {
488
+ id: String(issue.number),
489
+ identifier: `#${issue.number}`,
490
+ title: issue.title,
491
+ url: issue.html_url,
492
+ author: issue.user?.login ?? null,
493
+ state: issue.state,
494
+ stateType: issue.state,
495
+ priority: null,
496
+ assignee: issue.assignee?.login ?? null,
497
+ source: repoFullName,
498
+ labels: issue.labels.map((label) => typeof label === "string" ? label : label.name ?? "").filter(Boolean),
499
+ commentCount: issue.comments,
500
+ createdAt: issue.created_at,
501
+ updatedAt: issue.updated_at
502
+ };
503
+ } catch (err) {
504
+ if (isNotFoundError(err)) return null;
505
+ throw err;
506
+ }
507
+ }
508
+ async #createIntakeComment(input) {
509
+ const installationId = getGithubInstallationId(input.connection);
510
+ const parts = splitRepoFullName(requireSourceId(input.sourceId, "GitHub Intake requires a repository source."));
511
+ const issueNumber = parsePositiveInteger(input.issueId);
512
+ if (!parts || issueNumber === null) return null;
513
+ const octokit = this.getInstallationOctokit(installationId);
514
+ try {
515
+ const { data } = await octokit.issues.createComment({
516
+ owner: parts.owner,
517
+ repo: parts.repo,
518
+ issue_number: issueNumber,
519
+ body: input.body
520
+ });
521
+ return {
522
+ id: String(data.id),
523
+ url: data.html_url
524
+ };
525
+ } catch (err) {
526
+ if (isNotFoundError(err)) return null;
527
+ throw err;
528
+ }
529
+ }
530
+ #repositoryClient(connection, sourceId) {
531
+ const installationId = getGithubInstallationId(connection);
532
+ const parts = splitRepoFullName(sourceId);
533
+ if (!parts) throw new Error("GitHub pull requests require an owner/repository source.");
534
+ return {
535
+ octokit: this.getInstallationOctokit(installationId),
536
+ parts
537
+ };
538
+ }
539
+ async #listPullRequests(input) {
540
+ const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
541
+ const page = parsePositiveCursor(input.cursor);
542
+ const response = await octokit.pulls.list({
543
+ ...parts,
544
+ state: input.state ?? "open",
545
+ per_page: 30,
546
+ page
547
+ });
548
+ return {
549
+ pullRequests: response.data.filter((pr) => input.includeDrafts !== false || !pr.draft).map((pr) => parsePullRequest(pr)),
550
+ nextCursor: response.data.length === 30 ? String(page + 1) : null
551
+ };
552
+ }
553
+ async #getPullRequest(input) {
554
+ const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
555
+ const pullNumber = requirePullRequestNumber(input.pullRequestId);
556
+ try {
557
+ const { data } = await octokit.pulls.get({
558
+ ...parts,
559
+ pull_number: pullNumber
560
+ });
561
+ return parsePullRequest(data);
562
+ } catch (err) {
563
+ if (isNotFoundError(err)) return null;
564
+ throw err;
565
+ }
566
+ }
567
+ async #createPullRequest(input) {
568
+ const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
569
+ const { data } = await octokit.pulls.create({
570
+ ...parts,
571
+ title: input.title,
572
+ body: input.body,
573
+ base: input.baseBranch,
574
+ head: input.headBranch,
575
+ draft: input.draft
576
+ });
577
+ return parsePullRequest(data);
578
+ }
579
+ async #updatePullRequest(input) {
580
+ const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
581
+ const { data } = await octokit.pulls.update({
582
+ ...parts,
583
+ pull_number: requirePullRequestNumber(input.pullRequestId),
584
+ title: input.title,
585
+ body: input.body === null ? "" : input.body,
586
+ base: input.baseBranch,
587
+ state: input.state
588
+ });
589
+ return parsePullRequest(data);
590
+ }
591
+ async #closePullRequest(input) {
592
+ return this.#updatePullRequest({
593
+ ...input,
594
+ state: "closed"
595
+ });
596
+ }
597
+ async #mergePullRequest(input) {
598
+ const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
599
+ const { data } = await octokit.pulls.merge({
600
+ ...parts,
601
+ pull_number: requirePullRequestNumber(input.pullRequestId),
602
+ commit_title: input.commitTitle,
603
+ commit_message: input.commitMessage,
604
+ merge_method: input.method
605
+ });
606
+ return {
607
+ merged: data.merged,
608
+ message: data.message,
609
+ sha: data.sha ?? null
610
+ };
611
+ }
612
+ async #listComments(input) {
613
+ const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
614
+ const page = parsePositiveCursor(input.cursor);
615
+ const response = await octokit.issues.listComments({
616
+ ...parts,
617
+ issue_number: requirePullRequestNumber(input.pullRequestId),
618
+ per_page: 30,
619
+ page
620
+ });
621
+ return {
622
+ comments: response.data.map((comment) => parsePullRequestComment(comment)),
623
+ nextCursor: response.data.length === 30 ? String(page + 1) : null
624
+ };
625
+ }
626
+ async #createComment(input) {
627
+ const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
628
+ const { data } = await octokit.issues.createComment({
629
+ ...parts,
630
+ issue_number: requirePullRequestNumber(input.pullRequestId),
631
+ body: input.body
632
+ });
633
+ return parsePullRequestComment(data);
634
+ }
635
+ async #updateComment(input) {
636
+ const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
637
+ const { data } = await octokit.issues.updateComment({
638
+ ...parts,
639
+ comment_id: requirePositiveId(input.commentId, "comment"),
640
+ body: input.body
641
+ });
642
+ return parsePullRequestComment(data);
643
+ }
644
+ async #deleteComment(input) {
645
+ const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
646
+ await octokit.issues.deleteComment({
647
+ ...parts,
648
+ comment_id: requirePositiveId(input.commentId, "comment")
649
+ });
650
+ }
651
+ async #listReviews(input) {
652
+ const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
653
+ const page = parsePositiveCursor(input.cursor);
654
+ const response = await octokit.pulls.listReviews({
655
+ ...parts,
656
+ pull_number: requirePullRequestNumber(input.pullRequestId),
657
+ per_page: 30,
658
+ page
659
+ });
660
+ return {
661
+ reviews: response.data.map((review) => parseReview(review)),
662
+ nextCursor: response.data.length === 30 ? String(page + 1) : null
663
+ };
664
+ }
665
+ async #getReview(input) {
666
+ const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
667
+ try {
668
+ const { data } = await octokit.pulls.getReview({
669
+ ...parts,
670
+ pull_number: requirePullRequestNumber(input.pullRequestId),
671
+ review_id: requirePositiveId(input.reviewId, "review")
672
+ });
673
+ return parseReview(data);
674
+ } catch (err) {
675
+ if (isNotFoundError(err)) return null;
676
+ throw err;
677
+ }
678
+ }
679
+ async #createReview(input) {
680
+ const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
681
+ const { data } = await octokit.pulls.createReview({
682
+ ...parts,
683
+ pull_number: requirePullRequestNumber(input.pullRequestId),
684
+ body: input.body,
685
+ commit_id: input.commitId,
686
+ event: input.event ? reviewEventToGithub(input.event) : void 0
687
+ });
688
+ return parseReview(data);
689
+ }
690
+ async #updateReview(input) {
691
+ const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
692
+ const { data } = await octokit.pulls.updateReview({
693
+ ...parts,
694
+ pull_number: requirePullRequestNumber(input.pullRequestId),
695
+ review_id: requirePositiveId(input.reviewId, "review"),
696
+ body: input.body
697
+ });
698
+ return parseReview(data);
699
+ }
700
+ async #submitReview(input) {
701
+ const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
702
+ const { data } = await octokit.pulls.submitReview({
703
+ ...parts,
704
+ pull_number: requirePullRequestNumber(input.pullRequestId),
705
+ review_id: requirePositiveId(input.reviewId, "review"),
706
+ body: input.body,
707
+ event: reviewEventToGithub(input.event)
708
+ });
709
+ return parseReview(data);
710
+ }
711
+ async #dismissReview(input) {
712
+ const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
713
+ const { data } = await octokit.pulls.dismissReview({
714
+ ...parts,
715
+ pull_number: requirePullRequestNumber(input.pullRequestId),
716
+ review_id: requirePositiveId(input.reviewId, "review"),
717
+ message: input.message
718
+ });
719
+ return parseReview(data);
720
+ }
721
+ async #deletePendingReview(input) {
722
+ const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
723
+ await octokit.pulls.deletePendingReview({
724
+ ...parts,
725
+ pull_number: requirePullRequestNumber(input.pullRequestId),
726
+ review_id: requirePositiveId(input.reviewId, "review")
727
+ });
728
+ }
729
+ async #listReviewComments(input) {
730
+ const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
731
+ const page = parsePositiveCursor(input.cursor);
732
+ const response = await octokit.pulls.listReviewComments({
733
+ ...parts,
734
+ pull_number: requirePullRequestNumber(input.pullRequestId),
735
+ per_page: 30,
736
+ page
737
+ });
738
+ return {
739
+ comments: response.data.map((comment) => parseReviewComment(comment)),
740
+ nextCursor: response.data.length === 30 ? String(page + 1) : null
741
+ };
742
+ }
743
+ async #createReviewComment(input) {
744
+ const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
745
+ const pullNumber = requirePullRequestNumber(input.pullRequestId);
746
+ if (input.replyToId) {
747
+ const { data } = await octokit.pulls.createReplyForReviewComment({
748
+ ...parts,
749
+ pull_number: pullNumber,
750
+ comment_id: requirePositiveId(input.replyToId, "review comment"),
751
+ body: input.body
752
+ });
753
+ return parseReviewComment(data);
754
+ }
755
+ if (!input.commitId || !input.path || input.line === void 0 || !input.side) throw new Error("A review comment requires commitId, path, line, and side unless it is a reply.");
756
+ if (input.startLine === void 0 !== (input.startSide === void 0)) throw new Error("A multi-line review comment requires both startLine and startSide.");
757
+ const { data } = await octokit.pulls.createReviewComment({
758
+ ...parts,
759
+ pull_number: pullNumber,
760
+ body: input.body,
761
+ commit_id: input.commitId,
762
+ path: input.path,
763
+ line: input.line,
764
+ side: input.side.toUpperCase(),
765
+ start_line: input.startLine,
766
+ start_side: input.startSide?.toUpperCase()
767
+ });
768
+ return parseReviewComment(data);
769
+ }
770
+ async #updateReviewComment(input) {
771
+ const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
772
+ const { data } = await octokit.pulls.updateReviewComment({
773
+ ...parts,
774
+ comment_id: requirePositiveId(input.commentId, "review comment"),
775
+ body: input.body
776
+ });
777
+ return parseReviewComment(data);
778
+ }
779
+ async #deleteReviewComment(input) {
780
+ const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
781
+ await octokit.pulls.deleteReviewComment({
782
+ ...parts,
783
+ comment_id: requirePositiveId(input.commentId, "review comment")
784
+ });
785
+ }
786
+ async #listRequestedReviewers(input) {
787
+ const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
788
+ const { data } = await octokit.pulls.listRequestedReviewers({
789
+ ...parts,
790
+ pull_number: requirePullRequestNumber(input.pullRequestId)
791
+ });
792
+ return parseRequestedReviewers(data);
793
+ }
794
+ async #requestReviewers(input) {
795
+ const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
796
+ const { data } = await octokit.pulls.requestReviewers({
797
+ ...parts,
798
+ pull_number: requirePullRequestNumber(input.pullRequestId),
799
+ reviewers: input.users ?? [],
800
+ team_reviewers: input.teams ?? []
801
+ });
802
+ return parseRequestedReviewers(data);
803
+ }
804
+ async #removeReviewers(input) {
805
+ const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
806
+ const { data } = await octokit.pulls.removeRequestedReviewers({
807
+ ...parts,
808
+ pull_number: requirePullRequestNumber(input.pullRequestId),
809
+ reviewers: input.users ?? [],
810
+ team_reviewers: input.teams ?? []
811
+ });
812
+ return parseRequestedReviewers(data);
813
+ }
814
+ /** Add labels to an issue (deduplicated; no-op on empty/malformed input). */
815
+ async addIssueLabels(installationId, repoFullName, issueNumber, labels) {
816
+ const parts = splitRepoFullName(repoFullName);
817
+ if (!parts) return;
818
+ const uniqueLabels = [...new Set(labels.map((label) => label.trim()).filter(Boolean))];
819
+ if (uniqueLabels.length === 0) return;
820
+ await this.getInstallationOctokit(installationId).issues.addLabels({
821
+ owner: parts.owner,
822
+ repo: parts.repo,
823
+ issue_number: issueNumber,
824
+ labels: uniqueLabels
825
+ });
826
+ }
827
+ /**
828
+ * List one page of a repo's open issues through an installation token. The
829
+ * issues API also returns pull requests, so those are filtered out (the
830
+ * filter can make a non-final page shorter than the page size — `nextPage`
831
+ * is derived from the raw response length, not the filtered one).
832
+ */
833
+ async listRepoOpenIssues(installationId, repoFullName, page, options = {}) {
834
+ const parts = splitRepoFullName(repoFullName);
835
+ if (!parts) return {
836
+ issues: [],
837
+ nextPage: null
838
+ };
839
+ const response = await this.getInstallationOctokit(installationId).issues.listForRepo({
840
+ owner: parts.owner,
841
+ repo: parts.repo,
842
+ state: "open",
843
+ labels: options.label,
844
+ per_page: 30,
845
+ page
846
+ });
847
+ return {
848
+ issues: response.data.filter((issue) => !issue.pull_request).map((issue) => ({
849
+ number: issue.number,
850
+ title: issue.title,
851
+ url: issue.html_url,
852
+ author: issue.user?.login ?? null,
853
+ labels: issue.labels.map((label) => typeof label === "string" ? label : label.name ?? "").filter(Boolean),
854
+ comments: issue.comments,
855
+ createdAt: issue.created_at,
856
+ updatedAt: issue.updated_at
857
+ })),
858
+ nextPage: response.data.length === 30 ? page + 1 : null
859
+ };
860
+ }
861
+ /**
862
+ * Build the GitHub App install URL. `state` is carried through the install
863
+ * flow and validated on callback.
864
+ */
865
+ buildInstallUrl(state) {
866
+ const url = new URL(`https://github.com/apps/${this.#slug}/installations/new`);
867
+ url.searchParams.set("state", state);
868
+ return url.toString();
869
+ }
870
+ /**
871
+ * Build the OAuth identify URL (authorize) used to confirm the user's
872
+ * identity and obtain a user token for listing their installations.
873
+ */
874
+ buildOAuthIdentifyUrl(state, redirectUri) {
875
+ const url = new URL("https://github.com/login/oauth/authorize");
876
+ url.searchParams.set("client_id", this.#clientId);
877
+ url.searchParams.set("redirect_uri", redirectUri);
878
+ url.searchParams.set("state", state);
879
+ return url.toString();
880
+ }
881
+ /** Exchange an OAuth `code` for a user access token. */
882
+ async exchangeOAuthCode(code, redirectUri) {
883
+ const res = await fetch("https://github.com/login/oauth/access_token", {
884
+ method: "POST",
885
+ signal: AbortSignal.timeout(1e4),
886
+ headers: {
887
+ "content-type": "application/json",
888
+ accept: "application/json"
889
+ },
890
+ body: JSON.stringify({
891
+ client_id: this.#clientId,
892
+ client_secret: this.#clientSecret,
893
+ code,
894
+ redirect_uri: redirectUri
895
+ })
896
+ });
897
+ if (!res.ok) throw new Error(`GitHub OAuth token exchange failed: ${res.status}`);
898
+ const data = await res.json();
899
+ if (!data.access_token) throw new Error(`GitHub OAuth token exchange returned no token: ${data.error_description ?? data.error ?? "unknown"}`);
900
+ return data.access_token;
901
+ }
902
+ /**
903
+ * The integration's HTTP surface: the `/web/github/*` + `/auth/github/*`
904
+ * Mastra `apiRoutes` (webhook handler, install/OAuth flow, project +
905
+ * worktree + session operations). The factory folds these into the server's
906
+ * `apiRoutes` when the feature is ready. Handlers operate on this instance.
907
+ */
908
+ routes(ctx) {
909
+ this.#storage = ctx.storage;
910
+ const ingestFactoryEvent = attachGithubRules(this, ctx);
911
+ return buildGithubRoutes({
912
+ github: this,
913
+ auth: ctx.auth,
914
+ fleet: ctx.fleet,
915
+ storage: ctx.factoryStorage,
916
+ stateSigner: ctx.stateSigner,
917
+ baseUrl: ctx.baseUrl,
918
+ controller: ctx.controller,
919
+ runIssueTriage: ctx.controller ? (input) => runGithubIssueTriage({
920
+ controller: ctx.controller,
921
+ input
922
+ }) : void 0,
923
+ emitAudit: ctx.hooks?.emitAudit,
924
+ projects: ctx.storage.projects,
925
+ ingestFactoryEvent
926
+ });
927
+ }
928
+ /**
929
+ * Session-scoped agent tools for token refresh and PR subscriptions in
930
+ * sessions bound to a GitHub-backed project. Empty elsewhere.
931
+ */
932
+ sessionTools({ requestContext }) {
933
+ return createGithubSubscriptionTools(requestContext, this);
934
+ }
935
+ async postToolObserver({ toolContext, requestContext }) {
936
+ const pullRequestUrl = parseCreatedPullRequest(toolContext);
937
+ if (!pullRequestUrl || !requestContext) return;
938
+ await subscribeCurrentSessionToPullRequest(requestContext, pullRequestUrl, "auto-gh-pr-create", this);
939
+ }
940
+ /** Non-secret config snapshot for system diagnostics/startup logs. */
941
+ diagnostics() {
942
+ return {
943
+ slug: this.#slug,
944
+ webhookSecretConfigured: this.#webhookSecret !== void 0
945
+ };
946
+ }
947
+ };
948
+ function parsePullRequest(pr) {
949
+ return {
950
+ id: String(pr.number),
951
+ title: pr.title,
952
+ url: pr.html_url,
953
+ author: pr.user?.login ?? null,
954
+ body: pr.body?.trim() ? pr.body : null,
955
+ state: pr.state === "closed" ? "closed" : "open",
956
+ draft: pr.draft ?? false,
957
+ merged: pr.merged ?? Boolean(pr.merged_at),
958
+ mergeable: typeof pr.mergeable === "boolean" ? pr.mergeable : null,
959
+ baseBranch: pr.base.ref,
960
+ headBranch: pr.head.ref,
961
+ headSha: pr.head.sha,
962
+ createdAt: pr.created_at,
963
+ updatedAt: pr.updated_at
964
+ };
965
+ }
966
+ function parsePullRequestComment(comment) {
967
+ return {
968
+ id: String(comment.id),
969
+ url: comment.html_url,
970
+ author: comment.user?.login ?? null,
971
+ body: comment.body ?? "",
972
+ createdAt: comment.created_at,
973
+ updatedAt: comment.updated_at
974
+ };
975
+ }
976
+ function parseReview(review) {
977
+ return {
978
+ id: String(review.id),
979
+ url: review.html_url ?? null,
980
+ author: review.user?.login ?? null,
981
+ body: review.body?.trim() ? review.body : null,
982
+ state: parseReviewState(review.state),
983
+ commitId: review.commit_id ?? null,
984
+ submittedAt: review.submitted_at ?? null
985
+ };
986
+ }
987
+ function parseReviewComment(comment) {
988
+ const parsed = parsePullRequestComment(comment);
989
+ const side = comment.side?.toLowerCase();
990
+ return {
991
+ ...parsed,
992
+ path: comment.path,
993
+ line: comment.line ?? null,
994
+ side: side === "left" || side === "right" ? side : null,
995
+ commitId: comment.commit_id,
996
+ replyToId: comment.in_reply_to_id ? String(comment.in_reply_to_id) : null
997
+ };
998
+ }
999
+ function parseRequestedReviewers(data) {
1000
+ return {
1001
+ users: (data.users ?? data.requested_reviewers ?? []).flatMap((user) => user.login ? [user.login] : []),
1002
+ teams: (data.teams ?? data.requested_teams ?? []).flatMap((team) => team.slug ? [team.slug] : [])
1003
+ };
1004
+ }
1005
+ function parseReviewState(state) {
1006
+ if (state === "PENDING") return "pending";
1007
+ if (state === "COMMENTED") return "commented";
1008
+ if (state === "APPROVED") return "approved";
1009
+ if (state === "CHANGES_REQUESTED") return "changes-requested";
1010
+ if (state === "DISMISSED") return "dismissed";
1011
+ throw new Error(`Unsupported GitHub review state: ${state}`);
1012
+ }
1013
+ function reviewEventToGithub(event) {
1014
+ if (event === "approve") return "APPROVE";
1015
+ if (event === "request-changes") return "REQUEST_CHANGES";
1016
+ return "COMMENT";
1017
+ }
1018
+ function requirePullRequestNumber(value) {
1019
+ return requirePositiveId(value, "pull request");
1020
+ }
1021
+ function requirePositiveId(value, resource) {
1022
+ const parsed = parsePositiveInteger(value);
1023
+ if (parsed === null) throw new Error(`GitHub ${resource} id must be a positive integer.`);
1024
+ return parsed;
1025
+ }
1026
+ function getGithubInstallationId(connection) {
1027
+ if (connection.type !== "app-installation") throw new Error("GitHub capabilities require an app-installation connection.");
1028
+ return connection.installationId;
1029
+ }
1030
+ function getSingleSourceId(sourceIds, message) {
1031
+ if (sourceIds.length !== 1) throw new Error(message);
1032
+ return sourceIds[0];
1033
+ }
1034
+ function normalizeLabels(labels) {
1035
+ return [...new Set((labels ?? []).map((label) => label.trim()).filter(Boolean))];
1036
+ }
1037
+ function requireSourceId(sourceId, message) {
1038
+ if (!sourceId) throw new Error(message);
1039
+ return sourceId;
1040
+ }
1041
+ function parsePositiveCursor(cursor) {
1042
+ if (cursor === void 0) return 1;
1043
+ const page = parsePositiveInteger(cursor);
1044
+ if (page === null) throw new Error("GitHub cursor must be a positive page number.");
1045
+ return page;
1046
+ }
1047
+ function parsePositiveInteger(value) {
1048
+ if (!/^\d+$/.test(value)) return null;
1049
+ const parsed = Number(value);
1050
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
1051
+ }
1052
+ function isNotFoundError(error) {
1053
+ return typeof error === "object" && error !== null && "status" in error && error.status === 404;
1054
+ }
1055
+ /** Split an `owner/name` full name into its parts, or `null` when malformed. */
1056
+ function splitRepoFullName(repoFullName) {
1057
+ const slash = repoFullName.indexOf("/");
1058
+ if (slash <= 0 || slash === repoFullName.length - 1) return null;
1059
+ return {
1060
+ owner: repoFullName.slice(0, slash),
1061
+ repo: repoFullName.slice(slash + 1)
1062
+ };
1063
+ }
1064
+ function parseGithubExternalTarget(externalId) {
1065
+ const match = externalId.match(/^github:(\d+):(?:issue|pull-request):(\d+)$/) ?? externalId.match(/^(\d+):(\d+)$/);
1066
+ if (!match?.[1] || !match[2]) return null;
1067
+ const repositoryId = parsePositiveInteger(match[1]);
1068
+ const issueId = parsePositiveInteger(match[2]);
1069
+ return repositoryId && issueId ? {
1070
+ repositoryId: String(repositoryId),
1071
+ issueId: String(issueId)
1072
+ } : null;
1073
+ }
1074
+ //#endregion
1075
+ export { GithubIntegration, LIST_PAGE_SIZE, normalizePrivateKey };
1076
+
1077
+ //# sourceMappingURL=integration.js.map