@mesh-tech/mesh-cli
Version:
CLI for Mesh platform development utilities
444 lines (443 loc) • 19.3 kB
JavaScript
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import { hubOperatorRoleKeys, hubRoleKeyTenant, restrictedRoleBase } from "@mesh-tech/api-registry";
import { logInfo, logSuccess, logWarn } from "../../utils/log.js";
import { MeshCliError } from "../../utils/errors.js";
import { writeContextConfig } from "../login.js";
import { compose, hubPort, DEFAULT_HUB_PORT } from "./stack.js";
import { LOCAL_TENANT, LOCAL_ENV } from "./seed.js";
import { meshCacheDir } from "../../utils/cache-home.js";
export const ZITADEL_ISSUER = "http://localhost:8080";
export const LOGIN_CONTEXT = "local";
export const PLATFORM_ORG = "mesh";
export const CLI_PROJECT_NAME = "mesh-cli";
export const CLI_APP_NAME = "cli";
export const CLI_REDIRECT_URI = "http://localhost:9876/callback";
export const HUB_PROJECT_NAME = "hub";
export const HUB_APP_NAME = "ui";
export const hubRedirectUri = () => `http://localhost:${hubPort()}/oauth2/callback`;
export const HUB_DEFAULT_REDIRECT_URI = `http://localhost:${DEFAULT_HUB_PORT}/oauth2/callback`;
export const HUB_ROLES = hubOperatorRoleKeys([LOCAL_TENANT]);
export const ZITADEL_SSM_PARAM = `/mesh-platform/${LOCAL_TENANT}/${LOCAL_ENV}/platform/zitadel`;
export const TEST_USERS_SSM_PREFIX = `/mesh-platform/${LOCAL_TENANT}/${LOCAL_ENV}/temporal/test-users`;
export const TEST_USERS = [
{
name: "dev",
email: "dev@local.mesh",
firstName: "Dev",
lastName: "User",
password: "LocalDev1!",
},
{
name: "ops",
email: "ops@local.mesh",
firstName: "Ops",
lastName: "User",
password: "LocalDev1!",
},
];
export function buildCliAppPayload() {
return {
name: CLI_APP_NAME,
redirectUris: [CLI_REDIRECT_URI],
responseTypes: ["OIDC_RESPONSE_TYPE_CODE"],
grantTypes: ["OIDC_GRANT_TYPE_AUTHORIZATION_CODE", "OIDC_GRANT_TYPE_DEVICE_CODE"],
appType: "OIDC_APP_TYPE_NATIVE",
authMethodType: "OIDC_AUTH_METHOD_TYPE_NONE",
accessTokenType: "OIDC_TOKEN_TYPE_JWT",
accessTokenRoleAssertion: true,
idTokenRoleAssertion: true,
idTokenUserinfoAssertion: true,
devMode: true,
};
}
export function readSeederPat() {
const tmp = path.join(fs.mkdtempSync(path.join(os.tmpdir(), "mesh-zitadel-")), "pat.txt");
try {
compose(["cp", "zitadel:/machinekey/pat.txt", tmp]);
const pat = fs.readFileSync(tmp, "utf-8").trim();
if (!pat)
throw new Error("empty PAT file");
return pat;
}
catch {
throw new MeshCliError("Zitadel seeder PAT not found — the zitadel volume predates the bootstrap machine user (first-instance settings only apply on the first init).", { remediation: { command: "mesh stop --destroy && mesh start" } });
}
finally {
fs.rmSync(path.dirname(tmp), { recursive: true, force: true });
}
}
export async function api(pat, method, apiPath, body, orgId) {
const res = await fetch(`${ZITADEL_ISSUER}${apiPath}`, {
method,
headers: {
Authorization: `Bearer ${pat}`,
"Content-Type": "application/json",
...(orgId ? { "x-zitadel-orgid": orgId } : {}),
},
body: body === undefined ? undefined : JSON.stringify(body),
signal: AbortSignal.timeout(15_000),
});
const text = await res.text();
const data = text ? JSON.parse(text) : {};
if (!res.ok) {
const err = new Error(`Zitadel ${method} ${apiPath} → ${res.status}: ${data?.message ?? text}`);
err.status = res.status;
err.zitadelCode = data?.code;
throw err;
}
return data;
}
export const isAlreadyExists = (err) => err?.status === 409 || err?.zitadelCode === 6 || /already exists/i.test(err?.message ?? "");
export async function ensureZitadelProject(pat, name, opts = {}) {
const search = await api(pat, "POST", "/management/v1/projects/_search", { queries: [{ nameQuery: { name, method: "TEXT_QUERY_METHOD_EQUALS" } }] }, opts.orgId);
const existing = search?.result?.[0]?.id;
if (existing) {
if (opts.logExisting)
logInfo(`Zitadel project '${name}' already exists`);
return existing;
}
const created = await api(pat, "POST", "/management/v1/projects", { name, projectRoleAssertion: true }, opts.orgId);
logSuccess(`Created Zitadel project '${name}'${opts.describe ? ` (${opts.describe})` : ""}`);
return created.id;
}
export async function searchZitadelApp(pat, projectId, name, orgId) {
const search = await api(pat, "POST", `/management/v1/projects/${projectId}/apps/_search`, { queries: [{ nameQuery: { name, method: "TEXT_QUERY_METHOD_EQUALS" } }] }, orgId);
return search?.result?.[0];
}
const ensureCliProject = (pat) => ensureZitadelProject(pat, CLI_PROJECT_NAME, {
describe: "platform client feature",
logExisting: true,
});
async function ensureCliApp(pat, projectId) {
const existing = (await searchZitadelApp(pat, projectId, CLI_APP_NAME))?.oidcConfig?.clientId;
if (existing) {
logInfo(`Zitadel app '${CLI_APP_NAME}' already exists`);
return existing;
}
const created = await api(pat, "POST", `/management/v1/projects/${projectId}/apps/oidc`, buildCliAppPayload());
logSuccess(`Created Zitadel application '${CLI_APP_NAME}' (PKCE + device code)`);
return created.clientId;
}
async function ensureTestUsers(pat) {
for (const user of TEST_USERS) {
try {
await api(pat, "POST", "/management/v1/users/human/_import", {
userName: user.email,
profile: {
firstName: user.firstName,
lastName: user.lastName,
displayName: `${user.firstName} ${user.lastName}`,
},
email: { email: user.email, isEmailVerified: true },
password: user.password,
passwordChangeRequired: false,
});
logSuccess(`Created test user ${user.email}`);
}
catch (err) {
if (isAlreadyExists(err)) {
logInfo(`Test user ${user.email} already exists`);
}
else {
throw err;
}
}
}
}
export function hubAuthPath() {
return path.join(meshCacheDir("mesh-local"), "hub-auth.json");
}
export function readHubAuth() {
try {
const parsed = JSON.parse(fs.readFileSync(hubAuthPath(), "utf-8"));
if (parsed?.clientId && parsed?.clientSecret && parsed?.cookieSecret)
return parsed;
return null;
}
catch {
return null;
}
}
const ensureHubProject = (pat) => ensureZitadelProject(pat, HUB_PROJECT_NAME, { describe: "Hub platform app" });
function hubRoleDisplayName(roleKey) {
const tenant = hubRoleKeyTenant(roleKey);
return tenant === null ? roleKey : `${restrictedRoleBase(roleKey) ?? roleKey} (${tenant})`;
}
async function ensureHubRoles(pat, projectId) {
for (const role of HUB_ROLES) {
try {
await api(pat, "POST", `/management/v1/projects/${projectId}/roles`, {
roleKey: role,
displayName: hubRoleDisplayName(role),
});
logSuccess(`Created Hub role '${role}'`);
}
catch (err) {
if (!isAlreadyExists(err))
throw err;
}
}
}
export async function reconcileHubRedirectUris(pat, projectId, app) {
const cfg = app?.oidcConfig ?? {};
const redirectUris = [
...new Set([...(cfg.redirectUris ?? []), hubRedirectUri(), HUB_DEFAULT_REDIRECT_URI]),
];
const postLogoutRedirectUris = [
...new Set([
...(cfg.postLogoutRedirectUris ?? []),
`http://localhost:${hubPort()}`,
`http://localhost:${hubPort()}/`,
`http://localhost:${DEFAULT_HUB_PORT}`,
`http://localhost:${DEFAULT_HUB_PORT}/`,
]),
];
if (redirectUris.length === (cfg.redirectUris ?? []).length &&
postLogoutRedirectUris.length === (cfg.postLogoutRedirectUris ?? []).length) {
return;
}
await api(pat, "PUT", `/management/v1/projects/${projectId}/apps/${app.id}/oidc_config`, {
redirectUris,
postLogoutRedirectUris,
responseTypes: cfg.responseTypes ?? ["OIDC_RESPONSE_TYPE_CODE"],
grantTypes: cfg.grantTypes ?? [
"OIDC_GRANT_TYPE_AUTHORIZATION_CODE",
"OIDC_GRANT_TYPE_REFRESH_TOKEN",
],
appType: "OIDC_APP_TYPE_WEB",
authMethodType: "OIDC_AUTH_METHOD_TYPE_BASIC",
accessTokenType: cfg.accessTokenType ?? "OIDC_TOKEN_TYPE_JWT",
accessTokenRoleAssertion: cfg.accessTokenRoleAssertion ?? true,
idTokenRoleAssertion: cfg.idTokenRoleAssertion ?? true,
idTokenUserinfoAssertion: cfg.idTokenUserinfoAssertion ?? true,
devMode: cfg.devMode ?? true,
});
logSuccess(`Registered Hub redirect URI http://localhost:${hubPort()}/oauth2/callback (MESH_HUB_PORT)`);
}
async function ensureHubApp(pat, projectId) {
const existing = await searchZitadelApp(pat, projectId, HUB_APP_NAME);
if (existing) {
await reconcileHubRedirectUris(pat, projectId, existing);
const clientId = existing?.oidcConfig?.clientId;
const persisted = readHubAuth();
if (persisted && persisted.clientId === clientId)
return { clientId };
const regenerated = await api(pat, "POST", `/management/v1/projects/${projectId}/apps/${existing.id}/oidc_config/_generate_client_secret`, {});
logSuccess(`Regenerated Hub UI client secret (local copy was missing)`);
return { clientId, clientSecret: regenerated.clientSecret };
}
const created = await api(pat, "POST", `/management/v1/projects/${projectId}/apps/oidc`, {
name: HUB_APP_NAME,
redirectUris: [...new Set([hubRedirectUri(), HUB_DEFAULT_REDIRECT_URI])],
postLogoutRedirectUris: [
...new Set([
`http://localhost:${hubPort()}`,
`http://localhost:${hubPort()}/`,
`http://localhost:${DEFAULT_HUB_PORT}`,
`http://localhost:${DEFAULT_HUB_PORT}/`,
]),
],
responseTypes: ["OIDC_RESPONSE_TYPE_CODE"],
grantTypes: ["OIDC_GRANT_TYPE_AUTHORIZATION_CODE", "OIDC_GRANT_TYPE_REFRESH_TOKEN"],
appType: "OIDC_APP_TYPE_WEB",
authMethodType: "OIDC_AUTH_METHOD_TYPE_BASIC",
accessTokenType: "OIDC_TOKEN_TYPE_JWT",
accessTokenRoleAssertion: true,
idTokenRoleAssertion: true,
idTokenUserinfoAssertion: true,
devMode: true,
});
logSuccess(`Created Zitadel application '${HUB_APP_NAME}' (Hub UI web client, code flow)`);
return { clientId: created.clientId, clientSecret: created.clientSecret };
}
async function ensureHubGrants(pat, projectId) {
const grants = [
{ email: "admin@local.mesh", roles: ["ops"] },
{ email: "dev@local.mesh", roles: ["ops"] },
{ email: "ops@local.mesh", roles: ["developer"] },
];
for (const grant of grants) {
const search = await api(pat, "POST", "/management/v1/users/_search", {
queries: [{ userNameQuery: { userName: grant.email, method: "TEXT_QUERY_METHOD_EQUALS" } }],
});
const userId = search?.result?.[0]?.id;
if (!userId)
continue;
try {
await api(pat, "POST", `/management/v1/users/${userId}/grants`, {
projectId,
roleKeys: grant.roles,
});
logSuccess(`Granted Hub role(s) ${grant.roles.join(", ")} → ${grant.email}`);
}
catch (err) {
if (!isAlreadyExists(err))
throw err;
}
}
}
export async function seedHubAuth(pat) {
const projectId = await ensureHubProject(pat);
await ensureHubRoles(pat, projectId);
const app = await ensureHubApp(pat, projectId);
await ensureHubGrants(pat, projectId);
const persisted = readHubAuth();
const persistedCookie = persisted?.cookieSecret && [16, 24, 32].includes(persisted.cookieSecret.length)
? persisted.cookieSecret
: undefined;
const config = {
clientId: app.clientId,
projectId,
clientSecret: app.clientSecret ?? persisted?.clientSecret ?? "",
cookieSecret: persistedCookie ?? (await import("crypto")).randomBytes(16).toString("hex"),
};
fs.writeFileSync(hubAuthPath(), JSON.stringify(config, null, 2), { mode: 0o600 });
return config;
}
async function publishHubAuthzPointer(projectId, orgId, awsConfig) {
const { SSMClient, GetParameterCommand, PutParameterCommand } = await import("@aws-sdk/client-ssm");
const ssm = new SSMClient(awsConfig);
const name = `/mesh-platform/${LOCAL_TENANT}/${LOCAL_ENV}/apps/hub/stacks/local/authz`;
let current = {};
try {
const existing = await ssm.send(new GetParameterCommand({ Name: name }));
current = JSON.parse(existing.Parameter?.Value ?? "{}");
}
catch {
}
const value = {
spicedb: { instanceRefs: [] },
...current,
zitadel: { projectId, orgId, issuer: ZITADEL_ISSUER },
};
await ssm.send(new PutParameterCommand({
Name: name,
Type: "String",
Overwrite: true,
Value: JSON.stringify(value),
Description: "Hub authz pointer (local analog of the platform Pulumi program)",
}));
logSuccess(`Hub authz pointer published → project ${projectId} in org ${orgId}`);
}
async function writeRegistryParams(cliClientId, awsConfig) {
const { SSMClient, PutParameterCommand } = await import("@aws-sdk/client-ssm");
const ssm = new SSMClient(awsConfig);
await ssm.send(new PutParameterCommand({
Name: ZITADEL_SSM_PARAM,
Type: "String",
Overwrite: true,
Value: JSON.stringify({ endpoint: ZITADEL_ISSUER, cliClientId }),
Description: "Zitadel platform export (seeded by mesh start)",
}));
for (const user of TEST_USERS) {
await ssm.send(new PutParameterCommand({
Name: `${TEST_USERS_SSM_PREFIX}/${user.name}`,
Type: "String",
Overwrite: true,
Value: JSON.stringify({ email: user.email, password: user.password, roles: [] }),
Description: `Local test user '${user.name}' (dev-grade, seeded by mesh start)`,
}));
}
}
export const OPS_HUB_SECRET_ID = "mesh/local/dev/zitadel/ops-hub";
async function ensureOpsHubAdmin(pat, awsConfig) {
const { SecretsManagerClient, GetSecretValueCommand, CreateSecretCommand, PutSecretValueCommand, } = await import("@aws-sdk/client-secrets-manager");
const sm = new SecretsManagerClient(awsConfig);
const existing = await sm
.send(new GetSecretValueCommand({ SecretId: OPS_HUB_SECRET_ID }))
.catch(() => null);
if (existing?.SecretString) {
logSuccess("Hub admin key already provisioned (Zitadel writes enabled)");
return;
}
const userName = "hub-opshub";
const found = await api(pat, "POST", "/management/v1/users/_search", {
queries: [{ userNameQuery: { userName, method: "TEXT_QUERY_METHOD_EQUALS" } }],
});
let userId = found?.result?.[0]?.id;
if (!userId) {
const created = await api(pat, "POST", "/management/v1/users/machine", {
userName,
name: "Hub Ops Hub admin",
description: "Local Hub Zitadel admin plane (seeded by mesh start)",
});
userId = created?.userId;
}
if (!userId)
throw new MeshCliError("could not create the Hub's Zitadel admin user");
await api(pat, "POST", "/admin/v1/members", { userId, roles: ["IAM_OWNER"] }).catch(() => undefined);
const key = await api(pat, "POST", `/management/v1/users/${userId}/keys`, {
type: "KEY_TYPE_JSON",
});
if (!key?.keyDetails)
throw new MeshCliError("Zitadel did not return a machine key for the Hub admin user");
const raw = JSON.parse(Buffer.from(key.keyDetails, "base64").toString("utf8"));
const secretString = JSON.stringify({ keyId: raw.keyId, key: raw.key, userId: raw.userId });
await sm
.send(new CreateSecretCommand({ Name: OPS_HUB_SECRET_ID, SecretString: secretString }))
.catch(async () => {
await sm.send(new PutSecretValueCommand({ SecretId: OPS_HUB_SECRET_ID, SecretString: secretString }));
});
logSuccess(`Hub admin key provisioned → ${OPS_HUB_SECRET_ID} (Zitadel writes enabled)`);
}
const LOCAL_SMTP = {
host: "mailpit:1025",
from: "no-reply@local.mesh",
fromName: "Mesh (local)",
};
async function ensureLocalSmtp(pat) {
try {
const existing = await api(pat, "POST", "/admin/v1/smtp/_search", {}).catch(() => null);
const configs = existing?.result ?? [];
const match = configs.find((c) => c.host === LOCAL_SMTP.host);
if (!match) {
const created = await api(pat, "POST", "/admin/v1/smtp", {
senderAddress: LOCAL_SMTP.from,
senderName: LOCAL_SMTP.fromName,
tls: false,
host: LOCAL_SMTP.host,
user: "",
password: "",
});
if (created?.id)
await api(pat, "POST", `/admin/v1/smtp/${created.id}/_activate`, {});
logSuccess(`Local mailbox wired to Zitadel → ${LOCAL_SMTP.host} (view at http://localhost:8025)`);
return;
}
if (match.state !== "SMTP_CONFIG_ACTIVE" && match.id) {
await api(pat, "POST", `/admin/v1/smtp/${match.id}/_activate`, {});
}
}
catch (err) {
logWarn(`Could not wire the local mailbox to Zitadel (${err.message}). ` +
"Activation and password-reset mail will not be delivered locally.");
}
}
export async function seedZitadel(awsConfig) {
const pat = readSeederPat();
const org = await api(pat, "POST", "/admin/v1/orgs/_search", {
queries: [{ nameQuery: { name: PLATFORM_ORG, method: "TEXT_QUERY_METHOD_EQUALS" } }],
});
if (!org?.result?.[0]) {
throw new MeshCliError(`Platform org '${PLATFORM_ORG}' not found — the zitadel volume predates the org naming.`, { remediation: { command: "mesh stop --destroy && mesh start" } });
}
logSuccess(`Platform org '${PLATFORM_ORG}' ready (platform tenant root — holds platform-service auth config)`);
try {
await api(pat, "PUT", "/v2/features/instance", { loginV2: { required: false } });
}
catch {
}
await ensureLocalSmtp(pat);
const projectId = await ensureCliProject(pat);
const cliClientId = await ensureCliApp(pat, projectId);
await ensureTestUsers(pat);
const hubAuth = await seedHubAuth(pat);
await publishHubAuthzPointer(hubAuth.projectId, org.result[0].id, awsConfig);
await writeRegistryParams(cliClientId, awsConfig);
await ensureOpsHubAdmin(pat, awsConfig);
writeContextConfig(LOGIN_CONTEXT, { issuer: ZITADEL_ISSUER, clientId: cliClientId });
logSuccess(`Login context '${LOGIN_CONTEXT}' configured → try: mesh login ${LOGIN_CONTEXT}`);
return { projectId, cliClientId, hubAuth };
}