wip: working on artifacts

This commit is contained in:
Steve Korshakov 2025-09-16 22:35:09 -07:00
parent e961407993
commit 86bf8bf03c
8 changed files with 967 additions and 0 deletions

View File

@ -0,0 +1,24 @@
-- CreateTable
CREATE TABLE "Artifact" (
"id" TEXT NOT NULL,
"accountId" TEXT NOT NULL,
"header" BYTEA NOT NULL,
"headerVersion" INTEGER NOT NULL DEFAULT 0,
"body" BYTEA NOT NULL,
"bodyVersion" INTEGER NOT NULL DEFAULT 0,
"dataEncryptionKey" BYTEA NOT NULL,
"seq" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Artifact_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "Artifact_accountId_idx" ON "Artifact"("accountId");
-- CreateIndex
CREATE INDEX "Artifact_accountId_updatedAt_idx" ON "Artifact"("accountId", "updatedAt" DESC);
-- AddForeignKey
ALTER TABLE "Artifact" ADD CONSTRAINT "Artifact_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -44,6 +44,7 @@ model Account {
Machine Machine[]
UploadedFile UploadedFile[]
ServiceAccountToken ServiceAccountToken[]
Artifact Artifact[]
}
model TerminalAuthRequest {
@ -241,3 +242,24 @@ model ServiceAccountToken {
@@unique([accountId, vendor])
@@index([accountId])
}
//
// Artifacts
//
model Artifact {
id String @id // UUID provided by client
accountId String
account Account @relation(fields: [accountId], references: [id])
header Bytes // Encrypted header (can contain JSON)
headerVersion Int @default(0)
body Bytes // Encrypted body
bodyVersion Int @default(0)
dataEncryptionKey Bytes // Encryption key for this artifact
seq Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([accountId])
@@index([accountId, updatedAt(sort: Desc)])
}

View File

@ -14,6 +14,7 @@ import { machinesRoutes } from "./routes/machinesRoutes";
import { devRoutes } from "./routes/devRoutes";
import { versionRoutes } from "./routes/versionRoutes";
import { voiceRoutes } from "./routes/voiceRoutes";
import { artifactsRoutes } from "./routes/artifactsRoutes";
import { enableMonitoring } from "./utils/enableMonitoring";
import { enableErrorHandlers } from "./utils/enableErrorHandlers";
import { enableAuthentication } from "./utils/enableAuthentication";
@ -54,6 +55,7 @@ export async function startApi(eventRouter: EventRouter) {
accountRoutes(typed, eventRouter);
connectRoutes(typed, eventRouter);
machinesRoutes(typed, eventRouter);
artifactsRoutes(typed, eventRouter);
devRoutes(typed);
versionRoutes(typed);
voiceRoutes(typed);

View File

@ -0,0 +1,414 @@
import { EventRouter, buildNewArtifactUpdate, buildUpdateArtifactUpdate, buildDeleteArtifactUpdate } from "@/app/events/eventRouter";
import { db } from "@/storage/db";
import { Fastify } from "../types";
import { z } from "zod";
import { randomKeyNaked } from "@/utils/randomKeyNaked";
import { allocateUserSeq } from "@/storage/seq";
import { log } from "@/utils/log";
import * as privacyKit from "privacy-kit";
export function artifactsRoutes(app: Fastify, eventRouter: EventRouter) {
// GET /v1/artifacts - List all artifacts for the account
app.get('/v1/artifacts', {
preHandler: app.authenticate,
schema: {
response: {
200: z.array(z.object({
id: z.string(),
header: z.string(),
headerVersion: z.number(),
dataEncryptionKey: z.string(),
seq: z.number(),
createdAt: z.number(),
updatedAt: z.number()
})),
500: z.object({
error: z.literal('Failed to get artifacts')
})
}
}
}, async (request, reply) => {
const userId = request.userId;
try {
const artifacts = await db.artifact.findMany({
where: { accountId: userId },
orderBy: { updatedAt: 'desc' },
select: {
id: true,
header: true,
headerVersion: true,
dataEncryptionKey: true,
seq: true,
createdAt: true,
updatedAt: true
}
});
return reply.send(artifacts.map(a => ({
id: a.id,
header: privacyKit.encodeBase64(a.header),
headerVersion: a.headerVersion,
dataEncryptionKey: privacyKit.encodeBase64(a.dataEncryptionKey),
seq: a.seq,
createdAt: a.createdAt.getTime(),
updatedAt: a.updatedAt.getTime()
})));
} catch (error) {
log({ module: 'api', level: 'error' }, `Failed to get artifacts: ${error}`);
return reply.code(500).send({ error: 'Failed to get artifacts' });
}
});
// GET /v1/artifacts/:id - Get single artifact with full body
app.get('/v1/artifacts/:id', {
preHandler: app.authenticate,
schema: {
params: z.object({
id: z.string()
}),
response: {
200: z.object({
id: z.string(),
header: z.string(),
headerVersion: z.number(),
body: z.string(),
bodyVersion: z.number(),
dataEncryptionKey: z.string(),
seq: z.number(),
createdAt: z.number(),
updatedAt: z.number()
}),
404: z.object({
error: z.literal('Artifact not found')
}),
500: z.object({
error: z.literal('Failed to get artifact')
})
}
}
}, async (request, reply) => {
const userId = request.userId;
const { id } = request.params;
try {
const artifact = await db.artifact.findFirst({
where: {
id,
accountId: userId
}
});
if (!artifact) {
return reply.code(404).send({ error: 'Artifact not found' });
}
return reply.send({
id: artifact.id,
header: privacyKit.encodeBase64(artifact.header),
headerVersion: artifact.headerVersion,
body: privacyKit.encodeBase64(artifact.body),
bodyVersion: artifact.bodyVersion,
dataEncryptionKey: privacyKit.encodeBase64(artifact.dataEncryptionKey),
seq: artifact.seq,
createdAt: artifact.createdAt.getTime(),
updatedAt: artifact.updatedAt.getTime()
});
} catch (error) {
log({ module: 'api', level: 'error' }, `Failed to get artifact: ${error}`);
return reply.code(500).send({ error: 'Failed to get artifact' });
}
});
// POST /v1/artifacts - Create new artifact
app.post('/v1/artifacts', {
preHandler: app.authenticate,
schema: {
body: z.object({
id: z.string().uuid(),
header: z.string(),
body: z.string(),
dataEncryptionKey: z.string()
}),
response: {
200: z.object({
id: z.string(),
header: z.string(),
headerVersion: z.number(),
body: z.string(),
bodyVersion: z.number(),
dataEncryptionKey: z.string(),
seq: z.number(),
createdAt: z.number(),
updatedAt: z.number()
}),
409: z.object({
error: z.literal('Artifact with this ID already exists for another account')
}),
500: z.object({
error: z.literal('Failed to create artifact')
})
}
}
}, async (request, reply) => {
const userId = request.userId;
const { id, header, body, dataEncryptionKey } = request.body;
try {
// Check if artifact exists
const existingArtifact = await db.artifact.findUnique({
where: { id }
});
if (existingArtifact) {
// If exists for another account, return conflict
if (existingArtifact.accountId !== userId) {
return reply.code(409).send({
error: 'Artifact with this ID already exists for another account'
});
}
// If exists for same account, return existing (idempotent)
log({ module: 'api', artifactId: id, userId }, 'Found existing artifact');
return reply.send({
id: existingArtifact.id,
header: privacyKit.encodeBase64(existingArtifact.header),
headerVersion: existingArtifact.headerVersion,
body: privacyKit.encodeBase64(existingArtifact.body),
bodyVersion: existingArtifact.bodyVersion,
dataEncryptionKey: privacyKit.encodeBase64(existingArtifact.dataEncryptionKey),
seq: existingArtifact.seq,
createdAt: existingArtifact.createdAt.getTime(),
updatedAt: existingArtifact.updatedAt.getTime()
});
}
// Create new artifact
log({ module: 'api', artifactId: id, userId }, 'Creating new artifact');
const artifact = await db.artifact.create({
data: {
id,
accountId: userId,
header: privacyKit.decodeBase64(header),
headerVersion: 1,
body: privacyKit.decodeBase64(body),
bodyVersion: 1,
dataEncryptionKey: privacyKit.decodeBase64(dataEncryptionKey),
seq: 0
}
});
// Emit new-artifact event
const updSeq = await allocateUserSeq(userId);
const newArtifactPayload = buildNewArtifactUpdate(artifact, updSeq, randomKeyNaked(12));
eventRouter.emitUpdate({
userId,
payload: newArtifactPayload,
recipientFilter: { type: 'all-user-authenticated-connections' }
});
return reply.send({
id: artifact.id,
header: privacyKit.encodeBase64(artifact.header),
headerVersion: artifact.headerVersion,
body: privacyKit.encodeBase64(artifact.body),
bodyVersion: artifact.bodyVersion,
dataEncryptionKey: privacyKit.encodeBase64(artifact.dataEncryptionKey),
seq: artifact.seq,
createdAt: artifact.createdAt.getTime(),
updatedAt: artifact.updatedAt.getTime()
});
} catch (error) {
log({ module: 'api', level: 'error' }, `Failed to create artifact: ${error}`);
return reply.code(500).send({ error: 'Failed to create artifact' });
}
});
// POST /v1/artifacts/:id - Update artifact with version control
app.post('/v1/artifacts/:id', {
preHandler: app.authenticate,
schema: {
params: z.object({
id: z.string()
}),
body: z.object({
header: z.string().optional(),
expectedHeaderVersion: z.number().int().min(0).optional(),
body: z.string().optional(),
expectedBodyVersion: z.number().int().min(0).optional()
}),
response: {
200: z.union([
z.object({
success: z.literal(true),
headerVersion: z.number().optional(),
bodyVersion: z.number().optional()
}),
z.object({
success: z.literal(false),
error: z.literal('version-mismatch'),
currentHeaderVersion: z.number().optional(),
currentBodyVersion: z.number().optional(),
currentHeader: z.string().optional(),
currentBody: z.string().optional()
})
]),
404: z.object({
error: z.literal('Artifact not found')
}),
500: z.object({
error: z.literal('Failed to update artifact')
})
}
}
}, async (request, reply) => {
const userId = request.userId;
const { id } = request.params;
const { header, expectedHeaderVersion, body, expectedBodyVersion } = request.body;
try {
// Get current artifact for version check
const currentArtifact = await db.artifact.findFirst({
where: {
id,
accountId: userId
}
});
if (!currentArtifact) {
return reply.code(404).send({ error: 'Artifact not found' });
}
// Check version mismatches
const headerMismatch = header !== undefined && expectedHeaderVersion !== undefined &&
currentArtifact.headerVersion !== expectedHeaderVersion;
const bodyMismatch = body !== undefined && expectedBodyVersion !== undefined &&
currentArtifact.bodyVersion !== expectedBodyVersion;
if (headerMismatch || bodyMismatch) {
return reply.send({
success: false,
error: 'version-mismatch',
...(headerMismatch && {
currentHeaderVersion: currentArtifact.headerVersion,
currentHeader: privacyKit.encodeBase64(currentArtifact.header)
}),
...(bodyMismatch && {
currentBodyVersion: currentArtifact.bodyVersion,
currentBody: privacyKit.encodeBase64(currentArtifact.body)
})
});
}
// Build update data
const updateData: any = {
updatedAt: new Date()
};
let headerUpdate: { value: string; version: number } | undefined;
let bodyUpdate: { value: string; version: number } | undefined;
if (header !== undefined && expectedHeaderVersion !== undefined) {
updateData.header = privacyKit.decodeBase64(header);
updateData.headerVersion = expectedHeaderVersion + 1;
headerUpdate = {
value: header,
version: expectedHeaderVersion + 1
};
}
if (body !== undefined && expectedBodyVersion !== undefined) {
updateData.body = privacyKit.decodeBase64(body);
updateData.bodyVersion = expectedBodyVersion + 1;
bodyUpdate = {
value: body,
version: expectedBodyVersion + 1
};
}
// Increment seq
updateData.seq = currentArtifact.seq + 1;
// Update artifact
await db.artifact.update({
where: { id },
data: updateData
});
// Emit update-artifact event
const updSeq = await allocateUserSeq(userId);
const updatePayload = buildUpdateArtifactUpdate(id, updSeq, randomKeyNaked(12), headerUpdate, bodyUpdate);
eventRouter.emitUpdate({
userId,
payload: updatePayload,
recipientFilter: { type: 'all-user-authenticated-connections' }
});
return reply.send({
success: true,
...(headerUpdate && { headerVersion: headerUpdate.version }),
...(bodyUpdate && { bodyVersion: bodyUpdate.version })
});
} catch (error) {
log({ module: 'api', level: 'error' }, `Failed to update artifact: ${error}`);
return reply.code(500).send({ error: 'Failed to update artifact' });
}
});
// DELETE /v1/artifacts/:id - Delete artifact
app.delete('/v1/artifacts/:id', {
preHandler: app.authenticate,
schema: {
params: z.object({
id: z.string()
}),
response: {
200: z.object({
success: z.literal(true)
}),
404: z.object({
error: z.literal('Artifact not found')
}),
500: z.object({
error: z.literal('Failed to delete artifact')
})
}
}
}, async (request, reply) => {
const userId = request.userId;
const { id } = request.params;
try {
// Check if artifact exists and belongs to user
const artifact = await db.artifact.findFirst({
where: {
id,
accountId: userId
}
});
if (!artifact) {
return reply.code(404).send({ error: 'Artifact not found' });
}
// Delete artifact
await db.artifact.delete({
where: { id }
});
// Emit delete-artifact event
const updSeq = await allocateUserSeq(userId);
const deletePayload = buildDeleteArtifactUpdate(id, updSeq, randomKeyNaked(12));
eventRouter.emitUpdate({
userId,
payload: deletePayload,
recipientFilter: { type: 'all-user-authenticated-connections' }
});
return reply.send({ success: true });
} catch (error) {
log({ module: 'api', level: 'error' }, `Failed to delete artifact: ${error}`);
return reply.code(500).send({ error: 'Failed to delete artifact' });
}
});
}

View File

@ -10,6 +10,7 @@ import { rpcHandler } from "./socket/rpcHandler";
import { pingHandler } from "./socket/pingHandler";
import { sessionUpdateHandler } from "./socket/sessionUpdateHandler";
import { machineUpdateHandler } from "./socket/machineUpdateHandler";
import { artifactUpdateHandler } from "./socket/artifactUpdateHandler";
export function startSocket(app: Fastify, eventRouter: EventRouter) {
const io = new Server(app.server, {
@ -140,6 +141,7 @@ export function startSocket(app: Fastify, eventRouter: EventRouter) {
sessionUpdateHandler(userId, socket, connection, eventRouter);
pingHandler(socket);
machineUpdateHandler(userId, socket, eventRouter);
artifactUpdateHandler(userId, socket, eventRouter);
// Ready
log({ module: 'websocket' }, `User connected: ${userId}`);

View File

@ -0,0 +1,407 @@
import { websocketEventsCounter } from "@/app/monitoring/metrics2";
import { buildNewArtifactUpdate, buildUpdateArtifactUpdate, buildDeleteArtifactUpdate, EventRouter } from "@/app/events/eventRouter";
import { db } from "@/storage/db";
import { allocateUserSeq } from "@/storage/seq";
import { log } from "@/utils/log";
import { randomKeyNaked } from "@/utils/randomKeyNaked";
import { Socket } from "socket.io";
import * as privacyKit from "privacy-kit";
export function artifactUpdateHandler(userId: string, socket: Socket, eventRouter: EventRouter) {
// Read artifact with full body
socket.on('artifact-read', async (data: {
artifactId: string;
}, callback: (response: any) => void) => {
try {
websocketEventsCounter.inc({ event_type: 'artifact-read' });
const { artifactId } = data;
// Validate input
if (!artifactId) {
if (callback) {
callback({ result: 'error', message: 'Invalid parameters' });
}
return;
}
// Fetch artifact
const artifact = await db.artifact.findFirst({
where: {
id: artifactId,
accountId: userId
}
});
if (!artifact) {
if (callback) {
callback({ result: 'error', message: 'Artifact not found' });
}
return;
}
// Return artifact data
callback({
result: 'success',
artifact: {
id: artifact.id,
header: privacyKit.encodeBase64(artifact.header),
headerVersion: artifact.headerVersion,
body: privacyKit.encodeBase64(artifact.body),
bodyVersion: artifact.bodyVersion,
seq: artifact.seq,
createdAt: artifact.createdAt.getTime(),
updatedAt: artifact.updatedAt.getTime()
}
});
} catch (error) {
log({ module: 'websocket', level: 'error' }, `Error in artifact-read: ${error}`);
if (callback) {
callback({ result: 'error', message: 'Internal error' });
}
}
});
// Update artifact with optimistic concurrency control
socket.on('artifact-update', async (data: {
artifactId: string;
header?: {
data: string;
expectedVersion: number;
};
body?: {
data: string;
expectedVersion: number;
};
}, callback: (response: any) => void) => {
try {
websocketEventsCounter.inc({ event_type: 'artifact-update' });
const { artifactId, header, body } = data;
// Validate input
if (!artifactId) {
if (callback) {
callback({ result: 'error', message: 'Invalid parameters' });
}
return;
}
// At least one update must be provided
if (!header && !body) {
if (callback) {
callback({ result: 'error', message: 'No updates provided' });
}
return;
}
// Validate header structure if provided
if (header && (typeof header.data !== 'string' || typeof header.expectedVersion !== 'number')) {
if (callback) {
callback({ result: 'error', message: 'Invalid header parameters' });
}
return;
}
// Validate body structure if provided
if (body && (typeof body.data !== 'string' || typeof body.expectedVersion !== 'number')) {
if (callback) {
callback({ result: 'error', message: 'Invalid body parameters' });
}
return;
}
// Get current artifact
const currentArtifact = await db.artifact.findFirst({
where: {
id: artifactId,
accountId: userId
}
});
if (!currentArtifact) {
if (callback) {
callback({ result: 'error', message: 'Artifact not found' });
}
return;
}
// Check for version mismatches
const headerMismatch = header && currentArtifact.headerVersion !== header.expectedVersion;
const bodyMismatch = body && currentArtifact.bodyVersion !== body.expectedVersion;
if (headerMismatch || bodyMismatch) {
const response: any = { result: 'version-mismatch' };
if (headerMismatch) {
response.header = {
currentVersion: currentArtifact.headerVersion,
currentData: privacyKit.encodeBase64(currentArtifact.header)
};
}
if (bodyMismatch) {
response.body = {
currentVersion: currentArtifact.bodyVersion,
currentData: privacyKit.encodeBase64(currentArtifact.body)
};
}
callback(response);
return;
}
// Build update data
const updateData: any = {
updatedAt: new Date(),
seq: currentArtifact.seq + 1
};
let headerUpdate: { value: string; version: number } | undefined;
let bodyUpdate: { value: string; version: number } | undefined;
if (header) {
updateData.header = privacyKit.decodeBase64(header.data);
updateData.headerVersion = header.expectedVersion + 1;
headerUpdate = {
value: header.data,
version: header.expectedVersion + 1
};
}
if (body) {
updateData.body = privacyKit.decodeBase64(body.data);
updateData.bodyVersion = body.expectedVersion + 1;
bodyUpdate = {
value: body.data,
version: body.expectedVersion + 1
};
}
// Perform atomic update with version check
const { count } = await db.artifact.updateMany({
where: {
id: artifactId,
accountId: userId,
...(header && { headerVersion: header.expectedVersion }),
...(body && { bodyVersion: body.expectedVersion })
},
data: updateData
});
if (count === 0) {
// Re-fetch current version
const current = await db.artifact.findFirst({
where: {
id: artifactId,
accountId: userId
}
});
const response: any = { result: 'version-mismatch' };
if (header && current) {
response.header = {
currentVersion: current.headerVersion,
currentData: privacyKit.encodeBase64(current.header)
};
}
if (body && current) {
response.body = {
currentVersion: current.bodyVersion,
currentData: privacyKit.encodeBase64(current.body)
};
}
callback(response);
return;
}
// Emit update event
const updSeq = await allocateUserSeq(userId);
const updatePayload = buildUpdateArtifactUpdate(artifactId, updSeq, randomKeyNaked(12), headerUpdate, bodyUpdate);
eventRouter.emitUpdate({
userId,
payload: updatePayload,
recipientFilter: { type: 'all-user-authenticated-connections' }
});
// Send success response
const response: any = { result: 'success' };
if (headerUpdate) {
response.header = {
version: headerUpdate.version,
data: header!.data
};
}
if (bodyUpdate) {
response.body = {
version: bodyUpdate.version,
data: body!.data
};
}
callback(response);
} catch (error) {
log({ module: 'websocket', level: 'error' }, `Error in artifact-update: ${error}`);
if (callback) {
callback({ result: 'error', message: 'Internal error' });
}
}
});
// Create new artifact
socket.on('artifact-create', async (data: {
id: string;
header: string;
body: string;
dataEncryptionKey: string;
}, callback: (response: any) => void) => {
try {
websocketEventsCounter.inc({ event_type: 'artifact-create' });
const { id, header, body, dataEncryptionKey } = data;
// Validate input
if (!id || typeof header !== 'string' || typeof body !== 'string' || typeof dataEncryptionKey !== 'string') {
if (callback) {
callback({ result: 'error', message: 'Invalid parameters' });
}
return;
}
// Check if artifact already exists
const existingArtifact = await db.artifact.findUnique({
where: { id }
});
if (existingArtifact) {
// If exists for another account, return error
if (existingArtifact.accountId !== userId) {
if (callback) {
callback({ result: 'error', message: 'Artifact with this ID already exists for another account' });
}
return;
}
// If exists for same account, return existing (idempotent)
callback({
result: 'success',
artifact: {
id: existingArtifact.id,
header: privacyKit.encodeBase64(existingArtifact.header),
headerVersion: existingArtifact.headerVersion,
body: privacyKit.encodeBase64(existingArtifact.body),
bodyVersion: existingArtifact.bodyVersion,
seq: existingArtifact.seq,
createdAt: existingArtifact.createdAt.getTime(),
updatedAt: existingArtifact.updatedAt.getTime()
}
});
return;
}
// Create new artifact
const artifact = await db.artifact.create({
data: {
id,
accountId: userId,
header: privacyKit.decodeBase64(header),
headerVersion: 1,
body: privacyKit.decodeBase64(body),
bodyVersion: 1,
dataEncryptionKey: privacyKit.decodeBase64(dataEncryptionKey),
seq: 0
}
});
// Emit new-artifact event
const updSeq = await allocateUserSeq(userId);
const newArtifactPayload = buildNewArtifactUpdate(artifact, updSeq, randomKeyNaked(12));
eventRouter.emitUpdate({
userId,
payload: newArtifactPayload,
recipientFilter: { type: 'all-user-authenticated-connections' }
});
// Return created artifact
callback({
result: 'success',
artifact: {
id: artifact.id,
header: privacyKit.encodeBase64(artifact.header),
headerVersion: artifact.headerVersion,
body: privacyKit.encodeBase64(artifact.body),
bodyVersion: artifact.bodyVersion,
seq: artifact.seq,
createdAt: artifact.createdAt.getTime(),
updatedAt: artifact.updatedAt.getTime()
}
});
} catch (error) {
log({ module: 'websocket', level: 'error' }, `Error in artifact-create: ${error}`);
if (callback) {
callback({ result: 'error', message: 'Internal error' });
}
}
});
// Delete artifact
socket.on('artifact-delete', async (data: {
artifactId: string;
}, callback: (response: any) => void) => {
try {
websocketEventsCounter.inc({ event_type: 'artifact-delete' });
const { artifactId } = data;
// Validate input
if (!artifactId) {
if (callback) {
callback({ result: 'error', message: 'Invalid parameters' });
}
return;
}
// Check if artifact exists and belongs to user
const artifact = await db.artifact.findFirst({
where: {
id: artifactId,
accountId: userId
}
});
if (!artifact) {
if (callback) {
callback({ result: 'error', message: 'Artifact not found' });
}
return;
}
// Delete artifact
await db.artifact.delete({
where: { id: artifactId }
});
// Emit delete-artifact event
const updSeq = await allocateUserSeq(userId);
const deletePayload = buildDeleteArtifactUpdate(artifactId, updSeq, randomKeyNaked(12));
eventRouter.emitUpdate({
userId,
payload: deletePayload,
recipientFilter: { type: 'all-user-authenticated-connections' }
});
// Send success response
callback({ result: 'success' });
} catch (error) {
log({ module: 'websocket', level: 'error' }, `Error in artifact-delete: ${error}`);
if (callback) {
callback({ result: 'error', message: 'Internal error' });
}
}
});
}

View File

@ -105,6 +105,31 @@ export type UpdateEvent = {
version: number;
};
activeAt?: number;
} | {
type: 'new-artifact';
artifactId: string;
seq: number;
header: string;
headerVersion: number;
body: string;
bodyVersion: number;
dataEncryptionKey: string | null;
createdAt: number;
updatedAt: number;
} | {
type: 'update-artifact';
artifactId: string;
header?: {
value: string;
version: number;
};
body?: {
value: string;
version: number;
};
} | {
type: 'delete-artifact';
artifactId: string;
};
// === EPHEMERAL EVENT TYPES (Transient) ===
@ -447,4 +472,60 @@ export function buildMachineStatusEphemeral(machineId: string, online: boolean):
online,
timestamp: Date.now()
};
}
export function buildNewArtifactUpdate(artifact: {
id: string;
seq: number;
header: Uint8Array;
headerVersion: number;
body: Uint8Array;
bodyVersion: number;
dataEncryptionKey: Uint8Array;
createdAt: Date;
updatedAt: Date;
}, updateSeq: number, updateId: string): UpdatePayload {
return {
id: updateId,
seq: updateSeq,
body: {
t: 'new-artifact',
artifactId: artifact.id,
seq: artifact.seq,
header: Buffer.from(artifact.header).toString('base64'),
headerVersion: artifact.headerVersion,
body: Buffer.from(artifact.body).toString('base64'),
bodyVersion: artifact.bodyVersion,
dataEncryptionKey: Buffer.from(artifact.dataEncryptionKey).toString('base64'),
createdAt: artifact.createdAt.getTime(),
updatedAt: artifact.updatedAt.getTime()
},
createdAt: Date.now()
};
}
export function buildUpdateArtifactUpdate(artifactId: string, updateSeq: number, updateId: string, header?: { value: string; version: number }, body?: { value: string; version: number }): UpdatePayload {
return {
id: updateId,
seq: updateSeq,
body: {
t: 'update-artifact',
artifactId,
header,
body
},
createdAt: Date.now()
};
}
export function buildDeleteArtifactUpdate(artifactId: string, updateSeq: number, updateId: string): UpdatePayload {
return {
id: updateId,
seq: updateSeq,
body: {
t: 'delete-artifact',
artifactId
},
createdAt: Date.now()
};
}

View File

@ -11,4 +11,19 @@ export type AccountProfile = {
version: number;
} | null;
connectedServices: string[];
}
export type ArtifactInfo = {
id: string;
header: string;
headerVersion: number;
dataEncryptionKey: string;
seq: number;
createdAt: number;
updatedAt: number;
}
export type Artifact = ArtifactInfo & {
body: string;
bodyVersion: number;
}