@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.
- package/README.md +74 -246
- package/dist/bin/mesh.js +2179 -1328
- package/dist/bin/mesh.js.map +3 -3
- package/dist/build-info.json +2 -2
- package/dist/src/commands/db/axes.d.ts +19 -0
- package/dist/src/commands/db/axes.js +35 -0
- package/dist/src/commands/db/connect.d.ts +2 -4
- package/dist/src/commands/db/connect.js +4 -19
- package/dist/src/commands/db/credentials.d.ts +2 -5
- package/dist/src/commands/db/credentials.js +7 -23
- package/dist/src/commands/db/env.d.ts +2 -5
- package/dist/src/commands/db/env.js +7 -23
- package/dist/src/commands/db/exec.js +7 -24
- package/dist/src/commands/db/psql.d.ts +2 -5
- package/dist/src/commands/db/psql.js +8 -23
- package/dist/src/commands/init/platform-setup.d.ts +68 -0
- package/dist/src/commands/init/platform-setup.js +203 -0
- package/dist/src/commands/init/wizard.d.ts +20 -0
- package/dist/src/commands/init/wizard.js +89 -7
- package/dist/src/commands/init.js +42 -36
- package/dist/src/commands/local/index.d.ts +5 -3
- package/dist/src/commands/local/index.js +61 -23
- package/dist/src/commands/local/mock-idps.d.ts +48 -0
- package/dist/src/commands/local/mock-idps.js +119 -0
- package/dist/src/commands/local/seed-zitadel.d.ts +6 -0
- package/dist/src/commands/local/seed-zitadel.js +76 -0
- package/dist/src/commands/local/stack.d.ts +11 -3
- package/dist/src/commands/local/stack.js +29 -15
- package/dist/src/commands/login.d.ts +4 -0
- package/dist/src/commands/login.js +16 -2
- package/dist/src/commands/registry.d.ts +2 -2
- package/dist/src/commands/registry.js +11 -6
- package/dist/src/commands/secrets/context.d.ts +5 -0
- package/dist/src/commands/secrets/context.js +5 -0
- package/dist/src/commands/secrets/index.js +3 -0
- package/dist/src/commands/secrets/migrate-config.d.ts +1 -0
- package/dist/src/commands/secrets/migrate-config.js +2 -3
- package/dist/src/commands/secrets/reindex.d.ts +1 -0
- package/dist/src/commands/secrets/reindex.js +2 -2
- package/dist/src/commands/secrets/set.d.ts +1 -0
- package/dist/src/commands/secrets/set.js +2 -2
- package/dist/src/commands/temporal.js +35 -8
- package/dist/src/commands/temporal.js.map +2 -2
- package/dist/src/commands/tunnel/index.d.ts +8 -0
- package/dist/src/commands/tunnel/index.js +3 -3
- package/dist/src/index.js +324 -62
- package/dist/src/index.js.map +3 -3
- package/dist/src/utils/context.d.ts +6 -1
- package/dist/src/utils/context.js +302 -39
- package/dist/src/utils/registry-identity.d.ts +5 -0
- package/dist/src/utils/registry-identity.js +16 -0
- package/fragments/agents/AGENTS.md.hbs +10 -2
- package/fragments/base/index.ts.hbs +15 -5
- package/package.json +2 -2
- package/skills/core/SKILL.md +74 -7
- package/stack/docker-compose.idps.yml +104 -0
- package/stack/idps/dex.yaml +53 -0
- package/templates/api-auth/index.ts.hbs +5 -4
- package/templates/api-role-gating/index.ts.hbs +5 -4
- package/templates/external-service/index.ts.hbs +5 -4
- package/templates/platform-repo/README.md.hbs +17 -3
- package/templates/platform-repo/core/Pulumi.{{tenant}}-{{env}}.yaml.hbs +7 -0
- 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
|
-
|
|
58
|
-
overlayStarted: boolean;
|
|
66
|
+
overlays: readonly OverlayState[];
|
|
59
67
|
}): ServiceStatus[];
|
|
60
|
-
export declare function
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
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
|
-
|
|
366
|
-
|
|
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
|
|
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("
|
|
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
|
|
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("
|
|
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 { 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,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 {
|
|
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 =
|
|
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 =
|
|
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 });
|
|
@@ -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 =
|
|
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 });
|