Skip to content

Commit b4f674e

Browse files
committed
feat(approve): trustworthy redesign of the signing-consent prompt
The consent prompt is the surface a user sees when a site asks to sign with their key, so it now reads as a clear, calm security decision. - approve.css: dedicated stylesheet (inherits the popup design tokens). The requesting origin is the focal point — large monospace in a bordered inset that wraps/scrolls long or hostile origins without overflowing the layout or hiding the buttons. Deny is the safe default (solid, autofocused); Approve is deliberate (outlined danger-tone, not pre-focused, so a blind Enter denies). An amber note states that approving lets the site act as the user. - approve.html: restructured DOM (shield header, origin block, action row, scrollable payload, trust note, button row, live countdown). No inline styles with untrusted data; all dynamic fields are empty containers populated by JS. - approve.js: drives a visible, accurate 60s auto-deny countdown (rAF, with an "urgent" state under 10s). Security contract preserved byte-for-byte: reads id/origin/action/preview from the query string, sends {type:'APPROVE_SIGNING', requestId, approved} on choice, denies on beforeunload (close = deny), and auto-denies at 60s. A one-shot guard ensures the decision is sent exactly once. Origin and payload are written via textContent only — never innerHTML. Co-Authored-By: jjohare <github@thedreamlab.uk>
1 parent 4bc9514 commit b4f674e

3 files changed

Lines changed: 389 additions & 136 deletions

File tree

popup/approve.css

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
/**
2+
* Podkey - Signing-approval consent prompt
3+
* The most security-critical surface: a site is asking to act as the user.
4+
* Goal: unambiguous, calm, hard to mis-click. Deny is the safe default.
5+
* Inherits design tokens from popup.css (loaded first).
6+
*/
7+
8+
body {
9+
width: 380px;
10+
min-height: 0;
11+
}
12+
13+
.approve {
14+
display: flex;
15+
flex-direction: column;
16+
padding: 16px 16px 14px;
17+
}
18+
19+
/* ---- Header ---- */
20+
.approve-head {
21+
display: flex;
22+
align-items: center;
23+
gap: 10px;
24+
margin-bottom: 14px;
25+
}
26+
27+
.approve-shield {
28+
width: 36px;
29+
height: 36px;
30+
flex: none;
31+
display: grid;
32+
place-items: center;
33+
border-radius: 10px;
34+
background: var(--accent-soft);
35+
color: var(--accent);
36+
border: 1px solid var(--border);
37+
}
38+
39+
.approve-shield svg {
40+
width: 20px;
41+
height: 20px;
42+
}
43+
44+
.approve-title {
45+
font-size: 16px;
46+
font-weight: 700;
47+
letter-spacing: -0.01em;
48+
color: var(--text);
49+
}
50+
51+
.approve-sub {
52+
font-size: 11.5px;
53+
color: var(--text-muted);
54+
margin-top: 1px;
55+
}
56+
57+
/* ---- Origin block (the spoof-resistant focal point) ---- */
58+
.origin-block {
59+
background: var(--surface);
60+
border: 1px solid var(--border);
61+
border-radius: var(--radius);
62+
padding: 12px 14px;
63+
margin-bottom: 12px;
64+
box-shadow: var(--shadow-sm);
65+
}
66+
67+
.origin-eyebrow {
68+
font-size: 10.5px;
69+
font-weight: 700;
70+
text-transform: uppercase;
71+
letter-spacing: 0.06em;
72+
color: var(--text-faint);
73+
margin-bottom: 6px;
74+
}
75+
76+
.origin-badge {
77+
display: block;
78+
font-family: var(--mono);
79+
font-size: 14px;
80+
font-weight: 600;
81+
line-height: 1.4;
82+
color: var(--text);
83+
background: var(--surface-inset);
84+
border: 1px solid var(--border-strong);
85+
border-radius: var(--radius-sm);
86+
padding: 9px 11px;
87+
/* Long/hostile origins wrap and stay contained; cannot push layout. */
88+
word-break: break-all;
89+
overflow-wrap: anywhere;
90+
max-height: 84px;
91+
overflow-y: auto;
92+
}
93+
94+
/* ---- Action ---- */
95+
.action-row {
96+
display: flex;
97+
align-items: flex-start;
98+
gap: 9px;
99+
margin-bottom: 12px;
100+
font-size: 13.5px;
101+
color: var(--text);
102+
}
103+
104+
.action-row .action-icon {
105+
width: 18px;
106+
height: 18px;
107+
flex: none;
108+
margin-top: 1px;
109+
color: var(--text-muted);
110+
}
111+
112+
.action-row .action-text {
113+
font-weight: 600;
114+
}
115+
116+
/* ---- Preview ---- */
117+
.preview-wrap {
118+
margin-bottom: 12px;
119+
}
120+
121+
.preview-label {
122+
font-size: 10.5px;
123+
font-weight: 700;
124+
text-transform: uppercase;
125+
letter-spacing: 0.06em;
126+
color: var(--text-faint);
127+
margin-bottom: 6px;
128+
}
129+
130+
.event-preview {
131+
background: var(--surface-inset);
132+
border: 1px solid var(--border);
133+
border-radius: var(--radius-sm);
134+
padding: 11px 12px;
135+
font-family: var(--mono);
136+
font-size: 11.5px;
137+
line-height: 1.5;
138+
color: var(--text-muted);
139+
max-height: 150px;
140+
overflow: auto;
141+
white-space: pre;
142+
word-break: normal;
143+
tab-size: 2;
144+
}
145+
146+
/* ---- Trust warning ---- */
147+
.trust-note {
148+
display: flex;
149+
align-items: flex-start;
150+
gap: 8px;
151+
font-size: 11.5px;
152+
line-height: 1.45;
153+
color: var(--warn);
154+
margin-bottom: 14px;
155+
padding: 9px 11px;
156+
background: var(--warn-soft);
157+
border: 1px solid var(--warn-border);
158+
border-radius: var(--radius-sm);
159+
}
160+
161+
.trust-note svg {
162+
width: 15px;
163+
height: 15px;
164+
flex: none;
165+
margin-top: 1px;
166+
}
167+
168+
/* ---- Buttons ---- */
169+
.btn-row {
170+
display: flex;
171+
gap: 10px;
172+
}
173+
174+
.btn-row .btn {
175+
flex: 1;
176+
margin-bottom: 0;
177+
}
178+
179+
/* Deny = safe default: solid, visually settled, autofocused. */
180+
.btn-deny {
181+
background: var(--surface);
182+
color: var(--text);
183+
border: 1px solid var(--border-strong);
184+
}
185+
186+
.btn-deny:hover {
187+
background: var(--surface-2);
188+
}
189+
190+
.btn-deny:focus-visible {
191+
outline: none;
192+
box-shadow: 0 0 0 3px var(--accent-ring);
193+
}
194+
195+
/* Approve = deliberate: outlined danger-tone, not pre-focused. */
196+
.btn-approve {
197+
background: transparent;
198+
color: var(--danger);
199+
border: 1.5px solid var(--danger-border);
200+
}
201+
202+
.btn-approve:hover {
203+
background: var(--danger-soft);
204+
border-color: var(--danger);
205+
}
206+
207+
.btn-approve:focus-visible {
208+
outline: none;
209+
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.3);
210+
}
211+
212+
/* ---- Countdown ---- */
213+
.countdown {
214+
margin-top: 12px;
215+
}
216+
217+
.countdown-text {
218+
display: flex;
219+
align-items: center;
220+
justify-content: center;
221+
gap: 6px;
222+
font-size: 11px;
223+
color: var(--text-faint);
224+
margin-bottom: 7px;
225+
}
226+
227+
.countdown-text strong {
228+
color: var(--text-muted);
229+
font-variant-numeric: tabular-nums;
230+
font-weight: 700;
231+
}
232+
233+
.timeout-bar {
234+
height: 4px;
235+
background: var(--border);
236+
border-radius: 999px;
237+
overflow: hidden;
238+
}
239+
240+
.timeout-bar-inner {
241+
height: 100%;
242+
width: 100%;
243+
background: var(--text-faint);
244+
border-radius: 999px;
245+
transform-origin: left center;
246+
/* Width is driven from JS for an accurate, synced countdown. */
247+
transition: background 0.4s ease;
248+
}
249+
250+
.timeout-bar-inner.urgent {
251+
background: var(--danger);
252+
}

0 commit comments

Comments
 (0)