windows-development-sandbox 1.17.0 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (319) hide show
  1. package/README.md +92 -92
  2. package/config.example.json +97 -4
  3. package/dist/app/features/accounts/accounts.def.d.ts +116 -2
  4. package/dist/app/features/accounts/accounts.def.js +80 -12
  5. package/dist/app/features/accounts/accounts.run.d.ts +29 -0
  6. package/dist/app/features/accounts/accounts.run.js +36 -6
  7. package/dist/app/features/accounts/index.d.ts +2 -0
  8. package/dist/app/features/accounts/index.js +4 -1
  9. package/dist/app/features/accounts/services.def.d.ts +111 -0
  10. package/dist/app/features/accounts/services.def.js +187 -0
  11. package/dist/app/features/accounts/services.run.d.ts +65 -0
  12. package/dist/app/features/accounts/services.run.js +212 -0
  13. package/dist/app/features/defs.d.ts +112 -1
  14. package/dist/app/features/defs.js +5 -0
  15. package/dist/app/features/settings/categories.def.js +21 -4
  16. package/dist/app/registry.js +5 -1
  17. package/dist/bin/agent-sandbox.js +2 -2
  18. package/dist/bin/{chunk-QLGGAG3D.js → chunk-BKF43GJ3.js} +51 -16
  19. package/dist/bin/{chunk-REE3EMHD.js → chunk-YD3A67RS.js} +3812 -1702
  20. package/dist/bin/{chunk-XYUVIVJT.js → chunk-Z5D32FN3.js} +1 -1
  21. package/dist/bin/claude-host.js +2 -2
  22. package/dist/bin/claude-sbx.js +2 -2
  23. package/dist/bin/codex-sbx.js +2 -2
  24. package/dist/bin/gemini-sbx.js +2 -2
  25. package/dist/bin/sandbox.js +2 -2
  26. package/dist/bin/sbx-hide.js +2 -2
  27. package/dist/bin/sbx-show.js +2 -2
  28. package/dist/cli/commands/launch/launch.js +30 -1
  29. package/dist/cli/manifest.d.ts +93 -65
  30. package/dist/cli/manifest.js +23 -5
  31. package/dist/cli/menu/pages/accounts-page.js +31 -16
  32. package/dist/cli/services/accounts-service.d.ts +14 -4
  33. package/dist/cli/services/accounts-service.js +19 -6
  34. package/dist/cli/services/data-collect.js +26 -1
  35. package/dist/core/config/defaults.js +47 -4
  36. package/dist/core/detect/toolchains.d.ts +15 -1
  37. package/dist/core/detect/toolchains.js +45 -12
  38. package/dist/core/local-config/overrides.js +6 -0
  39. package/dist/core/schema/accounts.d.ts +27 -0
  40. package/dist/core/schema/accounts.js +32 -0
  41. package/dist/core/schema/config.d.ts +239 -0
  42. package/dist/core/schema/config.js +6 -0
  43. package/dist/core/schema/pc-installs.d.ts +7 -0
  44. package/dist/core/schema/pc-installs.js +17 -0
  45. package/dist/core/schema/toolchains.d.ts +61 -0
  46. package/dist/core/schema/toolchains.js +106 -0
  47. package/dist/core/types.d.ts +18 -0
  48. package/dist/guest/accounts/adapter.d.ts +25 -0
  49. package/dist/guest/accounts/adapter.js +21 -0
  50. package/dist/guest/accounts/adapters.d.ts +5 -0
  51. package/dist/guest/accounts/adapters.js +18 -0
  52. package/dist/guest/accounts/apply.d.ts +41 -0
  53. package/dist/guest/accounts/apply.js +47 -0
  54. package/dist/guest/accounts/kinds/appstore.d.ts +2 -0
  55. package/dist/guest/accounts/kinds/appstore.js +7 -0
  56. package/dist/guest/accounts/kinds/cfx.d.ts +2 -0
  57. package/dist/guest/accounts/kinds/cfx.js +7 -0
  58. package/dist/guest/accounts/kinds/epic.d.ts +2 -0
  59. package/dist/guest/accounts/kinds/epic.js +26 -0
  60. package/dist/guest/accounts/kinds/expo.d.ts +2 -0
  61. package/dist/guest/accounts/kinds/expo.js +7 -0
  62. package/dist/guest/accounts/kinds/keystore.d.ts +2 -0
  63. package/dist/guest/accounts/kinds/keystore.js +7 -0
  64. package/dist/guest/accounts/kinds/play.d.ts +2 -0
  65. package/dist/guest/accounts/kinds/play.js +7 -0
  66. package/dist/guest/accounts/kinds/steam.d.ts +4 -0
  67. package/dist/guest/accounts/kinds/steam.js +17 -0
  68. package/dist/guest/accounts/kinds/unity.d.ts +2 -0
  69. package/dist/guest/accounts/kinds/unity.js +7 -0
  70. package/dist/guest/accounts/watch.d.ts +28 -0
  71. package/dist/guest/accounts/watch.js +78 -0
  72. package/dist/guest/agents/agent-files.d.ts +6 -0
  73. package/dist/guest/agents/agent-files.js +14 -0
  74. package/dist/guest/agents/local-state.d.ts +2 -0
  75. package/dist/guest/agents/local-state.js +13 -1
  76. package/dist/guest/apps/resolvers-gamedev.d.ts +2 -0
  77. package/dist/guest/apps/resolvers-gamedev.js +1 -0
  78. package/dist/guest/apps/resolvers-mobile.d.ts +2 -0
  79. package/dist/guest/apps/resolvers-mobile.js +1 -0
  80. package/dist/guest/apps/resolvers.js +4 -0
  81. package/dist/guest/boot/boot.d.ts +5 -0
  82. package/dist/guest/boot/boot.js +36 -1
  83. package/dist/guest/daemon/accounts.d.ts +3 -0
  84. package/dist/guest/daemon/accounts.js +25 -0
  85. package/dist/guest/daemon/daemon.js +2 -0
  86. package/dist/guest/daemon/idle-shutdown.js +3 -1
  87. package/dist/guest/daemon/sessions.js +2 -0
  88. package/dist/guest/devtools/android.d.ts +2 -0
  89. package/dist/guest/devtools/android.js +7 -0
  90. package/dist/guest/devtools/devtool.d.ts +9 -0
  91. package/dist/guest/devtools/devtool.js +5 -0
  92. package/dist/guest/devtools/fivem.d.ts +2 -0
  93. package/dist/guest/devtools/fivem.js +7 -0
  94. package/dist/guest/devtools/registry.d.ts +6 -0
  95. package/dist/guest/devtools/registry.js +12 -0
  96. package/dist/guest/devtools/steam.d.ts +2 -0
  97. package/dist/guest/devtools/steam.js +228 -0
  98. package/dist/guest/devtools/unity.d.ts +2 -0
  99. package/dist/guest/devtools/unity.js +7 -0
  100. package/dist/guest/entries/daemon.js +2 -0
  101. package/dist/guest/entries/tool.d.ts +1 -0
  102. package/dist/guest/entries/tool.js +17 -0
  103. package/dist/guest/toolchains/engines/godot.d.ts +4 -0
  104. package/dist/guest/toolchains/engines/godot.js +4 -0
  105. package/dist/guest/toolchains/engines/unity-release.d.ts +15 -0
  106. package/dist/guest/toolchains/engines/unity-release.js +6 -0
  107. package/dist/guest/toolchains/engines/unity.d.ts +4 -0
  108. package/dist/guest/toolchains/engines/unity.js +4 -0
  109. package/dist/guest/toolchains/engines/unreal-engines.d.ts +4 -0
  110. package/dist/guest/toolchains/engines/unreal-engines.js +4 -0
  111. package/dist/guest/toolchains/fivem/artifacts.d.ts +6 -0
  112. package/dist/guest/toolchains/fivem/artifacts.js +7 -0
  113. package/dist/guest/toolchains/fivem/fivem.d.ts +4 -0
  114. package/dist/guest/toolchains/fivem/fivem.js +4 -0
  115. package/dist/guest/toolchains/installers.d.ts +12 -0
  116. package/dist/guest/toolchains/installers.js +39 -0
  117. package/dist/guest/toolchains/languages/java.js +1 -1
  118. package/dist/guest/toolchains/languages/kotlin.d.ts +3 -0
  119. package/dist/guest/toolchains/languages/kotlin.js +6 -0
  120. package/dist/guest/toolchains/mobile/android-repo.d.ts +11 -0
  121. package/dist/guest/toolchains/mobile/android-repo.js +10 -0
  122. package/dist/guest/toolchains/mobile/android.d.ts +17 -0
  123. package/dist/guest/toolchains/mobile/android.js +16 -0
  124. package/dist/guest/toolchains/mobile/dart.d.ts +4 -0
  125. package/dist/guest/toolchains/mobile/dart.js +4 -0
  126. package/dist/guest/toolchains/mobile/flutter.d.ts +4 -0
  127. package/dist/guest/toolchains/mobile/flutter.js +4 -0
  128. package/dist/guest/toolchains/mobile/react-native.d.ts +4 -0
  129. package/dist/guest/toolchains/mobile/react-native.js +4 -0
  130. package/dist/guest/toolchains/native/unreal.d.ts +3 -7
  131. package/dist/guest/toolchains/native/unreal.js +16 -155
  132. package/dist/guest/toolchains/native/visual-studio.d.ts +15 -0
  133. package/dist/guest/toolchains/native/visual-studio.js +159 -0
  134. package/dist/guest/toolchains/native/vs-request.d.ts +19 -0
  135. package/dist/guest/toolchains/native/vs-request.js +46 -0
  136. package/dist/guest/toolchains/not-yet.d.ts +2 -0
  137. package/dist/guest/toolchains/not-yet.js +6 -0
  138. package/dist/guest/toolchains/persisted.d.ts +48 -0
  139. package/dist/guest/toolchains/persisted.js +159 -0
  140. package/dist/guest/toolchains/plan.js +80 -4
  141. package/dist/guest/toolchains/steam/apps.d.ts +27 -0
  142. package/dist/guest/toolchains/steam/apps.js +112 -0
  143. package/dist/guest/toolchains/steam/client.d.ts +5 -0
  144. package/dist/guest/toolchains/steam/client.js +67 -0
  145. package/dist/guest/toolchains/steam/commands.d.ts +58 -0
  146. package/dist/guest/toolchains/steam/commands.js +155 -0
  147. package/dist/guest/toolchains/steam/steam.d.ts +4 -0
  148. package/dist/guest/toolchains/steam/steam.js +61 -0
  149. package/dist/guest/toolchains/steam/steamcmd-install.d.ts +29 -0
  150. package/dist/guest/toolchains/steam/steamcmd-install.js +177 -0
  151. package/dist/guest/toolchains/steam/steamcmd.d.ts +6 -0
  152. package/dist/guest/toolchains/steam/steamcmd.js +23 -0
  153. package/dist/guest/toolchains/step.d.ts +10 -0
  154. package/dist/host/accounts/deliver.d.ts +26 -0
  155. package/dist/host/accounts/deliver.js +80 -0
  156. package/dist/host/accounts/handoff.d.ts +17 -0
  157. package/dist/host/accounts/handoff.js +110 -0
  158. package/dist/host/accounts/index.d.ts +6 -0
  159. package/dist/host/accounts/index.js +8 -0
  160. package/dist/host/accounts/kinds/appstore.d.ts +2 -0
  161. package/dist/host/accounts/kinds/appstore.js +10 -0
  162. package/dist/host/accounts/kinds/cfx.d.ts +2 -0
  163. package/dist/host/accounts/kinds/cfx.js +7 -0
  164. package/dist/host/accounts/kinds/epic.d.ts +2 -0
  165. package/dist/host/accounts/kinds/epic.js +15 -0
  166. package/dist/host/accounts/kinds/expo.d.ts +2 -0
  167. package/dist/host/accounts/kinds/expo.js +7 -0
  168. package/dist/host/accounts/kinds/keystore.d.ts +2 -0
  169. package/dist/host/accounts/kinds/keystore.js +7 -0
  170. package/dist/host/accounts/kinds/play.d.ts +2 -0
  171. package/dist/host/accounts/kinds/play.js +7 -0
  172. package/dist/host/accounts/kinds/steam.d.ts +6 -0
  173. package/dist/host/accounts/kinds/steam.js +31 -0
  174. package/dist/host/accounts/kinds/unity.d.ts +2 -0
  175. package/dist/host/accounts/kinds/unity.js +13 -0
  176. package/dist/host/accounts/leases.d.ts +40 -0
  177. package/dist/host/accounts/leases.js +62 -0
  178. package/dist/host/accounts/source.d.ts +21 -0
  179. package/dist/host/accounts/source.js +15 -0
  180. package/dist/host/accounts/sources.d.ts +5 -0
  181. package/dist/host/accounts/sources.js +18 -0
  182. package/dist/host/accounts/store.d.ts +23 -0
  183. package/dist/host/accounts/store.js +74 -0
  184. package/dist/host/audit/log.d.ts +1 -1
  185. package/dist/host/launch/generate.d.ts +2 -0
  186. package/dist/host/launch/generate.js +16 -1
  187. package/dist/host/launch/hints/android.d.ts +3 -0
  188. package/dist/host/launch/hints/android.js +3 -0
  189. package/dist/host/launch/hints/engines.d.ts +3 -0
  190. package/dist/host/launch/hints/engines.js +3 -0
  191. package/dist/host/launch/hints/fivem.d.ts +3 -0
  192. package/dist/host/launch/hints/fivem.js +3 -0
  193. package/dist/host/launch/hints/hint.d.ts +2 -0
  194. package/dist/host/launch/hints/hint.js +1 -0
  195. package/dist/host/launch/hints/index.d.ts +5 -0
  196. package/dist/host/launch/hints/index.js +29 -0
  197. package/dist/host/launch/hints/mobile.d.ts +3 -0
  198. package/dist/host/launch/hints/mobile.js +3 -0
  199. package/dist/host/launch/hints/steam.d.ts +7 -0
  200. package/dist/host/launch/hints/steam.js +59 -0
  201. package/dist/host/launch/index.d.ts +2 -2
  202. package/dist/host/launch/index.js +2 -2
  203. package/dist/host/launch/mappings.js +2 -0
  204. package/dist/host/launch/session-file.js +2 -0
  205. package/dist/host/launch/stage.js +10 -1
  206. package/dist/host/launch/toolchains.d.ts +7 -0
  207. package/dist/host/launch/toolchains.js +11 -0
  208. package/dist/host/pc-installs/discover.d.ts +19 -0
  209. package/dist/host/pc-installs/discover.js +59 -0
  210. package/dist/host/pc-installs/epic.d.ts +2 -0
  211. package/dist/host/pc-installs/epic.js +7 -0
  212. package/dist/host/pc-installs/index.d.ts +3 -0
  213. package/dist/host/pc-installs/index.js +2 -0
  214. package/dist/host/pc-installs/source.d.ts +9 -0
  215. package/dist/host/pc-installs/source.js +1 -0
  216. package/dist/host/pc-installs/steam.d.ts +16 -0
  217. package/dist/host/pc-installs/steam.js +143 -0
  218. package/dist/host/pc-installs/unity.d.ts +2 -0
  219. package/dist/host/pc-installs/unity.js +7 -0
  220. package/dist/host/pc-installs/unreal.d.ts +2 -0
  221. package/dist/host/pc-installs/unreal.js +7 -0
  222. package/dist/host/profile/accounts.d.ts +21 -6
  223. package/dist/host/profile/accounts.js +108 -28
  224. package/dist/host/profile/claude-login.d.ts +32 -2
  225. package/dist/host/profile/claude-login.js +100 -8
  226. package/dist/host/profile/index.d.ts +1 -1
  227. package/dist/host/profile/index.js +1 -1
  228. package/dist/host/secrets/launch-secrets.js +1 -0
  229. package/dist/runtime/agent.js +321 -9
  230. package/dist/runtime/daemon.js +1987 -595
  231. package/dist/runtime/db.js +334 -11
  232. package/dist/runtime/main.js +2530 -709
  233. package/dist/runtime/netproxy.js +321 -9
  234. package/dist/runtime/tool.js +28549 -0
  235. package/dist/runtime/view.js +321 -9
  236. package/dist/selftest/accounts.test.js +172 -1
  237. package/dist/selftest/agent-paths.test.js +13 -1
  238. package/dist/selftest/android.test.d.ts +2 -0
  239. package/dist/selftest/android.test.js +7 -0
  240. package/dist/selftest/app.test.js +1 -1
  241. package/dist/selftest/engines.test.d.ts +2 -0
  242. package/dist/selftest/engines.test.js +7 -0
  243. package/dist/selftest/fivem.test.d.ts +2 -0
  244. package/dist/selftest/fivem.test.js +7 -0
  245. package/dist/selftest/generate.test.js +1 -1
  246. package/dist/selftest/index.js +18 -0
  247. package/dist/selftest/menu-screens.js +1 -0
  248. package/dist/selftest/menu.test.js +2 -1
  249. package/dist/selftest/mobile.test.d.ts +2 -0
  250. package/dist/selftest/mobile.test.js +7 -0
  251. package/dist/selftest/pc-installs.test.d.ts +2 -0
  252. package/dist/selftest/pc-installs.test.js +111 -0
  253. package/dist/selftest/persisted-installs.test.d.ts +2 -0
  254. package/dist/selftest/persisted-installs.test.js +107 -0
  255. package/dist/selftest/projects.test.js +6 -4
  256. package/dist/selftest/service-accounts.test.d.ts +2 -0
  257. package/dist/selftest/service-accounts.test.js +320 -0
  258. package/dist/selftest/steam.test.d.ts +2 -0
  259. package/dist/selftest/steam.test.js +316 -0
  260. package/dist/selftest/storage-helper.test.js +31 -8
  261. package/dist/selftest/support/engine-golden.js +3 -0
  262. package/dist/selftest/support/fixtures.d.ts +87 -0
  263. package/dist/selftest/support/fixtures.js +4 -3
  264. package/dist/selftest/support/pc-memory.d.ts +2 -0
  265. package/dist/selftest/support/pc-memory.js +17 -0
  266. package/dist/selftest/support/web-preview.js +20 -1
  267. package/dist/selftest/unity.test.d.ts +2 -0
  268. package/dist/selftest/unity.test.js +7 -0
  269. package/dist/shared/accounts/fingerprint.d.ts +1 -0
  270. package/dist/shared/accounts/fingerprint.js +20 -0
  271. package/dist/shared/accounts/index.d.ts +5 -0
  272. package/dist/shared/accounts/index.js +7 -0
  273. package/dist/shared/accounts/kinds/epic.d.ts +10 -0
  274. package/dist/shared/accounts/kinds/epic.js +56 -0
  275. package/dist/shared/accounts/kinds/steam.d.ts +28 -0
  276. package/dist/shared/accounts/kinds/steam.js +99 -0
  277. package/dist/shared/accounts/kinds.d.ts +41 -0
  278. package/dist/shared/accounts/kinds.js +154 -0
  279. package/dist/shared/accounts/payloads.d.ts +151 -0
  280. package/dist/shared/accounts/payloads.js +128 -0
  281. package/dist/shared/apps/catalogue-gamedev.d.ts +2 -0
  282. package/dist/shared/apps/catalogue-gamedev.js +1 -0
  283. package/dist/shared/apps/catalogue-mobile.d.ts +2 -0
  284. package/dist/shared/apps/catalogue-mobile.js +1 -0
  285. package/dist/shared/apps/catalogue-modding.d.ts +2 -0
  286. package/dist/shared/apps/catalogue-modding.js +1 -0
  287. package/dist/shared/apps/catalogue.d.ts +1 -0
  288. package/dist/shared/apps/catalogue.js +6 -1
  289. package/dist/shared/channel/schema.d.ts +183 -0
  290. package/dist/shared/channel/secrets.d.ts +42 -0
  291. package/dist/shared/channel/secrets.js +3 -0
  292. package/dist/shared/download/downloader.d.ts +7 -0
  293. package/dist/shared/download/downloader.js +10 -3
  294. package/dist/shared/installs.d.ts +19 -0
  295. package/dist/shared/installs.js +13 -0
  296. package/dist/shared/io/bounded.d.ts +6 -0
  297. package/dist/shared/io/bounded.js +38 -0
  298. package/dist/shared/io/index.d.ts +2 -0
  299. package/dist/shared/io/index.js +2 -0
  300. package/dist/shared/io/text.d.ts +9 -0
  301. package/dist/shared/io/text.js +18 -0
  302. package/dist/shared/paths/guest-paths.js +1 -1
  303. package/dist/shared/paths/index.d.ts +1 -0
  304. package/dist/shared/paths/index.js +1 -0
  305. package/dist/shared/paths/pc-installs.d.ts +20 -0
  306. package/dist/shared/paths/pc-installs.js +19 -0
  307. package/dist/shared/vdf.d.ts +25 -0
  308. package/dist/shared/vdf.js +151 -0
  309. package/dist/sync/sync-cycle.js +9 -0
  310. package/dist/web/index.html +1 -1
  311. package/dist/web/{main-JTH3FT4O.js → main-CWIF4VQI.js} +63 -61
  312. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  313. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  314. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  315. package/package.json +1 -1
  316. package/sandbox/scripts/add-firewall-port.ps1 +13 -0
  317. package/sandbox/templates/fivem/README.txt +2 -0
  318. package/schemas/config.schema.json +485 -0
  319. package/schemas/sandbox-config.schema.json +416 -0
@@ -1,20 +1,23 @@
1
- // Several Claude logins for the sandbox. Each account is a saved copy of Claude's .credentials.json plus the
2
- // `oauthAccount` block of .claude.json, kept in host-only storage (<storage>\accounts, never mapped into a sandbox)
3
- // and encrypted with Windows DPAPI (host/secrets): only the name, e-mail and date are readable in the file. Accounts
1
+ // Several Claude logins for the sandbox. Each account is a saved copy of Claude's .credentials.json (a subscription) or
2
+ // an Anthropic API key (one a Console login made, or one you gave), plus the `oauthAccount` block of .claude.json, kept
3
+ // in host-only storage (<storage>\accounts, never mapped into a sandbox) and encrypted with Windows DPAPI
4
+ // (host/secrets): only the name, e-mail, date, kind and a key's last 4 characters are readable in the file. Accounts
4
5
  // saved before that were plain; they are encrypted the first time they are read.
5
- // Switching writes them into the sandbox's persistent Claude state; when the sandbox is running the agent is then
6
- // restarted in place with --continue so the conversation carries on.
6
+ // Switching writes them into the sandbox's persistent Claude state, where Claude itself keeps that kind of login
7
+ // (claude-login.ts); when the sandbox is running the agent is then restarted in place with --continue so the
8
+ // conversation carries on.
7
9
  import fs from 'node:fs';
8
10
  import path from 'node:path';
9
11
  import { z } from 'zod';
10
12
  import { isSafeFileName } from '../../shared/paths/index.js';
11
13
  import { JsonFile } from '../../shared/io/index.js';
12
14
  import { storageOf } from '../storage/index.js';
13
- import { errMsg, fail, fileExists } from '../../core/util/index.js';
15
+ import { errMsg, fail, fileExists, isLink } from '../../core/util/index.js';
14
16
  import { SecretStore } from '../secrets/index.js';
15
17
  import { PluginShare } from './plugin-share.js';
16
- import { clearAccountCaches, emailOf, MAX_STATE_BYTES, readLogin, sandboxClaudeDir, readUntrusted, sandboxLogin, writeSandboxJson, } from './claude-login.js';
18
+ import { apiKeyProblem, clearAccountCaches, dropSubscription, emailOf, keyState, LOGIN_KINDS, maskApiKey, MAX_STATE_BYTES, readLogin, sandboxClaudeDir, readUntrusted, sandboxLogin, subscriptionState, writeSandboxJson, } from './claude-login.js';
17
19
  const NAME = /^[A-Za-z0-9_-]{1,40}$/;
20
+ /** A plain file from before accounts were encrypted (always a subscription). */
18
21
  const AccountSchema = z.object({
19
22
  name: z.string().regex(NAME),
20
23
  savedAt: z.string().max(64),
@@ -26,15 +29,27 @@ const IndexSchema = z.object({
26
29
  default: z.string().regex(NAME).optional(),
27
30
  active: z.record(z.string().max(200), z.string().regex(NAME)).optional(),
28
31
  });
29
- /** On disk: the readable details and the sealed login ({ credentials, oauthAccount }). */
32
+ /** On disk: the readable details and the sealed login ({ credentials, oauthAccount, apiKey }). */
30
33
  const SealedSchema = z.object({
31
34
  name: z.string().regex(NAME),
32
35
  savedAt: z.string().max(64),
33
36
  email: z.string().max(200).nullable(),
37
+ /** Missing in accounts saved before keys could be: a subscription. */
38
+ kind: z.enum(LOGIN_KINDS).optional(),
39
+ keyHint: z
40
+ .string()
41
+ .regex(/^sk-ant-\.\.\.[A-Za-z0-9_-]{4}$/)
42
+ .optional(),
34
43
  sealed: z.string().max(200000),
35
44
  });
36
45
  const StoredSchema = z.union([SealedSchema, AccountSchema]);
37
46
  const isSealed = (s) => 'sealed' in s;
47
+ /** What the sealed part holds. */
48
+ const LoginSchema = z.object({
49
+ credentials: z.unknown(),
50
+ oauthAccount: z.unknown(),
51
+ apiKey: z.string().max(512).optional(),
52
+ });
38
53
  export function accountsDir(storageRoot) {
39
54
  return path.join(storageRoot, 'accounts');
40
55
  }
@@ -95,18 +110,40 @@ export class AccountStore {
95
110
  return new JsonFile(StoredSchema, path.join(accountsDir(this.storageRoot), name + '.json'));
96
111
  }
97
112
  seal(account) {
98
- const { name, savedAt, email, credentials, oauthAccount } = account;
99
- return { name, savedAt, email, sealed: this.secrets.sealJson({ credentials, oauthAccount }) };
113
+ const { name, savedAt, email, kind, keyHint, credentials, oauthAccount, apiKey } = account;
114
+ return {
115
+ name,
116
+ savedAt,
117
+ email,
118
+ kind,
119
+ ...(keyHint ? { keyHint } : {}),
120
+ sealed: this.secrets.sealJson({ credentials, oauthAccount, ...(apiKey ? { apiKey } : {}) }),
121
+ };
100
122
  }
101
123
  store(name, login) {
102
- const account = { name, savedAt: new Date().toISOString(), email: emailOf(login.oauthAccount), ...login };
124
+ const account = {
125
+ name,
126
+ savedAt: new Date().toISOString(),
127
+ email: emailOf(login.oauthAccount),
128
+ keyHint: login.apiKey ? maskApiKey(login.apiKey) : null,
129
+ ...login,
130
+ };
103
131
  this.file(name).write(this.seal(account));
104
132
  return account;
105
133
  }
106
- /** The saved account's name, date and e-mail (nothing is decrypted). */
134
+ /** The saved account's name, date, e-mail and kind (nothing is decrypted). */
107
135
  meta(name) {
108
136
  const stored = validName(name) ? this.file(name).read() : null;
109
- return stored ? { name: stored.name, savedAt: stored.savedAt, email: stored.email } : null;
137
+ if (!stored)
138
+ return null;
139
+ const sealed = isSealed(stored) ? stored : null;
140
+ return {
141
+ name: stored.name,
142
+ savedAt: stored.savedAt,
143
+ email: stored.email,
144
+ kind: sealed?.kind ?? 'subscription',
145
+ keyHint: sealed?.keyHint ?? null,
146
+ };
110
147
  }
111
148
  /**
112
149
  * The saved account with its login decrypted. A plain file from an older version is encrypted on the spot (it stays
@@ -117,16 +154,29 @@ export class AccountStore {
117
154
  if (!stored)
118
155
  return null;
119
156
  if (isSealed(stored)) {
120
- const login = this.secrets.openJson(stored.sealed);
121
- return { name: stored.name, savedAt: stored.savedAt, email: stored.email, ...login };
157
+ const login = LoginSchema.parse(this.secrets.openJson(stored.sealed));
158
+ const kind = stored.kind ?? 'subscription';
159
+ if (kind !== 'subscription' && (login.apiKey === undefined || apiKeyProblem(login.apiKey) !== null))
160
+ fail('The saved account "' + name + '" holds no usable API key.', 'Remove it and save the key again.');
161
+ return {
162
+ name: stored.name,
163
+ savedAt: stored.savedAt,
164
+ email: stored.email,
165
+ kind,
166
+ keyHint: stored.keyHint ?? null,
167
+ credentials: login.credentials,
168
+ oauthAccount: login.oauthAccount,
169
+ ...(kind === 'subscription' ? {} : { apiKey: login.apiKey }),
170
+ };
122
171
  }
172
+ const plain = { ...stored, kind: 'subscription', keyHint: null };
123
173
  try {
124
- this.file(name).write(this.seal(stored));
174
+ this.file(name).write(this.seal(plain));
125
175
  }
126
176
  catch {
127
177
  // Could not encrypt now (DPAPI unavailable): the old plain file stays and is tried again next time.
128
178
  }
129
- return stored;
179
+ return plain;
130
180
  }
131
181
  /** Encrypts every account still saved as plain text (older versions); returns how many it did. */
132
182
  sealLegacy() {
@@ -171,22 +221,39 @@ export class AccountStore {
171
221
  isDefault: a.name === def,
172
222
  isActive: a.name === active,
173
223
  savedAt: a.savedAt,
224
+ kind: a.kind,
225
+ keyHint: a.keyHint,
174
226
  }));
175
227
  }
176
- /** Saves the sandbox's current login (or this PC's, with fromHost) under `name`. */
177
- save(dataPath, name, fromHost) {
228
+ /**
229
+ * Saves the sandbox's current login (or this PC's, with fromHost) under `name`, whichever kind it is (`how.keyOnly`:
230
+ * only an API key; `how.envKey`: this PC's ANTHROPIC_API_KEY when nothing else is found).
231
+ */
232
+ save(dataPath, name, fromHost, how = {}) {
178
233
  assertName(name);
179
234
  const source = fromHost ?? sandboxLogin(dataPath);
180
- const account = this.store(name, readLogin(source.claudeDir, source.globalStateFile));
181
- if (!fromHost)
235
+ return this.keep(dataPath, name, readLogin(source.claudeDir, source.globalStateFile, how), !fromHost);
236
+ }
237
+ /** Saves an API key you gave (typed in) under `name`. */
238
+ saveApiKey(dataPath, name, apiKey) {
239
+ assertName(name);
240
+ const key = apiKey.trim();
241
+ const problem = apiKeyProblem(key);
242
+ if (problem)
243
+ fail('That is not an Anthropic API key.', problem);
244
+ return this.keep(dataPath, name, { kind: 'api-key', credentials: null, oauthAccount: null, apiKey: key }, false);
245
+ }
246
+ keep(dataPath, name, login, fromSandbox) {
247
+ const account = this.store(name, login);
248
+ if (fromSandbox)
182
249
  setActive(dataPath, name);
183
250
  if (!this.defaultName())
184
251
  this.setDefault(name);
185
252
  return account;
186
253
  }
187
254
  /**
188
- * Makes `name` the sandbox's login. Claude refreshes (rotates) tokens while it runs, so the outgoing account's
189
- * current tokens are saved back first; otherwise its stored refresh token could already be invalid.
255
+ * Makes `name` the sandbox's login. Claude refreshes (rotates) subscription tokens while it runs, so the outgoing
256
+ * subscription's current tokens are saved back first; otherwise its stored refresh token could already be invalid.
190
257
  */
191
258
  activate(dataPath, name) {
192
259
  const account = this.read(name) ??
@@ -196,9 +263,12 @@ export class AccountStore {
196
263
  .join(', ') || 'none yet; save one with sandbox account save <name>'));
197
264
  const { claudeDir, globalStateFile } = sandboxLogin(dataPath);
198
265
  const outgoing = this.activeFor(dataPath);
199
- if (outgoing && outgoing !== name && this.meta(outgoing) && fileExists(path.join(claudeDir, '.credentials.json'))) {
266
+ if (outgoing && outgoing !== name && this.meta(outgoing)?.kind === 'subscription') {
200
267
  try {
201
- this.store(outgoing, readLogin(claudeDir, globalStateFile));
268
+ const current = readLogin(claudeDir, globalStateFile);
269
+ // Only a subscription is saved back over a subscription (a key login made inside would not be one).
270
+ if (current.kind === 'subscription')
271
+ this.store(outgoing, current);
202
272
  }
203
273
  catch {
204
274
  /* nothing usable to save back */
@@ -206,10 +276,20 @@ export class AccountStore {
206
276
  }
207
277
  fs.mkdirSync(claudeDir, { recursive: true });
208
278
  clearAccountCaches(claudeDir);
209
- writeSandboxJson(path.join(claudeDir, '.credentials.json'), account.credentials);
210
279
  const state = readUntrusted(globalStateFile, MAX_STATE_BYTES);
211
- if (account.oauthAccount && (state || !fileExists(globalStateFile)))
212
- writeSandboxJson(globalStateFile, { ...(state ?? {}), oauthAccount: account.oauthAccount });
280
+ // A state file that is there but cannot be read (too big, not JSON) is not replaced: it holds the projects' state.
281
+ const writable = state !== null || !fileExists(globalStateFile) || isLink(globalStateFile);
282
+ if (account.kind === 'subscription') {
283
+ writeSandboxJson(path.join(claudeDir, '.credentials.json'), account.credentials);
284
+ if (writable && (account.oauthAccount || typeof state?.primaryApiKey === 'string'))
285
+ writeSandboxJson(globalStateFile, subscriptionState(state ?? {}, account.oauthAccount));
286
+ }
287
+ else {
288
+ if (!writable)
289
+ fail("The sandbox's Claude state (" + globalStateFile + ') cannot be read, so the API key cannot be put in.', 'Delete that file (Claude makes a new one), then switch again.');
290
+ dropSubscription(claudeDir);
291
+ writeSandboxJson(globalStateFile, keyState(state ?? {}, account.apiKey ?? '', account.oauthAccount));
292
+ }
213
293
  this.shareStep(dataPath, claudeDir);
214
294
  setActive(dataPath, name);
215
295
  return account;
@@ -1,7 +1,25 @@
1
+ /** subscription: claude.ai OAuth; console: the key a Console login made; api-key: a key you gave. */
2
+ export type LoginKind = 'subscription' | 'console' | 'api-key';
3
+ export declare const LOGIN_KINDS: readonly ["subscription", "console", "api-key"];
1
4
  export interface Login {
5
+ readonly kind: LoginKind;
6
+ /** The text of .credentials.json (a subscription); null for a key. */
2
7
  readonly credentials: unknown;
3
8
  readonly oauthAccount: unknown;
9
+ /** The API key (console and api-key). */
10
+ readonly apiKey?: string;
4
11
  }
12
+ /** How a login is looked for. */
13
+ export interface CaptureOptions {
14
+ /** Only an API key (skip a subscription found next to it). */
15
+ readonly keyOnly?: boolean;
16
+ /** ANTHROPIC_API_KEY of this PC: the last place looked at (--from-host only). */
17
+ readonly envKey?: string | null;
18
+ }
19
+ /** What is wrong with a typed or found API key, or null when it has the right shape. Never quotes the key. */
20
+ export declare function apiKeyProblem(key: string): string | null;
21
+ /** The most of a key that is ever shown: sk-ant-...abcd. */
22
+ export declare function maskApiKey(key: string): string;
5
23
  export interface LoginFiles {
6
24
  readonly claudeDir: string;
7
25
  readonly globalStateFile: string;
@@ -12,8 +30,20 @@ export declare function sandboxClaudeDir(dataPath: string): string;
12
30
  export declare function emailOf(oauthAccount: unknown): string | null;
13
31
  /** Reads a JSON file that may be sandbox-written: no links, size-capped. */
14
32
  export declare function readUntrusted(file: string, max: number): unknown;
15
- /** The login currently in a Claude config folder (+ its global state file). */
16
- export declare function readLogin(claudeDir: string, globalStateFile: string): Login;
33
+ /**
34
+ * The login currently in a Claude config folder (+ its global state file), whichever kind it is: a subscription first
35
+ * (Claude uses it over a key), then a Console or API key in `primaryApiKey`, then `envKey`.
36
+ */
37
+ export declare function readLogin(claudeDir: string, globalStateFile: string, how?: CaptureOptions): Login;
38
+ /** The sandbox's .claude.json for a subscription: no `primaryApiKey` (Claude would ask about it), its e-mail block. */
39
+ export declare function subscriptionState(state: Record<string, unknown>, oauthAccount: unknown): Record<string, unknown>;
40
+ /** The sandbox's .claude.json for a key: the key where a Console login keeps it, approved, with its e-mail block. */
41
+ export declare function keyState(state: Record<string, unknown>, apiKey: string, oauthAccount: unknown): Record<string, unknown>;
42
+ /**
43
+ * Takes the subscription out of the sandbox's .credentials.json (Claude would use it over the key); the rest of the
44
+ * file (MCP server logins) stays. A file that cannot be read holds no login worth keeping and goes.
45
+ */
46
+ export declare function dropSubscription(claudeDir: string): void;
17
47
  /** The sandbox's Claude login files (refuses when the data folder holds a link on the way). */
18
48
  export declare function sandboxLogin(dataPath: string): LoginFiles;
19
49
  /** Replaces a file that may be a sandbox-planted link instead of writing through it. */
@@ -1,10 +1,43 @@
1
- // A Claude login as files: .credentials.json plus the `oauthAccount` block of .claude.json. The sandbox's copies are
2
- // guest-written: read without following links and size-capped; links are replaced, never written through.
1
+ // A Claude login as files. Claude Code keeps three kinds (checked against Claude Code 2.1.296):
2
+ // - a subscription (/login with Claude Pro, Max, Team ...): `claudeAiOauth` in .credentials.json;
3
+ // - a Console login (/login with a Claude Console account): Claude makes an API key and keeps it as `primaryApiKey` in
4
+ // .claude.json (not in .credentials.json), with the last 20 characters in `customApiKeyResponses.approved`;
5
+ // - an API key of your own (ANTHROPIC_API_KEY): goes where a Console key goes, so Claude uses it without asking.
6
+ // Plus the `oauthAccount` block of .claude.json (the e-mail). Claude uses a subscription over a `primaryApiKey`, so a
7
+ // key login goes in with no `claudeAiOauth` next to it, and a subscription goes in with no `primaryApiKey`.
8
+ // The sandbox's copies are guest-written: read without following links and size-capped; links are replaced, never
9
+ // written through. A key read from the sandbox must have the shape of an API key.
3
10
  import fs from 'node:fs';
4
11
  import path from 'node:path';
5
12
  import { assertTestWritable } from '../test-guard.js';
6
13
  import { hostDataFolder, hostDataRoot } from '../storage/index.js';
7
14
  import { assertNoLinks, fail, fileExists, isLink, removeLink, tryReadJson, writeJson } from '../../core/util/index.js';
15
+ export const LOGIN_KINDS = ['subscription', 'console', 'api-key'];
16
+ /** sk-ant-api03-...: a normal Anthropic API key (letters, digits, - and _, which is what Claude Code accepts). */
17
+ const API_KEY = /^sk-ant-api\d{2}-[A-Za-z0-9_-]{20,400}$/;
18
+ /** What is wrong with a typed or found API key, or null when it has the right shape. Never quotes the key. */
19
+ export function apiKeyProblem(key) {
20
+ if (API_KEY.test(key))
21
+ return null;
22
+ if (key.startsWith('sk-ant-oat'))
23
+ return 'That is a Claude subscription token (from claude setup-token), not an API key. Save the subscription login instead.';
24
+ if (key.startsWith('sk-ant-admin'))
25
+ return 'That is an Admin key: it cannot run Claude. Make an API key instead.';
26
+ return 'An Anthropic API key starts with sk-ant-api and has only letters, digits, - and _ (copy it from the Claude Console).';
27
+ }
28
+ /** The most of a key that is ever shown: sk-ant-...abcd. */
29
+ export function maskApiKey(key) {
30
+ return 'sk-ant-...' + key.slice(-4);
31
+ }
32
+ /** What Claude Code records in customApiKeyResponses for a key it may use (its last 20 characters). */
33
+ function approvalOf(key) {
34
+ return key.trim().slice(-20);
35
+ }
36
+ /** A subscription login: .credentials.json with a `claudeAiOauth` block. */
37
+ function hasSubscription(credentials) {
38
+ const oauth = credentials?.claudeAiOauth;
39
+ return typeof oauth === 'object' && oauth !== null;
40
+ }
8
41
  const MAX_LOGIN_BYTES = 1024 * 1024;
9
42
  /** .claude.json holds per-project state and can be large; still capped so a hostile file cannot exhaust memory. */
10
43
  export const MAX_STATE_BYTES = 256 * 1024 * 1024;
@@ -21,16 +54,75 @@ export function emailOf(oauthAccount) {
21
54
  export function readUntrusted(file, max) {
22
55
  return fileExists(file) && !isLink(file) && fs.statSync(file).size <= max ? tryReadJson(file) : null;
23
56
  }
24
- /** The login currently in a Claude config folder (+ its global state file). */
25
- export function readLogin(claudeDir, globalStateFile) {
57
+ /**
58
+ * The login currently in a Claude config folder (+ its global state file), whichever kind it is: a subscription first
59
+ * (Claude uses it over a key), then a Console or API key in `primaryApiKey`, then `envKey`.
60
+ */
61
+ export function readLogin(claudeDir, globalStateFile, how = {}) {
26
62
  const credentials = readUntrusted(path.join(claudeDir, '.credentials.json'), MAX_LOGIN_BYTES);
27
- if (!credentials || typeof credentials !== 'object')
28
- fail('No Claude login found in ' + claudeDir + '.', 'Sign in to Claude first (inside the sandbox, or on this PC with --from-host), then save it again.');
63
+ const state = readUntrusted(globalStateFile, MAX_STATE_BYTES);
64
+ const oauthAccount = state?.oauthAccount ?? null;
65
+ if (!how.keyOnly && hasSubscription(credentials))
66
+ return { kind: 'subscription', credentials, oauthAccount };
67
+ const managed = typeof state?.primaryApiKey === 'string' ? state.primaryApiKey.trim() : null;
68
+ if (managed !== null && apiKeyProblem(managed) === null)
69
+ return { kind: oauthAccount ? 'console' : 'api-key', credentials: null, oauthAccount, apiKey: managed };
70
+ const envKey = how.envKey?.trim();
71
+ if (envKey && apiKeyProblem(envKey) === null)
72
+ return { kind: 'api-key', credentials: null, oauthAccount: null, apiKey: envKey };
73
+ return how.keyOnly
74
+ ? fail('No Claude API key found in ' + claudeDir + (how.envKey === undefined ? '.' : ' or in ANTHROPIC_API_KEY.'), 'Sign in to Claude with a Claude Console account first, or save a key you type: sandbox account save <name> --api-key')
75
+ : fail('No Claude login found in ' + claudeDir + '.', 'Sign in to Claude first (inside the sandbox, or on this PC with --from-host), then save it again.');
76
+ }
77
+ /** customApiKeyResponses with `add` approved, or `remove` taken off the approved list (Claude's own shape). */
78
+ function approvals(state, change) {
79
+ const given = (state.customApiKeyResponses ?? {});
80
+ const strings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === 'string') : [];
81
+ const add = change.add === undefined ? undefined : approvalOf(change.add);
82
+ const remove = change.remove === undefined ? undefined : approvalOf(change.remove);
83
+ if (add === undefined && !strings(given.approved).includes(remove ?? ''))
84
+ return state;
85
+ const approved = strings(given.approved).filter((k) => k !== add && k !== remove);
29
86
  return {
30
- credentials,
31
- oauthAccount: readUntrusted(globalStateFile, MAX_STATE_BYTES)?.oauthAccount ?? null,
87
+ ...state,
88
+ customApiKeyResponses: {
89
+ approved: add === undefined ? approved : [...approved, add],
90
+ rejected: strings(given.rejected).filter((k) => k !== add),
91
+ },
32
92
  };
33
93
  }
94
+ /** The sandbox's .claude.json for a subscription: no `primaryApiKey` (Claude would ask about it), its e-mail block. */
95
+ export function subscriptionState(state, oauthAccount) {
96
+ const { primaryApiKey, ...rest } = state;
97
+ const cleaned = typeof primaryApiKey === 'string' ? approvals(rest, { remove: primaryApiKey }) : rest;
98
+ return oauthAccount ? { ...cleaned, oauthAccount } : cleaned;
99
+ }
100
+ /** The sandbox's .claude.json for a key: the key where a Console login keeps it, approved, with its e-mail block. */
101
+ export function keyState(state, apiKey, oauthAccount) {
102
+ const { primaryApiKey, oauthAccount: _outgoing, ...rest } = state;
103
+ const cleaned = typeof primaryApiKey === 'string' ? approvals(rest, { remove: primaryApiKey }) : rest;
104
+ return { ...approvals(cleaned, { add: apiKey }), primaryApiKey: apiKey, ...(oauthAccount ? { oauthAccount } : {}) };
105
+ }
106
+ /**
107
+ * Takes the subscription out of the sandbox's .credentials.json (Claude would use it over the key); the rest of the
108
+ * file (MCP server logins) stays. A file that cannot be read holds no login worth keeping and goes.
109
+ */
110
+ export function dropSubscription(claudeDir) {
111
+ const file = path.join(claudeDir, '.credentials.json');
112
+ assertTestWritable(file);
113
+ if (isLink(file))
114
+ return removeLink(file);
115
+ if (!fs.existsSync(file))
116
+ return;
117
+ const credentials = readUntrusted(file, MAX_LOGIN_BYTES);
118
+ if (typeof credentials !== 'object' || credentials === null || Array.isArray(credentials))
119
+ return fs.rmSync(file, { force: true });
120
+ const { claudeAiOauth: _login, ...rest } = credentials;
121
+ if (Object.keys(rest).length)
122
+ writeSandboxJson(file, rest);
123
+ else
124
+ fs.rmSync(file, { force: true });
125
+ }
34
126
  /** The sandbox's Claude login files (refuses when the data folder holds a link on the way). */
35
127
  export function sandboxLogin(dataPath) {
36
128
  assertNoLinks(hostDataRoot(dataPath), sandboxClaudeDir(dataPath));
@@ -6,4 +6,4 @@ export { toRegFile, type RegValue } from './reg-file.js';
6
6
  export { AccountStore, accountsDir, type Account, type AccountMeta, type AccountRow } from './accounts.js';
7
7
  export { PluginShare, PLUGIN_SETTING_KEYS, MAX_PLUGIN_FILES, MAX_PLUGIN_BYTES } from './plugin-share.js';
8
8
  export { AgentAccountStore } from './agent-accounts.js';
9
- export { sandboxClaudeDir } from './claude-login.js';
9
+ export { apiKeyProblem, maskApiKey, sandboxClaudeDir, type LoginKind } from './claude-login.js';
@@ -6,4 +6,4 @@ export { toRegFile } from './reg-file.js';
6
6
  export { AccountStore, accountsDir } from './accounts.js';
7
7
  export { PluginShare, PLUGIN_SETTING_KEYS, MAX_PLUGIN_FILES, MAX_PLUGIN_BYTES } from './plugin-share.js';
8
8
  export { AgentAccountStore } from './agent-accounts.js';
9
- export { sandboxClaudeDir } from './claude-login.js';
9
+ export { apiKeyProblem, maskApiKey, sandboxClaudeDir } from './claude-login.js';
@@ -14,6 +14,7 @@ export function writeSecrets(dataPath, id, secrets) {
14
14
  !secrets.vpn &&
15
15
  !secrets.wireguardConfig &&
16
16
  !secrets.githubToken &&
17
+ !Object.keys(secrets.serviceAccounts ?? {}).length &&
17
18
  !secrets.liveFoldersKey;
18
19
  if (empty)
19
20
  return null;