| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { internalAction } from "../_generated/server"; |
| import { internal } from "../_generated/api"; |
| import { v } from "convex/values"; |
|
|
| |
| const ENTITLEMENT_CACHE_TTL_SECONDS = 900; |
|
|
| |
| const REDIS_FETCH_TIMEOUT_MS = 5000; |
|
|
| |
| |
| |
| |
| function getEntitlementKey(userId: string): string { |
| const envPrefix = process.env.DODO_PAYMENTS_ENVIRONMENT === 'live_mode' ? 'live' : 'test'; |
| return `entitlements:${envPrefix}:${userId}`; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export const syncEntitlementCache = internalAction({ |
| args: { |
| userId: v.string(), |
| planKey: v.string(), |
| features: v.object({ |
| tier: v.number(), |
| maxDashboards: v.number(), |
| apiAccess: v.boolean(), |
| apiRateLimit: v.number(), |
| planLimits: v.optional(v.object({ |
| apiRequestsPerDay: v.union(v.number(), v.null()), |
| apiBurstRequestsPerMinute: v.union(v.number(), v.null()), |
| mcpCallsPerDay: v.union(v.number(), v.null()), |
| mcpBurstRequestsPerMinute: v.union(v.number(), v.null()), |
| })), |
| prioritySupport: v.boolean(), |
| exportFormats: v.array(v.string()), |
| |
| |
| mcpAccess: v.optional(v.boolean()), |
| |
| |
| apiDailyAllowance: v.optional(v.number()), |
| |
| |
| dataExport: v.optional(v.boolean()), |
| }), |
| validUntil: v.number(), |
| }, |
| handler: async (_ctx, args) => { |
| await writeEntitlementCacheToRedis(args.userId, args); |
| }, |
| }); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export const resyncEntitlementCacheFromDb = internalAction({ |
| args: { userId: v.string() }, |
| handler: async (ctx, args) => { |
| const current = await ctx.runQuery( |
| internal.entitlements.getEntitlementsByUserId, |
| { userId: args.userId }, |
| ); |
| await writeEntitlementCacheToRedis(args.userId, current); |
| }, |
| }); |
|
|
| async function writeEntitlementCacheToRedis( |
| userId: string, |
| payload: { planKey: string; features: unknown; validUntil: number }, |
| ): Promise<void> { |
| const url = process.env.UPSTASH_REDIS_REST_URL; |
| const token = process.env.UPSTASH_REDIS_REST_TOKEN; |
|
|
| if (!url || !token) { |
| console.warn( |
| "[cacheActions] UPSTASH_REDIS_REST_URL or UPSTASH_REDIS_REST_TOKEN not set -- skipping cache sync", |
| ); |
| return; |
| } |
|
|
| const key = getEntitlementKey(userId); |
| const value = JSON.stringify({ |
| planKey: payload.planKey, |
| features: payload.features, |
| validUntil: payload.validUntil, |
| }); |
|
|
| const controller = new AbortController(); |
| const timeout = setTimeout(() => controller.abort(), REDIS_FETCH_TIMEOUT_MS); |
| try { |
| const resp = await fetch( |
| `${url}/set/${encodeURIComponent(key)}/${encodeURIComponent(value)}/EX/${ENTITLEMENT_CACHE_TTL_SECONDS}`, |
| { |
| method: "POST", |
| headers: { Authorization: `Bearer ${token}` }, |
| signal: controller.signal, |
| }, |
| ); |
|
|
| if (!resp.ok) { |
| |
| |
| |
| |
| |
| |
| throw new Error( |
| `[cacheActions] Redis SET failed: HTTP ${resp.status} for user ${userId}`, |
| ); |
| } |
| } catch (err) { |
| console.warn( |
| "[cacheActions] Redis cache sync failed:", |
| err instanceof Error ? err.message : String(err), |
| ); |
| |
| |
| throw err; |
| } finally { |
| clearTimeout(timeout); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export const deleteEntitlementCache = internalAction({ |
| args: { userId: v.string() }, |
| handler: async (_ctx, args) => { |
| const url = process.env.UPSTASH_REDIS_REST_URL; |
| const token = process.env.UPSTASH_REDIS_REST_TOKEN; |
|
|
| if (!url || !token) return; |
|
|
| const key = getEntitlementKey(args.userId); |
|
|
| const controller = new AbortController(); |
| const timeout = setTimeout(() => controller.abort(), REDIS_FETCH_TIMEOUT_MS); |
| try { |
| const resp = await fetch( |
| `${url}/del/${encodeURIComponent(key)}`, |
| { |
| method: "POST", |
| headers: { Authorization: `Bearer ${token}` }, |
| signal: controller.signal, |
| }, |
| ); |
|
|
| if (!resp.ok) { |
| console.warn( |
| `[cacheActions] Redis DEL failed: HTTP ${resp.status} for key ${key}`, |
| ); |
| } |
| } catch (err) { |
| |
| |
| |
| |
| console.warn( |
| "[cacheActions] Redis cache delete failed:", |
| err instanceof Error ? err.message : String(err), |
| ); |
| } finally { |
| clearTimeout(timeout); |
| } |
| }, |
| }); |
|
|