@share-crm/sharecrm-cli 1.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.
- package/README.md +293 -0
- package/dist/cli/parser.js +79 -0
- package/dist/cli/root.js +63 -0
- package/dist/cli/router.js +32 -0
- package/dist/commands/auth/login.js +34 -0
- package/dist/commands/auth/logout.js +12 -0
- package/dist/commands/auth/status.js +41 -0
- package/dist/commands/auth/token.js +70 -0
- package/dist/commands/config/init.js +28 -0
- package/dist/commands/help/help.js +174 -0
- package/dist/commands/remote/execute.js +131 -0
- package/dist/core/auth/authTypes.js +2 -0
- package/dist/core/auth/deviceFlow.js +77 -0
- package/dist/core/auth/tokenManager.js +45 -0
- package/dist/core/cache/cacheTypes.js +2 -0
- package/dist/core/cache/commandCache.js +24 -0
- package/dist/core/config/authBaseUrl.js +11 -0
- package/dist/core/config/envPersistence.js +59 -0
- package/dist/core/config/interactive.js +60 -0
- package/dist/core/config/locale.js +9 -0
- package/dist/core/debug/debugOutput.js +18 -0
- package/dist/core/debug/runtimeDebug.js +19 -0
- package/dist/core/http/apiClient.js +320 -0
- package/dist/core/http/requestTypes.js +2 -0
- package/dist/core/output/errors.js +44 -0
- package/dist/core/output/stderr.js +6 -0
- package/dist/core/output/stdout.js +6 -0
- package/dist/core/state/authSessionStore.js +129 -0
- package/dist/core/state/authSessionTypes.js +2 -0
- package/dist/core/state/configStore.js +65 -0
- package/dist/core/state/fileLock.js +66 -0
- package/dist/core/state/legacySessionMigration.js +109 -0
- package/dist/core/state/paths.js +40 -0
- package/dist/core/state/secretStore/commonFileCrypto.js +61 -0
- package/dist/core/state/secretStore/index.js +28 -0
- package/dist/core/state/secretStore/secretStore.darwin.js +139 -0
- package/dist/core/state/secretStore/secretStore.linux.js +90 -0
- package/dist/core/state/secretStore/secretStore.unsupported.js +17 -0
- package/dist/core/state/secretStore/secretStore.win32.js +162 -0
- package/dist/core/state/secretStore/types.js +2 -0
- package/dist/core/state/sessionMetaStore.js +24 -0
- package/dist/core/state/sessionStore.js +23 -0
- package/dist/index.js +49 -0
- package/dist/shared/constants.js +13 -0
- package/dist/shared/env.js +69 -0
- package/dist/shared/generatedConfig.js +9 -0
- package/dist/shared/utils.js +14 -0
- package/dist/types/command.js +2 -0
- package/package.json +40 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ApiClient = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const tokenManager_1 = require("../auth/tokenManager");
|
|
6
|
+
const configStore_1 = require("../state/configStore");
|
|
7
|
+
const sessionStore_1 = require("../state/sessionStore");
|
|
8
|
+
const errors_1 = require("../output/errors");
|
|
9
|
+
const runtimeDebug_1 = require("../debug/runtimeDebug");
|
|
10
|
+
const debugOutput_1 = require("../debug/debugOutput");
|
|
11
|
+
const env_1 = require("../../shared/env");
|
|
12
|
+
const locale_1 = require("../config/locale");
|
|
13
|
+
class ApiClient {
|
|
14
|
+
configStore;
|
|
15
|
+
constructor(configStore = new configStore_1.ConfigStore()) {
|
|
16
|
+
this.configStore = configStore;
|
|
17
|
+
}
|
|
18
|
+
async _fetch(input, init) {
|
|
19
|
+
const urlStr = input instanceof URL ? input.toString() : input;
|
|
20
|
+
if ((0, runtimeDebug_1.isDebugEnabled)()) {
|
|
21
|
+
(0, debugOutput_1.writeDebugRequest)({ url: urlStr, method: String(init.method ?? 'GET'), headers: (init.headers ?? {}), body: init.body });
|
|
22
|
+
}
|
|
23
|
+
let response;
|
|
24
|
+
try {
|
|
25
|
+
response = await fetch(input, init);
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
if ((0, runtimeDebug_1.isDebugEnabled)()) {
|
|
29
|
+
(0, debugOutput_1.writeDebugException)({ name: err.name, message: err.message, stack: err.stack });
|
|
30
|
+
}
|
|
31
|
+
throw err;
|
|
32
|
+
}
|
|
33
|
+
let bodyText;
|
|
34
|
+
try {
|
|
35
|
+
bodyText = await response.text();
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
if ((0, runtimeDebug_1.isDebugEnabled)()) {
|
|
39
|
+
(0, debugOutput_1.writeDebugException)({ name: err.name, message: err.message, stack: err.stack });
|
|
40
|
+
}
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
if ((0, runtimeDebug_1.isDebugEnabled)()) {
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(bodyText);
|
|
46
|
+
(0, debugOutput_1.writeDebugResponse)({
|
|
47
|
+
url: urlStr,
|
|
48
|
+
status: response.status,
|
|
49
|
+
statusText: response.statusText,
|
|
50
|
+
headers: {},
|
|
51
|
+
body: parsed,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
(0, debugOutput_1.writeDebugException)({ name: err.name, message: err.message, stack: err.stack });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { bodyText, status: response.status, statusText: response.statusText, url: urlStr };
|
|
59
|
+
}
|
|
60
|
+
async request(options) {
|
|
61
|
+
const environment = await (0, env_1.resolveEnvironment)();
|
|
62
|
+
if (!environment.authBaseUrl) {
|
|
63
|
+
throw new errors_1.CliError('FS_CLI_AUTH_BASE_URL is required for auth API requests.', 1);
|
|
64
|
+
}
|
|
65
|
+
if (!environment.authBaseUrl.startsWith('https://')) {
|
|
66
|
+
throw new errors_1.CliError('FS_CLI_AUTH_BASE_URL 必须使用 HTTPS 协议。', 1);
|
|
67
|
+
}
|
|
68
|
+
const config = await this.configStore.initialize();
|
|
69
|
+
const locale = config.locale ?? environment.locale;
|
|
70
|
+
const { bodyText, status, statusText, url: requestUrl } = await this._fetch(new URL(options.path, environment.authBaseUrl), {
|
|
71
|
+
method: options.method ?? 'POST',
|
|
72
|
+
headers: {
|
|
73
|
+
'content-type': 'application/json',
|
|
74
|
+
'X-fs-cli-id': config.cliId,
|
|
75
|
+
'X-fs-cli-version': config.cliVersion,
|
|
76
|
+
'X-fs-source': 'cli',
|
|
77
|
+
'Accept-Language': locale,
|
|
78
|
+
'X-FS-Locale': locale,
|
|
79
|
+
...(options.headers ?? {}),
|
|
80
|
+
},
|
|
81
|
+
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
|
82
|
+
});
|
|
83
|
+
let payload;
|
|
84
|
+
try {
|
|
85
|
+
payload = JSON.parse(bodyText);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
const err = new errors_1.CliError('Failed to parse auth API response as JSON.', 1, {
|
|
89
|
+
url: requestUrl,
|
|
90
|
+
status,
|
|
91
|
+
statusText,
|
|
92
|
+
path: options.path,
|
|
93
|
+
responsePreview: bodyText.slice(0, 500),
|
|
94
|
+
});
|
|
95
|
+
if ((0, runtimeDebug_1.isDebugEnabled)()) {
|
|
96
|
+
(0, debugOutput_1.writeDebugException)({ name: err.name, message: err.message, exitCode: err.exitCode, details: err.details });
|
|
97
|
+
}
|
|
98
|
+
throw err;
|
|
99
|
+
}
|
|
100
|
+
if (payload.errorCode !== 0) {
|
|
101
|
+
const err = new errors_1.ApiResponseCliError(payload.errorMessage, payload.errorCode, {
|
|
102
|
+
traceId: payload.traceId,
|
|
103
|
+
path: options.path,
|
|
104
|
+
});
|
|
105
|
+
if ((0, runtimeDebug_1.isDebugEnabled)()) {
|
|
106
|
+
(0, debugOutput_1.writeDebugException)({ name: err.name, message: err.message, exitCode: err.exitCode, errorCode: err.errorCode, details: err.details });
|
|
107
|
+
}
|
|
108
|
+
throw err;
|
|
109
|
+
}
|
|
110
|
+
return payload;
|
|
111
|
+
}
|
|
112
|
+
async requestDeviceCode() {
|
|
113
|
+
return this.request({ path: '/oauth2.0/device/code' });
|
|
114
|
+
}
|
|
115
|
+
async requestDeviceToken(deviceCode) {
|
|
116
|
+
return this.request({
|
|
117
|
+
path: '/oauth2.0/deviceToken',
|
|
118
|
+
body: { deviceCode },
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
async refreshToken(refreshToken, appId) {
|
|
122
|
+
return this.request({
|
|
123
|
+
path: '/oauth2.0/token',
|
|
124
|
+
body: {
|
|
125
|
+
appId,
|
|
126
|
+
grantType: 'refresh_token',
|
|
127
|
+
refreshToken,
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
async fetchCommandManifest(session, commandKey) {
|
|
132
|
+
const current = await this.ensureValidSession(session);
|
|
133
|
+
return this.withAuthRetry(current, (s) => this.fetchCommandManifestOnce(s, commandKey));
|
|
134
|
+
}
|
|
135
|
+
async executeRemoteCommand(session, commandKey, argumentsPayload) {
|
|
136
|
+
const current = await this.ensureValidSession(session);
|
|
137
|
+
return this.withAuthRetry(current, (s) => this.executeRemoteCommandOnce(s, commandKey, argumentsPayload));
|
|
138
|
+
}
|
|
139
|
+
async ensureValidSession(session) {
|
|
140
|
+
const tm = new tokenManager_1.TokenManager();
|
|
141
|
+
if (!tm.shouldRefresh(session)) {
|
|
142
|
+
return session;
|
|
143
|
+
}
|
|
144
|
+
if (!session.refreshToken || !session.appId) {
|
|
145
|
+
return session;
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
const refreshed = await tm.refreshSession(session);
|
|
149
|
+
if (refreshed.accessToken === session.accessToken) {
|
|
150
|
+
return session;
|
|
151
|
+
}
|
|
152
|
+
return refreshed;
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
if (error instanceof tokenManager_1.SessionRefreshPersistenceError && error.session.accessToken !== session.accessToken) {
|
|
156
|
+
return error.session;
|
|
157
|
+
}
|
|
158
|
+
return session;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async fetchCommandManifestOnce(session, commandKey) {
|
|
162
|
+
const url = await this.createCommandUrl(session, '/api/v1/commands/manifest');
|
|
163
|
+
if (commandKey) {
|
|
164
|
+
url.searchParams.set('commandKey', commandKey);
|
|
165
|
+
}
|
|
166
|
+
const headers = await this.buildCommandHeaders(session);
|
|
167
|
+
const { bodyText, status, statusText, url: requestUrl } = await this._fetch(url, {
|
|
168
|
+
method: 'GET',
|
|
169
|
+
headers,
|
|
170
|
+
});
|
|
171
|
+
let payload;
|
|
172
|
+
try {
|
|
173
|
+
payload = JSON.parse(bodyText);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
const err = new errors_1.CliError('Failed to parse command manifest response as JSON.', 1, {
|
|
177
|
+
url: requestUrl,
|
|
178
|
+
status,
|
|
179
|
+
statusText,
|
|
180
|
+
commandKey,
|
|
181
|
+
responsePreview: bodyText.slice(0, 500),
|
|
182
|
+
});
|
|
183
|
+
if ((0, runtimeDebug_1.isDebugEnabled)()) {
|
|
184
|
+
(0, debugOutput_1.writeDebugException)({ name: err.name, message: err.message, exitCode: err.exitCode, details: err.details });
|
|
185
|
+
}
|
|
186
|
+
throw err;
|
|
187
|
+
}
|
|
188
|
+
if (payload.resultCode !== 'SUCCESS') {
|
|
189
|
+
const err = new errors_1.CliError('Failed to fetch command manifest.', 1, {
|
|
190
|
+
traceId: payload.traceId,
|
|
191
|
+
commandKey,
|
|
192
|
+
resultCode: payload.resultCode,
|
|
193
|
+
remoteError: payload.data,
|
|
194
|
+
});
|
|
195
|
+
if ((0, runtimeDebug_1.isDebugEnabled)()) {
|
|
196
|
+
(0, debugOutput_1.writeDebugException)({ name: err.name, message: err.message, exitCode: err.exitCode, details: err.details });
|
|
197
|
+
}
|
|
198
|
+
throw err;
|
|
199
|
+
}
|
|
200
|
+
return payload.data;
|
|
201
|
+
}
|
|
202
|
+
async executeRemoteCommandOnce(session, commandKey, argumentsPayload) {
|
|
203
|
+
const headers = await this.buildCommandHeaders(session);
|
|
204
|
+
const { bodyText, status, statusText, url: requestUrl } = await this._fetch(await this.createCommandUrl(session, '/api/v1/commands/execute'), {
|
|
205
|
+
method: 'POST',
|
|
206
|
+
headers,
|
|
207
|
+
body: JSON.stringify({
|
|
208
|
+
commandKey,
|
|
209
|
+
arguments: argumentsPayload,
|
|
210
|
+
}),
|
|
211
|
+
});
|
|
212
|
+
let payload;
|
|
213
|
+
try {
|
|
214
|
+
payload = JSON.parse(bodyText);
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
const err = new errors_1.CliError('Failed to parse remote command response as JSON.', 1, {
|
|
218
|
+
url: requestUrl,
|
|
219
|
+
status,
|
|
220
|
+
statusText,
|
|
221
|
+
commandKey,
|
|
222
|
+
responsePreview: bodyText.slice(0, 500),
|
|
223
|
+
});
|
|
224
|
+
if ((0, runtimeDebug_1.isDebugEnabled)()) {
|
|
225
|
+
(0, debugOutput_1.writeDebugException)({ name: err.name, message: err.message, exitCode: err.exitCode, details: err.details });
|
|
226
|
+
}
|
|
227
|
+
throw err;
|
|
228
|
+
}
|
|
229
|
+
if (payload.resultCode !== 'SUCCESS') {
|
|
230
|
+
const err = new errors_1.CliError('Failed to execute remote command.', 1, {
|
|
231
|
+
traceId: payload.traceId,
|
|
232
|
+
commandKey,
|
|
233
|
+
resultCode: payload.resultCode,
|
|
234
|
+
remoteError: payload.data,
|
|
235
|
+
});
|
|
236
|
+
if ((0, runtimeDebug_1.isDebugEnabled)()) {
|
|
237
|
+
(0, debugOutput_1.writeDebugException)({ name: err.name, message: err.message, exitCode: err.exitCode, details: err.details });
|
|
238
|
+
}
|
|
239
|
+
throw err;
|
|
240
|
+
}
|
|
241
|
+
return payload.data;
|
|
242
|
+
}
|
|
243
|
+
async withAuthRetry(session, request) {
|
|
244
|
+
try {
|
|
245
|
+
return await request(session);
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
if (!this.isAuthFailError(error)) {
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
if (!session.refreshToken || !session.appId) {
|
|
252
|
+
await this.clearStoredSession();
|
|
253
|
+
throw new errors_1.LoginExpiredCliError();
|
|
254
|
+
}
|
|
255
|
+
let refreshedSession;
|
|
256
|
+
try {
|
|
257
|
+
refreshedSession = await new tokenManager_1.TokenManager().refreshSession(session);
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
if (error instanceof tokenManager_1.SessionRefreshPersistenceError && error.session.accessToken !== session.accessToken) {
|
|
261
|
+
refreshedSession = error.session;
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
await this.clearStoredSession();
|
|
265
|
+
throw new errors_1.LoginExpiredCliError();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (refreshedSession.accessToken === session.accessToken) {
|
|
269
|
+
await this.clearStoredSession();
|
|
270
|
+
throw new errors_1.LoginExpiredCliError();
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
return await request(refreshedSession);
|
|
274
|
+
}
|
|
275
|
+
catch (retryError) {
|
|
276
|
+
if (!this.isAuthFailError(retryError)) {
|
|
277
|
+
throw retryError;
|
|
278
|
+
}
|
|
279
|
+
await this.clearStoredSession();
|
|
280
|
+
throw new errors_1.LoginExpiredCliError();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
async clearStoredSession() {
|
|
285
|
+
try {
|
|
286
|
+
await new sessionStore_1.SessionStore().clear();
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
// Ignore storage cleanup failure and still surface login expiration.
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
isAuthFailError(error) {
|
|
293
|
+
return error instanceof errors_1.CliError && error.details?.resultCode === 'AUTH_FAIL';
|
|
294
|
+
}
|
|
295
|
+
async createCommandUrl(session, path) {
|
|
296
|
+
const baseUrl = session.apiUrl ?? (await (0, env_1.resolveEnvironment)()).apiBaseUrl;
|
|
297
|
+
if (!baseUrl) {
|
|
298
|
+
throw new errors_1.CliError('Current session is missing apiUrl.', 1);
|
|
299
|
+
}
|
|
300
|
+
if (!baseUrl.startsWith('https://')) {
|
|
301
|
+
throw new errors_1.CliError('API 地址必须使用 HTTPS 协议。', 1);
|
|
302
|
+
}
|
|
303
|
+
return new URL(path.replace(/^\//, ''), baseUrl);
|
|
304
|
+
}
|
|
305
|
+
async buildCommandHeaders(session) {
|
|
306
|
+
const config = await this.configStore.initialize();
|
|
307
|
+
const locale = config.locale ?? locale_1.DEFAULT_LOCALE;
|
|
308
|
+
return {
|
|
309
|
+
'Content-Type': 'application/json',
|
|
310
|
+
Authorization: `Bearer ${session.accessToken}`,
|
|
311
|
+
'X-CLI-Id': config.cliId,
|
|
312
|
+
'X-CLI-Version': config.cliVersion,
|
|
313
|
+
'Accept-Language': locale,
|
|
314
|
+
'X-FS-Locale': locale,
|
|
315
|
+
'X-Request-Id': (0, node_crypto_1.randomUUID)(),
|
|
316
|
+
'X-Caller-Type': 'cli',
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
exports.ApiClient = ApiClient;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ApiResponseCliError = exports.LoginExpiredCliError = exports.AuthenticationRequiredCliError = exports.NotImplementedCliError = exports.CliError = void 0;
|
|
4
|
+
class CliError extends Error {
|
|
5
|
+
exitCode;
|
|
6
|
+
details;
|
|
7
|
+
constructor(message, exitCode = 1, details) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'CliError';
|
|
10
|
+
this.exitCode = exitCode;
|
|
11
|
+
this.details = details;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
exports.CliError = CliError;
|
|
15
|
+
class NotImplementedCliError extends CliError {
|
|
16
|
+
constructor(feature) {
|
|
17
|
+
super(`${feature} is not implemented yet.`, 2, { feature });
|
|
18
|
+
this.name = 'NotImplementedCliError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
exports.NotImplementedCliError = NotImplementedCliError;
|
|
22
|
+
class AuthenticationRequiredCliError extends CliError {
|
|
23
|
+
constructor() {
|
|
24
|
+
super('You must login before executing remote commands.\nsharecrm auth login', 1);
|
|
25
|
+
this.name = 'AuthenticationRequiredCliError';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
exports.AuthenticationRequiredCliError = AuthenticationRequiredCliError;
|
|
29
|
+
class LoginExpiredCliError extends CliError {
|
|
30
|
+
constructor() {
|
|
31
|
+
super('\n提示:登录状态已失效,请重新登录授权。\n执行命令:sharecrm auth login', 1);
|
|
32
|
+
this.name = 'LoginExpiredCliError';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
exports.LoginExpiredCliError = LoginExpiredCliError;
|
|
36
|
+
class ApiResponseCliError extends CliError {
|
|
37
|
+
errorCode;
|
|
38
|
+
constructor(message, errorCode, details) {
|
|
39
|
+
super(message, 1, details);
|
|
40
|
+
this.name = 'ApiResponseCliError';
|
|
41
|
+
this.errorCode = errorCode;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
exports.ApiResponseCliError = ApiResponseCliError;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AuthSessionStore = void 0;
|
|
4
|
+
const legacySessionMigration_1 = require("./legacySessionMigration");
|
|
5
|
+
const fileLock_1 = require("./fileLock");
|
|
6
|
+
const secretStore_1 = require("./secretStore");
|
|
7
|
+
const sessionMetaStore_1 = require("./sessionMetaStore");
|
|
8
|
+
class AuthSessionStore {
|
|
9
|
+
metaStore;
|
|
10
|
+
secretStore;
|
|
11
|
+
constructor(metaStore = new sessionMetaStore_1.SessionMetaStore(), secretStore = (0, secretStore_1.createSecretStore)()) {
|
|
12
|
+
this.metaStore = metaStore;
|
|
13
|
+
this.secretStore = secretStore;
|
|
14
|
+
}
|
|
15
|
+
async load() {
|
|
16
|
+
return (0, fileLock_1.withSessionLock)(async () => {
|
|
17
|
+
await (0, legacySessionMigration_1.migrateLegacySessionIfNeeded)(this.metaStore, this.secretStore);
|
|
18
|
+
const config = await this.metaStore.load();
|
|
19
|
+
const meta = config?.session;
|
|
20
|
+
if (!meta?.appId || !meta.userId) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const secret = (0, legacySessionMigration_1.parseStoredSecretRecord)(await this.secretStore.get((0, legacySessionMigration_1.toSessionAccountKey)(meta.appId, meta.userId)));
|
|
24
|
+
if (!secret) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
...meta,
|
|
29
|
+
accessToken: secret.accessToken,
|
|
30
|
+
refreshToken: secret.refreshToken,
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
async save(session) {
|
|
35
|
+
await (0, fileLock_1.withSessionLock)(async () => {
|
|
36
|
+
await (0, legacySessionMigration_1.migrateLegacySessionIfNeeded)(this.metaStore, this.secretStore);
|
|
37
|
+
const previousMeta = (await this.metaStore.load())?.session;
|
|
38
|
+
const previousAccount = previousMeta?.appId && previousMeta.userId
|
|
39
|
+
? (0, legacySessionMigration_1.toSessionAccountKey)(previousMeta.appId, previousMeta.userId)
|
|
40
|
+
: null;
|
|
41
|
+
if (!session.appId || !session.userId) {
|
|
42
|
+
const previousSecret = previousAccount
|
|
43
|
+
? await this.secretStore.get(previousAccount)
|
|
44
|
+
: null;
|
|
45
|
+
await this.metaStore.saveSession(undefined);
|
|
46
|
+
try {
|
|
47
|
+
if (previousAccount) {
|
|
48
|
+
await this.secretStore.remove(previousAccount);
|
|
49
|
+
}
|
|
50
|
+
await (0, legacySessionMigration_1.removeLegacySessionFile)();
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
await this.restoreClearedSession(previousMeta, previousAccount, previousSecret, error);
|
|
54
|
+
}
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const nextAccount = (0, legacySessionMigration_1.toSessionAccountKey)(session.appId, session.userId);
|
|
58
|
+
const previousSecretForSameAccount = previousAccount === nextAccount && previousAccount
|
|
59
|
+
? await this.secretStore.get(previousAccount)
|
|
60
|
+
: null;
|
|
61
|
+
await this.secretStore.set(nextAccount, JSON.stringify((0, legacySessionMigration_1.toStoredSecretRecord)(session)));
|
|
62
|
+
try {
|
|
63
|
+
await this.metaStore.saveSession((0, legacySessionMigration_1.toSessionMetaRecord)(session));
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (nextAccount !== previousAccount) {
|
|
67
|
+
await this.secretStore.remove(nextAccount).catch(() => { });
|
|
68
|
+
}
|
|
69
|
+
else if (previousSecretForSameAccount !== null) {
|
|
70
|
+
await this.secretStore.set(nextAccount, previousSecretForSameAccount).catch(() => { });
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
await this.secretStore.remove(nextAccount).catch(() => { });
|
|
74
|
+
}
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
if (previousAccount && previousAccount !== nextAccount) {
|
|
78
|
+
await this.secretStore.remove(previousAccount).catch(() => { });
|
|
79
|
+
}
|
|
80
|
+
await (0, legacySessionMigration_1.removeLegacySessionFile)();
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
async clear() {
|
|
84
|
+
await (0, fileLock_1.withSessionLock)(async () => {
|
|
85
|
+
const config = await this.metaStore.load();
|
|
86
|
+
const previousMeta = config?.session;
|
|
87
|
+
const previousAccount = previousMeta?.appId && previousMeta.userId
|
|
88
|
+
? (0, legacySessionMigration_1.toSessionAccountKey)(previousMeta.appId, previousMeta.userId)
|
|
89
|
+
: null;
|
|
90
|
+
const previousSecret = previousAccount
|
|
91
|
+
? await this.secretStore.get(previousAccount)
|
|
92
|
+
: null;
|
|
93
|
+
await this.metaStore.saveSession(undefined);
|
|
94
|
+
try {
|
|
95
|
+
if (previousAccount) {
|
|
96
|
+
await this.secretStore.remove(previousAccount);
|
|
97
|
+
}
|
|
98
|
+
await (0, legacySessionMigration_1.removeLegacySessionFile)();
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
await this.restoreClearedSession(previousMeta, previousAccount, previousSecret, error);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
async restoreClearedSession(previousMeta, previousAccount, previousSecret, error) {
|
|
106
|
+
const rollbackErrors = [];
|
|
107
|
+
if (previousMeta) {
|
|
108
|
+
try {
|
|
109
|
+
await this.metaStore.saveSession(previousMeta);
|
|
110
|
+
}
|
|
111
|
+
catch (restoreError) {
|
|
112
|
+
rollbackErrors.push(restoreError);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (previousAccount && previousSecret !== null) {
|
|
116
|
+
try {
|
|
117
|
+
await this.secretStore.set(previousAccount, previousSecret);
|
|
118
|
+
}
|
|
119
|
+
catch (restoreError) {
|
|
120
|
+
rollbackErrors.push(restoreError);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (rollbackErrors.length > 0) {
|
|
124
|
+
throw new AggregateError([error, ...rollbackErrors], 'Failed to restore session after clear failure.');
|
|
125
|
+
}
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
exports.AuthSessionStore = AuthSessionStore;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ConfigStore = void 0;
|
|
4
|
+
const promises_1 = require("node:fs/promises");
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
6
|
+
const constants_1 = require("../../shared/constants");
|
|
7
|
+
const paths_1 = require("./paths");
|
|
8
|
+
class ConfigStore {
|
|
9
|
+
async load() {
|
|
10
|
+
try {
|
|
11
|
+
const content = await (0, promises_1.readFile)((0, paths_1.resolveCliPaths)().configFile, 'utf8');
|
|
12
|
+
return JSON.parse(content);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async initialize() {
|
|
19
|
+
const paths = (0, paths_1.resolveCliPaths)();
|
|
20
|
+
await (0, promises_1.mkdir)(paths.rootDir, { recursive: true });
|
|
21
|
+
await (0, promises_1.chmod)(paths.rootDir, 0o700);
|
|
22
|
+
const existing = await this.load();
|
|
23
|
+
if (existing) {
|
|
24
|
+
return existing;
|
|
25
|
+
}
|
|
26
|
+
const record = {
|
|
27
|
+
cliId: (0, node_crypto_1.randomUUID)(),
|
|
28
|
+
cliVersion: constants_1.CLI_VERSION,
|
|
29
|
+
};
|
|
30
|
+
await (0, promises_1.writeFile)(paths.configFile, JSON.stringify(record, null, 2), 'utf8');
|
|
31
|
+
await (0, promises_1.chmod)(paths.configFile, 0o600);
|
|
32
|
+
return record;
|
|
33
|
+
}
|
|
34
|
+
async save(record) {
|
|
35
|
+
const paths = (0, paths_1.resolveCliPaths)();
|
|
36
|
+
await (0, promises_1.mkdir)(paths.rootDir, { recursive: true });
|
|
37
|
+
await (0, promises_1.chmod)(paths.rootDir, 0o700);
|
|
38
|
+
const sanitizedRecord = {
|
|
39
|
+
cliId: record.cliId,
|
|
40
|
+
cliVersion: record.cliVersion,
|
|
41
|
+
locale: record.locale,
|
|
42
|
+
authBaseUrl: record.authBaseUrl,
|
|
43
|
+
session: record.session
|
|
44
|
+
? {
|
|
45
|
+
userId: record.session.userId,
|
|
46
|
+
userName: record.session.userName,
|
|
47
|
+
appId: record.session.appId,
|
|
48
|
+
apiUrl: record.session.apiUrl,
|
|
49
|
+
scope: Array.isArray(record.session.scope) ? [...record.session.scope] : [],
|
|
50
|
+
grantedAt: record.session.grantedAt,
|
|
51
|
+
tokenExpireAt: record.session.tokenExpireAt,
|
|
52
|
+
identity: record.session.identity,
|
|
53
|
+
}
|
|
54
|
+
: undefined,
|
|
55
|
+
};
|
|
56
|
+
const tmpFile = `${paths.configFile}.${process.pid}.${(0, node_crypto_1.randomUUID)()}.tmp`;
|
|
57
|
+
await (0, promises_1.writeFile)(tmpFile, JSON.stringify(sanitizedRecord, null, 2), {
|
|
58
|
+
encoding: 'utf8',
|
|
59
|
+
mode: 0o600,
|
|
60
|
+
flag: 'wx',
|
|
61
|
+
});
|
|
62
|
+
await (0, promises_1.rename)(tmpFile, paths.configFile);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
exports.ConfigStore = ConfigStore;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.withSessionLock = withSessionLock;
|
|
4
|
+
const promises_1 = require("node:fs/promises");
|
|
5
|
+
const paths_1 = require("./paths");
|
|
6
|
+
const LOCK_RETRY_INTERVAL_MS = 50;
|
|
7
|
+
const LOCK_TIMEOUT_MS = 10_000;
|
|
8
|
+
const STALE_LOCK_MS = 30_000;
|
|
9
|
+
function isProcessAlive(pid) {
|
|
10
|
+
try {
|
|
11
|
+
process.kill(pid, 0);
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function tryClearStaleLock(lockFile) {
|
|
19
|
+
try {
|
|
20
|
+
const lockStat = await (0, promises_1.stat)(lockFile);
|
|
21
|
+
if (Date.now() - lockStat.mtimeMs > STALE_LOCK_MS) {
|
|
22
|
+
await (0, promises_1.unlink)(lockFile).catch(() => { });
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const content = await (0, promises_1.readFile)(lockFile, 'utf8').catch(() => '');
|
|
26
|
+
const lockPid = Number(content.trim());
|
|
27
|
+
if (Number.isInteger(lockPid) && !isProcessAlive(lockPid)) {
|
|
28
|
+
await (0, promises_1.unlink)(lockFile).catch(() => { });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// ignore — will retry next iteration
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function withSessionLock(fn) {
|
|
36
|
+
const lockFile = (0, paths_1.resolveCliPaths)().sessionLockFile;
|
|
37
|
+
const rootDir = (0, paths_1.resolveCliPaths)().rootDir;
|
|
38
|
+
await (0, promises_1.mkdir)(rootDir, { recursive: true });
|
|
39
|
+
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
40
|
+
let locked = false;
|
|
41
|
+
while (!locked && Date.now() < deadline) {
|
|
42
|
+
try {
|
|
43
|
+
await (0, promises_1.writeFile)(lockFile, String(process.pid), {
|
|
44
|
+
encoding: 'utf8',
|
|
45
|
+
flag: 'wx',
|
|
46
|
+
});
|
|
47
|
+
locked = true;
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
if (err.code !== 'EEXIST') {
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
await tryClearStaleLock(lockFile);
|
|
54
|
+
await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_INTERVAL_MS));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!locked) {
|
|
58
|
+
throw new Error('获取会话文件锁超时,可能存在其他进程正在操作。');
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
return await fn();
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
await (0, promises_1.unlink)(lockFile).catch(() => { });
|
|
65
|
+
}
|
|
66
|
+
}
|