@mearl/client 2.15.0 → 2.15.2

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/dist/buc.js DELETED
@@ -1,533 +0,0 @@
1
- import { accessSync, constants } from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import { spawn } from 'node:child_process';
5
- const BUC_ENV = 'production';
6
- const BUC_PROFILE_APP = 'alineiwai';
7
- const BUC_PROFILE_ORIGIN = 'https://work.alibaba-inc.com';
8
- const BUC_COMMAND_TIMEOUT_MS = 120_000;
9
- const BUC_INSTALL_TIMEOUT_MS = 120_000;
10
- const BUC_REQUEST_TIMEOUT_MS = 30_000;
11
- const MAX_COMMAND_OUTPUT = 1024 * 1024;
12
- const MAX_INSTALL_SCRIPT_LENGTH = 2 * 1024 * 1024;
13
- const MAX_RESPONSE_BYTES = 10 * 1024 * 1024;
14
- const MAX_PROFILE_REDIRECTS = 5;
15
- const MAX_PROFILE_COOKIES = 64;
16
- function appendOutput(current, chunk) {
17
- if (current.length >= MAX_COMMAND_OUTPUT)
18
- return current;
19
- return (current + chunk.toString('utf8')).slice(0, MAX_COMMAND_OUTPUT);
20
- }
21
- function sanitizedDiagnostic(value) {
22
- return value
23
- .replace(/\beyJ[A-Za-z0-9._~+/=-]{40,}\b/g, '<redacted>')
24
- .replace(/\bST-[A-Za-z0-9._~+/=-]{8,}\b/g, '<redacted>')
25
- .replace(/[\r\n]+/g, ' ')
26
- .trim()
27
- .slice(0, 500);
28
- }
29
- function runProgram(command, args, options = {}) {
30
- return new Promise((resolve, reject) => {
31
- const child = spawn(command, args, {
32
- stdio: ['pipe', 'pipe', 'pipe'],
33
- windowsHide: true,
34
- });
35
- let stdout = '';
36
- let stderr = '';
37
- let settled = false;
38
- const finish = (error) => {
39
- if (settled)
40
- return;
41
- settled = true;
42
- clearTimeout(timer);
43
- if (error)
44
- reject(error);
45
- else
46
- resolve({ stdout, stderr });
47
- };
48
- const timer = setTimeout(() => {
49
- child.kill('SIGTERM');
50
- finish(new Error(`BUC command timed out after ${(options.timeoutMs ?? BUC_COMMAND_TIMEOUT_MS) / 1000}s`));
51
- }, options.timeoutMs ?? BUC_COMMAND_TIMEOUT_MS);
52
- child.stdout.on('data', chunk => (stdout = appendOutput(stdout, chunk)));
53
- child.stderr.on('data', chunk => (stderr = appendOutput(stderr, chunk)));
54
- child.once('error', error => finish(error));
55
- child.once('exit', code => {
56
- if (code === 0) {
57
- finish();
58
- return;
59
- }
60
- const detail = sanitizedDiagnostic(stderr || stdout);
61
- finish(new Error(detail ? `BUC CLI failed: ${detail}` : `BUC CLI exited with code ${code}`));
62
- });
63
- child.stdin.end(options.input);
64
- });
65
- }
66
- function isExecutable(filePath) {
67
- try {
68
- accessSync(filePath, process.platform === 'win32' ? constants.F_OK : constants.X_OK);
69
- return true;
70
- }
71
- catch {
72
- return false;
73
- }
74
- }
75
- function resolveBucExecutable() {
76
- const names = process.platform === 'win32' ? ['buc.exe', 'buc.cmd', 'buc'] : ['buc'];
77
- const pathKey = Object.keys(process.env).find(key => key.toLowerCase() === 'path') ?? 'PATH';
78
- const pathDirectories = (process.env[pathKey] ?? '').split(path.delimiter).filter(Boolean);
79
- const directories = [path.join(os.homedir(), '.local', 'bin'), ...pathDirectories];
80
- for (const directory of directories) {
81
- for (const name of names) {
82
- const candidate = path.join(directory, name);
83
- if (isExecutable(candidate))
84
- return candidate;
85
- }
86
- }
87
- return 'buc';
88
- }
89
- export const runBucCommand = args => runProgram(resolveBucExecutable(), args);
90
- async function fetchInstaller(url) {
91
- const response = await fetch(url, { signal: AbortSignal.timeout(30_000) });
92
- if (!response.ok)
93
- throw new Error(`BUC installer returned HTTP ${response.status}`);
94
- const script = await response.text();
95
- if (!script || script.length > MAX_INSTALL_SCRIPT_LENGTH) {
96
- throw new Error('BUC installer returned an invalid script');
97
- }
98
- return script;
99
- }
100
- export async function installBucCli() {
101
- if (process.platform === 'win32') {
102
- const script = await fetchInstaller('https://cli-hub.alibaba-inc.com/buc/install.ps1');
103
- await runProgram('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', '-'], {
104
- input: script,
105
- timeoutMs: BUC_INSTALL_TIMEOUT_MS,
106
- });
107
- return;
108
- }
109
- const script = await fetchInstaller('https://cli-hub.alibaba-inc.com/buc/install.sh');
110
- await runProgram('sh', ['-s'], { input: script, timeoutMs: BUC_INSTALL_TIMEOUT_MS });
111
- }
112
- function isMissingCommandError(error) {
113
- return (error instanceof Error && 'code' in error && error.code === 'ENOENT');
114
- }
115
- async function runBucWithInstall(args, dependencies) {
116
- const runBuc = dependencies.runBuc ?? runBucCommand;
117
- try {
118
- return await runBuc(args);
119
- }
120
- catch (error) {
121
- if (!isMissingCommandError(error))
122
- throw error;
123
- }
124
- await (dependencies.installBuc ?? installBucCli)();
125
- try {
126
- return await runBuc(args);
127
- }
128
- catch (error) {
129
- throw new Error(`BUC CLI is unavailable after installation: ${sanitizedDiagnostic(error instanceof Error ? error.message : String(error))}`, { cause: error });
130
- }
131
- }
132
- function parseJsonOutput(output, label) {
133
- const value = output.trim();
134
- if (!value)
135
- throw new Error(`BUC CLI returned empty ${label} output`);
136
- try {
137
- const parsed = JSON.parse(value);
138
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
139
- throw new Error('not an object');
140
- return parsed;
141
- }
142
- catch (error) {
143
- throw new Error(`BUC CLI returned invalid ${label} JSON`, { cause: error });
144
- }
145
- }
146
- function outputData(payload) {
147
- return payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data)
148
- ? payload.data
149
- : payload;
150
- }
151
- function requiredCredentialValue(value, label) {
152
- if (typeof value !== 'string' || !value || value.length > 32 * 1024 || /[\r\n\0]/.test(value)) {
153
- throw new Error(`BUC CLI returned an invalid ${label}`);
154
- }
155
- return value;
156
- }
157
- function validateApp(app) {
158
- const value = typeof app === 'string' ? app.trim() : '';
159
- if (!/^[a-z][a-z0-9_-]{0,63}$/i.test(value))
160
- throw new Error('BUC app is invalid');
161
- return value;
162
- }
163
- async function createIdToken(app, dependencies) {
164
- const result = await runBucWithInstall(['--no-update-check', 'id-token', 'create', '--app', app, '--env', BUC_ENV], dependencies);
165
- const payload = parseJsonOutput(result.stdout, 'id-token');
166
- if (payload.success === false)
167
- throw new Error('BUC CLI could not create an ID token');
168
- const data = outputData(payload);
169
- return {
170
- idToken: requiredCredentialValue(data.idToken, 'ID token'),
171
- dpop: requiredCredentialValue(data.dpop, 'DPoP proof'),
172
- };
173
- }
174
- async function createSsoTicket(app, dependencies) {
175
- const result = await runBucWithInstall(['--no-update-check', 'ticket', 'create', '--app', app, '--env', BUC_ENV], dependencies);
176
- const payload = parseJsonOutput(result.stdout, 'ticket');
177
- if (payload.success === false)
178
- throw new Error('BUC CLI could not create an SSO ticket');
179
- return requiredCredentialValue(outputData(payload).ssoTicket, 'SSO ticket');
180
- }
181
- export async function bucWhoami(dependencies = {}) {
182
- const result = await runBucWithInstall(['--no-update-check', 'whoami'], dependencies);
183
- const payload = outputData(parseJsonOutput(result.stdout, 'whoami'));
184
- const empId = String(payload.empId ?? '').trim();
185
- if (!empId || empId.length > 128 || /[\r\n\0]/.test(empId)) {
186
- throw new Error('BUC CLI returned an invalid employee identity');
187
- }
188
- const name = typeof payload.name === 'string' ? payload.name.trim() : '';
189
- const agentId = typeof payload.agentId === 'string' ? payload.agentId.trim() : '';
190
- if (agentId && (agentId.length > 256 || /[\r\n\0]/.test(agentId))) {
191
- throw new Error('BUC CLI returned an invalid agent identity');
192
- }
193
- return {
194
- empId,
195
- ...(name ? { name: name.slice(0, 256) } : {}),
196
- ...(agentId ? { agentId } : {}),
197
- };
198
- }
199
- function splitSetCookieHeader(value) {
200
- return value.split(/,(?=\s*[!#$%&'*+\-.^_`|~0-9A-Za-z]+=)/u);
201
- }
202
- function getSetCookieHeaders(headers) {
203
- const values = headers.getSetCookie?.();
204
- if (values?.length)
205
- return values;
206
- const combined = headers.get('set-cookie');
207
- return combined ? splitSetCookieHeader(combined) : [];
208
- }
209
- function updateProfileCookies(cookies, headers) {
210
- for (const header of getSetCookieHeaders(headers)) {
211
- const [pair, ...attributes] = header.split(';');
212
- const separator = pair.indexOf('=');
213
- if (separator <= 0)
214
- continue;
215
- const name = pair.slice(0, separator).trim();
216
- const value = pair.slice(separator + 1).trim();
217
- if (!/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/u.test(name) ||
218
- value.length > 16 * 1024 ||
219
- /[\r\n\0]/.test(value)) {
220
- throw new Error('BUC profile response returned an invalid session cookie');
221
- }
222
- const expired = attributes.some(attribute => /^\s*max-age\s*=\s*0\s*$/iu.test(attribute));
223
- if (expired || !value)
224
- cookies.delete(name);
225
- else
226
- cookies.set(name, value);
227
- if (cookies.size > MAX_PROFILE_COOKIES) {
228
- throw new Error('BUC profile response returned too many session cookies');
229
- }
230
- }
231
- }
232
- function profileCookieHeader(cookies) {
233
- return [...cookies].map(([name, value]) => `${name}=${value}`).join('; ');
234
- }
235
- function profileRedirectUrl(location, base) {
236
- let url;
237
- try {
238
- url = new URL(location, base);
239
- }
240
- catch {
241
- throw new Error('BUC profile response returned an invalid redirect');
242
- }
243
- if (url.protocol !== 'https:' ||
244
- url.origin !== BUC_PROFILE_ORIGIN ||
245
- url.username ||
246
- url.password) {
247
- throw new Error('BUC profile redirects must stay on work.alibaba-inc.com');
248
- }
249
- return url;
250
- }
251
- async function fetchProfileHtml(empId, ticket, dependencies) {
252
- const requestFetch = dependencies.fetch ?? fetch;
253
- const cookies = new Map();
254
- let url = new URL(`/work/u/${encodeURIComponent(empId)}`, BUC_PROFILE_ORIGIN);
255
- let firstRequest = true;
256
- for (let redirectCount = 0; redirectCount <= MAX_PROFILE_REDIRECTS; redirectCount += 1) {
257
- const headers = new Headers();
258
- if (firstRequest)
259
- headers.set('SSO-TICKET', ticket);
260
- else if (cookies.size)
261
- headers.set('Cookie', profileCookieHeader(cookies));
262
- const response = await requestFetch(url, {
263
- method: 'GET',
264
- headers,
265
- redirect: 'manual',
266
- signal: AbortSignal.timeout(BUC_REQUEST_TIMEOUT_MS),
267
- });
268
- firstRequest = false;
269
- updateProfileCookies(cookies, response.headers);
270
- if (response.status >= 300 && response.status < 400) {
271
- const location = response.headers.get('location');
272
- if (!location)
273
- throw new Error('BUC profile response redirected without a location');
274
- if (redirectCount === MAX_PROFILE_REDIRECTS) {
275
- throw new Error(`BUC profile response exceeded ${MAX_PROFILE_REDIRECTS} redirects`);
276
- }
277
- url = profileRedirectUrl(location, url);
278
- continue;
279
- }
280
- if (!response.ok)
281
- throw new Error(`BUC profile request failed (HTTP ${response.status})`);
282
- return (await readResponseText(response)).replaceAll(ticket, '<redacted>');
283
- }
284
- throw new Error('BUC profile request did not complete');
285
- }
286
- function profileString(value) {
287
- if (typeof value !== 'string' && typeof value !== 'number')
288
- return undefined;
289
- const normalized = String(value).trim();
290
- if (!normalized)
291
- return undefined;
292
- if (normalized.length > 4096 || /[\r\n\0]/.test(normalized)) {
293
- throw new Error('BUC profile returned an invalid field');
294
- }
295
- return normalized;
296
- }
297
- function parseBucProfile(html, identity) {
298
- const match = html.match(/\bg_config\s*=\s*(\{[\s\S]*?\});/u);
299
- if (!match)
300
- throw new Error('BUC profile response did not contain user data');
301
- let user;
302
- try {
303
- user = JSON.parse(match[1]).user;
304
- }
305
- catch (error) {
306
- throw new Error('BUC profile response contained invalid user data', { cause: error });
307
- }
308
- if (!user || typeof user !== 'object' || Array.isArray(user)) {
309
- throw new Error('BUC profile response did not contain a user profile');
310
- }
311
- const source = user;
312
- const empId = profileString(source.empId);
313
- if (!empId || empId !== identity.empId) {
314
- throw new Error('BUC profile identity does not match the current employee');
315
- }
316
- const profile = {
317
- empId,
318
- ...(identity.agentId ? { agentId: identity.agentId } : {}),
319
- };
320
- const stringFields = [
321
- 'bucUserId',
322
- 'gid',
323
- 'empType',
324
- 'photoPath',
325
- 'displayName',
326
- 'nickName',
327
- 'deptId',
328
- 'buName',
329
- 'name',
330
- 'buNo',
331
- 'deptDesc',
332
- 'lang',
333
- 'cid',
334
- 'jobDesc',
335
- 'loginWorkNo',
336
- ];
337
- for (const field of stringFields) {
338
- const value = profileString(source[field]);
339
- if (value)
340
- profile[field] = value;
341
- }
342
- if (!profile.name && identity.name)
343
- profile.name = identity.name;
344
- if (typeof source.admin === 'boolean')
345
- profile.admin = source.admin;
346
- return profile;
347
- }
348
- export async function bucProfile(dependencies = {}) {
349
- const identity = await bucWhoami(dependencies);
350
- const ticket = await createSsoTicket(BUC_PROFILE_APP, dependencies);
351
- return parseBucProfile(await fetchProfileHtml(identity.empId, ticket, dependencies), identity);
352
- }
353
- function validateRequestUrl(input) {
354
- let url;
355
- try {
356
- url = new URL(input);
357
- }
358
- catch {
359
- throw new Error('BUC request URL is invalid');
360
- }
361
- const hostname = url.hostname.toLowerCase();
362
- if (url.protocol !== 'https:' ||
363
- (hostname !== 'alibaba-inc.com' && !hostname.endsWith('.alibaba-inc.com')) ||
364
- url.username ||
365
- url.password) {
366
- throw new Error('BUC requests only support HTTPS alibaba-inc.com URLs without credentials');
367
- }
368
- return url;
369
- }
370
- function validateTimeout(value) {
371
- if (value === undefined)
372
- return BUC_REQUEST_TIMEOUT_MS;
373
- if (!Number.isFinite(value) || Number(value) < 1 || Number(value) > 120_000) {
374
- throw new Error('BUC request timeoutMs must be between 1 and 120000');
375
- }
376
- return Number(value);
377
- }
378
- function safeRequestHeaders(input) {
379
- const headers = new Headers();
380
- for (const [name, value] of Object.entries(input ?? {})) {
381
- const normalized = name.trim().toLowerCase();
382
- if (normalized === 'authorization' ||
383
- normalized === 'dpop' ||
384
- normalized === 'cookie' ||
385
- normalized === 'sso-ticket') {
386
- throw new Error(`BUC request header is managed by Mearl: ${name}`);
387
- }
388
- headers.set(name, value);
389
- }
390
- return headers;
391
- }
392
- function validateRequestAuth(value) {
393
- if (value === undefined)
394
- return 'id-token';
395
- if (value === 'id-token' || value === 'ticket-header' || value === 'ticket-query')
396
- return value;
397
- throw new Error('BUC request auth must be id-token, ticket-header, or ticket-query');
398
- }
399
- function isSsoTicketParam(name) {
400
- return name.replace(/[-_]/g, '').toLowerCase() === 'ssoticket';
401
- }
402
- function rejectManagedQueryCredentials(url, params) {
403
- for (const name of url.searchParams.keys()) {
404
- if (isSsoTicketParam(name))
405
- throw new Error(`BUC request query is managed by Mearl: ${name}`);
406
- }
407
- for (const name of Object.keys(params ?? {})) {
408
- if (isSsoTicketParam(name))
409
- throw new Error(`BUC request query is managed by Mearl: ${name}`);
410
- }
411
- }
412
- async function readResponseText(response) {
413
- const declaredLength = Number(response.headers.get('content-length'));
414
- if (Number.isFinite(declaredLength) && declaredLength > MAX_RESPONSE_BYTES) {
415
- throw new Error(`BUC response exceeds ${MAX_RESPONSE_BYTES} bytes`);
416
- }
417
- const buffer = await response.arrayBuffer();
418
- if (buffer.byteLength > MAX_RESPONSE_BYTES) {
419
- throw new Error(`BUC response exceeds ${MAX_RESPONSE_BYTES} bytes`);
420
- }
421
- return new TextDecoder().decode(buffer);
422
- }
423
- function parseResponseBody(text, responseType) {
424
- if (responseType === 'text')
425
- return text;
426
- const trimmed = text.trimStart();
427
- const shouldParse = responseType === 'json' || trimmed.startsWith('{') || trimmed.startsWith('[');
428
- if (!shouldParse)
429
- return text;
430
- try {
431
- return JSON.parse(text);
432
- }
433
- catch (error) {
434
- if (responseType === 'json')
435
- throw new Error('BUC response is not valid JSON', { cause: error });
436
- return text;
437
- }
438
- }
439
- function redactCredentials(value, credentials) {
440
- return credentials.reduce((result, credential) => {
441
- const formEncoded = new URLSearchParams({ value: credential })
442
- .toString()
443
- .slice('value='.length);
444
- return [...new Set([credential, encodeURIComponent(credential), formEncoded])]
445
- .sort((left, right) => right.length - left.length)
446
- .reduce((redacted, candidate) => redacted.replaceAll(candidate, '<redacted>'), result);
447
- }, value);
448
- }
449
- function responseHeaders(headers, credentials) {
450
- return Object.fromEntries([...headers.entries()]
451
- .filter(([name]) => !['authorization', 'dpop', 'set-cookie', 'sso-ticket'].includes(name.toLowerCase()))
452
- .map(([name, value]) => [name, redactCredentials(value, credentials)]));
453
- }
454
- /** Executes one internal request with a fresh credential in the selected BUC authentication mode. */
455
- export async function bucRequest(input, dependencies = {}) {
456
- const app = validateApp(input?.app);
457
- const url = validateRequestUrl(input?.url);
458
- const auth = validateRequestAuth(input?.auth);
459
- rejectManagedQueryCredentials(url, input.params);
460
- for (const [name, value] of Object.entries(input.params ?? {})) {
461
- if (value !== undefined && value !== null)
462
- url.searchParams.set(name, String(value));
463
- }
464
- if (input.body !== undefined && input.formData !== undefined) {
465
- throw new Error('BUC request body and formData cannot be combined');
466
- }
467
- const headers = safeRequestHeaders(input.headers);
468
- let body;
469
- if (input.formData !== undefined) {
470
- const form = new FormData();
471
- for (const [name, value] of Object.entries(input.formData))
472
- form.append(name, String(value));
473
- body = form;
474
- }
475
- else if (input.body !== undefined) {
476
- if (typeof input.body === 'string')
477
- body = input.body;
478
- else {
479
- headers.set('Content-Type', 'application/json');
480
- body = JSON.stringify(input.body);
481
- }
482
- }
483
- const method = (input.method ?? (body === undefined ? 'GET' : 'POST')).trim().toUpperCase();
484
- if (!/^[A-Z]+$/.test(method))
485
- throw new Error('BUC request method is invalid');
486
- if ((method === 'GET' || method === 'HEAD') && body !== undefined) {
487
- throw new Error(`${method} BUC requests cannot include a body`);
488
- }
489
- let credentials;
490
- if (auth === 'id-token') {
491
- const credential = await createIdToken(app, dependencies);
492
- credentials = [credential.idToken, credential.dpop];
493
- headers.set('Authorization', `Bearer ${credential.idToken}`);
494
- headers.set('DPoP', credential.dpop);
495
- }
496
- else {
497
- const ticket = await createSsoTicket(app, dependencies);
498
- credentials = [ticket];
499
- if (auth === 'ticket-header')
500
- headers.set('SSO-TICKET', ticket);
501
- else
502
- url.searchParams.set('SSO_TICKET', ticket);
503
- }
504
- const requestFetch = dependencies.fetch ?? fetch;
505
- let response;
506
- try {
507
- response = await requestFetch(url, {
508
- method,
509
- headers,
510
- body,
511
- redirect: 'manual',
512
- signal: AbortSignal.timeout(validateTimeout(input.timeoutMs)),
513
- });
514
- }
515
- catch (error) {
516
- const detail = redactCredentials(sanitizedDiagnostic(error instanceof Error ? error.message : String(error)), credentials);
517
- // Raw transport errors can retain credential-bearing URLs, so only sanitized text crosses the API.
518
- // eslint-disable-next-line preserve-caught-error
519
- throw new Error(detail ? `BUC request failed: ${detail}` : 'BUC request failed');
520
- }
521
- const text = redactCredentials(await readResponseText(response), credentials);
522
- return {
523
- status: response.status,
524
- statusText: redactCredentials(response.statusText, credentials),
525
- headers: responseHeaders(response.headers, credentials),
526
- body: parseResponseBody(text, input.responseType ?? 'auto'),
527
- };
528
- }
529
- export const buc = Object.freeze({
530
- request: bucRequest,
531
- whoami: bucWhoami,
532
- profile: bucProfile,
533
- });