Convex: per-user dotfiles + environment storage (encrypted)
- 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
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
import { ConvexError, v } from 'convex/values';
|
||||
|
||||
import type { Id } from './_generated/dataModel';
|
||||
import type { QueryCtx } from './_generated/server';
|
||||
import { internalQuery, mutation, query } from './_generated/server';
|
||||
import { deriveHomeUsername, getRequiredUserId, optionalText } from './model';
|
||||
|
||||
const loadSettings = async (ctx: QueryCtx, ownerId: Id<'users'>) =>
|
||||
await ctx.db
|
||||
.query('userEnvironment')
|
||||
.withIndex('by_owner', (q) => q.eq('ownerId', ownerId))
|
||||
.unique();
|
||||
|
||||
/** Current user's environment settings + the resolved home username/first name. */
|
||||
export const getMine = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const ownerId = await getRequiredUserId(ctx);
|
||||
const [user, settings] = await Promise.all([
|
||||
ctx.db.get(ownerId),
|
||||
loadSettings(ctx, ownerId),
|
||||
]);
|
||||
const firstName = (user?.name ?? '').trim().split(/\s+/)[0] || 'you';
|
||||
const username = settings?.homeUsername ?? deriveHomeUsername(user?.name);
|
||||
return {
|
||||
enabled: settings?.enabled ?? true,
|
||||
username,
|
||||
firstName,
|
||||
dotfilesRepoUrl: settings?.dotfilesRepoUrl,
|
||||
dotfilesRepoRef: settings?.dotfilesRepoRef,
|
||||
setupCommand: settings?.setupCommand,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const updateMine = mutation({
|
||||
args: {
|
||||
enabled: v.optional(v.boolean()),
|
||||
dotfilesRepoUrl: v.optional(v.string()),
|
||||
dotfilesRepoRef: v.optional(v.string()),
|
||||
setupCommand: v.optional(v.string()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const ownerId = await getRequiredUserId(ctx);
|
||||
const repoUrl = optionalText(args.dotfilesRepoUrl);
|
||||
if (repoUrl && !/^https?:\/\//.test(repoUrl)) {
|
||||
throw new ConvexError('Dotfiles repo must be a public http(s) URL.');
|
||||
}
|
||||
const existing = await loadSettings(ctx, ownerId);
|
||||
const patch = {
|
||||
enabled: args.enabled ?? existing?.enabled ?? true,
|
||||
dotfilesRepoUrl: repoUrl,
|
||||
dotfilesRepoRef: optionalText(args.dotfilesRepoRef),
|
||||
setupCommand: optionalText(args.setupCommand),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
if (existing) {
|
||||
await ctx.db.patch(existing._id, patch);
|
||||
return { success: true };
|
||||
}
|
||||
await ctx.db.insert('userEnvironment', { ownerId, ...patch });
|
||||
return { success: true };
|
||||
},
|
||||
});
|
||||
|
||||
// Worker-facing: everything needed to materialize a job's owner's environment.
|
||||
// Content stays encrypted here; the Node action decrypts it. Resolves the owner
|
||||
// from the job.
|
||||
export const getRawEnvironmentForJobInternal = internalQuery({
|
||||
args: { jobId: v.id('agentJobs') },
|
||||
handler: async (ctx, { jobId }) => {
|
||||
const job = await ctx.db.get(jobId);
|
||||
if (!job) return null;
|
||||
const ownerId = job.ownerId;
|
||||
const [user, settings, files] = await Promise.all([
|
||||
ctx.db.get(ownerId),
|
||||
loadSettings(ctx, ownerId),
|
||||
ctx.db
|
||||
.query('userDotfiles')
|
||||
.withIndex('by_owner', (q) => q.eq('ownerId', ownerId))
|
||||
.collect(),
|
||||
]);
|
||||
return {
|
||||
username: settings?.homeUsername ?? deriveHomeUsername(user?.name),
|
||||
enabled: settings?.enabled ?? true,
|
||||
dotfilesRepoUrl: settings?.dotfilesRepoUrl,
|
||||
dotfilesRepoRef: settings?.dotfilesRepoRef,
|
||||
setupCommand: settings?.setupCommand,
|
||||
files: files.map((f) => ({
|
||||
path: f.path,
|
||||
encryptedContent: f.encryptedContent,
|
||||
isExecutable: f.isExecutable ?? false,
|
||||
})),
|
||||
};
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user