| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239 |
- // Set a user's aiFeatureUsage or aiWorkbench (token) usage to a specific value.
- //
- // Mirrors the period-reset behaviour of:
- // app/src/infrastructure/rate-limiters/AiFeatureUsageRateLimiter.mjs
- // app/src/infrastructure/rate-limiters/TokenUsageRateLimiter.mjs
- // i.e. if the existing period (24h from periodStart) has expired, periodStart
- // is reset to NOW before the new usage value is written. Otherwise periodStart
- // is preserved.
- //
- // Usage:
- //
- // node scripts/set_user_ai_usage.mjs \
- // --user-id <userId> \
- // --feature <aiFeatureUsage|aiWorkbench> \
- // --usage <number> \
- // [--commit]
- //
- // Or, to put the user one use below their tier's quota:
- //
- // node scripts/set_user_ai_usage.mjs \
- // --user-id <userId> \
- // --edge <free|basic|standard|unlimited|token> \
- // [--commit]
- //
- // Without --commit the script is a dry run.
- import minimist from 'minimist'
- import {
- db,
- ObjectId,
- READ_PREFERENCE_PRIMARY,
- } from '../app/src/infrastructure/mongodb.mjs'
- import { UserFeatureUsage } from '../app/src/models/UserFeatureUsage.mjs'
- import { scriptRunner } from './lib/ScriptRunner.mjs'
- const VALID_FEATURES = ['aiFeatureUsage', 'aiWorkbench']
- const PERIOD_HOURS = 24
- // Each value puts the user 1 below the limit for that tier.
- const EDGE_PRESETS = {
- free: { feature: 'aiFeatureUsage', usage: 4 },
- basic: { feature: 'aiFeatureUsage', usage: 4 },
- standard: { feature: 'aiFeatureUsage', usage: 9 },
- unlimited: { feature: 'aiFeatureUsage', usage: 299 },
- token: { feature: 'aiWorkbench', usage: 7_999_999 },
- }
- const argv = minimist(process.argv.slice(2), {
- string: ['user-id', 'feature', 'edge'],
- boolean: ['commit', 'help'],
- alias: { 'user-id': 'userId', h: 'help' },
- })
- const HELP_TEXT = `\
- Set a user's AI usage (aiFeatureUsage) or token usage (aiWorkbench) to a
- specific value, mirroring the 24h period-reset behaviour of the production
- rate limiters. Intended for staging QA of the Shared AI Quota system —
- paywalls, near-quota warnings, period-reset behaviour, and token-limit
- headers — without grinding a real account through the UI.
- Usage:
- node scripts/set_user_ai_usage.mjs --user-id <id> [--commit] (
- --feature <aiFeatureUsage|aiWorkbench> --usage <number>
- | --edge <free|basic|standard|unlimited|token>
- )
- Options:
- --user-id <id> Target user's Mongo _id (required).
- --feature <name> aiFeatureUsage (use count) or aiWorkbench (tokens).
- --usage <number> Non-negative number to set features.<feature>.usage to.
- --edge <tier> Shortcut: set the user 1 below their tier's limit.
- Implies --feature and --usage; cannot be combined with
- either. Tiers:
- free aiFeatureUsage = 4
- basic aiFeatureUsage = 4
- standard aiFeatureUsage = 9
- unlimited aiFeatureUsage = 299
- token aiWorkbench = 7,999,999
- --commit Apply the write. Without this flag the script is a
- dry run and only prints the current value.
- -h, --help Show this help and exit.
- Examples:
- # Dry run — show what's currently recorded for a user, no write
- node scripts/set_user_ai_usage.mjs \\
- --user-id 5f9a2c8e1b3d4f0012abcd34 \\
- --feature aiFeatureUsage --usage 4
- # Put a Free user one use away from the AI paywall
- node scripts/set_user_ai_usage.mjs \\
- --user-id 5f9a2c8e1b3d4f0012abcd34 \\
- --edge free --commit
- # Put a Standard user one use away from the AI paywall
- node scripts/set_user_ai_usage.mjs \\
- --user-id 5f9a2c8e1b3d4f0012abcd34 \\
- --edge standard --commit
- # Put a Workbench user one token below the token limit
- node scripts/set_user_ai_usage.mjs \\
- --user-id 5f9a2c8e1b3d4f0012abcd34 \\
- --edge token --commit
- # Reset a user's AI usage back to 0
- node scripts/set_user_ai_usage.mjs \\
- --user-id 5f9a2c8e1b3d4f0012abcd34 \\
- --feature aiFeatureUsage --usage 0 --commit
- `
- if (argv.help) {
- console.log(HELP_TEXT)
- process.exit(0)
- }
- const userIdArg = argv['user-id']
- const edgeArg = argv.edge
- const COMMIT = argv.commit === true
- if (!userIdArg) throw new Error('missing --user-id (use --help for usage)')
- if (!ObjectId.isValid(userIdArg)) {
- throw new Error(`invalid --user-id: ${userIdArg}`)
- }
- let feature
- let usage
- if (edgeArg !== undefined) {
- if (argv.feature !== undefined || argv.usage !== undefined) {
- throw new Error('--edge cannot be combined with --feature or --usage')
- }
- const preset = EDGE_PRESETS[edgeArg]
- if (!preset) {
- throw new Error(
- `invalid --edge ${JSON.stringify(edgeArg)}; expected one of ${Object.keys(EDGE_PRESETS).join(', ')}`
- )
- }
- feature = preset.feature
- usage = preset.usage
- } else {
- feature = argv.feature
- if (!VALID_FEATURES.includes(feature)) {
- throw new Error(
- `invalid --feature ${JSON.stringify(feature)}; expected one of ${VALID_FEATURES.join(', ')}`
- )
- }
- usage = Number(argv.usage)
- if (!Number.isFinite(usage) || usage < 0) {
- throw new Error(`invalid --usage: ${argv.usage}`)
- }
- }
- const userId = new ObjectId(userIdArg)
- async function main() {
- const user = await db.users.findOne(
- { _id: userId },
- {
- projection: { _id: 1, email: 1 },
- readPreference: READ_PREFERENCE_PRIMARY,
- }
- )
- if (!user) {
- throw new Error(`user not found: ${userIdArg}`)
- }
- const before = await UserFeatureUsage.findOne({ _id: userId }).lean().exec()
- console.log('Before:', {
- userId: userIdArg,
- email: user.email,
- feature,
- current: before?.features?.[feature] ?? null,
- })
- if (!COMMIT) {
- console.warn(`Dry run: would set features.${feature}.usage = ${usage}`)
- console.warn('Re-run with --commit to apply.')
- return
- }
- const updated = await UserFeatureUsage.findOneAndUpdate(
- { _id: userId },
- [
- // reset periodStart/usage if the previous period (24h) has elapsed
- {
- $set: {
- features: {
- [feature]: {
- $cond: {
- if: {
- $lte: [
- {
- $dateAdd: {
- startDate: `$features.${feature}.periodStart`,
- unit: 'hour',
- amount: PERIOD_HOURS,
- },
- },
- '$$NOW',
- ],
- },
- then: { usage: 0, periodStart: '$$NOW' },
- else: `$features.${feature}`,
- },
- },
- },
- },
- },
- // overwrite usage with the requested value, preserving periodStart
- {
- $set: {
- [`features.${feature}.usage`]: usage,
- },
- },
- ],
- { new: true, upsert: true }
- )
- .lean()
- .exec()
- console.log('After:', {
- userId: userIdArg,
- feature,
- current: updated?.features?.[feature] ?? null,
- })
- }
- try {
- await scriptRunner(main, {
- userId: userIdArg,
- feature,
- usage,
- edge: edgeArg,
- commit: COMMIT,
- })
- console.error('Done.')
- process.exit(0)
- } catch (error) {
- console.error({ error })
- process.exit(1)
- }
|