Files
ClintchizandClaude Opus 5 eeef2d79df fix(dev-server,security): repair two defects that only appear in a published build
The dev server shipped two entries, index and serve-entry, bundled
independently because the publish build set splitting:false. They share
pipeline.ts, which holds mutable module state -- compileCacheDir, set once
at startup by the bootstrap, and browserArtifactPaths, populated during
compilation and read when serving /__wrnexus/client/*. Duplicating the
module duplicated the state, so the writer and the reader addressed
different copies: every component client module 404'd and .wrn compilation
wrote nothing. It works from source, where there is one module instance,
which is why it reached a release. Emitting a shared chunk fixes it for
every package at once.

resetDevCache also ran several hundred lines after the plugin virtual
modules were written into the same directory, deleting them at every boot.
An app with no plugins never noticed; an app with one lost them every time.

Separately, secureCookieOptions spread ...options after its path default,
and setSecureCookie always forwards an explicit path key -- so omitting
path emitted a cookie with no Path at all, which the browser then scoped to
the request's directory.

Verified end to end against a real app installing the published packages:
17 artifacts written, client modules 200, and the sign-in form submits from
the UI and reaches /dashboard.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 09:04:12 +05:30

105 lines
3.7 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import {
assertSafeObject,
createTrustedHtml,
isPrivateAddress,
isTrustedHtml,
secureJsonStringify,
secureCookieOptions,
setSecureCookie,
unwrapTrustedHtml,
validateUrl,
} from "../src/index.ts";
describe("@wrnexus/security", () => {
test("escapes HTML-significant JSON and redacts secrets", () => {
const json = secureJsonStringify({ html: "</script><img>", token: "secret", count: 1 });
expect(json).toContain("\\u003c/script\\u003e");
expect(json).toContain("[REDACTED]");
expect(json).not.toContain("secret");
});
test("rejects prototype-pollution keys and unsafe URL protocols", () => {
const unsafe = JSON.parse('{"__proto__":{"admin":true}}');
expect(() => assertSafeObject(unsafe)).toThrow();
expect(() => validateUrl("javascript:alert(1)")).toThrow();
});
test("identifies private IP ranges", () => {
expect(isPrivateAddress("127.0.0.1")).toBe(true);
expect(isPrivateAddress("10.1.2.3")).toBe(true);
expect(isPrivateAddress("8.8.8.8")).toBe(false);
});
test("enforces __Host cookie rules", () => {
const writes: unknown[] = [];
const ctx = {
url: new URL("https://example.com"),
cookies: { set: (...args: unknown[]) => writes.push(args) },
} as any;
setSecureCookie(ctx, "__Host-session", "value");
expect(writes).toHaveLength(1);
expect(writes[0]).toEqual([
"__Host-session",
"value",
expect.objectContaining({ secure: true, httpOnly: true, path: "/", domain: undefined }),
]);
});
test("defaults a cookie's Path to / when the caller omits one", () => {
// setSecureCookie always forwards a `path` key, so an omitted path arrives
// as `path: undefined`. If that lands after the default in the returned
// object it wins, and the cookie ships with NO Path -- which the browser
// then scopes to the request's directory, so a cookie set from
// /api/oauth/google is never sent to /api/oauth/google/callback.
const writes: unknown[][] = [];
const ctx = {
url: new URL("https://example.com"),
cookies: { set: (...args: unknown[]) => writes.push(args) },
} as any;
setSecureCookie(ctx, "oauth_state", "abc", { sameSite: "Lax", maxAge: 600 });
expect(writes).toHaveLength(1);
expect((writes[0]![2] as { path?: string }).path).toBe("/");
});
test("keeps an explicitly requested cookie path", () => {
const writes: unknown[][] = [];
const ctx = {
url: new URL("https://example.com"),
cookies: { set: (...args: unknown[]) => writes.push(args) },
} as any;
setSecureCookie(ctx, "scoped", "abc", { path: "/admin" });
expect((writes[0]![2] as { path?: string }).path).toBe("/admin");
});
test("secureCookieOptions defaults Path even when handed an explicit undefined", () => {
const options = secureCookieOptions({ url: new URL("https://example.com") } as any, {
path: undefined,
});
expect(options.path).toBe("/");
});
test("accepts repeated references while still rejecting cycles", () => {
const shared = { value: 1 };
expect(() => assertSafeObject({ first: shared, second: shared })).not.toThrow();
const cyclic: Record<string, unknown> = {};
cyclic.self = cyclic;
expect(() => assertSafeObject(cyclic)).toThrow();
});
test("trusted HTML requires an explicit sanitizer policy", () => {
const value = createTrustedHtml('<p onclick="bad()">Hello</p>', {
name: "test-policy",
sanitize: (input) => input.replace(/\s+onclick="[^"]*"/g, ""),
});
expect(isTrustedHtml(value)).toBe(true);
expect(unwrapTrustedHtml(value)).toBe("<p>Hello</p>");
expect(() => unwrapTrustedHtml({ value: "<b>unsafe</b>" } as any)).toThrow();
});
});