@mesh-tech/mesh-cli 0.22.4 → 0.23.1

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.
@@ -9,15 +9,15 @@ import { performance } from 'node:perf_hooks'
9
9
  const AGENT = "claude"
10
10
  const CATALOG_COMMAND = "pnpm exec intent list --json --no-notices"
11
11
  const EDIT_TOOLS = new Set(["Edit","MultiEdit","NotebookEdit","Write"])
12
- // MESH-2162: name the exact runnable commands, not a vague "follow the setup".
12
+ // Name the exact runnable commands, not a vague "follow the setup".
13
13
  // An agent that hits this deny WITHOUT the SessionStart catalog in view (subagent,
14
14
  // post-compaction, headless) otherwise has no command to run — the "guess a bare
15
- // `intent`, fail, give up" loop MESH-2160 closed for the catalog. Reuse
15
+ // `intent`, fail, give up" loop the catalog closed. Reuse
16
16
  // CATALOG_COMMAND so the listing invocation stays in one place.
17
17
  const GATE_DENY_REASON = `Blocked: load matching TanStack guidance before editing. Run \`${CATALOG_COMMAND}\` to see available skills, then \`pnpm exec intent load <package>#<skill>\` for the matching one, then retry the edit. (No bare \`intent\` on PATH — use \`pnpm exec intent\`.)`
18
18
  const INTENT_COMMAND_PATTERN = /(?:^|&&|\|\||;|\|)\s*((?:bunx\s+@tanstack\/intent(?:@latest)?)|(?:pnpm\s+exec\s+intent)|(?:pnpm\s+dlx\s+@tanstack\/intent(?:@latest)?)|(?:npx\s+@tanstack\/intent(?:@latest)?)|(?:yarn\s+dlx\s+@tanstack\/intent(?:@latest)?)|(?:intent))\s+(list|load)(?:\s+([^\s|;&]+))?/i
19
19
 
20
- // LOCAL HARDENING (MESH-2020): this file started as `intent install` output, but it
20
+ // LOCAL HARDENING: this file started as `intent install` output, but it
21
21
  // is committed and CLI-managed — `mesh skills sync` (and `mesh init app-tenant --fix`)
22
22
  // overwrite it from the mesh-cli asset (libs/mesh-cli/assets/intent/intent-claude-gate.mjs
23
23
  // in mesh-platform). Never fix one copy without the other; in mesh-platform,
@@ -131,7 +131,7 @@ function formatSessionCatalog(result) {
131
131
  }
132
132
 
133
133
  function formatSkillCatalog(skills) {
134
- // Render each skill as a copy-paste load command (MESH-2160). `skill.use` is the
134
+ // Render each skill as a copy-paste load command. `skill.use` is the
135
135
  // exact `<package>#<skill>` id, so agents don't have to guess the invocation — the
136
136
  // vague "load with the Intent CLI" wording made them try a non-existent bare
137
137
  // `intent` and give up. LOCAL CUSTOMIZATION: re-apply if you re-run `intent install`.
package/bin/mesh.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * mesh dev launcher (MESH-2261).
3
+ * mesh dev launcher.
4
4
  *
5
5
  * In the monorepo this IS the package `bin`, and it ALWAYS runs the CLI from
6
6
  * TypeScript source via tsx — so the CLI is never stale, needs no build, and
package/dist/bin/mesh.js CHANGED
@@ -10798,6 +10798,20 @@ function authzSurface(app) {
10798
10798
  walk2(app);
10799
10799
  return hits;
10800
10800
  }
10801
+ function stringList(value) {
10802
+ if (!value) return void 0;
10803
+ const text = value.trim();
10804
+ if (!text.startsWith("[") || !text.endsWith("]")) return void 0;
10805
+ const out = [];
10806
+ for (const entry of splitTopLevel(text.slice(1, -1))) {
10807
+ const item = stripLeadingComments(entry).trim();
10808
+ if (!item) continue;
10809
+ const m = /^(["'])((?:(?!\1).)*)\1$/.exec(stripTrailingComments(item));
10810
+ if (!m) return void 0;
10811
+ out.push(m[2]);
10812
+ }
10813
+ return out;
10814
+ }
10801
10815
  function balancedSpan(text, open) {
10802
10816
  const closers = { "(": ")", "{": "}", "[": "]" };
10803
10817
  const stack = [];
@@ -10986,6 +11000,7 @@ function docsSiteFindings(text) {
10986
11000
  why: `mesh.apps.${decl.ctor} declares a docs site but neither its kind nor its audience \u2014 who may read it would fall out of the name heuristic, and a vendor surface must never default to partner-readable that way`
10987
11001
  });
10988
11002
  }
11003
+ for (const f of docsAudienceUngrantable(text, decl, args, docs, httpText)) findings.push(f);
10989
11004
  }
10990
11005
  for (const decl of surfaceDeclarations(text, RE_APIDOCS_NEW)) {
10991
11006
  const options = optionsObject(decl.body);
@@ -11001,6 +11016,56 @@ function docsSiteFindings(text) {
11001
11016
  }
11002
11017
  return findings;
11003
11018
  }
11019
+ function docsAudienceUngrantable(text, decl, args, docs, httpText) {
11020
+ const findings = [];
11021
+ let roles;
11022
+ if (docs.has("audience")) {
11023
+ const audience = objectLiteralOf(docs.get("audience"), text);
11024
+ roles = audience ? stringList(topLevelProperties(audience).get("roles")) : void 0;
11025
+ } else {
11026
+ const kind = decl.ctor === "AppApiSurface" ? "app" : decl.ctor === "VendorApiSurface" ? "vendor" : /^["']([a-z]+)["']$/.exec(args.get("kind") ?? "")?.[1];
11027
+ if (kind === "app") roles = DEFAULT_PARTNER_DOCS_ROLES;
11028
+ }
11029
+ if (!roles || roles.length === 0) return findings;
11030
+ const identities = [];
11031
+ for (const d of surfaceDeclarations(text, RE_IDENTITY_NEW)) {
11032
+ const options = optionsObject(d.body);
11033
+ if (!options) continue;
11034
+ const binding = [...text.matchAll(RE_IDENTITY_BINDING)].find((m) => text.indexOf(d.body, m.index) === m.index + m[0].length - 1)?.[1];
11035
+ identities.push({ binding, options: topLevelProperties(options) });
11036
+ }
11037
+ if (identities.length === 0) return findings;
11038
+ const docsAuth = objectLiteralOf(docs.get("auth"), text);
11039
+ const clientExpr = (docsAuth ? topLevelProperties(docsAuth).get("clientId") : void 0) ?? (httpText ? topLevelProperties(objectLiteralOf(topLevelProperties(httpText).get("auth"), text) ?? "{}").get("clientId") : void 0);
11040
+ const appName = clientExpr ? RE_IDENTITY_APP_CLIENT.exec(clientExpr)?.[1] : void 0;
11041
+ const clientRoot = clientExpr ? /^([A-Za-z_$][\w$]*)\.applications\./.exec(clientExpr.trim())?.[1] : void 0;
11042
+ const identity = (clientRoot ? identities.find((id) => id.binding === clientRoot) : void 0) ?? (identities.length === 1 ? identities[0] : void 0);
11043
+ if (!identity) return findings;
11044
+ const rolesText = identity.options.get("roles");
11045
+ const coarseText = identity.options.get("coarseRoles");
11046
+ const declared = rolesText !== void 0 ? stringList(rolesText) : coarseText !== void 0 ? stringList(coarseText) : [];
11047
+ if (declared) {
11048
+ for (const role of roles) {
11049
+ if (declared.includes(role)) continue;
11050
+ findings.push({
11051
+ code: "DOCS_AUDIENCE_UNGRANTABLE",
11052
+ line: decl.line,
11053
+ why: `the docs audience gates on role "${role}", which the app's ZitadelAppIdentity does not declare in roles \u2014 the Hub cannot grant it, so no one can read the site`
11054
+ });
11055
+ }
11056
+ }
11057
+ if (!appName) return findings;
11058
+ const apps = objectLiteralOf(identity.options.get("applications"), text);
11059
+ const app = apps ? objectLiteralOf(topLevelProperties(apps).get(appName), text) : void 0;
11060
+ if (app && topLevelProperties(app).get("idTokenRoleAssertion") === "false") {
11061
+ findings.push({
11062
+ code: "DOCS_AUDIENCE_UNGRANTABLE",
11063
+ line: decl.line,
11064
+ why: `the docs sign-in client is applications.${appName} on the ZitadelAppIdentity, which sets idTokenRoleAssertion: false \u2014 the proxy's ID token then carries no roles and the site's gate admits nobody`
11065
+ });
11066
+ }
11067
+ return findings;
11068
+ }
11004
11069
  function listApps(root) {
11005
11070
  const apps = [];
11006
11071
  const appsDir = path22.join(root, "apps");
@@ -11206,7 +11271,7 @@ function registerAppCommands(program2) {
11206
11271
  }
11207
11272
  });
11208
11273
  }
11209
- var APP_CHECK_RULES, APP_CHECK_CODES, SRC_EXT, SKIP_DIRS, NON_SERVICE_DIRS, RE_HTML, RE_OIDC, RE_COOKIE, RE_LOGGER, RE_EXPOSURE, RE_POINTER, RE_METADATA, RE_IAC_GRANT, RE_POINTER_OVERRIDE, RE_PASSWORD, RE_ALLOWLIST, RE_USERS_TABLE, RE_SURFACE_NEW, RE_APIDOCS_NEW, DOCS_SITE_FIELDS, RE_DEV_BYPASS, RE_PROPERTY;
11274
+ var APP_CHECK_RULES, APP_CHECK_CODES, SRC_EXT, SKIP_DIRS, NON_SERVICE_DIRS, RE_HTML, RE_OIDC, RE_COOKIE, RE_LOGGER, RE_EXPOSURE, RE_POINTER, RE_METADATA, RE_IAC_GRANT, RE_POINTER_OVERRIDE, RE_PASSWORD, RE_ALLOWLIST, RE_USERS_TABLE, RE_SURFACE_NEW, RE_APIDOCS_NEW, RE_IDENTITY_NEW, RE_IDENTITY_BINDING, DEFAULT_PARTNER_DOCS_ROLES, RE_IDENTITY_APP_CLIENT, DOCS_SITE_FIELDS, RE_DEV_BYPASS, RE_PROPERTY;
11210
11275
  var init_app_check = __esm({
11211
11276
  "libs/mesh-cli/src/commands/app-check.ts"() {
11212
11277
  "use strict";
@@ -11319,6 +11384,13 @@ var init_app_check = __esm({
11319
11384
  severity: "block",
11320
11385
  remediation: 'declare the surface as mesh.apps.AppApiSurface or VendorApiSurface (or pass kind: "app" | "vendor"), or state docs: { audience: { roles: [\u2026] } }',
11321
11386
  scope: "app"
11387
+ },
11388
+ DOCS_AUDIENCE_UNGRANTABLE: {
11389
+ gate: "H",
11390
+ title: "Docs audience grantable from the Hub",
11391
+ severity: "block",
11392
+ remediation: `declare every audience role on the ZitadelAppIdentity (roles: ["docs"]) so the Hub can grant it, and do not set idTokenRoleAssertion: false on the application the docs sign-in proxy uses (applications.<name>) \u2014 the identity asserts roles into the ID token by default, and the site's gate reads that token`,
11393
+ scope: "app"
11322
11394
  }
11323
11395
  };
11324
11396
  APP_CHECK_CODES = Object.keys(APP_CHECK_RULES);
@@ -11352,6 +11424,10 @@ var init_app_check = __esm({
11352
11424
  RE_USERS_TABLE = /create table (if not exists )?"?(users|roles|user_roles|permissions)"?/i;
11353
11425
  RE_SURFACE_NEW = /new\s+mesh\.apps\.(ApiSurface|AppApiSurface|VendorApiSurface|Integration)\s*\(/g;
11354
11426
  RE_APIDOCS_NEW = /new\s+mesh\.apps\.ApiDocs\s*\(/g;
11427
+ RE_IDENTITY_NEW = /new\s+mesh\.auth\.(ZitadelAppIdentity)\s*\(/g;
11428
+ RE_IDENTITY_BINDING = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*new\s+mesh\.auth\.ZitadelAppIdentity\s*\(/g;
11429
+ DEFAULT_PARTNER_DOCS_ROLES = ["docs"];
11430
+ RE_IDENTITY_APP_CLIENT = /\bapplications\.([A-Za-z_$][\w$]*)\.clientId\b/;
11355
11431
  DOCS_SITE_FIELDS = ["pages", "playground", "audience", "basePath", "ingress", "auth"];
11356
11432
  RE_DEV_BYPASS = /^["']dev-bypass["']$/;
11357
11433
  RE_PROPERTY = /^(?:"([^"]+)"|'([^']+)'|([A-Za-z_$][\w$]*))\s*(?::\s*([\s\S]*))?$/;