create-byan-agent 2.14.1 → 2.16.1

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.
@@ -13,6 +13,7 @@ const fs = require('fs-extra');
13
13
 
14
14
  // Module under test (loaded once; all I/O goes to tmpdir per test)
15
15
  const { runMigration } = require('../lib/migrate-mcp-config');
16
+ const { mcpConfig: { TOKEN_PLACEHOLDER } } = require('byan-platform-config');
16
17
 
17
18
  // ──────────────────────────────────────────────────────────────────────────────
18
19
  // Helpers
@@ -69,16 +70,16 @@ test('returns no-byan-server if .mcp.json has other servers but no byan entry',
69
70
  });
70
71
 
71
72
  // ──────────────────────────────────────────────────────────────────────────────
72
- // Test 3 — byan entry already has token and URL without /api suffix → already-ok
73
+ // Test 3 — byan entry already has placeholder + clean URL → already-ok
73
74
  // ──────────────────────────────────────────────────────────────────────────────
74
- test('returns already-ok if byan entry has token and no /api suffix', async () => {
75
+ test('returns already-ok if byan entry has no BYAN_API_TOKEN (token belongs in .env / settings.local.json)', async () => {
75
76
  const dir = await makeTmp();
76
77
  await writeMcp(dir, {
77
78
  mcpServers: {
78
79
  byan: {
79
80
  command: 'node',
80
81
  args: ['_byan/mcp/byan-mcp-server/server.js'],
81
- env: { BYAN_API_URL: 'https://api.byan.io', BYAN_API_TOKEN: 'byan_tok' },
82
+ env: { BYAN_API_URL: 'https://api.byan.io' },
82
83
  },
83
84
  },
84
85
  });
@@ -91,7 +92,7 @@ test('returns already-ok if byan entry has token and no /api suffix', async () =
91
92
  // ──────────────────────────────────────────────────────────────────────────────
92
93
  // Test 4 — needs token but .env + settings.local.json both empty → no-token-available
93
94
  // ──────────────────────────────────────────────────────────────────────────────
94
- test('returns no-token-available if .mcp.json needs token but sources are empty', async () => {
95
+ test('returns already-ok if .mcp.json has no BYAN_API_TOKEN (token absence is now the desired state)', async () => {
95
96
  const dir = await makeTmp();
96
97
  await writeMcp(dir, {
97
98
  mcpServers: {
@@ -99,23 +100,19 @@ test('returns no-token-available if .mcp.json needs token but sources are empty'
99
100
  command: 'node',
100
101
  args: ['_byan/mcp/byan-mcp-server/server.js'],
101
102
  env: { BYAN_API_URL: 'https://api.byan.io' },
102
- // no BYAN_API_TOKEN
103
103
  },
104
104
  },
105
105
  });
106
- // no .env, no settings.local.json
107
106
  const result = await runMigration(dir);
108
107
  expect(result.migrated).toBe(false);
109
- expect(result.reason).toBe('no-token-available');
110
- expect(typeof result.hint).toBe('string');
111
- expect(result.hint.length).toBeGreaterThan(0);
108
+ expect(result.reason).toBe('already-ok');
112
109
  await fs.remove(dir);
113
110
  });
114
111
 
115
112
  // ──────────────────────────────────────────────────────────────────────────────
116
113
  // Test 5 — migrates successfully: token from .env
117
114
  // ──────────────────────────────────────────────────────────────────────────────
118
- test('migrates: token from .env .mcp.json env.BYAN_API_TOKEN', async () => {
115
+ test('with token already absent in .mcp.json + .env populated: no-op (already-ok), .env preserved untouched', async () => {
119
116
  const dir = await makeTmp();
120
117
  await writeMcp(dir, {
121
118
  mcpServers: {
@@ -129,19 +126,18 @@ test('migrates: token from .env → .mcp.json env.BYAN_API_TOKEN', async () => {
129
126
  await writeDotenv(dir, 'BYAN_API_TOKEN=byan_from_dotenv\n');
130
127
 
131
128
  const result = await runMigration(dir);
132
- expect(result.migrated).toBe(true);
133
- expect(result.reason).toBe('healed');
134
- expect(result.changes.length).toBeGreaterThanOrEqual(1);
129
+ expect(result.migrated).toBe(false);
130
+ expect(result.reason).toBe('already-ok');
135
131
 
136
- const written = await readMcp(dir);
137
- expect(written.mcpServers.byan.env.BYAN_API_TOKEN).toBe('byan_from_dotenv');
132
+ const dotenvAfter = await fs.readFile(path.join(dir, '.env'), 'utf8');
133
+ expect(dotenvAfter).toBe('BYAN_API_TOKEN=byan_from_dotenv\n');
138
134
  await fs.remove(dir);
139
135
  });
140
136
 
141
137
  // ──────────────────────────────────────────────────────────────────────────────
142
138
  // Test 6 — migrates successfully: token from .claude/settings.local.json fallback
143
139
  // ──────────────────────────────────────────────────────────────────────────────
144
- test('migrates: token from settings.local.json fallback', async () => {
140
+ test('with token in settings.local.json + absent from .mcp.json: no-op (already-ok), settings preserved', async () => {
145
141
  const dir = await makeTmp();
146
142
  await writeMcp(dir, {
147
143
  mcpServers: {
@@ -152,22 +148,20 @@ test('migrates: token from settings.local.json fallback', async () => {
152
148
  },
153
149
  },
154
150
  });
155
- // no .env, token in settings.local.json
156
151
  await writeSettingsLocal(dir, { env: { BYAN_API_TOKEN: 'byan_from_settings' } });
157
152
 
158
153
  const result = await runMigration(dir);
159
- expect(result.migrated).toBe(true);
160
- expect(result.reason).toBe('healed');
161
-
162
- const written = await readMcp(dir);
163
- expect(written.mcpServers.byan.env.BYAN_API_TOKEN).toBe('byan_from_settings');
154
+ expect(result.migrated).toBe(false);
155
+ expect(result.reason).toBe('already-ok');
156
+ const settings = await fs.readJson(path.join(dir, '.claude', 'settings.local.json'));
157
+ expect(settings.env.BYAN_API_TOKEN).toBe('byan_from_settings');
164
158
  await fs.remove(dir);
165
159
  });
166
160
 
167
161
  // ──────────────────────────────────────────────────────────────────────────────
168
162
  // Test 7 — strips /api suffix from BYAN_API_URL
169
163
  // ──────────────────────────────────────────────────────────────────────────────
170
- test('strips /api suffix from BYAN_API_URL', async () => {
164
+ test('strips /api suffix and extracts clear token to .env (token removed from .mcp.json)', async () => {
171
165
  const dir = await makeTmp();
172
166
  await writeMcp(dir, {
173
167
  mcpServers: {
@@ -184,7 +178,60 @@ test('strips /api suffix from BYAN_API_URL', async () => {
184
178
 
185
179
  const written = await readMcp(dir);
186
180
  expect(written.mcpServers.byan.env.BYAN_API_URL).toBe('https://api.byan.io');
187
- expect(written.mcpServers.byan.env.BYAN_API_TOKEN).toBe('byan_tok');
181
+ expect(written.mcpServers.byan.env.BYAN_API_TOKEN).toBeUndefined();
182
+ const dotenvContent = await fs.readFile(path.join(dir, '.env'), 'utf8');
183
+ expect(dotenvContent).toContain('BYAN_API_TOKEN=byan_tok');
184
+ const raw = await fs.readFile(path.join(dir, '.mcp.json'), 'utf8');
185
+ expect(raw).not.toContain('byan_tok');
186
+ await fs.remove(dir);
187
+ });
188
+
189
+ test('extracts clear token from .mcp.json into .env and removes it from .mcp.json (security migration)', async () => {
190
+ const dir = await makeTmp();
191
+ await writeMcp(dir, {
192
+ mcpServers: {
193
+ byan: {
194
+ command: 'node',
195
+ args: ['_byan/mcp/byan-mcp-server/server.js'],
196
+ env: { BYAN_API_URL: 'https://api.byan.io', BYAN_API_TOKEN: 'byan_leaked_in_clear' },
197
+ },
198
+ },
199
+ });
200
+
201
+ const result = await runMigration(dir);
202
+ expect(result.migrated).toBe(true);
203
+ expect(result.reason).toBe('healed');
204
+ expect(result.changes.some((c) => /extracted/i.test(c))).toBe(true);
205
+
206
+ const written = await readMcp(dir);
207
+ expect(written.mcpServers.byan.env.BYAN_API_TOKEN).toBeUndefined();
208
+
209
+ const raw = await fs.readFile(path.join(dir, '.mcp.json'), 'utf8');
210
+ expect(raw).not.toContain('byan_leaked_in_clear');
211
+
212
+ const dotenvContent = await fs.readFile(path.join(dir, '.env'), 'utf8');
213
+ expect(dotenvContent).toContain('BYAN_API_TOKEN=byan_leaked_in_clear');
214
+ await fs.remove(dir);
215
+ });
216
+
217
+ test('removes ${BYAN_API_TOKEN} placeholder from .mcp.json (no value to extract)', async () => {
218
+ const dir = await makeTmp();
219
+ await writeMcp(dir, {
220
+ mcpServers: {
221
+ byan: {
222
+ command: 'node',
223
+ args: ['_byan/mcp/byan-mcp-server/server.js'],
224
+ env: { BYAN_API_URL: 'https://api.byan.io', BYAN_API_TOKEN: TOKEN_PLACEHOLDER },
225
+ },
226
+ },
227
+ });
228
+
229
+ const result = await runMigration(dir);
230
+ expect(result.migrated).toBe(true);
231
+ expect(result.reason).toBe('healed');
232
+
233
+ const written = await readMcp(dir);
234
+ expect(written.mcpServers.byan.env.BYAN_API_TOKEN).toBeUndefined();
188
235
  await fs.remove(dir);
189
236
  });
190
237
 
@@ -215,6 +262,9 @@ test('dry-run: does not write .mcp.json but returns changes', async () => {
215
262
  const afterMcp = await readMcp(dir);
216
263
  expect(afterMcp.mcpServers.byan.env.BYAN_API_URL).toBe('https://api.byan.io/api');
217
264
  expect(afterMcp.mcpServers.byan.env.BYAN_API_TOKEN).toBeUndefined();
265
+ // .env must NOT have been touched on dry-run either
266
+ const dotenvAfter = await fs.readFile(path.join(dir, '.env'), 'utf8');
267
+ expect(dotenvAfter).toBe('BYAN_API_TOKEN=byan_dryrun_tok\n');
218
268
  await fs.remove(dir);
219
269
  });
220
270
 
@@ -1,16 +1,20 @@
1
1
  /**
2
2
  * migrate-mcp-config — self-healing migration for pre-fix BYAN installs.
3
3
  *
4
- * Injects BYAN_API_TOKEN into .mcp.json env block if absent, and strips the
5
- * legacy /api suffix from BYAN_API_URL. Uses byan-platform-config primitives
6
- * exclusively; no duplicated logic here.
4
+ * Heals three drift cases on .mcp.json:
5
+ * 1. token missing → injects ${BYAN_API_TOKEN} placeholder
6
+ * 2. url has /api suffix → strips it
7
+ * 3. token in clear → extracts to .env and replaces with placeholder
8
+ * (security fix from BYAN >=2.16.0)
9
+ *
10
+ * Uses byan-platform-config primitives exclusively; no duplicated logic here.
7
11
  */
8
12
 
9
13
  const chalk = require('chalk');
10
14
  const ora = require('ora');
11
15
  const {
12
- mcpConfig: { readMcpConfig, ensureMcpConfig },
13
- envConfig: { readEnvToken },
16
+ mcpConfig: { readMcpConfig, ensureMcpConfig, TOKEN_PLACEHOLDER },
17
+ envConfig: { readEnvToken, updateDotenv },
14
18
  urlUtils: { stripApiSuffix },
15
19
  } = require('byan-platform-config');
16
20
 
@@ -65,32 +69,27 @@ async function runMigration(projectRoot, { dryRun = false, verbose = false } = {
65
69
  stopSpinner(chalk.gray('.mcp.json lu'), true);
66
70
 
67
71
  // 3. Diagnose
68
- const tokenMissing = !byan.env || !byan.env.BYAN_API_TOKEN;
69
- const urlHasApiSuffix = /\/api\/?$/.test(byan.env && byan.env.BYAN_API_URL || '');
72
+ const currentToken = (byan.env && byan.env.BYAN_API_TOKEN) || '';
73
+ const tokenIsPlaceholder = currentToken === TOKEN_PLACEHOLDER;
74
+ const tokenInClear = !!currentToken && !tokenIsPlaceholder;
75
+ const tokenIsLegacy = !!currentToken; // any non-empty value is legacy: token must not live in .mcp.json
76
+ const urlHasApiSuffix = /\/api\/?$/.test(byan.env && byan.env.BYAN_API_URL || '');
70
77
 
71
78
  // 4. Nothing to do?
72
- if (!tokenMissing && !urlHasApiSuffix) {
73
- log(chalk.green(' .mcp.json est deja a jour (token present, URL correcte)'));
79
+ if (!tokenIsLegacy && !urlHasApiSuffix) {
80
+ log(chalk.green(' .mcp.json est deja a jour (token absent du fichier, URL correcte)'));
74
81
  return { migrated: false, reason: 'already-ok' };
75
82
  }
76
83
 
77
84
  const changes = [];
78
-
79
- // 5. Resolve token
80
- let token = byan.env && byan.env.BYAN_API_TOKEN; // may already exist (url-only fix)
81
- if (tokenMissing) {
82
- startSpinner('Recherche BYAN_API_TOKEN (.env / settings.local.json)...');
83
- token = await readEnvToken(projectRoot);
84
- if (!token) {
85
- stopSpinner(chalk.yellow('Token introuvable'), false);
86
- return {
87
- migrated: false,
88
- reason: 'no-token-available',
89
- hint: 'Re-run npx create-byan-agent to prompt for a token',
90
- };
91
- }
92
- stopSpinner(chalk.green('Token trouve'), true);
93
- changes.push('BYAN_API_TOKEN injected into .mcp.json env');
85
+ let extractedClearToken = null;
86
+
87
+ // 5. Detect a clear-text token to relocate to .env
88
+ if (tokenInClear) {
89
+ extractedClearToken = currentToken;
90
+ changes.push('BYAN_API_TOKEN extracted from .mcp.json (clear) into .env, removed from .mcp.json');
91
+ } else if (tokenIsPlaceholder) {
92
+ changes.push('BYAN_API_TOKEN placeholder removed from .mcp.json (token now resolved via settings.local.json env)');
94
93
  }
95
94
 
96
95
  // 6. Resolve URL
@@ -108,9 +107,16 @@ async function runMigration(projectRoot, { dryRun = false, verbose = false } = {
108
107
  return { migrated: false, reason: 'dry-run', changes };
109
108
  }
110
109
 
111
- // 8. Apply via ensureMcpConfig
110
+ // 8. Apply
112
111
  startSpinner('Application de la migration...');
113
- await ensureMcpConfig(projectRoot, { apiUrl: cleanUrl || existingUrl, token });
112
+ if (extractedClearToken) {
113
+ const existingDotenvToken = await readEnvToken(projectRoot);
114
+ if (!existingDotenvToken || existingDotenvToken !== extractedClearToken) {
115
+ await updateDotenv(projectRoot, { BYAN_API_TOKEN: extractedClearToken });
116
+ }
117
+ }
118
+ // ensureMcpConfig always strips BYAN_API_TOKEN from .mcp.json (security)
119
+ await ensureMcpConfig(projectRoot, { apiUrl: cleanUrl || existingUrl });
114
120
  stopSpinner(chalk.green('.mcp.json migre avec succes'), true);
115
121
 
116
122
  // 9. Return result