Compare commits
2
Commits
ef6de302e9
...
2970d5fff3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2970d5fff3 | ||
|
|
eeef2d79df |
@@ -13,6 +13,12 @@
|
|||||||
],
|
],
|
||||||
"port": 3520
|
"port": 3520
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "sendline",
|
||||||
|
"runtimeExecutable": "bun",
|
||||||
|
"runtimeArgs": ["run", "--cwd", "D:/Company/sendline", "dev", "--port=3610"],
|
||||||
|
"port": 3610
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "component-showcase",
|
"name": "component-showcase",
|
||||||
"runtimeExecutable": "bun",
|
"runtimeExecutable": "bun",
|
||||||
|
|||||||
@@ -356,7 +356,7 @@
|
|||||||
},
|
},
|
||||||
"packages/dev-server": {
|
"packages/dev-server": {
|
||||||
"name": "@wrnexus/dev-server",
|
"name": "@wrnexus/dev-server",
|
||||||
"version": "0.8.43",
|
"version": "0.8.44",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/authz": "workspace:*",
|
"@wrnexus/authz": "workspace:*",
|
||||||
"@wrnexus/cache": "workspace:*",
|
"@wrnexus/cache": "workspace:*",
|
||||||
@@ -612,7 +612,7 @@
|
|||||||
},
|
},
|
||||||
"packages/security": {
|
"packages/security": {
|
||||||
"name": "@wrnexus/security",
|
"name": "@wrnexus/security",
|
||||||
"version": "0.8.8",
|
"version": "0.8.9",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/dev-server",
|
"name": "@wrnexus/dev-server",
|
||||||
"version": "0.8.43",
|
"version": "0.8.44",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -302,6 +302,17 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
|
|||||||
// Keep generated artifacts under the single framework state directory.
|
// Keep generated artifacts under the single framework state directory.
|
||||||
// Startup clears this cache and removes legacy PID-suffixed cache folders.
|
// Startup clears this cache and removes legacy PID-suffixed cache folders.
|
||||||
const cacheDir = join(appRoot, ".wrnexus", "cache");
|
const cacheDir = join(appRoot, ".wrnexus", "cache");
|
||||||
|
|
||||||
|
// Clear the cache BEFORE anything writes into it. This used to run several
|
||||||
|
// hundred lines below, which deleted the plugin virtual modules written just
|
||||||
|
// after this point -- so an app with a plugin (auth, say) lost its generated
|
||||||
|
// modules at every boot, while an app with none never noticed.
|
||||||
|
resetDevCache({
|
||||||
|
rootDir: appRoot,
|
||||||
|
cacheDir,
|
||||||
|
enabled: process.env.WRNEXUS_PRESERVE_CACHE !== "1",
|
||||||
|
});
|
||||||
|
|
||||||
const virtualDir = join(cacheDir, "virtual");
|
const virtualDir = join(cacheDir, "virtual");
|
||||||
mkdirSync(virtualDir, { recursive: true });
|
mkdirSync(virtualDir, { recursive: true });
|
||||||
for (const [index, module] of pluginContributions.virtualModules.entries()) {
|
for (const [index, module] of pluginContributions.virtualModules.entries()) {
|
||||||
@@ -360,12 +371,6 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
|
|||||||
? createDevToolbarCollector()
|
? createDevToolbarCollector()
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
resetDevCache({
|
|
||||||
rootDir: appRoot,
|
|
||||||
cacheDir,
|
|
||||||
enabled: process.env.WRNEXUS_PRESERVE_CACHE !== "1",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Compile every `.wrn` into ONE cache dir at the project root, instead of a
|
// Compile every `.wrn` into ONE cache dir at the project root, instead of a
|
||||||
// `.wrnexus/` next to each source file (and inside node_modules UI dirs).
|
// `.wrnexus/` next to each source file (and inside node_modules UI dirs).
|
||||||
setCompileCacheDir(cacheDir);
|
setCompileCacheDir(cacheDir);
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
|
||||||
|
const repoRoot = join(import.meta.dir, "..", "..", "..");
|
||||||
|
const publishScript = join(repoRoot, "scripts", "publish-packages.ts");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `@wrnexus/dev-server` ships two entries — `index` (the dev server bootstrap)
|
||||||
|
* and `serve-entry` (the request path that compiles `.wrn` files). 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/*`.
|
||||||
|
*
|
||||||
|
* Bundling each entry independently gives each its own copy of that state. The
|
||||||
|
* bootstrap then sets a cache dir the compiler never sees, and the compiler
|
||||||
|
* records artifact paths the server never sees — so every client module 404s
|
||||||
|
* and `.wrn` compilation writes nothing. It works from source (one module
|
||||||
|
* instance) and fails only once published, which is why it reached a release.
|
||||||
|
*/
|
||||||
|
test("the publish build shares chunks so module state is not duplicated per entry", () => {
|
||||||
|
const source = readFileSync(publishScript, "utf8");
|
||||||
|
|
||||||
|
expect(source).not.toMatch(/splitting:\s*false/);
|
||||||
|
expect(source).toMatch(/splitting:\s*true/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a built dev-server dist declares its mutable pipeline state exactly once", () => {
|
||||||
|
const dist = join(repoRoot, "packages", "dev-server", "dist");
|
||||||
|
if (!existsSync(dist)) {
|
||||||
|
// The dist is a build artifact, absent on a clean checkout. The
|
||||||
|
// configuration assertion above is the guard that always runs.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bundles = readdirSync(dist).filter((file) => file.endsWith(".js"));
|
||||||
|
expect(bundles.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
for (const declaration of [
|
||||||
|
"compileCacheDir",
|
||||||
|
"browserArtifactPaths",
|
||||||
|
"serveWrnBrowserArtifact",
|
||||||
|
]) {
|
||||||
|
const owners = bundles.filter((file) =>
|
||||||
|
readFileSync(join(dist, file), "utf8").includes(declaration),
|
||||||
|
);
|
||||||
|
expect({ declaration, owners }).toEqual({ declaration, owners: owners.slice(0, 1) });
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/security",
|
"name": "@wrnexus/security",
|
||||||
"version": "0.8.8",
|
"version": "0.8.9",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Security policies, safe serialization, SSRF protection, request limits, and secure cookie helpers for WRNexusJS.",
|
"description": "Security policies, safe serialization, SSRF protection, request limits, and secure cookie helpers for WRNexusJS.",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -21,8 +21,11 @@ export function secureCookieOptions(
|
|||||||
throw new SecurityError("WRN-SEC-COOKIE-HOST", "Host-only cookies cannot set Domain.");
|
throw new SecurityError("WRN-SEC-COOKIE-HOST", "Host-only cookies cannot set Domain.");
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
path: options.path ?? "/",
|
|
||||||
...options,
|
...options,
|
||||||
|
// After the spread, not before: `setSecureCookie` always forwards a `path`
|
||||||
|
// key, so an omitted path arrives as an explicit `undefined` that would
|
||||||
|
// otherwise overwrite this default and ship a cookie with no Path at all.
|
||||||
|
path: options.path ?? "/",
|
||||||
domain: options.hostOnly ? undefined : options.domain,
|
domain: options.hostOnly ? undefined : options.domain,
|
||||||
httpOnly: options.httpOnly ?? true,
|
httpOnly: options.httpOnly ?? true,
|
||||||
secure,
|
secure,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
isPrivateAddress,
|
isPrivateAddress,
|
||||||
isTrustedHtml,
|
isTrustedHtml,
|
||||||
secureJsonStringify,
|
secureJsonStringify,
|
||||||
|
secureCookieOptions,
|
||||||
setSecureCookie,
|
setSecureCookie,
|
||||||
unwrapTrustedHtml,
|
unwrapTrustedHtml,
|
||||||
validateUrl,
|
validateUrl,
|
||||||
@@ -45,6 +46,44 @@ describe("@wrnexus/security", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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", () => {
|
test("accepts repeated references while still rejecting cycles", () => {
|
||||||
const shared = { value: 1 };
|
const shared = { value: 1 };
|
||||||
expect(() => assertSafeObject({ first: shared, second: shared })).not.toThrow();
|
expect(() => assertSafeObject({ first: shared, second: shared })).not.toThrow();
|
||||||
|
|||||||
@@ -267,7 +267,14 @@ async function main() {
|
|||||||
external: [/^@wrnexus\//, /^bun:/, /^node:/],
|
external: [/^@wrnexus\//, /^bun:/, /^node:/],
|
||||||
tsconfig: join(repoRoot, "tsconfig.json"),
|
tsconfig: join(repoRoot, "tsconfig.json"),
|
||||||
clean: true,
|
clean: true,
|
||||||
splitting: false,
|
// Shared modules must be emitted ONCE as a chunk both entries import,
|
||||||
|
// not inlined into each. Several packages hold mutable module state --
|
||||||
|
// dev-server's compileCacheDir and browserArtifactPaths are set by one
|
||||||
|
// entry and read by another -- and duplicating the module duplicates the
|
||||||
|
// state, so the writer and the reader silently address different copies.
|
||||||
|
// This only manifests in a published build; from source there is one
|
||||||
|
// module instance and everything works.
|
||||||
|
splitting: true,
|
||||||
sourcemap: false,
|
sourcemap: false,
|
||||||
shims: false,
|
shims: false,
|
||||||
silent: true,
|
silent: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user