From 00bae1115776c3f9f1572b1a965acf8fe49be74a Mon Sep 17 00:00:00 2001 From: Cameron Taylor <50385537+ct3685@users.noreply.github.com> Date: Fri, 15 May 2026 12:12:19 -0400 Subject: [PATCH 1/7] fix: restore Atlassian MCP OAuth by removing stale Passport dependency (#1068) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary **Commit 1 — fix: Atlassian MCP OAuth 500 error** - **Root cause:** Commit `0ae736e41` intentionally disabled Passport.js session infrastructure when the AAI JWT/API-key auth system was integrated. The `atlassian-dynamic` Passport strategy was left in place but never ran because Passport was never initialized — causing a hard `500: Unknown authentication strategy "atlassian-dynamic"` on every OAuth callback. - **Fix:** Removed Passport from the Atlassian auth flow entirely. Token exchange logic now lives directly in the controller using the already-existing utility functions `exchangeCodeForTokens`, `createCompleteCredentialData`, `clearPendingRegistration` from `utils/index.ts`. - **Removed dead code:** The `GET /api/v1/atlassian-auth/` root route and `authenticate` controller method (both solely called `passport.authenticate(...)`) are gone. **Commit 2 — feat: pre-fetch cloudId context to prevent null cloudId errors (global fix for all Atlassian actions)** - **Root cause:** The Atlassian Remote MCP server requires a `cloudId` parameter for almost every Jira/Confluence tool call (`add_comment_to_jira_issue`, `search_jira_issues`, `create_jira_issue`, `write:confluence-content`, etc.). Without upfront knowledge of it, the LLM would pass `null`, receive a `-32602` validation error, then self-correct by calling `get_accessible_resources` before retrying — one wasted round-trip on every first call. - **Fix is global across all Atlassian MCP actions:** During `getTools()` initialization, the node invokes `get_accessible_resources` using its own `MCPTool` instance (no new MCP SDK imports), parses the cloud resource list, then iterates **every tool** returned by the MCP server and appends a cloudId hint to the description of any tool that declares a `cloudId` parameter in its zod schema. This covers all current and future Atlassian MCP tools automatically — no per-action changes needed. - Single site: `[cloudId for this Atlassian site: "8ca3c755..." (lastrev — https://lastrev.atlassian.net)]` - Multi-site: `[Available Atlassian cloudIds: "8ca3c755..." → lastrev, "f08c36cd..." → lastrev-new]` - Falls back silently if the pre-fetch fails, so no regression on errors. - Node version bumped `1.0 → 1.1`. ## Files changed | File | Change | |------|--------| | `packages/server/src/controllers/atlassian-auth/index.ts` | Removed Passport; inlined OAuth flow using existing utilities; removed dead `authenticate` method | | `packages/server/src/routes/atlassian-auth/index.ts` | Removed Passport import and middleware; removed dead root route | | `packages/server/src/config/passport.ts` | Removed `atlassian-dynamic` CustomStrategy and its unused imports | | `packages/components/nodes/tools/MCP/Atlassian/AtlassianMcp.ts` | Added `fetchCloudResources` + `enrichToolsWithCloudContext`; bumped version to 1.1 | ## OAuth callback flow (unchanged behavior) 1. UI calls `GET /api/v1/atlassian-auth/mcp-initialize` → registers dynamic OAuth client, returns `sessionId`, `client_id`, `authorization_endpoint`, `redirect_uri`, `scope` 2. UI opens popup → user authenticates → Atlassian redirects to `/api/v1/atlassian-auth/callback?code=...&state=` 3. Controller exchanges code for tokens using stored client credentials → builds credential object → posts `AUTH_SUCCESS` to opener ## Test plan - [ ] Start local dev server (`pnpm dev`) - [ ] Navigate to Credentials and create a new Atlassian MCP credential - [ ] Click "Connect with Atlassian" — popup opens and completes without 500 error - [ ] Save credential and add Atlassian MCP node to a chatflow - [ ] Verify the node's "Available Actions" dropdown loads correctly (cloudId hints visible in descriptions) - [ ] Run a Jira action (comment, search, create) — confirm it succeeds on the **first** attempt without a `get_accessible_resources` round-trip - [ ] Run a Confluence action — confirm same first-attempt success - [ ] Test with multiple Atlassian sites connected — confirm all cloudIds appear in the hint - [ ] Verify token refresh still works (`refreshStoredCredentialTokens` path unchanged) - [ ] Verify Salesforce and Google OAuth flows are unaffected ## Target branch `staging` --- package.json | 11 ++- .../nodes/tools/MCP/Atlassian/AtlassianMcp.ts | 71 ++++++++++++++-- packages/server/src/config/passport.ts | 84 ------------------- .../src/controllers/atlassian-auth/index.ts | 73 +++++++--------- .../server/src/routes/atlassian-auth/index.ts | 6 +- 5 files changed, 109 insertions(+), 136 deletions(-) diff --git a/package.json b/package.json index cc6b0561024..7a760cfed64 100644 --- a/package.json +++ b/package.json @@ -120,10 +120,19 @@ }, "pnpm": { "onlyBuiltDependencies": [ + "@parcel/watcher", + "@prisma/client", + "@prisma/engines", + "@swc/core", + "bufferutil", "cypress", "esbuild", "faiss-node", - "sqlite3" + "prisma", + "puppeteer", + "sharp", + "sqlite3", + "utf-8-validate" ], "overrides": { "@opentelemetry/sdk-trace-base": "1.27.0", diff --git a/packages/components/nodes/tools/MCP/Atlassian/AtlassianMcp.ts b/packages/components/nodes/tools/MCP/Atlassian/AtlassianMcp.ts index 1f187170f82..18e5eaddf12 100644 --- a/packages/components/nodes/tools/MCP/Atlassian/AtlassianMcp.ts +++ b/packages/components/nodes/tools/MCP/Atlassian/AtlassianMcp.ts @@ -9,6 +9,9 @@ * - Handles token refresh automatically before MCP initialization * - Connects to Atlassian's remote MCP server via SSE transport * - Supports both JIRA and Confluence through single integration + * - Pre-fetches get_accessible_resources at init time and injects cloudId context + * into the description of every tool that requires it, so the LLM never has to + * guess or discover the cloudId with a separate round-trip * * Required environment variables: * - ATLASSIAN_CLIENT_ID @@ -23,6 +26,13 @@ import { MCPToolkit } from '../core' import { getCredentialData } from '../../../../src/utils' import { ATLASSIAN_MCP_SERVER_URL } from '../../../../src/constants' +interface AtlassianCloudResource { + id: string + url: string + name: string + scopes?: string[] +} + class Atlassian_MCP implements INode { label: string name: string @@ -41,7 +51,7 @@ class Atlassian_MCP implements INode { constructor() { this.label = 'Atlassian MCP' this.name = 'atlassianMcp' - this.version = 1.0 + this.version = 1.1 this.type = 'Atlassian MCP Tool' this.icon = 'atlassian.svg' this.category = 'Tools (MCP)' @@ -71,7 +81,6 @@ class Atlassian_MCP implements INode { loadMethods = { listActions: async (nodeData: INodeData, options: ICommonObject): Promise => { try { - // Check if credential exists first if (!nodeData.credential) { return [ { @@ -108,7 +117,7 @@ class Atlassian_MCP implements INode { const tools = await this.getTools(nodeData, options) const _mcpActions = nodeData.inputs?.mcpActions - let mcpActions = [] + let mcpActions: string[] = [] if (_mcpActions) { try { mcpActions = typeof _mcpActions === 'string' ? JSON.parse(_mcpActions) : _mcpActions @@ -121,8 +130,6 @@ class Atlassian_MCP implements INode { } async getTools(nodeData: INodeData, options: ICommonObject): Promise { - // Token refresh is handled automatically by server before node initialization - // So we can directly use the access token from credential data const credentialData = await getCredentialData(nodeData.credential || '', options) if (!credentialData.access_token) { @@ -139,8 +146,62 @@ class Atlassian_MCP implements INode { const tools = toolkit.tools ?? [] + // Pre-fetch accessible cloud resources and inject cloudId context into + // the description of every tool that declares a cloudId parameter. + // This prevents the LLM from passing null/undefined and eliminates the + // need for a discovery round-trip during the actual conversation. + const getResourcesTool = tools.find((t) => t.name === 'get_accessible_resources') + if (getResourcesTool) { + const cloudResources = await this.fetchCloudResources(getResourcesTool) + if (cloudResources.length > 0) { + this.enrichToolsWithCloudContext(tools, cloudResources) + } + } + return tools } + + /** + * Calls get_accessible_resources via its existing MCP tool and returns the + * parsed list of Atlassian cloud sites available to the authenticated user. + * Returns an empty array on any error so the caller degrades gracefully. + */ + private async fetchCloudResources(getResourcesTool: Tool): Promise { + try { + // tool.invoke({}) returns JSON.stringify(res.content) from the MCP server, + // which is an array of content blocks, e.g.: + // [{"type":"text","text":"[{\"id\":\"...\",\"url\":\"...\",\"name\":\"...\"}]"}] + const rawResult = await getResourcesTool.invoke({}) + const contentBlocks = JSON.parse(rawResult) + const textBlock = Array.isArray(contentBlocks) ? contentBlocks.find((c: any) => c.type === 'text') : null + if (!textBlock?.text) return [] + + const resources = JSON.parse(textBlock.text) + return Array.isArray(resources) ? resources.filter((r: any) => r.id && r.url) : [] + } catch (err) { + console.warn('[Atlassian MCP] Could not pre-fetch cloud resources — cloudId context will not be injected:', err) + return [] + } + } + + /** + * Appends a human-readable cloudId hint to the description of every tool + * that declares a `cloudId` parameter in its zod schema. Mutates in-place. + * The LLM sees this hint in the tool description before choosing arguments. + */ + private enrichToolsWithCloudContext(tools: Tool[], resources: AtlassianCloudResource[]): void { + const cloudNote = + resources.length === 1 + ? ` [cloudId for this Atlassian site: "${resources[0].id}" (${resources[0].name} — ${resources[0].url})]` + : ` [Available Atlassian cloudIds: ${resources.map((r) => `"${r.id}" → ${r.name} (${r.url})`).join(', ')}]` + + for (const t of tools) { + const shape = (t as any).schema?.shape + if (shape && 'cloudId' in shape) { + t.description = `${t.description}${cloudNote}` + } + } + } } module.exports = { nodeClass: Atlassian_MCP } diff --git a/packages/server/src/config/passport.ts b/packages/server/src/config/passport.ts index 8fb0a998ee7..b6b4c9c853f 100644 --- a/packages/server/src/config/passport.ts +++ b/packages/server/src/config/passport.ts @@ -1,7 +1,5 @@ import { Strategy as GoogleStrategy } from 'passport-google-oauth20' import { Strategy as OAuth2Strategy } from 'passport-oauth2' -import { Strategy as CustomStrategy } from 'passport-custom' -import { fetchMCPMetadata } from '../utils/mcp-metadata' import { configureAuth0Strategy } from '../aai/auth/auth0Strategy' export default function (passport: any) { @@ -90,88 +88,6 @@ export default function (passport: any) { ) } - // Atlassian MCP OAuth Strategy - // Import the OAuth utilities from utils - const { getPendingRegistration, clearPendingRegistration, createCompleteCredentialData } = require('../utils') - - passport.use( - 'atlassian-dynamic', - new CustomStrategy(async (req: any, done: any) => { - try { - const code = req.query?.code as string - const state = req.query?.state as string - const error = req.query?.error as string - - if (error) { - return done(null, false, { message: `OAuth error: ${error}` }) - } - - if (!code) { - return done(null, false, { message: 'Authorization code missing' }) - } - - // Get MCP client info from state parameter (sessionId) - const sessionId = state - const mcpClientInfo = sessionId ? getPendingRegistration(sessionId) : null - - if (!mcpClientInfo) { - return done(null, false, { message: 'MCP client info not found. Please restart the OAuth flow.' }) - } - - // Fetch MCP metadata and use MCP client credentials - const metadata = await fetchMCPMetadata() - const tokenURL = metadata.token_endpoint - const clientId = mcpClientInfo.client_id - const clientSecret = mcpClientInfo.client_secret - - // Exchange authorization code for tokens - const tokenResponse = await fetch(tokenURL, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - Accept: 'application/json' - }, - body: new URLSearchParams({ - grant_type: 'authorization_code', - client_id: clientId, - client_secret: clientSecret, - code: code, - redirect_uri: `${process.env.API_HOST}/api/v1/atlassian-auth/callback` - }) - }) - - if (!tokenResponse.ok) { - const errorText = await tokenResponse.text() - console.error('Token exchange failed:', errorText) - return done(null, false, { message: `Token exchange failed: ${tokenResponse.status} ${tokenResponse.statusText}` }) - } - - const tokenData = await tokenResponse.json() - - // Use the centralized function to create complete credential data - const tokens = { - access_token: tokenData.access_token, - refresh_token: tokenData.refresh_token, - expires_in: tokenData.expires_in || 3600 - } - - const baseCredentialData = { - userInfo: {} // We'll skip profile fetching for now - } - - const newCredential = createCompleteCredentialData(sessionId, tokens, baseCredentialData) - - // Clean up the temporary session - clearPendingRegistration(sessionId) - - done(null, newCredential) - } catch (error) { - console.error('Atlassian OAuth Error:', error) - done(error, undefined) - } - }) - ) - passport.serializeUser((user: any, done: any) => { done(null, user) }) diff --git a/packages/server/src/controllers/atlassian-auth/index.ts b/packages/server/src/controllers/atlassian-auth/index.ts index 281d42a19a9..3de4beb039d 100644 --- a/packages/server/src/controllers/atlassian-auth/index.ts +++ b/packages/server/src/controllers/atlassian-auth/index.ts @@ -1,64 +1,56 @@ import { Request, Response, NextFunction } from 'express' import { InternalFlowiseError } from '../../errors/internalFlowiseError' import { StatusCodes } from 'http-status-codes' -import passport from 'passport' -import { registerOAuthClient } from '../../utils' - -// MCP OAuth controller - uses centralized OAuth utilities - -const authenticate = async (req: Request, res: Response, next: NextFunction) => { - try { - passport.authenticate('atlassian-dynamic')(req, res, next) - } catch (error) { - // eslint-disable-next-line no-console - console.log('Error: Atlassian MCP authController.authenticate', error) - next(error) - } -} - -const atlassianAuthCallback = async (req: Request, res: Response) => { - try { - if (!req.user) { - throw new InternalFlowiseError(StatusCodes.UNAUTHORIZED, 'Error: Atlassian authController.callback - Authentication failed') - } +import { registerOAuthClient, exchangeCodeForTokens, createCompleteCredentialData, clearPendingRegistration } from '../../utils' +const atlassianAuthCallback = async (req: Request, res: Response, next: NextFunction) => { + const sendPopupMessage = (payload: object) => { + const json = JSON.stringify(payload) res.setHeader('Content-Type', 'text/html') res.send(` `) + } + + try { + const code = req.query?.code as string + const state = req.query?.state as string + const error = req.query?.error as string + + if (error) { + return sendPopupMessage({ type: 'AUTH_ERROR', error: `OAuth error: ${error}` }) + } + + if (!code) { + throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, 'Error: atlassianAuthController.callback - authorization code missing') + } + + if (!state) { + throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, 'Error: atlassianAuthController.callback - state parameter missing') + } + + const redirectUri = `${process.env.API_HOST}/api/v1/atlassian-auth/callback` + const tokens = await exchangeCodeForTokens(state, code, redirectUri) + const credential = createCompleteCredentialData(state, tokens, {}) + clearPendingRegistration(state) + + return sendPopupMessage({ type: 'AUTH_SUCCESS', user: credential }) } catch (error) { console.error('Atlassian auth callback error:', error) - res.send(` - - - - - - `) + next(error) } } -const mcpInitialize = async (req: Request, res: Response) => { +const mcpInitialize = async (req: Request, res: Response, next: NextFunction) => { try { const redirectUri = `${process.env.API_HOST}/api/v1/atlassian-auth/callback` @@ -82,7 +74,6 @@ const mcpInitialize = async (req: Request, res: Response) => { } export default { - authenticate, atlassianAuthCallback, mcpInitialize } diff --git a/packages/server/src/routes/atlassian-auth/index.ts b/packages/server/src/routes/atlassian-auth/index.ts index 6745cddbdca..ed0f6e783d9 100644 --- a/packages/server/src/routes/atlassian-auth/index.ts +++ b/packages/server/src/routes/atlassian-auth/index.ts @@ -1,14 +1,10 @@ import express from 'express' -import passport from 'passport' import atlassianAuthController from '../../controllers/atlassian-auth' const router = express.Router() -// GET /api/v1/atlassian-auth/ -router.get('/', atlassianAuthController.authenticate) - // GET /api/v1/atlassian-auth/callback -router.get('/callback', passport.authenticate('atlassian-dynamic', { session: false }), atlassianAuthController.atlassianAuthCallback) +router.get('/callback', atlassianAuthController.atlassianAuthCallback) // GET /api/v1/atlassian-auth/mcp-initialize router.get('/mcp-initialize', atlassianAuthController.mcpInitialize) From 6451d8397538c2721355181577b3445423e7e003 Mon Sep 17 00:00:00 2001 From: Cameron Taylor <50385537+ct3685@users.noreply.github.com> Date: Fri, 15 May 2026 12:15:01 -0400 Subject: [PATCH 2/7] chore: update dependencies for improved compatibility and functionality (#1070) - Updated `glob` from version `^11.1.0` to `^13.0.6` in `package.json` and `pnpm-lock.yaml` for enhanced performance and features. - Updated `axios` from version `^1.13.5` to `^1.15.1` in `scripts/bws-secure/package.json` for better stability and security. - Updated `dotenv` from version `^17.2.4` to `^17.4.2` in `scripts/bws-secure/package.json` to leverage the latest improvements. - Added new utility functions in `bws-env-utils.js` for better handling of multi-project IDs and environment variable parsing. - Enhanced `secureRun.js` to support loading secrets from multiple BWS project IDs with improved logging and error handling. - Updated documentation in `README.md` and added a new guide for multi-project ID support in `MULTI_PROJECT_ID_GUIDE.md`. This update aims to streamline the environment variable management process and improve overall project maintainability. --- README.md | 29 +- package.json | 2 +- pnpm-lock.yaml | 381 +++++------- scripts/bws-secure/README.md | 7 +- scripts/bws-secure/bws-dotenv.js | 117 ++-- scripts/bws-secure/bws-env-utils.js | 119 ++++ .../check-vars/requiredRuntimeVars.js | 5 +- .../guides/MULTI_PROJECT_ID_GUIDE.md | 282 +++++++++ scripts/bws-secure/install.sh | 71 ++- scripts/bws-secure/package.json | 11 +- scripts/bws-secure/secureRun.js | 558 ++++++++++++------ scripts/bws-secure/tests/README.md | 40 +- .../bws-secure/tests/bws-env-utils.test.mjs | 55 ++ scripts/bws-secure/tests/e2e/env-sink.mjs | 16 + scripts/bws-secure/tests/e2e/fake-bws.mjs | 81 +++ scripts/bws-secure/tests/e2e/helpers.mjs | 159 +++++ .../tests/e2e/secureRun.e2e.test.mjs | 235 ++++++++ .../update-environments/map-env-files.js | 69 ++- .../bws-secure/update-environments/netlify.js | 113 ++-- 19 files changed, 1788 insertions(+), 562 deletions(-) create mode 100644 scripts/bws-secure/bws-env-utils.js create mode 100644 scripts/bws-secure/guides/MULTI_PROJECT_ID_GUIDE.md create mode 100644 scripts/bws-secure/tests/bws-env-utils.test.mjs create mode 100644 scripts/bws-secure/tests/e2e/env-sink.mjs create mode 100644 scripts/bws-secure/tests/e2e/fake-bws.mjs create mode 100644 scripts/bws-secure/tests/e2e/helpers.mjs create mode 100644 scripts/bws-secure/tests/e2e/secureRun.e2e.test.mjs diff --git a/README.md b/README.md index 1a1c14ce02c..fefe52fe7d7 100644 --- a/README.md +++ b/README.md @@ -498,34 +498,43 @@ This project uses [BWS Secure](https://github.com/last-rev-llc/bws-secure) for m ⚠️ **5.** Never commit this token to version control -### 🎯 Token Usage Options: +### 🎯 Token and project options -- **BWS_ACCESS_TOKEN**: Loads ALL projects associated with that token (recommended for multi-project setups) -- **BWS_PROJECT_ID**: Loads only a specific project (use for single-project or testing scenarios) +- **BWS_ACCESS_TOKEN**: Required to load secrets from Bitwarden (scoped to your machine account). +- **BWS_PROJECT_ID** (optional): Restrict to one or more BWS **project UUIDs**. Use a **single** UUID, or **comma-separated UUIDs** to merge projects (later IDs win when the same key exists in more than one project). Omit to use `bwsconfig.json` / project selection. Fully backward compatible with single-UUID setups. -**Example for single project:** +**Single project:** ``` BWS_PROJECT_ID=00000000-0000-0000-0000-000000000001 ``` -The project ID can be found in the Bitwarden Secrets Manager, within the list of projects. +**Multiple projects (optional):** +``` +BWS_PROJECT_ID=00000000-0000-0000-0000-000000000001, 11111111-1111-1111-1111-111111111111 +``` + +More detail: [Multi-project ID guide](https://github.com/last-rev-llc/bws-secure/blob/main/guides/MULTI_PROJECT_ID_GUIDE.md). + +### Transient `.env.secure` files + +Encrypted `.env.secure` / `.env.secure.*` files in the repo root are **removed when each run finishes** (after your command runs; secrets are already in the process environment). Set **`BWS_KEEP_SECURE_FILES=true`** only when you need to inspect those files. -### 🔧 Common Issues & Troubleshooting: +### 🔧 Common Issues & Troubleshooting - **"No projects found"**: Verify your token has project access permissions in Bitwarden -- **"Access denied"**: Check that the Machine Account has read permissions for the target projects +- **"Access denied"**: Check that the Machine Account has read permissions for the target projects - **Token not working**: Ensure no extra spaces when copying from Bitwarden -- **Multiple projects loading**: This is normal with BWS_ACCESS_TOKEN - use BWS_PROJECT_ID for single project +- **Multiple projects / overlays**: Order matters for duplicate keys—see the multi-project guide above ### Updating BWS Secure To update BWS Secure to the latest version, you can use the convenient script that was added to your package.json: ```bash -npm run bws-update # Or use your project's package manager: yarn bws-update, pnpm bws-update +npm run bws-update # Or: yarn bws-update, pnpm bws-update ``` -Alternatively, you can run the following command manually from your project root: +Alternatively, from your project root: ```bash rm -rf scripts/bws-secure && git clone git@github.com:last-rev-llc/bws-secure.git scripts/bws-secure && rm -rf scripts/bws-secure/.git && bash scripts/bws-secure/install.sh diff --git a/package.json b/package.json index 7a760cfed64..ee4999f9b67 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-solid": "0.12.0", "eslint-plugin-unused-imports": "^2.0.0", - "glob": "^11.1.0", + "glob": "^13.0.6", "husky": "^8.0.3", "jsdom": "^24.1.3", "knip": "^5.83.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6ba03a6d2af..7752c90c57e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -85,7 +85,7 @@ importers: version: 7.18.0(typescript@5.5.4) axios: specifier: ^1.13.5 - version: 1.13.5(debug@4.4.3) + version: 1.13.5(debug@4.4.1) cursor-tools: specifier: latest version: 0.6.0-alpha.12(@cfworker/json-schema@4.1.1)(@playwright/test@1.55.0)(bufferutil@4.0.9)(deepmerge@4.3.1)(encoding@0.1.13)(utf-8-validate@6.0.5)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5)) @@ -129,8 +129,8 @@ importers: specifier: ^2.0.0 version: 2.0.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.4))(eslint@8.57.1)(typescript@5.5.4))(eslint@8.57.1) glob: - specifier: ^11.1.0 - version: 11.1.0 + specifier: ^13.0.6 + version: 13.0.6 husky: specifier: ^8.0.3 version: 8.0.3 @@ -232,7 +232,7 @@ importers: version: 1.27.3(bufferutil@4.0.9)(utf-8-validate@6.0.5) axios: specifier: ^1.13.3 - version: 1.13.5(debug@4.4.3) + version: 1.13.5(debug@4.4.1) chromadb: specifier: ^1.10.5 version: 1.10.5(@google/generative-ai@0.24.1)(cohere-ai@7.19.0(aws-crt@1.27.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(encoding@0.1.13))(encoding@0.1.13)(ollama@0.5.18)(openai@4.96.0(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76)) @@ -528,7 +528,7 @@ importers: version: 2.14.1(@types/react@18.2.15)(immer@11.0.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) axios: specifier: ^1.13.3 - version: 1.13.5(debug@4.4.3) + version: 1.13.5(debug@4.4.1) clsx: specifier: ^1.2.1 version: 1.2.1 @@ -682,7 +682,7 @@ importers: version: 4.25.2 axios: specifier: ^1.13.3 - version: 1.13.5(debug@4.4.3) + version: 1.13.5(debug@4.4.1) bufferutil: specifier: ^4.0.9 version: 4.0.9 @@ -870,7 +870,7 @@ importers: version: 4.1.8 tsc-watch: specifier: ^6.3.1 - version: 6.3.1(typescript@5.9.2) + version: 6.3.1(typescript@5.5.4) packages/components: dependencies: @@ -885,7 +885,7 @@ importers: version: 1.1.0(@cfworker/json-schema@4.1.1)(zod@3.25.76) '@answerai/salesforce-mcp': specifier: ^0.1.0 - version: 0.1.0(@types/node@25.2.2)(encoding@0.1.13) + version: 0.1.0(@types/node@18.15.11)(encoding@0.1.13) '@apidevtools/json-schema-ref-parser': specifier: ^12.0.2 version: 12.0.2 @@ -936,7 +936,7 @@ importers: version: 3.9.25 '@getzep/zep-cloud': specifier: ~1.0.7 - version: 1.0.12(@langchain/core@0.3.61(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76)))(encoding@0.1.13)(langchain@0.3.35(ccfa7d77c90b4f7e0954a3992e04e03a)) + version: 1.0.12(@langchain/core@0.3.61(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76)))(encoding@0.1.13)(langchain@0.3.35(2f09207fbcc7b04adb1a5c2b37882e8f)) '@getzep/zep-js': specifier: ^0.9.0 version: 0.9.0 @@ -960,7 +960,7 @@ importers: version: 2.8.1 '@jlinc/langchain': specifier: ^0.1.4 - version: 0.1.4(06a2b4abceed266cae999e7815581a2a) + version: 0.1.4(88efa4f237aaf519e63a84735e8044f7) '@langchain/anthropic': specifier: 0.3.33 version: 0.3.33(@langchain/core@0.3.61(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76)))(zod@3.25.76) @@ -975,7 +975,7 @@ importers: version: 0.0.7(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(aws-crt@1.27.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76)) '@langchain/community': specifier: ^0.3.47 - version: 0.3.47(a209da930d3d662083731a25d846ee59) + version: 0.3.47(b545b8e28d3cee1192afd7359426439f) '@langchain/core': specifier: 0.3.61 version: 0.3.61(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76)) @@ -1020,7 +1020,7 @@ importers: version: 0.0.1(@langchain/core@0.3.61(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76)))(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5)) '@mem0/community': specifier: ^0.0.1 - version: 0.0.1(f670af68d36a1e09d6e06f42f2080e6d) + version: 0.0.1(a49a8ee6b857562bee536bea16cf5e44) '@mendable/firecrawl-js': specifier: ^1.18.2 version: 1.29.3 @@ -1065,7 +1065,7 @@ importers: version: 2.57.4(bufferutil@4.0.9)(utf-8-validate@6.0.5) '@tsmztech/mcp-server-salesforce': specifier: ^0.0.2 - version: 0.0.2(@types/node@25.2.2)(encoding@0.1.13) + version: 0.0.2(@types/node@18.15.11)(encoding@0.1.13) '@types/js-yaml': specifier: ^4.0.5 version: 4.0.9 @@ -1170,13 +1170,13 @@ importers: version: 3.13.0 langchain: specifier: ^0.3.5 - version: 0.3.35(ccfa7d77c90b4f7e0954a3992e04e03a) + version: 0.3.35(2f09207fbcc7b04adb1a5c2b37882e8f) langfuse: specifier: 3.3.4 version: 3.3.4 langfuse-langchain: specifier: ^3.3.4 - version: 3.37.6(langchain@0.3.35(ccfa7d77c90b4f7e0954a3992e04e03a)) + version: 3.37.6(langchain@0.3.35(2f09207fbcc7b04adb1a5c2b37882e8f)) langsmith: specifier: 0.1.6 version: 0.1.6 @@ -1278,7 +1278,7 @@ importers: version: 3.0.1(@cfworker/json-schema@4.1.1)(bufferutil@4.0.9)(utf-8-validate@6.0.5) typeorm: specifier: ^0.3.6 - version: 0.3.26(babel-plugin-macros@3.1.0)(ioredis@5.7.0)(mongodb@6.3.0(@aws-sdk/credential-providers@3.887.0(aws-crt@1.27.3(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(socks@2.8.7))(mysql2@3.14.5)(pg@8.16.3)(redis@4.7.1)(reflect-metadata@0.1.14)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@25.2.2)(typescript@5.5.4)) + version: 0.3.26(babel-plugin-macros@3.1.0)(ioredis@5.7.0)(mongodb@6.3.0(@aws-sdk/credential-providers@3.887.0(aws-crt@1.27.3(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(socks@2.8.7))(mysql2@3.14.5)(pg@8.16.3)(redis@4.7.1)(reflect-metadata@0.1.14)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@18.15.11)(typescript@5.5.4)) weaviate-ts-client: specifier: ^1.1.0 version: 1.6.0(encoding@0.1.13)(graphql@16.11.0) @@ -1342,13 +1342,13 @@ importers: version: 4.0.2 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@25.2.2)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@25.2.2)(typescript@5.5.4)) + version: 29.7.0(@types/node@18.15.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@18.15.11)(typescript@5.5.4)) rimraf: specifier: ^5.0.5 version: 5.0.10 ts-jest: specifier: ^29.3.2 - version: 29.4.1(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@30.0.5)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.0.5)(jest@29.7.0(@types/node@25.2.2)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@25.2.2)(typescript@5.5.4)))(typescript@5.5.4) + version: 29.4.1(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@30.0.5)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.0.5)(jest@29.7.0(@types/node@18.15.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@18.15.11)(typescript@5.5.4)))(typescript@5.5.4) tsc-watch: specifier: ^6.0.4 version: 6.3.1(typescript@5.5.4) @@ -1451,7 +1451,7 @@ importers: version: 1.5.0 axios: specifier: ^1.12.2 - version: 1.13.5(debug@4.4.3) + version: 1.13.5(debug@4.4.1) cors: specifier: ^2.8.5 version: 2.8.6 @@ -1955,7 +1955,7 @@ importers: version: 0.5.8 '@types/cookie-parser': specifier: ^1.4.7 - version: 1.4.10(@types/express@5.0.6) + version: 1.4.10(@types/express@4.17.25) '@types/cors': specifier: ^2.8.12 version: 2.8.19 @@ -2144,10 +2144,10 @@ importers: version: 17.2.4 flowise-embed: specifier: latest - version: 3.0.5 + version: 3.1.6 flowise-embed-react: specifier: latest - version: 3.0.5(@types/node@25.2.2)(flowise-embed@3.0.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.92.1)(terser@5.44.0)(typescript@5.5.4) + version: 3.1.6(@types/node@25.2.2)(flowise-embed@3.1.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.92.1)(terser@5.44.0)(typescript@5.5.4) flowise-react-json-view: specifier: '*' version: 1.21.7(@types/react@19.2.2)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -2280,7 +2280,7 @@ importers: version: 4.7.0(vite@5.4.21(@types/node@25.2.2)(sass@1.92.1)(terser@5.44.0)) pretty-quick: specifier: ^3.1.3 - version: 3.3.1(prettier@3.8.1) + version: 3.3.1(prettier@2.8.8) react-scripts: specifier: ^5.0.1 version: 5.0.1(@babel/plugin-syntax-flow@7.27.1(@babel/core@7.29.0))(@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.29.0))(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/babel__core@7.20.5)(bufferutil@4.0.9)(canvas@2.11.2(encoding@0.1.13))(eslint@8.57.1)(react@18.3.1)(sass@1.92.1)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@25.2.2)(typescript@5.5.4))(tsx@4.21.0)(type-fest@4.41.0)(typescript@5.5.4)(utf-8-validate@6.0.5) @@ -5471,79 +5471,67 @@ packages: resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm@1.0.5': resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-s390x@1.0.4': resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-x64@1.0.4': resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.0.4': resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.0.4': resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-linux-arm64@0.33.5': resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-linux-arm@0.33.5': resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-linux-s390x@0.33.5': resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-linux-x64@0.33.5': resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-linuxmusl-arm64@0.33.5': resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-x64@0.33.5': resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-wasm32@0.33.5': resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} @@ -6750,7 +6738,7 @@ packages: '@mui/base@5.0.0-beta.27': resolution: {integrity: sha512-duL37qxihT1N0pW/gyXVezP7SttLkF+cLAs/y6g6ubEFmVadjbnZ45SeF12/vAiKzqwf5M0uFH1cczIPXFZygA==} engines: {node: '>=12.0.0'} - deprecated: This package has been replaced by @base-ui-components/react + deprecated: This package has been replaced by @base-ui/react peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 react: ^17.0.0 || ^18.0.0 @@ -6762,7 +6750,7 @@ packages: '@mui/base@5.0.0-beta.40': resolution: {integrity: sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==} engines: {node: '>=12.0.0'} - deprecated: This package has been replaced by @base-ui-components/react + deprecated: This package has been replaced by @base-ui/react peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 react: ^17.0.0 || ^18.0.0 @@ -7028,70 +7016,60 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@napi-rs/canvas-linux-arm64-gnu@0.1.82': resolution: {integrity: sha512-AwLzwLBgmvk7kWeUgItOUor/QyG31xqtD26w1tLpf4yE0hiXTGp23yc669aawjB6FzgIkjh1NKaNS52B7/qEBQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@napi-rs/canvas-linux-arm64-musl@0.1.79': resolution: {integrity: sha512-KsrsR3+6uXv70W/1/kY0yRK4/bbdJgA1Vuxw4KyfSc6mjl1DMoYXDAjpBT/5w7AXy6cGG44jm3upvvt/y/dPfg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@napi-rs/canvas-linux-arm64-musl@0.1.82': resolution: {integrity: sha512-moZWuqepAwWBffdF4JDadt8TgBD02iMhG6I1FHZf8xO20AsIp9rB+p0B8Zma2h2vAF/YMjeFCDmW5un6+zZz9g==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@napi-rs/canvas-linux-riscv64-gnu@0.1.79': resolution: {integrity: sha512-EXaENnSJD6au6z4aKN2PpU9eVNWUsRI2cApm8gCa0WSRMaiYXZsFkXQmhB+Vz2pXahOS8BN2Zd8S1IeML/LCtg==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] - libc: [glibc] '@napi-rs/canvas-linux-riscv64-gnu@0.1.82': resolution: {integrity: sha512-w9++2df2kG9eC9LWYIHIlMLuhIrKGQYfUxs97CwgxYjITeFakIRazI9LYWgVzEc98QZ9x9GQvlicFsrROV59MQ==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] - libc: [glibc] '@napi-rs/canvas-linux-x64-gnu@0.1.79': resolution: {integrity: sha512-3xZhHlE9e3cd9D7Comy6/TTSs/8PUGXEXymIwYQrA1QxHojAlAOFlVai4rffzXd0bHylZu+/wD76LodvYqF1Yw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@napi-rs/canvas-linux-x64-gnu@0.1.82': resolution: {integrity: sha512-lZulOPwrRi6hEg/17CaqdwWEUfOlIJuhXxincx1aVzsVOCmyHf+xFq4i6liJl1P+x2v6Iz2Z/H5zHvXJCC7Bwg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@napi-rs/canvas-linux-x64-musl@0.1.79': resolution: {integrity: sha512-4yv550uCjIEoTFgrpxYZK67nFlDMCQa3LAheM2QrO+B8w1p5w04usIQSCHqHe6aPWlbLQCIqfVcew6/7Q4KuHg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@napi-rs/canvas-linux-x64-musl@0.1.82': resolution: {integrity: sha512-Be9Wf5RTv1w6GXlTph55K3PH3vsAh1Ax4T1FQY1UYM0QfD0yrwGdnJ8/fhqw7dEgMjd59zIbjJQC8C3msbGn5g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@napi-rs/canvas-win32-x64-msvc@0.1.79': resolution: {integrity: sha512-sD5qP2njBRnhNlTNFJDdpeCN6aR3qVamLySTwhX3ec8sdfeT/chf/x2dw2UXoIGMoVaVk/y2ifwxBj/h2a2jug==} @@ -7154,49 +7132,42 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@napi-rs/nice-linux-arm64-musl@1.1.1': resolution: {integrity: sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@napi-rs/nice-linux-ppc64-gnu@1.1.1': resolution: {integrity: sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==} engines: {node: '>= 10'} cpu: [ppc64] os: [linux] - libc: [glibc] '@napi-rs/nice-linux-riscv64-gnu@1.1.1': resolution: {integrity: sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] - libc: [glibc] '@napi-rs/nice-linux-s390x-gnu@1.1.1': resolution: {integrity: sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==} engines: {node: '>= 10'} cpu: [s390x] os: [linux] - libc: [glibc] '@napi-rs/nice-linux-x64-gnu@1.1.1': resolution: {integrity: sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@napi-rs/nice-linux-x64-musl@1.1.1': resolution: {integrity: sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@napi-rs/nice-openharmony-arm64@1.1.1': resolution: {integrity: sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==} @@ -7264,28 +7235,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@next/swc-linux-arm64-musl@14.2.33': resolution: {integrity: sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@next/swc-linux-x64-gnu@14.2.33': resolution: {integrity: sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@next/swc-linux-x64-musl@14.2.33': resolution: {integrity: sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@next/swc-win32-arm64-msvc@14.2.33': resolution: {integrity: sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==} @@ -8005,49 +7972,41 @@ packages: resolution: {integrity: sha512-Cwm6A071ww60QouJ9LoHAwBgEoZzHQ0Qaqk2E7WLfBdiQN9mLXIDhnrpn04hlRElRPhLiu/dtg+o5PPLvaINXQ==} cpu: [arm64] os: [linux] - libc: [glibc] '@oxc-resolver/binding-linux-arm64-musl@11.17.1': resolution: {integrity: sha512-+hwlE2v3m0r3sk93SchJL1uyaKcPjf+NGO/TD2DZUDo+chXx7FfaEj0nUMewigSt7oZ2sQN9Z4NJOtUa75HE5Q==} cpu: [arm64] os: [linux] - libc: [musl] '@oxc-resolver/binding-linux-ppc64-gnu@11.17.1': resolution: {integrity: sha512-bO+rsaE5Ox8cFyeL5Ct5tzot1TnQpFa/Wmu5k+hqBYSH2dNVDGoi0NizBN5QV8kOIC6O5MZr81UG4yW/2FyDTA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@oxc-resolver/binding-linux-riscv64-gnu@11.17.1': resolution: {integrity: sha512-B/P+hxKQ1oX4YstI9Lyh4PGzqB87Ddqj/A4iyRBbPdXTcxa+WW3oRLx1CsJKLmHPdDk461Hmbghq1Bm3pl+8Aw==} cpu: [riscv64] os: [linux] - libc: [glibc] '@oxc-resolver/binding-linux-riscv64-musl@11.17.1': resolution: {integrity: sha512-ulp2H3bFXzd/th2maH+QNKj5qgOhJ3v9Yspdf1svTw3CDOuuTl6sRKsWQ7MUw0vnkSNvQndtflBwVXgzZvURsQ==} cpu: [riscv64] os: [linux] - libc: [musl] '@oxc-resolver/binding-linux-s390x-gnu@11.17.1': resolution: {integrity: sha512-LAXYVe3rKk09Zo9YKF2ZLBcH8sz8Oj+JIyiUxiHtq0hiYLMsN6dOpCf2hzQEjPAmsSEA/hdC1PVKeXo+oma8mQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@oxc-resolver/binding-linux-x64-gnu@11.17.1': resolution: {integrity: sha512-3RAhxipMKE8RCSPn7O//sj440i+cYTgYbapLeOoDvQEt6R1QcJjTsFgI4iz99FhVj3YbPxlZmcLB5VW+ipyRTA==} cpu: [x64] os: [linux] - libc: [glibc] '@oxc-resolver/binding-linux-x64-musl@11.17.1': resolution: {integrity: sha512-wpjMEubGU8r9VjZTLdZR3aPHaBqTl8Jl8F4DBbgNoZ+yhkhQD1/MGvY70v2TLnAI6kAHSvcqgfvaqKDa2iWsPQ==} cpu: [x64] os: [linux] - libc: [musl] '@oxc-resolver/binding-openharmony-arm64@11.17.1': resolution: {integrity: sha512-XIE4w17RYAVIgx+9Gs3deTREq5tsmalbatYOOBGNdH7n0DfTE600c7wYXsp7ANc3BPDXsInnOzXDEPCvO1F6cg==} @@ -8109,42 +8068,36 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.1': resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.1': resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.1': resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.1': resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.1': resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - libc: [musl] '@parcel/watcher-win32-arm64@2.5.1': resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} @@ -8569,85 +8522,71 @@ packages: resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.59.0': resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.59.0': resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.59.0': resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.59.0': resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.59.0': resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} cpu: [loong64] os: [linux] - libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.59.0': resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.59.0': resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} cpu: [ppc64] os: [linux] - libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.59.0': resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.59.0': resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.59.0': resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.50.1': resolution: {integrity: sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.59.0': resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.59.0': resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openbsd-x64@4.59.0': resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} @@ -9759,28 +9698,24 @@ packages: engines: {node: '>=10'} cpu: [arm64] os: [linux] - libc: [glibc] '@swc/core-linux-arm64-musl@1.13.5': resolution: {integrity: sha512-9+ZxFN5GJag4CnYnq6apKTnnezpfJhCumyz0504/JbHLo+Ue+ZtJnf3RhyA9W9TINtLE0bC4hKpWi8ZKoETyOQ==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - libc: [musl] '@swc/core-linux-x64-gnu@1.13.5': resolution: {integrity: sha512-WD530qvHrki8Ywt/PloKUjaRKgstQqNGvmZl54g06kA+hqtSE2FTG9gngXr3UJxYu/cNAjJYiBifm7+w4nbHbA==} engines: {node: '>=10'} cpu: [x64] os: [linux] - libc: [glibc] '@swc/core-linux-x64-musl@1.13.5': resolution: {integrity: sha512-Luj8y4OFYx4DHNQTWjdIuKTq2f5k6uSXICqx+FSabnXptaOBAbJHNbHT/06JZh6NRUouaf0mYXN0mcsqvkhd7Q==} engines: {node: '>=10'} cpu: [x64] os: [linux] - libc: [musl] '@swc/core-win32-arm64-msvc@1.13.5': resolution: {integrity: sha512-cZ6UpumhF9SDJvv4DA2fo9WIzlNFuKSkZpZmPG1c+4PFSEMy5DFOjBSllCvnqihCabzXzpn6ykCwBmHpy31vQw==} @@ -10287,9 +10222,6 @@ packages: '@types/express@4.17.25': resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==} - '@types/express@5.0.6': - resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} - '@types/filesystem@0.0.36': resolution: {integrity: sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==} @@ -10657,9 +10589,6 @@ packages: '@types/serve-static@1.15.8': resolution: {integrity: sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==} - '@types/serve-static@2.2.0': - resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} - '@types/shimmer@1.2.0': resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==} @@ -10938,49 +10867,41 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] - libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -11815,6 +11736,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + bare-events@2.6.1: resolution: {integrity: sha512-AuTJkq9XmE6Vk0FJVNq5QxETrSA/vKHarWVBG5l/JbdCL1prJemiyJqUS0jrlXO0MftuPq4m3YVYhoNc5+aE/g==} @@ -11877,6 +11802,7 @@ packages: basic-ftp@5.0.5: resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==} engines: {node: '>=10.0.0'} + deprecated: Security vulnerability fixed in 5.2.1, please upgrade batch@0.6.1: resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==} @@ -11978,6 +11904,10 @@ packages: brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -13820,6 +13750,9 @@ packages: dompurify@3.3.0: resolution: {integrity: sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==} + dompurify@3.4.3: + resolution: {integrity: sha512-VVwJidIJcp1hpg2OMXML3ZVRPYSZiq4aX7qBh83BSIpOaRDqI+qxhXjjIWnpzkOXhmp0L81lnoME1mnCc9H48A==} + domutils@1.7.0: resolution: {integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==} @@ -14898,14 +14831,14 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} - flowise-embed-react@3.0.5: - resolution: {integrity: sha512-rcSU2JLKPQkx2Vxieh+W2IK1xzf8WiHIPQdcx/kFLef9aljz/ZU0IhUbGOskkzsZDQGy1ng6HqHkvZ/pcYt9mw==} + flowise-embed-react@3.1.6: + resolution: {integrity: sha512-Wxs4RMOoqgGplk2LSwMHNAUxz7koSSfYG5A5TSTHQN0G8C+EN7DGG/4onLcc+R8Zk8dpGfX1J74ogUaMdIottA==} peerDependencies: flowise-embed: '*' react: 18.x - flowise-embed@3.0.5: - resolution: {integrity: sha512-CCZRpZdTeSW2Oagj7LxxK7YZKEbqjylUs2ayCuhB2hNoufkOpEJlyvqdDKcy6FDc+DwKg5aSjSFyTH2AFVh1pQ==} + flowise-embed@3.1.6: + resolution: {integrity: sha512-xSrvBDiEvIIzRFsPBgmzG3SdfBeG4wBzdchdMurft5PQcdYO0HfB8cwGhn00fE5JOsdhVZXfJ3Fq6e/DgrW4ag==} flowise-nim-container-manager@1.0.11: resolution: {integrity: sha512-Er/leiIzJZ691kWqXkjCWv29g0G5qWA4juBOcGZ1P5kBLnkKFxbWc5x8JsSPcHuJQIiGNX/dYEzp2ZrOV3YYMg==} @@ -15279,11 +15212,6 @@ packages: deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true - glob@10.4.5: - resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - hasBin: true - glob@10.5.0: resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me @@ -15295,9 +15223,9 @@ packages: deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true - glob@13.0.1: - resolution: {integrity: sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w==} - engines: {node: 20 || >=22} + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} glob@7.1.6: resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} @@ -18367,6 +18295,10 @@ packages: resolution: {integrity: sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==} engines: {node: 20 || >=22} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -18413,6 +18345,10 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + minizlib@2.1.2: resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} engines: {node: '>= 8'} @@ -19536,6 +19472,10 @@ packages: resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} engines: {node: 20 || >=22} + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + path-to-regexp@0.1.12: resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} @@ -20589,6 +20529,7 @@ packages: prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. hasBin: true prelude-ls@1.1.2: @@ -23904,6 +23845,7 @@ packages: uuid@10.0.0: resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true uuid@11.1.0: @@ -23912,10 +23854,12 @@ packages: uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true uuid@9.0.1: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true uuidv7@0.6.3: @@ -25036,7 +24980,7 @@ snapshots: '@answerai/answeragent-mcp@1.2.0': dependencies: '@modelcontextprotocol/sdk': 0.6.0 - axios: 1.13.5(debug@4.4.3) + axios: 1.13.5(debug@4.4.1) dotenv: 16.6.1 express: 4.22.1 zod: 3.25.76 @@ -25062,11 +25006,11 @@ snapshots: - supports-color - zod - '@answerai/salesforce-mcp@0.1.0(@types/node@25.2.2)(encoding@0.1.13)': + '@answerai/salesforce-mcp@0.1.0(@types/node@18.15.11)(encoding@0.1.13)': dependencies: '@modelcontextprotocol/sdk': 0.5.0 dotenv: 16.6.1 - jsforce: 3.10.7(@types/node@25.2.2)(encoding@0.1.13) + jsforce: 3.10.7(@types/node@18.15.11)(encoding@0.1.13) transitivePeerDependencies: - '@types/node' - encoding @@ -30405,7 +30349,7 @@ snapshots: '@gar/promisify@1.1.3': optional: true - '@getzep/zep-cloud@1.0.12(@langchain/core@0.3.61(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76)))(encoding@0.1.13)(langchain@0.3.35(ccfa7d77c90b4f7e0954a3992e04e03a))': + '@getzep/zep-cloud@1.0.12(@langchain/core@0.3.61(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76)))(encoding@0.1.13)(langchain@0.3.35(2f09207fbcc7b04adb1a5c2b37882e8f))': dependencies: form-data: 4.0.4 node-fetch: 2.7.0(encoding@0.1.13) @@ -30414,7 +30358,7 @@ snapshots: zod: 3.25.76 optionalDependencies: '@langchain/core': 0.3.61(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76)) - langchain: 0.3.35(ccfa7d77c90b4f7e0954a3992e04e03a) + langchain: 0.3.35(2f09207fbcc7b04adb1a5c2b37882e8f) transitivePeerDependencies: - encoding @@ -30807,6 +30751,13 @@ snapshots: optionalDependencies: '@types/node': 25.2.2 + '@inquirer/external-editor@1.0.1(@types/node@18.15.11)': + dependencies: + chardet: 2.1.0 + iconv-lite: 0.6.3 + optionalDependencies: + '@types/node': 18.15.11 + '@inquirer/external-editor@1.0.1(@types/node@25.2.2)': dependencies: chardet: 2.1.0 @@ -31334,10 +31285,10 @@ snapshots: chalk: 4.1.2 optional: true - '@jlinc/langchain@0.1.4(06a2b4abceed266cae999e7815581a2a)': + '@jlinc/langchain@0.1.4(88efa4f237aaf519e63a84735e8044f7)': dependencies: - axios: 1.13.5(debug@4.4.3) - langchain: 0.3.35(9eed3cc13fe04054f7a9aeb5690c3438) + axios: 1.13.5(debug@4.4.1) + langchain: 0.3.35(d5b3d1f63cfb11c1d8be7f48376f0008) transitivePeerDependencies: - '@langchain/anthropic' - '@langchain/aws' @@ -31603,7 +31554,7 @@ snapshots: - aws-crt optional: true - '@langchain/community@0.3.47(a209da930d3d662083731a25d846ee59)': + '@langchain/community@0.3.47(83568a380315299720275f744bad1312)': dependencies: '@browserbasehq/stagehand': 1.14.0(@playwright/test@1.55.0)(bufferutil@4.0.9)(deepmerge@4.3.1)(dotenv@17.2.4)(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76))(utf-8-validate@6.0.5)(zod@3.25.76) '@ibm-cloud/watsonx-ai': 1.6.0(@langchain/core@0.3.61(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76))) @@ -31615,7 +31566,7 @@ snapshots: flat: 5.0.2 ibm-cloud-sdk-core: 5.3.2 js-yaml: 4.1.0 - langchain: 0.3.35(ccfa7d77c90b4f7e0954a3992e04e03a) + langchain: 0.3.35(12c11ee69b26dde6f1dd0598e11338f2) langsmith: 0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76)) openai: 4.96.0(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76) uuid: 10.0.0 @@ -31631,7 +31582,7 @@ snapshots: '@browserbasehq/sdk': 2.6.0(encoding@0.1.13) '@datastax/astra-db-ts': 1.5.0 '@elastic/elasticsearch': 8.19.1 - '@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.61(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76)))(encoding@0.1.13)(langchain@0.3.35(ccfa7d77c90b4f7e0954a3992e04e03a)) + '@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.61(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76)))(encoding@0.1.13)(langchain@0.3.35(2f09207fbcc7b04adb1a5c2b37882e8f)) '@getzep/zep-js': 0.9.0 '@gomomento/sdk': 1.116.0 '@gomomento/sdk-core': 1.116.0 @@ -31685,7 +31636,7 @@ snapshots: redis: 4.7.1 replicate: 0.31.1 srt-parser-2: 1.2.3 - typeorm: 0.3.26(babel-plugin-macros@3.1.0)(ioredis@5.7.0)(mongodb@6.3.0(@aws-sdk/credential-providers@3.887.0(aws-crt@1.27.3(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(socks@2.8.7))(mysql2@3.14.5)(pg@8.16.3)(redis@4.7.1)(reflect-metadata@0.1.14)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@25.2.2)(typescript@5.5.4)) + typeorm: 0.3.26(babel-plugin-macros@3.1.0)(ioredis@5.7.0)(mongodb@6.3.0(@aws-sdk/credential-providers@3.887.0(aws-crt@1.27.3(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(socks@2.8.7))(mysql2@3.14.5)(pg@8.16.3)(redis@4.7.1)(reflect-metadata@0.1.14)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@18.15.11)(typescript@5.5.4)) weaviate-client: 3.8.1(encoding@0.1.13) ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5) transitivePeerDependencies: @@ -31709,7 +31660,7 @@ snapshots: - handlebars - peggy - '@langchain/community@0.3.47(fe40d5f8931a91ff658e43b1151a94ef)': + '@langchain/community@0.3.47(b545b8e28d3cee1192afd7359426439f)': dependencies: '@browserbasehq/stagehand': 1.14.0(@playwright/test@1.55.0)(bufferutil@4.0.9)(deepmerge@4.3.1)(dotenv@17.2.4)(encoding@0.1.13)(openai@4.96.0(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76))(utf-8-validate@6.0.5)(zod@3.25.76) '@ibm-cloud/watsonx-ai': 1.6.0(@langchain/core@0.3.61(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76))) @@ -31721,7 +31672,7 @@ snapshots: flat: 5.0.2 ibm-cloud-sdk-core: 5.3.2 js-yaml: 4.1.0 - langchain: 0.3.35(02cf11db9505ce78e486241c67b25855) + langchain: 0.3.35(2f09207fbcc7b04adb1a5c2b37882e8f) langsmith: 0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76)) openai: 4.96.0(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76) uuid: 10.0.0 @@ -31737,7 +31688,7 @@ snapshots: '@browserbasehq/sdk': 2.6.0(encoding@0.1.13) '@datastax/astra-db-ts': 1.5.0 '@elastic/elasticsearch': 8.19.1 - '@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.61(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76)))(encoding@0.1.13)(langchain@0.3.35(ccfa7d77c90b4f7e0954a3992e04e03a)) + '@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.61(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76)))(encoding@0.1.13)(langchain@0.3.35(2f09207fbcc7b04adb1a5c2b37882e8f)) '@getzep/zep-js': 0.9.0 '@gomomento/sdk': 1.116.0 '@gomomento/sdk-core': 1.116.0 @@ -31791,7 +31742,7 @@ snapshots: redis: 4.7.1 replicate: 0.31.1 srt-parser-2: 1.2.3 - typeorm: 0.3.26(babel-plugin-macros@3.1.0)(ioredis@5.7.0)(mongodb@6.3.0(@aws-sdk/credential-providers@3.887.0(aws-crt@1.27.3(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(socks@2.8.7))(mysql2@3.14.5)(pg@8.16.3)(redis@4.7.1)(reflect-metadata@0.1.14)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@25.2.2)(typescript@5.5.4)) + typeorm: 0.3.26(babel-plugin-macros@3.1.0)(ioredis@5.7.0)(mongodb@6.3.0(@aws-sdk/credential-providers@3.887.0(aws-crt@1.27.3(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(socks@2.8.7))(mysql2@3.14.5)(pg@8.16.3)(redis@4.7.1)(reflect-metadata@0.1.14)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@18.15.11)(typescript@5.5.4)) weaviate-client: 3.8.1(encoding@0.1.13) ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5) transitivePeerDependencies: @@ -32244,9 +32195,9 @@ snapshots: '@types/react': 19.2.2 react: 18.3.1 - '@mem0/community@0.0.1(f670af68d36a1e09d6e06f42f2080e6d)': + '@mem0/community@0.0.1(a49a8ee6b857562bee536bea16cf5e44)': dependencies: - '@langchain/community': 0.3.47(fe40d5f8931a91ff658e43b1151a94ef) + '@langchain/community': 0.3.47(83568a380315299720275f744bad1312) '@langchain/core': 0.3.61(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76)) axios: 1.7.7 mem0ai: 2.1.38(@anthropic-ai/sdk@0.65.0(zod@3.25.76))(@cloudflare/workers-types@4.20250912.0)(@google/genai@0.7.0(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@6.0.5))(@langchain/core@0.3.61(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76)))(@mistralai/mistralai@0.1.3(encoding@0.1.13))(@qdrant/js-client-rest@1.15.1(typescript@5.5.4))(@supabase/supabase-js@2.57.4(bufferutil@4.0.9)(utf-8-validate@6.0.5))(@types/jest@29.5.14)(@types/pg@8.15.5)(@types/sqlite3@3.1.11)(cloudflare@4.5.0(encoding@0.1.13))(encoding@0.1.13)(groq-sdk@0.19.0(encoding@0.1.13))(neo4j-driver@5.28.1)(ollama@0.5.17)(pg@8.16.3)(redis@4.7.1)(sqlite3@5.1.7)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5)) @@ -32413,7 +32364,7 @@ snapshots: '@mendable/firecrawl-js@1.29.3': dependencies: - axios: 1.13.5(debug@4.4.3) + axios: 1.13.5(debug@4.4.1) typescript-event-target: 1.1.1 zod: 3.25.76 zod-to-json-schema: 3.24.6(zod@3.25.76) @@ -34947,7 +34898,7 @@ snapshots: '@slack/types': 2.16.0 '@types/is-stream': 1.1.0 '@types/node': 18.15.11 - axios: 1.13.5(debug@4.4.3) + axios: 1.13.5(debug@4.4.1) eventemitter3: 3.1.2 form-data: 4.0.4 is-electron: 2.2.2 @@ -36870,11 +36821,11 @@ snapshots: '@tsconfig/node16@1.0.4': {} - '@tsmztech/mcp-server-salesforce@0.0.2(@types/node@25.2.2)(encoding@0.1.13)': + '@tsmztech/mcp-server-salesforce@0.0.2(@types/node@18.15.11)(encoding@0.1.13)': dependencies: '@modelcontextprotocol/sdk': 0.5.0 dotenv: 16.6.1 - jsforce: 3.10.7(@types/node@25.2.2)(encoding@0.1.13) + jsforce: 3.10.7(@types/node@18.15.11)(encoding@0.1.13) transitivePeerDependencies: - '@types/node' - encoding @@ -36960,9 +36911,9 @@ snapshots: '@types/content-disposition@0.5.8': {} - '@types/cookie-parser@1.4.10(@types/express@5.0.6)': + '@types/cookie-parser@1.4.10(@types/express@4.17.25)': dependencies: - '@types/express': 5.0.6 + '@types/express': 4.17.25 '@types/cookiejar@2.1.5': {} @@ -37152,12 +37103,6 @@ snapshots: '@types/qs': 6.14.0 '@types/serve-static': 1.15.10 - '@types/express@5.0.6': - dependencies: - '@types/body-parser': 1.19.6 - '@types/express-serve-static-core': 4.19.8 - '@types/serve-static': 2.2.0 - '@types/filesystem@0.0.36': dependencies: '@types/filewriter': 0.0.33 @@ -37615,11 +37560,6 @@ snapshots: '@types/node': 24.3.1 '@types/send': 0.17.5 - '@types/serve-static@2.2.0': - dependencies: - '@types/http-errors': 2.0.5 - '@types/node': 18.15.11 - '@types/shimmer@1.2.0': {} '@types/sinonjs__fake-timers@8.1.1': {} @@ -38632,7 +38572,7 @@ snapshots: '@crawlee/types': 3.14.1 agentkeepalive: 4.6.0 async-retry: 1.3.3 - axios: 1.13.5(debug@4.4.3) + axios: 1.13.5(debug@4.4.1) content-type: 1.0.5 ow: 0.28.2 tslib: 2.8.1 @@ -38910,7 +38850,7 @@ snapshots: dependencies: '@aws-sdk/util-utf8-browser': 3.259.0 '@httptoolkit/websocket-stream': 6.0.1(bufferutil@4.0.9)(utf-8-validate@6.0.5) - axios: 1.13.5(debug@4.4.3) + axios: 1.13.5(debug@4.4.1) buffer: 6.0.3 crypto-js: 4.2.0 mqtt: 4.3.8(bufferutil@4.0.9)(utf-8-validate@6.0.5) @@ -38933,7 +38873,7 @@ snapshots: axios@1.12.0: dependencies: - follow-redirects: 1.15.11(debug@4.4.3) + follow-redirects: 1.15.11(debug@4.4.1) form-data: 4.0.4 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -38957,7 +38897,7 @@ snapshots: axios@1.7.7: dependencies: - follow-redirects: 1.15.11(debug@4.4.3) + follow-redirects: 1.15.11(debug@4.4.1) form-data: 4.0.4 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -39177,6 +39117,8 @@ snapshots: balanced-match@1.0.2: {} + balanced-match@4.0.4: {} + bare-events@2.6.1: optional: true @@ -39351,6 +39293,10 @@ snapshots: dependencies: balanced-match: 1.0.2 + brace-expansion@5.0.6: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -40253,7 +40199,7 @@ snapshots: contentful-management@11.57.1: dependencies: '@contentful/rich-text-types': 16.8.5 - axios: 1.13.5(debug@4.4.3) + axios: 1.13.5(debug@4.4.1) contentful-sdk-core: 9.2.0 fast-copy: 3.0.2 globals: 15.15.0 @@ -40286,7 +40232,7 @@ snapshots: dependencies: '@contentful/content-source-maps': 0.11.32 '@contentful/rich-text-types': 16.8.5 - axios: 1.13.5(debug@4.4.3) + axios: 1.13.5(debug@4.4.1) contentful-resolve-response: 1.9.3 contentful-sdk-core: 8.3.2 json-stringify-safe: 5.0.1 @@ -41562,6 +41508,10 @@ snapshots: optionalDependencies: '@types/trusted-types': 2.0.7 + dompurify@3.4.3: + optionalDependencies: + '@types/trusted-types': 2.0.7 + domutils@1.7.0: dependencies: dom-serializer: 0.2.2 @@ -43310,10 +43260,10 @@ snapshots: flatted@3.3.3: {} - flowise-embed-react@3.0.5(@types/node@25.2.2)(flowise-embed@3.0.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.92.1)(terser@5.44.0)(typescript@5.5.4): + flowise-embed-react@3.1.6(@types/node@25.2.2)(flowise-embed@3.1.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.92.1)(terser@5.44.0)(typescript@5.5.4): dependencies: '@ladle/react': 2.5.1(@types/node@25.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.92.1)(terser@5.44.0)(typescript@5.5.4) - flowise-embed: 3.0.5 + flowise-embed: 3.1.6 react: 18.3.1 transitivePeerDependencies: - '@types/node' @@ -43327,15 +43277,17 @@ snapshots: - terser - typescript - flowise-embed@3.0.5: + flowise-embed@3.1.6: dependencies: '@babel/core': 7.29.0 '@microsoft/fetch-event-source': 2.0.1 '@ts-stack/markdown': 1.5.0 - axios: 1.13.5(debug@4.4.3) + '@types/dompurify': 3.2.0 + axios: 1.13.5(debug@4.4.1) cors: 2.8.6 cross-env: 7.0.3 device-detector-js: 3.0.3 + dompurify: 3.4.3 dotenv: 16.6.1 express: 4.22.1 form-data: 4.0.4 @@ -43799,15 +43751,6 @@ snapshots: minipass: 7.1.2 path-scurry: 1.11.1 - glob@10.4.5: - dependencies: - foreground-child: 3.3.1 - jackspeak: 3.4.3 - minimatch: 9.0.5 - minipass: 7.1.2 - package-json-from-dist: 1.0.1 - path-scurry: 1.11.1 - glob@10.5.0: dependencies: foreground-child: 3.3.1 @@ -43826,11 +43769,11 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 2.0.1 - glob@13.0.1: + glob@13.0.6: dependencies: - minimatch: 10.1.2 - minipass: 7.1.2 - path-scurry: 2.0.1 + minimatch: 10.2.5 + minipass: 7.1.3 + path-scurry: 2.0.2 glob@7.1.6: dependencies: @@ -44864,9 +44807,9 @@ snapshots: transitivePeerDependencies: - encoding - inquirer@8.2.7(@types/node@25.2.2): + inquirer@8.2.7(@types/node@18.15.11): dependencies: - '@inquirer/external-editor': 1.0.1(@types/node@25.2.2) + '@inquirer/external-editor': 1.0.1(@types/node@18.15.11) ansi-escapes: 4.3.2 chalk: 4.1.2 cli-cursor: 3.1.0 @@ -46158,7 +46101,7 @@ snapshots: jira.js@2.20.1: dependencies: atlassian-jwt: 2.0.3 - axios: 1.13.5(debug@4.4.3) + axios: 1.13.5(debug@4.4.1) form-data: 4.0.4 oauth: 0.10.2 tslib: 2.8.1 @@ -46362,7 +46305,7 @@ snapshots: jsesc@3.1.0: {} - jsforce@3.10.7(@types/node@25.2.2)(encoding@0.1.13): + jsforce@3.10.7(@types/node@18.15.11)(encoding@0.1.13): dependencies: '@babel/runtime': 7.28.6 '@babel/runtime-corejs3': 7.28.4 @@ -46375,7 +46318,7 @@ snapshots: faye: 1.4.1 form-data: 4.0.4 https-proxy-agent: 5.0.1 - inquirer: 8.2.7(@types/node@25.2.2) + inquirer: 8.2.7(@types/node@18.15.11) multistream: 3.1.0 node-fetch: 2.7.0(encoding@0.1.13) open: 7.4.2 @@ -46636,7 +46579,7 @@ snapshots: '@supabase/supabase-js': 2.57.4(bufferutil@4.0.9)(utf-8-validate@6.0.5) apify-client: 2.17.0 assemblyai: 4.16.1(bufferutil@4.0.9)(utf-8-validate@6.0.5) - axios: 1.13.5(debug@4.4.3) + axios: 1.13.5(debug@4.4.1) cheerio: 1.1.2 chromadb: 1.10.5(@google/generative-ai@0.24.1)(cohere-ai@7.20.0(aws-crt@1.27.3(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(encoding@0.1.13)(ollama@0.5.18)(openai@4.96.0(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76)) couchbase: 4.4.1 @@ -46668,7 +46611,7 @@ snapshots: - encoding - openai - langchain@0.3.35(02cf11db9505ce78e486241c67b25855): + langchain@0.3.35(12c11ee69b26dde6f1dd0598e11338f2): dependencies: '@langchain/core': 0.3.61(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76)) '@langchain/openai': 0.6.3(@langchain/core@0.3.61(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76)))(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5)) @@ -46696,7 +46639,7 @@ snapshots: axios: 1.7.7 cheerio: 1.1.2 handlebars: 4.7.8 - typeorm: 0.3.26(babel-plugin-macros@3.1.0)(ioredis@5.7.0)(mongodb@6.3.0(@aws-sdk/credential-providers@3.887.0(aws-crt@1.27.3(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(socks@2.8.7))(mysql2@3.14.5)(pg@8.16.3)(redis@4.7.1)(reflect-metadata@0.1.14)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@25.2.2)(typescript@5.5.4)) + typeorm: 0.3.26(babel-plugin-macros@3.1.0)(ioredis@5.7.0)(mongodb@6.3.0(@aws-sdk/credential-providers@3.887.0(aws-crt@1.27.3(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(socks@2.8.7))(mysql2@3.14.5)(pg@8.16.3)(redis@4.7.1)(reflect-metadata@0.1.14)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@18.15.11)(typescript@5.5.4)) transitivePeerDependencies: - '@opentelemetry/api' - '@opentelemetry/exporter-trace-otlp-proto' @@ -46705,7 +46648,7 @@ snapshots: - openai - ws - langchain@0.3.35(9eed3cc13fe04054f7a9aeb5690c3438): + langchain@0.3.35(2f09207fbcc7b04adb1a5c2b37882e8f): dependencies: '@langchain/core': 0.3.61(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76)) '@langchain/openai': 0.6.3(@langchain/core@0.3.61(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76)))(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5)) @@ -46730,10 +46673,10 @@ snapshots: '@langchain/mistralai': 0.2.1(@langchain/core@0.3.61(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76))) '@langchain/ollama': 0.2.0(@langchain/core@0.3.61(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76))) '@langchain/xai': 0.0.1(@langchain/core@0.3.61(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76)))(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5)) - axios: 1.13.5(debug@4.4.3) + axios: 1.12.0 cheerio: 1.1.2 handlebars: 4.7.8 - typeorm: 0.3.26(babel-plugin-macros@3.1.0)(ioredis@5.7.0)(mongodb@6.3.0(@aws-sdk/credential-providers@3.887.0(aws-crt@1.27.3(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(socks@2.8.7))(mysql2@3.14.5)(pg@8.16.3)(redis@4.7.1)(reflect-metadata@0.1.14)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@25.2.2)(typescript@5.5.4)) + typeorm: 0.3.26(babel-plugin-macros@3.1.0)(ioredis@5.7.0)(mongodb@6.3.0(@aws-sdk/credential-providers@3.887.0(aws-crt@1.27.3(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(socks@2.8.7))(mysql2@3.14.5)(pg@8.16.3)(redis@4.7.1)(reflect-metadata@0.1.14)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@18.15.11)(typescript@5.5.4)) transitivePeerDependencies: - '@opentelemetry/api' - '@opentelemetry/exporter-trace-otlp-proto' @@ -46742,7 +46685,7 @@ snapshots: - openai - ws - langchain@0.3.35(ccfa7d77c90b4f7e0954a3992e04e03a): + langchain@0.3.35(d5b3d1f63cfb11c1d8be7f48376f0008): dependencies: '@langchain/core': 0.3.61(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76)) '@langchain/openai': 0.6.3(@langchain/core@0.3.61(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76)))(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5)) @@ -46767,10 +46710,10 @@ snapshots: '@langchain/mistralai': 0.2.1(@langchain/core@0.3.61(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76))) '@langchain/ollama': 0.2.0(@langchain/core@0.3.61(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76))) '@langchain/xai': 0.0.1(@langchain/core@0.3.61(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(openai@4.96.0(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76)))(encoding@0.1.13)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5)) - axios: 1.12.0 + axios: 1.13.5(debug@4.4.1) cheerio: 1.1.2 handlebars: 4.7.8 - typeorm: 0.3.26(babel-plugin-macros@3.1.0)(ioredis@5.7.0)(mongodb@6.3.0(@aws-sdk/credential-providers@3.887.0(aws-crt@1.27.3(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(socks@2.8.7))(mysql2@3.14.5)(pg@8.16.3)(redis@4.7.1)(reflect-metadata@0.1.14)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@25.2.2)(typescript@5.5.4)) + typeorm: 0.3.26(babel-plugin-macros@3.1.0)(ioredis@5.7.0)(mongodb@6.3.0(@aws-sdk/credential-providers@3.887.0(aws-crt@1.27.3(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(socks@2.8.7))(mysql2@3.14.5)(pg@8.16.3)(redis@4.7.1)(reflect-metadata@0.1.14)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@18.15.11)(typescript@5.5.4)) transitivePeerDependencies: - '@opentelemetry/api' - '@opentelemetry/exporter-trace-otlp-proto' @@ -46785,9 +46728,9 @@ snapshots: dependencies: mustache: 4.2.0 - langfuse-langchain@3.37.6(langchain@0.3.35(ccfa7d77c90b4f7e0954a3992e04e03a)): + langfuse-langchain@3.37.6(langchain@0.3.35(2f09207fbcc7b04adb1a5c2b37882e8f)): dependencies: - langchain: 0.3.35(ccfa7d77c90b4f7e0954a3992e04e03a) + langchain: 0.3.35(2f09207fbcc7b04adb1a5c2b37882e8f) langfuse: 3.38.6 langfuse-core: 3.38.6 @@ -48473,6 +48416,10 @@ snapshots: dependencies: '@isaacs/brace-expansion': 5.0.1 + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -48524,6 +48471,8 @@ snapshots: minipass@7.1.2: {} + minipass@7.1.3: {} + minizlib@2.1.2: dependencies: minipass: 3.3.6 @@ -49851,7 +49800,7 @@ snapshots: passport-auth0@1.4.5: dependencies: - axios: 1.13.5(debug@4.4.3) + axios: 1.13.5(debug@4.4.1) passport-oauth: 1.0.0 passport-oauth2: 1.8.0 transitivePeerDependencies: @@ -49961,6 +49910,11 @@ snapshots: lru-cache: 11.2.5 minipass: 7.1.2 + path-scurry@2.0.2: + dependencies: + lru-cache: 11.2.5 + minipass: 7.1.3 + path-to-regexp@0.1.12: {} path-to-regexp@6.3.0: {} @@ -51128,17 +51082,6 @@ snapshots: prettier: 2.8.8 tslib: 2.8.1 - pretty-quick@3.3.1(prettier@3.8.1): - dependencies: - execa: 4.1.0 - find-up: 4.1.0 - ignore: 5.3.2 - mri: 1.2.0 - picocolors: 1.1.1 - picomatch: 3.0.1 - prettier: 3.8.1 - tslib: 2.8.1 - pretty-time@1.1.0: {} prism-react-renderer@1.3.5(react@18.2.0): @@ -52683,11 +52626,11 @@ snapshots: rimraf@5.0.10: dependencies: - glob: 10.4.5 + glob: 10.5.0 rimraf@6.1.2: dependencies: - glob: 13.0.1 + glob: 13.0.6 package-json-from-dist: 1.0.1 roarr@2.15.4: @@ -53932,7 +53875,7 @@ snapshots: dependencies: '@jridgewell/gen-mapping': 0.3.13 commander: 4.1.1 - glob: 10.4.5 + glob: 10.5.0 lines-and-columns: 1.2.4 mz: 2.7.0 pirates: 4.0.7 @@ -54488,12 +54431,12 @@ snapshots: babel-jest: 29.7.0(@babel/core@7.29.0) jest-util: 30.0.5 - ts-jest@29.4.1(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@30.0.5)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.0.5)(jest@29.7.0(@types/node@25.2.2)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@25.2.2)(typescript@5.5.4)))(typescript@5.5.4): + ts-jest@29.4.1(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@30.0.5)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.0.5)(jest@29.7.0(@types/node@18.15.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@18.15.11)(typescript@5.5.4)))(typescript@5.5.4): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 handlebars: 4.7.8 - jest: 29.7.0(@types/node@25.2.2)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@25.2.2)(typescript@5.5.4)) + jest: 29.7.0(@types/node@18.15.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@18.15.11)(typescript@5.5.4)) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 @@ -54623,14 +54566,6 @@ snapshots: string-argv: 0.3.2 typescript: 5.5.4 - tsc-watch@6.3.1(typescript@5.9.2): - dependencies: - cross-spawn: 7.0.6 - node-cleanup: 2.1.2 - ps-tree: 1.2.0 - string-argv: 0.3.2 - typescript: 5.9.2 - tsconfck@3.1.6(typescript@5.5.4): optionalDependencies: typescript: 5.5.4 @@ -54831,7 +54766,7 @@ snapshots: debug: 4.4.1(supports-color@8.1.1) dedent: 1.7.0(babel-plugin-macros@3.1.0) dotenv: 16.6.1 - glob: 10.4.5 + glob: 10.5.0 reflect-metadata: 0.1.14 sha.js: 2.4.12 sql-highlight: 6.1.0 @@ -54860,7 +54795,7 @@ snapshots: debug: 4.4.1(supports-color@8.1.1) dedent: 1.7.0(babel-plugin-macros@3.1.0) dotenv: 16.6.1 - glob: 10.4.5 + glob: 10.5.0 reflect-metadata: 0.1.14 sha.js: 2.4.12 sql-highlight: 6.1.0 @@ -54879,7 +54814,7 @@ snapshots: - babel-plugin-macros - supports-color - typeorm@0.3.26(babel-plugin-macros@3.1.0)(ioredis@5.7.0)(mongodb@6.3.0(@aws-sdk/credential-providers@3.887.0(aws-crt@1.27.3(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(socks@2.8.7))(mysql2@3.14.5)(pg@8.16.3)(redis@4.7.1)(reflect-metadata@0.1.14)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@25.2.2)(typescript@5.5.4)): + typeorm@0.3.26(babel-plugin-macros@3.1.0)(ioredis@5.7.0)(mongodb@6.3.0(@aws-sdk/credential-providers@3.887.0(aws-crt@1.27.3(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(socks@2.8.7))(mysql2@3.14.5)(pg@8.16.3)(redis@4.7.1)(reflect-metadata@0.1.14)(sqlite3@5.1.7)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@18.15.11)(typescript@5.5.4)): dependencies: '@sqltools/formatter': 1.2.5 ansis: 3.17.0 @@ -54889,7 +54824,7 @@ snapshots: debug: 4.4.1(supports-color@8.1.1) dedent: 1.7.0(babel-plugin-macros@3.1.0) dotenv: 16.6.1 - glob: 10.4.5 + glob: 10.5.0 reflect-metadata: 0.1.14 sha.js: 2.4.12 sql-highlight: 6.1.0 @@ -54903,7 +54838,7 @@ snapshots: pg: 8.16.3 redis: 4.7.1 sqlite3: 5.1.7 - ts-node: 10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@25.2.2)(typescript@5.5.4) + ts-node: 10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@18.15.11)(typescript@5.5.4) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -55610,7 +55545,7 @@ snapshots: wait-on@7.2.0: dependencies: - axios: 1.13.5(debug@4.4.3) + axios: 1.13.5(debug@4.4.1) joi: 17.13.3 lodash: 4.17.23 minimist: 1.2.8 @@ -56086,7 +56021,7 @@ snapshots: wikipedia@2.1.2: dependencies: - axios: 1.13.5(debug@4.4.3) + axios: 1.13.5(debug@4.4.1) infobox-parser: 3.6.4 transitivePeerDependencies: - debug @@ -56456,7 +56391,7 @@ snapshots: youtube-captions-scraper@2.0.3: dependencies: - axios: 1.13.5(debug@4.4.3) + axios: 1.13.5(debug@4.4.1) he: 1.2.0 lodash: 4.17.23 striptags: 3.2.0 diff --git a/scripts/bws-secure/README.md b/scripts/bws-secure/README.md index dc5665adf47..68f4b69575e 100755 --- a/scripts/bws-secure/README.md +++ b/scripts/bws-secure/README.md @@ -171,7 +171,10 @@ Optional variables: - `DEBUG=true`: Enable detailed logging - `VERBOSE=true`: Show additional debug information - `BWS_ENV`: Force specific environment (prod/dev/local) -- `BWS_PROJECT_ID`: Directly specify a BWS project UUID to bypass project configuration (e.g., `4ba4dc04-091f-4bf9-ba82-b2f900ee7d2a`) +- `BWS_PROJECT_ID`: One BWS project UUID, or **comma-separated UUIDs** to merge multiple projects (later IDs override earlier for duplicate keys). Bypasses `bwsconfig.json` when set. See [guides/MULTI_PROJECT_ID_GUIDE.md](guides/MULTI_PROJECT_ID_GUIDE.md). +- `BWS_PROJECT_IDS`: When multiple projects are loaded, the full ordered list may be exposed for tooling; the **first** UUID is always in `BWS_PROJECT_ID` for child processes. +- `BWS_KEEP_SECURE_FILES=true`: Keep encrypted `.env.secure*` files after a run (default: they are deleted when the run finishes). +- `BWS_MULTI_PROJECT_FAIL_FAST=true`: Exit on the first failed project load when multiple `BWS_PROJECT_ID` values are configured (default: continue with warnings). - `BWS_NO_OVERRIDE=true`: Prevent automatic updates to local `bwsconfig.json` from BWS secrets (useful in CI/CD or when you want to preserve local configuration) - `BWS_SUPPRESS_ALL=true`: Suppress all secure-run output while preserving wrapped command output (errors remain visible) - `BWS_SUPPRESS_MISSING=true`: Suppress missing environment variable warnings during validation @@ -673,6 +676,8 @@ The system creates several types of environment files: - Shared across projects - Platform-specific settings +Transient `.env.secure` and `.env.secure.*` files are removed when `secureRun` finishes (after your wrapped command exits), so the host repo root does not accumulate encrypted artifacts. To keep them for debugging, set **`BWS_KEEP_SECURE_FILES=true`**. + ## Secret Management ### Upload to BWS diff --git a/scripts/bws-secure/bws-dotenv.js b/scripts/bws-secure/bws-dotenv.js index a38242cce7b..0a8fb54bd4d 100755 --- a/scripts/bws-secure/bws-dotenv.js +++ b/scripts/bws-secure/bws-dotenv.js @@ -7,38 +7,18 @@ import path from 'node:path'; import dotenv from 'dotenv'; import logger from './logger.js'; import { execBwsCommandWithRetrySync } from './bws-retry-utils.js'; +import { + parseProjectIdsDetailed, + parseEnvironmentOutput, + serializeEnvRecordToPlaintext +} from './bws-env-utils.js'; -// Helper function to properly parse multiline environment variables from BWS output -function parseEnvironmentOutput(output) { - const result = {}; - const lines = output.split('\n'); - let currentKey = null; - let currentValue = ''; - - for (const line of lines) { - // Check if this line starts a new variable (has = and doesn't start with whitespace) - if (line.includes('=') && !line.startsWith(' ') && !line.startsWith('\t')) { - // Save previous variable if exists - if (currentKey !== null) { - result[currentKey] = currentValue; - } - - // Start new variable - const equalIndex = line.indexOf('='); - currentKey = line.substring(0, equalIndex).trim(); - currentValue = line.substring(equalIndex + 1); - } else if (currentKey !== null) { - // This is a continuation line for the current variable (preserve empty lines too) - currentValue += '\n' + line; - } - } +const debugMode = () => process.env.DEBUG === 'true'; - // Don't forget the last variable - if (currentKey !== null) { - result[currentKey] = currentValue; +function logDebug(...args) { + if (debugMode()) { + console.log(...args); } - - return result; } // Helper function to get BWS organization ID with fallback @@ -144,7 +124,7 @@ function loadBwsSecrets(encryptionKey) { // First, try to load global secrets (auth tokens) try { - console.log('Debug: Loading global secrets...'); + logDebug('Debug: Loading global secrets...'); const output = execBwsCommandWithRetrySync( `./node_modules/.bin/bws secret list -t ${process.env.BWS_ACCESS_TOKEN} -o env`, @@ -160,7 +140,7 @@ function loadBwsSecrets(encryptionKey) { for (const [key, value] of Object.entries(globalSecrets)) { if (key === 'NETLIFY_AUTH_TOKEN' || key === 'VERCEL_AUTH_TOKEN') { mergedVariables[key] = value; - console.log('Debug: Found auth token:', key); + logDebug('Debug: Found auth token:', key); } } } catch (globalError) { @@ -168,41 +148,76 @@ function loadBwsSecrets(encryptionKey) { } // Then, if we have a project ID, load project-specific secrets + // Support both single and comma-separated multiple project IDs if (process.env.BWS_PROJECT_ID) { - try { - console.log('Debug: Loading project secrets for:', process.env.BWS_PROJECT_ID); - // NOSONAR: BWS CLI execution with system-controlled variables - no user input - /* sonar-disable-next-line sonar:S4721 */ - const projectOutput = execBwsCommandWithRetrySync( - `./node_modules/.bin/bws secret list ${process.env.BWS_PROJECT_ID} -t ${process.env.BWS_ACCESS_TOKEN} -o env`, - { encoding: 'utf-8' }, - `Loading project secrets for ${process.env.BWS_PROJECT_ID}` + const parsed = parseProjectIdsDetailed(process.env.BWS_PROJECT_ID); + const projectIds = parsed.ids; + + if (parsed.skippedInvalid.length > 0) { + console.warn( + 'Warning: BWS_PROJECT_ID contained non-UUID segments (skipped):', + parsed.skippedInvalid.join(', ') ); + } + if (parsed.skippedDuplicates.length > 0) { + console.warn( + 'Warning: BWS_PROJECT_ID contained duplicate UUIDs (skipped):', + parsed.skippedDuplicates.join(', ') + ); + } - const projectSecrets = parseEnvironmentOutput(projectOutput); + if (projectIds.length === 0) { + console.warn('Warning: BWS_PROJECT_ID is set but no valid UUIDs found'); + } - // More data processing - for (const [key, value] of Object.entries(projectSecrets)) { - if (key && value) { - mergedVariables[key] = value; + for (const [index, projectId] of projectIds.entries()) { + try { + const isMultiple = projectIds.length > 1; + const logPrefix = isMultiple ? `[${index + 1}/${projectIds.length}]` : ''; + logDebug(`Debug: ${logPrefix} Loading project secrets for:`, projectId); + + // NOSONAR: BWS CLI execution with system-controlled variables - no user input + /* sonar-disable-next-line sonar:S4721 */ + const projectOutput = execBwsCommandWithRetrySync( + `./node_modules/.bin/bws secret list ${projectId} -t ${process.env.BWS_ACCESS_TOKEN} -o env`, + { encoding: 'utf-8' }, + `Loading project secrets for ${projectId}` + ); + + const projectSecrets = parseEnvironmentOutput(projectOutput); + + // Merge with overlay strategy - later project IDs override earlier ones + for (const [key, value] of Object.entries(projectSecrets)) { + if (key) { + mergedVariables[key] = value === undefined || value === null ? '' : value; + } } + logDebug( + `Debug: ${logPrefix} Loaded ${ + Object.keys(projectSecrets).length + } keys from project ${projectId}` + ); + } catch (projectError) { + console.warn( + `Warning: Failed to load project secrets for ${projectId}:`, + projectError.message + ); } - console.log('Debug: Loaded project secrets:', Object.keys(mergedVariables).length); - } catch (projectError) { - console.warn('Warning: Failed to load project secrets:', projectError.message); + } + + if (projectIds.length > 0) { + logDebug('Debug: Total merged keys:', Object.keys(mergedVariables).length); } } - const environmentContent = Object.entries(mergedVariables) - .map(([key, value]) => `${key}=${value}`) - .join('\n'); + const environmentContent = serializeEnvRecordToPlaintext(mergedVariables); // Only create .env.secure if we have content if (encryptionKey && environmentContent) { try { const cipherText = encryptContent(environmentContent, encryptionKey); fs.writeFileSync('.env.secure', cipherText, { encoding: 'utf-8' }); - console.log('Debug: Created .env.secure file'); + logDebug('Debug: Created .env.secure file'); // Add decryption output if debug and show_decrypted are enabled if (process.env.DEBUG === 'true' && process.env.SHOW_DECRYPTED === 'true') { diff --git a/scripts/bws-secure/bws-env-utils.js b/scripts/bws-secure/bws-env-utils.js new file mode 100644 index 00000000000..078bff45f2a --- /dev/null +++ b/scripts/bws-secure/bws-env-utils.js @@ -0,0 +1,119 @@ +/** + * Shared parsing and serialization for BWS multi-project env handling. + * Keeps overlay merge round-trips safe for multiline secret values. + */ + +export const BWS_PROJECT_UUID_RE = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +/** + * Parse comma-separated project IDs: trim, validate UUIDs, dedupe (case-insensitive), preserve order. + * + * @param {string | undefined | null} projectIdString + * @returns {{ ids: string[], skippedInvalid: string[], skippedDuplicates: string[] }} + */ +export function parseProjectIdsDetailed(projectIdString) { + const skippedInvalid = []; + const skippedDuplicates = []; + if (!projectIdString || typeof projectIdString !== 'string') { + return { ids: [], skippedInvalid, skippedDuplicates }; + } + + const raw = projectIdString + .split(',') + .map((s) => s.trim()) + .filter((s) => s.length > 0); + + const seen = new Set(); + const ids = []; + + for (const token of raw) { + if (!BWS_PROJECT_UUID_RE.test(token)) { + skippedInvalid.push(token); + continue; + } + const dedupeKey = token.toLowerCase(); + if (seen.has(dedupeKey)) { + skippedDuplicates.push(token); + continue; + } + seen.add(dedupeKey); + ids.push(token); + } + + return { ids, skippedInvalid, skippedDuplicates }; +} + +/** + * Ordered unique UUIDs only (backward-compatible with simple call sites). + * @param {string | undefined | null} projectIdString + * @returns {string[]} + */ +export function parseProjectIds(projectIdString) { + return parseProjectIdsDetailed(projectIdString).ids; +} + +/** + * Parse env-like text (BWS CLI `-o env` or decrypted `.env.secure.`). + * Continuation lines follow a `KEY=value` line; lines that are not new `KEY=` assignments + * extend the previous value (including blank lines — matches bws-dotenv behavior). + * + * @param {string} output + * @param {{ stripSerializedContinuationSpace?: boolean }} [options] + * @returns {Record} + */ +export function parseEnvironmentOutput(output, options = {}) { + const stripCont = options.stripSerializedContinuationSpace === true; + const result = {}; + if (output === undefined || output === null) return result; + + const lines = String(output).split('\n'); + let currentKey = null; + let currentValue = ''; + + for (const line of lines) { + if (line.includes('=') && !line.startsWith(' ') && !line.startsWith('\t')) { + if (currentKey !== null) { + result[currentKey] = currentValue; + } + const equalIndex = line.indexOf('='); + currentKey = line.substring(0, equalIndex).trim(); + currentValue = line.substring(equalIndex + 1); + } else if (currentKey !== null) { + let segment = line; + if (stripCont && line.startsWith(' ')) { + segment = line.slice(1); + } + currentValue += '\n' + segment; + } + } + + if (currentKey !== null) { + result[currentKey] = currentValue; + } + + return result; +} + +/** + * Serialize a key-value record to plaintext parseable by parseEnvironmentOutput. + * Multiline values use a leading space on continuation lines so embedded `=` does not + * start a false new key line. + * + * @param {Record} record + * @returns {string} + */ +export function serializeEnvRecordToPlaintext(record) { + const lines = []; + for (const [key, value] of Object.entries(record)) { + if (key === undefined || key === null || key === '') continue; + const str = value === undefined || value === null ? '' : String(value); + const parts = str.split('\n'); + lines.push(`${key}=${parts[0] ?? ''}`); + for (let i = 1; i < parts.length; i++) { + // Prefix so line does not parse as a new KEY= row; parser strips one leading space on read. + lines.push(` ${parts[i]}`); + } + } + return lines.join('\n'); +} diff --git a/scripts/bws-secure/check-vars/requiredRuntimeVars.js b/scripts/bws-secure/check-vars/requiredRuntimeVars.js index 3f2351a311f..e54355076ef 100755 --- a/scripts/bws-secure/check-vars/requiredRuntimeVars.js +++ b/scripts/bws-secure/check-vars/requiredRuntimeVars.js @@ -68,7 +68,10 @@ const defaultDirs = [ 'api', 'apps/web/src', 'packages', - 'src' + 'src', + 'sanity', + 'studio', + 'app' ]; /** diff --git a/scripts/bws-secure/guides/MULTI_PROJECT_ID_GUIDE.md b/scripts/bws-secure/guides/MULTI_PROJECT_ID_GUIDE.md new file mode 100644 index 00000000000..ba664490278 --- /dev/null +++ b/scripts/bws-secure/guides/MULTI_PROJECT_ID_GUIDE.md @@ -0,0 +1,282 @@ +# Multi-Project ID Support Guide + +## Overview + +BWS Secure now supports loading secrets from multiple BWS project IDs simultaneously. This allows you to have a "main" source of truth for shared/common variables and load differentiating or additional variables from other projects. + +## Key Features + +- ✅ **Backward Compatible**: Single project IDs still work exactly as before +- ✅ **Comma-Separated Format**: Use simple comma-separated strings +- ✅ **Overlay Strategy**: Later project IDs override earlier ones for duplicate variables +- ✅ **Two Configuration Methods**: Works with both `BWS_PROJECT_ID` env var and `bwsconfig.json` + +### Child process environment + +After `secureRun` loads secrets, the wrapped command receives: + +- **`BWS_PROJECT_ID`**: the **first** (primary) UUID in your list — safe for tools that expect a single project id. +- **`BWS_PROJECT_IDS`**: set only when multiple IDs were used; comma-separated ordered list (same overlay order as loading). + +Invalid segments in a comma-separated list are skipped with a warning; duplicate UUIDs are deduped (case-insensitive) with a warning. + +### Partial failures + +By default, if one project fails to load (network, permissions, missing project), loading **continues** and earlier projects still apply. Warnings explain which IDs failed. + +Set **`BWS_MULTI_PROJECT_FAIL_FAST=true`** (alias: **`BWS_MULTI_PROJECT_FAIL_ON_PARTIAL=true`**) to **exit immediately** on the first failed project load when multiple IDs are configured. + +### Local multi-project mapping + +For **multiple** project IDs, `secureRun` writes a merged encrypted file **`.env.secure.`** (e.g. `.env.secure.local`) so `update-environments/map-env-files.js` can symlink it. Single-ID setups continue to use **`.env.secure.`** only. + +## Configuration Methods + +### Method 1: Direct Environment Variable + +Use comma-separated project IDs in the `BWS_PROJECT_ID` environment variable: + +```bash +# Single project (backward compatible) +BWS_PROJECT_ID="2d8c63de-62d5-4604-bb43-b357000e00b7" pnpm dev + +# Multiple projects (new feature) +BWS_PROJECT_ID="2d8c63de-62d5-4604-bb43-b357000e00b7, 8686161e-6b41-4498-b7bf-b3390109f1d3" pnpm dev +``` + +### Method 2: Configuration File + +Use comma-separated project IDs in your `bwsconfig.json`: + +```json +{ + "projects": [ + { + "platform": "vercel|netlify", + "projectName": "myProject", + "bwsProjectIds": { + "local": "2d8c63de-62d5-4604-bb43-b357000e00b7", + "dev": "8686161e-6b41-4498-b7bf-b3390109f1d3", + "prod": "2d8c63de-62d5-4604-bb43-b357000e00b7, 8686161e-6b41-4498-b7bf-b3390109f1d3" + }, + "preserveVars": ["BWS_ACCESS_TOKEN"] + } + ] +} +``` + +## Variable Overlay Strategy + +When multiple project IDs are specified, secrets are loaded in order with a "last wins" strategy: + +1. **Load Project ID 1** → Base set of variables +2. **Load Project ID 2** → Overlays and overrides duplicates from Project 1 +3. **Load Project ID 3** → Overlays and overrides duplicates from Projects 1 & 2 +4. **Result**: Later project IDs take precedence for matching variable names + +### Example Scenario + +**Project 1 Secrets:** +``` +API_KEY=shared-key +DATABASE_URL=postgres://shared-db +SERVICE_NAME=shared-service +``` + +**Project 2 Secrets:** +``` +DATABASE_URL=postgres://specific-db +CUSTOM_VAR=specific-value +``` + +**Final Merged Result:** +``` +API_KEY=shared-key # From Project 1 +DATABASE_URL=postgres://specific-db # From Project 2 (overrides Project 1) +SERVICE_NAME=shared-service # From Project 1 +CUSTOM_VAR=specific-value # From Project 2 +``` + +## Use Cases + +### Shared Common Variables + +Have 90% of variables in a "main" project and load differentiating variables from additional projects: + +```json +{ + "projects": [ + { + "platform": "vercel", + "projectName": "myApp", + "bwsProjectIds": { + "local": "main-project-id, environment-specific-id" + } + } + ] +} +``` + +### Multi-Environment Configurations + +Load different combinations of secrets for different environments: + +```json +{ + "bwsProjectIds": { + "local": "shared-secrets-id", + "dev": "shared-secrets-id, dev-specific-id", + "prod": "shared-secrets-id, prod-specific-id, compliance-id" + } +} +``` + +### Feature-Based Secret Management + +Organize secrets by feature and compose them as needed: + +```json +{ + "bwsProjectIds": { + "local": "base-config-id, auth-feature-id, payment-feature-id" + } +} +``` + +## Progress Indicators + +When loading multiple project IDs, the tool displays progress indicators: + +``` +[█████████████░░░░░░░░░░░░] 67% | Environment Setup [2/3] Loading secrets from project 2 +``` + +## Debug Logging + +Enable debug logging to see detailed information about multi-project loading: + +```bash +DEBUG=true BWS_PROJECT_ID="id1, id2, id3" pnpm dev +``` + +Debug output will show: +- Number of secrets loaded from each project +- Total merged variables +- Which project IDs are being processed + +## Backward Compatibility + +All existing single-project configurations continue to work without any changes: + +```bash +# Still works exactly as before +BWS_PROJECT_ID="2d8c63de-62d5-4604-bb43-b357000e00b7" pnpm dev +``` + +```json +{ + "bwsProjectIds": { + "local": "2d8c63de-62d5-4604-bb43-b357000e00b7" + } +} +``` + +## Validation + +Project IDs are validated as proper UUIDs. Invalid UUIDs in the comma-separated string are automatically filtered out with a warning: + +``` +Warning: BWS_PROJECT_ID is set but no valid UUIDs found +``` + +## Platform Builds (Netlify/Vercel) + +Multi-project ID support works seamlessly with platform builds. Each environment's project IDs are loaded and merged before deployment. + +## Examples + +### Example 1: Simple Two-Project Setup + +```bash +# Load base configuration + environment-specific overrides +BWS_PROJECT_ID="base-config-uuid, local-overrides-uuid" pnpm dev +``` + +### Example 2: Complex Multi-Environment + +```json +{ + "projects": [ + { + "platform": "vercel", + "projectName": "myApp", + "bwsProjectIds": { + "local": "shared-id", + "dev": "shared-id, dev-id", + "prod": "shared-id, prod-id, monitoring-id" + } + } + ] +} +``` + +### Example 3: Feature Composition + +```bash +# Compose secrets from multiple feature-specific projects +BWS_PROJECT_ID="base-uuid, auth-uuid, payments-uuid, analytics-uuid" pnpm dev +``` + +## Optional: adopting multiple project IDs + +There is **no required migration**. Existing single-UUID configs and `BWS_PROJECT_ID=one-uuid` continue to work unchanged. This section is only if you **choose** to split or layer secrets across more than one BWS project: + +1. Keep your current project ID as the first (base) ID in the list. +2. Add any additional BWS projects you need. +3. Use a comma-separated list in `bwsconfig.json` or `BWS_PROJECT_ID` (see examples above). +4. Verify overlay order locally with `DEBUG=true` before rolling out. + +## Best Practices + +1. **Order Matters**: List project IDs from most general to most specific +2. **Document Intent**: Add comments in your config explaining why multiple projects are used +3. **Keep it Simple**: Don't over-complicate - use multiple IDs only when beneficial +4. **Test Overlays**: Verify that variable overrides work as expected in debug mode +5. **Monitor Loading**: Check debug logs to ensure all projects load successfully + +## Troubleshooting + +### No secrets loaded +- Verify all project IDs are valid UUIDs +- Check that BWS_ACCESS_TOKEN has access to all project IDs +- Enable DEBUG=true to see detailed loading information + +### Unexpected variable values +- Review the order of project IDs (last wins for duplicates) +- Use DEBUG=true and SHOW_DECRYPTED=true to inspect merged results +- Verify each project contains expected secrets in BWS + +### Performance concerns +- Each project ID requires an API call to BWS +- Results are cached to avoid redundant loading +- Consider consolidating if loading too many projects becomes slow + +## Technical Details + +### Implementation + +- **Parsing**: `parseProjectIds()` function validates and extracts UUIDs +- **Loading**: Each project is loaded sequentially with retry logic +- **Merging**: `Object.assign()` provides the overlay strategy +- **Caching**: Loaded project IDs are tracked to prevent duplicate API calls + +### Files Modified + +- `bws-dotenv.js` - Core secret loading logic +- `secureRun.js` - Main execution and environment setup +- `testFolder/bwsconfig.json` - Example configuration + +## Support + +For issues, questions, or feature requests related to multi-project ID support, please refer to the main repository README or open an issue. + diff --git a/scripts/bws-secure/install.sh b/scripts/bws-secure/install.sh index 064bc38d634..3993846505b 100755 --- a/scripts/bws-secure/install.sh +++ b/scripts/bws-secure/install.sh @@ -105,7 +105,7 @@ if [ "$NODE_VERSION" -lt "20" ]; then packageJson.dependencies.yargs = '^18.0.0'; } if (packageJson.dependencies && packageJson.dependencies.glob) { - packageJson.dependencies.glob = '^11.1.0'; + packageJson.dependencies.glob = '^13.0.6'; } } else { // For Node.js < 20, use compatible versions @@ -359,7 +359,7 @@ try { // Add dependencies if they don't exist packageJson.devDependencies = packageJson.devDependencies || {}; - packageJson.devDependencies['dotenv'] = packageJson.devDependencies['dotenv'] || '^17.2.4'; + packageJson.devDependencies['dotenv'] = packageJson.devDependencies['dotenv'] || '^17.4.2'; packageJson.devDependencies['dotenv-cli'] = packageJson.devDependencies['dotenv-cli'] || '^11.0.0'; // Check Node.js version and apply appropriate versions @@ -368,14 +368,14 @@ try { if (nodeVersionNum >= 20) { // For Node.js 20+, use newer versions packageJson.devDependencies['yargs'] = packageJson.devDependencies['yargs'] || '^18.0.0'; - packageJson.devDependencies['glob'] = '^11.1.0'; + packageJson.devDependencies['glob'] = '^13.0.6'; } else { // For Node.js < 20, use compatible versions packageJson.devDependencies['yargs'] = packageJson.devDependencies['yargs'] || '^17.7.2'; packageJson.devDependencies['glob'] = '^10.3.10'; } - packageJson.devDependencies['axios'] = packageJson.devDependencies['axios'] || '^1.13.5'; + packageJson.devDependencies['axios'] = packageJson.devDependencies['axios'] || '^1.15.1'; // Detect existing indentation or use prettier config const originalContent = fs.readFileSync(packageJsonPath, 'utf8'); @@ -575,8 +575,9 @@ README_FILES=("README.md" "Readme.md" "readme.md") README_FOUND=false echo "Looking for README files in: $(pwd)" -# Define the BWS Secure documentation content -BWS_DOC_CONTENT=" +# Define the BWS Secure documentation content (injected into the monorepo README on install/update) +read -r -d '' BWS_DOC_CONTENT << 'BWS_DOC_EOF' || true + ## 🔒 BWS Secure Environmental Variable Integration This project uses [BWS Secure](https://github.com/last-rev-llc/bws-secure) for managing environment variables across different environments. @@ -589,53 +590,63 @@ This project uses [BWS Secure](https://github.com/last-rev-llc/bws-secure) for m 🖱️ **2.** Navigate to the Machine Accounts section, and follow these steps: - Select the appropriate Client/Set of Machine Accounts from the list - - Click on the \"Access Tokens\" tab - - Click \"+ New Access Token\" button - - Give the token a meaningful name (e.g., \"Your Name - Local Development\") - - Click \"Save\" to generate the token + - Click on the "Access Tokens" tab + - Click "+ New Access Token" button + - Give the token a meaningful name (e.g., "Your Name - Local Development") + - Click "Save" to generate the token 📋 **3.** Copy the displayed token (you won't be able to see it again after closing) 💾 **4.** Add it to your .env file in your project root: - \`\`\` + ``` BWS_ACCESS_TOKEN=your_token_here - \`\`\` + ``` ⚠️ **5.** Never commit this token to version control -### 🎯 Token Usage Options: +### 🎯 Token and project options -- **BWS_ACCESS_TOKEN**: Loads ALL projects associated with that token (recommended for multi-project setups) -- **BWS_PROJECT_ID**: Loads only a specific project (use for single-project or testing scenarios) +- **BWS_ACCESS_TOKEN**: Required to load secrets from Bitwarden (scoped to your machine account). +- **BWS_PROJECT_ID** (optional): Restrict to one or more BWS **project UUIDs**. Use a **single** UUID, or **comma-separated UUIDs** to merge projects (later IDs win when the same key exists in more than one project). Omit to use `bwsconfig.json` / project selection. Fully backward compatible with single-UUID setups. -**Example for single project:** -\`\`\` +**Single project:** +``` BWS_PROJECT_ID=00000000-0000-0000-0000-000000000001 -\`\`\` +``` -The project ID can be found in the Bitwarden Secrets Manager, within the list of projects. +**Multiple projects (optional):** +``` +BWS_PROJECT_ID=00000000-0000-0000-0000-000000000001, 11111111-1111-1111-1111-111111111111 +``` -### 🔧 Common Issues & Troubleshooting: +More detail: [Multi-project ID guide](https://github.com/last-rev-llc/bws-secure/blob/main/guides/MULTI_PROJECT_ID_GUIDE.md). -- **\"No projects found\"**: Verify your token has project access permissions in Bitwarden -- **\"Access denied\"**: Check that the Machine Account has read permissions for the target projects +### Transient `.env.secure` files + +Encrypted `.env.secure` / `.env.secure.*` files in the repo root are **removed when each run finishes** (after your command runs; secrets are already in the process environment). Set **`BWS_KEEP_SECURE_FILES=true`** only when you need to inspect those files. + +### 🔧 Common Issues & Troubleshooting + +- **"No projects found"**: Verify your token has project access permissions in Bitwarden +- **"Access denied"**: Check that the Machine Account has read permissions for the target projects - **Token not working**: Ensure no extra spaces when copying from Bitwarden -- **Multiple projects loading**: This is normal with BWS_ACCESS_TOKEN - use BWS_PROJECT_ID for single project +- **Multiple projects / overlays**: Order matters for duplicate keys—see the multi-project guide above ### Updating BWS Secure To update BWS Secure to the latest version, you can use the convenient script that was added to your package.json: -\`\`\`bash -npm run bws-update # Or use your project's package manager: yarn bws-update, pnpm bws-update -\`\`\` +```bash +npm run bws-update # Or: yarn bws-update, pnpm bws-update +``` -Alternatively, you can run the following command manually from your project root: +Alternatively, from your project root: -\`\`\`bash +```bash rm -rf scripts/bws-secure && git clone git@github.com:last-rev-llc/bws-secure.git scripts/bws-secure && rm -rf scripts/bws-secure/.git && bash scripts/bws-secure/install.sh -\`\`\` -" +``` + +BWS_DOC_EOF for README_FILE in "${README_FILES[@]}"; do echo "Checking for $README_FILE..." diff --git a/scripts/bws-secure/package.json b/scripts/bws-secure/package.json index 75c5217d153..ff4a7e33dbd 100644 --- a/scripts/bws-secure/package.json +++ b/scripts/bws-secure/package.json @@ -7,13 +7,16 @@ "start": "node secureRun.js", "upload": "node secureRun.js --upload-secrets", "list": "node list-projects.js", - "scan": "node check-vars/requiredRuntimeVars.js" + "scan": "node check-vars/requiredRuntimeVars.js", + "test:bws-env": "node --test tests/bws-env-utils.test.mjs", + "test:e2e": "node --test tests/e2e/secureRun.e2e.test.mjs", + "test": "node --test tests/bws-env-utils.test.mjs tests/e2e/secureRun.e2e.test.mjs" }, "dependencies": { - "axios": "^1.13.5", - "dotenv": "^17.2.4", + "axios": "^1.15.1", + "dotenv": "^17.4.2", "dotenv-cli": "^11.0.0", - "glob": "^11.1.0", + "glob": "^13.0.6", "yargs": "^18.0.0" } } diff --git a/scripts/bws-secure/secureRun.js b/scripts/bws-secure/secureRun.js index 4d03114f697..29344f37ffb 100755 --- a/scripts/bws-secure/secureRun.js +++ b/scripts/bws-secure/secureRun.js @@ -19,6 +19,15 @@ import { log } from './project-selector.js'; import { execBwsCommandWithRetrySync } from './bws-retry-utils.js'; +import { + parseProjectIds, + parseProjectIdsDetailed, + parseEnvironmentOutput, + serializeEnvRecordToPlaintext +} from './bws-env-utils.js'; + +/** Encrypted `.env.secure.*` files written by secureRun / loadBwsProjectSecrets use serializeEnvRecordToPlaintext. */ +const PARSE_SECURE_FILE = { stripSerializedContinuationSpace: true }; // Import functions from project-selector module // Get the directory name in ESM @@ -46,37 +55,39 @@ if (SUPPRESS_ALL) { console.debug = () => {}; } -// Helper function to properly parse multiline environment variables from BWS output -function parseEnvironmentOutput(output) { - const result = {}; - const lines = output.split('\n'); - let currentKey = null; - let currentValue = ''; - - for (const line of lines) { - // Check if this line starts a new variable (has = and doesn't start with whitespace) - if (line.includes('=') && !line.startsWith(' ') && !line.startsWith('\t')) { - // Save previous variable if exists - if (currentKey !== null) { - result[currentKey] = currentValue; - } +const multiProjectFailFast = + process.env.BWS_MULTI_PROJECT_FAIL_FAST === 'true' || + process.env.BWS_MULTI_PROJECT_FAIL_ON_PARTIAL === 'true'; - // Start new variable - const equalIndex = line.indexOf('='); - currentKey = line.substring(0, equalIndex).trim(); - currentValue = line.substring(equalIndex + 1); - } else if (currentKey !== null && line.trim() !== '') { - // This is a continuation line for the current variable - currentValue += '\n' + line; - } +function logParsedProjectIds(context, raw) { + const d = parseProjectIdsDetailed(raw); + if (d.skippedInvalid.length) { + log('warn', `[${context}] Skipped non-UUID segments: ${d.skippedInvalid.join(', ')}`); } - - // Don't forget the last variable - if (currentKey !== null) { - result[currentKey] = currentValue; + if (d.skippedDuplicates.length) { + log('warn', `[${context}] Skipped duplicate UUIDs: ${d.skippedDuplicates.join(', ')}`); } + if (d.ids.length) { + log('debug', `[${context}] Parsed ${d.ids.length} project ID(s)`); + } + return d.ids; +} - return result; +/** After CLI env restore, child processes get a single primary UUID in BWS_PROJECT_ID; full list in BWS_PROJECT_IDS when multi. */ +function normalizeBwsProjectIdForChild() { + const raw = process.env.BWS_PROJECT_ID; + if (!raw) { + delete process.env.BWS_PROJECT_IDS; + return; + } + const ids = parseProjectIdsDetailed(raw).ids; + if (ids.length === 0) return; + process.env.BWS_PROJECT_ID = ids[0]; + if (ids.length > 1) { + process.env.BWS_PROJECT_IDS = ids.join(','); + } else { + delete process.env.BWS_PROJECT_IDS; + } } // Helper function to get BWS organization ID with fallback @@ -504,7 +515,12 @@ function printEnvironmentSummary() { process.exit(1); } - const projectId = process.env.BWS_PROJECT_ID || 'none'; + const idInfo = parseProjectIdsDetailed(process.env.BWS_PROJECT_ID || ''); + let projectId = process.env.BWS_PROJECT_ID || 'none'; + if (idInfo.ids.length === 1) projectId = idInfo.ids[0]; + else if (idInfo.ids.length > 1) { + projectId = `${idInfo.ids[0]} (+${idInfo.ids.length - 1} more)`; + } // Cyan color code const cyan = '\x1b[36m'; @@ -534,7 +550,8 @@ function printEnvironmentSummary() { const originalEnvironment = { ...process.env }; let originalEnvironmentFileContent = ''; // Store original .env file content -// Helper function to get project ID with fallback to first available +// Helper function to get project ID(s) with fallback to first available +// Returns comma-separated string to maintain backward compatibility function getProjectIdWithFallback(project, environment) { let projectId = project.bwsProjectIds?.[environment]; @@ -550,6 +567,7 @@ function getProjectIdWithFallback(project, environment) { } } + // Return as-is (supports both single UUID and comma-separated UUIDs) return projectId; } @@ -578,17 +596,17 @@ async function setupEnvironment(options = { isPlatformBuild: false }) { process.env.BWS_EPHEMERAL_KEY = crypto.randomBytes(32).toString('hex'); } - // Direct BWS_PROJECT_ID bypass - if a valid UUID is provided, use it directly - if ( - process.env.BWS_PROJECT_ID && - process.env.BWS_PROJECT_ID.match( - /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i - ) - ) { - log('info', `Using direct BWS_PROJECT_ID: ${process.env.BWS_PROJECT_ID}`); + // Direct BWS_PROJECT_ID bypass - if valid UUID(s) provided, use them directly + const directProjectIds = process.env.BWS_PROJECT_ID + ? logParsedProjectIds('direct BWS_PROJECT_ID', process.env.BWS_PROJECT_ID) + : []; - // Load secrets directly using the provided project ID - const projectId = process.env.BWS_PROJECT_ID; + if (directProjectIds.length > 0) { + const isMultiple = directProjectIds.length > 1; + log( + 'info', + `Using direct BWS_PROJECT_ID${isMultiple ? 's' : ''}: ${directProjectIds.join(', ')}` + ); // Enable progress mode to suppress console interference enableProgressMode(); @@ -600,28 +618,85 @@ async function setupEnvironment(options = { isPlatformBuild: false }) { showSecureRunProgress('Environment Setup', 4.3, 6, `Connecting to BWS...`); await new Promise((resolve) => setTimeout(resolve, 150)); - await loadEnvironmentSecrets(projectId, projectId); + // Load secrets from all project IDs with overlay strategy (later IDs win) + const allDecryptedVariables = {}; + const directLoadFailures = []; - showSecureRunProgress('Environment Setup', 4.7, 6, `Processing environment variables...`); - await new Promise((resolve) => setTimeout(resolve, 150)); - loadedProjectIds.add(projectId); - - // Load the variables into process.env - const sourceFile = `.env.secure.${projectId}`; - if (fs.existsSync(sourceFile)) { - const content = fs.readFileSync(sourceFile, 'utf8'); - const decrypted = decryptContent(content, process.env.BWS_EPHEMERAL_KEY); - const decryptedVariables = parseEnvironmentOutput(decrypted); - - for (const key of Object.keys(decryptedVariables)) { - if (!(key in process.env)) { - process.env[key] = decryptedVariables[key]; + for (const [index, projectId] of directProjectIds.entries()) { + const progressLabel = isMultiple ? `[${index + 1}/${directProjectIds.length}]` : ''; + showSecureRunProgress( + 'Environment Setup', + 4.3 + (0.4 * (index + 1)) / directProjectIds.length, + 6, + `${progressLabel} Loading secrets from project ${index + 1}` + ); + + const ok = await loadBwsProjectSecrets(projectId); + if (ok) { + loadedProjectIds.add(projectId); + log('info', `${progressLabel} Successfully loaded BWS project ${projectId}`); + } else { + directLoadFailures.push(projectId); + log('error', `${progressLabel} Failed to load secrets for project ${projectId}`); + if (isMultiple && multiProjectFailFast) { + process.exit(1); } } - log('debug', `Loaded environment from direct BWS_PROJECT_ID: ${projectId}`); + const sourceFile = `.env.secure.${projectId}`; + if (fs.existsSync(sourceFile)) { + const content = fs.readFileSync(sourceFile, 'utf8'); + const decrypted = decryptContent(content, process.env.BWS_EPHEMERAL_KEY); + const decryptedVariables = parseEnvironmentOutput(decrypted, PARSE_SECURE_FILE); + Object.assign(allDecryptedVariables, decryptedVariables); + + log( + 'debug', + `${progressLabel} Merged ${ + Object.keys(decryptedVariables).length + } keys from project ${projectId}` + ); + } + + await new Promise((resolve) => setTimeout(resolve, 100)); + } + + if (directLoadFailures.length > 0 && isMultiple && !multiProjectFailFast) { + log( + 'warn', + `Multi-project: ${ + directLoadFailures.length + } project load(s) failed (${directLoadFailures.join( + ', ' + )}). Overlay may be incomplete. Set BWS_MULTI_PROJECT_FAIL_FAST=true to abort on first failure.` + ); + } + + const envLabelDirect = process.env.BWS_ENV || 'local'; + if (isMultiple && Object.keys(allDecryptedVariables).length > 0) { + const mergedPlain = serializeEnvRecordToPlaintext(allDecryptedVariables); + fs.writeFileSync( + `.env.secure.${envLabelDirect}`, + encryptContent(mergedPlain, process.env.BWS_EPHEMERAL_KEY), + { encoding: 'utf-8' } + ); + log('debug', `Wrote merged .env.secure.${envLabelDirect} for multi-project mapping`); } + // Apply merged variables (do not override existing process.env) + for (const [key, value] of Object.entries(allDecryptedVariables)) { + if (!(key in process.env)) { + process.env[key] = value ?? ''; + } + } + + log( + 'debug', + `Total merged keys from direct BWS_PROJECT_ID${isMultiple ? 's' : ''}: ${ + Object.keys(allDecryptedVariables).length + }` + ); + // Always show incremental progress to 100%, regardless of additional environments showSecureRunProgress('Environment Setup', 5.0, 6, `Configuring environment variables...`); await new Promise((resolve) => setTimeout(resolve, 200)); @@ -636,9 +711,14 @@ async function setupEnvironment(options = { isPlatformBuild: false }) { const project = config.projects.find((p) => p.projectName === process.env.BWS_PROJECT); if (project) { const currentEnv = process.env.BWS_ENV || 'local'; - const additionalProjectIds = Object.entries(project.bwsProjectIds).filter( - ([env, id]) => env !== currentEnv && id && !loadedProjectIds.has(id) - ); + + // Parse comma-separated project IDs and filter out already loaded ones + const additionalProjectIds = Object.entries(project.bwsProjectIds) + .filter(([env, projectIdString]) => env !== currentEnv && projectIdString) + .flatMap(([env, projectIdString]) => { + const projectIds = parseProjectIds(projectIdString); + return projectIds.filter((id) => !loadedProjectIds.has(id)).map((id) => [env, id]); + }); if (additionalProjectIds.length > 0) { let processedCount = 0; @@ -654,8 +734,8 @@ async function setupEnvironment(options = { isPlatformBuild: false }) { ); if (!loadedProjectIds.has(additionalProjectId)) { - await loadEnvironmentSecrets(additionalProjectId, additionalProjectId); - loadedProjectIds.add(additionalProjectId); + const ok = await loadBwsProjectSecrets(additionalProjectId); + if (ok) loadedProjectIds.add(additionalProjectId); } await new Promise((resolve) => setTimeout(resolve, 100)); } @@ -789,7 +869,9 @@ async function setupEnvironment(options = { isPlatformBuild: false }) { ); if (selectedProjectConfig && selectedProjectConfig.bwsProjectIds) { Object.values(selectedProjectConfig.bwsProjectIds).forEach((id) => { - if (id) projectIdsToLoad.add(id); + if (id) { + parseProjectIds(id).forEach((pid) => projectIdsToLoad.add(pid)); + } }); log('debug', `Will load secrets for project IDs: ${[...projectIdsToLoad].join(', ')}`); } @@ -895,41 +977,82 @@ async function setupEnvironment(options = { isPlatformBuild: false }) { // Track if any secrets were successfully loaded let secretsLoaded = false; - // Only load and create environment files that don't already exist - const environmentsToLoad = Object.entries(environmentMappings).filter( - ([_, projectId]) => projectId - ); + // Parse and expand comma-separated project IDs for each environment + const environmentsToLoad = Object.entries(environmentMappings) + .filter(([_, projectIdString]) => projectIdString) + .flatMap(([env, projectIdString]) => { + const projectIds = parseProjectIds(projectIdString); + return projectIds.map((id) => [env, id]); + }); + let envProcessedCount = 0; + // Group by environment to merge secrets properly + const envGroups = {}; for (const [environment_, projectId] of environmentsToLoad) { + if (!envGroups[environment_]) { + envGroups[environment_] = []; + } + envGroups[environment_].push({ projectId }); + } + + for (const [environment_, envData] of Object.entries(envGroups)) { envProcessedCount++; // Calculate incremental progress from 4.0 (67%) towards 6.0 (100%) - const progressStep = 4 + (2 * envProcessedCount) / environmentsToLoad.length; // 4.0 to 6.0 + const progressStep = 4 + (2 * envProcessedCount) / Object.keys(envGroups).length; showSecureRunProgress( 'Environment Setup', progressStep, 6, - `Loading project secrets ${envProcessedCount}/${environmentsToLoad.length} (${environment_})` + `Loading project secrets ${envProcessedCount}/${ + Object.keys(envGroups).length + } (${environment_})` ); - // Only load this project ID if we haven't already - if (!loadedProjectIds.has(projectId)) { - const success = await loadEnvironmentSecrets(projectId, projectId); - if (success) { + // Load all project IDs for this environment with overlay strategy + const mergedVariables = {}; + for (const { projectId } of envData) { + // Only load this project ID if we haven't already + if (!loadedProjectIds.has(projectId)) { + const success = await loadBwsProjectSecrets(projectId); + if (success) { + secretsLoaded = true; + loadedProjectIds.add(projectId); + } else { + log( + 'error', + `Failed to load secrets for project ${projectId} (environment ${environment_})` + ); + if (multiProjectFailFast && envData.length > 1) { + process.exit(1); + } + } + } else { secretsLoaded = true; - loadedProjectIds.add(projectId); } - } else { - // If we already loaded this ID, consider it a success - secretsLoaded = true; + + // Merge secrets with overlay strategy + const sourceFile = `.env.secure.${projectId}`; + if (fs.existsSync(sourceFile)) { + const content = fs.readFileSync(sourceFile, 'utf8'); + const decrypted = decryptContent(content, process.env.BWS_EPHEMERAL_KEY); + const decryptedVariables = parseEnvironmentOutput(decrypted, PARSE_SECURE_FILE); + + // Later project IDs override earlier ones + Object.assign(mergedVariables, decryptedVariables); + } } - // Create symlink or copy the file as needed - const sourceFile = `.env.secure.${projectId}`; - const targetFile = `.env.secure.${environment_}`; - if (fs.existsSync(sourceFile)) { - fs.copyFileSync(sourceFile, targetFile); - log('debug', `Created ${targetFile} from ${sourceFile}`); + // Write merged environment file (safe for multiline values) + if (Object.keys(mergedVariables).length > 0) { + const mergedContent = serializeEnvRecordToPlaintext(mergedVariables); + const cipherText = encryptContent(mergedContent, process.env.BWS_EPHEMERAL_KEY); + const targetFile = `.env.secure.${environment_}`; + fs.writeFileSync(targetFile, cipherText); + log( + 'debug', + `Created ${targetFile} with ${Object.keys(mergedVariables).length} merged secrets` + ); } } @@ -996,32 +1119,52 @@ async function setupEnvironment(options = { isPlatformBuild: false }) { (p) => p.projectName === originalEnvironment_.BWS_PROJECT ); if (project) { - let projectId = project.bwsProjectIds[originalEnvironment_.BWS_ENV]; + let projectIdString = project.bwsProjectIds[originalEnvironment_.BWS_ENV]; // If no project ID found for the specific environment, fall back to first available - if (!projectId && project.bwsProjectIds) { + if (!projectIdString && project.bwsProjectIds) { const availableProjectIds = Object.values(project.bwsProjectIds).filter((id) => id); if (availableProjectIds.length > 0) { - projectId = availableProjectIds[0]; + projectIdString = availableProjectIds[0]; log( 'info', - `No project ID found for environment '${originalEnvironment_.BWS_ENV}', using fallback: ${projectId}` + `No project ID found for environment '${originalEnvironment_.BWS_ENV}', using fallback: ${projectIdString}` ); } } - if (projectId) { - process.env.BWS_PROJECT_ID = projectId; - const sourceFile = `.env.secure.${projectId}`; - if (fs.existsSync(sourceFile)) { - const content = fs.readFileSync(sourceFile, 'utf8'); - const decrypted = decryptContent(content, process.env.BWS_EPHEMERAL_KEY); - Object.assign(process.env, dotenv.parse(decrypted)); - log( - 'info', - `Restored environment from ${sourceFile} for ${originalEnvironment_.BWS_ENV} environment` - ); + if (projectIdString) { + const projectIds = parseProjectIds(projectIdString); + const mergedVariables = {}; + + for (const projectId of projectIds) { + const sourceFile = `.env.secure.${projectId}`; + if (fs.existsSync(sourceFile)) { + const content = fs.readFileSync(sourceFile, 'utf8'); + const decrypted = decryptContent(content, process.env.BWS_EPHEMERAL_KEY); + Object.assign( + mergedVariables, + parseEnvironmentOutput(decrypted, PARSE_SECURE_FILE) + ); + } + } + + Object.assign(process.env, mergedVariables); + if (projectIds.length > 0) { + process.env.BWS_PROJECT_ID = projectIds[0]; + if (projectIds.length > 1) { + process.env.BWS_PROJECT_IDS = projectIds.join(','); + } else { + delete process.env.BWS_PROJECT_IDS; + } } + + log( + 'info', + `Restored ${Object.keys(mergedVariables).length} variables for ${ + originalEnvironment_.BWS_ENV + } environment` + ); } } } @@ -1060,9 +1203,16 @@ async function setupEnvironment(options = { isPlatformBuild: false }) { // For local development, prioritize loading the current environment first const environment = process.env.BWS_ENV || 'local'; - const currentProjectId = getProjectIdWithFallback(project, environment); + const currentProjectIdString = getProjectIdWithFallback(project, environment); + + if (currentProjectIdString) { + const currentProjectIds = logParsedProjectIds( + `bwsProjectIds[${environment}]`, + currentProjectIdString + ); + const isMultiple = currentProjectIds.length > 1; + const localLoadFailures = []; - if (currentProjectId) { // Enable progress mode to suppress console interference enableProgressMode(); @@ -1076,41 +1226,97 @@ async function setupEnvironment(options = { isPlatformBuild: false }) { log( 'debug', - `Loading secrets for current environment: ${environment} (project ID: ${currentProjectId})` + `Loading secrets for current environment: ${environment} (project ID${ + isMultiple ? 's' : '' + }: ${currentProjectIds.join(', ')})` ); - if (!loadedProjectIds.has(currentProjectId)) { - await loadEnvironmentSecrets(currentProjectId, currentProjectId); - loadedProjectIds.add(currentProjectId); - } - // Set the project ID in process.env - process.env.BWS_PROJECT_ID = currentProjectId; - - // Load the active environment variables into process.env - const sourceFile = `.env.secure.${currentProjectId}`; - if (fs.existsSync(sourceFile)) { - const content = fs.readFileSync(sourceFile, 'utf8'); - const decrypted = decryptContent(content, process.env.BWS_EPHEMERAL_KEY); - - // Parse decrypted content but don't override existing env vars - const decryptedVariables = parseEnvironmentOutput(decrypted); - for (const key of Object.keys(decryptedVariables)) { - // Only set if not already defined in process.env - if (!(key in process.env)) { - process.env[key] = decryptedVariables[key]; + const allDecryptedVariables = {}; + + for (const [index, projectId] of currentProjectIds.entries()) { + if (!loadedProjectIds.has(projectId)) { + const progressLabel = isMultiple ? `[${index + 1}/${currentProjectIds.length}]` : ''; + log('debug', `${progressLabel} Loading secrets from project ${projectId}`); + + const ok = await loadBwsProjectSecrets(projectId); + if (ok) { + loadedProjectIds.add(projectId); + } else { + localLoadFailures.push(projectId); + log('error', `${progressLabel} Failed to load secrets for project ${projectId}`); + if (isMultiple && multiProjectFailFast) { + process.exit(1); + } } } - log('debug', `Loaded environment from ${sourceFile} for local development`); + const sourceFile = `.env.secure.${projectId}`; + if (fs.existsSync(sourceFile)) { + const content = fs.readFileSync(sourceFile, 'utf8'); + const decrypted = decryptContent(content, process.env.BWS_EPHEMERAL_KEY); + Object.assign( + allDecryptedVariables, + parseEnvironmentOutput(decrypted, PARSE_SECURE_FILE) + ); + } + } + + if (localLoadFailures.length > 0 && isMultiple && !multiProjectFailFast) { + log( + 'warn', + `Multi-project: ${ + localLoadFailures.length + } project load(s) failed (${localLoadFailures.join( + ', ' + )}). Overlay may be incomplete. Set BWS_MULTI_PROJECT_FAIL_FAST=true to abort.` + ); + } + + if (isMultiple && Object.keys(allDecryptedVariables).length > 0) { + fs.writeFileSync( + `.env.secure.${environment}`, + encryptContent( + serializeEnvRecordToPlaintext(allDecryptedVariables), + process.env.BWS_EPHEMERAL_KEY + ), + { encoding: 'utf-8' } + ); + log('debug', `Wrote merged .env.secure.${environment} for multi-project mapping`); + } + + if (currentProjectIds.length > 0) { + process.env.BWS_PROJECT_ID = currentProjectIds[0]; + if (currentProjectIds.length > 1) { + process.env.BWS_PROJECT_IDS = currentProjectIds.join(','); + } else { + delete process.env.BWS_PROJECT_IDS; + } + } + + for (const [key, value] of Object.entries(allDecryptedVariables)) { + if (!(key in process.env)) { + process.env[key] = value ?? ''; + } } + + log( + 'debug', + `Loaded ${ + Object.keys(allDecryptedVariables).length + } total keys for local development (merged)` + ); } else { log('warn', `No project ID found for environment ${environment} and no fallback available`); } // Then load any other project IDs that might be needed - const additionalProjectIds = Object.entries(project.bwsProjectIds).filter( - ([env, projectId]) => env !== environment && projectId && !loadedProjectIds.has(projectId) - ); + // Parse each projectId string to handle comma-separated values + const additionalProjectIds = Object.entries(project.bwsProjectIds) + .filter(([env, projectIdString]) => env !== environment && projectIdString) + .flatMap(([env, projectIdString]) => { + const projectIds = parseProjectIds(projectIdString); + return projectIds.filter((id) => !loadedProjectIds.has(id)).map((id) => [env, id]); + }); if (additionalProjectIds.length > 0) { let processedCount = 0; @@ -1134,8 +1340,8 @@ async function setupEnvironment(options = { isPlatformBuild: false }) { 'debug', `Loading additional secrets for ${projectName} (${env}) project ID: ${projectId}` ); - await loadEnvironmentSecrets(projectId, projectId); - loadedProjectIds.add(projectId); + const ok = await loadBwsProjectSecrets(projectId); + if (ok) loadedProjectIds.add(projectId); } } @@ -1212,10 +1418,10 @@ function encryptContent(content, encryptionKey) { return `${nonce.toString('base64')}:${authTag.toString('base64')}:${encrypted}`; } -// New function to handle environment-specific secrets -async function loadEnvironmentSecrets(environment, projectId) { - if (!projectId || !environment) { - log('error', 'Critical Error: Missing projectId or environment name'); +/** Fetch secrets for one BWS project UUID and write `.env.secure.` (encrypted). */ +async function loadBwsProjectSecrets(projectId) { + if (!projectId) { + log('error', 'Critical Error: Missing projectId'); return false; } @@ -1225,10 +1431,8 @@ async function loadEnvironmentSecrets(environment, projectId) { } try { - // More concise logging - log('debug', `Loading secrets for ${projectId}...`); + log('debug', `Loading secrets for project ${projectId}...`); - // Use retry logic for BWS secret list command const output = execBwsCommandWithRetrySync( `${getBwsCommand()} secret list -t ${ process.env.BWS_ACCESS_TOKEN @@ -1240,31 +1444,26 @@ async function loadEnvironmentSecrets(environment, projectId) { let bwsSecrets; try { bwsSecrets = JSON.parse(output || '[]'); - // Validate that we actually got secrets back if (!Array.isArray(bwsSecrets) || bwsSecrets.length === 0) { - log( - 'error', - `Critical Error: No secrets found for projectId ${projectId} (${environment})` - ); + log('error', `Critical Error: No secrets found for projectId ${projectId}`); return false; } } catch (parseError) { - log( - 'error', - `Critical Error: Invalid secrets data for ${environment}: ${parseError.message}` - ); + log('error', `Critical Error: Invalid secrets JSON for ${projectId}: ${parseError.message}`); return false; } - // Create the secure file - const environmentContent = bwsSecrets.map(({ key, value }) => `${key}=${value}`).join('\n'); + const record = Object.fromEntries( + bwsSecrets.map(({ key, value }) => [key, value === undefined || value === null ? '' : value]) + ); + const environmentContent = serializeEnvRecordToPlaintext(record); + if (process.env.BWS_EPHEMERAL_KEY && environmentContent) { const cipherText = encryptContent(environmentContent, process.env.BWS_EPHEMERAL_KEY); fs.writeFileSync(`.env.secure.${projectId}`, cipherText, { encoding: 'utf-8' }); - // Only show detailed counts in debug mode if (process.env.DEBUG === 'true') { log('debug', `Created .env.secure.${projectId} with ${bwsSecrets.length} secrets`); } @@ -1277,14 +1476,11 @@ async function loadEnvironmentSecrets(environment, projectId) { return false; } catch (error) { if (error?.message?.includes('404 Not Found')) { - log( - 'error', - `Critical Error: Project ${projectId} (${environment}): no secrets found or no access` - ); + log('error', `Critical Error: Project ${projectId}: not found or no access`); return false; } - log('error', `Critical Error: Failed to load secrets for ${environment}: ${error.message}`); + log('error', `Critical Error: Failed to load secrets for ${projectId}: ${error.message}`); if (process.env.DEBUG === 'true') { if (error?.stdout) { log('debug', 'stdout:', error.stdout.toString()); @@ -1307,10 +1503,10 @@ function loadSecureEnvironment(environment) { const decrypted = decryptContent(encryptedText, process.env.BWS_EPHEMERAL_KEY); // Load decrypted vars into process.env - const parsedVariables = parseEnvironmentOutput(decrypted); + const parsedVariables = parseEnvironmentOutput(decrypted, PARSE_SECURE_FILE); for (const [key, value] of Object.entries(parsedVariables)) { - if (key && value !== undefined) { - process.env[key.trim()] = value; + if (key) { + process.env[key.trim()] = value ?? ''; } } log('debug', `${environment} environment secrets loaded into process.env`); @@ -1322,7 +1518,7 @@ function loadSecureEnvironment(environment) { // Move cleanup function to top level function cleanupSecureFiles(verbose = false) { try { - // Clean up all .env.secure.* files and .env.secure + // Clean up all .env.secure.* files and .env.secure (encrypted artifacts in consumer repo cwd) const files = fs.readdirSync(process.cwd()); for (const file of files) { if (file === '.env.secure' || file.startsWith('.env.secure.')) { @@ -1339,6 +1535,24 @@ function cleanupSecureFiles(verbose = false) { } } +/** + * Remove transient `.env.secure` / `.env.secure.*` files after secrets are in process.env. + * Nested secure-run (child) skips — parent owns cleanup. + * Set BWS_KEEP_SECURE_FILES=true to leave files for debugging (not recommended in CI). + */ +function finalizeBwsSecureArtifactsCleanup(verbose = false) { + if (isNestedExecution) { + return; + } + if (process.env.BWS_KEEP_SECURE_FILES === 'true') { + if (process.env.DEBUG === 'true') { + log('debug', 'BWS_KEEP_SECURE_FILES=true — skipping removal of .env.secure* files'); + } + return; + } + cleanupSecureFiles(verbose); +} + // Add function to restore original .env file content async function restoreOriginalEnvironmentFile() { try { @@ -1554,6 +1768,8 @@ async function handleUploadCommand() { process.env[key] = value; } + normalizeBwsProjectIdForChild(); + // Restore original .env file content (silently) await restoreOriginalEnvironmentFile(); } else { @@ -1575,6 +1791,7 @@ async function handleUploadCommand() { const command = process.argv.slice(2); if (command.length === 0) { log('warn', 'No command provided to execute'); + finalizeBwsSecureArtifactsCleanup(); process.exit(0); } @@ -1590,6 +1807,7 @@ async function handleUploadCommand() { shell: true }); + finalizeBwsSecureArtifactsCleanup(); process.exit(result.status); } catch (error) { // Always show critical errors even when suppressed @@ -1599,42 +1817,25 @@ async function handleUploadCommand() { } })(); -// Comment out cleanup registrations +// Backup cleanup if the process exits without going through finalizeBwsSecureArtifactsCleanup (e.g. early process.exit) process.on('exit', () => { - // Only clean up if this is the root execution - if (!isNestedExecution) { - // Clean up without verbose output (silent cleanup) - cleanupSecureFiles(false); - - // We don't want to restore the original .env file as it would remove the project selection - // Instead, the BWS_ENV is handled by restoreOriginalEnvironmentFile() which preserves project options - if (originalEnvironmentFileContent && process.env.DEBUG === 'true') { - log('debug', 'Skipping complete .env restoration to preserve BWS project selection'); - } + finalizeBwsSecureArtifactsCleanup(false); + if (originalEnvironmentFileContent && process.env.DEBUG === 'true') { + log('debug', 'Skipping complete .env restoration to preserve BWS project selection'); } }); process.on('SIGINT', async () => { - // Only clean up if this is the root execution + finalizeBwsSecureArtifactsCleanup(false); if (!isNestedExecution) { - // Clean up without verbose output (silent cleanup) - cleanupSecureFiles(false); - - // We don't want to restore the entire original .env file - // The restoreOriginalEnvironmentFile function will only restore the BWS_ENV value if needed await restoreOriginalEnvironmentFile(); } process.exit(0); }); process.on('SIGTERM', async () => { - // Only clean up if this is the root execution + finalizeBwsSecureArtifactsCleanup(false); if (!isNestedExecution) { - // Clean up without verbose output (silent cleanup) - cleanupSecureFiles(false); - - // We don't want to restore the entire original .env file - // The restoreOriginalEnvironmentFile function will only restore the BWS_ENV value if needed await restoreOriginalEnvironmentFile(); } process.exit(0); @@ -1646,13 +1847,8 @@ process.on('uncaughtException', async (error) => { disableProgressMode(); console.error('Uncaught Exception:', error); - // Only clean up if this is the root execution + finalizeBwsSecureArtifactsCleanup(false); if (!isNestedExecution) { - // Clean up without verbose output (silent cleanup) - cleanupSecureFiles(false); - - // We don't want to restore the entire original .env file - // The restoreOriginalEnvironmentFile function will only restore the BWS_ENV value if needed await restoreOriginalEnvironmentFile(); } process.exit(1); diff --git a/scripts/bws-secure/tests/README.md b/scripts/bws-secure/tests/README.md index 6d34849646f..5fef3f15d84 100644 --- a/scripts/bws-secure/tests/README.md +++ b/scripts/bws-secure/tests/README.md @@ -1,8 +1,44 @@ # BWS Secure Tests -This directory contains various test scripts for the BWS Secure environment management system. +This directory contains test scripts for the BWS Secure environment management system. -## Available Tests +## Automated tests (node --test) + +Run from the repo root: + +```bash +npm run test:bws-env # unit tests for parsing / serialization +npm run test:e2e # full end-to-end runs of secureRun.js +npm test # both suites (14 tests) +``` + +### `bws-env-utils.test.mjs` (unit) + +Covers `parseProjectIds`, UUID validation / dedupe / invalid segments, and the multiline-safe +`serializeEnvRecordToPlaintext` <-> `parseEnvironmentOutput` round-trip used by the encrypted +`.env.secure.*` files. + +### `e2e/secureRun.e2e.test.mjs` (end-to-end, Unix-only) + +Spawns the real `secureRun.js` against a fake `bws` CLI in a simulated consumer repo +(`/scripts/bws-secure/` + `/node_modules/.bin/bws` shim + `/.env`). +Scenarios covered: + +- single UUID direct `BWS_PROJECT_ID` bypass (backward compat) +- multi-UUID direct bypass with overlay order (later wins) and `BWS_PROJECT_IDS` exposed +- invalid UUID segments skipped, valid UUIDs still load +- duplicate UUIDs deduped (case-insensitive) +- cleanup: `.env.secure*` files removed after a successful run +- `BWS_KEEP_SECURE_FILES=true` preserves `.env.secure*` +- `BWS_MULTI_PROJECT_FAIL_FAST=true` exits non-zero on partial failure +- multiline secret (private-key style) round-trips through the merge unchanged + +Cross-platform: on Unix the fake `bws` is a `#!/usr/bin/env node` shebang script; on Windows +a `bws.cmd` wrapper is generated alongside a placeholder `bws` file (so +`ensureBwsInstalled()`'s existence check passes and `cmd.exe` resolves the explicit path via +`PATHEXT`). Set `E2E_DISABLE_WINDOWS=1` to skip the suite on Windows if a host misbehaves. + +## Available Tests (manual integration) ### Vercel API Test diff --git a/scripts/bws-secure/tests/bws-env-utils.test.mjs b/scripts/bws-secure/tests/bws-env-utils.test.mjs new file mode 100644 index 00000000000..2db118c16b2 --- /dev/null +++ b/scripts/bws-secure/tests/bws-env-utils.test.mjs @@ -0,0 +1,55 @@ +/** + * Run: node --test tests/bws-env-utils.test.mjs + * Or: npm run test:bws-env + */ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { + parseProjectIds, + parseProjectIdsDetailed, + parseEnvironmentOutput, + serializeEnvRecordToPlaintext +} from '../bws-env-utils.js'; + +const u1 = '2d8c63de-62d5-4604-bb43-b357000e00b7'; +const u2 = '8686161e-6b41-4498-b7bf-b3390109f1d3'; + +test('parseProjectIds: single UUID', () => { + assert.deepEqual(parseProjectIds(u1), [u1]); +}); + +test('parseProjectIds: multi with spaces', () => { + assert.deepEqual(parseProjectIds(` ${u1} , ${u2} `), [u1, u2]); +}); + +test('parseProjectIdsDetailed: invalid segments skipped, order preserved', () => { + const d = parseProjectIdsDetailed(`not-a-uuid,${u1},bad,${u2}`); + assert.deepEqual(d.ids, [u1, u2]); + assert.deepEqual(d.skippedInvalid, ['not-a-uuid', 'bad']); + assert.equal(d.skippedDuplicates.length, 0); +}); + +test('parseProjectIdsDetailed: duplicate UUIDs (case-insensitive)', () => { + const d = parseProjectIdsDetailed(`${u1},${u1.toUpperCase()},${u2}`); + assert.deepEqual(d.ids, [u1, u2]); + assert.equal(d.skippedDuplicates.length, 1); +}); + +test('parseEnvironmentOutput + serializeEnvRecordToPlaintext: multiline round-trip', () => { + const record = { + A: 'line1\nline=with=equals\nline3', + B: 'single', + EMPTY: '' + }; + const text = serializeEnvRecordToPlaintext(record); + const back = parseEnvironmentOutput(text, { stripSerializedContinuationSpace: true }); + assert.equal(back.A, record.A); + assert.equal(back.B, record.B); + assert.equal(back.EMPTY, record.EMPTY); +}); + +test('overlay merge semantics via Object.assign (documentation)', () => { + const base = { X: '1', Y: '2' }; + const overlay = { Y: '9', Z: '3' }; + assert.deepEqual({ ...base, ...overlay }, { X: '1', Y: '9', Z: '3' }); +}); diff --git a/scripts/bws-secure/tests/e2e/env-sink.mjs b/scripts/bws-secure/tests/e2e/env-sink.mjs new file mode 100644 index 00000000000..9745c4b1be3 --- /dev/null +++ b/scripts/bws-secure/tests/e2e/env-sink.mjs @@ -0,0 +1,16 @@ +#!/usr/bin/env node +/** + * Writes the child process's env (serialized JSON) to the path at E2E_ENV_SINK. + * Used by E2E tests to observe what the wrapped command received. + */ +import fs from 'node:fs'; + +const sink = process.env.E2E_ENV_SINK; +if (!sink) { + process.exit(0); +} +const snapshot = {}; +for (const [k, v] of Object.entries(process.env)) { + snapshot[k] = v; +} +fs.writeFileSync(sink, JSON.stringify(snapshot)); diff --git a/scripts/bws-secure/tests/e2e/fake-bws.mjs b/scripts/bws-secure/tests/e2e/fake-bws.mjs new file mode 100644 index 00000000000..1aff66e6e74 --- /dev/null +++ b/scripts/bws-secure/tests/e2e/fake-bws.mjs @@ -0,0 +1,81 @@ +#!/usr/bin/env node +/** + * Fake BWS CLI for E2E tests. + * + * Reads a JSON fixture at FAKE_BWS_FIXTURE containing: + * { + * "projects": { "": [ { "key": "K", "value": "V" }, ... ], ... }, + * "globalSecrets": [ { "key": "K", "value": "V" }, ... ] // optional, for tokenless secret list + * } + * Returns canned data based on argv. + * + * Supported invocations (covers what secureRun / bws-dotenv use): + * bws project list -t TOKEN + * bws secret list -t TOKEN --output json -> globalSecrets or [] + * bws secret list -t TOKEN --output json -> projects[UUID] or exit 1 + * bws secret list -t TOKEN -o env -> env-format for projects[UUID] + * bws secret list -t TOKEN -o env -> env-format for globalSecrets + */ +import fs from 'node:fs'; + +const args = process.argv.slice(2); +const fixturePath = process.env.FAKE_BWS_FIXTURE; +const fixture = + fixturePath && fs.existsSync(fixturePath) + ? JSON.parse(fs.readFileSync(fixturePath, 'utf8')) + : { projects: {}, globalSecrets: [] }; + +function findUuidArg() { + const uuidRe = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + return args.find((a) => uuidRe.test(a)); +} + +function emitEnvFormat(secrets) { + const lines = secrets.map(({ key, value }) => `${key}=${value}`); + process.stdout.write(lines.join('\n')); +} + +function emitJson(data) { + process.stdout.write(JSON.stringify(data)); +} + +try { + if (args[0] === 'project' && args[1] === 'list') { + process.exit(0); + } + if (args[0] === 'secret' && args[1] === 'list') { + const uuid = findUuidArg(); + const wantsJson = args.includes('--output') && args[args.indexOf('--output') + 1] === 'json'; + const wantsEnv = args.includes('-o') && args[args.indexOf('-o') + 1] === 'env'; + + if (uuid) { + const secrets = fixture.projects ? fixture.projects[uuid] : undefined; + if (!secrets) { + process.stderr.write(`404 Not Found: ${uuid}`); + process.exit(1); + } + if (wantsJson) { + emitJson(secrets); + } else if (wantsEnv) { + emitEnvFormat(secrets); + } else { + emitJson(secrets); + } + process.exit(0); + } + const globals = fixture.globalSecrets || []; + if (wantsJson) { + emitJson(globals); + } else if (wantsEnv) { + emitEnvFormat(globals); + } else { + emitJson(globals); + } + process.exit(0); + } + process.stderr.write(`fake-bws: unsupported args: ${args.join(' ')}`); + process.exit(2); +} catch (err) { + process.stderr.write(`fake-bws error: ${err.message}`); + process.exit(1); +} diff --git a/scripts/bws-secure/tests/e2e/helpers.mjs b/scripts/bws-secure/tests/e2e/helpers.mjs new file mode 100644 index 00000000000..1bc38565dbd --- /dev/null +++ b/scripts/bws-secure/tests/e2e/helpers.mjs @@ -0,0 +1,159 @@ +/** + * Helpers for E2E tests that spawn the real secureRun.js in a simulated consumer repo. + * + * Not Windows-safe (relies on #!/usr/bin/env node shebangs + executable bits). + */ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +export const REPO_ROOT = path.resolve(__dirname, '..', '..'); +export const FAKE_BWS = path.join(__dirname, 'fake-bws.mjs'); +export const ENV_SINK = path.join(__dirname, 'env-sink.mjs'); + +/** + * Copy the bws-secure source into a temp consumer repo layout: + * /.env (BWS_ACCESS_TOKEN=fake-token) + * /bwsconfig.json (when config provided) + * /node_modules/.bin/bws (node shim over fake-bws) + * /scripts/bws-secure/ (copy of the repo excluding heavy/binary paths) + * /scripts/bws-secure/requiredVars.env (pre-created to skip the codebase scan) + */ +export function createConsumerRepo({ + config = null, + envFile = 'BWS_ACCESS_TOKEN=fake-token\n' +} = {}) { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bws-e2e-')); + const bwsDest = path.join(tmpDir, 'scripts', 'bws-secure'); + fs.mkdirSync(path.dirname(bwsDest), { recursive: true }); + + fs.cpSync(REPO_ROOT, bwsDest, { + recursive: true, + dereference: false, + filter: (src) => { + const rel = path.relative(REPO_ROOT, src); + if ( + rel.startsWith('node_modules') || + rel.startsWith('.git') || + rel.startsWith('bin/backup') + ) { + return false; + } + return true; + } + }); + + fs.writeFileSync(path.join(bwsDest, 'requiredVars.env'), '', 'utf8'); + + // Give the copied bws-secure access to the real node_modules (pnpm layout is tricky to copy). + const copyNodeModules = path.join(bwsDest, 'node_modules'); + if (!fs.existsSync(copyNodeModules)) { + fs.symlinkSync(path.join(REPO_ROOT, 'node_modules'), copyNodeModules, 'dir'); + } + + const nmBin = path.join(tmpDir, 'node_modules', '.bin'); + fs.mkdirSync(nmBin, { recursive: true }); + + if (process.platform === 'win32') { + // Windows: cmd.exe shim resolves via PATHEXT for explicit relative paths too. + const cmdContent = `@ECHO OFF\r\nnode "${FAKE_BWS}" %*\r\n`; + fs.writeFileSync(path.join(nmBin, 'bws.cmd'), cmdContent, 'utf8'); + // Placeholder so ensureBwsInstalled()'s existsSync check on `bws` passes. + fs.writeFileSync(path.join(nmBin, 'bws'), '', 'utf8'); + } else { + const fakeBwsUrl = new URL('file://' + FAKE_BWS).href; + const shim = `#!/usr/bin/env node\nawait import(${JSON.stringify(fakeBwsUrl)});\n`; + const shimPath = path.join(nmBin, 'bws'); + fs.writeFileSync(shimPath, shim, 'utf8'); + fs.chmodSync(shimPath, 0o755); + } + + fs.writeFileSync(path.join(tmpDir, '.env'), envFile, 'utf8'); + + if (config) { + fs.writeFileSync(path.join(tmpDir, 'bwsconfig.json'), JSON.stringify(config, null, 2), 'utf8'); + } + + return { tmpDir, bwsDest }; +} + +export function writeFixture(tmpDir, fixture) { + const p = path.join(tmpDir, '.fake-bws-fixture.json'); + fs.writeFileSync(p, JSON.stringify(fixture), 'utf8'); + return p; +} + +/** + * Run secureRun.js inside a consumer repo, wrapping `node `. + * @param {{ tmpDir: string, bwsDest: string }} repo + * @param {{ env?: Record, timeoutMs?: number }} [opts] + * @returns {{ status: number|null, stdout: string, stderr: string, childEnv: Record, leftoverSecureFiles: string[], tmpDir: string }} + */ +export function runSecureRun(repo, opts = {}) { + const sink = path.join(repo.tmpDir, 'e2e-child-env.json'); + if (fs.existsSync(sink)) fs.unlinkSync(sink); + const fixture = path.join(repo.tmpDir, '.fake-bws-fixture.json'); + + const pathSep = process.platform === 'win32' ? ';' : ':'; + const binPath = path.join(repo.tmpDir, 'node_modules', '.bin'); + const env = { + ...process.env, + PATH: `${binPath}${pathSep}${process.env.PATH || ''}`, + ...(process.platform === 'win32' && process.env.Path + ? { Path: `${binPath};${process.env.Path}` } + : {}), + FAKE_BWS_FIXTURE: fixture, + E2E_ENV_SINK: sink, + BWS_SUPPRESS_ALL: 'true', + BWS_NO_OVERRIDE: 'true', + NETLIFY: '', + VERCEL: '', + DEBUG: '', + ...opts.env + }; + + const res = spawnSync( + process.execPath, + [path.join(repo.bwsDest, 'secureRun.js'), process.execPath, ENV_SINK], + { + cwd: repo.tmpDir, + env, + encoding: 'utf8', + timeout: opts.timeoutMs || 30000 + } + ); + + let childEnv = {}; + if (fs.existsSync(sink)) { + try { + childEnv = JSON.parse(fs.readFileSync(sink, 'utf8')); + } catch { + childEnv = {}; + } + } + + const leftoverSecureFiles = fs + .readdirSync(repo.tmpDir) + .filter((f) => f === '.env.secure' || f.startsWith('.env.secure.')); + + return { + status: res.status, + stdout: res.stdout || '', + stderr: res.stderr || '', + childEnv, + leftoverSecureFiles, + tmpDir: repo.tmpDir + }; +} + +export function teardown(tmpDir) { + try { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } catch { + // ignore + } +} diff --git a/scripts/bws-secure/tests/e2e/secureRun.e2e.test.mjs b/scripts/bws-secure/tests/e2e/secureRun.e2e.test.mjs new file mode 100644 index 00000000000..42417134487 --- /dev/null +++ b/scripts/bws-secure/tests/e2e/secureRun.e2e.test.mjs @@ -0,0 +1,235 @@ +/** + * E2E tests for secureRun.js multi-project behavior. + * Run: npm run test:e2e + * + * Strategy: + * - Copy bws-secure into a tmp consumer-style repo + * - Shim `./node_modules/.bin/bws` to a fake CLI that returns canned secrets + * - Invoke secureRun.js wrapping a sink script that serializes process.env + * - Assert on the child env and on leftover .env.secure* files + * + * Cross-platform: Unix uses a #!/usr/bin/env node shebang shim; Windows uses a `bws.cmd` + * wrapper. Set E2E_DISABLE_WINDOWS=1 to skip the suite on Windows if a host misbehaves. + */ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import path from 'node:path'; +import fs from 'node:fs'; + +import { createConsumerRepo, runSecureRun, teardown, writeFixture } from './helpers.mjs'; + +// E2E_DISABLE_WINDOWS=1 will skip on Windows if the cmd-shim path misbehaves on a given host. +const skipIfWindows = + process.platform === 'win32' && process.env.E2E_DISABLE_WINDOWS === '1' + ? { skip: 'E2E disabled on Windows via E2E_DISABLE_WINDOWS=1' } + : {}; + +const U1 = '11111111-1111-4111-8111-111111111111'; +const U2 = '22222222-2222-4222-8222-222222222222'; +const U3 = '33333333-3333-4333-8333-333333333333'; +const INVALID = 'not-a-uuid'; + +const baseSecrets = (overrides = {}) => ({ + BWS_TEST_VAR: overrides.BWS_TEST_VAR || 'present', + ...overrides +}); + +function toBwsSecrets(record) { + return Object.entries(record).map(([key, value]) => ({ key, value: String(value) })); +} + +test( + 'single UUID direct bypass: child env has secrets and BWS_PROJECT_ID=uuid', + skipIfWindows, + () => { + const repo = createConsumerRepo(); + writeFixture(repo.tmpDir, { + projects: { + [U1]: toBwsSecrets(baseSecrets({ API_KEY: 'single-key', DATABASE_URL: 'single-db' })) + } + }); + try { + const r = runSecureRun(repo, { env: { BWS_PROJECT_ID: U1 } }); + assert.equal(r.status, 0, `secureRun exit ${r.status}. stderr:\n${r.stderr}`); + assert.equal(r.childEnv.API_KEY, 'single-key'); + assert.equal(r.childEnv.DATABASE_URL, 'single-db'); + assert.equal(r.childEnv.BWS_TEST_VAR, 'present'); + assert.equal(r.childEnv.BWS_PROJECT_ID, U1); + assert.ok( + !('BWS_PROJECT_IDS' in r.childEnv), + 'BWS_PROJECT_IDS should be absent for single UUID' + ); + } finally { + teardown(repo.tmpDir); + } + } +); + +test( + 'multi UUID direct bypass: overlay order (later wins) + BWS_PROJECT_IDS set', + skipIfWindows, + () => { + const repo = createConsumerRepo(); + writeFixture(repo.tmpDir, { + projects: { + [U1]: toBwsSecrets( + baseSecrets({ API_KEY: 'shared', DATABASE_URL: 'db1', FROM_1: 'only1' }) + ), + [U2]: toBwsSecrets(baseSecrets({ DATABASE_URL: 'db2-overrides', FROM_2: 'only2' })) + } + }); + try { + const r = runSecureRun(repo, { env: { BWS_PROJECT_ID: `${U1}, ${U2}` } }); + assert.equal(r.status, 0, `secureRun exit ${r.status}. stderr:\n${r.stderr}`); + assert.equal(r.childEnv.API_KEY, 'shared', 'non-overlapping key from project 1 should load'); + assert.equal(r.childEnv.DATABASE_URL, 'db2-overrides', 'later project should win overlay'); + assert.equal(r.childEnv.FROM_1, 'only1'); + assert.equal(r.childEnv.FROM_2, 'only2'); + assert.equal( + r.childEnv.BWS_PROJECT_ID, + U1, + 'child BWS_PROJECT_ID should be the primary (first) UUID' + ); + assert.equal( + r.childEnv.BWS_PROJECT_IDS, + `${U1},${U2}`, + 'BWS_PROJECT_IDS should list all loaded UUIDs in order' + ); + } finally { + teardown(repo.tmpDir); + } + } +); + +test('invalid UUID segments are skipped, valid ones still load', skipIfWindows, () => { + const repo = createConsumerRepo(); + writeFixture(repo.tmpDir, { + projects: { + [U1]: toBwsSecrets(baseSecrets({ ONLY_VALID: 'yes' })) + } + }); + try { + const r = runSecureRun(repo, { env: { BWS_PROJECT_ID: `${INVALID}, ${U1}` } }); + assert.equal(r.status, 0, `secureRun exit ${r.status}. stderr:\n${r.stderr}`); + assert.equal(r.childEnv.ONLY_VALID, 'yes'); + assert.equal(r.childEnv.BWS_PROJECT_ID, U1); + } finally { + teardown(repo.tmpDir); + } +}); + +test('duplicate UUIDs (case-insensitive) are deduped, order preserved', skipIfWindows, () => { + const repo = createConsumerRepo(); + writeFixture(repo.tmpDir, { + projects: { + [U1]: toBwsSecrets(baseSecrets({ A: '1' })), + [U2]: toBwsSecrets(baseSecrets({ B: '2' })) + } + }); + try { + const r = runSecureRun(repo, { + env: { BWS_PROJECT_ID: `${U1}, ${U1.toUpperCase()}, ${U2}` } + }); + assert.equal(r.status, 0, `secureRun exit ${r.status}. stderr:\n${r.stderr}`); + assert.equal(r.childEnv.A, '1'); + assert.equal(r.childEnv.B, '2'); + assert.equal( + r.childEnv.BWS_PROJECT_IDS, + `${U1},${U2}`, + 'duplicates should not appear in BWS_PROJECT_IDS' + ); + } finally { + teardown(repo.tmpDir); + } +}); + +test('cleanup: .env.secure* files are removed after a successful run', skipIfWindows, () => { + const repo = createConsumerRepo(); + writeFixture(repo.tmpDir, { + projects: { + [U1]: toBwsSecrets(baseSecrets({ A: '1' })), + [U2]: toBwsSecrets(baseSecrets({ B: '2' })) + } + }); + try { + const r = runSecureRun(repo, { env: { BWS_PROJECT_ID: `${U1}, ${U2}` } }); + assert.equal(r.status, 0, `secureRun exit ${r.status}. stderr:\n${r.stderr}`); + assert.deepEqual( + r.leftoverSecureFiles, + [], + `expected no .env.secure* leftover, found: ${r.leftoverSecureFiles.join(', ')}` + ); + } finally { + teardown(repo.tmpDir); + } +}); + +test('BWS_KEEP_SECURE_FILES=true preserves .env.secure* after a run', skipIfWindows, () => { + const repo = createConsumerRepo(); + writeFixture(repo.tmpDir, { + projects: { + [U1]: toBwsSecrets(baseSecrets({ A: '1' })), + [U2]: toBwsSecrets(baseSecrets({ B: '2' })) + } + }); + try { + const r = runSecureRun(repo, { + env: { BWS_PROJECT_ID: `${U1}, ${U2}`, BWS_KEEP_SECURE_FILES: 'true' } + }); + assert.equal(r.status, 0, `secureRun exit ${r.status}. stderr:\n${r.stderr}`); + assert.ok(r.leftoverSecureFiles.length > 0, 'expected .env.secure* files preserved'); + assert.ok( + r.leftoverSecureFiles.some((f) => f === `.env.secure.${U1}`), + `expected .env.secure.${U1}, got ${r.leftoverSecureFiles.join(', ')}` + ); + } finally { + teardown(repo.tmpDir); + } +}); + +test( + 'fail-fast: one unknown UUID in multi + BWS_MULTI_PROJECT_FAIL_FAST=true exits non-zero', + skipIfWindows, + () => { + const repo = createConsumerRepo(); + writeFixture(repo.tmpDir, { + projects: { + [U1]: toBwsSecrets(baseSecrets({ A: '1' })) + // U3 deliberately missing → fake-bws returns 404 + } + }); + try { + const r = runSecureRun(repo, { + env: { + BWS_PROJECT_ID: `${U1}, ${U3}`, + BWS_MULTI_PROJECT_FAIL_FAST: 'true' + } + }); + assert.notEqual(r.status, 0, `expected non-zero exit, got ${r.status}. stderr:\n${r.stderr}`); + } finally { + teardown(repo.tmpDir); + } + } +); + +test('multiline secret round-trips across overlay merge', skipIfWindows, () => { + const multiline = '-----BEGIN KEY-----\nabc=def=ghi==\nline3\n-----END KEY-----'; + const repo = createConsumerRepo(); + writeFixture(repo.tmpDir, { + projects: { + [U1]: toBwsSecrets(baseSecrets({ PRIVATE_KEY: 'will-be-overridden' })), + [U2]: toBwsSecrets(baseSecrets({ PRIVATE_KEY: multiline })) + } + }); + try { + const r = runSecureRun(repo, { env: { BWS_PROJECT_ID: `${U1}, ${U2}` } }); + assert.equal(r.status, 0, `secureRun exit ${r.status}. stderr:\n${r.stderr}`); + assert.equal( + r.childEnv.PRIVATE_KEY, + multiline, + 'multiline value should round-trip intact through merge' + ); + } finally { + teardown(repo.tmpDir); + } +}); diff --git a/scripts/bws-secure/update-environments/map-env-files.js b/scripts/bws-secure/update-environments/map-env-files.js index 616fdb70f6a..717876b40f0 100644 --- a/scripts/bws-secure/update-environments/map-env-files.js +++ b/scripts/bws-secure/update-environments/map-env-files.js @@ -23,6 +23,7 @@ import { hideBin } from 'yargs/helpers'; import dotenv from 'dotenv'; import crypto from 'node:crypto'; import { log } from './utils.js'; +import { parseProjectIds } from '../bws-env-utils.js'; // Get the directory name in ESM const __filename = fileURLToPath(import.meta.url); @@ -107,18 +108,22 @@ async function mapEnvironmentFiles() { const currentProject = config.projects.find((p) => p.projectName === process.env.BWS_PROJECT); if (!currentProject) { // Handle direct BWS_PROJECT_ID usage for debug display only - if ( - process.env.BWS_PROJECT_ID && - process.env.BWS_PROJECT_ID.match( - /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i - ) && - process.env.DEBUG === 'true' && - process.env.SHOW_DECRYPTED === 'true' && - process.env.BWS_EPHEMERAL_KEY - ) { - const sourceFile = `.env.secure.${process.env.BWS_PROJECT_ID}`; - if (fs.existsSync(sourceFile)) { - displayDecryptedContent(sourceFile, process.env.BWS_EPHEMERAL_KEY); + if (process.env.BWS_PROJECT_ID) { + const projectIds = parseProjectIds(process.env.BWS_PROJECT_ID); + + if ( + projectIds.length > 0 && + process.env.DEBUG === 'true' && + process.env.SHOW_DECRYPTED === 'true' && + process.env.BWS_EPHEMERAL_KEY + ) { + // For multi-project IDs, display each project's secrets + for (const projectId of projectIds) { + const sourceFile = `.env.secure.${projectId}`; + if (fs.existsSync(sourceFile)) { + displayDecryptedContent(sourceFile, process.env.BWS_EPHEMERAL_KEY); + } + } } } log('warn', `Project ${process.env.BWS_PROJECT} not found in config`); @@ -126,10 +131,31 @@ async function mapEnvironmentFiles() { } const env = process.env.BWS_ENV || 'local'; - const projectId = currentProject.bwsProjectIds[env]; - const sourceFile = `.env.secure.${projectId}`; + const projectIdString = currentProject.bwsProjectIds[env]; + const projectIds = parseProjectIds(projectIdString); + + // Determine source file based on single vs multi-project setup + let sourceFile; const target = `.env.secure.${currentProject.projectName}.${env}`; + // For multi-project IDs, use the merged environment file created by secureRun.js + // For single project ID, use the direct project file + if (projectIds.length > 1) { + // Multi-project: Use merged environment file + sourceFile = `.env.secure.${env}`; + log( + 'debug', + `Using merged environment file for ${projectIds.length} projects: ${sourceFile}` + ); + } else if (projectIds.length === 1) { + // Single project: Use direct project file (backward compatible) + sourceFile = `.env.secure.${projectIds[0]}`; + log('debug', `Using single project file: ${sourceFile}`); + } else { + log('warn', `No valid project IDs found for environment ${env}`); + return; + } + // Create symlink for current environment if (fs.existsSync(sourceFile)) { createSymlink(sourceFile, target); @@ -142,12 +168,21 @@ async function mapEnvironmentFiles() { // Create symlinks for other environments (needed for platform deployments) if (currentProject) { - Object.entries(currentProject.bwsProjectIds).forEach(([envName, id]) => { + Object.entries(currentProject.bwsProjectIds).forEach(([envName, projectIdString]) => { const env = process.env.BWS_ENV || 'local'; if (envName !== env) { - const otherSource = `.env.secure.${id}`; + const envProjectIds = parseProjectIds(projectIdString); + let otherSource; + + // Use merged file for multi-project, direct file for single project + if (envProjectIds.length > 1) { + otherSource = `.env.secure.${envName}`; + } else if (envProjectIds.length === 1) { + otherSource = `.env.secure.${envProjectIds[0]}`; + } + const otherTarget = `.env.secure.${currentProject.projectName}.${envName}`; - if (fs.existsSync(otherSource)) { + if (otherSource && fs.existsSync(otherSource)) { createSymlink(otherSource, otherTarget); } } diff --git a/scripts/bws-secure/update-environments/netlify.js b/scripts/bws-secure/update-environments/netlify.js index e4e3b5586fd..f5cd678902e 100755 --- a/scripts/bws-secure/update-environments/netlify.js +++ b/scripts/bws-secure/update-environments/netlify.js @@ -31,6 +31,23 @@ const __dirname = path.dirname(__filename); // Local in-memory cache for Netlify environment variables (if needed for caching) const netlifyEnvironmentCache = new Map(); +/** + * Pro+ only: per-scope env vars (Netlify OpenAPI: granular scopes require Pro and above). + * Free accounts must omit `scopes` so Netlify applies "All scopes" (same as UI default). + * @see https://open-api.netlify.com/ — createEnvVars + */ +const NETLIFY_ENV_VAR_SCOPES_GRANULAR = ['builds', 'functions', 'runtime']; + +function netlifyEnvVarsWithoutScopes(variablesArray) { + return variablesArray.map(({ scopes: _omit, ...rest }) => rest); +} + +/** HTTP statuses where retrying without granular scopes may fix Free-tier / plan limits */ +function shouldRetryNetlifyEnvBatchWithoutScopes(error) { + const status = error.response?.status; + return status === 400 || status === 403 || status === 422; +} + /** * Helper function to handle API rate limiting with exponential backoff * @@ -209,7 +226,7 @@ async function updateNetlifyEnvironmentVariables(project) { // For now, we are setting is_secret to false for all variables. variablesToUpdate.push({ key, - scopes: ['builds', 'functions', 'runtime'], + scopes: NETLIFY_ENV_VAR_SCOPES_GRANULAR, values: contexts, is_secret: false }); @@ -259,20 +276,17 @@ async function updateNetlifyEnvironmentVariables(project) { } /** - * batchUpdateNetlifyEnvVars performs a single API call to update/create multiple environment variables. - * For large batches, it splits them into smaller chunks to avoid rate limiting. + * batchUpdateNetlifyEnvVars POSTs createEnvVars payloads. Large batches are chunked. + * On 400/403/422 with granular `scopes`, retries once with `scopes` omitted (Free tier "All scopes"). */ async function batchUpdateNetlifyEnvironmentVariables(site, netlifyToken, variablesArray) { - try { - const url = `https://api.netlify.com/api/v1/accounts/${site.account_id}/env`; - - // Split into smaller batches if the array is large - const maxBatchSize = 20; // Maximum number of variables to update in a single API call + const url = `https://api.netlify.com/api/v1/accounts/${site.account_id}/env`; + const maxBatchSize = 20; - if (variablesArray.length <= maxBatchSize) { - // Small enough batch, process normally + async function postBatches(payload) { + if (payload.length <= maxBatchSize) { await withRateLimitRetry(async () => { - await axios.post(url, variablesArray, { + await axios.post(url, payload, { headers: { 'Content-Type': 'application/json', 'Authorization': netlifyToken @@ -280,50 +294,67 @@ async function batchUpdateNetlifyEnvironmentVariables(site, netlifyToken, variab params: { site_id: site.id } }); }); + log('debug', `Batch updated ${payload.length} environment variables.`); + return; + } + + log( + 'debug', + `Splitting large batch of ${payload.length} variables into smaller chunks of ${maxBatchSize}` + ); - log('debug', `Batch updated ${variablesArray.length} environment variables.`); - } else { - // Large batch, split into chunks + for (let i = 0; i < payload.length; i += maxBatchSize) { + const chunk = payload.slice(i, i + maxBatchSize); log( 'debug', - `Splitting large batch of ${variablesArray.length} variables into smaller chunks of ${maxBatchSize}` + `Processing update chunk ${Math.floor(i / maxBatchSize) + 1}/${Math.ceil( + payload.length / maxBatchSize + )}` ); - for (let i = 0; i < variablesArray.length; i += maxBatchSize) { - const chunk = variablesArray.slice(i, i + maxBatchSize); - log( - 'debug', - `Processing update chunk ${Math.floor(i / maxBatchSize) + 1}/${Math.ceil( - variablesArray.length / maxBatchSize - )}` - ); - - await withRateLimitRetry(async () => { - await axios.post(url, chunk, { - headers: { - 'Content-Type': 'application/json', - 'Authorization': netlifyToken - }, - params: { site_id: site.id } - }); + await withRateLimitRetry(async () => { + await axios.post(url, chunk, { + headers: { + 'Content-Type': 'application/json', + 'Authorization': netlifyToken + }, + params: { site_id: site.id } }); + }); - // Add a delay between chunks to avoid rate limiting - if (i + maxBatchSize < variablesArray.length) { - log('debug', 'Adding delay between update batches to avoid rate limiting'); - await new Promise((resolve) => setTimeout(resolve, 1000)); - } + if (i + maxBatchSize < payload.length) { + log('debug', 'Adding delay between update batches to avoid rate limiting'); + await new Promise((resolve) => setTimeout(resolve, 1000)); } + } + log('debug', `Completed update of all ${payload.length} environment variables in chunks.`); + } + + const hadGranularScopes = variablesArray.some( + (v) => Array.isArray(v.scopes) && v.scopes.length > 0 + ); + + try { + await postBatches(variablesArray); + } catch (error) { + if (hadGranularScopes && shouldRetryNetlifyEnvBatchWithoutScopes(error)) { log( - 'debug', - `Completed update of all ${variablesArray.length} environment variables in chunks.` + 'warn', + 'Netlify rejected granular env `scopes` (often Free tier / plan limits). Retrying without `scopes` (all scopes).' ); + try { + await postBatches(netlifyEnvVarsWithoutScopes(variablesArray)); + return; + } catch (retryError) { + log('error', `Batch update failed: ${retryError.message}`); + log('error', `Critical Error: Failed to update Netlify environment variables`); + process.exit(1); + } } - } catch (error) { log('error', `Batch update failed: ${error.message}`); log('error', `Critical Error: Failed to update Netlify environment variables`); - process.exit(1); // Immediately exit with error code + process.exit(1); } } From 3e166d604d5c70eb82660edfee135cb6874b6b15 Mon Sep 17 00:00:00 2001 From: Cameron Taylor <50385537+ct3685@users.noreply.github.com> Date: Fri, 15 May 2026 13:10:56 -0400 Subject: [PATCH 3/7] fix: preserve workspaceId in bulkUpdateChatflows + optional template name sync (#1071) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - **Bug fix**: `bulkUpdateChatflows` was spreading the admin template entity without overriding `workspaceId`, causing every bulk template push to overwrite each user's chatflow workspace with the template owner's workspace ('Default Workspace'). This is the root cause of the repeated prod SQL hotfixes. - **Enhancement**: Adds an optional `updateName` flag so admins can choose to propagate the template name to all user copies during a bulk push (off by default — no behaviour change without opt-in). - **Data migration**: `1770000000002-FixBulkUpdateChatflowWorkspace` idempotently repairs chatflows already corrupted by previous bulk updates across all orgs (generalises the prod SQL fix, registered in `postgresMigrations`). ## Changes | File | Change | |------|--------| | `packages/server/src/services/chatflows/index.ts` | Add `workspaceId: targetChatflow.workspaceId` override + `options.updateName` conditional name logic | | `packages/server/src/controllers/chatflows/index.ts` | Extract and pass `options` from request body | | `packages/ui/src/api/chatflows.js` | Accept and forward `options` in PUT body | | `packages-answers/ui/src/Admin/Chatflows/index.tsx` | Replace raw button click with confirmation dialog + "Also update chatflow name" checkbox | | `packages/server/src/database/migrations/postgres/aai/1770000000002-FixBulkUpdateChatflowWorkspace.ts` | New idempotent data migration | | `packages/server/src/database/migrations/postgres/index.ts` | Import + register new migration | ## Root Cause ```typescript // Before — templateChatflow.workspaceId leaked onto every user's copy const updatedChatflow = { ...templateChatflow, // workspaceId from admin workspace leaked in id: targetChatflow.id, userId: targetChatflow.userId, // workspaceId NOT overridden } // After — workspace always preserved from the target const updatedChatflow = { ...templateChatflow, id: targetChatflow.id, userId: targetChatflow.userId, workspaceId: targetChatflow.workspaceId, // fixed name: options?.updateName ? templateChatflow.name : targetChatflow.name, } ``` ## Test Plan - [ ] Run migration on staging DB — chatflows in Default Workspace with `parentChatflowId` move to Personal Workspace - [ ] Trigger bulk update via Admin Chatflows UI — user chatflows stay in their Personal Workspace after push - [ ] Confirm "Update Selected" now opens a confirmation dialog before acting - [ ] Check "Also update chatflow name" — names synced to template post-update - [ ] Leave checkbox unchecked — names preserved as-is --- .../ui/src/Admin/Chatflows/index.tsx | 87 +++++++++++++++---- .../server/src/controllers/chatflows/index.ts | 4 +- ...00000002-FixBulkUpdateChatflowWorkspace.ts | 48 ++++++++++ .../src/database/migrations/postgres/index.ts | 5 +- .../server/src/services/chatflows/index.ts | 10 ++- packages/ui/src/api/chatflows.js | 2 +- 6 files changed, 133 insertions(+), 23 deletions(-) create mode 100644 packages/server/src/database/migrations/postgres/aai/1770000000002-FixBulkUpdateChatflowWorkspace.ts diff --git a/packages-answers/ui/src/Admin/Chatflows/index.tsx b/packages-answers/ui/src/Admin/Chatflows/index.tsx index 962958b67dd..2a2ec4bb80c 100644 --- a/packages-answers/ui/src/Admin/Chatflows/index.tsx +++ b/packages-answers/ui/src/Admin/Chatflows/index.tsx @@ -88,6 +88,10 @@ const AdminChatflows = () => { const [templateStatusFilter, setTemplateStatusFilter] = useState([]) const [selectedForUpdate, setSelectedForUpdate] = useState([]) + // Bulk update confirmation dialog state + const [bulkUpdateDialogOpen, setBulkUpdateDialogOpen] = useState(false) + const [bulkUpdateIncludeName, setBulkUpdateIncludeName] = useState(false) + // Versioning state const [versionModalOpen, setVersionModalOpen] = useState(false) const [selectedChatflowForVersions, setSelectedChatflowForVersions] = useState('') @@ -1027,22 +1031,9 @@ const AdminChatflows = () => { variant='contained' size='small' disabled={selectedForUpdate.length === 0} - onClick={async () => { - try { - const response = await chatflowsApi.bulkUpdateChatflows(selectedForUpdate) - - // Show success message and refresh data - if (response.updated > 0) { - // Refresh the chatflows data - window.location.reload() // Simple refresh for now - } - - // Clear selections - setSelectedForUpdate([]) - } catch (error) { - console.error('Bulk update failed:', error) - // TODO: Show error message to user - } + onClick={() => { + setBulkUpdateIncludeName(false) + setBulkUpdateDialogOpen(true) }} sx={{ bgcolor: alpha(theme.palette.warning.main, 0.8), @@ -1061,6 +1052,66 @@ const AdminChatflows = () => { + {/* Bulk update confirmation dialog */} + setBulkUpdateDialogOpen(false)} maxWidth='sm' fullWidth> + Confirm Template Update + + + You are about to push the latest template to {selectedForUpdate.length} chatflow + {selectedForUpdate.length !== 1 ? 's' : ''}. This will overwrite their flow configuration with the + current template version. + + setBulkUpdateIncludeName(e.target.checked)} + color='warning' + /> + } + label={ + + + Also update chatflow name to match template + + + Overwrites each user's chatflow name with the template name + + + } + /> + + + + + + + {selectedForUpdate.length > 0 && ( @@ -1071,7 +1122,9 @@ const AdminChatflows = () => {
• API settings, starter prompts, and system configuration will be updated
- • Name, description, owner, and organization will remain unchanged + • Description, owner, workspace, and organization will remain unchanged +
+ • Chatflow name is preserved unless "Also update name" is checked
• User customizations in flow logic may be overwritten
diff --git a/packages/server/src/controllers/chatflows/index.ts b/packages/server/src/controllers/chatflows/index.ts index 654f403c88b..66e7609d78a 100644 --- a/packages/server/src/controllers/chatflows/index.ts +++ b/packages/server/src/controllers/chatflows/index.ts @@ -291,12 +291,12 @@ const getDefaultChatflowTemplate = async (req: Request, res: Response, next: Nex const bulkUpdateChatflows = async (req: Request, res: Response, next: NextFunction) => { try { - const { chatflowIds } = req.body + const { chatflowIds, options } = req.body if (!Array.isArray(chatflowIds) || chatflowIds.length === 0) { return res.status(400).json({ error: 'chatflowIds must be a non-empty array' }) } - const apiResponse = await chatflowsService.bulkUpdateChatflows(chatflowIds, req.user!) + const apiResponse = await chatflowsService.bulkUpdateChatflows(chatflowIds, req.user!, options) return res.json(apiResponse) } catch (error) { next(error) diff --git a/packages/server/src/database/migrations/postgres/aai/1770000000002-FixBulkUpdateChatflowWorkspace.ts b/packages/server/src/database/migrations/postgres/aai/1770000000002-FixBulkUpdateChatflowWorkspace.ts new file mode 100644 index 00000000000..76d0a796ef2 --- /dev/null +++ b/packages/server/src/database/migrations/postgres/aai/1770000000002-FixBulkUpdateChatflowWorkspace.ts @@ -0,0 +1,48 @@ +/* eslint-disable no-console */ +import { MigrationInterface, QueryRunner } from 'typeorm' + +/** + * Fix chatflows whose workspaceId was overwritten by bulkUpdateChatflows. + * + * Root cause: bulkUpdateChatflows spread the admin template entity (including its + * workspaceId) onto each user's chatflow copy without overriding workspaceId. + * This caused all chatflows updated via the enterprise "push template" feature to + * land in the admin's workspace (typically 'Default Workspace') instead of each + * user's own 'Personal Workspace'. + * + * Migration 1770000000000 fixed the initial seeding case. This migration fixes + * the same class of breakage for chatflows re-corrupted by subsequent bulk updates + * after that migration ran. + * + * Idempotent: only moves chatflows that are currently in a 'Default Workspace'. + */ +export class FixBulkUpdateChatflowWorkspace1770000000002 implements MigrationInterface { + name = 'FixBulkUpdateChatflowWorkspace1770000000002' + + public async up(queryRunner: QueryRunner): Promise { + console.log('[FixBulkUpdateChatflowWorkspace] Starting: re-assign template chatflows to Personal Workspaces') + + const result = await queryRunner.query(` + UPDATE chat_flow cf + SET "workspaceId" = pw.id, + "updatedDate" = NOW() + FROM workspace pw, workspace cur + WHERE cur.id = cf."workspaceId" + AND pw."organizationId" = cf."organizationId" + AND pw.name = 'Personal Workspace' + AND pw."createdBy" = cf."userId" + AND cf."deletedDate" IS NULL + AND cf."parentChatflowId" IS NOT NULL + AND cur.name = 'Default Workspace' + `) + + const count = Array.isArray(result) ? result[1] ?? 0 : 0 + console.log(`[FixBulkUpdateChatflowWorkspace] Moved ${count} chatflows back to Personal Workspaces`) + console.log('[FixBulkUpdateChatflowWorkspace] Done') + } + + public async down(): Promise { + // Not reversible — chatflows should stay in Personal Workspaces + console.log('[FixBulkUpdateChatflowWorkspace] Down migration is a no-op') + } +} diff --git a/packages/server/src/database/migrations/postgres/index.ts b/packages/server/src/database/migrations/postgres/index.ts index e832e5bc16c..5b505f543bd 100644 --- a/packages/server/src/database/migrations/postgres/index.ts +++ b/packages/server/src/database/migrations/postgres/index.ts @@ -97,6 +97,7 @@ import { AddGuardrailsMetadataToChatMessage1753200000002 } from './aai/175320000 import { UpdateFiddlerCredentialsVisibility1768413137117 } from './aai/1768413137117-UpdateFiddlerCredentialsVisibility' import { MoveDefaultChatflowsToPersonalWorkspace1770000000000 } from './aai/1770000000000-MoveDefaultChatflowsToPersonalWorkspace' import { NormalizeLegacyCredentialNames1770000000001 } from './aai/1770000000001-NormalizeLegacyCredentialNames' +import { FixBulkUpdateChatflowWorkspace1770000000002 } from './aai/1770000000002-FixBulkUpdateChatflowWorkspace' export const postgresMigrations = [ Init1693891895163, @@ -196,5 +197,7 @@ export const postgresMigrations = [ // AAI: AGENT-674 - Move default sidekick chatflows to Personal Workspaces MoveDefaultChatflowsToPersonalWorkspace1770000000000, // AAI: Normalize legacy credential names in chat_flow.flowData and credential rows (e.g. JiraApi -> jiraApi) - NormalizeLegacyCredentialNames1770000000001 + NormalizeLegacyCredentialNames1770000000001, + // AAI: Fix chatflows re-assigned to Default Workspace by bulkUpdateChatflows workspace leak + FixBulkUpdateChatflowWorkspace1770000000002 ] diff --git a/packages/server/src/services/chatflows/index.ts b/packages/server/src/services/chatflows/index.ts index da99dbdda9f..712a394c13b 100644 --- a/packages/server/src/services/chatflows/index.ts +++ b/packages/server/src/services/chatflows/index.ts @@ -608,7 +608,11 @@ const getDefaultChatflowTemplate = async (): Promise<{ id: string; name: string } } -const bulkUpdateChatflows = async (chatflowIds: string[], user: IUser): Promise<{ updated: number; errors: string[] }> => { +const bulkUpdateChatflows = async ( + chatflowIds: string[], + user: IUser, + options?: { updateName?: boolean } +): Promise<{ updated: number; errors: string[] }> => { try { const appServer = getRunningExpressApp() const { id: _userId, organizationId } = user @@ -657,10 +661,12 @@ const bulkUpdateChatflows = async (chatflowIds: string[], user: IUser): Promise< const updatedChatflow = { ...templateChatflow, id: targetChatflow.id, - name: targetChatflow.name, // Preserve original name + // Only propagate template name when explicitly requested by admin + name: options?.updateName ? templateChatflow.name : targetChatflow.name, description: targetChatflow.description, // Preserve original description userId: targetChatflow.userId, // Preserve original owner organizationId: targetChatflow.organizationId, // Preserve original organization + workspaceId: targetChatflow.workspaceId, // Preserve original workspace (bug fix: was leaking template's workspaceId) parentChatflowId: targetChatflow.parentChatflowId, // Preserve parent relationship createdDate: targetChatflow.createdDate, // Preserve creation date currentVersion: (targetChatflow.currentVersion || 1) + 1, // Increment version diff --git a/packages/ui/src/api/chatflows.js b/packages/ui/src/api/chatflows.js index 497eb3f68fd..974fb6449b3 100644 --- a/packages/ui/src/api/chatflows.js +++ b/packages/ui/src/api/chatflows.js @@ -34,7 +34,7 @@ const getAdminChatflows = (filter, type = 'CHATFLOW') => { const getDefaultChatflowTemplate = () => client.get('/admin/chatflows/default-template') -const bulkUpdateChatflows = (chatflowIds) => client.put('/admin/chatflows/bulk-update', { chatflowIds }) +const bulkUpdateChatflows = (chatflowIds, options) => client.put('/admin/chatflows/bulk-update', { chatflowIds, options }) // Versioning API methods const getChatflowVersions = (id) => client.get(`/admin/chatflows/${id}/versions`) From 5ca478d30f94d06df92d3b8eb706e4bc2a01e6b1 Mon Sep 17 00:00:00 2001 From: Cameron Taylor <50385537+ct3685@users.noreply.github.com> Date: Fri, 15 May 2026 14:56:20 -0400 Subject: [PATCH 4/7] fix: reload page after bulk chatflow update and show in-progress state (#1072) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Patch for the Admin Chatflows bulk update UX: 1. **Page didn't reload after update** — The previous code gated `window.location.reload()` on `response.updated > 0`, but the axios client wraps the body in `.data`, so `response.updated` was always `undefined` and the condition never fired. Fixed by removing the condition and always reloading on success. 2. **No in-progress feedback** — The button showed no indication the update was running. Added `bulkUpdateInProgress` state that disables both the trigger button and the dialog confirm button, and changes their labels to `Updating N…` while the request is in-flight. On error, the state resets so the admin can retry. ## Changes `packages-answers/ui/src/Admin/Chatflows/index.tsx` only — no server changes. ## Test Plan - [ ] Click "Update Selected", confirm dialog, verify button switches to "Updating N…" and is disabled - [ ] After update completes, verify page reloads and outdated badges are gone - [ ] Simulate a network error and verify the button re-enables for retry --- .../ui/src/Admin/Chatflows/index.tsx | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/packages-answers/ui/src/Admin/Chatflows/index.tsx b/packages-answers/ui/src/Admin/Chatflows/index.tsx index 2a2ec4bb80c..b30cea48632 100644 --- a/packages-answers/ui/src/Admin/Chatflows/index.tsx +++ b/packages-answers/ui/src/Admin/Chatflows/index.tsx @@ -91,6 +91,7 @@ const AdminChatflows = () => { // Bulk update confirmation dialog state const [bulkUpdateDialogOpen, setBulkUpdateDialogOpen] = useState(false) const [bulkUpdateIncludeName, setBulkUpdateIncludeName] = useState(false) + const [bulkUpdateInProgress, setBulkUpdateInProgress] = useState(false) // Versioning state const [versionModalOpen, setVersionModalOpen] = useState(false) @@ -1030,7 +1031,7 @@ const AdminChatflows = () => { @@ -1087,18 +1090,18 @@ const AdminChatflows = () => { From 7314af580be26d7ce38abbc38aa0cb725921d7a6 Mon Sep 17 00:00:00 2001 From: Cameron Taylor <50385537+ct3685@users.noreply.github.com> Date: Fri, 15 May 2026 15:22:25 -0400 Subject: [PATCH 5/7] feat: turn template banner green when all chatflows are up to date (#1073) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary The Organization Default Template banner now reflects the actual sync state of the org: - **Any chatflows outdated** → amber/yellow (existing look, unchanged) - **All chatflows current** → green ## How it works A `hasOutdated` boolean is derived inline from `chatflowsData` (already in scope): ```ts const hasOutdated = chatflowsData.some((cf) => cf.templateStatus === 'outdated') ``` A `bc` (banner colors) palette object is keyed on that boolean, providing green vs amber values for every color token used in the banner: background, border, text, muted text, icon colors, hover states, chips, and the DEFAULT TEMPLATE badge. All hardcoded `isDarkMode ? 'rgba(255, 193, 7, ...)' : '#...'` strings inside the banner block are replaced with `bc.*` references. Nothing outside the banner block is touched. ## Test Plan - [ ] With at least one outdated chatflow → banner stays amber - [ ] After pushing updates so all are current → banner turns green - [ ] Works in both light and dark mode --- .../ui/src/Admin/Chatflows/index.tsx | 112 +++++++++++------- 1 file changed, 71 insertions(+), 41 deletions(-) diff --git a/packages-answers/ui/src/Admin/Chatflows/index.tsx b/packages-answers/ui/src/Admin/Chatflows/index.tsx index b30cea48632..aff5d85e52b 100644 --- a/packages-answers/ui/src/Admin/Chatflows/index.tsx +++ b/packages-answers/ui/src/Admin/Chatflows/index.tsx @@ -463,26 +463,58 @@ const AdminChatflows = () => { const fullDefaultTemplate = chatflowsData.find((chatflow: any) => chatflow.id === defaultTemplateData.id) if (!fullDefaultTemplate) return false + const hasOutdated = chatflowsData.some((cf: any) => cf.templateStatus === 'outdated') + const bc = hasOutdated + ? { + bg: isDarkMode ? 'rgba(255, 193, 7, 0.1)' : 'rgba(255, 193, 7, 0.08)', + border: isDarkMode ? '1px solid rgba(255, 193, 7, 0.3)' : '1px solid #b8860b', + text: isDarkMode ? 'rgba(255, 193, 7, 0.9)' : '#8b6914', + textMuted: isDarkMode ? 'rgba(255, 193, 7, 0.7)' : '#8b6914', + iconMain: isDarkMode ? 'rgba(255, 193, 7, 0.9)' : '#b8860b', + iconColor: isDarkMode ? 'rgba(255, 193, 7, 0.8)' : '#8b6914', + iconBg: isDarkMode ? 'rgba(255, 193, 7, 0.1)' : 'rgba(184, 134, 11, 0.1)', + iconHoverColor: isDarkMode ? 'rgba(255, 193, 7, 0.9)' : '#6b5210', + iconHoverBg: isDarkMode ? 'rgba(255, 193, 7, 0.2)' : 'rgba(184, 134, 11, 0.2)', + iconHoverBorder: isDarkMode ? 'rgba(255, 193, 7, 0.5)' : '#8b6914', + badgeBg: isDarkMode ? 'rgba(255, 193, 7, 0.3)' : 'rgba(184, 134, 11, 0.15)', + badgeBorder: isDarkMode ? '1px solid rgba(255, 193, 7, 0.5)' : '1px solid #b8860b', + chipBg: isDarkMode ? 'rgba(255, 193, 7, 0.2)' : 'rgba(184, 134, 11, 0.15)', + chipBorder: isDarkMode ? '1px solid rgba(255, 193, 7, 0.4)' : '1px solid #b8860b' + } + : { + bg: isDarkMode ? 'rgba(76, 175, 80, 0.1)' : 'rgba(76, 175, 80, 0.08)', + border: isDarkMode ? '1px solid rgba(76, 175, 80, 0.3)' : '1px solid #388e3c', + text: isDarkMode ? 'rgba(76, 175, 80, 0.9)' : '#2e7d32', + textMuted: isDarkMode ? 'rgba(76, 175, 80, 0.7)' : '#2e7d32', + iconMain: isDarkMode ? 'rgba(76, 175, 80, 0.9)' : '#388e3c', + iconColor: isDarkMode ? 'rgba(76, 175, 80, 0.8)' : '#2e7d32', + iconBg: isDarkMode ? 'rgba(76, 175, 80, 0.1)' : 'rgba(56, 142, 60, 0.1)', + iconHoverColor: isDarkMode ? 'rgba(76, 175, 80, 0.9)' : '#1b5e20', + iconHoverBg: isDarkMode ? 'rgba(76, 175, 80, 0.2)' : 'rgba(56, 142, 60, 0.2)', + iconHoverBorder: isDarkMode ? 'rgba(76, 175, 80, 0.5)' : '#388e3c', + badgeBg: isDarkMode ? 'rgba(76, 175, 80, 0.3)' : 'rgba(56, 142, 60, 0.15)', + badgeBorder: isDarkMode ? '1px solid rgba(76, 175, 80, 0.5)' : '1px solid #388e3c', + chipBg: isDarkMode ? 'rgba(76, 175, 80, 0.2)' : 'rgba(56, 142, 60, 0.15)', + chipBorder: isDarkMode ? '1px solid rgba(76, 175, 80, 0.4)' : '1px solid #388e3c' + } + return ( {/* Header Section */} - + - + Organization Default Template @@ -493,9 +525,9 @@ const AdminChatflows = () => { { { { { sx={{ height: 20, fontSize: '0.65rem', - bgcolor: isDarkMode ? 'rgba(255, 193, 7, 0.2)' : 'rgba(184, 134, 11, 0.15)', - color: isDarkMode ? 'rgba(255, 193, 7, 0.9)' : '#8b6914', - border: isDarkMode - ? '1px solid rgba(255, 193, 7, 0.4)' - : '1px solid #b8860b', + bgcolor: bc.chipBg, + color: bc.text, + border: bc.chipBorder, '& .MuiChip-label': { px: 0.75, py: 0.25 @@ -585,7 +615,7 @@ const AdminChatflows = () => { { { { { { size='small' onClick={() => window.open(getCanvasFullUrl(fullDefaultTemplate), '_blank')} sx={{ - color: isDarkMode ? 'rgba(255, 193, 7, 0.8)' : '#8b6914', - bgcolor: isDarkMode ? 'rgba(255, 193, 7, 0.1)' : 'rgba(184, 134, 11, 0.1)', - border: isDarkMode ? '1px solid rgba(255, 193, 7, 0.3)' : '1px solid #b8860b', + color: bc.iconColor, + bgcolor: bc.iconBg, + border: bc.border, '&:hover': { - color: isDarkMode ? 'rgba(255, 193, 7, 0.9)' : '#6b5210', - bgcolor: isDarkMode ? 'rgba(255, 193, 7, 0.2)' : 'rgba(184, 134, 11, 0.2)', - borderColor: isDarkMode ? 'rgba(255, 193, 7, 0.5)' : '#8b6914' + color: bc.iconHoverColor, + bgcolor: bc.iconHoverBg, + borderColor: bc.iconHoverBorder } }} > @@ -695,13 +725,13 @@ const AdminChatflows = () => { size='small' onClick={() => handleOpenMetrics(fullDefaultTemplate.id)} sx={{ - color: isDarkMode ? 'rgba(255, 193, 7, 0.8)' : '#8b6914', - bgcolor: isDarkMode ? 'rgba(255, 193, 7, 0.1)' : 'rgba(184, 134, 11, 0.1)', - border: isDarkMode ? '1px solid rgba(255, 193, 7, 0.3)' : '1px solid #b8860b', + color: bc.iconColor, + bgcolor: bc.iconBg, + border: bc.border, '&:hover': { - color: isDarkMode ? 'rgba(255, 193, 7, 0.9)' : '#6b5210', - bgcolor: isDarkMode ? 'rgba(255, 193, 7, 0.2)' : 'rgba(184, 134, 11, 0.2)', - borderColor: isDarkMode ? 'rgba(255, 193, 7, 0.5)' : '#8b6914' + color: bc.iconHoverColor, + bgcolor: bc.iconHoverBg, + borderColor: bc.iconHoverBorder } }} > @@ -713,13 +743,13 @@ const AdminChatflows = () => { size='small' onClick={() => handleOpenVersions(fullDefaultTemplate.id)} sx={{ - color: isDarkMode ? 'rgba(255, 193, 7, 0.8)' : '#8b6914', - bgcolor: isDarkMode ? 'rgba(255, 193, 7, 0.1)' : 'rgba(184, 134, 11, 0.1)', - border: isDarkMode ? '1px solid rgba(255, 193, 7, 0.3)' : '1px solid #b8860b', + color: bc.iconColor, + bgcolor: bc.iconBg, + border: bc.border, '&:hover': { - color: isDarkMode ? 'rgba(255, 193, 7, 0.9)' : '#6b5210', - bgcolor: isDarkMode ? 'rgba(255, 193, 7, 0.2)' : 'rgba(184, 134, 11, 0.2)', - borderColor: isDarkMode ? 'rgba(255, 193, 7, 0.5)' : '#8b6914' + color: bc.iconHoverColor, + bgcolor: bc.iconHoverBg, + borderColor: bc.iconHoverBorder } }} > From 99e0c8b7cf43f25473d184ea7ed4186d8edc363e Mon Sep 17 00:00:00 2001 From: Cameron Taylor <50385537+ct3685@users.noreply.github.com> Date: Fri, 15 May 2026 15:30:04 -0400 Subject: [PATCH 6/7] feat: collapsible template banner with count pill, green/amber theming, always-green badge (#1074) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Combines the green/amber theming (from #1073) with new collapsible + count pill enhancements, all in one PR targeting staging. ## What changed **Collapsible banner** - Clicking anywhere on the header row toggles the details section open/closed - Animated with `maxHeight` transition (same pattern as the filter panel) - Chevron icon rotates 180° when expanded - Smart default: expands automatically when there are outdated chatflows, collapses when all are current — no flash, no useEffect - User preference persisted in `localStorage` under `adminTemplateBannerExpanded`; once manually toggled the stored preference takes over **Count pill** - Always visible in the header (visible even when collapsed) - Shows `N outdated` in amber when any are behind, or `All current` in green when everything is synced **Green/amber theming** - Full `bc` color palette: amber when any chatflows outdated, green when all current — applied to border, background, text, muted labels, category chips, icon buttons, hover states - Action icon buttons use `e.stopPropagation()` so clicking View/Metrics/History doesn't accidentally toggle the banner **DEFAULT TEMPLATE badge always green** - Banner header badge: always green regardless of sync state - Table row chip: always green (was amber) ## Test Plan - [ ] With outdated chatflows: banner is amber, auto-expands on first load, shows "N outdated" pill - [ ] After pushing all current: banner turns green, auto-collapses on first load, shows "All current" pill - [ ] Click header to collapse/expand; reload and confirm preference is remembered - [ ] Click View/Metrics/Version History buttons — confirm banner does NOT toggle - [ ] DEFAULT TEMPLATE chip in table rows is green - [ ] Both light and dark mode --- .../ui/src/Admin/Chatflows/index.tsx | 510 ++++++++++-------- 1 file changed, 290 insertions(+), 220 deletions(-) diff --git a/packages-answers/ui/src/Admin/Chatflows/index.tsx b/packages-answers/ui/src/Admin/Chatflows/index.tsx index aff5d85e52b..ae51c2e2c24 100644 --- a/packages-answers/ui/src/Admin/Chatflows/index.tsx +++ b/packages-answers/ui/src/Admin/Chatflows/index.tsx @@ -93,6 +93,18 @@ const AdminChatflows = () => { const [bulkUpdateIncludeName, setBulkUpdateIncludeName] = useState(false) const [bulkUpdateInProgress, setBulkUpdateInProgress] = useState(false) + // Template banner collapse — smart default: open when outdated, closed when all current + const [templateBannerExpanded, setTemplateBannerExpanded] = useState( + localStorage.getItem('adminTemplateBannerExpanded') !== null + ? localStorage.getItem('adminTemplateBannerExpanded') !== 'false' + : true + ) + const toggleTemplateBanner = () => { + const next = !templateBannerExpanded + setTemplateBannerExpanded(next) + localStorage.setItem('adminTemplateBannerExpanded', String(next)) + } + // Versioning state const [versionModalOpen, setVersionModalOpen] = useState(false) const [selectedChatflowForVersions, setSelectedChatflowForVersions] = useState('') @@ -479,7 +491,10 @@ const AdminChatflows = () => { badgeBg: isDarkMode ? 'rgba(255, 193, 7, 0.3)' : 'rgba(184, 134, 11, 0.15)', badgeBorder: isDarkMode ? '1px solid rgba(255, 193, 7, 0.5)' : '1px solid #b8860b', chipBg: isDarkMode ? 'rgba(255, 193, 7, 0.2)' : 'rgba(184, 134, 11, 0.15)', - chipBorder: isDarkMode ? '1px solid rgba(255, 193, 7, 0.4)' : '1px solid #b8860b' + chipBorder: isDarkMode ? '1px solid rgba(255, 193, 7, 0.4)' : '1px solid #b8860b', + countBg: isDarkMode ? 'rgba(255, 193, 7, 0.2)' : 'rgba(184, 134, 11, 0.15)', + countBorder: isDarkMode ? '1px solid rgba(255, 193, 7, 0.4)' : '1px solid #b8860b', + countText: isDarkMode ? 'rgba(255, 193, 7, 0.9)' : '#8b6914' } : { bg: isDarkMode ? 'rgba(76, 175, 80, 0.1)' : 'rgba(76, 175, 80, 0.08)', @@ -495,9 +510,20 @@ const AdminChatflows = () => { badgeBg: isDarkMode ? 'rgba(76, 175, 80, 0.3)' : 'rgba(56, 142, 60, 0.15)', badgeBorder: isDarkMode ? '1px solid rgba(76, 175, 80, 0.5)' : '1px solid #388e3c', chipBg: isDarkMode ? 'rgba(76, 175, 80, 0.2)' : 'rgba(56, 142, 60, 0.15)', - chipBorder: isDarkMode ? '1px solid rgba(76, 175, 80, 0.4)' : '1px solid #388e3c' + chipBorder: isDarkMode ? '1px solid rgba(76, 175, 80, 0.4)' : '1px solid #388e3c', + countBg: isDarkMode ? 'rgba(76, 175, 80, 0.2)' : 'rgba(56, 142, 60, 0.15)', + countBorder: isDarkMode ? '1px solid rgba(76, 175, 80, 0.4)' : '1px solid #388e3c', + countText: isDarkMode ? 'rgba(76, 175, 80, 0.9)' : '#2e7d32' } + const outdatedCount = chatflowsData.filter((cf: any) => cf.templateStatus === 'outdated').length + const bannerOpen = localStorage.getItem('adminTemplateBannerExpanded') !== null ? templateBannerExpanded : hasOutdated + const greenBadge = { + bgcolor: isDarkMode ? 'rgba(76, 175, 80, 0.3)' : 'rgba(56, 142, 60, 0.15)', + color: isDarkMode ? 'rgba(76, 175, 80, 0.9)' : '#2e7d32', + border: isDarkMode ? '1px solid rgba(76, 175, 80, 0.5)' : '1px solid #388e3c' + } + return ( { backdropFilter: 'blur(20px)' }} > - {/* Header Section */} - + {/* Header — always visible, click to toggle */} + @@ -522,41 +557,79 @@ const AdminChatflows = () => { - + + + + + + {/* Collapsible details */} + + {/* Template Details Grid */} + + {/* Left Column */} + + {/* Name */} + + + NAME + + + {fullDefaultTemplate.name} + + - {/* Template Details Grid */} - - {/* Left Column */} - - {/* Name */} - - - NAME - - - {fullDefaultTemplate.name} - - + {/* Description */} + {fullDefaultTemplate.description && ( + + + DESCRIPTION + + + {fullDefaultTemplate.description} + + + )} - {/* Description */} - {fullDefaultTemplate.description && ( + {/* Category */} { mb: 0.5 }} > - DESCRIPTION - - - {fullDefaultTemplate.description} + CATEGORY + + {(fullDefaultTemplate.category || 'Uncategorized') + .split(';') + .map((category: string, index: number) => ( + + ))} + - )} - {/* Category */} - - - CATEGORY - - - {(fullDefaultTemplate.category || 'Uncategorized') - .split(';') - .map((category: string, index: number) => ( - - ))} + {/* Owner */} + + + OWNER + + + {fullDefaultTemplate.isOwner + ? 'Me' + : fullDefaultTemplate.user?.name || + fullDefaultTemplate.user?.email || + fullDefaultTemplate.userId} + - {/* Owner */} + {/* Right Column */} - - OWNER - - - {fullDefaultTemplate.isOwner - ? 'Me' - : fullDefaultTemplate.user?.name || - fullDefaultTemplate.user?.email || - fullDefaultTemplate.userId} - - - - - {/* Right Column */} - - {/* Created Date */} - - - CREATED - - - {fullDefaultTemplate.createdDate - ? format(new Date(fullDefaultTemplate.createdDate), 'MMM d, yyyy h:mm a') - : 'N/A'} - - + {/* Created Date */} + + + CREATED + + + {fullDefaultTemplate.createdDate + ? format(new Date(fullDefaultTemplate.createdDate), 'MMM d, yyyy h:mm a') + : 'N/A'} + + - {/* Updated Date */} - - - UPDATED - - - {fullDefaultTemplate.updatedDate - ? format(new Date(fullDefaultTemplate.updatedDate), 'MMM d, yyyy h:mm a') - : 'N/A'} - - + {/* Updated Date */} + + + UPDATED + + + {fullDefaultTemplate.updatedDate + ? format(new Date(fullDefaultTemplate.updatedDate), 'MMM d, yyyy h:mm a') + : 'N/A'} + + - {/* Version */} - - - VERSION - - - v{fullDefaultTemplate.currentVersion || 1} - - + {/* Version */} + + + VERSION + + + v{fullDefaultTemplate.currentVersion || 1} + + - {/* Actions */} - - - ACTIONS - - - - window.open(getCanvasFullUrl(fullDefaultTemplate), '_blank')} - sx={{ - color: bc.iconColor, - bgcolor: bc.iconBg, - border: bc.border, - '&:hover': { - color: bc.iconHoverColor, - bgcolor: bc.iconHoverBg, - borderColor: bc.iconHoverBorder - } - }} - > - - - - - handleOpenMetrics(fullDefaultTemplate.id)} - sx={{ - color: bc.iconColor, - bgcolor: bc.iconBg, - border: bc.border, - '&:hover': { - color: bc.iconHoverColor, - bgcolor: bc.iconHoverBg, - borderColor: bc.iconHoverBorder - } - }} - > - - - - - handleOpenVersions(fullDefaultTemplate.id)} - sx={{ - color: bc.iconColor, - bgcolor: bc.iconBg, - border: bc.border, - '&:hover': { - color: bc.iconHoverColor, - bgcolor: bc.iconHoverBg, - borderColor: bc.iconHoverBorder - } - }} - > - - - + {/* Actions */} + + + ACTIONS + + + + { + e.stopPropagation() + window.open(getCanvasFullUrl(fullDefaultTemplate), '_blank') + }} + sx={{ + color: bc.iconColor, + bgcolor: bc.iconBg, + border: bc.border, + '&:hover': { + color: bc.iconHoverColor, + bgcolor: bc.iconHoverBg, + borderColor: bc.iconHoverBorder + } + }} + > + + + + + { + e.stopPropagation() + handleOpenMetrics(fullDefaultTemplate.id) + }} + sx={{ + color: bc.iconColor, + bgcolor: bc.iconBg, + border: bc.border, + '&:hover': { + color: bc.iconHoverColor, + bgcolor: bc.iconHoverBg, + borderColor: bc.iconHoverBorder + } + }} + > + + + + + { + e.stopPropagation() + handleOpenVersions(fullDefaultTemplate.id) + }} + sx={{ + color: bc.iconColor, + bgcolor: bc.iconBg, + border: bc.border, + '&:hover': { + color: bc.iconHoverColor, + bgcolor: bc.iconHoverBg, + borderColor: bc.iconHoverBorder + } + }} + > + + + + - + {' '} + {/* end collapsible */} ) @@ -1348,9 +1414,13 @@ const AdminChatflows = () => { label='DEFAULT TEMPLATE' size='small' sx={{ - bgcolor: 'rgba(255, 193, 7, 0.2)', - color: 'rgba(255, 193, 7, 0.9)', - border: '1px solid rgba(255, 193, 7, 0.3)', + bgcolor: isDarkMode + ? 'rgba(76, 175, 80, 0.2)' + : 'rgba(56, 142, 60, 0.15)', + color: isDarkMode ? 'rgba(76, 175, 80, 0.9)' : '#2e7d32', + border: isDarkMode + ? '1px solid rgba(76, 175, 80, 0.3)' + : '1px solid #388e3c', fontSize: '0.65rem', height: '18px', fontWeight: 600 From 7d7216e0046b4fc4b0de99aac048feb410e58e85 Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Fri, 15 May 2026 15:32:19 -0400 Subject: [PATCH 7/7] fix: add missing KeyboardArrowDownIcon import --- packages-answers/ui/src/Admin/Chatflows/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages-answers/ui/src/Admin/Chatflows/index.tsx b/packages-answers/ui/src/Admin/Chatflows/index.tsx index ae51c2e2c24..a43731fa3fc 100644 --- a/packages-answers/ui/src/Admin/Chatflows/index.tsx +++ b/packages-answers/ui/src/Admin/Chatflows/index.tsx @@ -33,6 +33,7 @@ import FilterListIcon from '@mui/icons-material/FilterList' import TemplateIcon from '@mui/icons-material/AccountTree' import HistoryIcon from '@mui/icons-material/History' import RestoreIcon from '@mui/icons-material/Restore' +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown' import chatflowsApi from '@/api/chatflows' import useApi from '@ui/hooks/useApi' import { format } from 'date-fns'