@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,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toSessionMetaRecord = toSessionMetaRecord;
|
|
4
|
+
exports.toStoredSecretRecord = toStoredSecretRecord;
|
|
5
|
+
exports.toSessionAccountKey = toSessionAccountKey;
|
|
6
|
+
exports.parseStoredSecretRecord = parseStoredSecretRecord;
|
|
7
|
+
exports.removeLegacySessionFile = removeLegacySessionFile;
|
|
8
|
+
exports.migrateLegacySessionIfNeeded = migrateLegacySessionIfNeeded;
|
|
9
|
+
const promises_1 = require("node:fs/promises");
|
|
10
|
+
const paths_1 = require("./paths");
|
|
11
|
+
function toSessionMetaRecord(session) {
|
|
12
|
+
const scope = Array.isArray(session.scope) ? session.scope.filter((item) => typeof item === 'string') : [];
|
|
13
|
+
return {
|
|
14
|
+
userId: session.userId,
|
|
15
|
+
userName: session.userName,
|
|
16
|
+
appId: session.appId,
|
|
17
|
+
apiUrl: session.apiUrl,
|
|
18
|
+
scope,
|
|
19
|
+
grantedAt: session.grantedAt,
|
|
20
|
+
tokenExpireAt: session.tokenExpireAt,
|
|
21
|
+
identity: session.identity,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function toStoredSecretRecord(session) {
|
|
25
|
+
return {
|
|
26
|
+
accessToken: session.accessToken,
|
|
27
|
+
refreshToken: session.refreshToken,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function toSessionAccountKey(appId, userId) {
|
|
31
|
+
return `${appId}:${userId}`;
|
|
32
|
+
}
|
|
33
|
+
function parseStoredSecretRecord(value) {
|
|
34
|
+
if (!value) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const parsed = JSON.parse(value);
|
|
39
|
+
if (!parsed || typeof parsed.accessToken !== 'string' || parsed.accessToken.length === 0) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
if (parsed.refreshToken !== undefined && typeof parsed.refreshToken !== 'string') {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
accessToken: parsed.accessToken,
|
|
47
|
+
refreshToken: parsed.refreshToken,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function removeLegacySessionFile() {
|
|
55
|
+
await (0, promises_1.rm)((0, paths_1.resolveCliPaths)().legacySessionFile, { force: true });
|
|
56
|
+
}
|
|
57
|
+
async function hasSecureSession(metaStore, secretStore) {
|
|
58
|
+
const config = await metaStore.load();
|
|
59
|
+
const meta = config?.session;
|
|
60
|
+
if (!meta?.appId || !meta.userId) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
const rawSecret = await secretStore.get(toSessionAccountKey(meta.appId, meta.userId));
|
|
64
|
+
return parseStoredSecretRecord(rawSecret) !== null;
|
|
65
|
+
}
|
|
66
|
+
async function loadLegacySession() {
|
|
67
|
+
try {
|
|
68
|
+
const content = await (0, promises_1.readFile)((0, paths_1.resolveCliPaths)().legacySessionFile, 'utf8');
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(content);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (error.code === 'ENOENT') {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function migrateLegacySessionIfNeeded(metaStore, secretStore) {
|
|
84
|
+
if (await hasSecureSession(metaStore, secretStore)) {
|
|
85
|
+
await removeLegacySessionFile();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const legacySession = await loadLegacySession();
|
|
89
|
+
if (!legacySession) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (!legacySession.appId || !legacySession.userId || !legacySession.accessToken) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const account = toSessionAccountKey(legacySession.appId, legacySession.userId);
|
|
96
|
+
await metaStore.saveSession(toSessionMetaRecord(legacySession));
|
|
97
|
+
await secretStore.set(account, JSON.stringify(toStoredSecretRecord(legacySession)));
|
|
98
|
+
const verifiedConfig = await metaStore.load();
|
|
99
|
+
const verifiedMeta = verifiedConfig?.session;
|
|
100
|
+
if (!verifiedMeta?.appId || !verifiedMeta.userId) {
|
|
101
|
+
throw new Error('Failed to verify migrated session metadata.');
|
|
102
|
+
}
|
|
103
|
+
const verifiedAccount = toSessionAccountKey(verifiedMeta.appId, verifiedMeta.userId);
|
|
104
|
+
const verifiedSecret = parseStoredSecretRecord(await secretStore.get(verifiedAccount));
|
|
105
|
+
if (!verifiedSecret) {
|
|
106
|
+
throw new Error('Failed to verify migrated session secret.');
|
|
107
|
+
}
|
|
108
|
+
await removeLegacySessionFile();
|
|
109
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.resolveCliPaths = resolveCliPaths;
|
|
7
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
function resolveConfigDir(platform) {
|
|
10
|
+
const envConfigDir = process.env.FS_CLI_CONFIG_DIR;
|
|
11
|
+
if (envConfigDir)
|
|
12
|
+
return envConfigDir;
|
|
13
|
+
const homeDir = node_os_1.default.homedir() || process.env.HOME?.trim();
|
|
14
|
+
if (!homeDir) {
|
|
15
|
+
if (platform === 'win32') {
|
|
16
|
+
const userProfile = process.env.USERPROFILE?.trim();
|
|
17
|
+
if (userProfile)
|
|
18
|
+
return userProfile;
|
|
19
|
+
const homeDrive = process.env.HOMEDRIVE?.trim();
|
|
20
|
+
const homePath = process.env.HOMEPATH?.trim();
|
|
21
|
+
if (homeDrive && homePath)
|
|
22
|
+
return node_path_1.default.join(homeDrive, homePath);
|
|
23
|
+
}
|
|
24
|
+
throw new Error('无法确定配置目录位置,请设置 FS_CLI_CONFIG_DIR 环境变量。');
|
|
25
|
+
}
|
|
26
|
+
return node_path_1.default.join(homeDir, '.sharecrm-cli');
|
|
27
|
+
}
|
|
28
|
+
function resolveCliPaths(platform = process.platform) {
|
|
29
|
+
const rootDir = resolveConfigDir(platform);
|
|
30
|
+
return {
|
|
31
|
+
rootDir,
|
|
32
|
+
configFile: node_path_1.default.join(rootDir, 'config.json'),
|
|
33
|
+
sessionFile: node_path_1.default.join(rootDir, 'session.json'),
|
|
34
|
+
// Kept as an alias during staged auth-storage migration so existing readers continue to resolve the same path.
|
|
35
|
+
legacySessionFile: node_path_1.default.join(rootDir, 'session.json'),
|
|
36
|
+
keychainDir: node_path_1.default.join(rootDir, 'keychain'),
|
|
37
|
+
sessionLockFile: node_path_1.default.join(rootDir, 'session.lock'),
|
|
38
|
+
commandCacheFile: node_path_1.default.join(rootDir, 'command-cache.json'),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createMasterKey = createMasterKey;
|
|
4
|
+
exports.accountToCipherFileName = accountToCipherFileName;
|
|
5
|
+
exports.encryptToFile = encryptToFile;
|
|
6
|
+
exports.decryptFromFile = decryptFromFile;
|
|
7
|
+
const node_crypto_1 = require("node:crypto");
|
|
8
|
+
const promises_1 = require("node:fs/promises");
|
|
9
|
+
const AES_GCM_ALGORITHM = 'aes-256-gcm';
|
|
10
|
+
const AES_GCM_KEY_SIZE = 32;
|
|
11
|
+
const AES_GCM_IV_SIZE = 12;
|
|
12
|
+
const AES_GCM_TAG_SIZE = 16;
|
|
13
|
+
function assertKey(key) {
|
|
14
|
+
if (key.length !== AES_GCM_KEY_SIZE) {
|
|
15
|
+
throw new Error(`Invalid AES-256-GCM key length: ${key.length}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function createMasterKey() {
|
|
19
|
+
return (0, node_crypto_1.randomBytes)(AES_GCM_KEY_SIZE);
|
|
20
|
+
}
|
|
21
|
+
function accountToCipherFileName(account) {
|
|
22
|
+
if (!account) {
|
|
23
|
+
throw new Error('Account is required.');
|
|
24
|
+
}
|
|
25
|
+
return `${(0, node_crypto_1.createHash)('sha256').update(account).digest('hex')}.enc`;
|
|
26
|
+
}
|
|
27
|
+
async function encryptToFile(targetPath, plaintext, key) {
|
|
28
|
+
assertKey(key);
|
|
29
|
+
const iv = (0, node_crypto_1.randomBytes)(AES_GCM_IV_SIZE);
|
|
30
|
+
const cipher = (0, node_crypto_1.createCipheriv)(AES_GCM_ALGORITHM, key, iv);
|
|
31
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
|
|
32
|
+
const tag = cipher.getAuthTag();
|
|
33
|
+
const payload = Buffer.concat([iv, tag, ciphertext]);
|
|
34
|
+
const tmpPath = `${targetPath}.${process.pid}.${(0, node_crypto_1.randomUUID)()}.tmp`;
|
|
35
|
+
let moved = false;
|
|
36
|
+
try {
|
|
37
|
+
await (0, promises_1.writeFile)(tmpPath, payload, { mode: 0o600, flag: 'wx' });
|
|
38
|
+
await (0, promises_1.chmod)(tmpPath, 0o600);
|
|
39
|
+
await (0, promises_1.rename)(tmpPath, targetPath);
|
|
40
|
+
moved = true;
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
if (!moved) {
|
|
44
|
+
await (0, promises_1.rm)(tmpPath, { force: true }).catch(() => { });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function decryptFromFile(targetPath, key) {
|
|
49
|
+
assertKey(key);
|
|
50
|
+
const payload = await (0, promises_1.readFile)(targetPath);
|
|
51
|
+
if (payload.length < AES_GCM_IV_SIZE + AES_GCM_TAG_SIZE) {
|
|
52
|
+
throw new Error(`Encrypted payload is corrupted at ${targetPath}.`);
|
|
53
|
+
}
|
|
54
|
+
const iv = payload.subarray(0, AES_GCM_IV_SIZE);
|
|
55
|
+
const tag = payload.subarray(AES_GCM_IV_SIZE, AES_GCM_IV_SIZE + AES_GCM_TAG_SIZE);
|
|
56
|
+
const ciphertext = payload.subarray(AES_GCM_IV_SIZE + AES_GCM_TAG_SIZE);
|
|
57
|
+
const decipher = (0, node_crypto_1.createDecipheriv)(AES_GCM_ALGORITHM, key, iv);
|
|
58
|
+
decipher.setAuthTag(tag);
|
|
59
|
+
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
60
|
+
return plaintext.toString('utf8');
|
|
61
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createUnsupportedSecretStore = exports.createWin32SecretStore = exports.createDarwinSecretStore = exports.createLinuxSecretStore = void 0;
|
|
4
|
+
exports.createSecretStore = createSecretStore;
|
|
5
|
+
const secretStore_darwin_1 = require("./secretStore.darwin");
|
|
6
|
+
const secretStore_linux_1 = require("./secretStore.linux");
|
|
7
|
+
const secretStore_unsupported_1 = require("./secretStore.unsupported");
|
|
8
|
+
const secretStore_win32_1 = require("./secretStore.win32");
|
|
9
|
+
function createSecretStore(platform = process.platform) {
|
|
10
|
+
switch (platform) {
|
|
11
|
+
case 'linux':
|
|
12
|
+
return (0, secretStore_linux_1.createLinuxSecretStore)();
|
|
13
|
+
case 'darwin':
|
|
14
|
+
return (0, secretStore_darwin_1.createDarwinSecretStore)();
|
|
15
|
+
case 'win32':
|
|
16
|
+
return (0, secretStore_win32_1.createWin32SecretStore)();
|
|
17
|
+
default:
|
|
18
|
+
return (0, secretStore_unsupported_1.createUnsupportedSecretStore)(platform);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
var secretStore_linux_2 = require("./secretStore.linux");
|
|
22
|
+
Object.defineProperty(exports, "createLinuxSecretStore", { enumerable: true, get: function () { return secretStore_linux_2.createLinuxSecretStore; } });
|
|
23
|
+
var secretStore_darwin_2 = require("./secretStore.darwin");
|
|
24
|
+
Object.defineProperty(exports, "createDarwinSecretStore", { enumerable: true, get: function () { return secretStore_darwin_2.createDarwinSecretStore; } });
|
|
25
|
+
var secretStore_win32_2 = require("./secretStore.win32");
|
|
26
|
+
Object.defineProperty(exports, "createWin32SecretStore", { enumerable: true, get: function () { return secretStore_win32_2.createWin32SecretStore; } });
|
|
27
|
+
var secretStore_unsupported_2 = require("./secretStore.unsupported");
|
|
28
|
+
Object.defineProperty(exports, "createUnsupportedSecretStore", { enumerable: true, get: function () { return secretStore_unsupported_2.createUnsupportedSecretStore; } });
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createDarwinSecretStore = createDarwinSecretStore;
|
|
7
|
+
const node_child_process_1 = require("node:child_process");
|
|
8
|
+
const promises_1 = require("node:fs/promises");
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const node_util_1 = require("node:util");
|
|
11
|
+
const paths_1 = require("../paths");
|
|
12
|
+
const commonFileCrypto_1 = require("./commonFileCrypto");
|
|
13
|
+
const DEFAULT_SECURITY_SERVICE = 'sharecrm-cli.secret-store';
|
|
14
|
+
const DEFAULT_SECURITY_ACCOUNT = 'default';
|
|
15
|
+
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
16
|
+
async function defaultExecFile(file, args, options) {
|
|
17
|
+
const result = await execFileAsync(file, [...args], {
|
|
18
|
+
encoding: 'utf8',
|
|
19
|
+
env: options?.env,
|
|
20
|
+
});
|
|
21
|
+
return {
|
|
22
|
+
stdout: String(result.stdout ?? ''),
|
|
23
|
+
stderr: String(result.stderr ?? ''),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function isMissingSecurityItem(error) {
|
|
27
|
+
const maybe = error;
|
|
28
|
+
if (maybe.code === 44 || maybe.code === '44') {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
const detail = `${maybe.stderr ?? ''}\n${maybe.message ?? ''}`.toLowerCase();
|
|
32
|
+
return detail.includes('could not be found');
|
|
33
|
+
}
|
|
34
|
+
function isDuplicateSecurityItem(error) {
|
|
35
|
+
const maybe = error;
|
|
36
|
+
if (maybe.code === 45 || maybe.code === '45') {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
const detail = `${maybe.stderr ?? ''}\n${maybe.message ?? ''}`.toLowerCase();
|
|
40
|
+
return detail.includes('already exists') || detail.includes('-25299');
|
|
41
|
+
}
|
|
42
|
+
function decodeMasterKey(encodedValue) {
|
|
43
|
+
const key = Buffer.from(encodedValue.trim(), 'base64');
|
|
44
|
+
if (key.length !== 32) {
|
|
45
|
+
throw new Error(`Invalid macOS secret-store master key length: ${key.length}`);
|
|
46
|
+
}
|
|
47
|
+
return key;
|
|
48
|
+
}
|
|
49
|
+
async function ensureKeychainDir(keychainDir) {
|
|
50
|
+
await (0, promises_1.mkdir)(keychainDir, { recursive: true });
|
|
51
|
+
await (0, promises_1.chmod)(keychainDir, 0o700);
|
|
52
|
+
}
|
|
53
|
+
async function findMasterKey(execFile, securityService, securityAccount) {
|
|
54
|
+
try {
|
|
55
|
+
const result = await execFile('security', [
|
|
56
|
+
'find-generic-password',
|
|
57
|
+
'-s',
|
|
58
|
+
securityService,
|
|
59
|
+
'-a',
|
|
60
|
+
securityAccount,
|
|
61
|
+
'-w',
|
|
62
|
+
]);
|
|
63
|
+
return decodeMasterKey(result.stdout);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (isMissingSecurityItem(error)) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function createAndPersistMasterKey(execFile, securityService, securityAccount) {
|
|
73
|
+
const encoded = (0, commonFileCrypto_1.createMasterKey)().toString('base64');
|
|
74
|
+
await execFile('security', [
|
|
75
|
+
'add-generic-password',
|
|
76
|
+
'-s',
|
|
77
|
+
securityService,
|
|
78
|
+
'-a',
|
|
79
|
+
securityAccount,
|
|
80
|
+
'-w',
|
|
81
|
+
encoded,
|
|
82
|
+
]);
|
|
83
|
+
return decodeMasterKey(encoded);
|
|
84
|
+
}
|
|
85
|
+
function createDarwinSecretStore(options = {}) {
|
|
86
|
+
const keychainDir = options.keychainDir ?? (0, paths_1.resolveCliPaths)().keychainDir;
|
|
87
|
+
const securityService = options.securityService ?? DEFAULT_SECURITY_SERVICE;
|
|
88
|
+
const securityAccount = options.securityAccount ?? DEFAULT_SECURITY_ACCOUNT;
|
|
89
|
+
const execFile = options.execFile ?? defaultExecFile;
|
|
90
|
+
async function getMasterKey(allowCreate) {
|
|
91
|
+
const existing = await findMasterKey(execFile, securityService, securityAccount);
|
|
92
|
+
if (existing || !allowCreate) {
|
|
93
|
+
return existing;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
return await createAndPersistMasterKey(execFile, securityService, securityAccount);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
if (!isDuplicateSecurityItem(error)) {
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
return findMasterKey(execFile, securityService, securityAccount);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
async get(account) {
|
|
107
|
+
const fileName = (0, commonFileCrypto_1.accountToCipherFileName)(account);
|
|
108
|
+
const key = await getMasterKey(false);
|
|
109
|
+
if (!key) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
const payloadPath = node_path_1.default.join(keychainDir, fileName);
|
|
113
|
+
try {
|
|
114
|
+
return await (0, commonFileCrypto_1.decryptFromFile)(payloadPath, key);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (error.code === 'ENOENT') {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
async set(account, value) {
|
|
124
|
+
const fileName = (0, commonFileCrypto_1.accountToCipherFileName)(account);
|
|
125
|
+
await ensureKeychainDir(keychainDir);
|
|
126
|
+
const key = await getMasterKey(true);
|
|
127
|
+
if (!key) {
|
|
128
|
+
throw new Error('Unable to initialize macOS secret-store master key.');
|
|
129
|
+
}
|
|
130
|
+
const payloadPath = node_path_1.default.join(keychainDir, fileName);
|
|
131
|
+
await (0, commonFileCrypto_1.encryptToFile)(payloadPath, value, key);
|
|
132
|
+
},
|
|
133
|
+
async remove(account) {
|
|
134
|
+
const fileName = (0, commonFileCrypto_1.accountToCipherFileName)(account);
|
|
135
|
+
const payloadPath = node_path_1.default.join(keychainDir, fileName);
|
|
136
|
+
await (0, promises_1.rm)(payloadPath, { force: true });
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createLinuxSecretStore = createLinuxSecretStore;
|
|
7
|
+
const promises_1 = require("node:fs/promises");
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const paths_1 = require("../paths");
|
|
10
|
+
const commonFileCrypto_1 = require("./commonFileCrypto");
|
|
11
|
+
async function ensureKeychainDir(keychainDir) {
|
|
12
|
+
await (0, promises_1.mkdir)(keychainDir, { recursive: true });
|
|
13
|
+
await (0, promises_1.chmod)(keychainDir, 0o700);
|
|
14
|
+
}
|
|
15
|
+
function resolveMasterKeyPath(keychainDir) {
|
|
16
|
+
return node_path_1.default.join(keychainDir, 'master.key');
|
|
17
|
+
}
|
|
18
|
+
async function readMasterKey(masterKeyPath) {
|
|
19
|
+
try {
|
|
20
|
+
const key = await (0, promises_1.readFile)(masterKeyPath);
|
|
21
|
+
if (key.length !== 32) {
|
|
22
|
+
throw new Error(`Invalid Linux secret-store master key length: ${key.length}`);
|
|
23
|
+
}
|
|
24
|
+
return key;
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (error.code === 'ENOENT') {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function loadOrCreateMasterKey(keychainDir) {
|
|
34
|
+
await ensureKeychainDir(keychainDir);
|
|
35
|
+
const masterKeyPath = resolveMasterKeyPath(keychainDir);
|
|
36
|
+
const existing = await readMasterKey(masterKeyPath);
|
|
37
|
+
if (existing) {
|
|
38
|
+
return existing;
|
|
39
|
+
}
|
|
40
|
+
const created = (0, commonFileCrypto_1.createMasterKey)();
|
|
41
|
+
try {
|
|
42
|
+
await (0, promises_1.writeFile)(masterKeyPath, created, { mode: 0o600, flag: 'wx' });
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (error.code !== 'EEXIST') {
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
const raced = await readMasterKey(masterKeyPath);
|
|
49
|
+
if (!raced) {
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
return raced;
|
|
53
|
+
}
|
|
54
|
+
await (0, promises_1.chmod)(masterKeyPath, 0o600);
|
|
55
|
+
return created;
|
|
56
|
+
}
|
|
57
|
+
function createLinuxSecretStore(options = {}) {
|
|
58
|
+
const keychainDir = options.keychainDir ?? (0, paths_1.resolveCliPaths)().keychainDir;
|
|
59
|
+
const masterKeyPath = resolveMasterKeyPath(keychainDir);
|
|
60
|
+
return {
|
|
61
|
+
async get(account) {
|
|
62
|
+
const fileName = (0, commonFileCrypto_1.accountToCipherFileName)(account);
|
|
63
|
+
const key = await readMasterKey(masterKeyPath);
|
|
64
|
+
if (!key) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const payloadPath = node_path_1.default.join(keychainDir, fileName);
|
|
68
|
+
try {
|
|
69
|
+
return await (0, commonFileCrypto_1.decryptFromFile)(payloadPath, key);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (error.code === 'ENOENT') {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
async set(account, value) {
|
|
79
|
+
const fileName = (0, commonFileCrypto_1.accountToCipherFileName)(account);
|
|
80
|
+
const key = await loadOrCreateMasterKey(keychainDir);
|
|
81
|
+
const payloadPath = node_path_1.default.join(keychainDir, fileName);
|
|
82
|
+
await (0, commonFileCrypto_1.encryptToFile)(payloadPath, value, key);
|
|
83
|
+
},
|
|
84
|
+
async remove(account) {
|
|
85
|
+
const fileName = (0, commonFileCrypto_1.accountToCipherFileName)(account);
|
|
86
|
+
const payloadPath = node_path_1.default.join(keychainDir, fileName);
|
|
87
|
+
await (0, promises_1.rm)(payloadPath, { force: true });
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createUnsupportedSecretStore = createUnsupportedSecretStore;
|
|
4
|
+
function createUnsupportedSecretStore(platform = process.platform) {
|
|
5
|
+
const error = () => new Error(`SecretStore is not supported on platform "${platform}".`);
|
|
6
|
+
return {
|
|
7
|
+
async get() {
|
|
8
|
+
throw error();
|
|
9
|
+
},
|
|
10
|
+
async set() {
|
|
11
|
+
throw error();
|
|
12
|
+
},
|
|
13
|
+
async remove() {
|
|
14
|
+
throw error();
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createWin32SecretStore = createWin32SecretStore;
|
|
4
|
+
const node_child_process_1 = require("node:child_process");
|
|
5
|
+
const node_util_1 = require("node:util");
|
|
6
|
+
const DEFAULT_REGISTRY_PATH = 'HKCU:\\Software\\sharecrm-cli\\keychain';
|
|
7
|
+
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
8
|
+
const NOT_FOUND_MARKER = '__FS_CLI_NOT_FOUND__';
|
|
9
|
+
const FOUND_MARKER = '__FS_CLI_FOUND__';
|
|
10
|
+
const BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
11
|
+
const ENV_PLAIN_B64 = 'FS_CLI_SECRET_PLAIN_B64';
|
|
12
|
+
const ENV_PROTECTED_B64 = 'FS_CLI_SECRET_PROTECTED_B64';
|
|
13
|
+
const ENV_REGISTRY_PATH_B64 = 'FS_CLI_SECRET_REGISTRY_PATH_B64';
|
|
14
|
+
const ENV_REGISTRY_NAME_B64 = 'FS_CLI_SECRET_REGISTRY_NAME_B64';
|
|
15
|
+
const ENV_REGISTRY_VALUE_B64 = 'FS_CLI_SECRET_REGISTRY_VALUE_B64';
|
|
16
|
+
const PROTECT_SCRIPT = [
|
|
17
|
+
'Add-Type -AssemblyName System.Security',
|
|
18
|
+
`$plain64 = $env:${ENV_PLAIN_B64}`,
|
|
19
|
+
'if ($null -eq $plain64) { throw "Missing plaintext input." }',
|
|
20
|
+
'$plainBytes = [Convert]::FromBase64String($plain64)',
|
|
21
|
+
'$protectedBytes = [System.Security.Cryptography.ProtectedData]::Protect($plainBytes, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)',
|
|
22
|
+
'[Console]::Out.Write([Convert]::ToBase64String($protectedBytes))',
|
|
23
|
+
'',
|
|
24
|
+
].join('\n');
|
|
25
|
+
const UNPROTECT_SCRIPT = [
|
|
26
|
+
'Add-Type -AssemblyName System.Security',
|
|
27
|
+
`$protected64 = $env:${ENV_PROTECTED_B64}`,
|
|
28
|
+
'if ($null -eq $protected64) { throw "Missing protected input." }',
|
|
29
|
+
'$protectedBytes = [Convert]::FromBase64String($protected64)',
|
|
30
|
+
'$plainBytes = [System.Security.Cryptography.ProtectedData]::Unprotect($protectedBytes, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)',
|
|
31
|
+
'[Console]::Out.Write([Convert]::ToBase64String($plainBytes))',
|
|
32
|
+
'',
|
|
33
|
+
].join('\n');
|
|
34
|
+
const WRITE_REGISTRY_SCRIPT = [
|
|
35
|
+
`$path = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($env:${ENV_REGISTRY_PATH_B64}))`,
|
|
36
|
+
`$name = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($env:${ENV_REGISTRY_NAME_B64}))`,
|
|
37
|
+
`$value = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($env:${ENV_REGISTRY_VALUE_B64}))`,
|
|
38
|
+
'New-Item -Path $path -Force | Out-Null',
|
|
39
|
+
'Set-ItemProperty -Path $path -Name $name -Value $value',
|
|
40
|
+
'',
|
|
41
|
+
].join('\n');
|
|
42
|
+
const READ_REGISTRY_SCRIPT = [
|
|
43
|
+
`$path = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($env:${ENV_REGISTRY_PATH_B64}))`,
|
|
44
|
+
`$name = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($env:${ENV_REGISTRY_NAME_B64}))`,
|
|
45
|
+
'if (-not (Test-Path -Path $path)) {',
|
|
46
|
+
` [Console]::Out.Write('${NOT_FOUND_MARKER}')`,
|
|
47
|
+
' return',
|
|
48
|
+
'}',
|
|
49
|
+
'$item = Get-ItemProperty -Path $path -ErrorAction SilentlyContinue',
|
|
50
|
+
'if ($null -eq $item -or -not ($item.PSObject.Properties.Name -contains $name)) {',
|
|
51
|
+
` [Console]::Out.Write('${NOT_FOUND_MARKER}')`,
|
|
52
|
+
' return',
|
|
53
|
+
'}',
|
|
54
|
+
'$value = [string]$item.$name',
|
|
55
|
+
`[Console]::Out.Write('${FOUND_MARKER}' + $value)`,
|
|
56
|
+
'',
|
|
57
|
+
].join('\n');
|
|
58
|
+
const REMOVE_REGISTRY_SCRIPT = [
|
|
59
|
+
`$path = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($env:${ENV_REGISTRY_PATH_B64}))`,
|
|
60
|
+
`$name = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($env:${ENV_REGISTRY_NAME_B64}))`,
|
|
61
|
+
'if (Test-Path -Path $path) {',
|
|
62
|
+
' Remove-ItemProperty -Path $path -Name $name -ErrorAction SilentlyContinue',
|
|
63
|
+
'}',
|
|
64
|
+
'',
|
|
65
|
+
].join('\n');
|
|
66
|
+
async function defaultExecFile(file, args, options) {
|
|
67
|
+
const result = await execFileAsync(file, [...args], {
|
|
68
|
+
encoding: 'utf8',
|
|
69
|
+
env: options?.env,
|
|
70
|
+
});
|
|
71
|
+
return {
|
|
72
|
+
stdout: String(result.stdout ?? ''),
|
|
73
|
+
stderr: String(result.stderr ?? ''),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function toBase64Utf8(value) {
|
|
77
|
+
return Buffer.from(value, 'utf8').toString('base64');
|
|
78
|
+
}
|
|
79
|
+
function toBase64Utf16Le(value) {
|
|
80
|
+
return Buffer.from(value, 'utf16le').toString('base64');
|
|
81
|
+
}
|
|
82
|
+
function normalizePowerShellOutput(output) {
|
|
83
|
+
return output
|
|
84
|
+
.replace(/(?:^|\r?\n)#< CLIXML[\s\S]*$/m, '')
|
|
85
|
+
.replace(/^[\r\n]+|[\r\n]+$/g, '');
|
|
86
|
+
}
|
|
87
|
+
function decodeBase64Strict(value, label) {
|
|
88
|
+
const normalized = normalizePowerShellOutput(value);
|
|
89
|
+
if (normalized.length === 0) {
|
|
90
|
+
return '';
|
|
91
|
+
}
|
|
92
|
+
if (!BASE64_PATTERN.test(normalized)) {
|
|
93
|
+
throw new Error(`Failed to parse PowerShell ${label} output.`);
|
|
94
|
+
}
|
|
95
|
+
const decoded = Buffer.from(normalized, 'base64');
|
|
96
|
+
if (decoded.toString('base64') !== normalized) {
|
|
97
|
+
throw new Error(`Failed to parse PowerShell ${label} output.`);
|
|
98
|
+
}
|
|
99
|
+
return decoded.toString('utf8');
|
|
100
|
+
}
|
|
101
|
+
async function runPowerShell(execFile, script, env) {
|
|
102
|
+
const result = await execFile('powershell.exe', [
|
|
103
|
+
'-NoProfile',
|
|
104
|
+
'-NonInteractive',
|
|
105
|
+
'-ExecutionPolicy',
|
|
106
|
+
'Bypass',
|
|
107
|
+
'-EncodedCommand',
|
|
108
|
+
toBase64Utf16Le(script),
|
|
109
|
+
], {
|
|
110
|
+
env: {
|
|
111
|
+
...process.env,
|
|
112
|
+
...env,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
return result.stdout;
|
|
116
|
+
}
|
|
117
|
+
function buildRegistryEnv(registryPath, name) {
|
|
118
|
+
return {
|
|
119
|
+
[ENV_REGISTRY_PATH_B64]: toBase64Utf8(registryPath),
|
|
120
|
+
[ENV_REGISTRY_NAME_B64]: toBase64Utf8(name),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function parseReadRegistryOutput(output) {
|
|
124
|
+
const normalized = normalizePowerShellOutput(output);
|
|
125
|
+
if (normalized === NOT_FOUND_MARKER) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
if (!normalized.startsWith(FOUND_MARKER)) {
|
|
129
|
+
throw new Error(`Unexpected PowerShell registry read output: ${normalized}`);
|
|
130
|
+
}
|
|
131
|
+
return normalized.slice(FOUND_MARKER.length);
|
|
132
|
+
}
|
|
133
|
+
function createWin32SecretStore(options = {}) {
|
|
134
|
+
const execFile = options.execFile ?? defaultExecFile;
|
|
135
|
+
const registryPath = options.registryPath ?? DEFAULT_REGISTRY_PATH;
|
|
136
|
+
return {
|
|
137
|
+
async get(account) {
|
|
138
|
+
const protectedValue = parseReadRegistryOutput(await runPowerShell(execFile, READ_REGISTRY_SCRIPT, buildRegistryEnv(registryPath, account)));
|
|
139
|
+
if (protectedValue === null) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
return decodeBase64Strict(await runPowerShell(execFile, UNPROTECT_SCRIPT, {
|
|
143
|
+
[ENV_PROTECTED_B64]: protectedValue,
|
|
144
|
+
}), 'unprotect');
|
|
145
|
+
},
|
|
146
|
+
async set(account, value) {
|
|
147
|
+
const protectedValue = normalizePowerShellOutput(await runPowerShell(execFile, PROTECT_SCRIPT, {
|
|
148
|
+
[ENV_PLAIN_B64]: toBase64Utf8(value),
|
|
149
|
+
}));
|
|
150
|
+
if (!protectedValue) {
|
|
151
|
+
throw new Error('Failed to protect secret via DPAPI.');
|
|
152
|
+
}
|
|
153
|
+
await runPowerShell(execFile, WRITE_REGISTRY_SCRIPT, {
|
|
154
|
+
...buildRegistryEnv(registryPath, account),
|
|
155
|
+
[ENV_REGISTRY_VALUE_B64]: toBase64Utf8(protectedValue),
|
|
156
|
+
});
|
|
157
|
+
},
|
|
158
|
+
async remove(account) {
|
|
159
|
+
await runPowerShell(execFile, REMOVE_REGISTRY_SCRIPT, buildRegistryEnv(registryPath, account));
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SessionMetaStore = void 0;
|
|
4
|
+
const configStore_1 = require("./configStore");
|
|
5
|
+
class SessionMetaStore {
|
|
6
|
+
configStore;
|
|
7
|
+
constructor(configStore = new configStore_1.ConfigStore()) {
|
|
8
|
+
this.configStore = configStore;
|
|
9
|
+
}
|
|
10
|
+
async load() {
|
|
11
|
+
return this.configStore.load();
|
|
12
|
+
}
|
|
13
|
+
async loadSession() {
|
|
14
|
+
return (await this.load())?.session;
|
|
15
|
+
}
|
|
16
|
+
async saveSession(meta) {
|
|
17
|
+
const base = await this.configStore.initialize();
|
|
18
|
+
await this.configStore.save({
|
|
19
|
+
...base,
|
|
20
|
+
session: meta,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
exports.SessionMetaStore = SessionMetaStore;
|