Initial boilerplate scaffold for continuum-backend
Bun + ElysiaJS control plane: Drizzle/Postgres schema (printers, farms, print_jobs, pgvector frame_embeddings), R2 presigned URLs, Redis Streams frame ingestion queue, REST routes, and the /ws/edge/v1 edge-gateway WebSocket uplink.
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import { Elysia, t } from "elysia";
|
||||
import jwt from "@elysiajs/jwt";
|
||||
import { db } from "../db";
|
||||
import { farms } from "../db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
/**
|
||||
* Verifies Supabase-issued JWTs (HS256, shared secret) and issues short-lived
|
||||
* edge-gateway tokens scoped to a single farm for the /ws/edge/v1 handshake.
|
||||
*/
|
||||
export const authRoutes = new Elysia({ prefix: "/auth" })
|
||||
.use(
|
||||
jwt({
|
||||
name: "supabaseJwt",
|
||||
secret: process.env.SUPABASE_JWT_SECRET ?? "dev-secret-change-me",
|
||||
}),
|
||||
)
|
||||
.use(
|
||||
jwt({
|
||||
name: "edgeJwt",
|
||||
secret: process.env.EDGE_JWT_SECRET ?? "dev-edge-secret-change-me",
|
||||
}),
|
||||
)
|
||||
.post(
|
||||
"/session",
|
||||
async ({ supabaseJwt, headers, set }) => {
|
||||
const authHeader = headers.authorization;
|
||||
const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : undefined;
|
||||
if (!token) {
|
||||
set.status = 401;
|
||||
return { error: { code: "UNAUTHORIZED", message: "Missing bearer token" } };
|
||||
}
|
||||
|
||||
const payload = await supabaseJwt.verify(token);
|
||||
if (!payload) {
|
||||
set.status = 401;
|
||||
return { error: { code: "UNAUTHORIZED", message: "Invalid or expired token" } };
|
||||
}
|
||||
|
||||
return { userId: payload.sub, email: payload.email, role: payload.role };
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/edge-token",
|
||||
async ({ edgeJwt, body, set }) => {
|
||||
const farm = await db.query.farms.findFirst({ where: eq(farms.id, body.farmId) });
|
||||
if (!farm || farm.edgeProxyToken !== body.edgeProxyToken) {
|
||||
set.status = 401;
|
||||
return { error: { code: "UNAUTHORIZED", message: "Unknown farm or bad proxy token" } };
|
||||
}
|
||||
|
||||
const token = await edgeJwt.sign({ farmId: farm.id, scope: "edge" });
|
||||
return { token, expiresInSeconds: 3600 };
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
farmId: t.String({ format: "uuid" }),
|
||||
edgeProxyToken: t.String(),
|
||||
}),
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Elysia, t } from "elysia";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "../db";
|
||||
import { farms } from "../db/schema";
|
||||
|
||||
export const farmRoutes = new Elysia({ prefix: "/farms" })
|
||||
.get("/", async () => db.select().from(farms))
|
||||
.get("/:id", async ({ params, set }) => {
|
||||
const farm = await db.query.farms.findFirst({ where: eq(farms.id, params.id) });
|
||||
if (!farm) {
|
||||
set.status = 404;
|
||||
return { error: { code: "NOT_FOUND", message: `Farm ${params.id} not found` } };
|
||||
}
|
||||
return farm;
|
||||
})
|
||||
.post(
|
||||
"/",
|
||||
async ({ body, set }) => {
|
||||
const [farm] = await db
|
||||
.insert(farms)
|
||||
.values({
|
||||
name: body.name,
|
||||
ownerId: body.ownerId,
|
||||
edgeProxyToken: crypto.randomUUID(),
|
||||
location: body.location,
|
||||
})
|
||||
.returning();
|
||||
set.status = 201;
|
||||
return farm;
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
name: t.String({ minLength: 1 }),
|
||||
ownerId: t.String({ format: "uuid" }),
|
||||
location: t.Optional(t.String()),
|
||||
}),
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Elysia, t } from "elysia";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "../db";
|
||||
import { printJobs } from "../db/schema";
|
||||
import { getUploadUrl, jobFileKey } from "../s3";
|
||||
|
||||
export const jobRoutes = new Elysia({ prefix: "/jobs" })
|
||||
.get("/", async ({ query }) => {
|
||||
if (query.farmId) {
|
||||
return db.select().from(printJobs).where(eq(printJobs.farmId, query.farmId));
|
||||
}
|
||||
return db.select().from(printJobs);
|
||||
}, { query: t.Object({ farmId: t.Optional(t.String({ format: "uuid" })) }) })
|
||||
.get("/:id", async ({ params, set }) => {
|
||||
const job = await db.query.printJobs.findFirst({ where: eq(printJobs.id, params.id) });
|
||||
if (!job) {
|
||||
set.status = 404;
|
||||
return { error: { code: "NOT_FOUND", message: `Print job ${params.id} not found` } };
|
||||
}
|
||||
return job;
|
||||
})
|
||||
.post(
|
||||
"/",
|
||||
async ({ body, set }) => {
|
||||
const jobId = crypto.randomUUID();
|
||||
const r2ObjectKey = jobFileKey(body.farmId, jobId, body.fileName);
|
||||
|
||||
const [job] = await db
|
||||
.insert(printJobs)
|
||||
.values({
|
||||
id: jobId,
|
||||
farmId: body.farmId,
|
||||
fileName: body.fileName,
|
||||
r2ObjectKey,
|
||||
submittedBy: body.submittedBy,
|
||||
})
|
||||
.returning();
|
||||
|
||||
const uploadUrl = await getUploadUrl(r2ObjectKey, body.contentType);
|
||||
set.status = 201;
|
||||
return { job, uploadUrl };
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
farmId: t.String({ format: "uuid" }),
|
||||
fileName: t.String({ minLength: 1 }),
|
||||
contentType: t.Optional(t.String()),
|
||||
submittedBy: t.String({ format: "uuid" }),
|
||||
}),
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Elysia, t } from "elysia";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "../db";
|
||||
import { printers } from "../db/schema";
|
||||
|
||||
export const printerRoutes = new Elysia({ prefix: "/printers" })
|
||||
.get("/", async ({ query }) => {
|
||||
if (query.farmId) {
|
||||
return db.select().from(printers).where(eq(printers.farmId, query.farmId));
|
||||
}
|
||||
return db.select().from(printers);
|
||||
}, { query: t.Object({ farmId: t.Optional(t.String({ format: "uuid" })) }) })
|
||||
.get("/:id", async ({ params, set }) => {
|
||||
const printer = await db.query.printers.findFirst({ where: eq(printers.id, params.id) });
|
||||
if (!printer) {
|
||||
set.status = 404;
|
||||
return { error: { code: "NOT_FOUND", message: `Printer ${params.id} not found` } };
|
||||
}
|
||||
return printer;
|
||||
})
|
||||
.post(
|
||||
"/",
|
||||
async ({ body, set }) => {
|
||||
const [printer] = await db
|
||||
.insert(printers)
|
||||
.values({
|
||||
farmId: body.farmId,
|
||||
name: body.name,
|
||||
vendor: body.vendor,
|
||||
lanAddress: body.lanAddress,
|
||||
serialNumber: body.serialNumber,
|
||||
accessCode: body.accessCode,
|
||||
})
|
||||
.returning();
|
||||
set.status = 201;
|
||||
return printer;
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
farmId: t.String({ format: "uuid" }),
|
||||
name: t.String({ minLength: 1 }),
|
||||
vendor: t.Union([t.Literal("bambu"), t.Literal("prusa"), t.Literal("klipper")]),
|
||||
lanAddress: t.Optional(t.String()),
|
||||
serialNumber: t.Optional(t.String()),
|
||||
accessCode: t.Optional(t.String()),
|
||||
}),
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,89 @@
|
||||
import { Elysia, t } from "elysia";
|
||||
import jwt from "@elysiajs/jwt";
|
||||
import { enqueueFrameIngestion } from "../../queue";
|
||||
|
||||
interface EdgeConn {
|
||||
farmId: string;
|
||||
lastPing: number;
|
||||
}
|
||||
|
||||
const connections = new Map<string, EdgeConn>();
|
||||
|
||||
const edgeMessage = t.Union([
|
||||
t.Object({ type: t.Literal("auth"), token: t.String() }),
|
||||
t.Object({ type: t.Literal("heartbeat") }),
|
||||
t.Object({
|
||||
type: t.Literal("frame_batch"),
|
||||
printerId: t.String({ format: "uuid" }),
|
||||
printJobId: t.Optional(t.String({ format: "uuid" })),
|
||||
frames: t.Array(t.Object({ r2ObjectKey: t.String(), capturedAt: t.String() })),
|
||||
}),
|
||||
t.Object({
|
||||
type: t.Literal("telemetry"),
|
||||
printerId: t.String({ format: "uuid" }),
|
||||
status: t.String(),
|
||||
nozzleTempC: t.Optional(t.Number()),
|
||||
bedTempC: t.Optional(t.Number()),
|
||||
}),
|
||||
]);
|
||||
|
||||
/**
|
||||
* Edge-gateway uplink: continuum-proxy connects here with a short-lived
|
||||
* edge JWT (minted via POST /auth/edge-token) and streams telemetry +
|
||||
* camera-frame batches for the lifetime of the connection.
|
||||
*/
|
||||
export const edgeWsRoute = new Elysia()
|
||||
.use(jwt({ name: "edgeJwt", secret: process.env.EDGE_JWT_SECRET ?? "dev-edge-secret-change-me" }))
|
||||
.ws("/ws/edge/v1", {
|
||||
body: edgeMessage,
|
||||
open(ws) {
|
||||
connections.set(ws.id, { farmId: "", lastPing: Date.now() });
|
||||
},
|
||||
async message(ws, message) {
|
||||
const conn = connections.get(ws.id);
|
||||
if (!conn) return;
|
||||
|
||||
switch (message.type) {
|
||||
case "auth": {
|
||||
const payload = await ws.data.edgeJwt.verify(message.token);
|
||||
if (!payload || payload.scope !== "edge") {
|
||||
ws.send({ type: "auth_error", message: "invalid edge token" });
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
conn.farmId = payload.farmId as string;
|
||||
ws.send({ type: "auth_ok", farmId: conn.farmId });
|
||||
break;
|
||||
}
|
||||
case "heartbeat": {
|
||||
conn.lastPing = Date.now();
|
||||
ws.send({ type: "heartbeat_ack" });
|
||||
break;
|
||||
}
|
||||
case "telemetry": {
|
||||
if (!conn.farmId) return;
|
||||
// Fanned out to subscribers of the dashboard telemetry store; boilerplate
|
||||
// just acknowledges receipt here.
|
||||
ws.send({ type: "telemetry_ack", printerId: message.printerId });
|
||||
break;
|
||||
}
|
||||
case "frame_batch": {
|
||||
if (!conn.farmId) return;
|
||||
for (const frame of message.frames) {
|
||||
await enqueueFrameIngestion({
|
||||
farmId: conn.farmId,
|
||||
printerId: message.printerId,
|
||||
printJobId: message.printJobId ?? null,
|
||||
r2ObjectKey: frame.r2ObjectKey,
|
||||
capturedAt: frame.capturedAt,
|
||||
});
|
||||
}
|
||||
ws.send({ type: "frame_batch_ack", count: message.frames.length });
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
close(ws) {
|
||||
connections.delete(ws.id);
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user