@optima-chat/dev-skills 0.7.37 → 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.
@@ -50,6 +50,7 @@
50
50
  - `ci` - CI 持续集成环境(开发环境,默认)
51
51
  - `stage` - Stage 预发布环境(ECS Fargate)
52
52
  - `prod` - 生产环境(ECS Fargate)
53
+ - `cn-prod`(别名 `cn`)- 阿里云 cn-prod 环境(SAE,cn-beijing,经 buildbox 取日志)
53
54
 
54
55
  ## 示例
55
56
 
@@ -71,6 +72,7 @@
71
72
  - `ci` 或未指定 → 使用 SSH + Docker Compose(第 0 节,默认)
72
73
  - `stage` → 使用 AWS CloudWatch Logs - ECS(第 1 节)
73
74
  - `prod` → 使用 AWS CloudWatch Logs - ECS(第 2 节)
75
+ - `cn-prod` / `cn` → 阿里云 SAE,经 buildbox 调 `DescribeInstanceLog`(第 3 节)
74
76
 
75
77
  ### 0. CI 环境(environment = "ci" 或默认)
76
78
 
@@ -209,6 +211,59 @@ aws logs get-log-events --log-group-name /ecs/commerce-backend-prod --log-stream
209
211
 
210
212
  **注意**: `optima-store` 仅在 Stage 环境部署
211
213
 
214
+ ### 3. cn-prod 环境(environment = "cn-prod" 或 "cn")
215
+
216
+ **部署方式**: 阿里云 **SAE**(Serverless App Engine,cn-beijing),不是 ECS/CloudWatch。
217
+ **访问方式**: 经 buildbox(`root@47.94.105.163`,唯一装了 `aliyun-optima` profile + VPC 可达的机器)调 SAE OpenAPI。
218
+
219
+ > ⚠️ SAE **未配 SLS**,stdout 日志只能用 `DescribeInstanceLog` 取实例**当前缓冲**(非历史检索;重启/滚动后旧日志丢)。要历史检索需给 SAE 配 SLS。
220
+
221
+ **日志链**: service → AppId → GroupId → InstanceId → `DescribeInstanceLog`
222
+
223
+ **关键坑**(实测):
224
+ - API 是 RPC-style `aliyun sae DescribeInstanceLog`,ROA 路径是 `/pop/v1/sam/**instance**/describeInstanceLog`(不是 `/sam/app/...`,写错报 "can not find api by path")。
225
+ - buildbox 密码在 1P `op://Optima/gdmixcizjci5bvuu3nuvgg4ooe/password`(标题带括号必须用 item ID)。
226
+
227
+ **service → AppId 映射**(cn-prod,2026-06-01;新增/变动用 `aliyun sae ListApplications` 重查):
228
+
229
+ | service | AppId |
230
+ |---|---|
231
+ | `gateway-core` | `a08ce23f-3d3e-4d89-a2cf-53c8adba614e` |
232
+ | `agentic-chat` | `6e290c73-a646-43ef-9da5-ad0b2e7eff73` |
233
+ | `gw-admin` | `c6bc5a78-b27f-46e2-825a-eaa338c23645` |
234
+ | `user-auth` | `d6fbf9de-fed6-4165-978f-7b3a0a456acc` |
235
+ | `user-auth-admin` | `54093f99-37bf-4354-abd7-f6e76abdbcb6` |
236
+ | `optima-scout` | `f5bb7e82-e57c-4bd4-83ae-2c25f8d38647` |
237
+ | `optima-skills` | `55a63ee7-fb75-46e4-99f3-20854a699237` |
238
+ | `infisical` | `52b33fc6-b26d-495a-8885-225d38b3d42f` |
239
+
240
+ **完整脚本**(本地跑、经 buildbox 中转;改 `SERVICE`/`LINES`):
241
+ ```bash
242
+ SERVICE=gateway-core; LINES=80
243
+ BBPW=$(op-win read "op://Optima/gdmixcizjci5bvuu3nuvgg4ooe/password")
244
+ declare -A APPID=(
245
+ [gateway-core]=a08ce23f-3d3e-4d89-a2cf-53c8adba614e
246
+ [agentic-chat]=6e290c73-a646-43ef-9da5-ad0b2e7eff73
247
+ [gw-admin]=c6bc5a78-b27f-46e2-825a-eaa338c23645
248
+ [user-auth]=d6fbf9de-fed6-4165-978f-7b3a0a456acc
249
+ [user-auth-admin]=54093f99-37bf-4354-abd7-f6e76abdbcb6
250
+ [optima-scout]=f5bb7e82-e57c-4bd4-83ae-2c25f8d38647
251
+ [optima-skills]=55a63ee7-fb75-46e4-99f3-20854a699237
252
+ [infisical]=52b33fc6-b26d-495a-8885-225d38b3d42f )
253
+ AID=${APPID[$SERVICE]}
254
+ sshpass -p "$BBPW" ssh -o StrictHostKeyChecking=no root@47.94.105.163 "bash -s" <<REMOTE | tail -n $LINES
255
+ P="--region cn-beijing --profile aliyun-optima"
256
+ GID=\$(aliyun sae GET /pop/v1/sam/app/describeApplicationGroups --AppId $AID \$P 2>/dev/null | python3 -c "import sys,json;print(json.load(sys.stdin)['Data'][0]['GroupId'])")
257
+ IID=\$(aliyun sae GET /pop/v1/sam/app/describeApplicationInstances --AppId $AID --GroupId \$GID \$P 2>/dev/null | python3 -c "import sys,json;d=json.load(sys.stdin)['Data'];print((d if isinstance(d,list) else d['Instances'])[0]['InstanceId'])")
258
+ aliyun sae DescribeInstanceLog --InstanceId "\$IID" \$P 2>/dev/null | python3 -c "import sys,json;print(json.load(sys.stdin).get('Data',''))"
259
+ REMOTE
260
+ ```
261
+
262
+ **局限 / 提示**:
263
+ - 只取第 1 个 InstanceId;多副本要遍历 `Data` 里全部 instance。
264
+ - `agent-runtime` 不在 SAE(是 ECI 动态容器)——它的日志看 `gateway-core` 里 `eci-bridge` 的转发/错误行,或单独查对应 ECI。
265
+ - 过滤:把上面输出 `| grep -iE 'error|eci|fail'` 即可(例:排 agentic-chat 链路 ECI 失败就 grep `gateway-core` 日志的 `eci-bridge`)。
266
+
212
267
  ## 完整示例脚本
213
268
 
214
269
  ### Stage 环境
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  name: "grant-balance"
3
- description: "当用户请求赠送余额、充值 USD 余额、grant balance、加余额、奖励、补偿、推荐奖励、运营发放时,使用此技能。支持 Stage、Prod 两个环境。"
3
+ description: "当用户请求赠送余额、充值 USD 余额、grant balance、加余额、奖励、补偿、推荐奖励、运营发放时,使用此技能。支持 Stage、Prod、cn-prod 三个环境。"
4
4
  allowed-tools: ["Bash"]
5
5
  ---
6
6
 
7
7
  # 赠送 USD 余额(Grant Balance)
8
8
 
9
- 当你需要为用户赠送 wallet USD 余额时,使用这个场景。金额会加到 `usd_wallets.granted_balance_micros`,billing 服务在扣费时会优先消费 granted balance。
9
+ 当你需要为用户赠送 wallet USD 余额时,使用这个场景。金额按 $1=700 积分换算,经 billing API 入 **bonus 积分桶(30 天有效期)**(P15 钱包退役后语义),billing 服务在扣费时会优先消费 granted balance。
10
10
 
11
11
  ## 执行方式:使用 CLI 工具
12
12
 
@@ -17,8 +17,8 @@ optima-grant-balance <email> --amount <usd> [options]
17
17
  **为什么使用 CLI 工具**:
18
18
  - 自动通过 email 查找 userId(跨 user-auth 数据库)
19
19
  - 自动处理 SSH 隧道和数据库连接
20
- - 不会影响现有订阅和已有余额(纯追加到 granted balance)
21
- - 自动写入 audit trail(`usd_wallet_topups` source=admin_grant)
20
+ - 不会影响现有订阅和已有余额(发放 bonus 积分(30 天有效期,重复执行会叠加发放))
21
+ - 自动留审计痕迹(credit_lot 行,幂等键前缀 `dev-skills-grant:`)
22
22
 
23
23
  ## 适用情况
24
24
 
@@ -41,7 +41,7 @@ optima-grant-balance user@example.com --amount 20 --description "服务中断补
41
41
  ```
42
42
 
43
43
  > **单位是美元(USD)**。`--amount 5` 即赠送 $5.00 到 granted balance。
44
- > 数据库底层用 micro-USD 精度(1 USD = 1,000,000 micros),CLI 自动换算。
44
+ > 数据库底层用 micro-USD 精度(1 USD = 700 积分(P15 统一账本口径)
45
45
 
46
46
  ### 参数说明
47
47
 
@@ -50,7 +50,9 @@ optima-grant-balance user@example.com --amount 20 --description "服务中断补
50
50
  | `<email>` | 用户邮箱(必填) | - |
51
51
  | `--amount <usd>` | USD 金额(必填,> 0) | - |
52
52
  | `--description <text>` | 描述/原因(仅 console 输出) | - |
53
- | `--env <env>` | 环境:stage, prod | stage |
53
+ | `--env <env>` | 环境:stage, prod, cn-prod | stage |
54
+
55
+ > **cn-prod(国内环境)**:全程 HTTPS(auth-cn/billing-cn.optima.chat),email 查找走 user-auth internal lookup API(无 SSH 隧道)。金额输入仍是 USD($1 = 700 积分,与 CN ¥1 = 100 积分同一账本单位)。例:`optima-grant-balance user@example.com --amount 1 --env cn-prod`
54
56
 
55
57
  ## 与 grant-subscription 的区别
56
58
 
@@ -93,10 +95,10 @@ optima-grant-balance xxx@gmail.com --amount 20 --env stage
93
95
  1. **Stage 优先**:默认操作 Stage 环境
94
96
  2. **Prod 谨慎**:操作 Prod 前确认邮箱和金额
95
97
  3. **纯追加**:不会影响现有余额和订阅(累加到 granted balance)
96
- 4. **Audit trail**:每次赠送会插入一条 `usd_wallet_topups` 记录(source=`admin_grant`)
98
+ 4. **Audit trail**:每次赠送对应一个 credit_lot(幂等键前缀 `dev-skills-grant:`,type=bonus)
97
99
 
98
100
  ## 相关命令
99
101
 
100
102
  - `optima-grant-balance` - 赠送 USD 余额(主要方式)
101
103
  - `optima-grant-subscription` - 开通订阅计划
102
- - `optima-query-db` - 查询数据库验证结果(`SELECT granted_balance_micros FROM usd_wallets WHERE user_id=...`)
104
+ - `optima-query-db` - 查询数据库验证结果(`GET /api/billing/balance(credits.byType.bonus)或 query-db credit_lot
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: "grant-subscription"
3
- description: "当用户请求开通会员、赠送订阅、grant subscription、开通 Pro/Starter/Enterprise、升级计划、给用户开会员时,使用此技能。支持 Stage、Prod 两个环境。"
3
+ description: "当用户请求开通会员、赠送订阅、grant subscription、开通 Pro/Starter/Enterprise、升级计划、给用户开会员时,使用此技能。支持 Stage、Prod、cn-prod 三个环境。"
4
4
  allowed-tools: ["Bash"]
5
5
  ---
6
6
 
@@ -19,8 +19,8 @@ optima-grant-subscription <email> [options]
19
19
  **为什么使用 CLI 工具**:
20
20
  - 自动通过 email 查找 userId(跨 user-auth 数据库)
21
21
  - 自动处理 SSH 隧道和数据库连接
22
- - 自动取消旧订阅、重置 wallet granted balance
23
- - 自动按 plan 配置授予 USD wallet 余额和 token quota
22
+ - 自动取消旧订阅、经 billing API supersede 旧授予(void 旧 subscription lot + 发新)
23
+ - 自动按 plan 配置发放 subscription 积分(期末过期)和 token quota
24
24
  - 一条命令完成所有操作
25
25
 
26
26
  ## 适用情况
@@ -46,6 +46,10 @@ optima-grant-subscription user@example.com --plan starter --months 3
46
46
 
47
47
  # 开通 Enterprise,Prod 环境
48
48
  optima-grant-subscription user@example.com --plan enterprise --env prod
49
+
50
+ # cn-prod(国内环境):plan 用 -cn 档,默认 pro-cn
51
+ optima-grant-subscription user@example.com --env cn-prod
52
+ optima-grant-subscription user@example.com --plan starter-cn --env cn-prod
49
53
  ```
50
54
 
51
55
  ### 参数说明
@@ -55,7 +59,9 @@ optima-grant-subscription user@example.com --plan enterprise --env prod
55
59
  | `<email>` | 用户邮箱(必填) | - |
56
60
  | `--plan <id>` | 计划:trial, starter, pro, enterprise | pro |
57
61
  | `--months <n>` | 时长(月) | 1 |
58
- | `--env <env>` | 环境:stage, prod | stage |
62
+ | `--env <env>` | 环境:stage, prod, cn-prod | stage |
63
+
64
+ > **cn-prod(国内环境)**:plan 用 CNY 定价的 `-cn` 档(trial / starter-cn / pro-cn / enterprise-cn,默认 pro-cn);裸 USD 档 id 会被客户端拒绝(防止给 CN 用户误赠 USD 档积分量)。email 查找走 user-auth internal lookup API(无 SSH 隧道)。
59
65
 
60
66
  ### 计划配置
61
67
 
@@ -66,7 +72,7 @@ optima-grant-subscription user@example.com --plan enterprise --env prod
66
72
  | pro | $20.00 | 2,000 | 8M | 40M |
67
73
  | enterprise | $100.00 | 10,000 | 16M | 80M |
68
74
 
69
- > 1 credit = $0.01 = 10,000 micros。授予额存入 `usd_wallets.granted_balance_micros`。
75
+ > 积分经 billing API 发放(subscription 桶,期末过期;P15 后无 wallet)
70
76
 
71
77
  ## 常见使用场景
72
78
 
@@ -102,7 +108,7 @@ optima-grant-subscription user@example.com --plan starter --env prod
102
108
  2. 加载对应 plan 的配置(月授予额、token 限额等)
103
109
  3. 取消该用户的所有活跃订阅
104
110
  4. 创建新订阅(设置到期时间)
105
- 5. 重置 wallet granted balance 并授予新额度
111
+ 5. billing API supersede 旧授予(void 旧 subscription lot + 发新) 并授予新额度
106
112
  6. 记录 topup 审计记录(source: subscription_grant)
107
113
  7. 更新 token quota 限额
108
114
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: "logs"
3
- description: "当用户请求查看日志、查看服务日志、排查问题、看看日志、检查日志、商品服务日志、后端日志、API日志、正式环境日志、生产环境日志、CI环境日志、开发环境日志时,使用此技能。支持 CI、Stage、Prod 三个环境的 commerce-backend、user-auth、agentic-chat、bi-backendsession-gateway 等服务。"
3
+ description: "当用户请求查看日志、查看服务日志、排查问题、看看日志、检查日志、商品服务日志、后端日志、API日志、正式环境日志、生产环境日志、CI环境日志、开发环境日志、阿里云日志、cn-prod 日志、SAE 日志时,使用此技能。支持 CI、Stage、Prod(AWS ECS/CloudWatch)以及 cn-prod(阿里云 SAE,经 buildbox)四个环境的 commerce-backend、user-auth、agentic-chat、gateway-coreoptima-scout、optima-skills 等服务。"
4
4
  allowed-tools: ["Bash", "SlashCommand"]
5
5
  ---
6
6
 
@@ -175,6 +175,19 @@ INFO - Database query took 3200ms: SELECT * FROM products WHERE...
175
175
  - 通过 CloudWatch Logs 查看
176
176
  - 日志保留 7 天
177
177
 
178
+ ### cn-prod 环境(阿里云)
179
+
180
+ ```
181
+ /logs gateway-core 100 cn-prod
182
+ /logs agentic-chat 80 cn
183
+ ```
184
+
185
+ **特点**:
186
+ - 阿里云 **SAE**(cn-beijing),不是 AWS
187
+ - 经 buildbox(`root@47.94.105.163`)调 SAE `DescribeInstanceLog`
188
+ - SAE 未配 SLS → 只取实例**当前 stdout 缓冲**(非历史检索)
189
+ - 技术细节 + service→AppId 映射见 `/logs --help`(第 3 节)
190
+
178
191
  ## 支持的服务列表
179
192
 
180
193
  | 服务 | 说明 | CI | Stage | Prod |
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: "grant-balance"
3
- description: "Use when the user wants to grant USD wallet balance to an Optima user — for promotional grants, compensation, referral rewards, etc. Adds to granted_balance_micros without affecting subscriptions."
3
+ description: "Use when the user wants to grant credits (bonus, 30-day expiry) to an Optima user — for promotional grants, compensation, referral rewards, etc. $1 = 700 credits via billing API; does not affect subscriptions."
4
4
  ---
5
5
 
6
6
  # 赠送 USD 余额(Grant Balance)
7
7
 
8
- 当你需要为用户赠送 wallet USD 余额时,使用这个场景。金额会加到 `usd_wallets.granted_balance_micros`,billing 服务在扣费时会优先消费 granted balance。
8
+ 当你需要为用户赠送 wallet USD 余额时,使用这个场景。金额按 $1=700 积分换算,经 billing API 入 **bonus 积分桶(30 天有效期)**(P15 钱包退役后语义),billing 服务在扣费时会优先消费 granted balance。
9
9
 
10
10
  ## 执行方式:使用 CLI 工具
11
11
 
@@ -16,8 +16,8 @@ optima-grant-balance <email> --amount <usd> [options]
16
16
  **为什么使用 CLI 工具**:
17
17
  - 自动通过 email 查找 userId(跨 user-auth 数据库)
18
18
  - 自动处理 SSH 隧道和数据库连接
19
- - 不会影响现有订阅和已有余额(纯追加到 granted balance)
20
- - 自动写入 audit trail(`usd_wallet_topups` source=admin_grant)
19
+ - 不会影响现有订阅和已有余额(发放 bonus 积分(30 天有效期,重复执行会叠加发放))
20
+ - 自动留审计痕迹(credit_lot 行,幂等键前缀 `dev-skills-grant:`)
21
21
 
22
22
  ## 适用情况
23
23
 
@@ -40,7 +40,7 @@ optima-grant-balance user@example.com --amount 20 --description "服务中断补
40
40
  ```
41
41
 
42
42
  > **单位是美元(USD)**。`--amount 5` 即赠送 $5.00 到 granted balance。
43
- > 数据库底层用 micro-USD 精度(1 USD = 1,000,000 micros),CLI 自动换算。
43
+ > 数据库底层用 micro-USD 精度(1 USD = 700 积分(P15 统一账本口径)
44
44
 
45
45
  ### 参数说明
46
46
 
@@ -49,7 +49,9 @@ optima-grant-balance user@example.com --amount 20 --description "服务中断补
49
49
  | `<email>` | 用户邮箱(必填) | - |
50
50
  | `--amount <usd>` | USD 金额(必填,> 0) | - |
51
51
  | `--description <text>` | 描述/原因(仅 console 输出) | - |
52
- | `--env <env>` | 环境:stage, prod | stage |
52
+ | `--env <env>` | 环境:stage, prod, cn-prod | stage |
53
+
54
+ > **cn-prod**:走 HTTPS(auth-cn/billing-cn.optima.chat),email 查找经 user-auth internal lookup API(无 SSH 隧道)。金额输入仍是 USD($1 = 700 积分 = ¥7 档积分口径一致)。
53
55
 
54
56
  ## 与 grant-subscription 的区别
55
57
 
@@ -92,10 +94,10 @@ optima-grant-balance xxx@gmail.com --amount 20 --env stage
92
94
  1. **Stage 优先**:默认操作 Stage 环境
93
95
  2. **Prod 谨慎**:操作 Prod 前确认邮箱和金额
94
96
  3. **纯追加**:不会影响现有余额和订阅(累加到 granted balance)
95
- 4. **Audit trail**:每次赠送会插入一条 `usd_wallet_topups` 记录(source=`admin_grant`)
97
+ 4. **Audit trail**:每次赠送对应一个 credit_lot(幂等键前缀 `dev-skills-grant:`,type=bonus)
96
98
 
97
99
  ## 相关命令
98
100
 
99
101
  - `optima-grant-balance` - 赠送 USD 余额(主要方式)
100
102
  - `optima-grant-subscription` - 开通订阅计划
101
- - `optima-query-db` - 查询数据库验证结果(`SELECT granted_balance_micros FROM usd_wallets WHERE user_id=...`)
103
+ - `optima-query-db` - 查询数据库验证结果(`GET /api/billing/balance(credits.byType.bonus)或 query-db credit_lot
@@ -21,9 +21,16 @@ optima-grant-subscription user@example.com --plan starter --months 3
21
21
  optima-grant-subscription user@example.com --plan enterprise --env prod
22
22
  ```
23
23
 
24
+ ```bash
25
+ # cn-prod(国内环境):plan 用 -cn 档,默认 pro-cn
26
+ optima-grant-subscription user@example.com --env cn-prod
27
+ optima-grant-subscription user@example.com --plan starter-cn --env cn-prod
28
+ ```
29
+
24
30
  ## Guidance
25
31
 
26
32
  - Default to `stage`.
27
- - Confirm the user email before running on `prod`.
33
+ - Confirm the user email before running on `prod` / `cn-prod`.
34
+ - `cn-prod` plans are the CNY-priced `-cn` ids (`starter-cn`, `pro-cn`, `enterprise-cn`, plus `trial`); bare USD ids are rejected client-side to avoid granting a USD-priced plan to a CN user.
28
35
  - This operation replaces existing subscription state and resets the wallet granted balance according to the selected plan.
29
36
  - Use `optima-query-db` afterward if the user asks for verification.
package/bin/cli.js CHANGED
@@ -32,6 +32,7 @@ switch (command) {
32
32
  log('Available Commands:', 'yellow');
33
33
  log(' optima-query-db <service> "<sql>" [env] Query database', 'cyan');
34
34
  log(' optima-show-env <service> [env] Show service env vars', 'cyan');
35
+ log(' optima-verify-health <service> [--env cn|prod|all] Probe L1-L5 上线健康', 'cyan');
35
36
  log(' optima-generate-test-token [--env production] Generate test token', 'cyan');
36
37
  log(' optima-grant-balance <email> --amount <usd> [--env] Grant USD wallet balance', 'cyan');
37
38
  log(' optima-grant-subscription <email> --plan <p> [--env] Grant subscription', 'cyan');
@@ -5,8 +5,14 @@ import { getInfisicalConfig, getInfisicalToken } from './db-utils';
5
5
  const USER_AUTH_URLS: Record<string, string> = {
6
6
  stage: 'https://auth.stage.optima.onl',
7
7
  prod: 'https://auth.optima.onl',
8
+ 'cn-prod': 'https://auth-cn.optima.chat',
8
9
  };
9
10
 
11
+ // cn-prod URLs are hardcoded: cn Infisical (secrets-cn.optima.chat) is a
12
+ // separate instance dev-skills has no machine identity for, and these domains
13
+ // are stable. AWS envs keep reading /shared-secrets/domain-urls.
14
+ const CN_PROD_BILLING_URL = 'https://billing-cn.optima.chat';
15
+
10
16
  /**
11
17
  * Validate the --env flag value at command entry, before any I/O.
12
18
  *
@@ -24,6 +30,20 @@ export function validateEnv(env: string): 'stage' | 'prod' {
24
30
  return env;
25
31
  }
26
32
 
33
+ /**
34
+ * Variant for commands that also support cn-prod — currently grant-balance /
35
+ * grant-subscription, which reach billing + user-auth over HTTPS only.
36
+ * Other commands resolve users via the AWS RDS SSH tunnel, which does not
37
+ * exist for cn-prod (Aliyun VPC-internal RDS) — keep them on validateEnv so a
38
+ * cn-prod typo fails fast instead of dying inside the tunnel setup.
39
+ */
40
+ export function validateEnvCnProd(env: string): 'stage' | 'prod' | 'cn-prod' {
41
+ if (env !== 'stage' && env !== 'prod' && env !== 'cn-prod') {
42
+ throw new Error(`--env must be "stage", "prod" or "cn-prod" (got: ${env})`);
43
+ }
44
+ return env;
45
+ }
46
+
27
47
  // T1 discovered: client_id differs per env (stage=dev-skills-ubd3qz6n,
28
48
  // prod=dev-skills-hinxa0rs). Both stored in Infisical alongside the
29
49
  // secret at /shared-secrets/oauth-clients/.
@@ -31,6 +51,21 @@ const DEV_SKILLS_OAUTH_PATH = '/shared-secrets/oauth-clients';
31
51
  const DEV_SKILLS_CLIENT_ID_KEY = 'DEV_SKILLS_OAUTH_CLIENT_ID';
32
52
  const DEV_SKILLS_CLIENT_SECRET_KEY = 'DEV_SKILLS_OAUTH_CLIENT_SECRET';
33
53
 
54
+ // cn-prod: the client (dev-skills-ecee51qo) lives in cn user-auth, but its
55
+ // credentials are mirrored into AWS Infisical (prod environment, same path)
56
+ // under CN_PROD-prefixed keys so we reuse the existing Infisical access —
57
+ // zero new credential chain. Canonical copy lives in cn Infisical
58
+ // /shared-secrets/oauth-clients (DEV_SKILLS_OAUTH_CLIENT_ID/SECRET).
59
+ const DEV_SKILLS_CN_CLIENT_ID_KEY = 'DEV_SKILLS_CN_PROD_OAUTH_CLIENT_ID';
60
+ const DEV_SKILLS_CN_CLIENT_SECRET_KEY = 'DEV_SKILLS_CN_PROD_OAUTH_CLIENT_SECRET';
61
+
62
+ // cn-prod tokens must carry this scope: resolveUserIdByEmail calls cn
63
+ // user-auth POST /api/v1/internal/users/lookup, whose guard
64
+ // (verify_internal_service_token) requires it. user-auth issues
65
+ // request∩allowed_scopes and an unscoped request yields scope="" (verified
66
+ // against cn-prod 2026-06-12), so the request must name it explicitly.
67
+ const CN_PROD_TOKEN_SCOPE = 'internal:users:write';
68
+
34
69
  // ───── Cache (process-lifetime) ─────────────────────────────────────────────
35
70
  // One CLI invocation does at most a handful of HTTP calls. We mint the M2M
36
71
  // token once and reuse it. Cross-invocation re-mint is fine — JWT TTL is
@@ -46,6 +81,7 @@ const billingUrlCache: Record<string, string> = {};
46
81
  const skillsUrlCache: Record<string, string> = {};
47
82
 
48
83
  function getBillingUrl(env: string): string {
84
+ if (env === 'cn-prod') return CN_PROD_BILLING_URL;
49
85
  if (billingUrlCache[env]) return billingUrlCache[env];
50
86
  const url = fetchInfisicalSecret(env, '/shared-secrets/domain-urls', 'BILLING_URL');
51
87
  billingUrlCache[env] = url;
@@ -65,13 +101,20 @@ export function getServiceToken(env: string): string {
65
101
  const cfg = getInfisicalConfig();
66
102
  const tok = getInfisicalToken(cfg);
67
103
  // Fetch BOTH client_id and client_secret from Infisical — they differ per env.
68
- const clientId = fetchInfisicalSecret(env, DEV_SKILLS_OAUTH_PATH, DEV_SKILLS_CLIENT_ID_KEY, cfg, tok);
69
- const clientSecret = fetchInfisicalSecret(env, DEV_SKILLS_OAUTH_PATH, DEV_SKILLS_CLIENT_SECRET_KEY, cfg, tok);
104
+ // cn-prod credentials are mirrored in the AWS Infisical *prod* environment
105
+ // (fetchInfisicalSecret has no cn-prod env slug), under CN-specific keys.
106
+ const isCn = env === 'cn-prod';
107
+ const infisicalEnv = isCn ? 'prod' : env;
108
+ const idKey = isCn ? DEV_SKILLS_CN_CLIENT_ID_KEY : DEV_SKILLS_CLIENT_ID_KEY;
109
+ const secretKey = isCn ? DEV_SKILLS_CN_CLIENT_SECRET_KEY : DEV_SKILLS_CLIENT_SECRET_KEY;
110
+ const clientId = fetchInfisicalSecret(infisicalEnv, DEV_SKILLS_OAUTH_PATH, idKey, cfg, tok);
111
+ const clientSecret = fetchInfisicalSecret(infisicalEnv, DEV_SKILLS_OAUTH_PATH, secretKey, cfg, tok);
70
112
 
71
113
  const authUrl = USER_AUTH_URLS[env];
72
114
  if (!authUrl) throw new Error(`Unknown env: ${env}`);
73
115
 
74
- const body = `grant_type=client_credentials&client_id=${encodeURIComponent(clientId)}&client_secret=${encodeURIComponent(clientSecret)}`;
116
+ const scopeParam = isCn ? `&scope=${encodeURIComponent(CN_PROD_TOKEN_SCOPE)}` : '';
117
+ const body = `grant_type=client_credentials&client_id=${encodeURIComponent(clientId)}&client_secret=${encodeURIComponent(clientSecret)}${scopeParam}`;
75
118
  const response = execSync(
76
119
  `curl -s -X POST '${authUrl}/api/v1/oauth/token' -H 'Content-Type: application/x-www-form-urlencoded' -d '${body}'`,
77
120
  { encoding: 'utf-8' },
@@ -189,3 +232,40 @@ export async function callSkills<T = unknown>(
189
232
  ): Promise<ServiceResponse<T>> {
190
233
  return callService<T>(getSkillsUrl(env), env, method, path, body);
191
234
  }
235
+
236
+ /**
237
+ * Resolve a user's id by email via user-auth's internal lookup endpoint
238
+ * (POST /api/v1/internal/users/lookup). cn-prod only: AWS envs resolve via
239
+ * the RDS SSH tunnel (db-utils resolveUserId) and their dev-skills clients
240
+ * don't carry the internal:users:write scope this endpoint requires.
241
+ */
242
+ export async function resolveUserIdByEmail(env: string, email: string): Promise<string> {
243
+ console.log(`Looking up user by email: ${email}`);
244
+ const token = getServiceToken(env);
245
+ const authUrl = USER_AUTH_URLS[env];
246
+ if (!authUrl) throw new Error(`Unknown env: ${env}`);
247
+
248
+ const res = await fetch(`${authUrl}/api/v1/internal/users/lookup`, {
249
+ method: 'POST',
250
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
251
+ body: JSON.stringify({ email }),
252
+ });
253
+ const text = await res.text();
254
+ if (res.status === 404) {
255
+ throw new Error(`User not found (${env}): ${email}`);
256
+ }
257
+ if (!res.ok) {
258
+ throw new Error(formatServiceError(res.status, res.statusText, text));
259
+ }
260
+ let parsed: { user_id?: string };
261
+ try {
262
+ parsed = JSON.parse(text);
263
+ } catch {
264
+ throw new Error(`user-auth lookup returned non-JSON 2xx body: ${text.slice(0, 200)}`);
265
+ }
266
+ if (!parsed.user_id) {
267
+ throw new Error(`user-auth lookup response missing user_id: ${text.slice(0, 200)}`);
268
+ }
269
+ console.log(`✓ Found user: ${parsed.user_id}`);
270
+ return parsed.user_id;
271
+ }