@hasna/accounts 0.1.18 → 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";
16669
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "node:fs";
16668
+ import { join as join2 } from "node:path";
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,14 +16862,17 @@ 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 });
16866
+ if (existsSync2(path))
16867
+ chmodSync(path, 384);
16797
16868
  writeFileSync(path, JSON.stringify(store, null, 2) + `
16798
16869
  `, { mode: 384 });
16870
+ chmodSync(path, 384);
16799
16871
  }
16800
16872
 
16801
16873
  // src/lib/tools.ts
16802
16874
  import { homedir as homedir2 } from "node:os";
16803
- import { join as join2 } from "node:path";
16875
+ import { join as join3 } from "node:path";
16804
16876
  var BUILTIN_TOOLS = [
16805
16877
  {
16806
16878
  id: "claude",
@@ -16809,7 +16881,7 @@ var BUILTIN_TOOLS = [
16809
16881
  extraEnv: {
16810
16882
  TELEGRAM_STATE_DIR: "{profileDir}/channels/telegram"
16811
16883
  },
16812
- defaultDir: join2(homedir2(), ".claude"),
16884
+ defaultDir: join3(homedir2(), ".claude"),
16813
16885
  bin: "claude",
16814
16886
  loginHint: "run /login inside Claude, then /exit when done",
16815
16887
  resumeArgs: ["--continue"],
@@ -16829,7 +16901,7 @@ var BUILTIN_TOOLS = [
16829
16901
  id: "codex-app",
16830
16902
  label: "Codex App",
16831
16903
  envVar: "CODEX_HOME",
16832
- defaultDir: join2(homedir2(), ".codex"),
16904
+ defaultDir: join3(homedir2(), ".codex"),
16833
16905
  bin: "/Applications/Codex.app/Contents/MacOS/Codex",
16834
16906
  loginHint: "sign in inside Codex.app, then quit the app when the profile is ready",
16835
16907
  launchArgs: ["--user-data-dir={profileDir}/electron-user-data"],
@@ -16839,7 +16911,7 @@ var BUILTIN_TOOLS = [
16839
16911
  id: "codex",
16840
16912
  label: "Codex CLI",
16841
16913
  envVar: "CODEX_HOME",
16842
- defaultDir: join2(homedir2(), ".codex"),
16914
+ defaultDir: join3(homedir2(), ".codex"),
16843
16915
  bin: "codex",
16844
16916
  loginArgs: ["login"],
16845
16917
  loginHint: "complete the Codex login flow for this CODEX_HOME",
@@ -16852,7 +16924,7 @@ var BUILTIN_TOOLS = [
16852
16924
  id: "takumi",
16853
16925
  label: "Takumi",
16854
16926
  envVar: "TAKUMI_CONFIG_DIR",
16855
- defaultDir: join2(homedir2(), ".takumi"),
16927
+ defaultDir: join3(homedir2(), ".takumi"),
16856
16928
  bin: "takumi",
16857
16929
  loginHint: "complete Takumi auth in this TAKUMI_CONFIG_DIR",
16858
16930
  resumeArgs: ["--continue"],
@@ -16872,7 +16944,7 @@ var BUILTIN_TOOLS = [
16872
16944
  id: "gemini",
16873
16945
  label: "Gemini CLI",
16874
16946
  envVar: "GEMINI_CONFIG_DIR",
16875
- defaultDir: join2(homedir2(), ".gemini"),
16947
+ defaultDir: join3(homedir2(), ".gemini"),
16876
16948
  bin: "gemini",
16877
16949
  loginHint: "complete Gemini auth in this GEMINI_CONFIG_DIR",
16878
16950
  permissionArgs: {
@@ -16890,7 +16962,7 @@ var BUILTIN_TOOLS = [
16890
16962
  XDG_CONFIG_HOME: "{profileDir}/xdg-config",
16891
16963
  XDG_DATA_HOME: "{profileDir}/xdg-data"
16892
16964
  },
16893
- defaultDir: join2(homedir2(), ".config", "opencode"),
16965
+ defaultDir: join3(homedir2(), ".config", "opencode"),
16894
16966
  bin: "opencode",
16895
16967
  loginArgs: ["auth", "login"],
16896
16968
  loginHint: "complete opencode auth login for this isolated config/data root",
@@ -16900,7 +16972,7 @@ var BUILTIN_TOOLS = [
16900
16972
  id: "cursor",
16901
16973
  label: "Cursor Agent",
16902
16974
  envVar: "CURSOR_CONFIG_DIR",
16903
- defaultDir: join2(homedir2(), ".cursor"),
16975
+ defaultDir: join3(homedir2(), ".cursor"),
16904
16976
  bin: "cursor-agent",
16905
16977
  loginArgs: ["login"],
16906
16978
  loginHint: "complete cursor-agent login for this CURSOR_CONFIG_DIR"
@@ -16909,7 +16981,7 @@ var BUILTIN_TOOLS = [
16909
16981
  id: "pi",
16910
16982
  label: "Pi Coding Agent",
16911
16983
  envVar: "PI_CODING_AGENT_HOME",
16912
- defaultDir: join2(homedir2(), ".pi"),
16984
+ defaultDir: join3(homedir2(), ".pi"),
16913
16985
  bin: "pi",
16914
16986
  loginHint: "complete Pi coding agent auth in this PI_CODING_AGENT_HOME"
16915
16987
  },
@@ -16917,7 +16989,7 @@ var BUILTIN_TOOLS = [
16917
16989
  id: "hermes",
16918
16990
  label: "Hermes",
16919
16991
  envVar: "HERMES_HOME",
16920
- defaultDir: join2(homedir2(), ".hermes"),
16992
+ defaultDir: join3(homedir2(), ".hermes"),
16921
16993
  bin: "hermes",
16922
16994
  loginHint: "complete Hermes auth in this HERMES_HOME",
16923
16995
  permissionArgs: {
@@ -16929,7 +17001,7 @@ var BUILTIN_TOOLS = [
16929
17001
  id: "kimi",
16930
17002
  label: "Kimi Code",
16931
17003
  envVar: "KIMI_CODE_HOME",
16932
- defaultDir: join2(homedir2(), ".kimi-code"),
17004
+ defaultDir: join3(homedir2(), ".kimi-code"),
16933
17005
  bin: "kimi",
16934
17006
  loginArgs: ["login"],
16935
17007
  loginHint: "complete kimi login for this KIMI_CODE_HOME",
@@ -16944,7 +17016,7 @@ var BUILTIN_TOOLS = [
16944
17016
  id: "grok",
16945
17017
  label: "Grok Build",
16946
17018
  envVar: "HOME",
16947
- defaultDir: join2(homedir2(), ".grok"),
17019
+ defaultDir: join3(homedir2(), ".grok"),
16948
17020
  bin: "grok",
16949
17021
  loginArgs: ["login"],
16950
17022
  loginHint: "complete grok login in this process-scoped HOME; prefer launch/shell over exporting HOME globally"
@@ -17064,11 +17136,11 @@ function currentProfile(toolId) {
17064
17136
 
17065
17137
  // src/lib/claude-auth.ts
17066
17138
  import { copyFileSync, existsSync as existsSync3, lstatSync as lstatSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync2, statSync, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
17067
- import { dirname as dirname3, join as join4 } from "node:path";
17139
+ import { dirname as dirname3, join as join5 } from "node:path";
17068
17140
 
17069
17141
  // src/lib/claude-layout.ts
17070
17142
  import { homedir as homedir3 } from "node:os";
17071
- import { dirname as dirname2, join as join3 } from "node:path";
17143
+ import { dirname as dirname2, join as join4 } from "node:path";
17072
17144
  var CLAUDE_KEYCHAIN_SERVICE = "Claude Code-credentials";
17073
17145
  var ACCOUNTS_AUTH_DIR = ".accounts-auth";
17074
17146
  var OAUTH_SNAPSHOT = "oauth-account.json";
@@ -17080,32 +17152,32 @@ function liveClaudeBase() {
17080
17152
  }
17081
17153
  function liveClaudePaths() {
17082
17154
  const base = liveClaudeBase();
17083
- const configDir = join3(base, ".claude");
17155
+ const configDir = join4(base, ".claude");
17084
17156
  return {
17085
17157
  configDir,
17086
- homeJson: join3(base, ".claude.json"),
17087
- credentialsFile: join3(configDir, ".credentials.json")
17158
+ homeJson: join4(base, ".claude.json"),
17159
+ credentialsFile: join4(configDir, ".credentials.json")
17088
17160
  };
17089
17161
  }
17090
17162
  function profileAccountJsonPaths(profileDir, tool) {
17091
17163
  if (!tool.accountFile)
17092
17164
  return [];
17093
- const paths = [join3(profileDir, tool.accountFile)];
17165
+ const paths = [join4(profileDir, tool.accountFile)];
17094
17166
  if (profileDir === tool.defaultDir)
17095
- paths.push(join3(dirname2(profileDir), tool.accountFile));
17167
+ paths.push(join4(dirname2(profileDir), tool.accountFile));
17096
17168
  return paths;
17097
17169
  }
17098
17170
  function profileAuthDir(profileDir) {
17099
- return join3(profileDir, ACCOUNTS_AUTH_DIR);
17171
+ return join4(profileDir, ACCOUNTS_AUTH_DIR);
17100
17172
  }
17101
17173
  function profileOAuthSnapshot(profileDir) {
17102
- return join3(profileAuthDir(profileDir), OAUTH_SNAPSHOT);
17174
+ return join4(profileAuthDir(profileDir), OAUTH_SNAPSHOT);
17103
17175
  }
17104
17176
  function profileCredentialsSnapshot(profileDir) {
17105
- return join3(profileAuthDir(profileDir), CREDENTIALS_SNAPSHOT);
17177
+ return join4(profileAuthDir(profileDir), CREDENTIALS_SNAPSHOT);
17106
17178
  }
17107
17179
  function profileKeychainSnapshot(profileDir) {
17108
- return join3(profileAuthDir(profileDir), KEYCHAIN_SNAPSHOT);
17180
+ return join4(profileAuthDir(profileDir), KEYCHAIN_SNAPSHOT);
17109
17181
  }
17110
17182
 
17111
17183
  // src/lib/keychain.ts
@@ -17259,7 +17331,7 @@ function mergeOAuthInto(paths, oauth, allowDelete, stayUnder) {
17259
17331
  }
17260
17332
  }
17261
17333
  function sanitizeSettingsFile(configDir, stayUnder) {
17262
- const settingsPath = join4(configDir, "settings.json");
17334
+ const settingsPath = join5(configDir, "settings.json");
17263
17335
  const settings = readJsonFile(settingsPath);
17264
17336
  if (!settings)
17265
17337
  return false;
@@ -17306,7 +17378,7 @@ function liveOAuthEmail() {
17306
17378
  }
17307
17379
  function snapshotLiveAuthToProfile(profileDir, _tool) {
17308
17380
  const authDir = profileAuthDir(profileDir);
17309
- assertSafeWritePath(join4(authDir, OAUTH_SNAPSHOT), { mustStayUnder: profileDir });
17381
+ assertSafeWritePath(join5(authDir, OAUTH_SNAPSHOT), { mustStayUnder: profileDir });
17310
17382
  mkdirSync3(authDir, { recursive: true });
17311
17383
  const live = liveClaudePaths();
17312
17384
  const oauth = readOAuthFromPaths([live.homeJson]);
@@ -17325,14 +17397,14 @@ function snapshotLiveAuthToProfile(profileDir, _tool) {
17325
17397
  }
17326
17398
  function ensureProfileAuthSnapshot(profileDir, tool, opts = {}) {
17327
17399
  const authDir = profileAuthDir(profileDir);
17328
- assertSafeWritePath(join4(authDir, OAUTH_SNAPSHOT), { mustStayUnder: profileDir });
17400
+ assertSafeWritePath(join5(authDir, OAUTH_SNAPSHOT), { mustStayUnder: profileDir });
17329
17401
  mkdirSync3(authDir, { recursive: true });
17330
17402
  const oauthSource = findOAuthSource(profileAccountJsonPaths(profileDir, tool));
17331
17403
  const oauthSnap = profileOAuthSnapshot(profileDir);
17332
17404
  if (oauthSource && (opts.overwrite || snapshotIsStale(oauthSource.path, oauthSnap))) {
17333
17405
  writeJsonFile(oauthSnap, { oauthAccount: oauthSource.oauth }, profileDir);
17334
17406
  }
17335
- const credFile = join4(profileDir, ".credentials.json");
17407
+ const credFile = join5(profileDir, ".credentials.json");
17336
17408
  const credSnap = profileCredentialsSnapshot(profileDir);
17337
17409
  if (existsSync3(credFile) && (opts.overwrite || snapshotIsStale(credFile, credSnap))) {
17338
17410
  assertSafeWritePath(credSnap, { mustStayUnder: profileDir });
@@ -17394,9 +17466,9 @@ function hasAuthSnapshot(profileDir) {
17394
17466
 
17395
17467
  // src/lib/apply-lock.ts
17396
17468
  import { closeSync, existsSync as existsSync4, mkdirSync as mkdirSync4, openSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "node:fs";
17397
- import { join as join5 } from "node:path";
17469
+ import { join as join6 } from "node:path";
17398
17470
  function lockPath() {
17399
- return join5(accountsHome(), ".apply.lock");
17471
+ return join6(accountsHome(), ".apply.lock");
17400
17472
  }
17401
17473
  function withApplyLock(fn) {
17402
17474
  const home = accountsHome();
@@ -17467,7 +17539,7 @@ function applyProfileUnlocked(name, toolId) {
17467
17539
 
17468
17540
  // src/lib/codex-app.ts
17469
17541
  import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
17470
- import { join as join6 } from "node:path";
17542
+ import { join as join7 } from "node:path";
17471
17543
  var FILE_CREDENTIALS_LINE = 'cli_auth_credentials_store = "file"';
17472
17544
  function insertRootConfigLine(config2, line) {
17473
17545
  if (config2.trim() === "")
@@ -17494,7 +17566,7 @@ ${after}${after.endsWith(`
17494
17566
  }
17495
17567
  function ensureCodexAppProfileConfig(profileDir) {
17496
17568
  mkdirSync5(profileDir, { recursive: true });
17497
- const configPath = join6(profileDir, "config.toml");
17569
+ const configPath = join7(profileDir, "config.toml");
17498
17570
  const current = existsSync5(configPath) ? readFileSync3(configPath, "utf8") : "";
17499
17571
  if (/^\s*cli_auth_credentials_store\s*=/.test(current))
17500
17572
  return;
@@ -17579,20 +17651,20 @@ function switchProfile(name, opts = {}) {
17579
17651
  import { createHash } from "node:crypto";
17580
17652
  import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync4, readdirSync, rmSync, writeFileSync as writeFileSync5 } from "node:fs";
17581
17653
  import { createConnection, createServer } from "node:net";
17582
- import { basename, join as join7 } from "node:path";
17654
+ import { basename, join as join8 } from "node:path";
17583
17655
  var STATE_SUFFIX = ".json";
17584
17656
  function supervisorDir() {
17585
- return join7(accountsHome(), "supervisors");
17657
+ return join8(accountsHome(), "supervisors");
17586
17658
  }
17587
17659
  function supervisorStatePath(toolId) {
17588
- return join7(supervisorDir(), `${toolId}${STATE_SUFFIX}`);
17660
+ return join8(supervisorDir(), `${toolId}${STATE_SUFFIX}`);
17589
17661
  }
17590
17662
  function supervisorSocketPath(toolId) {
17591
17663
  if (process.platform === "win32") {
17592
17664
  const hash = createHash("sha1").update(accountsHome()).digest("hex").slice(0, 12);
17593
17665
  return `\\\\.\\pipe\\hasna-accounts-${hash}-${toolId}`;
17594
17666
  }
17595
- return join7(supervisorDir(), `${toolId}.sock`);
17667
+ return join8(supervisorDir(), `${toolId}.sock`);
17596
17668
  }
17597
17669
  function parseState(raw) {
17598
17670
  const data = JSON.parse(raw);
@@ -1 +1 @@
1
- {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"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"}
package/dist/storage.js CHANGED
@@ -18,8 +18,8 @@ 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";
22
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "node:fs";
21
+ import { join as join2 } from "node:path";
22
+ import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "node:fs";
23
23
 
24
24
  // node_modules/zod/v3/external.js
25
25
  var exports_external = {};
@@ -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,9 +4243,12 @@ 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 });
4247
+ if (existsSync2(path))
4248
+ chmodSync(path, 384);
4178
4249
  writeFileSync(path, JSON.stringify(store, null, 2) + `
4179
4250
  `, { mode: 384 });
4251
+ chmodSync(path, 384);
4180
4252
  }
4181
4253
  function firstEnv(env, primary, fallback) {
4182
4254
  return env[primary] || env[fallback] || undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/accounts",
3
- "version": "0.1.18",
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",