feat: add machine data encryption key and new-machine update

This commit is contained in:
Steve Korshakov 2025-09-07 22:28:49 -07:00
parent 31bb2892f2
commit d4570c6b8f
5 changed files with 85 additions and 6 deletions

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Machine" ADD COLUMN "dataEncryptionKey" BYTEA;

View File

@ -199,6 +199,7 @@ model Machine {
metadataVersion Int @default(0) metadataVersion Int @default(0)
daemonState String? // Encrypted - contains dynamic daemon state daemonState String? // Encrypted - contains dynamic daemon state
daemonStateVersion Int @default(0) daemonStateVersion Int @default(0)
dataEncryptionKey Bytes?
seq Int @default(0) seq Int @default(0)
active Boolean @default(true) active Boolean @default(true)
lastActiveAt DateTime @default(now()) lastActiveAt DateTime @default(now())

View File

@ -5,7 +5,7 @@ import { db } from "@/storage/db";
import { log } from "@/utils/log"; import { log } from "@/utils/log";
import { randomKeyNaked } from "@/utils/randomKeyNaked"; import { randomKeyNaked } from "@/utils/randomKeyNaked";
import { allocateUserSeq } from "@/storage/seq"; import { allocateUserSeq } from "@/storage/seq";
import { buildUpdateMachineUpdate } from "@/app/events/eventRouter"; import { buildNewMachineUpdate, buildUpdateMachineUpdate } from "@/app/events/eventRouter";
export function machinesRoutes(app: Fastify, eventRouter: EventRouter) { export function machinesRoutes(app: Fastify, eventRouter: EventRouter) {
app.post('/v1/machines', { app.post('/v1/machines', {
@ -14,12 +14,13 @@ export function machinesRoutes(app: Fastify, eventRouter: EventRouter) {
body: z.object({ body: z.object({
id: z.string(), id: z.string(),
metadata: z.string(), // Encrypted metadata metadata: z.string(), // Encrypted metadata
daemonState: z.string().optional() // Encrypted daemon state daemonState: z.string().optional(), // Encrypted daemon state
dataEncryptionKey: z.string().nullish()
}) })
} }
}, async (request, reply) => { }, async (request, reply) => {
const userId = request.userId; const userId = request.userId;
const { id, metadata, daemonState } = request.body; const { id, metadata, daemonState, dataEncryptionKey } = request.body;
// Check if machine exists (like sessions do) // Check if machine exists (like sessions do)
const machine = await db.machine.findFirst({ const machine = await db.machine.findFirst({
@ -39,6 +40,7 @@ export function machinesRoutes(app: Fastify, eventRouter: EventRouter) {
metadataVersion: machine.metadataVersion, metadataVersion: machine.metadataVersion,
daemonState: machine.daemonState, daemonState: machine.daemonState,
daemonStateVersion: machine.daemonStateVersion, daemonStateVersion: machine.daemonStateVersion,
dataEncryptionKey: machine.dataEncryptionKey ? Buffer.from(machine.dataEncryptionKey).toString('base64') : null,
active: machine.active, active: machine.active,
activeAt: machine.lastActiveAt.getTime(), // Return as activeAt for API consistency activeAt: machine.lastActiveAt.getTime(), // Return as activeAt for API consistency
createdAt: machine.createdAt.getTime(), createdAt: machine.createdAt.getTime(),
@ -57,19 +59,30 @@ export function machinesRoutes(app: Fastify, eventRouter: EventRouter) {
metadataVersion: 1, metadataVersion: 1,
daemonState: daemonState || null, daemonState: daemonState || null,
daemonStateVersion: daemonState ? 1 : 0, daemonStateVersion: daemonState ? 1 : 0,
dataEncryptionKey: dataEncryptionKey ? Buffer.from(dataEncryptionKey, 'base64') : undefined,
// Default to offline - in case the user does not start daemon // Default to offline - in case the user does not start daemon
active: false, active: false,
// lastActiveAt and activeAt defaults to now() in schema // lastActiveAt and activeAt defaults to now() in schema
} }
}); });
// Emit update for new machine // Emit both new-machine and update-machine events for backward compatibility
const updSeq = await allocateUserSeq(userId); const updSeq1 = await allocateUserSeq(userId);
const updSeq2 = await allocateUserSeq(userId);
// Emit new-machine event with all data including dataEncryptionKey
const newMachinePayload = buildNewMachineUpdate(newMachine, updSeq1, randomKeyNaked(12));
eventRouter.emitUpdate({
userId,
payload: newMachinePayload
});
// Emit update-machine event for backward compatibility (without dataEncryptionKey)
const machineMetadata = { const machineMetadata = {
version: 1, version: 1,
value: metadata value: metadata
}; };
const updatePayload = buildUpdateMachineUpdate(newMachine.id, updSeq, randomKeyNaked(12), machineMetadata); const updatePayload = buildUpdateMachineUpdate(newMachine.id, updSeq2, randomKeyNaked(12), machineMetadata);
eventRouter.emitUpdate({ eventRouter.emitUpdate({
userId, userId,
payload: updatePayload payload: updatePayload
@ -82,6 +95,7 @@ export function machinesRoutes(app: Fastify, eventRouter: EventRouter) {
metadataVersion: newMachine.metadataVersion, metadataVersion: newMachine.metadataVersion,
daemonState: newMachine.daemonState, daemonState: newMachine.daemonState,
daemonStateVersion: newMachine.daemonStateVersion, daemonStateVersion: newMachine.daemonStateVersion,
dataEncryptionKey: newMachine.dataEncryptionKey ? Buffer.from(newMachine.dataEncryptionKey).toString('base64') : null,
active: newMachine.active, active: newMachine.active,
activeAt: newMachine.lastActiveAt.getTime(), // Return as activeAt for API consistency activeAt: newMachine.lastActiveAt.getTime(), // Return as activeAt for API consistency
createdAt: newMachine.createdAt.getTime(), createdAt: newMachine.createdAt.getTime(),
@ -109,6 +123,7 @@ export function machinesRoutes(app: Fastify, eventRouter: EventRouter) {
metadataVersion: m.metadataVersion, metadataVersion: m.metadataVersion,
daemonState: m.daemonState, daemonState: m.daemonState,
daemonStateVersion: m.daemonStateVersion, daemonStateVersion: m.daemonStateVersion,
dataEncryptionKey: m.dataEncryptionKey ? Buffer.from(m.dataEncryptionKey).toString('base64') : null,
seq: m.seq, seq: m.seq,
active: m.active, active: m.active,
activeAt: m.lastActiveAt.getTime(), activeAt: m.lastActiveAt.getTime(),
@ -147,6 +162,7 @@ export function machinesRoutes(app: Fastify, eventRouter: EventRouter) {
metadataVersion: machine.metadataVersion, metadataVersion: machine.metadataVersion,
daemonState: machine.daemonState, daemonState: machine.daemonState,
daemonStateVersion: machine.daemonStateVersion, daemonStateVersion: machine.daemonStateVersion,
dataEncryptionKey: machine.dataEncryptionKey ? Buffer.from(machine.dataEncryptionKey).toString('base64') : null,
seq: machine.seq, seq: machine.seq,
active: machine.active, active: machine.active,
activeAt: machine.lastActiveAt.getTime(), activeAt: machine.lastActiveAt.getTime(),

View File

@ -80,6 +80,19 @@ export type UpdateEvent = {
version: number; version: number;
} | null | undefined; } | null | undefined;
github?: GitHubProfile | null | undefined; github?: GitHubProfile | null | undefined;
} | {
type: 'new-machine';
machineId: string;
seq: number;
metadata: string;
metadataVersion: number;
daemonState: string | null;
daemonStateVersion: number;
dataEncryptionKey: string | null;
active: boolean;
activeAt: number;
createdAt: number;
updatedAt: number;
} | { } | {
type: 'update-machine'; type: 'update-machine';
machineId: string; machineId: string;
@ -349,6 +362,40 @@ export function buildUpdateAccountUpdate(userId: string, profile: Partial<Accoun
}; };
} }
export function buildNewMachineUpdate(machine: {
id: string;
seq: number;
metadata: string;
metadataVersion: number;
daemonState: string | null;
daemonStateVersion: number;
dataEncryptionKey: Uint8Array | null;
active: boolean;
lastActiveAt: Date;
createdAt: Date;
updatedAt: Date;
}, updateSeq: number, updateId: string): UpdatePayload {
return {
id: updateId,
seq: updateSeq,
body: {
t: 'new-machine',
machineId: machine.id,
seq: machine.seq,
metadata: machine.metadata,
metadataVersion: machine.metadataVersion,
daemonState: machine.daemonState,
daemonStateVersion: machine.daemonStateVersion,
dataEncryptionKey: machine.dataEncryptionKey ? Buffer.from(machine.dataEncryptionKey).toString('base64') : null,
active: machine.active,
activeAt: machine.lastActiveAt.getTime(),
createdAt: machine.createdAt.getTime(),
updatedAt: machine.updatedAt.getTime()
},
createdAt: Date.now()
};
}
export function buildUpdateMachineUpdate(machineId: string, updateSeq: number, updateId: string, metadata?: { value: string; version: number }, daemonState?: { value: string; version: number }): UpdatePayload { export function buildUpdateMachineUpdate(machineId: string, updateSeq: number, updateId: string, metadata?: { value: string; version: number }, daemonState?: { value: string; version: number }): UpdatePayload {
return { return {
id: updateId, id: updateId,

View File

@ -64,6 +64,19 @@ declare global {
version: number; version: number;
} | null | undefined; } | null | undefined;
github?: GitHubProfileType | null | undefined; github?: GitHubProfileType | null | undefined;
} | {
t: 'new-machine';
machineId: string;
seq: number;
metadata: string;
metadataVersion: number;
daemonState: string | null;
daemonStateVersion: number;
dataEncryptionKey: string | null;
active: boolean;
activeAt: number;
createdAt: number;
updatedAt: number;
} | { } | {
t: 'update-machine'; t: 'update-machine';
machineId: string; machineId: string;