feat(starter): sync encrypted envvars to db on-save in settings modal, fetch during scheduled execution & decrypt

This commit is contained in:
Waleed Latif
2025-02-19 12:35:30 -08:00
parent c8a8d9608a
commit d5f102d419
10 changed files with 921 additions and 69 deletions

View File

@@ -2,11 +2,13 @@ import { NextRequest, NextResponse } from 'next/server'
import { Cron } from 'croner'
import { eq, lte } from 'drizzle-orm'
import { v4 as uuidv4 } from 'uuid'
import { z } from 'zod'
import { persistLog } from '@/lib/logging'
import { decryptSecret } from '@/lib/utils'
import { BlockState, WorkflowState } from '@/stores/workflow/types'
import { mergeSubblockState } from '@/stores/workflow/utils'
import { db } from '@/db'
import { workflow, workflowSchedule } from '@/db/schema'
import { userEnvironment, workflow, workflowSchedule } from '@/db/schema'
import { Executor } from '@/executor'
import { Serializer } from '@/serializer'
@@ -114,6 +116,9 @@ function calculateNextRunTime(
}
}
// Define the schema for environment variables
const EnvVarsSchema = z.record(z.string())
export const config = {
runtime: 'edge',
schedule: '*/1 * * * *',
@@ -161,18 +166,56 @@ export async function POST(req: NextRequest) {
// Use the same execution flow as in use-workflow-execution.ts
const mergedStates = mergeSubblockState(blocks)
const currentBlockStates = Object.entries(mergedStates).reduce(
(acc, [id, block]) => {
acc[id] = Object.entries(block.subBlocks).reduce(
(subAcc, [key, subBlock]) => {
subAcc[key] = subBlock.value
// Retrieve environment variables for this user
const [userEnv] = await db
.select()
.from(userEnvironment)
.where(eq(userEnvironment.userId, workflowRecord.userId))
.limit(1)
if (!userEnv) {
throw new Error('No environment variables found for this user')
}
// Parse and validate environment variables
const variables = EnvVarsSchema.parse(userEnv.variables)
// Replace environment variables in the block states
const currentBlockStates = await Object.entries(mergedStates).reduce(
async (accPromise, [id, block]) => {
const acc = await accPromise
acc[id] = await Object.entries(block.subBlocks).reduce(
async (subAccPromise, [key, subBlock]) => {
const subAcc = await subAccPromise
let value = subBlock.value
// If the value is a string and contains environment variable syntax
if (typeof value === 'string' && value.includes('{{') && value.includes('}}')) {
const matches = value.match(/{{([^}]+)}}/g)
if (matches) {
// Process all matches sequentially
for (const match of matches) {
const varName = match.slice(2, -2) // Remove {{ and }}
const encryptedValue = variables[varName]
if (!encryptedValue) {
throw new Error(`Environment variable "${varName}" was not found`)
}
// Decrypt the value
const { decrypted } = await decryptSecret(encryptedValue)
value = (value as string).replace(match, decrypted)
}
}
}
subAcc[key] = value
return subAcc
},
{} as Record<string, any>
Promise.resolve({} as Record<string, any>)
)
return acc
},
{} as Record<string, Record<string, any>>
Promise.resolve({} as Record<string, Record<string, any>>)
)
// Serialize and execute the workflow
@@ -265,18 +308,56 @@ export async function GET(req: NextRequest) {
// Use the same execution flow as in use-workflow-execution.ts
const mergedStates = mergeSubblockState(blocks)
const currentBlockStates = Object.entries(mergedStates).reduce(
(acc, [id, block]) => {
acc[id] = Object.entries(block.subBlocks).reduce(
(subAcc, [key, subBlock]) => {
subAcc[key] = subBlock.value
// Retrieve environment variables for this user
const [userEnv] = await db
.select()
.from(userEnvironment)
.where(eq(userEnvironment.userId, workflowRecord.userId))
.limit(1)
if (!userEnv) {
throw new Error('No environment variables found for this user')
}
// Parse and validate environment variables
const variables = EnvVarsSchema.parse(userEnv.variables)
// Replace environment variables in the block states
const currentBlockStates = await Object.entries(mergedStates).reduce(
async (accPromise, [id, block]) => {
const acc = await accPromise
acc[id] = await Object.entries(block.subBlocks).reduce(
async (subAccPromise, [key, subBlock]) => {
const subAcc = await subAccPromise
let value = subBlock.value
// If the value is a string and contains environment variable syntax
if (typeof value === 'string' && value.includes('{{') && value.includes('}}')) {
const matches = value.match(/{{([^}]+)}}/g)
if (matches) {
// Process all matches sequentially
for (const match of matches) {
const varName = match.slice(2, -2) // Remove {{ and }}
const encryptedValue = variables[varName]
if (!encryptedValue) {
throw new Error(`Environment variable "${varName}" was not found`)
}
// Decrypt the value
const { decrypted } = await decryptSecret(encryptedValue)
value = (value as string).replace(match, decrypted)
}
}
}
subAcc[key] = value
return subAcc
},
{} as Record<string, any>
Promise.resolve({} as Record<string, any>)
)
return acc
},
{} as Record<string, Record<string, any>>
Promise.resolve({} as Record<string, Record<string, any>>)
)
// Serialize and execute the workflow

View File

@@ -1,54 +1,61 @@
import { NextResponse } from 'next/server'
import { NextRequest, NextResponse } from 'next/server'
import { eq } from 'drizzle-orm'
import { nanoid } from 'nanoid'
import { z } from 'zod'
import { hashSecret } from '@/lib/utils'
import { getSession } from '@/lib/auth'
import { encryptSecret } from '@/lib/utils'
import { EnvironmentVariable } from '@/stores/settings/environment/types'
import { db } from '@/db'
import { userEnvironment } from '@/db/schema'
const EnvironmentSchema = z.object({
userId: z.string(),
data: z.string(), // A JSON stringified object of envvars
// Schema for environment variable updates
const EnvVarSchema = z.object({
variables: z.record(z.string()),
})
export async function POST(request: Request) {
export async function POST(req: NextRequest) {
try {
const body = await request.json()
const { userId, data } = EnvironmentSchema.parse(body)
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// Parse the incoming JSON string
const parsedData = JSON.parse(data) as Record<string, EnvironmentVariable>
const body = await req.json()
const { variables } = EnvVarSchema.parse(body)
// Hash all environment variables with unique salts
const securedData = await Promise.all(
Object.entries(parsedData).map(async ([key, value]) => {
const { hash, salt } = await hashSecret(value.value)
return [key, { key, value: hash, salt }]
})
)
// Encrypt each environment variable value
const encryptedVariables: Record<string, string> = {}
for (const [key, value] of Object.entries(variables)) {
const { encrypted } = await encryptSecret(value)
encryptedVariables[key] = encrypted
}
// Store the hashed values
// Upsert the environment variables
await db
.insert(userEnvironment)
.values({
id: nanoid(),
userId,
variables: Object.fromEntries(securedData),
id: crypto.randomUUID(),
userId: session.user.id,
variables: encryptedVariables,
updatedAt: new Date(),
})
.onConflictDoUpdate({
target: [userEnvironment.userId],
set: {
variables: Object.fromEntries(securedData),
variables: encryptedVariables,
updatedAt: new Date(),
},
})
return NextResponse.json({ success: true }, { status: 200 })
} catch (error: any) {
console.error('Environment update error:', error)
return NextResponse.json({ error: error.message }, { status: 500 })
return NextResponse.json({ success: true })
} catch (error) {
console.error('Error updating environment variables:', error)
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Invalid request data', details: error.errors },
{ status: 400 }
)
}
return NextResponse.json({ error: 'Failed to update environment variables' }, { status: 500 })
}
}

View File

@@ -169,19 +169,27 @@ export function EnvironmentVariables({ onOpenChange }: EnvironmentVariablesProps
setEnvVars(newEnvVars.length ? newEnvVars : [INITIAL_ENV_VAR])
}
const handleSave = () => {
const validVars = envVars.filter((v) => v.key && v.value)
validVars.forEach((v) => setVariable(v.key, v.value))
const handleSave = async () => {
try {
const validVars = envVars.filter((v) => v.key && v.value)
validVars.forEach((v) => setVariable(v.key, v.value))
const currentKeys = new Set(validVars.map((v) => v.key))
Object.keys(variables).forEach((key) => {
if (!currentKeys.has(key)) {
removeVariable(key)
}
})
const currentKeys = new Set(validVars.map((v) => v.key))
Object.keys(variables).forEach((key) => {
if (!currentKeys.has(key)) {
removeVariable(key)
}
})
setShowUnsavedChanges(false)
onOpenChange(false)
// Sync with database
await useEnvironmentStore.getState().syncWithDatabase()
setShowUnsavedChanges(false)
onOpenChange(false)
} catch (error) {
console.error('Failed to save environment variables:', error)
// You might want to show an error notification here
}
}
const renderEnvVarRow = (envVar: UIEnvironmentVariable, index: number) => (

View File

@@ -0,0 +1 @@
ALTER TABLE "workflow_schedule" ADD CONSTRAINT "workflow_schedule_workflow_id_unique" UNIQUE("workflow_id");

View File

@@ -0,0 +1,684 @@
{
"id": "84e3d05a-f1f6-4d4f-9176-eb629f21837c",
"prevId": "6ee96eef-e39f-4051-83dc-74ec54eebc20",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.account": {
"name": "account",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"account_id": {
"name": "account_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"provider_id": {
"name": "provider_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"id_token": {
"name": "id_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"access_token_expires_at": {
"name": "access_token_expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"refresh_token_expires_at": {
"name": "refresh_token_expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"scope": {
"name": "scope",
"type": "text",
"primaryKey": false,
"notNull": false
},
"password": {
"name": "password",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"account_user_id_user_id_fk": {
"name": "account_user_id_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.workflow_logs": {
"name": "workflow_logs",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"workflow_id": {
"name": "workflow_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"execution_id": {
"name": "execution_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"level": {
"name": "level",
"type": "text",
"primaryKey": false,
"notNull": true
},
"message": {
"name": "message",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"workflow_logs_workflow_id_workflow_id_fk": {
"name": "workflow_logs_workflow_id_workflow_id_fk",
"tableFrom": "workflow_logs",
"tableTo": "workflow",
"columnsFrom": ["workflow_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.session": {
"name": "session",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"token": {
"name": "token",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"ip_address": {
"name": "ip_address",
"type": "text",
"primaryKey": false,
"notNull": false
},
"user_agent": {
"name": "user_agent",
"type": "text",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"session_user_id_user_id_fk": {
"name": "session_user_id_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"session_token_unique": {
"name": "session_token_unique",
"nullsNotDistinct": false,
"columns": ["token"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.settings": {
"name": "settings",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"general": {
"name": "general",
"type": "json",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"settings_user_id_user_id_fk": {
"name": "settings_user_id_user_id_fk",
"tableFrom": "settings",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"settings_user_id_unique": {
"name": "settings_user_id_unique",
"nullsNotDistinct": false,
"columns": ["user_id"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email_verified": {
"name": "email_verified",
"type": "boolean",
"primaryKey": false,
"notNull": true
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_email_unique": {
"name": "user_email_unique",
"nullsNotDistinct": false,
"columns": ["email"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.environment": {
"name": "environment",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"variables": {
"name": "variables",
"type": "json",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"environment_user_id_user_id_fk": {
"name": "environment_user_id_user_id_fk",
"tableFrom": "environment",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"environment_user_id_unique": {
"name": "environment_user_id_unique",
"nullsNotDistinct": false,
"columns": ["user_id"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.verification": {
"name": "verification",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"identifier": {
"name": "identifier",
"type": "text",
"primaryKey": false,
"notNull": true
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.waitlist": {
"name": "waitlist",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'pending'"
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"waitlist_email_unique": {
"name": "waitlist_email_unique",
"nullsNotDistinct": false,
"columns": ["email"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.workflow": {
"name": "workflow",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"state": {
"name": "state",
"type": "json",
"primaryKey": false,
"notNull": true
},
"last_synced": {
"name": "last_synced",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"workflow_user_id_user_id_fk": {
"name": "workflow_user_id_user_id_fk",
"tableFrom": "workflow",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.workflow_schedule": {
"name": "workflow_schedule",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"workflow_id": {
"name": "workflow_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"cron_expression": {
"name": "cron_expression",
"type": "text",
"primaryKey": false,
"notNull": false
},
"next_run_at": {
"name": "next_run_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"last_ran_at": {
"name": "last_ran_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"trigger_type": {
"name": "trigger_type",
"type": "text",
"primaryKey": false,
"notNull": true
},
"timezone": {
"name": "timezone",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'UTC'"
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"workflow_schedule_workflow_id_workflow_id_fk": {
"name": "workflow_schedule_workflow_id_workflow_id_fk",
"tableFrom": "workflow_schedule",
"tableTo": "workflow",
"columnsFrom": ["workflow_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"workflow_schedule_workflow_id_unique": {
"name": "workflow_schedule_workflow_id_unique",
"nullsNotDistinct": false,
"columns": ["workflow_id"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -50,6 +50,13 @@
"when": 1739939661615,
"tag": "0006_plain_zzzax",
"breakpoints": true
},
{
"idx": 7,
"version": "7",
"when": 1739995526085,
"tag": "0007_mute_stepford_cuckoos",
"breakpoints": true
}
]
}

View File

@@ -106,7 +106,8 @@ export const workflowSchedule = pgTable('workflow_schedule', {
id: text('id').primaryKey(),
workflowId: text('workflow_id')
.notNull()
.references(() => workflow.id, { onDelete: 'cascade' }),
.references(() => workflow.id, { onDelete: 'cascade' })
.unique(),
cronExpression: text('cron_expression'),
nextRunAt: timestamp('next_run_at'),
lastRanAt: timestamp('last_ran_at'),

View File

@@ -1,27 +1,63 @@
import { type ClassValue, clsx } from 'clsx'
import { createHash } from 'crypto'
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
function getEncryptionKey(): Buffer {
const key = process.env.ENCRYPTION_KEY
if (!key || key.length !== 64) {
throw new Error('ENCRYPTION_KEY must be set to a 64-character hex string (32 bytes)')
}
return Buffer.from(key, 'hex')
}
/**
* Hashes a secret using SHA-256 with a salt
* @param secret - The secret to hash
* @param salt - Optional salt to use for hashing. If not provided, a random salt will be generated
* @returns A promise that resolves to an object containing the hashed secret and salt
* Encrypts a secret using AES-256-GCM
* @param secret - The secret to encrypt
* @returns A promise that resolves to an object containing the encrypted secret and IV
*/
export async function hashSecret(
secret: string,
salt?: string
): Promise<{ hash: string; salt: string }> {
const useSalt =
salt || createHash('sha256').update(crypto.randomUUID()).digest('hex').slice(0, 16)
const hash = createHash('sha256')
.update(secret + useSalt)
.digest('hex')
return { hash, salt: useSalt }
export async function encryptSecret(secret: string): Promise<{ encrypted: string; iv: string }> {
const iv = randomBytes(16)
const key = getEncryptionKey()
const cipher = createCipheriv('aes-256-gcm', key, iv)
let encrypted = cipher.update(secret, 'utf8', 'hex')
encrypted += cipher.final('hex')
const authTag = cipher.getAuthTag()
// Format: iv:encrypted:authTag
return {
encrypted: `${iv.toString('hex')}:${encrypted}:${authTag.toString('hex')}`,
iv: iv.toString('hex'),
}
}
/**
* Decrypts an encrypted secret
* @param encryptedValue - The encrypted value in format "iv:encrypted:authTag"
* @returns A promise that resolves to an object containing the decrypted secret
*/
export async function decryptSecret(encryptedValue: string): Promise<{ decrypted: string }> {
const [ivHex, encrypted, authTagHex] = encryptedValue.split(':')
if (!ivHex || !encrypted || !authTagHex) {
throw new Error('Invalid encrypted value format. Expected "iv:encrypted:authTag"')
}
const key = getEncryptionKey()
const iv = Buffer.from(ivHex, 'hex')
const authTag = Buffer.from(authTagHex, 'hex')
const decipher = createDecipheriv('aes-256-gcm', key, iv)
decipher.setAuthTag(authTag)
let decrypted = decipher.update(encrypted, 'hex', 'utf8')
decrypted += decipher.final('utf8')
return { decrypted }
}
export function convertScheduleOptionsToCron(

View File

@@ -34,6 +34,32 @@ export const useEnvironmentStore = create<EnvironmentStore>()(
getAllVariables: () => {
return get().variables
},
syncWithDatabase: async () => {
const variables = get().variables
const variableValues = Object.entries(variables).reduce(
(acc, [key, value]) => ({
...acc,
[key]: value.value,
}),
{}
)
try {
const response = await fetch('/api/settings/environment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ variables: variableValues }),
})
if (!response.ok) {
throw new Error('Failed to sync environment variables')
}
} catch (error) {
console.error('Error syncing environment variables:', error)
throw error
}
},
}),
{
name: 'environment-store',

View File

@@ -13,4 +13,5 @@ export interface EnvironmentStore extends EnvironmentState {
clearVariables: () => void
getVariable: (key: string) => string | undefined
getAllVariables: () => Record<string, EnvironmentVariable>
syncWithDatabase: () => Promise<void>
}