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)
daemonState String? // Encrypted - contains dynamic daemon state
daemonStateVersion Int @default(0)
dataEncryptionKey Bytes?
seq Int @default(0)
active Boolean @default(true)
lastActiveAt DateTime @default(now())

View File

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

View File

@ -80,6 +80,19 @@ export type UpdateEvent = {
version: number;
} | 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';
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 {
return {
id: updateId,

View File

@ -64,6 +64,19 @@ declare global {
version: number;
} | 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';
machineId: string;