@nifrajs/web 4.0.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (388) hide show
  1. package/README.md +11 -0
  2. package/dist/build-vite.d.ts +19 -0
  3. package/dist/build-vite.d.ts.map +1 -1
  4. package/dist/build-vite.js +31 -10
  5. package/dist/build-vite.js.map +1 -1
  6. package/dist/build.d.ts +8 -10
  7. package/dist/build.d.ts.map +1 -1
  8. package/dist/build.js +90 -31
  9. package/dist/build.js.map +1 -1
  10. package/dist/cdn.d.ts +8 -0
  11. package/dist/cdn.d.ts.map +1 -1
  12. package/dist/cdn.js +14 -15
  13. package/dist/cdn.js.map +1 -1
  14. package/dist/client.d.ts +1 -1
  15. package/dist/client.d.ts.map +1 -1
  16. package/dist/client.js +49 -26
  17. package/dist/client.js.map +1 -1
  18. package/dist/deferred.d.ts +2 -2
  19. package/dist/deferred.d.ts.map +1 -1
  20. package/dist/deferred.js +28 -7
  21. package/dist/deferred.js.map +1 -1
  22. package/dist/dev-feed.d.ts.map +1 -1
  23. package/dist/dev-feed.js +24 -5
  24. package/dist/dev-feed.js.map +1 -1
  25. package/dist/dev-session.d.ts.map +1 -1
  26. package/dist/dev-session.js +4 -14
  27. package/dist/dev-session.js.map +1 -1
  28. package/dist/dev.d.ts +7 -17
  29. package/dist/dev.d.ts.map +1 -1
  30. package/dist/dev.js +13 -1
  31. package/dist/dev.js.map +1 -1
  32. package/dist/draft.d.ts +8 -5
  33. package/dist/draft.d.ts.map +1 -1
  34. package/dist/draft.js +30 -13
  35. package/dist/draft.js.map +1 -1
  36. package/dist/fonts-google.d.ts.map +1 -1
  37. package/dist/fonts-google.js +7 -0
  38. package/dist/fonts-google.js.map +1 -1
  39. package/dist/index.d.ts +2 -2
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +2 -2
  42. package/dist/index.js.map +1 -1
  43. package/dist/internal/codegen.d.ts +1 -0
  44. package/dist/internal/codegen.d.ts.map +1 -1
  45. package/dist/internal/codegen.js +56 -29
  46. package/dist/internal/codegen.js.map +1 -1
  47. package/dist/internal/disposable-dir.d.ts +8 -0
  48. package/dist/internal/disposable-dir.d.ts.map +1 -0
  49. package/dist/internal/disposable-dir.js +25 -0
  50. package/dist/internal/disposable-dir.js.map +1 -0
  51. package/dist/internal/entry-chunks.d.ts +9 -0
  52. package/dist/internal/entry-chunks.d.ts.map +1 -0
  53. package/dist/internal/entry-chunks.js +15 -0
  54. package/dist/internal/entry-chunks.js.map +1 -0
  55. package/dist/internal/evidence-traversal.d.ts +12 -0
  56. package/dist/internal/evidence-traversal.d.ts.map +1 -0
  57. package/dist/internal/evidence-traversal.js +48 -0
  58. package/dist/internal/evidence-traversal.js.map +1 -0
  59. package/dist/internal/head-merge.d.ts +6 -0
  60. package/dist/internal/head-merge.d.ts.map +1 -1
  61. package/dist/internal/head-merge.js +9 -0
  62. package/dist/internal/head-merge.js.map +1 -1
  63. package/dist/internal/js-string.d.ts +2 -0
  64. package/dist/internal/js-string.d.ts.map +1 -1
  65. package/dist/internal/js-string.js +5 -1
  66. package/dist/internal/js-string.js.map +1 -1
  67. package/dist/internal/mount-shadow.js +1 -1
  68. package/dist/internal/mount-shadow.js.map +1 -1
  69. package/dist/internal/output-guard.d.ts +7 -0
  70. package/dist/internal/output-guard.d.ts.map +1 -1
  71. package/dist/internal/output-guard.js +80 -8
  72. package/dist/internal/output-guard.js.map +1 -1
  73. package/dist/internal/page-execution.d.ts +6 -0
  74. package/dist/internal/page-execution.d.ts.map +1 -1
  75. package/dist/internal/page-execution.js +377 -108
  76. package/dist/internal/page-execution.js.map +1 -1
  77. package/dist/internal/progressive-cookies.d.ts +14 -0
  78. package/dist/internal/progressive-cookies.d.ts.map +1 -0
  79. package/dist/internal/progressive-cookies.js +43 -0
  80. package/dist/internal/progressive-cookies.js.map +1 -0
  81. package/dist/internal/public-env.d.ts +2 -0
  82. package/dist/internal/public-env.d.ts.map +1 -0
  83. package/dist/internal/public-env.js +13 -0
  84. package/dist/internal/public-env.js.map +1 -0
  85. package/dist/internal/render-document.d.ts +1 -0
  86. package/dist/internal/render-document.d.ts.map +1 -1
  87. package/dist/internal/render-document.js +28 -5
  88. package/dist/internal/render-document.js.map +1 -1
  89. package/dist/internal/response-controls.d.ts +2 -0
  90. package/dist/internal/response-controls.d.ts.map +1 -1
  91. package/dist/internal/response-controls.js +26 -16
  92. package/dist/internal/response-controls.js.map +1 -1
  93. package/dist/internal/revive-dates.d.ts +16 -0
  94. package/dist/internal/revive-dates.d.ts.map +1 -0
  95. package/dist/internal/revive-dates.js +32 -0
  96. package/dist/internal/revive-dates.js.map +1 -0
  97. package/dist/internal/route-path.d.ts +8 -0
  98. package/dist/internal/route-path.d.ts.map +1 -0
  99. package/dist/internal/route-path.js +8 -0
  100. package/dist/internal/route-path.js.map +1 -0
  101. package/dist/internal/secret-scan.d.ts.map +1 -1
  102. package/dist/internal/secret-scan.js +105 -19
  103. package/dist/internal/secret-scan.js.map +1 -1
  104. package/dist/internal/server-boundary.d.ts.map +1 -1
  105. package/dist/internal/server-boundary.js +33 -5
  106. package/dist/internal/server-boundary.js.map +1 -1
  107. package/dist/internal/web-app.d.ts +21 -16
  108. package/dist/internal/web-app.d.ts.map +1 -1
  109. package/dist/internal/web-app.js +73 -21
  110. package/dist/internal/web-app.js.map +1 -1
  111. package/dist/internal/wire-date.d.ts +3 -0
  112. package/dist/internal/wire-date.d.ts.map +1 -0
  113. package/dist/internal/wire-date.js +14 -0
  114. package/dist/internal/wire-date.js.map +1 -0
  115. package/dist/internal/zone-graph.d.ts +4 -0
  116. package/dist/internal/zone-graph.d.ts.map +1 -1
  117. package/dist/internal/zone-graph.js +54 -14
  118. package/dist/internal/zone-graph.js.map +1 -1
  119. package/dist/isr.d.ts +58 -2
  120. package/dist/isr.d.ts.map +1 -1
  121. package/dist/isr.js +296 -39
  122. package/dist/isr.js.map +1 -1
  123. package/dist/llms-txt.d.ts +2 -0
  124. package/dist/llms-txt.d.ts.map +1 -1
  125. package/dist/llms-txt.js +29 -3
  126. package/dist/llms-txt.js.map +1 -1
  127. package/dist/manifest.d.ts +28 -4
  128. package/dist/manifest.d.ts.map +1 -1
  129. package/dist/manifest.js +14 -3
  130. package/dist/manifest.js.map +1 -1
  131. package/dist/open-resolved.d.ts +18 -0
  132. package/dist/open-resolved.d.ts.map +1 -0
  133. package/dist/open-resolved.js +62 -0
  134. package/dist/open-resolved.js.map +1 -0
  135. package/dist/plugins/kit.d.ts +1 -0
  136. package/dist/plugins/kit.d.ts.map +1 -1
  137. package/dist/plugins/kit.js +26 -11
  138. package/dist/plugins/kit.js.map +1 -1
  139. package/dist/plugins/vite-leak-guard.d.ts +3 -1
  140. package/dist/plugins/vite-leak-guard.d.ts.map +1 -1
  141. package/dist/plugins/vite-leak-guard.js +4 -0
  142. package/dist/plugins/vite-leak-guard.js.map +1 -1
  143. package/dist/plugins/zone-guard.d.ts.map +1 -1
  144. package/dist/plugins/zone-guard.js +10 -2
  145. package/dist/plugins/zone-guard.js.map +1 -1
  146. package/dist/public-dir.d.ts +8 -1
  147. package/dist/public-dir.d.ts.map +1 -1
  148. package/dist/public-dir.js +17 -6
  149. package/dist/public-dir.js.map +1 -1
  150. package/dist/query.d.ts +13 -4
  151. package/dist/query.d.ts.map +1 -1
  152. package/dist/query.js +89 -26
  153. package/dist/query.js.map +1 -1
  154. package/dist/render-seam.d.ts +2 -0
  155. package/dist/render-seam.d.ts.map +1 -1
  156. package/dist/render-seam.js +1 -0
  157. package/dist/render-seam.js.map +1 -1
  158. package/dist/route-types.d.ts.map +1 -1
  159. package/dist/route-types.js +20 -9
  160. package/dist/route-types.js.map +1 -1
  161. package/dist/router.d.ts +9 -2
  162. package/dist/router.d.ts.map +1 -1
  163. package/dist/router.js +39 -46
  164. package/dist/router.js.map +1 -1
  165. package/dist/server-components/build-info.d.ts +3 -0
  166. package/dist/server-components/build-info.d.ts.map +1 -0
  167. package/dist/server-components/build-info.js +7 -0
  168. package/dist/server-components/build-info.js.map +1 -0
  169. package/dist/server-components/build-target.d.ts +12 -0
  170. package/dist/server-components/build-target.d.ts.map +1 -0
  171. package/dist/server-components/build-target.js +153 -0
  172. package/dist/server-components/build-target.js.map +1 -0
  173. package/dist/server-components/build.d.ts +24 -0
  174. package/dist/server-components/build.d.ts.map +1 -0
  175. package/dist/server-components/build.js +366 -0
  176. package/dist/server-components/build.js.map +1 -0
  177. package/dist/server-components/bundle-proof.d.ts +3 -0
  178. package/dist/server-components/bundle-proof.d.ts.map +1 -0
  179. package/dist/server-components/bundle-proof.js +23 -0
  180. package/dist/server-components/bundle-proof.js.map +1 -0
  181. package/dist/server-components/client.d.ts +46 -0
  182. package/dist/server-components/client.d.ts.map +1 -0
  183. package/dist/server-components/client.js +594 -0
  184. package/dist/server-components/client.js.map +1 -0
  185. package/dist/server-components/definition.d.ts +11 -0
  186. package/dist/server-components/definition.d.ts.map +1 -0
  187. package/dist/server-components/definition.js +94 -0
  188. package/dist/server-components/definition.js.map +1 -0
  189. package/dist/server-components/dev-http.d.ts +5 -0
  190. package/dist/server-components/dev-http.d.ts.map +1 -0
  191. package/dist/server-components/dev-http.js +71 -0
  192. package/dist/server-components/dev-http.js.map +1 -0
  193. package/dist/server-components/error.d.ts +9 -0
  194. package/dist/server-components/error.d.ts.map +1 -0
  195. package/dist/server-components/error.js +24 -0
  196. package/dist/server-components/error.js.map +1 -0
  197. package/dist/server-components/frames.d.ts +7 -0
  198. package/dist/server-components/frames.d.ts.map +1 -0
  199. package/dist/server-components/frames.js +169 -0
  200. package/dist/server-components/frames.js.map +1 -0
  201. package/dist/server-components/html-policy.d.ts +6 -0
  202. package/dist/server-components/html-policy.d.ts.map +1 -0
  203. package/dist/server-components/html-policy.js +75 -0
  204. package/dist/server-components/html-policy.js.map +1 -0
  205. package/dist/server-components/html.d.ts +4 -0
  206. package/dist/server-components/html.d.ts.map +1 -0
  207. package/dist/server-components/html.js +67 -0
  208. package/dist/server-components/html.js.map +1 -0
  209. package/dist/server-components/islands.d.ts +44 -0
  210. package/dist/server-components/islands.d.ts.map +1 -0
  211. package/dist/server-components/islands.js +42 -0
  212. package/dist/server-components/islands.js.map +1 -0
  213. package/dist/server-components/native.d.ts +4 -0
  214. package/dist/server-components/native.d.ts.map +1 -0
  215. package/dist/server-components/native.js +81 -0
  216. package/dist/server-components/native.js.map +1 -0
  217. package/dist/server-components/portal.d.ts +6 -0
  218. package/dist/server-components/portal.d.ts.map +1 -0
  219. package/dist/server-components/portal.js +35 -0
  220. package/dist/server-components/portal.js.map +1 -0
  221. package/dist/server-components/props.d.ts +3 -0
  222. package/dist/server-components/props.d.ts.map +1 -0
  223. package/dist/server-components/props.js +14 -0
  224. package/dist/server-components/props.js.map +1 -0
  225. package/dist/server-components/protocol.d.ts +44 -0
  226. package/dist/server-components/protocol.d.ts.map +1 -0
  227. package/dist/server-components/protocol.js +139 -0
  228. package/dist/server-components/protocol.js.map +1 -0
  229. package/dist/server-components/range.d.ts +24 -0
  230. package/dist/server-components/range.d.ts.map +1 -0
  231. package/dist/server-components/range.js +263 -0
  232. package/dist/server-components/range.js.map +1 -0
  233. package/dist/server-components/registry.d.ts +6 -0
  234. package/dist/server-components/registry.d.ts.map +1 -0
  235. package/dist/server-components/registry.js +30 -0
  236. package/dist/server-components/registry.js.map +1 -0
  237. package/dist/server-components/response.d.ts +4 -0
  238. package/dist/server-components/response.d.ts.map +1 -0
  239. package/dist/server-components/response.js +32 -0
  240. package/dist/server-components/response.js.map +1 -0
  241. package/dist/server-components/router-integration.d.ts +9 -0
  242. package/dist/server-components/router-integration.d.ts.map +1 -0
  243. package/dist/server-components/router-integration.js +29 -0
  244. package/dist/server-components/router-integration.js.map +1 -0
  245. package/dist/server-components/runtime.d.ts +14 -0
  246. package/dist/server-components/runtime.d.ts.map +1 -0
  247. package/dist/server-components/runtime.js +276 -0
  248. package/dist/server-components/runtime.js.map +1 -0
  249. package/dist/server-components/scheduler.d.ts +14 -0
  250. package/dist/server-components/scheduler.d.ts.map +1 -0
  251. package/dist/server-components/scheduler.js +74 -0
  252. package/dist/server-components/scheduler.js.map +1 -0
  253. package/dist/server-components/schema.d.ts +4 -0
  254. package/dist/server-components/schema.d.ts.map +1 -0
  255. package/dist/server-components/schema.js +77 -0
  256. package/dist/server-components/schema.js.map +1 -0
  257. package/dist/server-components/server-entry.d.ts +12 -0
  258. package/dist/server-components/server-entry.d.ts.map +1 -0
  259. package/dist/server-components/server-entry.js +90 -0
  260. package/dist/server-components/server-entry.js.map +1 -0
  261. package/dist/server-components/source.d.ts +15 -0
  262. package/dist/server-components/source.d.ts.map +1 -0
  263. package/dist/server-components/source.js +269 -0
  264. package/dist/server-components/source.js.map +1 -0
  265. package/dist/server-components/state.d.ts +4 -0
  266. package/dist/server-components/state.d.ts.map +1 -0
  267. package/dist/server-components/state.js +123 -0
  268. package/dist/server-components/state.js.map +1 -0
  269. package/dist/server-components/stream.d.ts +6 -0
  270. package/dist/server-components/stream.d.ts.map +1 -0
  271. package/dist/server-components/stream.js +70 -0
  272. package/dist/server-components/stream.js.map +1 -0
  273. package/dist/server-components/trusted-html.d.ts +3 -0
  274. package/dist/server-components/trusted-html.d.ts.map +1 -0
  275. package/dist/server-components/trusted-html.js +14 -0
  276. package/dist/server-components/trusted-html.js.map +1 -0
  277. package/dist/server-components/types.d.ts +107 -0
  278. package/dist/server-components/types.d.ts.map +1 -0
  279. package/dist/server-components/types.js +2 -0
  280. package/dist/server-components/types.js.map +1 -0
  281. package/dist/server-components/typescript.d.ts +11 -0
  282. package/dist/server-components/typescript.d.ts.map +1 -0
  283. package/dist/server-components/typescript.js +28 -0
  284. package/dist/server-components/typescript.js.map +1 -0
  285. package/dist/server-components/value.d.ts +5 -0
  286. package/dist/server-components/value.d.ts.map +1 -0
  287. package/dist/server-components/value.js +77 -0
  288. package/dist/server-components/value.js.map +1 -0
  289. package/dist/server-components/vite.d.ts +12 -0
  290. package/dist/server-components/vite.d.ts.map +1 -0
  291. package/dist/server-components/vite.js +197 -0
  292. package/dist/server-components/vite.js.map +1 -0
  293. package/dist/server-components.d.ts +5 -0
  294. package/dist/server-components.d.ts.map +1 -0
  295. package/dist/server-components.js +4 -0
  296. package/dist/server-components.js.map +1 -0
  297. package/dist/service-worker.d.ts +8 -1
  298. package/dist/service-worker.d.ts.map +1 -1
  299. package/dist/service-worker.js +20 -6
  300. package/dist/service-worker.js.map +1 -1
  301. package/dist/vite.d.ts +12 -0
  302. package/dist/vite.d.ts.map +1 -1
  303. package/dist/vite.js +77 -17
  304. package/dist/vite.js.map +1 -1
  305. package/dist/zones.d.ts +40 -0
  306. package/dist/zones.d.ts.map +1 -1
  307. package/dist/zones.js +170 -6
  308. package/dist/zones.js.map +1 -1
  309. package/package.json +97 -4
  310. package/src/build-vite.ts +31 -9
  311. package/src/build.ts +109 -31
  312. package/src/cdn.ts +18 -12
  313. package/src/client.ts +48 -23
  314. package/src/deferred.ts +27 -9
  315. package/src/dev-feed.ts +24 -5
  316. package/src/dev-session.ts +4 -13
  317. package/src/dev.ts +29 -1
  318. package/src/draft.ts +35 -14
  319. package/src/fonts-google.ts +10 -0
  320. package/src/index.ts +3 -2
  321. package/src/internal/codegen.ts +60 -29
  322. package/src/internal/disposable-dir.ts +24 -0
  323. package/src/internal/entry-chunks.ts +18 -0
  324. package/src/internal/evidence-traversal.ts +57 -0
  325. package/src/internal/head-merge.ts +16 -0
  326. package/src/internal/js-string.ts +6 -1
  327. package/src/internal/mount-shadow.ts +1 -1
  328. package/src/internal/output-guard.ts +88 -7
  329. package/src/internal/page-execution.ts +506 -108
  330. package/src/internal/progressive-cookies.ts +60 -0
  331. package/src/internal/public-env.ts +14 -0
  332. package/src/internal/render-document.ts +26 -11
  333. package/src/internal/response-controls.ts +31 -14
  334. package/src/internal/revive-dates.ts +35 -0
  335. package/src/internal/route-path.ts +8 -0
  336. package/src/internal/secret-scan.ts +104 -17
  337. package/src/internal/server-boundary.ts +29 -5
  338. package/src/internal/web-app.ts +116 -37
  339. package/src/internal/wire-date.ts +16 -0
  340. package/src/internal/zone-graph.ts +59 -19
  341. package/src/isr.ts +351 -43
  342. package/src/llms-txt.ts +32 -3
  343. package/src/manifest.ts +43 -7
  344. package/src/open-resolved.ts +64 -0
  345. package/src/plugins/kit.ts +31 -11
  346. package/src/plugins/vite-leak-guard.ts +5 -1
  347. package/src/plugins/zone-guard.ts +11 -2
  348. package/src/public-dir.ts +27 -8
  349. package/src/query.ts +107 -26
  350. package/src/render-seam.ts +2 -0
  351. package/src/route-types.ts +34 -7
  352. package/src/router.ts +58 -55
  353. package/src/server-components/build-info.ts +11 -0
  354. package/src/server-components/build-target.ts +194 -0
  355. package/src/server-components/build.ts +419 -0
  356. package/src/server-components/bundle-proof.ts +33 -0
  357. package/src/server-components/client.ts +663 -0
  358. package/src/server-components/definition.ts +125 -0
  359. package/src/server-components/dev-http.ts +75 -0
  360. package/src/server-components/error.ts +24 -0
  361. package/src/server-components/frames.ts +181 -0
  362. package/src/server-components/html-policy.ts +94 -0
  363. package/src/server-components/html.ts +81 -0
  364. package/src/server-components/islands.ts +111 -0
  365. package/src/server-components/native.ts +101 -0
  366. package/src/server-components/portal.ts +42 -0
  367. package/src/server-components/props.ts +19 -0
  368. package/src/server-components/protocol.ts +167 -0
  369. package/src/server-components/range.ts +313 -0
  370. package/src/server-components/registry.ts +34 -0
  371. package/src/server-components/response.ts +44 -0
  372. package/src/server-components/router-integration.ts +40 -0
  373. package/src/server-components/runtime.ts +376 -0
  374. package/src/server-components/scheduler.ts +84 -0
  375. package/src/server-components/schema.ts +69 -0
  376. package/src/server-components/server-entry.ts +98 -0
  377. package/src/server-components/source.ts +324 -0
  378. package/src/server-components/state.ts +143 -0
  379. package/src/server-components/stream.ts +71 -0
  380. package/src/server-components/trusted-html.ts +21 -0
  381. package/src/server-components/types.ts +138 -0
  382. package/src/server-components/typescript.ts +39 -0
  383. package/src/server-components/value.ts +82 -0
  384. package/src/server-components/vite.ts +242 -0
  385. package/src/server-components.ts +32 -0
  386. package/src/service-worker.ts +24 -7
  387. package/src/vite.ts +68 -17
  388. package/src/zones.ts +208 -7
@@ -0,0 +1,60 @@
1
+ import type { CookieOptions } from "@nifrajs/core/server"
2
+
3
+ export interface ProgressiveCookie {
4
+ readonly name: string
5
+ readonly value: string
6
+ readonly options?: CookieOptions | undefined
7
+ readonly deleted?: boolean
8
+ }
9
+
10
+ /** Parent-domain acceptance belongs to the browser's cookie jar and public-suffix policy. A Secure
11
+ * cookie on a request seen as `http:` does too: behind a TLS terminator only the browser knows its
12
+ * scheme, so it decides whether the cookie is kept. */
13
+ export function progressiveCookieRedirect(
14
+ request: Request,
15
+ changes: readonly ProgressiveCookie[],
16
+ ): boolean {
17
+ const url = new URL(request.url)
18
+ const host = url.hostname.toLowerCase()
19
+ return changes.some((change) => {
20
+ const domain = change.options?.domain?.replace(/^\./, "").toLowerCase()
21
+ if (domain !== undefined && domain !== host && host.endsWith("." + domain)) return true
22
+ return secureOnPlainHttp(url, change)
23
+ })
24
+ }
25
+
26
+ const secureOnPlainHttp = (url: URL, change: ProgressiveCookie): boolean =>
27
+ (change.options?.secure ?? !change.deleted) && url.protocol !== "https:"
28
+
29
+ /** Reauthorize the internal GET with action cookies that are valid for this exact owner URL. */
30
+ export function progressiveCookieHeader(
31
+ request: Request,
32
+ changes: readonly ProgressiveCookie[],
33
+ ): string {
34
+ const url = new URL(request.url)
35
+ let pairs = (request.headers.get("cookie") ?? "")
36
+ .split(";")
37
+ .map((part) => part.trim())
38
+ .filter(Boolean)
39
+ for (const change of changes) {
40
+ const options = change.options
41
+ const domain = options?.domain?.replace(/^\./, "").toLowerCase()
42
+ // Parent-domain cookies need a browser cookie jar/public-suffix decision; never synthesize them.
43
+ if (domain !== undefined && domain !== url.hostname.toLowerCase()) continue
44
+ if (secureOnPlainHttp(url, change)) continue
45
+ const path = options?.path ?? "/"
46
+ if (
47
+ url.pathname !== path &&
48
+ !(url.pathname.startsWith(path) && (path.endsWith("/") || url.pathname[path.length] === "/"))
49
+ )
50
+ continue
51
+ pairs = pairs.filter((pair) => pair.slice(0, pair.indexOf("=")).trim() !== change.name)
52
+ const expired =
53
+ options?.maxAge !== undefined
54
+ ? options.maxAge <= 0
55
+ : options?.expires !== undefined && options.expires.getTime() <= Date.now()
56
+ if (!change.deleted && !expired)
57
+ pairs.push(change.name + "=" + encodeURIComponent(change.value))
58
+ }
59
+ return pairs.join("; ")
60
+ }
@@ -0,0 +1,14 @@
1
+ export function publicEnvDefines(
2
+ prefix: string,
3
+ env: Readonly<Record<string, string | undefined>>,
4
+ ): Record<string, string> {
5
+ const defines: Record<string, string> = {}
6
+ if (prefix === "") return defines // opt-out: bake in nothing
7
+ for (const [name, value] of Object.entries(env)) {
8
+ // Skip `undefined` (a deleted/unset key can still enumerate) so we never bake in `"undefined"`.
9
+ if (name.startsWith(prefix) && value !== undefined) {
10
+ defines[`process.env.${name}`] = JSON.stringify(value)
11
+ }
12
+ }
13
+ return defines
14
+ }
@@ -76,6 +76,7 @@ export interface RenderedPage {
76
76
  }
77
77
 
78
78
  export interface RenderPageOptions {
79
+ readonly serverComponents?: import("../server-components/types.ts").ComponentState
79
80
  readonly adapter: RenderAdapter
80
81
  /** The layout chain to render - outermost layout → page (opaque; the adapter renders it). */
81
82
  readonly chain: readonly unknown[]
@@ -343,6 +344,10 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
343
344
  boundarySplit === undefined
344
345
  ? ""
345
346
  : `,"${BOUNDARY_GLOBAL}":${serializeData(boundarySplit.forClient)}`
347
+ const componentTail =
348
+ options.serverComponents === undefined
349
+ ? ""
350
+ : `,"__NIFRA_COMPONENTS__":${serializeData(options.serverComponents)}`
346
351
  const deferredRuntime =
347
352
  allDeferred.length > 0 ? `<script${nonceAttr}>${DEFERRED_RUNTIME}</script>` : ""
348
353
  // The regex only injects the CSP nonce into the (constant) hydration head; with no nonce it's a
@@ -443,7 +448,7 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
443
448
  // Tail - the page-state handover + the client module. Module scripts defer (run after parse), so
444
449
  // the handover + every streamed deferred resolution are in the document before the entry hydrates.
445
450
  const tailHtml = hydrate
446
- ? `${slot.tailPre}${serializeData(forClient)}${action}${layoutTail}${boundaryTail}${slot.tailPost}`
451
+ ? `${slot.tailPre}${serializeData(forClient)}${action}${layoutTail}${boundaryTail}${componentTail}${slot.tailPost}`
447
452
  : (slot.tailPost as string)
448
453
  const headers: Record<string, string> = { "content-type": "text/html; charset=utf-8" }
449
454
  // Caller-supplied headers (a terminal status page's `cache-control`, say). Applied before the
@@ -469,6 +474,9 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
469
474
  if (policy !== undefined) headers["content-security-policy"] = policy
470
475
  }
471
476
  const renderProps: RenderProps = {
477
+ ...(options.serverComponents === undefined
478
+ ? {}
479
+ : { serverComponents: options.serverComponents }),
472
480
  data: forComponent,
473
481
  actionData: actionSplit?.forComponent,
474
482
  // `params`/`path` thread the matched route + URL to the adapter's `useParams`/`useLocation` so they
@@ -616,13 +624,17 @@ function streamDocument(
616
624
  enc: TextEncoder,
617
625
  nonceAttr: string,
618
626
  ): ReadableStream<Uint8Array> {
627
+ const reader = stream.getReader()
628
+ // A client that goes away cancels the body: the renderer stops, and nothing more is written.
629
+ let cancelled = false
630
+ let released = false
619
631
  return new ReadableStream({
620
632
  async start(controller) {
621
- const reader = stream.getReader()
622
633
  try {
623
634
  controller.enqueue(shell)
624
635
  for (;;) {
625
636
  const { done, value } = await reader.read()
637
+ if (cancelled) return
626
638
  if (done) break
627
639
  controller.enqueue(value)
628
640
  }
@@ -631,34 +643,37 @@ function streamDocument(
631
643
  // defer() must not block a faster one; each script self-addresses by id, so order is irrelevant.
632
644
  await Promise.all(
633
645
  deferred.map(async (d) => {
646
+ let script: string
634
647
  try {
635
648
  const value = serializeData(await d.promise)
636
649
  // `serializeData` JSON-serializes the value and escapes `<`, `>`, and Unicode line
637
650
  // separators before this framework-owned script is emitted.
638
- controller.enqueue(
639
- enc.encode(`<script${nonceAttr}>window.__nifraResolve(${d.id},${value})</script>`), // nosemgrep: javascript.lang.security.audit.unknown-value-with-script-tag.unknown-value-with-script-tag
640
- )
651
+ script = `<script${nonceAttr}>window.__nifraResolve(${d.id},${value})</script>` // nosemgrep: javascript.lang.security.audit.unknown-value-with-script-tag.unknown-value-with-script-tag
641
652
  } catch (err) {
642
653
  // A rejected deferred streams __nifraReject (the client `<Await>` surfaces it) - it must
643
654
  // not break the whole body. Redact: stream a stable opaque code, never the raw error
644
655
  // text; log the real reason server-side.
645
656
  console.error("[nifra/web] deferred value rejected:", err)
646
- controller.enqueue(
647
- enc.encode(
648
- `<script${nonceAttr}>window.__nifraReject(${d.id},${serializeData(DEFERRED_ERROR_CODE)})</script>`,
649
- ),
650
- )
657
+ script = `<script${nonceAttr}>window.__nifraReject(${d.id},${serializeData(DEFERRED_ERROR_CODE)})</script>`
651
658
  }
659
+ if (!cancelled) controller.enqueue(enc.encode(script))
652
660
  }),
653
661
  )
662
+ if (cancelled) return
654
663
  controller.enqueue(tail)
655
664
  controller.close()
656
665
  } catch (err) {
657
- controller.error(err)
666
+ if (!cancelled) controller.error(err)
658
667
  } finally {
668
+ released = true
659
669
  reader.releaseLock()
660
670
  }
661
671
  },
672
+ cancel(reason) {
673
+ cancelled = true
674
+ // Once the renderer is drained the reader is released, and there is nothing left to stop.
675
+ return released ? undefined : reader.cancel(reason)
676
+ },
662
677
  })
663
678
  }
664
679
 
@@ -2,9 +2,15 @@ import {
2
2
  type CookieOptions,
3
3
  type ResponseResult,
4
4
  status as statusResult,
5
+ writableResponse,
5
6
  } from "@nifrajs/core/server"
6
7
  import type { LoaderResponseControls } from "../manifest.ts"
7
8
  import { DATA_HEADER } from "../router.ts"
9
+ import {
10
+ type ProgressiveCookie,
11
+ progressiveCookieHeader,
12
+ progressiveCookieRedirect,
13
+ } from "./progressive-cookies.ts"
8
14
  import { isResponseResult } from "./render-document.ts"
9
15
 
10
16
  /** The serving app's response controls - the structural subset of a route handler's `c.set` used here. */
@@ -107,19 +113,9 @@ export function privateOutcome(outcome: unknown): unknown {
107
113
 
108
114
  function privateResponse(response: Response): Response {
109
115
  if (response.headers.get(CACHE_CONTROL) === PRIVATE_NO_STORE) return response
110
- try {
111
- response.headers.set(CACHE_CONTROL, PRIVATE_NO_STORE)
112
- return response
113
- } catch {
114
- // Immutable headers (`Response.redirect()`, a `fetch()` result): answer with a copy.
115
- const headers = new Headers(response.headers)
116
- headers.set(CACHE_CONTROL, PRIVATE_NO_STORE)
117
- return new Response(response.body, {
118
- status: response.status,
119
- statusText: response.statusText,
120
- headers,
121
- })
122
- }
116
+ const target = writableResponse(response)
117
+ target.headers.set(CACHE_CONTROL, PRIVATE_NO_STORE)
118
+ return target
123
119
  }
124
120
 
125
121
  interface WrittenHeaders {
@@ -140,6 +136,7 @@ export class PageResponseControls {
140
136
  #written: WrittenHeaders[] | undefined
141
137
  #sealed = false
142
138
  #personalized = false
139
+ #cookieChanges: ProgressiveCookie[] | undefined
143
140
 
144
141
  constructor(c: { readonly set: CoreResponseControls }) {
145
142
  this.#c = c
@@ -167,7 +164,27 @@ export class PageResponseControls {
167
164
  cookies(member: string): CoreResponseControls {
168
165
  this.assertOpen(member)
169
166
  this.personalize()
170
- return this.#c.set
167
+ const controls = this.#c.set
168
+ return {
169
+ headers: controls.headers,
170
+ cookie: (name, value, options) => {
171
+ controls.cookie(name, value, options)
172
+ this.#cookieChanges ??= []
173
+ this.#cookieChanges.push({ name, value, options })
174
+ },
175
+ deleteCookie: (name, options) => {
176
+ controls.deleteCookie(name, options)
177
+ this.#cookieChanges ??= []
178
+ this.#cookieChanges.push({ name, value: "", options, deleted: true })
179
+ },
180
+ }
181
+ }
182
+
183
+ progressiveCookies(request: Request): string {
184
+ return progressiveCookieHeader(request, this.#cookieChanges ?? [])
185
+ }
186
+ progressiveRedirect(request: Request): boolean {
187
+ return progressiveCookieRedirect(request, this.#cookieChanges ?? [])
171
188
  }
172
189
 
173
190
  /** Mark the response as one visitor's - a queued cookie, or a draft render - so no cache stores it. */
@@ -0,0 +1,35 @@
1
+ /**
2
+ * `t.date()` across the wire, browser half: every place the browser parses page data rebuilds the
3
+ * `{ "__nifra_date": iso }` tags the server's `wireDate()` wrote into `Date`s. Kept apart from
4
+ * `wire-date.ts` so client bundles never carry the server class.
5
+ */
6
+
7
+ export const DATE_TAG = "__nifra_date"
8
+ const TAGGED = `"${DATE_TAG}"`
9
+
10
+ // A JSON.parse reviver runs leaves first and defines each result as an own property, so a key named
11
+ // `__proto__` never becomes the prototype.
12
+ const revive = (_key: string, value: unknown): unknown => {
13
+ if (typeof value !== "object" || value === null) return value
14
+ const tagged: unknown = Reflect.get(value, DATE_TAG)
15
+ return typeof tagged === "string" && Object.keys(value).length === 1 ? new Date(tagged) : value
16
+ }
17
+
18
+ /** `JSON.parse` that rebuilds every `{ "__nifra_date": iso }` into a `Date`, when the text has one. */
19
+ export const parsePageData = (text: string): unknown =>
20
+ text.includes(TAGGED) ? JSON.parse(text, revive) : JSON.parse(text)
21
+
22
+ /**
23
+ * {@link parsePageData}'s reviver as source declaring `__nifraDates`, for client code nifra generates rather than
24
+ * bundles (server-function stubs; `DEFERRED_RUNTIME` carries the same text). A literal with nothing
25
+ * interpolated: Bun keeps a template that interpolates an import, and with it every string built from
26
+ * it, in client bundles that never use them.
27
+ */
28
+ export const REVIVE_DATES_SOURCE = `const __nifraDates = (d) => {
29
+ if (!d || typeof d !== "object") return d
30
+ if (Array.isArray(d)) { for (let i = 0; i < d.length; i++) d[i] = __nifraDates(d[i]); return d }
31
+ const k = Object.keys(d)
32
+ if (k.length === 1 && k[0] === "__nifra_date" && typeof d.__nifra_date === "string") return new Date(d.__nifra_date)
33
+ 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 }) }
34
+ return d
35
+ }`
@@ -0,0 +1,8 @@
1
+ /**
2
+ * A route pattern's literal text as a request path spells it. A route file named `café.tsx` is
3
+ * requested as `/caf%C3%A9`, so the routers, which compare the raw request path, are given the pattern
4
+ * that way. Only controls, space and non-ASCII are encoded (what a browser encodes in a path), so ASCII
5
+ * punctuation such as a param constraint keeps its meaning. Runs once per pattern, never per request.
6
+ */
7
+ export const requestPattern = (pattern: string): string =>
8
+ pattern.normalize("NFC").replace(/[^!-~]/gu, encodeURIComponent)
@@ -148,15 +148,54 @@ 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
157
+
158
+ /** Whether a match's name is what the literal is assigned to. A quoted name must be the whole string,
159
+ * not the tail of one (`? "new-password" : "current-password"`), and a key is never preceded by a
160
+ * ternary's `?` or a member access's `.` (`ready ? config.apiKey : "..."`). */
161
+ function namesTheTarget(text: string, start: number, quote: string, operator: string): boolean {
162
+ let before = start - 1
163
+ if (quote !== "") {
164
+ if (text[before] !== quote) return false
165
+ before--
166
+ }
167
+ if (operator !== ":") return true
168
+ while (before >= 0 && /\s/.test(text[before] ?? "")) before--
169
+ return text[before] !== "?" && text[before] !== "."
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
+ }
156
191
 
157
192
  const EXTRA_SECRET_NAME =
158
193
  /(?:accesskey|signingkey|encryptionkey|masterkey|serviceaccountkey|connectionstring)$/
159
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
+
160
199
  /** Values that look like keys but are public by design. */
161
200
  const PUBLIC_VALUE =
162
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+)$/
@@ -186,6 +225,32 @@ function looksRandom(value: string, minLength: number): boolean {
186
225
  return classes >= 2 && entropy(value) >= 3.5
187
226
  }
188
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
+
189
254
  function jwtRole(payload: string): unknown {
190
255
  try {
191
256
  const json = atob(payload.replaceAll("-", "+").replaceAll("_", "/"))
@@ -217,11 +282,7 @@ function formatMatches(text: string): Match[] {
217
282
  matches.push({ rule: "private-key", index: found.index, text: found[0], what: "private key" })
218
283
  }
219
284
  for (const found of text.matchAll(CREDENTIAL_URL)) {
220
- const user = found[1] ?? ""
221
- const password = found[2] ?? ""
222
- // @nifra-gate-reviewed: a password equal to its user name is a placeholder in the text being
223
- // scanned; nothing is authenticated here.
224
- if (PLACEHOLDER.test(password) || password === user || /[${}<>*]/.test(password)) continue
285
+ if (placeholderPassword(found[1] ?? "", found[2] ?? "")) continue
225
286
  matches.push({
226
287
  rule: "credential-url",
227
288
  index: found.index,
@@ -232,20 +293,46 @@ function formatMatches(text: string): Match[] {
232
293
  return matches
233
294
  }
234
295
 
296
+ /** The credential a literal given to `name` carries, when the name is secret-like and the literal
297
+ * looks random. */
298
+ function credentialOf(name: string, value: string): string | undefined {
299
+ const key = name.toLowerCase().replace(/[-_]/g, "")
300
+ const header = AUTHORIZATION_NAME.test(key)
301
+ if (!header && !isSensitiveFieldName(name) && !EXTRA_SECRET_NAME.test(key)) return undefined
302
+ const secret = header ? authorizationCredential(value) : value
303
+ if (
304
+ secret === undefined ||
305
+ PUBLIC_VALUE.test(secret) ||
306
+ PLACEHOLDER.test(secret) ||
307
+ !looksRandom(secret, 16)
308
+ )
309
+ return undefined
310
+ return secret
311
+ }
312
+
235
313
  function assignedMatches(text: string): Match[] {
236
314
  const matches: Match[] = []
237
- for (const found of text.matchAll(ASSIGNED)) {
238
- const name = found[1] ?? ""
239
- if (!isSensitiveFieldName(name) && !EXTRA_SECRET_NAME.test(name.toLowerCase())) continue
240
- const value = found[2] ?? found[3] ?? found[4] ?? ""
241
- if (PUBLIC_VALUE.test(value) || PLACEHOLDER.test(value) || !looksRandom(value, 16)) continue
315
+ const add = (name: string, value: string, valueAt: number): void => {
316
+ const secret = credentialOf(name, value)
317
+ if (secret === undefined) return
242
318
  matches.push({
243
319
  rule: "assigned-secret",
244
- index: found.index + found[0].indexOf(value),
245
- text: value,
320
+ // The credential is the value's tail: a header value's scheme is not part of it.
321
+ index: valueAt + value.length - secret.length,
322
+ text: secret,
246
323
  what: `literal assigned to "${name}"`,
247
324
  })
248
325
  }
326
+ for (const found of text.matchAll(ASSIGNED)) {
327
+ if (!namesTheTarget(text, found.index, found[2] ?? "", found[3] ?? "")) continue
328
+ const value = found[4] ?? found[5] ?? found[6] ?? ""
329
+ add(found[1] ?? "", value, found.index + found[0].lastIndexOf(value))
330
+ }
331
+ for (const found of text.matchAll(PAIRED)) {
332
+ const value = found[3] ?? found[4] ?? found[5] ?? ""
333
+ if (NAME_LIKE.test(value) || !opensAPair(text, found.index, found[6] ?? "")) continue
334
+ add(found[2] ?? "", value, found.index + found[0].lastIndexOf(value))
335
+ }
249
336
  return matches
250
337
  }
251
338
 
@@ -493,7 +580,7 @@ export function createRedactor(
493
580
  for (const { what, pattern } of KEY_FORMATS) out = out.replace(pattern, `[redacted:${what}]`)
494
581
  out = out.replace(JWT, "[redacted:JWT]")
495
582
  out = out.replace(CREDENTIAL_URL, (match, user: string, password: string) =>
496
- // @nifra-gate-reviewed: the same placeholder test as the scan; nothing is authenticated here.
583
+ // nifra-expect NF-S002: the same placeholder test as the scan; nothing is authenticated here.
497
584
  PLACEHOLDER.test(password) || password === user
498
585
  ? match
499
586
  : 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
  }