signalk-ssl 0.4.0 → 0.5.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/plugin/index.js +63 -3
- package/dist/plugin/storage.d.ts +9 -0
- package/dist/plugin/storage.js +17 -1
- package/package.json +1 -1
package/dist/plugin/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import 'reflect-metadata';
|
|
6
6
|
import { CertStore } from './storage.js';
|
|
7
7
|
import { PassphraseSource } from './passphrase-source.js';
|
|
8
|
-
import { SslService } from './service.js';
|
|
8
|
+
import { SslService, discoverAdvertisedHostname } from './service.js';
|
|
9
9
|
import { startRenewalScheduler } from './scheduler.js';
|
|
10
10
|
import { buildPublicRoutes, registerAdminRoutes } from './api.js';
|
|
11
11
|
import { ConfigSchema, DEFAULT_CONFIG } from './schema.js';
|
|
@@ -18,6 +18,37 @@ const resolveConfig = (raw) => {
|
|
|
18
18
|
const resolveConfigPath = (app, fallback) => {
|
|
19
19
|
return app.config?.configPath ?? fallback;
|
|
20
20
|
};
|
|
21
|
+
const sansAreEmpty = (config) => config.sans.dnsNames.length === 0 && config.sans.ipAddresses.length === 0;
|
|
22
|
+
/**
|
|
23
|
+
* One-shot seed: on first run with an empty SAN list, pre-populate dnsNames
|
|
24
|
+
* with the name the server advertises on mDNS so the user sees a sensible,
|
|
25
|
+
* server-specific default in the plugin config screen instead of a blank field.
|
|
26
|
+
*
|
|
27
|
+
* Guarded by a persistent marker so it runs at most once per install: a user
|
|
28
|
+
* who deletes the seeded name will not see it re-added on the next restart
|
|
29
|
+
* (the SAN-provenance rule from AGENTS.md). Returns true when it triggered a
|
|
30
|
+
* restart, in which case the caller must stop — the plugin is being restarted
|
|
31
|
+
* with the new config.
|
|
32
|
+
*/
|
|
33
|
+
const maybeSeedHostname = async (store, config, rawHostname, restart, log) => {
|
|
34
|
+
if (!sansAreEmpty(config) || (await store.hasSeededHostname())) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
const hostname = discoverAdvertisedHostname(rawHostname);
|
|
38
|
+
if (hostname === null) {
|
|
39
|
+
// No useful suggestion (container ID, localhost, etc.). Don't write the
|
|
40
|
+
// marker — if the host gets a real name later, we still want to seed then.
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
await store.markHostnameSeeded(hostname);
|
|
44
|
+
const newConfig = {
|
|
45
|
+
...config,
|
|
46
|
+
sans: { ...config.sans, dnsNames: [hostname] }
|
|
47
|
+
};
|
|
48
|
+
log(`${PLUGIN_ID} seeding empty SANs with discovered hostname ${hostname}`);
|
|
49
|
+
restart(newConfig);
|
|
50
|
+
return true;
|
|
51
|
+
};
|
|
21
52
|
const pluginConstructor = (app) => {
|
|
22
53
|
const extended = app;
|
|
23
54
|
let scheduler = null;
|
|
@@ -30,7 +61,7 @@ const pluginConstructor = (app) => {
|
|
|
30
61
|
name: PLUGIN_NAME,
|
|
31
62
|
description: PLUGIN_DESCRIPTION,
|
|
32
63
|
schema: () => ConfigSchema,
|
|
33
|
-
start(rawConfig,
|
|
64
|
+
start(rawConfig, restart) {
|
|
34
65
|
config = resolveConfig(rawConfig);
|
|
35
66
|
const dataDir = app.getDataDirPath();
|
|
36
67
|
const configPath = resolveConfigPath(extended, dataDir);
|
|
@@ -38,10 +69,31 @@ const pluginConstructor = (app) => {
|
|
|
38
69
|
passphrase = new PassphraseSource(config.passphraseMode, store);
|
|
39
70
|
service = new SslService({ store, passphrase, config, configPath });
|
|
40
71
|
const svcLocal = service;
|
|
72
|
+
const storeLocal = store;
|
|
73
|
+
const cfgLocal = config;
|
|
41
74
|
const logSchedulerError = (err) => {
|
|
42
75
|
app.error(`${PLUGIN_ID} scheduled renewal failed: ${String(err)}`);
|
|
43
76
|
};
|
|
44
|
-
void
|
|
77
|
+
void storeLocal
|
|
78
|
+
.init()
|
|
79
|
+
.then(async () => {
|
|
80
|
+
// First-run SAN seed must run before the issue flow: if it triggers a
|
|
81
|
+
// restart, start() runs again with the seeded config and the issue
|
|
82
|
+
// happens then. Bail out here so we don't issue against empty SANs and
|
|
83
|
+
// immediately get restarted out from under it.
|
|
84
|
+
try {
|
|
85
|
+
const rawHostname = extended.config?.getExternalHostname?.() ?? '';
|
|
86
|
+
const restarted = await maybeSeedHostname(storeLocal, cfgLocal, rawHostname, restart, (msg) => {
|
|
87
|
+
app.debug(msg);
|
|
88
|
+
});
|
|
89
|
+
if (restarted) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
// A seed failure must never block cert issuance — log and continue.
|
|
95
|
+
app.error(`${PLUGIN_ID} hostname seed failed: ${String(e)}`);
|
|
96
|
+
}
|
|
45
97
|
try {
|
|
46
98
|
const warning = await svcLocal.checkWritePermissions();
|
|
47
99
|
if (warning !== null) {
|
|
@@ -61,6 +113,14 @@ const pluginConstructor = (app) => {
|
|
|
61
113
|
// Start the scheduler only after init + initial issue have completed,
|
|
62
114
|
// so a short test interval can't race against an uninitialised store.
|
|
63
115
|
scheduler = startRenewalScheduler(svcLocal, logSchedulerError);
|
|
116
|
+
})
|
|
117
|
+
// Terminal guard: store.init() (mkdir on a read-only / UID-shifted data
|
|
118
|
+
// dir) or a throw from startRenewalScheduler would otherwise surface as
|
|
119
|
+
// an unhandled rejection. The per-block try/catch above keep the issue
|
|
120
|
+
// flow going; this only catches what they can't. scheduler is left null
|
|
121
|
+
// on failure (the assignment is the last statement), so stop() is safe.
|
|
122
|
+
.catch((e) => {
|
|
123
|
+
app.error(`${PLUGIN_ID} startup failed: ${String(e)}`);
|
|
64
124
|
});
|
|
65
125
|
app.debug(`${PLUGIN_ID} started; data dir=${dataDir}; configPath=${configPath}`);
|
|
66
126
|
},
|
package/dist/plugin/storage.d.ts
CHANGED
|
@@ -34,4 +34,13 @@ export declare class CertStore {
|
|
|
34
34
|
writeSettings(settings: SettingsOnDisk): Promise<void>;
|
|
35
35
|
readConvenienceEnvelope(): Promise<ConveniencePassphraseEnvelope | null>;
|
|
36
36
|
writeConvenienceEnvelope(env: ConveniencePassphraseEnvelope): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Whether the plugin has already attempted its one-shot SAN seed (auto-adding
|
|
39
|
+
* the discovered mDNS hostname to an empty dnsNames list on first run). The
|
|
40
|
+
* marker makes the seed a one-time action: once it exists, a user who deletes
|
|
41
|
+
* the seeded name will not see it re-added on the next restart. Presence is
|
|
42
|
+
* the whole signal; the stored body is informational only.
|
|
43
|
+
*/
|
|
44
|
+
hasSeededHostname(): Promise<boolean>;
|
|
45
|
+
markHostnameSeeded(hostname: string): Promise<void>;
|
|
37
46
|
}
|
package/dist/plugin/storage.js
CHANGED
|
@@ -9,7 +9,8 @@ const FILES = {
|
|
|
9
9
|
state: 'state.json',
|
|
10
10
|
leafState: 'leaf-state.json',
|
|
11
11
|
settings: 'settings.json',
|
|
12
|
-
convenience: 'passphrase.kdf.json'
|
|
12
|
+
convenience: 'passphrase.kdf.json',
|
|
13
|
+
hostnameSeeded: 'hostname-seeded.json'
|
|
13
14
|
};
|
|
14
15
|
const PEM_KEY_MODE = 0o600;
|
|
15
16
|
const PEM_CERT_MODE = 0o644;
|
|
@@ -130,4 +131,19 @@ export class CertStore {
|
|
|
130
131
|
await this.init();
|
|
131
132
|
await atomicWrite(this.path('convenience'), JSON.stringify(env, null, 2), JSON_MODE);
|
|
132
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Whether the plugin has already attempted its one-shot SAN seed (auto-adding
|
|
136
|
+
* the discovered mDNS hostname to an empty dnsNames list on first run). The
|
|
137
|
+
* marker makes the seed a one-time action: once it exists, a user who deletes
|
|
138
|
+
* the seeded name will not see it re-added on the next restart. Presence is
|
|
139
|
+
* the whole signal; the stored body is informational only.
|
|
140
|
+
*/
|
|
141
|
+
async hasSeededHostname() {
|
|
142
|
+
return (await readIfExists(this.path('hostnameSeeded'))) !== null;
|
|
143
|
+
}
|
|
144
|
+
async markHostnameSeeded(hostname) {
|
|
145
|
+
await this.init();
|
|
146
|
+
const body = JSON.stringify({ hostname, seededAt: new Date().toISOString() }, null, 2);
|
|
147
|
+
await atomicWrite(this.path('hostnameSeeded'), body, JSON_MODE);
|
|
148
|
+
}
|
|
133
149
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "signalk-ssl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "SSL/TLS certificate management plugin for SignalK Node Server — generate a local CA, issue server certs, distribute the root via QR to phones/tablets",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/plugin/index.js",
|