@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/README.md +70 -3
- package/dist/index.js +661 -91
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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,
|
|
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((
|
|
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((
|
|
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
|
|
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
|
|
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/${
|
|
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/${
|
|
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(
|
|
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
|
|
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
|
-
|
|
2066
|
-
|
|
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"]
|
|
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
|
|
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" && !
|
|
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
|
|
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
|
|
3056
|
+
for (const relative3 of candidates) {
|
|
2496
3057
|
try {
|
|
2497
|
-
const raw = readFileSync(fileURLToPath(new URL(
|
|
3058
|
+
const raw = readFileSync(fileURLToPath(new URL(relative3, import.meta.url)), "utf8");
|
|
2498
3059
|
const pkg = JSON.parse(raw);
|
|
2499
|
-
if (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 =
|
|
2538
|
-
const pb =
|
|
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
|
|
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
|
|
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);
|