import { getAuthUserId } from '@convex-dev/auth/server'; import { ConvexError } from 'convex/values'; import type { Doc, Id } from './_generated/dataModel'; import type { MutationCtx, QueryCtx } from './_generated/server'; type Ctx = QueryCtx | MutationCtx; export const getRequiredUserId = async (ctx: Ctx): Promise> => { const userId = await getAuthUserId(ctx); if (!userId) throw new ConvexError('Not authenticated.'); return userId; }; export const getOwnedSpoon = async ( ctx: Ctx, spoonId: Id<'spoons'>, ownerId: Id<'users'>, ): Promise> => { const spoon = await ctx.db.get(spoonId); if (spoon?.ownerId !== ownerId) { throw new ConvexError('Spoon not found.'); } return spoon; }; export const requireText = (value: string, label: string): string => { const trimmed = value.trim(); if (!trimmed) throw new ConvexError(`${label} is required.`); return trimmed; }; export const optionalText = (value: string | undefined) => { const trimmed = value?.trim(); if (!trimmed) return undefined; return trimmed; }; // Linux username for the per-user container home (/home/). Derived // from the first token of the profile name, sanitized; falls back to "user". export const deriveHomeUsername = (name?: string): string => { const first = (name ?? '').trim().split(/\s+/)[0] ?? ''; const sanitized = first.toLowerCase().replace(/[^a-z0-9_-]/g, ''); return sanitized || 'user'; }; // Normalizes a dotfile path to a safe HOME-relative path (no leading slash, no // "..", no empty segments). Throws on anything that would escape HOME. export const normalizeDotfilePath = (rawPath: string): string => { const cleaned = rawPath .trim() .replace(/^\.\/+/, '') .replace(/^\/+/, ''); const segments = cleaned.split('/').filter((s) => s.length > 0); if (segments.length === 0) { throw new ConvexError('A dotfile path is required.'); } if (segments.some((s) => s === '..' || s === '.')) { throw new ConvexError(`Invalid dotfile path: ${rawPath}`); } return segments.join('/'); };