Files changed (3) hide show
  1. src/db.ts +127 -0
  2. src/rate-limiter.ts +43 -0
  3. src/review.ts +31 -25
src/db.ts ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import postgres from 'postgres'
2
+
3
+ const sql = postgres(process.env.DATABASE_URL!, {
4
+ max: 10,
5
+ idle_timeout: 20,
6
+ connect_timeout: 10,
7
+ transform: {
8
+ undefined: null,
9
+ },
10
+ })
11
+
12
+ export { sql }
13
+
14
+ export async function initializeDatabase() {
15
+ try {
16
+ await sql`
17
+ CREATE TABLE IF NOT EXISTS users (
18
+ id VARCHAR(50) PRIMARY KEY,
19
+ github_id INTEGER UNIQUE NOT NULL,
20
+ username VARCHAR(100) NOT NULL,
21
+ email VARCHAR(255) UNIQUE,
22
+ avatar_url TEXT,
23
+ plan VARCHAR(20) DEFAULT 'free',
24
+ created_at TIMESTAMP DEFAULT NOW(),
25
+ updated_at TIMESTAMP DEFAULT NOW()
26
+ )
27
+ `
28
+
29
+ await sql`
30
+ CREATE TABLE IF NOT EXISTS user_events (
31
+ id SERIAL PRIMARY KEY,
32
+ event_type VARCHAR(50) NOT NULL,
33
+ user_id VARCHAR(100),
34
+ session_id VARCHAR(100) NOT NULL,
35
+ page_url TEXT,
36
+ referrer TEXT,
37
+ user_agent TEXT,
38
+ ip_hash VARCHAR(64),
39
+ metadata JSONB,
40
+ created_at TIMESTAMP DEFAULT NOW()
41
+ )
42
+ `
43
+
44
+ await sql`
45
+ CREATE TABLE IF NOT EXISTS revenue_events (
46
+ id SERIAL PRIMARY KEY,
47
+ event_type VARCHAR(50) NOT NULL,
48
+ amount DECIMAL(10, 2) NOT NULL,
49
+ currency VARCHAR(3) DEFAULT 'USD',
50
+ customer_id VARCHAR(100),
51
+ github_id INTEGER,
52
+ subscription_tier VARCHAR(20),
53
+ metadata JSONB,
54
+ created_at TIMESTAMP DEFAULT NOW()
55
+ )
56
+ `
57
+
58
+ await sql`
59
+ CREATE TABLE IF NOT EXISTS affiliate_users (
60
+ id VARCHAR(100) PRIMARY KEY,
61
+ github_id INTEGER UNIQUE NOT NULL,
62
+ username VARCHAR(100) NOT NULL,
63
+ affiliate_code VARCHAR(50) UNIQUE NOT NULL,
64
+ referral_count INTEGER DEFAULT 0,
65
+ paid_referral_count INTEGER DEFAULT 0,
66
+ tier VARCHAR(20) DEFAULT 'free',
67
+ created_at TIMESTAMP DEFAULT NOW()
68
+ )
69
+ `
70
+
71
+ await sql`
72
+ CREATE TABLE IF NOT EXISTS referrals (
73
+ id VARCHAR(100) PRIMARY KEY,
74
+ affiliate_id VARCHAR(100) NOT NULL REFERENCES affiliate_users(id),
75
+ referred_github_id INTEGER UNIQUE NOT NULL,
76
+ referred_username VARCHAR(100) NOT NULL,
77
+ referred_ip_hash VARCHAR(64) NOT NULL,
78
+ has_purchased BOOLEAN DEFAULT FALSE,
79
+ created_at TIMESTAMP DEFAULT NOW()
80
+ )
81
+ `
82
+
83
+ await sql`
84
+ CREATE TABLE IF NOT EXISTS affiliate_events (
85
+ id SERIAL PRIMARY KEY,
86
+ event_type VARCHAR(50) NOT NULL,
87
+ affiliate_id VARCHAR(100) NOT NULL,
88
+ affiliate_code VARCHAR(50),
89
+ referrer_id VARCHAR(100),
90
+ commission_amount DECIMAL(10, 2) DEFAULT 0,
91
+ conversion_status VARCHAR(20) DEFAULT 'pending',
92
+ metadata JSONB,
93
+ created_at TIMESTAMP DEFAULT NOW()
94
+ )
95
+ `
96
+
97
+ await sql`
98
+ CREATE TABLE IF NOT EXISTS dashboard_metrics (
99
+ id SERIAL PRIMARY KEY,
100
+ metric_type VARCHAR(50) NOT NULL,
101
+ metric_name VARCHAR(100) NOT NULL,
102
+ metric_value DECIMAL(15, 2) NOT NULL,
103
+ metadata JSONB,
104
+ recorded_at TIMESTAMP DEFAULT NOW(),
105
+ UNIQUE(metric_type, metric_name, recorded_at)
106
+ )
107
+ `
108
+
109
+ await sql`
110
+ CREATE TABLE IF NOT EXISTS daily_aggregates (
111
+ id SERIAL PRIMARY KEY,
112
+ date DATE NOT NULL,
113
+ metric_category VARCHAR(50) NOT NULL,
114
+ metric_name VARCHAR(100) NOT NULL,
115
+ total_value DECIMAL(15, 2) NOT NULL,
116
+ count INTEGER DEFAULT 1,
117
+ metadata JSONB,
118
+ created_at TIMESTAMP DEFAULT NOW(),
119
+ UNIQUE(date, metric_category, metric_name)
120
+ )
121
+ `
122
+
123
+ console.log('Backend database initialized successfully')
124
+ } catch (error) {
125
+ console.error('Failed to initialize backend database:', error)
126
+ }
127
+ }
src/rate-limiter.ts ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { sql } from './db'
2
+
3
+ export async function isRateLimited(githubId: number, plan: string): Promise<boolean> {
4
+ const limits: Record<string, number> = {
5
+ 'free': 10, // 10 PRs per month
6
+ 'starter': 100, // 100 PRs per month
7
+ 'pro': 1000, // 1000 PRs per month
8
+ 'enterprise': 10000 // 10000 PRs per month
9
+ }
10
+
11
+ const limit = limits[plan] || limits['free']
12
+
13
+ try {
14
+ const firstDayOfMonth = new Date()
15
+ firstDayOfMonth.setDate(1)
16
+ firstDayOfMonth.setHours(0, 0, 0, 0)
17
+
18
+ const result = await sql`
19
+ SELECT count(*) as count
20
+ FROM user_events
21
+ WHERE user_id = ${githubId.toString()}
22
+ AND event_type = 'api_call'
23
+ AND created_at >= ${firstDayOfMonth}
24
+ `
25
+
26
+ const count = parseInt(result[0].count, 10)
27
+ return count >= limit
28
+ } catch (error) {
29
+ console.error('Rate limit check failed:', error)
30
+ return false // Fail open to not block users on DB error
31
+ }
32
+ }
33
+
34
+ export async function recordApiCall(githubId: number): Promise<void> {
35
+ try {
36
+ await sql`
37
+ INSERT INTO user_events (event_type, user_id, session_id, metadata)
38
+ VALUES ('api_call', ${githubId.toString()}, 'backend_session', ${JSON.stringify({ source: 'github_app' })})
39
+ `
40
+ } catch (error) {
41
+ console.error('Failed to record API call:', error)
42
+ }
43
+ }
src/review.ts CHANGED
@@ -1,7 +1,5 @@
1
  import {prixExec} from './utils'
2
  import {Project, SyntaxKind} from 'ts-morph'
3
- import {readFileSync, existsSync} from 'fs'
4
- import {join} from 'path'
5
 
6
  import pLimit from 'p-limit'
7
  import {Bot} from './bot'
@@ -28,6 +26,8 @@ import {semanticBugSearch, SimilarBugResult} from './semantic-search'
28
  import {confidenceCalibrator} from './confidence'
29
  import {testGenerator} from './test-generator'
30
  import {FixVerifier, VerificationResult} from './fix-verifier'
 
 
31
 
32
  let error = console.error
33
  let info = console.log
@@ -56,13 +56,10 @@ export const codeReview = async (
56
  const commenter: Commenter = new Commenter()
57
  const project = new Project()
58
 
59
- // Initialize shared context engine once at the start using the cloned repo path
60
  try {
61
- const store = als.getStore()
62
- const workingDir = store?.workingDir || process.cwd()
63
- const repoInfo = store?.repo
64
- const stableId = repoInfo ? `${repoInfo.owner}/${repoInfo.repo}` : undefined
65
- await unifiedContextEngine.initialize(workingDir, stableId)
66
  } catch (e) {
67
  info(`Context engine initialization failed: ${e}`)
68
  }
@@ -86,19 +83,34 @@ export const codeReview = async (
86
 
87
  // Skip audit for PRs opened by bots to prevent recursive loops
88
  // Also skip if the branch name indicates it's an AI remedy branch
89
- const prAuthor = context.payload.pull_request.user.type
 
 
90
  const branchName = context.payload.pull_request.head.ref
 
91
  if (
92
- prAuthor === 'Bot' ||
93
  branchName.startsWith('ai-remedy/') ||
94
  branchName.startsWith('github-actions[bot]')
95
  ) {
96
  info(
97
- `Skipping audit: PR author type=${prAuthor}, branch=${branchName}`
98
  )
99
  return
100
  }
101
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  const inputs: Inputs = new Inputs()
103
  inputs.title = context.payload.pull_request.title
104
  if (context.payload.pull_request.body != null) {
@@ -229,20 +241,14 @@ export const codeReview = async (
229
  return null
230
  }
231
  try {
232
- const store = als.getStore()
233
- const workingDir = store?.workingDir || process.cwd()
234
- const localPath = join(workingDir, file.filename)
235
-
236
- if (existsSync(localPath)) {
237
- fileContent = readFileSync(localPath, 'utf8')
238
- } else {
239
- const contents = await octokit.repos.getContent({
240
- owner: repo.owner,
241
- repo: repo.repo,
242
- path: file.filename,
243
- ref: context.payload.pull_request.base.sha
244
- })
245
- if (contents.data != null && !Array.isArray(contents.data)) {
246
  if (
247
  contents.data.type === 'file' &&
248
  contents.data.content != null
 
1
  import {prixExec} from './utils'
2
  import {Project, SyntaxKind} from 'ts-morph'
 
 
3
 
4
  import pLimit from 'p-limit'
5
  import {Bot} from './bot'
 
26
  import {confidenceCalibrator} from './confidence'
27
  import {testGenerator} from './test-generator'
28
  import {FixVerifier, VerificationResult} from './fix-verifier'
29
+ import {checkUserPlan} from './user-store'
30
+ import {isRateLimited, recordApiCall} from './rate-limiter'
31
 
32
  let error = console.error
33
  let info = console.log
 
56
  const commenter: Commenter = new Commenter()
57
  const project = new Project()
58
 
59
+ // Initialize shared context engine once at the start
60
  try {
61
+ const workingDir = prixExec('pwd', {encoding: 'utf8'}).trim()
62
+ await unifiedContextEngine.initialize(workingDir)
 
 
 
63
  } catch (e) {
64
  info(`Context engine initialization failed: ${e}`)
65
  }
 
83
 
84
  // Skip audit for PRs opened by bots to prevent recursive loops
85
  // Also skip if the branch name indicates it's an AI remedy branch
86
+ const prAuthorType = context.payload.pull_request.user.type
87
+ const prAuthorId = context.payload.pull_request.user.id
88
+ const prAuthorLogin = context.payload.pull_request.user.login
89
  const branchName = context.payload.pull_request.head.ref
90
+
91
  if (
92
+ prAuthorType === 'Bot' ||
93
  branchName.startsWith('ai-remedy/') ||
94
  branchName.startsWith('github-actions[bot]')
95
  ) {
96
  info(
97
+ `Skipping audit: PR author type=${prAuthorType}, branch=${branchName}`
98
  )
99
  return
100
  }
101
 
102
+ // Check user plan
103
+ const plan = await checkUserPlan(prAuthorId)
104
+
105
+ // Rate limit check
106
+ if (await isRateLimited(prAuthorId, plan)) {
107
+ info(`Rate limited: User @${prAuthorLogin} on ${plan} plan.`)
108
+ return
109
+ }
110
+
111
+ // Record usage
112
+ await recordApiCall(prAuthorId)
113
+
114
  const inputs: Inputs = new Inputs()
115
  inputs.title = context.payload.pull_request.title
116
  if (context.payload.pull_request.body != null) {
 
241
  return null
242
  }
243
  try {
244
+ const contents = await octokit.repos.getContent({
245
+ owner: repo.owner,
246
+ repo: repo.repo,
247
+ path: file.filename,
248
+ ref: context.payload.pull_request.base.sha
249
+ })
250
+ if (contents.data != null) {
251
+ if (!Array.isArray(contents.data)) {
 
 
 
 
 
 
252
  if (
253
  contents.data.type === 'file' &&
254
  contents.data.content != null