From 5f414437f1baf48866be0ddf8e6c0bd955da50d5 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Thu, 15 Jan 2026 09:27:54 +0000 Subject: [PATCH 1/3] chore: bump Node to v20.20 and add maxDepth guard to attribute flattener https://nodejs.org/en/blog/vulnerability/january-2026-dos-mitigation-async-hooks Update CI/node configs to Node 20.20 and align container base images. - Bump node versions across GitHub Actions workflows, .nvmrc files, Dockerfile ARGs, and Containerfile to 20.20 / matching runtime tags. This ensures CI and runtime environments use the newer Node release. Add depth-limiting and safety checks to attribute flattening logic. - Introduce DEFAULT_MAX_DEPTH and accept an optional maxDepth in flattenAttributes/unflattenAttributes APIs. - Pass depth through recursive calls and stop recursion when depth exceeds maxDepth to prevent stack overflows and memory exhaustion. - Early-return when attribute count limit is reached to avoid wasted work. - Update tests to cover deep nesting (objects and arrays), maxDepth enforcement, and default maxDepth behavior. Fix tests and Containerfile hashing to match updated Node images. --- .github/workflows/changesets-pr.yml | 4 +- .github/workflows/claude.yml | 2 +- .github/workflows/e2e.yml | 2 +- .github/workflows/release.yml | 4 +- .github/workflows/typecheck.yml | 2 +- .github/workflows/unit-tests-internal.yml | 4 +- .github/workflows/unit-tests-packages.yml | 4 +- .github/workflows/unit-tests-webapp.yml | 4 +- .nvmrc | 2 +- apps/supervisor/.nvmrc | 2 +- apps/supervisor/Containerfile | 2 +- docker/Dockerfile | 2 +- .../core/src/v3/utils/flattenAttributes.ts | 43 +++++++--- packages/core/test/flattenAttributes.test.ts | 86 +++++++++++++++++++ references/prisma-7/.nvmrc | 2 +- 15 files changed, 134 insertions(+), 31 deletions(-) diff --git a/.github/workflows/changesets-pr.yml b/.github/workflows/changesets-pr.yml index ec21972361..e2fdc18761 100644 --- a/.github/workflows/changesets-pr.yml +++ b/.github/workflows/changesets-pr.yml @@ -34,7 +34,7 @@ jobs: - name: Setup node uses: buildjet/setup-node@v4 with: - node-version: 20.19.0 + node-version: 20.20.0 cache: "pnpm" - name: Install dependencies @@ -83,7 +83,7 @@ jobs: - name: Setup node uses: buildjet/setup-node@v4 with: - node-version: 20.19.0 + node-version: 20.20.0 - name: Install and update lockfile run: pnpm install --no-frozen-lockfile diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 4a8bd858b0..cadbe31773 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -38,7 +38,7 @@ jobs: - name: ⎔ Setup node uses: buildjet/setup-node@v4 with: - node-version: 20.19.0 + node-version: 20.20.0 cache: "pnpm" - name: 📥 Download deps diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 97170c2225..9518ca6157 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -36,7 +36,7 @@ jobs: - name: ⎔ Setup node uses: buildjet/setup-node@v4 with: - node-version: 20.19.0 + node-version: 20.20.0 - name: 📥 Download deps run: pnpm install --frozen-lockfile --filter trigger.dev... diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 684d36dec3..ca0f0ebf16 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -86,7 +86,7 @@ jobs: - name: Setup node uses: buildjet/setup-node@v4 with: - node-version: 20.19.0 + node-version: 20.20.0 cache: "pnpm" # npm v11.5.1 or newer is required for OIDC support @@ -154,7 +154,7 @@ jobs: - name: Setup node uses: buildjet/setup-node@v4 with: - node-version: 20.19.0 + node-version: 20.20.0 cache: "pnpm" # npm v11.5.1 or newer is required for OIDC support diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index 3eb98e5177..665d54b256 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -24,7 +24,7 @@ jobs: - name: ⎔ Setup node uses: buildjet/setup-node@v4 with: - node-version: 20.19.0 + node-version: 20.20.0 cache: "pnpm" - name: 📥 Download deps diff --git a/.github/workflows/unit-tests-internal.yml b/.github/workflows/unit-tests-internal.yml index 0b9d0959bc..92b951e8aa 100644 --- a/.github/workflows/unit-tests-internal.yml +++ b/.github/workflows/unit-tests-internal.yml @@ -58,7 +58,7 @@ jobs: - name: ⎔ Setup node uses: buildjet/setup-node@v4 with: - node-version: 20.19.0 + node-version: 20.20.0 cache: "pnpm" # ..to avoid rate limits when pulling images @@ -127,7 +127,7 @@ jobs: - name: ⎔ Setup node uses: buildjet/setup-node@v4 with: - node-version: 20.19.0 + node-version: 20.20.0 # no cache enabled, we're not installing deps - name: Download blob reports from GitHub Actions Artifacts diff --git a/.github/workflows/unit-tests-packages.yml b/.github/workflows/unit-tests-packages.yml index d17a828d61..78474e03f2 100644 --- a/.github/workflows/unit-tests-packages.yml +++ b/.github/workflows/unit-tests-packages.yml @@ -58,7 +58,7 @@ jobs: - name: ⎔ Setup node uses: buildjet/setup-node@v4 with: - node-version: 20.19.0 + node-version: 20.20.0 cache: "pnpm" # ..to avoid rate limits when pulling images @@ -127,7 +127,7 @@ jobs: - name: ⎔ Setup node uses: buildjet/setup-node@v4 with: - node-version: 20.19.0 + node-version: 20.20.0 # no cache enabled, we're not installing deps - name: Download blob reports from GitHub Actions Artifacts diff --git a/.github/workflows/unit-tests-webapp.yml b/.github/workflows/unit-tests-webapp.yml index 0795a3e0fc..523a1887db 100644 --- a/.github/workflows/unit-tests-webapp.yml +++ b/.github/workflows/unit-tests-webapp.yml @@ -58,7 +58,7 @@ jobs: - name: ⎔ Setup node uses: buildjet/setup-node@v4 with: - node-version: 20.19.0 + node-version: 20.20.0 cache: "pnpm" # ..to avoid rate limits when pulling images @@ -135,7 +135,7 @@ jobs: - name: ⎔ Setup node uses: buildjet/setup-node@v4 with: - node-version: 20.19.0 + node-version: 20.20.0 # no cache enabled, we're not installing deps - name: Download blob reports from GitHub Actions Artifacts diff --git a/.nvmrc b/.nvmrc index 3bf34c2761..7c663e0a0b 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v20.19.0 \ No newline at end of file +v20.20.0 \ No newline at end of file diff --git a/apps/supervisor/.nvmrc b/apps/supervisor/.nvmrc index dc0bb0f439..42a1c98ac5 100644 --- a/apps/supervisor/.nvmrc +++ b/apps/supervisor/.nvmrc @@ -1 +1 @@ -v22.12.0 +v22.22.0 diff --git a/apps/supervisor/Containerfile b/apps/supervisor/Containerfile index d5bb5862e9..58dd1ceb40 100644 --- a/apps/supervisor/Containerfile +++ b/apps/supervisor/Containerfile @@ -1,4 +1,4 @@ -FROM node:22-alpine@sha256:9bef0ef1e268f60627da9ba7d7605e8831d5b56ad07487d24d1aa386336d1944 AS node-22-alpine +FROM node:22.22.0-alpine@sha256:bcccf7410b77ca7447d292f616c7b0a89deff87e335fe91352ea04ce8babf50f AS node-22-alpine WORKDIR /app diff --git a/docker/Dockerfile b/docker/Dockerfile index 57cd1412ba..60ed0809b6 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -ARG NODE_IMAGE=node:20.11.1-bullseye-slim@sha256:5a5a92b3a8d392691c983719dbdc65d9f30085d6dcd65376e7a32e6fe9bf4cbe +ARG NODE_IMAGE=node:20.20.0-bullseye-slim@sha256:f52726bba3d47831859be141b4a57d3f7b93323f8fddfbd8375386e2c3b72319 FROM golang:1.23-alpine AS goose_builder RUN go install github.com/pressly/goose/v3/cmd/goose@latest diff --git a/packages/core/src/v3/utils/flattenAttributes.ts b/packages/core/src/v3/utils/flattenAttributes.ts index 568346d48a..83d1a14f2c 100644 --- a/packages/core/src/v3/utils/flattenAttributes.ts +++ b/packages/core/src/v3/utils/flattenAttributes.ts @@ -3,13 +3,16 @@ import { Attributes } from "@opentelemetry/api"; export const NULL_SENTINEL = "$@null(("; export const CIRCULAR_REFERENCE_SENTINEL = "$@circular(("; +const DEFAULT_MAX_DEPTH = 128; + export function flattenAttributes( obj: unknown, prefix?: string, - maxAttributeCount?: number + maxAttributeCount?: number, + maxDepth: number = DEFAULT_MAX_DEPTH ): Attributes { - const flattener = new AttributeFlattener(maxAttributeCount); - flattener.doFlatten(obj, prefix); + const flattener = new AttributeFlattener(maxAttributeCount, maxDepth); + flattener.doFlatten(obj, prefix, 0); return flattener.attributes; } @@ -18,7 +21,10 @@ class AttributeFlattener { private attributeCounter: number = 0; private result: Attributes = {}; - constructor(private maxAttributeCount?: number) {} + constructor( + private maxAttributeCount?: number, + private maxDepth: number = DEFAULT_MAX_DEPTH + ) {} get attributes(): Attributes { return this.result; @@ -37,11 +43,16 @@ class AttributeFlattener { return true; } - doFlatten(obj: unknown, prefix?: string) { + doFlatten(obj: unknown, prefix?: string, depth: number = 0) { if (!this.canAddMoreAttributes()) { return; } + // Check depth limit to prevent stack overflow + if (depth > this.maxDepth) { + return; + } + // Check if obj is null or undefined if (obj === undefined) { return; @@ -94,7 +105,7 @@ class AttributeFlattener { let index = 0; for (const item of obj) { if (!this.canAddMoreAttributes()) break; - this.#processValue(item, `${prefix || "set"}.[${index}]`); + this.#processValue(item, `${prefix || "set"}.[${index}]`, depth); index++; } return; @@ -106,7 +117,7 @@ class AttributeFlattener { if (!this.canAddMoreAttributes()) break; // Use the key directly if it's a string, otherwise convert it const keyStr = typeof key === "string" ? key : String(key); - this.#processValue(value, `${prefix || "map"}.${keyStr}`); + this.#processValue(value, `${prefix || "map"}.${keyStr}`, depth); } return; } @@ -196,15 +207,15 @@ class AttributeFlattener { if (!this.canAddMoreAttributes()) { break; } - this.#processValue(value[i], `${newPrefix}.[${i}]`); + this.#processValue(value[i], `${newPrefix}.[${i}]`, depth); } } else { - this.#processValue(value, newPrefix); + this.#processValue(value, newPrefix, depth); } } } - #processValue(value: unknown, prefix: string) { + #processValue(value: unknown, prefix: string, depth: number) { if (!this.canAddMoreAttributes()) { return; } @@ -224,9 +235,9 @@ class AttributeFlattener { return; } - // Handle non-primitive values by recursing + // Handle non-primitive values by recursing (increment depth) if (typeof value === "object" || typeof value === "function") { - this.doFlatten(value as any, prefix); + this.doFlatten(value as any, prefix, depth + 1); } else { // Convert other types to strings (bigint, symbol, etc.) this.addAttribute(prefix, String(value)); @@ -240,7 +251,8 @@ function isRecord(value: unknown): value is Record { export function unflattenAttributes( obj: Attributes, - filteredKeys?: string[] + filteredKeys?: string[], + maxDepth: number = DEFAULT_MAX_DEPTH ): Record | string | number | boolean | null | undefined { if (typeof obj !== "object" || obj === null || Array.isArray(obj)) { return obj; @@ -285,6 +297,11 @@ export function unflattenAttributes( [] as (string | number)[] ); + // Skip keys that exceed max depth to prevent memory exhaustion + if (parts.length > maxDepth) { + continue; + } + let current: any = result; for (let i = 0; i < parts.length - 1; i++) { const part = parts[i]; diff --git a/packages/core/test/flattenAttributes.test.ts b/packages/core/test/flattenAttributes.test.ts index 8970ae6a7d..28f137deaf 100644 --- a/packages/core/test/flattenAttributes.test.ts +++ b/packages/core/test/flattenAttributes.test.ts @@ -500,6 +500,53 @@ describe("flattenAttributes", () => { expect(result["bigint"]).toBe("999"); expect(typeof result["symbol"]).toBe("string"); }); + + it("respects maxDepth limit", () => { + const obj = { + a: { + b: { + c: { + d: { + e: { + f: "deep", + }, + }, + }, + }, + }, + }; + + // With maxDepth of 3, should not include keys deeper than 3 levels + const result = flattenAttributes(obj, undefined, undefined, 3); + + // a.b.c should exist (depth 3) + expect(result["a.b.c"]).toBeUndefined(); // c is an object, not a leaf + // a.b.c.d should not exist (would require going to depth 4) + expect(result["a.b.c.d"]).toBeUndefined(); + expect(result["a.b.c.d.e.f"]).toBeUndefined(); + }); + + it("does not crash with deeply nested objects", () => { + // Create object iteratively with 500 levels of nesting + let deepObj: any = { value: "leaf" }; + for (let i = 0; i < 500; i++) { + deepObj = { n: deepObj }; + } + + // Should complete without stack overflow + expect(() => flattenAttributes(deepObj)).not.toThrow(); + }); + + it("does not crash with deeply nested arrays", () => { + // Create deeply nested array structure iteratively + let deepArray: any = ["leaf"]; + for (let i = 0; i < 500; i++) { + deepArray = [deepArray]; + } + + // Should complete without stack overflow + expect(() => flattenAttributes({ arr: deepArray })).not.toThrow(); + }); }); describe("unflattenAttributes", () => { @@ -581,4 +628,43 @@ describe("unflattenAttributes", () => { blogPosts: [{ title: "Post 1", author: "[Circular Reference]" }], }); }); + + it("respects maxDepth limit and skips overly deep keys", () => { + // Create a flattened object with keys at various depths + const flattened = { + "a.b.c": "shallow", // depth 3 - should be included + "a.b.c.d.e.f.g": "deep", // depth 7 - should be skipped with maxDepth=5 + "x.y": "also shallow", // depth 2 - should be included + }; + + const result = unflattenAttributes(flattened, undefined, 5); + + // Shallow keys should be included + expect(result).toHaveProperty("a.b.c", "shallow"); + expect(result).toHaveProperty("x.y", "also shallow"); + + // Deep key should be skipped (not create the nested structure) + expect((result as any)?.a?.b?.c?.d?.e?.f?.g).toBeUndefined(); + }); + + it("uses default maxDepth of 128", () => { + // Create a key with 129 parts - should be skipped + const deepKey = Array(129).fill("x").join("."); + const flattened = { + [deepKey]: "too deep", + "a.b": "shallow", + }; + + const result = unflattenAttributes(flattened); + + // Shallow key should work + expect(result).toHaveProperty("a.b", "shallow"); + + // Deep key should be skipped + let current: any = result; + for (let i = 0; i < 129 && current; i++) { + current = current?.x; + } + expect(current).toBeUndefined(); + }); }); diff --git a/references/prisma-7/.nvmrc b/references/prisma-7/.nvmrc index 829e9737e4..76be14da5c 100644 --- a/references/prisma-7/.nvmrc +++ b/references/prisma-7/.nvmrc @@ -1 +1 @@ -20.19.0 \ No newline at end of file +20.20.0 \ No newline at end of file From 9e4456ecdaa3ab89f60f8a228663bedf0d77d754 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Thu, 15 Jan 2026 09:34:59 +0000 Subject: [PATCH 2/3] add changeset --- .changeset/thirty-trainers-reflect.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/thirty-trainers-reflect.md diff --git a/.changeset/thirty-trainers-reflect.md b/.changeset/thirty-trainers-reflect.md new file mode 100644 index 0000000000..681f87acd8 --- /dev/null +++ b/.changeset/thirty-trainers-reflect.md @@ -0,0 +1,5 @@ +--- +"@trigger.dev/core": patch +--- + +Add a maxDepth to flatten/unflattenAttributes to prevent possible issues From 38956fbbf474c8600d9510ef6dcaff70e20b5262 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Thu, 15 Jan 2026 10:25:06 +0000 Subject: [PATCH 3/3] Update AGENTS and CONTRIBUTING for the new repo node version --- AGENTS.md | 2 +- CONTRIBUTING.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 846c6d827c..99496f91bd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,7 +13,7 @@ This repository is a pnpm monorepo managed with Turbo. It contains multiple apps See `ai/references/repo.md` for a more complete explanation of the workspaces. ## Development setup -1. Install dependencies with `pnpm i` (pnpm `10.23.0` and Node.js `20.11.1` are required). +1. Install dependencies with `pnpm i` (pnpm `10.23.0` and Node.js `20.20.0` are required). 2. Copy `.env.example` to `.env` and generate a random 16 byte hex string for `ENCRYPTION_KEY` (`openssl rand -hex 16`). Update other secrets if needed. 3. Start the local services with Docker: ```bash diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5924f89da3..0ade530500 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,7 @@ branch are tagged into a release periodically. ### Prerequisites -- [Node.js](https://nodejs.org/en) version 20.11.1 +- [Node.js](https://nodejs.org/en) version 20.20.0 - [pnpm package manager](https://pnpm.io/installation) version 10.23.0 - [Docker](https://www.docker.com/get-started/) - [protobuf](https://github.com/protocolbuffers/protobuf) @@ -34,7 +34,7 @@ branch are tagged into a release periodically. ``` cd trigger.dev ``` -3. Ensure you are on the correct version of Node.js (20.11.1). If you are using `nvm`, there is an `.nvmrc` file that will automatically select the correct version of Node.js when you navigate to the repository. +3. Ensure you are on the correct version of Node.js (20.20.0). If you are using `nvm`, there is an `.nvmrc` file that will automatically select the correct version of Node.js when you navigate to the repository. 4. Run `corepack enable` to use the correct version of pnpm (`10.23.0`) as specified in the root `package.json` file.