@mesh-tech/mesh-cli 0.26.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/dist/bin/mesh.js +1056 -778
- package/dist/bin/mesh.js.map +3 -3
- package/dist/build-info.json +2 -2
- 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/temporal.js +10 -0
- package/dist/src/commands/temporal.js.map +2 -2
- package/package.json +2 -2
- package/skills/core/SKILL.md +1 -1
- package/stack/docker-compose.idps.yml +104 -0
- package/stack/idps/dex.yaml +53 -0
package/dist/build-info.json
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import { type ServiceStatus, type StackEndpoint } from "./stack.js";
|
|
2
|
+
import { type OverlayState, type ServiceStatus, type StackEndpoint } from "./stack.js";
|
|
3
3
|
export interface WaitForStackIo {
|
|
4
4
|
probe: (endpoint: StackEndpoint) => Promise<boolean>;
|
|
5
5
|
services: () => ServiceStatus[];
|
|
6
|
-
|
|
6
|
+
overlays: () => readonly OverlayState[];
|
|
7
7
|
}
|
|
8
|
-
export declare function waitForStack(
|
|
8
|
+
export declare function waitForStack(opts: {
|
|
9
|
+
idpsStarted: boolean;
|
|
10
|
+
}, io?: WaitForStackIo): Promise<void>;
|
|
9
11
|
export declare function startHereLines(hubRunning: boolean, up?: ReadonlyMap<string, boolean>): string[];
|
|
10
12
|
export declare function registerLocalCommands(program: Command): void;
|
|
@@ -3,12 +3,13 @@ import * as path from "path";
|
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { logError, logInfo, logSuccess, logWarn } from "../../utils/log.js";
|
|
5
5
|
import { MeshCliError } from "../../utils/errors.js";
|
|
6
|
-
import { compose, COMPOSE_PROJECT, composeStreamed, crashedServices, DEFAULT_HUB_PORT, ensureDockerAvailable, findPackageRoot, hubApiRunning, hubEndpoints,
|
|
6
|
+
import { compose, COMPOSE_PROJECT, composeStreamed, crashedServices, DEFAULT_HUB_PORT, ensureDockerAvailable, findPackageRoot, hubApiRunning, hubEndpoints, hubOverlayServices, hubPort, idpsOverlayServices, ONE_SHOT_SERVICES, overlayRunning, probeEndpoint, STACK_ENDPOINTS, stackOwnedElsewhere, stackServices, } from "./stack.js";
|
|
7
7
|
import { probeRegistryToken, REGISTRY_LOGIN_FIX } from "../../utils/auth-preflight.js";
|
|
8
8
|
import { ensureHubAuthImage, ensureHubImages, buildHubImagesFromSource, readHubCompiledAuthz, hasRegistryAuth, localHubVersion, planWithHubRefresh, } from "./hub-local.js";
|
|
9
9
|
import { reconcileRegistryFromZitadel } from "./auth-provision.js";
|
|
10
10
|
import { LOCAL_AWS_CONFIG, localAwsEnv, seedLocalPlatform, verifyFabric } from "./seed.js";
|
|
11
|
-
import { LOGIN_CONTEXT, readHubAuth, seedZitadel, TEST_USERS } from "./seed-zitadel.js";
|
|
11
|
+
import { LOGIN_CONTEXT, readHubAuth, seedMockIdps, seedZitadel, TEST_USERS } from "./seed-zitadel.js";
|
|
12
|
+
import { idpEndpoints, mockIdpComposeEnv, MOCK_OIDC_PORT, MOCK_SAML_PORT } from "./mock-idps.js";
|
|
12
13
|
import { publishHubAuthzCatalog } from "./seed-hub-catalog.js";
|
|
13
14
|
import { describePortConflicts, findPortConflicts, hostPortsOf } from "./helpers.js";
|
|
14
15
|
const WAIT_TIMEOUT_MS = 180_000;
|
|
@@ -20,10 +21,13 @@ function crashError(crashed) {
|
|
|
20
21
|
},
|
|
21
22
|
});
|
|
22
23
|
}
|
|
23
|
-
export async function waitForStack(io = {
|
|
24
|
+
export async function waitForStack(opts, io = {
|
|
24
25
|
probe: probeEndpoint,
|
|
25
26
|
services: stackServices,
|
|
26
|
-
|
|
27
|
+
overlays: () => [
|
|
28
|
+
{ services: hubOverlayServices(), started: false },
|
|
29
|
+
{ services: idpsOverlayServices(), started: opts.idpsStarted },
|
|
30
|
+
],
|
|
27
31
|
}) {
|
|
28
32
|
const deadline = Date.now() + WAIT_TIMEOUT_MS;
|
|
29
33
|
let pending = STACK_ENDPOINTS.filter((e) => e.probe.kind !== "none");
|
|
@@ -39,10 +43,7 @@ export async function waitForStack(io = {
|
|
|
39
43
|
}
|
|
40
44
|
}
|
|
41
45
|
pending = stillDown;
|
|
42
|
-
const crashed = crashedServices(io.services(), {
|
|
43
|
-
overlayServices: io.overlayServices(),
|
|
44
|
-
overlayStarted: false,
|
|
45
|
-
});
|
|
46
|
+
const crashed = crashedServices(io.services(), { overlays: io.overlays() });
|
|
46
47
|
if (crashed.length > 0)
|
|
47
48
|
throw crashError(crashed);
|
|
48
49
|
if (pending.length === 0)
|
|
@@ -112,6 +113,11 @@ function portMoveHints() {
|
|
|
112
113
|
Number(hubPort()),
|
|
113
114
|
"MESH_HUB_PORT=<free port> mesh start (or mesh start --no-hub, which starts everything else)",
|
|
114
115
|
],
|
|
116
|
+
[MOCK_SAML_PORT, "mesh start (without --with-idps — the mock identity providers are opt-in)"],
|
|
117
|
+
[
|
|
118
|
+
MOCK_OIDC_PORT,
|
|
119
|
+
"mesh start (without --with-idps; 5556 is Dex's own default port, so a Dex of your own is the usual holder)",
|
|
120
|
+
],
|
|
115
121
|
]);
|
|
116
122
|
}
|
|
117
123
|
function repoRootForHubSource() {
|
|
@@ -133,6 +139,7 @@ export function registerLocalCommands(program) {
|
|
|
133
139
|
.option("--no-hub", "start without the Hub (API + UI)")
|
|
134
140
|
.option("--with-hub", "force-refresh the Hub to the latest published @mesh-tech/hub (the Hub is included by default when its images exist or registry auth is available)")
|
|
135
141
|
.option("--hub-from-source", "build the Hub images from THIS checkout's apps/hub instead of the published tarball (the only way to exercise a Hub change locally)")
|
|
142
|
+
.option("--with-idps", "also run mock SAML and OIDC identity providers, and federate the local Zitadel with both (for testing enterprise SSO without a cloud tenant or an IdP admin)", false)
|
|
136
143
|
.option("--takeover", "re-own a stack that another checkout started (may recreate shared containers with THIS checkout's config)", false)
|
|
137
144
|
.option("--skip-port-check", "start even though a host port the stack wants is already held (the service that loses the bind will be broken — use when the holder is something you cannot stop)", false)
|
|
138
145
|
.action(async (opts) => {
|
|
@@ -151,7 +158,11 @@ export function registerLocalCommands(program) {
|
|
|
151
158
|
logWarn(`Taking over the running stack (previously driven from ${foreign}).`);
|
|
152
159
|
}
|
|
153
160
|
if (!opts.skipPortCheck && !stackServices().some((s) => s.state === "running")) {
|
|
154
|
-
const ports = hostPortsOf([
|
|
161
|
+
const ports = hostPortsOf([
|
|
162
|
+
...STACK_ENDPOINTS,
|
|
163
|
+
...(opts.hub ? hubEndpoints() : []),
|
|
164
|
+
...(opts.withIdps ? idpEndpoints() : []),
|
|
165
|
+
]);
|
|
155
166
|
const conflicts = await findPortConflicts(ports, `${COMPOSE_PROJECT}-`);
|
|
156
167
|
if (conflicts.length > 0) {
|
|
157
168
|
throw new MeshCliError(describePortConflicts(conflicts, portMoveHints()), {
|
|
@@ -197,9 +208,10 @@ export function registerLocalCommands(program) {
|
|
|
197
208
|
}
|
|
198
209
|
}
|
|
199
210
|
logInfo("Starting the local Mesh platform (docker compose project: mesh-local)…");
|
|
200
|
-
|
|
211
|
+
const idpEnv = opts.withIdps ? mockIdpComposeEnv() : undefined;
|
|
212
|
+
await composeStreamed(["up", "-d"], { idps: opts.withIdps, env: idpEnv });
|
|
201
213
|
compose(["restart", "prometheus", "blackbox-exporter"], { inherit: true });
|
|
202
|
-
await waitForStack();
|
|
214
|
+
await waitForStack({ idpsStarted: opts.withIdps });
|
|
203
215
|
let hubAuth = readHubAuth();
|
|
204
216
|
if (opts.seed) {
|
|
205
217
|
logInfo("Seeding local platform (tenant registry, artifacts bucket)…");
|
|
@@ -207,6 +219,15 @@ export function registerLocalCommands(program) {
|
|
|
207
219
|
logInfo("Seeding Zitadel (Platform project, Mesh CLI app, Hub auth, test users)…");
|
|
208
220
|
const zitadel = await seedZitadel(LOCAL_AWS_CONFIG);
|
|
209
221
|
hubAuth = zitadel.hubAuth;
|
|
222
|
+
if (opts.withIdps) {
|
|
223
|
+
logInfo("Federating local Zitadel with the mock SAML and OIDC identity providers…");
|
|
224
|
+
await seedMockIdps();
|
|
225
|
+
logInfo(`Mock SAML IdP: http://localhost:${MOCK_SAML_PORT} · ` +
|
|
226
|
+
`mock OIDC IdP: http://localhost:${MOCK_OIDC_PORT}/dex · ` +
|
|
227
|
+
"sign in at http://localhost:8080 and both federation buttons are offered");
|
|
228
|
+
logInfo("Both are listed in the Zitadel console under Settings → Identity Providers (owner: Instance): " +
|
|
229
|
+
"http://localhost:8080/ui/console/org-settings?id=idp");
|
|
230
|
+
}
|
|
210
231
|
if (hubVersion) {
|
|
211
232
|
const compiled = readHubCompiledAuthz(hubVersion);
|
|
212
233
|
if (compiled) {
|
|
@@ -246,19 +267,23 @@ export function registerLocalCommands(program) {
|
|
|
246
267
|
hubEnv.MESH_HUB_OAUTH2_CLIENT_SECRET = hubAuth.clientSecret;
|
|
247
268
|
hubEnv.MESH_HUB_OAUTH2_COOKIE_SECRET = hubAuth.cookieSecret;
|
|
248
269
|
}
|
|
249
|
-
await composeStreamed(["up", "-d"], {
|
|
270
|
+
await composeStreamed(["up", "-d"], {
|
|
271
|
+
hub: true,
|
|
272
|
+
idps: opts.withIdps,
|
|
273
|
+
env: { ...idpEnv, ...hubEnv },
|
|
274
|
+
});
|
|
250
275
|
if (opts.seed) {
|
|
251
276
|
compose(["restart", "hub-api"], { hub: true, env: hubEnv });
|
|
252
277
|
}
|
|
253
|
-
const
|
|
278
|
+
const overlays = [
|
|
279
|
+
{ services: hubOverlayServices(), started: true },
|
|
280
|
+
{ services: idpsOverlayServices(), started: opts.withIdps },
|
|
281
|
+
];
|
|
254
282
|
for (const endpoint of hubEndpoints()) {
|
|
255
283
|
logInfo(`Waiting for ${endpoint.label}…`);
|
|
256
284
|
let up = await probeEndpoint(endpoint);
|
|
257
285
|
for (let i = 0; i < 60 && !up; i++) {
|
|
258
|
-
const crashed = crashedServices(stackServices(), {
|
|
259
|
-
overlayServices,
|
|
260
|
-
overlayStarted: true,
|
|
261
|
-
});
|
|
286
|
+
const crashed = crashedServices(stackServices(), { overlays });
|
|
262
287
|
if (crashed.length > 0)
|
|
263
288
|
throw crashError(crashed);
|
|
264
289
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
@@ -310,11 +335,20 @@ export function registerLocalCommands(program) {
|
|
|
310
335
|
remediation: { command: "mesh start" },
|
|
311
336
|
});
|
|
312
337
|
}
|
|
313
|
-
const
|
|
314
|
-
const
|
|
315
|
-
const
|
|
338
|
+
const hubOverlay = hubOverlayServices();
|
|
339
|
+
const idpsOverlay = idpsOverlayServices();
|
|
340
|
+
const idpsStarted = overlayRunning(services, idpsOverlay);
|
|
341
|
+
const overlays = [
|
|
342
|
+
{ services: hubOverlay, started: overlayRunning(services, hubOverlay) },
|
|
343
|
+
{ services: idpsOverlay, started: idpsStarted },
|
|
344
|
+
];
|
|
345
|
+
const notStarted = (s) => s.state === "exited" && overlays.some((o) => !o.started && o.services.has(s.name));
|
|
316
346
|
const hasHub = hubApiRunning(services);
|
|
317
|
-
const endpoints =
|
|
347
|
+
const endpoints = [
|
|
348
|
+
...STACK_ENDPOINTS,
|
|
349
|
+
...(hasHub ? hubEndpoints() : []),
|
|
350
|
+
...(idpsStarted ? idpEndpoints() : []),
|
|
351
|
+
];
|
|
318
352
|
const probes = await Promise.all(endpoints.map(async (endpoint) => ({
|
|
319
353
|
service: endpoint.service,
|
|
320
354
|
label: endpoint.label,
|
|
@@ -337,6 +371,7 @@ export function registerLocalCommands(program) {
|
|
|
337
371
|
services,
|
|
338
372
|
endpoints: probes,
|
|
339
373
|
fabric,
|
|
374
|
+
mockIdps: idpsStarted,
|
|
340
375
|
awsEnv: localAwsEnv(),
|
|
341
376
|
}));
|
|
342
377
|
if (fabric && !fabric.ok)
|
|
@@ -352,7 +387,7 @@ export function registerLocalCommands(program) {
|
|
|
352
387
|
const idle = notStarted(service);
|
|
353
388
|
const icon = ok || oneShotDone ? chalk.green("●") : idle ? chalk.dim("○") : chalk.red("●");
|
|
354
389
|
const health = service.health ? ` (${service.health})` : "";
|
|
355
|
-
const note = idle ? chalk.dim(" (
|
|
390
|
+
const note = idle ? chalk.dim(" (not started this run)") : "";
|
|
356
391
|
console.log(` ${icon} ${service.name.padEnd(22)} ${service.state}${health}${note}`);
|
|
357
392
|
}
|
|
358
393
|
console.log("");
|
|
@@ -366,8 +401,11 @@ export function registerLocalCommands(program) {
|
|
|
366
401
|
const detail = fabric.ok ? "8 KB Advanced-tier round-trip OK" : fabric.error;
|
|
367
402
|
console.log(` ${icon} ${"AWS fabric check".padEnd(34)} ${chalk.dim(detail ?? "")}`);
|
|
368
403
|
}
|
|
404
|
+
if (!idpsStarted) {
|
|
405
|
+
console.log(` ${chalk.dim("○")} ${"Mock IdPs (SAML + OIDC)".padEnd(34)} ${chalk.dim("not started — mesh start --with-idps")}`);
|
|
406
|
+
}
|
|
369
407
|
const down = probes.filter((p) => !p.up);
|
|
370
|
-
const crashed = crashedServices(services, {
|
|
408
|
+
const crashed = crashedServices(services, { overlays });
|
|
371
409
|
if (fabric && !fabric.ok)
|
|
372
410
|
process.exitCode = 1;
|
|
373
411
|
if (down.length > 0 || crashed.length > 0) {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { StackEndpoint } from "./stack.js";
|
|
2
|
+
export declare const MOCK_SAML_PORT = 4569;
|
|
3
|
+
export declare const MOCK_OIDC_PORT = 5556;
|
|
4
|
+
export declare const idpEndpoints: () => StackEndpoint[];
|
|
5
|
+
export declare const MOCK_SAML_IDP_NAME = "Mock SAML (local)";
|
|
6
|
+
export declare const MOCK_OIDC_IDP_NAME = "Mock OIDC (local)";
|
|
7
|
+
export declare const mockSamlMetadataUrl: () => string;
|
|
8
|
+
export declare const mockOidcEndpoints: () => {
|
|
9
|
+
authorization: string;
|
|
10
|
+
token: string;
|
|
11
|
+
user: string;
|
|
12
|
+
};
|
|
13
|
+
export declare function ensureMockSamlSigningKey(): {
|
|
14
|
+
publicKeyB64: string;
|
|
15
|
+
privateKeyB64: string;
|
|
16
|
+
};
|
|
17
|
+
export declare const mockIdpComposeEnv: () => Record<string, string>;
|
|
18
|
+
export declare const buildMockSamlIdpBody: (metadataXml: string) => {
|
|
19
|
+
name: string;
|
|
20
|
+
metadataXml: string;
|
|
21
|
+
binding: string;
|
|
22
|
+
withSignedRequest: boolean;
|
|
23
|
+
providerOptions: {
|
|
24
|
+
isLinkingAllowed: boolean;
|
|
25
|
+
isCreationAllowed: boolean;
|
|
26
|
+
isAutoCreation: boolean;
|
|
27
|
+
isAutoUpdate: boolean;
|
|
28
|
+
autoLinking: string;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
export declare const buildMockOauthIdpBody: () => {
|
|
32
|
+
name: string;
|
|
33
|
+
clientId: string;
|
|
34
|
+
clientSecret: string;
|
|
35
|
+
authorizationEndpoint: string;
|
|
36
|
+
tokenEndpoint: string;
|
|
37
|
+
userEndpoint: string;
|
|
38
|
+
scopes: string[];
|
|
39
|
+
idAttribute: string;
|
|
40
|
+
providerOptions: {
|
|
41
|
+
isLinkingAllowed: boolean;
|
|
42
|
+
isCreationAllowed: boolean;
|
|
43
|
+
isAutoCreation: boolean;
|
|
44
|
+
isAutoUpdate: boolean;
|
|
45
|
+
autoLinking: string;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
export declare const missingLoginPolicyIdps: (attached: readonly string[], wanted: readonly string[]) => string[];
|
|
@@ -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) {
|
|
@@ -604,6 +604,15 @@ var init_src = __esm({
|
|
|
604
604
|
}
|
|
605
605
|
});
|
|
606
606
|
|
|
607
|
+
// libs/mesh-cli/src/commands/local/mock-idps.ts
|
|
608
|
+
var init_mock_idps = __esm({
|
|
609
|
+
"libs/mesh-cli/src/commands/local/mock-idps.ts"() {
|
|
610
|
+
"use strict";
|
|
611
|
+
init_cache_home();
|
|
612
|
+
init_errors();
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
|
|
607
616
|
// libs/mesh-cli/src/commands/local/seed-zitadel.ts
|
|
608
617
|
var HUB_DEFAULT_REDIRECT_URI, HUB_ROLES, ZITADEL_SSM_PARAM, TEST_USERS_SSM_PREFIX;
|
|
609
618
|
var init_seed_zitadel = __esm({
|
|
@@ -616,6 +625,7 @@ var init_seed_zitadel = __esm({
|
|
|
616
625
|
init_stack();
|
|
617
626
|
init_seed();
|
|
618
627
|
init_cache_home();
|
|
628
|
+
init_mock_idps();
|
|
619
629
|
HUB_DEFAULT_REDIRECT_URI = `http://localhost:${DEFAULT_HUB_PORT}/oauth2/callback`;
|
|
620
630
|
HUB_ROLES = hubOperatorRoleKeys([LOCAL_TENANT]);
|
|
621
631
|
ZITADEL_SSM_PARAM = `/mesh-platform/${LOCAL_TENANT}/${LOCAL_ENV}/platform/zitadel`;
|