@verentis/cli 0.2.4 → 0.2.5

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/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
- import { mkdir, stat, writeFile, readFile, readdir, mkdtemp, rm, glob } from 'fs/promises';
3
+ import { mkdir, stat, writeFile, readFile, readdir, realpath, mkdtemp, rm, glob } from 'fs/promises';
4
4
  import { tmpdir, homedir } from 'os';
5
- import { resolve, basename, dirname, posix, join, sep, relative } from 'path';
5
+ import { resolve, basename, dirname, posix, join, isAbsolute, extname, relative, sep } from 'path';
6
6
  import { spawn, spawnSync } from 'child_process';
7
7
  import { generateKeyPairSync, createHash, randomUUID, randomBytes, createCipheriv, createPublicKey, verify, diffieHellman, hkdfSync, sign, createPrivateKey } from 'crypto';
8
- import { parse } from 'yaml';
8
+ import { parse, stringify } from 'yaml';
9
9
  import { createWriteStream, readFileSync, createReadStream } from 'fs';
10
10
  import { Readable } from 'stream';
11
11
  import { pipeline } from 'stream/promises';
@@ -130,7 +130,7 @@ async function refreshIdentityToken(apiUrl, refreshToken) {
130
130
  `Session could not be renewed${payload?.error ? ` (${payload.error})` : ` (${res.status})`}. Run \`verentis login\` again.`
131
131
  );
132
132
  }
133
- var sleep = (ms) => new Promise((resolve8) => setTimeout(resolve8, ms));
133
+ var sleep = (ms) => new Promise((resolve11) => setTimeout(resolve11, ms));
134
134
 
135
135
  // src/http.ts
136
136
  var ApiError = class extends Error {
@@ -298,7 +298,6 @@ ${truncate(text)}`;
298
298
  }
299
299
  var SCOPES = {
300
300
  publisherRead: "marketplace.publisher.read",
301
- publisherCreate: "marketplace.publisher.create",
302
301
  publisherUpdate: "marketplace.publisher.update",
303
302
  packageRead: "marketplace.package.read",
304
303
  packageReadAll: "marketplace.package.read-all",
@@ -367,6 +366,8 @@ var MARKETPLACE = {
367
366
  packageRead: "marketplace.package.read",
368
367
  packageUpdate: "marketplace.package.update",
369
368
  packageVisibilityManage: "marketplace.package.visibility.manage",
369
+ publisherCreate: "marketplace.publisher.create",
370
+ publisherUpdate: "marketplace.publisher.update",
370
371
  statsRead: "marketplace.stats.read"};
371
372
  var ENGINE_DEFAULT_PERMISSIONS = [
372
373
  NODE.fileRead,
@@ -455,6 +456,10 @@ var execution = {
455
456
  syncEngines: e({ method: "POST", path: "/v1/engines/sync", scopes: [EXECUTION.run], workspaceBound: true })
456
457
  };
457
458
  var marketplace = {
459
+ createPublisher: e({ method: "POST", path: "/v1/publishers", scopes: [MARKETPLACE.publisherCreate] }),
460
+ updatePublisher: e({ method: "PUT", path: "/v1/publishers/{id}", scopes: [MARKETPLACE.publisherUpdate] }),
461
+ setPublisherLogo: e({ method: "PUT", path: "/v1/publishers/{id}/logo", scopes: [MARKETPLACE.publisherUpdate] }),
462
+ removePublisherLogo: e({ method: "DELETE", path: "/v1/publishers/{id}/logo", scopes: [MARKETPLACE.publisherUpdate] }),
458
463
  getPackage: e({ method: "GET", path: "/v1/packages/{id}", scopes: [MARKETPLACE.packageRead] }),
459
464
  updatePackage: e({ method: "PUT", path: "/v1/packages/{id}", scopes: [MARKETPLACE.packageUpdate] }),
460
465
  archivePackage: e({ method: "DELETE", path: "/v1/packages/{id}", scopes: [MARKETPLACE.packageDelete] }),
@@ -845,6 +850,122 @@ function contextEnv(context, contextPath) {
845
850
  VERENTIS_TIMEOUT: String(context.execution.timeout)
846
851
  };
847
852
  }
853
+ var KIND_MAP = {
854
+ application: "Application",
855
+ executionengine: "ExecutionEngine",
856
+ bundle: "Bundle"
857
+ };
858
+ function normalizeKind(kind) {
859
+ const mapped = KIND_MAP[kind.replace(/[^a-z]/gi, "").toLowerCase()];
860
+ if (!mapped) throw new Error(`Unsupported manifest kind '${kind}' \u2014 expected Application, ExecutionEngine or Bundle.`);
861
+ return mapped;
862
+ }
863
+ function validateLaunchManifestKind(kind, spec) {
864
+ if (kind === "Application" || !spec || typeof spec !== "object" || Array.isArray(spec)) return void 0;
865
+ if (!Object.prototype.hasOwnProperty.call(spec, "launch")) return void 0;
866
+ return "spec.launch is only supported for Application manifests";
867
+ }
868
+ async function loadManifest(path) {
869
+ const text = await readFile(path, "utf8");
870
+ const manifest = parse(text);
871
+ if (!manifest || typeof manifest !== "object") throw new Error(`Failed to parse manifest at ${path}.`);
872
+ if (!manifest.kind) throw new Error(`Manifest at ${path} is missing 'kind'.`);
873
+ if (!manifest.metadata?.name) throw new Error(`Manifest at ${path} is missing 'metadata.name'.`);
874
+ if (!manifest.metadata?.version) throw new Error(`Manifest at ${path} is missing 'metadata.version'.`);
875
+ const kind = normalizeKind(manifest.kind);
876
+ const launchKindError = validateLaunchManifestKind(kind, manifest.spec);
877
+ if (launchKindError) throw new Error(`Invalid manifest at ${path}: ${launchKindError}.`);
878
+ const marketplaceErrors = validateMarketplaceSection(manifest.marketplace);
879
+ if (marketplaceErrors.length > 0) {
880
+ throw new Error(`Invalid marketplace section in ${path}: ${marketplaceErrors.join(" ")}`);
881
+ }
882
+ return manifest;
883
+ }
884
+ function validateMarketplaceSection(marketplace2) {
885
+ if (marketplace2 === void 0) return [];
886
+ if (!marketplace2 || typeof marketplace2 !== "object" || Array.isArray(marketplace2)) {
887
+ return ["marketplace must be an object."];
888
+ }
889
+ const section = marketplace2;
890
+ const errors = [];
891
+ for (const field of ["logo", "readme", "changelog"]) {
892
+ const value = section[field];
893
+ if (value !== void 0 && (typeof value !== "string" || value.trim().length === 0)) {
894
+ errors.push(`marketplace.${field} must be a non-empty relative path.`);
895
+ }
896
+ }
897
+ if (section.hero !== void 0) {
898
+ if (!section.hero || typeof section.hero !== "object" || Array.isArray(section.hero)) {
899
+ errors.push("marketplace.hero must be an object.");
900
+ } else {
901
+ const hero = section.hero;
902
+ if (typeof hero.source !== "string" || hero.source.trim().length === 0) {
903
+ errors.push("marketplace.hero.source must be a non-empty relative path.");
904
+ }
905
+ if (typeof hero.alt !== "string" || hero.alt.trim().length === 0) {
906
+ errors.push("marketplace.hero.alt is required and must not be empty.");
907
+ }
908
+ }
909
+ }
910
+ if (section.images !== void 0) {
911
+ if (!Array.isArray(section.images)) {
912
+ errors.push("marketplace.images must be a list.");
913
+ } else {
914
+ if (section.images.length > 8) errors.push("marketplace.images may contain at most 8 images.");
915
+ section.images.forEach((value, index) => {
916
+ const at = `marketplace.images[${index}]`;
917
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
918
+ errors.push(`${at} must be an object.`);
919
+ return;
920
+ }
921
+ const image = value;
922
+ if (typeof image.source !== "string" || image.source.trim().length === 0) {
923
+ errors.push(`${at}.source must be a non-empty relative path.`);
924
+ }
925
+ if (typeof image.alt !== "string" || image.alt.trim().length === 0) {
926
+ errors.push(`${at}.alt is required and must not be empty.`);
927
+ }
928
+ if (image.caption !== void 0 && typeof image.caption !== "string") {
929
+ errors.push(`${at}.caption must be a string when provided.`);
930
+ }
931
+ });
932
+ }
933
+ }
934
+ if (section.assets !== void 0) {
935
+ if (!Array.isArray(section.assets)) {
936
+ errors.push("marketplace.assets must be a list of relative glob strings.");
937
+ } else {
938
+ section.assets.forEach((value, index) => {
939
+ if (typeof value !== "string" || value.trim().length === 0) {
940
+ errors.push(`marketplace.assets[${index}] must be a non-empty relative glob string.`);
941
+ }
942
+ });
943
+ }
944
+ }
945
+ return errors;
946
+ }
947
+ async function findManifest(dir) {
948
+ const explicit = join(dir, "manifest.yaml");
949
+ try {
950
+ if ((await stat(explicit)).isFile()) return explicit;
951
+ } catch {
952
+ }
953
+ const entries = await readdir(dir);
954
+ const candidates = entries.filter((name) => /\.(app|engine|bundle)\.ya?ml$/.test(name));
955
+ if (candidates.length === 1) return join(dir, candidates[0]);
956
+ if (candidates.length === 0) {
957
+ throw new Error(`No manifest found in ${dir}. Expected manifest.yaml or *.app.yaml / *.engine.yaml / *.bundle.yaml.`);
958
+ }
959
+ throw new Error(`Multiple manifests found in ${dir} (${candidates.join(", ")}). Pass --manifest to disambiguate.`);
960
+ }
961
+ var NAME_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
962
+ function validatePackageName(name, label) {
963
+ if (!NAME_PATTERN.test(name)) {
964
+ throw new Error(`Invalid ${label} '${name}' \u2014 use lowercase letters, digits and hyphens (no leading/trailing hyphen).`);
965
+ }
966
+ }
967
+
968
+ // src/dev/manifest.ts
848
969
  async function readManifestDocument(path) {
849
970
  const text = await readFile(path, "utf8");
850
971
  const doc = parse(text);
@@ -874,6 +995,8 @@ function validateManifest(manifest) {
874
995
  if (!meta[field]) error(`metadata.${field} is required`);
875
996
  }
876
997
  const spec = manifest.spec ?? {};
998
+ const launchKindError = validateLaunchManifestKind(kind, spec);
999
+ if (launchKindError) error(launchKindError);
877
1000
  if (kind === "ExecutionEngine") {
878
1001
  const runtimes = spec.runtimes ?? {};
879
1002
  if (Object.keys(runtimes).length === 0) {
@@ -902,12 +1025,62 @@ function validateManifest(manifest) {
902
1025
  "an Application must declare at least one contribution: spec.entry / spec.entry-points[] (iframe UI), spec.install[] (installed files), or spec.scopes[] (identity)"
903
1026
  );
904
1027
  }
1028
+ validateApplicationLaunch(spec, issues);
905
1029
  }
906
1030
  validateInstallDirectives(spec.install, issues);
1031
+ for (const message of validateMarketplaceSection(manifest.marketplace)) error(message);
907
1032
  const packaging = manifest.packaging ?? {};
908
1033
  if (!packaging.publisher) warning("packaging.publisher is not set \u2014 `verentis pack`/`publish` will refuse the package");
909
1034
  return issues;
910
1035
  }
1036
+ function validateApplicationLaunch(spec, issues) {
1037
+ const launch = spec.launch;
1038
+ if (launch === void 0) return;
1039
+ const error = (message) => issues.push({ level: "error", message });
1040
+ if (!launch || typeof launch !== "object" || Array.isArray(launch)) {
1041
+ error("spec.launch must be an object");
1042
+ return;
1043
+ }
1044
+ if (launch.pinnable === true && launch.enabled !== true) {
1045
+ error("spec.launch.pinnable requires spec.launch.enabled");
1046
+ }
1047
+ if (launch.enabled !== true) return;
1048
+ const launchEntryPoint = launch["entry-point"];
1049
+ if (launchEntryPoint !== void 0 && (typeof launchEntryPoint !== "string" || !launchEntryPoint.trim())) {
1050
+ error("spec.launch.entry-point must be a non-empty entry-point id");
1051
+ return;
1052
+ }
1053
+ if (typeof launchEntryPoint === "string") {
1054
+ const entryPoint = spec["entry-points"]?.find((candidate) => candidate.id === launchEntryPoint);
1055
+ if (!entryPoint) {
1056
+ error(`spec.launch.entry-point '${launchEntryPoint}' does not match spec.entry-points[].id`);
1057
+ return;
1058
+ }
1059
+ if (!resolvesIframeEntry(spec.entry, entryPoint.entry ?? entryPoint.route)) {
1060
+ error("spec.launch.enabled requires the selected entry point to resolve an iframe entry");
1061
+ }
1062
+ return;
1063
+ }
1064
+ if (!isAbsoluteHttpUrl(spec.entry)) {
1065
+ error("spec.launch.enabled requires spec.entry or spec.launch.entry-point");
1066
+ }
1067
+ }
1068
+ function isNonEmptyString(value) {
1069
+ return typeof value === "string" && value.trim().length > 0;
1070
+ }
1071
+ function resolvesIframeEntry(baseEntry, routeOrEntry) {
1072
+ if (isAbsoluteHttpUrl(routeOrEntry)) return true;
1073
+ return isAbsoluteHttpUrl(baseEntry);
1074
+ }
1075
+ function isAbsoluteHttpUrl(value) {
1076
+ if (!isNonEmptyString(value)) return false;
1077
+ try {
1078
+ const url = new URL(value);
1079
+ return url.protocol === "http:" || url.protocol === "https:";
1080
+ } catch {
1081
+ return false;
1082
+ }
1083
+ }
911
1084
  function validateInstallDirectives(install, issues) {
912
1085
  if (install === void 0) return;
913
1086
  const error = (message) => issues.push({ level: "error", message });
@@ -937,46 +1110,6 @@ function validateInstallDirectives(install, issues) {
937
1110
  }
938
1111
  });
939
1112
  }
940
- var KIND_MAP = {
941
- application: "Application",
942
- executionengine: "ExecutionEngine",
943
- bundle: "Bundle"
944
- };
945
- function normalizeKind(kind) {
946
- const mapped = KIND_MAP[kind.replace(/[^a-z]/gi, "").toLowerCase()];
947
- if (!mapped) throw new Error(`Unsupported manifest kind '${kind}' \u2014 expected Application, ExecutionEngine or Bundle.`);
948
- return mapped;
949
- }
950
- async function loadManifest(path) {
951
- const text = await readFile(path, "utf8");
952
- const manifest = parse(text);
953
- if (!manifest || typeof manifest !== "object") throw new Error(`Failed to parse manifest at ${path}.`);
954
- if (!manifest.kind) throw new Error(`Manifest at ${path} is missing 'kind'.`);
955
- if (!manifest.metadata?.name) throw new Error(`Manifest at ${path} is missing 'metadata.name'.`);
956
- if (!manifest.metadata?.version) throw new Error(`Manifest at ${path} is missing 'metadata.version'.`);
957
- normalizeKind(manifest.kind);
958
- return manifest;
959
- }
960
- async function findManifest(dir) {
961
- const explicit = join(dir, "manifest.yaml");
962
- try {
963
- if ((await stat(explicit)).isFile()) return explicit;
964
- } catch {
965
- }
966
- const entries = await readdir(dir);
967
- const candidates = entries.filter((name) => /\.(app|engine|bundle)\.ya?ml$/.test(name));
968
- if (candidates.length === 1) return join(dir, candidates[0]);
969
- if (candidates.length === 0) {
970
- throw new Error(`No manifest found in ${dir}. Expected manifest.yaml or *.app.yaml / *.engine.yaml / *.bundle.yaml.`);
971
- }
972
- throw new Error(`Multiple manifests found in ${dir} (${candidates.join(", ")}). Pass --manifest to disambiguate.`);
973
- }
974
- var NAME_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
975
- function validatePackageName(name, label) {
976
- if (!NAME_PATTERN.test(name)) {
977
- throw new Error(`Invalid ${label} '${name}' \u2014 use lowercase letters, digits and hyphens (no leading/trailing hyphen).`);
978
- }
979
- }
980
1113
 
981
1114
  // src/commands/dev.ts
982
1115
  async function resolveDevScopes(flags) {
@@ -1326,7 +1459,7 @@ async function followExecution(api, workspaceId, executionId, json) {
1326
1459
  if (exec.status !== "Completed") process.exitCode = 1;
1327
1460
  return;
1328
1461
  }
1329
- await new Promise((resolve8) => setTimeout(resolve8, 1e3));
1462
+ await new Promise((resolve11) => setTimeout(resolve11, 1e3));
1330
1463
  }
1331
1464
  console.error("Timed out waiting for a terminal status \u2014 check `verentis exec get`.");
1332
1465
  process.exitCode = 1;
@@ -1479,10 +1612,31 @@ metadata:
1479
1612
  version: 0.1.0
1480
1613
  author: ""
1481
1614
 
1615
+ # Marketplace presentation (optional):
1616
+ # marketplace:
1617
+ # logo: ./marketplace/logo.png
1618
+ # hero:
1619
+ # source: ./marketplace/hero.webp
1620
+ # alt: A concise description of the hero background
1621
+ # images:
1622
+ # - source: ./marketplace/screenshot.png
1623
+ # alt: A concise description of the screenshot
1624
+ # caption: Optional visible caption
1625
+ # readme: ./README.md
1626
+ # changelog: ./CHANGELOG.md
1627
+ # assets:
1628
+ # - "marketplace/**/*.{png,jpg,jpeg,webp,svg}"
1629
+
1482
1630
  spec:
1483
1631
  entry: https://example.com # hosted app origin
1484
1632
  scope: global
1485
1633
 
1634
+ # Opt into a standalone workspace surface. Contextual file handlers should omit this block.
1635
+ # launch:
1636
+ # enabled: true
1637
+ # entry-point: home # optional; otherwise spec.entry is used
1638
+ # pinnable: true
1639
+
1486
1640
  entry-points: []
1487
1641
  capabilities: []
1488
1642
  permissions: []
@@ -1509,6 +1663,20 @@ metadata:
1509
1663
  version: 0.1.0
1510
1664
  author: ""
1511
1665
 
1666
+ # Marketplace presentation (optional):
1667
+ # marketplace:
1668
+ # logo: ./marketplace/logo.png
1669
+ # hero:
1670
+ # source: ./marketplace/hero.webp
1671
+ # alt: A concise description of the hero background
1672
+ # images:
1673
+ # - source: ./marketplace/screenshot.png
1674
+ # alt: A concise description of the screenshot
1675
+ # readme: ./README.md
1676
+ # changelog: ./CHANGELOG.md
1677
+ # assets:
1678
+ # - "marketplace/**/*.{png,jpg,jpeg,webp,svg}"
1679
+
1512
1680
  spec:
1513
1681
  runtimes: {}
1514
1682
  file-types: []
@@ -1530,6 +1698,20 @@ metadata:
1530
1698
  version: 0.1.0
1531
1699
  author: ""
1532
1700
 
1701
+ # Marketplace presentation (optional):
1702
+ # marketplace:
1703
+ # logo: ./marketplace/logo.png
1704
+ # hero:
1705
+ # source: ./marketplace/hero.webp
1706
+ # alt: A concise description of the hero background
1707
+ # images:
1708
+ # - source: ./marketplace/screenshot.png
1709
+ # alt: A concise description of the screenshot
1710
+ # readme: ./README.md
1711
+ # changelog: ./CHANGELOG.md
1712
+ # assets:
1713
+ # - "marketplace/**/*.{png,jpg,jpeg,webp,svg}"
1714
+
1533
1715
  packaging:
1534
1716
  publisher: "" # your publisher name
1535
1717
  files:
@@ -1909,20 +2091,260 @@ function rawX25519Private(key) {
1909
2091
  function publicKeyFromRaw(raw) {
1910
2092
  return createPublicKey({ key: { kty: "OKP", crv: "X25519", x: raw.toString("base64url") }, format: "jwk" });
1911
2093
  }
2094
+ var MIB = 1024 * 1024;
2095
+ var MAX_LOGO_BYTES = MIB;
2096
+ var MAX_IMAGE_BYTES = 5 * MIB;
2097
+ var MAX_TOTAL_IMAGE_BYTES = 25 * MIB;
2098
+ var MAX_MARKDOWN_BYTES = MIB;
2099
+ async function buildCatalog(projectDir, marketplace2) {
2100
+ const root = await realpath(projectDir);
2101
+ const entries = /* @__PURE__ */ new Map();
2102
+ const imageDigests = /* @__PURE__ */ new Map();
2103
+ const index = { schemaVersion: 1 };
2104
+ const addImage = async (source, label, maxBytes) => {
2105
+ const file = await resolveDirectFile(root, source, label);
2106
+ if (file.bytes.length > maxBytes) {
2107
+ throw new Error(`${label} exceeds the ${formatMiB(maxBytes)} MiB limit (${file.bytes.length} bytes).`);
2108
+ }
2109
+ const image = validateImage(file.bytes, file.path, label);
2110
+ const reference = addContentAddressedEntry(file, image, "assets", entries);
2111
+ imageDigests.set(reference.sha256, reference.size);
2112
+ return reference;
2113
+ };
2114
+ const addMarkdown = async (source, label) => {
2115
+ const file = await resolveDirectFile(root, source, label);
2116
+ if (file.bytes.length > MAX_MARKDOWN_BYTES) {
2117
+ throw new Error(`${label} exceeds the 1 MiB limit (${file.bytes.length} bytes).`);
2118
+ }
2119
+ validateMarkdown(file.bytes, file.path, label);
2120
+ return addContentAddressedEntry(file, { extension: ".md", mediaType: "text/markdown" }, "docs", entries);
2121
+ };
2122
+ if (marketplace2.logo) index.logo = await addImage(marketplace2.logo, "marketplace.logo", MAX_LOGO_BYTES);
2123
+ if (marketplace2.hero) {
2124
+ const reference = await addImage(marketplace2.hero.source, "marketplace.hero.source", MAX_IMAGE_BYTES);
2125
+ index.hero = { ...reference, alt: marketplace2.hero.alt.trim() };
2126
+ }
2127
+ if (marketplace2.images?.length) {
2128
+ index.images = [];
2129
+ for (const [position, image] of marketplace2.images.entries()) {
2130
+ const reference = await addImage(image.source, `marketplace.images[${position}].source`, MAX_IMAGE_BYTES);
2131
+ index.images.push({
2132
+ ...reference,
2133
+ alt: image.alt.trim(),
2134
+ ...image.caption !== void 0 ? { caption: image.caption } : {}
2135
+ });
2136
+ }
2137
+ }
2138
+ if (marketplace2.readme) index.readme = await addMarkdown(marketplace2.readme, "marketplace.readme");
2139
+ if (marketplace2.changelog) index.changelog = await addMarkdown(marketplace2.changelog, "marketplace.changelog");
2140
+ if (marketplace2.assets?.length) {
2141
+ index.assets = [];
2142
+ const assetFiles = await resolveAssetGlobs(root, marketplace2.assets);
2143
+ for (const file of assetFiles) {
2144
+ if (file.bytes.length > MAX_IMAGE_BYTES) {
2145
+ throw new Error(`marketplace.assets matched '${file.source}', which exceeds the 5 MiB per-image limit (${file.bytes.length} bytes).`);
2146
+ }
2147
+ const image = validateImage(file.bytes, file.path, `marketplace.assets entry '${file.source}'`);
2148
+ const reference = addContentAddressedEntry(file, image, "assets", entries);
2149
+ imageDigests.set(reference.sha256, reference.size);
2150
+ index.assets.push(reference);
2151
+ }
2152
+ }
2153
+ const totalImageBytes = [...imageDigests.values()].reduce((sum, size) => sum + size, 0);
2154
+ if (totalImageBytes > MAX_TOTAL_IMAGE_BYTES) {
2155
+ throw new Error(`Marketplace image assets exceed the 25 MiB total limit (${totalImageBytes} bytes).`);
2156
+ }
2157
+ const indexJson = `${JSON.stringify(index, null, 2)}
2158
+ `;
2159
+ return {
2160
+ index,
2161
+ indexJson,
2162
+ entries: [...entries.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([path, bytes]) => ({ path, bytes }))
2163
+ };
2164
+ }
2165
+ async function resolveDirectFile(root, source, label) {
2166
+ const normalized = normalizeRelativeReference(source, label);
2167
+ return readContainedFile(root, normalized, label);
2168
+ }
2169
+ async function resolveAssetGlobs(root, patterns) {
2170
+ const matched = /* @__PURE__ */ new Set();
2171
+ for (const sourcePattern of patterns) {
2172
+ const pattern = normalizeRelativeReference(sourcePattern, "marketplace.assets glob");
2173
+ for await (const entry of glob(pattern, { cwd: root })) {
2174
+ const normalized = toEntryPath(entry);
2175
+ const info = await stat(join(root, normalized)).catch(() => null);
2176
+ if (info?.isFile()) matched.add(normalized);
2177
+ }
2178
+ }
2179
+ const files2 = [];
2180
+ for (const source of [...matched].sort()) {
2181
+ files2.push(await readContainedFile(root, source, `marketplace.assets entry '${source}'`));
2182
+ }
2183
+ return files2;
2184
+ }
2185
+ async function readContainedFile(root, source, label) {
2186
+ const candidate = resolve(root, source);
2187
+ const actual = await realpath(candidate).catch(() => {
2188
+ throw new Error(`${label} references missing file '${source}'.`);
2189
+ });
2190
+ const fromRoot = relative(root, actual);
2191
+ if (fromRoot === "" || fromRoot.startsWith(`..${sep}`) || fromRoot === ".." || isAbsolute(fromRoot)) {
2192
+ throw new Error(`${label} references '${source}', which escapes the manifest directory.`);
2193
+ }
2194
+ const info = await stat(actual);
2195
+ if (!info.isFile()) throw new Error(`${label} references '${source}', which is not a file.`);
2196
+ return { source: toEntryPath(source), path: actual, bytes: await readFile(actual) };
2197
+ }
2198
+ function normalizeRelativeReference(source, label) {
2199
+ const value = source.trim();
2200
+ if (value.length === 0 || value.includes("\0") || value.includes("\\") || isAbsolute(value) || /^[a-z]:/i.test(value) || value.split("/").includes("..")) {
2201
+ throw new Error(`${label} must be relative to the manifest directory and must not escape it (got '${source}').`);
2202
+ }
2203
+ const normalized = value.replace(/^(?:\.\/)+/, "");
2204
+ if (normalized.length === 0 || normalized === ".") throw new Error(`${label} must reference a file inside the manifest directory.`);
2205
+ return normalized;
2206
+ }
2207
+ function addContentAddressedEntry(file, type, directory, entries) {
2208
+ const sha256 = createHash("sha256").update(file.bytes).digest("hex");
2209
+ const path = `catalog/${directory}/${sha256}${type.extension}`;
2210
+ entries.set(path, file.bytes);
2211
+ return {
2212
+ source: file.source,
2213
+ path,
2214
+ mediaType: type.mediaType,
2215
+ size: file.bytes.length,
2216
+ sha256
2217
+ };
2218
+ }
2219
+ function validateImage(bytes, path, label) {
2220
+ const pngSignature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
2221
+ if (bytes.length >= 24 && bytes.subarray(0, 8).equals(pngSignature)) {
2222
+ if (bytes.readUInt32BE(8) !== 13 || bytes.toString("ascii", 12, 16) !== "IHDR") throw new Error(`${label} is not a valid PNG file.`);
2223
+ return { extension: ".png", mediaType: "image/png" };
2224
+ }
2225
+ if (bytes.length >= 4 && bytes[0] === 255 && bytes[1] === 216) {
2226
+ if (bytes[bytes.length - 2] !== 255 || bytes[bytes.length - 1] !== 217) throw new Error(`${label} is not a valid JPEG file.`);
2227
+ return { extension: ".jpg", mediaType: "image/jpeg" };
2228
+ }
2229
+ if (bytes.length >= 12 && bytes.toString("ascii", 0, 4) === "RIFF" && bytes.toString("ascii", 8, 12) === "WEBP") {
2230
+ const declaredLength = bytes.readUInt32LE(4) + 8;
2231
+ if (declaredLength !== bytes.length) throw new Error(`${label} has an invalid WebP length.`);
2232
+ return { extension: ".webp", mediaType: "image/webp" };
2233
+ }
2234
+ const extension = extname(path).toLowerCase();
2235
+ if (extension === ".svg" || looksLikeSvg(bytes)) {
2236
+ validateSvg(bytes, label);
2237
+ return { extension: ".svg", mediaType: "image/svg+xml" };
2238
+ }
2239
+ throw new Error(`${label} must be a valid PNG, JPEG, WebP or SVG image.`);
2240
+ }
2241
+ function looksLikeSvg(bytes) {
2242
+ const prefix = bytes.subarray(0, Math.min(bytes.length, 512)).toString("utf8").replace(/^\uFEFF/, "").trimStart();
2243
+ return prefix.startsWith("<svg") || prefix.startsWith("<?xml") || prefix.startsWith("<!--");
2244
+ }
2245
+ function validateSvg(bytes, label) {
2246
+ let text;
2247
+ try {
2248
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
2249
+ } catch {
2250
+ throw new Error(`${label} is not valid UTF-8 SVG content.`);
2251
+ }
2252
+ const trimmed = text.replace(/^\uFEFF/, "").trim();
2253
+ const root = /^(?:<\?xml[\s\S]*?\?>\s*)?(?:<!--[\s\S]*?-->\s*)*<svg\b/i;
2254
+ if (!root.test(trimmed) || !/<\/svg>\s*$/i.test(trimmed) && !/<svg\b[\s\S]*\/>\s*$/i.test(trimmed)) {
2255
+ throw new Error(`${label} is not a well-formed SVG document.`);
2256
+ }
2257
+ if (/&#(?:x[0-9a-f]+|[0-9]+);/i.test(trimmed)) {
2258
+ throw new Error(`${label} contains numeric XML character references, which are not allowed in SVG content.`);
2259
+ }
2260
+ if (/<!DOCTYPE|<!ENTITY/i.test(trimmed) || /<(?:script|foreignObject|iframe|object|embed|audio|video)\b/i.test(trimmed) || /\son[a-z]+\s*=/i.test(trimmed) || /(?:javascript|vbscript)\s*:/i.test(trimmed) || /data\s*:\s*text\/html/i.test(trimmed)) {
2261
+ throw new Error(`${label} contains active SVG content, which is not allowed.`);
2262
+ }
2263
+ }
2264
+ function validateMarkdown(bytes, _path, label) {
2265
+ try {
2266
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
2267
+ if (text.includes("\0")) throw new Error("NUL");
2268
+ } catch {
2269
+ throw new Error(`${label} must contain valid UTF-8 Markdown text.`);
2270
+ }
2271
+ }
2272
+ function formatMiB(bytes) {
2273
+ return bytes / MIB;
2274
+ }
2275
+ function toEntryPath(path) {
2276
+ return path.split(sep).join("/");
2277
+ }
2278
+ function isPlainObject(value) {
2279
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2280
+ }
2281
+ function deepMerge(base, overlay) {
2282
+ if (!isPlainObject(base) || !isPlainObject(overlay)) {
2283
+ return overlay;
2284
+ }
2285
+ const result = { ...base };
2286
+ for (const [key, value] of Object.entries(overlay)) {
2287
+ result[key] = key in result ? deepMerge(result[key], value) : value;
2288
+ }
2289
+ return result;
2290
+ }
2291
+ function applyOverlay(base, overlay) {
2292
+ return deepMerge(base, overlay);
2293
+ }
2294
+ function resolveOverlayPath(projectDir, selector) {
2295
+ if (selector.overlay) {
2296
+ return isAbsolute(selector.overlay) ? selector.overlay : resolve(selector.overlay);
2297
+ }
2298
+ if (selector.env) {
2299
+ return join(projectDir, "environments", `${selector.env}.yaml`);
2300
+ }
2301
+ return void 0;
2302
+ }
2303
+ async function loadOverlay(path) {
2304
+ let text;
2305
+ try {
2306
+ text = await readFile(path, "utf8");
2307
+ } catch (error) {
2308
+ if (error?.code === "ENOENT") {
2309
+ throw new Error(`Overlay file not found: ${path}`);
2310
+ }
2311
+ throw error;
2312
+ }
2313
+ const parsed = parse(text);
2314
+ if (parsed === null || parsed === void 0) return {};
2315
+ if (!isPlainObject(parsed)) {
2316
+ throw new Error(`Overlay ${path} must be a YAML mapping (got ${Array.isArray(parsed) ? "a list" : typeof parsed}).`);
2317
+ }
2318
+ return parsed;
2319
+ }
2320
+
2321
+ // src/vpkg/pack.ts
1912
2322
  async function pack(options) {
1913
2323
  const manifestPath = resolve(options.manifestPath);
1914
2324
  const projectDir = dirname(manifestPath);
1915
- const manifest = await loadManifest(manifestPath);
2325
+ const base = await loadManifest(manifestPath);
2326
+ const overlayPath = resolveOverlayPath(projectDir, { env: options.env, overlay: options.overlay });
2327
+ let manifest = overlayPath ? applyOverlay(base, await loadOverlay(overlayPath)) : base;
2328
+ let overrideApplied = overlayPath !== void 0;
2329
+ if (options.version !== void 0) {
2330
+ manifest = { ...manifest, metadata: { ...manifest.metadata, version: options.version } };
2331
+ overrideApplied = true;
2332
+ }
1916
2333
  const kind = normalizeKind(manifest.kind);
2334
+ const launchKindError = validateLaunchManifestKind(kind, manifest.spec);
2335
+ if (launchKindError) throw new Error(`Invalid manifest: ${launchKindError}.`);
2336
+ const marketplaceErrors = validateMarketplaceSection(manifest.marketplace);
2337
+ if (marketplaceErrors.length > 0) throw new Error(`Invalid marketplace section: ${marketplaceErrors.join(" ")}`);
1917
2338
  const name = (options.name ?? manifest.packaging?.name ?? manifest.metadata.name).toLowerCase();
1918
2339
  const publisher = options.publisher.toLowerCase();
1919
2340
  validatePackageName(name, "package name");
1920
2341
  validatePackageName(publisher, "publisher name");
1921
2342
  const version = manifest.metadata.version;
1922
2343
  const payloadFiles = await resolvePayloadFiles(projectDir, manifestPath, manifest);
2344
+ const catalog = manifest.marketplace ? await buildCatalog(projectDir, manifest.marketplace) : void 0;
1923
2345
  const staging = await mkdtemp(join(tmpdir(), "vpkg-"));
1924
2346
  try {
1925
- const manifestText = await readFile(manifestPath, "utf8");
2347
+ const manifestText = overrideApplied ? stringify(manifest) : await readFile(manifestPath, "utf8");
1926
2348
  await writeFile(join(staging, "manifest.yaml"), manifestText);
1927
2349
  const encrypt = (options.encryptFor?.length ?? 0) > 0;
1928
2350
  const contentKey = encrypt ? generateContentKey() : null;
@@ -1931,13 +2353,24 @@ async function pack(options) {
1931
2353
  };
1932
2354
  for (const file of payloadFiles) {
1933
2355
  const sourcePath = join(projectDir, file);
1934
- const entryName = `files/${toEntryPath(file)}`;
2356
+ const entryName = `files/${toEntryPath2(file)}`;
1935
2357
  const targetPath = join(staging, "files", file);
1936
2358
  await mkdir(dirname(targetPath), { recursive: true });
1937
2359
  const bytes = contentKey ? encryptBytes(contentKey, await readFile(sourcePath)) : await readFile(sourcePath);
1938
2360
  await writeFile(targetPath, bytes);
1939
2361
  digests[entryName] = sha256Hex(bytes);
1940
2362
  }
2363
+ if (catalog) {
2364
+ for (const entry of catalog.entries) {
2365
+ const targetPath = join(staging, entry.path);
2366
+ await mkdir(dirname(targetPath), { recursive: true });
2367
+ await writeFile(targetPath, entry.bytes);
2368
+ digests[entry.path] = sha256Hex(entry.bytes);
2369
+ }
2370
+ await mkdir(join(staging, "catalog"), { recursive: true });
2371
+ await writeFile(join(staging, "catalog", "index.json"), catalog.indexJson);
2372
+ digests["catalog/index.json"] = sha256Hex(Buffer.from(catalog.indexJson, "utf8"));
2373
+ }
1941
2374
  await mkdir(join(staging, ".verentis", "signatures"), { recursive: true });
1942
2375
  if (contentKey) {
1943
2376
  const encryption = {
@@ -1973,16 +2406,19 @@ async function pack(options) {
1973
2406
  ".verentis/digests.json",
1974
2407
  ...contentKey ? [".verentis/encryption.json"] : [],
1975
2408
  ...signed ? [".verentis/signatures/developer.dsse.json"] : [],
1976
- ...payloadFiles.map((file) => `files/${toEntryPath(file)}`)
2409
+ ...payloadFiles.map((file) => `files/${toEntryPath2(file)}`),
2410
+ ...catalog ? ["catalog/index.json", ...catalog.entries.map((entry) => entry.path)] : []
1977
2411
  ];
1978
- await tar.create({ gzip: true, file: outFile, cwd: staging, portable: true }, entries);
2412
+ await tar.create({ gzip: true, file: outFile, cwd: staging, portable: true, mtime: /* @__PURE__ */ new Date(0) }, entries);
1979
2413
  return {
1980
2414
  outFile,
1981
2415
  name,
1982
2416
  publisher,
1983
2417
  version,
1984
2418
  kind,
1985
- files: payloadFiles.map(toEntryPath),
2419
+ files: payloadFiles.map(toEntryPath2),
2420
+ catalog: catalog?.index,
2421
+ catalogFiles: catalog ? ["index.json", ...catalog.entries.map((entry) => entry.path.replace(/^catalog\//, ""))] : [],
1986
2422
  treeDigest: sha256Hex(Buffer.from(digestsJson, "utf8")),
1987
2423
  signed,
1988
2424
  encrypted: encrypt
@@ -2009,7 +2445,7 @@ async function resolvePayloadFiles(projectDir, manifestPath, manifest) {
2009
2445
  }
2010
2446
  function defaultIncludes(manifest) {
2011
2447
  const sources = /* @__PURE__ */ new Set();
2012
- collectSchemaSources(manifest, sources);
2448
+ collectSchemaSources(manifest.spec, sources);
2013
2449
  collectInstallSources(manifest, sources);
2014
2450
  if (sources.size === 0) return [];
2015
2451
  return [...sources];
@@ -2040,7 +2476,7 @@ function collectSchemaSources(node, sources) {
2040
2476
  }
2041
2477
  }
2042
2478
  }
2043
- function toEntryPath(file) {
2479
+ function toEntryPath2(file) {
2044
2480
  return file.split(sep).join("/");
2045
2481
  }
2046
2482
  function sha256Hex(data) {
@@ -2061,21 +2497,18 @@ async function readVpkg(vpkgPath) {
2061
2497
  });
2062
2498
  const metadata = parse(packageYaml);
2063
2499
  const declared = JSON.parse(digestsJson);
2064
- const files2 = [];
2065
- for await (const entry of glob("files/**/*", { cwd: staging })) {
2066
- const abs = join(staging, entry);
2067
- const info = await readFile(abs).catch(() => null);
2068
- if (info === null) continue;
2069
- files2.push(entry.split(sep).join("/"));
2070
- }
2071
- files2.sort();
2500
+ const files2 = await listFiles(staging, "files/**/*");
2501
+ const catalogFiles = await listFiles(staging, "catalog/**/*");
2502
+ const contentFiles = [...files2, ...catalogFiles];
2072
2503
  const digestErrors = [];
2073
2504
  const entries = declared.entries ?? {};
2074
2505
  const manifestDigest = createHash("sha256").update(manifestYaml, "utf8").digest("hex");
2075
- if (entries["manifest.yaml"] && entries["manifest.yaml"].toLowerCase() !== manifestDigest) {
2506
+ if (!entries["manifest.yaml"]) {
2507
+ digestErrors.push("Entry 'manifest.yaml' is not covered by digests.json.");
2508
+ } else if (entries["manifest.yaml"].toLowerCase() !== manifestDigest) {
2076
2509
  digestErrors.push("Digest mismatch for manifest.yaml.");
2077
2510
  }
2078
- for (const file of files2) {
2511
+ for (const file of contentFiles) {
2079
2512
  const declaredDigest = entries[file];
2080
2513
  if (!declaredDigest) {
2081
2514
  digestErrors.push(`Entry '${file}' is not covered by digests.json.`);
@@ -2085,10 +2518,21 @@ async function readVpkg(vpkgPath) {
2085
2518
  if (actual !== declaredDigest.toLowerCase()) digestErrors.push(`Digest mismatch for '${file}'.`);
2086
2519
  }
2087
2520
  for (const declaredPath of Object.keys(entries)) {
2088
- if (declaredPath !== "manifest.yaml" && !files2.includes(declaredPath)) {
2521
+ if (declaredPath !== "manifest.yaml" && !contentFiles.includes(declaredPath)) {
2089
2522
  digestErrors.push(`Digest declared for missing entry '${declaredPath}'.`);
2090
2523
  }
2091
2524
  }
2525
+ let catalog;
2526
+ if (catalogFiles.includes("catalog/index.json")) {
2527
+ try {
2528
+ catalog = JSON.parse(await readFile(join(staging, "catalog", "index.json"), "utf8"));
2529
+ if (catalog.schemaVersion !== 1) digestErrors.push(`Unsupported catalog schema version '${String(catalog.schemaVersion)}'.`);
2530
+ } catch {
2531
+ digestErrors.push("Invalid catalog/index.json.");
2532
+ }
2533
+ } else if (catalogFiles.length > 0) {
2534
+ digestErrors.push("Catalog entries are present but catalog/index.json is missing.");
2535
+ }
2092
2536
  const developerSignature = await readJsonIfExists(join(staging, ".verentis", "signatures", "developer.dsse.json"));
2093
2537
  const marketplaceSignature = await readJsonIfExists(join(staging, ".verentis", "signatures", "marketplace.dsse.json"));
2094
2538
  const encryption = await readJsonIfExists(join(staging, ".verentis", "encryption.json"));
@@ -2101,12 +2545,23 @@ async function readVpkg(vpkgPath) {
2101
2545
  marketplaceSignature: marketplaceSignature ?? void 0,
2102
2546
  encryption: encryption ?? void 0,
2103
2547
  files: files2.map((f) => f.replace(/^files\//, "")),
2548
+ catalog,
2549
+ catalogFiles: catalogFiles.map((f) => f.replace(/^catalog\//, "")),
2104
2550
  digestErrors
2105
2551
  };
2106
2552
  } finally {
2107
2553
  await rm(staging, { recursive: true, force: true });
2108
2554
  }
2109
2555
  }
2556
+ async function listFiles(root, pattern) {
2557
+ const files2 = [];
2558
+ for await (const entry of glob(pattern, { cwd: root })) {
2559
+ const abs = join(root, entry);
2560
+ const info = await readFile(abs).catch(() => null);
2561
+ if (info !== null) files2.push(entry.split(sep).join("/"));
2562
+ }
2563
+ return files2.sort();
2564
+ }
2110
2565
  async function readJsonIfExists(path) {
2111
2566
  try {
2112
2567
  return JSON.parse(await readFile(path, "utf8"));
@@ -2117,7 +2572,7 @@ async function readJsonIfExists(path) {
2117
2572
 
2118
2573
  // src/commands/pack.ts
2119
2574
  function registerPack(program2) {
2120
- program2.command("pack [dir]").description("Build a signed .vpkg from a project directory").option("--manifest <path>", "manifest path (default: auto-detected in the project directory)").option("--publisher <name>", "publisher name (default: packaging.publisher in the manifest)").option("--name <name>", "override the package name").option("--out <dir>", "output directory (default: the project directory)").option("--key <name>", "signing key name (default: the configured default key)").option("--no-sign", "produce an unsigned package").option("--encrypt", "seal the payload (AES-256-GCM); requires at least one recipient").option("--recipient <id=base64key...>", "additional X25519 recipient (repeatable)", collectRecipients, []).action(async (dir, options) => {
2575
+ program2.command("pack [dir]").description("Build a signed .vpkg from a project directory").option("--manifest <path>", "manifest path (default: auto-detected in the project directory)").option("--publisher <name>", "publisher name (default: packaging.publisher in the manifest)").option("--name <name>", "override the package name").option("--env <name>", "merge a per-environment overlay (environments/<name>.yaml) before packing").option("--overlay <path>", "merge an explicit overlay file before packing (wins over --env)").option("--set-version <version>", "override metadata.version (e.g. a CI-derived build number)").option("--out <dir>", "output directory (default: the project directory)").option("--key <name>", "signing key name (default: the configured default key)").option("--no-sign", "produce an unsigned package").option("--encrypt", "seal the payload (AES-256-GCM); requires at least one recipient").option("--recipient <id=base64key...>", "additional X25519 recipient (repeatable)", collectRecipients, []).action(async (dir, options) => {
2121
2576
  const projectDir = resolve(dir ?? ".");
2122
2577
  const manifestPath = options.manifest ? resolve(options.manifest) : await findManifest(projectDir);
2123
2578
  const manifest = await loadManifest(manifestPath);
@@ -2152,6 +2607,9 @@ function registerPack(program2) {
2152
2607
  manifestPath,
2153
2608
  publisher,
2154
2609
  name: options.name,
2610
+ env: options.env,
2611
+ overlay: options.overlay,
2612
+ version: options.setVersion,
2155
2613
  outDir: options.out,
2156
2614
  signingKey,
2157
2615
  encryptFor
@@ -2159,6 +2617,7 @@ function registerPack(program2) {
2159
2617
  console.log(`Packed ${result.publisher}/${result.name}@${result.version} (${result.kind})`);
2160
2618
  console.log(` ${result.outFile}`);
2161
2619
  console.log(` ${result.files.length} payload file(s), tree digest ${result.treeDigest.slice(0, 16)}\u2026, ${result.signed ? "signed" : "UNSIGNED"}${result.encrypted ? ", ENCRYPTED" : ""}`);
2620
+ if (result.catalog) console.log(` ${result.catalogFiles.length} plaintext catalog file(s)`);
2162
2621
  });
2163
2622
  program2.command("seal-keygen").description("Generate an X25519 sealing keypair (for platform escrow or custom recipients)").action(() => {
2164
2623
  const pair = generateSealingKeyPair();
@@ -2169,7 +2628,11 @@ function registerPack(program2) {
2169
2628
  program2.command("verify <vpkg>").description("Verify the digests (and, when possible, the developer signature) of a .vpkg").option("--key <name>", "verify the developer signature against a local key").action(async (vpkgPath, options) => {
2170
2629
  const contents = await readVpkg(resolve(vpkgPath));
2171
2630
  console.log(`${contents.metadata.publisher}/${contents.metadata.name}@${contents.metadata.version} (${contents.metadata.kind})`);
2172
- console.log(` files: ${contents.files.length}, tree digest ${contents.treeDigest.slice(0, 16)}\u2026`);
2631
+ console.log(` payload files: ${contents.files.length}, catalog files: ${contents.catalogFiles.length}, tree digest ${contents.treeDigest.slice(0, 16)}\u2026`);
2632
+ if (contents.catalog) {
2633
+ const docs = Number(!!contents.catalog.readme) + Number(!!contents.catalog.changelog);
2634
+ console.log(` catalog schema v${contents.catalog.schemaVersion}: ${contents.catalog.images?.length ?? 0} gallery image(s), ${docs} document(s)${contents.catalog.logo ? ", logo" : ""}${contents.catalog.hero ? ", hero" : ""}`);
2635
+ }
2173
2636
  if (contents.encryption) {
2174
2637
  console.log(` \u{1F512} encrypted (${contents.encryption.algorithm}); recipients: ${contents.encryption.recipients.map((r) => r.id).join(", ")}`);
2175
2638
  }
@@ -2310,19 +2773,62 @@ function registerPackage(program2) {
2310
2773
  output(pageItems(page), { packageName: "Package", version: "Version", status: "Status", installPath: "Path", id: "Id" }, options, page);
2311
2774
  });
2312
2775
  }
2776
+ var MAX_LOGO_BYTES2 = 1024 * 1024;
2777
+ var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
2778
+ async function readPublisherLogo(path) {
2779
+ const absolutePath = resolve(path);
2780
+ const info = await stat(absolutePath).catch(() => {
2781
+ throw new Error(`Publisher logo '${absolutePath}' does not exist.`);
2782
+ });
2783
+ if (!info.isFile()) throw new Error(`Publisher logo '${absolutePath}' is not a file.`);
2784
+ if (info.size > MAX_LOGO_BYTES2) {
2785
+ throw new Error(`Publisher logo exceeds the 1 MiB limit (${info.size} bytes).`);
2786
+ }
2787
+ const bytes = await readFile(absolutePath);
2788
+ return { path: absolutePath, bytes, contentType: detectPublisherLogoContentType(bytes) };
2789
+ }
2790
+ function detectPublisherLogoContentType(bytes) {
2791
+ if (bytes.length >= 24 && bytes.subarray(0, 8).equals(PNG_SIGNATURE) && bytes.readUInt32BE(8) === 13 && bytes.toString("ascii", 12, 16) === "IHDR") {
2792
+ return "image/png";
2793
+ }
2794
+ if (bytes.length >= 4 && bytes[0] === 255 && bytes[1] === 216 && bytes[bytes.length - 2] === 255 && bytes[bytes.length - 1] === 217) {
2795
+ return "image/jpeg";
2796
+ }
2797
+ if (bytes.length >= 12 && bytes.toString("ascii", 0, 4) === "RIFF" && bytes.toString("ascii", 8, 12) === "WEBP" && bytes.readUInt32LE(4) + 8 === bytes.length) {
2798
+ return "image/webp";
2799
+ }
2800
+ let text;
2801
+ try {
2802
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes).replace(/^\uFEFF/, "").trim();
2803
+ } catch {
2804
+ throw unsupportedLogoError();
2805
+ }
2806
+ const root = /^(?:<\?xml[\s\S]*?\?>\s*)?(?:<!--[\s\S]*?-->\s*)*<svg\b/i;
2807
+ const closed = /<\/svg>\s*$/i.test(text) || /<svg\b[\s\S]*\/>\s*$/i.test(text);
2808
+ if (!root.test(text) || !closed) throw unsupportedLogoError();
2809
+ if (/&#(?:x[0-9a-f]+|[0-9]+);/i.test(text)) {
2810
+ throw new Error("Publisher logo SVG contains numeric XML character references, which are not allowed.");
2811
+ }
2812
+ if (/<!DOCTYPE|<!ENTITY/i.test(text) || /<(?:script|foreignObject|iframe|object|embed|audio|video)\b/i.test(text) || /\son[a-z]+\s*=/i.test(text) || /(?:javascript|vbscript)\s*:/i.test(text) || /data\s*:\s*text\/html/i.test(text)) {
2813
+ throw new Error("Publisher logo SVG contains active content, which is not allowed.");
2814
+ }
2815
+ return "image/svg+xml";
2816
+ }
2817
+ function unsupportedLogoError() {
2818
+ return new Error("Publisher logo must be a valid PNG, JPEG, WebP or SVG image.");
2819
+ }
2313
2820
 
2314
2821
  // src/commands/publisher.ts
2315
2822
  function registerPublisher(program2) {
2316
2823
  const publisherCmd = program2.command("publisher").description("Manage your publisher profile");
2317
- publisherCmd.command("create").description("Onboard your account as a marketplace publisher").requiredOption("--name <name>", "URL-safe publisher name (e.g. acme)").requiredOption("--display-name <displayName>", "human-readable publisher name").option("--description <description>").option("--website <url>").option("--email <email>").action(async (options) => {
2824
+ publisherCmd.command("create").description("Onboard your account as a marketplace publisher").requiredOption("--name <name>", "URL-safe publisher name (e.g. acme)").requiredOption("--display-name <displayName>", "human-readable publisher name").option("--description <description>").option("--website <url>").option("--email <email>").option("--logo <file>", "PNG, JPEG, WebP or SVG logo (maximum 1 MiB)").action(async (options) => {
2318
2825
  const api = await createApiClient();
2319
2826
  const existing = await getMyPublisher(api);
2320
2827
  if (existing) {
2321
2828
  console.log(`This account already has publisher '${existing.name}'.`);
2322
2829
  return;
2323
2830
  }
2324
- const id = await api.request("POST", "/v1/publishers", {
2325
- scopes: [SCOPES.publisherCreate],
2831
+ const id = await call(api, marketplace.createPublisher, {
2326
2832
  body: {
2327
2833
  name: options.name,
2328
2834
  displayName: options.displayName,
@@ -2332,8 +2838,51 @@ function registerPublisher(program2) {
2332
2838
  }
2333
2839
  });
2334
2840
  console.log(`Publisher '${options.name}' created (${id}).`);
2841
+ if (options.logo) {
2842
+ await uploadPublisherLogo(api, id, options.logo);
2843
+ console.log(`Publisher logo uploaded from ${options.logo}.`);
2844
+ }
2335
2845
  console.log("Next: generate and register a signing key with `verentis keygen`.");
2336
2846
  });
2847
+ publisherCmd.command("update").description("Update your publisher profile").option("--display-name <displayName>").option("--description <description>").option("--website <url>").option("--email <email>").action(async (options) => {
2848
+ if (Object.values(options).every((value) => value === void 0)) {
2849
+ throw new Error("No publisher fields supplied. Pass --display-name, --description, --website or --email.");
2850
+ }
2851
+ const api = await createApiClient();
2852
+ const current = await requireMyPublisher(api);
2853
+ await call(api, marketplace.updatePublisher, {
2854
+ params: { id: current.id },
2855
+ body: {
2856
+ id: current.id,
2857
+ displayName: options.displayName ?? current.displayName,
2858
+ description: options.description ?? current.description ?? null,
2859
+ websiteUrl: options.website ?? current.websiteUrl ?? null,
2860
+ contactEmail: options.email ?? current.contactEmail ?? null
2861
+ }
2862
+ });
2863
+ console.log(`Publisher '${current.name}' updated.`);
2864
+ });
2865
+ const logoCmd = publisherCmd.command("logo").description("Manage your publisher logo");
2866
+ logoCmd.command("set <file>").description("Upload a PNG, JPEG, WebP or SVG publisher logo (maximum 1 MiB)").action(async (file) => {
2867
+ const api = await createApiClient();
2868
+ const publisher = await requireMyPublisher(api);
2869
+ await uploadPublisherLogo(api, publisher.id, file);
2870
+ console.log(`Publisher logo uploaded from ${file}.`);
2871
+ });
2872
+ logoCmd.command("remove").description("Remove your publisher logo").action(async () => {
2873
+ const api = await createApiClient();
2874
+ const publisher = await requireMyPublisher(api);
2875
+ await call(api, marketplace.removePublisherLogo, { params: { id: publisher.id } });
2876
+ console.log("Publisher logo removed.");
2877
+ });
2878
+ }
2879
+ async function uploadPublisherLogo(api, publisherId, path) {
2880
+ const logo = await readPublisherLogo(path);
2881
+ await callRaw(api, marketplace.setPublisherLogo, {
2882
+ params: { id: publisherId },
2883
+ rawBody: new Blob([new Uint8Array(logo.bytes)], { type: logo.contentType }),
2884
+ contentType: logo.contentType
2885
+ });
2337
2886
  }
2338
2887
  function registerRegistry(program2) {
2339
2888
  program2.command("publish <vpkg>").description("Upload a packed .vpkg to the marketplace registry (creates the package on first publish)").option("--visibility <visibility>", "catalog visibility for a newly created package: Public | Private", "Private").action(async (vpkgPath, options) => {
@@ -2492,11 +3041,11 @@ function getCliVersion() {
2492
3041
  }
2493
3042
  function readVersion() {
2494
3043
  const candidates = ["../package.json", "../../package.json"];
2495
- for (const relative2 of candidates) {
3044
+ for (const relative3 of candidates) {
2496
3045
  try {
2497
- const raw = readFileSync(fileURLToPath(new URL(relative2, import.meta.url)), "utf8");
3046
+ const raw = readFileSync(fileURLToPath(new URL(relative3, import.meta.url)), "utf8");
2498
3047
  const pkg = JSON.parse(raw);
2499
- if (pkg.version && (relative2 === "../package.json" || pkg.name === PACKAGE_NAME)) return pkg.version;
3048
+ if (pkg.version && (relative3 === "../package.json" || pkg.name === PACKAGE_NAME)) return pkg.version;
2500
3049
  } catch {
2501
3050
  }
2502
3051
  }
@@ -2534,8 +3083,8 @@ function isNewerVersion(candidate, base) {
2534
3083
  return compareVersions(candidate, base) > 0;
2535
3084
  }
2536
3085
  function compareVersions(a, b) {
2537
- const pa = parse4(a);
2538
- const pb = parse4(b);
3086
+ const pa = parse5(a);
3087
+ const pb = parse5(b);
2539
3088
  for (let i = 0; i < 3; i++) {
2540
3089
  if (pa.main[i] !== pb.main[i]) return pa.main[i] < pb.main[i] ? -1 : 1;
2541
3090
  }
@@ -2562,7 +3111,7 @@ function compareVersions(a, b) {
2562
3111
  }
2563
3112
  return 0;
2564
3113
  }
2565
- function parse4(version) {
3114
+ function parse5(version) {
2566
3115
  const cleaned = version.trim().replace(/^v/i, "").split("+", 1)[0];
2567
3116
  const dash = cleaned.indexOf("-");
2568
3117
  const core = dash === -1 ? cleaned : cleaned.slice(0, dash);
@@ -2839,13 +3388,22 @@ function registerUser(program2) {
2839
3388
  });
2840
3389
  }
2841
3390
  function registerValidate(program2) {
2842
- program2.command("validate [pathOrDir]").description("Validate an engine/app/bundle manifest (structure + registration-time checks)").action(async (pathOrDir) => {
3391
+ program2.command("validate [pathOrDir]").description("Validate an engine/app/bundle manifest (structure + registration-time checks)").option("--env <name>", "merge a per-environment overlay (environments/<name>.yaml) before validating").option("--overlay <path>", "merge an explicit overlay file before validating (wins over --env)").action(async (pathOrDir, options) => {
2843
3392
  const target = resolve(pathOrDir ?? ".");
2844
3393
  const info = await stat(target).catch(() => null);
2845
3394
  const manifestPath = info?.isDirectory() ? await findManifest(target) : target;
2846
3395
  if (!info) throw new Error(`${target} does not exist.`);
2847
- const manifest = await readManifestDocument(manifestPath);
3396
+ const base = await readManifestDocument(manifestPath);
3397
+ const overlayPath = resolveOverlayPath(dirname(manifestPath), { env: options.env, overlay: options.overlay });
3398
+ const manifest = overlayPath ? applyOverlay(base, await loadOverlay(overlayPath)) : base;
2848
3399
  const issues = validateManifest(manifest);
3400
+ if (manifest.marketplace && !issues.some((issue) => issue.level === "error" && issue.message.startsWith("marketplace"))) {
3401
+ try {
3402
+ await buildCatalog(dirname(manifestPath), manifest.marketplace);
3403
+ } catch (error) {
3404
+ issues.push({ level: "error", message: error instanceof Error ? error.message : String(error) });
3405
+ }
3406
+ }
2849
3407
  const errors = issues.filter((issue) => issue.level === "error");
2850
3408
  const warnings = issues.filter((issue) => issue.level === "warning");
2851
3409
  console.log(`${manifestPath} \u2014 kind ${manifest.kind ?? "(unknown)"}, ${manifest.metadata?.name ?? "?"}@${manifest.metadata?.version ?? "?"}`);
@@ -2944,7 +3502,7 @@ function registerWorkspace(program2) {
2944
3502
  }
2945
3503
 
2946
3504
  // src/index.ts
2947
- var program = new Command().name("verentis").description("Verentis CLI \u2014 admin, marketplace publishing, and the engine/app developer loop").version(getCliVersion());
3505
+ var program = new Command().enablePositionalOptions().name("verentis").description("Verentis CLI \u2014 admin, marketplace publishing, and the engine/app developer loop").version(getCliVersion());
2948
3506
  registerLogin(program);
2949
3507
  registerUse(program);
2950
3508
  registerAccount(program);