limn-review 0.1.123

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 (474) hide show
  1. package/LICENSE +110 -0
  2. package/README.md +133 -0
  3. package/dist/ai/runtime.js +535 -0
  4. package/dist/api/plugins/auth.js +169 -0
  5. package/dist/api/plugins/error-handler.js +114 -0
  6. package/dist/api/plugins/rate-limit.js +855 -0
  7. package/dist/api/plugins/security.js +321 -0
  8. package/dist/api/routes/activity.js +103 -0
  9. package/dist/api/routes/ai-runtime.js +67 -0
  10. package/dist/api/routes/auth.js +194 -0
  11. package/dist/api/routes/billing.js +209 -0
  12. package/dist/api/routes/bot-triage.js +1289 -0
  13. package/dist/api/routes/conflicts.js +730 -0
  14. package/dist/api/routes/contact.js +318 -0
  15. package/dist/api/routes/daily-brief.js +58 -0
  16. package/dist/api/routes/flow.js +71 -0
  17. package/dist/api/routes/health.js +7 -0
  18. package/dist/api/routes/insights.js +200 -0
  19. package/dist/api/routes/me.js +458 -0
  20. package/dist/api/routes/mentions.js +22 -0
  21. package/dist/api/routes/mergers.js +8 -0
  22. package/dist/api/routes/ml-labels.js +109 -0
  23. package/dist/api/routes/open-prs.js +35 -0
  24. package/dist/api/routes/prs.js +1633 -0
  25. package/dist/api/routes/reactions.js +217 -0
  26. package/dist/api/routes/repos.js +563 -0
  27. package/dist/api/routes/search.js +41 -0
  28. package/dist/api/routes/threads.js +118 -0
  29. package/dist/api/routes/timeline.js +150 -0
  30. package/dist/api/routes/users.js +67 -0
  31. package/dist/api/routes/webhooks.js +181 -0
  32. package/dist/api/routes/workspaces.js +322 -0
  33. package/dist/app.js +274 -0
  34. package/dist/ascii.js +19 -0
  35. package/dist/auth/account.js +482 -0
  36. package/dist/auth/crypto.js +46 -0
  37. package/dist/bot-triage/resolve.js +59 -0
  38. package/dist/cli.js +388 -0
  39. package/dist/coding/activity.js +80 -0
  40. package/dist/coding/agent.js +288 -0
  41. package/dist/coding/ai-fix/auto-fix.js +212 -0
  42. package/dist/coding/ai-fix/manager.js +370 -0
  43. package/dist/coding/ai-fix/persist.js +213 -0
  44. package/dist/coding/ai-fix/pr-context.js +58 -0
  45. package/dist/coding/ai-fix/prompts.js +138 -0
  46. package/dist/coding/ai-fix/review-seed.js +357 -0
  47. package/dist/coding/ai-fix/routes.js +425 -0
  48. package/dist/coding/git-ops.js +321 -0
  49. package/dist/coding/git.js +184 -0
  50. package/dist/coding/merge.js +262 -0
  51. package/dist/coding/safe-path.js +75 -0
  52. package/dist/coding/schema.js +43 -0
  53. package/dist/config.js +648 -0
  54. package/dist/conflict/diff.js +304 -0
  55. package/dist/conflict/git.js +118 -0
  56. package/dist/conflict/hash.js +73 -0
  57. package/dist/conflict/janitor.js +218 -0
  58. package/dist/conflict/land.js +675 -0
  59. package/dist/conflict/model-types.js +2 -0
  60. package/dist/conflict/model.js +1081 -0
  61. package/dist/conflict/parse.js +142 -0
  62. package/dist/conflict/seam.js +136 -0
  63. package/dist/conflict/session.js +532 -0
  64. package/dist/conflict/suggestion.js +398 -0
  65. package/dist/conflict/tokens.js +72 -0
  66. package/dist/data-dir.js +112 -0
  67. package/dist/db/actor-lanes.js +213 -0
  68. package/dist/db/automation-ids.js +97 -0
  69. package/dist/db/automation-output.js +349 -0
  70. package/dist/db/blast-radius-query.js +36 -0
  71. package/dist/db/blast-radius.js +226 -0
  72. package/dist/db/bot-dormancy.js +115 -0
  73. package/dist/db/bot-overlap.js +579 -0
  74. package/dist/db/bot-volume.js +722 -0
  75. package/dist/db/bot-window.js +35 -0
  76. package/dist/db/branch-queries.js +310 -0
  77. package/dist/db/change-shape.js +243 -0
  78. package/dist/db/changepoint.js +75 -0
  79. package/dist/db/cleanup.js +36 -0
  80. package/dist/db/client.js +102 -0
  81. package/dist/db/code-loc.js +388 -0
  82. package/dist/db/credits.js +79 -0
  83. package/dist/db/daily-brief.js +406 -0
  84. package/dist/db/dependency-cards.js +145 -0
  85. package/dist/db/erase-account.js +255 -0
  86. package/dist/db/export-account.js +189 -0
  87. package/dist/db/failing-checks.js +124 -0
  88. package/dist/db/file-coupling.js +197 -0
  89. package/dist/db/flow-detail.js +661 -0
  90. package/dist/db/flow-pointers.js +197 -0
  91. package/dist/db/flow-settings.js +67 -0
  92. package/dist/db/forecast.js +114 -0
  93. package/dist/db/line-overlap.js +81 -0
  94. package/dist/db/merge-queue.js +189 -0
  95. package/dist/db/migrate.js +17 -0
  96. package/dist/db/migrations/0000_purple_ben_grimm.sql +155 -0
  97. package/dist/db/migrations/0001_colorful_ozymandias.sql +31 -0
  98. package/dist/db/migrations/0002_famous_sersi.sql +9 -0
  99. package/dist/db/migrations/0003_clever_shinobi_shaw.sql +3 -0
  100. package/dist/db/migrations/0004_pale_scalphunter.sql +1 -0
  101. package/dist/db/migrations/0005_daffy_guardian.sql +2 -0
  102. package/dist/db/migrations/0006_cute_violations.sql +45 -0
  103. package/dist/db/migrations/0007_dark_anthem.sql +2 -0
  104. package/dist/db/migrations/0008_multitenant_accounts.sql +44 -0
  105. package/dist/db/migrations/0009_lean_bodies.sql +13 -0
  106. package/dist/db/migrations/0010_lean_bodies_nullable.sql +56 -0
  107. package/dist/db/migrations/0011_pr_diff_size.sql +10 -0
  108. package/dist/db/migrations/0012_findings_included_default.sql +7 -0
  109. package/dist/db/migrations/0013_review_routing.sql +9 -0
  110. package/dist/db/migrations/0014_account_last_active.sql +6 -0
  111. package/dist/db/migrations/0015_repos_inbox_watch.sql +9 -0
  112. package/dist/db/migrations/0016_repo_viewer_permission.sql +7 -0
  113. package/dist/db/migrations/0017_account_display_name.sql +7 -0
  114. package/dist/db/migrations/0018_finding_comment_kind.sql +7 -0
  115. package/dist/db/migrations/0019_finding_file_in_diff.sql +7 -0
  116. package/dist/db/migrations/0020_claude_review_cost_telemetry.sql +11 -0
  117. package/dist/db/migrations/0021_account_feed_last_seen.sql +7 -0
  118. package/dist/db/migrations/0022_ci_status_events.sql +21 -0
  119. package/dist/db/migrations/0023_ai_usage.sql +21 -0
  120. package/dist/db/migrations/0024_pr_head_ref_name.sql +6 -0
  121. package/dist/db/migrations/0025_account_billing.sql +7 -0
  122. package/dist/db/migrations/0026_ai_credit_allowance.sql +5 -0
  123. package/dist/db/migrations/0027_user_github_type.sql +6 -0
  124. package/dist/db/migrations/0028_bot_review_classification.sql +21 -0
  125. package/dist/db/migrations/0029_bot_mute_rules.sql +19 -0
  126. package/dist/db/migrations/0030_teams.sql +32 -0
  127. package/dist/db/migrations/0031_events_pr_idx.sql +7 -0
  128. package/dist/db/migrations/0032_benchmark_contributions.sql +29 -0
  129. package/dist/db/migrations/0033_thread_addressed_confidence.sql +9 -0
  130. package/dist/db/migrations/0034_thread_resolved_at.sql +6 -0
  131. package/dist/db/migrations/0035_pr_first_review_requested_at.sql +5 -0
  132. package/dist/db/migrations/0036_search_index.sql +25 -0
  133. package/dist/db/migrations/0037_author_indexes.sql +15 -0
  134. package/dist/db/migrations/0038_auto_merge_requests.sql +37 -0
  135. package/dist/db/migrations/0039_branch_status.sql +44 -0
  136. package/dist/db/migrations/0040_pr_review_decision.sql +10 -0
  137. package/dist/db/migrations/0041_branch_commit_detail.sql +38 -0
  138. package/dist/db/migrations/0042_repo_reviewers.sql +176 -0
  139. package/dist/db/migrations/0043_account_reviewers.sql +131 -0
  140. package/dist/db/migrations/0044_workspaces.sql +138 -0
  141. package/dist/db/migrations/0045_workspace_reviewers.sql +180 -0
  142. package/dist/db/migrations/0046_drop_inbox_watch.sql +25 -0
  143. package/dist/db/migrations/0047_ml_comment_labels.sql +40 -0
  144. package/dist/db/migrations/0048_ml_vendor_severity.sql +31 -0
  145. package/dist/db/migrations/0049_seat_pricing_repo_description.sql +21 -0
  146. package/dist/db/migrations/0050_users_app_slug.sql +12 -0
  147. package/dist/db/migrations/0051_auto_merge_queue.sql +17 -0
  148. package/dist/db/migrations/0052_trunk_ci_status_events.sql +43 -0
  149. package/dist/db/migrations/0053_automation_roles.sql +79 -0
  150. package/dist/db/migrations/0054_automation_vendor_kinds.sql +514 -0
  151. package/dist/db/migrations/0055_auto_merge_phase.sql +12 -0
  152. package/dist/db/migrations/0056_pr_mentions.sql +22 -0
  153. package/dist/db/migrations/0057_large_pr_threshold.sql +14 -0
  154. package/dist/db/migrations/0058_pending_mute.sql +53 -0
  155. package/dist/db/migrations/0059_merge_queue_state.sql +33 -0
  156. package/dist/db/migrations/0060_drop_my_turn_dismissals.sql +33 -0
  157. package/dist/db/migrations/0061_auto_merge_durable_anchors.sql +27 -0
  158. package/dist/db/migrations/0062_blast_radius_config.sql +21 -0
  159. package/dist/db/migrations/0063_repo_file_coupling.sql +33 -0
  160. package/dist/db/migrations/0064_pr_content_kind.sql +21 -0
  161. package/dist/db/migrations/0065_workspace_flow_settings.sql +13 -0
  162. package/dist/db/migrations/0066_review_request_events.sql +31 -0
  163. package/dist/db/migrations/0067_pr_security_signals.sql +86 -0
  164. package/dist/db/migrations/0068_my_turn_settings.sql +18 -0
  165. package/dist/db/migrations/0069_my_turn_dismissals.sql +41 -0
  166. package/dist/db/migrations/0070_claude_review_follow_up_ticket.sql +22 -0
  167. package/dist/db/migrations/0071_claude_review_trigger.sql +11 -0
  168. package/dist/db/migrations/0072_commit_message_headline.sql +13 -0
  169. package/dist/db/migrations/0073_claude_review_chat.sql +31 -0
  170. package/dist/db/migrations/0074_adopt_agentic_tables.sql +98 -0
  171. package/dist/db/migrations/0075_review_lens_drop_learnings.sql +15 -0
  172. package/dist/db/migrations/0076_claude_review_threads.sql +17 -0
  173. package/dist/db/migrations/0077_claude_review_ci_failures.sql +10 -0
  174. package/dist/db/migrations/0078_ai_fix_change_report.sql +17 -0
  175. package/dist/db/migrations/0079_claude_finding_story.sql +16 -0
  176. package/dist/db/migrations/meta/0000_snapshot.json +1116 -0
  177. package/dist/db/migrations/meta/0001_snapshot.json +1321 -0
  178. package/dist/db/migrations/meta/0002_snapshot.json +1375 -0
  179. package/dist/db/migrations/meta/0003_snapshot.json +1396 -0
  180. package/dist/db/migrations/meta/0004_snapshot.json +1416 -0
  181. package/dist/db/migrations/meta/0005_snapshot.json +1430 -0
  182. package/dist/db/migrations/meta/0006_snapshot.json +1745 -0
  183. package/dist/db/migrations/meta/0007_snapshot.json +1759 -0
  184. package/dist/db/migrations/meta/_journal.json +566 -0
  185. package/dist/db/migrations-pg/0000_tired_alex_power.sql +268 -0
  186. package/dist/db/migrations-pg/0001_dizzy_mockingbird.sql +3 -0
  187. package/dist/db/migrations-pg/0002_glorious_ikaris.sql +4 -0
  188. package/dist/db/migrations-pg/0003_melted_secret_warriors.sql +2 -0
  189. package/dist/db/migrations-pg/0004_numerous_triton.sql +1 -0
  190. package/dist/db/migrations-pg/0005_thick_blockbuster.sql +2 -0
  191. package/dist/db/migrations-pg/0006_majestic_dracula.sql +1 -0
  192. package/dist/db/migrations-pg/0007_same_human_torch.sql +2 -0
  193. package/dist/db/migrations-pg/0008_slimy_toxin.sql +1 -0
  194. package/dist/db/migrations-pg/0009_fuzzy_jubilee.sql +4 -0
  195. package/dist/db/migrations-pg/0010_late_hammerhead.sql +1 -0
  196. package/dist/db/migrations-pg/0011_happy_leech.sql +16 -0
  197. package/dist/db/migrations-pg/0012_woozy_captain_midlands.sql +16 -0
  198. package/dist/db/migrations-pg/0013_yielding_nicolaos.sql +1 -0
  199. package/dist/db/migrations-pg/0014_wakeful_bill_hollister.sql +2 -0
  200. package/dist/db/migrations-pg/0015_aspiring_black_tom.sql +1 -0
  201. package/dist/db/migrations-pg/0016_cute_ghost_rider.sql +30 -0
  202. package/dist/db/migrations-pg/0017_far_changeling.sql +24 -0
  203. package/dist/db/migrations-pg/0018_fancy_miss_america.sql +1 -0
  204. package/dist/db/migrations-pg/0019_benchmark_contributions.sql +22 -0
  205. package/dist/db/migrations-pg/0020_majestic_edwin_jarvis.sql +3 -0
  206. package/dist/db/migrations-pg/0021_green_salo.sql +1 -0
  207. package/dist/db/migrations-pg/0022_wise_baron_strucker.sql +1 -0
  208. package/dist/db/migrations-pg/0023_search_index.sql +19 -0
  209. package/dist/db/migrations-pg/0024_author_indexes.sql +9 -0
  210. package/dist/db/migrations-pg/0025_auto_merge_requests.sql +26 -0
  211. package/dist/db/migrations-pg/0026_branch_status.sql +32 -0
  212. package/dist/db/migrations-pg/0027_pr_review_decision.sql +5 -0
  213. package/dist/db/migrations-pg/0028_branch_commit_detail.sql +9 -0
  214. package/dist/db/migrations-pg/0029_repo_reviewers.sql +127 -0
  215. package/dist/db/migrations-pg/0030_account_reviewers.sql +77 -0
  216. package/dist/db/migrations-pg/0031_workspaces.sql +131 -0
  217. package/dist/db/migrations-pg/0032_workspace_reviewers.sql +165 -0
  218. package/dist/db/migrations-pg/0033_drop_inbox_watch.sql +14 -0
  219. package/dist/db/migrations-pg/0034_ml_comment_labels.sql +33 -0
  220. package/dist/db/migrations-pg/0035_ml_vendor_severity.sql +16 -0
  221. package/dist/db/migrations-pg/0036_seat_pricing_repo_description.sql +12 -0
  222. package/dist/db/migrations-pg/0037_users_app_slug.sql +9 -0
  223. package/dist/db/migrations-pg/0038_auto_merge_queue.sql +10 -0
  224. package/dist/db/migrations-pg/0039_trunk_ci_status_events.sql +27 -0
  225. package/dist/db/migrations-pg/0040_automation_roles.sql +63 -0
  226. package/dist/db/migrations-pg/0041_automation_vendor_kinds.sql +488 -0
  227. package/dist/db/migrations-pg/0042_auto_merge_phase.sql +8 -0
  228. package/dist/db/migrations-pg/0043_pr_mentions.sql +19 -0
  229. package/dist/db/migrations-pg/0044_large_pr_threshold.sql +6 -0
  230. package/dist/db/migrations-pg/0045_pending_mute.sql +26 -0
  231. package/dist/db/migrations-pg/0046_merge_queue_state.sql +18 -0
  232. package/dist/db/migrations-pg/0047_drop_my_turn_dismissals.sql +21 -0
  233. package/dist/db/migrations-pg/0048_auto_merge_durable_anchors.sql +27 -0
  234. package/dist/db/migrations-pg/0049_blast_radius_config.sql +8 -0
  235. package/dist/db/migrations-pg/0050_repo_file_coupling.sql +13 -0
  236. package/dist/db/migrations-pg/0051_pr_content_kind.sql +6 -0
  237. package/dist/db/migrations-pg/0052_workspace_flow_settings.sql +4 -0
  238. package/dist/db/migrations-pg/0053_review_request_events.sql +18 -0
  239. package/dist/db/migrations-pg/0054_pr_security_signals.sql +59 -0
  240. package/dist/db/migrations-pg/0055_my_turn_settings.sql +6 -0
  241. package/dist/db/migrations-pg/0056_my_turn_dismissals.sql +21 -0
  242. package/dist/db/migrations-pg/0057_claude_review_follow_up_ticket.sql +8 -0
  243. package/dist/db/migrations-pg/0058_claude_review_trigger.sql +4 -0
  244. package/dist/db/migrations-pg/0059_commit_message_headline.sql +3 -0
  245. package/dist/db/migrations-pg/0060_claude_review_chat.sql +24 -0
  246. package/dist/db/migrations-pg/0061_adopt_agentic_tables.sql +79 -0
  247. package/dist/db/migrations-pg/0062_review_lens_drop_learnings.sql +5 -0
  248. package/dist/db/migrations-pg/0063_claude_review_threads.sql +7 -0
  249. package/dist/db/migrations-pg/0064_claude_review_ci_failures.sql +4 -0
  250. package/dist/db/migrations-pg/0065_ai_fix_change_report.sql +9 -0
  251. package/dist/db/migrations-pg/0066_claude_finding_story.sql +7 -0
  252. package/dist/db/migrations-pg/meta/0000_snapshot.json +2137 -0
  253. package/dist/db/migrations-pg/meta/0001_snapshot.json +2143 -0
  254. package/dist/db/migrations-pg/meta/0002_snapshot.json +2170 -0
  255. package/dist/db/migrations-pg/meta/0003_snapshot.json +2182 -0
  256. package/dist/db/migrations-pg/meta/0004_snapshot.json +2188 -0
  257. package/dist/db/migrations-pg/meta/0005_snapshot.json +2201 -0
  258. package/dist/db/migrations-pg/meta/0006_snapshot.json +2207 -0
  259. package/dist/db/migrations-pg/meta/0007_snapshot.json +2219 -0
  260. package/dist/db/migrations-pg/meta/0008_snapshot.json +2226 -0
  261. package/dist/db/migrations-pg/meta/0009_snapshot.json +2250 -0
  262. package/dist/db/migrations-pg/meta/0010_snapshot.json +2256 -0
  263. package/dist/db/migrations-pg/meta/0011_snapshot.json +2412 -0
  264. package/dist/db/migrations-pg/meta/0012_snapshot.json +2528 -0
  265. package/dist/db/migrations-pg/meta/0013_snapshot.json +2534 -0
  266. package/dist/db/migrations-pg/meta/0014_snapshot.json +2547 -0
  267. package/dist/db/migrations-pg/meta/0015_snapshot.json +2553 -0
  268. package/dist/db/migrations-pg/meta/0016_snapshot.json +2774 -0
  269. package/dist/db/migrations-pg/meta/0017_snapshot.json +2999 -0
  270. package/dist/db/migrations-pg/meta/0018_snapshot.json +3020 -0
  271. package/dist/db/migrations-pg/meta/0020_snapshot.json +3228 -0
  272. package/dist/db/migrations-pg/meta/0021_snapshot.json +3234 -0
  273. package/dist/db/migrations-pg/meta/0022_snapshot.json +3240 -0
  274. package/dist/db/migrations-pg/meta/_journal.json +475 -0
  275. package/dist/db/ml-labels.js +1666 -0
  276. package/dist/db/my-turn-dismissals.js +343 -0
  277. package/dist/db/my-turn-settings.js +34 -0
  278. package/dist/db/pending-classify.js +46 -0
  279. package/dist/db/pending-mute.js +171 -0
  280. package/dist/db/pending-tabs.js +238 -0
  281. package/dist/db/period-metrics.js +820 -0
  282. package/dist/db/person-period.js +763 -0
  283. package/dist/db/pr-intervals.js +930 -0
  284. package/dist/db/pr-liveness.js +240 -0
  285. package/dist/db/pr-mentions.js +279 -0
  286. package/dist/db/pr-merge-queue-stamp.js +91 -0
  287. package/dist/db/queries.js +14631 -0
  288. package/dist/db/reaction-targets.js +94 -0
  289. package/dist/db/repo-activity.js +171 -0
  290. package/dist/db/retention.js +138 -0
  291. package/dist/db/review-threads-for-review.js +240 -0
  292. package/dist/db/run-migrations.js +24 -0
  293. package/dist/db/schema.pg.js +1220 -0
  294. package/dist/db/schema.sqlite.js +1801 -0
  295. package/dist/db/search.js +317 -0
  296. package/dist/db/security-alerts.js +218 -0
  297. package/dist/db/synthesis-input.js +565 -0
  298. package/dist/db/triage.js +320 -0
  299. package/dist/db/usage.js +63 -0
  300. package/dist/db/work-plan.js +1086 -0
  301. package/dist/db/working-hours.js +161 -0
  302. package/dist/feed/my-turn.js +122 -0
  303. package/dist/github/actions-logs.js +397 -0
  304. package/dist/github/auth.js +86 -0
  305. package/dist/github/branch-queries.js +229 -0
  306. package/dist/github/client.js +454 -0
  307. package/dist/github/codeowners.js +221 -0
  308. package/dist/github/commit-checks.js +105 -0
  309. package/dist/github/compare.js +110 -0
  310. package/dist/github/diff-anchor.js +227 -0
  311. package/dist/github/mutations.js +706 -0
  312. package/dist/github/pr-file-content.js +254 -0
  313. package/dist/github/pr-liveness.js +161 -0
  314. package/dist/github/queries.js +875 -0
  315. package/dist/github/rate-budget.js +129 -0
  316. package/dist/github/reactions.js +161 -0
  317. package/dist/github/reviewer-suggest.js +104 -0
  318. package/dist/github/team-reviewers.js +96 -0
  319. package/dist/index.js +184 -0
  320. package/dist/merge/auto-merge-runner.js +960 -0
  321. package/dist/ml/severity-client.js +218 -0
  322. package/dist/pr/detail-enricher.js +26 -0
  323. package/dist/pro/bind.js +302 -0
  324. package/dist/pro/contract.js +37 -0
  325. package/dist/pro/migrate.js +64 -0
  326. package/dist/review/agent-context.js +127 -0
  327. package/dist/review/agent.js +385 -0
  328. package/dist/review/agentic.js +59 -0
  329. package/dist/review/auth.js +94 -0
  330. package/dist/review/chat-agent.js +179 -0
  331. package/dist/review/claude-review/ai-ready.js +16 -0
  332. package/dist/review/claude-review/auto-settings.js +139 -0
  333. package/dist/review/claude-review/auto.js +244 -0
  334. package/dist/review/claude-review/chat.js +489 -0
  335. package/dist/review/claude-review/ci-failures.js +350 -0
  336. package/dist/review/claude-review/follow-up.js +310 -0
  337. package/dist/review/claude-review/manager.js +686 -0
  338. package/dist/review/claude-review/persist.js +996 -0
  339. package/dist/review/claude-review/prompts.js +357 -0
  340. package/dist/review/claude-review/routes.js +518 -0
  341. package/dist/review/claude-review/routing.js +61 -0
  342. package/dist/review/claude-review/specialists.js +190 -0
  343. package/dist/review/claude-review/threads.js +265 -0
  344. package/dist/review/claude-review/ticket.js +282 -0
  345. package/dist/review/clone-hygiene.js +380 -0
  346. package/dist/review/clone-manager.js +367 -0
  347. package/dist/review/llm.js +113 -0
  348. package/dist/review/model-options.js +48 -0
  349. package/dist/review/plugin-providers.js +12 -0
  350. package/dist/review/post-review.js +216 -0
  351. package/dist/review/post-seam.js +161 -0
  352. package/dist/review/prepare.js +168 -0
  353. package/dist/review/pricing.js +17 -0
  354. package/dist/review/schema.js +156 -0
  355. package/dist/review/submit-map.js +41 -0
  356. package/dist/review/usage.js +90 -0
  357. package/dist/shared/claude-review.js +335 -0
  358. package/dist/shared/conflict-fold.js +151 -0
  359. package/dist/shared/conflicts.js +30 -0
  360. package/dist/shared/flow-settings.js +191 -0
  361. package/dist/shared/index.js +8 -0
  362. package/dist/shared/my-turn-settings.js +238 -0
  363. package/dist/shared/pending-rules.js +281 -0
  364. package/dist/shared/product.js +54 -0
  365. package/dist/shared/types.js +896 -0
  366. package/dist/status.js +386 -0
  367. package/dist/sync/adaptive.js +185 -0
  368. package/dist/sync/app-attribution.js +66 -0
  369. package/dist/sync/auth-notices.js +36 -0
  370. package/dist/sync/backfill-ci-history.js +315 -0
  371. package/dist/sync/backfill-commit-headlines.js +136 -0
  372. package/dist/sync/backfill-pr-security.js +175 -0
  373. package/dist/sync/backfill-review-requests.js +97 -0
  374. package/dist/sync/benchmark-rollup.js +50 -0
  375. package/dist/sync/bot-detection.js +309 -0
  376. package/dist/sync/bot-resolution-markers.js +30 -0
  377. package/dist/sync/branch-status.js +634 -0
  378. package/dist/sync/classify-change-shape.js +150 -0
  379. package/dist/sync/commit-files.js +74 -0
  380. package/dist/sync/derive-thread-state.js +99 -0
  381. package/dist/sync/drain-review-threads.js +212 -0
  382. package/dist/sync/hydrate-detail.js +482 -0
  383. package/dist/sync/mention-scan.js +89 -0
  384. package/dist/sync/ml-enrichment.js +478 -0
  385. package/dist/sync/pr-change-signal.js +96 -0
  386. package/dist/sync/pr-liveness-sweep.js +139 -0
  387. package/dist/sync/pr-settle.js +381 -0
  388. package/dist/sync/refresh-pr.js +163 -0
  389. package/dist/sync/reparse-vendor-badges.js +495 -0
  390. package/dist/sync/resync-after-write.js +280 -0
  391. package/dist/sync/review-fingerprint.js +117 -0
  392. package/dist/sync/reviewer-behavior.js +103 -0
  393. package/dist/sync/reviewer-classify.js +503 -0
  394. package/dist/sync/routing-files.js +97 -0
  395. package/dist/sync/scheduled-jobs.js +12 -0
  396. package/dist/sync/scheduler.js +159 -0
  397. package/dist/sync/security-detect.js +521 -0
  398. package/dist/sync/sync-manager.js +750 -0
  399. package/dist/sync/sync-one-pr.js +205 -0
  400. package/dist/sync/sync-repo.js +400 -0
  401. package/dist/sync/unsettled-prs.js +506 -0
  402. package/dist/sync/upsert.js +1190 -0
  403. package/package.json +56 -0
  404. package/public/apple-touch-icon.png +0 -0
  405. package/public/assets/AiFixTab-CQP6PT5u.js +2 -0
  406. package/public/assets/ClaudeReviewTab-DQJ-58ZA.js +8 -0
  407. package/public/assets/PrBotBehaviourTab-CCnFrWbC.js +1 -0
  408. package/public/assets/archivo-latin-600-BN-JkNrl.woff2 +0 -0
  409. package/public/assets/highlight-BEHUn5zE.css +10 -0
  410. package/public/assets/highlight-CIIZLa6Z.js +12 -0
  411. package/public/assets/index-BDnYs6zg.js +73 -0
  412. package/public/assets/index-mCuI--RC.css +1 -0
  413. package/public/assets/markdown-I_JLLVAZ.js +32 -0
  414. package/public/assets/perfProbe-Da13EhTE.js +1 -0
  415. package/public/assets/tanstack-5qsxxaLe.js +17 -0
  416. package/public/assets/vis-C3BCkP5U.js +1280 -0
  417. package/public/assets/vis-ziCVehMy.css +1 -0
  418. package/public/favicon-16.png +0 -0
  419. package/public/favicon-32.png +0 -0
  420. package/public/favicon-48.png +0 -0
  421. package/public/favicon.svg +5 -0
  422. package/public/index.html +41 -0
  423. package/public-landing/apple-touch-icon.png +0 -0
  424. package/public-landing/assets/archivo-latin-500-BWmvx2Mg.woff2 +0 -0
  425. package/public-landing/assets/archivo-latin-600-BN-JkNrl.woff2 +0 -0
  426. package/public-landing/assets/index-CA3eEGXO.js +40 -0
  427. package/public-landing/assets/index-naQrQ6Fe.css +1 -0
  428. package/public-landing/assets/jetbrains-mono-latin-400-RruCX7RW.woff2 +0 -0
  429. package/public-landing/assets/newsreader-latin-400-DExrRRrN.woff2 +0 -0
  430. package/public-landing/assets/newsreader-latin-400-italic-b0I-AwbV.woff2 +0 -0
  431. package/public-landing/bots/index.html +156 -0
  432. package/public-landing/contact/index.html +156 -0
  433. package/public-landing/cookies/index.html +156 -0
  434. package/public-landing/demo/limn-walkthrough-poster.jpg +0 -0
  435. package/public-landing/demo/limn-walkthrough.cues.json +44 -0
  436. package/public-landing/demo/limn-walkthrough.mp4 +0 -0
  437. package/public-landing/favicon-16.png +0 -0
  438. package/public-landing/favicon-32.png +0 -0
  439. package/public-landing/favicon-48.png +0 -0
  440. package/public-landing/favicon.svg +5 -0
  441. package/public-landing/features/index.html +156 -0
  442. package/public-landing/for-developers/index.html +156 -0
  443. package/public-landing/for-managers/index.html +156 -0
  444. package/public-landing/how-it-works/index.html +156 -0
  445. package/public-landing/how-we-measure/index.html +156 -0
  446. package/public-landing/icon-192.png +0 -0
  447. package/public-landing/icon-512.png +0 -0
  448. package/public-landing/index.html +156 -0
  449. package/public-landing/insights/index.html +156 -0
  450. package/public-landing/og-image.png +0 -0
  451. package/public-landing/pricing/index.html +156 -0
  452. package/public-landing/privacy/index.html +156 -0
  453. package/public-landing/pro/index.html +156 -0
  454. package/public-landing/reviews/index.html +156 -0
  455. package/public-landing/robots.txt +8 -0
  456. package/public-landing/shots/benchmark.png +0 -0
  457. package/public-landing/shots/bot-roi.png +0 -0
  458. package/public-landing/shots/bot-settings.png +0 -0
  459. package/public-landing/shots/chronology.png +0 -0
  460. package/public-landing/shots/feed.png +0 -0
  461. package/public-landing/shots/flow-metrics.png +0 -0
  462. package/public-landing/shots/free-reports.png +0 -0
  463. package/public-landing/shots/pending-board.png +0 -0
  464. package/public-landing/shots/pending-card.png +0 -0
  465. package/public-landing/shots/period-report.png +0 -0
  466. package/public-landing/shots/pr-detail.png +0 -0
  467. package/public-landing/shots/pr-threads.png +0 -0
  468. package/public-landing/shots/reach.png +0 -0
  469. package/public-landing/shots/repo-rows.png +0 -0
  470. package/public-landing/shots/severity-findings.png +0 -0
  471. package/public-landing/shots/severity-strip.png +0 -0
  472. package/public-landing/site.webmanifest +14 -0
  473. package/public-landing/sitemap.xml +51 -0
  474. package/public-landing/terms/index.html +156 -0
@@ -0,0 +1,118 @@
1
+ import { getAccessToken, getAccountUserId } from '../../auth/account.js';
2
+ import { getThreadDetail, getThreadWriteContext, stampThreadRepliedState, stampThreadResolved, upsertLocalReply, } from '../../db/queries.js';
3
+ import { addReviewThreadReply, setReviewThreadResolved, } from '../../github/mutations.js';
4
+ import { hydrateThreadDetail } from '../../sync/hydrate-detail.js';
5
+ import { notePrChangedForPr } from '../../sync/pr-settle.js';
6
+ import { accountIdOf } from '../plugins/auth.js';
7
+ const idParamSchema = {
8
+ params: {
9
+ type: 'object',
10
+ required: ['id'],
11
+ properties: { id: { type: 'integer' } },
12
+ },
13
+ };
14
+ const replySchema = {
15
+ ...idParamSchema,
16
+ body: {
17
+ type: 'object',
18
+ required: ['body'],
19
+ additionalProperties: false,
20
+ properties: { body: { type: 'string' } },
21
+ },
22
+ };
23
+ const resolveSchema = {
24
+ ...idParamSchema,
25
+ body: {
26
+ type: 'object',
27
+ required: ['resolved'],
28
+ additionalProperties: false,
29
+ properties: { resolved: { type: 'boolean' } },
30
+ },
31
+ };
32
+ export async function threadRoutes(app) {
33
+ app.get('/api/threads/:id', { schema: idParamSchema }, async (req, reply) => {
34
+ const { id } = req.params;
35
+ const accountId = accountIdOf(req);
36
+ const thread = await getThreadDetail(id, accountId);
37
+ if (!thread) {
38
+ reply.status(404);
39
+ return { error: 'NotFound', message: `Thread ${id} not found` };
40
+ }
41
+ return hydrateThreadDetail(thread, accountId);
42
+ });
43
+ // Reply to an existing review thread. GraphQL addPullRequestReviewThreadReply,
44
+ // then optimistically stamp the new comment locally so it shows before sync.
45
+ app.post('/api/threads/:id/reply', { schema: replySchema }, async (req, reply) => {
46
+ const { id } = req.params;
47
+ const { body } = req.body;
48
+ const accountId = accountIdOf(req);
49
+ const ctx = await getThreadWriteContext(id, accountId);
50
+ if (!ctx) {
51
+ reply.status(404);
52
+ return { error: 'NotFound', message: `Thread ${id} not found` };
53
+ }
54
+ try {
55
+ const token = await getAccessToken(accountId);
56
+ const gh = await addReviewThreadReply(token, ctx.threadNodeId, body);
57
+ const authorId = await getAccountUserId(accountId);
58
+ const rowId = await upsertLocalReply(ctx.prId, id, authorId, gh);
59
+ // Bump the parent thread off 'untouched' so its badge reflects the reply
60
+ // before the next sync re-derives.
61
+ await stampThreadRepliedState(id);
62
+ // The stamp moved a board-visible fact (the thread's state, so the Pending thread
63
+ // cards): raise the SPA change signal BEFORE replying, so the write's own ordered
64
+ // `['repos']` read already sees it (sync/pr-settle.ts). Never throws.
65
+ await notePrChangedForPr(accountId, ctx.prId);
66
+ const result = {
67
+ id: rowId,
68
+ authorId,
69
+ body: gh.body,
70
+ diffHunk: null,
71
+ createdAt: new Date(gh.createdAt).toISOString(),
72
+ url: gh.url,
73
+ };
74
+ return result;
75
+ }
76
+ catch (err) {
77
+ reply.status(502);
78
+ return {
79
+ error: 'GitHubError',
80
+ message: err instanceof Error ? err.message : String(err),
81
+ };
82
+ }
83
+ });
84
+ // Resolve (resolved=true) or unresolve (resolved=false) a review thread, then
85
+ // stamp the local derivedState so the UI reflects it before the next sync.
86
+ app.post('/api/threads/:id/resolve', { schema: resolveSchema }, async (req, reply) => {
87
+ const { id } = req.params;
88
+ const { resolved } = req.body;
89
+ const accountId = accountIdOf(req);
90
+ const ctx = await getThreadWriteContext(id, accountId);
91
+ if (!ctx) {
92
+ reply.status(404);
93
+ return { error: 'NotFound', message: `Thread ${id} not found` };
94
+ }
95
+ try {
96
+ const token = await getAccessToken(accountId);
97
+ const gh = await setReviewThreadResolved(token, ctx.threadNodeId, resolved);
98
+ // Ownership already confirmed above, so the stamp is non-null.
99
+ const derivedState = await stampThreadResolved(id, resolved, accountId);
100
+ // Same as the reply: the stamp is board-visible, so signal it before replying.
101
+ await notePrChangedForPr(accountId, ctx.prId);
102
+ const result = {
103
+ threadId: id,
104
+ isResolved: gh.isResolved,
105
+ derivedState: derivedState ?? (resolved ? 'resolved' : 'untouched'),
106
+ };
107
+ return result;
108
+ }
109
+ catch (err) {
110
+ reply.status(502);
111
+ return {
112
+ error: 'GitHubError',
113
+ message: err instanceof Error ? err.message : String(err),
114
+ };
115
+ }
116
+ });
117
+ }
118
+ //# sourceMappingURL=threads.js.map
@@ -0,0 +1,150 @@
1
+ import { config } from '../../config.js';
2
+ import { getTimeline, resolveWorkspaceScope } from '../../db/queries.js';
3
+ import { accountIdOf } from '../plugins/auth.js';
4
+ const DAY_MS = 24 * 60 * 60 * 1000;
5
+ // Local copies of the shared value constants. `@pierre-review/shared` is a
6
+ // types-only workspace package that is NOT shipped in the published tarball, so
7
+ // the backend must not import runtime values from it (only `import type`, which
8
+ // `verbatimModuleSyntax` erases). Keep these in sync with packages/shared.
9
+ const EVENT_TYPES = [
10
+ 'pr_opened',
11
+ 'pr_merged',
12
+ 'pr_closed',
13
+ 'pr_reopened',
14
+ 'pr_ready_for_review',
15
+ 'review_submitted',
16
+ 'review_comment',
17
+ 'pr_comment',
18
+ 'commit_pushed',
19
+ ];
20
+ const PR_STATUSES = ['draft', 'open', 'merged', 'closed'];
21
+ const REVIEW_FILTER_STATES = [
22
+ 'approved',
23
+ 'changes_requested',
24
+ 'commented',
25
+ 'dismissed',
26
+ ];
27
+ // `repoIds` → the `narrow` argument of `resolveWorkspaceScope` (never a scope in its own right);
28
+ // `prIds`/`userIds`/`allowBotIds` keep their own meanings. Empty/absent → null = no narrowing.
29
+ function parseIntList(raw) {
30
+ if (!raw)
31
+ return null;
32
+ const ids = raw
33
+ .split(',')
34
+ .map((s) => Number.parseInt(s.trim(), 10))
35
+ .filter((n) => Number.isFinite(n));
36
+ return ids.length > 0 ? ids : null;
37
+ }
38
+ function parseTypes(raw) {
39
+ if (!raw)
40
+ return null;
41
+ const allowed = new Set(EVENT_TYPES);
42
+ const types = raw
43
+ .split(',')
44
+ .map((s) => s.trim())
45
+ .filter((s) => allowed.has(s));
46
+ return types.length > 0 ? types : null;
47
+ }
48
+ // Absent (undefined) → null = no status filter (show all). Present, even empty
49
+ // ("") → an explicit (possibly empty) set, so deselecting every status shows
50
+ // nothing rather than falling back to "all".
51
+ function parseStatuses(raw) {
52
+ if (raw === undefined)
53
+ return null;
54
+ const allowed = new Set(PR_STATUSES);
55
+ return raw
56
+ .split(',')
57
+ .map((s) => s.trim())
58
+ .filter((s) => allowed.has(s));
59
+ }
60
+ // Absent (undefined) → null = no review-verdict filter (show all). Present, even
61
+ // empty ("") → an explicit (possibly empty) set, so deselecting every verdict hides
62
+ // all review markers rather than falling back to "all". Mirrors parseStatuses.
63
+ function parseReviewStates(raw) {
64
+ if (raw === undefined)
65
+ return null;
66
+ const allowed = new Set(REVIEW_FILTER_STATES);
67
+ return raw
68
+ .split(',')
69
+ .map((s) => s.trim())
70
+ .filter((s) => allowed.has(s));
71
+ }
72
+ function parseDate(raw, fallback) {
73
+ if (!raw)
74
+ return fallback;
75
+ const d = new Date(raw);
76
+ return Number.isNaN(d.getTime()) ? fallback : d;
77
+ }
78
+ // ---- Window clamp ----
79
+ // `from`/`to` are free-form client dates, and getTimeline's cost scales with the span: it
80
+ // loads every matching PR and event in the range. `?from=1970-01-01&to=2100-01-01` therefore
81
+ // asked for the account's ENTIRE retained dataset in one JSON body — a cheap way for one
82
+ // authenticated request to exhaust the heap of a process that, in cloud, serves every tenant.
83
+ //
84
+ // Nothing legitimate needs a wide window: the SPA's widest preset is 90 days, and the server
85
+ // only retains `config.retentionDays` (default 180) of history anyway, so anything beyond
86
+ // that is asking for rows that cannot exist. The clamp is silent and generous rather than a
87
+ // 400, because a stale bookmark with an old `from=` should still render a board.
88
+ //
89
+ // Belt-and-braces: db/queries.ts additionally caps the ROW COUNT and reports `truncated`,
90
+ // because a legitimately-sized window can still be huge for a tenant watching 100 busy repos.
91
+ function clampWindow(from, to, now, maxSpanDays) {
92
+ // A reversed range would produce an empty board with no explanation — normalise it.
93
+ let start = from;
94
+ let end = to;
95
+ if (start.getTime() > end.getTime())
96
+ [start, end] = [end, start];
97
+ // Never look further ahead than "now" (+1 day of slack for clock skew / timezones).
98
+ const maxEnd = now.getTime() + DAY_MS;
99
+ if (end.getTime() > maxEnd)
100
+ end = new Date(maxEnd);
101
+ // Never look further back than the retention horizon — there is no data there.
102
+ const earliest = end.getTime() - maxSpanDays * DAY_MS;
103
+ if (start.getTime() < earliest)
104
+ start = new Date(earliest);
105
+ return { from: start, to: end };
106
+ }
107
+ export async function timelineRoutes(app) {
108
+ // The widest span a request may ask for: the retention horizon (there is nothing older
109
+ // to return), with a floor so a deployment that disables retention (RETENTION_DAYS=0)
110
+ // still gets a bound rather than an unbounded one.
111
+ const maxSpanDays = config.retentionDays > 0 ? config.retentionDays : 365;
112
+ // `?workspace=<id>` is the board's scope; `?repoIds=` only narrows WITHIN it.
113
+ //
114
+ // The board carried repo ids alone while `null` meant "every repo of the account". Under a
115
+ // workspace scope that is two bugs: an EMPTY workspace sends no ids and the server would render
116
+ // the whole account, and two workspaces both sending no ids produce the same query string, so
117
+ // React Query serves one board under the other's cache key. `resolveWorkspaceScope` returns
118
+ // `membership ∩ (?repoIds= ?? membership)` — always a concrete array, `[]` for an empty
119
+ // workspace, which `getTimeline` renders as an empty board rather than everything.
120
+ //
121
+ // ⚠ The pr-focus (`prIds`) branch inside `getTimeline` deliberately BYPASSES the repo scope: an
122
+ // isolate tab must load its subject PR even when the board's repo filter would hide it.
123
+ // Ownership is still bound by `accountId`, so that is a scope bypass, never a tenancy one.
124
+ app.get('/api/timeline', async (req) => {
125
+ const q = req.query;
126
+ const now = new Date();
127
+ const window = clampWindow(parseDate(q.from, new Date(now.getTime() - 14 * DAY_MS)), parseDate(q.to, now), now, maxSpanDays);
128
+ const accountId = accountIdOf(req);
129
+ const scope = await resolveWorkspaceScope(accountId, q.workspace, parseIntList(q.repoIds));
130
+ const filters = {
131
+ accountId,
132
+ // The excludeBots union reads the automated-reviewer verdict at this grain — the same
133
+ // resolved scope the repo narrowing came from, never a second resolution.
134
+ workspaceId: scope.workspaceId,
135
+ from: window.from,
136
+ to: window.to,
137
+ repoIds: scope.repoIds,
138
+ prIds: parseIntList(q.prIds),
139
+ userIds: parseIntList(q.userIds),
140
+ types: parseTypes(q.types),
141
+ statuses: parseStatuses(q.statuses),
142
+ reviewStates: parseReviewStates(q.reviewStates),
143
+ excludeBots: q.excludeBots === 'true',
144
+ allowBotIds: parseIntList(q.allowBotIds),
145
+ excludeStale: q.excludeStale === 'true',
146
+ };
147
+ return getTimeline(filters);
148
+ });
149
+ }
150
+ //# sourceMappingURL=timeline.js.map
@@ -0,0 +1,67 @@
1
+ import { getUserStats, listUsers, resolveWorkspaceScope } from '../../db/queries.js';
2
+ import { accountIdOf } from '../plugins/auth.js';
3
+ // Parse a comma-separated id list into a positive-int array, or null when empty/absent ("no
4
+ // explicit narrowing"). It is fed to `resolveWorkspaceScope` as the `narrow` argument, never used
5
+ // as a scope in its own right. Mirrors the parser in bot-triage.ts.
6
+ function parseIntList(raw) {
7
+ if (raw == null || raw.trim() === '')
8
+ return null;
9
+ const ids = raw
10
+ .split(',')
11
+ .map((s) => Number.parseInt(s.trim(), 10))
12
+ .filter((n) => Number.isInteger(n) && n > 0);
13
+ return ids.length > 0 ? ids : null;
14
+ }
15
+ // GET /api/users/:id/stats — all-time contribution counts for one user. `workspace` is the scope
16
+ // (a plain integer; absent / unknown / foreign ⇒ the account's Default workspace), and `repoIds`
17
+ // NARROWS within it rather than replacing it: `resolveWorkspaceScope` returns
18
+ // `membership ∩ (repoIds ?? membership)`, so a repo id from another workspace simply drops out and
19
+ // cannot measure one workspace through another's repos.
20
+ const statsSchema = {
21
+ params: {
22
+ type: 'object',
23
+ required: ['id'],
24
+ properties: { id: { type: 'integer' } },
25
+ },
26
+ querystring: {
27
+ type: 'object',
28
+ additionalProperties: false,
29
+ properties: {
30
+ workspace: { type: 'string' },
31
+ repoIds: { type: 'string' },
32
+ },
33
+ },
34
+ };
35
+ // GitHub actor metadata for the Members panel / avatars.
36
+ //
37
+ // `users` is a GLOBAL table (a login is the same person for every tenant), but the LISTING is
38
+ // account-scoped: `listUsers(accountId)` returns only actors that appear in the caller's own
39
+ // synced data. Unscoped, this route handed any tenant the login, display name and avatar of
40
+ // every user any other tenant had ever synced — including contributors to private repos the
41
+ // caller cannot see.
42
+ //
43
+ // `PATCH /api/users/:id` was REMOVED. It called `setUserBot(id, isBot)`, which wrote the
44
+ // `isBot` + sticky `isBotOverridden` flags to the global row with no ownership check, so one
45
+ // account could permanently reclassify any enumerable user id for every other tenant. It had
46
+ // no frontend caller — bot classification goes through the account-scoped
47
+ // `PATCH /api/bot-reviewers/:userId` (api/routes/bot-triage.ts), which writes the per-account,
48
+ // per-WORKSPACE `workspace_reviewers` table. See the note in db/queries.ts.
49
+ export async function userRoutes(app) {
50
+ app.get('/api/users', async (req) => listUsers(accountIdOf(req)));
51
+ // All-time PR / review / comment counts for one user, counted only over THIS account's
52
+ // synced data (getUserStats binds every source to pullRequests.accountId).
53
+ //
54
+ // There is deliberately NO ownership 404: `users` is global, so a foreign or unknown id is
55
+ // not an object this account can own or fail to own — it simply has no rows here and comes
56
+ // back all zeros. That keeps user ids non-enumerable (404 vs 200 would be an oracle) and
57
+ // leaks nothing, which is also why the response echoes no profile field.
58
+ app.get('/api/users/:id/stats', { schema: statsSchema }, async (req) => {
59
+ const { id } = req.params;
60
+ const { workspace, repoIds } = req.query;
61
+ const accountId = accountIdOf(req);
62
+ const scope = await resolveWorkspaceScope(accountId, workspace, parseIntList(repoIds));
63
+ const resp = await getUserStats(accountId, id, scope.repoIds);
64
+ return resp;
65
+ });
66
+ }
67
+ //# sourceMappingURL=users.js.map
@@ -0,0 +1,181 @@
1
+ // GitHub App webhook receiver — real-time sync Phase 1 (see docs/REALTIME-SYNC.md).
2
+ //
3
+ // A GitHub App has ONE webhook URL that receives events for every installation, so this
4
+ // single route serves every tenant. On a PR-affecting event it resolves which watched
5
+ // repos the change touches and fires a debounced, targeted single-PR sync (Phase 0's
6
+ // enqueuePrSync) — near-real-time freshness at ~1 GraphQL point per change, no window
7
+ // re-walk. It is ADDITIVE: the periodic poll (SYNC.md) stays as the backstop for dropped
8
+ // deliveries and for accounts signed in via the OAuth App (no App install → no webhooks).
9
+ //
10
+ // Structure mirrors the Stripe webhook (api/routes/billing.ts): authenticity is the
11
+ // signature, not a session (so the route is exempted from the cloud auth gate); the raw
12
+ // body is read via an ENCAPSULATED content-type parser so the rest of the API keeps
13
+ // normal JSON parsing. Registered in both modes; inert until GITHUB_APP_WEBHOOK_SECRET is
14
+ // set (replies 501 unconfigured).
15
+ import { createHmac, timingSafeEqual } from 'node:crypto';
16
+ import { and, eq } from 'drizzle-orm';
17
+ import { config } from '../../config.js';
18
+ import { db, schema } from '../../db/client.js';
19
+ import { enqueuePrSync } from '../../sync/sync-one-pr.js';
20
+ const { repos } = schema;
21
+ /**
22
+ * Verify a GitHub webhook signature (the `X-Hub-Signature-256` header) without a
23
+ * dependency. Format: `sha256=<hex>`, where the hex is HMAC-SHA256(secret) over the raw
24
+ * request bytes. Compared timing-safely. Pure so it's unit-testable without Fastify.
25
+ */
26
+ export function verifyGithubSignature(rawBody, header, secret) {
27
+ const prefix = 'sha256=';
28
+ if (!header.startsWith(prefix))
29
+ return false;
30
+ const provided = header.slice(prefix.length);
31
+ const expected = createHmac('sha256', secret).update(rawBody).digest('hex');
32
+ const providedBuf = Buffer.from(provided, 'utf-8');
33
+ const expectedBuf = Buffer.from(expected, 'utf-8');
34
+ return (providedBuf.length === expectedBuf.length &&
35
+ timingSafeEqual(providedBuf, expectedBuf));
36
+ }
37
+ function asRecord(v) {
38
+ return v !== null && typeof v === 'object' ? v : null;
39
+ }
40
+ // A positive integer `number` field off an object (a PR reference), else null.
41
+ function prNumberOf(v) {
42
+ const n = asRecord(v)?.['number'];
43
+ return typeof n === 'number' && Number.isInteger(n) && n > 0 ? n : null;
44
+ }
45
+ /**
46
+ * Map a GitHub webhook (event type + payload) to the repo + PR numbers it should refresh.
47
+ * Pure — no DB, no Fastify — so the per-event extraction is unit-testable. Every handled
48
+ * event carries `repository.owner.login` + `repository.name`; the PR number(s) live in a
49
+ * per-event slot. Returns null when there's nothing to sync.
50
+ */
51
+ export function extractPrTargets(eventType, payload) {
52
+ const p = asRecord(payload);
53
+ if (!p)
54
+ return null;
55
+ const repo = asRecord(p['repository']);
56
+ const owner = asRecord(repo?.['owner'])?.['login'];
57
+ const name = repo?.['name'];
58
+ if (typeof owner !== 'string' || typeof name !== 'string')
59
+ return null;
60
+ const numbers = new Set();
61
+ const addNum = (v) => {
62
+ const n = prNumberOf(v);
63
+ if (n != null)
64
+ numbers.add(n);
65
+ };
66
+ const addArray = (v) => {
67
+ if (Array.isArray(v))
68
+ for (const e of v)
69
+ addNum(e);
70
+ };
71
+ switch (eventType) {
72
+ // All the PR-scoped events carry the PR directly.
73
+ case 'pull_request':
74
+ case 'pull_request_review':
75
+ case 'pull_request_review_comment':
76
+ case 'pull_request_review_thread':
77
+ addNum(p['pull_request']);
78
+ break;
79
+ // issue_comment also fires on plain issues; only PRs carry `issue.pull_request`.
80
+ case 'issue_comment': {
81
+ const issue = asRecord(p['issue']);
82
+ if (issue && issue['pull_request'] != null)
83
+ addNum(issue);
84
+ break;
85
+ }
86
+ // CI events carry the associated PRs (same-repo only) — this is how a check
87
+ // finishing (which never bumps a PR's updatedAt) drives a refresh.
88
+ case 'check_run':
89
+ addArray(asRecord(p['check_run'])?.['pull_requests']);
90
+ break;
91
+ case 'check_suite':
92
+ addArray(asRecord(p['check_suite'])?.['pull_requests']);
93
+ break;
94
+ // ping / push / installation / anything else → no targeted PR (a push to a PR's head
95
+ // arrives separately as pull_request `synchronize`, which the case above handles).
96
+ default:
97
+ return null;
98
+ }
99
+ if (numbers.size === 0)
100
+ return null;
101
+ return { owner, name, prNumbers: [...numbers] };
102
+ }
103
+ export async function webhookRoutes(app) {
104
+ // A STABLE logger for the detached (debounced) syncs — the request-scoped req.log is
105
+ // gone by the time enqueuePrSync's timer fires. Mirrors scheduler.ts's adapter.
106
+ const log = {
107
+ info: (m, ...a) => app.log.info(a.length ? { a } : {}, m),
108
+ warn: (m, ...a) => app.log.warn(a.length ? { a } : {}, m),
109
+ error: (m, ...a) => app.log.error(a.length ? { a } : {}, m),
110
+ };
111
+ // The signature signs the exact bytes, so this route needs the RAW body. Fastify's
112
+ // content-type parsers are ENCAPSULATED, so registering the buffer parser in this
113
+ // nested scope affects ONLY /api/webhooks/github — the rest of the API keeps normal
114
+ // JSON parsing (proven by webhooks.test.ts's sibling-route case).
115
+ await app.register(async (scope) => {
116
+ scope.addContentTypeParser('application/json', { parseAs: 'buffer' }, (_req, body, done) => {
117
+ done(null, body);
118
+ });
119
+ // GitHub posts unauthenticated (exempted from the auth gate); authenticity is the
120
+ // signature. Never 500 on a data mismatch — GitHub retries non-2xx.
121
+ scope.post('/api/webhooks/github', async (req, reply) => {
122
+ if (!config.githubAppWebhookSecret) {
123
+ return reply.code(501).send({ error: 'github webhooks not configured' });
124
+ }
125
+ const raw = req.body;
126
+ if (!Buffer.isBuffer(raw)) {
127
+ return reply.code(400).send({ error: 'expected a JSON body' });
128
+ }
129
+ const sig = req.headers['x-hub-signature-256'];
130
+ if (typeof sig !== 'string' ||
131
+ !verifyGithubSignature(raw, sig, config.githubAppWebhookSecret)) {
132
+ return reply.code(401).send({ error: 'invalid signature' });
133
+ }
134
+ const eventType = req.headers['x-github-event'];
135
+ if (typeof eventType !== 'string') {
136
+ return reply.code(400).send({ error: 'missing X-GitHub-Event' });
137
+ }
138
+ // GitHub pings a newly-configured webhook once — ack it.
139
+ if (eventType === 'ping')
140
+ return { received: true, queued: 0 };
141
+ let payload;
142
+ try {
143
+ payload = JSON.parse(raw.toString('utf-8'));
144
+ }
145
+ catch {
146
+ return reply.code(400).send({ error: 'invalid JSON' });
147
+ }
148
+ const target = extractPrTargets(eventType, payload);
149
+ if (!target)
150
+ return { received: true, queued: 0 };
151
+ // Route by (owner, name) across ALL accounts watching this repo. `repos` is keyed
152
+ // (accountId, owner, name), so one webhook fans out to every tenant's copy — each
153
+ // synced with ITS OWN token inside syncOnePr (isolation is structural). No
154
+ // installation→account table is needed.
155
+ const rows = await db
156
+ .select({ id: repos.id })
157
+ .from(repos)
158
+ .where(and(eq(repos.owner, target.owner), eq(repos.name, target.name)))
159
+ .execute();
160
+ let queued = 0;
161
+ for (const row of rows) {
162
+ for (const prNumber of target.prNumbers) {
163
+ // Debounced + coalesced + reservation-guarded: a burst for one PR collapses to
164
+ // a single targeted sync (Phase 0). Fire-and-forget; syncOnePr swallows errors.
165
+ enqueuePrSync(row.id, prNumber, log);
166
+ queued += 1;
167
+ }
168
+ }
169
+ if (queued > 0) {
170
+ req.log.info({
171
+ repo: `${target.owner}/${target.name}`,
172
+ event: eventType,
173
+ prs: target.prNumbers,
174
+ watchers: rows.length,
175
+ }, 'github webhook → queued targeted sync');
176
+ }
177
+ return { received: true, queued };
178
+ });
179
+ });
180
+ }
181
+ //# sourceMappingURL=webhooks.js.map