@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.
- package/assets/intent/intent-claude-gate.mjs +4 -4
- package/bin/mesh.mjs +1 -1
- package/dist/bin/mesh.js +77 -1
- package/dist/bin/mesh.js.map +1 -1
- package/dist/build-info.json +2 -2
- package/dist/src/commands/app-check.d.ts +2 -1
- package/dist/src/commands/app-check.js +95 -0
- package/fragments/agents/CLAUDE.md.hbs +1 -1
- package/fragments/base/index.ts.hbs +4 -1
- package/package.json +2 -2
|
@@ -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
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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
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]*))?$/;
|