Compare commits
2
Commits
ef6de302e9
...
2970d5fff3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2970d5fff3 | ||
|
|
eeef2d79df |
@@ -13,6 +13,12 @@
|
||||
],
|
||||
"port": 3520
|
||||
},
|
||||
{
|
||||
"name": "sendline",
|
||||
"runtimeExecutable": "bun",
|
||||
"runtimeArgs": ["run", "--cwd", "D:/Company/sendline", "dev", "--port=3610"],
|
||||
"port": 3610
|
||||
},
|
||||
{
|
||||
"name": "component-showcase",
|
||||
"runtimeExecutable": "bun",
|
||||
|
||||
@@ -356,7 +356,7 @@
|
||||
},
|
||||
"packages/dev-server": {
|
||||
"name": "@wrnexus/dev-server",
|
||||
"version": "0.8.43",
|
||||
"version": "0.8.44",
|
||||
"dependencies": {
|
||||
"@wrnexus/authz": "workspace:*",
|
||||
"@wrnexus/cache": "workspace:*",
|
||||
@@ -612,7 +612,7 @@
|
||||
},
|
||||
"packages/security": {
|
||||
"name": "@wrnexus/security",
|
||||
"version": "0.8.8",
|
||||
"version": "0.8.9",
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "workspace:*",
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/dev-server",
|
||||
"version": "0.8.43",
|
||||
"version": "0.8.44",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -302,6 +302,17 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
|
||||
// Keep generated artifacts under the single framework state directory.
|
||||
// Startup clears this cache and removes legacy PID-suffixed cache folders.
|
||||
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");
|
||||
mkdirSync(virtualDir, { recursive: true });
|
||||
for (const [index, module] of pluginContributions.virtualModules.entries()) {
|
||||
@@ -360,12 +371,6 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
|
||||
? createDevToolbarCollector()
|
||||
: 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
|
||||
// `.wrnexus/` next to each source file (and inside node_modules UI dirs).
|
||||
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",
|
||||
"version": "0.8.8",
|
||||
"version": "0.8.9",
|
||||
"type": "module",
|
||||
"description": "Security policies, safe serialization, SSRF protection, request limits, and secure cookie helpers for WRNexusJS.",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -21,8 +21,11 @@ export function secureCookieOptions(
|
||||
throw new SecurityError("WRN-SEC-COOKIE-HOST", "Host-only cookies cannot set Domain.");
|
||||
}
|
||||
return {
|
||||
path: options.path ?? "/",
|
||||
...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,
|
||||
httpOnly: options.httpOnly ?? true,
|
||||
secure,
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
isPrivateAddress,
|
||||
isTrustedHtml,
|
||||
secureJsonStringify,
|
||||
secureCookieOptions,
|
||||
setSecureCookie,
|
||||
unwrapTrustedHtml,
|
||||
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", () => {
|
||||
const shared = { value: 1 };
|
||||
expect(() => assertSafeObject({ first: shared, second: shared })).not.toThrow();
|
||||
|
||||
@@ -267,7 +267,14 @@ async function main() {
|
||||
external: [/^@wrnexus\//, /^bun:/, /^node:/],
|
||||
tsconfig: join(repoRoot, "tsconfig.json"),
|
||||
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,
|
||||
shims: false,
|
||||
silent: true,
|
||||
|
||||
Reference in New Issue
Block a user