Replace TS game shell with Bevy/Rust client, add auth service, purge legacy archive

- Replace apps/game (TypeScript/Vite/R3F) with a Bevy 0.16 Rust client
  featuring main menu, star map, and flight states
- Add services/auth: Express + better-auth server with SpacetimeDB
  token exchange endpoint
- Delete archive/legacy-static/ (old JS demos, CSS, assets)
- Update docs pages (architecture, gameplay, roadmap, social, overview)
  to reflect Bevy pivot
- Clean up workspace config: remove apps/game from pnpm workspace,
  update dev scripts, tsconfig, and AGENTS.md files
- Add .vscode/settings.json for Rust tooling
This commit is contained in:
2026-06-04 01:10:02 -04:00
parent 7c93b8a1ae
commit a1717e12db
138 changed files with 7352 additions and 24075 deletions

BIN
services/auth/auth.db Normal file

Binary file not shown.

View File

@@ -0,0 +1,24 @@
{
"name": "@void-nav/auth",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts",
"start": "tsx src/index.ts",
"migrate": "npx auth migrate"
},
"dependencies": {
"better-auth": "^1.2.0",
"better-sqlite3": "^12.0.0",
"cors": "^2.8.5",
"express": "^5.1.0"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.3",
"tsx": "^4.19.4",
"typescript": "^5.8.3"
}
}

38
services/auth/src/auth.ts Normal file
View File

@@ -0,0 +1,38 @@
import { betterAuth } from "better-auth";
import Database from "better-sqlite3";
import { jwt } from "better-auth/plugins";
import { oidcProvider } from "better-auth/plugins";
export const auth = betterAuth({
database: new Database("./auth.db"),
baseURL: process.env.BETTER_AUTH_URL ?? "http://localhost:4000",
emailAndPassword: {
enabled: true,
},
session: {
cookieCache: {
enabled: true,
maxAge: 60 * 5, // 5 minutes
},
},
plugins: [
jwt({
// SpacetimeDB will verify JWTs via JWKS
jwks: true,
// Include standard OIDC claims that SpacetimeDB expects
definePayload: async ({ session, user }) => ({
sub: user.id,
email: user.email,
name: user.name,
}),
}),
oidcProvider({
loginPage: "/sign-in",
metadata: {
issuer: process.env.BETTER_AUTH_URL ?? "http://localhost:4000",
},
}),
],
});
export type Auth = typeof auth;

View File

@@ -0,0 +1,53 @@
import express from "express";
import cors from "cors";
import { toNodeHandler, fromNodeHeaders } from "better-auth/node";
import { auth } from "./auth.js";
const app = express();
const PORT = Number(process.env.AUTH_PORT ?? 4000);
app.use(
cors({
origin: [
"http://localhost:5173",
"http://localhost:5174",
"http://localhost:5175",
],
credentials: true,
}),
);
// Mount better-auth handler FIRST — it handles /api/auth/*splat
// Note: do NOT use express.json() before this handler
app.all("/api/auth/*splat", toNodeHandler(auth));
// Mount json middleware only for routes below
app.use(express.json());
// Token exchange endpoint: game client calls this with its session cookie
// and receives a JWT suitable for SpacetimeDB
app.get("/api/auth/spacetimedb-token", async (req, res) => {
const session = await auth.api.getSession({
headers: fromNodeHeaders(req.headers),
});
if (!session) {
return res.status(401).json({ error: "Not authenticated" });
}
// Get a JWT for this session
const jwtResult = await auth.api.getToken({
headers: fromNodeHeaders(req.headers),
});
if (!jwtResult?.token) {
return res.status(500).json({ error: "Failed to generate token" });
}
return res.json({ token: jwtResult.token });
});
app.listen(PORT, () => {
console.log(`[auth] Auth server running on http://localhost:${PORT}`);
console.log(`[auth] OIDC issuer: ${process.env.BETTER_AUTH_URL ?? `http://localhost:${PORT}`}`);
});

View File

@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"types": ["node"]
},
"include": ["src"]
}

View File

@@ -3,7 +3,7 @@
Package: `@void-nav/spacetimedb`
Single file: `src/index.ts` (~595 lines)
This is the **entire server-side game logic** — all tables, all reducers, all validation. SpacetimeDB runs this as a hosted module; clients subscribe to tables and call reducers.
This is the **entire server-side game logic** — all tables, all reducers, all validation. SpacetimeDB runs this as a hosted module; clients subscribe to tables and call reducers. Designed for single-player local use first, with the architecture supporting optional co-op servers later.
## Tables (9)

View File

@@ -1,4 +1,9 @@
import { schema, table, t } from "spacetimedb/server";
import { schema, table, t, SenderError } from "spacetimedb/server";
// Better Auth OIDC configuration — must match the BETTER_AUTH_URL env var of the auth server.
// SpacetimeDB modules don't have access to process.env, so this is hardcoded.
const BETTER_AUTH_ISSUER = "http://localhost:4000";
const OIDC_AUDIENCE = BETTER_AUTH_ISSUER;
const STARTER_SYSTEM_ID = "solace";
const STARTER_STATION_ID = "solace-prime";
@@ -135,6 +140,29 @@ const spacetimedb = schema({
});
export default spacetimedb;
// ── OIDC Authentication ────────────────────────────────────────────────────────
// Validate incoming JWTs from Better Auth when clients connect.
// SpacetimeDB decodes the JWT automatically; we verify issuer & audience.
export const onConnect = spacetimedb.clientConnected((ctx) => {
const jwt = ctx.senderAuth.jwt;
if (jwt == null) {
throw new SenderError("Unauthorized: JWT is required to connect.");
}
// Restrict to tokens issued by our Better Auth server
if (jwt.issuer !== BETTER_AUTH_ISSUER) {
throw new SenderError(`Unauthorized: Invalid issuer '${jwt.issuer}'.`);
}
// Verify the audience matches our application
if (!jwt.audience.some((aud) => aud === OIDC_AUDIENCE)) {
throw new SenderError(`Unauthorized: Invalid audience '${jwt.audience}'.`);
}
// Client authenticated successfully — sub: jwt.subject, iss: jwt.issuer
});
export const seedWorld = spacetimedb.reducer({}, (ctx) => {
seedStarterWorld(ctx);
writeEvent(ctx, "world_seeded", "Starter system and station are available.");