@mesh-tech/mesh-cli
Version:
CLI for Mesh platform development utilities
799 lines (798 loc) • 33.6 kB
JavaScript
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\(| 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;
}
});
}