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,194 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { config } from '../../config.js';
|
|
3
|
+
import { encryptToken } from '../../auth/crypto.js';
|
|
4
|
+
import { getAccessToken, upsertCloudAccount } from '../../auth/account.js';
|
|
5
|
+
// Revoke the OAuth App's grant for a user's token (Basic auth = the app's own id/secret). This
|
|
6
|
+
// forces the NEXT authorize to re-show consent — which is what re-injects the SAML SSO step; a
|
|
7
|
+
// silent re-issue would skip it and re-mint a still-de-authorized token. 404 = already gone.
|
|
8
|
+
async function revokeOAuthGrant(token) {
|
|
9
|
+
const basic = Buffer.from(`${config.githubOAuthClientId}:${config.githubOAuthClientSecret}`).toString('base64');
|
|
10
|
+
const res = await fetch(`https://api.github.com/applications/${config.githubOAuthClientId}/grant`, {
|
|
11
|
+
method: 'DELETE',
|
|
12
|
+
headers: {
|
|
13
|
+
authorization: `Basic ${basic}`,
|
|
14
|
+
accept: 'application/vnd.github+json',
|
|
15
|
+
'x-github-api-version': '2022-11-28',
|
|
16
|
+
},
|
|
17
|
+
body: JSON.stringify({ access_token: token }),
|
|
18
|
+
});
|
|
19
|
+
if (!res.ok && res.status !== 404) {
|
|
20
|
+
throw new Error(`revoke grant -> ${res.status}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// Per-provider credential + the scope to request (empty for the GitHub App).
|
|
24
|
+
function providerCredentials(provider) {
|
|
25
|
+
return provider === 'app'
|
|
26
|
+
? {
|
|
27
|
+
clientId: config.githubAppClientId,
|
|
28
|
+
clientSecret: config.githubAppClientSecret,
|
|
29
|
+
scope: '',
|
|
30
|
+
enabled: config.appProviderEnabled,
|
|
31
|
+
}
|
|
32
|
+
: {
|
|
33
|
+
clientId: config.githubOAuthClientId,
|
|
34
|
+
clientSecret: config.githubOAuthClientSecret,
|
|
35
|
+
scope: config.oauthScope,
|
|
36
|
+
enabled: config.oauthProviderEnabled,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export async function authRoutes(app) {
|
|
40
|
+
const redirectUri = `${config.appBaseUrl}/api/auth/callback`;
|
|
41
|
+
const secureCookie = config.appBaseUrl.startsWith('https://');
|
|
42
|
+
// The default provider for the bare /api/auth/login link (landing/billing CTAs): prefer the
|
|
43
|
+
// frictionless OAuth App; fall back to the GitHub App if that's the only one configured.
|
|
44
|
+
const defaultProvider = config.oauthProviderEnabled ? 'oauth' : 'app';
|
|
45
|
+
// Which sign-in methods this deployment offers — read by the (signed-out) SignInGate to render
|
|
46
|
+
// the right button(s), and by the SIGNED-IN Settings "GitHub App" section, which needs the slug
|
|
47
|
+
// to build the install link (signing in via the App does NOT install it — see
|
|
48
|
+
// docs/REALTIME-SYNC.md). Unauthenticated: it sits under the /api/auth/* auth-gate exemption.
|
|
49
|
+
app.get('/api/auth/providers', async () => ({
|
|
50
|
+
oauth: config.oauthProviderEnabled,
|
|
51
|
+
app: config.appProviderEnabled,
|
|
52
|
+
// Slug drives the install link on the gate + in Settings; only meaningful with the App.
|
|
53
|
+
appSlug: config.appProviderEnabled ? config.githubAppSlug : '',
|
|
54
|
+
}));
|
|
55
|
+
const startLogin = (provider, reply) => {
|
|
56
|
+
const cred = providerCredentials(provider);
|
|
57
|
+
if (!cred.enabled)
|
|
58
|
+
return reply.redirect('/app?auth=unavailable');
|
|
59
|
+
// Fold the provider into the state so the callback knows which credential to exchange with.
|
|
60
|
+
const state = `${provider}.${randomBytes(16).toString('hex')}`;
|
|
61
|
+
reply.setCookie('pierre_oauth_state', state, {
|
|
62
|
+
path: '/',
|
|
63
|
+
httpOnly: true,
|
|
64
|
+
sameSite: 'lax',
|
|
65
|
+
secure: secureCookie,
|
|
66
|
+
signed: true,
|
|
67
|
+
maxAge: 600,
|
|
68
|
+
});
|
|
69
|
+
const params = new URLSearchParams({
|
|
70
|
+
client_id: cred.clientId,
|
|
71
|
+
redirect_uri: redirectUri,
|
|
72
|
+
state,
|
|
73
|
+
});
|
|
74
|
+
if (cred.scope)
|
|
75
|
+
params.set('scope', cred.scope);
|
|
76
|
+
return reply.redirect(`https://github.com/login/oauth/authorize?${params.toString()}`);
|
|
77
|
+
};
|
|
78
|
+
// Explicit provider choice (the SignInGate's two buttons).
|
|
79
|
+
app.get('/api/auth/login/:provider', async (req, reply) => {
|
|
80
|
+
const provider = req.params.provider === 'app' ? 'app' : 'oauth';
|
|
81
|
+
return startLogin(provider, reply);
|
|
82
|
+
});
|
|
83
|
+
// Bare login (landing / billing CTAs). When BOTH providers are configured, don't silently
|
|
84
|
+
// pick one — bounce to the in-app sign-in gate so the user explicitly chooses OAuth App vs
|
|
85
|
+
// GitHub App. When only one is configured there's no choice to make, so go straight to it.
|
|
86
|
+
app.get('/api/auth/login', async (_req, reply) => {
|
|
87
|
+
if (config.oauthProviderEnabled && config.appProviderEnabled) {
|
|
88
|
+
return reply.redirect('/app');
|
|
89
|
+
}
|
|
90
|
+
return startLogin(defaultProvider, reply);
|
|
91
|
+
});
|
|
92
|
+
// "Reconnect GitHub" (the SAML-block banner action): revoke this app's grant so the re-auth
|
|
93
|
+
// shows FRESH consent — forcing the SAML SSO step that a silent re-issue would skip — then
|
|
94
|
+
// clear our session (so the callback re-runs the code exchange, not the already-signed-in
|
|
95
|
+
// short-circuit) and start a fresh OAuth login. Best-effort revoke: even if it fails, the
|
|
96
|
+
// re-login can still succeed if the user's SAML session is active.
|
|
97
|
+
app.get('/api/auth/reconnect', async (req, reply) => {
|
|
98
|
+
const accountId = req.session.get('accountId');
|
|
99
|
+
if (accountId != null) {
|
|
100
|
+
try {
|
|
101
|
+
await revokeOAuthGrant(await getAccessToken(accountId));
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
req.log.warn({ err }, 'reconnect: grant revoke failed, continuing to re-auth');
|
|
105
|
+
}
|
|
106
|
+
req.session.delete();
|
|
107
|
+
}
|
|
108
|
+
return startLogin('oauth', reply);
|
|
109
|
+
});
|
|
110
|
+
// Verify state → exchange code for a token (against the state's provider) → fetch the user →
|
|
111
|
+
// upsert account → set session → 302 to the app.
|
|
112
|
+
app.get('/api/auth/callback', async (req, reply) => {
|
|
113
|
+
// Already signed in? The callback URL carries a one-time ?code= that GitHub expires in
|
|
114
|
+
// ~10 min and invalidates on first use. A back-button, a restored tab, or a Chrome profile
|
|
115
|
+
// switch can re-request this exact URL; re-running the exchange would then fail on the
|
|
116
|
+
// already-consumed code. Short-circuit a valid session straight to the app instead.
|
|
117
|
+
if (req.session.get('accountId') != null) {
|
|
118
|
+
return reply.redirect('/app');
|
|
119
|
+
}
|
|
120
|
+
const { code, state } = req.query;
|
|
121
|
+
const rawCookie = req.cookies.pierre_oauth_state;
|
|
122
|
+
const unsigned = rawCookie
|
|
123
|
+
? req.unsignCookie(rawCookie)
|
|
124
|
+
: { valid: false, value: null };
|
|
125
|
+
reply.clearCookie('pierre_oauth_state', { path: '/' });
|
|
126
|
+
if (!code || !state || !unsigned.valid || unsigned.value !== state) {
|
|
127
|
+
// Stale/replayed callback (state cookie expired or CSRF mismatch). Bounce back so the
|
|
128
|
+
// user starts a fresh sign-in — never render raw JSON to a human.
|
|
129
|
+
return reply.redirect('/app?auth=expired');
|
|
130
|
+
}
|
|
131
|
+
// The provider chosen at /login is encoded in the state; pick the matching credential.
|
|
132
|
+
const provider = state.startsWith('app.') ? 'app' : 'oauth';
|
|
133
|
+
const cred = providerCredentials(provider);
|
|
134
|
+
// Exchange the code for a user access token.
|
|
135
|
+
let token;
|
|
136
|
+
try {
|
|
137
|
+
const res = await fetch('https://github.com/login/oauth/access_token', {
|
|
138
|
+
method: 'POST',
|
|
139
|
+
headers: { accept: 'application/json', 'content-type': 'application/json' },
|
|
140
|
+
body: JSON.stringify({
|
|
141
|
+
client_id: cred.clientId,
|
|
142
|
+
client_secret: cred.clientSecret,
|
|
143
|
+
code,
|
|
144
|
+
redirect_uri: redirectUri,
|
|
145
|
+
}),
|
|
146
|
+
});
|
|
147
|
+
const json = (await res.json());
|
|
148
|
+
if (!json.access_token) {
|
|
149
|
+
req.log.warn({ err: json.error_description ?? json.error, provider }, 'oauth token exchange returned no token');
|
|
150
|
+
return reply.redirect('/app?auth=failed');
|
|
151
|
+
}
|
|
152
|
+
token = json.access_token;
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
req.log.error({ err, provider }, 'oauth token exchange request failed');
|
|
156
|
+
return reply.redirect('/app?auth=error');
|
|
157
|
+
}
|
|
158
|
+
// Identify the user.
|
|
159
|
+
let user;
|
|
160
|
+
try {
|
|
161
|
+
const res = await fetch('https://api.github.com/user', {
|
|
162
|
+
headers: {
|
|
163
|
+
authorization: `token ${token}`,
|
|
164
|
+
accept: 'application/vnd.github+json',
|
|
165
|
+
'x-github-api-version': '2022-11-28',
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
if (!res.ok) {
|
|
169
|
+
req.log.warn({ status: res.status }, 'failed to fetch github user');
|
|
170
|
+
return reply.redirect('/app?auth=failed');
|
|
171
|
+
}
|
|
172
|
+
user = (await res.json());
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
req.log.error({ err }, 'fetching github user failed');
|
|
176
|
+
return reply.redirect('/app?auth=error');
|
|
177
|
+
}
|
|
178
|
+
const account = await upsertCloudAccount({
|
|
179
|
+
githubUserId: user.node_id,
|
|
180
|
+
githubLogin: user.login,
|
|
181
|
+
displayName: user.name ?? null,
|
|
182
|
+
avatarUrl: user.avatar_url,
|
|
183
|
+
accessTokenEnc: encryptToken(token),
|
|
184
|
+
});
|
|
185
|
+
req.session.set('accountId', account.id);
|
|
186
|
+
return reply.redirect('/app');
|
|
187
|
+
});
|
|
188
|
+
// Clear the session.
|
|
189
|
+
app.post('/api/auth/logout', async (req, reply) => {
|
|
190
|
+
req.session.delete();
|
|
191
|
+
return reply.code(204).send();
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// Stripe billing seam ("carve the path"): a checkout redirect + the webhook that
|
|
2
|
+
// flips `accounts.plan`. Deliberately dependency-free — signature verification is
|
|
3
|
+
// hand-rolled with node:crypto (no stripe SDK), and the whole feature is inert
|
|
4
|
+
// until STRIPE_PAYMENT_LINK_URL / STRIPE_WEBHOOK_SECRET are set (the webhook
|
|
5
|
+
// replies 501 unconfigured). Registered unconditionally in app.ts, both modes.
|
|
6
|
+
//
|
|
7
|
+
// Entitlement flows from `accounts.plan`: /api/me intersects the capability
|
|
8
|
+
// singleton with the plan (pro/contract.ts entitledProCapabilities) and the cloud
|
|
9
|
+
// auth gate 402s free-plan /api/pro/* requests (api/plugins/auth.ts). Local
|
|
10
|
+
// accounts are always fully entitled — this file never affects local behavior.
|
|
11
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
12
|
+
import { config } from '../../config.js';
|
|
13
|
+
import { getAccountById, getAccountByStripeCustomerId, setAccountPlan, } from '../../auth/account.js';
|
|
14
|
+
// Stripe's default signature tolerance: reject events older/newer than 5 minutes
|
|
15
|
+
// (replay protection).
|
|
16
|
+
export const STRIPE_SIGNATURE_TOLERANCE_SEC = 300;
|
|
17
|
+
/**
|
|
18
|
+
* Verify a Stripe webhook signature (the `stripe-signature` header) without the
|
|
19
|
+
* stripe SDK. Header format: `t=<unix-seconds>,v1=<hex>[,v1=<hex>…]` (multiple
|
|
20
|
+
* v1 entries appear during secret rotation). The signed payload is
|
|
21
|
+
* `<t>.<rawBody>`; the signature is HMAC-SHA256(secret) over it, hex-encoded.
|
|
22
|
+
* Compared timing-safely against every v1; the timestamp must be within
|
|
23
|
+
* STRIPE_SIGNATURE_TOLERANCE_SEC of `nowMs`.
|
|
24
|
+
*/
|
|
25
|
+
export function verifyStripeSignature(rawBody, header, secret, nowMs = Date.now()) {
|
|
26
|
+
let ts = null;
|
|
27
|
+
const v1s = [];
|
|
28
|
+
for (const part of header.split(',')) {
|
|
29
|
+
const idx = part.indexOf('=');
|
|
30
|
+
if (idx < 0)
|
|
31
|
+
continue;
|
|
32
|
+
const key = part.slice(0, idx).trim();
|
|
33
|
+
const value = part.slice(idx + 1).trim();
|
|
34
|
+
if (!value)
|
|
35
|
+
continue;
|
|
36
|
+
if (key === 't') {
|
|
37
|
+
const n = Number.parseInt(value, 10);
|
|
38
|
+
if (Number.isFinite(n))
|
|
39
|
+
ts = n;
|
|
40
|
+
}
|
|
41
|
+
else if (key === 'v1') {
|
|
42
|
+
v1s.push(value);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (ts == null || v1s.length === 0)
|
|
46
|
+
return false;
|
|
47
|
+
if (Math.abs(nowMs / 1000 - ts) > STRIPE_SIGNATURE_TOLERANCE_SEC)
|
|
48
|
+
return false;
|
|
49
|
+
const expected = createHmac('sha256', secret)
|
|
50
|
+
.update(`${ts}.`)
|
|
51
|
+
.update(rawBody)
|
|
52
|
+
.digest('hex');
|
|
53
|
+
const expectedBuf = Buffer.from(expected, 'utf-8');
|
|
54
|
+
return v1s.some((sig) => {
|
|
55
|
+
const sigBuf = Buffer.from(sig, 'utf-8');
|
|
56
|
+
return (sigBuf.length === expectedBuf.length && timingSafeEqual(sigBuf, expectedBuf));
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
// Stripe `customer` fields are a string id or an expanded object with `.id`.
|
|
60
|
+
function customerIdOf(value) {
|
|
61
|
+
if (typeof value === 'string' && value !== '')
|
|
62
|
+
return value;
|
|
63
|
+
if (value != null &&
|
|
64
|
+
typeof value === 'object' &&
|
|
65
|
+
typeof value.id === 'string') {
|
|
66
|
+
return value.id;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
/** Map a parsed Stripe event to the plan action it implies. Pure. */
|
|
71
|
+
export function stripeEventAction(event) {
|
|
72
|
+
if (event == null || typeof event !== 'object')
|
|
73
|
+
return { kind: 'ignore' };
|
|
74
|
+
const { type, data } = event;
|
|
75
|
+
if (typeof type !== 'string')
|
|
76
|
+
return { kind: 'ignore' };
|
|
77
|
+
const obj = (data?.object ?? null);
|
|
78
|
+
if (obj == null || typeof obj !== 'object')
|
|
79
|
+
return { kind: 'ignore' };
|
|
80
|
+
if (type === 'checkout.session.completed' ||
|
|
81
|
+
type === 'checkout.session.async_payment_succeeded') {
|
|
82
|
+
// Delayed-notification methods (SEPA, ACH…) fire `completed` with
|
|
83
|
+
// payment_status 'unpaid' and confirm later via async_payment_succeeded —
|
|
84
|
+
// only grant once the money actually landed. `no_payment_required` covers
|
|
85
|
+
// trials.
|
|
86
|
+
const paid = obj['payment_status'] === 'paid' ||
|
|
87
|
+
obj['payment_status'] === 'no_payment_required';
|
|
88
|
+
if (!paid)
|
|
89
|
+
return { kind: 'ignore' };
|
|
90
|
+
const ref = obj['client_reference_id'];
|
|
91
|
+
const accountId = typeof ref === 'string' && /^\d+$/.test(ref)
|
|
92
|
+
? Number.parseInt(ref, 10)
|
|
93
|
+
: typeof ref === 'number' && Number.isInteger(ref)
|
|
94
|
+
? ref
|
|
95
|
+
: null;
|
|
96
|
+
return {
|
|
97
|
+
kind: 'checkout_completed',
|
|
98
|
+
accountId,
|
|
99
|
+
stripeCustomerId: customerIdOf(obj['customer']),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
if (type === 'customer.subscription.updated' ||
|
|
103
|
+
type === 'customer.subscription.deleted') {
|
|
104
|
+
const status = obj['status'];
|
|
105
|
+
const active = type !== 'customer.subscription.deleted' &&
|
|
106
|
+
(status === 'active' || status === 'trialing');
|
|
107
|
+
return {
|
|
108
|
+
kind: 'subscription_status',
|
|
109
|
+
customerId: customerIdOf(obj['customer']),
|
|
110
|
+
plan: active ? 'pro' : 'free',
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return { kind: 'ignore' };
|
|
114
|
+
}
|
|
115
|
+
export async function billingRoutes(app) {
|
|
116
|
+
// Redirect the signed-in user to the Stripe Payment Link, tagging the checkout
|
|
117
|
+
// with their account id (client_reference_id rides the Payment Link URL and
|
|
118
|
+
// comes back on checkout.session.completed). Exempted from the cloud auth gate
|
|
119
|
+
// (it's a plain browser navigation from the public pricing page), so it must
|
|
120
|
+
// handle anonymous visitors itself: bounce them to sign-in rather than a 401.
|
|
121
|
+
app.get('/api/billing/checkout', async (req, reply) => {
|
|
122
|
+
if (!config.stripePaymentLinkUrl) {
|
|
123
|
+
// No Payment Link configured — which is the state today. Send the visitor to
|
|
124
|
+
// the contact form rather than back to a marketing page with an error flag on
|
|
125
|
+
// it: Pro is currently GIVEN, free for a month, to anyone who asks, so there is
|
|
126
|
+
// a real next step here and "unavailable" would be the wrong word for it.
|
|
127
|
+
// `?topic=pro` preselects the right subject in the form.
|
|
128
|
+
return reply.redirect('/contact?topic=pro', 302);
|
|
129
|
+
}
|
|
130
|
+
if (!req.account) {
|
|
131
|
+
return reply.redirect('/api/auth/login', 302);
|
|
132
|
+
}
|
|
133
|
+
const url = new URL(config.stripePaymentLinkUrl);
|
|
134
|
+
url.searchParams.set('client_reference_id', String(req.account.id));
|
|
135
|
+
return reply.redirect(url.toString(), 302);
|
|
136
|
+
});
|
|
137
|
+
// The webhook needs the RAW request body (the signature signs the exact bytes).
|
|
138
|
+
// Fastify content-type parsers are ENCAPSULATED, so registering the buffer
|
|
139
|
+
// parser inside this nested plugin scope affects ONLY the routes declared here
|
|
140
|
+
// — the rest of the API keeps normal JSON parsing (proven by billing.test.ts).
|
|
141
|
+
await app.register(async (scope) => {
|
|
142
|
+
scope.addContentTypeParser('application/json', { parseAs: 'buffer' }, (_req, body, done) => {
|
|
143
|
+
done(null, body);
|
|
144
|
+
});
|
|
145
|
+
// Stripe posts unauthenticated (exempted from the auth gate); authenticity
|
|
146
|
+
// comes from the signature. Never 500 on an unknown account — Stripe retries
|
|
147
|
+
// on non-2xx, and an unknown account is a data mismatch, not a server fault.
|
|
148
|
+
scope.post('/api/billing/webhook', async (req, reply) => {
|
|
149
|
+
if (!config.stripeWebhookSecret) {
|
|
150
|
+
return reply.code(501).send({ error: 'billing not configured' });
|
|
151
|
+
}
|
|
152
|
+
const raw = req.body;
|
|
153
|
+
if (!Buffer.isBuffer(raw)) {
|
|
154
|
+
return reply.code(400).send({ error: 'expected a JSON body' });
|
|
155
|
+
}
|
|
156
|
+
const sig = req.headers['stripe-signature'];
|
|
157
|
+
if (typeof sig !== 'string' ||
|
|
158
|
+
!verifyStripeSignature(raw, sig, config.stripeWebhookSecret)) {
|
|
159
|
+
return reply.code(400).send({ error: 'invalid signature' });
|
|
160
|
+
}
|
|
161
|
+
let event;
|
|
162
|
+
try {
|
|
163
|
+
event = JSON.parse(raw.toString('utf-8'));
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return reply.code(400).send({ error: 'invalid JSON' });
|
|
167
|
+
}
|
|
168
|
+
const action = stripeEventAction(event);
|
|
169
|
+
if (action.kind === 'checkout_completed') {
|
|
170
|
+
const account = action.accountId != null ? await getAccountById(action.accountId) : null;
|
|
171
|
+
if (account) {
|
|
172
|
+
// client_reference_id is attacker-influencable (it rides the public
|
|
173
|
+
// Payment Link URL), so never let a new checkout re-bind an account
|
|
174
|
+
// that already has a DIFFERENT Stripe customer — that would orphan
|
|
175
|
+
// the original subscription's webhooks.
|
|
176
|
+
const rebind = account.stripeCustomerId != null &&
|
|
177
|
+
action.stripeCustomerId != null &&
|
|
178
|
+
account.stripeCustomerId !== action.stripeCustomerId;
|
|
179
|
+
await setAccountPlan(account.id, 'pro', rebind ? undefined : action.stripeCustomerId);
|
|
180
|
+
if (rebind) {
|
|
181
|
+
req.log.warn({
|
|
182
|
+
accountId: account.id,
|
|
183
|
+
existingCustomerId: account.stripeCustomerId,
|
|
184
|
+
newCustomerId: action.stripeCustomerId,
|
|
185
|
+
}, 'stripe checkout for an account already bound to a different customer — kept the existing binding');
|
|
186
|
+
}
|
|
187
|
+
req.log.info({ accountId: account.id }, 'stripe checkout completed — plan set to pro');
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
req.log.warn({ clientReferenceId: action.accountId }, 'stripe checkout completed for unknown account — ignored');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
else if (action.kind === 'subscription_status') {
|
|
194
|
+
const account = action.customerId
|
|
195
|
+
? await getAccountByStripeCustomerId(action.customerId)
|
|
196
|
+
: null;
|
|
197
|
+
if (account) {
|
|
198
|
+
await setAccountPlan(account.id, action.plan);
|
|
199
|
+
req.log.info({ accountId: account.id, plan: action.plan }, 'stripe subscription status — plan updated');
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
req.log.warn({ customerId: action.customerId }, 'stripe subscription event for unknown customer — ignored');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return { received: true };
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=billing.js.map
|