@optima-chat/dev-skills 0.7.38 → 0.7.40

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.
@@ -1,20 +1,33 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
+ // P15 D8b:USD 钱包退役——原「SSH 直写 subscriptions/usd_wallets/token_quotas」
5
+ // 作废,改调 billing 服务态端点(与用户态 /api/admin/grant-subscription 同
6
+ // 业务体:supersede 旧授予 + 实得 credits + token quota,重试双发不双倍)。
4
7
  const db_utils_1 = require("./db-utils");
8
+ const billing_http_1 = require("./billing-http");
9
+ // cn-prod sells the CNY-priced -cn plans (P12); the bare USD plan ids also
10
+ // exist in the cn DB, so a per-env whitelist (not billing-side validation)
11
+ // is what prevents accidentally granting a USD-priced plan to a CN user.
12
+ const PLANS_BY_ENV = {
13
+ stage: ['trial', 'starter', 'pro', 'enterprise'],
14
+ prod: ['trial', 'starter', 'pro', 'enterprise'],
15
+ 'cn-prod': ['trial', 'starter-cn', 'pro-cn', 'enterprise-cn'],
16
+ };
5
17
  function parseArgs(args) {
6
18
  if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
7
19
  console.log(`Usage: optima-grant-subscription <email> [options]
8
20
 
9
21
  Options:
10
22
  --plan <id> Plan: trial, starter, pro, enterprise (default: pro)
23
+ cn-prod plans: trial, starter-cn, pro-cn, enterprise-cn (default: pro-cn)
11
24
  --months <n> Duration in months (default: 1)
12
- --env <env> Environment: stage, prod (default: stage)
25
+ --env <env> Environment: stage, prod, cn-prod (default: stage)
13
26
  -h, --help Show this help`);
14
27
  process.exit(0);
15
28
  }
16
29
  const email = args[0];
17
- let plan = 'pro';
30
+ let plan = null;
18
31
  let months = 1;
19
32
  let env = 'stage';
20
33
  for (let i = 1; i < args.length; i++) {
@@ -28,103 +41,40 @@ Options:
28
41
  env = args[++i];
29
42
  }
30
43
  }
31
- if (!['trial', 'starter', 'pro', 'enterprise'].includes(plan)) {
32
- console.error(`Unknown plan: ${plan}. Available: trial, starter, pro, enterprise`);
44
+ (0, billing_http_1.validateEnvCnProd)(env);
45
+ plan = plan ?? (env === 'cn-prod' ? 'pro-cn' : 'pro');
46
+ const allowed = PLANS_BY_ENV[env];
47
+ if (!allowed.includes(plan)) {
48
+ console.error(`Unknown plan for ${env}: ${plan}. Available: ${allowed.join(', ')}`);
33
49
  process.exit(1);
34
50
  }
35
51
  if (months < 1) {
36
52
  console.error('Months must be >= 1');
37
53
  process.exit(1);
38
54
  }
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
55
  return { email, plan, months, env };
44
56
  }
45
57
  async function main() {
46
58
  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
59
  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);
60
+ // cn-prod has no SSH tunnel into the Aliyun RDS — resolve via user-auth's
61
+ // internal lookup API instead of the direct SQL path.
62
+ let userId;
63
+ if (env === 'cn-prod') {
64
+ userId = await (0, billing_http_1.resolveUserIdByEmail)(env, email);
59
65
  }
60
- const [planName, monthlyCreditsStr, sessionTokenLimitStr, weeklyTokenLimitStr] = planRow.split('|');
61
- const monthlyCredits = parseInt(monthlyCreditsStr, 10);
62
- const grantMicros = monthlyCredits * 10000; // 1 credit = $0.01 = 10,000 micros
63
- const sessionTokenLimit = parseInt(sessionTokenLimitStr, 10);
64
- const weeklyTokenLimit = parseInt(weeklyTokenLimitStr, 10);
65
- console.log(`✓ Plan: ${planName} (grant: $${(grantMicros / 1000000).toFixed(2)}, session: ${sessionTokenLimit.toLocaleString()}, weekly: ${weeklyTokenLimit.toLocaleString()})`);
66
- // Execute all mutations in a single transaction
67
- const now = new Date().toISOString();
68
- const periodEnd = new Date();
69
- periodEnd.setMonth(periodEnd.getMonth() + months);
70
- const periodEndISO = periodEnd.toISOString();
71
- const sessionEnd = new Date(new Date().getTime() + 5 * 60 * 60 * 1000).toISOString();
72
- const weekEnd = new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000).toISOString();
73
- const safeUserId = (0, db_utils_1.escapeSQL)(userId);
74
- const safePlan = (0, db_utils_1.escapeSQL)(plan);
75
- const safePlanName = (0, db_utils_1.escapeSQL)(planName);
76
- console.log('Executing transaction...');
77
- const txSQL = `
78
- BEGIN;
79
-
80
- -- Cancel active subscriptions
81
- UPDATE subscriptions SET status='canceled', canceled_at='${now}'
82
- WHERE user_id='${safeUserId}' AND status IN ('active','trialing');
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
- -- Ensure wallet exists (upsert)
89
- INSERT INTO usd_wallets (id, user_id, balance_micros, reserved_micros, granted_balance_micros, created_at, updated_at)
90
- VALUES (gen_random_uuid(), '${safeUserId}', 0, 0, 0, '${now}', '${now}')
91
- ON CONFLICT (user_id) DO NOTHING;
92
-
93
- -- Reset granted balance and set new grant
94
- UPDATE usd_wallets SET granted_balance_micros = ${grantMicros}, updated_at = '${now}'
95
- WHERE user_id = '${safeUserId}';
96
-
97
- -- Record topup for audit trail
98
- INSERT INTO usd_wallet_topups (id, wallet_id, amount_micros, service_fee_micros, net_credit_micros, status, source, service_namespace, created_at, completed_at)
99
- SELECT gen_random_uuid(), w.id, ${grantMicros}, 0, ${grantMicros}, 'completed', 'subscription_grant', 'platform', '${now}', '${now}'
100
- FROM usd_wallets w WHERE w.user_id = '${safeUserId}';
101
-
102
- -- Update existing active session quota, or insert new one if none exists
103
- UPDATE token_quotas SET plan_id='${safePlan}', monthly_limit=${sessionTokenLimit}, updated_at='${now}'
104
- WHERE user_id='${safeUserId}' AND period_type='session' AND period_end > '${now}';
105
-
106
- INSERT INTO token_quotas (id, user_id, plan_id, period_type, monthly_limit, monthly_used, period_start, period_end, created_at, updated_at)
107
- SELECT concat('tq_sess_', substr(md5(random()::text), 1, 16)), '${safeUserId}', '${safePlan}', 'session', ${sessionTokenLimit}, 0, '${now}', '${sessionEnd}', '${now}', '${now}'
108
- WHERE NOT EXISTS (SELECT 1 FROM token_quotas WHERE user_id='${safeUserId}' AND period_type='session' AND period_end > '${now}');
109
-
110
- -- Update existing active weekly quota, or insert new one if none exists
111
- UPDATE token_quotas SET plan_id='${safePlan}', monthly_limit=${weeklyTokenLimit}, updated_at='${now}'
112
- WHERE user_id='${safeUserId}' AND period_type='weekly' AND period_end > '${now}';
113
-
114
- INSERT INTO token_quotas (id, user_id, plan_id, period_type, monthly_limit, monthly_used, period_start, period_end, created_at, updated_at)
115
- SELECT concat('tq_week_', substr(md5(random()::text), 1, 16)), '${safeUserId}', '${safePlan}', 'weekly', ${weeklyTokenLimit}, 0, '${now}', '${weekEnd}', '${now}', '${now}'
116
- WHERE NOT EXISTS (SELECT 1 FROM token_quotas WHERE user_id='${safeUserId}' AND period_type='weekly' AND period_end > '${now}');
117
-
118
- COMMIT;
119
- `.trim();
120
- bq(txSQL);
121
- console.log('✓ Old subscriptions canceled');
122
- console.log(`✓ ${planName} subscription created (expires: ${periodEnd.toLocaleDateString()})`);
123
- if (grantMicros > 0) {
124
- console.log(`✓ Wallet granted $${(grantMicros / 1000000).toFixed(2)} (${monthlyCredits} credits equivalent)`);
66
+ else {
67
+ const infisicalConfig = (0, db_utils_1.getInfisicalConfig)();
68
+ const token = (0, db_utils_1.getInfisicalToken)(infisicalConfig);
69
+ userId = await (0, db_utils_1.resolveUserId)(email, env, infisicalConfig, token);
125
70
  }
126
- console.log(`✓ Token quotas updated (session: ${sessionTokenLimit.toLocaleString()}, weekly: ${weeklyTokenLimit.toLocaleString()})`);
127
- console.log(`\n✅ Done! ${email} now has ${planName} plan until ${periodEnd.toLocaleDateString()}\n`);
71
+ const { body } = await (0, billing_http_1.callBilling)(env, 'POST', '/api/billing/admin/grant-subscription', { userId, planId: plan, months });
72
+ console.log(`✓ Subscription ${body.subscriptionId} (${body.planId})`);
73
+ console.log(`✓ Credits: ${body.credits.toLocaleString()} (expires ${body.expiresAt})`);
74
+ console.log(`✓ Token limits: session ${body.sessionTokenLimit.toLocaleString()} / weekly ${body.weeklyTokenLimit.toLocaleString()}`);
75
+ if (body.warning)
76
+ console.log(`⚠️ ${body.warning}`);
77
+ console.log(`\n✅ Done! ${email} now has ${body.planId} until ${body.expiresAt}\n`);
128
78
  }
129
79
  main().catch(error => {
130
80
  console.error('\n❌ Error:', error.message);
File without changes
File without changes
@@ -114,44 +114,8 @@ const RDS_HOSTS = {
114
114
  stage: 'optima-stage-postgres.ctg866o0ehac.ap-southeast-1.rds.amazonaws.com',
115
115
  prod: 'optima-prod-postgres.ctg866o0ehac.ap-southeast-1.rds.amazonaws.com'
116
116
  };
117
- function parseDatabaseUrl(url) {
118
- // postgresql://user:password@host:port/database?params
119
- const match = url.match(/^postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/([^?]+)/);
120
- if (!match) {
121
- throw new Error(`Failed to parse DATABASE_URL: ${url}`);
122
- }
123
- return {
124
- user: decodeURIComponent(match[1]),
125
- password: decodeURIComponent(match[2]),
126
- host: match[3],
127
- port: parseInt(match[4]),
128
- database: match[5]
129
- };
130
- }
131
- function getGitHubVariable(name) {
132
- return (0, child_process_1.execSync)(`gh variable get ${name} -R Optima-Chat/optima-dev-skills`, { encoding: 'utf-8' }).trim();
133
- }
134
- function getInfisicalConfig() {
135
- return {
136
- url: getGitHubVariable('INFISICAL_URL'),
137
- clientId: getGitHubVariable('INFISICAL_CLIENT_ID'),
138
- clientSecret: getGitHubVariable('INFISICAL_CLIENT_SECRET'),
139
- projectId: getGitHubVariable('INFISICAL_PROJECT_ID')
140
- };
141
- }
142
- function getInfisicalToken(config) {
143
- 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' });
144
- return JSON.parse(response).accessToken;
145
- }
146
- function getInfisicalSecrets(config, token, environment, secretPath) {
147
- 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' });
148
- const data = JSON.parse(response);
149
- const secrets = {};
150
- for (const secret of data.secrets || []) {
151
- secrets[secret.secretKey] = secret.secretValue;
152
- }
153
- return secrets;
154
- }
117
+ // parseDatabaseUrl 统一用 db-utils 的实现:右切 userinfo 容忍密码特殊字符,
118
+ // 且报错不回显 URL(这里曾把含密码的完整 URL 打进错误信息)。
155
119
  function findPsqlPath() {
156
120
  // 1. 优先从 PATH 中查找
157
121
  const whichCmd = process.platform === 'win32' ? 'where psql' : 'which psql';
@@ -206,7 +170,7 @@ async function main() {
206
170
  console.error('Usage: query-db.ts <service> <sql> [environment]');
207
171
  console.error('');
208
172
  console.error('Services: commerce-backend, user-auth, agentic-chat, bi-backend, session-gateway, gateway-core, optima-logistics, billing, ads-backend, amazon-backend, browser-backend, shopify-backend, optima-generation, optima-sentinel');
209
- console.error('Environments: ci (default), stage, prod');
173
+ console.error('Environments: ci (default), stage, prod, cn (阿里云 cn-prod)');
210
174
  console.error('');
211
175
  console.error('Example: query-db.ts user-auth "SELECT COUNT(*) FROM users" prod');
212
176
  process.exit(1);
@@ -217,6 +181,27 @@ async function main() {
217
181
  console.error('Available services:', Object.keys(SERVICE_DB_MAP).join(', '));
218
182
  process.exit(1);
219
183
  }
184
+ // cn-prod(阿里云):独立 Infisical + 经 buildbox 跳板连内网 RDS(动态端口隧道)。
185
+ // 两类 cred:① shared-secrets/database-users(按 prefix)② 服务自己的 DATABASE_URL(展开引用)。
186
+ if ((0, db_utils_1.isCnEnv)(environment)) {
187
+ const prodCfg = SERVICE_DB_MAP[service].prod;
188
+ let db;
189
+ if (prodCfg?.userKey) {
190
+ const prefix = prodCfg.userKey.replace(/_DB_USER$/, '');
191
+ console.log(`\n🔍 Querying ${service} (CN-PROD, prefix ${prefix})...`);
192
+ db = (0, db_utils_1.connectCnDB)(prefix);
193
+ }
194
+ else if (prodCfg?.databaseUrlPath) {
195
+ console.log(`\n🔍 Querying ${service} (CN-PROD, DATABASE_URL @ ${prodCfg.databaseUrlPath})...`);
196
+ db = (0, db_utils_1.connectCnDBFromUrl)(prodCfg.databaseUrlPath);
197
+ }
198
+ else {
199
+ console.error(`cn-prod query 暂不支持 ${service}(既无 userKey 也无 databaseUrlPath)。见 optima-dev-skills#21。`);
200
+ process.exit(1);
201
+ }
202
+ console.log('\n' + db.query(sql));
203
+ return;
204
+ }
220
205
  const serviceConfig = SERVICE_DB_MAP[service][environment];
221
206
  if (!serviceConfig) {
222
207
  console.error(`Service ${service} is not available in ${environment.toUpperCase()} environment.`);
@@ -228,18 +213,18 @@ async function main() {
228
213
  console.log(`\n🔍 Querying ${service} (${environment.toUpperCase()})...`);
229
214
  if (environment === 'ci') {
230
215
  // CI 环境:通过 SSH + Docker Exec
231
- const ciUser = getGitHubVariable('CI_SSH_USER');
232
- const ciHost = getGitHubVariable('CI_SSH_HOST');
233
- const ciPassword = getGitHubVariable('CI_SSH_PASSWORD');
216
+ const ciUser = (0, db_utils_1.getGitHubVariable)('CI_SSH_USER');
217
+ const ciHost = (0, db_utils_1.getGitHubVariable)('CI_SSH_HOST');
218
+ const ciPassword = (0, db_utils_1.getGitHubVariable)('CI_SSH_PASSWORD');
234
219
  const { container, user, database } = serviceConfig;
235
220
  const result = (0, child_process_1.execSync)(`sshpass -p "${ciPassword}" ssh -o StrictHostKeyChecking=no ${ciUser}@${ciHost} "docker exec ${container} psql -U ${user} -d ${database} -c \\"${sql}\\""`, { encoding: 'utf-8' });
236
221
  console.log('\n' + result);
237
222
  }
238
223
  else {
239
224
  // Stage/Prod 环境:通过 SSH 隧道访问 RDS
240
- const infisicalConfig = getInfisicalConfig();
225
+ const infisicalConfig = (0, db_utils_1.getInfisicalConfig)();
241
226
  console.log('✓ Loaded Infisical config from GitHub Variables');
242
- const token = getInfisicalToken(infisicalConfig);
227
+ const token = (0, db_utils_1.getInfisicalToken)(infisicalConfig);
243
228
  console.log('✓ Obtained Infisical access token');
244
229
  const infisicalEnv = environment === 'stage' ? 'staging' : 'prod';
245
230
  let dbUser;
@@ -249,13 +234,13 @@ async function main() {
249
234
  if ('databaseUrlPath' in serviceConfig) {
250
235
  // 从服务路径获取 DATABASE_URL 并解析
251
236
  const { databaseUrlPath, databaseUrlKey } = serviceConfig;
252
- const secrets = getInfisicalSecrets(infisicalConfig, token, infisicalEnv, databaseUrlPath);
237
+ const secrets = (0, db_utils_1.getInfisicalSecrets)(infisicalConfig, token, infisicalEnv, databaseUrlPath);
253
238
  console.log(`✓ Retrieved DATABASE_URL from Infisical (path: ${databaseUrlPath})`);
254
239
  const databaseUrl = secrets[databaseUrlKey];
255
240
  if (!databaseUrl) {
256
241
  throw new Error(`DATABASE_URL not found in Infisical at ${databaseUrlPath}`);
257
242
  }
258
- const parsed = parseDatabaseUrl(databaseUrl);
243
+ const parsed = (0, db_utils_1.parseDatabaseUrl)(databaseUrl);
259
244
  dbUser = parsed.user;
260
245
  dbPassword = parsed.password;
261
246
  dbHost = parsed.host;
@@ -263,7 +248,7 @@ async function main() {
263
248
  }
264
249
  else {
265
250
  // 从 shared-secrets/database-users 获取凭证
266
- const secrets = getInfisicalSecrets(infisicalConfig, token, infisicalEnv, '/shared-secrets/database-users');
251
+ const secrets = (0, db_utils_1.getInfisicalSecrets)(infisicalConfig, token, infisicalEnv, '/shared-secrets/database-users');
267
252
  console.log('✓ Retrieved database credentials from Infisical');
268
253
  const { userKey, passwordKey } = serviceConfig;
269
254
  database = serviceConfig.database;
@@ -274,8 +259,7 @@ async function main() {
274
259
  throw new Error(`Database credentials not found in Infisical for ${service}. Keys: ${userKey}, ${passwordKey}`);
275
260
  }
276
261
  }
277
- const localPort = environment === 'stage' ? 15432 : 15433;
278
- (0, db_utils_1.setupTunnel)(dbHost, localPort);
262
+ const localPort = (0, db_utils_1.ensureTunnel)(dbHost);
279
263
  const result = queryDatabase('localhost', localPort, dbUser, dbPassword, database, sql);
280
264
  console.log('\n' + result);
281
265
  }
@@ -2,6 +2,7 @@
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const child_process_1 = require("child_process");
5
+ const db_utils_1 = require("./db-utils");
5
6
  // 支持的服务列表(Infisical 路径为 /services/<service-name>)
6
7
  const SUPPORTED_SERVICES = [
7
8
  'ads-backend',
@@ -35,21 +36,8 @@ const ENV_MAP = {
35
36
  stage: 'staging',
36
37
  prod: 'prod'
37
38
  };
38
- function getGitHubVariable(name) {
39
- return (0, child_process_1.execSync)(`gh variable get ${name} -R Optima-Chat/optima-dev-skills`, { encoding: 'utf-8' }).trim();
40
- }
41
- function getInfisicalConfig() {
42
- return {
43
- url: getGitHubVariable('INFISICAL_URL'),
44
- clientId: getGitHubVariable('INFISICAL_CLIENT_ID'),
45
- clientSecret: getGitHubVariable('INFISICAL_CLIENT_SECRET'),
46
- projectId: getGitHubVariable('INFISICAL_PROJECT_ID')
47
- };
48
- }
49
- function getInfisicalToken(config) {
50
- 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' });
51
- return JSON.parse(response).accessToken;
52
- }
39
+ // NOTE: getInfisicalSecrets is kept local because it encodes secretPath (encodeURIComponent),
40
+ // unlike db-utils' raw-path variant; getGitHubVariable/getInfisicalConfig/getInfisicalToken are shared from db-utils.
53
41
  function getInfisicalSecrets(config, token, environment, secretPath) {
54
42
  const response = (0, child_process_1.execSync)(`curl -s "${config.url}/api/v3/secrets/raw?workspaceId=${config.projectId}&environment=${environment}&secretPath=${encodeURIComponent(secretPath)}" -H "Authorization: Bearer ${token}"`, { encoding: 'utf-8' });
55
43
  const data = JSON.parse(response);
@@ -122,9 +110,9 @@ async function main() {
122
110
  const secretPath = `/services/${service}`;
123
111
  console.log(`\n🔍 Fetching environment variables for ${service} (${environment.toUpperCase()})...\n`);
124
112
  try {
125
- const infisicalConfig = getInfisicalConfig();
113
+ const infisicalConfig = (0, db_utils_1.getInfisicalConfig)();
126
114
  console.log('✓ Loaded Infisical config from GitHub Variables');
127
- const token = getInfisicalToken(infisicalConfig);
115
+ const token = (0, db_utils_1.getInfisicalToken)(infisicalConfig);
128
116
  console.log('✓ Obtained Infisical access token');
129
117
  const infisicalEnv = ENV_MAP[environment];
130
118
  const secrets = getInfisicalSecrets(infisicalConfig, token, infisicalEnv, secretPath);
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optima-chat/dev-skills",
3
- "version": "0.7.38",
3
+ "version": "0.7.40",
4
4
  "description": "Claude Code Skills for Optima development team - cross-environment collaboration tools",
5
5
  "main": "index.js",
6
6
  "bin": {