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.
- package/LICENSE +110 -0
- package/README.md +133 -0
- package/dist/ai/runtime.js +535 -0
- package/dist/api/plugins/auth.js +169 -0
- package/dist/api/plugins/error-handler.js +114 -0
- package/dist/api/plugins/rate-limit.js +855 -0
- package/dist/api/plugins/security.js +321 -0
- package/dist/api/routes/activity.js +103 -0
- package/dist/api/routes/ai-runtime.js +67 -0
- package/dist/api/routes/auth.js +194 -0
- package/dist/api/routes/billing.js +209 -0
- package/dist/api/routes/bot-triage.js +1289 -0
- package/dist/api/routes/conflicts.js +730 -0
- package/dist/api/routes/contact.js +318 -0
- package/dist/api/routes/daily-brief.js +58 -0
- package/dist/api/routes/flow.js +71 -0
- package/dist/api/routes/health.js +7 -0
- package/dist/api/routes/insights.js +200 -0
- package/dist/api/routes/me.js +458 -0
- package/dist/api/routes/mentions.js +22 -0
- package/dist/api/routes/mergers.js +8 -0
- package/dist/api/routes/ml-labels.js +109 -0
- package/dist/api/routes/open-prs.js +35 -0
- package/dist/api/routes/prs.js +1633 -0
- package/dist/api/routes/reactions.js +217 -0
- package/dist/api/routes/repos.js +563 -0
- package/dist/api/routes/search.js +41 -0
- package/dist/api/routes/threads.js +118 -0
- package/dist/api/routes/timeline.js +150 -0
- package/dist/api/routes/users.js +67 -0
- package/dist/api/routes/webhooks.js +181 -0
- package/dist/api/routes/workspaces.js +322 -0
- package/dist/app.js +274 -0
- package/dist/ascii.js +19 -0
- package/dist/auth/account.js +482 -0
- package/dist/auth/crypto.js +46 -0
- package/dist/bot-triage/resolve.js +59 -0
- package/dist/cli.js +388 -0
- package/dist/coding/activity.js +80 -0
- package/dist/coding/agent.js +288 -0
- package/dist/coding/ai-fix/auto-fix.js +212 -0
- package/dist/coding/ai-fix/manager.js +370 -0
- package/dist/coding/ai-fix/persist.js +213 -0
- package/dist/coding/ai-fix/pr-context.js +58 -0
- package/dist/coding/ai-fix/prompts.js +138 -0
- package/dist/coding/ai-fix/review-seed.js +357 -0
- package/dist/coding/ai-fix/routes.js +425 -0
- package/dist/coding/git-ops.js +321 -0
- package/dist/coding/git.js +184 -0
- package/dist/coding/merge.js +262 -0
- package/dist/coding/safe-path.js +75 -0
- package/dist/coding/schema.js +43 -0
- package/dist/config.js +648 -0
- package/dist/conflict/diff.js +304 -0
- package/dist/conflict/git.js +118 -0
- package/dist/conflict/hash.js +73 -0
- package/dist/conflict/janitor.js +218 -0
- package/dist/conflict/land.js +675 -0
- package/dist/conflict/model-types.js +2 -0
- package/dist/conflict/model.js +1081 -0
- package/dist/conflict/parse.js +142 -0
- package/dist/conflict/seam.js +136 -0
- package/dist/conflict/session.js +532 -0
- package/dist/conflict/suggestion.js +398 -0
- package/dist/conflict/tokens.js +72 -0
- package/dist/data-dir.js +112 -0
- package/dist/db/actor-lanes.js +213 -0
- package/dist/db/automation-ids.js +97 -0
- package/dist/db/automation-output.js +349 -0
- package/dist/db/blast-radius-query.js +36 -0
- package/dist/db/blast-radius.js +226 -0
- package/dist/db/bot-dormancy.js +115 -0
- package/dist/db/bot-overlap.js +579 -0
- package/dist/db/bot-volume.js +722 -0
- package/dist/db/bot-window.js +35 -0
- package/dist/db/branch-queries.js +310 -0
- package/dist/db/change-shape.js +243 -0
- package/dist/db/changepoint.js +75 -0
- package/dist/db/cleanup.js +36 -0
- package/dist/db/client.js +102 -0
- package/dist/db/code-loc.js +388 -0
- package/dist/db/credits.js +79 -0
- package/dist/db/daily-brief.js +406 -0
- package/dist/db/dependency-cards.js +145 -0
- package/dist/db/erase-account.js +255 -0
- package/dist/db/export-account.js +189 -0
- package/dist/db/failing-checks.js +124 -0
- package/dist/db/file-coupling.js +197 -0
- package/dist/db/flow-detail.js +661 -0
- package/dist/db/flow-pointers.js +197 -0
- package/dist/db/flow-settings.js +67 -0
- package/dist/db/forecast.js +114 -0
- package/dist/db/line-overlap.js +81 -0
- package/dist/db/merge-queue.js +189 -0
- package/dist/db/migrate.js +17 -0
- package/dist/db/migrations/0000_purple_ben_grimm.sql +155 -0
- package/dist/db/migrations/0001_colorful_ozymandias.sql +31 -0
- package/dist/db/migrations/0002_famous_sersi.sql +9 -0
- package/dist/db/migrations/0003_clever_shinobi_shaw.sql +3 -0
- package/dist/db/migrations/0004_pale_scalphunter.sql +1 -0
- package/dist/db/migrations/0005_daffy_guardian.sql +2 -0
- package/dist/db/migrations/0006_cute_violations.sql +45 -0
- package/dist/db/migrations/0007_dark_anthem.sql +2 -0
- package/dist/db/migrations/0008_multitenant_accounts.sql +44 -0
- package/dist/db/migrations/0009_lean_bodies.sql +13 -0
- package/dist/db/migrations/0010_lean_bodies_nullable.sql +56 -0
- package/dist/db/migrations/0011_pr_diff_size.sql +10 -0
- package/dist/db/migrations/0012_findings_included_default.sql +7 -0
- package/dist/db/migrations/0013_review_routing.sql +9 -0
- package/dist/db/migrations/0014_account_last_active.sql +6 -0
- package/dist/db/migrations/0015_repos_inbox_watch.sql +9 -0
- package/dist/db/migrations/0016_repo_viewer_permission.sql +7 -0
- package/dist/db/migrations/0017_account_display_name.sql +7 -0
- package/dist/db/migrations/0018_finding_comment_kind.sql +7 -0
- package/dist/db/migrations/0019_finding_file_in_diff.sql +7 -0
- package/dist/db/migrations/0020_claude_review_cost_telemetry.sql +11 -0
- package/dist/db/migrations/0021_account_feed_last_seen.sql +7 -0
- package/dist/db/migrations/0022_ci_status_events.sql +21 -0
- package/dist/db/migrations/0023_ai_usage.sql +21 -0
- package/dist/db/migrations/0024_pr_head_ref_name.sql +6 -0
- package/dist/db/migrations/0025_account_billing.sql +7 -0
- package/dist/db/migrations/0026_ai_credit_allowance.sql +5 -0
- package/dist/db/migrations/0027_user_github_type.sql +6 -0
- package/dist/db/migrations/0028_bot_review_classification.sql +21 -0
- package/dist/db/migrations/0029_bot_mute_rules.sql +19 -0
- package/dist/db/migrations/0030_teams.sql +32 -0
- package/dist/db/migrations/0031_events_pr_idx.sql +7 -0
- package/dist/db/migrations/0032_benchmark_contributions.sql +29 -0
- package/dist/db/migrations/0033_thread_addressed_confidence.sql +9 -0
- package/dist/db/migrations/0034_thread_resolved_at.sql +6 -0
- package/dist/db/migrations/0035_pr_first_review_requested_at.sql +5 -0
- package/dist/db/migrations/0036_search_index.sql +25 -0
- package/dist/db/migrations/0037_author_indexes.sql +15 -0
- package/dist/db/migrations/0038_auto_merge_requests.sql +37 -0
- package/dist/db/migrations/0039_branch_status.sql +44 -0
- package/dist/db/migrations/0040_pr_review_decision.sql +10 -0
- package/dist/db/migrations/0041_branch_commit_detail.sql +38 -0
- package/dist/db/migrations/0042_repo_reviewers.sql +176 -0
- package/dist/db/migrations/0043_account_reviewers.sql +131 -0
- package/dist/db/migrations/0044_workspaces.sql +138 -0
- package/dist/db/migrations/0045_workspace_reviewers.sql +180 -0
- package/dist/db/migrations/0046_drop_inbox_watch.sql +25 -0
- package/dist/db/migrations/0047_ml_comment_labels.sql +40 -0
- package/dist/db/migrations/0048_ml_vendor_severity.sql +31 -0
- package/dist/db/migrations/0049_seat_pricing_repo_description.sql +21 -0
- package/dist/db/migrations/0050_users_app_slug.sql +12 -0
- package/dist/db/migrations/0051_auto_merge_queue.sql +17 -0
- package/dist/db/migrations/0052_trunk_ci_status_events.sql +43 -0
- package/dist/db/migrations/0053_automation_roles.sql +79 -0
- package/dist/db/migrations/0054_automation_vendor_kinds.sql +514 -0
- package/dist/db/migrations/0055_auto_merge_phase.sql +12 -0
- package/dist/db/migrations/0056_pr_mentions.sql +22 -0
- package/dist/db/migrations/0057_large_pr_threshold.sql +14 -0
- package/dist/db/migrations/0058_pending_mute.sql +53 -0
- package/dist/db/migrations/0059_merge_queue_state.sql +33 -0
- package/dist/db/migrations/0060_drop_my_turn_dismissals.sql +33 -0
- package/dist/db/migrations/0061_auto_merge_durable_anchors.sql +27 -0
- package/dist/db/migrations/0062_blast_radius_config.sql +21 -0
- package/dist/db/migrations/0063_repo_file_coupling.sql +33 -0
- package/dist/db/migrations/0064_pr_content_kind.sql +21 -0
- package/dist/db/migrations/0065_workspace_flow_settings.sql +13 -0
- package/dist/db/migrations/0066_review_request_events.sql +31 -0
- package/dist/db/migrations/0067_pr_security_signals.sql +86 -0
- package/dist/db/migrations/0068_my_turn_settings.sql +18 -0
- package/dist/db/migrations/0069_my_turn_dismissals.sql +41 -0
- package/dist/db/migrations/0070_claude_review_follow_up_ticket.sql +22 -0
- package/dist/db/migrations/0071_claude_review_trigger.sql +11 -0
- package/dist/db/migrations/0072_commit_message_headline.sql +13 -0
- package/dist/db/migrations/0073_claude_review_chat.sql +31 -0
- package/dist/db/migrations/0074_adopt_agentic_tables.sql +98 -0
- package/dist/db/migrations/0075_review_lens_drop_learnings.sql +15 -0
- package/dist/db/migrations/0076_claude_review_threads.sql +17 -0
- package/dist/db/migrations/0077_claude_review_ci_failures.sql +10 -0
- package/dist/db/migrations/0078_ai_fix_change_report.sql +17 -0
- package/dist/db/migrations/0079_claude_finding_story.sql +16 -0
- package/dist/db/migrations/meta/0000_snapshot.json +1116 -0
- package/dist/db/migrations/meta/0001_snapshot.json +1321 -0
- package/dist/db/migrations/meta/0002_snapshot.json +1375 -0
- package/dist/db/migrations/meta/0003_snapshot.json +1396 -0
- package/dist/db/migrations/meta/0004_snapshot.json +1416 -0
- package/dist/db/migrations/meta/0005_snapshot.json +1430 -0
- package/dist/db/migrations/meta/0006_snapshot.json +1745 -0
- package/dist/db/migrations/meta/0007_snapshot.json +1759 -0
- package/dist/db/migrations/meta/_journal.json +566 -0
- package/dist/db/migrations-pg/0000_tired_alex_power.sql +268 -0
- package/dist/db/migrations-pg/0001_dizzy_mockingbird.sql +3 -0
- package/dist/db/migrations-pg/0002_glorious_ikaris.sql +4 -0
- package/dist/db/migrations-pg/0003_melted_secret_warriors.sql +2 -0
- package/dist/db/migrations-pg/0004_numerous_triton.sql +1 -0
- package/dist/db/migrations-pg/0005_thick_blockbuster.sql +2 -0
- package/dist/db/migrations-pg/0006_majestic_dracula.sql +1 -0
- package/dist/db/migrations-pg/0007_same_human_torch.sql +2 -0
- package/dist/db/migrations-pg/0008_slimy_toxin.sql +1 -0
- package/dist/db/migrations-pg/0009_fuzzy_jubilee.sql +4 -0
- package/dist/db/migrations-pg/0010_late_hammerhead.sql +1 -0
- package/dist/db/migrations-pg/0011_happy_leech.sql +16 -0
- package/dist/db/migrations-pg/0012_woozy_captain_midlands.sql +16 -0
- package/dist/db/migrations-pg/0013_yielding_nicolaos.sql +1 -0
- package/dist/db/migrations-pg/0014_wakeful_bill_hollister.sql +2 -0
- package/dist/db/migrations-pg/0015_aspiring_black_tom.sql +1 -0
- package/dist/db/migrations-pg/0016_cute_ghost_rider.sql +30 -0
- package/dist/db/migrations-pg/0017_far_changeling.sql +24 -0
- package/dist/db/migrations-pg/0018_fancy_miss_america.sql +1 -0
- package/dist/db/migrations-pg/0019_benchmark_contributions.sql +22 -0
- package/dist/db/migrations-pg/0020_majestic_edwin_jarvis.sql +3 -0
- package/dist/db/migrations-pg/0021_green_salo.sql +1 -0
- package/dist/db/migrations-pg/0022_wise_baron_strucker.sql +1 -0
- package/dist/db/migrations-pg/0023_search_index.sql +19 -0
- package/dist/db/migrations-pg/0024_author_indexes.sql +9 -0
- package/dist/db/migrations-pg/0025_auto_merge_requests.sql +26 -0
- package/dist/db/migrations-pg/0026_branch_status.sql +32 -0
- package/dist/db/migrations-pg/0027_pr_review_decision.sql +5 -0
- package/dist/db/migrations-pg/0028_branch_commit_detail.sql +9 -0
- package/dist/db/migrations-pg/0029_repo_reviewers.sql +127 -0
- package/dist/db/migrations-pg/0030_account_reviewers.sql +77 -0
- package/dist/db/migrations-pg/0031_workspaces.sql +131 -0
- package/dist/db/migrations-pg/0032_workspace_reviewers.sql +165 -0
- package/dist/db/migrations-pg/0033_drop_inbox_watch.sql +14 -0
- package/dist/db/migrations-pg/0034_ml_comment_labels.sql +33 -0
- package/dist/db/migrations-pg/0035_ml_vendor_severity.sql +16 -0
- package/dist/db/migrations-pg/0036_seat_pricing_repo_description.sql +12 -0
- package/dist/db/migrations-pg/0037_users_app_slug.sql +9 -0
- package/dist/db/migrations-pg/0038_auto_merge_queue.sql +10 -0
- package/dist/db/migrations-pg/0039_trunk_ci_status_events.sql +27 -0
- package/dist/db/migrations-pg/0040_automation_roles.sql +63 -0
- package/dist/db/migrations-pg/0041_automation_vendor_kinds.sql +488 -0
- package/dist/db/migrations-pg/0042_auto_merge_phase.sql +8 -0
- package/dist/db/migrations-pg/0043_pr_mentions.sql +19 -0
- package/dist/db/migrations-pg/0044_large_pr_threshold.sql +6 -0
- package/dist/db/migrations-pg/0045_pending_mute.sql +26 -0
- package/dist/db/migrations-pg/0046_merge_queue_state.sql +18 -0
- package/dist/db/migrations-pg/0047_drop_my_turn_dismissals.sql +21 -0
- package/dist/db/migrations-pg/0048_auto_merge_durable_anchors.sql +27 -0
- package/dist/db/migrations-pg/0049_blast_radius_config.sql +8 -0
- package/dist/db/migrations-pg/0050_repo_file_coupling.sql +13 -0
- package/dist/db/migrations-pg/0051_pr_content_kind.sql +6 -0
- package/dist/db/migrations-pg/0052_workspace_flow_settings.sql +4 -0
- package/dist/db/migrations-pg/0053_review_request_events.sql +18 -0
- package/dist/db/migrations-pg/0054_pr_security_signals.sql +59 -0
- package/dist/db/migrations-pg/0055_my_turn_settings.sql +6 -0
- package/dist/db/migrations-pg/0056_my_turn_dismissals.sql +21 -0
- package/dist/db/migrations-pg/0057_claude_review_follow_up_ticket.sql +8 -0
- package/dist/db/migrations-pg/0058_claude_review_trigger.sql +4 -0
- package/dist/db/migrations-pg/0059_commit_message_headline.sql +3 -0
- package/dist/db/migrations-pg/0060_claude_review_chat.sql +24 -0
- package/dist/db/migrations-pg/0061_adopt_agentic_tables.sql +79 -0
- package/dist/db/migrations-pg/0062_review_lens_drop_learnings.sql +5 -0
- package/dist/db/migrations-pg/0063_claude_review_threads.sql +7 -0
- package/dist/db/migrations-pg/0064_claude_review_ci_failures.sql +4 -0
- package/dist/db/migrations-pg/0065_ai_fix_change_report.sql +9 -0
- package/dist/db/migrations-pg/0066_claude_finding_story.sql +7 -0
- package/dist/db/migrations-pg/meta/0000_snapshot.json +2137 -0
- package/dist/db/migrations-pg/meta/0001_snapshot.json +2143 -0
- package/dist/db/migrations-pg/meta/0002_snapshot.json +2170 -0
- package/dist/db/migrations-pg/meta/0003_snapshot.json +2182 -0
- package/dist/db/migrations-pg/meta/0004_snapshot.json +2188 -0
- package/dist/db/migrations-pg/meta/0005_snapshot.json +2201 -0
- package/dist/db/migrations-pg/meta/0006_snapshot.json +2207 -0
- package/dist/db/migrations-pg/meta/0007_snapshot.json +2219 -0
- package/dist/db/migrations-pg/meta/0008_snapshot.json +2226 -0
- package/dist/db/migrations-pg/meta/0009_snapshot.json +2250 -0
- package/dist/db/migrations-pg/meta/0010_snapshot.json +2256 -0
- package/dist/db/migrations-pg/meta/0011_snapshot.json +2412 -0
- package/dist/db/migrations-pg/meta/0012_snapshot.json +2528 -0
- package/dist/db/migrations-pg/meta/0013_snapshot.json +2534 -0
- package/dist/db/migrations-pg/meta/0014_snapshot.json +2547 -0
- package/dist/db/migrations-pg/meta/0015_snapshot.json +2553 -0
- package/dist/db/migrations-pg/meta/0016_snapshot.json +2774 -0
- package/dist/db/migrations-pg/meta/0017_snapshot.json +2999 -0
- package/dist/db/migrations-pg/meta/0018_snapshot.json +3020 -0
- package/dist/db/migrations-pg/meta/0020_snapshot.json +3228 -0
- package/dist/db/migrations-pg/meta/0021_snapshot.json +3234 -0
- package/dist/db/migrations-pg/meta/0022_snapshot.json +3240 -0
- package/dist/db/migrations-pg/meta/_journal.json +475 -0
- package/dist/db/ml-labels.js +1666 -0
- package/dist/db/my-turn-dismissals.js +343 -0
- package/dist/db/my-turn-settings.js +34 -0
- package/dist/db/pending-classify.js +46 -0
- package/dist/db/pending-mute.js +171 -0
- package/dist/db/pending-tabs.js +238 -0
- package/dist/db/period-metrics.js +820 -0
- package/dist/db/person-period.js +763 -0
- package/dist/db/pr-intervals.js +930 -0
- package/dist/db/pr-liveness.js +240 -0
- package/dist/db/pr-mentions.js +279 -0
- package/dist/db/pr-merge-queue-stamp.js +91 -0
- package/dist/db/queries.js +14631 -0
- package/dist/db/reaction-targets.js +94 -0
- package/dist/db/repo-activity.js +171 -0
- package/dist/db/retention.js +138 -0
- package/dist/db/review-threads-for-review.js +240 -0
- package/dist/db/run-migrations.js +24 -0
- package/dist/db/schema.pg.js +1220 -0
- package/dist/db/schema.sqlite.js +1801 -0
- package/dist/db/search.js +317 -0
- package/dist/db/security-alerts.js +218 -0
- package/dist/db/synthesis-input.js +565 -0
- package/dist/db/triage.js +320 -0
- package/dist/db/usage.js +63 -0
- package/dist/db/work-plan.js +1086 -0
- package/dist/db/working-hours.js +161 -0
- package/dist/feed/my-turn.js +122 -0
- package/dist/github/actions-logs.js +397 -0
- package/dist/github/auth.js +86 -0
- package/dist/github/branch-queries.js +229 -0
- package/dist/github/client.js +454 -0
- package/dist/github/codeowners.js +221 -0
- package/dist/github/commit-checks.js +105 -0
- package/dist/github/compare.js +110 -0
- package/dist/github/diff-anchor.js +227 -0
- package/dist/github/mutations.js +706 -0
- package/dist/github/pr-file-content.js +254 -0
- package/dist/github/pr-liveness.js +161 -0
- package/dist/github/queries.js +875 -0
- package/dist/github/rate-budget.js +129 -0
- package/dist/github/reactions.js +161 -0
- package/dist/github/reviewer-suggest.js +104 -0
- package/dist/github/team-reviewers.js +96 -0
- package/dist/index.js +184 -0
- package/dist/merge/auto-merge-runner.js +960 -0
- package/dist/ml/severity-client.js +218 -0
- package/dist/pr/detail-enricher.js +26 -0
- package/dist/pro/bind.js +302 -0
- package/dist/pro/contract.js +37 -0
- package/dist/pro/migrate.js +64 -0
- package/dist/review/agent-context.js +127 -0
- package/dist/review/agent.js +385 -0
- package/dist/review/agentic.js +59 -0
- package/dist/review/auth.js +94 -0
- package/dist/review/chat-agent.js +179 -0
- package/dist/review/claude-review/ai-ready.js +16 -0
- package/dist/review/claude-review/auto-settings.js +139 -0
- package/dist/review/claude-review/auto.js +244 -0
- package/dist/review/claude-review/chat.js +489 -0
- package/dist/review/claude-review/ci-failures.js +350 -0
- package/dist/review/claude-review/follow-up.js +310 -0
- package/dist/review/claude-review/manager.js +686 -0
- package/dist/review/claude-review/persist.js +996 -0
- package/dist/review/claude-review/prompts.js +357 -0
- package/dist/review/claude-review/routes.js +518 -0
- package/dist/review/claude-review/routing.js +61 -0
- package/dist/review/claude-review/specialists.js +190 -0
- package/dist/review/claude-review/threads.js +265 -0
- package/dist/review/claude-review/ticket.js +282 -0
- package/dist/review/clone-hygiene.js +380 -0
- package/dist/review/clone-manager.js +367 -0
- package/dist/review/llm.js +113 -0
- package/dist/review/model-options.js +48 -0
- package/dist/review/plugin-providers.js +12 -0
- package/dist/review/post-review.js +216 -0
- package/dist/review/post-seam.js +161 -0
- package/dist/review/prepare.js +168 -0
- package/dist/review/pricing.js +17 -0
- package/dist/review/schema.js +156 -0
- package/dist/review/submit-map.js +41 -0
- package/dist/review/usage.js +90 -0
- package/dist/shared/claude-review.js +335 -0
- package/dist/shared/conflict-fold.js +151 -0
- package/dist/shared/conflicts.js +30 -0
- package/dist/shared/flow-settings.js +191 -0
- package/dist/shared/index.js +8 -0
- package/dist/shared/my-turn-settings.js +238 -0
- package/dist/shared/pending-rules.js +281 -0
- package/dist/shared/product.js +54 -0
- package/dist/shared/types.js +896 -0
- package/dist/status.js +386 -0
- package/dist/sync/adaptive.js +185 -0
- package/dist/sync/app-attribution.js +66 -0
- package/dist/sync/auth-notices.js +36 -0
- package/dist/sync/backfill-ci-history.js +315 -0
- package/dist/sync/backfill-commit-headlines.js +136 -0
- package/dist/sync/backfill-pr-security.js +175 -0
- package/dist/sync/backfill-review-requests.js +97 -0
- package/dist/sync/benchmark-rollup.js +50 -0
- package/dist/sync/bot-detection.js +309 -0
- package/dist/sync/bot-resolution-markers.js +30 -0
- package/dist/sync/branch-status.js +634 -0
- package/dist/sync/classify-change-shape.js +150 -0
- package/dist/sync/commit-files.js +74 -0
- package/dist/sync/derive-thread-state.js +99 -0
- package/dist/sync/drain-review-threads.js +212 -0
- package/dist/sync/hydrate-detail.js +482 -0
- package/dist/sync/mention-scan.js +89 -0
- package/dist/sync/ml-enrichment.js +478 -0
- package/dist/sync/pr-change-signal.js +96 -0
- package/dist/sync/pr-liveness-sweep.js +139 -0
- package/dist/sync/pr-settle.js +381 -0
- package/dist/sync/refresh-pr.js +163 -0
- package/dist/sync/reparse-vendor-badges.js +495 -0
- package/dist/sync/resync-after-write.js +280 -0
- package/dist/sync/review-fingerprint.js +117 -0
- package/dist/sync/reviewer-behavior.js +103 -0
- package/dist/sync/reviewer-classify.js +503 -0
- package/dist/sync/routing-files.js +97 -0
- package/dist/sync/scheduled-jobs.js +12 -0
- package/dist/sync/scheduler.js +159 -0
- package/dist/sync/security-detect.js +521 -0
- package/dist/sync/sync-manager.js +750 -0
- package/dist/sync/sync-one-pr.js +205 -0
- package/dist/sync/sync-repo.js +400 -0
- package/dist/sync/unsettled-prs.js +506 -0
- package/dist/sync/upsert.js +1190 -0
- package/package.json +56 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/assets/AiFixTab-CQP6PT5u.js +2 -0
- package/public/assets/ClaudeReviewTab-DQJ-58ZA.js +8 -0
- package/public/assets/PrBotBehaviourTab-CCnFrWbC.js +1 -0
- package/public/assets/archivo-latin-600-BN-JkNrl.woff2 +0 -0
- package/public/assets/highlight-BEHUn5zE.css +10 -0
- package/public/assets/highlight-CIIZLa6Z.js +12 -0
- package/public/assets/index-BDnYs6zg.js +73 -0
- package/public/assets/index-mCuI--RC.css +1 -0
- package/public/assets/markdown-I_JLLVAZ.js +32 -0
- package/public/assets/perfProbe-Da13EhTE.js +1 -0
- package/public/assets/tanstack-5qsxxaLe.js +17 -0
- package/public/assets/vis-C3BCkP5U.js +1280 -0
- package/public/assets/vis-ziCVehMy.css +1 -0
- package/public/favicon-16.png +0 -0
- package/public/favicon-32.png +0 -0
- package/public/favicon-48.png +0 -0
- package/public/favicon.svg +5 -0
- package/public/index.html +41 -0
- package/public-landing/apple-touch-icon.png +0 -0
- package/public-landing/assets/archivo-latin-500-BWmvx2Mg.woff2 +0 -0
- package/public-landing/assets/archivo-latin-600-BN-JkNrl.woff2 +0 -0
- package/public-landing/assets/index-CA3eEGXO.js +40 -0
- package/public-landing/assets/index-naQrQ6Fe.css +1 -0
- package/public-landing/assets/jetbrains-mono-latin-400-RruCX7RW.woff2 +0 -0
- package/public-landing/assets/newsreader-latin-400-DExrRRrN.woff2 +0 -0
- package/public-landing/assets/newsreader-latin-400-italic-b0I-AwbV.woff2 +0 -0
- package/public-landing/bots/index.html +156 -0
- package/public-landing/contact/index.html +156 -0
- package/public-landing/cookies/index.html +156 -0
- package/public-landing/demo/limn-walkthrough-poster.jpg +0 -0
- package/public-landing/demo/limn-walkthrough.cues.json +44 -0
- package/public-landing/demo/limn-walkthrough.mp4 +0 -0
- package/public-landing/favicon-16.png +0 -0
- package/public-landing/favicon-32.png +0 -0
- package/public-landing/favicon-48.png +0 -0
- package/public-landing/favicon.svg +5 -0
- package/public-landing/features/index.html +156 -0
- package/public-landing/for-developers/index.html +156 -0
- package/public-landing/for-managers/index.html +156 -0
- package/public-landing/how-it-works/index.html +156 -0
- package/public-landing/how-we-measure/index.html +156 -0
- package/public-landing/icon-192.png +0 -0
- package/public-landing/icon-512.png +0 -0
- package/public-landing/index.html +156 -0
- package/public-landing/insights/index.html +156 -0
- package/public-landing/og-image.png +0 -0
- package/public-landing/pricing/index.html +156 -0
- package/public-landing/privacy/index.html +156 -0
- package/public-landing/pro/index.html +156 -0
- package/public-landing/reviews/index.html +156 -0
- package/public-landing/robots.txt +8 -0
- package/public-landing/shots/benchmark.png +0 -0
- package/public-landing/shots/bot-roi.png +0 -0
- package/public-landing/shots/bot-settings.png +0 -0
- package/public-landing/shots/chronology.png +0 -0
- package/public-landing/shots/feed.png +0 -0
- package/public-landing/shots/flow-metrics.png +0 -0
- package/public-landing/shots/free-reports.png +0 -0
- package/public-landing/shots/pending-board.png +0 -0
- package/public-landing/shots/pending-card.png +0 -0
- package/public-landing/shots/period-report.png +0 -0
- package/public-landing/shots/pr-detail.png +0 -0
- package/public-landing/shots/pr-threads.png +0 -0
- package/public-landing/shots/reach.png +0 -0
- package/public-landing/shots/repo-rows.png +0 -0
- package/public-landing/shots/severity-findings.png +0 -0
- package/public-landing/shots/severity-strip.png +0 -0
- package/public-landing/site.webmanifest +14 -0
- package/public-landing/sitemap.xml +51 -0
- 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
|