@verentis/cli 0.2.4 → 0.2.6

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,17 +1,22 @@
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
+ import { Agent } from 'https';
4
5
  import { tmpdir, homedir } from 'os';
5
- import { resolve, basename, dirname, posix, join, sep, relative } from 'path';
6
+ import { resolve, basename, dirname, posix, join, isAbsolute, extname, relative, sep } from 'path';
6
7
  import { spawn, spawnSync } from 'child_process';
7
8
  import { generateKeyPairSync, createHash, randomUUID, randomBytes, createCipheriv, createPublicKey, verify, diffieHellman, hkdfSync, sign, createPrivateKey } from 'crypto';
8
- import { parse } from 'yaml';
9
+ import { parse, stringify } from 'yaml';
9
10
  import { createWriteStream, readFileSync, createReadStream } from 'fs';
10
11
  import { Readable } from 'stream';
11
12
  import { pipeline } from 'stream/promises';
12
13
  import * as tar from 'tar';
13
14
  import { fileURLToPath } from 'url';
14
15
 
16
+ var insecureAgent = new Agent({ rejectUnauthorized: false });
17
+ function insecureFetchOptions(insecure) {
18
+ return insecure ? { dispatcher: insecureAgent } : {};
19
+ }
15
20
  var configDir = () => process.env.VERENTIS_CONFIG_DIR ?? join(homedir(), ".verentis");
16
21
  var configPath = () => join(configDir(), "config.json");
17
22
  var keysDir = () => join(configDir(), "keys");
@@ -53,11 +58,12 @@ function isExpired(token, skewSeconds = 60) {
53
58
 
54
59
  // src/auth/deviceFlow.ts
55
60
  var CLI_CLIENT_ID = "Verentis.Cli";
56
- async function startDeviceAuthorization(apiUrl) {
61
+ async function startDeviceAuthorization(apiUrl, insecure) {
57
62
  const res = await fetch(`${apiUrl}/connect/device/authorize`, {
58
63
  method: "POST",
59
64
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
60
- body: new URLSearchParams({ client_id: CLI_CLIENT_ID })
65
+ body: new URLSearchParams({ client_id: CLI_CLIENT_ID }),
66
+ ...insecureFetchOptions(insecure)
61
67
  });
62
68
  if (!res.ok) {
63
69
  const text = await res.text().catch(() => "");
@@ -65,7 +71,7 @@ async function startDeviceAuthorization(apiUrl) {
65
71
  }
66
72
  return await res.json();
67
73
  }
68
- async function pollForIdentityToken(apiUrl, authorization, onPoll) {
74
+ async function pollForIdentityToken(apiUrl, authorization, insecure, onPoll) {
69
75
  let intervalMs = Math.max(authorization.interval, 1) * 1e3;
70
76
  const deadline = Date.now() + authorization.expires_in * 1e3;
71
77
  while (Date.now() < deadline) {
@@ -78,7 +84,8 @@ async function pollForIdentityToken(apiUrl, authorization, onPoll) {
78
84
  grant_type: "device_code",
79
85
  device_code: authorization.device_code,
80
86
  client_id: CLI_CLIENT_ID
81
- })
87
+ }),
88
+ ...insecureFetchOptions(insecure)
82
89
  });
83
90
  let payload;
84
91
  try {
@@ -107,7 +114,7 @@ async function pollForIdentityToken(apiUrl, authorization, onPoll) {
107
114
  }
108
115
  var RefreshTokenError = class extends Error {
109
116
  };
110
- async function refreshIdentityToken(apiUrl, refreshToken) {
117
+ async function refreshIdentityToken(apiUrl, refreshToken, insecure) {
111
118
  const res = await fetch(`${apiUrl}/connect/token`, {
112
119
  method: "POST",
113
120
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
@@ -115,7 +122,8 @@ async function refreshIdentityToken(apiUrl, refreshToken) {
115
122
  grant_type: "refresh_token",
116
123
  refresh_token: refreshToken,
117
124
  client_id: CLI_CLIENT_ID
118
- })
125
+ }),
126
+ ...insecureFetchOptions(insecure)
119
127
  });
120
128
  let payload;
121
129
  try {
@@ -130,7 +138,7 @@ async function refreshIdentityToken(apiUrl, refreshToken) {
130
138
  `Session could not be renewed${payload?.error ? ` (${payload.error})` : ` (${res.status})`}. Run \`verentis login\` again.`
131
139
  );
132
140
  }
133
- var sleep = (ms) => new Promise((resolve8) => setTimeout(resolve8, ms));
141
+ var sleep = (ms) => new Promise((resolve11) => setTimeout(resolve11, ms));
134
142
 
135
143
  // src/http.ts
136
144
  var ApiError = class extends Error {
@@ -146,6 +154,7 @@ var AUDIENCE = "verentis";
146
154
  async function createApiClient(overrides) {
147
155
  const config = { ...await loadConfig(), ...overrides };
148
156
  const apiUrl = requireApiUrl(config);
157
+ const insecure = config.insecure;
149
158
  const tokenCache = /* @__PURE__ */ new Map();
150
159
  const envIdentity = process.env.VERENTIS_TOKEN;
151
160
  let currentIdentity = envIdentity ?? config.identityToken;
@@ -172,7 +181,7 @@ async function createApiClient(overrides) {
172
181
  if (!refreshInflight) {
173
182
  refreshInflight = (async () => {
174
183
  try {
175
- const refreshed = await refreshIdentityToken(apiUrl, currentRefresh);
184
+ const refreshed = await refreshIdentityToken(apiUrl, currentRefresh, insecure);
176
185
  currentIdentity = refreshed.idToken;
177
186
  currentRefresh = refreshed.refreshToken ?? currentRefresh;
178
187
  await persistTokens(refreshed.idToken, refreshed.refreshToken);
@@ -217,7 +226,8 @@ async function createApiClient(overrides) {
217
226
  ...options.workspaceId ? { workspaceId: options.workspaceId } : {},
218
227
  ...options.accountId ? { accountId: options.accountId } : {},
219
228
  ...options.resourceId ? { resourceId: options.resourceId, resourceKind: options.resourceKind ?? "Node" } : {}
220
- })
229
+ }),
230
+ ...insecureFetchOptions(insecure)
221
231
  });
222
232
  if (!res.ok) {
223
233
  const text = await res.text().catch(() => "");
@@ -244,7 +254,7 @@ ${truncate(text)}` : ""}`);
244
254
  headers["Content-Type"] = "application/json";
245
255
  }
246
256
  for (const [key, value] of Object.entries(options.headers ?? {})) headers[key] = value;
247
- const res = await fetch(url, { method, headers, body, ...options.rawBody !== void 0 ? { duplex: "half" } : {} });
257
+ const res = await fetch(url, { method, headers, body, ...insecureFetchOptions(insecure), ...options.rawBody !== void 0 ? { duplex: "half" } : {} });
248
258
  if (!res.ok) {
249
259
  const text = await res.text().catch(() => "");
250
260
  throw new ApiError(res.status, `${method} ${path} failed (${res.status}). ${hintFor(res.status)}${formatErrorBody(text)}`);
@@ -298,7 +308,6 @@ ${truncate(text)}`;
298
308
  }
299
309
  var SCOPES = {
300
310
  publisherRead: "marketplace.publisher.read",
301
- publisherCreate: "marketplace.publisher.create",
302
311
  publisherUpdate: "marketplace.publisher.update",
303
312
  packageRead: "marketplace.package.read",
304
313
  packageReadAll: "marketplace.package.read-all",
@@ -367,6 +376,8 @@ var MARKETPLACE = {
367
376
  packageRead: "marketplace.package.read",
368
377
  packageUpdate: "marketplace.package.update",
369
378
  packageVisibilityManage: "marketplace.package.visibility.manage",
379
+ publisherCreate: "marketplace.publisher.create",
380
+ publisherUpdate: "marketplace.publisher.update",
370
381
  statsRead: "marketplace.stats.read"};
371
382
  var ENGINE_DEFAULT_PERMISSIONS = [
372
383
  NODE.fileRead,
@@ -455,6 +466,10 @@ var execution = {
455
466
  syncEngines: e({ method: "POST", path: "/v1/engines/sync", scopes: [EXECUTION.run], workspaceBound: true })
456
467
  };
457
468
  var marketplace = {
469
+ createPublisher: e({ method: "POST", path: "/v1/publishers", scopes: [MARKETPLACE.publisherCreate] }),
470
+ updatePublisher: e({ method: "PUT", path: "/v1/publishers/{id}", scopes: [MARKETPLACE.publisherUpdate] }),
471
+ setPublisherLogo: e({ method: "PUT", path: "/v1/publishers/{id}/logo", scopes: [MARKETPLACE.publisherUpdate] }),
472
+ removePublisherLogo: e({ method: "DELETE", path: "/v1/publishers/{id}/logo", scopes: [MARKETPLACE.publisherUpdate] }),
458
473
  getPackage: e({ method: "GET", path: "/v1/packages/{id}", scopes: [MARKETPLACE.packageRead] }),
459
474
  updatePackage: e({ method: "PUT", path: "/v1/packages/{id}", scopes: [MARKETPLACE.packageUpdate] }),
460
475
  archivePackage: e({ method: "DELETE", path: "/v1/packages/{id}", scopes: [MARKETPLACE.packageDelete] }),
@@ -845,6 +860,122 @@ function contextEnv(context, contextPath) {
845
860
  VERENTIS_TIMEOUT: String(context.execution.timeout)
846
861
  };
847
862
  }
863
+ var KIND_MAP = {
864
+ application: "Application",
865
+ executionengine: "ExecutionEngine",
866
+ bundle: "Bundle"
867
+ };
868
+ function normalizeKind(kind) {
869
+ const mapped = KIND_MAP[kind.replace(/[^a-z]/gi, "").toLowerCase()];
870
+ if (!mapped) throw new Error(`Unsupported manifest kind '${kind}' \u2014 expected Application, ExecutionEngine or Bundle.`);
871
+ return mapped;
872
+ }
873
+ function validateLaunchManifestKind(kind, spec) {
874
+ if (kind === "Application" || !spec || typeof spec !== "object" || Array.isArray(spec)) return void 0;
875
+ if (!Object.prototype.hasOwnProperty.call(spec, "launch")) return void 0;
876
+ return "spec.launch is only supported for Application manifests";
877
+ }
878
+ async function loadManifest(path) {
879
+ const text = await readFile(path, "utf8");
880
+ const manifest = parse(text);
881
+ if (!manifest || typeof manifest !== "object") throw new Error(`Failed to parse manifest at ${path}.`);
882
+ if (!manifest.kind) throw new Error(`Manifest at ${path} is missing 'kind'.`);
883
+ if (!manifest.metadata?.name) throw new Error(`Manifest at ${path} is missing 'metadata.name'.`);
884
+ if (!manifest.metadata?.version) throw new Error(`Manifest at ${path} is missing 'metadata.version'.`);
885
+ const kind = normalizeKind(manifest.kind);
886
+ const launchKindError = validateLaunchManifestKind(kind, manifest.spec);
887
+ if (launchKindError) throw new Error(`Invalid manifest at ${path}: ${launchKindError}.`);
888
+ const marketplaceErrors = validateMarketplaceSection(manifest.marketplace);
889
+ if (marketplaceErrors.length > 0) {
890
+ throw new Error(`Invalid marketplace section in ${path}: ${marketplaceErrors.join(" ")}`);
891
+ }
892
+ return manifest;
893
+ }
894
+ function validateMarketplaceSection(marketplace2) {
895
+ if (marketplace2 === void 0) return [];
896
+ if (!marketplace2 || typeof marketplace2 !== "object" || Array.isArray(marketplace2)) {
897
+ return ["marketplace must be an object."];
898
+ }
899
+ const section = marketplace2;
900
+ const errors = [];
901
+ for (const field of ["logo", "readme", "changelog"]) {
902
+ const value = section[field];
903
+ if (value !== void 0 && (typeof value !== "string" || value.trim().length === 0)) {
904
+ errors.push(`marketplace.${field} must be a non-empty relative path.`);
905
+ }
906
+ }
907
+ if (section.hero !== void 0) {
908
+ if (!section.hero || typeof section.hero !== "object" || Array.isArray(section.hero)) {
909
+ errors.push("marketplace.hero must be an object.");
910
+ } else {
911
+ const hero = section.hero;
912
+ if (typeof hero.source !== "string" || hero.source.trim().length === 0) {
913
+ errors.push("marketplace.hero.source must be a non-empty relative path.");
914
+ }
915
+ if (typeof hero.alt !== "string" || hero.alt.trim().length === 0) {
916
+ errors.push("marketplace.hero.alt is required and must not be empty.");
917
+ }
918
+ }
919
+ }
920
+ if (section.images !== void 0) {
921
+ if (!Array.isArray(section.images)) {
922
+ errors.push("marketplace.images must be a list.");
923
+ } else {
924
+ if (section.images.length > 8) errors.push("marketplace.images may contain at most 8 images.");
925
+ section.images.forEach((value, index) => {
926
+ const at = `marketplace.images[${index}]`;
927
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
928
+ errors.push(`${at} must be an object.`);
929
+ return;
930
+ }
931
+ const image = value;
932
+ if (typeof image.source !== "string" || image.source.trim().length === 0) {
933
+ errors.push(`${at}.source must be a non-empty relative path.`);
934
+ }
935
+ if (typeof image.alt !== "string" || image.alt.trim().length === 0) {
936
+ errors.push(`${at}.alt is required and must not be empty.`);
937
+ }
938
+ if (image.caption !== void 0 && typeof image.caption !== "string") {
939
+ errors.push(`${at}.caption must be a string when provided.`);
940
+ }
941
+ });
942
+ }
943
+ }
944
+ if (section.assets !== void 0) {
945
+ if (!Array.isArray(section.assets)) {
946
+ errors.push("marketplace.assets must be a list of relative glob strings.");
947
+ } else {
948
+ section.assets.forEach((value, index) => {
949
+ if (typeof value !== "string" || value.trim().length === 0) {
950
+ errors.push(`marketplace.assets[${index}] must be a non-empty relative glob string.`);
951
+ }
952
+ });
953
+ }
954
+ }
955
+ return errors;
956
+ }
957
+ async function findManifest(dir) {
958
+ const explicit = join(dir, "manifest.yaml");
959
+ try {
960
+ if ((await stat(explicit)).isFile()) return explicit;
961
+ } catch {
962
+ }
963
+ const entries = await readdir(dir);
964
+ const candidates = entries.filter((name) => /\.(app|engine|bundle)\.ya?ml$/.test(name));
965
+ if (candidates.length === 1) return join(dir, candidates[0]);
966
+ if (candidates.length === 0) {
967
+ throw new Error(`No manifest found in ${dir}. Expected manifest.yaml or *.app.yaml / *.engine.yaml / *.bundle.yaml.`);
968
+ }
969
+ throw new Error(`Multiple manifests found in ${dir} (${candidates.join(", ")}). Pass --manifest to disambiguate.`);
970
+ }
971
+ var NAME_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
972
+ function validatePackageName(name, label) {
973
+ if (!NAME_PATTERN.test(name)) {
974
+ throw new Error(`Invalid ${label} '${name}' \u2014 use lowercase letters, digits and hyphens (no leading/trailing hyphen).`);
975
+ }
976
+ }
977
+
978
+ // src/dev/manifest.ts
848
979
  async function readManifestDocument(path) {
849
980
  const text = await readFile(path, "utf8");
850
981
  const doc = parse(text);
@@ -874,6 +1005,8 @@ function validateManifest(manifest) {
874
1005
  if (!meta[field]) error(`metadata.${field} is required`);
875
1006
  }
876
1007
  const spec = manifest.spec ?? {};
1008
+ const launchKindError = validateLaunchManifestKind(kind, spec);
1009
+ if (launchKindError) error(launchKindError);
877
1010
  if (kind === "ExecutionEngine") {
878
1011
  const runtimes = spec.runtimes ?? {};
879
1012
  if (Object.keys(runtimes).length === 0) {
@@ -902,12 +1035,62 @@ function validateManifest(manifest) {
902
1035
  "an Application must declare at least one contribution: spec.entry / spec.entry-points[] (iframe UI), spec.install[] (installed files), or spec.scopes[] (identity)"
903
1036
  );
904
1037
  }
1038
+ validateApplicationLaunch(spec, issues);
905
1039
  }
906
1040
  validateInstallDirectives(spec.install, issues);
1041
+ for (const message of validateMarketplaceSection(manifest.marketplace)) error(message);
907
1042
  const packaging = manifest.packaging ?? {};
908
1043
  if (!packaging.publisher) warning("packaging.publisher is not set \u2014 `verentis pack`/`publish` will refuse the package");
909
1044
  return issues;
910
1045
  }
1046
+ function validateApplicationLaunch(spec, issues) {
1047
+ const launch = spec.launch;
1048
+ if (launch === void 0) return;
1049
+ const error = (message) => issues.push({ level: "error", message });
1050
+ if (!launch || typeof launch !== "object" || Array.isArray(launch)) {
1051
+ error("spec.launch must be an object");
1052
+ return;
1053
+ }
1054
+ if (launch.pinnable === true && launch.enabled !== true) {
1055
+ error("spec.launch.pinnable requires spec.launch.enabled");
1056
+ }
1057
+ if (launch.enabled !== true) return;
1058
+ const launchEntryPoint = launch["entry-point"];
1059
+ if (launchEntryPoint !== void 0 && (typeof launchEntryPoint !== "string" || !launchEntryPoint.trim())) {
1060
+ error("spec.launch.entry-point must be a non-empty entry-point id");
1061
+ return;
1062
+ }
1063
+ if (typeof launchEntryPoint === "string") {
1064
+ const entryPoint = spec["entry-points"]?.find((candidate) => candidate.id === launchEntryPoint);
1065
+ if (!entryPoint) {
1066
+ error(`spec.launch.entry-point '${launchEntryPoint}' does not match spec.entry-points[].id`);
1067
+ return;
1068
+ }
1069
+ if (!resolvesIframeEntry(spec.entry, entryPoint.entry ?? entryPoint.route)) {
1070
+ error("spec.launch.enabled requires the selected entry point to resolve an iframe entry");
1071
+ }
1072
+ return;
1073
+ }
1074
+ if (!isAbsoluteHttpUrl(spec.entry)) {
1075
+ error("spec.launch.enabled requires spec.entry or spec.launch.entry-point");
1076
+ }
1077
+ }
1078
+ function isNonEmptyString(value) {
1079
+ return typeof value === "string" && value.trim().length > 0;
1080
+ }
1081
+ function resolvesIframeEntry(baseEntry, routeOrEntry) {
1082
+ if (isAbsoluteHttpUrl(routeOrEntry)) return true;
1083
+ return isAbsoluteHttpUrl(baseEntry);
1084
+ }
1085
+ function isAbsoluteHttpUrl(value) {
1086
+ if (!isNonEmptyString(value)) return false;
1087
+ try {
1088
+ const url = new URL(value);
1089
+ return url.protocol === "http:" || url.protocol === "https:";
1090
+ } catch {
1091
+ return false;
1092
+ }
1093
+ }
911
1094
  function validateInstallDirectives(install, issues) {
912
1095
  if (install === void 0) return;
913
1096
  const error = (message) => issues.push({ level: "error", message });
@@ -937,46 +1120,6 @@ function validateInstallDirectives(install, issues) {
937
1120
  }
938
1121
  });
939
1122
  }
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
1123
 
981
1124
  // src/commands/dev.ts
982
1125
  async function resolveDevScopes(flags) {
@@ -1326,7 +1469,7 @@ async function followExecution(api, workspaceId, executionId, json) {
1326
1469
  if (exec.status !== "Completed") process.exitCode = 1;
1327
1470
  return;
1328
1471
  }
1329
- await new Promise((resolve8) => setTimeout(resolve8, 1e3));
1472
+ await new Promise((resolve11) => setTimeout(resolve11, 1e3));
1330
1473
  }
1331
1474
  console.error("Timed out waiting for a terminal status \u2014 check `verentis exec get`.");
1332
1475
  process.exitCode = 1;
@@ -1479,10 +1622,31 @@ metadata:
1479
1622
  version: 0.1.0
1480
1623
  author: ""
1481
1624
 
1625
+ # Marketplace presentation (optional):
1626
+ # marketplace:
1627
+ # logo: ./marketplace/logo.png
1628
+ # hero:
1629
+ # source: ./marketplace/hero.webp
1630
+ # alt: A concise description of the hero background
1631
+ # images:
1632
+ # - source: ./marketplace/screenshot.png
1633
+ # alt: A concise description of the screenshot
1634
+ # caption: Optional visible caption
1635
+ # readme: ./README.md
1636
+ # changelog: ./CHANGELOG.md
1637
+ # assets:
1638
+ # - "marketplace/**/*.{png,jpg,jpeg,webp,svg}"
1639
+
1482
1640
  spec:
1483
1641
  entry: https://example.com # hosted app origin
1484
1642
  scope: global
1485
1643
 
1644
+ # Opt into a standalone workspace surface. Contextual file handlers should omit this block.
1645
+ # launch:
1646
+ # enabled: true
1647
+ # entry-point: home # optional; otherwise spec.entry is used
1648
+ # pinnable: true
1649
+
1486
1650
  entry-points: []
1487
1651
  capabilities: []
1488
1652
  permissions: []
@@ -1509,6 +1673,20 @@ metadata:
1509
1673
  version: 0.1.0
1510
1674
  author: ""
1511
1675
 
1676
+ # Marketplace presentation (optional):
1677
+ # marketplace:
1678
+ # logo: ./marketplace/logo.png
1679
+ # hero:
1680
+ # source: ./marketplace/hero.webp
1681
+ # alt: A concise description of the hero background
1682
+ # images:
1683
+ # - source: ./marketplace/screenshot.png
1684
+ # alt: A concise description of the screenshot
1685
+ # readme: ./README.md
1686
+ # changelog: ./CHANGELOG.md
1687
+ # assets:
1688
+ # - "marketplace/**/*.{png,jpg,jpeg,webp,svg}"
1689
+
1512
1690
  spec:
1513
1691
  runtimes: {}
1514
1692
  file-types: []
@@ -1530,6 +1708,20 @@ metadata:
1530
1708
  version: 0.1.0
1531
1709
  author: ""
1532
1710
 
1711
+ # Marketplace presentation (optional):
1712
+ # marketplace:
1713
+ # logo: ./marketplace/logo.png
1714
+ # hero:
1715
+ # source: ./marketplace/hero.webp
1716
+ # alt: A concise description of the hero background
1717
+ # images:
1718
+ # - source: ./marketplace/screenshot.png
1719
+ # alt: A concise description of the screenshot
1720
+ # readme: ./README.md
1721
+ # changelog: ./CHANGELOG.md
1722
+ # assets:
1723
+ # - "marketplace/**/*.{png,jpg,jpeg,webp,svg}"
1724
+
1533
1725
  packaging:
1534
1726
  publisher: "" # your publisher name
1535
1727
  files:
@@ -1735,10 +1927,11 @@ function registerKeygen(program2) {
1735
1927
 
1736
1928
  // src/commands/login.ts
1737
1929
  function registerLogin(program2) {
1738
- program2.command("login").description("Sign in to Verentis (browser device flow by default; --api-key/--token for non-interactive use)").option("--api-url <url>", "Verentis API gateway URL").option("--api-key <key>", "Verentis API key (vrt_...)").option("--token <token>", "Verentis identity token (exchanged per request for scoped access tokens)").option("--sealing-key <key>", "the platform's base64 X25519 sealing PUBLIC key (used by `pack --encrypt`)").action(async (options) => {
1930
+ program2.command("login").description("Sign in to Verentis (browser device flow by default; --api-key/--token for non-interactive use)").option("--api-url <url>", "Verentis API gateway URL").option("--api-key <key>", "Verentis API key (vrt_...)").option("--token <token>", "Verentis identity token (exchanged per request for scoped access tokens)").option("--sealing-key <key>", "the platform's base64 X25519 sealing PUBLIC key (used by `pack --encrypt`)").option("--insecure", "skip TLS certificate verification (for local dev with self-signed certs)").action(async (options) => {
1739
1931
  const config = await loadConfig();
1740
1932
  if (options.apiUrl) config.apiUrl = options.apiUrl;
1741
1933
  if (options.sealingKey) config.sealingPublicKey = options.sealingKey;
1934
+ if (options.insecure !== void 0) config.insecure = options.insecure;
1742
1935
  if (!config.apiUrl) throw new Error("An API URL is required. Pass --api-url <url>.");
1743
1936
  if (options.apiKey) {
1744
1937
  if (!options.apiKey.startsWith("vrt_")) throw new Error('API keys start with "vrt_".');
@@ -1758,14 +1951,15 @@ function registerLogin(program2) {
1758
1951
  return;
1759
1952
  }
1760
1953
  const apiUrl = config.apiUrl.replace(/\/+$/, "");
1761
- const authorization = await startDeviceAuthorization(apiUrl);
1954
+ const insecure = config.insecure;
1955
+ const authorization = await startDeviceAuthorization(apiUrl, insecure);
1762
1956
  console.log("To sign in, open this URL in a browser:");
1763
1957
  console.log(`
1764
1958
  ${authorization.verification_uri_complete ?? authorization.verification_uri}
1765
1959
  `);
1766
1960
  console.log(`and confirm the code: ${authorization.user_code}`);
1767
1961
  console.log("\nWaiting for approval\u2026");
1768
- const identity = await pollForIdentityToken(apiUrl, authorization, () => process.stderr.write("."));
1962
+ const identity = await pollForIdentityToken(apiUrl, authorization, insecure, () => process.stderr.write("."));
1769
1963
  process.stderr.write("\n");
1770
1964
  config.identityToken = identity.idToken;
1771
1965
  if (identity.refreshToken) config.refreshToken = identity.refreshToken;
@@ -1909,20 +2103,260 @@ function rawX25519Private(key) {
1909
2103
  function publicKeyFromRaw(raw) {
1910
2104
  return createPublicKey({ key: { kty: "OKP", crv: "X25519", x: raw.toString("base64url") }, format: "jwk" });
1911
2105
  }
2106
+ var MIB = 1024 * 1024;
2107
+ var MAX_LOGO_BYTES = MIB;
2108
+ var MAX_IMAGE_BYTES = 5 * MIB;
2109
+ var MAX_TOTAL_IMAGE_BYTES = 25 * MIB;
2110
+ var MAX_MARKDOWN_BYTES = MIB;
2111
+ async function buildCatalog(projectDir, marketplace2) {
2112
+ const root = await realpath(projectDir);
2113
+ const entries = /* @__PURE__ */ new Map();
2114
+ const imageDigests = /* @__PURE__ */ new Map();
2115
+ const index = { schemaVersion: 1 };
2116
+ const addImage = async (source, label, maxBytes) => {
2117
+ const file = await resolveDirectFile(root, source, label);
2118
+ if (file.bytes.length > maxBytes) {
2119
+ throw new Error(`${label} exceeds the ${formatMiB(maxBytes)} MiB limit (${file.bytes.length} bytes).`);
2120
+ }
2121
+ const image = validateImage(file.bytes, file.path, label);
2122
+ const reference = addContentAddressedEntry(file, image, "assets", entries);
2123
+ imageDigests.set(reference.sha256, reference.size);
2124
+ return reference;
2125
+ };
2126
+ const addMarkdown = async (source, label) => {
2127
+ const file = await resolveDirectFile(root, source, label);
2128
+ if (file.bytes.length > MAX_MARKDOWN_BYTES) {
2129
+ throw new Error(`${label} exceeds the 1 MiB limit (${file.bytes.length} bytes).`);
2130
+ }
2131
+ validateMarkdown(file.bytes, file.path, label);
2132
+ return addContentAddressedEntry(file, { extension: ".md", mediaType: "text/markdown" }, "docs", entries);
2133
+ };
2134
+ if (marketplace2.logo) index.logo = await addImage(marketplace2.logo, "marketplace.logo", MAX_LOGO_BYTES);
2135
+ if (marketplace2.hero) {
2136
+ const reference = await addImage(marketplace2.hero.source, "marketplace.hero.source", MAX_IMAGE_BYTES);
2137
+ index.hero = { ...reference, alt: marketplace2.hero.alt.trim() };
2138
+ }
2139
+ if (marketplace2.images?.length) {
2140
+ index.images = [];
2141
+ for (const [position, image] of marketplace2.images.entries()) {
2142
+ const reference = await addImage(image.source, `marketplace.images[${position}].source`, MAX_IMAGE_BYTES);
2143
+ index.images.push({
2144
+ ...reference,
2145
+ alt: image.alt.trim(),
2146
+ ...image.caption !== void 0 ? { caption: image.caption } : {}
2147
+ });
2148
+ }
2149
+ }
2150
+ if (marketplace2.readme) index.readme = await addMarkdown(marketplace2.readme, "marketplace.readme");
2151
+ if (marketplace2.changelog) index.changelog = await addMarkdown(marketplace2.changelog, "marketplace.changelog");
2152
+ if (marketplace2.assets?.length) {
2153
+ index.assets = [];
2154
+ const assetFiles = await resolveAssetGlobs(root, marketplace2.assets);
2155
+ for (const file of assetFiles) {
2156
+ if (file.bytes.length > MAX_IMAGE_BYTES) {
2157
+ throw new Error(`marketplace.assets matched '${file.source}', which exceeds the 5 MiB per-image limit (${file.bytes.length} bytes).`);
2158
+ }
2159
+ const image = validateImage(file.bytes, file.path, `marketplace.assets entry '${file.source}'`);
2160
+ const reference = addContentAddressedEntry(file, image, "assets", entries);
2161
+ imageDigests.set(reference.sha256, reference.size);
2162
+ index.assets.push(reference);
2163
+ }
2164
+ }
2165
+ const totalImageBytes = [...imageDigests.values()].reduce((sum, size) => sum + size, 0);
2166
+ if (totalImageBytes > MAX_TOTAL_IMAGE_BYTES) {
2167
+ throw new Error(`Marketplace image assets exceed the 25 MiB total limit (${totalImageBytes} bytes).`);
2168
+ }
2169
+ const indexJson = `${JSON.stringify(index, null, 2)}
2170
+ `;
2171
+ return {
2172
+ index,
2173
+ indexJson,
2174
+ entries: [...entries.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([path, bytes]) => ({ path, bytes }))
2175
+ };
2176
+ }
2177
+ async function resolveDirectFile(root, source, label) {
2178
+ const normalized = normalizeRelativeReference(source, label);
2179
+ return readContainedFile(root, normalized, label);
2180
+ }
2181
+ async function resolveAssetGlobs(root, patterns) {
2182
+ const matched = /* @__PURE__ */ new Set();
2183
+ for (const sourcePattern of patterns) {
2184
+ const pattern = normalizeRelativeReference(sourcePattern, "marketplace.assets glob");
2185
+ for await (const entry of glob(pattern, { cwd: root })) {
2186
+ const normalized = toEntryPath(entry);
2187
+ const info = await stat(join(root, normalized)).catch(() => null);
2188
+ if (info?.isFile()) matched.add(normalized);
2189
+ }
2190
+ }
2191
+ const files2 = [];
2192
+ for (const source of [...matched].sort()) {
2193
+ files2.push(await readContainedFile(root, source, `marketplace.assets entry '${source}'`));
2194
+ }
2195
+ return files2;
2196
+ }
2197
+ async function readContainedFile(root, source, label) {
2198
+ const candidate = resolve(root, source);
2199
+ const actual = await realpath(candidate).catch(() => {
2200
+ throw new Error(`${label} references missing file '${source}'.`);
2201
+ });
2202
+ const fromRoot = relative(root, actual);
2203
+ if (fromRoot === "" || fromRoot.startsWith(`..${sep}`) || fromRoot === ".." || isAbsolute(fromRoot)) {
2204
+ throw new Error(`${label} references '${source}', which escapes the manifest directory.`);
2205
+ }
2206
+ const info = await stat(actual);
2207
+ if (!info.isFile()) throw new Error(`${label} references '${source}', which is not a file.`);
2208
+ return { source: toEntryPath(source), path: actual, bytes: await readFile(actual) };
2209
+ }
2210
+ function normalizeRelativeReference(source, label) {
2211
+ const value = source.trim();
2212
+ if (value.length === 0 || value.includes("\0") || value.includes("\\") || isAbsolute(value) || /^[a-z]:/i.test(value) || value.split("/").includes("..")) {
2213
+ throw new Error(`${label} must be relative to the manifest directory and must not escape it (got '${source}').`);
2214
+ }
2215
+ const normalized = value.replace(/^(?:\.\/)+/, "");
2216
+ if (normalized.length === 0 || normalized === ".") throw new Error(`${label} must reference a file inside the manifest directory.`);
2217
+ return normalized;
2218
+ }
2219
+ function addContentAddressedEntry(file, type, directory, entries) {
2220
+ const sha256 = createHash("sha256").update(file.bytes).digest("hex");
2221
+ const path = `catalog/${directory}/${sha256}${type.extension}`;
2222
+ entries.set(path, file.bytes);
2223
+ return {
2224
+ source: file.source,
2225
+ path,
2226
+ mediaType: type.mediaType,
2227
+ size: file.bytes.length,
2228
+ sha256
2229
+ };
2230
+ }
2231
+ function validateImage(bytes, path, label) {
2232
+ const pngSignature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
2233
+ if (bytes.length >= 24 && bytes.subarray(0, 8).equals(pngSignature)) {
2234
+ if (bytes.readUInt32BE(8) !== 13 || bytes.toString("ascii", 12, 16) !== "IHDR") throw new Error(`${label} is not a valid PNG file.`);
2235
+ return { extension: ".png", mediaType: "image/png" };
2236
+ }
2237
+ if (bytes.length >= 4 && bytes[0] === 255 && bytes[1] === 216) {
2238
+ if (bytes[bytes.length - 2] !== 255 || bytes[bytes.length - 1] !== 217) throw new Error(`${label} is not a valid JPEG file.`);
2239
+ return { extension: ".jpg", mediaType: "image/jpeg" };
2240
+ }
2241
+ if (bytes.length >= 12 && bytes.toString("ascii", 0, 4) === "RIFF" && bytes.toString("ascii", 8, 12) === "WEBP") {
2242
+ const declaredLength = bytes.readUInt32LE(4) + 8;
2243
+ if (declaredLength !== bytes.length) throw new Error(`${label} has an invalid WebP length.`);
2244
+ return { extension: ".webp", mediaType: "image/webp" };
2245
+ }
2246
+ const extension = extname(path).toLowerCase();
2247
+ if (extension === ".svg" || looksLikeSvg(bytes)) {
2248
+ validateSvg(bytes, label);
2249
+ return { extension: ".svg", mediaType: "image/svg+xml" };
2250
+ }
2251
+ throw new Error(`${label} must be a valid PNG, JPEG, WebP or SVG image.`);
2252
+ }
2253
+ function looksLikeSvg(bytes) {
2254
+ const prefix = bytes.subarray(0, Math.min(bytes.length, 512)).toString("utf8").replace(/^\uFEFF/, "").trimStart();
2255
+ return prefix.startsWith("<svg") || prefix.startsWith("<?xml") || prefix.startsWith("<!--");
2256
+ }
2257
+ function validateSvg(bytes, label) {
2258
+ let text;
2259
+ try {
2260
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
2261
+ } catch {
2262
+ throw new Error(`${label} is not valid UTF-8 SVG content.`);
2263
+ }
2264
+ const trimmed = text.replace(/^\uFEFF/, "").trim();
2265
+ const root = /^(?:<\?xml[\s\S]*?\?>\s*)?(?:<!--[\s\S]*?-->\s*)*<svg\b/i;
2266
+ if (!root.test(trimmed) || !/<\/svg>\s*$/i.test(trimmed) && !/<svg\b[\s\S]*\/>\s*$/i.test(trimmed)) {
2267
+ throw new Error(`${label} is not a well-formed SVG document.`);
2268
+ }
2269
+ if (/&#(?:x[0-9a-f]+|[0-9]+);/i.test(trimmed)) {
2270
+ throw new Error(`${label} contains numeric XML character references, which are not allowed in SVG content.`);
2271
+ }
2272
+ 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)) {
2273
+ throw new Error(`${label} contains active SVG content, which is not allowed.`);
2274
+ }
2275
+ }
2276
+ function validateMarkdown(bytes, _path, label) {
2277
+ try {
2278
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
2279
+ if (text.includes("\0")) throw new Error("NUL");
2280
+ } catch {
2281
+ throw new Error(`${label} must contain valid UTF-8 Markdown text.`);
2282
+ }
2283
+ }
2284
+ function formatMiB(bytes) {
2285
+ return bytes / MIB;
2286
+ }
2287
+ function toEntryPath(path) {
2288
+ return path.split(sep).join("/");
2289
+ }
2290
+ function isPlainObject(value) {
2291
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2292
+ }
2293
+ function deepMerge(base, overlay) {
2294
+ if (!isPlainObject(base) || !isPlainObject(overlay)) {
2295
+ return overlay;
2296
+ }
2297
+ const result = { ...base };
2298
+ for (const [key, value] of Object.entries(overlay)) {
2299
+ result[key] = key in result ? deepMerge(result[key], value) : value;
2300
+ }
2301
+ return result;
2302
+ }
2303
+ function applyOverlay(base, overlay) {
2304
+ return deepMerge(base, overlay);
2305
+ }
2306
+ function resolveOverlayPath(projectDir, selector) {
2307
+ if (selector.overlay) {
2308
+ return isAbsolute(selector.overlay) ? selector.overlay : resolve(selector.overlay);
2309
+ }
2310
+ if (selector.env) {
2311
+ return join(projectDir, "environments", `${selector.env}.yaml`);
2312
+ }
2313
+ return void 0;
2314
+ }
2315
+ async function loadOverlay(path) {
2316
+ let text;
2317
+ try {
2318
+ text = await readFile(path, "utf8");
2319
+ } catch (error) {
2320
+ if (error?.code === "ENOENT") {
2321
+ throw new Error(`Overlay file not found: ${path}`);
2322
+ }
2323
+ throw error;
2324
+ }
2325
+ const parsed = parse(text);
2326
+ if (parsed === null || parsed === void 0) return {};
2327
+ if (!isPlainObject(parsed)) {
2328
+ throw new Error(`Overlay ${path} must be a YAML mapping (got ${Array.isArray(parsed) ? "a list" : typeof parsed}).`);
2329
+ }
2330
+ return parsed;
2331
+ }
2332
+
2333
+ // src/vpkg/pack.ts
1912
2334
  async function pack(options) {
1913
2335
  const manifestPath = resolve(options.manifestPath);
1914
2336
  const projectDir = dirname(manifestPath);
1915
- const manifest = await loadManifest(manifestPath);
2337
+ const base = await loadManifest(manifestPath);
2338
+ const overlayPath = resolveOverlayPath(projectDir, { env: options.env, overlay: options.overlay });
2339
+ let manifest = overlayPath ? applyOverlay(base, await loadOverlay(overlayPath)) : base;
2340
+ let overrideApplied = overlayPath !== void 0;
2341
+ if (options.version !== void 0) {
2342
+ manifest = { ...manifest, metadata: { ...manifest.metadata, version: options.version } };
2343
+ overrideApplied = true;
2344
+ }
1916
2345
  const kind = normalizeKind(manifest.kind);
2346
+ const launchKindError = validateLaunchManifestKind(kind, manifest.spec);
2347
+ if (launchKindError) throw new Error(`Invalid manifest: ${launchKindError}.`);
2348
+ const marketplaceErrors = validateMarketplaceSection(manifest.marketplace);
2349
+ if (marketplaceErrors.length > 0) throw new Error(`Invalid marketplace section: ${marketplaceErrors.join(" ")}`);
1917
2350
  const name = (options.name ?? manifest.packaging?.name ?? manifest.metadata.name).toLowerCase();
1918
2351
  const publisher = options.publisher.toLowerCase();
1919
2352
  validatePackageName(name, "package name");
1920
2353
  validatePackageName(publisher, "publisher name");
1921
2354
  const version = manifest.metadata.version;
1922
2355
  const payloadFiles = await resolvePayloadFiles(projectDir, manifestPath, manifest);
2356
+ const catalog = manifest.marketplace ? await buildCatalog(projectDir, manifest.marketplace) : void 0;
1923
2357
  const staging = await mkdtemp(join(tmpdir(), "vpkg-"));
1924
2358
  try {
1925
- const manifestText = await readFile(manifestPath, "utf8");
2359
+ const manifestText = overrideApplied ? stringify(manifest) : await readFile(manifestPath, "utf8");
1926
2360
  await writeFile(join(staging, "manifest.yaml"), manifestText);
1927
2361
  const encrypt = (options.encryptFor?.length ?? 0) > 0;
1928
2362
  const contentKey = encrypt ? generateContentKey() : null;
@@ -1931,13 +2365,24 @@ async function pack(options) {
1931
2365
  };
1932
2366
  for (const file of payloadFiles) {
1933
2367
  const sourcePath = join(projectDir, file);
1934
- const entryName = `files/${toEntryPath(file)}`;
2368
+ const entryName = `files/${toEntryPath2(file)}`;
1935
2369
  const targetPath = join(staging, "files", file);
1936
2370
  await mkdir(dirname(targetPath), { recursive: true });
1937
2371
  const bytes = contentKey ? encryptBytes(contentKey, await readFile(sourcePath)) : await readFile(sourcePath);
1938
2372
  await writeFile(targetPath, bytes);
1939
2373
  digests[entryName] = sha256Hex(bytes);
1940
2374
  }
2375
+ if (catalog) {
2376
+ for (const entry of catalog.entries) {
2377
+ const targetPath = join(staging, entry.path);
2378
+ await mkdir(dirname(targetPath), { recursive: true });
2379
+ await writeFile(targetPath, entry.bytes);
2380
+ digests[entry.path] = sha256Hex(entry.bytes);
2381
+ }
2382
+ await mkdir(join(staging, "catalog"), { recursive: true });
2383
+ await writeFile(join(staging, "catalog", "index.json"), catalog.indexJson);
2384
+ digests["catalog/index.json"] = sha256Hex(Buffer.from(catalog.indexJson, "utf8"));
2385
+ }
1941
2386
  await mkdir(join(staging, ".verentis", "signatures"), { recursive: true });
1942
2387
  if (contentKey) {
1943
2388
  const encryption = {
@@ -1973,16 +2418,19 @@ async function pack(options) {
1973
2418
  ".verentis/digests.json",
1974
2419
  ...contentKey ? [".verentis/encryption.json"] : [],
1975
2420
  ...signed ? [".verentis/signatures/developer.dsse.json"] : [],
1976
- ...payloadFiles.map((file) => `files/${toEntryPath(file)}`)
2421
+ ...payloadFiles.map((file) => `files/${toEntryPath2(file)}`),
2422
+ ...catalog ? ["catalog/index.json", ...catalog.entries.map((entry) => entry.path)] : []
1977
2423
  ];
1978
- await tar.create({ gzip: true, file: outFile, cwd: staging, portable: true }, entries);
2424
+ await tar.create({ gzip: true, file: outFile, cwd: staging, portable: true, mtime: /* @__PURE__ */ new Date(0) }, entries);
1979
2425
  return {
1980
2426
  outFile,
1981
2427
  name,
1982
2428
  publisher,
1983
2429
  version,
1984
2430
  kind,
1985
- files: payloadFiles.map(toEntryPath),
2431
+ files: payloadFiles.map(toEntryPath2),
2432
+ catalog: catalog?.index,
2433
+ catalogFiles: catalog ? ["index.json", ...catalog.entries.map((entry) => entry.path.replace(/^catalog\//, ""))] : [],
1986
2434
  treeDigest: sha256Hex(Buffer.from(digestsJson, "utf8")),
1987
2435
  signed,
1988
2436
  encrypted: encrypt
@@ -2009,7 +2457,7 @@ async function resolvePayloadFiles(projectDir, manifestPath, manifest) {
2009
2457
  }
2010
2458
  function defaultIncludes(manifest) {
2011
2459
  const sources = /* @__PURE__ */ new Set();
2012
- collectSchemaSources(manifest, sources);
2460
+ collectSchemaSources(manifest.spec, sources);
2013
2461
  collectInstallSources(manifest, sources);
2014
2462
  if (sources.size === 0) return [];
2015
2463
  return [...sources];
@@ -2040,7 +2488,7 @@ function collectSchemaSources(node, sources) {
2040
2488
  }
2041
2489
  }
2042
2490
  }
2043
- function toEntryPath(file) {
2491
+ function toEntryPath2(file) {
2044
2492
  return file.split(sep).join("/");
2045
2493
  }
2046
2494
  function sha256Hex(data) {
@@ -2061,21 +2509,18 @@ async function readVpkg(vpkgPath) {
2061
2509
  });
2062
2510
  const metadata = parse(packageYaml);
2063
2511
  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();
2512
+ const files2 = await listFiles(staging, "files/**/*");
2513
+ const catalogFiles = await listFiles(staging, "catalog/**/*");
2514
+ const contentFiles = [...files2, ...catalogFiles];
2072
2515
  const digestErrors = [];
2073
2516
  const entries = declared.entries ?? {};
2074
2517
  const manifestDigest = createHash("sha256").update(manifestYaml, "utf8").digest("hex");
2075
- if (entries["manifest.yaml"] && entries["manifest.yaml"].toLowerCase() !== manifestDigest) {
2518
+ if (!entries["manifest.yaml"]) {
2519
+ digestErrors.push("Entry 'manifest.yaml' is not covered by digests.json.");
2520
+ } else if (entries["manifest.yaml"].toLowerCase() !== manifestDigest) {
2076
2521
  digestErrors.push("Digest mismatch for manifest.yaml.");
2077
2522
  }
2078
- for (const file of files2) {
2523
+ for (const file of contentFiles) {
2079
2524
  const declaredDigest = entries[file];
2080
2525
  if (!declaredDigest) {
2081
2526
  digestErrors.push(`Entry '${file}' is not covered by digests.json.`);
@@ -2085,10 +2530,21 @@ async function readVpkg(vpkgPath) {
2085
2530
  if (actual !== declaredDigest.toLowerCase()) digestErrors.push(`Digest mismatch for '${file}'.`);
2086
2531
  }
2087
2532
  for (const declaredPath of Object.keys(entries)) {
2088
- if (declaredPath !== "manifest.yaml" && !files2.includes(declaredPath)) {
2533
+ if (declaredPath !== "manifest.yaml" && !contentFiles.includes(declaredPath)) {
2089
2534
  digestErrors.push(`Digest declared for missing entry '${declaredPath}'.`);
2090
2535
  }
2091
2536
  }
2537
+ let catalog;
2538
+ if (catalogFiles.includes("catalog/index.json")) {
2539
+ try {
2540
+ catalog = JSON.parse(await readFile(join(staging, "catalog", "index.json"), "utf8"));
2541
+ if (catalog.schemaVersion !== 1) digestErrors.push(`Unsupported catalog schema version '${String(catalog.schemaVersion)}'.`);
2542
+ } catch {
2543
+ digestErrors.push("Invalid catalog/index.json.");
2544
+ }
2545
+ } else if (catalogFiles.length > 0) {
2546
+ digestErrors.push("Catalog entries are present but catalog/index.json is missing.");
2547
+ }
2092
2548
  const developerSignature = await readJsonIfExists(join(staging, ".verentis", "signatures", "developer.dsse.json"));
2093
2549
  const marketplaceSignature = await readJsonIfExists(join(staging, ".verentis", "signatures", "marketplace.dsse.json"));
2094
2550
  const encryption = await readJsonIfExists(join(staging, ".verentis", "encryption.json"));
@@ -2101,12 +2557,23 @@ async function readVpkg(vpkgPath) {
2101
2557
  marketplaceSignature: marketplaceSignature ?? void 0,
2102
2558
  encryption: encryption ?? void 0,
2103
2559
  files: files2.map((f) => f.replace(/^files\//, "")),
2560
+ catalog,
2561
+ catalogFiles: catalogFiles.map((f) => f.replace(/^catalog\//, "")),
2104
2562
  digestErrors
2105
2563
  };
2106
2564
  } finally {
2107
2565
  await rm(staging, { recursive: true, force: true });
2108
2566
  }
2109
2567
  }
2568
+ async function listFiles(root, pattern) {
2569
+ const files2 = [];
2570
+ for await (const entry of glob(pattern, { cwd: root })) {
2571
+ const abs = join(root, entry);
2572
+ const info = await readFile(abs).catch(() => null);
2573
+ if (info !== null) files2.push(entry.split(sep).join("/"));
2574
+ }
2575
+ return files2.sort();
2576
+ }
2110
2577
  async function readJsonIfExists(path) {
2111
2578
  try {
2112
2579
  return JSON.parse(await readFile(path, "utf8"));
@@ -2117,7 +2584,7 @@ async function readJsonIfExists(path) {
2117
2584
 
2118
2585
  // src/commands/pack.ts
2119
2586
  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) => {
2587
+ 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
2588
  const projectDir = resolve(dir ?? ".");
2122
2589
  const manifestPath = options.manifest ? resolve(options.manifest) : await findManifest(projectDir);
2123
2590
  const manifest = await loadManifest(manifestPath);
@@ -2152,6 +2619,9 @@ function registerPack(program2) {
2152
2619
  manifestPath,
2153
2620
  publisher,
2154
2621
  name: options.name,
2622
+ env: options.env,
2623
+ overlay: options.overlay,
2624
+ version: options.setVersion,
2155
2625
  outDir: options.out,
2156
2626
  signingKey,
2157
2627
  encryptFor
@@ -2159,6 +2629,7 @@ function registerPack(program2) {
2159
2629
  console.log(`Packed ${result.publisher}/${result.name}@${result.version} (${result.kind})`);
2160
2630
  console.log(` ${result.outFile}`);
2161
2631
  console.log(` ${result.files.length} payload file(s), tree digest ${result.treeDigest.slice(0, 16)}\u2026, ${result.signed ? "signed" : "UNSIGNED"}${result.encrypted ? ", ENCRYPTED" : ""}`);
2632
+ if (result.catalog) console.log(` ${result.catalogFiles.length} plaintext catalog file(s)`);
2162
2633
  });
2163
2634
  program2.command("seal-keygen").description("Generate an X25519 sealing keypair (for platform escrow or custom recipients)").action(() => {
2164
2635
  const pair = generateSealingKeyPair();
@@ -2169,7 +2640,11 @@ function registerPack(program2) {
2169
2640
  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
2641
  const contents = await readVpkg(resolve(vpkgPath));
2171
2642
  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`);
2643
+ console.log(` payload files: ${contents.files.length}, catalog files: ${contents.catalogFiles.length}, tree digest ${contents.treeDigest.slice(0, 16)}\u2026`);
2644
+ if (contents.catalog) {
2645
+ const docs = Number(!!contents.catalog.readme) + Number(!!contents.catalog.changelog);
2646
+ 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" : ""}`);
2647
+ }
2173
2648
  if (contents.encryption) {
2174
2649
  console.log(` \u{1F512} encrypted (${contents.encryption.algorithm}); recipients: ${contents.encryption.recipients.map((r) => r.id).join(", ")}`);
2175
2650
  }
@@ -2310,19 +2785,62 @@ function registerPackage(program2) {
2310
2785
  output(pageItems(page), { packageName: "Package", version: "Version", status: "Status", installPath: "Path", id: "Id" }, options, page);
2311
2786
  });
2312
2787
  }
2788
+ var MAX_LOGO_BYTES2 = 1024 * 1024;
2789
+ var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
2790
+ async function readPublisherLogo(path) {
2791
+ const absolutePath = resolve(path);
2792
+ const info = await stat(absolutePath).catch(() => {
2793
+ throw new Error(`Publisher logo '${absolutePath}' does not exist.`);
2794
+ });
2795
+ if (!info.isFile()) throw new Error(`Publisher logo '${absolutePath}' is not a file.`);
2796
+ if (info.size > MAX_LOGO_BYTES2) {
2797
+ throw new Error(`Publisher logo exceeds the 1 MiB limit (${info.size} bytes).`);
2798
+ }
2799
+ const bytes = await readFile(absolutePath);
2800
+ return { path: absolutePath, bytes, contentType: detectPublisherLogoContentType(bytes) };
2801
+ }
2802
+ function detectPublisherLogoContentType(bytes) {
2803
+ if (bytes.length >= 24 && bytes.subarray(0, 8).equals(PNG_SIGNATURE) && bytes.readUInt32BE(8) === 13 && bytes.toString("ascii", 12, 16) === "IHDR") {
2804
+ return "image/png";
2805
+ }
2806
+ if (bytes.length >= 4 && bytes[0] === 255 && bytes[1] === 216 && bytes[bytes.length - 2] === 255 && bytes[bytes.length - 1] === 217) {
2807
+ return "image/jpeg";
2808
+ }
2809
+ if (bytes.length >= 12 && bytes.toString("ascii", 0, 4) === "RIFF" && bytes.toString("ascii", 8, 12) === "WEBP" && bytes.readUInt32LE(4) + 8 === bytes.length) {
2810
+ return "image/webp";
2811
+ }
2812
+ let text;
2813
+ try {
2814
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes).replace(/^\uFEFF/, "").trim();
2815
+ } catch {
2816
+ throw unsupportedLogoError();
2817
+ }
2818
+ const root = /^(?:<\?xml[\s\S]*?\?>\s*)?(?:<!--[\s\S]*?-->\s*)*<svg\b/i;
2819
+ const closed = /<\/svg>\s*$/i.test(text) || /<svg\b[\s\S]*\/>\s*$/i.test(text);
2820
+ if (!root.test(text) || !closed) throw unsupportedLogoError();
2821
+ if (/&#(?:x[0-9a-f]+|[0-9]+);/i.test(text)) {
2822
+ throw new Error("Publisher logo SVG contains numeric XML character references, which are not allowed.");
2823
+ }
2824
+ 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)) {
2825
+ throw new Error("Publisher logo SVG contains active content, which is not allowed.");
2826
+ }
2827
+ return "image/svg+xml";
2828
+ }
2829
+ function unsupportedLogoError() {
2830
+ return new Error("Publisher logo must be a valid PNG, JPEG, WebP or SVG image.");
2831
+ }
2313
2832
 
2314
2833
  // src/commands/publisher.ts
2315
2834
  function registerPublisher(program2) {
2316
2835
  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) => {
2836
+ 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
2837
  const api = await createApiClient();
2319
2838
  const existing = await getMyPublisher(api);
2320
2839
  if (existing) {
2321
2840
  console.log(`This account already has publisher '${existing.name}'.`);
2322
2841
  return;
2323
2842
  }
2324
- const id = await api.request("POST", "/v1/publishers", {
2325
- scopes: [SCOPES.publisherCreate],
2843
+ const id = await call(api, marketplace.createPublisher, {
2326
2844
  body: {
2327
2845
  name: options.name,
2328
2846
  displayName: options.displayName,
@@ -2332,8 +2850,51 @@ function registerPublisher(program2) {
2332
2850
  }
2333
2851
  });
2334
2852
  console.log(`Publisher '${options.name}' created (${id}).`);
2853
+ if (options.logo) {
2854
+ await uploadPublisherLogo(api, id, options.logo);
2855
+ console.log(`Publisher logo uploaded from ${options.logo}.`);
2856
+ }
2335
2857
  console.log("Next: generate and register a signing key with `verentis keygen`.");
2336
2858
  });
2859
+ publisherCmd.command("update").description("Update your publisher profile").option("--display-name <displayName>").option("--description <description>").option("--website <url>").option("--email <email>").action(async (options) => {
2860
+ if (Object.values(options).every((value) => value === void 0)) {
2861
+ throw new Error("No publisher fields supplied. Pass --display-name, --description, --website or --email.");
2862
+ }
2863
+ const api = await createApiClient();
2864
+ const current = await requireMyPublisher(api);
2865
+ await call(api, marketplace.updatePublisher, {
2866
+ params: { id: current.id },
2867
+ body: {
2868
+ id: current.id,
2869
+ displayName: options.displayName ?? current.displayName,
2870
+ description: options.description ?? current.description ?? null,
2871
+ websiteUrl: options.website ?? current.websiteUrl ?? null,
2872
+ contactEmail: options.email ?? current.contactEmail ?? null
2873
+ }
2874
+ });
2875
+ console.log(`Publisher '${current.name}' updated.`);
2876
+ });
2877
+ const logoCmd = publisherCmd.command("logo").description("Manage your publisher logo");
2878
+ logoCmd.command("set <file>").description("Upload a PNG, JPEG, WebP or SVG publisher logo (maximum 1 MiB)").action(async (file) => {
2879
+ const api = await createApiClient();
2880
+ const publisher = await requireMyPublisher(api);
2881
+ await uploadPublisherLogo(api, publisher.id, file);
2882
+ console.log(`Publisher logo uploaded from ${file}.`);
2883
+ });
2884
+ logoCmd.command("remove").description("Remove your publisher logo").action(async () => {
2885
+ const api = await createApiClient();
2886
+ const publisher = await requireMyPublisher(api);
2887
+ await call(api, marketplace.removePublisherLogo, { params: { id: publisher.id } });
2888
+ console.log("Publisher logo removed.");
2889
+ });
2890
+ }
2891
+ async function uploadPublisherLogo(api, publisherId, path) {
2892
+ const logo = await readPublisherLogo(path);
2893
+ await callRaw(api, marketplace.setPublisherLogo, {
2894
+ params: { id: publisherId },
2895
+ rawBody: new Blob([new Uint8Array(logo.bytes)], { type: logo.contentType }),
2896
+ contentType: logo.contentType
2897
+ });
2337
2898
  }
2338
2899
  function registerRegistry(program2) {
2339
2900
  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 +3053,11 @@ function getCliVersion() {
2492
3053
  }
2493
3054
  function readVersion() {
2494
3055
  const candidates = ["../package.json", "../../package.json"];
2495
- for (const relative2 of candidates) {
3056
+ for (const relative3 of candidates) {
2496
3057
  try {
2497
- const raw = readFileSync(fileURLToPath(new URL(relative2, import.meta.url)), "utf8");
3058
+ const raw = readFileSync(fileURLToPath(new URL(relative3, import.meta.url)), "utf8");
2498
3059
  const pkg = JSON.parse(raw);
2499
- if (pkg.version && (relative2 === "../package.json" || pkg.name === PACKAGE_NAME)) return pkg.version;
3060
+ if (pkg.version && (relative3 === "../package.json" || pkg.name === PACKAGE_NAME)) return pkg.version;
2500
3061
  } catch {
2501
3062
  }
2502
3063
  }
@@ -2534,8 +3095,8 @@ function isNewerVersion(candidate, base) {
2534
3095
  return compareVersions(candidate, base) > 0;
2535
3096
  }
2536
3097
  function compareVersions(a, b) {
2537
- const pa = parse4(a);
2538
- const pb = parse4(b);
3098
+ const pa = parse5(a);
3099
+ const pb = parse5(b);
2539
3100
  for (let i = 0; i < 3; i++) {
2540
3101
  if (pa.main[i] !== pb.main[i]) return pa.main[i] < pb.main[i] ? -1 : 1;
2541
3102
  }
@@ -2562,7 +3123,7 @@ function compareVersions(a, b) {
2562
3123
  }
2563
3124
  return 0;
2564
3125
  }
2565
- function parse4(version) {
3126
+ function parse5(version) {
2566
3127
  const cleaned = version.trim().replace(/^v/i, "").split("+", 1)[0];
2567
3128
  const dash = cleaned.indexOf("-");
2568
3129
  const core = dash === -1 ? cleaned : cleaned.slice(0, dash);
@@ -2839,13 +3400,22 @@ function registerUser(program2) {
2839
3400
  });
2840
3401
  }
2841
3402
  function registerValidate(program2) {
2842
- program2.command("validate [pathOrDir]").description("Validate an engine/app/bundle manifest (structure + registration-time checks)").action(async (pathOrDir) => {
3403
+ 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
3404
  const target = resolve(pathOrDir ?? ".");
2844
3405
  const info = await stat(target).catch(() => null);
2845
3406
  const manifestPath = info?.isDirectory() ? await findManifest(target) : target;
2846
3407
  if (!info) throw new Error(`${target} does not exist.`);
2847
- const manifest = await readManifestDocument(manifestPath);
3408
+ const base = await readManifestDocument(manifestPath);
3409
+ const overlayPath = resolveOverlayPath(dirname(manifestPath), { env: options.env, overlay: options.overlay });
3410
+ const manifest = overlayPath ? applyOverlay(base, await loadOverlay(overlayPath)) : base;
2848
3411
  const issues = validateManifest(manifest);
3412
+ if (manifest.marketplace && !issues.some((issue) => issue.level === "error" && issue.message.startsWith("marketplace"))) {
3413
+ try {
3414
+ await buildCatalog(dirname(manifestPath), manifest.marketplace);
3415
+ } catch (error) {
3416
+ issues.push({ level: "error", message: error instanceof Error ? error.message : String(error) });
3417
+ }
3418
+ }
2849
3419
  const errors = issues.filter((issue) => issue.level === "error");
2850
3420
  const warnings = issues.filter((issue) => issue.level === "warning");
2851
3421
  console.log(`${manifestPath} \u2014 kind ${manifest.kind ?? "(unknown)"}, ${manifest.metadata?.name ?? "?"}@${manifest.metadata?.version ?? "?"}`);
@@ -2944,7 +3514,7 @@ function registerWorkspace(program2) {
2944
3514
  }
2945
3515
 
2946
3516
  // 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());
3517
+ var program = new Command().enablePositionalOptions().name("verentis").description("Verentis CLI \u2014 admin, marketplace publishing, and the engine/app developer loop").version(getCliVersion());
2948
3518
  registerLogin(program);
2949
3519
  registerUse(program);
2950
3520
  registerAccount(program);