@solongate/proxy 0.6.8 → 0.7.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
@@ -2061,7 +2061,7 @@ function parseCliArgs() {
2061
2061
  }
2062
2062
  }
2063
2063
  if (!apiKey) {
2064
- log5("ERROR: API key not found.");
2064
+ log5(red("ERROR: API key not found."));
2065
2065
  log5("");
2066
2066
  log5("Set it in .env file:");
2067
2067
  log5(" SOLONGATE_API_KEY=sg_live_...");
@@ -2071,53 +2071,169 @@ function parseCliArgs() {
2071
2071
  process.exit(1);
2072
2072
  }
2073
2073
  if (!apiKey.startsWith("sg_live_")) {
2074
- log5("ERROR: Pull/push requires a live API key (sg_live_...).");
2074
+ log5(red("ERROR: Pull/push/list requires a live API key (sg_live_...)."));
2075
2075
  process.exit(1);
2076
2076
  }
2077
2077
  return { command, apiKey, file: resolve5(file), policyId };
2078
2078
  }
2079
2079
  async function listPolicies(apiKey) {
2080
- const res = await fetch("https://api.solongate.com/api/v1/policies", {
2080
+ const res = await fetch(`${API_URL}/api/v1/policies`, {
2081
2081
  headers: { "Authorization": `Bearer ${apiKey}` }
2082
2082
  });
2083
2083
  if (!res.ok) throw new Error(`Failed to list policies (${res.status})`);
2084
2084
  const data = await res.json();
2085
2085
  return data.policies ?? [];
2086
2086
  }
2087
+ async function list(apiKey, policyId) {
2088
+ const policies = await listPolicies(apiKey);
2089
+ if (policies.length === 0) {
2090
+ log5(yellow("No policies found. Create one in the dashboard first."));
2091
+ log5(dim(" https://dashboard.solongate.com/policies"));
2092
+ return;
2093
+ }
2094
+ if (policyId) {
2095
+ const match = policies.find((p) => p.id === policyId);
2096
+ if (!match) {
2097
+ log5(red(`Policy not found: ${policyId}`));
2098
+ log5("");
2099
+ log5("Available policies:");
2100
+ for (const p of policies) {
2101
+ log5(` ${dim("\u2022")} ${p.id}`);
2102
+ }
2103
+ process.exit(1);
2104
+ }
2105
+ const full = await fetchCloudPolicy(apiKey, API_URL, policyId);
2106
+ printPolicyDetail(full);
2107
+ return;
2108
+ }
2109
+ log5("");
2110
+ log5(bold(` Policies (${policies.length})`));
2111
+ log5(dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2112
+ log5("");
2113
+ for (const p of policies) {
2114
+ try {
2115
+ const full = await fetchCloudPolicy(apiKey, API_URL, p.id);
2116
+ printPolicySummary(p, full.rules);
2117
+ } catch {
2118
+ printPolicySummary(p, []);
2119
+ }
2120
+ }
2121
+ log5(dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2122
+ log5("");
2123
+ log5(` ${dim("View details:")} solongate-proxy list --policy-id <ID>`);
2124
+ log5(` ${dim("Pull policy:")} solongate-proxy pull --policy-id <ID>`);
2125
+ log5(` ${dim("Push policy:")} solongate-proxy push --policy-id <ID>`);
2126
+ log5("");
2127
+ }
2128
+ function printPolicySummary(p, rules) {
2129
+ const ruleCount = rules.length;
2130
+ const allowCount = rules.filter((r) => r.effect === "ALLOW").length;
2131
+ const denyCount = rules.filter((r) => r.effect === "DENY").length;
2132
+ log5(` ${cyan(p.id)}`);
2133
+ log5(` ${bold(p.name)} ${dim(`v${p.version ?? "?"}`)}`);
2134
+ log5(` ${dim("Rules:")} ${ruleCount} ${green(`${allowCount} ALLOW`)} ${red(`${denyCount} DENY`)}`);
2135
+ if (p.created_at) {
2136
+ log5(` ${dim("Updated:")} ${new Date(p.created_at).toLocaleString()}`);
2137
+ }
2138
+ log5("");
2139
+ }
2140
+ function printPolicyDetail(policy) {
2141
+ log5("");
2142
+ log5(bold(` ${policy.name}`));
2143
+ log5(` ${dim("ID:")} ${cyan(policy.id)} ${dim("Version:")} ${policy.version} ${dim("Rules:")} ${policy.rules.length}`);
2144
+ log5("");
2145
+ if (policy.rules.length === 0) {
2146
+ log5(yellow(" No rules defined."));
2147
+ log5("");
2148
+ return;
2149
+ }
2150
+ log5(dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2151
+ for (const rule of policy.rules) {
2152
+ const effectColor = rule.effect === "ALLOW" ? green : red;
2153
+ log5("");
2154
+ log5(` ${effectColor(rule.effect.padEnd(5))} ${bold(rule.toolPattern)} ${dim(`P:${rule.priority}`)}`);
2155
+ if (rule.description) {
2156
+ log5(` ${dim(rule.description)}`);
2157
+ }
2158
+ log5(` ${dim(`${rule.permission} trust:${rule.minimumTrustLevel || "UNTRUSTED"}`)}`);
2159
+ if (rule.pathConstraints) {
2160
+ const pc = rule.pathConstraints;
2161
+ if (pc.rootDirectory) log5(` ${magenta("ROOT")} ${pc.rootDirectory}`);
2162
+ if (pc.allowed?.length) log5(` ${green("PATHS")} ${pc.allowed.join(", ")}`);
2163
+ if (pc.denied?.length) log5(` ${red("DENY")} ${pc.denied.join(", ")}`);
2164
+ }
2165
+ if (rule.commandConstraints) {
2166
+ const cc = rule.commandConstraints;
2167
+ if (cc.allowed?.length) log5(` ${green("CMDS")} ${cc.allowed.join(", ")}`);
2168
+ if (cc.denied?.length) log5(` ${red("DENY")} ${cc.denied.join(", ")}`);
2169
+ }
2170
+ if (rule.filenameConstraints) {
2171
+ const fc = rule.filenameConstraints;
2172
+ if (fc.allowed?.length) log5(` ${green("FILES")} ${fc.allowed.join(", ")}`);
2173
+ if (fc.denied?.length) log5(` ${red("DENY")} ${fc.denied.join(", ")}`);
2174
+ }
2175
+ if (rule.urlConstraints) {
2176
+ const uc = rule.urlConstraints;
2177
+ if (uc.allowed?.length) log5(` ${green("URLS")} ${uc.allowed.join(", ")}`);
2178
+ if (uc.denied?.length) log5(` ${red("DENY")} ${uc.denied.join(", ")}`);
2179
+ }
2180
+ }
2181
+ log5("");
2182
+ log5(dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2183
+ log5("");
2184
+ }
2087
2185
  async function pull(apiKey, file, policyId) {
2088
- const apiUrl = "https://api.solongate.com";
2089
2186
  if (!policyId) {
2090
2187
  const policies = await listPolicies(apiKey);
2091
2188
  if (policies.length === 0) {
2092
- log5("No policies found. Create one in the dashboard first.");
2189
+ log5(red("No policies found. Create one in the dashboard first."));
2093
2190
  process.exit(1);
2094
2191
  }
2095
- if (policies.length > 1) {
2096
- log5(`Found ${policies.length} policies:
2097
- `);
2192
+ if (policies.length === 1) {
2193
+ policyId = policies[0].id;
2194
+ log5(dim(`Auto-selecting only policy: ${policyId}`));
2195
+ } else {
2196
+ log5(yellow(`Found ${policies.length} policies:`));
2197
+ log5("");
2098
2198
  for (const p of policies) {
2099
- log5(` ${p.id} ${p.name} (${(p.rules || []).length} rules)`);
2199
+ log5(` ${cyan(p.id)} ${p.name} ${dim(`v${p.version ?? "?"}`)}`);
2100
2200
  }
2101
2201
  log5("");
2102
2202
  log5("Use --policy-id <ID> to specify which one to pull.");
2103
2203
  process.exit(1);
2104
2204
  }
2105
2205
  }
2106
- log5(`Pulling policy from dashboard...`);
2107
- const policy = await fetchCloudPolicy(apiKey, apiUrl, policyId);
2108
- const json = JSON.stringify(policy, null, 2) + "\n";
2206
+ log5(`Pulling ${cyan(policyId)} from dashboard...`);
2207
+ const policy = await fetchCloudPolicy(apiKey, API_URL, policyId);
2208
+ const { id: _id, ...policyWithoutId } = policy;
2209
+ const json = JSON.stringify(policyWithoutId, null, 2) + "\n";
2109
2210
  writeFileSync5(file, json, "utf-8");
2110
- log5(`Saved: ${file}`);
2111
- log5(` Name: ${policy.name}`);
2112
- log5(` Version: ${policy.version}`);
2113
- log5(` Rules: ${policy.rules.length}`);
2114
2211
  log5("");
2115
- log5("Done. Policy pulled from dashboard to local file.");
2212
+ log5(green(" Saved to: ") + file);
2213
+ log5(` ${dim("Name:")} ${policy.name}`);
2214
+ log5(` ${dim("Version:")} ${policy.version}`);
2215
+ log5(` ${dim("Rules:")} ${policy.rules.length}`);
2216
+ log5("");
2217
+ log5(dim("The policy file does not contain an ID."));
2218
+ log5(dim("Use --policy-id to specify the target when pushing/pulling."));
2219
+ log5("");
2116
2220
  }
2117
- async function push(apiKey, file) {
2118
- const apiUrl = "https://api.solongate.com";
2221
+ async function push(apiKey, file, policyId) {
2119
2222
  if (!existsSync6(file)) {
2120
- log5(`ERROR: File not found: ${file}`);
2223
+ log5(red(`ERROR: File not found: ${file}`));
2224
+ process.exit(1);
2225
+ }
2226
+ if (!policyId) {
2227
+ log5(red("ERROR: --policy-id is required for push."));
2228
+ log5("");
2229
+ log5("This determines which cloud policy to update.");
2230
+ log5("");
2231
+ log5("Usage:");
2232
+ log5(" solongate-proxy push --policy-id my-policy");
2233
+ log5(" solongate-proxy push --policy-id my-policy --file custom.json");
2234
+ log5("");
2235
+ log5("List your policies:");
2236
+ log5(" solongate-proxy list");
2121
2237
  process.exit(1);
2122
2238
  }
2123
2239
  const content = readFileSync5(file, "utf-8");
@@ -2125,21 +2241,26 @@ async function push(apiKey, file) {
2125
2241
  try {
2126
2242
  policy = JSON.parse(content);
2127
2243
  } catch {
2128
- log5(`ERROR: Invalid JSON in ${file}`);
2244
+ log5(red(`ERROR: Invalid JSON in ${file}`));
2129
2245
  process.exit(1);
2130
2246
  }
2131
- log5(`Pushing policy to dashboard...`);
2132
- log5(` File: ${file}`);
2133
- log5(` Name: ${policy.name || "Unnamed"}`);
2134
- log5(` Rules: ${(policy.rules || []).length}`);
2135
- const res = await fetch(`${apiUrl}/api/v1/policies`, {
2136
- method: "POST",
2247
+ log5(`Pushing to ${cyan(policyId)}...`);
2248
+ log5(` ${dim("File:")} ${file}`);
2249
+ log5(` ${dim("Name:")} ${policy.name || "Unnamed"}`);
2250
+ log5(` ${dim("Rules:")} ${(policy.rules || []).length}`);
2251
+ const checkRes = await fetch(`${API_URL}/api/v1/policies/${policyId}`, {
2252
+ headers: { "Authorization": `Bearer ${apiKey}` }
2253
+ });
2254
+ const method = checkRes.ok ? "PUT" : "POST";
2255
+ const url = checkRes.ok ? `${API_URL}/api/v1/policies/${policyId}` : `${API_URL}/api/v1/policies`;
2256
+ const res = await fetch(url, {
2257
+ method,
2137
2258
  headers: {
2138
2259
  "Authorization": `Bearer ${apiKey}`,
2139
2260
  "Content-Type": "application/json"
2140
2261
  },
2141
2262
  body: JSON.stringify({
2142
- id: policy.id || "default",
2263
+ id: policyId,
2143
2264
  name: policy.name || "Local Policy",
2144
2265
  description: policy.description || "Pushed from local file",
2145
2266
  version: policy.version || 1,
@@ -2148,13 +2269,15 @@ async function push(apiKey, file) {
2148
2269
  });
2149
2270
  if (!res.ok) {
2150
2271
  const body = await res.text().catch(() => "");
2151
- log5(`ERROR: Push failed (${res.status}): ${body}`);
2272
+ log5(red(`ERROR: Push failed (${res.status}): ${body}`));
2152
2273
  process.exit(1);
2153
2274
  }
2154
2275
  const data = await res.json();
2155
- log5(` Cloud version: ${data._version ?? "created"}`);
2156
2276
  log5("");
2157
- log5("Done. Policy pushed from local file to dashboard.");
2277
+ log5(green(` Pushed to cloud: v${data._version ?? "created"}`));
2278
+ log5(` ${dim("Policy ID:")} ${policyId}`);
2279
+ log5(` ${dim("Method:")} ${method === "PUT" ? "Updated existing" : "Created new"}`);
2280
+ log5("");
2158
2281
  }
2159
2282
  async function main4() {
2160
2283
  const { command, apiKey, file, policyId } = parseCliArgs();
@@ -2162,24 +2285,45 @@ async function main4() {
2162
2285
  if (command === "pull") {
2163
2286
  await pull(apiKey, file, policyId);
2164
2287
  } else if (command === "push") {
2165
- await push(apiKey, file);
2288
+ await push(apiKey, file, policyId);
2289
+ } else if (command === "list" || command === "ls") {
2290
+ await list(apiKey, policyId);
2166
2291
  } else {
2167
- log5(`Unknown command: ${command}`);
2168
- log5("Usage: solongate-proxy pull|push [--file policy.json]");
2292
+ log5(red(`Unknown command: ${command}`));
2293
+ log5("");
2294
+ log5(bold("Usage:"));
2295
+ log5(" solongate-proxy list List all policies");
2296
+ log5(" solongate-proxy list --policy-id <ID> Show policy details");
2297
+ log5(" solongate-proxy pull --policy-id <ID> Pull policy to local file");
2298
+ log5(" solongate-proxy push --policy-id <ID> Push local file to cloud");
2299
+ log5("");
2300
+ log5(bold("Flags:"));
2301
+ log5(" --policy-id, --id <ID> Cloud policy ID (required for push)");
2302
+ log5(" --file, -f <path> Local file path (default: policy.json)");
2303
+ log5(" --api-key <key> API key (or set SOLONGATE_API_KEY)");
2304
+ log5("");
2169
2305
  process.exit(1);
2170
2306
  }
2171
2307
  } catch (err) {
2172
- log5(`ERROR: ${err instanceof Error ? err.message : String(err)}`);
2308
+ log5(red(`ERROR: ${err instanceof Error ? err.message : String(err)}`));
2173
2309
  process.exit(1);
2174
2310
  }
2175
2311
  }
2176
- var log5;
2312
+ var log5, dim, bold, green, red, yellow, cyan, magenta, API_URL;
2177
2313
  var init_pull_push = __esm({
2178
2314
  "src/pull-push.ts"() {
2179
2315
  "use strict";
2180
2316
  init_config();
2181
- log5 = (...args) => process.stderr.write(`[SolonGate] ${args.map(String).join(" ")}
2317
+ log5 = (...args) => process.stderr.write(`${args.map(String).join(" ")}
2182
2318
  `);
2319
+ dim = (s) => `\x1B[2m${s}\x1B[0m`;
2320
+ bold = (s) => `\x1B[1m${s}\x1B[0m`;
2321
+ green = (s) => `\x1B[32m${s}\x1B[0m`;
2322
+ red = (s) => `\x1B[31m${s}\x1B[0m`;
2323
+ yellow = (s) => `\x1B[33m${s}\x1B[0m`;
2324
+ cyan = (s) => `\x1B[36m${s}\x1B[0m`;
2325
+ magenta = (s) => `\x1B[35m${s}\x1B[0m`;
2326
+ API_URL = "https://api.solongate.com";
2183
2327
  main4();
2184
2328
  }
2185
2329
  });
@@ -3744,6 +3888,7 @@ var PolicySyncManager = class {
3744
3888
  pollTimer = null;
3745
3889
  watcher = null;
3746
3890
  isLiveKey;
3891
+ /** The cloud policy ID from --policy-id flag. This is the ONLY source of truth for which cloud policy to use. */
3747
3892
  policyId;
3748
3893
  constructor(opts) {
3749
3894
  this.localPath = opts.localPath;
@@ -3872,17 +4017,24 @@ var PolicySyncManager = class {
3872
4017
  }
3873
4018
  /**
3874
4019
  * Push policy to cloud API.
4020
+ * Uses this.policyId (from --policy-id CLI flag) as the cloud policy ID.
4021
+ * Falls back to policy.id from local file only if --policy-id was not set.
3875
4022
  */
3876
4023
  async pushToCloud(policy) {
3877
- const url = `${this.apiUrl}/api/v1/policies`;
4024
+ const cloudId = this.policyId || policy.id || "default";
4025
+ const existingRes = await fetch(`${this.apiUrl}/api/v1/policies/${cloudId}`, {
4026
+ headers: { "Authorization": `Bearer ${this.apiKey}` }
4027
+ });
4028
+ const method = existingRes.ok ? "PUT" : "POST";
4029
+ const url = existingRes.ok ? `${this.apiUrl}/api/v1/policies/${cloudId}` : `${this.apiUrl}/api/v1/policies`;
3878
4030
  const res = await fetch(url, {
3879
- method: "POST",
4031
+ method,
3880
4032
  headers: {
3881
4033
  "Authorization": `Bearer ${this.apiKey}`,
3882
4034
  "Content-Type": "application/json"
3883
4035
  },
3884
4036
  body: JSON.stringify({
3885
- id: policy.id || "default",
4037
+ id: cloudId,
3886
4038
  name: policy.name || "Default Policy",
3887
4039
  description: policy.description || "Synced from proxy",
3888
4040
  version: policy.version || 1,
@@ -3898,12 +4050,14 @@ var PolicySyncManager = class {
3898
4050
  }
3899
4051
  /**
3900
4052
  * Write policy to local file (with loop prevention).
4053
+ * Does NOT write the 'id' field — cloud ID is managed by --policy-id flag.
3901
4054
  */
3902
4055
  writeToFile(policy) {
3903
4056
  if (!this.localPath) return;
3904
4057
  this.skipNextWatch = true;
3905
4058
  try {
3906
- const json = JSON.stringify(policy, null, 2) + "\n";
4059
+ const { id: _id, ...rest } = policy;
4060
+ const json = JSON.stringify(rest, null, 2) + "\n";
3907
4061
  writeFileSync(this.localPath, json, "utf-8");
3908
4062
  } catch (err) {
3909
4063
  log(`File write error: ${err instanceof Error ? err.message : String(err)}`);
@@ -3911,10 +4065,10 @@ var PolicySyncManager = class {
3911
4065
  }
3912
4066
  }
3913
4067
  /**
3914
- * Compare two policies by rules content (ignoring timestamps).
4068
+ * Compare two policies by rules content (ignoring timestamps and id).
3915
4069
  */
3916
4070
  policiesEqual(a, b) {
3917
- if (a.id !== b.id || a.name !== b.name || a.rules.length !== b.rules.length) return false;
4071
+ if (a.name !== b.name || a.rules.length !== b.rules.length) return false;
3918
4072
  return JSON.stringify(a.rules) === JSON.stringify(b.rules);
3919
4073
  }
3920
4074
  };
@@ -4396,7 +4550,7 @@ async function main5() {
4396
4550
  await Promise.resolve().then(() => (init_create(), create_exports));
4397
4551
  return;
4398
4552
  }
4399
- if (subcommand === "pull" || subcommand === "push") {
4553
+ if (subcommand === "pull" || subcommand === "push" || subcommand === "list" || subcommand === "ls") {
4400
4554
  await Promise.resolve().then(() => (init_pull_push(), pull_push_exports));
4401
4555
  return;
4402
4556
  }
package/dist/pull-push.js CHANGED
@@ -44,8 +44,15 @@ async function fetchCloudPolicy(apiKey, apiUrl, policyId) {
44
44
  }
45
45
 
46
46
  // src/pull-push.ts
47
- var log = (...args) => process.stderr.write(`[SolonGate] ${args.map(String).join(" ")}
47
+ var log = (...args) => process.stderr.write(`${args.map(String).join(" ")}
48
48
  `);
49
+ var dim = (s) => `\x1B[2m${s}\x1B[0m`;
50
+ var bold = (s) => `\x1B[1m${s}\x1B[0m`;
51
+ var green = (s) => `\x1B[32m${s}\x1B[0m`;
52
+ var red = (s) => `\x1B[31m${s}\x1B[0m`;
53
+ var yellow = (s) => `\x1B[33m${s}\x1B[0m`;
54
+ var cyan = (s) => `\x1B[36m${s}\x1B[0m`;
55
+ var magenta = (s) => `\x1B[35m${s}\x1B[0m`;
49
56
  function loadEnv() {
50
57
  if (process.env.SOLONGATE_API_KEY) return;
51
58
  const envPath = resolve2(".env");
@@ -93,7 +100,7 @@ function parseCliArgs() {
93
100
  }
94
101
  }
95
102
  if (!apiKey) {
96
- log("ERROR: API key not found.");
103
+ log(red("ERROR: API key not found."));
97
104
  log("");
98
105
  log("Set it in .env file:");
99
106
  log(" SOLONGATE_API_KEY=sg_live_...");
@@ -103,53 +110,170 @@ function parseCliArgs() {
103
110
  process.exit(1);
104
111
  }
105
112
  if (!apiKey.startsWith("sg_live_")) {
106
- log("ERROR: Pull/push requires a live API key (sg_live_...).");
113
+ log(red("ERROR: Pull/push/list requires a live API key (sg_live_...)."));
107
114
  process.exit(1);
108
115
  }
109
116
  return { command, apiKey, file: resolve2(file), policyId };
110
117
  }
118
+ var API_URL = "https://api.solongate.com";
111
119
  async function listPolicies(apiKey) {
112
- const res = await fetch("https://api.solongate.com/api/v1/policies", {
120
+ const res = await fetch(`${API_URL}/api/v1/policies`, {
113
121
  headers: { "Authorization": `Bearer ${apiKey}` }
114
122
  });
115
123
  if (!res.ok) throw new Error(`Failed to list policies (${res.status})`);
116
124
  const data = await res.json();
117
125
  return data.policies ?? [];
118
126
  }
127
+ async function list(apiKey, policyId) {
128
+ const policies = await listPolicies(apiKey);
129
+ if (policies.length === 0) {
130
+ log(yellow("No policies found. Create one in the dashboard first."));
131
+ log(dim(" https://dashboard.solongate.com/policies"));
132
+ return;
133
+ }
134
+ if (policyId) {
135
+ const match = policies.find((p) => p.id === policyId);
136
+ if (!match) {
137
+ log(red(`Policy not found: ${policyId}`));
138
+ log("");
139
+ log("Available policies:");
140
+ for (const p of policies) {
141
+ log(` ${dim("\u2022")} ${p.id}`);
142
+ }
143
+ process.exit(1);
144
+ }
145
+ const full = await fetchCloudPolicy(apiKey, API_URL, policyId);
146
+ printPolicyDetail(full);
147
+ return;
148
+ }
149
+ log("");
150
+ log(bold(` Policies (${policies.length})`));
151
+ log(dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
152
+ log("");
153
+ for (const p of policies) {
154
+ try {
155
+ const full = await fetchCloudPolicy(apiKey, API_URL, p.id);
156
+ printPolicySummary(p, full.rules);
157
+ } catch {
158
+ printPolicySummary(p, []);
159
+ }
160
+ }
161
+ log(dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
162
+ log("");
163
+ log(` ${dim("View details:")} solongate-proxy list --policy-id <ID>`);
164
+ log(` ${dim("Pull policy:")} solongate-proxy pull --policy-id <ID>`);
165
+ log(` ${dim("Push policy:")} solongate-proxy push --policy-id <ID>`);
166
+ log("");
167
+ }
168
+ function printPolicySummary(p, rules) {
169
+ const ruleCount = rules.length;
170
+ const allowCount = rules.filter((r) => r.effect === "ALLOW").length;
171
+ const denyCount = rules.filter((r) => r.effect === "DENY").length;
172
+ log(` ${cyan(p.id)}`);
173
+ log(` ${bold(p.name)} ${dim(`v${p.version ?? "?"}`)}`);
174
+ log(` ${dim("Rules:")} ${ruleCount} ${green(`${allowCount} ALLOW`)} ${red(`${denyCount} DENY`)}`);
175
+ if (p.created_at) {
176
+ log(` ${dim("Updated:")} ${new Date(p.created_at).toLocaleString()}`);
177
+ }
178
+ log("");
179
+ }
180
+ function printPolicyDetail(policy) {
181
+ log("");
182
+ log(bold(` ${policy.name}`));
183
+ log(` ${dim("ID:")} ${cyan(policy.id)} ${dim("Version:")} ${policy.version} ${dim("Rules:")} ${policy.rules.length}`);
184
+ log("");
185
+ if (policy.rules.length === 0) {
186
+ log(yellow(" No rules defined."));
187
+ log("");
188
+ return;
189
+ }
190
+ log(dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
191
+ for (const rule of policy.rules) {
192
+ const effectColor = rule.effect === "ALLOW" ? green : red;
193
+ log("");
194
+ log(` ${effectColor(rule.effect.padEnd(5))} ${bold(rule.toolPattern)} ${dim(`P:${rule.priority}`)}`);
195
+ if (rule.description) {
196
+ log(` ${dim(rule.description)}`);
197
+ }
198
+ log(` ${dim(`${rule.permission} trust:${rule.minimumTrustLevel || "UNTRUSTED"}`)}`);
199
+ if (rule.pathConstraints) {
200
+ const pc = rule.pathConstraints;
201
+ if (pc.rootDirectory) log(` ${magenta("ROOT")} ${pc.rootDirectory}`);
202
+ if (pc.allowed?.length) log(` ${green("PATHS")} ${pc.allowed.join(", ")}`);
203
+ if (pc.denied?.length) log(` ${red("DENY")} ${pc.denied.join(", ")}`);
204
+ }
205
+ if (rule.commandConstraints) {
206
+ const cc = rule.commandConstraints;
207
+ if (cc.allowed?.length) log(` ${green("CMDS")} ${cc.allowed.join(", ")}`);
208
+ if (cc.denied?.length) log(` ${red("DENY")} ${cc.denied.join(", ")}`);
209
+ }
210
+ if (rule.filenameConstraints) {
211
+ const fc = rule.filenameConstraints;
212
+ if (fc.allowed?.length) log(` ${green("FILES")} ${fc.allowed.join(", ")}`);
213
+ if (fc.denied?.length) log(` ${red("DENY")} ${fc.denied.join(", ")}`);
214
+ }
215
+ if (rule.urlConstraints) {
216
+ const uc = rule.urlConstraints;
217
+ if (uc.allowed?.length) log(` ${green("URLS")} ${uc.allowed.join(", ")}`);
218
+ if (uc.denied?.length) log(` ${red("DENY")} ${uc.denied.join(", ")}`);
219
+ }
220
+ }
221
+ log("");
222
+ log(dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
223
+ log("");
224
+ }
119
225
  async function pull(apiKey, file, policyId) {
120
- const apiUrl = "https://api.solongate.com";
121
226
  if (!policyId) {
122
227
  const policies = await listPolicies(apiKey);
123
228
  if (policies.length === 0) {
124
- log("No policies found. Create one in the dashboard first.");
229
+ log(red("No policies found. Create one in the dashboard first."));
125
230
  process.exit(1);
126
231
  }
127
- if (policies.length > 1) {
128
- log(`Found ${policies.length} policies:
129
- `);
232
+ if (policies.length === 1) {
233
+ policyId = policies[0].id;
234
+ log(dim(`Auto-selecting only policy: ${policyId}`));
235
+ } else {
236
+ log(yellow(`Found ${policies.length} policies:`));
237
+ log("");
130
238
  for (const p of policies) {
131
- log(` ${p.id} ${p.name} (${(p.rules || []).length} rules)`);
239
+ log(` ${cyan(p.id)} ${p.name} ${dim(`v${p.version ?? "?"}`)}`);
132
240
  }
133
241
  log("");
134
242
  log("Use --policy-id <ID> to specify which one to pull.");
135
243
  process.exit(1);
136
244
  }
137
245
  }
138
- log(`Pulling policy from dashboard...`);
139
- const policy = await fetchCloudPolicy(apiKey, apiUrl, policyId);
140
- const json = JSON.stringify(policy, null, 2) + "\n";
246
+ log(`Pulling ${cyan(policyId)} from dashboard...`);
247
+ const policy = await fetchCloudPolicy(apiKey, API_URL, policyId);
248
+ const { id: _id, ...policyWithoutId } = policy;
249
+ const json = JSON.stringify(policyWithoutId, null, 2) + "\n";
141
250
  writeFileSync(file, json, "utf-8");
142
- log(`Saved: ${file}`);
143
- log(` Name: ${policy.name}`);
144
- log(` Version: ${policy.version}`);
145
- log(` Rules: ${policy.rules.length}`);
146
251
  log("");
147
- log("Done. Policy pulled from dashboard to local file.");
252
+ log(green(" Saved to: ") + file);
253
+ log(` ${dim("Name:")} ${policy.name}`);
254
+ log(` ${dim("Version:")} ${policy.version}`);
255
+ log(` ${dim("Rules:")} ${policy.rules.length}`);
256
+ log("");
257
+ log(dim("The policy file does not contain an ID."));
258
+ log(dim("Use --policy-id to specify the target when pushing/pulling."));
259
+ log("");
148
260
  }
149
- async function push(apiKey, file) {
150
- const apiUrl = "https://api.solongate.com";
261
+ async function push(apiKey, file, policyId) {
151
262
  if (!existsSync2(file)) {
152
- log(`ERROR: File not found: ${file}`);
263
+ log(red(`ERROR: File not found: ${file}`));
264
+ process.exit(1);
265
+ }
266
+ if (!policyId) {
267
+ log(red("ERROR: --policy-id is required for push."));
268
+ log("");
269
+ log("This determines which cloud policy to update.");
270
+ log("");
271
+ log("Usage:");
272
+ log(" solongate-proxy push --policy-id my-policy");
273
+ log(" solongate-proxy push --policy-id my-policy --file custom.json");
274
+ log("");
275
+ log("List your policies:");
276
+ log(" solongate-proxy list");
153
277
  process.exit(1);
154
278
  }
155
279
  const content = readFileSync2(file, "utf-8");
@@ -157,21 +281,26 @@ async function push(apiKey, file) {
157
281
  try {
158
282
  policy = JSON.parse(content);
159
283
  } catch {
160
- log(`ERROR: Invalid JSON in ${file}`);
284
+ log(red(`ERROR: Invalid JSON in ${file}`));
161
285
  process.exit(1);
162
286
  }
163
- log(`Pushing policy to dashboard...`);
164
- log(` File: ${file}`);
165
- log(` Name: ${policy.name || "Unnamed"}`);
166
- log(` Rules: ${(policy.rules || []).length}`);
167
- const res = await fetch(`${apiUrl}/api/v1/policies`, {
168
- method: "POST",
287
+ log(`Pushing to ${cyan(policyId)}...`);
288
+ log(` ${dim("File:")} ${file}`);
289
+ log(` ${dim("Name:")} ${policy.name || "Unnamed"}`);
290
+ log(` ${dim("Rules:")} ${(policy.rules || []).length}`);
291
+ const checkRes = await fetch(`${API_URL}/api/v1/policies/${policyId}`, {
292
+ headers: { "Authorization": `Bearer ${apiKey}` }
293
+ });
294
+ const method = checkRes.ok ? "PUT" : "POST";
295
+ const url = checkRes.ok ? `${API_URL}/api/v1/policies/${policyId}` : `${API_URL}/api/v1/policies`;
296
+ const res = await fetch(url, {
297
+ method,
169
298
  headers: {
170
299
  "Authorization": `Bearer ${apiKey}`,
171
300
  "Content-Type": "application/json"
172
301
  },
173
302
  body: JSON.stringify({
174
- id: policy.id || "default",
303
+ id: policyId,
175
304
  name: policy.name || "Local Policy",
176
305
  description: policy.description || "Pushed from local file",
177
306
  version: policy.version || 1,
@@ -180,13 +309,15 @@ async function push(apiKey, file) {
180
309
  });
181
310
  if (!res.ok) {
182
311
  const body = await res.text().catch(() => "");
183
- log(`ERROR: Push failed (${res.status}): ${body}`);
312
+ log(red(`ERROR: Push failed (${res.status}): ${body}`));
184
313
  process.exit(1);
185
314
  }
186
315
  const data = await res.json();
187
- log(` Cloud version: ${data._version ?? "created"}`);
188
316
  log("");
189
- log("Done. Policy pushed from local file to dashboard.");
317
+ log(green(` Pushed to cloud: v${data._version ?? "created"}`));
318
+ log(` ${dim("Policy ID:")} ${policyId}`);
319
+ log(` ${dim("Method:")} ${method === "PUT" ? "Updated existing" : "Created new"}`);
320
+ log("");
190
321
  }
191
322
  async function main() {
192
323
  const { command, apiKey, file, policyId } = parseCliArgs();
@@ -194,14 +325,27 @@ async function main() {
194
325
  if (command === "pull") {
195
326
  await pull(apiKey, file, policyId);
196
327
  } else if (command === "push") {
197
- await push(apiKey, file);
328
+ await push(apiKey, file, policyId);
329
+ } else if (command === "list" || command === "ls") {
330
+ await list(apiKey, policyId);
198
331
  } else {
199
- log(`Unknown command: ${command}`);
200
- log("Usage: solongate-proxy pull|push [--file policy.json]");
332
+ log(red(`Unknown command: ${command}`));
333
+ log("");
334
+ log(bold("Usage:"));
335
+ log(" solongate-proxy list List all policies");
336
+ log(" solongate-proxy list --policy-id <ID> Show policy details");
337
+ log(" solongate-proxy pull --policy-id <ID> Pull policy to local file");
338
+ log(" solongate-proxy push --policy-id <ID> Push local file to cloud");
339
+ log("");
340
+ log(bold("Flags:"));
341
+ log(" --policy-id, --id <ID> Cloud policy ID (required for push)");
342
+ log(" --file, -f <path> Local file path (default: policy.json)");
343
+ log(" --api-key <key> API key (or set SOLONGATE_API_KEY)");
344
+ log("");
201
345
  process.exit(1);
202
346
  }
203
347
  } catch (err) {
204
- log(`ERROR: ${err instanceof Error ? err.message : String(err)}`);
348
+ log(red(`ERROR: ${err instanceof Error ? err.message : String(err)}`));
205
349
  process.exit(1);
206
350
  }
207
351
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.6.8",
3
+ "version": "0.7.0",
4
4
  "description": "MCP security proxy — protect any MCP server with customizable policies, path/command constraints, rate limiting, and audit logging. Zero code changes required.",
5
5
  "type": "module",
6
6
  "bin": {