@mesh-tech/mesh-cli 0.25.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +74 -246
  2. package/dist/bin/mesh.js +2179 -1328
  3. package/dist/bin/mesh.js.map +3 -3
  4. package/dist/build-info.json +2 -2
  5. package/dist/src/commands/db/axes.d.ts +19 -0
  6. package/dist/src/commands/db/axes.js +35 -0
  7. package/dist/src/commands/db/connect.d.ts +2 -4
  8. package/dist/src/commands/db/connect.js +4 -19
  9. package/dist/src/commands/db/credentials.d.ts +2 -5
  10. package/dist/src/commands/db/credentials.js +7 -23
  11. package/dist/src/commands/db/env.d.ts +2 -5
  12. package/dist/src/commands/db/env.js +7 -23
  13. package/dist/src/commands/db/exec.js +7 -24
  14. package/dist/src/commands/db/psql.d.ts +2 -5
  15. package/dist/src/commands/db/psql.js +8 -23
  16. package/dist/src/commands/init/platform-setup.d.ts +68 -0
  17. package/dist/src/commands/init/platform-setup.js +203 -0
  18. package/dist/src/commands/init/wizard.d.ts +20 -0
  19. package/dist/src/commands/init/wizard.js +89 -7
  20. package/dist/src/commands/init.js +42 -36
  21. package/dist/src/commands/local/index.d.ts +5 -3
  22. package/dist/src/commands/local/index.js +61 -23
  23. package/dist/src/commands/local/mock-idps.d.ts +48 -0
  24. package/dist/src/commands/local/mock-idps.js +119 -0
  25. package/dist/src/commands/local/seed-zitadel.d.ts +6 -0
  26. package/dist/src/commands/local/seed-zitadel.js +76 -0
  27. package/dist/src/commands/local/stack.d.ts +11 -3
  28. package/dist/src/commands/local/stack.js +29 -15
  29. package/dist/src/commands/login.d.ts +4 -0
  30. package/dist/src/commands/login.js +16 -2
  31. package/dist/src/commands/registry.d.ts +2 -2
  32. package/dist/src/commands/registry.js +11 -6
  33. package/dist/src/commands/secrets/context.d.ts +5 -0
  34. package/dist/src/commands/secrets/context.js +5 -0
  35. package/dist/src/commands/secrets/index.js +3 -0
  36. package/dist/src/commands/secrets/migrate-config.d.ts +1 -0
  37. package/dist/src/commands/secrets/migrate-config.js +2 -3
  38. package/dist/src/commands/secrets/reindex.d.ts +1 -0
  39. package/dist/src/commands/secrets/reindex.js +2 -2
  40. package/dist/src/commands/secrets/set.d.ts +1 -0
  41. package/dist/src/commands/secrets/set.js +2 -2
  42. package/dist/src/commands/temporal.js +35 -8
  43. package/dist/src/commands/temporal.js.map +2 -2
  44. package/dist/src/commands/tunnel/index.d.ts +8 -0
  45. package/dist/src/commands/tunnel/index.js +3 -3
  46. package/dist/src/index.js +324 -62
  47. package/dist/src/index.js.map +3 -3
  48. package/dist/src/utils/context.d.ts +6 -1
  49. package/dist/src/utils/context.js +302 -39
  50. package/dist/src/utils/registry-identity.d.ts +5 -0
  51. package/dist/src/utils/registry-identity.js +16 -0
  52. package/fragments/agents/AGENTS.md.hbs +10 -2
  53. package/fragments/base/index.ts.hbs +15 -5
  54. package/package.json +2 -2
  55. package/skills/core/SKILL.md +74 -7
  56. package/stack/docker-compose.idps.yml +104 -0
  57. package/stack/idps/dex.yaml +53 -0
  58. package/templates/api-auth/index.ts.hbs +5 -4
  59. package/templates/api-role-gating/index.ts.hbs +5 -4
  60. package/templates/external-service/index.ts.hbs +5 -4
  61. package/templates/platform-repo/README.md.hbs +17 -3
  62. package/templates/platform-repo/core/Pulumi.{{tenant}}-{{env}}.yaml.hbs +7 -0
  63. package/templates/workflow/index.ts.hbs +5 -4
@@ -0,0 +1,119 @@
1
+ import { execFileSync } from "child_process";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import { meshCacheDir } from "../../utils/cache-home.js";
5
+ import { MeshCliError } from "../../utils/errors.js";
6
+ export const MOCK_SAML_PORT = 4569;
7
+ export const MOCK_OIDC_PORT = 5556;
8
+ const MOCK_OIDC_SERVICE = "mock-oidc";
9
+ export const idpEndpoints = () => [
10
+ {
11
+ service: "mock-saml",
12
+ label: "Mock SAML IdP",
13
+ url: `http://localhost:${MOCK_SAML_PORT}`,
14
+ probe: { kind: "http", url: mockSamlMetadataUrl() },
15
+ hint: "sign in as any email you type; federation metadata at /api/saml/metadata",
16
+ },
17
+ {
18
+ service: MOCK_OIDC_SERVICE,
19
+ label: "Mock OIDC IdP",
20
+ url: `http://localhost:${MOCK_OIDC_PORT}/dex`,
21
+ probe: {
22
+ kind: "http",
23
+ url: `http://localhost:${MOCK_OIDC_PORT}/dex/.well-known/openid-configuration`,
24
+ },
25
+ hint: "dev@local.mesh or ops@local.mesh / password",
26
+ },
27
+ ];
28
+ export const MOCK_SAML_IDP_NAME = "Mock SAML (local)";
29
+ export const MOCK_OIDC_IDP_NAME = "Mock OIDC (local)";
30
+ const MOCK_OIDC_CLIENT_ID = "mesh-local";
31
+ const MOCK_OIDC_CLIENT_SECRET = "mesh-local-secret";
32
+ export const mockSamlMetadataUrl = () => `http://localhost:${MOCK_SAML_PORT}/api/saml/metadata`;
33
+ export const mockOidcEndpoints = () => ({
34
+ authorization: `http://localhost:${MOCK_OIDC_PORT}/dex/auth`,
35
+ token: `http://${MOCK_OIDC_SERVICE}:${MOCK_OIDC_PORT}/dex/token`,
36
+ user: `http://${MOCK_OIDC_SERVICE}:${MOCK_OIDC_PORT}/dex/userinfo`,
37
+ });
38
+ const keyDir = () => path.join(meshCacheDir("mesh-local"), "mock-idp");
39
+ export function ensureMockSamlSigningKey() {
40
+ const dir = keyDir();
41
+ const certPath = path.join(dir, "cert.pem");
42
+ const keyPath = path.join(dir, "key.pem");
43
+ if (!fs.existsSync(certPath) || !fs.existsSync(keyPath)) {
44
+ fs.mkdirSync(dir, { recursive: true });
45
+ try {
46
+ execFileSync("openssl", [
47
+ "req",
48
+ "-x509",
49
+ "-newkey",
50
+ "rsa:2048",
51
+ "-keyout",
52
+ keyPath,
53
+ "-out",
54
+ certPath,
55
+ "-days",
56
+ "3650",
57
+ "-nodes",
58
+ "-subj",
59
+ "/CN=mesh-local-mock-saml",
60
+ ], { stdio: "ignore" });
61
+ }
62
+ catch (cause) {
63
+ throw new MeshCliError("Could not generate the mock SAML signing key.", {
64
+ remediation: {
65
+ command: "openssl version # openssl must be on PATH for --with-idps",
66
+ },
67
+ cause,
68
+ });
69
+ }
70
+ }
71
+ return {
72
+ publicKeyB64: fs.readFileSync(certPath).toString("base64"),
73
+ privateKeyB64: fs.readFileSync(keyPath).toString("base64"),
74
+ };
75
+ }
76
+ export const mockIdpComposeEnv = () => {
77
+ const { publicKeyB64, privateKeyB64 } = ensureMockSamlSigningKey();
78
+ return {
79
+ MESH_MOCK_SAML_PUBLIC_KEY: publicKeyB64,
80
+ MESH_MOCK_SAML_PRIVATE_KEY: privateKeyB64,
81
+ };
82
+ };
83
+ export const buildMockSamlIdpBody = (metadataXml) => ({
84
+ name: MOCK_SAML_IDP_NAME,
85
+ metadataXml: Buffer.from(metadataXml, "utf-8").toString("base64"),
86
+ binding: "SAML_BINDING_POST",
87
+ withSignedRequest: true,
88
+ providerOptions: {
89
+ isLinkingAllowed: true,
90
+ isCreationAllowed: true,
91
+ isAutoCreation: true,
92
+ isAutoUpdate: true,
93
+ autoLinking: "AUTO_LINKING_OPTION_EMAIL",
94
+ },
95
+ });
96
+ export const buildMockOauthIdpBody = () => {
97
+ const endpoints = mockOidcEndpoints();
98
+ return {
99
+ name: MOCK_OIDC_IDP_NAME,
100
+ clientId: MOCK_OIDC_CLIENT_ID,
101
+ clientSecret: MOCK_OIDC_CLIENT_SECRET,
102
+ authorizationEndpoint: endpoints.authorization,
103
+ tokenEndpoint: endpoints.token,
104
+ userEndpoint: endpoints.user,
105
+ scopes: ["openid", "profile", "email"],
106
+ idAttribute: "sub",
107
+ providerOptions: {
108
+ isLinkingAllowed: true,
109
+ isCreationAllowed: true,
110
+ isAutoCreation: true,
111
+ isAutoUpdate: true,
112
+ autoLinking: "AUTO_LINKING_OPTION_EMAIL",
113
+ },
114
+ };
115
+ };
116
+ export const missingLoginPolicyIdps = (attached, wanted) => {
117
+ const have = new Set(attached);
118
+ return [...new Set(wanted)].filter((id) => !have.has(id));
119
+ };
@@ -73,4 +73,10 @@ export interface ZitadelSeedResult {
73
73
  hubAuth: HubAuthConfig;
74
74
  }
75
75
  export declare function seedZitadel(awsConfig: object): Promise<ZitadelSeedResult>;
76
+ export declare const IDP_SEARCH_PATH = "/admin/v1/idps/templates/_search";
77
+ export declare const pickIdpIdByName: (searchResult: any, name: string) => string | undefined;
78
+ export declare function seedMockIdps(): Promise<{
79
+ samlIdpId: string;
80
+ oidcIdpId: string;
81
+ }>;
76
82
  export {};
@@ -8,6 +8,7 @@ import { writeContextConfig } from "../login.js";
8
8
  import { compose, hubPort, DEFAULT_HUB_PORT } from "./stack.js";
9
9
  import { LOCAL_TENANT, LOCAL_ENV } from "./seed.js";
10
10
  import { meshCacheDir } from "../../utils/cache-home.js";
11
+ import { buildMockOauthIdpBody, buildMockSamlIdpBody, missingLoginPolicyIdps, MOCK_OIDC_IDP_NAME, MOCK_SAML_IDP_NAME, mockSamlMetadataUrl, } from "./mock-idps.js";
11
12
  export const ZITADEL_ISSUER = "http://localhost:8080";
12
13
  export const LOGIN_CONTEXT = "local";
13
14
  export const PLATFORM_ORG = "mesh";
@@ -441,3 +442,78 @@ export async function seedZitadel(awsConfig) {
441
442
  logSuccess(`Login context '${LOGIN_CONTEXT}' configured → try: mesh login ${LOGIN_CONTEXT}`);
442
443
  return { projectId, cliClientId, hubAuth };
443
444
  }
445
+ async function fetchMockSamlMetadata() {
446
+ const url = mockSamlMetadataUrl();
447
+ let lastError = "no attempt made";
448
+ for (let attempt = 0; attempt < 30; attempt++) {
449
+ try {
450
+ const res = await fetch(url, { signal: AbortSignal.timeout(5_000) });
451
+ const body = await res.text();
452
+ if (res.ok && body.includes("EntityDescriptor")) {
453
+ if (/<ds:X509Certificate\s*\/>|<ds:X509Certificate>\s*<\/ds:X509Certificate>/.test(body)) {
454
+ throw new MeshCliError("The mock SAML IdP served metadata with an empty signing certificate.", {
455
+ remediation: {
456
+ command: "rm -rf ~/.cache/mesh/mesh-local/mock-idp && mesh stop && mesh start --with-idps",
457
+ },
458
+ });
459
+ }
460
+ return body;
461
+ }
462
+ lastError = `HTTP ${res.status}`;
463
+ }
464
+ catch (cause) {
465
+ if (cause instanceof MeshCliError)
466
+ throw cause;
467
+ lastError = String(cause);
468
+ }
469
+ await new Promise((resolve) => setTimeout(resolve, 2_000));
470
+ }
471
+ throw new MeshCliError(`Could not read mock SAML metadata from ${url} (${lastError}).`, {
472
+ remediation: { command: "mesh status # is mock-saml running?" },
473
+ });
474
+ }
475
+ async function attachedLoginPolicyIdps(pat) {
476
+ const res = await api(pat, "POST", "/admin/v1/policies/login/idps/_search", {});
477
+ return (res?.result ?? []).map((idp) => String(idp.idpId ?? idp.id)).filter(Boolean);
478
+ }
479
+ export const IDP_SEARCH_PATH = "/admin/v1/idps/templates/_search";
480
+ export const pickIdpIdByName = (searchResult, name) => {
481
+ const hit = (searchResult?.result ?? []).find((idp) => idp?.name === name);
482
+ return hit?.id ? String(hit.id) : undefined;
483
+ };
484
+ async function findIdpByName(pat, name) {
485
+ return pickIdpIdByName(await api(pat, "POST", IDP_SEARCH_PATH, {}), name);
486
+ }
487
+ export async function seedMockIdps() {
488
+ const pat = readSeederPat();
489
+ const metadataXml = await fetchMockSamlMetadata();
490
+ let samlIdpId = await findIdpByName(pat, MOCK_SAML_IDP_NAME);
491
+ if (!samlIdpId) {
492
+ const created = await api(pat, "POST", "/admin/v1/idps/saml", buildMockSamlIdpBody(metadataXml));
493
+ samlIdpId = String(created.id);
494
+ }
495
+ logSuccess(`Mock SAML IdP ready (${MOCK_SAML_IDP_NAME}, id ${samlIdpId})`);
496
+ let oidcIdpId = await findIdpByName(pat, MOCK_OIDC_IDP_NAME);
497
+ if (!oidcIdpId) {
498
+ const created = await api(pat, "POST", "/admin/v1/idps/oauth", buildMockOauthIdpBody());
499
+ oidcIdpId = String(created.id);
500
+ }
501
+ logSuccess(`Mock OIDC IdP ready (${MOCK_OIDC_IDP_NAME}, id ${oidcIdpId})`);
502
+ const policy = await api(pat, "GET", "/admin/v1/policies/login");
503
+ if (policy?.policy?.allowExternalIdp !== true) {
504
+ await api(pat, "PUT", "/admin/v1/policies/login", {
505
+ ...policy.policy,
506
+ allowExternalIdp: true,
507
+ });
508
+ logInfo("Instance login policy: allowExternalIdp turned on (federation buttons were hidden)");
509
+ }
510
+ const attached = await attachedLoginPolicyIdps(pat);
511
+ const toAttach = missingLoginPolicyIdps(attached, [samlIdpId, oidcIdpId]);
512
+ for (const idpId of toAttach) {
513
+ await api(pat, "POST", "/admin/v1/policies/login/idps", { idpId, ownerType: "IDP_OWNER_TYPE_SYSTEM" });
514
+ }
515
+ const finalIdps = await attachedLoginPolicyIdps(pat);
516
+ logSuccess(`Login policy carries ${finalIdps.length} federation(s) — both mocks attached, ` +
517
+ "which is the state a login-policy change has to preserve");
518
+ return { samlIdpId, oidcIdpId };
519
+ }
@@ -29,20 +29,25 @@ export declare function hubPort(): string;
29
29
  export declare function hubEndpoints(): StackEndpoint[];
30
30
  export declare function findPackageRoot(startDir?: string): string;
31
31
  export declare function stackDir(): string;
32
+ export declare const BASE_COMPOSE_FILE = "docker-compose.yml";
32
33
  export declare const HUB_OVERLAY_FILE = "docker-compose.hub.yml";
34
+ export declare const IDPS_OVERLAY_FILE = "docker-compose.idps.yml";
33
35
  export declare function parseComposeServiceNames(yamlText: string): string[];
34
36
  export declare function hubOverlayServices(): ReadonlySet<string>;
37
+ export declare function idpsOverlayServices(): ReadonlySet<string>;
35
38
  export declare function stackOwnedElsewhere(): string | undefined;
36
39
  export declare function ensureDockerAvailable(): void;
37
40
  export declare function compose(args: string[], opts?: {
38
41
  inherit?: boolean;
39
42
  hub?: boolean;
43
+ idps?: boolean;
40
44
  env?: Record<string, string>;
41
45
  }): string;
42
46
  export declare function summarizeComposeFailure(output: string): string;
43
47
  export declare function composeLogPath(op: string): string;
44
48
  export declare function composeStreamed(args: string[], opts?: {
45
49
  hub?: boolean;
50
+ idps?: boolean;
46
51
  env?: Record<string, string>;
47
52
  }): Promise<void>;
48
53
  export interface ServiceStatus {
@@ -53,10 +58,13 @@ export interface ServiceStatus {
53
58
  export declare function parseComposePs(output: string): ServiceStatus[];
54
59
  export declare function stackServices(): ServiceStatus[];
55
60
  export declare const ONE_SHOT_SERVICES: Set<string>;
61
+ export interface OverlayState {
62
+ services: ReadonlySet<string>;
63
+ started: boolean;
64
+ }
56
65
  export declare function crashedServices(services: ServiceStatus[], opts: {
57
- overlayServices: ReadonlySet<string>;
58
- overlayStarted: boolean;
66
+ overlays: readonly OverlayState[];
59
67
  }): ServiceStatus[];
60
- export declare function hubOverlayRunning(services: ServiceStatus[], overlayServices: ReadonlySet<string>): boolean;
68
+ export declare function overlayRunning(services: ServiceStatus[], overlayServices: ReadonlySet<string>): boolean;
61
69
  export declare function hubApiRunning(services: ServiceStatus[]): boolean;
62
70
  export declare function probeEndpoint(endpoint: StackEndpoint): Promise<boolean>;
@@ -189,23 +189,34 @@ export function stackDir() {
189
189
  }
190
190
  return dir;
191
191
  }
192
+ export const BASE_COMPOSE_FILE = "docker-compose.yml";
192
193
  export const HUB_OVERLAY_FILE = "docker-compose.hub.yml";
194
+ export const IDPS_OVERLAY_FILE = "docker-compose.idps.yml";
193
195
  export function parseComposeServiceNames(yamlText) {
194
196
  const doc = parseYaml(yamlText);
195
197
  return Object.keys(doc?.services ?? {});
196
198
  }
197
- let hubOverlayServicesCache;
198
- export function hubOverlayServices() {
199
- if (!hubOverlayServicesCache) {
200
- const file = path.join(stackDir(), HUB_OVERLAY_FILE);
201
- if (!fs.existsSync(file)) {
202
- throw new MeshCliError(`Local stack assets are incomplete: ${file} is missing.`, {
203
- remediation: { command: "npm install -g @mesh-tech/mesh-cli" },
204
- });
205
- }
206
- hubOverlayServicesCache = new Set(parseComposeServiceNames(fs.readFileSync(file, "utf-8")));
199
+ const overlayServicesCache = new Map();
200
+ function overlayServicesOf(overlayFile) {
201
+ const cached = overlayServicesCache.get(overlayFile);
202
+ if (cached)
203
+ return cached;
204
+ const file = path.join(stackDir(), overlayFile);
205
+ if (!fs.existsSync(file)) {
206
+ throw new MeshCliError(`Local stack assets are incomplete: ${file} is missing.`, {
207
+ remediation: { command: "npm install -g @mesh-tech/mesh-cli" },
208
+ });
207
209
  }
208
- return hubOverlayServicesCache;
210
+ const base = new Set(parseComposeServiceNames(fs.readFileSync(path.join(stackDir(), BASE_COMPOSE_FILE), "utf-8")));
211
+ const names = new Set(parseComposeServiceNames(fs.readFileSync(file, "utf-8")).filter((n) => !base.has(n)));
212
+ overlayServicesCache.set(overlayFile, names);
213
+ return names;
214
+ }
215
+ export function hubOverlayServices() {
216
+ return overlayServicesOf(HUB_OVERLAY_FILE);
217
+ }
218
+ export function idpsOverlayServices() {
219
+ return overlayServicesOf(IDPS_OVERLAY_FILE);
209
220
  }
210
221
  export function stackOwnedElsewhere() {
211
222
  try {
@@ -244,6 +255,8 @@ export function compose(args, opts = {}) {
244
255
  const files = ["-f", path.join(dir, "docker-compose.yml")];
245
256
  if (opts.hub)
246
257
  files.push("-f", path.join(dir, "docker-compose.hub.yml"));
258
+ if (opts.idps)
259
+ files.push("-f", path.join(dir, IDPS_OVERLAY_FILE));
247
260
  const fullArgs = ["compose", "-p", COMPOSE_PROJECT, ...files, ...args];
248
261
  return execFileSync("docker", fullArgs, {
249
262
  cwd: dir,
@@ -276,6 +289,8 @@ export async function composeStreamed(args, opts = {}) {
276
289
  const files = ["-f", path.join(dir, "docker-compose.yml")];
277
290
  if (opts.hub)
278
291
  files.push("-f", path.join(dir, "docker-compose.hub.yml"));
292
+ if (opts.idps)
293
+ files.push("-f", path.join(dir, IDPS_OVERLAY_FILE));
279
294
  const fullArgs = ["compose", "-p", COMPOSE_PROJECT, ...files, ...args];
280
295
  const env = {
281
296
  ...process.env,
@@ -362,11 +377,10 @@ export function stackServices() {
362
377
  }
363
378
  export const ONE_SHOT_SERVICES = new Set(["spicedb-migrate", "zitadel-machinekey-init"]);
364
379
  export function crashedServices(services, opts) {
365
- return services.filter((s) => s.state === "exited" &&
366
- !ONE_SHOT_SERVICES.has(s.name) &&
367
- (opts.overlayStarted || !opts.overlayServices.has(s.name)));
380
+ const dormant = new Set(opts.overlays.filter((o) => !o.started).flatMap((o) => [...o.services]));
381
+ return services.filter((s) => s.state === "exited" && !ONE_SHOT_SERVICES.has(s.name) && !dormant.has(s.name));
368
382
  }
369
- export function hubOverlayRunning(services, overlayServices) {
383
+ export function overlayRunning(services, overlayServices) {
370
384
  return services.some((s) => overlayServices.has(s.name) && s.state === "running");
371
385
  }
372
386
  export function hubApiRunning(services) {
@@ -50,6 +50,10 @@ export declare function registerLoginCommand(program: Command): void;
50
50
  export declare const RESERVED_REGISTRY_CONTEXT = "registry";
51
51
  export declare const NO_REGISTRY_DEPRECATED = "--no-registry is deprecated and ignored: mesh login no longer touches the registry.";
52
52
  export declare const REGISTRY_SEPARATE_HINT = "Package registry access is separate from platform sign-in \u2014 run: mesh registry login";
53
+ export declare function shorthandContext(context: string): {
54
+ tenant: string;
55
+ env: string;
56
+ } | null;
53
57
  export declare function renderNoConfigHelp(context: string): string;
54
58
  export declare function tokenStillValid(expiresAt: string, marginMs?: number, now?: number): boolean;
55
59
  declare function getValidToken(context: string, opts?: {
@@ -622,7 +622,7 @@ async function showStatus(context, config) {
622
622
  export function registerLoginCommand(program) {
623
623
  program
624
624
  .command("login")
625
- .description("Authenticate with Zitadel (OIDC PKCE or Device Code)")
625
+ .description("PLATFORM access: sign in to a deployed Mesh platform (`<tenant>.<env>`) — what `mesh deploy`, `mesh stack`, the Hub and the VPN use. It never touches your ~/.npmrc, and you do not need it to install packages: that is `mesh registry login`. OIDC PKCE, or device code with --device.")
626
626
  .argument("<context>", 'Platform context (e.g., "mesh.dev")')
627
627
  .option("--status", "Show current authentication status — the Zitadel session if one is cached, otherwise the AWS identity the credential chain resolves (SSO profile or key pair). Exits non-zero only when neither works.")
628
628
  .option("--device", "Force device code flow (no callback server needed)")
@@ -668,7 +668,7 @@ export function registerLoginCommand(program) {
668
668
  });
669
669
  program
670
670
  .command("logout")
671
- .description("Clear cached Zitadel credentials")
671
+ .description("Clear a platform context's cached credentials (the package-registry session under `mesh registry` is untouched)")
672
672
  .argument("<context>", 'Platform context (e.g., "mesh.dev")')
673
673
  .action((context) => {
674
674
  refuseReservedRegistryContext(context, "logout");
@@ -698,8 +698,20 @@ async function hintRegistryIfMissing() {
698
698
  catch {
699
699
  }
700
700
  }
701
+ export function shorthandContext(context) {
702
+ const parts = context.split(".");
703
+ if (parts.length !== 2)
704
+ return null;
705
+ const [tenant, env] = parts;
706
+ return tenant && env ? { tenant, env } : null;
707
+ }
701
708
  export function renderNoConfigHelp(context) {
702
709
  const pinned = firstPartyDomainFor(context);
710
+ const shorthand = shorthandContext(context);
711
+ const createOne = shorthand
712
+ ? `mesh init --tenant ${shorthand.tenant} --new-platform ${shorthand.env}`
713
+ : "mesh init --tenant <tenant> --new-platform <env>";
714
+ const scaffoldOne = `mesh init platform ${shorthand?.tenant ?? "<tenant>"} --domain <a domain you control>`;
703
715
  return (`No configuration found for "${context}".\n\n` +
704
716
  `Tried:\n` +
705
717
  ` 1. SSM: /mesh-platform/${context.split(".")[0]}/${context.split(".")[1]}/platform/zitadel\n` +
@@ -709,6 +721,8 @@ export function renderNoConfigHelp(context) {
709
721
  : ` 2. HTTPS: https://cli.${context}/.well-known/mesh.json\n` +
710
722
  ` (only attempted for a full domain, or a known first-party context)\n\n`) +
711
723
  `To fix:\n` +
724
+ ` - No platform deployed yet? Create one: ${createOne}\n` +
725
+ ` (or the scaffold on its own: ${scaffoldOne})\n` +
712
726
  ` - Use the platform's full domain: mesh login <env>.<tenant>.meshtech.io\n` +
713
727
  ` - Or, if you have AWS access, ensure SSO is active: aws sso login --profile <profile>\n` +
714
728
  ` - Or add config manually to ${CONFIG_FILE}:\n\n` +
@@ -2,7 +2,7 @@ import { Command } from "commander";
2
2
  import { MeshCliError } from "../utils/errors.js";
3
3
  import type { RegistryTokenState } from "../utils/auth-preflight.js";
4
4
  import { type RegistryBrokerFailure, type RegistryGrant } from "../utils/registry-broker.js";
5
- import { readRegistrySession } from "../utils/registry-identity.js";
5
+ import { readRegistrySession, type RegistryAccountState } from "../utils/registry-identity.js";
6
6
  export declare function codeartifactLogin(env?: Record<string, string>): boolean;
7
7
  export declare function resolvePublisherRoleArn({ roleArnFlag, ssmArn, }: {
8
8
  roleArnFlag?: string;
@@ -12,7 +12,7 @@ export declare function resolveCredentials(context?: string, roleArn?: string):
12
12
  export declare function applyGrantToUserNpmrc(grant: RegistryGrant, npmrcPath?: string): {
13
13
  path: string;
14
14
  };
15
- export declare function describeBrokerFailure(failure: RegistryBrokerFailure): {
15
+ export declare function describeBrokerFailure(failure: RegistryBrokerFailure, account?: RegistryAccountState): {
16
16
  message: string;
17
17
  hint?: string;
18
18
  fatal: boolean;
@@ -5,7 +5,7 @@ import { logInfo, logSuccess, logWarn } from "../utils/log.js";
5
5
  import { MeshCliError } from "../utils/errors.js";
6
6
  import { awsConfigHasProfile, findUnscopedCodeArtifactRegistry, homeNpmrcPath, npmrcAuthKeyForEndpoint, probeRegistryToken, readAwsConfig, SSO_SESSION_SNIPPET, stripUnscopedCodeArtifactRegistry, upsertNpmrcLines, } from "../utils/auth-preflight.js";
7
7
  import { fetchRegistryGrant, realGrantDeps, PUBLISHED_BROKER_TIMEOUT_MS, } from "../utils/registry-broker.js";
8
- import { clearRegistrySession, describeSessionExpiry, ensureRegistrySession, identityChanged, logRegistryIdentity, readRegistrySession, refreshRegistrySession, resolveRegistryIdentity, } from "../utils/registry-identity.js";
8
+ import { clearRegistrySession, describeSessionExpiry, ensureRegistrySession, identityChanged, logRegistryIdentity, readRegistrySession, refreshRegistrySession, registryAccountState, resolveRegistryIdentity, } from "../utils/registry-identity.js";
9
9
  import { readCredentials } from "./login.js";
10
10
  import { registerRegistryPublish } from "./registry-publish.js";
11
11
  const CA_DOMAIN = "mesh-platform";
@@ -222,7 +222,11 @@ export function applyGrantToUserNpmrc(grant, npmrcPath = homeNpmrcPath()) {
222
222
  }
223
223
  return { path: npmrcPath };
224
224
  }
225
- export function describeBrokerFailure(failure) {
225
+ export function describeBrokerFailure(failure, account = {}) {
226
+ const unverified = account.emailVerified === false
227
+ ? `\n Your Mesh account${account.email ? ` (${account.email})` : ""} has never confirmed its email address, and an unconfirmed account cannot hold a registry grant.` +
228
+ `\n This is not something a role fixes: ask your Mesh contact to verify the address on your account (a self-registered, passkey-only sign-up lands here), then run: mesh registry logout && mesh registry login`
229
+ : "";
226
230
  switch (failure.kind) {
227
231
  case "no-broker":
228
232
  return {
@@ -237,7 +241,7 @@ export function describeBrokerFailure(failure) {
237
241
  };
238
242
  case "unauthenticated":
239
243
  return {
240
- message: "The registry broker rejected your session (expired or wrong audience).",
244
+ message: "The registry broker rejected your session (expired or wrong audience)." + unverified,
241
245
  hint: "mesh registry logout && mesh registry login",
242
246
  fatal: false,
243
247
  };
@@ -251,7 +255,8 @@ export function describeBrokerFailure(failure) {
251
255
  (failure.requestUrl
252
256
  ? `\n Request access: ${failure.requestUrl}`
253
257
  : "\n Ask a Mesh platform admin to grant your account registry access.") +
254
- `\n ${NOT_AUTHORIZED_RERUN}`,
258
+ `\n ${NOT_AUTHORIZED_RERUN}` +
259
+ unverified,
255
260
  fatal: true,
256
261
  };
257
262
  case "unavailable":
@@ -327,7 +332,7 @@ async function tryBrokerLogin(opts) {
327
332
  }
328
333
  if (result.ok)
329
334
  return { grant: result.grant };
330
- const { message, hint, fatal } = describeBrokerFailure(result.failure);
335
+ const { message, hint, fatal } = describeBrokerFailure(result.failure, registryAccountState(readCredentials(session.context)?.idToken));
331
336
  if (fatal) {
332
337
  throw new RegistryNotAuthorizedError(message);
333
338
  }
@@ -627,7 +632,7 @@ export function describeAwsChainFailure(outcome) {
627
632
  export function registerRegistryCommands(program) {
628
633
  const registry = program
629
634
  .command("registry")
630
- .description("Manage npm registry auth for @mesh-tech packages");
635
+ .description("PACKAGE access: the sign-in that makes @mesh-tech/* installable. One global identity — no AWS account, no platform, no `<tenant>.<env>` context. (`mesh login <tenant>.<env>` is the other one; that is for deploying to or operating a platform.)");
631
636
  registry
632
637
  .command("login")
633
638
  .description("Sign in to the Mesh package registry (browser, or device code with --device) and configure ~/.npmrc + the project .npmrc. Needs no platform context and no AWS account.")
@@ -0,0 +1,5 @@
1
+ import { type MeshContext } from "../../utils/context.js";
2
+ import { type StackSelector } from "../../utils/stack-flag.js";
3
+ export declare function resolveSecretsContext(opts: StackSelector & {
4
+ tenant?: string;
5
+ }, stackOpt?: string | undefined): MeshContext;
@@ -0,0 +1,5 @@
1
+ import { detectContext } from "../../utils/context.js";
2
+ import { resolveStackOption } from "../../utils/stack-flag.js";
3
+ export function resolveSecretsContext(opts, stackOpt = resolveStackOption(opts)) {
4
+ return detectContext(stackOpt, { tenant: opts.tenant, stack: stackOpt });
5
+ }
@@ -21,6 +21,7 @@ export function registerSecretsCommands(program) {
21
21
  .option("--key <key>", "Instance key for multi-instance services (e.g., FI ID)")
22
22
  .option("--all", "Prompt for all fields including optional ones")
23
23
  .option("--json <json>", "Non-interactive: provide values as JSON")
24
+ .option("-t, --tenant <tenant>", "Tenant to address (override detection)")
24
25
  .option("--stack <stack>", "Pulumi stack name (override detection)")
25
26
  .addOption(new Option("--stage <stack>", "Deprecated alias for --stack").hideHelp())
26
27
  .option("--region <region>", "AWS region (default: us-east-2)")
@@ -31,6 +32,7 @@ export function registerSecretsCommands(program) {
31
32
  .command("reindex [service]")
32
33
  .description("Rebuild a multi-instance external service's instance index (the {prefix}/.index secret listInstances() reads) from the per-key secrets actually in Secrets Manager — the repair for an instance secret created or deleted out of band")
33
34
  .option("--dry-run", "Show what would change without writing")
35
+ .option("-t, --tenant <tenant>", "Tenant to address (override detection)")
34
36
  .option("--stack <stack>", "Pulumi stack name (override detection)")
35
37
  .addOption(new Option("--stage <stack>", "Deprecated alias for --stack").hideHelp())
36
38
  .option("--region <region>", "AWS region (default: us-east-2)")
@@ -42,6 +44,7 @@ export function registerSecretsCommands(program) {
42
44
  .description("Backfill .config mirror secrets for all external services")
43
45
  .option("--force", "Overwrite existing .config mirrors")
44
46
  .option("--dry-run", "Show what would be written without writing")
47
+ .option("-t, --tenant <tenant>", "Tenant to address (override detection)")
45
48
  .option("--stack <stack>", "Pulumi stack name (override detection)")
46
49
  .addOption(new Option("--stage <stack>", "Deprecated alias for --stack").hideHelp())
47
50
  .option("--region <region>", "AWS region (default: us-east-2)")
@@ -1,6 +1,7 @@
1
1
  export declare function migrateConfigCommand(opts: {
2
2
  force?: boolean;
3
3
  dryRun?: boolean;
4
+ tenant?: string;
4
5
  stack?: string;
5
6
  stage?: string;
6
7
  region?: string;
@@ -1,8 +1,7 @@
1
1
  import { SSMClient, GetParametersByPathCommand, } from "@aws-sdk/client-ssm";
2
2
  import { SecretsManagerClient, GetSecretValueCommand, PutSecretValueCommand, CreateSecretCommand, ListSecretsCommand, } from "@aws-sdk/client-secrets-manager";
3
- import { detectContext } from "../../utils/context.js";
4
3
  import { logInfo, logSuccess, logError } from "../../utils/log.js";
5
- import { resolveStackOption } from "../../utils/stack-flag.js";
4
+ import { resolveSecretsContext } from "./context.js";
6
5
  function normalizeEntry(raw) {
7
6
  if (raw.type === "group" && raw.fields) {
8
7
  return {
@@ -198,7 +197,7 @@ async function migrateService(smClient, meta, schema, opts) {
198
197
  return { migrated, skipped, errors };
199
198
  }
200
199
  export async function migrateConfigCommand(opts) {
201
- const context = detectContext(resolveStackOption(opts));
200
+ const context = resolveSecretsContext(opts);
202
201
  const region = opts.region ?? process.env.AWS_REGION ?? "us-east-2";
203
202
  const ssmClient = new SSMClient({ region });
204
203
  const smClient = new SecretsManagerClient({ region });
@@ -2,6 +2,7 @@ import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
2
2
  export declare function scanInstanceKeys(client: SecretsManagerClient, secretPrefix: string): Promise<string[]>;
3
3
  export declare function indexAlreadyMatches(missing: string[], stale: string[], indexExists: boolean, malformed: boolean): boolean;
4
4
  export declare function reindexCommand(servicePath: string | undefined, opts: {
5
+ tenant?: string;
5
6
  stack?: string;
6
7
  stage?: string;
7
8
  region?: string;
@@ -1,9 +1,9 @@
1
1
  import { SSMClient } from "@aws-sdk/client-ssm";
2
2
  import { SecretsManagerClient, ListSecretsCommand, GetSecretValueCommand, PutSecretValueCommand, CreateSecretCommand, } from "@aws-sdk/client-secrets-manager";
3
3
  import { buildInstanceIndexValue, instanceIndexSecretId, instanceKeyFromSecretName, parseInstanceIndex, } from "@mesh-tech/secrets";
4
- import { detectContext } from "../../utils/context.js";
5
4
  import { logInfo, logSuccess, logError } from "../../utils/log.js";
6
5
  import { resolveStackOption } from "../../utils/stack-flag.js";
6
+ import { resolveSecretsContext } from "./context.js";
7
7
  import { discoverExternalServices, ensureAwsCredentialsForStack, isResourceNotFound, } from "./set.js";
8
8
  export async function scanInstanceKeys(client, secretPrefix) {
9
9
  const keys = [];
@@ -27,7 +27,7 @@ export function indexAlreadyMatches(missing, stale, indexExists, malformed) {
27
27
  }
28
28
  export async function reindexCommand(servicePath, opts) {
29
29
  const stackOpt = resolveStackOption(opts);
30
- const context = detectContext(stackOpt);
30
+ const context = resolveSecretsContext(opts, stackOpt);
31
31
  const region = opts.region ?? process.env.AWS_REGION ?? "us-east-2";
32
32
  await ensureAwsCredentialsForStack(stackOpt ?? context.stage);
33
33
  const ssmClient = new SSMClient({ region });
@@ -33,6 +33,7 @@ export declare function setCommand(servicePath: string | undefined, opts: {
33
33
  key?: string;
34
34
  all?: boolean;
35
35
  json?: string;
36
+ tenant?: string;
36
37
  stack?: string;
37
38
  stage?: string;
38
39
  region?: string;
@@ -3,9 +3,9 @@ import { SecretsManagerClient, GetSecretValueCommand, PutSecretValueCommand, Cre
3
3
  import input from "@inquirer/input";
4
4
  import password from "@inquirer/password";
5
5
  import { buildInstanceIndexValue, instanceIndexSecretId, isInstanceKey, parseInstanceIndex, } from "@mesh-tech/secrets";
6
- import { detectContext } from "../../utils/context.js";
7
6
  import { logInfo, logSuccess, logError } from "../../utils/log.js";
8
7
  import { resolveStackOption } from "../../utils/stack-flag.js";
8
+ import { resolveSecretsContext } from "./context.js";
9
9
  import { findAppRoot, readStackConfig } from "../../utils/pulumi.js";
10
10
  import { resolveAwsCredentials } from "../../utils/aws-auth.js";
11
11
  export async function ensureAwsCredentialsForStack(stack) {
@@ -147,7 +147,7 @@ function displaySummary(fields, values, depth = 0) {
147
147
  }
148
148
  export async function setCommand(servicePath, opts) {
149
149
  const stackOpt = resolveStackOption(opts);
150
- const context = detectContext(stackOpt);
150
+ const context = resolveSecretsContext(opts, stackOpt);
151
151
  const region = opts.region ?? process.env.AWS_REGION ?? "us-east-2";
152
152
  await ensureAwsCredentialsForStack(stackOpt ?? context.stage);
153
153
  const ssmClient = new SSMClient({ region });