@harperfast/harper 5.3.0 → 5.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/bin/copyDb.ts +5 -1
- package/bin/deployRenderer.ts +3 -0
- package/components/Application.ts +66 -22
- package/components/DESIGN.md +89 -36
- package/components/componentLoader.ts +2 -0
- package/components/deploymentRecorder.ts +18 -1
- package/components/installFingerprint.ts +123 -0
- package/components/mcp/DESIGN.md +4 -4
- package/components/operations.js +29 -0
- package/config/DESIGN.md +5 -5
- package/dataLayer/DESIGN.md +37 -22
- package/dataLayer/backupRepository.ts +248 -0
- package/dataLayer/restoreMarker.ts +30 -75
- package/dataLayer/rocksdbBackup.ts +253 -110
- package/dist/bin/copyDb.js +3 -0
- package/dist/bin/copyDb.js.map +1 -1
- package/dist/bin/deployRenderer.js +3 -0
- package/dist/bin/deployRenderer.js.map +1 -1
- package/dist/components/Application.d.ts +5 -0
- package/dist/components/Application.js +51 -19
- package/dist/components/Application.js.map +1 -1
- package/dist/components/componentLoader.js +2 -0
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/deploymentRecorder.js +18 -1
- package/dist/components/deploymentRecorder.js.map +1 -1
- package/dist/components/installFingerprint.d.ts +39 -0
- package/dist/components/installFingerprint.js +113 -0
- package/dist/components/installFingerprint.js.map +1 -0
- package/dist/components/operations.d.ts +2 -0
- package/dist/components/operations.js +31 -0
- package/dist/components/operations.js.map +1 -1
- package/dist/dataLayer/backupRepository.d.ts +33 -0
- package/dist/dataLayer/backupRepository.js +232 -0
- package/dist/dataLayer/backupRepository.js.map +1 -0
- package/dist/dataLayer/restoreMarker.d.ts +4 -1
- package/dist/dataLayer/restoreMarker.js +29 -68
- package/dist/dataLayer/restoreMarker.js.map +1 -1
- package/dist/dataLayer/rocksdbBackup.js +221 -112
- package/dist/dataLayer/rocksdbBackup.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js.map +1 -1
- package/dist/json/systemSchema.json +14 -0
- package/dist/resources/DatabaseTransaction.d.ts +140 -11
- package/dist/resources/DatabaseTransaction.js +1055 -464
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/IterableEventQueue.d.ts +2 -1
- package/dist/resources/IterableEventQueue.js +16 -8
- package/dist/resources/IterableEventQueue.js.map +1 -1
- package/dist/resources/LMDBTransaction.d.ts +13 -2
- package/dist/resources/LMDBTransaction.js +219 -93
- package/dist/resources/LMDBTransaction.js.map +1 -1
- package/dist/resources/RecordEncoder.js +2 -3
- package/dist/resources/RecordEncoder.js.map +1 -1
- package/dist/resources/Resource.js +5 -6
- package/dist/resources/Resource.js.map +1 -1
- package/dist/resources/ResourceInterface.d.ts +3 -1
- package/dist/resources/ResourceInterface.js.map +1 -1
- package/dist/resources/RocksIndexStore.d.ts +2 -1
- package/dist/resources/RocksIndexStore.js +3 -0
- package/dist/resources/RocksIndexStore.js.map +1 -1
- package/dist/resources/Table.js +116 -12
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/analytics/write.js +6 -2
- package/dist/resources/analytics/write.js.map +1 -1
- package/dist/resources/auditStore.d.ts +71 -17
- package/dist/resources/auditStore.js +248 -64
- package/dist/resources/auditStore.js.map +1 -1
- package/dist/resources/branchDatabase.js +2 -0
- package/dist/resources/branchDatabase.js.map +1 -1
- package/dist/resources/databases.js +8 -2
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/indexes/fullTextNativeBinding.d.ts +6 -2
- package/dist/resources/indexes/fullTextNativeBinding.js +12 -2
- package/dist/resources/indexes/fullTextNativeBinding.js.map +1 -1
- package/dist/resources/indexes/fullTextQueryIndex.d.ts +4 -0
- package/dist/resources/indexes/fullTextQueryIndex.js +207 -10
- package/dist/resources/indexes/fullTextQueryIndex.js.map +1 -1
- package/dist/resources/models/Models.d.ts +11 -0
- package/dist/resources/models/Models.js +95 -4
- package/dist/resources/models/Models.js.map +1 -1
- package/dist/resources/models/backendRegistry.d.ts +8 -0
- package/dist/resources/models/backendRegistry.js +15 -1
- package/dist/resources/models/backendRegistry.js.map +1 -1
- package/dist/resources/models/bootstrap.d.ts +2 -0
- package/dist/resources/models/bootstrap.js +6 -0
- package/dist/resources/models/bootstrap.js.map +1 -1
- package/dist/resources/models/calibration.d.ts +88 -0
- package/dist/resources/models/calibration.js +244 -0
- package/dist/resources/models/calibration.js.map +1 -0
- package/dist/resources/models/calibrationStore.d.ts +163 -0
- package/dist/resources/models/calibrationStore.js +986 -0
- package/dist/resources/models/calibrationStore.js.map +1 -0
- package/dist/resources/models/decisionStore.d.ts +24 -0
- package/dist/resources/models/decisionStore.js +45 -0
- package/dist/resources/models/decisionStore.js.map +1 -1
- package/dist/resources/models/generativeDecision.js +44 -15
- package/dist/resources/models/generativeDecision.js.map +1 -1
- package/dist/resources/models/types.d.ts +7 -0
- package/dist/resources/recordLock.d.ts +8 -5
- package/dist/resources/recordLock.js +6 -4
- package/dist/resources/recordLock.js.map +1 -1
- package/dist/resources/replayLogs.js +8 -1
- package/dist/resources/replayLogs.js.map +1 -1
- package/dist/resources/scheduler/engine.d.ts +4 -0
- package/dist/resources/scheduler/engine.js +30 -2
- package/dist/resources/scheduler/engine.js.map +1 -1
- package/dist/resources/search.d.ts +16 -4
- package/dist/resources/search.js +117 -13
- package/dist/resources/search.js.map +1 -1
- package/dist/resources/transaction.js +85 -5
- package/dist/resources/transaction.js.map +1 -1
- package/dist/resources/transactionBroadcast.d.ts +8 -0
- package/dist/resources/transactionBroadcast.js +65 -0
- package/dist/resources/transactionBroadcast.js.map +1 -1
- package/dist/security/keys.d.ts +1 -1
- package/dist/security/keys.js +170 -120
- package/dist/security/keys.js.map +1 -1
- package/dist/server/http.js +10 -0
- package/dist/server/http.js.map +1 -1
- package/dist/server/serverHelpers/Request.js +11 -2
- package/dist/server/serverHelpers/Request.js.map +1 -1
- package/dist/server/serverHelpers/contentTypes.js +8 -3
- package/dist/server/serverHelpers/contentTypes.js.map +1 -1
- package/dist/server/serverHelpers/uwsServer.js +38 -7
- package/dist/server/serverHelpers/uwsServer.js.map +1 -1
- package/dist/server/threads/manageThreads.d.ts +3 -3
- package/dist/server/threads/manageThreads.js +15 -13
- package/dist/server/threads/manageThreads.js.map +1 -1
- package/dist/server/threads/threadServer.d.ts +3 -0
- package/dist/server/threads/threadServer.js +24 -6
- package/dist/server/threads/threadServer.js.map +1 -1
- package/dist/upgrade/directives/5-3-0.js +9 -3
- package/dist/upgrade/directives/5-3-0.js.map +1 -1
- package/dist/utility/durableFile.d.ts +36 -0
- package/dist/utility/durableFile.js +106 -0
- package/dist/utility/durableFile.js.map +1 -0
- package/dist/utility/errors/hdbError.d.ts +4 -0
- package/dist/utility/errors/hdbError.js +10 -1
- package/dist/utility/errors/hdbError.js.map +1 -1
- package/dist/utility/globalSchema.d.ts +9 -0
- package/dist/utility/hdbTerms.d.ts +1 -0
- package/dist/utility/hdbTerms.js +1 -0
- package/dist/utility/hdbTerms.js.map +1 -1
- package/dist/validation/configValidator.js +29 -0
- package/dist/validation/configValidator.js.map +1 -1
- package/index.ts +7 -0
- package/json/systemSchema.json +14 -0
- package/npm-shrinkwrap.json +22 -22
- package/package.json +2 -2
- package/resources/DESIGN.md +57 -272
- package/resources/DatabaseTransaction.ts +708 -87
- package/resources/FULL_TEXT.md +6 -2
- package/resources/IterableEventQueue.ts +14 -7
- package/resources/LMDBTransaction.ts +212 -84
- package/resources/RecordEncoder.ts +2 -3
- package/resources/Resource.ts +5 -6
- package/resources/ResourceInterface.ts +3 -1
- package/resources/RocksIndexStore.ts +5 -0
- package/resources/Table.ts +118 -15
- package/resources/analytics/write.ts +6 -2
- package/resources/audit-retention.md +203 -0
- package/resources/auditStore.ts +306 -84
- package/resources/branchDatabase.ts +2 -0
- package/resources/databases.ts +6 -2
- package/resources/indexes/DESIGN.md +120 -177
- package/resources/indexes/fullTextNativeBinding.ts +22 -3
- package/resources/indexes/fullTextQueryIndex.ts +274 -11
- package/resources/models/DESIGN.md +11 -3
- package/resources/models/Models.ts +127 -4
- package/resources/models/backendRegistry.ts +17 -0
- package/resources/models/bootstrap.ts +9 -1
- package/resources/models/calibration.ts +298 -0
- package/resources/models/calibrationStore.ts +1167 -0
- package/resources/models/decisionStore.ts +57 -0
- package/resources/models/generativeDecision.ts +54 -16
- package/resources/models/types.ts +6 -0
- package/resources/record-locks.md +155 -141
- package/resources/recordLock.ts +12 -6
- package/resources/replayLogs.ts +9 -2
- package/resources/scheduler/DESIGN.md +4 -3
- package/resources/scheduler/engine.ts +35 -2
- package/resources/search.ts +154 -16
- package/resources/transaction.ts +50 -5
- package/resources/transactionBroadcast.ts +69 -0
- package/security/DESIGN.md +9 -30
- package/security/keys.ts +175 -127
- package/server/DESIGN.md +70 -36
- package/server/http.ts +10 -0
- package/server/serverHelpers/Request.ts +10 -2
- package/server/serverHelpers/contentTypes.ts +5 -3
- package/server/serverHelpers/uwsServer.ts +34 -7
- package/server/threads/manageThreads.js +16 -14
- package/server/threads/threadServer.js +24 -6
- package/studio/web/assets/{Chat-CNEKMllJ.js → Chat-4LMxI0yB.js} +1 -1
- package/studio/web/assets/{FloatingChat-CEnWhHeR.js → FloatingChat-kWHApEtF.js} +3 -3
- package/studio/web/assets/{SearchDialog-CwhJdEdY.js → SearchDialog-B7ymna97.js} +1 -1
- package/studio/web/assets/{apiToken-DC5vsaaO.js → apiToken-CE3AAt9f.js} +1 -1
- package/studio/web/assets/{applications-DQZxDy0x.js → applications-DdN7CqsB.js} +1 -1
- package/studio/web/assets/{cssMode-Dz2DxF4e.js → cssMode-BpySZUSt.js} +1 -1
- package/studio/web/assets/{editor-B3mjd3tz.js → editor-myLCVv9C.js} +1 -1
- package/studio/web/assets/{html-BsRGM7tp.js → html-DGNYO55O.js} +1 -1
- package/studio/web/assets/{htmlMode-tUKJaA3o.js → htmlMode-BUgpQr41.js} +1 -1
- package/studio/web/assets/{index-BuqFjN5K.js → index-FN4KC9va.js} +39 -39
- package/studio/web/assets/index-QLZw6xrU.css +1 -0
- package/studio/web/assets/{index.lazy-CcpfQFY_.js → index.lazy-DZFuaumG.js} +1 -1
- package/studio/web/assets/{javascript-BZ_uRGHl.js → javascript-DiBzh4pj.js} +1 -1
- package/studio/web/assets/{jsonMode-DgB-aFMj.js → jsonMode-Bm312nmu.js} +1 -1
- package/studio/web/assets/{languageServices-BleuBw17.js → languageServices-Dz2K9rZN.js} +1 -1
- package/studio/web/assets/{lspLanguageFeatures-ByVYLSsQ.js → lspLanguageFeatures-CNu1L5xn.js} +1 -1
- package/studio/web/assets/{notifications-TuzN0IMi.js → notifications--nwFLGmM.js} +1 -1
- package/studio/web/assets/{notifications-D1Nf-Edn.js → notifications-BelKw5bR.js} +1 -1
- package/studio/web/assets/{profile-D7C7Lupz.js → profile-AU5FjOBx.js} +1 -1
- package/studio/web/assets/{regions-nGQCm-v8.js → regions-C0sTSEHH.js} +1 -1
- package/studio/web/assets/{register-C3x9Ze6P.js → register-DyNR2rGk.js} +2 -2
- package/studio/web/assets/{setComponentFile-DpjrRqeG.js → setComponentFile-BkWx8AN3.js} +1 -1
- package/studio/web/assets/{setup-CGiKLZ9e.js → setup-D6LZS78d.js} +2 -2
- package/studio/web/assets/{status-CGJwrNPH.js → status-BCWnn70o.js} +1 -1
- package/studio/web/assets/{toggleHighContrast-C-Ne6P7T.js → toggleHighContrast-pyeGJ6W1.js} +1 -1
- package/studio/web/assets/{tsMode-CSreYTys.js → tsMode-CGV3iaWC.js} +1 -1
- package/studio/web/assets/{typescript-MnxBk89W.js → typescript-Ci7Khvwo.js} +1 -1
- package/studio/web/assets/{useEntityRestURL-TNrh77-s.js → useEntityRestURL-DNiz3aZ0.js} +1 -1
- package/studio/web/assets/{workers-BfZlUNVI.js → workers-byzxjbqy.js} +1 -1
- package/studio/web/assets/{xml-BHd6qGH7.js → xml-DGnHQ3CX.js} +1 -1
- package/studio/web/assets/{yaml-C53R8OQq.js → yaml-BUoTnPWb.js} +1 -1
- package/studio/web/index.html +2 -2
- package/upgrade/directives/5-3-0.ts +10 -3
- package/utility/durableFile.ts +96 -0
- package/utility/errors/hdbError.ts +9 -0
- package/utility/hdbTerms.ts +1 -0
- package/validation/configValidator.ts +31 -0
- package/studio/web/assets/index-72cEH474.css +0 -1
package/security/keys.ts
CHANGED
|
@@ -17,7 +17,7 @@ import * as hdbTerms from '../utility/hdbTerms.ts';
|
|
|
17
17
|
|
|
18
18
|
import * as certificatesTerms from '../utility/terms/certificates.js';
|
|
19
19
|
const tls = require('node:tls');
|
|
20
|
-
import { relative, join } from 'node:path';
|
|
20
|
+
import { relative, join, dirname, resolve } from 'node:path';
|
|
21
21
|
|
|
22
22
|
import assignCmdenvVars from '../utility/assignCmdEnvVariables.ts';
|
|
23
23
|
import * as configUtils from '../config/configUtils.ts';
|
|
@@ -27,9 +27,18 @@ const { CONFIG_PARAMS } = hdbTerms;
|
|
|
27
27
|
const { CERTIFICATE_VALUES } = certificatesTerms;
|
|
28
28
|
import { getThisNodeName, getThisNodeUrl, urlToNodeName, clearThisNodeName } from '../server/nodeName.ts';
|
|
29
29
|
|
|
30
|
-
export const getPrivateKeys = () =>
|
|
30
|
+
export const getPrivateKeys = () => {
|
|
31
|
+
for (const name of configuredPrivateKeyPaths.keys()) {
|
|
32
|
+
try {
|
|
33
|
+
getPrivateKeyByName(name);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
forComponent('tls').conditional.trace?.('Could not refresh configured private key:', name, error);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return privateKeys;
|
|
39
|
+
};
|
|
31
40
|
|
|
32
|
-
import { readFileSync, statSync } from 'node:fs';
|
|
41
|
+
import { readFileSync, statSync, watchFile } from 'node:fs';
|
|
33
42
|
import { getTicketKeys, onMessageFromWorkers } from '../server/threads/manageThreads.js';
|
|
34
43
|
import { isMainThread } from 'worker_threads';
|
|
35
44
|
import {
|
|
@@ -152,9 +161,9 @@ export async function getReplicationCertAuth() {
|
|
|
152
161
|
|
|
153
162
|
let configuredCertsLoaded;
|
|
154
163
|
const privateKeys = new Map();
|
|
164
|
+
const configuredPrivateKeyPaths = new Map<string, string>();
|
|
165
|
+
const filePrivateKeys = new Map<string, string>();
|
|
155
166
|
|
|
156
|
-
// Debounce window (ms) for rebuilding TLS secure contexts. Shared by the hdb_certificate
|
|
157
|
-
// subscription and the private-key hot-reload trigger so both coalesce on the same cadence.
|
|
158
167
|
const TLS_REBUILD_DEBOUNCE_MS = 1500;
|
|
159
168
|
|
|
160
169
|
// Self-retry backoff cap: a permanently bad record must not cost every selector on every
|
|
@@ -163,39 +172,6 @@ const TLS_FAILURE_RETRY_MAX_DELAY_MS = 300_000;
|
|
|
163
172
|
// While a failure signature is unchanged, repeat occurrences log a summary at most this often.
|
|
164
173
|
const TLS_FAILURE_SUMMARY_INTERVAL_MS = 3_600_000;
|
|
165
174
|
|
|
166
|
-
// Debounced rebuild triggers, one per live server TLS selector (registered in createTLSSelector's
|
|
167
|
-
// initialize). When a private key is hot-reloaded on this thread, every live selector re-runs
|
|
168
|
-
// updateTLS so a secure context built with a stale key — or built before this key arrived — is
|
|
169
|
-
// regenerated. Transient selectors (getReplicationCert) opt out so they don't accumulate here.
|
|
170
|
-
const liveTLSRebuilders = new Set<() => void>();
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Trigger a debounced rebuild of every live server's TLS secure contexts on this thread.
|
|
174
|
-
*
|
|
175
|
-
* Workers load their private key directly from disk into the privateKeys map (there is no table
|
|
176
|
-
* propagation for keys), so a key rotation must rebuild the secure contexts locally. The cert side
|
|
177
|
-
* already propagates via the hdb_certificate subscription; without this, a worker that rebuilt for
|
|
178
|
-
* the new cert before reloading the matching key would serve the new cert paired with the old key
|
|
179
|
-
* until the next cert-table change.
|
|
180
|
-
*/
|
|
181
|
-
function rebuildLiveTLSContexts() {
|
|
182
|
-
for (const scheduleRebuild of liveTLSRebuilders) scheduleRebuild();
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Handle a private-key (re)load: update the in-thread map and, on any content change, trigger a
|
|
187
|
-
* local TLS context rebuild. The `previous !== private_key` guard skips identical-content reloads
|
|
188
|
-
* (so neither chokidar nor the periodic poll thrashes) while still rebuilding when a key first
|
|
189
|
-
* appears or is restored after boot — the recovery case we must not strand. During normal startup
|
|
190
|
-
* this runs before any TLS selector is registered, so the rebuild is a harmless no-op on an empty
|
|
191
|
-
* rebuilder set.
|
|
192
|
-
*/
|
|
193
|
-
function handlePrivateKeyReload(private_key_name, private_key) {
|
|
194
|
-
const previous = privateKeys.get(private_key_name);
|
|
195
|
-
privateKeys.set(private_key_name, private_key);
|
|
196
|
-
if (previous !== private_key) rebuildLiveTLSContexts();
|
|
197
|
-
}
|
|
198
|
-
|
|
199
175
|
/**
|
|
200
176
|
* This is responsible for loading any certificates that are in the harperdb-config.yaml file and putting them into the hdbCertificate table.
|
|
201
177
|
* @return {*}
|
|
@@ -222,18 +198,26 @@ export function loadCertificates() {
|
|
|
222
198
|
// need to relativize the paths so they aren't exposed
|
|
223
199
|
let private_key_name = privateKeyPath && relative(join(rootPath, 'keys'), privateKeyPath);
|
|
224
200
|
if (private_key_name) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
(
|
|
228
|
-
|
|
229
|
-
|
|
201
|
+
configuredPrivateKeyPaths.set(private_key_name, privateKeyPath);
|
|
202
|
+
try {
|
|
203
|
+
cacheFilePrivateKey(private_key_name, readPEM(privateKeyPath));
|
|
204
|
+
} catch (error) {
|
|
205
|
+
forComponent('tls').conditional.error?.('Error loading private key:', privateKeyPath, error);
|
|
206
|
+
}
|
|
230
207
|
}
|
|
231
208
|
for (let ca of [false, true]) {
|
|
232
209
|
let path = config[ca ? 'certificateAuthority' : 'certificate'];
|
|
233
210
|
if (path && isMainThread) {
|
|
211
|
+
let pendingPairTimer;
|
|
212
|
+
const reportPendingPair = () => {
|
|
213
|
+
forComponent('tls').conditional.error?.(
|
|
214
|
+
`TLS certificate at ${path} still has no matching private key at ${privateKeyPath}`
|
|
215
|
+
);
|
|
216
|
+
pendingPairTimer = setTimeout(reportPendingPair, TLS_FAILURE_SUMMARY_INTERVAL_MS).unref();
|
|
217
|
+
};
|
|
234
218
|
loadAndWatch(
|
|
235
219
|
path,
|
|
236
|
-
(certificate) => {
|
|
220
|
+
(certificate, fileStats) => {
|
|
237
221
|
if (CERTIFICATE_VALUES.cert === certificate) {
|
|
238
222
|
// this is the compromised Harper certificate authority, and we do not even want to bother to
|
|
239
223
|
// load it or tempted to use it anywhere
|
|
@@ -241,7 +225,7 @@ export function loadCertificates() {
|
|
|
241
225
|
}
|
|
242
226
|
let hostnames = config.hostname ?? config.hostnames ?? config.host ?? config.hosts;
|
|
243
227
|
if (hostnames && !Array.isArray(hostnames)) hostnames = [hostnames];
|
|
244
|
-
const certificatePem =
|
|
228
|
+
const certificatePem = certificate;
|
|
245
229
|
const x509Cert = new X509Certificate(certificatePem);
|
|
246
230
|
let certCn;
|
|
247
231
|
try {
|
|
@@ -259,14 +243,25 @@ export function loadCertificates() {
|
|
|
259
243
|
// Check if cert issued by compromised Harper certificate authority, if it is, do not load it
|
|
260
244
|
if (x509Cert.checkIssued(new X509Certificate(CERTIFICATE_VALUES.cert))) return;
|
|
261
245
|
|
|
262
|
-
//
|
|
263
|
-
// If cert file is newer, add it to table
|
|
264
|
-
// getSync (not get): this runs in a synchronous loadAndWatch callback. On RocksDB get()
|
|
265
|
-
// returns a Promise on a block-cache miss, which is truthy but has no timestamp fields,
|
|
266
|
-
// so the staleness guard below is bypassed and a stale on-disk cert can overwrite a
|
|
267
|
-
// newer replicated record (TLS churn / downgrade) once hdb_certificate evicts.
|
|
246
|
+
// The synchronous timestamp guard must not treat a cold RocksDB read Promise as a record.
|
|
268
247
|
const certRecord = certificateTable.primaryStore.getSync(certCn);
|
|
269
|
-
|
|
248
|
+
if (!ca && privateKeyPath) {
|
|
249
|
+
const privateKey = readPEM(privateKeyPath);
|
|
250
|
+
if (!x509Cert.checkPrivateKey(createPrivateKey(privateKey))) {
|
|
251
|
+
if (!pendingPairTimer) {
|
|
252
|
+
forComponent('tls').conditional.warn?.(
|
|
253
|
+
`Waiting for matching TLS certificate and private key: ${path}, ${privateKeyPath}`
|
|
254
|
+
);
|
|
255
|
+
if (!certRecord || certRecord.is_self_signed) reportPendingPair();
|
|
256
|
+
else pendingPairTimer = setTimeout(reportPendingPair, 30_000).unref();
|
|
257
|
+
}
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
cacheFilePrivateKey(private_key_name, privateKey);
|
|
261
|
+
clearTimeout(pendingPairTimer);
|
|
262
|
+
pendingPairTimer = undefined;
|
|
263
|
+
}
|
|
264
|
+
let fileTimestamp = fileStats.mtimeMs;
|
|
270
265
|
let recordTimestamp =
|
|
271
266
|
!certRecord || certRecord.is_self_signed
|
|
272
267
|
? 1
|
|
@@ -283,7 +278,7 @@ export function loadCertificates() {
|
|
|
283
278
|
return;
|
|
284
279
|
}
|
|
285
280
|
|
|
286
|
-
// Returned so loadAndWatch can roll back its
|
|
281
|
+
// Returned so loadAndWatch can roll back its fingerprint latch if the write fails —
|
|
287
282
|
// assigned as well to preserve loadCertificates()'s awaited-return contract.
|
|
288
283
|
return (promise = certificateTable.put({
|
|
289
284
|
name: certCn,
|
|
@@ -304,7 +299,8 @@ export function loadCertificates() {
|
|
|
304
299
|
},
|
|
305
300
|
}));
|
|
306
301
|
},
|
|
307
|
-
ca ? 'certificate authority' : 'certificate'
|
|
302
|
+
ca ? 'certificate authority' : 'certificate',
|
|
303
|
+
!ca && privateKeyPath ? [privateKeyPath] : []
|
|
308
304
|
);
|
|
309
305
|
}
|
|
310
306
|
}
|
|
@@ -340,20 +336,12 @@ const certificateWatchTimers = new Map<string, NodeJS.Timeout>();
|
|
|
340
336
|
const certificateWatchPollers = new Map<string, () => void>();
|
|
341
337
|
|
|
342
338
|
/**
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
* Two detection mechanisms feed the same reload path (and share the `lastModified` fingerprint,
|
|
346
|
-
* so they never double-reload for the same change):
|
|
347
|
-
* 1. chokidar inotify watcher — the fast path, reacts immediately to fs events.
|
|
348
|
-
* 2. A periodic re-read (main thread only) — a safety net for filesystems where inotify is
|
|
349
|
-
* unreliable (overlayfs/containers/network mounts), where a real renewal can otherwise be
|
|
350
|
-
* silently missed. Interval is tls.certificateWatchInterval (default 5m); 0 disables it.
|
|
351
|
-
* @param path
|
|
352
|
-
* @param loadCert
|
|
353
|
-
* @param type
|
|
339
|
+
* Watch the certificate and related files through filtered parent directories and the main-thread safety poll.
|
|
340
|
+
* A false loader result remains unapplied so an unchanged fingerprint can be retried.
|
|
354
341
|
*/
|
|
355
|
-
function loadAndWatch(path, loadCert, type) {
|
|
342
|
+
function loadAndWatch(path, loadCert, type, relatedPaths: string[] = []) {
|
|
356
343
|
let lastModified;
|
|
344
|
+
let lastAttempted;
|
|
357
345
|
const loadFile = (path, stats?) => {
|
|
358
346
|
// The latch dedupes chokidar/poll but must mean "last successfully APPLIED", or a failed
|
|
359
347
|
// apply is deduplicated forever (#2382). Rollbacks are equality-guarded so an old failure
|
|
@@ -361,69 +349,110 @@ function loadAndWatch(path, loadCert, type) {
|
|
|
361
349
|
const previousModified = lastModified;
|
|
362
350
|
let modified;
|
|
363
351
|
try {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
if (modified
|
|
369
|
-
if (lastModified && isMainThread) logger.warn?.(`Reloading ${type}:`, path);
|
|
352
|
+
const fileStats = stats ?? statSync(path);
|
|
353
|
+
const fingerprints = [fileStats, ...relatedPaths.map((relatedPath) => statSync(relatedPath))];
|
|
354
|
+
modified = JSON.stringify(fingerprints.map(({ mtimeMs, ino, size }) => [mtimeMs, ino, size]));
|
|
355
|
+
// Restoring an applied fingerprint after a failed attempt must clear the loader's pending state.
|
|
356
|
+
if (modified !== lastModified || modified !== lastAttempted) {
|
|
357
|
+
if (lastModified && modified !== lastAttempted && isMainThread) logger.warn?.(`Reloading ${type}:`, path);
|
|
358
|
+
lastAttempted = modified;
|
|
370
359
|
lastModified = modified;
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
360
|
+
const rollback = () => {
|
|
361
|
+
if (lastModified === modified) lastModified = previousModified === modified ? undefined : previousModified;
|
|
362
|
+
};
|
|
363
|
+
const applied = loadCert(readPEM(path), fileStats);
|
|
364
|
+
if (applied === false) rollback();
|
|
365
|
+
else if (typeof (applied as any)?.then === 'function') {
|
|
366
|
+
(applied as Promise<unknown>)
|
|
367
|
+
.then((result) => {
|
|
368
|
+
if (result === false) rollback();
|
|
369
|
+
})
|
|
370
|
+
.catch((error) => {
|
|
371
|
+
logger.error?.(`Error applying ${type}:`, path, error);
|
|
372
|
+
rollback();
|
|
373
|
+
});
|
|
377
374
|
}
|
|
378
375
|
}
|
|
379
376
|
} catch (error) {
|
|
380
377
|
logger.error?.(`Error loading ${type}:`, path, error);
|
|
381
|
-
if (modified !== undefined && lastModified === modified)
|
|
378
|
+
if (modified !== undefined && lastModified === modified)
|
|
379
|
+
lastModified = previousModified === modified ? undefined : previousModified;
|
|
382
380
|
}
|
|
383
381
|
};
|
|
384
382
|
if (fs.existsSync(path)) loadFile(path, statSync(path));
|
|
385
383
|
else logger.error?.(`${type} file not found:`, path);
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
384
|
+
const watchedDirectories = new Map<string, { mustPoll: boolean; files: Set<string> }>();
|
|
385
|
+
for (const filePath of [path, ...relatedPaths]) {
|
|
386
|
+
const target = resolveWatchTarget(filePath);
|
|
387
|
+
const directory = resolveWatchTarget(dirname(target.path));
|
|
388
|
+
let watched = watchedDirectories.get(directory.path);
|
|
389
|
+
if (!watched)
|
|
390
|
+
watchedDirectories.set(
|
|
391
|
+
directory.path,
|
|
392
|
+
(watched = { mustPoll: target.mustPoll || directory.mustPoll, files: new Set() })
|
|
393
|
+
);
|
|
394
|
+
watched.files.add(resolve(target.path));
|
|
395
|
+
}
|
|
396
|
+
for (const [directory, watched] of watchedDirectories) {
|
|
397
|
+
let usingPolling = watched.mustPoll;
|
|
398
|
+
let pollingFiles = false;
|
|
399
|
+
let liveWatcher;
|
|
400
|
+
const openWatcher = () => {
|
|
401
|
+
const opened = (liveWatcher = guardedWatch(directory, {
|
|
402
|
+
persistent: false,
|
|
403
|
+
depth: 0,
|
|
404
|
+
ignoreInitial: true,
|
|
405
|
+
ignored: (filePath) => {
|
|
406
|
+
const resolved = resolve(filePath);
|
|
407
|
+
return resolved !== resolve(directory) && !watched.files.has(resolved);
|
|
408
|
+
},
|
|
409
|
+
...(usingPolling ? POLLING_FALLBACK_OPTIONS : {}),
|
|
410
|
+
}));
|
|
411
|
+
const reload = () => loadFile(path);
|
|
412
|
+
const reopen = () =>
|
|
413
|
+
Promise.resolve()
|
|
414
|
+
.then(() => opened.close())
|
|
415
|
+
.catch(() => {})
|
|
416
|
+
.then(openWatcher)
|
|
417
|
+
.catch((error) => logger.error?.(`Could not reopen the ${type} watch on polling:`, directory, error));
|
|
418
|
+
opened
|
|
419
|
+
.on('add', reload)
|
|
420
|
+
.on('change', reload)
|
|
421
|
+
.on('error', (error) => {
|
|
422
|
+
if (claimLostNativeWatchError(error)) return;
|
|
423
|
+
if (pollingFiles || liveWatcher !== opened) return;
|
|
424
|
+
const errorCode = (error as NodeJS.ErrnoException).code;
|
|
425
|
+
if (errorCode === 'EACCES' || errorCode === 'EPERM') {
|
|
426
|
+
forComponent('tls').conditional.warn?.(
|
|
427
|
+
`Cannot watch TLS directory ${directory}; polling its configured files instead`,
|
|
428
|
+
error
|
|
429
|
+
);
|
|
430
|
+
pollingFiles = true;
|
|
431
|
+
Promise.resolve()
|
|
432
|
+
.then(() => opened.close())
|
|
433
|
+
.catch(() => {});
|
|
434
|
+
// watchFile stays armed across ENOENT without needing to list the unreadable parent.
|
|
435
|
+
for (const file of watched.files)
|
|
436
|
+
watchFile(file, { persistent: false, interval: POLLING_FALLBACK_OPTIONS.interval }, (stats) => {
|
|
437
|
+
if (stats.nlink) reload();
|
|
438
|
+
});
|
|
439
|
+
reload();
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (isWatcherExhaustionError(error)) {
|
|
443
|
+
if (usingPolling || liveWatcher !== opened) return;
|
|
444
|
+
warnWatcherFallback(directory);
|
|
445
|
+
usingPolling = true;
|
|
446
|
+
reopen();
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
logger.error?.(`Error watching ${type}:`, path, error);
|
|
450
|
+
});
|
|
451
|
+
};
|
|
452
|
+
openWatcher();
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (isMainThread) {
|
|
427
456
|
const poll = () => {
|
|
428
457
|
let stats;
|
|
429
458
|
try {
|
|
@@ -619,7 +648,13 @@ export async function getCertAuthority() {
|
|
|
619
648
|
let match;
|
|
620
649
|
for (let cert of allCerts) {
|
|
621
650
|
if (!cert.is_authority) continue;
|
|
622
|
-
|
|
651
|
+
let matchingPrivateKey;
|
|
652
|
+
try {
|
|
653
|
+
matchingPrivateKey = getPrivateKeyByName(cert.private_key_name);
|
|
654
|
+
} catch (error) {
|
|
655
|
+
if (error.code !== 'ENOENT') throw error;
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
623
658
|
if (cert.private_key_name && matchingPrivateKey) {
|
|
624
659
|
const keyCheck = new X509Certificate(cert.certificate).checkPrivateKey(createPrivateKey(matchingPrivateKey));
|
|
625
660
|
if (keyCheck) {
|
|
@@ -1032,7 +1067,7 @@ export function getEffectiveTlsCiphers(type, mtlsOptions?): string | undefined {
|
|
|
1032
1067
|
* Create a TLS selector that will choose the best TLS configuration/context for a given hostname
|
|
1033
1068
|
* @param type
|
|
1034
1069
|
* @param mtlsOptions
|
|
1035
|
-
* @param liveReload when true (default) the selector
|
|
1070
|
+
* @param liveReload when true (default) the selector subscribes to certificate-table updates.
|
|
1036
1071
|
* Pass false for transient, single-use selectors (e.g. getReplicationCert) so they don't accumulate.
|
|
1037
1072
|
* @return {(function(*, *): (*|undefined))|*}
|
|
1038
1073
|
*/
|
|
@@ -1447,7 +1482,7 @@ export function createTLSSelector(type, mtlsOptions?, liveReload = true): any {
|
|
|
1447
1482
|
// derive it from more than the selector's mtlsOptions) so this compares like with like
|
|
1448
1483
|
const effectiveCiphers =
|
|
1449
1484
|
getEffectiveTlsCiphers(type, server.verifiesClientCerts ?? Boolean(mtlsOptions)) ?? null;
|
|
1450
|
-
// latch per distinct value: rebuilds recur (cert-table changes,
|
|
1485
|
+
// latch per distinct value: rebuilds recur (cert-table changes, failure retries) and the
|
|
1451
1486
|
// pending change shouldn't re-warn on every cycle until the restart happens
|
|
1452
1487
|
if (effectiveCiphers !== server.appliedCiphers && server.lastWarnedCiphers !== effectiveCiphers) {
|
|
1453
1488
|
server.lastWarnedCiphers = effectiveCiphers;
|
|
@@ -1476,10 +1511,7 @@ export function createTLSSelector(type, mtlsOptions?, liveReload = true): any {
|
|
|
1476
1511
|
updateTLS();
|
|
1477
1512
|
}, TLS_REBUILD_DEBOUNCE_MS).unref();
|
|
1478
1513
|
};
|
|
1479
|
-
|
|
1480
|
-
// updateTLS() pass: that pass may find the system database not yet loaded and retry
|
|
1481
|
-
// (see above) rather than subscribe, so this must not depend on it succeeding.
|
|
1482
|
-
if (liveReload) liveTLSRebuilders.add(scheduleRebuild);
|
|
1514
|
+
|
|
1483
1515
|
updateTLS();
|
|
1484
1516
|
}));
|
|
1485
1517
|
};
|
|
@@ -1513,8 +1545,24 @@ export function createTLSSelector(type, mtlsOptions?, liveReload = true): any {
|
|
|
1513
1545
|
}
|
|
1514
1546
|
}
|
|
1515
1547
|
|
|
1548
|
+
function cacheFilePrivateKey(name: string, key: string) {
|
|
1549
|
+
if (!privateKeys.has(name) || privateKeys.get(name) === filePrivateKeys.get(name)) privateKeys.set(name, key);
|
|
1550
|
+
filePrivateKeys.set(name, key);
|
|
1551
|
+
return privateKeys.get(name);
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1516
1554
|
function getPrivateKeyByName(private_key_name) {
|
|
1517
1555
|
const private_key = privateKeys.get(private_key_name);
|
|
1556
|
+
const configuredPath = configuredPrivateKeyPaths.get(private_key_name);
|
|
1557
|
+
if (configuredPath && (!private_key || private_key === filePrivateKeys.get(private_key_name))) {
|
|
1558
|
+
try {
|
|
1559
|
+
return cacheFilePrivateKey(private_key_name, readPEM(configuredPath));
|
|
1560
|
+
} catch (error) {
|
|
1561
|
+
if (!private_key) throw error;
|
|
1562
|
+
forComponent('tls').conditional.trace?.('Could not refresh configured private key:', private_key_name, error);
|
|
1563
|
+
return private_key;
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1518
1566
|
if (!private_key && private_key_name) {
|
|
1519
1567
|
return fs.readFileSync(
|
|
1520
1568
|
path.join(envManager.get(CONFIG_PARAMS.ROOTPATH), hdbTerms.LICENSE_KEY_DIR_NAME, private_key_name),
|