- New nostr_identities DB table (pubkey, trust_score, tier, interaction_count, sats_absorbed_today, last_seen) - nullable nostr_pubkey FK on sessions + jobs tables; schema pushed - TrustService: getTier, getOrCreate, recordSuccess/Failure, HMAC token (issue/verify) - Soft score decay (lazy, on read) when identity absent > N days - POST /api/identity/challenge + POST /api/identity/verify (NIP-01 sig verification) - GET /api/identity/me — look up trust profile by X-Nostr-Token - POST /api/sessions + POST /api/jobs accept optional nostr_token; bind pubkey to row - GET /sessions/:id + GET /jobs/:id include trust_tier in response - recordSuccess/Failure called after session request + job work completes - X-Nostr-Token added to CORS allowedHeaders + exposedHeaders - TIMMY_TOKEN_SECRET set as persistent shared env var
181 lines
5.4 KiB
TypeScript
181 lines
5.4 KiB
TypeScript
import express, { type Express } from "express";
|
|
import cors from "cors";
|
|
import path from "path";
|
|
import router from "./routes/index.js";
|
|
import { responseTimeMiddleware } from "./middlewares/response-time.js";
|
|
|
|
const app: Express = express();
|
|
|
|
app.set("trust proxy", 1);
|
|
|
|
// ── CORS (#5) ────────────────────────────────────────────────────────────────
|
|
// CORS_ORIGINS = comma-separated list of allowed origins.
|
|
// Default in production: alexanderwhitestone.com (and www. variant).
|
|
// Default in development: all origins permitted.
|
|
const isProd = process.env["NODE_ENV"] === "production";
|
|
|
|
const rawOrigins = process.env["CORS_ORIGINS"];
|
|
const allowedOrigins: string[] = rawOrigins
|
|
? rawOrigins.split(",").map((o) => o.trim()).filter(Boolean)
|
|
: isProd
|
|
? [
|
|
"https://alexanderwhitestone.com",
|
|
"https://www.alexanderwhitestone.com",
|
|
"https://alexanderwhitestone.ai",
|
|
"https://www.alexanderwhitestone.ai",
|
|
]
|
|
: [];
|
|
|
|
app.use(
|
|
cors({
|
|
origin:
|
|
allowedOrigins.length === 0
|
|
? true
|
|
: (origin, callback) => {
|
|
if (!origin || allowedOrigins.includes(origin)) {
|
|
callback(null, true);
|
|
} else {
|
|
callback(new Error(`CORS: origin '${origin}' not allowed`));
|
|
}
|
|
},
|
|
credentials: true,
|
|
methods: ["GET", "POST", "PATCH", "DELETE", "OPTIONS"],
|
|
allowedHeaders: ["Content-Type", "Authorization", "X-Session-Token", "X-Nostr-Token"],
|
|
exposedHeaders: ["X-Session-Token", "X-Nostr-Token"],
|
|
}),
|
|
);
|
|
|
|
app.use(express.json());
|
|
app.use(express.urlencoded({ extended: true }));
|
|
app.use(responseTimeMiddleware);
|
|
|
|
app.use("/api", router);
|
|
|
|
// ── Tower (Matrix 3D frontend) ───────────────────────────────────────────────
|
|
// Serve the pre-built Three.js world at /tower. WS client auto-connects to
|
|
// /api/ws on the same host. process.cwd() = workspace root at runtime.
|
|
const towerDist = path.resolve(process.cwd(), "the-matrix", "dist");
|
|
app.use("/tower", express.static(towerDist));
|
|
app.get("/tower/*splat", (_req, res) => res.sendFile(path.join(towerDist, "index.html")));
|
|
|
|
// Vite builds asset references as absolute /assets/... paths.
|
|
// Mirror them at the root so the browser can load them from /tower.
|
|
app.use("/assets", express.static(path.join(towerDist, "assets")));
|
|
app.use("/sw.js", express.static(path.join(towerDist, "sw.js")));
|
|
app.use("/manifest.json", express.static(path.join(towerDist, "manifest.json")));
|
|
|
|
app.get("/", (_req, res) => {
|
|
res.setHeader("Content-Type", "text/html");
|
|
res.send(`<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8"/>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
<title>Alexander Whitestone</title>
|
|
<style>
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
html, body {
|
|
height: 100%;
|
|
background: #050508;
|
|
color: #e8e8f0;
|
|
font-family: 'SF Mono', 'Fira Code', 'Courier New', monospace;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
overflow: hidden;
|
|
}
|
|
canvas {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 0;
|
|
opacity: 0.18;
|
|
}
|
|
main {
|
|
position: relative;
|
|
z-index: 1;
|
|
text-align: center;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 40px;
|
|
}
|
|
h1 {
|
|
font-family: system-ui, sans-serif;
|
|
font-size: clamp(1.4rem, 4vw, 2.4rem);
|
|
font-weight: 300;
|
|
letter-spacing: 0.25em;
|
|
text-transform: uppercase;
|
|
color: #c8c8d8;
|
|
}
|
|
h1 em {
|
|
font-style: normal;
|
|
color: #f7931a;
|
|
}
|
|
p {
|
|
color: #44445a;
|
|
font-size: 0.78rem;
|
|
letter-spacing: 0.15em;
|
|
max-width: 320px;
|
|
line-height: 1.8;
|
|
}
|
|
a.enter {
|
|
display: inline-block;
|
|
padding: 14px 48px;
|
|
border: 1px solid #2a2a3a;
|
|
border-radius: 4px;
|
|
color: #6b6b80;
|
|
font-size: 0.72rem;
|
|
letter-spacing: 0.3em;
|
|
text-transform: uppercase;
|
|
text-decoration: none;
|
|
transition: border-color 0.3s, color 0.3s;
|
|
cursor: pointer;
|
|
}
|
|
a.enter:hover {
|
|
border-color: #f7931a44;
|
|
color: #f7931a;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<canvas id="c"></canvas>
|
|
<main>
|
|
<h1>Alexander <em>Whitestone</em></h1>
|
|
<p>AI infrastructure & Lightning-native agents.</p>
|
|
<a class="enter" href="/tower">enter</a>
|
|
</main>
|
|
<script>
|
|
// Subtle falling-digit rain behind the landing page
|
|
const canvas = document.getElementById('c');
|
|
const ctx = canvas.getContext('2d');
|
|
let cols, drops;
|
|
function resize() {
|
|
canvas.width = window.innerWidth;
|
|
canvas.height = window.innerHeight;
|
|
cols = Math.floor(canvas.width / 18);
|
|
drops = Array.from({ length: cols }, () => Math.random() * -80 | 0);
|
|
}
|
|
resize();
|
|
window.addEventListener('resize', resize);
|
|
setInterval(() => {
|
|
ctx.fillStyle = 'rgba(5,5,8,0.15)';
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
ctx.fillStyle = '#f7931a';
|
|
ctx.font = '13px monospace';
|
|
drops.forEach((y, i) => {
|
|
const ch = Math.random() > 0.5
|
|
? String.fromCharCode(0x30A0 + Math.random() * 96 | 0)
|
|
: (Math.random() * 10 | 0).toString();
|
|
ctx.fillText(ch, i * 18, y * 18);
|
|
if (y * 18 > canvas.height && Math.random() > 0.97) drops[i] = 0;
|
|
else drops[i]++;
|
|
});
|
|
}, 60);
|
|
</script>
|
|
</body>
|
|
</html>`);
|
|
});
|
|
app.get("/api", (_req, res) => res.redirect("/api/ui"));
|
|
|
|
export default app;
|