@mesh-tech/mesh-cli 0.29.0 → 0.31.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.
package/dist/bin/mesh.js CHANGED
@@ -3892,6 +3892,7 @@ __export(login_exports, {
3892
3892
  REGISTRY_SEPARATE_HINT: () => REGISTRY_SEPARATE_HINT,
3893
3893
  RESERVED_REGISTRY_CONTEXT: () => RESERVED_REGISTRY_CONTEXT,
3894
3894
  accountIdFromRoleArn: () => accountIdFromRoleArn,
3895
+ adoptPublishedHubProject: () => adoptPublishedHubProject,
3895
3896
  atomicWriteFileSync: () => atomicWriteFileSync,
3896
3897
  clearContextConfig: () => clearContextConfig,
3897
3898
  clearCredentials: () => clearCredentials,
@@ -3904,6 +3905,7 @@ __export(login_exports, {
3904
3905
  getContextConfig: () => getContextConfig,
3905
3906
  getValidToken: () => getValidToken,
3906
3907
  isRemoteEnvironment: () => isRemoteEnvironment,
3908
+ loginScopes: () => loginScopes,
3907
3909
  loginTimeoutMessage: () => loginTimeoutMessage,
3908
3910
  probeCredentials: () => probeCredentials,
3909
3911
  readAllContextConfigs: () => readAllContextConfigs,
@@ -3919,7 +3921,9 @@ __export(login_exports, {
3919
3921
  resolveUserAwsConfigPath: () => resolveUserAwsConfigPath,
3920
3922
  runLoginFlow: () => runLoginFlow,
3921
3923
  shorthandContext: () => shorthandContext,
3924
+ tokenAudience: () => tokenAudience,
3922
3925
  tokenStillValid: () => tokenStillValid,
3926
+ withDiscoveredFields: () => withDiscoveredFields,
3923
3927
  writeContextConfig: () => writeContextConfig
3924
3928
  });
3925
3929
  import * as http from "http";
@@ -3927,6 +3931,17 @@ import * as crypto2 from "crypto";
3927
3931
  import * as fs11 from "fs";
3928
3932
  import * as path11 from "path";
3929
3933
  import { execFileSync as execFileSync10 } from "child_process";
3934
+ function loginScopes(config) {
3935
+ return config.hubProjectId ? `${BASE_SCOPES} urn:zitadel:iam:org:project:id:${config.hubProjectId}:aud` : BASE_SCOPES;
3936
+ }
3937
+ function withDiscoveredFields(config, data) {
3938
+ const out = { ...config };
3939
+ for (const key of ["vpn", "vpnJoinBroker", "registryBroker", "hubProjectId"]) {
3940
+ const value = data[key];
3941
+ if (typeof value === "string" && value) out[key] = value;
3942
+ }
3943
+ return out;
3944
+ }
3930
3945
  function readConfig() {
3931
3946
  if (!fs11.existsSync(CONFIG_FILE)) return {};
3932
3947
  try {
@@ -3957,12 +3972,12 @@ function parseTenantEnv(context) {
3957
3972
  if (parts.length !== 2 || !parts[0] || !parts[1]) return null;
3958
3973
  return { tenant: parts[0], env: parts[1] };
3959
3974
  }
3960
- async function discoverConfigFromSsm(context) {
3975
+ async function discoverConfigFromSsm(context, opts = {}) {
3961
3976
  const parsed = parseTenantEnv(context);
3962
3977
  if (!parsed) return null;
3963
3978
  const { tenant, env } = parsed;
3964
3979
  const ssmPath = `/mesh-platform/${tenant}/${env}/platform/zitadel`;
3965
- logInfo(`Attempting SSM discovery from ${ssmPath}...`);
3980
+ if (!opts.quiet) logInfo(`Attempting SSM discovery from ${ssmPath}...`);
3966
3981
  try {
3967
3982
  const { SSMClient: SSMClient5, GetParameterCommand: GetParameterCommand2 } = await import("@aws-sdk/client-ssm");
3968
3983
  const region = process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION ?? "us-east-2";
@@ -3970,30 +3985,31 @@ async function discoverConfigFromSsm(context) {
3970
3985
  const resp = await ssm.send(new GetParameterCommand2({ Name: ssmPath }));
3971
3986
  const raw = resp.Parameter?.Value;
3972
3987
  if (!raw) {
3973
- logWarn(`SSM parameter ${ssmPath} has no value`);
3988
+ if (!opts.silent) logWarn(`SSM parameter ${ssmPath} has no value`);
3974
3989
  return null;
3975
3990
  }
3976
3991
  const data = JSON.parse(raw);
3977
3992
  const issuer = data.endpoint;
3978
3993
  const clientId = data.cliClientId;
3979
3994
  if (typeof issuer !== "string" || !issuer) {
3980
- logWarn("SSM zitadel entry missing 'endpoint' field");
3995
+ if (!opts.silent) logWarn("SSM zitadel entry missing 'endpoint' field");
3981
3996
  return null;
3982
3997
  }
3983
3998
  if (typeof clientId !== "string" || !clientId) {
3984
- logWarn(
3985
- "SSM zitadel entry missing 'cliClientId' field.\n The platform needs to be deployed with an updated ZitadelPlatformIdentity\n that exports cliClientId. Until then, contact your platform admin."
3986
- );
3999
+ if (!opts.silent) {
4000
+ logWarn(
4001
+ "SSM zitadel entry missing 'cliClientId' field.\n The platform needs to be deployed with an updated ZitadelPlatformIdentity\n that exports cliClientId. Until then, contact your platform admin."
4002
+ );
4003
+ }
3987
4004
  return null;
3988
4005
  }
3989
- const config = { issuer, clientId };
3990
- if (typeof data.vpn === "string") config.vpn = data.vpn;
3991
- if (typeof data.vpnJoinBroker === "string") config.vpnJoinBroker = data.vpnJoinBroker;
3992
- if (typeof data.registryBroker === "string") config.registryBroker = data.registryBroker;
4006
+ const config = withDiscoveredFields({ issuer, clientId }, data);
4007
+ if (opts.quiet) return config;
3993
4008
  writeContextConfig(context, config);
3994
4009
  logSuccess(`Discovered platform configuration for ${context} via SSM`);
3995
4010
  return config;
3996
4011
  } catch (err) {
4012
+ if (opts.silent) return null;
3997
4013
  const message = err instanceof Error ? err.message : String(err);
3998
4014
  if (message.includes("ExpiredToken") || message.includes("credentials")) {
3999
4015
  logWarn(`SSM discovery failed: AWS credentials expired or unavailable.`);
@@ -4021,26 +4037,24 @@ async function discoverConfig(domain, contextKey = domain, opts = {}) {
4021
4037
  const resp = await fetch(url, { signal: controller.signal });
4022
4038
  clearTimeout(timeout);
4023
4039
  if (!resp.ok) {
4024
- logWarn(`Discovery endpoint returned ${resp.status}`);
4040
+ if (!opts.silent) logWarn(`Discovery endpoint returned ${resp.status}`);
4025
4041
  return null;
4026
4042
  }
4027
4043
  const data = await resp.json();
4028
4044
  const issuer = data.issuer;
4029
4045
  const clientId = data.clientId;
4030
4046
  if (typeof issuer !== "string" || !issuer || typeof clientId !== "string" || !clientId) {
4031
- logWarn("Discovery endpoint returned invalid config (missing issuer or clientId)");
4047
+ if (!opts.silent) logWarn("Discovery endpoint returned invalid config (missing issuer or clientId)");
4032
4048
  return null;
4033
4049
  }
4034
- const config = { issuer, clientId };
4035
- if (typeof data.vpn === "string") config.vpn = data.vpn;
4036
- if (typeof data.vpnJoinBroker === "string") config.vpnJoinBroker = data.vpnJoinBroker;
4037
- if (typeof data.registryBroker === "string") config.registryBroker = data.registryBroker;
4050
+ const config = withDiscoveredFields({ issuer, clientId }, data);
4038
4051
  if (!opts.quiet) {
4039
4052
  writeContextConfig(contextKey, config);
4040
4053
  logSuccess(`Discovered platform configuration for ${contextKey}`);
4041
4054
  }
4042
4055
  return config;
4043
4056
  } catch (err) {
4057
+ if (opts.silent) return null;
4044
4058
  const message = err instanceof Error ? err.message : String(err);
4045
4059
  if (message.includes("abort")) {
4046
4060
  logWarn("Discovery timed out");
@@ -4149,7 +4163,7 @@ function login(context, config) {
4149
4163
  authUrl.searchParams.set("client_id", config.clientId);
4150
4164
  authUrl.searchParams.set("redirect_uri", REDIRECT_URI);
4151
4165
  authUrl.searchParams.set("response_type", "code");
4152
- authUrl.searchParams.set("scope", SCOPES);
4166
+ authUrl.searchParams.set("scope", loginScopes(config));
4153
4167
  authUrl.searchParams.set("code_challenge", codeChallenge);
4154
4168
  authUrl.searchParams.set("code_challenge_method", "S256");
4155
4169
  authUrl.searchParams.set("state", state);
@@ -4277,7 +4291,7 @@ async function attemptDeviceCode(context, config) {
4277
4291
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
4278
4292
  body: new URLSearchParams({
4279
4293
  client_id: config.clientId,
4280
- scope: SCOPES
4294
+ scope: loginScopes(config)
4281
4295
  }).toString()
4282
4296
  });
4283
4297
  if (!resp.ok) {
@@ -4499,9 +4513,11 @@ async function showStatus(context, config) {
4499
4513
  return;
4500
4514
  }
4501
4515
  let tenants = [];
4516
+ let audience = [];
4502
4517
  try {
4503
4518
  const accessPayload = decodeJwtPayload(creds.accessToken);
4504
4519
  tenants = accessPayload["urn:mesh:tenants"] ?? [];
4520
+ audience = tokenAudience(accessPayload);
4505
4521
  } catch {
4506
4522
  }
4507
4523
  logSuccess(`Logged in as ${creds.email ?? "unknown"} (${context})`);
@@ -4510,6 +4526,25 @@ async function showStatus(context, config) {
4510
4526
  if (tenants.length > 0) {
4511
4527
  logInfo(`Deployable tenants: ${tenants.join(", ")}`);
4512
4528
  }
4529
+ if (audience.length > 0) {
4530
+ logInfo(`Audience: ${audience.join(", ")}`);
4531
+ if (config.hubProjectId && !audience.includes(config.hubProjectId)) {
4532
+ logWarn(
4533
+ `This session does not carry the Hub project audience (${config.hubProjectId}); a Hub that enforces its audience will refuse it. Re-run: mesh login ${context}`
4534
+ );
4535
+ }
4536
+ }
4537
+ if (!config.hubProjectId) {
4538
+ logInfo(
4539
+ `Hub project: not in this context's discovery record, so this session was requested without a Hub audience \u2014 a Hub that enforces its audience would refuse it. \`mesh login ${context}\` re-checks what the platform publishes.`
4540
+ );
4541
+ }
4542
+ }
4543
+ function tokenAudience(payload) {
4544
+ const aud = payload.aud;
4545
+ if (typeof aud === "string") return [aud];
4546
+ if (Array.isArray(aud)) return aud.filter((a) => typeof a === "string");
4547
+ return [];
4513
4548
  }
4514
4549
  function registerLoginCommand(program2) {
4515
4550
  program2.command("login").description(
@@ -4560,6 +4595,7 @@ function registerLoginCommand(program2) {
4560
4595
  if (opts.registry === false) {
4561
4596
  logWarn(NO_REGISTRY_DEPRECATED);
4562
4597
  }
4598
+ config = await adoptPublishedHubProject(context, config);
4563
4599
  try {
4564
4600
  await runLoginFlow(context, config, opts);
4565
4601
  } catch (err) {
@@ -4756,8 +4792,9 @@ async function ensureValidToken(context, config, opts) {
4756
4792
  let token = await getValidToken(context);
4757
4793
  if (token) return token;
4758
4794
  logInfo(`No valid Zitadel session for ${context} \u2014 running login flow`);
4795
+ const adopted = await adoptPublishedHubProject(context, config);
4759
4796
  try {
4760
- await runLoginFlow(context, config, opts);
4797
+ await runLoginFlow(context, adopted, opts);
4761
4798
  } catch (err) {
4762
4799
  logError(`Login failed: ${err.message}`);
4763
4800
  return null;
@@ -4777,23 +4814,34 @@ async function ensureLogin(context, opts = {}) {
4777
4814
  if (!token) return null;
4778
4815
  return readCredentials(context);
4779
4816
  }
4780
- async function discoverConfigGuarded(context) {
4817
+ async function discoverConfigGuarded(context, opts = {}) {
4781
4818
  let config = null;
4782
4819
  if (parseTenantEnv(context)) {
4783
- config = await discoverConfigFromSsm(context);
4820
+ config = await discoverConfigFromSsm(context, opts);
4784
4821
  }
4785
4822
  if (!config) {
4786
4823
  const pinned = firstPartyDomainFor(context);
4787
4824
  if (pinned) {
4788
- logInfo(`"${context}" is a known Mesh platform \u2014 resolving via ${pinned} (no AWS needed)`);
4789
- config = await discoverConfig(pinned, context);
4825
+ if (!opts.quiet) {
4826
+ logInfo(`"${context}" is a known Mesh platform \u2014 resolving via ${pinned} (no AWS needed)`);
4827
+ }
4828
+ config = await discoverConfig(pinned, context, opts);
4790
4829
  }
4791
4830
  }
4792
4831
  if (!config && context.split(".").length - 1 >= 2) {
4793
- config = await discoverConfig(context);
4832
+ config = await discoverConfig(context, context, opts);
4794
4833
  }
4795
4834
  return config;
4796
4835
  }
4836
+ async function adoptPublishedHubProject(context, config, discover = discoverConfigGuarded, write = writeContextConfig) {
4837
+ if (config.hubProjectId) return config;
4838
+ const fresh = await discover(context, { quiet: true, silent: true });
4839
+ if (!fresh?.hubProjectId) return config;
4840
+ const merged = { ...config, hubProjectId: fresh.hubProjectId };
4841
+ write(context, merged);
4842
+ logInfo(`${context} publishes its Hub project (${fresh.hubProjectId}) \u2014 requesting the Hub audience for this login`);
4843
+ return merged;
4844
+ }
4797
4845
  async function exportAwsCredentials(context, config, opts) {
4798
4846
  const token = await ensureValidToken(context, config, opts);
4799
4847
  if (!token) {
@@ -4914,7 +4962,7 @@ function readAllContextConfigs() {
4914
4962
  async function discoverConfigForContext(context) {
4915
4963
  return getContextConfig(context) ?? await discoverConfigGuarded(context);
4916
4964
  }
4917
- var CONFIG_DIR, CONFIG_FILE, CREDENTIALS_FILE, REDIRECT_PORT, REDIRECT_URI, SCOPES, atomicWriteCounter, MAX_DEVICE_CODES, RESERVED_REGISTRY_CONTEXT, NO_REGISTRY_DEPRECATED, REGISTRY_SEPARATE_HINT;
4965
+ var CONFIG_DIR, CONFIG_FILE, CREDENTIALS_FILE, REDIRECT_PORT, REDIRECT_URI, BASE_SCOPES, atomicWriteCounter, MAX_DEVICE_CODES, RESERVED_REGISTRY_CONTEXT, NO_REGISTRY_DEPRECATED, REGISTRY_SEPARATE_HINT;
4918
4966
  var init_login = __esm({
4919
4967
  "libs/mesh-cli/src/commands/login.ts"() {
4920
4968
  "use strict";
@@ -4931,7 +4979,7 @@ var init_login = __esm({
4931
4979
  CREDENTIALS_FILE = path11.join(CONFIG_DIR, "credentials.json");
4932
4980
  REDIRECT_PORT = 9876;
4933
4981
  REDIRECT_URI = `http://localhost:${REDIRECT_PORT}/callback`;
4934
- SCOPES = "openid email profile offline_access urn:zitadel:iam:org:project:id:zitadel:aud";
4982
+ BASE_SCOPES = "openid email profile offline_access urn:zitadel:iam:org:project:id:zitadel:aud";
4935
4983
  atomicWriteCounter = 0;
4936
4984
  MAX_DEVICE_CODES = 3;
4937
4985
  RESERVED_REGISTRY_CONTEXT = "registry";
@@ -11755,7 +11803,7 @@ function docsSiteFindings(text) {
11755
11803
  findings.push({
11756
11804
  code: "DOCS_SITE_PUBLIC",
11757
11805
  line: decl.line,
11758
- why: `mesh.apps.${decl.ctor} declares a docs site with ${named} \u2014 retired: a docs site has no hostname, no sign-in and no door switch of its own; it is read inside the Hub through the docs door, and the primitive ignores ${retired.length > 1 ? "them" : "it"}`
11806
+ why: `mesh.apps.${decl.ctor} declares a docs site with ${named} \u2014 deprecated: the Hub docs door is the path this site is published on, and it is already built for it (no hostname, no sign-in and no door switch of its own), so the primitive ignores ${retired.length > 1 ? "them" : "it"}`
11759
11807
  });
11760
11808
  }
11761
11809
  const kindStated = decl.ctor === "AppApiSurface" || decl.ctor === "VendorApiSurface" || args.has("kind");
@@ -11788,7 +11836,7 @@ function docsSiteFindings(text) {
11788
11836
  findings.push({
11789
11837
  code: "DOCS_SITE_PUBLIC",
11790
11838
  line: decl.line,
11791
- why: `mesh.apps.ApiDocs is published on an ingress \u2014 a docs site has no hostname of its own; it is read inside the Hub through the docs door of the API surface that declares it, and the ingress is retired (it still deploys, with a preview warning, until a coming minor release refuses it)`
11839
+ why: `mesh.apps.ApiDocs is published on an ingress \u2014 deprecated: the Hub docs door is the path a docs site is published on, and it opens for the API surface that declares the site. The hostname still deploys, with a preview warning, as the compatibility path`
11792
11840
  });
11793
11841
  }
11794
11842
  return findings;
@@ -12172,12 +12220,12 @@ var init_app_check = __esm({
12172
12220
  },
12173
12221
  DOCS_SITE_PUBLIC: {
12174
12222
  gate: "0.3",
12175
- title: "Docs site read through the Hub, not on a hostname of its own",
12176
- // Advisory while the hostname is deprecated rather than refused: a site
12177
- // that still deploys on one keeps deploying this release, with a preview
12178
- // warning, and its tenant CI should say so without going red.
12223
+ title: "Docs site published through the Hub docs door",
12224
+ // Advisory because the public shape is the compatibility path, not a
12225
+ // fault: a site on it keeps deploying, and its tenant CI should read the
12226
+ // route without going red.
12179
12227
  severity: "advisory",
12180
- remediation: "drop the docs hostname \u2014 a docs site is read inside the Hub through the docs door, which signs every reader in: on an API surface remove docs.ingress (docs.auth, docs.basePath and docs.hubDoor are retired with it); on a mesh.apps.ApiDocs remove the ingress (and its auth / authentication) and declare the API as a mesh.apps.AppApiSurface whose docs block builds the site",
12228
+ remediation: "publish the site through the Hub docs door \u2014 declare the API as a mesh.apps.AppApiSurface whose docs block builds this same site (docs: { spec, pages }), declare its readers' role on the ZitadelAppIdentity and grant it from the Hub's Access tab; then drop the hostname (docs.ingress on an API surface, where docs.auth, docs.basePath and docs.hubDoor do nothing either; the ingress with its auth and authentication on a mesh.apps.ApiDocs). The public shape keeps deploying until you do",
12181
12229
  scope: "app"
12182
12230
  },
12183
12231
  DOCS_AUDIENCE_IMPLICIT: {