UNPKG

@mesh-tech/mesh-cli

Version:

CLI for Mesh platform development utilities

799 lines (798 loc) 33.6 kB
import * as fs from "node:fs"; import * as path from "node:path"; import { logError, logInfo } from "../utils/index.js"; import { MeshCliError, emitJsonPayload } from "../utils/errors.js"; import { resolveTargetRoot } from "./skills.js"; import { ICONS, aggregateStatus, jsonReport, renderHuman, runChecks, } from "./dev-doctor.js"; export const APP_CHECK_RULES = { UI_IN_API: { gate: "0.1", title: "UI is its own service", severity: "block", remediation: 'move the pages to apps/<app>/ui and declare new mesh.apps.Service("ui", { env, src: "./ui", … })', scope: "app", }, NO_UI_SERVICE: { gate: "0.1", title: "UI service declared", severity: "block", remediation: 'add apps/<app>/ui + new mesh.apps.Service("ui", …) in index.ts', scope: "app", }, NO_REGISTER: { gate: "0.2", title: "App registers with the Hub", severity: "block", remediation: "end index.ts with `export const app = env.register({ description })`", scope: "app", }, CUSTOM_SESSION_AUTH: { gate: "0.3", title: "Sign-in through the platform", severity: "block", remediation: 'delete the OIDC + cookie code and protect the UI service with auth: { provider: "mesh" }', scope: "app", }, NON_PLATFORM_LOGGER: { gate: "0.4", title: "Logs through @mesh-tech/logger", severity: "block", remediation: "import { createLogger } from \"@mesh-tech/logger\" and log through it (requestLogger for HTTP)", scope: "app", }, LOCAL_AUTH_LIB: { gate: "0.5", title: "No tenant-local auth library", severity: "advisory", remediation: "check whether @mesh-tech/authn already owns this; if the platform lacks it, file a platform item", scope: "repo", }, NO_AUTHZ_POINTER: { gate: "H", title: "Access surfaced through the Hub", severity: "block", remediation: 'end index.ts with new mesh.auth.AppAuthzPointer("authz", { env, mode, opsHubMetadata: compileOpsHubMetadata(schemaDef) }) (role-gating) or { env, mode, opsHubMetadataRef: schema.opsHubMetadataRef } (policy-engine)', scope: "app", }, METADATA_UNPUBLISHED: { gate: "H", title: "Ops-hub metadata published", severity: "block", remediation: "export compileOpsHubMetadata(schemaDef) with mesh.exports.Export (role-gating) or let SpiceDBSchema publish it (policy-engine)", scope: "app", }, IAC_GRANTS: { gate: "H", title: "No role grants in IaC", severity: "block", remediation: "remove the per-user grant map; grant roles from the Hub (App → Access → People)", scope: "app", }, POINTER_PROJECT_OVERRIDE: { gate: "H", title: "Pointer publishes the env's project", severity: "advisory", remediation: "drop `zitadel:` from AppAuthzPointer and pass `zitadelAppProjectId: identity.projectId` to AppEnvironment — the override is the Hub's", scope: "app", }, PASSWORD_STORE: { gate: "H", title: "No app-side credentials", severity: "block", remediation: "delete the password code; the Hub creates sign-in users with a one-time temporary password", scope: "app", }, EMAIL_ALLOWLIST: { gate: "H", title: "No email allowlist for roles", severity: "block", remediation: "declare a role on the ZitadelAppIdentity and let the Hub grant it", scope: "app", }, USERS_TABLE: { gate: "H", title: "No hand-rolled users/roles table", severity: "block", remediation: "drop the table; people and roles live in Zitadel and are administered from the Hub", scope: "app", }, DOCS_SITE_UNAUTHED: { gate: "0.3", title: "Docs site behind the platform sign-in", severity: "block", remediation: 'on an API surface: docs: { auth: { clientId, clientSecret } } (or a client on surfaces.http.auth), or opt out with docs: { site: false }; on a mesh.apps.ApiDocs: auth: { provider: "mesh", clientId, clientSecret }, or drop the ingress (an in-cluster site is reachable through mesh dev)', scope: "app", }, DOCS_AUDIENCE_IMPLICIT: { gate: "H", title: "Docs audience stated, not derived", severity: "block", remediation: 'declare the surface as mesh.apps.AppApiSurface or VendorApiSurface (or pass kind: "app" | "vendor"), or state docs: { audience: { roles: […] } }', scope: "app", }, DOCS_AUDIENCE_UNGRANTABLE: { gate: "H", title: "Docs audience grantable from the Hub", severity: "block", remediation: 'declare every audience role on the ZitadelAppIdentity (roles: ["docs"]) so the Hub can grant it, and do not set idTokenRoleAssertion: false on the application the docs sign-in proxy uses (applications.<name>) — the identity asserts roles into the ID token by default, and the site\'s gate reads that token', scope: "app", }, }; export const APP_CHECK_CODES = Object.keys(APP_CHECK_RULES); const SRC_EXT = new Set([".ts", ".tsx", ".js", ".mjs", ".jsx"]); const SKIP_DIRS = new Set(["node_modules", "dist", "build", ".generated", "test", "tests", "__tests__", "fixtures", "scripts"]); const NON_SERVICE_DIRS = new Set([ "docs", "scripts", "lib", "libs", "migrations", "test", "tests", "node_modules", "infra", "mocks", "skills", "prisma", ]); function rel(root, abs) { return path.relative(root, abs).split(path.sep).join("/"); } function isDir(p) { try { return fs.statSync(p).isDirectory(); } catch { return false; } } function readText(p) { try { return fs.readFileSync(p, "utf8"); } catch { return ""; } } export function srcFiles(dir) { const out = []; const walk = (d) => { let entries; try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return; } for (const e of entries) { const p = path.join(d, e.name); if (e.isDirectory()) { if (SKIP_DIRS.has(e.name) || e.name.startsWith("scenario")) continue; walk(p); continue; } if (!e.isFile()) continue; if (!SRC_EXT.has(path.extname(e.name))) continue; if (/\.(test|spec)\./.test(e.name) || /\.config\./.test(e.name) || e.name === "build.mjs") continue; out.push(p); } }; walk(dir); return out.sort(); } function isTestHarness(name) { return name.startsWith("test-") || name.endsWith("-test"); } export function isUiDir(name) { return ["ui", "web", "frontend", "client", "portal-ui", "app"].includes(name) || name.endsWith("-ui"); } function serviceDirs(app) { let entries; try { entries = fs.readdirSync(app, { withFileTypes: true }); } catch { return []; } return entries .filter((e) => e.isDirectory() && !NON_SERVICE_DIRS.has(e.name) && !e.name.startsWith(".") && !isTestHarness(e.name)) .map((e) => path.join(app, e.name)) .sort(); } function authzSurface(app) { const hits = []; const walk = (d) => { let entries; try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return; } for (const e of entries) { const p = path.join(d, e.name); if (e.isDirectory()) { if (["node_modules", "dist", "build"].includes(e.name)) continue; walk(p); continue; } if (!e.isFile()) continue; if (!/\.(ts|tsx|yaml|sql)$/.test(e.name) || /\.(test|spec)\.tsx?$/.test(e.name) || e.name.endsWith(".d.ts")) continue; const lines = readText(p).split("\n"); lines.forEach((text, i) => { if (/^\s*(\/\/|\*|\/\*|#|--)/.test(text)) return; hits.push({ file: p, line: i + 1, text }); }); } }; walk(app); return hits; } const RE_HTML = /from ["']hono\/(html|jsx)["']|react-dom\/server|\bc\.html\(|<!doctype html|<!DOCTYPE html/; const RE_OIDC = /openid-client|code_verifier|authorization_code|buildAuthorizeUrl|end_session/; const RE_COOKIE = /setCookie\(|hono\/cookie|Set-Cookie/; const RE_LOGGER = /from ["'](pino|hono\/logger|winston|bunyan)["']|require\(["'](pino|winston|bunyan)["']\)|^[^/*]*\bconsole\.(log|info|warn|error)\(/; const RE_EXPOSURE = /ZitadelAppIdentity|createOAuthMiddleware|createAuthorizationMiddleware|zitadelJwtScheme|createAuthzMiddleware|bearerAuth|type: *"oidc"|^\s+auth: *\{|_ROLES *= *\{|coarseRoles|ZITADEL_(AUDIENCE|CLIENT_ID)|OIDC_CLIENT_ID|zitadel\.(ApplicationOidc|ApplicationApi|Project|UserGrant)\(/; const RE_POINTER = /AppAuthzPointer/; const RE_METADATA = /compileOpsHubMetadata|ops-hub-metadata|SpiceDBSchema/; const RE_IAC_GRANT = /userId: *"[0-9]{6,}"/; const RE_POINTER_OVERRIDE = /\bzitadel: *\{\s*projectId\b/; const RE_PASSWORD = /(^|[^a-z])(bcrypt|argon2)|password_hash|passwordHash|hashPassword|temporaryPassword.*(send|mail)|(send|mail).*temporaryPassword/; const RE_ALLOWLIST = /ADMIN_EMAILS|ALLOWED_EMAILS|allowedEmails|adminEmails/; const RE_USERS_TABLE = /create table (if not exists )?"?(users|roles|user_roles|permissions)"?/i; const RE_SURFACE_NEW = /new\s+mesh\.apps\.(ApiSurface|AppApiSurface|VendorApiSurface|Integration)\s*\(/g; const RE_APIDOCS_NEW = /new\s+mesh\.apps\.ApiDocs\s*\(/g; const RE_IDENTITY_NEW = /new\s+mesh\.auth\.(ZitadelAppIdentity)\s*\(/g; const RE_IDENTITY_BINDING = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*new\s+mesh\.auth\.ZitadelAppIdentity\s*\(/g; const DEFAULT_PARTNER_DOCS_ROLES = ["docs"]; const RE_IDENTITY_APP_CLIENT = /\bapplications\.([A-Za-z_$][\w$]*)\.clientId\b/; export function stringList(value) { if (!value) return undefined; const text = value.trim(); if (!text.startsWith("[") || !text.endsWith("]")) return undefined; const out = []; for (const entry of splitTopLevel(text.slice(1, -1))) { const item = stripLeadingComments(entry).trim(); if (!item) continue; const m = /^(["'])((?:(?!\1).)*)\1$/.exec(stripTrailingComments(item)); if (!m) return undefined; out.push(m[2]); } return out; } const DOCS_SITE_FIELDS = ["pages", "playground", "audience", "basePath", "ingress", "auth"]; const RE_DEV_BYPASS = /^["']dev-bypass["']$/; export function balancedSpan(text, open) { const closers = { "(": ")", "{": "}", "[": "]" }; const stack = []; let quote; for (let i = open; i < text.length; i++) { const ch = text[i]; if (quote) { if (ch === "\\") i++; else if (ch === quote) quote = undefined; continue; } if (ch === '"' || ch === "'" || ch === "`") { quote = ch; continue; } if (ch === "/" && text[i + 1] === "/") { const eol = text.indexOf("\n", i); if (eol === -1) return ""; i = eol; continue; } if (ch === "/" && text[i + 1] === "*") { const end = text.indexOf("*/", i + 2); if (end === -1) return ""; i = end + 1; continue; } if (closers[ch]) stack.push(closers[ch]); else if (ch === ")" || ch === "}" || ch === "]") { if (stack.pop() !== ch) return ""; if (stack.length === 0) return text.slice(open, i + 1); } } return ""; } export function splitTopLevel(inner) { const parts = []; let depth = 0; let quote; let start = 0; for (let i = 0; i < inner.length; i++) { const ch = inner[i]; if (quote) { if (ch === "\\") i++; else if (ch === quote) quote = undefined; continue; } if (ch === '"' || ch === "'" || ch === "`") { quote = ch; continue; } if (ch === "/" && inner[i + 1] === "/") { const eol = inner.indexOf("\n", i); if (eol === -1) break; i = eol; continue; } if (ch === "/" && inner[i + 1] === "*") { const end = inner.indexOf("*/", i + 2); if (end === -1) break; i = end + 1; continue; } if (ch === "(" || ch === "{" || ch === "[") depth++; else if (ch === ")" || ch === "}" || ch === "]") depth--; else if (ch === "," && depth === 0) { parts.push(inner.slice(start, i)); start = i + 1; } } const last = inner.slice(start); if (last.trim()) parts.push(last); return parts; } const RE_PROPERTY = /^(?:"([^"]+)"|'([^']+)'|([A-Za-z_$][\w$]*))\s*(?::\s*([\s\S]*))?$/; function stripLeadingComments(entry) { let text = entry; for (;;) { const next = text.replace(/^\s*(?:\/\/[^\n]*(?:\n|$)|\/\*[\s\S]*?\*\/)/, ""); if (next === text) return text; text = next; } } export function stripTrailingComments(value) { let out = ""; let depth = 0; let quote; for (let i = 0; i < value.length; i++) { const ch = value[i]; if (quote) { out += ch; if (ch === "\\") { out += value[i + 1] ?? ""; i++; } else if (ch === quote) quote = undefined; continue; } if (ch === '"' || ch === "'" || ch === "`") { quote = ch; out += ch; continue; } if (depth === 0 && ch === "/" && value[i + 1] === "/") { const eol = value.indexOf("\n", i); if (eol === -1) break; i = eol; out += "\n"; continue; } if (depth === 0 && ch === "/" && value[i + 1] === "*") { const end = value.indexOf("*/", i + 2); if (end === -1) break; i = end + 1; continue; } if (ch === "(" || ch === "{" || ch === "[") depth++; else if (ch === ")" || ch === "}" || ch === "]") depth--; out += ch; } return out.trim(); } export function topLevelProperties(objectText) { const props = new Map(); if (!objectText.startsWith("{") || !objectText.endsWith("}")) return props; for (const entry of splitTopLevel(objectText.slice(1, -1))) { const text = stripLeadingComments(entry).trim(); if (!text || text.startsWith("...")) continue; const m = RE_PROPERTY.exec(text); if (!m) continue; const key = m[1] ?? m[2] ?? m[3]; props.set(key, stripTrailingComments(m[4] ?? key)); } return props; } function objectLiteralOf(value, program) { if (!value) return undefined; if (value.startsWith("{")) return balancedSpan(value, 0) || undefined; const id = /^[A-Za-z_$][\w$]*$/.exec(value)?.[0]; if (!id) return undefined; const decl = new RegExp(`\\bconst\\s+${id}\\s*(?::[^=]+)?=\\s*\\{`).exec(program); return decl ? balancedSpan(program, decl.index + decl[0].length - 1) || undefined : undefined; } function optionsObject(body) { for (const arg of splitTopLevel(body.slice(1, -1))) { const trimmed = arg.trim(); if (trimmed.startsWith("{")) return balancedSpan(trimmed, 0) || undefined; } return undefined; } export function surfaceDeclarations(text, pattern = RE_SURFACE_NEW) { const out = []; for (const m of text.matchAll(pattern)) { const body = balancedSpan(text, m.index + m[0].length - 1); if (!body) continue; out.push({ ctor: m[1] ?? "ApiDocs", body, line: text.slice(0, m.index).split("\n").length }); } return out; } function isClientAuth(value) { return value !== undefined && value !== "undefined" && !RE_DEV_BYPASS.test(value); } export function docsSiteFindings(text) { const findings = []; for (const decl of surfaceDeclarations(text)) { const options = optionsObject(decl.body); if (!options) continue; const args = topLevelProperties(options); const docsText = objectLiteralOf(args.get("docs"), text); if (!docsText) continue; const docs = topLevelProperties(docsText); if (docs.get("site") === "false") continue; if (docs.get("site") !== "true" && !DOCS_SITE_FIELDS.some((f) => docs.has(f))) continue; const httpText = objectLiteralOf(topLevelProperties(objectLiteralOf(args.get("surfaces"), text) ?? "{}").get("http"), text); const hasClient = isClientAuth(docs.get("auth")) || isClientAuth(topLevelProperties(httpText ?? "{}").get("auth")); if (!hasClient) { findings.push({ code: "DOCS_SITE_UNAUTHED", line: decl.line, why: `mesh.apps.${decl.ctor} declares a docs site with no OIDC client — the site is served behind the platform sign-in proxy, which needs one (docs.auth, or a client on surfaces.http.auth)`, }); } const kindStated = decl.ctor === "AppApiSurface" || decl.ctor === "VendorApiSurface" || args.has("kind"); if (!kindStated && !docs.has("audience")) { findings.push({ code: "DOCS_AUDIENCE_IMPLICIT", line: decl.line, why: `mesh.apps.${decl.ctor} declares a docs site but neither its kind nor its audience — who may read it would fall out of the name heuristic, and a vendor surface must never default to partner-readable that way`, }); } for (const f of docsAudienceUngrantable(text, decl, args, docs, httpText)) findings.push(f); } for (const decl of surfaceDeclarations(text, RE_APIDOCS_NEW)) { const options = optionsObject(decl.body); if (!options) continue; const args = topLevelProperties(options); if (!args.has("ingress") || args.get("ingress") === "undefined") continue; if (isClientAuth(args.get("auth"))) continue; findings.push({ code: "DOCS_SITE_UNAUTHED", line: decl.line, why: `mesh.apps.ApiDocs is published on an ingress with no auth — the site (reference and playground) is reachable by anyone; put the platform sign-in proxy in front of it, or drop the ingress`, }); } return findings; } function docsAudienceUngrantable(text, decl, args, docs, httpText) { const findings = []; let roles; if (docs.has("audience")) { const audience = objectLiteralOf(docs.get("audience"), text); roles = audience ? stringList(topLevelProperties(audience).get("roles")) : undefined; } else { const kind = decl.ctor === "AppApiSurface" ? "app" : decl.ctor === "VendorApiSurface" ? "vendor" : /^["']([a-z]+)["']$/.exec(args.get("kind") ?? "")?.[1]; if (kind === "app") roles = DEFAULT_PARTNER_DOCS_ROLES; } if (!roles || roles.length === 0) return findings; const identities = []; for (const d of surfaceDeclarations(text, RE_IDENTITY_NEW)) { const options = optionsObject(d.body); if (!options) continue; const binding = [...text.matchAll(RE_IDENTITY_BINDING)].find((m) => text.indexOf(d.body, m.index) === m.index + m[0].length - 1)?.[1]; identities.push({ binding, options: topLevelProperties(options) }); } if (identities.length === 0) return findings; const docsAuth = objectLiteralOf(docs.get("auth"), text); const clientExpr = (docsAuth ? topLevelProperties(docsAuth).get("clientId") : undefined) ?? (httpText ? topLevelProperties(objectLiteralOf(topLevelProperties(httpText).get("auth"), text) ?? "{}").get("clientId") : undefined); const appName = clientExpr ? RE_IDENTITY_APP_CLIENT.exec(clientExpr)?.[1] : undefined; const clientRoot = clientExpr ? /^([A-Za-z_$][\w$]*)\.applications\./.exec(clientExpr.trim())?.[1] : undefined; const identity = (clientRoot ? identities.find((id) => id.binding === clientRoot) : undefined) ?? (identities.length === 1 ? identities[0] : undefined); if (!identity) return findings; const rolesText = identity.options.get("roles"); const coarseText = identity.options.get("coarseRoles"); const declared = rolesText !== undefined ? stringList(rolesText) : coarseText !== undefined ? stringList(coarseText) : []; if (declared) { for (const role of roles) { if (declared.includes(role)) continue; findings.push({ code: "DOCS_AUDIENCE_UNGRANTABLE", line: decl.line, why: `the docs audience gates on role "${role}", which the app's ZitadelAppIdentity does not declare in roles — the Hub cannot grant it, so no one can read the site`, }); } } if (!appName) return findings; const apps = objectLiteralOf(identity.options.get("applications"), text); const app = apps ? objectLiteralOf(topLevelProperties(apps).get(appName), text) : undefined; if (app && topLevelProperties(app).get("idTokenRoleAssertion") === "false") { findings.push({ code: "DOCS_AUDIENCE_UNGRANTABLE", line: decl.line, why: `the docs sign-in client is applications.${appName} on the ZitadelAppIdentity, which sets idTokenRoleAssertion: false — the proxy's ID token then carries no roles and the site's gate admits nobody`, }); } return findings; } export function listApps(root) { const apps = []; const appsDir = path.join(root, "apps"); if (isDir(appsDir)) { for (const e of fs.readdirSync(appsDir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) { if (!e.isDirectory()) continue; const d = path.join(appsDir, e.name); if (fs.existsSync(path.join(d, "index.ts")) || fs.existsSync(path.join(d, "Pulumi.yaml"))) apps.push(rel(root, d)); } } if (fs.existsSync(path.join(root, "index.ts")) && fs.existsSync(path.join(root, "Pulumi.yaml"))) apps.push("."); return apps; } export function scanApp(root, appRel) { const app = path.resolve(root, appRel); const index = path.join(app, "index.ts"); const findings = []; const hit = (code, p, why) => { findings.push({ code, path: p, why }); }; const hasProgram = fs.existsSync(index) || fs.existsSync(path.join(app, "Pulumi.yaml")); if (!hasProgram) return findings; if (fs.existsSync(index)) { const indexRel = rel(root, index); let servesHtml = false; let hasUiService = false; for (const svc of serviceDirs(app)) { const svcName = path.basename(svc); if (isUiDir(svcName)) { hasUiService = true; continue; } for (const f of srcFiles(svc)) { let why = ""; if (/\.(tsx|jsx)$/.test(f)) why = `JSX view file inside the ${svcName} service`; else if (RE_HTML.test(readText(f))) why = `renders HTML inside the ${svcName} service`; if (why) { servesHtml = true; hit("UI_IN_API", rel(root, f), `${why} — a browser UI is its own mesh.apps.Service (apps/<app>/ui)`); } } } if (servesHtml && !hasUiService) { hit("NO_UI_SERVICE", indexRel, 'the app serves HTML but declares no ui/ service — add apps/<app>/ui + new mesh.apps.Service("ui", …)'); } if (!/env\.register\(|\.annotate\(/.test(readText(index))) { hit("NO_REGISTER", indexRel, "never calls env.register({…}) — the Hub cannot list this app"); } for (const f of docsSiteFindings(readText(index))) hit(f.code, `${indexRel}:${f.line}`, f.why); for (const svc of serviceDirs(app)) { for (const f of srcFiles(svc)) { const text = readText(f); if (RE_OIDC.test(text) && RE_COOKIE.test(text)) { hit("CUSTOM_SESSION_AUTH", rel(root, f), 'app-written OIDC + cookie session — protect the UI service with auth: { provider: "mesh" } instead'); } } } for (const svc of serviceDirs(app)) { for (const f of srcFiles(svc)) { const lines = readText(f).split("\n"); const i = lines.findIndex((l) => RE_LOGGER.test(l) && !l.includes("mesh-tech/logger")); if (i === -1) continue; hit("NON_PLATFORM_LOGGER", `${rel(root, f)}:${i + 1}`, `logs outside @mesh-tech/logger (${lines[i]?.trim() ?? ""})`); } } } const surface = authzSurface(app); const first = (re) => surface.find((h) => re.test(h.text)); const exposure = first(RE_EXPOSURE); const pointer = first(RE_POINTER); const metadata = first(RE_METADATA); const appLabel = appRel === "." ? "(root)" : path.basename(appRel); if (exposure && !pointer) { const at = surface.find((h) => RE_EXPOSURE.test(h.text) && /(^|\/)index\.ts$/.test(h.file)) ?? exposure; hit("NO_AUTHZ_POINTER", rel(root, at.file), `${appLabel} authenticates callers or declares roles but publishes no mesh.auth.AppAuthzPointer — the Hub's Access tab is blank for this app`); } else if (exposure && pointer && !metadata) { hit("METADATA_UNPUBLISHED", rel(root, pointer.file), "AppAuthzPointer present but no compiled ops-hub metadata (opsHubMetadata: compileOpsHubMetadata(schemaDef) on the pointer, or a SpiceDBSchema) — the Hub's create-key and role catalog 409 METADATA_UNPUBLISHED"); } const anti = (code, re, why) => { const files = [...new Set(surface.filter((h) => re.test(h.text)).map((h) => h.file))].sort().slice(0, 3); for (const f of files) hit(code, rel(root, f), why); }; anti("IAC_GRANTS", RE_IAC_GRANT, "per-user role grant baked into IaC — grants belong in the Hub (Access → People), not in a config redeploy"); anti("POINTER_PROJECT_OVERRIDE", RE_POINTER_OVERRIDE, "AppAuthzPointer is given a `zitadel:` override — an app binds its project with AppEnvironment's zitadelAppProjectId so every consumer of env.zitadel agrees; the override is reserved for the Hub"); anti("PASSWORD_STORE", RE_PASSWORD, "password storage or hashing — Zitadel owns credentials; the Hub creates sign-in users with a one-time temporary password"); anti("EMAIL_ALLOWLIST", RE_ALLOWLIST, "email allowlist standing in for roles — declare a role and let the Hub grant it"); anti("USERS_TABLE", RE_USERS_TABLE, "hand-rolled users/roles table — people and roles live in Zitadel and are administered from the Hub"); return findings; } function isPlatformPackage(dir) { try { const pkg = JSON.parse(readText(path.join(dir, "package.json"))); return typeof pkg.name === "string" && pkg.name.startsWith("@mesh-tech/"); } catch { return false; } } export function scanRepo(root) { const findings = []; for (const parent of ["libs", "packages"]) { const dir = path.join(root, parent); if (!isDir(dir)) continue; for (const e of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) { if (!e.isDirectory() || !e.name.includes("auth")) continue; const lib = path.join(dir, e.name); if (isPlatformPackage(lib)) continue; const verifies = srcFiles(lib) .filter((f) => f.endsWith(".ts")) .some((f) => /introspect|jwtVerify|createRemoteJWKSet|from ["']jose["']/.test(readText(f))); if (verifies) { findings.push({ code: "LOCAL_AUTH_LIB", path: rel(root, lib), why: "tenant-local token verification — check whether @mesh-tech/authn already owns this", }); } } } return findings; } export function resultFor(code, findings) { const rule = APP_CHECK_RULES[code]; const mine = findings.filter((f) => f.code === code); if (mine.length === 0) return { status: "ok", summary: `gate ${rule.gate}: no ${code} finding` }; return { status: rule.severity === "block" ? "error" : "warn", summary: `gate ${rule.gate}: ${code} × ${mine.length}`, detail: mine.map((f) => `${f.path} — ${f.why}`).join("\n "), remediation: rule.remediation, }; } export function appContractChecks(scope) { return APP_CHECK_CODES.filter((code) => APP_CHECK_RULES[code].scope === scope).map((code) => ({ id: code, title: `${APP_CHECK_RULES[code].title} (${APP_CHECK_RULES[code].gate})`, phases: ["ondemand", "preflight"], run: async (ctx) => resultFor(code, ctx.findings), })); } export async function runAppCheck(root, apps) { if (!isDir(root)) throw new MeshCliError(`no such checkout: ${root}`); const targets = apps && apps.length > 0 ? apps.map((a) => a.replace(/\/+$/, "")) : listApps(root); for (const a of targets) { if (!isDir(path.resolve(root, a))) throw new MeshCliError(`no such app dir: ${a}`); } const scopes = []; for (const app of targets) { const results = await runChecks({ root, app, findings: scanApp(root, app) }, "ondemand", appContractChecks("app")); scopes.push({ scope: app, ...jsonReport(results) }); } const repoResults = await runChecks({ root, app: null, findings: scanRepo(root) }, "ondemand", appContractChecks("repo")); scopes.push({ scope: "(repo)", ...jsonReport(repoResults) }); return { root, status: aggregateStatus(scopes.map((s) => ({ status: s.status, summary: "" }))), scopes }; } export function renderReport(report, opts) { const out = []; for (const scope of report.scopes) { const shown = opts.verbose ? scope.checks : scope.checks.filter((c) => c.status !== "ok"); if (shown.length === 0 && !opts.verbose) continue; out.push(`${ICONS[scope.status]} ${scope.scope}`); out.push(renderHuman(shown.map((c) => ({ check: { id: c.id, title: `${c.id} — ${APP_CHECK_RULES[c.id]?.title ?? c.id}` }, result: { status: c.status, summary: c.summary, remediation: c.remediation ?? undefined, detail: c.detail ?? undefined }, }))) .split("\n") .map((l) => ` ${l}`) .join("\n")); } out.push(`${ICONS[report.status]} app contract: ${report.status}`); return out.filter(Boolean).join("\n"); } export function registerAppCommands(program) { const app = program.command("app").description("Inspect a tenant app against the Mesh app contract"); app .command("check") .description("Check tenant apps against the Mesh app contract (@mesh-tech/app-kit skills/apps/references/app-contract.md): a UI is its own service, the app registers with the Hub, sign-in goes through the platform proxy, logs go through @mesh-tech/logger, and people/roles/keys are surfaced through the Hub. Exit 1 on any blocking finding — the same gate mesh create-app, tenant CI and the platform reviewer run.") .argument("[apps...]", "repo-relative app dirs to check (default: every apps/* with a Pulumi program)") .option("--root <dir>", "checkout to scan (default: the enclosing git root)") .option("--json", "machine-readable output (one report per app plus the repo-scoped checks)", false) .option("--verbose", "also print the gates that passed", false) .action(async (apps, opts) => { const root = path.resolve(opts.root ?? resolveTargetRoot()); const report = await runAppCheck(root, apps); if (opts.json) { emitJsonPayload({ ok: report.status !== "error", ...report }); } else { if (report.scopes.length === 1) logInfo(`no apps/* with a Pulumi program under ${root}`); console.log(renderReport(report, { verbose: opts.verbose })); } if (report.status === "error") { if (!opts.json) logError("the app contract is not met — every ✗ above names the gate, the file and the replacement"); process.exitCode = 1; } }); }