@bongos/core 1.19.665 → 1.19.667
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +40 -35
- package/docs/module-api-changelog.md +4 -0
- package/modules/dev-box/app/src/session-store.js +33 -9
- package/modules/hall-ui/public/shell.js +1 -1
- package/modules/public-landing/public/index.html +1 -1
- package/modules/public-landing/public/projects.html +1 -1
- package/modules/security/artifact-scan/detectors/credentials.js +8 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/box-infra.js +1 -1
- package/scripts/gds/cli-lib.js +55 -16
- package/scripts/gds/conductor-dispatch.js +102 -4
- package/scripts/gds/fetch-art-key.js +1 -1
- package/scripts/gds/init.js +1 -1
- package/scripts/gds/paste-token.js +10 -5
- package/scripts/gds/portability-smoke-memory.sh +1 -1
- package/scripts/gds/setup.js +4 -4
- package/scripts/gds/smoke-write.sh +2 -1
- package/scripts/hall-preview/refresh-fixtures.js +1 -1
- package/src/bongos/auth.js +27 -5
- package/src/bongos/middleware/rate-limit.js +17 -2
- package/src/bongos/routes/auth.js +46 -15
- package/src/instance-config.js +28 -2
- package/src/module-api.js +1 -1
- package/tests/cli_sessions.mjs +6 -5
- package/tests/conductor.mjs +106 -0
- package/tests/landing_page.mjs +5 -1
- package/tests/lib_sh_resolution.mjs +84 -17
- package/tests/session_rename_fallback.mjs +383 -0
- package/tests/setup_device_flow_fallback.mjs +4 -1
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
// tests/session_rename_fallback.mjs — the C3b rename keeps every OLD name readable (task 1003704).
|
|
2
|
+
//
|
|
3
|
+
// Two artefacts get renamed here, and the whole risk is in the fallback, not the rename:
|
|
4
|
+
//
|
|
5
|
+
// 1. ~/.config/<instance>/gds-session.json → bongos-session.json. This file is on disk on every
|
|
6
|
+
// builder machine and every live dev box. Renaming it with no fallback signs all of them out
|
|
7
|
+
// at once and every /builder-* command dies with 'no Bongos session'.
|
|
8
|
+
// 2. The `gds_session` browser cookie → `bongos_session`. Renaming it logs out every signed-in
|
|
9
|
+
// session, the owner's included. The read-list is THREE names, because `pms_session` predates
|
|
10
|
+
// the previous rename and is still in the wild.
|
|
11
|
+
//
|
|
12
|
+
// So these tests assert the FALLBACK path fires — a happy-path test on the new name alone would
|
|
13
|
+
// pass just as well with the back-compat deleted, which is exactly the regression that matters.
|
|
14
|
+
//
|
|
15
|
+
// Three of the read-lists are DUPLICATED rather than imported, because their modules cannot reach
|
|
16
|
+
// the canonical one (dependency-free middleware, a separately-bundled Electron app). Those copies
|
|
17
|
+
// are pinned here against the real list by parsing the source — an unpinned copy is how a rename
|
|
18
|
+
// half-lands.
|
|
19
|
+
|
|
20
|
+
import test from 'node:test';
|
|
21
|
+
import assert from 'node:assert/strict';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import os from 'node:os';
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
26
|
+
import { createRequire } from 'node:module';
|
|
27
|
+
import { spawnSync } from 'node:child_process';
|
|
28
|
+
|
|
29
|
+
const REPO_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
30
|
+
const require = createRequire(import.meta.url);
|
|
31
|
+
const ic = require(path.join(REPO_ROOT, 'src', 'instance-config.js'));
|
|
32
|
+
const lib = require(path.join(REPO_ROOT, 'scripts', 'gds', 'cli-lib.js'));
|
|
33
|
+
const auth = require(path.join(REPO_ROOT, 'src', 'bongos', 'auth.js'));
|
|
34
|
+
|
|
35
|
+
const read = (rel) => fs.readFileSync(path.join(REPO_ROOT, rel), 'utf8');
|
|
36
|
+
|
|
37
|
+
// ── the home sandbox ────────────────────────────────────────────────────────────────────────
|
|
38
|
+
//
|
|
39
|
+
// SESSION_PATH is frozen at module load from os.homedir(), so anything touching a real save path
|
|
40
|
+
// runs in a child with its own home. SETTING `HOME` ALONE IS NOT ISOLATION: on Windows os.homedir()
|
|
41
|
+
// reads USERPROFILE and never consults HOME, and the miss cost real credentials once already
|
|
42
|
+
// (task 1003760 — a suite like this one overwrote the developer's live CLI session). CI is Linux,
|
|
43
|
+
// where HOME *is* honoured, so a miss here can never go red there. Hence both layers, copied
|
|
44
|
+
// deliberately from tests/cli_sessions.mjs: override every variable a platform might read, AND
|
|
45
|
+
// fail closed in the child before any module load can have a side effect.
|
|
46
|
+
function sandboxEnv(home) {
|
|
47
|
+
const { root } = path.parse(home);
|
|
48
|
+
return {
|
|
49
|
+
...process.env,
|
|
50
|
+
HOME: home,
|
|
51
|
+
USERPROFILE: home,
|
|
52
|
+
HOMEDRIVE: root.replace(/[\\/]+$/, ''),
|
|
53
|
+
HOMEPATH: home.slice(root.length - 1) || '\\',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function inSandbox(body) {
|
|
58
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'bongos-rename-'));
|
|
59
|
+
try {
|
|
60
|
+
const code = `const R=${JSON.stringify(REPO_ROOT)};
|
|
61
|
+
const SANDBOX=${JSON.stringify(home)};
|
|
62
|
+
const os=require('node:os');
|
|
63
|
+
if (os.homedir() !== SANDBOX) {
|
|
64
|
+
console.error('ERR sandbox-escape: os.homedir()=' + os.homedir() + ' but the sandbox is ' + SANDBOX);
|
|
65
|
+
process.exit(2);
|
|
66
|
+
}
|
|
67
|
+
const fs=require('node:fs'), path=require('node:path');
|
|
68
|
+
const ic=require(R+'/src/instance-config.js');
|
|
69
|
+
const lib=require(R+'/scripts/gds/cli-lib.js');
|
|
70
|
+
const CFG=path.dirname(ic.configPath('x'));
|
|
71
|
+
const w=(name,obj)=>{fs.mkdirSync(CFG,{recursive:true});fs.writeFileSync(path.join(CFG,name),JSON.stringify(obj,null,2));};
|
|
72
|
+
const has=(name)=>fs.existsSync(path.join(CFG,name));
|
|
73
|
+
const slurp=(name)=>fs.readFileSync(path.join(CFG,name),'utf8');
|
|
74
|
+
(async()=>{ ${body} })().catch((e)=>{ console.error('ERR '+e.message); process.exit(1); });`;
|
|
75
|
+
const r = spawnSync(process.execPath, ['-e', code], { encoding: 'utf8', env: sandboxEnv(home) });
|
|
76
|
+
return { home, out: `${r.stdout || ''}${r.stderr || ''}`, status: r.status };
|
|
77
|
+
} finally {
|
|
78
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const NEW_FILE = 'bongos-session.json';
|
|
83
|
+
const OLD_FILES = ['gds-session.json', 'pms-session.json'];
|
|
84
|
+
|
|
85
|
+
// ── 1. the session-file read-list ───────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
test('SESSION_FILENAMES is newest-first, frozen, and still carries every old name', () => {
|
|
88
|
+
assert.deepEqual(ic.SESSION_FILENAMES, [NEW_FILE, ...OLD_FILES]);
|
|
89
|
+
assert.ok(Object.isFrozen(ic.SESSION_FILENAMES), 'the read-list must not be mutable at runtime');
|
|
90
|
+
// cli-lib must not keep a SECOND copy — it reads instance-config's.
|
|
91
|
+
assert.equal(lib.SESSION_FILENAMES, ic.SESSION_FILENAMES, 'cli-lib must share the one list object');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('SESSION_READ_PATHS[0] IS SESSION_PATH — migration detection depends on it', () => {
|
|
95
|
+
// loadSession migrates when the hit path !== SESSION_PATH. If the configured dir's new-name file
|
|
96
|
+
// were not first, the common case would rewrite the file on every single command.
|
|
97
|
+
assert.equal(lib.SESSION_READ_PATHS[0], lib.SESSION_PATH);
|
|
98
|
+
assert.ok(lib.SESSION_PATH.endsWith(NEW_FILE), `SESSION_PATH must be the new name, got ${lib.SESSION_PATH}`);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('SESSION_READ_PATHS is filename-major: the newest name in ANY dir beats an older name', () => {
|
|
102
|
+
const idx = (name) => lib.SESSION_READ_PATHS.findIndex((p) => p.endsWith(name));
|
|
103
|
+
for (const old of OLD_FILES) {
|
|
104
|
+
assert.ok(idx(old) > -1, `${old} must still be readable`);
|
|
105
|
+
assert.ok(idx(NEW_FILE) < idx(old), `${NEW_FILE} must be tried before ${old}`);
|
|
106
|
+
}
|
|
107
|
+
// gds- before pms-: the order of the rename chain.
|
|
108
|
+
assert.ok(idx('gds-session.json') < idx('pms-session.json'));
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ── 2. the fallback actually fires, and migrates (acceptance 1 + 2) ─────────────────────────
|
|
112
|
+
|
|
113
|
+
for (const old of OLD_FILES) {
|
|
114
|
+
test(`with ONLY ${old}: the session loads, migrates to the new name, and the old file is LEFT INTACT`, () => {
|
|
115
|
+
const { out, status } = inSandbox(`
|
|
116
|
+
w(${JSON.stringify(old)}, { token:'TOK-OLD', api_base:'https://x.example.com', gemini_api_key:'KEEP' });
|
|
117
|
+
const before = slurp(${JSON.stringify(old)});
|
|
118
|
+
const s = await lib.loadSession();
|
|
119
|
+
console.log(JSON.stringify({
|
|
120
|
+
token: s && s.token,
|
|
121
|
+
kept: s && s.gemini_api_key,
|
|
122
|
+
migrated: has(${JSON.stringify(NEW_FILE)}),
|
|
123
|
+
oldStillThere: has(${JSON.stringify(old)}),
|
|
124
|
+
oldUnchanged: slurp(${JSON.stringify(old)}) === before,
|
|
125
|
+
newMatches: has(${JSON.stringify(NEW_FILE)}) && JSON.parse(slurp(${JSON.stringify(NEW_FILE)})).token === 'TOK-OLD',
|
|
126
|
+
}));`);
|
|
127
|
+
assert.equal(status, 0, out);
|
|
128
|
+
const r = JSON.parse(out.trim().split('\n').pop());
|
|
129
|
+
assert.equal(r.token, 'TOK-OLD', 'the old-name session must still resolve');
|
|
130
|
+
assert.equal(r.kept, 'KEEP', 'unknown fields must ride through the fallback read');
|
|
131
|
+
assert.equal(r.migrated, true, `a read through ${old} must write ${NEW_FILE}`);
|
|
132
|
+
assert.equal(r.newMatches, true, 'the migrated file must carry the same token');
|
|
133
|
+
// The old file is deliberately NOT deleted: a builder may roll back to an older CLI, which can
|
|
134
|
+
// only see the old name. Part 4 (task 1003706) deletes it.
|
|
135
|
+
assert.equal(r.oldStillThere, true, `${old} must NOT be deleted by the migration`);
|
|
136
|
+
assert.equal(r.oldUnchanged, true, `${old} must be byte-identical after the migration`);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
test(`with ONLY ${NEW_FILE}: it loads and NOTHING is rewritten`, () => {
|
|
141
|
+
const { out, status } = inSandbox(`
|
|
142
|
+
w(${JSON.stringify(NEW_FILE)}, { token:'TOK-NEW', api_base:'https://x.example.com' });
|
|
143
|
+
const before = fs.statSync(path.join(CFG, ${JSON.stringify(NEW_FILE)})).mtimeMs;
|
|
144
|
+
const s = await lib.loadSession();
|
|
145
|
+
console.log(JSON.stringify({
|
|
146
|
+
token: s && s.token,
|
|
147
|
+
untouched: fs.statSync(path.join(CFG, ${JSON.stringify(NEW_FILE)})).mtimeMs === before,
|
|
148
|
+
noOldFilesInvented: ${JSON.stringify(OLD_FILES)}.every((f) => !has(f)),
|
|
149
|
+
}));`);
|
|
150
|
+
assert.equal(status, 0, out);
|
|
151
|
+
const r = JSON.parse(out.trim().split('\n').pop());
|
|
152
|
+
assert.equal(r.token, 'TOK-NEW');
|
|
153
|
+
assert.equal(r.untouched, true, 'the common path must not rewrite the file on every command');
|
|
154
|
+
assert.equal(r.noOldFilesInvented, true, 'a forward rename must never create the old names');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('the NEW name wins when both are present, even with a different token', () => {
|
|
158
|
+
const { out, status } = inSandbox(`
|
|
159
|
+
w('gds-session.json', { token:'TOK-STALE', api_base:'https://x.example.com' });
|
|
160
|
+
w(${JSON.stringify(NEW_FILE)}, { token:'TOK-FRESH', api_base:'https://x.example.com' });
|
|
161
|
+
const s = await lib.loadSession();
|
|
162
|
+
console.log(JSON.stringify({ token: s && s.token }));`);
|
|
163
|
+
assert.equal(status, 0, out);
|
|
164
|
+
assert.equal(JSON.parse(out.trim().split('\n').pop()).token, 'TOK-FRESH');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('readSessionConfigSync falls back too — it is what resolveApiBase reads', () => {
|
|
168
|
+
// instance-config cannot import cli-lib (cli-lib imports IT), so it has its own reader; a
|
|
169
|
+
// fallback that only existed in cli-lib would leave api_base resolution blind on an
|
|
170
|
+
// un-migrated machine.
|
|
171
|
+
const { out, status } = inSandbox(`
|
|
172
|
+
w('gds-session.json', { token:'T', api_base:'https://fallback.example.com' });
|
|
173
|
+
console.log(JSON.stringify({
|
|
174
|
+
raw: !!ic.readSessionConfigSync(),
|
|
175
|
+
base: ic.resolveApiBase({ env: {} }),
|
|
176
|
+
}));`);
|
|
177
|
+
assert.equal(status, 0, out);
|
|
178
|
+
const r = JSON.parse(out.trim().split('\n').pop());
|
|
179
|
+
assert.equal(r.raw, true, 'readSessionConfigSync must find the old name');
|
|
180
|
+
assert.equal(r.base, 'https://fallback.example.com', 'api_base must resolve from an old-name session');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// ── 3. the cookie read-list (acceptance 3) ──────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
const req = (cookie, authz) => ({ headers: { cookie, ...(authz ? { authorization: authz } : {}) } });
|
|
186
|
+
|
|
187
|
+
test('SESSION_COOKIE_NAMES is newest-first and frozen', () => {
|
|
188
|
+
assert.deepEqual(auth.SESSION_COOKIE_NAMES, ['bongos_session', 'gds_session', 'pms_session']);
|
|
189
|
+
assert.ok(Object.isFrozen(auth.SESSION_COOKIE_NAMES));
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
for (const name of ['bongos_session', 'gds_session', 'pms_session']) {
|
|
193
|
+
test(`a request carrying ONLY ${name} still resolves a token`, () => {
|
|
194
|
+
assert.deepEqual(auth.extractTokens(req(`${name}=TOKEN-X`)), ['TOKEN-X']);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
test('all three names present → newest first, regardless of header order', () => {
|
|
199
|
+
// Header order deliberately reversed: preference must come from the read-list, not the browser.
|
|
200
|
+
assert.deepEqual(
|
|
201
|
+
auth.extractTokens(req('pms_session=P; gds_session=G; bongos_session=B')),
|
|
202
|
+
['B', 'G', 'P'],
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('a name appearing TWICE still surfaces both values (#851 must not regress)', () => {
|
|
207
|
+
// A browser can carry a stale host-only cookie AND the current apex-scoped one under the same
|
|
208
|
+
// name. Grouping by name must keep a LIST, or the stale duplicate shadows the live session and
|
|
209
|
+
// 401s the builder — the exact bug tests/auth_duplicate_cookie.mjs was written for.
|
|
210
|
+
assert.deepEqual(auth.extractTokens(req('gds_session=STALE; gds_session=LIVE')), ['STALE', 'LIVE']);
|
|
211
|
+
assert.deepEqual(
|
|
212
|
+
auth.extractTokens(req('bongos_session=B1; gds_session=G1; bongos_session=B2')),
|
|
213
|
+
['B1', 'B2', 'G1'],
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('a non-session cookie is never mistaken for one', () => {
|
|
218
|
+
assert.deepEqual(auth.extractTokens(req('gds_oauth_state=NOPE; gds_return=NOPE2')), []);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// ── 4. the router sets the new name and clears the old ones ─────────────────────────────────
|
|
222
|
+
|
|
223
|
+
test('routes/auth.js sets ONLY the head name and derives the clear-list from the tail', () => {
|
|
224
|
+
const src = read('src/bongos/routes/auth.js');
|
|
225
|
+
assert.match(src, /const SESSION_COOKIE = auth\.SESSION_COOKIE_NAMES\[0\];/,
|
|
226
|
+
'the SET name must be the read-list head, not a second literal');
|
|
227
|
+
assert.match(src, /const SESSION_COOKIE_OLD = auth\.SESSION_COOKIE_NAMES\.slice\(1\);/,
|
|
228
|
+
'the clear-list must be the read-list tail');
|
|
229
|
+
assert.doesNotMatch(src, /'gds_session'|"gds_session"|'pms_session'|"pms_session"/,
|
|
230
|
+
'no session-cookie name may be re-declared as a literal here — that is how the lists drift');
|
|
231
|
+
// Superseded names must be cleared on BOTH the sign-in write and the logout, or a stale cookie
|
|
232
|
+
// outlives the session it belonged to.
|
|
233
|
+
const clears = src.split('clearSupersededSessionCookies(req, res)').length - 1;
|
|
234
|
+
assert.ok(clears >= 3, `expected the clear helper defined + used at sign-in and logout, saw ${clears}`);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// ── 5. the duplicated copies are pinned to the canonical lists ──────────────────────────────
|
|
238
|
+
|
|
239
|
+
test('rate-limit.js — its dependency-free copy of the cookie names matches the real list', () => {
|
|
240
|
+
const src = read('src/bongos/middleware/rate-limit.js');
|
|
241
|
+
const m = /const SESSION_COOKIE_RES = \[([^\]]*)\]/.exec(src);
|
|
242
|
+
assert.ok(m, 'SESSION_COOKIE_RES not found — did the limiter stop reading session cookies?');
|
|
243
|
+
const names = [...m[1].matchAll(/'([a-z_]+)'/g)].map((x) => x[1]);
|
|
244
|
+
assert.deepEqual(names, auth.SESSION_COOKIE_NAMES,
|
|
245
|
+
'the limiter’s copy drifted from src/bongos/auth.js SESSION_COOKIE_NAMES');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('dev-box session-store — its bundled copy of the filenames matches the real list', () => {
|
|
249
|
+
const src = read('modules/dev-box/app/src/session-store.js');
|
|
250
|
+
const head = /const FILE = path\.join\(DIR, '([^']+)'\)/.exec(src);
|
|
251
|
+
assert.ok(head, 'FILE not found in the dev-box session store');
|
|
252
|
+
const legacy = /const FILES_LEGACY = \[([^\]]*)\]/.exec(src);
|
|
253
|
+
assert.ok(legacy, 'FILES_LEGACY not found — the app would stop reading un-migrated sessions');
|
|
254
|
+
const names = [head[1], ...[...legacy[1].matchAll(/'([^']+\.json)'/g)].map((x) => x[1])];
|
|
255
|
+
assert.deepEqual(names, ic.SESSION_FILENAMES,
|
|
256
|
+
'the dev-box app’s copy drifted from src/instance-config.js SESSION_FILENAMES');
|
|
257
|
+
// It must WRITE only the head name.
|
|
258
|
+
assert.doesNotMatch(src, /writeFileSync\(\s*FILES_LEGACY/, 'the app must never write a legacy name');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test('the browser logout clears every session-cookie name', () => {
|
|
262
|
+
// Three static surfaces expire the cookie client-side. One that misses the current name leaves a
|
|
263
|
+
// live cookie behind after "sign out".
|
|
264
|
+
for (const rel of ['modules/hall-ui/public/shell.js',
|
|
265
|
+
'modules/public-landing/public/index.html',
|
|
266
|
+
'modules/public-landing/public/projects.html']) {
|
|
267
|
+
const m = /\[((?:'[a-z_]+',?\s*)+)\]\.forEach\(function \(name\) \{/.exec(read(rel));
|
|
268
|
+
assert.ok(m, `${rel}: cookie-clearing list not found`);
|
|
269
|
+
const names = [...m[1].matchAll(/'([a-z_]+)'/g)].map((x) => x[1]);
|
|
270
|
+
assert.deepEqual(names, auth.SESSION_COOKIE_NAMES, `${rel}: clear-list drifted`);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// ── 6. the box scripts read both names ──────────────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
test('the baked box scripts scan BOTH session filenames', () => {
|
|
277
|
+
// These run on live dev boxes and are baked at provision time, so they cannot import anything.
|
|
278
|
+
// A box provisioned AFTER the rename writes only the new name; one provisioned before has only
|
|
279
|
+
// the old. Missing either silently breaks heartbeat/reporting on half the fleet.
|
|
280
|
+
for (const rel of ['infra/box-heartbeat.sh', 'infra/box-report-host-keys.sh',
|
|
281
|
+
'infra/box-report-terminal.sh', 'infra/box-report-version.sh']) {
|
|
282
|
+
const src = read(rel);
|
|
283
|
+
const glob = src.split('\n').find((l) => l.includes('for _sess in'));
|
|
284
|
+
assert.ok(glob, `${rel}: session glob not found`);
|
|
285
|
+
for (const name of [NEW_FILE, 'gds-session.json']) {
|
|
286
|
+
assert.ok(glob.includes(name), `${rel}: glob must scan ${name} — got: ${glob.trim()}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// ── 7. the dev-box app's own copy of the fallback, and its sign-out ─────────────────────────
|
|
292
|
+
//
|
|
293
|
+
// The Electron app shares the CLI's session file on purpose (ADR 0045). It ships as its own
|
|
294
|
+
// bundle, so it cannot import instance-config and carries its own read-list — which means its
|
|
295
|
+
// fallback needs exercising, not just pinning.
|
|
296
|
+
function inDevBoxSandbox(body) {
|
|
297
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'bongos-devbox-'));
|
|
298
|
+
try {
|
|
299
|
+
const code = `const R=${JSON.stringify(REPO_ROOT)};
|
|
300
|
+
const SANDBOX=${JSON.stringify(home)};
|
|
301
|
+
const os=require('node:os');
|
|
302
|
+
if (os.homedir() !== SANDBOX) {
|
|
303
|
+
console.error('ERR sandbox-escape: os.homedir()=' + os.homedir() + ' but the sandbox is ' + SANDBOX);
|
|
304
|
+
process.exit(2);
|
|
305
|
+
}
|
|
306
|
+
const fs=require('node:fs'), path=require('node:path');
|
|
307
|
+
const brand=require(R+'/modules/dev-box/app/src/brand.js');
|
|
308
|
+
const CFG=path.join(os.homedir(), '.config', brand.CONFIG_DIR);
|
|
309
|
+
const store=require(R+'/modules/dev-box/app/src/session-store.js');
|
|
310
|
+
const w=(name,obj)=>{fs.mkdirSync(CFG,{recursive:true});fs.writeFileSync(path.join(CFG,name),JSON.stringify(obj,null,2));};
|
|
311
|
+
const has=(name)=>fs.existsSync(path.join(CFG,name));
|
|
312
|
+
const j=(name)=>JSON.parse(fs.readFileSync(path.join(CFG,name),'utf8'));
|
|
313
|
+
(async()=>{ ${body} })().catch((e)=>{ console.error('ERR '+e.message); process.exit(1); });`;
|
|
314
|
+
const r = spawnSync(process.execPath, ['-e', code], { encoding: 'utf8', env: sandboxEnv(home) });
|
|
315
|
+
return { out: `${r.stdout || ''}${r.stderr || ''}`, status: r.status };
|
|
316
|
+
} finally {
|
|
317
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
test('dev-box: reads an old-name session, and writes migrate to the new name', () => {
|
|
322
|
+
const { out, status } = inDevBoxSandbox(`
|
|
323
|
+
w('gds-session.json', { token:'TOK-OLD', api_base:'https://x.example.com', gemini_api_key:'KEEP' });
|
|
324
|
+
const got = store.read();
|
|
325
|
+
store.write({ token:'TOK-NEW', apiBase:'https://x.example.com' });
|
|
326
|
+
console.log(JSON.stringify({
|
|
327
|
+
readOld: got && got.token,
|
|
328
|
+
newFile: has('bongos-session.json') && j('bongos-session.json').token,
|
|
329
|
+
keptExtra: has('bongos-session.json') && j('bongos-session.json').gemini_api_key,
|
|
330
|
+
oldKept: has('gds-session.json'),
|
|
331
|
+
}));`);
|
|
332
|
+
assert.equal(status, 0, out);
|
|
333
|
+
const r = JSON.parse(out.trim().split('\n').pop());
|
|
334
|
+
assert.equal(r.readOld, 'TOK-OLD', 'the app must still read an un-migrated session');
|
|
335
|
+
assert.equal(r.newFile, 'TOK-NEW', 'a write must land on the new name');
|
|
336
|
+
assert.equal(r.keptExtra, 'KEEP', 'merge-preserving write must carry unknown fields across the rename');
|
|
337
|
+
assert.equal(r.oldKept, true, 'the app must not delete the old file either');
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test('dev-box: sign-out strips the credential from EVERY accepted filename', () => {
|
|
341
|
+
// THE BUG THIS PINS. clear() rewrote only the CURRENT filename, but read() falls through to an
|
|
342
|
+
// older name when the current file carries no token — so sign-out left a live token in the
|
|
343
|
+
// superseded file and the very next read signed the builder straight back in. Harmless while
|
|
344
|
+
// the only legacy name was the long-dead pms-session.json; the common case the moment
|
|
345
|
+
// gds-session.json joined the read-list (task 1003704).
|
|
346
|
+
const { out, status } = inDevBoxSandbox(`
|
|
347
|
+
w('bongos-session.json', { token:'TOK-NEW', api_base:'https://x.example.com' });
|
|
348
|
+
w('gds-session.json', { token:'TOK-OLD', api_base:'https://x.example.com', gemini_api_key:'KEEP' });
|
|
349
|
+
store.clear();
|
|
350
|
+
console.log(JSON.stringify({
|
|
351
|
+
stillSignedIn: !!store.read(),
|
|
352
|
+
newToken: j('bongos-session.json').token ?? null,
|
|
353
|
+
oldToken: j('gds-session.json').token ?? null,
|
|
354
|
+
keptExtra: j('gds-session.json').gemini_api_key,
|
|
355
|
+
}));`);
|
|
356
|
+
assert.equal(status, 0, out);
|
|
357
|
+
const r = JSON.parse(out.trim().split('\n').pop());
|
|
358
|
+
assert.equal(r.stillSignedIn, false, 'sign-out must not leave a readable session under ANY name');
|
|
359
|
+
assert.equal(r.newToken, null, 'the current file must lose its token');
|
|
360
|
+
assert.equal(r.oldToken, null, 'the superseded file must lose its token too — this is the bug');
|
|
361
|
+
// Sign-out drops credentials only; CLI-adjacent config must survive in every file it touches.
|
|
362
|
+
assert.equal(r.keptExtra, 'KEEP', 'clear() must preserve non-credential fields');
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// ── 8. the credential detector follows the rename ───────────────────────────────────────────
|
|
366
|
+
|
|
367
|
+
test('the artifact-scan credential detector flags a read of EVERY session filename', () => {
|
|
368
|
+
// A rename that moved the file but not the detector would open a hole rather than close one:
|
|
369
|
+
// scanned third-party code reaching for bongos-session.json would score no credential hit at
|
|
370
|
+
// all on an instance whose configDir is not 'otb' (the rule's second alternative only covers
|
|
371
|
+
// ~/.config/otb). Both directions matter — the old names are still live credentials.
|
|
372
|
+
const src = read('modules/security/artifact-scan/detectors/credentials.js');
|
|
373
|
+
const rule = src.split('\n').find((l) => l.includes('CLI session token'));
|
|
374
|
+
assert.ok(rule, 'the session-token credential rule is gone');
|
|
375
|
+
for (const name of ic.SESSION_FILENAMES) {
|
|
376
|
+
const stem = name.replace('-session.json', '');
|
|
377
|
+
assert.ok(rule.includes(stem), `the detector must still match ${name} — got: ${rule.trim()}`);
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
// The box resolver is EXECUTED — a post-rename box and a migrated one — in
|
|
381
|
+
// tests/lib_sh_resolution.mjs, which already owns the extraction harness for that shell
|
|
382
|
+
// block. Deliberately not duplicated here: two harnesses on one block is two things to
|
|
383
|
+
// fix when the names next move (task 1003704).
|
|
@@ -35,6 +35,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
35
35
|
|
|
36
36
|
const require = createRequire(import.meta.url);
|
|
37
37
|
const setup = require('../scripts/gds/setup.js');
|
|
38
|
+
const ic = require('../src/instance-config.js');
|
|
38
39
|
|
|
39
40
|
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
40
41
|
const SETUP_JS = path.join(REPO_ROOT, 'scripts', 'gds', 'setup.js');
|
|
@@ -149,7 +150,9 @@ function findSessions(home) {
|
|
|
149
150
|
for (const f of entries) {
|
|
150
151
|
const p = path.join(dir, f.name);
|
|
151
152
|
if (f.isDirectory()) walk(p);
|
|
152
|
-
|
|
153
|
+
// The CURRENT session filename, read off the canonical list — not a literal, which is
|
|
154
|
+
// how this walk came to report 0 files for a perfectly good write (task 1003704).
|
|
155
|
+
else if (f.name === ic.SESSION_FILENAMES[0]) hits.push(p);
|
|
153
156
|
}
|
|
154
157
|
})(home);
|
|
155
158
|
return hits;
|