@mesh-tech/mesh-cli 0.22.3 → 0.23.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/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { jsonReport, type Check, type CheckResult, type CheckStatus } from "./dev-doctor.js";
|
|
3
|
-
export type AppCheckCode = "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_UNAUTHED" | "DOCS_AUDIENCE_IMPLICIT";
|
|
3
|
+
export type AppCheckCode = "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_UNAUTHED" | "DOCS_AUDIENCE_IMPLICIT" | "DOCS_AUDIENCE_UNGRANTABLE";
|
|
4
4
|
export interface AppCheckRule {
|
|
5
5
|
gate: string;
|
|
6
6
|
title: string;
|
|
@@ -17,6 +17,7 @@ export interface Finding {
|
|
|
17
17
|
}
|
|
18
18
|
export declare function srcFiles(dir: string): string[];
|
|
19
19
|
export declare function isUiDir(name: string): boolean;
|
|
20
|
+
export declare function stringList(value: string | undefined): string[] | undefined;
|
|
20
21
|
export declare function balancedSpan(text: string, open: number): string;
|
|
21
22
|
export declare function splitTopLevel(inner: string): string[];
|
|
22
23
|
export declare function stripTrailingComments(value: string): string;
|
|
@@ -110,6 +110,13 @@ export const APP_CHECK_RULES = {
|
|
|
110
110
|
remediation: 'declare the surface as mesh.apps.AppApiSurface or VendorApiSurface (or pass kind: "app" | "vendor"), or state docs: { audience: { roles: […] } }',
|
|
111
111
|
scope: "app",
|
|
112
112
|
},
|
|
113
|
+
DOCS_AUDIENCE_UNGRANTABLE: {
|
|
114
|
+
gate: "H",
|
|
115
|
+
title: "Docs audience grantable from the Hub",
|
|
116
|
+
severity: "block",
|
|
117
|
+
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>) — the identity asserts roles into the ID token by default, and the site\'s gate reads that token',
|
|
118
|
+
scope: "app",
|
|
119
|
+
},
|
|
113
120
|
};
|
|
114
121
|
export const APP_CHECK_CODES = Object.keys(APP_CHECK_RULES);
|
|
115
122
|
const SRC_EXT = new Set([".ts", ".tsx", ".js", ".mjs", ".jsx"]);
|
|
@@ -243,6 +250,28 @@ const RE_ALLOWLIST = /ADMIN_EMAILS|ALLOWED_EMAILS|allowedEmails|adminEmails/;
|
|
|
243
250
|
const RE_USERS_TABLE = /create table (if not exists )?"?(users|roles|user_roles|permissions)"?/i;
|
|
244
251
|
const RE_SURFACE_NEW = /new\s+mesh\.apps\.(ApiSurface|AppApiSurface|VendorApiSurface|Integration)\s*\(/g;
|
|
245
252
|
const RE_APIDOCS_NEW = /new\s+mesh\.apps\.ApiDocs\s*\(/g;
|
|
253
|
+
const RE_IDENTITY_NEW = /new\s+mesh\.auth\.(ZitadelAppIdentity)\s*\(/g;
|
|
254
|
+
const RE_IDENTITY_BINDING = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*new\s+mesh\.auth\.ZitadelAppIdentity\s*\(/g;
|
|
255
|
+
const DEFAULT_PARTNER_DOCS_ROLES = ["docs"];
|
|
256
|
+
const RE_IDENTITY_APP_CLIENT = /\bapplications\.([A-Za-z_$][\w$]*)\.clientId\b/;
|
|
257
|
+
export function stringList(value) {
|
|
258
|
+
if (!value)
|
|
259
|
+
return undefined;
|
|
260
|
+
const text = value.trim();
|
|
261
|
+
if (!text.startsWith("[") || !text.endsWith("]"))
|
|
262
|
+
return undefined;
|
|
263
|
+
const out = [];
|
|
264
|
+
for (const entry of splitTopLevel(text.slice(1, -1))) {
|
|
265
|
+
const item = stripLeadingComments(entry).trim();
|
|
266
|
+
if (!item)
|
|
267
|
+
continue;
|
|
268
|
+
const m = /^(["'])((?:(?!\1).)*)\1$/.exec(stripTrailingComments(item));
|
|
269
|
+
if (!m)
|
|
270
|
+
return undefined;
|
|
271
|
+
out.push(m[2]);
|
|
272
|
+
}
|
|
273
|
+
return out;
|
|
274
|
+
}
|
|
246
275
|
const DOCS_SITE_FIELDS = ["pages", "playground", "audience", "basePath", "ingress", "auth"];
|
|
247
276
|
const RE_DEV_BYPASS = /^["']dev-bypass["']$/;
|
|
248
277
|
export function balancedSpan(text, open) {
|
|
@@ -467,6 +496,8 @@ export function docsSiteFindings(text) {
|
|
|
467
496
|
why: `mesh.apps.${decl.ctor} declares a docs site but neither its kind nor its audience — who may read it would fall out of the name heuristic, and a vendor surface must never default to partner-readable that way`,
|
|
468
497
|
});
|
|
469
498
|
}
|
|
499
|
+
for (const f of docsAudienceUngrantable(text, decl, args, docs, httpText))
|
|
500
|
+
findings.push(f);
|
|
470
501
|
}
|
|
471
502
|
for (const decl of surfaceDeclarations(text, RE_APIDOCS_NEW)) {
|
|
472
503
|
const options = optionsObject(decl.body);
|
|
@@ -485,6 +516,70 @@ export function docsSiteFindings(text) {
|
|
|
485
516
|
}
|
|
486
517
|
return findings;
|
|
487
518
|
}
|
|
519
|
+
function docsAudienceUngrantable(text, decl, args, docs, httpText) {
|
|
520
|
+
const findings = [];
|
|
521
|
+
let roles;
|
|
522
|
+
if (docs.has("audience")) {
|
|
523
|
+
const audience = objectLiteralOf(docs.get("audience"), text);
|
|
524
|
+
roles = audience ? stringList(topLevelProperties(audience).get("roles")) : undefined;
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
const kind = decl.ctor === "AppApiSurface"
|
|
528
|
+
? "app"
|
|
529
|
+
: decl.ctor === "VendorApiSurface"
|
|
530
|
+
? "vendor"
|
|
531
|
+
: /^["']([a-z]+)["']$/.exec(args.get("kind") ?? "")?.[1];
|
|
532
|
+
if (kind === "app")
|
|
533
|
+
roles = DEFAULT_PARTNER_DOCS_ROLES;
|
|
534
|
+
}
|
|
535
|
+
if (!roles || roles.length === 0)
|
|
536
|
+
return findings;
|
|
537
|
+
const identities = [];
|
|
538
|
+
for (const d of surfaceDeclarations(text, RE_IDENTITY_NEW)) {
|
|
539
|
+
const options = optionsObject(d.body);
|
|
540
|
+
if (!options)
|
|
541
|
+
continue;
|
|
542
|
+
const binding = [...text.matchAll(RE_IDENTITY_BINDING)].find((m) => text.indexOf(d.body, m.index) === m.index + m[0].length - 1)?.[1];
|
|
543
|
+
identities.push({ binding, options: topLevelProperties(options) });
|
|
544
|
+
}
|
|
545
|
+
if (identities.length === 0)
|
|
546
|
+
return findings;
|
|
547
|
+
const docsAuth = objectLiteralOf(docs.get("auth"), text);
|
|
548
|
+
const clientExpr = (docsAuth ? topLevelProperties(docsAuth).get("clientId") : undefined) ??
|
|
549
|
+
(httpText ? topLevelProperties(objectLiteralOf(topLevelProperties(httpText).get("auth"), text) ?? "{}").get("clientId") : undefined);
|
|
550
|
+
const appName = clientExpr ? RE_IDENTITY_APP_CLIENT.exec(clientExpr)?.[1] : undefined;
|
|
551
|
+
const clientRoot = clientExpr ? /^([A-Za-z_$][\w$]*)\.applications\./.exec(clientExpr.trim())?.[1] : undefined;
|
|
552
|
+
const identity = (clientRoot ? identities.find((id) => id.binding === clientRoot) : undefined) ??
|
|
553
|
+
(identities.length === 1 ? identities[0] : undefined);
|
|
554
|
+
if (!identity)
|
|
555
|
+
return findings;
|
|
556
|
+
const rolesText = identity.options.get("roles");
|
|
557
|
+
const coarseText = identity.options.get("coarseRoles");
|
|
558
|
+
const declared = rolesText !== undefined ? stringList(rolesText) : coarseText !== undefined ? stringList(coarseText) : [];
|
|
559
|
+
if (declared) {
|
|
560
|
+
for (const role of roles) {
|
|
561
|
+
if (declared.includes(role))
|
|
562
|
+
continue;
|
|
563
|
+
findings.push({
|
|
564
|
+
code: "DOCS_AUDIENCE_UNGRANTABLE",
|
|
565
|
+
line: decl.line,
|
|
566
|
+
why: `the docs audience gates on role "${role}", which the app's ZitadelAppIdentity does not declare in roles — the Hub cannot grant it, so no one can read the site`,
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (!appName)
|
|
571
|
+
return findings;
|
|
572
|
+
const apps = objectLiteralOf(identity.options.get("applications"), text);
|
|
573
|
+
const app = apps ? objectLiteralOf(topLevelProperties(apps).get(appName), text) : undefined;
|
|
574
|
+
if (app && topLevelProperties(app).get("idTokenRoleAssertion") === "false") {
|
|
575
|
+
findings.push({
|
|
576
|
+
code: "DOCS_AUDIENCE_UNGRANTABLE",
|
|
577
|
+
line: decl.line,
|
|
578
|
+
why: `the docs sign-in client is applications.${appName} on the ZitadelAppIdentity, which sets idTokenRoleAssertion: false — the proxy's ID token then carries no roles and the site's gate admits nobody`,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
return findings;
|
|
582
|
+
}
|
|
488
583
|
export function listApps(root) {
|
|
489
584
|
const apps = [];
|
|
490
585
|
const appsDir = path.join(root, "apps");
|
|
@@ -106,7 +106,10 @@ const worker = new mesh.apps.TemporalWorker("worker", {
|
|
|
106
106
|
// ingress, so nothing is published on a hostname until you choose to. To
|
|
107
107
|
// publish it, give it an ingress AND the platform sign-in (`auth: { provider:
|
|
108
108
|
// "mesh", clientId, clientSecret }` from a ZitadelAppIdentity application),
|
|
109
|
-
//
|
|
109
|
+
// choose its readers with `auth.requireRoles: ["docs"]` (signed in AND holding
|
|
110
|
+
// that role in this app's project — declare the role on the identity, which
|
|
111
|
+
// then asserts it into the application's ID token; omit it for every signed-in
|
|
112
|
+
// user), then list it in the Hub with `env.register({ docs: { url: docs.url,
|
|
110
113
|
// provider: "zudoku" } })`. An API surface (`mesh.apps.ApiSurface`) declares
|
|
111
114
|
// the same site on its `docs` block and wires all of that itself.
|
|
112
115
|
// =============================================================================
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mesh-tech/mesh-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "CLI for Mesh platform development utilities",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@mesh-tech/agent-contracts": "0.2.0",
|
|
45
|
-
"@mesh-tech/tsconfig": "2.
|
|
45
|
+
"@mesh-tech/tsconfig": "2.11.0",
|
|
46
46
|
"@mesh-tech/workflow-viz": "0.3.0",
|
|
47
47
|
"@tanstack/intent": "^0.3.6",
|
|
48
48
|
"@types/node": "^20.0.0",
|