Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
jsonValuesEqual,
readProviderOptions,
readRecord,
stringifyJsonValue,
stringifyToolResultValue,
unwrapToolInputSchema,
} from "veryfront/provider/shared";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {
jsonValuesEqual,
readProviderOptions,
stringifyJsonValue,
stringifyToolArguments,
stringifyToolResultValue,
unwrapToolInputSchema,
Expand Down
4 changes: 3 additions & 1 deletion src/cache/config-hash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
} from "#veryfront/transforms/import-rewriter/url-builder.ts";
import { buildDependencyPinningCacheVariant } from "./keys/dependency-pinning.ts";

const JSONStringify = JSON.stringify;

/**
* Configuration that affects transform output.
*/
Expand Down Expand Up @@ -62,7 +64,7 @@ export function computeConfigHash(config: TransformConfig): Promise<string> {
tailwind: TAILWIND_VERSION,
};

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

/**
Expand Down
35 changes: 23 additions & 12 deletions src/modules/import-map/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { rendererLogger as logger } from "#veryfront/utils";
import { dirname, join } from "#veryfront/compat/path/index.ts";
import type { RuntimeAdapter } from "#veryfront/platform/adapters/base.ts";
import { isVirtualFilesystem } from "#veryfront/platform/adapters/fs/wrapper.ts";
import { getConfig } from "#veryfront/config";
import { getConfig, type VeryfrontConfig } from "#veryfront/config";
import type { ImportMapConfig } from "./types.ts";
import { getDefaultImportMap } from "./default-import-map.ts";
import { mergeImportMaps } from "./merger.ts";
Expand Down Expand Up @@ -135,9 +135,20 @@ async function loadDenoJsonImportMap(
return null;
}

function getConfigImportMap(config: VeryfrontConfig): ImportMapConfig | null {
const importMap = config.resolve?.importMap;
if (!importMap || typeof importMap !== "object") return null;

return {
imports: importMap.imports ?? {},
scopes: importMap.scopes ?? {},
};
}

export function loadImportMap(
startPath: string,
adapter?: RuntimeAdapter,
config?: VeryfrontConfig,
): Promise<ImportMapConfig> {
return withSpan(
"modules.importMap.load",
Expand All @@ -147,19 +158,19 @@ export function loadImportMap(
// First, load import map from deno.json (if exists)
const denoJsonMap = await loadDenoJsonImportMap(startPath, runtimeAdapter);

// Then, try to get config's import map
// Then, try to get config's import map. A config already validated for
// the authenticated request takes precedence over re-reading it from the
// project source.
let configMap: ImportMapConfig | null = null;
try {
const cfg = await getConfig(startPath, runtimeAdapter);
const importMap = cfg?.resolve?.importMap;
if (importMap && typeof importMap === "object") {
configMap = {
imports: importMap.imports ?? {},
scopes: importMap.scopes ?? {},
};
if (config) {
configMap = getConfigImportMap(config);
} else {
try {
const cfg = await getConfig(startPath, runtimeAdapter);
if (cfg) configMap = getConfigImportMap(cfg);
} catch (_) {
/* expected: config not found or invalid, continue without it */
}
} catch (_) {
/* expected: config not found or invalid, continue without it */
}

// Merge: defaults < deno.json < config
Expand Down
153 changes: 153 additions & 0 deletions src/modules/import-map/preloader-primordial-poisoning.worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import type { VeryfrontConfig } from "#veryfront/config";
import type { RuntimeAdapter } from "#veryfront/platform/adapters/base.ts";
import { ImportMapPreloader } from "./preloader.ts";

const adapter = {
fs: {},
env: {},
} as unknown as RuntimeAdapter;
const configA = {
resolve: {
importMap: {
imports: { package: "https://example.com/package-a.ts" },
},
},
} as VeryfrontConfig;
const configB = {
resolve: {
importMap: {
imports: { package: "https://example.com/package-b.ts" },
},
},
} as VeryfrontConfig;

async function runPoisoningRegression() {
const original = {
arrayMap: Array.prototype.map,
arraySort: Array.prototype.sort,
dateNow: Date.now,
jsonStringify: JSON.stringify,
map: Map,
mapClear: Map.prototype.clear,
mapDelete: Map.prototype.delete,
mapForEach: Map.prototype.forEach,
mapGet: Map.prototype.get,
mapSet: Map.prototype.set,
mapSize: Object.getOwnPropertyDescriptor(Map.prototype, "size")!,
mathMin: Math.min,
numberIsFinite: Number.isFinite,
numberIsSafeInteger: Number.isSafeInteger,
objectEntries: Object.entries,
promise: Promise,
promiseResolve: Promise.resolve,
set: Set,
setAdd: Set.prototype.add,
setDelete: Set.prototype.delete,
setSize: Object.getOwnPropertyDescriptor(Set.prototype, "size")!,
};
const poisoned = () => {
throw new Error("poisoned primordial");
};
let first: Awaited<ReturnType<ImportMapPreloader["preload"]>> | undefined;
let firstAgain: Awaited<ReturnType<ImportMapPreloader["preload"]>> | undefined;
let second: Awaited<ReturnType<ImportMapPreloader["preload"]>> | undefined;
let evicted:
| Awaited<ReturnType<ImportMapPreloader["getCached"]>>
| undefined;
let loads = 0;

try {
Reflect.set(Array.prototype, "map", poisoned);
Reflect.set(Array.prototype, "sort", poisoned);
Reflect.set(Date, "now", poisoned);
Reflect.set(JSON, "stringify", poisoned);
Reflect.set(globalThis, "Map", class PoisonedMap {});
Reflect.set(original.map.prototype, "clear", poisoned);
Reflect.set(original.map.prototype, "delete", poisoned);
Reflect.set(original.map.prototype, "forEach", poisoned);
Reflect.set(original.map.prototype, "get", poisoned);
Reflect.set(original.map.prototype, "set", poisoned);
Object.defineProperty(original.map.prototype, "size", {
configurable: true,
get: poisoned,
});
Reflect.set(Math, "min", poisoned);
Reflect.set(Number, "isFinite", poisoned);
Reflect.set(Number, "isSafeInteger", poisoned);
Reflect.set(Object, "entries", poisoned);
Reflect.set(globalThis, "Promise", class PoisonedPromise {});
Reflect.set(original.promise, "resolve", poisoned);
Reflect.set(globalThis, "Set", class PoisonedSet {});
Reflect.set(original.set.prototype, "add", poisoned);
Reflect.set(original.set.prototype, "delete", poisoned);
Object.defineProperty(original.set.prototype, "size", {
configurable: true,
get: poisoned,
});

const preloader = new ImportMapPreloader({
maxProjects: 1,
maxVariantsPerProject: 1,
ttlMs: 1_000,
loadImportMap: async (_path, _adapter, config) => ({
imports: {
loaded: String(++loads),
package: config?.resolve?.importMap?.imports?.package ?? "",
},
}),
});
const contextA = { contentSourceId: "source", config: configA };
const contextB = { contentSourceId: "source", config: configB };

first = await preloader.preload("/project", adapter, "project", contextA);
firstAgain = await preloader.preload(
"/project",
adapter,
"project",
contextA,
);
second = await preloader.preload("/project", adapter, "project", contextB);
evicted = await preloader.getCached("project", contextA);
} finally {
Reflect.set(Array.prototype, "map", original.arrayMap);
Reflect.set(Array.prototype, "sort", original.arraySort);
Reflect.set(Date, "now", original.dateNow);
Reflect.set(JSON, "stringify", original.jsonStringify);
Reflect.set(original.map.prototype, "clear", original.mapClear);
Reflect.set(original.map.prototype, "delete", original.mapDelete);
Reflect.set(original.map.prototype, "forEach", original.mapForEach);
Reflect.set(original.map.prototype, "get", original.mapGet);
Reflect.set(original.map.prototype, "set", original.mapSet);
Object.defineProperty(original.map.prototype, "size", original.mapSize);
Reflect.set(Math, "min", original.mathMin);
Reflect.set(Number, "isFinite", original.numberIsFinite);
Reflect.set(Number, "isSafeInteger", original.numberIsSafeInteger);
Reflect.set(Object, "entries", original.objectEntries);
Reflect.set(original.promise, "resolve", original.promiseResolve);
Reflect.set(original.set.prototype, "add", original.setAdd);
Reflect.set(original.set.prototype, "delete", original.setDelete);
Object.defineProperty(original.set.prototype, "size", original.setSize);
Reflect.set(globalThis, "Map", original.map);
Reflect.set(globalThis, "Promise", original.promise);
Reflect.set(globalThis, "Set", original.set);
}

return {
firstLoaded: first?.imports?.loaded,
firstSame: firstAgain === first,
secondLoaded: second?.imports?.loaded,
secondPackage: second?.imports?.package,
evicted: evicted === undefined,
loads,
};
}

try {
const result = await runPoisoningRegression();
postMessage({ ok: true, result });
} catch (error) {
postMessage({
ok: false,
error: error instanceof Error ? (error.stack ?? error.message) : String(error),
});
}
Loading
Loading