456a91d289
- F1 [HIGH]: added a root ChunkLoadErrorBoundary (react-error-boundary) wrapping
the routed app in main.tsx, ABOVE all the route-level/Aside/AiChatWindow
Suspense boundaries. A stale-deploy chunk 404 (React.lazy reject) is caught and
auto-reloads once (sessionStorage-guarded against a reload loop), else shows a
manual "new version available" reload UI — instead of unmounting the whole tree
to a white screen. Existing per-feature ErrorBoundaries untouched.
- F2 [MED-HIGH]: posthog no longer blocks/blanks the cloud first paint. main.tsx
now renders <App/> immediately for everyone, then `void initAnalytics()` — which
keeps the exact cloud gate, dynamically imports posthog, and RE-RENDERS the same
React root wrapped in PostHogProvider (React reconciles onto the painted DOM, so
cloud ends up wrapped exactly as before). The import+init is try/catch'd: a
failed analytics chunk (network / stale-404 / ad-blocker on a "posthog" chunk)
degrades to no-analytics instead of a permanently blank page.
- F3: sanitize-url.test.ts mirroring editor-ext's security contract (javascript:/
data:/vbscript:/obfuscated → ""; https/relative/mailto preserved).
- F4: the idle-warm `void import(...)` prefetch in layout.tsx gets `.catch(()=>{})`
so a failed best-effort prefetch can't surface as an unhandledrejection.
No new deps (lockfile unchanged). Gate: client tsc 0, sanitize test 3/3, client
build succeeds (entry chunk still 556K, posthog in separate dynamic chunks).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
32 lines
1.3 KiB
TypeScript
32 lines
1.3 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { sanitizeUrl } from "./sanitize-url";
|
|
|
|
// `sanitizeUrl` is a byte-identical client-local copy of editor-ext's wrapper
|
|
// around @braintree/sanitize-url: it maps the sanitizer's "about:blank" XSS
|
|
// sentinel to "". These assertions mirror editor-ext's own security-contract
|
|
// test so the extracted copy keeps the same guarantees.
|
|
describe("sanitizeUrl", () => {
|
|
it("blocks dangerous schemes (returns empty string)", () => {
|
|
expect(sanitizeUrl("javascript:alert(1)")).toBe("");
|
|
expect(sanitizeUrl("data:text/html,<script>alert(1)</script>")).toBe("");
|
|
expect(sanitizeUrl("vbscript:msgbox(1)")).toBe("");
|
|
// Case / whitespace obfuscation must not slip past the sanitizer.
|
|
expect(sanitizeUrl(" JaVaScRiPt:alert(1)")).toBe("");
|
|
});
|
|
|
|
it("returns empty string for empty / undefined input", () => {
|
|
expect(sanitizeUrl(undefined)).toBe("");
|
|
expect(sanitizeUrl("")).toBe("");
|
|
});
|
|
|
|
it("allows safe https, relative file and mailto URLs", () => {
|
|
expect(sanitizeUrl("https://example.com/page")).toMatch(
|
|
/^https:\/\/example\.com\/page/,
|
|
);
|
|
expect(sanitizeUrl("/api/files/abc-123")).toBe("/api/files/abc-123");
|
|
expect(sanitizeUrl("mailto:user@example.com")).toBe(
|
|
"mailto:user@example.com",
|
|
);
|
|
});
|
|
});
|