@nifrajs/web 3.5.0 → 4.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 (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 +111 -15
  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 +485 -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 +386 -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 +72 -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 +187 -36
  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 +111 -18
  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 +623 -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 +670 -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 +110 -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 +207 -36
  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,349 @@
1
+ /**
2
+ * The browser half of the dev feed: a small inline script a dev server puts first in each page's
3
+ * `<head>`, so the agent sees what the page did after it left the server - uncaught errors, unhandled
4
+ * rejections, failed script/stylesheet loads, hydration mismatches and console output.
5
+ *
6
+ * Inline and first, rather than a module, so it is listening before any module (the framework, the
7
+ * HMR client, the app) runs. Dev only: nothing here is reachable from a production build.
8
+ */
9
+
10
+ import { createHash } from "node:crypto"
11
+ import { type DevLogLevel, isDevLogLevel } from "./dev-feed.ts"
12
+ import { admitInlineScript } from "./internal/dev-csp.ts"
13
+
14
+ export interface DevClientConfig {
15
+ /** Where the batches go (same origin). */
16
+ readonly ingestPath: string
17
+ /** Proves a batch came from a page this server served. */
18
+ readonly pageToken: string
19
+ /** The request that rendered this document: browser entries from it carry the same id. */
20
+ readonly requestId?: string | undefined
21
+ /** The document's path + search, so a client-side navigation away stops claiming that request. */
22
+ readonly documentPath?: string | undefined
23
+ /** The issues indicator module, loaded on the first error the server answers for; none when off. */
24
+ readonly indicatorPath?: string | undefined
25
+ }
26
+
27
+ // ES2017, no dependencies, one global: `__nifraDevClient`, the idempotence flag that also queues issues
28
+ // for the indicator module. Keep it small: it ships in every dev page, and its hash goes into every CSP
29
+ // that needs one.
30
+ const CLIENT_SOURCE = String.raw`(function(C){
31
+ if(window.__nifraDevClient)return;var S=window.__nifraDevClient={issues:[]};
32
+ var nonce=document.currentScript&&document.currentScript.nonce,loading=0;
33
+ var q=[],timer=0,inside=0,MSG=8000,STACK=16000;
34
+ function cap(s,n){s=String(s);return s.length>n?s.slice(0,n)+"...":s}
35
+ function where(){return location.pathname+location.search}
36
+ function ser(v,d){
37
+ if(typeof v==="string")return v;
38
+ if(v instanceof Error)return v.stack||v.name+": "+v.message;
39
+ if(typeof v==="function")return"[Function "+(v.name||"anonymous")+"]";
40
+ if(typeof v!=="object"||v===null)return String(v);
41
+ if(typeof Element!=="undefined"&&v instanceof Element)return"<"+v.tagName.toLowerCase()+(v.id?"#"+v.id:"")+">";
42
+ if(d>2)return Array.isArray(v)?"[Array]":"[Object]";
43
+ var seen=[];
44
+ try{return JSON.stringify(v,function(k,x){
45
+ if(typeof x==="object"&&x!==null){if(seen.indexOf(x)!==-1)return"[Circular]";seen.push(x)}
46
+ if(typeof x==="bigint")return x+"n";
47
+ if(x instanceof Error)return x.name+": "+x.message;
48
+ return x})}catch(e){return Object.prototype.toString.call(v)}}
49
+ function fmt(a){
50
+ var args=Array.prototype.slice.call(a),out=[];
51
+ if(typeof args[0]==="string"&&args[0].indexOf("%")!==-1){
52
+ var rest=args.slice(1);
53
+ out.push(args[0].replace(/%([sdifoOjc%])/g,function(m,k){
54
+ if(k==="%")return"%";
55
+ if(!rest.length)return m;
56
+ var v=rest.shift();
57
+ if(k==="c")return"";
58
+ if(k==="d"||k==="i")return String(parseInt(v,10));
59
+ if(k==="f")return String(parseFloat(v));
60
+ return ser(v,0)}));
61
+ args=rest}
62
+ for(var i=0;i<args.length;i++)out.push(ser(args[i],0));
63
+ return out.join(" ")}
64
+ function send(keep){
65
+ clearTimeout(timer);timer=0;
66
+ while(q.length){
67
+ var body=JSON.stringify({token:C.t,events:q.splice(0,50)});
68
+ try{fetch(C.u,{method:"POST",body:body,keepalive:!!keep&&body.length<60000,headers:{"content-type":"application/json"},credentials:"same-origin"}).then(function(r){if(r.status===200&&C.i)return r.json().then(issues)}).catch(function(){})}catch(e){}}}
69
+ function issues(j){
70
+ var list=j&&j.issues;if(!list||!list.length)return;
71
+ if(S.indicator){S.indicator.add(list);return}
72
+ S.issues=S.issues.concat(list);if(loading)return;loading=1;
73
+ var s=document.createElement("script");s.type="module";s.src=C.i;if(nonce)s.nonce=nonce;
74
+ (document.head||document.documentElement).appendChild(s)}
75
+ function push(e){
76
+ e.page=cap(where(),2000);
77
+ if(C.r&&e.page===C.p)e.requestId=C.r;
78
+ q.push(e);
79
+ if(q.length>=50)send(false);else if(!timer)timer=setTimeout(send,100)}
80
+ ["debug","info","log","warn","error"].forEach(function(level){
81
+ var orig=console[level];if(typeof orig!=="function")return;
82
+ console[level]=function(){
83
+ if(!inside){inside=1;try{
84
+ var ev={kind:"console",level:level,message:cap(fmt(arguments),MSG)};
85
+ if(level==="error")for(var i=0;i<arguments.length;i++){var x=arguments[i];
86
+ if(x instanceof Error){ev.error={name:cap(x.name,200),message:cap(x.message,MSG),stack:cap(x.stack||"",STACK)};break}}
87
+ push(ev)}catch(e){}inside=0}
88
+ return orig.apply(this,arguments)}});
89
+ addEventListener("error",function(ev){
90
+ var t=ev.target;
91
+ if(t&&t!==window&&t.tagName){
92
+ var tag=t.tagName.toLowerCase(),rel=(t.rel||"").toLowerCase();
93
+ if(tag==="script"||(tag==="link"&&/stylesheet|modulepreload|preload/.test(rel)))push({kind:"resource",tag:tag,url:cap(t.src||t.href||"",2000)});
94
+ return}
95
+ var e=ev.error;
96
+ if(!e&&ev.message==="Script error.")return;
97
+ push({kind:"error",name:cap(e&&e.name?e.name:"Error",200),message:cap(e&&e.message!=null?e.message:ev.message,MSG),
98
+ stack:cap(e&&e.stack?e.stack:(ev.filename?" at "+ev.filename+":"+ev.lineno+":"+ev.colno:""),STACK)})},true);
99
+ addEventListener("unhandledrejection",function(ev){
100
+ var r=ev.reason;
101
+ if(r instanceof Error)push({kind:"rejection",name:cap(r.name,200),message:cap(r.message,MSG),stack:cap(r.stack||"",STACK)});
102
+ else push({kind:"rejection",name:"UnhandledRejection",message:cap(ser(r,0),MSG)})});
103
+ addEventListener("pagehide",function(){send(true)});
104
+ document.addEventListener("visibilitychange",function(){if(document.visibilityState==="hidden")send(true)});
105
+ })`
106
+
107
+ // `</script` inside a JSON value would end the element early; `<!--` can start a script-data escape.
108
+ const scriptSafe = (json: string): string =>
109
+ json
110
+ .replaceAll("<", "\\u003c")
111
+ .replaceAll(">", "\\u003e")
112
+ .replaceAll("\u2028", "\\u2028")
113
+ .replaceAll("\u2029", "\\u2029")
114
+
115
+ /** The script's text (without the `<script>` element). */
116
+ export function devClientSource(config: DevClientConfig): string {
117
+ const settings: Record<string, string> = { u: config.ingestPath, t: config.pageToken }
118
+ if (config.indicatorPath !== undefined) settings.i = config.indicatorPath
119
+ if (config.requestId !== undefined && config.documentPath !== undefined) {
120
+ settings.r = config.requestId
121
+ settings.p = config.documentPath
122
+ }
123
+ return `${CLIENT_SOURCE}(${scriptSafe(JSON.stringify(settings))})`
124
+ }
125
+
126
+ /** The CSP source that admits exactly `source`. */
127
+ export function scriptHash(source: string): string {
128
+ return `'sha256-${createHash("sha256").update(source).digest("base64")}'`
129
+ }
130
+
131
+ /** Whether a response is a page the script belongs in: HTML, with a body nobody has encoded yet. */
132
+ export function isInjectablePage(response: Response, method: string): boolean {
133
+ if (method === "HEAD" || response.body === null) return false
134
+ if (response.status === 204 || response.status === 304) return false
135
+ if (response.headers.has("content-encoding")) return false
136
+ return (response.headers.get("content-type") ?? "").toLowerCase().includes("text/html")
137
+ }
138
+
139
+ /**
140
+ * Prepare `headers` for the script: admit it in the page's CSP, by hash unless `nonced` (the caller
141
+ * puts a nonce on every script afterwards). Returns the `<script>` element to insert, or undefined
142
+ * when the page's CSP forbids scripts and nothing should be inserted.
143
+ */
144
+ export function devClientTag(
145
+ config: DevClientConfig,
146
+ headers: Headers,
147
+ origin: string,
148
+ nonced = false,
149
+ ): string | undefined {
150
+ const source = devClientSource(config)
151
+ const hash = nonced ? undefined : scriptHash(source)
152
+ // A nonced client hands its nonce to the module it loads, so only a hashed one needs the URL admitted.
153
+ const indicator =
154
+ nonced || config.indicatorPath === undefined ? undefined : new URL(config.indicatorPath, origin)
155
+ if (!admitInlineScript(headers, hash, new URL(config.ingestPath, origin), indicator))
156
+ return undefined
157
+ return `<script data-nifra-dev>${source}</script>`
158
+ }
159
+
160
+ // Latin-1 views keep string offsets equal to byte offsets, so a match position is a splice point.
161
+ const latin1 = new TextDecoder("latin1")
162
+ const HEAD_OPEN = /<head(?:\s[^>]*)?>/i
163
+ const META_CHARSET = /^\s*<meta\s[^>]*charset\s*=[^>]*>/i
164
+ // Past this, a page without `<head>` is a fragment: stop looking and stream it as it is.
165
+ const HEAD_SEARCH_LIMIT = 64 * 1024
166
+ // Enough to see a `<meta charset>` that immediately follows `<head>`.
167
+ const META_LOOKAHEAD = 256
168
+
169
+ /** Where the script goes in `text`: after `<head>`, and after a `<meta charset>` that leads it. */
170
+ function insertionPoint(text: string, final: boolean): number | "wait" | "none" {
171
+ const head = HEAD_OPEN.exec(text)
172
+ if (head === null) {
173
+ const body = /<body[\s>]/i.test(text)
174
+ return final || body || text.length > HEAD_SEARCH_LIMIT ? "none" : "wait"
175
+ }
176
+ const afterHead = head.index + head[0].length
177
+ const rest = text.slice(afterHead)
178
+ const meta = META_CHARSET.exec(rest)
179
+ if (meta !== null) return afterHead + meta[0].length
180
+ if (!final && rest.length < META_LOOKAHEAD && !/<(?!meta)/i.test(rest.replace(/^\s+/, "")))
181
+ return "wait"
182
+ return afterHead
183
+ }
184
+
185
+ /** `html` with `tag` inserted into its head; unchanged when there is no `<head>`. */
186
+ export function injectIntoHtml(html: string, tag: string): string {
187
+ const at = insertionPoint(html, true)
188
+ return typeof at === "number" ? html.slice(0, at) + tag + html.slice(at) : html
189
+ }
190
+
191
+ function concat(chunks: readonly Uint8Array[], length: number): Uint8Array {
192
+ const out = new Uint8Array(length)
193
+ let offset = 0
194
+ for (const chunk of chunks) {
195
+ out.set(chunk, offset)
196
+ offset += chunk.byteLength
197
+ }
198
+ return out
199
+ }
200
+
201
+ /**
202
+ * Stream `body` with `tag` inserted into its head. Only the prefix up to the insertion point is held
203
+ * back; the rest streams through untouched, so a streamed SSR page keeps its first-byte time.
204
+ */
205
+ export function injectIntoStream(
206
+ body: ReadableStream<Uint8Array>,
207
+ tag: string,
208
+ ): ReadableStream<Uint8Array> {
209
+ const insert = new TextEncoder().encode(tag)
210
+ const held: Uint8Array[] = []
211
+ let heldBytes = 0
212
+ let done = false
213
+ const release = (
214
+ controller: TransformStreamDefaultController<Uint8Array>,
215
+ final: boolean,
216
+ ): void => {
217
+ const bytes = concat(held, heldBytes)
218
+ const at = insertionPoint(latin1.decode(bytes), final)
219
+ if (at === "wait") return
220
+ held.length = 0
221
+ heldBytes = 0
222
+ done = true
223
+ if (at === "none") {
224
+ controller.enqueue(bytes)
225
+ return
226
+ }
227
+ controller.enqueue(bytes.subarray(0, at))
228
+ controller.enqueue(insert)
229
+ controller.enqueue(bytes.subarray(at))
230
+ }
231
+ return body.pipeThrough(
232
+ new TransformStream<Uint8Array, Uint8Array>({
233
+ transform(chunk, controller) {
234
+ if (done) {
235
+ controller.enqueue(chunk)
236
+ return
237
+ }
238
+ held.push(chunk)
239
+ heldBytes += chunk.byteLength
240
+ release(controller, false)
241
+ },
242
+ flush(controller) {
243
+ if (!done && heldBytes > 0) release(controller, true)
244
+ },
245
+ }),
246
+ )
247
+ }
248
+
249
+ // ---------------------------------------------------------------------------------------------------
250
+ // What the script sends
251
+ // ---------------------------------------------------------------------------------------------------
252
+
253
+ /** One batch is bounded by what `fetch(keepalive)` may carry. */
254
+ export const CLIENT_BATCH_MAX_BYTES = 64 * 1024
255
+ export const CLIENT_BATCH_MAX_EVENTS = 50
256
+
257
+ interface EventContext {
258
+ readonly page: string
259
+ readonly requestId?: string | undefined
260
+ }
261
+
262
+ export interface BrowserError {
263
+ readonly name: string
264
+ readonly message: string
265
+ readonly stack: string
266
+ }
267
+
268
+ export type ClientEvent = EventContext &
269
+ (
270
+ | {
271
+ readonly kind: "error" | "rejection"
272
+ readonly name: string
273
+ readonly message: string
274
+ readonly stack: string
275
+ }
276
+ | { readonly kind: "resource"; readonly tag: string; readonly url: string }
277
+ | {
278
+ readonly kind: "console"
279
+ readonly level: DevLogLevel
280
+ readonly message: string
281
+ /** The first Error a `console.error` call was given: frameworks report caught errors so. */
282
+ readonly error?: BrowserError | undefined
283
+ }
284
+ )
285
+
286
+ export interface ClientBatch {
287
+ readonly token: string
288
+ readonly events: readonly ClientEvent[]
289
+ }
290
+
291
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
292
+ typeof value === "object" && value !== null && !Array.isArray(value)
293
+
294
+ const text = (value: unknown, max: number): string | undefined =>
295
+ typeof value === "string" ? value.slice(0, max) : undefined
296
+
297
+ // The script only ever tags an event with the id the server put in it; anything else is dropped.
298
+ const REQUEST_ID = /^r\d{1,12}$/
299
+
300
+ function parseBrowserError(value: unknown): BrowserError | undefined {
301
+ if (!isRecord(value)) return undefined
302
+ const message = text(value.message, 8 * 1024)
303
+ if (message === undefined) return undefined
304
+ return {
305
+ name: text(value.name, 200) || "Error",
306
+ message,
307
+ stack: text(value.stack, 16 * 1024) ?? "",
308
+ }
309
+ }
310
+
311
+ function parseEvent(value: unknown): ClientEvent | undefined {
312
+ if (!isRecord(value)) return undefined
313
+ const page = text(value.page, 2048)
314
+ if (page === undefined || !page.startsWith("/")) return undefined
315
+ const requestId =
316
+ typeof value.requestId === "string" && REQUEST_ID.test(value.requestId)
317
+ ? value.requestId
318
+ : undefined
319
+ if (value.kind === "error" || value.kind === "rejection") {
320
+ const error = parseBrowserError(value)
321
+ return error === undefined ? undefined : { kind: value.kind, ...error, page, requestId }
322
+ }
323
+ if (value.kind === "resource") {
324
+ const tag = text(value.tag, 20)
325
+ const url = text(value.url, 2048)
326
+ if (tag === undefined || url === undefined) return undefined
327
+ return { kind: "resource", tag, url, page, requestId }
328
+ }
329
+ if (value.kind === "console") {
330
+ const message = text(value.message, 8 * 1024)
331
+ if (message === undefined || !isDevLogLevel(value.level)) return undefined
332
+ const error = parseBrowserError(value.error)
333
+ return { kind: "console", level: value.level, message, error, page, requestId }
334
+ }
335
+ return undefined
336
+ }
337
+
338
+ /** A batch the script could have sent, or undefined. Malformed events are dropped, not fatal. */
339
+ export function parseClientBatch(value: unknown): ClientBatch | undefined {
340
+ if (!isRecord(value) || typeof value.token !== "string" || !Array.isArray(value.events))
341
+ return undefined
342
+ if (value.events.length > CLIENT_BATCH_MAX_EVENTS) return undefined
343
+ const events: ClientEvent[] = []
344
+ for (const raw of value.events) {
345
+ const event = parseEvent(raw)
346
+ if (event !== undefined) events.push(event)
347
+ }
348
+ return { token: value.token, events }
349
+ }
@@ -8,8 +8,34 @@
8
8
  *
9
9
  * Vite once shipped without the `/__nifra/last-error` endpoint at all - the drift this module prevents.
10
10
  */
11
+ import { relative } from "node:path"
11
12
  import { renderDiagnosticOverlay } from "./dev-error.ts"
12
- import { buildDiagnostic, type Diagnostic, LAST_ERROR_PATH } from "./diagnostic.ts"
13
+ import { buildDiagnostic, type Diagnostic, fixPrompts, LAST_ERROR_PATH } from "./diagnostic.ts"
14
+ import { browserDenial, createZoneClassifier } from "./zones.ts"
15
+
16
+ const CODEFRAME_SOURCE = /\.(?:[cm]?[jt]sx?|vue|svelte|astro|mdx)$/i
17
+
18
+ /**
19
+ * Whether a codeframe may show `file` (the canonical path it would read) to a browser: source in a
20
+ * browser zone. A page reports its own stack frames, so it can name any path: data files and the dev
21
+ * server's own `.nifra/` files (the discovery record holds the agent token) never qualify.
22
+ */
23
+ export function createSourceGate(root: string): (file: string) => boolean {
24
+ const zones = createZoneClassifier({ appRoot: root })
25
+ return (file) => {
26
+ if (!CODEFRAME_SOURCE.test(file)) return false
27
+ if ((relative(zones.appRoot, file).split(/[\\/]/)[0] ?? "").startsWith(".nifra")) return false
28
+ return browserDenial(zones.classify(file)) === undefined
29
+ }
30
+ }
31
+
32
+ /** A failure as the dev feed recorded it: the entry and request let its agent prompt name a check. */
33
+ export interface CapturedFailure {
34
+ readonly diagnostic: Diagnostic
35
+ readonly entry?: { readonly id: string; readonly seq: number } | undefined
36
+ readonly requestId?: string | undefined
37
+ readonly category?: string | undefined
38
+ }
13
39
 
14
40
  export interface DevDiagnostics {
15
41
  /** True when a request path targets the structured last-error endpoint. */
@@ -20,12 +46,36 @@ export interface DevDiagnostics {
20
46
  /** Capture a thrown SSR failure: store it for the endpoint and return the overlay HTML. The overlay a
21
47
  * person sees and the JSON an agent reads come from this one Diagnostic, so they can never disagree. */
22
48
  capture(err: unknown, request: { readonly method: string; readonly url: string }): string
49
+ /** The overlay HTML for a failure the feed already recorded, which also becomes the last error. */
50
+ show(captured: CapturedFailure): string
23
51
  }
24
52
 
25
53
  /** One diagnostics surface per dev server. `root` is the resolved project root; it scopes the codeframe
26
- * to the project (see buildDiagnostic). Both dev servers resolve a concrete root before calling this. */
27
- export function createDevDiagnostics(root: string): DevDiagnostics {
54
+ * to the project (see buildDiagnostic). Both dev servers resolve a concrete root before calling this.
55
+ * `build` replaces the default Diagnostic builder: the dev session passes one that also records the
56
+ * failure in its feed (redacted), so the overlay, this endpoint and the feed show the same object. */
57
+ export function createDevDiagnostics(
58
+ root: string,
59
+ build?: (
60
+ err: unknown,
61
+ request: { readonly method: string; readonly url: string },
62
+ ) => CapturedFailure,
63
+ ): DevDiagnostics {
28
64
  let last: Diagnostic | undefined
65
+ const showSource = createSourceGate(root)
66
+ const show = (captured: CapturedFailure): string => {
67
+ last = captured.diagnostic
68
+ return renderDiagnosticOverlay(
69
+ last,
70
+ fixPrompts(last, {
71
+ surface: "overlay",
72
+ root,
73
+ entry: captured.entry,
74
+ requestId: captured.requestId,
75
+ category: captured.category ?? "ssr",
76
+ }),
77
+ )
78
+ }
29
79
  return {
30
80
  isLastErrorPath: (pathname) => pathname === LAST_ERROR_PATH,
31
81
  lastError: () => ({
@@ -39,9 +89,12 @@ export function createDevDiagnostics(root: string): DevDiagnostics {
39
89
  "x-nifra-diagnostic": "true",
40
90
  },
41
91
  }),
42
- capture: (err, request) => {
43
- last = buildDiagnostic(err, { root, request })
44
- return renderDiagnosticOverlay(last)
45
- },
92
+ capture: (err, request) =>
93
+ show(
94
+ build?.(err, request) ?? {
95
+ diagnostic: buildDiagnostic(err, { root, request, showSource }),
96
+ },
97
+ ),
98
+ show,
46
99
  }
47
100
  }
package/src/dev-error.ts CHANGED
@@ -8,7 +8,8 @@
8
8
  * The overlay and the agent surfaces (`/__nifra/last-error`, `nifra_explain`) render from the SAME
9
9
  * `Diagnostic`, so a person and an agent see the identical failure, one as HTML and one as JSON.
10
10
  */
11
- import type { Diagnostic } from "./diagnostic.ts"
11
+ import { createHash } from "node:crypto"
12
+ import type { Diagnostic, FixPrompt } from "./diagnostic.ts"
12
13
 
13
14
  const esc = (s: string): string =>
14
15
  s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;")
@@ -32,15 +33,48 @@ function fixHtml(diagnostic: Diagnostic): string {
32
33
  if (diagnostic.fix === undefined) return ""
33
34
  const anchor =
34
35
  diagnostic.docsAnchor !== undefined
35
- ? `<div class="fix-docs">docs: <code>${esc(diagnostic.docsAnchor)}</code></div>`
36
+ ? `<div class="fix-docs">docs: <a href="https://nifra.dev/docs/${esc(diagnostic.docsAnchor)}" target="_blank" rel="noreferrer"><code>${esc(diagnostic.docsAnchor)}</code></a></div>`
36
37
  : ""
37
38
  const cause =
38
39
  diagnostic.cause !== undefined ? `<p class="fix-cause">${esc(diagnostic.cause)}</p>` : ""
39
40
  return `<div class="fix"><div class="fix-tag">likely fix</div>${cause}<p class="fix-do">${esc(diagnostic.fix)}</p>${anchor}</div>`
40
41
  }
41
42
 
42
- /** Render the overlay HTML from a prebuilt Diagnostic (the same object the agent surfaces serve). */
43
- export function renderDiagnosticOverlay(diagnostic: Diagnostic): string {
43
+ // Copies a prompt; falls back to selecting it in its textarea where the Clipboard API is unavailable
44
+ // (a dev server reached over plain http from another machine is not a secure context).
45
+ const COPY_SCRIPT = `(function(){var s=document.getElementById("nifra-copied");document.querySelectorAll("button[data-prompt]").forEach(function(b){b.addEventListener("click",function(){var t=document.getElementById("nifra-prompt-"+b.getAttribute("data-prompt"));if(!t)return;var d=t.closest("details");function done(ok){s.textContent=ok?"Copied. Paste it into your coding agent.":"Select the text below and copy it."}function manual(){var was=d.open;d.open=true;t.focus();t.select();var ok=false;try{ok=document.execCommand("copy")}catch(e){}if(ok)d.open=was;done(ok)}if(window.isSecureContext&&navigator.clipboard)navigator.clipboard.writeText(t.value).then(function(){done(true)},manual);else manual()})})})()`
46
+
47
+ const COPY_SCRIPT_HASH = `'sha256-${createHash("sha256").update(COPY_SCRIPT).digest("base64")}'`
48
+
49
+ // The overlay renders text a request can influence (its URL, a thrown message), so it allows only its
50
+ // own copy script.
51
+ const OVERLAY_CSP = `default-src 'none'; script-src ${COPY_SCRIPT_HASH}; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'`
52
+
53
+ /** The copy-a-prompt block: one button per fix, the prompts themselves readable without JS. */
54
+ function promptsHtml(prompts: readonly FixPrompt[] | undefined): string {
55
+ if (prompts === undefined || prompts.length === 0) return ""
56
+ const single = prompts.length === 1
57
+ const buttons = prompts
58
+ .map(
59
+ (p, i) =>
60
+ `<button type="button" data-prompt="${i}">${single ? "Copy prompt" : `Copy prompt: ${esc(p.label)}`}</button>`,
61
+ )
62
+ .join("")
63
+ const areas = prompts
64
+ .map(
65
+ (p, i) =>
66
+ `<label for="nifra-prompt-${i}">${esc(p.label)}</label><textarea id="nifra-prompt-${i}" readonly rows="12">${esc(p.prompt)}</textarea>`,
67
+ )
68
+ .join("")
69
+ return `<section class="prompts"><h1>Prompt for your coding agent</h1><p class="prompts-note">The error, the code and ${single ? "the fix" : "one fix"}, ready to paste. Paths are relative to the project and known secrets are redacted.</p><div class="prompt-buttons">${buttons}<span id="nifra-copied" aria-live="polite"></span></div><details><summary>Show the prompt${single ? "" : "s"}</summary>${areas}</details></section><script>${COPY_SCRIPT}</script>`
70
+ }
71
+
72
+ /** Render the overlay HTML from a prebuilt Diagnostic (the same object the agent surfaces serve), with
73
+ * the paste-ready agent prompts for it when the caller built them. */
74
+ export function renderDiagnosticOverlay(
75
+ diagnostic: Diagnostic,
76
+ prompts?: readonly FixPrompt[],
77
+ ): string {
44
78
  const name = esc(diagnostic.name || "Error")
45
79
  const code = esc(diagnostic.code)
46
80
  const reqLine =
@@ -51,7 +85,7 @@ export function renderDiagnosticOverlay(diagnostic: Diagnostic): string {
51
85
  diagnostic.frames.length > 0
52
86
  ? `<ol class="frames">${diagnostic.frames.map((f) => `<li>${esc(f.raw)}</li>`).join("")}</ol>`
53
87
  : `<p class="no-frames">No stack frames.</p>`
54
- return `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${name} - nifra dev</title>
88
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="${OVERLAY_CSP}"><title>${name} - nifra dev</title>
55
89
  <style>
56
90
  :root { color-scheme: dark }
57
91
  body { margin: 0; background: #0b0d10; color: #e6e6e6; font: 14px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }
@@ -80,11 +114,21 @@ export function renderDiagnosticOverlay(diagnostic: Diagnostic): string {
80
114
  .frames li:first-child { color: #d6e2ee; }
81
115
  .no-frames { color: #778; }
82
116
  footer { margin-top: 28px; color: #667; font-size: 12px; }
117
+ .prompts-note { margin: 0 0 10px; color: #9aa; }
118
+ .prompt-buttons { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
119
+ .prompt-buttons button { font: inherit; font-size: 13px; color: #d6f2e4; background: #10241a; border: 1px solid #1f4d38; border-radius: 6px; padding: 6px 12px; cursor: pointer; }
120
+ .prompt-buttons button:hover, .prompt-buttons button:focus-visible { border-color: #7fe0af; outline: none; }
121
+ #nifra-copied { color: #7fe0af; font-size: 12px; }
122
+ details { margin-top: 10px; }
123
+ summary { cursor: pointer; color: #9aa; }
124
+ details label { display: block; margin: 12px 0 4px; color: #c98; font-size: 12px; }
125
+ textarea { width: 100%; box-sizing: border-box; background: #0f1319; color: #cdd6e0; border: 1px solid #222; border-radius: 8px; font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; padding: 8px; }
83
126
  </style></head><body>
84
127
  <div class="bar"><span class="tag">nifra dev</span><span class="code">${code}</span><span>${name}</span>${reqLine}</div>
85
128
  <main>
86
129
  <p class="message">${esc(diagnostic.message)}</p>
87
130
  ${fixHtml(diagnostic)}
131
+ ${promptsHtml(prompts)}
88
132
  ${codeframeHtml(diagnostic)}
89
133
  <h1>Stack</h1>
90
134
  ${framesHtml}