Uh oh!
There was an error while loading. Please reload this page.
[APPS-2792] Add: reject Node built-in imports in backend files - #476
[APPS-2792] Add: reject Node built-in imports in backend files#476tyffical wants to merge 4 commits into
Conversation
84b9e52 to
ec0f520CompareBackend 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.
Backend functions have no raw network access in every real production runtime -- Deno's --allow-net is off today, and the planned Terrapin-based v2 sandbox restricts it the same way -- so any outbound call must go through an Action Platform action ($.Actions or an @datadog/action-catalog typed wrapper), never a direct HTTP client. rejectNodeBuiltinImports only catches import specifiers; fetch and friends need no import at all, so this adds a separate, eslint-scope-based check for unshadowed references to fetch, XMLHttpRequest, WebSocket, and EventSource. Also corrects rejectNodeBuiltinImports' doc comment and error message, which previously pointed to fetch-based/isomorphic APIs as the allowed escape hatch -- no longer accurate now that fetch itself is blocked too.
ec0f520 to
e17c9c8Comparetyffical
commented
Aug 21, 2026
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:e17c9c8754
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Pull request overview
Friend, this PR adds build-time restrictions for unsupported Node built-ins and network globals in backend functions.
Changes:
- Adds AST validation for Node built-in imports and restricted globals.
- Integrates validation into the Vite backend transform.
- Adds unit and transform-level tests.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
packages/plugins/apps/src/vite/index.ts | Runs backend restrictions during transformation. |
packages/plugins/apps/src/vite/index.test.ts | Tests transform-level built-in rejection. |
packages/plugins/apps/src/backend/ast-parsing/reject-restricted-globals.ts | Detects unresolved restricted globals. |
packages/plugins/apps/src/backend/ast-parsing/reject-restricted-globals.test.ts | Tests global detection and shadowing. |
packages/plugins/apps/src/backend/ast-parsing/reject-node-builtin-imports.ts | Detects Node built-in imports. |
packages/plugins/apps/src/backend/ast-parsing/reject-node-builtin-imports.test.ts | Tests import restrictions and exceptions. |
Suppressed comments (2)
packages/plugins/apps/src/backend/ast-parsing/reject-node-builtin-imports.ts:40
- The suggested remedy is inaccurate for non-privileged built-ins such as
path,util, orevents: an Action Platform action is not a replacement for those APIs. Mention standard JavaScript or a runtime-neutral package for portable functionality, reserving the Action Platform guidance for privileged operations, so the error remains actionable for every module this guard rejects.
`Backend functions run in a restricted environment and must use an Action ` +
`Platform action ($.Actions or an @datadog/action-catalog typed wrapper) instead: ${filePath}`,
packages/plugins/apps/src/backend/ast-parsing/reject-node-builtin-imports.test.ts:84
- Repository guidance disallows passing a function call directly into another call. Store the import declaration first so this test follows that rule.
const ast = program([importDecl(source)]);
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
…red global bypasses
reject-node-builtin-imports only walked ImportDeclarations, so a named
re-export like `export { readFile as handler } from 'node:fs'` loaded the
same builtin without ever binding a local identifier, sailing through
undetected. Now also walks ExportNamedDeclaration nodes with a source,
reusing the same restricted-source check.
reject-restricted-globals only caught bare identifier references resolved
via eslint-scope, missing globalThis.fetch(...)/globalThis['fetch'] (ESTree
represents the property as a member access, not a reference eslint-scope
tracks) and const { fetch } = globalThis (binds a local that then shadows
the scope-based check entirely). Added a dedicated AST walk for both forms.
Also replaced an `as` cast in a test with the codebase's existing
intersection-type pattern for ESTree's incomplete parser-metadata types, and
named two inlined function-call results before passing them to another call,
per this repo's no-inlined-call-arguments convention.…ule imports reject-node-builtin-imports and reject-restricted-globals only ran against the entry .backend.ts file matched by the outer Vite transform filter. A backend function importing a local app helper module is a supported flow, but the helper's own source was never scanned by either check — only the nested backend build (which bundles a single function's own module graph) actually walks into it, so a helper doing `import fs from 'fs'` or calling a bare `fetch()` shipped undetected even though the entry file itself was clean. Adds a Vite plugin that re-runs both checks against every app-local module the nested backend build resolves, via the same moduleParsed hook and module-id normalization/exclusion the connection-ID collector already uses for the same module set. Wired into both the production build-backend-functions path and the dev bundleBackendFunction path, matching how the connection-ID collector itself is wired into both.
tyffical
commented
Aug 21, 2026
@codex review |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
packages/plugins/apps/src/backend/ast-parsing/reject-node-builtin-imports.ts:33
- A literal dynamic import still bypasses this check:
await import('node:fs')is represented as anImportExpression(or Rollup's importCallExpression), while this loop only visits top-level declarations. Because the specifier is static, this can be rejected at build time just like anImportDeclaration; otherwise the built-in reaches the backend bundle and fails only at runtime. Walk both dynamic-import AST representations and reject literal restricted sources, while retaining the documented exemption for computed specifiers.
for (const node of program.body) {
if (node.type === 'ImportDeclaration' && !isTypeOnly(node)) {
rejectIfRestrictedSource(node.source, filePath);
continue;
| function restrictedGlobalThisMemberName(node: MemberExpression): string | undefined { | ||
| if (node.object.type !== 'Identifier' || node.object.name !== GLOBAL_THIS_NAME) { | ||
| return undefined; |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:b32490a571
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (node.type === 'ExportNamedDeclaration' && node.source && !isTypeOnly(node)) { | ||
| rejectIfRestrictedSource(node.source, filePath); |
There was a problem hiding this comment.
Reject built-ins re-exported with export star
When a reachable helper contains export * from 'node:fs' or export * as fs from 'node:fs', Rollup represents it as an ExportAllDeclaration, so this named-export-only branch never validates the source and the restricted built-in can still enter the backend bundle. Handle non-type ExportAllDeclaration sources alongside named re-exports.
Useful? React with 👍 / 👎.
| if (node.object.type !== 'Identifier' || node.object.name !== GLOBAL_THIS_NAME) { | ||
| return undefined; |
There was a problem hiding this comment.
Respect locally shadowed globalThis bindings
When backend code shadows globalThis, such as function run(globalThis) { return globalThis.fetch(); }, this name-only test rejects the local object's method even though no ambient network global is accessed. Resolve the object identifier through the existing scope analysis and reject it only when it is the unresolved ambient globalThis; the destructuring check needs the same distinction.
Useful? React with 👍 / 👎.
| if ( | ||
| property.type === 'RestElement' || | ||
| property.computed || | ||
| property.key.type !== 'Identifier' | ||
| ) { |
There was a problem hiding this comment.
Inspect computed globalThis destructuring keys
When code uses const { ['fetch']: request } = globalThis, this condition skips the computed property and the later request reference resolves locally, so neither validation rejects the raw network access. The fresh evidence after the prior fix is the newly added property.computed skip here; inspect computed string-literal keys just as the member-expression path does.
Useful? React with 👍 / 👎.
Motivation
fetch); everything must go through an Action Platform action ($.Actionsor an@datadog/action-catalogtyped wrapper).fs,child_process,net, etc.) in.backend.tsfiles 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.fetch,XMLHttpRequest,WebSocket,EventSource) need a separate check: they're bare globals, not imports, so import-specifier restriction can't catch them. This closes a real trap:fetchworks fine during local dev (nothing stopped it before this check existed) but fails once the app is published, since production's sandbox blocks it.fetchin the first place. That reduces how often this gets written at all, but only this build-time check guarantees it never ships, regardless of whether the code came from an AI, a human, or a copy-pasted snippet. Both layers exist for a reason — this PR isn't superseded by that guidance work.$that omit Node-specific types) is deferred — see Out of Scope below.Changes
rejectNodeBuiltinImports, which walks a.backend.tsfile's staticImportDeclarations and throws if any source is a Node built-in (vianode:prefix or Node's ownbuiltinModuleslist).rejectRestrictedGlobals, an eslint-scope-based check that throws on any unshadowed reference tofetch/XMLHttpRequest/WebSocket/EventSource— i.e. any reference that doesn't resolve to a local declaration or import sharing the same name, meaning it falls through to the real ambient global.rejectNodeBuiltinImports' doc comment and error message, which previously pointed to fetch-based/isomorphic APIs as the allowed escape hatch — no longer accurate now thatfetchitself is blocked too.this.parse(code)and before export extraction.node:fs, barefs,child_process,net,fs/promises), and edge cases (type-only imports, non-import statements).fetch()calls, referencingfetchwithout calling it,new XMLHttpRequest()/WebSocket()/EventSource()), and allowed cases (an imported action-catalog function, a locally-declared function or parameter that happens to be namedfetch— shadowing-safe)..backend.tsfile with anode:fsimport through the actual transform handler (using rollup's realparseAst, not a hand-built AST) to confirm the rejection fires through the genuine pipeline.QA Instructions
Build the plugin and link it into a scratch Vite project, then confirm a backend file importing a Node built-in — or referencing
fetch— is rejected while an ordinary backend file still transforms correctly.Blast Radius
.backend.tsfiles' static imports and top-level global references only. No feature flag — this is a build-time compile error for a pattern (Node built-ins, or raw network globals) that wasn't previously usable in production anyway, since production's real sandbox already blocks both.importspecifiers (notrequire()or a dynamically computedimport()); the global-reference check only catches references eslint-scope can't resolve to a local declaration/import of the same name..backend.tsfile that doesn't import a Node built-in or reference one of the four restricted globals directly (306/306 existing apps-plugin tests pass unchanged).Out of Scope / Follow-ups
backend-function-globals.d.ts(ambient TypeScript type for$that omitsDeno/process/Node-builtin globals).d.tsinto the publisheddist/tarball requires new build-tooling wiring inpackages/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'stsconfig.json.fetch; not blocking today's v1 rolloutDocumentation
.plans/high-code-apps-local-node-execution-design.md(dd-source repo)