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 cc6b0561024..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", @@ -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-answers/ui/src/Admin/Chatflows/index.tsx b/packages-answers/ui/src/Admin/Chatflows/index.tsx index 962958b67dd..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' @@ -88,6 +89,23 @@ 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) + 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('') @@ -458,26 +476,81 @@ 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', + 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)', + 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', + 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 ( - {/* Header Section */} - + {/* Header — always visible, click to toggle */} + - + - + Organization Default Template @@ -485,246 +558,275 @@ 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 */} - 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: 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', - '&: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' - } - }} - > - - - - - 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', - '&: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' - } - }} - > - - - - - 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', - '&: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' - } - }} - > - - - + {/* 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 */} ) @@ -1026,23 +1128,10 @@ 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 +1224,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
@@ -1260,9 +1415,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 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/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/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) 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`) 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); } }