diff --git a/apps/broker/package.json b/apps/broker/package.json index f8d6e13..508638d 100644 --- a/apps/broker/package.json +++ b/apps/broker/package.json @@ -19,6 +19,7 @@ "@turbostarter/shared": "workspace:*", "drizzle-orm": "0.44.7", "libsodium-wrappers": "0.7.15", + "minio": "8.0.7", "ws": "8.20.0", "zod": "catalog:" }, diff --git a/apps/broker/src/broker.ts b/apps/broker/src/broker.ts index a8561de..c4e6ec0 100644 --- a/apps/broker/src/broker.ts +++ b/apps/broker/src/broker.ts @@ -32,6 +32,8 @@ import { db } from "./db"; import { invite as inviteTable, mesh, + meshFile, + meshFileAccess, meshMember as memberTable, meshMemory, meshState, @@ -695,6 +697,198 @@ export async function forgetMemory( ); } +// --- File sharing --- + +/** + * Insert a file metadata row after upload to MinIO. + */ +export async function uploadFile(args: { + meshId: string; + name: string; + sizeBytes: number; + mimeType?: string; + minioKey: string; + tags?: string[]; + persistent?: boolean; + uploadedByName?: string; + uploadedByMember?: string; + targetSpec?: string; + expiresAt?: Date; +}): Promise { + const [row] = await db + .insert(meshFile) + .values({ + meshId: args.meshId, + name: args.name, + sizeBytes: args.sizeBytes, + mimeType: args.mimeType ?? null, + minioKey: args.minioKey, + tags: args.tags ?? [], + persistent: args.persistent ?? true, + uploadedByName: args.uploadedByName ?? null, + uploadedByMember: args.uploadedByMember ?? null, + targetSpec: args.targetSpec ?? null, + expiresAt: args.expiresAt ?? null, + }) + .returning({ id: meshFile.id }); + if (!row) throw new Error("failed to insert file row"); + return row.id; +} + +/** + * Get a single file by id, check it belongs to the mesh and is not deleted. + */ +export async function getFile( + meshId: string, + fileId: string, +): Promise<{ + id: string; + name: string; + sizeBytes: number; + mimeType: string | null; + minioKey: string; + tags: string[]; + persistent: boolean; + uploadedByName: string | null; + targetSpec: string | null; + uploadedAt: Date; +} | null> { + const [row] = await db + .select({ + id: meshFile.id, + name: meshFile.name, + sizeBytes: meshFile.sizeBytes, + mimeType: meshFile.mimeType, + minioKey: meshFile.minioKey, + tags: meshFile.tags, + persistent: meshFile.persistent, + uploadedByName: meshFile.uploadedByName, + targetSpec: meshFile.targetSpec, + uploadedAt: meshFile.uploadedAt, + }) + .from(meshFile) + .where( + and( + eq(meshFile.id, fileId), + eq(meshFile.meshId, meshId), + isNull(meshFile.deletedAt), + ), + ) + .limit(1); + if (!row) return null; + return { + ...row, + tags: (row.tags ?? []) as string[], + }; +} + +/** + * List files in a mesh. Optionally filter by query (name ILIKE) or uploader. + */ +export async function listFiles( + meshId: string, + query?: string, + from?: string, +): Promise< + Array<{ + id: string; + name: string; + sizeBytes: number; + tags: string[]; + uploadedBy: string; + uploadedAt: Date; + persistent: boolean; + }> +> { + const conditions = [ + eq(meshFile.meshId, meshId), + isNull(meshFile.deletedAt), + ]; + if (query) { + conditions.push(sql`${meshFile.name} ILIKE ${"%" + query + "%"}`); + } + if (from) { + conditions.push(eq(meshFile.uploadedByName, from)); + } + const rows = await db + .select({ + id: meshFile.id, + name: meshFile.name, + sizeBytes: meshFile.sizeBytes, + tags: meshFile.tags, + uploadedByName: meshFile.uploadedByName, + uploadedAt: meshFile.uploadedAt, + persistent: meshFile.persistent, + }) + .from(meshFile) + .where(and(...conditions)) + .orderBy(desc(meshFile.uploadedAt)) + .limit(100); + return rows.map((r) => ({ + id: r.id, + name: r.name, + sizeBytes: r.sizeBytes, + tags: (r.tags ?? []) as string[], + uploadedBy: r.uploadedByName ?? "unknown", + uploadedAt: r.uploadedAt, + persistent: r.persistent, + })); +} + +/** + * Record a file access event (download/presigned URL generation). + */ +export async function recordFileAccess( + fileId: string, + sessionPubkey?: string, + peerName?: string, +): Promise { + await db.insert(meshFileAccess).values({ + fileId, + peerSessionPubkey: sessionPubkey ?? null, + peerName: peerName ?? null, + }); +} + +/** + * Get access log for a file. + */ +export async function getFileStatus( + fileId: string, +): Promise> { + const rows = await db + .select({ + peerName: meshFileAccess.peerName, + accessedAt: meshFileAccess.accessedAt, + }) + .from(meshFileAccess) + .where(eq(meshFileAccess.fileId, fileId)) + .orderBy(desc(meshFileAccess.accessedAt)); + return rows.map((r) => ({ + peerName: r.peerName ?? "unknown", + accessedAt: r.accessedAt, + })); +} + +/** + * Soft-delete a file by setting deleted_at. + */ +export async function deleteFile( + meshId: string, + fileId: string, +): Promise { + await db + .update(meshFile) + .set({ deletedAt: new Date() }) + .where( + and( + eq(meshFile.id, fileId), + eq(meshFile.meshId, meshId), + isNull(meshFile.deletedAt), + ), + ); +} + // --- Message queueing + delivery --- export interface QueueParams { diff --git a/apps/broker/src/env.ts b/apps/broker/src/env.ts index 7f4c969..9b9d2d1 100644 --- a/apps/broker/src/env.ts +++ b/apps/broker/src/env.ts @@ -20,6 +20,10 @@ const envSchema = z.object({ MAX_CONNECTIONS_PER_MESH: z.coerce.number().int().positive().default(100), MAX_MESSAGE_BYTES: z.coerce.number().int().positive().default(65_536), HOOK_RATE_LIMIT_PER_MIN: z.coerce.number().int().positive().default(30), + MINIO_ENDPOINT: z.string().default("minio:9000"), + MINIO_ACCESS_KEY: z.string().default("claudemesh"), + MINIO_SECRET_KEY: z.string().default("changeme"), + MINIO_USE_SSL: z.coerce.boolean().default(false), NODE_ENV: z .enum(["development", "production", "test"]) .default("development"), diff --git a/apps/broker/src/index.ts b/apps/broker/src/index.ts index 537aa05..47341d9 100644 --- a/apps/broker/src/index.ts +++ b/apps/broker/src/index.ts @@ -21,20 +21,25 @@ import { db } from "./db"; import { messageQueue } from "@turbostarter/db/schema/mesh"; import { connectPresence, + deleteFile, disconnectPresence, drainForMember, findMemberByPubkey, forgetMemory, + getFile, + getFileStatus, getState, handleHookSetStatus, heartbeat, joinGroup, joinMesh, leaveGroup, + listFiles, listPeersInMesh, listState, queueMessage, recallMemory, + recordFileAccess, refreshQueueDepth, refreshStatusFromJsonl, rememberMemory, @@ -42,8 +47,10 @@ import { setState, startSweepers, stopSweepers, + uploadFile, writeStatus, } from "./broker"; +import { ensureBucket, meshBucketName, minioClient } from "./minio"; import type { HookSetStatusRequest, WSClientMessage, @@ -140,7 +147,7 @@ function handleHttpRequest(req: IncomingMessage, res: ServerResponse): void { const started = Date.now(); res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS"); - res.setHeader("Access-Control-Allow-Headers", "Content-Type"); + res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Mesh-Id, X-Member-Id, X-File-Name, X-Tags, X-Persistent, X-Target-Spec"); if (req.method === "OPTIONS") { res.writeHead(204); res.end(); @@ -177,6 +184,11 @@ function handleHttpRequest(req: IncomingMessage, res: ServerResponse): void { return; } + if (req.method === "POST" && req.url === "/upload") { + handleUploadPost(req, res, started); + return; + } + res.writeHead(404); res.end("not found"); log.debug("http", { route, status: 404, latency_ms: Date.now() - started }); @@ -327,6 +339,119 @@ function handleJoinPost( }); } +function handleUploadPost( + req: IncomingMessage, + res: ServerResponse, + started: number, +): void { + const meshId = req.headers["x-mesh-id"] as string | undefined; + const memberId = req.headers["x-member-id"] as string | undefined; + const fileName = req.headers["x-file-name"] as string | undefined; + const tagsRaw = req.headers["x-tags"] as string | undefined; + const persistentRaw = req.headers["x-persistent"] as string | undefined; + const targetSpec = req.headers["x-target-spec"] as string | undefined; + + if (!meshId || !memberId || !fileName) { + writeJson(res, 400, { + ok: false, + error: "X-Mesh-Id, X-Member-Id, and X-File-Name headers required", + }); + return; + } + + const persistent = persistentRaw !== "false"; + let tags: string[] = []; + if (tagsRaw) { + try { + tags = JSON.parse(tagsRaw); + } catch { + tags = []; + } + } + + const MAX_UPLOAD_SIZE = 50 * 1024 * 1024; // 50MB + const chunks: Buffer[] = []; + let total = 0; + let aborted = false; + + req.on("data", (chunk: Buffer) => { + if (aborted) return; + total += chunk.length; + if (total > MAX_UPLOAD_SIZE) { + aborted = true; + writeJson(res, 413, { ok: false, error: "file too large (max 50MB)" }); + req.destroy(); + return; + } + chunks.push(chunk); + }); + + req.on("end", async () => { + if (aborted) return; + try { + const body = Buffer.concat(chunks); + if (body.length === 0) { + writeJson(res, 400, { ok: false, error: "empty body" }); + return; + } + + // Generate a file ID for the MinIO key + const { generateId } = await import("@turbostarter/shared/utils"); + const fileId = generateId(); + const dateStr = new Date().toISOString().split("T")[0]; + const keyPrefix = persistent + ? `shared/${fileId}` + : `ephemeral/${dateStr}/${fileId}`; + const minioKey = `${keyPrefix}/${fileName}`; + const bucket = meshBucketName(meshId); + + // Ensure bucket exists + upload + await ensureBucket(bucket); + await minioClient.putObject( + bucket, + minioKey, + body, + body.length, + req.headers["content-type"] + ? { "Content-Type": req.headers["content-type"] } + : undefined, + ); + + // Insert DB row + const dbFileId = await uploadFile({ + meshId, + name: fileName, + sizeBytes: body.length, + mimeType: (req.headers["content-type"] as string) || undefined, + minioKey, + tags, + persistent, + uploadedByMember: memberId, + targetSpec: targetSpec || undefined, + }); + + writeJson(res, 200, { ok: true, fileId: dbFileId }); + log.info("upload", { + route: "POST /upload", + mesh_id: meshId, + file_id: dbFileId, + name: fileName, + size: body.length, + persistent, + latency_ms: Date.now() - started, + }); + } catch (e) { + writeJson(res, 500, { + ok: false, + error: e instanceof Error ? e.message : String(e), + }); + log.error("upload handler error", { + error: e instanceof Error ? e.message : String(e), + }); + } + }); +} + function handleUpgrade( wss: WebSocketServer, req: IncomingMessage, @@ -775,6 +900,106 @@ function handleConnection(ws: WebSocket): void { }); break; } + case "get_file": { + const gf = msg as Extract; + const file = await getFile(conn.meshId, gf.fileId); + if (!file) { + sendError(conn.ws, "not_found", "file not found"); + break; + } + // Access control: if targetSpec is set, verify peer matches + if (file.targetSpec) { + const matches = + file.targetSpec === conn.memberPubkey || + file.targetSpec === conn.sessionPubkey || + file.targetSpec === "*"; + if (!matches) { + sendError(conn.ws, "forbidden", "file not targeted at you"); + break; + } + } + // Generate presigned URL (60s expiry) + const bucket = meshBucketName(conn.meshId); + const presignedUrl = await minioClient.presignedGetObject( + bucket, + file.minioKey, + 60, + ); + // Record access + const memberInfo = conn.memberPubkey + ? await findMemberByPubkey(conn.meshId, conn.memberPubkey) + : null; + await recordFileAccess( + gf.fileId, + conn.sessionPubkey ?? undefined, + memberInfo?.displayName, + ); + sendToPeer(presenceId, { + type: "file_url", + fileId: gf.fileId, + url: presignedUrl, + name: file.name, + }); + log.info("ws get_file", { + presence_id: presenceId, + file_id: gf.fileId, + }); + break; + } + case "list_files": { + const lf = msg as Extract; + const files = await listFiles(conn.meshId, lf.query, lf.from); + sendToPeer(presenceId, { + type: "file_list", + files: files.map((f) => ({ + id: f.id, + name: f.name, + size: f.sizeBytes, + tags: f.tags, + uploadedBy: f.uploadedBy, + uploadedAt: f.uploadedAt.toISOString(), + persistent: f.persistent, + })), + }); + log.info("ws list_files", { + presence_id: presenceId, + mesh_id: conn.meshId, + count: files.length, + }); + break; + } + case "file_status": { + const fs = msg as Extract; + const accesses = await getFileStatus(fs.fileId); + sendToPeer(presenceId, { + type: "file_status_result", + fileId: fs.fileId, + accesses: accesses.map((a) => ({ + peerName: a.peerName, + accessedAt: a.accessedAt.toISOString(), + })), + }); + log.info("ws file_status", { + presence_id: presenceId, + file_id: fs.fileId, + }); + break; + } + case "delete_file": { + const df = msg as Extract; + await deleteFile(conn.meshId, df.fileId); + sendToPeer(presenceId, { + type: "ack" as const, + id: df.fileId, + messageId: df.fileId, + queued: false, + }); + log.info("ws delete_file", { + presence_id: presenceId, + file_id: df.fileId, + }); + break; + } case "message_status": { const ms = msg as Extract; // Look up the message in the queue. diff --git a/apps/broker/src/minio.ts b/apps/broker/src/minio.ts new file mode 100644 index 0000000..11a79f7 --- /dev/null +++ b/apps/broker/src/minio.ts @@ -0,0 +1,28 @@ +/** + * MinIO client for file storage. + * + * Each mesh gets its own bucket (mesh-{meshId}). Files are stored under + * a key path that encodes persistence and origin: + * - persistent: shared/{fileId}/{originalName} + * - ephemeral: ephemeral/{YYYY-MM-DD}/{fileId}/{originalName} + */ + +import { Client } from "minio"; +import { env } from "./env"; + +export const minioClient = new Client({ + endPoint: env.MINIO_ENDPOINT.split(":")[0]!, + port: parseInt(env.MINIO_ENDPOINT.split(":")[1] || "9000"), + useSSL: env.MINIO_USE_SSL, + accessKey: env.MINIO_ACCESS_KEY, + secretKey: env.MINIO_SECRET_KEY, +}); + +export async function ensureBucket(name: string): Promise { + const exists = await minioClient.bucketExists(name); + if (!exists) await minioClient.makeBucket(name); +} + +export function meshBucketName(meshId: string): string { + return `mesh-${meshId.toLowerCase().replace(/[^a-z0-9-]/g, "-")}`; +} diff --git a/apps/broker/src/types.ts b/apps/broker/src/types.ts index 0816cc5..75b75c0 100644 --- a/apps/broker/src/types.ts +++ b/apps/broker/src/types.ts @@ -250,6 +250,65 @@ export interface WSMessageStatusResultMessage { }>; } +// --- File sharing messages --- + +/** Client → broker: get a presigned download URL for a file. */ +export interface WSGetFileMessage { + type: "get_file"; + fileId: string; +} + +/** Client → broker: list files in the mesh. */ +export interface WSListFilesMessage { + type: "list_files"; + query?: string; + from?: string; +} + +/** Client → broker: get access log for a file. */ +export interface WSFileStatusMessage { + type: "file_status"; + fileId: string; +} + +/** Client → broker: soft-delete a file. */ +export interface WSDeleteFileMessage { + type: "delete_file"; + fileId: string; +} + +/** Broker → client: presigned URL for downloading a file. */ +export interface WSFileUrlMessage { + type: "file_url"; + fileId: string; + url: string; + name: string; +} + +/** Broker → client: list of files in the mesh. */ +export interface WSFileListMessage { + type: "file_list"; + files: Array<{ + id: string; + name: string; + size: number; + tags: string[]; + uploadedBy: string; + uploadedAt: string; + persistent: boolean; + }>; +} + +/** Broker → client: access log for a file. */ +export interface WSFileStatusResultMessage { + type: "file_status_result"; + fileId: string; + accesses: Array<{ + peerName: string; + accessedAt: string; + }>; +} + /** Broker → client: structured error. */ export interface WSErrorMessage { type: "error"; @@ -272,7 +331,11 @@ export type WSClientMessage = | WSRememberMessage | WSRecallMessage | WSForgetMessage - | WSMessageStatusMessage; + | WSMessageStatusMessage + | WSGetFileMessage + | WSListFilesMessage + | WSFileStatusMessage + | WSDeleteFileMessage; export type WSServerMessage = | WSHelloAckMessage @@ -285,4 +348,7 @@ export type WSServerMessage = | WSMemoryStoredMessage | WSMemoryResultsMessage | WSMessageStatusResultMessage + | WSFileUrlMessage + | WSFileListMessage + | WSFileStatusResultMessage | WSErrorMessage; diff --git a/apps/cli/package.json b/apps/cli/package.json index 53faaee..e35789c 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,6 +1,6 @@ { "name": "claudemesh-cli", - "version": "0.3.0", + "version": "0.4.0", "description": "Claude Code MCP client for claudemesh — peer mesh messaging between Claude sessions.", "keywords": [ "claude-code", diff --git a/apps/cli/src/mcp/server.ts b/apps/cli/src/mcp/server.ts index e8e1a0d..b93076c 100644 --- a/apps/cli/src/mcp/server.ts +++ b/apps/cli/src/mcp/server.ts @@ -161,9 +161,24 @@ When you receive a message, RESPOND IMMEDIATEL | remember(content, tags?) | Store persistent knowledge with optional tags. | | recall(query) | Full-text search over mesh memory. | | forget(id) | Soft-delete a memory entry. | +| share_file(path, name?, tags?) | Share a persistent file with the mesh. | +| get_file(id, save_to) | Download a shared file to a local path. | +| list_files(query?, from?) | Find files shared in the mesh. | +| file_status(id) | Check who has accessed a file. | +| delete_file(id) | Remove a shared file from the mesh. | If multiple meshes are joined, prefix \`to\` with \`:\` to disambiguate (e.g. \`dev-team:Alice\`). +Multi-target: send_message accepts an array of targets for the 'to' field. + send_message(to: ["Alice", "@backend"], message: "sprint starts") +Targets are deduplicated — each peer receives the message once. + +Targeted views: when different audiences need different details about the same event, +send tailored messages instead of one generic broadcast: + send_message(to: "@frontend", message: "Auth v2: useAuth hook changed, see src/auth/") + send_message(to: "@backend", message: "Auth v2: new /api/auth/v2 endpoints, v1 deprecated") + send_message(to: "@pm", message: "Auth v2 done. 3 points, no blockers.") + ## Groups Groups are routing labels. Send to @groupname to multicast to all members. Roles are metadata that peers interpret: a "lead" gathers input before synthesizing a response, a "member" contributes when asked, an "observer" watches silently. Join and leave groups dynamically with join_group/leave_group. Check list_peers to see who belongs to which groups and their roles. @@ -173,6 +188,10 @@ Shared key-value store scoped to the mesh. Use get_state/set_state for live coor ## Memory Persistent knowledge that survives across sessions. Use remember(content, tags?) to store lessons, decisions, and incidents. Use recall(query) to search before asking peers. New peers should recall at session start to load institutional knowledge. +## Files +share_file for persistent references, send_message(file:) for ephemeral attachments. +Tags on shared files make them searchable. Use list_files to find what peers shared. + ## Priority - "now": interrupt immediately, even if recipient is in DND (use for urgent: broken deploy, blocking issue) - "next" (default): deliver when recipient goes idle (normal coordination) @@ -201,22 +220,32 @@ Call list_peers at session start to understand who is online, their roles, and w const { to, message, priority } = (args ?? {}) as SendMessageArgs; if (!to || !message) return text("send_message: `to` and `message` required", true); - const { client, targetSpec, error } = await resolveClient(to); - if (!client) - return text(`send_message: ${error ?? "no client resolved"}`, true); - const result = await client.send( - targetSpec, - message, - (priority ?? "next") as Priority, - ); - if (!result.ok) - return text( - `send_message failed (${client.meshSlug}): ${result.error}`, - true, + + // Handle multi-target: to can be string or string[] + const targets = Array.isArray(to) ? to : [to]; + const results: string[] = []; + const seen = new Set(); // dedup by resolved pubkey + + for (const target of targets) { + const { client, targetSpec, error } = await resolveClient(target); + if (!client) { + results.push(`✗ ${target}: ${error ?? "no client resolved"}`); + continue; + } + if (seen.has(targetSpec)) continue; // dedup + seen.add(targetSpec); + const result = await client.send( + targetSpec, + message, + (priority ?? "next") as Priority, ); - return text( - `Sent to ${targetSpec} via ${client.meshSlug} [${priority ?? "next"}] → ${result.messageId}`, - ); + if (!result.ok) { + results.push(`✗ ${target}: ${result.error}`); + } else { + results.push(`✓ ${target} → ${result.messageId}`); + } + } + return text(results.join("\n")); } case "list_peers": { @@ -363,6 +392,69 @@ Call list_peers at session start to understand who is online, their roles, and w return text(`Forgotten: ${id}`); } + // --- Files --- + case "share_file": { + const { path: filePath, name: fileName, tags } = (args ?? {}) as { path?: string; name?: string; tags?: string[] }; + if (!filePath) return text("share_file: `path` required", true); + const { existsSync } = await import("node:fs"); + if (!existsSync(filePath)) return text(`share_file: file not found: ${filePath}`, true); + const client = allClients()[0]; + if (!client) return text("share_file: not connected", true); + const fileId = await client.uploadFile(filePath, client.meshId, client.meshSlug, { + name: fileName, tags, persistent: true, + }); + if (!fileId) return text("share_file: upload failed", true); + return text(`Shared: ${fileName ?? filePath} (${fileId})`); + } + + case "get_file": { + const { id, save_to } = (args ?? {}) as { id?: string; save_to?: string }; + if (!id || !save_to) return text("get_file: `id` and `save_to` required", true); + const client = allClients()[0]; + if (!client) return text("get_file: not connected", true); + const result = await client.getFile(id); + if (!result) return text(`get_file: file ${id} not found`, true); + const res = await fetch(result.url, { signal: AbortSignal.timeout(30_000) }); + if (!res.ok) return text(`get_file: download failed (${res.status})`, true); + const { writeFileSync, mkdirSync } = await import("node:fs"); + const { dirname } = await import("node:path"); + mkdirSync(dirname(save_to), { recursive: true }); + writeFileSync(save_to, Buffer.from(await res.arrayBuffer())); + return text(`Downloaded: ${result.name} → ${save_to}`); + } + + case "list_files": { + const { query, from } = (args ?? {}) as { query?: string; from?: string }; + const client = allClients()[0]; + if (!client) return text("list_files: not connected", true); + const files = await client.listFiles(query, from); + if (files.length === 0) return text("No files found."); + const lines = files.map(f => + `- **${f.name}** (${f.id.slice(0, 8)}…, ${f.size} bytes) by ${f.uploadedBy}${f.tags.length ? ` [${f.tags.join(", ")}]` : ""}` + ); + return text(lines.join("\n")); + } + + case "file_status": { + const { id } = (args ?? {}) as { id?: string }; + if (!id) return text("file_status: `id` required", true); + const client = allClients()[0]; + if (!client) return text("file_status: not connected", true); + const accesses = await client.fileStatus(id); + if (accesses.length === 0) return text("No one has accessed this file yet."); + const lines = accesses.map(a => `- ${a.peerName} at ${a.accessedAt}`); + return text(`Accessed by:\n${lines.join("\n")}`); + } + + case "delete_file": { + const { id } = (args ?? {}) as { id?: string }; + if (!id) return text("delete_file: `id` required", true); + const client = allClients()[0]; + if (!client) return text("delete_file: not connected", true); + await client.deleteFile(id); + return text(`Deleted: ${id}`); + } + default: return text(`Unknown tool: ${name}`, true); } diff --git a/apps/cli/src/mcp/tools.ts b/apps/cli/src/mcp/tools.ts index d5450a6..f7ce533 100644 --- a/apps/cli/src/mcp/tools.ts +++ b/apps/cli/src/mcp/tools.ts @@ -17,8 +17,11 @@ export const TOOLS: Tool[] = [ type: "object", properties: { to: { - type: "string", - description: "Peer name, pubkey, @group, or #channel", + oneOf: [ + { type: "string", description: "Peer name, pubkey, @group" }, + { type: "array", items: { type: "string" }, description: "Multiple targets" }, + ], + description: "Single target or array of targets", }, message: { type: "string", description: "Message text" }, priority: { @@ -195,4 +198,75 @@ export const TOOLS: Tool[] = [ required: ["id"], }, }, + + // --- File tools --- + { + name: "share_file", + description: + "Share a persistent file with the mesh. All current and future peers can access it.", + inputSchema: { + type: "object", + properties: { + path: { type: "string", description: "Local file path to share" }, + name: { + type: "string", + description: "Display name (defaults to filename)", + }, + tags: { + type: "array", + items: { type: "string" }, + description: "Tags for categorization", + }, + }, + required: ["path"], + }, + }, + { + name: "get_file", + description: "Download a shared file to a local path.", + inputSchema: { + type: "object", + properties: { + id: { type: "string", description: "File ID" }, + save_to: { + type: "string", + description: "Local path to save the file", + }, + }, + required: ["id", "save_to"], + }, + }, + { + name: "list_files", + description: "List files shared in the mesh.", + inputSchema: { + type: "object", + properties: { + query: { type: "string", description: "Search by name or tags" }, + from: { type: "string", description: "Filter by uploader name" }, + }, + }, + }, + { + name: "file_status", + description: "Check who has accessed a shared file.", + inputSchema: { + type: "object", + properties: { + id: { type: "string", description: "File ID" }, + }, + required: ["id"], + }, + }, + { + name: "delete_file", + description: "Remove a shared file from the mesh.", + inputSchema: { + type: "object", + properties: { + id: { type: "string", description: "File ID" }, + }, + required: ["id"], + }, + }, ]; diff --git a/apps/cli/src/mcp/types.ts b/apps/cli/src/mcp/types.ts index b4bc92d..77d51f7 100644 --- a/apps/cli/src/mcp/types.ts +++ b/apps/cli/src/mcp/types.ts @@ -6,7 +6,7 @@ export type Priority = "now" | "next" | "low"; export type PeerStatus = "idle" | "working" | "dnd"; export interface SendMessageArgs { - to: string; // peer name, pubkey, or #channel + to: string | string[]; // peer name, pubkey, @group, or array of targets message: string; priority?: Priority; } diff --git a/apps/cli/src/ws/client.ts b/apps/cli/src/ws/client.ts index 2495459..91bad07 100644 --- a/apps/cli/src/ws/client.ts +++ b/apps/cli/src/ws/client.ts @@ -412,6 +412,9 @@ export class BrokerClient { /** Check delivery status of a sent message. */ private messageStatusResolvers: Array<(result: { messageId: string; targetSpec: string; delivered: boolean; deliveredAt: string | null; recipients: Array<{ name: string; pubkey: string; status: string }> } | null) => void> = []; + private fileUrlResolvers: Array<(result: { url: string; name: string } | null) => void> = []; + private fileListResolvers: Array<(files: Array<{ id: string; name: string; size: number; tags: string[]; uploadedBy: string; uploadedAt: string; persistent: boolean }>) => void> = []; + private fileStatusResolvers: Array<(accesses: Array<{ peerName: string; accessedAt: string }>) => void> = []; async messageStatus(messageId: string): Promise<{ messageId: string; targetSpec: string; delivered: boolean; deliveredAt: string | null; recipients: Array<{ name: string; pubkey: string; status: string }> } | null> { if (!this.ws || this.ws.readyState !== this.ws.OPEN) return null; @@ -425,6 +428,95 @@ export class BrokerClient { }); } + // --- Files --- + + /** Get a download URL for a shared file. */ + async getFile(fileId: string): Promise<{ url: string; name: string } | null> { + if (!this.ws || this.ws.readyState !== this.ws.OPEN) return null; + return new Promise((resolve) => { + this.fileUrlResolvers.push(resolve); + this.ws!.send(JSON.stringify({ type: "get_file", fileId })); + setTimeout(() => { + const idx = this.fileUrlResolvers.indexOf(resolve); + if (idx !== -1) { + this.fileUrlResolvers.splice(idx, 1); + resolve(null); + } + }, 5_000); + }); + } + + /** List files shared in the mesh. */ + async listFiles(query?: string, from?: string): Promise> { + if (!this.ws || this.ws.readyState !== this.ws.OPEN) return []; + return new Promise((resolve) => { + this.fileListResolvers.push(resolve); + this.ws!.send(JSON.stringify({ type: "list_files", query, from })); + setTimeout(() => { + const idx = this.fileListResolvers.indexOf(resolve); + if (idx !== -1) { + this.fileListResolvers.splice(idx, 1); + resolve([]); + } + }, 5_000); + }); + } + + /** Check who has accessed a shared file. */ + async fileStatus(fileId: string): Promise> { + if (!this.ws || this.ws.readyState !== this.ws.OPEN) return []; + return new Promise((resolve) => { + this.fileStatusResolvers.push(resolve); + this.ws!.send(JSON.stringify({ type: "file_status", fileId })); + setTimeout(() => { + const idx = this.fileStatusResolvers.indexOf(resolve); + if (idx !== -1) { + this.fileStatusResolvers.splice(idx, 1); + resolve([]); + } + }, 5_000); + }); + } + + /** Delete a shared file from the mesh. */ + async deleteFile(fileId: string): Promise { + if (!this.ws || this.ws.readyState !== this.ws.OPEN) return; + this.ws.send(JSON.stringify({ type: "delete_file", fileId })); + } + + /** Upload a file to the broker via HTTP POST. Returns file ID or null. */ + async uploadFile(filePath: string, meshId: string, memberId: string, opts: { + name?: string; tags?: string[]; persistent?: boolean; targetSpec?: string; + }): Promise { + const { readFileSync } = await import("node:fs"); + const { basename } = await import("node:path"); + const data = readFileSync(filePath); + const fileName = opts.name ?? basename(filePath); + + // Convert WS broker URL to HTTP + const brokerHttp = this.mesh.brokerUrl + .replace("wss://", "https://") + .replace("ws://", "http://") + .replace("/ws", ""); + + const res = await fetch(`${brokerHttp}/upload`, { + method: "POST", + headers: { + "Content-Type": "application/octet-stream", + "X-Mesh-Id": meshId, + "X-Member-Id": memberId, + "X-File-Name": fileName, + "X-Tags": JSON.stringify(opts.tags ?? []), + "X-Persistent": String(opts.persistent ?? true), + "X-Target-Spec": opts.targetSpec ?? "", + }, + body: data, + signal: AbortSignal.timeout(30_000), + }); + const body = await res.json() as { ok?: boolean; fileId?: string }; + return body.fileId ?? null; + } + /** Subscribe to state change notifications. Returns an unsubscribe function. */ onStateChange(handler: (change: { key: string; value: unknown; updatedBy: string }) => void): () => void { this.stateChangeHandlers.add(handler); @@ -583,6 +675,29 @@ export class BrokerClient { if (resolver) resolver(msg as any); return; } + if (msg.type === "file_url") { + const resolver = this.fileUrlResolvers.shift(); + if (resolver) { + if (msg.url) { + resolver({ url: String(msg.url), name: String(msg.name ?? "") }); + } else { + resolver(null); + } + } + return; + } + if (msg.type === "file_list") { + const files = (msg.files as Array<{ id: string; name: string; size: number; tags: string[]; uploadedBy: string; uploadedAt: string; persistent: boolean }>) ?? []; + const resolver = this.fileListResolvers.shift(); + if (resolver) resolver(files); + return; + } + if (msg.type === "file_status_result") { + const accesses = (msg.accesses as Array<{ peerName: string; accessedAt: string }>) ?? []; + const resolver = this.fileStatusResolvers.shift(); + if (resolver) resolver(accesses); + return; + } if (msg.type === "error") { this.debug(`broker error: ${msg.code} ${msg.message}`); const id = msg.id ? String(msg.id) : null; diff --git a/docker-compose.production.yml b/docker-compose.production.yml index ba8dfe8..35b5d6b 100644 --- a/docker-compose.production.yml +++ b/docker-compose.production.yml @@ -28,6 +28,26 @@ services: networks: - claudemesh-internal + minio: + image: minio/minio + command: server /data --console-address ":9001" + restart: always + volumes: + - minio-data:/data + environment: + MINIO_ROOT_USER: claudemesh + MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:-changeme} + expose: + - "9000" + networks: + - claudemesh-internal + healthcheck: + test: ["CMD", "mc", "ready", "local"] + interval: 15s + timeout: 5s + start_period: 10s + retries: 3 + broker: image: ${BROKER_IMAGE:-claudemesh-broker:latest} restart: always @@ -40,11 +60,18 @@ services: MAX_CONNECTIONS_PER_MESH: ${MAX_CONNECTIONS_PER_MESH:-100} MAX_MESSAGE_BYTES: ${MAX_MESSAGE_BYTES:-65536} HOOK_RATE_LIMIT_PER_MIN: ${HOOK_RATE_LIMIT_PER_MIN:-30} + MINIO_ENDPOINT: minio:9000 + MINIO_ACCESS_KEY: claudemesh + MINIO_SECRET_KEY: ${MINIO_SECRET_KEY:-changeme} + MINIO_USE_SSL: "false" expose: - "7900" networks: - coolify - claudemesh-internal + depends_on: + minio: + condition: service_healthy healthcheck: test: ["CMD", "bun", "-e", "fetch('http://localhost:7900/health').then(r=>{process.exit(r.ok?0:1)}).catch(()=>process.exit(1))"] interval: 15s @@ -85,6 +112,9 @@ services: start_period: 20s retries: 3 +volumes: + minio-data: + networks: # Coolify's shared Traefik network — must already exist on the host coolify: diff --git a/packages/db/migrations/0009_add-file-tables.sql b/packages/db/migrations/0009_add-file-tables.sql new file mode 100644 index 0000000..d0b8227 --- /dev/null +++ b/packages/db/migrations/0009_add-file-tables.sql @@ -0,0 +1,28 @@ +CREATE TABLE "mesh"."file" ( + "id" text PRIMARY KEY NOT NULL, + "mesh_id" text NOT NULL, + "name" text NOT NULL, + "size_bytes" integer NOT NULL, + "mime_type" text, + "minio_key" text NOT NULL, + "tags" text[] DEFAULT '{}', + "persistent" boolean DEFAULT true NOT NULL, + "uploaded_by_name" text, + "uploaded_by_member" text, + "target_spec" text, + "uploaded_at" timestamp DEFAULT now() NOT NULL, + "expires_at" timestamp, + "deleted_at" timestamp +); +--> statement-breakpoint +CREATE TABLE "mesh"."file_access" ( + "id" text PRIMARY KEY NOT NULL, + "file_id" text NOT NULL, + "peer_session_pubkey" text, + "peer_name" text, + "accessed_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "mesh"."file" ADD CONSTRAINT "file_mesh_id_mesh_id_fk" FOREIGN KEY ("mesh_id") REFERENCES "mesh"."mesh"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "mesh"."file" ADD CONSTRAINT "file_uploaded_by_member_member_id_fk" FOREIGN KEY ("uploaded_by_member") REFERENCES "mesh"."member"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "mesh"."file_access" ADD CONSTRAINT "file_access_file_id_file_id_fk" FOREIGN KEY ("file_id") REFERENCES "mesh"."file"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/packages/db/migrations/meta/0009_snapshot.json b/packages/db/migrations/meta/0009_snapshot.json new file mode 100644 index 0000000..30da6a2 --- /dev/null +++ b/packages/db/migrations/meta/0009_snapshot.json @@ -0,0 +1,3237 @@ +{ + "id": "cced9051-7468-4428-8d46-472edd5f4509", + "prevId": "c7ccc82a-517e-4ebe-b467-89ff66306354", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "invitation_organizationId_idx": { + "name": "invitation_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "member_organizationId_idx": { + "name": "member_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_userId_idx": { + "name": "member_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.passkey": { + "name": "passkey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "aaguid": { + "name": "aaguid", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "passkey_userId_idx": { + "name": "passkey_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "passkey_credentialID_idx": { + "name": "passkey_credentialID_idx", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "passkey_user_id_user_id_fk": { + "name": "passkey_user_id_user_id_fk", + "tableFrom": "passkey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.two_factor": { + "name": "two_factor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backup_codes": { + "name": "backup_codes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "twoFactor_secret_idx": { + "name": "twoFactor_secret_idx", + "columns": [ + { + "expression": "secret", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "twoFactor_userId_idx": { + "name": "twoFactor_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "two_factor_user_id_user_id_fk": { + "name": "two_factor_user_id_user_id_fk", + "tableFrom": "two_factor", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "is_anonymous": { + "name": "is_anonymous", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credit_transaction": { + "name": "credit_transaction", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "credit_transaction_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "balance_after": { + "name": "balance_after", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "credit_transaction_customer_id_customer_id_fk": { + "name": "credit_transaction_customer_id_customer_id_fk", + "tableFrom": "credit_transaction", + "tableTo": "customer", + "columnsFrom": [ + "customer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.customer": { + "name": "customer", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "plan": { + "name": "plan", + "type": "plan", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "credits": { + "name": "credits", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "customer_user_id_user_id_fk": { + "name": "customer_user_id_user_id_fk", + "tableFrom": "customer", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "customer_userId_unique": { + "name": "customer_userId_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + }, + "customer_customerId_unique": { + "name": "customer_customerId_unique", + "nullsNotDistinct": false, + "columns": [ + "customer_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "chat.chat": { + "name": "chat", + "schema": "chat", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "chat.message": { + "name": "message", + "schema": "chat", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "role", + "typeSchema": "chat", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "message_chat_id_chat_id_fk": { + "name": "message_chat_id_chat_id_fk", + "tableFrom": "message", + "tableTo": "chat", + "schemaTo": "chat", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "chat.part": { + "name": "part", + "schema": "chat", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "part_message_id_message_id_fk": { + "name": "part_message_id_message_id_fk", + "tableFrom": "part", + "tableTo": "message", + "schemaTo": "chat", + "columnsFrom": [ + "message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "pdf.chat": { + "name": "chat", + "schema": "pdf", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "pdf.citation_unit": { + "name": "citation_unit", + "schema": "pdf", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "retrieval_chunk_id": { + "name": "retrieval_chunk_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "page_number": { + "name": "page_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "paragraph_index": { + "name": "paragraph_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "char_start": { + "name": "char_start", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "char_end": { + "name": "char_end", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "bbox_x": { + "name": "bbox_x", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "bbox_y": { + "name": "bbox_y", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "bbox_width": { + "name": "bbox_width", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "bbox_height": { + "name": "bbox_height", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "section_title": { + "name": "section_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "unit_type": { + "name": "unit_type", + "type": "unit_type", + "typeSchema": "pdf", + "primaryKey": false, + "notNull": false, + "default": "'prose'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_cu_document": { + "name": "idx_cu_document", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cu_retrieval": { + "name": "idx_cu_retrieval", + "columns": [ + { + "expression": "retrieval_chunk_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cu_page": { + "name": "idx_cu_page", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "page_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cu_unique": { + "name": "idx_cu_unique", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "page_number", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "paragraph_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "citation_unit_document_id_document_id_fk": { + "name": "citation_unit_document_id_document_id_fk", + "tableFrom": "citation_unit", + "tableTo": "document", + "schemaTo": "pdf", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "citation_unit_retrieval_chunk_id_retrieval_chunk_id_fk": { + "name": "citation_unit_retrieval_chunk_id_retrieval_chunk_id_fk", + "tableFrom": "citation_unit", + "tableTo": "retrieval_chunk", + "schemaTo": "pdf", + "columnsFrom": [ + "retrieval_chunk_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "pdf.document": { + "name": "document", + "schema": "pdf", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "processing_status": { + "name": "processing_status", + "type": "processing_status", + "typeSchema": "pdf", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "document_chat_id_chat_id_fk": { + "name": "document_chat_id_chat_id_fk", + "tableFrom": "document", + "tableTo": "chat", + "schemaTo": "pdf", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "pdf.embedding": { + "name": "embedding", + "schema": "pdf", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "page_number": { + "name": "page_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "char_start": { + "name": "char_start", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "char_end": { + "name": "char_end", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "section_title": { + "name": "section_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "pdf_embeddingIndex": { + "name": "pdf_embeddingIndex", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": {} + } + }, + "foreignKeys": { + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "schemaTo": "pdf", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "pdf.message": { + "name": "message", + "schema": "pdf", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "role", + "typeSchema": "pdf", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "message_chat_id_chat_id_fk": { + "name": "message_chat_id_chat_id_fk", + "tableFrom": "message", + "tableTo": "chat", + "schemaTo": "pdf", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "pdf.retrieval_chunk": { + "name": "retrieval_chunk", + "schema": "pdf", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "page_start": { + "name": "page_start", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "page_end": { + "name": "page_end", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "section_hierarchy": { + "name": "section_hierarchy", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "chunk_type": { + "name": "chunk_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'prose'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_rc_document": { + "name": "idx_rc_document", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_rc_embedding": { + "name": "idx_rc_embedding", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": {} + } + }, + "foreignKeys": { + "retrieval_chunk_document_id_document_id_fk": { + "name": "retrieval_chunk_document_id_document_id_fk", + "tableFrom": "retrieval_chunk", + "tableTo": "document", + "schemaTo": "pdf", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "image.generation": { + "name": "generation", + "schema": "image", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "aspect_ratio": { + "name": "aspect_ratio", + "type": "aspect_ratio", + "typeSchema": "image", + "primaryKey": false, + "notNull": true, + "default": "'square'" + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "generation_user_id_user_id_fk": { + "name": "generation_user_id_user_id_fk", + "tableFrom": "generation", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "image.image": { + "name": "image", + "schema": "image", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "generation_id": { + "name": "generation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "image_generation_id_generation_id_fk": { + "name": "image_generation_id_generation_id_fk", + "tableFrom": "image", + "tableTo": "generation", + "schemaTo": "image", + "columnsFrom": [ + "generation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "mesh.audit_log": { + "name": "audit_log", + "schema": "mesh", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mesh_id": { + "name": "mesh_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_peer_id": { + "name": "actor_peer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_peer_id": { + "name": "target_peer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "audit_log_mesh_id_mesh_id_fk": { + "name": "audit_log_mesh_id_mesh_id_fk", + "tableFrom": "audit_log", + "tableTo": "mesh", + "schemaTo": "mesh", + "columnsFrom": [ + "mesh_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "mesh.invite": { + "name": "invite", + "schema": "mesh", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mesh_id": { + "name": "mesh_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_bytes": { + "name": "token_bytes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_uses": { + "name": "max_uses", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "used_count": { + "name": "used_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "role": { + "name": "role", + "type": "role", + "typeSchema": "mesh", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "invite_mesh_id_mesh_id_fk": { + "name": "invite_mesh_id_mesh_id_fk", + "tableFrom": "invite", + "tableTo": "mesh", + "schemaTo": "mesh", + "columnsFrom": [ + "mesh_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "invite_created_by_user_id_fk": { + "name": "invite_created_by_user_id_fk", + "tableFrom": "invite", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invite_token_unique": { + "name": "invite_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "mesh.mesh": { + "name": "mesh", + "schema": "mesh", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "visibility", + "typeSchema": "mesh", + "primaryKey": false, + "notNull": true, + "default": "'private'" + }, + "transport": { + "name": "transport", + "type": "transport", + "typeSchema": "mesh", + "primaryKey": false, + "notNull": true, + "default": "'managed'" + }, + "max_peers": { + "name": "max_peers", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tier": { + "name": "tier", + "type": "tier", + "typeSchema": "mesh", + "primaryKey": false, + "notNull": true, + "default": "'free'" + }, + "owner_pubkey": { + "name": "owner_pubkey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_secret_key": { + "name": "owner_secret_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "root_key": { + "name": "root_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mesh_owner_user_id_user_id_fk": { + "name": "mesh_owner_user_id_user_id_fk", + "tableFrom": "mesh", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mesh_slug_unique": { + "name": "mesh_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "mesh.file": { + "name": "file", + "schema": "mesh", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mesh_id": { + "name": "mesh_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "minio_key": { + "name": "minio_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "persistent": { + "name": "persistent", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "uploaded_by_name": { + "name": "uploaded_by_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_by_member": { + "name": "uploaded_by_member", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_spec": { + "name": "target_spec", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "file_mesh_id_mesh_id_fk": { + "name": "file_mesh_id_mesh_id_fk", + "tableFrom": "file", + "tableTo": "mesh", + "schemaTo": "mesh", + "columnsFrom": [ + "mesh_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "file_uploaded_by_member_member_id_fk": { + "name": "file_uploaded_by_member_member_id_fk", + "tableFrom": "file", + "tableTo": "member", + "schemaTo": "mesh", + "columnsFrom": [ + "uploaded_by_member" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "mesh.file_access": { + "name": "file_access", + "schema": "mesh", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "peer_session_pubkey": { + "name": "peer_session_pubkey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "peer_name": { + "name": "peer_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessed_at": { + "name": "accessed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "file_access_file_id_file_id_fk": { + "name": "file_access_file_id_file_id_fk", + "tableFrom": "file_access", + "tableTo": "file", + "schemaTo": "mesh", + "columnsFrom": [ + "file_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "mesh.member": { + "name": "member", + "schema": "mesh", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mesh_id": { + "name": "mesh_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "peer_pubkey": { + "name": "peer_pubkey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "role", + "typeSchema": "mesh", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "member_mesh_id_mesh_id_fk": { + "name": "member_mesh_id_mesh_id_fk", + "tableFrom": "member", + "tableTo": "mesh", + "schemaTo": "mesh", + "columnsFrom": [ + "mesh_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "mesh.memory": { + "name": "memory", + "schema": "mesh", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mesh_id": { + "name": "mesh_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "remembered_by": { + "name": "remembered_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "remembered_by_name": { + "name": "remembered_by_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "remembered_at": { + "name": "remembered_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "forgotten_at": { + "name": "forgotten_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "memory_mesh_id_mesh_id_fk": { + "name": "memory_mesh_id_mesh_id_fk", + "tableFrom": "memory", + "tableTo": "mesh", + "schemaTo": "mesh", + "columnsFrom": [ + "mesh_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "memory_remembered_by_member_id_fk": { + "name": "memory_remembered_by_member_id_fk", + "tableFrom": "memory", + "tableTo": "member", + "schemaTo": "mesh", + "columnsFrom": [ + "remembered_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "mesh.state": { + "name": "state", + "schema": "mesh", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mesh_id": { + "name": "mesh_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_by_presence": { + "name": "updated_by_presence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_name": { + "name": "updated_by_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "state_mesh_key_idx": { + "name": "state_mesh_key_idx", + "columns": [ + { + "expression": "mesh_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "state_mesh_id_mesh_id_fk": { + "name": "state_mesh_id_mesh_id_fk", + "tableFrom": "state", + "tableTo": "mesh", + "schemaTo": "mesh", + "columnsFrom": [ + "mesh_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "mesh.message_queue": { + "name": "message_queue", + "schema": "mesh", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mesh_id": { + "name": "mesh_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sender_member_id": { + "name": "sender_member_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sender_session_pubkey": { + "name": "sender_session_pubkey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_spec": { + "name": "target_spec", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "message_priority", + "typeSchema": "mesh", + "primaryKey": false, + "notNull": true, + "default": "'next'" + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "message_queue_mesh_id_mesh_id_fk": { + "name": "message_queue_mesh_id_mesh_id_fk", + "tableFrom": "message_queue", + "tableTo": "mesh", + "schemaTo": "mesh", + "columnsFrom": [ + "mesh_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "message_queue_sender_member_id_member_id_fk": { + "name": "message_queue_sender_member_id_member_id_fk", + "tableFrom": "message_queue", + "tableTo": "member", + "schemaTo": "mesh", + "columnsFrom": [ + "sender_member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "mesh.pending_status": { + "name": "pending_status", + "schema": "mesh", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "pid": { + "name": "pid", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status_source": { + "name": "status_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "applied_at": { + "name": "applied_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "mesh.presence": { + "name": "presence", + "schema": "mesh", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "member_id": { + "name": "member_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_pubkey": { + "name": "session_pubkey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pid": { + "name": "pid", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "presence_status", + "typeSchema": "mesh", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "status_source": { + "name": "status_source", + "type": "presence_status_source", + "typeSchema": "mesh", + "primaryKey": false, + "notNull": true, + "default": "'jsonl'" + }, + "status_updated_at": { + "name": "status_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "groups": { + "name": "groups", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "connected_at": { + "name": "connected_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_ping_at": { + "name": "last_ping_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "disconnected_at": { + "name": "disconnected_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "presence_member_id_member_id_fk": { + "name": "presence_member_id_member_id_fk", + "tableFrom": "presence", + "tableTo": "member", + "schemaTo": "mesh", + "columnsFrom": [ + "member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.credit_transaction_type": { + "name": "credit_transaction_type", + "schema": "public", + "values": [ + "signup", + "purchase", + "usage", + "admin_grant", + "admin_deduct", + "refund", + "promo", + "referral", + "expiry" + ] + }, + "public.status": { + "name": "status", + "schema": "public", + "values": [ + "active", + "canceled", + "incomplete", + "incomplete_expired", + "past_due", + "paused", + "trialing", + "unpaid" + ] + }, + "public.plan": { + "name": "plan", + "schema": "public", + "values": [ + "free", + "premium", + "enterprise" + ] + }, + "chat.role": { + "name": "role", + "schema": "chat", + "values": [ + "system", + "assistant", + "user" + ] + }, + "pdf.role": { + "name": "role", + "schema": "pdf", + "values": [ + "user", + "assistant", + "system" + ] + }, + "pdf.processing_status": { + "name": "processing_status", + "schema": "pdf", + "values": [ + "pending", + "processing", + "ready", + "failed" + ] + }, + "pdf.unit_type": { + "name": "unit_type", + "schema": "pdf", + "values": [ + "prose", + "heading", + "list", + "table", + "code" + ] + }, + "image.aspect_ratio": { + "name": "aspect_ratio", + "schema": "image", + "values": [ + "square", + "standard", + "landscape", + "portrait" + ] + }, + "mesh.role": { + "name": "role", + "schema": "mesh", + "values": [ + "admin", + "member" + ] + }, + "mesh.tier": { + "name": "tier", + "schema": "mesh", + "values": [ + "free", + "pro", + "team", + "enterprise" + ] + }, + "mesh.transport": { + "name": "transport", + "schema": "mesh", + "values": [ + "managed", + "tailscale", + "self_hosted" + ] + }, + "mesh.visibility": { + "name": "visibility", + "schema": "mesh", + "values": [ + "private", + "public" + ] + }, + "mesh.message_priority": { + "name": "message_priority", + "schema": "mesh", + "values": [ + "now", + "next", + "low" + ] + }, + "mesh.presence_status": { + "name": "presence_status", + "schema": "mesh", + "values": [ + "idle", + "working", + "dnd" + ] + }, + "mesh.presence_status_source": { + "name": "presence_status_source", + "schema": "mesh", + "values": [ + "hook", + "manual", + "jsonl" + ] + } + }, + "schemas": { + "chat": "chat", + "pdf": "pdf", + "image": "image", + "mesh": "mesh" + }, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 60e0213..a8fa4f1 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -64,6 +64,13 @@ "when": 1775477883426, "tag": "0008_add-state-and-memory", "breakpoints": true + }, + { + "idx": 9, + "version": "7", + "when": 1775480008546, + "tag": "0009_add-file-tables", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema/mesh.ts b/packages/db/src/schema/mesh.ts index 724949f..820c360 100644 --- a/packages/db/src/schema/mesh.ts +++ b/packages/db/src/schema/mesh.ts @@ -1,5 +1,6 @@ import { relations } from "drizzle-orm"; import { + boolean, integer, jsonb, pgSchema, @@ -289,6 +290,43 @@ export const meshMemory = meshSchema.table("memory", { forgottenAt: timestamp(), }); +/** + * File metadata for shared files in a mesh. Actual bytes live in MinIO; + * this table tracks ownership, access control, and soft-deletion. + */ +export const meshFile = meshSchema.table("file", { + id: text().primaryKey().notNull().$defaultFn(generateId), + meshId: text() + .references(() => mesh.id, { onDelete: "cascade", onUpdate: "cascade" }) + .notNull(), + name: text().notNull(), + sizeBytes: integer().notNull(), + mimeType: text(), + minioKey: text().notNull(), + tags: text().array().default([]), + persistent: boolean().notNull().default(true), + uploadedByName: text(), + uploadedByMember: text().references(() => meshMember.id), + targetSpec: text(), // null = entire mesh + uploadedAt: timestamp().defaultNow().notNull(), + expiresAt: timestamp(), + deletedAt: timestamp(), +}); + +/** + * Access log for file downloads. Tracks which peer accessed which file + * and when, for auditability and read-receipt semantics. + */ +export const meshFileAccess = meshSchema.table("file_access", { + id: text().primaryKey().notNull().$defaultFn(generateId), + fileId: text() + .references(() => meshFile.id, { onDelete: "cascade" }) + .notNull(), + peerSessionPubkey: text(), + peerName: text(), + accessedAt: timestamp().defaultNow().notNull(), +}); + export const meshRelations = relations(mesh, ({ one, many }) => ({ owner: one(user, { fields: [mesh.ownerUserId], @@ -367,6 +405,25 @@ export const meshMemoryRelations = relations(meshMemory, ({ one }) => ({ }), })); +export const meshFileRelations = relations(meshFile, ({ one, many }) => ({ + mesh: one(mesh, { + fields: [meshFile.meshId], + references: [mesh.id], + }), + uploader: one(meshMember, { + fields: [meshFile.uploadedByMember], + references: [meshMember.id], + }), + accesses: many(meshFileAccess), +})); + +export const meshFileAccessRelations = relations(meshFileAccess, ({ one }) => ({ + file: one(meshFile, { + fields: [meshFileAccess.fileId], + references: [meshFile.id], + }), +})); + export const selectMeshSchema = createSelectSchema(mesh); export const insertMeshSchema = createInsertSchema(mesh); export const selectMemberSchema = createSelectSchema(meshMember); @@ -404,3 +461,11 @@ export type SelectMeshState = typeof meshState.$inferSelect; export type InsertMeshState = typeof meshState.$inferInsert; export type SelectMeshMemory = typeof meshMemory.$inferSelect; export type InsertMeshMemory = typeof meshMemory.$inferInsert; +export const selectMeshFileSchema = createSelectSchema(meshFile); +export const insertMeshFileSchema = createInsertSchema(meshFile); +export const selectMeshFileAccessSchema = createSelectSchema(meshFileAccess); +export const insertMeshFileAccessSchema = createInsertSchema(meshFileAccess); +export type SelectMeshFile = typeof meshFile.$inferSelect; +export type InsertMeshFile = typeof meshFile.$inferInsert; +export type SelectMeshFileAccess = typeof meshFileAccess.$inferSelect; +export type InsertMeshFileAccess = typeof meshFileAccess.$inferInsert;