@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/build-info.json
CHANGED
|
@@ -108,9 +108,9 @@ export const APP_CHECK_RULES = {
|
|
|
108
108
|
},
|
|
109
109
|
DOCS_SITE_PUBLIC: {
|
|
110
110
|
gate: "0.3",
|
|
111
|
-
title: "Docs site
|
|
111
|
+
title: "Docs site published through the Hub docs door",
|
|
112
112
|
severity: "advisory",
|
|
113
|
-
remediation: "
|
|
113
|
+
remediation: "publish the site through the Hub docs door — 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",
|
|
114
114
|
scope: "app",
|
|
115
115
|
},
|
|
116
116
|
DOCS_AUDIENCE_IMPLICIT: {
|
|
@@ -509,7 +509,7 @@ export function docsSiteFindings(text) {
|
|
|
509
509
|
findings.push({
|
|
510
510
|
code: "DOCS_SITE_PUBLIC",
|
|
511
511
|
line: decl.line,
|
|
512
|
-
why: `mesh.apps.${decl.ctor} declares a docs site with ${named} —
|
|
512
|
+
why: `mesh.apps.${decl.ctor} declares a docs site with ${named} — 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"}`,
|
|
513
513
|
});
|
|
514
514
|
}
|
|
515
515
|
const kindStated = decl.ctor === "AppApiSurface" || decl.ctor === "VendorApiSurface" || args.has("kind");
|
|
@@ -545,7 +545,7 @@ export function docsSiteFindings(text) {
|
|
|
545
545
|
findings.push({
|
|
546
546
|
code: "DOCS_SITE_PUBLIC",
|
|
547
547
|
line: decl.line,
|
|
548
|
-
why: `mesh.apps.ApiDocs is published on an ingress —
|
|
548
|
+
why: `mesh.apps.ApiDocs is published on an ingress — 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`,
|
|
549
549
|
});
|
|
550
550
|
}
|
|
551
551
|
return findings;
|
|
@@ -3,6 +3,7 @@ import { type AwsIdentity } from "../utils/aws-auth.js";
|
|
|
3
3
|
declare const CONFIG_DIR: string;
|
|
4
4
|
declare const CONFIG_FILE: string;
|
|
5
5
|
declare const CREDENTIALS_FILE: string;
|
|
6
|
+
export declare function loginScopes(config: Pick<ContextConfig, "hubProjectId">): string;
|
|
6
7
|
interface ContextConfig {
|
|
7
8
|
issuer: string;
|
|
8
9
|
clientId: string;
|
|
@@ -12,7 +13,9 @@ interface ContextConfig {
|
|
|
12
13
|
vpn?: string;
|
|
13
14
|
vpnJoinBroker?: string;
|
|
14
15
|
registryBroker?: string;
|
|
16
|
+
hubProjectId?: string;
|
|
15
17
|
}
|
|
18
|
+
export declare function withDiscoveredFields(config: ContextConfig, data: Record<string, unknown>): ContextConfig;
|
|
16
19
|
interface CachedCredentials {
|
|
17
20
|
idToken: string;
|
|
18
21
|
accessToken: string;
|
|
@@ -46,6 +49,7 @@ export declare function renderAwsFallbackStatus(context: string, reason: string,
|
|
|
46
49
|
lines: StatusLine[];
|
|
47
50
|
exitCode: 0 | 1;
|
|
48
51
|
};
|
|
52
|
+
export declare function tokenAudience(payload: Record<string, unknown>): string[];
|
|
49
53
|
export declare function registerLoginCommand(program: Command): void;
|
|
50
54
|
export declare const RESERVED_REGISTRY_CONTEXT = "registry";
|
|
51
55
|
export declare const NO_REGISTRY_DEPRECATED = "--no-registry is deprecated and ignored: mesh login no longer touches the registry.";
|
|
@@ -107,6 +111,10 @@ export declare function ensureLogin(context: string, opts?: {
|
|
|
107
111
|
device?: boolean;
|
|
108
112
|
interactive?: boolean;
|
|
109
113
|
}): Promise<CachedCredentials | null>;
|
|
114
|
+
export declare function adoptPublishedHubProject(context: string, config: ContextConfig, discover?: (context: string, opts: {
|
|
115
|
+
quiet: true;
|
|
116
|
+
silent: true;
|
|
117
|
+
}) => Promise<ContextConfig | null>, write?: (context: string, config: ContextConfig) => void): Promise<ContextConfig>;
|
|
110
118
|
export { CONFIG_DIR, CONFIG_FILE, CREDENTIALS_FILE };
|
|
111
119
|
export type { ContextConfig, CachedCredentials, CredentialsFile };
|
|
112
120
|
export declare function readAllContextConfigs(): ConfigFile;
|
|
@@ -13,7 +13,21 @@ const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
|
13
13
|
const CREDENTIALS_FILE = path.join(CONFIG_DIR, "credentials.json");
|
|
14
14
|
const REDIRECT_PORT = 9876;
|
|
15
15
|
const REDIRECT_URI = `http://localhost:${REDIRECT_PORT}/callback`;
|
|
16
|
-
const
|
|
16
|
+
const BASE_SCOPES = "openid email profile offline_access urn:zitadel:iam:org:project:id:zitadel:aud";
|
|
17
|
+
export function loginScopes(config) {
|
|
18
|
+
return config.hubProjectId
|
|
19
|
+
? `${BASE_SCOPES} urn:zitadel:iam:org:project:id:${config.hubProjectId}:aud`
|
|
20
|
+
: BASE_SCOPES;
|
|
21
|
+
}
|
|
22
|
+
export function withDiscoveredFields(config, data) {
|
|
23
|
+
const out = { ...config };
|
|
24
|
+
for (const key of ["vpn", "vpnJoinBroker", "registryBroker", "hubProjectId"]) {
|
|
25
|
+
const value = data[key];
|
|
26
|
+
if (typeof value === "string" && value)
|
|
27
|
+
out[key] = value;
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
17
31
|
function readConfig() {
|
|
18
32
|
if (!fs.existsSync(CONFIG_FILE))
|
|
19
33
|
return {};
|
|
@@ -48,13 +62,14 @@ function parseTenantEnv(context) {
|
|
|
48
62
|
return null;
|
|
49
63
|
return { tenant: parts[0], env: parts[1] };
|
|
50
64
|
}
|
|
51
|
-
async function discoverConfigFromSsm(context) {
|
|
65
|
+
async function discoverConfigFromSsm(context, opts = {}) {
|
|
52
66
|
const parsed = parseTenantEnv(context);
|
|
53
67
|
if (!parsed)
|
|
54
68
|
return null;
|
|
55
69
|
const { tenant, env } = parsed;
|
|
56
70
|
const ssmPath = `/mesh-platform/${tenant}/${env}/platform/zitadel`;
|
|
57
|
-
|
|
71
|
+
if (!opts.quiet)
|
|
72
|
+
logInfo(`Attempting SSM discovery from ${ssmPath}...`);
|
|
58
73
|
try {
|
|
59
74
|
const { SSMClient, GetParameterCommand } = await import("@aws-sdk/client-ssm");
|
|
60
75
|
const region = process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION ?? "us-east-2";
|
|
@@ -62,34 +77,36 @@ async function discoverConfigFromSsm(context) {
|
|
|
62
77
|
const resp = await ssm.send(new GetParameterCommand({ Name: ssmPath }));
|
|
63
78
|
const raw = resp.Parameter?.Value;
|
|
64
79
|
if (!raw) {
|
|
65
|
-
|
|
80
|
+
if (!opts.silent)
|
|
81
|
+
logWarn(`SSM parameter ${ssmPath} has no value`);
|
|
66
82
|
return null;
|
|
67
83
|
}
|
|
68
84
|
const data = JSON.parse(raw);
|
|
69
85
|
const issuer = data.endpoint;
|
|
70
86
|
const clientId = data.cliClientId;
|
|
71
87
|
if (typeof issuer !== "string" || !issuer) {
|
|
72
|
-
|
|
88
|
+
if (!opts.silent)
|
|
89
|
+
logWarn("SSM zitadel entry missing 'endpoint' field");
|
|
73
90
|
return null;
|
|
74
91
|
}
|
|
75
92
|
if (typeof clientId !== "string" || !clientId) {
|
|
76
|
-
|
|
77
|
-
"
|
|
78
|
-
|
|
93
|
+
if (!opts.silent) {
|
|
94
|
+
logWarn("SSM zitadel entry missing 'cliClientId' field.\n" +
|
|
95
|
+
" The platform needs to be deployed with an updated ZitadelPlatformIdentity\n" +
|
|
96
|
+
" that exports cliClientId. Until then, contact your platform admin.");
|
|
97
|
+
}
|
|
79
98
|
return null;
|
|
80
99
|
}
|
|
81
|
-
const config = { issuer, clientId };
|
|
82
|
-
if (
|
|
83
|
-
config
|
|
84
|
-
if (typeof data.vpnJoinBroker === "string")
|
|
85
|
-
config.vpnJoinBroker = data.vpnJoinBroker;
|
|
86
|
-
if (typeof data.registryBroker === "string")
|
|
87
|
-
config.registryBroker = data.registryBroker;
|
|
100
|
+
const config = withDiscoveredFields({ issuer, clientId }, data);
|
|
101
|
+
if (opts.quiet)
|
|
102
|
+
return config;
|
|
88
103
|
writeContextConfig(context, config);
|
|
89
104
|
logSuccess(`Discovered platform configuration for ${context} via SSM`);
|
|
90
105
|
return config;
|
|
91
106
|
}
|
|
92
107
|
catch (err) {
|
|
108
|
+
if (opts.silent)
|
|
109
|
+
return null;
|
|
93
110
|
const message = err instanceof Error ? err.message : String(err);
|
|
94
111
|
if (message.includes("ExpiredToken") || message.includes("credentials")) {
|
|
95
112
|
logWarn(`SSM discovery failed: AWS credentials expired or unavailable.`);
|
|
@@ -121,23 +138,19 @@ async function discoverConfig(domain, contextKey = domain, opts = {}) {
|
|
|
121
138
|
const resp = await fetch(url, { signal: controller.signal });
|
|
122
139
|
clearTimeout(timeout);
|
|
123
140
|
if (!resp.ok) {
|
|
124
|
-
|
|
141
|
+
if (!opts.silent)
|
|
142
|
+
logWarn(`Discovery endpoint returned ${resp.status}`);
|
|
125
143
|
return null;
|
|
126
144
|
}
|
|
127
145
|
const data = (await resp.json());
|
|
128
146
|
const issuer = data.issuer;
|
|
129
147
|
const clientId = data.clientId;
|
|
130
148
|
if (typeof issuer !== "string" || !issuer || typeof clientId !== "string" || !clientId) {
|
|
131
|
-
|
|
149
|
+
if (!opts.silent)
|
|
150
|
+
logWarn("Discovery endpoint returned invalid config (missing issuer or clientId)");
|
|
132
151
|
return null;
|
|
133
152
|
}
|
|
134
|
-
const config = { issuer, clientId };
|
|
135
|
-
if (typeof data.vpn === "string")
|
|
136
|
-
config.vpn = data.vpn;
|
|
137
|
-
if (typeof data.vpnJoinBroker === "string")
|
|
138
|
-
config.vpnJoinBroker = data.vpnJoinBroker;
|
|
139
|
-
if (typeof data.registryBroker === "string")
|
|
140
|
-
config.registryBroker = data.registryBroker;
|
|
153
|
+
const config = withDiscoveredFields({ issuer, clientId }, data);
|
|
141
154
|
if (!opts.quiet) {
|
|
142
155
|
writeContextConfig(contextKey, config);
|
|
143
156
|
logSuccess(`Discovered platform configuration for ${contextKey}`);
|
|
@@ -145,6 +158,8 @@ async function discoverConfig(domain, contextKey = domain, opts = {}) {
|
|
|
145
158
|
return config;
|
|
146
159
|
}
|
|
147
160
|
catch (err) {
|
|
161
|
+
if (opts.silent)
|
|
162
|
+
return null;
|
|
148
163
|
const message = err instanceof Error ? err.message : String(err);
|
|
149
164
|
if (message.includes("abort")) {
|
|
150
165
|
logWarn("Discovery timed out");
|
|
@@ -260,7 +275,7 @@ function login(context, config) {
|
|
|
260
275
|
authUrl.searchParams.set("client_id", config.clientId);
|
|
261
276
|
authUrl.searchParams.set("redirect_uri", REDIRECT_URI);
|
|
262
277
|
authUrl.searchParams.set("response_type", "code");
|
|
263
|
-
authUrl.searchParams.set("scope",
|
|
278
|
+
authUrl.searchParams.set("scope", loginScopes(config));
|
|
264
279
|
authUrl.searchParams.set("code_challenge", codeChallenge);
|
|
265
280
|
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
266
281
|
authUrl.searchParams.set("state", state);
|
|
@@ -385,7 +400,7 @@ async function attemptDeviceCode(context, config) {
|
|
|
385
400
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
386
401
|
body: new URLSearchParams({
|
|
387
402
|
client_id: config.clientId,
|
|
388
|
-
scope:
|
|
403
|
+
scope: loginScopes(config),
|
|
389
404
|
}).toString(),
|
|
390
405
|
});
|
|
391
406
|
if (!resp.ok) {
|
|
@@ -606,9 +621,11 @@ async function showStatus(context, config) {
|
|
|
606
621
|
return;
|
|
607
622
|
}
|
|
608
623
|
let tenants = [];
|
|
624
|
+
let audience = [];
|
|
609
625
|
try {
|
|
610
626
|
const accessPayload = decodeJwtPayload(creds.accessToken);
|
|
611
627
|
tenants = accessPayload["urn:mesh:tenants"] ?? [];
|
|
628
|
+
audience = tokenAudience(accessPayload);
|
|
612
629
|
}
|
|
613
630
|
catch {
|
|
614
631
|
}
|
|
@@ -618,6 +635,26 @@ async function showStatus(context, config) {
|
|
|
618
635
|
if (tenants.length > 0) {
|
|
619
636
|
logInfo(`Deployable tenants: ${tenants.join(", ")}`);
|
|
620
637
|
}
|
|
638
|
+
if (audience.length > 0) {
|
|
639
|
+
logInfo(`Audience: ${audience.join(", ")}`);
|
|
640
|
+
if (config.hubProjectId && !audience.includes(config.hubProjectId)) {
|
|
641
|
+
logWarn(`This session does not carry the Hub project audience (${config.hubProjectId}); ` +
|
|
642
|
+
`a Hub that enforces its audience will refuse it. Re-run: mesh login ${context}`);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
if (!config.hubProjectId) {
|
|
646
|
+
logInfo(`Hub project: not in this context's discovery record, so this session was requested without ` +
|
|
647
|
+
`a Hub audience — a Hub that enforces its audience would refuse it. ` +
|
|
648
|
+
`\`mesh login ${context}\` re-checks what the platform publishes.`);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
export function tokenAudience(payload) {
|
|
652
|
+
const aud = payload.aud;
|
|
653
|
+
if (typeof aud === "string")
|
|
654
|
+
return [aud];
|
|
655
|
+
if (Array.isArray(aud))
|
|
656
|
+
return aud.filter((a) => typeof a === "string");
|
|
657
|
+
return [];
|
|
621
658
|
}
|
|
622
659
|
export function registerLoginCommand(program) {
|
|
623
660
|
program
|
|
@@ -657,6 +694,7 @@ export function registerLoginCommand(program) {
|
|
|
657
694
|
if (opts.registry === false) {
|
|
658
695
|
logWarn(NO_REGISTRY_DEPRECATED);
|
|
659
696
|
}
|
|
697
|
+
config = await adoptPublishedHubProject(context, config);
|
|
660
698
|
try {
|
|
661
699
|
await runLoginFlow(context, config, opts);
|
|
662
700
|
}
|
|
@@ -872,8 +910,9 @@ async function ensureValidToken(context, config, opts) {
|
|
|
872
910
|
if (token)
|
|
873
911
|
return token;
|
|
874
912
|
logInfo(`No valid Zitadel session for ${context} — running login flow`);
|
|
913
|
+
const adopted = await adoptPublishedHubProject(context, config);
|
|
875
914
|
try {
|
|
876
|
-
await runLoginFlow(context,
|
|
915
|
+
await runLoginFlow(context, adopted, opts);
|
|
877
916
|
}
|
|
878
917
|
catch (err) {
|
|
879
918
|
logError(`Login failed: ${err.message}`);
|
|
@@ -897,23 +936,36 @@ export async function ensureLogin(context, opts = {}) {
|
|
|
897
936
|
return null;
|
|
898
937
|
return readCredentials(context);
|
|
899
938
|
}
|
|
900
|
-
async function discoverConfigGuarded(context) {
|
|
939
|
+
async function discoverConfigGuarded(context, opts = {}) {
|
|
901
940
|
let config = null;
|
|
902
941
|
if (parseTenantEnv(context)) {
|
|
903
|
-
config = await discoverConfigFromSsm(context);
|
|
942
|
+
config = await discoverConfigFromSsm(context, opts);
|
|
904
943
|
}
|
|
905
944
|
if (!config) {
|
|
906
945
|
const pinned = firstPartyDomainFor(context);
|
|
907
946
|
if (pinned) {
|
|
908
|
-
|
|
909
|
-
|
|
947
|
+
if (!opts.quiet) {
|
|
948
|
+
logInfo(`"${context}" is a known Mesh platform — resolving via ${pinned} (no AWS needed)`);
|
|
949
|
+
}
|
|
950
|
+
config = await discoverConfig(pinned, context, opts);
|
|
910
951
|
}
|
|
911
952
|
}
|
|
912
953
|
if (!config && context.split(".").length - 1 >= 2) {
|
|
913
|
-
config = await discoverConfig(context);
|
|
954
|
+
config = await discoverConfig(context, context, opts);
|
|
914
955
|
}
|
|
915
956
|
return config;
|
|
916
957
|
}
|
|
958
|
+
export async function adoptPublishedHubProject(context, config, discover = discoverConfigGuarded, write = writeContextConfig) {
|
|
959
|
+
if (config.hubProjectId)
|
|
960
|
+
return config;
|
|
961
|
+
const fresh = await discover(context, { quiet: true, silent: true });
|
|
962
|
+
if (!fresh?.hubProjectId)
|
|
963
|
+
return config;
|
|
964
|
+
const merged = { ...config, hubProjectId: fresh.hubProjectId };
|
|
965
|
+
write(context, merged);
|
|
966
|
+
logInfo(`${context} publishes its Hub project (${fresh.hubProjectId}) — requesting the Hub audience for this login`);
|
|
967
|
+
return merged;
|
|
968
|
+
}
|
|
917
969
|
async function exportAwsCredentials(context, config, opts) {
|
|
918
970
|
const token = await ensureValidToken(context, config, opts);
|
|
919
971
|
if (!token) {
|
|
@@ -816,6 +816,17 @@ import * as crypto from "crypto";
|
|
|
816
816
|
import * as fs2 from "fs";
|
|
817
817
|
import * as path2 from "path";
|
|
818
818
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
819
|
+
function loginScopes(config) {
|
|
820
|
+
return config.hubProjectId ? `${BASE_SCOPES} urn:zitadel:iam:org:project:id:${config.hubProjectId}:aud` : BASE_SCOPES;
|
|
821
|
+
}
|
|
822
|
+
function withDiscoveredFields(config, data) {
|
|
823
|
+
const out = { ...config };
|
|
824
|
+
for (const key of ["vpn", "vpnJoinBroker", "registryBroker", "hubProjectId"]) {
|
|
825
|
+
const value = data[key];
|
|
826
|
+
if (typeof value === "string" && value) out[key] = value;
|
|
827
|
+
}
|
|
828
|
+
return out;
|
|
829
|
+
}
|
|
819
830
|
function readConfig() {
|
|
820
831
|
if (!fs2.existsSync(CONFIG_FILE)) return {};
|
|
821
832
|
try {
|
|
@@ -839,12 +850,12 @@ function parseTenantEnv(context) {
|
|
|
839
850
|
if (parts.length !== 2 || !parts[0] || !parts[1]) return null;
|
|
840
851
|
return { tenant: parts[0], env: parts[1] };
|
|
841
852
|
}
|
|
842
|
-
async function discoverConfigFromSsm(context) {
|
|
853
|
+
async function discoverConfigFromSsm(context, opts = {}) {
|
|
843
854
|
const parsed = parseTenantEnv(context);
|
|
844
855
|
if (!parsed) return null;
|
|
845
856
|
const { tenant, env } = parsed;
|
|
846
857
|
const ssmPath = `/mesh-platform/${tenant}/${env}/platform/zitadel`;
|
|
847
|
-
logInfo(`Attempting SSM discovery from ${ssmPath}...`);
|
|
858
|
+
if (!opts.quiet) logInfo(`Attempting SSM discovery from ${ssmPath}...`);
|
|
848
859
|
try {
|
|
849
860
|
const { SSMClient: SSMClient2, GetParameterCommand: GetParameterCommand2 } = await import("@aws-sdk/client-ssm");
|
|
850
861
|
const region = process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION ?? "us-east-2";
|
|
@@ -852,30 +863,31 @@ async function discoverConfigFromSsm(context) {
|
|
|
852
863
|
const resp = await ssm.send(new GetParameterCommand2({ Name: ssmPath }));
|
|
853
864
|
const raw = resp.Parameter?.Value;
|
|
854
865
|
if (!raw) {
|
|
855
|
-
logWarn(`SSM parameter ${ssmPath} has no value`);
|
|
866
|
+
if (!opts.silent) logWarn(`SSM parameter ${ssmPath} has no value`);
|
|
856
867
|
return null;
|
|
857
868
|
}
|
|
858
869
|
const data = JSON.parse(raw);
|
|
859
870
|
const issuer = data.endpoint;
|
|
860
871
|
const clientId = data.cliClientId;
|
|
861
872
|
if (typeof issuer !== "string" || !issuer) {
|
|
862
|
-
logWarn("SSM zitadel entry missing 'endpoint' field");
|
|
873
|
+
if (!opts.silent) logWarn("SSM zitadel entry missing 'endpoint' field");
|
|
863
874
|
return null;
|
|
864
875
|
}
|
|
865
876
|
if (typeof clientId !== "string" || !clientId) {
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
877
|
+
if (!opts.silent) {
|
|
878
|
+
logWarn(
|
|
879
|
+
"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."
|
|
880
|
+
);
|
|
881
|
+
}
|
|
869
882
|
return null;
|
|
870
883
|
}
|
|
871
|
-
const config = { issuer, clientId };
|
|
872
|
-
if (
|
|
873
|
-
if (typeof data.vpnJoinBroker === "string") config.vpnJoinBroker = data.vpnJoinBroker;
|
|
874
|
-
if (typeof data.registryBroker === "string") config.registryBroker = data.registryBroker;
|
|
884
|
+
const config = withDiscoveredFields({ issuer, clientId }, data);
|
|
885
|
+
if (opts.quiet) return config;
|
|
875
886
|
writeContextConfig(context, config);
|
|
876
887
|
logSuccess(`Discovered platform configuration for ${context} via SSM`);
|
|
877
888
|
return config;
|
|
878
889
|
} catch (err) {
|
|
890
|
+
if (opts.silent) return null;
|
|
879
891
|
const message = err instanceof Error ? err.message : String(err);
|
|
880
892
|
if (message.includes("ExpiredToken") || message.includes("credentials")) {
|
|
881
893
|
logWarn(`SSM discovery failed: AWS credentials expired or unavailable.`);
|
|
@@ -903,26 +915,24 @@ async function discoverConfig(domain, contextKey = domain, opts = {}) {
|
|
|
903
915
|
const resp = await fetch(url, { signal: controller.signal });
|
|
904
916
|
clearTimeout(timeout);
|
|
905
917
|
if (!resp.ok) {
|
|
906
|
-
logWarn(`Discovery endpoint returned ${resp.status}`);
|
|
918
|
+
if (!opts.silent) logWarn(`Discovery endpoint returned ${resp.status}`);
|
|
907
919
|
return null;
|
|
908
920
|
}
|
|
909
921
|
const data = await resp.json();
|
|
910
922
|
const issuer = data.issuer;
|
|
911
923
|
const clientId = data.clientId;
|
|
912
924
|
if (typeof issuer !== "string" || !issuer || typeof clientId !== "string" || !clientId) {
|
|
913
|
-
logWarn("Discovery endpoint returned invalid config (missing issuer or clientId)");
|
|
925
|
+
if (!opts.silent) logWarn("Discovery endpoint returned invalid config (missing issuer or clientId)");
|
|
914
926
|
return null;
|
|
915
927
|
}
|
|
916
|
-
const config = { issuer, clientId };
|
|
917
|
-
if (typeof data.vpn === "string") config.vpn = data.vpn;
|
|
918
|
-
if (typeof data.vpnJoinBroker === "string") config.vpnJoinBroker = data.vpnJoinBroker;
|
|
919
|
-
if (typeof data.registryBroker === "string") config.registryBroker = data.registryBroker;
|
|
928
|
+
const config = withDiscoveredFields({ issuer, clientId }, data);
|
|
920
929
|
if (!opts.quiet) {
|
|
921
930
|
writeContextConfig(contextKey, config);
|
|
922
931
|
logSuccess(`Discovered platform configuration for ${contextKey}`);
|
|
923
932
|
}
|
|
924
933
|
return config;
|
|
925
934
|
} catch (err) {
|
|
935
|
+
if (opts.silent) return null;
|
|
926
936
|
const message = err instanceof Error ? err.message : String(err);
|
|
927
937
|
if (message.includes("abort")) {
|
|
928
938
|
logWarn("Discovery timed out");
|
|
@@ -1020,7 +1030,7 @@ function login(context, config) {
|
|
|
1020
1030
|
authUrl.searchParams.set("client_id", config.clientId);
|
|
1021
1031
|
authUrl.searchParams.set("redirect_uri", REDIRECT_URI);
|
|
1022
1032
|
authUrl.searchParams.set("response_type", "code");
|
|
1023
|
-
authUrl.searchParams.set("scope",
|
|
1033
|
+
authUrl.searchParams.set("scope", loginScopes(config));
|
|
1024
1034
|
authUrl.searchParams.set("code_challenge", codeChallenge);
|
|
1025
1035
|
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
1026
1036
|
authUrl.searchParams.set("state", state);
|
|
@@ -1148,7 +1158,7 @@ async function attemptDeviceCode(context, config) {
|
|
|
1148
1158
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1149
1159
|
body: new URLSearchParams({
|
|
1150
1160
|
client_id: config.clientId,
|
|
1151
|
-
scope:
|
|
1161
|
+
scope: loginScopes(config)
|
|
1152
1162
|
}).toString()
|
|
1153
1163
|
});
|
|
1154
1164
|
if (!resp.ok) {
|
|
@@ -1309,8 +1319,9 @@ async function ensureValidToken(context, config, opts) {
|
|
|
1309
1319
|
let token = await getValidToken(context);
|
|
1310
1320
|
if (token) return token;
|
|
1311
1321
|
logInfo(`No valid Zitadel session for ${context} \u2014 running login flow`);
|
|
1322
|
+
const adopted = await adoptPublishedHubProject(context, config);
|
|
1312
1323
|
try {
|
|
1313
|
-
await runLoginFlow(context,
|
|
1324
|
+
await runLoginFlow(context, adopted, opts);
|
|
1314
1325
|
} catch (err) {
|
|
1315
1326
|
logError(`Login failed: ${err.message}`);
|
|
1316
1327
|
return null;
|
|
@@ -1330,24 +1341,35 @@ async function ensureLogin(context, opts = {}) {
|
|
|
1330
1341
|
if (!token) return null;
|
|
1331
1342
|
return readCredentials(context);
|
|
1332
1343
|
}
|
|
1333
|
-
async function discoverConfigGuarded(context) {
|
|
1344
|
+
async function discoverConfigGuarded(context, opts = {}) {
|
|
1334
1345
|
let config = null;
|
|
1335
1346
|
if (parseTenantEnv(context)) {
|
|
1336
|
-
config = await discoverConfigFromSsm(context);
|
|
1347
|
+
config = await discoverConfigFromSsm(context, opts);
|
|
1337
1348
|
}
|
|
1338
1349
|
if (!config) {
|
|
1339
1350
|
const pinned = firstPartyDomainFor(context);
|
|
1340
1351
|
if (pinned) {
|
|
1341
|
-
|
|
1342
|
-
|
|
1352
|
+
if (!opts.quiet) {
|
|
1353
|
+
logInfo(`"${context}" is a known Mesh platform \u2014 resolving via ${pinned} (no AWS needed)`);
|
|
1354
|
+
}
|
|
1355
|
+
config = await discoverConfig(pinned, context, opts);
|
|
1343
1356
|
}
|
|
1344
1357
|
}
|
|
1345
1358
|
if (!config && context.split(".").length - 1 >= 2) {
|
|
1346
|
-
config = await discoverConfig(context);
|
|
1359
|
+
config = await discoverConfig(context, context, opts);
|
|
1347
1360
|
}
|
|
1348
1361
|
return config;
|
|
1349
1362
|
}
|
|
1350
|
-
|
|
1363
|
+
async function adoptPublishedHubProject(context, config, discover = discoverConfigGuarded, write = writeContextConfig) {
|
|
1364
|
+
if (config.hubProjectId) return config;
|
|
1365
|
+
const fresh = await discover(context, { quiet: true, silent: true });
|
|
1366
|
+
if (!fresh?.hubProjectId) return config;
|
|
1367
|
+
const merged = { ...config, hubProjectId: fresh.hubProjectId };
|
|
1368
|
+
write(context, merged);
|
|
1369
|
+
logInfo(`${context} publishes its Hub project (${fresh.hubProjectId}) \u2014 requesting the Hub audience for this login`);
|
|
1370
|
+
return merged;
|
|
1371
|
+
}
|
|
1372
|
+
var CONFIG_DIR, CONFIG_FILE, CREDENTIALS_FILE, REDIRECT_PORT, REDIRECT_URI, BASE_SCOPES, atomicWriteCounter, MAX_DEVICE_CODES, RESERVED_REGISTRY_CONTEXT;
|
|
1351
1373
|
var init_login = __esm({
|
|
1352
1374
|
"libs/mesh-cli/src/commands/login.ts"() {
|
|
1353
1375
|
"use strict";
|
|
@@ -1364,7 +1386,7 @@ var init_login = __esm({
|
|
|
1364
1386
|
CREDENTIALS_FILE = path2.join(CONFIG_DIR, "credentials.json");
|
|
1365
1387
|
REDIRECT_PORT = 9876;
|
|
1366
1388
|
REDIRECT_URI = `http://localhost:${REDIRECT_PORT}/callback`;
|
|
1367
|
-
|
|
1389
|
+
BASE_SCOPES = "openid email profile offline_access urn:zitadel:iam:org:project:id:zitadel:aud";
|
|
1368
1390
|
atomicWriteCounter = 0;
|
|
1369
1391
|
MAX_DEVICE_CODES = 3;
|
|
1370
1392
|
RESERVED_REGISTRY_CONTEXT = "registry";
|