95 lines
2.7 KiB
TypeScript
95 lines
2.7 KiB
TypeScript
'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<Id<'users'>> => {
|
|
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<Id<'spoonSecrets'>> => {
|
|
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 };
|
|
},
|
|
});
|