@nifrajs/web 3.5.0 → 4.0.2

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 (390) hide show
  1. package/dist/backend-only.d.ts +15 -0
  2. package/dist/backend-only.d.ts.map +1 -0
  3. package/dist/backend-only.js +16 -0
  4. package/dist/backend-only.js.map +1 -0
  5. package/dist/boundary.d.ts +5 -0
  6. package/dist/boundary.d.ts.map +1 -1
  7. package/dist/boundary.js +11 -2
  8. package/dist/boundary.js.map +1 -1
  9. package/dist/build-plan.d.ts +18 -6
  10. package/dist/build-plan.d.ts.map +1 -1
  11. package/dist/build-plan.js +82 -34
  12. package/dist/build-plan.js.map +1 -1
  13. package/dist/build-vite.d.ts +4 -0
  14. package/dist/build-vite.d.ts.map +1 -1
  15. package/dist/build-vite.js +68 -13
  16. package/dist/build-vite.js.map +1 -1
  17. package/dist/build.d.ts +57 -23
  18. package/dist/build.d.ts.map +1 -1
  19. package/dist/build.js +356 -85
  20. package/dist/build.js.map +1 -1
  21. package/dist/bun-dev-entry.d.ts +1 -1
  22. package/dist/bun-dev-entry.js +1 -1
  23. package/dist/cdn.d.ts +74 -0
  24. package/dist/cdn.d.ts.map +1 -0
  25. package/dist/cdn.js +132 -0
  26. package/dist/cdn.js.map +1 -0
  27. package/dist/client.d.ts +3 -1
  28. package/dist/client.d.ts.map +1 -1
  29. package/dist/client.js +136 -35
  30. package/dist/client.js.map +1 -1
  31. package/dist/conformance.d.ts +4 -3
  32. package/dist/conformance.d.ts.map +1 -1
  33. package/dist/conformance.js +12 -3
  34. package/dist/conformance.js.map +1 -1
  35. package/dist/csp.d.ts +93 -0
  36. package/dist/csp.d.ts.map +1 -0
  37. package/dist/csp.js +122 -0
  38. package/dist/csp.js.map +1 -0
  39. package/dist/dev-client.d.ts +76 -0
  40. package/dist/dev-client.d.ts.map +1 -0
  41. package/dist/dev-client.js +280 -0
  42. package/dist/dev-client.js.map +1 -0
  43. package/dist/dev-diagnostics.d.ts +26 -2
  44. package/dist/dev-diagnostics.d.ts.map +1 -1
  45. package/dist/dev-diagnostics.js +38 -7
  46. package/dist/dev-diagnostics.js.map +1 -1
  47. package/dist/dev-error.d.ts +4 -13
  48. package/dist/dev-error.d.ts.map +1 -1
  49. package/dist/dev-error.js +46 -4
  50. package/dist/dev-error.js.map +1 -1
  51. package/dist/dev-feed.d.ts +298 -0
  52. package/dist/dev-feed.d.ts.map +1 -0
  53. package/dist/dev-feed.js +797 -0
  54. package/dist/dev-feed.js.map +1 -0
  55. package/dist/dev-indicator.d.ts +38 -0
  56. package/dist/dev-indicator.d.ts.map +1 -0
  57. package/dist/dev-indicator.js +93 -0
  58. package/dist/dev-indicator.js.map +1 -0
  59. package/dist/dev-session.d.ts +77 -0
  60. package/dist/dev-session.d.ts.map +1 -0
  61. package/dist/dev-session.js +580 -0
  62. package/dist/dev-session.js.map +1 -0
  63. package/dist/dev-sourcemap.d.ts +27 -0
  64. package/dist/dev-sourcemap.d.ts.map +1 -0
  65. package/dist/dev-sourcemap.js +235 -0
  66. package/dist/dev-sourcemap.js.map +1 -0
  67. package/dist/dev-ssr-graph.d.ts.map +1 -1
  68. package/dist/dev-ssr-graph.js +13 -5
  69. package/dist/dev-ssr-graph.js.map +1 -1
  70. package/dist/dev.d.ts +15 -2
  71. package/dist/dev.d.ts.map +1 -1
  72. package/dist/dev.js +69 -44
  73. package/dist/dev.js.map +1 -1
  74. package/dist/diagnostic-catalog.d.ts +35 -0
  75. package/dist/diagnostic-catalog.d.ts.map +1 -0
  76. package/dist/diagnostic-catalog.js +203 -0
  77. package/dist/diagnostic-catalog.js.map +1 -0
  78. package/dist/diagnostic-prompt.d.ts +49 -0
  79. package/dist/diagnostic-prompt.d.ts.map +1 -0
  80. package/dist/diagnostic-prompt.js +303 -0
  81. package/dist/diagnostic-prompt.js.map +1 -0
  82. package/dist/diagnostic.d.ts +17 -23
  83. package/dist/diagnostic.d.ts.map +1 -1
  84. package/dist/diagnostic.js +31 -39
  85. package/dist/diagnostic.js.map +1 -1
  86. package/dist/draft.d.ts.map +1 -1
  87. package/dist/draft.js +4 -1
  88. package/dist/draft.js.map +1 -1
  89. package/dist/fn.d.ts +5 -0
  90. package/dist/fn.d.ts.map +1 -1
  91. package/dist/fn.js +13 -3
  92. package/dist/fn.js.map +1 -1
  93. package/dist/fs.d.ts.map +1 -1
  94. package/dist/fs.js +4 -2
  95. package/dist/fs.js.map +1 -1
  96. package/dist/index.d.ts +13 -15
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +11 -12
  99. package/dist/index.js.map +1 -1
  100. package/dist/internal/cdn/cloudflare.d.ts +48 -0
  101. package/dist/internal/cdn/cloudflare.d.ts.map +1 -0
  102. package/dist/internal/cdn/cloudflare.js +94 -0
  103. package/dist/internal/cdn/cloudflare.js.map +1 -0
  104. package/dist/internal/cdn/core.d.ts +91 -0
  105. package/dist/internal/cdn/core.d.ts.map +1 -0
  106. package/dist/internal/cdn/core.js +220 -0
  107. package/dist/internal/cdn/core.js.map +1 -0
  108. package/dist/internal/cdn/fastly.d.ts +17 -0
  109. package/dist/internal/cdn/fastly.d.ts.map +1 -0
  110. package/dist/internal/cdn/fastly.js +43 -0
  111. package/dist/internal/cdn/fastly.js.map +1 -0
  112. package/dist/internal/cdn/vercel.d.ts +31 -0
  113. package/dist/internal/cdn/vercel.d.ts.map +1 -0
  114. package/dist/internal/cdn/vercel.js +56 -0
  115. package/dist/internal/cdn/vercel.js.map +1 -0
  116. package/dist/internal/code-unit-order.d.ts +4 -0
  117. package/dist/internal/code-unit-order.d.ts.map +1 -0
  118. package/dist/internal/code-unit-order.js +6 -0
  119. package/dist/internal/code-unit-order.js.map +1 -0
  120. package/dist/internal/codegen.d.ts.map +1 -1
  121. package/dist/internal/codegen.js +141 -25
  122. package/dist/internal/codegen.js.map +1 -1
  123. package/dist/internal/dev-csp.d.ts +24 -0
  124. package/dist/internal/dev-csp.d.ts.map +1 -0
  125. package/dist/internal/dev-csp.js +160 -0
  126. package/dist/internal/dev-csp.js.map +1 -0
  127. package/dist/internal/dev-reserved.d.ts +25 -0
  128. package/dist/internal/dev-reserved.d.ts.map +1 -0
  129. package/dist/internal/dev-reserved.js +39 -0
  130. package/dist/internal/dev-reserved.js.map +1 -0
  131. package/dist/internal/html-spans.d.ts +10 -0
  132. package/dist/internal/html-spans.d.ts.map +1 -0
  133. package/dist/internal/html-spans.js +46 -0
  134. package/dist/internal/html-spans.js.map +1 -0
  135. package/dist/internal/loading-runtime.d.ts +44 -0
  136. package/dist/internal/loading-runtime.d.ts.map +1 -0
  137. package/dist/internal/loading-runtime.js +206 -0
  138. package/dist/internal/loading-runtime.js.map +1 -0
  139. package/dist/internal/matches-runtime.d.ts +8 -0
  140. package/dist/internal/matches-runtime.d.ts.map +1 -0
  141. package/dist/internal/matches-runtime.js +61 -0
  142. package/dist/internal/matches-runtime.js.map +1 -0
  143. package/dist/internal/mount-shadow.d.ts +36 -0
  144. package/dist/internal/mount-shadow.d.ts.map +1 -0
  145. package/dist/internal/mount-shadow.js +65 -0
  146. package/dist/internal/mount-shadow.js.map +1 -0
  147. package/dist/internal/node-builtins.d.ts +3 -0
  148. package/dist/internal/node-builtins.d.ts.map +1 -0
  149. package/dist/internal/node-builtins.js +79 -0
  150. package/dist/internal/node-builtins.js.map +1 -0
  151. package/dist/internal/output-guard.d.ts +65 -0
  152. package/dist/internal/output-guard.d.ts.map +1 -0
  153. package/dist/internal/output-guard.js +630 -0
  154. package/dist/internal/output-guard.js.map +1 -0
  155. package/dist/internal/page-execution.d.ts +20 -8
  156. package/dist/internal/page-execution.d.ts.map +1 -1
  157. package/dist/internal/page-execution.js +571 -105
  158. package/dist/internal/page-execution.js.map +1 -1
  159. package/dist/internal/parity.d.ts +21 -0
  160. package/dist/internal/parity.d.ts.map +1 -1
  161. package/dist/internal/parity.js +104 -22
  162. package/dist/internal/parity.js.map +1 -1
  163. package/dist/internal/private-env.d.ts +12 -0
  164. package/dist/internal/private-env.d.ts.map +1 -0
  165. package/dist/internal/private-env.js +335 -0
  166. package/dist/internal/private-env.js.map +1 -0
  167. package/dist/internal/public-url.d.ts +3 -0
  168. package/dist/internal/public-url.d.ts.map +1 -0
  169. package/dist/internal/public-url.js +18 -0
  170. package/dist/internal/public-url.js.map +1 -0
  171. package/dist/internal/render-document.d.ts +25 -19
  172. package/dist/internal/render-document.d.ts.map +1 -1
  173. package/dist/internal/render-document.js +77 -61
  174. package/dist/internal/render-document.js.map +1 -1
  175. package/dist/internal/response-controls.d.ts +75 -0
  176. package/dist/internal/response-controls.d.ts.map +1 -0
  177. package/dist/internal/response-controls.js +256 -0
  178. package/dist/internal/response-controls.js.map +1 -0
  179. package/dist/internal/runtime-contract.d.ts +11 -16
  180. package/dist/internal/runtime-contract.d.ts.map +1 -1
  181. package/dist/internal/runtime-contract.js.map +1 -1
  182. package/dist/internal/secret-scan.d.ts +104 -0
  183. package/dist/internal/secret-scan.d.ts.map +1 -0
  184. package/dist/internal/secret-scan.js +503 -0
  185. package/dist/internal/secret-scan.js.map +1 -0
  186. package/dist/internal/server-boundary.d.ts +6 -29
  187. package/dist/internal/server-boundary.d.ts.map +1 -1
  188. package/dist/internal/server-boundary.js +6 -66
  189. package/dist/internal/server-boundary.js.map +1 -1
  190. package/dist/internal/source-map.d.ts +47 -0
  191. package/dist/internal/source-map.d.ts.map +1 -0
  192. package/dist/internal/source-map.js +225 -0
  193. package/dist/internal/source-map.js.map +1 -0
  194. package/dist/internal/target-compat.d.ts +27 -0
  195. package/dist/internal/target-compat.d.ts.map +1 -0
  196. package/dist/internal/target-compat.js +77 -0
  197. package/dist/internal/target-compat.js.map +1 -0
  198. package/dist/internal/timing-safe-equal.d.ts +6 -0
  199. package/dist/internal/timing-safe-equal.d.ts.map +1 -1
  200. package/dist/internal/timing-safe-equal.js +10 -0
  201. package/dist/internal/timing-safe-equal.js.map +1 -1
  202. package/dist/internal/vitals-runtime.d.ts +18 -0
  203. package/dist/internal/vitals-runtime.d.ts.map +1 -0
  204. package/dist/internal/vitals-runtime.js +38 -0
  205. package/dist/internal/vitals-runtime.js.map +1 -0
  206. package/dist/internal/web-app.d.ts +38 -9
  207. package/dist/internal/web-app.d.ts.map +1 -1
  208. package/dist/internal/web-app.js +199 -59
  209. package/dist/internal/web-app.js.map +1 -1
  210. package/dist/internal/zone-graph.d.ts +89 -0
  211. package/dist/internal/zone-graph.d.ts.map +1 -0
  212. package/dist/internal/zone-graph.js +492 -0
  213. package/dist/internal/zone-graph.js.map +1 -0
  214. package/dist/isr.d.ts +83 -6
  215. package/dist/isr.d.ts.map +1 -1
  216. package/dist/isr.js +263 -36
  217. package/dist/isr.js.map +1 -1
  218. package/dist/llms-txt.d.ts.map +1 -1
  219. package/dist/llms-txt.js +31 -6
  220. package/dist/llms-txt.js.map +1 -1
  221. package/dist/manifest.d.ts +220 -13
  222. package/dist/manifest.d.ts.map +1 -1
  223. package/dist/manifest.js +394 -9
  224. package/dist/manifest.js.map +1 -1
  225. package/dist/module-graph.d.ts +26 -5
  226. package/dist/module-graph.d.ts.map +1 -1
  227. package/dist/module-graph.js +17 -7
  228. package/dist/module-graph.js.map +1 -1
  229. package/dist/navigation.d.ts +17 -3
  230. package/dist/navigation.d.ts.map +1 -1
  231. package/dist/navigation.js +11 -5
  232. package/dist/navigation.js.map +1 -1
  233. package/dist/nonce.d.ts +47 -0
  234. package/dist/nonce.d.ts.map +1 -0
  235. package/dist/nonce.js +63 -0
  236. package/dist/nonce.js.map +1 -0
  237. package/dist/plugins/css-modules.d.ts.map +1 -1
  238. package/dist/plugins/css-modules.js +4 -3
  239. package/dist/plugins/css-modules.js.map +1 -1
  240. package/dist/plugins/kit.d.ts +9 -0
  241. package/dist/plugins/kit.d.ts.map +1 -1
  242. package/dist/plugins/kit.js +18 -0
  243. package/dist/plugins/kit.js.map +1 -1
  244. package/dist/plugins/svg.d.ts +22 -3
  245. package/dist/plugins/svg.d.ts.map +1 -1
  246. package/dist/plugins/svg.js +112 -1
  247. package/dist/plugins/svg.js.map +1 -1
  248. package/dist/plugins/vite-leak-guard.d.ts +107 -3
  249. package/dist/plugins/vite-leak-guard.d.ts.map +1 -1
  250. package/dist/plugins/vite-leak-guard.js +378 -17
  251. package/dist/plugins/vite-leak-guard.js.map +1 -1
  252. package/dist/plugins/vite-zone-guard.d.ts +61 -0
  253. package/dist/plugins/vite-zone-guard.d.ts.map +1 -0
  254. package/dist/plugins/vite-zone-guard.js +208 -0
  255. package/dist/plugins/vite-zone-guard.js.map +1 -0
  256. package/dist/plugins/zone-guard.d.ts +27 -0
  257. package/dist/plugins/zone-guard.d.ts.map +1 -0
  258. package/dist/plugins/zone-guard.js +80 -0
  259. package/dist/plugins/zone-guard.js.map +1 -0
  260. package/dist/prerender.d.ts +10 -0
  261. package/dist/prerender.d.ts.map +1 -1
  262. package/dist/prerender.js +35 -3
  263. package/dist/prerender.js.map +1 -1
  264. package/dist/public-dir.d.ts +9 -0
  265. package/dist/public-dir.d.ts.map +1 -1
  266. package/dist/public-dir.js +67 -9
  267. package/dist/public-dir.js.map +1 -1
  268. package/dist/pwa-manifest.d.ts +68 -0
  269. package/dist/pwa-manifest.d.ts.map +1 -0
  270. package/dist/pwa-manifest.js +178 -0
  271. package/dist/pwa-manifest.js.map +1 -0
  272. package/dist/query.js +4 -4
  273. package/dist/query.js.map +1 -1
  274. package/dist/render-seam.d.ts +42 -2
  275. package/dist/render-seam.d.ts.map +1 -1
  276. package/dist/render-seam.js +6 -0
  277. package/dist/render-seam.js.map +1 -1
  278. package/dist/route-manifest.d.ts +3 -0
  279. package/dist/route-manifest.d.ts.map +1 -1
  280. package/dist/route-manifest.js +26 -3
  281. package/dist/route-manifest.js.map +1 -1
  282. package/dist/route-types.d.ts +24 -0
  283. package/dist/route-types.d.ts.map +1 -0
  284. package/dist/route-types.js +195 -0
  285. package/dist/route-types.js.map +1 -0
  286. package/dist/router.d.ts +28 -6
  287. package/dist/router.d.ts.map +1 -1
  288. package/dist/router.js +129 -40
  289. package/dist/router.js.map +1 -1
  290. package/dist/vitals.d.ts +33 -0
  291. package/dist/vitals.d.ts.map +1 -0
  292. package/dist/vitals.js +16 -0
  293. package/dist/vitals.js.map +1 -0
  294. package/dist/vite.d.ts +16 -1
  295. package/dist/vite.d.ts.map +1 -1
  296. package/dist/vite.js +248 -43
  297. package/dist/vite.js.map +1 -1
  298. package/dist/zones.d.ts +70 -0
  299. package/dist/zones.d.ts.map +1 -0
  300. package/dist/zones.js +365 -0
  301. package/dist/zones.js.map +1 -0
  302. package/package.json +67 -14
  303. package/src/backend-only.ts +16 -0
  304. package/src/boundary.ts +18 -5
  305. package/src/build-plan.ts +90 -35
  306. package/src/build-vite.ts +77 -11
  307. package/src/build.ts +466 -96
  308. package/src/bun-dev-entry.ts +1 -1
  309. package/src/cdn.ts +216 -0
  310. package/src/client.ts +135 -36
  311. package/src/conformance.ts +16 -3
  312. package/src/csp.ts +194 -0
  313. package/src/dev-client.ts +349 -0
  314. package/src/dev-diagnostics.ts +60 -7
  315. package/src/dev-error.ts +49 -5
  316. package/src/dev-feed.ts +1140 -0
  317. package/src/dev-indicator.ts +113 -0
  318. package/src/dev-session.ts +729 -0
  319. package/src/dev-sourcemap.ts +281 -0
  320. package/src/dev-ssr-graph.ts +16 -5
  321. package/src/dev.ts +86 -43
  322. package/src/diagnostic-catalog.ts +243 -0
  323. package/src/diagnostic-prompt.ts +355 -0
  324. package/src/diagnostic.ts +63 -66
  325. package/src/draft.ts +4 -1
  326. package/src/fn.ts +19 -3
  327. package/src/fs.ts +13 -2
  328. package/src/index.ts +42 -10
  329. package/src/internal/cdn/cloudflare.ts +146 -0
  330. package/src/internal/cdn/core.ts +316 -0
  331. package/src/internal/cdn/fastly.ts +62 -0
  332. package/src/internal/cdn/vercel.ts +95 -0
  333. package/src/internal/code-unit-order.ts +5 -0
  334. package/src/internal/codegen.ts +161 -24
  335. package/src/internal/dev-csp.ts +176 -0
  336. package/src/internal/dev-reserved.ts +52 -0
  337. package/src/internal/html-spans.ts +50 -0
  338. package/src/internal/loading-runtime.ts +244 -0
  339. package/src/internal/matches-runtime.ts +62 -0
  340. package/src/internal/mount-shadow.ts +88 -0
  341. package/src/internal/node-builtins.ts +79 -0
  342. package/src/internal/output-guard.ts +701 -0
  343. package/src/internal/page-execution.ts +795 -185
  344. package/src/internal/parity.ts +150 -36
  345. package/src/internal/private-env.ts +334 -0
  346. package/src/internal/public-url.ts +22 -0
  347. package/src/internal/render-document.ts +114 -67
  348. package/src/internal/response-controls.ts +284 -0
  349. package/src/internal/runtime-contract.ts +10 -15
  350. package/src/internal/secret-scan.ts +638 -0
  351. package/src/internal/server-boundary.ts +6 -69
  352. package/src/internal/source-map.ts +256 -0
  353. package/src/internal/target-compat.ts +92 -0
  354. package/src/internal/timing-safe-equal.ts +13 -0
  355. package/src/internal/vitals-runtime.ts +53 -0
  356. package/src/internal/web-app.ts +262 -84
  357. package/src/internal/zone-graph.ts +593 -0
  358. package/src/isr.ts +325 -37
  359. package/src/llms-txt.ts +28 -4
  360. package/src/manifest.ts +682 -16
  361. package/src/module-graph.ts +53 -9
  362. package/src/navigation.ts +29 -6
  363. package/src/nonce.ts +104 -0
  364. package/src/plugins/css-modules.ts +5 -3
  365. package/src/plugins/kit.ts +24 -0
  366. package/src/plugins/svg.ts +135 -4
  367. package/src/plugins/vite-leak-guard.ts +529 -16
  368. package/src/plugins/vite-zone-guard.ts +263 -0
  369. package/src/plugins/zone-guard.ts +119 -0
  370. package/src/prerender.ts +49 -3
  371. package/src/public-dir.ts +69 -8
  372. package/src/pwa-manifest.ts +226 -0
  373. package/src/query.ts +4 -4
  374. package/src/render-seam.ts +46 -1
  375. package/src/route-manifest.ts +15 -3
  376. package/src/route-types.ts +264 -0
  377. package/src/router.ts +163 -63
  378. package/src/vitals.ts +44 -0
  379. package/src/vite.ts +257 -42
  380. package/src/zones.ts +430 -0
  381. package/dist/plugins/vite-server-only.d.ts +0 -49
  382. package/dist/plugins/vite-server-only.d.ts.map +0 -1
  383. package/dist/plugins/vite-server-only.js +0 -57
  384. package/dist/plugins/vite-server-only.js.map +0 -1
  385. package/dist/server-only.d.ts +0 -21
  386. package/dist/server-only.d.ts.map +0 -1
  387. package/dist/server-only.js +0 -25
  388. package/dist/server-only.js.map +0 -1
  389. package/src/plugins/vite-server-only.ts +0 -75
  390. package/src/server-only.ts +0 -25
@@ -0,0 +1,701 @@
1
+ /**
2
+ * The data guard. Every value a loader, action, boundary loader or server function sends toward the
3
+ * browser passes through its declared output schema first:
4
+ *
5
+ * - keys the schema does not declare are dropped - that is the guarantee, and it holds even when the
6
+ * schema itself would accept them;
7
+ * - a declared field of the wrong shape fails the request, and the error names field paths only,
8
+ * never a value;
9
+ * - a deferred value must be declared as one (`t.deferred(inner)`), and its resolved value is guarded
10
+ * by `inner` before it streams.
11
+ *
12
+ * Projection reads the schema's JSON Schema: a nifra schema's `jsonSchema`, or a Standard JSON Schema
13
+ * (`~standard.jsonSchema.output`). A schema that exposes neither is trusted to strip unknown keys in
14
+ * its own `validate` output, as zod and valibot objects do by default.
15
+ */
16
+ import type { StandardResult, StandardSchemaV1 } from "@nifrajs/core/server"
17
+
18
+ // Registry symbols shared with core and `@nifrajs/schema`, so two copies of either still agree.
19
+ const RESPONSE_RESULT = Symbol.for("nifra.response.result")
20
+ const STATUS_SIGNAL = Symbol.for("nifra.web.status-signal")
21
+ /** Set by `t.deferred(inner)` on its schema node: `inner`'s own validate. */
22
+ const DEFERRED_VALIDATE = Symbol.for("nifra.schema.deferredValidate")
23
+
24
+ export const DECLASSIFIED_KEY = "x-nifra-declassified"
25
+ const DEFERRED_KEY = "x-nifra-deferred"
26
+
27
+ /** A field name that usually holds a credential or personal identifier. */
28
+ export function isSensitiveFieldName(name: string): boolean {
29
+ const key = name.toLowerCase().replace(/[-_]/g, "")
30
+ return SENSITIVE_NAMES.has(key) || SENSITIVE_SUFFIXES.some((suffix) => key.endsWith(suffix))
31
+ }
32
+
33
+ const SENSITIVE_NAMES: ReadonlySet<string> = new Set([
34
+ "passwd",
35
+ "token",
36
+ "sessionid",
37
+ "ssn",
38
+ "socialsecuritynumber",
39
+ "creditcard",
40
+ "creditcardnumber",
41
+ "cardnumber",
42
+ "cvv",
43
+ "cvc",
44
+ "otp",
45
+ ])
46
+ const SENSITIVE_SUFFIXES: readonly string[] = [
47
+ "password",
48
+ "passwordhash",
49
+ "secret",
50
+ "apikey",
51
+ "apitoken",
52
+ "privatekey",
53
+ "secretkey",
54
+ "accesstoken",
55
+ "refreshtoken",
56
+ "idtoken",
57
+ "authtoken",
58
+ "sessiontoken",
59
+ "bearertoken",
60
+ ]
61
+
62
+ /** A browser-bound value failed its output contract. The message carries paths, never values. */
63
+ export class OutputGuardError extends Error {
64
+ override readonly name = "OutputGuardError"
65
+ readonly paths: readonly string[]
66
+ constructor(message: string, paths: readonly string[] = []) {
67
+ super(`[nifra/web] ${message}`)
68
+ this.paths = paths
69
+ }
70
+ }
71
+
72
+ type Node = { readonly [key: string]: unknown }
73
+ interface RunState {
74
+ readonly label: string
75
+ readonly issues: string[]
76
+ }
77
+ type Project = (value: unknown, state: RunState) => unknown
78
+
79
+ interface CompiledGuard {
80
+ /** Undefined when the schema exposes no JSON Schema to project by. */
81
+ readonly project: Project | undefined
82
+ readonly validate: StandardSchemaV1["~standard"]["validate"]
83
+ }
84
+
85
+ interface Deferred {
86
+ readonly __nifra_deferred: true
87
+ readonly id: number
88
+ readonly promise: Promise<unknown>
89
+ }
90
+
91
+ const isDeferred = (value: unknown): value is Deferred =>
92
+ typeof value === "object" &&
93
+ value !== null &&
94
+ (value as { __nifra_deferred?: unknown }).__nifra_deferred === true
95
+
96
+ const isNode = (value: unknown): value is Node =>
97
+ typeof value === "object" && value !== null && !Array.isArray(value)
98
+
99
+ const isPlain = (value: object): boolean => {
100
+ const proto = Object.getPrototypeOf(value)
101
+ return proto === Object.prototype || proto === null
102
+ }
103
+
104
+ /** Re-root the issues children pushed since `from` under `key`. Runs only on failure. */
105
+ function prefix(state: RunState, from: number, key: string | number): void {
106
+ for (let i = from; i < state.issues.length; i++) state.issues[i] = `/${key}${state.issues[i]}`
107
+ }
108
+
109
+ function assign(out: Record<string, unknown>, key: string, value: unknown): void {
110
+ if (key === "__proto__") {
111
+ Object.defineProperty(out, key, { value, enumerable: true, writable: true, configurable: true })
112
+ } else out[key] = value
113
+ }
114
+
115
+ /** Every deferred marker inside a value no schema declares as deferred is an issue. */
116
+ const UNKNOWN: Project = (value, state) => {
117
+ if (typeof value !== "object" || value === null) return value
118
+ if (isDeferred(value)) {
119
+ state.issues.push("")
120
+ return value
121
+ }
122
+ if (Array.isArray(value)) {
123
+ for (let i = 0; i < value.length; i++) {
124
+ const from = state.issues.length
125
+ UNKNOWN(value[i], state)
126
+ if (state.issues.length > from) prefix(state, from, i)
127
+ }
128
+ } else if (isPlain(value)) {
129
+ for (const key of Object.keys(value)) {
130
+ const from = state.issues.length
131
+ UNKNOWN((value as Record<string, unknown>)[key], state)
132
+ if (state.issues.length > from) prefix(state, from, key)
133
+ }
134
+ }
135
+ return value
136
+ }
137
+
138
+ /**
139
+ * A scalar schema: the validator decides, and a deferred marker cannot pass a scalar check. A `Date`
140
+ * becomes what JSON makes of it (its ISO string), so a `t.string()` field accepts a timestamp column
141
+ * and the page renders on the server with the value the browser receives.
142
+ */
143
+ const LEAF: Project = (value) => (value instanceof Date ? value.toJSON() : value)
144
+
145
+ /** An array whose items are all scalars, passed through whole. */
146
+ const LEAF_ARRAY: Project = (value) => value
147
+
148
+ const typesOf = (node: Node): readonly unknown[] | undefined =>
149
+ Array.isArray(node.type) ? node.type : node.type === undefined ? undefined : [node.type]
150
+
151
+ const COMBINATORS = ["anyOf", "oneOf", "allOf"] as const
152
+
153
+ /** What a schema node (with no `$ref` or combinator) projects as. */
154
+ function shapeOf(node: Node): "deferred" | "object" | "array" | "both" | "leaf" | "unknown" {
155
+ if (node[DEFERRED_KEY] === true) return "deferred"
156
+ const types = typesOf(node)
157
+ const object =
158
+ types?.includes("object") === true ||
159
+ (types === undefined &&
160
+ (node.properties !== undefined ||
161
+ node.patternProperties !== undefined ||
162
+ node.additionalProperties !== undefined))
163
+ const array =
164
+ types?.includes("array") === true ||
165
+ (types === undefined && (node.items !== undefined || node.prefixItems !== undefined))
166
+ if (object && array) return "both"
167
+ if (object) return "object"
168
+ if (array) return "array"
169
+ return types !== undefined ||
170
+ node.const !== undefined ||
171
+ node.enum !== undefined ||
172
+ node.not !== undefined
173
+ ? "leaf"
174
+ : "unknown"
175
+ }
176
+
177
+ class Compiler {
178
+ private readonly memo = new Map<Node, Project>()
179
+ private readonly ids = new Map<string, Node>()
180
+ readonly sensitive: string[] = []
181
+
182
+ constructor(private readonly root: Node) {
183
+ this.collectIds(root, new Set())
184
+ }
185
+
186
+ private collectIds(value: unknown, seen: Set<unknown>): void {
187
+ if (typeof value !== "object" || value === null || seen.has(value)) return
188
+ seen.add(value)
189
+ if (Array.isArray(value)) {
190
+ for (const item of value) this.collectIds(item, seen)
191
+ return
192
+ }
193
+ const id = (value as Node).$id
194
+ if (typeof id === "string" && !this.ids.has(id)) this.ids.set(id, value as Node)
195
+ for (const key of Object.keys(value)) this.collectIds((value as Node)[key], seen)
196
+ }
197
+
198
+ private resolve(ref: string, where: string): Node {
199
+ const byId = this.ids.get(ref)
200
+ if (byId !== undefined) return byId
201
+ if (ref === "#") return this.root
202
+ if (ref.startsWith("#/")) {
203
+ let node: unknown = this.root
204
+ for (const raw of ref.slice(2).split("/")) {
205
+ const segment = raw.replaceAll("~1", "/").replaceAll("~0", "~")
206
+ node = isNode(node) ? node[segment] : undefined
207
+ }
208
+ if (isNode(node)) return node
209
+ }
210
+ throw new OutputGuardError(
211
+ `the output schema references "${ref}" at ${where || "/"}, which it does not define`,
212
+ )
213
+ }
214
+
215
+ compile(node: Node, where: string): Project {
216
+ const known = this.memo.get(node)
217
+ if (known !== undefined) return known
218
+ let impl: Project = LEAF
219
+ const late: Project = (value, state) => impl(value, state)
220
+ this.memo.set(node, late)
221
+ impl = this.build(node, where)
222
+ this.memo.set(node, impl)
223
+ return impl
224
+ }
225
+
226
+ private build(node: Node, where: string): Project {
227
+ if (typeof node.$ref === "string") return this.compile(this.resolve(node.$ref, where), where)
228
+ if (COMBINATORS.some((key) => Array.isArray(node[key]))) return this.merged(node, where)
229
+ switch (shapeOf(node)) {
230
+ case "deferred":
231
+ return this.deferred(node, where)
232
+ case "both":
233
+ return this.dispatch(this.object(node, where), this.array(node, where), undefined)
234
+ case "object":
235
+ return this.object(node, where)
236
+ case "array":
237
+ return this.array(node, where)
238
+ case "leaf":
239
+ return LEAF
240
+ default:
241
+ return UNKNOWN
242
+ }
243
+ }
244
+
245
+ private extra(schema: unknown, where: string): Project | undefined {
246
+ if (schema === true || (isNode(schema) && Object.keys(schema).length === 0)) return UNKNOWN
247
+ return isNode(schema) ? this.compile(schema, `${where}/*`) : undefined
248
+ }
249
+
250
+ private object(node: Node, where: string): Project {
251
+ const declared = new Map<string, Project>()
252
+ for (const [key, sub] of Object.entries(isNode(node.properties) ? node.properties : {})) {
253
+ if (!isNode(sub)) continue
254
+ if (isSensitiveFieldName(key) && typeof sub[DECLASSIFIED_KEY] !== "string") {
255
+ this.sensitive.push(`${where}/${key}`)
256
+ }
257
+ declared.set(key, this.compile(sub, `${where}/${key}`))
258
+ }
259
+ const patterns: Array<readonly [RegExp, Project]> = []
260
+ for (const [pattern, sub] of Object.entries(
261
+ isNode(node.patternProperties) ? node.patternProperties : {},
262
+ )) {
263
+ if (isNode(sub)) patterns.push([new RegExp(pattern, "u"), this.compile(sub, `${where}/*`)])
264
+ }
265
+ // Undeclared means dropped. Only an explicit `true` or schema keeps extra keys open.
266
+ const extra =
267
+ this.extra(node.additionalProperties, where) ??
268
+ (node.additionalProperties === undefined
269
+ ? this.extra(node.unevaluatedProperties, where)
270
+ : undefined)
271
+ const open = extra !== undefined || patterns.length > 0
272
+ const undeclared = (key: string): Project | undefined => {
273
+ for (const [pattern, project] of patterns) if (pattern.test(key)) return project
274
+ return extra
275
+ }
276
+ // A closed object drops a deferred marker's keys unless it declares them, so only an open one, or
277
+ // one declaring the marker key, could pass a marker on.
278
+ const checksMarker = open || declared.has("__nifra_deferred")
279
+ const project: Project = (value, state) => {
280
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return value
281
+ if (checksMarker && isDeferred(value)) {
282
+ state.issues.push("")
283
+ return value
284
+ }
285
+ const proto = Object.getPrototypeOf(value)
286
+ if (proto !== Object.prototype && proto !== null) {
287
+ if (value instanceof Date) return value.toJSON()
288
+ // A class instance: its own fields as a plain object, so no prototype `toJSON` or getter
289
+ // decides what is serialized.
290
+ const snapshot: Record<string, unknown> = {}
291
+ for (const key of Object.keys(value))
292
+ assign(snapshot, key, (value as Record<string, unknown>)[key])
293
+ return project(snapshot, state)
294
+ }
295
+ const input = value as Record<string, unknown>
296
+ // Copy-on-write: a value that already holds only declared keys is returned as it is.
297
+ let out: Record<string, unknown> | undefined
298
+ for (const key in input) {
299
+ // An inherited enumerable key (a polluted prototype) is never data.
300
+ if (!Object.hasOwn(input, key)) continue
301
+ const projector = declared.get(key) ?? (open ? undeclared(key) : undefined)
302
+ if (projector === undefined) {
303
+ out ??= copyBefore(input, key)
304
+ continue
305
+ }
306
+ const before = input[key]
307
+ if (projector === LEAF_ARRAY || (projector === LEAF && !(before instanceof Date))) {
308
+ if (out !== undefined) assign(out, key, before)
309
+ continue
310
+ }
311
+ const from = state.issues.length
312
+ const after = projector(before, state)
313
+ if (state.issues.length > from) prefix(state, from, key)
314
+ if (out !== undefined) assign(out, key, after)
315
+ else if (after !== before) {
316
+ out = copyBefore(input, key)
317
+ assign(out, key, after)
318
+ }
319
+ }
320
+ return out ?? value
321
+ }
322
+ return project
323
+ }
324
+
325
+ private array(node: Node, where: string): Project {
326
+ const prefixNodes = Array.isArray(node.prefixItems)
327
+ ? node.prefixItems
328
+ : Array.isArray(node.items)
329
+ ? node.items
330
+ : []
331
+ const head = prefixNodes.map((sub, i) =>
332
+ isNode(sub) ? this.compile(sub, `${where}/${i}`) : UNKNOWN,
333
+ )
334
+ const restSchema = Array.isArray(node.items) ? node.additionalItems : node.items
335
+ const rest = this.extra(restSchema, where) ?? UNKNOWN
336
+ // An array of scalars is the validator's alone: no key to drop, and walking it here would cost
337
+ // every `string[]` a loop. A `Date` inside one reaches the validator as it is.
338
+ if (rest === LEAF && head.every((projector) => projector === LEAF)) return LEAF_ARRAY
339
+ return (value, state) => {
340
+ if (!Array.isArray(value)) return value
341
+ let out: unknown[] | undefined
342
+ for (let i = 0; i < value.length; i++) {
343
+ const before = value[i]
344
+ const projector = head[i] ?? rest
345
+ if (projector === LEAF && !(before instanceof Date)) {
346
+ if (out !== undefined) out[i] = before
347
+ continue
348
+ }
349
+ const from = state.issues.length
350
+ const after = projector(before, state)
351
+ if (state.issues.length > from) prefix(state, from, i)
352
+ if (out !== undefined) out[i] = after
353
+ else if (after !== before) {
354
+ out = value.slice(0, i)
355
+ out[i] = after
356
+ }
357
+ }
358
+ return out ?? value
359
+ }
360
+ }
361
+
362
+ /** The branches of a union or intersection, flattened, `$ref`s resolved. */
363
+ private branches(node: Node, where: string, into: Node[], seen: Set<Node>): void {
364
+ if (seen.has(node)) return
365
+ seen.add(node)
366
+ if (typeof node.$ref === "string") {
367
+ this.branches(this.resolve(node.$ref, where), where, into, seen)
368
+ return
369
+ }
370
+ let combined = false
371
+ const own: Record<string, unknown> = {}
372
+ for (const [key, value] of Object.entries(node)) {
373
+ if ((COMBINATORS as readonly string[]).includes(key) && Array.isArray(value)) {
374
+ combined = true
375
+ for (const branch of value) if (isNode(branch)) this.branches(branch, where, into, seen)
376
+ } else own[key] = value
377
+ }
378
+ if (!combined) into.push(node)
379
+ else if (
380
+ own.type !== undefined ||
381
+ own.properties !== undefined ||
382
+ own.patternProperties !== undefined ||
383
+ own.additionalProperties !== undefined ||
384
+ own.items !== undefined ||
385
+ own.prefixItems !== undefined
386
+ ) {
387
+ into.push(own)
388
+ }
389
+ }
390
+
391
+ /**
392
+ * A union or intersection keeps a key when any branch declares it. That can keep a key a
393
+ * different branch than the one the value matches declares - still a declared name, and a strict
394
+ * branch then fails validation - but never an undeclared one.
395
+ */
396
+ private merged(node: Node, where: string): Project {
397
+ const branches: Node[] = []
398
+ this.branches(node, where, branches, new Set())
399
+ const kinds = branches.map(shapeOf)
400
+ if (kinds.includes("unknown")) return UNKNOWN
401
+ const deferreds = branches.filter((_, i) => kinds[i] === "deferred")
402
+ if (deferreds.length > 1) {
403
+ throw new OutputGuardError(
404
+ `the output schema at ${where || "/"} unions more than one deferred value`,
405
+ )
406
+ }
407
+ const objects = branches.filter((_, i) => kinds[i] === "object" || kinds[i] === "both")
408
+ const arrays = branches.filter((_, i) => kinds[i] === "array" || kinds[i] === "both")
409
+ const deferred = deferreds[0]
410
+ return this.dispatch(
411
+ objects.length === 0 ? undefined : this.object(mergeObjects(objects), where),
412
+ arrays.length === 0 ? undefined : this.array(mergeArrays(arrays), where),
413
+ deferred === undefined ? undefined : this.compile(deferred, where),
414
+ )
415
+ }
416
+
417
+ private dispatch(
418
+ object: Project | undefined,
419
+ array: Project | undefined,
420
+ deferred: Project | undefined,
421
+ ): Project {
422
+ return (value, state) => {
423
+ if (typeof value !== "object" || value === null) return value
424
+ if (value instanceof Date) return value.toJSON()
425
+ if (Array.isArray(value)) return array === undefined ? value : array(value, state)
426
+ if (isDeferred(value)) {
427
+ if (deferred !== undefined) return deferred(value, state)
428
+ state.issues.push("")
429
+ return value
430
+ }
431
+ return object === undefined ? value : object(value, state)
432
+ }
433
+ }
434
+
435
+ private deferred(node: Node, where: string): Project {
436
+ const validate = (node as { readonly [DEFERRED_VALIDATE]?: unknown })[DEFERRED_VALIDATE]
437
+ if (!isNode(node.inner) || typeof validate !== "function") {
438
+ throw new OutputGuardError(
439
+ `the deferred value at ${where || "/"} has no inner schema; declare it with t.deferred(schema)`,
440
+ )
441
+ }
442
+ const inner: CompiledGuard = {
443
+ project: this.compile(node.inner, where),
444
+ validate: validate as CompiledGuard["validate"],
445
+ }
446
+ return (value, state) => {
447
+ if (!isDeferred(value)) return value
448
+ const label = `${state.label}, deferred value ${where || "/"},`
449
+ const promise = value.promise.then((resolved) => runGuard(inner, resolved, label))
450
+ // Claimed like `defer()` claims its own: the stream consumer attaches later.
451
+ void promise.catch(() => {})
452
+ return { __nifra_deferred: true, id: value.id, promise }
453
+ }
454
+ }
455
+ }
456
+
457
+ /** The keys of a plain object before `stop`, every one of them kept as it is. */
458
+ function copyBefore(input: Record<string, unknown>, stop: string): Record<string, unknown> {
459
+ const out: Record<string, unknown> = {}
460
+ for (const key in input) {
461
+ if (key === stop) break
462
+ if (Object.hasOwn(input, key)) assign(out, key, input[key])
463
+ }
464
+ return out
465
+ }
466
+
467
+ function mergeObjects(branches: readonly Node[]): Node {
468
+ const properties = new Map<string, Node[]>()
469
+ const patterns = new Map<string, Node[]>()
470
+ const extras: Node[] = []
471
+ let open = false
472
+ for (const branch of branches) {
473
+ if (isNode(branch.properties)) {
474
+ for (const [key, sub] of Object.entries(branch.properties)) {
475
+ if (isNode(sub)) properties.set(key, [...(properties.get(key) ?? []), sub])
476
+ }
477
+ }
478
+ if (isNode(branch.patternProperties)) {
479
+ for (const [pattern, sub] of Object.entries(branch.patternProperties)) {
480
+ if (isNode(sub)) patterns.set(pattern, [...(patterns.get(pattern) ?? []), sub])
481
+ }
482
+ }
483
+ const extra = branch.additionalProperties ?? branch.unevaluatedProperties
484
+ if (extra === true) open = true
485
+ else if (isNode(extra)) extras.push(extra)
486
+ }
487
+ const one = (subs: readonly Node[]): Node => {
488
+ if (subs.length === 1) return subs[0] as Node
489
+ // Declassified only when every branch declassifies the field.
490
+ const reason = subs.every((sub) => typeof sub[DECLASSIFIED_KEY] === "string")
491
+ ? subs[0]?.[DECLASSIFIED_KEY]
492
+ : undefined
493
+ return reason === undefined ? { anyOf: subs } : { anyOf: subs, [DECLASSIFIED_KEY]: reason }
494
+ }
495
+ return {
496
+ type: "object",
497
+ properties: Object.fromEntries([...properties].map(([key, subs]) => [key, one(subs)])),
498
+ patternProperties: Object.fromEntries([...patterns].map(([key, subs]) => [key, one(subs)])),
499
+ ...(open
500
+ ? { additionalProperties: true }
501
+ : extras.length > 0
502
+ ? { additionalProperties: one(extras) }
503
+ : {}),
504
+ }
505
+ }
506
+
507
+ function mergeArrays(branches: readonly Node[]): Node {
508
+ const items: Node[] = []
509
+ let open = false
510
+ for (const branch of branches) {
511
+ const item = Array.isArray(branch.items) ? undefined : branch.items
512
+ if (item === undefined || item === true) open = true
513
+ else if (isNode(item)) items.push(item)
514
+ // A tuple branch: its positions merge into the shared item schema.
515
+ for (const sub of [
516
+ ...(Array.isArray(branch.prefixItems) ? branch.prefixItems : []),
517
+ ...(Array.isArray(branch.items) ? branch.items : []),
518
+ ]) {
519
+ if (isNode(sub)) items.push(sub)
520
+ }
521
+ }
522
+ return {
523
+ type: "array",
524
+ ...(open ? {} : { items: items.length === 1 ? items[0] : { anyOf: items } }),
525
+ }
526
+ }
527
+
528
+ /** The JSON Schema a schema exposes to project by, if any. */
529
+ function jsonSchemaOf(schema: StandardSchemaV1): Node | undefined {
530
+ const own = (schema as { readonly jsonSchema?: unknown }).jsonSchema
531
+ if (isNode(own)) return own
532
+ const standard = schema["~standard"] as {
533
+ readonly jsonSchema?: { readonly output?: (options: { target: string }) => unknown }
534
+ }
535
+ if (typeof standard.jsonSchema?.output !== "function") return undefined
536
+ try {
537
+ const out = standard.jsonSchema.output({ target: "draft-2020-12" })
538
+ return isNode(out) ? out : undefined
539
+ } catch {
540
+ return undefined
541
+ }
542
+ }
543
+
544
+ const compiled = new WeakMap<object, CompiledGuard>()
545
+
546
+ /**
547
+ * Prepare `schema` as the output contract of `owner`. Throws when the schema declares a sensitive
548
+ * field it does not declassify, or references a definition it lacks - at route load, not per request.
549
+ */
550
+ export function outputGuard(schema: unknown, owner: string, exportName: string): CompiledGuard {
551
+ if (
552
+ typeof schema !== "object" ||
553
+ schema === null ||
554
+ typeof (schema as StandardSchemaV1)["~standard"]?.validate !== "function"
555
+ ) {
556
+ throw new OutputGuardError(`${owner} exports ${exportName} that is not a Standard Schema`)
557
+ }
558
+ const cached = compiled.get(schema)
559
+ if (cached !== undefined) return cached
560
+ const standard = schema as StandardSchemaV1
561
+ const json = jsonSchemaOf(standard)
562
+ let project: Project | undefined
563
+ if (json !== undefined) {
564
+ const compiler = new Compiler(json)
565
+ project = compiler.compile(json, "")
566
+ if (compiler.sensitive.length > 0) {
567
+ throw new OutputGuardError(
568
+ `${owner} declares sensitive field(s) ${compiler.sensitive.join(", ")} in ${exportName}. Remove them, or mark a field that must reach the browser with t.declassified("why it may", schema)`,
569
+ compiler.sensitive,
570
+ )
571
+ }
572
+ }
573
+ const guard: CompiledGuard = { project, validate: standard["~standard"].validate }
574
+ compiled.set(schema, guard)
575
+ return guard
576
+ }
577
+
578
+ const pathOf = (issue: { readonly path?: ReadonlyArray<unknown> | undefined }): string =>
579
+ (issue.path ?? [])
580
+ .map(
581
+ (segment) =>
582
+ `/${String(typeof segment === "object" && segment !== null ? (segment as { key: unknown }).key : segment)}`,
583
+ )
584
+ .join("") || "/"
585
+
586
+ function settle(guard: CompiledGuard, result: StandardResult<unknown>, label: string): unknown {
587
+ if (result.issues !== undefined) {
588
+ const paths = [...new Set(result.issues.map(pathOf))]
589
+ throw new OutputGuardError(
590
+ `${label} does not match its output schema at: ${paths.join(", ")}`,
591
+ paths,
592
+ )
593
+ }
594
+ if (guard.project === undefined) {
595
+ const state: RunState = { label, issues: [] }
596
+ UNKNOWN(result.value, state)
597
+ if (state.issues.length > 0) throw undeclaredDeferred(label, state.issues)
598
+ }
599
+ return result.value
600
+ }
601
+
602
+ const undeclaredDeferred = (label: string, paths: readonly string[]): OutputGuardError =>
603
+ new OutputGuardError(
604
+ `${label} returns a deferred value its output schema does not declare at: ${paths.map((p) => p || "/").join(", ")}. Declare it with t.deferred(schema)`,
605
+ paths,
606
+ )
607
+
608
+ /** Project `value` through `guard`, then validate it. Throws (or rejects) an {@link OutputGuardError}. */
609
+ export function runGuard(guard: CompiledGuard, value: unknown, label: string): unknown {
610
+ let projected = value
611
+ if (guard.project !== undefined) {
612
+ const state: RunState = { label, issues: [] }
613
+ projected = guard.project(value, state)
614
+ if (state.issues.length > 0) throw undeclaredDeferred(label, state.issues)
615
+ }
616
+ const result = guard.validate(projected)
617
+ return result instanceof Promise
618
+ ? result.then((settled) => settle(guard, settled, label))
619
+ : settle(guard, result, label)
620
+ }
621
+
622
+ /** What a browser-bound channel sends, and how it is named in errors. */
623
+ export interface ChannelContract {
624
+ /** `the loader of "blog/[slug].tsx"` */
625
+ readonly label: string
626
+ readonly guard: CompiledGuard | undefined
627
+ /** Where the missing schema goes, for the error when data arrives without one. */
628
+ readonly missing: string
629
+ /** An action may wrap its data in `revalidate(paths, data)`. */
630
+ readonly revalidate?: boolean
631
+ }
632
+
633
+ const isResponseResult = (
634
+ value: object,
635
+ ): value is { readonly plain?: { readonly status: number }; toResponse(): Response } =>
636
+ (value as { readonly [RESPONSE_RESULT]?: unknown })[RESPONSE_RESULT] === true &&
637
+ typeof (value as { readonly toResponse?: unknown }).toResponse === "function"
638
+
639
+ /**
640
+ * Guard one value a channel produced. Nothing, a redirect and an error status pass as they are; a 2xx
641
+ * `Response` is refused because its body would reach the browser without passing the schema.
642
+ */
643
+ export function guardValue(contract: ChannelContract, value: unknown): unknown {
644
+ if (value === undefined || value === null) return value
645
+ if (typeof value === "object") {
646
+ let status: number | undefined
647
+ if (value instanceof Response) status = STATUS_SIGNAL in value ? undefined : value.status
648
+ else if (isResponseResult(value)) status = value.plain?.status ?? value.toResponse().status
649
+ else if (contract.revalidate === true && "__nifraRevalidate" in value) {
650
+ const wrapper = value as { readonly data?: unknown }
651
+ const data = guardValue(contract, wrapper.data)
652
+ return data instanceof Promise
653
+ ? data.then((settled) => ({ ...wrapper, data: settled }))
654
+ : { ...wrapper, data }
655
+ }
656
+ if (status !== undefined && status >= 200 && status < 300) {
657
+ throw new OutputGuardError(
658
+ `${contract.label} returned a ${status} Response, whose body would reach the browser without passing its output schema. Return the data itself, or serve the endpoint from backend/app.ts`,
659
+ )
660
+ }
661
+ if (value instanceof Response || isResponseResult(value)) return value
662
+ }
663
+ if (contract.guard === undefined) {
664
+ throw new OutputGuardError(
665
+ `${contract.label} returned data, but declares no output schema. Every value sent to the browser needs one: ${contract.missing}`,
666
+ )
667
+ }
668
+ return runGuard(contract.guard, value, contract.label)
669
+ }
670
+
671
+ /**
672
+ * Wrap a channel function so everything it returns passes {@link guardValue}. A scalar result stays
673
+ * synchronous; an object result settles through a promise, as `await` would. A thrown `Response` is
674
+ * the same answer as a returned one, so a thrown 2xx is refused as well.
675
+ */
676
+ export function guardChannel<Args extends unknown[]>(
677
+ fn: (...args: Args) => unknown,
678
+ contract: ChannelContract,
679
+ ): (...args: Args) => unknown {
680
+ const check = (value: unknown): unknown => guardValue(contract, value)
681
+ const checkThrown = (error: unknown): never => {
682
+ if (
683
+ typeof error === "object" &&
684
+ error !== null &&
685
+ (error instanceof Response || isResponseResult(error))
686
+ )
687
+ guardValue(contract, error)
688
+ throw error
689
+ }
690
+ return (...args) => {
691
+ let value: unknown
692
+ try {
693
+ value = fn(...args)
694
+ } catch (error) {
695
+ return checkThrown(error)
696
+ }
697
+ return value !== null && (typeof value === "object" || typeof value === "function")
698
+ ? Promise.resolve(value).then(check, checkThrown)
699
+ : check(value)
700
+ }
701
+ }