Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
136 changes: 135 additions & 1 deletion scripts/build/compile-binary.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { walk } from "#std/fs/walk";
import { assertEquals } from "#veryfront/testing/assert.ts";
import { assertEquals, assertThrows } from "#veryfront/testing/assert.ts";
import { it } from "#veryfront/testing/bdd.ts";
import { FIRST_PARTY_DEFERRED_BUILTIN_EXTENSION_POLICIES } from "#veryfront/extensions/first-party-defaults.ts";
import {
Expand All @@ -8,6 +8,79 @@ import {
PROXY_INCLUDES,
} from "./compile-binary.ts";

interface DenoInfoDependency {
code?: { specifier: string };
type?: { specifier: string };
}

interface DenoInfoModule {
dependencies?: DenoInfoDependency[] | null;
specifier: string;
}

interface DenoInfoGraph {
roots: string[];
modules: DenoInfoModule[];
}

function isResolvedDependency(value: unknown): value is DenoInfoDependency {
if (!value || typeof value !== "object") return false;
const dependency = value as Record<string, unknown>;
if (
typeof dependency.specifier !== "string" ||
dependency.specifier.length === 0
) {
return false;
}

const targets = [dependency.code, dependency.type].filter((target) =>
target != null
);
return targets.length > 0 &&
targets.every((target) =>
typeof target === "object" &&
target !== null &&
typeof (target as Record<string, unknown>).specifier === "string" &&
((target as Record<string, unknown>).specifier as string).length > 0
);
}

function parseDenoInfoGraph(value: unknown): DenoInfoGraph {
if (!value || typeof value !== "object") {
throw new TypeError("Invalid deno info graph");
}
const graph = value as Record<string, unknown>;
if (
!Array.isArray(graph.roots) ||
graph.roots.length === 0 ||
!graph.roots.every((root) => typeof root === "string" && root.length > 0) ||
!Array.isArray(graph.modules) ||
graph.modules.length === 0
) {
throw new TypeError("Invalid deno info graph");
}

for (const module of graph.modules) {
if (!module || typeof module !== "object") {
throw new TypeError("Invalid deno info module");
}
const record = module as Record<string, unknown>;
if (typeof record.specifier !== "string" || record.specifier.length === 0) {
throw new TypeError("Invalid deno info module specifier");
}
if (
record.dependencies !== undefined &&
record.dependencies !== null &&
(!Array.isArray(record.dependencies) ||
!record.dependencies.every(isResolvedDependency))
) {
throw new TypeError("Invalid deno info dependency");
}
}

return graph as unknown as DenoInfoGraph;
}

it("compiled CLI embeds the default Node WebSocket extension for HMR", () => {
const args = createCompileArgs({
entrypoint: "cli/main.ts",
Expand Down Expand Up @@ -184,6 +257,67 @@ it("proxy binary embeds only the runtime-resolved proxy entrypoint", async () =>
}
});

it("proxy binary cannot reach declarative config worker modules", async () => {
const command = new Deno.Command(Deno.execPath(), {
args: ["info", "--json", "cli/proxy-main.ts"],
stdout: "piped",
stderr: "piped",
});
const output = await command.output();
assertEquals(
output.success,
true,
new TextDecoder().decode(output.stderr),
);

const info = parseDenoInfoGraph(
JSON.parse(new TextDecoder().decode(output.stdout)),
);
const modules = new Map(
info.modules.map((module) => [module.specifier, module]),
);
for (const root of info.roots) {
if (!modules.has(root)) {
throw new TypeError("Deno info root is missing from the module graph");
}
}
const reachable = new Set(info.roots);
const pending = [...reachable];
while (pending.length > 0) {
const specifier = pending.shift()!;
for (const dependency of modules.get(specifier)?.dependencies ?? []) {
const dependencySpecifier = dependency.code?.specifier;
if (!dependencySpecifier || reachable.has(dependencySpecifier)) continue;
reachable.add(dependencySpecifier);
pending.push(dependencySpecifier);
}
}
const evaluatorWorkerModules = [...reachable]
.filter((specifier) => specifier.includes("declarative-evaluator-worker"));

assertEquals(
evaluatorWorkerModules,
[],
"the proxy must not reach project config evaluation; it only forwards requests to the production server",
);
});

it("proxy graph validation rejects incomplete metadata", () => {
for (
const graph of [
{},
{ roots: [], modules: [] },
{ roots: ["proxy"], modules: [{}] },
{
roots: ["proxy"],
modules: [{ specifier: "proxy", dependencies: [{}] }],
},
]
) {
assertThrows(() => parseDenoInfoGraph(graph), TypeError);
}
});

it("proxy release verifies lock freshness and publishes an exact SBOM", async () => {
const workflow = await Deno.readTextFile(".github/workflows/cicd.yml");
const denoConfig = JSON.parse(await Deno.readTextFile("deno.json")) as {
Expand Down
19 changes: 6 additions & 13 deletions scripts/build/compile-binary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,19 +69,12 @@ export const DEFAULT_INCLUDES = [
];

export const PROXY_INCLUDES = [
// Deliberately omits UNTRACEABLE_WORKER_INCLUDES, on build grounds only.
// Adding them fails the compile outright: the worker entry's graph wants
// @babel/types@7.29.8 plus @babel/helper-string-parser and
// @babel/helper-validator-identifier, while proxy-deno.lock pins
// @babel/types@7.29.0, and --frozen refuses to update the lock.
//
// This is NOT evidence that the proxy is safe. The lock already carries a
// babel parse toolchain (parser, generator, traverse, types), so "the deps
// are absent, therefore the worker never runs here" does not follow --
// the conflict is a version skew, not an absence. `cli/proxy-main.ts` does
// pull the evaluator's runner into its graph, so whether the proxy can reach
// a spawn at runtime is an open question, tracked in veryfront-issue-inbox#382.
// If it can, this list and the proxy lock have to be regenerated together.
// Deliberately omits UNTRACEABLE_WORKER_INCLUDES. The standalone proxy
// forwards project requests to the production server and never evaluates
// project config. Its entry graph must therefore remain unable to reach the
// declarative evaluator worker. compile-binary.test.ts enforces that runtime
// boundary. If project config evaluation is added to the proxy, embed every
// reachable worker here and regenerate the proxy lock in the same change.
//
// The proxy runtime is loaded after provider activation. Providers are
// statically referenced by cli/proxy-main.ts so --include does not embed the
Expand Down
5 changes: 4 additions & 1 deletion src/proxy/control-plane-signature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ import {
verifyControlPlaneJwsSignature,
verifyDispatchJwsSignature,
} from "#veryfront/channels/control-plane.ts";
import { isRequestBodyTooLargeError, readBodyWithLimit } from "#veryfront/security/index.ts";
import {
isRequestBodyTooLargeError,
readBodyWithLimit,
} from "#veryfront/security/input-validation/limits.ts";
import { DEFAULT_MAX_BODY_SIZE_BYTES } from "#veryfront/utils/constants/index.ts";
import { isWellFormedString } from "#veryfront/utils/is-well-formed-string.ts";
import { isCanonicalOpaqueProjectIdentifier } from "#veryfront/utils/project-identity.ts";
Expand Down