@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/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 (existsSync(path) && lstatSync(path).isSymbolicLink()) {
16684
+ if (lstatIfExists(path)?.isSymbolicLink()) {
16676
16685
  throw new AccountsError(`${label}: ${path}`);
16677
16686
  }
16678
16687
  }
16679
- function assertDirChainSafe(targetFile, mustStayUnder) {
16680
- const absFile = resolve(targetFile);
16681
- const parent = dirname(absFile);
16682
- const base = mustStayUnder ? resolve(mustStayUnder) : undefined;
16683
- if (base) {
16684
- const rel = absFile.startsWith(base + "/") ? absFile.slice(base.length + 1) : "";
16685
- if (!rel && absFile !== base) {
16686
- throw new AccountsError(`refusing to write outside profile directory: ${targetFile}`);
16687
- }
16688
- const segments = rel.split("/").filter(Boolean);
16689
- if (segments.some((s) => s === "..")) {
16690
- throw new AccountsError(`refusing path traversal: ${targetFile}`);
16691
- }
16692
- let cursor = base;
16693
- for (let i = 0;i < segments.length - 1; i++) {
16694
- cursor = resolve(cursor, segments[i]);
16695
- throwIfSymlink(cursor, "refusing to write under symlink directory");
16696
- }
16697
- } else {
16698
- let cursor = parent;
16699
- for (;; ) {
16700
- throwIfSymlink(cursor, "refusing to write under symlink directory");
16701
- const next = dirname(cursor);
16702
- if (next === cursor)
16703
- break;
16704
- cursor = next;
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
- if (!existsSync(parent))
16712
- mkdirSync(parent, { recursive: true });
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 (opts?.mustStayUnder) {
16717
- const base = realpathSync(resolve(opts.mustStayUnder));
16718
- if (resolved !== base && !resolved.startsWith(base + "/")) {
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 join(homedir(), ".hasna", "accounts");
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 join(accountsHome(), "accounts.json");
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(join(path, ".."), { recursive: true });
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 join2 } from "node:path";
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: join2(homedir2(), ".claude"),
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: join2(homedir2(), ".codex"),
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: join2(homedir2(), ".codex"),
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: join2(homedir2(), ".takumi"),
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: join2(homedir2(), ".gemini"),
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: join2(homedir2(), ".config", "opencode"),
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: join2(homedir2(), ".cursor"),
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: join2(homedir2(), ".pi"),
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: join2(homedir2(), ".hermes"),
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: join2(homedir2(), ".kimi-code"),
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: join2(homedir2(), ".grok"),
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 join4 } from "node:path";
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 join3 } from "node:path";
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 = join3(base, ".claude");
17155
+ const configDir = join4(base, ".claude");
17087
17156
  return {
17088
17157
  configDir,
17089
- homeJson: join3(base, ".claude.json"),
17090
- credentialsFile: join3(configDir, ".credentials.json")
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 = [join3(profileDir, tool.accountFile)];
17165
+ const paths = [join4(profileDir, tool.accountFile)];
17097
17166
  if (profileDir === tool.defaultDir)
17098
- paths.push(join3(dirname2(profileDir), tool.accountFile));
17167
+ paths.push(join4(dirname2(profileDir), tool.accountFile));
17099
17168
  return paths;
17100
17169
  }
17101
17170
  function profileAuthDir(profileDir) {
17102
- return join3(profileDir, ACCOUNTS_AUTH_DIR);
17171
+ return join4(profileDir, ACCOUNTS_AUTH_DIR);
17103
17172
  }
17104
17173
  function profileOAuthSnapshot(profileDir) {
17105
- return join3(profileAuthDir(profileDir), OAUTH_SNAPSHOT);
17174
+ return join4(profileAuthDir(profileDir), OAUTH_SNAPSHOT);
17106
17175
  }
17107
17176
  function profileCredentialsSnapshot(profileDir) {
17108
- return join3(profileAuthDir(profileDir), CREDENTIALS_SNAPSHOT);
17177
+ return join4(profileAuthDir(profileDir), CREDENTIALS_SNAPSHOT);
17109
17178
  }
17110
17179
  function profileKeychainSnapshot(profileDir) {
17111
- return join3(profileAuthDir(profileDir), KEYCHAIN_SNAPSHOT);
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 = join4(configDir, "settings.json");
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(join4(authDir, OAUTH_SNAPSHOT), { mustStayUnder: profileDir });
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(join4(authDir, OAUTH_SNAPSHOT), { mustStayUnder: profileDir });
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 = join4(profileDir, ".credentials.json");
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 join5 } from "node:path";
17469
+ import { join as join6 } from "node:path";
17401
17470
  function lockPath() {
17402
- return join5(accountsHome(), ".apply.lock");
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 join6 } from "node:path";
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 = join6(profileDir, "config.toml");
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 join7 } from "node:path";
17654
+ import { basename, join as join8 } from "node:path";
17586
17655
  var STATE_SUFFIX = ".json";
17587
17656
  function supervisorDir() {
17588
- return join7(accountsHome(), "supervisors");
17657
+ return join8(accountsHome(), "supervisors");
17589
17658
  }
17590
17659
  function supervisorStatePath(toolId) {
17591
- return join7(supervisorDir(), `${toolId}${STATE_SUFFIX}`);
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 join7(supervisorDir(), `${toolId}.sock`);
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 (existsSync(path) && lstatSync(path).isSymbolicLink()) {
4052
+ if (lstatIfExists(path)?.isSymbolicLink()) {
4044
4053
  throw new AccountsError(`${label}: ${path}`);
4045
4054
  }
4046
4055
  }
4047
- function assertDirChainSafe(targetFile, mustStayUnder) {
4048
- const absFile = resolve(targetFile);
4049
- const parent = dirname(absFile);
4050
- const base = mustStayUnder ? resolve(mustStayUnder) : undefined;
4051
- if (base) {
4052
- const rel = absFile.startsWith(base + "/") ? absFile.slice(base.length + 1) : "";
4053
- if (!rel && absFile !== base) {
4054
- throw new AccountsError(`refusing to write outside profile directory: ${targetFile}`);
4055
- }
4056
- const segments = rel.split("/").filter(Boolean);
4057
- if (segments.some((s) => s === "..")) {
4058
- throw new AccountsError(`refusing path traversal: ${targetFile}`);
4059
- }
4060
- let cursor = base;
4061
- for (let i = 0;i < segments.length - 1; i++) {
4062
- cursor = resolve(cursor, segments[i]);
4063
- throwIfSymlink(cursor, "refusing to write under symlink directory");
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
- } else {
4066
- let cursor = parent;
4067
- for (;; ) {
4068
- throwIfSymlink(cursor, "refusing to write under symlink directory");
4069
- const next = dirname(cursor);
4070
- if (next === cursor)
4071
- break;
4072
- cursor = next;
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
- if (!existsSync(parent))
4080
- mkdirSync(parent, { recursive: true });
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 (opts?.mustStayUnder) {
4085
- const base = realpathSync(resolve(opts.mustStayUnder));
4086
- if (resolved !== base && !resolved.startsWith(base + "/")) {
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 join(homedir(), ".hasna", "accounts");
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 join(accountsHome(), "accounts.json");
4200
+ return join2(accountsHome(), "accounts.json");
4132
4201
  }
4133
4202
  function profilesDir() {
4134
- return join(accountsHome(), "profiles");
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(join(path, ".."), { recursive: true });
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.19",
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",