683fc62129
- schema: userDotfiles (one encrypted row per file, HOME-relative path) and
userEnvironment (home username + optional public dotfiles repo + setup command)
- userDotfiles.ts: list/remove/removeDirectory/rename + internal upsert/getRaw
- userDotfilesNode.ts ('use node'): putFile/importFiles/getFileContent (encrypt
/decrypt via secretCrypto) + getEnvironmentForJob (worker-token, returns the
owner's decrypted dotfiles + repo/setup config)
- userEnvironment.ts: getMine/updateMine + getRawEnvironmentForJobInternal
- model.ts: deriveHomeUsername + normalizeDotfilePath helpers
63 lines
2.0 KiB
TypeScript
63 lines
2.0 KiB
TypeScript
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<Id<'users'>> => {
|
|
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<Doc<'spoons'>> => {
|
|
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/<username>). 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('/');
|
|
};
|