@logicseek/hello-cc 0.1.8 → 1.0.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.
Files changed (108) hide show
  1. package/CHANGELOG.md +117 -0
  2. package/README.md +26 -4
  3. package/README.zh-CN.md +22 -3
  4. package/bin/hcc.mjs +3 -5622
  5. package/docs/README.md +8 -0
  6. package/docs/README.zh-CN.md +6 -0
  7. package/docs/architecture.md +29 -18
  8. package/docs/architecture.zh-CN.md +20 -10
  9. package/docs/commands.md +16 -5
  10. package/docs/commands.zh-CN.md +16 -5
  11. package/docs/current-state-review-2026-08.md +341 -0
  12. package/docs/defect-review-2026-07.md +382 -0
  13. package/docs/guide.md +53 -5
  14. package/docs/guide.zh-CN.md +46 -3
  15. package/docs/superpowers/plans/2026-08-03-runtime-safety-plan.md +294 -0
  16. package/docs/superpowers/plans/2026-08-03-v1-data-identity-plan.md +400 -0
  17. package/docs/superpowers/plans/2026-08-03-v1-release-verification-plan.md +284 -0
  18. package/docs/superpowers/plans/2026-08-03-web-api-hardening-plan.md +303 -0
  19. package/docs/superpowers/plans/2026-08-04-runtime-buffer-gc-two-phase.md +71 -0
  20. package/docs/superpowers/specs/2026-08-03-runtime-safety-design.md +117 -0
  21. package/docs/superpowers/specs/2026-08-03-v1-data-identity-design.md +120 -0
  22. package/docs/superpowers/specs/2026-08-03-v1-release-verification-design.md +117 -0
  23. package/docs/superpowers/specs/2026-08-03-web-api-hardening-design.md +104 -0
  24. package/lib/cli/app.mjs +642 -0
  25. package/lib/cli/commands/coordination.mjs +247 -0
  26. package/lib/cli/commands/doctor.mjs +81 -0
  27. package/lib/cli/commands/gc.mjs +852 -0
  28. package/lib/cli/commands/hook.mjs +205 -0
  29. package/lib/cli/commands/install.mjs +294 -0
  30. package/lib/cli/commands/lifecycle.mjs +101 -0
  31. package/lib/cli/commands/lock.mjs +197 -0
  32. package/lib/cli/commands/misc.mjs +61 -0
  33. package/lib/cli/commands/msg.mjs +152 -0
  34. package/lib/cli/commands/peer.mjs +233 -0
  35. package/lib/cli/commands/query.mjs +96 -0
  36. package/lib/cli/commands/run.mjs +463 -0
  37. package/lib/cli/commands/task.mjs +405 -0
  38. package/lib/cli/commands/team.mjs +131 -0
  39. package/lib/cli/commands/tmux.mjs +838 -0
  40. package/lib/cli/commands/up.mjs +135 -0
  41. package/lib/cli-args.mjs +16 -2
  42. package/lib/coordination-state.mjs +112 -51
  43. package/lib/core/coordination/automation.mjs +5 -8
  44. package/lib/core/coordination/clock-safety.mjs +323 -0
  45. package/lib/core/coordination/event-retention.mjs +17 -0
  46. package/lib/core/coordination/gc-plan.mjs +614 -0
  47. package/lib/core/coordination/lease-renewal.mjs +102 -0
  48. package/lib/core/coordination/lock-evidence.mjs +77 -0
  49. package/lib/core/coordination/optimistic-evidence.mjs +26 -0
  50. package/lib/core/coordination/tasks.mjs +156 -36
  51. package/lib/core/peers/evidence-runtime.mjs +312 -0
  52. package/lib/core/peers/evidence.mjs +103 -0
  53. package/lib/core/peers/hook-owner.mjs +32 -0
  54. package/lib/core/peers/liveness.mjs +44 -4
  55. package/lib/core/peers/peer-helpers.mjs +121 -0
  56. package/lib/core/peers/session.mjs +8 -2
  57. package/lib/core/peers/tmux-safety.mjs +290 -0
  58. package/lib/core/sessions/launch.mjs +15 -1
  59. package/lib/db/connection.mjs +104 -0
  60. package/lib/db/events.mjs +34 -0
  61. package/lib/db/migration-backup.mjs +225 -0
  62. package/lib/db/schema.mjs +102 -5
  63. package/lib/db/stores/peers.mjs +34 -0
  64. package/lib/integrations/peers/identity.mjs +62 -16
  65. package/lib/integrations/shims/script.mjs +139 -15
  66. package/lib/process/identity.mjs +192 -0
  67. package/lib/process/pty-lifecycle.mjs +149 -0
  68. package/lib/runtime/buffer-directory-lease.mjs +47 -0
  69. package/lib/runtime/buffer-evidence.mjs +336 -0
  70. package/lib/runtime/buffer-gc-protocol.mjs +167 -0
  71. package/lib/runtime/buffer-gc.mjs +270 -0
  72. package/lib/runtime/client.mjs +61 -8
  73. package/lib/runtime/fatal-shutdown.mjs +54 -0
  74. package/lib/runtime/project-path.mjs +139 -0
  75. package/lib/runtime/projects.mjs +320 -40
  76. package/lib/runtime/state.mjs +232 -29
  77. package/lib/shared/cleanup-error.mjs +54 -0
  78. package/lib/shared/clock-grace.mjs +106 -0
  79. package/lib/shared/errors.mjs +9 -0
  80. package/lib/shared/file-lock.mjs +370 -0
  81. package/lib/shared/json-file.mjs +6 -2
  82. package/lib/shared/redact.mjs +60 -0
  83. package/lib/shared/socket-lock-worker.mjs +201 -0
  84. package/lib/terminal/tmux.mjs +78 -4
  85. package/lib/tmux.mjs +4 -0
  86. package/lib/ui/help.mjs +53 -15
  87. package/lib/web/api-version.mjs +42 -0
  88. package/lib/web/auto-attach.mjs +185 -0
  89. package/lib/web/buffer-gc-runtime.mjs +175 -0
  90. package/lib/web/cookie-auth.mjs +119 -0
  91. package/lib/web/external-sessions.mjs +254 -0
  92. package/lib/web/http-routes.mjs +542 -0
  93. package/lib/web/http.mjs +143 -7
  94. package/lib/web/liveness-reaper.mjs +145 -0
  95. package/lib/web/peer-actions.mjs +132 -23
  96. package/lib/web/project-contexts.mjs +178 -0
  97. package/lib/web/pty-sessions.mjs +203 -0
  98. package/lib/web/runtime-main.mjs +714 -0
  99. package/lib/web/runtime.mjs +91 -36
  100. package/lib/web/session-serialize.mjs +0 -0
  101. package/lib/web/startup.mjs +333 -0
  102. package/lib/web/tls.mjs +365 -0
  103. package/lib/web/tmux-sessions.mjs +1361 -0
  104. package/lib/web/tmux-stream.mjs +166 -0
  105. package/lib/web/ui-template.mjs +152 -20
  106. package/package.json +5 -2
  107. package/scripts/check-free-identifiers.mjs +290 -0
  108. package/scripts/regression.mjs +5646 -262
@@ -0,0 +1,365 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { createPublicKey, randomBytes, X509Certificate } from 'node:crypto';
6
+ import { isIP } from 'node:net';
7
+ import { CliError } from '../shared/errors.mjs';
8
+ import { globalStateDir } from '../runtime/paths.mjs';
9
+
10
+ const CERT_RENEW_BEFORE_MS = 7 * 24 * 60 * 60 * 1000;
11
+ const GENERATION_CLEANUP_GRACE_MS = 60 * 60 * 1000;
12
+ const GENERATION_NAME_RE = /^generation-[a-z0-9-]+$/i;
13
+ const OWNED_GENERATION_NAME_RE = /^generation-[a-z0-9]+-(\d+)-[a-f0-9]+$/i;
14
+
15
+ // hello-cc can serve the web console over HTTPS with an auto-generated
16
+ // self-signed certificate. This protects against passive LAN sniffing of the
17
+ // access token and terminal stream (net-02). The browser will warn about the
18
+ // self-signed cert until the user trusts it manually — that is the accepted
19
+ // tradeoff for not requiring a CA-signed cert.
20
+
21
+ function which(binary) {
22
+ const result = spawnSync('which', [binary], { encoding: 'utf8' });
23
+ return result.status === 0 ? result.stdout.trim() : null;
24
+ }
25
+
26
+ export function opensslAvailable() {
27
+ return Boolean(which('openssl'));
28
+ }
29
+
30
+ function hostSanEntry(host) {
31
+ const value = String(host || '').trim().replace(/^\[|\]$/g, '');
32
+ if (!value || value === '0.0.0.0' || value === '::') return null;
33
+ return isIP(value) ? `IP:${value}` : `DNS:${value}`;
34
+ }
35
+
36
+ function lanIpSanEntries(extraHosts = []) {
37
+ const entries = ['DNS:localhost', 'IP:127.0.0.1', hostSanEntry(os.hostname())];
38
+ for (const host of extraHosts) entries.push(hostSanEntry(host));
39
+ try {
40
+ const ifaces = os.networkInterfaces();
41
+ for (const list of Object.values(ifaces)) {
42
+ for (const addr of list || []) {
43
+ if (addr.family === 'IPv4' && !addr.internal) entries.push(`IP:${addr.address}`);
44
+ }
45
+ }
46
+ } catch {}
47
+ // Dedup while preserving order.
48
+ return [...new Set(entries.filter(Boolean))];
49
+ }
50
+
51
+ function generateCert(keyPath, certPath, sanEntries, configPath = null) {
52
+ const args = [
53
+ 'req', '-x509', '-newkey', 'rsa:2048', '-nodes',
54
+ '-keyout', keyPath, '-out', certPath,
55
+ '-days', '365', '-subj', '/CN=localhost'
56
+ ];
57
+ if (configPath) args.push('-config', configPath, '-extensions', 'v3_req');
58
+ else args.push('-addext', `subjectAltName=${sanEntries.join(',')}`);
59
+ return spawnSync('openssl', args, { encoding: 'utf8' });
60
+ }
61
+
62
+ function certificateCoversSans(certificate, sanEntries) {
63
+ return sanEntries.every((entry) => {
64
+ const separator = entry.indexOf(':');
65
+ const type = entry.slice(0, separator);
66
+ const value = entry.slice(separator + 1);
67
+ if (type === 'DNS') return Boolean(certificate.checkHost(value, { subject: 'never' }));
68
+ if (type === 'IP') return Boolean(certificate.checkIP(value));
69
+ return false;
70
+ });
71
+ }
72
+
73
+ function certificateMatchesKey(certificate, key) {
74
+ const certPublicKey = certificate.publicKey.export({ format: 'der', type: 'spki' });
75
+ const keyPublicKey = createPublicKey(key).export({ format: 'der', type: 'spki' });
76
+ return certPublicKey.equals(keyPublicKey);
77
+ }
78
+
79
+ export function tlsCredentialsAreReusable({ key, cert, sanEntries, nowMs = Date.now() }) {
80
+ try {
81
+ const certificate = new X509Certificate(cert);
82
+ const validFromMs = Date.parse(certificate.validFrom);
83
+ const validToMs = Date.parse(certificate.validTo);
84
+ if (!Number.isFinite(nowMs) ||
85
+ !Number.isFinite(validFromMs) ||
86
+ !Number.isFinite(validToMs) ||
87
+ validFromMs > nowMs ||
88
+ validToMs <= nowMs + CERT_RENEW_BEFORE_MS) return false;
89
+ return certificateCoversSans(certificate, sanEntries) &&
90
+ certificateMatchesKey(certificate, key);
91
+ } catch {
92
+ return false;
93
+ }
94
+ }
95
+
96
+ function readReusableCredentials(keyPath, certPath, sanEntries) {
97
+ try {
98
+ const key = fs.readFileSync(keyPath, 'utf8');
99
+ const cert = fs.readFileSync(certPath, 'utf8');
100
+ if (!tlsCredentialsAreReusable({ key, cert, sanEntries })) return null;
101
+ return { key, cert, certPath };
102
+ } catch {
103
+ return null;
104
+ }
105
+ }
106
+
107
+ function opensslConfig(sanEntries) {
108
+ return [
109
+ '[req]',
110
+ 'distinguished_name = req_distinguished_name',
111
+ 'x509_extensions = v3_req',
112
+ 'prompt = no',
113
+ '',
114
+ '[req_distinguished_name]',
115
+ 'CN = localhost',
116
+ '',
117
+ '[v3_req]',
118
+ `subjectAltName = ${sanEntries.join(',')}`,
119
+ ''
120
+ ].join('\n');
121
+ }
122
+
123
+ function generateCredentials(dir, keyPath, certPath, sanEntries) {
124
+ if (!opensslAvailable()) {
125
+ throw new CliError('TLS_UNAVAILABLE',
126
+ 'Cannot generate a self-signed TLS certificate: openssl is not installed. ' +
127
+ 'Install openssl, or run without --tls (prefer --local for loopback-only access).');
128
+ }
129
+
130
+ const suffix = `${process.pid}-${randomBytes(6).toString('hex')}`;
131
+ const tmpKeyPath = path.join(dir, `.self-signed.${suffix}.key.tmp`);
132
+ const tmpCertPath = path.join(dir, `.self-signed.${suffix}.crt.tmp`);
133
+ const tmpConfigPath = path.join(dir, `.self-signed.${suffix}.cnf.tmp`);
134
+ try {
135
+ let result = generateCert(tmpKeyPath, tmpCertPath, sanEntries);
136
+ if (result.status !== 0) {
137
+ try { fs.rmSync(tmpKeyPath, { force: true }); } catch {}
138
+ try { fs.rmSync(tmpCertPath, { force: true }); } catch {}
139
+ fs.writeFileSync(tmpConfigPath, opensslConfig(sanEntries), { mode: 0o600 });
140
+ result = generateCert(tmpKeyPath, tmpCertPath, sanEntries, tmpConfigPath);
141
+ }
142
+ if (result.status !== 0) {
143
+ throw new CliError('TLS_UNAVAILABLE',
144
+ `Failed to generate a self-signed TLS certificate: ${(result.stderr || result.stdout || '').trim() || 'openssl exited with status ' + result.status}`);
145
+ }
146
+
147
+ try { fs.chmodSync(tmpKeyPath, 0o600); } catch {}
148
+ try { fs.chmodSync(tmpCertPath, 0o600); } catch {}
149
+ const generated = readReusableCredentials(tmpKeyPath, tmpCertPath, sanEntries);
150
+ if (!generated) {
151
+ throw new CliError('TLS_UNAVAILABLE', 'Generated TLS certificate failed validation');
152
+ }
153
+
154
+ // These files live in an unpublished generation directory. The generation
155
+ // becomes active only after both renames and validation complete.
156
+ fs.renameSync(tmpCertPath, certPath);
157
+ fs.renameSync(tmpKeyPath, keyPath);
158
+ return { key: generated.key, cert: generated.cert, certPath };
159
+ } finally {
160
+ for (const file of [tmpKeyPath, tmpCertPath, tmpConfigPath]) {
161
+ try { fs.rmSync(file, { force: true }); } catch {}
162
+ }
163
+ }
164
+ }
165
+
166
+ function readCurrentGeneration(dir) {
167
+ try {
168
+ const pointer = JSON.parse(fs.readFileSync(path.join(dir, 'current.json'), 'utf8'));
169
+ const generation = String(pointer?.generation || '');
170
+ return GENERATION_NAME_RE.test(generation) ? generation : null;
171
+ } catch {
172
+ return null;
173
+ }
174
+ }
175
+
176
+ function processIsAlive(pid) {
177
+ if (!Number.isInteger(pid) || pid <= 0) return false;
178
+ try {
179
+ process.kill(pid, 0);
180
+ return true;
181
+ } catch (err) {
182
+ return err?.code === 'EPERM';
183
+ }
184
+ }
185
+
186
+ function generationCreationIsActive(generationDir, generation) {
187
+ const creatingPath = path.join(generationDir, '.creating');
188
+ const nameMatch = generation.match(OWNED_GENERATION_NAME_RE);
189
+ const namePid = nameMatch ? Number.parseInt(nameMatch[1], 10) : null;
190
+ try {
191
+ const ownerPid = Number.parseInt(fs.readFileSync(creatingPath, 'utf8').trim(), 10);
192
+ if (Number.isInteger(ownerPid) && ownerPid > 0 && (!namePid || ownerPid === namePid)) {
193
+ return processIsAlive(ownerPid);
194
+ }
195
+ } catch {}
196
+ if (fs.existsSync(path.join(generationDir, '.published'))) return false;
197
+
198
+ // Backward compatibility for generations created before lifecycle markers
199
+ // were added, including the mkdir-to-marker window in a concurrent creator.
200
+ return namePid ? processIsAlive(namePid) : false;
201
+ }
202
+
203
+ function writeGenerationCreatingMarker(generationDir) {
204
+ const markerPath = path.join(generationDir, '.creating');
205
+ const tmpMarkerPath = path.join(generationDir, `.creating.${process.pid}-${randomBytes(4).toString('hex')}.tmp`);
206
+ try {
207
+ fs.writeFileSync(tmpMarkerPath, `${process.pid}\n`, { mode: 0o600 });
208
+ fs.renameSync(tmpMarkerPath, markerPath);
209
+ } finally {
210
+ try { fs.rmSync(tmpMarkerPath, { force: true }); } catch {}
211
+ }
212
+ }
213
+
214
+ function markGenerationPublished(generationDir) {
215
+ const creatingPath = path.join(generationDir, '.creating');
216
+ const publishedPath = path.join(generationDir, '.published');
217
+ try {
218
+ fs.renameSync(creatingPath, publishedPath);
219
+ } catch {
220
+ try { fs.writeFileSync(publishedPath, `${process.pid}\n`, { mode: 0o600 }); } catch {}
221
+ if (fs.existsSync(publishedPath)) {
222
+ try { fs.rmSync(creatingPath, { force: true }); } catch {}
223
+ }
224
+ }
225
+ try { fs.chmodSync(publishedPath, 0o600); } catch {}
226
+ }
227
+
228
+ function currentGenerationCredentials(dir, sanEntries) {
229
+ const pointerPath = path.join(dir, 'current.json');
230
+ try {
231
+ const generation = readCurrentGeneration(dir);
232
+ if (!generation) return null;
233
+ const generationDir = path.join(dir, generation);
234
+ const keyPath = path.join(generationDir, 'self-signed.key');
235
+ const certPath = path.join(generationDir, 'self-signed.crt');
236
+ const credentials = readReusableCredentials(keyPath, certPath, sanEntries);
237
+ if (!credentials) return null;
238
+ try { fs.chmodSync(generationDir, 0o700); } catch {}
239
+ try { fs.chmodSync(keyPath, 0o600); } catch {}
240
+ try { fs.chmodSync(certPath, 0o600); } catch {}
241
+ try { fs.chmodSync(pointerPath, 0o600); } catch {}
242
+ markGenerationPublished(generationDir);
243
+ return credentials;
244
+ } catch {
245
+ return null;
246
+ }
247
+ }
248
+
249
+ function publishGeneration(dir, generation) {
250
+ const pointerPath = path.join(dir, 'current.json');
251
+ const tmpPointerPath = path.join(dir, `.current.${process.pid}-${randomBytes(6).toString('hex')}.tmp`);
252
+ try {
253
+ fs.writeFileSync(tmpPointerPath, `${JSON.stringify({ generation })}\n`, { mode: 0o600 });
254
+ fs.renameSync(tmpPointerPath, pointerPath);
255
+ try { fs.chmodSync(pointerPath, 0o600); } catch {}
256
+ } finally {
257
+ try { fs.rmSync(tmpPointerPath, { force: true }); } catch {}
258
+ }
259
+ }
260
+
261
+ function pruneOldGenerations(dir, t = Date.now()) {
262
+ try {
263
+ const current = readCurrentGeneration(dir);
264
+ if (!current) return;
265
+
266
+ const generations = fs.readdirSync(dir, { withFileTypes: true })
267
+ .filter((entry) => entry.isDirectory() && GENERATION_NAME_RE.test(entry.name))
268
+ .map((entry) => {
269
+ try {
270
+ return { name: entry.name, mtimeMs: fs.statSync(path.join(dir, entry.name)).mtimeMs };
271
+ } catch {
272
+ return null;
273
+ }
274
+ })
275
+ .filter(Boolean)
276
+ .sort((a, b) => b.mtimeMs - a.mtimeMs);
277
+
278
+ let keptPrevious = false;
279
+ for (const generation of generations) {
280
+ if (generation.name === current) continue;
281
+ const generationDir = path.join(dir, generation.name);
282
+ const hasCreatingMarker = fs.existsSync(path.join(generationDir, '.creating'));
283
+ if (generationCreationIsActive(generationDir, generation.name)) continue;
284
+ let complete = false;
285
+ try {
286
+ complete = fs.statSync(path.join(generationDir, 'self-signed.key')).size > 0 &&
287
+ fs.statSync(path.join(generationDir, 'self-signed.crt')).size > 0;
288
+ } catch {}
289
+ if (!keptPrevious && complete && !hasCreatingMarker) {
290
+ keptPrevious = true;
291
+ continue;
292
+ }
293
+ // A concurrently generated directory may not be published yet. Leave
294
+ // recent candidates alone; a later startup will collect stale losers.
295
+ if (t - generation.mtimeMs < GENERATION_CLEANUP_GRACE_MS) continue;
296
+ // current.json can change while this best-effort scan is running.
297
+ if (readCurrentGeneration(dir) === generation.name) continue;
298
+ if (generationCreationIsActive(generationDir, generation.name)) continue;
299
+ try { fs.rmSync(generationDir, { recursive: true, force: true }); } catch {}
300
+ }
301
+ } catch {
302
+ // Certificate cleanup is best-effort and must never prevent HTTPS startup.
303
+ }
304
+ }
305
+
306
+ function createGeneration(dir, sanEntries, source = null) {
307
+ const generation = `generation-${Date.now().toString(36)}-${process.pid}-${randomBytes(6).toString('hex')}`;
308
+ const generationDir = path.join(dir, generation);
309
+ const keyPath = path.join(generationDir, 'self-signed.key');
310
+ const certPath = path.join(generationDir, 'self-signed.crt');
311
+ fs.mkdirSync(generationDir, { mode: 0o700 });
312
+ try {
313
+ writeGenerationCreatingMarker(generationDir);
314
+ let credentials;
315
+ if (source) {
316
+ fs.writeFileSync(keyPath, source.key, { mode: 0o600 });
317
+ fs.writeFileSync(certPath, source.cert, { mode: 0o600 });
318
+ credentials = readReusableCredentials(keyPath, certPath, sanEntries);
319
+ if (!credentials) throw new CliError('TLS_UNAVAILABLE', 'Existing TLS credentials failed generation validation');
320
+ } else {
321
+ credentials = generateCredentials(generationDir, keyPath, certPath, sanEntries);
322
+ }
323
+ publishGeneration(dir, generation);
324
+ markGenerationPublished(generationDir);
325
+ pruneOldGenerations(dir);
326
+ return credentials;
327
+ } catch (err) {
328
+ if (readCurrentGeneration(dir) !== generation) {
329
+ try { fs.rmSync(generationDir, { recursive: true, force: true }); } catch {}
330
+ }
331
+ throw err;
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Ensures a stable self-signed certificate exists under ~/.hello-cc/tls/ and
337
+ * returns its PEM strings plus the cert path. Reused across restarts so the
338
+ * browser trust decision is stable.
339
+ */
340
+ export function ensureSelfSignedCert(extraHosts = []) {
341
+ const dir = path.join(globalStateDir(), 'tls');
342
+ const sanEntries = lanIpSanEntries(Array.isArray(extraHosts) ? extraHosts : [extraHosts]);
343
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
344
+ try { fs.chmodSync(dir, 0o700); } catch {}
345
+
346
+ const existing = currentGenerationCredentials(dir, sanEntries);
347
+ if (existing) {
348
+ pruneOldGenerations(dir);
349
+ return existing;
350
+ }
351
+
352
+ // Migrate a valid legacy fixed pair without rotating its trust identity.
353
+ const legacy = readReusableCredentials(
354
+ path.join(dir, 'self-signed.key'),
355
+ path.join(dir, 'self-signed.crt'),
356
+ sanEntries
357
+ );
358
+ if (legacy) {
359
+ try { fs.chmodSync(path.join(dir, 'self-signed.key'), 0o600); } catch {}
360
+ try { fs.chmodSync(path.join(dir, 'self-signed.crt'), 0o600); } catch {}
361
+ return createGeneration(dir, sanEntries, legacy);
362
+ }
363
+
364
+ return createGeneration(dir, sanEntries);
365
+ }