@nifrajs/web 4.0.2 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (384) hide show
  1. package/README.md +11 -0
  2. package/dist/build-vite.d.ts +19 -0
  3. package/dist/build-vite.d.ts.map +1 -1
  4. package/dist/build-vite.js +31 -10
  5. package/dist/build-vite.js.map +1 -1
  6. package/dist/build.d.ts +8 -10
  7. package/dist/build.d.ts.map +1 -1
  8. package/dist/build.js +90 -31
  9. package/dist/build.js.map +1 -1
  10. package/dist/cdn.d.ts +8 -0
  11. package/dist/cdn.d.ts.map +1 -1
  12. package/dist/cdn.js +14 -15
  13. package/dist/cdn.js.map +1 -1
  14. package/dist/client.d.ts +1 -1
  15. package/dist/client.d.ts.map +1 -1
  16. package/dist/client.js +25 -7
  17. package/dist/client.js.map +1 -1
  18. package/dist/deferred.d.ts +2 -2
  19. package/dist/deferred.d.ts.map +1 -1
  20. package/dist/deferred.js +28 -7
  21. package/dist/deferred.js.map +1 -1
  22. package/dist/dev-feed.d.ts.map +1 -1
  23. package/dist/dev-feed.js +24 -5
  24. package/dist/dev-feed.js.map +1 -1
  25. package/dist/dev-session.d.ts.map +1 -1
  26. package/dist/dev-session.js +4 -14
  27. package/dist/dev-session.js.map +1 -1
  28. package/dist/dev.d.ts +7 -17
  29. package/dist/dev.d.ts.map +1 -1
  30. package/dist/dev.js +13 -1
  31. package/dist/dev.js.map +1 -1
  32. package/dist/draft.d.ts +8 -5
  33. package/dist/draft.d.ts.map +1 -1
  34. package/dist/draft.js +30 -13
  35. package/dist/draft.js.map +1 -1
  36. package/dist/fonts-google.d.ts.map +1 -1
  37. package/dist/fonts-google.js +7 -0
  38. package/dist/fonts-google.js.map +1 -1
  39. package/dist/index.d.ts +2 -2
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +2 -2
  42. package/dist/index.js.map +1 -1
  43. package/dist/internal/codegen.d.ts +1 -0
  44. package/dist/internal/codegen.d.ts.map +1 -1
  45. package/dist/internal/codegen.js +56 -29
  46. package/dist/internal/codegen.js.map +1 -1
  47. package/dist/internal/disposable-dir.d.ts +8 -0
  48. package/dist/internal/disposable-dir.d.ts.map +1 -0
  49. package/dist/internal/disposable-dir.js +25 -0
  50. package/dist/internal/disposable-dir.js.map +1 -0
  51. package/dist/internal/entry-chunks.d.ts +9 -0
  52. package/dist/internal/entry-chunks.d.ts.map +1 -0
  53. package/dist/internal/entry-chunks.js +15 -0
  54. package/dist/internal/entry-chunks.js.map +1 -0
  55. package/dist/internal/evidence-traversal.d.ts +12 -0
  56. package/dist/internal/evidence-traversal.d.ts.map +1 -0
  57. package/dist/internal/evidence-traversal.js +48 -0
  58. package/dist/internal/evidence-traversal.js.map +1 -0
  59. package/dist/internal/head-merge.d.ts +6 -0
  60. package/dist/internal/head-merge.d.ts.map +1 -1
  61. package/dist/internal/head-merge.js +9 -0
  62. package/dist/internal/head-merge.js.map +1 -1
  63. package/dist/internal/js-string.d.ts +2 -0
  64. package/dist/internal/js-string.d.ts.map +1 -1
  65. package/dist/internal/js-string.js +5 -1
  66. package/dist/internal/js-string.js.map +1 -1
  67. package/dist/internal/mount-shadow.js +1 -1
  68. package/dist/internal/mount-shadow.js.map +1 -1
  69. package/dist/internal/output-guard.d.ts +7 -0
  70. package/dist/internal/output-guard.d.ts.map +1 -1
  71. package/dist/internal/output-guard.js +80 -8
  72. package/dist/internal/output-guard.js.map +1 -1
  73. package/dist/internal/page-execution.d.ts +6 -0
  74. package/dist/internal/page-execution.d.ts.map +1 -1
  75. package/dist/internal/page-execution.js +377 -108
  76. package/dist/internal/page-execution.js.map +1 -1
  77. package/dist/internal/progressive-cookies.d.ts +14 -0
  78. package/dist/internal/progressive-cookies.d.ts.map +1 -0
  79. package/dist/internal/progressive-cookies.js +43 -0
  80. package/dist/internal/progressive-cookies.js.map +1 -0
  81. package/dist/internal/public-env.d.ts +2 -0
  82. package/dist/internal/public-env.d.ts.map +1 -0
  83. package/dist/internal/public-env.js +13 -0
  84. package/dist/internal/public-env.js.map +1 -0
  85. package/dist/internal/render-document.d.ts +1 -0
  86. package/dist/internal/render-document.d.ts.map +1 -1
  87. package/dist/internal/render-document.js +28 -5
  88. package/dist/internal/render-document.js.map +1 -1
  89. package/dist/internal/response-controls.d.ts +2 -0
  90. package/dist/internal/response-controls.d.ts.map +1 -1
  91. package/dist/internal/response-controls.js +26 -16
  92. package/dist/internal/response-controls.js.map +1 -1
  93. package/dist/internal/revive-dates.d.ts +16 -0
  94. package/dist/internal/revive-dates.d.ts.map +1 -0
  95. package/dist/internal/revive-dates.js +32 -0
  96. package/dist/internal/revive-dates.js.map +1 -0
  97. package/dist/internal/route-path.d.ts +8 -0
  98. package/dist/internal/route-path.d.ts.map +1 -0
  99. package/dist/internal/route-path.js +8 -0
  100. package/dist/internal/route-path.js.map +1 -0
  101. package/dist/internal/secret-scan.d.ts.map +1 -1
  102. package/dist/internal/secret-scan.js +88 -20
  103. package/dist/internal/secret-scan.js.map +1 -1
  104. package/dist/internal/server-boundary.d.ts.map +1 -1
  105. package/dist/internal/server-boundary.js +33 -5
  106. package/dist/internal/server-boundary.js.map +1 -1
  107. package/dist/internal/web-app.d.ts +21 -16
  108. package/dist/internal/web-app.d.ts.map +1 -1
  109. package/dist/internal/web-app.js +73 -21
  110. package/dist/internal/web-app.js.map +1 -1
  111. package/dist/internal/wire-date.d.ts +3 -0
  112. package/dist/internal/wire-date.d.ts.map +1 -0
  113. package/dist/internal/wire-date.js +14 -0
  114. package/dist/internal/wire-date.js.map +1 -0
  115. package/dist/internal/zone-graph.d.ts +4 -0
  116. package/dist/internal/zone-graph.d.ts.map +1 -1
  117. package/dist/internal/zone-graph.js +54 -14
  118. package/dist/internal/zone-graph.js.map +1 -1
  119. package/dist/isr.d.ts +58 -2
  120. package/dist/isr.d.ts.map +1 -1
  121. package/dist/isr.js +296 -39
  122. package/dist/isr.js.map +1 -1
  123. package/dist/llms-txt.d.ts +2 -0
  124. package/dist/llms-txt.d.ts.map +1 -1
  125. package/dist/llms-txt.js +29 -3
  126. package/dist/llms-txt.js.map +1 -1
  127. package/dist/manifest.d.ts +28 -4
  128. package/dist/manifest.d.ts.map +1 -1
  129. package/dist/manifest.js +3 -0
  130. package/dist/manifest.js.map +1 -1
  131. package/dist/open-resolved.d.ts +18 -0
  132. package/dist/open-resolved.d.ts.map +1 -0
  133. package/dist/open-resolved.js +62 -0
  134. package/dist/open-resolved.js.map +1 -0
  135. package/dist/plugins/kit.d.ts +1 -0
  136. package/dist/plugins/kit.d.ts.map +1 -1
  137. package/dist/plugins/kit.js +26 -11
  138. package/dist/plugins/kit.js.map +1 -1
  139. package/dist/plugins/vite-leak-guard.d.ts +3 -1
  140. package/dist/plugins/vite-leak-guard.d.ts.map +1 -1
  141. package/dist/plugins/vite-leak-guard.js +4 -0
  142. package/dist/plugins/vite-leak-guard.js.map +1 -1
  143. package/dist/public-dir.d.ts +8 -1
  144. package/dist/public-dir.d.ts.map +1 -1
  145. package/dist/public-dir.js +17 -6
  146. package/dist/public-dir.js.map +1 -1
  147. package/dist/query.d.ts +13 -4
  148. package/dist/query.d.ts.map +1 -1
  149. package/dist/query.js +89 -26
  150. package/dist/query.js.map +1 -1
  151. package/dist/render-seam.d.ts +2 -0
  152. package/dist/render-seam.d.ts.map +1 -1
  153. package/dist/render-seam.js +1 -0
  154. package/dist/render-seam.js.map +1 -1
  155. package/dist/route-types.d.ts.map +1 -1
  156. package/dist/route-types.js +20 -9
  157. package/dist/route-types.js.map +1 -1
  158. package/dist/router.d.ts +9 -2
  159. package/dist/router.d.ts.map +1 -1
  160. package/dist/router.js +39 -46
  161. package/dist/router.js.map +1 -1
  162. package/dist/server-components/build-info.d.ts +3 -0
  163. package/dist/server-components/build-info.d.ts.map +1 -0
  164. package/dist/server-components/build-info.js +7 -0
  165. package/dist/server-components/build-info.js.map +1 -0
  166. package/dist/server-components/build-target.d.ts +12 -0
  167. package/dist/server-components/build-target.d.ts.map +1 -0
  168. package/dist/server-components/build-target.js +153 -0
  169. package/dist/server-components/build-target.js.map +1 -0
  170. package/dist/server-components/build.d.ts +24 -0
  171. package/dist/server-components/build.d.ts.map +1 -0
  172. package/dist/server-components/build.js +366 -0
  173. package/dist/server-components/build.js.map +1 -0
  174. package/dist/server-components/bundle-proof.d.ts +3 -0
  175. package/dist/server-components/bundle-proof.d.ts.map +1 -0
  176. package/dist/server-components/bundle-proof.js +23 -0
  177. package/dist/server-components/bundle-proof.js.map +1 -0
  178. package/dist/server-components/client.d.ts +46 -0
  179. package/dist/server-components/client.d.ts.map +1 -0
  180. package/dist/server-components/client.js +594 -0
  181. package/dist/server-components/client.js.map +1 -0
  182. package/dist/server-components/definition.d.ts +11 -0
  183. package/dist/server-components/definition.d.ts.map +1 -0
  184. package/dist/server-components/definition.js +94 -0
  185. package/dist/server-components/definition.js.map +1 -0
  186. package/dist/server-components/dev-http.d.ts +5 -0
  187. package/dist/server-components/dev-http.d.ts.map +1 -0
  188. package/dist/server-components/dev-http.js +71 -0
  189. package/dist/server-components/dev-http.js.map +1 -0
  190. package/dist/server-components/error.d.ts +9 -0
  191. package/dist/server-components/error.d.ts.map +1 -0
  192. package/dist/server-components/error.js +24 -0
  193. package/dist/server-components/error.js.map +1 -0
  194. package/dist/server-components/frames.d.ts +7 -0
  195. package/dist/server-components/frames.d.ts.map +1 -0
  196. package/dist/server-components/frames.js +169 -0
  197. package/dist/server-components/frames.js.map +1 -0
  198. package/dist/server-components/html-policy.d.ts +6 -0
  199. package/dist/server-components/html-policy.d.ts.map +1 -0
  200. package/dist/server-components/html-policy.js +75 -0
  201. package/dist/server-components/html-policy.js.map +1 -0
  202. package/dist/server-components/html.d.ts +4 -0
  203. package/dist/server-components/html.d.ts.map +1 -0
  204. package/dist/server-components/html.js +67 -0
  205. package/dist/server-components/html.js.map +1 -0
  206. package/dist/server-components/islands.d.ts +44 -0
  207. package/dist/server-components/islands.d.ts.map +1 -0
  208. package/dist/server-components/islands.js +42 -0
  209. package/dist/server-components/islands.js.map +1 -0
  210. package/dist/server-components/native.d.ts +4 -0
  211. package/dist/server-components/native.d.ts.map +1 -0
  212. package/dist/server-components/native.js +81 -0
  213. package/dist/server-components/native.js.map +1 -0
  214. package/dist/server-components/portal.d.ts +6 -0
  215. package/dist/server-components/portal.d.ts.map +1 -0
  216. package/dist/server-components/portal.js +35 -0
  217. package/dist/server-components/portal.js.map +1 -0
  218. package/dist/server-components/props.d.ts +3 -0
  219. package/dist/server-components/props.d.ts.map +1 -0
  220. package/dist/server-components/props.js +14 -0
  221. package/dist/server-components/props.js.map +1 -0
  222. package/dist/server-components/protocol.d.ts +44 -0
  223. package/dist/server-components/protocol.d.ts.map +1 -0
  224. package/dist/server-components/protocol.js +139 -0
  225. package/dist/server-components/protocol.js.map +1 -0
  226. package/dist/server-components/range.d.ts +24 -0
  227. package/dist/server-components/range.d.ts.map +1 -0
  228. package/dist/server-components/range.js +263 -0
  229. package/dist/server-components/range.js.map +1 -0
  230. package/dist/server-components/registry.d.ts +6 -0
  231. package/dist/server-components/registry.d.ts.map +1 -0
  232. package/dist/server-components/registry.js +30 -0
  233. package/dist/server-components/registry.js.map +1 -0
  234. package/dist/server-components/response.d.ts +4 -0
  235. package/dist/server-components/response.d.ts.map +1 -0
  236. package/dist/server-components/response.js +32 -0
  237. package/dist/server-components/response.js.map +1 -0
  238. package/dist/server-components/router-integration.d.ts +9 -0
  239. package/dist/server-components/router-integration.d.ts.map +1 -0
  240. package/dist/server-components/router-integration.js +29 -0
  241. package/dist/server-components/router-integration.js.map +1 -0
  242. package/dist/server-components/runtime.d.ts +14 -0
  243. package/dist/server-components/runtime.d.ts.map +1 -0
  244. package/dist/server-components/runtime.js +276 -0
  245. package/dist/server-components/runtime.js.map +1 -0
  246. package/dist/server-components/scheduler.d.ts +14 -0
  247. package/dist/server-components/scheduler.d.ts.map +1 -0
  248. package/dist/server-components/scheduler.js +74 -0
  249. package/dist/server-components/scheduler.js.map +1 -0
  250. package/dist/server-components/schema.d.ts +4 -0
  251. package/dist/server-components/schema.d.ts.map +1 -0
  252. package/dist/server-components/schema.js +77 -0
  253. package/dist/server-components/schema.js.map +1 -0
  254. package/dist/server-components/server-entry.d.ts +12 -0
  255. package/dist/server-components/server-entry.d.ts.map +1 -0
  256. package/dist/server-components/server-entry.js +90 -0
  257. package/dist/server-components/server-entry.js.map +1 -0
  258. package/dist/server-components/source.d.ts +15 -0
  259. package/dist/server-components/source.d.ts.map +1 -0
  260. package/dist/server-components/source.js +269 -0
  261. package/dist/server-components/source.js.map +1 -0
  262. package/dist/server-components/state.d.ts +4 -0
  263. package/dist/server-components/state.d.ts.map +1 -0
  264. package/dist/server-components/state.js +123 -0
  265. package/dist/server-components/state.js.map +1 -0
  266. package/dist/server-components/stream.d.ts +6 -0
  267. package/dist/server-components/stream.d.ts.map +1 -0
  268. package/dist/server-components/stream.js +70 -0
  269. package/dist/server-components/stream.js.map +1 -0
  270. package/dist/server-components/trusted-html.d.ts +3 -0
  271. package/dist/server-components/trusted-html.d.ts.map +1 -0
  272. package/dist/server-components/trusted-html.js +14 -0
  273. package/dist/server-components/trusted-html.js.map +1 -0
  274. package/dist/server-components/types.d.ts +107 -0
  275. package/dist/server-components/types.d.ts.map +1 -0
  276. package/dist/server-components/types.js +2 -0
  277. package/dist/server-components/types.js.map +1 -0
  278. package/dist/server-components/typescript.d.ts +11 -0
  279. package/dist/server-components/typescript.d.ts.map +1 -0
  280. package/dist/server-components/typescript.js +28 -0
  281. package/dist/server-components/typescript.js.map +1 -0
  282. package/dist/server-components/value.d.ts +5 -0
  283. package/dist/server-components/value.d.ts.map +1 -0
  284. package/dist/server-components/value.js +77 -0
  285. package/dist/server-components/value.js.map +1 -0
  286. package/dist/server-components/vite.d.ts +12 -0
  287. package/dist/server-components/vite.d.ts.map +1 -0
  288. package/dist/server-components/vite.js +197 -0
  289. package/dist/server-components/vite.js.map +1 -0
  290. package/dist/server-components.d.ts +5 -0
  291. package/dist/server-components.d.ts.map +1 -0
  292. package/dist/server-components.js +4 -0
  293. package/dist/server-components.js.map +1 -0
  294. package/dist/service-worker.d.ts +8 -1
  295. package/dist/service-worker.d.ts.map +1 -1
  296. package/dist/service-worker.js +20 -6
  297. package/dist/service-worker.js.map +1 -1
  298. package/dist/vite.d.ts +12 -0
  299. package/dist/vite.d.ts.map +1 -1
  300. package/dist/vite.js +7 -1
  301. package/dist/vite.js.map +1 -1
  302. package/dist/zones.d.ts +40 -0
  303. package/dist/zones.d.ts.map +1 -1
  304. package/dist/zones.js +170 -6
  305. package/dist/zones.js.map +1 -1
  306. package/package.json +97 -4
  307. package/src/build-vite.ts +31 -9
  308. package/src/build.ts +109 -31
  309. package/src/cdn.ts +18 -12
  310. package/src/client.ts +25 -6
  311. package/src/deferred.ts +27 -9
  312. package/src/dev-feed.ts +24 -5
  313. package/src/dev-session.ts +4 -13
  314. package/src/dev.ts +29 -1
  315. package/src/draft.ts +35 -14
  316. package/src/fonts-google.ts +10 -0
  317. package/src/index.ts +3 -2
  318. package/src/internal/codegen.ts +60 -29
  319. package/src/internal/disposable-dir.ts +24 -0
  320. package/src/internal/entry-chunks.ts +18 -0
  321. package/src/internal/evidence-traversal.ts +57 -0
  322. package/src/internal/head-merge.ts +16 -0
  323. package/src/internal/js-string.ts +6 -1
  324. package/src/internal/mount-shadow.ts +1 -1
  325. package/src/internal/output-guard.ts +88 -7
  326. package/src/internal/page-execution.ts +506 -108
  327. package/src/internal/progressive-cookies.ts +60 -0
  328. package/src/internal/public-env.ts +14 -0
  329. package/src/internal/render-document.ts +26 -11
  330. package/src/internal/response-controls.ts +31 -14
  331. package/src/internal/revive-dates.ts +35 -0
  332. package/src/internal/route-path.ts +8 -0
  333. package/src/internal/secret-scan.ts +90 -18
  334. package/src/internal/server-boundary.ts +29 -5
  335. package/src/internal/web-app.ts +116 -37
  336. package/src/internal/wire-date.ts +16 -0
  337. package/src/internal/zone-graph.ts +59 -19
  338. package/src/isr.ts +351 -43
  339. package/src/llms-txt.ts +32 -3
  340. package/src/manifest.ts +28 -4
  341. package/src/open-resolved.ts +64 -0
  342. package/src/plugins/kit.ts +31 -11
  343. package/src/plugins/vite-leak-guard.ts +5 -1
  344. package/src/public-dir.ts +27 -8
  345. package/src/query.ts +107 -26
  346. package/src/render-seam.ts +2 -0
  347. package/src/route-types.ts +34 -7
  348. package/src/router.ts +58 -55
  349. package/src/server-components/build-info.ts +11 -0
  350. package/src/server-components/build-target.ts +194 -0
  351. package/src/server-components/build.ts +419 -0
  352. package/src/server-components/bundle-proof.ts +33 -0
  353. package/src/server-components/client.ts +663 -0
  354. package/src/server-components/definition.ts +125 -0
  355. package/src/server-components/dev-http.ts +75 -0
  356. package/src/server-components/error.ts +24 -0
  357. package/src/server-components/frames.ts +181 -0
  358. package/src/server-components/html-policy.ts +94 -0
  359. package/src/server-components/html.ts +81 -0
  360. package/src/server-components/islands.ts +111 -0
  361. package/src/server-components/native.ts +101 -0
  362. package/src/server-components/portal.ts +42 -0
  363. package/src/server-components/props.ts +19 -0
  364. package/src/server-components/protocol.ts +167 -0
  365. package/src/server-components/range.ts +313 -0
  366. package/src/server-components/registry.ts +34 -0
  367. package/src/server-components/response.ts +44 -0
  368. package/src/server-components/router-integration.ts +40 -0
  369. package/src/server-components/runtime.ts +376 -0
  370. package/src/server-components/scheduler.ts +84 -0
  371. package/src/server-components/schema.ts +69 -0
  372. package/src/server-components/server-entry.ts +98 -0
  373. package/src/server-components/source.ts +324 -0
  374. package/src/server-components/state.ts +143 -0
  375. package/src/server-components/stream.ts +71 -0
  376. package/src/server-components/trusted-html.ts +21 -0
  377. package/src/server-components/types.ts +138 -0
  378. package/src/server-components/typescript.ts +39 -0
  379. package/src/server-components/value.ts +82 -0
  380. package/src/server-components/vite.ts +242 -0
  381. package/src/server-components.ts +32 -0
  382. package/src/service-worker.ts +24 -7
  383. package/src/vite.ts +8 -1
  384. package/src/zones.ts +208 -7
package/src/deferred.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  * resolution scripts settle. The per-adapter `<Await>` lives in `@nifrajs/web-{solid,react}/client`.
7
7
  */
8
8
  import { plainJsonCodec, type TransportCodec } from "@nifrajs/core/transport-codec"
9
+ import { parsePageData } from "./internal/revive-dates.ts"
9
10
 
10
11
  /**
11
12
  * A loader value marked to stream in after the shell. The component consumes it with the adapter's
@@ -164,25 +165,29 @@ export function ndjsonStream(
164
165
  deferred: ReadonlyArray<{ readonly id: number; readonly promise: Promise<unknown> }>,
165
166
  codec: TransportCodec = plainJsonCodec,
166
167
  ): ReadableStream<Uint8Array> {
168
+ // A client that goes away cancels the body; a value settling after that has nowhere to go.
169
+ let cancelled = false
167
170
  return new ReadableStream({
168
171
  async start(controller) {
169
172
  controller.enqueue(NDJSON_ENCODER.encode(`${codec.encode(forClient)}\n`))
170
173
  await Promise.all(
171
174
  deferred.map(async (d) => {
175
+ let line: string
172
176
  try {
173
- const value = await d.promise
174
- controller.enqueue(NDJSON_ENCODER.encode(`${codec.encode({ i: d.id, v: value })}\n`))
177
+ line = codec.encode({ i: d.id, v: await d.promise })
175
178
  } catch (err) {
176
179
  // Redact: stream a stable opaque code, never the raw error text; log the real reason
177
180
  // server-side. The client's <Await> errorFallback receives this code.
178
181
  console.error("[nifra/web] deferred value rejected:", err)
179
- controller.enqueue(
180
- NDJSON_ENCODER.encode(`${codec.encode({ i: d.id, e: DEFERRED_ERROR_CODE })}\n`),
181
- )
182
+ line = codec.encode({ i: d.id, e: DEFERRED_ERROR_CODE })
182
183
  }
184
+ if (!cancelled) controller.enqueue(NDJSON_ENCODER.encode(`${line}\n`))
183
185
  }),
184
186
  )
185
- controller.close()
187
+ if (!cancelled) controller.close()
188
+ },
189
+ cancel() {
190
+ cancelled = true
186
191
  },
187
192
  })
188
193
  }
@@ -213,7 +218,16 @@ export const DEFERRED_RUNTIME = `(() => {
213
218
  // Tag the promise (status/value/reason) - React's use() reads them and returns SYNCHRONOUSLY at
214
219
  // hydration, so it renders the resolved content directly into the <Suspense> boundary the server
215
220
  // streamed (no re-suspend, no fallback flash). r/j also wake awaiters (Solid uses its own _$HY).
216
- window.__nifraResolve = (id, v) => { const e = get(id); e.p.status = "fulfilled"; e.p.value = v; e.r(v); };
221
+ // A t.date() value arrives tagged; rebuild it before anything reads the settled value.
222
+ const __nifraDates = (d) => {
223
+ if (!d || typeof d !== "object") return d
224
+ if (Array.isArray(d)) { for (let i = 0; i < d.length; i++) d[i] = __nifraDates(d[i]); return d }
225
+ const k = Object.keys(d)
226
+ if (k.length === 1 && k[0] === "__nifra_date" && typeof d.__nifra_date === "string") return new Date(d.__nifra_date)
227
+ for (const x of k) { const v = __nifraDates(d[x]); if (v !== d[x]) Object.defineProperty(d, x, { value: v, enumerable: true, writable: true, configurable: true }) }
228
+ return d
229
+ };
230
+ window.__nifraResolve = (id, v) => { v = __nifraDates(v); const e = get(id); e.p.status = "fulfilled"; e.p.value = v; e.r(v); };
217
231
  window.__nifraReject = (id, x) => { const e = get(id); e.p.status = "rejected"; e.p.reason = x; e.j(x); };
218
232
  })();`
219
233
 
@@ -225,6 +239,7 @@ export const DEFERRED_RUNTIME = `(() => {
225
239
  export const MAP_DEFERRED_SOURCE = `const mapDeferred = (d) => {
226
240
  if (!d || typeof d !== "object") return d
227
241
  if (typeof d.__nifra_deferred === "number") return { __nifra_deferred: true, id: d.__nifra_deferred, promise: window.__nifraDeferred(d.__nifra_deferred) }
242
+ if (typeof d.__nifra_date === "string" && Object.keys(d).length === 1) return new Date(d.__nifra_date)
228
243
  if (Array.isArray(d)) return d.map(mapDeferred)
229
244
  const out = {}
230
245
  for (const k of Object.keys(d)) Object.defineProperty(out, k, { value: mapDeferred(d[k]), enumerable: true, writable: true, configurable: true })
@@ -326,8 +341,11 @@ export async function parseNdjsonData(
326
341
  }
327
342
 
328
343
  const pending: Pending = new Map()
344
+ // Only plain JSON carries t.date() tags; a richer codec decodes its own dates.
345
+ const decode = (line: string): unknown =>
346
+ codec === plainJsonCodec ? parsePageData(line) : codec.decode(line)
329
347
  const first = await readLine()
330
- const data = markersFromPlaceholders(first === null ? null : codec.decode(first), pending)
348
+ const data = markersFromPlaceholders(first === null ? null : decode(first), pending)
331
349
 
332
350
  void (async () => {
333
351
  try {
@@ -335,7 +353,7 @@ export async function parseNdjsonData(
335
353
  const line = await readLine()
336
354
  if (line === null) break
337
355
  if (line === "") continue
338
- const msg = codec.decode(line) as { i: number; v?: unknown; e?: string }
356
+ const msg = decode(line) as { i: number; v?: unknown; e?: string }
339
357
  const entry = pending.get(msg.i)
340
358
  if (entry === undefined) continue
341
359
  pending.delete(msg.i)
package/src/dev-feed.ts CHANGED
@@ -392,6 +392,25 @@ function fnv1a(text: string): string {
392
392
  return (hash >>> 0).toString(16).padStart(8, "0")
393
393
  }
394
394
 
395
+ /**
396
+ * A query value may be a single-use ticket or a token no redaction pattern knows, so the feed keeps
397
+ * which keys a request sent and never their values. `/x?ticket=abc&page` -> `/x?ticket=[redacted]&[redacted]`.
398
+ */
399
+ function queryKeysOnly(target: string): string {
400
+ const at = target.indexOf("?")
401
+ if (at === -1) return target
402
+ const hash = target.indexOf("#", at)
403
+ const parts = target
404
+ .slice(at + 1, hash === -1 ? undefined : hash)
405
+ .split("&")
406
+ .filter((part) => part.length > 0)
407
+ .map((part) => {
408
+ const eq = part.indexOf("=")
409
+ return eq === -1 ? "[redacted]" : `${part.slice(0, eq)}=[redacted]`
410
+ })
411
+ return parts.length === 0 ? target.slice(0, at) : `${target.slice(0, at)}?${parts.join("&")}`
412
+ }
413
+
395
414
  const cap = (text: string, max: number): string =>
396
415
  text.length <= max ? text : `${text.slice(0, max)}... [${text.length - max} more chars]`
397
416
 
@@ -477,7 +496,7 @@ export function createDevFeed(options: DevFeedOptions): DevFeed {
477
496
  if (diagnostic.request !== undefined) {
478
497
  redacted.request = {
479
498
  method: diagnostic.request.method,
480
- url: cap(redactText(diagnostic.request.url), 2048),
499
+ url: cap(redactText(queryKeysOnly(diagnostic.request.url)), 2048),
481
500
  }
482
501
  }
483
502
  if (diagnostic.codeframe !== undefined) {
@@ -530,7 +549,7 @@ export function createDevFeed(options: DevFeedOptions): DevFeed {
530
549
  record.resolvedAt = undefined
531
550
  if (requestId !== undefined) record.requestId = requestId
532
551
  if (meta.route !== undefined) record.route = meta.route
533
- if (meta.page !== undefined) record.page = meta.page
552
+ if (meta.page !== undefined) record.page = cap(redactText(queryKeysOnly(meta.page)), 2048)
534
553
  // Re-insert: Map order is recency order, which eviction relies on.
535
554
  errors.delete(key)
536
555
  errors.set(key, record)
@@ -557,7 +576,7 @@ export function createDevFeed(options: DevFeedOptions): DevFeed {
557
576
  message: text,
558
577
  }
559
578
  if (requestId !== undefined) entry.requestId = requestId
560
- if (meta.page !== undefined) entry.page = meta.page
579
+ if (meta.page !== undefined) entry.page = cap(redactText(queryKeysOnly(meta.page)), 2048)
561
580
  logs.push(entry)
562
581
  logBytes += text.length
563
582
  if (logs.length > limits.logs || logBytes > limits.logBytes) {
@@ -607,7 +626,7 @@ export function createDevFeed(options: DevFeedOptions): DevFeed {
607
626
  seq,
608
627
  requestId,
609
628
  method,
610
- path: cap(redactText(path), 2048),
629
+ path: cap(redactText(queryKeysOnly(path)), 2048),
611
630
  at: nowIso(),
612
631
  status: undefined,
613
632
  durationMs: undefined,
@@ -856,7 +875,7 @@ export function writeDevServerRecord(root: string, record: DevServerRecord): voi
856
875
 
857
876
  /** Remove the record only while it is still this server's: a newer server may have replaced it. */
858
877
  export function removeDevServerRecord(root: string, token: string): void {
859
- // @nifra-gate-reviewed: the server comparing its own token with a file on this machine; no request
878
+ // nifra-expect NF-S002: the server comparing its own token with a file on this machine; no request
860
879
  // reaches this comparison.
861
880
  if (readDevServerRecord(root)?.token !== token) return
862
881
  rmSync(join(root, DEV_SERVER_RECORD_FILE), { force: true })
@@ -8,6 +8,7 @@
8
8
  * either. `/__nifra/last-error` keeps its older, token-free contract behind the same Host check.
9
9
  */
10
10
  import { performance } from "node:perf_hooks"
11
+ import { writableResponse } from "@nifrajs/core/server"
11
12
  import {
12
13
  type BrowserError,
13
14
  CLIENT_BATCH_MAX_BYTES,
@@ -649,19 +650,9 @@ export function createDevSession(options: DevSessionOptions): DevSession {
649
650
  },
650
651
  performance.now() - started,
651
652
  )
652
- try {
653
- response.headers.set(DEV_REQUEST_ID_HEADER, requestId)
654
- return response
655
- } catch {
656
- // `Response.redirect()` and fetched responses carry immutable headers on some runtimes.
657
- const headers = new Headers(response.headers)
658
- headers.set(DEV_REQUEST_ID_HEADER, requestId)
659
- return new Response(response.body, {
660
- status: response.status,
661
- statusText: response.statusText,
662
- headers,
663
- })
664
- }
653
+ const target = writableResponse(response)
654
+ target.headers.set(DEV_REQUEST_ID_HEADER, requestId)
655
+ return target
665
656
  }
666
657
 
667
658
  return {
package/src/dev.ts CHANGED
@@ -58,7 +58,12 @@ import { dirname, relative, resolve } from "node:path"
58
58
  import { type BuildClientOptions, buildClient } from "./build.ts"
59
59
  import { type DevEntryMatch, resolveDevEntry } from "./bun-dev-entry.ts"
60
60
  import { explainBindFailure } from "./dev-port.ts"
61
- import { createDevSession, type DevAppHooks, type DevSession } from "./dev-session.ts"
61
+ import {
62
+ createDevSession,
63
+ type DevAppHooks,
64
+ type DevSession,
65
+ isLoopbackHost,
66
+ } from "./dev-session.ts"
62
67
  import { createSsrGraph, type SsrGraph } from "./dev-ssr-graph.ts"
63
68
  import { discoverRoutes } from "./fs.ts"
64
69
  import { DEFAULT_DEV_PORT, generateClientEntry } from "./index.ts"
@@ -112,6 +117,13 @@ export interface DevServerOptions extends Omit<BuildClientOptions, "minify"> {
112
117
  * fix (default `true`). Off, the errors still reach the feed.
113
118
  */
114
119
  readonly indicator?: boolean
120
+ /**
121
+ * Host names the dev server answers besides loopback ones (`localhost`, `*.localhost`, `127.0.0.1`,
122
+ * `[::1]`), port ignored. Every other `Host` gets a 403: a page on another site whose name a DNS
123
+ * rebind points at 127.0.0.1 would otherwise read the dev server's pages, loader data and error
124
+ * overlay. Add a name here when a hosts-file entry or tunnel reaches the server under it.
125
+ */
126
+ readonly allowedHosts?: readonly string[]
115
127
  }
116
128
 
117
129
  export interface DevServer {
@@ -465,6 +477,7 @@ export async function createDevServer(options: DevServerOptions): Promise<DevSer
465
477
  }
466
478
  }
467
479
 
480
+ const allowedHosts = new Set((options.allowedHosts ?? []).map(hostName))
468
481
  try {
469
482
  server = serve({
470
483
  port,
@@ -474,6 +487,12 @@ export async function createDevServer(options: DevServerOptions): Promise<DevSer
474
487
  routes: { [PROBE_PATH]: htmlModule.default },
475
488
  fetch: async (req: Request): Promise<Response> => {
476
489
  const url = new URL(req.url)
490
+ const host = req.headers.get("host") ?? url.host
491
+ if (!isLoopbackHost(host) && !allowedHosts.has(hostName(host)))
492
+ return new Response(
493
+ `[nifra/web/dev] ${hostName(host)} is not a loopback host; add it to allowedHosts if this server should answer it.\n`,
494
+ { status: 403, headers: { "content-type": "text/plain; charset=utf-8" } },
495
+ )
477
496
  // The stable client-entry URL. Pages reference THIS; it redirects to whichever hashed chunk Bun
478
497
  // is serving at the moment the browser asks. A redirect rather than a proxy so the module's final
479
498
  // URL is Bun's own - its HMR client derives its socket and module identity from `import.meta.url`,
@@ -620,6 +639,15 @@ export async function createDevServer(options: DevServerOptions): Promise<DevSer
620
639
  * whose message is just "Bundle failed" - the actionable part (which file, which import) lives in
621
640
  * `.errors`. Surface it, or the guard reports a failure while hiding the reason.
622
641
  */
642
+ /** A Host header's name: lowercased, without its port or IPv6 brackets. */
643
+ function hostName(host: string): string {
644
+ const name = host.trim().toLowerCase()
645
+ if (name.startsWith("["))
646
+ return name.slice(1, name.indexOf("]") === -1 ? undefined : name.indexOf("]"))
647
+ const colon = name.lastIndexOf(":")
648
+ return colon === -1 ? name : name.slice(0, colon)
649
+ }
650
+
623
651
  export function buildFailureDetail(err: unknown): string {
624
652
  return err instanceof AggregateError && err.errors.length > 0
625
653
  ? err.errors.map((e) => ` ${e instanceof Error ? e.message : String(e)}`).join("\n")
package/src/draft.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  /**
2
2
  * Draft / preview mode - let an editor see unpublished content by flipping a signed cookie that
3
3
  * loaders read (`ctx.draft`) and ISR bypasses (fresh render, never cached). The cookie is **HMAC-signed**
4
- * (so a visitor can't forge it) and **HttpOnly** (JS can't read it). You gate `enableDraft` yourself -
4
+ * (so a visitor can't forge it), **HttpOnly** (JS can't read it) and carries its own expiry, which the
5
+ * server checks (a copied cookie stops working when the browser would have dropped it). You gate
6
+ * `enableDraft` yourself -
5
7
  * behind an editor login or a `?token=` check - exactly like Next's `draftMode().enable()`.
6
8
  *
7
9
  * // a route you've protected (e.g. checked ?token= against a secret):
@@ -26,9 +28,23 @@ import { assertTokenSecret, timingSafeEqual } from "./internal/timing-safe-equal
26
28
  /** The cookie name nifra uses for draft/preview mode. */
27
29
  export const DRAFT_COOKIE = "__nifra_draft"
28
30
 
29
- // The signed cookie's payload is a fixed marker - a *valid signature* (not the value) is the signal
30
- // that the server issued this via enableDraft(). Nothing secret is stored in it.
31
- const DRAFT_MARKER = "1"
31
+ // The signed payload is `1:<expiry in Unix seconds>`. A valid signature says the server issued it; the
32
+ // expiry bounds a replayed copy, which Max-Age alone cannot, since a client can resend any cookie it holds.
33
+ const DRAFT_PREFIX = "1:"
34
+
35
+ function draftPayload(options: EnableDraftOptions): string {
36
+ return `${DRAFT_PREFIX}${Math.floor(Date.now() / 1000) + draftMaxAge(options)}`
37
+ }
38
+
39
+ function draftMaxAge(options: EnableDraftOptions): number {
40
+ const seconds = options.maxAgeSeconds ?? 3600
41
+ if (!Number.isSafeInteger(seconds) || seconds <= 0) {
42
+ throw new RangeError(
43
+ `[nifra] draft maxAgeSeconds must be a positive whole number of seconds, got ${seconds}`,
44
+ )
45
+ }
46
+ return seconds
47
+ }
32
48
 
33
49
  /** The response-cookie surface `enableDraft`/`disableDraft` need - nifra's `c.set`. Structural, so any
34
50
  * nifra handler context satisfies it without importing the full `Context`. */
@@ -38,7 +54,8 @@ export interface DraftCookieControls {
38
54
  }
39
55
 
40
56
  export interface EnableDraftOptions {
41
- /** Cookie lifetime in **seconds** (default `3600` = 1h). Keep it short - draft is an editor session. */
57
+ /** Draft session lifetime in **seconds** (default `3600` = 1h): the cookie's `Max-Age`, and the expiry
58
+ * signed into the cookie that the server enforces. Keep it short - draft is an editor session. */
42
59
  readonly maxAgeSeconds?: number
43
60
  /** Cookie `Path` (default `"/"`). */
44
61
  readonly path?: string
@@ -57,8 +74,8 @@ export async function enableDraft(
57
74
  secret: string,
58
75
  options: EnableDraftOptions = {},
59
76
  ): Promise<void> {
60
- const signed = await signValue(DRAFT_MARKER, secret)
61
- c.set.cookie(DRAFT_COOKIE, signed, draftCookieOptions(options))
77
+ const cookieOptions = draftCookieOptions(options)
78
+ c.set.cookie(DRAFT_COOKIE, await signValue(draftPayload(options), secret), cookieOptions)
62
79
  }
63
80
 
64
81
  /**
@@ -77,7 +94,7 @@ function draftCookieOptions(options: EnableDraftOptions): CookieOptions {
77
94
  httpOnly: true,
78
95
  sameSite: "lax",
79
96
  path: options.path ?? "/",
80
- maxAge: options.maxAgeSeconds ?? 3600,
97
+ maxAge: draftMaxAge(options),
81
98
  secure: options.secure ?? true,
82
99
  }
83
100
  }
@@ -91,14 +108,17 @@ export function disableDraft(
91
108
  }
92
109
 
93
110
  /**
94
- * Whether `request` carries a **valid** signed draft cookie (constant-time verify via `unsignValue`).
95
- * `createWebApp` uses it to set `ctx.draft`; `withISR` uses it to bypass the cache for editors. A
96
- * missing, forged, or tampered cookie returns `false`.
111
+ * Whether `request` carries a **valid**, unexpired signed draft cookie (constant-time verify via
112
+ * `unsignValue`). `createWebApp` uses it to set `ctx.draft`; `withISR` uses it to bypass the cache for
113
+ * editors. A missing, forged, tampered, or expired cookie returns `false`.
97
114
  */
98
115
  export async function isDraftEnabled(request: Request, secret: string): Promise<boolean> {
99
116
  const cookie = parseCookies(request.headers.get("cookie"))[DRAFT_COOKIE]
100
117
  if (cookie === undefined) return false
101
- return (await unsignValue(cookie, secret)) !== null
118
+ const payload = await unsignValue(cookie, secret)
119
+ if (payload === null || !payload.startsWith(DRAFT_PREFIX)) return false
120
+ const expires = Number(payload.slice(DRAFT_PREFIX.length))
121
+ return Number.isSafeInteger(expires) && Date.now() / 1000 < expires
102
122
  }
103
123
 
104
124
  /**
@@ -173,7 +193,8 @@ export function previewEndpoint(
173
193
  `[nifra] previewEndpoint fallbackPath must be a site-relative path, got ${JSON.stringify(fallbackPath)}`,
174
194
  )
175
195
  }
176
- const cookieOptions = draftCookieOptions(options.cookie ?? {})
196
+ const draftOptions = options.cookie ?? {}
197
+ const cookieOptions = draftCookieOptions(draftOptions)
177
198
  return async (request) => {
178
199
  const url = new URL(request.url)
179
200
  if (!timingSafeEqual(url.searchParams.get(tokenParam) ?? "", options.secret)) {
@@ -184,7 +205,7 @@ export function previewEndpoint(
184
205
  if (destination === null) {
185
206
  return Response.json({ ok: false, error: "invalid_redirect" }, { status: 400 })
186
207
  }
187
- const signed = await signValue(DRAFT_MARKER, options.draftSecret)
208
+ const signed = await signValue(draftPayload(draftOptions), options.draftSecret)
188
209
  return new Response(null, {
189
210
  status: 302,
190
211
  headers: {
@@ -113,6 +113,10 @@ const DEFAULT_MAX_FONT_BYTES = 5 * 1024 * 1024
113
113
  const MAX_CSS_BYTES = 512 * 1024
114
114
  const MAX_TEXT_LEN = 2048
115
115
 
116
+ /** A face's weight as Google writes it: `400`, a variable `100 900`, or a keyword. It lands in the
117
+ * written file's name, so anything else (a tampered `400/../..`) is refused. */
118
+ const FACE_WEIGHT = /^(?:\d{1,4}(?: \d{1,4})?|normal|bold)$/
119
+
116
120
  const VALID_DISPLAY: ReadonlySet<string> = new Set([
117
121
  "auto",
118
122
  "block",
@@ -420,6 +424,12 @@ export async function loadGoogleFont(
420
424
  "the stylesheet may be tampered with",
421
425
  )
422
426
  }
427
+ if (!FACE_WEIGHT.test(face.weight)) {
428
+ throw new Error(
429
+ `loadGoogleFont: refusing an unexpected font-weight ${JSON.stringify(face.weight.slice(0, 40))}; ` +
430
+ "the stylesheet may be tampered with",
431
+ )
432
+ }
423
433
  const bytes = await fetchBounded(fetchImpl, source.url, maxBytes)
424
434
  const hash = await contentHash(bytes)
425
435
  const style = face.style === "italic" ? "italic" : "normal"
package/src/index.ts CHANGED
@@ -85,8 +85,8 @@ export {
85
85
  generateServerManifest,
86
86
  } from "./internal/codegen.ts"
87
87
  // Generated client entries intentionally import the browser router from the client subpath:
88
- // `import { createClientRouter, createMatcher, mergeHeads, resolveMeta } from "@nifrajs/web/client"`.
89
- export { mergeHeads, resolveMeta } from "./internal/head-merge.ts"
88
+ // `import { createClientRouter, createMatcher, mergeHeads, resolveChainMeta } from "@nifrajs/web/client"`.
89
+ export { mergeHeads, resolveChainMeta, resolveMeta } from "./internal/head-merge.ts"
90
90
  export {
91
91
  canonical,
92
92
  gone,
@@ -141,6 +141,7 @@ export {
141
141
  type ISROptions,
142
142
  type ISRPlatform,
143
143
  type ISRQuery,
144
+ type ISRStoreOp,
144
145
  isCacheablePage,
145
146
  KVCacheStore,
146
147
  type KVCacheStoreOptions,