@nifrajs/web 4.0.2 → 5.1.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 (426) hide show
  1. package/README.md +19 -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 +6 -6
  15. package/dist/client.d.ts.map +1 -1
  16. package/dist/client.js +46 -18
  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-port.d.ts +3 -2
  26. package/dist/dev-port.d.ts.map +1 -1
  27. package/dist/dev-port.js +2 -6
  28. package/dist/dev-port.js.map +1 -1
  29. package/dist/dev-session.d.ts.map +1 -1
  30. package/dist/dev-session.js +4 -14
  31. package/dist/dev-session.js.map +1 -1
  32. package/dist/dev.d.ts +7 -17
  33. package/dist/dev.d.ts.map +1 -1
  34. package/dist/dev.js +13 -1
  35. package/dist/dev.js.map +1 -1
  36. package/dist/draft.d.ts +8 -5
  37. package/dist/draft.d.ts.map +1 -1
  38. package/dist/draft.js +30 -13
  39. package/dist/draft.js.map +1 -1
  40. package/dist/fonts-google.d.ts.map +1 -1
  41. package/dist/fonts-google.js +7 -0
  42. package/dist/fonts-google.js.map +1 -1
  43. package/dist/index.d.ts +5 -4
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +4 -3
  46. package/dist/index.js.map +1 -1
  47. package/dist/internal/codegen.d.ts +1 -0
  48. package/dist/internal/codegen.d.ts.map +1 -1
  49. package/dist/internal/codegen.js +56 -29
  50. package/dist/internal/codegen.js.map +1 -1
  51. package/dist/internal/disposable-dir.d.ts +8 -0
  52. package/dist/internal/disposable-dir.d.ts.map +1 -0
  53. package/dist/internal/disposable-dir.js +25 -0
  54. package/dist/internal/disposable-dir.js.map +1 -0
  55. package/dist/internal/entry-chunks.d.ts +9 -0
  56. package/dist/internal/entry-chunks.d.ts.map +1 -0
  57. package/dist/internal/entry-chunks.js +15 -0
  58. package/dist/internal/entry-chunks.js.map +1 -0
  59. package/dist/internal/evidence-traversal.d.ts +12 -0
  60. package/dist/internal/evidence-traversal.d.ts.map +1 -0
  61. package/dist/internal/evidence-traversal.js +48 -0
  62. package/dist/internal/evidence-traversal.js.map +1 -0
  63. package/dist/internal/head-merge.d.ts +6 -0
  64. package/dist/internal/head-merge.d.ts.map +1 -1
  65. package/dist/internal/head-merge.js +9 -0
  66. package/dist/internal/head-merge.js.map +1 -1
  67. package/dist/internal/js-string.d.ts +2 -0
  68. package/dist/internal/js-string.d.ts.map +1 -1
  69. package/dist/internal/js-string.js +5 -1
  70. package/dist/internal/js-string.js.map +1 -1
  71. package/dist/internal/mount-shadow.js +1 -1
  72. package/dist/internal/mount-shadow.js.map +1 -1
  73. package/dist/internal/output-guard.d.ts +7 -0
  74. package/dist/internal/output-guard.d.ts.map +1 -1
  75. package/dist/internal/output-guard.js +80 -8
  76. package/dist/internal/output-guard.js.map +1 -1
  77. package/dist/internal/package-entries.d.ts +34 -0
  78. package/dist/internal/package-entries.d.ts.map +1 -0
  79. package/dist/internal/package-entries.js +324 -0
  80. package/dist/internal/package-entries.js.map +1 -0
  81. package/dist/internal/page-execution.d.ts +6 -0
  82. package/dist/internal/page-execution.d.ts.map +1 -1
  83. package/dist/internal/page-execution.js +437 -114
  84. package/dist/internal/page-execution.js.map +1 -1
  85. package/dist/internal/parity.d.ts.map +1 -1
  86. package/dist/internal/parity.js +15 -2
  87. package/dist/internal/parity.js.map +1 -1
  88. package/dist/internal/private-env.d.ts +8 -0
  89. package/dist/internal/private-env.d.ts.map +1 -1
  90. package/dist/internal/private-env.js +25 -10
  91. package/dist/internal/private-env.js.map +1 -1
  92. package/dist/internal/progressive-cookies.d.ts +14 -0
  93. package/dist/internal/progressive-cookies.d.ts.map +1 -0
  94. package/dist/internal/progressive-cookies.js +43 -0
  95. package/dist/internal/progressive-cookies.js.map +1 -0
  96. package/dist/internal/public-env.d.ts +2 -0
  97. package/dist/internal/public-env.d.ts.map +1 -0
  98. package/dist/internal/public-env.js +13 -0
  99. package/dist/internal/public-env.js.map +1 -0
  100. package/dist/internal/render-document.d.ts +5 -44
  101. package/dist/internal/render-document.d.ts.map +1 -1
  102. package/dist/internal/render-document.js +51 -63
  103. package/dist/internal/render-document.js.map +1 -1
  104. package/dist/internal/response-controls.d.ts +2 -0
  105. package/dist/internal/response-controls.d.ts.map +1 -1
  106. package/dist/internal/response-controls.js +26 -16
  107. package/dist/internal/response-controls.js.map +1 -1
  108. package/dist/internal/revive-dates.d.ts +16 -0
  109. package/dist/internal/revive-dates.d.ts.map +1 -0
  110. package/dist/internal/revive-dates.js +32 -0
  111. package/dist/internal/revive-dates.js.map +1 -0
  112. package/dist/internal/route-path.d.ts +14 -0
  113. package/dist/internal/route-path.d.ts.map +1 -0
  114. package/dist/internal/route-path.js +19 -0
  115. package/dist/internal/route-path.js.map +1 -0
  116. package/dist/internal/secret-scan.d.ts.map +1 -1
  117. package/dist/internal/secret-scan.js +190 -20
  118. package/dist/internal/secret-scan.js.map +1 -1
  119. package/dist/internal/server-boundary.d.ts.map +1 -1
  120. package/dist/internal/server-boundary.js +33 -5
  121. package/dist/internal/server-boundary.js.map +1 -1
  122. package/dist/internal/web-app.d.ts +25 -18
  123. package/dist/internal/web-app.d.ts.map +1 -1
  124. package/dist/internal/web-app.js +81 -24
  125. package/dist/internal/web-app.js.map +1 -1
  126. package/dist/internal/wire-date.d.ts +3 -0
  127. package/dist/internal/wire-date.d.ts.map +1 -0
  128. package/dist/internal/wire-date.js +14 -0
  129. package/dist/internal/wire-date.js.map +1 -0
  130. package/dist/internal/without-query.d.ts +4 -0
  131. package/dist/internal/without-query.d.ts.map +1 -0
  132. package/dist/internal/without-query.js +9 -0
  133. package/dist/internal/without-query.js.map +1 -0
  134. package/dist/internal/zone-graph.d.ts +4 -0
  135. package/dist/internal/zone-graph.d.ts.map +1 -1
  136. package/dist/internal/zone-graph.js +71 -20
  137. package/dist/internal/zone-graph.js.map +1 -1
  138. package/dist/isr.d.ts +59 -3
  139. package/dist/isr.d.ts.map +1 -1
  140. package/dist/isr.js +311 -41
  141. package/dist/isr.js.map +1 -1
  142. package/dist/llms-txt.d.ts +2 -0
  143. package/dist/llms-txt.d.ts.map +1 -1
  144. package/dist/llms-txt.js +29 -3
  145. package/dist/llms-txt.js.map +1 -1
  146. package/dist/manifest.d.ts +37 -4
  147. package/dist/manifest.d.ts.map +1 -1
  148. package/dist/manifest.js +4 -0
  149. package/dist/manifest.js.map +1 -1
  150. package/dist/open-resolved.d.ts +18 -0
  151. package/dist/open-resolved.d.ts.map +1 -0
  152. package/dist/open-resolved.js +62 -0
  153. package/dist/open-resolved.js.map +1 -0
  154. package/dist/plugins/kit.d.ts +1 -0
  155. package/dist/plugins/kit.d.ts.map +1 -1
  156. package/dist/plugins/kit.js +26 -11
  157. package/dist/plugins/kit.js.map +1 -1
  158. package/dist/plugins/vite-leak-guard.d.ts +9 -1
  159. package/dist/plugins/vite-leak-guard.d.ts.map +1 -1
  160. package/dist/plugins/vite-leak-guard.js +84 -7
  161. package/dist/plugins/vite-leak-guard.js.map +1 -1
  162. package/dist/plugins/vite-zone-guard.d.ts.map +1 -1
  163. package/dist/plugins/vite-zone-guard.js +17 -21
  164. package/dist/plugins/vite-zone-guard.js.map +1 -1
  165. package/dist/plugins/zone-guard.d.ts.map +1 -1
  166. package/dist/plugins/zone-guard.js +44 -3
  167. package/dist/plugins/zone-guard.js.map +1 -1
  168. package/dist/prerender.d.ts +5 -1
  169. package/dist/prerender.d.ts.map +1 -1
  170. package/dist/prerender.js +41 -19
  171. package/dist/prerender.js.map +1 -1
  172. package/dist/public-dir.d.ts +8 -1
  173. package/dist/public-dir.d.ts.map +1 -1
  174. package/dist/public-dir.js +17 -6
  175. package/dist/public-dir.js.map +1 -1
  176. package/dist/query.d.ts +13 -4
  177. package/dist/query.d.ts.map +1 -1
  178. package/dist/query.js +89 -26
  179. package/dist/query.js.map +1 -1
  180. package/dist/render-seam.d.ts +25 -2
  181. package/dist/render-seam.d.ts.map +1 -1
  182. package/dist/render-seam.js +1 -0
  183. package/dist/render-seam.js.map +1 -1
  184. package/dist/route-types.d.ts.map +1 -1
  185. package/dist/route-types.js +20 -9
  186. package/dist/route-types.js.map +1 -1
  187. package/dist/router.d.ts +9 -2
  188. package/dist/router.d.ts.map +1 -1
  189. package/dist/router.js +47 -50
  190. package/dist/router.js.map +1 -1
  191. package/dist/seo.d.ts +49 -0
  192. package/dist/seo.d.ts.map +1 -0
  193. package/dist/seo.js +48 -0
  194. package/dist/seo.js.map +1 -0
  195. package/dist/server-components/build-info.d.ts +3 -0
  196. package/dist/server-components/build-info.d.ts.map +1 -0
  197. package/dist/server-components/build-info.js +7 -0
  198. package/dist/server-components/build-info.js.map +1 -0
  199. package/dist/server-components/build-target.d.ts +12 -0
  200. package/dist/server-components/build-target.d.ts.map +1 -0
  201. package/dist/server-components/build-target.js +153 -0
  202. package/dist/server-components/build-target.js.map +1 -0
  203. package/dist/server-components/build.d.ts +24 -0
  204. package/dist/server-components/build.d.ts.map +1 -0
  205. package/dist/server-components/build.js +366 -0
  206. package/dist/server-components/build.js.map +1 -0
  207. package/dist/server-components/bundle-proof.d.ts +3 -0
  208. package/dist/server-components/bundle-proof.d.ts.map +1 -0
  209. package/dist/server-components/bundle-proof.js +23 -0
  210. package/dist/server-components/bundle-proof.js.map +1 -0
  211. package/dist/server-components/client.d.ts +46 -0
  212. package/dist/server-components/client.d.ts.map +1 -0
  213. package/dist/server-components/client.js +594 -0
  214. package/dist/server-components/client.js.map +1 -0
  215. package/dist/server-components/definition.d.ts +11 -0
  216. package/dist/server-components/definition.d.ts.map +1 -0
  217. package/dist/server-components/definition.js +94 -0
  218. package/dist/server-components/definition.js.map +1 -0
  219. package/dist/server-components/dev-http.d.ts +5 -0
  220. package/dist/server-components/dev-http.d.ts.map +1 -0
  221. package/dist/server-components/dev-http.js +71 -0
  222. package/dist/server-components/dev-http.js.map +1 -0
  223. package/dist/server-components/error.d.ts +9 -0
  224. package/dist/server-components/error.d.ts.map +1 -0
  225. package/dist/server-components/error.js +24 -0
  226. package/dist/server-components/error.js.map +1 -0
  227. package/dist/server-components/frames.d.ts +7 -0
  228. package/dist/server-components/frames.d.ts.map +1 -0
  229. package/dist/server-components/frames.js +169 -0
  230. package/dist/server-components/frames.js.map +1 -0
  231. package/dist/server-components/html-policy.d.ts +6 -0
  232. package/dist/server-components/html-policy.d.ts.map +1 -0
  233. package/dist/server-components/html-policy.js +75 -0
  234. package/dist/server-components/html-policy.js.map +1 -0
  235. package/dist/server-components/html.d.ts +4 -0
  236. package/dist/server-components/html.d.ts.map +1 -0
  237. package/dist/server-components/html.js +67 -0
  238. package/dist/server-components/html.js.map +1 -0
  239. package/dist/server-components/islands.d.ts +44 -0
  240. package/dist/server-components/islands.d.ts.map +1 -0
  241. package/dist/server-components/islands.js +42 -0
  242. package/dist/server-components/islands.js.map +1 -0
  243. package/dist/server-components/native.d.ts +4 -0
  244. package/dist/server-components/native.d.ts.map +1 -0
  245. package/dist/server-components/native.js +81 -0
  246. package/dist/server-components/native.js.map +1 -0
  247. package/dist/server-components/portal.d.ts +6 -0
  248. package/dist/server-components/portal.d.ts.map +1 -0
  249. package/dist/server-components/portal.js +35 -0
  250. package/dist/server-components/portal.js.map +1 -0
  251. package/dist/server-components/props.d.ts +3 -0
  252. package/dist/server-components/props.d.ts.map +1 -0
  253. package/dist/server-components/props.js +14 -0
  254. package/dist/server-components/props.js.map +1 -0
  255. package/dist/server-components/protocol.d.ts +44 -0
  256. package/dist/server-components/protocol.d.ts.map +1 -0
  257. package/dist/server-components/protocol.js +139 -0
  258. package/dist/server-components/protocol.js.map +1 -0
  259. package/dist/server-components/range.d.ts +24 -0
  260. package/dist/server-components/range.d.ts.map +1 -0
  261. package/dist/server-components/range.js +263 -0
  262. package/dist/server-components/range.js.map +1 -0
  263. package/dist/server-components/registry.d.ts +6 -0
  264. package/dist/server-components/registry.d.ts.map +1 -0
  265. package/dist/server-components/registry.js +30 -0
  266. package/dist/server-components/registry.js.map +1 -0
  267. package/dist/server-components/response.d.ts +4 -0
  268. package/dist/server-components/response.d.ts.map +1 -0
  269. package/dist/server-components/response.js +32 -0
  270. package/dist/server-components/response.js.map +1 -0
  271. package/dist/server-components/router-integration.d.ts +9 -0
  272. package/dist/server-components/router-integration.d.ts.map +1 -0
  273. package/dist/server-components/router-integration.js +29 -0
  274. package/dist/server-components/router-integration.js.map +1 -0
  275. package/dist/server-components/runtime.d.ts +14 -0
  276. package/dist/server-components/runtime.d.ts.map +1 -0
  277. package/dist/server-components/runtime.js +276 -0
  278. package/dist/server-components/runtime.js.map +1 -0
  279. package/dist/server-components/scheduler.d.ts +14 -0
  280. package/dist/server-components/scheduler.d.ts.map +1 -0
  281. package/dist/server-components/scheduler.js +74 -0
  282. package/dist/server-components/scheduler.js.map +1 -0
  283. package/dist/server-components/schema.d.ts +4 -0
  284. package/dist/server-components/schema.d.ts.map +1 -0
  285. package/dist/server-components/schema.js +77 -0
  286. package/dist/server-components/schema.js.map +1 -0
  287. package/dist/server-components/server-entry.d.ts +12 -0
  288. package/dist/server-components/server-entry.d.ts.map +1 -0
  289. package/dist/server-components/server-entry.js +90 -0
  290. package/dist/server-components/server-entry.js.map +1 -0
  291. package/dist/server-components/source.d.ts +15 -0
  292. package/dist/server-components/source.d.ts.map +1 -0
  293. package/dist/server-components/source.js +269 -0
  294. package/dist/server-components/source.js.map +1 -0
  295. package/dist/server-components/state.d.ts +4 -0
  296. package/dist/server-components/state.d.ts.map +1 -0
  297. package/dist/server-components/state.js +123 -0
  298. package/dist/server-components/state.js.map +1 -0
  299. package/dist/server-components/stream.d.ts +6 -0
  300. package/dist/server-components/stream.d.ts.map +1 -0
  301. package/dist/server-components/stream.js +70 -0
  302. package/dist/server-components/stream.js.map +1 -0
  303. package/dist/server-components/trusted-html.d.ts +3 -0
  304. package/dist/server-components/trusted-html.d.ts.map +1 -0
  305. package/dist/server-components/trusted-html.js +14 -0
  306. package/dist/server-components/trusted-html.js.map +1 -0
  307. package/dist/server-components/types.d.ts +107 -0
  308. package/dist/server-components/types.d.ts.map +1 -0
  309. package/dist/server-components/types.js +2 -0
  310. package/dist/server-components/types.js.map +1 -0
  311. package/dist/server-components/typescript.d.ts +11 -0
  312. package/dist/server-components/typescript.d.ts.map +1 -0
  313. package/dist/server-components/typescript.js +28 -0
  314. package/dist/server-components/typescript.js.map +1 -0
  315. package/dist/server-components/value.d.ts +5 -0
  316. package/dist/server-components/value.d.ts.map +1 -0
  317. package/dist/server-components/value.js +77 -0
  318. package/dist/server-components/value.js.map +1 -0
  319. package/dist/server-components/vite.d.ts +12 -0
  320. package/dist/server-components/vite.d.ts.map +1 -0
  321. package/dist/server-components/vite.js +197 -0
  322. package/dist/server-components/vite.js.map +1 -0
  323. package/dist/server-components.d.ts +5 -0
  324. package/dist/server-components.d.ts.map +1 -0
  325. package/dist/server-components.js +4 -0
  326. package/dist/server-components.js.map +1 -0
  327. package/dist/service-worker.d.ts +8 -1
  328. package/dist/service-worker.d.ts.map +1 -1
  329. package/dist/service-worker.js +20 -6
  330. package/dist/service-worker.js.map +1 -1
  331. package/dist/vite.d.ts +12 -0
  332. package/dist/vite.d.ts.map +1 -1
  333. package/dist/vite.js +7 -1
  334. package/dist/vite.js.map +1 -1
  335. package/dist/zones.d.ts +64 -2
  336. package/dist/zones.d.ts.map +1 -1
  337. package/dist/zones.js +331 -28
  338. package/dist/zones.js.map +1 -1
  339. package/package.json +102 -4
  340. package/src/build-vite.ts +31 -9
  341. package/src/build.ts +109 -31
  342. package/src/cdn.ts +18 -12
  343. package/src/client.ts +48 -17
  344. package/src/deferred.ts +27 -9
  345. package/src/dev-feed.ts +24 -5
  346. package/src/dev-port.ts +4 -10
  347. package/src/dev-session.ts +4 -13
  348. package/src/dev.ts +29 -1
  349. package/src/draft.ts +35 -14
  350. package/src/fonts-google.ts +10 -0
  351. package/src/index.ts +5 -6
  352. package/src/internal/codegen.ts +60 -29
  353. package/src/internal/disposable-dir.ts +24 -0
  354. package/src/internal/entry-chunks.ts +18 -0
  355. package/src/internal/evidence-traversal.ts +57 -0
  356. package/src/internal/head-merge.ts +16 -0
  357. package/src/internal/js-string.ts +6 -1
  358. package/src/internal/mount-shadow.ts +1 -1
  359. package/src/internal/output-guard.ts +88 -7
  360. package/src/internal/package-entries.ts +331 -0
  361. package/src/internal/page-execution.ts +573 -114
  362. package/src/internal/parity.ts +15 -2
  363. package/src/internal/private-env.ts +27 -13
  364. package/src/internal/progressive-cookies.ts +60 -0
  365. package/src/internal/public-env.ts +14 -0
  366. package/src/internal/render-document.ts +66 -89
  367. package/src/internal/response-controls.ts +31 -14
  368. package/src/internal/revive-dates.ts +35 -0
  369. package/src/internal/route-path.ts +24 -0
  370. package/src/internal/secret-scan.ts +190 -18
  371. package/src/internal/server-boundary.ts +29 -5
  372. package/src/internal/web-app.ts +126 -42
  373. package/src/internal/wire-date.ts +16 -0
  374. package/src/internal/without-query.ts +8 -0
  375. package/src/internal/zone-graph.ts +78 -25
  376. package/src/isr.ts +364 -45
  377. package/src/llms-txt.ts +32 -3
  378. package/src/manifest.ts +38 -4
  379. package/src/open-resolved.ts +64 -0
  380. package/src/plugins/kit.ts +31 -11
  381. package/src/plugins/vite-leak-guard.ts +104 -8
  382. package/src/plugins/vite-zone-guard.ts +21 -19
  383. package/src/plugins/zone-guard.ts +41 -2
  384. package/src/prerender.ts +39 -18
  385. package/src/public-dir.ts +27 -8
  386. package/src/query.ts +107 -26
  387. package/src/render-seam.ts +30 -2
  388. package/src/route-types.ts +34 -7
  389. package/src/router.ts +66 -59
  390. package/src/seo.ts +68 -0
  391. package/src/server-components/build-info.ts +11 -0
  392. package/src/server-components/build-target.ts +194 -0
  393. package/src/server-components/build.ts +419 -0
  394. package/src/server-components/bundle-proof.ts +33 -0
  395. package/src/server-components/client.ts +663 -0
  396. package/src/server-components/definition.ts +125 -0
  397. package/src/server-components/dev-http.ts +75 -0
  398. package/src/server-components/error.ts +24 -0
  399. package/src/server-components/frames.ts +181 -0
  400. package/src/server-components/html-policy.ts +94 -0
  401. package/src/server-components/html.ts +81 -0
  402. package/src/server-components/islands.ts +111 -0
  403. package/src/server-components/native.ts +101 -0
  404. package/src/server-components/portal.ts +42 -0
  405. package/src/server-components/props.ts +19 -0
  406. package/src/server-components/protocol.ts +167 -0
  407. package/src/server-components/range.ts +313 -0
  408. package/src/server-components/registry.ts +34 -0
  409. package/src/server-components/response.ts +44 -0
  410. package/src/server-components/router-integration.ts +40 -0
  411. package/src/server-components/runtime.ts +376 -0
  412. package/src/server-components/scheduler.ts +84 -0
  413. package/src/server-components/schema.ts +69 -0
  414. package/src/server-components/server-entry.ts +98 -0
  415. package/src/server-components/source.ts +324 -0
  416. package/src/server-components/state.ts +143 -0
  417. package/src/server-components/stream.ts +71 -0
  418. package/src/server-components/trusted-html.ts +21 -0
  419. package/src/server-components/types.ts +138 -0
  420. package/src/server-components/typescript.ts +39 -0
  421. package/src/server-components/value.ts +82 -0
  422. package/src/server-components/vite.ts +242 -0
  423. package/src/server-components.ts +32 -0
  424. package/src/service-worker.ts +24 -7
  425. package/src/vite.ts +8 -1
  426. package/src/zones.ts +418 -32
package/src/client.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  * a full page load). DOM-only - never imported on the server, so the store stays SSR-safe.
6
6
  */
7
7
  import { trustedHeadAttributes } from "./internal/head-attributes.ts"
8
+ import { appUrl } from "./internal/route-path.ts"
8
9
  import { EXECUTABLE_SCRIPT_TYPES, INERT_SCRIPT_TYPES } from "./internal/script-types.ts"
9
10
  import type { Meta } from "./manifest.ts"
10
11
  import {
@@ -40,7 +41,7 @@ export function currentDocumentNonce(): string | undefined {
40
41
  * serves modules as written, so the browser evaluated `public-dir.ts` and died on `node:fs/promises`
41
42
  * before hydrating. The fix is for the entry not to name the server module in the first place.
42
43
  */
43
- export { mergeHeads, resolveMeta } from "./internal/head-merge.ts"
44
+ export { mergeHeads, resolveChainMeta, resolveMeta } from "./internal/head-merge.ts"
44
45
  export {
45
46
  getBrowserNavigate,
46
47
  IDLE_BLOCKER,
@@ -232,7 +233,7 @@ export function installHistory(
232
233
  router: ClientRouter,
233
234
  options: InstallHistoryOptions = {},
234
235
  ): () => void {
235
- const fallback = options.fallback ?? ((path: string) => location.assign(path))
236
+ const fallback = options.fallback ?? ((path: string) => location.assign(appUrl(path)))
236
237
 
237
238
  // We manage scroll ourselves (save per history entry, restore after the navigated content
238
239
  // renders), so disable the browser's native restoration which would otherwise fight us.
@@ -364,7 +365,7 @@ export function installHistory(
364
365
  if (mode === "replace") {
365
366
  // Replace the current entry (same index) with the new URL. The entry we're leaving is discarded, so
366
367
  // its scroll isn't worth saving; the new route scrolls to its fragment target, else the top.
367
- history.replaceState(entryState, "", path)
368
+ history.replaceState(entryState, "", appUrl(path))
368
369
  pendingScroll = url.hash !== "" ? { hash: hashId(url.hash) } : { pos: [0, 0] }
369
370
  } else {
370
371
  // Save the leaving entry's scroll (spread keeps its nifraIndex), push a fresh higher-indexed entry
@@ -372,7 +373,7 @@ export function installHistory(
372
373
  history.replaceState({ ...(history.state ?? {}), nifraScroll: [scrollX, scrollY] }, "")
373
374
  index += 1
374
375
  entryState.nifraIndex = index
375
- history.pushState(entryState, "", path)
376
+ history.pushState(entryState, "", appUrl(path))
376
377
  pendingScroll = url.hash !== "" ? { hash: hashId(url.hash) } : { pos: [0, 0] }
377
378
  }
378
379
  here = url.pathname + url.search + url.hash
@@ -555,9 +556,9 @@ export function installHistory(
555
556
  if (expected === undefined) {
556
557
  history.replaceState({ ...(history.state ?? {}), nifraScroll: [scrollX, scrollY] }, "")
557
558
  index += 1
558
- history.pushState({ nifraIndex: index }, "", path)
559
+ history.pushState({ nifraIndex: index }, "", appUrl(path))
559
560
  } else {
560
- history.replaceState(history.state, "", path + location.hash)
561
+ history.replaceState(history.state, "", appUrl(path) + location.hash)
561
562
  }
562
563
  here = location.pathname + location.search + location.hash
563
564
  pendingScroll = location.hash !== "" ? { hash: hashId(location.hash) } : { pos: [0, 0] }
@@ -610,11 +611,6 @@ export function installHistory(
610
611
  }
611
612
  }
612
613
 
613
- /**
614
- * Intercept submissions of same-origin `<form method="post">` whose action targets an app route:
615
- * submit via the router (no full reload - POST the action, then revalidate the active loader). On
616
- * failure it falls back to a native submit, so the form still works. Returns a teardown function.
617
- */
618
614
  /**
619
615
  * Sync the document head to a route's resolved {@link Meta} on client navigation. Sets the title
620
616
  * (when provided) and replaces the **managed** (`data-nifra`) `<meta>`/`<link>` tags - static head
@@ -689,24 +685,59 @@ export function applyHead(head: Meta): void {
689
685
  }
690
686
  }
691
687
 
688
+ // A control named like a form property shadows it (`<input name="target">` makes `form.target` that
689
+ // input), so the form's own state is read through the prototype accessors.
690
+ function formProperty(form: HTMLFormElement, name: "action" | "method" | "target"): string {
691
+ return String(Reflect.get(HTMLFormElement.prototype, name, form))
692
+ }
693
+
694
+ /**
695
+ * Intercept submissions of same-origin `<form method="post">` whose action targets an app route:
696
+ * submit via the router (no full reload - POST the action, then revalidate the active loader). On
697
+ * failure it falls back to a native submit, so the form still works. Returns a teardown function.
698
+ */
692
699
  export function installForms(router: ClientRouter): () => void {
700
+ // Forms whose data submit failed before the server answered: their next submit event is the native
701
+ // post that replaces it.
702
+ const native = new WeakSet<HTMLFormElement>()
693
703
  const onSubmit = (event: SubmitEvent): void => {
694
704
  if (event.defaultPrevented) return
695
705
  const form = event.target
696
706
  if (!(form instanceof HTMLFormElement)) return
697
- if (form.method.toLowerCase() !== "post") return // GET forms submit natively
698
- const url = sameDocumentUrl(form.action) // resolved (or the current document URL if unset)
707
+ if (native.delete(form)) return
708
+ // The button that submitted can override the form's method, action and target, and its
709
+ // name=value is part of the submission (`<button name="intent" value="delete">`).
710
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: a submit event's submitter is the submit button or input that fired it, or null
711
+ const submitter = event.submitter as HTMLButtonElement | null
712
+ const method = submitter?.hasAttribute("formmethod")
713
+ ? submitter.formMethod
714
+ : formProperty(form, "method")
715
+ if (method.toLowerCase() !== "post") return // GET forms submit natively
716
+ const target = submitter?.hasAttribute("formtarget")
717
+ ? submitter.formTarget
718
+ : formProperty(form, "target")
719
+ if (target !== "" && target !== "_self") return // it loads in another browsing context
720
+ // Resolved (or the current document URL if unset).
721
+ const url = sameDocumentUrl(
722
+ submitter?.hasAttribute("formaction") ? submitter.formAction : formProperty(form, "action"),
723
+ )
699
724
  if (url === null) return
700
725
  if (router.match(url.pathname) === null) return // not an app route → native submit
701
726
  event.preventDefault()
702
727
  // `data-nifra-revalidate="false"` opts out of the post-action loader revalidation (the action's
703
728
  // actionData drives the update); absent or any other value keeps the default revalidation.
704
- const revalidate = form.dataset.nifraRevalidate !== "false"
705
- router.submit(url.pathname + url.search, new FormData(form), { revalidate }).catch((error) => {
729
+ const revalidate =
730
+ Element.prototype.getAttribute.call(form, "data-nifra-revalidate") !== "false"
731
+ const body = new FormData(form, submitter)
732
+ router.submit(url.pathname + url.search, body, { revalidate }).catch((error) => {
706
733
  const to = redirectOf(error)
707
- // The action ran: load the page that shows its result rather than run it again.
734
+ // The server answered: load the page that shows the result rather than run the action again.
708
735
  if (to !== undefined) location.assign(to)
709
- else form.submit() // data submit failed - fall back to a full-page POST
736
+ else {
737
+ // Nothing reached the server - post natively, through the same button.
738
+ native.add(form)
739
+ HTMLFormElement.prototype.requestSubmit.call(form, submitter)
740
+ }
710
741
  })
711
742
  }
712
743
  document.addEventListener("submit", onSubmit)
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 })
package/src/dev-port.ts CHANGED
@@ -38,13 +38,14 @@ export const explainBindFailure = (err: unknown, port: number): Error =>
38
38
 
39
39
  /** The bits of a Node server {@link listenOrExplain} touches - structural, so a test can fake it. */
40
40
  export interface ListenTarget {
41
- listen(port: number, cb: () => void): void
41
+ listen(port: number, hostname: string, cb: () => void): void
42
42
  once(event: "error", cb: (err: unknown) => void): void
43
43
  removeListener(event: "error", cb: (err: unknown) => void): void
44
44
  }
45
45
 
46
46
  /**
47
47
  * `listen`, but a bind failure becomes a readable nifra error instead of Node's raw internal throw.
48
+ * The hostname is required: omitting it binds every interface, and a dev server is local-only.
48
49
  *
49
50
  * Without an `error` listener attached, Node throws from deep inside `node:events`, the new process dies
50
51
  * in the background, and nothing connects that death to the stale page in front of you.
@@ -52,7 +53,7 @@ export interface ListenTarget {
52
53
  export function listenOrExplain(
53
54
  server: ListenTarget,
54
55
  port: number,
55
- hostname?: string,
56
+ hostname: string,
56
57
  ): Promise<void> {
57
58
  return new Promise<void>((resolve, reject) => {
58
59
  const onError = (err: unknown): void => reject(explainBindFailure(err, port))
@@ -63,13 +64,6 @@ export function listenOrExplain(
63
64
  server.removeListener("error", onError)
64
65
  resolve()
65
66
  }
66
- if (hostname === undefined) server.listen(port, onListening)
67
- else {
68
- ;(
69
- server as ListenTarget & {
70
- listen(port: number, hostname: string, cb: () => void): void
71
- }
72
- ).listen(port, hostname, onListening)
73
- }
67
+ server.listen(port, hostname, onListening)
74
68
  })
75
69
  }
@@ -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,15 +85,11 @@ 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
- canonical,
92
91
  gone,
93
- jsonLd,
94
92
  notFound,
95
- type OpenGraphInput,
96
- openGraph,
97
93
  type RedirectOptions,
98
94
  type RenderAssemblyCache,
99
95
  type RenderedPage,
@@ -141,6 +137,7 @@ export {
141
137
  type ISROptions,
142
138
  type ISRPlatform,
143
139
  type ISRQuery,
140
+ type ISRStoreOp,
144
141
  isCacheablePage,
145
142
  KVCacheStore,
146
143
  type KVCacheStoreOptions,
@@ -256,6 +253,7 @@ export {
256
253
  ACTION_GLOBAL,
257
254
  BOUNDARY_GLOBAL,
258
255
  DATA_GLOBAL,
256
+ type DocumentParts,
259
257
  HANDOVER_ID,
260
258
  LAYOUT_DATA_GLOBAL,
261
259
  type MatchChain,
@@ -297,6 +295,7 @@ export {
297
295
  // ctx + `renderPage`) and a client adapter mount call this with the same URL + schema, so the two sides
298
296
  // produce the identical value by construction. An adapter's `useSearch` binding reads its result.
299
297
  export { type SearchOf, searchOf, searchOfChain, serializeSearch } from "./search.ts"
298
+ export { canonical, jsonLd, type OpenGraphInput, openGraph } from "./seo.ts"
300
299
  export {
301
300
  type HtmlSanitizer,
302
301
  type SanitizedHtml,