From 42fb434354e64124881c66b0ea34ff333538efa2 Mon Sep 17 00:00:00 2001 From: Waleed Date: Wed, 1 Apr 2026 10:46:58 -0700 Subject: [PATCH 01/16] fix(encryption): specify authTagLength on all AES-GCM cipher/decipher calls (#3883) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: specify authTagLength in AES-GCM decipheriv calls Fixes missing authTagLength parameter in createDecipheriv calls using AES-256-GCM mode. Without explicit tag length specification, the application may be tricked into accepting shorter authentication tags, potentially allowing ciphertext spoofing. CWE-310: Cryptographic Issues (gcm-no-tag-length) * fix: specify authTagLength on createCipheriv calls for AES-GCM consistency Complements #3881 by adding explicit authTagLength: 16 to the encrypt side as well, ensuring both cipher and decipher specify the tag length. Co-Authored-By: Claude Opus 4.6 * refactor: clean up crypto modules - Fix error: any → error: unknown with proper type guard in encryption.ts - Eliminate duplicate iv.toString('hex') calls in both encrypt functions - Remove redundant string split in decryptApiKey (was splitting twice) Co-Authored-By: Claude Opus 4.6 * new turborepo version --------- Co-authored-by: Claude Opus 4.6 Co-authored-by: Lakee Sivaraya <71339072+lakeesiv@users.noreply.github.com> Co-authored-by: Vikhyath Mondreti Co-authored-by: Vikhyath Mondreti Co-authored-by: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com> Co-authored-by: NLmejiro --- apps/sim/lib/api-key/crypto.ts | 18 ++++++++++-------- apps/sim/lib/core/security/encryption.ts | 15 +++++++++------ bun.lock | 16 ++++++++-------- package.json | 2 +- .../scripts/migrate-block-api-keys-to-byok.ts | 4 ++-- 5 files changed, 30 insertions(+), 25 deletions(-) diff --git a/apps/sim/lib/api-key/crypto.ts b/apps/sim/lib/api-key/crypto.ts index 3cac7ee0f5c..aeee9097218 100644 --- a/apps/sim/lib/api-key/crypto.ts +++ b/apps/sim/lib/api-key/crypto.ts @@ -36,16 +36,17 @@ export async function encryptApiKey(apiKey: string): Promise<{ encrypted: string } const iv = randomBytes(16) - const cipher = createCipheriv('aes-256-gcm', key, iv) + const cipher = createCipheriv('aes-256-gcm', key, iv, { authTagLength: 16 }) let encrypted = cipher.update(apiKey, 'utf8', 'hex') encrypted += cipher.final('hex') const authTag = cipher.getAuthTag() + const ivHex = iv.toString('hex') // Format: iv:encrypted:authTag return { - encrypted: `${iv.toString('hex')}:${encrypted}:${authTag.toString('hex')}`, - iv: iv.toString('hex'), + encrypted: `${ivHex}:${encrypted}:${authTag.toString('hex')}`, + iv: ivHex, } } @@ -55,8 +56,10 @@ export async function encryptApiKey(apiKey: string): Promise<{ encrypted: string * @returns A promise that resolves to an object containing the decrypted API key */ export async function decryptApiKey(encryptedValue: string): Promise<{ decrypted: string }> { + const parts = encryptedValue.split(':') + // Check if this is actually encrypted (contains colons) - if (!encryptedValue.includes(':') || encryptedValue.split(':').length !== 3) { + if (parts.length !== 3) { // This is a plain text key, return as-is return { decrypted: encryptedValue } } @@ -68,10 +71,9 @@ export async function decryptApiKey(encryptedValue: string): Promise<{ decrypted return { decrypted: encryptedValue } } - const parts = encryptedValue.split(':') const ivHex = parts[0] - const authTagHex = parts[parts.length - 1] - const encrypted = parts.slice(1, -1).join(':') + const authTagHex = parts[2] + const encrypted = parts[1] if (!ivHex || !encrypted || !authTagHex) { throw new Error('Invalid encrypted API key format. Expected "iv:encrypted:authTag"') @@ -81,7 +83,7 @@ export async function decryptApiKey(encryptedValue: string): Promise<{ decrypted const authTag = Buffer.from(authTagHex, 'hex') try { - const decipher = createDecipheriv('aes-256-gcm', key, iv) + const decipher = createDecipheriv('aes-256-gcm', key, iv, { authTagLength: 16 }) decipher.setAuthTag(authTag) let decrypted = decipher.update(encrypted, 'hex', 'utf8') diff --git a/apps/sim/lib/core/security/encryption.ts b/apps/sim/lib/core/security/encryption.ts index ab4fcdab71d..811a60ada48 100644 --- a/apps/sim/lib/core/security/encryption.ts +++ b/apps/sim/lib/core/security/encryption.ts @@ -21,16 +21,17 @@ export async function encryptSecret(secret: string): Promise<{ encrypted: string const iv = randomBytes(16) const key = getEncryptionKey() - const cipher = createCipheriv('aes-256-gcm', key, iv) + const cipher = createCipheriv('aes-256-gcm', key, iv, { authTagLength: 16 }) let encrypted = cipher.update(secret, 'utf8', 'hex') encrypted += cipher.final('hex') const authTag = cipher.getAuthTag() + const ivHex = iv.toString('hex') // Format: iv:encrypted:authTag return { - encrypted: `${iv.toString('hex')}:${encrypted}:${authTag.toString('hex')}`, - iv: iv.toString('hex'), + encrypted: `${ivHex}:${encrypted}:${authTag.toString('hex')}`, + iv: ivHex, } } @@ -54,15 +55,17 @@ export async function decryptSecret(encryptedValue: string): Promise<{ decrypted const authTag = Buffer.from(authTagHex, 'hex') try { - const decipher = createDecipheriv('aes-256-gcm', key, iv) + const decipher = createDecipheriv('aes-256-gcm', key, iv, { authTagLength: 16 }) decipher.setAuthTag(authTag) let decrypted = decipher.update(encrypted, 'hex', 'utf8') decrypted += decipher.final('utf8') return { decrypted } - } catch (error: any) { - logger.error('Decryption error:', { error: error.message }) + } catch (error: unknown) { + logger.error('Decryption error:', { + error: error instanceof Error ? error.message : 'Unknown error', + }) throw error } } diff --git a/bun.lock b/bun.lock index 181248e1ee7..e773f35d67e 100644 --- a/bun.lock +++ b/bun.lock @@ -10,7 +10,7 @@ "glob": "13.0.0", "husky": "9.1.7", "lint-staged": "16.0.0", - "turbo": "2.9.1", + "turbo": "2.9.3", }, }, "apps/docs": { @@ -1486,17 +1486,17 @@ "@trigger.dev/sdk": ["@trigger.dev/sdk@4.4.3", "", { "dependencies": { "@opentelemetry/api": "1.9.0", "@opentelemetry/semantic-conventions": "1.36.0", "@trigger.dev/core": "4.4.3", "chalk": "^5.2.0", "cronstrue": "^2.21.0", "debug": "^4.3.4", "evt": "^2.4.13", "slug": "^6.0.0", "ulid": "^2.3.0", "uncrypto": "^0.1.3", "uuid": "^9.0.0", "ws": "^8.11.0" }, "peerDependencies": { "ai": "^4.2.0 || ^5.0.0 || ^6.0.0", "zod": "^3.0.0 || ^4.0.0" }, "optionalPeers": ["ai"] }, "sha512-ghJkak+PTBJJ9HiHMcnahJmzjsgCzYiIHu5Qj5R7I9q5LS6i7mkx169rB/tOE9HLadd4HSu3yYA5DrH4wXhZuw=="], - "@turbo/darwin-64": ["@turbo/darwin-64@2.9.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-d1zTcIf6VWT7cdfjhi0X36C2PRsUi2HdEwYzVgkLHmuuYtL+1Y1Zu3JdlouoB/NjG2vX3q4NnKLMNhDOEweoIg=="], + "@turbo/darwin-64": ["@turbo/darwin-64@2.9.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-P8foouaP+y/p+hhEGBoZpzMbpVvUMwPjDpcy6wN7EYfvvyISD1USuV27qWkczecihwuPJzQ1lDBuL8ERcavTyg=="], - "@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.9.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-AwJ4mA++Kpem33Lcov093hS1LrgqbKxqq5FCReoqsA8ayEG6eAJAo8ItDd9qQTdBiXxZH8GHCspLAMIe1t3Xyw=="], + "@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.9.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-SIzEkvtNdzdI50FJDaIQ6kQGqgSSdFPcdn0wqmmONN6iGKjy6hsT+EH99GP65FsfV7DLZTh2NmtTIRl2kdoz5Q=="], - "@turbo/linux-64": ["@turbo/linux-64@2.9.1", "", { "os": "linux", "cpu": "x64" }, "sha512-HT9SjKkjEw9uvlgly/qwCGEm4wOXOwQPSPS+wkg+/O1Qan3F1uU/0PFYzxl3m4lfuV3CP9wr2Dq5dPrUX+B9Ag=="], + "@turbo/linux-64": ["@turbo/linux-64@2.9.3", "", { "os": "linux", "cpu": "x64" }, "sha512-pLRwFmcHHNBvsCySLS6OFabr/07kDT2pxEt/k6eBf/3asiVQZKJ7Rk88AafQx2aYA641qek4RsXvYO3JYpiBug=="], - "@turbo/linux-arm64": ["@turbo/linux-arm64@2.9.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-+4s5GZs3kjxc1KMhLBhoQy4UBkXjOhgidA9ipNllkA4JLivSqUCuOgU1Xbyp6vzYrsqHJ9vvwo/2mXgEtD6ZHg=="], + "@turbo/linux-arm64": ["@turbo/linux-arm64@2.9.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-gy6ApUroC2Nzv+qjGtE/uPNkhHAFU4c8God+zd5Aiv9L9uBgHlxVJpHT3XWl5xwlJZ2KWuMrlHTaS5kmNB+q1Q=="], - "@turbo/windows-64": ["@turbo/windows-64@2.9.1", "", { "os": "win32", "cpu": "x64" }, "sha512-ZO7GCyQd5HV564XWHc9KysjanFfM3DmnWquyEByu+hQMq42g9OMU/fYOCfHS6Xj2aXkIg2FHJeRV+iAck2YrbQ=="], + "@turbo/windows-64": ["@turbo/windows-64@2.9.3", "", { "os": "win32", "cpu": "x64" }, "sha512-d0YelTX6hAsB7kIEtGB3PzIzSfAg3yDoUlHwuwJc3adBXUsyUIs0YLG+1NNtuhcDOUGnWQeKUoJ2pGWvbpRj7w=="], - "@turbo/windows-arm64": ["@turbo/windows-arm64@2.9.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-BjX2fdz38mBb/H94JXrD5cJ+mEq8NmsCbYdC42JzQebJ0X8EdNgyFoEhOydPGViOmaRmhhdZnPZKKn6wahSpcA=="], + "@turbo/windows-arm64": ["@turbo/windows-arm64@2.9.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-/08CwpKJl3oRY8nOlh2YgilZVJDHsr60XTNxRhuDeuFXONpUZ5X+Nv65izbG/xBew9qxcJFbDX9/sAmAX+ITcQ=="], "@tweenjs/tween.js": ["@tweenjs/tween.js@23.1.3", "", {}, "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA=="], @@ -3632,7 +3632,7 @@ "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], - "turbo": ["turbo@2.9.1", "", { "optionalDependencies": { "@turbo/darwin-64": "2.9.1", "@turbo/darwin-arm64": "2.9.1", "@turbo/linux-64": "2.9.1", "@turbo/linux-arm64": "2.9.1", "@turbo/windows-64": "2.9.1", "@turbo/windows-arm64": "2.9.1" }, "bin": { "turbo": "bin/turbo" } }, "sha512-TO9du8MwLTAKoXcGezekh9cPJabJUb0+8KxtpMR6kXdRASrmJ8qXf2GkVbCREgzbMQakzfNcux9cZtxheDY4RQ=="], + "turbo": ["turbo@2.9.3", "", { "optionalDependencies": { "@turbo/darwin-64": "2.9.3", "@turbo/darwin-arm64": "2.9.3", "@turbo/linux-64": "2.9.3", "@turbo/linux-arm64": "2.9.3", "@turbo/windows-64": "2.9.3", "@turbo/windows-arm64": "2.9.3" }, "bin": { "turbo": "bin/turbo" } }, "sha512-J/VUvsGRykPb9R8Kh8dHVBOqioDexLk9BhLCU/ZybRR+HN9UR3cURdazFvNgMDt9zPP8TF6K73Z+tplfmi0PqQ=="], "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="], diff --git a/package.json b/package.json index 84fff1afa0b..9cd7279b997 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "glob": "13.0.0", "husky": "9.1.7", "lint-staged": "16.0.0", - "turbo": "2.9.1" + "turbo": "2.9.3" }, "lint-staged": { "*.{js,jsx,ts,tsx,json,css,scss}": [ diff --git a/packages/db/scripts/migrate-block-api-keys-to-byok.ts b/packages/db/scripts/migrate-block-api-keys-to-byok.ts index 06a507a5e92..88ee98e2204 100644 --- a/packages/db/scripts/migrate-block-api-keys-to-byok.ts +++ b/packages/db/scripts/migrate-block-api-keys-to-byok.ts @@ -125,7 +125,7 @@ function getEncryptionKeyBuffer(): Buffer { async function encryptSecret(secret: string): Promise { const iv = randomBytes(16) const key = getEncryptionKeyBuffer() - const cipher = createCipheriv('aes-256-gcm', key, iv) + const cipher = createCipheriv('aes-256-gcm', key, iv, { authTagLength: 16 }) let encrypted = cipher.update(secret, 'utf8', 'hex') encrypted += cipher.final('hex') const authTag = cipher.getAuthTag() @@ -146,7 +146,7 @@ async function decryptSecret(encryptedValue: string): Promise { const iv = Buffer.from(ivHex, 'hex') const authTag = Buffer.from(authTagHex, 'hex') - const decipher = createDecipheriv('aes-256-gcm', key, iv) + const decipher = createDecipheriv('aes-256-gcm', key, iv, { authTagLength: 16 }) decipher.setAuthTag(authTag) let decrypted = decipher.update(encrypted, 'hex', 'utf8') From 2ede12aa0e8dcd45508376fd38348c9f614ea346 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 1 Apr 2026 11:42:19 -0700 Subject: [PATCH 02/16] fix(cost): worker crash incremenental case (#3885) --- apps/sim/lib/logs/execution/logging-session.test.ts | 10 ++++++---- apps/sim/lib/logs/execution/logging-session.ts | 9 +++++++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/apps/sim/lib/logs/execution/logging-session.test.ts b/apps/sim/lib/logs/execution/logging-session.test.ts index a44bc5e72d5..dfc2a306d2e 100644 --- a/apps/sim/lib/logs/execution/logging-session.test.ts +++ b/apps/sim/lib/logs/execution/logging-session.test.ts @@ -391,10 +391,12 @@ describe('LoggingSession completion retries', () => { await session.onBlockComplete('block-2', 'Transform', 'function', { endedAt: '2025-01-01T00:00:01.000Z', - output: { value: true }, - cost: { total: 1, input: 1, output: 0 }, - tokens: { input: 1, output: 0, total: 1 }, - model: 'test-model', + output: { + value: true, + cost: { total: 1, input: 1, output: 0 }, + tokens: { input: 1, output: 0, total: 1 }, + model: 'test-model', + }, }) const completionPromise = session.safeComplete({ finalOutput: { ok: true } }) diff --git a/apps/sim/lib/logs/execution/logging-session.ts b/apps/sim/lib/logs/execution/logging-session.ts index 7fb72bc1d73..3b0a20bd642 100644 --- a/apps/sim/lib/logs/execution/logging-session.ts +++ b/apps/sim/lib/logs/execution/logging-session.ts @@ -294,11 +294,16 @@ export class LoggingSession { }) ) - if (!output?.cost || typeof output.cost.total !== 'number' || output.cost.total <= 0) { + const blockOutput = output?.output + if ( + !blockOutput?.cost || + typeof blockOutput.cost.total !== 'number' || + blockOutput.cost.total <= 0 + ) { return } - const { cost, tokens, model } = output + const { cost, tokens, model } = blockOutput this.accumulatedCost.total += cost.total || 0 this.accumulatedCost.input += cost.input || 0 From df6ceb61a444e61a267ad4241b725063437bad8d Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 1 Apr 2026 14:01:13 -0700 Subject: [PATCH 03/16] fix(envvar): remove dead env var --- apps/sim/lib/core/bullmq/connection.ts | 4 ++-- apps/sim/lib/core/config/env.ts | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/sim/lib/core/bullmq/connection.ts b/apps/sim/lib/core/bullmq/connection.ts index e888f5772a7..80def9d5cb5 100644 --- a/apps/sim/lib/core/bullmq/connection.ts +++ b/apps/sim/lib/core/bullmq/connection.ts @@ -1,8 +1,8 @@ import type { ConnectionOptions } from 'bullmq' -import { env, isTruthy } from '@/lib/core/config/env' +import { env } from '@/lib/core/config/env' export function isBullMQEnabled(): boolean { - return isTruthy(env.CONCURRENCY_CONTROL_ENABLED) && Boolean(env.REDIS_URL) + return Boolean(env.REDIS_URL) } export function getBullMQConnectionOptions(): ConnectionOptions { diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index d58892e6887..c6b05f076f8 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -187,7 +187,6 @@ export const env = createEnv({ FREE_PLAN_LOG_RETENTION_DAYS: z.string().optional(), // Log retention days for free plan users // Admission & Burst Protection - CONCURRENCY_CONTROL_ENABLED: z.string().optional().default('false'), // Set to 'true' to enable BullMQ-based concurrency control (default: inline execution) ADMISSION_GATE_MAX_INFLIGHT: z.string().optional().default('500'), // Max concurrent in-flight execution requests per pod DISPATCH_MAX_QUEUE_PER_WORKSPACE: z.string().optional().default('1000'), // Max queued dispatch jobs per workspace DISPATCH_MAX_QUEUE_GLOBAL: z.string().optional().default('50000'), // Max queued dispatch jobs globally From 076c835ba2ca98b26754a83357d792bf70310fce Mon Sep 17 00:00:00 2001 From: Waleed Date: Wed, 1 Apr 2026 15:48:49 -0700 Subject: [PATCH 04/16] improvement(credentials): consolidate OAuth modals and auto-fill credential name (#3887) * improvement(credentials): consolidate OAuth modals and auto-fill credential name * fix(credentials): context-aware subtitle for KB vs workflow --- .../components/oauth-modal.test.ts | 72 +++++ .../[workspaceId]/components/oauth-modal.tsx | 304 ++++++++++++++++++ .../add-connector-modal.tsx | 5 +- .../connectors-section/connectors-section.tsx | 9 +- .../components/connect-credential-modal.tsx | 222 ------------- .../components/oauth-required-modal.tsx | 190 ----------- .../credential-selector.tsx | 9 +- .../components/tools/credential-selector.tsx | 9 +- .../[workspaceId]/w/[workflowId]/workflow.tsx | 33 +- 9 files changed, 408 insertions(+), 445 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/components/oauth-modal.test.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/components/oauth-modal.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/connect-credential-modal.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx diff --git a/apps/sim/app/workspace/[workspaceId]/components/oauth-modal.test.ts b/apps/sim/app/workspace/[workspaceId]/components/oauth-modal.test.ts new file mode 100644 index 00000000000..d0ca2abfe6f --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/components/oauth-modal.test.ts @@ -0,0 +1,72 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +vi.mock('@/lib/auth/auth-client', () => ({ + client: { oauth2: { link: vi.fn() } }, + useSession: vi.fn(() => ({ data: null, isPending: false, error: null })), +})) + +vi.mock('@/lib/credentials/client-state', () => ({ + writeOAuthReturnContext: vi.fn(), +})) + +vi.mock('@/hooks/queries/credentials', () => ({ + useCreateCredentialDraft: vi.fn(() => ({ mutateAsync: vi.fn(), isPending: false })), +})) + +vi.mock('@/lib/oauth', () => ({ + getCanonicalScopesForProvider: vi.fn(() => []), + getProviderIdFromServiceId: vi.fn((id: string) => id), + OAUTH_PROVIDERS: {}, + parseProvider: vi.fn((p: string) => ({ baseProvider: p, variant: null })), +})) + +vi.mock('@/lib/oauth/utils', () => ({ + getScopeDescription: vi.fn((s: string) => s), +})) + +import { getDefaultCredentialName } from '@/app/workspace/[workspaceId]/components/oauth-modal' + +describe('getDefaultCredentialName', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('uses the user name when available', () => { + expect(getDefaultCredentialName('Waleed', 'Google Drive', 0)).toBe("Waleed's Google Drive 1") + }) + + it('increments the number based on existing credential count', () => { + expect(getDefaultCredentialName('Waleed', 'Google Drive', 2)).toBe("Waleed's Google Drive 3") + }) + + it('falls back to "My" when user name is null', () => { + expect(getDefaultCredentialName(null, 'Slack', 0)).toBe('My Slack 1') + }) + + it('falls back to "My" when user name is undefined', () => { + expect(getDefaultCredentialName(undefined, 'Gmail', 1)).toBe('My Gmail 2') + }) + + it('falls back to "My" when user name is empty string', () => { + expect(getDefaultCredentialName('', 'GitHub', 0)).toBe('My GitHub 1') + }) + + it('falls back to "My" when user name is whitespace-only', () => { + expect(getDefaultCredentialName(' ', 'Notion', 0)).toBe('My Notion 1') + }) + + it('trims whitespace from user name', () => { + expect(getDefaultCredentialName(' Waleed ', 'Linear', 0)).toBe("Waleed's Linear 1") + }) + + it('works with zero existing credentials', () => { + expect(getDefaultCredentialName('Alice', 'Jira', 0)).toBe("Alice's Jira 1") + }) + + it('works with many existing credentials', () => { + expect(getDefaultCredentialName('Bob', 'Slack', 9)).toBe("Bob's Slack 10") + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/components/oauth-modal.tsx b/apps/sim/app/workspace/[workspaceId]/components/oauth-modal.tsx new file mode 100644 index 00000000000..5f4e89edd09 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/components/oauth-modal.tsx @@ -0,0 +1,304 @@ +'use client' + +import { useMemo, useState } from 'react' +import { createLogger } from '@sim/logger' +import { Check } from 'lucide-react' +import { + Badge, + Button, + Input, + Label, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, +} from '@/components/emcn' +import { client, useSession } from '@/lib/auth/auth-client' +import type { OAuthReturnContext } from '@/lib/credentials/client-state' +import { writeOAuthReturnContext } from '@/lib/credentials/client-state' +import { + getCanonicalScopesForProvider, + getProviderIdFromServiceId, + OAUTH_PROVIDERS, + type OAuthProvider, + parseProvider, +} from '@/lib/oauth' +import { getScopeDescription } from '@/lib/oauth/utils' +import { useCreateCredentialDraft } from '@/hooks/queries/credentials' + +const logger = createLogger('OAuthModal') +const EMPTY_SCOPES: string[] = [] + +/** + * Generates a default credential display name. + * Format: "{User}'s {Provider} {N}" or "My {Provider} {N}" when no user name is available. + */ +export function getDefaultCredentialName( + userName: string | null | undefined, + providerName: string, + credentialCount: number +): string { + const trimmed = userName?.trim() + const num = credentialCount + 1 + if (trimmed) { + return `${trimmed}'s ${providerName} ${num}` + } + return `My ${providerName} ${num}` +} + +interface OAuthModalBaseProps { + isOpen: boolean + onClose: () => void + provider: OAuthProvider + serviceId: string +} + +type OAuthModalConnectProps = OAuthModalBaseProps & { + mode: 'connect' + workspaceId: string + credentialCount: number +} & ( + | { workflowId: string; knowledgeBaseId?: never } + | { workflowId?: never; knowledgeBaseId: string } + ) + +interface OAuthModalReauthorizeProps extends OAuthModalBaseProps { + mode: 'reauthorize' + toolName: string + requiredScopes?: string[] + newScopes?: string[] + onConnect?: () => Promise | void +} + +export type OAuthModalProps = OAuthModalConnectProps | OAuthModalReauthorizeProps + +export function OAuthModal(props: OAuthModalProps) { + const { isOpen, onClose, provider, serviceId, mode } = props + + const isConnect = mode === 'connect' + const credentialCount = isConnect ? props.credentialCount : 0 + const workspaceId = isConnect ? props.workspaceId : '' + const workflowId = isConnect ? props.workflowId : undefined + const knowledgeBaseId = isConnect ? props.knowledgeBaseId : undefined + const toolName = !isConnect ? props.toolName : '' + const requiredScopes = !isConnect ? (props.requiredScopes ?? EMPTY_SCOPES) : EMPTY_SCOPES + const newScopes = !isConnect ? (props.newScopes ?? EMPTY_SCOPES) : EMPTY_SCOPES + const onConnectOverride = !isConnect ? props.onConnect : undefined + + const { data: session } = useSession() + const [error, setError] = useState(null) + const createDraft = useCreateCredentialDraft() + + const { providerName, ProviderIcon } = useMemo(() => { + const { baseProvider } = parseProvider(provider) + const baseProviderConfig = OAUTH_PROVIDERS[baseProvider] + let name = baseProviderConfig?.name || provider + let Icon = baseProviderConfig?.icon || (() => null) + if (baseProviderConfig) { + for (const [key, service] of Object.entries(baseProviderConfig.services)) { + if (key === serviceId || service.providerId === provider) { + name = service.name + Icon = service.icon + break + } + } + } + return { providerName: name, ProviderIcon: Icon } + }, [provider, serviceId]) + + const providerId = getProviderIdFromServiceId(serviceId) + + const [displayName, setDisplayName] = useState(() => + isConnect ? getDefaultCredentialName(session?.user?.name, providerName, credentialCount) : '' + ) + + const newScopesSet = useMemo( + () => + new Set( + newScopes.filter( + (scope) => !scope.includes('userinfo.email') && !scope.includes('userinfo.profile') + ) + ), + [newScopes] + ) + + const displayScopes = useMemo(() => { + if (isConnect) { + return getCanonicalScopesForProvider(providerId).filter( + (scope) => !scope.includes('userinfo.email') && !scope.includes('userinfo.profile') + ) + } + const filtered = requiredScopes.filter( + (scope) => !scope.includes('userinfo.email') && !scope.includes('userinfo.profile') + ) + return filtered.sort((a, b) => { + const aIsNew = newScopesSet.has(a) + const bIsNew = newScopesSet.has(b) + if (aIsNew && !bIsNew) return -1 + if (!aIsNew && bIsNew) return 1 + return 0 + }) + }, [isConnect, providerId, requiredScopes, newScopesSet]) + + const handleClose = () => { + setError(null) + onClose() + } + + const handleConnect = async () => { + setError(null) + + try { + if (isConnect) { + const trimmedName = displayName.trim() + if (!trimmedName) { + setError('Display name is required.') + return + } + + await createDraft.mutateAsync({ + workspaceId, + providerId, + displayName: trimmedName, + }) + + const baseContext = { + displayName: trimmedName, + providerId, + preCount: credentialCount, + workspaceId, + requestedAt: Date.now(), + } + + const returnContext: OAuthReturnContext = knowledgeBaseId + ? { ...baseContext, origin: 'kb-connectors' as const, knowledgeBaseId } + : { ...baseContext, origin: 'workflow' as const, workflowId: workflowId! } + + writeOAuthReturnContext(returnContext) + } + + if (!isConnect && onConnectOverride) { + await onConnectOverride() + onClose() + return + } + + if (!isConnect) { + logger.info('Linking OAuth2:', { + providerId, + requiredScopes, + hasNewScopes: newScopes.length > 0, + }) + } + + if (providerId === 'trello') { + if (!isConnect) onClose() + window.location.href = '/api/auth/trello/authorize' + return + } + + if (providerId === 'shopify') { + if (!isConnect) onClose() + const returnUrl = encodeURIComponent(window.location.href) + window.location.href = `/api/auth/shopify/authorize?returnUrl=${returnUrl}` + return + } + + await client.oauth2.link({ providerId, callbackURL: window.location.href }) + handleClose() + } catch (err) { + logger.error('Failed to initiate OAuth connection', { error: err }) + setError('Failed to connect. Please try again.') + } + } + + const isPending = isConnect && createDraft.isPending + const isConnectDisabled = isConnect ? !displayName.trim() || Boolean(isPending) : false + + const subtitle = isConnect + ? `Grant access to use ${providerName} in your ${knowledgeBaseId ? 'knowledge base' : 'workflow'}` + : `The "${toolName}" tool requires access to your account` + + return ( + !open && handleClose()}> + + Connect {providerName} + +
+
+
+ +
+
+

+ Connect your {providerName} account +

+

{subtitle}

+
+
+ + {displayScopes.length > 0 && ( +
+
+

+ Permissions requested +

+
+
    + {displayScopes.map((scope) => ( +
  • +
    + +
    +
    + {getScopeDescription(scope)} + {!isConnect && newScopesSet.has(scope) && ( + + New + + )} +
    +
  • + ))} +
+
+ )} + + {isConnect && ( +
+ + { + setDisplayName(e.target.value) + setError(null) + }} + onKeyDown={(e) => { + if (e.key === 'Enter' && !isPending) void handleConnect() + }} + placeholder={`My ${providerName} account`} + autoComplete='off' + data-lpignore='true' + className='mt-1.5' + /> +
+ )} + + {error &&

{error}

} +
+
+ + + + +
+
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx index 9e266ec4e80..223091360bc 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx @@ -21,8 +21,8 @@ import { } from '@/components/emcn' import { consumeOAuthReturnContext } from '@/lib/credentials/client-state' import { getProviderIdFromServiceId, type OAuthProvider } from '@/lib/oauth' +import { OAuthModal } from '@/app/workspace/[workspaceId]/components/oauth-modal' import { ConnectorSelectorField } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/components/connector-selector-field' -import { ConnectCredentialModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/connect-credential-modal' import { getDependsOnFields } from '@/blocks/utils' import { CONNECTOR_REGISTRY } from '@/connectors/registry' import type { ConnectorConfig, ConnectorConfigField } from '@/connectors/types' @@ -553,7 +553,8 @@ export function AddConnectorModal({ open, onOpenChange, knowledgeBaseId }: AddCo connectorConfig && connectorConfig.auth.mode === 'oauth' && connectorProviderId && ( - { consumeOAuthReturnContext() diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx index 37049a212d2..578b6df254e 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx @@ -35,9 +35,8 @@ import { type OAuthProvider, } from '@/lib/oauth' import { getMissingRequiredScopes } from '@/lib/oauth/utils' +import { OAuthModal } from '@/app/workspace/[workspaceId]/components/oauth-modal' import { EditConnectorModal } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/edit-connector-modal' -import { ConnectCredentialModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/connect-credential-modal' -import { OAuthRequiredModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal' import { CONNECTOR_REGISTRY } from '@/connectors/registry' import type { ConnectorData, SyncLogData } from '@/hooks/queries/kb/connectors' import { @@ -520,7 +519,8 @@ function ConnectorCard({ )} {showOAuthModal && serviceId && providerId && !connector.credentialId && ( - { consumeOAuthReturnContext() @@ -535,7 +535,8 @@ function ConnectorCard({ )} {showOAuthModal && serviceId && providerId && connector.credentialId && ( - { consumeOAuthReturnContext() diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/connect-credential-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/connect-credential-modal.tsx deleted file mode 100644 index c6574780423..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/connect-credential-modal.tsx +++ /dev/null @@ -1,222 +0,0 @@ -'use client' - -import { useMemo, useState } from 'react' -import { createLogger } from '@sim/logger' -import { Check } from 'lucide-react' -import { - Button, - Input, - Label, - Modal, - ModalBody, - ModalContent, - ModalFooter, - ModalHeader, -} from '@/components/emcn' -import { client } from '@/lib/auth/auth-client' -import type { OAuthReturnContext } from '@/lib/credentials/client-state' -import { writeOAuthReturnContext } from '@/lib/credentials/client-state' -import { - getCanonicalScopesForProvider, - getProviderIdFromServiceId, - OAUTH_PROVIDERS, - type OAuthProvider, - parseProvider, -} from '@/lib/oauth' -import { getScopeDescription } from '@/lib/oauth/utils' -import { useCreateCredentialDraft } from '@/hooks/queries/credentials' - -const logger = createLogger('ConnectCredentialModal') - -interface ConnectCredentialModalBaseProps { - isOpen: boolean - onClose: () => void - provider: OAuthProvider - serviceId: string - workspaceId: string - /** Number of existing credentials for this provider — used to detect a successful new connection. */ - credentialCount: number -} - -export type ConnectCredentialModalProps = ConnectCredentialModalBaseProps & - ( - | { workflowId: string; knowledgeBaseId?: never } - | { workflowId?: never; knowledgeBaseId: string } - ) - -export function ConnectCredentialModal({ - isOpen, - onClose, - provider, - serviceId, - workspaceId, - workflowId, - knowledgeBaseId, - credentialCount, -}: ConnectCredentialModalProps) { - const [displayName, setDisplayName] = useState('') - const [error, setError] = useState(null) - - const createDraft = useCreateCredentialDraft() - - const { providerName, ProviderIcon } = useMemo(() => { - const { baseProvider } = parseProvider(provider) - const baseProviderConfig = OAUTH_PROVIDERS[baseProvider] - let name = baseProviderConfig?.name || provider - let Icon = baseProviderConfig?.icon || (() => null) - if (baseProviderConfig) { - for (const [key, service] of Object.entries(baseProviderConfig.services)) { - if (key === serviceId || service.providerId === provider) { - name = service.name - Icon = service.icon - break - } - } - } - return { providerName: name, ProviderIcon: Icon } - }, [provider, serviceId]) - - const providerId = getProviderIdFromServiceId(serviceId) - - const displayScopes = useMemo( - () => - getCanonicalScopesForProvider(providerId).filter( - (scope) => !scope.includes('userinfo.email') && !scope.includes('userinfo.profile') - ), - [providerId] - ) - - const handleClose = () => { - setDisplayName('') - setError(null) - onClose() - } - - const handleConnect = async () => { - const trimmedName = displayName.trim() - if (!trimmedName) { - setError('Display name is required.') - return - } - - setError(null) - - try { - await createDraft.mutateAsync({ workspaceId, providerId, displayName: trimmedName }) - - const baseContext = { - displayName: trimmedName, - providerId, - preCount: credentialCount, - workspaceId, - requestedAt: Date.now(), - } - - const returnContext: OAuthReturnContext = knowledgeBaseId - ? { ...baseContext, origin: 'kb-connectors' as const, knowledgeBaseId } - : { ...baseContext, origin: 'workflow' as const, workflowId: workflowId! } - - writeOAuthReturnContext(returnContext) - - if (providerId === 'trello') { - window.location.href = '/api/auth/trello/authorize' - return - } - - if (providerId === 'shopify') { - const returnUrl = encodeURIComponent(window.location.href) - window.location.href = `/api/auth/shopify/authorize?returnUrl=${returnUrl}` - return - } - - await client.oauth2.link({ providerId, callbackURL: window.location.href }) - handleClose() - } catch (err) { - logger.error('Failed to initiate OAuth connection', { error: err }) - setError('Failed to connect. Please try again.') - } - } - - const isPending = createDraft.isPending - - return ( - !open && handleClose()}> - - Connect {providerName} - -
-
-
- -
-
-

- Connect your {providerName} account -

-

- Grant access to use {providerName} in your workflow -

-
-
- - {displayScopes.length > 0 && ( -
-
-

- Permissions requested -

-
-
    - {displayScopes.map((scope) => ( -
  • -
    - -
    -
    - {getScopeDescription(scope)} -
    -
  • - ))} -
-
- )} - -
- - { - setDisplayName(e.target.value) - setError(null) - }} - onKeyDown={(e) => { - if (e.key === 'Enter' && !isPending) void handleConnect() - }} - placeholder={`My ${providerName} account`} - autoComplete='off' - data-lpignore='true' - className='mt-1.5' - /> -
- - {error &&

{error}

} -
-
- - - - -
-
- ) -} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx deleted file mode 100644 index 61e95bc3cc9..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx +++ /dev/null @@ -1,190 +0,0 @@ -'use client' - -import { useMemo, useState } from 'react' -import { createLogger } from '@sim/logger' -import { Check } from 'lucide-react' -import { - Badge, - Button, - Modal, - ModalBody, - ModalContent, - ModalFooter, - ModalHeader, -} from '@/components/emcn' -import { client } from '@/lib/auth/auth-client' -import { - getProviderIdFromServiceId, - getScopeDescription, - OAUTH_PROVIDERS, - type OAuthProvider, - parseProvider, -} from '@/lib/oauth' - -const logger = createLogger('OAuthRequiredModal') - -export interface OAuthRequiredModalProps { - isOpen: boolean - onClose: () => void - provider: OAuthProvider - toolName: string - requiredScopes?: string[] - serviceId: string - newScopes?: string[] - onConnect?: () => Promise | void -} - -export function OAuthRequiredModal({ - isOpen, - onClose, - provider, - toolName, - requiredScopes = [], - serviceId, - newScopes = [], - onConnect, -}: OAuthRequiredModalProps) { - const [error, setError] = useState(null) - const { baseProvider } = parseProvider(provider) - const baseProviderConfig = OAUTH_PROVIDERS[baseProvider] - - let providerName = baseProviderConfig?.name || provider - let ProviderIcon = baseProviderConfig?.icon || (() => null) - - if (baseProviderConfig) { - for (const [key, service] of Object.entries(baseProviderConfig.services)) { - if (key === serviceId || service.providerId === provider) { - providerName = service.name - ProviderIcon = service.icon - break - } - } - } - - const newScopesSet = useMemo( - () => - new Set( - (newScopes || []).filter( - (scope) => !scope.includes('userinfo.email') && !scope.includes('userinfo.profile') - ) - ), - [newScopes] - ) - - const displayScopes = useMemo(() => { - const filtered = requiredScopes.filter( - (scope) => !scope.includes('userinfo.email') && !scope.includes('userinfo.profile') - ) - return filtered.sort((a, b) => { - const aIsNew = newScopesSet.has(a) - const bIsNew = newScopesSet.has(b) - if (aIsNew && !bIsNew) return -1 - if (!aIsNew && bIsNew) return 1 - return 0 - }) - }, [requiredScopes, newScopesSet]) - - const handleConnectDirectly = async () => { - setError(null) - - try { - if (onConnect) { - await onConnect() - onClose() - return - } - - const providerId = getProviderIdFromServiceId(serviceId) - - logger.info('Linking OAuth2:', { - providerId, - requiredScopes, - hasNewScopes: newScopes.length > 0, - }) - - if (providerId === 'trello') { - onClose() - window.location.href = '/api/auth/trello/authorize' - return - } - - if (providerId === 'shopify') { - onClose() - const returnUrl = encodeURIComponent(window.location.href) - window.location.href = `/api/auth/shopify/authorize?returnUrl=${returnUrl}` - return - } - - await client.oauth2.link({ - providerId, - callbackURL: window.location.href, - }) - onClose() - } catch (err) { - logger.error('Error initiating OAuth flow:', { error: err }) - setError('Failed to connect. Please try again.') - } - } - - return ( - !open && onClose()}> - - Connect {providerName} - -
-
-
- -
-
-

- Connect your {providerName} account -

-

- The "{toolName}" tool requires access to your account -

-
-
- - {displayScopes.length > 0 && ( -
-
-

- Permissions requested -

-
-
    - {displayScopes.map((scope) => ( -
  • -
    - -
    -
    - {getScopeDescription(scope)} - {newScopesSet.has(scope) && ( - - New - - )} -
    -
  • - ))} -
-
- )} - - {error &&

{error}

} -
-
- - - - -
-
- ) -} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx index 26c897cf3d3..fcdd82b37cb 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx @@ -16,8 +16,7 @@ import { parseProvider, } from '@/lib/oauth' import { getMissingRequiredScopes } from '@/lib/oauth/utils' -import { ConnectCredentialModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/connect-credential-modal' -import { OAuthRequiredModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal' +import { OAuthModal } from '@/app/workspace/[workspaceId]/components/oauth-modal' import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' import type { SubBlockConfig } from '@/blocks/types' @@ -378,7 +377,8 @@ export function CredentialSelector({ )} {showConnectModal && ( - setShowConnectModal(false)} provider={provider} @@ -390,7 +390,8 @@ export function CredentialSelector({ )} {showOAuthModal && ( - { consumeOAuthReturnContext() diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/credential-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/credential-selector.tsx index 8193f36ff9d..8758bec1557 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/credential-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/credential-selector.tsx @@ -15,8 +15,7 @@ import { parseProvider, } from '@/lib/oauth' import { getMissingRequiredScopes } from '@/lib/oauth/utils' -import { ConnectCredentialModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/connect-credential-modal' -import { OAuthRequiredModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal' +import { OAuthModal } from '@/app/workspace/[workspaceId]/components/oauth-modal' import { useWorkspaceCredential } from '@/hooks/queries/credentials' import { useOAuthCredentials } from '@/hooks/queries/oauth/oauth-credentials' import { useWorkflowMap } from '@/hooks/queries/workflows' @@ -245,7 +244,8 @@ export function ToolCredentialSelector({ )} {showConnectModal && ( - setShowConnectModal(false)} provider={provider} @@ -257,7 +257,8 @@ export function ToolCredentialSelector({ )} {showOAuthModal && ( - { consumeOAuthReturnContext() diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index 55b6d056705..5e9bf92463b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -23,6 +23,7 @@ import { consumeOAuthReturnContext, writeOAuthReturnContext } from '@/lib/creden import type { OAuthProvider } from '@/lib/oauth' import { BLOCK_DIMENSIONS, CONTAINER_DIMENSIONS } from '@/lib/workflows/blocks/block-dimensions' import { TriggerUtils } from '@/lib/workflows/triggers/triggers' +import { OAuthModal } from '@/app/workspace/[workspaceId]/components/oauth-modal' import { useWorkspacePermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { CommandList, @@ -97,11 +98,6 @@ const LazyChat = lazy(() => default: mod.Chat, })) ) -const LazyOAuthRequiredModal = lazy(() => - import( - '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal' - ).then((mod) => ({ default: mod.OAuthRequiredModal })) -) const logger = createLogger('Workflow') @@ -4127,20 +4123,19 @@ const WorkflowContent = React.memo( {(!embedded || sandbox) && } {!embedded && !sandbox && oauthModal && ( - - { - consumeOAuthReturnContext() - setOauthModal(null) - }} - provider={oauthModal.provider} - toolName={oauthModal.providerName} - serviceId={oauthModal.serviceId} - requiredScopes={oauthModal.requiredScopes} - newScopes={oauthModal.newScopes} - /> - + { + consumeOAuthReturnContext() + setOauthModal(null) + }} + provider={oauthModal.provider} + toolName={oauthModal.providerName} + serviceId={oauthModal.serviceId} + requiredScopes={oauthModal.requiredScopes} + newScopes={oauthModal.newScopes} + /> )} ) From 8527ae5d3bcb7655a9464ddf8a4fecd362b5ef47 Mon Sep 17 00:00:00 2001 From: Waleed Date: Wed, 1 Apr 2026 16:27:54 -0700 Subject: [PATCH 05/16] feat(providers): server-side credential hiding for Azure and Bedrock (#3884) * fix: allow Bedrock provider to use AWS SDK default credential chain Remove hard requirement for explicit AWS credentials in Bedrock provider. When access key and secret key are not provided, the AWS SDK automatically falls back to its default credential chain (env vars, instance profile, ECS task role, EKS IRSA, SSO). Closes #3694 Signed-off-by: majiayu000 <1835304752@qq.com> * fix: add partial credential guard for Bedrock provider Reject configurations where only one of bedrockAccessKeyId or bedrockSecretKey is provided, preventing silent fallback to the default credential chain with a potentially different identity. Add tests covering all credential configuration scenarios. Signed-off-by: majiayu000 <1835304752@qq.com> * fix: clean up bedrock test lint and dead code Remove unused config parameter and dead _lastConfig assignment from mock factory. Break long mockReturnValue chain to satisfy biome line-length rule. Signed-off-by: majiayu000 <1835304752@qq.com> * fix: address greptile review feedback on PR #3708 Use BedrockRuntimeClientConfig from SDK instead of inline type. Add default return value for prepareToolsWithUsageControl mock. Signed-off-by: majiayu000 <1835304752@qq.com> * feat(providers): server-side credential hiding for Azure and Bedrock * fix(providers): revert Bedrock credential fields to required with original placeholders * fix(blocks): add hideWhenEnvSet to getProviderCredentialSubBlocks for Azure and Bedrock * fix(agent): use getProviderCredentialSubBlocks() instead of duplicating credential subblocks * fix(blocks): consolidate Vertex credential into shared factory with basic/advanced mode * fix(types): resolve pre-existing TypeScript errors across auth, secrets, and copilot * lint * improvement(blocks): make Vertex AI project ID a password field * fix(blocks): preserve vertexCredential subblock ID for backwards compatibility * fix(blocks): follow canonicalParamId pattern correctly for vertex credential subblocks * fix(blocks): keep vertexCredential subblock ID stable to preserve saved workflow state * fix(blocks): add canonicalParamId to vertexCredential basic subblock to complete the swap pair * fix types * more types --------- Signed-off-by: majiayu000 <1835304752@qq.com> Co-authored-by: majiayu000 <1835304752@qq.com> Co-authored-by: Vikhyath Mondreti --- .cursor/skills/add-hosted-key/SKILL.md | 2 +- apps/sim/.env.example | 8 ++ .../app/api/tools/secrets_manager/utils.ts | 5 +- .../hooks/use-editor-subblock-layout.ts | 6 +- .../workflow-block/workflow-block.tsx | 4 +- apps/sim/blocks/blocks/agent.ts | 136 +----------------- apps/sim/blocks/blocks/function.ts | 2 +- apps/sim/blocks/types.ts | 3 +- apps/sim/blocks/utils.ts | 44 +++++- apps/sim/lib/api-key/byok.ts | 11 +- apps/sim/lib/auth/auth.ts | 5 +- .../orchestrator/tool-executor/index.ts | 4 +- apps/sim/lib/copilot/vfs/serializers.ts | 4 +- apps/sim/lib/core/config/env.ts | 4 + apps/sim/lib/core/config/feature-flags.ts | 8 ++ .../sim/lib/workflows/subblocks/visibility.ts | 18 ++- apps/sim/providers/azure-anthropic/index.ts | 17 ++- apps/sim/providers/azure-openai/index.ts | 68 +++++---- apps/sim/providers/bedrock/index.test.ts | 115 +++++++++++++++ apps/sim/providers/bedrock/index.ts | 34 +++-- apps/sim/providers/utils.ts | 9 +- apps/sim/serializer/index.ts | 4 +- apps/sim/tools/params.ts | 8 +- bun.lock | 1 - helm/sim/examples/values-aws.yaml | 4 + helm/sim/examples/values-azure.yaml | 11 ++ helm/sim/values.yaml | 25 +++- 27 files changed, 343 insertions(+), 217 deletions(-) create mode 100644 apps/sim/providers/bedrock/index.test.ts diff --git a/.cursor/skills/add-hosted-key/SKILL.md b/.cursor/skills/add-hosted-key/SKILL.md index a6e0f07052d..2181910f8d9 100644 --- a/.cursor/skills/add-hosted-key/SKILL.md +++ b/.cursor/skills/add-hosted-key/SKILL.md @@ -192,7 +192,7 @@ In the block config (`blocks/blocks/{service}.ts`), add `hideWhenHosted: true` t }, ``` -The visibility is controlled by `isSubBlockHiddenByHostedKey()` in `lib/workflows/subblocks/visibility.ts`, which checks the `isHosted` feature flag. +The visibility is controlled by `isSubBlockHidden()` in `lib/workflows/subblocks/visibility.ts`, which checks both the `isHosted` feature flag (`hideWhenHosted`) and optional env var conditions (`hideWhenEnvSet`). ### Excluding Specific Operations from Hosted Key Support diff --git a/apps/sim/.env.example b/apps/sim/.env.example index 8db5d82c1af..fe9514e358e 100644 --- a/apps/sim/.env.example +++ b/apps/sim/.env.example @@ -29,6 +29,14 @@ API_ENCRYPTION_KEY=your_api_encryption_key # Use `openssl rand -hex 32` to gener # VLLM_BASE_URL=http://localhost:8000 # Base URL for your self-hosted vLLM (OpenAI-compatible) # VLLM_API_KEY= # Optional bearer token if your vLLM instance requires auth # FIREWORKS_API_KEY= # Optional Fireworks AI API key for model listing +# NEXT_PUBLIC_BEDROCK_DEFAULT_CREDENTIALS=true # Set when using AWS default credential chain (IAM roles, ECS task roles, IRSA). Hides credential fields in Agent block UI. +# AZURE_OPENAI_ENDPOINT= # Azure OpenAI endpoint (hides field in UI when set alongside NEXT_PUBLIC_AZURE_CONFIGURED) +# AZURE_OPENAI_API_KEY= # Azure OpenAI API key +# AZURE_OPENAI_API_VERSION= # Azure OpenAI API version +# AZURE_ANTHROPIC_ENDPOINT= # Azure Anthropic endpoint (AI Foundry) +# AZURE_ANTHROPIC_API_KEY= # Azure Anthropic API key +# AZURE_ANTHROPIC_API_VERSION= # Azure Anthropic API version (e.g., 2023-06-01) +# NEXT_PUBLIC_AZURE_CONFIGURED=true # Set when Azure credentials are pre-configured above. Hides endpoint/key/version fields in Agent block UI. # Admin API (Optional - for self-hosted GitOps) # ADMIN_API_KEY= # Use `openssl rand -hex 32` to generate. Enables admin API for workflow export/import. diff --git a/apps/sim/app/api/tools/secrets_manager/utils.ts b/apps/sim/app/api/tools/secrets_manager/utils.ts index 331197fceca..dacbbc30be1 100644 --- a/apps/sim/app/api/tools/secrets_manager/utils.ts +++ b/apps/sim/app/api/tools/secrets_manager/utils.ts @@ -1,3 +1,4 @@ +import type { SecretListEntry, Tag } from '@aws-sdk/client-secrets-manager' import { CreateSecretCommand, DeleteSecretCommand, @@ -61,7 +62,7 @@ export async function listSecrets( }) const response = await client.send(command) - const secrets = (response.SecretList ?? []).map((secret) => ({ + const secrets = (response.SecretList ?? []).map((secret: SecretListEntry) => ({ name: secret.Name ?? '', arn: secret.ARN ?? '', description: secret.Description ?? null, @@ -69,7 +70,7 @@ export async function listSecrets( lastChangedDate: secret.LastChangedDate?.toISOString() ?? null, lastAccessedDate: secret.LastAccessedDate?.toISOString() ?? null, rotationEnabled: secret.RotationEnabled ?? false, - tags: secret.Tags?.map((t) => ({ key: t.Key ?? '', value: t.Value ?? '' })) ?? [], + tags: secret.Tags?.map((t: Tag) => ({ key: t.Key ?? '', value: t.Value ?? '' })) ?? [], })) return { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-editor-subblock-layout.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-editor-subblock-layout.ts index 0cf118e428e..1ebc925262e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-editor-subblock-layout.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-editor-subblock-layout.ts @@ -3,7 +3,7 @@ import { buildCanonicalIndex, evaluateSubBlockCondition, isSubBlockFeatureEnabled, - isSubBlockHiddenByHostedKey, + isSubBlockHidden, isSubBlockVisibleForMode, } from '@/lib/workflows/subblocks/visibility' import type { BlockConfig, SubBlockConfig, SubBlockType } from '@/blocks/types' @@ -109,8 +109,8 @@ export function useEditorSubblockLayout( // Check required feature if specified - declarative feature gating if (!isSubBlockFeatureEnabled(block)) return false - // Hide tool API key fields when hosted - if (isSubBlockHiddenByHostedKey(block)) return false + // Hide tool API key fields when hosted or when env var is set + if (isSubBlockHidden(block)) return false // Special handling for trigger-config type (legacy trigger configuration UI) if (block.type === ('trigger-config' as SubBlockType)) { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx index 70ddefc95e8..b7b04f38bb2 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx @@ -17,7 +17,7 @@ import { evaluateSubBlockCondition, hasAdvancedValues, isSubBlockFeatureEnabled, - isSubBlockHiddenByHostedKey, + isSubBlockHidden, isSubBlockVisibleForMode, resolveDependencyValue, } from '@/lib/workflows/subblocks/visibility' @@ -980,7 +980,7 @@ export const WorkflowBlock = memo(function WorkflowBlock({ if (block.hidden) return false if (block.hideFromPreview) return false if (!isSubBlockFeatureEnabled(block)) return false - if (isSubBlockHiddenByHostedKey(block)) return false + if (isSubBlockHidden(block)) return false const isPureTriggerBlock = config?.triggers?.enabled && config.category === 'triggers' diff --git a/apps/sim/blocks/blocks/agent.ts b/apps/sim/blocks/blocks/agent.ts index b44ef4658c3..caad30d9e2d 100644 --- a/apps/sim/blocks/blocks/agent.ts +++ b/apps/sim/blocks/blocks/agent.ts @@ -1,9 +1,12 @@ import { createLogger } from '@sim/logger' import { AgentIcon } from '@/components/icons' -import { getScopesForService } from '@/lib/oauth/utils' import type { BlockConfig } from '@/blocks/types' import { AuthMode, IntegrationType } from '@/blocks/types' -import { getApiKeyCondition, getModelOptions, RESPONSE_FORMAT_WAND_CONFIG } from '@/blocks/utils' +import { + getModelOptions, + getProviderCredentialSubBlocks, + RESPONSE_FORMAT_WAND_CONFIG, +} from '@/blocks/utils' import { getBaseModelProviders, getMaxTemperature, @@ -12,7 +15,6 @@ import { getModelsWithReasoningEffort, getModelsWithThinking, getModelsWithVerbosity, - getProviderModels, getReasoningEffortValuesForModel, getThinkingLevelsForModel, getVerbosityValuesForModel, @@ -23,9 +25,6 @@ import { useSubBlockStore } from '@/stores/workflows/subblock/store' import type { ToolResponse } from '@/tools/types' const logger = createLogger('AgentBlock') -const VERTEX_MODELS = getProviderModels('vertex') -const BEDROCK_MODELS = getProviderModels('bedrock') -const AZURE_MODELS = [...getProviderModels('azure-openai'), ...getProviderModels('azure-anthropic')] const MODELS_WITH_REASONING_EFFORT = getModelsWithReasoningEffort() const MODELS_WITH_VERBOSITY = getModelsWithVerbosity() const MODELS_WITH_THINKING = getModelsWithThinking() @@ -134,34 +133,6 @@ Return ONLY the JSON array.`, defaultValue: 'claude-sonnet-4-5', options: getModelOptions, }, - { - id: 'vertexCredential', - title: 'Google Cloud Account', - type: 'oauth-input', - serviceId: 'vertex-ai', - canonicalParamId: 'oauthCredential', - mode: 'basic', - requiredScopes: getScopesForService('vertex-ai'), - placeholder: 'Select Google Cloud account', - required: true, - condition: { - field: 'model', - value: VERTEX_MODELS, - }, - }, - { - id: 'manualCredential', - title: 'Google Cloud Account', - type: 'short-input', - canonicalParamId: 'oauthCredential', - mode: 'advanced', - placeholder: 'Enter credential ID', - required: true, - condition: { - field: 'model', - value: VERTEX_MODELS, - }, - }, { id: 'reasoningEffort', title: 'Reasoning Effort', @@ -318,100 +289,7 @@ Return ONLY the JSON array.`, }, }, - { - id: 'azureEndpoint', - title: 'Azure Endpoint', - type: 'short-input', - password: true, - placeholder: 'https://your-resource.services.ai.azure.com', - connectionDroppable: false, - condition: { - field: 'model', - value: AZURE_MODELS, - }, - }, - { - id: 'azureApiVersion', - title: 'Azure API Version', - type: 'short-input', - placeholder: 'Enter API version', - connectionDroppable: false, - condition: { - field: 'model', - value: AZURE_MODELS, - }, - }, - { - id: 'vertexProject', - title: 'Vertex AI Project', - type: 'short-input', - placeholder: 'your-gcp-project-id', - connectionDroppable: false, - required: true, - condition: { - field: 'model', - value: VERTEX_MODELS, - }, - }, - { - id: 'vertexLocation', - title: 'Vertex AI Location', - type: 'short-input', - placeholder: 'us-central1', - connectionDroppable: false, - required: true, - condition: { - field: 'model', - value: VERTEX_MODELS, - }, - }, - { - id: 'bedrockAccessKeyId', - title: 'AWS Access Key ID', - type: 'short-input', - password: true, - placeholder: 'Enter your AWS Access Key ID', - connectionDroppable: false, - required: true, - condition: { - field: 'model', - value: BEDROCK_MODELS, - }, - }, - { - id: 'bedrockSecretKey', - title: 'AWS Secret Access Key', - type: 'short-input', - password: true, - placeholder: 'Enter your AWS Secret Access Key', - connectionDroppable: false, - required: true, - condition: { - field: 'model', - value: BEDROCK_MODELS, - }, - }, - { - id: 'bedrockRegion', - title: 'AWS Region', - type: 'short-input', - placeholder: 'us-east-1', - connectionDroppable: false, - condition: { - field: 'model', - value: BEDROCK_MODELS, - }, - }, - { - id: 'apiKey', - title: 'API Key', - type: 'short-input', - placeholder: 'Enter your API key', - password: true, - connectionDroppable: false, - required: true, - condition: getApiKeyCondition(), - }, + ...getProviderCredentialSubBlocks(), { id: 'tools', title: 'Tools', @@ -661,7 +539,7 @@ Return ONLY the JSON array.`, apiKey: { type: 'string', description: 'Provider API key' }, azureEndpoint: { type: 'string', description: 'Azure endpoint URL' }, azureApiVersion: { type: 'string', description: 'Azure API version' }, - oauthCredential: { type: 'string', description: 'OAuth credential for Vertex AI' }, + vertexCredential: { type: 'string', description: 'OAuth credential for Vertex AI' }, vertexProject: { type: 'string', description: 'Google Cloud project ID for Vertex AI' }, vertexLocation: { type: 'string', description: 'Google Cloud location for Vertex AI' }, bedrockAccessKeyId: { type: 'string', description: 'AWS Access Key ID for Bedrock' }, diff --git a/apps/sim/blocks/blocks/function.ts b/apps/sim/blocks/blocks/function.ts index bdfc15ab11c..341f6482456 100644 --- a/apps/sim/blocks/blocks/function.ts +++ b/apps/sim/blocks/blocks/function.ts @@ -29,7 +29,7 @@ export const FunctionBlock: BlockConfig = { ], placeholder: 'Select language', value: () => CodeLanguage.JavaScript, - requiresFeature: 'NEXT_PUBLIC_E2B_ENABLED', + showWhenEnvSet: 'NEXT_PUBLIC_E2B_ENABLED', }, { id: 'code', diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts index 614c686ec54..a471af5aada 100644 --- a/apps/sim/blocks/types.ts +++ b/apps/sim/blocks/types.ts @@ -327,8 +327,9 @@ export interface SubBlockConfig { connectionDroppable?: boolean hidden?: boolean hideFromPreview?: boolean // Hide this subblock from the workflow block preview - requiresFeature?: string // Environment variable name that must be truthy for this subblock to be visible + showWhenEnvSet?: string // Show this subblock only when the named NEXT_PUBLIC_ env var is truthy hideWhenHosted?: boolean // Hide this subblock when running on hosted sim + hideWhenEnvSet?: string // Hide this subblock when the named NEXT_PUBLIC_ env var is truthy description?: string tooltip?: string // Tooltip text displayed via info icon next to the title value?: (params: Record) => string diff --git a/apps/sim/blocks/utils.ts b/apps/sim/blocks/utils.ts index e06dbdf5add..b68ac4cbbbe 100644 --- a/apps/sim/blocks/utils.ts +++ b/apps/sim/blocks/utils.ts @@ -1,4 +1,5 @@ -import { isHosted } from '@/lib/core/config/feature-flags' +import { isAzureConfigured, isHosted } from '@/lib/core/config/feature-flags' +import { getScopesForService } from '@/lib/oauth/utils' import type { BlockOutput, OutputFieldDefinition, SubBlockConfig } from '@/blocks/types' import { getHostedModels, @@ -8,9 +9,12 @@ import { } from '@/providers/models' import { useProvidersStore } from '@/stores/providers/store' -const VERTEX_MODELS = getProviderModels('vertex') -const BEDROCK_MODELS = getProviderModels('bedrock') -const AZURE_MODELS = [...getProviderModels('azure-openai'), ...getProviderModels('azure-anthropic')] +export const VERTEX_MODELS = getProviderModels('vertex') +export const BEDROCK_MODELS = getProviderModels('bedrock') +export const AZURE_MODELS = [ + ...getProviderModels('azure-openai'), + ...getProviderModels('azure-anthropic'), +] /** * Returns model options for combobox subblocks, combining all provider sources. @@ -105,6 +109,16 @@ function shouldRequireApiKeyForModel(model: string): boolean { return false } + if ( + isAzureConfigured && + (normalizedModel.startsWith('azure/') || + normalizedModel.startsWith('azure-openai/') || + normalizedModel.startsWith('azure-anthropic/') || + AZURE_MODELS.some((m) => m.toLowerCase() === normalizedModel)) + ) { + return false + } + if (normalizedModel.startsWith('vllm/')) { return false } @@ -158,7 +172,9 @@ export function getProviderCredentialSubBlocks(): SubBlockConfig[] { title: 'Google Cloud Account', type: 'oauth-input', serviceId: 'vertex-ai', - requiredScopes: ['https://www.googleapis.com/auth/cloud-platform'], + canonicalParamId: 'vertexCredential', + mode: 'basic', + requiredScopes: getScopesForService('vertex-ai'), placeholder: 'Select Google Cloud account', required: true, condition: { @@ -166,6 +182,19 @@ export function getProviderCredentialSubBlocks(): SubBlockConfig[] { value: VERTEX_MODELS, }, }, + { + id: 'vertexManualCredential', + title: 'Google Cloud Account', + type: 'short-input', + canonicalParamId: 'vertexCredential', + mode: 'advanced', + placeholder: 'Enter credential ID', + required: true, + condition: { + field: 'model', + value: VERTEX_MODELS, + }, + }, { id: 'apiKey', title: 'API Key', @@ -183,6 +212,7 @@ export function getProviderCredentialSubBlocks(): SubBlockConfig[] { password: true, placeholder: 'https://your-resource.services.ai.azure.com', connectionDroppable: false, + hideWhenEnvSet: 'NEXT_PUBLIC_AZURE_CONFIGURED', condition: { field: 'model', value: AZURE_MODELS, @@ -194,6 +224,7 @@ export function getProviderCredentialSubBlocks(): SubBlockConfig[] { type: 'short-input', placeholder: 'Enter API version', connectionDroppable: false, + hideWhenEnvSet: 'NEXT_PUBLIC_AZURE_CONFIGURED', condition: { field: 'model', value: AZURE_MODELS, @@ -203,6 +234,7 @@ export function getProviderCredentialSubBlocks(): SubBlockConfig[] { id: 'vertexProject', title: 'Vertex AI Project', type: 'short-input', + password: true, placeholder: 'your-gcp-project-id', connectionDroppable: false, required: true, @@ -231,6 +263,7 @@ export function getProviderCredentialSubBlocks(): SubBlockConfig[] { placeholder: 'Enter your AWS Access Key ID', connectionDroppable: false, required: true, + hideWhenEnvSet: 'NEXT_PUBLIC_BEDROCK_DEFAULT_CREDENTIALS', condition: { field: 'model', value: BEDROCK_MODELS, @@ -244,6 +277,7 @@ export function getProviderCredentialSubBlocks(): SubBlockConfig[] { placeholder: 'Enter your AWS Secret Access Key', connectionDroppable: false, required: true, + hideWhenEnvSet: 'NEXT_PUBLIC_BEDROCK_DEFAULT_CREDENTIALS', condition: { field: 'model', value: BEDROCK_MODELS, diff --git a/apps/sim/lib/api-key/byok.ts b/apps/sim/lib/api-key/byok.ts index 901d86af5fe..a2aa198e859 100644 --- a/apps/sim/lib/api-key/byok.ts +++ b/apps/sim/lib/api-key/byok.ts @@ -8,6 +8,7 @@ import { isHosted } from '@/lib/core/config/feature-flags' import { decryptSecret } from '@/lib/core/security/encryption' import { getWorkspaceById } from '@/lib/workspaces/permissions/utils' import { getHostedModels } from '@/providers/models' +import { PROVIDER_PLACEHOLDER_KEY } from '@/providers/utils' import { useProvidersStore } from '@/stores/providers/store' import type { BYOKProviderId } from '@/tools/types' @@ -95,7 +96,15 @@ export async function getApiKeyWithBYOK( const isBedrockModel = provider === 'bedrock' || model.startsWith('bedrock/') if (isBedrockModel) { - return { apiKey: 'bedrock-uses-own-credentials', isBYOK: false } + return { apiKey: PROVIDER_PLACEHOLDER_KEY, isBYOK: false } + } + + if (provider === 'azure-openai') { + return { apiKey: userProvidedKey || env.AZURE_OPENAI_API_KEY || '', isBYOK: false } + } + + if (provider === 'azure-anthropic') { + return { apiKey: userProvidedKey || env.AZURE_ANTHROPIC_API_KEY || '', isBYOK: false } } const isOpenAIModel = provider === 'openai' diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index 7bfaa64889d..69a308eccc7 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -559,12 +559,12 @@ export const auth = betterAuth({ github: { clientId: env.GITHUB_CLIENT_ID as string, clientSecret: env.GITHUB_CLIENT_SECRET as string, - scopes: ['user:email', 'repo'], + scope: ['user:email', 'repo'], }, google: { clientId: env.GOOGLE_CLIENT_ID as string, clientSecret: env.GOOGLE_CLIENT_SECRET as string, - scopes: [ + scope: [ 'https://www.googleapis.com/auth/userinfo.email', 'https://www.googleapis.com/auth/userinfo.profile', ], @@ -602,7 +602,6 @@ export const auth = betterAuth({ emailAndPassword: { enabled: true, requireEmailVerification: isEmailVerificationEnabled, - sendVerificationOnSignUp: isEmailVerificationEnabled, // Auto-send verification OTP on signup when verification is required throwOnMissingCredentials: true, throwOnInvalidCredentials: true, sendResetPassword: async ({ user, url, token }, request) => { diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts b/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts index 95252daf8bf..cf6b623717e 100644 --- a/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts +++ b/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts @@ -954,10 +954,10 @@ async function generateOAuthLink( const { headers: getHeaders } = await import('next/headers') const reqHeaders = await getHeaders() - const data = (await auth.api.oAuth2LinkAccount({ + const data = await auth.api.oAuth2LinkAccount({ body: { providerId, callbackURL }, headers: reqHeaders, - })) as { url?: string; redirect?: boolean } + }) if (!data?.url) { throw new Error('oAuth2LinkAccount did not return an authorization URL') diff --git a/apps/sim/lib/copilot/vfs/serializers.ts b/apps/sim/lib/copilot/vfs/serializers.ts index e1d282d0d55..524e8567e01 100644 --- a/apps/sim/lib/copilot/vfs/serializers.ts +++ b/apps/sim/lib/copilot/vfs/serializers.ts @@ -1,6 +1,6 @@ import { getCopilotToolDescription } from '@/lib/copilot/tool-descriptions' import { isHosted } from '@/lib/core/config/feature-flags' -import { isSubBlockHiddenByHostedKey } from '@/lib/workflows/subblocks/visibility' +import { isSubBlockHidden } from '@/lib/workflows/subblocks/visibility' import type { BlockConfig, SubBlockConfig } from '@/blocks/types' import { PROVIDER_DEFINITIONS } from '@/providers/models' import type { ToolConfig } from '@/tools/types' @@ -369,7 +369,7 @@ function serializeSubBlock(sb: SubBlockConfig): Record { * Serialize a block schema for VFS components/blocks/{type}.json */ export function serializeBlockSchema(block: BlockConfig): string { - const hiddenIds = new Set(block.subBlocks.filter(isSubBlockHiddenByHostedKey).map((sb) => sb.id)) + const hiddenIds = new Set(block.subBlocks.filter(isSubBlockHidden).map((sb) => sb.id)) const subBlocks = block.subBlocks .filter((sb) => !hiddenIds.has(sb.id)) diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index c6b05f076f8..9db115984e7 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -403,6 +403,8 @@ export const env = createEnv({ NEXT_PUBLIC_SUPPORT_EMAIL: z.string().email().optional(), // Custom support email NEXT_PUBLIC_E2B_ENABLED: z.string().optional(), + NEXT_PUBLIC_BEDROCK_DEFAULT_CREDENTIALS: z.string().optional(), // Hide Bedrock credential fields when deployment uses AWS default credential chain (IAM roles, instance profiles, ECS task roles, IRSA) + NEXT_PUBLIC_AZURE_CONFIGURED: z.string().optional(), // Hide Azure credential fields when endpoint/key/version are pre-configured server-side NEXT_PUBLIC_COPILOT_TRAINING_ENABLED: z.string().optional(), NEXT_PUBLIC_ENABLE_PLAYGROUND: z.string().optional(), // Enable component playground at /playground NEXT_PUBLIC_DOCUMENTATION_URL: z.string().url().optional(), // Custom documentation URL @@ -461,6 +463,8 @@ export const env = createEnv({ NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED: process.env.NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED, NEXT_PUBLIC_TURNSTILE_SITE_KEY: process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY, NEXT_PUBLIC_E2B_ENABLED: process.env.NEXT_PUBLIC_E2B_ENABLED, + NEXT_PUBLIC_BEDROCK_DEFAULT_CREDENTIALS: process.env.NEXT_PUBLIC_BEDROCK_DEFAULT_CREDENTIALS, + NEXT_PUBLIC_AZURE_CONFIGURED: process.env.NEXT_PUBLIC_AZURE_CONFIGURED, NEXT_PUBLIC_COPILOT_TRAINING_ENABLED: process.env.NEXT_PUBLIC_COPILOT_TRAINING_ENABLED, NEXT_PUBLIC_ENABLE_PLAYGROUND: process.env.NEXT_PUBLIC_ENABLE_PLAYGROUND, NEXT_PUBLIC_POSTHOG_ENABLED: process.env.NEXT_PUBLIC_POSTHOG_ENABLED, diff --git a/apps/sim/lib/core/config/feature-flags.ts b/apps/sim/lib/core/config/feature-flags.ts index 012d1d5e026..b688924afed 100644 --- a/apps/sim/lib/core/config/feature-flags.ts +++ b/apps/sim/lib/core/config/feature-flags.ts @@ -122,6 +122,14 @@ export const isInboxEnabled = isTruthy(env.INBOX_ENABLED) */ export const isE2bEnabled = isTruthy(env.E2B_ENABLED) +/** + * Whether Azure OpenAI / Azure Anthropic credentials are pre-configured at the server level + * (via AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_API_KEY, AZURE_ANTHROPIC_ENDPOINT, etc.). + * When true, the endpoint, API key, and API version fields are hidden in the Agent block UI. + * Set NEXT_PUBLIC_AZURE_CONFIGURED=true in self-hosted deployments on Azure. + */ +export const isAzureConfigured = isTruthy(getEnv('NEXT_PUBLIC_AZURE_CONFIGURED')) + /** * Are invitations disabled globally * When true, workspace invitations are disabled for all users diff --git a/apps/sim/lib/workflows/subblocks/visibility.ts b/apps/sim/lib/workflows/subblocks/visibility.ts index 44cddf1224d..356ab0507bf 100644 --- a/apps/sim/lib/workflows/subblocks/visibility.ts +++ b/apps/sim/lib/workflows/subblocks/visibility.ts @@ -285,15 +285,19 @@ export function resolveDependencyValue( * Check if a subblock is gated by a feature flag. */ export function isSubBlockFeatureEnabled(subBlock: SubBlockConfig): boolean { - if (!subBlock.requiresFeature) return true - return isTruthy(getEnv(subBlock.requiresFeature)) + if (!subBlock.showWhenEnvSet) return true + return isTruthy(getEnv(subBlock.showWhenEnvSet)) } /** - * Check if a subblock should be hidden because we're running on hosted Sim. - * Used for tool API key fields that should be hidden when Sim provides hosted keys. + * Check if a subblock should be hidden based on environment conditions. + * Covers two cases: + * - `hideWhenHosted`: hidden when running on hosted Sim (tool API key fields) + * - `hideWhenEnvSet`: hidden when a specific NEXT_PUBLIC_ env var is truthy + * (credential fields hidden when the deployment provides them server-side) */ -export function isSubBlockHiddenByHostedKey(subBlock: SubBlockConfig): boolean { - if (!subBlock.hideWhenHosted) return false - return isHosted +export function isSubBlockHidden(subBlock: SubBlockConfig): boolean { + if (subBlock.hideWhenHosted && isHosted) return true + if (subBlock.hideWhenEnvSet && isTruthy(getEnv(subBlock.hideWhenEnvSet))) return true + return false } diff --git a/apps/sim/providers/azure-anthropic/index.ts b/apps/sim/providers/azure-anthropic/index.ts index 721e363394a..710d1ba90e8 100644 --- a/apps/sim/providers/azure-anthropic/index.ts +++ b/apps/sim/providers/azure-anthropic/index.ts @@ -1,5 +1,6 @@ import Anthropic from '@anthropic-ai/sdk' import { createLogger } from '@sim/logger' +import { env } from '@/lib/core/config/env' import type { StreamingExecution } from '@/executor/types' import { executeAnthropicProviderRequest } from '@/providers/anthropic/core' import { getProviderDefaultModel, getProviderModels } from '@/providers/models' @@ -18,14 +19,16 @@ export const azureAnthropicProvider: ProviderConfig = { executeRequest: async ( request: ProviderRequest ): Promise => { - if (!request.azureEndpoint) { + const azureEndpoint = request.azureEndpoint || env.AZURE_ANTHROPIC_ENDPOINT + if (!azureEndpoint) { throw new Error( - 'Azure endpoint is required for Azure Anthropic. Please provide it via the azureEndpoint parameter.' + 'Azure endpoint is required for Azure Anthropic. Please provide it via the azureEndpoint parameter or AZURE_ANTHROPIC_ENDPOINT environment variable.' ) } - if (!request.apiKey) { - throw new Error('API key is required for Azure Anthropic') + const apiKey = request.apiKey + if (!apiKey) { + throw new Error('API key is required for Azure Anthropic.') } // Strip the azure-anthropic/ prefix from the model name if present @@ -33,14 +36,16 @@ export const azureAnthropicProvider: ProviderConfig = { // Azure AI Foundry hosts Anthropic models at {endpoint}/anthropic // The SDK appends /v1/messages automatically - const baseURL = `${request.azureEndpoint.replace(/\/$/, '')}/anthropic` + const baseURL = `${azureEndpoint.replace(/\/$/, '')}/anthropic` - const anthropicVersion = request.azureApiVersion || '2023-06-01' + const anthropicVersion = + request.azureApiVersion || env.AZURE_ANTHROPIC_API_VERSION || '2023-06-01' return executeAnthropicProviderRequest( { ...request, model: modelName, + apiKey, }, { providerId: 'azure-anthropic', diff --git a/apps/sim/providers/azure-openai/index.ts b/apps/sim/providers/azure-openai/index.ts index 930c31035a3..72ee513e80e 100644 --- a/apps/sim/providers/azure-openai/index.ts +++ b/apps/sim/providers/azure-openai/index.ts @@ -65,7 +65,7 @@ async function executeChatCompletionsRequest( }) const azureOpenAI = new AzureOpenAI({ - apiKey: request.apiKey, + apiKey: request.apiKey!, apiVersion: azureApiVersion, endpoint: azureEndpoint, }) @@ -623,8 +623,9 @@ export const azureOpenAIProvider: ProviderConfig = { ) } - if (!request.apiKey) { - throw new Error('API key is required for Azure OpenAI') + const apiKey = request.apiKey + if (!apiKey) { + throw new Error('API key is required for Azure OpenAI.') } // Check if the endpoint is a full chat completions URL @@ -653,7 +654,12 @@ export const azureOpenAIProvider: ProviderConfig = { apiVersion: azureApiVersion, }) - return executeChatCompletionsRequest(request, baseUrl, azureApiVersion, deploymentName) + return executeChatCompletionsRequest( + { ...request, apiKey }, + baseUrl, + azureApiVersion, + deploymentName + ) } // Check if the endpoint is already a full responses API URL @@ -663,18 +669,21 @@ export const azureOpenAIProvider: ProviderConfig = { const deploymentName = request.model.replace('azure/', '') // Use the URL as-is since it's already complete - return executeResponsesProviderRequest(request, { - providerId: 'azure-openai', - providerLabel: 'Azure OpenAI', - modelName: deploymentName, - endpoint: azureEndpoint, - headers: { - 'Content-Type': 'application/json', - 'OpenAI-Beta': 'responses=v1', - 'api-key': request.apiKey, - }, - logger, - }) + return executeResponsesProviderRequest( + { ...request, apiKey }, + { + providerId: 'azure-openai', + providerLabel: 'Azure OpenAI', + modelName: deploymentName, + endpoint: azureEndpoint, + headers: { + 'Content-Type': 'application/json', + 'OpenAI-Beta': 'responses=v1', + 'api-key': apiKey, + }, + logger, + } + ) } // Default: base URL provided, construct the responses API URL @@ -684,17 +693,20 @@ export const azureOpenAIProvider: ProviderConfig = { const deploymentName = request.model.replace('azure/', '') const apiUrl = `${azureEndpoint.replace(/\/$/, '')}/openai/v1/responses?api-version=${azureApiVersion}` - return executeResponsesProviderRequest(request, { - providerId: 'azure-openai', - providerLabel: 'Azure OpenAI', - modelName: deploymentName, - endpoint: apiUrl, - headers: { - 'Content-Type': 'application/json', - 'OpenAI-Beta': 'responses=v1', - 'api-key': request.apiKey, - }, - logger, - }) + return executeResponsesProviderRequest( + { ...request, apiKey }, + { + providerId: 'azure-openai', + providerLabel: 'Azure OpenAI', + modelName: deploymentName, + endpoint: apiUrl, + headers: { + 'Content-Type': 'application/json', + 'OpenAI-Beta': 'responses=v1', + 'api-key': apiKey, + }, + logger, + } + ) }, } diff --git a/apps/sim/providers/bedrock/index.test.ts b/apps/sim/providers/bedrock/index.test.ts new file mode 100644 index 00000000000..8c938e01c87 --- /dev/null +++ b/apps/sim/providers/bedrock/index.test.ts @@ -0,0 +1,115 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const mockSend = vi.fn() + +vi.mock('@aws-sdk/client-bedrock-runtime', () => ({ + BedrockRuntimeClient: vi.fn().mockImplementation(() => { + return { send: mockSend } + }), + ConverseCommand: vi.fn(), + ConverseStreamCommand: vi.fn(), +})) + +vi.mock('@/providers/bedrock/utils', () => ({ + getBedrockInferenceProfileId: vi + .fn() + .mockReturnValue('us.anthropic.claude-3-5-sonnet-20241022-v2:0'), + checkForForcedToolUsage: vi.fn(), + createReadableStreamFromBedrockStream: vi.fn(), + generateToolUseId: vi.fn().mockReturnValue('tool-1'), +})) + +vi.mock('@/providers/models', () => ({ + getProviderModels: vi.fn().mockReturnValue([]), + getProviderDefaultModel: vi.fn().mockReturnValue('us.anthropic.claude-3-5-sonnet-20241022-v2:0'), +})) + +vi.mock('@/providers/utils', () => ({ + calculateCost: vi.fn().mockReturnValue({ input: 0, output: 0, total: 0, pricing: null }), + prepareToolExecution: vi.fn(), + prepareToolsWithUsageControl: vi.fn().mockReturnValue({ + tools: [], + toolChoice: 'auto', + forcedTools: [], + }), + sumToolCosts: vi.fn().mockReturnValue(0), +})) + +vi.mock('@/tools', () => ({ + executeTool: vi.fn(), +})) + +import { BedrockRuntimeClient } from '@aws-sdk/client-bedrock-runtime' +import { bedrockProvider } from '@/providers/bedrock/index' + +describe('bedrockProvider credential handling', () => { + beforeEach(() => { + vi.clearAllMocks() + mockSend.mockResolvedValue({ + output: { message: { content: [{ text: 'response' }] } }, + usage: { inputTokens: 10, outputTokens: 5 }, + }) + }) + + const baseRequest = { + model: 'us.anthropic.claude-3-5-sonnet-20241022-v2:0', + systemPrompt: 'You are helpful.', + messages: [{ role: 'user' as const, content: 'Hello' }], + } + + it('throws when only bedrockAccessKeyId is provided', async () => { + await expect( + bedrockProvider.executeRequest({ + ...baseRequest, + bedrockAccessKeyId: 'AKIAIOSFODNN7EXAMPLE', + }) + ).rejects.toThrow('Both bedrockAccessKeyId and bedrockSecretKey must be provided together') + }) + + it('throws when only bedrockSecretKey is provided', async () => { + await expect( + bedrockProvider.executeRequest({ + ...baseRequest, + bedrockSecretKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', + }) + ).rejects.toThrow('Both bedrockAccessKeyId and bedrockSecretKey must be provided together') + }) + + it('creates client with explicit credentials when both are provided', async () => { + await bedrockProvider.executeRequest({ + ...baseRequest, + bedrockAccessKeyId: 'AKIAIOSFODNN7EXAMPLE', + bedrockSecretKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', + }) + + expect(BedrockRuntimeClient).toHaveBeenCalledWith({ + region: 'us-east-1', + credentials: { + accessKeyId: 'AKIAIOSFODNN7EXAMPLE', + secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', + }, + }) + }) + + it('creates client without credentials when neither is provided', async () => { + await bedrockProvider.executeRequest(baseRequest) + + expect(BedrockRuntimeClient).toHaveBeenCalledWith({ + region: 'us-east-1', + }) + }) + + it('uses custom region when provided', async () => { + await bedrockProvider.executeRequest({ + ...baseRequest, + bedrockRegion: 'eu-west-1', + }) + + expect(BedrockRuntimeClient).toHaveBeenCalledWith({ + region: 'eu-west-1', + }) + }) +}) diff --git a/apps/sim/providers/bedrock/index.ts b/apps/sim/providers/bedrock/index.ts index ec0af6ab04b..d3223cfebd0 100644 --- a/apps/sim/providers/bedrock/index.ts +++ b/apps/sim/providers/bedrock/index.ts @@ -1,6 +1,7 @@ import { type Message as BedrockMessage, BedrockRuntimeClient, + type BedrockRuntimeClientConfig, type ContentBlock, type ConversationRole, ConverseCommand, @@ -50,14 +51,6 @@ export const bedrockProvider: ProviderConfig = { executeRequest: async ( request: ProviderRequest ): Promise => { - if (!request.bedrockAccessKeyId) { - throw new Error('AWS Access Key ID is required for Bedrock') - } - - if (!request.bedrockSecretKey) { - throw new Error('AWS Secret Access Key is required for Bedrock') - } - const region = request.bedrockRegion || 'us-east-1' const bedrockModelId = getBedrockInferenceProfileId(request.model, region) @@ -67,13 +60,24 @@ export const bedrockProvider: ProviderConfig = { region, }) - const client = new BedrockRuntimeClient({ - region, - credentials: { - accessKeyId: request.bedrockAccessKeyId || '', - secretAccessKey: request.bedrockSecretKey || '', - }, - }) + const hasAccessKey = Boolean(request.bedrockAccessKeyId) + const hasSecretKey = Boolean(request.bedrockSecretKey) + if (hasAccessKey !== hasSecretKey) { + throw new Error( + 'Both bedrockAccessKeyId and bedrockSecretKey must be provided together. ' + + 'Provide both for explicit credentials, or omit both to use the AWS default credential chain.' + ) + } + + const clientConfig: BedrockRuntimeClientConfig = { region } + if (request.bedrockAccessKeyId && request.bedrockSecretKey) { + clientConfig.credentials = { + accessKeyId: request.bedrockAccessKeyId, + secretAccessKey: request.bedrockSecretKey, + } + } + + const client = new BedrockRuntimeClient(clientConfig) const messages: BedrockMessage[] = [] const systemContent: SystemContentBlock[] = [] diff --git a/apps/sim/providers/utils.ts b/apps/sim/providers/utils.ts index 30fe1467eb1..40d4a0ebdf7 100644 --- a/apps/sim/providers/utils.ts +++ b/apps/sim/providers/utils.ts @@ -718,6 +718,13 @@ export function shouldBillModelUsage(model: string): boolean { return hostedModels.some((hostedModel) => model.toLowerCase() === hostedModel.toLowerCase()) } +/** + * Placeholder returned for providers that use their own credential mechanism + * rather than a user-supplied API key (e.g. AWS Bedrock via IAM/instance profiles). + * Must be truthy so upstream key-presence checks don't reject it. + */ +export const PROVIDER_PLACEHOLDER_KEY = 'provider-uses-own-credentials' + /** * Get an API key for a specific provider, handling rotation and fallbacks * For use server-side only @@ -740,7 +747,7 @@ export function getApiKey(provider: string, model: string, userProvidedKey?: str // Bedrock uses its own credentials (bedrockAccessKeyId/bedrockSecretKey), not apiKey const isBedrockModel = provider === 'bedrock' || model.startsWith('bedrock/') if (isBedrockModel) { - return 'bedrock-uses-own-credentials' + return PROVIDER_PLACEHOLDER_KEY } const isOpenAIModel = provider === 'openai' diff --git a/apps/sim/serializer/index.ts b/apps/sim/serializer/index.ts index 9c21661deb5..485b4e3cc2c 100644 --- a/apps/sim/serializer/index.ts +++ b/apps/sim/serializer/index.ts @@ -9,7 +9,7 @@ import { isCanonicalPair, isNonEmptyValue, isSubBlockFeatureEnabled, - isSubBlockHiddenByHostedKey, + isSubBlockHidden, resolveCanonicalMode, } from '@/lib/workflows/subblocks/visibility' import { getBlock } from '@/blocks' @@ -49,7 +49,7 @@ function shouldSerializeSubBlock( canonicalModeOverrides?: CanonicalModeOverrides ): boolean { if (!isSubBlockFeatureEnabled(subBlockConfig)) return false - if (isSubBlockHiddenByHostedKey(subBlockConfig)) return false + if (isSubBlockHidden(subBlockConfig)) return false if (subBlockConfig.mode === 'trigger') { if (!isTriggerContext && !isTriggerCategory) return false diff --git a/apps/sim/tools/params.ts b/apps/sim/tools/params.ts index 925e4fe6f50..9cc8c883655 100644 --- a/apps/sim/tools/params.ts +++ b/apps/sim/tools/params.ts @@ -5,7 +5,7 @@ import { type CanonicalModeOverrides, evaluateSubBlockCondition, isCanonicalPair, - isSubBlockHiddenByHostedKey, + isSubBlockHidden, resolveCanonicalMode, type SubBlockCondition, } from '@/lib/workflows/subblocks/visibility' @@ -320,7 +320,7 @@ export function getToolParametersConfig( ) if (subBlock) { - if (isSubBlockHiddenByHostedKey(subBlock)) { + if (isSubBlockHidden(subBlock)) { toolParam.visibility = 'hidden' } @@ -946,8 +946,8 @@ export function getSubBlocksForToolInput( // Skip trigger-mode-only subblocks if (sb.mode === 'trigger') continue - // Hide tool API key fields when running on hosted Sim - if (isSubBlockHiddenByHostedKey(sb)) continue + // Hide tool API key fields when running on hosted Sim or when env var is set + if (isSubBlockHidden(sb)) continue // Determine the effective param ID (canonical or subblock id) const effectiveParamId = sb.canonicalParamId || sb.id diff --git a/bun.lock b/bun.lock index e773f35d67e..98f2b875168 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "simstudio", diff --git a/helm/sim/examples/values-aws.yaml b/helm/sim/examples/values-aws.yaml index c8451efc2a9..a5f21eb2294 100644 --- a/helm/sim/examples/values-aws.yaml +++ b/helm/sim/examples/values-aws.yaml @@ -45,6 +45,10 @@ app: NODE_ENV: "production" NEXT_TELEMETRY_DISABLED: "1" + # AWS Bedrock - when using IRSA (see serviceAccount below), the default credential chain + # resolves automatically. Setting this hides the credential fields in the Agent block UI. + NEXT_PUBLIC_BEDROCK_DEFAULT_CREDENTIALS: "true" # Uncomment if using Bedrock with IRSA + # AWS S3 Cloud Storage Configuration (RECOMMENDED for production) # Create S3 buckets in your AWS account and configure IAM permissions AWS_REGION: "us-west-2" diff --git a/helm/sim/examples/values-azure.yaml b/helm/sim/examples/values-azure.yaml index a11b55adc93..30c97798bfa 100644 --- a/helm/sim/examples/values-azure.yaml +++ b/helm/sim/examples/values-azure.yaml @@ -47,6 +47,17 @@ app: NODE_ENV: "production" NEXT_TELEMETRY_DISABLED: "1" + # Azure OpenAI / Azure Anthropic (Azure AI Foundry) — set to use server-side credentials. + # When NEXT_PUBLIC_AZURE_CONFIGURED is true, the endpoint/key/version fields are hidden in + # the Agent block UI so users just pick a model and run. + AZURE_OPENAI_ENDPOINT: "" # e.g. https://your-resource.openai.azure.com + AZURE_OPENAI_API_KEY: "" # Azure OpenAI API key + AZURE_OPENAI_API_VERSION: "" # e.g. 2024-07-01-preview + AZURE_ANTHROPIC_ENDPOINT: "" # Azure AI Foundry endpoint for Anthropic models + AZURE_ANTHROPIC_API_KEY: "" # Azure Anthropic API key + AZURE_ANTHROPIC_API_VERSION: "" # Azure Anthropic API version (e.g., 2023-06-01) + NEXT_PUBLIC_AZURE_CONFIGURED: "true" # Set to "true" once credentials are configured above + # Azure Blob Storage Configuration (RECOMMENDED for production) # Create a storage account and containers in your Azure subscription AZURE_ACCOUNT_NAME: "simstudiostorageacct" # Azure storage account name diff --git a/helm/sim/values.yaml b/helm/sim/values.yaml index 92c163b4222..9fbe6195b67 100644 --- a/helm/sim/values.yaml +++ b/helm/sim/values.yaml @@ -113,13 +113,24 @@ app: # Google Vertex AI Configuration VERTEX_PROJECT: "" # Google Cloud project ID for Vertex AI VERTEX_LOCATION: "us-central1" # Google Cloud region for Vertex AI (e.g., "us-central1") - + + # Azure OpenAI Configuration (leave empty if not using Azure OpenAI) + AZURE_OPENAI_ENDPOINT: "" # Azure OpenAI service endpoint (e.g., https://your-resource.openai.azure.com) + AZURE_OPENAI_API_KEY: "" # Azure OpenAI API key + AZURE_OPENAI_API_VERSION: "" # Azure OpenAI API version (e.g., 2024-07-01-preview) + + # Azure Anthropic Configuration (leave empty if not using Azure Anthropic via AI Foundry) + AZURE_ANTHROPIC_ENDPOINT: "" # Azure AI Foundry endpoint for Anthropic models + AZURE_ANTHROPIC_API_KEY: "" # Azure Anthropic API key + AZURE_ANTHROPIC_API_VERSION: "" # Azure Anthropic API version (e.g., 2023-06-01) + # AI Provider API Keys (leave empty if not using) OPENAI_API_KEY: "" # Primary OpenAI API key OPENAI_API_KEY_1: "" # Additional OpenAI API key for load balancing OPENAI_API_KEY_2: "" # Additional OpenAI API key for load balancing OPENAI_API_KEY_3: "" # Additional OpenAI API key for load balancing MISTRAL_API_KEY: "" # Mistral AI API key + FIREWORKS_API_KEY: "" # Fireworks AI API key (for hosted model access) ANTHROPIC_API_KEY_1: "" # Primary Anthropic Claude API key ANTHROPIC_API_KEY_2: "" # Additional Anthropic API key for load balancing ANTHROPIC_API_KEY_3: "" # Additional Anthropic API key for load balancing @@ -223,6 +234,18 @@ app: SSO_ENABLED: "" # Enable SSO authentication ("true" to enable) NEXT_PUBLIC_SSO_ENABLED: "" # Show SSO login button in UI ("true" to enable) + # AWS Bedrock Credential Mode + # Set to "true" when the deployment uses AWS default credential chain (IAM roles, instance + # profiles, ECS task roles, IRSA, etc.) instead of explicit access key/secret per workflow. + # When enabled, the AWS Access Key ID and Secret fields are hidden in the Agent block UI. + NEXT_PUBLIC_BEDROCK_DEFAULT_CREDENTIALS: "" # Set to "true" to hide Bedrock credential fields + + # Azure Provider Credential Mode + # Set to "true" when AZURE_OPENAI_ENDPOINT/API_KEY (and/or AZURE_ANTHROPIC_*) are configured + # server-side. When enabled, the Azure endpoint, API key, and API version fields are hidden + # in the Agent block UI — users just pick an Azure model and run. + NEXT_PUBLIC_AZURE_CONFIGURED: "" # Set to "true" to hide Azure credential fields + # AWS S3 Cloud Storage Configuration (optional - for file storage) # If configured, files will be stored in S3 instead of local storage AWS_REGION: "" # AWS region (e.g., "us-east-1") From ac831b85b2d9cc2d1657210756a821f9988f3dc6 Mon Sep 17 00:00:00 2001 From: Waleed Date: Wed, 1 Apr 2026 17:21:00 -0700 Subject: [PATCH 06/16] chore(bun): update bunfig.toml (#3889) * chore(bun): update bunfig.toml * outdated bun lock * chore(deps): downgrade @aws-sdk/client-secrets-manager to 3.940.0 --- apps/sim/package.json | 2 +- bun.lock | 55 +++---------------------------------------- bunfig.toml | 1 + 3 files changed, 5 insertions(+), 53 deletions(-) diff --git a/apps/sim/package.json b/apps/sim/package.json index 8fa56f2078c..2b2ebded398 100644 --- a/apps/sim/package.json +++ b/apps/sim/package.json @@ -40,7 +40,7 @@ "@aws-sdk/client-dynamodb": "3.940.0", "@aws-sdk/client-rds-data": "3.940.0", "@aws-sdk/client-s3": "^3.779.0", - "@aws-sdk/client-secrets-manager": "3.1021.0", + "@aws-sdk/client-secrets-manager": "3.940.0", "@aws-sdk/client-sqs": "3.947.0", "@aws-sdk/lib-dynamodb": "3.940.0", "@aws-sdk/s3-request-presigner": "^3.779.0", diff --git a/bun.lock b/bun.lock index 98f2b875168..a9edfffb951 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "simstudio", @@ -59,7 +60,7 @@ "@aws-sdk/client-dynamodb": "3.940.0", "@aws-sdk/client-rds-data": "3.940.0", "@aws-sdk/client-s3": "^3.779.0", - "@aws-sdk/client-secrets-manager": "3.1021.0", + "@aws-sdk/client-secrets-manager": "3.940.0", "@aws-sdk/client-sqs": "3.947.0", "@aws-sdk/lib-dynamodb": "3.940.0", "@aws-sdk/s3-request-presigner": "^3.779.0", @@ -419,7 +420,7 @@ "@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.1015.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.24", "@aws-sdk/credential-provider-node": "^3.972.25", "@aws-sdk/middleware-bucket-endpoint": "^3.972.8", "@aws-sdk/middleware-expect-continue": "^3.972.8", "@aws-sdk/middleware-flexible-checksums": "^3.974.4", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-location-constraint": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", "@aws-sdk/middleware-sdk-s3": "^3.972.24", "@aws-sdk/middleware-ssec": "^3.972.8", "@aws-sdk/middleware-user-agent": "^3.972.25", "@aws-sdk/region-config-resolver": "^3.972.9", "@aws-sdk/signature-v4-multi-region": "^3.996.12", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.11", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.12", "@smithy/eventstream-serde-browser": "^4.2.12", "@smithy/eventstream-serde-config-resolver": "^4.3.12", "@smithy/eventstream-serde-node": "^4.2.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-blob-browser": "^4.2.13", "@smithy/hash-node": "^4.2.12", "@smithy/hash-stream-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/md5-js": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.27", "@smithy/middleware-retry": "^4.4.44", "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.43", "@smithy/util-defaults-mode-node": "^4.2.47", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/util-stream": "^4.5.20", "@smithy/util-utf8": "^4.2.2", "@smithy/util-waiter": "^4.2.13", "tslib": "^2.6.2" } }, "sha512-yo+Y+/fq5/E684SynTRO+VA3a+98MeE/hs7J52XpNI5SchOCSrLhLtcDKVASlGhHQdNLGLzblRgps1OZaf8sbA=="], - "@aws-sdk/client-secrets-manager": ["@aws-sdk/client-secrets-manager@3.1021.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.26", "@aws-sdk/credential-provider-node": "^3.972.29", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.9", "@aws-sdk/middleware-user-agent": "^3.972.28", "@aws-sdk/region-config-resolver": "^3.972.10", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.14", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.13", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.28", "@smithy/middleware-retry": "^4.4.46", "@smithy/middleware-serde": "^4.2.16", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.1", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.44", "@smithy/util-defaults-mode-node": "^4.2.48", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.13", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-Z2z4eEuXDBiLXwu51icmP7GYIXHoQ4KRQaNESquKa6n57rWnQ6kD6ZhsbQow/39gHvbU9uA6t+aHeTdYxw0JbQ=="], + "@aws-sdk/client-secrets-manager": ["@aws-sdk/client-secrets-manager@3.940.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.940.0", "@aws-sdk/credential-provider-node": "3.940.0", "@aws-sdk/middleware-host-header": "3.936.0", "@aws-sdk/middleware-logger": "3.936.0", "@aws-sdk/middleware-recursion-detection": "3.936.0", "@aws-sdk/middleware-user-agent": "3.940.0", "@aws-sdk/region-config-resolver": "3.936.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@aws-sdk/util-user-agent-browser": "3.936.0", "@aws-sdk/util-user-agent-node": "3.940.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.12", "@smithy/middleware-retry": "^4.4.12", "@smithy/middleware-serde": "^4.2.6", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.11", "@smithy/util-defaults-mode-node": "^4.2.14", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-fpxSRsGyuXmyNqEwdGJUDWVgN0v8xR7tr32Quls3K+HnYlnBGFmISu5Pcc+BfwmrZHnPaVpPc+S3PUzTnFpOJg=="], "@aws-sdk/client-sesv2": ["@aws-sdk/client-sesv2@3.1015.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.24", "@aws-sdk/credential-provider-node": "^3.972.25", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", "@aws-sdk/middleware-user-agent": "^3.972.25", "@aws-sdk/region-config-resolver": "^3.972.9", "@aws-sdk/signature-v4-multi-region": "^3.996.12", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.11", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.27", "@smithy/middleware-retry": "^4.4.44", "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.43", "@smithy/util-defaults-mode-node": "^4.2.47", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FoIU3k4Z3ccEEgZFRwLHa84erkMk25r17q7UfsNLrpP/ef3EzNYss+QM15imeS8ekbH8+P/5T6/5/9sUO3L6kA=="], @@ -3865,28 +3866,6 @@ "@aws-sdk/client-s3/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.11", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.25", "@aws-sdk/types": "^3.973.6", "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-1qdXbXo2s5MMLpUvw00284LsbhtlQ4ul7Zzdn5n+7p4WVgCMLqhxImpHIrjSoc72E/fyc4Wq8dLtUld2Gsh+lA=="], - "@aws-sdk/client-secrets-manager/@aws-sdk/core": ["@aws-sdk/core@3.973.26", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws-sdk/xml-builder": "^3.972.16", "@smithy/core": "^3.23.13", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-A/E6n2W42ruU+sfWk+mMUOyVXbsSgGrY3MJ9/0Az5qUdG67y8I6HYzzoAa+e/lzxxl1uCYmEL6BTMi9ZiZnplQ=="], - - "@aws-sdk/client-secrets-manager/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.29", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.24", "@aws-sdk/credential-provider-http": "^3.972.26", "@aws-sdk/credential-provider-ini": "^3.972.28", "@aws-sdk/credential-provider-process": "^3.972.24", "@aws-sdk/credential-provider-sso": "^3.972.28", "@aws-sdk/credential-provider-web-identity": "^3.972.28", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-clSzDcvndpFJAggLDnDb36sPdlZYyEs5Zm6zgZjjUhwsJgSWiWKwFIXUVBcbruidNyBdbpOv2tNDL9sX8y3/0g=="], - - "@aws-sdk/client-secrets-manager/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ=="], - - "@aws-sdk/client-secrets-manager/@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA=="], - - "@aws-sdk/client-secrets-manager/@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-/Wt5+CT8dpTFQxEJ9iGy/UGrXr7p2wlIOEHvIr/YcHYByzoLjrqkYqXdJjd9UIgWjv7eqV2HnFJen93UTuwfTQ=="], - - "@aws-sdk/client-secrets-manager/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.28", "", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@smithy/core": "^3.23.13", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-retry": "^4.2.13", "tslib": "^2.6.2" } }, "sha512-cfWZFlVh7Va9lRay4PN2A9ARFzaBYcA097InT5M2CdRS05ECF5yaz86jET8Wsl2WcyKYEvVr/QNmKtYtafUHtQ=="], - - "@aws-sdk/client-secrets-manager/@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/config-resolver": "^4.4.13", "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-1dq9ToC6e070QvnVhhbAs3bb5r6cQ10gTVc6cyRV5uvQe7P138TV2uG2i6+Yok4bAkVAcx5AqkTEBUvWEtBlsQ=="], - - "@aws-sdk/client-secrets-manager/@aws-sdk/types": ["@aws-sdk/types@3.973.6", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw=="], - - "@aws-sdk/client-secrets-manager/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.5", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-endpoints": "^3.3.3", "tslib": "^2.6.2" } }, "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw=="], - - "@aws-sdk/client-secrets-manager/@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA=="], - - "@aws-sdk/client-secrets-manager/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.14", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.28", "@aws-sdk/types": "^3.973.6", "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-vNSB/DYaPOyujVZBg/zUznH9QC142MaTHVmaFlF7uzzfg3CgT9f/l4C0Yi+vU/tbBhxVcXVB90Oohk5+o+ZbWw=="], - "@aws-sdk/client-secrets-manager/@smithy/core": ["@smithy/core@3.23.13", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-stream": "^4.5.21", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-J+2TT9D6oGsUVXVEMvz8h2EmdVnkBiy2auCie4aSJMvKlzUtO5hqjEzXhoCUkIMo7gAYjbQcN0g/MMSXEhDs1Q=="], "@aws-sdk/client-secrets-manager/@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.28", "", { "dependencies": { "@smithy/core": "^3.23.13", "@smithy/middleware-serde": "^4.2.16", "@smithy/node-config-provider": "^4.3.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-middleware": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-p1gfYpi91CHcs5cBq982UlGlDrxoYUX6XdHSo91cQ2KFuz6QloHosO7Jc60pJiVmkWrKOV8kFYlGFFbQ2WUKKQ=="], @@ -4559,20 +4538,6 @@ "@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.24", "", { "dependencies": { "@aws-sdk/core": "^3.973.24", "@aws-sdk/nested-clients": "^3.996.14", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-J6H4R1nvr3uBTqD/EeIPAskrBtET4WFfNhpFySr2xW7bVZOXpQfPjrLSIx65jcNjBmLXzWq8QFLdVoGxiGG/SA=="], - "@aws-sdk/client-secrets-manager/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.16", "", { "dependencies": { "@smithy/types": "^4.13.1", "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" } }, "sha512-iu2pyvaqmeatIJLURLqx9D+4jKAdTH20ntzB6BFwjyN7V960r4jK32mx0Zf7YbtOYAbmbtQfDNuL60ONinyw7A=="], - - "@aws-sdk/client-secrets-manager/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.24", "", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-FWg8uFmT6vQM7VuzELzwVo5bzExGaKHdubn0StjgrcU5FvuLExUe+k06kn/40uKv59rYzhez8eFNM4yYE/Yb/w=="], - - "@aws-sdk/client-secrets-manager/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.26", "", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/types": "^3.973.6", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/node-http-handler": "^4.5.1", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/util-stream": "^4.5.21", "tslib": "^2.6.2" } }, "sha512-CY4ppZ+qHYqcXqBVi//sdHST1QK3KzOEiLtpLsc9W2k2vfZPKExGaQIsOwcyvjpjUEolotitmd3mUNY56IwDEA=="], - - "@aws-sdk/client-secrets-manager/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.28", "", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/credential-provider-env": "^3.972.24", "@aws-sdk/credential-provider-http": "^3.972.26", "@aws-sdk/credential-provider-login": "^3.972.28", "@aws-sdk/credential-provider-process": "^3.972.24", "@aws-sdk/credential-provider-sso": "^3.972.28", "@aws-sdk/credential-provider-web-identity": "^3.972.28", "@aws-sdk/nested-clients": "^3.996.18", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-wXYvq3+uQcZV7k+bE4yDXCTBdzWTU9x/nMiKBfzInmv6yYK1veMK0AKvRfRBd72nGWYKcL6AxwiPg9z/pYlgpw=="], - - "@aws-sdk/client-secrets-manager/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.24", "", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Q2k/XLrFXhEztPHqj4SLCNID3hEPdlhh1CDLBpNnM+1L8fq7P+yON9/9M1IGN/dA5W45v44ylERfXtDAlmMNmw=="], - - "@aws-sdk/client-secrets-manager/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.28", "", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/nested-clients": "^3.996.18", "@aws-sdk/token-providers": "3.1021.0", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-IoUlmKMLEITFn1SiCTjPfR6KrE799FBo5baWyk/5Ppar2yXZoUdaRqZzJzK6TcJxx450M8m8DbpddRVYlp5R/A=="], - - "@aws-sdk/client-secrets-manager/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.28", "", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/nested-clients": "^3.996.18", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-d+6h0SD8GGERzKe27v5rOzNGKOl0D+l0bWJdqrxH8WSQzHzjsQFIAPgIeOTUwBHVsKKwtSxc91K/SWax6XgswQ=="], - "@aws-sdk/client-secrets-manager/@smithy/core/@smithy/util-stream": ["@smithy/util-stream@4.5.21", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.15", "@smithy/node-http-handler": "^4.5.1", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-KzSg+7KKywLnkoKejRtIBXDmwBfjGvg1U1i/etkC7XSWUyFCoLno1IohV2c74IzQqdhX5y3uE44r/8/wuK+A7Q=="], "@aws-sdk/client-secrets-manager/@smithy/smithy-client/@smithy/util-stream": ["@smithy/util-stream@4.5.21", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.15", "@smithy/node-http-handler": "^4.5.1", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-KzSg+7KKywLnkoKejRtIBXDmwBfjGvg1U1i/etkC7XSWUyFCoLno1IohV2c74IzQqdhX5y3uE44r/8/wuK+A7Q=="], @@ -5127,20 +5092,6 @@ "@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.14", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.24", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", "@aws-sdk/middleware-user-agent": "^3.972.25", "@aws-sdk/region-config-resolver": "^3.972.9", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.11", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.27", "@smithy/middleware-retry": "^4.4.44", "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.43", "@smithy/util-defaults-mode-node": "^4.2.47", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-fSESKvh1VbfjtV3QMnRkCPZWkUbQof6T/DOpiLp33yP2wA+rbwwnZeG3XT3Ekljgw2I8X4XaQPnw+zSR8yxJ5Q=="], - "@aws-sdk/client-secrets-manager/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="], - - "@aws-sdk/client-secrets-manager/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/util-stream": ["@smithy/util-stream@4.5.21", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.15", "@smithy/node-http-handler": "^4.5.1", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-KzSg+7KKywLnkoKejRtIBXDmwBfjGvg1U1i/etkC7XSWUyFCoLno1IohV2c74IzQqdhX5y3uE44r/8/wuK+A7Q=="], - - "@aws-sdk/client-secrets-manager/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.28", "", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/nested-clients": "^3.996.18", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-ZSTfO6jqUTCysbdBPtEX5OUR//3rbD0lN7jO3sQeS2Gjr/Y+DT6SbIJ0oT2cemNw3UzKu97sNONd1CwNMthuZQ=="], - - "@aws-sdk/client-secrets-manager/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.18", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.26", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.9", "@aws-sdk/middleware-user-agent": "^3.972.28", "@aws-sdk/region-config-resolver": "^3.972.10", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.14", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.13", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.28", "@smithy/middleware-retry": "^4.4.46", "@smithy/middleware-serde": "^4.2.16", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.1", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.44", "@smithy/util-defaults-mode-node": "^4.2.48", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.13", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-c7ZSIXrESxHKx2Mcopgd8AlzZgoXMr20fkx5ViPWPOLBvmyhw9VwJx/Govg8Ef/IhEon5R9l53Z8fdYSEmp6VA=="], - - "@aws-sdk/client-secrets-manager/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.18", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.26", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.9", "@aws-sdk/middleware-user-agent": "^3.972.28", "@aws-sdk/region-config-resolver": "^3.972.10", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.14", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.13", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.28", "@smithy/middleware-retry": "^4.4.46", "@smithy/middleware-serde": "^4.2.16", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.1", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.44", "@smithy/util-defaults-mode-node": "^4.2.48", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.13", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-c7ZSIXrESxHKx2Mcopgd8AlzZgoXMr20fkx5ViPWPOLBvmyhw9VwJx/Govg8Ef/IhEon5R9l53Z8fdYSEmp6VA=="], - - "@aws-sdk/client-secrets-manager/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1021.0", "", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/nested-clients": "^3.996.18", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-TKY6h9spUk3OLs5v1oAgW9mAeBE3LAGNBwJokLy96wwmd4W2v/tYlXseProyed9ValDj2u1jK/4Rg1T+1NXyJA=="], - - "@aws-sdk/client-secrets-manager/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.18", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.26", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.9", "@aws-sdk/middleware-user-agent": "^3.972.28", "@aws-sdk/region-config-resolver": "^3.972.10", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.14", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.13", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.28", "@smithy/middleware-retry": "^4.4.46", "@smithy/middleware-serde": "^4.2.16", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.1", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.44", "@smithy/util-defaults-mode-node": "^4.2.48", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.13", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-c7ZSIXrESxHKx2Mcopgd8AlzZgoXMr20fkx5ViPWPOLBvmyhw9VwJx/Govg8Ef/IhEon5R9l53Z8fdYSEmp6VA=="], - "@aws-sdk/client-sesv2/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="], "@aws-sdk/client-sesv2/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.24", "", { "dependencies": { "@aws-sdk/core": "^3.973.24", "@aws-sdk/nested-clients": "^3.996.14", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-sIk8oa6AzDoUhxsR11svZESqvzGuXesw62Rl2oW6wguZx8i9cdGCvkFg+h5K7iucUZP8wyWibUbJMc+J66cu5g=="], diff --git a/bunfig.toml b/bunfig.toml index 9288db20dae..6f0019a2f81 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -1,5 +1,6 @@ [install] exact = true +minimumReleaseAge = 259200 [run] env = { NEXT_PUBLIC_APP_URL = "http://localhost:3000" } From 2c174ca4f663930468f4a15d5f3014d62d1bef45 Mon Sep 17 00:00:00 2001 From: Waleed Date: Wed, 1 Apr 2026 18:23:35 -0700 Subject: [PATCH 07/16] feat(landing): added models pages (#3888) * feat(landing): added models pages * fix(models): address PR review feedback Correct model structured-data price bounds, remove dead code in the models catalog helpers, and harden OG font loading with graceful fallbacks. Made-with: Cursor * relative imports, build fix * lint * fix(models): remove dead og-utils exports, fix formatTokenCount null guard --- .../app/(home)/components/footer/footer.tsx | 1 + .../app/(landing)/components/landing-faq.tsx | 63 ++ .../[slug]/components/integration-faq.tsx | 52 +- .../[provider]/[model]/opengraph-image.tsx | 42 + .../models/[provider]/[model]/page.tsx | 390 +++++++++ .../models/[provider]/opengraph-image.tsx | 43 + .../app/(landing)/models/[provider]/page.tsx | 294 +++++++ .../models/components/model-directory.tsx | 291 +++++++ .../models/components/model-primitives.tsx | 214 +++++ apps/sim/app/(landing)/models/layout.tsx | 42 + apps/sim/app/(landing)/models/og-utils.tsx | 205 +++++ .../app/(landing)/models/opengraph-image.tsx | 29 + apps/sim/app/(landing)/models/page.tsx | 293 +++++++ apps/sim/app/(landing)/models/utils.ts | 790 ++++++++++++++++++ apps/sim/app/sitemap.ts | 17 +- 15 files changed, 2715 insertions(+), 51 deletions(-) create mode 100644 apps/sim/app/(landing)/components/landing-faq.tsx create mode 100644 apps/sim/app/(landing)/models/[provider]/[model]/opengraph-image.tsx create mode 100644 apps/sim/app/(landing)/models/[provider]/[model]/page.tsx create mode 100644 apps/sim/app/(landing)/models/[provider]/opengraph-image.tsx create mode 100644 apps/sim/app/(landing)/models/[provider]/page.tsx create mode 100644 apps/sim/app/(landing)/models/components/model-directory.tsx create mode 100644 apps/sim/app/(landing)/models/components/model-primitives.tsx create mode 100644 apps/sim/app/(landing)/models/layout.tsx create mode 100644 apps/sim/app/(landing)/models/og-utils.tsx create mode 100644 apps/sim/app/(landing)/models/opengraph-image.tsx create mode 100644 apps/sim/app/(landing)/models/page.tsx create mode 100644 apps/sim/app/(landing)/models/utils.ts diff --git a/apps/sim/app/(home)/components/footer/footer.tsx b/apps/sim/app/(home)/components/footer/footer.tsx index 0f167ee787f..33ef8dcf9b0 100644 --- a/apps/sim/app/(home)/components/footer/footer.tsx +++ b/apps/sim/app/(home)/components/footer/footer.tsx @@ -26,6 +26,7 @@ const RESOURCES_LINKS: FooterItem[] = [ { label: 'Blog', href: '/blog' }, // { label: 'Templates', href: '/templates' }, { label: 'Docs', href: 'https://docs.sim.ai', external: true }, + { label: 'Models', href: '/models' }, // { label: 'Academy', href: '/academy' }, { label: 'Partners', href: '/partners' }, { label: 'Careers', href: 'https://jobs.ashbyhq.com/sim', external: true }, diff --git a/apps/sim/app/(landing)/components/landing-faq.tsx b/apps/sim/app/(landing)/components/landing-faq.tsx new file mode 100644 index 00000000000..435d0ca8e90 --- /dev/null +++ b/apps/sim/app/(landing)/components/landing-faq.tsx @@ -0,0 +1,63 @@ +'use client' + +import { useState } from 'react' +import { ChevronDown } from '@/components/emcn' +import { cn } from '@/lib/core/utils/cn' + +export interface LandingFAQItem { + question: string + answer: string +} + +interface LandingFAQProps { + faqs: LandingFAQItem[] +} + +export function LandingFAQ({ faqs }: LandingFAQProps) { + const [openIndex, setOpenIndex] = useState(0) + + return ( +
+ {faqs.map(({ question, answer }, index) => { + const isOpen = openIndex === index + + return ( +
+ + + {isOpen && ( +
+

+ {answer} +

+
+ )} +
+ ) + })} +
+ ) +} diff --git a/apps/sim/app/(landing)/integrations/[slug]/components/integration-faq.tsx b/apps/sim/app/(landing)/integrations/[slug]/components/integration-faq.tsx index a0c6a8e1c70..6c71bc3e8e5 100644 --- a/apps/sim/app/(landing)/integrations/[slug]/components/integration-faq.tsx +++ b/apps/sim/app/(landing)/integrations/[slug]/components/integration-faq.tsx @@ -1,8 +1,4 @@ -'use client' - -import { useState } from 'react' -import { ChevronDown } from '@/components/emcn' -import { cn } from '@/lib/core/utils/cn' +import { LandingFAQ } from '@/app/(landing)/components/landing-faq' import type { FAQItem } from '@/app/(landing)/integrations/data/types' interface IntegrationFAQProps { @@ -10,49 +6,5 @@ interface IntegrationFAQProps { } export function IntegrationFAQ({ faqs }: IntegrationFAQProps) { - const [openIndex, setOpenIndex] = useState(0) - - return ( -
- {faqs.map(({ question, answer }, index) => { - const isOpen = openIndex === index - return ( -
- - - {isOpen && ( -
-

- {answer} -

-
- )} -
- ) - })} -
- ) + return } diff --git a/apps/sim/app/(landing)/models/[provider]/[model]/opengraph-image.tsx b/apps/sim/app/(landing)/models/[provider]/[model]/opengraph-image.tsx new file mode 100644 index 00000000000..0d7911dc110 --- /dev/null +++ b/apps/sim/app/(landing)/models/[provider]/[model]/opengraph-image.tsx @@ -0,0 +1,42 @@ +import { notFound } from 'next/navigation' +import { createModelsOgImage } from '@/app/(landing)/models/og-utils' +import { + formatPrice, + formatTokenCount, + getModelBySlug, + getProviderBySlug, +} from '@/app/(landing)/models/utils' + +export const runtime = 'edge' +export const contentType = 'image/png' +export const size = { + width: 1200, + height: 630, +} + +export default async function Image({ + params, +}: { + params: Promise<{ provider: string; model: string }> +}) { + const { provider: providerSlug, model: modelSlug } = await params + const provider = getProviderBySlug(providerSlug) + const model = getModelBySlug(providerSlug, modelSlug) + + if (!provider || !model) { + notFound() + } + + return createModelsOgImage({ + eyebrow: `${provider.name} model`, + title: model.displayName, + subtitle: `${provider.name} pricing, context window, and feature support generated from Sim's model registry.`, + pills: [ + `Input ${formatPrice(model.pricing.input)}/1M`, + `Output ${formatPrice(model.pricing.output)}/1M`, + model.contextWindow ? `${formatTokenCount(model.contextWindow)} context` : 'Unknown context', + model.capabilityTags[0] ?? 'Capabilities tracked', + ], + domainLabel: `sim.ai${model.href}`, + }) +} diff --git a/apps/sim/app/(landing)/models/[provider]/[model]/page.tsx b/apps/sim/app/(landing)/models/[provider]/[model]/page.tsx new file mode 100644 index 00000000000..fd7557e37c7 --- /dev/null +++ b/apps/sim/app/(landing)/models/[provider]/[model]/page.tsx @@ -0,0 +1,390 @@ +import type { Metadata } from 'next' +import Link from 'next/link' +import { notFound } from 'next/navigation' +import { getBaseUrl } from '@/lib/core/utils/urls' +import { LandingFAQ } from '@/app/(landing)/components/landing-faq' +import { + Breadcrumbs, + CapabilityTags, + DetailItem, + ModelCard, + ProviderIcon, + StatCard, +} from '@/app/(landing)/models/components/model-primitives' +import { + ALL_CATALOG_MODELS, + buildModelCapabilityFacts, + buildModelFaqs, + formatPrice, + formatTokenCount, + formatUpdatedAt, + getModelBySlug, + getPricingBounds, + getProviderBySlug, + getRelatedModels, +} from '@/app/(landing)/models/utils' + +const baseUrl = getBaseUrl() + +export async function generateStaticParams() { + return ALL_CATALOG_MODELS.map((model) => ({ + provider: model.providerSlug, + model: model.slug, + })) +} + +export async function generateMetadata({ + params, +}: { + params: Promise<{ provider: string; model: string }> +}): Promise { + const { provider: providerSlug, model: modelSlug } = await params + const provider = getProviderBySlug(providerSlug) + const model = getModelBySlug(providerSlug, modelSlug) + + if (!provider || !model) { + return {} + } + + return { + title: `${model.displayName} Pricing, Context Window, and Features`, + description: `${model.displayName} by ${provider.name}: pricing, cached input cost, output cost, context window, and capability support. Explore the full generated model page on Sim.`, + keywords: [ + model.displayName, + `${model.displayName} pricing`, + `${model.displayName} context window`, + `${model.displayName} features`, + `${provider.name} ${model.displayName}`, + `${provider.name} model pricing`, + ...model.capabilityTags, + ], + openGraph: { + title: `${model.displayName} Pricing, Context Window, and Features | Sim`, + description: `${model.displayName} by ${provider.name}: pricing, context window, and model capability details.`, + url: `${baseUrl}${model.href}`, + type: 'website', + images: [ + { + url: `${baseUrl}${model.href}/opengraph-image`, + width: 1200, + height: 630, + alt: `${model.displayName} on Sim`, + }, + ], + }, + twitter: { + card: 'summary_large_image', + title: `${model.displayName} | Sim`, + description: model.summary, + images: [ + { url: `${baseUrl}${model.href}/opengraph-image`, alt: `${model.displayName} on Sim` }, + ], + }, + alternates: { + canonical: `${baseUrl}${model.href}`, + }, + } +} + +export default async function ModelPage({ + params, +}: { + params: Promise<{ provider: string; model: string }> +}) { + const { provider: providerSlug, model: modelSlug } = await params + const provider = getProviderBySlug(providerSlug) + const model = getModelBySlug(providerSlug, modelSlug) + + if (!provider || !model) { + notFound() + } + + const faqs = buildModelFaqs(provider, model) + const capabilityFacts = buildModelCapabilityFacts(model) + const pricingBounds = getPricingBounds(model.pricing) + const relatedModels = getRelatedModels(model, 6) + + const breadcrumbJsonLd = { + '@context': 'https://schema.org', + '@type': 'BreadcrumbList', + itemListElement: [ + { '@type': 'ListItem', position: 1, name: 'Home', item: baseUrl }, + { '@type': 'ListItem', position: 2, name: 'Models', item: `${baseUrl}/models` }, + { '@type': 'ListItem', position: 3, name: provider.name, item: `${baseUrl}${provider.href}` }, + { + '@type': 'ListItem', + position: 4, + name: model.displayName, + item: `${baseUrl}${model.href}`, + }, + ], + } + + const productJsonLd = { + '@context': 'https://schema.org', + '@type': 'Product', + name: model.displayName, + brand: provider.name, + category: 'AI language model', + description: model.summary, + sku: model.id, + offers: { + '@type': 'AggregateOffer', + priceCurrency: 'USD', + lowPrice: pricingBounds.lowPrice.toString(), + highPrice: pricingBounds.highPrice.toString(), + }, + } + + const faqJsonLd = { + '@context': 'https://schema.org', + '@type': 'FAQPage', + mainEntity: faqs.map((faq) => ({ + '@type': 'Question', + name: faq.question, + acceptedAnswer: { + '@type': 'Answer', + text: faq.answer, + }, + })), + } + + return ( + <> +