@mesh-tech/mesh-cli 0.31.1 → 0.33.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.
@@ -1,4 +1,4 @@
1
1
  {
2
- "commit": "3c02d5aa7",
3
- "builtAt": "2026-09-22T17:09:01.518Z"
2
+ "commit": "b286e1b11",
3
+ "builtAt": "2026-09-22T20:38:52.960Z"
4
4
  }
@@ -1,7 +1,7 @@
1
1
  import { Command } from "commander";
2
2
  import { type ProbeResult } from "./app-check-resolve.js";
3
3
  import { jsonReport, type Check, type CheckResult, type CheckStatus } from "./dev-doctor.js";
4
- export type AppCheckCode = "UNRESOLVABLE_IMPORT" | "UI_IN_API" | "NO_UI_SERVICE" | "NO_REGISTER" | "CUSTOM_SESSION_AUTH" | "NON_PLATFORM_LOGGER" | "LOCAL_AUTH_LIB" | "NO_AUTHZ_POINTER" | "METADATA_UNPUBLISHED" | "IAC_GRANTS" | "POINTER_PROJECT_OVERRIDE" | "PASSWORD_STORE" | "EMAIL_ALLOWLIST" | "USERS_TABLE" | "DOCS_SITE_PUBLIC" | "DOCS_AUDIENCE_IMPLICIT" | "DOCS_AUDIENCE_UNGRANTABLE" | "DOCS_VENDOR_AUDIENCE_PARTNER";
4
+ export type AppCheckCode = "UNRESOLVABLE_IMPORT" | "UI_IN_API" | "NO_UI_SERVICE" | "NO_REGISTER" | "CUSTOM_SESSION_AUTH" | "NON_PLATFORM_LOGGER" | "LOCAL_AUTH_LIB" | "NO_AUTHZ_POINTER" | "METADATA_UNPUBLISHED" | "IAC_GRANTS" | "POINTER_PROJECT_OVERRIDE" | "PASSWORD_STORE" | "EMAIL_ALLOWLIST" | "USERS_TABLE" | "DOCS_SITE_PUBLIC" | "DOCS_AUDIENCE_IMPLICIT" | "DOCS_AUDIENCE_UNGRANTABLE" | "DOCS_VENDOR_AUDIENCE_PARTNER" | "EVENTS_ROLES_UNDECLARED";
5
5
  export interface AppCheckRule {
6
6
  gate: string;
7
7
  title: string;
@@ -35,6 +35,11 @@ export declare function docsSiteFindings(text: string): Array<{
35
35
  why: string;
36
36
  }>;
37
37
  export declare function statedKind(decl: Pick<SurfaceDeclaration, "ctor">, args: Map<string, string>): string | undefined;
38
+ export declare function eventsRoleFindings(text: string): Array<{
39
+ code: AppCheckCode;
40
+ line: number;
41
+ why: string;
42
+ }>;
38
43
  export declare function listApps(root: string): string[];
39
44
  export declare function probeResolution(root: string, appRel: string): ProbeResult;
40
45
  export declare function scanApp(root: string, appRel: string, resolution?: ProbeResult): Finding[];
@@ -127,6 +127,13 @@ export const APP_CHECK_RULES = {
127
127
  remediation: 'declare every audience role on the ZitadelAppIdentity (roles: ["docs"]) so the Hub can grant it from the app\'s Access tab — the audience is published on the surface\'s registry entry as the reader set for the Hub to admit by, and a role nobody can be granted admits nobody',
128
128
  scope: "app",
129
129
  },
130
+ EVENTS_ROLES_UNDECLARED: {
131
+ gate: "H",
132
+ title: "Events roles grantable from the Hub",
133
+ severity: "block",
134
+ remediation: "spread mesh.apps.EVENTS_PROJECT_ROLES into the ZitadelAppIdentity's roles (roles: [...schemaDef.coarseRoles, ...mesh.apps.EVENTS_PROJECT_ROLES]) so events:subscriber and events:admin are project roles the Hub's Access tab can grant — the primitive cannot add them, because the identity builds its roles before the events service exists and a role added afterwards is absent from the coarseRoles the Hub reads",
135
+ scope: "app",
136
+ },
130
137
  DOCS_VENDOR_AUDIENCE_PARTNER: {
131
138
  gate: "H",
132
139
  title: "Vendor reference never partner-readable",
@@ -257,7 +264,7 @@ const RE_HTML = /from ["']hono\/(html|jsx)["']|react-dom\/server|\bc\.html\(|<!d
257
264
  const RE_OIDC = /openid-client|code_verifier|authorization_code|buildAuthorizeUrl|end_session/;
258
265
  const RE_COOKIE = /setCookie\(|hono\/cookie|Set-Cookie/;
259
266
  const RE_LOGGER = /from ["'](pino|hono\/logger|winston|bunyan)["']|require\(["'](pino|winston|bunyan)["']\)|^[^/*]*\bconsole\.(log|info|warn|error)\(/;
260
- const RE_EXPOSURE = /ZitadelAppIdentity|createOAuthMiddleware|createAuthorizationMiddleware|zitadelJwtScheme|createAuthzMiddleware|bearerAuth|type: *"oidc"|^\s+auth: *\{|_ROLES *= *\{|coarseRoles|ZITADEL_(AUDIENCE|CLIENT_ID)|OIDC_CLIENT_ID|zitadel\.(ApplicationOidc|ApplicationApi|Project|UserGrant)\(/;
267
+ const RE_EXPOSURE = /ZitadelAppIdentity|createOAuthMiddleware|createAuthorizationMiddleware|zitadelJwtScheme|createAuthzMiddleware|bearerAuth|type: *"oidc"|^\s+auth: *\{|_ROLES *= *\{|coarseRoles|ZITADEL_(AUDIENCE|CLIENT_ID)|OIDC_CLIENT_ID|zitadel\.(ApplicationOidc|ApplicationApi|Project|UserGrant)\(|mesh\.apps\.EventsService\(/;
261
268
  const RE_POINTER = /AppAuthzPointer/;
262
269
  const RE_METADATA = /compileOpsHubMetadata|ops-hub-metadata|SpiceDBSchema/;
263
270
  const RE_IAC_GRANT = /userId: *"[0-9]{6,}"/;
@@ -268,6 +275,8 @@ const RE_USERS_TABLE = /create table (if not exists )?"?(users|roles|user_roles|
268
275
  const RE_SURFACE_NEW = /new\s+mesh\.apps\.(ApiSurface|AppApiSurface|VendorApiSurface|Integration)\s*\(/g;
269
276
  const RE_APIDOCS_NEW = /new\s+mesh\.apps\.ApiDocs\s*\(/g;
270
277
  const RE_IDENTITY_NEW = /new\s+mesh\.auth\.(ZitadelAppIdentity)\s*\(/g;
278
+ const RE_EVENTS_NEW = /new\s+mesh\.apps\.(EventsService)\s*\(/g;
279
+ const EVENTS_PROJECT_ROLES = ["events:subscriber", "events:admin"];
271
280
  const RE_IDENTITY_BINDING = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*new\s+mesh\.auth\.ZitadelAppIdentity\s*\(/g;
272
281
  const DEFAULT_PARTNER_DOCS_ROLES = ["docs"];
273
282
  export function stringList(value) {
@@ -557,6 +566,62 @@ export function statedKind(decl, args) {
557
566
  return "vendor";
558
567
  return /^["']([a-z]+)["']$/.exec(args.get("kind") ?? "")?.[1];
559
568
  }
569
+ function programIdentities(text) {
570
+ const identities = [];
571
+ for (const d of surfaceDeclarations(text, RE_IDENTITY_NEW)) {
572
+ const options = optionsObject(d.body, text);
573
+ if (!options)
574
+ continue;
575
+ const binding = [...text.matchAll(RE_IDENTITY_BINDING)].find((m) => text.indexOf(d.body, m.index) === m.index + m[0].length - 1)?.[1];
576
+ identities.push({ binding, options: topLevelProperties(options) });
577
+ }
578
+ return identities;
579
+ }
580
+ function declaredProjectRoles(identity) {
581
+ const rolesText = identity.options.get("roles");
582
+ const coarseText = identity.options.get("coarseRoles");
583
+ if (rolesText !== undefined)
584
+ return stringList(rolesText);
585
+ if (coarseText !== undefined)
586
+ return stringList(coarseText);
587
+ return [];
588
+ }
589
+ export function eventsRoleFindings(text) {
590
+ const findings = [];
591
+ const identities = programIdentities(text);
592
+ for (const decl of surfaceDeclarations(text, RE_EVENTS_NEW)) {
593
+ const options = optionsObject(decl.body, text);
594
+ if (!options)
595
+ continue;
596
+ const args = topLevelProperties(options);
597
+ if (args.get("enabled") === "false")
598
+ continue;
599
+ if (identities.length === 0) {
600
+ continue;
601
+ }
602
+ const identityText = objectLiteralOf(args.get("identity"), text);
603
+ const projectExpr = identityText ? topLevelProperties(identityText).get("projectId") : undefined;
604
+ const projectRoot = projectExpr ? /^([A-Za-z_$][\w$]*)\.projectId\b/.exec(projectExpr.trim())?.[1] : undefined;
605
+ const identity = (projectRoot ? identities.find((id) => id.binding === projectRoot) : undefined) ??
606
+ (identities.length === 1 ? identities[0] : undefined);
607
+ if (!identity)
608
+ continue;
609
+ const declared = declaredProjectRoles(identity);
610
+ if (!declared)
611
+ continue;
612
+ const missing = EVENTS_PROJECT_ROLES.filter((r) => !declared.includes(r));
613
+ if (missing.length === 0)
614
+ continue;
615
+ findings.push({
616
+ code: "EVENTS_ROLES_UNDECLARED",
617
+ line: decl.line,
618
+ why: `mesh.apps.EventsService is declared but the app's ZitadelAppIdentity does not declare ${missing
619
+ .map((r) => `"${r}"`)
620
+ .join(" or ")} — the events service authorizes bearer callers by these project roles, and a role the identity never declares cannot be granted from the Hub's Access tab, so no subscriber can be admitted`,
621
+ });
622
+ }
623
+ return findings;
624
+ }
560
625
  function docsAudienceUngrantable(text, decl, args, docs, httpText) {
561
626
  const findings = [];
562
627
  let roles;
@@ -569,14 +634,7 @@ function docsAudienceUngrantable(text, decl, args, docs, httpText) {
569
634
  }
570
635
  if (!roles || roles.length === 0)
571
636
  return findings;
572
- const identities = [];
573
- for (const d of surfaceDeclarations(text, RE_IDENTITY_NEW)) {
574
- const options = optionsObject(d.body, text);
575
- if (!options)
576
- continue;
577
- const binding = [...text.matchAll(RE_IDENTITY_BINDING)].find((m) => text.indexOf(d.body, m.index) === m.index + m[0].length - 1)?.[1];
578
- identities.push({ binding, options: topLevelProperties(options) });
579
- }
637
+ const identities = programIdentities(text);
580
638
  if (identities.length === 0)
581
639
  return findings;
582
640
  const docsAuth = objectLiteralOf(docs.get("auth"), text);
@@ -587,9 +645,7 @@ function docsAudienceUngrantable(text, decl, args, docs, httpText) {
587
645
  (identities.length === 1 ? identities[0] : undefined);
588
646
  if (!identity)
589
647
  return findings;
590
- const rolesText = identity.options.get("roles");
591
- const coarseText = identity.options.get("coarseRoles");
592
- const declared = rolesText !== undefined ? stringList(rolesText) : coarseText !== undefined ? stringList(coarseText) : [];
648
+ const declared = declaredProjectRoles(identity);
593
649
  if (declared) {
594
650
  for (const role of roles) {
595
651
  if (declared.includes(role))
@@ -668,6 +724,8 @@ export function scanApp(root, appRel, resolution = probeResolution(root, appRel)
668
724
  }
669
725
  for (const f of docsSiteFindings(readText(index)))
670
726
  hit(f.code, `${indexRel}:${f.line}`, f.why);
727
+ for (const f of eventsRoleFindings(readText(index)))
728
+ hit(f.code, `${indexRel}:${f.line}`, f.why);
671
729
  for (const svc of serviceDirs(app)) {
672
730
  for (const f of srcFiles(svc)) {
673
731
  const text = readText(f);
@@ -27,10 +27,13 @@ export declare function generateComposableApp(appDir: string, context: {
27
27
  database: boolean;
28
28
  temporal: boolean;
29
29
  bucket: boolean;
30
+ events: boolean;
30
31
  region: string;
31
32
  workspaceDeps: boolean;
32
33
  meshRange: string;
33
34
  esmBundleRange: string;
35
+ eventsServiceRange: string;
36
+ authzRange: string;
34
37
  typesNodeRange: string;
35
38
  tsxRange: string;
36
39
  typescriptRange: string;
@@ -30,6 +30,7 @@ const PRIMITIVES = {
30
30
  database: "Database (PostgreSQL via Prisma)",
31
31
  temporal: "Temporal (workflow orchestration)",
32
32
  bucket: "S3 Bucket (file storage)",
33
+ events: "Events and webhooks (publish, subscriptions, signed delivery)",
33
34
  };
34
35
  const HUB_ACCOUNTS = {
35
36
  mesh: "159923586610",
@@ -198,6 +199,10 @@ async function promptForOptions(options) {
198
199
  primitives.push("service");
199
200
  logInfo("Added 'service' — bucket requires an API service for S3 helpers.");
200
201
  }
202
+ if (primitives.includes("events") && !primitives.includes("service")) {
203
+ primitives.push("service");
204
+ logInfo("Added 'service' — the events publisher lives in the api/ directory.");
205
+ }
201
206
  return { tenant, name, primitives };
202
207
  }
203
208
  export function copyTemplate(srcDir, destDir, context) {
@@ -311,6 +316,13 @@ export function generateComposableApp(appDir, context) {
311
316
  }
312
317
  copyTemplate(bucketDir, appDir, context);
313
318
  }
319
+ if (context.events) {
320
+ const eventsDir = path.join(fragmentsDir, "events");
321
+ if (!fs.existsSync(eventsDir)) {
322
+ throw new Error(`Fragment directory not found: ${eventsDir}`);
323
+ }
324
+ copyTemplate(eventsDir, appDir, context);
325
+ }
314
326
  const agentsDir = path.join(fragmentsDir, "agents");
315
327
  if (fs.existsSync(agentsDir)) {
316
328
  copyTemplate(agentsDir, appDir, context);
@@ -375,6 +387,10 @@ export function registerCreateAppCommand(program) {
375
387
  primitives.push("service");
376
388
  logInfo("Added 'service' — bucket requires an API service for S3 helpers.");
377
389
  }
390
+ if (primitives.includes("events") && !primitives.includes("service")) {
391
+ primitives.push("service");
392
+ logInfo("Added 'service' — the events publisher lives in the api/ directory.");
393
+ }
378
394
  await runComposable(tenant, name, primitives, options.test);
379
395
  });
380
396
  }
@@ -496,6 +512,12 @@ async function runComposable(tenant, name, primitives, test) {
496
512
  const esmBundle = workspaceDeps
497
513
  ? { range: "", resolved: true }
498
514
  : resolvePublishedRange("@mesh-tech/esm-bundle", repoRoot);
515
+ const eventsService = workspaceDeps || !primitives.includes("events")
516
+ ? { range: "", resolved: true }
517
+ : resolvePublishedRange("@mesh-tech/events-service", repoRoot);
518
+ const authz = workspaceDeps || !primitives.includes("events")
519
+ ? { range: "", resolved: true }
520
+ : resolvePublishedRange("@mesh-tech/authz", repoRoot);
499
521
  if (!workspaceDeps) {
500
522
  logInfo(mesh.resolved
501
523
  ? `@mesh-tech/* pinned to ${mesh.range} (current published line)`
@@ -508,10 +530,13 @@ async function runComposable(tenant, name, primitives, test) {
508
530
  database: primitives.includes("database"),
509
531
  temporal: primitives.includes("temporal"),
510
532
  bucket: primitives.includes("bucket"),
533
+ events: primitives.includes("events"),
511
534
  region,
512
535
  workspaceDeps,
513
536
  meshRange: mesh.range,
514
537
  esmBundleRange: esmBundle.range,
538
+ eventsServiceRange: eventsService.range,
539
+ authzRange: authz.range,
515
540
  typesNodeRange: SCAFFOLD_TOOLCHAIN["@types/node"],
516
541
  tsxRange: SCAFFOLD_TOOLCHAIN.tsx,
517
542
  typescriptRange: SCAFFOLD_TOOLCHAIN.typescript,
@@ -1,6 +1,11 @@
1
1
  import { Command } from "commander";
2
2
  import { type SessionState } from "../dev.js";
3
- export declare const HUB_SESSION = "mesh-hub-dev";
3
+ export declare const HUB_SESSION_PREFIX = "mesh-hub-dev";
4
+ export declare function hubSessionName(apiPort: number): string;
5
+ export declare function isHubSession(name: string): boolean;
6
+ export declare function listHubSessions(): string[];
7
+ export declare function killTargets(live: string[], named: string | null): string[];
8
+ export declare function killInstructions(targets: string[]): string[];
4
9
  export declare const DEFAULT_HUB_UI_PORT: number;
5
10
  export declare const DEFAULT_HUB_API_PORT = 3002;
6
11
  export interface DiscoveredSession {
@@ -10,7 +10,37 @@ import { MeshCliError } from "../../utils/errors.js";
10
10
  import { DEFAULT_HUB_PORT } from "../local/stack.js";
11
11
  import { logInfo, logSuccess, logWarn } from "../../utils/log.js";
12
12
  import { findAppRoot } from "../../utils/pulumi.js";
13
- export const HUB_SESSION = "mesh-hub-dev";
13
+ export const HUB_SESSION_PREFIX = "mesh-hub-dev";
14
+ export function hubSessionName(apiPort) {
15
+ return `${HUB_SESSION_PREFIX}-${apiPort}`;
16
+ }
17
+ const HUB_SESSION_RE = new RegExp(`^${HUB_SESSION_PREFIX}(-\\d+)?$`);
18
+ export function isHubSession(name) {
19
+ return HUB_SESSION_RE.test(name);
20
+ }
21
+ export function listHubSessions() {
22
+ try {
23
+ return execFileSync("tmux", ["list-sessions", "-F", "#{session_name}"], {
24
+ encoding: "utf8",
25
+ stdio: ["ignore", "pipe", "ignore"],
26
+ })
27
+ .split("\n")
28
+ .map((line) => line.trim())
29
+ .filter(isHubSession);
30
+ }
31
+ catch {
32
+ return [];
33
+ }
34
+ }
35
+ export function killTargets(live, named) {
36
+ return named ? live.filter((name) => name === named) : live;
37
+ }
38
+ export function killInstructions(targets) {
39
+ return targets.map((name) => {
40
+ const port = name.slice(HUB_SESSION_PREFIX.length + 1);
41
+ return port ? ` ${name}: mesh hub dev --kill --api-port ${port}` : ` ${name}: tmux kill-session -t ${name}`;
42
+ });
43
+ }
14
44
  export const DEFAULT_HUB_UI_PORT = Number(DEFAULT_HUB_PORT);
15
45
  export const DEFAULT_HUB_API_PORT = 3002;
16
46
  export function devSessionsDir() {
@@ -248,8 +278,8 @@ function isPortListening(port) {
248
278
  socket.once("error", () => done(false));
249
279
  });
250
280
  }
251
- function hubEnvDir() {
252
- return path.join(os.tmpdir(), "mesh-dev-sessions", HUB_SESSION);
281
+ function hubEnvDir(sessionName) {
282
+ return path.join(os.tmpdir(), "mesh-dev-sessions", sessionName);
253
283
  }
254
284
  export function parsePort(raw, fallback, source) {
255
285
  if (raw === undefined)
@@ -273,20 +303,30 @@ export function redactEnv(env) {
273
303
  }
274
304
  async function hubDevAction(opts) {
275
305
  if (opts.kill) {
276
- if (tmuxSessionExists(HUB_SESSION)) {
277
- execFileSync("tmux", ["kill-session", "-t", HUB_SESSION], { stdio: "ignore" });
278
- fs.rmSync(hubEnvDir(), { recursive: true, force: true });
279
- logSuccess(`Killed Hub session '${HUB_SESSION}'.`);
306
+ const namedPort = opts.apiPort || process.env.MESH_HUB_DEV_API_PORT;
307
+ const named = namedPort
308
+ ? hubSessionName(parsePort(namedPort, DEFAULT_HUB_API_PORT, opts.apiPort ? "--api-port" : "MESH_HUB_DEV_API_PORT"))
309
+ : null;
310
+ const targets = killTargets(listHubSessions(), named);
311
+ if (targets.length === 0) {
312
+ logInfo(named ? `No '${named}' tmux session running.` : "No `mesh hub dev` session running.");
313
+ return;
280
314
  }
281
- else {
282
- logInfo(`No '${HUB_SESSION}' tmux session running.`);
315
+ if (targets.length > 1) {
316
+ throw new MeshCliError(`${targets.length} Hub sessions are running — name the one to stop.\n${killInstructions(targets).join("\n")}`, { remediation: { command: "mesh hub dev --kill --api-port <port> # the API port that Hub was started on" } });
283
317
  }
318
+ const target = targets[0];
319
+ execFileSync("tmux", ["kill-session", "-t", target], { stdio: "ignore" });
320
+ fs.rmSync(hubEnvDir(target), { recursive: true, force: true });
321
+ logSuccess(`Killed Hub session '${target}'.`);
284
322
  return;
285
323
  }
286
324
  const uiPort = opts.port
287
325
  ? parsePort(opts.port, DEFAULT_HUB_UI_PORT, "--port")
288
326
  : parsePort(process.env.MESH_HUB_DEV_PORT, DEFAULT_HUB_UI_PORT, "MESH_HUB_DEV_PORT");
289
- const apiPort = parsePort(opts.apiPort, DEFAULT_HUB_API_PORT, "--api-port");
327
+ const apiPort = opts.apiPort
328
+ ? parsePort(opts.apiPort, DEFAULT_HUB_API_PORT, "--api-port")
329
+ : parsePort(process.env.MESH_HUB_DEV_API_PORT, DEFAULT_HUB_API_PORT, "MESH_HUB_DEV_API_PORT");
290
330
  const platformDir = resolvePlatformDir(opts.platformDir, process.cwd());
291
331
  const cwdAppRoot = findAppRoot(process.cwd());
292
332
  const session = pickDevSession(listDevSessions(), { session: opts.session, cwdAppRoot });
@@ -309,9 +349,10 @@ async function hubDevAction(opts) {
309
349
  console.log(` ${k}=${v}`);
310
350
  return;
311
351
  }
312
- if (tmuxSessionExists(HUB_SESSION)) {
313
- throw new MeshCliError(`A '${HUB_SESSION}' tmux session is already running.`, {
314
- remediation: { command: "mesh hub dev --kill # then re-run mesh hub dev" },
352
+ const hubSession = hubSessionName(apiPort);
353
+ if (tmuxSessionExists(hubSession)) {
354
+ throw new MeshCliError(`A '${hubSession}' tmux session is already running.`, {
355
+ remediation: { command: `mesh hub dev --kill --api-port ${apiPort} # then re-run` },
315
356
  });
316
357
  }
317
358
  for (const [label, port, flag] of [
@@ -333,20 +374,20 @@ async function hubDevAction(opts) {
333
374
  logWarn(warning);
334
375
  const apiDir = path.join(platformDir, "apps", "hub", "api");
335
376
  const uiDir = path.join(platformDir, "apps", "hub", "ui");
336
- execFileSync("tmux", ["new-session", "-d", "-s", HUB_SESSION, "-n", "api", "-c", apiDir]);
377
+ execFileSync("tmux", ["new-session", "-d", "-s", hubSession, "-n", "api", "-c", apiDir]);
337
378
  if (!assembled.apiEnv.DEV_USER_TOKEN_URL && assembled.apiEnv.DEV_USER_ID_TOKEN) {
338
379
  const platform = session.state.devOutput.platform;
339
380
  const credContext = `mesh.${platform.env}`;
340
381
  const tokenPort = await findFreePort();
341
382
  const tokenUrl = `http://127.0.0.1:${tokenPort}`;
342
- execFileSync("tmux", ["new-window", "-t", HUB_SESSION, "-n", "token-server", "-c", platformDir]);
343
- execFileSync("tmux", ["set-option", "-t", `${HUB_SESSION}:token-server`, "remain-on-exit", "on"], {
383
+ execFileSync("tmux", ["new-window", "-t", hubSession, "-n", "token-server", "-c", platformDir]);
384
+ execFileSync("tmux", ["set-option", "-t", `${hubSession}:token-server`, "remain-on-exit", "on"], {
344
385
  stdio: "ignore",
345
386
  });
346
387
  execFileSync("tmux", [
347
388
  "send-keys",
348
389
  "-t",
349
- `${HUB_SESSION}:token-server`,
390
+ `${hubSession}:token-server`,
350
391
  `npx mesh dev __token-server ${tokenPort} ${credContext}`,
351
392
  "Enter",
352
393
  ]);
@@ -358,15 +399,15 @@ async function hubDevAction(opts) {
358
399
  logSuccess(`Dev-user token-server: ${tokenUrl} (context ${credContext})`);
359
400
  }
360
401
  const launch = (window, dir, env, cmd, createWindow) => {
361
- const envFile = path.join(hubEnvDir(), `${window}.env.sh`);
402
+ const envFile = path.join(hubEnvDir(hubSession), `${window}.env.sh`);
362
403
  writeEnvFile(envFile, env);
363
404
  if (createWindow) {
364
- execFileSync("tmux", ["new-window", "-t", HUB_SESSION, "-n", window, "-c", dir]);
405
+ execFileSync("tmux", ["new-window", "-t", hubSession, "-n", window, "-c", dir]);
365
406
  }
366
- execFileSync("tmux", ["set-option", "-t", `${HUB_SESSION}:${window}`, "remain-on-exit", "on"], {
407
+ execFileSync("tmux", ["set-option", "-t", `${hubSession}:${window}`, "remain-on-exit", "on"], {
367
408
  stdio: "ignore",
368
409
  });
369
- execFileSync("tmux", ["send-keys", "-t", `${HUB_SESSION}:${window}`, buildLaunchCommand(envFile, dir, cmd), "Enter"]);
410
+ execFileSync("tmux", ["send-keys", "-t", `${hubSession}:${window}`, buildLaunchCommand(envFile, dir, cmd), "Enter"]);
370
411
  };
371
412
  launch("api", apiDir, assembled.apiEnv, "pnpm dev", false);
372
413
  launch("ui", uiDir, assembled.uiEnv, `pnpm dev --port ${uiPort} --strictPort`, true);
@@ -374,14 +415,14 @@ async function hubDevAction(opts) {
374
415
  const apiUp = await waitForPort("127.0.0.1", apiPort, 90_000);
375
416
  const uiUp = apiUp && (await waitForPort("127.0.0.1", uiPort, 90_000));
376
417
  if (!apiUp || !uiUp) {
377
- throw new MeshCliError(`Hub ${apiUp ? "UI" : "API"} did not start listening (api :${apiPort}, ui :${uiPort}).`, { remediation: { command: `tmux attach -t ${HUB_SESSION} # inspect the ${apiUp ? "ui" : "api"} window` } });
418
+ throw new MeshCliError(`Hub ${apiUp ? "UI" : "API"} did not start listening (api :${apiPort}, ui :${uiPort}).`, { remediation: { command: `tmux attach -t ${hubSession} # inspect the ${apiUp ? "ui" : "api"} window` } });
378
419
  }
379
420
  console.log("");
380
421
  logSuccess(`Mesh Hub (dev-local scope ${assembled.scope.scopeTenants.join(",")} @ ${assembled.scope.scopeEnv})`);
381
422
  console.log(` ${chalk.bold.cyan(`http://localhost:${uiPort}`)} (also reachable on your tailnet)`);
382
423
  console.log(` api http://localhost:${apiPort}`);
383
- console.log(` logs tmux attach -t ${HUB_SESSION}`);
384
- console.log(` stop mesh hub dev --kill`);
424
+ console.log(` logs tmux attach -t ${hubSession}`);
425
+ console.log(` stop mesh hub dev --kill --api-port ${apiPort}`);
385
426
  }
386
427
  export function registerHubCommands(program) {
387
428
  const hub = program.command("hub").description("Local Hub over dev-local stacks");
@@ -392,9 +433,9 @@ export function registerHubCommands(program) {
392
433
  .option("--tenants <list>", "comma-separated HUB_SCOPE_TENANTS override (default: the session's tenant)")
393
434
  .option("--audience <who>", "who the Hub renders for: `operator` (platform staff — the default here) or `tenant` (a client's operators). Anything else reads as `tenant`")
394
435
  .option("--port <port>", `Hub UI host port (default: $MESH_HUB_DEV_PORT or ${DEFAULT_HUB_UI_PORT})`)
395
- .option("--api-port <port>", `Hub API port (default: ${DEFAULT_HUB_API_PORT})`)
436
+ .option("--api-port <port>", `Hub API port, and the key its tmux session is named after — several Hubs coexist on different ones (default: $MESH_HUB_DEV_API_PORT or ${DEFAULT_HUB_API_PORT})`)
396
437
  .option("--platform-dir <dir>", "mesh-platform checkout to run the Hub from (default: $MESH_PLATFORM_DIR or walk up from cwd)")
397
438
  .option("--print-env", "print the assembled env (secrets redacted) and exit without launching")
398
- .option("--kill", "tear down the running Hub session")
439
+ .option("--kill", "tear down a running Hub session; with several up, name it with --api-port (or $MESH_HUB_DEV_API_PORT)")
399
440
  .action(hubDevAction);
400
441
  }
@@ -533,7 +533,7 @@ var init_src = __esm({
533
533
  z.object({ op: z.string().trim().min(1) }),
534
534
  z.object({ unavailable: z.string().trim().min(1) })
535
535
  ]);
536
- apiSurfaceKindSchema = z.enum(["app", "vendor"]);
536
+ apiSurfaceKindSchema = z.enum(["app", "vendor", "events"]);
537
537
  apiDocsSiteAudienceSchema = z.discriminatedUnion("readers", [
538
538
  z.object({ readers: z.literal("partner"), roles: z.array(z.string().min(1)) }),
539
539
  z.object({ readers: z.literal("operator") })