@jurislm/coolify-plugin 4.0.28 → 4.1.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coolify-plugin",
3
- "version": "4.0.28",
3
+ "version": "4.1.0",
4
4
  "description": "Fallback Codex metadata for local stdio Coolify tools.",
5
5
  "author": {
6
6
  "name": "JurisLM",
package/dist/client.d.ts CHANGED
@@ -16,7 +16,8 @@ export interface BinaryEnvelope {
16
16
  }
17
17
  export type FetchLike = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
18
18
  type Operation = Pick<GeneratedOperation, "method" | "path" | "parameters">;
19
- export declare function redactSensitive<T>(value: T): T;
19
+ export declare function isEnvOperation(path: string): boolean;
20
+ export declare function redactSensitive<T>(value: T, tokens?: Array<string | undefined>, envResponse?: boolean): T;
20
21
  export declare class CoolifyClient {
21
22
  private readonly config;
22
23
  private readonly fetchImpl;
package/dist/client.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { isAlias, isScalar, parseDocument, stringify, visit } from "yaml";
2
2
  import { CoolifyApiError, redactErrorText } from "./errors.js";
3
3
  const sensitiveKey = /(^value$|real_?value|private_?key|api_?key|user_?key|webhook_?url|token|secret|password|authorization|cookie)/iu;
4
+ const envValueKey = /^(value|real_value)$/u;
5
+ const envPath = /\/envs(\/|$)/u;
4
6
  const composeKey = /^(docker_compose(_pr)?(_raw)?|configuration)$/u;
5
- const composeSecretName = /(password|passwd|secret|token|private|credential|api_?key|access_?key|authorization|cookie)/iu;
7
+ const composeSecretName = /(password|passwd|secret|token|private|credential|api_?key|access_?key|root_?user|authorization|cookie)/iu;
6
8
  const composeSecretSegment = /(^|[_.-])(key|pass|pwd|auth)($|[_.-])/iu;
7
9
  const interpolation = /^\$\{?[A-Za-z_][A-Za-z0-9_]*\}?$/u;
8
10
  const urlCredential = /([a-z][a-z0-9+.-]{0,31}:\/\/[^\s:/@]{0,256}):([^\s/?#]{1,256})@/giu;
@@ -26,12 +28,65 @@ function redactUrlCredentials(text) {
26
28
  function isQuerySecret(name) {
27
29
  return querySecretName.test(name) || name.toLowerCase().split(/[_.-]/u).some((segment) => querySecretSegments.has(segment));
28
30
  }
31
+ const commandKey = /^(command|entrypoint|test)$/u;
32
+ const secretFlagName = /(requirepass|masterauth)/iu;
33
+ const portValue = /^[\d.:[\]]+(\/(tcp|udp|sctp))?$/iu;
34
+ const envOption = /(^|\s)(-e|--env)(=|\s+)(?:"([A-Za-z_][\w.]{0,127})=([^"\n]{0,4096})"|'([A-Za-z_][\w.]{0,127})=([^'\n]{0,4096})'|([A-Za-z_][\w.]{0,127})=("[^"\n]{0,4096}"|'[^'\n]{0,4096}'|[^\s"']*))/gu;
35
+ const flagEquals = /(^|\s)(--?[A-Za-z][\w-]{0,63})=("[^"\n]{0,4096}"|'[^'\n]{0,4096}'|[^\s"']+)/gu;
36
+ const flagSpace = /(^|\s)(--?[A-Za-z][\w-]{0,63})(\s+)("[^"\n]{0,4096}"|'[^'\n]{0,4096}'|[^\s"'-][^\s"']*)/gu;
37
+ function unquote(value) {
38
+ return value.replace(/^(["'])([\s\S]*)\1$/u, "$2");
39
+ }
40
+ function quoteOf(value) {
41
+ return value.startsWith("\"") || value.startsWith("'") ? value[0] : "";
42
+ }
43
+ function isSecretFlag(flag) {
44
+ if (flag === "-p")
45
+ return true;
46
+ if (!flag.startsWith("--"))
47
+ return false;
48
+ const name = flag.slice(2).replace(/-/gu, "_");
49
+ return composeSecretName.test(name) || composeSecretSegment.test(name) || secretFlagName.test(name);
50
+ }
51
+ function isSecretFlagValue(flag, value) {
52
+ const bare = unquote(value);
53
+ if (bare === "" || bare === "[REDACTED]" || interpolation.test(bare) || flagValue.test(bare))
54
+ return false;
55
+ return flag !== "-p" || !portValue.test(bare);
56
+ }
57
+ function redactCommandLine(text) {
58
+ return text
59
+ .replace(envOption, (match, lead, flag, separator, doubleKey, _doubleValue, singleKey, _singleValue, key, value) => {
60
+ if (doubleKey !== undefined)
61
+ return isComposeSecret(doubleKey, _doubleValue) ? `${lead}${flag}${separator}"${doubleKey}=[REDACTED]"` : match;
62
+ if (singleKey !== undefined)
63
+ return isComposeSecret(singleKey, _singleValue) ? `${lead}${flag}${separator}'${singleKey}=[REDACTED]'` : match;
64
+ if (key === undefined || value === undefined)
65
+ return match;
66
+ const quote = quoteOf(value);
67
+ return isComposeSecret(key, unquote(value)) ? `${lead}${flag}${separator}${key}=${quote}[REDACTED]${quote}` : match;
68
+ })
69
+ .replace(flagEquals, (match, lead, flag, value) => isSecretFlag(flag) && isSecretFlagValue(flag, value) ? `${lead}${flag}=${quoteOf(value)}[REDACTED]${quoteOf(value)}` : match)
70
+ .replace(flagSpace, (match, lead, flag, space, value) => isSecretFlag(flag) && isSecretFlagValue(flag, value) ? `${lead}${flag}${space}${quoteOf(value)}[REDACTED]${quoteOf(value)}` : match);
71
+ }
72
+ function redactCommand(value) {
73
+ if (typeof value === "string")
74
+ return redactCommandLine(value);
75
+ if (!Array.isArray(value))
76
+ return value;
77
+ return value.map((token, index) => {
78
+ if (typeof token !== "string")
79
+ return token;
80
+ const flag = value[index - 1];
81
+ return typeof flag === "string" && !flag.includes("=") && isSecretFlag(flag) && isSecretFlagValue(flag, token) ? "[REDACTED]" : redactCommandLine(token);
82
+ });
83
+ }
29
84
  const composeAliasLimit = 100;
30
85
  function redactComposeValue(value) {
31
86
  if (Array.isArray(value))
32
87
  return value.map(redactComposeValue);
33
88
  if (value && typeof value === "object") {
34
- return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, isComposeSecret(key, child) ? "[REDACTED]" : redactComposeValue(child)]));
89
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, isComposeSecret(key, child) ? "[REDACTED]" : redactComposeValue(commandKey.test(key) ? redactCommand(child) : child)]));
35
90
  }
36
91
  if (typeof value !== "string")
37
92
  return value;
@@ -84,18 +139,34 @@ function redactComposeText(text) {
84
139
  const decoded = decodeBase64Text(text);
85
140
  return decoded === undefined ? redactComposeYaml(text) : Buffer.from(redactComposeYaml(decoded), "utf8").toString("base64");
86
141
  }
87
- export function redactSensitive(value) {
142
+ function withoutTokens(text, tokens) {
143
+ return tokens.reduce((redacted, token) => token ? redacted.replaceAll(token, "[REDACTED]") : redacted, text);
144
+ }
145
+ export function isEnvOperation(path) {
146
+ return envPath.test(path);
147
+ }
148
+ export function redactSensitive(value, tokens = [], envResponse = false) {
88
149
  if (typeof value === "string")
89
- return redactUrlCredentials(value);
150
+ return withoutTokens(redactUrlCredentials(value), tokens);
90
151
  if (Array.isArray(value))
91
- return value.map(redactSensitive);
152
+ return value.map((item) => redactSensitive(item, tokens, envResponse));
92
153
  if (!value || typeof value !== "object")
93
154
  return value;
94
155
  const binary = value.encoding === "base64" && typeof value.contentType === "string" && typeof value.value === "string";
95
- return Object.fromEntries(Object.entries(value).map(([key, child]) => [
96
- key,
97
- sensitiveKey.test(key) && !(binary && key === "value") ? "[REDACTED]" : composeKey.test(key) && typeof child === "string" ? redactComposeText(child) : redactSensitive(child),
98
- ]));
156
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, redactField(key, child, tokens, envResponse, binary)]));
157
+ }
158
+ function redactField(key, child, tokens, envResponse, binary) {
159
+ if (binary && key === "value")
160
+ return child;
161
+ if (envResponse && envValueKey.test(key))
162
+ return typeof child === "string" ? withoutTokens(child, tokens) : child;
163
+ if (sensitiveKey.test(key))
164
+ return "[REDACTED]";
165
+ if (composeKey.test(key) && typeof child === "string")
166
+ return withoutTokens(redactComposeText(child), tokens);
167
+ if (key === "custom_docker_run_options" && typeof child === "string")
168
+ return withoutTokens(redactUrlCredentials(redactCommandLine(child)), tokens);
169
+ return redactSensitive(child, tokens, envResponse);
99
170
  }
100
171
  function pathValue(value, name) {
101
172
  if (value === undefined || value === null)
@@ -193,7 +264,7 @@ export class CoolifyClient {
193
264
  catch {
194
265
  throw new Error("Coolify API returned an unreadable response body");
195
266
  }
196
- return redactSensitive({ data, status: response.status, request: { method: operation.method, path } });
267
+ return redactSensitive({ data, status: response.status, request: { method: operation.method, path } }, [token], isEnvOperation(operation.path));
197
268
  }
198
269
  }
199
270
  export { CoolifyApiError };
package/dist/server.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
- import { CoolifyClient, redactSensitive } from "./client.js";
3
+ import { CoolifyClient, isEnvOperation, redactSensitive } from "./client.js";
4
4
  import { formatToolError } from "./errors.js";
5
5
  import { registerCapabilities } from "./capabilities.js";
6
6
  import { operations } from "./generated/operations.js";
@@ -8,7 +8,7 @@ import { pluginVersion } from "./version.js";
8
8
  const outputRequestSchema = z.object({ method: z.string(), path: z.string() });
9
9
  export function createServer(config, fetchImpl) {
10
10
  const client = new CoolifyClient(config, fetchImpl);
11
- const server = new McpServer({ name: "coolify-plugin", version: pluginVersion }, { instructions: "Use read tools to identify exact Coolify resources before mutations. Never expose tokens or secret values." });
11
+ const server = new McpServer({ name: "coolify-plugin", version: pluginVersion }, { instructions: "Use read tools to identify exact Coolify resources before mutations. Never expose the Coolify access token. Env tools return actual environment variable values; report them only when the user asks." });
12
12
  for (const operation of operations) {
13
13
  server.registerTool(operation.name, {
14
14
  title: operation.title,
@@ -20,7 +20,7 @@ export function createServer(config, fetchImpl) {
20
20
  try {
21
21
  const envelope = await client.request(operation, input);
22
22
  const data = operation.responseSchema.parse(envelope.data);
23
- const structuredContent = redactSensitive({ data, status: envelope.status, request: envelope.request });
23
+ const structuredContent = redactSensitive({ data, status: envelope.status, request: envelope.request }, [config.token], isEnvOperation(operation.path));
24
24
  return { structuredContent, content: [{ type: "text", text: JSON.stringify(structuredContent) }] };
25
25
  }
26
26
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jurislm/coolify-plugin",
3
- "version": "4.0.28",
3
+ "version": "4.1.0",
4
4
  "description": "Portable local stdio MCP plugin for Coolify",
5
5
  "publishConfig": {
6
6
  "access": "public"
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "coolify-plugin",
4
- "version": "4.0.28",
4
+ "version": "4.1.0",
5
5
  "description": "Portable Coolify MCP plugin with local stdio tools generated from the official OpenAPI contract.",
6
6
  "author": {
7
7
  "name": "JurisLM",
@@ -5,4 +5,4 @@ description: Manage a configured Coolify instance through generated local MCP to
5
5
 
6
6
  # Coolify
7
7
 
8
- Use `coolify_*` tools only after resolving the target resource. Read before mutations and do not expose `COOLIFY_ACCESS_TOKEN` or returned secret values.
8
+ Use `coolify_*` tools only after resolving the target resource. Read before mutations and do not expose `COOLIFY_ACCESS_TOKEN`. Env tools return actual environment variable values, including `is_shown_once` ones; report them only when the user asks.