@optima-chat/dev-skills 0.7.43 → 0.8.1
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/.claude/skills/grant-balance/SKILL.md +4 -2
- package/.claude/skills/grant-subscription/SKILL.md +5 -1
- package/.codex/skills/grant-balance/SKILL.md +2 -1
- package/.codex/skills/grant-subscription/SKILL.md +6 -2
- package/bin/cli.js +1 -1
- package/bin/helpers/billing-http.ts +55 -16
- package/bin/helpers/confirm-prompt.ts +8 -4
- package/bin/helpers/db-utils.ts +34 -12
- package/bin/helpers/discount/create.ts +3 -3
- package/bin/helpers/discount/disable.ts +3 -3
- package/bin/helpers/discount/generate.ts +3 -3
- package/bin/helpers/discount/list.ts +3 -3
- package/bin/helpers/entitlement/grant.ts +4 -7
- package/bin/helpers/entitlement/list.ts +4 -7
- package/bin/helpers/entitlement/revoke.ts +4 -7
- package/bin/helpers/generate-test-token.ts +28 -10
- package/bin/helpers/grant-balance.ts +6 -5
- package/bin/helpers/grant-subscription.ts +10 -8
- package/bin/helpers/query-db.ts +9 -6
- package/bin/helpers/show-env.ts +34 -15
- package/bin/helpers/verify-health.ts +10 -9
- package/dist/bin/helpers/billing-http.js +55 -14
- package/dist/bin/helpers/confirm-prompt.js +7 -4
- package/dist/bin/helpers/db-utils.js +34 -12
- package/dist/bin/helpers/discount/create.js +2 -2
- package/dist/bin/helpers/discount/disable.js +2 -2
- package/dist/bin/helpers/discount/generate.js +2 -2
- package/dist/bin/helpers/discount/list.js +2 -2
- package/dist/bin/helpers/entitlement/grant.js +3 -6
- package/dist/bin/helpers/entitlement/list.js +3 -6
- package/dist/bin/helpers/entitlement/revoke.js +3 -6
- package/dist/bin/helpers/generate-test-token.js +29 -9
- package/dist/bin/helpers/grant-balance.js +6 -5
- package/dist/bin/helpers/grant-subscription.js +10 -8
- package/dist/bin/helpers/query-db.js +9 -6
- package/dist/bin/helpers/show-env.js +31 -12
- package/dist/bin/helpers/verify-health.js +8 -7
- package/package.json +1 -1
|
@@ -60,7 +60,7 @@ export function assertPhoneMatch(inputPhone: string, accountPhone: string | null
|
|
|
60
60
|
*/
|
|
61
61
|
export function assertAwsEmailOnly(env: string, kind: 'email' | 'phone' | 'userId'): void {
|
|
62
62
|
if ((env === 'stage' || env === 'prod') && kind !== 'email') {
|
|
63
|
-
throw new Error('stage/prod 仅支持 email 标识;手机号/userId 解析仅 cn-prod 可用');
|
|
63
|
+
throw new Error('stage/prod 仅支持 email 标识;手机号/userId 解析仅 cn-prod / cn-stage 可用');
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
@@ -71,6 +71,8 @@ const PLANS_BY_ENV: Record<string, string[]> = {
|
|
|
71
71
|
stage: ['trial', 'starter', 'pro', 'enterprise'],
|
|
72
72
|
prod: ['trial', 'starter', 'pro', 'enterprise'],
|
|
73
73
|
'cn-prod': ['trial', 'starter-cn', 'pro-cn', 'enterprise-cn'],
|
|
74
|
+
// cn-stage 卖同一套 CNY -cn plan(billing plan 目录与 cn-prod 一致)
|
|
75
|
+
'cn-stage': ['trial', 'starter-cn', 'pro-cn', 'enterprise-cn'],
|
|
74
76
|
};
|
|
75
77
|
|
|
76
78
|
function parseArgs(args: string[]): { identifier: string; plan: string; months: number; env: string } {
|
|
@@ -79,9 +81,9 @@ function parseArgs(args: string[]): { identifier: string; plan: string; months:
|
|
|
79
81
|
|
|
80
82
|
Options:
|
|
81
83
|
--plan <id> Plan: trial, starter, pro, enterprise (default: pro)
|
|
82
|
-
cn-prod plans: trial, starter-cn, pro-cn, enterprise-cn (default: pro-cn)
|
|
84
|
+
cn-prod/cn-stage plans: trial, starter-cn, pro-cn, enterprise-cn (default: pro-cn)
|
|
83
85
|
--months <n> Duration in months (default: 1)
|
|
84
|
-
--env <env> Environment: stage, prod, cn-prod (default: stage)
|
|
86
|
+
--env <env> Environment: stage, prod, cn-prod, cn-stage (default: stage)
|
|
85
87
|
-h, --help Show this help`);
|
|
86
88
|
process.exit(0);
|
|
87
89
|
}
|
|
@@ -98,7 +100,7 @@ Options:
|
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
validateEnvCnProd(env);
|
|
101
|
-
plan = plan ?? (env === 'cn-prod' ? 'pro-cn' : 'pro');
|
|
103
|
+
plan = plan ?? (env === 'cn-prod' || env === 'cn-stage' ? 'pro-cn' : 'pro');
|
|
102
104
|
const allowed = PLANS_BY_ENV[env];
|
|
103
105
|
if (!allowed.includes(plan)) {
|
|
104
106
|
console.error(`Unknown plan for ${env}: ${plan}. Available: ${allowed.join(', ')}`);
|
|
@@ -118,13 +120,13 @@ async function main() {
|
|
|
118
120
|
|
|
119
121
|
// identity is the verified target account to print on success. AWS keeps the
|
|
120
122
|
// pre-change behavior (email-only, no internal HTTP reverse-verify); only
|
|
121
|
-
// cn-prod
|
|
123
|
+
// cn-prod / cn-stage run the full classify→resolve→getUserById→phone-assert防呆 path.
|
|
122
124
|
let userId: string;
|
|
123
125
|
let identity: { phone: string | null; email: string | null };
|
|
124
126
|
|
|
125
|
-
if (env === 'cn-prod') {
|
|
126
|
-
// cn-prod
|
|
127
|
-
// and
|
|
127
|
+
if (env === 'cn-prod' || env === 'cn-stage') {
|
|
128
|
+
// cn-prod / cn-stage have no SSH tunnel into the Aliyun RDS — resolve via
|
|
129
|
+
// user-auth, and the dev-skills token carries internal:users:write for lookups.
|
|
128
130
|
if (kind === 'userId') {
|
|
129
131
|
userId = identifier;
|
|
130
132
|
} else if (kind === 'phone') {
|
package/bin/helpers/query-db.ts
CHANGED
|
@@ -157,7 +157,7 @@ async function main() {
|
|
|
157
157
|
console.error('Usage: query-db.ts <service> <sql> [environment]');
|
|
158
158
|
console.error('');
|
|
159
159
|
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');
|
|
160
|
-
console.error('Environments: ci (default), stage, prod, cn (
|
|
160
|
+
console.error('Environments: ci (default), stage, prod, cn-prod (阿里云生产), cn-stage (阿里云预发)');
|
|
161
161
|
console.error('');
|
|
162
162
|
console.error('Example: query-db.ts user-auth "SELECT COUNT(*) FROM users" prod');
|
|
163
163
|
process.exit(1);
|
|
@@ -174,17 +174,20 @@ async function main() {
|
|
|
174
174
|
// cn-prod(阿里云):独立 Infisical + 经 buildbox 跳板连内网 RDS(动态端口隧道)。
|
|
175
175
|
// 两类 cred:① shared-secrets/database-users(按 prefix)② 服务自己的 DATABASE_URL(展开引用)。
|
|
176
176
|
if (isCnEnv(environment)) {
|
|
177
|
+
// cn-prod / cn-stage 共用 SERVICE_DB_MAP[*].prod 的 userKey/databaseUrlPath(prefix、
|
|
178
|
+
// /services/<svc> 路径两环境一致);connectCn* 按 env 切 cn Infisical 环境 + RDS 实例。
|
|
179
|
+
const label = environment === 'cn-stage' ? 'CN-STAGE' : 'CN-PROD';
|
|
177
180
|
const prodCfg = SERVICE_DB_MAP[service as keyof typeof SERVICE_DB_MAP].prod as any;
|
|
178
181
|
let db: { query: (sql: string) => string };
|
|
179
182
|
if (prodCfg?.userKey) {
|
|
180
183
|
const prefix = prodCfg.userKey.replace(/_DB_USER$/, '');
|
|
181
|
-
console.log(`\n🔍 Querying ${service} (
|
|
182
|
-
db = connectCnDB(prefix);
|
|
184
|
+
console.log(`\n🔍 Querying ${service} (${label}, prefix ${prefix})...`);
|
|
185
|
+
db = connectCnDB(prefix, environment);
|
|
183
186
|
} else if (prodCfg?.databaseUrlPath) {
|
|
184
|
-
console.log(`\n🔍 Querying ${service} (
|
|
185
|
-
db = connectCnDBFromUrl(prodCfg.databaseUrlPath);
|
|
187
|
+
console.log(`\n🔍 Querying ${service} (${label}, DATABASE_URL @ ${prodCfg.databaseUrlPath})...`);
|
|
188
|
+
db = connectCnDBFromUrl(prodCfg.databaseUrlPath, environment);
|
|
186
189
|
} else {
|
|
187
|
-
console.error(
|
|
190
|
+
console.error(`${label} query 暂不支持 ${service}(既无 userKey 也无 databaseUrlPath)。见 optima-dev-skills#21。`);
|
|
188
191
|
process.exit(1);
|
|
189
192
|
}
|
|
190
193
|
console.log('\n' + db.query(sql));
|
package/bin/helpers/show-env.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { execSync } from 'child_process';
|
|
4
|
-
import { getInfisicalConfig, getInfisicalToken, InfisicalConfig } from './db-utils';
|
|
4
|
+
import { getInfisicalConfig, getInfisicalToken, InfisicalConfig, isCnEnv, cnInfisicalEnv, getCnInfisicalToken, getCnSecrets } from './db-utils';
|
|
5
5
|
|
|
6
6
|
// 支持的服务列表(Infisical 路径为 /services/<service-name>)
|
|
7
7
|
const SUPPORTED_SERVICES = [
|
|
@@ -32,12 +32,17 @@ const SUPPORTED_SERVICES = [
|
|
|
32
32
|
'shopify-backend',
|
|
33
33
|
];
|
|
34
34
|
|
|
35
|
-
// 环境到 Infisical environment
|
|
35
|
+
// 环境到 AWS Infisical environment 的映射(cn 走独立 cn Infisical,见下方分支)
|
|
36
36
|
const ENV_MAP: Record<string, string> = {
|
|
37
37
|
stage: 'staging',
|
|
38
38
|
prod: 'prod'
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
+
/** 该 environment 是否被支持(AWS stage/prod 或阿里云 cn-prod/cn-stage)。 */
|
|
42
|
+
function isSupportedEnv(env: string): boolean {
|
|
43
|
+
return ENV_MAP[env] !== undefined || isCnEnv(env);
|
|
44
|
+
}
|
|
45
|
+
|
|
41
46
|
// NOTE: getInfisicalSecrets is kept local because it encodes secretPath (encodeURIComponent),
|
|
42
47
|
// unlike db-utils' raw-path variant; getGitHubVariable/getInfisicalConfig/getInfisicalToken are shared from db-utils.
|
|
43
48
|
function getInfisicalSecrets(config: InfisicalConfig, token: string, environment: string, secretPath: string): Record<string, string> {
|
|
@@ -60,17 +65,22 @@ Usage: optima-show-env <service> <environment> [options]
|
|
|
60
65
|
|
|
61
66
|
Arguments:
|
|
62
67
|
service Service name (${SUPPORTED_SERVICES.join(', ')})
|
|
63
|
-
environment Environment (stage, prod)
|
|
68
|
+
environment Environment (stage, prod, cn-prod, cn-stage)
|
|
64
69
|
|
|
65
70
|
Options:
|
|
66
71
|
--filter Filter by key pattern (e.g., --filter DATABASE)
|
|
67
72
|
--keys-only Show only key names, not values
|
|
68
73
|
--help Show this help message
|
|
69
74
|
|
|
75
|
+
Note:
|
|
76
|
+
cn-prod / cn-stage 读阿里云 cn Infisical(需 INFISICAL_CN_EMAIL/PASSWORD 环境变量,
|
|
77
|
+
admin user,见 optima-dev-skills#21)。stage/prod 读 AWS Infisical(GitHub Variables)。
|
|
78
|
+
|
|
70
79
|
Examples:
|
|
71
80
|
optima-show-env commerce-backend stage
|
|
72
81
|
optima-show-env user-auth prod --filter DATABASE
|
|
73
|
-
optima-show-env
|
|
82
|
+
optima-show-env gateway-core cn-stage
|
|
83
|
+
optima-show-env bi-backend stage --keys-only
|
|
74
84
|
`);
|
|
75
85
|
}
|
|
76
86
|
|
|
@@ -114,9 +124,9 @@ async function main() {
|
|
|
114
124
|
}
|
|
115
125
|
|
|
116
126
|
// 验证环境
|
|
117
|
-
if (!
|
|
127
|
+
if (!isSupportedEnv(environment)) {
|
|
118
128
|
console.error(`Error: Unknown environment '${environment}'`);
|
|
119
|
-
console.error('Available environments: stage, prod');
|
|
129
|
+
console.error('Available environments: stage, prod, cn-prod, cn-stage');
|
|
120
130
|
process.exit(1);
|
|
121
131
|
}
|
|
122
132
|
|
|
@@ -126,15 +136,24 @@ async function main() {
|
|
|
126
136
|
console.log(`\n🔍 Fetching environment variables for ${service} (${environment.toUpperCase()})...\n`);
|
|
127
137
|
|
|
128
138
|
try {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
139
|
+
let secrets: Record<string, string>;
|
|
140
|
+
if (isCnEnv(environment)) {
|
|
141
|
+
// 阿里云 cn Infisical(独立实例,admin email/password 认证;prod / staging 双环境)
|
|
142
|
+
const cnToken = getCnInfisicalToken();
|
|
143
|
+
console.log('✓ Obtained cn Infisical access token');
|
|
144
|
+
secrets = getCnSecrets(cnToken, secretPath, false, cnInfisicalEnv(environment));
|
|
145
|
+
console.log(`✓ Retrieved secrets from cn Infisical (env: ${cnInfisicalEnv(environment)}, path: ${secretPath})\n`);
|
|
146
|
+
} else {
|
|
147
|
+
const infisicalConfig = getInfisicalConfig();
|
|
148
|
+
console.log('✓ Loaded Infisical config from GitHub Variables');
|
|
149
|
+
|
|
150
|
+
const token = getInfisicalToken(infisicalConfig);
|
|
151
|
+
console.log('✓ Obtained Infisical access token');
|
|
152
|
+
|
|
153
|
+
const infisicalEnv = ENV_MAP[environment];
|
|
154
|
+
secrets = getInfisicalSecrets(infisicalConfig, token, infisicalEnv, secretPath);
|
|
155
|
+
console.log(`✓ Retrieved secrets from Infisical (path: ${secretPath})\n`);
|
|
156
|
+
}
|
|
138
157
|
|
|
139
158
|
const keys = Object.keys(secrets).sort();
|
|
140
159
|
|
|
@@ -29,22 +29,23 @@ import { promises as dns } from 'node:dns';
|
|
|
29
29
|
import * as tls from 'node:tls';
|
|
30
30
|
import * as https from 'node:https';
|
|
31
31
|
|
|
32
|
-
type Env = 'stage' | 'prod' | 'cn';
|
|
33
|
-
interface SvcCfg { path: string; stage?: string; prod?: string; cn?: string; cn_path?: string; }
|
|
32
|
+
type Env = 'stage' | 'prod' | 'cn' | 'cn-stage';
|
|
33
|
+
interface SvcCfg { path: string; stage?: string; prod?: string; cn?: string; cn_path?: string; 'cn-stage'?: string; 'cn-stage_path'?: string; }
|
|
34
34
|
|
|
35
35
|
// 服务 × 环境 FQDN 表。某服务某环境没部署 → 该 env 键缺省,探时跳过。
|
|
36
36
|
// cn-prod 真实 subdomain 抄自 optima-terraform alicloud/stacks/cn-prod-ingress-sae/main.tf。
|
|
37
37
|
// #201 (2026-06-12): yzsgo.com 全量迁移完成,旧 *-cn.optima.chat 路由已下线。
|
|
38
|
+
// cn-stage(阿里云预发)域名 *.stage.optima.chat,抄自 cn-stage-ingress-sae services map。
|
|
38
39
|
const SERVICES: Record<string, SvcCfg> = {
|
|
39
|
-
'user-auth': { path: '/health', stage: 'auth-stage.optima.onl', prod: 'auth.optima.onl', cn: 'auth.yzsgo.com' },
|
|
40
|
-
'agentic-chat': { path: '/api/health', stage: 'ai-stage.optima.onl', prod: 'ai.optima.onl', cn: 'app.yzsgo.com' },
|
|
41
|
-
'commerce-backend': { path: '/health', stage: 'api-stage.optima.onl', prod: 'api.optima.onl', cn: 'commerce.yzsgo.com', cn_path: '/health/live' },
|
|
40
|
+
'user-auth': { path: '/health', stage: 'auth-stage.optima.onl', prod: 'auth.optima.onl', cn: 'auth.yzsgo.com', 'cn-stage': 'auth.stage.optima.chat' },
|
|
41
|
+
'agentic-chat': { path: '/api/health', stage: 'ai-stage.optima.onl', prod: 'ai.optima.onl', cn: 'app.yzsgo.com', 'cn-stage': 'app.stage.optima.chat' },
|
|
42
|
+
'commerce-backend': { path: '/health', stage: 'api-stage.optima.onl', prod: 'api.optima.onl', cn: 'commerce.yzsgo.com', cn_path: '/health/live', 'cn-stage': 'commerce.stage.optima.chat', 'cn-stage_path': '/health/live' },
|
|
42
43
|
'mcp-host': { path: '/health', stage: 'mcp-stage.optima.onl', prod: 'mcp.optima.onl' },
|
|
43
|
-
'gateway-core': { path: '/health', cn: 'gw.yzsgo.com' },
|
|
44
|
-
'optima-scout': { path: '/health', cn: 'scout.yzsgo.com' },
|
|
45
|
-
'optima-skills': { path: '/health', cn: 'skills.yzsgo.com' },
|
|
44
|
+
'gateway-core': { path: '/health', cn: 'gw.yzsgo.com', 'cn-stage': 'gw.stage.optima.chat' },
|
|
45
|
+
'optima-scout': { path: '/health', cn: 'scout.yzsgo.com', 'cn-stage': 'scout.stage.optima.chat' },
|
|
46
|
+
'optima-skills': { path: '/health', cn: 'skills.yzsgo.com', 'cn-stage': 'skills.stage.optima.chat' },
|
|
46
47
|
};
|
|
47
|
-
const ENVS: Env[] = ['stage', 'prod', 'cn'];
|
|
48
|
+
const ENVS: Env[] = ['stage', 'prod', 'cn', 'cn-stage'];
|
|
48
49
|
|
|
49
50
|
const G = '\x1b[32m', R = '\x1b[31m', Y = '\x1b[33m', B = '\x1b[34m', N = '\x1b[0m';
|
|
50
51
|
const MARK: Record<string, string> = { ok: `${G}✅${N}`, fail: `${R}❌${N}`, warn: `${Y}⚠️ ${N}`, na: `${B}··${N}` };
|
|
@@ -6,6 +6,7 @@ exports.getServiceToken = getServiceToken;
|
|
|
6
6
|
exports.callBilling = callBilling;
|
|
7
7
|
exports.callSkills = callSkills;
|
|
8
8
|
exports.resolveUserIdByEmail = resolveUserIdByEmail;
|
|
9
|
+
exports.resolveUserIdByEmailAnyEnv = resolveUserIdByEmailAnyEnv;
|
|
9
10
|
exports.resolveUserIdByPhone = resolveUserIdByPhone;
|
|
10
11
|
exports.getUserById = getUserById;
|
|
11
12
|
const child_process_1 = require("child_process");
|
|
@@ -16,12 +17,15 @@ const USER_AUTH_URLS = {
|
|
|
16
17
|
prod: 'https://auth.optima.onl',
|
|
17
18
|
// #201: yzsgo.com 全量迁移 (2026-06-12), 旧 auth-cn.optima.chat 路由已下线
|
|
18
19
|
'cn-prod': 'https://auth.yzsgo.com',
|
|
20
|
+
// cn-stage(阿里云预发,独立于 AWS stage `.optima.onl`)
|
|
21
|
+
'cn-stage': 'https://auth.stage.optima.chat',
|
|
19
22
|
};
|
|
20
|
-
// cn
|
|
23
|
+
// cn URLs are hardcoded: cn Infisical (secrets-cn.optima.chat) is a
|
|
21
24
|
// separate instance dev-skills has no machine identity for, and these domains
|
|
22
25
|
// are stable. AWS envs keep reading /shared-secrets/domain-urls.
|
|
23
26
|
// #201: yzsgo.com 全量迁移 (2026-06-12), 旧 billing-cn.optima.chat 路由已下线
|
|
24
27
|
const CN_PROD_BILLING_URL = 'https://billing-api.yzsgo.com';
|
|
28
|
+
const CN_STAGE_BILLING_URL = 'https://billing-api.stage.optima.chat';
|
|
25
29
|
/**
|
|
26
30
|
* Validate the --env flag value at command entry, before any I/O.
|
|
27
31
|
*
|
|
@@ -46,8 +50,8 @@ function validateEnv(env) {
|
|
|
46
50
|
* cn-prod typo fails fast instead of dying inside the tunnel setup.
|
|
47
51
|
*/
|
|
48
52
|
function validateEnvCnProd(env) {
|
|
49
|
-
if (env !== 'stage' && env !== 'prod' && env !== 'cn-prod') {
|
|
50
|
-
throw new Error(`--env must be "stage", "prod" or "cn-
|
|
53
|
+
if (env !== 'stage' && env !== 'prod' && env !== 'cn-prod' && env !== 'cn-stage') {
|
|
54
|
+
throw new Error(`--env must be "stage", "prod", "cn-prod" or "cn-stage" (got: ${env})`);
|
|
51
55
|
}
|
|
52
56
|
return env;
|
|
53
57
|
}
|
|
@@ -86,6 +90,8 @@ const skillsUrlCache = {};
|
|
|
86
90
|
function getBillingUrl(env) {
|
|
87
91
|
if (env === 'cn-prod')
|
|
88
92
|
return CN_PROD_BILLING_URL;
|
|
93
|
+
if (env === 'cn-stage')
|
|
94
|
+
return CN_STAGE_BILLING_URL;
|
|
89
95
|
if (billingUrlCache[env])
|
|
90
96
|
return billingUrlCache[env];
|
|
91
97
|
const url = (0, infisical_secrets_1.fetchInfisicalSecret)(env, '/shared-secrets/domain-urls', 'BILLING_URL');
|
|
@@ -102,17 +108,34 @@ function getSkillsUrl(env) {
|
|
|
102
108
|
function getServiceToken(env) {
|
|
103
109
|
if (tokenCache[env])
|
|
104
110
|
return tokenCache[env];
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
//
|
|
108
|
-
// cn-
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
111
|
+
// cn-prod 与 cn-stage 都用 client_credentials M2M token,scope=internal:users:write。
|
|
112
|
+
// 凭证来源不同:
|
|
113
|
+
// · cn-prod —— 镜像在 AWS Infisical prod 环境(CN_PROD_ 前缀键),复用既有 AWS 访问。
|
|
114
|
+
// · cn-stage —— canonical 在 cn Infisical staging /shared-secrets/oauth-clients
|
|
115
|
+
// (dev-skills 无该 client 的 AWS 镜像;用 admin email/password 直读,
|
|
116
|
+
// 运行时需 INFISICAL_CN_EMAIL/PASSWORD,与 query-db cn 同一前提)。
|
|
117
|
+
const isCn = env === 'cn-prod' || env === 'cn-stage';
|
|
118
|
+
let clientId;
|
|
119
|
+
let clientSecret;
|
|
120
|
+
if (env === 'cn-stage') {
|
|
121
|
+
const cnTok = (0, db_utils_1.getCnInfisicalToken)();
|
|
122
|
+
const oc = (0, db_utils_1.getCnSecrets)(cnTok, '/shared-secrets/oauth-clients', false, 'staging');
|
|
123
|
+
clientId = oc['DEV_SKILLS_OAUTH_CLIENT_ID'];
|
|
124
|
+
clientSecret = oc['DEV_SKILLS_OAUTH_CLIENT_SECRET'];
|
|
125
|
+
if (!clientId || !clientSecret) {
|
|
126
|
+
throw new Error('cn-stage dev-skills OAuth client 凭证缺失(cn Infisical staging /shared-secrets/oauth-clients 的 DEV_SKILLS_OAUTH_CLIENT_ID/SECRET)。');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
const cfg = (0, db_utils_1.getInfisicalConfig)();
|
|
131
|
+
const tok = (0, db_utils_1.getInfisicalToken)(cfg);
|
|
132
|
+
// Fetch BOTH client_id and client_secret from Infisical — they differ per env.
|
|
133
|
+
const infisicalEnv = env === 'cn-prod' ? 'prod' : env;
|
|
134
|
+
const idKey = env === 'cn-prod' ? DEV_SKILLS_CN_CLIENT_ID_KEY : DEV_SKILLS_CLIENT_ID_KEY;
|
|
135
|
+
const secretKey = env === 'cn-prod' ? DEV_SKILLS_CN_CLIENT_SECRET_KEY : DEV_SKILLS_CLIENT_SECRET_KEY;
|
|
136
|
+
clientId = (0, infisical_secrets_1.fetchInfisicalSecret)(infisicalEnv, DEV_SKILLS_OAUTH_PATH, idKey, cfg, tok);
|
|
137
|
+
clientSecret = (0, infisical_secrets_1.fetchInfisicalSecret)(infisicalEnv, DEV_SKILLS_OAUTH_PATH, secretKey, cfg, tok);
|
|
138
|
+
}
|
|
116
139
|
const authUrl = USER_AUTH_URLS[env];
|
|
117
140
|
if (!authUrl)
|
|
118
141
|
throw new Error(`Unknown env: ${env}`);
|
|
@@ -234,6 +257,24 @@ async function resolveUserIdByEmail(env, email) {
|
|
|
234
257
|
console.log(`✓ Found user: ${parsed.user_id}`);
|
|
235
258
|
return parsed.user_id;
|
|
236
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* Resolve email → userId across all envs, picking the path that exists for
|
|
262
|
+
* each cloud. AWS (stage/prod) reaches the database over the RDS SSH tunnel
|
|
263
|
+
* (db-utils.resolveUserId). cn-prod / cn-stage have no tunnel into the Aliyun
|
|
264
|
+
* VPC-internal RDS, so they resolve via user-auth's internal HTTP lookup
|
|
265
|
+
* (resolveUserIdByEmail) — the cn dev-skills token carries the
|
|
266
|
+
* internal:users:write scope that endpoint requires. Lets the entitlement
|
|
267
|
+
* subcommands support cn the same way grant-subscription already does, without
|
|
268
|
+
* each call site re-implementing the branch.
|
|
269
|
+
*/
|
|
270
|
+
async function resolveUserIdByEmailAnyEnv(env, email) {
|
|
271
|
+
if (env === 'cn-prod' || env === 'cn-stage') {
|
|
272
|
+
return resolveUserIdByEmail(env, email);
|
|
273
|
+
}
|
|
274
|
+
const cfg = (0, db_utils_1.getInfisicalConfig)();
|
|
275
|
+
const token = (0, db_utils_1.getInfisicalToken)(cfg);
|
|
276
|
+
return (0, db_utils_1.resolveUserId)(email, env, cfg, token);
|
|
277
|
+
}
|
|
237
278
|
/**
|
|
238
279
|
* Resolve a user's id by phone via user-auth's internal lookup endpoint
|
|
239
280
|
* (POST /api/v1/internal/users/lookup with {phone}). Mirrors
|
|
@@ -36,13 +36,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.confirmIfProd = confirmIfProd;
|
|
37
37
|
const readline = __importStar(require("readline"));
|
|
38
38
|
/**
|
|
39
|
-
* On
|
|
40
|
-
*
|
|
39
|
+
* On a production env, print the resolved action and require typing "yes" to
|
|
40
|
+
* proceed. Both AWS `prod` and Aliyun `cn-prod` are production — cn-prod must
|
|
41
|
+
* gate identically or the cn rollout silently loses the prod safety prompt.
|
|
42
|
+
* No-op on stage / cn-stage or when --yes was passed. Exits 1 if user declines.
|
|
41
43
|
*/
|
|
44
|
+
const PROD_ENVS = new Set(['prod', 'cn-prod']);
|
|
42
45
|
async function confirmIfProd(env, actionDescription, skipFlag) {
|
|
43
|
-
if (env
|
|
46
|
+
if (!PROD_ENVS.has(env) || skipFlag)
|
|
44
47
|
return;
|
|
45
|
-
console.log(`\n⚠️ About to perform on
|
|
48
|
+
console.log(`\n⚠️ About to perform on ${env.toUpperCase()}:\n${actionDescription}\n`);
|
|
46
49
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
47
50
|
const answer = await new Promise((resolve) => {
|
|
48
51
|
rl.question('Type "yes" to confirm: ', (a) => { rl.close(); resolve(a.trim()); });
|
|
@@ -35,6 +35,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.RDS_HOSTS = void 0;
|
|
37
37
|
exports.isCnEnv = isCnEnv;
|
|
38
|
+
exports.isCnStageEnv = isCnStageEnv;
|
|
39
|
+
exports.cnInfisicalEnv = cnInfisicalEnv;
|
|
40
|
+
exports.cnRdsHost = cnRdsHost;
|
|
38
41
|
exports.escapeSQL = escapeSQL;
|
|
39
42
|
exports.getGitHubVariable = getGitHubVariable;
|
|
40
43
|
exports.getInfisicalConfig = getInfisicalConfig;
|
|
@@ -67,14 +70,29 @@ const EC2_HOST = '3.0.210.113';
|
|
|
67
70
|
// ─── cn-prod (阿里云) 常量 ────────────────────────────────────────────────────
|
|
68
71
|
// cn-prod 跑在阿里云 SAE,与海外 AWS 完全独立:独立 Infisical (secrets-cn.optima.chat)
|
|
69
72
|
// + 内网 RDS(无公网端点)经 buildbox ECS 跳板 SSH 隧道访问。设计见 optima-dev-skills#21。
|
|
73
|
+
// cn Infisical 是【单实例双环境】:prod=cn-prod、staging=cn-stage(URL/org/project 共享,
|
|
74
|
+
// 只切 environment 参数)。cn-stage 数据面共用 prod VPC,故走【同一 buildbox】跳板。
|
|
70
75
|
const CN_INFISICAL_URL = process.env.INFISICAL_CN_URL || 'https://secrets-cn.optima.chat';
|
|
71
76
|
const CN_INFISICAL_ORG = process.env.INFISICAL_CN_ORG || 'f44012fa-0659-4f7e-b0ac-ed01244efc8a';
|
|
72
77
|
const CN_INFISICAL_PROJECT = process.env.INFISICAL_CN_PROJECT || '4deef229-11be-40a5-8f56-b61f0bce7240';
|
|
73
|
-
const CN_RDS_HOST = 'pgm-2zexwx9eso9e4yla.pg.rds.aliyuncs.com';
|
|
78
|
+
const CN_RDS_HOST = 'pgm-2zexwx9eso9e4yla.pg.rds.aliyuncs.com'; // cn-prod RDS
|
|
79
|
+
const CN_STAGE_RDS_HOST = 'pgm-2zem1u9zdh06boim.pg.rds.aliyuncs.com'; // cn-stage RDS(独立实例,共用 prod VPC)
|
|
74
80
|
const CN_BUILDBOX_HOST = process.env.OPTIMA_CN_BUILDBOX_HOST || '47.94.105.163';
|
|
75
|
-
/** env
|
|
81
|
+
/** env 是否指向阿里云(cn-prod 或 cn-stage)。接受 `cn` / `cn-prod` / `cn-stage`。 */
|
|
76
82
|
function isCnEnv(env) {
|
|
77
|
-
return env === 'cn' || env === 'cn-prod';
|
|
83
|
+
return env === 'cn' || env === 'cn-prod' || env === 'cn-stage';
|
|
84
|
+
}
|
|
85
|
+
/** env 是否指向 cn-stage(阿里云预发)。 */
|
|
86
|
+
function isCnStageEnv(env) {
|
|
87
|
+
return env === 'cn-stage';
|
|
88
|
+
}
|
|
89
|
+
/** cn env → cn Infisical environment slug。cn-stage→staging,其余 cn→prod。 */
|
|
90
|
+
function cnInfisicalEnv(env) {
|
|
91
|
+
return env === 'cn-stage' ? 'staging' : 'prod';
|
|
92
|
+
}
|
|
93
|
+
/** cn env → 内网 RDS host。cn-stage→stage 实例,其余 cn→prod 实例。 */
|
|
94
|
+
function cnRdsHost(env) {
|
|
95
|
+
return env === 'cn-stage' ? CN_STAGE_RDS_HOST : CN_RDS_HOST;
|
|
78
96
|
}
|
|
79
97
|
// ─── SQL escaping ───────────────────────────────────────────────────────────
|
|
80
98
|
/** Escape a string value for safe inclusion in SQL single-quoted literals. */
|
|
@@ -146,10 +164,13 @@ function getCnInfisicalToken() {
|
|
|
146
164
|
throw new Error(`cn Infisical select-organization 失败: ${JSON.stringify(org).slice(0, 200)}`);
|
|
147
165
|
return org.token;
|
|
148
166
|
}
|
|
149
|
-
/**
|
|
150
|
-
|
|
167
|
+
/**
|
|
168
|
+
* 读 cn Infisical 某 folder → key→value map。expand=true 让服务端解析 `${...}` 引用。
|
|
169
|
+
* environment:cn-prod 用 `prod`、cn-stage 用 `staging`(同一实例双环境)。
|
|
170
|
+
*/
|
|
171
|
+
function getCnSecrets(token, secretPath, expand = false, environment = 'prod') {
|
|
151
172
|
const data = curlJson([
|
|
152
|
-
`${CN_INFISICAL_URL}/api/v3/secrets/raw?workspaceId=${CN_INFISICAL_PROJECT}&environment
|
|
173
|
+
`${CN_INFISICAL_URL}/api/v3/secrets/raw?workspaceId=${CN_INFISICAL_PROJECT}&environment=${environment}&secretPath=${encodeURIComponent(secretPath)}&expandSecretReferences=${expand}`,
|
|
153
174
|
'-H', `Authorization: Bearer ${token}`,
|
|
154
175
|
]);
|
|
155
176
|
const secrets = {};
|
|
@@ -460,17 +481,18 @@ function queryDB(conn, sql) {
|
|
|
460
481
|
* 连 cn-prod 某服务库:creds 取自 cn Infisical 的 /shared-secrets/database-users +
|
|
461
482
|
* /database-names(按 prefix,如 AUTH/BILLING),经 buildbox 跳板隧道(动态端口)。
|
|
462
483
|
*/
|
|
463
|
-
function connectCnDB(prefix) {
|
|
484
|
+
function connectCnDB(prefix, cnEnv = 'cn-prod') {
|
|
485
|
+
const infEnv = cnInfisicalEnv(cnEnv);
|
|
464
486
|
const token = getCnInfisicalToken();
|
|
465
|
-
const users = getCnSecrets(token, '/shared-secrets/database-users');
|
|
466
|
-
const names = getCnSecrets(token, '/shared-secrets/database-names');
|
|
487
|
+
const users = getCnSecrets(token, '/shared-secrets/database-users', false, infEnv);
|
|
488
|
+
const names = getCnSecrets(token, '/shared-secrets/database-names', false, infEnv);
|
|
467
489
|
const user = users[`${prefix}_DB_USER`];
|
|
468
490
|
const password = users[`${prefix}_DB_PASSWORD`];
|
|
469
491
|
const database = names[`${prefix}_DB_NAME`];
|
|
470
492
|
if (!user || !password || !database) {
|
|
471
493
|
throw new Error(`cn DB creds 不全 for ${prefix}(需 ${prefix}_DB_USER/PASSWORD@database-users + ${prefix}_DB_NAME@database-names)。该服务 cn 可能用字面 DATABASE_URL,见 optima-dev-skills#21。`);
|
|
472
494
|
}
|
|
473
|
-
const port = ensureTunnel(
|
|
495
|
+
const port = ensureTunnel(cnRdsHost(cnEnv), 'cn-buildbox');
|
|
474
496
|
const conn = { host: 'localhost', port, user, password, database };
|
|
475
497
|
return { query: (sql) => queryDB(conn, sql) };
|
|
476
498
|
}
|
|
@@ -479,9 +501,9 @@ function connectCnDB(prefix) {
|
|
|
479
501
|
* 用于 cred 不在 shared-secrets/database-users 的服务(如 gateway-core)。
|
|
480
502
|
* expandSecretReferences=true 让 cn Infisical 解析 `${...}` 引用为字面值。
|
|
481
503
|
*/
|
|
482
|
-
function connectCnDBFromUrl(servicePath) {
|
|
504
|
+
function connectCnDBFromUrl(servicePath, cnEnv = 'cn-prod') {
|
|
483
505
|
const token = getCnInfisicalToken();
|
|
484
|
-
const secrets = getCnSecrets(token, servicePath, true);
|
|
506
|
+
const secrets = getCnSecrets(token, servicePath, true, cnInfisicalEnv(cnEnv));
|
|
485
507
|
const dbUrl = secrets['DATABASE_URL'];
|
|
486
508
|
if (!dbUrl)
|
|
487
509
|
throw new Error(`cn DATABASE_URL 未找到 at ${servicePath}`);
|
|
@@ -24,7 +24,7 @@ Optional:
|
|
|
24
24
|
--ends <date> Valid-until
|
|
25
25
|
--max <N> Max total redemptions (default: unlimited; 1 = single-use)
|
|
26
26
|
--campaign <label> Grouping label
|
|
27
|
-
--env stage|prod
|
|
27
|
+
--env stage|prod|cn-prod|cn-stage (default: stage)
|
|
28
28
|
--yes Skip prod confirmation`);
|
|
29
29
|
process.exit(0);
|
|
30
30
|
}
|
|
@@ -81,7 +81,7 @@ Optional:
|
|
|
81
81
|
}
|
|
82
82
|
async function runCreate(argv) {
|
|
83
83
|
const args = parseArgs(argv);
|
|
84
|
-
(0, billing_http_1.
|
|
84
|
+
(0, billing_http_1.validateEnvCnProd)(args.env);
|
|
85
85
|
await (0, confirm_prompt_1.confirmIfProd)(args.env, `Create discount code ${args.code.toUpperCase()} (${args.percentOff}% off)`, args.yes);
|
|
86
86
|
const body = { code: args.code, percentOff: args.percentOff };
|
|
87
87
|
if (args.productKeys)
|
|
@@ -11,7 +11,7 @@ Required:
|
|
|
11
11
|
--code <CODE>
|
|
12
12
|
|
|
13
13
|
Optional:
|
|
14
|
-
--env stage|prod
|
|
14
|
+
--env stage|prod|cn-prod|cn-stage (default: stage)
|
|
15
15
|
--yes Skip prod confirmation`);
|
|
16
16
|
process.exit(0);
|
|
17
17
|
}
|
|
@@ -40,7 +40,7 @@ Optional:
|
|
|
40
40
|
}
|
|
41
41
|
async function runDisable(argv) {
|
|
42
42
|
const args = parseArgs(argv);
|
|
43
|
-
(0, billing_http_1.
|
|
43
|
+
(0, billing_http_1.validateEnvCnProd)(args.env);
|
|
44
44
|
await (0, confirm_prompt_1.confirmIfProd)(args.env, `Disable discount code ${args.code.toUpperCase()}`, args.yes);
|
|
45
45
|
const res = await (0, billing_http_1.callBilling)(args.env, 'PATCH', `/api/billing/admin/discount-codes/${encodeURIComponent(args.code)}`, { status: 'DISABLED' });
|
|
46
46
|
console.log(`✓ Disabled (HTTP ${res.status}):`);
|
|
@@ -59,7 +59,7 @@ Optional:
|
|
|
59
59
|
--products <a,b,...> Limit to these productKeys
|
|
60
60
|
--starts <date> Valid-from (YYYY-MM-DD or ISO)
|
|
61
61
|
--ends <date> Valid-until
|
|
62
|
-
--env stage|prod
|
|
62
|
+
--env stage|prod|cn-prod|cn-stage (default: stage)
|
|
63
63
|
--yes Skip prod confirmation`);
|
|
64
64
|
process.exit(0);
|
|
65
65
|
}
|
|
@@ -114,7 +114,7 @@ Optional:
|
|
|
114
114
|
}
|
|
115
115
|
async function runGenerate(argv) {
|
|
116
116
|
const args = parseArgs(argv);
|
|
117
|
-
(0, billing_http_1.
|
|
117
|
+
(0, billing_http_1.validateEnvCnProd)(args.env);
|
|
118
118
|
await (0, confirm_prompt_1.confirmIfProd)(args.env, `Generate ${args.count} unique discount codes (${args.percentOff}% off, campaign ${args.campaign})`, args.yes);
|
|
119
119
|
const body = { count: args.count, percentOff: args.percentOff, campaign: args.campaign };
|
|
120
120
|
if (args.productKeys)
|
|
@@ -10,7 +10,7 @@ Optional:
|
|
|
10
10
|
--campaign <label> Filter by campaign
|
|
11
11
|
--code <CODE> Filter by exact code
|
|
12
12
|
--limit <N> Max rows (default 500, max 1000)
|
|
13
|
-
--env stage|prod
|
|
13
|
+
--env stage|prod|cn-prod|cn-stage (default: stage)`);
|
|
14
14
|
process.exit(0);
|
|
15
15
|
}
|
|
16
16
|
const out = { env: 'stage' };
|
|
@@ -45,7 +45,7 @@ Optional:
|
|
|
45
45
|
}
|
|
46
46
|
async function runList(argv) {
|
|
47
47
|
const args = parseArgs(argv);
|
|
48
|
-
(0, billing_http_1.
|
|
48
|
+
(0, billing_http_1.validateEnvCnProd)(args.env);
|
|
49
49
|
const qs = new URLSearchParams();
|
|
50
50
|
if (args.campaign)
|
|
51
51
|
qs.set('campaign', args.campaign);
|
|
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.runGrant = runGrant;
|
|
4
4
|
const billing_http_1 = require("../billing-http");
|
|
5
5
|
const confirm_prompt_1 = require("../confirm-prompt");
|
|
6
|
-
const db_utils_1 = require("../db-utils");
|
|
7
6
|
function parseArgs(argv) {
|
|
8
7
|
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
9
8
|
console.log(`Usage: optima-entitlement grant --email <user> --product-key <slug> --justification "..." [options]
|
|
@@ -15,7 +14,7 @@ Required:
|
|
|
15
14
|
|
|
16
15
|
Optional:
|
|
17
16
|
--yes Skip prod confirmation prompt (no-op on stage)
|
|
18
|
-
--env stage|prod
|
|
17
|
+
--env stage|prod|cn-prod|cn-stage (default: stage)
|
|
19
18
|
|
|
20
19
|
Hardcoded server-side: source=ADMIN_GRANT, priceCents=0, currency=USD, grantedBy=<clientId>.`);
|
|
21
20
|
process.exit(0);
|
|
@@ -57,10 +56,8 @@ Hardcoded server-side: source=ADMIN_GRANT, priceCents=0, currency=USD, grantedBy
|
|
|
57
56
|
}
|
|
58
57
|
async function runGrant(argv) {
|
|
59
58
|
const args = parseArgs(argv);
|
|
60
|
-
(0, billing_http_1.
|
|
61
|
-
const
|
|
62
|
-
const token = (0, db_utils_1.getInfisicalToken)(cfg);
|
|
63
|
-
const userId = await (0, db_utils_1.resolveUserId)(args.email, args.env, cfg, token);
|
|
59
|
+
(0, billing_http_1.validateEnvCnProd)(args.env);
|
|
60
|
+
const userId = await (0, billing_http_1.resolveUserIdByEmailAnyEnv)(args.env, args.email);
|
|
64
61
|
await (0, confirm_prompt_1.confirmIfProd)(args.env, `Action: GRANT product '${args.productKey}' to user ${args.email} (userId=${userId}) on ${args.env.toUpperCase()}\nJustification: ${args.justification}`, args.yes);
|
|
65
62
|
console.log(`\n🎁 Granting ${args.productKey} to ${args.email}...`);
|
|
66
63
|
const res = await (0, billing_http_1.callBilling)(args.env, 'POST', '/api/billing/admin/grant-entitlement', {
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.runList = runList;
|
|
4
4
|
const billing_http_1 = require("../billing-http");
|
|
5
|
-
const db_utils_1 = require("../db-utils");
|
|
6
5
|
function parseArgs(argv) {
|
|
7
6
|
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
8
7
|
console.log(`Usage: optima-entitlement list --email <user-email> [options]
|
|
@@ -11,7 +10,7 @@ Required:
|
|
|
11
10
|
--email <user-email> Resolved to userId via user-auth DB
|
|
12
11
|
|
|
13
12
|
Optional:
|
|
14
|
-
--env stage|prod
|
|
13
|
+
--env stage|prod|cn-prod|cn-stage (default: stage)`);
|
|
15
14
|
process.exit(0);
|
|
16
15
|
}
|
|
17
16
|
const out = { env: 'stage' };
|
|
@@ -36,10 +35,8 @@ Optional:
|
|
|
36
35
|
}
|
|
37
36
|
async function runList(argv) {
|
|
38
37
|
const args = parseArgs(argv);
|
|
39
|
-
(0, billing_http_1.
|
|
40
|
-
const
|
|
41
|
-
const token = (0, db_utils_1.getInfisicalToken)(cfg);
|
|
42
|
-
const userId = await (0, db_utils_1.resolveUserId)(args.email, args.env, cfg, token);
|
|
38
|
+
(0, billing_http_1.validateEnvCnProd)(args.env);
|
|
39
|
+
const userId = await (0, billing_http_1.resolveUserIdByEmailAnyEnv)(args.env, args.email);
|
|
43
40
|
const res = await (0, billing_http_1.callBilling)(args.env, 'GET', `/api/billing/admin/entitlements?userId=${encodeURIComponent(userId)}`);
|
|
44
41
|
const rows = res.body.entitlements ?? [];
|
|
45
42
|
if (rows.length === 0) {
|
|
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.runRevoke = runRevoke;
|
|
4
4
|
const billing_http_1 = require("../billing-http");
|
|
5
5
|
const confirm_prompt_1 = require("../confirm-prompt");
|
|
6
|
-
const db_utils_1 = require("../db-utils");
|
|
7
6
|
function parseArgs(argv) {
|
|
8
7
|
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
9
8
|
console.log(`Usage: optima-entitlement revoke --email <user> --product-key <slug> --reason "..." [options]
|
|
@@ -15,7 +14,7 @@ Required:
|
|
|
15
14
|
|
|
16
15
|
Optional:
|
|
17
16
|
--yes Skip prod confirmation prompt (no-op on stage)
|
|
18
|
-
--env stage|prod
|
|
17
|
+
--env stage|prod|cn-prod|cn-stage (default: stage)
|
|
19
18
|
|
|
20
19
|
Refuses non-ADMIN_GRANT sources (PAYMENT, PARTNER) with source-specific
|
|
21
20
|
error pointing to the right reversal flow.`);
|
|
@@ -60,10 +59,8 @@ const PAYMENT_REFUSAL = `refusing to revoke a PAYMENT-source entitlement via CLI
|
|
|
60
59
|
const PARTNER_REFUSAL = `refusing to revoke a PARTNER-source entitlement via CLI; PARTNER grants are issued out-of-band and must be reversed via the partner contract / process that issued them. Manual psql is the escape hatch if absolutely necessary.`;
|
|
61
60
|
async function runRevoke(argv) {
|
|
62
61
|
const args = parseArgs(argv);
|
|
63
|
-
(0, billing_http_1.
|
|
64
|
-
const
|
|
65
|
-
const token = (0, db_utils_1.getInfisicalToken)(cfg);
|
|
66
|
-
const userId = await (0, db_utils_1.resolveUserId)(args.email, args.env, cfg, token);
|
|
62
|
+
(0, billing_http_1.validateEnvCnProd)(args.env);
|
|
63
|
+
const userId = await (0, billing_http_1.resolveUserIdByEmailAnyEnv)(args.env, args.email);
|
|
67
64
|
// Step 1: Fetch user's entitlements
|
|
68
65
|
const listRes = await (0, billing_http_1.callBilling)(args.env, 'GET', `/api/billing/admin/entitlements?userId=${encodeURIComponent(userId)}`);
|
|
69
66
|
const all = listRes.body.entitlements ?? [];
|