@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.
Files changed (179) hide show
  1. package/dist/resources/analytics/write.d.ts +3 -0
  2. package/dist/resources/analytics/write.js +39 -13
  3. package/dist/resources/analytics/write.js.map +1 -1
  4. package/dist/security/keys.js +258 -66
  5. package/dist/security/keys.js.map +1 -1
  6. package/dist/server/threads/threadServer.d.ts +3 -0
  7. package/dist/server/threads/threadServer.js +24 -6
  8. package/dist/server/threads/threadServer.js.map +1 -1
  9. package/npm-shrinkwrap.json +2 -2
  10. package/package.json +1 -1
  11. package/resources/analytics/write.ts +40 -14
  12. package/security/keys.ts +251 -66
  13. package/server/threads/threadServer.js +24 -6
  14. package/studio/web/assets/{Chat-C97PMlIn.js → Chat-CeEDA28S.js} +13 -13
  15. package/studio/web/assets/FloatingChat-J_P5KaR4.js +23 -0
  16. package/studio/web/assets/SearchDialog-BWMGseDd.js +1 -0
  17. package/studio/web/assets/{abnfDiagram-O67JEVCF-DpPZWMOf.js → abnfDiagram-O67JEVCF-raXE027J.js} +1 -1
  18. package/studio/web/assets/alertDialog-BbWD9Xxa.js +1 -0
  19. package/studio/web/assets/apiToken-BKot-xR5.js +1 -0
  20. package/studio/web/assets/applications-BlJChLsS.js +296 -0
  21. package/studio/web/assets/architecture-7GRP2DOG-DDmz3hpI.js +1 -0
  22. package/studio/web/assets/{architectureDiagram-NJMV4G6O-OmgLraID.js → architectureDiagram-NJMV4G6O-C8_Eet7w.js} +1 -1
  23. package/studio/web/assets/arrow-up-D2g_57vT.js +1 -0
  24. package/studio/web/assets/authStore-CJCaADw9.js +3 -0
  25. package/studio/web/assets/{blockDiagram-BEXU5L5S-Cv7CD3Ai.js → blockDiagram-BEXU5L5S-DR6S5-1D.js} +1 -1
  26. package/studio/web/assets/{button-MFP9n8Ck.js → button-DWwfgGWI.js} +2 -2
  27. package/studio/web/assets/{c4Diagram-YGBWAQC7-Ds1vKYnN.js → c4Diagram-YGBWAQC7-BAfZ1G01.js} +1 -1
  28. package/studio/web/assets/channel-D0AkPjJ8.js +1 -0
  29. package/studio/web/assets/{chevron-up-CFe8x5Bp.js → chevron-up-CFxbMGv2.js} +1 -1
  30. package/studio/web/assets/{chunk-3FUC2YCW-DpnBPH8B.js → chunk-3FUC2YCW-DuRp-j19.js} +1 -1
  31. package/studio/web/assets/{chunk-5DYCD2WN-LLLCQSzg.js → chunk-5DYCD2WN-BceBfIHT.js} +1 -1
  32. package/studio/web/assets/{chunk-742MDFTN-BL8J_pIO.js → chunk-742MDFTN-CIkt8ijt.js} +1 -1
  33. package/studio/web/assets/{chunk-7INBJB4K-C-hJHiiK.js → chunk-7INBJB4K-BpeAXyOi.js} +1 -1
  34. package/studio/web/assets/{chunk-7M6MHVWA-C6ZqkFbA.js → chunk-7M6MHVWA-h6WlaTzO.js} +1 -1
  35. package/studio/web/assets/{chunk-7PRAP22T-CUXM8BG6.js → chunk-7PRAP22T-KyqX6x3f.js} +1 -1
  36. package/studio/web/assets/{chunk-GTNCS2PH-DJ2BFvS1.js → chunk-GTNCS2PH-DGHLXyow.js} +1 -1
  37. package/studio/web/assets/{chunk-GWA4HPMP-C42bSqLL.js → chunk-GWA4HPMP-DsejB6gA.js} +1 -1
  38. package/studio/web/assets/{chunk-MBY4JIJT-C-5jz6Lo.js → chunk-MBY4JIJT-BwSGweQP.js} +1 -1
  39. package/studio/web/assets/{chunk-NETBCI7D-D-bzlN4r.js → chunk-NETBCI7D-C8UnOw_f.js} +1 -1
  40. package/studio/web/assets/{chunk-O7XYJQB3-ep14zA23.js → chunk-O7XYJQB3-CBS3nZYV.js} +1 -1
  41. package/studio/web/assets/{chunk-UA2S7LBM-BMRWRHED.js → chunk-UA2S7LBM-BHpEdH8D.js} +1 -1
  42. package/studio/web/assets/{chunk-WEXAMYUT-DdLYVdX1.js → chunk-WEXAMYUT-BAIhTwyh.js} +1 -1
  43. package/studio/web/assets/{chunk-Z7XXMR3K-BmKEDjcP.js → chunk-Z7XXMR3K-DSHbImdf.js} +1 -1
  44. package/studio/web/assets/{chunk-ZIGJFQKS-DLZgdNxA.js → chunk-ZIGJFQKS-Cs8fUn7y.js} +1 -1
  45. package/studio/web/assets/{classDiagram-v2-NBCMYWYE-DOI5vgKT.js → classDiagram-v2-NBCMYWYE-DmDPkt2u.js} +1 -1
  46. package/studio/web/assets/cn-6T8VranJ.js +1 -0
  47. package/studio/web/assets/{codicon-Brq4_Ui5.ttf → codicon-DF1abBS2.ttf} +0 -0
  48. package/studio/web/assets/{createLucideIcon-bziYxJli.js → createLucideIcon-fO5dL7D5.js} +1 -1
  49. package/studio/web/assets/{css.worker-l1psR_wG.js → css.worker-DoT-OOzx.js} +36 -36
  50. package/studio/web/assets/{cssMode-DyLZPMjo.js → cssMode-DjeA9Hx_.js} +1 -1
  51. package/studio/web/assets/{cynefin-OW5HDTMX-BSW-DB7S.js → cynefin-OW5HDTMX-DIvbXLSj.js} +1 -1
  52. package/studio/web/assets/{cynefinDiagram-VND7K2PF-CbAHEwxt.js → cynefinDiagram-VND7K2PF-BdtaXiCE.js} +1 -1
  53. package/studio/web/assets/{dagre-6A5THRUB-Csxewgs4.js → dagre-6A5THRUB-C7RUWHyz.js} +1 -1
  54. package/studio/web/assets/{diagram-22UHCM2B-a0rt8tY4.js → diagram-22UHCM2B-DJCvcaWP.js} +1 -1
  55. package/studio/web/assets/{diagram-3UASUU5V-lEj2SVNh.js → diagram-3UASUU5V-Du6VKWln.js} +1 -1
  56. package/studio/web/assets/{diagram-ATOU4E4O-HcrAeB-q.js → diagram-ATOU4E4O-dxRfeG4B.js} +1 -1
  57. package/studio/web/assets/{diagram-CDSNMT55-CYxiuyEx.js → diagram-CDSNMT55-l5Ga0u-W.js} +1 -1
  58. package/studio/web/assets/{diagram-MLGK6HIB-ChbCJTgJ.js → diagram-MLGK6HIB-CNQY47LA.js} +1 -1
  59. package/studio/web/assets/{diagram-MPIPVDR6-DaX9Y-N3.js → diagram-MPIPVDR6-7prveD1B.js} +1 -1
  60. package/studio/web/assets/{dist-Cz2Y6-6G.js → dist-BrRuMUzt.js} +1 -1
  61. package/studio/web/assets/{download-CtostjYn.js → download-Cn6tPG4L.js} +1 -1
  62. package/studio/web/assets/{ebnfDiagram-ZINNZB2B-CwAsENB1.js → ebnfDiagram-ZINNZB2B-5OnkTpfi.js} +1 -1
  63. package/studio/web/assets/{editor-CFQ5kbiT.js → editor-DKwRNgor.js} +1 -1
  64. package/studio/web/assets/editor.worker-Yd1NR0QT.js +33 -0
  65. package/studio/web/assets/{elk-276RUBZZ-Hexg9E4D.js → elk-276RUBZZ-DxDQPjQT.js} +1 -1
  66. package/studio/web/assets/{erDiagram-OPXOYQCR-CeC-wwO1.js → erDiagram-OPXOYQCR-CQlCX84D.js} +1 -1
  67. package/studio/web/assets/eventmodeling-NTZA5JFV-DONQf5aG.js +1 -0
  68. package/studio/web/assets/flowDiagram-KWPJA3E3-BoZ8_QR1.js +1 -0
  69. package/studio/web/assets/{ganttDiagram-FUAMR5RP-C2_Hhl1-.js → ganttDiagram-FUAMR5RP-bloX3Voy.js} +1 -1
  70. package/studio/web/assets/{getAnalytics-CjthMQqF.js → getAnalytics-C0FHjtdQ.js} +1 -1
  71. package/studio/web/assets/{gitGraph-4MIJSDKK-xIk5Vuew.js → gitGraph-4MIJSDKK-BrXWmJt2.js} +1 -1
  72. package/studio/web/assets/{gitGraphDiagram-X574FWY7-CMxJ34up.js → gitGraphDiagram-X574FWY7-C2KnnZeY.js} +1 -1
  73. package/studio/web/assets/{html-DdMBJEGu.js → html-KzEcNmGL.js} +1 -1
  74. package/studio/web/assets/{html.worker-DdwM4IrA.js → html.worker-B1dwY0Ay.js} +27 -27
  75. package/studio/web/assets/{htmlMode-DNtBx3OV.js → htmlMode-Bipij2Sy.js} +1 -1
  76. package/studio/web/assets/index-DMUQ34Jl.js +828 -0
  77. package/studio/web/assets/index-QLZw6xrU.css +1 -0
  78. package/studio/web/assets/index.lazy-D5_uqR4Q.js +2 -0
  79. package/studio/web/assets/{info-A6RAGUB7-o49TEbj-.js → info-A6RAGUB7-BhzBHe4U.js} +1 -1
  80. package/studio/web/assets/{infoDiagram-VRGFBTTK-Cjhh7yzZ.js → infoDiagram-VRGFBTTK-B0J1132p.js} +1 -1
  81. package/studio/web/assets/{ishikawaDiagram-OU5B5YK6-C63aXRmh.js → ishikawaDiagram-OU5B5YK6-BNk0tuQx.js} +1 -1
  82. package/studio/web/assets/{javascript-Dy6KbeHT.js → javascript-QytBwXUJ.js} +1 -1
  83. package/studio/web/assets/{journeyDiagram-ZHPQQLJL-D_o0P5Mu.js → journeyDiagram-ZHPQQLJL-Be79RMQM.js} +1 -1
  84. package/studio/web/assets/json.worker-BQqXGI3X.js +65 -0
  85. package/studio/web/assets/{jsonMode-BiuDVrzl.js → jsonMode-CIoh4zwS.js} +1 -1
  86. package/studio/web/assets/{kanban-definition-PNTS6WVX-CTI9SSxY.js → kanban-definition-PNTS6WVX-4uuoQP9W.js} +1 -1
  87. package/studio/web/assets/{languageServices-Dk8_6B25.js → languageServices-BvFCul4Y.js} +1 -1
  88. package/studio/web/assets/localStorageKeys-etzFY_xV.js +1 -0
  89. package/studio/web/assets/{lspLanguageFeatures-DwJKXzQa.js → lspLanguageFeatures-BvDuJHGH.js} +1 -1
  90. package/studio/web/assets/{mermaid-parser.core-C2lcH_jP.js → mermaid-parser.core-V1UYMeXV.js} +3 -3
  91. package/studio/web/assets/{mermaid.core-BG5bjfIG.js → mermaid.core-1PX6R49U.js} +4 -4
  92. package/studio/web/assets/{mindmap-definition-NLK3R4M7-BK7ho6JD.js → mindmap-definition-NLK3R4M7-C70Ctm0J.js} +1 -1
  93. package/studio/web/assets/notifications-C5EZskVd.js +1 -0
  94. package/studio/web/assets/notifications-jQfsyiVJ.js +1 -0
  95. package/studio/web/assets/{packet-AYTQ26CC-BHPy6KBV.js → packet-AYTQ26CC-DWbXHx5g.js} +1 -1
  96. package/studio/web/assets/{pegDiagram-GJSIUBJH-DzjwG3jO.js → pegDiagram-GJSIUBJH-d6z1cc7W.js} +1 -1
  97. package/studio/web/assets/{pie-WAS4IAKB-D0cP-DTU.js → pie-WAS4IAKB-DjoTm3yr.js} +1 -1
  98. package/studio/web/assets/{pieDiagram-5QR66LMP-C_hOP9Lv.js → pieDiagram-5QR66LMP-Dz3RHpDA.js} +1 -1
  99. package/studio/web/assets/profile-CkLzjQm2.js +1 -0
  100. package/studio/web/assets/{quadrantDiagram-O4NWA36T-CZW8PbV1.js → quadrantDiagram-O4NWA36T-CIF5Qkou.js} +1 -1
  101. package/studio/web/assets/{radar-RG4KPBEZ-C8fk0U81.js → radar-RG4KPBEZ-CVBgjCOj.js} +1 -1
  102. package/studio/web/assets/{railroad-74A4TZTK-B60FWm17.js → railroad-74A4TZTK-BdhXw10Q.js} +1 -1
  103. package/studio/web/assets/railroad-abnf-HS5TGJTU-BcoQPN39.js +1 -0
  104. package/studio/web/assets/railroad-ebnf-LZEXJU2U-B1Lke2JN.js +1 -0
  105. package/studio/web/assets/railroad-peg-WCYAUIDC-D-oJFkiV.js +1 -0
  106. package/studio/web/assets/{railroadDiagram-XR7U4H2S-DyyNmgbU.js → railroadDiagram-XR7U4H2S-y3Jv2U6N.js} +1 -1
  107. package/studio/web/assets/regions-b7HODz0P.js +1 -0
  108. package/studio/web/assets/register-DGmUOJc1.js +909 -0
  109. package/studio/web/assets/register-DjpsE8to.css +1 -0
  110. package/studio/web/assets/{requirementDiagram-PLB6GJNP-C4H-mLKu.js → requirementDiagram-PLB6GJNP-ChINZEmu.js} +1 -1
  111. package/studio/web/assets/restartGate-nHAFLDh-.js +1 -0
  112. package/studio/web/assets/{sankeyDiagram-IPEJSGJF-DHDyga4c.js → sankeyDiagram-IPEJSGJF-D9AyLce2.js} +1 -1
  113. package/studio/web/assets/{sequenceDiagram-PO4LG4MO-CdNcHXRI.js → sequenceDiagram-PO4LG4MO-hfk4aIEm.js} +1 -1
  114. package/studio/web/assets/{setComponentFile-BavlhfY0.js → setComponentFile-DlUcJ4Y0.js} +1 -1
  115. package/studio/web/assets/{setup-04yVzr2I.js → setup-D1jywSGk.js} +2 -2
  116. package/studio/web/assets/{stateDiagram-v2-GCMORJYK-CpKuXq15.js → stateDiagram-v2-GCMORJYK-Bs8RIb9K.js} +1 -1
  117. package/studio/web/assets/status-CjVoPieo.js +61 -0
  118. package/studio/web/assets/{swimlanes-2SLR337P-NRHa2D8I.js → swimlanes-2SLR337P-B52xrf6t.js} +1 -1
  119. package/studio/web/assets/swimlanesDiagram-TC7HE7FX-DfN7BAP5.js +8 -0
  120. package/studio/web/assets/tabs-BCTQzoQK.js +1 -0
  121. package/studio/web/assets/{timeline-definition-EJHVYXUP-D39Y-xpq.js → timeline-definition-EJHVYXUP-DJSITpQX.js} +1 -1
  122. package/studio/web/assets/toggleHighContrast-C9CKwOn9.js +51 -0
  123. package/studio/web/assets/toggleHighContrast-n7clI0si.css +1 -0
  124. package/studio/web/assets/{treeView-Q6P3EWNA-CRWR9OqD.js → treeView-Q6P3EWNA-C-epdxRo.js} +1 -1
  125. package/studio/web/assets/{treemap-WGGIJYW6-ZfXyJgLO.js → treemap-WGGIJYW6-DOVyp_zw.js} +1 -1
  126. package/studio/web/assets/{ts.worker-DDMUrpqh.js → ts.worker-Clm12uY6.js} +131 -131
  127. package/studio/web/assets/{tsMode-DGeWfx0W.js → tsMode-CRibSU_y.js} +1 -1
  128. package/studio/web/assets/{typescript-62_1FUbs.js → typescript-BRCpg8By.js} +1 -1
  129. package/studio/web/assets/{useEntityRestURL-DSVwev9J.js → useEntityRestURL-pPVmpjIi.js} +1 -1
  130. package/studio/web/assets/{usecaseDiagram-POWQR4AR-jf-zIDZ_.js → usecaseDiagram-POWQR4AR-B6252cbq.js} +1 -1
  131. package/studio/web/assets/{vendor-core-BdkdbUek.js → vendor-core-6LhfJlRC.js} +1 -1
  132. package/studio/web/assets/vendor-datadog-ozKf-pts.js +6 -0
  133. package/studio/web/assets/{vendor-react-izWchTRL.js → vendor-react-BOliS0BL.js} +1 -1
  134. package/studio/web/assets/vendor-tanstack-Bntfka-M.js +2 -0
  135. package/studio/web/assets/vendor-ui-9EYYDgoN.js +41 -0
  136. package/studio/web/assets/{vennDiagram-UO4OBE2U-BKYAZUh_.js → vennDiagram-UO4OBE2U-DZTsDA4k.js} +1 -1
  137. package/studio/web/assets/{wardley-WFR3VGLG-BO7f36ci.js → wardley-WFR3VGLG-Cfqoe6z4.js} +1 -1
  138. package/studio/web/assets/{wardleyDiagram-VNRHLVJA-Djb0JDQc.js → wardleyDiagram-VNRHLVJA-qu_f7Tvl.js} +1 -1
  139. package/studio/web/assets/{workers-Cs52GyHv.js → workers-BBDZZ4qw.js} +3 -3
  140. package/studio/web/assets/{xml-Co5ow_Ix.js → xml-tWNEwt7n.js} +1 -1
  141. package/studio/web/assets/{xychartDiagram-PMCCYNJV-BQh9ubvb.js → xychartDiagram-PMCCYNJV-UrOKRsmR.js} +1 -1
  142. package/studio/web/assets/{yaml-BcngpXye.js → yaml-BZgmBIf5.js} +1 -1
  143. package/studio/web/assets/{yaml.worker-DAK2o_H1.js → yaml.worker-D9JekGdp.js} +183 -183
  144. package/studio/web/index.html +15 -15
  145. package/studio/web/assets/FloatingChat-BUlsFGMV.js +0 -23
  146. package/studio/web/assets/alertDialog-UrbiMgQt.js +0 -1
  147. package/studio/web/assets/apiToken-CPhbc_zN.js +0 -1
  148. package/studio/web/assets/applications-D3Z6BVDl.js +0 -296
  149. package/studio/web/assets/architecture-7GRP2DOG-11l4Ny1l.js +0 -1
  150. package/studio/web/assets/authStore-5jSS5pTW.js +0 -3
  151. package/studio/web/assets/channel-BDtouPd6.js +0 -1
  152. package/studio/web/assets/dialog-DTepachC.js +0 -1
  153. package/studio/web/assets/editor.worker-CkkvdXvO.js +0 -33
  154. package/studio/web/assets/eventmodeling-NTZA5JFV-CFb03e0o.js +0 -1
  155. package/studio/web/assets/flowDiagram-KWPJA3E3-Cidi00_Y.js +0 -1
  156. package/studio/web/assets/index-CPoDzQTe.css +0 -1
  157. package/studio/web/assets/index-zZCxAIEb.js +0 -824
  158. package/studio/web/assets/index.lazy-CKbsHbSN.js +0 -2
  159. package/studio/web/assets/json.worker-CSi2hF-a.js +0 -65
  160. package/studio/web/assets/notifications-DJqAtWxD.js +0 -1
  161. package/studio/web/assets/notifications-DsDtb41c.js +0 -1
  162. package/studio/web/assets/profile-zTGUN51x.js +0 -1
  163. package/studio/web/assets/railroad-abnf-HS5TGJTU-CLMVZvH0.js +0 -1
  164. package/studio/web/assets/railroad-ebnf-LZEXJU2U--tlwFoVB.js +0 -1
  165. package/studio/web/assets/railroad-peg-WCYAUIDC-Dpuslblm.js +0 -1
  166. package/studio/web/assets/regions-SOtABPB9.js +0 -1
  167. package/studio/web/assets/register-7JezVakc.js +0 -848
  168. package/studio/web/assets/register-jjEx9u7D.css +0 -1
  169. package/studio/web/assets/setLocalStorage-D_kflv4U.js +0 -1
  170. package/studio/web/assets/status-g_mNYE7j.js +0 -61
  171. package/studio/web/assets/swimlanesDiagram-TC7HE7FX-BBk4t4hO.js +0 -8
  172. package/studio/web/assets/tabs-CnQ_zenf.js +0 -1
  173. package/studio/web/assets/toggleHighContrast--ixms01u.css +0 -1
  174. package/studio/web/assets/toggleHighContrast-i2FjCrzO.js +0 -49
  175. package/studio/web/assets/useLocalStorage-BYoZF60G.js +0 -1
  176. package/studio/web/assets/vendor-datadog-fCQhBHGk.js +0 -6
  177. package/studio/web/assets/vendor-tanstack-CQ1aRjVa.js +0 -2
  178. package/studio/web/assets/vendor-ui-aE1tHcW7.js +0 -41
  179. package/studio/web/assets/x-vYntCxm4.js +0 -1
@@ -193,6 +193,11 @@ const privateKeys = new Map();
193
193
  // Debounce window (ms) for rebuilding TLS secure contexts. Shared by the hdb_certificate
194
194
  // subscription and the private-key hot-reload trigger so both coalesce on the same cadence.
195
195
  const TLS_REBUILD_DEBOUNCE_MS = 1500;
196
+ // Self-retry backoff cap: a permanently bad record must not cost every selector on every
197
+ // thread a table scan + X509 parse per debounce interval, forever.
198
+ const TLS_FAILURE_RETRY_MAX_DELAY_MS = 300_000;
199
+ // While a failure signature is unchanged, repeat occurrences log a summary at most this often.
200
+ const TLS_FAILURE_SUMMARY_INTERVAL_MS = 3_600_000;
196
201
  // Debounced rebuild triggers, one per live server TLS selector (registered in createTLSSelector's
197
202
  // initialize). When a private key is hot-reloaded on this thread, every live selector re-runs
198
203
  // updateTLS so a secure context built with a stale key — or built before this key arrived — is
@@ -297,7 +302,9 @@ function loadCertificates() {
297
302
  logger.info?.(`Certificate ${certCn} at ${path} is older (${new Date(fileTimestamp)}) than the certificate in the database (${recordTimestamp > 1 ? new Date(recordTimestamp) : 'only self signed certificate available'})`);
298
303
  return;
299
304
  }
300
- promise = certificateTable.put({
305
+ // Returned so loadAndWatch can roll back its mtime latch if the write fails —
306
+ // assigned as well to preserve loadCertificates()'s awaited-return contract.
307
+ return (promise = certificateTable.put({
301
308
  name: certCn,
302
309
  uses: config.uses ?? (configKey.includes('operations') ? ['operations-api'] : []),
303
310
  ciphers: config.ciphers,
@@ -314,7 +321,7 @@ function loadCertificates() {
314
321
  valid_from: x509Cert.validFrom,
315
322
  valid_to: x509Cert.validTo,
316
323
  },
317
- });
324
+ }));
318
325
  }, ca ? 'certificate authority' : 'certificate');
319
326
  }
320
327
  }
@@ -365,20 +372,34 @@ const certificateWatchPollers = new Map();
365
372
  function loadAndWatch(path, loadCert, type) {
366
373
  let lastModified;
367
374
  const loadFile = (path, stats) => {
375
+ // The latch dedupes chokidar/poll but must mean "last successfully APPLIED", or a failed
376
+ // apply is deduplicated forever (#2382). Rollbacks are equality-guarded so an old failure
377
+ // can't unlatch a newer successful reload.
378
+ const previousModified = lastModified;
379
+ let modified;
368
380
  try {
369
381
  // chokidar's 'change' event omits stats unless alwaysStat is set (default off in v4), so
370
382
  // stat the file here when it's missing — otherwise the inotify fast path would throw and
371
383
  // silently never reload, leaving only the periodic poll to catch the change.
372
- let modified = (stats ?? (0, node_fs_1.statSync)(path)).mtimeMs;
384
+ modified = (stats ?? (0, node_fs_1.statSync)(path)).mtimeMs;
373
385
  if (modified && modified !== lastModified) {
374
386
  if (lastModified && worker_threads_1.isMainThread)
375
387
  logger.warn?.(`Reloading ${type}:`, path);
376
388
  lastModified = modified;
377
- loadCert(readPEM(path));
389
+ const applied = loadCert(readPEM(path));
390
+ if (typeof applied?.then === 'function') {
391
+ applied.catch((error) => {
392
+ logger.error?.(`Error applying ${type}:`, path, error);
393
+ if (lastModified === modified)
394
+ lastModified = previousModified;
395
+ });
396
+ }
378
397
  }
379
398
  }
380
399
  catch (error) {
381
400
  logger.error?.(`Error loading ${type}:`, path, error);
401
+ if (modified !== undefined && lastModified === modified)
402
+ lastModified = previousModified;
382
403
  }
383
404
  };
384
405
  if (fs.existsSync(path))
@@ -962,10 +983,79 @@ function createTLSSelector(type, mtlsOptions, liveReload = true) {
962
983
  let subscribedTable = null;
963
984
  let activeSubscription = null;
964
985
  return (SNICallback.ready = new Promise((resolve, reject) => {
986
+ // Pass-level failure before `.ready` settles still rejects (boot surfaces it); after,
987
+ // the only safe response is to keep serving live state and retry.
988
+ let readySettled = false;
989
+ const settle = (value) => {
990
+ readySettled = true;
991
+ resolve(value);
992
+ };
993
+ // Self-retry state for passes with per-record failures: the signature identifies the
994
+ // failure set, the delay backs off while it is unchanged, and summaries are rate-limited.
995
+ let failureSignature = '';
996
+ let failureRetryDelay = TLS_REBUILD_DEBOUNCE_MS;
997
+ let failureRetryTimer;
998
+ let failureRetryCount = 0;
999
+ let failureLastSummaryAt = 0;
1000
+ const scheduleFailureRetry = () => {
1001
+ // An armed external rebuild (or an armed retry) will run a pass that re-arms if needed.
1002
+ if (failureRetryTimer || rebuildTimer)
1003
+ return;
1004
+ failureRetryTimer = setTimeout(() => {
1005
+ failureRetryTimer = undefined;
1006
+ updateTLS();
1007
+ }, failureRetryDelay).unref();
1008
+ failureRetryDelay = Math.min(failureRetryDelay * 2, TLS_FAILURE_RETRY_MAX_DELAY_MS);
1009
+ };
1010
+ // Log each failure on a new/changed failure set; while unchanged, stay silent apart from a
1011
+ // rate-limited summary — an un-throttled 1.5s retry would emit ~57k lines/day per selector.
1012
+ const reportFailures = (failures) => {
1013
+ const signature = failures
1014
+ .map(({ cert, error }) => `${cert.name}: ${error?.message}`)
1015
+ .sort()
1016
+ .join('; ');
1017
+ if (signature !== failureSignature) {
1018
+ failureSignature = signature;
1019
+ failureRetryDelay = TLS_REBUILD_DEBOUNCE_MS;
1020
+ failureRetryCount = 0;
1021
+ failureLastSummaryAt = Date.now();
1022
+ // A timer armed for the previous signature would run the next pass on its stale
1023
+ // (possibly maxed-out) delay; re-arm fresh for the new failure set.
1024
+ if (failureRetryTimer) {
1025
+ clearTimeout(failureRetryTimer);
1026
+ failureRetryTimer = undefined;
1027
+ }
1028
+ for (const { cert, error } of failures)
1029
+ logger.error?.('Error applying TLS for', cert.name, `on the '${type}' listener`, error);
1030
+ }
1031
+ else {
1032
+ failureRetryCount++;
1033
+ // Stays at error: a stuck rotation must keep an alertable signal — the retained cert
1034
+ // is aging toward its notAfter the whole time.
1035
+ if (Date.now() - failureLastSummaryAt >= TLS_FAILURE_SUMMARY_INTERVAL_MS) {
1036
+ failureLastSummaryAt = Date.now();
1037
+ logger.error?.(`TLS rebuild for the '${type}' listener is still failing for [${failures
1038
+ .map(({ cert }) => cert.name)
1039
+ .join(', ')}] after ${failureRetryCount} retries; serving the last good state`);
1040
+ }
1041
+ }
1042
+ };
1043
+ const clearFailureState = () => {
1044
+ if (failureSignature) {
1045
+ logger.warn?.(`TLS rebuild for the '${type}' listener recovered; all certificates applied`);
1046
+ }
1047
+ failureSignature = '';
1048
+ failureRetryDelay = TLS_REBUILD_DEBOUNCE_MS;
1049
+ failureRetryCount = 0;
1050
+ if (failureRetryTimer) {
1051
+ clearTimeout(failureRetryTimer);
1052
+ failureRetryTimer = undefined;
1053
+ }
1054
+ };
965
1055
  function updateTLS() {
966
1056
  try {
967
1057
  if (databases_ts_1.databases === undefined) {
968
- resolve();
1058
+ settle();
969
1059
  return;
970
1060
  }
971
1061
  if (databases_ts_1.databases.system?.hdb_certificate === undefined) {
@@ -993,19 +1083,19 @@ function createTLSSelector(type, mtlsOptions, liveReload = true) {
993
1083
  scheduleRebuild();
994
1084
  return;
995
1085
  }
996
- // Guards above have returned before touching secureContexts/caCerts, so the retry
997
- // path never wipes a previously-published (non-empty) state before failing to
998
- // replace it.
999
- secureContexts.clear();
1000
- caCerts.clear();
1086
+ // Transactional publication (#2382): build the whole replacement state off to the
1087
+ // side and reconcile the live maps only after the pass completes, so a failure of
1088
+ // any shape leaves the currently-served state intact.
1089
+ const candidateContexts = new Map();
1090
+ const candidateCAs = new Map();
1091
+ let candidateHasWildcards = false;
1001
1092
  let bestQuality = 0;
1002
- // Whether THIS pass produced a default context. The `defaultContext` closure variable
1003
- // is deliberately never reset (a transient zero-cert pass must keep serving the prior
1004
- // default while the retry below waits for certs to come back), so it can't be used to
1005
- // ask "did this pass find anything?" — after the first successful pass it is truthy
1006
- // forever, which would let a later transient zero-cert rebuild skip the retry and
1007
- // publish an empty certificates list (the #1998 symptom) on the post-boot path.
1093
+ let candidateDefault;
1094
+ // Whether THIS pass produced a default. The `defaultContext` closure variable is never
1095
+ // reset (a transient zero-cert pass keeps serving the prior default), so keying the
1096
+ // zero-cert retry off it would republish empty post-boot — the #1998 symptom.
1008
1097
  let defaultContextSetThisPass = false;
1098
+ const failedThisPass = [];
1009
1099
  // Track the actual table instance, not just whether we've ever subscribed: resetDatabases()
1010
1100
  // (copy_db, ITC restart handling) replaces databases.system.hdb_certificate with a new
1011
1101
  // table object, and a boolean flag would never re-subscribe to it, permanently losing
@@ -1036,24 +1126,28 @@ function createTLSSelector(type, mtlsOptions, liveReload = true) {
1036
1126
  previousSubscription.then((subscription) => subscription?.end?.()).catch(() => { });
1037
1127
  }
1038
1128
  }
1039
- for (const cert of databases_ts_1.databases.system.hdb_certificate.search([])) {
1129
+ // One snapshot drives both loops: separate search() calls own separate read
1130
+ // snapshots, and a write committing between them could publish a renewed leaf
1131
+ // against the previous CA set.
1132
+ const certRecords = Array.from(databases_ts_1.databases.system.hdb_certificate.search([]));
1133
+ for (const cert of certRecords) {
1040
1134
  const certificate = cert.certificate;
1041
1135
  let certParsed;
1042
1136
  try {
1043
1137
  certParsed = new node_crypto_1.X509Certificate(certificate);
1044
1138
  }
1045
1139
  catch (error) {
1046
- // One unparseable record shouldn't abort the whole pass before any context is
1047
- // built — the second loop's per-cert try/catch doesn't cover this loop.
1048
- logger.error?.('Skipping unparseable certificate record', cert.name, error);
1140
+ // A pass failure like any other (throttle + retry). Its caCerts entry can't be
1141
+ // retained — the subject is unrecoverable from a PEM that won't parse.
1142
+ failedThisPass.push({ cert, error });
1049
1143
  continue;
1050
1144
  }
1051
1145
  if (cert.is_authority) {
1052
1146
  certParsed.asString = certificate;
1053
- caCerts.set(certParsed.subject, certificate);
1147
+ candidateCAs.set(certParsed.subject, certificate);
1054
1148
  }
1055
1149
  }
1056
- for (const cert of databases_ts_1.databases.system.hdb_certificate.search([])) {
1150
+ for (const cert of certRecords) {
1057
1151
  try {
1058
1152
  if (cert.is_authority) {
1059
1153
  continue;
@@ -1071,8 +1165,8 @@ function createTLSSelector(type, mtlsOptions, liveReload = true) {
1071
1165
  const private_key = getPrivateKeyByName(cert.private_key_name);
1072
1166
  let certificate = cert.certificate;
1073
1167
  const certParsed = new node_crypto_1.X509Certificate(certificate);
1074
- if (caCerts.has(certParsed.issuer)) {
1075
- certificate += '\n' + caCerts.get(certParsed.issuer);
1168
+ if (candidateCAs.has(certParsed.issuer)) {
1169
+ certificate += '\n' + candidateCAs.get(certParsed.issuer);
1076
1170
  }
1077
1171
  if (!private_key || !certificate) {
1078
1172
  throw new Error('Missing private key or certificate for secure server');
@@ -1080,8 +1174,10 @@ function createTLSSelector(type, mtlsOptions, liveReload = true) {
1080
1174
  const secureOptions = {
1081
1175
  ciphers: cert.ciphers,
1082
1176
  ticketKeys: (0, manageThreads_js_1.getTicketKeys)(),
1083
- availableCAs: caCerts, // preserve the record of caCerts even if not used for mTLS here
1084
- ca: mtlsOptions && Array.from(caCerts.values()),
1177
+ // the live map, not the candidate: its identity survives publication, so contexts
1178
+ // keep seeing the current CA set after later rebuilds
1179
+ availableCAs: caCerts,
1180
+ ca: mtlsOptions && Array.from(candidateCAs.values()),
1085
1181
  cert: certificate,
1086
1182
  key: private_key,
1087
1183
  key_file: cert.private_key_name,
@@ -1100,7 +1196,7 @@ function createTLSSelector(type, mtlsOptions, liveReload = true) {
1100
1196
  secureContext.name = cert.name;
1101
1197
  secureContext.options = secureOptions;
1102
1198
  secureContext.quality = quality;
1103
- secureContext.certificateAuthorities = Array.from(caCerts);
1199
+ secureContext.certificateAuthorities = Array.from(candidateCAs);
1104
1200
  // we store the first 100 bytes of the certificate just for debug logging
1105
1201
  secureContext.certStart = certificate.toString().slice(0, 100);
1106
1202
  // we want to configure SNI handling to pick the right certificate based on all the registered SANs
@@ -1108,14 +1204,14 @@ function createTLSSelector(type, mtlsOptions, liveReload = true) {
1108
1204
  for (let hostname of hostnames) {
1109
1205
  if (hostname) {
1110
1206
  if (hostname[0] === '*') {
1111
- hasWildcards = true;
1207
+ candidateHasWildcards = true;
1112
1208
  hostname = hostname.slice(1);
1113
1209
  }
1114
1210
  // we use this certificate if it has a higher quality than the existing one for this hostname
1115
- let existingCertQuality = secureContexts.get(hostname)?.quality ?? 0;
1211
+ let existingCertQuality = candidateContexts.get(hostname)?.quality ?? 0;
1116
1212
  logger.trace?.('Assigning TLS for hostname', hostname, 'if', quality, '>', existingCertQuality);
1117
1213
  if (quality > existingCertQuality) {
1118
- secureContexts.set(hostname, secureContext);
1214
+ candidateContexts.set(hostname, secureContext);
1119
1215
  }
1120
1216
  }
1121
1217
  else {
@@ -1125,57 +1221,147 @@ function createTLSSelector(type, mtlsOptions, liveReload = true) {
1125
1221
  logger.trace?.('Adding TLS', secureContext.name, 'for', server?.ports || 'client', 'cert named', cert.name, 'hostnames', hostnames, 'quality', quality, 'best quality', bestQuality);
1126
1222
  if (quality > bestQuality /* && hasIpAddress*/) {
1127
1223
  // we use this certificate as the default if it has a higher quality than the existing one
1128
- SNICallback.defaultContext = defaultContext = secureContext;
1224
+ candidateDefault = secureContext;
1129
1225
  defaultContextSetThisPass = true;
1130
1226
  bestQuality = quality;
1131
- if (server) {
1132
- server.defaultContext = secureContext;
1133
- // note that we can not set the secure context on the server here, because this creates an
1134
- // indeterminate situation of whether openssl will use this certificate or the one from the SNI
1135
- // callback
1136
- //server.setSecureContext?.(server, secureOptions);
1137
- }
1138
1227
  }
1139
1228
  }
1140
1229
  catch (error) {
1141
- logger.error?.('Error applying TLS for', cert.name, error);
1230
+ failedThisPass.push({ cert, error });
1142
1231
  }
1143
1232
  }
1144
- if (liveReload && secureContexts.size === 0 && !defaultContextSetThisPass) {
1145
- // The not-loaded-yet guard above only covers the table object being absent, not the
1146
- // table being present but every row failing to apply (e.g. a private key not yet
1147
- // available on this thread, caught above per-cert). Resolving here would still write
1148
- // an empty `certificates:` list — the exact customer-visible symptom this PR exists to
1149
- // fix. Retry on the same debounce instead of publishing that state; a rebuild trigger
1150
- // (cert-table change or private-key reload, both already wired to scheduleRebuild) is
1151
- // almost certainly still coming on a normal boot, and this just avoids a window where
1152
- // we'd otherwise publish empty in the meantime.
1153
- //
1154
- // `!defaultContextSetThisPass` because an empty hostname map is not the same as "no TLS
1155
- // available": a cert whose hostnames resolve to [] (no usable SANs and no CN) builds no
1156
- // per-hostname entry but still sets a serviceable default context — that listener must
1157
- // resolve and serve via the default rather than retry forever for hostname entries that
1158
- // can't exist. It must be the per-pass flag, not the persistent `defaultContext` (see its
1159
- // declaration note): keying off the closure variable would disarm this retry for every
1160
- // pass after the first success, reopening the publish-empty window on live rebuilds.
1161
- //
1162
- // Gated on liveReload: transient, single-use selectors (getReplicationCert) legitimately
1163
- // resolve empty — e.g. the bootstrap check `if (!(await getReplicationCert())) { create
1164
- // one }` in generateCertAuthority's caller depends on an empty resolution meaning "no
1165
- // cert yet," and must not hang waiting for a cert that this exact call is about to create.
1166
- //
1167
- // Latched like the system-db wait above: this retries indefinitely on the debounce, and
1168
- // an unlatched warn would emit ~57k lines/day from a listener stuck this way.
1233
+ // Retain-last-good: a record still in the table whose build failed keeps its live
1234
+ // hostname entries and its default candidacy; deletion remains the way to drop them.
1235
+ // On an mTLS listener whose CA set changed, the retained pair is rebuilt against the
1236
+ // current trust material — new handshakes never see stale trust (established sessions
1237
+ // and outstanding tickets are unaffected, as on any build); if the rebuild fails the entries drop
1238
+ // unless nothing else is servable (the zero-cert guard below then keeps the old
1239
+ // state). Full contract in DESIGN.md "TLS hot-reload".
1240
+ const caSetUnchanged = (previous) => {
1241
+ const builtWith = previous.certificateAuthorities;
1242
+ if (!Array.isArray(builtWith))
1243
+ return candidateCAs.size === 0;
1244
+ return (builtWith.length === candidateCAs.size &&
1245
+ builtWith.every(([subject, pem]) => candidateCAs.get(subject) === pem));
1246
+ };
1247
+ const rebuiltRetentions = new Map();
1248
+ const retentionFailures = [];
1249
+ const retainable = (previous) => {
1250
+ // Without mTLS nothing consulted depends on the CA set — skip the rebuild, but
1251
+ // refresh the CA bookkeeping mirrored into socket metadata for fronting proxies.
1252
+ if (!mtlsOptions || caSetUnchanged(previous)) {
1253
+ previous.certificateAuthorities = Array.from(candidateCAs);
1254
+ return previous;
1255
+ }
1256
+ if (rebuiltRetentions.has(previous))
1257
+ return rebuiltRetentions.get(previous);
1258
+ try {
1259
+ const secureOptions = {
1260
+ ...previous.options,
1261
+ ticketKeys: (0, manageThreads_js_1.getTicketKeys)(),
1262
+ availableCAs: caCerts,
1263
+ ca: mtlsOptions && Array.from(candidateCAs.values()),
1264
+ };
1265
+ const rebuilt = tls.createSecureContext(secureOptions);
1266
+ rebuilt.name = previous.name;
1267
+ rebuilt.options = secureOptions;
1268
+ rebuilt.quality = previous.quality;
1269
+ rebuilt.certificateAuthorities = Array.from(candidateCAs);
1270
+ rebuilt.certStart = previous.certStart;
1271
+ rebuiltRetentions.set(previous, rebuilt);
1272
+ return rebuilt;
1273
+ }
1274
+ catch (error) {
1275
+ retentionFailures.push({
1276
+ cert: { name: `${previous.name} (retained-context rebuild)` },
1277
+ error,
1278
+ });
1279
+ rebuiltRetentions.set(previous, undefined);
1280
+ return undefined;
1281
+ }
1282
+ };
1283
+ for (const { cert } of failedThisPass) {
1284
+ // Ties go to the incumbent: a hostname the retained context already owned, and the
1285
+ // live default when it is the retained record, must not flip to an equal-quality
1286
+ // sibling because of a transient failure. A context retained only for a hostname
1287
+ // still needs strict > to become the default.
1288
+ const retain = (previous, hostname) => {
1289
+ const retained = retainable(previous);
1290
+ if (!retained)
1291
+ return;
1292
+ const previousQuality = retained.quality ?? 0;
1293
+ if (hostname !== undefined) {
1294
+ if (previousQuality >= (candidateContexts.get(hostname)?.quality ?? 0)) {
1295
+ candidateContexts.set(hostname, retained);
1296
+ }
1297
+ if (hostname[0] === '.')
1298
+ candidateHasWildcards = true;
1299
+ }
1300
+ const winsDefault = previous === defaultContext ? previousQuality >= bestQuality : previousQuality > bestQuality;
1301
+ if (winsDefault) {
1302
+ candidateDefault = retained;
1303
+ defaultContextSetThisPass = true;
1304
+ bestQuality = previousQuality;
1305
+ }
1306
+ };
1307
+ for (const [hostname, previous] of secureContexts) {
1308
+ if (previous.name === cert.name)
1309
+ retain(previous, hostname);
1310
+ }
1311
+ if (defaultContext?.name === cert.name)
1312
+ retain(defaultContext);
1313
+ }
1314
+ if (liveReload && candidateContexts.size === 0 && !defaultContextSetThisPass) {
1315
+ // Every row failed to apply (e.g. keys not yet on this thread). Publishing or
1316
+ // resolving would write an empty `certificates:` list (the #1998 symptom), so keep
1317
+ // live state and retry. `!defaultContextSetThisPass`, not the persistent
1318
+ // `defaultContext`: a cert with no usable hostnames still sets a serviceable
1319
+ // default (must resolve, not retry forever), while the closure variable is truthy
1320
+ // forever after the first success. Gated on liveReload: transient selectors
1321
+ // (getReplicationCert) legitimately resolve empty — bootstrap depends on that
1322
+ // meaning "no cert yet". Warn is latched: unlatched would emit ~57k lines/day.
1169
1323
  if (server && !server.tlsSelectorWarnedZeroCerts) {
1170
1324
  server.tlsSelectorWarnedZeroCerts = true;
1171
1325
  logger.warn?.(`TLS selector for the '${type}' listener resolved zero certificates; retrying every ${TLS_REBUILD_DEBOUNCE_MS}ms`);
1172
1326
  }
1173
- scheduleRebuild();
1327
+ if (failedThisPass.length > 0) {
1328
+ // Surface WHY the pass came up empty and put the retry on the backoff.
1329
+ reportFailures(failedThisPass.concat(retentionFailures));
1330
+ scheduleFailureRetry();
1331
+ }
1332
+ else {
1333
+ scheduleRebuild();
1334
+ }
1174
1335
  return;
1175
1336
  }
1337
+ // Publish: reconcile the live maps in place (their identity is aliased by
1338
+ // server.secureContexts and every context's availableCAs) and advance all default
1339
+ // references together so SNI and non-SNI traffic can't diverge.
1340
+ secureContexts.clear();
1341
+ for (const [hostname, context] of candidateContexts)
1342
+ secureContexts.set(hostname, context);
1343
+ caCerts.clear();
1344
+ for (const [subject, certificate] of candidateCAs)
1345
+ caCerts.set(subject, certificate);
1176
1346
  // only listener selectors publish: a one-shot client selector's pass may see no authority rows
1177
1347
  if (liveReload)
1178
1348
  (0, trustedIssuers_ts_1.publishTrustedAuthorities)(caCerts.values());
1349
+ hasWildcards = candidateHasWildcards;
1350
+ if (candidateDefault) {
1351
+ SNICallback.defaultContext = defaultContext = candidateDefault;
1352
+ // note that we can not set the secure context on the server here, because this creates an
1353
+ // indeterminate situation of whether openssl will use this certificate or the one from the SNI
1354
+ // callback
1355
+ if (server)
1356
+ server.defaultContext = candidateDefault;
1357
+ }
1358
+ if (failedThisPass.length > 0) {
1359
+ reportFailures(failedThisPass.concat(retentionFailures));
1360
+ scheduleFailureRetry();
1361
+ }
1362
+ else {
1363
+ clearFailureState();
1364
+ }
1179
1365
  // A successful pass ends any warn latches so a later recurrence logs again.
1180
1366
  if (server) {
1181
1367
  server.tlsSelectorWaitedForSystemDb = false;
@@ -1198,9 +1384,15 @@ function createTLSSelector(type, mtlsOptions, liveReload = true) {
1198
1384
  }
1199
1385
  }
1200
1386
  server?.secureContextsListeners.forEach((listener) => listener());
1201
- resolve(defaultContext);
1387
+ settle(defaultContext);
1202
1388
  }
1203
1389
  catch (error) {
1390
+ if (readySettled) {
1391
+ // Live state is untouched (candidates are pass-local): keep serving it and retry.
1392
+ reportFailures([{ cert: { name: `(${type} rebuild pass)` }, error }]);
1393
+ scheduleFailureRetry();
1394
+ return;
1395
+ }
1204
1396
  reject(error);
1205
1397
  }
1206
1398
  }