@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 +82 -34
- package/dist/bin/mesh.js.map +1 -1
- package/dist/build-info.json +2 -2
- package/dist/src/commands/app-check.js +4 -4
- package/dist/src/commands/login.d.ts +8 -0
- package/dist/src/commands/login.js +84 -32
- package/dist/src/commands/temporal.js +49 -27
- package/dist/src/commands/temporal.js.map +1 -1
- package/fragments/agents/AGENTS.md.hbs +8 -4
- package/fragments/base/index.ts.hbs +14 -10
- package/package.json +2 -2
- package/skills/core/SKILL.md +2 -2
- package/templates/api-auth/api/src/schema.ts.hbs +4 -0
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
|
-
|
|
3985
|
-
|
|
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 (
|
|
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",
|
|
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:
|
|
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,
|
|
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
|
-
|
|
4789
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
12176
|
-
// Advisory
|
|
12177
|
-
//
|
|
12178
|
-
//
|
|
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: "
|
|
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: {
|