1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
use *;
/// localStorage key prefix used to record that a `route` has been
/// unlocked. The value itself is just a constant `"1"` sentinel — the
/// password digest lives only in [`DocsPage::password_hash`].
///
/// Route is alphanumeric-only into the key so that slashes / locale
/// prefixes don't collide and so the key is plain ASCII (localStorage
/// keys must not contain newlines or other control chars).
const UNLOCK_KEY_PREFIX: &str = "euv-docs:unlocked:";
/// Calls `crypto.subtle.digest("SHA-256", ...)` on the supplied UTF-8
/// string via a tiny inline JS shim and returns the lower-case hex
/// digest (64 chars). Returns `None` on environments without
/// `crypto.subtle` (rare outside of `http://` test fixtures or
/// non-secure-context loads; the form will then reject all attempts
/// so the content stays protected).
///
/// Using `eval` keeps the binding typed-but-loose — we don't
/// need the typed `web_sys::Crypto` / `web_sys::SubtleCrypto` surfaces,
/// which aren't enabled in euv's web-sys feature set. The result is a
/// `Promise<ArrayBuffer>` that we await via `wasm_bindgen_futures`.
async
/// Sanitises a route string for use as a localStorage key. Slashes and
/// non-alphanumeric characters are replaced with `-` so the key is
/// ASCII and reversible for grep.
/// Renders the password prompt that gates a `private` markdown page.
///
/// # Behaviour
///
/// - On mount, shows an empty password input plus an "Unlock" button.
/// - On submit, hashes the entered password via `crypto.subtle.digest`
/// and compares it byte-for-byte against `expected_hash`.
/// - On match, writes `"euv-docs:unlocked:<route>" = "1"` into
/// `localStorage` so the same browser session re-renders the page
/// body without re-asking on subsequent navigations and refreshes.
/// The route signal then re-resolves and the parent unmounts the
/// gate in favour of the page body.
/// - On mismatch, replaces the input with a red border + an error
/// message; the password field is cleared so the user can retry
/// without leaking what they typed into form history.
/// # Why a custom form instead of `<euv_field>` / `<euv_button>`
///
/// `<euv_field>` auto-routes the `Enter` keypress to the surrounding
/// form's `onsubmit` only when wrapped in `<form>`. The password gate
/// needs explicit submit handling so the password never lingers in
/// the DOM after a wrong attempt — using a bare `<input>` + `<button>`
/// keeps the HTML minimal and lets us clear the value imperatively.
pub
/// Builds the submit handler. Hashes the input value with WebCrypto
/// SHA-256 and compares against `expected_hash`. On success writes the
/// unlock record into localStorage **and** forces a route re-resolution
/// so the parent renders the page body instead of the gate. On failure
/// sets an error message and clears the input.
/// Updates the bound input signal on each keystroke so the value flows
/// back into the controlled `<input>` after a wrong-submit clear.
/// Submits when the user presses `Enter` so they don't have to click
/// the button with the mouse.
/// Returns whether the supplied route has already been unlocked in
/// this browser. The parent calls this on every render — including the
/// direct-URL-paste case where the page is mounted into a freshly
/// loaded `<div id="app">` — so the gate only renders when the user
/// hasn't unlocked this route yet in this session.
/// Reads from `localStorage` synchronously; the call is cheap and the
/// result is a single `bool` branch.
pub