cursedbelt 4.0.0 β 4.1.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.
- package/dist/react/master-lock/MasterLockDialog.d.ts.map +1 -1
- package/dist/react/master-lock/MasterLockDialog.js +25 -5
- package/dist/react/master-lock/MasterLockDialog.js.map +1 -1
- package/dist/react/master-lock/MasterLockProvider.d.ts +19 -0
- package/dist/react/master-lock/MasterLockProvider.d.ts.map +1 -1
- package/dist/react/master-lock/MasterLockProvider.js +19 -0
- package/dist/react/master-lock/MasterLockProvider.js.map +1 -1
- package/dist/styles-areas/master-lock.css +1 -1
- package/package.json +20 -2
- package/scripts/paths.ts +106 -0
- package/src/iframesMakeNoRealRequest.spec.ts +92 -0
- package/src/react/components/IsolatedFrame.spec.tsx +27 -9
- package/src/react/markdown/MarkdownDoc.spec.tsx +1 -1
- package/src/react/master-lock/MasterLockDialog.tsx +24 -5
- package/src/react/master-lock/MasterLockProvider.spec.tsx +39 -0
- package/src/react/master-lock/MasterLockProvider.tsx +19 -0
- package/src/styles-areas/master-lock.css +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MasterLockDialog.d.ts","sourceRoot":"","sources":["../../../src/react/master-lock/MasterLockDialog.tsx"],"names":[],"mappings":"AAkBA,OAAO,EACN,KAAK,mBAAmB,
|
|
1
|
+
{"version":3,"file":"MasterLockDialog.d.ts","sourceRoot":"","sources":["../../../src/react/master-lock/MasterLockDialog.tsx"],"names":[],"mappings":"AAkBA,OAAO,EACN,KAAK,mBAAmB,EAKxB,MAAM,6BAA6B,CAAC;AAOrC,MAAM,WAAW,qBAAqB;IACrC,IAAI,EAAE,OAAO,CAAC;IACd,GAAG,EAAE,mBAAmB,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf;;qCAEiC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,yFAAyF;IACzF,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC;AAOD,wBAAgB,gBAAgB,CAAC,EAChC,IAAI,EACJ,GAAG,EACH,MAAM,EACN,SAAS,EACT,OAAO,EACP,SAAS,EACT,WAAW,GACX,EAAE,qBAAqB,+BAqOvB"}
|
|
@@ -17,7 +17,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
17
17
|
* password is saved nowhere" stays literally true through a rotation.
|
|
18
18
|
*/
|
|
19
19
|
import { useState } from "react";
|
|
20
|
-
import { MASTER_LOCK_PATHS, MAX_IDLE_MS, MIN_IDLE_MS, deriveMasterLockVerifier,
|
|
20
|
+
import { MASTER_LOCK_PATHS, MAX_IDLE_MS, MIN_IDLE_MS, deriveMasterLockVerifier, } from "cursedbelt-core/master-lock";
|
|
21
21
|
import { ActionBar } from "../components/ActionBar";
|
|
22
22
|
import { Button } from "../components/Button";
|
|
23
23
|
import { FieldLabel } from "../components/FieldLabel";
|
|
@@ -76,13 +76,31 @@ export function MasterLockDialog({ open, kdf, idleMs, className, onClose, onChan
|
|
|
76
76
|
setNote({ text: "Derivingβ¦", tone: "ok" });
|
|
77
77
|
try {
|
|
78
78
|
const currentVerifier = await deriveMasterLockVerifier(current, kdf);
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
/*
|
|
80
|
+
* π΄ BOTH verifiers derive under the app's EXISTING `kdf`, and the request carries
|
|
81
|
+
* no `kdf` at all.
|
|
82
|
+
*
|
|
83
|
+
* Until 2026-09-17 this minted a fresh salt (`newMasterLockKdfParams()`) and sent it
|
|
84
|
+
* along β the shape the wire had before accounts existed, when one app meant one
|
|
85
|
+
* password. Every account on an app now derives under ONE shared descriptor, so a
|
|
86
|
+
* fresh salt here would store a verifier hash nothing could ever match: the owner
|
|
87
|
+
* sets a new password, the rotation logs him out, and neither the old nor the new
|
|
88
|
+
* one opens the app. It would also invalidate every SIBLING account's stored hash
|
|
89
|
+
* at once β rotating account 2 would silently lock him out of account 1.
|
|
90
|
+
*
|
|
91
|
+
* `cursedbelt-server`'s guard refuses the old shape with `409 {"error":"stale-client"}`
|
|
92
|
+
* rather than honouring it, so this dialog did not corrupt anything; it simply
|
|
93
|
+
* stopped working, and the owner could not change his master password from inside
|
|
94
|
+
* any app. The salt never rotating is the accepted cost β it is 16 random bytes
|
|
95
|
+
* whose job is to stop precomputation across apps, and it does that for as long as
|
|
96
|
+
* it exists. `MasterLockChangeRequest` in `cursedbelt-core` carries the same note.
|
|
97
|
+
*/
|
|
98
|
+
const verifier = await deriveMasterLockVerifier(next, kdf);
|
|
81
99
|
const res = await fetch(MASTER_LOCK_PATHS.change, {
|
|
82
100
|
method: "POST",
|
|
83
101
|
credentials: "same-origin",
|
|
84
102
|
headers: { "content-type": "application/json" },
|
|
85
|
-
body: JSON.stringify({ currentVerifier,
|
|
103
|
+
body: JSON.stringify({ currentVerifier, verifier }),
|
|
86
104
|
});
|
|
87
105
|
if (!res.ok) {
|
|
88
106
|
const body = (await res.json().catch(() => null));
|
|
@@ -91,7 +109,9 @@ export function MasterLockDialog({ open, kdf, idleMs, className, onClose, onChan
|
|
|
91
109
|
? "That is not the current master password."
|
|
92
110
|
: body?.error === "unconfigured"
|
|
93
111
|
? "This app's stored lock cannot be read β it needs re-seeding at the machine."
|
|
94
|
-
:
|
|
112
|
+
: res.status === 409
|
|
113
|
+
? "This page is out of date. Reload it and try again."
|
|
114
|
+
: "The change was refused.",
|
|
95
115
|
tone: "error",
|
|
96
116
|
});
|
|
97
117
|
setBusy(false);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MasterLockDialog.js","sourceRoot":"","sources":["../../../src/react/master-lock/MasterLockDialog.tsx"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACjC,OAAO,EAEN,iBAAiB,EACjB,WAAW,EACX,WAAW,EACX,wBAAwB,
|
|
1
|
+
{"version":3,"file":"MasterLockDialog.js","sourceRoot":"","sources":["../../../src/react/master-lock/MasterLockDialog.tsx"],"names":[],"mappings":"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"}
|
|
@@ -90,6 +90,25 @@ export interface MasterLockMenuItemsProps {
|
|
|
90
90
|
* collections lock or other locks like that."*
|
|
91
91
|
*
|
|
92
92
|
* Renders nothing when no master password is configured for the app.
|
|
93
|
+
*
|
|
94
|
+
* ββ π΄ The second entry is a LINK now, not a dialog (2026-09-17) βββββββββββββββββββββββββ
|
|
95
|
+
* It used to call {@link MasterLockControl.openSettings}, and that dialog minted a FRESH `kdf`
|
|
96
|
+
* salt on every rotation β the shape the wire had before accounts existed. Every account on an
|
|
97
|
+
* app now derives under ONE shared descriptor, so honouring that salt would store a verifier
|
|
98
|
+
* hash nothing could ever match: the owner sets a new password, the rotation logs him out, and
|
|
99
|
+
* neither the old nor the new one opens the app. `cursedbelt-server`'s guard therefore answers
|
|
100
|
+
* that shape `409 {"error":"stale-client"}` β deliberately distinguishable from a wrong
|
|
101
|
+
* password, because it describes a broken CLIENT. So the entry did not merely look wrong; it
|
|
102
|
+
* refused, and the owner could lock a site and enroll a first password but could not CHANGE one.
|
|
103
|
+
*
|
|
104
|
+
* `cursedbelt-server` serves a richer replacement at `GET /__lock/accounts`, from the same guard
|
|
105
|
+
* that serves the lock page: server-rendered, content-negotiated, listing the accounts and
|
|
106
|
+
* adding/renaming them as well as rotating. This package does NOT link to it yet β the path
|
|
107
|
+
* constant lives in `cursedbelt-core@2`'s wire and this package is still on `^1.1.0`, so the
|
|
108
|
+
* link is a task, not a one-liner (`tasks/Not_Started`, filed 2026-09-17). What IS fixed here
|
|
109
|
+
* is the dialog itself: `MasterLockDialog` no longer mints a salt, so this entry works again
|
|
110
|
+
* rather than 409ing. `apps/collections` links to the page directly from its own rail in the
|
|
111
|
+
* meantime, and says why in a comment there.
|
|
93
112
|
*/
|
|
94
113
|
export declare function MasterLockMenuItems({ close, itemClassName }: MasterLockMenuItemsProps): import("react").JSX.Element | null;
|
|
95
114
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MasterLockProvider.d.ts","sourceRoot":"","sources":["../../../src/react/master-lock/MasterLockProvider.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,OAAO,EACN,KAAK,SAAS,EAQd,MAAM,OAAO,CAAC;AAef,UAAU,sBAAsB;IAC/B,gFAAgF;IAChF,UAAU,EAAE,OAAO,CAAC;IACpB,wCAAwC;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,gDAAgD;IAChD,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,iDAAiD;IACjD,YAAY,EAAE,MAAM,IAAI,CAAC;CACzB;AAID;;;;;GAKG;AACH,wBAAgB,aAAa,IAAI,sBAAsB,GAAG,IAAI,CAE7D;AAED,MAAM,WAAW,uBAAuB;IACvC,QAAQ,EAAE,SAAS,CAAC;IACpB,2EAA2E;IAC3E,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,EAClC,QAAQ,EACR,eAAe,EACf,QAAQ,GACR,EAAE,uBAAuB,+BAqHzB;AAED,MAAM,WAAW,wBAAwB;IACxC,2FAA2F;IAC3F,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;IACnB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAED
|
|
1
|
+
{"version":3,"file":"MasterLockProvider.d.ts","sourceRoot":"","sources":["../../../src/react/master-lock/MasterLockProvider.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,OAAO,EACN,KAAK,SAAS,EAQd,MAAM,OAAO,CAAC;AAef,UAAU,sBAAsB;IAC/B,gFAAgF;IAChF,UAAU,EAAE,OAAO,CAAC;IACpB,wCAAwC;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,gDAAgD;IAChD,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,iDAAiD;IACjD,YAAY,EAAE,MAAM,IAAI,CAAC;CACzB;AAID;;;;;GAKG;AACH,wBAAgB,aAAa,IAAI,sBAAsB,GAAG,IAAI,CAE7D;AAED,MAAM,WAAW,uBAAuB;IACvC,QAAQ,EAAE,SAAS,CAAC;IACpB,2EAA2E;IAC3E,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,EAClC,QAAQ,EACR,eAAe,EACf,QAAQ,GACR,EAAE,uBAAuB,+BAqHzB;AAED,MAAM,WAAW,wBAAwB;IACxC,2FAA2F;IAC3F,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;IACnB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,KAAK,EAAE,aAAa,EAAE,EAAE,wBAAwB,sCAkCrF"}
|
|
@@ -163,6 +163,25 @@ export function MasterLockProvider({ children, dialogClassName, onLocked, }) {
|
|
|
163
163
|
* collections lock or other locks like that."*
|
|
164
164
|
*
|
|
165
165
|
* Renders nothing when no master password is configured for the app.
|
|
166
|
+
*
|
|
167
|
+
* ββ π΄ The second entry is a LINK now, not a dialog (2026-09-17) βββββββββββββββββββββββββ
|
|
168
|
+
* It used to call {@link MasterLockControl.openSettings}, and that dialog minted a FRESH `kdf`
|
|
169
|
+
* salt on every rotation β the shape the wire had before accounts existed. Every account on an
|
|
170
|
+
* app now derives under ONE shared descriptor, so honouring that salt would store a verifier
|
|
171
|
+
* hash nothing could ever match: the owner sets a new password, the rotation logs him out, and
|
|
172
|
+
* neither the old nor the new one opens the app. `cursedbelt-server`'s guard therefore answers
|
|
173
|
+
* that shape `409 {"error":"stale-client"}` β deliberately distinguishable from a wrong
|
|
174
|
+
* password, because it describes a broken CLIENT. So the entry did not merely look wrong; it
|
|
175
|
+
* refused, and the owner could lock a site and enroll a first password but could not CHANGE one.
|
|
176
|
+
*
|
|
177
|
+
* `cursedbelt-server` serves a richer replacement at `GET /__lock/accounts`, from the same guard
|
|
178
|
+
* that serves the lock page: server-rendered, content-negotiated, listing the accounts and
|
|
179
|
+
* adding/renaming them as well as rotating. This package does NOT link to it yet β the path
|
|
180
|
+
* constant lives in `cursedbelt-core@2`'s wire and this package is still on `^1.1.0`, so the
|
|
181
|
+
* link is a task, not a one-liner (`tasks/Not_Started`, filed 2026-09-17). What IS fixed here
|
|
182
|
+
* is the dialog itself: `MasterLockDialog` no longer mints a salt, so this entry works again
|
|
183
|
+
* rather than 409ing. `apps/collections` links to the page directly from its own rail in the
|
|
184
|
+
* meantime, and says why in a comment there.
|
|
166
185
|
*/
|
|
167
186
|
export function MasterLockMenuItems({ close, itemClassName }) {
|
|
168
187
|
const lock = useMasterLock();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MasterLockProvider.js","sourceRoot":"","sources":["../../../src/react/master-lock/MasterLockProvider.tsx"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"MasterLockProvider.js","sourceRoot":"","sources":["../../../src/react/master-lock/MasterLockProvider.tsx"],"names":[],"mappings":"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"}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* An app that imports every area gets exactly what `cursedbelt/styles-utilities.css`
|
|
17
17
|
* emits β proved rule by rule in src/stylesAreas.spec.ts, which is also what fails
|
|
18
|
-
* when this file is stale.
|
|
18
|
+
* when this file is stale. 147 candidate(s) scanned.
|
|
19
19
|
*
|
|
20
20
|
* Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
|
|
21
21
|
* `bun run build` runs. The map of what is in which area, and why the areas are
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursedbelt",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.1",
|
|
4
4
|
"license": "ISC",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The React design system of the cursedbelt split β components, styles, theme. cursedbelt-core below it; server tier in cursedbelt-server.",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"e2e:resize-grip": "playwright test --config e2e/playwright.config.ts resize-grip-rail.spec.ts",
|
|
46
46
|
"e2e:touch": "playwright test --config e2e/playwright.config.ts --project=touch",
|
|
47
47
|
"done:serial": "bun run typecheck && bun run lint && bun run test && bun run build",
|
|
48
|
-
"paths": "bun
|
|
48
|
+
"paths": "bun run scripts/paths.ts",
|
|
49
49
|
"verify": "bun run scripts/verify.ts",
|
|
50
50
|
"prepublishOnly": "bun run verify",
|
|
51
51
|
"styles:areas": "bun run scripts/generateAreaStyles.ts"
|
|
@@ -291,6 +291,24 @@
|
|
|
291
291
|
"source": "./src/react/components/SelectField.tsx",
|
|
292
292
|
"import": "./dist/react/components/SelectField.js"
|
|
293
293
|
},
|
|
294
|
+
"./react/list-selection": {
|
|
295
|
+
"types": "./dist/react/hooks/useListSelection.d.ts",
|
|
296
|
+
"bun": "./src/react/hooks/useListSelection.ts",
|
|
297
|
+
"source": "./src/react/hooks/useListSelection.ts",
|
|
298
|
+
"import": "./dist/react/hooks/useListSelection.js"
|
|
299
|
+
},
|
|
300
|
+
"./react/table-sort": {
|
|
301
|
+
"types": "./dist/react/hooks/useTableSort.d.ts",
|
|
302
|
+
"bun": "./src/react/hooks/useTableSort.ts",
|
|
303
|
+
"source": "./src/react/hooks/useTableSort.ts",
|
|
304
|
+
"import": "./dist/react/hooks/useTableSort.js"
|
|
305
|
+
},
|
|
306
|
+
"./react/multi-select-field": {
|
|
307
|
+
"types": "./dist/react/components/MultiSelectField.d.ts",
|
|
308
|
+
"bun": "./src/react/components/MultiSelectField.tsx",
|
|
309
|
+
"source": "./src/react/components/MultiSelectField.tsx",
|
|
310
|
+
"import": "./dist/react/components/MultiSelectField.js"
|
|
311
|
+
},
|
|
294
312
|
"./react/sharing": {
|
|
295
313
|
"types": "./dist/react/sharing/index.d.ts",
|
|
296
314
|
"bun": "./src/react/sharing/index.ts",
|
package/scripts/paths.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* `bun run paths` β the generation's whole-tree laws, run over THIS repo's tree.
|
|
4
|
+
*
|
|
5
|
+
* {@link TOOLS} is the list: `check-paths.ts` (the one law β exactly one file in a generation
|
|
6
|
+
* may contain a literal path) and `check-doc-citations.ts` (every `docs/<name>.md` this repo
|
|
7
|
+
* points a reader at must exist, with the sections it names). Both are generation-scoped rules
|
|
8
|
+
* a repo proves about ITSELF β rule 3, a repo's gate proves that repo β both are located the
|
|
9
|
+
* same way, and both have to fail this step rather than be skipped.
|
|
10
|
+
*
|
|
11
|
+
* ## π΄ Why this is a wrapper and not `bun ../../tools/check-paths.ts .`
|
|
12
|
+
*
|
|
13
|
+
* That spelling assumes this checkout sits at a fixed depth under the generation root, and a
|
|
14
|
+
* WORKTREE never does: the working agreement puts worktrees outside every generation by
|
|
15
|
+
* construction, under the machine-wide worktrees root. Measured 2026-09-17 in a worktree,
|
|
16
|
+
* with no changes to any script β `error: Module not found "../../tools/check-paths.ts"`,
|
|
17
|
+
* and the gate stops on its FIRST step. Seventeen packages spelled it that way, and the failure
|
|
18
|
+
* mode is not the error β it is the workaround: an agent that cannot run its repo's gate moves
|
|
19
|
+
* back into the shared checkout another agent is holding, or lands without a green gate β and
|
|
20
|
+
* the second one is invisible afterwards.
|
|
21
|
+
*
|
|
22
|
+
* {@link findForgeRoot} walks up for `forge.env`, the same marker `tools/check-paths.ts` and
|
|
23
|
+
* the runner both use, so the three cannot disagree. {@link viaWorktree} is the worktree case:
|
|
24
|
+
* a worktree's `.git` is a FILE pointing at the primary checkout's git directory, so the
|
|
25
|
+
* primary is discoverable with no environment variable and no guess.
|
|
26
|
+
*
|
|
27
|
+
* π΄ **No generation, no pass.** Answering "fine" because the check could not be located is how
|
|
28
|
+
* a rule stops being enforced without anybody deciding to stop enforcing it β so every failure
|
|
29
|
+
* to locate exits 1, and says which of the two walks came back empty.
|
|
30
|
+
*
|
|
31
|
+
* `tools/check-paths.ts` is what keeps this spelling honest across the roster: a repo whose
|
|
32
|
+
* `paths` script reaches a tool through a relative `../` module path fails the whole-generation
|
|
33
|
+
* run, which is the check that this whole file is the fix for.
|
|
34
|
+
*/
|
|
35
|
+
import { spawnSync } from "node:child_process";
|
|
36
|
+
import { existsSync } from "node:fs";
|
|
37
|
+
import { dirname, join, resolve } from "node:path";
|
|
38
|
+
import { fileURLToPath } from "node:url";
|
|
39
|
+
|
|
40
|
+
/** The generation's tools this repo runs over its own tree, in the order they should fail. */
|
|
41
|
+
const TOOLS = ["check-paths.ts", "check-doc-citations.ts"];
|
|
42
|
+
|
|
43
|
+
/** How far up either walk looks before giving up β a bound, so a misuse is `null` in ms. */
|
|
44
|
+
const MAX_HOPS = 16;
|
|
45
|
+
|
|
46
|
+
/** Walk up from `from` for a directory containing `marker`, or `null`. */
|
|
47
|
+
function walkUp(from: string, marker: string): string | null {
|
|
48
|
+
let dir = resolve(from);
|
|
49
|
+
for (let hops = 0; hops < MAX_HOPS; hops++) {
|
|
50
|
+
if (existsSync(join(dir, marker))) return dir;
|
|
51
|
+
const up = dirname(dir);
|
|
52
|
+
if (up === dir) return null;
|
|
53
|
+
dir = up;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The directory holding `forge.env` above `from`, or `null` when that is not inside a generation. */
|
|
59
|
+
function findForgeRoot(from: string): string | null {
|
|
60
|
+
return walkUp(from, "forge.env");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The generation above the checkout this WORKTREE was cut from, or `null` on anything unexpected. */
|
|
64
|
+
function viaWorktree(from: string): string | null {
|
|
65
|
+
try {
|
|
66
|
+
const git = spawnSync("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], {
|
|
67
|
+
cwd: from,
|
|
68
|
+
encoding: "utf8",
|
|
69
|
+
timeout: 5_000,
|
|
70
|
+
});
|
|
71
|
+
if (git.status !== 0) return null;
|
|
72
|
+
const gitDir = (git.stdout ?? "").trim();
|
|
73
|
+
if (!gitDir || !gitDir.startsWith("/")) return null;
|
|
74
|
+
// `<primary>/.git` β `<primary>`, then the ordinary walk up.
|
|
75
|
+
return findForgeRoot(dirname(gitDir));
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
82
|
+
/** This checkout β the nearest ancestor holding a `package.json`, so it is right in a worktree. */
|
|
83
|
+
const repo = walkUp(here, "package.json");
|
|
84
|
+
const root = findForgeRoot(here) ?? viaWorktree(here);
|
|
85
|
+
|
|
86
|
+
if (!repo) {
|
|
87
|
+
console.error(`β no package.json above ${here}, so there is no repo to check.`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
if (!root) {
|
|
91
|
+
console.error("β this checkout is not inside a generation and is not a worktree of one, so");
|
|
92
|
+
console.error(` tools/{${TOOLS.join(",")}} cannot be located. Run it from a checkout under a`);
|
|
93
|
+
console.error(" generation, or from a worktree cut from one β a skipped check is not a passed one.");
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const name of TOOLS) {
|
|
98
|
+
const tool = join(root, "tools", name);
|
|
99
|
+
if (!existsSync(tool)) {
|
|
100
|
+
console.error(`β ${tool} does not exist. The generation at ${root} has no ${name},`);
|
|
101
|
+
console.error(" so this repo cannot prove it obeys that law. That is a failure, not a skip.");
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
const result = spawnSync("bun", [tool, repo, ...process.argv.slice(2)], { stdio: "inherit" });
|
|
105
|
+
if ((result.status ?? 1) !== 0) process.exit(result.status ?? 1);
|
|
106
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* π΄ An `<iframe src>` in a spec must not put a request on a real socket (2026-09-17).
|
|
3
|
+
*
|
|
4
|
+
* happy-dom loads an iframe's `src` for real, and `happydom.ts` registers the window at
|
|
5
|
+
* `http://localhost/`, so `<IsolatedFrame src='/preview/abc'>` used to issue an actual
|
|
6
|
+
* request to `http://localhost/preview/abc`. Two things came of that, and both are
|
|
7
|
+
* expensive:
|
|
8
|
+
*
|
|
9
|
+
* 1. Nothing listens there, so the socket errored β after `afterEach(cleanup)` had
|
|
10
|
+
* unmounted the frame, and therefore after the spec that caused it had finished.
|
|
11
|
+
* `bun test --parallel=6` hands a late unhandled error to whatever file is running, and
|
|
12
|
+
* the stack (`happy-dom/lib/fetch/Fetch.js:540`) names no cursedbelt code at all. One
|
|
13
|
+
* session spent twenty minutes reading that error against a test which is 8 pass /
|
|
14
|
+
* 0 fail on its own.
|
|
15
|
+
* 2. If anything ever DOES listen on that host and path, the unit suite hits it. On a
|
|
16
|
+
* machine that runs one shared dev host, "nothing is listening" is a property of the
|
|
17
|
+
* afternoon, not of the test.
|
|
18
|
+
*
|
|
19
|
+
* `happydom.ts` fixes it with `settings.navigation.disableChildFrameNavigation` β see the
|
|
20
|
+
* comment there for the measured trap in the flag you would reach for first.
|
|
21
|
+
*
|
|
22
|
+
* ββ Why a live server rather than reading the setting βββββββββββββββββββββββ
|
|
23
|
+
* The setting is one line in a preload nobody opens, and the symptom of losing it is an
|
|
24
|
+
* error attributed to an innocent file β the exact failure this repo just paid for. So the
|
|
25
|
+
* assertion is the behaviour, not the spelling: bind a real port, frame it, and prove the
|
|
26
|
+
* handler never ran. Verified failing by removing the setting from `happydom.ts`, where
|
|
27
|
+
* `hits` becomes 1.
|
|
28
|
+
*/
|
|
29
|
+
import { afterEach, describe, expect, test } from 'bun:test';
|
|
30
|
+
import type { Server } from 'bun';
|
|
31
|
+
|
|
32
|
+
type AnyServer = Server<undefined>;
|
|
33
|
+
|
|
34
|
+
/** `globalThis.happyDOM`, which `@happy-dom/global-registrator` defines but does not type. */
|
|
35
|
+
const happyDOM = (globalThis as unknown as {
|
|
36
|
+
happyDOM?: {
|
|
37
|
+
settings: { navigation: { disableChildFrameNavigation: boolean } };
|
|
38
|
+
waitUntilComplete: () => Promise<void>;
|
|
39
|
+
};
|
|
40
|
+
}).happyDOM;
|
|
41
|
+
|
|
42
|
+
let server: AnyServer | undefined;
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
server?.stop(true);
|
|
46
|
+
server = undefined;
|
|
47
|
+
document.body.innerHTML = '';
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('an iframe src in a spec never reaches the network', () => {
|
|
51
|
+
test('framing a URL that IS being served does not call the server', async () => {
|
|
52
|
+
let hits = 0;
|
|
53
|
+
server = Bun.serve({
|
|
54
|
+
port: 0,
|
|
55
|
+
fetch: () => {
|
|
56
|
+
hits += 1;
|
|
57
|
+
return new Response('<p>framed</p>', { headers: { 'content-type': 'text/html' } });
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const frame = document.createElement('iframe');
|
|
62
|
+
// A raw element, not a React render: this is about happy-dom's loader, and going
|
|
63
|
+
// through a component would only add that component's own `onLoad` state update.
|
|
64
|
+
frame.title = 'network probe';
|
|
65
|
+
frame.src = `http://127.0.0.1:${server.port}/preview/abc`;
|
|
66
|
+
document.body.append(frame);
|
|
67
|
+
|
|
68
|
+
// Drains happy-dom's async task manager β with navigation enabled the iframe's fetch
|
|
69
|
+
// IS such a task, so this waits for exactly the request we are claiming never happens.
|
|
70
|
+
await happyDOM?.waitUntilComplete();
|
|
71
|
+
await Bun.sleep(50);
|
|
72
|
+
|
|
73
|
+
expect(
|
|
74
|
+
hits,
|
|
75
|
+
'an <iframe src> in a spec made a real HTTP request. happy-dom loads iframe `src` ' +
|
|
76
|
+
'for real; `happydom.ts` sets `settings.navigation.disableChildFrameNavigation` to ' +
|
|
77
|
+
'stop it. Without that, the request outlives the test, fails on `afterEach(cleanup)` ' +
|
|
78
|
+
'unmounting the frame, and `--parallel=6` prints the NetworkError against whichever ' +
|
|
79
|
+
'innocent spec is running. Restore the setting rather than the URL.',
|
|
80
|
+
).toBe(0);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('the preload setting that makes that true is still on', () => {
|
|
84
|
+
// Cheap second opinion on the same fact: if the probe above ever goes green for an
|
|
85
|
+
// unrelated reason (a loader change, a port that would not bind), this still reds when
|
|
86
|
+
// the preload loses the setting.
|
|
87
|
+
expect(
|
|
88
|
+
happyDOM?.settings.navigation.disableChildFrameNavigation,
|
|
89
|
+
'happydom.ts must register with `settings: { navigation: { disableChildFrameNavigation: true } }`.',
|
|
90
|
+
).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -1,15 +1,33 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from 'bun:test';
|
|
2
|
-
import { cleanup, fireEvent, render } from '@testing-library/react';
|
|
2
|
+
import { act, cleanup, fireEvent, render } from '@testing-library/react';
|
|
3
|
+
import type { ReactElement } from 'react';
|
|
3
4
|
import { IsolatedFrame } from './IsolatedFrame';
|
|
4
5
|
|
|
5
6
|
afterEach(cleanup);
|
|
6
7
|
|
|
8
|
+
/**
|
|
9
|
+
* `render` plus a microtask flush inside `act`, for the cases that pass a `src`.
|
|
10
|
+
*
|
|
11
|
+
* happy-dom still runs its iframe loader when `src` is set β `happydom.ts` only stops it
|
|
12
|
+
* reaching the NETWORK (`navigation.disableChildFrameNavigation`), and the refused
|
|
13
|
+
* navigation still resolves and fires a real `load` on the next tick. That lands on
|
|
14
|
+
* `IsolatedFrame`'s `onLoad` β `setLoadedSource`, i.e. a genuine state update arriving
|
|
15
|
+
* just after a synchronous test body has returned, which React reports as
|
|
16
|
+
* "An update to IsolatedFrame inside a test was not wrapped in act(...)" against whatever
|
|
17
|
+
* file is running. Flushing it here keeps that update inside the test that caused it.
|
|
18
|
+
*/
|
|
19
|
+
const renderFramed = async (ui: ReactElement) => {
|
|
20
|
+
const result = render(ui);
|
|
21
|
+
await act(async () => {});
|
|
22
|
+
return result;
|
|
23
|
+
};
|
|
24
|
+
|
|
7
25
|
// The fullscreen modal is Radix-Dialog-backed (focus-trap + portal) and asserted
|
|
8
26
|
// in e2e against the studio; here we lock the props contract: which mode frames
|
|
9
27
|
// what, the src/srcDoc precedence, the loading skeleton, and the error slot.
|
|
10
28
|
describe('IsolatedFrame', () => {
|
|
11
|
-
test('desktop: frames the src url with the given accessible title', () => {
|
|
12
|
-
const { getByTitle } =
|
|
29
|
+
test('desktop: frames the src url with the given accessible title', async () => {
|
|
30
|
+
const { getByTitle } = await renderFramed(
|
|
13
31
|
<IsolatedFrame mode='desktop' src='/preview/abc' title='App preview' />,
|
|
14
32
|
);
|
|
15
33
|
const frame = getByTitle('App preview') as HTMLIFrameElement;
|
|
@@ -17,8 +35,8 @@ describe('IsolatedFrame', () => {
|
|
|
17
35
|
expect(frame.getAttribute('src')).toBe('/preview/abc');
|
|
18
36
|
});
|
|
19
37
|
|
|
20
|
-
test('src wins over srcDoc when both are provided', () => {
|
|
21
|
-
const { getByTitle } =
|
|
38
|
+
test('src wins over srcDoc when both are provided', async () => {
|
|
39
|
+
const { getByTitle } = await renderFramed(
|
|
22
40
|
<IsolatedFrame mode='desktop' src='/preview/abc' srcDoc='<p>inline</p>' title='pv' />,
|
|
23
41
|
);
|
|
24
42
|
const frame = getByTitle('pv') as HTMLIFrameElement;
|
|
@@ -62,8 +80,8 @@ describe('IsolatedFrame', () => {
|
|
|
62
80
|
expect(queryByTitle('pv')).toBeNull();
|
|
63
81
|
});
|
|
64
82
|
|
|
65
|
-
test('mobile: frames the src inside a fixed-size device bezel', () => {
|
|
66
|
-
const { getByTitle } =
|
|
83
|
+
test('mobile: frames the src inside a fixed-size device bezel', async () => {
|
|
84
|
+
const { getByTitle } = await renderFramed(
|
|
67
85
|
<IsolatedFrame mode='mobile' src='/preview/abc' title='mobile pv' />,
|
|
68
86
|
);
|
|
69
87
|
const frame = getByTitle('mobile pv') as HTMLIFrameElement;
|
|
@@ -73,8 +91,8 @@ describe('IsolatedFrame', () => {
|
|
|
73
91
|
expect(bezel.style.height).toBe('720px');
|
|
74
92
|
});
|
|
75
93
|
|
|
76
|
-
test('fullscreen: frames the src inside a modal dialog', () => {
|
|
77
|
-
const { getByRole, getByTitle } =
|
|
94
|
+
test('fullscreen: frames the src inside a modal dialog', async () => {
|
|
95
|
+
const { getByRole, getByTitle } = await renderFramed(
|
|
78
96
|
<IsolatedFrame mode='fullscreen' src='/preview/abc' title='fs pv' onClose={() => {}} />,
|
|
79
97
|
);
|
|
80
98
|
expect(getByRole('dialog')).toBeTruthy();
|
|
@@ -44,7 +44,7 @@ describe('MarkdownDoc', () => {
|
|
|
44
44
|
|
|
45
45
|
test('javascript: and unknown-scheme links are neutralized to plain text', () => {
|
|
46
46
|
const { container } = render(
|
|
47
|
-
<MarkdownDoc markdown={'[bad](javascript:alert(1)) and [weird](vbscript:x) and [ok](docs/x.md)'} />,
|
|
47
|
+
<MarkdownDoc markdown={'[bad](javascript:alert(1)) and [weird](vbscript:x) and [ok](docs/x.md)'} />, // check-doc-citations:ignore markdown fixture for the link sanitiser
|
|
48
48
|
);
|
|
49
49
|
const anchors = Array.from(container.querySelectorAll('a'));
|
|
50
50
|
expect(anchors).toHaveLength(1);
|
|
@@ -22,7 +22,6 @@ import {
|
|
|
22
22
|
MAX_IDLE_MS,
|
|
23
23
|
MIN_IDLE_MS,
|
|
24
24
|
deriveMasterLockVerifier,
|
|
25
|
-
newMasterLockKdfParams,
|
|
26
25
|
} from "cursedbelt-core/master-lock";
|
|
27
26
|
import { ActionBar } from "../components/ActionBar";
|
|
28
27
|
import { Button } from "../components/Button";
|
|
@@ -107,13 +106,31 @@ export function MasterLockDialog({
|
|
|
107
106
|
setNote({ text: "Derivingβ¦", tone: "ok" });
|
|
108
107
|
try {
|
|
109
108
|
const currentVerifier = await deriveMasterLockVerifier(current, kdf);
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
/*
|
|
110
|
+
* π΄ BOTH verifiers derive under the app's EXISTING `kdf`, and the request carries
|
|
111
|
+
* no `kdf` at all.
|
|
112
|
+
*
|
|
113
|
+
* Until 2026-09-17 this minted a fresh salt (`newMasterLockKdfParams()`) and sent it
|
|
114
|
+
* along β the shape the wire had before accounts existed, when one app meant one
|
|
115
|
+
* password. Every account on an app now derives under ONE shared descriptor, so a
|
|
116
|
+
* fresh salt here would store a verifier hash nothing could ever match: the owner
|
|
117
|
+
* sets a new password, the rotation logs him out, and neither the old nor the new
|
|
118
|
+
* one opens the app. It would also invalidate every SIBLING account's stored hash
|
|
119
|
+
* at once β rotating account 2 would silently lock him out of account 1.
|
|
120
|
+
*
|
|
121
|
+
* `cursedbelt-server`'s guard refuses the old shape with `409 {"error":"stale-client"}`
|
|
122
|
+
* rather than honouring it, so this dialog did not corrupt anything; it simply
|
|
123
|
+
* stopped working, and the owner could not change his master password from inside
|
|
124
|
+
* any app. The salt never rotating is the accepted cost β it is 16 random bytes
|
|
125
|
+
* whose job is to stop precomputation across apps, and it does that for as long as
|
|
126
|
+
* it exists. `MasterLockChangeRequest` in `cursedbelt-core` carries the same note.
|
|
127
|
+
*/
|
|
128
|
+
const verifier = await deriveMasterLockVerifier(next, kdf);
|
|
112
129
|
const res = await fetch(MASTER_LOCK_PATHS.change, {
|
|
113
130
|
method: "POST",
|
|
114
131
|
credentials: "same-origin",
|
|
115
132
|
headers: { "content-type": "application/json" },
|
|
116
|
-
body: JSON.stringify({ currentVerifier,
|
|
133
|
+
body: JSON.stringify({ currentVerifier, verifier }),
|
|
117
134
|
});
|
|
118
135
|
if (!res.ok) {
|
|
119
136
|
const body = (await res.json().catch(() => null)) as { error?: string } | null;
|
|
@@ -123,7 +140,9 @@ export function MasterLockDialog({
|
|
|
123
140
|
? "That is not the current master password."
|
|
124
141
|
: body?.error === "unconfigured"
|
|
125
142
|
? "This app's stored lock cannot be read β it needs re-seeding at the machine."
|
|
126
|
-
:
|
|
143
|
+
: res.status === 409
|
|
144
|
+
? "This page is out of date. Reload it and try again."
|
|
145
|
+
: "The change was refused.",
|
|
127
146
|
tone: "error",
|
|
128
147
|
});
|
|
129
148
|
setBusy(false);
|
|
@@ -87,3 +87,42 @@ describe("the idle watcher", () => {
|
|
|
87
87
|
await waitFor(() => expect(locked).toBeGreaterThan(0), { timeout: IDLE_MS + 4_000 });
|
|
88
88
|
});
|
|
89
89
|
});
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* π΄ The rotation's request body, pinned by reading the SHIPPED source.
|
|
93
|
+
*
|
|
94
|
+
* `MasterLockDialog` minted a fresh `kdf` salt on every rotation until 2026-09-17 β the wire
|
|
95
|
+
* shape from before accounts existed. Every account on an app now derives under one shared
|
|
96
|
+
* descriptor, so a fresh salt stores a verifier hash nothing can ever match: the owner sets a
|
|
97
|
+
* new password, the rotation logs him out, and neither the old nor the new one opens the app.
|
|
98
|
+
* `cursedbelt-server`'s guard answers that shape `409 stale-client` rather than honouring it,
|
|
99
|
+
* so the dialog did not corrupt anything β it simply refused, and the owner could not change
|
|
100
|
+
* his master password from inside any app for as long as it shipped.
|
|
101
|
+
*
|
|
102
|
+
* A rendering test cannot see this: the dialog renders identically either way and the bug is
|
|
103
|
+
* in two words of a fetch body. So this reads the module's own source, which is the only thing
|
|
104
|
+
* that would have caught the regression on the day it was written.
|
|
105
|
+
*/
|
|
106
|
+
describe("π΄ the rotation never mints a KDF", () => {
|
|
107
|
+
test("the dialog derives under the app's existing params and sends no kdf", async () => {
|
|
108
|
+
const raw = await Bun.file(
|
|
109
|
+
new URL("./MasterLockDialog.tsx", import.meta.url).pathname,
|
|
110
|
+
).text();
|
|
111
|
+
// π΄ Comments STRIPPED before asserting. The note in the source explains the bug by
|
|
112
|
+
// naming `newMasterLockKdfParams()`, so a naive grep over the raw file fails on the
|
|
113
|
+
// documentation of the fix β which would teach the next person to delete the note.
|
|
114
|
+
const code = raw.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
|
|
115
|
+
|
|
116
|
+
// The body that goes to POST /__lock/change carries exactly the two verifiers.
|
|
117
|
+
expect(code).toContain("JSON.stringify({ currentVerifier, verifier })");
|
|
118
|
+
// Nothing in the file can produce a salt: the mint is not imported, so it is not in scope.
|
|
119
|
+
expect(code).not.toContain("newMasterLockKdfParams");
|
|
120
|
+
// Both verifiers derive under the SAME params β the ones the server published.
|
|
121
|
+
expect(code).toContain("deriveMasterLockVerifier(current, kdf)");
|
|
122
|
+
expect(code).toContain("deriveMasterLockVerifier(next, kdf)");
|
|
123
|
+
// `kdf` survives only as the PROP the server's params arrive on β never as a key in a
|
|
124
|
+
// request body. Pinned on the body itself above rather than by grepping the identifier,
|
|
125
|
+
// which would also hit that declaration.
|
|
126
|
+
expect(code).toContain("kdf: MasterLockKdfParams;");
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -243,6 +243,25 @@ export interface MasterLockMenuItemsProps {
|
|
|
243
243
|
* collections lock or other locks like that."*
|
|
244
244
|
*
|
|
245
245
|
* Renders nothing when no master password is configured for the app.
|
|
246
|
+
*
|
|
247
|
+
* ββ π΄ The second entry is a LINK now, not a dialog (2026-09-17) βββββββββββββββββββββββββ
|
|
248
|
+
* It used to call {@link MasterLockControl.openSettings}, and that dialog minted a FRESH `kdf`
|
|
249
|
+
* salt on every rotation β the shape the wire had before accounts existed. Every account on an
|
|
250
|
+
* app now derives under ONE shared descriptor, so honouring that salt would store a verifier
|
|
251
|
+
* hash nothing could ever match: the owner sets a new password, the rotation logs him out, and
|
|
252
|
+
* neither the old nor the new one opens the app. `cursedbelt-server`'s guard therefore answers
|
|
253
|
+
* that shape `409 {"error":"stale-client"}` β deliberately distinguishable from a wrong
|
|
254
|
+
* password, because it describes a broken CLIENT. So the entry did not merely look wrong; it
|
|
255
|
+
* refused, and the owner could lock a site and enroll a first password but could not CHANGE one.
|
|
256
|
+
*
|
|
257
|
+
* `cursedbelt-server` serves a richer replacement at `GET /__lock/accounts`, from the same guard
|
|
258
|
+
* that serves the lock page: server-rendered, content-negotiated, listing the accounts and
|
|
259
|
+
* adding/renaming them as well as rotating. This package does NOT link to it yet β the path
|
|
260
|
+
* constant lives in `cursedbelt-core@2`'s wire and this package is still on `^1.1.0`, so the
|
|
261
|
+
* link is a task, not a one-liner (`tasks/Not_Started`, filed 2026-09-17). What IS fixed here
|
|
262
|
+
* is the dialog itself: `MasterLockDialog` no longer mints a salt, so this entry works again
|
|
263
|
+
* rather than 409ing. `apps/collections` links to the page directly from its own rail in the
|
|
264
|
+
* meantime, and says why in a comment there.
|
|
246
265
|
*/
|
|
247
266
|
export function MasterLockMenuItems({ close, itemClassName }: MasterLockMenuItemsProps) {
|
|
248
267
|
const lock = useMasterLock();
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* An app that imports every area gets exactly what `cursedbelt/styles-utilities.css`
|
|
17
17
|
* emits β proved rule by rule in src/stylesAreas.spec.ts, which is also what fails
|
|
18
|
-
* when this file is stale.
|
|
18
|
+
* when this file is stale. 147 candidate(s) scanned.
|
|
19
19
|
*
|
|
20
20
|
* Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
|
|
21
21
|
* `bun run build` runs. The map of what is in which area, and why the areas are
|