diff --git a/apps/interfaces/grpc/package.json b/apps/interfaces/grpc/package.json new file mode 100644 index 0000000..e465adc --- /dev/null +++ b/apps/interfaces/grpc/package.json @@ -0,0 +1,13 @@ +{ + "name": "@zesdex/grpc", + "version": "1.21.2", + "private": true, + "type": "module", + "scripts": { + "grpc": "bun src/main.ts", + "build": "true" + }, + "devDependencies": { + "@types/bun": "^1.2.0" + } +} diff --git a/apps/interfaces/grpc/src/index.ts b/apps/interfaces/grpc/src/index.ts new file mode 100644 index 0000000..4099b5b --- /dev/null +++ b/apps/interfaces/grpc/src/index.ts @@ -0,0 +1,6 @@ +/** + * Zesdex gRPC interface package. + * Mirrors `apps/interfaces/grpc/`. + */ +export { startGrpcServer, handleGrpcRequest } from "./server.ts"; +export type { GrpcState } from "./server.ts"; diff --git a/apps/interfaces/grpc/src/main.ts b/apps/interfaces/grpc/src/main.ts new file mode 100644 index 0000000..87f1463 --- /dev/null +++ b/apps/interfaces/grpc/src/main.ts @@ -0,0 +1,9 @@ +#!/usr/bin/env bun +/** + * Zesdex gRPC server — standalone entry point (HTTP health stub). + * Mirrors the `--grpc` mode of the Rust gateway. + */ +import { startGrpcServer } from "./index.ts"; + +const port = Number.parseInt(process.env.ZESDEX_GRPC_PORT ?? "50051", 10); +startGrpcServer(port); diff --git a/apps/interfaces/grpc/src/server.ts b/apps/interfaces/grpc/src/server.ts new file mode 100644 index 0000000..6edff94 --- /dev/null +++ b/apps/interfaces/grpc/src/server.ts @@ -0,0 +1,41 @@ +#!/usr/bin/env bun +/** + * Zesdex gRPC interface — high-performance RPC with protobuf. + * Mirrors `apps/interfaces/grpc/src/lib.rs`. + * + * For now this crate provides a minimal HTTP health-check endpoint so + * consumers can verify the gRPC server is reachable. Full gRPC would use + * tonic+prost (Rust) — in Bun/TS a real gRPC server would use `@grpc/grpc-js` + * + generated protobuf types. This is intentionally a health stub. + */ + +/** gRPC server state (minimal for health checks). */ +export interface GrpcState { + version: string; +} + +/** Build the gRPC health router. */ +export function handleGrpcRequest(_state: GrpcState, req: Request): Response { + const url = new URL(req.url); + if (url.pathname === "/grpc/health") { + return new Response("gRPC server is running", { + status: 200, + headers: { "Content-Type": "text/plain; charset=utf-8" }, + }); + } + return new Response("Not found", { status: 404 }); +} + +/** Start the gRPC server (HTTP health stub). */ +export function startGrpcServer(port: number): { stop: () => void } { + const state: GrpcState = { version: "1.21.2" }; + const server = Bun.serve({ + port, + fetch(req) { + return handleGrpcRequest(state, req); + }, + }); + console.log(`zesdex-grpc listening on http://0.0.0.0:${server.port}`); + console.log("Note: gRPC currently runs HTTP health endpoint. Add @grpc/grpc-js for full gRPC."); + return { stop: () => server.stop(true) }; +} diff --git a/apps/interfaces/web/package.json b/apps/interfaces/web/package.json new file mode 100644 index 0000000..331d8ef --- /dev/null +++ b/apps/interfaces/web/package.json @@ -0,0 +1,13 @@ +{ + "name": "@zesdex/web", + "version": "1.21.2", + "private": true, + "type": "module", + "scripts": { + "web": "bun src/main.ts", + "build": "true" + }, + "devDependencies": { + "@types/bun": "^1.2.0" + } +} diff --git a/apps/interfaces/web/src/index.ts b/apps/interfaces/web/src/index.ts new file mode 100644 index 0000000..f01cc2d --- /dev/null +++ b/apps/interfaces/web/src/index.ts @@ -0,0 +1,6 @@ +/** + * Zesdex web frontend interface package. + * Mirrors `apps/interfaces/web/`. + */ +export { startWebServer, handleWebRequest } from "./server.ts"; +export type { WebState } from "./server.ts"; diff --git a/apps/interfaces/web/src/main.ts b/apps/interfaces/web/src/main.ts new file mode 100644 index 0000000..acdcda6 --- /dev/null +++ b/apps/interfaces/web/src/main.ts @@ -0,0 +1,10 @@ +#!/usr/bin/env bun +/** + * Zesdex web server — standalone entry point. + * Mirrors the `--web` mode of the Rust gateway. + */ +import { startWebServer } from "./index.ts"; + +const port = Number.parseInt(process.env.ZESDEX_WEB_PORT ?? "3000", 10); +const staticDir = process.env.ZESDEX_WEB_DIR ?? undefined; +startWebServer(port, staticDir); diff --git a/apps/interfaces/web/src/server.ts b/apps/interfaces/web/src/server.ts new file mode 100644 index 0000000..b08c51d --- /dev/null +++ b/apps/interfaces/web/src/server.ts @@ -0,0 +1,121 @@ +#!/usr/bin/env bun +/** + * Zesdex web frontend interface — serves static browser assets. + * Mirrors `apps/interfaces/web/src/lib.rs`. + * + * Serves files from a `dist/`/static directory with path-traversal protection: + * every requested path is canonicalized and must resolve inside `static_dir`. + * Falls back to a minimal placeholder page when no frontend is built. + */ +import * as fs from "node:fs"; +import * as path from "node:path"; + +/** Web server state. */ +export interface WebState { + static_dir: string; +} + +const MIME: Record = { + ".html": "text/html; charset=utf-8", + ".js": "text/javascript; charset=utf-8", + ".mjs": "text/javascript; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".json": "application/json", + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".gif": "image/gif", + ".svg": "image/svg+xml", + ".ico": "image/x-icon", + ".webp": "image/webp", + ".woff": "font/woff", + ".woff2": "font/woff2", + ".ttf": "font/ttf", + ".map": "application/json", + ".txt": "text/plain; charset=utf-8", +}; + +function mimeFor(file: string): string { + return MIME[path.extname(file).toLowerCase()] ?? "application/octet-stream"; +} + +/** Placeholder page when no frontend is built. */ +const PLACEHOLDER = ` +Zesdex Web + + + +

Zesdex Web

+

Web interface is ready.

+

To connect the frontend:

+
    +
  1. Build the frontend: cd apps/interfaces/web && npm install && npm run build
  2. +
  3. Restart with --web-dir apps/interfaces/web/dist
  4. +
+`; + +/** Resolve a requested path safely inside `staticDir`; returns null on traversal/not-found. */ +function safeResolve(staticDir: string, rel: string): string | null { + // Strip any leading slash so a relative path is resolved against staticDir + // (path.resolve would otherwise ignore staticDir for absolute-ish inputs). + const clean = rel.replace(/^\/+/, ""); + const candidate = path.resolve(staticDir, clean || "index.html"); + const canonStatic = path.resolve(staticDir); + // Prevent directory traversal: resolved path must stay inside static dir. + if (!candidate.startsWith(canonStatic + path.sep) && candidate !== canonStatic) { + return null; + } + // Only serve regular files; reject if the target is a directory or missing. + try { + const st = fs.statSync(candidate); + return st.isFile() ? candidate : null; + } catch { + return null; + } +} + +/** Build the web router (static file serving). */ +export function handleWebRequest(state: WebState, req: Request): Response { + const url = new URL(req.url); + let rel = decodeURIComponent(url.pathname); + if (rel.endsWith("/")) rel += "index.html"; + + const file = safeResolve(state.static_dir, rel); + if (!file) { + // Root always returns the placeholder index (even without a built frontend). + if (rel === "/index.html") { + return new Response(PLACEHOLDER, { + status: 200, + headers: { "Content-Type": "text/html; charset=utf-8" }, + }); + } + return new Response("Not found", { status: 404 }); + } + + const data = fs.readFileSync(file); + return new Response(data, { + status: 200, + headers: { "Content-Type": mimeFor(file) }, + }); +} + +/** Start the web frontend server. */ +export function startWebServer(port: number, staticDir?: string): { stop: () => void } { + const dir = + staticDir && staticDir !== "" + ? staticDir + : (() => { + const p = path.resolve("apps/interfaces/web/dist"); + return fs.existsSync(p) ? p : process.cwd(); + })(); + const state: WebState = { static_dir: dir }; + const server = Bun.serve({ + port, + fetch(req) { + return handleWebRequest(state, req); + }, + }); + console.log(`zesdex-web listening on http://0.0.0.0:${server.port} (dir: ${state.static_dir})`); + return { stop: () => server.stop(true) }; +} diff --git a/bun.lock b/bun.lock index cc00f25..70f1a6c 100644 --- a/bun.lock +++ b/bun.lock @@ -36,6 +36,20 @@ "@types/bun": "^1.2.0", }, }, + "apps/interfaces/grpc": { + "name": "@zesdex/grpc", + "version": "1.21.2", + "devDependencies": { + "@types/bun": "^1.2.0", + }, + }, + "apps/interfaces/web": { + "name": "@zesdex/web", + "version": "1.21.2", + "devDependencies": { + "@types/bun": "^1.2.0", + }, + }, "apps/interfaces/ws": { "name": "@zesdex/ws", "version": "1.21.2", @@ -121,8 +135,12 @@ "@zesdex/domain": ["@zesdex/domain@workspace:apps/packages/domain"], + "@zesdex/grpc": ["@zesdex/grpc@workspace:apps/interfaces/grpc"], + "@zesdex/infrastructure": ["@zesdex/infrastructure@workspace:apps/packages/infrastructure"], + "@zesdex/web": ["@zesdex/web@workspace:apps/interfaces/web"], + "@zesdex/ws": ["@zesdex/ws@workspace:apps/interfaces/ws"], "bun-types": ["bun-types@1.4.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="],