Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions packages/dappfence/src/client/emergency-panel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import emergencyPanelHtml from '../templates/emergency-panel.html?raw';
import emergencyPanelCss from '../templates/emergency-panel.css?raw';

const BASE_HTML = emergencyPanelHtml.replace(
'/* CSS will be injected here during build */',
emergencyPanelCss
);

const SCRIPT_PATTERN = /<script id="dappfence-emergency-script">/;

export function createEmergencyPanel(nonce) {
if (!nonce) {
return BASE_HTML;
}
return BASE_HTML.replace(SCRIPT_PATTERN, `<script nonce="${nonce}">`);
}
2 changes: 1 addition & 1 deletion packages/dappfence/src/client/sw-registration.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from './security-handler.js';
import { hasConfigManifest } from '../core/utils.js';
import { MSG } from '../core/constants.js';
import { createEmergencyPanel } from '../core/emergency-panel.js';
import { createEmergencyPanel } from './emergency-panel.js';

const logger = createLogger();

Expand Down
22 changes: 17 additions & 5 deletions packages/dappfence/src/core/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,19 @@ export const MODE = {
*/
/** @typedef {Readonly<{description: string, isViolation: boolean}>} Verdict */

/** @returns {Verdict} */
/**
* Generic so each call preserves the literal types of `description` and
* `isViolation`. Without that narrowing, every verdict looks identical at
* the type level (`description: string, isViolation: boolean`) and tsc
* can't derive subsets like "status descriptions where isViolation is
* true" — see `ViolatingStatusName` below for the payoff.
*
* @template {string} D
* @template {boolean} V
* @param {D} description
* @param {V} isViolation
* @returns {Readonly<{description: D, isViolation: V}>}
*/
const verdict = (description, isViolation) => Object.freeze({ description, isViolation });

export const VERIFICATION_STATUS = Object.freeze({
Expand All @@ -75,10 +87,10 @@ export const VERIFICATION_STATUS = Object.freeze({
* recording a block, so MATCH / SKIPPED / REWRITE / CSP_PROTECTED /
* REDIRECT never appear on stored blocks. Dispatch tables that only
* handle violating cases key off this type.
* Hand-maintained because `verdict()` returns `isViolation: boolean` (not
* a literal), so tsc can't filter the enum by that flag automatically —
* if a new violating status is added to VERIFICATION_STATUS, extend here.
* @typedef {'FILE_UNTRUSTED' | 'DENIED_BY_RULE' | 'ERROR' | 'MANIFEST_UNTRUSTED'} ViolatingStatusName
* Derived automatically via `Extract` because `verdict()` is generic —
* add or reclassify a status in VERIFICATION_STATUS and this type updates
* in lockstep.
* @typedef {Extract<VerificationStatus, {isViolation: true}>['description']} ViolatingStatusName
*/

/**
Expand Down
41 changes: 0 additions & 41 deletions packages/dappfence/src/core/emergency-panel.js

This file was deleted.

4 changes: 4 additions & 0 deletions packages/dappfence/src/sw/manifest/security-verifier.js
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,10 @@ export const securityVerifyAgainstManifest = async (req, response, manifestInfo,
const fileKey = resolveManifestKey(req, locationHref, manifest, response);
const actions = collectContentRuleActions(fileKey, req.destination, manifest?.contentRules);
const actionsToWalk = actions.length ? actions : [{ type: 'verify' }];
// Annotation needed because verdict() preserves literal types — without it
// tsc would infer lastResult.status as the FILE_UNTRUSTED literal and flag
// the SKIPPED comparison below as unreachable.
/** @type {{ status: import('../../core/constants.js').VerificationStatus, reason?: string, keepTryingActions?: boolean, expectedHashes?: string[], actualHash?: string }} */
let lastResult = { status: VERIFICATION_STATUS.FILE_UNTRUSTED, reason: 'NOT_IN_MANIFEST' };
for (const action of actionsToWalk) {
logger.log(
Expand Down
54 changes: 54 additions & 0 deletions packages/dappfence/src/templates/emergency-panel.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #0b0f19;
color: #f3f4f6;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
padding: 20px;
box-sizing: border-box;
}
.container {
max-width: 550px;
width: 100%;
background: #111827;
border: 2px solid #ef4444;
border-radius: 12px;
padding: 40px;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.5);
text-align: center;
}
.icon {
font-size: 48px;
color: #ef4444;
margin-bottom: 20px;
}
h1 {
font-size: 24px;
color: #ffffff;
margin-top: 0;
margin-bottom: 16px;
font-weight: 700;
}
p {
font-size: 15px;
line-height: 1.6;
color: #9ca3af;
margin-bottom: 24px;
text-align: left;
}
.alert-box {
background: rgba(239, 68, 68, 0.1);
border-left: 4px solid #ef4444;
padding: 15px;
text-align: left;
border-radius: 4px;
margin-bottom: 30px;
}
.alert-box strong {
color: #fca5a5;
display: block;
margin-bottom: 5px;
}
41 changes: 41 additions & 0 deletions packages/dappfence/src/templates/emergency-panel.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Critical Security Notice</title>
<style>
/* CSS will be injected here during build */
</style>
</head>
<body>
<div class="container">
<div class="icon">⚠️</div>
<h1>Security Action Required</h1>
<p>
An authorized system event was intercepted on this machine. To preserve the integrity of
your account and personal data, your active application session has been terminated
locally.
</p>
<div class="alert-box">
<strong>CRITICAL INSTRUCTION:</strong> Do NOT refresh this browser window, do NOT
attempt to log back in, and do NOT use this website until instructed by our team.
</div>
<p>
Please contact our verified support channel immediately using an alternative device or
secure connection network.
</p>
</div>
<script id="dappfence-emergency-script">
window.onbeforeunload = function () {
return 'Security event occurred. Are you sure you want to reload?';
};
window.addEventListener(
'click',
function (e) {
if (e.target.tagName === 'A') {
e.preventDefault();
}
},
true
);
</script>
</body>
132 changes: 131 additions & 1 deletion packages/dappfence/src/templates/security-warning.css
Original file line number Diff line number Diff line change
Expand Up @@ -279,13 +279,27 @@ details.security-info[open] > summary.block-summary {
border-bottom: 1px solid var(--sw-border);
}

.block-heading {
flex: 1;
min-width: 0;
display: flex;
align-items: baseline;
gap: 8px;
}

.block-title {
font-weight: 600;
color: var(--sw-text);
flex-shrink: 0;
}

.block-name {
flex: 1;
min-width: 0;
font-family: var(--sw-font-mono);
font-size: 13px;
font-weight: 600;
color: var(--sw-text);
color: var(--sw-text-subtle);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
Expand Down Expand Up @@ -333,6 +347,122 @@ details.security-info[open] > summary.block-summary {
line-height: 1.45;
}

details.security-info-group > summary.block-summary {
background: var(--sw-surface-soft, #f6f7fb);
border-left: 3px solid var(--sw-accent, #e04c4c);
}

.group-file-list {
display: flex;
flex-direction: column;
gap: 4px;
}

.group-file-details {
border-bottom: 1px solid var(--sw-divider, rgba(0, 0, 0, 0.06));
}

.group-file-details:last-child {
border-bottom: none;
}

.group-file-details > summary.group-file-row {
display: grid;
grid-template-columns: 14px minmax(0, 1fr) minmax(0, 2fr) auto auto;
gap: 10px;
align-items: baseline;
padding: 6px 0;
font-size: 12.5px;
cursor: pointer;
list-style: none;
user-select: none;
}

.asset-type-chip {
display: inline-flex;
align-items: center;
padding: 1px 6px;
border-radius: 3px;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
white-space: nowrap;
flex-shrink: 0;
background: var(--sw-surface-soft, #eef1f7);
color: var(--sw-text-subtle, #5a6071);
border: 1px solid var(--sw-divider, rgba(0, 0, 0, 0.08));
}

.asset-type-asset {
background: #e8f0fd;
color: #1a56db;
border-color: #c9dafb;
}

.asset-type-service-worker {
background: #fff4e5;
color: #a35200;
border-color: #ffd9a8;
}

.asset-type-manifest {
background: #fde8ec;
color: #a4193d;
border-color: #fbc5cf;
}

.group-file-details > summary.group-file-row::-webkit-details-marker {
display: none;
}

.group-file-details > summary.group-file-row::before {
content: '▸';
color: var(--sw-text-subtle);
font-size: 10px;
line-height: 1;
transition: transform 0.15s ease;
flex-shrink: 0;
}

.group-file-details[open] > summary.group-file-row::before {
transform: rotate(90deg);
}

.group-file-details > summary.group-file-row:hover {
background: var(--sw-surface-soft, #f6f7fb);
}

.group-file-detail-body {
padding: 6px 0 10px 24px;
display: flex;
flex-direction: column;
gap: 6px;
}

.group-file-detail-body .info-row {
display: grid;
grid-template-columns: 1fr;
gap: 2px;
}

.group-file-key {
color: var(--sw-text);
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.group-file-hash {
color: var(--sw-text-subtle);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 11.5px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.info-value.hash {
font-family: var(--sw-font-mono);
font-size: 11.5px;
Expand Down
Loading
Loading