@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
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
- promise = certificateTable.put({
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
- let modified = (stats ?? statSync(path)).mtimeMs;
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
- resolve();
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
- // Guards above have returned before touching secureContexts/caCerts, so the retry
1028
- // path never wipes a previously-published (non-empty) state before failing to
1029
- // replace it.
1030
- secureContexts.clear();
1031
- caCerts.clear();
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
- // Whether THIS pass produced a default context. The `defaultContext` closure variable
1034
- // is deliberately never reset (a transient zero-cert pass must keep serving the prior
1035
- // default while the retry below waits for certs to come back), so it can't be used to
1036
- // ask "did this pass find anything?" — after the first successful pass it is truthy
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
- for (const cert of databases.system.hdb_certificate.search([])) {
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
- // One unparseable record shouldn't abort the whole pass before any context is
1076
- // built — the second loop's per-cert try/catch doesn't cover this loop.
1077
- logger.error?.('Skipping unparseable certificate record', cert.name, error);
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
- caCerts.set(certParsed.subject, certificate);
1175
+ candidateCAs.set(certParsed.subject, certificate);
1083
1176
  }
1084
1177
  }
1085
1178
 
1086
- for (const cert of databases.system.hdb_certificate.search([])) {
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 (caCerts.has(certParsed.issuer)) {
1105
- certificate += '\n' + caCerts.get(certParsed.issuer);
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
- availableCAs: caCerts, // preserve the record of caCerts even if not used for mTLS here
1114
- ca: mtlsOptions && Array.from(caCerts.values()),
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(caCerts);
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
- hasWildcards = true;
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 = secureContexts.get(hostname)?.quality ?? 0;
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
- secureContexts.set(hostname, secureContext);
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
- (SNICallback as any).defaultContext = defaultContext = secureContext;
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
- logger.error?.('Error applying TLS for', cert.name, error);
1267
+ failedThisPass.push({ cert, error });
1180
1268
  }
1181
1269
  }
1182
- if (liveReload && secureContexts.size === 0 && !defaultContextSetThisPass) {
1183
- // The not-loaded-yet guard above only covers the table object being absent, not the
1184
- // table being present but every row failing to apply (e.g. a private key not yet
1185
- // available on this thread, caught above per-cert). Resolving here would still write
1186
- // an empty `certificates:` list — the exact customer-visible symptom this PR exists to
1187
- // fix. Retry on the same debounce instead of publishing that state; a rebuild trigger
1188
- // (cert-table change or private-key reload, both already wired to scheduleRebuild) is
1189
- // almost certainly still coming on a normal boot, and this just avoids a window where
1190
- // we'd otherwise publish empty in the meantime.
1191
- //
1192
- // `!defaultContextSetThisPass` because an empty hostname map is not the same as "no TLS
1193
- // available": a cert whose hostnames resolve to [] (no usable SANs and no CN) builds no
1194
- // per-hostname entry but still sets a serviceable default context — that listener must
1195
- // resolve and serve via the default rather than retry forever for hostname entries that
1196
- // can't exist. It must be the per-pass flag, not the persistent `defaultContext` (see its
1197
- // declaration note): keying off the closure variable would disarm this retry for every
1198
- // pass after the first success, reopening the publish-empty window on live rebuilds.
1199
- //
1200
- // Gated on liveReload: transient, single-use selectors (getReplicationCert) legitimately
1201
- // resolve empty — e.g. the bootstrap check `if (!(await getReplicationCert())) { create
1202
- // one }` in generateCertAuthority's caller depends on an empty resolution meaning "no
1203
- // cert yet," and must not hang waiting for a cert that this exact call is about to create.
1204
- //
1205
- // Latched like the system-db wait above: this retries indefinitely on the debounce, and
1206
- // an unlatched warn would emit ~57k lines/day from a listener stuck this way.
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
- scheduleRebuild();
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
- resolve(defaultContext);
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
- if (isDomainSocketPathTooLong(port)) {
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(port)}-byte path exceeds the platform limit of ${getDomainSocketPathMaxBytes()} bytes. Continuing without this domain socket.`
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: port }, onListening);
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();