fix: make the app boot on an insecure origin, and re-aim the capture harness
Three things, all found by trying to re-shoot the product imagery and failing.
**`crypto.randomUUID` is secure-context only.** `main.ts` called it at module top
level for its three wire identities, so on any origin that is not HTTPS and not
`localhost` the call threw before the scene was built and the app stopped at
"Starting up" with one TypeError and no other symptom. Every way a developer
normally opens this app is a secure context — `vite dev` and `vite preview` serve
localhost, the deployed site is HTTPS — which is why this survived since 3326d2e.
It breaks the brand-capture harness, which serves `dist/` over
`http://tera.lumbridgecorp.com:5210` so the app can read its own hostname and
decide which door it is; it breaks the plain `http://` static host STATIC.md
explicitly invites; and it breaks opening the dev server by LAN IP to try it on a
phone. `src/ids.ts` prefers the platform's `randomUUID` and falls back to
`getRandomValues`, which carries no such restriction. The tests exercise the
fallback specifically, because the happy path was never the broken one.
**`waitForFunction` was ignoring its own timeout.** Playwright's signature is
`(pageFunction, arg, options)` and all four call sites in `shots.mjs` and
`films.mjs` passed `{ timeout: 180_000 }` second, binding it as the predicate's
argument. The wait silently used the 30 s default, which was invisible for as
long as the app booted inside thirty seconds and started failing the moment the
California board grew its relief — with "Timeout 30000ms exceeded" reported
against a line that plainly reads 180_000.
**The office shot list photographed a building that no longer exists.** The
`expect` guard caught it and refused to shoot, which is exactly what it is for:
chapter 0 is "Front Door" now, not "The Floor". But the captions were staler than
the labels — they described forty-eight metres by eighteen, thirty-six seats in
four benches and a fourteen-metre interstitial commons, and Lumbridge HQ is a
live/work studio now. Re-aimed at the buildings that exist: `office-floor` and
`office-desks` at the SF studio (the second specifically at the bench, because the
desk mic and the machine speaker are the new thing there), `office-commons` at the
LA courtyard, `office-hangar` still at Frontier Valley and now honest about being
in development. The four ids are deliberately unchanged: the manifest emits a
`ShotId` union that v4 imports, so renaming one fails v4's typecheck at push.
Also `#onboarding-host` joins the harness's clutter list. The first-run card is
correct behaviour for a real visitor and wrong in a product photograph, and it had
quietly placed itself in the middle of every frame — which is the general hazard
that list exists for, because a shot with a stray card still renders and still
looks deliberate.
`bay-relief`'s daylight frame moves from 07:40 to 09:10. At 07:40 the marine layer
buried the heightfield the shot exists to demonstrate; the version currently on the
site is almost entirely white. Its `note` moves with it, since the note names the
hour and a stale one describes light that is not in the picture.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+76
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* A random opaque identifier, in every context the app is actually served from.
|
||||
*
|
||||
* `crypto.randomUUID()` is only defined in a **secure context** — HTTPS, or a
|
||||
* `localhost` origin. `crypto.getRandomValues()` has no such restriction and is
|
||||
* available everywhere `crypto` is. That difference is easy to never notice,
|
||||
* because every way a developer normally opens this app is a secure context:
|
||||
* `vite dev` and `vite preview` both serve `localhost`, and the deployed site is
|
||||
* HTTPS.
|
||||
*
|
||||
* It is not a hypothetical gap. `main.ts` called `crypto.randomUUID()` at module
|
||||
* top level for its three wire identities, so on any other origin the call threw
|
||||
* before the scene was built and the app stopped at "Starting up" with one
|
||||
* `TypeError` in the console and no other symptom. That is:
|
||||
*
|
||||
* - the brand-capture harness, which serves `dist/` over
|
||||
* `http://tera.lumbridgecorp.com:5210` so the app can read its own hostname and
|
||||
* decide which door it is — this is how the bug was found, as a screenshot run
|
||||
* that timed out waiting for a boot that was never coming;
|
||||
* - a plain `http://` static host, which `deploy/STATIC.md` explicitly invites
|
||||
* ("Serve `dist/` from anything. A Content-Security-Policy of `default-src
|
||||
* 'self'` is sufficient") — the zero-config path this project cares about;
|
||||
* - anyone opening the dev server by LAN IP to try it on a phone.
|
||||
*
|
||||
* So the fallback is not defensive clutter. It is the difference between "runs
|
||||
* anywhere" being true and being a sentence in a document.
|
||||
*
|
||||
* The output is UUID-shaped rather than merely random because these strings go
|
||||
* on the realtime wire next to ids minted by other clients, and one shape is
|
||||
* easier to read in a log than two. Version and variant nibbles are set so it is
|
||||
* a well-formed v4 and not something that merely looks like one.
|
||||
*/
|
||||
|
||||
/** 16 random bytes, from the best source this context actually has. */
|
||||
function randomBytes(): Uint8Array {
|
||||
const bytes = new Uint8Array(16);
|
||||
const source = globalThis.crypto;
|
||||
if (source !== undefined && typeof source.getRandomValues === "function") {
|
||||
source.getRandomValues(bytes);
|
||||
return bytes;
|
||||
}
|
||||
// No `crypto` at all. Not reachable in a browser this app supports, and
|
||||
// reachable under a bare test runner, so it returns something valid rather
|
||||
// than throwing — these ids are opaque handles, never a security boundary.
|
||||
for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
const HEX: string[] = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, "0"));
|
||||
|
||||
/**
|
||||
* A v4-shaped identifier: `xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx`.
|
||||
*
|
||||
* Prefers the platform's own `randomUUID` when it exists, so that in the common
|
||||
* case this is exactly what the code it replaced produced.
|
||||
*/
|
||||
export function randomId(): string {
|
||||
const source = globalThis.crypto;
|
||||
if (source !== undefined && typeof source.randomUUID === "function") {
|
||||
return source.randomUUID();
|
||||
}
|
||||
|
||||
const b = randomBytes();
|
||||
// Version 4 in the high nibble of byte 6, RFC 4122 variant in byte 8.
|
||||
b[6] = (b[6]! & 0x0f) | 0x40;
|
||||
b[8] = (b[8]! & 0x3f) | 0x80;
|
||||
|
||||
const h = (i: number): string => HEX[b[i]!]!;
|
||||
return (
|
||||
h(0) + h(1) + h(2) + h(3) + "-" +
|
||||
h(4) + h(5) + "-" +
|
||||
h(6) + h(7) + "-" +
|
||||
h(8) + h(9) + "-" +
|
||||
h(10) + h(11) + h(12) + h(13) + h(14) + h(15)
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user