@wix/himalaya-cli 0.1437.0 → 0.1439.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/cli.mjs CHANGED
@@ -6939,7 +6939,7 @@ function validator(schema) {
6939
6939
  }
6940
6940
  function compileV2(html, profile, options) {
6941
6941
  const diagnostics = [], sourceMap2 = {}, dependencies = [options.file];
6942
- const requiredCapabilities = /* @__PURE__ */ new Set();
6942
+ const requiredCapabilities2 = /* @__PURE__ */ new Set();
6943
6943
  const $2 = cheerio2.load(html, { sourceCodeLocationInfo: true });
6944
6944
  const loc = (el2) => ({ file: options.file, line: el2?.sourceCodeLocation?.startLine ?? 1, column: el2?.sourceCodeLocation?.startCol ?? 1 });
6945
6945
  const error = (el2, code, message, hint) => diagnostics.push({ ...loc(el2), code, severity: "error", message, ...hint ? { hint } : {} });
@@ -6973,7 +6973,7 @@ function compileV2(html, profile, options) {
6973
6973
  } else error(script, "HHP-SCRIPT", "Use one external application/himalaya-controller script. Inline/page JavaScript is unavailable.");
6974
6974
  }
6975
6975
  let info4;
6976
- for (const cap of moduleCapabilities(controller?.source ?? directService?.source ?? "", controller?.file ?? options.file, profile)) requiredCapabilities.add(cap);
6976
+ for (const cap of moduleCapabilities(controller?.source ?? directService?.source ?? "", controller?.file ?? options.file, profile)) requiredCapabilities2.add(cap);
6977
6977
  if (controller) {
6978
6978
  dependencies.push(controller.file);
6979
6979
  info4 = analyzeController(controller.source, controller.file, profile, diagnostics);
@@ -7049,7 +7049,7 @@ function compileV2(html, profile, options) {
7049
7049
  if (!info4?.handlers.has(action.slice(`tier5.${serviceName}.`.length))) error(el2, "HHP-EVENT", `Unknown structured handler ${action}.`);
7050
7050
  } else if (action.startsWith("capability.")) {
7051
7051
  const [, cap, method] = action.split(".");
7052
- requiredCapabilities.add(cap);
7052
+ requiredCapabilities2.add(cap);
7053
7053
  if (!grants(profile.declaredCapabilities, cap)) error(el2, "HHP-CAPABILITY-MISSING", `Action requires declared capability ${cap}.`);
7054
7054
  if (!own5(profile.capabilities, cap)?.methods.includes(method)) error(el2, "HHP-EVENT", `Unknown capability method ${action}.`);
7055
7055
  } else if (!profile.handlers?.includes(action)) error(el2, "HHP-EVENT", `Action ${action} is not declared by this app.`);
@@ -7288,7 +7288,7 @@ function compileV2(html, profile, options) {
7288
7288
  if (tag === "a") {
7289
7289
  if (attrs.href?.startsWith("#")) props.action = { actionId: "himi.open", params: { screenId: attrs.href.slice(1) } };
7290
7290
  else if (/^https:\/\//.test(attrs.href ?? "")) {
7291
- requiredCapabilities.add("open_url");
7291
+ requiredCapabilities2.add("open_url");
7292
7292
  if (!grants(profile.declaredCapabilities, "open_url")) error(el2, "HHP-CAPABILITY-MISSING", "External links require open_url.");
7293
7293
  props.action = { actionId: "capability.open_url.open", args: { url: attrs.href } };
7294
7294
  } else error(el2, "HHP-LINK", "Links require #screenId or an HTTPS URL.");
@@ -7419,7 +7419,7 @@ ${directService.source}
7419
7419
  bundleName,
7420
7420
  sourceMap: sourceMap2,
7421
7421
  dependencies,
7422
- manifest: { profileVersion: "hhp/2", catalogVersion: profile.catalogVersion, screenId, bundleName, serviceName: info4 ? serviceName : null, requiredCapabilities: [...requiredCapabilities].sort(), inputSha256: createHash6("sha256").update(html).digest("hex") }
7422
+ manifest: { profileVersion: "hhp/2", catalogVersion: profile.catalogVersion, screenId, bundleName, serviceName: info4 ? serviceName : null, requiredCapabilities: [...requiredCapabilities2].sort(), inputSha256: createHash6("sha256").update(html).digest("hex") }
7423
7423
  };
7424
7424
  }
7425
7425
  var camel, semantic, actionAliases, ajv, validators;
@@ -8078,7 +8078,6 @@ function resolveTranslation(value, catalog3, sourceCatalog) {
8078
8078
  var BINDING_SIGILS, BINDING_SIGIL_RE, TRANSLATION_PREFIX, TRANSLATION_KEY_RE;
8079
8079
  var init_bindings = __esm({
8080
8080
  "../../core/ts/src/bindings.ts"() {
8081
- "use strict";
8082
8081
  BINDING_SIGILS = ["service", "flag", "item", "t"];
8083
8082
  BINDING_SIGIL_RE = new RegExp(
8084
8083
  `^\\$(?:${BINDING_SIGILS.filter((s2) => s2 !== "t").join("|")})\\b|^\\$t:`
@@ -15051,7 +15050,7 @@ async function compileContentHhp(root, opts = {}) {
15051
15050
  seen.add(file3);
15052
15051
  const source = readFileSync16(file3, "utf8");
15053
15052
  inspectModule(source, file3, profile, diagnostics);
15054
- for (const cap of moduleCapabilities(source, file3, profile)) requiredCapabilities.add(cap);
15053
+ for (const cap of moduleCapabilities(source, file3, profile)) requiredCapabilities2.add(cap);
15055
15054
  const sf2 = ts3.createSourceFile(file3, source, ts3.ScriptTarget.Latest, true);
15056
15055
  for (const stmt of sf2.statements) if ((ts3.isImportDeclaration(stmt) || ts3.isExportDeclaration(stmt)) && stmt.moduleSpecifier && ts3.isStringLiteral(stmt.moduleSpecifier) && stmt.moduleSpecifier.text.startsWith(".")) {
15057
15056
  let imported = localPath(root, stmt.moduleSpecifier.text, file3);
@@ -15066,9 +15065,9 @@ async function compileContentHhp(root, opts = {}) {
15066
15065
  diagnostics.push(...result2.diagnostics);
15067
15066
  results.push(result2);
15068
15067
  const seen = /* @__PURE__ */ new Set();
15069
- const requiredCapabilities = new Set(result2.manifest.requiredCapabilities);
15068
+ const requiredCapabilities2 = new Set(result2.manifest.requiredCapabilities);
15070
15069
  for (const dependency of result2.dependencies.filter((f2) => /\.tsx?$/.test(f2))) inspect2(dependency);
15071
- result2.manifest.requiredCapabilities = [...requiredCapabilities].sort();
15070
+ result2.manifest.requiredCapabilities = [...requiredCapabilities2].sort();
15072
15071
  outputs.set(`dev/hhp/${id}.descriptor.json`, JSON.stringify(result2.descriptor, null, 2) + "\n");
15073
15072
  outputs.set(`dev/hhp/${id}.map.json`, JSON.stringify({ ...result2.manifest, nodes: Object.fromEntries(Object.entries(result2.sourceMap).map(([key3, node]) => [key3, { ...node, file: relative3(root, node.file).split(sep6).join("/") }])) }, null, 2) + "\n");
15074
15073
  if (result2.worker && result2.bundleName) {
@@ -17852,6 +17851,7 @@ function resolveIdiom(tokens2, ctx = {}) {
17852
17851
  var IDIOM_CONTEXTS, UNSAFE_KEYS;
17853
17852
  var init_idiom = __esm({
17854
17853
  "../../core/ts/src/idiom.ts"() {
17854
+ "use strict";
17855
17855
  IDIOM_CONTEXTS = {
17856
17856
  ios: { native: ["ios"] },
17857
17857
  android: { native: ["material"] },
@@ -22774,6 +22774,9 @@ var init_plan = __esm({
22774
22774
  });
22775
22775
 
22776
22776
  // ../../core/sdk/capability-catalog/src/builtins.ts
22777
+ function hasBuiltin(platform, id) {
22778
+ return CAPABILITY_BUILTINS[platform].includes(id);
22779
+ }
22777
22780
  var IOS_BUILTINS, WEB_BUILTINS, CAPABILITY_BUILTINS;
22778
22781
  var init_builtins = __esm({
22779
22782
  "../../core/sdk/capability-catalog/src/builtins.ts"() {
@@ -23282,6 +23285,10 @@ function binaryCoreSatellitePath(product) {
23282
23285
  function mavenArtifactForGradleProject(project) {
23283
23286
  return project.replace(/^:/, "");
23284
23287
  }
23288
+ function binaryModuleLinks(buildSet2) {
23289
+ const { spm, gradle } = modulesFor(buildSet2);
23290
+ return { spm, maven: gradle.map((g2) => `${BINARY_CORE_MAVEN_GROUP}:${mavenArtifactForGradleProject(g2)}`) };
23291
+ }
23285
23292
  function coreVersionHasModules(version4) {
23286
23293
  const parse4 = (v2) => /^(\d+)\.(\d+)\.(\d+)/.exec(v2.trim())?.slice(1).map(Number);
23287
23294
  const a2 = parse4(version4);
@@ -23340,6 +23347,58 @@ var init_binary_core = __esm({
23340
23347
  });
23341
23348
 
23342
23349
  // ../../core/sdk/capability-catalog/src/index.ts
23350
+ var src_exports = {};
23351
+ __export(src_exports, {
23352
+ BINARY_CORE_ANDROID_PUBLISH_BUILDS: () => BINARY_CORE_ANDROID_PUBLISH_BUILDS,
23353
+ BINARY_CORE_IOS_PATH_ONLY_SATELLITES: () => BINARY_CORE_IOS_PATH_ONLY_SATELLITES,
23354
+ BINARY_CORE_IOS_SATELLITES: () => BINARY_CORE_IOS_SATELLITES,
23355
+ BINARY_CORE_IOS_TRAIT_SATELLITES: () => BINARY_CORE_IOS_TRAIT_SATELLITES,
23356
+ BINARY_CORE_IOS_TRAIT_SELECTOR: () => BINARY_CORE_IOS_TRAIT_SELECTOR,
23357
+ BINARY_CORE_MAVEN_GROUP: () => BINARY_CORE_MAVEN_GROUP,
23358
+ BINARY_CORE_MODULES_SINCE: () => BINARY_CORE_MODULES_SINCE,
23359
+ BINARY_CORE_PRE_SPLIT_USAGE_STRINGS: () => BINARY_CORE_PRE_SPLIT_USAGE_STRINGS,
23360
+ BINARY_CORE_SPM_PACKAGE: () => BINARY_CORE_SPM_PACKAGE,
23361
+ BINARY_CORE_USAGE_STRINGS: () => BINARY_CORE_USAGE_STRINGS,
23362
+ BRIDGE_CAPABILITY_IDS: () => BRIDGE_CAPABILITY_IDS,
23363
+ CAPABILITY_BUILTINS: () => CAPABILITY_BUILTINS,
23364
+ CAPABILITY_IMPLIES: () => CAPABILITY_IMPLIES,
23365
+ CAPABILITY_NATIVE: () => CAPABILITY_NATIVE,
23366
+ CATALOG_CAPABILITY_IDS: () => CATALOG_CAPABILITY_IDS,
23367
+ COMPONENT_MODULES: () => COMPONENT_MODULES,
23368
+ KNOWN_CAPABILITY_IDS: () => KNOWN_CAPABILITY_IDS,
23369
+ NATIVE_MODULES: () => NATIVE_MODULES,
23370
+ NOT_DIST_ADDABLE: () => NOT_DIST_ADDABLE,
23371
+ androidPlanFor: () => androidPlanFor,
23372
+ binaryCoreSatellitePath: () => binaryCoreSatellitePath,
23373
+ binaryCoreSatelliteTrait: () => binaryCoreSatelliteTrait,
23374
+ binaryCoreUsageStringsFor: () => binaryCoreUsageStringsFor,
23375
+ binaryCoreVersion: () => binaryCoreVersion,
23376
+ binaryModuleLinks: () => binaryModuleLinks,
23377
+ capabilitiesOfModule: () => capabilitiesOfModule,
23378
+ capabilitiesUsedBy: () => capabilitiesUsedBy,
23379
+ capabilityDispatchesIn: () => capabilityDispatchesIn,
23380
+ catalogKey: () => catalogKey,
23381
+ componentModuleRequirements: () => componentModuleRequirements,
23382
+ coreVersionHasModules: () => coreVersionHasModules,
23383
+ distAddRefusal: () => distAddRefusal,
23384
+ distributionOptIns: () => distributionOptIns,
23385
+ expandCapabilities: () => expandCapabilities,
23386
+ grants: () => grants,
23387
+ hasBuiltin: () => hasBuiltin,
23388
+ iosPlanFor: () => iosPlanFor,
23389
+ isBinaryCore: () => isBinaryCore,
23390
+ isKnownCapability: () => isKnownCapability,
23391
+ linksModule: () => linksModule,
23392
+ macosPlanFor: () => macosPlanFor,
23393
+ mavenArtifactForGradleProject: () => mavenArtifactForGradleProject,
23394
+ moduleForGradleProject: () => moduleForGradleProject,
23395
+ moduleForSpmProduct: () => moduleForSpmProduct,
23396
+ modulesFor: () => modulesFor,
23397
+ nativeRequirementsFor: () => nativeRequirementsFor,
23398
+ needsNativeBuild: () => needsNativeBuild,
23399
+ unlinkedComponentModules: () => unlinkedComponentModules,
23400
+ usageStringCopyFor: () => usageStringCopyFor
23401
+ });
23343
23402
  var init_src2 = __esm({
23344
23403
  "../../core/sdk/capability-catalog/src/index.ts"() {
23345
23404
  init_types();
@@ -131112,8 +131171,8 @@ var init_AppRegistry = __esm({
131112
131171
  });
131113
131172
 
131114
131173
  // ../../core/web-renderer/src/index.ts
131115
- var src_exports = {};
131116
- __export(src_exports, {
131174
+ var src_exports2 = {};
131175
+ __export(src_exports2, {
131117
131176
  Bound: () => Bound,
131118
131177
  BoundList: () => BoundList,
131119
131178
  FakeServiceProvider: () => FakeServiceProvider,
@@ -131175,12 +131234,14 @@ __export(render_descriptor_exports, {
131175
131234
  clientEnvFor: () => clientEnvFor,
131176
131235
  collectNativeOnly: () => collectNativeOnly,
131177
131236
  isMissingPlaywrightExecutable: () => isMissingPlaywrightExecutable,
131237
+ launchPngBrowser: () => launchPngBrowser,
131178
131238
  nodeHidden: () => nodeHidden,
131179
131239
  pngAvailable: () => pngAvailable,
131180
131240
  pngPageHtml: () => pngPageHtml,
131181
131241
  renderDescriptorWeb: () => renderDescriptorWeb,
131182
131242
  renderPngBatch: () => renderPngBatch,
131183
131243
  renderSchemeFor: () => renderSchemeFor,
131244
+ setPngEgress: () => setPngEgress,
131184
131245
  setPngFontCss: () => setPngFontCss,
131185
131246
  stabilizePngPage: () => stabilizePngPage,
131186
131247
  withRetry: () => withRetry2
@@ -131474,7 +131535,7 @@ async function renderDescriptorWeb(descriptor, tokens2, scheme, wantPng, service
131474
131535
  ) : void 0;
131475
131536
  let html;
131476
131537
  try {
131477
- const { renderDescriptorToHtml: renderDescriptorToHtml2 } = await Promise.resolve().then(() => (init_src5(), src_exports));
131538
+ const { renderDescriptorToHtml: renderDescriptorToHtml2 } = await Promise.resolve().then(() => (init_src5(), src_exports2));
131478
131539
  html = renderDescriptorToHtml2(
131479
131540
  payload,
131480
131541
  tokens2 ?? {},
@@ -131571,8 +131632,24 @@ async function stabilizePngPage(page, scheme) {
131571
131632
  await new Promise((resolve66) => browser.requestAnimationFrame(() => browser.requestAnimationFrame(() => resolve66())));
131572
131633
  }, scheme);
131573
131634
  }
131574
- async function renderPngBatch(items) {
131575
- if (!items.length) return [];
131635
+ function setPngEgress(start) {
131636
+ const prev = pngEgress;
131637
+ pngEgress = start;
131638
+ return prev;
131639
+ }
131640
+ async function pngEgressStart() {
131641
+ if (pngEgress !== void 0) return pngEgress;
131642
+ const spec = "./png-egress.js";
131643
+ try {
131644
+ return (await import(
131645
+ /* @vite-ignore */
131646
+ spec
131647
+ )).startEgressProxy;
131648
+ } catch {
131649
+ return null;
131650
+ }
131651
+ }
131652
+ async function launchPngBrowser() {
131576
131653
  let chromium;
131577
131654
  try {
131578
131655
  ({ chromium } = await import("playwright"));
@@ -131580,13 +131657,41 @@ async function renderPngBatch(items) {
131580
131657
  return null;
131581
131658
  }
131582
131659
  if (!chromium) return null;
131660
+ const start = await pngEgressStart();
131661
+ if (!start) return null;
131662
+ let egress;
131663
+ try {
131664
+ egress = await start();
131665
+ } catch {
131666
+ return null;
131667
+ }
131583
131668
  const launch = chromium.launch.bind(chromium);
131584
131669
  let browser;
131585
131670
  try {
131586
- browser = await withRetry2(() => launch(), 3);
131671
+ browser = await withRetry2(() => launch({
131672
+ proxy: { server: egress.url, bypass: "<-loopback>" },
131673
+ args: ["--force-webrtc-ip-handling-policy=disable_non_proxied_udp", "--webrtc-ip-handling-policy=disable_non_proxied_udp"]
131674
+ }), 3);
131587
131675
  } catch {
131676
+ await egress.close().catch(() => {
131677
+ });
131588
131678
  return null;
131589
131679
  }
131680
+ return {
131681
+ browser,
131682
+ close: async () => {
131683
+ await browser.close().catch(() => {
131684
+ });
131685
+ await egress.close().catch(() => {
131686
+ });
131687
+ }
131688
+ };
131689
+ }
131690
+ async function renderPngBatch(items) {
131691
+ if (!items.length) return [];
131692
+ const launched = await launchPngBrowser();
131693
+ if (!launched) return null;
131694
+ const { browser } = launched;
131590
131695
  try {
131591
131696
  const out2 = [];
131592
131697
  for (const it2 of items) {
@@ -131607,11 +131712,10 @@ async function renderPngBatch(items) {
131607
131712
  }
131608
131713
  return out2;
131609
131714
  } finally {
131610
- await browser.close().catch(() => {
131611
- });
131715
+ await launched.close();
131612
131716
  }
131613
131717
  }
131614
- var A11Y_MODIFIER_KEYS, TREE_TEXT_VALUE_PROPS, asStringOrNumber, asStringOnly, asOtpCode, VALUE_PATH_TEXT, MEDIA_PLAYER_QUEUE_PROPERTIES, NATIVE_ONLY_TYPES, HOST_PAGE_FONT, pngFontCss;
131718
+ var A11Y_MODIFIER_KEYS, TREE_TEXT_VALUE_PROPS, asStringOrNumber, asStringOnly, asOtpCode, VALUE_PATH_TEXT, MEDIA_PLAYER_QUEUE_PROPERTIES, NATIVE_ONLY_TYPES, HOST_PAGE_FONT, pngFontCss, pngEgress;
131615
131719
  var init_render_descriptor = __esm({
131616
131720
  "../../core/dev-server/src/render-descriptor.ts"() {
131617
131721
  init_tokens2();
@@ -131655,6 +131759,229 @@ var init_render_descriptor = __esm({
131655
131759
  }
131656
131760
  });
131657
131761
 
131762
+ // ../../core/dev-server/src/png-egress.ts
131763
+ import http from "node:http";
131764
+ import net from "node:net";
131765
+ import { lookup as lookup2 } from "node:dns/promises";
131766
+ function isPublicAddress(ip) {
131767
+ const family = net.isIP(ip);
131768
+ if (family === 0) return false;
131769
+ return family === 4 ? !BLOCKED_V4.check(ip, "ipv4") : !BLOCKED_V6.check(ip, "ipv6");
131770
+ }
131771
+ function originKey(origin) {
131772
+ try {
131773
+ const u2 = new URL(origin);
131774
+ return hostPortKey(u2.hostname, u2.port || (u2.protocol === "https:" ? "443" : "80"));
131775
+ } catch {
131776
+ return null;
131777
+ }
131778
+ }
131779
+ function hostPortKey(host, port) {
131780
+ return `${host.replace(/^\[|\]$/g, "").replace(/\.$/, "").toLowerCase()}:${port}`;
131781
+ }
131782
+ function allowPngOrigins(origins) {
131783
+ const keys = origins.map(originKey).filter((k2) => !!k2);
131784
+ for (const k2 of keys) allowedOrigins.set(k2, (allowedOrigins.get(k2) ?? 0) + 1);
131785
+ return () => {
131786
+ for (const k2 of keys) {
131787
+ const n2 = (allowedOrigins.get(k2) ?? 1) - 1;
131788
+ if (n2 <= 0) allowedOrigins.delete(k2);
131789
+ else allowedOrigins.set(k2, n2);
131790
+ }
131791
+ };
131792
+ }
131793
+ function ownOrigins(port, publicBase) {
131794
+ return [`http://localhost:${port}`, `http://127.0.0.1:${port}`, `http://[::1]:${port}`, ...publicBase ? [publicBase] : []];
131795
+ }
131796
+ async function resolveEgress(host, port, allow = allowedOrigins) {
131797
+ const bare = host.replace(/^\[|\]$/g, "");
131798
+ let addrs;
131799
+ try {
131800
+ addrs = net.isIP(bare) ? [{ address: bare, family: net.isIP(bare) }] : await lookup2(bare.replace(/\.$/, ""), { all: true, order: "ipv4first" });
131801
+ } catch {
131802
+ return null;
131803
+ }
131804
+ if (!addrs.length) return null;
131805
+ if (allow.has(hostPortKey(host, port))) return addrs;
131806
+ return addrs.every((a2) => isPublicAddress(a2.address)) ? addrs : null;
131807
+ }
131808
+ function connectFirst(addrs, port, track) {
131809
+ return new Promise((resolve66, reject) => {
131810
+ const tryAt = (i2) => {
131811
+ if (i2 >= addrs.length) return reject(new Error("no address answered"));
131812
+ const s2 = net.connect({ host: addrs[i2].address, family: addrs[i2].family, port });
131813
+ track(s2);
131814
+ s2.setTimeout(CONNECT_TIMEOUT_MS, () => s2.destroy(new Error("connect timeout")));
131815
+ const onFail = () => {
131816
+ s2.destroy();
131817
+ tryAt(i2 + 1);
131818
+ };
131819
+ s2.once("error", onFail);
131820
+ s2.once("connect", () => {
131821
+ s2.off("error", onFail);
131822
+ s2.setTimeout(0);
131823
+ resolve66(s2);
131824
+ });
131825
+ };
131826
+ tryAt(0);
131827
+ });
131828
+ }
131829
+ async function startEgressProxy() {
131830
+ const blocked = [];
131831
+ const sockets = /* @__PURE__ */ new Set();
131832
+ const track = (s2) => {
131833
+ sockets.add(s2);
131834
+ s2.once("close", () => sockets.delete(s2));
131835
+ };
131836
+ const guard = (s2) => {
131837
+ track(s2);
131838
+ s2.on("error", () => s2.destroy());
131839
+ };
131840
+ const server = http.createServer(async (req, res) => {
131841
+ let u2;
131842
+ try {
131843
+ u2 = new URL(req.url ?? "");
131844
+ } catch {
131845
+ res.writeHead(400).end();
131846
+ return;
131847
+ }
131848
+ const port2 = Number(u2.port || 80);
131849
+ const addrs = u2.protocol === "http:" ? await resolveEgress(u2.hostname, port2) : null;
131850
+ if (!addrs) {
131851
+ blocked.push(hostPortKey(u2.hostname, port2));
131852
+ res.writeHead(403).end();
131853
+ return;
131854
+ }
131855
+ let sock;
131856
+ try {
131857
+ sock = await connectFirst(addrs, port2, guard);
131858
+ } catch {
131859
+ res.writeHead(502).end();
131860
+ return;
131861
+ }
131862
+ const headers2 = { ...req.headers, host: u2.host };
131863
+ delete headers2["proxy-connection"];
131864
+ const upstream = http.request({ createConnection: () => sock, method: req.method, path: `${u2.pathname}${u2.search}`, headers: headers2 }, (up) => {
131865
+ res.writeHead(up.statusCode ?? 502, up.headers);
131866
+ up.pipe(res);
131867
+ });
131868
+ upstream.on("error", () => {
131869
+ if (!res.headersSent) res.writeHead(502);
131870
+ res.end();
131871
+ });
131872
+ req.pipe(upstream);
131873
+ });
131874
+ server.on("connection", guard);
131875
+ server.on("clientError", (_e2, socket) => socket.destroy());
131876
+ server.on("connect", async (req, socket, head) => {
131877
+ guard(socket);
131878
+ const m2 = /^\[?([^\]]+?)\]?:(\d+)$/.exec(req.url ?? "");
131879
+ const addrs = m2 ? await resolveEgress(m2[1], Number(m2[2])) : null;
131880
+ if (!m2 || !addrs) {
131881
+ blocked.push(m2 ? hostPortKey(m2[1], m2[2]) : String(req.url));
131882
+ socket.end("HTTP/1.1 403 Forbidden\r\n\r\n");
131883
+ return;
131884
+ }
131885
+ let upstream;
131886
+ try {
131887
+ upstream = await connectFirst(addrs, Number(m2[2]), guard);
131888
+ } catch {
131889
+ socket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
131890
+ return;
131891
+ }
131892
+ socket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
131893
+ if (head.length) upstream.write(head);
131894
+ upstream.pipe(socket);
131895
+ socket.pipe(upstream);
131896
+ });
131897
+ server.on("upgrade", async (req, socket, head) => {
131898
+ guard(socket);
131899
+ let u2;
131900
+ try {
131901
+ u2 = new URL(req.url ?? "");
131902
+ } catch {
131903
+ socket.destroy();
131904
+ return;
131905
+ }
131906
+ const port2 = Number(u2.port || 80);
131907
+ const addrs = await resolveEgress(u2.hostname, port2);
131908
+ if (!addrs) {
131909
+ blocked.push(hostPortKey(u2.hostname, port2));
131910
+ socket.end("HTTP/1.1 403 Forbidden\r\n\r\n");
131911
+ return;
131912
+ }
131913
+ let upstream;
131914
+ try {
131915
+ upstream = await connectFirst(addrs, port2, guard);
131916
+ } catch {
131917
+ socket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
131918
+ return;
131919
+ }
131920
+ const lines = [`${req.method} ${u2.pathname}${u2.search} HTTP/1.1`];
131921
+ for (let i2 = 0; i2 < req.rawHeaders.length; i2 += 2) lines.push(`${req.rawHeaders[i2]}: ${req.rawHeaders[i2 + 1]}`);
131922
+ upstream.write(`${lines.join("\r\n")}\r
131923
+ \r
131924
+ `);
131925
+ if (head.length) upstream.write(head);
131926
+ upstream.pipe(socket);
131927
+ socket.pipe(upstream);
131928
+ });
131929
+ await new Promise((resolve66, reject) => {
131930
+ server.once("error", reject);
131931
+ server.listen(0, "127.0.0.1", () => resolve66());
131932
+ });
131933
+ const { port } = server.address();
131934
+ return {
131935
+ url: `http://127.0.0.1:${port}`,
131936
+ blocked,
131937
+ close: () => new Promise((resolve66) => {
131938
+ server.close(() => resolve66());
131939
+ for (const s2 of sockets) s2.destroy();
131940
+ server.closeAllConnections?.();
131941
+ })
131942
+ };
131943
+ }
131944
+ var BLOCKED_V4, BLOCKED_V6, CONNECT_TIMEOUT_MS, allowedOrigins;
131945
+ var init_png_egress = __esm({
131946
+ "../../core/dev-server/src/png-egress.ts"() {
131947
+ BLOCKED_V4 = new net.BlockList();
131948
+ BLOCKED_V6 = new net.BlockList();
131949
+ for (const [addr, prefix] of [
131950
+ ["0.0.0.0", 8],
131951
+ ["10.0.0.0", 8],
131952
+ ["100.64.0.0", 10],
131953
+ ["127.0.0.0", 8],
131954
+ ["169.254.0.0", 16],
131955
+ ["172.16.0.0", 12],
131956
+ ["192.0.0.0", 24],
131957
+ ["192.0.2.0", 24],
131958
+ ["192.88.99.0", 24],
131959
+ ["192.168.0.0", 16],
131960
+ ["198.18.0.0", 15],
131961
+ ["198.51.100.0", 24],
131962
+ ["203.0.113.0", 24],
131963
+ ["224.0.0.0", 4],
131964
+ ["240.0.0.0", 4]
131965
+ ]) BLOCKED_V4.addSubnet(addr, prefix, "ipv4");
131966
+ for (const [addr, prefix] of [
131967
+ ["::", 96],
131968
+ ["::ffff:0:0", 96],
131969
+ ["64:ff9b::", 96],
131970
+ ["64:ff9b:1::", 48],
131971
+ ["100::", 64],
131972
+ ["2001::", 23],
131973
+ ["2001:db8::", 32],
131974
+ ["2002::", 16],
131975
+ ["fc00::", 7],
131976
+ ["fe80::", 10],
131977
+ ["fec0::", 10],
131978
+ ["ff00::", 8]
131979
+ ]) BLOCKED_V6.addSubnet(addr, prefix, "ipv6");
131980
+ CONNECT_TIMEOUT_MS = 1e4;
131981
+ allowedOrigins = /* @__PURE__ */ new Map();
131982
+ }
131983
+ });
131984
+
131658
131985
  // ../../core/dev-server/src/render-fonts.ts
131659
131986
  import { existsSync as existsSync45, readFileSync as readFileSync49 } from "node:fs";
131660
131987
  import { extname, join as join44 } from "node:path";
@@ -131744,7 +132071,7 @@ async function pngFontFaceCss(tokens2, opts = {}) {
131744
132071
  return rules.join("");
131745
132072
  }
131746
132073
  async function idiomFontFaceCss(idiomContext) {
131747
- const { controlIdiomFor: controlIdiomFor2, idiomControls: idiomControls2 } = await Promise.resolve().then(() => (init_src5(), src_exports));
132074
+ const { controlIdiomFor: controlIdiomFor2, idiomControls: idiomControls2 } = await Promise.resolve().then(() => (init_src5(), src_exports2));
131748
132075
  const bundled = idiomControls2(controlIdiomFor2(idiomContext)).font.bundled;
131749
132076
  if (!bundled) return "";
131750
132077
  try {
@@ -136245,6 +136572,17 @@ function createDevServer(opts = {}) {
136245
136572
  })();
136246
136573
  };
136247
136574
  const httpServer = createServer3(handleRequest);
136575
+ let disallowOwnOrigins;
136576
+ httpServer.on("listening", () => {
136577
+ const addr = httpServer.address();
136578
+ if (!addr || typeof addr === "string") return;
136579
+ disallowOwnOrigins?.();
136580
+ disallowOwnOrigins = allowPngOrigins(ownOrigins(addr.port, process.env.HIMALAYA_DEV_PUBLIC_URL));
136581
+ });
136582
+ httpServer.on("close", () => {
136583
+ disallowOwnOrigins?.();
136584
+ disallowOwnOrigins = void 0;
136585
+ });
136248
136586
  httpServer.inject = (request) => injectRequest(handleRequest, request);
136249
136587
  httpServer.on("clientError", (err, socket) => {
136250
136588
  console.error(`[himalaya-dev] malformed request rejected by the HTTP parser: ${err.code ?? ""} ${err.message}`);
@@ -136320,6 +136658,7 @@ var init_src6 = __esm({
136320
136658
  init_check();
136321
136659
  init_openapi();
136322
136660
  init_render_descriptor();
136661
+ init_png_egress();
136323
136662
  init_render_fonts();
136324
136663
  init_app_config();
136325
136664
  init_tier();
@@ -136331,6 +136670,7 @@ var init_src6 = __esm({
136331
136670
  init_descriptor_postprocess();
136332
136671
  ROOT6 = resolve38(fileURLToPath22(new URL("../../..", import.meta.url)));
136333
136672
  setPngFontCss(async (tokens2, idiomContext) => await pngFontFaceCss(tokens2) + (idiomContext ? await idiomFontFaceCss(idiomContext) : ""));
136673
+ setPngEgress(startEgressProxy);
136334
136674
  TIER3_ROOT = resolve38(ROOT6, "core/dev-server/web");
136335
136675
  TIER4_ROOT = resolve38(ROOT6, "core/dev-server/tier4-bundles");
136336
136676
  tier4Verifier = new TierFourVerifier(TIER4_ROOT);
@@ -140282,7 +140622,7 @@ async function buildFromLoaded(preloaded, dest, opts = {}) {
140282
140622
  };
140283
140623
  let handlers;
140284
140624
  let minBinaryVersion = null;
140285
- const requiredCapabilities = new Set(config.capabilities ?? []);
140625
+ const requiredCapabilities2 = new Set(config.capabilities ?? []);
140286
140626
  const capabilityUsage = {
140287
140627
  app: [...config.capabilities ?? []],
140288
140628
  ...Array.isArray(config.capabilityEnvelope) ? { envelope: [...config.capabilityEnvelope] } : {},
@@ -140293,7 +140633,7 @@ async function buildFromLoaded(preloaded, dest, opts = {}) {
140293
140633
  const ids = Array.isArray(declared) ? declared.filter((c2) => typeof c2 === "string") : [];
140294
140634
  const components2 = [...collectComponentTypes(descriptor)].sort();
140295
140635
  capabilityUsage.screens.push({ screenId, bundle, declared: ids, components: components2 });
140296
- for (const c2 of componentModuleRequirements(components2, [...requiredCapabilities, ...ids])) requiredCapabilities.add(c2);
140636
+ for (const c2 of componentModuleRequirements(components2, [...requiredCapabilities2, ...ids])) requiredCapabilities2.add(c2);
140297
140637
  };
140298
140638
  const requiredNativeComponents = /* @__PURE__ */ new Set();
140299
140639
  const tierScreens = [];
@@ -140537,7 +140877,7 @@ async function buildFromLoaded(preloaded, dest, opts = {}) {
140537
140877
  minBinaryVersion = maximumBinaryVersion(minBinaryVersion, lb.minBinaryVersion);
140538
140878
  const declared = lb.declaredCapabilities ?? lb.declared_capabilities;
140539
140879
  if (Array.isArray(declared)) {
140540
- for (const capability of declared) if (typeof capability === "string") requiredCapabilities.add(capability);
140880
+ for (const capability of declared) if (typeof capability === "string") requiredCapabilities2.add(capability);
140541
140881
  }
140542
140882
  noteScreenCapabilities(screenId, bundleName, declared, descriptor);
140543
140883
  } else {
@@ -140687,7 +141027,7 @@ async function buildFromLoaded(preloaded, dest, opts = {}) {
140687
141027
  minBinaryVersion = maximumBinaryVersion(minBinaryVersion, lb.minBinaryVersion);
140688
141028
  const declared = lb.declaredCapabilities ?? lb.declared_capabilities;
140689
141029
  if (Array.isArray(declared)) {
140690
- for (const c2 of declared) if (typeof c2 === "string") requiredCapabilities.add(c2);
141030
+ for (const c2 of declared) if (typeof c2 === "string") requiredCapabilities2.add(c2);
140691
141031
  }
140692
141032
  noteScreenCapabilities(screenId, bundleName, declared, descriptor);
140693
141033
  } else {
@@ -140855,7 +141195,7 @@ async function buildFromLoaded(preloaded, dest, opts = {}) {
140855
141195
  // whose source catalog serves copy (native screens, #4221/#4250) now carries `en` here.
140856
141196
  ...Object.keys(catalogs).length ? { catalogs } : {},
140857
141197
  playerRequirements: {
140858
- capabilities: [...requiredCapabilities].sort(),
141198
+ capabilities: [...requiredCapabilities2].sort(),
140859
141199
  nativeComponents: [...requiredNativeComponents].sort(),
140860
141200
  // `none` is the scaffold's spelling of "no auth" (`new-app --auth=none`), not a provider a
140861
141201
  // host must carry — recording it blocked every such app in the Player picker (#3695).
@@ -150220,7 +150560,7 @@ function defineAppLiteral(file2) {
150220
150560
  return void 0;
150221
150561
  }
150222
150562
  function loginOf(app) {
150223
- const auth = lookup2(app, "auth");
150563
+ const auth = lookup3(app, "auth");
150224
150564
  if (auth.kind !== "written") return { kind: auth.kind === "absent" ? "other" : "opaque" };
150225
150565
  const value = unwrap(auth.node.initializer);
150226
150566
  if (ts5.isObjectLiteralExpression(value)) return { kind: "other" };
@@ -150248,7 +150588,7 @@ function unwrap(node) {
150248
150588
  }
150249
150589
  return current;
150250
150590
  }
150251
- function lookup2(literal, name) {
150591
+ function lookup3(literal, name) {
150252
150592
  let found = { kind: "absent" };
150253
150593
  for (const p2 of literal.properties) {
150254
150594
  const key3 = p2.name && (ts5.isIdentifier(p2.name) || ts5.isStringLiteral(p2.name)) ? p2.name.text : void 0;
@@ -150259,7 +150599,7 @@ function lookup2(literal, name) {
150259
150599
  }
150260
150600
  function stringValue(node, name) {
150261
150601
  const literal = unwrap(node);
150262
- const found = ts5.isObjectLiteralExpression(literal) ? lookup2(literal, name) : void 0;
150602
+ const found = ts5.isObjectLiteralExpression(literal) ? lookup3(literal, name) : void 0;
150263
150603
  const value = found?.kind === "written" ? found.node.initializer : void 0;
150264
150604
  return value && ts5.isStringLiteralLike(value) ? value.text : void 0;
150265
150605
  }
@@ -150371,7 +150711,7 @@ function enablePushInConfig(source, options) {
150371
150711
  };
150372
150712
  }
150373
150713
  function planPush(app, pingAppName, plan) {
150374
- const push = lookup2(app, "push");
150714
+ const push = lookup3(app, "push");
150375
150715
  if (push.kind === "absent") {
150376
150716
  plan.appended.push([
150377
150717
  ROUTES_COMMENT,
@@ -150402,7 +150742,7 @@ function planPush(app, pingAppName, plan) {
150402
150742
  }
150403
150743
  function planRetiredFamily(block, plan) {
150404
150744
  const literal = unwrap(block);
150405
- const found = ts6.isObjectLiteralExpression(literal) ? lookup2(literal, "pingAppName") : void 0;
150745
+ const found = ts6.isObjectLiteralExpression(literal) ? lookup3(literal, "pingAppName") : void 0;
150406
150746
  const value = found?.kind === "written" ? unwrap(found.node.initializer) : void 0;
150407
150747
  const replacement = value && ts6.isStringLiteralLike(value) ? replacementPingFamily(value.text) : void 0;
150408
150748
  if (!value || !replacement) return;
@@ -150410,7 +150750,7 @@ function planRetiredFamily(block, plan) {
150410
150750
  plan.applied.push(`pingAppName ${JSON.stringify(replacement)} in place of ${value.getText()}`);
150411
150751
  }
150412
150752
  function planCapability(app, plan) {
150413
- const capabilities = lookup2(app, "capabilities");
150753
+ const capabilities = lookup3(app, "capabilities");
150414
150754
  if (capabilities.kind === "absent") {
150415
150755
  plan.appended.push(['capabilities: ["push"],']);
150416
150756
  plan.applied.push("push capability");
@@ -150440,7 +150780,7 @@ function planAuthSite(login, file2, site, plan) {
150440
150780
  );
150441
150781
  return;
150442
150782
  }
150443
- const siteId = lookup2(login.literal, "siteId");
150783
+ const siteId = lookup3(login.literal, "siteId");
150444
150784
  if (siteId.kind === "absent") {
150445
150785
  plan.splices.push(appendProperties(login.literal, file2, [`${wanted},`]));
150446
150786
  plan.applied.push("auth siteId");
@@ -150527,8 +150867,8 @@ async function ask(question, deps) {
150527
150867
  function defer(dir, input, scope, reason) {
150528
150868
  return recordPushDeferral(dir, { reason, scope, missing: missingPushInputs(input, scope), identity: input, resume: resume(dir) });
150529
150869
  }
150530
- function renderPushStatus(http, refresh = false) {
150531
- const body = http.body ?? {};
150870
+ function renderPushStatus(http2, refresh = false) {
150871
+ const body = http2.body ?? {};
150532
150872
  const verification = obj2(body.verification);
150533
150873
  const state = typeof body.state === "string" && STATES.has(body.state) ? body.state : "unknown";
150534
150874
  const mode = verification.mode === "live" ? "live" : verification.mode === "cached" ? "cached" : "unavailable";
@@ -150537,10 +150877,10 @@ function renderPushStatus(http, refresh = false) {
150537
150877
  return [name, typeof value === "string" && CHECK_STATES.has(value) ? value : "unknown"];
150538
150878
  }));
150539
150879
  const rawCode = typeof body.error === "string" ? body.error.split(":")[0] : void 0;
150540
- const responseError = http.error ?? (rawCode ? ERROR_CODES.has(rawCode) ? rawCode : "push_setup_failed" : void 0);
150541
- const error = responseError ?? (!http.ok ? "push_setup_failed" : refresh && mode !== "live" ? "live_verification_unavailable" : void 0);
150880
+ const responseError = http2.error ?? (rawCode ? ERROR_CODES.has(rawCode) ? rawCode : "push_setup_failed" : void 0);
150881
+ const error = responseError ?? (!http2.ok ? "push_setup_failed" : refresh && mode !== "live" ? "live_verification_unavailable" : void 0);
150542
150882
  return {
150543
- ok: http.ok && state === "ready" && !error && (!refresh || verification.backendConfigurationReady === true),
150883
+ ok: http2.ok && state === "ready" && !error && (!refresh || verification.backendConfigurationReady === true),
150544
150884
  state,
150545
150885
  verificationMode: mode,
150546
150886
  backendConfigurationReady: verification.backendConfigurationReady === true,
@@ -150552,8 +150892,8 @@ function renderPushStatus(http, refresh = false) {
150552
150892
  note: "Backend setup is separate from building the app, device registration and notification delivery."
150553
150893
  };
150554
150894
  }
150555
- function report(http, flags, io2, dir, config) {
150556
- const status = renderPushStatus(http, flags.refresh === true);
150895
+ function report(http2, flags, io2, dir, config) {
150896
+ const status = renderPushStatus(http2, flags.refresh === true);
150557
150897
  const result = config && !config.pushOn ? { ...status, ok: false, error: status.error ?? (config.rolledBack ? "config_edit_rolled_back" : "config_push_not_on") } : status;
150558
150898
  const local = readPushOnboarding(dir).push;
150559
150899
  io2.log(JSON.stringify({ ...result, ...local ? { localOnboarding: "deferred" } : {}, ...config ? { config } : {} }, null, 2));
@@ -154670,7 +155010,7 @@ async function runCrawl(opts) {
154670
155010
  for (const d2 of list) if (d2.screenId !== rep.screenId) coveredBy.set(d2.screenId, rep.screenId);
154671
155011
  }
154672
155012
  }
154673
- const net = opts.offline ? null : opts.netPath ? { rules: readNetRules(resolve51(opts.netPath)), from: opts.netPath } : defaultNetRules(opts.dir);
155013
+ const net2 = opts.offline ? null : opts.netPath ? { rules: readNetRules(resolve51(opts.netPath)), from: opts.netPath } : defaultNetRules(opts.dir);
154674
155014
  const suppressions = opts.config?.skip ?? [];
154675
155015
  const suppressed = [];
154676
155016
  const matchSuppression = (screen, actionId, componentId, invariant) => suppressions.find((s2) => (s2.screen === void 0 || s2.screen === screen) && (s2.action === void 0 || s2.action === actionId) && (s2.component === void 0 || s2.component === componentId) && (s2.invariant === void 0 || s2.invariant === invariant));
@@ -154698,7 +155038,7 @@ async function runCrawl(opts) {
154698
155038
  bundleMap: baked.bundleMap ?? null,
154699
155039
  knownScreenIds,
154700
155040
  ...opts.linkableScreenIds?.size ? { linkableScreenIds: opts.linkableScreenIds } : {},
154701
- rules: net?.rules ?? null,
155041
+ rules: net2?.rules ?? null,
154702
155042
  ...opts.cmsSeed && Object.keys(opts.cmsSeed).length ? { cmsSeed: opts.cmsSeed } : {},
154703
155043
  ...opts.canned?.count ? { canned: opts.canned.byKey } : {},
154704
155044
  ...localDbSchema?.length ? { localDbSchema } : {},
@@ -154840,7 +155180,7 @@ async function runCrawl(opts) {
154840
155180
  bundleJs: baked.bundleJs,
154841
155181
  bundleMap: baked.bundleMap ?? null,
154842
155182
  knownScreenIds,
154843
- rules: net?.rules ?? null,
155183
+ rules: net2?.rules ?? null,
154844
155184
  ...opts.cmsSeed && Object.keys(opts.cmsSeed).length ? { cmsSeed: opts.cmsSeed } : {},
154845
155185
  ...opts.canned?.count ? { canned: opts.canned.byKey } : {},
154846
155186
  ...localDbSchema?.length ? { localDbSchema } : {},
@@ -154907,7 +155247,7 @@ async function runCrawl(opts) {
154907
155247
  // so a run over static or bundle-less screens injects the dependency and replays nothing —
154908
155248
  // and the label then advertised "+ relations" beside `relations.checked: 0`. This is the
154909
155249
  // second time this line has overstated itself: it first claimed the axis unconditionally.
154910
- transport: describeTransport(net, opts, {
155250
+ transport: describeTransport(net2, opts, {
154911
155251
  relations: relationsChecked > 0,
154912
155252
  hostile: opts.hostile !== false && Boolean(deps.crawlScreenHostile),
154913
155253
  deep: Boolean(opts.deep && opts.deep >= 2 && deps.crawlScreenDeep)
@@ -154945,9 +155285,9 @@ async function runCrawl(opts) {
154945
155285
  ...results.some((r2) => r2.observations) ? { observations: Object.fromEntries(results.filter((r2) => r2.observations).map((r2) => [r2.screen, r2.observations])) } : {}
154946
155286
  };
154947
155287
  }
154948
- function describeTransport(net, opts, ran) {
155288
+ function describeTransport(net2, opts, ran) {
154949
155289
  const layers3 = [];
154950
- if (net) layers3.push(`mocked (${net.from})`);
155290
+ if (net2) layers3.push(`mocked (${net2.from})`);
154951
155291
  const cmsCount = Object.keys(opts.cmsSeed ?? {}).length;
154952
155292
  if (cmsCount) layers3.push(`cms (${cmsCount} seeded collection${cmsCount === 1 ? "" : "s"})`);
154953
155293
  if (opts.canned?.count) layers3.push(`canned (${opts.canned.count} frozen endpoints, ${opts.canned.from} \u2014 exercises code paths, not API-contract truth)`);
@@ -164024,7 +164364,7 @@ function siteFinding(app, siteId, fileName) {
164024
164364
  const check = `make sure the app's member login uses siteId ${JSON.stringify(siteId)}: a build bakes the purchases app only for the site its login resolves to.`;
164025
164365
  const login = loginOf(app);
164026
164366
  if (login.kind !== "member") return { instruction: `${fileName} does not write auth as a WixConsumerAuthProvider({ \u2026 }) call it can read, so ${check}` };
164027
- const found = lookup2(login.literal, "siteId");
164367
+ const found = lookup3(login.literal, "siteId");
164028
164368
  const value = found.kind === "written" ? unwrap(found.node.initializer) : void 0;
164029
164369
  if (!value || !ts8.isStringLiteralLike(value)) return { instruction: `auth's siteId is not written as a literal, so ${check}` };
164030
164370
  if (value.text.toLowerCase() === siteId.toLowerCase()) return {};
@@ -164035,7 +164375,7 @@ function bundleFinding(app, { bundleId, storeBundleId }, fileName) {
164035
164375
  if (storeBundleId === bundleId) return {};
164036
164376
  return { conflict: `store/store.config.yaml builds bundle "${storeBundleId}", but the purchases app is registered for "${bundleId}". ${fileName} was not edited.` };
164037
164377
  }
164038
- const found = lookup2(app, "bundleId");
164378
+ const found = lookup3(app, "bundleId");
164039
164379
  const value = found.kind === "written" ? unwrap(found.node.initializer) : void 0;
164040
164380
  if (!value || !ts8.isStringLiteralLike(value)) {
164041
164381
  return { instruction: `${fileName} does not write bundleId as a literal: make sure this app builds bundle ${JSON.stringify(bundleId)}, the one the purchases app is registered for.` };
@@ -164044,11 +164384,11 @@ function bundleFinding(app, { bundleId, storeBundleId }, fileName) {
164044
164384
  return { conflict: `${fileName} builds bundle "${value.text}", but the purchases app is registered for "${bundleId}". This is another app's config, so it was not edited.` };
164045
164385
  }
164046
164386
  function bindingFinding(app, file2, purchasesAppId, wanted, fileName) {
164047
- const wixIap = lookup2(app, "wixIap");
164387
+ const wixIap = lookup3(app, "wixIap");
164048
164388
  if (wixIap.kind === "absent") return { splice: appendProperties(app, file2, [`${wanted},`]), applied: "wixIap.purchasesAppId" };
164049
164389
  const literal = wixIap.kind === "written" ? unwrap(wixIap.node.initializer) : void 0;
164050
164390
  if (!literal || !ts8.isObjectLiteralExpression(literal)) return { instruction: `${fileName} computes wixIap, so it was not edited: it must evaluate to ${wanted}.` };
164051
- const current = lookup2(literal, "purchasesAppId");
164391
+ const current = lookup3(literal, "purchasesAppId");
164052
164392
  if (current.kind === "absent") return { splice: appendProperties(literal, file2, [`purchasesAppId: ${JSON.stringify(purchasesAppId)},`]), applied: "wixIap.purchasesAppId" };
164053
164393
  const value = current.kind === "written" ? unwrap(current.node.initializer) : void 0;
164054
164394
  if (!value || !ts8.isStringLiteralLike(value)) return { instruction: `${fileName} computes wixIap.purchasesAppId, so it was not edited: it must evaluate to ${JSON.stringify(purchasesAppId)}.` };
@@ -164056,7 +164396,7 @@ function bindingFinding(app, file2, purchasesAppId, wanted, fileName) {
164056
164396
  return { conflict: `${fileName} already binds purchases app "${value.text}", but this site's purchases app for the bundle is "${purchasesAppId}". Nothing was changed; fix whichever is stale.` };
164057
164397
  }
164058
164398
  function capabilityFinding(app, fileName) {
164059
- const capabilities = lookup2(app, "capabilities");
164399
+ const capabilities = lookup3(app, "capabilities");
164060
164400
  const list = capabilities.kind === "written" ? unwrap(capabilities.node.initializer) : void 0;
164061
164401
  if (capabilities.kind !== "absent" && (!list || !ts8.isArrayLiteralExpression(list))) {
164062
164402
  return { instruction: `Add "iap" to capabilities: the purchases app is baked only into a build that declares it, and ${fileName} does not list capabilities inline.` };
@@ -164756,14 +165096,13 @@ function createReviewScreenRenderer() {
164756
165096
  }
164757
165097
  return built;
164758
165098
  }
165099
+ let closeLaunched;
164759
165100
  async function launch() {
164760
- let chromium;
164761
- try {
164762
- ({ chromium } = await import("playwright"));
164763
- } catch {
164764
- throw new Error("taking a screenshot needs Playwright's Chromium: run `npx playwright install chromium`");
164765
- }
164766
- return chromium.launch();
165101
+ const { launchPngBrowser: launchPngBrowser2 } = await Promise.resolve().then(() => (init_render_descriptor(), render_descriptor_exports));
165102
+ const launched = await launchPngBrowser2();
165103
+ if (!launched) throw new Error("taking a screenshot needs Playwright's Chromium: run `npx playwright install chromium`");
165104
+ closeLaunched = launched.close;
165105
+ return launched.browser;
164767
165106
  }
164768
165107
  return {
164769
165108
  async render({ contentDir: contentDir2, screenId, input, host, entityId }) {
@@ -164806,7 +165145,8 @@ function createReviewScreenRenderer() {
164806
165145
  return pngAvailable2();
164807
165146
  },
164808
165147
  async close() {
164809
- if (browser) await (await browser.catch(() => void 0))?.close().catch(() => {
165148
+ if (browser) await browser.catch(() => void 0);
165149
+ await closeLaunched?.().catch(() => {
164810
165150
  });
164811
165151
  }
164812
165152
  };
@@ -167114,8 +167454,8 @@ async function runNotifyPing(flags, io2, deps = {}) {
167114
167454
  requestId: requestId2,
167115
167455
  ...flags["dry-run"] === true ? { dryRun: true } : {}
167116
167456
  };
167117
- const http = await pushSendRequest({ target: session.target, wixToken: session.wixToken, siteToken: siteToken2, body, previewTag: session.previewTag, fetchImpl: deps.fetchImpl });
167118
- return report2(http, body, flags, io2);
167457
+ const http2 = await pushSendRequest({ target: session.target, wixToken: session.wixToken, siteToken: siteToken2, body, previewTag: session.previewTag, fetchImpl: deps.fetchImpl });
167458
+ return report2(http2, body, flags, io2);
167119
167459
  }
167120
167460
  async function runAllMembers(args) {
167121
167461
  const { input, siteToken: siteToken2, session, requestId: requestId2, flags, io: io2, deps } = args;
@@ -167204,25 +167544,25 @@ function flagRefusal(flags) {
167204
167544
  if (SWITCHES.some((f2) => flags[f2] !== void 0 && typeof flags[f2] !== "boolean")) return "invalid_boolean_flag";
167205
167545
  return void 0;
167206
167546
  }
167207
- function report2(http, sent, flags, io2) {
167208
- const code = refusalCode2(http);
167547
+ function report2(http2, sent, flags, io2) {
167548
+ const code = refusalCode2(http2);
167209
167549
  if (code === "send_outcome_unknown") {
167210
167550
  return refuse6(io2, code, `Ping may have accepted it. Re-run the same command with --request-id ${sent.requestId}: Ping runs one request at most once per site for 24 hours.`, 1, { requestId: sent.requestId });
167211
167551
  }
167212
- if (code === "rate_limited" && http.retryAfterSec) {
167213
- return refuse6(io2, code, `Test sends are limited per owner; retry in ${http.retryAfterSec} seconds.`, 1, { retryAfterSec: http.retryAfterSec });
167552
+ if (code === "rate_limited" && http2.retryAfterSec) {
167553
+ return refuse6(io2, code, `Test sends are limited per owner; retry in ${http2.retryAfterSec} seconds.`, 1, { retryAfterSec: http2.retryAfterSec });
167214
167554
  }
167215
167555
  if (code) return refuse6(io2, code, HINTS[code], 1);
167216
- if (flags.json === true) io2.log(JSON.stringify(http.body, null, 2));
167217
- else io2.log(summary(http.body ?? {}, sent));
167556
+ if (flags.json === true) io2.log(JSON.stringify(http2.body, null, 2));
167557
+ else io2.log(summary(http2.body ?? {}, sent));
167218
167558
  return 0;
167219
167559
  }
167220
- function refusalCode2(http) {
167221
- if (http.error) return http.error === "push_send_unreachable" ? "send_outcome_unknown" : http.error;
167222
- if (http.ok && (http.body?.state === "accepted" || http.body?.state === "dry_run")) return void 0;
167223
- const code = String(http.body?.error ?? "");
167560
+ function refusalCode2(http2) {
167561
+ if (http2.error) return http2.error === "push_send_unreachable" ? "send_outcome_unknown" : http2.error;
167562
+ if (http2.ok && (http2.body?.state === "accepted" || http2.body?.state === "dry_run")) return void 0;
167563
+ const code = String(http2.body?.error ?? "");
167224
167564
  if (KNOWN_CODES.has(code)) return code;
167225
- return http.ok || http.status >= 500 ? "send_outcome_unknown" : "push_send_failed";
167565
+ return http2.ok || http2.status >= 500 ? "send_outcome_unknown" : "push_send_failed";
167226
167566
  }
167227
167567
  function summary(result, sent) {
167228
167568
  if (result.state === "dry_run") {
@@ -167797,10 +168137,13 @@ __export(plan_exports, {
167797
168137
  RESERVED_SCREEN_IDS: () => RESERVED_SCREEN_IDS,
167798
168138
  displayName: () => displayName2,
167799
168139
  gateFor: () => gateFor,
168140
+ hasLinkField: () => hasLinkField,
167800
168141
  humanize: () => humanize,
168142
+ missingCapabilities: () => missingCapabilities,
167801
168143
  planEntity: () => planEntity,
167802
168144
  planEntityScreens: () => planEntityScreens,
167803
168145
  pluralize: () => pluralize,
168146
+ requiredCapabilities: () => requiredCapabilities,
167804
168147
  rowTitleField: () => rowTitleField,
167805
168148
  snake: () => snake
167806
168149
  });
@@ -167876,6 +168219,12 @@ function fieldReadOnly(f2) {
167876
168219
  const rls = f2.rls;
167877
168220
  return !!rls && typeof rls === "object" && "write" in rls && rls.write !== true;
167878
168221
  }
168222
+ function requiredCapabilities(sp) {
168223
+ return ["base44", ...sp.plans.some(hasLinkField) ? ["open_url"] : []];
168224
+ }
168225
+ function missingCapabilities(sp, declared) {
168226
+ return requiredCapabilities(sp).filter((c2) => !declared.has(c2)).map((capability) => ({ capability, entities: (capability === "open_url" ? sp.plans.filter(hasLinkField) : sp.plans).map((p2) => p2.name) }));
168227
+ }
167879
168228
  function ruleFor(spec, op) {
167880
168229
  const rls = spec.rls;
167881
168230
  if (!rls || typeof rls !== "object") return void 0;
@@ -167954,7 +168303,7 @@ function planEntityScreens(specs, app = "") {
167954
168303
  if (names2.length <= ENTITY_TAB_SLOTS) return { plans, tabs: names2, more: [] };
167955
168304
  return { plans, tabs: names2.slice(0, ENTITY_TAB_SLOTS - 1), more: names2.slice(ENTITY_TAB_SLOTS - 1) };
167956
168305
  }
167957
- var PlanError, RESERVED_SCREEN_IDS, MORE_SCREEN_ID, ENTITY_TAB_SLOTS, OWNER_RULES, pascal, PREFERRED_TITLES, LONG_TEXT, IMAGE_KEY, SEGMENTED_MAX, isPlainString, isEnum, isDate, isImage, isLongText, nounOf;
168306
+ var PlanError, RESERVED_SCREEN_IDS, MORE_SCREEN_ID, ENTITY_TAB_SLOTS, OWNER_RULES, pascal, PREFERRED_TITLES, LONG_TEXT, IMAGE_KEY, SEGMENTED_MAX, isPlainString, isEnum, isDate, isImage, isLongText, hasLinkField, nounOf;
167958
168307
  var init_plan4 = __esm({
167959
168308
  "src/base44-screens/plan.ts"() {
167960
168309
  init_base44_schema();
@@ -167980,6 +168329,7 @@ var init_plan4 = __esm({
167980
168329
  isDate = (f2) => f2.type === "string" && (f2.format === "date" || f2.format === "date-time");
167981
168330
  isImage = (f2) => f2.type === "string" && !isEnum(f2) && (f2.format === "file" || IMAGE_KEY.test(f2.key));
167982
168331
  isLongText = (f2) => f2.type === "string" && !isEnum(f2) && (f2.format === "richtext" || LONG_TEXT.test(f2.key));
168332
+ hasLinkField = (p2) => p2.detail.some((d2) => d2.show === "link");
167983
168333
  nounOf = (display) => display.split(" ").map((w2) => /^[A-Z]{2,}s?$/.test(w2) ? w2 : w2.toLowerCase()).join(" ");
167984
168334
  }
167985
168335
  });
@@ -168031,13 +168381,25 @@ export default defineEntityList(PLAN);
168031
168381
  `;
168032
168382
  }
168033
168383
  function renderDetailEntry(p2) {
168034
- return `// One ${p2.name} (${p2.ids.detail}), pushed from ${p2.ids.list} with its id \u2014 generated by \`himi base44 screens\`.
168384
+ if (!hasLinkField(p2)) {
168385
+ return `// One ${p2.name} (${p2.ids.detail}), pushed from ${p2.ids.list} with its id \u2014 generated by \`himi base44 screens\`.
168035
168386
  // The behaviour lives in the shared engine (../_shared/entity-detail.ts); this file only names the
168036
168387
  // entity's plan.
168037
168388
  import { defineEntityDetail } from "../_shared/entity-detail.js";
168038
168389
  import { PLAN } from "../_shared/${planFile(p2)}.js";
168039
168390
 
168040
168391
  export default defineEntityDetail(PLAN);
168392
+ `;
168393
+ }
168394
+ return `// One ${p2.name} (${p2.ids.detail}), pushed from ${p2.ids.list} with its id \u2014 generated by \`himi base44 screens\`.
168395
+ // The behaviour lives in the shared engine (../_shared/entity-detail.ts); this file names the
168396
+ // entity's plan and hands it \`open\`, which an email / URL fact opens through (the open_url
168397
+ // capability \u2014 only an entity with such a field imports it).
168398
+ import { open } from "@himalaya/open-url";
168399
+ import { defineEntityDetail } from "../_shared/entity-detail.js";
168400
+ import { PLAN } from "../_shared/${planFile(p2)}.js";
168401
+
168402
+ export default defineEntityDetail(PLAN, { open });
168041
168403
  `;
168042
168404
  }
168043
168405
  function renderDevRegistrations(sp) {
@@ -168763,8 +169125,8 @@ function readEntities(files, dirs) {
168763
169125
  function functionSignature(code) {
168764
169126
  if (code === void 0) return { kind: "unknown" };
168765
169127
  const methods = [...new Set([...code.matchAll(/\b(?:req|request)\.method\s*[!=]==?\s*["'](GET|POST|PUT|PATCH|DELETE)["']/g)].map((m2) => m2[1]))];
168766
- const http = /\bDeno\.serve\s*\(|\bserve\s*\(\s*(?:async\s*)?\(?\s*req/.test(code);
168767
- return { kind: http ? "http" : "invoke", ...methods.length ? { methods } : {} };
169128
+ const http2 = /\bDeno\.serve\s*\(|\bserve\s*\(\s*(?:async\s*)?\(?\s*req/.test(code);
169129
+ return { kind: http2 ? "http" : "invoke", ...methods.length ? { methods } : {} };
168768
169130
  }
168769
169131
  function automationsOf(v2) {
168770
169132
  if (!Array.isArray(v2)) return void 0;
@@ -170755,9 +171117,9 @@ function cmsStatusNote(taxonomy) {
170755
171117
  bits.push("skipped: no owner token \u2014 re-run with `--site <msid>` (owner auth) to list the site's collections");
170756
171118
  break;
170757
171119
  case "error": {
170758
- const http = taxonomy.cmsErrorStatus;
170759
- const remedy = http === 429 ? "the site rate-limited the listing \u2014 wait and re-run" : http != null && http >= 500 ? "Wix Data failed upstream \u2014 retry later, and investigate the service if it persists" : http === 401 || http === 403 ? "check the owner token is current and carries the Wix Data read permission, then re-run" : "check the owner token and the `--site <msid>` you passed, then re-run";
170760
- bits.push(`error${http ? ` HTTP ${http}` : ""}: the listing call failed \u2014 ${remedy}`);
171120
+ const http2 = taxonomy.cmsErrorStatus;
171121
+ const remedy = http2 === 429 ? "the site rate-limited the listing \u2014 wait and re-run" : http2 != null && http2 >= 500 ? "Wix Data failed upstream \u2014 retry later, and investigate the service if it persists" : http2 === 401 || http2 === 403 ? "check the owner token is current and carries the Wix Data read permission, then re-run" : "check the owner token and the `--site <msid>` you passed, then re-run";
171122
+ bits.push(`error${http2 ? ` HTTP ${http2}` : ""}: the listing call failed \u2014 ${remedy}`);
170761
171123
  break;
170762
171124
  }
170763
171125
  default:
@@ -174974,7 +175336,7 @@ var init_dist = __esm({
174974
175336
  return promiseCallback.reject(new Error(SAME_SITE_CONTEXT_VAL_ERR));
174975
175337
  }
174976
175338
  }
174977
- const http = options.http ?? true;
175339
+ const http2 = options.http ?? true;
174978
175340
  const now = Date.now();
174979
175341
  const expireCheck = options.expire ?? true;
174980
175342
  const allPaths = options.allPaths ?? false;
@@ -174995,7 +175357,7 @@ var init_dist = __esm({
174995
175357
  if (c2.secure && !potentiallyTrustworthy) {
174996
175358
  return false;
174997
175359
  }
174998
- if (c2.httpOnly && !http) {
175360
+ if (c2.httpOnly && !http2) {
174999
175361
  return false;
175000
175362
  }
175001
175363
  if (sameSiteLevel) {
@@ -184778,7 +185140,9 @@ var USAGE = `himi \u2014 the Himalaya authoring CLI (remote dev loop)
184778
185140
  base44.entities entity (base44-starter packages), write
184779
185141
  controls gated by each entity's rls. Re-running is a
184780
185142
  no-op; a hand-edited generated file is skipped-edited
184781
- unless --force. Never calls Base44.
185143
+ unless --force. An email / URL field opens through
185144
+ open_url, so config.ts capabilities must list it
185145
+ (else missing_capability). Never calls Base44.
184782
185146
  himi base44 entities create [--app <a> | --content <dir>] [--yes] [--allow-destructive]
184783
185147
  [--apply-permissions] [--empty-app-ok]
184784
185148
  create/update the app's OWNED Himi<App>* entities. Base44's
@@ -185040,7 +185404,7 @@ function execNpmConfig(file2, args, options) {
185040
185404
  });
185041
185405
  }
185042
185406
  var npmScopedWixRegistry = createScopedRegistryResolver();
185043
- async function scopedRegistryBefore(lookup3, deadline, onTimeout) {
185407
+ async function scopedRegistryBefore(lookup4, deadline, onTimeout) {
185044
185408
  let cancel = () => {
185045
185409
  };
185046
185410
  const expired = new Promise((resolve66) => {
@@ -185051,16 +185415,16 @@ async function scopedRegistryBefore(lookup3, deadline, onTimeout) {
185051
185415
  cancel = () => clearTimeout(timer);
185052
185416
  });
185053
185417
  try {
185054
- return await Promise.race([Promise.resolve().then(() => lookup3(Math.max(0, deadline - Date.now()))), expired]);
185418
+ return await Promise.race([Promise.resolve().then(() => lookup4(Math.max(0, deadline - Date.now()))), expired]);
185055
185419
  } catch {
185056
185420
  return void 0;
185057
185421
  } finally {
185058
185422
  cancel();
185059
185423
  }
185060
185424
  }
185061
- function lookupOnce(lookup3) {
185425
+ function lookupOnce(lookup4) {
185062
185426
  let first;
185063
- return (budgetMs) => first ??= Promise.resolve().then(() => lookup3(budgetMs));
185427
+ return (budgetMs) => first ??= Promise.resolve().then(() => lookup4(budgetMs));
185064
185428
  }
185065
185429
  function registryCandidates(env, scoped) {
185066
185430
  const ordered = [isTrustedRegistry(scoped) ? normalizeRegistry(scoped) : void 0, trustedConfiguredRegistry(env), PUBLIC_NPM_REGISTRY];
@@ -192866,9 +193230,12 @@ ${report3.ok ? "OK \u2014 data + serve plane reachable." : typeof report3.hint =
192866
193230
  }
192867
193231
  const app = await appId(dir);
192868
193232
  let base44;
193233
+ let appCapabilities;
192869
193234
  try {
192870
193235
  const { loadAppConfigModule: loadAppConfigModule2 } = await Promise.resolve().then(() => (init_app_config_loader(), app_config_loader_exports));
192871
- base44 = (await loadAppConfigModule2(dir))?.base44;
193236
+ const loaded = await loadAppConfigModule2(dir);
193237
+ base44 = loaded?.base44;
193238
+ appCapabilities = loaded?.capabilities;
192872
193239
  } catch (e2) {
192873
193240
  const finding = (await Promise.resolve().then(() => (init_base44_lint(), base44_lint_exports))).base44DefineAppFinding(e2);
192874
193241
  if (finding) {
@@ -192887,7 +193254,7 @@ ${report3.ok ? "OK \u2014 data + serve plane reachable." : typeof report3.hint =
192887
193254
  io2.error(JSON.stringify({ ok: false, code: "invalid_config", app, issues: errors, hint: '`himi validate` reports these with pass: "base44"' }));
192888
193255
  return 1;
192889
193256
  }
192890
- const { planEntityScreens: planEntityScreens2, PlanError: PlanError2 } = await Promise.resolve().then(() => (init_plan4(), plan_exports));
193257
+ const { planEntityScreens: planEntityScreens2, PlanError: PlanError2, missingCapabilities: missingCapabilities2, requiredCapabilities: requiredCapabilities2 } = await Promise.resolve().then(() => (init_plan4(), plan_exports));
192891
193258
  let sp;
192892
193259
  try {
192893
193260
  sp = planEntityScreens2(specs, app);
@@ -192895,6 +193262,18 @@ ${report3.ok ? "OK \u2014 data + serve plane reachable." : typeof report3.hint =
192895
193262
  if (e2 instanceof PlanError2) return fail6(e2.code, e2.message, { app, ...e2.extra });
192896
193263
  throw e2;
192897
193264
  }
193265
+ const { catalogKey: catalogKey2, expandCapabilities: expandCapabilities2 } = await Promise.resolve().then(() => (init_src2(), src_exports));
193266
+ const declaredCaps = expandCapabilities2((Array.isArray(appCapabilities) ? appCapabilities : []).filter((c2) => typeof c2 === "string").map(catalogKey2));
193267
+ const missingCaps = missingCapabilities2(sp, declaredCaps);
193268
+ if (missingCaps.length) {
193269
+ const [first] = missingCaps;
193270
+ return fail6("missing_capability", `config.ts capabilities does not declare "${first.capability}", which the generated screens need (${first.entities.join(", ")})`, {
193271
+ app,
193272
+ capability: first.capability,
193273
+ missing: missingCaps,
193274
+ hint: `add it \u2014 capabilities: [..., ${missingCaps.map((m2) => `"${m2.capability}"`).join(", ")}] \u2014 then re-run; the native config follows from the capability catalog (himi docs capabilities). Nothing was written.`
193275
+ });
193276
+ }
192898
193277
  const rendered = gen.renderAll(sp);
192899
193278
  const dryRun = flags["dry-run"] === true;
192900
193279
  const { files } = gen.writeGenerated(dir, rendered, { force: flags.force === true, dryRun });
@@ -192905,6 +193284,7 @@ ${report3.ok ? "OK \u2014 data + serve plane reachable." : typeof report3.hint =
192905
193284
  ...dryRun ? { dryRun: true } : {},
192906
193285
  files,
192907
193286
  plans: sp.plans.map((p2) => ({ entity: p2.name, physical: p2.entity, list: p2.ids.list, detail: p2.ids.detail, gates: p2.gates })),
193287
+ capabilities: requiredCapabilities2(sp),
192908
193288
  tabs: sp.tabs,
192909
193289
  more: sp.more,
192910
193290
  ...orphaned.length ? { orphaned, note: "generated files for entities no longer declared \u2014 never deleted; remove them by hand if the screens should go" } : {},
@@ -139,12 +139,12 @@ Write `.himi/screen-plan.md` before editing code. For each entity in `book.entit
139
139
  | `string` + long text (`description`, `notes`, `body`, …) | `Text` with `maxLines` | `TextField` with `multilineMaxLines` |
140
140
  | `string` + `enum` | `Badge` / `Text` | `SegmentedControl` (≤ 4 values) or `Picker` |
141
141
  | `string` + `format: date` / `date-time` | formatted `Text` | `DatePicker` |
142
- | `string` + `format: email` / `uri` | `Text` (the generator does not make it tappable yet) | `TextField` with `keyboard: "email"` / `"url"` |
142
+ | `string` + `format: email` / `uri` | a tappable row: an email opens mail, a URL the in-app browser, through `@himalaya/open-url` (needs `open_url`, below) | `TextField` with `keyboard: "email"` / `"url"` |
143
143
  | `string` holding an image URL (`image`, `photo`, `avatar`, `*_url`) | `Image` | read-only, see [file upload](#what-the-sdk-cannot-do) |
144
144
  | `number` / `integer` | `Text` (with unit) | `Stepper` (small ranges) or numeric `TextField` |
145
145
  | `boolean` | `Badge` or `Text` | `Toggle` |
146
- | `array` of strings | comma-joined `Text` | `ChipsInput` |
147
- | `object` / nested `array` of objects | its JSON as `Text` (a sub-section of rows is yours to build) | none. Edit it on the web app |
146
+ | `array` of strings | read-only `FilterChips` (no selection, no `onChange`) | `ChipsInput` |
147
+ | `object` / nested `array` of objects | a labelled sub-section of key/value rows, one level deep: one row per key (an array: one row per item, `Item 1`, …), a deeper value inline as `Key: value · …` | none. Edit it on the web app |
148
148
  | built-ins `id`, `created_date`, `updated_date`, `created_by` | "Created …" metadata row | never editable |
149
149
 
150
150
  4. **Respect the entity's rules.** `rls` (and field-level `rls`) decide what the signed-in user
@@ -161,7 +161,17 @@ Write `.himi/screen-plan.md` before editing code. For each entity in `book.entit
161
161
  A field whose field-level `rls.write` is not `true` is read-only in the form. The gate is
162
162
  advisory: a denied call still fails with Base44's message, and every screen paints that error
163
163
  rather than swallowing it.
164
- 5. **Use the sample rows** (`sampleRows`, already redacted) to size rows and choose the subtitle,
164
+ 5. **Declare `open_url` when an entity has an email or URL field.** The detail opens those facts
165
+ through `@himalaya/open-url`, so the app must list the capability:
166
+ `capabilities: ["base44", "open_url"]` in `config.ts`. Without it `himi base44 screens` writes
167
+ nothing and fails with `code: "missing_capability"` (`capability: "open_url"`, and `missing[]`
168
+ names the entities that need it). The binary's native config follows from that list (the
169
+ capability catalog, `himi docs capabilities`), so a shell built without it needs a new
170
+ `himi dist`. Only the detail of an entity with such a field imports `@himalaya/open-url` and
171
+ declares `open_url`. A value that is not an address or an http(s) URL (`javascript:`, an app
172
+ scheme) is shown as plain text and never opened. A link, chips or section fact with no value
173
+ shows a `—` row instead.
174
+ 6. **Use the sample rows** (`sampleRows`, already redacted) to size rows and choose the subtitle,
165
175
  never as data the app ships.
166
176
 
167
177
  Compose each screen with [`designing-himi-screens`](designing-himi-screens.md)
@@ -202,8 +212,10 @@ Re-running is safe. Each generated file starts with
202
212
  `skipped-edited` and left alone, and the command still exits 0. Pass `--force` to overwrite it;
203
213
  - files for an entity you removed from `config.ts` are reported in `orphaned[]` and never deleted.
204
214
 
205
- `--dry-run` reports the same outcomes without writing. Failures carry a top-level `code`
206
- (`no_entities_block`, `entity_id_collision`, `content_dir_not_base44_starter`), per
215
+ `--dry-run` reports the same outcomes without writing. On success `capabilities[]` lists what the
216
+ generated screens need (`base44`, plus `open_url` with a link field). Failures carry a top-level
217
+ `code` (`no_entities_block`, `entity_id_collision`, `missing_capability`,
218
+ `content_dir_not_base44_starter`), per
207
219
  [`himi-json-output`](himi-json-output.md). The generator never calls Base44.
208
220
 
209
221
  To change a screen beyond what the plan expresses (a filtered list, a child section, a custom
@@ -324,9 +324,14 @@ on stderr (exit 1):
324
324
  - `no_entities_block`: `config.ts` declares no `base44.entities`, so there is nothing to generate.
325
325
  - `entity_id_collision`: two entities would generate the same screen id (or one would shadow
326
326
  `b44_functions` / `b44_account` / `b44_more`); `entity`, `screenId` and `with` name the clash.
327
+ - `missing_capability`: an entity has an email / URL field, which the detail opens through
328
+ `@himalaya/open-url`, and `config.ts` `capabilities` does not list `open_url`. `capability` names
329
+ it and `missing[]` gives `{ capability, entities }` per missing id. Nothing is written. Add it to
330
+ `capabilities` and re-run.
327
331
  - `config_unloadable` / `invalid_config`: as for `himi base44 entities` above.
328
332
 
329
- On success (`ok: true`, stdout, exit 0) `files[]` gives every generated path with its `outcome`:
333
+ On success (`ok: true`, stdout, exit 0) `capabilities[]` lists the ids the generated screens need
334
+ (`base44`, plus `open_url` with a link field), and `files[]` gives every generated path with its `outcome`:
330
335
  `written`, `unchanged`, or `skipped-edited` (the file was edited by hand and was left alone; pass
331
336
  `--force` to overwrite it). A skipped file is not a failure. `orphaned[]`, when present, lists
332
337
  generated files for entities no longer declared. They are never deleted.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.1437.0",
3
+ "version": "0.1439.0",
4
4
  "type": "module",
5
5
  "description": "himi — the Himalaya authoring CLI. Build a Tier-5 content package on your own machine, push it to the production serve as a draft, preview it on web + Himi Player, and read worker logs back — the remote dev loop. Wraps @himalaya/serve-cli for transport.",
6
6
  "keywords": [
@@ -71,5 +71,5 @@
71
71
  "artifactId": "himalaya-cli"
72
72
  }
73
73
  },
74
- "falconPackageHash": "9420ac69c3606bfc2c5c00103d9082f80dbed8b2d813d658111e6eeb"
74
+ "falconPackageHash": "517214ad79ba1f66fcc679a54a5edf1e5242a258037728e41501f8ce"
75
75
  }