@brainmcp/brainmcp 0.1.12 → 0.1.14

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 (108) hide show
  1. package/README.md +79 -5
  2. package/dist/agent-config.js +155 -24
  3. package/dist/commands/auth.d.ts +47 -0
  4. package/dist/commands/auth.js +392 -0
  5. package/dist/commands/auth.js.map +1 -0
  6. package/dist/commands/capture.d.ts +34 -0
  7. package/dist/commands/capture.js +349 -0
  8. package/dist/commands/capture.js.map +1 -0
  9. package/dist/commands/dispatch.d.ts +43 -0
  10. package/dist/commands/dispatch.js +287 -0
  11. package/dist/commands/dispatch.js.map +1 -0
  12. package/dist/commands/index.d.ts +4 -0
  13. package/dist/commands/index.js +198 -36
  14. package/dist/commands/index.js.map +1 -1
  15. package/dist/hooks/adapters.d.ts +14 -0
  16. package/dist/hooks/adapters.js +41 -0
  17. package/dist/hooks/adapters.js.map +1 -0
  18. package/dist/hooks/claude.d.ts +24 -0
  19. package/dist/hooks/claude.js +36 -0
  20. package/dist/hooks/claude.js.map +1 -0
  21. package/dist/hooks/codex.d.ts +18 -0
  22. package/dist/hooks/codex.js +36 -0
  23. package/dist/hooks/codex.js.map +1 -0
  24. package/dist/hooks/cursor.d.ts +22 -0
  25. package/dist/hooks/cursor.js +26 -0
  26. package/dist/hooks/cursor.js.map +1 -0
  27. package/dist/hooks/managed-hooks.d.ts +30 -0
  28. package/dist/hooks/managed-hooks.js +189 -0
  29. package/dist/hooks/managed-hooks.js.map +1 -0
  30. package/dist/hooks/vscode.d.ts +22 -0
  31. package/dist/hooks/vscode.js +27 -0
  32. package/dist/hooks/vscode.js.map +1 -0
  33. package/dist/index.js +7 -102
  34. package/dist/index.js.map +1 -1
  35. package/dist/lib/api-client.d.ts +32 -0
  36. package/dist/lib/api-client.js +207 -0
  37. package/dist/lib/api-client.js.map +1 -0
  38. package/dist/lib/callback-server.d.ts +17 -0
  39. package/dist/lib/callback-server.js +145 -0
  40. package/dist/lib/callback-server.js.map +1 -0
  41. package/dist/lib/cli-contracts.d.ts +193 -0
  42. package/dist/lib/cli-contracts.js +242 -0
  43. package/dist/lib/cli-contracts.js.map +1 -0
  44. package/dist/lib/config.d.ts +2 -0
  45. package/dist/lib/config.js +11 -0
  46. package/dist/lib/config.js.map +1 -1
  47. package/dist/lib/credentials.d.ts +89 -0
  48. package/dist/lib/credentials.js +304 -0
  49. package/dist/lib/credentials.js.map +1 -0
  50. package/dist/lib/endpoints.d.ts +5 -0
  51. package/dist/lib/endpoints.js +31 -0
  52. package/dist/lib/endpoints.js.map +1 -0
  53. package/dist/lib/http.d.ts +3 -0
  54. package/dist/lib/http.js +51 -0
  55. package/dist/lib/http.js.map +1 -0
  56. package/dist/lib/managed.d.ts +8 -0
  57. package/dist/lib/managed.js +72 -6
  58. package/dist/lib/managed.js.map +1 -1
  59. package/dist/lib/oauth-client.d.ts +55 -0
  60. package/dist/lib/oauth-client.js +155 -0
  61. package/dist/lib/oauth-client.js.map +1 -0
  62. package/dist/lib/pkce.d.ts +14 -0
  63. package/dist/lib/pkce.js +33 -0
  64. package/dist/lib/pkce.js.map +1 -0
  65. package/dist/lib/vault.d.ts +26 -0
  66. package/dist/lib/vault.js +221 -0
  67. package/dist/lib/vault.js.map +1 -0
  68. package/dist/runtime/brainmcp-hook-runtime.mjs +3243 -0
  69. package/dist/runtime/capture.d.ts +83 -0
  70. package/dist/runtime/capture.js +332 -0
  71. package/dist/runtime/capture.js.map +1 -0
  72. package/dist/runtime/diagnostics.d.ts +7 -0
  73. package/dist/runtime/diagnostics.js +75 -0
  74. package/dist/runtime/diagnostics.js.map +1 -0
  75. package/dist/runtime/doctor-checks.d.ts +15 -0
  76. package/dist/runtime/doctor-checks.js +233 -0
  77. package/dist/runtime/doctor-checks.js.map +1 -0
  78. package/dist/runtime/flush.d.ts +30 -0
  79. package/dist/runtime/flush.js +193 -0
  80. package/dist/runtime/flush.js.map +1 -0
  81. package/dist/runtime/hook-dispatcher.d.ts +31 -0
  82. package/dist/runtime/hook-dispatcher.js +381 -0
  83. package/dist/runtime/hook-dispatcher.js.map +1 -0
  84. package/dist/runtime/index.d.ts +12 -0
  85. package/dist/runtime/index.js +69 -0
  86. package/dist/runtime/index.js.map +1 -0
  87. package/dist/runtime/installer.d.ts +26 -0
  88. package/dist/runtime/installer.js +134 -0
  89. package/dist/runtime/installer.js.map +1 -0
  90. package/dist/runtime/lock.d.ts +19 -0
  91. package/dist/runtime/lock.js +116 -0
  92. package/dist/runtime/lock.js.map +1 -0
  93. package/dist/runtime/orient.d.ts +55 -0
  94. package/dist/runtime/orient.js +256 -0
  95. package/dist/runtime/orient.js.map +1 -0
  96. package/dist/runtime/queue.d.ts +7 -0
  97. package/dist/runtime/queue.js +80 -0
  98. package/dist/runtime/queue.js.map +1 -0
  99. package/dist/runtime/retention.d.ts +26 -0
  100. package/dist/runtime/retention.js +175 -0
  101. package/dist/runtime/retention.js.map +1 -0
  102. package/dist/runtime/session.d.ts +68 -0
  103. package/dist/runtime/session.js +283 -0
  104. package/dist/runtime/session.js.map +1 -0
  105. package/dist/runtime/storage.d.ts +32 -0
  106. package/dist/runtime/storage.js +236 -0
  107. package/dist/runtime/storage.js.map +1 -0
  108. package/package.json +3 -2
@@ -0,0 +1,304 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { withSessionLock } from '../runtime/lock.js';
3
+ import { closeSync, constants, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from 'node:fs';
4
+ import { dirname, join } from 'node:path';
5
+ import { assertNotSymlink, ensureDirectory0700, getBrainHome, getCurrentAccountId, resolveStateDir, writeAtomic0600 } from '../runtime/storage.js';
6
+ import { createPlatformVault, UnavailableSecretVault, vaultAccountKey, LEGACY_VAULT_ACCOUNT } from './vault.js';
7
+ export class MemoryCredentialStore {
8
+ creds = null;
9
+ locked = false;
10
+ async get() {
11
+ return this.creds ? JSON.parse(JSON.stringify(this.creds)) : null;
12
+ }
13
+ async set(credentials) {
14
+ this.creds = JSON.parse(JSON.stringify(credentials));
15
+ }
16
+ async clear() {
17
+ this.creds = null;
18
+ }
19
+ async adoptLegacySecret() { }
20
+ async withRefreshLock(action) {
21
+ while (this.locked) {
22
+ await new Promise((r) => setTimeout(r, 20));
23
+ }
24
+ this.locked = true;
25
+ try {
26
+ return await action(await this.get());
27
+ }
28
+ finally {
29
+ this.locked = false;
30
+ }
31
+ }
32
+ }
33
+ export class FileCredentialStore {
34
+ lockContext = new AsyncLocalStorage();
35
+ baseDir;
36
+ warn;
37
+ allowInsecure;
38
+ constructor(options = {}) {
39
+ this.baseDir = options.baseDir ?? getBrainHome();
40
+ this.warn = options.warn ?? ((msg) => console.warn(`[brainmcp credentials] ${msg}`));
41
+ this.allowInsecure = options.allowInsecureModesForTesting ?? false;
42
+ }
43
+ credentialPaths() {
44
+ const stateDir = this.lockContext.getStore() ?? resolveStateDir(this.baseDir);
45
+ return {
46
+ stateDir,
47
+ filePath: join(stateDir, 'credentials.json'),
48
+ lockPath: join(stateDir, 'credentials.lock'),
49
+ };
50
+ }
51
+ assertSafePath(targetPath, checkFileExists = true) {
52
+ if (this.allowInsecure)
53
+ return;
54
+ // Check directory
55
+ const dir = dirname(targetPath);
56
+ if (existsSync(dir)) {
57
+ const dirStat = lstatSync(dir);
58
+ if (dirStat.isSymbolicLink()) {
59
+ throw new Error(`Insecure configuration: directory ${dir} is a symbolic link.`);
60
+ }
61
+ if (process.platform !== 'win32') {
62
+ const mode = dirStat.mode & 0o777;
63
+ if (mode !== 0o700) {
64
+ throw new Error(`Insecure permissions on ${dir}: permissions are ${mode.toString(8)}, must be 700.`);
65
+ }
66
+ if (process.getuid && dirStat.uid !== process.getuid()) {
67
+ throw new Error(`Insecure ownership: directory ${dir} is not owned by current user.`);
68
+ }
69
+ }
70
+ }
71
+ // Check target file if it exists
72
+ if (existsSync(targetPath)) {
73
+ const fileStat = lstatSync(targetPath);
74
+ if (fileStat.isSymbolicLink()) {
75
+ throw new Error(`Insecure configuration: file ${targetPath} is a symbolic link.`);
76
+ }
77
+ if (!fileStat.isFile()) {
78
+ throw new Error(`Insecure configuration: ${targetPath} is not a regular file.`);
79
+ }
80
+ if (process.platform !== 'win32') {
81
+ const mode = fileStat.mode & 0o777;
82
+ if (mode !== 0o600) {
83
+ throw new Error(`Insecure permissions on ${targetPath}: permissions are ${mode.toString(8)}, must be 600.`);
84
+ }
85
+ if (process.getuid && fileStat.uid !== process.getuid()) {
86
+ throw new Error(`Insecure ownership: file ${targetPath} is not owned by current user.`);
87
+ }
88
+ }
89
+ }
90
+ else if (checkFileExists) {
91
+ // File doesn't exist yet, which is fine
92
+ }
93
+ }
94
+ ensureDirectory() {
95
+ if (!existsSync(this.baseDir)) {
96
+ mkdirSync(this.baseDir, { recursive: true, mode: 0o700 });
97
+ }
98
+ else if (!this.allowInsecure) {
99
+ const homeStat = lstatSync(this.baseDir);
100
+ if (homeStat.isSymbolicLink()) {
101
+ throw new Error(`Insecure configuration: directory ${this.baseDir} is a symbolic link.`);
102
+ }
103
+ }
104
+ const paths = this.credentialPaths();
105
+ ensureDirectory0700(paths.stateDir);
106
+ this.assertSafePath(paths.filePath, false);
107
+ return paths;
108
+ }
109
+ async get() {
110
+ const { filePath } = this.credentialPaths();
111
+ if (!existsSync(filePath)) {
112
+ return null;
113
+ }
114
+ this.assertSafePath(filePath);
115
+ try {
116
+ const raw = readFileSync(filePath, 'utf-8');
117
+ const parsed = JSON.parse(raw);
118
+ if (parsed.version !== 1) {
119
+ this.warn(`Unsupported credentials schema version: ${parsed.version}`);
120
+ return null;
121
+ }
122
+ return parsed;
123
+ }
124
+ catch (error) {
125
+ this.warn(`Failed to read credentials: ${error instanceof Error ? error.message : String(error)}`);
126
+ return null;
127
+ }
128
+ }
129
+ async set(credentials) {
130
+ const { filePath, stateDir } = this.ensureDirectory();
131
+ const tempFile = join(stateDir, `credentials.tmp.${process.pid}.${Math.random().toString(36).slice(2, 8)}`);
132
+ const data = JSON.stringify(credentials, null, 2);
133
+ // Write to temp file with mode 0600
134
+ try {
135
+ const fd = openSync(tempFile, constants.O_CREAT |
136
+ constants.O_EXCL |
137
+ constants.O_WRONLY |
138
+ (typeof constants.O_NOFOLLOW === 'number' ? constants.O_NOFOLLOW : 0), 0o600);
139
+ try {
140
+ writeFileSync(fd, data, 'utf-8');
141
+ fsyncSync(fd);
142
+ }
143
+ finally {
144
+ closeSync(fd);
145
+ }
146
+ this.assertSafePath(tempFile);
147
+ // Atomically rename
148
+ renameSync(tempFile, filePath);
149
+ this.assertSafePath(filePath);
150
+ }
151
+ catch (error) {
152
+ try {
153
+ if (existsSync(tempFile))
154
+ unlinkSync(tempFile);
155
+ }
156
+ catch {
157
+ // Best-effort cleanup.
158
+ }
159
+ throw error;
160
+ }
161
+ }
162
+ async clear() {
163
+ const { filePath } = this.credentialPaths();
164
+ if (existsSync(filePath)) {
165
+ this.assertSafePath(filePath);
166
+ unlinkSync(filePath);
167
+ }
168
+ }
169
+ async adoptLegacySecret() { }
170
+ async withRefreshLock(action) {
171
+ const stateDir = this.credentialPaths().stateDir;
172
+ return withSessionLock('credentials', () => this.lockContext.run(stateDir, async () => action(await this.get())), { baseDir: stateDir });
173
+ }
174
+ }
175
+ export class VaultCredentialStore {
176
+ vault;
177
+ lockStore;
178
+ baseDir;
179
+ lockContext = new AsyncLocalStorage();
180
+ constructor(vault, lockStore, baseDir) {
181
+ this.vault = vault;
182
+ this.lockStore = lockStore;
183
+ this.baseDir = baseDir;
184
+ }
185
+ get backend() {
186
+ return 'os_vault';
187
+ }
188
+ backendMarkerPath() {
189
+ return join(this.lockContext.getStore()?.stateDir ?? resolveStateDir(this.baseDir ?? getBrainHome()), 'credentials.backend.json');
190
+ }
191
+ secretKey() {
192
+ if (this.lockContext.getStore())
193
+ return this.lockContext.getStore().key;
194
+ const accountId = getCurrentAccountId(this.baseDir ?? getBrainHome());
195
+ return accountId ? vaultAccountKey(accountId) : LEGACY_VAULT_ACCOUNT;
196
+ }
197
+ async get() {
198
+ // A fresh/isolated Brain home must not probe or adopt another home's
199
+ // machine-wide vault entry merely because it uses the same account key.
200
+ if (!existsSync(this.backendMarkerPath()))
201
+ return null;
202
+ assertNotSymlink(this.backendMarkerPath());
203
+ const raw = await this.vault.getSecret(this.secretKey());
204
+ if (!raw)
205
+ return null;
206
+ try {
207
+ const parsed = JSON.parse(raw);
208
+ if (parsed.version !== 1)
209
+ return null;
210
+ return parsed;
211
+ }
212
+ catch {
213
+ return null;
214
+ }
215
+ }
216
+ async set(credentials) {
217
+ const marker = this.backendMarkerPath();
218
+ await this.vault.setSecret(JSON.stringify(credentials), this.secretKey());
219
+ ensureDirectory0700(dirname(marker));
220
+ writeAtomic0600(marker, JSON.stringify({ backend: 'os_vault', updatedAt: new Date().toISOString() }));
221
+ }
222
+ async clear() {
223
+ const marker = this.backendMarkerPath();
224
+ await this.vault.deleteSecret(this.secretKey());
225
+ if (existsSync(marker)) {
226
+ assertNotSymlink(marker);
227
+ unlinkSync(marker);
228
+ }
229
+ }
230
+ async adoptLegacySecret() {
231
+ const accountId = getCurrentAccountId(this.baseDir ?? getBrainHome());
232
+ if (!accountId)
233
+ return;
234
+ const namespaced = vaultAccountKey(accountId);
235
+ if (await this.vault.getSecret(namespaced))
236
+ return;
237
+ const legacy = await this.vault.getSecret(LEGACY_VAULT_ACCOUNT);
238
+ if (!legacy)
239
+ return;
240
+ await this.vault.setSecret(legacy, namespaced);
241
+ await this.vault.deleteSecret(LEGACY_VAULT_ACCOUNT);
242
+ writeAtomic0600(this.backendMarkerPath(), JSON.stringify({ backend: 'os_vault', updatedAt: new Date().toISOString() }));
243
+ }
244
+ async withRefreshLock(action) {
245
+ const context = { key: this.secretKey(), stateDir: resolveStateDir(this.baseDir ?? getBrainHome()) };
246
+ return this.lockStore.withRefreshLock(() => this.lockContext.run(context, async () => action(await this.get())));
247
+ }
248
+ }
249
+ export const FILE_CREDENTIALS_ACK_FILENAME = 'allow-file-credentials';
250
+ export function fileCredentialsAcknowledged(baseDir) {
251
+ if (process.env.BRAINMCP_ALLOW_FILE_CREDENTIALS === '1')
252
+ return true;
253
+ const home = baseDir ?? getBrainHome();
254
+ const acknowledgement = join(home, FILE_CREDENTIALS_ACK_FILENAME);
255
+ if (!existsSync(acknowledgement))
256
+ return false;
257
+ const stat = lstatSync(acknowledgement);
258
+ if (stat.isSymbolicLink() || !stat.isFile())
259
+ return false;
260
+ if (process.platform !== 'win32') {
261
+ if ((stat.mode & 0o777) !== 0o600)
262
+ return false;
263
+ if (process.getuid && stat.uid !== process.getuid())
264
+ return false;
265
+ }
266
+ return true;
267
+ }
268
+ export function writeFileCredentialsAcknowledgement(baseDir) {
269
+ const home = baseDir ?? getBrainHome();
270
+ ensureDirectory0700(home);
271
+ writeAtomic0600(join(home, FILE_CREDENTIALS_ACK_FILENAME), JSON.stringify({ acknowledgedAt: new Date().toISOString() }));
272
+ }
273
+ export async function createDefaultCredentialStore(options = {}) {
274
+ const warn = options.warn ?? ((msg) => console.warn(`[brainmcp credentials] ${msg}`));
275
+ const fileStore = new FileCredentialStore({
276
+ baseDir: options.baseDir,
277
+ warn,
278
+ });
279
+ const vault = options.vault ??
280
+ (options.baseDir ? new UnavailableSecretVault() : createPlatformVault());
281
+ const home = options.baseDir ?? getBrainHome();
282
+ const existingFile = existsSync(join(resolveStateDir(home), 'credentials.json'));
283
+ // An acknowledged file login remains usable when the OS vault later becomes
284
+ // available. Only the active account's file determines that backend.
285
+ if (existingFile)
286
+ return { store: fileStore, backend: 'file' };
287
+ if (await vault.isAvailable()) {
288
+ return { store: new VaultCredentialStore(vault, fileStore, options.baseDir), backend: 'os_vault' };
289
+ }
290
+ const allowFile = options.allowFileCredentials === true ||
291
+ existingFile ||
292
+ fileCredentialsAcknowledged(options.baseDir);
293
+ if (!allowFile) {
294
+ throw new Error('OS credential vault is unavailable. Re-run `brainmcp login --allow-file-credentials` to store tokens in an owner-only ~/.brainmcp file (0700/0600, no symlinks).');
295
+ }
296
+ if (!existingFile) {
297
+ warn('OS credential vault is unavailable; using the owner-only ~/.brainmcp file store. This is an explicit fallback, not the default.');
298
+ }
299
+ if (options.persistFileAcknowledgement) {
300
+ writeFileCredentialsAcknowledgement(options.baseDir);
301
+ }
302
+ return { store: fileStore, backend: 'file' };
303
+ }
304
+ //# sourceMappingURL=credentials.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credentials.js","sourceRoot":"","sources":["../../src/lib/credentials.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export declare const COMPANION_WORKSPACES_PATH = "/api/mcp/workspaces";
2
+ export declare function resolveCompanionApiUrl(env?: NodeJS.Dict<string | undefined>): string;
3
+ export declare function resolveCompanionOAuthIssuer(env?: NodeJS.Dict<string | undefined>): string;
4
+ export declare function describeMisdirectedCompanionApi(apiUrl: string): string | null;
5
+ export declare function companionWorkspacesUrl(apiUrl?: string): string;
@@ -0,0 +1,31 @@
1
+ import { BRAIN_API_ORIGIN, BRAIN_MCP_ORIGIN } from '../agent-config.js';
2
+ import { validateHttpUrl } from './http.js';
3
+ export const COMPANION_WORKSPACES_PATH = '/api/mcp/workspaces';
4
+ const MCP_REST_HOSTS = new Set(['mcp.brainmcp.ai', 'dash.brainmcp.ai']);
5
+ function absoluteHttpUrl(value) {
6
+ const url = validateHttpUrl(value.replace(/\/+$/, ''));
7
+ const path = url.pathname === '/' ? '' : url.pathname.replace(/\/+$/, '');
8
+ return `${url.origin}${path}`;
9
+ }
10
+ export function resolveCompanionApiUrl(env = process.env) {
11
+ return absoluteHttpUrl(env.BRAINMCP_API_URL?.trim() || BRAIN_API_ORIGIN);
12
+ }
13
+ export function resolveCompanionOAuthIssuer(env = process.env) {
14
+ return absoluteHttpUrl(env.BRAINMCP_OAUTH_ISSUER?.trim() || BRAIN_MCP_ORIGIN);
15
+ }
16
+ export function describeMisdirectedCompanionApi(apiUrl) {
17
+ let hostname;
18
+ try {
19
+ hostname = new URL(apiUrl).hostname;
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ if (!MCP_REST_HOSTS.has(hostname))
25
+ return null;
26
+ return `Companion REST must use ${BRAIN_API_ORIGIN}, not ${apiUrl}. Unset BRAINMCP_API_URL or point it at the API origin.`;
27
+ }
28
+ export function companionWorkspacesUrl(apiUrl = resolveCompanionApiUrl()) {
29
+ return new URL(COMPANION_WORKSPACES_PATH, `${absoluteHttpUrl(apiUrl)}/`).toString();
30
+ }
31
+ //# sourceMappingURL=endpoints.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"endpoints.js","sourceRoot":"","sources":["../../src/lib/endpoints.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC3E,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAE5C,MAAM,CAAC,MAAM,yBAAyB,GAAG,qBAAqB,CAAC;AAE/D,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,CAAC,iBAAiB,EAAE,kBAAkB,CAAC,CAAC,CAAC;AAExE,SAAS,eAAe,CAAC,KAAa;IACpC,MAAM,GAAG,GAAG,eAAe,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC;IACvD,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,KAAK,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAC1E,OAAO,GAAG,GAAG,CAAC,MAAM,GAAG,IAAI,EAAE,CAAC;AAChC,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,MAAuC,OAAO,CAAC,GAAG;IACvF,OAAO,eAAe,CAAC,GAAG,CAAC,gBAAgB,EAAE,IAAI,EAAE,IAAI,gBAAgB,CAAC,CAAC;AAC3E,CAAC;AAED,MAAM,UAAU,2BAA2B,CAAC,MAAuC,OAAO,CAAC,GAAG;IAC5F,OAAO,eAAe,CAAC,GAAG,CAAC,qBAAqB,EAAE,IAAI,EAAE,IAAI,gBAAgB,CAAC,CAAC;AAChF,CAAC;AAED,MAAM,UAAU,+BAA+B,CAAC,MAAc;IAC5D,IAAI,QAAgB,CAAC;IACrB,IAAI,CAAC;QACH,QAAQ,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC;IACtC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAC;IAC/C,OAAO,2BAA2B,gBAAgB,SAAS,MAAM,yDAAyD,CAAC;AAC7H,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,SAAiB,sBAAsB,EAAE;IAC9E,OAAO,IAAI,GAAG,CAAC,yBAAyB,EAAE,GAAG,eAAe,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,CAAC;AACtF,CAAC"}
@@ -0,0 +1,3 @@
1
+ /** Keep deadlines active through response-body consumption as well as headers. */
2
+ export declare function withRequestDeadline<T>(timeoutMs: number, signal: AbortSignal | undefined, action: (signal: AbortSignal) => Promise<T>): Promise<T>;
3
+ export declare function validateHttpUrl(value: string, trustedBase?: string): URL;
@@ -0,0 +1,51 @@
1
+ import { BRAIN_API_ORIGIN, BRAIN_DASHBOARD_URL, BRAIN_MCP_ORIGIN } from '../agent-config.js';
2
+ /** Keep deadlines active through response-body consumption as well as headers. */
3
+ export async function withRequestDeadline(timeoutMs, signal, action) {
4
+ const controller = new AbortController();
5
+ const abort = () => controller.abort(signal?.reason);
6
+ signal?.addEventListener('abort', abort, { once: true });
7
+ if (signal?.aborted)
8
+ abort();
9
+ const timer = setTimeout(() => controller.abort(new Error('Request timed out')), timeoutMs);
10
+ let onAbort;
11
+ try {
12
+ controller.signal.throwIfAborted();
13
+ const cancelled = new Promise((_, reject) => {
14
+ onAbort = () => reject(new Error('Request cancelled or timed out'));
15
+ controller.signal.addEventListener('abort', onAbort, { once: true });
16
+ });
17
+ return await Promise.race([action(controller.signal), cancelled]);
18
+ }
19
+ finally {
20
+ clearTimeout(timer);
21
+ signal?.removeEventListener('abort', abort);
22
+ if (onAbort)
23
+ controller.signal.removeEventListener('abort', onAbort);
24
+ }
25
+ }
26
+ const BRAIN_ORIGINS = new Set([
27
+ BRAIN_MCP_ORIGIN,
28
+ BRAIN_API_ORIGIN,
29
+ new URL(BRAIN_DASHBOARD_URL).origin,
30
+ ]);
31
+ export function validateHttpUrl(value, trustedBase) {
32
+ let url;
33
+ try {
34
+ url = new URL(value);
35
+ }
36
+ catch {
37
+ throw new Error('Invalid API or OAuth endpoint URL.');
38
+ }
39
+ const loopback = url.hostname === '127.0.0.1' || url.hostname === 'localhost' || url.hostname === '[::1]';
40
+ if ((url.protocol !== 'https:' && !(loopback && url.protocol === 'http:')) || url.username || url.password || url.hash) {
41
+ throw new Error('API and OAuth endpoints require HTTPS (HTTP is allowed only on loopback), without credentials or fragments.');
42
+ }
43
+ if (trustedBase) {
44
+ const origin = new URL(trustedBase).origin;
45
+ if (url.origin !== origin && !(BRAIN_ORIGINS.has(origin) && BRAIN_ORIGINS.has(url.origin))) {
46
+ throw new Error('OAuth endpoint origin does not match the trusted Brain issuer.');
47
+ }
48
+ }
49
+ return url;
50
+ }
51
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/lib/http.ts"],"names":[],"mappings":"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"}
@@ -8,3 +8,11 @@ export declare function removeTomlBlock(existing: string | undefined): string |
8
8
  export declare function upsertJsonServer(existing: string | undefined, client: ClientId, rootKey?: 'mcpServers' | 'servers', scope?: Scope): string;
9
9
  export declare function removeJsonServer(existing: string | undefined, rootKey?: 'mcpServers' | 'servers'): string | undefined;
10
10
  export declare function parseJsonc(source: string): Record<string, unknown>;
11
+ /**
12
+ * Update one JSONC property without reserializing the rest of the document.
13
+ * This is used for hook files so comments and unrelated host configuration
14
+ * survive BrainMCP install/remove operations.
15
+ */
16
+ export declare function upsertJsoncPath(existing: string | undefined, path: readonly string[], value: unknown): string;
17
+ /** Remove one JSONC property while preserving the rest of the source text. */
18
+ export declare function removeJsoncPath(existing: string | undefined, path: readonly string[]): string | undefined;
@@ -130,6 +130,58 @@ export function parseJsonc(source) {
130
130
  throw new Error(`Invalid JSON/JSONC configuration: ${error instanceof Error ? error.message : String(error)}`);
131
131
  }
132
132
  }
133
+ /**
134
+ * Update one JSONC property without reserializing the rest of the document.
135
+ * This is used for hook files so comments and unrelated host configuration
136
+ * survive BrainMCP install/remove operations.
137
+ */
138
+ export function upsertJsoncPath(existing, path, value) {
139
+ if (path.length === 0)
140
+ throw new Error('A JSONC property path is required.');
141
+ let source = existing?.trim() ? existing : '{}\n';
142
+ parseJsonc(source);
143
+ let objectOpen = rootObjectOpen(source);
144
+ for (const segment of path.slice(0, -1)) {
145
+ const object = findObjectPropertyObject(source, objectOpen, segment);
146
+ if (!object) {
147
+ source = insertJsonObjectProperty(source, objectOpen, segment, {});
148
+ objectOpen = findObjectPropertyObject(source, objectOpen, segment).valueOpen;
149
+ continue;
150
+ }
151
+ objectOpen = object.valueOpen;
152
+ }
153
+ const propertyName = path.at(-1);
154
+ const property = findObjectProperty(source, objectOpen, propertyName);
155
+ if (property) {
156
+ const parsed = parseJsonc(source);
157
+ let current = parsed;
158
+ for (const segment of path) {
159
+ current = current && typeof current === 'object' && !Array.isArray(current)
160
+ ? current[segment]
161
+ : undefined;
162
+ }
163
+ if (sameJsonValue(current, value))
164
+ return existing ?? source;
165
+ return replaceJsonPropertyValue(source, property, value);
166
+ }
167
+ return insertJsonObjectProperty(source, objectOpen, propertyName, value);
168
+ }
169
+ /** Remove one JSONC property while preserving the rest of the source text. */
170
+ export function removeJsoncPath(existing, path) {
171
+ if (existing === undefined)
172
+ return undefined;
173
+ if (path.length === 0)
174
+ throw new Error('A JSONC property path is required.');
175
+ parseJsonc(existing);
176
+ let objectOpen = rootObjectOpen(existing);
177
+ for (const segment of path.slice(0, -1)) {
178
+ const object = findObjectPropertyObject(existing, objectOpen, segment);
179
+ if (!object)
180
+ return existing;
181
+ objectOpen = object.valueOpen;
182
+ }
183
+ return removeObjectProperty(existing, objectOpen, path.at(-1));
184
+ }
133
185
  function isBrainConfig(value) {
134
186
  if (typeof value !== 'object' || value === null || Array.isArray(value))
135
187
  return false;
@@ -268,6 +320,20 @@ function replaceJsonPropertyValue(source, property, value) {
268
320
  const rendered = JSON.stringify(value, null, 2).replace(/\n/g, `\n${indent}`);
269
321
  return `${source.slice(0, property.valueStart)}${rendered}${source.slice(property.valueEnd + 1)}`;
270
322
  }
323
+ function insertJsonObjectProperty(source, objectOpen, propertyName, value) {
324
+ const close = matchingBrace(source, objectOpen);
325
+ const inner = source.slice(objectOpen + 1, close);
326
+ const baseIndent = lineIndentAt(source, objectOpen);
327
+ const entryIndent = `${baseIndent} `;
328
+ const renderedValue = JSON.stringify(value, null, 2).replace(/\n/g, `\n${entryIndent}`);
329
+ const rendered = `${JSON.stringify(propertyName)}: ${renderedValue}`;
330
+ if (!inner.trim()) {
331
+ return `${source.slice(0, objectOpen + 1)}\n${entryIndent}${rendered}\n${baseIndent}${source.slice(close)}`;
332
+ }
333
+ const end = objectOpen + 1 + inner.search(/\s*$/);
334
+ const comma = source.slice(objectOpen + 1, end).trimEnd().endsWith(',') ? '' : ',';
335
+ return `${source.slice(0, end)}${comma}\n${entryIndent}${rendered}${source.slice(end)}`;
336
+ }
271
337
  /** Finds an object-valued property at the current object's top level, ignoring strings/comments. */
272
338
  function findObjectPropertyObject(source, objectOpen, propertyName) {
273
339
  const property = findObjectProperty(source, objectOpen, propertyName);
@@ -281,21 +347,21 @@ function findObjectPropertyObject(source, objectOpen, propertyName) {
281
347
  valueClose: property.valueEnd,
282
348
  };
283
349
  }
284
- /** Removes one top-level object property without reformatting its siblings or their comments. */
350
+ /** Removes one top-level JSON property without reformatting its siblings or comments. */
285
351
  function removeObjectProperty(source, objectOpen, propertyName) {
286
- const property = findObjectPropertyObject(source, objectOpen, propertyName);
352
+ const property = findObjectProperty(source, objectOpen, propertyName);
287
353
  if (!property)
288
- throw new Error(`Could not locate the ${propertyName} object.`);
354
+ return source;
289
355
  const objectClose = matchingBrace(source, objectOpen);
290
- const next = skipJsoncTrivia(source, property.valueClose + 1, objectClose);
356
+ const next = skipJsoncTrivia(source, property.valueEnd + 1, objectClose);
291
357
  if (source[next] === ',') {
292
358
  return `${source.slice(0, property.keyStart)}${source.slice(next + 1)}`;
293
359
  }
294
360
  const previousComma = previousTopLevelComma(source, objectOpen, property.keyStart);
295
361
  if (previousComma >= 0) {
296
- return `${source.slice(0, previousComma)}${source.slice(property.valueClose + 1)}`;
362
+ return `${source.slice(0, previousComma)}${source.slice(property.valueEnd + 1)}`;
297
363
  }
298
- return `${source.slice(0, property.keyStart)}${source.slice(property.valueClose + 1)}`;
364
+ return `${source.slice(0, property.keyStart)}${source.slice(property.valueEnd + 1)}`;
299
365
  }
300
366
  function rootObjectOpen(source) {
301
367
  const open = skipJsoncTrivia(source, 0, source.length);