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.
@@ -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 Show control per password input, each input keeping its autocomplete`, () => {
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 ? ' minlength="10"' : ""
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="10">
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:68px}
166
- .secret .reveal{position:absolute;right:5px;top:50%;transform:translateY(-50%);width:auto;height:30px;
167
- min-width:56px;padding:0 10px;border-radius:7px;background:none;color:var(--ml-muted);
168
- font-size:12.5px;font-weight:600}
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 a Show/Hide control (task 077-438's library share) — a
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
- '<span class="secret">$1<button type="button" class="reveal" data-reveal aria-label="Show password" aria-pressed="false">Show</button></span>',
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 = 10;
314
+ const MIN_LENGTH = ${MASTER_LOCK_MIN_PASSWORD_LENGTH};
296
315
 
297
316
  let kdf = null;
298
317
  let busy = false;