@nifrajs/web 5.5.1 → 5.6.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 (457) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/dist/boundary.d.ts +67 -7
  3. package/dist/boundary.d.ts.map +1 -1
  4. package/dist/boundary.js +6 -4
  5. package/dist/boundary.js.map +1 -1
  6. package/dist/build-plan.d.ts +3 -0
  7. package/dist/build-plan.d.ts.map +1 -1
  8. package/dist/build-plan.js.map +1 -1
  9. package/dist/build-vite.d.ts.map +1 -1
  10. package/dist/build-vite.js +2 -0
  11. package/dist/build-vite.js.map +1 -1
  12. package/dist/build.d.ts +9 -2
  13. package/dist/build.d.ts.map +1 -1
  14. package/dist/build.js +9 -0
  15. package/dist/build.js.map +1 -1
  16. package/dist/client.d.ts +2 -1
  17. package/dist/client.d.ts.map +1 -1
  18. package/dist/client.js +140 -12
  19. package/dist/client.js.map +1 -1
  20. package/dist/csp.d.ts +8 -0
  21. package/dist/csp.d.ts.map +1 -1
  22. package/dist/csp.js +11 -2
  23. package/dist/csp.js.map +1 -1
  24. package/dist/deferred.d.ts +6 -4
  25. package/dist/deferred.d.ts.map +1 -1
  26. package/dist/deferred.js +10 -6
  27. package/dist/deferred.js.map +1 -1
  28. package/dist/dev-feed.d.ts.map +1 -1
  29. package/dist/dev-feed.js +11 -2
  30. package/dist/dev-feed.js.map +1 -1
  31. package/dist/dev-session.d.ts.map +1 -1
  32. package/dist/dev-session.js +13 -6
  33. package/dist/dev-session.js.map +1 -1
  34. package/dist/dev.d.ts +4 -1
  35. package/dist/dev.d.ts.map +1 -1
  36. package/dist/dev.js +12 -5
  37. package/dist/dev.js.map +1 -1
  38. package/dist/diagnostic-catalog.d.ts +4 -0
  39. package/dist/diagnostic-catalog.d.ts.map +1 -1
  40. package/dist/diagnostic-catalog.js +48 -0
  41. package/dist/diagnostic-catalog.js.map +1 -1
  42. package/dist/diagnostic.d.ts +1 -1
  43. package/dist/diagnostic.d.ts.map +1 -1
  44. package/dist/diagnostic.js +1 -1
  45. package/dist/diagnostic.js.map +1 -1
  46. package/dist/fn.d.ts +5 -3
  47. package/dist/fn.d.ts.map +1 -1
  48. package/dist/fn.js +20 -5
  49. package/dist/fn.js.map +1 -1
  50. package/dist/fs.d.ts.map +1 -1
  51. package/dist/fs.js +13 -3
  52. package/dist/fs.js.map +1 -1
  53. package/dist/identity.d.ts +22 -0
  54. package/dist/identity.d.ts.map +1 -0
  55. package/dist/identity.js +60 -0
  56. package/dist/identity.js.map +1 -0
  57. package/dist/index.d.ts +7 -6
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +4 -2
  60. package/dist/index.js.map +1 -1
  61. package/dist/internal/cdn/cloudflare.d.ts +2 -1
  62. package/dist/internal/cdn/cloudflare.d.ts.map +1 -1
  63. package/dist/internal/cdn/cloudflare.js +2 -1
  64. package/dist/internal/cdn/cloudflare.js.map +1 -1
  65. package/dist/internal/certified-components.d.ts +1 -1
  66. package/dist/internal/certified-components.js +1 -1
  67. package/dist/internal/codegen.d.ts +11 -10
  68. package/dist/internal/codegen.d.ts.map +1 -1
  69. package/dist/internal/codegen.js +65 -8
  70. package/dist/internal/codegen.js.map +1 -1
  71. package/dist/internal/dev-websocket.d.ts +73 -0
  72. package/dist/internal/dev-websocket.d.ts.map +1 -0
  73. package/dist/internal/dev-websocket.js +145 -0
  74. package/dist/internal/dev-websocket.js.map +1 -0
  75. package/dist/internal/hole-runtime.d.ts +66 -0
  76. package/dist/internal/hole-runtime.d.ts.map +1 -0
  77. package/dist/internal/hole-runtime.js +211 -0
  78. package/dist/internal/hole-runtime.js.map +1 -0
  79. package/dist/internal/identity-epoch.d.ts +50 -0
  80. package/dist/internal/identity-epoch.d.ts.map +1 -0
  81. package/dist/internal/identity-epoch.js +179 -0
  82. package/dist/internal/identity-epoch.js.map +1 -0
  83. package/dist/internal/invalidation-contract.d.ts +23 -0
  84. package/dist/internal/invalidation-contract.d.ts.map +1 -0
  85. package/dist/internal/invalidation-contract.js +11 -0
  86. package/dist/internal/invalidation-contract.js.map +1 -0
  87. package/dist/internal/invalidation.d.ts +62 -0
  88. package/dist/internal/invalidation.d.ts.map +1 -0
  89. package/dist/internal/invalidation.js +149 -0
  90. package/dist/internal/invalidation.js.map +1 -0
  91. package/dist/internal/island-bindings.d.ts +51 -0
  92. package/dist/internal/island-bindings.d.ts.map +1 -0
  93. package/dist/internal/island-bindings.js +147 -0
  94. package/dist/internal/island-bindings.js.map +1 -0
  95. package/dist/internal/island-list.d.ts +26 -0
  96. package/dist/internal/island-list.d.ts.map +1 -0
  97. package/dist/internal/island-list.js +78 -0
  98. package/dist/internal/island-list.js.map +1 -0
  99. package/dist/internal/island-mount.d.ts +4 -0
  100. package/dist/internal/island-mount.d.ts.map +1 -0
  101. package/dist/internal/island-mount.js +66 -0
  102. package/dist/internal/island-mount.js.map +1 -0
  103. package/dist/internal/island-owner.d.ts +5 -0
  104. package/dist/internal/island-owner.d.ts.map +1 -0
  105. package/dist/internal/island-owner.js +20 -0
  106. package/dist/internal/island-owner.js.map +1 -0
  107. package/dist/internal/island-resource.d.ts +30 -0
  108. package/dist/internal/island-resource.d.ts.map +1 -0
  109. package/dist/internal/island-resource.js +57 -0
  110. package/dist/internal/island-resource.js.map +1 -0
  111. package/dist/internal/island-signals.d.ts +51 -0
  112. package/dist/internal/island-signals.d.ts.map +1 -0
  113. package/dist/internal/island-signals.js +220 -0
  114. package/dist/internal/island-signals.js.map +1 -0
  115. package/dist/internal/keep-alive-table.d.ts +23 -0
  116. package/dist/internal/keep-alive-table.d.ts.map +1 -0
  117. package/dist/internal/keep-alive-table.js +53 -0
  118. package/dist/internal/keep-alive-table.js.map +1 -0
  119. package/dist/internal/live-contract.d.ts +96 -0
  120. package/dist/internal/live-contract.d.ts.map +1 -0
  121. package/dist/internal/live-contract.js +5 -0
  122. package/dist/internal/live-contract.js.map +1 -0
  123. package/dist/internal/live-queries.d.ts +22 -0
  124. package/dist/internal/live-queries.d.ts.map +1 -0
  125. package/dist/internal/live-queries.js +54 -0
  126. package/dist/internal/live-queries.js.map +1 -0
  127. package/dist/internal/page-events.d.ts +10 -0
  128. package/dist/internal/page-events.d.ts.map +1 -0
  129. package/dist/internal/page-events.js +26 -0
  130. package/dist/internal/page-events.js.map +1 -0
  131. package/dist/internal/page-execution.d.ts +14 -1
  132. package/dist/internal/page-execution.d.ts.map +1 -1
  133. package/dist/internal/page-execution.js +415 -31
  134. package/dist/internal/page-execution.js.map +1 -1
  135. package/dist/internal/ppr/admission.d.ts +83 -0
  136. package/dist/internal/ppr/admission.d.ts.map +1 -0
  137. package/dist/internal/ppr/admission.js +221 -0
  138. package/dist/internal/ppr/admission.js.map +1 -0
  139. package/dist/internal/ppr/canary.d.ts +24 -0
  140. package/dist/internal/ppr/canary.d.ts.map +1 -0
  141. package/dist/internal/ppr/canary.js +102 -0
  142. package/dist/internal/ppr/canary.js.map +1 -0
  143. package/dist/internal/ppr/compile-shell.d.ts +39 -0
  144. package/dist/internal/ppr/compile-shell.d.ts.map +1 -0
  145. package/dist/internal/ppr/compile-shell.js +298 -0
  146. package/dist/internal/ppr/compile-shell.js.map +1 -0
  147. package/dist/internal/ppr/contract.d.ts +177 -0
  148. package/dist/internal/ppr/contract.d.ts.map +1 -0
  149. package/dist/internal/ppr/contract.js +80 -0
  150. package/dist/internal/ppr/contract.js.map +1 -0
  151. package/dist/internal/ppr/frames.d.ts +71 -0
  152. package/dist/internal/ppr/frames.d.ts.map +1 -0
  153. package/dist/internal/ppr/frames.js +119 -0
  154. package/dist/internal/ppr/frames.js.map +1 -0
  155. package/dist/internal/ppr/integration.d.ts +19 -0
  156. package/dist/internal/ppr/integration.d.ts.map +1 -0
  157. package/dist/internal/ppr/integration.js +106 -0
  158. package/dist/internal/ppr/integration.js.map +1 -0
  159. package/dist/internal/ppr/options.d.ts +96 -0
  160. package/dist/internal/ppr/options.d.ts.map +1 -0
  161. package/dist/internal/ppr/options.js +125 -0
  162. package/dist/internal/ppr/options.js.map +1 -0
  163. package/dist/internal/ppr/route-rules.d.ts +12 -0
  164. package/dist/internal/ppr/route-rules.d.ts.map +1 -0
  165. package/dist/internal/ppr/route-rules.js +53 -0
  166. package/dist/internal/ppr/route-rules.js.map +1 -0
  167. package/dist/internal/ppr/runtime.d.ts +29 -0
  168. package/dist/internal/ppr/runtime.d.ts.map +1 -0
  169. package/dist/internal/ppr/runtime.js +32 -0
  170. package/dist/internal/ppr/runtime.js.map +1 -0
  171. package/dist/internal/ppr/server.d.ts +29 -0
  172. package/dist/internal/ppr/server.d.ts.map +1 -0
  173. package/dist/internal/ppr/server.js +1407 -0
  174. package/dist/internal/ppr/server.js.map +1 -0
  175. package/dist/internal/ppr/shell-store.d.ts +38 -0
  176. package/dist/internal/ppr/shell-store.d.ts.map +1 -0
  177. package/dist/internal/ppr/shell-store.js +90 -0
  178. package/dist/internal/ppr/shell-store.js.map +1 -0
  179. package/dist/internal/prerendered-pages.d.ts +1 -1
  180. package/dist/internal/prerendered-pages.d.ts.map +1 -1
  181. package/dist/internal/prerendered-pages.js +7 -4
  182. package/dist/internal/prerendered-pages.js.map +1 -1
  183. package/dist/internal/query-digest.d.ts +9 -0
  184. package/dist/internal/query-digest.d.ts.map +1 -0
  185. package/dist/internal/query-digest.js +23 -0
  186. package/dist/internal/query-digest.js.map +1 -0
  187. package/dist/internal/query-runtime.d.ts +6 -1
  188. package/dist/internal/query-runtime.d.ts.map +1 -1
  189. package/dist/internal/query-runtime.js +9 -0
  190. package/dist/internal/query-runtime.js.map +1 -1
  191. package/dist/internal/query-seam.d.ts +29 -0
  192. package/dist/internal/query-seam.d.ts.map +1 -0
  193. package/dist/internal/query-seam.js +23 -0
  194. package/dist/internal/query-seam.js.map +1 -0
  195. package/dist/internal/render-document.d.ts +28 -1
  196. package/dist/internal/render-document.d.ts.map +1 -1
  197. package/dist/internal/render-document.js +83 -18
  198. package/dist/internal/render-document.js.map +1 -1
  199. package/dist/internal/request-abort.d.ts +8 -0
  200. package/dist/internal/request-abort.d.ts.map +1 -0
  201. package/dist/internal/request-abort.js +22 -0
  202. package/dist/internal/request-abort.js.map +1 -0
  203. package/dist/internal/server-boundary.d.ts +2 -0
  204. package/dist/internal/server-boundary.d.ts.map +1 -1
  205. package/dist/internal/server-boundary.js +8 -1
  206. package/dist/internal/server-boundary.js.map +1 -1
  207. package/dist/internal/share-data.d.ts +10 -0
  208. package/dist/internal/share-data.d.ts.map +1 -0
  209. package/dist/internal/share-data.js +135 -0
  210. package/dist/internal/share-data.js.map +1 -0
  211. package/dist/internal/store-signature.d.ts +15 -0
  212. package/dist/internal/store-signature.d.ts.map +1 -0
  213. package/dist/internal/store-signature.js +114 -0
  214. package/dist/internal/store-signature.js.map +1 -0
  215. package/dist/internal/vite-plugin-compat.d.ts +2 -2
  216. package/dist/internal/vite-plugin-compat.js +2 -2
  217. package/dist/internal/web-app.d.ts +25 -0
  218. package/dist/internal/web-app.d.ts.map +1 -1
  219. package/dist/internal/web-app.js +78 -3
  220. package/dist/internal/web-app.js.map +1 -1
  221. package/dist/islands-swap.d.ts +41 -0
  222. package/dist/islands-swap.d.ts.map +1 -0
  223. package/dist/islands-swap.js +270 -0
  224. package/dist/islands-swap.js.map +1 -0
  225. package/dist/islands.d.ts +86 -24
  226. package/dist/islands.d.ts.map +1 -1
  227. package/dist/islands.js +138 -78
  228. package/dist/islands.js.map +1 -1
  229. package/dist/isr.d.ts +67 -13
  230. package/dist/isr.d.ts.map +1 -1
  231. package/dist/isr.js +247 -99
  232. package/dist/isr.js.map +1 -1
  233. package/dist/keep-alive.d.ts +147 -0
  234. package/dist/keep-alive.d.ts.map +1 -0
  235. package/dist/keep-alive.js +1069 -0
  236. package/dist/keep-alive.js.map +1 -0
  237. package/dist/live/bus.d.ts +56 -0
  238. package/dist/live/bus.d.ts.map +1 -0
  239. package/dist/live/bus.js +32 -0
  240. package/dist/live/bus.js.map +1 -0
  241. package/dist/live/core.d.ts +25 -0
  242. package/dist/live/core.d.ts.map +1 -0
  243. package/dist/live/core.js +29 -0
  244. package/dist/live/core.js.map +1 -0
  245. package/dist/live/history.d.ts +38 -0
  246. package/dist/live/history.d.ts.map +1 -0
  247. package/dist/live/history.js +104 -0
  248. package/dist/live/history.js.map +1 -0
  249. package/dist/live/hub-protocol.d.ts +46 -0
  250. package/dist/live/hub-protocol.d.ts.map +1 -0
  251. package/dist/live/hub-protocol.js +108 -0
  252. package/dist/live/hub-protocol.js.map +1 -0
  253. package/dist/live/hub.d.ts +181 -0
  254. package/dist/live/hub.d.ts.map +1 -0
  255. package/dist/live/hub.js +539 -0
  256. package/dist/live/hub.js.map +1 -0
  257. package/dist/live/keyring.d.ts +87 -0
  258. package/dist/live/keyring.d.ts.map +1 -0
  259. package/dist/live/keyring.js +260 -0
  260. package/dist/live/keyring.js.map +1 -0
  261. package/dist/live/read-capped.d.ts +6 -0
  262. package/dist/live/read-capped.d.ts.map +1 -0
  263. package/dist/live/read-capped.js +42 -0
  264. package/dist/live/read-capped.js.map +1 -0
  265. package/dist/live/revocations.d.ts +52 -0
  266. package/dist/live/revocations.d.ts.map +1 -0
  267. package/dist/live/revocations.js +116 -0
  268. package/dist/live/revocations.js.map +1 -0
  269. package/dist/live/settings.d.ts +14 -0
  270. package/dist/live/settings.d.ts.map +1 -0
  271. package/dist/live/settings.js +38 -0
  272. package/dist/live/settings.js.map +1 -0
  273. package/dist/live-client.d.ts +34 -0
  274. package/dist/live-client.d.ts.map +1 -0
  275. package/dist/live-client.js +840 -0
  276. package/dist/live-client.js.map +1 -0
  277. package/dist/live-hub.d.ts +130 -0
  278. package/dist/live-hub.d.ts.map +1 -0
  279. package/dist/live-hub.js +507 -0
  280. package/dist/live-hub.js.map +1 -0
  281. package/dist/live-redis.d.ts +48 -0
  282. package/dist/live-redis.d.ts.map +1 -0
  283. package/dist/live-redis.js +201 -0
  284. package/dist/live-redis.js.map +1 -0
  285. package/dist/live.d.ts +94 -0
  286. package/dist/live.d.ts.map +1 -0
  287. package/dist/live.js +869 -0
  288. package/dist/live.js.map +1 -0
  289. package/dist/manifest.d.ts +113 -2
  290. package/dist/manifest.d.ts.map +1 -1
  291. package/dist/manifest.js +109 -5
  292. package/dist/manifest.js.map +1 -1
  293. package/dist/navigation.d.ts +9 -3
  294. package/dist/navigation.d.ts.map +1 -1
  295. package/dist/navigation.js +13 -3
  296. package/dist/navigation.js.map +1 -1
  297. package/dist/plugins/vite-zone-guard.d.ts +6 -0
  298. package/dist/plugins/vite-zone-guard.d.ts.map +1 -1
  299. package/dist/plugins/vite-zone-guard.js +8 -2
  300. package/dist/plugins/vite-zone-guard.js.map +1 -1
  301. package/dist/ppr.d.ts +73 -0
  302. package/dist/ppr.d.ts.map +1 -0
  303. package/dist/ppr.js +76 -0
  304. package/dist/ppr.js.map +1 -0
  305. package/dist/query.d.ts +8 -13
  306. package/dist/query.d.ts.map +1 -1
  307. package/dist/query.js +112 -18
  308. package/dist/query.js.map +1 -1
  309. package/dist/render-seam.d.ts +30 -0
  310. package/dist/render-seam.d.ts.map +1 -1
  311. package/dist/render-seam.js +5 -0
  312. package/dist/render-seam.js.map +1 -1
  313. package/dist/route-types.d.ts.map +1 -1
  314. package/dist/route-types.js +2 -2
  315. package/dist/route-types.js.map +1 -1
  316. package/dist/router.d.ts +72 -4
  317. package/dist/router.d.ts.map +1 -1
  318. package/dist/router.js +374 -99
  319. package/dist/router.js.map +1 -1
  320. package/dist/server-components/build.d.ts.map +1 -1
  321. package/dist/server-components/build.js +3 -3
  322. package/dist/server-components/build.js.map +1 -1
  323. package/dist/server-components/client.d.ts +23 -1
  324. package/dist/server-components/client.d.ts.map +1 -1
  325. package/dist/server-components/client.js +92 -5
  326. package/dist/server-components/client.js.map +1 -1
  327. package/dist/server-components/hole.d.ts +8 -0
  328. package/dist/server-components/hole.d.ts.map +1 -0
  329. package/dist/server-components/hole.js +20 -0
  330. package/dist/server-components/hole.js.map +1 -0
  331. package/dist/server-components/html-policy.d.ts.map +1 -1
  332. package/dist/server-components/html-policy.js +62 -4
  333. package/dist/server-components/html-policy.js.map +1 -1
  334. package/dist/server-components/html-scan.d.ts +17 -0
  335. package/dist/server-components/html-scan.d.ts.map +1 -0
  336. package/dist/server-components/html-scan.js +180 -0
  337. package/dist/server-components/html-scan.js.map +1 -0
  338. package/dist/server-components/html.d.ts.map +1 -1
  339. package/dist/server-components/html.js +19 -4
  340. package/dist/server-components/html.js.map +1 -1
  341. package/dist/server-components/router-integration.d.ts +4 -1
  342. package/dist/server-components/router-integration.d.ts.map +1 -1
  343. package/dist/server-components/router-integration.js +23 -2
  344. package/dist/server-components/router-integration.js.map +1 -1
  345. package/dist/server-components/runtime.d.ts +6 -1
  346. package/dist/server-components/runtime.d.ts.map +1 -1
  347. package/dist/server-components/runtime.js +86 -28
  348. package/dist/server-components/runtime.js.map +1 -1
  349. package/dist/server-components/state.d.ts +3 -1
  350. package/dist/server-components/state.d.ts.map +1 -1
  351. package/dist/server-components/state.js +50 -38
  352. package/dist/server-components/state.js.map +1 -1
  353. package/dist/server-components/types.d.ts +9 -0
  354. package/dist/server-components/types.d.ts.map +1 -1
  355. package/dist/vite.d.ts +5 -1
  356. package/dist/vite.d.ts.map +1 -1
  357. package/dist/vite.js +58 -4
  358. package/dist/vite.js.map +1 -1
  359. package/package.json +48 -8
  360. package/src/boundary.ts +77 -5
  361. package/src/build-plan.ts +3 -0
  362. package/src/build-vite.ts +2 -0
  363. package/src/build.ts +19 -1
  364. package/src/client.ts +147 -10
  365. package/src/csp.ts +16 -2
  366. package/src/deferred.ts +11 -5
  367. package/src/dev-feed.ts +11 -2
  368. package/src/dev-session.ts +22 -11
  369. package/src/dev.ts +29 -8
  370. package/src/diagnostic-catalog.ts +53 -0
  371. package/src/diagnostic.ts +1 -0
  372. package/src/fn.ts +24 -7
  373. package/src/fs.ts +22 -8
  374. package/src/identity.ts +61 -0
  375. package/src/index.ts +19 -0
  376. package/src/internal/cdn/cloudflare.ts +2 -1
  377. package/src/internal/certified-components.ts +1 -1
  378. package/src/internal/codegen.ts +87 -8
  379. package/src/internal/dev-websocket.ts +230 -0
  380. package/src/internal/hole-runtime.ts +286 -0
  381. package/src/internal/identity-epoch.ts +235 -0
  382. package/src/internal/invalidation-contract.ts +35 -0
  383. package/src/internal/invalidation.ts +213 -0
  384. package/src/internal/island-bindings.ts +191 -0
  385. package/src/internal/island-list.ts +92 -0
  386. package/src/internal/island-mount.ts +68 -0
  387. package/src/internal/island-owner.ts +20 -0
  388. package/src/internal/island-resource.ts +76 -0
  389. package/src/internal/island-signals.ts +249 -0
  390. package/src/internal/keep-alive-table.ts +72 -0
  391. package/src/internal/live-contract.ts +106 -0
  392. package/src/internal/live-queries.ts +60 -0
  393. package/src/internal/page-events.ts +27 -0
  394. package/src/internal/page-execution.ts +579 -40
  395. package/src/internal/ppr/admission.ts +300 -0
  396. package/src/internal/ppr/canary.ts +122 -0
  397. package/src/internal/ppr/compile-shell.ts +379 -0
  398. package/src/internal/ppr/contract.ts +265 -0
  399. package/src/internal/ppr/frames.ts +170 -0
  400. package/src/internal/ppr/integration.ts +137 -0
  401. package/src/internal/ppr/options.ts +221 -0
  402. package/src/internal/ppr/route-rules.ts +77 -0
  403. package/src/internal/ppr/runtime.ts +36 -0
  404. package/src/internal/ppr/server.ts +1764 -0
  405. package/src/internal/ppr/shell-store.ts +139 -0
  406. package/src/internal/prerendered-pages.ts +7 -4
  407. package/src/internal/query-digest.ts +34 -0
  408. package/src/internal/query-runtime.ts +14 -1
  409. package/src/internal/query-seam.ts +48 -0
  410. package/src/internal/render-document.ts +149 -22
  411. package/src/internal/request-abort.ts +26 -0
  412. package/src/internal/server-boundary.ts +10 -1
  413. package/src/internal/share-data.ts +136 -0
  414. package/src/internal/store-signature.ts +149 -0
  415. package/src/internal/vite-plugin-compat.ts +2 -2
  416. package/src/internal/web-app.ts +121 -3
  417. package/src/islands-swap.ts +283 -0
  418. package/src/islands.ts +169 -72
  419. package/src/isr.ts +348 -106
  420. package/src/keep-alive.ts +1315 -0
  421. package/src/live/bus.ts +77 -0
  422. package/src/live/core.ts +52 -0
  423. package/src/live/history.ts +109 -0
  424. package/src/live/hub-protocol.ts +133 -0
  425. package/src/live/hub.ts +615 -0
  426. package/src/live/keyring.ts +379 -0
  427. package/src/live/read-capped.ts +36 -0
  428. package/src/live/revocations.ts +155 -0
  429. package/src/live/settings.ts +46 -0
  430. package/src/live-client.ts +973 -0
  431. package/src/live-hub.ts +658 -0
  432. package/src/live-redis.ts +259 -0
  433. package/src/live.ts +1096 -0
  434. package/src/manifest.ts +274 -6
  435. package/src/navigation.ts +14 -2
  436. package/src/plugins/vite-zone-guard.ts +15 -2
  437. package/src/ppr.ts +153 -0
  438. package/src/query.ts +147 -20
  439. package/src/render-seam.ts +35 -0
  440. package/src/route-types.ts +3 -1
  441. package/src/router.ts +494 -101
  442. package/src/server-components/build.ts +9 -3
  443. package/src/server-components/client.ts +89 -5
  444. package/src/server-components/hole.ts +23 -0
  445. package/src/server-components/html-policy.ts +63 -4
  446. package/src/server-components/html-scan.ts +159 -0
  447. package/src/server-components/html.ts +18 -4
  448. package/src/server-components/router-integration.ts +21 -2
  449. package/src/server-components/runtime.ts +112 -30
  450. package/src/server-components/state.ts +66 -44
  451. package/src/server-components/types.ts +16 -0
  452. package/src/vite.ts +75 -6
  453. package/dist/nano.d.ts +0 -125
  454. package/dist/nano.d.ts.map +0 -1
  455. package/dist/nano.js +0 -190
  456. package/dist/nano.js.map +0 -1
  457. package/src/nano.ts +0 -246
@@ -0,0 +1,379 @@
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
+ const KID_BYTES = 16
23
+ const NONCE_BYTES = 12
24
+ const GCM_TAG_BYTES = 16
25
+ const HOUR_MS = 3_600_000
26
+ const HOUR_SECONDS = 3600
27
+ const IAT_SKEW_SECONDS = 60
28
+ const SUBKEY_CACHE_SIZE = 1024
29
+ const TAG_BYTES = 16
30
+ /**
31
+ * Seals under one subkey before it refuses more. Random 96-bit nonces stay safe for about 2^32
32
+ * encryptions per key; an hourly per-process subkey at 10,000 grants a second sees 3.6e7.
33
+ */
34
+ export const MAX_SEALS_PER_SUBKEY = 2 ** 31
35
+
36
+ const INFO_SECRET_ID = TEXT.encode("nifra live secret id v1")
37
+ const INFO_GRANT = TEXT.encode("nifra live grant v1")
38
+ const INFO_TAG = TEXT.encode("nifra live tag v1")
39
+ const INFO_SUBJECT = TEXT.encode("nifra live subject v1")
40
+ const NO_SALT = new Uint8Array(0)
41
+
42
+ export interface GrantOwner {
43
+ /** The keys the owner's loader declared. */
44
+ readonly k: readonly string[]
45
+ /** The bus seq read before the owner's data was loaded. */
46
+ readonly w: number
47
+ }
48
+
49
+ export interface GrantClaims {
50
+ readonly v: 1
51
+ readonly sid: string
52
+ readonly sub: string | null
53
+ readonly path: string
54
+ readonly frame: string
55
+ readonly owners: Readonly<Record<string, GrantOwner>>
56
+ /** The seq read at issuance: revocations above it refuse the grant. */
57
+ readonly s: number
58
+ /** Renewals re-signed without loaders since the last load that ran them all. */
59
+ readonly r: number
60
+ readonly iat: number
61
+ readonly exp: number
62
+ readonly aud: string
63
+ }
64
+
65
+ export type GrantInput = Omit<GrantClaims, "v" | "iat" | "exp" | "aud">
66
+
67
+ export interface OpenedGrant {
68
+ readonly claims: GrantClaims
69
+ /** Index of the secret that decrypted it; that secret also derives the grant's tags. */
70
+ readonly secret: number
71
+ }
72
+
73
+ export interface Keyring {
74
+ readonly appId: string
75
+ /** Grant lifetime in seconds. */
76
+ readonly ttl: number
77
+ readonly secretCount: number
78
+ /** Each secret's public 32-bit id, newest first: a token names its secret by it. */
79
+ readonly secretIds: readonly number[]
80
+ /**
81
+ * Encrypt under the first secret; `undefined` when the token would exceed {@link MAX_GRANT_CHARS} or
82
+ * the hour's subkey reached {@link MAX_SEALS_PER_SUBKEY}.
83
+ */
84
+ seal(input: GrantInput, nowMs: number): Promise<string | undefined>
85
+ /** Authenticate and decrypt, or `undefined` for anything not issued by this keyring and still valid. */
86
+ open(token: unknown, nowMs: number): Promise<OpenedGrant | undefined>
87
+ /** The 128-bit wire tag of a key under one secret (default: the first). */
88
+ tag(key: string, secret?: number): Promise<string>
89
+ /**
90
+ * The subject's stable opaque tag under each listed secret, newest first. Across a rotation the
91
+ * same viewer keeps one tag in common, whichever process served the load.
92
+ */
93
+ subjectTags(subject: string | null): Promise<string[]>
94
+ }
95
+
96
+ /** A rotation list: newest first, never empty. */
97
+ export type LiveSecrets = readonly [string, ...string[]]
98
+
99
+ export interface KeyringOptions {
100
+ readonly secrets: LiveSecrets
101
+ readonly appId: string
102
+ readonly ttl: number
103
+ /** Fixed per process; random when omitted. Tests pin it. */
104
+ readonly processId?: Uint8Array
105
+ /** Tests lower it; see {@link MAX_SEALS_PER_SUBKEY}. */
106
+ readonly maxSealsPerSubkey?: number
107
+ }
108
+
109
+ interface SecretEntry {
110
+ readonly index: number
111
+ readonly base: CryptoKey
112
+ readonly id: Uint8Array<ArrayBuffer>
113
+ readonly idKey: number
114
+ readonly tagKey: CryptoKey
115
+ readonly subjectKey: CryptoKey
116
+ }
117
+
118
+ /** Synchronous boot checks: count and strength. Duplicate ids need derivation, see {@link createKeyring}. */
119
+ export function assertLiveSecrets(secrets: readonly unknown[]): asserts secrets is LiveSecrets {
120
+ if (secrets.length === 0) throw new TypeError("[nifra/web] live: `secret` lists no secret")
121
+ if (secrets.length > MAX_SECRETS) {
122
+ throw new TypeError(
123
+ `[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.`,
124
+ )
125
+ }
126
+ for (const secret of secrets) {
127
+ const length = typeof secret === "string" ? TEXT.encode(secret).length : -1
128
+ if (length < MIN_SECRET_BYTES) {
129
+ throw new TypeError(
130
+ `[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`,
131
+ )
132
+ }
133
+ }
134
+ }
135
+
136
+ /** Derive every key a secret needs. Rejects when two secrets share a `secretId`. */
137
+ export async function createKeyring(options: KeyringOptions): Promise<Keyring> {
138
+ assertLiveSecrets(options.secrets)
139
+ const appSalt = TEXT.encode(options.appId)
140
+ const processId = options.processId ?? crypto.getRandomValues(new Uint8Array(8))
141
+ if (processId.length !== 8) throw new TypeError("[nifra/web] live: processId must be 8 bytes")
142
+ const derive = async (secret: string, index: number): Promise<SecretEntry> => {
143
+ const base = await crypto.subtle.importKey("raw", TEXT.encode(secret), "HKDF", false, [
144
+ "deriveBits",
145
+ "deriveKey",
146
+ ])
147
+ const id = new Uint8Array(await hkdf(base, NO_SALT, INFO_SECRET_ID, 32)).slice(0, 4)
148
+ const tagKey = await hmacKey(new Uint8Array(await hkdf(base, appSalt, INFO_TAG, 32)))
149
+ const subjectKey = await hmacKey(new Uint8Array(await hkdf(base, appSalt, INFO_SUBJECT, 32)))
150
+ return { index, base, id, idKey: readU32(id, 0), tagKey, subjectKey }
151
+ }
152
+ const [head, ...rest] = options.secrets
153
+ const first = await derive(head, 0)
154
+ const entries = [first, ...(await Promise.all(rest.map((secret, i) => derive(secret, i + 1))))]
155
+ const byId = new Map<number, SecretEntry>()
156
+ for (const entry of entries) {
157
+ if (byId.has(entry.idKey)) {
158
+ throw new TypeError(
159
+ "[nifra/web] live: two configured secrets share a secret id; replace one of them (a repeated secret does this too)",
160
+ )
161
+ }
162
+ byId.set(entry.idKey, entry)
163
+ }
164
+
165
+ // Least recently used first: a hit is moved to the end. Only a kid that sealed or opened a grant
166
+ // is kept, so forged kids cannot push out the subkeys real grants use.
167
+ const subkeys = new Map<string, Promise<CryptoKey>>()
168
+ const cachedSubkey = (name: string): Promise<CryptoKey> | undefined => {
169
+ const cached = subkeys.get(name)
170
+ if (cached !== undefined) {
171
+ subkeys.delete(name)
172
+ subkeys.set(name, cached)
173
+ }
174
+ return cached
175
+ }
176
+ const remember = (name: string, key: Promise<CryptoKey>): void => {
177
+ if (subkeys.has(name)) return
178
+ if (subkeys.size >= SUBKEY_CACHE_SIZE) {
179
+ const oldest = subkeys.keys().next().value
180
+ if (oldest !== undefined) subkeys.delete(oldest)
181
+ }
182
+ subkeys.set(name, key)
183
+ }
184
+ const deriveSubkey = (entry: SecretEntry, kid: Uint8Array<ArrayBuffer>): Promise<CryptoKey> =>
185
+ crypto.subtle.deriveKey(
186
+ { name: "HKDF", hash: "SHA-256", salt: kid, info: INFO_GRANT },
187
+ entry.base,
188
+ { name: "AES-GCM", length: 256 },
189
+ false,
190
+ ["encrypt", "decrypt"],
191
+ )
192
+
193
+ const ttl = options.ttl
194
+ const appId = options.appId
195
+ const maxSeals = options.maxSealsPerSubkey ?? MAX_SEALS_PER_SUBKEY
196
+ // The subkey `seal` uses changes with the hour, and only `seal` encrypts.
197
+ let sealHour = -1
198
+ let seals = 0
199
+
200
+ return {
201
+ appId,
202
+ ttl,
203
+ secretCount: entries.length,
204
+ secretIds: entries.map((entry) => entry.idKey),
205
+ async seal(input, nowMs) {
206
+ const iat = Math.floor(nowMs / 1000)
207
+ const hour = Math.floor(iat / HOUR_SECONDS)
208
+ if (hour !== sealHour) {
209
+ sealHour = hour
210
+ seals = 0
211
+ }
212
+ if (seals >= maxSeals) return undefined
213
+ seals++
214
+ const claims: GrantClaims = { v: 1, ...input, iat, exp: iat + ttl, aud: appId }
215
+ const kid = new Uint8Array(KID_BYTES)
216
+ kid.set(first.id, 0)
217
+ kid.set(processId, 4)
218
+ writeU32(kid, 12, hour)
219
+ const nonce = crypto.getRandomValues(new Uint8Array(NONCE_BYTES))
220
+ const name = latin1(kid)
221
+ let key = cachedSubkey(name)
222
+ if (key === undefined) {
223
+ key = deriveSubkey(first, kid)
224
+ remember(name, key)
225
+ }
226
+ const sealed = new Uint8Array(
227
+ await crypto.subtle.encrypt(
228
+ { name: "AES-GCM", iv: nonce, additionalData: kid, tagLength: GCM_TAG_BYTES * 8 },
229
+ await key,
230
+ TEXT.encode(JSON.stringify(claims)),
231
+ ),
232
+ )
233
+ const token = new Uint8Array(KID_BYTES + NONCE_BYTES + sealed.length)
234
+ token.set(kid, 0)
235
+ token.set(nonce, KID_BYTES)
236
+ token.set(sealed, KID_BYTES + NONCE_BYTES)
237
+ const encoded = toBase64Url(token)
238
+ return encoded.length > MAX_GRANT_CHARS ? undefined : encoded
239
+ },
240
+ async open(token, nowMs) {
241
+ // Size and framing first, then the secret id and hour: none of these refusals costs any crypto.
242
+ if (typeof token !== "string" || token.length > MAX_GRANT_CHARS) return undefined
243
+ const bytes = fromBase64Url(token)
244
+ if (bytes === null || bytes.length < KID_BYTES + NONCE_BYTES + GCM_TAG_BYTES) return undefined
245
+ const entry = byId.get(readU32(bytes, 0))
246
+ if (entry === undefined) return undefined
247
+ const hour = readU32(bytes, 12)
248
+ const nowHour = Math.floor(nowMs / HOUR_MS)
249
+ if (hour !== nowHour && hour !== nowHour - 1) return undefined
250
+ const kid = bytes.slice(0, KID_BYTES)
251
+ const name = latin1(kid)
252
+ const cached = cachedSubkey(name)
253
+ const key = cached ?? deriveSubkey(entry, kid)
254
+ let plain: ArrayBuffer
255
+ try {
256
+ plain = await crypto.subtle.decrypt(
257
+ {
258
+ name: "AES-GCM",
259
+ iv: bytes.slice(KID_BYTES, KID_BYTES + NONCE_BYTES),
260
+ additionalData: kid,
261
+ tagLength: GCM_TAG_BYTES * 8,
262
+ },
263
+ await key,
264
+ bytes.slice(KID_BYTES + NONCE_BYTES),
265
+ )
266
+ } catch {
267
+ return undefined
268
+ }
269
+ if (cached === undefined) remember(name, key)
270
+ let claims: unknown
271
+ try {
272
+ claims = JSON.parse(UTF8.decode(plain))
273
+ } catch {
274
+ return undefined
275
+ }
276
+ return validClaims(claims, appId, ttl, hour, Math.floor(nowMs / 1000))
277
+ ? { claims, secret: entry.index }
278
+ : undefined
279
+ },
280
+ async tag(key, secret = 0) {
281
+ const tagKey = (entries[secret] ?? first).tagKey
282
+ const mac = await crypto.subtle.sign("HMAC", tagKey, TEXT.encode(key))
283
+ return toBase64Url(new Uint8Array(mac).slice(0, TAG_BYTES))
284
+ },
285
+ subjectTags(subject) {
286
+ // A prefix per kind: no subject string can collide with the anonymous viewer's input.
287
+ const input = TEXT.encode(subject === null ? "a" : `s${subject}`)
288
+ return Promise.all(
289
+ entries.map(async (entry) =>
290
+ toBase64Url(
291
+ new Uint8Array(await crypto.subtle.sign("HMAC", entry.subjectKey, input)).slice(
292
+ 0,
293
+ TAG_BYTES,
294
+ ),
295
+ ),
296
+ ),
297
+ )
298
+ },
299
+ }
300
+ }
301
+
302
+ function validClaims(
303
+ value: unknown,
304
+ appId: string,
305
+ ttl: number,
306
+ hour: number,
307
+ nowSeconds: number,
308
+ ): value is GrantClaims {
309
+ if (!isRecord(value)) return false
310
+ const { v, sid, sub, path, frame, owners, s, r, iat, exp, aud } = value
311
+ if (v !== 1 || aud !== appId) return false
312
+ if (typeof sid !== "string" || !SID_PATTERN.test(sid)) return false
313
+ if (sub !== null && typeof sub !== "string") return false
314
+ if (typeof path !== "string" || typeof frame !== "string") return false
315
+ if (!isCount(s) || !isCount(r) || !isInteger(iat) || !isInteger(exp)) return false
316
+ if (iat > nowSeconds + IAT_SKEW_SECONDS) return false
317
+ if (Math.floor(iat / HOUR_SECONDS) !== hour) return false
318
+ if (exp - iat !== ttl || exp <= nowSeconds) return false
319
+ if (!isRecord(owners)) return false
320
+ let keys = 0
321
+ for (const owner of Object.values(owners)) {
322
+ if (!isRecord(owner) || !isCount(owner.w) || !Array.isArray(owner.k)) return false
323
+ for (const key of owner.k) if (typeof key !== "string" || !KEY_PATTERN.test(key)) return false
324
+ keys += owner.k.length
325
+ }
326
+ return keys <= MAX_KEYS_PER_FRAME
327
+ }
328
+
329
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
330
+ typeof value === "object" && value !== null && !Array.isArray(value)
331
+
332
+ const isInteger = (value: unknown): value is number =>
333
+ typeof value === "number" && Number.isSafeInteger(value)
334
+
335
+ const isCount = (value: unknown): value is number => isInteger(value) && value >= 0
336
+
337
+ const hkdf = (
338
+ base: CryptoKey,
339
+ salt: Uint8Array<ArrayBuffer>,
340
+ info: Uint8Array<ArrayBuffer>,
341
+ bytes: number,
342
+ ): Promise<ArrayBuffer> =>
343
+ crypto.subtle.deriveBits({ name: "HKDF", hash: "SHA-256", salt, info }, base, bytes * 8)
344
+
345
+ const hmacKey = (raw: Uint8Array<ArrayBuffer>): Promise<CryptoKey> =>
346
+ crypto.subtle.importKey("raw", raw, { name: "HMAC", hash: "SHA-256" }, false, ["sign"])
347
+
348
+ const view = (bytes: Uint8Array): DataView =>
349
+ new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
350
+
351
+ const readU32 = (bytes: Uint8Array, at: number): number => view(bytes).getUint32(at)
352
+
353
+ const writeU32 = (bytes: Uint8Array, at: number, value: number): void =>
354
+ view(bytes).setUint32(at, value)
355
+
356
+ const latin1 = (bytes: Uint8Array): string => {
357
+ let out = ""
358
+ for (const byte of bytes) out += String.fromCharCode(byte)
359
+ return out
360
+ }
361
+
362
+ export const toBase64Url = (bytes: Uint8Array): string =>
363
+ btoa(latin1(bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")
364
+
365
+ const BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
366
+ const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/
367
+
368
+ /** Canonical encodings only: a second spelling of the same bytes is refused. */
369
+ function fromBase64Url(s: string): Uint8Array<ArrayBuffer> | null {
370
+ if (s.length === 0 || s.length % 4 === 1 || !BASE64URL_PATTERN.test(s)) return null
371
+ const remainder = s.length % 4
372
+ const last = BASE64URL_ALPHABET.indexOf(s.charAt(s.length - 1))
373
+ if ((remainder === 2 && (last & 0x0f) !== 0) || (remainder === 3 && (last & 0x03) !== 0))
374
+ return null
375
+ const bin = atob(s.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - remainder) % 4))
376
+ const bytes = new Uint8Array(bin.length)
377
+ for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i)
378
+ return bytes
379
+ }
@@ -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
+ }