feat: release design system engine, studio and component contracts
This commit is contained in:
1 parent
ff1065fb27
commit
3943b946d6
693 files changed
+175546
-3894
No files matched your search
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/core",
|
||||
"version": "0.8.17",
|
||||
"version": "0.8.25",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -4,6 +4,18 @@ import { Buffer } from "node:buffer";
|
||||
type Registry = { id: string; definitions: unknown[] };
|
||||
const registries = new WeakMap<object, Registry>();
|
||||
|
||||
/** Read a control definition owned by the active request for SSR preparation. */
|
||||
export function resolveControlTemplate(reference: string): unknown {
|
||||
const ctx = getRequestContext();
|
||||
const registry = ctx && registries.get(ctx);
|
||||
if (!registry) return undefined;
|
||||
const prefix = `@${registry.id}:`;
|
||||
if (!reference.startsWith(prefix)) return undefined;
|
||||
const index = reference.slice(prefix.length);
|
||||
if (!/^(0|[1-9][0-9]*)$/.test(index)) return undefined;
|
||||
return registry.definitions[Number(index)];
|
||||
}
|
||||
|
||||
/** Hoist nested templates out of each other for linear-size hydration data. */
|
||||
export function encodeControlTemplate(value: unknown): string {
|
||||
const ctx = getRequestContext();
|
||||
@@ -17,10 +29,59 @@ export function encodeControlTemplate(value: unknown): string {
|
||||
return `@${registry.id}:${index}`;
|
||||
}
|
||||
|
||||
export function renderControlTemplates(): string {
|
||||
/** Nested controls own their rendered content; keep it once in their definition. */
|
||||
function compactControlMarkup(markup: string): string {
|
||||
const tokenPattern =
|
||||
/<template\s+(?:data-wrn-(?:if|each)="([^"]+)"|data-wrn-control-end)(?:\s[^>]*)?>\s*<\/template>/g;
|
||||
const stack: Array<{ markerStart: number; marker: string; owned: boolean }> = [];
|
||||
const replacements: Array<{ start: number; end: number; value: string }> = [];
|
||||
for (const match of markup.matchAll(tokenPattern)) {
|
||||
if (match[1]) {
|
||||
stack.push({
|
||||
markerStart: match.index!,
|
||||
marker: match[0],
|
||||
owned: !!resolveControlTemplate(match[1]),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const open = stack.pop();
|
||||
if (!open || !open.owned || stack.some((entry) => entry.owned)) continue;
|
||||
replacements.push({
|
||||
start: open.markerStart,
|
||||
end: match.index!,
|
||||
value: open.marker.replace(/>\s*<\/template>$/, " data-wrn-control-client></template>"),
|
||||
});
|
||||
}
|
||||
// Do not alter incomplete control ranges.
|
||||
if (stack.length) return markup;
|
||||
for (const replacement of replacements.reverse()) {
|
||||
markup = markup.slice(0, replacement.start) + replacement.value + markup.slice(replacement.end);
|
||||
}
|
||||
return markup;
|
||||
}
|
||||
|
||||
export function renderControlTemplates(transformMarkup?: (markup: string) => string): string {
|
||||
const ctx = getRequestContext();
|
||||
const registry = ctx && registries.get(ctx);
|
||||
if (!registry) return "";
|
||||
const json = JSON.stringify(registry.definitions).replace(/</g, "\\u003c");
|
||||
const definitions: unknown[] = [];
|
||||
// A markup transform may register shared metadata while visiting a template.
|
||||
// Include those new request-owned entries in this same document.
|
||||
for (let index = 0; index < registry.definitions.length; index++) {
|
||||
const definition = registry.definitions[index];
|
||||
const transform = (entry: unknown) => {
|
||||
if (!entry || typeof entry !== "object") return entry;
|
||||
const copy = { ...entry } as Record<string, unknown>;
|
||||
for (const key of ["body", "empty"]) {
|
||||
if (typeof copy[key] === "string")
|
||||
copy[key] = compactControlMarkup(
|
||||
transformMarkup ? transformMarkup(copy[key]) : copy[key],
|
||||
);
|
||||
}
|
||||
return copy;
|
||||
};
|
||||
definitions.push(Array.isArray(definition) ? definition.map(transform) : transform(definition));
|
||||
}
|
||||
const json = JSON.stringify(definitions).replace(/</g, "\\u003c");
|
||||
return `<script type="application/json" data-wrn-controls id="${registry.id}">${json}</script>`;
|
||||
}
|
||||
@@ -93,6 +93,10 @@ export interface SecurityConfig {
|
||||
crossOriginOpenerPolicy?: false | "same-origin" | "same-origin-allow-popups" | "unsafe-none";
|
||||
/** Defaults to "SAMEORIGIN". */
|
||||
frameOptions?: false | "DENY" | "SAMEORIGIN";
|
||||
/** Allow trusted routes to supply X-WRNexus-Frame-Ancestors (exact HTTPS origins only). */
|
||||
routeFrameAncestors?: boolean;
|
||||
/** Allow a trusted route's explicit CSP to replace the framework default. Opt-in only. */
|
||||
routeContentSecurityPolicy?: boolean;
|
||||
/** Defaults to "strict-origin-when-cross-origin". */
|
||||
referrerPolicy?: false | string;
|
||||
/** Defaults to "same-origin". */
|
||||
@@ -219,6 +223,9 @@ export function withSecurityHeaders(
|
||||
nonce?: string,
|
||||
): Response {
|
||||
const headers = new Headers(res.headers);
|
||||
const routeCsp = headers.get("Content-Security-Policy");
|
||||
const routeAncestors = headers.get("X-WRNexus-Frame-Ancestors");
|
||||
headers.delete("X-WRNexus-Frame-Ancestors");
|
||||
const cors = normalizeCors(security?.cors);
|
||||
|
||||
if (cors.enabled) {
|
||||
@@ -227,6 +234,46 @@ export function withSecurityHeaders(
|
||||
|
||||
if (security?.headers !== false) {
|
||||
applyBaseSecurityHeaders(headers, mode, security, nonce);
|
||||
if (security?.routeContentSecurityPolicy && routeCsp?.trim()) {
|
||||
headers.set("Content-Security-Policy", routeCsp);
|
||||
// CSP frame-ancestors is authoritative for explicitly embeddable routes.
|
||||
if (/(?:^|;)\s*frame-ancestors\s+/i.test(routeCsp)) headers.delete("X-Frame-Options");
|
||||
}
|
||||
if (
|
||||
security?.routeFrameAncestors &&
|
||||
routeAncestors !== null &&
|
||||
headers.has("Content-Security-Policy")
|
||||
) {
|
||||
const sources = routeAncestors.trim().split(/\s+/);
|
||||
const valid =
|
||||
sources.length > 0 &&
|
||||
sources.length <= 21 &&
|
||||
sources.every((source) => {
|
||||
if (source === "'self'" || source === "'none'") return true;
|
||||
try {
|
||||
const url = new URL(source);
|
||||
return (
|
||||
url.protocol === "https:" &&
|
||||
!url.username &&
|
||||
!url.password &&
|
||||
url.origin === source &&
|
||||
!source.includes("*")
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}) &&
|
||||
(!sources.includes("'none'") || sources.length === 1);
|
||||
const ancestors = valid ? sources.join(" ") : "'none'";
|
||||
const policy = headers
|
||||
.get("Content-Security-Policy")!
|
||||
.split(";")
|
||||
.filter((value) => !/^\s*frame-ancestors(?:\s|$)/i.test(value));
|
||||
headers.set("Content-Security-Policy", policy.join(";") + "; frame-ancestors " + ancestors);
|
||||
if (ancestors === "'none'") headers.set("X-Frame-Options", "DENY");
|
||||
else if (ancestors === "'self'") headers.set("X-Frame-Options", "SAMEORIGIN");
|
||||
else headers.delete("X-Frame-Options");
|
||||
}
|
||||
}
|
||||
|
||||
if (security?.extraHeaders) {
|
||||
@@ -261,7 +308,11 @@ function applyBaseSecurityHeaders(
|
||||
// A handler can deliberately expose a public image for email embedding.
|
||||
// An explicitly configured application policy still takes precedence.
|
||||
const responseCorp = headers.get("Cross-Origin-Resource-Policy");
|
||||
if (corp !== false && (security?.crossOriginResourcePolicy !== undefined || !["same-origin", "same-site", "cross-origin"].includes(responseCorp ?? ""))) {
|
||||
if (
|
||||
corp !== false &&
|
||||
(security?.crossOriginResourcePolicy !== undefined ||
|
||||
!["same-origin", "same-site", "cross-origin"].includes(responseCorp ?? ""))
|
||||
) {
|
||||
headers.set("Cross-Origin-Resource-Policy", corp);
|
||||
}
|
||||
if (security?.originAgentCluster !== false) headers.set("Origin-Agent-Cluster", "?1");
|
||||
|
||||
@@ -237,4 +237,8 @@ export type {
|
||||
IdempotencyRecord,
|
||||
IdempotencyStore,
|
||||
} from "./platform.ts";
|
||||
export { encodeControlTemplate, renderControlTemplates } from "./control-templates.ts";
|
||||
export {
|
||||
encodeControlTemplate,
|
||||
resolveControlTemplate,
|
||||
renderControlTemplates,
|
||||
} from "./control-templates.ts";
|
||||
@@ -1,23 +1,60 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { encodeControlTemplate, renderControlTemplates } from "../src/control-templates.ts";
|
||||
import {
|
||||
encodeControlTemplate,
|
||||
resolveControlTemplate,
|
||||
renderControlTemplates,
|
||||
} from "../src/control-templates.ts";
|
||||
import { runWithRequestContext } from "../src/request-context.ts";
|
||||
import { createContext } from "../src/context.ts";
|
||||
import { resolveRequestUrl } from "../src/headers.ts";
|
||||
|
||||
test("registry templates omit duplicated SSR ranges but preserve source and foreign controls", () => {
|
||||
runWithRequestContext({} as never, () => {
|
||||
const leaf = `<p>${"Large leaf".repeat(400)}</p>`;
|
||||
let body = leaf;
|
||||
for (let depth = 0; depth < 12; depth++) {
|
||||
const reference = encodeControlTemplate([{ cond: "open", body }]);
|
||||
body = `<template data-wrn-if="${reference}"></template>${body}<template data-wrn-control-end></template>`;
|
||||
}
|
||||
const html = renderControlTemplates();
|
||||
expect(html.length).toBeLessThan(10000);
|
||||
const compacted = JSON.parse(/data-wrn-controls[^>]*>(.*?)<\/script>/s.exec(html)![1]!);
|
||||
expect(
|
||||
compacted.filter((branches: Array<{ body: string }>) => branches[0]!.body.includes(leaf)),
|
||||
).toHaveLength(1);
|
||||
expect(html).toContain("data-wrn-control-client");
|
||||
expect(body).toContain(leaf);
|
||||
const foreign =
|
||||
'<template data-wrn-if="@foreign:0"></template><b>Keep</b><template data-wrn-control-end></template>';
|
||||
const incomplete = body.replace(/<template data-wrn-control-end><\/template>$/, "");
|
||||
encodeControlTemplate([
|
||||
{ cond: null, body: foreign },
|
||||
{ cond: null, body: incomplete },
|
||||
]);
|
||||
const definitions = JSON.parse(
|
||||
/data-wrn-controls[^>]*>(.*?)<\/script>/s.exec(renderControlTemplates())![1]!,
|
||||
);
|
||||
expect(definitions[12][0].body).toBe(foreign);
|
||||
expect(definitions[12][1].body).toBe(incomplete);
|
||||
});
|
||||
});
|
||||
|
||||
test("nested controls use short references and HTML-safe isolated registries", async () => {
|
||||
const results = await Promise.all(["first", "second"].map((name) =>
|
||||
runWithRequestContext({} as never, async () => {
|
||||
let body = `</script><p>${name}</p>`;
|
||||
for (let i = 0; i < 30; i++) {
|
||||
body = `<template data-wrn-if="${encodeControlTemplate([{cond: null, body}])}"></template>`;
|
||||
}
|
||||
await Promise.resolve();
|
||||
const html = renderControlTemplates();
|
||||
expect(html.length).toBeLessThan(7000);
|
||||
expect(html.match(/<\/script>/g)).toHaveLength(1);
|
||||
return html;
|
||||
}),
|
||||
));
|
||||
const results = await Promise.all(
|
||||
["first", "second"].map((name) =>
|
||||
runWithRequestContext({} as never, async () => {
|
||||
let body = `</script><p>${name}</p>`;
|
||||
for (let i = 0; i < 30; i++) {
|
||||
body = `<template data-wrn-if="${encodeControlTemplate([{ cond: null, body }])}"></template>`;
|
||||
}
|
||||
await Promise.resolve();
|
||||
const html = renderControlTemplates();
|
||||
expect(html.length).toBeLessThan(7000);
|
||||
expect(html.match(/<\/script>/g)).toHaveLength(1);
|
||||
return html;
|
||||
}),
|
||||
),
|
||||
);
|
||||
expect(results[0]).toContain("first");
|
||||
expect(results[0]).not.toContain("second");
|
||||
expect(results[1]).not.toContain("first");
|
||||
@@ -28,11 +65,34 @@ test("standalone rendering keeps the base64 format", () => {
|
||||
expect(JSON.parse(Buffer.from(encodeControlTemplate([1]), "base64").toString())).toEqual([1]);
|
||||
});
|
||||
|
||||
test("SSR control resolution accepts only the owning request and exact registry index", async () => {
|
||||
let reference = "";
|
||||
await runWithRequestContext({} as never, async () => {
|
||||
const definition = [{ cond: "review", body: "<p>Review</p>" }];
|
||||
reference = encodeControlTemplate(definition);
|
||||
expect(resolveControlTemplate(reference)).toBe(definition);
|
||||
expect(resolveControlTemplate(reference + "0")).toBeUndefined();
|
||||
expect(resolveControlTemplate(reference.replace(/:0$/, ":-1"))).toBeUndefined();
|
||||
expect(resolveControlTemplate(reference.replace(/:0$/, ":1"))).toBeUndefined();
|
||||
await runWithRequestContext({} as never, async () => {
|
||||
encodeControlTemplate([{ body: "Other request" }]);
|
||||
expect(resolveControlTemplate(reference)).toBeUndefined();
|
||||
});
|
||||
expect(resolveControlTemplate(reference)).toBe(definition);
|
||||
});
|
||||
expect(resolveControlTemplate(reference)).toBeUndefined();
|
||||
});
|
||||
|
||||
test("trusted proxy request URL and body agree with context; untrusted headers are ignored", async () => {
|
||||
for (const trust of [true, false]) {
|
||||
const request = new Request("http://internal:3000/api/captcha/challenge", {
|
||||
method: "POST", body: '{"test":1}',
|
||||
headers: { "x-forwarded-proto": "https", "x-forwarded-host": "wrnexus.com", origin: "https://evil.example" },
|
||||
method: "POST",
|
||||
body: '{"test":1}',
|
||||
headers: {
|
||||
"x-forwarded-proto": "https",
|
||||
"x-forwarded-host": "wrnexus.com",
|
||||
origin: "https://evil.example",
|
||||
},
|
||||
});
|
||||
const context = createContext(request, resolveRequestUrl(request, trust));
|
||||
expect(context.req.url).toBe(context.url.href);
|
||||
|
||||
@@ -1,10 +1,29 @@
|
||||
import {expect,test} from "bun:test";
|
||||
import {withSecurityHeaders} from "../src/headers.ts";
|
||||
test("explicit response resource policies survive defaults while configured application policy wins",()=>{
|
||||
const req=new Request("https://example.com/image.png");
|
||||
const image=()=>new Response("image",{headers:{"Cross-Origin-Resource-Policy":"cross-origin","Content-Type":"image/png"}});
|
||||
expect(withSecurityHeaders(req,image(),"production").headers.get("Cross-Origin-Resource-Policy")).toBe("cross-origin");
|
||||
expect(withSecurityHeaders(req,new Response("private"),"production").headers.get("Cross-Origin-Resource-Policy")).toBe("same-origin");
|
||||
expect(withSecurityHeaders(req,image(),"production",{crossOriginResourcePolicy:"same-origin"}).headers.get("Cross-Origin-Resource-Policy")).toBe("same-origin");
|
||||
expect(withSecurityHeaders(req,new Response("x",{headers:{"Cross-Origin-Resource-Policy":"invalid"}}),"production").headers.get("Cross-Origin-Resource-Policy")).toBe("same-origin");
|
||||
import { expect, test } from "bun:test";
|
||||
import { withSecurityHeaders } from "../src/headers.ts";
|
||||
test("explicit response resource policies survive defaults while configured application policy wins", () => {
|
||||
const req = new Request("https://example.com/image.png");
|
||||
const image = () =>
|
||||
new Response("image", {
|
||||
headers: { "Cross-Origin-Resource-Policy": "cross-origin", "Content-Type": "image/png" },
|
||||
});
|
||||
expect(
|
||||
withSecurityHeaders(req, image(), "production").headers.get("Cross-Origin-Resource-Policy"),
|
||||
).toBe("cross-origin");
|
||||
expect(
|
||||
withSecurityHeaders(req, new Response("private"), "production").headers.get(
|
||||
"Cross-Origin-Resource-Policy",
|
||||
),
|
||||
).toBe("same-origin");
|
||||
expect(
|
||||
withSecurityHeaders(req, image(), "production", {
|
||||
crossOriginResourcePolicy: "same-origin",
|
||||
}).headers.get("Cross-Origin-Resource-Policy"),
|
||||
).toBe("same-origin");
|
||||
expect(
|
||||
withSecurityHeaders(
|
||||
req,
|
||||
new Response("x", { headers: { "Cross-Origin-Resource-Policy": "invalid" } }),
|
||||
"production",
|
||||
).headers.get("Cross-Origin-Resource-Policy"),
|
||||
).toBe("same-origin");
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { withSecurityHeaders } from "../src/headers.ts";
|
||||
const request = new Request("https://app.example.test/");
|
||||
const policy =
|
||||
"default-src 'none'; style-src 'unsafe-inline'; frame-ancestors https://customer.example.test; sandbox";
|
||||
test("route CSP does not override the default without explicit application opt-in", () => {
|
||||
const response = withSecurityHeaders(
|
||||
request,
|
||||
new Response("static", { headers: { "content-security-policy": policy } }),
|
||||
"production",
|
||||
);
|
||||
expect(response.headers.get("content-security-policy")).toContain("default-src 'self'");
|
||||
expect(response.headers.get("x-frame-options")).toBe("SAMEORIGIN");
|
||||
});
|
||||
test("trusted static routes retain strict CSP and explicit cross-origin framing when enabled", () => {
|
||||
const response = withSecurityHeaders(
|
||||
request,
|
||||
new Response("static", { headers: { "content-security-policy": policy } }),
|
||||
"production",
|
||||
{ routeContentSecurityPolicy: true },
|
||||
);
|
||||
expect(response.headers.get("content-security-policy")).toBe(policy);
|
||||
expect(response.headers.has("x-frame-options")).toBe(false);
|
||||
expect(response.headers.get("x-content-type-options")).toBe("nosniff");
|
||||
expect(response.headers.get("strict-transport-security")).toContain("max-age=");
|
||||
});
|
||||
test("ordinary routes keep defaults and a route without frame-ancestors keeps frame restrictions", () => {
|
||||
const ordinary = withSecurityHeaders(request, new Response("app"), "production", {
|
||||
routeContentSecurityPolicy: true,
|
||||
});
|
||||
expect(ordinary.headers.get("content-security-policy")).toContain("default-src 'self'");
|
||||
expect(ordinary.headers.get("x-frame-options")).toBe("SAMEORIGIN");
|
||||
const staticResponse = withSecurityHeaders(
|
||||
request,
|
||||
new Response("static", {
|
||||
headers: { "content-security-policy": "default-src 'none'; sandbox" },
|
||||
}),
|
||||
"production",
|
||||
{ routeContentSecurityPolicy: true },
|
||||
);
|
||||
expect(staticResponse.headers.get("content-security-policy")).toBe("default-src 'none'; sandbox");
|
||||
expect(staticResponse.headers.get("x-frame-options")).toBe("SAMEORIGIN");
|
||||
});
|
||||
Reference in new issue
Block a user