'use node'; import { getAuthUserId } from '@convex-dev/auth/server'; import { ConvexError, v } from 'convex/values'; import type { Id } from './_generated/dataModel'; import type { ActionCtx } from './_generated/server'; import { internal } from './_generated/api'; import { action } from './_generated/server'; import { encryptSecret } from './secretCrypto'; const secretNamePattern = /^[A-Z_][A-Z0-9_]*$/; const getRequiredUserId = async (ctx: ActionCtx): Promise> => { const userId = await getAuthUserId(ctx); if (!userId) throw new ConvexError('Not authenticated.'); return userId; }; const previewSecret = (value: string) => { const trimmed = value.trim(); if (!trimmed) return 'empty'; if (trimmed.length <= 8) return 'configured'; return `${trimmed.slice(0, 3)}...${trimmed.slice(-3)}`; }; const normalizeName = (name: string) => { const normalized = name.trim().toUpperCase(); if (!secretNamePattern.test(normalized)) { throw new ConvexError( 'Secret names must look like environment variables, for example AUTH_SECRET.', ); } return normalized; }; const optionalText = (value?: string) => { const trimmed = value?.trim(); if (!trimmed) return undefined; return trimmed; }; export const create = action({ args: { spoonId: v.id('spoons'), name: v.string(), value: v.string(), description: v.optional(v.string()), }, handler: async (ctx, args): Promise> => { const ownerId = await getRequiredUserId(ctx); await ctx.runQuery(internal.spoons.getOwnedForAction, { spoonId: args.spoonId, ownerId, }); const value = args.value.trim(); if (!value) throw new ConvexError('Secret value is required.'); return await ctx.runMutation( internal.spoonSecrets.upsertEncryptedInternal, { spoonId: args.spoonId, ownerId, name: normalizeName(args.name), encryptedValue: encryptSecret(value), valuePreview: previewSecret(value), description: optionalText(args.description), }, ); }, }); export const update = action({ args: { secretId: v.id('spoonSecrets'), value: v.optional(v.string()), description: v.optional(v.string()), }, handler: async (ctx, args): Promise<{ success: true }> => { const ownerId = await getRequiredUserId(ctx); const patch = args.value?.trim() ? { encryptedValue: encryptSecret(args.value.trim()), valuePreview: previewSecret(args.value.trim()), } : {}; await ctx.runMutation(internal.spoonSecrets.patchEncryptedInternal, { secretId: args.secretId, ownerId, description: optionalText(args.description), ...patch, }); return { success: true }; }, });