@mesh-tech/mesh-cli 0.21.0 → 0.22.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/dist/bin/mesh.js CHANGED
@@ -6300,7 +6300,12 @@ var init_src = __esm({
6300
6300
  description: z.string().optional(),
6301
6301
  surfaces: apiSurfaceSchema,
6302
6302
  credentials: z.object({ keyedBy: z.string().nullish() }).optional(),
6303
- docs: z.object({ url: z.string().min(1), contentHash: z.string().min(1) }),
6303
+ docs: z.object({
6304
+ url: z.string().min(1),
6305
+ contentHash: z.string().min(1),
6306
+ siteUrl: z.string().min(1).optional(),
6307
+ siteInternalUrl: z.string().min(1).optional()
6308
+ }),
6304
6309
  enabledOps: z.array(z.string()),
6305
6310
  // The consumer schema strips unknown keys, so a producer-side field that is
6306
6311
  // not mirrored here never reaches a consumer — mirror every addition.
@@ -10793,6 +10798,209 @@ function authzSurface(app) {
10793
10798
  walk2(app);
10794
10799
  return hits;
10795
10800
  }
10801
+ function balancedSpan(text, open) {
10802
+ const closers = { "(": ")", "{": "}", "[": "]" };
10803
+ const stack = [];
10804
+ let quote;
10805
+ for (let i = open; i < text.length; i++) {
10806
+ const ch = text[i];
10807
+ if (quote) {
10808
+ if (ch === "\\") i++;
10809
+ else if (ch === quote) quote = void 0;
10810
+ continue;
10811
+ }
10812
+ if (ch === '"' || ch === "'" || ch === "`") {
10813
+ quote = ch;
10814
+ continue;
10815
+ }
10816
+ if (ch === "/" && text[i + 1] === "/") {
10817
+ const eol = text.indexOf("\n", i);
10818
+ if (eol === -1) return "";
10819
+ i = eol;
10820
+ continue;
10821
+ }
10822
+ if (ch === "/" && text[i + 1] === "*") {
10823
+ const end = text.indexOf("*/", i + 2);
10824
+ if (end === -1) return "";
10825
+ i = end + 1;
10826
+ continue;
10827
+ }
10828
+ if (closers[ch]) stack.push(closers[ch]);
10829
+ else if (ch === ")" || ch === "}" || ch === "]") {
10830
+ if (stack.pop() !== ch) return "";
10831
+ if (stack.length === 0) return text.slice(open, i + 1);
10832
+ }
10833
+ }
10834
+ return "";
10835
+ }
10836
+ function splitTopLevel(inner) {
10837
+ const parts = [];
10838
+ let depth = 0;
10839
+ let quote;
10840
+ let start = 0;
10841
+ for (let i = 0; i < inner.length; i++) {
10842
+ const ch = inner[i];
10843
+ if (quote) {
10844
+ if (ch === "\\") i++;
10845
+ else if (ch === quote) quote = void 0;
10846
+ continue;
10847
+ }
10848
+ if (ch === '"' || ch === "'" || ch === "`") {
10849
+ quote = ch;
10850
+ continue;
10851
+ }
10852
+ if (ch === "/" && inner[i + 1] === "/") {
10853
+ const eol = inner.indexOf("\n", i);
10854
+ if (eol === -1) break;
10855
+ i = eol;
10856
+ continue;
10857
+ }
10858
+ if (ch === "/" && inner[i + 1] === "*") {
10859
+ const end = inner.indexOf("*/", i + 2);
10860
+ if (end === -1) break;
10861
+ i = end + 1;
10862
+ continue;
10863
+ }
10864
+ if (ch === "(" || ch === "{" || ch === "[") depth++;
10865
+ else if (ch === ")" || ch === "}" || ch === "]") depth--;
10866
+ else if (ch === "," && depth === 0) {
10867
+ parts.push(inner.slice(start, i));
10868
+ start = i + 1;
10869
+ }
10870
+ }
10871
+ const last = inner.slice(start);
10872
+ if (last.trim()) parts.push(last);
10873
+ return parts;
10874
+ }
10875
+ function stripLeadingComments(entry) {
10876
+ let text = entry;
10877
+ for (; ; ) {
10878
+ const next = text.replace(/^\s*(?:\/\/[^\n]*(?:\n|$)|\/\*[\s\S]*?\*\/)/, "");
10879
+ if (next === text) return text;
10880
+ text = next;
10881
+ }
10882
+ }
10883
+ function stripTrailingComments(value) {
10884
+ let out = "";
10885
+ let depth = 0;
10886
+ let quote;
10887
+ for (let i = 0; i < value.length; i++) {
10888
+ const ch = value[i];
10889
+ if (quote) {
10890
+ out += ch;
10891
+ if (ch === "\\") {
10892
+ out += value[i + 1] ?? "";
10893
+ i++;
10894
+ } else if (ch === quote) quote = void 0;
10895
+ continue;
10896
+ }
10897
+ if (ch === '"' || ch === "'" || ch === "`") {
10898
+ quote = ch;
10899
+ out += ch;
10900
+ continue;
10901
+ }
10902
+ if (depth === 0 && ch === "/" && value[i + 1] === "/") {
10903
+ const eol = value.indexOf("\n", i);
10904
+ if (eol === -1) break;
10905
+ i = eol;
10906
+ out += "\n";
10907
+ continue;
10908
+ }
10909
+ if (depth === 0 && ch === "/" && value[i + 1] === "*") {
10910
+ const end = value.indexOf("*/", i + 2);
10911
+ if (end === -1) break;
10912
+ i = end + 1;
10913
+ continue;
10914
+ }
10915
+ if (ch === "(" || ch === "{" || ch === "[") depth++;
10916
+ else if (ch === ")" || ch === "}" || ch === "]") depth--;
10917
+ out += ch;
10918
+ }
10919
+ return out.trim();
10920
+ }
10921
+ function topLevelProperties(objectText) {
10922
+ const props = /* @__PURE__ */ new Map();
10923
+ if (!objectText.startsWith("{") || !objectText.endsWith("}")) return props;
10924
+ for (const entry of splitTopLevel(objectText.slice(1, -1))) {
10925
+ const text = stripLeadingComments(entry).trim();
10926
+ if (!text || text.startsWith("...")) continue;
10927
+ const m = RE_PROPERTY.exec(text);
10928
+ if (!m) continue;
10929
+ const key = m[1] ?? m[2] ?? m[3];
10930
+ props.set(key, stripTrailingComments(m[4] ?? key));
10931
+ }
10932
+ return props;
10933
+ }
10934
+ function objectLiteralOf(value, program2) {
10935
+ if (!value) return void 0;
10936
+ if (value.startsWith("{")) return balancedSpan(value, 0) || void 0;
10937
+ const id = /^[A-Za-z_$][\w$]*$/.exec(value)?.[0];
10938
+ if (!id) return void 0;
10939
+ const decl = new RegExp(`\\bconst\\s+${id}\\s*(?::[^=]+)?=\\s*\\{`).exec(program2);
10940
+ return decl ? balancedSpan(program2, decl.index + decl[0].length - 1) || void 0 : void 0;
10941
+ }
10942
+ function optionsObject(body) {
10943
+ for (const arg of splitTopLevel(body.slice(1, -1))) {
10944
+ const trimmed = arg.trim();
10945
+ if (trimmed.startsWith("{")) return balancedSpan(trimmed, 0) || void 0;
10946
+ }
10947
+ return void 0;
10948
+ }
10949
+ function surfaceDeclarations(text, pattern = RE_SURFACE_NEW) {
10950
+ const out = [];
10951
+ for (const m of text.matchAll(pattern)) {
10952
+ const body = balancedSpan(text, m.index + m[0].length - 1);
10953
+ if (!body) continue;
10954
+ out.push({ ctor: m[1] ?? "ApiDocs", body, line: text.slice(0, m.index).split("\n").length });
10955
+ }
10956
+ return out;
10957
+ }
10958
+ function isClientAuth(value) {
10959
+ return value !== void 0 && value !== "undefined" && !RE_DEV_BYPASS.test(value);
10960
+ }
10961
+ function docsSiteFindings(text) {
10962
+ const findings = [];
10963
+ for (const decl of surfaceDeclarations(text)) {
10964
+ const options = optionsObject(decl.body);
10965
+ if (!options) continue;
10966
+ const args = topLevelProperties(options);
10967
+ const docsText = objectLiteralOf(args.get("docs"), text);
10968
+ if (!docsText) continue;
10969
+ const docs = topLevelProperties(docsText);
10970
+ if (docs.get("site") === "false") continue;
10971
+ if (docs.get("site") !== "true" && !DOCS_SITE_FIELDS.some((f) => docs.has(f))) continue;
10972
+ const httpText = objectLiteralOf(topLevelProperties(objectLiteralOf(args.get("surfaces"), text) ?? "{}").get("http"), text);
10973
+ const hasClient = isClientAuth(docs.get("auth")) || isClientAuth(topLevelProperties(httpText ?? "{}").get("auth"));
10974
+ if (!hasClient) {
10975
+ findings.push({
10976
+ code: "DOCS_SITE_UNAUTHED",
10977
+ line: decl.line,
10978
+ why: `mesh.apps.${decl.ctor} declares a docs site with no OIDC client \u2014 the site is served behind the platform sign-in proxy, which needs one (docs.auth, or a client on surfaces.http.auth)`
10979
+ });
10980
+ }
10981
+ const kindStated = decl.ctor === "AppApiSurface" || decl.ctor === "VendorApiSurface" || args.has("kind");
10982
+ if (!kindStated && !docs.has("audience")) {
10983
+ findings.push({
10984
+ code: "DOCS_AUDIENCE_IMPLICIT",
10985
+ line: decl.line,
10986
+ 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
+ });
10988
+ }
10989
+ }
10990
+ for (const decl of surfaceDeclarations(text, RE_APIDOCS_NEW)) {
10991
+ const options = optionsObject(decl.body);
10992
+ if (!options) continue;
10993
+ const args = topLevelProperties(options);
10994
+ if (!args.has("ingress") || args.get("ingress") === "undefined") continue;
10995
+ if (isClientAuth(args.get("auth"))) continue;
10996
+ findings.push({
10997
+ code: "DOCS_SITE_UNAUTHED",
10998
+ line: decl.line,
10999
+ why: `mesh.apps.ApiDocs is published on an ingress with no auth \u2014 the site (reference and playground) is reachable by anyone; put the platform sign-in proxy in front of it, or drop the ingress`
11000
+ });
11001
+ }
11002
+ return findings;
11003
+ }
10796
11004
  function listApps(root) {
10797
11005
  const apps = [];
10798
11006
  const appsDir = path22.join(root, "apps");
@@ -10841,6 +11049,7 @@ function scanApp(root, appRel) {
10841
11049
  if (!/env\.register\(|\.annotate\(/.test(readText(index))) {
10842
11050
  hit("NO_REGISTER", indexRel, "never calls env.register({\u2026}) \u2014 the Hub cannot list this app");
10843
11051
  }
11052
+ for (const f of docsSiteFindings(readText(index))) hit(f.code, `${indexRel}:${f.line}`, f.why);
10844
11053
  for (const svc of serviceDirs(app)) {
10845
11054
  for (const f of srcFiles(svc)) {
10846
11055
  const text = readText(f);
@@ -10997,7 +11206,7 @@ function registerAppCommands(program2) {
10997
11206
  }
10998
11207
  });
10999
11208
  }
11000
- 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;
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;
11001
11210
  var init_app_check = __esm({
11002
11211
  "libs/mesh-cli/src/commands/app-check.ts"() {
11003
11212
  "use strict";
@@ -11096,6 +11305,20 @@ var init_app_check = __esm({
11096
11305
  severity: "block",
11097
11306
  remediation: "drop the table; people and roles live in Zitadel and are administered from the Hub",
11098
11307
  scope: "app"
11308
+ },
11309
+ DOCS_SITE_UNAUTHED: {
11310
+ gate: "0.3",
11311
+ title: "Docs site behind the platform sign-in",
11312
+ severity: "block",
11313
+ remediation: 'on an API surface: docs: { auth: { clientId, clientSecret } } (or a client on surfaces.http.auth), or opt out with docs: { site: false }; on a mesh.apps.ApiDocs: auth: { provider: "mesh", clientId, clientSecret }, or drop the ingress (an in-cluster site is reachable through mesh dev)',
11314
+ scope: "app"
11315
+ },
11316
+ DOCS_AUDIENCE_IMPLICIT: {
11317
+ gate: "H",
11318
+ title: "Docs audience stated, not derived",
11319
+ severity: "block",
11320
+ remediation: 'declare the surface as mesh.apps.AppApiSurface or VendorApiSurface (or pass kind: "app" | "vendor"), or state docs: { audience: { roles: [\u2026] } }',
11321
+ scope: "app"
11099
11322
  }
11100
11323
  };
11101
11324
  APP_CHECK_CODES = Object.keys(APP_CHECK_RULES);
@@ -11127,6 +11350,11 @@ var init_app_check = __esm({
11127
11350
  RE_PASSWORD = /(^|[^a-z])(bcrypt|argon2)|password_hash|passwordHash|hashPassword|temporaryPassword.*(send|mail)|(send|mail).*temporaryPassword/;
11128
11351
  RE_ALLOWLIST = /ADMIN_EMAILS|ALLOWED_EMAILS|allowedEmails|adminEmails/;
11129
11352
  RE_USERS_TABLE = /create table (if not exists )?"?(users|roles|user_roles|permissions)"?/i;
11353
+ RE_SURFACE_NEW = /new\s+mesh\.apps\.(ApiSurface|AppApiSurface|VendorApiSurface|Integration)\s*\(/g;
11354
+ RE_APIDOCS_NEW = /new\s+mesh\.apps\.ApiDocs\s*\(/g;
11355
+ DOCS_SITE_FIELDS = ["pages", "playground", "audience", "basePath", "ingress", "auth"];
11356
+ RE_DEV_BYPASS = /^["']dev-bypass["']$/;
11357
+ RE_PROPERTY = /^(?:"([^"]+)"|'([^']+)'|([A-Za-z_$][\w$]*))\s*(?::\s*([\s\S]*))?$/;
11130
11358
  }
11131
11359
  });
11132
11360