@hasna/accounts 0.1.19 → 0.1.20
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/cli.js +167 -98
- package/dist/index.js +156 -87
- package/dist/lib/safe-path.d.ts.map +1 -1
- package/dist/mcp.js +142 -73
- package/dist/storage.js +107 -38
- package/package.json +1 -1
package/dist/mcp.js
CHANGED
|
@@ -16665,57 +16665,126 @@ class AccountsError extends Error {
|
|
|
16665
16665
|
|
|
16666
16666
|
// src/storage.ts
|
|
16667
16667
|
import { homedir } from "node:os";
|
|
16668
|
-
import { join } from "node:path";
|
|
16668
|
+
import { join as join2 } from "node:path";
|
|
16669
16669
|
import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "node:fs";
|
|
16670
16670
|
|
|
16671
16671
|
// src/lib/safe-path.ts
|
|
16672
16672
|
import { existsSync, lstatSync, mkdirSync, realpathSync } from "node:fs";
|
|
16673
|
-
import { dirname, resolve } from "node:path";
|
|
16673
|
+
import { dirname, isAbsolute, join, parse as parse5, relative, resolve, sep } from "node:path";
|
|
16674
|
+
function lstatIfExists(path) {
|
|
16675
|
+
try {
|
|
16676
|
+
return lstatSync(path);
|
|
16677
|
+
} catch (err) {
|
|
16678
|
+
if (err.code !== "ENOENT")
|
|
16679
|
+
throw err;
|
|
16680
|
+
return;
|
|
16681
|
+
}
|
|
16682
|
+
}
|
|
16674
16683
|
function throwIfSymlink(path, label) {
|
|
16675
|
-
if (
|
|
16684
|
+
if (lstatIfExists(path)?.isSymbolicLink()) {
|
|
16676
16685
|
throw new AccountsError(`${label}: ${path}`);
|
|
16677
16686
|
}
|
|
16678
16687
|
}
|
|
16679
|
-
function
|
|
16680
|
-
|
|
16681
|
-
|
|
16682
|
-
|
|
16683
|
-
|
|
16684
|
-
|
|
16685
|
-
|
|
16686
|
-
|
|
16687
|
-
|
|
16688
|
-
|
|
16689
|
-
|
|
16690
|
-
|
|
16691
|
-
}
|
|
16692
|
-
|
|
16693
|
-
|
|
16694
|
-
|
|
16695
|
-
|
|
16696
|
-
}
|
|
16697
|
-
}
|
|
16698
|
-
|
|
16699
|
-
|
|
16700
|
-
|
|
16701
|
-
|
|
16702
|
-
|
|
16703
|
-
|
|
16704
|
-
|
|
16688
|
+
function isAllowedSystemDirectorySymlink(path) {
|
|
16689
|
+
if (path !== "/var" && path !== "/tmp")
|
|
16690
|
+
return false;
|
|
16691
|
+
try {
|
|
16692
|
+
return realpathSync(path) === `/private${path}`;
|
|
16693
|
+
} catch {
|
|
16694
|
+
return false;
|
|
16695
|
+
}
|
|
16696
|
+
}
|
|
16697
|
+
function assertDirectory(path, label, opts) {
|
|
16698
|
+
const stat = lstatIfExists(path);
|
|
16699
|
+
if (!stat) {
|
|
16700
|
+
throw new AccountsError(`refusing to write under missing directory: ${path}`);
|
|
16701
|
+
}
|
|
16702
|
+
if (stat.isSymbolicLink()) {
|
|
16703
|
+
if (opts?.allowSystemSymlink && isAllowedSystemDirectorySymlink(path))
|
|
16704
|
+
return;
|
|
16705
|
+
throw new AccountsError(`${label}: ${path}`);
|
|
16706
|
+
}
|
|
16707
|
+
if (!stat.isDirectory()) {
|
|
16708
|
+
throw new AccountsError(`refusing to write under non-directory path: ${path}`);
|
|
16709
|
+
}
|
|
16710
|
+
}
|
|
16711
|
+
function assertInsideBase(absPath, base, originalPath) {
|
|
16712
|
+
const rel = relative(base, absPath);
|
|
16713
|
+
if (rel === "")
|
|
16714
|
+
return rel;
|
|
16715
|
+
if (rel === ".." || rel.startsWith(".." + sep) || isAbsolute(rel)) {
|
|
16716
|
+
throw new AccountsError(`refusing to write outside profile directory: ${originalPath}`);
|
|
16717
|
+
}
|
|
16718
|
+
return rel;
|
|
16719
|
+
}
|
|
16720
|
+
function assertExistingDirectoryComponentsSafe(path, label) {
|
|
16721
|
+
const root = parse5(path).root;
|
|
16722
|
+
const rel = relative(root, path);
|
|
16723
|
+
const segments = rel ? rel.split(sep).filter(Boolean) : [];
|
|
16724
|
+
let cursor = root;
|
|
16725
|
+
assertDirectory(cursor, label, { allowSystemSymlink: true });
|
|
16726
|
+
for (const segment of segments) {
|
|
16727
|
+
cursor = join(cursor, segment);
|
|
16728
|
+
if (!lstatIfExists(cursor))
|
|
16729
|
+
return;
|
|
16730
|
+
assertDirectory(cursor, label, { allowSystemSymlink: true });
|
|
16731
|
+
}
|
|
16732
|
+
}
|
|
16733
|
+
function ensureBoundaryRootSafe(base) {
|
|
16734
|
+
const missing = [];
|
|
16735
|
+
let cursor = base;
|
|
16736
|
+
while (!lstatIfExists(cursor)) {
|
|
16737
|
+
missing.unshift(cursor);
|
|
16738
|
+
const parent = dirname(cursor);
|
|
16739
|
+
if (parent === cursor)
|
|
16740
|
+
break;
|
|
16741
|
+
cursor = parent;
|
|
16742
|
+
}
|
|
16743
|
+
assertDirectory(cursor, "refusing to use symlink base directory");
|
|
16744
|
+
for (const dir of missing) {
|
|
16745
|
+
if (lstatIfExists(dir)) {
|
|
16746
|
+
assertDirectory(dir, "refusing to use symlink base directory");
|
|
16747
|
+
} else {
|
|
16748
|
+
mkdirSync(dir);
|
|
16749
|
+
assertDirectory(dir, "refusing to use symlink base directory");
|
|
16750
|
+
}
|
|
16751
|
+
}
|
|
16752
|
+
assertDirectory(base, "refusing to use symlink base directory");
|
|
16753
|
+
}
|
|
16754
|
+
function ensureDirectoryChainSafe(parent, startAt) {
|
|
16755
|
+
const root = startAt ?? parse5(parent).root;
|
|
16756
|
+
const rel = relative(root, parent);
|
|
16757
|
+
const segments = rel ? rel.split(sep).filter(Boolean) : [];
|
|
16758
|
+
let cursor = root;
|
|
16759
|
+
if (startAt)
|
|
16760
|
+
assertDirectory(startAt, "refusing to write under symlink directory");
|
|
16761
|
+
for (const segment of segments) {
|
|
16762
|
+
cursor = join(cursor, segment);
|
|
16763
|
+
if (lstatIfExists(cursor)) {
|
|
16764
|
+
assertDirectory(cursor, "refusing to write under symlink directory");
|
|
16765
|
+
} else {
|
|
16766
|
+
mkdirSync(cursor);
|
|
16767
|
+
assertDirectory(cursor, "refusing to write under symlink directory");
|
|
16705
16768
|
}
|
|
16706
16769
|
}
|
|
16707
16770
|
}
|
|
16708
16771
|
function assertSafeWritePath(filePath, opts) {
|
|
16709
16772
|
const absFile = resolve(filePath);
|
|
16710
16773
|
const parent = dirname(absFile);
|
|
16711
|
-
|
|
16712
|
-
|
|
16774
|
+
const base = opts?.mustStayUnder ? resolve(opts.mustStayUnder) : undefined;
|
|
16775
|
+
if (base) {
|
|
16776
|
+
assertInsideBase(absFile, base, filePath);
|
|
16777
|
+
assertExistingDirectoryComponentsSafe(base, "refusing to use symlink base directory");
|
|
16778
|
+
ensureBoundaryRootSafe(base);
|
|
16779
|
+
ensureDirectoryChainSafe(parent, base);
|
|
16780
|
+
} else {
|
|
16781
|
+
ensureDirectoryChainSafe(parent);
|
|
16782
|
+
}
|
|
16713
16783
|
throwIfSymlink(absFile, "refusing to write through symlink");
|
|
16714
|
-
assertDirChainSafe(absFile, opts?.mustStayUnder);
|
|
16715
16784
|
const resolved = realpathSync(existsSync(absFile) ? absFile : parent);
|
|
16716
|
-
if (
|
|
16717
|
-
const
|
|
16718
|
-
if (resolved !==
|
|
16785
|
+
if (base) {
|
|
16786
|
+
const realBase = realpathSync(base);
|
|
16787
|
+
if (resolved !== realBase && !resolved.startsWith(realBase + sep)) {
|
|
16719
16788
|
throw new AccountsError(`refusing to write outside profile directory: ${filePath}`);
|
|
16720
16789
|
}
|
|
16721
16790
|
}
|
|
@@ -16744,13 +16813,13 @@ function accountsHome() {
|
|
|
16744
16813
|
const override = process.env.ACCOUNTS_HOME;
|
|
16745
16814
|
if (override && override.trim())
|
|
16746
16815
|
return validateEnvPath(override, "ACCOUNTS_HOME");
|
|
16747
|
-
return
|
|
16816
|
+
return join2(homedir(), ".hasna", "accounts");
|
|
16748
16817
|
}
|
|
16749
16818
|
function storePath() {
|
|
16750
16819
|
const override = process.env.ACCOUNTS_STORE_PATH;
|
|
16751
16820
|
if (override && override.trim())
|
|
16752
16821
|
return validateEnvPath(override, "ACCOUNTS_STORE_PATH");
|
|
16753
|
-
return
|
|
16822
|
+
return join2(accountsHome(), "accounts.json");
|
|
16754
16823
|
}
|
|
16755
16824
|
var EMPTY_STORE = { version: 1, current: {}, applied: {}, profiles: [], tools: [] };
|
|
16756
16825
|
function loadStore() {
|
|
@@ -16793,7 +16862,7 @@ function loadStore() {
|
|
|
16793
16862
|
function saveStore(store) {
|
|
16794
16863
|
const path = storePath();
|
|
16795
16864
|
assertSafeWritePath(path, { mustStayUnder: accountsHome() });
|
|
16796
|
-
mkdirSync2(
|
|
16865
|
+
mkdirSync2(join2(path, ".."), { recursive: true });
|
|
16797
16866
|
if (existsSync2(path))
|
|
16798
16867
|
chmodSync(path, 384);
|
|
16799
16868
|
writeFileSync(path, JSON.stringify(store, null, 2) + `
|
|
@@ -16803,7 +16872,7 @@ function saveStore(store) {
|
|
|
16803
16872
|
|
|
16804
16873
|
// src/lib/tools.ts
|
|
16805
16874
|
import { homedir as homedir2 } from "node:os";
|
|
16806
|
-
import { join as
|
|
16875
|
+
import { join as join3 } from "node:path";
|
|
16807
16876
|
var BUILTIN_TOOLS = [
|
|
16808
16877
|
{
|
|
16809
16878
|
id: "claude",
|
|
@@ -16812,7 +16881,7 @@ var BUILTIN_TOOLS = [
|
|
|
16812
16881
|
extraEnv: {
|
|
16813
16882
|
TELEGRAM_STATE_DIR: "{profileDir}/channels/telegram"
|
|
16814
16883
|
},
|
|
16815
|
-
defaultDir:
|
|
16884
|
+
defaultDir: join3(homedir2(), ".claude"),
|
|
16816
16885
|
bin: "claude",
|
|
16817
16886
|
loginHint: "run /login inside Claude, then /exit when done",
|
|
16818
16887
|
resumeArgs: ["--continue"],
|
|
@@ -16832,7 +16901,7 @@ var BUILTIN_TOOLS = [
|
|
|
16832
16901
|
id: "codex-app",
|
|
16833
16902
|
label: "Codex App",
|
|
16834
16903
|
envVar: "CODEX_HOME",
|
|
16835
|
-
defaultDir:
|
|
16904
|
+
defaultDir: join3(homedir2(), ".codex"),
|
|
16836
16905
|
bin: "/Applications/Codex.app/Contents/MacOS/Codex",
|
|
16837
16906
|
loginHint: "sign in inside Codex.app, then quit the app when the profile is ready",
|
|
16838
16907
|
launchArgs: ["--user-data-dir={profileDir}/electron-user-data"],
|
|
@@ -16842,7 +16911,7 @@ var BUILTIN_TOOLS = [
|
|
|
16842
16911
|
id: "codex",
|
|
16843
16912
|
label: "Codex CLI",
|
|
16844
16913
|
envVar: "CODEX_HOME",
|
|
16845
|
-
defaultDir:
|
|
16914
|
+
defaultDir: join3(homedir2(), ".codex"),
|
|
16846
16915
|
bin: "codex",
|
|
16847
16916
|
loginArgs: ["login"],
|
|
16848
16917
|
loginHint: "complete the Codex login flow for this CODEX_HOME",
|
|
@@ -16855,7 +16924,7 @@ var BUILTIN_TOOLS = [
|
|
|
16855
16924
|
id: "takumi",
|
|
16856
16925
|
label: "Takumi",
|
|
16857
16926
|
envVar: "TAKUMI_CONFIG_DIR",
|
|
16858
|
-
defaultDir:
|
|
16927
|
+
defaultDir: join3(homedir2(), ".takumi"),
|
|
16859
16928
|
bin: "takumi",
|
|
16860
16929
|
loginHint: "complete Takumi auth in this TAKUMI_CONFIG_DIR",
|
|
16861
16930
|
resumeArgs: ["--continue"],
|
|
@@ -16875,7 +16944,7 @@ var BUILTIN_TOOLS = [
|
|
|
16875
16944
|
id: "gemini",
|
|
16876
16945
|
label: "Gemini CLI",
|
|
16877
16946
|
envVar: "GEMINI_CONFIG_DIR",
|
|
16878
|
-
defaultDir:
|
|
16947
|
+
defaultDir: join3(homedir2(), ".gemini"),
|
|
16879
16948
|
bin: "gemini",
|
|
16880
16949
|
loginHint: "complete Gemini auth in this GEMINI_CONFIG_DIR",
|
|
16881
16950
|
permissionArgs: {
|
|
@@ -16893,7 +16962,7 @@ var BUILTIN_TOOLS = [
|
|
|
16893
16962
|
XDG_CONFIG_HOME: "{profileDir}/xdg-config",
|
|
16894
16963
|
XDG_DATA_HOME: "{profileDir}/xdg-data"
|
|
16895
16964
|
},
|
|
16896
|
-
defaultDir:
|
|
16965
|
+
defaultDir: join3(homedir2(), ".config", "opencode"),
|
|
16897
16966
|
bin: "opencode",
|
|
16898
16967
|
loginArgs: ["auth", "login"],
|
|
16899
16968
|
loginHint: "complete opencode auth login for this isolated config/data root",
|
|
@@ -16903,7 +16972,7 @@ var BUILTIN_TOOLS = [
|
|
|
16903
16972
|
id: "cursor",
|
|
16904
16973
|
label: "Cursor Agent",
|
|
16905
16974
|
envVar: "CURSOR_CONFIG_DIR",
|
|
16906
|
-
defaultDir:
|
|
16975
|
+
defaultDir: join3(homedir2(), ".cursor"),
|
|
16907
16976
|
bin: "cursor-agent",
|
|
16908
16977
|
loginArgs: ["login"],
|
|
16909
16978
|
loginHint: "complete cursor-agent login for this CURSOR_CONFIG_DIR"
|
|
@@ -16912,7 +16981,7 @@ var BUILTIN_TOOLS = [
|
|
|
16912
16981
|
id: "pi",
|
|
16913
16982
|
label: "Pi Coding Agent",
|
|
16914
16983
|
envVar: "PI_CODING_AGENT_HOME",
|
|
16915
|
-
defaultDir:
|
|
16984
|
+
defaultDir: join3(homedir2(), ".pi"),
|
|
16916
16985
|
bin: "pi",
|
|
16917
16986
|
loginHint: "complete Pi coding agent auth in this PI_CODING_AGENT_HOME"
|
|
16918
16987
|
},
|
|
@@ -16920,7 +16989,7 @@ var BUILTIN_TOOLS = [
|
|
|
16920
16989
|
id: "hermes",
|
|
16921
16990
|
label: "Hermes",
|
|
16922
16991
|
envVar: "HERMES_HOME",
|
|
16923
|
-
defaultDir:
|
|
16992
|
+
defaultDir: join3(homedir2(), ".hermes"),
|
|
16924
16993
|
bin: "hermes",
|
|
16925
16994
|
loginHint: "complete Hermes auth in this HERMES_HOME",
|
|
16926
16995
|
permissionArgs: {
|
|
@@ -16932,7 +17001,7 @@ var BUILTIN_TOOLS = [
|
|
|
16932
17001
|
id: "kimi",
|
|
16933
17002
|
label: "Kimi Code",
|
|
16934
17003
|
envVar: "KIMI_CODE_HOME",
|
|
16935
|
-
defaultDir:
|
|
17004
|
+
defaultDir: join3(homedir2(), ".kimi-code"),
|
|
16936
17005
|
bin: "kimi",
|
|
16937
17006
|
loginArgs: ["login"],
|
|
16938
17007
|
loginHint: "complete kimi login for this KIMI_CODE_HOME",
|
|
@@ -16947,7 +17016,7 @@ var BUILTIN_TOOLS = [
|
|
|
16947
17016
|
id: "grok",
|
|
16948
17017
|
label: "Grok Build",
|
|
16949
17018
|
envVar: "HOME",
|
|
16950
|
-
defaultDir:
|
|
17019
|
+
defaultDir: join3(homedir2(), ".grok"),
|
|
16951
17020
|
bin: "grok",
|
|
16952
17021
|
loginArgs: ["login"],
|
|
16953
17022
|
loginHint: "complete grok login in this process-scoped HOME; prefer launch/shell over exporting HOME globally"
|
|
@@ -17067,11 +17136,11 @@ function currentProfile(toolId) {
|
|
|
17067
17136
|
|
|
17068
17137
|
// src/lib/claude-auth.ts
|
|
17069
17138
|
import { copyFileSync, existsSync as existsSync3, lstatSync as lstatSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync2, statSync, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
17070
|
-
import { dirname as dirname3, join as
|
|
17139
|
+
import { dirname as dirname3, join as join5 } from "node:path";
|
|
17071
17140
|
|
|
17072
17141
|
// src/lib/claude-layout.ts
|
|
17073
17142
|
import { homedir as homedir3 } from "node:os";
|
|
17074
|
-
import { dirname as dirname2, join as
|
|
17143
|
+
import { dirname as dirname2, join as join4 } from "node:path";
|
|
17075
17144
|
var CLAUDE_KEYCHAIN_SERVICE = "Claude Code-credentials";
|
|
17076
17145
|
var ACCOUNTS_AUTH_DIR = ".accounts-auth";
|
|
17077
17146
|
var OAUTH_SNAPSHOT = "oauth-account.json";
|
|
@@ -17083,32 +17152,32 @@ function liveClaudeBase() {
|
|
|
17083
17152
|
}
|
|
17084
17153
|
function liveClaudePaths() {
|
|
17085
17154
|
const base = liveClaudeBase();
|
|
17086
|
-
const configDir =
|
|
17155
|
+
const configDir = join4(base, ".claude");
|
|
17087
17156
|
return {
|
|
17088
17157
|
configDir,
|
|
17089
|
-
homeJson:
|
|
17090
|
-
credentialsFile:
|
|
17158
|
+
homeJson: join4(base, ".claude.json"),
|
|
17159
|
+
credentialsFile: join4(configDir, ".credentials.json")
|
|
17091
17160
|
};
|
|
17092
17161
|
}
|
|
17093
17162
|
function profileAccountJsonPaths(profileDir, tool) {
|
|
17094
17163
|
if (!tool.accountFile)
|
|
17095
17164
|
return [];
|
|
17096
|
-
const paths = [
|
|
17165
|
+
const paths = [join4(profileDir, tool.accountFile)];
|
|
17097
17166
|
if (profileDir === tool.defaultDir)
|
|
17098
|
-
paths.push(
|
|
17167
|
+
paths.push(join4(dirname2(profileDir), tool.accountFile));
|
|
17099
17168
|
return paths;
|
|
17100
17169
|
}
|
|
17101
17170
|
function profileAuthDir(profileDir) {
|
|
17102
|
-
return
|
|
17171
|
+
return join4(profileDir, ACCOUNTS_AUTH_DIR);
|
|
17103
17172
|
}
|
|
17104
17173
|
function profileOAuthSnapshot(profileDir) {
|
|
17105
|
-
return
|
|
17174
|
+
return join4(profileAuthDir(profileDir), OAUTH_SNAPSHOT);
|
|
17106
17175
|
}
|
|
17107
17176
|
function profileCredentialsSnapshot(profileDir) {
|
|
17108
|
-
return
|
|
17177
|
+
return join4(profileAuthDir(profileDir), CREDENTIALS_SNAPSHOT);
|
|
17109
17178
|
}
|
|
17110
17179
|
function profileKeychainSnapshot(profileDir) {
|
|
17111
|
-
return
|
|
17180
|
+
return join4(profileAuthDir(profileDir), KEYCHAIN_SNAPSHOT);
|
|
17112
17181
|
}
|
|
17113
17182
|
|
|
17114
17183
|
// src/lib/keychain.ts
|
|
@@ -17262,7 +17331,7 @@ function mergeOAuthInto(paths, oauth, allowDelete, stayUnder) {
|
|
|
17262
17331
|
}
|
|
17263
17332
|
}
|
|
17264
17333
|
function sanitizeSettingsFile(configDir, stayUnder) {
|
|
17265
|
-
const settingsPath =
|
|
17334
|
+
const settingsPath = join5(configDir, "settings.json");
|
|
17266
17335
|
const settings = readJsonFile(settingsPath);
|
|
17267
17336
|
if (!settings)
|
|
17268
17337
|
return false;
|
|
@@ -17309,7 +17378,7 @@ function liveOAuthEmail() {
|
|
|
17309
17378
|
}
|
|
17310
17379
|
function snapshotLiveAuthToProfile(profileDir, _tool) {
|
|
17311
17380
|
const authDir = profileAuthDir(profileDir);
|
|
17312
|
-
assertSafeWritePath(
|
|
17381
|
+
assertSafeWritePath(join5(authDir, OAUTH_SNAPSHOT), { mustStayUnder: profileDir });
|
|
17313
17382
|
mkdirSync3(authDir, { recursive: true });
|
|
17314
17383
|
const live = liveClaudePaths();
|
|
17315
17384
|
const oauth = readOAuthFromPaths([live.homeJson]);
|
|
@@ -17328,14 +17397,14 @@ function snapshotLiveAuthToProfile(profileDir, _tool) {
|
|
|
17328
17397
|
}
|
|
17329
17398
|
function ensureProfileAuthSnapshot(profileDir, tool, opts = {}) {
|
|
17330
17399
|
const authDir = profileAuthDir(profileDir);
|
|
17331
|
-
assertSafeWritePath(
|
|
17400
|
+
assertSafeWritePath(join5(authDir, OAUTH_SNAPSHOT), { mustStayUnder: profileDir });
|
|
17332
17401
|
mkdirSync3(authDir, { recursive: true });
|
|
17333
17402
|
const oauthSource = findOAuthSource(profileAccountJsonPaths(profileDir, tool));
|
|
17334
17403
|
const oauthSnap = profileOAuthSnapshot(profileDir);
|
|
17335
17404
|
if (oauthSource && (opts.overwrite || snapshotIsStale(oauthSource.path, oauthSnap))) {
|
|
17336
17405
|
writeJsonFile(oauthSnap, { oauthAccount: oauthSource.oauth }, profileDir);
|
|
17337
17406
|
}
|
|
17338
|
-
const credFile =
|
|
17407
|
+
const credFile = join5(profileDir, ".credentials.json");
|
|
17339
17408
|
const credSnap = profileCredentialsSnapshot(profileDir);
|
|
17340
17409
|
if (existsSync3(credFile) && (opts.overwrite || snapshotIsStale(credFile, credSnap))) {
|
|
17341
17410
|
assertSafeWritePath(credSnap, { mustStayUnder: profileDir });
|
|
@@ -17397,9 +17466,9 @@ function hasAuthSnapshot(profileDir) {
|
|
|
17397
17466
|
|
|
17398
17467
|
// src/lib/apply-lock.ts
|
|
17399
17468
|
import { closeSync, existsSync as existsSync4, mkdirSync as mkdirSync4, openSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
17400
|
-
import { join as
|
|
17469
|
+
import { join as join6 } from "node:path";
|
|
17401
17470
|
function lockPath() {
|
|
17402
|
-
return
|
|
17471
|
+
return join6(accountsHome(), ".apply.lock");
|
|
17403
17472
|
}
|
|
17404
17473
|
function withApplyLock(fn) {
|
|
17405
17474
|
const home = accountsHome();
|
|
@@ -17470,7 +17539,7 @@ function applyProfileUnlocked(name, toolId) {
|
|
|
17470
17539
|
|
|
17471
17540
|
// src/lib/codex-app.ts
|
|
17472
17541
|
import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
17473
|
-
import { join as
|
|
17542
|
+
import { join as join7 } from "node:path";
|
|
17474
17543
|
var FILE_CREDENTIALS_LINE = 'cli_auth_credentials_store = "file"';
|
|
17475
17544
|
function insertRootConfigLine(config2, line) {
|
|
17476
17545
|
if (config2.trim() === "")
|
|
@@ -17497,7 +17566,7 @@ ${after}${after.endsWith(`
|
|
|
17497
17566
|
}
|
|
17498
17567
|
function ensureCodexAppProfileConfig(profileDir) {
|
|
17499
17568
|
mkdirSync5(profileDir, { recursive: true });
|
|
17500
|
-
const configPath =
|
|
17569
|
+
const configPath = join7(profileDir, "config.toml");
|
|
17501
17570
|
const current = existsSync5(configPath) ? readFileSync3(configPath, "utf8") : "";
|
|
17502
17571
|
if (/^\s*cli_auth_credentials_store\s*=/.test(current))
|
|
17503
17572
|
return;
|
|
@@ -17582,20 +17651,20 @@ function switchProfile(name, opts = {}) {
|
|
|
17582
17651
|
import { createHash } from "node:crypto";
|
|
17583
17652
|
import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync4, readdirSync, rmSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
17584
17653
|
import { createConnection, createServer } from "node:net";
|
|
17585
|
-
import { basename, join as
|
|
17654
|
+
import { basename, join as join8 } from "node:path";
|
|
17586
17655
|
var STATE_SUFFIX = ".json";
|
|
17587
17656
|
function supervisorDir() {
|
|
17588
|
-
return
|
|
17657
|
+
return join8(accountsHome(), "supervisors");
|
|
17589
17658
|
}
|
|
17590
17659
|
function supervisorStatePath(toolId) {
|
|
17591
|
-
return
|
|
17660
|
+
return join8(supervisorDir(), `${toolId}${STATE_SUFFIX}`);
|
|
17592
17661
|
}
|
|
17593
17662
|
function supervisorSocketPath(toolId) {
|
|
17594
17663
|
if (process.platform === "win32") {
|
|
17595
17664
|
const hash = createHash("sha1").update(accountsHome()).digest("hex").slice(0, 12);
|
|
17596
17665
|
return `\\\\.\\pipe\\hasna-accounts-${hash}-${toolId}`;
|
|
17597
17666
|
}
|
|
17598
|
-
return
|
|
17667
|
+
return join8(supervisorDir(), `${toolId}.sock`);
|
|
17599
17668
|
}
|
|
17600
17669
|
function parseState(raw) {
|
|
17601
17670
|
const data = JSON.parse(raw);
|
package/dist/storage.js
CHANGED
|
@@ -18,7 +18,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
18
18
|
// src/storage.ts
|
|
19
19
|
import { homedir } from "node:os";
|
|
20
20
|
import { hostname } from "node:os";
|
|
21
|
-
import { join } from "node:path";
|
|
21
|
+
import { join as join2 } from "node:path";
|
|
22
22
|
import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "node:fs";
|
|
23
23
|
|
|
24
24
|
// node_modules/zod/v3/external.js
|
|
@@ -4038,52 +4038,121 @@ class AccountsError extends Error {
|
|
|
4038
4038
|
|
|
4039
4039
|
// src/lib/safe-path.ts
|
|
4040
4040
|
import { existsSync, lstatSync, mkdirSync, realpathSync } from "node:fs";
|
|
4041
|
-
import { dirname, resolve } from "node:path";
|
|
4041
|
+
import { dirname, isAbsolute, join, parse, relative, resolve, sep } from "node:path";
|
|
4042
|
+
function lstatIfExists(path) {
|
|
4043
|
+
try {
|
|
4044
|
+
return lstatSync(path);
|
|
4045
|
+
} catch (err) {
|
|
4046
|
+
if (err.code !== "ENOENT")
|
|
4047
|
+
throw err;
|
|
4048
|
+
return;
|
|
4049
|
+
}
|
|
4050
|
+
}
|
|
4042
4051
|
function throwIfSymlink(path, label) {
|
|
4043
|
-
if (
|
|
4052
|
+
if (lstatIfExists(path)?.isSymbolicLink()) {
|
|
4044
4053
|
throw new AccountsError(`${label}: ${path}`);
|
|
4045
4054
|
}
|
|
4046
4055
|
}
|
|
4047
|
-
function
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
}
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4056
|
+
function isAllowedSystemDirectorySymlink(path) {
|
|
4057
|
+
if (path !== "/var" && path !== "/tmp")
|
|
4058
|
+
return false;
|
|
4059
|
+
try {
|
|
4060
|
+
return realpathSync(path) === `/private${path}`;
|
|
4061
|
+
} catch {
|
|
4062
|
+
return false;
|
|
4063
|
+
}
|
|
4064
|
+
}
|
|
4065
|
+
function assertDirectory(path, label, opts) {
|
|
4066
|
+
const stat = lstatIfExists(path);
|
|
4067
|
+
if (!stat) {
|
|
4068
|
+
throw new AccountsError(`refusing to write under missing directory: ${path}`);
|
|
4069
|
+
}
|
|
4070
|
+
if (stat.isSymbolicLink()) {
|
|
4071
|
+
if (opts?.allowSystemSymlink && isAllowedSystemDirectorySymlink(path))
|
|
4072
|
+
return;
|
|
4073
|
+
throw new AccountsError(`${label}: ${path}`);
|
|
4074
|
+
}
|
|
4075
|
+
if (!stat.isDirectory()) {
|
|
4076
|
+
throw new AccountsError(`refusing to write under non-directory path: ${path}`);
|
|
4077
|
+
}
|
|
4078
|
+
}
|
|
4079
|
+
function assertInsideBase(absPath, base, originalPath) {
|
|
4080
|
+
const rel = relative(base, absPath);
|
|
4081
|
+
if (rel === "")
|
|
4082
|
+
return rel;
|
|
4083
|
+
if (rel === ".." || rel.startsWith(".." + sep) || isAbsolute(rel)) {
|
|
4084
|
+
throw new AccountsError(`refusing to write outside profile directory: ${originalPath}`);
|
|
4085
|
+
}
|
|
4086
|
+
return rel;
|
|
4087
|
+
}
|
|
4088
|
+
function assertExistingDirectoryComponentsSafe(path, label) {
|
|
4089
|
+
const root = parse(path).root;
|
|
4090
|
+
const rel = relative(root, path);
|
|
4091
|
+
const segments = rel ? rel.split(sep).filter(Boolean) : [];
|
|
4092
|
+
let cursor = root;
|
|
4093
|
+
assertDirectory(cursor, label, { allowSystemSymlink: true });
|
|
4094
|
+
for (const segment of segments) {
|
|
4095
|
+
cursor = join(cursor, segment);
|
|
4096
|
+
if (!lstatIfExists(cursor))
|
|
4097
|
+
return;
|
|
4098
|
+
assertDirectory(cursor, label, { allowSystemSymlink: true });
|
|
4099
|
+
}
|
|
4100
|
+
}
|
|
4101
|
+
function ensureBoundaryRootSafe(base) {
|
|
4102
|
+
const missing = [];
|
|
4103
|
+
let cursor = base;
|
|
4104
|
+
while (!lstatIfExists(cursor)) {
|
|
4105
|
+
missing.unshift(cursor);
|
|
4106
|
+
const parent = dirname(cursor);
|
|
4107
|
+
if (parent === cursor)
|
|
4108
|
+
break;
|
|
4109
|
+
cursor = parent;
|
|
4110
|
+
}
|
|
4111
|
+
assertDirectory(cursor, "refusing to use symlink base directory");
|
|
4112
|
+
for (const dir of missing) {
|
|
4113
|
+
if (lstatIfExists(dir)) {
|
|
4114
|
+
assertDirectory(dir, "refusing to use symlink base directory");
|
|
4115
|
+
} else {
|
|
4116
|
+
mkdirSync(dir);
|
|
4117
|
+
assertDirectory(dir, "refusing to use symlink base directory");
|
|
4064
4118
|
}
|
|
4065
|
-
}
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4119
|
+
}
|
|
4120
|
+
assertDirectory(base, "refusing to use symlink base directory");
|
|
4121
|
+
}
|
|
4122
|
+
function ensureDirectoryChainSafe(parent, startAt) {
|
|
4123
|
+
const root = startAt ?? parse(parent).root;
|
|
4124
|
+
const rel = relative(root, parent);
|
|
4125
|
+
const segments = rel ? rel.split(sep).filter(Boolean) : [];
|
|
4126
|
+
let cursor = root;
|
|
4127
|
+
if (startAt)
|
|
4128
|
+
assertDirectory(startAt, "refusing to write under symlink directory");
|
|
4129
|
+
for (const segment of segments) {
|
|
4130
|
+
cursor = join(cursor, segment);
|
|
4131
|
+
if (lstatIfExists(cursor)) {
|
|
4132
|
+
assertDirectory(cursor, "refusing to write under symlink directory");
|
|
4133
|
+
} else {
|
|
4134
|
+
mkdirSync(cursor);
|
|
4135
|
+
assertDirectory(cursor, "refusing to write under symlink directory");
|
|
4073
4136
|
}
|
|
4074
4137
|
}
|
|
4075
4138
|
}
|
|
4076
4139
|
function assertSafeWritePath(filePath, opts) {
|
|
4077
4140
|
const absFile = resolve(filePath);
|
|
4078
4141
|
const parent = dirname(absFile);
|
|
4079
|
-
|
|
4080
|
-
|
|
4142
|
+
const base = opts?.mustStayUnder ? resolve(opts.mustStayUnder) : undefined;
|
|
4143
|
+
if (base) {
|
|
4144
|
+
assertInsideBase(absFile, base, filePath);
|
|
4145
|
+
assertExistingDirectoryComponentsSafe(base, "refusing to use symlink base directory");
|
|
4146
|
+
ensureBoundaryRootSafe(base);
|
|
4147
|
+
ensureDirectoryChainSafe(parent, base);
|
|
4148
|
+
} else {
|
|
4149
|
+
ensureDirectoryChainSafe(parent);
|
|
4150
|
+
}
|
|
4081
4151
|
throwIfSymlink(absFile, "refusing to write through symlink");
|
|
4082
|
-
assertDirChainSafe(absFile, opts?.mustStayUnder);
|
|
4083
4152
|
const resolved = realpathSync(existsSync(absFile) ? absFile : parent);
|
|
4084
|
-
if (
|
|
4085
|
-
const
|
|
4086
|
-
if (resolved !==
|
|
4153
|
+
if (base) {
|
|
4154
|
+
const realBase = realpathSync(base);
|
|
4155
|
+
if (resolved !== realBase && !resolved.startsWith(realBase + sep)) {
|
|
4087
4156
|
throw new AccountsError(`refusing to write outside profile directory: ${filePath}`);
|
|
4088
4157
|
}
|
|
4089
4158
|
}
|
|
@@ -4122,16 +4191,16 @@ function accountsHome() {
|
|
|
4122
4191
|
const override = process.env.ACCOUNTS_HOME;
|
|
4123
4192
|
if (override && override.trim())
|
|
4124
4193
|
return validateEnvPath(override, "ACCOUNTS_HOME");
|
|
4125
|
-
return
|
|
4194
|
+
return join2(homedir(), ".hasna", "accounts");
|
|
4126
4195
|
}
|
|
4127
4196
|
function storePath() {
|
|
4128
4197
|
const override = process.env.ACCOUNTS_STORE_PATH;
|
|
4129
4198
|
if (override && override.trim())
|
|
4130
4199
|
return validateEnvPath(override, "ACCOUNTS_STORE_PATH");
|
|
4131
|
-
return
|
|
4200
|
+
return join2(accountsHome(), "accounts.json");
|
|
4132
4201
|
}
|
|
4133
4202
|
function profilesDir() {
|
|
4134
|
-
return
|
|
4203
|
+
return join2(accountsHome(), "profiles");
|
|
4135
4204
|
}
|
|
4136
4205
|
var EMPTY_STORE = { version: 1, current: {}, applied: {}, profiles: [], tools: [] };
|
|
4137
4206
|
function loadStore() {
|
|
@@ -4174,7 +4243,7 @@ function loadStore() {
|
|
|
4174
4243
|
function saveStore(store) {
|
|
4175
4244
|
const path = storePath();
|
|
4176
4245
|
assertSafeWritePath(path, { mustStayUnder: accountsHome() });
|
|
4177
|
-
mkdirSync2(
|
|
4246
|
+
mkdirSync2(join2(path, ".."), { recursive: true });
|
|
4178
4247
|
if (existsSync2(path))
|
|
4179
4248
|
chmodSync(path, 384);
|
|
4180
4249
|
writeFileSync(path, JSON.stringify(store, null, 2) + `
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/accounts",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
4
4
|
"description": "Manage and switch between multiple Claude Code (and other AI coding tool) profiles/accounts locally — isolated config dirs, per-account email, one-command switching.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|