cursedbelt-server 4.44.0 → 4.44.1
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.
|
@@ -26,8 +26,15 @@ export interface LockPageOptions {
|
|
|
26
26
|
* would not say which app it is cannot tell the owner which password to type.
|
|
27
27
|
*/
|
|
28
28
|
export declare function lockPageHtml(options: LockPageOptions): string;
|
|
29
|
-
/**
|
|
30
|
-
|
|
29
|
+
/**
|
|
30
|
+
* The stylesheet. Follows the viewer's theme and commits to nothing else.
|
|
31
|
+
*
|
|
32
|
+
* 🔴 Every field is 16px, never `inherit` alone. The body is 15px, and iOS zooms the page into
|
|
33
|
+
* any field under 16px when it takes focus — then the unlock's `location.reload()` keeps that
|
|
34
|
+
* zoom, so the app opened "slightly zoomed in" behind the wall (owner, collections on an iPhone,
|
|
35
|
+
* 2026-10-10). `fieldsNeverZoomIos.spec.ts` holds every page this module serves to it.
|
|
36
|
+
*/
|
|
37
|
+
export declare const LOCK_STYLE = ":root{color-scheme:light dark;\n --ml-bg:#f4f5f7;--ml-card:#ffffff;--ml-text:#14171c;--ml-muted:#69707c;--ml-border:#d9dde3;\n --ml-accent:#1f6feb;--ml-accent-text:#ffffff;--ml-danger:#b42318;\n --ml-shadow:0 18px 48px rgba(16,18,22,.14)}\n@media (prefers-color-scheme:dark){:root{\n --ml-bg:#0d0f13;--ml-card:#171a20;--ml-text:#e7eaef;--ml-muted:#8b93a1;--ml-border:#2b3039;\n --ml-accent:#4f8cf7;--ml-accent-text:#0b0d11;--ml-danger:#f97066;\n --ml-shadow:0 18px 48px rgba(0,0,0,.55)}}\n*{box-sizing:border-box}\nbody{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px;\n background:var(--ml-bg);color:var(--ml-text);\n font:15px/1.55 ui-sans-serif,system-ui,-apple-system,\"Segoe UI\",sans-serif}\n.card{width:100%;max-width:23rem;background:var(--ml-card);border:1px solid var(--ml-border);\n border-radius:16px;padding:28px 26px 24px;box-shadow:var(--ml-shadow);text-align:center}\n.glyph{font-size:30px;line-height:1;margin-bottom:10px}\nh1{margin:0 0 6px;font-size:18px;font-weight:650;letter-spacing:-.01em}\n.note{margin:0 0 20px;font-size:13px;color:var(--ml-muted)}\n.field{display:block;text-align:left;margin-bottom:14px}\n.field span{display:block;font-size:12px;font-weight:600;color:var(--ml-muted);margin-bottom:6px}\ninput{width:100%;height:40px;padding:0 12px;border:1px solid var(--ml-border);border-radius:9px;\n background:var(--ml-bg);color:inherit;font:inherit;font-size:16px}\ninput:focus{outline:2px solid var(--ml-accent);outline-offset:1px;border-color:transparent}\nbutton{width:100%;height:40px;border:0;border-radius:9px;background:var(--ml-accent);\n color:var(--ml-accent-text);font:inherit;font-weight:600;cursor:pointer}\nbutton[disabled]{opacity:.6;cursor:progress}\n.status{margin:14px 0 0;min-height:1.2em;font-size:12.5px;color:var(--ml-muted)}\n.status[data-tone=\"error\"]{color:var(--ml-danger)}\n.field span em{font-style:normal;font-weight:500;text-transform:none;opacity:.7}\nbutton.quiet{margin-top:10px;height:32px;background:none;color:var(--ml-muted);font-weight:500;\n font-size:12.5px;text-decoration:underline;text-underline-offset:3px}\nbutton.quiet:hover{color:var(--ml-text)}\nbutton.quiet[disabled]{opacity:.5}\n.hints{margin:10px 0 0;padding:12px 14px;text-align:left;border:1px solid var(--ml-border);\n border-radius:10px;background:var(--ml-bg);font-size:12.5px}\n.hints dt{font-weight:650;color:var(--ml-text)}\n.hints dd{margin:2px 0 10px;color:var(--ml-muted);overflow-wrap:anywhere}\n.hints dd:last-child{margin-bottom:0}\n.secret{position:relative;display:block}\n.secret input{padding-right:48px}\n.secret .reveal{position:absolute;right:5px;top:50%;transform:translateY(-50%);width:36px;height:32px;\n display:grid;place-items:center;padding:0;border-radius:7px;background:none;color:var(--ml-muted)}\n.secret .reveal svg{width:18px;height:18px}\n.secret .reveal .eye-off,.secret .reveal[aria-pressed=\"true\"] .eye{display:none}\n.secret .reveal[aria-pressed=\"true\"] .eye-off{display:block}\n.secret .reveal:hover{color:var(--ml-text);background:var(--ml-card)}\n.secret .reveal:focus-visible{outline:2px solid var(--ml-accent);outline-offset:1px}\n";
|
|
31
38
|
export declare function withRevealToggles(html: string): string;
|
|
32
39
|
/**
|
|
33
40
|
* The toggle's behaviour, as browser source. It flips the input BEFORE it to `text` and back,
|
|
@@ -96,7 +96,14 @@ export function lockPageHtml(options) {
|
|
|
96
96
|
</html>
|
|
97
97
|
`);
|
|
98
98
|
}
|
|
99
|
-
/**
|
|
99
|
+
/**
|
|
100
|
+
* The stylesheet. Follows the viewer's theme and commits to nothing else.
|
|
101
|
+
*
|
|
102
|
+
* 🔴 Every field is 16px, never `inherit` alone. The body is 15px, and iOS zooms the page into
|
|
103
|
+
* any field under 16px when it takes focus — then the unlock's `location.reload()` keeps that
|
|
104
|
+
* zoom, so the app opened "slightly zoomed in" behind the wall (owner, collections on an iPhone,
|
|
105
|
+
* 2026-10-10). `fieldsNeverZoomIos.spec.ts` holds every page this module serves to it.
|
|
106
|
+
*/
|
|
100
107
|
export const LOCK_STYLE = `:root{color-scheme:light dark;
|
|
101
108
|
--ml-bg:#f4f5f7;--ml-card:#ffffff;--ml-text:#14171c;--ml-muted:#69707c;--ml-border:#d9dde3;
|
|
102
109
|
--ml-accent:#1f6feb;--ml-accent-text:#ffffff;--ml-danger:#b42318;
|
|
@@ -117,7 +124,7 @@ h1{margin:0 0 6px;font-size:18px;font-weight:650;letter-spacing:-.01em}
|
|
|
117
124
|
.field{display:block;text-align:left;margin-bottom:14px}
|
|
118
125
|
.field span{display:block;font-size:12px;font-weight:600;color:var(--ml-muted);margin-bottom:6px}
|
|
119
126
|
input{width:100%;height:40px;padding:0 12px;border:1px solid var(--ml-border);border-radius:9px;
|
|
120
|
-
background:var(--ml-bg);color:inherit;font:inherit}
|
|
127
|
+
background:var(--ml-bg);color:inherit;font:inherit;font-size:16px}
|
|
121
128
|
input:focus{outline:2px solid var(--ml-accent);outline-offset:1px;border-color:transparent}
|
|
122
129
|
button{width:100%;height:40px;border:0;border-radius:9px;background:var(--ml-accent);
|
|
123
130
|
color:var(--ml-accent-text);font:inherit;font-weight:600;cursor:pointer}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursedbelt-server",
|
|
3
|
-
"version": "4.44.
|
|
3
|
+
"version": "4.44.1",
|
|
4
4
|
"license": "ISC",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The app-facing Bun/Hono server tier of the cursedbelt split — storage, sharing, activity, guard, sync. React-free; cursedbelt-core below it.",
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🔴 No field on a page the master lock serves is under 16px — iOS zooms into one that is.
|
|
3
|
+
*
|
|
4
|
+
* Owner, collections on an iPhone, 2026-10-10: *"Loading the site on iphone sometimes starts the
|
|
5
|
+
* view slightly zoomed in, making the user pinch the screen in and let go to see the full
|
|
6
|
+
* window."* The wall's password field inherited the body's 15px; focusing it zoomed the page, and
|
|
7
|
+
* the unlock's `location.reload()` kept the zoom, so the app itself opened magnified — only on the
|
|
8
|
+
* loads that went through the wall, which is the "sometimes". The apps' Apple pass (FRM-11) never
|
|
9
|
+
* saw it because its harness signs in past the wall.
|
|
10
|
+
*
|
|
11
|
+
* Read off the STYLESHEET each page actually serves: the last declaration that sizes a rule naming
|
|
12
|
+
* `input`, `select` or `textarea` is the size such a field gets.
|
|
13
|
+
*/
|
|
14
|
+
import { describe, expect, test } from "bun:test";
|
|
15
|
+
import { MASTER_LOCK_PATHS } from "cursedbelt-core/master-lock";
|
|
16
|
+
import { accountsPageHtml, MASTER_LOCK_ACCOUNTS_PAGE_PATHS, MASTER_LOCK_ACCOUNTS_STYLE } from "./accountsPage.js";
|
|
17
|
+
import { LOCK_STYLE, lockPageHtml } from "./lockPage.js";
|
|
18
|
+
|
|
19
|
+
/** Every stylesheet a wall page links, by the href it links it at. A page linking one not
|
|
20
|
+
* listed here fails below rather than going unread. */
|
|
21
|
+
const SERVED_CSS: Record<string, string> = {
|
|
22
|
+
[MASTER_LOCK_PATHS.style]: LOCK_STYLE,
|
|
23
|
+
[MASTER_LOCK_ACCOUNTS_PAGE_PATHS.style]: MASTER_LOCK_ACCOUNTS_STYLE,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** The px size the stylesheet gives `field`, or null when no rule names it. */
|
|
27
|
+
export function fieldFontPx(css: string, field: "input" | "select" | "textarea", bodyPx: number): number | null {
|
|
28
|
+
let size: number | null = null;
|
|
29
|
+
for (const match of css.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
|
|
30
|
+
const selectors = (match[1] ?? "").split(",").map((part) => part.trim());
|
|
31
|
+
if (!selectors.some((selector) => new RegExp(`(^|[\\s>+~])${field}(?![\\w-])`).test(selector))) continue;
|
|
32
|
+
for (const decl of (match[2] ?? "").split(";")) {
|
|
33
|
+
const [prop, ...rest] = decl.split(":");
|
|
34
|
+
const name = prop?.trim();
|
|
35
|
+
const value = rest.join(":").trim();
|
|
36
|
+
if (name === "font-size" || name === "font") {
|
|
37
|
+
if (value === "inherit") size = bodyPx;
|
|
38
|
+
const px = /(\d+(?:\.\d+)?)px/.exec(value);
|
|
39
|
+
if (px) size = Number(px[1]);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return size;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const OPTIONS = { appLabel: "Collections" };
|
|
47
|
+
|
|
48
|
+
describe("no master-lock field makes iOS zoom", () => {
|
|
49
|
+
const pages = {
|
|
50
|
+
lock: lockPageHtml(OPTIONS),
|
|
51
|
+
enroll: lockPageHtml({ ...OPTIONS, enroll: true }),
|
|
52
|
+
accounts: accountsPageHtml(OPTIONS),
|
|
53
|
+
};
|
|
54
|
+
for (const [name, html] of Object.entries(pages)) {
|
|
55
|
+
test(`${name}: every field is at least 16px`, () => {
|
|
56
|
+
const hrefs = [...html.matchAll(/<link rel="stylesheet" href="([^"]+)">/g)].map((m) => m[1] ?? "");
|
|
57
|
+
for (const href of hrefs) expect(SERVED_CSS[href], `${name} links ${href}, which this check does not read`).toBeDefined();
|
|
58
|
+
const css = [...hrefs.map((href) => SERVED_CSS[href] ?? ""), ...[...html.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/g)].map((m) => m[1])].join("\n");
|
|
59
|
+
expect(css.length).toBeGreaterThan(0);
|
|
60
|
+
const bodyPx = Number(/body\{[^}]*?font:\s*(\d+)px/.exec(css)?.[1] ?? 16);
|
|
61
|
+
for (const field of ["input", "select", "textarea"] as const) {
|
|
62
|
+
if (!new RegExp(`<${field}\\b`).test(html)) continue;
|
|
63
|
+
const px = fieldFontPx(css, field, bodyPx);
|
|
64
|
+
expect(px ?? bodyPx, `${name}: <${field}> is ${px ?? bodyPx}px — iOS zooms into a field under 16px`).toBeGreaterThanOrEqual(16);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
test("the reader sees an inherited 15px as 15px", () => {
|
|
70
|
+
expect(fieldFontPx("input{font:inherit}", "input", 15)).toBe(15);
|
|
71
|
+
expect(fieldFontPx("input{font:inherit;font-size:16px}", "input", 15)).toBe(16);
|
|
72
|
+
expect(fieldFontPx(".secret input{padding:0}", "input", 15)).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -130,7 +130,14 @@ export function lockPageHtml(options: LockPageOptions): string {
|
|
|
130
130
|
`);
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
/**
|
|
133
|
+
/**
|
|
134
|
+
* The stylesheet. Follows the viewer's theme and commits to nothing else.
|
|
135
|
+
*
|
|
136
|
+
* 🔴 Every field is 16px, never `inherit` alone. The body is 15px, and iOS zooms the page into
|
|
137
|
+
* any field under 16px when it takes focus — then the unlock's `location.reload()` keeps that
|
|
138
|
+
* zoom, so the app opened "slightly zoomed in" behind the wall (owner, collections on an iPhone,
|
|
139
|
+
* 2026-10-10). `fieldsNeverZoomIos.spec.ts` holds every page this module serves to it.
|
|
140
|
+
*/
|
|
134
141
|
export const LOCK_STYLE = `:root{color-scheme:light dark;
|
|
135
142
|
--ml-bg:#f4f5f7;--ml-card:#ffffff;--ml-text:#14171c;--ml-muted:#69707c;--ml-border:#d9dde3;
|
|
136
143
|
--ml-accent:#1f6feb;--ml-accent-text:#ffffff;--ml-danger:#b42318;
|
|
@@ -151,7 +158,7 @@ h1{margin:0 0 6px;font-size:18px;font-weight:650;letter-spacing:-.01em}
|
|
|
151
158
|
.field{display:block;text-align:left;margin-bottom:14px}
|
|
152
159
|
.field span{display:block;font-size:12px;font-weight:600;color:var(--ml-muted);margin-bottom:6px}
|
|
153
160
|
input{width:100%;height:40px;padding:0 12px;border:1px solid var(--ml-border);border-radius:9px;
|
|
154
|
-
background:var(--ml-bg);color:inherit;font:inherit}
|
|
161
|
+
background:var(--ml-bg);color:inherit;font:inherit;font-size:16px}
|
|
155
162
|
input:focus{outline:2px solid var(--ml-accent);outline-offset:1px;border-color:transparent}
|
|
156
163
|
button{width:100%;height:40px;border:0;border-radius:9px;background:var(--ml-accent);
|
|
157
164
|
color:var(--ml-accent-text);font:inherit;font-weight:600;cursor:pointer}
|