@optima-chat/dev-skills 0.7.38 → 0.7.41
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 +10 -8
- package/.claude/skills/grant-subscription/SKILL.md +12 -6
- package/.codex/skills/grant-balance/SKILL.md +10 -8
- package/.codex/skills/grant-subscription/SKILL.md +8 -1
- package/bin/helpers/billing-http.ts +85 -3
- package/bin/helpers/db-utils.ts +274 -37
- package/bin/helpers/generate-test-token.ts +17 -8
- package/bin/helpers/grant-balance.ts +39 -46
- package/bin/helpers/grant-subscription.ts +47 -89
- package/bin/helpers/query-db.ts +25 -60
- package/bin/helpers/show-env.ts +3 -28
- package/bin/helpers/verify-health.ts +9 -8
- package/dist/bin/helpers/billing-http.js +84 -3
- package/dist/bin/helpers/db-utils.js +292 -45
- package/dist/bin/helpers/generate-test-token.js +15 -6
- package/dist/bin/helpers/grant-balance.js +35 -43
- package/dist/bin/helpers/grant-subscription.js +36 -86
- package/dist/bin/helpers/query-db.js +33 -49
- package/dist/bin/helpers/show-env.js +5 -17
- package/dist/bin/helpers/verify-health.js +9 -8
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
-
|
|
21
|
-
-
|
|
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 =
|
|
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
|
|
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` - 查询数据库验证结果(`
|
|
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
|
-
-
|
|
23
|
-
- 自动按 plan
|
|
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
|
-
>
|
|
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.
|
|
111
|
+
5. 经 billing API supersede 旧授予(void 旧 subscription lot + 发新) 并授予新额度
|
|
106
112
|
6. 记录 topup 审计记录(source: subscription_grant)
|
|
107
113
|
7. 更新 token quota 限额
|
|
108
114
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: "grant-balance"
|
|
3
|
-
description: "Use when the user wants to grant
|
|
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
|
|
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
|
-
-
|
|
20
|
-
-
|
|
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 =
|
|
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
|
|
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` - 查询数据库验证结果(`
|
|
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.
|
|
@@ -5,8 +5,16 @@ 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
|
+
// #201: yzsgo.com 全量迁移 (2026-06-12), 旧 auth-cn.optima.chat 路由已下线
|
|
9
|
+
'cn-prod': 'https://auth.yzsgo.com',
|
|
8
10
|
};
|
|
9
11
|
|
|
12
|
+
// cn-prod URLs are hardcoded: cn Infisical (secrets-cn.optima.chat) is a
|
|
13
|
+
// separate instance dev-skills has no machine identity for, and these domains
|
|
14
|
+
// are stable. AWS envs keep reading /shared-secrets/domain-urls.
|
|
15
|
+
// #201: yzsgo.com 全量迁移 (2026-06-12), 旧 billing-cn.optima.chat 路由已下线
|
|
16
|
+
const CN_PROD_BILLING_URL = 'https://billing-api.yzsgo.com';
|
|
17
|
+
|
|
10
18
|
/**
|
|
11
19
|
* Validate the --env flag value at command entry, before any I/O.
|
|
12
20
|
*
|
|
@@ -24,6 +32,20 @@ export function validateEnv(env: string): 'stage' | 'prod' {
|
|
|
24
32
|
return env;
|
|
25
33
|
}
|
|
26
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Variant for commands that also support cn-prod — currently grant-balance /
|
|
37
|
+
* grant-subscription, which reach billing + user-auth over HTTPS only.
|
|
38
|
+
* Other commands resolve users via the AWS RDS SSH tunnel, which does not
|
|
39
|
+
* exist for cn-prod (Aliyun VPC-internal RDS) — keep them on validateEnv so a
|
|
40
|
+
* cn-prod typo fails fast instead of dying inside the tunnel setup.
|
|
41
|
+
*/
|
|
42
|
+
export function validateEnvCnProd(env: string): 'stage' | 'prod' | 'cn-prod' {
|
|
43
|
+
if (env !== 'stage' && env !== 'prod' && env !== 'cn-prod') {
|
|
44
|
+
throw new Error(`--env must be "stage", "prod" or "cn-prod" (got: ${env})`);
|
|
45
|
+
}
|
|
46
|
+
return env;
|
|
47
|
+
}
|
|
48
|
+
|
|
27
49
|
// T1 discovered: client_id differs per env (stage=dev-skills-ubd3qz6n,
|
|
28
50
|
// prod=dev-skills-hinxa0rs). Both stored in Infisical alongside the
|
|
29
51
|
// secret at /shared-secrets/oauth-clients/.
|
|
@@ -31,6 +53,21 @@ const DEV_SKILLS_OAUTH_PATH = '/shared-secrets/oauth-clients';
|
|
|
31
53
|
const DEV_SKILLS_CLIENT_ID_KEY = 'DEV_SKILLS_OAUTH_CLIENT_ID';
|
|
32
54
|
const DEV_SKILLS_CLIENT_SECRET_KEY = 'DEV_SKILLS_OAUTH_CLIENT_SECRET';
|
|
33
55
|
|
|
56
|
+
// cn-prod: the client (dev-skills-ecee51qo) lives in cn user-auth, but its
|
|
57
|
+
// credentials are mirrored into AWS Infisical (prod environment, same path)
|
|
58
|
+
// under CN_PROD-prefixed keys so we reuse the existing Infisical access —
|
|
59
|
+
// zero new credential chain. Canonical copy lives in cn Infisical
|
|
60
|
+
// /shared-secrets/oauth-clients (DEV_SKILLS_OAUTH_CLIENT_ID/SECRET).
|
|
61
|
+
const DEV_SKILLS_CN_CLIENT_ID_KEY = 'DEV_SKILLS_CN_PROD_OAUTH_CLIENT_ID';
|
|
62
|
+
const DEV_SKILLS_CN_CLIENT_SECRET_KEY = 'DEV_SKILLS_CN_PROD_OAUTH_CLIENT_SECRET';
|
|
63
|
+
|
|
64
|
+
// cn-prod tokens must carry this scope: resolveUserIdByEmail calls cn
|
|
65
|
+
// user-auth POST /api/v1/internal/users/lookup, whose guard
|
|
66
|
+
// (verify_internal_service_token) requires it. user-auth issues
|
|
67
|
+
// request∩allowed_scopes and an unscoped request yields scope="" (verified
|
|
68
|
+
// against cn-prod 2026-06-12), so the request must name it explicitly.
|
|
69
|
+
const CN_PROD_TOKEN_SCOPE = 'internal:users:write';
|
|
70
|
+
|
|
34
71
|
// ───── Cache (process-lifetime) ─────────────────────────────────────────────
|
|
35
72
|
// One CLI invocation does at most a handful of HTTP calls. We mint the M2M
|
|
36
73
|
// token once and reuse it. Cross-invocation re-mint is fine — JWT TTL is
|
|
@@ -46,6 +83,7 @@ const billingUrlCache: Record<string, string> = {};
|
|
|
46
83
|
const skillsUrlCache: Record<string, string> = {};
|
|
47
84
|
|
|
48
85
|
function getBillingUrl(env: string): string {
|
|
86
|
+
if (env === 'cn-prod') return CN_PROD_BILLING_URL;
|
|
49
87
|
if (billingUrlCache[env]) return billingUrlCache[env];
|
|
50
88
|
const url = fetchInfisicalSecret(env, '/shared-secrets/domain-urls', 'BILLING_URL');
|
|
51
89
|
billingUrlCache[env] = url;
|
|
@@ -65,13 +103,20 @@ export function getServiceToken(env: string): string {
|
|
|
65
103
|
const cfg = getInfisicalConfig();
|
|
66
104
|
const tok = getInfisicalToken(cfg);
|
|
67
105
|
// Fetch BOTH client_id and client_secret from Infisical — they differ per env.
|
|
68
|
-
|
|
69
|
-
|
|
106
|
+
// cn-prod credentials are mirrored in the AWS Infisical *prod* environment
|
|
107
|
+
// (fetchInfisicalSecret has no cn-prod env slug), under CN-specific keys.
|
|
108
|
+
const isCn = env === 'cn-prod';
|
|
109
|
+
const infisicalEnv = isCn ? 'prod' : env;
|
|
110
|
+
const idKey = isCn ? DEV_SKILLS_CN_CLIENT_ID_KEY : DEV_SKILLS_CLIENT_ID_KEY;
|
|
111
|
+
const secretKey = isCn ? DEV_SKILLS_CN_CLIENT_SECRET_KEY : DEV_SKILLS_CLIENT_SECRET_KEY;
|
|
112
|
+
const clientId = fetchInfisicalSecret(infisicalEnv, DEV_SKILLS_OAUTH_PATH, idKey, cfg, tok);
|
|
113
|
+
const clientSecret = fetchInfisicalSecret(infisicalEnv, DEV_SKILLS_OAUTH_PATH, secretKey, cfg, tok);
|
|
70
114
|
|
|
71
115
|
const authUrl = USER_AUTH_URLS[env];
|
|
72
116
|
if (!authUrl) throw new Error(`Unknown env: ${env}`);
|
|
73
117
|
|
|
74
|
-
const
|
|
118
|
+
const scopeParam = isCn ? `&scope=${encodeURIComponent(CN_PROD_TOKEN_SCOPE)}` : '';
|
|
119
|
+
const body = `grant_type=client_credentials&client_id=${encodeURIComponent(clientId)}&client_secret=${encodeURIComponent(clientSecret)}${scopeParam}`;
|
|
75
120
|
const response = execSync(
|
|
76
121
|
`curl -s -X POST '${authUrl}/api/v1/oauth/token' -H 'Content-Type: application/x-www-form-urlencoded' -d '${body}'`,
|
|
77
122
|
{ encoding: 'utf-8' },
|
|
@@ -189,3 +234,40 @@ export async function callSkills<T = unknown>(
|
|
|
189
234
|
): Promise<ServiceResponse<T>> {
|
|
190
235
|
return callService<T>(getSkillsUrl(env), env, method, path, body);
|
|
191
236
|
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Resolve a user's id by email via user-auth's internal lookup endpoint
|
|
240
|
+
* (POST /api/v1/internal/users/lookup). cn-prod only: AWS envs resolve via
|
|
241
|
+
* the RDS SSH tunnel (db-utils resolveUserId) and their dev-skills clients
|
|
242
|
+
* don't carry the internal:users:write scope this endpoint requires.
|
|
243
|
+
*/
|
|
244
|
+
export async function resolveUserIdByEmail(env: string, email: string): Promise<string> {
|
|
245
|
+
console.log(`Looking up user by email: ${email}`);
|
|
246
|
+
const token = getServiceToken(env);
|
|
247
|
+
const authUrl = USER_AUTH_URLS[env];
|
|
248
|
+
if (!authUrl) throw new Error(`Unknown env: ${env}`);
|
|
249
|
+
|
|
250
|
+
const res = await fetch(`${authUrl}/api/v1/internal/users/lookup`, {
|
|
251
|
+
method: 'POST',
|
|
252
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
253
|
+
body: JSON.stringify({ email }),
|
|
254
|
+
});
|
|
255
|
+
const text = await res.text();
|
|
256
|
+
if (res.status === 404) {
|
|
257
|
+
throw new Error(`User not found (${env}): ${email}`);
|
|
258
|
+
}
|
|
259
|
+
if (!res.ok) {
|
|
260
|
+
throw new Error(formatServiceError(res.status, res.statusText, text));
|
|
261
|
+
}
|
|
262
|
+
let parsed: { user_id?: string };
|
|
263
|
+
try {
|
|
264
|
+
parsed = JSON.parse(text);
|
|
265
|
+
} catch {
|
|
266
|
+
throw new Error(`user-auth lookup returned non-JSON 2xx body: ${text.slice(0, 200)}`);
|
|
267
|
+
}
|
|
268
|
+
if (!parsed.user_id) {
|
|
269
|
+
throw new Error(`user-auth lookup response missing user_id: ${text.slice(0, 200)}`);
|
|
270
|
+
}
|
|
271
|
+
console.log(`✓ Found user: ${parsed.user_id}`);
|
|
272
|
+
return parsed.user_id;
|
|
273
|
+
}
|