cursedbelt-server 4.30.1 → 4.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/d1/pullD1.d.ts +57 -8
- package/dist/server/d1/pullD1.js +157 -89
- package/dist/server/master-lock/accountsPage.d.ts +3 -3
- package/dist/server/master-lock/accountsPage.js +4 -4
- package/dist/server/master-lock/lockPage.d.ts +8 -13
- package/dist/server/master-lock/lockPage.js +25 -10
- package/package.json +1 -1
- package/src/server/d1/pullD1.spec.ts +111 -90
- package/src/server/d1/pullD1.ts +162 -84
- package/src/server/master-lock/accountsPage.ts +4 -4
- package/src/server/master-lock/lockPage.spec.ts +16 -1
- package/src/server/master-lock/lockPage.ts +29 -10
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
LOCK_SCRIPT,
|
|
20
20
|
LOCK_STYLE,
|
|
21
21
|
MASTER_LOCK_DERIVE_SOURCE,
|
|
22
|
+
MASTER_LOCK_MIN_PASSWORD_LENGTH,
|
|
22
23
|
MASTER_LOCK_REVEAL_SOURCE,
|
|
23
24
|
lockPageHtml,
|
|
24
25
|
withRevealToggles,
|
|
@@ -107,7 +108,7 @@ describe("🔴 every password field on the wall can be shown and hidden (task 07
|
|
|
107
108
|
};
|
|
108
109
|
|
|
109
110
|
for (const [name, html] of Object.entries(pages)) {
|
|
110
|
-
test(`${name}: one
|
|
111
|
+
test(`${name}: one eye control per password input, each input keeping its autocomplete`, () => {
|
|
111
112
|
const inputs = [...html.matchAll(/<input\b[^>]*\btype="password"[^>]*>/g)].map((m) => m[0]);
|
|
112
113
|
expect(inputs.length).toBeGreaterThan(0);
|
|
113
114
|
expect((html.match(/data-reveal/g) ?? []).length).toBe(inputs.length);
|
|
@@ -117,6 +118,20 @@ describe("🔴 every password field on the wall can be shown and hidden (task 07
|
|
|
117
118
|
});
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
test("🔴 the control is an EYE, not a word, and every minimum is 6 (owner, 2026-09-24)", () => {
|
|
122
|
+
for (const html of Object.values(pages)) {
|
|
123
|
+
for (const button of html.match(/<button[^>]*data-reveal[^>]*>[\s\S]*?<\/button>/g) ?? []) {
|
|
124
|
+
expect(button).toContain('class="eye"');
|
|
125
|
+
expect(button).toContain('class="eye-off"');
|
|
126
|
+
// No visible word: strip the markup and nothing readable is left.
|
|
127
|
+
expect(button.replace(/<[^>]+>/g, "").trim()).toBe("");
|
|
128
|
+
}
|
|
129
|
+
for (const m of html.matchAll(/minlength="(\d+)"/g)) expect(m[1]).toBe("6");
|
|
130
|
+
}
|
|
131
|
+
expect(MASTER_LOCK_MIN_PASSWORD_LENGTH).toBe(6);
|
|
132
|
+
expect(LOCK_SCRIPT).toContain("const MIN_LENGTH = 6;");
|
|
133
|
+
});
|
|
134
|
+
|
|
120
135
|
test("both served scripts carry the ONE reveal source and call it", () => {
|
|
121
136
|
for (const script of [LOCK_SCRIPT, MASTER_LOCK_ACCOUNTS_SCRIPT]) {
|
|
122
137
|
expect(script).toContain(MASTER_LOCK_REVEAL_SOURCE);
|
|
@@ -27,6 +27,13 @@
|
|
|
27
27
|
*/
|
|
28
28
|
import { MASTER_LOCK_PATHS } from "cursedbelt-core/master-lock";
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* The shortest master password the wall accepts — every `minlength` on both pages and both
|
|
32
|
+
* scripts' own checks read this one number, so a rotation cannot quietly differ from enrollment.
|
|
33
|
+
* 10 until 2026-09-24; the owner: *"make the minimum password length 6 characters"*.
|
|
34
|
+
*/
|
|
35
|
+
export const MASTER_LOCK_MIN_PASSWORD_LENGTH = 6;
|
|
36
|
+
|
|
30
37
|
/** Everything the page needs to know about the app it is standing in front of. */
|
|
31
38
|
export interface LockPageOptions {
|
|
32
39
|
/** What to call this app — "Collections", "binary-server inspector". */
|
|
@@ -88,7 +95,7 @@ export function lockPageHtml(options: LockPageOptions): string {
|
|
|
88
95
|
<input id="ml-input" type="password" name="master-password"
|
|
89
96
|
autocomplete="${enroll ? "new-password" : "current-password"}"
|
|
90
97
|
autofocus required spellcheck="false" enterkeyhint="${enroll ? "next" : "go"}"${
|
|
91
|
-
enroll ?
|
|
98
|
+
enroll ? ` minlength="${MASTER_LOCK_MIN_PASSWORD_LENGTH}"` : ""
|
|
92
99
|
}>
|
|
93
100
|
</label>${
|
|
94
101
|
enroll
|
|
@@ -97,7 +104,7 @@ export function lockPageHtml(options: LockPageOptions): string {
|
|
|
97
104
|
<span>Confirm master password</span>
|
|
98
105
|
<input id="ml-confirm" type="password" name="confirm-master-password"
|
|
99
106
|
autocomplete="new-password" required spellcheck="false" enterkeyhint="go"
|
|
100
|
-
minlength="
|
|
107
|
+
minlength="${MASTER_LOCK_MIN_PASSWORD_LENGTH}">
|
|
101
108
|
</label>
|
|
102
109
|
<label class="field">
|
|
103
110
|
<span>Hint <em>optional</em></span>
|
|
@@ -162,16 +169,18 @@ button.quiet[disabled]{opacity:.5}
|
|
|
162
169
|
.hints dd{margin:2px 0 10px;color:var(--ml-muted);overflow-wrap:anywhere}
|
|
163
170
|
.hints dd:last-child{margin-bottom:0}
|
|
164
171
|
.secret{position:relative;display:block}
|
|
165
|
-
.secret input{padding-right:
|
|
166
|
-
.secret .reveal{position:absolute;right:5px;top:50%;transform:translateY(-50%);width:
|
|
167
|
-
|
|
168
|
-
|
|
172
|
+
.secret input{padding-right:48px}
|
|
173
|
+
.secret .reveal{position:absolute;right:5px;top:50%;transform:translateY(-50%);width:36px;height:32px;
|
|
174
|
+
display:grid;place-items:center;padding:0;border-radius:7px;background:none;color:var(--ml-muted)}
|
|
175
|
+
.secret .reveal svg{width:18px;height:18px}
|
|
176
|
+
.secret .reveal .eye-off,.secret .reveal[aria-pressed="true"] .eye{display:none}
|
|
177
|
+
.secret .reveal[aria-pressed="true"] .eye-off{display:block}
|
|
169
178
|
.secret .reveal:hover{color:var(--ml-text);background:var(--ml-card)}
|
|
170
179
|
.secret .reveal:focus-visible{outline:2px solid var(--ml-accent);outline-offset:1px}
|
|
171
180
|
`;
|
|
172
181
|
|
|
173
182
|
/**
|
|
174
|
-
* 🔴 Every password field on the wall gets
|
|
183
|
+
* 🔴 Every password field on the wall gets an eye (show/hide) control (task 077-438's library share) — a
|
|
175
184
|
* master password is long, typed once and never recoverable, and a typo in "choose" is a lock-out.
|
|
176
185
|
*
|
|
177
186
|
* Markup: {@link withRevealToggles} wraps each `type="password"` input in `.secret` with a
|
|
@@ -181,10 +190,21 @@ button.quiet[disabled]{opacity:.5}
|
|
|
181
190
|
* CSP is `script-src 'self'` and allows no inline script (`MASTER_LOCK_CSP`, pinned by
|
|
182
191
|
* `beaconNeverReachesTheWall.spec.ts`). Nothing about the CSP changes for this.
|
|
183
192
|
*/
|
|
193
|
+
/*
|
|
194
|
+
* An eye, and an eye struck through — the owner, 2026-09-24: *"use an eye icon instead of words
|
|
195
|
+
* like 'Show'"*. Both are in the button; the stylesheet shows one by `aria-pressed`, so the
|
|
196
|
+
* script only flips state and the accessible name, never markup. Same glyphs as cursedbelt's
|
|
197
|
+
* React `SecretInput`, drawn inline because this page ships no icon font and no framework.
|
|
198
|
+
*/
|
|
199
|
+
const EYE =
|
|
200
|
+
'<svg class="eye" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/></svg>';
|
|
201
|
+
const EYE_OFF =
|
|
202
|
+
'<svg class="eye-off" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.9 4.2A10.4 10.4 0 0 1 12 4c6.5 0 10 8 10 8a17.6 17.6 0 0 1-2.3 3.3"/><path d="M6.6 6.6C3.9 8.4 2 12 2 12s3.5 7 10 7c1.8 0 3.4-.5 4.8-1.3"/><path d="M9.9 9.9a3 3 0 0 0 4.2 4.2"/><path d="M2 2l20 20"/></svg>';
|
|
203
|
+
|
|
184
204
|
export function withRevealToggles(html: string): string {
|
|
185
205
|
return html.replace(
|
|
186
206
|
/(<input\b[^>]*\btype="password"[^>]*>)/g,
|
|
187
|
-
|
|
207
|
+
`<span class="secret">$1<button type="button" class="reveal" data-reveal aria-label="Show password" aria-pressed="false">${EYE}${EYE_OFF}</button></span>`,
|
|
188
208
|
);
|
|
189
209
|
}
|
|
190
210
|
|
|
@@ -203,7 +223,6 @@ export const MASTER_LOCK_REVEAL_SOURCE = `function wireMasterLockReveal(doc) {
|
|
|
203
223
|
input.type = on ? "text" : "password";
|
|
204
224
|
button.setAttribute("aria-pressed", on ? "true" : "false");
|
|
205
225
|
button.setAttribute("aria-label", on ? "Hide password" : "Show password");
|
|
206
|
-
button.textContent = on ? "Hide" : "Show";
|
|
207
226
|
};
|
|
208
227
|
button.addEventListener("click", () => {
|
|
209
228
|
show(input.type === "password");
|
|
@@ -292,7 +311,7 @@ const enrolling = document.body.dataset.mode === "enroll";
|
|
|
292
311
|
|
|
293
312
|
/** The shortest password worth calling one. Matched by the input's own minlength, and
|
|
294
313
|
* checked here too because a pasted value can beat the constraint on some engines. */
|
|
295
|
-
const MIN_LENGTH =
|
|
314
|
+
const MIN_LENGTH = ${MASTER_LOCK_MIN_PASSWORD_LENGTH};
|
|
296
315
|
|
|
297
316
|
let kdf = null;
|
|
298
317
|
let busy = false;
|