@calimero-network/registry-cli 1.22.3 → 1.23.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
@@ -15,7 +15,7 @@ var DEFAULT_REGISTRY_URL = "https://apps.calimero.network";
15
15
  function defaultConfig() {
16
16
  return { registry: { url: DEFAULT_REGISTRY_URL } };
17
17
  }
18
- var RemoteConfig = class {
18
+ var RemoteConfig = class _RemoteConfig {
19
19
  configPath;
20
20
  config;
21
21
  constructor() {
@@ -57,7 +57,13 @@ var RemoteConfig = class {
57
57
  ...this.config.registry.apiKey ? { apiKey: this.config.registry.apiKey } : {}
58
58
  }
59
59
  };
60
- fs7.writeFileSync(this.configPath, JSON.stringify(configToSave, null, 2));
60
+ fs7.writeFileSync(this.configPath, JSON.stringify(configToSave, null, 2), {
61
+ mode: 384
62
+ });
63
+ try {
64
+ fs7.chmodSync(this.configPath, 384);
65
+ } catch {
66
+ }
61
67
  }
62
68
  /**
63
69
  * Get registry URL with priority:
@@ -72,9 +78,44 @@ var RemoteConfig = class {
72
78
  * Set registry URL
73
79
  */
74
80
  setRegistryUrl(url) {
81
+ if (_RemoteConfig.isInsecureRemoteUrl(url)) {
82
+ console.warn(
83
+ "\u26A0\uFE0F Registry URL uses http:// on a non-local host. Your API key would be sent in cleartext \u2014 prefer https:// for remote registries."
84
+ );
85
+ }
75
86
  this.config.registry.url = url;
76
87
  this.saveConfig();
77
88
  }
89
+ /**
90
+ * True when `url` is http:// to a host other than localhost/loopback, i.e. a
91
+ * Bearer token would traverse the network in cleartext.
92
+ */
93
+ static isInsecureRemoteUrl(url) {
94
+ let parsed;
95
+ try {
96
+ parsed = new URL(url);
97
+ } catch {
98
+ return false;
99
+ }
100
+ if (parsed.protocol !== "http:") return false;
101
+ const host = parsed.hostname.toLowerCase().replace(/^\[(.*)\]$/, "$1");
102
+ const isLocal = host === "localhost" || host === "127.0.0.1" || host === "::1" || host.endsWith(".localhost");
103
+ return !isLocal;
104
+ }
105
+ /**
106
+ * Throws when an API key would be sent to `url` over plain http:// to a
107
+ * non-loopback host. Called wherever the effective registry URL (flag, env
108
+ * var or config file) is about to receive the Bearer key. https:// and
109
+ * http://localhost / 127.0.0.1 / [::1] are always allowed.
110
+ */
111
+ static assertApiKeyTransport(url, apiKey) {
112
+ if (!apiKey) return;
113
+ if (_RemoteConfig.isInsecureRemoteUrl(url)) {
114
+ throw new Error(
115
+ `Refusing to send the API key to ${url}: plain http:// is only allowed for localhost / 127.0.0.1 / [::1]. Use an https:// registry URL (check --url, CALIMERO_REGISTRY_URL and \`calimero-registry config get registry-url\`).`
116
+ );
117
+ }
118
+ }
78
119
  /**
79
120
  * Get API key with priority:
80
121
  * 1. Environment variable CALIMERO_API_KEY
@@ -749,6 +790,18 @@ var LocalArtifactServer = class {
749
790
  };
750
791
  }
751
792
  };
793
+ function isAllowedOrigin(origin, allowedHosts) {
794
+ let parsed;
795
+ try {
796
+ parsed = new URL(origin);
797
+ } catch {
798
+ return false;
799
+ }
800
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
801
+ return false;
802
+ }
803
+ return allowedHosts.has(parsed.hostname.toLowerCase());
804
+ }
752
805
  var LocalRegistryServer = class {
753
806
  config;
754
807
  dataStore;
@@ -794,6 +847,43 @@ var LocalRegistryServer = class {
794
847
  });
795
848
  }
796
849
  });
850
+ const allowedOriginHosts = /* @__PURE__ */ new Set();
851
+ for (const name of allowedHosts) {
852
+ allowedOriginHosts.add(name.replace(/:\d+$/, ""));
853
+ }
854
+ this.server.addHook("onRequest", async (request, reply) => {
855
+ const method = request.method.toUpperCase();
856
+ if (method === "GET" || method === "HEAD" || method === "OPTIONS") {
857
+ return;
858
+ }
859
+ const origin = request.headers.origin;
860
+ if (origin !== void 0 && !isAllowedOrigin(origin, allowedOriginHosts)) {
861
+ return reply.code(403).send({
862
+ statusCode: 403,
863
+ error: "Forbidden",
864
+ message: "Cross-origin request not allowed"
865
+ });
866
+ }
867
+ const fetchSite = request.headers["sec-fetch-site"];
868
+ if (fetchSite !== void 0 && fetchSite !== "same-origin" && fetchSite !== "none") {
869
+ return reply.code(403).send({
870
+ statusCode: 403,
871
+ error: "Forbidden",
872
+ message: "Cross-site request not allowed"
873
+ });
874
+ }
875
+ const pathname = request.url.split("?")[0];
876
+ if (pathname.startsWith("/local/")) {
877
+ const contentType = String(request.headers["content-type"] ?? "").split(";")[0].trim().toLowerCase();
878
+ if (contentType !== "application/json") {
879
+ return reply.code(415).send({
880
+ statusCode: 415,
881
+ error: "Unsupported Media Type",
882
+ message: "Content-Type must be application/json"
883
+ });
884
+ }
885
+ }
886
+ });
797
887
  await this.registerRoutes();
798
888
  await this.server.listen({ port: serverPort, host });
799
889
  this.isRunning = true;
@@ -1728,11 +1818,12 @@ Note:
1728
1818
  - Config file values are used unless overridden by flags or environment variables
1729
1819
  - Priority: flag > environment variable > config file > default
1730
1820
  `
1731
- ).action(async (bundleFile, options) => {
1821
+ ).action(async (bundleFile, options, command) => {
1822
+ const urlFlag = command.optsWithGlobals().url;
1732
1823
  let mpkPath;
1733
1824
  let tempMpk = null;
1734
1825
  try {
1735
- if (!options.remote && (options.url || options.apiKey)) {
1826
+ if (!options.remote && (urlFlag || options.apiKey)) {
1736
1827
  console.warn(
1737
1828
  "\u26A0\uFE0F Warning: --url and --api-key are only used with --remote flag"
1738
1829
  );
@@ -1862,8 +1953,9 @@ Note:
1862
1953
  );
1863
1954
  } else if (useRemote) {
1864
1955
  const remoteConfig = new RemoteConfig();
1865
- const registryUrl = options.url || process.env.CALIMERO_REGISTRY_URL || remoteConfig.getRegistryUrl();
1956
+ const registryUrl = urlFlag || process.env.CALIMERO_REGISTRY_URL || remoteConfig.getRegistryUrl();
1866
1957
  const apiKey = options.apiKey || process.env.CALIMERO_API_KEY || remoteConfig.getApiKey();
1958
+ RemoteConfig.assertApiKeyTransport(registryUrl, apiKey);
1867
1959
  await pushToRemote(mpkPath, manifest, registryUrl, apiKey);
1868
1960
  }
1869
1961
  } catch (error) {
@@ -1922,7 +2014,8 @@ Examples:
1922
2014
  # 3) With custom registry URL:
1923
2015
  $ calimero-registry bundle edit com.calimero.myapp 1.0.0 --remote --url https://apps.calimero.network --manifest signed-manifest.json
1924
2016
  `
1925
- ).action(async (pkg, version2, options) => {
2017
+ ).action(async (pkg, version2, options, command) => {
2018
+ const urlFlag = command.optsWithGlobals().url;
1926
2019
  try {
1927
2020
  if (!options.remote) {
1928
2021
  console.error(
@@ -1931,8 +2024,9 @@ Examples:
1931
2024
  process.exit(1);
1932
2025
  }
1933
2026
  const remoteConfig = new RemoteConfig();
1934
- const registryUrl = options.url || process.env.CALIMERO_REGISTRY_URL || remoteConfig.getRegistryUrl();
2027
+ const registryUrl = urlFlag || process.env.CALIMERO_REGISTRY_URL || remoteConfig.getRegistryUrl();
1935
2028
  const apiKey = options.apiKey || process.env.CALIMERO_API_KEY || remoteConfig.getApiKey();
2029
+ RemoteConfig.assertApiKeyTransport(registryUrl, apiKey);
1936
2030
  const baseUrl = registryUrl.replace(/\/$/, "");
1937
2031
  const bundleUrl = `${baseUrl}/api/v2/bundles/${encodeURIComponent(pkg)}/${encodeURIComponent(version2)}`;
1938
2032
  const headers = {
@@ -2399,7 +2493,7 @@ function getGlobalOpts(command) {
2399
2493
  timeout: parseInt(String(opts.timeout ?? DEFAULT_TIMEOUT), 10) || DEFAULT_TIMEOUT
2400
2494
  };
2401
2495
  }
2402
- function getAuthHeaders() {
2496
+ function getAuthHeaders(url) {
2403
2497
  const remoteConfig = new RemoteConfig();
2404
2498
  const apiKey = remoteConfig.getApiKey();
2405
2499
  if (!apiKey) {
@@ -2407,6 +2501,7 @@ function getAuthHeaders() {
2407
2501
  "API token required for org write operations.\nGet a token from the Organizations page in the web UI, then run:\n calimero-registry config set api-key <token>"
2408
2502
  );
2409
2503
  }
2504
+ RemoteConfig.assertApiKeyTransport(url, apiKey);
2410
2505
  return { Authorization: `Bearer ${apiKey}` };
2411
2506
  }
2412
2507
  async function fetchJson(url, options = {}) {
@@ -2434,9 +2529,9 @@ var orgCommand = new Command("org").description("Manage organizations (create, l
2434
2529
  const spinner2 = ora4("Resolving identity...").start();
2435
2530
  let authHeaders;
2436
2531
  try {
2437
- authHeaders = getAuthHeaders();
2532
+ authHeaders = getAuthHeaders(url);
2438
2533
  } catch (e) {
2439
- spinner2.fail("API token required");
2534
+ spinner2.fail("Cannot use API token");
2440
2535
  console.error(chalk4.red(e instanceof Error ? e.message : String(e)));
2441
2536
  process.exit(1);
2442
2537
  }
@@ -2511,7 +2606,7 @@ var orgCommand = new Command("org").description("Manage organizations (create, l
2511
2606
  const { url, timeout } = getGlobalOpts(command);
2512
2607
  const spinner2 = ora4("Creating organization...").start();
2513
2608
  try {
2514
- const authHeaders = getAuthHeaders();
2609
+ const authHeaders = getAuthHeaders(url);
2515
2610
  const pathname = "/api/v2/orgs";
2516
2611
  const body = {
2517
2612
  name: options.name.trim(),
@@ -2572,7 +2667,7 @@ var orgCommand = new Command("org").description("Manage organizations (create, l
2572
2667
  const { url, timeout } = getGlobalOpts(command);
2573
2668
  const spinner2 = ora4("Updating organization...").start();
2574
2669
  try {
2575
- const authHeaders = getAuthHeaders();
2670
+ const authHeaders = getAuthHeaders(url);
2576
2671
  const pathname = `/api/v2/orgs/${encodeURIComponent(orgId)}`;
2577
2672
  const body = {};
2578
2673
  if (options.name !== void 0) body.name = options.name;
@@ -2637,7 +2732,7 @@ var orgCommand = new Command("org").description("Manage organizations (create, l
2637
2732
  }
2638
2733
  const spinner2 = ora4("Deleting organization...").start();
2639
2734
  try {
2640
- const authHeaders = getAuthHeaders();
2735
+ const authHeaders = getAuthHeaders(url);
2641
2736
  const pathname = `/api/v2/orgs/${encodeURIComponent(orgId)}`;
2642
2737
  const base = url.replace(/\/$/, "");
2643
2738
  const { data, status } = await fetchJson(`${base}${pathname}`, {
@@ -2693,7 +2788,7 @@ var orgCommand = new Command("org").description("Manage organizations (create, l
2693
2788
  const { url, timeout } = getGlobalOpts(command);
2694
2789
  const spinner2 = ora4("Adding member...").start();
2695
2790
  try {
2696
- const authHeaders = getAuthHeaders();
2791
+ const authHeaders = getAuthHeaders(url);
2697
2792
  const pathname = `/api/v2/orgs/${encodeURIComponent(orgId)}/members`;
2698
2793
  const body = {
2699
2794
  email: email.trim(),
@@ -2730,7 +2825,7 @@ var orgCommand = new Command("org").description("Manage organizations (create, l
2730
2825
  const role = options.role === "admin" ? "admin" : "member";
2731
2826
  const spinner2 = ora4("Updating member role...").start();
2732
2827
  try {
2733
- const authHeaders = getAuthHeaders();
2828
+ const authHeaders = getAuthHeaders(url);
2734
2829
  const pathname = `/api/v2/orgs/${encodeURIComponent(orgId)}/members/${encodeURIComponent(email.trim())}`;
2735
2830
  const body = { role };
2736
2831
  const base = url.replace(/\/$/, "");
@@ -2763,7 +2858,7 @@ var orgCommand = new Command("org").description("Manage organizations (create, l
2763
2858
  const { url, timeout } = getGlobalOpts(command);
2764
2859
  const spinner2 = ora4("Removing member...").start();
2765
2860
  try {
2766
- const authHeaders = getAuthHeaders();
2861
+ const authHeaders = getAuthHeaders(url);
2767
2862
  const pathname = `/api/v2/orgs/${encodeURIComponent(orgId)}/members/${encodeURIComponent(email.trim())}`;
2768
2863
  const base = url.replace(/\/$/, "");
2769
2864
  const { data, status } = await fetchJson(`${base}${pathname}`, {
@@ -2796,7 +2891,7 @@ var orgCommand = new Command("org").description("Manage organizations (create, l
2796
2891
  const { url, timeout } = getGlobalOpts(command);
2797
2892
  const spinner2 = ora4("Linking package...").start();
2798
2893
  try {
2799
- const authHeaders = getAuthHeaders();
2894
+ const authHeaders = getAuthHeaders(url);
2800
2895
  const pathname = `/api/v2/orgs/${encodeURIComponent(orgId)}/packages`;
2801
2896
  const body = { package: pkg.trim() };
2802
2897
  const base = url.replace(/\/$/, "");
@@ -2829,7 +2924,7 @@ var orgCommand = new Command("org").description("Manage organizations (create, l
2829
2924
  const { url, timeout } = getGlobalOpts(command);
2830
2925
  const spinner2 = ora4("Unlinking package...").start();
2831
2926
  try {
2832
- const authHeaders = getAuthHeaders();
2927
+ const authHeaders = getAuthHeaders(url);
2833
2928
  const pathname = `/api/v2/orgs/${encodeURIComponent(orgId)}/packages/${encodeURIComponent(pkg.trim())}`;
2834
2929
  const base = url.replace(/\/$/, "");
2835
2930
  const { data, status } = await fetchJson(`${base}${pathname}`, {