1- import type IORedis from 'ioredis' ;
21import type { SecurityPolicyDoc } from './db' ;
3- import { createRedisClient } from './redis' ;
4-
5- const DEFAULT_REDIS_URL = process . env . REDIS_URL || 'redis://127.0.0.1:6379' ;
6-
7- let redisClient : IORedis | null = null ;
8- let redisHealthy = false ;
9-
10- function getRedis ( ) : IORedis | null {
11- if ( ! redisClient ) {
12- try {
13- redisClient = createRedisClient ( DEFAULT_REDIS_URL ) ;
14- redisClient . on ( 'error' , ( err ) => {
15- // eslint-disable-next-line no-console
16- console . error ( 'budget: redis error' , err ) ;
17- redisHealthy = false ;
18- } ) ;
19- redisClient . on ( 'connect' , ( ) => {
20- redisHealthy = true ;
21- } ) ;
22- } catch ( err ) {
23- // eslint-disable-next-line no-console
24- console . error ( 'budget: failed to create redis client' , err ) ;
25- redisHealthy = false ;
26- redisClient = null ;
27- }
28- }
29- return redisHealthy ? redisClient : null ;
30- }
2+ import { getOrgDailyBudgetsCollection } from './db' ;
313
324function currentDateKeyUtc ( ) : string {
335 const now = new Date ( ) ;
@@ -39,30 +11,57 @@ function currentDateKeyUtc(): string {
3911 return `${ y } ${ mm } ${ dd } ` ;
4012}
4113
42- async function incrementBudgetCounter (
43- key : string ,
14+ async function incrementMongoBudgetCounter (
15+ orgId : any ,
16+ date : string ,
17+ field : 'scan_count' | 'gpt_count' ,
4418 limit : number | null
4519) : Promise < boolean > {
4620 if ( limit == null ) {
47- return true ;
48- }
49-
50- const client = getRedis ( ) ;
51- if ( ! client ) {
52- // Redis unavailable: fail open, do not block scans or GPT
21+ // Unlimited budget
5322 return true ;
5423 }
5524
5625 try {
57- const count = await client . incr ( key ) ;
58- if ( count === 1 ) {
59- // Keep key a bit longer than one day to tolerate clock skew
60- await client . expire ( key , 60 * 60 * 24 + 60 * 5 ) ;
26+ const col = await getOrgDailyBudgetsCollection ( ) ;
27+ const now = new Date ( ) ;
28+
29+ const result = ( await col . findOneAndUpdate (
30+ { org_id : orgId , date } ,
31+ {
32+ $setOnInsert : {
33+ org_id : orgId ,
34+ date,
35+ created_at : now
36+ } ,
37+ $inc : { [ field ] : 1 } ,
38+ $set : { updated_at : now }
39+ } ,
40+ {
41+ upsert : true ,
42+ returnDocument : 'after'
43+ }
44+ ) ) as any ;
45+
46+ const doc = result && ( result . value ?? result ) ;
47+ if ( ! doc ) {
48+ // Should not happen in normal operation; fail open to preserve
49+ // existing semantics where infra issues do not hard-block scans/GPT.
50+ // eslint-disable-next-line no-console
51+ console . error ( 'budget: Mongo findOneAndUpdate returned no document' , {
52+ orgId,
53+ date,
54+ field
55+ } ) ;
56+ return true ;
6157 }
62- return count <= limit ;
58+
59+ const current = typeof doc [ field ] === 'number' ? doc [ field ] : 0 ;
60+ return current <= limit ;
6361 } catch ( err ) {
62+ // Preserve fail-open semantics from the Redis implementation.
6463 // eslint-disable-next-line no-console
65- console . error ( 'budget: increment failed, failing open' , err ) ;
64+ console . error ( 'budget: Mongo increment failed, failing open' , err ) ;
6665 return true ;
6766 }
6867}
@@ -74,8 +73,7 @@ export async function checkAndConsumeScanBudget(
7473 const limit =
7574 policy . max_scans_per_day == null ? null : policy . max_scans_per_day ;
7675 const date = currentDateKeyUtc ( ) ;
77- const key = `budget:scan:${ String ( orgId ) } :${ date } ` ;
78- return incrementBudgetCounter ( key , limit ) ;
76+ return incrementMongoBudgetCounter ( orgId , date , 'scan_count' , limit ) ;
7977}
8078
8179export async function checkAndConsumeGptBudget (
@@ -85,6 +83,5 @@ export async function checkAndConsumeGptBudget(
8583 const limit =
8684 policy . max_gpt_calls_per_day == null ? null : policy . max_gpt_calls_per_day ;
8785 const date = currentDateKeyUtc ( ) ;
88- const key = `budget:gpt:${ String ( orgId ) } :${ date } ` ;
89- return incrementBudgetCounter ( key , limit ) ;
86+ return incrementMongoBudgetCounter ( orgId , date , 'gpt_count' , limit ) ;
9087}
0 commit comments