Add BlackRoad info service module

This commit is contained in:
Alexa Amundson
2025-11-20 20:00:52 -06:00
parent 9e5f17ab3e
commit f19c9ec37d
13 changed files with 265 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
# Port for the Express server
PORT=3000
# Optional environment metadata
SERVICE_BASE_URL=https://blackroad-info.up.railway.app
OS_ROOT=https://blackroad.systems

View File

@@ -0,0 +1,16 @@
# Builder
FROM node:18-alpine AS builder
WORKDIR /app
COPY package.json tsconfig.json services.ts ./
COPY src ./src
RUN npm install --production=false && npm run build
# Runner
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/package.json ./
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "dist/index.js"]

View File

@@ -0,0 +1,38 @@
# BlackRoad Info Service
Backend service for the BlackRoad Operating System exposing metadata and health endpoints.
---
**SERVICE METADATA**
- **Service Name:** BlackRoad Info Service
- **Service ID:** info
- **Kind:** backend
- **Repo URL:** https://github.com/blackroad-os/BlackRoad-Operating-System
- **Base URL (Railway):** https://blackroad-info.up.railway.app
---
## Endpoints
- `GET /health` — basic health check.
- `GET /info` — service metadata.
- `GET /version` — package version.
- `GET /debug/env` — safe environment variable dump.
## Development
```bash
npm install
npm run dev
```
## Testing
```bash
npm test
```
## Building
```bash
npm run build
npm start
```

View File

@@ -0,0 +1,11 @@
import type { Config } from "jest";
const config: Config = {
preset: "ts-jest",
testEnvironment: "node",
roots: ["<rootDir>/src"],
moduleFileExtensions: ["ts", "js", "json"],
collectCoverage: false
};
export default config;

View File

@@ -0,0 +1,35 @@
{
"name": "blackroad-info-service",
"version": "1.0.0",
"description": "BlackRoad Operating System metadata service.",
"main": "dist/index.js",
"scripts": {
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
"build": "tsc -p tsconfig.json",
"start": "node dist/index.js",
"test": "jest --runInBand"
},
"keywords": [
"blackroad",
"service",
"metadata"
],
"author": "BlackRoad OS",
"license": "MIT",
"dependencies": {
"cors": "^2.8.5",
"express": "^4.19.2"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/jest": "^29.5.12",
"@types/node": "^20.14.2",
"@types/supertest": "^6.0.3",
"jest": "^29.7.0",
"supertest": "^6.3.4",
"ts-jest": "^29.1.2",
"ts-node-dev": "^2.0.0",
"typescript": "^5.4.5"
}
}

View File

@@ -0,0 +1,11 @@
{
"build": {
"builder": "NIXPACKS",
"buildCommand": "npm install && npm run build"
},
"deploy": {
"startCommand": "npm start",
"healthcheckPath": "/health",
"restartPolicyType": "ON_FAILURE"
}
}

View File

@@ -0,0 +1,17 @@
import { SERVICE_BASE_URL, SERVICE_ID, SERVICE_NAME } from "./src/constants";
export const services = {
[SERVICE_ID]: {
name: SERVICE_NAME,
id: SERVICE_ID,
baseUrl: SERVICE_BASE_URL
}
};
export type ServiceDescriptor = {
name: string;
id: string;
baseUrl: string;
};
export default services;

View File

@@ -0,0 +1,4 @@
export const SERVICE_ID = "info";
export const SERVICE_NAME = "BlackRoad Info Service";
export const SERVICE_BASE_URL = "https://blackroad-info.up.railway.app";
export const OS_ROOT = "https://blackroad.systems";

View File

@@ -0,0 +1,21 @@
import request from "supertest";
import app from "./index";
import { SERVICE_ID, SERVICE_NAME } from "./constants";
import pkg from "../package.json";
describe("BlackRoad Info Service", () => {
it("returns health", async () => {
const res = await request(app).get("/health");
expect(res.status).toBe(200);
expect(res.body).toEqual({ ok: true, service: SERVICE_ID });
});
it("returns info", async () => {
const res = await request(app).get("/info");
expect(res.status).toBe(200);
expect(res.body.name).toBe(SERVICE_NAME);
expect(res.body.id).toBe(SERVICE_ID);
expect(res.body.version).toBe(pkg.version);
expect(typeof res.body.time).toBe("string");
});
});

View File

@@ -0,0 +1,64 @@
import express, { Request, Response, NextFunction } from "express";
import cors from "cors";
import healthRouter from "./routes/health";
import infoRouter from "./routes/info";
import pkg from "../package.json";
import { OS_ROOT, SERVICE_BASE_URL, SERVICE_ID, SERVICE_NAME } from "./constants";
const app = express();
const port = process.env.PORT || 3000;
app.use(cors());
app.use(express.json());
// Logging middleware
app.use((req: Request, res: Response, next: NextFunction) => {
const start = Date.now();
res.on("finish", () => {
const duration = Date.now() - start;
const logEntry = {
ts: new Date().toISOString(),
method: req.method,
path: req.originalUrl,
status: res.statusCode,
duration_ms: duration,
service_id: SERVICE_ID
};
console.log(JSON.stringify(logEntry));
});
next();
});
app.get("/version", (_req, res) => {
res.json({ version: pkg.version });
});
app.get("/debug/env", (_req, res) => {
const allowedEnv = ["NODE_ENV", "SERVICE_BASE_URL", "OS_ROOT", "PORT"];
const safeEnv: Record<string, string | undefined> = {};
allowedEnv.forEach((key) => {
if (process.env[key]) {
safeEnv[key] = process.env[key];
}
});
safeEnv.SERVICE_BASE_URL = SERVICE_BASE_URL;
safeEnv.OS_ROOT = OS_ROOT;
res.json({ ok: true, env: safeEnv, service: SERVICE_ID });
});
app.use(healthRouter);
app.use(infoRouter);
// Error handling
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
console.error(err);
res.status(500).json({ ok: false, error: err.message, service: SERVICE_ID });
});
if (require.main === module) {
app.listen(port, () => {
console.log(`${SERVICE_NAME} listening on port ${port}`);
});
}
export default app;

View File

@@ -0,0 +1,10 @@
import { Router } from "express";
import { SERVICE_ID } from "../constants";
const router = Router();
router.get("/health", (_req, res) => {
res.json({ ok: true, service: SERVICE_ID });
});
export default router;

View File

@@ -0,0 +1,16 @@
import { Router } from "express";
import { SERVICE_ID, SERVICE_NAME } from "../constants";
import pkg from "../../package.json";
const router = Router();
router.get("/info", (_req, res) => {
res.json({
name: SERVICE_NAME,
id: SERVICE_ID,
version: pkg.version,
time: new Date().toISOString()
});
});
export default router;

View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"outDir": "dist",
"rootDir": ".",
"strict": true,
"esModuleInterop": true,
"moduleResolution": "node",
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"include": ["src/**/*", "services.ts"],
"exclude": ["node_modules", "dist"]
}