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,321 @@
|
|
|
1
|
+
import { config } from '../../config.js';
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// Security headers + cross-origin request guard.
|
|
4
|
+
//
|
|
5
|
+
// Deliberately hand-rolled rather than @fastify/helmet: helmet would be a new
|
|
6
|
+
// runtime dependency that must also be threaded through the curated release
|
|
7
|
+
// manifest (scripts/build-release.mjs) and the pinned lockfile, and its default
|
|
8
|
+
// CSP is wrong for this app in three ways anyway (it forbids the inline styles
|
|
9
|
+
// vis-timeline and React both emit, it forbids the `https:` images every GitHub
|
|
10
|
+
// markdown body embeds, and it knows nothing about the local/cloud split). The
|
|
11
|
+
// header set below is the same posture, sized to this app.
|
|
12
|
+
//
|
|
13
|
+
// Everything here is computed ONCE at module load — the per-request hook only
|
|
14
|
+
// does a couple of string comparisons and reply.header() calls.
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Google Tag Manager / GA4 endpoints. Analytics is CLOUD-ONLY (both analytics.ts
|
|
17
|
+
// callers gate on it) and now additionally consent-gated in the browser, so these
|
|
18
|
+
// only enter the CSP in cloud mode — a local install's CSP names no third party.
|
|
19
|
+
const GA_SCRIPT = 'https://www.googletagmanager.com';
|
|
20
|
+
const GA_CONNECT = [
|
|
21
|
+
'https://www.google-analytics.com',
|
|
22
|
+
'https://*.google-analytics.com',
|
|
23
|
+
'https://*.analytics.google.com',
|
|
24
|
+
'https://*.googletagmanager.com',
|
|
25
|
+
].join(' ');
|
|
26
|
+
// `style-src 'unsafe-inline'` is unavoidable and accepted: React writes inline
|
|
27
|
+
// `style` attributes, vis-timeline positions every item by mutating element.style,
|
|
28
|
+
// and the landing's splash-caret keyframes are an inline <style>. CSS injection is
|
|
29
|
+
// a far weaker primitive than script injection, and script-src stays strict — no
|
|
30
|
+
// 'unsafe-inline', no 'unsafe-eval' (verified: neither vis-timeline, vis-data nor
|
|
31
|
+
// vis-util contains eval/new Function, so the timeline runs under this policy).
|
|
32
|
+
//
|
|
33
|
+
// `img-src https:` is required, not lax: PR/comment bodies are third-party markdown
|
|
34
|
+
// that legitimately embeds images from arbitrary hosts (user-content, camo, shields,
|
|
35
|
+
// gravatar). The markdown pipeline already strips javascript:/data: URLs via
|
|
36
|
+
// rehype-sanitize, so the residual risk is a tracking pixel, not code execution.
|
|
37
|
+
const SPA_CSP = [
|
|
38
|
+
"default-src 'self'",
|
|
39
|
+
`script-src 'self'${config.isCloud ? ` ${GA_SCRIPT}` : ''}`,
|
|
40
|
+
"style-src 'self' 'unsafe-inline'",
|
|
41
|
+
"font-src 'self'",
|
|
42
|
+
"img-src 'self' data: blob: https:",
|
|
43
|
+
"media-src 'self' https:",
|
|
44
|
+
`connect-src 'self'${config.isCloud ? ` ${GA_CONNECT}` : ''}`,
|
|
45
|
+
"worker-src 'self' blob:",
|
|
46
|
+
"frame-src 'none'",
|
|
47
|
+
"object-src 'none'",
|
|
48
|
+
"base-uri 'self'",
|
|
49
|
+
"form-action 'self'",
|
|
50
|
+
"frame-ancestors 'none'",
|
|
51
|
+
].join('; ');
|
|
52
|
+
// The landing is a plain marketing page: no third-party images beyond its own
|
|
53
|
+
// assets, no embedded frames. Tighter than the SPA on purpose.
|
|
54
|
+
const LANDING_CSP = [
|
|
55
|
+
"default-src 'self'",
|
|
56
|
+
`script-src 'self' ${GA_SCRIPT}`,
|
|
57
|
+
"style-src 'self' 'unsafe-inline'",
|
|
58
|
+
"font-src 'self'",
|
|
59
|
+
"img-src 'self' data:",
|
|
60
|
+
`connect-src 'self' ${GA_CONNECT}`,
|
|
61
|
+
"object-src 'none'",
|
|
62
|
+
"base-uri 'self'",
|
|
63
|
+
"form-action 'self'",
|
|
64
|
+
"frame-ancestors 'none'",
|
|
65
|
+
].join('; ');
|
|
66
|
+
// API responses are JSON and are never a document. `default-src 'none'` means that
|
|
67
|
+
// even if a response body were somehow rendered as HTML (a content-sniffing browser,
|
|
68
|
+
// a `view-source`-style navigation, an error page), nothing in it can load or run.
|
|
69
|
+
const API_CSP = [
|
|
70
|
+
"default-src 'none'",
|
|
71
|
+
"base-uri 'none'",
|
|
72
|
+
"form-action 'none'",
|
|
73
|
+
"frame-ancestors 'none'",
|
|
74
|
+
].join('; ');
|
|
75
|
+
const PERMISSIONS_POLICY = [
|
|
76
|
+
'accelerometer=()',
|
|
77
|
+
'autoplay=()',
|
|
78
|
+
'camera=()',
|
|
79
|
+
'display-capture=()',
|
|
80
|
+
'encrypted-media=()',
|
|
81
|
+
'geolocation=()',
|
|
82
|
+
'gyroscope=()',
|
|
83
|
+
'magnetometer=()',
|
|
84
|
+
'microphone=()',
|
|
85
|
+
'midi=()',
|
|
86
|
+
'payment=()',
|
|
87
|
+
'usb=()',
|
|
88
|
+
'xr-spatial-tracking=()',
|
|
89
|
+
].join(', ');
|
|
90
|
+
/**
|
|
91
|
+
* Sets the response security headers on every request, plus (cloud only) HSTS and
|
|
92
|
+
* the www → apex canonical redirect. Registered FIRST in app.ts so the headers ride
|
|
93
|
+
* on every response including redirects, 404s and error bodies.
|
|
94
|
+
*/
|
|
95
|
+
export function registerSecurityHeaders(app) {
|
|
96
|
+
// Cloud only: the canonical host, for the www redirect. hostname (not host) so a
|
|
97
|
+
// configured port never enters the comparison.
|
|
98
|
+
let canonicalHost = '';
|
|
99
|
+
if (config.isCloud) {
|
|
100
|
+
try {
|
|
101
|
+
canonicalHost = new URL(config.appBaseUrl).hostname.toLowerCase();
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
canonicalHost = '';
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
app.addHook('onRequest', async (req, reply) => {
|
|
108
|
+
const path = pathOf(req);
|
|
109
|
+
const isApi = path.startsWith('/api');
|
|
110
|
+
// ---- HSTS + canonical host (cloud only) ----
|
|
111
|
+
// Local mode runs on http://127.0.0.1, where HSTS would wrongly pin localhost
|
|
112
|
+
// to HTTPS and a www redirect is meaningless — so both stay cloud-gated.
|
|
113
|
+
if (config.isCloud) {
|
|
114
|
+
// Honored only over HTTPS (Railway terminates TLS); ignored on plain HTTP.
|
|
115
|
+
// `includeSubDomains` also covers www. No `preload` — it is hard to undo.
|
|
116
|
+
// HSTS_MAX_AGE=0 disables it.
|
|
117
|
+
if (config.hstsMaxAge > 0) {
|
|
118
|
+
reply.header('Strict-Transport-Security', `max-age=${config.hstsMaxAge}; includeSubDomains`);
|
|
119
|
+
}
|
|
120
|
+
// 301 www.<apex> → <apex> so the OAuth round-trip and the session cookie stay
|
|
121
|
+
// on a single origin (and crawlers see one canonical URL).
|
|
122
|
+
if (canonicalHost) {
|
|
123
|
+
const host = (req.headers.host ?? '').toLowerCase().split(':')[0] ?? '';
|
|
124
|
+
if (host !== canonicalHost &&
|
|
125
|
+
host.replace(/^www\./, '') === canonicalHost) {
|
|
126
|
+
return reply.redirect(`${config.appBaseUrl}${req.url}`, 301);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// ---- Content-Security-Policy, per surface ----
|
|
131
|
+
reply.header('Content-Security-Policy', isApi ? API_CSP : path === '/app' || path.startsWith('/app/') ? SPA_CSP : LANDING_CSP);
|
|
132
|
+
// ---- The rest, everywhere ----
|
|
133
|
+
// nosniff: stops a browser deciding a JSON error body is HTML and rendering it.
|
|
134
|
+
reply.header('X-Content-Type-Options', 'nosniff');
|
|
135
|
+
// Clickjacking. CSP frame-ancestors above is the modern control; X-Frame-Options
|
|
136
|
+
// is kept for older browsers that ignore it.
|
|
137
|
+
reply.header('X-Frame-Options', 'DENY');
|
|
138
|
+
// App URLs carry repo/PR ids in the query string — send the origin only, never
|
|
139
|
+
// the path, to any cross-origin destination.
|
|
140
|
+
reply.header('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
141
|
+
reply.header('Permissions-Policy', PERMISSIONS_POLICY);
|
|
142
|
+
// Isolate the browsing context from anything this app opens (and vice-versa).
|
|
143
|
+
// Safe here: OAuth is a top-level redirect, not a popup+postMessage handshake.
|
|
144
|
+
reply.header('Cross-Origin-Opener-Policy', 'same-origin');
|
|
145
|
+
if (isApi) {
|
|
146
|
+
// Block cross-origin no-cors embedding of API JSON (a <script src> / <img src>
|
|
147
|
+
// side-channel). Applied to /api ONLY — the landing's og-image.png must stay
|
|
148
|
+
// embeddable for link previews.
|
|
149
|
+
reply.header('Cross-Origin-Resource-Policy', 'same-origin');
|
|
150
|
+
// Never let PR data, feeds or account state land in a browser or proxy cache.
|
|
151
|
+
// Nothing in the app relies on HTTP caching (React Query + IndexedDB do their
|
|
152
|
+
// own caching at the application layer).
|
|
153
|
+
reply.header('Cache-Control', 'no-store');
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// Cross-origin request guard
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
/** The path with any query string stripped. */
|
|
161
|
+
function pathOf(req) {
|
|
162
|
+
return req.url.split('?')[0] ?? req.url;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Origins allowed to make credentialed / state-changing calls to this instance.
|
|
166
|
+
*
|
|
167
|
+
* CLOUD: exactly the deployment's own origin.
|
|
168
|
+
*
|
|
169
|
+
* LOCAL: any loopback origin, on any port. This is deliberately a set of PATTERNS
|
|
170
|
+
* rather than a fixed list, because local mode is reached from several ports that
|
|
171
|
+
* are not knowable here — the Vite dev server (5173), the landing dev server (5174),
|
|
172
|
+
* the isolated demo stack (5273), the packaged CLI serving the SPA from the API port
|
|
173
|
+
* itself, and whatever PORT/BACKEND_PORT a user overrides. What it does NOT match is
|
|
174
|
+
* the case that matters: a page on a real website (https://evil.example) that the
|
|
175
|
+
* developer happens to have open while Pierre is running.
|
|
176
|
+
*
|
|
177
|
+
* ALLOWED_ORIGINS (comma-separated) adds extra origins for anything exotic.
|
|
178
|
+
*/
|
|
179
|
+
const EXTRA_ORIGINS = (process.env.ALLOWED_ORIGINS ?? '')
|
|
180
|
+
.split(',')
|
|
181
|
+
.map((o) => o.trim().replace(/\/$/, ''))
|
|
182
|
+
.filter(Boolean);
|
|
183
|
+
const LOOPBACK_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);
|
|
184
|
+
function isLoopbackOrigin(origin) {
|
|
185
|
+
try {
|
|
186
|
+
const u = new URL(origin);
|
|
187
|
+
if (u.protocol !== 'http:' && u.protocol !== 'https:')
|
|
188
|
+
return false;
|
|
189
|
+
return LOOPBACK_HOSTNAMES.has(u.hostname.toLowerCase());
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
export function isAllowedOrigin(origin) {
|
|
196
|
+
const o = origin.replace(/\/$/, '');
|
|
197
|
+
if (EXTRA_ORIGINS.includes(o))
|
|
198
|
+
return true;
|
|
199
|
+
return config.isCloud ? o === config.appBaseUrl : isLoopbackOrigin(o);
|
|
200
|
+
}
|
|
201
|
+
/** The @fastify/cors `origin` option: an allowlist in BOTH modes (was `true` locally). */
|
|
202
|
+
export function corsOriginDelegate(origin, cb) {
|
|
203
|
+
// No Origin header = a same-origin navigation or a non-browser client (curl, the
|
|
204
|
+
// CLI, a webhook). CORS has nothing to decide; the guard below handles the
|
|
205
|
+
// state-changing case.
|
|
206
|
+
if (!origin)
|
|
207
|
+
return cb(null, true);
|
|
208
|
+
cb(null, isAllowedOrigin(origin));
|
|
209
|
+
}
|
|
210
|
+
// Server-to-server POSTs authenticated by an HMAC signature instead of an origin.
|
|
211
|
+
// GitHub and Stripe send no Origin and no Sec-Fetch-* headers, so they would pass
|
|
212
|
+
// the guard regardless — listed explicitly so that stays true by intent, not luck.
|
|
213
|
+
const GUARD_EXEMPT_PATHS = new Set([
|
|
214
|
+
'/api/webhooks/github',
|
|
215
|
+
'/api/billing/webhook',
|
|
216
|
+
]);
|
|
217
|
+
// GET routes that change server state and so need the same protection as a POST.
|
|
218
|
+
// /api/auth/reconnect revokes the GitHub grant and drops the session; the frontend
|
|
219
|
+
// reaches it as a same-origin <a href> navigation, so guarding it is free.
|
|
220
|
+
// NOT guarded: /api/auth/login and /api/billing/checkout — those are legitimately
|
|
221
|
+
// entered as cross-site top-level navigations (a link from the README, a bookmark).
|
|
222
|
+
const GUARDED_GET_PATHS = new Set(['/api/auth/reconnect']);
|
|
223
|
+
/**
|
|
224
|
+
* Rejects state-changing /api requests that arrive from a foreign origin.
|
|
225
|
+
*
|
|
226
|
+
* This is NOT redundant with the CORS allowlist. CORS only governs whether the
|
|
227
|
+
* browser lets the *attacker's page* READ the response — a "simple" cross-origin
|
|
228
|
+
* POST (or a form submission, or a fetch with a non-preflighted content type) is
|
|
229
|
+
* still DELIVERED and still EXECUTES on the server. In local mode that matters a
|
|
230
|
+
* great deal, because there is no session cookie to miss: every request resolves to
|
|
231
|
+
* the single local account, so a drive-by POST from any page the developer has open
|
|
232
|
+
* would run as them — resolve review threads, trigger a paid AI run, add a repo.
|
|
233
|
+
* SameSite=Lax covers the cloud cookie case, but this closes the local hole and
|
|
234
|
+
* gives cloud a second, independent layer.
|
|
235
|
+
*
|
|
236
|
+
* Detection prefers `Sec-Fetch-Site`, which every current browser sends and no page
|
|
237
|
+
* can forge, and falls back to `Origin`. A request with neither (curl, a CI script,
|
|
238
|
+
* a webhook) is allowed: CSRF requires a browser, and blocking header-less clients
|
|
239
|
+
* would break the CLI and every integration.
|
|
240
|
+
*/
|
|
241
|
+
export function registerCrossOriginGuard(app) {
|
|
242
|
+
app.addHook('onRequest', async (req, reply) => {
|
|
243
|
+
const path = pathOf(req);
|
|
244
|
+
if (!path.startsWith('/api/'))
|
|
245
|
+
return;
|
|
246
|
+
if (GUARD_EXEMPT_PATHS.has(path))
|
|
247
|
+
return;
|
|
248
|
+
const method = req.method.toUpperCase();
|
|
249
|
+
const mutating = method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS';
|
|
250
|
+
if (!mutating && !GUARDED_GET_PATHS.has(path))
|
|
251
|
+
return;
|
|
252
|
+
// `Sec-Fetch-Site` is set by the browser, unforgeable by page script:
|
|
253
|
+
// same-origin / same-site → our own UI
|
|
254
|
+
// none → user-initiated (address bar, bookmark)
|
|
255
|
+
// cross-site → another site caused this request
|
|
256
|
+
const site = req.headers['sec-fetch-site'];
|
|
257
|
+
if (typeof site === 'string') {
|
|
258
|
+
if (site === 'cross-site')
|
|
259
|
+
await deny(reply, req, 'sec-fetch-site');
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
// Older/exotic browsers: fall back to Origin. Absent = not a cross-origin
|
|
263
|
+
// browser request, so allow.
|
|
264
|
+
const origin = req.headers.origin;
|
|
265
|
+
if (typeof origin === 'string' && origin !== 'null' && !isAllowedOrigin(origin)) {
|
|
266
|
+
await deny(reply, req, 'origin');
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
async function deny(reply, req, via) {
|
|
271
|
+
req.log.warn({ path: pathOf(req), method: req.method, origin: req.headers.origin, via }, 'blocked cross-origin state-changing request');
|
|
272
|
+
// 403, not 401: the caller may be perfectly well authenticated — the problem is
|
|
273
|
+
// where the request came from, and retrying with credentials will not help.
|
|
274
|
+
await reply.code(403).send({
|
|
275
|
+
error: 'Forbidden',
|
|
276
|
+
message: 'Cross-origin request blocked.',
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
// Host guard (local mode): DNS-rebinding defence
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
/**
|
|
283
|
+
* Local mode binds 127.0.0.1, so nothing on the network can reach it directly — but
|
|
284
|
+
* a hostname an attacker controls can be made to RESOLVE to 127.0.0.1 (DNS
|
|
285
|
+
* rebinding), at which point their page is same-origin with the app and every
|
|
286
|
+
* same-origin protection above evaporates. The defence is to check the Host header:
|
|
287
|
+
* a rebound request still carries `Host: evil.example`, never `localhost`.
|
|
288
|
+
*
|
|
289
|
+
* Only applied when the server is actually bound to a loopback address. If a user
|
|
290
|
+
* has deliberately set HOST to expose a local instance on their LAN, they need
|
|
291
|
+
* arbitrary Host values to work, so the check stands down (and ALLOWED_HOSTS lets
|
|
292
|
+
* them re-enable it for named hosts).
|
|
293
|
+
*/
|
|
294
|
+
const EXTRA_HOSTS = (process.env.ALLOWED_HOSTS ?? '')
|
|
295
|
+
.split(',')
|
|
296
|
+
.map((h) => h.trim().toLowerCase())
|
|
297
|
+
.filter(Boolean);
|
|
298
|
+
export function registerHostGuard(app) {
|
|
299
|
+
if (config.isCloud)
|
|
300
|
+
return;
|
|
301
|
+
const boundToLoopback = LOOPBACK_HOSTNAMES.has(config.host.toLowerCase());
|
|
302
|
+
if (!boundToLoopback && EXTRA_HOSTS.length === 0)
|
|
303
|
+
return;
|
|
304
|
+
app.addHook('onRequest', async (req, reply) => {
|
|
305
|
+
// Host may carry a port, and an IPv6 literal is bracketed: [::1]:4000.
|
|
306
|
+
const raw = (req.headers.host ?? '').toLowerCase();
|
|
307
|
+
const hostname = raw.startsWith('[')
|
|
308
|
+
? (raw.split(']')[0] ?? '') + ']'
|
|
309
|
+
: (raw.split(':')[0] ?? '');
|
|
310
|
+
if (LOOPBACK_HOSTNAMES.has(hostname) || EXTRA_HOSTS.includes(hostname))
|
|
311
|
+
return;
|
|
312
|
+
// An absent Host header (HTTP/1.0, a raw socket probe) is not a browser doing
|
|
313
|
+
// DNS rebinding — but it is also not something the app needs to serve.
|
|
314
|
+
req.log.warn({ host: raw, path: pathOf(req) }, 'blocked unexpected Host header');
|
|
315
|
+
await reply.code(421).send({
|
|
316
|
+
error: 'MisdirectedRequest',
|
|
317
|
+
message: 'This instance only serves requests addressed to localhost. Set ALLOWED_HOSTS to permit another hostname.',
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
//# sourceMappingURL=security.js.map
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { getActivity, getConsolidatedFeed, listClaudeReviewsByRepo, markFeedSeen, resolveWorkspaceScope, } from '../../db/queries.js';
|
|
2
|
+
import { accountIdOf } from '../plugins/auth.js';
|
|
3
|
+
// Parse a comma-separated id list into an int array, or null when empty/absent (so
|
|
4
|
+
// `resolveWorkspaceScope` treats it as "no narrowing — every repo in the workspace").
|
|
5
|
+
//
|
|
6
|
+
// ⚠ ITS RESULT IS NEVER A SCOPE. It is the `narrow` argument to `resolveWorkspaceScope`, which
|
|
7
|
+
// INTERSECTS it with the workspace's membership; a handler that passed this list straight to a
|
|
8
|
+
// getter would let `?workspace=5&repoIds=<a repo of workspace 9>` render one workspace's console
|
|
9
|
+
// out of another's repos.
|
|
10
|
+
function parseIntList(raw) {
|
|
11
|
+
if (!raw)
|
|
12
|
+
return null;
|
|
13
|
+
const ids = raw
|
|
14
|
+
.split(',')
|
|
15
|
+
.map((s) => Number.parseInt(s.trim(), 10))
|
|
16
|
+
.filter((n) => Number.isFinite(n));
|
|
17
|
+
return ids.length > 0 ? ids : null;
|
|
18
|
+
}
|
|
19
|
+
// ---- WHY THESE TWO CONTENT ROUTES TAKE `?workspace=` ----
|
|
20
|
+
//
|
|
21
|
+
// They carried only `?repoIds=` for as long as `null` meant "every repo of the account", and
|
|
22
|
+
// that is exactly what broke under a workspace scope, two ways over:
|
|
23
|
+
//
|
|
24
|
+
// • a workspace with ZERO repos yields `repoIds: []`, every client query-string builder drops
|
|
25
|
+
// an empty array, and the server then answered with EVERY repo in the account — the precise
|
|
26
|
+
// opposite of "this workspace is empty";
|
|
27
|
+
// • two different workspaces both sitting on `repoIds = null` produced the SAME query string,
|
|
28
|
+
// so React Query served one workspace's data under the other's cache key with no refetch.
|
|
29
|
+
//
|
|
30
|
+
// `resolveWorkspaceScope` is the one resolver: absent / unparseable / another tenant's id all
|
|
31
|
+
// degrade to the account's DEFAULT workspace (never a 404 — every id yields the same response
|
|
32
|
+
// shape, so it is not an existence oracle), and the returned `repoIds` is ALWAYS
|
|
33
|
+
// `membership ∩ (?repoIds= ?? membership)`. A `[]` therefore reaches the getters as a concrete
|
|
34
|
+
// empty set and comes back empty, which is what an empty workspace must render.
|
|
35
|
+
export async function activityRoutes(app) {
|
|
36
|
+
// The Activity aggregate: per repo, current-state stats + thread totals +
|
|
37
|
+
// attention/unread flags + open PRs. Scoped to the active WORKSPACE — every repo in it, with
|
|
38
|
+
// no second visibility axis; `repoIds` + `userIds` narrow WITHIN that. Pure DB read — no
|
|
39
|
+
// GitHub sync, no AI.
|
|
40
|
+
//
|
|
41
|
+
// It takes the whole `BotScope`, not just the repo list: its acted-on bot stat needs the
|
|
42
|
+
// WORKSPACE to know who counts as a review bot, while `scope.repoIds` only narrows the data.
|
|
43
|
+
app.get('/api/activity', async (req) => {
|
|
44
|
+
const q = req.query;
|
|
45
|
+
const accountId = accountIdOf(req);
|
|
46
|
+
const scope = await resolveWorkspaceScope(accountId, q.workspace, parseIntList(q.repoIds));
|
|
47
|
+
return getActivity(accountId, scope, parseIntList(q.userIds));
|
|
48
|
+
});
|
|
49
|
+
// The consolidated Feed (the Activity "Feed" entry): one flat, chronological stream of real
|
|
50
|
+
// activity, each row flagged `isMyTurn` by participation. Scoped to the active WORKSPACE —
|
|
51
|
+
// every repo in it, with no second visibility axis; `repoIds` + `userIds` narrow WITHIN that.
|
|
52
|
+
// Pure DB read — no GitHub sync, no AI.
|
|
53
|
+
//
|
|
54
|
+
// `workspaceId` is passed SEPARATELY from `repoIds` and is required: the bots-only path
|
|
55
|
+
// resolves an automated-reviewer set, and that answer is a workspace fact. The two deliberately
|
|
56
|
+
// disagree on the single-PR isolation path (`prId`), which reaches a PR whose repo may be
|
|
57
|
+
// filtered out of `repoIds` — the workspace still owns "is this login a bot" there.
|
|
58
|
+
app.get('/api/activity/feed', async (req) => {
|
|
59
|
+
const q = req.query;
|
|
60
|
+
const accountId = accountIdOf(req);
|
|
61
|
+
const scope = await resolveWorkspaceScope(accountId, q.workspace, parseIntList(q.repoIds));
|
|
62
|
+
const limit = q.limit != null ? Number(q.limit) : null;
|
|
63
|
+
const offset = q.offset != null ? Number(q.offset) : 0;
|
|
64
|
+
const prId = q.prId != null ? Number(q.prId) : null;
|
|
65
|
+
// Bot-only feed window (days) — clamped to 1..90; only honored on the botsOnly path.
|
|
66
|
+
const botWindowDaysRaw = q.botWindowDays != null ? Number(q.botWindowDays) : null;
|
|
67
|
+
const botWindowDays = botWindowDaysRaw != null && Number.isFinite(botWindowDaysRaw)
|
|
68
|
+
? Math.min(90, Math.max(1, Math.trunc(botWindowDaysRaw)))
|
|
69
|
+
: null;
|
|
70
|
+
return getConsolidatedFeed(accountId, {
|
|
71
|
+
workspaceId: scope.workspaceId,
|
|
72
|
+
repoIds: scope.repoIds,
|
|
73
|
+
userIds: parseIntList(q.userIds),
|
|
74
|
+
prId: prId != null && Number.isFinite(prId) ? prId : null,
|
|
75
|
+
limit: Number.isFinite(limit) && limit != null && limit > 0 ? limit : null,
|
|
76
|
+
offset: Number.isFinite(offset) && offset > 0 ? offset : 0,
|
|
77
|
+
excludeBots: q.excludeBots === 'true',
|
|
78
|
+
allowBotIds: parseIntList(q.allowBotIds),
|
|
79
|
+
botsOnly: q.botsOnly === 'true',
|
|
80
|
+
botWindowDays,
|
|
81
|
+
includeAllCommits: q.includeAllCommits === 'true',
|
|
82
|
+
// Opt-in CI-failure rows (PR heads + the default branch). OFF unless explicitly asked
|
|
83
|
+
// for, like includeAllCommits — a missing/garbage value means off, never on. Still a
|
|
84
|
+
// pure DB read over two indexed transition logs, so no new rate-limit tier is needed.
|
|
85
|
+
includeCiFailures: q.includeCiFailures === 'true',
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
// Mark the Activity Feed as seen (bumps the account's server-side "seen" marker to
|
|
89
|
+
// now). Called when the user views the feed; resets the "new My Turn since last here"
|
|
90
|
+
// count that drives the Welcome-back banner. Account-scoped; no body.
|
|
91
|
+
app.post('/api/activity/feed/mark-seen', async (req) => {
|
|
92
|
+
const at = await markFeedSeen(accountIdOf(req));
|
|
93
|
+
return { feedLastSeenAt: at.toISOString() };
|
|
94
|
+
});
|
|
95
|
+
// Repo-scoped Claude-review history for the Activity single-repo console. Ownership +
|
|
96
|
+
// feature-gating are handled inside the query (an unowned repo / disabled feature
|
|
97
|
+
// both return an empty list), so the caller never leaks another account's data.
|
|
98
|
+
app.get('/api/repos/:id/claude-reviews', async (req) => {
|
|
99
|
+
const { id } = req.params;
|
|
100
|
+
return listClaudeReviewsByRepo(Number.parseInt(id, 10), accountIdOf(req));
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=activity.js.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { config } from '../../config.js';
|
|
2
|
+
import { getAiRuntimeStatus, installAiRuntime } from '../../ai/runtime.js';
|
|
3
|
+
// POST /api/ai/runtime/install — "Set up AI": download the pinned AI runtime (the Agent SDK and its
|
|
4
|
+
// three peers, ~110 MB) into `<dataDir>/ai-runtime`, streaming npm's progress as SSE.
|
|
5
|
+
// ai/runtime.ts owns the work; this route only relays it.
|
|
6
|
+
//
|
|
7
|
+
// LOCAL ONLY, STRUCTURALLY: registered only when `config.aiEnabled` (which is false in cloud and
|
|
8
|
+
// under LIMN_AI_DISABLED), and it refuses again on its own if ever reached in cloud.
|
|
9
|
+
//
|
|
10
|
+
// ⚠ IT IS A POST, SO THE DISCONNECT SIGNAL IS THE REPLY SOCKET (`reply.raw`), never
|
|
11
|
+
// `req.raw.on('close')` — a POST's request `close` fires as soon as the body is read (CLAUDE.md,
|
|
12
|
+
// Conventions). And a disconnect only UNSUBSCRIBES: the download is single-flight and keeps going,
|
|
13
|
+
// so a reload re-POSTs and joins the same run instead of starting a second one.
|
|
14
|
+
export async function aiRuntimeRoutes(app) {
|
|
15
|
+
app.post('/api/ai/runtime/install', async (_req, reply) => {
|
|
16
|
+
if (config.isCloud || !config.aiEnabled) {
|
|
17
|
+
return reply.code(404).send({ error: 'NotFound', message: 'Not available here.' });
|
|
18
|
+
}
|
|
19
|
+
const before = getAiRuntimeStatus();
|
|
20
|
+
reply.hijack();
|
|
21
|
+
const raw = reply.raw;
|
|
22
|
+
raw.writeHead(200, {
|
|
23
|
+
'Content-Type': 'text/event-stream',
|
|
24
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
25
|
+
Connection: 'keep-alive',
|
|
26
|
+
'X-Accel-Buffering': 'no',
|
|
27
|
+
});
|
|
28
|
+
let open = true;
|
|
29
|
+
const send = (e) => {
|
|
30
|
+
if (open && !raw.writableEnded)
|
|
31
|
+
raw.write(`data: ${JSON.stringify(e)}\n\n`);
|
|
32
|
+
};
|
|
33
|
+
const hb = setInterval(() => {
|
|
34
|
+
if (open && !raw.writableEnded)
|
|
35
|
+
raw.write(': hb\n\n');
|
|
36
|
+
}, 15_000);
|
|
37
|
+
const finish = () => {
|
|
38
|
+
if (!open)
|
|
39
|
+
return;
|
|
40
|
+
open = false;
|
|
41
|
+
clearInterval(hb);
|
|
42
|
+
if (!raw.writableEnded)
|
|
43
|
+
raw.end();
|
|
44
|
+
};
|
|
45
|
+
raw.on('close', () => {
|
|
46
|
+
open = false;
|
|
47
|
+
clearInterval(hb);
|
|
48
|
+
});
|
|
49
|
+
// Already there (a second tab, or the workspace's own SDK in dev): say so and stop.
|
|
50
|
+
if (before.runtime === 'ready') {
|
|
51
|
+
send({ type: 'done', runtime: 'ready' });
|
|
52
|
+
finish();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const result = await installAiRuntime((p) => {
|
|
56
|
+
if (p.phase === 'done' || p.phase === 'failed')
|
|
57
|
+
return; // the terminal event is sent below
|
|
58
|
+
send({ type: 'progress', phase: p.phase, message: p.message });
|
|
59
|
+
});
|
|
60
|
+
if (result.ok)
|
|
61
|
+
send({ type: 'done', runtime: 'ready' });
|
|
62
|
+
else
|
|
63
|
+
send({ type: 'error', message: result.message });
|
|
64
|
+
finish();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=ai-runtime.js.map
|