weaver-work-cli 0.1.0

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.
Files changed (90) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +422 -0
  3. package/dist/cmd/action.js +35 -0
  4. package/dist/cmd/commands/index.js +15 -0
  5. package/dist/cmd/config/index.js +64 -0
  6. package/dist/cmd/docs/index.js +56 -0
  7. package/dist/cmd/doctor/index.js +45 -0
  8. package/dist/cmd/e10/index.js +25 -0
  9. package/dist/cmd/index.js +18 -0
  10. package/dist/cmd/register.js +19 -0
  11. package/dist/cmd/root.js +36 -0
  12. package/dist/cmd/run/index.js +38 -0
  13. package/dist/cmd/setup/index.js +77 -0
  14. package/dist/cmd/skills/index.js +72 -0
  15. package/dist/core/config-store.js +43 -0
  16. package/dist/core/context.js +31 -0
  17. package/dist/core/errors.js +60 -0
  18. package/dist/core/hooks.js +41 -0
  19. package/dist/core/logger.js +27 -0
  20. package/dist/core/output.js +25 -0
  21. package/dist/core/write-with-retry.js +21 -0
  22. package/dist/index.js +11 -0
  23. package/dist/internal/e10/auth/commands.js +594 -0
  24. package/dist/internal/e10/auth/crypto.js +126 -0
  25. package/dist/internal/e10/auth/paths.js +12 -0
  26. package/dist/internal/e10/auth/session.js +449 -0
  27. package/dist/internal/e10/client.js +104 -0
  28. package/dist/internal/e10/context.js +20 -0
  29. package/dist/internal/e10/index.js +2 -0
  30. package/dist/internal/e10/types.js +1 -0
  31. package/dist/internal/skills/install.js +99 -0
  32. package/dist/internal/skills/reader.js +119 -0
  33. package/dist/services/command-runner.js +44 -0
  34. package/dist/shortcuts/index.js +4 -0
  35. package/dist/shortcuts/invoice/continuation.js +70 -0
  36. package/dist/shortcuts/invoice/errors.js +59 -0
  37. package/dist/shortcuts/invoice/host.js +337 -0
  38. package/dist/shortcuts/invoice/index.js +301 -0
  39. package/dist/shortcuts/invoice/manifest.js +288 -0
  40. package/dist/shortcuts/invoice/operations/add.js +106 -0
  41. package/dist/shortcuts/invoice/operations/delete.js +92 -0
  42. package/dist/shortcuts/invoice/operations/download.js +16 -0
  43. package/dist/shortcuts/invoice/operations/get.js +7 -0
  44. package/dist/shortcuts/invoice/operations/import.js +128 -0
  45. package/dist/shortcuts/invoice/operations/list.js +12 -0
  46. package/dist/shortcuts/invoice/operations/ocr-preview.js +40 -0
  47. package/dist/shortcuts/invoice/operations/registry.js +50 -0
  48. package/dist/shortcuts/invoice/operations/shared.js +640 -0
  49. package/dist/shortcuts/invoice/operations/types.js +1 -0
  50. package/dist/shortcuts/invoice/operations/update.js +109 -0
  51. package/dist/shortcuts/invoice/operations/upload.js +8 -0
  52. package/dist/shortcuts/invoice/operations/validate-preview.js +39 -0
  53. package/dist/shortcuts/invoice/operations.js +8 -0
  54. package/dist/types.js +1 -0
  55. package/docs/SKILL.md +36 -0
  56. package/docs/_catalog.md +12 -0
  57. package/docs/agent-invoice.md +53 -0
  58. package/docs/e10-auth.md +51 -0
  59. package/docs/invoice.md +99 -0
  60. package/package.json +51 -0
  61. package/scripts/clean.mjs +4 -0
  62. package/scripts/package-skill.mjs +164 -0
  63. package/scripts/sync-docs.mjs +17 -0
  64. package/skill-template/business-info.json +4 -0
  65. package/skill-template/domains/invoice.md +23 -0
  66. package/skill-template/domains/shared.md +8 -0
  67. package/skill-template/master-skill-template.md +29 -0
  68. package/skill-template/skill-template.md +60 -0
  69. package/skills/weaver-work-cli-invoice/SKILL.md +103 -0
  70. package/skills/weaver-work-cli-invoice/product.json +8 -0
  71. package/skills/weaver-work-cli-invoice/references/invoice-add.md +18 -0
  72. package/skills/weaver-work-cli-invoice/references/invoice-agent-entry.md +38 -0
  73. package/skills/weaver-work-cli-invoice/references/invoice-delete.md +20 -0
  74. package/skills/weaver-work-cli-invoice/references/invoice-detail.md +29 -0
  75. package/skills/weaver-work-cli-invoice/references/invoice-disabled-capabilities.md +26 -0
  76. package/skills/weaver-work-cli-invoice/references/invoice-download.md +22 -0
  77. package/skills/weaver-work-cli-invoice/references/invoice-enterprise-list.md +35 -0
  78. package/skills/weaver-work-cli-invoice/references/invoice-file-upload.md +28 -0
  79. package/skills/weaver-work-cli-invoice/references/invoice-import.md +28 -0
  80. package/skills/weaver-work-cli-invoice/references/invoice-ocr-preview.md +33 -0
  81. package/skills/weaver-work-cli-invoice/references/invoice-personal-list.md +47 -0
  82. package/skills/weaver-work-cli-invoice/references/invoice-update.md +38 -0
  83. package/skills/weaver-work-cli-invoice/references/invoice-validation-preview.md +26 -0
  84. package/skills/weaver-work-cli-shared/SKILL.md +53 -0
  85. package/skills/weaver-work-cli-shared/references/e10-auth-and-session.md +83 -0
  86. package/skills/weaver-work-cli-shared/references/high-risk-write.md +36 -0
  87. package/skills/weaver-work-cli-shared/references/json-output-contract.md +61 -0
  88. package/skills/weaver-work-cli-shared/references/weaver-work-cli-installation.md +61 -0
  89. package/templates/shortcut/index.ts.tpl +19 -0
  90. package/templates/shortcut/operations/ping.ts.tpl +7 -0
@@ -0,0 +1,126 @@
1
+ import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
2
+ import { existsSync, mkdirSync, readFileSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { resolve } from 'node:path';
5
+ import { Entry as KeyringEntry } from '@napi-rs/keyring';
6
+ import { writeWithRetry } from '../../../core/write-with-retry.js';
7
+ import { getE10AuthRoot } from './paths.js';
8
+ const ALGORITHM = 'aes-256-gcm';
9
+ const IV_LENGTH = 12;
10
+ const TAG_LENGTH = 16;
11
+ const KEYCHAIN_SERVICE = 'weaver-work-cli';
12
+ const LEGACY_KEYCHAIN_SERVICE = 'e10-cli';
13
+ const KEYCHAIN_ACCOUNT = 'auth-key';
14
+ let encKey = null;
15
+ function getKeyFilePath() {
16
+ return resolve(getE10AuthRoot(), '.key');
17
+ }
18
+ function getLegacyKeyFilePath() {
19
+ return resolve(homedir(), '.e10-cli', '.key');
20
+ }
21
+ function readKeychainKey(service) {
22
+ try {
23
+ const entry = new KeyringEntry(service, KEYCHAIN_ACCOUNT);
24
+ const stored = entry.getPassword();
25
+ if (stored && /^[A-Za-z0-9+/=]{40,}$/.test(stored)) {
26
+ const key = Buffer.from(stored, 'base64');
27
+ if (key.length === 32)
28
+ return key;
29
+ }
30
+ }
31
+ catch {
32
+ // Keychain may be unavailable in headless environments.
33
+ }
34
+ return null;
35
+ }
36
+ function readKeyFile(filePath) {
37
+ if (!existsSync(filePath))
38
+ return null;
39
+ try {
40
+ const key = Buffer.from(readFileSync(filePath, 'utf-8').trim(), 'base64');
41
+ return key.length === 32 ? key : null;
42
+ }
43
+ catch {
44
+ return null;
45
+ }
46
+ }
47
+ function getEncKey() {
48
+ if (encKey)
49
+ return encKey;
50
+ const storedKey = readKeychainKey(KEYCHAIN_SERVICE);
51
+ if (storedKey) {
52
+ encKey = storedKey;
53
+ return encKey;
54
+ }
55
+ try {
56
+ const fresh = randomBytes(32);
57
+ const entry = new KeyringEntry(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
58
+ entry.setPassword(fresh.toString('base64'));
59
+ encKey = fresh;
60
+ return encKey;
61
+ }
62
+ catch {
63
+ // Keychain may be unavailable in headless environments; fall back to a 0600 file.
64
+ }
65
+ const keyFile = getKeyFilePath();
66
+ const fileKey = readKeyFile(keyFile);
67
+ if (fileKey) {
68
+ encKey = fileKey;
69
+ return encKey;
70
+ }
71
+ encKey = randomBytes(32);
72
+ mkdirSync(getE10AuthRoot(), { recursive: true });
73
+ writeWithRetry(keyFile, encKey.toString('base64'), { mode: 0o600 });
74
+ return encKey;
75
+ }
76
+ function getDecryptKeys() {
77
+ const keys = [getEncKey()];
78
+ const legacyKey = readKeychainKey(LEGACY_KEYCHAIN_SERVICE) || readKeyFile(getLegacyKeyFilePath());
79
+ if (legacyKey && !keys.some((key) => key.equals(legacyKey)))
80
+ keys.push(legacyKey);
81
+ return keys;
82
+ }
83
+ export function encryptAuthData(plaintext) {
84
+ const iv = randomBytes(IV_LENGTH);
85
+ const cipher = createCipheriv(ALGORITHM, getEncKey(), iv, { authTagLength: TAG_LENGTH });
86
+ const encrypted = Buffer.concat([cipher.update(plaintext, 'utf-8'), cipher.final()]);
87
+ const tag = cipher.getAuthTag();
88
+ const combined = Buffer.concat([iv, encrypted, tag]);
89
+ return { enc: true, v: 1, data: combined.toString('base64') };
90
+ }
91
+ export function decryptAuthData(wrapper) {
92
+ const combined = Buffer.from(wrapper.data, 'base64');
93
+ if (combined.length < IV_LENGTH + TAG_LENGTH + 1)
94
+ return null;
95
+ const iv = combined.subarray(0, IV_LENGTH);
96
+ const tag = combined.subarray(combined.length - TAG_LENGTH);
97
+ const encrypted = combined.subarray(IV_LENGTH, combined.length - TAG_LENGTH);
98
+ for (const key of getDecryptKeys()) {
99
+ try {
100
+ const decipher = createDecipheriv(ALGORITHM, key, iv, { authTagLength: TAG_LENGTH });
101
+ decipher.setAuthTag(tag);
102
+ return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString('utf-8');
103
+ }
104
+ catch {
105
+ // Try the next known key.
106
+ }
107
+ }
108
+ return null;
109
+ }
110
+ export function parseAuthFile(raw) {
111
+ try {
112
+ const parsed = JSON.parse(raw);
113
+ if (parsed.enc === true && typeof parsed.data === 'string') {
114
+ const decrypted = decryptAuthData(parsed);
115
+ if (!decrypted)
116
+ return null;
117
+ return JSON.parse(decrypted);
118
+ }
119
+ if (parsed.cookies && parsed.userId)
120
+ return parsed;
121
+ return null;
122
+ }
123
+ catch {
124
+ return null;
125
+ }
126
+ }
@@ -0,0 +1,12 @@
1
+ import { homedir } from 'node:os';
2
+ import { resolve } from 'node:path';
3
+ export function getE10AuthRoot() {
4
+ const override = process.env.WEAVER_WORK_CLI_AUTH_ROOT?.trim();
5
+ return override ? resolve(override) : resolve(homedir(), '.weaver-work-cli', 'e10');
6
+ }
7
+ export function getE10ProfilesRoot() {
8
+ return resolve(getE10AuthRoot(), 'profiles');
9
+ }
10
+ export function getE10ActiveProfilePath() {
11
+ return resolve(getE10AuthRoot(), 'profile');
12
+ }
@@ -0,0 +1,449 @@
1
+ import { constants, createDecipheriv, createHash, publicEncrypt } from 'node:crypto';
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync } from 'node:fs';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { CliError, ExitCode } from '../../../core/errors.js';
5
+ import { writeWithRetry } from '../../../core/write-with-retry.js';
6
+ import { encryptAuthData, parseAuthFile } from './crypto.js';
7
+ import { getE10ActiveProfilePath, getE10ProfilesRoot } from './paths.js';
8
+ const RSA_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
9
+ MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCEOk/WyyJKdAHIfMODFnKg7MBoraFBnL3F18hhLx5n01J5g4hCkmpc5+KQFKIQIA/QWk9m7QBzI/OV9ZDFk54LaWm3xP8C+P1TPp7UVGz3iNDLdvgSxuGEYPmML3NaqkbVlvW7/NlNA3ED4Kao7rR5GUYSdFfbaBoxBFae638hzQIDAQAB
10
+ -----END PUBLIC KEY-----`;
11
+ const AES_SEED = 'yoJXYSzZhAOpbEFJklGtqd74X2ChTKOL';
12
+ function sha1(data) {
13
+ return createHash('sha1').update(data).digest();
14
+ }
15
+ function getAesKey() {
16
+ const h = sha1(sha1(Buffer.from(AES_SEED))).toString('hex').slice(0, 32);
17
+ return Buffer.from(h, 'hex');
18
+ }
19
+ function decryptAES(key, encryptedHex) {
20
+ const encrypted = Buffer.from(encryptedHex, 'hex');
21
+ const decipher = createDecipheriv('aes-128-ecb', key, null);
22
+ decipher.setAutoPadding(true);
23
+ return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString('utf-8');
24
+ }
25
+ async function fetchPublicKey(baseUrl, passportUrl) {
26
+ try {
27
+ const url = `${passportUrl}/papi/passport/info/getUrlInfo?type=login&service=${encodeURIComponent(baseUrl)}`;
28
+ const resp = await fetch(url, {
29
+ headers: {
30
+ accept: 'application/json',
31
+ langtype: 'zh_CN',
32
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
33
+ },
34
+ });
35
+ if (resp.status !== 200)
36
+ return null;
37
+ const data = await resp.json();
38
+ if (data.code !== 200 || !data.data?.publicKey)
39
+ return null;
40
+ const decrypted = decryptAES(getAesKey(), data.data.publicKey);
41
+ return '-----BEGIN PUBLIC KEY-----\n' + decrypted + '\n-----END PUBLIC KEY-----';
42
+ }
43
+ catch {
44
+ return null;
45
+ }
46
+ }
47
+ export function getActiveProfile() {
48
+ const activeProfileFile = getE10ActiveProfilePath();
49
+ try {
50
+ if (existsSync(activeProfileFile)) {
51
+ const name = readFileSync(activeProfileFile, 'utf-8').trim();
52
+ if (name)
53
+ return name;
54
+ }
55
+ }
56
+ catch {
57
+ // Fall through to default.
58
+ }
59
+ return 'default';
60
+ }
61
+ export function setActiveProfile(name) {
62
+ mkdirSync(getE10ProfilesRoot(), { recursive: true });
63
+ mkdirSync(resolve(getE10ProfilesRoot(), name), { recursive: true });
64
+ writeWithRetry(getE10ActiveProfilePath(), name, 'utf-8');
65
+ }
66
+ export function listProfiles() {
67
+ try {
68
+ if (!existsSync(getE10ProfilesRoot()))
69
+ return [];
70
+ return readdirSync(getE10ProfilesRoot(), { withFileTypes: true })
71
+ .filter((entry) => entry.isDirectory())
72
+ .map((entry) => entry.name);
73
+ }
74
+ catch {
75
+ return [];
76
+ }
77
+ }
78
+ export function getProfileDir(name) {
79
+ return resolve(getE10ProfilesRoot(), name);
80
+ }
81
+ export function getProfileAuthPath(name) {
82
+ return resolve(getProfileDir(name), 'auth');
83
+ }
84
+ export function getProfileConfigPath(name) {
85
+ return resolve(getProfileDir(name), 'config.json');
86
+ }
87
+ export function getActiveConfigPath() {
88
+ return getProfileConfigPath(getActiveProfile());
89
+ }
90
+ export function profileNameFromUrl(baseUrl) {
91
+ try {
92
+ const host = new URL(baseUrl).hostname;
93
+ const parts = host.split('.');
94
+ return parts.length >= 2 ? parts[parts.length - 2] : host;
95
+ }
96
+ catch {
97
+ return 'default';
98
+ }
99
+ }
100
+ export function readProfileConfig(profile = getActiveProfile()) {
101
+ const configPath = getProfileConfigPath(profile);
102
+ if (existsSync(configPath)) {
103
+ try {
104
+ return JSON.parse(readFileSync(configPath, 'utf-8'));
105
+ }
106
+ catch {
107
+ return {};
108
+ }
109
+ }
110
+ return {};
111
+ }
112
+ export function saveProfileConfig(baseUrl, passportUrl, profile = getActiveProfile(), tenantKey) {
113
+ const configPath = getProfileConfigPath(profile);
114
+ mkdirSync(dirname(configPath), { recursive: true });
115
+ writeWithRetry(configPath, JSON.stringify({ baseUrl, passportUrl, tenantKey }, null, 2) + '\n', { mode: 0o600 });
116
+ }
117
+ export function saveAuthFile(data, profile = getActiveProfile()) {
118
+ const authPath = getProfileAuthPath(profile);
119
+ mkdirSync(dirname(authPath), { recursive: true });
120
+ writeWithRetry(authPath, JSON.stringify(encryptAuthData(JSON.stringify(data))), { mode: 0o600 });
121
+ return authPath;
122
+ }
123
+ export function clearProfileAuthFile(profile = getActiveProfile()) {
124
+ const authFile = getProfileAuthPath(profile);
125
+ if (!existsSync(authFile))
126
+ return { profile, authFile, cleared: false };
127
+ unlinkSync(authFile);
128
+ return { profile, authFile, cleared: true };
129
+ }
130
+ export function findAuthFile(startDir = process.cwd(), options = {}) {
131
+ if (options.auth)
132
+ return options.auth;
133
+ if (options.profile) {
134
+ const profileAuth = getProfileAuthPath(options.profile);
135
+ if (existsSync(profileAuth))
136
+ return profileAuth;
137
+ }
138
+ const envAuth = process.env.E10_AUTH_PATH;
139
+ if (envAuth && existsSync(envAuth))
140
+ return envAuth;
141
+ const activeProfileAuth = getProfileAuthPath(getActiveProfile());
142
+ if (existsSync(activeProfileAuth))
143
+ return activeProfileAuth;
144
+ return null;
145
+ }
146
+ function asRecord(value) {
147
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
148
+ }
149
+ export function readTeamsCheckUserInfo(resp, data) {
150
+ const root = asRecord(data);
151
+ const nested = asRecord(root.data);
152
+ const currentUser = asRecord(root.currentUser || nested.currentUser);
153
+ const currentTenant = asRecord(root.currentTenant || nested.currentTenant);
154
+ const department = asRecord(currentUser.department);
155
+ const position = asRecord(currentUser.position);
156
+ const userId = resp.headers.get('employeeId')
157
+ || currentUser.employeeId
158
+ || currentUser.id
159
+ || nested.employeeId
160
+ || root.employeeId
161
+ || '';
162
+ const tenantKey = currentUser.tenantKey
163
+ || currentTenant.tenantKey
164
+ || nested.tenantKey
165
+ || nested.tenantkey
166
+ || root.tenantKey
167
+ || root.tenantkey
168
+ || '';
169
+ return {
170
+ userId: String(userId || ''),
171
+ tenantKey: String(tenantKey || ''),
172
+ tenantName: String(currentTenant.tenantName || currentTenant.name || ''),
173
+ department: String(department.name || ''),
174
+ subcompany: String(root.subCompanyName || nested.subCompanyName || ''),
175
+ position: String(position.name || ''),
176
+ };
177
+ }
178
+ export function buildE10CookieHeader(rawCookie, eteamsId, agentType = '') {
179
+ let cookie = rawCookie || '';
180
+ if (cookie) {
181
+ if (/ETEAMSID=/.test(cookie)) {
182
+ cookie = cookie.replace(/ETEAMSID=[^;]+/, `ETEAMSID=${eteamsId}`);
183
+ }
184
+ else if (eteamsId) {
185
+ cookie += `; ETEAMSID=${eteamsId}`;
186
+ }
187
+ }
188
+ else if (eteamsId) {
189
+ cookie = `ETEAMSID=${eteamsId}`;
190
+ }
191
+ if (agentType && !/agentType=/.test(cookie)) {
192
+ cookie = cookie ? `${cookie}; agentType=${agentType}` : `agentType=${agentType}`;
193
+ }
194
+ if (!/isAgent=/.test(cookie)) {
195
+ cookie = cookie ? `${cookie}; isAgent=true` : 'isAgent=true';
196
+ }
197
+ return cookie;
198
+ }
199
+ function splitSetCookie(value) {
200
+ return value.split(/,\s*(?=[^;,]+=)/).filter(Boolean);
201
+ }
202
+ export function extractRawSetCookie(resp) {
203
+ const headers = resp.headers;
204
+ const setCookies = headers.getSetCookie?.() || splitSetCookie(headers.get('set-cookie') || '');
205
+ return setCookies.map((cookie) => cookie.split(';')[0]).filter(Boolean).join('; ');
206
+ }
207
+ export class E10AuthSession {
208
+ filePath;
209
+ data;
210
+ constructor(data, filePath = null, baseUrlFallback, passportUrlFallback) {
211
+ this.filePath = filePath;
212
+ this.data = data;
213
+ this.data.baseUrl = this.data.baseUrl || baseUrlFallback || '';
214
+ this.data.passportUrl = this.data.passportUrl || passportUrlFallback || this.data.baseUrl;
215
+ }
216
+ get cookies() {
217
+ return this.data.cookies || {};
218
+ }
219
+ get eteamsId() {
220
+ return this.cookies.ETEAMSID || '';
221
+ }
222
+ get userId() {
223
+ return this.data.userId || '';
224
+ }
225
+ get tenantKey() {
226
+ return this.data.tenantKey || '';
227
+ }
228
+ get tenantName() {
229
+ return this.data.tenantName || '';
230
+ }
231
+ get department() {
232
+ return this.data.department || '';
233
+ }
234
+ get subcompany() {
235
+ return this.data.subcompany || '';
236
+ }
237
+ get position() {
238
+ return this.data.position || '';
239
+ }
240
+ get agentType() {
241
+ return this.data.agentType || this.data.agent_type || '';
242
+ }
243
+ get version() {
244
+ return this.data.version || this.data.baselineVersion || '';
245
+ }
246
+ getBaseUrl() {
247
+ return (this.data.baseUrl || '').replace(/\/$/, '');
248
+ }
249
+ getPassportUrl() {
250
+ return (this.data.passportUrl || this.getBaseUrl()).replace(/\/$/, '');
251
+ }
252
+ cookieHeader() {
253
+ const raw = this.cookies.raw || '';
254
+ return buildE10CookieHeader(raw, this.eteamsId, this.agentType);
255
+ }
256
+ uaHeader() {
257
+ return this.agentType ? `AgentType=${this.agentType},IsAgent=true` : 'e10-login';
258
+ }
259
+ toJSON() {
260
+ return {
261
+ userId: this.userId,
262
+ tenantKey: this.tenantKey,
263
+ tenantName: this.tenantName,
264
+ department: this.department,
265
+ subcompany: this.subcompany,
266
+ position: this.position,
267
+ agentType: this.agentType,
268
+ version: this.version,
269
+ baseUrl: this.getBaseUrl(),
270
+ passportUrl: this.getPassportUrl(),
271
+ stTicket: this.data.stTicket,
272
+ redirectUrl: this.data.redirectUrl,
273
+ cookieSavedAt: this.data.cookieSavedAt,
274
+ mainUrl: this.data.mainUrl,
275
+ hasEteamsId: Boolean(this.eteamsId),
276
+ };
277
+ }
278
+ static fromFile(filePath, baseUrlFallback, passportUrlFallback) {
279
+ try {
280
+ const raw = readFileSync(filePath, 'utf-8');
281
+ const data = parseAuthFile(raw);
282
+ if (!data || !data.cookies?.ETEAMSID || !data.baseUrl)
283
+ return null;
284
+ return new E10AuthSession(data, filePath, baseUrlFallback, passportUrlFallback);
285
+ }
286
+ catch {
287
+ return null;
288
+ }
289
+ }
290
+ static async fromOptions(options, startDir = process.cwd()) {
291
+ let baseUrl = options.baseUrl;
292
+ let passportUrl = options.passportUrl;
293
+ if (options.profile) {
294
+ const cfg = readProfileConfig(options.profile);
295
+ baseUrl ||= cfg.baseUrl;
296
+ passportUrl ||= cfg.passportUrl;
297
+ }
298
+ const authPath = findAuthFile(startDir, options);
299
+ if (!authPath)
300
+ return null;
301
+ const session = E10AuthSession.fromFile(authPath, baseUrl, passportUrl);
302
+ if (session)
303
+ await session.fetchEmployeeId();
304
+ return session;
305
+ }
306
+ async fetchEmployeeId() {
307
+ try {
308
+ const resp = await fetch(`${this.getBaseUrl()}/api/baseserver/layout/teamsCheck?clientType=not_xinchuang&client=WEB&domainName=${encodeURIComponent(this.getBaseUrl())}`, {
309
+ method: 'POST',
310
+ headers: {
311
+ accept: 'application/json',
312
+ 'content-type': 'application/json',
313
+ eteamsid: this.eteamsId,
314
+ cookie: this.cookieHeader(),
315
+ 'user-agent': this.uaHeader(),
316
+ origin: this.getBaseUrl(),
317
+ },
318
+ });
319
+ const data = await resp.json().catch(() => ({}));
320
+ const userInfo = readTeamsCheckUserInfo(resp, data);
321
+ if (userInfo.userId && userInfo.userId !== this.data.userId)
322
+ this.data.userId = userInfo.userId;
323
+ if (userInfo.tenantKey)
324
+ this.data.tenantKey = userInfo.tenantKey;
325
+ if (userInfo.tenantName)
326
+ this.data.tenantName = userInfo.tenantName;
327
+ if (userInfo.department)
328
+ this.data.department = userInfo.department;
329
+ if (userInfo.subcompany)
330
+ this.data.subcompany = userInfo.subcompany;
331
+ if (userInfo.position)
332
+ this.data.position = userInfo.position;
333
+ }
334
+ catch {
335
+ // Non-critical profile enrichment.
336
+ }
337
+ }
338
+ async checkValid() {
339
+ try {
340
+ const resp = await fetch(`${this.getBaseUrl()}/api/ecode/tree/list?parentId=0`, {
341
+ headers: {
342
+ accept: 'application/json',
343
+ eteamsid: this.eteamsId,
344
+ cookie: this.cookieHeader(),
345
+ 'user-agent': this.uaHeader(),
346
+ },
347
+ redirect: 'manual',
348
+ });
349
+ return resp.status === 200;
350
+ }
351
+ catch {
352
+ return false;
353
+ }
354
+ }
355
+ async ensureValid() {
356
+ if (await this.checkValid())
357
+ return true;
358
+ const account = process.env.E10_ACCOUNT;
359
+ const password = process.env.E10_PASSWORD;
360
+ if (!account || !password)
361
+ return false;
362
+ await this.rsaLogin(account, password);
363
+ this.save();
364
+ return true;
365
+ }
366
+ async rsaLogin(account, password) {
367
+ const passport = this.getPassportUrl();
368
+ const base = this.getBaseUrl();
369
+ const pem = (await fetchPublicKey(base, passport)) || RSA_PUBLIC_KEY_PEM;
370
+ const enc = (value) => publicEncrypt({ key: pem, padding: constants.RSA_PKCS1_PADDING }, Buffer.from(value, 'utf-8')).toString('base64');
371
+ const resp = await fetch(`${passport}/papi/passport/login/pclogin`, {
372
+ method: 'POST',
373
+ headers: {
374
+ accept: 'application/json, text/plain, */*',
375
+ 'content-type': 'application/json;charset=UTF-8',
376
+ langtype: 'zh_CN',
377
+ origin: passport,
378
+ },
379
+ body: JSON.stringify({
380
+ account: enc(account),
381
+ password: enc(password),
382
+ service: base,
383
+ tenantKey: 'all_teams',
384
+ teamsInfo: JSON.stringify({ urlSearchParams: `?service=${encodeURIComponent(base)}` }),
385
+ passportCusLoginDomain: passport,
386
+ serviceToken: '',
387
+ auxiliaryToken: '',
388
+ language: 'zh_CN',
389
+ onlySelfTenant: 0,
390
+ loginRedirectUrl: '',
391
+ devType: '72',
392
+ registerDataSource: '',
393
+ registerSource: '',
394
+ registerSourceUrl: '',
395
+ moduleSource: '',
396
+ integratedLoginId: '',
397
+ agreementChecked: '0',
398
+ needPassword: true,
399
+ }),
400
+ redirect: 'manual',
401
+ });
402
+ const result = await resp.json();
403
+ if (result.code !== 200 || !result.status || !result.data?.stTicket) {
404
+ throw new CliError(result.msg || 'E10 RSA login failed', ExitCode.AuthError);
405
+ }
406
+ const ticketResp = await fetch(`${base}/papi/baseserver/layout/checkTicket`, {
407
+ method: 'POST',
408
+ headers: {
409
+ accept: 'application/json',
410
+ 'content-type': 'application/json',
411
+ origin: base,
412
+ },
413
+ body: JSON.stringify({ ticket: result.data.stTicket, service: base }),
414
+ redirect: 'manual',
415
+ });
416
+ const headers = ticketResp.headers;
417
+ const setCookies = headers.getSetCookie?.() || [];
418
+ const cookies = {};
419
+ for (const cookie of setCookies) {
420
+ const match = cookie.match(/^([^=]+)=([^;]+)/);
421
+ if (match)
422
+ cookies[match[1].trim()] = match[2];
423
+ }
424
+ const rawCookie = extractRawSetCookie(ticketResp);
425
+ if (rawCookie)
426
+ cookies.raw = rawCookie;
427
+ const ticketData = await ticketResp.json().catch(() => ({}));
428
+ const userInfo = readTeamsCheckUserInfo(ticketResp, ticketData);
429
+ this.data = {
430
+ cookies,
431
+ userId: userInfo.userId || account,
432
+ tenantKey: userInfo.tenantKey || this.tenantKey,
433
+ tenantName: userInfo.tenantName,
434
+ department: userInfo.department,
435
+ subcompany: userInfo.subcompany,
436
+ position: userInfo.position,
437
+ baseUrl: base,
438
+ passportUrl: passport,
439
+ stTicket: '',
440
+ redirectUrl: '',
441
+ cookieSavedAt: Date.now(),
442
+ };
443
+ }
444
+ save() {
445
+ if (!this.filePath)
446
+ return;
447
+ writeWithRetry(this.filePath, JSON.stringify(encryptAuthData(JSON.stringify(this.data))), { mode: 0o600 });
448
+ }
449
+ }
@@ -0,0 +1,104 @@
1
+ export class E10ApiClient {
2
+ auth;
3
+ retries;
4
+ constructor(auth, retries = 3) {
5
+ this.auth = auth;
6
+ this.retries = retries;
7
+ }
8
+ getBaseUrl() {
9
+ return this.auth.getBaseUrl();
10
+ }
11
+ url(pathOrUrl) {
12
+ if (/^https?:\/\//i.test(pathOrUrl))
13
+ return pathOrUrl;
14
+ return this.getBaseUrl() + (pathOrUrl.startsWith('/') ? pathOrUrl : '/' + pathOrUrl);
15
+ }
16
+ defaultHeaders() {
17
+ return {
18
+ accept: 'application/json, text/plain, */*',
19
+ langtype: 'zh_CN',
20
+ eteamsid: this.auth.eteamsId,
21
+ cookie: this.auth.cookieHeader(),
22
+ 'user-agent': this.auth.uaHeader(),
23
+ };
24
+ }
25
+ async get(url, extraHeaders = {}) {
26
+ return this.request('GET', url, undefined, extraHeaders);
27
+ }
28
+ async post(url, body, extraHeaders = {}) {
29
+ return this.request('POST', url, body, extraHeaders);
30
+ }
31
+ async postForm(url, formData, extraHeaders = {}) {
32
+ const headers = {
33
+ ...this.defaultHeaders(),
34
+ 'content-type': 'application/x-www-form-urlencoded',
35
+ ...extraHeaders,
36
+ };
37
+ return this.requestRaw(this.url(url), { method: 'POST', headers, body: String(formData) });
38
+ }
39
+ async postMultipart(url, form, extraHeaders = {}) {
40
+ const headers = { ...this.defaultHeaders(), ...extraHeaders };
41
+ return this.requestRaw(this.url(url), { method: 'POST', headers, body: form });
42
+ }
43
+ async request(method, url, body, extraHeaders = {}) {
44
+ const headers = { ...this.defaultHeaders(), ...extraHeaders };
45
+ let fetchBody;
46
+ if (body !== undefined && body !== null) {
47
+ if (typeof body === 'object' && !(body instanceof FormData) && !(body instanceof URLSearchParams)) {
48
+ headers['content-type'] = 'application/json;charset=UTF-8';
49
+ fetchBody = JSON.stringify(body);
50
+ }
51
+ else {
52
+ fetchBody = body;
53
+ }
54
+ }
55
+ return this.requestRaw(this.url(url), { method, headers, body: fetchBody });
56
+ }
57
+ async requestRaw(url, init) {
58
+ let lastError = null;
59
+ for (let attempt = 1; attempt <= this.retries; attempt++) {
60
+ try {
61
+ const resp = await fetch(url, { ...init, redirect: 'manual' });
62
+ if (resp.status === 302) {
63
+ const ok = await this.auth.ensureValid();
64
+ if (!ok)
65
+ throw new Error('E10 session expired; please login again');
66
+ const headers = (init.headers || {});
67
+ headers.eteamsid = this.auth.eteamsId;
68
+ headers.cookie = this.auth.cookieHeader();
69
+ headers['user-agent'] = this.auth.uaHeader();
70
+ init.headers = headers;
71
+ continue;
72
+ }
73
+ const text = await resp.text();
74
+ if (!text)
75
+ return null;
76
+ try {
77
+ return JSON.parse(text);
78
+ }
79
+ catch {
80
+ return text;
81
+ }
82
+ }
83
+ catch (error) {
84
+ lastError = error instanceof Error ? error : new Error(String(error));
85
+ if (attempt < this.retries) {
86
+ const delay = Math.min(1000 * Math.pow(2, attempt - 1), 8000);
87
+ await new Promise((resolve) => setTimeout(resolve, delay));
88
+ }
89
+ }
90
+ }
91
+ throw lastError || new Error('E10 API request failed');
92
+ }
93
+ static buildMultipart(fields) {
94
+ const boundary = `----WebKitFormBoundary${Math.random().toString(36).slice(2, 18)}`;
95
+ let body = '';
96
+ for (const [name, value] of Object.entries(fields)) {
97
+ body += `--${boundary}\r\n`;
98
+ body += `Content-Disposition: form-data; name="${name}"\r\n\r\n`;
99
+ body += `${value}\r\n`;
100
+ }
101
+ body += `--${boundary}--\r\n`;
102
+ return { body, boundary };
103
+ }
104
+ }