1import {
2 packageWebsiteArtifact,
3 stageWebsiteArtifact,
4} from "@/Neon/Website/Artifact.ts";
5import * as NodeServices from "@effect/platform-node/NodeServices";
6import { expect, it } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import * as FileSystem from "effect/FileSystem";
9import * as Path from "effect/Path";
10import * as Result from "effect/Result";
11import * as Stream from "effect/Stream";
12import * as ChildProcess from "effect/process/ChildProcess";
13import { unzipSync } from "fflate";
14import * as FetchHttpClient from "effect/http/FetchHttpClient";
15import { createHash } from "node:crypto";
16import { gzipSync } from "node:zlib";
17
18const fixture = Effect.gen(function* () {
19 const fs = yield* FileSystem.FileSystem;
20 const path = yield* Path.Path;
21 const root = yield* fs.makeTempDirectoryScoped({
22 prefix: "neon-website-test-",
23 });
24 const dist = path.join(root, "dist");
25 yield* fs.makeDirectory(dist);
26 yield* fs.writeFileString(path.join(dist, "index.html"), "<h1>Neon</h1>");
27 return { fs, path, root, dist };
28});
29
30it.effect(
31 "static archives are deterministic and omit env and source maps",
32 () =>
33 Effect.gen(function* () {
34 const { fs, path, root, dist } = yield* fixture;
35 yield* fs.writeFileString(
36 path.join(dist, ".env.production"),
37 "TOKEN=do-not-package",
38 );
39 yield* fs.writeFileString(path.join(dist, "app.js.map"), "source-secret");
40 const props = {
41 root,
42 distDir: dist,
43 static: { notFoundHandling: "spa" as const },
44 };
45 const first = yield* packageWebsiteArtifact(props);
46 const second = yield* packageWebsiteArtifact(props);
47 expect(first.hash).toBe(second.hash);
48 const files = yield* Effect.sync(() => unzipSync(first.archive));
49 expect(files["index.mjs"]).toBeDefined();
50 expect(
51 Object.keys(files).some(
52 (name) => name.includes(".env") || name.endsWith(".map"),
53 ),
54 ).toBe(false);
55 const source = yield* Effect.sync(() =>
56 new TextDecoder().decode(files["index.mjs"]),
57 );
58 expect(source).toContain("export default");
59 expect(source).not.toContain(".listen(");
60 yield* fs.writeFileString(
61 path.join(dist, "index.html"),
62 "<h1>Updated</h1>",
63 );
64 expect((yield* packageWebsiteArtifact(props)).hash).not.toBe(first.hash);
65 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
66 { tags: ["unit", "provider:neon", "provider:neon:website", "local"] },
67);
68
69for (const kind of [
70 "escape",
71 "native",
72 "executable",
73 "secret-alias",
74 "cycle",
75 "error-page",
76] as const) {
77 it.effect(
78 `rejects unsafe ${kind} output`,
79 () =>
80 Effect.gen(function* () {
81 const { fs, path, root, dist } = yield* fixture;
82 if (kind === "escape") {
83 yield* fs.writeFileString(path.join(root, "secret.txt"), "private");
84 yield* fs.symlink("../secret.txt", path.join(dist, "leak.txt"));
85 } else if (kind === "native")
86 yield* fs.writeFileString(path.join(dist, "addon.node"), "native");
87 else if (kind === "executable")
88 yield* fs.writeFile(
89 path.join(dist, "binary"),
90 new Uint8Array([0xcf, 0xfa, 0xed, 0xfe]),
91 );
92 else if (kind === "secret-alias") {
93 yield* fs.writeFileString(path.join(dist, ".env"), "SECRET=hidden");
94 yield* fs.symlink(".env", path.join(dist, "public.txt"));
95 } else if (kind === "cycle")
96 yield* fs.symlink(".", path.join(dist, "cycle"));
97 const result = yield* stageWebsiteArtifact({
98 root,
99 distDir: dist,
100 static: {
101 errorPage: kind === "error-page" ? "../secret.txt" : undefined,
102 },
103 }).pipe(Effect.result);
104 expect(Result.isFailure(result)).toBe(true);
105 if (Result.isFailure(result))
106 expect(result.failure._tag).toBe("WebsiteArtifactError");
107 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
108 { tags: ["unit", "provider:neon", "provider:neon:website", "local"] },
109 );
110}
111
112for (const sensitive of [
113 ".env.production",
114 ".alchemy/state file.json",
115 "private.pem",
116])
117 for (const alias of [false, true])
118 it.effect(
119 `rejects traced ${JSON.stringify(sensitive)}${alias ? " through a symlink" : ""} with a sanitized filename`,
120 () =>
121 Effect.gen(function* () {
122 const { fs, path, root, dist } = yield* fixture;
123 const secret = path.join(root, sensitive);
124 yield* fs.makeDirectory(path.dirname(secret), { recursive: true });
125 yield* fs.writeFileString(secret, "fixture-secret-do-not-log");
126 const dependency = alias ? "runtime-data.txt" : sensitive;
127 if (alias) yield* fs.symlink(secret, path.join(root, dependency));
128 const serverEntry = path.join(dist, "serve.mjs");
129 yield* fs.writeFileString(
130 serverEntry,
131 `import { readFileSync } from "node:fs";
132const value = readFileSync(new URL(${JSON.stringify(`../${dependency}`)}, import.meta.url), "utf8");
133export default { fetch: () => new Response(value) };`,
134 );
135 const result = yield* stageWebsiteArtifact({
136 root,
137 distDir: dist,
138 serverEntry,
139 }).pipe(Effect.result);
140 expect(Result.isFailure(result)).toBe(true);
141 if (Result.isFailure(result)) {
142 expect(result.failure._tag).toBe("WebsiteArtifactError");
143 expect(result.failure.message).toBe(
144 `A traced dependency selects a sensitive file: ${sensitive.replace(/[^a-zA-Z0-9_./@+-]/g, "_")}`,
145 );
146 expect(result.failure.message).not.toContain(root);
147 expect(result.failure.message).not.toContain(
148 "fixture-secret-do-not-log",
149 );
150 }
151 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
152 { tags: ["unit", "provider:neon", "provider:neon:website", "local"] },
153 );
154
155it.effect(
156 "traces referenced source data without selecting neighboring state or secrets",
157 () =>
158 Effect.gen(function* () {
159 const { fs, path, root, dist } = yield* fixture;
160 yield* fs.makeDirectory(path.join(root, "src"));
161 yield* fs.makeDirectory(path.join(root, ".alchemy"));
162 yield* fs.writeFileString(path.join(root, "src/guide.mdx"), "# Guide");
163 yield* fs.writeFileString(path.join(root, ".env"), "fixture-secret");
164 yield* fs.writeFileString(path.join(root, "private.key"), "fixture-key");
165 yield* fs.writeFileString(
166 path.join(root, ".alchemy/state.json"),
167 '{"private":"fixture-state"}',
168 );
169 const serverEntry = path.join(dist, "serve.mjs");
170 yield* fs.writeFileString(
171 serverEntry,
172 'import { readFileSync } from "node:fs"; const guide = readFileSync(new URL("../src/guide.mdx", import.meta.url), "utf8"); export default { fetch: () => new Response(guide) };',
173 );
174 const props = { root, distDir: dist, serverEntry };
175 const first = yield* packageWebsiteArtifact(props);
176 const archive = yield* Effect.sync(() => unzipSync(first.archive));
177 expect(Object.keys(archive).sort()).toEqual([
178 "files/dist/index.html",
179 "files/dist/serve.mjs",
180 "files/src/guide.mdx",
181 "index.mjs",
182 ]);
183 yield* fs.writeFileString(
184 path.join(root, "src/guide.mdx"),
185 "# Updated guide",
186 );
187 expect((yield* packageWebsiteArtifact(props)).hash).not.toBe(first.hash);
188 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
189 { tags: ["unit", "provider:neon", "provider:neon:website", "local"] },
190);
191
192for (const alias of [false, true])
193 it.effect(
194 `rejects a traced workspace escape${alias ? " through a symlink" : ""}`,
195 () =>
196 Effect.gen(function* () {
197 const { fs, path, root, dist } = yield* fixture;
198 const outside = yield* fs.makeTempDirectoryScoped();
199 const dependency = path.join(outside, "runtime-data.txt");
200 yield* fs.writeFileString(dependency, "outside-workspace");
201 const selected = alias
202 ? path.join(root, "runtime-data.txt")
203 : dependency;
204 if (alias) yield* fs.symlink(dependency, selected);
205 const serverEntry = path.join(dist, "serve.mjs");
206 yield* fs.writeFileString(
207 serverEntry,
208 `import { readFileSync } from "node:fs"; const value = readFileSync(${JSON.stringify(selected)}, "utf8"); export default { fetch: () => new Response(value) };`,
209 );
210 const result = yield* stageWebsiteArtifact({
211 root,
212 distDir: dist,
213 serverEntry,
214 }).pipe(Effect.result);
215 expect(Result.isFailure(result)).toBe(true);
216 if (Result.isFailure(result)) {
217 expect(result.failure._tag).toBe("WebsiteArtifactError");
218 expect(result.failure.message).toBe(
219 "A traced dependency escapes the application workspace.",
220 );
221 }
222 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
223 { tags: ["unit", "provider:neon", "provider:neon:website", "local"] },
224 );
225
226for (const mode of ["import", "require"] as const)
227 it.effect(
228 `resolves pnpm ${mode} aliases without copying untraced dependencies`,
229 () =>
230 Effect.gen(function* () {
231 const { fs, path, root, dist } = yield* fixture;
232 const store = path.join(
233 root,
234 "node_modules",
235 ".pnpm",
236 "answer@1",
237 "node_modules",
238 "answer",
239 );
240 yield* fs.makeDirectory(store, { recursive: true });
241 yield* fs.writeFileString(
242 path.join(root, "package.json"),
243 '{"type":"module"}',
244 );
245 yield* fs.writeFileString(
246 path.join(store, "package.json"),
247 '{"name":"answer","type":"module","exports":"./index.js"}',
248 );
249 yield* fs.writeFileString(
250 path.join(store, "index.js"),
251 "export const answer = 42;",
252 );
253 yield* fs.writeFileString(path.join(store, "unused.txt"), "not-traced");
254 yield* fs.symlink(
255 ".pnpm/answer@1/node_modules/answer",
256 path.join(root, "node_modules", "answer"),
257 );
258 const other = path.join(
259 root,
260 "node_modules/.pnpm/answer@2/node_modules/answer",
261 );
262 yield* fs.makeDirectory(other, { recursive: true });
263 yield* fs.writeFileString(
264 path.join(other, "package.json"),
265 '{"name":"answer","type":"module","exports":"./index.js"}',
266 );
267 yield* fs.writeFileString(
268 path.join(other, "index.js"),
269 "export const answer = 99;",
270 );
271 yield* fs.makeDirectory(
272 path.join(root, "node_modules/.pnpm/node_modules"),
273 );
274 yield* fs.symlink(
275 "../answer@2/node_modules/answer",
276 path.join(root, "node_modules/.pnpm/node_modules/answer"),
277 );
278 yield* fs.writeFileString(
279 path.join(dist, "other.mjs"),
280 'import "../node_modules/.pnpm/node_modules/answer/index.js";',
281 );
282 const serverEntry = path.join(dist, "serve-neon.mjs");
283 yield* fs.writeFileString(
284 serverEntry,
285 (mode === "import"
286 ? 'import { answer } from "answer";'
287 : 'import { createRequire } from "node:module"; const require = createRequire(import.meta.url); const { answer } = require("answer");') +
288 " export default { fetch() { return new Response(String(answer)); } };",
289 );
290 const props = { root, distDir: dist, serverEntry };
291 const first = yield* packageWebsiteArtifact(props);
292 const files = yield* Effect.sync(() => unzipSync(first.archive));
293 expect(
294 Object.keys(files).some((name) =>
295 name.endsWith("node_modules/answer/index.js"),
296 ),
297 ).toBe(true);
298 expect(
299 Object.keys(files).some((name) => name.endsWith("unused.txt")),
300 ).toBe(false);
301 const staged = yield* stageWebsiteArtifact(props);
302 const proc = yield* ChildProcess.make("node", [
303 "--input-type=module",
304 "-e",
305 `const {default: handler} = await import(${JSON.stringify(path.join(staged.directory, "index.mjs"))}); console.log(await handler.fetch(new Request("http://localhost/")).text());`,
306 ]);
307 const [code, stdout, stderr] = yield* Effect.all(
308 [
309 proc.exitCode,
310 proc.stdout.pipe(Stream.decodeText, Stream.mkString),
311 proc.stderr.pipe(Stream.decodeText, Stream.mkString),
312 ],
313 { concurrency: "unbounded" },
314 );
315 expect({ code, stderr }).toEqual({ code: 0, stderr: "" });
316 expect(stdout.trim()).toBe("42");
317 yield* fs.writeFileString(
318 path.join(store, "index.js"),
319 "export const answer = 43;",
320 );
321 expect((yield* packageWebsiteArtifact(props)).hash).not.toBe(
322 first.hash,
323 );
324 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
325 { tags: ["unit", "provider:neon", "provider:neon:website", "local"] },
326 );
327
328const elf = (machine = 183) => {
329 const bytes = new Uint8Array(64);
330 bytes.set([0x7f, 0x45, 0x4c, 0x46, 2, 1, 1]);
331 bytes[16] = 3;
332 bytes[18] = machine;
333 return bytes;
334};
335
336for (const machine of [183, 62])
337 it.effect(
338 `validates ELF target architecture ${machine}`,
339 () =>
340 Effect.gen(function* () {
341 const { fs, path, root, dist } = yield* fixture;
342 yield* fs.writeFile(
343 path.join(dist, "addon.node"),
344 yield* Effect.sync(() => elf(machine)),
345 );
346 const result = yield* stageWebsiteArtifact({
347 root,
348 distDir: dist,
349 static: {},
350 }).pipe(Effect.result);
351 expect(Result.isSuccess(result)).toBe(machine === 183);
352 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
353 { tags: ["unit", "provider:neon", "provider:neon:website", "local"] },
354 );
355
356it.effect(
357 "Sharp metadata-only traces do not materialize native packages",
358 () =>
359 Effect.gen(function* () {
360 const { fs, path, root, dist } = yield* fixture;
361 const sharp = path.join(root, "node_modules/sharp");
362 yield* fs.makeDirectory(sharp, { recursive: true });
363 yield* fs.writeFileString(
364 path.join(sharp, "package.json"),
365 JSON.stringify({ name: "sharp", version: "0.35.4" }),
366 );
367 const serverEntry = path.join(dist, "serve.mjs");
368 yield* fs.writeFileString(
369 serverEntry,
370 'import manifest from "sharp/package.json" with { type: "json" }; export default { fetch: () => new Response(manifest.version) };',
371 );
372 const artifact = yield* stageWebsiteArtifact({
373 root,
374 distDir: dist,
375 serverEntry,
376 });
377 expect(
378 yield* fs.exists(
379 path.join(
380 artifact.directory,
381 "files/node_modules/sharp/package.json",
382 ),
383 ),
384 ).toBe(true);
385 expect(
386 yield* fs.exists(
387 path.join(artifact.directory, "files/node_modules/@img"),
388 ),
389 ).toBe(false);
390 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
391 { tags: ["unit", "provider:neon", "provider:neon:website", "local"] },
392);
393
394// Minimal ustar fixtures exercise the registry boundary without downloading packages.
395const tarball = (files: [string, Uint8Array, string?][]) => {
396 const blocks: Buffer[] = [];
397 for (const [name, content, type = "0"] of files) {
398 const header = Buffer.alloc(512);
399 header.write(name, 0);
400 header.write("0000644\0", 100);
401 header.write(content.length.toString(8).padStart(11, "0") + "\0", 124);
402 header.fill(32, 148, 156);
403 header.write(type, 156);
404 header.write("ustar\0", 257);
405 const checksum = header.reduce(
406 (sum: number, byte: number) => sum + byte,
407 0,
408 );
409 header.write(checksum.toString(8).padStart(6, "0") + "\0 ", 148);
410 blocks.push(
411 header,
412 Buffer.from(content),
413 Buffer.alloc((512 - (content.length % 512)) % 512),
414 );
415 }
416 return gzipSync(Buffer.concat([...blocks, Buffer.alloc(1024)]));
417};
418
419for (const scenario of [
420 "valid",
421 "integrity",
422 "identity",
423 "origin",
424 "traversal",
425 "symlink",
426 "architecture",
427 "musl",
428] as const)
429 it.effect(
430 `Sharp platform replacement verifies ${scenario}`,
431 () =>
432 Effect.gen(function* () {
433 const { fs, path, root, dist } = yield* fixture;
434 const sharp = path.join(root, "node_modules/sharp");
435 yield* fs.makeDirectory(sharp, { recursive: true });
436 yield* fs.writeFileString(
437 path.join(sharp, "package.json"),
438 JSON.stringify({
439 name: "sharp",
440 version: "0.34.5",
441 main: "index.js",
442 optionalDependencies: {
443 "@img/sharp-linux-arm64": "0.34.5",
444 "@img/sharp-libvips-linux-arm64": "1.2.4",
445 },
446 }),
447 );
448 yield* fs.writeFileString(
449 path.join(sharp, "index.js"),
450 "module.exports = 42;",
451 );
452 const serverEntry = path.join(dist, "serve.mjs");
453 yield* fs.writeFileString(
454 serverEntry,
455 'import sharp from "sharp"; export default { fetch: () => new Response(String(sharp)) };',
456 );
457 const requested: string[] = [];
458 const fetch = ((input: string | URL | Request) =>
459 Effect.runPromise(
460 Effect.sync(() => {
461 const url = String(input);
462 requested.push(url);
463 const name = url.includes("libvips")
464 ? "@img/sharp-libvips-linux-arm64"
465 : "@img/sharp-linux-arm64";
466 const version = name.includes("libvips") ? "1.2.4" : "0.34.5";
467 const manifest = {
468 name,
469 version,
470 os: ["linux"],
471 cpu: ["arm64"],
472 libc: [scenario === "musl" ? "musl" : "glibc"],
473 };
474 const binary = name.includes("libvips")
475 ? "lib/libvips.so.42"
476 : "lib/sharp-linux-arm64.node";
477 const archive = tarball([
478 ["package/package.json", Buffer.from(JSON.stringify(manifest))],
479 [
480 scenario === "traversal"
481 ? "package/../escape"
482 : `package/${binary}`,
483 elf(scenario === "architecture" ? 62 : 183),
484 scenario === "symlink" ? "2" : "0",
485 ],
486 ]);
487 if (url.endsWith(".tgz")) return new Response(archive);
488 return Response.json({
489 ...manifest,
490 version: scenario === "identity" ? "99.0.0" : version,
491 dist: {
492 integrity: `sha512-${createHash("sha512")
493 .update(scenario === "integrity" ? "wrong" : archive)
494 .digest("base64")}`,
495 tarball: `${scenario === "origin" ? "https://untrusted.invalid" : "https://registry.npmjs.org"}/${name}/-/${name.split("/")[1]}-${version}.tgz`,
496 },
497 });
498 }),
499 )) as typeof globalThis.fetch;
500 const result = yield* stageWebsiteArtifact({
501 root,
502 distDir: dist,
503 serverEntry,
504 }).pipe(
505 Effect.provideService(FetchHttpClient.Fetch, fetch),
506 Effect.result,
507 );
508 expect(Result.isSuccess(result)).toBe(scenario === "valid");
509 expect(requested.length).toBeGreaterThan(0);
510 if (Result.isFailure(result))
511 expect(result.failure._tag).toBe("WebsiteArtifactError");
512 if (Result.isSuccess(result)) {
513 const packageFile = path.join(
514 result.success.directory,
515 "files/node_modules/@img/sharp-linux-arm64/package.json",
516 );
517 expect(
518 JSON.parse(yield* fs.readFileString(packageFile)).version,
519 ).toBe("0.34.5");
520 expect(
521 JSON.parse(
522 yield* fs.readFileString(path.join(sharp, "package.json")),
523 ).version,
524 ).toBe("0.34.5");
525 }
526 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
527 { tags: ["unit", "provider:neon", "provider:neon:website", "local"] },
528 );