@bongos/core 1.19.336 → 1.19.338
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 +30 -30
- package/modules/builder-settings/routes/settings.js +18 -16
- package/modules/dev-box/routes/box.js +25 -23
- package/modules/discord/discord-bot.js +37 -35
- package/modules/discord/discord-role-sync.js +14 -12
- package/modules/government/routes/government.js +15 -13
- package/modules/ideas/routes/inbox.js +16 -14
- package/modules/lifecycle/routes/claims.js +11 -9
- package/modules/lifecycle/routes/goals.js +25 -23
- package/modules/lifecycle/routes/task-write-routes.js +17 -15
- package/modules/lifecycle/routes/tasks.js +21 -19
- package/modules/memory/routes/memory.js +18 -16
- package/modules/platform-identity/routes/sso.js +13 -11
- package/modules/provisioning/routes/provisioning.js +29 -18
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/fitness.js +6 -0
- package/src/bongos/auth-admission.js +9 -6
- package/src/bongos/routes/auth.js +20 -17
- package/src/bongos/routes/backup.js +7 -4
- package/src/bongos/routes/builders.js +17 -14
- package/src/bongos/routes/public.js +9 -6
- package/src/bongos/uptime-poller.js +8 -5
- package/src/module-api.js +44 -6
- package/tests/logger.mjs +67 -0
- package/tests/provisioning_github_app_routes.mjs +40 -1
|
@@ -7,6 +7,9 @@ const { pairStore } = require('../app-pair');
|
|
|
7
7
|
const { publicOrigin, parseCookie, validateOrRespond } = require('./_helpers');
|
|
8
8
|
const { branding } = require('../../branding');
|
|
9
9
|
const seams = require('../../module-seams');
|
|
10
|
+
// task 1003208: structured logging — was console.*. A core file logs through
|
|
11
|
+
// the kernel logger directly; only MODULES go via api.logger (the doorway).
|
|
12
|
+
const log = require('../logger').logger.child({ src: 'auth' });
|
|
10
13
|
|
|
11
14
|
function escapeHtml(s) {
|
|
12
15
|
return String(s ?? '').replace(/[&<>"']/g, (c) => ({
|
|
@@ -358,7 +361,7 @@ module.exports = function buildAuthRouter() {
|
|
|
358
361
|
const out = await auth.startDeviceFlow();
|
|
359
362
|
res.json(out);
|
|
360
363
|
} catch (err) {
|
|
361
|
-
|
|
364
|
+
log.error('[gds] device/start', err);
|
|
362
365
|
// Surface the underlying reason (task 2126). A thrown device-flow start is almost
|
|
363
366
|
// always a GitHub OAuth-App misconfig (device_flow_disabled / bad client) — safe,
|
|
364
367
|
// non-secret, and actionable — so return it instead of an opaque "internal error".
|
|
@@ -394,7 +397,7 @@ module.exports = function buildAuthRouter() {
|
|
|
394
397
|
if (err && err.code === 'NOT_APPROVED') {
|
|
395
398
|
return res.status(403).json({ state: 'not_approved', error: 'not_approved' });
|
|
396
399
|
}
|
|
397
|
-
|
|
400
|
+
log.error('[gds] device/poll', err);
|
|
398
401
|
// 424 for the same reason as device/start above (finding W2, task 1003052):
|
|
399
402
|
// this catch proxies GitHub's poll refusals too, and a 5xx body never
|
|
400
403
|
// survives Cloudflare — the JSON envelope (its code at minimum) must.
|
|
@@ -432,7 +435,7 @@ module.exports = function buildAuthRouter() {
|
|
|
432
435
|
}
|
|
433
436
|
// A bad / expired / forged / wrong-audience assertion is an auth failure (401),
|
|
434
437
|
// not a server error — never mint a session for it.
|
|
435
|
-
|
|
438
|
+
log.error('[gds] /auth/device/exchange', err);
|
|
436
439
|
res.fail('invalid_assertion', { status: 401, message: String(err.message || 'invalid assertion').slice(0, 160) });
|
|
437
440
|
}
|
|
438
441
|
});
|
|
@@ -536,7 +539,7 @@ module.exports = function buildAuthRouter() {
|
|
|
536
539
|
return res.json({ admitted: !!admitted });
|
|
537
540
|
} catch (err) {
|
|
538
541
|
// Fail closed and quietly — the page just keeps polling.
|
|
539
|
-
|
|
542
|
+
log.error('[gds] auth/web/admission-status', err);
|
|
540
543
|
return res.json({ admitted: false });
|
|
541
544
|
}
|
|
542
545
|
});
|
|
@@ -594,7 +597,7 @@ module.exports = function buildAuthRouter() {
|
|
|
594
597
|
try {
|
|
595
598
|
await db.upsertGithubToken({ builderId: builder.id, token: githubToken, scope: githubScope });
|
|
596
599
|
} catch (e) {
|
|
597
|
-
|
|
600
|
+
log.error('[gds] web/callback: repo-token capture failed (non-blocking):', e.message);
|
|
598
601
|
}
|
|
599
602
|
}
|
|
600
603
|
// The state cookie is apex-scoped now (#734) — clear it with the same
|
|
@@ -636,7 +639,7 @@ module.exports = function buildAuthRouter() {
|
|
|
636
639
|
} catch (probeErr) {
|
|
637
640
|
// Never let the classifier turn a clean 403 into a 500 — fall through
|
|
638
641
|
// to the existing page, which is the safe answer for a stranger.
|
|
639
|
-
|
|
642
|
+
log.error('[gds] web/callback admission classify', probeErr);
|
|
640
643
|
}
|
|
641
644
|
if (expectedSoon) {
|
|
642
645
|
return res.status(202).type('text/html').send(renderAccessPendingPage(err.login, recoveryStartHref(req)));
|
|
@@ -657,7 +660,7 @@ module.exports = function buildAuthRouter() {
|
|
|
657
660
|
},
|
|
658
661
|
));
|
|
659
662
|
}
|
|
660
|
-
|
|
663
|
+
log.error('[gds] web/callback', err);
|
|
661
664
|
res.status(500).type('text/html').send(renderAuthErrorPage(
|
|
662
665
|
'The handshake threw an error',
|
|
663
666
|
`The server tried to exchange the auth code for a session and something went wrong: ${err.message}. Try again; if it persists, ask the ${admitterLabel()} to investigate the server logs.`,
|
|
@@ -778,7 +781,7 @@ module.exports = function buildAuthRouter() {
|
|
|
778
781
|
});
|
|
779
782
|
joined = true;
|
|
780
783
|
} catch (e) {
|
|
781
|
-
|
|
784
|
+
log.info('[gds] discord auto-join failed (non-blocking):', e.message);
|
|
782
785
|
}
|
|
783
786
|
}
|
|
784
787
|
// F5 role-sync belt-and-braces (ADR 0033 §4): set/repair the rank-role even
|
|
@@ -790,7 +793,7 @@ module.exports = function buildAuthRouter() {
|
|
|
790
793
|
// callback returns there (settings.js surfaces the ?discord= toast).
|
|
791
794
|
res.redirect(joined ? '/builders/settings?discord=joined' : '/builders/settings?discord=linked');
|
|
792
795
|
} catch (err) {
|
|
793
|
-
|
|
796
|
+
log.error('[gds] discord/callback', err);
|
|
794
797
|
res.status(500).type('text/html').send(renderAuthErrorPage(
|
|
795
798
|
'The Discord link threw an error',
|
|
796
799
|
`Linking failed unexpectedly. Try again; if it persists, ask the ${admitterLabel()} to check the server logs.`,
|
|
@@ -814,13 +817,13 @@ module.exports = function buildAuthRouter() {
|
|
|
814
817
|
)(null);
|
|
815
818
|
if (guildId) {
|
|
816
819
|
auth.removeDiscordGuildMember({ guildId, discordUserId: link.discord_user_id })
|
|
817
|
-
.then((r) =>
|
|
818
|
-
.catch((e) =>
|
|
820
|
+
.then((r) => log.info(`[gds] discord unlink kick: ${r.removed ? 'removed' : r.reason}`))
|
|
821
|
+
.catch((e) => log.info('[gds] discord unlink kick failed (non-blocking):', e.message));
|
|
819
822
|
}
|
|
820
823
|
}
|
|
821
824
|
res.json({ ok: true, unlinked: removed });
|
|
822
825
|
} catch (err) {
|
|
823
|
-
|
|
826
|
+
log.error('[gds] discord/unlink', err);
|
|
824
827
|
res.fail('unlink_failed', 500);
|
|
825
828
|
}
|
|
826
829
|
});
|
|
@@ -897,7 +900,7 @@ module.exports = function buildAuthRouter() {
|
|
|
897
900
|
if (builder) await db.revokeAllSessionsForBuilder(builder.id);
|
|
898
901
|
res.json({ ok: true });
|
|
899
902
|
} catch (err) {
|
|
900
|
-
|
|
903
|
+
log.error('[gds] /auth/backchannel-logout revoke', err);
|
|
901
904
|
res.fail('server_error', { status: 500, message: 'logout failed' });
|
|
902
905
|
}
|
|
903
906
|
});
|
|
@@ -941,7 +944,7 @@ module.exports = function buildAuthRouter() {
|
|
|
941
944
|
paste_command: `node scripts/gds/paste-token.js ${token}`,
|
|
942
945
|
});
|
|
943
946
|
} catch (err) {
|
|
944
|
-
|
|
947
|
+
log.error('[gds] POST /auth/cli-token/issue', err);
|
|
945
948
|
res.fail('issue_failed', { status: 500, message: 'internal error' });
|
|
946
949
|
}
|
|
947
950
|
});
|
|
@@ -968,7 +971,7 @@ module.exports = function buildAuthRouter() {
|
|
|
968
971
|
const revoked = await db.revokeSessionById(id);
|
|
969
972
|
res.json({ ok: true, revoked });
|
|
970
973
|
} catch (err) {
|
|
971
|
-
|
|
974
|
+
log.error('[gds] POST /auth/revoke', err);
|
|
972
975
|
res.fail('revoke_failed', { status: 500, message: 'internal error' });
|
|
973
976
|
}
|
|
974
977
|
});
|
|
@@ -995,7 +998,7 @@ module.exports = function buildAuthRouter() {
|
|
|
995
998
|
const revoked = await db.revokeAllSessionsForBuilder(id);
|
|
996
999
|
res.json({ ok: true, revoked });
|
|
997
1000
|
} catch (err) {
|
|
998
|
-
|
|
1001
|
+
log.error('[gds] POST /auth/revoke-all', err);
|
|
999
1002
|
res.fail('revoke_all_failed', { status: 500, message: 'internal error' });
|
|
1000
1003
|
}
|
|
1001
1004
|
});
|
|
@@ -1092,7 +1095,7 @@ module.exports = function buildAuthRouter() {
|
|
|
1092
1095
|
}
|
|
1093
1096
|
res.json({ ok: true });
|
|
1094
1097
|
} catch (err) {
|
|
1095
|
-
|
|
1098
|
+
log.error('[gds] POST /auth/app-pair/approve', err);
|
|
1096
1099
|
res.fail('approve_failed', { status: 500, message: 'internal error' });
|
|
1097
1100
|
}
|
|
1098
1101
|
});
|
|
@@ -23,6 +23,9 @@ const { execFile } = require('node:child_process');
|
|
|
23
23
|
const auth = require('../auth');
|
|
24
24
|
const { branding } = require('../../branding');
|
|
25
25
|
const { asyncHandler } = require('./_helpers');
|
|
26
|
+
// task 1003208: structured logging — was console.*. A core file logs through
|
|
27
|
+
// the kernel logger directly; only MODULES go via api.logger (the doorway).
|
|
28
|
+
const log = require('../logger').logger.child({ src: 'backup' });
|
|
26
29
|
|
|
27
30
|
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..');
|
|
28
31
|
const BACKUP_DIR = process.env.GDS_BACKUP_DIR || '/var/backups/gds';
|
|
@@ -132,15 +135,15 @@ module.exports = function buildBackupRouter() {
|
|
|
132
135
|
triggerRunning = true;
|
|
133
136
|
const startedAt = new Date().toISOString();
|
|
134
137
|
const builderId = req.builder && req.builder.id;
|
|
135
|
-
|
|
138
|
+
log.info(`[gds] backup triggered by builder ${builderId} at ${startedAt}`);
|
|
136
139
|
|
|
137
140
|
execFile('bash', [BACKUP_SCRIPT], { cwd: REPO_ROOT, timeout: 120_000 }, (err, stdout, stderr) => {
|
|
138
141
|
triggerRunning = false;
|
|
139
142
|
if (err) {
|
|
140
|
-
|
|
141
|
-
if (stderr)
|
|
143
|
+
log.error(`[gds] backup trigger failed (builder ${builderId}):`, err.message);
|
|
144
|
+
if (stderr) log.error('[gds] backup stderr:', stderr.toString().trim());
|
|
142
145
|
} else {
|
|
143
|
-
|
|
146
|
+
log.info(`[gds] backup trigger completed (builder ${builderId})`);
|
|
144
147
|
}
|
|
145
148
|
});
|
|
146
149
|
|
|
@@ -10,6 +10,9 @@ const { validateOrRespond, LIMITS, parseId } = require('./_helpers');
|
|
|
10
10
|
const { pool } = require('../pool');
|
|
11
11
|
const seams = require('../../module-seams');
|
|
12
12
|
const { branding } = require('../../branding');
|
|
13
|
+
// task 1003208: structured logging — was console.*. A core file logs through
|
|
14
|
+
// the kernel logger directly; only MODULES go via api.logger (the doorway).
|
|
15
|
+
const log = require('../logger').logger.child({ src: 'builders' });
|
|
13
16
|
// The lifecycle (tasks · claims · ship) carved to modules/lifecycle/ (BV1.R86 /
|
|
14
17
|
// ADR 0093 §3): the Archon roster, the per-builder stale-claim view, the
|
|
15
18
|
// per-builder grade breakdown, and the shipped-count read resolve the `lifecycle`
|
|
@@ -89,7 +92,7 @@ function buildRankChangeBroadcast({ displayName, direction, newRank }) {
|
|
|
89
92
|
function fireDiscordBroadcast(text) {
|
|
90
93
|
if (!text) return;
|
|
91
94
|
if (process.env.CHAT_DRY_RUN === '1') {
|
|
92
|
-
|
|
95
|
+
log.info(`[gds] [broadcast dry-run] would post: ${text}`);
|
|
93
96
|
return;
|
|
94
97
|
}
|
|
95
98
|
try {
|
|
@@ -99,13 +102,13 @@ function fireDiscordBroadcast(text) {
|
|
|
99
102
|
{ cwd: REPO_ROOT, timeout: 10_000 },
|
|
100
103
|
(err, _stdout, stderr) => {
|
|
101
104
|
if (err) {
|
|
102
|
-
|
|
103
|
-
if (stderr)
|
|
105
|
+
log.info('[gds] rank-change broadcast failed (non-blocking):', err.message);
|
|
106
|
+
if (stderr) log.info('[gds] discord stderr:', stderr.toString().trim());
|
|
104
107
|
}
|
|
105
108
|
}
|
|
106
109
|
);
|
|
107
110
|
} catch (e) {
|
|
108
|
-
|
|
111
|
+
log.info('[gds] rank-change broadcast spawn failed (non-blocking):', e && e.message);
|
|
109
112
|
}
|
|
110
113
|
}
|
|
111
114
|
|
|
@@ -182,7 +185,7 @@ module.exports = function buildBuildersRouter() {
|
|
|
182
185
|
if (err && err.code === 'MULTI_RUNG_PROMOTION') {
|
|
183
186
|
return res.fail('multi_rung_promotion', { status: 400, message: `Promotions advance one rung at a time. From ${err.from}, the next rung is ${err.next_rung} — promote there first.`, details: { from: err.from, to: err.to, next_rung: err.next_rung } });
|
|
184
187
|
}
|
|
185
|
-
|
|
188
|
+
log.error('[gds] PATCH /builders/:id/rank', err);
|
|
186
189
|
res.fail('rank_change_failed', { status: 500, message: 'internal error' });
|
|
187
190
|
}
|
|
188
191
|
});
|
|
@@ -233,7 +236,7 @@ module.exports = function buildBuildersRouter() {
|
|
|
233
236
|
if (err && err.code === 'NOT_INACTIVE') {
|
|
234
237
|
return res.fail('not_inactive', 409, { status: err.status });
|
|
235
238
|
}
|
|
236
|
-
|
|
239
|
+
log.error('[gds] PATCH /builders/:id/status', err);
|
|
237
240
|
res.fail('status_change_failed', { status: 500, message: 'internal error' });
|
|
238
241
|
}
|
|
239
242
|
});
|
|
@@ -268,7 +271,7 @@ module.exports = function buildBuildersRouter() {
|
|
|
268
271
|
if (!updated) return res.fail('builder_not_found', 404);
|
|
269
272
|
res.json({ builder: updated });
|
|
270
273
|
} catch (err) {
|
|
271
|
-
|
|
274
|
+
log.error('[gds] PATCH /builders/:id/budget', err);
|
|
272
275
|
res.fail('budget_change_failed', { status: 500, message: 'internal error' });
|
|
273
276
|
}
|
|
274
277
|
});
|
|
@@ -301,7 +304,7 @@ module.exports = function buildBuildersRouter() {
|
|
|
301
304
|
const builders = await lc().getBuildersRoster({ staleHours });
|
|
302
305
|
res.json({ builders, stale_hours: staleHours });
|
|
303
306
|
} catch (err) {
|
|
304
|
-
|
|
307
|
+
log.error('[gds] GET /builders/roster', err);
|
|
305
308
|
res.fail('roster_failed', { status: 500, message: 'internal error' });
|
|
306
309
|
}
|
|
307
310
|
});
|
|
@@ -319,7 +322,7 @@ module.exports = function buildBuildersRouter() {
|
|
|
319
322
|
const builders = await db.listActiveBuilders();
|
|
320
323
|
res.json({ builders });
|
|
321
324
|
} catch (err) {
|
|
322
|
-
|
|
325
|
+
log.error('[gds] GET /builders/directory', err);
|
|
323
326
|
res.fail('directory_failed', { status: 500, message: 'internal error' });
|
|
324
327
|
}
|
|
325
328
|
});
|
|
@@ -348,7 +351,7 @@ module.exports = function buildBuildersRouter() {
|
|
|
348
351
|
const builders = await lc().gradeByBuilder(days);
|
|
349
352
|
res.json({ window_days: days, builders });
|
|
350
353
|
} catch (err) {
|
|
351
|
-
|
|
354
|
+
log.error('[gds] GET /grades/by-builder', err);
|
|
352
355
|
res.fail('grades_by_builder_failed', { status: 500, message: 'internal error' });
|
|
353
356
|
}
|
|
354
357
|
});
|
|
@@ -377,7 +380,7 @@ module.exports = function buildBuildersRouter() {
|
|
|
377
380
|
const claims = await lc().getActiveClaimsForBuilderWithStale(targetId, { staleHours });
|
|
378
381
|
res.json({ builder_id: targetId, claims, stale_hours: staleHours });
|
|
379
382
|
} catch (err) {
|
|
380
|
-
|
|
383
|
+
log.error('[gds] GET /builders/:id/active-claims', err);
|
|
381
384
|
res.fail('active_claims_failed', { status: 500, message: 'internal error' });
|
|
382
385
|
}
|
|
383
386
|
});
|
|
@@ -432,7 +435,7 @@ module.exports = function buildBuildersRouter() {
|
|
|
432
435
|
res.set('Cache-Control', 'private, max-age=10');
|
|
433
436
|
res.json({ builder_id: targetId, onboarding: shaped });
|
|
434
437
|
} catch (err) {
|
|
435
|
-
|
|
438
|
+
log.error('[gds] GET /builders/:id/onboarding', err);
|
|
436
439
|
res.fail('onboarding_read_failed', { status: 500, message: 'internal error' });
|
|
437
440
|
}
|
|
438
441
|
});
|
|
@@ -513,7 +516,7 @@ module.exports = function buildBuildersRouter() {
|
|
|
513
516
|
achievements,
|
|
514
517
|
});
|
|
515
518
|
} catch (err) {
|
|
516
|
-
|
|
519
|
+
log.error('[gds] GET /builders/:id/profile', err);
|
|
517
520
|
res.fail('profile_read_failed', { status: 500, message: 'internal error' });
|
|
518
521
|
}
|
|
519
522
|
});
|
|
@@ -549,7 +552,7 @@ module.exports = function buildBuildersRouter() {
|
|
|
549
552
|
});
|
|
550
553
|
res.json({ change, greeting });
|
|
551
554
|
} catch (err) {
|
|
552
|
-
|
|
555
|
+
log.error('[gds] GET /builders/me/pending-rank-change', err);
|
|
553
556
|
res.fail('pending_rank_change_failed', { status: 500, message: 'internal error' });
|
|
554
557
|
}
|
|
555
558
|
});
|
|
@@ -19,6 +19,9 @@ const { clientBranding, branding } = require('../../branding');
|
|
|
19
19
|
const { clientModules } = require('../../modules');
|
|
20
20
|
const { publicReadRateLimitMiddleware } = require('../middleware/rate-limit');
|
|
21
21
|
const { parseId, corsPublicGet, parsePagination, pageMeta } = require('./_helpers');
|
|
22
|
+
// task 1003208: structured logging — was console.*. A core file logs through
|
|
23
|
+
// the kernel logger directly; only MODULES go via api.logger (the doorway).
|
|
24
|
+
const log = require('../logger').logger.child({ src: 'public' });
|
|
22
25
|
|
|
23
26
|
// Pure projection for the single-task resolver. Exported for unit tests.
|
|
24
27
|
// Privacy gate: a full title/value_summary is exposed ONLY for already-public
|
|
@@ -282,7 +285,7 @@ module.exports = function buildPublicRouter() {
|
|
|
282
285
|
res.set('Cache-Control', 'public, max-age=30');
|
|
283
286
|
res.json({ services: result });
|
|
284
287
|
} catch (err) {
|
|
285
|
-
|
|
288
|
+
log.error('[gds] GET /public/uptime', err);
|
|
286
289
|
res.fail('uptime_failed', 500);
|
|
287
290
|
}
|
|
288
291
|
});
|
|
@@ -355,7 +358,7 @@ module.exports = function buildPublicRouter() {
|
|
|
355
358
|
trend: trendRows.rows,
|
|
356
359
|
});
|
|
357
360
|
} catch (err) {
|
|
358
|
-
|
|
361
|
+
log.error('[gds] GET /public/repo-health', err);
|
|
359
362
|
res.fail('repo_health_failed', 500);
|
|
360
363
|
}
|
|
361
364
|
});
|
|
@@ -394,7 +397,7 @@ module.exports = function buildPublicRouter() {
|
|
|
394
397
|
res.set('Cache-Control', 'public, max-age=60');
|
|
395
398
|
res.json({ task: projectPublicTask(task) });
|
|
396
399
|
} catch (err) {
|
|
397
|
-
|
|
400
|
+
log.error('[gds] GET /public/tasks/:id', err);
|
|
398
401
|
res.fail('task_resolve_failed', 500);
|
|
399
402
|
}
|
|
400
403
|
});
|
|
@@ -415,7 +418,7 @@ module.exports = function buildPublicRouter() {
|
|
|
415
418
|
res.set('Cache-Control', 'public, max-age=60');
|
|
416
419
|
res.json({ idea: projectPublicIdea(rows[0]) });
|
|
417
420
|
} catch (err) {
|
|
418
|
-
|
|
421
|
+
log.error('[gds] GET /public/ideas/:id', err);
|
|
419
422
|
res.fail('idea_resolve_failed', 500);
|
|
420
423
|
}
|
|
421
424
|
});
|
|
@@ -432,7 +435,7 @@ module.exports = function buildPublicRouter() {
|
|
|
432
435
|
res.set('Cache-Control', 'public, max-age=60');
|
|
433
436
|
res.json({ blocker: projectPublicBlocker(rows[0]) });
|
|
434
437
|
} catch (err) {
|
|
435
|
-
|
|
438
|
+
log.error('[gds] GET /public/blockers/:id', err);
|
|
436
439
|
res.fail('blocker_resolve_failed', 500);
|
|
437
440
|
}
|
|
438
441
|
});
|
|
@@ -461,7 +464,7 @@ module.exports = function buildPublicRouter() {
|
|
|
461
464
|
blockers: blockers.rows.map((r) => Number(r.id)),
|
|
462
465
|
});
|
|
463
466
|
} catch (err) {
|
|
464
|
-
|
|
467
|
+
log.error('[gds] GET /public/ref-ids', err);
|
|
465
468
|
res.fail('ref_ids_failed', 500);
|
|
466
469
|
}
|
|
467
470
|
});
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
|
|
11
11
|
const http = require('node:http');
|
|
12
12
|
const { pool } = require('./pool');
|
|
13
|
+
// task 1003208: structured logging — was console.*. A core file logs through
|
|
14
|
+
// the kernel logger directly; only MODULES go via api.logger (the doorway).
|
|
15
|
+
const log = require('./logger').logger.child({ src: 'uptime-poller' });
|
|
13
16
|
|
|
14
17
|
const INTERVAL_MS = 60_000;
|
|
15
18
|
const TIMEOUT_MS = 8_000;
|
|
@@ -65,26 +68,26 @@ async function runChecks() {
|
|
|
65
68
|
[svc.name, status, latency ?? null]
|
|
66
69
|
);
|
|
67
70
|
} catch (err) {
|
|
68
|
-
|
|
71
|
+
log.error(`[uptime] check failed for ${svc.name}:`, err.message);
|
|
69
72
|
}
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
// Prune old rows (fire-and-forget)
|
|
73
76
|
pool.query(
|
|
74
77
|
`DELETE FROM uptime_checks WHERE checked_at < now() - interval '${PRUNE_DAYS} days'`
|
|
75
|
-
).catch((err) =>
|
|
78
|
+
).catch((err) => log.error('[uptime] prune error:', err.message));
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
function start() {
|
|
79
82
|
// Stagger first check by 10s so the server finishes binding before we hit it
|
|
80
83
|
setTimeout(() => {
|
|
81
|
-
runChecks().catch((err) =>
|
|
84
|
+
runChecks().catch((err) => log.error('[uptime] initial check error:', err.message));
|
|
82
85
|
setInterval(() => {
|
|
83
|
-
runChecks().catch((err) =>
|
|
86
|
+
runChecks().catch((err) => log.error('[uptime] check error:', err.message));
|
|
84
87
|
}, INTERVAL_MS);
|
|
85
88
|
}, 10_000);
|
|
86
89
|
|
|
87
|
-
|
|
90
|
+
log.info('[uptime] poller started — checking every 60s');
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
module.exports = { start };
|
package/src/module-api.js
CHANGED
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
// modules behind kernel-composed auth is BV1.R41 / task 1410.
|
|
20
20
|
|
|
21
21
|
const auth = require('./bongos/auth');
|
|
22
|
+
const util = require('node:util');
|
|
23
|
+
// task 1003208: api.logger() returns a real pino child from here (see below).
|
|
24
|
+
const { logger: coreLogger } = require('./bongos/logger');
|
|
22
25
|
const { pool, instanceDbName } = require('./bongos/pool');
|
|
23
26
|
const branding = require('./branding');
|
|
24
27
|
const instanceConfig = require('./instance-config');
|
|
@@ -1099,18 +1102,53 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
1099
1102
|
// landed since 1.19.334 with no explicit bump. run 32795232997. (task 1002620)
|
|
1100
1103
|
// 1.19.336 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1101
1104
|
// landed since 1.19.335 with no explicit bump. run 32795564021. (task 1002620)
|
|
1105
|
+
// 1.19.337 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1106
|
+
// landed since 1.19.336 with no explicit bump. run 32795896926. (task 1002620)
|
|
1107
|
+
// 1.19.338 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1108
|
+
// landed since 1.19.337 with no explicit bump. run 32796733697. (task 1002620)
|
|
1102
1109
|
// ---------------------------------------------------------------------------
|
|
1103
|
-
const CORE_VERSION = '1.19.
|
|
1110
|
+
const CORE_VERSION = '1.19.338'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
1104
1111
|
|
|
1105
1112
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
1106
1113
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
1114
|
+
// task 1003208 (goal 1000079): this USED TO BE `console.log(tag, ...a)`. Every
|
|
1115
|
+
// module that had "adopted the logger" was therefore still writing unstructured
|
|
1116
|
+
// text to stdout with no request correlation and no redaction — the criterion's
|
|
1117
|
+
// complaint ("used across the server paths, not just installed") was true even of
|
|
1118
|
+
// the files that looked migrated. It now returns a real pino child from
|
|
1119
|
+
// src/bongos/logger.js, so every existing call site became structured,
|
|
1120
|
+
// correlated and redacted without one of them changing.
|
|
1121
|
+
//
|
|
1122
|
+
// CONSOLE-COMPATIBLE ON PURPOSE. Call sites were written against console's
|
|
1123
|
+
// signature — `log.warn('failed:', err.message)` — and pino's is
|
|
1124
|
+
// `(mergingObject, message)`, which silently DROPS console-style extra args. So:
|
|
1125
|
+
// * OBJECT arguments are merged into the record, never formatted into the
|
|
1126
|
+
// message. That is what makes redaction work: pino censors by FIELD PATH, so
|
|
1127
|
+
// `log.info('token was', { token: 'abc' })` logs token:"[Redacted]", where
|
|
1128
|
+
// flattening to a string would have written the secret out verbatim.
|
|
1129
|
+
// * everything else goes through util.format — exactly what console does — so
|
|
1130
|
+
// the message text is unchanged.
|
|
1131
|
+
// * an Error is passed as `err` so pino serialises its stack and type.
|
|
1132
|
+
// A secret INTERPOLATED into a message string is still not redactable here; that
|
|
1133
|
+
// is the scrubber's and the prevent hook's job, as logger.js's own header says.
|
|
1107
1134
|
function logger(moduleKey) {
|
|
1108
|
-
const
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1135
|
+
const child = coreLogger.child({ module: moduleKey || 'module' });
|
|
1136
|
+
const emit = (level) => (...args) => {
|
|
1137
|
+
if (args.length === 0) return child[level]('');
|
|
1138
|
+
const bindings = {};
|
|
1139
|
+
const rest = [];
|
|
1140
|
+
let hasBindings = false;
|
|
1141
|
+
for (const a of args) {
|
|
1142
|
+
if (a instanceof Error) { bindings.err = a; hasBindings = true; continue; }
|
|
1143
|
+
if (a !== null && typeof a === 'object' && !Array.isArray(a)) {
|
|
1144
|
+
Object.assign(bindings, a); hasBindings = true; continue;
|
|
1145
|
+
}
|
|
1146
|
+
rest.push(a);
|
|
1147
|
+
}
|
|
1148
|
+
const msg = rest.length ? util.format(...rest) : '';
|
|
1149
|
+
return hasBindings ? child[level](bindings, msg) : child[level](msg);
|
|
1113
1150
|
};
|
|
1151
|
+
return { info: emit('info'), warn: emit('warn'), error: emit('error'), debug: emit('debug') };
|
|
1114
1152
|
}
|
|
1115
1153
|
|
|
1116
1154
|
module.exports = {
|
package/tests/logger.mjs
CHANGED
|
@@ -87,3 +87,70 @@ test('requestIdMiddleware adopts a valid inbound id and echoes it; mints otherwi
|
|
|
87
87
|
const huge = mk({ 'x-request-id': 'x'.repeat(500) });
|
|
88
88
|
assert.match(huge.req.reqId, /^[0-9a-f]{16}$/, 'over-long inbound id is not adopted');
|
|
89
89
|
});
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// task 1003208 (goal 1000079) — the DOORWAY logger, which is what server paths
|
|
93
|
+
// actually call.
|
|
94
|
+
//
|
|
95
|
+
// api.logger() used to be `console.log(tag, ...args)`. Every module that had
|
|
96
|
+
// "adopted the logger" was therefore still emitting unstructured text with no
|
|
97
|
+
// correlation and no redaction, so the tests above proved a property of a logger
|
|
98
|
+
// almost nothing in modules/ was using. These run the REAL doorway in a
|
|
99
|
+
// subprocess — pino writes through sonic-boom straight to the fd, so capturing
|
|
100
|
+
// the singleton in-process is not possible, and a re-built copy would prove
|
|
101
|
+
// something about the copy.
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
const { spawnSync } = require('node:child_process');
|
|
104
|
+
|
|
105
|
+
function runDoorway(body) {
|
|
106
|
+
const r = spawnSync(process.execPath, ['-e', `
|
|
107
|
+
const api = require(${JSON.stringify(path.join(ROOT, 'src', 'module-api.js'))});
|
|
108
|
+
const log = api.logger('probe');
|
|
109
|
+
${body}
|
|
110
|
+
`], { encoding: 'utf8' });
|
|
111
|
+
assert.equal(r.status, 0, `doorway probe exited ${r.status}: ${r.stderr}`);
|
|
112
|
+
return r.stdout.split('\n').filter((l) => l.trim().startsWith('{')).map((l) => JSON.parse(l));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
test('api.logger emits STRUCTURED json, not console text (the 1003208 regression)', () => {
|
|
116
|
+
const [line] = runDoorway(`log.info('mounted');`);
|
|
117
|
+
assert.equal(line.msg, 'mounted');
|
|
118
|
+
assert.equal(line.module, 'probe', 'the namespace is a field, not a "[tag]" prefix');
|
|
119
|
+
assert.equal(typeof line.level, 'number');
|
|
120
|
+
assert.ok(line.time, 'carries a timestamp');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('a secret handed to api.logger console-style is redacted in the emitted line', () => {
|
|
124
|
+
const [line] = runDoorway(`log.info('storing credentials', { token: 'PLANTED-SECRET-VALUE', ok: true });`);
|
|
125
|
+
assert.equal(line.token, '[Redacted]', 'the secret-bearing FIELD is censored');
|
|
126
|
+
assert.equal(line.ok, true, 'the neighbouring field survives');
|
|
127
|
+
assert.equal(line.msg, 'storing credentials');
|
|
128
|
+
assert.ok(!JSON.stringify(line).includes('PLANTED-SECRET-VALUE'),
|
|
129
|
+
'the planted secret appears nowhere in the record');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('object args MERGE rather than being formatted into the message', () => {
|
|
133
|
+
// This is the property redaction depends on: pino censors by field path, so an
|
|
134
|
+
// object flattened into the message string would emit the secret verbatim.
|
|
135
|
+
const [line] = runDoorway(`log.warn('ctx', { password: 'hunter2' }, 'and more');`);
|
|
136
|
+
assert.equal(line.password, '[Redacted]');
|
|
137
|
+
assert.equal(line.msg, 'ctx and more', 'non-object args still format exactly as console did');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('an Error arg is serialised, not stringified into the message', () => {
|
|
141
|
+
const [line] = runDoorway(`log.error('failed', new Error('boom'));`);
|
|
142
|
+
assert.equal(line.msg, 'failed');
|
|
143
|
+
assert.equal(line.err.type, 'Error');
|
|
144
|
+
assert.equal(line.err.message, 'boom');
|
|
145
|
+
assert.ok(line.err.stack, 'the stack is kept as a field');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('a secret INTERPOLATED into the message is NOT redacted — the honest boundary', () => {
|
|
149
|
+
// Documented rather than pretended: pino redacts by field path, so a secret
|
|
150
|
+
// pasted into free text is the scrubber's and the prevent hook's job. Asserting
|
|
151
|
+
// it here stops anyone reading the tests above and concluding the logger alone
|
|
152
|
+
// makes a leak impossible.
|
|
153
|
+
const [line] = runDoorway(`log.info('token=' + 'PLANTED-SECRET-VALUE');`);
|
|
154
|
+
assert.ok(line.msg.includes('PLANTED-SECRET-VALUE'),
|
|
155
|
+
'interpolated secrets survive — redaction is field-path based, by design');
|
|
156
|
+
});
|
|
@@ -11,10 +11,13 @@
|
|
|
11
11
|
|
|
12
12
|
import { strict as assert } from 'node:assert';
|
|
13
13
|
import { createRequire } from 'node:module';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
14
15
|
import http from 'node:http';
|
|
15
16
|
|
|
16
17
|
const require = createRequire(import.meta.url);
|
|
17
|
-
|
|
18
|
+
// fileURLToPath, not .pathname: a checkout path with a space reads back percent-encoded
|
|
19
|
+
// (and drive-prefixed) from .pathname on Windows, and require() cannot follow it
|
|
20
|
+
const ROOT = fileURLToPath(new URL('..', import.meta.url));
|
|
18
21
|
|
|
19
22
|
// Stub the data-access fns BEFORE the route factory captures the module object.
|
|
20
23
|
const provisioning = require(`${ROOT}/modules/provisioning/provisioning.js`);
|
|
@@ -92,6 +95,42 @@ console.log('\nGET /provisioning/github-app/callback (the public GitHub redirect
|
|
|
92
95
|
const html = await res.text();
|
|
93
96
|
t('good state → 200 text/html progress page wired to auto-open', res.status === 200 && /text\/html/.test(res.headers.get('content-type') || '') && /Setting up your instance/.test(html) && /Placing credentials/.test(html) && /__CB_SETUP__/.test(html) && html.includes('GOODSTATE'), `status=${res.status}`);
|
|
94
97
|
t('the page names the instance host', /mercury\.example\.com/.test(html));
|
|
98
|
+
// task 1003236: a same-tab return (pop-up blocked → form target=_self) must never be
|
|
99
|
+
// navigated away from the checklist. The page carries a hidden way back, and the
|
|
100
|
+
// countdown sits behind the pop-up guard.
|
|
101
|
+
t('carries a hidden "finish your remaining setup steps" link back to the wizard', /<p class="muted hidden" id="back"><a href="\/projects\?view=new">Finish your remaining setup steps/.test(html));
|
|
102
|
+
t('the auto-open countdown is reachable only past the pop-up guard', /var popup=!!window\.opener\|\|window\.name==='ghAppSetup';/.test(html) && /if\(!popup\)\{[^}]*return;\}\s*var n=4;\(function tick\(\)\{if\(n<=0\)\{location\.href=target;/.test(html) && (html.match(/location\.href=target/g) || []).length === 1);
|
|
103
|
+
|
|
104
|
+
// Run the served script itself (not a copy) against a minimal DOM, once as the
|
|
105
|
+
// wizard's pop-up and once as a same-tab return, and watch what it does to location.
|
|
106
|
+
const script = (html.match(/<script>\s*(\(function\(\)\{[\s\S]*?)<\/script>\s*<\/body>/) || [])[1];
|
|
107
|
+
t('the progress page ships the client script', typeof script === 'string' && script.length > 200);
|
|
108
|
+
async function runCallback({ opener, name }) {
|
|
109
|
+
const vm = await import('node:vm');
|
|
110
|
+
const el = () => { const cls = new Set(); return { textContent: '', classList: { add: (...c) => c.forEach((x) => cls.add(x)), remove: (...c) => c.forEach((x) => cls.delete(x)), contains: (c) => cls.has(c) }, querySelector: () => el() }; };
|
|
111
|
+
const els = { steps: el(), status: el(), cta: el(), go: el(), foot: el(), back: el() };
|
|
112
|
+
els.steps.querySelector = () => el();
|
|
113
|
+
els.back.classList.add('hidden'); els.cta.classList.add('hidden');
|
|
114
|
+
const timers = [];
|
|
115
|
+
const sandbox = {
|
|
116
|
+
__CB_SETUP__: { state: 'GOODSTATE', domain: 'mercury.example.com' },
|
|
117
|
+
opener, name,
|
|
118
|
+
document: { getElementById: (id) => els[id] || null },
|
|
119
|
+
location: { pathname: '/api/bongos/provisioning/github-app/callback', href: 'about:callback' },
|
|
120
|
+
fetch: async () => ({ ok: true, json: async () => ({ ok: true, ready: true, status: 'done' }) }),
|
|
121
|
+
setTimeout: (fn) => { timers.push(fn); return timers.length; },
|
|
122
|
+
encodeURIComponent,
|
|
123
|
+
};
|
|
124
|
+
sandbox.window = sandbox;
|
|
125
|
+
vm.runInNewContext(script, sandbox);
|
|
126
|
+
for (let i = 0; i < 5; i++) await new Promise((r) => setImmediate(r)); // let the poll's promise chain settle
|
|
127
|
+
for (let guard = 0; guard < 10 && timers.length; guard++) { const fn = timers.shift(); fn(); }
|
|
128
|
+
return { href: sandbox.location.href, go: els.go.textContent, backHidden: els.back.classList.contains('hidden'), foot: els.foot.textContent };
|
|
129
|
+
}
|
|
130
|
+
const popup = await runCallback({ opener: {}, name: 'ghAppSetup' });
|
|
131
|
+
t('as the wizard’s pop-up it still counts down and opens the instance', popup.href === 'https://mercury.example.com', JSON.stringify(popup));
|
|
132
|
+
const sameTab = await runCallback({ opener: null, name: '' });
|
|
133
|
+
t('as a same-tab return it stays put: door shown, way back revealed, no navigation', sameTab.href === 'about:callback' && sameTab.go === 'Open mercury.example.com →' && sameTab.backHidden === false && /checklist is waiting/.test(sameTab.foot), JSON.stringify(sameTab));
|
|
95
134
|
}
|
|
96
135
|
{
|
|
97
136
|
const res = await fetch(`${base}/provisioning/github-app/callback?code=X&state=NOPE`);
|