@harperfast/harper 5.2.14 → 5.2.15
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/resources/analytics/write.d.ts +3 -0
- package/dist/resources/analytics/write.js +39 -13
- package/dist/resources/analytics/write.js.map +1 -1
- package/dist/security/keys.js +258 -66
- package/dist/security/keys.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/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/resources/analytics/write.ts +40 -14
- package/security/keys.ts +251 -66
- package/server/threads/threadServer.js +24 -6
- package/studio/web/assets/{Chat-C97PMlIn.js → Chat-CeEDA28S.js} +13 -13
- package/studio/web/assets/FloatingChat-J_P5KaR4.js +23 -0
- package/studio/web/assets/SearchDialog-BWMGseDd.js +1 -0
- package/studio/web/assets/{abnfDiagram-O67JEVCF-DpPZWMOf.js → abnfDiagram-O67JEVCF-raXE027J.js} +1 -1
- package/studio/web/assets/alertDialog-BbWD9Xxa.js +1 -0
- package/studio/web/assets/apiToken-BKot-xR5.js +1 -0
- package/studio/web/assets/applications-BlJChLsS.js +296 -0
- package/studio/web/assets/architecture-7GRP2DOG-DDmz3hpI.js +1 -0
- package/studio/web/assets/{architectureDiagram-NJMV4G6O-OmgLraID.js → architectureDiagram-NJMV4G6O-C8_Eet7w.js} +1 -1
- package/studio/web/assets/arrow-up-D2g_57vT.js +1 -0
- package/studio/web/assets/authStore-CJCaADw9.js +3 -0
- package/studio/web/assets/{blockDiagram-BEXU5L5S-Cv7CD3Ai.js → blockDiagram-BEXU5L5S-DR6S5-1D.js} +1 -1
- package/studio/web/assets/{button-MFP9n8Ck.js → button-DWwfgGWI.js} +2 -2
- package/studio/web/assets/{c4Diagram-YGBWAQC7-Ds1vKYnN.js → c4Diagram-YGBWAQC7-BAfZ1G01.js} +1 -1
- package/studio/web/assets/channel-D0AkPjJ8.js +1 -0
- package/studio/web/assets/{chevron-up-CFe8x5Bp.js → chevron-up-CFxbMGv2.js} +1 -1
- package/studio/web/assets/{chunk-3FUC2YCW-DpnBPH8B.js → chunk-3FUC2YCW-DuRp-j19.js} +1 -1
- package/studio/web/assets/{chunk-5DYCD2WN-LLLCQSzg.js → chunk-5DYCD2WN-BceBfIHT.js} +1 -1
- package/studio/web/assets/{chunk-742MDFTN-BL8J_pIO.js → chunk-742MDFTN-CIkt8ijt.js} +1 -1
- package/studio/web/assets/{chunk-7INBJB4K-C-hJHiiK.js → chunk-7INBJB4K-BpeAXyOi.js} +1 -1
- package/studio/web/assets/{chunk-7M6MHVWA-C6ZqkFbA.js → chunk-7M6MHVWA-h6WlaTzO.js} +1 -1
- package/studio/web/assets/{chunk-7PRAP22T-CUXM8BG6.js → chunk-7PRAP22T-KyqX6x3f.js} +1 -1
- package/studio/web/assets/{chunk-GTNCS2PH-DJ2BFvS1.js → chunk-GTNCS2PH-DGHLXyow.js} +1 -1
- package/studio/web/assets/{chunk-GWA4HPMP-C42bSqLL.js → chunk-GWA4HPMP-DsejB6gA.js} +1 -1
- package/studio/web/assets/{chunk-MBY4JIJT-C-5jz6Lo.js → chunk-MBY4JIJT-BwSGweQP.js} +1 -1
- package/studio/web/assets/{chunk-NETBCI7D-D-bzlN4r.js → chunk-NETBCI7D-C8UnOw_f.js} +1 -1
- package/studio/web/assets/{chunk-O7XYJQB3-ep14zA23.js → chunk-O7XYJQB3-CBS3nZYV.js} +1 -1
- package/studio/web/assets/{chunk-UA2S7LBM-BMRWRHED.js → chunk-UA2S7LBM-BHpEdH8D.js} +1 -1
- package/studio/web/assets/{chunk-WEXAMYUT-DdLYVdX1.js → chunk-WEXAMYUT-BAIhTwyh.js} +1 -1
- package/studio/web/assets/{chunk-Z7XXMR3K-BmKEDjcP.js → chunk-Z7XXMR3K-DSHbImdf.js} +1 -1
- package/studio/web/assets/{chunk-ZIGJFQKS-DLZgdNxA.js → chunk-ZIGJFQKS-Cs8fUn7y.js} +1 -1
- package/studio/web/assets/{classDiagram-v2-NBCMYWYE-DOI5vgKT.js → classDiagram-v2-NBCMYWYE-DmDPkt2u.js} +1 -1
- package/studio/web/assets/cn-6T8VranJ.js +1 -0
- package/studio/web/assets/{codicon-Brq4_Ui5.ttf → codicon-DF1abBS2.ttf} +0 -0
- package/studio/web/assets/{createLucideIcon-bziYxJli.js → createLucideIcon-fO5dL7D5.js} +1 -1
- package/studio/web/assets/{css.worker-l1psR_wG.js → css.worker-DoT-OOzx.js} +36 -36
- package/studio/web/assets/{cssMode-DyLZPMjo.js → cssMode-DjeA9Hx_.js} +1 -1
- package/studio/web/assets/{cynefin-OW5HDTMX-BSW-DB7S.js → cynefin-OW5HDTMX-DIvbXLSj.js} +1 -1
- package/studio/web/assets/{cynefinDiagram-VND7K2PF-CbAHEwxt.js → cynefinDiagram-VND7K2PF-BdtaXiCE.js} +1 -1
- package/studio/web/assets/{dagre-6A5THRUB-Csxewgs4.js → dagre-6A5THRUB-C7RUWHyz.js} +1 -1
- package/studio/web/assets/{diagram-22UHCM2B-a0rt8tY4.js → diagram-22UHCM2B-DJCvcaWP.js} +1 -1
- package/studio/web/assets/{diagram-3UASUU5V-lEj2SVNh.js → diagram-3UASUU5V-Du6VKWln.js} +1 -1
- package/studio/web/assets/{diagram-ATOU4E4O-HcrAeB-q.js → diagram-ATOU4E4O-dxRfeG4B.js} +1 -1
- package/studio/web/assets/{diagram-CDSNMT55-CYxiuyEx.js → diagram-CDSNMT55-l5Ga0u-W.js} +1 -1
- package/studio/web/assets/{diagram-MLGK6HIB-ChbCJTgJ.js → diagram-MLGK6HIB-CNQY47LA.js} +1 -1
- package/studio/web/assets/{diagram-MPIPVDR6-DaX9Y-N3.js → diagram-MPIPVDR6-7prveD1B.js} +1 -1
- package/studio/web/assets/{dist-Cz2Y6-6G.js → dist-BrRuMUzt.js} +1 -1
- package/studio/web/assets/{download-CtostjYn.js → download-Cn6tPG4L.js} +1 -1
- package/studio/web/assets/{ebnfDiagram-ZINNZB2B-CwAsENB1.js → ebnfDiagram-ZINNZB2B-5OnkTpfi.js} +1 -1
- package/studio/web/assets/{editor-CFQ5kbiT.js → editor-DKwRNgor.js} +1 -1
- package/studio/web/assets/editor.worker-Yd1NR0QT.js +33 -0
- package/studio/web/assets/{elk-276RUBZZ-Hexg9E4D.js → elk-276RUBZZ-DxDQPjQT.js} +1 -1
- package/studio/web/assets/{erDiagram-OPXOYQCR-CeC-wwO1.js → erDiagram-OPXOYQCR-CQlCX84D.js} +1 -1
- package/studio/web/assets/eventmodeling-NTZA5JFV-DONQf5aG.js +1 -0
- package/studio/web/assets/flowDiagram-KWPJA3E3-BoZ8_QR1.js +1 -0
- package/studio/web/assets/{ganttDiagram-FUAMR5RP-C2_Hhl1-.js → ganttDiagram-FUAMR5RP-bloX3Voy.js} +1 -1
- package/studio/web/assets/{getAnalytics-CjthMQqF.js → getAnalytics-C0FHjtdQ.js} +1 -1
- package/studio/web/assets/{gitGraph-4MIJSDKK-xIk5Vuew.js → gitGraph-4MIJSDKK-BrXWmJt2.js} +1 -1
- package/studio/web/assets/{gitGraphDiagram-X574FWY7-CMxJ34up.js → gitGraphDiagram-X574FWY7-C2KnnZeY.js} +1 -1
- package/studio/web/assets/{html-DdMBJEGu.js → html-KzEcNmGL.js} +1 -1
- package/studio/web/assets/{html.worker-DdwM4IrA.js → html.worker-B1dwY0Ay.js} +27 -27
- package/studio/web/assets/{htmlMode-DNtBx3OV.js → htmlMode-Bipij2Sy.js} +1 -1
- package/studio/web/assets/index-DMUQ34Jl.js +828 -0
- package/studio/web/assets/index-QLZw6xrU.css +1 -0
- package/studio/web/assets/index.lazy-D5_uqR4Q.js +2 -0
- package/studio/web/assets/{info-A6RAGUB7-o49TEbj-.js → info-A6RAGUB7-BhzBHe4U.js} +1 -1
- package/studio/web/assets/{infoDiagram-VRGFBTTK-Cjhh7yzZ.js → infoDiagram-VRGFBTTK-B0J1132p.js} +1 -1
- package/studio/web/assets/{ishikawaDiagram-OU5B5YK6-C63aXRmh.js → ishikawaDiagram-OU5B5YK6-BNk0tuQx.js} +1 -1
- package/studio/web/assets/{javascript-Dy6KbeHT.js → javascript-QytBwXUJ.js} +1 -1
- package/studio/web/assets/{journeyDiagram-ZHPQQLJL-D_o0P5Mu.js → journeyDiagram-ZHPQQLJL-Be79RMQM.js} +1 -1
- package/studio/web/assets/json.worker-BQqXGI3X.js +65 -0
- package/studio/web/assets/{jsonMode-BiuDVrzl.js → jsonMode-CIoh4zwS.js} +1 -1
- package/studio/web/assets/{kanban-definition-PNTS6WVX-CTI9SSxY.js → kanban-definition-PNTS6WVX-4uuoQP9W.js} +1 -1
- package/studio/web/assets/{languageServices-Dk8_6B25.js → languageServices-BvFCul4Y.js} +1 -1
- package/studio/web/assets/localStorageKeys-etzFY_xV.js +1 -0
- package/studio/web/assets/{lspLanguageFeatures-DwJKXzQa.js → lspLanguageFeatures-BvDuJHGH.js} +1 -1
- package/studio/web/assets/{mermaid-parser.core-C2lcH_jP.js → mermaid-parser.core-V1UYMeXV.js} +3 -3
- package/studio/web/assets/{mermaid.core-BG5bjfIG.js → mermaid.core-1PX6R49U.js} +4 -4
- package/studio/web/assets/{mindmap-definition-NLK3R4M7-BK7ho6JD.js → mindmap-definition-NLK3R4M7-C70Ctm0J.js} +1 -1
- package/studio/web/assets/notifications-C5EZskVd.js +1 -0
- package/studio/web/assets/notifications-jQfsyiVJ.js +1 -0
- package/studio/web/assets/{packet-AYTQ26CC-BHPy6KBV.js → packet-AYTQ26CC-DWbXHx5g.js} +1 -1
- package/studio/web/assets/{pegDiagram-GJSIUBJH-DzjwG3jO.js → pegDiagram-GJSIUBJH-d6z1cc7W.js} +1 -1
- package/studio/web/assets/{pie-WAS4IAKB-D0cP-DTU.js → pie-WAS4IAKB-DjoTm3yr.js} +1 -1
- package/studio/web/assets/{pieDiagram-5QR66LMP-C_hOP9Lv.js → pieDiagram-5QR66LMP-Dz3RHpDA.js} +1 -1
- package/studio/web/assets/profile-CkLzjQm2.js +1 -0
- package/studio/web/assets/{quadrantDiagram-O4NWA36T-CZW8PbV1.js → quadrantDiagram-O4NWA36T-CIF5Qkou.js} +1 -1
- package/studio/web/assets/{radar-RG4KPBEZ-C8fk0U81.js → radar-RG4KPBEZ-CVBgjCOj.js} +1 -1
- package/studio/web/assets/{railroad-74A4TZTK-B60FWm17.js → railroad-74A4TZTK-BdhXw10Q.js} +1 -1
- package/studio/web/assets/railroad-abnf-HS5TGJTU-BcoQPN39.js +1 -0
- package/studio/web/assets/railroad-ebnf-LZEXJU2U-B1Lke2JN.js +1 -0
- package/studio/web/assets/railroad-peg-WCYAUIDC-D-oJFkiV.js +1 -0
- package/studio/web/assets/{railroadDiagram-XR7U4H2S-DyyNmgbU.js → railroadDiagram-XR7U4H2S-y3Jv2U6N.js} +1 -1
- package/studio/web/assets/regions-b7HODz0P.js +1 -0
- package/studio/web/assets/register-DGmUOJc1.js +909 -0
- package/studio/web/assets/register-DjpsE8to.css +1 -0
- package/studio/web/assets/{requirementDiagram-PLB6GJNP-C4H-mLKu.js → requirementDiagram-PLB6GJNP-ChINZEmu.js} +1 -1
- package/studio/web/assets/restartGate-nHAFLDh-.js +1 -0
- package/studio/web/assets/{sankeyDiagram-IPEJSGJF-DHDyga4c.js → sankeyDiagram-IPEJSGJF-D9AyLce2.js} +1 -1
- package/studio/web/assets/{sequenceDiagram-PO4LG4MO-CdNcHXRI.js → sequenceDiagram-PO4LG4MO-hfk4aIEm.js} +1 -1
- package/studio/web/assets/{setComponentFile-BavlhfY0.js → setComponentFile-DlUcJ4Y0.js} +1 -1
- package/studio/web/assets/{setup-04yVzr2I.js → setup-D1jywSGk.js} +2 -2
- package/studio/web/assets/{stateDiagram-v2-GCMORJYK-CpKuXq15.js → stateDiagram-v2-GCMORJYK-Bs8RIb9K.js} +1 -1
- package/studio/web/assets/status-CjVoPieo.js +61 -0
- package/studio/web/assets/{swimlanes-2SLR337P-NRHa2D8I.js → swimlanes-2SLR337P-B52xrf6t.js} +1 -1
- package/studio/web/assets/swimlanesDiagram-TC7HE7FX-DfN7BAP5.js +8 -0
- package/studio/web/assets/tabs-BCTQzoQK.js +1 -0
- package/studio/web/assets/{timeline-definition-EJHVYXUP-D39Y-xpq.js → timeline-definition-EJHVYXUP-DJSITpQX.js} +1 -1
- package/studio/web/assets/toggleHighContrast-C9CKwOn9.js +51 -0
- package/studio/web/assets/toggleHighContrast-n7clI0si.css +1 -0
- package/studio/web/assets/{treeView-Q6P3EWNA-CRWR9OqD.js → treeView-Q6P3EWNA-C-epdxRo.js} +1 -1
- package/studio/web/assets/{treemap-WGGIJYW6-ZfXyJgLO.js → treemap-WGGIJYW6-DOVyp_zw.js} +1 -1
- package/studio/web/assets/{ts.worker-DDMUrpqh.js → ts.worker-Clm12uY6.js} +131 -131
- package/studio/web/assets/{tsMode-DGeWfx0W.js → tsMode-CRibSU_y.js} +1 -1
- package/studio/web/assets/{typescript-62_1FUbs.js → typescript-BRCpg8By.js} +1 -1
- package/studio/web/assets/{useEntityRestURL-DSVwev9J.js → useEntityRestURL-pPVmpjIi.js} +1 -1
- package/studio/web/assets/{usecaseDiagram-POWQR4AR-jf-zIDZ_.js → usecaseDiagram-POWQR4AR-B6252cbq.js} +1 -1
- package/studio/web/assets/{vendor-core-BdkdbUek.js → vendor-core-6LhfJlRC.js} +1 -1
- package/studio/web/assets/vendor-datadog-ozKf-pts.js +6 -0
- package/studio/web/assets/{vendor-react-izWchTRL.js → vendor-react-BOliS0BL.js} +1 -1
- package/studio/web/assets/vendor-tanstack-Bntfka-M.js +2 -0
- package/studio/web/assets/vendor-ui-9EYYDgoN.js +41 -0
- package/studio/web/assets/{vennDiagram-UO4OBE2U-BKYAZUh_.js → vennDiagram-UO4OBE2U-DZTsDA4k.js} +1 -1
- package/studio/web/assets/{wardley-WFR3VGLG-BO7f36ci.js → wardley-WFR3VGLG-Cfqoe6z4.js} +1 -1
- package/studio/web/assets/{wardleyDiagram-VNRHLVJA-Djb0JDQc.js → wardleyDiagram-VNRHLVJA-qu_f7Tvl.js} +1 -1
- package/studio/web/assets/{workers-Cs52GyHv.js → workers-BBDZZ4qw.js} +3 -3
- package/studio/web/assets/{xml-Co5ow_Ix.js → xml-tWNEwt7n.js} +1 -1
- package/studio/web/assets/{xychartDiagram-PMCCYNJV-BQh9ubvb.js → xychartDiagram-PMCCYNJV-UrOKRsmR.js} +1 -1
- package/studio/web/assets/{yaml-BcngpXye.js → yaml-BZgmBIf5.js} +1 -1
- package/studio/web/assets/{yaml.worker-DAK2o_H1.js → yaml.worker-D9JekGdp.js} +183 -183
- package/studio/web/index.html +15 -15
- package/studio/web/assets/FloatingChat-BUlsFGMV.js +0 -23
- package/studio/web/assets/alertDialog-UrbiMgQt.js +0 -1
- package/studio/web/assets/apiToken-CPhbc_zN.js +0 -1
- package/studio/web/assets/applications-D3Z6BVDl.js +0 -296
- package/studio/web/assets/architecture-7GRP2DOG-11l4Ny1l.js +0 -1
- package/studio/web/assets/authStore-5jSS5pTW.js +0 -3
- package/studio/web/assets/channel-BDtouPd6.js +0 -1
- package/studio/web/assets/dialog-DTepachC.js +0 -1
- package/studio/web/assets/editor.worker-CkkvdXvO.js +0 -33
- package/studio/web/assets/eventmodeling-NTZA5JFV-CFb03e0o.js +0 -1
- package/studio/web/assets/flowDiagram-KWPJA3E3-Cidi00_Y.js +0 -1
- package/studio/web/assets/index-CPoDzQTe.css +0 -1
- package/studio/web/assets/index-zZCxAIEb.js +0 -824
- package/studio/web/assets/index.lazy-CKbsHbSN.js +0 -2
- package/studio/web/assets/json.worker-CSi2hF-a.js +0 -65
- package/studio/web/assets/notifications-DJqAtWxD.js +0 -1
- package/studio/web/assets/notifications-DsDtb41c.js +0 -1
- package/studio/web/assets/profile-zTGUN51x.js +0 -1
- package/studio/web/assets/railroad-abnf-HS5TGJTU-CLMVZvH0.js +0 -1
- package/studio/web/assets/railroad-ebnf-LZEXJU2U--tlwFoVB.js +0 -1
- package/studio/web/assets/railroad-peg-WCYAUIDC-Dpuslblm.js +0 -1
- package/studio/web/assets/regions-SOtABPB9.js +0 -1
- package/studio/web/assets/register-7JezVakc.js +0 -848
- package/studio/web/assets/register-jjEx9u7D.css +0 -1
- package/studio/web/assets/setLocalStorage-D_kflv4U.js +0 -1
- package/studio/web/assets/status-g_mNYE7j.js +0 -61
- package/studio/web/assets/swimlanesDiagram-TC7HE7FX-BBk4t4hO.js +0 -8
- package/studio/web/assets/tabs-CnQ_zenf.js +0 -1
- package/studio/web/assets/toggleHighContrast--ixms01u.css +0 -1
- package/studio/web/assets/toggleHighContrast-i2FjCrzO.js +0 -49
- package/studio/web/assets/useLocalStorage-BYoZF60G.js +0 -1
- package/studio/web/assets/vendor-datadog-fCQhBHGk.js +0 -6
- package/studio/web/assets/vendor-tanstack-CQ1aRjVa.js +0 -2
- package/studio/web/assets/vendor-ui-aE1tHcW7.js +0 -41
- package/studio/web/assets/x-vYntCxm4.js +0 -1
package/security/keys.ts
CHANGED
|
@@ -150,6 +150,12 @@ const privateKeys = new Map();
|
|
|
150
150
|
// subscription and the private-key hot-reload trigger so both coalesce on the same cadence.
|
|
151
151
|
const TLS_REBUILD_DEBOUNCE_MS = 1500;
|
|
152
152
|
|
|
153
|
+
// Self-retry backoff cap: a permanently bad record must not cost every selector on every
|
|
154
|
+
// thread a table scan + X509 parse per debounce interval, forever.
|
|
155
|
+
const TLS_FAILURE_RETRY_MAX_DELAY_MS = 300_000;
|
|
156
|
+
// While a failure signature is unchanged, repeat occurrences log a summary at most this often.
|
|
157
|
+
const TLS_FAILURE_SUMMARY_INTERVAL_MS = 3_600_000;
|
|
158
|
+
|
|
153
159
|
// Debounced rebuild triggers, one per live server TLS selector (registered in createTLSSelector's
|
|
154
160
|
// initialize). When a private key is hot-reloaded on this thread, every live selector re-runs
|
|
155
161
|
// updateTLS so a secure context built with a stale key — or built before this key arrived — is
|
|
@@ -270,7 +276,9 @@ export function loadCertificates() {
|
|
|
270
276
|
return;
|
|
271
277
|
}
|
|
272
278
|
|
|
273
|
-
|
|
279
|
+
// Returned so loadAndWatch can roll back its mtime latch if the write fails —
|
|
280
|
+
// assigned as well to preserve loadCertificates()'s awaited-return contract.
|
|
281
|
+
return (promise = certificateTable.put({
|
|
274
282
|
name: certCn,
|
|
275
283
|
uses: config.uses ?? (configKey.includes('operations') ? ['operations-api'] : []),
|
|
276
284
|
ciphers: config.ciphers,
|
|
@@ -287,7 +295,7 @@ export function loadCertificates() {
|
|
|
287
295
|
valid_from: x509Cert.validFrom,
|
|
288
296
|
valid_to: x509Cert.validTo,
|
|
289
297
|
},
|
|
290
|
-
});
|
|
298
|
+
}));
|
|
291
299
|
},
|
|
292
300
|
ca ? 'certificate authority' : 'certificate'
|
|
293
301
|
);
|
|
@@ -340,18 +348,30 @@ const certificateWatchPollers = new Map<string, () => void>();
|
|
|
340
348
|
function loadAndWatch(path, loadCert, type) {
|
|
341
349
|
let lastModified;
|
|
342
350
|
const loadFile = (path, stats) => {
|
|
351
|
+
// The latch dedupes chokidar/poll but must mean "last successfully APPLIED", or a failed
|
|
352
|
+
// apply is deduplicated forever (#2382). Rollbacks are equality-guarded so an old failure
|
|
353
|
+
// can't unlatch a newer successful reload.
|
|
354
|
+
const previousModified = lastModified;
|
|
355
|
+
let modified;
|
|
343
356
|
try {
|
|
344
357
|
// chokidar's 'change' event omits stats unless alwaysStat is set (default off in v4), so
|
|
345
358
|
// stat the file here when it's missing — otherwise the inotify fast path would throw and
|
|
346
359
|
// silently never reload, leaving only the periodic poll to catch the change.
|
|
347
|
-
|
|
360
|
+
modified = (stats ?? statSync(path)).mtimeMs;
|
|
348
361
|
if (modified && modified !== lastModified) {
|
|
349
362
|
if (lastModified && isMainThread) logger.warn?.(`Reloading ${type}:`, path);
|
|
350
363
|
lastModified = modified;
|
|
351
|
-
loadCert(readPEM(path));
|
|
364
|
+
const applied = loadCert(readPEM(path));
|
|
365
|
+
if (typeof (applied as any)?.then === 'function') {
|
|
366
|
+
(applied as Promise<unknown>).catch((error) => {
|
|
367
|
+
logger.error?.(`Error applying ${type}:`, path, error);
|
|
368
|
+
if (lastModified === modified) lastModified = previousModified;
|
|
369
|
+
});
|
|
370
|
+
}
|
|
352
371
|
}
|
|
353
372
|
} catch (error) {
|
|
354
373
|
logger.error?.(`Error loading ${type}:`, path, error);
|
|
374
|
+
if (modified !== undefined && lastModified === modified) lastModified = previousModified;
|
|
355
375
|
}
|
|
356
376
|
};
|
|
357
377
|
if (fs.existsSync(path)) loadFile(path, statSync(path));
|
|
@@ -991,10 +1011,79 @@ export function createTLSSelector(type, mtlsOptions?, liveReload = true): any {
|
|
|
991
1011
|
let subscribedTable = null;
|
|
992
1012
|
let activeSubscription: Promise<any> | null = null;
|
|
993
1013
|
return ((SNICallback as any).ready = new Promise<void>((resolve, reject) => {
|
|
1014
|
+
// Pass-level failure before `.ready` settles still rejects (boot surfaces it); after,
|
|
1015
|
+
// the only safe response is to keep serving live state and retry.
|
|
1016
|
+
let readySettled = false;
|
|
1017
|
+
const settle = (value?) => {
|
|
1018
|
+
readySettled = true;
|
|
1019
|
+
resolve(value);
|
|
1020
|
+
};
|
|
1021
|
+
// Self-retry state for passes with per-record failures: the signature identifies the
|
|
1022
|
+
// failure set, the delay backs off while it is unchanged, and summaries are rate-limited.
|
|
1023
|
+
let failureSignature = '';
|
|
1024
|
+
let failureRetryDelay = TLS_REBUILD_DEBOUNCE_MS;
|
|
1025
|
+
let failureRetryTimer;
|
|
1026
|
+
let failureRetryCount = 0;
|
|
1027
|
+
let failureLastSummaryAt = 0;
|
|
1028
|
+
const scheduleFailureRetry = () => {
|
|
1029
|
+
// An armed external rebuild (or an armed retry) will run a pass that re-arms if needed.
|
|
1030
|
+
if (failureRetryTimer || rebuildTimer) return;
|
|
1031
|
+
failureRetryTimer = setTimeout(() => {
|
|
1032
|
+
failureRetryTimer = undefined;
|
|
1033
|
+
updateTLS();
|
|
1034
|
+
}, failureRetryDelay).unref();
|
|
1035
|
+
failureRetryDelay = Math.min(failureRetryDelay * 2, TLS_FAILURE_RETRY_MAX_DELAY_MS);
|
|
1036
|
+
};
|
|
1037
|
+
// Log each failure on a new/changed failure set; while unchanged, stay silent apart from a
|
|
1038
|
+
// rate-limited summary — an un-throttled 1.5s retry would emit ~57k lines/day per selector.
|
|
1039
|
+
const reportFailures = (failures: { cert: any; error: any }[]) => {
|
|
1040
|
+
const signature = failures
|
|
1041
|
+
.map(({ cert, error }) => `${cert.name}: ${error?.message}`)
|
|
1042
|
+
.sort()
|
|
1043
|
+
.join('; ');
|
|
1044
|
+
if (signature !== failureSignature) {
|
|
1045
|
+
failureSignature = signature;
|
|
1046
|
+
failureRetryDelay = TLS_REBUILD_DEBOUNCE_MS;
|
|
1047
|
+
failureRetryCount = 0;
|
|
1048
|
+
failureLastSummaryAt = Date.now();
|
|
1049
|
+
// A timer armed for the previous signature would run the next pass on its stale
|
|
1050
|
+
// (possibly maxed-out) delay; re-arm fresh for the new failure set.
|
|
1051
|
+
if (failureRetryTimer) {
|
|
1052
|
+
clearTimeout(failureRetryTimer);
|
|
1053
|
+
failureRetryTimer = undefined;
|
|
1054
|
+
}
|
|
1055
|
+
for (const { cert, error } of failures)
|
|
1056
|
+
logger.error?.('Error applying TLS for', cert.name, `on the '${type}' listener`, error);
|
|
1057
|
+
} else {
|
|
1058
|
+
failureRetryCount++;
|
|
1059
|
+
// Stays at error: a stuck rotation must keep an alertable signal — the retained cert
|
|
1060
|
+
// is aging toward its notAfter the whole time.
|
|
1061
|
+
if (Date.now() - failureLastSummaryAt >= TLS_FAILURE_SUMMARY_INTERVAL_MS) {
|
|
1062
|
+
failureLastSummaryAt = Date.now();
|
|
1063
|
+
logger.error?.(
|
|
1064
|
+
`TLS rebuild for the '${type}' listener is still failing for [${failures
|
|
1065
|
+
.map(({ cert }) => cert.name)
|
|
1066
|
+
.join(', ')}] after ${failureRetryCount} retries; serving the last good state`
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1071
|
+
const clearFailureState = () => {
|
|
1072
|
+
if (failureSignature) {
|
|
1073
|
+
logger.warn?.(`TLS rebuild for the '${type}' listener recovered; all certificates applied`);
|
|
1074
|
+
}
|
|
1075
|
+
failureSignature = '';
|
|
1076
|
+
failureRetryDelay = TLS_REBUILD_DEBOUNCE_MS;
|
|
1077
|
+
failureRetryCount = 0;
|
|
1078
|
+
if (failureRetryTimer) {
|
|
1079
|
+
clearTimeout(failureRetryTimer);
|
|
1080
|
+
failureRetryTimer = undefined;
|
|
1081
|
+
}
|
|
1082
|
+
};
|
|
994
1083
|
function updateTLS() {
|
|
995
1084
|
try {
|
|
996
1085
|
if (databases === undefined) {
|
|
997
|
-
|
|
1086
|
+
settle();
|
|
998
1087
|
return;
|
|
999
1088
|
}
|
|
1000
1089
|
if (databases.system?.hdb_certificate === undefined) {
|
|
@@ -1024,19 +1113,19 @@ export function createTLSSelector(type, mtlsOptions?, liveReload = true): any {
|
|
|
1024
1113
|
scheduleRebuild();
|
|
1025
1114
|
return;
|
|
1026
1115
|
}
|
|
1027
|
-
//
|
|
1028
|
-
//
|
|
1029
|
-
//
|
|
1030
|
-
|
|
1031
|
-
|
|
1116
|
+
// Transactional publication (#2382): build the whole replacement state off to the
|
|
1117
|
+
// side and reconcile the live maps only after the pass completes, so a failure of
|
|
1118
|
+
// any shape leaves the currently-served state intact.
|
|
1119
|
+
const candidateContexts = new Map();
|
|
1120
|
+
const candidateCAs = new Map();
|
|
1121
|
+
let candidateHasWildcards = false;
|
|
1032
1122
|
let bestQuality = 0;
|
|
1033
|
-
|
|
1034
|
-
//
|
|
1035
|
-
//
|
|
1036
|
-
//
|
|
1037
|
-
// forever, which would let a later transient zero-cert rebuild skip the retry and
|
|
1038
|
-
// publish an empty certificates list (the #1998 symptom) on the post-boot path.
|
|
1123
|
+
let candidateDefault;
|
|
1124
|
+
// Whether THIS pass produced a default. The `defaultContext` closure variable is never
|
|
1125
|
+
// reset (a transient zero-cert pass keeps serving the prior default), so keying the
|
|
1126
|
+
// zero-cert retry off it would republish empty post-boot — the #1998 symptom.
|
|
1039
1127
|
let defaultContextSetThisPass = false;
|
|
1128
|
+
const failedThisPass: { cert: any; error: any }[] = [];
|
|
1040
1129
|
// Track the actual table instance, not just whether we've ever subscribed: resetDatabases()
|
|
1041
1130
|
// (copy_db, ITC restart handling) replaces databases.system.hdb_certificate with a new
|
|
1042
1131
|
// table object, and a boolean flag would never re-subscribe to it, permanently losing
|
|
@@ -1066,24 +1155,28 @@ export function createTLSSelector(type, mtlsOptions?, liveReload = true): any {
|
|
|
1066
1155
|
previousSubscription.then((subscription: any) => subscription?.end?.()).catch(() => {});
|
|
1067
1156
|
}
|
|
1068
1157
|
}
|
|
1069
|
-
|
|
1158
|
+
// One snapshot drives both loops: separate search() calls own separate read
|
|
1159
|
+
// snapshots, and a write committing between them could publish a renewed leaf
|
|
1160
|
+
// against the previous CA set.
|
|
1161
|
+
const certRecords = Array.from(databases.system.hdb_certificate.search([])) as any[];
|
|
1162
|
+
for (const cert of certRecords) {
|
|
1070
1163
|
const certificate = cert.certificate;
|
|
1071
1164
|
let certParsed;
|
|
1072
1165
|
try {
|
|
1073
1166
|
certParsed = new X509Certificate(certificate);
|
|
1074
1167
|
} catch (error) {
|
|
1075
|
-
//
|
|
1076
|
-
//
|
|
1077
|
-
|
|
1168
|
+
// A pass failure like any other (throttle + retry). Its caCerts entry can't be
|
|
1169
|
+
// retained — the subject is unrecoverable from a PEM that won't parse.
|
|
1170
|
+
failedThisPass.push({ cert, error });
|
|
1078
1171
|
continue;
|
|
1079
1172
|
}
|
|
1080
1173
|
if (cert.is_authority) {
|
|
1081
1174
|
(certParsed as any).asString = certificate;
|
|
1082
|
-
|
|
1175
|
+
candidateCAs.set(certParsed.subject, certificate);
|
|
1083
1176
|
}
|
|
1084
1177
|
}
|
|
1085
1178
|
|
|
1086
|
-
for (const cert of
|
|
1179
|
+
for (const cert of certRecords) {
|
|
1087
1180
|
try {
|
|
1088
1181
|
if (cert.is_authority) {
|
|
1089
1182
|
continue;
|
|
@@ -1101,8 +1194,8 @@ export function createTLSSelector(type, mtlsOptions?, liveReload = true): any {
|
|
|
1101
1194
|
|
|
1102
1195
|
let certificate = cert.certificate;
|
|
1103
1196
|
const certParsed = new X509Certificate(certificate);
|
|
1104
|
-
if (
|
|
1105
|
-
certificate += '\n' +
|
|
1197
|
+
if (candidateCAs.has(certParsed.issuer)) {
|
|
1198
|
+
certificate += '\n' + candidateCAs.get(certParsed.issuer);
|
|
1106
1199
|
}
|
|
1107
1200
|
if (!private_key || !certificate) {
|
|
1108
1201
|
throw new Error('Missing private key or certificate for secure server');
|
|
@@ -1110,8 +1203,10 @@ export function createTLSSelector(type, mtlsOptions?, liveReload = true): any {
|
|
|
1110
1203
|
const secureOptions = {
|
|
1111
1204
|
ciphers: cert.ciphers,
|
|
1112
1205
|
ticketKeys: getTicketKeys(),
|
|
1113
|
-
|
|
1114
|
-
|
|
1206
|
+
// the live map, not the candidate: its identity survives publication, so contexts
|
|
1207
|
+
// keep seeing the current CA set after later rebuilds
|
|
1208
|
+
availableCAs: caCerts,
|
|
1209
|
+
ca: mtlsOptions && Array.from(candidateCAs.values()),
|
|
1115
1210
|
cert: certificate,
|
|
1116
1211
|
key: private_key,
|
|
1117
1212
|
key_file: cert.private_key_name,
|
|
@@ -1127,7 +1222,7 @@ export function createTLSSelector(type, mtlsOptions?, liveReload = true): any {
|
|
|
1127
1222
|
(secureContext as any).name = cert.name;
|
|
1128
1223
|
(secureContext as any).options = secureOptions;
|
|
1129
1224
|
(secureContext as any).quality = quality;
|
|
1130
|
-
(secureContext as any).certificateAuthorities = Array.from(
|
|
1225
|
+
(secureContext as any).certificateAuthorities = Array.from(candidateCAs);
|
|
1131
1226
|
// we store the first 100 bytes of the certificate just for debug logging
|
|
1132
1227
|
(secureContext as any).certStart = certificate.toString().slice(0, 100);
|
|
1133
1228
|
// we want to configure SNI handling to pick the right certificate based on all the registered SANs
|
|
@@ -1135,14 +1230,14 @@ export function createTLSSelector(type, mtlsOptions?, liveReload = true): any {
|
|
|
1135
1230
|
for (let hostname of hostnames) {
|
|
1136
1231
|
if (hostname) {
|
|
1137
1232
|
if (hostname[0] === '*') {
|
|
1138
|
-
|
|
1233
|
+
candidateHasWildcards = true;
|
|
1139
1234
|
hostname = hostname.slice(1);
|
|
1140
1235
|
}
|
|
1141
1236
|
// we use this certificate if it has a higher quality than the existing one for this hostname
|
|
1142
|
-
let existingCertQuality =
|
|
1237
|
+
let existingCertQuality = candidateContexts.get(hostname)?.quality ?? 0;
|
|
1143
1238
|
logger.trace?.('Assigning TLS for hostname', hostname, 'if', quality, '>', existingCertQuality);
|
|
1144
1239
|
if (quality > existingCertQuality) {
|
|
1145
|
-
|
|
1240
|
+
candidateContexts.set(hostname, secureContext);
|
|
1146
1241
|
}
|
|
1147
1242
|
} else {
|
|
1148
1243
|
logger.error?.('No hostname found for certificate at', (tls as any).certificate);
|
|
@@ -1164,57 +1259,141 @@ export function createTLSSelector(type, mtlsOptions?, liveReload = true): any {
|
|
|
1164
1259
|
);
|
|
1165
1260
|
if (quality > bestQuality /* && hasIpAddress*/) {
|
|
1166
1261
|
// we use this certificate as the default if it has a higher quality than the existing one
|
|
1167
|
-
|
|
1262
|
+
candidateDefault = secureContext;
|
|
1168
1263
|
defaultContextSetThisPass = true;
|
|
1169
1264
|
bestQuality = quality;
|
|
1170
|
-
if (server) {
|
|
1171
|
-
server.defaultContext = secureContext;
|
|
1172
|
-
// note that we can not set the secure context on the server here, because this creates an
|
|
1173
|
-
// indeterminate situation of whether openssl will use this certificate or the one from the SNI
|
|
1174
|
-
// callback
|
|
1175
|
-
//server.setSecureContext?.(server, secureOptions);
|
|
1176
|
-
}
|
|
1177
1265
|
}
|
|
1178
1266
|
} catch (error) {
|
|
1179
|
-
|
|
1267
|
+
failedThisPass.push({ cert, error });
|
|
1180
1268
|
}
|
|
1181
1269
|
}
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
//
|
|
1202
|
-
//
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1270
|
+
|
|
1271
|
+
// Retain-last-good: a record still in the table whose build failed keeps its live
|
|
1272
|
+
// hostname entries and its default candidacy; deletion remains the way to drop them.
|
|
1273
|
+
// On an mTLS listener whose CA set changed, the retained pair is rebuilt against the
|
|
1274
|
+
// current trust material — new handshakes never see stale trust (established sessions
|
|
1275
|
+
// and outstanding tickets are unaffected, as on any build); if the rebuild fails the entries drop
|
|
1276
|
+
// unless nothing else is servable (the zero-cert guard below then keeps the old
|
|
1277
|
+
// state). Full contract in DESIGN.md "TLS hot-reload".
|
|
1278
|
+
const caSetUnchanged = (previous) => {
|
|
1279
|
+
const builtWith = (previous as any).certificateAuthorities;
|
|
1280
|
+
if (!Array.isArray(builtWith)) return candidateCAs.size === 0;
|
|
1281
|
+
return (
|
|
1282
|
+
builtWith.length === candidateCAs.size &&
|
|
1283
|
+
builtWith.every(([subject, pem]) => candidateCAs.get(subject) === pem)
|
|
1284
|
+
);
|
|
1285
|
+
};
|
|
1286
|
+
const rebuiltRetentions = new Map();
|
|
1287
|
+
const retentionFailures: { cert: any; error: any }[] = [];
|
|
1288
|
+
const retainable = (previous) => {
|
|
1289
|
+
// Without mTLS nothing consulted depends on the CA set — skip the rebuild, but
|
|
1290
|
+
// refresh the CA bookkeeping mirrored into socket metadata for fronting proxies.
|
|
1291
|
+
if (!mtlsOptions || caSetUnchanged(previous)) {
|
|
1292
|
+
(previous as any).certificateAuthorities = Array.from(candidateCAs);
|
|
1293
|
+
return previous;
|
|
1294
|
+
}
|
|
1295
|
+
if (rebuiltRetentions.has(previous)) return rebuiltRetentions.get(previous);
|
|
1296
|
+
try {
|
|
1297
|
+
const secureOptions = {
|
|
1298
|
+
...(previous as any).options,
|
|
1299
|
+
ticketKeys: getTicketKeys(),
|
|
1300
|
+
availableCAs: caCerts,
|
|
1301
|
+
ca: mtlsOptions && Array.from(candidateCAs.values()),
|
|
1302
|
+
};
|
|
1303
|
+
const rebuilt = tls.createSecureContext(secureOptions);
|
|
1304
|
+
(rebuilt as any).name = (previous as any).name;
|
|
1305
|
+
(rebuilt as any).options = secureOptions;
|
|
1306
|
+
(rebuilt as any).quality = (previous as any).quality;
|
|
1307
|
+
(rebuilt as any).certificateAuthorities = Array.from(candidateCAs);
|
|
1308
|
+
(rebuilt as any).certStart = (previous as any).certStart;
|
|
1309
|
+
rebuiltRetentions.set(previous, rebuilt);
|
|
1310
|
+
return rebuilt;
|
|
1311
|
+
} catch (error) {
|
|
1312
|
+
retentionFailures.push({
|
|
1313
|
+
cert: { name: `${(previous as any).name} (retained-context rebuild)` },
|
|
1314
|
+
error,
|
|
1315
|
+
});
|
|
1316
|
+
rebuiltRetentions.set(previous, undefined);
|
|
1317
|
+
return undefined;
|
|
1318
|
+
}
|
|
1319
|
+
};
|
|
1320
|
+
for (const { cert } of failedThisPass) {
|
|
1321
|
+
// Ties go to the incumbent: a hostname the retained context already owned, and the
|
|
1322
|
+
// live default when it is the retained record, must not flip to an equal-quality
|
|
1323
|
+
// sibling because of a transient failure. A context retained only for a hostname
|
|
1324
|
+
// still needs strict > to become the default.
|
|
1325
|
+
const retain = (previous, hostname?) => {
|
|
1326
|
+
const retained = retainable(previous);
|
|
1327
|
+
if (!retained) return;
|
|
1328
|
+
const previousQuality = (retained as any).quality ?? 0;
|
|
1329
|
+
if (hostname !== undefined) {
|
|
1330
|
+
if (previousQuality >= ((candidateContexts.get(hostname) as any)?.quality ?? 0)) {
|
|
1331
|
+
candidateContexts.set(hostname, retained);
|
|
1332
|
+
}
|
|
1333
|
+
if (hostname[0] === '.') candidateHasWildcards = true;
|
|
1334
|
+
}
|
|
1335
|
+
const winsDefault =
|
|
1336
|
+
previous === defaultContext ? previousQuality >= bestQuality : previousQuality > bestQuality;
|
|
1337
|
+
if (winsDefault) {
|
|
1338
|
+
candidateDefault = retained;
|
|
1339
|
+
defaultContextSetThisPass = true;
|
|
1340
|
+
bestQuality = previousQuality;
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
for (const [hostname, previous] of secureContexts) {
|
|
1344
|
+
if ((previous as any).name === cert.name) retain(previous, hostname);
|
|
1345
|
+
}
|
|
1346
|
+
if ((defaultContext as any)?.name === cert.name) retain(defaultContext);
|
|
1347
|
+
}
|
|
1348
|
+
if (liveReload && candidateContexts.size === 0 && !defaultContextSetThisPass) {
|
|
1349
|
+
// Every row failed to apply (e.g. keys not yet on this thread). Publishing or
|
|
1350
|
+
// resolving would write an empty `certificates:` list (the #1998 symptom), so keep
|
|
1351
|
+
// live state and retry. `!defaultContextSetThisPass`, not the persistent
|
|
1352
|
+
// `defaultContext`: a cert with no usable hostnames still sets a serviceable
|
|
1353
|
+
// default (must resolve, not retry forever), while the closure variable is truthy
|
|
1354
|
+
// forever after the first success. Gated on liveReload: transient selectors
|
|
1355
|
+
// (getReplicationCert) legitimately resolve empty — bootstrap depends on that
|
|
1356
|
+
// meaning "no cert yet". Warn is latched: unlatched would emit ~57k lines/day.
|
|
1207
1357
|
if (server && !server.tlsSelectorWarnedZeroCerts) {
|
|
1208
1358
|
server.tlsSelectorWarnedZeroCerts = true;
|
|
1209
1359
|
logger.warn?.(
|
|
1210
1360
|
`TLS selector for the '${type}' listener resolved zero certificates; retrying every ${TLS_REBUILD_DEBOUNCE_MS}ms`
|
|
1211
1361
|
);
|
|
1212
1362
|
}
|
|
1213
|
-
|
|
1363
|
+
if (failedThisPass.length > 0) {
|
|
1364
|
+
// Surface WHY the pass came up empty and put the retry on the backoff.
|
|
1365
|
+
reportFailures(failedThisPass.concat(retentionFailures));
|
|
1366
|
+
scheduleFailureRetry();
|
|
1367
|
+
} else {
|
|
1368
|
+
scheduleRebuild();
|
|
1369
|
+
}
|
|
1214
1370
|
return;
|
|
1215
1371
|
}
|
|
1372
|
+
// Publish: reconcile the live maps in place (their identity is aliased by
|
|
1373
|
+
// server.secureContexts and every context's availableCAs) and advance all default
|
|
1374
|
+
// references together so SNI and non-SNI traffic can't diverge.
|
|
1375
|
+
secureContexts.clear();
|
|
1376
|
+
for (const [hostname, context] of candidateContexts) secureContexts.set(hostname, context);
|
|
1377
|
+
caCerts.clear();
|
|
1378
|
+
for (const [subject, certificate] of candidateCAs) caCerts.set(subject, certificate);
|
|
1216
1379
|
// only listener selectors publish: a one-shot client selector's pass may see no authority rows
|
|
1217
1380
|
if (liveReload) publishTrustedAuthorities(caCerts.values());
|
|
1381
|
+
hasWildcards = candidateHasWildcards;
|
|
1382
|
+
if (candidateDefault) {
|
|
1383
|
+
(SNICallback as any).defaultContext = defaultContext = candidateDefault;
|
|
1384
|
+
// note that we can not set the secure context on the server here, because this creates an
|
|
1385
|
+
// indeterminate situation of whether openssl will use this certificate or the one from the SNI
|
|
1386
|
+
// callback
|
|
1387
|
+
if (server) server.defaultContext = candidateDefault;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
if (failedThisPass.length > 0) {
|
|
1391
|
+
reportFailures(failedThisPass.concat(retentionFailures));
|
|
1392
|
+
scheduleFailureRetry();
|
|
1393
|
+
} else {
|
|
1394
|
+
clearFailureState();
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1218
1397
|
// A successful pass ends any warn latches so a later recurrence logs again.
|
|
1219
1398
|
if (server) {
|
|
1220
1399
|
server.tlsSelectorWaitedForSystemDb = false;
|
|
@@ -1240,8 +1419,14 @@ export function createTLSSelector(type, mtlsOptions?, liveReload = true): any {
|
|
|
1240
1419
|
}
|
|
1241
1420
|
}
|
|
1242
1421
|
server?.secureContextsListeners.forEach((listener) => listener());
|
|
1243
|
-
|
|
1422
|
+
settle(defaultContext);
|
|
1244
1423
|
} catch (error) {
|
|
1424
|
+
if (readySettled) {
|
|
1425
|
+
// Live state is untouched (candidates are pass-local): keep serving it and retry.
|
|
1426
|
+
reportFailures([{ cert: { name: `(${type} rebuild pass)` }, error }]);
|
|
1427
|
+
scheduleFailureRetry();
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1245
1430
|
reject(error);
|
|
1246
1431
|
}
|
|
1247
1432
|
}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const { isMainThread, parentPort, threadId, workerData } = require('node:worker_threads');
|
|
4
4
|
const { createServer: createSocketServer } = require('node:net');
|
|
5
|
-
const { unlinkSync, existsSync, mkdirSync } = require('fs');
|
|
6
|
-
const { join } = require('path');
|
|
5
|
+
const { unlinkSync, existsSync, mkdirSync, renameSync } = require('node:fs');
|
|
6
|
+
const { join, dirname } = require('node:path');
|
|
7
7
|
let componentsLoadedResolve;
|
|
8
8
|
exports.whenComponentsLoaded = new Promise((resolve) => {
|
|
9
9
|
componentsLoadedResolve = resolve;
|
|
@@ -259,22 +259,35 @@ function startServers() {
|
|
|
259
259
|
* An overlong path is the only condition that can skip a domain-socket listener. Check before
|
|
260
260
|
* listen() because supported Node versions differ: some reject the path while others truncate and
|
|
261
261
|
* bind it somewhere clients cannot reach using the configured path.
|
|
262
|
+
*
|
|
263
|
+
* A per-thread mirror binds at a temp name and is renamed over its published path: libuv unlinks a
|
|
264
|
+
* pipe server's bound path on close whoever owns it by then (server/DESIGN.md).
|
|
262
265
|
*/
|
|
263
266
|
function listenOnDomainSocket(port, server) {
|
|
264
|
-
|
|
267
|
+
const bindPath = server.isPerThreadSocket && !isWindows ? mirrorBindPath(port) : port;
|
|
268
|
+
const overlong = [port, bindPath].find((path) => isDomainSocketPathTooLong(path));
|
|
269
|
+
if (overlong) {
|
|
265
270
|
httpComponent.markUdsBindFailed(port);
|
|
266
271
|
harperLogger.error(
|
|
267
|
-
`Not binding domain socket listener${server.name ? ` for '${server.name}'` : ''} at ${port}: the ${Buffer.byteLength(
|
|
272
|
+
`Not binding domain socket listener${server.name ? ` for '${server.name}'` : ''} at ${port}: the ${Buffer.byteLength(overlong)}-byte path ${overlong} exceeds the platform limit of ${getDomainSocketPathMaxBytes()} bytes. Continuing without this domain socket.`
|
|
268
273
|
);
|
|
269
274
|
return Promise.resolve({ port, failed: true });
|
|
270
275
|
}
|
|
271
|
-
if (existsSync(port)) unlinkSync(port);
|
|
276
|
+
if (bindPath === port && existsSync(port)) unlinkSync(port);
|
|
272
277
|
return new Promise((resolve, reject) => {
|
|
273
278
|
function onError(error) {
|
|
274
279
|
reject(error);
|
|
275
280
|
}
|
|
276
281
|
function onListening() {
|
|
277
282
|
server.removeListener('error', onError);
|
|
283
|
+
if (bindPath !== port) {
|
|
284
|
+
try {
|
|
285
|
+
renameSync(bindPath, port);
|
|
286
|
+
} catch (error) {
|
|
287
|
+
server.close(); // libuv unlinks the temp name, which still exists
|
|
288
|
+
return reject(error);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
278
291
|
// Record ownership of the inode we just bound, so cleanupUdsFiles()/markUdsBindFailed()
|
|
279
292
|
// (see http.ts) can tell this worker's own file apart from a replacement's later rebind
|
|
280
293
|
// at the same path (see registerUdsCleanupPaths). A no-op for domain sockets that aren't
|
|
@@ -285,7 +298,7 @@ function listenOnDomainSocket(port, server) {
|
|
|
285
298
|
}
|
|
286
299
|
try {
|
|
287
300
|
server.once('error', onError);
|
|
288
|
-
server.listen({ path:
|
|
301
|
+
server.listen({ path: bindPath }, onListening);
|
|
289
302
|
} catch (error) {
|
|
290
303
|
server.removeListener('error', onError);
|
|
291
304
|
reject(error);
|
|
@@ -293,6 +306,11 @@ function listenOnDomainSocket(port, server) {
|
|
|
293
306
|
});
|
|
294
307
|
}
|
|
295
308
|
|
|
309
|
+
let mirrorBindSequence = 0;
|
|
310
|
+
function mirrorBindPath(socketPath) {
|
|
311
|
+
return join(dirname(socketPath), `.${threadId}.${++mirrorBindSequence}`);
|
|
312
|
+
}
|
|
313
|
+
|
|
296
314
|
let listening;
|
|
297
315
|
function listenOnPorts() {
|
|
298
316
|
if (isBun) return listenOnPortsBun();
|