@calimero-network/registry-cli 1.22.4 → 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
@@ -98,10 +98,24 @@ var RemoteConfig = class _RemoteConfig {
98
98
  return false;
99
99
  }
100
100
  if (parsed.protocol !== "http:") return false;
101
- const host = parsed.hostname.toLowerCase();
101
+ const host = parsed.hostname.toLowerCase().replace(/^\[(.*)\]$/, "$1");
102
102
  const isLocal = host === "localhost" || host === "127.0.0.1" || host === "::1" || host.endsWith(".localhost");
103
103
  return !isLocal;
104
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
+ }
105
119
  /**
106
120
  * Get API key with priority:
107
121
  * 1. Environment variable CALIMERO_API_KEY
@@ -776,6 +790,18 @@ var LocalArtifactServer = class {
776
790
  };
777
791
  }
778
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
+ }
779
805
  var LocalRegistryServer = class {
780
806
  config;
781
807
  dataStore;
@@ -821,6 +847,43 @@ var LocalRegistryServer = class {
821
847
  });
822
848
  }
823
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
+ });
824
887
  await this.registerRoutes();
825
888
  await this.server.listen({ port: serverPort, host });
826
889
  this.isRunning = true;
@@ -1755,11 +1818,12 @@ Note:
1755
1818
  - Config file values are used unless overridden by flags or environment variables
1756
1819
  - Priority: flag > environment variable > config file > default
1757
1820
  `
1758
- ).action(async (bundleFile, options) => {
1821
+ ).action(async (bundleFile, options, command) => {
1822
+ const urlFlag = command.optsWithGlobals().url;
1759
1823
  let mpkPath;
1760
1824
  let tempMpk = null;
1761
1825
  try {
1762
- if (!options.remote && (options.url || options.apiKey)) {
1826
+ if (!options.remote && (urlFlag || options.apiKey)) {
1763
1827
  console.warn(
1764
1828
  "\u26A0\uFE0F Warning: --url and --api-key are only used with --remote flag"
1765
1829
  );
@@ -1889,8 +1953,9 @@ Note:
1889
1953
  );
1890
1954
  } else if (useRemote) {
1891
1955
  const remoteConfig = new RemoteConfig();
1892
- const registryUrl = options.url || process.env.CALIMERO_REGISTRY_URL || remoteConfig.getRegistryUrl();
1956
+ const registryUrl = urlFlag || process.env.CALIMERO_REGISTRY_URL || remoteConfig.getRegistryUrl();
1893
1957
  const apiKey = options.apiKey || process.env.CALIMERO_API_KEY || remoteConfig.getApiKey();
1958
+ RemoteConfig.assertApiKeyTransport(registryUrl, apiKey);
1894
1959
  await pushToRemote(mpkPath, manifest, registryUrl, apiKey);
1895
1960
  }
1896
1961
  } catch (error) {
@@ -1949,7 +2014,8 @@ Examples:
1949
2014
  # 3) With custom registry URL:
1950
2015
  $ calimero-registry bundle edit com.calimero.myapp 1.0.0 --remote --url https://apps.calimero.network --manifest signed-manifest.json
1951
2016
  `
1952
- ).action(async (pkg, version2, options) => {
2017
+ ).action(async (pkg, version2, options, command) => {
2018
+ const urlFlag = command.optsWithGlobals().url;
1953
2019
  try {
1954
2020
  if (!options.remote) {
1955
2021
  console.error(
@@ -1958,8 +2024,9 @@ Examples:
1958
2024
  process.exit(1);
1959
2025
  }
1960
2026
  const remoteConfig = new RemoteConfig();
1961
- const registryUrl = options.url || process.env.CALIMERO_REGISTRY_URL || remoteConfig.getRegistryUrl();
2027
+ const registryUrl = urlFlag || process.env.CALIMERO_REGISTRY_URL || remoteConfig.getRegistryUrl();
1962
2028
  const apiKey = options.apiKey || process.env.CALIMERO_API_KEY || remoteConfig.getApiKey();
2029
+ RemoteConfig.assertApiKeyTransport(registryUrl, apiKey);
1963
2030
  const baseUrl = registryUrl.replace(/\/$/, "");
1964
2031
  const bundleUrl = `${baseUrl}/api/v2/bundles/${encodeURIComponent(pkg)}/${encodeURIComponent(version2)}`;
1965
2032
  const headers = {
@@ -2426,7 +2493,7 @@ function getGlobalOpts(command) {
2426
2493
  timeout: parseInt(String(opts.timeout ?? DEFAULT_TIMEOUT), 10) || DEFAULT_TIMEOUT
2427
2494
  };
2428
2495
  }
2429
- function getAuthHeaders() {
2496
+ function getAuthHeaders(url) {
2430
2497
  const remoteConfig = new RemoteConfig();
2431
2498
  const apiKey = remoteConfig.getApiKey();
2432
2499
  if (!apiKey) {
@@ -2434,6 +2501,7 @@ function getAuthHeaders() {
2434
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>"
2435
2502
  );
2436
2503
  }
2504
+ RemoteConfig.assertApiKeyTransport(url, apiKey);
2437
2505
  return { Authorization: `Bearer ${apiKey}` };
2438
2506
  }
2439
2507
  async function fetchJson(url, options = {}) {
@@ -2461,9 +2529,9 @@ var orgCommand = new Command("org").description("Manage organizations (create, l
2461
2529
  const spinner2 = ora4("Resolving identity...").start();
2462
2530
  let authHeaders;
2463
2531
  try {
2464
- authHeaders = getAuthHeaders();
2532
+ authHeaders = getAuthHeaders(url);
2465
2533
  } catch (e) {
2466
- spinner2.fail("API token required");
2534
+ spinner2.fail("Cannot use API token");
2467
2535
  console.error(chalk4.red(e instanceof Error ? e.message : String(e)));
2468
2536
  process.exit(1);
2469
2537
  }
@@ -2538,7 +2606,7 @@ var orgCommand = new Command("org").description("Manage organizations (create, l
2538
2606
  const { url, timeout } = getGlobalOpts(command);
2539
2607
  const spinner2 = ora4("Creating organization...").start();
2540
2608
  try {
2541
- const authHeaders = getAuthHeaders();
2609
+ const authHeaders = getAuthHeaders(url);
2542
2610
  const pathname = "/api/v2/orgs";
2543
2611
  const body = {
2544
2612
  name: options.name.trim(),
@@ -2599,7 +2667,7 @@ var orgCommand = new Command("org").description("Manage organizations (create, l
2599
2667
  const { url, timeout } = getGlobalOpts(command);
2600
2668
  const spinner2 = ora4("Updating organization...").start();
2601
2669
  try {
2602
- const authHeaders = getAuthHeaders();
2670
+ const authHeaders = getAuthHeaders(url);
2603
2671
  const pathname = `/api/v2/orgs/${encodeURIComponent(orgId)}`;
2604
2672
  const body = {};
2605
2673
  if (options.name !== void 0) body.name = options.name;
@@ -2664,7 +2732,7 @@ var orgCommand = new Command("org").description("Manage organizations (create, l
2664
2732
  }
2665
2733
  const spinner2 = ora4("Deleting organization...").start();
2666
2734
  try {
2667
- const authHeaders = getAuthHeaders();
2735
+ const authHeaders = getAuthHeaders(url);
2668
2736
  const pathname = `/api/v2/orgs/${encodeURIComponent(orgId)}`;
2669
2737
  const base = url.replace(/\/$/, "");
2670
2738
  const { data, status } = await fetchJson(`${base}${pathname}`, {
@@ -2720,7 +2788,7 @@ var orgCommand = new Command("org").description("Manage organizations (create, l
2720
2788
  const { url, timeout } = getGlobalOpts(command);
2721
2789
  const spinner2 = ora4("Adding member...").start();
2722
2790
  try {
2723
- const authHeaders = getAuthHeaders();
2791
+ const authHeaders = getAuthHeaders(url);
2724
2792
  const pathname = `/api/v2/orgs/${encodeURIComponent(orgId)}/members`;
2725
2793
  const body = {
2726
2794
  email: email.trim(),
@@ -2757,7 +2825,7 @@ var orgCommand = new Command("org").description("Manage organizations (create, l
2757
2825
  const role = options.role === "admin" ? "admin" : "member";
2758
2826
  const spinner2 = ora4("Updating member role...").start();
2759
2827
  try {
2760
- const authHeaders = getAuthHeaders();
2828
+ const authHeaders = getAuthHeaders(url);
2761
2829
  const pathname = `/api/v2/orgs/${encodeURIComponent(orgId)}/members/${encodeURIComponent(email.trim())}`;
2762
2830
  const body = { role };
2763
2831
  const base = url.replace(/\/$/, "");
@@ -2790,7 +2858,7 @@ var orgCommand = new Command("org").description("Manage organizations (create, l
2790
2858
  const { url, timeout } = getGlobalOpts(command);
2791
2859
  const spinner2 = ora4("Removing member...").start();
2792
2860
  try {
2793
- const authHeaders = getAuthHeaders();
2861
+ const authHeaders = getAuthHeaders(url);
2794
2862
  const pathname = `/api/v2/orgs/${encodeURIComponent(orgId)}/members/${encodeURIComponent(email.trim())}`;
2795
2863
  const base = url.replace(/\/$/, "");
2796
2864
  const { data, status } = await fetchJson(`${base}${pathname}`, {
@@ -2823,7 +2891,7 @@ var orgCommand = new Command("org").description("Manage organizations (create, l
2823
2891
  const { url, timeout } = getGlobalOpts(command);
2824
2892
  const spinner2 = ora4("Linking package...").start();
2825
2893
  try {
2826
- const authHeaders = getAuthHeaders();
2894
+ const authHeaders = getAuthHeaders(url);
2827
2895
  const pathname = `/api/v2/orgs/${encodeURIComponent(orgId)}/packages`;
2828
2896
  const body = { package: pkg.trim() };
2829
2897
  const base = url.replace(/\/$/, "");
@@ -2856,7 +2924,7 @@ var orgCommand = new Command("org").description("Manage organizations (create, l
2856
2924
  const { url, timeout } = getGlobalOpts(command);
2857
2925
  const spinner2 = ora4("Unlinking package...").start();
2858
2926
  try {
2859
- const authHeaders = getAuthHeaders();
2927
+ const authHeaders = getAuthHeaders(url);
2860
2928
  const pathname = `/api/v2/orgs/${encodeURIComponent(orgId)}/packages/${encodeURIComponent(pkg.trim())}`;
2861
2929
  const base = url.replace(/\/$/, "");
2862
2930
  const { data, status } = await fetchJson(`${base}${pathname}`, {