@optima-chat/dev-skills 0.7.21 → 0.7.23
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/bin/helpers/db-utils.ts +143 -0
- package/bin/helpers/grant-credits.ts +65 -0
- package/bin/helpers/grant-subscription.ts +128 -0
- package/dist/bin/helpers/db-utils.js +166 -0
- package/dist/bin/helpers/grant-credits.js +71 -0
- package/dist/bin/helpers/grant-subscription.js +127 -0
- package/package.json +4 -2
- package/.claude/settings.local.json +0 -46
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
|
|
4
|
+
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
5
|
+
export interface InfisicalConfig { url: string; clientId: string; clientSecret: string; projectId: string }
|
|
6
|
+
|
|
7
|
+
export interface DBConnection {
|
|
8
|
+
host: string;
|
|
9
|
+
port: number;
|
|
10
|
+
user: string;
|
|
11
|
+
password: string;
|
|
12
|
+
database: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ─── Constants ──────────────────────────────────────────────────────────────
|
|
16
|
+
export const RDS_HOSTS: Record<string, string> = {
|
|
17
|
+
stage: 'optima-stage-postgres.ctg866o0ehac.ap-southeast-1.rds.amazonaws.com',
|
|
18
|
+
prod: 'optima-prod-postgres.ctg866o0ehac.ap-southeast-1.rds.amazonaws.com',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const EC2_HOST = '3.0.210.113';
|
|
22
|
+
|
|
23
|
+
// ─── SQL escaping ───────────────────────────────────────────────────────────
|
|
24
|
+
/** Escape a string value for safe inclusion in SQL single-quoted literals. */
|
|
25
|
+
export function escapeSQL(value: string): string {
|
|
26
|
+
return value.replace(/'/g, "''").replace(/\\/g, '\\\\');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ─── GitHub Variables ───────────────────────────────────────────────────────
|
|
30
|
+
export function getGitHubVariable(name: string): string {
|
|
31
|
+
return execSync(`gh variable get ${name} -R Optima-Chat/optima-dev-skills`, { encoding: 'utf-8' }).trim();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ─── Infisical ──────────────────────────────────────────────────────────────
|
|
35
|
+
export function getInfisicalConfig(): InfisicalConfig {
|
|
36
|
+
return {
|
|
37
|
+
url: getGitHubVariable('INFISICAL_URL'),
|
|
38
|
+
clientId: getGitHubVariable('INFISICAL_CLIENT_ID'),
|
|
39
|
+
clientSecret: getGitHubVariable('INFISICAL_CLIENT_SECRET'),
|
|
40
|
+
projectId: getGitHubVariable('INFISICAL_PROJECT_ID'),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function getInfisicalToken(config: InfisicalConfig): string {
|
|
45
|
+
const response = execSync(
|
|
46
|
+
`curl -s -X POST "${config.url}/api/v1/auth/universal-auth/login" -H "Content-Type: application/json" -d '{"clientId": "${config.clientId}", "clientSecret": "${config.clientSecret}"}'`,
|
|
47
|
+
{ encoding: 'utf-8' }
|
|
48
|
+
);
|
|
49
|
+
return JSON.parse(response).accessToken;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getInfisicalSecrets(config: InfisicalConfig, token: string, environment: string, secretPath: string): Record<string, string> {
|
|
53
|
+
const response = execSync(
|
|
54
|
+
`curl -s "${config.url}/api/v3/secrets/raw?workspaceId=${config.projectId}&environment=${environment}&secretPath=${secretPath}" -H "Authorization: Bearer ${token}"`,
|
|
55
|
+
{ encoding: 'utf-8' }
|
|
56
|
+
);
|
|
57
|
+
const data = JSON.parse(response);
|
|
58
|
+
const secrets: Record<string, string> = {};
|
|
59
|
+
for (const secret of data.secrets || []) {
|
|
60
|
+
secrets[secret.secretKey] = secret.secretValue;
|
|
61
|
+
}
|
|
62
|
+
return secrets;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─── Database URL parsing ───────────────────────────────────────────────────
|
|
66
|
+
export function parseDatabaseUrl(url: string): { user: string; password: string; host: string; port: number; database: string } {
|
|
67
|
+
const match = url.match(/^postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/([^?]+)/);
|
|
68
|
+
if (!match) throw new Error('Failed to parse DATABASE_URL (format: postgresql://user:pass@host:port/db)');
|
|
69
|
+
return { user: decodeURIComponent(match[1]), password: decodeURIComponent(match[2]), host: match[3], port: parseInt(match[4], 10), database: match[5] };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ─── SSH tunnel ─────────────────────────────────────────────────────────────
|
|
73
|
+
export function setupSSHTunnel(dbHost: string, localPort: number): void {
|
|
74
|
+
try { execSync(`lsof -ti:${localPort}`, { stdio: 'ignore' }); return; } catch { /* need tunnel */ }
|
|
75
|
+
const sshKeyPath = `${process.env.HOME}/.ssh/optima-ec2-key`;
|
|
76
|
+
if (!fs.existsSync(sshKeyPath)) throw new Error(`SSH key not found: ${sshKeyPath}. Please obtain optima-ec2-key from xbfool.`);
|
|
77
|
+
console.log(`Creating SSH tunnel: localhost:${localPort} -> ${EC2_HOST} -> ${dbHost}:5432`);
|
|
78
|
+
execSync(`ssh -i ${sshKeyPath} -f -N -o StrictHostKeyChecking=no -L ${localPort}:${dbHost}:5432 ec2-user@${EC2_HOST}`, { stdio: 'inherit' });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ─── psql ───────────────────────────────────────────────────────────────────
|
|
82
|
+
function findPsqlPath(): string {
|
|
83
|
+
try {
|
|
84
|
+
const result = execSync('which psql', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] });
|
|
85
|
+
if (result.trim()) return result.trim();
|
|
86
|
+
} catch { /* fallback */ }
|
|
87
|
+
const paths = ['/usr/local/opt/postgresql@16/bin/psql', '/usr/local/opt/postgresql@15/bin/psql', '/opt/homebrew/bin/psql', '/usr/bin/psql', '/usr/local/bin/psql'];
|
|
88
|
+
for (const p of paths) { if (fs.existsSync(p)) return p; }
|
|
89
|
+
throw new Error('PostgreSQL client (psql) not found. Install with: brew install postgresql@16');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function queryDB(conn: DBConnection, sql: string): string {
|
|
93
|
+
const psql = findPsqlPath();
|
|
94
|
+
return execSync(`"${psql}" -h ${conn.host} -p ${conn.port} -U ${conn.user} -d ${conn.database} -t -A --quiet -c "${sql.replace(/"/g, '\\"')}"`, {
|
|
95
|
+
encoding: 'utf-8',
|
|
96
|
+
env: { ...process.env, PGPASSWORD: conn.password },
|
|
97
|
+
}).trim();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ─── High-level connection helpers ──────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
/** Connect to user-auth DB and return a query function. */
|
|
103
|
+
export async function connectAuthDB(env: string, infisicalConfig: InfisicalConfig, token: string): Promise<{ query: (sql: string) => string }> {
|
|
104
|
+
const infisicalEnv = env === 'stage' ? 'staging' : 'prod';
|
|
105
|
+
const secrets = getInfisicalSecrets(infisicalConfig, token, infisicalEnv, '/shared-secrets/database-users');
|
|
106
|
+
const host = RDS_HOSTS[env as 'stage' | 'prod'];
|
|
107
|
+
const port = env === 'stage' ? 15432 : 15433;
|
|
108
|
+
|
|
109
|
+
setupSSHTunnel(host, port);
|
|
110
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
111
|
+
|
|
112
|
+
const conn: DBConnection = { host: 'localhost', port, user: secrets['AUTH_DB_USER'], password: secrets['AUTH_DB_PASSWORD'], database: 'optima_auth' };
|
|
113
|
+
return { query: (sql: string) => queryDB(conn, sql) };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Connect to billing DB and return a query function. */
|
|
117
|
+
export async function connectBillingDB(env: string, infisicalConfig: InfisicalConfig, token: string): Promise<{ query: (sql: string) => string }> {
|
|
118
|
+
const infisicalEnv = env === 'stage' ? 'staging' : 'prod';
|
|
119
|
+
const secrets = getInfisicalSecrets(infisicalConfig, token, infisicalEnv, '/services/billing');
|
|
120
|
+
const dbUrl = secrets['DATABASE_URL'];
|
|
121
|
+
if (!dbUrl) throw new Error('DATABASE_URL not found for billing service');
|
|
122
|
+
|
|
123
|
+
const parsed = parseDatabaseUrl(dbUrl);
|
|
124
|
+
const port = env === 'stage' ? 15434 : 15435;
|
|
125
|
+
setupSSHTunnel(parsed.host, port);
|
|
126
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
127
|
+
|
|
128
|
+
const conn: DBConnection = { host: 'localhost', port, user: parsed.user, password: parsed.password, database: parsed.database };
|
|
129
|
+
return { query: (sql: string) => queryDB(conn, sql) };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Look up user_id by email from user-auth DB. Throws if not found. */
|
|
133
|
+
export async function resolveUserId(email: string, env: string, infisicalConfig: InfisicalConfig, token: string): Promise<string> {
|
|
134
|
+
console.log(`Looking up user by email: ${email}`);
|
|
135
|
+
const auth = await connectAuthDB(env, infisicalConfig, token);
|
|
136
|
+
const userId = auth.query(`SELECT id FROM users WHERE email='${escapeSQL(email)}' LIMIT 1`);
|
|
137
|
+
if (!userId) {
|
|
138
|
+
console.error(`❌ User not found: ${email}`);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
console.log(`✓ Found user: ${userId}`);
|
|
142
|
+
return userId;
|
|
143
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { getInfisicalConfig, getInfisicalToken, resolveUserId, connectBillingDB, escapeSQL } from './db-utils';
|
|
4
|
+
|
|
5
|
+
function parseArgs(args: string[]): { email: string; amount: number; type: string; description: string | null; env: string } {
|
|
6
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
7
|
+
console.log(`Usage: optima-grant-credits <email> --amount <n> [options]
|
|
8
|
+
|
|
9
|
+
Options:
|
|
10
|
+
--amount <n> Credits to grant (required)
|
|
11
|
+
--type <type> Credit type: bonus, referral (default: bonus)
|
|
12
|
+
--description <text> Description (optional)
|
|
13
|
+
--env <env> Environment: stage, prod (default: stage)
|
|
14
|
+
-h, --help Show this help`);
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const email = args[0];
|
|
19
|
+
let amount = 0;
|
|
20
|
+
let type = 'bonus';
|
|
21
|
+
let description: string | null = null;
|
|
22
|
+
let env = 'stage';
|
|
23
|
+
|
|
24
|
+
for (let i = 1; i < args.length; i++) {
|
|
25
|
+
if (args[i] === '--amount' && args[i + 1]) { amount = parseInt(args[++i], 10); }
|
|
26
|
+
else if (args[i] === '--type' && args[i + 1]) { type = args[++i]; }
|
|
27
|
+
else if (args[i] === '--description' && args[i + 1]) { description = args[++i]; }
|
|
28
|
+
else if (args[i] === '--env' && args[i + 1]) { env = args[++i]; }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (amount < 1) { console.error('--amount is required and must be >= 1'); process.exit(1); }
|
|
32
|
+
if (!['bonus', 'referral'].includes(type)) { console.error(`Unknown type: ${type}. Available: bonus, referral`); process.exit(1); }
|
|
33
|
+
if (!['stage', 'prod'].includes(env)) { console.error('Env must be stage or prod (billing DB not available in CI)'); process.exit(1); }
|
|
34
|
+
|
|
35
|
+
return { email, amount, type, description, env };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function main() {
|
|
39
|
+
const { email, amount, type, description, env } = parseArgs(process.argv.slice(2));
|
|
40
|
+
const infisicalConfig = getInfisicalConfig();
|
|
41
|
+
const token = getInfisicalToken(infisicalConfig);
|
|
42
|
+
|
|
43
|
+
console.log(`\n🎁 Granting ${amount} ${type} credits to ${email} [${env.toUpperCase()}]\n`);
|
|
44
|
+
|
|
45
|
+
const userId = await resolveUserId(email, env, infisicalConfig, token);
|
|
46
|
+
const billing = await connectBillingDB(env, infisicalConfig, token);
|
|
47
|
+
const bq = billing.query;
|
|
48
|
+
|
|
49
|
+
const now = new Date().toISOString();
|
|
50
|
+
const safeUserId = escapeSQL(userId);
|
|
51
|
+
const safeType = escapeSQL(type);
|
|
52
|
+
const safeDesc = escapeSQL(description || `Admin ${type} credit grant`);
|
|
53
|
+
|
|
54
|
+
console.log(`Inserting ${amount} ${type} credits...`);
|
|
55
|
+
const ledgerId = bq(`INSERT INTO credit_ledger (id, user_id, type, description, initial_amount, remaining, created_at) VALUES (concat('crd_${safeType}_', substr(md5(random()::text), 1, 16)), '${safeUserId}', '${safeType}', '${safeDesc}', ${amount}, ${amount}, '${now}') RETURNING id`);
|
|
56
|
+
console.log(`✓ Credits granted (ledger ID: ${ledgerId})`);
|
|
57
|
+
|
|
58
|
+
const balance = bq(`SELECT COALESCE(SUM(remaining), 0) FROM credit_ledger WHERE user_id='${safeUserId}' AND remaining > 0 AND (expires_at IS NULL OR expires_at > NOW())`);
|
|
59
|
+
console.log(`\n✅ Done! ${email} now has ${balance} total credits\n`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
main().catch(error => {
|
|
63
|
+
console.error('\n❌ Error:', error.message);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { getInfisicalConfig, getInfisicalToken, resolveUserId, connectBillingDB, escapeSQL } from './db-utils';
|
|
4
|
+
|
|
5
|
+
function parseArgs(args: string[]): { email: string; plan: string; months: number; env: string } {
|
|
6
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
7
|
+
console.log(`Usage: optima-grant-subscription <email> [options]
|
|
8
|
+
|
|
9
|
+
Options:
|
|
10
|
+
--plan <id> Plan: free, pro, enterprise (default: enterprise)
|
|
11
|
+
--months <n> Duration in months (default: 1)
|
|
12
|
+
--env <env> Environment: stage, prod (default: stage)
|
|
13
|
+
-h, --help Show this help`);
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const email = args[0];
|
|
18
|
+
let plan = 'enterprise';
|
|
19
|
+
let months = 1;
|
|
20
|
+
let env = 'stage';
|
|
21
|
+
|
|
22
|
+
for (let i = 1; i < args.length; i++) {
|
|
23
|
+
if (args[i] === '--plan' && args[i + 1]) { plan = args[++i]; }
|
|
24
|
+
else if (args[i] === '--months' && args[i + 1]) { months = parseInt(args[++i], 10); }
|
|
25
|
+
else if (args[i] === '--env' && args[i + 1]) { env = args[++i]; }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!['free', 'pro', 'enterprise'].includes(plan)) {
|
|
29
|
+
console.error(`Unknown plan: ${plan}. Available: free, pro, enterprise`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
if (months < 1) { console.error('Months must be >= 1'); process.exit(1); }
|
|
33
|
+
if (!['stage', 'prod'].includes(env)) { console.error('Env must be stage or prod (billing DB not available in CI)'); process.exit(1); }
|
|
34
|
+
|
|
35
|
+
return { email, plan, months, env };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function main() {
|
|
39
|
+
const { email, plan, months, env } = parseArgs(process.argv.slice(2));
|
|
40
|
+
const infisicalConfig = getInfisicalConfig();
|
|
41
|
+
const token = getInfisicalToken(infisicalConfig);
|
|
42
|
+
|
|
43
|
+
console.log(`\n🎁 Granting ${plan} subscription to ${email} for ${months} month(s) [${env.toUpperCase()}]\n`);
|
|
44
|
+
|
|
45
|
+
const userId = await resolveUserId(email, env, infisicalConfig, token);
|
|
46
|
+
const billing = await connectBillingDB(env, infisicalConfig, token);
|
|
47
|
+
const bq = billing.query;
|
|
48
|
+
|
|
49
|
+
// Read plan config from DB
|
|
50
|
+
console.log(`Loading plan config: ${plan}`);
|
|
51
|
+
const planRow = bq(`SELECT name, monthly_credits, session_token_limit, weekly_token_limit FROM plans WHERE id='${escapeSQL(plan)}'`);
|
|
52
|
+
if (!planRow) { console.error(`❌ Plan not found in DB: ${plan}`); process.exit(1); }
|
|
53
|
+
|
|
54
|
+
const [planName, monthlyCreditsStr, sessionTokenLimitStr, weeklyTokenLimitStr] = planRow.split('|');
|
|
55
|
+
const monthlyCredits = parseInt(monthlyCreditsStr, 10);
|
|
56
|
+
const sessionTokenLimit = parseInt(sessionTokenLimitStr, 10);
|
|
57
|
+
const weeklyTokenLimit = parseInt(weeklyTokenLimitStr, 10);
|
|
58
|
+
console.log(`✓ Plan: ${planName} (credits: ${monthlyCredits}, session: ${sessionTokenLimit.toLocaleString()}, weekly: ${weeklyTokenLimit.toLocaleString()})`);
|
|
59
|
+
|
|
60
|
+
// Execute all mutations in a single transaction
|
|
61
|
+
const now = new Date().toISOString();
|
|
62
|
+
const periodEnd = new Date();
|
|
63
|
+
periodEnd.setMonth(periodEnd.getMonth() + months);
|
|
64
|
+
const periodEndISO = periodEnd.toISOString();
|
|
65
|
+
const sessionEnd = new Date(new Date().getTime() + 5 * 60 * 60 * 1000).toISOString();
|
|
66
|
+
const weekEnd = new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
67
|
+
|
|
68
|
+
const safeUserId = escapeSQL(userId);
|
|
69
|
+
const safePlan = escapeSQL(plan);
|
|
70
|
+
const safePlanName = escapeSQL(planName);
|
|
71
|
+
|
|
72
|
+
console.log('Executing transaction...');
|
|
73
|
+
const txSQL = `
|
|
74
|
+
BEGIN;
|
|
75
|
+
|
|
76
|
+
-- Cancel active subscriptions
|
|
77
|
+
UPDATE subscriptions SET status='canceled', canceled_at='${now}'
|
|
78
|
+
WHERE user_id='${safeUserId}' AND status IN ('active','trialing');
|
|
79
|
+
|
|
80
|
+
-- Zero out old subscription credits
|
|
81
|
+
UPDATE credit_ledger SET remaining=0
|
|
82
|
+
WHERE user_id='${safeUserId}' AND type IN ('monthly_grant','subscription') AND remaining > 0;
|
|
83
|
+
|
|
84
|
+
-- Create new subscription
|
|
85
|
+
INSERT INTO subscriptions (id, user_id, plan_id, status, billing_interval, current_period_start, current_period_end, created_at, updated_at)
|
|
86
|
+
VALUES (concat('sub_gift_', substr(md5(random()::text), 1, 16)), '${safeUserId}', '${safePlan}', 'active', 'monthly', '${now}', '${periodEndISO}', '${now}', '${now}');
|
|
87
|
+
|
|
88
|
+
-- Grant monthly credits
|
|
89
|
+
INSERT INTO credit_ledger (id, user_id, type, description, initial_amount, remaining, expires_at, created_at)
|
|
90
|
+
SELECT concat('crd_gift_', substr(md5(random()::text), 1, 16)), '${safeUserId}', 'subscription', '${safePlanName} plan gift (${months} month)', ${monthlyCredits}, ${monthlyCredits}, '${periodEndISO}', '${now}'
|
|
91
|
+
WHERE ${monthlyCredits} > 0;
|
|
92
|
+
|
|
93
|
+
-- Update existing active session quota, or insert new one if none exists
|
|
94
|
+
UPDATE token_quotas SET plan_id='${safePlan}', monthly_limit=${sessionTokenLimit}, updated_at='${now}'
|
|
95
|
+
WHERE user_id='${safeUserId}' AND period_type='session' AND period_end > '${now}';
|
|
96
|
+
|
|
97
|
+
INSERT INTO token_quotas (id, user_id, plan_id, period_type, monthly_limit, monthly_used, period_start, period_end, created_at, updated_at)
|
|
98
|
+
SELECT concat('tq_sess_', substr(md5(random()::text), 1, 16)), '${safeUserId}', '${safePlan}', 'session', ${sessionTokenLimit}, 0, '${now}', '${sessionEnd}', '${now}', '${now}'
|
|
99
|
+
WHERE NOT EXISTS (SELECT 1 FROM token_quotas WHERE user_id='${safeUserId}' AND period_type='session' AND period_end > '${now}');
|
|
100
|
+
|
|
101
|
+
-- Update existing active weekly quota, or insert new one if none exists
|
|
102
|
+
UPDATE token_quotas SET plan_id='${safePlan}', monthly_limit=${weeklyTokenLimit}, updated_at='${now}'
|
|
103
|
+
WHERE user_id='${safeUserId}' AND period_type='weekly' AND period_end > '${now}';
|
|
104
|
+
|
|
105
|
+
INSERT INTO token_quotas (id, user_id, plan_id, period_type, monthly_limit, monthly_used, period_start, period_end, created_at, updated_at)
|
|
106
|
+
SELECT concat('tq_week_', substr(md5(random()::text), 1, 16)), '${safeUserId}', '${safePlan}', 'weekly', ${weeklyTokenLimit}, 0, '${now}', '${weekEnd}', '${now}', '${now}'
|
|
107
|
+
WHERE NOT EXISTS (SELECT 1 FROM token_quotas WHERE user_id='${safeUserId}' AND period_type='weekly' AND period_end > '${now}');
|
|
108
|
+
|
|
109
|
+
COMMIT;
|
|
110
|
+
`.trim();
|
|
111
|
+
|
|
112
|
+
bq(txSQL);
|
|
113
|
+
|
|
114
|
+
console.log('✓ Old subscriptions canceled');
|
|
115
|
+
console.log('✓ Old credits cleared');
|
|
116
|
+
console.log(`✓ ${planName} subscription created (expires: ${periodEnd.toLocaleDateString()})`);
|
|
117
|
+
if (monthlyCredits > 0) {
|
|
118
|
+
console.log(`✓ ${monthlyCredits} credits granted`);
|
|
119
|
+
}
|
|
120
|
+
console.log(`✓ Token quotas updated (session: ${sessionTokenLimit.toLocaleString()}, weekly: ${weeklyTokenLimit.toLocaleString()})`);
|
|
121
|
+
|
|
122
|
+
console.log(`\n✅ Done! ${email} now has ${planName} plan until ${periodEnd.toLocaleDateString()}\n`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
main().catch(error => {
|
|
126
|
+
console.error('\n❌ Error:', error.message);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
});
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.RDS_HOSTS = void 0;
|
|
37
|
+
exports.escapeSQL = escapeSQL;
|
|
38
|
+
exports.getGitHubVariable = getGitHubVariable;
|
|
39
|
+
exports.getInfisicalConfig = getInfisicalConfig;
|
|
40
|
+
exports.getInfisicalToken = getInfisicalToken;
|
|
41
|
+
exports.getInfisicalSecrets = getInfisicalSecrets;
|
|
42
|
+
exports.parseDatabaseUrl = parseDatabaseUrl;
|
|
43
|
+
exports.setupSSHTunnel = setupSSHTunnel;
|
|
44
|
+
exports.queryDB = queryDB;
|
|
45
|
+
exports.connectAuthDB = connectAuthDB;
|
|
46
|
+
exports.connectBillingDB = connectBillingDB;
|
|
47
|
+
exports.resolveUserId = resolveUserId;
|
|
48
|
+
const child_process_1 = require("child_process");
|
|
49
|
+
const fs = __importStar(require("fs"));
|
|
50
|
+
// ─── Constants ──────────────────────────────────────────────────────────────
|
|
51
|
+
exports.RDS_HOSTS = {
|
|
52
|
+
stage: 'optima-stage-postgres.ctg866o0ehac.ap-southeast-1.rds.amazonaws.com',
|
|
53
|
+
prod: 'optima-prod-postgres.ctg866o0ehac.ap-southeast-1.rds.amazonaws.com',
|
|
54
|
+
};
|
|
55
|
+
const EC2_HOST = '3.0.210.113';
|
|
56
|
+
// ─── SQL escaping ───────────────────────────────────────────────────────────
|
|
57
|
+
/** Escape a string value for safe inclusion in SQL single-quoted literals. */
|
|
58
|
+
function escapeSQL(value) {
|
|
59
|
+
return value.replace(/'/g, "''").replace(/\\/g, '\\\\');
|
|
60
|
+
}
|
|
61
|
+
// ─── GitHub Variables ───────────────────────────────────────────────────────
|
|
62
|
+
function getGitHubVariable(name) {
|
|
63
|
+
return (0, child_process_1.execSync)(`gh variable get ${name} -R Optima-Chat/optima-dev-skills`, { encoding: 'utf-8' }).trim();
|
|
64
|
+
}
|
|
65
|
+
// ─── Infisical ──────────────────────────────────────────────────────────────
|
|
66
|
+
function getInfisicalConfig() {
|
|
67
|
+
return {
|
|
68
|
+
url: getGitHubVariable('INFISICAL_URL'),
|
|
69
|
+
clientId: getGitHubVariable('INFISICAL_CLIENT_ID'),
|
|
70
|
+
clientSecret: getGitHubVariable('INFISICAL_CLIENT_SECRET'),
|
|
71
|
+
projectId: getGitHubVariable('INFISICAL_PROJECT_ID'),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function getInfisicalToken(config) {
|
|
75
|
+
const response = (0, child_process_1.execSync)(`curl -s -X POST "${config.url}/api/v1/auth/universal-auth/login" -H "Content-Type: application/json" -d '{"clientId": "${config.clientId}", "clientSecret": "${config.clientSecret}"}'`, { encoding: 'utf-8' });
|
|
76
|
+
return JSON.parse(response).accessToken;
|
|
77
|
+
}
|
|
78
|
+
function getInfisicalSecrets(config, token, environment, secretPath) {
|
|
79
|
+
const response = (0, child_process_1.execSync)(`curl -s "${config.url}/api/v3/secrets/raw?workspaceId=${config.projectId}&environment=${environment}&secretPath=${secretPath}" -H "Authorization: Bearer ${token}"`, { encoding: 'utf-8' });
|
|
80
|
+
const data = JSON.parse(response);
|
|
81
|
+
const secrets = {};
|
|
82
|
+
for (const secret of data.secrets || []) {
|
|
83
|
+
secrets[secret.secretKey] = secret.secretValue;
|
|
84
|
+
}
|
|
85
|
+
return secrets;
|
|
86
|
+
}
|
|
87
|
+
// ─── Database URL parsing ───────────────────────────────────────────────────
|
|
88
|
+
function parseDatabaseUrl(url) {
|
|
89
|
+
const match = url.match(/^postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/([^?]+)/);
|
|
90
|
+
if (!match)
|
|
91
|
+
throw new Error('Failed to parse DATABASE_URL (format: postgresql://user:pass@host:port/db)');
|
|
92
|
+
return { user: decodeURIComponent(match[1]), password: decodeURIComponent(match[2]), host: match[3], port: parseInt(match[4], 10), database: match[5] };
|
|
93
|
+
}
|
|
94
|
+
// ─── SSH tunnel ─────────────────────────────────────────────────────────────
|
|
95
|
+
function setupSSHTunnel(dbHost, localPort) {
|
|
96
|
+
try {
|
|
97
|
+
(0, child_process_1.execSync)(`lsof -ti:${localPort}`, { stdio: 'ignore' });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
catch { /* need tunnel */ }
|
|
101
|
+
const sshKeyPath = `${process.env.HOME}/.ssh/optima-ec2-key`;
|
|
102
|
+
if (!fs.existsSync(sshKeyPath))
|
|
103
|
+
throw new Error(`SSH key not found: ${sshKeyPath}. Please obtain optima-ec2-key from xbfool.`);
|
|
104
|
+
console.log(`Creating SSH tunnel: localhost:${localPort} -> ${EC2_HOST} -> ${dbHost}:5432`);
|
|
105
|
+
(0, child_process_1.execSync)(`ssh -i ${sshKeyPath} -f -N -o StrictHostKeyChecking=no -L ${localPort}:${dbHost}:5432 ec2-user@${EC2_HOST}`, { stdio: 'inherit' });
|
|
106
|
+
}
|
|
107
|
+
// ─── psql ───────────────────────────────────────────────────────────────────
|
|
108
|
+
function findPsqlPath() {
|
|
109
|
+
try {
|
|
110
|
+
const result = (0, child_process_1.execSync)('which psql', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] });
|
|
111
|
+
if (result.trim())
|
|
112
|
+
return result.trim();
|
|
113
|
+
}
|
|
114
|
+
catch { /* fallback */ }
|
|
115
|
+
const paths = ['/usr/local/opt/postgresql@16/bin/psql', '/usr/local/opt/postgresql@15/bin/psql', '/opt/homebrew/bin/psql', '/usr/bin/psql', '/usr/local/bin/psql'];
|
|
116
|
+
for (const p of paths) {
|
|
117
|
+
if (fs.existsSync(p))
|
|
118
|
+
return p;
|
|
119
|
+
}
|
|
120
|
+
throw new Error('PostgreSQL client (psql) not found. Install with: brew install postgresql@16');
|
|
121
|
+
}
|
|
122
|
+
function queryDB(conn, sql) {
|
|
123
|
+
const psql = findPsqlPath();
|
|
124
|
+
return (0, child_process_1.execSync)(`"${psql}" -h ${conn.host} -p ${conn.port} -U ${conn.user} -d ${conn.database} -t -A --quiet -c "${sql.replace(/"/g, '\\"')}"`, {
|
|
125
|
+
encoding: 'utf-8',
|
|
126
|
+
env: { ...process.env, PGPASSWORD: conn.password },
|
|
127
|
+
}).trim();
|
|
128
|
+
}
|
|
129
|
+
// ─── High-level connection helpers ──────────────────────────────────────────
|
|
130
|
+
/** Connect to user-auth DB and return a query function. */
|
|
131
|
+
async function connectAuthDB(env, infisicalConfig, token) {
|
|
132
|
+
const infisicalEnv = env === 'stage' ? 'staging' : 'prod';
|
|
133
|
+
const secrets = getInfisicalSecrets(infisicalConfig, token, infisicalEnv, '/shared-secrets/database-users');
|
|
134
|
+
const host = exports.RDS_HOSTS[env];
|
|
135
|
+
const port = env === 'stage' ? 15432 : 15433;
|
|
136
|
+
setupSSHTunnel(host, port);
|
|
137
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
138
|
+
const conn = { host: 'localhost', port, user: secrets['AUTH_DB_USER'], password: secrets['AUTH_DB_PASSWORD'], database: 'optima_auth' };
|
|
139
|
+
return { query: (sql) => queryDB(conn, sql) };
|
|
140
|
+
}
|
|
141
|
+
/** Connect to billing DB and return a query function. */
|
|
142
|
+
async function connectBillingDB(env, infisicalConfig, token) {
|
|
143
|
+
const infisicalEnv = env === 'stage' ? 'staging' : 'prod';
|
|
144
|
+
const secrets = getInfisicalSecrets(infisicalConfig, token, infisicalEnv, '/services/billing');
|
|
145
|
+
const dbUrl = secrets['DATABASE_URL'];
|
|
146
|
+
if (!dbUrl)
|
|
147
|
+
throw new Error('DATABASE_URL not found for billing service');
|
|
148
|
+
const parsed = parseDatabaseUrl(dbUrl);
|
|
149
|
+
const port = env === 'stage' ? 15434 : 15435;
|
|
150
|
+
setupSSHTunnel(parsed.host, port);
|
|
151
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
152
|
+
const conn = { host: 'localhost', port, user: parsed.user, password: parsed.password, database: parsed.database };
|
|
153
|
+
return { query: (sql) => queryDB(conn, sql) };
|
|
154
|
+
}
|
|
155
|
+
/** Look up user_id by email from user-auth DB. Throws if not found. */
|
|
156
|
+
async function resolveUserId(email, env, infisicalConfig, token) {
|
|
157
|
+
console.log(`Looking up user by email: ${email}`);
|
|
158
|
+
const auth = await connectAuthDB(env, infisicalConfig, token);
|
|
159
|
+
const userId = auth.query(`SELECT id FROM users WHERE email='${escapeSQL(email)}' LIMIT 1`);
|
|
160
|
+
if (!userId) {
|
|
161
|
+
console.error(`❌ User not found: ${email}`);
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
console.log(`✓ Found user: ${userId}`);
|
|
165
|
+
return userId;
|
|
166
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const db_utils_1 = require("./db-utils");
|
|
5
|
+
function parseArgs(args) {
|
|
6
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
7
|
+
console.log(`Usage: optima-grant-credits <email> --amount <n> [options]
|
|
8
|
+
|
|
9
|
+
Options:
|
|
10
|
+
--amount <n> Credits to grant (required)
|
|
11
|
+
--type <type> Credit type: bonus, referral (default: bonus)
|
|
12
|
+
--description <text> Description (optional)
|
|
13
|
+
--env <env> Environment: stage, prod (default: stage)
|
|
14
|
+
-h, --help Show this help`);
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
const email = args[0];
|
|
18
|
+
let amount = 0;
|
|
19
|
+
let type = 'bonus';
|
|
20
|
+
let description = null;
|
|
21
|
+
let env = 'stage';
|
|
22
|
+
for (let i = 1; i < args.length; i++) {
|
|
23
|
+
if (args[i] === '--amount' && args[i + 1]) {
|
|
24
|
+
amount = parseInt(args[++i], 10);
|
|
25
|
+
}
|
|
26
|
+
else if (args[i] === '--type' && args[i + 1]) {
|
|
27
|
+
type = args[++i];
|
|
28
|
+
}
|
|
29
|
+
else if (args[i] === '--description' && args[i + 1]) {
|
|
30
|
+
description = args[++i];
|
|
31
|
+
}
|
|
32
|
+
else if (args[i] === '--env' && args[i + 1]) {
|
|
33
|
+
env = args[++i];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (amount < 1) {
|
|
37
|
+
console.error('--amount is required and must be >= 1');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
if (!['bonus', 'referral'].includes(type)) {
|
|
41
|
+
console.error(`Unknown type: ${type}. Available: bonus, referral`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
if (!['stage', 'prod'].includes(env)) {
|
|
45
|
+
console.error('Env must be stage or prod (billing DB not available in CI)');
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
return { email, amount, type, description, env };
|
|
49
|
+
}
|
|
50
|
+
async function main() {
|
|
51
|
+
const { email, amount, type, description, env } = parseArgs(process.argv.slice(2));
|
|
52
|
+
const infisicalConfig = (0, db_utils_1.getInfisicalConfig)();
|
|
53
|
+
const token = (0, db_utils_1.getInfisicalToken)(infisicalConfig);
|
|
54
|
+
console.log(`\n🎁 Granting ${amount} ${type} credits to ${email} [${env.toUpperCase()}]\n`);
|
|
55
|
+
const userId = await (0, db_utils_1.resolveUserId)(email, env, infisicalConfig, token);
|
|
56
|
+
const billing = await (0, db_utils_1.connectBillingDB)(env, infisicalConfig, token);
|
|
57
|
+
const bq = billing.query;
|
|
58
|
+
const now = new Date().toISOString();
|
|
59
|
+
const safeUserId = (0, db_utils_1.escapeSQL)(userId);
|
|
60
|
+
const safeType = (0, db_utils_1.escapeSQL)(type);
|
|
61
|
+
const safeDesc = (0, db_utils_1.escapeSQL)(description || `Admin ${type} credit grant`);
|
|
62
|
+
console.log(`Inserting ${amount} ${type} credits...`);
|
|
63
|
+
const ledgerId = bq(`INSERT INTO credit_ledger (id, user_id, type, description, initial_amount, remaining, created_at) VALUES (concat('crd_${safeType}_', substr(md5(random()::text), 1, 16)), '${safeUserId}', '${safeType}', '${safeDesc}', ${amount}, ${amount}, '${now}') RETURNING id`);
|
|
64
|
+
console.log(`✓ Credits granted (ledger ID: ${ledgerId})`);
|
|
65
|
+
const balance = bq(`SELECT COALESCE(SUM(remaining), 0) FROM credit_ledger WHERE user_id='${safeUserId}' AND remaining > 0 AND (expires_at IS NULL OR expires_at > NOW())`);
|
|
66
|
+
console.log(`\n✅ Done! ${email} now has ${balance} total credits\n`);
|
|
67
|
+
}
|
|
68
|
+
main().catch(error => {
|
|
69
|
+
console.error('\n❌ Error:', error.message);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const db_utils_1 = require("./db-utils");
|
|
5
|
+
function parseArgs(args) {
|
|
6
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
7
|
+
console.log(`Usage: optima-grant-subscription <email> [options]
|
|
8
|
+
|
|
9
|
+
Options:
|
|
10
|
+
--plan <id> Plan: free, pro, enterprise (default: enterprise)
|
|
11
|
+
--months <n> Duration in months (default: 1)
|
|
12
|
+
--env <env> Environment: stage, prod (default: stage)
|
|
13
|
+
-h, --help Show this help`);
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}
|
|
16
|
+
const email = args[0];
|
|
17
|
+
let plan = 'enterprise';
|
|
18
|
+
let months = 1;
|
|
19
|
+
let env = 'stage';
|
|
20
|
+
for (let i = 1; i < args.length; i++) {
|
|
21
|
+
if (args[i] === '--plan' && args[i + 1]) {
|
|
22
|
+
plan = args[++i];
|
|
23
|
+
}
|
|
24
|
+
else if (args[i] === '--months' && args[i + 1]) {
|
|
25
|
+
months = parseInt(args[++i], 10);
|
|
26
|
+
}
|
|
27
|
+
else if (args[i] === '--env' && args[i + 1]) {
|
|
28
|
+
env = args[++i];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (!['free', 'pro', 'enterprise'].includes(plan)) {
|
|
32
|
+
console.error(`Unknown plan: ${plan}. Available: free, pro, enterprise`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
if (months < 1) {
|
|
36
|
+
console.error('Months must be >= 1');
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
if (!['stage', 'prod'].includes(env)) {
|
|
40
|
+
console.error('Env must be stage or prod (billing DB not available in CI)');
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
return { email, plan, months, env };
|
|
44
|
+
}
|
|
45
|
+
async function main() {
|
|
46
|
+
const { email, plan, months, env } = parseArgs(process.argv.slice(2));
|
|
47
|
+
const infisicalConfig = (0, db_utils_1.getInfisicalConfig)();
|
|
48
|
+
const token = (0, db_utils_1.getInfisicalToken)(infisicalConfig);
|
|
49
|
+
console.log(`\n🎁 Granting ${plan} subscription to ${email} for ${months} month(s) [${env.toUpperCase()}]\n`);
|
|
50
|
+
const userId = await (0, db_utils_1.resolveUserId)(email, env, infisicalConfig, token);
|
|
51
|
+
const billing = await (0, db_utils_1.connectBillingDB)(env, infisicalConfig, token);
|
|
52
|
+
const bq = billing.query;
|
|
53
|
+
// Read plan config from DB
|
|
54
|
+
console.log(`Loading plan config: ${plan}`);
|
|
55
|
+
const planRow = bq(`SELECT name, monthly_credits, session_token_limit, weekly_token_limit FROM plans WHERE id='${(0, db_utils_1.escapeSQL)(plan)}'`);
|
|
56
|
+
if (!planRow) {
|
|
57
|
+
console.error(`❌ Plan not found in DB: ${plan}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
const [planName, monthlyCreditsStr, sessionTokenLimitStr, weeklyTokenLimitStr] = planRow.split('|');
|
|
61
|
+
const monthlyCredits = parseInt(monthlyCreditsStr, 10);
|
|
62
|
+
const sessionTokenLimit = parseInt(sessionTokenLimitStr, 10);
|
|
63
|
+
const weeklyTokenLimit = parseInt(weeklyTokenLimitStr, 10);
|
|
64
|
+
console.log(`✓ Plan: ${planName} (credits: ${monthlyCredits}, session: ${sessionTokenLimit.toLocaleString()}, weekly: ${weeklyTokenLimit.toLocaleString()})`);
|
|
65
|
+
// Execute all mutations in a single transaction
|
|
66
|
+
const now = new Date().toISOString();
|
|
67
|
+
const periodEnd = new Date();
|
|
68
|
+
periodEnd.setMonth(periodEnd.getMonth() + months);
|
|
69
|
+
const periodEndISO = periodEnd.toISOString();
|
|
70
|
+
const sessionEnd = new Date(new Date().getTime() + 5 * 60 * 60 * 1000).toISOString();
|
|
71
|
+
const weekEnd = new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
72
|
+
const safeUserId = (0, db_utils_1.escapeSQL)(userId);
|
|
73
|
+
const safePlan = (0, db_utils_1.escapeSQL)(plan);
|
|
74
|
+
const safePlanName = (0, db_utils_1.escapeSQL)(planName);
|
|
75
|
+
console.log('Executing transaction...');
|
|
76
|
+
const txSQL = `
|
|
77
|
+
BEGIN;
|
|
78
|
+
|
|
79
|
+
-- Cancel active subscriptions
|
|
80
|
+
UPDATE subscriptions SET status='canceled', canceled_at='${now}'
|
|
81
|
+
WHERE user_id='${safeUserId}' AND status IN ('active','trialing');
|
|
82
|
+
|
|
83
|
+
-- Zero out old subscription credits
|
|
84
|
+
UPDATE credit_ledger SET remaining=0
|
|
85
|
+
WHERE user_id='${safeUserId}' AND type IN ('monthly_grant','subscription') AND remaining > 0;
|
|
86
|
+
|
|
87
|
+
-- Create new subscription
|
|
88
|
+
INSERT INTO subscriptions (id, user_id, plan_id, status, billing_interval, current_period_start, current_period_end, created_at, updated_at)
|
|
89
|
+
VALUES (concat('sub_gift_', substr(md5(random()::text), 1, 16)), '${safeUserId}', '${safePlan}', 'active', 'monthly', '${now}', '${periodEndISO}', '${now}', '${now}');
|
|
90
|
+
|
|
91
|
+
-- Grant monthly credits
|
|
92
|
+
INSERT INTO credit_ledger (id, user_id, type, description, initial_amount, remaining, expires_at, created_at)
|
|
93
|
+
SELECT concat('crd_gift_', substr(md5(random()::text), 1, 16)), '${safeUserId}', 'subscription', '${safePlanName} plan gift (${months} month)', ${monthlyCredits}, ${monthlyCredits}, '${periodEndISO}', '${now}'
|
|
94
|
+
WHERE ${monthlyCredits} > 0;
|
|
95
|
+
|
|
96
|
+
-- Update existing active session quota, or insert new one if none exists
|
|
97
|
+
UPDATE token_quotas SET plan_id='${safePlan}', monthly_limit=${sessionTokenLimit}, updated_at='${now}'
|
|
98
|
+
WHERE user_id='${safeUserId}' AND period_type='session' AND period_end > '${now}';
|
|
99
|
+
|
|
100
|
+
INSERT INTO token_quotas (id, user_id, plan_id, period_type, monthly_limit, monthly_used, period_start, period_end, created_at, updated_at)
|
|
101
|
+
SELECT concat('tq_sess_', substr(md5(random()::text), 1, 16)), '${safeUserId}', '${safePlan}', 'session', ${sessionTokenLimit}, 0, '${now}', '${sessionEnd}', '${now}', '${now}'
|
|
102
|
+
WHERE NOT EXISTS (SELECT 1 FROM token_quotas WHERE user_id='${safeUserId}' AND period_type='session' AND period_end > '${now}');
|
|
103
|
+
|
|
104
|
+
-- Update existing active weekly quota, or insert new one if none exists
|
|
105
|
+
UPDATE token_quotas SET plan_id='${safePlan}', monthly_limit=${weeklyTokenLimit}, updated_at='${now}'
|
|
106
|
+
WHERE user_id='${safeUserId}' AND period_type='weekly' AND period_end > '${now}';
|
|
107
|
+
|
|
108
|
+
INSERT INTO token_quotas (id, user_id, plan_id, period_type, monthly_limit, monthly_used, period_start, period_end, created_at, updated_at)
|
|
109
|
+
SELECT concat('tq_week_', substr(md5(random()::text), 1, 16)), '${safeUserId}', '${safePlan}', 'weekly', ${weeklyTokenLimit}, 0, '${now}', '${weekEnd}', '${now}', '${now}'
|
|
110
|
+
WHERE NOT EXISTS (SELECT 1 FROM token_quotas WHERE user_id='${safeUserId}' AND period_type='weekly' AND period_end > '${now}');
|
|
111
|
+
|
|
112
|
+
COMMIT;
|
|
113
|
+
`.trim();
|
|
114
|
+
bq(txSQL);
|
|
115
|
+
console.log('✓ Old subscriptions canceled');
|
|
116
|
+
console.log('✓ Old credits cleared');
|
|
117
|
+
console.log(`✓ ${planName} subscription created (expires: ${periodEnd.toLocaleDateString()})`);
|
|
118
|
+
if (monthlyCredits > 0) {
|
|
119
|
+
console.log(`✓ ${monthlyCredits} credits granted`);
|
|
120
|
+
}
|
|
121
|
+
console.log(`✓ Token quotas updated (session: ${sessionTokenLimit.toLocaleString()}, weekly: ${weeklyTokenLimit.toLocaleString()})`);
|
|
122
|
+
console.log(`\n✅ Done! ${email} now has ${planName} plan until ${periodEnd.toLocaleDateString()}\n`);
|
|
123
|
+
}
|
|
124
|
+
main().catch(error => {
|
|
125
|
+
console.error('\n❌ Error:', error.message);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
});
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@optima-chat/dev-skills",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.23",
|
|
4
4
|
"description": "Claude Code Skills for Optima development team - cross-environment collaboration tools",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"optima-dev-skills": "./bin/cli.js",
|
|
8
8
|
"optima-query-db": "./dist/bin/helpers/query-db.js",
|
|
9
9
|
"optima-generate-test-token": "./dist/bin/helpers/generate-test-token.js",
|
|
10
|
-
"optima-show-env": "./dist/bin/helpers/show-env.js"
|
|
10
|
+
"optima-show-env": "./dist/bin/helpers/show-env.js",
|
|
11
|
+
"optima-grant-subscription": "./dist/bin/helpers/grant-subscription.js",
|
|
12
|
+
"optima-grant-credits": "./dist/bin/helpers/grant-credits.js"
|
|
11
13
|
},
|
|
12
14
|
"scripts": {
|
|
13
15
|
"postinstall": "node scripts/install.js",
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"WebSearch",
|
|
5
|
-
"WebFetch(domain:code.claude.com)",
|
|
6
|
-
"WebFetch(domain:platform.claude.com)",
|
|
7
|
-
"WebFetch(domain:github.com)",
|
|
8
|
-
"Bash(gh repo view:*)",
|
|
9
|
-
"Bash(gh repo clone:*)",
|
|
10
|
-
"Bash(gh repo list:*)",
|
|
11
|
-
"Read(//private/tmp/optima-docs/**)",
|
|
12
|
-
"Read(//tmp/optima-docs/**)",
|
|
13
|
-
"Bash(git init:*)",
|
|
14
|
-
"Bash(gh repo create:*)",
|
|
15
|
-
"Read(//private/tmp/optima-workspace/**)",
|
|
16
|
-
"Read(//tmp/optima-workspace/**)",
|
|
17
|
-
"Read(//tmp/optima-workspace/.claude/commands/**)",
|
|
18
|
-
"Bash(git add:*)",
|
|
19
|
-
"Bash(git push:*)",
|
|
20
|
-
"Bash(find:*)",
|
|
21
|
-
"Bash(git commit:*)",
|
|
22
|
-
"Bash(aws logs get-log-events:*)",
|
|
23
|
-
"Bash(npm install:*)",
|
|
24
|
-
"Bash(optima-dev-skills:*)",
|
|
25
|
-
"Bash(optima-generate-test-token:*)",
|
|
26
|
-
"Bash(optima-query-db:*)",
|
|
27
|
-
"Bash(gh variable set:*)",
|
|
28
|
-
"Bash(npm publish:*)",
|
|
29
|
-
"Bash(python3:*)",
|
|
30
|
-
"Bash(gh api:*)",
|
|
31
|
-
"Bash(curl -s http://auth.optima.chat/openapi.json)",
|
|
32
|
-
"Bash(curl -s https://auth.optima.chat/openapi.json)",
|
|
33
|
-
"Bash(cat:*)",
|
|
34
|
-
"Bash(node /Users/verypro/optima-dev-skills/scripts/install.js:*)",
|
|
35
|
-
"Bash(aws logs tail:*)",
|
|
36
|
-
"Bash(grep:*)",
|
|
37
|
-
"Bash(npm view:*)",
|
|
38
|
-
"Bash(npm version:*)",
|
|
39
|
-
"Bash(git checkout:*)",
|
|
40
|
-
"Bash(git pull:*)",
|
|
41
|
-
"Bash(node scripts/install.js:*)"
|
|
42
|
-
],
|
|
43
|
-
"deny": [],
|
|
44
|
-
"ask": []
|
|
45
|
-
}
|
|
46
|
-
}
|