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>
105 lines
3.7 KiB
TypeScript
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();
|
|
});
|
|
});
|