Skip to content

[APPS-2792] Add: reject Node built-in imports in backend files - #476

Draft
tyffical wants to merge 1 commit into
masterfrom
tiffany.trinh/apps-2792-sandboxing-import-restriction
Draft

[APPS-2792] Add: reject Node built-in imports in backend files#476
tyffical wants to merge 1 commit into
masterfrom
tiffany.trinh/apps-2792-sandboxing-import-restriction

Conversation

@tyffical

@tyffical tyffical commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Motivation

  • Part of APPS-2792 — local Node execution for App Builder backend functions. Backend functions will run in a restricted, isomorphic environment (fetch-based APIs only), not a full Node runtime with unrestricted filesystem/process/network access.
  • Static imports of Node built-in modules (fs, child_process, net, etc.) in .backend.ts files are rejected at build time, so an author gets immediate, actionable feedback instead of code that silently behaves differently (or breaks) once local Node execution lands.
  • This is one of two "Layer 2" static defenses proposed in the design doc's Sandboxing section; the companion item (ambient TypeScript globals for $ that omit Node-specific types) is deferred — see Out of Scope below.

Changes

What changed File
Added rejectNodeBuiltinImports, which walks a .backend.ts file's static ImportDeclarations and throws if any source is a Node built-in (via node: prefix or Node's own builtinModules list). reject-node-builtin-imports.ts
Wired the new check into the Vite transform hook, right after this.parse(code) and before export extraction. vite/index.ts
Added unit tests covering allowed imports (relative, scoped, ordinary npm packages), rejected imports (node:fs, bare fs, child_process, net, fs/promises), and edge cases (type-only imports, non-import statements). reject-node-builtin-imports.test.ts
Added an end-to-end test that runs a real .backend.ts file with a node:fs import through the actual transform handler (using rollup's real parseAst, not a hand-built AST) to confirm the rejection fires through the genuine pipeline. vite/index.test.ts

QA Instructions

Build the plugin and link it into a scratch Vite project, then confirm a backend file importing a Node built-in is rejected while an ordinary backend file still transforms correctly.

# 1. Build and link the plugin from this branch
cd ~/dd/build-plugins/packages/published/vite-plugin
yarn build
npm link

# 2. Scaffold a throwaway consumer project
mkdir -p ~/import-restriction-qa/src && cd ~/import-restriction-qa
cat > package.json <<'EOF'
{ "name": "import-restriction-qa", "private": true, "type": "module", "devDependencies": { "vite": "^5.0.0" } }
EOF
cat > vite.config.ts <<'EOF'
import { datadogVitePlugin } from '@datadog/vite-plugin/dist/src';
import { defineConfig } from 'vite';
export default defineConfig({
    plugins: [datadogVitePlugin({ apps: { identifier: 'qa-app-id', name: 'import-restriction-qa', dryRun: true } })],
});
EOF
cat > src/badImport.backend.ts <<'EOF'
import fs from 'node:fs';
export function readSecret() { return fs.readFileSync('/etc/passwd', 'utf8'); }
EOF
cat > src/goodImport.backend.ts <<'EOF'
export function doubleNumber(input: number) { return input * 2; }
EOF
npm install && npm link @datadog/vite-plugin

# 3. Confirm the bad import is rejected with a clear error
npx vite --port 5199 --strictPort &
sleep 3
curl -s http://localhost:5199/src/badImport.backend.ts | grep -o 'Importing Node built-in module.*not supported in .backend.ts files'
# Expected: Importing Node built-in module "node:fs" is not supported in .backend.ts files ✅ VERIFIED
kill %1

# 4. Confirm an ordinary backend file still transforms into a working proxy
npx vite --port 5198 --strictPort &
sleep 3
curl -s http://localhost:5198/src/goodImport.backend.ts
# Expected: export async function doubleNumber(...args) { return globalThis.DD_APPS_RUNTIME.executeBackendFunction(...); } ✅ VERIFIED
kill %1
# Automated pass
yarn test:unit packages/plugins/apps
# Expected: Test Suites: 23 passed, 23 total / Tests: 297 passed, 297 total ✅ VERIFIED
yarn workspace @dd/apps-plugin run typecheck
# Expected: no output, exit 0 ✅ VERIFIED

Blast Radius

  • Scoped to .backend.ts files' static imports only. No feature flag — this is a build-time compile error for a pattern (importing Node built-ins in a restricted-runtime file) that wasn't previously usable in practice anyway, since there's no Node/Deno runtime backing .backend.ts execution yet.
  • Best-effort, defense-in-depth: it only catches static import specifiers, not require() or a dynamically computed import().
  • Risk: low. No behavioral change for any existing .backend.ts file that doesn't import a Node built-in directly (297/297 existing apps-plugin tests pass unchanged).

Out of Scope / Follow-ups

Item Status Next step
Ship backend-function-globals.d.ts (ambient TypeScript type for $ that omits Deno/process/Node-builtin globals) Deferred Editor-only DX polish, not an enforced guarantee — Task #5 (this PR) already enforces the restriction regardless of what types an author's editor shows. Getting a hand-written .d.ts into the published dist/ tarball requires new build-tooling wiring in packages/tools/src/rollupConfig.mjs (shared by all 5 published bundler plugins), which is disproportionate scope for this PR. Revisit once a scaffold tool exists to actually wire the type into a consumer's tsconfig.json.

Documentation

Backend functions run in a restricted environment (isomorphic/fetch-based
APIs only), so direct static imports of Node built-in modules (fs,
child_process, net, etc.) in .backend.ts files are now rejected at build
time in the Vite transform hook, right after AST parsing.

This is a best-effort, defense-in-depth check on static import specifiers
only — it does not catch require() or dynamic import() of a computed
specifier.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant