@cnbcool/cnb-api-generate 2.5.0 → 2.5.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/client/index.ts CHANGED
@@ -7,6 +7,7 @@ import { registerFallbackAction } from './lib/register-fallback';
7
7
  import { registerLoginCommand } from './lib/login';
8
8
  import { registerLogoutCommand } from './lib/logout';
9
9
  import { registerStatusCommand } from './lib/status';
10
+ import { registerGitCredentialCommand } from './lib/git-credential';
10
11
 
11
12
  // ============================================================
12
13
  // Commander 程序定义
@@ -30,6 +31,7 @@ program
30
31
  registerLoginCommand(program);
31
32
  registerLogoutCommand(program);
32
33
  registerStatusCommand(program);
34
+ registerGitCredentialCommand(program);
33
35
  registerModuleCommands(program);
34
36
  registerFallbackAction(program);
35
37
 
@@ -0,0 +1,182 @@
1
+ import { Command } from 'commander';
2
+ import { resolveToken } from '../utils/resolve-token';
3
+
4
+ /**
5
+ * 用作 git credential helper。
6
+ *
7
+ * 在 .gitconfig 中配置:
8
+ * [credential]
9
+ * helper = "<cli> git-credential"
10
+ * useHttpPath = true
11
+ *
12
+ * git 在执行需要凭据的操作时,会以 `get`、`store`、`erase` 之一作为 action 调用本命令,
13
+ * 并通过 stdin 传入若干 `key=value` 形式的字段(protocol/host/path 等)。
14
+ * 我们仅处理 `get`:根据 host 白名单校验后,从后端获取凭据,按 git-credential
15
+ * 协议把 `username=...` / `password=...` 写到 stdout;`store` / `erase` 直接忽略。
16
+ */
17
+
18
+ interface GitCredentialInput {
19
+ protocol?: string;
20
+ host?: string;
21
+ path?: string;
22
+ [key: string]: string | undefined;
23
+ }
24
+
25
+ interface Credential {
26
+ username: string;
27
+ password: string;
28
+ }
29
+
30
+ const ALLOWED_HOSTS = ['cnb.cool', 'cnb.woa.com'];
31
+
32
+ /** git-credential get 等待 stdin 的最长时间,避免 git 无限挂起。 */
33
+ const GET_INPUT_TIMEOUT_MS = 8000;
34
+
35
+ export function registerGitCredentialCommand(program: Command): void {
36
+ program
37
+ .command('git-credential <action>')
38
+ .description('作为 git credential helper,向 git 提供 CNB 凭据(仅供 git 内部调用)')
39
+ .allowUnknownOption()
40
+ .allowExcessArguments()
41
+ .helpOption('-h, --help', '显示帮助文档')
42
+ .action(async (action: string, _opts, cmd: Command) => {
43
+ // 透传给用户:剩余的位置参数都视为自定义参数
44
+ const customArgs = cmd.args.slice(0, -1);
45
+ if (customArgs.length > 0) {
46
+ console.error(`[git-credential]: custom args: ${customArgs.join(' ')}`);
47
+ }
48
+
49
+ // store / erase:保持与原 helper 一致,忽略并退出 0
50
+ if (action === 'store' || action === 'erase') {
51
+ console.error(`[git-credential]: ${action} ignored`);
52
+ process.exit(0);
53
+ }
54
+
55
+ if (action !== 'get') {
56
+ console.error(`[git-credential]: unknown action: ${action}`);
57
+ process.exit(1);
58
+ }
59
+
60
+ // get:从 stdin 读取字段并输出凭据
61
+ try {
62
+ const input = await readStdin(GET_INPUT_TIMEOUT_MS);
63
+ const data = parseText(input, '=') as GitCredentialInput;
64
+ normalizePath(data);
65
+
66
+ await auth(data);
67
+ process.exit(0);
68
+ } catch (err: any) {
69
+ console.error(err?.message ? err.message : String(err));
70
+ process.exit(2);
71
+ }
72
+ });
73
+ }
74
+
75
+ /**
76
+ * 校验白名单并向后端获取凭据,按照 git-credential 协议输出 username/password。
77
+ */
78
+ async function auth(data: GitCredentialInput): Promise<void> {
79
+ const { host } = data;
80
+ console.error(
81
+ `[git-credential]: for ${data.protocol}://${data.host}/${data.path ?? ''}`,
82
+ );
83
+
84
+ // 必须校验域名,否则传入第三方域名就会被盗密码
85
+ if (!host || !ALLOWED_HOSTS.includes(host)) {
86
+ throw new Error(`unknown host: ${host}`);
87
+ }
88
+
89
+ const credential = await fetchCredential(data);
90
+
91
+ // git-credential 协议:通过 stdout 输出 key=value
92
+ process.stdout.write(`username=${credential.username}\n`);
93
+ process.stdout.write(`password=${credential.password}\n`);
94
+
95
+ console.error('[git-credential]: done');
96
+ }
97
+
98
+ async function fetchCredential(_data: GitCredentialInput): Promise<Credential> {
99
+ // 使用 resolveToken 获取 access_token 作为 password,username 固定为 cnb
100
+ const token = await resolveToken();
101
+ return {
102
+ username: 'cnb',
103
+ password: token,
104
+ };
105
+ }
106
+
107
+ /**
108
+ * git-credential 协议中 path 通常包含仓库目录,需要规范化。
109
+ */
110
+ function normalizePath(data: GitCredentialInput): void {
111
+ if (!data.path) return;
112
+ if (data.path.endsWith('.git')) {
113
+ data.path = data.path.slice(0, -4);
114
+ } else if (data.path.endsWith('.git/info/lfs')) {
115
+ data.path = data.path.slice(0, -13);
116
+ }
117
+ }
118
+
119
+ /**
120
+ * 从 stdin 收集所有数据。
121
+ *
122
+ * 同原始 helper 一致:必须设置超时,否则 git 在拿不到响应时会让用户输入账号密码,
123
+ * 进而把进程挂起。同时支持单字节回车(0x0a)作为提前结束的信号。
124
+ */
125
+ function readStdin(timeoutMs: number): Promise<string> {
126
+ return new Promise((resolve, reject) => {
127
+ const buff: Buffer[] = [];
128
+ let settled = false;
129
+
130
+ const finish = (): void => {
131
+ if (settled) return;
132
+ settled = true;
133
+ clearTimeout(timer);
134
+ resolve(Buffer.concat(buff).toString('utf-8'));
135
+ };
136
+
137
+ const timer = setTimeout(() => {
138
+ if (settled) return;
139
+ settled = true;
140
+ console.error('[git-credential]: timeout occurred');
141
+ reject(new Error('stdin timeout'));
142
+ }, timeoutMs);
143
+
144
+ process.stdin.on('data', (chunk: Buffer) => {
145
+ buff.push(chunk);
146
+ // git 会在最后发送一个空行(仅一个 \n)作为结束标志
147
+ if (chunk.length === 1 && chunk[0] === 0x0a) {
148
+ finish();
149
+ }
150
+ });
151
+
152
+ process.stdin.on('end', () => {
153
+ finish();
154
+ });
155
+
156
+ process.stdin.on('error', (err) => {
157
+ if (settled) return;
158
+ settled = true;
159
+ clearTimeout(timer);
160
+ reject(err);
161
+ });
162
+ });
163
+ }
164
+
165
+ /**
166
+ * 把 git 通过 stdin 传入的多行 key=value 文本解析为对象。
167
+ */
168
+ function parseText(content: string, separator: string): Record<string, string> {
169
+ return content
170
+ .split(/(?:\r\n|\r|\n)/)
171
+ .filter(Boolean)
172
+ .map((line) => {
173
+ const idx = line.indexOf(separator);
174
+ const key = idx >= 0 ? line.substring(0, idx).trim() : line.trim();
175
+ const value = idx >= 0 ? line.substring(idx + 1).trim() : '';
176
+ return { key, value };
177
+ })
178
+ .reduce<Record<string, string>>((acc, { key, value }) => {
179
+ if (key) acc[key] = value;
180
+ return acc;
181
+ }, {});
182
+ }
@@ -0,0 +1,7 @@
1
+ // connector name 由 WorkBuddy 与 CLI 团队约定,CLI 写死
2
+ const CONNECTOR_NAME = 'cnb-app';
3
+
4
+ export function getTokenEnvKey(): string {
5
+ // cnb-app → WORKBUDDY_TOKEN_URL_CNB_APP
6
+ return `WORKBUDDY_TOKEN_URL_${CONNECTOR_NAME.toUpperCase().replace(/-/g, '_')}`;
7
+ }
@@ -0,0 +1,3 @@
1
+ export function isWorkBuddySandbox(): boolean {
2
+ return !!process.env.AGENTOS_RUNTIME_ID;
3
+ }
@@ -10,7 +10,7 @@ export async function refreshAccessToken(store: TokenStore): Promise<string> {
10
10
  throw new Error('no refresh_token');
11
11
  }
12
12
 
13
- const platformURL = store.platform_url || 'https://cnb-dev.woa.com';
13
+ const platformURL = store.platform_url || 'https://cnb.cool';
14
14
  const clientID = store.client_id || process.env.OAUTH2_CLIENT_ID || 'cnb_cli';
15
15
  const tokenURL = `${platformURL.replace(/\/+$/, '')}/oauth2/token`;
16
16
 
@@ -1,3 +1,5 @@
1
+ import { getTokenEnvKey } from './get-token-env-key';
2
+ import { isWorkBuddySandbox } from './is-workbuddy-sandbox';
1
3
  import { loadToken } from './load-token';
2
4
  import { refreshAccessToken } from './refresh-token';
3
5
 
@@ -9,6 +11,38 @@ import { refreshAccessToken } from './refresh-token';
9
11
  * 刷新失败或无 refresh_token 时终止进程并提示用户重新登录。
10
12
  */
11
13
  export async function resolveToken(): Promise<string> {
14
+ // WorkBuddy 沙箱环境,从远程接口获取 token
15
+ if (isWorkBuddySandbox()) {
16
+ const tokenURL = process.env[getTokenEnvKey()];
17
+ if (!tokenURL) {
18
+ console.error(`未找到环境变量 ${getTokenEnvKey()},无法在 WorkBuddy 沙箱环境中获取 token。`);
19
+ process.exit(1);
20
+ }
21
+
22
+ try {
23
+ const resp = await fetch(tokenURL, {
24
+ method: 'GET',
25
+ headers: { Accept: 'application/json' },
26
+ });
27
+
28
+ if (!resp.ok) {
29
+ console.error(`从 WorkBuddy 获取 token 失败,HTTP 状态码: ${resp.status} ${resp.statusText}`);
30
+ process.exit(1);
31
+ }
32
+
33
+ const body: any = await resp.json();
34
+ if (body?.code !== 0 || !body?.data?.access_token) {
35
+ console.error(`从 WorkBuddy 获取 token 失败,响应内容异常: ${JSON.stringify(body)}`);
36
+ process.exit(1);
37
+ }
38
+
39
+ return body.data.access_token as string;
40
+ } catch (err: any) {
41
+ console.error(`从 WorkBuddy 获取 token 失败: ${err?.message || err}`);
42
+ process.exit(1);
43
+ }
44
+ }
45
+
12
46
  // 环境变量优先 CodeBuddy
13
47
  if (process.env.CNB_TOKEN_FOR_CODEBUDDY) {
14
48
  if (!process.env.CNB_NPC_SLUG && process.env.CNB_NPC_NAME === 'CodeBuddy') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cnbcool/cnb-api-generate",
3
- "version": "2.5.0",
3
+ "version": "2.5.1",
4
4
  "main": "./built/index.js",
5
5
  "module": "./src/index.ts",
6
6
  "types": "./src/index.ts",