@calimero-network/registry-cli 1.14.1 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import axios from 'axios';
8
8
  import fs9, { existsSync, writeFileSync } from 'fs';
9
9
  import path7 from 'path';
10
10
  import os from 'os';
11
- import crypto from 'crypto';
11
+ import crypto2 from 'crypto';
12
12
  import fastify from 'fastify';
13
13
  import cors from '@fastify/cors';
14
14
  import * as tar from 'tar';
@@ -817,7 +817,7 @@ var LocalArtifactServer = class {
817
817
  }
818
818
  const stats = fs9.statSync(filePath);
819
819
  const fileBuffer = fs9.readFileSync(filePath);
820
- const sha256 = crypto.createHash("sha256").update(fileBuffer).digest("hex");
820
+ const sha256 = crypto2.createHash("sha256").update(fileBuffer).digest("hex");
821
821
  return {
822
822
  size: stats.size,
823
823
  sha256
@@ -826,7 +826,7 @@ var LocalArtifactServer = class {
826
826
  // Calculate file hash
827
827
  async calculateFileHash(filePath) {
828
828
  const fileBuffer = fs9.readFileSync(filePath);
829
- return crypto.createHash("sha256").update(fileBuffer).digest("hex");
829
+ return crypto2.createHash("sha256").update(fileBuffer).digest("hex");
830
830
  }
831
831
  // Get artifact URL for local serving
832
832
  getArtifactUrl(appId, version2, filename) {
@@ -1888,6 +1888,111 @@ var RemoteConfig = class {
1888
1888
  }
1889
1889
  };
1890
1890
 
1891
+ // src/lib/services.ts
1892
+ var SERVICE_NAME_RE = /^[a-z0-9][a-z0-9_-]*$/;
1893
+ function validateServiceName(name) {
1894
+ if (typeof name !== "string" || !SERVICE_NAME_RE.test(name)) {
1895
+ throw new Error(
1896
+ `Invalid service name "${name}". Use lowercase letters, digits, "-" or "_" (must start alphanumeric).`
1897
+ );
1898
+ }
1899
+ if (name.length > 64) {
1900
+ throw new Error(
1901
+ `Invalid service name "${name}": must be at most 64 characters.`
1902
+ );
1903
+ }
1904
+ if (name === "app") {
1905
+ throw new Error(
1906
+ 'Service name "app" is reserved for the main application. Choose another name.'
1907
+ );
1908
+ }
1909
+ }
1910
+ function parseServiceSpec(spec) {
1911
+ const eq = spec.indexOf("=");
1912
+ if (eq === -1) {
1913
+ throw new Error(
1914
+ `Invalid --service "${spec}". Expected "name=path.wasm" or "name=path.wasm,abi=path.json".`
1915
+ );
1916
+ }
1917
+ const name = spec.slice(0, eq).trim();
1918
+ let rest = spec.slice(eq + 1).trim();
1919
+ let abi;
1920
+ const abiMatch = rest.match(/,\s*abi\s*=\s*([^,]*)$/);
1921
+ if (abiMatch) {
1922
+ abi = abiMatch[1].trim();
1923
+ rest = rest.slice(0, abiMatch.index).trim();
1924
+ if (!abi) {
1925
+ throw new Error(`Invalid --service "${spec}": empty abi path.`);
1926
+ }
1927
+ }
1928
+ const wasm = rest;
1929
+ if (!wasm) {
1930
+ throw new Error(`Invalid --service "${spec}": missing WASM path.`);
1931
+ }
1932
+ if (wasm.includes(",")) {
1933
+ throw new Error(
1934
+ `Invalid --service "${spec}": unexpected comma in WASM path. Use "name=path.wasm,abi=path.json"; commas in paths are not supported.`
1935
+ );
1936
+ }
1937
+ validateServiceName(name);
1938
+ return abi ? { name, wasm, abi } : { name, wasm };
1939
+ }
1940
+ function serviceWasmPath(name) {
1941
+ return `services/${name}.wasm`;
1942
+ }
1943
+ function serviceAbiPath(name) {
1944
+ return `services/${name}.abi.json`;
1945
+ }
1946
+ function isUnderServicesDir(p) {
1947
+ return typeof p === "string" && /^services[\\/]/.test(p);
1948
+ }
1949
+ function assertUniqueServiceNames(services) {
1950
+ const seen = /* @__PURE__ */ new Set();
1951
+ for (const s of services) {
1952
+ if (seen.has(s.name)) {
1953
+ throw new Error(`Duplicate service name "${s.name}".`);
1954
+ }
1955
+ seen.add(s.name);
1956
+ }
1957
+ }
1958
+ function assertSafeBundlePath(p) {
1959
+ const segments = p.split(/[\\/]/);
1960
+ if (!p || p.startsWith("/") || /^[a-zA-Z]:/.test(p) || segments.includes("..") || segments.includes(".") || segments.includes("")) {
1961
+ throw new Error(
1962
+ `Unsafe bundle path "${p}": must be a non-empty relative path with no "." or ".." segments.`
1963
+ );
1964
+ }
1965
+ }
1966
+ function collectBundleFiles(manifest) {
1967
+ const files = ["manifest.json"];
1968
+ if (manifest.wasm?.path) files.push(manifest.wasm.path);
1969
+ if (manifest.abi?.path) files.push(manifest.abi.path);
1970
+ for (const svc of manifest.services ?? []) {
1971
+ const label = svc?.name ?? "<unnamed>";
1972
+ if (!svc.wasm?.path) {
1973
+ throw new Error(
1974
+ `Service "${label}" has no wasm.path; cannot pack the bundle.`
1975
+ );
1976
+ }
1977
+ if (!isUnderServicesDir(svc.wasm.path)) {
1978
+ throw new Error(
1979
+ `Service "${label}" wasm.path "${svc.wasm.path}" must be under services/.`
1980
+ );
1981
+ }
1982
+ files.push(svc.wasm.path);
1983
+ if (svc.abi?.path) {
1984
+ if (!isUnderServicesDir(svc.abi.path)) {
1985
+ throw new Error(
1986
+ `Service "${label}" abi.path "${svc.abi.path}" must be under services/.`
1987
+ );
1988
+ }
1989
+ files.push(svc.abi.path);
1990
+ }
1991
+ }
1992
+ files.forEach(assertSafeBundlePath);
1993
+ return [...new Set(files)];
1994
+ }
1995
+
1891
1996
  // src/commands/bundle.ts
1892
1997
  var bundleCommand = new Command("bundle").description("Manage application bundles (V2)").addCommand(createCreateCommand()).addCommand(createPushCommand()).addCommand(createGetCommand()).addCommand(createEditCommand());
1893
1998
  function createCreateCommand() {
@@ -1906,7 +2011,13 @@ function createCreateCommand() {
1906
2011
  (value, prev) => {
1907
2012
  return [...prev || [], value];
1908
2013
  }
1909
- ).option("--abi <path>", "Path to ABI JSON file to include in bundle").addHelpText(
2014
+ ).option("--abi <path>", "Path to ABI JSON file to include in bundle").option(
2015
+ "--service <spec>",
2016
+ 'Add a service WASM alongside the main app: "name=path.wasm" or "name=path.wasm,abi=path.json" (repeatable)',
2017
+ (value, prev) => {
2018
+ return [...prev || [], parseServiceSpec(value)];
2019
+ }
2020
+ ).addHelpText(
1910
2021
  "after",
1911
2022
  `
1912
2023
  Examples:
@@ -1916,6 +2027,7 @@ Examples:
1916
2027
  $ calimero-registry bundle create app.wasm com.calimero.myapp 1.0.0 --name "My App" --frontend https://app.example.com
1917
2028
  $ calimero-registry bundle create app.wasm com.calimero.myapp 1.0.0 --abi abi.json
1918
2029
  $ calimero-registry bundle create app.wasm --manifest manifest.json --abi res/abi.json
2030
+ $ calimero-registry bundle create tictactoe.wasm com.calimero.ttt 1.0.0 --service lobby=lobby.wasm,abi=lobby-abi.json
1919
2031
 
1920
2032
  Manifest File Format (JSON):
1921
2033
  {
@@ -1930,7 +2042,10 @@ Manifest File Format (JSON):
1930
2042
  "docs": "https://docs.example.com",
1931
2043
  "exports": ["interface1", "interface2"],
1932
2044
  "uses": ["interface3"],
1933
- "abi": "res/abi.json"
2045
+ "abi": "res/abi.json",
2046
+ "services": [
2047
+ { "name": "lobby", "wasm": "res/lobby.wasm", "abi": "res/lobby-abi.json" }
2048
+ ]
1934
2049
  }
1935
2050
 
1936
2051
  Note:
@@ -1940,6 +2055,8 @@ Note:
1940
2055
  - The -o/--output option overrides manifest.output if both are provided
1941
2056
  - ABI can be provided via --abi flag or manifest.abi field (file path string)
1942
2057
  - ABI file will be included in the bundle and referenced in the manifest
2058
+ - The main app is always packed as app.wasm; services are packed under services/<name>.wasm
2059
+ - Services merge from --service flags and manifest.services; names must be unique
1943
2060
  `
1944
2061
  ).action(async (wasmFile, pkg, version2, options) => {
1945
2062
  try {
@@ -1988,6 +2105,10 @@ Note:
1988
2105
  console.error('\u274C Invalid manifest: "uses" must be an array');
1989
2106
  process.exit(1);
1990
2107
  }
2108
+ if (manifestConfig.services !== void 0 && !Array.isArray(manifestConfig.services)) {
2109
+ console.error('\u274C Invalid manifest: "services" must be an array');
2110
+ process.exit(1);
2111
+ }
1991
2112
  const finalExports = options.export && options.export.length > 0 ? options.export : manifestExports || [];
1992
2113
  const finalUses = options.use && options.use.length > 0 ? options.use : manifestUses || [];
1993
2114
  if (!wasmFile) {
@@ -2026,7 +2147,7 @@ Note:
2026
2147
  console.log(`\u{1F4E6} Creating MPK bundle from: ${path7.basename(wasmPath)}`);
2027
2148
  const wasmContent = fs9.readFileSync(wasmPath);
2028
2149
  const wasmSize = wasmContent.length;
2029
- const wasmHash = crypto.createHash("sha256").update(wasmContent).digest("hex");
2150
+ const wasmHash = crypto2.createHash("sha256").update(wasmContent).digest("hex");
2030
2151
  let abiArtifact = void 0;
2031
2152
  let abiContent = null;
2032
2153
  const abiPathFromOption = options.abi;
@@ -2045,7 +2166,7 @@ Note:
2045
2166
  try {
2046
2167
  abiContent = fs9.readFileSync(abiResolvedPath);
2047
2168
  JSON.parse(abiContent.toString("utf8"));
2048
- const abiHash = crypto.createHash("sha256").update(abiContent).digest("hex");
2169
+ const abiHash = crypto2.createHash("sha256").update(abiContent).digest("hex");
2049
2170
  abiArtifact = {
2050
2171
  path: "abi.json",
2051
2172
  hash: abiHash,
@@ -2072,6 +2193,105 @@ Note:
2072
2193
  );
2073
2194
  process.exit(1);
2074
2195
  }
2196
+ const cliServices = options.service || [];
2197
+ const manifestServices = Array.isArray(
2198
+ manifestConfig.services
2199
+ ) ? manifestConfig.services : [];
2200
+ const manifestDir = options.manifest ? path7.dirname(path7.resolve(options.manifest)) : process.cwd();
2201
+ const allServiceSources = [
2202
+ ...cliServices.map((src) => ({ src, baseDir: process.cwd() })),
2203
+ ...manifestServices.map((src) => ({ src, baseDir: manifestDir }))
2204
+ ];
2205
+ const serviceArtifacts = [];
2206
+ const serviceFilesToWrite = [];
2207
+ for (const { src } of allServiceSources) {
2208
+ try {
2209
+ validateServiceName(src.name);
2210
+ } catch (e) {
2211
+ console.error(`\u274C ${e instanceof Error ? e.message : String(e)}`);
2212
+ process.exit(1);
2213
+ }
2214
+ }
2215
+ try {
2216
+ assertUniqueServiceNames(allServiceSources.map((s) => s.src));
2217
+ } catch (e) {
2218
+ console.error(`\u274C ${e instanceof Error ? e.message : String(e)}`);
2219
+ process.exit(1);
2220
+ }
2221
+ for (const { src, baseDir } of allServiceSources) {
2222
+ if (typeof src.wasm !== "string" || src.wasm.length === 0) {
2223
+ console.error(
2224
+ `\u274C Service "${src.name}" wasm must be a file path string.`
2225
+ );
2226
+ process.exit(1);
2227
+ }
2228
+ if (src.abi !== void 0 && (typeof src.abi !== "string" || src.abi.length === 0)) {
2229
+ console.error(
2230
+ `\u274C Service "${src.name}" abi must be a file path string.`
2231
+ );
2232
+ process.exit(1);
2233
+ }
2234
+ const svcWasmPath = path7.resolve(baseDir, src.wasm);
2235
+ if (!fs9.existsSync(svcWasmPath)) {
2236
+ console.error(
2237
+ `\u274C Service "${src.name}" WASM not found: ${src.wasm}`
2238
+ );
2239
+ console.error(` Resolved path: ${svcWasmPath}`);
2240
+ process.exit(1);
2241
+ }
2242
+ const svcWasmContent = fs9.readFileSync(svcWasmPath);
2243
+ const svcWasmHash = crypto2.createHash("sha256").update(svcWasmContent).digest("hex");
2244
+ const wasmArchivePath = serviceWasmPath(src.name);
2245
+ const entry = {
2246
+ name: src.name,
2247
+ wasm: {
2248
+ path: wasmArchivePath,
2249
+ hash: svcWasmHash,
2250
+ size: svcWasmContent.length
2251
+ }
2252
+ };
2253
+ serviceFilesToWrite.push({
2254
+ relPath: wasmArchivePath,
2255
+ content: svcWasmContent
2256
+ });
2257
+ if (src.abi) {
2258
+ const svcAbiPath = path7.resolve(baseDir, src.abi);
2259
+ if (!fs9.existsSync(svcAbiPath)) {
2260
+ console.error(
2261
+ `\u274C Service "${src.name}" ABI not found: ${src.abi}`
2262
+ );
2263
+ console.error(` Resolved path: ${svcAbiPath}`);
2264
+ process.exit(1);
2265
+ }
2266
+ const svcAbiContent = fs9.readFileSync(svcAbiPath);
2267
+ try {
2268
+ JSON.parse(svcAbiContent.toString("utf8"));
2269
+ } catch (error) {
2270
+ console.error(
2271
+ `\u274C Service "${src.name}" ABI is not valid JSON: ${src.abi}`
2272
+ );
2273
+ console.error(
2274
+ error instanceof Error ? error.message : "Invalid JSON"
2275
+ );
2276
+ process.exit(1);
2277
+ }
2278
+ const svcAbiHash = crypto2.createHash("sha256").update(svcAbiContent).digest("hex");
2279
+ const abiArchivePath = serviceAbiPath(src.name);
2280
+ entry.abi = {
2281
+ path: abiArchivePath,
2282
+ hash: svcAbiHash,
2283
+ size: svcAbiContent.length
2284
+ };
2285
+ serviceFilesToWrite.push({
2286
+ relPath: abiArchivePath,
2287
+ content: svcAbiContent
2288
+ });
2289
+ }
2290
+ serviceArtifacts.push(entry);
2291
+ console.log(
2292
+ ` Including service: ${src.name} (${path7.basename(svcWasmPath)})`
2293
+ );
2294
+ }
2075
2295
  const metadata = {
2076
2296
  name: finalName || finalPackage,
2077
2297
  description: finalDescription || "",
@@ -2097,6 +2317,7 @@ Note:
2097
2317
  size: wasmSize
2098
2318
  },
2099
2319
  abi: abiArtifact || void 0,
2320
+ services: serviceArtifacts.length > 0 ? serviceArtifacts : void 0,
2100
2321
  migrations: [],
2101
2322
  links: Object.keys(links).length > 0 ? links : void 0,
2102
2323
  signature: void 0
@@ -2113,6 +2334,19 @@ Note:
2113
2334
  if (abiContent) {
2114
2335
  fs9.writeFileSync(path7.join(outputDir, "abi.json"), abiContent);
2115
2336
  }
2337
+ const servicesDir = path7.join(outputDir, "services");
2338
+ for (const { relPath, content } of serviceFilesToWrite) {
2339
+ const dest = path7.join(outputDir, relPath);
2340
+ const rel = path7.relative(servicesDir, dest);
2341
+ if (rel.startsWith("..") || path7.isAbsolute(rel)) {
2342
+ console.error(
2343
+ `\u274C Refusing to write service file outside services/: ${relPath}`
2344
+ );
2345
+ process.exit(1);
2346
+ }
2347
+ fs9.mkdirSync(path7.dirname(dest), { recursive: true });
2348
+ fs9.writeFileSync(dest, content);
2349
+ }
2116
2350
  console.log(`\u2705 Bundle files written to: ${outputDir}`);
2117
2351
  console.log(` Package: ${finalPackage}`);
2118
2352
  console.log(` Version: ${finalVersion}`);
@@ -2120,6 +2354,11 @@ Note:
2120
2354
  if (abiArtifact) {
2121
2355
  console.log(` ABI Hash: ${abiArtifact.hash}`);
2122
2356
  }
2357
+ if (serviceArtifacts.length > 0) {
2358
+ console.log(
2359
+ ` Services: ${serviceArtifacts.map((s) => s.name).join(", ")}`
2360
+ );
2361
+ }
2123
2362
  console.log("");
2124
2363
  console.log("Next steps:");
2125
2364
  console.log(
@@ -2212,16 +2451,57 @@ Note:
2212
2451
  process.exit(1);
2213
2452
  }
2214
2453
  tempMpk = path7.join(fullPath, `.temp-push-${Date.now()}.mpk`);
2215
- const archiveFiles = ["manifest.json", "app.wasm"];
2216
- if (fs9.existsSync(path7.join(fullPath, "abi.json"))) {
2217
- archiveFiles.push("abi.json");
2454
+ let dirManifest;
2455
+ try {
2456
+ dirManifest = JSON.parse(
2457
+ fs9.readFileSync(manifestInDir, "utf8")
2458
+ );
2459
+ } catch (e) {
2460
+ console.error(
2461
+ `\u274C Failed to parse manifest.json in ${fullPath}: ${e instanceof Error ? e.message : String(e)}`
2462
+ );
2463
+ console.error(
2464
+ " Fix the manifest JSON and retry (service files would otherwise be silently omitted)."
2465
+ );
2466
+ process.exit(1);
2467
+ }
2468
+ if (!dirManifest.wasm?.path) {
2469
+ console.error(
2470
+ "\u274C manifest.json has no main wasm (wasm.path). A bundle must include its main application."
2471
+ );
2472
+ process.exit(1);
2473
+ }
2474
+ for (const svc of dirManifest.services ?? []) {
2475
+ if (!svc?.wasm?.path) {
2476
+ console.error(
2477
+ `\u274C Service "${svc?.name ?? "<unnamed>"}" in manifest.json has no wasm.path.`
2478
+ );
2479
+ process.exit(1);
2480
+ }
2481
+ }
2482
+ let archiveFiles;
2483
+ try {
2484
+ archiveFiles = collectBundleFiles(dirManifest);
2485
+ } catch (e) {
2486
+ console.error(`\u274C ${e instanceof Error ? e.message : String(e)}`);
2487
+ process.exit(1);
2488
+ }
2489
+ for (const f of archiveFiles) {
2490
+ if (!fs9.existsSync(path7.join(fullPath, f))) {
2491
+ console.error(
2492
+ `\u274C Bundle file referenced by manifest not found: ${f}`
2493
+ );
2494
+ process.exit(1);
2495
+ }
2218
2496
  }
2219
2497
  await tar.create(
2220
2498
  { gzip: true, file: tempMpk, cwd: fullPath },
2221
2499
  archiveFiles
2222
2500
  );
2223
2501
  mpkPath = tempMpk;
2224
- console.log(`\u{1F4E6} Packed directory into temporary bundle`);
2502
+ console.log(
2503
+ `\u{1F4E6} Packed directory into temporary bundle (${archiveFiles.length} files)`
2504
+ );
2225
2505
  }
2226
2506
  console.log(`\u{1F4E6} Processing bundle: ${path7.basename(mpkPath)}`);
2227
2507
  const manifest = await extractManifest(mpkPath);
@@ -2476,6 +2756,9 @@ async function pushToRemote(bundlePath, manifest, registryUrl, apiKey) {
2476
2756
  if (manifest.abi !== void 0) {
2477
2757
  payload.abi = manifest.abi;
2478
2758
  }
2759
+ if (manifest.services !== void 0) {
2760
+ payload.services = manifest.services;
2761
+ }
2479
2762
  payload.migrations = manifest.migrations ?? [];
2480
2763
  if (manifest.links) {
2481
2764
  payload.links = manifest.links;