diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b032f2f7a72..604fcf09072 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -292,7 +292,7 @@ jobs: if [ -n "$TRIGGER_BRANCH" ]; then TARGET_ARGS+=(--branch "$TRIGGER_BRANCH") fi - bunx trigger.dev@4.8.2 deploy "${TARGET_ARGS[@]}" --skip-promotion + bunx trigger.dev@4.8.3 deploy "${TARGET_ARGS[@]}" --skip-promotion - name: Validate deployment version output env: @@ -643,7 +643,7 @@ jobs: if [ -n "$TRIGGER_BRANCH" ]; then TARGET_ARGS+=(--branch "$TRIGGER_BRANCH") fi - bunx trigger.dev@4.8.2 promote "$VERSION" "${TARGET_ARGS[@]}" + bunx trigger.dev@4.8.3 promote "$VERSION" "${TARGET_ARGS[@]}" # Production only, once the new app serves traffic: submit the library and comparison URLs this # push changed to IndexNow. Best effort: nothing depends on it, and a failure never fails the run. diff --git a/.github/workflows/trigger-dev.yml b/.github/workflows/trigger-dev.yml index 4ad1d348200..c34d6115764 100644 --- a/.github/workflows/trigger-dev.yml +++ b/.github/workflows/trigger-dev.yml @@ -41,7 +41,7 @@ jobs: set -euo pipefail : "${TRIGGER_ACCESS_TOKEN:?TRIGGER_ACCESS_TOKEN_PREVIEW or TRIGGER_ACCESS_TOKEN must be configured}" : "${TRIGGER_PROJECT_ID:?TRIGGER_PROJECT_ID must be configured}" - bunx trigger.dev@4.8.2 deploy --env preview --branch dev-sim --skip-promotion + bunx trigger.dev@4.8.3 deploy --env preview --branch dev-sim --skip-promotion - name: Promote current dev preview working-directory: ./apps/sim @@ -61,4 +61,4 @@ jobs: echo "::notice::Skipping superseded dev task version $VERSION" exit 0 fi - bunx trigger.dev@4.8.2 promote "$VERSION" --env preview --branch dev-sim + bunx trigger.dev@4.8.3 promote "$VERSION" --env preview --branch dev-sim diff --git a/apps/sim/lib/core/config/appconfig.ts b/apps/sim/lib/core/config/appconfig.ts index 6046ea1a9ef..da35e356bb6 100644 --- a/apps/sim/lib/core/config/appconfig.ts +++ b/apps/sim/lib/core/config/appconfig.ts @@ -8,7 +8,7 @@ import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { toRecordOrNull } from '@sim/utils/object' import { LRUCache } from 'lru-cache' -import { getAwsCredentialsFromEnv } from '@/lib/core/config/aws' +import { getAwsCredentials } from '@/lib/core/config/aws' import { env } from '@/lib/core/config/env' const logger = createLogger('AppConfig') @@ -121,7 +121,7 @@ function getClient(): AppConfigDataClient { if (!client) { client = new AppConfigDataClient({ region: env.AWS_REGION, - credentials: getAwsCredentialsFromEnv(), + credentials: getAwsCredentials(), }) } return client diff --git a/apps/sim/lib/core/config/aws.ts b/apps/sim/lib/core/config/aws.ts index 13f5570a2e6..c060e0a048c 100644 --- a/apps/sim/lib/core/config/aws.ts +++ b/apps/sim/lib/core/config/aws.ts @@ -1,24 +1,126 @@ +import { createLogger } from '@sim/logger' +import { type AWSCredentials, awsCredentialProvider } from '@trigger.dev/sdk/oidc/aws' import { env } from '@/lib/core/config/env' -interface AwsCredentials { - accessKeyId: string - secretAccessKey: string +const logger = createLogger('AwsCredentials') + +type AwsCredentialsProvider = () => Promise + +/** + * Refresh workload credentials this long before they expire. It must exceed the AWS SDK's own + * five-minute refresh window, or a client keeps asking for credentials this module still + * considers fresh. + */ +const REFRESH_BEFORE_EXPIRY_MS = 10 * 60_000 + +/** + * At the start of a run, refresh workload credentials that expire within this window. A token can + * only be minted while an attempt is executing, and work a run leaves behind (the hosted-key + * metrics flush) still calls AWS after the attempt ends. + */ +const RUN_START_MIN_REMAINING_MS = 30 * 60_000 + +/** + * One AWS session for the whole worker process, not one per run. Every run in a deployment + * assumes the same role, and Sim's AWS clients are process-wide, so a run-named session would + * attribute later runs' calls to the run that minted it. + */ +const WORKLOAD_ROLE_SESSION_NAME = 'sim-trigger-worker' + +let workloadProvider: AwsCredentialsProvider | null = null +let currentWorkloadCredentials: AWSCredentials | null = null +let pendingWorkloadCredentials: Promise | null = null + +function getWorkloadProvider(roleArn: string): AwsCredentialsProvider { + if (!workloadProvider) { + if (!env.AWS_REGION) { + throw new Error('TRIGGER_AWS_ROLE_ARN is set but AWS_REGION is not; set both.') + } + workloadProvider = awsCredentialProvider({ + roleArn, + region: env.AWS_REGION, + roleSessionName: WORKLOAD_ROLE_SESSION_NAME, + }) + } + return workloadProvider +} + +/** Credentials this close to expiry are treated as expired, so a signed request never outlives them. */ +const EXPIRY_SKEW_MS = 60_000 + +function remainingMs(credentials: AWSCredentials | null): number { + return credentials ? credentials.expiration.getTime() - Date.now() : 0 +} + +/** + * The cached workload credentials while they have more than `minRemainingMs` left, otherwise a + * fresh exchange. Concurrent callers share one exchange. When the exchange fails, still-valid + * cached credentials are served instead: outside an executing attempt no token can be minted, and + * work a run leaves behind (the metrics flush) must not lose credentials that have not expired. + */ +function resolveWorkloadCredentials( + roleArn: string, + minRemainingMs: number +): Promise { + const current = currentWorkloadCredentials + if (current && remainingMs(current) > minRemainingMs) { + return Promise.resolve(current) + } + if (!pendingWorkloadCredentials) { + pendingWorkloadCredentials = getWorkloadProvider(roleArn)() + .then((credentials) => { + currentWorkloadCredentials = credentials + return credentials + }) + .catch((error: unknown) => { + const cached = currentWorkloadCredentials + if (!cached || remainingMs(cached) <= EXPIRY_SKEW_MS) throw error + logger.warn('Workload-identity exchange failed; serving cached credentials until expiry', { + error, + remainingMs: remainingMs(cached), + }) + return cached + }) + .finally(() => { + pendingWorkloadCredentials = null + }) + } + return pendingWorkloadCredentials } /** - * Explicit AWS credentials from the environment, or `undefined` to defer to the - * default AWS provider chain (the ECS task role in our deployments). + * Credentials for Sim's own AWS clients: file storage (S3), AppConfig, email (SES), hosted-key + * metrics and hosted Bedrock. Never for a user's integration, which brings its own keys. * - * Shared by every AWS SDK client (S3, AppConfig, …) so credential resolution is - * identical everywhere: explicit keys when both `AWS_ACCESS_KEY_ID` and - * `AWS_SECRET_ACCESS_KEY` are set (self-hosted, trigger.dev workers), otherwise - * the instance/task role. + * With `TRIGGER_AWS_ROLE_ARN` set (deployed Trigger.dev runs), temporary credentials for that + * role, exchanged for the run's Trigger.dev workload-identity token. It takes precedence over any + * static keys in the environment, so a failed exchange surfaces instead of silently falling back + * to a stored key. Otherwise `undefined`: the AWS SDK's default chain, which reads + * `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_SESSION_TOKEN`, then shared profiles, then + * the ECS task role. + */ +export function getAwsCredentials(): AwsCredentialsProvider | undefined { + const roleArn = env.TRIGGER_AWS_ROLE_ARN + if (!roleArn) return undefined + return () => resolveWorkloadCredentials(roleArn, REFRESH_BEFORE_EXPIRY_MS) +} + +/** Whether Sim's own AWS clients have credentials of their own rather than the default chain. */ +export function hasExplicitAwsCredentials(): boolean { + return Boolean(env.TRIGGER_AWS_ROLE_ARN || (env.AWS_ACCESS_KEY_ID && env.AWS_SECRET_ACCESS_KEY)) +} + +/** + * Refreshes workload credentials at the start of a Trigger.dev run, while a token can be minted, + * so they outlive the attempt. A no-op outside workload identity. Never throws: a failure is + * logged here, and the run's first AWS call retries the exchange and fails with the cause. */ -export function getAwsCredentialsFromEnv(): AwsCredentials | undefined { - return env.AWS_ACCESS_KEY_ID && env.AWS_SECRET_ACCESS_KEY - ? { - accessKeyId: env.AWS_ACCESS_KEY_ID, - secretAccessKey: env.AWS_SECRET_ACCESS_KEY, - } - : undefined +export async function refreshAwsCredentialsForRun(): Promise { + const roleArn = env.TRIGGER_AWS_ROLE_ARN + if (!roleArn) return + try { + await resolveWorkloadCredentials(roleArn, RUN_START_MIN_REMAINING_MS) + } catch (error) { + logger.error('Could not exchange the workload-identity token for AWS credentials', { error }) + } } diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index 5aa643f904a..1af9bdf88ba 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -364,6 +364,7 @@ export const env = createEnv({ AWS_REGION: z.string().optional(), // AWS region for S3 buckets AWS_ACCESS_KEY_ID: z.string().optional(), // AWS access key ID AWS_SECRET_ACCESS_KEY: z.string().optional(), // AWS secret access key + TRIGGER_AWS_ROLE_ARN: z.string().optional(), // Role Trigger.dev runs assume through workload identity, instead of static keys S3_BUCKET_NAME: z.string().optional(), // S3 bucket for general file storage S3_KB_BUCKET_NAME: z.string().optional(), // S3 bucket for knowledge base files S3_EXECUTION_FILES_BUCKET_NAME: z.string().optional(), // S3 bucket for workflow execution files diff --git a/apps/sim/lib/core/config/trigger-attempt.ts b/apps/sim/lib/core/config/trigger-attempt.ts new file mode 100644 index 00000000000..c932f59614f --- /dev/null +++ b/apps/sim/lib/core/config/trigger-attempt.ts @@ -0,0 +1,13 @@ +import { refreshAwsCredentialsForRun } from '@/lib/core/config/aws' +import { warmRedisConnection } from '@/lib/core/config/redis' + +/** + * The per-attempt setup the Trigger.dev `init` hook awaits before `run()`. Warms the shared Redis + * connection, and refreshes workload-identity AWS credentials while the attempt is executing and a + * token can still be minted, so work a run leaves behind (the hosted-key metrics flush) has + * credentials after the attempt ends. Never throws, like each step: a throw in `init` fails the + * attempt. + */ +export async function prepareTriggerAttempt(): Promise { + await Promise.all([warmRedisConnection(), refreshAwsCredentialsForRun()]) +} diff --git a/apps/sim/lib/messaging/email/providers/ses.ts b/apps/sim/lib/messaging/email/providers/ses.ts index 2a6fbd701ee..93efc679c06 100644 --- a/apps/sim/lib/messaging/email/providers/ses.ts +++ b/apps/sim/lib/messaging/email/providers/ses.ts @@ -1,20 +1,22 @@ import { SESv2Client, SendEmailCommand } from '@aws-sdk/client-sesv2' import nodemailer from 'nodemailer' import type SESTransport from 'nodemailer/lib/ses-transport' +import { getAwsCredentials } from '@/lib/core/config/aws' import { env } from '@/lib/core/config/env' import { sendViaNodemailer } from '@/lib/messaging/email/providers/send-via-nodemailer' import type { MailProvider } from '@/lib/messaging/email/types' /** * AWS SES via nodemailer's SES transport using the AWS SDK v3 client. - * Credentials resolve through the SDK's default provider chain (env vars, - * shared config, ECS/EKS task role, EC2 instance profile, SSO). + * Credentials come from `getAwsCredentials()`: workload identity in Trigger.dev + * runs, otherwise the SDK's default provider chain (env vars, shared config, + * ECS/EKS task role, EC2 instance profile, SSO). */ export function createSesProvider(): MailProvider | null { const region = env.AWS_SES_REGION if (!region) return null - const sesClient = new SESv2Client({ region }) + const sesClient = new SESv2Client({ region, credentials: getAwsCredentials() }) const sesOptions: SESTransport.Options = { SES: { sesClient, SendEmailCommand }, } diff --git a/apps/sim/lib/monitoring/metrics.ts b/apps/sim/lib/monitoring/metrics.ts index 4f6f980bd39..9639078773f 100644 --- a/apps/sim/lib/monitoring/metrics.ts +++ b/apps/sim/lib/monitoring/metrics.ts @@ -30,6 +30,8 @@ import { } from '@aws-sdk/client-cloudwatch' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' +import { getAwsCredentials } from '@/lib/core/config/aws' +import { isHosted } from '@/lib/core/config/env-flags' const logger = createLogger('HostedKeyMetrics') @@ -44,10 +46,10 @@ type QueueReason = 'actor_requests' | 'dimension' | 'queue_position' /** `metering` marks a provider call that succeeded but could not be priced. */ type FailureReason = 'rate_limited' | 'auth' | 'other' | 'metering' -// Deployed envs (app + trigger worker) carry static AWS creds; local dev does -// not. No creds → no-op, so recorders stay always-safe to call (same contract -// as the previous no-op-meter behavior). -const ENABLED = Boolean(process.env.AWS_ACCESS_KEY_ID) +// Hosted keys exist only on Sim Cloud, whose app and workers can always reach +// CloudWatch (task role or workload identity). Elsewhere recorders are no-ops, +// so they stay always-safe to call. +const ENABLED = isHosted const ENVIRONMENT = process.env.OTEL_DEPLOYMENT_ENVIRONMENT || @@ -63,7 +65,10 @@ let handlersRegistered = false function getClient(): CloudWatchClient { if (!client) { - client = new CloudWatchClient({ region: process.env.AWS_REGION || 'us-east-1' }) + client = new CloudWatchClient({ + region: process.env.AWS_REGION || 'us-east-1', + credentials: getAwsCredentials(), + }) } return client } diff --git a/apps/sim/lib/uploads/core/setup.server.ts b/apps/sim/lib/uploads/core/setup.server.ts index b446382e87d..8f0a2217717 100644 --- a/apps/sim/lib/uploads/core/setup.server.ts +++ b/apps/sim/lib/uploads/core/setup.server.ts @@ -2,6 +2,7 @@ import { existsSync } from 'fs' import { mkdir } from 'fs/promises' import path, { join } from 'path' import { createLogger } from '@sim/logger' +import { hasExplicitAwsCredentials } from '@/lib/core/config/aws' import { env } from '@/lib/core/config/env' import { getStorageProvider, @@ -88,7 +89,7 @@ if (typeof process !== 'undefined') { if (!env.S3_BUCKET_NAME || !env.AWS_REGION) { logger.warn('S3 storage configuration is incomplete') logger.warn('Set S3_BUCKET_NAME and AWS_REGION for S3 storage') - } else if (!env.AWS_ACCESS_KEY_ID || !env.AWS_SECRET_ACCESS_KEY) { + } else if (!hasExplicitAwsCredentials()) { logger.warn('AWS credentials are not set in environment variables') logger.warn('Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY for S3 storage') } else { diff --git a/apps/sim/lib/uploads/providers/s3/client.ts b/apps/sim/lib/uploads/providers/s3/client.ts index 4c6d60a5dc4..a0b38ea57c5 100644 --- a/apps/sim/lib/uploads/providers/s3/client.ts +++ b/apps/sim/lib/uploads/providers/s3/client.ts @@ -16,7 +16,7 @@ import { import { getSignedUrl } from '@aws-sdk/s3-request-presigner' import { getErrorMessage } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' -import { getAwsCredentialsFromEnv } from '@/lib/core/config/aws' +import { getAwsCredentials } from '@/lib/core/config/aws' import { assertKnownSizeWithinLimit, readNodeStreamToBufferWithLimit, @@ -62,7 +62,7 @@ export function getS3Client(): S3Client { region, endpoint: S3_CONFIG.endpoint, forcePathStyle: S3_CONFIG.forcePathStyle, - credentials: getAwsCredentialsFromEnv(), + credentials: getAwsCredentials(), }) return _s3Client diff --git a/apps/sim/lib/uploads/providers/s3/presigned-upload.test.ts b/apps/sim/lib/uploads/providers/s3/presigned-upload.test.ts index 20eaf1053c8..911a94a7ec5 100644 --- a/apps/sim/lib/uploads/providers/s3/presigned-upload.test.ts +++ b/apps/sim/lib/uploads/providers/s3/presigned-upload.test.ts @@ -4,17 +4,13 @@ * missing from `headers`, or a metadata key both signed as a header and hoisted * into the query, makes the provider reject or strip the upload. */ -import { resetEnvMock, setEnv } from '@sim/testing/mocks/env.mock' import { setUploadsConfig, uploadsConfigMock } from '@sim/testing/mocks/uploads-config.mock' -import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' vi.mock('@/lib/uploads/config', () => uploadsConfigMock) import { getS3PresignedUploadUrl, resetS3ClientForTesting } from '@/lib/uploads/providers/s3/client' -setEnv({ AWS_ACCESS_KEY_ID: 'test-access-key', AWS_SECRET_ACCESS_KEY: 'test-secret-key' }) -afterAll(resetEnvMock) - /** Headers the browser sets itself and the uploader never supplies. */ const TRANSPORT_HEADERS = new Set(['host', 'content-length']) @@ -44,7 +40,12 @@ async function presign() { } describe('getS3PresignedUploadUrl', () => { - beforeEach(() => configure(undefined)) + beforeEach(() => { + // The client resolves credentials through the SDK's default chain, which reads process.env. + vi.stubEnv('AWS_ACCESS_KEY_ID', 'test-access-key') + vi.stubEnv('AWS_SECRET_ACCESS_KEY', 'test-secret-key') + configure(undefined) + }) it('signs metadata as uploader-sent headers for a custom S3-compatible endpoint', async () => { configure('https://s3.de.io.cloud.ovh.net') diff --git a/apps/sim/package.json b/apps/sim/package.json index afbc2dc9748..84241bd6f36 100644 --- a/apps/sim/package.json +++ b/apps/sim/package.json @@ -177,8 +177,8 @@ "@tiptap/starter-kit": "3.31.4", "@tiptap/suggestion": "3.31.4", "@tiptap/y-tiptap": "3.0.7", - "@trigger.dev/core": "4.8.2", - "@trigger.dev/sdk": "4.8.2", + "@trigger.dev/core": "4.8.3", + "@trigger.dev/sdk": "4.8.3", "@typescript/typescript6": "^6.0.2", "@xterm/addon-fit": "0.11.0", "@xterm/addon-unicode11": "0.9.0", @@ -298,7 +298,7 @@ "@tailwindcss/postcss": "^4.3.3", "@tailwindcss/typography": "0.5.20", "@testing-library/jest-dom": "^6.10.0", - "@trigger.dev/build": "4.8.2", + "@trigger.dev/build": "4.8.3", "@types/archiver": "8.0.0", "@types/busboy": "1.5.4", "@types/chrome": "0.3.6", diff --git a/apps/sim/providers/bedrock/index.ts b/apps/sim/providers/bedrock/index.ts index 7b7f19585a6..26f79b5977a 100644 --- a/apps/sim/providers/bedrock/index.ts +++ b/apps/sim/providers/bedrock/index.ts @@ -15,6 +15,7 @@ import { import { createLogger } from '@sim/logger' import { getErrorMessage, toError } from '@sim/utils/errors' import { isRecordLike } from '@sim/utils/object' +import { getAwsCredentials } from '@/lib/core/config/aws' import { validateAwsRegion } from '@/lib/core/security/input-validation' import type { IterationToolCall, NormalizedBlockOutput, StreamingExecution } from '@/executor/types' import { MAX_TOOL_ITERATIONS } from '@/providers' @@ -158,12 +159,12 @@ export const bedrockProvider: ProviderConfig = { ) } - const clientConfig: BedrockRuntimeClientConfig = { region } - if (request.bedrockAccessKeyId && request.bedrockSecretKey) { - clientConfig.credentials = { - accessKeyId: request.bedrockAccessKeyId, - secretAccessKey: request.bedrockSecretKey, - } + const clientConfig: BedrockRuntimeClientConfig = { + region, + credentials: + request.bedrockAccessKeyId && request.bedrockSecretKey + ? { accessKeyId: request.bedrockAccessKeyId, secretAccessKey: request.bedrockSecretKey } + : getAwsCredentials(), } // Key on the full credential (access key id + secret) so a corrected secret diff --git a/apps/sim/trigger.config.ts b/apps/sim/trigger.config.ts index 02107306cd9..85e7ba6edf2 100644 --- a/apps/sim/trigger.config.ts +++ b/apps/sim/trigger.config.ts @@ -160,7 +160,8 @@ export default defineConfig({ * because a throw here fails the run. The execution-signal subscriber is * deliberately not warmed here: only the tasks that execute a workflow ever * subscribe, and they are a minority of runs, so that connection is warmed - * on intent at the execution entry point instead. + * on intent at the execution entry point instead. The same step refreshes + * workload-identity AWS credentials (`prepareTriggerAttempt`). * * @see https://trigger.dev/docs/config/config-file#lifecycle-functions */ @@ -172,8 +173,8 @@ export default defineConfig({ markInsideTriggerRun() const { reportAccountTokenSealing } = await import('@sim/db/sealed-text') reportAccountTokenSealing() - const { warmRedisConnection } = await import('./lib/core/config/redis') - await warmRedisConnection() + const { prepareTriggerAttempt } = await import('./lib/core/config/trigger-attempt') + await prepareTriggerAttempt() }, ...(grafanaTelemetry ? { telemetry: grafanaTelemetry } : {}), build: { diff --git a/bun.lock b/bun.lock index a10ed775525..4076326f666 100644 --- a/bun.lock +++ b/bun.lock @@ -335,8 +335,8 @@ "@tiptap/starter-kit": "3.31.4", "@tiptap/suggestion": "3.31.4", "@tiptap/y-tiptap": "3.0.7", - "@trigger.dev/core": "4.8.2", - "@trigger.dev/sdk": "4.8.2", + "@trigger.dev/core": "4.8.3", + "@trigger.dev/sdk": "4.8.3", "@typescript/typescript6": "^6.0.2", "@xterm/addon-fit": "0.11.0", "@xterm/addon-unicode11": "0.9.0", @@ -455,7 +455,7 @@ "@tailwindcss/postcss": "^4.3.3", "@tailwindcss/typography": "0.5.20", "@testing-library/jest-dom": "^6.10.0", - "@trigger.dev/build": "4.8.2", + "@trigger.dev/build": "4.8.3", "@types/archiver": "8.0.0", "@types/busboy": "1.5.4", "@types/chrome": "0.3.6", @@ -2236,11 +2236,11 @@ "@tootallnate/once": ["@tootallnate/once@2.0.1", "", {}, "sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ=="], - "@trigger.dev/build": ["@trigger.dev/build@4.8.2", "", { "dependencies": { "@prisma/config": "^6.10.0", "@trigger.dev/core": "4.8.2", "mlly": "^1.7.1", "pkg-types": "^1.1.3", "tinyglobby": "^0.2.2", "tsconfck": "3.1.3" }, "peerDependencies": { "@typescript/typescript6": "^6.0.0", "typescript": ">=5.0.0" }, "optionalPeers": ["@typescript/typescript6", "typescript"] }, "sha512-mxyisGjBxJPqrCuoe3sqlTgQ7Oc1mgMbUkxzp6uXQX1maPxtbYUkFp/6nPEargc3ul331jxSP8smtatFqtSy5g=="], + "@trigger.dev/build": ["@trigger.dev/build@4.8.3", "", { "dependencies": { "@prisma/config": "^6.10.0", "@trigger.dev/core": "4.8.3", "mlly": "^1.7.1", "pkg-types": "^1.1.3", "tinyglobby": "^0.2.2", "tsconfck": "3.1.3" }, "peerDependencies": { "@typescript/typescript6": "^6.0.0", "typescript": ">=5.0.0" }, "optionalPeers": ["@typescript/typescript6", "typescript"] }, "sha512-4KohHZdx0urxL96zisBAqvL4J4uKZ5u4rejWYyE8UkxDdOviR1YD3nxVmbYN4lWnpHEGxue116ZZrRoZr0+1MQ=="], - "@trigger.dev/core": ["@trigger.dev/core@4.8.2", "", { "dependencies": { "@bugsnag/cuid": "^3.1.1", "@electric-sql/client": "1.0.14", "@google-cloud/precise-date": "^4.0.0", "@jsonhero/path": "^1.0.21", "@opentelemetry/api": "1.9.1", "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/exporter-logs-otlp-http": "0.219.0", "@opentelemetry/exporter-metrics-otlp-http": "0.219.0", "@opentelemetry/exporter-trace-otlp-http": "0.219.0", "@opentelemetry/host-metrics": "^0.38.3", "@opentelemetry/instrumentation": "0.219.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-logs": "0.219.0", "@opentelemetry/sdk-metrics": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0", "@opentelemetry/sdk-trace-node": "2.8.0", "@s2-dev/streamstore": "0.25.0", "dequal": "^2.0.3", "eventsource": "^3.0.5", "eventsource-parser": "^3.0.0", "humanize-duration": "^3.27.3", "jose": "^5.4.0", "nanoid": "3.3.18", "prom-client": "^15.1.0", "socket.io-client": "4.8.4", "std-env": "^3.8.1", "tinyexec": "^0.3.2", "uncrypto": "^0.1.3", "zod": "^3.25.56 || ^4.0.0", "zod-validation-error": "^5.0.0" } }, "sha512-wRD259+2Bxz78ULW733xb8V36DAUnA9hkj/zIkwMw6phf5hq1j/KHHT6xpSjiCzf5h/xloLhDxP8L7kyqSIAvw=="], + "@trigger.dev/core": ["@trigger.dev/core@4.8.3", "", { "dependencies": { "@bugsnag/cuid": "^3.1.1", "@electric-sql/client": "1.0.14", "@google-cloud/precise-date": "^4.0.0", "@jsonhero/path": "^1.0.21", "@opentelemetry/api": "1.9.1", "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/exporter-logs-otlp-http": "0.219.0", "@opentelemetry/exporter-metrics-otlp-http": "0.219.0", "@opentelemetry/exporter-trace-otlp-http": "0.219.0", "@opentelemetry/host-metrics": "^0.38.3", "@opentelemetry/instrumentation": "0.219.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-logs": "0.219.0", "@opentelemetry/sdk-metrics": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0", "@opentelemetry/sdk-trace-node": "2.8.0", "@s2-dev/streamstore": "0.25.0", "dequal": "^2.0.3", "eventsource": "^3.0.5", "eventsource-parser": "^3.0.0", "humanize-duration": "^3.27.3", "jose": "^5.4.0", "nanoid": "3.3.18", "prom-client": "^15.1.0", "socket.io-client": "4.8.4", "std-env": "^3.8.1", "tinyexec": "^0.3.2", "uncrypto": "^0.1.3", "zod": "^3.25.56 || ^4.0.0", "zod-validation-error": "^5.0.0" } }, "sha512-V/7nOgnshOMMIUQ392wKAojLmE1t/xuod7WOnFE5s62d2IhnOaYGLBFtN0WnuNQP/GVxmRVGARwRv8j43XH0XA=="], - "@trigger.dev/sdk": ["@trigger.dev/sdk@4.8.2", "", { "dependencies": { "@opentelemetry/api": "1.9.1", "@opentelemetry/semantic-conventions": "1.41.1", "@trigger.dev/core": "4.8.2", "uncrypto": "^0.1.3" }, "peerDependencies": { "@ai-sdk/otel": ">=1.0.0-0 <2", "ai": "^5.0.0 || ^6.0.0 || >=7.0.0-canary <8", "react": "^18.0 || ^19.0", "zod": "^3.25.56 || ^4.0.0" }, "optionalPeers": ["@ai-sdk/otel", "ai", "react"] }, "sha512-IZD8+JfFQK4R7RdgYAeGiBohNfYJbB2I5ScdeUeR0ahLZl9yxXFRG5txByXAYk7FsEJcrvAUp1fq50OHTK0ylw=="], + "@trigger.dev/sdk": ["@trigger.dev/sdk@4.8.3", "", { "dependencies": { "@opentelemetry/api": "1.9.1", "@opentelemetry/semantic-conventions": "1.41.1", "@trigger.dev/core": "4.8.3", "uncrypto": "^0.1.3" }, "peerDependencies": { "@ai-sdk/otel": ">=1.0.0-0 <2", "@aws-sdk/client-sts": "^3.840.0", "ai": "^5.0.0 || ^6.0.0 || >=7.0.0-canary <8", "react": "^18.0 || ^19.0", "zod": "^3.25.56 || ^4.0.0" }, "optionalPeers": ["@ai-sdk/otel", "@aws-sdk/client-sts", "ai", "react"] }, "sha512-0IkDT/mjRXVhRaaXEMlCbqb0YwafgUW75LvkVQMLe3w1L0sO/+Ud/YjNjQqrl8foZO1w2a+a08NEueyNKu9reg=="], "@turbo/darwin-64": ["@turbo/darwin-64@2.9.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-t7QiPflaEyBE4oayeZtSmu4mEfjgIrcNlNNl1z1dmIVPqEdtA7+CfTf8d7KXsOGPh6aNgWjKxyvQg9uGfDQF+A=="], diff --git a/bunfig.toml b/bunfig.toml index c711d24aede..3a21b48225b 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -4,8 +4,18 @@ exact = true # (blocks freshly published, potentially compromised releases). minimumReleaseAge = 604800 # Explicit framework pins must install before their release reaches the age threshold: a -# security release of Next.js is pinned exactly the day it ships. -minimumReleaseAgeExcludes = ["next", "@next/env"] +# security release of Next.js is pinned exactly the day it ships. Trigger.dev 4.8.3 (published +# 2026-10-30, the first release with workload identity) is exempted until it clears the threshold +# on 2026-11-06; drop the four Trigger.dev entries then. They are bare names because Bun 1.4 ignores +# version-pinned exclusions; `exact = true` and the lockfile still hold the packages at 4.8.3. +minimumReleaseAgeExcludes = [ + "next", + "@next/env", + "@trigger.dev/sdk", + "@trigger.dev/core", + "@trigger.dev/build", + "trigger.dev", +] [run] env = { NEXT_PUBLIC_APP_URL = "http://localhost:3000" }