Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
c8e7c9e
fix(modules): harden the import-map preloader against poisoning and u…
kojiwakayama Aug 3, 2026
a1cd585
chore: drop unused stringifyJsonValue imports to unblock pre-push lint
kojiwakayama Aug 3, 2026
b6db54e
chore(lint): remove preloader.test.ts from the test-typecheck baseline
kojiwakayama Aug 3, 2026
1a6e9c9
fix(transforms): canonicalize cache identities safely
kojiwakayama Aug 3, 2026
0126305
Stabilize import-map preloading under reviewed cache edges
kojiwakayama Aug 3, 2026
897a299
Keep generated bundles aligned with cache hardening
kojiwakayama Aug 3, 2026
0247bd4
fix(modules): complete import-map isolation hardening
kojiwakayama Aug 3, 2026
d4bf779
Merge remote-tracking branch 'origin/main' into codex/pr3308-review-fix
kojiwakayama Aug 3, 2026
399b7c8
Merge remote-tracking branch 'origin/fix/import-map-preloader-hardeni…
kojiwakayama Aug 3, 2026
e662c5c
Keep import-map hardening compatible with the release baseline
kojiwakayama Aug 3, 2026
722cd22
Merge remote-tracking branch 'origin/fix/import-map-preloader-hardeni…
kojiwakayama Aug 3, 2026
4dec2eb
Prevent poisoned primordials from collapsing import-map identities
kojiwakayama Aug 3, 2026
5211fc7
fix(modules): close import-map review gaps
kojiwakayama Aug 3, 2026
0056d6a
chore(modules): remove redundant context guard
kojiwakayama Aug 3, 2026
3777ddf
fix(modules): capture descriptor primitive before cache getters
kojiwakayama Aug 3, 2026
f29e56d
Unify concurrent import-map hardening before review handoff
kojiwakayama Aug 3, 2026
3731374
Keep the review gate meaningful under parallel scheduler load
kojiwakayama Aug 3, 2026
dcec078
Preserve descriptor capture before cache getter snapshots
kojiwakayama Aug 3, 2026
8182395
Keep import-map hardening current with verified mainline fixes
kojiwakayama Aug 3, 2026
97a7799
Keep the hardening branch current with durable agent events
kojiwakayama Aug 3, 2026
9a21a73
fix(modules): close import-map review gaps
kojiwakayama Aug 3, 2026
7276df1
refactor(platform): centralize trusted array primordials
kojiwakayama Aug 3, 2026
d752f16
Align import-map integration coverage with scoped framework hardening
kojiwakayama Aug 3, 2026
a1e93a5
Merge remote-tracking branch 'origin/main' into codex/pr3308-review-f…
kojiwakayama Aug 3, 2026
4f7e508
Clarify import-map follow-up review paths
kojiwakayama Aug 3, 2026
a258e7f
fix(transforms): bind pipeline cache identities
kojiwakayama Aug 3, 2026
ad56bd4
Preserve PR cache identity compatibility
kojiwakayama Aug 3, 2026
4a8fefa
Harden cache identity execution primordials
kojiwakayama Aug 3, 2026
cf03534
fix(modules): close remaining cache poisoning gaps
kojiwakayama Aug 3, 2026
00db338
Require concrete transform plugin stages
kojiwakayama Aug 3, 2026
cb3e27c
fix(transforms): preserve fractional plugin stages
kojiwakayama Aug 3, 2026
95fd72b
test(rendering): lock import-map preload context wiring end to end
kwakayama Aug 3, 2026
65b0ddb
Bound timed-out import-map loads by project
kojiwakayama Aug 3, 2026
c645be7
Keep timed-out import-map loads inside project admission limits
kojiwakayama Aug 3, 2026
d84d573
Keep import-map admission within its documented ceilings
kojiwakayama Aug 3, 2026
69d3138
Preserve import-map metadata compatibility during preload
kojiwakayama Aug 3, 2026
080e050
test(modules): lock import-map capacity transitions
kojiwakayama Aug 3, 2026
ad6f7fe
Preserve HTTP cache identity after URL prototype mutation
kojiwakayama Aug 3, 2026
a85156a
Close remaining HTTP identity poisoning paths
kojiwakayama Aug 3, 2026
60abe11
Reject poisoned cached bundle paths
kojiwakayama Aug 3, 2026
b430d7f
Merge origin/main into PR 3308 after dev UI extraction
kojiwakayama Aug 3, 2026
04b5bad
Keep transform cache boundaries exact
kojiwakayama Aug 3, 2026
11eca22
Unify SSR import-map cache identity
kojiwakayama Aug 3, 2026
97e3373
Keep HTTP specifier routing stable after prefix poisoning
kojiwakayama Aug 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion scripts/lint/test-typecheck-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
"src/mcp/server.test.ts",
"src/middleware/builtin/security/security-headers.test.ts",
"src/middleware/core/pipeline/composer.test.ts",
"src/modules/import-map/preloader.test.ts",
"src/modules/react-loader/ssr-module-loader.stress.test.ts",
"src/platform/adapters/fs/veryfront/directory-operations.test.ts",
"src/platform/adapters/redis/node.test.ts",
Expand Down
2 changes: 1 addition & 1 deletion src/build/production-build/templates.ts

Large diffs are not rendered by default.

163 changes: 163 additions & 0 deletions src/cache/config-hash.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,99 @@
import "#veryfront/schemas/_test-setup.ts";
import { assertEquals, assertNotEquals } from "#veryfront/testing/assert.ts";
import { describe, it } from "#veryfront/testing/bdd.ts";
import { buildDependencyPinningCacheVariant } from "#veryfront/cache/keys/dependency-pinning.ts";
import {
CSSTYPE_VERSION,
DEFAULT_REACT_VERSION,
TAILWIND_VERSION,
} from "#veryfront/transforms/import-rewriter/url-builder.ts";
import { computeHash } from "#veryfront/utils/hash-utils.ts";
import { VERSION } from "#veryfront/utils/version.ts";
import { computeConfigHash, computeConfigHashSync } from "./config-hash.ts";

const CANONICAL_PIN_KEY = "on:z7bg3qnfgtcb";
const CHANGED_CANONICAL_PIN_KEY = "on:z7bg3qnfgtcc";

describe("cache/config-hash", () => {
describe("computeConfigHash", () => {
it("preserves the established serialized identity for the default config", async () => {
const identity = JSON.stringify({
transformVersion: VERSION,
reactVersion: DEFAULT_REACT_VERSION,
jsxImportSource: "react",
moduleServerUrl: null,
vendorBundleHash: null,
apiBaseUrl: null,
studioEmbed: false,
dev: false,
csstype: CSSTYPE_VERSION,
tailwind: TAILWIND_VERSION,
});
assertEquals(
await computeConfigHash({}),
await computeHash(identity),
);
});

it("preserves the established serialized identity for a fully scoped config", async () => {
const dependencyPinningCacheVariant = buildDependencyPinningCacheVariant(
CANONICAL_PIN_KEY,
"https://preview.example.test",
);
const identity = JSON.stringify({
transformVersion: VERSION,
reactVersion: "18.3.1",
jsxImportSource: "preact",
moduleServerUrl: "https://modules.example.test/_vf_modules",
vendorBundleHash: "vendor-a",
apiBaseUrl: "https://api.example.test",
studioEmbed: true,
dev: true,
...(dependencyPinningCacheVariant ? { dependencyPinningCacheVariant } : {}),
csstype: CSSTYPE_VERSION,
tailwind: TAILWIND_VERSION,
});
assertEquals(
await computeConfigHash({
reactVersion: "18.3.1",
jsxImportSource: "preact",
moduleServerUrl: "https://modules.example.test/_vf_modules",
moduleServerOrigin: "https://preview.example.test",
vendorBundleHash: "vendor-a",
apiBaseUrl: "https://api.example.test",
studioEmbed: true,
dev: true,
dependencyPinningCacheKey: CANONICAL_PIN_KEY,
}),
await computeHash(identity),
);
});

it("keeps distinct hashes stable when array push and join are poisoned", async () => {
const firstConfig = { reactVersion: "18.3.1", dev: false };
const secondConfig = { reactVersion: "19.2.4", dev: true };
const firstBaseline = await computeConfigHash(firstConfig);
const secondBaseline = await computeConfigHash(secondConfig);
const originalPush = Array.prototype.push;
const originalJoin = Array.prototype.join;
let firstPoisoned: string | undefined;
let secondPoisoned: string | undefined;

try {
Reflect.set(Array.prototype, "push", () => 0);
Reflect.set(Array.prototype, "join", () => "poisoned");
firstPoisoned = await computeConfigHash(firstConfig);
secondPoisoned = await computeConfigHash(secondConfig);
} finally {
Reflect.set(Array.prototype, "push", originalPush);
Reflect.set(Array.prototype, "join", originalJoin);
}

assertEquals(firstPoisoned, firstBaseline);
assertEquals(secondPoisoned, secondBaseline);
assertNotEquals(firstPoisoned, secondPoisoned);
});

it("should return a 64-char hex hash", async () => {
const hash = await computeConfigHash({});
assertEquals(hash.length, 64);
Expand Down Expand Up @@ -115,6 +201,83 @@ describe("cache/config-hash", () => {
});

describe("computeConfigHashSync", () => {
it("matches the golden identity for the default transform config", () => {
assertEquals(
computeConfigHashSync({}),
`v${VERSION}:${DEFAULT_REACT_VERSION}:react`,
);
});

it("matches the golden identity for a fully scoped transform config", () => {
assertEquals(
computeConfigHashSync({
reactVersion: "18.3.1",
jsxImportSource: "preact",
moduleServerUrl: "https://modules.example.test/_vf_modules",
moduleServerOrigin: "https://preview.example.test",
vendorBundleHash: "vendor-a",
apiBaseUrl: "https://api.example.test",
studioEmbed: true,
dev: true,
dependencyPinningCacheKey: CANONICAL_PIN_KEY,
}),
`v${VERSION}:18.3.1:preact:modules:40:https://modules.example.test/_vf_modules:vendor:8:vendor-a:api:24:https://api.example.test:studio:dev:pins:on:z7bg3qnfgtcb:origin:aHR0cHM6Ly9wcmV2aWV3LmV4YW1wbGUudGVzdA`,
);
});

it("preserves the established identity after array primordial poisoning", () => {
const originalFilter = Array.prototype.filter;
const originalJoin = Array.prototype.join;
const originalPush = Array.prototype.push;
let identity: string | undefined;
try {
Reflect.set(Array.prototype, "filter", () => []);
Reflect.set(Array.prototype, "join", () => "poisoned");
Reflect.set(Array.prototype, "push", () => 0);
identity = computeConfigHashSync({
moduleServerUrl: "https://modules.example.test/_vf_modules",
vendorBundleHash: "vendor-a",
apiBaseUrl: "https://api.example.test",
studioEmbed: true,
dev: true,
});
} finally {
Reflect.set(Array.prototype, "filter", originalFilter);
Reflect.set(Array.prototype, "join", originalJoin);
Reflect.set(Array.prototype, "push", originalPush);
}

assertEquals(
identity,
`v${VERSION}:${DEFAULT_REACT_VERSION}:react:modules:40:https://modules.example.test/_vf_modules:vendor:8:vendor-a:api:24:https://api.example.test:studio:dev`,
);
});

it("keeps distinct sync identities stable when array push and join are poisoned", () => {
const firstConfig = { reactVersion: "18.3.1", dev: false };
const secondConfig = { reactVersion: "19.2.4", dev: true };
const firstBaseline = computeConfigHashSync(firstConfig);
const secondBaseline = computeConfigHashSync(secondConfig);
const originalPush = Array.prototype.push;
const originalJoin = Array.prototype.join;
let firstPoisoned: string | undefined;
let secondPoisoned: string | undefined;

try {
Reflect.set(Array.prototype, "push", () => 0);
Reflect.set(Array.prototype, "join", () => "poisoned");
firstPoisoned = computeConfigHashSync(firstConfig);
secondPoisoned = computeConfigHashSync(secondConfig);
} finally {
Reflect.set(Array.prototype, "push", originalPush);
Reflect.set(Array.prototype, "join", originalJoin);
}

assertEquals(firstPoisoned, firstBaseline);
assertEquals(secondPoisoned, secondBaseline);
assertNotEquals(firstPoisoned, secondPoisoned);
});

it("should return a string", () => {
const hash = computeConfigHashSync({});
assertEquals(typeof hash, "string");
Expand Down
64 changes: 38 additions & 26 deletions src/cache/config-hash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,20 @@

import { computeHash } from "#veryfront/utils";
import { VERSION } from "#veryfront/utils/version.ts";
import {
primordialArrayJoin as arrayJoin,
primordialArrayPush as arrayPush,
} from "#veryfront/platform/compat/primordials/array.ts";
import {
CSSTYPE_VERSION,
DEFAULT_REACT_VERSION,
TAILWIND_VERSION,
} from "#veryfront/transforms/import-rewriter/url-builder.ts";
import { buildDependencyPinningCacheVariant } from "./keys/dependency-pinning.ts";

const JSONStringify = JSON.stringify;
const ObjectCreate = Object.create;

/**
* Configuration that affects transform output.
*/
Expand Down Expand Up @@ -48,21 +55,24 @@ export function computeConfigHash(config: TransformConfig): Promise<string> {
config.dependencyPinningCacheKey,
config.moduleServerOrigin,
);
const normalized = {
transformVersion: VERSION,
reactVersion: config.reactVersion ?? DEFAULT_REACT_VERSION,
jsxImportSource: config.jsxImportSource ?? "react",
moduleServerUrl: config.moduleServerUrl ?? null,
vendorBundleHash: config.vendorBundleHash ?? null,
apiBaseUrl: config.apiBaseUrl ?? null,
studioEmbed: config.studioEmbed ?? false,
dev: config.dev ?? false,
...(dependencyPinningCacheVariant ? { dependencyPinningCacheVariant } : {}),
csstype: CSSTYPE_VERSION,
tailwind: TAILWIND_VERSION,
};
// Null-prototype storage preserves the existing JSON cache-key format while
// preventing project code from injecting an inherited toJSON hook.
const normalized = ObjectCreate(null) as Record<string, string | boolean | null>;
normalized.transformVersion = VERSION;
normalized.reactVersion = config.reactVersion ?? DEFAULT_REACT_VERSION;
normalized.jsxImportSource = config.jsxImportSource ?? "react";
normalized.moduleServerUrl = config.moduleServerUrl ?? null;
normalized.vendorBundleHash = config.vendorBundleHash ?? null;
normalized.apiBaseUrl = config.apiBaseUrl ?? null;
normalized.studioEmbed = config.studioEmbed ?? false;
normalized.dev = config.dev ?? false;
if (dependencyPinningCacheVariant) {
normalized.dependencyPinningCacheVariant = dependencyPinningCacheVariant;
}
normalized.csstype = CSSTYPE_VERSION;
normalized.tailwind = TAILWIND_VERSION;

return computeHash(JSON.stringify(normalized));
return computeHash(JSONStringify(normalized));
}

/**
Expand All @@ -71,25 +81,27 @@ export function computeConfigHash(config: TransformConfig): Promise<string> {
* Use this when you need a config hash but can't afford async overhead.
*/
export function computeConfigHashSync(config: TransformConfig): string {
const parts = [
`v${VERSION}`,
config.reactVersion ?? DEFAULT_REACT_VERSION,
config.jsxImportSource ?? "react",
encodeConfigPart("modules", config.moduleServerUrl),
encodeConfigPart("vendor", config.vendorBundleHash),
encodeConfigPart("api", config.apiBaseUrl),
config.studioEmbed ? "studio" : "",
config.dev ? "dev" : "",
].filter(Boolean);
const parts: string[] = [];
arrayPush(parts, `v${VERSION}`);
arrayPush(parts, config.reactVersion ?? DEFAULT_REACT_VERSION);
arrayPush(parts, config.jsxImportSource ?? "react");
const moduleServerUrlPart = encodeConfigPart("modules", config.moduleServerUrl);
if (moduleServerUrlPart) arrayPush(parts, moduleServerUrlPart);
const vendorBundleHashPart = encodeConfigPart("vendor", config.vendorBundleHash);
if (vendorBundleHashPart) arrayPush(parts, vendorBundleHashPart);
const apiBaseUrlPart = encodeConfigPart("api", config.apiBaseUrl);
if (apiBaseUrlPart) arrayPush(parts, apiBaseUrlPart);
if (config.studioEmbed) arrayPush(parts, "studio");
if (config.dev) arrayPush(parts, "dev");
const dependencyPinningCacheVariant = buildDependencyPinningCacheVariant(
config.dependencyPinningCacheKey,
config.moduleServerOrigin,
);
if (dependencyPinningCacheVariant) {
parts.push(`pins:${dependencyPinningCacheVariant}`);
arrayPush(parts, `pins:${dependencyPinningCacheVariant}`);
}

return parts.join(":");
return arrayJoin(parts, ":");
}

function encodeConfigPart(label: string, value: string | undefined): string {
Expand Down
32 changes: 27 additions & 5 deletions src/embedding/rag-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -839,14 +839,36 @@ describe("ragStore", () => {

const searchPromise = store.search("needle");
await queryEmbeddingStarted;
const listDocumentsPromise = store.listDocuments();
let blockedTimer: ReturnType<typeof setTimeout> | undefined;
let observedDocuments: Awaited<ReturnType<typeof store.listDocuments>> | "blocked";
try {
observedDocuments = await Promise.race([
listDocumentsPromise,
new Promise<"blocked">((resolve) => {
blockedTimer = setTimeout(() => resolve("blocked"), 5_000);
}),
]);
} finally {
if (blockedTimer !== undefined) clearTimeout(blockedTimer);
releaseQueryEmbedding();
}
Comment thread
kojiwakayama marked this conversation as resolved.
let settleTimer: ReturnType<typeof setTimeout> | undefined;
const documents = await Promise.race([
store.listDocuments(),
new Promise<"blocked">((resolve) => setTimeout(() => resolve("blocked"), 50)),
]);
releaseQueryEmbedding();
listDocumentsPromise,
new Promise<never>((_, reject) => {
settleTimer = setTimeout(
() => reject(new Error("listDocuments did not settle after query embedding release")),
5_000,
);
}),
]).finally(() => {
if (settleTimer !== undefined) clearTimeout(settleTimer);
});
await searchPromise;

assert(Array.isArray(documents));
assert(Array.isArray(observedDocuments));
assertEquals(observedDocuments, documents);
assertEquals(documents.length, 1);
assertEquals(documents[0]?.title, "Doc");
});
Expand Down
8 changes: 6 additions & 2 deletions src/modules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,12 +142,14 @@ module before importing it.

```typescript
import { loadImportMap, mergeImportMaps, resolveImport } from "#veryfront/modules";
import type { VeryfrontConfig } from "#veryfront/config";
import type { RuntimeAdapter } from "#veryfront/platform/adapters/base.ts";

export async function resolveReact(
adapter: RuntimeAdapter,
validatedConfig?: VeryfrontConfig,
) {
const projectMap = await loadImportMap("/workspace/site", adapter);
const projectMap = await loadImportMap("/workspace/site", adapter, validatedConfig);
const overrides = {
imports: {
"@app/": "/_vf_modules/app/",
Expand All @@ -162,7 +164,9 @@ export async function resolveReact(
`mergeImportMaps` accepts maps as separate arguments. Later maps win for exact
keys, while scoped maps are merged per scope. `loadImportMap` applies framework
defaults, project `deno.json`, and Veryfront configuration in that order and
then enforces the framework React mappings.
then enforces the framework React mappings. Its optional third argument accepts
an already validated request configuration; without it, the loader discovers
the project configuration from the project path.

## Operational contracts

Expand Down
Loading