/** * {{titleCase name}} API — platform auth on both planes. * * authn (@mesh-tech/authn): verifies Zitadel JWTs (zitadel-jwt scheme). * authz (@mesh-tech/authz): schema-driven permission checks via SpiceDB. * * Routes: * GET /health public (no token) * GET /api/me authenticated (valid JWT → 200, none → 401) * GET /api/report permission-gated (needs report:view via SpiceDB → else 403) * * Environment (mesh dev injects all of these — local or tethered): * PORT, ZITADEL_ISSUER, ZITADEL_PROJECT_ID, SPICEDB_HTTP_ENDPOINT, SPICEDB_TOKEN */ import { Hono } from "hono"; import { serve } from "@hono/node-server"; import { createAuthn } from "@mesh-tech/authn"; import { zitadelJwtScheme } from "@mesh-tech/authn/schemes/zitadel-jwt"; import { createAuthnMiddleware, type AuthnVariables } from "@mesh-tech/authn/hono"; import { createAuthz } from "@mesh-tech/authz/runtime"; import { createAuthzMiddleware, type AuthzVariables } from "@mesh-tech/authz/hono"; import { createLogger, requestLogger } from "@mesh-tech/logger"; import { schema } from "./schema.js"; import { spiceDbHttpProvider } from "./spicedb-http-provider.js"; const PORT = parseInt(process.env.PORT ?? "3000", 10); const ZITADEL_ISSUER = process.env.ZITADEL_ISSUER ?? "http://localhost:8080"; // Every service logs through @mesh-tech/logger (the Mesh app contract, gate 0.4). const log = createLogger({ service: "{{name}}-api" }); // --- authn: who is calling --- // Audience: locally, mesh dev injects the seeded CLI app's clientId // (tokens come from `mesh login local`); deployed, the app's OWN Zitadel // project id (ZitadelAppIdentity in index.ts) is the audience, and passing it // as `projectId` makes the scheme read the project-keyed roles claim. const ZITADEL_PROJECT_ID = process.env.ZITADEL_PROJECT_ID; const AUTH_AUDIENCE = process.env.AUTH_AUDIENCE ?? ZITADEL_PROJECT_ID ?? "{{name}}"; const authn = createAuthn({ schemes: { "zitadel-jwt": zitadelJwtScheme({ issuer: ZITADEL_ISSUER, audience: AUTH_AUDIENCE, ...(ZITADEL_PROJECT_ID ? { projectId: ZITADEL_PROJECT_ID } : {}), }), }, }); const { middleware: requireAuth, public: publicRoute } = createAuthnMiddleware(authn); // --- authz: what they may do --- const authz = createAuthz({ schemas: { "{{name}}": { schema, provider: spiceDbHttpProvider({ endpoint: process.env.SPICEDB_HTTP_ENDPOINT ?? "http://localhost:8443", // "local-dev-key" is the `mesh start` stack's key — local ONLY. In // any deployed env `spicedb.link()` (index.ts) injects SPICEDB_TOKEN; // fail loudly rather than silently authing with the public default. presharedKey: process.env.SPICEDB_TOKEN ?? process.env.SPICEDB_PRESHARED_KEY ?? (process.env.NODE_ENV === "production" ? (() => { throw new Error("SPICEDB_TOKEN is required in production"); })() : "local-dev-key"), }), }, }, }); // Explicit subject mapping: authenticated humans are the schema's `user` // subject. Required, not optional — the default resolver types EVERY caller as // `api_key`, and a relation cannot target `api_key`, so this app's // `viewer: { subject: "user" }` relation would never match without this. const m = createAuthzMiddleware(authz, { resolveSubject: (c) => ({ type: "user", id: (c as { get(key: "auth"): { subjectSub: string } }).get("auth").subjectSub }), }); // --- routes --- const app = new Hono<{ Variables: AuthnVariables & AuthzVariables }>(); app.use("*", requestLogger(log)); // Open: no token needed (also excluded from the oauth2-proxy in the cloud) app.get("/health", publicRoute(), (c) => c.json({ status: "ok" })); // Everything under /api requires a valid Zitadel JWT… app.use("/api/*", requireAuth()); app.use("/api/*", m.authContextMiddleware()); // …authenticated is enough here: app.get("/api/me", (c) => c.json({ sub: c.var.auth.subjectSub, email: c.var.auth.claims?.email }), ); // …and this one additionally needs the report:view permission (SpiceDB): app.get("/api/report", m.require("report", "view", { id: () => "quarterly" }), (c) => c.json({ id: "quarterly", title: "Quarterly Report", classification: "internal" }), ); serve({ fetch: app.fetch, port: PORT }, () => { log.info({ port: PORT, issuer: ZITADEL_ISSUER }, "{{name}} api listening"); });