( workspaceId: string, newVars: Record<string, string>, actingUserId: string )
| 313 | * Encrypts and upserts workspace environment variables, merging with existing. |
| 314 | */ |
| 315 | export async function upsertWorkspaceEnvVars( |
| 316 | workspaceId: string, |
| 317 | newVars: Record<string, string>, |
| 318 | actingUserId: string |
| 319 | ): Promise<string[]> { |
| 320 | const updatedKeys: string[] = [] |
| 321 | if (Object.keys(newVars).length === 0) return updatedKeys |
| 322 | |
| 323 | const wsRows = await db |
| 324 | .select() |
| 325 | .from(workspaceEnvironment) |
| 326 | .where(eq(workspaceEnvironment.workspaceId, workspaceId)) |
| 327 | .limit(1) |
| 328 | const existingWsEncrypted = (wsRows[0]?.variables as Record<string, string>) || {} |
| 329 | |
| 330 | const newlyEncrypted: Record<string, string> = {} |
| 331 | for (const [key, val] of Object.entries(newVars)) { |
| 332 | const { encrypted } = await encryptSecret(val) |
| 333 | newlyEncrypted[key] = encrypted |
| 334 | updatedKeys.push(key) |
| 335 | } |
| 336 | |
| 337 | const merged = { ...existingWsEncrypted, ...newlyEncrypted } |
| 338 | |
| 339 | await db |
| 340 | .insert(workspaceEnvironment) |
| 341 | .values({ |
| 342 | id: generateId(), |
| 343 | workspaceId, |
| 344 | variables: merged, |
| 345 | createdAt: new Date(), |
| 346 | updatedAt: new Date(), |
| 347 | }) |
| 348 | .onConflictDoUpdate({ |
| 349 | target: [workspaceEnvironment.workspaceId], |
| 350 | set: { variables: merged, updatedAt: new Date() }, |
| 351 | }) |
| 352 | |
| 353 | invalidateEffectiveDecryptedEnvCache({ workspaceId }) |
| 354 | const newKeys = Object.keys(newVars).filter((k) => !(k in existingWsEncrypted)) |
| 355 | await createWorkspaceEnvCredentials({ workspaceId, newKeys, actingUserId }) |
| 356 | |
| 357 | return updatedKeys |
| 358 | } |
| 359 | |
| 360 | /** |
| 361 | * Returns a merged decrypted env map for webhook/copilot/MCP config resolution. |
no test coverage detected