[grade=B urn:ump:3wvdd3yegylr7e2pzn6tebyq5bisf2awmss7p4j3uwpwsujtnwoq] DC-134/135/136: Shipdeck integration - data-driven login pages, deploy events, badge suppression

DC-134: /api/v1/auth/login-page serves a generic gated auto-login page for
any service registered in services.json without a curated flow (App Selector
installs, DC-131 git installs). Curated pages always win; unknown services
still 404; sanitizer keeps digits/hyphens (shipdeck-style ids); display
names HTML-escaped. Kills the sso-gate.js edit + restart per new install.

DC-135: shipdeck journal.jsonl tail worker (startShipdeckWorker) ingests
deploy/rollback lifecycle rows into the Security Center as
source_type=shipdeck (notice/success, error/failure via verify[] block).

DC-136: deploy-aware badge suppression - suppressDuringDeploy() before the
bridge call in /deploy and /rollback, clearDeploySuppression() in finally
(every exit path incl. rejected fetches), reference-counted for overlapping
deploys, 10-min TTL auto-expiry (HEALTH_DEPLOY_SUPPRESS_MAX_MS).

23 new tests across 4 suites; full suite 2923/2923 green.

Codex judge: C (r1) -> C (r2) -> B (r3) -> B zero-blockers (r4,
urn:ump:3wvdd3yegylr7e2pzn6tebyq5bisf2awmss7p4j3uwpwsujtnwoq).
This commit is contained in:
DashCaddy Polish Loop
2026-09-16 03:10:54 -07:00
parent 0dd8493f98
commit 11c719e635
10 changed files with 912 additions and 11 deletions
+45 -6
View File
@@ -267,14 +267,22 @@ module.exports = function(deps) {
});
// Serve service-specific auto-login page (auth enforced by Caddy forward_auth upstream)
router.get('/auth/login-page', (req, res) => {
const service = (req.query.service || '').replace(/[^a-z]/g, '');
router.get('/auth/login-page', asyncHandler(async (req, res) => {
// DC-134: ids may contain digits and hyphens (shipdeck installs like
// demo-hi3) — keep them, strip everything else. The value is only ever
// compared against the curated page keys and service ids.
const service = (req.query.service || '').replace(/[^a-z0-9-]/g, '');
const configuredHost = siteConfig?.dashboardHost;
const dashboardOrigin = typeof configuredHost === 'string'
&& /^[a-zA-Z0-9][a-zA-Z0-9.-]*$/.test(configuredHost)
? `https://${configuredHost}`
: 'https://status.sami';
const html = buildLoginPage(service, dashboardOrigin);
// DC-134: read the live services list so any registered service without a
// curated auto-login flow still gets a gated generic login page instead
// of a 404. Read failure falls back to curated-only behavior.
let services = null;
try { services = await servicesStateManager.read(); } catch (_) { services = null; }
const html = buildLoginPage(service, dashboardOrigin, services);
if (!html) return res.status(404).send('Unknown service');
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.setHeader('Cache-Control', 'no-store');
@@ -287,12 +295,12 @@ module.exports = function(deps) {
// one response only; every other route keeps the strict app-wide policy.
res.setHeader('Content-Security-Policy', "default-src 'self'; style-src 'self'; script-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self'; font-src 'self' data:; object-src 'none'; media-src 'self'; frame-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'");
res.send(html);
});
}));
return router;
};
function buildLoginPage(service, dashboardOrigin = 'https://status.sami') {
function buildLoginPage(service, dashboardOrigin = 'https://status.sami', services = null) {
// Pre-auth check via <meta http-equiv="refresh"> so it fires even when JS is
// disabled or blocked. The cookie is sent automatically because we hit the
// same origin (plex.sami); if the API returns 200 the user has a valid
@@ -401,7 +409,38 @@ ft('chat').then(function(r){return r.text()}).then(function(t){
};
const cfg = pages[service];
if (!cfg) return null;
if (!cfg) {
// DC-134: data-driven fallback. Any service registered in services.json
// (App Selector install, DC-131 git install, UI add) gets a generic gated
// auto-login page — session was already verified by the SHELL above, so
// the body just enters the app the same way the `sec` page does.
// ?direct=1 bypasses the Caddy @needsAutoLogin redirect loop. Curated
// pages above always win; unknown services still 404 below.
const registered = Array.isArray(services) &&
services.some(s => s && (s.id === service || s.subdomain === service));
if (registered) {
const name = (() => {
const s = services.find(x => x && (x.id === service || x.subdomain === service));
const raw = (s && typeof s.name === 'string' && s.name) || service;
// HTML-safe: the title is interpolated into the page shell.
return String(raw).replace(/[&<>"']/g, c => (
{ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]
));
})();
const fallback = {
title: `Signing in to ${name}...`,
bg: '#0a0a0a',
accent: '#60a5fa',
body: `d.textContent='Session verified, opening dashboard...';go('/?direct=1');`,
};
return SHELL(fallback.body)
.replace(/__TITLE__/g, fallback.title)
.replace('__BG__', fallback.bg)
.replace('__ACCENT__', fallback.accent)
.replace('__DASHBOARD_ORIGIN__', JSON.stringify(dashboardOrigin));
}
return null;
}
return SHELL(cfg.body)
.replace(/__TITLE__/g, cfg.title)
.replace('__BG__', cfg.bg)