@askexenow/exe-os 0.9.30 → 0.9.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/backfill-conversations.js +135 -7
- package/dist/bin/backfill-responses.js +135 -7
- package/dist/bin/backfill-vectors.js +135 -7
- package/dist/bin/cleanup-stale-review-tasks.js +139 -11
- package/dist/bin/cli.js +812 -486
- package/dist/bin/exe-assign.js +135 -7
- package/dist/bin/exe-boot.js +422 -113
- package/dist/bin/exe-cloud.js +160 -9
- package/dist/bin/exe-dispatch.js +136 -8
- package/dist/bin/exe-doctor.js +255 -13
- package/dist/bin/exe-export-behaviors.js +136 -8
- package/dist/bin/exe-forget.js +136 -8
- package/dist/bin/exe-gateway.js +171 -24
- package/dist/bin/exe-heartbeat.js +141 -13
- package/dist/bin/exe-kill.js +140 -12
- package/dist/bin/exe-launch-agent.js +143 -15
- package/dist/bin/exe-link.js +357 -48
- package/dist/bin/exe-pending-messages.js +136 -8
- package/dist/bin/exe-pending-notifications.js +136 -8
- package/dist/bin/exe-pending-reviews.js +138 -10
- package/dist/bin/exe-review.js +136 -8
- package/dist/bin/exe-search.js +155 -20
- package/dist/bin/exe-session-cleanup.js +166 -38
- package/dist/bin/exe-start-codex.js +142 -14
- package/dist/bin/exe-start-opencode.js +140 -12
- package/dist/bin/exe-status.js +148 -20
- package/dist/bin/exe-team.js +136 -8
- package/dist/bin/git-sweep.js +138 -10
- package/dist/bin/graph-backfill.js +135 -7
- package/dist/bin/graph-export.js +136 -8
- package/dist/bin/intercom-check.js +153 -25
- package/dist/bin/scan-tasks.js +138 -10
- package/dist/bin/setup.js +447 -121
- package/dist/bin/shard-migrate.js +135 -7
- package/dist/gateway/index.js +151 -23
- package/dist/hooks/bug-report-worker.js +151 -23
- package/dist/hooks/codex-stop-task-finalizer.js +145 -17
- package/dist/hooks/commit-complete.js +138 -10
- package/dist/hooks/error-recall.js +159 -24
- package/dist/hooks/ingest.js +142 -14
- package/dist/hooks/instructions-loaded.js +136 -8
- package/dist/hooks/notification.js +136 -8
- package/dist/hooks/post-compact.js +136 -8
- package/dist/hooks/post-tool-combined.js +159 -24
- package/dist/hooks/pre-compact.js +136 -8
- package/dist/hooks/pre-tool-use.js +144 -16
- package/dist/hooks/prompt-submit.js +195 -55
- package/dist/hooks/session-end.js +141 -13
- package/dist/hooks/session-start.js +165 -30
- package/dist/hooks/stop.js +136 -8
- package/dist/hooks/subagent-stop.js +136 -8
- package/dist/hooks/summary-worker.js +374 -65
- package/dist/index.js +136 -8
- package/dist/lib/cloud-sync.js +355 -46
- package/dist/lib/consolidation.js +1 -0
- package/dist/lib/exe-daemon.js +469 -127
- package/dist/lib/hybrid-search.js +155 -20
- package/dist/lib/keychain.js +191 -7
- package/dist/lib/schedules.js +138 -10
- package/dist/lib/store.js +135 -7
- package/dist/mcp/server.js +706 -213
- package/dist/runtime/index.js +136 -8
- package/dist/tui/App.js +208 -31
- package/package.json +1 -1
|
@@ -1083,8 +1083,8 @@ function findPackageRoot() {
|
|
|
1083
1083
|
function getAvailableMemoryGB() {
|
|
1084
1084
|
if (process.platform === "darwin") {
|
|
1085
1085
|
try {
|
|
1086
|
-
const { execSync:
|
|
1087
|
-
const vmstat =
|
|
1086
|
+
const { execSync: execSync5 } = __require("child_process");
|
|
1087
|
+
const vmstat = execSync5("vm_stat", { encoding: "utf8" });
|
|
1088
1088
|
const pageSize = 16384;
|
|
1089
1089
|
const pageSizeMatch = vmstat.match(/page size of (\d+) bytes/);
|
|
1090
1090
|
const actualPageSize = pageSizeMatch ? parseInt(pageSizeMatch[1], 10) : pageSize;
|
|
@@ -2768,6 +2768,7 @@ var init_database = __esm({
|
|
|
2768
2768
|
// src/lib/keychain.ts
|
|
2769
2769
|
import { readFile as readFile3, writeFile as writeFile3, unlink, mkdir as mkdir3, chmod as chmod2 } from "fs/promises";
|
|
2770
2770
|
import { existsSync as existsSync6 } from "fs";
|
|
2771
|
+
import { execSync as execSync2 } from "child_process";
|
|
2771
2772
|
import path6 from "path";
|
|
2772
2773
|
import os5 from "os";
|
|
2773
2774
|
function getKeyDir() {
|
|
@@ -2776,6 +2777,59 @@ function getKeyDir() {
|
|
|
2776
2777
|
function getKeyPath() {
|
|
2777
2778
|
return path6.join(getKeyDir(), "master.key");
|
|
2778
2779
|
}
|
|
2780
|
+
function macKeychainGet() {
|
|
2781
|
+
if (process.platform !== "darwin") return null;
|
|
2782
|
+
try {
|
|
2783
|
+
return execSync2(
|
|
2784
|
+
`security find-generic-password -s "${SERVICE}" -a "${ACCOUNT}" -w 2>/dev/null`,
|
|
2785
|
+
{ encoding: "utf-8", timeout: 5e3 }
|
|
2786
|
+
).trim();
|
|
2787
|
+
} catch {
|
|
2788
|
+
return null;
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
function macKeychainSet(value) {
|
|
2792
|
+
if (process.platform !== "darwin") return false;
|
|
2793
|
+
try {
|
|
2794
|
+
try {
|
|
2795
|
+
execSync2(
|
|
2796
|
+
`security delete-generic-password -s "${SERVICE}" -a "${ACCOUNT}" 2>/dev/null`,
|
|
2797
|
+
{ timeout: 5e3 }
|
|
2798
|
+
);
|
|
2799
|
+
} catch {
|
|
2800
|
+
}
|
|
2801
|
+
execSync2(
|
|
2802
|
+
`security add-generic-password -s "${SERVICE}" -a "${ACCOUNT}" -w "${value}"`,
|
|
2803
|
+
{ timeout: 5e3 }
|
|
2804
|
+
);
|
|
2805
|
+
return true;
|
|
2806
|
+
} catch {
|
|
2807
|
+
return false;
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
function linuxSecretGet() {
|
|
2811
|
+
if (process.platform !== "linux") return null;
|
|
2812
|
+
try {
|
|
2813
|
+
return execSync2(
|
|
2814
|
+
`secret-tool lookup service "${SERVICE}" account "${ACCOUNT}" 2>/dev/null`,
|
|
2815
|
+
{ encoding: "utf-8", timeout: 5e3 }
|
|
2816
|
+
).trim();
|
|
2817
|
+
} catch {
|
|
2818
|
+
return null;
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
function linuxSecretSet(value) {
|
|
2822
|
+
if (process.platform !== "linux") return false;
|
|
2823
|
+
try {
|
|
2824
|
+
execSync2(
|
|
2825
|
+
`echo -n "${value}" | secret-tool store --label="exe-os master key" service "${SERVICE}" account "${ACCOUNT}"`,
|
|
2826
|
+
{ timeout: 5e3 }
|
|
2827
|
+
);
|
|
2828
|
+
return true;
|
|
2829
|
+
} catch {
|
|
2830
|
+
return false;
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2779
2833
|
async function tryKeytar() {
|
|
2780
2834
|
try {
|
|
2781
2835
|
return await import("keytar");
|
|
@@ -2783,13 +2837,63 @@ async function tryKeytar() {
|
|
|
2783
2837
|
return null;
|
|
2784
2838
|
}
|
|
2785
2839
|
}
|
|
2840
|
+
function deriveMachineKey() {
|
|
2841
|
+
try {
|
|
2842
|
+
const crypto3 = __require("crypto");
|
|
2843
|
+
const material = [
|
|
2844
|
+
os5.hostname(),
|
|
2845
|
+
os5.userInfo().username,
|
|
2846
|
+
os5.arch(),
|
|
2847
|
+
os5.platform(),
|
|
2848
|
+
// Machine ID on Linux (stable across reboots)
|
|
2849
|
+
process.platform === "linux" ? readMachineId() : ""
|
|
2850
|
+
].join("|");
|
|
2851
|
+
return crypto3.createHash("sha256").update(material).digest();
|
|
2852
|
+
} catch {
|
|
2853
|
+
return null;
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
function readMachineId() {
|
|
2857
|
+
try {
|
|
2858
|
+
const { readFileSync: readFileSync6 } = __require("fs");
|
|
2859
|
+
return readFileSync6("/etc/machine-id", "utf-8").trim();
|
|
2860
|
+
} catch {
|
|
2861
|
+
return "";
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
function decryptWithMachineKey(encrypted, machineKey) {
|
|
2865
|
+
if (!encrypted.startsWith(ENCRYPTED_PREFIX)) return null;
|
|
2866
|
+
try {
|
|
2867
|
+
const crypto3 = __require("crypto");
|
|
2868
|
+
const parts = encrypted.slice(ENCRYPTED_PREFIX.length).split(":");
|
|
2869
|
+
if (parts.length !== 3) return null;
|
|
2870
|
+
const [ivB64, tagB64, cipherB64] = parts;
|
|
2871
|
+
const iv = Buffer.from(ivB64, "base64");
|
|
2872
|
+
const authTag = Buffer.from(tagB64, "base64");
|
|
2873
|
+
const decipher = crypto3.createDecipheriv("aes-256-gcm", machineKey, iv);
|
|
2874
|
+
decipher.setAuthTag(authTag);
|
|
2875
|
+
let decrypted = decipher.update(cipherB64, "base64", "utf-8");
|
|
2876
|
+
decrypted += decipher.final("utf-8");
|
|
2877
|
+
return decrypted;
|
|
2878
|
+
} catch {
|
|
2879
|
+
return null;
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2786
2882
|
async function getMasterKey() {
|
|
2883
|
+
const nativeValue = macKeychainGet() ?? linuxSecretGet();
|
|
2884
|
+
if (nativeValue) {
|
|
2885
|
+
return Buffer.from(nativeValue, "base64");
|
|
2886
|
+
}
|
|
2787
2887
|
const keytar = await tryKeytar();
|
|
2788
2888
|
if (keytar) {
|
|
2789
2889
|
try {
|
|
2790
|
-
const
|
|
2791
|
-
if (
|
|
2792
|
-
|
|
2890
|
+
const keytarValue = await keytar.getPassword(SERVICE, ACCOUNT);
|
|
2891
|
+
if (keytarValue) {
|
|
2892
|
+
const migrated = macKeychainSet(keytarValue) || linuxSecretSet(keytarValue);
|
|
2893
|
+
if (migrated) {
|
|
2894
|
+
process.stderr.write("[keychain] Migrated key from keytar to native keychain.\n");
|
|
2895
|
+
}
|
|
2896
|
+
return Buffer.from(keytarValue, "base64");
|
|
2793
2897
|
}
|
|
2794
2898
|
} catch {
|
|
2795
2899
|
}
|
|
@@ -2803,8 +2907,31 @@ async function getMasterKey() {
|
|
|
2803
2907
|
return null;
|
|
2804
2908
|
}
|
|
2805
2909
|
try {
|
|
2806
|
-
const content = await readFile3(keyPath, "utf-8");
|
|
2807
|
-
|
|
2910
|
+
const content = (await readFile3(keyPath, "utf-8")).trim();
|
|
2911
|
+
let b64Value;
|
|
2912
|
+
if (content.startsWith(ENCRYPTED_PREFIX)) {
|
|
2913
|
+
const machineKey = deriveMachineKey();
|
|
2914
|
+
if (!machineKey) {
|
|
2915
|
+
process.stderr.write("[keychain] Cannot derive machine key to decrypt stored key.\n");
|
|
2916
|
+
return null;
|
|
2917
|
+
}
|
|
2918
|
+
const decrypted = decryptWithMachineKey(content, machineKey);
|
|
2919
|
+
if (!decrypted) {
|
|
2920
|
+
process.stderr.write(
|
|
2921
|
+
"[keychain] Key decryption failed \u2014 machine may have changed.\n Use your 24-word recovery phrase: exe-os link import\n"
|
|
2922
|
+
);
|
|
2923
|
+
return null;
|
|
2924
|
+
}
|
|
2925
|
+
b64Value = decrypted;
|
|
2926
|
+
} else {
|
|
2927
|
+
b64Value = content;
|
|
2928
|
+
}
|
|
2929
|
+
const key = Buffer.from(b64Value, "base64");
|
|
2930
|
+
const migrated = macKeychainSet(b64Value) || linuxSecretSet(b64Value);
|
|
2931
|
+
if (migrated) {
|
|
2932
|
+
process.stderr.write("[keychain] Migrated key from file to native keychain.\n");
|
|
2933
|
+
}
|
|
2934
|
+
return key;
|
|
2808
2935
|
} catch (err) {
|
|
2809
2936
|
process.stderr.write(
|
|
2810
2937
|
`[keychain] Key read failed at ${keyPath}: ${err instanceof Error ? err.message : String(err)}
|
|
@@ -2813,12 +2940,13 @@ async function getMasterKey() {
|
|
|
2813
2940
|
return null;
|
|
2814
2941
|
}
|
|
2815
2942
|
}
|
|
2816
|
-
var SERVICE, ACCOUNT;
|
|
2943
|
+
var SERVICE, ACCOUNT, ENCRYPTED_PREFIX;
|
|
2817
2944
|
var init_keychain = __esm({
|
|
2818
2945
|
"src/lib/keychain.ts"() {
|
|
2819
2946
|
"use strict";
|
|
2820
2947
|
SERVICE = "exe-mem";
|
|
2821
2948
|
ACCOUNT = "master-key";
|
|
2949
|
+
ENCRYPTED_PREFIX = "enc:";
|
|
2822
2950
|
}
|
|
2823
2951
|
});
|
|
2824
2952
|
|
|
@@ -4341,7 +4469,7 @@ __export(project_name_exports, {
|
|
|
4341
4469
|
_resetCache: () => _resetCache,
|
|
4342
4470
|
getProjectName: () => getProjectName
|
|
4343
4471
|
});
|
|
4344
|
-
import { execSync as
|
|
4472
|
+
import { execSync as execSync3 } from "child_process";
|
|
4345
4473
|
import path9 from "path";
|
|
4346
4474
|
function getProjectName(cwd) {
|
|
4347
4475
|
const dir = cwd ?? process.cwd();
|
|
@@ -4349,7 +4477,7 @@ function getProjectName(cwd) {
|
|
|
4349
4477
|
try {
|
|
4350
4478
|
let repoRoot;
|
|
4351
4479
|
try {
|
|
4352
|
-
const gitCommonDir =
|
|
4480
|
+
const gitCommonDir = execSync3("git rev-parse --path-format=absolute --git-common-dir", {
|
|
4353
4481
|
cwd: dir,
|
|
4354
4482
|
encoding: "utf8",
|
|
4355
4483
|
timeout: 2e3,
|
|
@@ -4357,7 +4485,7 @@ function getProjectName(cwd) {
|
|
|
4357
4485
|
}).trim();
|
|
4358
4486
|
repoRoot = path9.dirname(gitCommonDir);
|
|
4359
4487
|
} catch {
|
|
4360
|
-
repoRoot =
|
|
4488
|
+
repoRoot = execSync3("git rev-parse --show-toplevel", {
|
|
4361
4489
|
cwd: dir,
|
|
4362
4490
|
encoding: "utf8",
|
|
4363
4491
|
timeout: 2e3,
|
|
@@ -4391,14 +4519,14 @@ var file_grep_exports = {};
|
|
|
4391
4519
|
__export(file_grep_exports, {
|
|
4392
4520
|
grepProjectFiles: () => grepProjectFiles
|
|
4393
4521
|
});
|
|
4394
|
-
import { execSync as
|
|
4522
|
+
import { execSync as execSync4 } from "child_process";
|
|
4395
4523
|
import { readFileSync as readFileSync5, readdirSync as readdirSync2, statSync as statSync2, existsSync as existsSync9 } from "fs";
|
|
4396
4524
|
import path10 from "path";
|
|
4397
4525
|
import crypto2 from "crypto";
|
|
4398
4526
|
function hasRipgrep() {
|
|
4399
4527
|
if (_hasRg === null) {
|
|
4400
4528
|
try {
|
|
4401
|
-
|
|
4529
|
+
execSync4("rg --version", { stdio: "ignore", timeout: 2e3 });
|
|
4402
4530
|
_hasRg = true;
|
|
4403
4531
|
} catch {
|
|
4404
4532
|
_hasRg = false;
|
|
@@ -4464,7 +4592,7 @@ function grepWithRipgrep(pattern, projectRoot, patterns) {
|
|
|
4464
4592
|
const globs = (patterns ?? DEFAULT_PATTERNS).map((p) => `--glob '${p}'`).join(" ");
|
|
4465
4593
|
const excludes = EXCLUDE_DIRS.map((d) => `--glob '!${d}'`).join(" ");
|
|
4466
4594
|
const cmd = `rg -i -c --hidden --no-config --no-ignore '${pattern.replace(/'/g, "\\'")}' . ${globs} ${excludes} --max-filesize ${MAX_FILE_SIZE} 2>/dev/null || true`;
|
|
4467
|
-
const output =
|
|
4595
|
+
const output = execSync4(cmd, {
|
|
4468
4596
|
cwd: projectRoot,
|
|
4469
4597
|
encoding: "utf8",
|
|
4470
4598
|
timeout: 3e3,
|
|
@@ -4479,12 +4607,12 @@ function grepWithRipgrep(pattern, projectRoot, patterns) {
|
|
|
4479
4607
|
const matchCount = parseInt(line.slice(colonIdx + 1));
|
|
4480
4608
|
if (isNaN(matchCount) || matchCount === 0) continue;
|
|
4481
4609
|
try {
|
|
4482
|
-
const firstMatch =
|
|
4610
|
+
const firstMatch = execSync4(
|
|
4483
4611
|
`rg -i -n --hidden '${pattern.replace(/'/g, "\\'")}' '${filePath}' --max-count 1 2>/dev/null | head -1`,
|
|
4484
4612
|
{ cwd: projectRoot, encoding: "utf8", timeout: 1e3 }
|
|
4485
4613
|
).trim();
|
|
4486
4614
|
const lineNum = parseInt(firstMatch.split(":")[0] ?? "1");
|
|
4487
|
-
const totalLines =
|
|
4615
|
+
const totalLines = execSync4(`wc -l < '${filePath}'`, {
|
|
4488
4616
|
cwd: projectRoot,
|
|
4489
4617
|
encoding: "utf8",
|
|
4490
4618
|
timeout: 1e3
|
|
@@ -5113,10 +5241,17 @@ async function applyEntityBoost(results, query, client) {
|
|
|
5113
5241
|
if (ENTITY_BOOST_WEIGHT === 0 || results.length === 0) {
|
|
5114
5242
|
return emptyResult;
|
|
5115
5243
|
}
|
|
5116
|
-
|
|
5244
|
+
const debugStart = process.env.EXE_DEBUG_HOOKS ? performance.now() : 0;
|
|
5245
|
+
const debugEnd = () => {
|
|
5246
|
+
if (!process.env.EXE_DEBUG_HOOKS) return;
|
|
5247
|
+
process.stderr.write(
|
|
5248
|
+
`[entity-boost] ${(performance.now() - debugStart).toFixed(3)}ms
|
|
5249
|
+
`
|
|
5250
|
+
);
|
|
5251
|
+
};
|
|
5117
5252
|
const entities = await matchEntities(query, client);
|
|
5118
5253
|
if (entities.length === 0) {
|
|
5119
|
-
|
|
5254
|
+
debugEnd();
|
|
5120
5255
|
return emptyResult;
|
|
5121
5256
|
}
|
|
5122
5257
|
const boostMap = /* @__PURE__ */ new Map();
|
|
@@ -5138,7 +5273,7 @@ async function applyEntityBoost(results, query, client) {
|
|
|
5138
5273
|
await traverseAndScore(entities, client, boostMap, resultIds, graphContextMap);
|
|
5139
5274
|
await applyHyperedgeBoost(entities, client, boostMap, resultIds);
|
|
5140
5275
|
if (boostMap.size === 0) {
|
|
5141
|
-
|
|
5276
|
+
debugEnd();
|
|
5142
5277
|
return emptyResult;
|
|
5143
5278
|
}
|
|
5144
5279
|
const scored = results.map((r, i) => ({
|
|
@@ -5149,7 +5284,7 @@ async function applyEntityBoost(results, query, client) {
|
|
|
5149
5284
|
scored.sort(
|
|
5150
5285
|
(a, b) => b.baseScore + b.entityBoost - (a.baseScore + a.entityBoost)
|
|
5151
5286
|
);
|
|
5152
|
-
|
|
5287
|
+
debugEnd();
|
|
5153
5288
|
return {
|
|
5154
5289
|
results: scored.map((s) => s.record),
|
|
5155
5290
|
graphContext: graphContextMap
|
package/dist/lib/keychain.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
1
8
|
// src/lib/keychain.ts
|
|
2
9
|
import { readFile, writeFile, unlink, mkdir, chmod } from "fs/promises";
|
|
3
10
|
import { existsSync } from "fs";
|
|
11
|
+
import { execSync } from "child_process";
|
|
4
12
|
import path from "path";
|
|
5
13
|
import os from "os";
|
|
6
14
|
var SERVICE = "exe-mem";
|
|
@@ -11,6 +19,83 @@ function getKeyDir() {
|
|
|
11
19
|
function getKeyPath() {
|
|
12
20
|
return path.join(getKeyDir(), "master.key");
|
|
13
21
|
}
|
|
22
|
+
function macKeychainGet() {
|
|
23
|
+
if (process.platform !== "darwin") return null;
|
|
24
|
+
try {
|
|
25
|
+
return execSync(
|
|
26
|
+
`security find-generic-password -s "${SERVICE}" -a "${ACCOUNT}" -w 2>/dev/null`,
|
|
27
|
+
{ encoding: "utf-8", timeout: 5e3 }
|
|
28
|
+
).trim();
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function macKeychainSet(value) {
|
|
34
|
+
if (process.platform !== "darwin") return false;
|
|
35
|
+
try {
|
|
36
|
+
try {
|
|
37
|
+
execSync(
|
|
38
|
+
`security delete-generic-password -s "${SERVICE}" -a "${ACCOUNT}" 2>/dev/null`,
|
|
39
|
+
{ timeout: 5e3 }
|
|
40
|
+
);
|
|
41
|
+
} catch {
|
|
42
|
+
}
|
|
43
|
+
execSync(
|
|
44
|
+
`security add-generic-password -s "${SERVICE}" -a "${ACCOUNT}" -w "${value}"`,
|
|
45
|
+
{ timeout: 5e3 }
|
|
46
|
+
);
|
|
47
|
+
return true;
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function macKeychainDelete() {
|
|
53
|
+
if (process.platform !== "darwin") return false;
|
|
54
|
+
try {
|
|
55
|
+
execSync(
|
|
56
|
+
`security delete-generic-password -s "${SERVICE}" -a "${ACCOUNT}" 2>/dev/null`,
|
|
57
|
+
{ timeout: 5e3 }
|
|
58
|
+
);
|
|
59
|
+
return true;
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function linuxSecretGet() {
|
|
65
|
+
if (process.platform !== "linux") return null;
|
|
66
|
+
try {
|
|
67
|
+
return execSync(
|
|
68
|
+
`secret-tool lookup service "${SERVICE}" account "${ACCOUNT}" 2>/dev/null`,
|
|
69
|
+
{ encoding: "utf-8", timeout: 5e3 }
|
|
70
|
+
).trim();
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function linuxSecretSet(value) {
|
|
76
|
+
if (process.platform !== "linux") return false;
|
|
77
|
+
try {
|
|
78
|
+
execSync(
|
|
79
|
+
`echo -n "${value}" | secret-tool store --label="exe-os master key" service "${SERVICE}" account "${ACCOUNT}"`,
|
|
80
|
+
{ timeout: 5e3 }
|
|
81
|
+
);
|
|
82
|
+
return true;
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function linuxSecretDelete() {
|
|
88
|
+
if (process.platform !== "linux") return false;
|
|
89
|
+
try {
|
|
90
|
+
execSync(
|
|
91
|
+
`secret-tool clear service "${SERVICE}" account "${ACCOUNT}" 2>/dev/null`,
|
|
92
|
+
{ timeout: 5e3 }
|
|
93
|
+
);
|
|
94
|
+
return true;
|
|
95
|
+
} catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
14
99
|
async function tryKeytar() {
|
|
15
100
|
try {
|
|
16
101
|
return await import("keytar");
|
|
@@ -18,13 +103,73 @@ async function tryKeytar() {
|
|
|
18
103
|
return null;
|
|
19
104
|
}
|
|
20
105
|
}
|
|
106
|
+
var ENCRYPTED_PREFIX = "enc:";
|
|
107
|
+
function deriveMachineKey() {
|
|
108
|
+
try {
|
|
109
|
+
const crypto = __require("crypto");
|
|
110
|
+
const material = [
|
|
111
|
+
os.hostname(),
|
|
112
|
+
os.userInfo().username,
|
|
113
|
+
os.arch(),
|
|
114
|
+
os.platform(),
|
|
115
|
+
// Machine ID on Linux (stable across reboots)
|
|
116
|
+
process.platform === "linux" ? readMachineId() : ""
|
|
117
|
+
].join("|");
|
|
118
|
+
return crypto.createHash("sha256").update(material).digest();
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function readMachineId() {
|
|
124
|
+
try {
|
|
125
|
+
const { readFileSync } = __require("fs");
|
|
126
|
+
return readFileSync("/etc/machine-id", "utf-8").trim();
|
|
127
|
+
} catch {
|
|
128
|
+
return "";
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function encryptWithMachineKey(plaintext, machineKey) {
|
|
132
|
+
const crypto = __require("crypto");
|
|
133
|
+
const iv = crypto.randomBytes(12);
|
|
134
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", machineKey, iv);
|
|
135
|
+
let encrypted = cipher.update(plaintext, "utf-8", "base64");
|
|
136
|
+
encrypted += cipher.final("base64");
|
|
137
|
+
const authTag = cipher.getAuthTag().toString("base64");
|
|
138
|
+
return `${ENCRYPTED_PREFIX}${iv.toString("base64")}:${authTag}:${encrypted}`;
|
|
139
|
+
}
|
|
140
|
+
function decryptWithMachineKey(encrypted, machineKey) {
|
|
141
|
+
if (!encrypted.startsWith(ENCRYPTED_PREFIX)) return null;
|
|
142
|
+
try {
|
|
143
|
+
const crypto = __require("crypto");
|
|
144
|
+
const parts = encrypted.slice(ENCRYPTED_PREFIX.length).split(":");
|
|
145
|
+
if (parts.length !== 3) return null;
|
|
146
|
+
const [ivB64, tagB64, cipherB64] = parts;
|
|
147
|
+
const iv = Buffer.from(ivB64, "base64");
|
|
148
|
+
const authTag = Buffer.from(tagB64, "base64");
|
|
149
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", machineKey, iv);
|
|
150
|
+
decipher.setAuthTag(authTag);
|
|
151
|
+
let decrypted = decipher.update(cipherB64, "base64", "utf-8");
|
|
152
|
+
decrypted += decipher.final("utf-8");
|
|
153
|
+
return decrypted;
|
|
154
|
+
} catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
21
158
|
async function getMasterKey() {
|
|
159
|
+
const nativeValue = macKeychainGet() ?? linuxSecretGet();
|
|
160
|
+
if (nativeValue) {
|
|
161
|
+
return Buffer.from(nativeValue, "base64");
|
|
162
|
+
}
|
|
22
163
|
const keytar = await tryKeytar();
|
|
23
164
|
if (keytar) {
|
|
24
165
|
try {
|
|
25
|
-
const
|
|
26
|
-
if (
|
|
27
|
-
|
|
166
|
+
const keytarValue = await keytar.getPassword(SERVICE, ACCOUNT);
|
|
167
|
+
if (keytarValue) {
|
|
168
|
+
const migrated = macKeychainSet(keytarValue) || linuxSecretSet(keytarValue);
|
|
169
|
+
if (migrated) {
|
|
170
|
+
process.stderr.write("[keychain] Migrated key from keytar to native keychain.\n");
|
|
171
|
+
}
|
|
172
|
+
return Buffer.from(keytarValue, "base64");
|
|
28
173
|
}
|
|
29
174
|
} catch {
|
|
30
175
|
}
|
|
@@ -38,8 +183,31 @@ async function getMasterKey() {
|
|
|
38
183
|
return null;
|
|
39
184
|
}
|
|
40
185
|
try {
|
|
41
|
-
const content = await readFile(keyPath, "utf-8");
|
|
42
|
-
|
|
186
|
+
const content = (await readFile(keyPath, "utf-8")).trim();
|
|
187
|
+
let b64Value;
|
|
188
|
+
if (content.startsWith(ENCRYPTED_PREFIX)) {
|
|
189
|
+
const machineKey = deriveMachineKey();
|
|
190
|
+
if (!machineKey) {
|
|
191
|
+
process.stderr.write("[keychain] Cannot derive machine key to decrypt stored key.\n");
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
const decrypted = decryptWithMachineKey(content, machineKey);
|
|
195
|
+
if (!decrypted) {
|
|
196
|
+
process.stderr.write(
|
|
197
|
+
"[keychain] Key decryption failed \u2014 machine may have changed.\n Use your 24-word recovery phrase: exe-os link import\n"
|
|
198
|
+
);
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
b64Value = decrypted;
|
|
202
|
+
} else {
|
|
203
|
+
b64Value = content;
|
|
204
|
+
}
|
|
205
|
+
const key = Buffer.from(b64Value, "base64");
|
|
206
|
+
const migrated = macKeychainSet(b64Value) || linuxSecretSet(b64Value);
|
|
207
|
+
if (migrated) {
|
|
208
|
+
process.stderr.write("[keychain] Migrated key from file to native keychain.\n");
|
|
209
|
+
}
|
|
210
|
+
return key;
|
|
43
211
|
} catch (err) {
|
|
44
212
|
process.stderr.write(
|
|
45
213
|
`[keychain] Key read failed at ${keyPath}: ${err instanceof Error ? err.message : String(err)}
|
|
@@ -50,6 +218,9 @@ async function getMasterKey() {
|
|
|
50
218
|
}
|
|
51
219
|
async function setMasterKey(key) {
|
|
52
220
|
const b64 = key.toString("base64");
|
|
221
|
+
if (macKeychainSet(b64) || linuxSecretSet(b64)) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
53
224
|
const keytar = await tryKeytar();
|
|
54
225
|
if (keytar) {
|
|
55
226
|
try {
|
|
@@ -61,10 +232,23 @@ async function setMasterKey(key) {
|
|
|
61
232
|
const dir = getKeyDir();
|
|
62
233
|
await mkdir(dir, { recursive: true });
|
|
63
234
|
const keyPath = getKeyPath();
|
|
64
|
-
|
|
65
|
-
|
|
235
|
+
const machineKey = deriveMachineKey();
|
|
236
|
+
if (machineKey) {
|
|
237
|
+
const encrypted = encryptWithMachineKey(b64, machineKey);
|
|
238
|
+
await writeFile(keyPath, encrypted + "\n", "utf-8");
|
|
239
|
+
await chmod(keyPath, 384);
|
|
240
|
+
process.stderr.write("[keychain] Key stored encrypted (machine-bound).\n");
|
|
241
|
+
} else {
|
|
242
|
+
await writeFile(keyPath, b64 + "\n", "utf-8");
|
|
243
|
+
await chmod(keyPath, 384);
|
|
244
|
+
process.stderr.write(
|
|
245
|
+
"[keychain] WARNING: Key stored in plaintext file \u2014 no OS keychain available.\n"
|
|
246
|
+
);
|
|
247
|
+
}
|
|
66
248
|
}
|
|
67
249
|
async function deleteMasterKey() {
|
|
250
|
+
macKeychainDelete();
|
|
251
|
+
linuxSecretDelete();
|
|
68
252
|
const keytar = await tryKeytar();
|
|
69
253
|
if (keytar) {
|
|
70
254
|
try {
|