diff --git a/frontend/feature-loader.js b/frontend/feature-loader.js
index 34f0559..1a21b87 100644
--- a/frontend/feature-loader.js
+++ b/frontend/feature-loader.js
@@ -47,7 +47,8 @@ function createFeatureLoader({ document, urls, timeoutMs = 10000 }) {
} catch (_error) {
if (status) {
const label = name === 'issue-capture' ? 'Issue capture' :
- name === 'pull-workflow' ? 'Pull workspace' : name.replace(/-/g, ' ');
+ name === 'pull-workflow' ? 'Pull workspace' :
+ name === 'security-center' ? 'Security center' : name.replace(/-/g, ' ');
status.textContent = label + ' could not load. ' + retryLabel;
}
return false;
diff --git a/frontend/index.html b/frontend/index.html
index d66e605..de52bb2 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -831,6 +831,7 @@
+
diff --git a/frontend/security-center.js b/frontend/security-center.js
new file mode 100644
index 0000000..2a91e3d
--- /dev/null
+++ b/frontend/security-center.js
@@ -0,0 +1,185 @@
+(function (root, factory) {
+ if (typeof module !== 'undefined' && module.exports) module.exports = factory;
+ else root.attachSecurityCenter = boundary => factory({ root, boundary });
+})(typeof window !== 'undefined' ? window : this, function attachSecurityCenter({ root, boundary }) {
+ const devicesButton = root.document.getElementById('active-devices');
+ const devicesSheet = root.document.getElementById('active-devices-sheet');
+ const devicesList = root.document.getElementById('active-devices-list');
+ const devicesStatus = root.document.getElementById('active-devices-status');
+ const passkeysList = root.document.getElementById('enrolled-passkeys-list');
+ const passkeysStatus = root.document.getElementById('enrolled-passkeys-status');
+ const enrollPasskey = root.document.getElementById('enroll-passkey');
+ const activityList = root.document.getElementById('security-activity-list');
+ const activityStatus = root.document.getElementById('security-activity-status');
+ const loadMoreActivity = root.document.getElementById('load-more-security-activity');
+ let activityCursor = null;
+
+ const renderDevices = async () => {
+ devicesStatus.textContent = 'Loading active devices…';
+ devicesList.replaceChildren();
+ try {
+ const devices = await boundary.listActiveDevices();
+ devices.forEach(device => {
+ const row = root.document.createElement('article');
+ row.className = 'active-device';
+ const details = root.document.createElement('div');
+ const label = root.document.createElement('strong');
+ label.textContent = device.device_label;
+ const timing = root.document.createElement('span');
+ timing.className = 'small muted';
+ timing.textContent = `Signed in ${new Date(device.created_at * 1000).toLocaleString()} · expires ${new Date(device.expires_at * 1000).toLocaleString()}`;
+ details.append(label, timing);
+ if (device.current) {
+ const current = root.document.createElement('span');
+ current.className = 'active-device-current';
+ current.textContent = 'This device';
+ details.append(current);
+ }
+ const revoke = root.document.createElement('button');
+ revoke.type = 'button';
+ revoke.textContent = device.current ? 'Sign out' : 'Revoke';
+ revoke.addEventListener('click', async () => {
+ if (device.current) await boundary.signOut();
+ else if (await boundary.revokeActiveDevice(device)) await renderDevices();
+ });
+ row.append(details, revoke);
+ devicesList.append(row);
+ });
+ devicesStatus.textContent = devices.length ? `${devices.length} active device${devices.length === 1 ? '' : 's'}` : 'No active devices.';
+ } catch (_error) {
+ devicesStatus.textContent = 'Active devices could not be loaded. Try again.';
+ }
+ };
+
+ const renderPasskeys = async () => {
+ if (!passkeysList || !passkeysStatus) return;
+ passkeysStatus.textContent = 'Loading enrolled passkeys…';
+ passkeysList.replaceChildren();
+ try {
+ const enrolled = await boundary.listPasskeys();
+ enrolled.forEach(passkey => {
+ const row = root.document.createElement('article');
+ row.className = 'enrolled-passkey';
+ const details = root.document.createElement('div');
+ const label = root.document.createElement('strong');
+ label.textContent = passkey.device_label;
+ const timing = root.document.createElement('span');
+ timing.className = 'small muted';
+ const state = passkey.current ? 'This active device' : (passkey.active ? 'Active device' : 'No active session');
+ timing.textContent = `Enrolled ${new Date(passkey.created_at * 1000).toLocaleString()} · ${state}`;
+ details.append(label, timing);
+ const remove = root.document.createElement('button');
+ remove.type = 'button';
+ remove.textContent = 'Remove passkey';
+ remove.addEventListener('click', async () => {
+ remove.disabled = true;
+ passkeysStatus.textContent = `Removing passkey for ${passkey.device_label}…`;
+ try {
+ const outcome = await boundary.revokePasskey(passkey);
+ if (outcome) {
+ passkeysStatus.textContent = outcome.current_session
+ ? 'Passkey removed. This session remains active; keep your recovery token available.'
+ : 'Passkey removed.';
+ await renderPasskeys();
+ if (outcome.session_revoked) await renderDevices();
+ } else {
+ remove.disabled = false;
+ passkeysStatus.textContent = `${enrolled.length} enrolled passkey${enrolled.length === 1 ? '' : 's'}`;
+ }
+ } catch (_error) {
+ passkeysStatus.textContent = 'Passkey could not be removed. Refresh to verify before retrying.';
+ remove.disabled = false;
+ }
+ });
+ row.append(details, remove);
+ passkeysList.append(row);
+ });
+ passkeysStatus.textContent = enrolled.length
+ ? `${enrolled.length} enrolled passkey${enrolled.length === 1 ? '' : 's'}`
+ : 'No passkeys enrolled.';
+ } catch (_error) {
+ passkeysStatus.textContent = 'Enrolled passkeys could not be loaded. Try again.';
+ }
+ };
+
+ const renderSecurityActivity = async (append = false) => {
+ if (!activityList || !activityStatus || !loadMoreActivity) return;
+ activityStatus.textContent = append ? 'Loading older activity…' : 'Loading security activity…';
+ loadMoreActivity.hidden = true;
+ if (!append) {
+ activityCursor = null;
+ activityList.replaceChildren();
+ }
+ try {
+ const page = await boundary.listSecurityEvents(append ? activityCursor : null);
+ if (!append) {
+ page.authentication_alerts.forEach(alert => {
+ const row = root.document.createElement('article');
+ row.className = 'security-event authentication-alert';
+ const formatted = boundary.formatAuthenticationAlert(alert);
+ const title = root.document.createElement('strong');
+ title.textContent = formatted.title;
+ const details = root.document.createElement('span');
+ details.className = 'small muted';
+ details.textContent = formatted.detail;
+ row.append(title, details);
+ activityList.append(row);
+ });
+ }
+ const labels = {
+ sign_in: 'Signed in', sign_out: 'Signed out', passkey_enrolled: 'Passkey enrolled',
+ device_revoked: 'Device access revoked', all_sessions_revoked: 'All device access revoked',
+ issue_closed: 'Issue closed', pull_merged: 'Pull request merged',
+ pull_review_approved: 'Pull request approved',
+ pull_review_changes_requested: 'Changes requested', gitea_time_logged: 'Gitea time logged',
+ };
+ page.events.forEach(event => {
+ const row = root.document.createElement('article');
+ row.className = 'security-event';
+ const title = root.document.createElement('strong');
+ title.textContent = labels[event.kind] || 'Security event';
+ const details = root.document.createElement('span');
+ details.className = 'small muted';
+ const context = [event.device_label, event.method, event.target,
+ event.status === 'pending' ? 'Outcome confirmation pending' : null]
+ .filter(value => typeof value === 'string' && value).join(' · ');
+ details.textContent = `${new Date(event.created_at * 1000).toLocaleString()}${context ? ' · ' + context : ''}`;
+ row.append(title, details);
+ activityList.append(row);
+ });
+ activityCursor = page.next_cursor;
+ activityStatus.textContent = activityList.children.length
+ ? `${activityList.children.length} recent security event${activityList.children.length === 1 ? '' : 's'}`
+ : 'No security activity yet.';
+ loadMoreActivity.textContent = 'Load older activity';
+ loadMoreActivity.hidden = !activityCursor;
+ } catch (_error) {
+ activityStatus.textContent = 'Security activity could not be loaded.';
+ loadMoreActivity.textContent = 'Retry activity';
+ loadMoreActivity.hidden = false;
+ }
+ };
+
+ const open = () => {
+ if (!devicesSheet) return;
+ devicesSheet.hidden = false;
+ root.document.getElementById('close-active-devices')?.focus();
+ return Promise.all([renderDevices(), renderPasskeys(), renderSecurityActivity()]);
+ };
+ devicesButton?.addEventListener('click', open);
+ loadMoreActivity?.addEventListener('click', () => renderSecurityActivity(Boolean(activityCursor)));
+ enrollPasskey?.addEventListener('click', async () => {
+ enrollPasskey.disabled = true;
+ devicesStatus.textContent = 'Waiting for your device passkey…';
+ try {
+ await boundary.enrollPasskey();
+ devicesStatus.textContent = 'Passkey enrolled. You can use it at sign-in and authorization prompts.';
+ await renderPasskeys();
+ } catch (_error) {
+ devicesStatus.textContent = 'Passkey enrollment was not completed. Try again.';
+ } finally {
+ enrollPasskey.disabled = false;
+ }
+ });
+ return { open, renderDevices, renderPasskeys, renderSecurityActivity };
+});
diff --git a/frontend/service-worker.js b/frontend/service-worker.js
index a7db818..b7efc84 100644
--- a/frontend/service-worker.js
+++ b/frontend/service-worker.js
@@ -14,6 +14,7 @@ const SHELL = [
BASE + 'static/icons/stackchain-512.png',
BASE + 'static/session.js',
BASE + 'static/feature-loader.js',
+ BASE + 'static/security-center.js',
BASE + 'static/markdown.js',
BASE + 'static/commands.js',
BASE + 'static/search-preview.js',
diff --git a/frontend/session.js b/frontend/session.js
index f6564f8..d16452f 100644
--- a/frontend/session.js
+++ b/frontend/session.js
@@ -36,192 +36,42 @@
if (allDevicesButton) allDevicesButton.addEventListener('click', () => boundary.signOutAllDevices());
const devicesButton = root.document.getElementById('active-devices');
const devicesSheet = root.document.getElementById('active-devices-sheet');
- const devicesList = root.document.getElementById('active-devices-list');
const devicesStatus = root.document.getElementById('active-devices-status');
- const passkeysList = root.document.getElementById('enrolled-passkeys-list');
- const passkeysStatus = root.document.getElementById('enrolled-passkeys-status');
const closeDevices = root.document.getElementById('close-active-devices');
- const enrollPasskey = root.document.getElementById('enroll-passkey');
- const activityList = root.document.getElementById('security-activity-list');
- const activityStatus = root.document.getElementById('security-activity-status');
- const loadMoreActivity = root.document.getElementById('load-more-security-activity');
- let activityCursor = null;
- const renderDevices = async () => {
- devicesStatus.textContent = 'Loading active devices…';
- devicesList.replaceChildren();
- try {
- const devices = await boundary.listActiveDevices();
- devices.forEach(device => {
- const row = root.document.createElement('article');
- row.className = 'active-device';
- const details = root.document.createElement('div');
- const label = root.document.createElement('strong');
- label.textContent = device.device_label;
- const timing = root.document.createElement('span');
- timing.className = 'small muted';
- timing.textContent = `Signed in ${new Date(device.created_at * 1000).toLocaleString()} · expires ${new Date(device.expires_at * 1000).toLocaleString()}`;
- details.append(label, timing);
- if (device.current) {
- const current = root.document.createElement('span');
- current.className = 'active-device-current';
- current.textContent = 'This device';
- details.append(current);
- }
- const revoke = root.document.createElement('button');
- revoke.type = 'button';
- revoke.textContent = device.current ? 'Sign out' : 'Revoke';
- revoke.addEventListener('click', async () => {
- if (device.current) await boundary.signOut();
- else if (await boundary.revokeActiveDevice(device)) await renderDevices();
- });
- row.append(details, revoke);
- devicesList.append(row);
- });
- devicesStatus.textContent = devices.length ? `${devices.length} active device${devices.length === 1 ? '' : 's'}` : 'No active devices.';
- } catch (_error) {
- devicesStatus.textContent = 'Active devices could not be loaded. Try again.';
- }
- };
- const renderPasskeys = async () => {
- if (!passkeysList || !passkeysStatus) return;
- passkeysStatus.textContent = 'Loading enrolled passkeys…';
- passkeysList.replaceChildren();
- try {
- const enrolled = await boundary.listPasskeys();
- enrolled.forEach(passkey => {
- const row = root.document.createElement('article');
- row.className = 'enrolled-passkey';
- const details = root.document.createElement('div');
- const label = root.document.createElement('strong');
- label.textContent = passkey.device_label;
- const timing = root.document.createElement('span');
- timing.className = 'small muted';
- const state = passkey.current ? 'This active device' : (passkey.active ? 'Active device' : 'No active session');
- timing.textContent = `Enrolled ${new Date(passkey.created_at * 1000).toLocaleString()} · ${state}`;
- details.append(label, timing);
- const remove = root.document.createElement('button');
- remove.type = 'button';
- remove.textContent = 'Remove passkey';
- remove.addEventListener('click', async () => {
- remove.disabled = true;
- passkeysStatus.textContent = `Removing passkey for ${passkey.device_label}…`;
- try {
- const outcome = await boundary.revokePasskey(passkey);
- if (outcome) {
- passkeysStatus.textContent = outcome.current_session
- ? 'Passkey removed. This session remains active; keep your recovery token available.'
- : 'Passkey removed.';
- await renderPasskeys();
- if (outcome.session_revoked) await renderDevices();
- } else {
- remove.disabled = false;
- passkeysStatus.textContent = `${enrolled.length} enrolled passkey${enrolled.length === 1 ? '' : 's'}`;
- }
- } catch (_error) {
- passkeysStatus.textContent = 'Passkey could not be removed. Refresh to verify before retrying.';
- remove.disabled = false;
- }
- });
- row.append(details, remove);
- passkeysList.append(row);
- });
- passkeysStatus.textContent = enrolled.length
- ? `${enrolled.length} enrolled passkey${enrolled.length === 1 ? '' : 's'}`
- : 'No passkeys enrolled.';
- } catch (_error) {
- passkeysStatus.textContent = 'Enrolled passkeys could not be loaded. Try again.';
- }
- };
- const renderSecurityActivity = async (append = false) => {
- if (!activityList || !activityStatus || !loadMoreActivity) return;
- activityStatus.textContent = append ? 'Loading older activity…' : 'Loading security activity…';
- loadMoreActivity.hidden = true;
- if (!append) {
- activityCursor = null;
- activityList.replaceChildren();
- }
- try {
- const page = await boundary.listSecurityEvents(append ? activityCursor : null);
- if (!append) {
- page.authentication_alerts.forEach(alert => {
- const row = root.document.createElement('article');
- row.className = 'security-event authentication-alert';
- const formatted = boundary.formatAuthenticationAlert(alert);
- const title = root.document.createElement('strong');
- title.textContent = formatted.title;
- const details = root.document.createElement('span');
- details.className = 'small muted';
- details.textContent = formatted.detail;
- row.append(title, details);
- activityList.append(row);
- });
- }
- const labels = {
- sign_in: 'Signed in',
- sign_out: 'Signed out',
- passkey_enrolled: 'Passkey enrolled',
- device_revoked: 'Device access revoked',
- all_sessions_revoked: 'All device access revoked',
- issue_closed: 'Issue closed',
- pull_merged: 'Pull request merged',
- pull_review_approved: 'Pull request approved',
- pull_review_changes_requested: 'Changes requested',
- gitea_time_logged: 'Gitea time logged',
- };
- page.events.forEach(event => {
- const row = root.document.createElement('article');
- row.className = 'security-event';
- const title = root.document.createElement('strong');
- title.textContent = labels[event.kind] || 'Security event';
- const details = root.document.createElement('span');
- details.className = 'small muted';
- const context = [
- event.device_label,
- event.method,
- event.target,
- event.status === 'pending' ? 'Outcome confirmation pending' : null,
- ].filter(value => typeof value === 'string' && value).join(' · ');
- details.textContent = `${new Date(event.created_at * 1000).toLocaleString()}${context ? ' · ' + context : ''}`;
- row.append(title, details);
- activityList.append(row);
- });
- activityCursor = page.next_cursor;
- activityStatus.textContent = activityList.children.length
- ? `${activityList.children.length} recent security event${activityList.children.length === 1 ? '' : 's'}`
- : 'No security activity yet.';
- loadMoreActivity.textContent = 'Load older activity';
- loadMoreActivity.hidden = !activityCursor;
- } catch (_error) {
- activityStatus.textContent = 'Security activity could not be loaded.';
- loadMoreActivity.textContent = 'Retry activity';
- loadMoreActivity.hidden = false;
- }
- };
- loadMoreActivity?.addEventListener('click', () => renderSecurityActivity(Boolean(activityCursor)));
- if (devicesButton && devicesSheet) devicesButton.addEventListener('click', () => {
+ const securityFeatures = root.createFeatureLoader({
+ document: root.document,
+ urls: {
+ 'security-center': root.document.querySelector(
+ 'meta[name="stackchain-feature-security-center"]'
+ )?.content || '',
+ },
+ });
+ let loadingSecurityCenter = false;
+ const openSecurityCenter = async () => {
+ if (loadingSecurityCenter) return;
+ loadingSecurityCenter = true;
devicesSheet.hidden = false;
closeDevices?.focus();
- renderDevices();
- renderPasskeys();
- renderSecurityActivity();
- });
+ try {
+ await securityFeatures.run('security-center', {
+ trigger: devicesButton,
+ status: devicesStatus,
+ retryLabel: 'Tap Active devices to retry.',
+ }, () => {
+ devicesButton.removeEventListener('click', openSecurityCenter);
+ root.attachSecurityCenter(boundary).open();
+ });
+ } finally {
+ loadingSecurityCenter = false;
+ }
+ };
+ if (devicesButton && devicesSheet) {
+ devicesButton.addEventListener('click', openSecurityCenter);
+ }
if (closeDevices && devicesSheet) closeDevices.addEventListener('click', () => {
devicesSheet.hidden = true;
devicesButton?.focus();
});
- enrollPasskey?.addEventListener('click', async () => {
- enrollPasskey.disabled = true;
- devicesStatus.textContent = 'Waiting for your device passkey…';
- try {
- await boundary.enrollPasskey();
- devicesStatus.textContent = 'Passkey enrolled. You can use it at sign-in and authorization prompts.';
- await renderPasskeys();
- } catch (_error) {
- devicesStatus.textContent = 'Passkey enrollment was not completed. Try again.';
- } finally {
- enrollPasskey.disabled = false;
- }
- });
boundary.refreshOfflineLease().then(valid => {
if (valid) boundary.resumeQueuedWork();
});
diff --git a/src/frontend_bundle.py b/src/frontend_bundle.py
index 19876a6..49c8bff 100644
--- a/src/frontend_bundle.py
+++ b/src/frontend_bundle.py
@@ -26,6 +26,7 @@ FEATURE_SOURCES = {
"pull-workflow": ("static/pull-sheet.js", "static/review-sheet.js"),
"push-notifications": ("static/push-notifications.js",),
"device-setup": ("static/install-app.js", "static/mobile-device-setup.js"),
+ "security-center": ("static/security-center.js",),
"today-timer": (
"static/mobile-task-dock.js", "static/today-timer.js", "static/today-recap.js",
"static/today-rollover.js",
diff --git a/tests/test_dashboard_session_frontend.py b/tests/test_dashboard_session_frontend.py
index 6ac51b8..29d832b 100644
--- a/tests/test_dashboard_session_frontend.py
+++ b/tests/test_dashboard_session_frontend.py
@@ -9,6 +9,7 @@ from tests.dashboard_bundle import dashboard
ROOT = Path(__file__).resolve().parents[1]
SESSION_JS = ROOT / "frontend" / "session.js"
+SECURITY_CENTER_JS = ROOT / "frontend" / "security-center.js"
def run_session_scenario(scenario: str) -> dict:
@@ -753,7 +754,7 @@ process.stdout.write(JSON.stringify(state));
def test_security_activity_renders_authentication_alerts_before_journal_events():
- source = SESSION_JS.read_text()
+ source = SECURITY_CENTER_JS.read_text()
alerts = source.index("page.authentication_alerts.forEach")
events = source.index("page.events.forEach")
@@ -765,14 +766,14 @@ def test_security_activity_renders_authentication_alerts_before_journal_events()
def test_security_activity_explains_pending_outcome_confirmation():
- source = SESSION_JS.read_text()
+ source = SECURITY_CENTER_JS.read_text()
assert "Outcome confirmation pending" in source
assert "event.status === 'pending'" in source
def test_security_activity_labels_passkey_enrollment_without_html_rendering():
- source = SESSION_JS.read_text()
+ source = SECURITY_CENTER_JS.read_text()
assert "passkey_enrolled: 'Passkey enrolled'" in source
assert "title.textContent = labels[event.kind]" in source
@@ -780,7 +781,7 @@ def test_security_activity_labels_passkey_enrollment_without_html_rendering():
def test_security_activity_labels_consequential_pull_review_decisions():
- source = SESSION_JS.read_text()
+ source = SECURITY_CENTER_JS.read_text()
assert "pull_review_approved: 'Pull request approved'" in source
assert "pull_review_changes_requested: 'Changes requested'" in source
@@ -818,6 +819,6 @@ async def test_dashboard_loads_session_boundary_first_and_offers_sign_out():
def test_security_activity_names_confirmed_gitea_time_logging():
- source = SESSION_JS.read_text()
+ source = SECURITY_CENTER_JS.read_text()
assert "gitea_time_logged: 'Gitea time logged'" in source
diff --git a/tests/test_feature_loader.py b/tests/test_feature_loader.py
index 965d95f..0982f4b 100644
--- a/tests/test_feature_loader.py
+++ b/tests/test_feature_loader.py
@@ -89,3 +89,22 @@ console.log(JSON.stringify({first,second,failedStatus,opened,appends:state.appen
"opened": 1,
"appends": 2,
}
+
+
+def test_security_center_failure_is_accessible_and_retryable_from_active_devices():
+ result = run_loader("""
+const trigger={disabled:false}; const status={textContent:''}; let opened=0;
+const loader=createFeatureLoader({document, urls:{'security-center':'feature-security.js'}, timeoutMs:100});
+const failed=loader.run('security-center',{trigger,status,retryLabel:'Tap Active devices to retry.'},()=>{opened++;});
+state.node.onerror(); const first=await failed; const failedStatus=status.textContent;
+const retry=loader.run('security-center',{trigger,status,retryLabel:'Tap Active devices to retry.'},()=>{opened++;});
+state.node.onload(); const second=await retry;
+console.log(JSON.stringify({first,second,failedStatus,opened,appends:state.appends}));
+""")
+ assert result == {
+ "first": False,
+ "second": True,
+ "failedStatus": "Security center could not load. Tap Active devices to retry.",
+ "opened": 1,
+ "appends": 2,
+ }
diff --git a/tests/test_frontend_bundle.py b/tests/test_frontend_bundle.py
index d7c6431..75b6c2d 100644
--- a/tests/test_frontend_bundle.py
+++ b/tests/test_frontend_bundle.py
@@ -49,7 +49,7 @@ def test_product_workflows_are_stable_lazy_feature_chunks(tmp_path):
assert set(first.feature_bundles) == {
"comment-actions", "issue-capture", "pull-workflow", "push-notifications", "device-setup",
- "today-timer",
+ "today-timer", "security-center",
}
assert (
f'\n'
@@ -63,12 +63,19 @@ def test_product_workflows_are_stable_lazy_feature_chunks(tmp_path):
assert b"function createReviewController" not in first.runtime_bytes
assert b"function createPullSheet" in pull_workflow.runtime_bytes
assert b"function createReviewController" in pull_workflow.runtime_bytes
+ security_center = first.feature_bundles["security-center"]
+ assert b"function attachSecurityCenter" not in first.runtime_bytes
+ assert b"function attachSecurityCenter" in security_center.runtime_bytes
+ assert b"gitea_time_logged" not in first.runtime_bytes
+ assert b"gitea_time_logged" in security_center.runtime_bytes
# The recap adds only startup wiring; its UI remains in the lazy Today bundle.
- assert len(first.runtime_gzip_bytes) <= 96 * 1024
+ assert len(first.runtime_gzip_bytes) <= 95 * 1024
assert f'name="stackchain-feature-issue-capture" content="{capture.runtime_name}"' in first.dashboard_html
assert f'name="stackchain-feature-pull-workflow" content="{pull_workflow.runtime_name}"' in first.dashboard_html
assert f"BASE + '{capture.runtime_name}'" in first.service_worker_source
assert f"BASE + '{pull_workflow.runtime_name}'" in first.service_worker_source
+ assert f'name="stackchain-feature-security-center" content="{security_center.runtime_name}"' in first.dashboard_html
+ assert f"BASE + '{security_center.runtime_name}'" in first.service_worker_source
changed_frontend = tmp_path / "frontend"
shutil.copytree(FRONTEND, changed_frontend)
@@ -85,6 +92,12 @@ def test_product_workflows_are_stable_lazy_feature_chunks(tmp_path):
assert pull_changed.runtime_name == first.runtime_name
assert pull_changed.feature_bundles["pull-workflow"].runtime_name != pull_workflow.runtime_name
+ security_source = changed_frontend / "security-center.js"
+ security_source.write_text(security_source.read_text() + "\n// security-center-only change\n")
+ security_changed = build_frontend(changed_frontend)
+ assert security_changed.runtime_name == first.runtime_name
+ assert security_changed.feature_bundles["security-center"].runtime_name != security_center.runtime_name
+
def test_shipped_browser_bundles_are_valid_javascript(tmp_path):
build = build_frontend(FRONTEND)
diff --git a/tests/test_security_center.py b/tests/test_security_center.py
new file mode 100644
index 0000000..84417d7
--- /dev/null
+++ b/tests/test_security_center.py
@@ -0,0 +1,49 @@
+import json
+import subprocess
+from pathlib import Path
+
+
+SECURITY_CENTER = Path(__file__).parents[1] / "frontend" / "security-center.js"
+
+
+def test_open_security_center_loads_all_sections_concurrently_and_is_awaitable():
+ harness = f"""
+const attachSecurityCenter=require({json.dumps(str(SECURITY_CENTER))});
+const calls=[];
+const element=()=>({{
+ hidden:true, disabled:false, textContent:'', children:[],
+ addEventListener(){{}}, focus(){{this.focused=true;}},
+ replaceChildren(){{this.children=[];}}, append(value){{this.children.push(value);}},
+}});
+const ids={{
+ 'active-devices':element(), 'active-devices-sheet':element(),
+ 'active-devices-list':element(), 'active-devices-status':element(),
+ 'enrolled-passkeys-list':element(), 'enrolled-passkeys-status':element(),
+ 'enroll-passkey':element(), 'security-activity-list':element(),
+ 'security-activity-status':element(), 'load-more-security-activity':element(),
+ 'close-active-devices':element(),
+}};
+const root={{document:{{getElementById:id=>ids[id]||null,createElement:()=>element()}}}};
+const boundary={{
+ listActiveDevices:async()=>{{calls.push('devices');return[];}},
+ listPasskeys:async()=>{{calls.push('passkeys');return[];}},
+ listSecurityEvents:async()=>{{calls.push('activity');return{{events:[],authentication_alerts:[],next_cursor:null}};}},
+}};
+(async()=>{{
+ const controller=attachSecurityCenter({{root,boundary}});
+ const loading=controller.open();
+ const awaitable=Boolean(loading&&typeof loading.then==='function');
+ if (loading) await loading;
+ console.log(JSON.stringify({{calls,awaitable,hidden:ids['active-devices-sheet'].hidden,focused:ids['close-active-devices'].focused||false}}));
+}})().catch(error=>{{console.error(error);process.exit(1);}});
+"""
+ completed = subprocess.run(
+ ["node", "-e", harness], check=True, capture_output=True, text=True
+ )
+
+ assert json.loads(completed.stdout) == {
+ "calls": ["devices", "passkeys", "activity"],
+ "awaitable": True,
+ "hidden": False,
+ "focused": True,
+ }
diff --git a/tests/test_service_worker.py b/tests/test_service_worker.py
index 971fb9b..f049c0a 100644
--- a/tests/test_service_worker.py
+++ b/tests/test_service_worker.py
@@ -661,6 +661,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
"/dashboard/static/icons/stackchain-512.png",
"/dashboard/static/session.js",
"/dashboard/static/feature-loader.js",
+ "/dashboard/static/security-center.js",
"/dashboard/static/markdown.js",
"/dashboard/static/commands.js",
"/dashboard/static/search-preview.js",