@iann29/synapse 1.8.7 → 1.8.9
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/lib/commands/_help.js +1 -0
- package/lib/commands/https-doctor.js +73 -0
- package/lib/commands/https-migrate.js +168 -0
- package/lib/commands/https-remove.js +103 -0
- package/lib/commands/https-setup.js +263 -0
- package/lib/commands/https-status.js +154 -0
- package/lib/doctor/checks.js +249 -1
- package/lib/doctor/renderer.js +19 -2
- package/lib/doctor/runner.js +10 -1
- package/lib/https/detect.js +603 -0
- package/lib/https/executor.js +102 -0
- package/lib/https/hosts.js +356 -0
- package/lib/https/mkcert.js +131 -0
- package/lib/https/nextjs.js +91 -0
- package/lib/https/planner.js +412 -0
- package/package.json +1 -1
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
// The planner turns a Detection object (see detect.js) into a list
|
|
2
|
+
// of Steps the executor can run. Steps are declarative records the
|
|
3
|
+
// preview UI can also print without executing — enabling --dry-run
|
|
4
|
+
// and -v shows for free.
|
|
5
|
+
//
|
|
6
|
+
// Each Step is shaped:
|
|
7
|
+
// {
|
|
8
|
+
// id: string, // stable; logs key off this
|
|
9
|
+
// title: string, // human-readable one-liner
|
|
10
|
+
// kind: "exec" | "skip" | "blocker" | "warn",
|
|
11
|
+
// reason: string, // why we chose this path
|
|
12
|
+
// run?: (ctx) => Promise<{ changed, ... }>
|
|
13
|
+
// skipReason?: string, // why we're NOT running
|
|
14
|
+
// blocker?: string, // why the whole plan is blocked
|
|
15
|
+
// }
|
|
16
|
+
//
|
|
17
|
+
// `kind: blocker` halts the plan before execution (e.g. mkcert
|
|
18
|
+
// missing — we can't proceed). The executor refuses to run if any
|
|
19
|
+
// blocker is present.
|
|
20
|
+
|
|
21
|
+
const fs = require("node:fs");
|
|
22
|
+
const path = require("node:path");
|
|
23
|
+
const detectMod = require("./detect");
|
|
24
|
+
const mkcertMod = require("./mkcert");
|
|
25
|
+
const hostsMod = require("./hosts");
|
|
26
|
+
const nextjsMod = require("./nextjs");
|
|
27
|
+
|
|
28
|
+
// Maps a detected platform + pkg manager combo to the canonical
|
|
29
|
+
// install instructions for mkcert. We don't auto-install (mutating
|
|
30
|
+
// pkg managers from a CLI feels wrong); we just print the command.
|
|
31
|
+
function mkcertInstallHint(detection) {
|
|
32
|
+
const { id } = detection.platform;
|
|
33
|
+
const managers = detection.packageManagers;
|
|
34
|
+
if (id === "linux" || id === "wsl") {
|
|
35
|
+
if (managers.includes("pacman")) return "sudo pacman -S mkcert nss";
|
|
36
|
+
if (managers.includes("apt-get")) return "sudo apt-get install mkcert libnss3-tools";
|
|
37
|
+
if (managers.includes("dnf")) return "sudo dnf install mkcert nss-tools";
|
|
38
|
+
if (managers.includes("yum")) return "sudo yum install mkcert nss-tools";
|
|
39
|
+
if (managers.includes("zypper")) return "sudo zypper install mkcert mozilla-nss-tools";
|
|
40
|
+
if (managers.includes("apk")) return "sudo apk add mkcert nss-tools";
|
|
41
|
+
return "Install mkcert from https://github.com/FiloSottile/mkcert/releases";
|
|
42
|
+
}
|
|
43
|
+
if (id === "macos") {
|
|
44
|
+
if (managers.includes("brew")) return "brew install mkcert nss";
|
|
45
|
+
return "Install Homebrew (https://brew.sh) then run: brew install mkcert nss";
|
|
46
|
+
}
|
|
47
|
+
if (id === "windows") {
|
|
48
|
+
if (managers.includes("scoop")) return "scoop bucket add extras && scoop install mkcert";
|
|
49
|
+
if (managers.includes("choco")) return "choco install mkcert";
|
|
50
|
+
if (managers.includes("winget")) return "winget install FiloSottile.mkcert";
|
|
51
|
+
return "Install mkcert: https://github.com/FiloSottile/mkcert/releases (download the .exe to a PATH directory)";
|
|
52
|
+
}
|
|
53
|
+
return "Install mkcert from https://github.com/FiloSottile/mkcert/releases";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Computes the canonical dev:https script for a domain + cert path
|
|
57
|
+
// pair. Always uses POSIX-style forward slashes — Next.js accepts
|
|
58
|
+
// them on Windows too, and they make the script identical across
|
|
59
|
+
// platforms (so a project shared between Linux + Windows partners
|
|
60
|
+
// reads the same).
|
|
61
|
+
function devHttpsCommandFor(detection, domain) {
|
|
62
|
+
const { certPath, keyPath } = detection.existingCerts || detectMod.certFilesFor(domain);
|
|
63
|
+
// Convert to posix path style; on Linux/macOS this is a no-op.
|
|
64
|
+
const toPosix = (p) => p.replace(/\\/g, "/");
|
|
65
|
+
return detectMod.buildDevHttpsScript(domain, toPosix(certPath), toPosix(keyPath));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// The main entry. Given a detection + options, returns an array of
|
|
69
|
+
// Steps. Caller decides whether to print, prompt, then execute.
|
|
70
|
+
function plan(detection, {
|
|
71
|
+
force = false,
|
|
72
|
+
skipHosts = false,
|
|
73
|
+
skipScript = false,
|
|
74
|
+
} = {}) {
|
|
75
|
+
const steps = [];
|
|
76
|
+
|
|
77
|
+
// ---- Pre-validate (blockers go first) ------------------------------
|
|
78
|
+
if (!detection.validation.ok) {
|
|
79
|
+
steps.push({
|
|
80
|
+
id: "validate",
|
|
81
|
+
title: "Validate domain",
|
|
82
|
+
kind: "blocker",
|
|
83
|
+
reason: detection.validation.reason,
|
|
84
|
+
blocker: detection.validation.reason,
|
|
85
|
+
});
|
|
86
|
+
return steps;
|
|
87
|
+
}
|
|
88
|
+
if (!detection.mkcert.present) {
|
|
89
|
+
const hint = mkcertInstallHint(detection);
|
|
90
|
+
steps.push({
|
|
91
|
+
id: "mkcert-missing",
|
|
92
|
+
title: "mkcert installed",
|
|
93
|
+
kind: "blocker",
|
|
94
|
+
reason: "mkcert not found in PATH",
|
|
95
|
+
blocker: `mkcert is required but not installed. Run:\n ${hint}\nThen re-run \`synapse https setup\`.`,
|
|
96
|
+
});
|
|
97
|
+
return steps;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---- Step: mkcert -install (CA trust) ------------------------------
|
|
101
|
+
if (!detection.caTrusted) {
|
|
102
|
+
steps.push({
|
|
103
|
+
id: "ca-install",
|
|
104
|
+
title: "Install local CA into system + browser trust stores (mkcert -install)",
|
|
105
|
+
kind: "exec",
|
|
106
|
+
reason: "rootCA.pem not present in mkcert's CAROOT — first-time setup",
|
|
107
|
+
async run() {
|
|
108
|
+
return mkcertMod.installCA();
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
} else {
|
|
112
|
+
steps.push({
|
|
113
|
+
id: "ca-install",
|
|
114
|
+
title: "Install local CA",
|
|
115
|
+
kind: "skip",
|
|
116
|
+
reason: "CA already trusted (rootCA.pem present)",
|
|
117
|
+
skipReason: "idempotent",
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
// Firefox/NSS reminder — not a blocker, just a warning.
|
|
121
|
+
if (!detection.hasCertutil && (detection.platform.id === "linux" || detection.platform.id === "wsl")) {
|
|
122
|
+
steps.push({
|
|
123
|
+
id: "nss-warn",
|
|
124
|
+
title: "Firefox trust (NSS)",
|
|
125
|
+
kind: "warn",
|
|
126
|
+
reason:
|
|
127
|
+
"certutil not found — Firefox uses a separate NSS DB. Without it, Chrome/Edge will trust the cert but Firefox won't.",
|
|
128
|
+
skipReason:
|
|
129
|
+
"Install NSS tools to fix Firefox trust on this machine (apt: libnss3-tools, pacman: nss, dnf: nss-tools), then re-run `mkcert -install`.",
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---- Step: ensure cert dir exists ----------------------------------
|
|
134
|
+
const { dir: certDir, certPath, keyPath } = detection.existingCerts || detectMod.certFilesFor(detection.domain);
|
|
135
|
+
if (!fs.existsSync(certDir)) {
|
|
136
|
+
steps.push({
|
|
137
|
+
id: "cert-dir",
|
|
138
|
+
title: `Create ${certDir}`,
|
|
139
|
+
kind: "exec",
|
|
140
|
+
reason: "cert directory missing",
|
|
141
|
+
async run() {
|
|
142
|
+
fs.mkdirSync(certDir, { recursive: true, mode: 0o700 });
|
|
143
|
+
return { changed: true };
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
} else {
|
|
147
|
+
steps.push({
|
|
148
|
+
id: "cert-dir",
|
|
149
|
+
title: `Cert directory ${certDir}`,
|
|
150
|
+
kind: "skip",
|
|
151
|
+
reason: "directory already exists",
|
|
152
|
+
skipReason: "idempotent",
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ---- Step: generate certificate ------------------------------------
|
|
157
|
+
const certPresent =
|
|
158
|
+
detection.existingCerts && detection.existingCerts.present && !force;
|
|
159
|
+
if (certPresent) {
|
|
160
|
+
steps.push({
|
|
161
|
+
id: "cert-generate",
|
|
162
|
+
title: `Generate cert pair for ${detection.domain}`,
|
|
163
|
+
kind: "skip",
|
|
164
|
+
reason: "cert + key already exist in canonical location",
|
|
165
|
+
skipReason:
|
|
166
|
+
"use `synapse https setup <domain> --force` to regenerate (invalidates the current cert).",
|
|
167
|
+
});
|
|
168
|
+
} else {
|
|
169
|
+
steps.push({
|
|
170
|
+
id: "cert-generate",
|
|
171
|
+
title: `Generate cert pair for ${detection.domain} (mkcert)`,
|
|
172
|
+
kind: "exec",
|
|
173
|
+
reason: force ? "--force: regenerating" : "no cert present in canonical location",
|
|
174
|
+
async run() {
|
|
175
|
+
return mkcertMod.generateCert({
|
|
176
|
+
domain: detection.domain,
|
|
177
|
+
certPath,
|
|
178
|
+
keyPath,
|
|
179
|
+
});
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ---- Step: hosts file ----------------------------------------------
|
|
185
|
+
if (skipHosts) {
|
|
186
|
+
steps.push({
|
|
187
|
+
id: "hosts",
|
|
188
|
+
title: "Hosts file entry",
|
|
189
|
+
kind: "skip",
|
|
190
|
+
reason: "--skip-hosts requested",
|
|
191
|
+
skipReason: "operator opted out",
|
|
192
|
+
});
|
|
193
|
+
} else if (
|
|
194
|
+
detection.resolution.resolvesToLoopback &&
|
|
195
|
+
detection.resolution.source === "dns"
|
|
196
|
+
) {
|
|
197
|
+
steps.push({
|
|
198
|
+
id: "hosts",
|
|
199
|
+
title: "Hosts file entry",
|
|
200
|
+
kind: "skip",
|
|
201
|
+
reason: `${detection.domain} already resolves to 127.0.0.1 via public DNS`,
|
|
202
|
+
skipReason:
|
|
203
|
+
"no hosts edit needed — DNS A record points at loopback (any machine on any OS resolves correctly)",
|
|
204
|
+
});
|
|
205
|
+
} else if (detection.hosts.matches.some((m) => m.address === "127.0.0.1")) {
|
|
206
|
+
steps.push({
|
|
207
|
+
id: "hosts",
|
|
208
|
+
title: "Hosts file entry",
|
|
209
|
+
kind: "skip",
|
|
210
|
+
reason: `${detection.hosts.path} already maps ${detection.domain} to 127.0.0.1`,
|
|
211
|
+
skipReason: "entry present",
|
|
212
|
+
});
|
|
213
|
+
} else {
|
|
214
|
+
const needsElevation = !detection.hosts.writable;
|
|
215
|
+
steps.push({
|
|
216
|
+
id: "hosts",
|
|
217
|
+
title: `Add "127.0.0.1 ${detection.domain}" to ${detection.hosts.path}`,
|
|
218
|
+
kind: "exec",
|
|
219
|
+
reason: needsElevation
|
|
220
|
+
? `${detection.hosts.path} requires elevation (sudo/Administrator)`
|
|
221
|
+
: "writable directly",
|
|
222
|
+
async run() {
|
|
223
|
+
return hostsMod.addEntry(detection.hosts.path, detection.domain);
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ---- Step: WSL Windows hosts companion -----------------------------
|
|
229
|
+
if (
|
|
230
|
+
detection.platform.id === "wsl" &&
|
|
231
|
+
detection.wslWindowsHosts &&
|
|
232
|
+
detection.wslWindowsHosts.exists &&
|
|
233
|
+
!detection.wslWindowsHosts.matches.some((m) => m.address === "127.0.0.1") &&
|
|
234
|
+
!detection.resolution.resolvesToLoopback
|
|
235
|
+
) {
|
|
236
|
+
steps.push({
|
|
237
|
+
id: "hosts-wsl",
|
|
238
|
+
title: `Add Windows-side hosts entry (${detection.wslWindowsHosts.path})`,
|
|
239
|
+
kind: "warn",
|
|
240
|
+
reason:
|
|
241
|
+
"WSL Linux hosts file does NOT affect Windows browsers. Edit the Windows hosts file from a Windows Administrator shell:",
|
|
242
|
+
skipReason: `Open PowerShell as Administrator and append: 127.0.0.1\t${detection.domain} to ${detection.wslWindowsHosts.path}`,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ---- Step: package.json script -------------------------------------
|
|
247
|
+
if (skipScript) {
|
|
248
|
+
steps.push({
|
|
249
|
+
id: "script",
|
|
250
|
+
title: "package.json dev:https",
|
|
251
|
+
kind: "skip",
|
|
252
|
+
reason: "--skip-script requested",
|
|
253
|
+
skipReason: "operator opted out",
|
|
254
|
+
});
|
|
255
|
+
} else if (!detection.pkg.present) {
|
|
256
|
+
steps.push({
|
|
257
|
+
id: "script",
|
|
258
|
+
title: "package.json dev:https",
|
|
259
|
+
kind: "skip",
|
|
260
|
+
reason: "no package.json in cwd",
|
|
261
|
+
skipReason: `cd into your Next.js project, then re-run, OR add manually:\n "dev:https": "${devHttpsCommandFor(detection, detection.domain)}"`,
|
|
262
|
+
});
|
|
263
|
+
} else if (!detection.pkg.hasNext) {
|
|
264
|
+
steps.push({
|
|
265
|
+
id: "script",
|
|
266
|
+
title: "package.json dev:https",
|
|
267
|
+
kind: "warn",
|
|
268
|
+
reason: "package.json present but `next` not in dependencies",
|
|
269
|
+
skipReason:
|
|
270
|
+
"not a Next.js project — skipping the dev:https script. The cert is still ready in ~/.config/dev-certs.",
|
|
271
|
+
});
|
|
272
|
+
} else {
|
|
273
|
+
const command = devHttpsCommandFor(detection, detection.domain);
|
|
274
|
+
const existing = detection.pkg.existingDevHttps;
|
|
275
|
+
if (existing && detectMod.devHttpsScriptsEqual(existing, command)) {
|
|
276
|
+
steps.push({
|
|
277
|
+
id: "script",
|
|
278
|
+
title: "package.json dev:https",
|
|
279
|
+
kind: "skip",
|
|
280
|
+
reason: "script already matches the canonical command",
|
|
281
|
+
skipReason: "idempotent",
|
|
282
|
+
});
|
|
283
|
+
} else {
|
|
284
|
+
steps.push({
|
|
285
|
+
id: "script",
|
|
286
|
+
title: existing
|
|
287
|
+
? "Update package.json dev:https script"
|
|
288
|
+
: "Add package.json dev:https script",
|
|
289
|
+
kind: "exec",
|
|
290
|
+
reason: existing
|
|
291
|
+
? `existing script differs from canonical — will replace.\n before: ${existing}\n after: ${command}`
|
|
292
|
+
: "no dev:https script in package.json",
|
|
293
|
+
async run() {
|
|
294
|
+
return nextjsMod.setDevHttpsScript(detection.pkg.path, command);
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ---- Step: legacy cert migration hint ------------------------------
|
|
301
|
+
if (detection.legacyCerts && detection.legacyCerts.length > 0) {
|
|
302
|
+
const names = detection.legacyCerts.map((c) => c.domain).join(", ");
|
|
303
|
+
steps.push({
|
|
304
|
+
id: "legacy-warn",
|
|
305
|
+
title: "Legacy certs detected in project root",
|
|
306
|
+
kind: "warn",
|
|
307
|
+
reason: `Found ${detection.legacyCerts.length} legacy cert pair(s) in cwd: ${names}`,
|
|
308
|
+
skipReason:
|
|
309
|
+
"Run `synapse https migrate` to move them under ~/.config/dev-certs/ and update package.json paths.",
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return steps;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Used by the remove command. Generates an undo plan symmetric to
|
|
317
|
+
// the setup plan: removes cert files, hosts entry, dev:https script.
|
|
318
|
+
function planRemove(detection, { keepCerts = false, keepScript = false, keepHosts = false } = {}) {
|
|
319
|
+
const steps = [];
|
|
320
|
+
if (!detection.validation.ok) {
|
|
321
|
+
steps.push({
|
|
322
|
+
id: "validate",
|
|
323
|
+
title: "Validate domain",
|
|
324
|
+
kind: "blocker",
|
|
325
|
+
reason: detection.validation.reason,
|
|
326
|
+
blocker: detection.validation.reason,
|
|
327
|
+
});
|
|
328
|
+
return steps;
|
|
329
|
+
}
|
|
330
|
+
const { dir: certDir, certPath, keyPath } = detection.existingCerts || detectMod.certFilesFor(detection.domain);
|
|
331
|
+
|
|
332
|
+
if (!keepCerts) {
|
|
333
|
+
if (detection.existingCerts && detection.existingCerts.present) {
|
|
334
|
+
steps.push({
|
|
335
|
+
id: "cert-remove",
|
|
336
|
+
title: `Delete ${certDir}`,
|
|
337
|
+
kind: "exec",
|
|
338
|
+
reason: "cert present in canonical location",
|
|
339
|
+
async run() {
|
|
340
|
+
fs.rmSync(certDir, { recursive: true, force: true });
|
|
341
|
+
return { changed: true };
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
} else {
|
|
345
|
+
steps.push({
|
|
346
|
+
id: "cert-remove",
|
|
347
|
+
title: "Delete cert directory",
|
|
348
|
+
kind: "skip",
|
|
349
|
+
reason: "no cert present in canonical location",
|
|
350
|
+
skipReason: "nothing to delete",
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (!keepHosts) {
|
|
356
|
+
if (detection.hosts.matches.length > 0) {
|
|
357
|
+
steps.push({
|
|
358
|
+
id: "hosts-remove",
|
|
359
|
+
title: `Remove ${detection.domain} from ${detection.hosts.path}`,
|
|
360
|
+
kind: "exec",
|
|
361
|
+
reason: "managed hosts entry present",
|
|
362
|
+
async run() {
|
|
363
|
+
return hostsMod.removeEntry(detection.hosts.path, detection.domain);
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
} else {
|
|
367
|
+
steps.push({
|
|
368
|
+
id: "hosts-remove",
|
|
369
|
+
title: "Remove hosts entry",
|
|
370
|
+
kind: "skip",
|
|
371
|
+
reason: "no entry found",
|
|
372
|
+
skipReason: "nothing to remove",
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (!keepScript) {
|
|
378
|
+
if (detection.pkg.existingDevHttps) {
|
|
379
|
+
steps.push({
|
|
380
|
+
id: "script-remove",
|
|
381
|
+
title: "Remove dev:https script from package.json",
|
|
382
|
+
kind: "exec",
|
|
383
|
+
reason: "script present",
|
|
384
|
+
async run() {
|
|
385
|
+
return nextjsMod.removeDevHttpsScript(detection.pkg.path);
|
|
386
|
+
},
|
|
387
|
+
});
|
|
388
|
+
} else {
|
|
389
|
+
steps.push({
|
|
390
|
+
id: "script-remove",
|
|
391
|
+
title: "Remove dev:https script",
|
|
392
|
+
kind: "skip",
|
|
393
|
+
reason: "no script present",
|
|
394
|
+
skipReason: "nothing to remove",
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return steps;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Tells the caller whether the plan is executable (no blockers).
|
|
402
|
+
function planIsExecutable(steps) {
|
|
403
|
+
return steps.every((s) => s.kind !== "blocker");
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
module.exports = {
|
|
407
|
+
plan,
|
|
408
|
+
planRemove,
|
|
409
|
+
planIsExecutable,
|
|
410
|
+
mkcertInstallHint,
|
|
411
|
+
devHttpsCommandFor,
|
|
412
|
+
};
|