@nifrajs/web 5.5.1 → 5.7.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 (481) hide show
  1. package/CHANGELOG.md +243 -0
  2. package/README.md +21 -11
  3. package/dist/boundary.d.ts +67 -7
  4. package/dist/boundary.d.ts.map +1 -1
  5. package/dist/boundary.js +6 -4
  6. package/dist/boundary.js.map +1 -1
  7. package/dist/build-plan.d.ts +3 -0
  8. package/dist/build-plan.d.ts.map +1 -1
  9. package/dist/build-plan.js.map +1 -1
  10. package/dist/build-vite.d.ts.map +1 -1
  11. package/dist/build-vite.js +2 -0
  12. package/dist/build-vite.js.map +1 -1
  13. package/dist/build.d.ts +9 -2
  14. package/dist/build.d.ts.map +1 -1
  15. package/dist/build.js +10 -0
  16. package/dist/build.js.map +1 -1
  17. package/dist/client.d.ts +3 -1
  18. package/dist/client.d.ts.map +1 -1
  19. package/dist/client.js +174 -23
  20. package/dist/client.js.map +1 -1
  21. package/dist/csp.d.ts +8 -0
  22. package/dist/csp.d.ts.map +1 -1
  23. package/dist/csp.js +11 -2
  24. package/dist/csp.js.map +1 -1
  25. package/dist/deferred.d.ts +6 -4
  26. package/dist/deferred.d.ts.map +1 -1
  27. package/dist/deferred.js +10 -6
  28. package/dist/deferred.js.map +1 -1
  29. package/dist/dev-feed.d.ts.map +1 -1
  30. package/dist/dev-feed.js +11 -2
  31. package/dist/dev-feed.js.map +1 -1
  32. package/dist/dev-session.d.ts.map +1 -1
  33. package/dist/dev-session.js +13 -6
  34. package/dist/dev-session.js.map +1 -1
  35. package/dist/dev.d.ts +4 -1
  36. package/dist/dev.d.ts.map +1 -1
  37. package/dist/dev.js +12 -5
  38. package/dist/dev.js.map +1 -1
  39. package/dist/diagnostic-catalog.d.ts +4 -0
  40. package/dist/diagnostic-catalog.d.ts.map +1 -1
  41. package/dist/diagnostic-catalog.js +48 -0
  42. package/dist/diagnostic-catalog.js.map +1 -1
  43. package/dist/diagnostic.d.ts +1 -1
  44. package/dist/diagnostic.d.ts.map +1 -1
  45. package/dist/diagnostic.js +1 -1
  46. package/dist/diagnostic.js.map +1 -1
  47. package/dist/fn.d.ts +5 -3
  48. package/dist/fn.d.ts.map +1 -1
  49. package/dist/fn.js +20 -5
  50. package/dist/fn.js.map +1 -1
  51. package/dist/fs.d.ts.map +1 -1
  52. package/dist/fs.js +13 -3
  53. package/dist/fs.js.map +1 -1
  54. package/dist/identity.d.ts +22 -0
  55. package/dist/identity.d.ts.map +1 -0
  56. package/dist/identity.js +60 -0
  57. package/dist/identity.js.map +1 -0
  58. package/dist/index.d.ts +8 -6
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +6 -3
  61. package/dist/index.js.map +1 -1
  62. package/dist/internal/cdn/cloudflare.d.ts +2 -1
  63. package/dist/internal/cdn/cloudflare.d.ts.map +1 -1
  64. package/dist/internal/cdn/cloudflare.js +2 -1
  65. package/dist/internal/cdn/cloudflare.js.map +1 -1
  66. package/dist/internal/certified-components.d.ts +1 -1
  67. package/dist/internal/certified-components.js +1 -1
  68. package/dist/internal/codegen.d.ts +11 -10
  69. package/dist/internal/codegen.d.ts.map +1 -1
  70. package/dist/internal/codegen.js +73 -10
  71. package/dist/internal/codegen.js.map +1 -1
  72. package/dist/internal/dev-websocket.d.ts +73 -0
  73. package/dist/internal/dev-websocket.d.ts.map +1 -0
  74. package/dist/internal/dev-websocket.js +145 -0
  75. package/dist/internal/dev-websocket.js.map +1 -0
  76. package/dist/internal/hole-runtime.d.ts +66 -0
  77. package/dist/internal/hole-runtime.d.ts.map +1 -0
  78. package/dist/internal/hole-runtime.js +211 -0
  79. package/dist/internal/hole-runtime.js.map +1 -0
  80. package/dist/internal/identity-epoch.d.ts +50 -0
  81. package/dist/internal/identity-epoch.d.ts.map +1 -0
  82. package/dist/internal/identity-epoch.js +180 -0
  83. package/dist/internal/identity-epoch.js.map +1 -0
  84. package/dist/internal/invalidation-contract.d.ts +23 -0
  85. package/dist/internal/invalidation-contract.d.ts.map +1 -0
  86. package/dist/internal/invalidation-contract.js +11 -0
  87. package/dist/internal/invalidation-contract.js.map +1 -0
  88. package/dist/internal/invalidation.d.ts +62 -0
  89. package/dist/internal/invalidation.d.ts.map +1 -0
  90. package/dist/internal/invalidation.js +149 -0
  91. package/dist/internal/invalidation.js.map +1 -0
  92. package/dist/internal/island-bindings.d.ts +51 -0
  93. package/dist/internal/island-bindings.d.ts.map +1 -0
  94. package/dist/internal/island-bindings.js +147 -0
  95. package/dist/internal/island-bindings.js.map +1 -0
  96. package/dist/internal/island-list.d.ts +26 -0
  97. package/dist/internal/island-list.d.ts.map +1 -0
  98. package/dist/internal/island-list.js +78 -0
  99. package/dist/internal/island-list.js.map +1 -0
  100. package/dist/internal/island-mount.d.ts +4 -0
  101. package/dist/internal/island-mount.d.ts.map +1 -0
  102. package/dist/internal/island-mount.js +66 -0
  103. package/dist/internal/island-mount.js.map +1 -0
  104. package/dist/internal/island-owner.d.ts +5 -0
  105. package/dist/internal/island-owner.d.ts.map +1 -0
  106. package/dist/internal/island-owner.js +20 -0
  107. package/dist/internal/island-owner.js.map +1 -0
  108. package/dist/internal/island-resource.d.ts +30 -0
  109. package/dist/internal/island-resource.d.ts.map +1 -0
  110. package/dist/internal/island-resource.js +57 -0
  111. package/dist/internal/island-resource.js.map +1 -0
  112. package/dist/internal/island-signals.d.ts +51 -0
  113. package/dist/internal/island-signals.d.ts.map +1 -0
  114. package/dist/internal/island-signals.js +220 -0
  115. package/dist/internal/island-signals.js.map +1 -0
  116. package/dist/internal/keep-alive-table.d.ts +23 -0
  117. package/dist/internal/keep-alive-table.d.ts.map +1 -0
  118. package/dist/internal/keep-alive-table.js +53 -0
  119. package/dist/internal/keep-alive-table.js.map +1 -0
  120. package/dist/internal/live-contract.d.ts +101 -0
  121. package/dist/internal/live-contract.d.ts.map +1 -0
  122. package/dist/internal/live-contract.js +5 -0
  123. package/dist/internal/live-contract.js.map +1 -0
  124. package/dist/internal/live-queries.d.ts +22 -0
  125. package/dist/internal/live-queries.d.ts.map +1 -0
  126. package/dist/internal/live-queries.js +54 -0
  127. package/dist/internal/live-queries.js.map +1 -0
  128. package/dist/internal/page-events.d.ts +10 -0
  129. package/dist/internal/page-events.d.ts.map +1 -0
  130. package/dist/internal/page-events.js +26 -0
  131. package/dist/internal/page-events.js.map +1 -0
  132. package/dist/internal/page-execution.d.ts +16 -1
  133. package/dist/internal/page-execution.d.ts.map +1 -1
  134. package/dist/internal/page-execution.js +432 -31
  135. package/dist/internal/page-execution.js.map +1 -1
  136. package/dist/internal/parity.d.ts +3 -0
  137. package/dist/internal/parity.d.ts.map +1 -1
  138. package/dist/internal/parity.js +28 -5
  139. package/dist/internal/parity.js.map +1 -1
  140. package/dist/internal/ppr/admission.d.ts +83 -0
  141. package/dist/internal/ppr/admission.d.ts.map +1 -0
  142. package/dist/internal/ppr/admission.js +221 -0
  143. package/dist/internal/ppr/admission.js.map +1 -0
  144. package/dist/internal/ppr/canary.d.ts +24 -0
  145. package/dist/internal/ppr/canary.d.ts.map +1 -0
  146. package/dist/internal/ppr/canary.js +102 -0
  147. package/dist/internal/ppr/canary.js.map +1 -0
  148. package/dist/internal/ppr/compile-shell.d.ts +39 -0
  149. package/dist/internal/ppr/compile-shell.d.ts.map +1 -0
  150. package/dist/internal/ppr/compile-shell.js +298 -0
  151. package/dist/internal/ppr/compile-shell.js.map +1 -0
  152. package/dist/internal/ppr/contract.d.ts +177 -0
  153. package/dist/internal/ppr/contract.d.ts.map +1 -0
  154. package/dist/internal/ppr/contract.js +80 -0
  155. package/dist/internal/ppr/contract.js.map +1 -0
  156. package/dist/internal/ppr/frames.d.ts +71 -0
  157. package/dist/internal/ppr/frames.d.ts.map +1 -0
  158. package/dist/internal/ppr/frames.js +119 -0
  159. package/dist/internal/ppr/frames.js.map +1 -0
  160. package/dist/internal/ppr/integration.d.ts +19 -0
  161. package/dist/internal/ppr/integration.d.ts.map +1 -0
  162. package/dist/internal/ppr/integration.js +106 -0
  163. package/dist/internal/ppr/integration.js.map +1 -0
  164. package/dist/internal/ppr/options.d.ts +96 -0
  165. package/dist/internal/ppr/options.d.ts.map +1 -0
  166. package/dist/internal/ppr/options.js +125 -0
  167. package/dist/internal/ppr/options.js.map +1 -0
  168. package/dist/internal/ppr/route-rules.d.ts +12 -0
  169. package/dist/internal/ppr/route-rules.d.ts.map +1 -0
  170. package/dist/internal/ppr/route-rules.js +53 -0
  171. package/dist/internal/ppr/route-rules.js.map +1 -0
  172. package/dist/internal/ppr/runtime.d.ts +29 -0
  173. package/dist/internal/ppr/runtime.d.ts.map +1 -0
  174. package/dist/internal/ppr/runtime.js +35 -0
  175. package/dist/internal/ppr/runtime.js.map +1 -0
  176. package/dist/internal/ppr/server.d.ts +29 -0
  177. package/dist/internal/ppr/server.d.ts.map +1 -0
  178. package/dist/internal/ppr/server.js +1409 -0
  179. package/dist/internal/ppr/server.js.map +1 -0
  180. package/dist/internal/ppr/shell-store.d.ts +39 -0
  181. package/dist/internal/ppr/shell-store.d.ts.map +1 -0
  182. package/dist/internal/ppr/shell-store.js +106 -0
  183. package/dist/internal/ppr/shell-store.js.map +1 -0
  184. package/dist/internal/prerendered-pages.d.ts +1 -1
  185. package/dist/internal/prerendered-pages.d.ts.map +1 -1
  186. package/dist/internal/prerendered-pages.js +7 -4
  187. package/dist/internal/prerendered-pages.js.map +1 -1
  188. package/dist/internal/present.d.ts +11 -0
  189. package/dist/internal/present.d.ts.map +1 -0
  190. package/dist/internal/present.js +13 -0
  191. package/dist/internal/present.js.map +1 -0
  192. package/dist/internal/query-digest.d.ts +9 -0
  193. package/dist/internal/query-digest.d.ts.map +1 -0
  194. package/dist/internal/query-digest.js +23 -0
  195. package/dist/internal/query-digest.js.map +1 -0
  196. package/dist/internal/query-runtime.d.ts +6 -1
  197. package/dist/internal/query-runtime.d.ts.map +1 -1
  198. package/dist/internal/query-runtime.js +9 -0
  199. package/dist/internal/query-runtime.js.map +1 -1
  200. package/dist/internal/query-seam.d.ts +29 -0
  201. package/dist/internal/query-seam.d.ts.map +1 -0
  202. package/dist/internal/query-seam.js +23 -0
  203. package/dist/internal/query-seam.js.map +1 -0
  204. package/dist/internal/render-document.d.ts +34 -64
  205. package/dist/internal/render-document.d.ts.map +1 -1
  206. package/dist/internal/render-document.js +82 -64
  207. package/dist/internal/render-document.js.map +1 -1
  208. package/dist/internal/request-abort.d.ts +8 -0
  209. package/dist/internal/request-abort.d.ts.map +1 -0
  210. package/dist/internal/request-abort.js +22 -0
  211. package/dist/internal/request-abort.js.map +1 -0
  212. package/dist/internal/server-boundary.d.ts +2 -0
  213. package/dist/internal/server-boundary.d.ts.map +1 -1
  214. package/dist/internal/server-boundary.js +8 -1
  215. package/dist/internal/server-boundary.js.map +1 -1
  216. package/dist/internal/share-data.d.ts +10 -0
  217. package/dist/internal/share-data.d.ts.map +1 -0
  218. package/dist/internal/share-data.js +135 -0
  219. package/dist/internal/share-data.js.map +1 -0
  220. package/dist/internal/status-page.d.ts +68 -0
  221. package/dist/internal/status-page.d.ts.map +1 -0
  222. package/dist/internal/status-page.js +66 -0
  223. package/dist/internal/status-page.js.map +1 -0
  224. package/dist/internal/store-signature.d.ts +15 -0
  225. package/dist/internal/store-signature.d.ts.map +1 -0
  226. package/dist/internal/store-signature.js +114 -0
  227. package/dist/internal/store-signature.js.map +1 -0
  228. package/dist/internal/vite-plugin-compat.d.ts +2 -2
  229. package/dist/internal/vite-plugin-compat.js +2 -2
  230. package/dist/internal/web-app.d.ts +26 -0
  231. package/dist/internal/web-app.d.ts.map +1 -1
  232. package/dist/internal/web-app.js +79 -3
  233. package/dist/internal/web-app.js.map +1 -1
  234. package/dist/islands-swap.d.ts +41 -0
  235. package/dist/islands-swap.d.ts.map +1 -0
  236. package/dist/islands-swap.js +270 -0
  237. package/dist/islands-swap.js.map +1 -0
  238. package/dist/islands.d.ts +86 -24
  239. package/dist/islands.d.ts.map +1 -1
  240. package/dist/islands.js +138 -78
  241. package/dist/islands.js.map +1 -1
  242. package/dist/isr.d.ts +67 -13
  243. package/dist/isr.d.ts.map +1 -1
  244. package/dist/isr.js +247 -99
  245. package/dist/isr.js.map +1 -1
  246. package/dist/keep-alive.d.ts +147 -0
  247. package/dist/keep-alive.d.ts.map +1 -0
  248. package/dist/keep-alive.js +1102 -0
  249. package/dist/keep-alive.js.map +1 -0
  250. package/dist/live/bus.d.ts +70 -0
  251. package/dist/live/bus.d.ts.map +1 -0
  252. package/dist/live/bus.js +55 -0
  253. package/dist/live/bus.js.map +1 -0
  254. package/dist/live/core.d.ts +25 -0
  255. package/dist/live/core.d.ts.map +1 -0
  256. package/dist/live/core.js +29 -0
  257. package/dist/live/core.js.map +1 -0
  258. package/dist/live/history.d.ts +38 -0
  259. package/dist/live/history.d.ts.map +1 -0
  260. package/dist/live/history.js +104 -0
  261. package/dist/live/history.js.map +1 -0
  262. package/dist/live/hub-protocol.d.ts +46 -0
  263. package/dist/live/hub-protocol.d.ts.map +1 -0
  264. package/dist/live/hub-protocol.js +108 -0
  265. package/dist/live/hub-protocol.js.map +1 -0
  266. package/dist/live/hub.d.ts +185 -0
  267. package/dist/live/hub.d.ts.map +1 -0
  268. package/dist/live/hub.js +544 -0
  269. package/dist/live/hub.js.map +1 -0
  270. package/dist/live/keyring.d.ts +92 -0
  271. package/dist/live/keyring.d.ts.map +1 -0
  272. package/dist/live/keyring.js +265 -0
  273. package/dist/live/keyring.js.map +1 -0
  274. package/dist/live/read-capped.d.ts +6 -0
  275. package/dist/live/read-capped.d.ts.map +1 -0
  276. package/dist/live/read-capped.js +42 -0
  277. package/dist/live/read-capped.js.map +1 -0
  278. package/dist/live/revocations.d.ts +52 -0
  279. package/dist/live/revocations.d.ts.map +1 -0
  280. package/dist/live/revocations.js +116 -0
  281. package/dist/live/revocations.js.map +1 -0
  282. package/dist/live/settings.d.ts +14 -0
  283. package/dist/live/settings.d.ts.map +1 -0
  284. package/dist/live/settings.js +38 -0
  285. package/dist/live/settings.js.map +1 -0
  286. package/dist/live-client.d.ts +34 -0
  287. package/dist/live-client.d.ts.map +1 -0
  288. package/dist/live-client.js +878 -0
  289. package/dist/live-client.js.map +1 -0
  290. package/dist/live-hub.d.ts +130 -0
  291. package/dist/live-hub.d.ts.map +1 -0
  292. package/dist/live-hub.js +507 -0
  293. package/dist/live-hub.js.map +1 -0
  294. package/dist/live-redis.d.ts +60 -0
  295. package/dist/live-redis.d.ts.map +1 -0
  296. package/dist/live-redis.js +212 -0
  297. package/dist/live-redis.js.map +1 -0
  298. package/dist/live.d.ts +150 -0
  299. package/dist/live.d.ts.map +1 -0
  300. package/dist/live.js +972 -0
  301. package/dist/live.js.map +1 -0
  302. package/dist/manifest.d.ts +113 -2
  303. package/dist/manifest.d.ts.map +1 -1
  304. package/dist/manifest.js +109 -5
  305. package/dist/manifest.js.map +1 -1
  306. package/dist/navigation.d.ts +9 -3
  307. package/dist/navigation.d.ts.map +1 -1
  308. package/dist/navigation.js +13 -3
  309. package/dist/navigation.js.map +1 -1
  310. package/dist/plugins/vite-zone-guard.d.ts +6 -0
  311. package/dist/plugins/vite-zone-guard.d.ts.map +1 -1
  312. package/dist/plugins/vite-zone-guard.js +8 -2
  313. package/dist/plugins/vite-zone-guard.js.map +1 -1
  314. package/dist/ppr.d.ts +73 -0
  315. package/dist/ppr.d.ts.map +1 -0
  316. package/dist/ppr.js +76 -0
  317. package/dist/ppr.js.map +1 -0
  318. package/dist/query.d.ts +8 -13
  319. package/dist/query.d.ts.map +1 -1
  320. package/dist/query.js +112 -18
  321. package/dist/query.js.map +1 -1
  322. package/dist/render-seam.d.ts +32 -0
  323. package/dist/render-seam.d.ts.map +1 -1
  324. package/dist/render-seam.js +7 -0
  325. package/dist/render-seam.js.map +1 -1
  326. package/dist/route-types.d.ts.map +1 -1
  327. package/dist/route-types.js +2 -2
  328. package/dist/route-types.js.map +1 -1
  329. package/dist/router.d.ts +72 -4
  330. package/dist/router.d.ts.map +1 -1
  331. package/dist/router.js +416 -129
  332. package/dist/router.js.map +1 -1
  333. package/dist/server-components/build.d.ts.map +1 -1
  334. package/dist/server-components/build.js +3 -3
  335. package/dist/server-components/build.js.map +1 -1
  336. package/dist/server-components/client.d.ts +23 -1
  337. package/dist/server-components/client.d.ts.map +1 -1
  338. package/dist/server-components/client.js +92 -5
  339. package/dist/server-components/client.js.map +1 -1
  340. package/dist/server-components/definition.d.ts.map +1 -1
  341. package/dist/server-components/definition.js +2 -1
  342. package/dist/server-components/definition.js.map +1 -1
  343. package/dist/server-components/hole.d.ts +8 -0
  344. package/dist/server-components/hole.d.ts.map +1 -0
  345. package/dist/server-components/hole.js +20 -0
  346. package/dist/server-components/hole.js.map +1 -0
  347. package/dist/server-components/html-policy.d.ts.map +1 -1
  348. package/dist/server-components/html-policy.js +62 -4
  349. package/dist/server-components/html-policy.js.map +1 -1
  350. package/dist/server-components/html-scan.d.ts +17 -0
  351. package/dist/server-components/html-scan.d.ts.map +1 -0
  352. package/dist/server-components/html-scan.js +180 -0
  353. package/dist/server-components/html-scan.js.map +1 -0
  354. package/dist/server-components/html.d.ts.map +1 -1
  355. package/dist/server-components/html.js +19 -4
  356. package/dist/server-components/html.js.map +1 -1
  357. package/dist/server-components/router-integration.d.ts +4 -1
  358. package/dist/server-components/router-integration.d.ts.map +1 -1
  359. package/dist/server-components/router-integration.js +23 -2
  360. package/dist/server-components/router-integration.js.map +1 -1
  361. package/dist/server-components/runtime.d.ts +6 -1
  362. package/dist/server-components/runtime.d.ts.map +1 -1
  363. package/dist/server-components/runtime.js +88 -29
  364. package/dist/server-components/runtime.js.map +1 -1
  365. package/dist/server-components/state.d.ts +3 -1
  366. package/dist/server-components/state.d.ts.map +1 -1
  367. package/dist/server-components/state.js +50 -38
  368. package/dist/server-components/state.js.map +1 -1
  369. package/dist/server-components/types.d.ts +9 -0
  370. package/dist/server-components/types.d.ts.map +1 -1
  371. package/dist/server-components/vite.d.ts.map +1 -1
  372. package/dist/server-components/vite.js +26 -9
  373. package/dist/server-components/vite.js.map +1 -1
  374. package/dist/vite.d.ts +5 -1
  375. package/dist/vite.d.ts.map +1 -1
  376. package/dist/vite.js +58 -4
  377. package/dist/vite.js.map +1 -1
  378. package/package.json +48 -8
  379. package/src/boundary.ts +77 -5
  380. package/src/build-plan.ts +3 -0
  381. package/src/build-vite.ts +2 -0
  382. package/src/build.ts +20 -1
  383. package/src/client.ts +183 -23
  384. package/src/csp.ts +16 -2
  385. package/src/deferred.ts +11 -5
  386. package/src/dev-feed.ts +11 -2
  387. package/src/dev-session.ts +22 -11
  388. package/src/dev.ts +29 -8
  389. package/src/diagnostic-catalog.ts +53 -0
  390. package/src/diagnostic.ts +1 -0
  391. package/src/fn.ts +24 -7
  392. package/src/fs.ts +22 -8
  393. package/src/identity.ts +61 -0
  394. package/src/index.ts +20 -4
  395. package/src/internal/cdn/cloudflare.ts +2 -1
  396. package/src/internal/certified-components.ts +1 -1
  397. package/src/internal/codegen.ts +95 -9
  398. package/src/internal/dev-websocket.ts +230 -0
  399. package/src/internal/hole-runtime.ts +286 -0
  400. package/src/internal/identity-epoch.ts +239 -0
  401. package/src/internal/invalidation-contract.ts +35 -0
  402. package/src/internal/invalidation.ts +213 -0
  403. package/src/internal/island-bindings.ts +191 -0
  404. package/src/internal/island-list.ts +92 -0
  405. package/src/internal/island-mount.ts +68 -0
  406. package/src/internal/island-owner.ts +20 -0
  407. package/src/internal/island-resource.ts +76 -0
  408. package/src/internal/island-signals.ts +249 -0
  409. package/src/internal/keep-alive-table.ts +72 -0
  410. package/src/internal/live-contract.ts +111 -0
  411. package/src/internal/live-queries.ts +60 -0
  412. package/src/internal/page-events.ts +27 -0
  413. package/src/internal/page-execution.ts +605 -45
  414. package/src/internal/parity.ts +35 -6
  415. package/src/internal/ppr/admission.ts +300 -0
  416. package/src/internal/ppr/canary.ts +122 -0
  417. package/src/internal/ppr/compile-shell.ts +379 -0
  418. package/src/internal/ppr/contract.ts +265 -0
  419. package/src/internal/ppr/frames.ts +170 -0
  420. package/src/internal/ppr/integration.ts +137 -0
  421. package/src/internal/ppr/options.ts +221 -0
  422. package/src/internal/ppr/route-rules.ts +77 -0
  423. package/src/internal/ppr/runtime.ts +39 -0
  424. package/src/internal/ppr/server.ts +1766 -0
  425. package/src/internal/ppr/shell-store.ts +152 -0
  426. package/src/internal/prerendered-pages.ts +7 -4
  427. package/src/internal/present.ts +20 -0
  428. package/src/internal/query-digest.ts +34 -0
  429. package/src/internal/query-runtime.ts +14 -1
  430. package/src/internal/query-seam.ts +48 -0
  431. package/src/internal/render-document.ts +158 -113
  432. package/src/internal/request-abort.ts +26 -0
  433. package/src/internal/server-boundary.ts +10 -1
  434. package/src/internal/share-data.ts +136 -0
  435. package/src/internal/status-page.ts +95 -0
  436. package/src/internal/store-signature.ts +149 -0
  437. package/src/internal/vite-plugin-compat.ts +2 -2
  438. package/src/internal/web-app.ts +123 -3
  439. package/src/islands-swap.ts +283 -0
  440. package/src/islands.ts +169 -72
  441. package/src/isr.ts +348 -106
  442. package/src/keep-alive.ts +1354 -0
  443. package/src/live/bus.ts +104 -0
  444. package/src/live/core.ts +52 -0
  445. package/src/live/history.ts +109 -0
  446. package/src/live/hub-protocol.ts +133 -0
  447. package/src/live/hub.ts +620 -0
  448. package/src/live/keyring.ts +384 -0
  449. package/src/live/read-capped.ts +36 -0
  450. package/src/live/revocations.ts +155 -0
  451. package/src/live/settings.ts +46 -0
  452. package/src/live-client.ts +1011 -0
  453. package/src/live-hub.ts +658 -0
  454. package/src/live-redis.ts +284 -0
  455. package/src/live.ts +1257 -0
  456. package/src/manifest.ts +274 -6
  457. package/src/navigation.ts +14 -2
  458. package/src/plugins/vite-zone-guard.ts +15 -2
  459. package/src/ppr.ts +153 -0
  460. package/src/query.ts +147 -20
  461. package/src/render-seam.ts +37 -0
  462. package/src/route-types.ts +3 -1
  463. package/src/router.ts +534 -132
  464. package/src/server-components/build.ts +9 -3
  465. package/src/server-components/client.ts +89 -5
  466. package/src/server-components/definition.ts +2 -1
  467. package/src/server-components/hole.ts +23 -0
  468. package/src/server-components/html-policy.ts +63 -4
  469. package/src/server-components/html-scan.ts +159 -0
  470. package/src/server-components/html.ts +18 -4
  471. package/src/server-components/router-integration.ts +21 -2
  472. package/src/server-components/runtime.ts +114 -31
  473. package/src/server-components/state.ts +66 -44
  474. package/src/server-components/types.ts +16 -0
  475. package/src/server-components/vite.ts +33 -14
  476. package/src/vite.ts +75 -6
  477. package/dist/nano.d.ts +0 -125
  478. package/dist/nano.d.ts.map +0 -1
  479. package/dist/nano.js +0 -190
  480. package/dist/nano.js.map +0 -1
  481. package/src/nano.ts +0 -246
@@ -0,0 +1,384 @@
1
+ /**
2
+ * The live keyring: AES-256-GCM grants under hourly per-process subkeys, and the HMAC tags and subject
3
+ * tags derived from the same secrets. Everything here is WebCrypto, so it runs unchanged on Bun, Node,
4
+ * Deno and Workers.
5
+ *
6
+ * Token: `base64url(kid || nonce || AES-256-GCM(JSON claims))`, with the 16-byte `kid` authenticated as
7
+ * associated data. `kid = secretId (4) || processId (8) || hour (4)`.
8
+ */
9
+
10
+ const TEXT = new TextEncoder()
11
+ const UTF8 = new TextDecoder("utf-8", { fatal: true })
12
+
13
+ export const MIN_SECRET_BYTES = 32
14
+ export const MAX_SECRETS = 3
15
+ /** A grant token longer than this is refused before decoding, and never issued. */
16
+ export const MAX_GRANT_CHARS = 10_240
17
+ export const MAX_KEYS_PER_FRAME = 64
18
+ /** The cache-tag charset: a letter, then up to 127 of `A-Z a-z 0-9 . _ : / -`. */
19
+ export const KEY_PATTERN = /^[A-Za-z][A-Za-z0-9._:/-]{0,127}$/
20
+ export const SID_PATTERN = /^[0-9a-f]{32}$/
21
+ /**
22
+ * The socket id of a seed: the grant a server-rendered page carries before it has a socket. No socket
23
+ * has it, so no watch accepts a seed; the load endpoint exchanges one for a grant bound to a socket.
24
+ */
25
+ export const SEED_SID = ""
26
+
27
+ const KID_BYTES = 16
28
+ const NONCE_BYTES = 12
29
+ const GCM_TAG_BYTES = 16
30
+ const HOUR_MS = 3_600_000
31
+ const HOUR_SECONDS = 3600
32
+ const IAT_SKEW_SECONDS = 60
33
+ const SUBKEY_CACHE_SIZE = 1024
34
+ const TAG_BYTES = 16
35
+ /**
36
+ * Seals under one subkey before it refuses more. Random 96-bit nonces stay safe for about 2^32
37
+ * encryptions per key; an hourly per-process subkey at 10,000 grants a second sees 3.6e7.
38
+ */
39
+ export const MAX_SEALS_PER_SUBKEY = 2 ** 31
40
+
41
+ const INFO_SECRET_ID = TEXT.encode("nifra live secret id v1")
42
+ const INFO_GRANT = TEXT.encode("nifra live grant v1")
43
+ const INFO_TAG = TEXT.encode("nifra live tag v1")
44
+ const INFO_SUBJECT = TEXT.encode("nifra live subject v1")
45
+ const NO_SALT = new Uint8Array(0)
46
+
47
+ export interface GrantOwner {
48
+ /** The keys the owner's loader declared. */
49
+ readonly k: readonly string[]
50
+ /** The bus seq read before the owner's data was loaded. */
51
+ readonly w: number
52
+ }
53
+
54
+ export interface GrantClaims {
55
+ readonly v: 1
56
+ readonly sid: string
57
+ readonly sub: string | null
58
+ readonly path: string
59
+ readonly frame: string
60
+ readonly owners: Readonly<Record<string, GrantOwner>>
61
+ /** The seq read at issuance: revocations above it refuse the grant. */
62
+ readonly s: number
63
+ /** Renewals re-signed without loaders since the last load that ran them all. */
64
+ readonly r: number
65
+ readonly iat: number
66
+ readonly exp: number
67
+ readonly aud: string
68
+ }
69
+
70
+ export type GrantInput = Omit<GrantClaims, "v" | "iat" | "exp" | "aud">
71
+
72
+ export interface OpenedGrant {
73
+ readonly claims: GrantClaims
74
+ /** Index of the secret that decrypted it; that secret also derives the grant's tags. */
75
+ readonly secret: number
76
+ }
77
+
78
+ export interface Keyring {
79
+ readonly appId: string
80
+ /** Grant lifetime in seconds. */
81
+ readonly ttl: number
82
+ readonly secretCount: number
83
+ /** Each secret's public 32-bit id, newest first: a token names its secret by it. */
84
+ readonly secretIds: readonly number[]
85
+ /**
86
+ * Encrypt under the first secret; `undefined` when the token would exceed {@link MAX_GRANT_CHARS} or
87
+ * the hour's subkey reached {@link MAX_SEALS_PER_SUBKEY}.
88
+ */
89
+ seal(input: GrantInput, nowMs: number): Promise<string | undefined>
90
+ /** Authenticate and decrypt, or `undefined` for anything not issued by this keyring and still valid. */
91
+ open(token: unknown, nowMs: number): Promise<OpenedGrant | undefined>
92
+ /** The 128-bit wire tag of a key under one secret (default: the first). */
93
+ tag(key: string, secret?: number): Promise<string>
94
+ /**
95
+ * The subject's stable opaque tag under each listed secret, newest first. Across a rotation the
96
+ * same viewer keeps one tag in common, whichever process served the load.
97
+ */
98
+ subjectTags(subject: string | null): Promise<string[]>
99
+ }
100
+
101
+ /** A rotation list: newest first, never empty. */
102
+ export type LiveSecrets = readonly [string, ...string[]]
103
+
104
+ export interface KeyringOptions {
105
+ readonly secrets: LiveSecrets
106
+ readonly appId: string
107
+ readonly ttl: number
108
+ /** Fixed per process; random when omitted. Tests pin it. */
109
+ readonly processId?: Uint8Array
110
+ /** Tests lower it; see {@link MAX_SEALS_PER_SUBKEY}. */
111
+ readonly maxSealsPerSubkey?: number
112
+ }
113
+
114
+ interface SecretEntry {
115
+ readonly index: number
116
+ readonly base: CryptoKey
117
+ readonly id: Uint8Array<ArrayBuffer>
118
+ readonly idKey: number
119
+ readonly tagKey: CryptoKey
120
+ readonly subjectKey: CryptoKey
121
+ }
122
+
123
+ /** Synchronous boot checks: count and strength. Duplicate ids need derivation, see {@link createKeyring}. */
124
+ export function assertLiveSecrets(secrets: readonly unknown[]): asserts secrets is LiveSecrets {
125
+ if (secrets.length === 0) throw new TypeError("[nifra/web] live: `secret` lists no secret")
126
+ if (secrets.length > MAX_SECRETS) {
127
+ throw new TypeError(
128
+ `[nifra/web] live: \`secret\` lists ${secrets.length} secrets; at most ${MAX_SECRETS} are allowed, since every publish goes out under each one. Remove a demoted secret once grantTtl plus the renewal window has passed.`,
129
+ )
130
+ }
131
+ for (const secret of secrets) {
132
+ const length = typeof secret === "string" ? TEXT.encode(secret).length : -1
133
+ if (length < MIN_SECRET_BYTES) {
134
+ throw new TypeError(
135
+ `[nifra/web] live: every secret must be a string of at least ${MIN_SECRET_BYTES} bytes (256-bit). Generate one with: openssl rand -base64 32`,
136
+ )
137
+ }
138
+ }
139
+ }
140
+
141
+ /** Derive every key a secret needs. Rejects when two secrets share a `secretId`. */
142
+ export async function createKeyring(options: KeyringOptions): Promise<Keyring> {
143
+ assertLiveSecrets(options.secrets)
144
+ const appSalt = TEXT.encode(options.appId)
145
+ const processId = options.processId ?? crypto.getRandomValues(new Uint8Array(8))
146
+ if (processId.length !== 8) throw new TypeError("[nifra/web] live: processId must be 8 bytes")
147
+ const derive = async (secret: string, index: number): Promise<SecretEntry> => {
148
+ const base = await crypto.subtle.importKey("raw", TEXT.encode(secret), "HKDF", false, [
149
+ "deriveBits",
150
+ "deriveKey",
151
+ ])
152
+ const id = new Uint8Array(await hkdf(base, NO_SALT, INFO_SECRET_ID, 32)).slice(0, 4)
153
+ const tagKey = await hmacKey(new Uint8Array(await hkdf(base, appSalt, INFO_TAG, 32)))
154
+ const subjectKey = await hmacKey(new Uint8Array(await hkdf(base, appSalt, INFO_SUBJECT, 32)))
155
+ return { index, base, id, idKey: readU32(id, 0), tagKey, subjectKey }
156
+ }
157
+ const [head, ...rest] = options.secrets
158
+ const first = await derive(head, 0)
159
+ const entries = [first, ...(await Promise.all(rest.map((secret, i) => derive(secret, i + 1))))]
160
+ const byId = new Map<number, SecretEntry>()
161
+ for (const entry of entries) {
162
+ if (byId.has(entry.idKey)) {
163
+ throw new TypeError(
164
+ "[nifra/web] live: two configured secrets share a secret id; replace one of them (a repeated secret does this too)",
165
+ )
166
+ }
167
+ byId.set(entry.idKey, entry)
168
+ }
169
+
170
+ // Least recently used first: a hit is moved to the end. Only a kid that sealed or opened a grant
171
+ // is kept, so forged kids cannot push out the subkeys real grants use.
172
+ const subkeys = new Map<string, Promise<CryptoKey>>()
173
+ const cachedSubkey = (name: string): Promise<CryptoKey> | undefined => {
174
+ const cached = subkeys.get(name)
175
+ if (cached !== undefined) {
176
+ subkeys.delete(name)
177
+ subkeys.set(name, cached)
178
+ }
179
+ return cached
180
+ }
181
+ const remember = (name: string, key: Promise<CryptoKey>): void => {
182
+ if (subkeys.has(name)) return
183
+ if (subkeys.size >= SUBKEY_CACHE_SIZE) {
184
+ const oldest = subkeys.keys().next().value
185
+ if (oldest !== undefined) subkeys.delete(oldest)
186
+ }
187
+ subkeys.set(name, key)
188
+ }
189
+ const deriveSubkey = (entry: SecretEntry, kid: Uint8Array<ArrayBuffer>): Promise<CryptoKey> =>
190
+ crypto.subtle.deriveKey(
191
+ { name: "HKDF", hash: "SHA-256", salt: kid, info: INFO_GRANT },
192
+ entry.base,
193
+ { name: "AES-GCM", length: 256 },
194
+ false,
195
+ ["encrypt", "decrypt"],
196
+ )
197
+
198
+ const ttl = options.ttl
199
+ const appId = options.appId
200
+ const maxSeals = options.maxSealsPerSubkey ?? MAX_SEALS_PER_SUBKEY
201
+ // The subkey `seal` uses changes with the hour, and only `seal` encrypts.
202
+ let sealHour = -1
203
+ let seals = 0
204
+
205
+ return {
206
+ appId,
207
+ ttl,
208
+ secretCount: entries.length,
209
+ secretIds: entries.map((entry) => entry.idKey),
210
+ async seal(input, nowMs) {
211
+ const iat = Math.floor(nowMs / 1000)
212
+ const hour = Math.floor(iat / HOUR_SECONDS)
213
+ if (hour !== sealHour) {
214
+ sealHour = hour
215
+ seals = 0
216
+ }
217
+ if (seals >= maxSeals) return undefined
218
+ seals++
219
+ const claims: GrantClaims = { v: 1, ...input, iat, exp: iat + ttl, aud: appId }
220
+ const kid = new Uint8Array(KID_BYTES)
221
+ kid.set(first.id, 0)
222
+ kid.set(processId, 4)
223
+ writeU32(kid, 12, hour)
224
+ const nonce = crypto.getRandomValues(new Uint8Array(NONCE_BYTES))
225
+ const name = latin1(kid)
226
+ let key = cachedSubkey(name)
227
+ if (key === undefined) {
228
+ key = deriveSubkey(first, kid)
229
+ remember(name, key)
230
+ }
231
+ const sealed = new Uint8Array(
232
+ await crypto.subtle.encrypt(
233
+ { name: "AES-GCM", iv: nonce, additionalData: kid, tagLength: GCM_TAG_BYTES * 8 },
234
+ await key,
235
+ TEXT.encode(JSON.stringify(claims)),
236
+ ),
237
+ )
238
+ const token = new Uint8Array(KID_BYTES + NONCE_BYTES + sealed.length)
239
+ token.set(kid, 0)
240
+ token.set(nonce, KID_BYTES)
241
+ token.set(sealed, KID_BYTES + NONCE_BYTES)
242
+ const encoded = toBase64Url(token)
243
+ return encoded.length > MAX_GRANT_CHARS ? undefined : encoded
244
+ },
245
+ async open(token, nowMs) {
246
+ // Size and framing first, then the secret id and hour: none of these refusals costs any crypto.
247
+ if (typeof token !== "string" || token.length > MAX_GRANT_CHARS) return undefined
248
+ const bytes = fromBase64Url(token)
249
+ if (bytes === null || bytes.length < KID_BYTES + NONCE_BYTES + GCM_TAG_BYTES) return undefined
250
+ const entry = byId.get(readU32(bytes, 0))
251
+ if (entry === undefined) return undefined
252
+ const hour = readU32(bytes, 12)
253
+ const nowHour = Math.floor(nowMs / HOUR_MS)
254
+ if (hour !== nowHour && hour !== nowHour - 1) return undefined
255
+ const kid = bytes.slice(0, KID_BYTES)
256
+ const name = latin1(kid)
257
+ const cached = cachedSubkey(name)
258
+ const key = cached ?? deriveSubkey(entry, kid)
259
+ let plain: ArrayBuffer
260
+ try {
261
+ plain = await crypto.subtle.decrypt(
262
+ {
263
+ name: "AES-GCM",
264
+ iv: bytes.slice(KID_BYTES, KID_BYTES + NONCE_BYTES),
265
+ additionalData: kid,
266
+ tagLength: GCM_TAG_BYTES * 8,
267
+ },
268
+ await key,
269
+ bytes.slice(KID_BYTES + NONCE_BYTES),
270
+ )
271
+ } catch {
272
+ return undefined
273
+ }
274
+ if (cached === undefined) remember(name, key)
275
+ let claims: unknown
276
+ try {
277
+ claims = JSON.parse(UTF8.decode(plain))
278
+ } catch {
279
+ return undefined
280
+ }
281
+ return validClaims(claims, appId, ttl, hour, Math.floor(nowMs / 1000))
282
+ ? { claims, secret: entry.index }
283
+ : undefined
284
+ },
285
+ async tag(key, secret = 0) {
286
+ const tagKey = (entries[secret] ?? first).tagKey
287
+ const mac = await crypto.subtle.sign("HMAC", tagKey, TEXT.encode(key))
288
+ return toBase64Url(new Uint8Array(mac).slice(0, TAG_BYTES))
289
+ },
290
+ subjectTags(subject) {
291
+ // A prefix per kind: no subject string can collide with the anonymous viewer's input.
292
+ const input = TEXT.encode(subject === null ? "a" : `s${subject}`)
293
+ return Promise.all(
294
+ entries.map(async (entry) =>
295
+ toBase64Url(
296
+ new Uint8Array(await crypto.subtle.sign("HMAC", entry.subjectKey, input)).slice(
297
+ 0,
298
+ TAG_BYTES,
299
+ ),
300
+ ),
301
+ ),
302
+ )
303
+ },
304
+ }
305
+ }
306
+
307
+ function validClaims(
308
+ value: unknown,
309
+ appId: string,
310
+ ttl: number,
311
+ hour: number,
312
+ nowSeconds: number,
313
+ ): value is GrantClaims {
314
+ if (!isRecord(value)) return false
315
+ const { v, sid, sub, path, frame, owners, s, r, iat, exp, aud } = value
316
+ if (v !== 1 || aud !== appId) return false
317
+ if (typeof sid !== "string" || (sid !== SEED_SID && !SID_PATTERN.test(sid))) return false
318
+ if (sub !== null && typeof sub !== "string") return false
319
+ if (typeof path !== "string" || typeof frame !== "string") return false
320
+ if (!isCount(s) || !isCount(r) || !isInteger(iat) || !isInteger(exp)) return false
321
+ if (iat > nowSeconds + IAT_SKEW_SECONDS) return false
322
+ if (Math.floor(iat / HOUR_SECONDS) !== hour) return false
323
+ if (exp - iat !== ttl || exp <= nowSeconds) return false
324
+ if (!isRecord(owners)) return false
325
+ let keys = 0
326
+ for (const owner of Object.values(owners)) {
327
+ if (!isRecord(owner) || !isCount(owner.w) || !Array.isArray(owner.k)) return false
328
+ for (const key of owner.k) if (typeof key !== "string" || !KEY_PATTERN.test(key)) return false
329
+ keys += owner.k.length
330
+ }
331
+ return keys <= MAX_KEYS_PER_FRAME
332
+ }
333
+
334
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
335
+ typeof value === "object" && value !== null && !Array.isArray(value)
336
+
337
+ const isInteger = (value: unknown): value is number =>
338
+ typeof value === "number" && Number.isSafeInteger(value)
339
+
340
+ const isCount = (value: unknown): value is number => isInteger(value) && value >= 0
341
+
342
+ const hkdf = (
343
+ base: CryptoKey,
344
+ salt: Uint8Array<ArrayBuffer>,
345
+ info: Uint8Array<ArrayBuffer>,
346
+ bytes: number,
347
+ ): Promise<ArrayBuffer> =>
348
+ crypto.subtle.deriveBits({ name: "HKDF", hash: "SHA-256", salt, info }, base, bytes * 8)
349
+
350
+ const hmacKey = (raw: Uint8Array<ArrayBuffer>): Promise<CryptoKey> =>
351
+ crypto.subtle.importKey("raw", raw, { name: "HMAC", hash: "SHA-256" }, false, ["sign"])
352
+
353
+ const view = (bytes: Uint8Array): DataView =>
354
+ new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
355
+
356
+ const readU32 = (bytes: Uint8Array, at: number): number => view(bytes).getUint32(at)
357
+
358
+ const writeU32 = (bytes: Uint8Array, at: number, value: number): void =>
359
+ view(bytes).setUint32(at, value)
360
+
361
+ const latin1 = (bytes: Uint8Array): string => {
362
+ let out = ""
363
+ for (const byte of bytes) out += String.fromCharCode(byte)
364
+ return out
365
+ }
366
+
367
+ export const toBase64Url = (bytes: Uint8Array): string =>
368
+ btoa(latin1(bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")
369
+
370
+ const BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
371
+ const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/
372
+
373
+ /** Canonical encodings only: a second spelling of the same bytes is refused. */
374
+ function fromBase64Url(s: string): Uint8Array<ArrayBuffer> | null {
375
+ if (s.length === 0 || s.length % 4 === 1 || !BASE64URL_PATTERN.test(s)) return null
376
+ const remainder = s.length % 4
377
+ const last = BASE64URL_ALPHABET.indexOf(s.charAt(s.length - 1))
378
+ if ((remainder === 2 && (last & 0x0f) !== 0) || (remainder === 3 && (last & 0x03) !== 0))
379
+ return null
380
+ const bin = atob(s.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - remainder) % 4))
381
+ const bytes = new Uint8Array(bin.length)
382
+ for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i)
383
+ return bytes
384
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * A request body as text, read only up to `max` bytes, or the status to refuse it with: 413 past the
3
+ * cap, declared or delivered; 400 for a malformed length or invalid UTF-8.
4
+ */
5
+ export async function readCapped(req: Request, max: number): Promise<string | 400 | 413> {
6
+ const declared = req.headers.get("content-length")
7
+ if (declared !== null) {
8
+ if (!/^\d+$/.test(declared)) return 400
9
+ if (Number(declared) > max) return 413
10
+ }
11
+ if (req.body === null) return ""
12
+ const reader = req.body.getReader()
13
+ const chunks: Uint8Array[] = []
14
+ let total = 0
15
+ for (;;) {
16
+ const { done, value } = await reader.read()
17
+ if (done) break
18
+ total += value.byteLength
19
+ if (total > max) {
20
+ await Promise.allSettled([reader.cancel()])
21
+ return 413
22
+ }
23
+ chunks.push(value)
24
+ }
25
+ const bytes = new Uint8Array(total)
26
+ let offset = 0
27
+ for (const chunk of chunks) {
28
+ bytes.set(chunk, offset)
29
+ offset += chunk.byteLength
30
+ }
31
+ try {
32
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes)
33
+ } catch {
34
+ return 400
35
+ }
36
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Stored revocations, checked on every use of a grant. An entry is kept for one grant lifetime plus
3
+ * clock skew: by then every grant issued before it has expired.
4
+ */
5
+ import type { LiveRevocation } from "./bus.ts"
6
+ import type { GrantClaims } from "./keyring.ts"
7
+
8
+ /** Past this many stored entries, one global revocation replaces them all. */
9
+ export const MAX_REVOCATIONS = 100_000
10
+ /** How far clocks may disagree; a revocation is kept this much past the grant lifetime. */
11
+ export const REVOCATION_SKEW_MS = 120_000
12
+
13
+ interface Stamp {
14
+ readonly seq: number
15
+ readonly at: number
16
+ }
17
+
18
+ /** What a load checks a grant against. */
19
+ export interface RevocationCheck {
20
+ grantRevoked(claims: GrantClaims): boolean
21
+ keyRevoked(key: string, claims: GrantClaims): boolean
22
+ }
23
+
24
+ /** The stored revocations that bear on a few subjects and keys: what the Workers hub tells a load. */
25
+ export interface RevocationSnapshot {
26
+ readonly floor: number
27
+ readonly subjects: Readonly<Record<string, number>>
28
+ readonly keys: Readonly<Record<string, number>>
29
+ /** By `key + "\0" + subject`. */
30
+ readonly subjectKeys: Readonly<Record<string, number>>
31
+ }
32
+
33
+ export class Revocations implements RevocationCheck {
34
+ /** A grant issued below this seq is refused whole: `revokeAll`, or history this process lacks. */
35
+ floor: number
36
+ private readonly subjects = new Map<string, Stamp>()
37
+ private readonly keys = new Map<string, Stamp>()
38
+ /** `key + "\0" + subject`; the key charset has no NUL. */
39
+ private readonly subjectKeys = new Map<string, Stamp>()
40
+
41
+ constructor(
42
+ head: number,
43
+ private readonly ttlMs: number,
44
+ private readonly limit = MAX_REVOCATIONS,
45
+ ) {
46
+ this.floor = head
47
+ }
48
+
49
+ apply(event: LiveRevocation & { readonly seq: number }, nowMs: number): void {
50
+ this.prune(nowMs)
51
+ const stamp = { seq: event.seq, at: nowMs }
52
+ if (event.key === null) {
53
+ if (event.sub === null) this.raise(event.seq)
54
+ else renew(this.subjects, event.sub, stamp)
55
+ } else if (event.sub === null) {
56
+ renew(this.keys, event.key, stamp)
57
+ } else {
58
+ renew(this.subjectKeys, `${event.key}\0${event.sub}`, stamp)
59
+ }
60
+ if (this.subjects.size + this.keys.size + this.subjectKeys.size > this.limit) {
61
+ console.error(
62
+ `[nifra/web] live: more than ${this.limit} stored revocations; every grant issued before now is refused instead`,
63
+ )
64
+ this.raise(event.seq)
65
+ }
66
+ }
67
+
68
+ /** Refuse every grant issued below `seq`; the stored entries are all older, so they go. */
69
+ raise(seq: number): void {
70
+ if (seq > this.floor) this.floor = seq
71
+ this.subjects.clear()
72
+ this.keys.clear()
73
+ this.subjectKeys.clear()
74
+ }
75
+
76
+ grantRevoked(claims: GrantClaims): boolean {
77
+ return (
78
+ claims.s < this.floor ||
79
+ (claims.sub !== null && above(this.subjects.get(claims.sub), claims.s))
80
+ )
81
+ }
82
+
83
+ keyRevoked(key: string, claims: GrantClaims): boolean {
84
+ return (
85
+ above(this.keys.get(key), claims.s) ||
86
+ (claims.sub !== null && above(this.subjectKeys.get(`${key}\0${claims.sub}`), claims.s))
87
+ )
88
+ }
89
+
90
+ /** The entries for these subjects and keys, and every pairing of them. */
91
+ snapshot(subjects: readonly string[], keys: readonly string[]): RevocationSnapshot {
92
+ const pairs: string[] = []
93
+ for (const key of keys) for (const sub of subjects) pairs.push(`${key}\0${sub}`)
94
+ return {
95
+ floor: this.floor,
96
+ subjects: pick(this.subjects, subjects),
97
+ keys: pick(this.keys, keys),
98
+ subjectKeys: pick(this.subjectKeys, pairs),
99
+ }
100
+ }
101
+
102
+ private prune(nowMs: number): void {
103
+ const horizon = nowMs - this.ttlMs - REVOCATION_SKEW_MS
104
+ for (const map of [this.subjects, this.keys, this.subjectKeys]) {
105
+ // Insertion order is age order (see `renew`), so the stale entries are a prefix.
106
+ for (const [name, stamp] of map) {
107
+ if (stamp.at >= horizon) break
108
+ map.delete(name)
109
+ }
110
+ }
111
+ }
112
+ }
113
+
114
+ const above = (stamp: Stamp | undefined, s: number): boolean => stamp !== undefined && stamp.seq > s
115
+
116
+ /** Built from entries, so a name such as `__proto__` is an own property like any other. */
117
+ function pick(map: Map<string, Stamp>, names: readonly string[]): Record<string, number> {
118
+ const out: [string, number][] = []
119
+ for (const name of names) {
120
+ const stamp = map.get(name)
121
+ if (stamp !== undefined) out.push([name, stamp.seq])
122
+ }
123
+ return Object.fromEntries(out)
124
+ }
125
+
126
+ /** Re-insert so the map stays in age order. */
127
+ function renew(map: Map<string, Stamp>, name: string, stamp: Stamp): void {
128
+ map.delete(name)
129
+ map.set(name, stamp)
130
+ }
131
+
132
+ /** {@link RevocationCheck} over a {@link RevocationSnapshot}: the same answers for what it covers. */
133
+ export class RevocationView implements RevocationCheck {
134
+ constructor(private readonly snapshot: RevocationSnapshot) {}
135
+
136
+ grantRevoked(claims: GrantClaims): boolean {
137
+ return (
138
+ claims.s < this.snapshot.floor ||
139
+ (claims.sub !== null && stampAbove(this.snapshot.subjects, claims.sub, claims.s))
140
+ )
141
+ }
142
+
143
+ keyRevoked(key: string, claims: GrantClaims): boolean {
144
+ return (
145
+ stampAbove(this.snapshot.keys, key, claims.s) ||
146
+ (claims.sub !== null &&
147
+ stampAbove(this.snapshot.subjectKeys, `${key}\0${claims.sub}`, claims.s))
148
+ )
149
+ }
150
+ }
151
+
152
+ function stampAbove(stamps: Readonly<Record<string, number>>, name: string, s: number): boolean {
153
+ const seq = Object.hasOwn(stamps, name) ? stamps[name] : undefined
154
+ return seq !== undefined && seq > s
155
+ }
@@ -0,0 +1,46 @@
1
+ /** Option checks `live()` and `liveHub()` share: an app and its hub must read the same options alike. */
2
+
3
+ export const DEFAULT_TTL_SECONDS = 600
4
+ export const DEFAULT_MAX_SOCKETS = 10_000
5
+ export const DEFAULT_RELOAD_RATE = 500
6
+ export const DEFAULT_APP_ID = "nifra"
7
+
8
+ export function positive(value: number | undefined, fallback: number, name: string): number {
9
+ const resolved = value ?? fallback
10
+ if (!Number.isSafeInteger(resolved) || resolved < 1) {
11
+ throw new TypeError(`[nifra/web] live: \`${name}\` must be a positive whole number`)
12
+ }
13
+ return resolved
14
+ }
15
+
16
+ /**
17
+ * At most an hour: a grant's subkey names the hour it was sealed in, and only that hour and the next
18
+ * open it, so a longer lifetime would end early.
19
+ */
20
+ export function grantTtlOf(value: number | undefined): number {
21
+ const ttl = value ?? DEFAULT_TTL_SECONDS
22
+ if (!Number.isSafeInteger(ttl) || ttl < 60 || ttl > 3600) {
23
+ throw new TypeError(
24
+ "[nifra/web] live: `grantTtl` must be a whole number of seconds from 60 to 3600",
25
+ )
26
+ }
27
+ return ttl
28
+ }
29
+
30
+ export function spreadFloorOf(value: number | undefined): number {
31
+ const floor = value ?? 0
32
+ if (!Number.isSafeInteger(floor) || floor < 0 || floor > 5000) {
33
+ throw new TypeError("[nifra/web] live: `spreadFloor` must be a whole number of ms up to 5000")
34
+ }
35
+ return floor
36
+ }
37
+
38
+ export function appIdOf(value: string | undefined): string {
39
+ const appId = value ?? DEFAULT_APP_ID
40
+ if (typeof appId !== "string" || appId === "" || appId.length > 128) {
41
+ throw new TypeError(
42
+ "[nifra/web] live: `appId` must be a non-empty string of at most 128 characters",
43
+ )
44
+ }
45
+ return appId
46
+ }