@astrale-os/shell-react 0.1.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 (378) hide show
  1. package/LICENSE +202 -0
  2. package/dist/application/apps/app-host.d.ts +31 -0
  3. package/dist/application/apps/app-host.d.ts.map +1 -0
  4. package/dist/application/apps/app-host.js +59 -0
  5. package/dist/application/apps/app-logo.d.ts +19 -0
  6. package/dist/application/apps/app-logo.d.ts.map +1 -0
  7. package/dist/application/apps/app-logo.js +31 -0
  8. package/dist/application/apps/apps.hook.d.ts +12 -0
  9. package/dist/application/apps/apps.hook.d.ts.map +1 -0
  10. package/dist/application/apps/apps.hook.js +51 -0
  11. package/dist/application/apps/open-app.hook.d.ts +10 -0
  12. package/dist/application/apps/open-app.hook.d.ts.map +1 -0
  13. package/dist/application/apps/open-app.hook.js +7 -0
  14. package/dist/application/index.d.ts +18 -0
  15. package/dist/application/index.d.ts.map +1 -0
  16. package/dist/application/index.js +12 -0
  17. package/dist/application/roles.hook.d.ts +7 -0
  18. package/dist/application/roles.hook.d.ts.map +1 -0
  19. package/dist/application/roles.hook.js +23 -0
  20. package/dist/application/spaces/current-space.hook.d.ts +7 -0
  21. package/dist/application/spaces/current-space.hook.d.ts.map +1 -0
  22. package/dist/application/spaces/current-space.hook.js +27 -0
  23. package/dist/application/spaces/current-space.store.d.ts +32 -0
  24. package/dist/application/spaces/current-space.store.d.ts.map +1 -0
  25. package/dist/application/spaces/current-space.store.js +79 -0
  26. package/dist/application/spaces/space.hook.d.ts +5 -0
  27. package/dist/application/spaces/space.hook.d.ts.map +1 -0
  28. package/dist/application/spaces/space.hook.js +25 -0
  29. package/dist/application/spaces/spaces.hook.d.ts +14 -0
  30. package/dist/application/spaces/spaces.hook.d.ts.map +1 -0
  31. package/dist/application/spaces/spaces.hook.js +82 -0
  32. package/dist/application/user/user.hook.d.ts +10 -0
  33. package/dist/application/user/user.hook.d.ts.map +1 -0
  34. package/dist/application/user/user.hook.js +19 -0
  35. package/dist/application/user/user.store.d.ts +35 -0
  36. package/dist/application/user/user.store.d.ts.map +1 -0
  37. package/dist/application/user/user.store.js +51 -0
  38. package/dist/auth/act-as.context.d.ts +40 -0
  39. package/dist/auth/act-as.context.d.ts.map +1 -0
  40. package/dist/auth/act-as.context.js +84 -0
  41. package/dist/auth/auth.hook.d.ts +3 -0
  42. package/dist/auth/auth.hook.d.ts.map +1 -0
  43. package/dist/auth/auth.hook.js +25 -0
  44. package/dist/auth/can.hook.d.ts +40 -0
  45. package/dist/auth/can.hook.d.ts.map +1 -0
  46. package/dist/auth/can.hook.js +119 -0
  47. package/dist/auth/index.d.ts +6 -0
  48. package/dist/auth/index.d.ts.map +1 -0
  49. package/dist/auth/index.js +5 -0
  50. package/dist/auth/self.hook.d.ts +3 -0
  51. package/dist/auth/self.hook.d.ts.map +1 -0
  52. package/dist/auth/self.hook.js +11 -0
  53. package/dist/auth/session-kernel.d.ts +4 -0
  54. package/dist/auth/session-kernel.d.ts.map +1 -0
  55. package/dist/auth/session-kernel.js +21 -0
  56. package/dist/graph/action.d.ts +22 -0
  57. package/dist/graph/action.d.ts.map +1 -0
  58. package/dist/graph/action.js +85 -0
  59. package/dist/graph/async.d.ts +11 -0
  60. package/dist/graph/async.d.ts.map +1 -0
  61. package/dist/graph/async.js +60 -0
  62. package/dist/graph/call/call.hook.d.ts +21 -0
  63. package/dist/graph/call/call.hook.d.ts.map +1 -0
  64. package/dist/graph/call/call.hook.js +23 -0
  65. package/dist/graph/call/stream.hook.d.ts +28 -0
  66. package/dist/graph/call/stream.hook.d.ts.map +1 -0
  67. package/dist/graph/call/stream.hook.js +137 -0
  68. package/dist/graph/graph.context.d.ts +4 -0
  69. package/dist/graph/graph.context.d.ts.map +1 -0
  70. package/dist/graph/graph.context.js +10 -0
  71. package/dist/graph/guards.error.d.ts +9 -0
  72. package/dist/graph/guards.error.d.ts.map +1 -0
  73. package/dist/graph/guards.error.js +7 -0
  74. package/dist/graph/index.d.ts +20 -0
  75. package/dist/graph/index.d.ts.map +1 -0
  76. package/dist/graph/index.js +9 -0
  77. package/dist/graph/memory/base.d.ts +21 -0
  78. package/dist/graph/memory/base.d.ts.map +1 -0
  79. package/dist/graph/memory/base.js +33 -0
  80. package/dist/graph/memory/read/envelope.d.ts +71 -0
  81. package/dist/graph/memory/read/envelope.d.ts.map +1 -0
  82. package/dist/graph/memory/read/envelope.js +29 -0
  83. package/dist/graph/memory/read/idle.d.ts +18 -0
  84. package/dist/graph/memory/read/idle.d.ts.map +1 -0
  85. package/dist/graph/memory/read/idle.js +16 -0
  86. package/dist/graph/memory/read/read.d.ts +9 -0
  87. package/dist/graph/memory/read/read.d.ts.map +1 -0
  88. package/dist/graph/memory/read/read.js +87 -0
  89. package/dist/graph/memory/scope.d.ts +7 -0
  90. package/dist/graph/memory/scope.d.ts.map +1 -0
  91. package/dist/graph/memory/scope.js +25 -0
  92. package/dist/graph/mutate/move.hook.d.ts +7 -0
  93. package/dist/graph/mutate/move.hook.d.ts.map +1 -0
  94. package/dist/graph/mutate/move.hook.js +49 -0
  95. package/dist/graph/mutate/mutate.hook.d.ts +17 -0
  96. package/dist/graph/mutate/mutate.hook.d.ts.map +1 -0
  97. package/dist/graph/mutate/mutate.hook.js +45 -0
  98. package/dist/graph/query/children.hook.d.ts +29 -0
  99. package/dist/graph/query/children.hook.d.ts.map +1 -0
  100. package/dist/graph/query/children.hook.js +64 -0
  101. package/dist/graph/query/graph.hook.d.ts +9 -0
  102. package/dist/graph/query/graph.hook.d.ts.map +1 -0
  103. package/dist/graph/query/graph.hook.js +5 -0
  104. package/dist/graph/query/neighbors.hook.d.ts +7 -0
  105. package/dist/graph/query/neighbors.hook.d.ts.map +1 -0
  106. package/dist/graph/query/neighbors.hook.js +79 -0
  107. package/dist/graph/query/node.hook.d.ts +14 -0
  108. package/dist/graph/query/node.hook.d.ts.map +1 -0
  109. package/dist/graph/query/node.hook.js +43 -0
  110. package/dist/graph/query/query.hook.d.ts +14 -0
  111. package/dist/graph/query/query.hook.d.ts.map +1 -0
  112. package/dist/graph/query/query.hook.js +23 -0
  113. package/dist/index.d.ts +43 -0
  114. package/dist/index.d.ts.map +1 -0
  115. package/dist/index.js +34 -0
  116. package/dist/intents/envelope.d.ts +33 -0
  117. package/dist/intents/envelope.d.ts.map +1 -0
  118. package/dist/intents/envelope.js +19 -0
  119. package/dist/intents/index.d.ts +6 -0
  120. package/dist/intents/index.d.ts.map +1 -0
  121. package/dist/intents/index.js +3 -0
  122. package/dist/intents/listen-intent.hook.d.ts +16 -0
  123. package/dist/intents/listen-intent.hook.d.ts.map +1 -0
  124. package/dist/intents/listen-intent.hook.js +75 -0
  125. package/dist/intents/reply.d.ts +9 -0
  126. package/dist/intents/reply.d.ts.map +1 -0
  127. package/dist/intents/reply.js +14 -0
  128. package/dist/intents/send-intent.hook.d.ts +12 -0
  129. package/dist/intents/send-intent.hook.d.ts.map +1 -0
  130. package/dist/intents/send-intent.hook.js +86 -0
  131. package/dist/navigation/adapters/adapter.d.ts +15 -0
  132. package/dist/navigation/adapters/adapter.d.ts.map +1 -0
  133. package/dist/navigation/adapters/adapter.js +1 -0
  134. package/dist/navigation/adapters/memory.adapter.d.ts +9 -0
  135. package/dist/navigation/adapters/memory.adapter.d.ts.map +1 -0
  136. package/dist/navigation/adapters/memory.adapter.js +15 -0
  137. package/dist/navigation/adapters/url.adapter.d.ts +6 -0
  138. package/dist/navigation/adapters/url.adapter.d.ts.map +1 -0
  139. package/dist/navigation/adapters/url.adapter.js +59 -0
  140. package/dist/navigation/history.d.ts +63 -0
  141. package/dist/navigation/history.d.ts.map +1 -0
  142. package/dist/navigation/history.js +144 -0
  143. package/dist/navigation/hooks/ancestors.hook.d.ts +13 -0
  144. package/dist/navigation/hooks/ancestors.hook.d.ts.map +1 -0
  145. package/dist/navigation/hooks/ancestors.hook.js +56 -0
  146. package/dist/navigation/hooks/location.hook.d.ts +4 -0
  147. package/dist/navigation/hooks/location.hook.d.ts.map +1 -0
  148. package/dist/navigation/hooks/location.hook.js +17 -0
  149. package/dist/navigation/hooks/navigate.hook.d.ts +12 -0
  150. package/dist/navigation/hooks/navigate.hook.d.ts.map +1 -0
  151. package/dist/navigation/hooks/navigate.hook.js +33 -0
  152. package/dist/navigation/hooks/scope.hook.d.ts +12 -0
  153. package/dist/navigation/hooks/scope.hook.d.ts.map +1 -0
  154. package/dist/navigation/hooks/scope.hook.js +21 -0
  155. package/dist/navigation/index.d.ts +16 -0
  156. package/dist/navigation/index.d.ts.map +1 -0
  157. package/dist/navigation/index.js +10 -0
  158. package/dist/navigation/link.d.ts +11 -0
  159. package/dist/navigation/link.d.ts.map +1 -0
  160. package/dist/navigation/link.js +62 -0
  161. package/dist/navigation/location.d.ts +48 -0
  162. package/dist/navigation/location.d.ts.map +1 -0
  163. package/dist/navigation/location.js +88 -0
  164. package/dist/navigation/scope.context.d.ts +64 -0
  165. package/dist/navigation/scope.context.d.ts.map +1 -0
  166. package/dist/navigation/scope.context.js +178 -0
  167. package/dist/navigation/snapshot.d.ts +22 -0
  168. package/dist/navigation/snapshot.d.ts.map +1 -0
  169. package/dist/navigation/snapshot.js +65 -0
  170. package/dist/schema/bind-read.d.ts +37 -0
  171. package/dist/schema/bind-read.d.ts.map +1 -0
  172. package/dist/schema/bind-read.js +205 -0
  173. package/dist/schema/binding.d.ts +21 -0
  174. package/dist/schema/binding.d.ts.map +1 -0
  175. package/dist/schema/binding.js +55 -0
  176. package/dist/schema/domain.hook.d.ts +10 -0
  177. package/dist/schema/domain.hook.d.ts.map +1 -0
  178. package/dist/schema/domain.hook.js +7 -0
  179. package/dist/schema/function.hook.d.ts +9 -0
  180. package/dist/schema/function.hook.d.ts.map +1 -0
  181. package/dist/schema/function.hook.js +40 -0
  182. package/dist/schema/index.d.ts +10 -0
  183. package/dist/schema/index.d.ts.map +1 -0
  184. package/dist/schema/index.js +8 -0
  185. package/dist/schema/kernel.hook.d.ts +10 -0
  186. package/dist/schema/kernel.hook.d.ts.map +1 -0
  187. package/dist/schema/kernel.hook.js +8 -0
  188. package/dist/schema/method.hook.d.ts +14 -0
  189. package/dist/schema/method.hook.d.ts.map +1 -0
  190. package/dist/schema/method.hook.js +49 -0
  191. package/dist/schema/tag.d.ts +11 -0
  192. package/dist/schema/tag.d.ts.map +1 -0
  193. package/dist/schema/tag.js +11 -0
  194. package/dist/schema/types.d.ts +73 -0
  195. package/dist/schema/types.d.ts.map +1 -0
  196. package/dist/schema/types.js +1 -0
  197. package/dist/schema/write.d.ts +15 -0
  198. package/dist/schema/write.d.ts.map +1 -0
  199. package/dist/schema/write.js +45 -0
  200. package/dist/selection/index.d.ts +4 -0
  201. package/dist/selection/index.d.ts.map +1 -0
  202. package/dist/selection/index.js +4 -0
  203. package/dist/selection/live.d.ts +21 -0
  204. package/dist/selection/live.d.ts.map +1 -0
  205. package/dist/selection/live.js +116 -0
  206. package/dist/selection/scope.context.d.ts +26 -0
  207. package/dist/selection/scope.context.d.ts.map +1 -0
  208. package/dist/selection/scope.context.js +100 -0
  209. package/dist/selection/selection.hook.d.ts +4 -0
  210. package/dist/selection/selection.hook.d.ts.map +1 -0
  211. package/dist/selection/selection.hook.js +14 -0
  212. package/dist/session/boot/boot.d.ts +19 -0
  213. package/dist/session/boot/boot.d.ts.map +1 -0
  214. package/dist/session/boot/boot.js +45 -0
  215. package/dist/session/boot/ownership.d.ts +18 -0
  216. package/dist/session/boot/ownership.d.ts.map +1 -0
  217. package/dist/session/boot/ownership.js +39 -0
  218. package/dist/session/boot/status.d.ts +24 -0
  219. package/dist/session/boot/status.d.ts.map +1 -0
  220. package/dist/session/boot/status.js +11 -0
  221. package/dist/session/connection/connection.hook.d.ts +3 -0
  222. package/dist/session/connection/connection.hook.d.ts.map +1 -0
  223. package/dist/session/connection/connection.hook.js +12 -0
  224. package/dist/session/connection/connection.store.d.ts +24 -0
  225. package/dist/session/connection/connection.store.d.ts.map +1 -0
  226. package/dist/session/connection/connection.store.js +31 -0
  227. package/dist/session/index.d.ts +8 -0
  228. package/dist/session/index.d.ts.map +1 -0
  229. package/dist/session/index.js +6 -0
  230. package/dist/session/kernel-client.hook.d.ts +9 -0
  231. package/dist/session/kernel-client.hook.d.ts.map +1 -0
  232. package/dist/session/kernel-client.hook.js +5 -0
  233. package/dist/session/kernel-instance.hook.d.ts +9 -0
  234. package/dist/session/kernel-instance.hook.d.ts.map +1 -0
  235. package/dist/session/kernel-instance.hook.js +10 -0
  236. package/dist/session/provider.d.ts +31 -0
  237. package/dist/session/provider.d.ts.map +1 -0
  238. package/dist/session/provider.js +165 -0
  239. package/dist/session/session.context.d.ts +18 -0
  240. package/dist/session/session.context.d.ts.map +1 -0
  241. package/dist/session/session.context.js +8 -0
  242. package/dist/session/shell.hook.d.ts +4 -0
  243. package/dist/session/shell.hook.d.ts.map +1 -0
  244. package/dist/session/shell.hook.js +15 -0
  245. package/dist/window/adopt.d.ts +27 -0
  246. package/dist/window/adopt.d.ts.map +1 -0
  247. package/dist/window/adopt.js +27 -0
  248. package/dist/window/hooks/close-guard.hook.d.ts +6 -0
  249. package/dist/window/hooks/close-guard.hook.d.ts.map +1 -0
  250. package/dist/window/hooks/close-guard.hook.js +90 -0
  251. package/dist/window/hooks/target-node.hook.d.ts +7 -0
  252. package/dist/window/hooks/target-node.hook.d.ts.map +1 -0
  253. package/dist/window/hooks/target-node.hook.js +77 -0
  254. package/dist/window/hooks/views-for.hook.d.ts +8 -0
  255. package/dist/window/hooks/views-for.hook.d.ts.map +1 -0
  256. package/dist/window/hooks/views-for.hook.js +51 -0
  257. package/dist/window/hooks/window.hook.d.ts +4 -0
  258. package/dist/window/hooks/window.hook.d.ts.map +1 -0
  259. package/dist/window/hooks/window.hook.js +14 -0
  260. package/dist/window/hooks/windows.hook.d.ts +15 -0
  261. package/dist/window/hooks/windows.hook.d.ts.map +1 -0
  262. package/dist/window/hooks/windows.hook.js +25 -0
  263. package/dist/window/host.d.ts +31 -0
  264. package/dist/window/host.d.ts.map +1 -0
  265. package/dist/window/host.js +270 -0
  266. package/dist/window/hub.d.ts +41 -0
  267. package/dist/window/hub.d.ts.map +1 -0
  268. package/dist/window/hub.js +100 -0
  269. package/dist/window/index.d.ts +15 -0
  270. package/dist/window/index.d.ts.map +1 -0
  271. package/dist/window/index.js +10 -0
  272. package/dist/window/lifecycle/mount.d.ts +47 -0
  273. package/dist/window/lifecycle/mount.d.ts.map +1 -0
  274. package/dist/window/lifecycle/mount.js +92 -0
  275. package/dist/window/lifecycle/states.d.ts +33 -0
  276. package/dist/window/lifecycle/states.d.ts.map +1 -0
  277. package/dist/window/lifecycle/states.js +7 -0
  278. package/dist/window/placement.d.ts +33 -0
  279. package/dist/window/placement.d.ts.map +1 -0
  280. package/dist/window/placement.js +57 -0
  281. package/dist/window/stage.d.ts +3 -0
  282. package/dist/window/stage.d.ts.map +1 -0
  283. package/dist/window/stage.js +41 -0
  284. package/package.json +59 -0
  285. package/src/application/apps/app-host.tsx +113 -0
  286. package/src/application/apps/app-logo.tsx +56 -0
  287. package/src/application/apps/apps.hook.ts +81 -0
  288. package/src/application/apps/open-app.hook.ts +18 -0
  289. package/src/application/index.ts +22 -0
  290. package/src/application/roles.hook.ts +41 -0
  291. package/src/application/spaces/current-space.hook.ts +43 -0
  292. package/src/application/spaces/current-space.store.ts +110 -0
  293. package/src/application/spaces/space.hook.ts +41 -0
  294. package/src/application/spaces/spaces.hook.ts +113 -0
  295. package/src/application/user/user.hook.ts +27 -0
  296. package/src/application/user/user.store.ts +85 -0
  297. package/src/auth/act-as.context.tsx +124 -0
  298. package/src/auth/auth.hook.ts +36 -0
  299. package/src/auth/can.hook.ts +149 -0
  300. package/src/auth/index.ts +6 -0
  301. package/src/auth/self.hook.ts +14 -0
  302. package/src/auth/session-kernel.ts +24 -0
  303. package/src/graph/action.ts +127 -0
  304. package/src/graph/async.ts +87 -0
  305. package/src/graph/call/call.hook.ts +42 -0
  306. package/src/graph/call/stream.hook.ts +200 -0
  307. package/src/graph/graph.context.ts +14 -0
  308. package/src/graph/guards.error.ts +58 -0
  309. package/src/graph/index.ts +20 -0
  310. package/src/graph/memory/base.ts +50 -0
  311. package/src/graph/memory/read/envelope.ts +98 -0
  312. package/src/graph/memory/read/idle.ts +24 -0
  313. package/src/graph/memory/read/read.ts +103 -0
  314. package/src/graph/memory/scope.ts +29 -0
  315. package/src/graph/mutate/move.hook.ts +70 -0
  316. package/src/graph/mutate/mutate.hook.ts +69 -0
  317. package/src/graph/query/children.hook.ts +136 -0
  318. package/src/graph/query/graph.hook.ts +13 -0
  319. package/src/graph/query/neighbors.hook.ts +124 -0
  320. package/src/graph/query/node.hook.ts +83 -0
  321. package/src/graph/query/query.hook.ts +42 -0
  322. package/src/index.ts +202 -0
  323. package/src/intents/envelope.ts +52 -0
  324. package/src/intents/index.ts +6 -0
  325. package/src/intents/listen-intent.hook.ts +92 -0
  326. package/src/intents/reply.ts +24 -0
  327. package/src/intents/send-intent.hook.ts +111 -0
  328. package/src/navigation/adapters/adapter.ts +13 -0
  329. package/src/navigation/adapters/memory.adapter.ts +22 -0
  330. package/src/navigation/adapters/url.adapter.ts +61 -0
  331. package/src/navigation/history.ts +170 -0
  332. package/src/navigation/hooks/ancestors.hook.ts +71 -0
  333. package/src/navigation/hooks/location.hook.ts +21 -0
  334. package/src/navigation/hooks/navigate.hook.ts +43 -0
  335. package/src/navigation/hooks/scope.hook.ts +34 -0
  336. package/src/navigation/index.ts +17 -0
  337. package/src/navigation/link.tsx +110 -0
  338. package/src/navigation/location.ts +122 -0
  339. package/src/navigation/scope.context.tsx +267 -0
  340. package/src/navigation/snapshot.ts +79 -0
  341. package/src/schema/bind-read.ts +319 -0
  342. package/src/schema/binding.ts +91 -0
  343. package/src/schema/domain.hook.ts +16 -0
  344. package/src/schema/function.hook.ts +68 -0
  345. package/src/schema/index.ts +25 -0
  346. package/src/schema/kernel.hook.ts +17 -0
  347. package/src/schema/method.hook.ts +103 -0
  348. package/src/schema/tag.ts +19 -0
  349. package/src/schema/types.ts +150 -0
  350. package/src/schema/write.ts +80 -0
  351. package/src/selection/index.ts +5 -0
  352. package/src/selection/live.ts +142 -0
  353. package/src/selection/scope.context.tsx +151 -0
  354. package/src/selection/selection.hook.ts +18 -0
  355. package/src/session/boot/boot.ts +57 -0
  356. package/src/session/boot/ownership.ts +54 -0
  357. package/src/session/boot/status.ts +32 -0
  358. package/src/session/connection/connection.hook.ts +16 -0
  359. package/src/session/connection/connection.store.ts +58 -0
  360. package/src/session/index.ts +8 -0
  361. package/src/session/kernel-client.hook.ts +13 -0
  362. package/src/session/kernel-instance.hook.ts +22 -0
  363. package/src/session/provider.tsx +205 -0
  364. package/src/session/session.context.ts +27 -0
  365. package/src/session/shell.hook.ts +19 -0
  366. package/src/window/adopt.ts +57 -0
  367. package/src/window/hooks/close-guard.hook.ts +100 -0
  368. package/src/window/hooks/target-node.hook.ts +94 -0
  369. package/src/window/hooks/views-for.hook.ts +82 -0
  370. package/src/window/hooks/window.hook.ts +18 -0
  371. package/src/window/hooks/windows.hook.ts +40 -0
  372. package/src/window/host.tsx +347 -0
  373. package/src/window/hub.ts +156 -0
  374. package/src/window/index.ts +17 -0
  375. package/src/window/lifecycle/mount.ts +134 -0
  376. package/src/window/lifecycle/states.ts +23 -0
  377. package/src/window/placement.ts +84 -0
  378. package/src/window/stage.tsx +46 -0
@@ -0,0 +1,41 @@
1
+ import type { RoleNode } from '@astrale-os/shell'
2
+
3
+ /**
4
+ * useRoles (API §12.2) — the roles self holds (`has_role` out-edges), over the
5
+ * async OS namespace (one-shot today, §12 flag).
6
+ */
7
+ import { useMemo } from 'react'
8
+
9
+ import type { ReadState } from '../graph/memory/read/envelope'
10
+
11
+ import { useAsyncResource } from '../graph/async'
12
+ import { useShell } from '../session/shell.hook'
13
+ import { isOsPlaneAvailable } from './user/user.store'
14
+
15
+ const NO_ROLES: readonly RoleNode[] = []
16
+
17
+ export interface RolesResult extends ReadState {
18
+ readonly roles: readonly RoleNode[]
19
+ }
20
+
21
+ export function useRoles(): RolesResult {
22
+ const shell = useShell()
23
+ // OS plane unavailable (§12.2) → idle: no roles read.
24
+ const available = isOsPlaneAvailable(shell)
25
+ const res = useAsyncResource<readonly RoleNode[]>(
26
+ available ? () => shell.user.roles() : null,
27
+ NO_ROLES,
28
+ [shell, available],
29
+ )
30
+ return useMemo(
31
+ () => ({
32
+ roles: res.data,
33
+ pending: res.pending,
34
+ error: res.error,
35
+ live: false,
36
+ seq: 0,
37
+ refetch: res.refetch,
38
+ }),
39
+ [res],
40
+ )
41
+ }
@@ -0,0 +1,43 @@
1
+ import type { SpaceNode } from '@astrale-os/shell'
2
+
3
+ /**
4
+ * useCurrentSpace (API §12.2) — the selected space, projected from the shared
5
+ * per-Shell current-space store (so a switch anywhere re-themes it). One-shot
6
+ * today (§12 flag); OS-plane-unavailable reads idle.
7
+ */
8
+ import { useEffect, useMemo, useSyncExternalStore } from 'react'
9
+
10
+ import type { ReadState } from '../../graph/memory/read/envelope'
11
+
12
+ import { useShell } from '../../session/shell.hook'
13
+ import { isOsPlaneAvailable } from '../user/user.store'
14
+ import { currentSpaceStore, idleSnapshot, noopSubscribe } from './current-space.store'
15
+
16
+ export interface CurrentSpaceResult extends ReadState {
17
+ readonly space: SpaceNode | null
18
+ }
19
+
20
+ export function useCurrentSpace(): CurrentSpaceResult {
21
+ const shell = useShell()
22
+ const available = isOsPlaneAvailable(shell)
23
+ const store = currentSpaceStore(shell)
24
+ useEffect(() => {
25
+ if (available) store.ensure()
26
+ }, [store, available])
27
+ const snap = useSyncExternalStore(
28
+ available ? store.subscribe : noopSubscribe,
29
+ available ? store.getSnapshot : idleSnapshot,
30
+ available ? store.getSnapshot : idleSnapshot,
31
+ )
32
+ return useMemo(
33
+ () => ({
34
+ space: snap.space,
35
+ pending: snap.pending,
36
+ error: snap.error,
37
+ live: false,
38
+ seq: 0,
39
+ refetch: store.refresh,
40
+ }),
41
+ [snap, store],
42
+ )
43
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * The shared current-space store (API §12.2). ONE reactive resolver per Shell, so
3
+ * useCurrentSpace / useApps / useSpaces.selected all observe the SAME selected
4
+ * space: a `switch` in any window re-themes every consumer in this window at once,
5
+ * with a manual optimistic override applied ahead of the round-trip (§1.6). This
6
+ * closes the same-window cross-hook divergence; cross-WINDOW liveness still waits
7
+ * on the memory-shape move (§12 flag).
8
+ */
9
+ import type { Shell, SpaceNode } from '@astrale-os/shell'
10
+
11
+ export interface CurrentSpaceSnapshot {
12
+ readonly space: SpaceNode | null
13
+ readonly pending: boolean
14
+ readonly error: unknown
15
+ }
16
+
17
+ // Stable idles for when the OS plane is unavailable (§12.2) — a fixed snapshot
18
+ // and a no-op subscription keep `useSyncExternalStore` tear-free without touching
19
+ // (and re-triggering a fetch on) the real current-space store.
20
+ export const IDLE_SNAPSHOT: CurrentSpaceSnapshot = { space: null, pending: false, error: undefined }
21
+ export const idleSnapshot = (): CurrentSpaceSnapshot => IDLE_SNAPSHOT
22
+ export const noopSubscribe = (): (() => void) => () => {}
23
+
24
+ export interface CurrentSpaceStore {
25
+ subscribe(cb: () => void): () => void
26
+ getSnapshot(): CurrentSpaceSnapshot
27
+ /** Kick a first load if the confirmed value was never fetched. */
28
+ ensure(): void
29
+ /** Refetch the confirmed selected space; resolves when it lands. */
30
+ refresh(): Promise<void>
31
+ /** Apply an optimistic override (a switch in flight) — every consumer re-renders. */
32
+ setOptimistic(space: SpaceNode | null): void
33
+ /** Drop the optimistic override, revealing confirmed truth again. */
34
+ clearOptimistic(): void
35
+ }
36
+
37
+ function createStore(load: () => Promise<SpaceNode | null>): CurrentSpaceStore {
38
+ let confirmed: SpaceNode | null = null
39
+ let loaded = false
40
+ let pending = false
41
+ let error: unknown = undefined
42
+ let override: { space: SpaceNode | null } | null = null
43
+ let seq = 0
44
+ let snap: CurrentSpaceSnapshot = { space: null, pending: false, error: undefined }
45
+ const listeners = new Set<() => void>()
46
+
47
+ /** Recompute the projection (override wins) and notify only on a real change. */
48
+ function rebuild(): void {
49
+ const space = override !== null ? override.space : confirmed
50
+ if (space === snap.space && pending === snap.pending && error === snap.error) return
51
+ snap = { space, pending, error }
52
+ for (const l of listeners) l()
53
+ }
54
+
55
+ function fetch(): Promise<void> {
56
+ const my = ++seq
57
+ pending = true
58
+ rebuild()
59
+ return load().then(
60
+ (space) => {
61
+ if (my !== seq) return
62
+ confirmed = space
63
+ loaded = true
64
+ pending = false
65
+ error = undefined
66
+ rebuild()
67
+ },
68
+ (e: unknown) => {
69
+ if (my !== seq) return
70
+ pending = false
71
+ error = e
72
+ rebuild()
73
+ },
74
+ )
75
+ }
76
+
77
+ return {
78
+ subscribe(cb) {
79
+ listeners.add(cb)
80
+ if (!loaded && !pending) void fetch()
81
+ return () => listeners.delete(cb)
82
+ },
83
+ getSnapshot: () => snap,
84
+ ensure() {
85
+ if (!loaded && !pending) void fetch()
86
+ },
87
+ refresh: () => fetch(),
88
+ setOptimistic(space) {
89
+ override = { space }
90
+ rebuild()
91
+ },
92
+ clearOptimistic() {
93
+ if (override === null) return
94
+ override = null
95
+ rebuild()
96
+ },
97
+ }
98
+ }
99
+
100
+ const stores = new WeakMap<Shell, CurrentSpaceStore>()
101
+
102
+ /** The one current-space store for a Shell (created on first touch). */
103
+ export function currentSpaceStore(shell: Shell): CurrentSpaceStore {
104
+ let s = stores.get(shell)
105
+ if (s === undefined) {
106
+ s = createStore(() => shell.user.currentSpace())
107
+ stores.set(shell, s)
108
+ }
109
+ return s
110
+ }
@@ -0,0 +1,41 @@
1
+ import type { NodeRefInput } from '@astrale-os/kernel-client'
2
+ import type { SpaceNode } from '@astrale-os/shell'
3
+
4
+ import { normalizeRef } from '@astrale-os/kernel-client'
5
+ /**
6
+ * useSpace(ref) (API §12.2) — a single space by ref; `null`/`undefined` (or an
7
+ * unavailable OS plane) is idle (§1.3). One-shot today (§12 flag).
8
+ */
9
+ import { useMemo } from 'react'
10
+
11
+ import type { CurrentSpaceResult } from './current-space.hook'
12
+
13
+ import { useAsyncResource } from '../../graph/async'
14
+ import { isIdleInput } from '../../graph/memory/read/idle'
15
+ import { useShell } from '../../session/shell.hook'
16
+ import { isOsPlaneAvailable } from '../user/user.store'
17
+
18
+ export type SpaceResult = CurrentSpaceResult
19
+
20
+ export function useSpace(ref: NodeRefInput | null | undefined): SpaceResult {
21
+ const shell = useShell()
22
+ const idle = isIdleInput(ref) || !isOsPlaneAvailable(shell)
23
+ // The inline guard (not the stored `idle`) narrows `ref` to non-null below.
24
+ const key = idle ? null : normalizeRef(ref as NodeRefInput)
25
+ const res = useAsyncResource<SpaceNode | null>(
26
+ idle ? null : () => shell.spaces.get(ref as NodeRefInput),
27
+ null,
28
+ [shell, key],
29
+ )
30
+ return useMemo(
31
+ () => ({
32
+ space: idle ? null : res.data,
33
+ pending: idle ? false : res.pending,
34
+ error: idle ? undefined : res.error,
35
+ live: false,
36
+ seq: 0,
37
+ refetch: res.refetch,
38
+ }),
39
+ [idle, res],
40
+ )
41
+ }
@@ -0,0 +1,113 @@
1
+ import type { NodeRefInput } from '@astrale-os/kernel-client'
2
+ import type { SpaceNode } from '@astrale-os/shell'
3
+
4
+ import { normalizeRef } from '@astrale-os/kernel-client'
5
+ /**
6
+ * useSpaces (API §12.2) — the spaces list, the selected space, and a `switch`
7
+ * Action that repoints the `selected_space` edge. Reads the SAME per-Shell
8
+ * current-space store as useCurrentSpace/useApps, so a switch re-themes every
9
+ * consumer in the window at once — optimistically ahead of the round-trip (§1.6),
10
+ * commit reconciles, failure rolls back. Reads are one-shot today (§12 flag).
11
+ */
12
+ import { useEffect, useMemo, useSyncExternalStore } from 'react'
13
+
14
+ import type { Action } from '../../graph/action'
15
+ import type { ReadState } from '../../graph/memory/read/envelope'
16
+
17
+ import { useAction } from '../../graph/action'
18
+ import { useAsyncResource } from '../../graph/async'
19
+ import { useShell } from '../../session/shell.hook'
20
+ import { isOsPlaneAvailable } from '../user/user.store'
21
+ import { currentSpaceStore, idleSnapshot, noopSubscribe } from './current-space.store'
22
+
23
+ const NO_SPACES: readonly SpaceNode[] = []
24
+
25
+ export interface SpacesResult extends ReadState {
26
+ /** Spaces-dir children, live (one-shot today), the caller's readable set. */
27
+ readonly spaces: readonly SpaceNode[]
28
+ /** The `selected_space` target — optimistically ahead during a `switch`. */
29
+ readonly selected: SpaceNode | null
30
+ /** `user::switchSpace` with a manual optimistic overlay (§1.6). */
31
+ readonly switch: Action<[NodeRefInput], void>
32
+ }
33
+
34
+ export function useSpaces(): SpacesResult {
35
+ const shell = useShell()
36
+ const available = isOsPlaneAvailable(shell)
37
+ const store = currentSpaceStore(shell)
38
+
39
+ const spacesRes = useAsyncResource<readonly SpaceNode[]>(
40
+ available ? () => shell.spaces.list() : null,
41
+ NO_SPACES,
42
+ [shell, available],
43
+ )
44
+ useEffect(() => {
45
+ if (available) store.ensure()
46
+ }, [store, available])
47
+ const selectedSnap = useSyncExternalStore(
48
+ available ? store.subscribe : noopSubscribe,
49
+ available ? store.getSnapshot : idleSnapshot,
50
+ available ? store.getSnapshot : idleSnapshot,
51
+ )
52
+
53
+ const spaces = spacesRes.data
54
+
55
+ const switchAction = useAction(async (ref: NodeRefInput): Promise<void> => {
56
+ // Optimistic override on the SHARED store, so useCurrentSpace / useApps ahead
57
+ // of the round-trip too (§12.2). Only overlay a known target (a path ref that
58
+ // matches nothing in the list simply skips the preview).
59
+ const target = findSpace(spaces, ref)
60
+ if (target !== null) store.setOptimistic(target)
61
+ try {
62
+ await shell.user.switchSpace(ref)
63
+ await store.refresh() // reconcile confirmed truth BEFORE dropping the preview
64
+ store.clearOptimistic()
65
+ } catch (error) {
66
+ store.clearOptimistic() // rollback: reveal the confirmed (previous) space
67
+ throw error
68
+ }
69
+ })
70
+
71
+ return useMemo<SpacesResult>(
72
+ () => ({
73
+ spaces,
74
+ selected: selectedSnap.space,
75
+ switch: switchAction,
76
+ pending: spacesRes.pending || selectedSnap.pending,
77
+ error: spacesRes.error ?? selectedSnap.error,
78
+ live: false,
79
+ seq: 0,
80
+ refetch: async () => {
81
+ await Promise.all([spacesRes.refetch(), store.refresh()])
82
+ },
83
+ }),
84
+ [
85
+ spaces,
86
+ selectedSnap,
87
+ switchAction,
88
+ spacesRes.pending,
89
+ spacesRes.error,
90
+ spacesRes.refetch,
91
+ store,
92
+ ],
93
+ )
94
+ }
95
+
96
+ /** The space in `spaces` denoted by `ref`, or `null` (path refs don't match by id). */
97
+ function findSpace(spaces: readonly SpaceNode[], ref: NodeRefInput): SpaceNode | null {
98
+ const id = refIdOf(ref)
99
+ if (id === null) return null
100
+ return spaces.find((s) => s.id === id) ?? null
101
+ }
102
+
103
+ /** A bound-node-ish subject carries a graph id. */
104
+ function hasId(x: unknown): x is { readonly id: string } {
105
+ return typeof x === 'object' && x !== null && 'id' in x
106
+ }
107
+
108
+ /** The graph id a subject denotes, or `null` when it is a path (no id to match). */
109
+ function refIdOf(x: NodeRefInput | { readonly id: string }): string | null {
110
+ if (hasId(x)) return x.id
111
+ const raw = normalizeRef(x)
112
+ return raw.startsWith('@') ? raw.slice(1) : null
113
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * useUser (API §12.2) — self as the shell-domain `User` (typed
3
+ * firstName/lastName/email). The provider PRE-RESOLVES it at boot (`primeUser`),
4
+ * so `useUser` is synchronous below the provider (API §2). OS-plane absent → throws
5
+ * descriptively to the nearest boundary; a never-primed shell suspends on first use
6
+ * (§1.4). FLAG: not yet re-rendered on rename — becomes live via a memory shape.
7
+ */
8
+ import type { UserNode } from '@astrale-os/shell'
9
+
10
+ import { useShell } from '../../session/shell.hook'
11
+ import { loadUser, userCache } from './user.store'
12
+
13
+ export function useUser(): UserNode {
14
+ const shell = useShell()
15
+ const entry = userCache.get(shell) ?? loadUser(shell)
16
+ if (entry.status === 'pending') throw entry.promise
17
+ // OS plane resolved absent at boot — throw to the nearest boundary; do NOT
18
+ // re-load (the plane is not coming back this session).
19
+ if (entry.status === 'unavailable') throw entry.error
20
+ if (entry.status === 'rejected') {
21
+ // Retryable transient: drop the poisoned cell so an error-boundary retry
22
+ // re-loads `me()` instead of re-throwing the same failure forever.
23
+ userCache.delete(shell)
24
+ throw entry.error
25
+ }
26
+ return entry.value
27
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * The OS-plane user store (API §2/§12.2). Provider-boot priming (`primeUser`) seeds
3
+ * the per-Shell user cache so `useUser` is synchronous, and tracks OS-plane
4
+ * availability: a missing/rejecting `shell.user.me` marks the plane down (the other
5
+ * OS hooks then read idle) without failing boot. A never-primed shell falls back to
6
+ * a lazy cold read that SUSPENDS on first use (§1.4).
7
+ */
8
+ import type { Shell, UserNode } from '@astrale-os/shell'
9
+
10
+ export type UserEntry =
11
+ | { status: 'pending'; promise: Promise<void> }
12
+ | { status: 'resolved'; value: UserNode }
13
+ | { status: 'rejected'; error: unknown }
14
+ /** OS plane absent — throw descriptively, do NOT re-load (a reload just refails). */
15
+ | { status: 'unavailable'; error: unknown }
16
+
17
+ export const userCache = new WeakMap<Shell, UserEntry>()
18
+
19
+ /** Shells whose OS plane could not be resolved at boot — OS hooks read idle. */
20
+ const osPlaneDown = new WeakSet<Shell>()
21
+
22
+ /** Is the OS plane (shell.user/spaces/apps) usable for this shell? */
23
+ export function isOsPlaneAvailable(shell: Shell): boolean {
24
+ return !osPlaneDown.has(shell)
25
+ }
26
+
27
+ interface UserSurface {
28
+ user?: { me?: () => Promise<UserNode> }
29
+ }
30
+
31
+ function osPlaneError(cause: unknown): Error {
32
+ const detail = cause instanceof Error ? cause.message : String(cause)
33
+ return new Error(
34
+ `The OS plane is unavailable (useUser/useSpaces/useApps): shell.user.me() could not resolve ` +
35
+ `— the workspace/shell domain may not be installed for this identity. (${detail})`,
36
+ )
37
+ }
38
+
39
+ /**
40
+ * Provider-boot priming (API §2). Best-effort: seed the user cache so `useUser`
41
+ * is synchronous, or mark the OS plane down. NEVER throws — boot must not fail on
42
+ * a missing OS plane.
43
+ */
44
+ export async function primeUser(shell: Shell): Promise<void> {
45
+ const me = (shell as unknown as UserSurface).user?.me
46
+ if (typeof me !== 'function') {
47
+ osPlaneDown.add(shell)
48
+ userCache.set(shell, {
49
+ status: 'unavailable',
50
+ error: osPlaneError('shell.user.me is not a function'),
51
+ })
52
+ return
53
+ }
54
+ try {
55
+ const value = await me.call((shell as unknown as UserSurface).user)
56
+ osPlaneDown.delete(shell)
57
+ userCache.set(shell, { status: 'resolved', value })
58
+ } catch (error) {
59
+ osPlaneDown.add(shell)
60
+ userCache.set(shell, { status: 'unavailable', error: osPlaneError(error) })
61
+ }
62
+ }
63
+
64
+ /** Kick off `me()` lazily (a shell that was never primed) and cache a suspense cell. */
65
+ export function loadUser(shell: Shell): UserEntry {
66
+ const cell: {
67
+ status: 'pending' | 'resolved' | 'rejected'
68
+ promise?: Promise<void>
69
+ value?: UserNode
70
+ error?: unknown
71
+ } = { status: 'pending' }
72
+ cell.promise = shell.user.me().then(
73
+ (value) => {
74
+ cell.status = 'resolved'
75
+ cell.value = value
76
+ },
77
+ (error: unknown) => {
78
+ cell.status = 'rejected'
79
+ cell.error = error
80
+ },
81
+ )
82
+ const entry = cell as unknown as UserEntry
83
+ userCache.set(shell, entry)
84
+ return entry
85
+ }
@@ -0,0 +1,124 @@
1
+ import type { CredentialInput, FnMap } from '@astrale-os/kernel-client'
2
+ import type { BoundClientSessionView } from '@astrale-os/kernel-client/session'
3
+ import type { ReactElement, ReactNode } from 'react'
4
+
5
+ /**
6
+ * The acting-principal binding (API §4). `IdentityContext` seats the effective
7
+ * principal a subtree dispatches as — its bound kernel view + whoami + memory
8
+ * scope; `useIdentityBinding` resolves it (an `<ActAs>` override, else the root
9
+ * off ShellContext). `<ActAs>` rebinds a subtree to another principal with a
10
+ * DISTINCT memory scope so results never leak across principals (§5.4).
11
+ */
12
+ import { createContext, useContext, useEffect, useMemo, useState } from 'react'
13
+
14
+ import type { SelfNode } from '../session/session.context'
15
+
16
+ import { ShellContext } from '../session/session.context'
17
+
18
+ /** A bound session view — the kernel surface `session.as(cred)` yields. */
19
+ export type SessionView = BoundClientSessionView<FnMap>
20
+
21
+ /** A whoami-shaped identity/graph node (kernel-client does not yet re-export `Node` — G8). */
22
+ export type NodeValue = SelfNode
23
+
24
+ /** The root scope key — the memory's own default scope (`createMemory` default). */
25
+ export const ROOT_SCOPE = 'default'
26
+
27
+ /**
28
+ * The principal a subtree acts as (API §4). `scope` is the memory isolation key
29
+ * (§5.4): the graph plane scopes its memory by `memory.forScope(scope)` so an
30
+ * <ActAs> subtree never shares cached results with another principal.
31
+ */
32
+ export interface IdentityBinding {
33
+ readonly kernel: SessionView
34
+ readonly self: SelfNode
35
+ readonly scope: string
36
+ }
37
+ export const IdentityContext = createContext<IdentityBinding | null>(null)
38
+ IdentityContext.displayName = 'AstraleIdentityContext'
39
+
40
+ /**
41
+ * The principal this subtree dispatches as. An <ActAs> value wins; otherwise the
42
+ * root binding off ShellContext. Throws outside a provider, or when the bound
43
+ * kernel resolved no identity (a bare prebuilt Shell with no auth surface).
44
+ */
45
+ export function useIdentityBinding(): IdentityBinding {
46
+ const override = useContext(IdentityContext)
47
+ if (override !== null) return override
48
+
49
+ const runtime = useContext(ShellContext)
50
+ if (runtime === null) {
51
+ throw new Error('identity hooks must be used within a <ShellProvider>')
52
+ }
53
+ if (runtime.self === null) {
54
+ throw new Error(
55
+ 'identity hooks require an identity: this shell exposed no auth surface, so whoami ' +
56
+ 'resolved nothing at boot',
57
+ )
58
+ }
59
+ // The root binding reads stable fields (self, shell.kernel), so a fresh object
60
+ // per render is inert — consumers key off `kernel`/`self`, both stable.
61
+ return { kernel: runtime.shell.kernel, self: runtime.self, scope: ROOT_SCOPE }
62
+ }
63
+
64
+ export interface ActAsProps {
65
+ /** The principal to act as. A thunk is resolved by the kernel per dispatch. */
66
+ credential: CredentialInput | (() => CredentialInput)
67
+ children: ReactNode
68
+ }
69
+
70
+ /**
71
+ * A subtree that dispatches as another principal: rebinds the kernel (session.as),
72
+ * resolves whoami, and provides a fresh identity binding with a DISTINCT memory
73
+ * scope (`as:<id>`). Nestable — it rebinds from the current effective principal. A
74
+ * brief nested boot holds the subtree (renders null) until whoami resolves.
75
+ */
76
+ export function ActAs({ credential, children }: ActAsProps): ReactElement | null {
77
+ const parent = useIdentityBinding()
78
+ // Key the rebind on a STABLE credential identity (like `configIdentity` does),
79
+ // so an inline object credential (`credential={{ … }}`) does not re-mint the
80
+ // session view — and re-run whoami, unmounting the subtree — every parent
81
+ // render. A thunk still keys by reference: pass a stable thunk to avoid churn.
82
+ const credKey = credentialIdentity(credential)
83
+ // eslint-disable-next-line react-hooks/exhaustive-deps
84
+ const kernel = useMemo(() => parent.kernel.as(credential), [parent.kernel, credKey])
85
+
86
+ const [self, setSelf] = useState<SelfNode | null>(null)
87
+ useEffect(() => {
88
+ let alive = true
89
+ setSelf(null)
90
+ void kernel.auth.whoami().then(
91
+ (node) => {
92
+ if (alive) setSelf(node)
93
+ },
94
+ () => {
95
+ /* held (null) subtree on resolve failure — transport errors surface via the provider */
96
+ },
97
+ )
98
+ return () => {
99
+ alive = false
100
+ }
101
+ }, [kernel])
102
+
103
+ const value = useMemo<IdentityBinding | null>(
104
+ () => (self === null ? null : { kernel, self, scope: `as:${self.id}` }),
105
+ [kernel, self],
106
+ )
107
+
108
+ if (value === null) return null
109
+ return <IdentityContext.Provider value={value}>{children}</IdentityContext.Provider>
110
+ }
111
+
112
+ /**
113
+ * A stable identity for a credential: the thunk by reference, a string as itself,
114
+ * and an object by a serialized form — so an equal inline object does not churn
115
+ * the memo (worst case a serialize failure falls back to reference identity).
116
+ */
117
+ function credentialIdentity(credential: CredentialInput | (() => CredentialInput)): unknown {
118
+ if (typeof credential === 'function' || typeof credential === 'string') return credential
119
+ try {
120
+ return `cred:${JSON.stringify(credential)}`
121
+ } catch {
122
+ return credential
123
+ }
124
+ }
@@ -0,0 +1,36 @@
1
+ import type { AuthApi } from '@astrale-os/kernel-client/auth'
2
+
3
+ /**
4
+ * useAuth (API §4) — the imperative AuthApi of the effective principal
5
+ * (whoami/credential/mint/delegate/check/grant/revoke/bind/unbind). Grant and
6
+ * revoke additionally nudge the principal's useCan ledger on success, delivering
7
+ * the Tier-0 local re-check (SPEC §11.3). DEVIATION: API §4 describes a pure
8
+ * passthrough; the wrap is the only way to surface a local grant/revoke today,
9
+ * since permissions dispatch as identity methods rather than graph patches.
10
+ * Cross-client liveness still awaits the watch tiers (G7).
11
+ */
12
+ import { useMemo } from 'react'
13
+
14
+ import type { SessionView } from './act-as.context'
15
+
16
+ import { useIdentityBinding } from './act-as.context'
17
+ import { ledgerFor } from './can.hook'
18
+
19
+ export function useAuth(): AuthApi {
20
+ const { kernel } = useIdentityBinding()
21
+ return useMemo(() => withLedgerNudge(kernel), [kernel])
22
+ }
23
+
24
+ /** Wrap grant/revoke so a successful local change re-checks the principal's cells. */
25
+ function withLedgerNudge(kernel: SessionView): AuthApi {
26
+ const auth = kernel.auth
27
+ const ledger = ledgerFor(kernel)
28
+ const invalidate = (): void => ledger.invalidateAll()
29
+
30
+ const grant = ((arg: never) =>
31
+ Promise.resolve(auth.grant(arg)).then(invalidate)) as AuthApi['grant']
32
+ const revoke = ((arg: never) =>
33
+ Promise.resolve(auth.revoke(arg)).then(invalidate)) as AuthApi['revoke']
34
+
35
+ return { ...auth, grant, revoke }
36
+ }