@geminixiang/mama 0.2.0-beta.1 → 0.2.0-beta.10

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 (271) hide show
  1. package/README.md +168 -371
  2. package/dist/adapter.d.ts +36 -12
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/adapter.js.map +1 -1
  5. package/dist/adapters/discord/bot.d.ts +12 -7
  6. package/dist/adapters/discord/bot.d.ts.map +1 -1
  7. package/dist/adapters/discord/bot.js +358 -135
  8. package/dist/adapters/discord/bot.js.map +1 -1
  9. package/dist/adapters/discord/context.d.ts +1 -1
  10. package/dist/adapters/discord/context.d.ts.map +1 -1
  11. package/dist/adapters/discord/context.js +100 -36
  12. package/dist/adapters/discord/context.js.map +1 -1
  13. package/dist/adapters/shared.d.ts +71 -0
  14. package/dist/adapters/shared.d.ts.map +1 -0
  15. package/dist/adapters/shared.js +168 -0
  16. package/dist/adapters/shared.js.map +1 -0
  17. package/dist/adapters/slack/bot.d.ts +30 -24
  18. package/dist/adapters/slack/bot.d.ts.map +1 -1
  19. package/dist/adapters/slack/bot.js +620 -224
  20. package/dist/adapters/slack/bot.js.map +1 -1
  21. package/dist/adapters/slack/branch-manager.d.ts +22 -0
  22. package/dist/adapters/slack/branch-manager.d.ts.map +1 -0
  23. package/dist/adapters/slack/branch-manager.js +97 -0
  24. package/dist/adapters/slack/branch-manager.js.map +1 -0
  25. package/dist/adapters/slack/context.d.ts +1 -1
  26. package/dist/adapters/slack/context.d.ts.map +1 -1
  27. package/dist/adapters/slack/context.js +127 -72
  28. package/dist/adapters/slack/context.js.map +1 -1
  29. package/dist/adapters/slack/session.d.ts +3 -0
  30. package/dist/adapters/slack/session.d.ts.map +1 -0
  31. package/dist/adapters/slack/session.js +16 -0
  32. package/dist/adapters/slack/session.js.map +1 -0
  33. package/dist/adapters/slack/tools/attach.d.ts +1 -1
  34. package/dist/adapters/slack/tools/attach.d.ts.map +1 -1
  35. package/dist/adapters/slack/tools/attach.js.map +1 -1
  36. package/dist/adapters/telegram/bot.d.ts +4 -2
  37. package/dist/adapters/telegram/bot.d.ts.map +1 -1
  38. package/dist/adapters/telegram/bot.js +193 -147
  39. package/dist/adapters/telegram/bot.js.map +1 -1
  40. package/dist/adapters/telegram/context.d.ts.map +1 -1
  41. package/dist/adapters/telegram/context.js +58 -111
  42. package/dist/adapters/telegram/context.js.map +1 -1
  43. package/dist/adapters/telegram/html.d.ts +3 -0
  44. package/dist/adapters/telegram/html.d.ts.map +1 -0
  45. package/dist/adapters/telegram/html.js +98 -0
  46. package/dist/adapters/telegram/html.js.map +1 -0
  47. package/dist/agent.d.ts +9 -13
  48. package/dist/agent.d.ts.map +1 -1
  49. package/dist/agent.js +601 -567
  50. package/dist/agent.js.map +1 -1
  51. package/dist/commands/auto-reply.d.ts +16 -0
  52. package/dist/commands/auto-reply.d.ts.map +1 -0
  53. package/dist/commands/auto-reply.js +69 -0
  54. package/dist/commands/auto-reply.js.map +1 -0
  55. package/dist/commands/index.d.ts +5 -0
  56. package/dist/commands/index.d.ts.map +1 -0
  57. package/dist/commands/index.js +19 -0
  58. package/dist/commands/index.js.map +1 -0
  59. package/dist/commands/login.d.ts +5 -0
  60. package/dist/commands/login.d.ts.map +1 -0
  61. package/dist/commands/login.js +76 -0
  62. package/dist/commands/login.js.map +1 -0
  63. package/dist/commands/model.d.ts +14 -0
  64. package/dist/commands/model.d.ts.map +1 -0
  65. package/dist/commands/model.js +112 -0
  66. package/dist/commands/model.js.map +1 -0
  67. package/dist/commands/new.d.ts +9 -0
  68. package/dist/commands/new.d.ts.map +1 -0
  69. package/dist/commands/new.js +28 -0
  70. package/dist/commands/new.js.map +1 -0
  71. package/dist/commands/registry.d.ts +7 -0
  72. package/dist/commands/registry.d.ts.map +1 -0
  73. package/dist/commands/registry.js +14 -0
  74. package/dist/commands/registry.js.map +1 -0
  75. package/dist/commands/sandbox.d.ts +10 -0
  76. package/dist/commands/sandbox.d.ts.map +1 -0
  77. package/dist/commands/sandbox.js +88 -0
  78. package/dist/commands/sandbox.js.map +1 -0
  79. package/dist/commands/session-view.d.ts +5 -0
  80. package/dist/commands/session-view.d.ts.map +1 -0
  81. package/dist/commands/session-view.js +62 -0
  82. package/dist/commands/session-view.js.map +1 -0
  83. package/dist/commands/types.d.ts +41 -0
  84. package/dist/commands/types.d.ts.map +1 -0
  85. package/dist/commands/types.js +2 -0
  86. package/dist/commands/types.js.map +1 -0
  87. package/dist/commands/utils.d.ts +8 -0
  88. package/dist/commands/utils.d.ts.map +1 -0
  89. package/dist/commands/utils.js +14 -0
  90. package/dist/commands/utils.js.map +1 -0
  91. package/dist/config.d.ts +49 -30
  92. package/dist/config.d.ts.map +1 -1
  93. package/dist/config.js +313 -75
  94. package/dist/config.js.map +1 -1
  95. package/dist/context.d.ts +10 -42
  96. package/dist/context.d.ts.map +1 -1
  97. package/dist/context.js +14 -127
  98. package/dist/context.js.map +1 -1
  99. package/dist/events.d.ts +13 -6
  100. package/dist/events.d.ts.map +1 -1
  101. package/dist/events.js +118 -64
  102. package/dist/events.js.map +1 -1
  103. package/dist/execution-resolver.d.ts +9 -5
  104. package/dist/execution-resolver.d.ts.map +1 -1
  105. package/dist/execution-resolver.js +82 -18
  106. package/dist/execution-resolver.js.map +1 -1
  107. package/dist/file-guards.d.ts +6 -0
  108. package/dist/file-guards.d.ts.map +1 -0
  109. package/dist/file-guards.js +48 -0
  110. package/dist/file-guards.js.map +1 -0
  111. package/dist/fs-atomic.d.ts +10 -0
  112. package/dist/fs-atomic.d.ts.map +1 -0
  113. package/dist/fs-atomic.js +45 -0
  114. package/dist/fs-atomic.js.map +1 -0
  115. package/dist/index.d.ts +7 -0
  116. package/dist/index.d.ts.map +1 -0
  117. package/dist/index.js +4 -0
  118. package/dist/index.js.map +1 -0
  119. package/dist/instrument.d.ts.map +1 -1
  120. package/dist/instrument.js +4 -11
  121. package/dist/instrument.js.map +1 -1
  122. package/dist/log.d.ts +1 -5
  123. package/dist/log.d.ts.map +1 -1
  124. package/dist/log.js +13 -38
  125. package/dist/log.js.map +1 -1
  126. package/dist/{login.d.ts → login/index.d.ts} +16 -4
  127. package/dist/login/index.d.ts.map +1 -0
  128. package/dist/{login.js → login/index.js} +55 -17
  129. package/dist/login/index.js.map +1 -0
  130. package/dist/{link-server.d.ts → login/portal.d.ts} +7 -4
  131. package/dist/login/portal.d.ts.map +1 -0
  132. package/dist/login/portal.js +1453 -0
  133. package/dist/login/portal.js.map +1 -0
  134. package/dist/{link-token.d.ts → login/session.d.ts} +4 -3
  135. package/dist/login/session.d.ts.map +1 -0
  136. package/dist/{link-token.js → login/session.js} +1 -1
  137. package/dist/login/session.js.map +1 -0
  138. package/dist/main.d.ts.map +1 -1
  139. package/dist/main.js +151 -373
  140. package/dist/main.js.map +1 -1
  141. package/dist/provisioner.d.ts +38 -52
  142. package/dist/provisioner.d.ts.map +1 -1
  143. package/dist/provisioner.js +212 -111
  144. package/dist/provisioner.js.map +1 -1
  145. package/dist/runtime/conversation-orchestrator.d.ts +42 -0
  146. package/dist/runtime/conversation-orchestrator.d.ts.map +1 -0
  147. package/dist/runtime/conversation-orchestrator.js +150 -0
  148. package/dist/runtime/conversation-orchestrator.js.map +1 -0
  149. package/dist/runtime/index.d.ts +2 -0
  150. package/dist/runtime/index.d.ts.map +1 -0
  151. package/dist/runtime/index.js +2 -0
  152. package/dist/runtime/index.js.map +1 -0
  153. package/dist/runtime/session-runtime.d.ts +27 -0
  154. package/dist/runtime/session-runtime.d.ts.map +1 -0
  155. package/dist/runtime/session-runtime.js +211 -0
  156. package/dist/runtime/session-runtime.js.map +1 -0
  157. package/dist/sandbox/cloudflare.d.ts +15 -0
  158. package/dist/sandbox/cloudflare.d.ts.map +1 -0
  159. package/dist/sandbox/cloudflare.js +137 -0
  160. package/dist/sandbox/cloudflare.js.map +1 -0
  161. package/dist/sandbox/container.d.ts +2 -1
  162. package/dist/sandbox/container.d.ts.map +1 -1
  163. package/dist/sandbox/container.js +5 -1
  164. package/dist/sandbox/container.js.map +1 -1
  165. package/dist/sandbox/firecracker.d.ts +2 -1
  166. package/dist/sandbox/firecracker.d.ts.map +1 -1
  167. package/dist/sandbox/firecracker.js +6 -0
  168. package/dist/sandbox/firecracker.js.map +1 -1
  169. package/dist/sandbox/host.d.ts +2 -3
  170. package/dist/sandbox/host.d.ts.map +1 -1
  171. package/dist/sandbox/host.js +5 -5
  172. package/dist/sandbox/host.js.map +1 -1
  173. package/dist/sandbox/index.d.ts +6 -4
  174. package/dist/sandbox/index.d.ts.map +1 -1
  175. package/dist/sandbox/index.js +9 -6
  176. package/dist/sandbox/index.js.map +1 -1
  177. package/dist/sandbox/path-context.d.ts +4 -0
  178. package/dist/sandbox/path-context.d.ts.map +1 -0
  179. package/dist/sandbox/path-context.js +20 -0
  180. package/dist/sandbox/path-context.js.map +1 -0
  181. package/dist/sandbox/types.d.ts +17 -1
  182. package/dist/sandbox/types.d.ts.map +1 -1
  183. package/dist/sandbox/types.js.map +1 -1
  184. package/dist/sentry.d.ts +1 -1
  185. package/dist/sentry.d.ts.map +1 -1
  186. package/dist/sentry.js +4 -2
  187. package/dist/sentry.js.map +1 -1
  188. package/dist/session-policy.d.ts +13 -0
  189. package/dist/session-policy.d.ts.map +1 -0
  190. package/dist/session-policy.js +23 -0
  191. package/dist/session-policy.js.map +1 -0
  192. package/dist/session-store.d.ts +34 -3
  193. package/dist/session-store.d.ts.map +1 -1
  194. package/dist/session-store.js +184 -22
  195. package/dist/session-store.js.map +1 -1
  196. package/dist/session-view/command.d.ts +5 -0
  197. package/dist/session-view/command.d.ts.map +1 -0
  198. package/dist/session-view/command.js +11 -0
  199. package/dist/session-view/command.js.map +1 -0
  200. package/dist/session-view/portal.d.ts +16 -0
  201. package/dist/session-view/portal.d.ts.map +1 -0
  202. package/dist/session-view/portal.js +1742 -0
  203. package/dist/session-view/portal.js.map +1 -0
  204. package/dist/session-view/service.d.ts +34 -0
  205. package/dist/session-view/service.d.ts.map +1 -0
  206. package/dist/session-view/service.js +427 -0
  207. package/dist/session-view/service.js.map +1 -0
  208. package/dist/session-view/store.d.ts +18 -0
  209. package/dist/session-view/store.d.ts.map +1 -0
  210. package/dist/session-view/store.js +39 -0
  211. package/dist/session-view/store.js.map +1 -0
  212. package/dist/store.d.ts +3 -6
  213. package/dist/store.d.ts.map +1 -1
  214. package/dist/store.js +22 -48
  215. package/dist/store.js.map +1 -1
  216. package/dist/tool-diagnostics.d.ts +2 -0
  217. package/dist/tool-diagnostics.d.ts.map +1 -0
  218. package/dist/tool-diagnostics.js +7 -0
  219. package/dist/tool-diagnostics.js.map +1 -0
  220. package/dist/tools/bash.d.ts +1 -1
  221. package/dist/tools/bash.d.ts.map +1 -1
  222. package/dist/tools/bash.js.map +1 -1
  223. package/dist/tools/edit.d.ts +1 -1
  224. package/dist/tools/edit.d.ts.map +1 -1
  225. package/dist/tools/edit.js.map +1 -1
  226. package/dist/tools/event.d.ts +43 -2
  227. package/dist/tools/event.d.ts.map +1 -1
  228. package/dist/tools/event.js +48 -13
  229. package/dist/tools/event.js.map +1 -1
  230. package/dist/tools/index.d.ts +2 -1
  231. package/dist/tools/index.d.ts.map +1 -1
  232. package/dist/tools/index.js +3 -3
  233. package/dist/tools/index.js.map +1 -1
  234. package/dist/tools/read.d.ts +1 -1
  235. package/dist/tools/read.d.ts.map +1 -1
  236. package/dist/tools/read.js.map +1 -1
  237. package/dist/tools/write.d.ts +1 -1
  238. package/dist/tools/write.d.ts.map +1 -1
  239. package/dist/tools/write.js.map +1 -1
  240. package/dist/trigger.d.ts +31 -0
  241. package/dist/trigger.d.ts.map +1 -0
  242. package/dist/trigger.js +98 -0
  243. package/dist/trigger.js.map +1 -0
  244. package/dist/ui-copy.d.ts +1 -0
  245. package/dist/ui-copy.d.ts.map +1 -1
  246. package/dist/ui-copy.js +3 -0
  247. package/dist/ui-copy.js.map +1 -1
  248. package/dist/vault-routing.d.ts +1 -7
  249. package/dist/vault-routing.d.ts.map +1 -1
  250. package/dist/vault-routing.js +6 -48
  251. package/dist/vault-routing.js.map +1 -1
  252. package/dist/vault.d.ts +21 -55
  253. package/dist/vault.d.ts.map +1 -1
  254. package/dist/vault.js +138 -263
  255. package/dist/vault.js.map +1 -1
  256. package/package.json +12 -10
  257. package/dist/bindings.d.ts +0 -63
  258. package/dist/bindings.d.ts.map +0 -1
  259. package/dist/bindings.js +0 -94
  260. package/dist/bindings.js.map +0 -1
  261. package/dist/link-server.d.ts.map +0 -1
  262. package/dist/link-server.js +0 -839
  263. package/dist/link-server.js.map +0 -1
  264. package/dist/link-token.d.ts.map +0 -1
  265. package/dist/link-token.js.map +0 -1
  266. package/dist/login.d.ts.map +0 -1
  267. package/dist/login.js.map +0 -1
  268. package/dist/vault.test.d.ts +0 -2
  269. package/dist/vault.test.d.ts.map +0 -1
  270. package/dist/vault.test.js +0 -67
  271. package/dist/vault.test.js.map +0 -1
package/dist/vault.js CHANGED
@@ -1,8 +1,25 @@
1
- import { chmodSync, closeSync, constants as fsConstants, existsSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeSync, } from "fs";
2
- import { randomBytes } from "crypto";
3
- import { basename, dirname, isAbsolute, join, normalize, sep } from "path";
1
+ import { chmodSync, copyFileSync, existsSync, mkdirSync, readdirSync, rmSync } from "fs";
2
+ import { dirname, isAbsolute, join, normalize, sep } from "path";
3
+ import { readTextFileIfExists } from "./file-guards.js";
4
+ import { atomicWritePrivateFile } from "./fs-atomic.js";
4
5
  const PRIVATE_DIR_MODE = 0o700;
5
- const PRIVATE_FILE_MODE = 0o600;
6
+ const SHARED_VAULT_DIR = "shared";
7
+ export function normalizeSharedVaultName(name) {
8
+ const trimmed = name.trim();
9
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(trimmed))
10
+ return undefined;
11
+ return trimmed;
12
+ }
13
+ export function sharedVaultKey(name) {
14
+ const normalized = normalizeSharedVaultName(name);
15
+ return normalized ? `${SHARED_VAULT_DIR}/${normalized}` : undefined;
16
+ }
17
+ function sanitizeCloudflareSandboxId(value) {
18
+ return (value
19
+ .toLowerCase()
20
+ .replace(/[^a-z0-9-]+/g, "-")
21
+ .replace(/^-+|-+$/g, "") || "unknown");
22
+ }
6
23
  // ── parseEnvFile ───────────────────────────────────────────────────────────────
7
24
  /**
8
25
  * Parse a KEY=VALUE env file. Supports:
@@ -26,7 +43,6 @@ export function parseEnvFile(content) {
26
43
  if (!key)
27
44
  continue;
28
45
  let value = trimmed.slice(eqIndex + 1);
29
- // Strip matching quotes
30
46
  if ((value.startsWith('"') && value.endsWith('"')) ||
31
47
  (value.startsWith("'") && value.endsWith("'"))) {
32
48
  value = value.slice(1, -1);
@@ -38,178 +54,86 @@ export function parseEnvFile(content) {
38
54
  // ── FileVaultManager ───────────────────────────────────────────────────────────
39
55
  export class FileVaultManager {
40
56
  constructor(stateDir) {
41
- this.config = null;
42
57
  this.vaultsDir = join(stateDir, "vaults");
43
- this.configPath = join(this.vaultsDir, "vault.json");
44
- this.reload();
45
- }
46
- reload() {
47
- if (!existsSync(this.configPath)) {
48
- this.config = null;
49
- return;
50
- }
51
- try {
52
- const raw = readFileSync(this.configPath, "utf-8");
53
- const parsed = JSON.parse(raw);
54
- if (!parsed ||
55
- typeof parsed !== "object" ||
56
- !parsed.vaults ||
57
- typeof parsed.vaults !== "object") {
58
- console.error(`vault: malformed vault.json — expected { vaults: { ... } }`);
59
- this.config = null;
60
- return;
61
- }
62
- this.config = parsed;
63
- this.warnUnsupportedSandboxTypes();
64
- }
65
- catch (err) {
66
- console.error(`vault: failed to read ${this.configPath}:`, err);
67
- this.config = null;
68
- }
69
- }
70
- /** Warn for legacy or insecure vault sandbox overrides that are no longer allowed. */
71
- warnUnsupportedSandboxTypes() {
72
- if (!this.config)
73
- return;
74
- for (const [key, entry] of Object.entries(this.config.vaults)) {
75
- if (entry.sandbox?.type === "host") {
76
- console.error(`vault: "${key}" uses sandbox.type=host, which is blocked for credential isolation. ` +
77
- "Use sandbox.type=image or sandbox.type=firecracker.");
78
- }
79
- if (entry.sandbox?.type === "container" || entry.sandbox?.type === "docker") {
80
- console.error(`vault: "${key}" uses sandbox.type=${entry.sandbox.type}, which is blocked for credential isolation. ` +
81
- "Use sandbox.type=image for per-user containers or sandbox.type=firecracker.");
82
- }
83
- }
84
58
  }
85
59
  isEnabled() {
86
- return this.config !== null;
60
+ return existsSync(this.vaultsDir);
87
61
  }
88
62
  hasEntry(key) {
89
- return !!this.config?.vaults[key];
63
+ return existsSync(join(this.vaultsDir, key));
64
+ }
65
+ listSharedVaults() {
66
+ const sharedDir = join(this.vaultsDir, SHARED_VAULT_DIR);
67
+ if (!existsSync(sharedDir))
68
+ return [];
69
+ return readdirSync(sharedDir, { withFileTypes: true })
70
+ .filter((entry) => entry.isDirectory() && normalizeSharedVaultName(entry.name) === entry.name)
71
+ .map((entry) => entry.name)
72
+ .toSorted((left, right) => left.localeCompare(right));
73
+ }
74
+ deleteSharedVault(name) {
75
+ const key = sharedVaultKey(name);
76
+ if (!key)
77
+ throw new Error(`vault: invalid shared login name: ${name}`);
78
+ const dir = join(this.vaultsDir, key);
79
+ const existed = existsSync(dir);
80
+ rmSync(dir, { recursive: true, force: true });
81
+ return existed;
82
+ }
83
+ copySharedVaultTo(name, targetKey) {
84
+ const sourceKey = sharedVaultKey(name);
85
+ if (!sourceKey)
86
+ throw new Error(`vault: invalid shared login name: ${name}`);
87
+ const sourceDir = join(this.vaultsDir, sourceKey);
88
+ if (!existsSync(sourceDir))
89
+ throw new Error(`vault: shared login "${name}" does not exist`);
90
+ const targetDir = join(this.vaultsDir, targetKey);
91
+ ensurePrivateDir(this.vaultsDir);
92
+ ensurePrivateDir(targetDir);
93
+ return copyVaultDir(sourceDir, targetDir);
90
94
  }
91
95
  resolve(userId) {
92
- const entry = this.config?.vaults[userId];
93
- if (!entry)
96
+ const dir = join(this.vaultsDir, userId);
97
+ if (!existsSync(dir))
94
98
  return undefined;
95
- return this.buildResolved(userId, entry);
99
+ return this.buildResolved(userId);
96
100
  }
97
101
  getSandboxConfig(userId, baseConfig) {
98
- const vault = this.resolve(userId);
99
- if (!vault?.sandboxOverride)
100
- return baseConfig;
101
- const override = vault.sandboxOverride;
102
- if (override.type === "image") {
103
- if (baseConfig.type !== "image") {
104
- throw new Error(`vault "${userId}" sets sandbox.type=image, but base sandbox is "${baseConfig.type}". ` +
105
- "Use --sandbox=image:<image> to enable per-user managed containers.");
106
- }
107
- const container = override.container || `mama-sandbox-${userId}`;
108
- return { type: "container", container };
109
- }
110
- if (override.type === "firecracker") {
111
- if (!override.vmId)
112
- return baseConfig;
113
- if (baseConfig.type !== "firecracker") {
114
- throw new Error(`vault "${userId}" sets sandbox.type=firecracker, but base sandbox is "${baseConfig.type}". ` +
115
- "Use --sandbox=firecracker:<vm-id>:<host-path> so /workspace stays mapped to the real workspace.");
116
- }
102
+ if (baseConfig.type === "cloudflare") {
117
103
  return {
118
- type: "firecracker",
119
- vmId: override.vmId,
120
- hostPath: baseConfig.hostPath,
121
- sshUser: override.sshUser,
122
- sshPort: override.sshPort,
104
+ type: "cloudflare",
105
+ sandboxId: `${baseConfig.sandboxId}-${sanitizeCloudflareSandboxId(userId)}`,
123
106
  };
124
107
  }
125
- if (override.type === "host") {
126
- throw new Error(`vault "${userId}" uses sandbox.type=host, which is blocked for credential isolation. ` +
127
- "Use sandbox.type=image or sandbox.type=firecracker.");
128
- }
129
- if (override.type === "container" || override.type === "docker") {
130
- throw new Error(`vault "${userId}" uses sandbox.type=${override.type}, which is blocked for credential isolation. ` +
131
- "Use sandbox.type=image for per-user containers or sandbox.type=firecracker.");
132
- }
133
- // No type override — return base config unchanged
134
108
  return baseConfig;
135
109
  }
136
110
  list() {
137
- if (!this.config)
111
+ if (!existsSync(this.vaultsDir))
138
112
  return [];
139
- const results = [];
140
- for (const [key, entry] of Object.entries(this.config.vaults)) {
141
- results.push(this.buildResolved(key, entry));
113
+ const keys = new Set();
114
+ for (const entry of readdirSync(this.vaultsDir, { withFileTypes: true })) {
115
+ if (entry.isDirectory())
116
+ keys.add(entry.name);
142
117
  }
143
- return results;
144
- }
145
- addEntry(key, entry) {
146
- if (!this.config) {
147
- this.config = { vaults: {} };
148
- }
149
- // Idempotent: skip if already exists
150
- if (this.config.vaults[key])
151
- return;
152
- this.config.vaults[key] = entry;
153
- this.persistConfig();
154
- }
155
- ensureImageSandboxEntry(key, entry) {
156
- if (entry.sandbox?.type !== "image") {
157
- throw new Error(`vault: ensureImageSandboxEntry requires sandbox.type=image for "${key}"`);
158
- }
159
- if (!this.config) {
160
- this.config = { vaults: {} };
161
- }
162
- const existing = this.config.vaults[key];
163
- if (!existing) {
164
- this.config.vaults[key] = entry;
165
- this.persistConfig();
166
- return;
167
- }
168
- let nextEntry = existing;
169
- let changed = false;
170
- if (!existing.platform && entry.platform) {
171
- nextEntry = { ...nextEntry, platform: entry.platform };
172
- changed = true;
173
- }
174
- const existingSandbox = existing.sandbox;
175
- if (!existingSandbox?.type) {
176
- nextEntry = { ...nextEntry, sandbox: entry.sandbox };
177
- changed = true;
178
- }
179
- else if (existingSandbox.type === "image" &&
180
- !existingSandbox.container &&
181
- entry.sandbox.container) {
182
- nextEntry = {
183
- ...nextEntry,
184
- sandbox: { ...existingSandbox, container: entry.sandbox.container },
185
- };
186
- changed = true;
187
- }
188
- if (!changed) {
189
- return;
190
- }
191
- this.config.vaults[key] = nextEntry;
192
- this.persistConfig();
118
+ return Array.from(keys, (key) => this.buildResolved(key));
193
119
  }
194
120
  upsertEnv(key, env) {
195
121
  const dir = join(this.vaultsDir, key);
196
122
  const envPath = join(dir, "env");
197
123
  ensurePrivateDir(this.vaultsDir);
198
124
  ensurePrivateDir(dir);
199
- const existing = existsSync(envPath)
200
- ? parseEnvFile(readFileSync(envPath, "utf-8"))
201
- : {};
125
+ const existingContent = readTextFileIfExists(envPath);
126
+ const existing = existingContent ? parseEnvFile(existingContent) : {};
202
127
  const merged = { ...existing, ...env };
203
128
  const content = Object.entries(merged)
204
- .sort(([left], [right]) => left.localeCompare(right))
129
+ .toSorted(([left], [right]) => left.localeCompare(right))
205
130
  .map(([envKey, value]) => `${envKey}=${value}`)
206
131
  .join("\n") + "\n";
207
132
  atomicWritePrivateFile(envPath, content);
208
133
  }
209
134
  upsertFile(key, relativePath, content, targetPath) {
210
135
  const normalizedPath = normalizeVaultRelativePath(relativePath);
211
- const normalizedTarget = normalizeVaultTargetPath(targetPath);
212
- if (!normalizedPath || (targetPath !== undefined && !normalizedTarget)) {
136
+ if (!normalizedPath || (targetPath !== undefined && !normalizeVaultTargetPath(targetPath))) {
213
137
  throw new Error(`vault: invalid relative secret file path for "${key}": ${relativePath}`);
214
138
  }
215
139
  const dir = join(this.vaultsDir, key);
@@ -220,69 +144,16 @@ export class FileVaultManager {
220
144
  if (parentDir !== dir)
221
145
  ensurePrivateDir(parentDir);
222
146
  atomicWritePrivateFile(filePath, content);
223
- this.ensureMountEntry(key, normalizedPath, normalizedTarget);
224
147
  }
225
148
  // ── private ────────────────────────────────────────────────────────────────
226
- persistConfig() {
227
- ensurePrivateDir(this.vaultsDir);
228
- // Preserve concurrent external edits: pull in any entries that appear on
229
- // disk but not in our in-memory view, so a background edit (e.g. another
230
- // admin adding a user) is not silently dropped by the next upsert here.
231
- // Individual field edits still follow last-writer-wins per key.
232
- const onDisk = this.readConfigFromDisk();
233
- if (onDisk && this.config) {
234
- for (const [key, entry] of Object.entries(onDisk.vaults)) {
235
- if (!(key in this.config.vaults)) {
236
- this.config.vaults[key] = entry;
237
- }
238
- }
239
- }
240
- atomicWritePrivateFile(this.configPath, JSON.stringify(this.config, null, 2) + "\n");
241
- }
242
- readConfigFromDisk() {
243
- if (!existsSync(this.configPath))
244
- return null;
245
- try {
246
- const parsed = JSON.parse(readFileSync(this.configPath, "utf-8"));
247
- if (!parsed ||
248
- typeof parsed !== "object" ||
249
- !parsed.vaults ||
250
- typeof parsed.vaults !== "object") {
251
- return null;
252
- }
253
- return parsed;
254
- }
255
- catch {
256
- return null;
257
- }
258
- }
259
- ensureMountEntry(key, relativePath, targetPath) {
260
- if (!this.config?.vaults[key]) {
261
- throw new Error(`vault: cannot add mount "${relativePath}" for missing entry "${key}"`);
262
- }
263
- const existing = this.config.vaults[key];
264
- const mounts = existing.mounts ?? [];
265
- if (mounts.some((mount) => typeof mount === "string"
266
- ? mount === relativePath && !targetPath
267
- : mount.source === relativePath && mount.target === targetPath)) {
268
- return;
269
- }
270
- this.config.vaults[key] = {
271
- ...existing,
272
- mounts: [...mounts, targetPath ? { source: relativePath, target: targetPath } : relativePath],
273
- };
274
- this.persistConfig();
275
- }
276
- buildResolved(key, entry) {
149
+ buildResolved(key) {
277
150
  const dir = join(this.vaultsDir, key);
278
- const mounts = (entry.mounts ?? [])
279
- .map((mount) => this.resolveMountEntry(dir, mount))
280
- .filter((mount) => mount !== undefined);
151
+ const mounts = inferMountsFromDir(dir);
281
152
  let env = {};
282
- const envPath = join(dir, "env");
283
- if (entry.envFile !== false && existsSync(envPath)) {
153
+ const envContent = readTextFileIfExists(join(dir, "env"));
154
+ if (envContent !== undefined) {
284
155
  try {
285
- env = parseEnvFile(readFileSync(envPath, "utf-8"));
156
+ env = parseEnvFile(envContent);
286
157
  }
287
158
  catch (err) {
288
159
  console.error(`vault: failed to parse env file for "${key}":`, err);
@@ -290,76 +161,64 @@ export class FileVaultManager {
290
161
  }
291
162
  return {
292
163
  userId: key,
293
- displayName: entry.displayName,
164
+ displayName: key,
294
165
  dir,
295
166
  mounts,
296
167
  env,
297
- sandboxOverride: entry.sandbox,
298
168
  };
299
169
  }
300
- resolveMountEntry(dir, mount) {
301
- if (typeof mount === "string") {
302
- const normalizedSource = normalizeVaultRelativePath(mount);
303
- if (!normalizedSource)
304
- return undefined;
305
- return {
306
- source: join(dir, normalizedSource),
307
- target: defaultVaultTargetPath(normalizedSource),
308
- };
309
- }
310
- if (!mount || typeof mount !== "object")
311
- return undefined;
312
- const normalizedSource = normalizeVaultRelativePath(mount.source);
313
- if (!normalizedSource)
314
- return undefined;
315
- const normalizedTarget = normalizeVaultTargetPath(mount.target);
316
- return {
317
- source: join(dir, normalizedSource),
318
- target: normalizedTarget ?? defaultVaultTargetPath(normalizedSource),
319
- };
170
+ }
171
+ function inferMountsFromDir(dir) {
172
+ if (!existsSync(dir))
173
+ return [];
174
+ const mounts = [];
175
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
176
+ if (entry.name === "env")
177
+ continue;
178
+ const source = join(dir, entry.name);
179
+ const target = inferredVaultTargetPath(entry.name);
180
+ if (!target)
181
+ continue;
182
+ mounts.push({ source, target });
320
183
  }
184
+ return mounts;
321
185
  }
322
186
  function ensurePrivateDir(path) {
323
187
  mkdirSync(path, { recursive: true, mode: PRIVATE_DIR_MODE });
324
188
  chmodSync(path, PRIVATE_DIR_MODE);
325
189
  }
326
- /**
327
- * Write `content` to `targetPath` with mode 0600, even when `targetPath`
328
- * already exists. Uses O_CREAT|O_EXCL on a temp sibling (so the kernel
329
- * guarantees permissions at creation, not after a racy chmod) and then
330
- * rename(2) into place for atomicity. Readers never see a torn write.
331
- */
332
- function atomicWritePrivateFile(targetPath, content) {
333
- const dir = dirname(targetPath);
334
- const tmpPath = join(dir, `.${basename(targetPath)}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`);
335
- const fd = openSync(tmpPath, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL, PRIVATE_FILE_MODE);
336
- try {
337
- writeSync(fd, content);
338
- }
339
- catch (err) {
340
- try {
341
- unlinkSync(tmpPath);
342
- }
343
- catch {
344
- // ignore — original error is more informative
345
- }
346
- throw err;
347
- }
348
- finally {
349
- closeSync(fd);
350
- }
351
- try {
352
- renameSync(tmpPath, targetPath);
353
- }
354
- catch (err) {
355
- try {
356
- unlinkSync(tmpPath);
190
+ function copyVaultDir(sourceDir, targetDir) {
191
+ let filesCopied = 0;
192
+ let envKeysCopied = 0;
193
+ for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
194
+ const sourcePath = join(sourceDir, entry.name);
195
+ const targetPath = join(targetDir, entry.name);
196
+ if (entry.name === "env" && entry.isFile()) {
197
+ const sourceEnv = parseEnvFile(readTextFileIfExists(sourcePath) ?? "");
198
+ const targetEnv = parseEnvFile(readTextFileIfExists(targetPath) ?? "");
199
+ const merged = { ...targetEnv, ...sourceEnv };
200
+ const content = Object.entries(merged)
201
+ .toSorted(([left], [right]) => left.localeCompare(right))
202
+ .map(([envKey, value]) => `${envKey}=${value}`)
203
+ .join("\n") + "\n";
204
+ atomicWritePrivateFile(targetPath, content);
205
+ envKeysCopied += Object.keys(sourceEnv).length;
206
+ continue;
357
207
  }
358
- catch {
359
- // ignore
208
+ if (entry.isDirectory()) {
209
+ ensurePrivateDir(targetPath);
210
+ const nested = copyVaultDir(sourcePath, targetPath);
211
+ filesCopied += nested.filesCopied;
212
+ envKeysCopied += nested.envKeysCopied;
213
+ continue;
360
214
  }
361
- throw err;
215
+ if (!entry.isFile())
216
+ continue;
217
+ copyFileSync(sourcePath, targetPath);
218
+ chmodSync(targetPath, 0o600);
219
+ filesCopied++;
362
220
  }
221
+ return { filesCopied, envKeysCopied };
363
222
  }
364
223
  function normalizeVaultRelativePath(relativePath) {
365
224
  const trimmed = relativePath.trim();
@@ -372,13 +231,11 @@ function normalizeVaultRelativePath(relativePath) {
372
231
  return normalized;
373
232
  }
374
233
  function normalizeVaultTargetPath(targetPath) {
375
- if (targetPath === undefined) {
234
+ if (targetPath === undefined)
376
235
  return undefined;
377
- }
378
236
  const trimmed = targetPath.trim();
379
- if (!trimmed || !trimmed.startsWith("/")) {
237
+ if (!trimmed || !trimmed.startsWith("/"))
380
238
  return undefined;
381
- }
382
239
  const normalized = normalize(trimmed).split(sep).join("/");
383
240
  return normalized.startsWith("/") ? normalized : undefined;
384
241
  }
@@ -386,4 +243,22 @@ export function defaultVaultTargetPath(relativePath) {
386
243
  const normalized = normalizeVaultRelativePath(relativePath) ?? relativePath.replace(/^\/+/, "");
387
244
  return `/root/${normalized}`;
388
245
  }
246
+ function inferredVaultTargetPath(relativePath) {
247
+ const normalized = normalizeVaultRelativePath(relativePath);
248
+ if (!normalized)
249
+ return undefined;
250
+ if (normalized === "gws.json") {
251
+ return "/root/.config/gws/credentials.json";
252
+ }
253
+ if (normalized === ".ssh" || normalized.startsWith(".ssh/")) {
254
+ return "/root/.ssh";
255
+ }
256
+ if (normalized === ".kube" || normalized.startsWith(".kube/")) {
257
+ return "/root/.kube";
258
+ }
259
+ if (normalized === ".config/gh" || normalized.startsWith(".config/gh/")) {
260
+ return "/root/.config/gh";
261
+ }
262
+ return defaultVaultTargetPath(normalized);
263
+ }
389
264
  //# sourceMappingURL=vault.js.map
package/dist/vault.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"vault.js","sourceRoot":"","sources":["../src/vault.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n chmodSync,\n closeSync,\n constants as fsConstants,\n existsSync,\n mkdirSync,\n openSync,\n readFileSync,\n renameSync,\n unlinkSync,\n writeSync,\n} from \"fs\";\nimport { randomBytes } from \"crypto\";\nimport { basename, dirname, isAbsolute, join, normalize, sep } from \"path\";\nimport type { SandboxConfig } from \"./sandbox.js\";\n\nconst PRIVATE_DIR_MODE = 0o700;\nconst PRIVATE_FILE_MODE = 0o600;\n\n// ── Types ──────────────────────────────────────────────────────────────────────\n\n/** Shape of workspace/vaults/vault.json */\nexport interface VaultConfig {\n vaults: Record<string, VaultEntry>;\n}\n\n/** Per-user vault mount entry in vault.json */\nexport interface VaultMountEntry {\n source: string;\n target?: string;\n}\n\n/** Per-user vault entry in vault.json */\nexport interface VaultEntry {\n displayName: string;\n platform?: \"slack\" | \"discord\" | \"telegram\";\n /** Subdirs/files in vault dir to mount into sandbox (e.g. [\".gcloud\", \".ssh\", \".kube\"]) */\n mounts?: Array<string | VaultMountEntry>;\n /** Whether to load env file as environment variables (default: true if env file exists) */\n envFile?: boolean;\n /** Per-user sandbox config override */\n sandbox?: {\n type?: \"image\" | \"firecracker\" | \"host\" | \"container\" | \"docker\";\n container?: string;\n image?: string;\n vmId?: string;\n sshUser?: string;\n sshPort?: number;\n };\n}\n\nexport interface ResolvedVaultMount {\n source: string;\n target: string;\n}\n\n/** Resolved vault ready for use at runtime */\nexport interface ResolvedVault {\n userId: string;\n displayName: string;\n /** Absolute path to vault directory */\n dir: string;\n /** Absolute mount specs */\n mounts: ResolvedVaultMount[];\n /** Parsed from env file */\n env: Record<string, string>;\n sandboxOverride?: VaultEntry[\"sandbox\"];\n}\n\nexport interface VaultManager {\n /** Return true when vault.json contains this exact key. */\n hasEntry(key: string): boolean;\n /** Resolve vault for a user; returns undefined when no entry exists. */\n resolve(userId: string): ResolvedVault | undefined;\n /** Get sandbox config with credential injection for a user */\n getSandboxConfig(userId: string, baseConfig: SandboxConfig): SandboxConfig;\n /** List all configured vaults */\n list(): ResolvedVault[];\n /** Re-read vault.json without restart */\n reload(): void;\n /** Check if vault system is enabled (vault.json exists) */\n isEnabled(): boolean;\n /**\n * Add a vault entry and persist to disk.\n * No-op if the key already exists (idempotent).\n */\n addEntry(key: string, entry: VaultEntry): void;\n /**\n * Ensure a vault entry has image sandbox metadata.\n * Creates the entry when missing and upgrades existing entries that lack sandbox.type.\n */\n ensureImageSandboxEntry(key: string, entry: VaultEntry): void;\n /** Merge environment variables into vaults/<key>/env and persist them to disk. */\n upsertEnv(key: string, env: Record<string, string>): void;\n /** Write a private file into vaults/<key>/ and ensure it is mounted into the sandbox. */\n upsertFile(key: string, relativePath: string, content: string, targetPath?: string): void;\n}\n\n// ── parseEnvFile ───────────────────────────────────────────────────────────────\n\n/**\n * Parse a KEY=VALUE env file. Supports:\n * - Lines starting with # are comments\n * - Empty lines are skipped\n * - Values can be quoted with single or double quotes (quotes are stripped)\n * - No variable expansion\n * - The value is everything after the first `=` to end of line (no inline comments)\n */\nexport function parseEnvFile(content: string): Record<string, string> {\n const env: Record<string, string> = {};\n const lines = content.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\").split(\"\\n\");\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith(\"#\")) continue;\n\n const eqIndex = trimmed.indexOf(\"=\");\n if (eqIndex === -1) continue;\n\n const key = trimmed.slice(0, eqIndex).trim();\n if (!key) continue;\n\n let value = trimmed.slice(eqIndex + 1);\n\n // Strip matching quotes\n if (\n (value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))\n ) {\n value = value.slice(1, -1);\n }\n\n env[key] = value;\n }\n\n return env;\n}\n\n// ── FileVaultManager ───────────────────────────────────────────────────────────\n\nexport class FileVaultManager implements VaultManager {\n private config: VaultConfig | null = null;\n private readonly vaultsDir: string;\n private readonly configPath: string;\n\n constructor(stateDir: string) {\n this.vaultsDir = join(stateDir, \"vaults\");\n this.configPath = join(this.vaultsDir, \"vault.json\");\n this.reload();\n }\n\n reload(): void {\n if (!existsSync(this.configPath)) {\n this.config = null;\n return;\n }\n\n try {\n const raw = readFileSync(this.configPath, \"utf-8\");\n const parsed = JSON.parse(raw);\n\n if (\n !parsed ||\n typeof parsed !== \"object\" ||\n !parsed.vaults ||\n typeof parsed.vaults !== \"object\"\n ) {\n console.error(`vault: malformed vault.json — expected { vaults: { ... } }`);\n this.config = null;\n return;\n }\n\n this.config = parsed as VaultConfig;\n this.warnUnsupportedSandboxTypes();\n } catch (err) {\n console.error(`vault: failed to read ${this.configPath}:`, err);\n this.config = null;\n }\n }\n\n /** Warn for legacy or insecure vault sandbox overrides that are no longer allowed. */\n private warnUnsupportedSandboxTypes(): void {\n if (!this.config) return;\n for (const [key, entry] of Object.entries(this.config.vaults)) {\n if (entry.sandbox?.type === \"host\") {\n console.error(\n `vault: \"${key}\" uses sandbox.type=host, which is blocked for credential isolation. ` +\n \"Use sandbox.type=image or sandbox.type=firecracker.\",\n );\n }\n if (entry.sandbox?.type === \"container\" || entry.sandbox?.type === \"docker\") {\n console.error(\n `vault: \"${key}\" uses sandbox.type=${entry.sandbox.type}, which is blocked for credential isolation. ` +\n \"Use sandbox.type=image for per-user containers or sandbox.type=firecracker.\",\n );\n }\n }\n }\n\n isEnabled(): boolean {\n return this.config !== null;\n }\n\n hasEntry(key: string): boolean {\n return !!this.config?.vaults[key];\n }\n\n resolve(userId: string): ResolvedVault | undefined {\n const entry = this.config?.vaults[userId];\n if (!entry) return undefined;\n return this.buildResolved(userId, entry);\n }\n\n getSandboxConfig(userId: string, baseConfig: SandboxConfig): SandboxConfig {\n const vault = this.resolve(userId);\n if (!vault?.sandboxOverride) return baseConfig;\n\n const override = vault.sandboxOverride;\n\n if (override.type === \"image\") {\n if (baseConfig.type !== \"image\") {\n throw new Error(\n `vault \"${userId}\" sets sandbox.type=image, but base sandbox is \"${baseConfig.type}\". ` +\n \"Use --sandbox=image:<image> to enable per-user managed containers.\",\n );\n }\n const container = override.container || `mama-sandbox-${userId}`;\n return { type: \"container\", container };\n }\n\n if (override.type === \"firecracker\") {\n if (!override.vmId) return baseConfig;\n if (baseConfig.type !== \"firecracker\") {\n throw new Error(\n `vault \"${userId}\" sets sandbox.type=firecracker, but base sandbox is \"${baseConfig.type}\". ` +\n \"Use --sandbox=firecracker:<vm-id>:<host-path> so /workspace stays mapped to the real workspace.\",\n );\n }\n return {\n type: \"firecracker\",\n vmId: override.vmId,\n hostPath: baseConfig.hostPath,\n sshUser: override.sshUser,\n sshPort: override.sshPort,\n };\n }\n\n if (override.type === \"host\") {\n throw new Error(\n `vault \"${userId}\" uses sandbox.type=host, which is blocked for credential isolation. ` +\n \"Use sandbox.type=image or sandbox.type=firecracker.\",\n );\n }\n\n if (override.type === \"container\" || override.type === \"docker\") {\n throw new Error(\n `vault \"${userId}\" uses sandbox.type=${override.type}, which is blocked for credential isolation. ` +\n \"Use sandbox.type=image for per-user containers or sandbox.type=firecracker.\",\n );\n }\n\n // No type override — return base config unchanged\n return baseConfig;\n }\n\n list(): ResolvedVault[] {\n if (!this.config) return [];\n\n const results: ResolvedVault[] = [];\n for (const [key, entry] of Object.entries(this.config.vaults)) {\n results.push(this.buildResolved(key, entry));\n }\n return results;\n }\n\n addEntry(key: string, entry: VaultEntry): void {\n if (!this.config) {\n this.config = { vaults: {} };\n }\n // Idempotent: skip if already exists\n if (this.config.vaults[key]) return;\n this.config.vaults[key] = entry;\n this.persistConfig();\n }\n\n ensureImageSandboxEntry(key: string, entry: VaultEntry): void {\n if (entry.sandbox?.type !== \"image\") {\n throw new Error(`vault: ensureImageSandboxEntry requires sandbox.type=image for \"${key}\"`);\n }\n\n if (!this.config) {\n this.config = { vaults: {} };\n }\n\n const existing = this.config.vaults[key];\n if (!existing) {\n this.config.vaults[key] = entry;\n this.persistConfig();\n return;\n }\n\n let nextEntry = existing;\n let changed = false;\n\n if (!existing.platform && entry.platform) {\n nextEntry = { ...nextEntry, platform: entry.platform };\n changed = true;\n }\n\n const existingSandbox = existing.sandbox;\n if (!existingSandbox?.type) {\n nextEntry = { ...nextEntry, sandbox: entry.sandbox };\n changed = true;\n } else if (\n existingSandbox.type === \"image\" &&\n !existingSandbox.container &&\n entry.sandbox.container\n ) {\n nextEntry = {\n ...nextEntry,\n sandbox: { ...existingSandbox, container: entry.sandbox.container },\n };\n changed = true;\n }\n\n if (!changed) {\n return;\n }\n\n this.config.vaults[key] = nextEntry;\n this.persistConfig();\n }\n\n upsertEnv(key: string, env: Record<string, string>): void {\n const dir = join(this.vaultsDir, key);\n const envPath = join(dir, \"env\");\n ensurePrivateDir(this.vaultsDir);\n ensurePrivateDir(dir);\n const existing = existsSync(envPath)\n ? parseEnvFile(readFileSync(envPath, \"utf-8\"))\n : ({} as Record<string, string>);\n const merged = { ...existing, ...env };\n const content =\n Object.entries(merged)\n .sort(([left], [right]) => left.localeCompare(right))\n .map(([envKey, value]) => `${envKey}=${value}`)\n .join(\"\\n\") + \"\\n\";\n atomicWritePrivateFile(envPath, content);\n }\n\n upsertFile(key: string, relativePath: string, content: string, targetPath?: string): void {\n const normalizedPath = normalizeVaultRelativePath(relativePath);\n const normalizedTarget = normalizeVaultTargetPath(targetPath);\n if (!normalizedPath || (targetPath !== undefined && !normalizedTarget)) {\n throw new Error(`vault: invalid relative secret file path for \"${key}\": ${relativePath}`);\n }\n\n const dir = join(this.vaultsDir, key);\n const filePath = join(dir, normalizedPath);\n\n ensurePrivateDir(this.vaultsDir);\n ensurePrivateDir(dir);\n const parentDir = dirname(filePath);\n if (parentDir !== dir) ensurePrivateDir(parentDir);\n atomicWritePrivateFile(filePath, content);\n this.ensureMountEntry(key, normalizedPath, normalizedTarget);\n }\n\n // ── private ────────────────────────────────────────────────────────────────\n\n private persistConfig(): void {\n ensurePrivateDir(this.vaultsDir);\n\n // Preserve concurrent external edits: pull in any entries that appear on\n // disk but not in our in-memory view, so a background edit (e.g. another\n // admin adding a user) is not silently dropped by the next upsert here.\n // Individual field edits still follow last-writer-wins per key.\n const onDisk = this.readConfigFromDisk();\n if (onDisk && this.config) {\n for (const [key, entry] of Object.entries(onDisk.vaults)) {\n if (!(key in this.config.vaults)) {\n this.config.vaults[key] = entry;\n }\n }\n }\n\n atomicWritePrivateFile(this.configPath, JSON.stringify(this.config, null, 2) + \"\\n\");\n }\n\n private readConfigFromDisk(): VaultConfig | null {\n if (!existsSync(this.configPath)) return null;\n try {\n const parsed = JSON.parse(readFileSync(this.configPath, \"utf-8\"));\n if (\n !parsed ||\n typeof parsed !== \"object\" ||\n !parsed.vaults ||\n typeof parsed.vaults !== \"object\"\n ) {\n return null;\n }\n return parsed as VaultConfig;\n } catch {\n return null;\n }\n }\n\n private ensureMountEntry(key: string, relativePath: string, targetPath?: string): void {\n if (!this.config?.vaults[key]) {\n throw new Error(`vault: cannot add mount \"${relativePath}\" for missing entry \"${key}\"`);\n }\n\n const existing = this.config.vaults[key];\n const mounts = existing.mounts ?? [];\n if (\n mounts.some((mount) =>\n typeof mount === \"string\"\n ? mount === relativePath && !targetPath\n : mount.source === relativePath && mount.target === targetPath,\n )\n ) {\n return;\n }\n\n this.config.vaults[key] = {\n ...existing,\n mounts: [...mounts, targetPath ? { source: relativePath, target: targetPath } : relativePath],\n };\n this.persistConfig();\n }\n\n private buildResolved(key: string, entry: VaultEntry): ResolvedVault {\n const dir = join(this.vaultsDir, key);\n\n const mounts = (entry.mounts ?? [])\n .map((mount) => this.resolveMountEntry(dir, mount))\n .filter((mount): mount is ResolvedVaultMount => mount !== undefined);\n\n let env: Record<string, string> = {};\n const envPath = join(dir, \"env\");\n if (entry.envFile !== false && existsSync(envPath)) {\n try {\n env = parseEnvFile(readFileSync(envPath, \"utf-8\"));\n } catch (err) {\n console.error(`vault: failed to parse env file for \"${key}\":`, err);\n }\n }\n\n return {\n userId: key,\n displayName: entry.displayName,\n dir,\n mounts,\n env,\n sandboxOverride: entry.sandbox,\n };\n }\n\n private resolveMountEntry(\n dir: string,\n mount: string | VaultMountEntry,\n ): ResolvedVaultMount | undefined {\n if (typeof mount === \"string\") {\n const normalizedSource = normalizeVaultRelativePath(mount);\n if (!normalizedSource) return undefined;\n return {\n source: join(dir, normalizedSource),\n target: defaultVaultTargetPath(normalizedSource),\n };\n }\n\n if (!mount || typeof mount !== \"object\") return undefined;\n const normalizedSource = normalizeVaultRelativePath(mount.source);\n if (!normalizedSource) return undefined;\n const normalizedTarget = normalizeVaultTargetPath(mount.target);\n return {\n source: join(dir, normalizedSource),\n target: normalizedTarget ?? defaultVaultTargetPath(normalizedSource),\n };\n }\n}\n\nfunction ensurePrivateDir(path: string): void {\n mkdirSync(path, { recursive: true, mode: PRIVATE_DIR_MODE });\n chmodSync(path, PRIVATE_DIR_MODE);\n}\n\n/**\n * Write `content` to `targetPath` with mode 0600, even when `targetPath`\n * already exists. Uses O_CREAT|O_EXCL on a temp sibling (so the kernel\n * guarantees permissions at creation, not after a racy chmod) and then\n * rename(2) into place for atomicity. Readers never see a torn write.\n */\nfunction atomicWritePrivateFile(targetPath: string, content: string): void {\n const dir = dirname(targetPath);\n const tmpPath = join(\n dir,\n `.${basename(targetPath)}.${process.pid}.${randomBytes(8).toString(\"hex\")}.tmp`,\n );\n const fd = openSync(\n tmpPath,\n fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL,\n PRIVATE_FILE_MODE,\n );\n try {\n writeSync(fd, content);\n } catch (err) {\n try {\n unlinkSync(tmpPath);\n } catch {\n // ignore — original error is more informative\n }\n throw err;\n } finally {\n closeSync(fd);\n }\n try {\n renameSync(tmpPath, targetPath);\n } catch (err) {\n try {\n unlinkSync(tmpPath);\n } catch {\n // ignore\n }\n throw err;\n }\n}\n\nfunction normalizeVaultRelativePath(relativePath: string): string | undefined {\n const trimmed = relativePath.trim();\n if (!trimmed || isAbsolute(trimmed)) return undefined;\n\n const normalized = normalize(trimmed).split(sep).join(\"/\");\n if (!normalized || normalized === \".\" || normalized === \"..\" || normalized.startsWith(\"../\")) {\n return undefined;\n }\n return normalized;\n}\n\nfunction normalizeVaultTargetPath(targetPath?: string): string | undefined {\n if (targetPath === undefined) {\n return undefined;\n }\n\n const trimmed = targetPath.trim();\n if (!trimmed || !trimmed.startsWith(\"/\")) {\n return undefined;\n }\n\n const normalized = normalize(trimmed).split(sep).join(\"/\");\n return normalized.startsWith(\"/\") ? normalized : undefined;\n}\n\nexport function defaultVaultTargetPath(relativePath: string): string {\n const normalized = normalizeVaultRelativePath(relativePath) ?? relativePath.replace(/^\\/+/, \"\");\n return `/root/${normalized}`;\n}\n"]}
1
+ {"version":3,"file":"vault.js","sourceRoot":"","sources":["../src/vault.ts"],"names":[],"mappings":"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","sourcesContent":["import { chmodSync, copyFileSync, existsSync, mkdirSync, readdirSync, rmSync } from \"fs\";\nimport { dirname, isAbsolute, join, normalize, sep } from \"path\";\nimport { readTextFileIfExists } from \"./file-guards.js\";\nimport type { SandboxConfig } from \"./sandbox.js\";\nimport { atomicWritePrivateFile } from \"./fs-atomic.js\";\n\nconst PRIVATE_DIR_MODE = 0o700;\nconst SHARED_VAULT_DIR = \"shared\";\n\nexport function normalizeSharedVaultName(name: string): string | undefined {\n const trimmed = name.trim();\n if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(trimmed)) return undefined;\n return trimmed;\n}\n\nexport function sharedVaultKey(name: string): string | undefined {\n const normalized = normalizeSharedVaultName(name);\n return normalized ? `${SHARED_VAULT_DIR}/${normalized}` : undefined;\n}\n\nfunction sanitizeCloudflareSandboxId(value: string): string {\n return (\n value\n .toLowerCase()\n .replace(/[^a-z0-9-]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\") || \"unknown\"\n );\n}\n\n// ── Types ──────────────────────────────────────────────────────────────────────\n\nexport interface ResolvedVaultMount {\n source: string;\n target: string;\n}\n\n/** Resolved vault ready for use at runtime */\nexport interface ResolvedVault {\n userId: string;\n displayName: string;\n /** Absolute path to vault directory */\n dir: string;\n /** Absolute mount specs */\n mounts: ResolvedVaultMount[];\n /** Parsed from env file */\n env: Record<string, string>;\n}\n\nexport interface VaultManager {\n /** Return true when a vault directory exists for this exact key. */\n hasEntry(key: string): boolean;\n /** Resolve vault for a user; returns undefined when no directory exists. */\n resolve(userId: string): ResolvedVault | undefined;\n /** Get sandbox config with credential injection for a user */\n getSandboxConfig(userId: string, baseConfig: SandboxConfig): SandboxConfig;\n /** List all vaults discovered under vaults/. */\n list(): ResolvedVault[];\n /** Check if the vaults directory exists. */\n isEnabled(): boolean;\n /** Merge environment variables into vaults/<key>/env and persist them to disk. */\n upsertEnv(key: string, env: Record<string, string>): void;\n /** Write a private file into vaults/<key>/ and ensure it is mounted into the sandbox. */\n upsertFile(key: string, relativePath: string, content: string, targetPath?: string): void;\n /** List named shared login profiles under vaults/shared/. */\n listSharedVaults(): string[];\n /** Delete a shared login profile's directory. Returns true when it existed. */\n deleteSharedVault(name: string): boolean;\n /** Copy a shared login profile's files into another vault directory. */\n copySharedVaultTo(\n name: string,\n targetKey: string,\n ): { filesCopied: number; envKeysCopied: number };\n}\n\n// ── parseEnvFile ───────────────────────────────────────────────────────────────\n\n/**\n * Parse a KEY=VALUE env file. Supports:\n * - Lines starting with # are comments\n * - Empty lines are skipped\n * - Values can be quoted with single or double quotes (quotes are stripped)\n * - No variable expansion\n * - The value is everything after the first `=` to end of line (no inline comments)\n */\nexport function parseEnvFile(content: string): Record<string, string> {\n const env: Record<string, string> = {};\n const lines = content.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\").split(\"\\n\");\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith(\"#\")) continue;\n\n const eqIndex = trimmed.indexOf(\"=\");\n if (eqIndex === -1) continue;\n\n const key = trimmed.slice(0, eqIndex).trim();\n if (!key) continue;\n\n let value = trimmed.slice(eqIndex + 1);\n\n if (\n (value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))\n ) {\n value = value.slice(1, -1);\n }\n\n env[key] = value;\n }\n\n return env;\n}\n\n// ── FileVaultManager ───────────────────────────────────────────────────────────\n\nexport class FileVaultManager implements VaultManager {\n private readonly vaultsDir: string;\n\n constructor(stateDir: string) {\n this.vaultsDir = join(stateDir, \"vaults\");\n }\n\n isEnabled(): boolean {\n return existsSync(this.vaultsDir);\n }\n\n hasEntry(key: string): boolean {\n return existsSync(join(this.vaultsDir, key));\n }\n\n listSharedVaults(): string[] {\n const sharedDir = join(this.vaultsDir, SHARED_VAULT_DIR);\n if (!existsSync(sharedDir)) return [];\n return readdirSync(sharedDir, { withFileTypes: true })\n .filter((entry) => entry.isDirectory() && normalizeSharedVaultName(entry.name) === entry.name)\n .map((entry) => entry.name)\n .toSorted((left, right) => left.localeCompare(right));\n }\n\n deleteSharedVault(name: string): boolean {\n const key = sharedVaultKey(name);\n if (!key) throw new Error(`vault: invalid shared login name: ${name}`);\n const dir = join(this.vaultsDir, key);\n const existed = existsSync(dir);\n rmSync(dir, { recursive: true, force: true });\n return existed;\n }\n\n copySharedVaultTo(\n name: string,\n targetKey: string,\n ): { filesCopied: number; envKeysCopied: number } {\n const sourceKey = sharedVaultKey(name);\n if (!sourceKey) throw new Error(`vault: invalid shared login name: ${name}`);\n const sourceDir = join(this.vaultsDir, sourceKey);\n if (!existsSync(sourceDir)) throw new Error(`vault: shared login \"${name}\" does not exist`);\n\n const targetDir = join(this.vaultsDir, targetKey);\n ensurePrivateDir(this.vaultsDir);\n ensurePrivateDir(targetDir);\n return copyVaultDir(sourceDir, targetDir);\n }\n\n resolve(userId: string): ResolvedVault | undefined {\n const dir = join(this.vaultsDir, userId);\n if (!existsSync(dir)) return undefined;\n return this.buildResolved(userId);\n }\n\n getSandboxConfig(userId: string, baseConfig: SandboxConfig): SandboxConfig {\n if (baseConfig.type === \"cloudflare\") {\n return {\n type: \"cloudflare\",\n sandboxId: `${baseConfig.sandboxId}-${sanitizeCloudflareSandboxId(userId)}`,\n };\n }\n return baseConfig;\n }\n\n list(): ResolvedVault[] {\n if (!existsSync(this.vaultsDir)) return [];\n const keys = new Set<string>();\n for (const entry of readdirSync(this.vaultsDir, { withFileTypes: true })) {\n if (entry.isDirectory()) keys.add(entry.name);\n }\n return Array.from(keys, (key) => this.buildResolved(key));\n }\n\n upsertEnv(key: string, env: Record<string, string>): void {\n const dir = join(this.vaultsDir, key);\n const envPath = join(dir, \"env\");\n ensurePrivateDir(this.vaultsDir);\n ensurePrivateDir(dir);\n const existingContent = readTextFileIfExists(envPath);\n const existing = existingContent ? parseEnvFile(existingContent) : {};\n const merged = { ...existing, ...env };\n const content =\n Object.entries(merged)\n .toSorted(([left], [right]) => left.localeCompare(right))\n .map(([envKey, value]) => `${envKey}=${value}`)\n .join(\"\\n\") + \"\\n\";\n atomicWritePrivateFile(envPath, content);\n }\n\n upsertFile(key: string, relativePath: string, content: string, targetPath?: string): void {\n const normalizedPath = normalizeVaultRelativePath(relativePath);\n if (!normalizedPath || (targetPath !== undefined && !normalizeVaultTargetPath(targetPath))) {\n throw new Error(`vault: invalid relative secret file path for \"${key}\": ${relativePath}`);\n }\n\n const dir = join(this.vaultsDir, key);\n const filePath = join(dir, normalizedPath);\n\n ensurePrivateDir(this.vaultsDir);\n ensurePrivateDir(dir);\n const parentDir = dirname(filePath);\n if (parentDir !== dir) ensurePrivateDir(parentDir);\n atomicWritePrivateFile(filePath, content);\n }\n\n // ── private ────────────────────────────────────────────────────────────────\n\n private buildResolved(key: string): ResolvedVault {\n const dir = join(this.vaultsDir, key);\n const mounts = inferMountsFromDir(dir);\n\n let env: Record<string, string> = {};\n const envContent = readTextFileIfExists(join(dir, \"env\"));\n if (envContent !== undefined) {\n try {\n env = parseEnvFile(envContent);\n } catch (err) {\n console.error(`vault: failed to parse env file for \"${key}\":`, err);\n }\n }\n\n return {\n userId: key,\n displayName: key,\n dir,\n mounts,\n env,\n };\n }\n}\n\nfunction inferMountsFromDir(dir: string): ResolvedVaultMount[] {\n if (!existsSync(dir)) return [];\n\n const mounts: ResolvedVaultMount[] = [];\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (entry.name === \"env\") continue;\n const source = join(dir, entry.name);\n const target = inferredVaultTargetPath(entry.name);\n if (!target) continue;\n mounts.push({ source, target });\n }\n return mounts;\n}\n\nfunction ensurePrivateDir(path: string): void {\n mkdirSync(path, { recursive: true, mode: PRIVATE_DIR_MODE });\n chmodSync(path, PRIVATE_DIR_MODE);\n}\n\nfunction copyVaultDir(\n sourceDir: string,\n targetDir: string,\n): {\n filesCopied: number;\n envKeysCopied: number;\n} {\n let filesCopied = 0;\n let envKeysCopied = 0;\n\n for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {\n const sourcePath = join(sourceDir, entry.name);\n const targetPath = join(targetDir, entry.name);\n\n if (entry.name === \"env\" && entry.isFile()) {\n const sourceEnv = parseEnvFile(readTextFileIfExists(sourcePath) ?? \"\");\n const targetEnv = parseEnvFile(readTextFileIfExists(targetPath) ?? \"\");\n const merged = { ...targetEnv, ...sourceEnv };\n const content =\n Object.entries(merged)\n .toSorted(([left], [right]) => left.localeCompare(right))\n .map(([envKey, value]) => `${envKey}=${value}`)\n .join(\"\\n\") + \"\\n\";\n atomicWritePrivateFile(targetPath, content);\n envKeysCopied += Object.keys(sourceEnv).length;\n continue;\n }\n\n if (entry.isDirectory()) {\n ensurePrivateDir(targetPath);\n const nested = copyVaultDir(sourcePath, targetPath);\n filesCopied += nested.filesCopied;\n envKeysCopied += nested.envKeysCopied;\n continue;\n }\n\n if (!entry.isFile()) continue;\n copyFileSync(sourcePath, targetPath);\n chmodSync(targetPath, 0o600);\n filesCopied++;\n }\n\n return { filesCopied, envKeysCopied };\n}\n\nfunction normalizeVaultRelativePath(relativePath: string): string | undefined {\n const trimmed = relativePath.trim();\n if (!trimmed || isAbsolute(trimmed)) return undefined;\n\n const normalized = normalize(trimmed).split(sep).join(\"/\");\n if (!normalized || normalized === \".\" || normalized === \"..\" || normalized.startsWith(\"../\")) {\n return undefined;\n }\n return normalized;\n}\n\nfunction normalizeVaultTargetPath(targetPath?: string): string | undefined {\n if (targetPath === undefined) return undefined;\n\n const trimmed = targetPath.trim();\n if (!trimmed || !trimmed.startsWith(\"/\")) return undefined;\n\n const normalized = normalize(trimmed).split(sep).join(\"/\");\n return normalized.startsWith(\"/\") ? normalized : undefined;\n}\n\nexport function defaultVaultTargetPath(relativePath: string): string {\n const normalized = normalizeVaultRelativePath(relativePath) ?? relativePath.replace(/^\\/+/, \"\");\n return `/root/${normalized}`;\n}\n\nfunction inferredVaultTargetPath(relativePath: string): string | undefined {\n const normalized = normalizeVaultRelativePath(relativePath);\n if (!normalized) return undefined;\n\n if (normalized === \"gws.json\") {\n return \"/root/.config/gws/credentials.json\";\n }\n if (normalized === \".ssh\" || normalized.startsWith(\".ssh/\")) {\n return \"/root/.ssh\";\n }\n if (normalized === \".kube\" || normalized.startsWith(\".kube/\")) {\n return \"/root/.kube\";\n }\n if (normalized === \".config/gh\" || normalized.startsWith(\".config/gh/\")) {\n return \"/root/.config/gh\";\n }\n\n return defaultVaultTargetPath(normalized);\n}\n"]}