diff --git a/apps/server/src/cloud/pinnedRuntime.test.ts b/apps/server/src/cloud/pinnedRuntime.test.ts index a0ca9e5f0fa0..4755b05d5830 100644 --- a/apps/server/src/cloud/pinnedRuntime.test.ts +++ b/apps/server/src/cloud/pinnedRuntime.test.ts @@ -228,6 +228,67 @@ it.layer(NodeServices.layer)("ensurePinnedRuntimeInstalled", (it) => { }), ); + it.effect("drops every spelling of the inherited allow-scripts policy from the installer", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-pinned-runtime-env-" }); + // npx exports the lowercase name and npm reads the uppercase one just the + // same. Unrelated npm configuration must still reach the child. + const stubbed = { + npm_config_allow_scripts: "true", + NPM_CONFIG_ALLOW_SCRIPTS: "true", + npm_config_registry: "https://registry.example.test/", + }; + const previous = Object.fromEntries( + Object.keys(stubbed).map((key) => [key, process.env[key]]), + ); + Object.assign(process.env, stubbed); + yield* Effect.addFinalizer(() => + Effect.sync(() => { + for (const [key, value] of Object.entries(previous)) { + if (value === undefined) delete process.env[key]; + else process.env[key] = value; + } + }), + ); + + // Spawn a real child with exactly what the installer hands npm, so the + // assertion covers the environment the child sees rather than the input. + const real = yield* Effect.service(ProcessRunner.ProcessRunner).pipe( + Effect.provide(ProcessRunner.layer), + ); + const inner = successfulRunner(fs, path); + let childEnv: Record | undefined; + const runner = ProcessRunner.ProcessRunner.of({ + run: (input) => + Effect.gen(function* () { + const probe = yield* real.run({ + ...input, + command: process.execPath, + args: ["-e", "process.stdout.write(JSON.stringify(process.env))"], + }); + childEnv = JSON.parse(probe.stdout) as Record; + return yield* inner.run(input); + }), + }); + + yield* ensurePinnedRuntimeInstalled({ + baseDir, + version: "1.2.3", + fs, + path, + runner, + validate: () => Effect.void, + }); + + assert.isDefined(childEnv); + assert.notProperty(childEnv, "npm_config_allow_scripts"); + assert.notProperty(childEnv, "NPM_CONFIG_ALLOW_SCRIPTS"); + assert.equal(childEnv.npm_config_registry, "https://registry.example.test/"); + }), + ); + it.effect("removes staging when installation is interrupted", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; diff --git a/apps/server/src/cloud/pinnedRuntime.ts b/apps/server/src/cloud/pinnedRuntime.ts index 534ed917218f..4be012164d49 100644 --- a/apps/server/src/cloud/pinnedRuntime.ts +++ b/apps/server/src/cloud/pinnedRuntime.ts @@ -6,6 +6,7 @@ import * as PlatformError from "effect/PlatformError"; import * as Schema from "effect/Schema"; import * as Option from "effect/Option"; import * as Semaphore from "effect/Semaphore"; +import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; import * as ProcessRunner from "../processRunner.ts"; @@ -161,10 +162,22 @@ const installPinnedRuntime = Effect.fn("cloud.pinned_runtime.ensure_installed")( "--no-audit", `t3@${input.version}`, ]; + // npx exports its resolved allow-scripts policy as npm_config_allow_scripts, + // which npm 12 reads as a project-scoped --allow-scripts and refuses with + // EALLOWSCRIPTS. npm matches config variable names case-insensitively, so + // unset every spelling the child would inherit (Node drops undefined + // entries) and leave the rest of the npm configuration alone. + const hostEnvironment = yield* HostProcessEnvironment; + const installEnv = Object.fromEntries( + Object.keys(hostEnvironment) + .filter((key) => /^npm_config_allow[-_]scripts$/i.test(key)) + .map((key) => [key, undefined]), + ); yield* runner .run({ command: "npm", args: installArgs, + env: installEnv, // Native dependencies may compile from source on slower machines. timeout: PINNED_RUNTIME_INSTALL_TIMEOUT, }) @@ -178,6 +191,7 @@ const installPinnedRuntime = Effect.fn("cloud.pinned_runtime.ensure_installed")( runner.run({ command: "pnpm", args: ["--package=npm@11", "dlx", "npm", ...installArgs], + env: installEnv, timeout: PINNED_RUNTIME_INSTALL_TIMEOUT, }) : Effect.fail(error),