@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
@@ -148,11 +148,12 @@ const JWT = /\beyJ[A-Za-z0-9_-]{8,}\.(eyJ[A-Za-z0-9_-]{8,})\.[A-Za-z0-9_-]{16,}/
148
148
  const PLACEHOLDER =
149
149
  /^(?:user(?:name)?|pass(?:word)?|pwd|secret|admin|root|guest|test|demo|example|changeme|change-me|xxx+|\*+|\.\.\.+|my-?password|your-?password|<[^>]*>|\$\{[^}]*\}|\{\{[^}]*\}\}|%[A-Za-z_]+%?)$/i
150
150
 
151
- /** A literal assigned to a name: `apiKey: "..."`, `const secret = '...'`. Template literals with an
152
- * interpolation are not literals. The lookbehind starts a name only where an identifier starts: a
153
- * match from inside a long word run backtracks the rest of that run, quadratic in its length. */
151
+ /** A literal assigned to a name: `apiKey: "..."`, `const secret = '...'`, `"x-api-key": "..."`.
152
+ * Template literals with an interpolation are not literals. The lookbehind starts a name only where
153
+ * an identifier starts: a match from inside a long word run backtracks the rest of that run,
154
+ * quadratic in its length. A hyphenated name starts only right after a quote, for the same reason. */
154
155
  const ASSIGNED =
155
- /(?<![\w$])([A-Za-z_$][\w$]*)(["']?)\s*([:=]|\?\?=|\|\|=)\s*(?:"([^"\\\r\n]{16,256})"|'([^'\\\r\n]{16,256})'|`([^`\\$\r\n]{16,256})`)/g
156
+ /(?<![\w$])((?<=["'])[A-Za-z_$][\w$-]*(?=["'])|[A-Za-z_$][\w$]*)(["']?)\s*([:=]|\?\?=|\|\|=)\s*(?:"([^"\\\r\n]{16,256})"|'([^'\\\r\n]{16,256})'|`([^`\\$\r\n]{16,256})`)/g
156
157
 
157
158
  /** Whether a match's name is what the literal is assigned to. A quoted name must be the whole string,
158
159
  * not the tail of one (`? "new-password" : "current-password"`), and a key is never preceded by a
@@ -168,9 +169,33 @@ function namesTheTarget(text: string, start: number, quote: string, operator: st
168
169
  return text[before] !== "?" && text[before] !== "."
169
170
  }
170
171
 
172
+ /** A quoted name and a literal written as a pair: `headers.set("x-api-key", "...")`,
173
+ * `new Headers([["x-api-key", "..."]])`. The last group is what follows the literal. */
174
+ const PAIRED =
175
+ /(["'])([A-Za-z_$][\w$-]*)\1\s*,\s*(?:"([^"\\\r\n]{16,256})"|'([^'\\\r\n]{16,256})'|`([^`\\$\r\n]{16,256})`)\s*([\]),]?)/g
176
+
177
+ /** A value spelled as words or as a constant's name: another field (`["password",
178
+ * "passwordConfirmation"]`), an environment variable, a stand-in token (`"Bearer not-a-token"`). */
179
+ const NAME_LIKE =
180
+ /^(?:[A-Za-z][a-z]*(?:[A-Z][a-z]+|[_.-][A-Za-z][a-z]*)+|[A-Z][A-Z0-9]*(?:_[A-Z][A-Z0-9]*)+)$/
181
+
182
+ /** Whether a pair is a whole two-element array, or the first two arguments of `.set(` or `.append(`:
183
+ * an element in a longer list is not a pair. */
184
+ function opensAPair(text: string, quoteAt: number, after: string): boolean {
185
+ let before = quoteAt - 1
186
+ while (before >= 0 && /\s/.test(text[before] ?? "")) before--
187
+ if (text[before] === "[") return after === "]"
188
+ if (text[before] !== "(" || (after !== ")" && after !== ",")) return false
189
+ return /\.(?:set|append)$/.test(text.slice(Math.max(0, before - 7), before))
190
+ }
191
+
171
192
  const EXTRA_SECRET_NAME =
172
193
  /(?:accesskey|signingkey|encryptionkey|masterkey|serviceaccountkey|connectionstring)$/
173
194
 
195
+ /** Header names whose value puts a scheme before the credential: `"Bearer <token>"`. */
196
+ const AUTHORIZATION_NAME = /^(?:proxy)?authorization$/
197
+ const AUTHORIZATION_VALUE = /^(?:(bearer)|basic) +(\S+)$/i
198
+
174
199
  /** Values that look like keys but are public by design. */
175
200
  const PUBLIC_VALUE =
176
201
  /^(?:AIza[0-9A-Za-z_-]{35}|pk_(?:live|test)_[0-9A-Za-z]+|6L[0-9A-Za-z_-]{38}|G-[A-Z0-9]{6,}|UA-\d+-\d+)$/
@@ -200,6 +225,32 @@ function looksRandom(value: string, minLength: number): boolean {
200
225
  return classes >= 2 && entropy(value) >= 3.5
201
226
  }
202
227
 
228
+ /** A password the scanned text spells as a placeholder or a dev default. */
229
+ function placeholderPassword(user: string, password: string): boolean {
230
+ // nifra-expect NF-S002: a password equal to its user name is a placeholder in the text being
231
+ // scanned; nothing is authenticated here.
232
+ return PLACEHOLDER.test(password) || password === user || /[${}<>*]/.test(password)
233
+ }
234
+
235
+ /** The credential an `Authorization` literal carries: a Bearer token, or a Basic `user:password`
236
+ * that is not a placeholder. */
237
+ function authorizationCredential(value: string): string | undefined {
238
+ const found = AUTHORIZATION_VALUE.exec(value)
239
+ if (found === null) return undefined
240
+ const credential = found[2] ?? ""
241
+ if (found[1] !== undefined) return NAME_LIKE.test(credential) ? undefined : credential
242
+ let decoded: string
243
+ try {
244
+ decoded = atob(credential)
245
+ } catch {
246
+ return undefined
247
+ }
248
+ const colon = decoded.indexOf(":")
249
+ if (colon === -1 || placeholderPassword(decoded.slice(0, colon), decoded.slice(colon + 1)))
250
+ return undefined
251
+ return credential
252
+ }
253
+
203
254
  function jwtRole(payload: string): unknown {
204
255
  try {
205
256
  const json = atob(payload.replaceAll("-", "+").replaceAll("_", "/"))
@@ -231,11 +282,7 @@ function formatMatches(text: string): Match[] {
231
282
  matches.push({ rule: "private-key", index: found.index, text: found[0], what: "private key" })
232
283
  }
233
284
  for (const found of text.matchAll(CREDENTIAL_URL)) {
234
- const user = found[1] ?? ""
235
- const password = found[2] ?? ""
236
- // @nifra-gate-reviewed: a password equal to its user name is a placeholder in the text being
237
- // scanned; nothing is authenticated here.
238
- if (PLACEHOLDER.test(password) || password === user || /[${}<>*]/.test(password)) continue
285
+ if (placeholderPassword(found[1] ?? "", found[2] ?? "")) continue
239
286
  matches.push({
240
287
  rule: "credential-url",
241
288
  index: found.index,
@@ -246,21 +293,146 @@ function formatMatches(text: string): Match[] {
246
293
  return matches
247
294
  }
248
295
 
296
+ /** Words that mark a whole `_`/`-` segment of a value as a stand-in: `your_signing_secret`. */
297
+ const PLACEHOLDER_SEGMENT =
298
+ /^(?:your|example|placeholder|sample|dummy|changeme|replace|generated|x{3,})$/
299
+
300
+ const placeholderSegments = (value: string): boolean =>
301
+ value
302
+ .toLowerCase()
303
+ .split(/[_-]/)
304
+ .some((segment) => PLACEHOLDER_SEGMENT.test(segment))
305
+
306
+ /** A word-cased label: letters and marks, hyphen-joined words, an acronym. No digits, no runs of
307
+ * mixed case, no credential punctuation. */
308
+ const LABEL = /^(?:\p{Lu}{1,4}|\p{Lu}?[\p{Ll}\p{M}]+)(?:-(?:\p{Lu}{1,4}|\p{Lu}?[\p{Ll}\p{M}]+))*$/u
309
+
310
+ /** How many string values a catalog holds at least, and the share of them that is prose. */
311
+ const CATALOG_VALUES = 10
312
+
313
+ /** Where a `/` starts a regular expression: after anything but a value's end. */
314
+ const VALUE_END = /[\w$)\]}"'`]/
315
+
316
+ /**
317
+ * The spans of top-level object literals that read as message catalogs: at least
318
+ * {@link CATALOG_VALUES} string property values, most of them prose with whitespace. A lexer, not a
319
+ * parser, so a build scans without TypeScript; strings, comments and regular expressions are skipped.
320
+ */
321
+ function catalogSpans(text: string): Array<readonly [number, number]> {
322
+ const spans: Array<readonly [number, number]> = []
323
+ let depth = 0
324
+ let start = 0
325
+ let values = 0
326
+ let prose = 0
327
+ // Whether the open top-level brace starts an object literal, not a block such as a function body.
328
+ let literal = false
329
+ let last = ""
330
+ let i = 0
331
+ while (i < text.length) {
332
+ const c = text.charAt(i)
333
+ if (c === "/" && text[i + 1] === "/") {
334
+ const end = text.indexOf("\n", i)
335
+ i = end === -1 ? text.length : end
336
+ continue
337
+ }
338
+ if (c === "/" && text[i + 1] === "*") {
339
+ const end = text.indexOf("*/", i + 2)
340
+ i = end === -1 ? text.length : end + 2
341
+ continue
342
+ }
343
+ if (c === '"' || c === "'" || c === "`" || (c === "/" && !VALUE_END.test(last))) {
344
+ let j = i + 1
345
+ let inClass = false
346
+ for (; j < text.length; j++) {
347
+ const d = text[j]
348
+ if (d === "\\") {
349
+ j++
350
+ continue
351
+ }
352
+ if (c !== "`" && d === "\n") break
353
+ if (c !== "/") {
354
+ if (d === c) break
355
+ continue
356
+ }
357
+ if (d === "[") inClass = true
358
+ else if (d === "]") inClass = false
359
+ else if (d === "/" && !inClass) break
360
+ }
361
+ // A string after `:` is a property's value.
362
+ if (depth > 0 && c !== "/" && c !== "`" && last === ":") {
363
+ values++
364
+ if (/\s/.test(text.slice(i + 1, j))) prose++
365
+ }
366
+ last = c
367
+ i = j + 1
368
+ continue
369
+ }
370
+ if (c === "{") {
371
+ if (depth === 0) {
372
+ start = i
373
+ values = 0
374
+ prose = 0
375
+ const word = /([\w$]+)\s*$/.exec(text.slice(Math.max(0, i - 16), i))?.[1]
376
+ literal = last === "" || "=(,:[?".includes(last) || word === "default"
377
+ }
378
+ depth++
379
+ } else if (c === "}" && depth > 0 && --depth === 0) {
380
+ if (literal && values >= CATALOG_VALUES && prose * 2 > values) spans.push([start, i + 1])
381
+ }
382
+ if (!/\s/.test(c)) last = c
383
+ i++
384
+ }
385
+ return spans
386
+ }
387
+
388
+ /** The credential a literal given to `name` carries, when the name is secret-like and the literal
389
+ * looks random. */
390
+ function credentialOf(name: string, value: string): string | undefined {
391
+ const key = name.toLowerCase().replace(/[-_]/g, "")
392
+ const header = AUTHORIZATION_NAME.test(key)
393
+ if (!header && !isSensitiveFieldName(name) && !EXTRA_SECRET_NAME.test(key)) return undefined
394
+ const secret = header ? authorizationCredential(value) : value
395
+ if (
396
+ secret === undefined ||
397
+ PUBLIC_VALUE.test(secret) ||
398
+ PLACEHOLDER.test(secret) ||
399
+ placeholderSegments(secret) ||
400
+ !looksRandom(secret, 16)
401
+ )
402
+ return undefined
403
+ return secret
404
+ }
405
+
249
406
  function assignedMatches(text: string): Match[] {
250
407
  const matches: Match[] = []
251
- for (const found of text.matchAll(ASSIGNED)) {
252
- const name = found[1] ?? ""
253
- if (!isSensitiveFieldName(name) && !EXTRA_SECRET_NAME.test(name.toLowerCase())) continue
254
- if (!namesTheTarget(text, found.index, found[2] ?? "", found[3] ?? "")) continue
255
- const value = found[4] ?? found[5] ?? found[6] ?? ""
256
- if (PUBLIC_VALUE.test(value) || PLACEHOLDER.test(value) || !looksRandom(value, 16)) continue
408
+ let catalogs: ReadonlyArray<readonly [number, number]> | undefined
409
+ /** A label in a message catalog: `password: "Lykilorð"` names the field in another language. */
410
+ const catalogLabel = (value: string, valueAt: number): boolean => {
411
+ if (!LABEL.test(value)) return false
412
+ catalogs ??= catalogSpans(text)
413
+ return catalogs.some(([start, end]) => valueAt > start && valueAt < end)
414
+ }
415
+ const add = (name: string, value: string, valueAt: number): void => {
416
+ const secret = credentialOf(name, value)
417
+ if (secret === undefined || catalogLabel(value, valueAt)) return
257
418
  matches.push({
258
419
  rule: "assigned-secret",
259
- index: found.index + found[0].indexOf(value),
260
- text: value,
420
+ // The credential is the value's tail: a header value's scheme is not part of it.
421
+ index: valueAt + value.length - secret.length,
422
+ text: secret,
261
423
  what: `literal assigned to "${name}"`,
262
424
  })
263
425
  }
426
+ for (const found of text.matchAll(ASSIGNED)) {
427
+ if (!namesTheTarget(text, found.index, found[2] ?? "", found[3] ?? "")) continue
428
+ const value = found[4] ?? found[5] ?? found[6] ?? ""
429
+ add(found[1] ?? "", value, found.index + found[0].lastIndexOf(value))
430
+ }
431
+ for (const found of text.matchAll(PAIRED)) {
432
+ const value = found[3] ?? found[4] ?? found[5] ?? ""
433
+ if (NAME_LIKE.test(value) || !opensAPair(text, found.index, found[6] ?? "")) continue
434
+ add(found[2] ?? "", value, found.index + found[0].lastIndexOf(value))
435
+ }
264
436
  return matches
265
437
  }
266
438
 
@@ -508,7 +680,7 @@ export function createRedactor(
508
680
  for (const { what, pattern } of KEY_FORMATS) out = out.replace(pattern, `[redacted:${what}]`)
509
681
  out = out.replace(JWT, "[redacted:JWT]")
510
682
  out = out.replace(CREDENTIAL_URL, (match, user: string, password: string) =>
511
- // @nifra-gate-reviewed: the same placeholder test as the scan; nothing is authenticated here.
683
+ // nifra-expect NF-S002: the same placeholder test as the scan; nothing is authenticated here.
512
684
  PLACEHOLDER.test(password) || password === user
513
685
  ? match
514
686
  : match.replace(`:${password}@`, ":[redacted]@"),
@@ -14,6 +14,7 @@
14
14
  export const SERVER_FN_MODULE = /\.fn(\.[cm]?[jt]sx?)?$/
15
15
 
16
16
  import { jsStringLiteral } from "./js-string.ts"
17
+ import { DATE_TAG, REVIVE_DATES_SOURCE } from "./revive-dates.ts"
17
18
 
18
19
  // ---------------------------------------------------------------------------------------------------
19
20
  // `.fn` - turn a server-function module into the client stubs that call it.
@@ -50,15 +51,18 @@ const DECLARATION =
50
51
  const ANY_CALL = /\bserverFn\s*[<(]/g
51
52
  /** Strip line and block comments by forward index scan, so prose cannot register as code. Not a
52
53
  * regex: every regex shape for comment stripping either backtracks polynomially on adversarial
53
- * source or is opaque enough that reviewers cannot tell. A `//` preceded by `:` is kept (URL in a
54
- * string - the same heuristic the previous implementation used); an unterminated block comment
55
- * swallows the rest of the source, matching a real parser. */
54
+ * source or is opaque enough that reviewers cannot tell. String literals are stepped over, so a
55
+ * `"image/*"` cannot open a comment; a `//` preceded by `:` is kept (a URL outside a string); an
56
+ * unterminated block comment swallows the rest of the source, matching a real parser. */
56
57
  function stripComments(source: string): string {
57
58
  const parts: string[] = []
58
59
  let keepFrom = 0
59
60
  let i = 0
60
61
  while (i < source.length) {
61
- if (source.startsWith("/*", i)) {
62
+ const char = source[i]
63
+ if (char === '"' || char === "'" || char === "`") {
64
+ i = stringEnd(source, i)
65
+ } else if (source.startsWith("/*", i)) {
62
66
  const close = source.indexOf("*/", i + 2)
63
67
  parts.push(source.slice(keepFrom, i))
64
68
  i = close === -1 ? source.length : close + 2
@@ -76,6 +80,21 @@ function stripComments(source: string): string {
76
80
  return parts.join("")
77
81
  }
78
82
 
83
+ /** The index just past the string literal opening at `start`. A quote or apostrophe string also ends
84
+ * at a line break, so a stray one (JSX text, a regex literal) misreads one line at most. */
85
+ function stringEnd(source: string, start: number): number {
86
+ const quote = source[start]
87
+ let i = start + 1
88
+ while (i < source.length) {
89
+ const char = source[i]
90
+ if (char === "\\") i += 2
91
+ else if (char === quote) return i + 1
92
+ else if (char === "\n" && quote !== "`") return i
93
+ else i++
94
+ }
95
+ return source.length
96
+ }
97
+
79
98
  /**
80
99
  * The namespace a `*.fn.ts` module is mounted under: its filename without the `.fn` suffix.
81
100
  *
@@ -143,9 +162,14 @@ async function __nifraCall(name, input) {
143
162
  }
144
163
  throw new Error(\`[nifra/fn] \${name} failed: \${response.status}\${detail ? \` \${detail}\` : ""}\`)
145
164
  }
146
- return response.status === 204 ? undefined : await response.json()
165
+ if (response.status === 204) return undefined
166
+ const text = await response.text()
167
+ const value = JSON.parse(text)
168
+ return text.includes('"${DATE_TAG}"') ? __nifraDates(value) : value
147
169
  }
148
170
 
171
+ ${REVIVE_DATES_SOURCE}
172
+
149
173
  ${calls}
150
174
  `
151
175
  }
@@ -1,3 +1,5 @@
1
+ import { emitRequestErrorLog } from "@nifrajs/core/edge-kit"
2
+ import { jsonLogger } from "@nifrajs/core/logger"
1
3
  import {
2
4
  type BackendEvidenceProvider,
3
5
  type BackendMount,
@@ -16,12 +18,15 @@ import { CACHE_CHANNEL } from "../isr.ts"
16
18
  import { generateLlmsTxt } from "../llms-txt.ts"
17
19
  import type { Manifest } from "../manifest.ts"
18
20
  import type { RenderAdapter } from "../render-seam.ts"
19
- import { PRERENDERED_LIST_PATH } from "../router.ts"
21
+ import { createMatcher, PRERENDERED_LIST_PATH } from "../router.ts"
22
+ import type { EvidenceAncestry } from "./evidence-traversal.ts"
20
23
  import { formatShadowedPages, shadowedPages } from "./mount-shadow.ts"
21
24
  import { createPageRequestExecutor, type NonceResolver } from "./page-execution.ts"
25
+ import { requestPattern } from "./route-path.ts"
22
26
 
23
27
  export type { NonceResolver } from "./page-execution.ts"
24
28
  export interface CreateWebAppOptions<Env = unknown> {
29
+ readonly serverComponents?: import("../server-components/types.ts").ServerComponentRuntime
25
30
  readonly adapter: RenderAdapter
26
31
  readonly manifest: Manifest
27
32
  /** URL of the built client entry (module script) injected into every page. */
@@ -78,9 +83,10 @@ export interface CreateWebAppOptions<Env = unknown> {
78
83
  *
79
84
  * **Why the timing matters.** `beforeHandle`, `afterHandle`, `around`, `derive`/`decorate`, and
80
85
  * `onError` are snapshotted into each route AS IT IS DECLARED, and `createWebApp` declares every
81
- * page (plus the `/*` catch-all) before it returns - so a caller's `app.use(…)` afterwards binds
82
- * them to nothing, silently. `requestId()` is exactly that case: it is a `derive`, so applied late
83
- * it leaves every page without `c.requestId`. Route assurance evidence is order-scoped the same
86
+ * page (plus the `/*` catch-all) before it returns - so a caller's `app.use(…)` afterwards would
87
+ * bind them to no page, and outside production it throws (`HOOK_AFTER_PAGES`). `requestId()` is
88
+ * exactly that case: it is a `derive`, so applied late it would leave every page without
89
+ * `c.requestId`. A `group()` is its own scope and stays allowed. Route assurance evidence is order-scoped the same
84
90
  * way, so a late `securityHeaders()` leaves the pages unable to PROVE the header to `nifra assure`
85
91
  * even though it does still set it.
86
92
  *
@@ -101,10 +107,11 @@ export interface CreateWebAppOptions<Env = unknown> {
101
107
  * per-route via `@nifrajs/client`'s `createRoutes`). Opaque to the core.
102
108
  *
103
109
  * **Auto-mount.** Every `inProcessClient(backend)` exposes the symbol-keyed platform-aware backend
104
- * mount interface from `@nifrajs/core/mount`; `createWebApp` also serves that backend over HTTP at
105
- * {@link apiPrefix} (default `/api`): a request whose pathname starts with the prefix is dispatched
106
- * before page routing with the same `env`/`waitUntil` platform context, and the backend's `Response`
107
- * is returned untouched. The mount runs in `nifra dev` too. Pass `apiPrefix: ""` to disable it.
110
+ * mount interface from `@nifrajs/core/mount`; given an {@link apiPrefix}, `createWebApp` also serves
111
+ * that backend over HTTP there: a request whose pathname starts with the prefix is dispatched before
112
+ * page routing with the same `env`/`waitUntil` platform context, and the backend's `Response` is
113
+ * returned untouched. The mount runs in `nifra dev` too. Without `apiPrefix` the backend is
114
+ * loader-only.
108
115
  *
109
116
  * **Request-scoped loader calls.** While a page renders, its loaders, actions and boundaries get a
110
117
  * `ctx.api` bound to that request's platform identity: a backend handler reached through it sees the
@@ -114,12 +121,12 @@ export interface CreateWebAppOptions<Env = unknown> {
114
121
  * does not lose it. Headers do NOT travel: the page request's `cookie` and `authorization` are never
115
122
  * copied, so a loader call is anonymous unless the loader passes headers on the call itself. */
116
123
  readonly api?: unknown
117
- /** HTTP path prefix the {@link api} backend is auto-mounted at (default `"/api"`). A request whose
124
+ /** HTTP path prefix the {@link api} backend is mounted at, such as `"/api"`. A request whose
118
125
  * pathname is exactly the prefix or starts with `prefix + "/"` is dispatched to the backend before
119
126
  * page routing; the backend therefore defines its routes at the **full** path (`server().post("/api/
120
- * sync", …)`), matching the in-process `inProcessClient` call sites. Set to `""` to disable the
121
- * auto-mount entirely (the app serves pages only and `api` stays a loader-only `ctx.api`). Mounting
122
- * is also a no-op when `api` does not expose the symbol mount.
127
+ * sync", …)`), matching the in-process `inProcessClient` call sites. Unset or `""`, nothing is
128
+ * mounted: the app serves pages only and `api` stays a loader-only `ctx.api`. Mounting is also a
129
+ * no-op when `api` does not expose the symbol mount.
123
130
  *
124
131
  * A page file under the prefix (`routes/api/report.tsx`) could never render - the backend answers
125
132
  * first and its 404 is final - so `createWebApp` throws, naming the file. The same holds for every
@@ -146,7 +153,8 @@ export interface CreateWebAppOptions<Env = unknown> {
146
153
  * Tried longest-path-first and BEFORE the `api` mount, so a mount at `/api/auth` wins over a backend
147
154
  * at `/api` no matter which was declared first. `stripPrefix` is the per-mount form of {@link apiStrip}:
148
155
  * leave it off to pass the full path through. `opaque` states why a mount whose routes nifra cannot
149
- * read (better-auth's handler) is outside assurance; it is then listed as a known gap.
156
+ * read (better-auth's handler) is outside assurance; it is then listed as a known gap, with any
157
+ * routes it declares reported as declared.
150
158
  */
151
159
  readonly mounts?: ReadonlyArray<{
152
160
  readonly path: string
@@ -154,7 +162,7 @@ export interface CreateWebAppOptions<Env = unknown> {
154
162
  readonly stripPrefix?: boolean
155
163
  readonly priority?: number
156
164
  readonly fallbackOn?: 404
157
- readonly opaque?: string
165
+ readonly opaque?: MountOptions<Env>["opaque"]
158
166
  }>
159
167
  /** Secret for **draft / preview mode** (see `enableDraft`). When set, a request carrying a valid
160
168
  * signed `__nifra_draft` cookie gets `ctx.draft === true` in loaders/actions (else always `false`).
@@ -191,8 +199,10 @@ export interface CreateWebAppOptions<Env = unknown> {
191
199
  * team - unreleased feature names, internal hostnames, and "don't touch X yet" notes live there
192
200
  * routinely. Turn it on only for a repo whose guidelines you would publish as a page. */
193
201
  readonly publishLocalGuidelines?: boolean
194
- /** Serve `/llms.txt` and `/llms-full.txt` (default `true`): the page routes, and the backend routes
195
- * when the backend is mounted over HTTP at {@link apiPrefix}. `false` registers neither path. */
202
+ /** Serve `/llms.txt` and `/llms-full.txt`: the page routes, and the backend routes when the backend
203
+ * is mounted over HTTP at {@link apiPrefix}, with their request and response schemas in the full
204
+ * text. **Off by default**: both are public and unauthenticated, and they list every route, admin
205
+ * and internal ones included. Turn it on for an app whose whole surface you would publish. */
196
206
  readonly llmsTxt?: boolean
197
207
  /** SSG: per dynamic route pattern, its `getStaticPaths` `fallback` (from `enumerateStaticRoutes` or
198
208
  * the build's `prerendered.json`). A route mapped to `"404"` rejects any path NOT in
@@ -204,8 +214,9 @@ export interface CreateWebAppOptions<Env = unknown> {
204
214
  readonly staticBoundaryCache?: import("../boundary.ts").StaticBoundaryCache
205
215
  /** Observe every loader/action failure - for error-reporting plugins (Sentry-style). Called for
206
216
  * real throws (not control-flow `Response`s like `redirect`), **before** the nearest `_error`
207
- * boundary renders / a soft-nav 500 / a rethrow - so it sees errors that the boundary would
208
- * otherwise hide. Observation only; its own throws are swallowed so a faulty reporter can't break
217
+ * boundary renders / a soft-nav 500 / a rethrow. A failure a boundary answers is also written to the
218
+ * server's `logger` (under its `errorLogDetail`) whether or not this is set, exactly as an unhandled
219
+ * error is. Observation only; its own throws are swallowed so a faulty reporter can't break
209
220
  * rendering. (`beforeLoader` is intentionally omitted - the core HTTP hooks already cover
210
221
  * pre-request work.) */
211
222
  readonly onLoaderError?: (
@@ -258,10 +269,14 @@ function backendMountOf<Env>(api: unknown): MountableApp<Env> | undefined {
258
269
  return mount
259
270
  }
260
271
 
261
- function evidenceProviderOf(value: unknown): BackendEvidenceProvider | undefined {
272
+ /** A provider a web app composes: it also takes the chain of web providers above it, which a
273
+ * provider that composes nothing ignores. */
274
+ type ComposedEvidenceProvider = (ancestry?: EvidenceAncestry) => ReturnType<BackendEvidenceProvider>
275
+
276
+ function evidenceProviderOf(value: unknown): ComposedEvidenceProvider | undefined {
262
277
  if ((typeof value !== "object" && typeof value !== "function") || value === null) return undefined
263
278
  const provider = (value as { [NIFRA_BACKEND_EVIDENCE]?: unknown })[NIFRA_BACKEND_EVIDENCE]
264
- return typeof provider === "function" ? (provider as BackendEvidenceProvider) : undefined
279
+ return typeof provider === "function" ? (provider as ComposedEvidenceProvider) : undefined
265
280
  }
266
281
 
267
282
  function mountPathPrefix(path: string): string {
@@ -274,6 +289,15 @@ function mountPathPrefix(path: string): string {
274
289
  return withoutWildcard.slice(0, end)
275
290
  }
276
291
 
292
+ /** The server's own error log, for page failures an `_error` boundary answers before it sees them. */
293
+ function boundaryErrorLog(
294
+ options: ServerOptions | undefined,
295
+ ): (error: unknown, request: Request) => void {
296
+ const logger = options?.logger ?? jsonLogger()
297
+ const detail = options?.errorLogDetail ?? "full"
298
+ return (error, request) => emitRequestErrorLog(logger, detail, error, { req: request })
299
+ }
300
+
277
301
  /**
278
302
  * Build a nifra app from a route manifest: every route SSRs its layout chain via `renderPage`,
279
303
  * and a wildcard catch-all renders `_404` (or a plain 404). Reuses @nifrajs/core's router +
@@ -306,6 +330,38 @@ export function createWebApp<Env = unknown>(
306
330
  // `beforeHandle`, assurance evidence) actually bind to them - see the `use` option. A caller
307
331
  // cannot do this after the fact: by the time this function returns, the routes are declared.
308
332
  options.use?.(app)
333
+ if (options.serverComponents !== undefined) {
334
+ const runtime = options.serverComponents
335
+ const match = createMatcher(
336
+ manifest.routes
337
+ .filter((route) => runtime.owns(route.id))
338
+ .map((route) => ({ routeId: route.id, pattern: route.pattern })),
339
+ )
340
+ const privateRoute = (request: Request): boolean =>
341
+ request.headers.has("x-nifra-component") || match(new URL(request.url).pathname) !== undefined
342
+ Object.defineProperty(app, Symbol.for("nifra.web.private-route"), { value: privateRoute })
343
+ app.onResponse((response, request) => {
344
+ if (!privateRoute(request)) return response
345
+ const headers = new Headers(response.headers)
346
+ headers.set("cache-control", "private, no-store")
347
+ const vary = new Set(
348
+ (headers.get("vary") ?? "")
349
+ .split(",")
350
+ .map((name) => name.trim().toLowerCase())
351
+ .filter(Boolean),
352
+ )
353
+ vary.add("x-nifra-component")
354
+ vary.add("x-nifra-data")
355
+ headers.set("vary", [...vary].join(", "))
356
+ headers.delete("x-nifra-isr-revalidate")
357
+ headers.delete("x-nifra-isr-tags")
358
+ return new Response(response.body, {
359
+ status: response.status,
360
+ statusText: response.statusText,
361
+ headers,
362
+ })
363
+ })
364
+ }
309
365
  // A resolver created by `createNonceResolver({ header })` carries a paired response hook. Register it
310
366
  // after caller middleware so its request-specific CSP value is the last default applied to a page.
311
367
  const nonceResponse = options.nonce?.onResponse
@@ -322,15 +378,15 @@ export function createWebApp<Env = unknown>(
322
378
  cacheChannel.enabled = true
323
379
  },
324
380
  })
325
- // Auto-mount the in-process backend over HTTP at `apiPrefix` (default `/api`), BEFORE page routing.
381
+ // Mount the in-process backend over HTTP at `apiPrefix`, when one is given, BEFORE page routing.
326
382
  // Core's pre-route mount seam runs before the page wildcard `/*`; parent request hooks run first,
327
383
  // child request hooks run inside the mount, and parent response hooks still wrap the result.
328
384
  // The mount handles every HTTP method and preserves backend 404 responses instead of letting the
329
385
  // page catch-all render them. We dispatch the same `Request` object (its body unread), so
330
386
  // streamed/large bodies pass through untouched. The backend defines full prefixed paths
331
- // (`server().post("/api/sync", …)`) unless `apiStrip` is enabled. `apiPrefix: ""` opts out;
332
- // a non-mountable `api` remains loader-only and pages-only apps keep the no-mount fast path.
333
- const apiPrefix = options.apiPrefix ?? "/api"
387
+ // (`server().post("/api/sync", …)`) unless `apiStrip` is enabled. Without a prefix, or with a
388
+ // non-mountable `api`, the backend is loader-only and pages-only apps keep the no-mount fast path.
389
+ const apiPrefix = options.apiPrefix ?? ""
334
390
  const apiStrip = options.apiStrip === true
335
391
  const mountedApi = backendMountOf<Env>(api)
336
392
  // Longest path first, so a more specific mount (`/api/auth`) is tried before a broader one (`/api`)
@@ -361,6 +417,22 @@ export function createWebApp<Env = unknown>(
361
417
  ? undefined
362
418
  : servedPrerenderedList(options.prerenderedPaths)
363
419
  const pageExecutor = createPageRequestExecutor<Env>({
420
+ ...(options.serverComponents === undefined
421
+ ? {}
422
+ : { serverComponents: options.serverComponents }),
423
+ ...(options.serverComponents === undefined
424
+ ? {}
425
+ : {
426
+ rerenderOwnerGET: async (
427
+ request: Request,
428
+ context: import("./page-execution.ts").PageRouteContext<Env>,
429
+ ) =>
430
+ app.fetch(request, {
431
+ env: context.env,
432
+ ...(context.waitUntil === undefined ? {} : { waitUntil: context.waitUntil }),
433
+ ...(context.clientIp === undefined ? {} : { clientIp: context.clientIp }),
434
+ }),
435
+ }),
364
436
  adapter,
365
437
  manifest,
366
438
  clientEntry,
@@ -384,18 +456,32 @@ export function createWebApp<Env = unknown>(
384
456
  ? {}
385
457
  : { nonce: cspNonceResolver(options.csp) as NonceResolver<Env>, csp: options.csp }),
386
458
  ...(options.onLoaderError === undefined ? {} : { onLoaderError: options.onLoaderError }),
459
+ logBoundaryError: boundaryErrorLog(options.server),
387
460
  cacheChannel,
388
461
  })
389
462
 
463
+ let ownsRootCatchAll = false
390
464
  for (const route of manifest.routes) {
391
- app.register("GET", route.pattern, undefined, pageExecutor.get(route))
392
- app.register("POST", route.pattern, undefined, pageExecutor.post(route))
465
+ const pattern = requestPattern(route.pattern)
466
+ if (pattern.startsWith("/*")) ownsRootCatchAll = true
467
+ app.register(
468
+ "GET",
469
+ pattern,
470
+ options.serverComponents?.owns(route.id)
471
+ ? { capabilities: options.serverComponents.capabilities(route.id) }
472
+ : undefined,
473
+ pageExecutor.get(route),
474
+ )
475
+ app.register("POST", pattern, undefined, pageExecutor.post(route))
393
476
  }
394
477
  // llms.txt & llms-full.txt describe what a visitor can reach: the pages, and the backend only when it
395
478
  // is served over HTTP - a backend the loaders call in-process has no public surface to describe. An
396
479
  // app route at either path takes precedence, and each text is built once per app.
397
- if (options.llmsTxt !== false) {
398
- const llmsOptions = { includeLocalGuidelines: options.publishLocalGuidelines === true }
480
+ if (options.llmsTxt === true) {
481
+ const llmsOptions = {
482
+ includeLocalGuidelines: options.publishLocalGuidelines === true,
483
+ apiPathPrefix: apiStrip ? mountPathPrefix(apiPrefix) : "",
484
+ }
399
485
  const publicApi = mountedApi !== undefined && apiPrefix !== "" ? api : undefined
400
486
  for (const [path, full] of [
401
487
  ["/llms.txt", false],
@@ -436,16 +522,13 @@ export function createWebApp<Env = unknown>(
436
522
  }
437
523
 
438
524
  // Wildcard catch-all: unmatched paths render the nearest `_404` (404), or a plain text 404 if
439
- // absent.
440
- app.register("GET", "/*", undefined, pageExecutor.fallback)
525
+ // absent. A root catch-all page (`[...slug].tsx`) already answers every path, the root included,
526
+ // and the router refuses a second `/*` under another param name.
527
+ if (!ownsRootCatchAll) app.register("GET", "/*", undefined, pageExecutor.fallback)
441
528
 
442
- let evidenceComposing = false
443
- const evidenceProvider: BackendEvidenceProvider = async () => {
444
- if (evidenceComposing) {
445
- throw new Error("[nifra/web] cyclic composed assurance evidence mount")
446
- }
447
- evidenceComposing = true
448
- try {
529
+ const evidenceProvider: ComposedEvidenceProvider = async (passed) => {
530
+ const { traverseEvidence } = await import("./evidence-traversal.ts")
531
+ return traverseEvidence(evidenceProvider, passed, async (ancestry) => {
449
532
  const { composeProjectEvidence, snapshotProjectEvidence } = await import(
450
533
  "@nifrajs/core/evidence"
451
534
  )
@@ -464,7 +547,7 @@ export function createWebApp<Env = unknown>(
464
547
  )
465
548
  }
466
549
  parts.push({
467
- evidence: await provider.call(mount.app),
550
+ evidence: await provider.call(mount.app, ancestry),
468
551
  ...(mount.stripPrefix === true ? { pathPrefix: mountPathPrefix(mount.path) } : {}),
469
552
  })
470
553
  }
@@ -476,14 +559,13 @@ export function createWebApp<Env = unknown>(
476
559
  )
477
560
  }
478
561
  parts.push({
479
- evidence: await provider.call(api),
562
+ evidence: await provider.call(api, ancestry),
563
+ // biome-ignore lint/plugin/noConditionalEmptyObjectSpread: Evidence paths acquire a prefix only when the mounted backend strips it.
480
564
  ...(apiStrip ? { pathPrefix: mountPathPrefix(apiPrefix) } : {}),
481
565
  })
482
566
  }
483
567
  return composeProjectEvidence(parts)
484
- } finally {
485
- evidenceComposing = false
486
- }
568
+ })
487
569
  }
488
570
  Object.defineProperty(app, NIFRA_BACKEND_EVIDENCE, {
489
571
  configurable: false,
@@ -491,6 +573,8 @@ export function createWebApp<Env = unknown>(
491
573
  value: evidenceProvider,
492
574
  writable: false,
493
575
  })
576
+ // Core's dev-time hook audit reads this: an order-scoped hook added from here on skips every page.
577
+ Object.defineProperty(app, Symbol.for("nifra.web.pagesDeclared"), { value: true })
494
578
 
495
579
  return app
496
580
  }