@calimero-network/registry-cli 1.22.4 → 1.24.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/README.md CHANGED
@@ -48,9 +48,9 @@ calimero-registry org update <orgId> [-n <name>] [-m <json>]
48
48
  calimero-registry org delete <orgId> [-y] # irreversible
49
49
 
50
50
  calimero-registry org members list <orgId>
51
- calimero-registry org members add <orgId> <email> [-r admin|member]
52
- calimero-registry org members update <orgId> <email> -r admin|member
53
- calimero-registry org members remove <orgId> <email>
51
+ calimero-registry org members add <orgId> <username> [-r admin|member]
52
+ calimero-registry org members update <orgId> <username> -r admin|member
53
+ calimero-registry org members remove <orgId> <username>
54
54
 
55
55
  calimero-registry org packages link <orgId> <package>
56
56
  calimero-registry org packages unlink <orgId> <package>
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}`, {
@@ -2690,7 +2758,7 @@ var orgCommand = new Command("org").description("Manage organizations (create, l
2690
2758
  }
2691
2759
  )
2692
2760
  ).addCommand(
2693
- new Command("members").description("List, add, or remove organization members").addCommand(
2761
+ new Command("members").description("List, invite, or remove organization members").addCommand(
2694
2762
  new Command("list").description("List members of an org").argument("<orgId>", "Organization id or slug").action(async (orgId, _options, command) => {
2695
2763
  const { url, timeout } = getGlobalOpts(command);
2696
2764
  const base = url.replace(/\/$/, "");
@@ -2715,15 +2783,17 @@ var orgCommand = new Command("org").description("Manage organizations (create, l
2715
2783
  }
2716
2784
  })
2717
2785
  ).addCommand(
2718
- new Command("add").description("Add a member by email").argument("<orgId>", "Organization id or slug").argument("<email>", "Member email address").option("-r, --role <role>", "Role: member or admin", "member").action(
2719
- async (orgId, email, options, command) => {
2786
+ new Command("add").description(
2787
+ "Invite a member by username; they join once they accept"
2788
+ ).argument("<orgId>", "Organization id or slug").argument("<username>", "Member username").option("-r, --role <role>", "Role: member or admin", "member").action(
2789
+ async (orgId, username, options, command) => {
2720
2790
  const { url, timeout } = getGlobalOpts(command);
2721
- const spinner2 = ora4("Adding member...").start();
2791
+ const spinner2 = ora4("Inviting member...").start();
2722
2792
  try {
2723
- const authHeaders = getAuthHeaders();
2793
+ const authHeaders = getAuthHeaders(url);
2724
2794
  const pathname = `/api/v2/orgs/${encodeURIComponent(orgId)}/members`;
2725
2795
  const body = {
2726
- email: email.trim(),
2796
+ username: username.trim().replace(/^@+/, ""),
2727
2797
  role: options.role === "admin" ? "admin" : "member"
2728
2798
  };
2729
2799
  const base = url.replace(/\/$/, "");
@@ -2740,7 +2810,9 @@ var orgCommand = new Command("org").description("Manage organizations (create, l
2740
2810
  console.error(chalk4.red(JSON.stringify(data)));
2741
2811
  process.exit(1);
2742
2812
  }
2743
- spinner2.succeed("Member added");
2813
+ spinner2.succeed(
2814
+ status === 202 ? "Invitation sent; the user joins once they accept it" : "Member added"
2815
+ );
2744
2816
  } catch (e) {
2745
2817
  spinner2.fail("Failed");
2746
2818
  console.error(
@@ -2751,13 +2823,13 @@ var orgCommand = new Command("org").description("Manage organizations (create, l
2751
2823
  }
2752
2824
  )
2753
2825
  ).addCommand(
2754
- new Command("update").description("Update a member role (admin or member)").argument("<orgId>", "Organization id or slug").argument("<email>", "Member email address").requiredOption("-r, --role <role>", "New role: admin or member").action(
2826
+ new Command("update").description("Update a member role (admin or member)").argument("<orgId>", "Organization id or slug").argument("<member>", "Member username or email address").requiredOption("-r, --role <role>", "New role: admin or member").action(
2755
2827
  async (orgId, email, options, command) => {
2756
2828
  const { url, timeout } = getGlobalOpts(command);
2757
2829
  const role = options.role === "admin" ? "admin" : "member";
2758
2830
  const spinner2 = ora4("Updating member role...").start();
2759
2831
  try {
2760
- const authHeaders = getAuthHeaders();
2832
+ const authHeaders = getAuthHeaders(url);
2761
2833
  const pathname = `/api/v2/orgs/${encodeURIComponent(orgId)}/members/${encodeURIComponent(email.trim())}`;
2762
2834
  const body = { role };
2763
2835
  const base = url.replace(/\/$/, "");
@@ -2785,12 +2857,12 @@ var orgCommand = new Command("org").description("Manage organizations (create, l
2785
2857
  }
2786
2858
  )
2787
2859
  ).addCommand(
2788
- new Command("remove").description("Remove a member by email").argument("<orgId>", "Organization id or slug").argument("<email>", "Member email address").action(
2860
+ new Command("remove").description("Remove a member by username or email").argument("<orgId>", "Organization id or slug").argument("<member>", "Member username or email address").action(
2789
2861
  async (orgId, email, _options, command) => {
2790
2862
  const { url, timeout } = getGlobalOpts(command);
2791
2863
  const spinner2 = ora4("Removing member...").start();
2792
2864
  try {
2793
- const authHeaders = getAuthHeaders();
2865
+ const authHeaders = getAuthHeaders(url);
2794
2866
  const pathname = `/api/v2/orgs/${encodeURIComponent(orgId)}/members/${encodeURIComponent(email.trim())}`;
2795
2867
  const base = url.replace(/\/$/, "");
2796
2868
  const { data, status } = await fetchJson(`${base}${pathname}`, {
@@ -2823,7 +2895,7 @@ var orgCommand = new Command("org").description("Manage organizations (create, l
2823
2895
  const { url, timeout } = getGlobalOpts(command);
2824
2896
  const spinner2 = ora4("Linking package...").start();
2825
2897
  try {
2826
- const authHeaders = getAuthHeaders();
2898
+ const authHeaders = getAuthHeaders(url);
2827
2899
  const pathname = `/api/v2/orgs/${encodeURIComponent(orgId)}/packages`;
2828
2900
  const body = { package: pkg.trim() };
2829
2901
  const base = url.replace(/\/$/, "");
@@ -2856,7 +2928,7 @@ var orgCommand = new Command("org").description("Manage organizations (create, l
2856
2928
  const { url, timeout } = getGlobalOpts(command);
2857
2929
  const spinner2 = ora4("Unlinking package...").start();
2858
2930
  try {
2859
- const authHeaders = getAuthHeaders();
2931
+ const authHeaders = getAuthHeaders(url);
2860
2932
  const pathname = `/api/v2/orgs/${encodeURIComponent(orgId)}/packages/${encodeURIComponent(pkg.trim())}`;
2861
2933
  const base = url.replace(/\/$/, "");
2862
2934
  const { data, status } = await fetchJson(`${base}${pathname}`, {