@yawlabs/caddy-mcp 2.2.0 → 2.3.1
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 +276 -246
- package/bin/caddy-mcp.mjs +334 -175
- package/dist/index.js +196 -54
- package/dist/server.js +196 -54
- package/dist/snapshots.d.ts +10 -0
- package/package.json +82 -82
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
6
6
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
7
|
|
|
8
8
|
// src/api.ts
|
|
9
|
+
import { request as httpRequest } from "http";
|
|
9
10
|
var DEFAULT_URL = "http://localhost:2019";
|
|
10
11
|
var TIMEOUT = 1e4;
|
|
11
12
|
var RETRY_BASE_MS = 100;
|
|
@@ -45,6 +46,15 @@ function invalidateRelated(path) {
|
|
|
45
46
|
function getBaseUrl() {
|
|
46
47
|
return (process.env.CADDY_ADMIN_URL || DEFAULT_URL).replace(/\/+$/, "");
|
|
47
48
|
}
|
|
49
|
+
function getUnixSocketPath() {
|
|
50
|
+
const raw = (process.env.CADDY_ADMIN_URL || "").trim();
|
|
51
|
+
if (!raw) return void 0;
|
|
52
|
+
const urlForm = /^unix:\/\/(\/.*)$/.exec(raw);
|
|
53
|
+
if (urlForm) return urlForm[1];
|
|
54
|
+
const caddyForm = /^unix\/(\/.*)$/.exec(raw);
|
|
55
|
+
if (caddyForm) return caddyForm[1];
|
|
56
|
+
return void 0;
|
|
57
|
+
}
|
|
48
58
|
var warnedRetryClamp = false;
|
|
49
59
|
function getMaxRetries() {
|
|
50
60
|
const raw = process.env.CADDY_MAX_RETRIES;
|
|
@@ -65,11 +75,12 @@ function getAdminOrigin() {
|
|
|
65
75
|
return void 0;
|
|
66
76
|
}
|
|
67
77
|
}
|
|
68
|
-
function getHeaders(contentType) {
|
|
78
|
+
function getHeaders(contentType, overUnixSocket = false) {
|
|
69
79
|
const headers = {};
|
|
70
80
|
if (contentType) headers["Content-Type"] = contentType;
|
|
71
81
|
const token = process.env.CADDY_API_TOKEN;
|
|
72
82
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
83
|
+
if (overUnixSocket) return headers;
|
|
73
84
|
const origin = getAdminOrigin();
|
|
74
85
|
if (origin) headers.Origin = origin;
|
|
75
86
|
return headers;
|
|
@@ -102,39 +113,85 @@ function isRetryableMethod(method, path) {
|
|
|
102
113
|
if (method !== "POST") return true;
|
|
103
114
|
return !path.startsWith("/config/") && !path.startsWith("/id/");
|
|
104
115
|
}
|
|
105
|
-
|
|
116
|
+
function getMalformedUnixUrl() {
|
|
117
|
+
const raw = (process.env.CADDY_ADMIN_URL || "").trim();
|
|
118
|
+
if (!raw || !/^unix[:/]/i.test(raw)) return void 0;
|
|
119
|
+
return getUnixSocketPath() === void 0 ? raw : void 0;
|
|
120
|
+
}
|
|
121
|
+
async function caddyRequest(method, path, body, contentType, timeout, rawStringBody = false) {
|
|
122
|
+
const malformed = getMalformedUnixUrl();
|
|
123
|
+
if (malformed) {
|
|
124
|
+
return {
|
|
125
|
+
ok: false,
|
|
126
|
+
status: 0,
|
|
127
|
+
error: `CADDY_ADMIN_URL="${malformed}" looks like a unix socket address but is not a recognized form. Use "unix:///absolute/path.sock" (URL form) or "unix//absolute/path.sock" (Caddy's own spelling). A single slash after "unix:", or a relative path, will not parse.`
|
|
128
|
+
};
|
|
129
|
+
}
|
|
106
130
|
const maxRetries = getMaxRetries();
|
|
107
131
|
let attempt = 0;
|
|
108
|
-
let res = await attemptRequest(method, path, body, contentType, timeout);
|
|
132
|
+
let res = await attemptRequest(method, path, body, contentType, timeout, rawStringBody);
|
|
109
133
|
while (isRetryableMethod(method, path) && isTransientFailure(res) && attempt < maxRetries) {
|
|
110
134
|
attempt++;
|
|
111
135
|
const backoff = Math.min(RETRY_BASE_MS * 2 ** (attempt - 1), RETRY_MAX_DELAY_MS);
|
|
112
136
|
const delay = backoff + Math.random() * RETRY_MAX_JITTER_MS;
|
|
113
137
|
await sleep(delay);
|
|
114
|
-
res = await attemptRequest(method, path, body, contentType, timeout);
|
|
138
|
+
res = await attemptRequest(method, path, body, contentType, timeout, rawStringBody);
|
|
115
139
|
}
|
|
116
140
|
return res;
|
|
117
141
|
}
|
|
118
|
-
|
|
142
|
+
function sendViaUnixSocket(socketPath, path, method, headers, body, timeoutMs) {
|
|
143
|
+
return new Promise((resolve, reject) => {
|
|
144
|
+
const req = httpRequest({ socketPath, path, method, headers, signal: AbortSignal.timeout(timeoutMs) }, (res) => {
|
|
145
|
+
const chunks = [];
|
|
146
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
147
|
+
res.on("error", reject);
|
|
148
|
+
res.on("end", () => {
|
|
149
|
+
const status = res.statusCode ?? 0;
|
|
150
|
+
const etag = res.headers.etag;
|
|
151
|
+
resolve({
|
|
152
|
+
ok: status >= 200 && status < 300,
|
|
153
|
+
status,
|
|
154
|
+
text: Buffer.concat(chunks).toString("utf8"),
|
|
155
|
+
etag: typeof etag === "string" ? etag : void 0
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
req.on("error", reject);
|
|
160
|
+
if (body !== void 0) req.write(body);
|
|
161
|
+
req.end();
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
async function sendViaFetch(url, method, headers, body, timeoutMs) {
|
|
165
|
+
const res = await fetch(url, {
|
|
166
|
+
method,
|
|
167
|
+
headers,
|
|
168
|
+
body,
|
|
169
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
170
|
+
});
|
|
171
|
+
return {
|
|
172
|
+
ok: res.ok,
|
|
173
|
+
status: res.status,
|
|
174
|
+
text: await res.text(),
|
|
175
|
+
etag: res.headers.get("ETag") || void 0
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
async function attemptRequest(method, path, body, contentType, timeout, rawStringBody = false) {
|
|
179
|
+
const socketPath = getUnixSocketPath();
|
|
119
180
|
const url = `${getBaseUrl()}${path}`;
|
|
120
181
|
const effectiveTimeout = timeout ?? getRequestTimeout();
|
|
121
182
|
try {
|
|
122
183
|
const hasBody = body !== void 0;
|
|
123
|
-
const headers = getHeaders(hasBody ? contentType || "application/json" : void 0);
|
|
184
|
+
const headers = getHeaders(hasBody ? contentType || "application/json" : void 0, socketPath !== void 0);
|
|
124
185
|
const isConfigPath = path.startsWith("/config/") || path.startsWith("/id/");
|
|
125
186
|
const isWrite = method !== "GET";
|
|
126
187
|
if (isWrite && isConfigPath) {
|
|
127
188
|
const cachedEtag = etagCache.get(path);
|
|
128
189
|
if (cachedEtag) headers["If-Match"] = cachedEtag;
|
|
129
190
|
}
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
signal: AbortSignal.timeout(effectiveTimeout)
|
|
135
|
-
});
|
|
136
|
-
const text = await res.text();
|
|
137
|
-
const etag = res.headers.get("ETag") || void 0;
|
|
191
|
+
const serializedBody = hasBody ? rawStringBody && typeof body === "string" ? body : JSON.stringify(body) : void 0;
|
|
192
|
+
const res = socketPath ? await sendViaUnixSocket(socketPath, path, method, headers, serializedBody, effectiveTimeout) : await sendViaFetch(url, method, headers, serializedBody, effectiveTimeout);
|
|
193
|
+
const text = res.text;
|
|
194
|
+
const etag = res.etag;
|
|
138
195
|
if (method === "GET" && etag && isConfigPath) {
|
|
139
196
|
setEtag(path, etag);
|
|
140
197
|
}
|
|
@@ -158,10 +215,11 @@ async function attemptRequest(method, path, body, contentType, timeout) {
|
|
|
158
215
|
return { ok: false, status: res.status, error: `HTTP ${res.status}${hint}` };
|
|
159
216
|
}
|
|
160
217
|
if (res.status === 403 && /origin/i.test(text)) {
|
|
218
|
+
const hint = socketPath ? `Over a unix socket caddy-mcp deliberately sends no Origin header, because Caddy builds no default origin allowlist for a unix listener (sending one would fail against an empty list). Reaching here means admin.enforce_origin is enabled -- disable it, or move the admin endpoint to a TCP address.` : `Set CADDY_ADMIN_URL to the exact origin Caddy allows (default http://localhost:2019), or add this origin to the admin.origins list in Caddy's config.`;
|
|
161
219
|
return {
|
|
162
220
|
ok: false,
|
|
163
221
|
status: 403,
|
|
164
|
-
error: `${text.trim()} -- Caddy's admin API rejected this client's Origin.
|
|
222
|
+
error: `${text.trim()} -- Caddy's admin API rejected this client's Origin. ${hint}`
|
|
165
223
|
};
|
|
166
224
|
}
|
|
167
225
|
return { ok: false, status: res.status, error: text };
|
|
@@ -174,17 +232,25 @@ async function attemptRequest(method, path, body, contentType, timeout) {
|
|
|
174
232
|
}
|
|
175
233
|
} catch (err) {
|
|
176
234
|
const msg = err instanceof Error ? err.message : String(err);
|
|
235
|
+
if (socketPath && msg.includes("ENOENT")) {
|
|
236
|
+
return {
|
|
237
|
+
ok: false,
|
|
238
|
+
status: 0,
|
|
239
|
+
error: `No socket at ${socketPath} \u2014 check the path in CADDY_ADMIN_URL and that Caddy's admin endpoint is configured to listen on it.`
|
|
240
|
+
};
|
|
241
|
+
}
|
|
177
242
|
if (msg.includes("ECONNREFUSED") || msg.includes("fetch failed")) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
243
|
+
let target = socketPath ?? getBaseUrl();
|
|
244
|
+
if (!socketPath) {
|
|
245
|
+
try {
|
|
246
|
+
target = new URL(target).origin;
|
|
247
|
+
} catch {
|
|
248
|
+
}
|
|
183
249
|
}
|
|
184
250
|
return {
|
|
185
251
|
ok: false,
|
|
186
252
|
status: 0,
|
|
187
|
-
error: `Cannot connect to Caddy admin API at ${
|
|
253
|
+
error: `Cannot connect to Caddy admin API at ${target} \u2014 is Caddy running?`
|
|
188
254
|
};
|
|
189
255
|
}
|
|
190
256
|
if (msg.includes("abort") || msg.includes("timeout")) {
|
|
@@ -242,12 +308,12 @@ function getLoadTimeout() {
|
|
|
242
308
|
return floored;
|
|
243
309
|
}
|
|
244
310
|
async function loadConfig(config, contentType) {
|
|
245
|
-
const res = await caddyRequest("POST", "/load", config, contentType, getLoadTimeout());
|
|
311
|
+
const res = await caddyRequest("POST", "/load", config, contentType, getLoadTimeout(), true);
|
|
246
312
|
if (res.ok) etagCache.clear();
|
|
247
313
|
return res;
|
|
248
314
|
}
|
|
249
315
|
function adapt(config, adapter = "caddyfile") {
|
|
250
|
-
return caddyRequest("POST", "/adapt", config, `text/${adapter}
|
|
316
|
+
return caddyRequest("POST", "/adapt", config, `text/${adapter}`, void 0, true);
|
|
251
317
|
}
|
|
252
318
|
function stop() {
|
|
253
319
|
return caddyRequest("POST", "/stop");
|
|
@@ -594,25 +660,83 @@ ${warnLines.join("\n")}` });
|
|
|
594
660
|
import { z as z3 } from "zod";
|
|
595
661
|
|
|
596
662
|
// src/snapshots.ts
|
|
663
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
664
|
+
import { join } from "path";
|
|
597
665
|
var MAX_SNAPSHOTS = 10;
|
|
598
666
|
var store = [];
|
|
667
|
+
function isSnapshotableConfig(data) {
|
|
668
|
+
return data !== null && typeof data === "object" && !Array.isArray(data);
|
|
669
|
+
}
|
|
670
|
+
var SNAPSHOT_FILE_RE = /^snapshot-(\d+)-([\w-]+)\.json$/;
|
|
671
|
+
function snapshotDir() {
|
|
672
|
+
const dir = process.env.CADDY_MCP_SNAPSHOT_DIR?.trim();
|
|
673
|
+
return dir ? dir : void 0;
|
|
674
|
+
}
|
|
675
|
+
var hydrated = false;
|
|
676
|
+
function hydrate() {
|
|
677
|
+
if (hydrated) return;
|
|
678
|
+
hydrated = true;
|
|
679
|
+
const dir = snapshotDir();
|
|
680
|
+
if (!dir || !existsSync(dir)) return;
|
|
681
|
+
try {
|
|
682
|
+
const names = readdirSync(dir).filter((n) => SNAPSHOT_FILE_RE.test(n)).sort().reverse().slice(0, MAX_SNAPSHOTS);
|
|
683
|
+
const loaded = [];
|
|
684
|
+
for (const name of names) {
|
|
685
|
+
const m = SNAPSHOT_FILE_RE.exec(name);
|
|
686
|
+
if (!m) continue;
|
|
687
|
+
try {
|
|
688
|
+
const config = JSON.parse(readFileSync(join(dir, name), "utf-8"));
|
|
689
|
+
if (!isSnapshotableConfig(config)) continue;
|
|
690
|
+
loaded.push({ config, timestamp: Number(m[1]), trigger: m[2] });
|
|
691
|
+
} catch {
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
loaded.sort((a, b) => b.timestamp - a.timestamp);
|
|
695
|
+
store.push(...loaded);
|
|
696
|
+
} catch {
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
function persist(snap) {
|
|
700
|
+
const dir = snapshotDir();
|
|
701
|
+
if (!dir) return;
|
|
702
|
+
try {
|
|
703
|
+
mkdirSync(dir, { recursive: true });
|
|
704
|
+
const safeTrigger = snap.trigger.replace(/[^\w-]/g, "_");
|
|
705
|
+
writeFileSync(
|
|
706
|
+
join(dir, `snapshot-${snap.timestamp}-${safeTrigger}.json`),
|
|
707
|
+
JSON.stringify(snap.config, null, 2),
|
|
708
|
+
"utf-8"
|
|
709
|
+
);
|
|
710
|
+
const files = readdirSync(dir).filter((n) => SNAPSHOT_FILE_RE.test(n)).sort().reverse();
|
|
711
|
+
for (const stale of files.slice(MAX_SNAPSHOTS)) {
|
|
712
|
+
rmSync(join(dir, stale), { force: true });
|
|
713
|
+
}
|
|
714
|
+
} catch {
|
|
715
|
+
}
|
|
716
|
+
}
|
|
599
717
|
function saveSnapshot(config, trigger) {
|
|
600
|
-
|
|
718
|
+
hydrate();
|
|
719
|
+
let timestamp = Date.now();
|
|
720
|
+
if (store.length > 0 && timestamp <= store[0].timestamp) {
|
|
721
|
+
timestamp = store[0].timestamp + 1;
|
|
722
|
+
}
|
|
723
|
+
const snap = { config, timestamp, trigger };
|
|
724
|
+
store.unshift(snap);
|
|
601
725
|
if (store.length > MAX_SNAPSHOTS) {
|
|
602
726
|
store.length = MAX_SNAPSHOTS;
|
|
603
727
|
}
|
|
728
|
+
persist(snap);
|
|
604
729
|
}
|
|
605
730
|
function listSnapshots() {
|
|
731
|
+
hydrate();
|
|
606
732
|
return store;
|
|
607
733
|
}
|
|
608
734
|
function getSnapshot(index) {
|
|
735
|
+
hydrate();
|
|
609
736
|
return store[index];
|
|
610
737
|
}
|
|
611
738
|
|
|
612
739
|
// src/tools/config.ts
|
|
613
|
-
function isSnapshotableConfig(data) {
|
|
614
|
-
return data !== null && typeof data === "object" && !Array.isArray(data);
|
|
615
|
-
}
|
|
616
740
|
function registerConfigTools(server) {
|
|
617
741
|
server.tool(
|
|
618
742
|
"caddy_config_get",
|
|
@@ -692,7 +816,7 @@ function registerConfigTools(server) {
|
|
|
692
816
|
);
|
|
693
817
|
server.tool(
|
|
694
818
|
"caddy_revert",
|
|
695
|
-
"Manage config snapshots for rollback. Snapshots are auto-captured before caddy_load
|
|
819
|
+
"Manage config snapshots for rollback. Snapshots are auto-captured before caddy_load (last 10). By default they live in memory only and are LOST when this server restarts -- set CADDY_MCP_SNAPSHOT_DIR to a writable directory to persist them across restarts (they contain full Caddy configs, so pick the location deliberately). Actions: 'list' shows snapshots with timestamps, 'save' manually captures the current config, 'apply' restores a snapshot (requires confirm=true).",
|
|
696
820
|
{
|
|
697
821
|
action: z3.enum(["list", "save", "apply"]).describe("Action to perform"),
|
|
698
822
|
index: z3.number().int().nonnegative().optional().default(0).describe("Snapshot index for 'apply' (0 = most recent, default)"),
|
|
@@ -1235,6 +1359,7 @@ function buildTlsConfig(fields) {
|
|
|
1235
1359
|
const issuer = { module: "acme" };
|
|
1236
1360
|
if (fields.email) issuer.email = fields.email;
|
|
1237
1361
|
if (fields.ca) issuer.ca = fields.ca;
|
|
1362
|
+
if (fields.profile) issuer.profile = fields.profile;
|
|
1238
1363
|
return {
|
|
1239
1364
|
automation: {
|
|
1240
1365
|
policies: [{ issuers: [issuer] }]
|
|
@@ -1268,6 +1393,7 @@ function mergeIssuerFields(existing, fields) {
|
|
|
1268
1393
|
const issuer = automation.policies[0].issuers[0];
|
|
1269
1394
|
if (fields.email !== void 0) issuer.email = fields.email;
|
|
1270
1395
|
if (fields.ca !== void 0) issuer.ca = fields.ca;
|
|
1396
|
+
if (fields.profile !== void 0) issuer.profile = fields.profile;
|
|
1271
1397
|
return merged;
|
|
1272
1398
|
}
|
|
1273
1399
|
function validateIssuerShape(tls) {
|
|
@@ -1338,47 +1464,63 @@ async function safeFallback(label, patchRes, fields) {
|
|
|
1338
1464
|
};
|
|
1339
1465
|
}
|
|
1340
1466
|
const merged = mergeIssuerFields(getRes.data, fields);
|
|
1341
|
-
const
|
|
1342
|
-
if (
|
|
1343
|
-
return { kind: "tool-error", result: bothErrors(label, patchRes,
|
|
1467
|
+
const mergeRes = await configPatch("apps/tls", merged);
|
|
1468
|
+
if (mergeRes.ok) return { kind: "ok" };
|
|
1469
|
+
return { kind: "tool-error", result: bothErrors(label, patchRes, mergeRes, "PATCH apps/tls") };
|
|
1470
|
+
}
|
|
1471
|
+
async function setIssuerField(field, value, label) {
|
|
1472
|
+
const patchRes = await configPatch(`apps/tls/automation/policies/0/issuers/0/${field}`, value);
|
|
1473
|
+
const ok = { content: [{ type: "text", text: `${label} set to: ${value}` }] };
|
|
1474
|
+
if (patchRes.ok) return ok;
|
|
1475
|
+
const outcome = await safeFallback(label, patchRes, { [field]: value });
|
|
1476
|
+
return outcome.kind === "ok" ? ok : outcome.result;
|
|
1477
|
+
}
|
|
1478
|
+
function missingArgError(text) {
|
|
1479
|
+
return { isError: true, content: [{ type: "text", text: `Error: ${text}` }] };
|
|
1344
1480
|
}
|
|
1345
1481
|
function registerTlsTools(server) {
|
|
1346
1482
|
server.tool(
|
|
1347
1483
|
"caddy_tls",
|
|
1348
|
-
"Get or configure TLS/HTTPS settings. Actions: 'status' shows current TLS config, 'set_email' sets the ACME email, 'set_acme_ca' sets the ACME CA URL. Works on both fresh and existing Caddy instances.",
|
|
1484
|
+
"Get or configure TLS/HTTPS settings. Actions: 'status' shows current TLS config, 'set_email' sets the ACME email, 'set_acme_ca' sets the ACME CA URL, 'set_acme_profile' sets the ACME profile (Caddy 2.10+), 'ech_status' reads the Encrypted ClientHello config (Caddy 2.10+, read-only here). Works on both fresh and existing Caddy instances. Writes target policies[0].issuers[0] only -- on a multi-policy TLS config, edit the intended policy with caddy_config_set instead.",
|
|
1349
1485
|
{
|
|
1350
|
-
action: z5.enum(["status", "set_email", "set_acme_ca"]).describe("Action to perform"),
|
|
1486
|
+
action: z5.enum(["status", "set_email", "set_acme_ca", "set_acme_profile", "ech_status"]).describe("Action to perform"),
|
|
1351
1487
|
email: z5.string().optional().describe("ACME email address (for 'set_email' action)"),
|
|
1352
|
-
ca: z5.string().optional().describe("ACME CA URL (for 'set_acme_ca' action)")
|
|
1488
|
+
ca: z5.string().optional().describe("ACME CA URL (for 'set_acme_ca' action)"),
|
|
1489
|
+
profile: z5.string().optional().describe(
|
|
1490
|
+
"ACME profile name (for 'set_acme_profile'). Requires Caddy 2.10+ and a CA that offers profiles; Let's Encrypt uses 'shortlived' for 6-day certificates. Valid names are defined by the CA, not by Caddy."
|
|
1491
|
+
)
|
|
1353
1492
|
},
|
|
1354
1493
|
{ readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1355
|
-
async ({ action, email, ca }) => {
|
|
1494
|
+
async ({ action, email, ca, profile }) => {
|
|
1356
1495
|
if (action === "status") {
|
|
1357
1496
|
return formatResult(await configGet("apps/tls"));
|
|
1358
1497
|
}
|
|
1359
|
-
if (action === "
|
|
1360
|
-
|
|
1498
|
+
if (action === "ech_status") {
|
|
1499
|
+
const echRes = await configGet("apps/tls/ech");
|
|
1500
|
+
const absent = !echRes.ok && echRes.status === 404 || echRes.ok && (echRes.data === void 0 || echRes.data === null);
|
|
1501
|
+
if (absent) {
|
|
1361
1502
|
return {
|
|
1362
|
-
|
|
1363
|
-
|
|
1503
|
+
content: [
|
|
1504
|
+
{
|
|
1505
|
+
type: "text",
|
|
1506
|
+
text: "ECH (Encrypted ClientHello) is not configured on this instance. Requires Caddy 2.10+; enable it by applying a config with apps/tls/ech via caddy_load."
|
|
1507
|
+
}
|
|
1508
|
+
]
|
|
1364
1509
|
};
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
return
|
|
1510
|
+
}
|
|
1511
|
+
return formatResult(echRes);
|
|
1512
|
+
}
|
|
1513
|
+
if (action === "set_email") {
|
|
1514
|
+
if (!email) return missingArgError("email is required for set_email action");
|
|
1515
|
+
return setIssuerField("email", email, "ACME email");
|
|
1370
1516
|
}
|
|
1371
1517
|
if (action === "set_acme_ca") {
|
|
1372
|
-
if (!ca)
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
if (patchRes.ok) return { content: [{ type: "text", text: `ACME CA set to: ${ca}` }] };
|
|
1379
|
-
const outcome = await safeFallback("ACME CA", patchRes, { ca });
|
|
1380
|
-
if (outcome.kind === "ok") return { content: [{ type: "text", text: `ACME CA set to: ${ca}` }] };
|
|
1381
|
-
return outcome.result;
|
|
1518
|
+
if (!ca) return missingArgError("ca is required for set_acme_ca action");
|
|
1519
|
+
return setIssuerField("ca", ca, "ACME CA");
|
|
1520
|
+
}
|
|
1521
|
+
if (action === "set_acme_profile") {
|
|
1522
|
+
if (!profile) return missingArgError("profile is required for set_acme_profile action");
|
|
1523
|
+
return setIssuerField("profile", profile, "ACME profile");
|
|
1382
1524
|
}
|
|
1383
1525
|
return { isError: true, content: [{ type: "text", text: `Unknown action: ${action}` }] };
|
|
1384
1526
|
}
|