@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.
- package/LICENSE +202 -0
- package/dist/application/apps/app-host.d.ts +31 -0
- package/dist/application/apps/app-host.d.ts.map +1 -0
- package/dist/application/apps/app-host.js +59 -0
- package/dist/application/apps/app-logo.d.ts +19 -0
- package/dist/application/apps/app-logo.d.ts.map +1 -0
- package/dist/application/apps/app-logo.js +31 -0
- package/dist/application/apps/apps.hook.d.ts +12 -0
- package/dist/application/apps/apps.hook.d.ts.map +1 -0
- package/dist/application/apps/apps.hook.js +51 -0
- package/dist/application/apps/open-app.hook.d.ts +10 -0
- package/dist/application/apps/open-app.hook.d.ts.map +1 -0
- package/dist/application/apps/open-app.hook.js +7 -0
- package/dist/application/index.d.ts +18 -0
- package/dist/application/index.d.ts.map +1 -0
- package/dist/application/index.js +12 -0
- package/dist/application/roles.hook.d.ts +7 -0
- package/dist/application/roles.hook.d.ts.map +1 -0
- package/dist/application/roles.hook.js +23 -0
- package/dist/application/spaces/current-space.hook.d.ts +7 -0
- package/dist/application/spaces/current-space.hook.d.ts.map +1 -0
- package/dist/application/spaces/current-space.hook.js +27 -0
- package/dist/application/spaces/current-space.store.d.ts +32 -0
- package/dist/application/spaces/current-space.store.d.ts.map +1 -0
- package/dist/application/spaces/current-space.store.js +79 -0
- package/dist/application/spaces/space.hook.d.ts +5 -0
- package/dist/application/spaces/space.hook.d.ts.map +1 -0
- package/dist/application/spaces/space.hook.js +25 -0
- package/dist/application/spaces/spaces.hook.d.ts +14 -0
- package/dist/application/spaces/spaces.hook.d.ts.map +1 -0
- package/dist/application/spaces/spaces.hook.js +82 -0
- package/dist/application/user/user.hook.d.ts +10 -0
- package/dist/application/user/user.hook.d.ts.map +1 -0
- package/dist/application/user/user.hook.js +19 -0
- package/dist/application/user/user.store.d.ts +35 -0
- package/dist/application/user/user.store.d.ts.map +1 -0
- package/dist/application/user/user.store.js +51 -0
- package/dist/auth/act-as.context.d.ts +40 -0
- package/dist/auth/act-as.context.d.ts.map +1 -0
- package/dist/auth/act-as.context.js +84 -0
- package/dist/auth/auth.hook.d.ts +3 -0
- package/dist/auth/auth.hook.d.ts.map +1 -0
- package/dist/auth/auth.hook.js +25 -0
- package/dist/auth/can.hook.d.ts +40 -0
- package/dist/auth/can.hook.d.ts.map +1 -0
- package/dist/auth/can.hook.js +119 -0
- package/dist/auth/index.d.ts +6 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +5 -0
- package/dist/auth/self.hook.d.ts +3 -0
- package/dist/auth/self.hook.d.ts.map +1 -0
- package/dist/auth/self.hook.js +11 -0
- package/dist/auth/session-kernel.d.ts +4 -0
- package/dist/auth/session-kernel.d.ts.map +1 -0
- package/dist/auth/session-kernel.js +21 -0
- package/dist/graph/action.d.ts +22 -0
- package/dist/graph/action.d.ts.map +1 -0
- package/dist/graph/action.js +85 -0
- package/dist/graph/async.d.ts +11 -0
- package/dist/graph/async.d.ts.map +1 -0
- package/dist/graph/async.js +60 -0
- package/dist/graph/call/call.hook.d.ts +21 -0
- package/dist/graph/call/call.hook.d.ts.map +1 -0
- package/dist/graph/call/call.hook.js +23 -0
- package/dist/graph/call/stream.hook.d.ts +28 -0
- package/dist/graph/call/stream.hook.d.ts.map +1 -0
- package/dist/graph/call/stream.hook.js +137 -0
- package/dist/graph/graph.context.d.ts +4 -0
- package/dist/graph/graph.context.d.ts.map +1 -0
- package/dist/graph/graph.context.js +10 -0
- package/dist/graph/guards.error.d.ts +9 -0
- package/dist/graph/guards.error.d.ts.map +1 -0
- package/dist/graph/guards.error.js +7 -0
- package/dist/graph/index.d.ts +20 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +9 -0
- package/dist/graph/memory/base.d.ts +21 -0
- package/dist/graph/memory/base.d.ts.map +1 -0
- package/dist/graph/memory/base.js +33 -0
- package/dist/graph/memory/read/envelope.d.ts +71 -0
- package/dist/graph/memory/read/envelope.d.ts.map +1 -0
- package/dist/graph/memory/read/envelope.js +29 -0
- package/dist/graph/memory/read/idle.d.ts +18 -0
- package/dist/graph/memory/read/idle.d.ts.map +1 -0
- package/dist/graph/memory/read/idle.js +16 -0
- package/dist/graph/memory/read/read.d.ts +9 -0
- package/dist/graph/memory/read/read.d.ts.map +1 -0
- package/dist/graph/memory/read/read.js +87 -0
- package/dist/graph/memory/scope.d.ts +7 -0
- package/dist/graph/memory/scope.d.ts.map +1 -0
- package/dist/graph/memory/scope.js +25 -0
- package/dist/graph/mutate/move.hook.d.ts +7 -0
- package/dist/graph/mutate/move.hook.d.ts.map +1 -0
- package/dist/graph/mutate/move.hook.js +49 -0
- package/dist/graph/mutate/mutate.hook.d.ts +17 -0
- package/dist/graph/mutate/mutate.hook.d.ts.map +1 -0
- package/dist/graph/mutate/mutate.hook.js +45 -0
- package/dist/graph/query/children.hook.d.ts +29 -0
- package/dist/graph/query/children.hook.d.ts.map +1 -0
- package/dist/graph/query/children.hook.js +64 -0
- package/dist/graph/query/graph.hook.d.ts +9 -0
- package/dist/graph/query/graph.hook.d.ts.map +1 -0
- package/dist/graph/query/graph.hook.js +5 -0
- package/dist/graph/query/neighbors.hook.d.ts +7 -0
- package/dist/graph/query/neighbors.hook.d.ts.map +1 -0
- package/dist/graph/query/neighbors.hook.js +79 -0
- package/dist/graph/query/node.hook.d.ts +14 -0
- package/dist/graph/query/node.hook.d.ts.map +1 -0
- package/dist/graph/query/node.hook.js +43 -0
- package/dist/graph/query/query.hook.d.ts +14 -0
- package/dist/graph/query/query.hook.d.ts.map +1 -0
- package/dist/graph/query/query.hook.js +23 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/intents/envelope.d.ts +33 -0
- package/dist/intents/envelope.d.ts.map +1 -0
- package/dist/intents/envelope.js +19 -0
- package/dist/intents/index.d.ts +6 -0
- package/dist/intents/index.d.ts.map +1 -0
- package/dist/intents/index.js +3 -0
- package/dist/intents/listen-intent.hook.d.ts +16 -0
- package/dist/intents/listen-intent.hook.d.ts.map +1 -0
- package/dist/intents/listen-intent.hook.js +75 -0
- package/dist/intents/reply.d.ts +9 -0
- package/dist/intents/reply.d.ts.map +1 -0
- package/dist/intents/reply.js +14 -0
- package/dist/intents/send-intent.hook.d.ts +12 -0
- package/dist/intents/send-intent.hook.d.ts.map +1 -0
- package/dist/intents/send-intent.hook.js +86 -0
- package/dist/navigation/adapters/adapter.d.ts +15 -0
- package/dist/navigation/adapters/adapter.d.ts.map +1 -0
- package/dist/navigation/adapters/adapter.js +1 -0
- package/dist/navigation/adapters/memory.adapter.d.ts +9 -0
- package/dist/navigation/adapters/memory.adapter.d.ts.map +1 -0
- package/dist/navigation/adapters/memory.adapter.js +15 -0
- package/dist/navigation/adapters/url.adapter.d.ts +6 -0
- package/dist/navigation/adapters/url.adapter.d.ts.map +1 -0
- package/dist/navigation/adapters/url.adapter.js +59 -0
- package/dist/navigation/history.d.ts +63 -0
- package/dist/navigation/history.d.ts.map +1 -0
- package/dist/navigation/history.js +144 -0
- package/dist/navigation/hooks/ancestors.hook.d.ts +13 -0
- package/dist/navigation/hooks/ancestors.hook.d.ts.map +1 -0
- package/dist/navigation/hooks/ancestors.hook.js +56 -0
- package/dist/navigation/hooks/location.hook.d.ts +4 -0
- package/dist/navigation/hooks/location.hook.d.ts.map +1 -0
- package/dist/navigation/hooks/location.hook.js +17 -0
- package/dist/navigation/hooks/navigate.hook.d.ts +12 -0
- package/dist/navigation/hooks/navigate.hook.d.ts.map +1 -0
- package/dist/navigation/hooks/navigate.hook.js +33 -0
- package/dist/navigation/hooks/scope.hook.d.ts +12 -0
- package/dist/navigation/hooks/scope.hook.d.ts.map +1 -0
- package/dist/navigation/hooks/scope.hook.js +21 -0
- package/dist/navigation/index.d.ts +16 -0
- package/dist/navigation/index.d.ts.map +1 -0
- package/dist/navigation/index.js +10 -0
- package/dist/navigation/link.d.ts +11 -0
- package/dist/navigation/link.d.ts.map +1 -0
- package/dist/navigation/link.js +62 -0
- package/dist/navigation/location.d.ts +48 -0
- package/dist/navigation/location.d.ts.map +1 -0
- package/dist/navigation/location.js +88 -0
- package/dist/navigation/scope.context.d.ts +64 -0
- package/dist/navigation/scope.context.d.ts.map +1 -0
- package/dist/navigation/scope.context.js +178 -0
- package/dist/navigation/snapshot.d.ts +22 -0
- package/dist/navigation/snapshot.d.ts.map +1 -0
- package/dist/navigation/snapshot.js +65 -0
- package/dist/schema/bind-read.d.ts +37 -0
- package/dist/schema/bind-read.d.ts.map +1 -0
- package/dist/schema/bind-read.js +205 -0
- package/dist/schema/binding.d.ts +21 -0
- package/dist/schema/binding.d.ts.map +1 -0
- package/dist/schema/binding.js +55 -0
- package/dist/schema/domain.hook.d.ts +10 -0
- package/dist/schema/domain.hook.d.ts.map +1 -0
- package/dist/schema/domain.hook.js +7 -0
- package/dist/schema/function.hook.d.ts +9 -0
- package/dist/schema/function.hook.d.ts.map +1 -0
- package/dist/schema/function.hook.js +40 -0
- package/dist/schema/index.d.ts +10 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +8 -0
- package/dist/schema/kernel.hook.d.ts +10 -0
- package/dist/schema/kernel.hook.d.ts.map +1 -0
- package/dist/schema/kernel.hook.js +8 -0
- package/dist/schema/method.hook.d.ts +14 -0
- package/dist/schema/method.hook.d.ts.map +1 -0
- package/dist/schema/method.hook.js +49 -0
- package/dist/schema/tag.d.ts +11 -0
- package/dist/schema/tag.d.ts.map +1 -0
- package/dist/schema/tag.js +11 -0
- package/dist/schema/types.d.ts +73 -0
- package/dist/schema/types.d.ts.map +1 -0
- package/dist/schema/types.js +1 -0
- package/dist/schema/write.d.ts +15 -0
- package/dist/schema/write.d.ts.map +1 -0
- package/dist/schema/write.js +45 -0
- package/dist/selection/index.d.ts +4 -0
- package/dist/selection/index.d.ts.map +1 -0
- package/dist/selection/index.js +4 -0
- package/dist/selection/live.d.ts +21 -0
- package/dist/selection/live.d.ts.map +1 -0
- package/dist/selection/live.js +116 -0
- package/dist/selection/scope.context.d.ts +26 -0
- package/dist/selection/scope.context.d.ts.map +1 -0
- package/dist/selection/scope.context.js +100 -0
- package/dist/selection/selection.hook.d.ts +4 -0
- package/dist/selection/selection.hook.d.ts.map +1 -0
- package/dist/selection/selection.hook.js +14 -0
- package/dist/session/boot/boot.d.ts +19 -0
- package/dist/session/boot/boot.d.ts.map +1 -0
- package/dist/session/boot/boot.js +45 -0
- package/dist/session/boot/ownership.d.ts +18 -0
- package/dist/session/boot/ownership.d.ts.map +1 -0
- package/dist/session/boot/ownership.js +39 -0
- package/dist/session/boot/status.d.ts +24 -0
- package/dist/session/boot/status.d.ts.map +1 -0
- package/dist/session/boot/status.js +11 -0
- package/dist/session/connection/connection.hook.d.ts +3 -0
- package/dist/session/connection/connection.hook.d.ts.map +1 -0
- package/dist/session/connection/connection.hook.js +12 -0
- package/dist/session/connection/connection.store.d.ts +24 -0
- package/dist/session/connection/connection.store.d.ts.map +1 -0
- package/dist/session/connection/connection.store.js +31 -0
- package/dist/session/index.d.ts +8 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +6 -0
- package/dist/session/kernel-client.hook.d.ts +9 -0
- package/dist/session/kernel-client.hook.d.ts.map +1 -0
- package/dist/session/kernel-client.hook.js +5 -0
- package/dist/session/kernel-instance.hook.d.ts +9 -0
- package/dist/session/kernel-instance.hook.d.ts.map +1 -0
- package/dist/session/kernel-instance.hook.js +10 -0
- package/dist/session/provider.d.ts +31 -0
- package/dist/session/provider.d.ts.map +1 -0
- package/dist/session/provider.js +165 -0
- package/dist/session/session.context.d.ts +18 -0
- package/dist/session/session.context.d.ts.map +1 -0
- package/dist/session/session.context.js +8 -0
- package/dist/session/shell.hook.d.ts +4 -0
- package/dist/session/shell.hook.d.ts.map +1 -0
- package/dist/session/shell.hook.js +15 -0
- package/dist/window/adopt.d.ts +27 -0
- package/dist/window/adopt.d.ts.map +1 -0
- package/dist/window/adopt.js +27 -0
- package/dist/window/hooks/close-guard.hook.d.ts +6 -0
- package/dist/window/hooks/close-guard.hook.d.ts.map +1 -0
- package/dist/window/hooks/close-guard.hook.js +90 -0
- package/dist/window/hooks/target-node.hook.d.ts +7 -0
- package/dist/window/hooks/target-node.hook.d.ts.map +1 -0
- package/dist/window/hooks/target-node.hook.js +77 -0
- package/dist/window/hooks/views-for.hook.d.ts +8 -0
- package/dist/window/hooks/views-for.hook.d.ts.map +1 -0
- package/dist/window/hooks/views-for.hook.js +51 -0
- package/dist/window/hooks/window.hook.d.ts +4 -0
- package/dist/window/hooks/window.hook.d.ts.map +1 -0
- package/dist/window/hooks/window.hook.js +14 -0
- package/dist/window/hooks/windows.hook.d.ts +15 -0
- package/dist/window/hooks/windows.hook.d.ts.map +1 -0
- package/dist/window/hooks/windows.hook.js +25 -0
- package/dist/window/host.d.ts +31 -0
- package/dist/window/host.d.ts.map +1 -0
- package/dist/window/host.js +270 -0
- package/dist/window/hub.d.ts +41 -0
- package/dist/window/hub.d.ts.map +1 -0
- package/dist/window/hub.js +100 -0
- package/dist/window/index.d.ts +15 -0
- package/dist/window/index.d.ts.map +1 -0
- package/dist/window/index.js +10 -0
- package/dist/window/lifecycle/mount.d.ts +47 -0
- package/dist/window/lifecycle/mount.d.ts.map +1 -0
- package/dist/window/lifecycle/mount.js +92 -0
- package/dist/window/lifecycle/states.d.ts +33 -0
- package/dist/window/lifecycle/states.d.ts.map +1 -0
- package/dist/window/lifecycle/states.js +7 -0
- package/dist/window/placement.d.ts +33 -0
- package/dist/window/placement.d.ts.map +1 -0
- package/dist/window/placement.js +57 -0
- package/dist/window/stage.d.ts +3 -0
- package/dist/window/stage.d.ts.map +1 -0
- package/dist/window/stage.js +41 -0
- package/package.json +59 -0
- package/src/application/apps/app-host.tsx +113 -0
- package/src/application/apps/app-logo.tsx +56 -0
- package/src/application/apps/apps.hook.ts +81 -0
- package/src/application/apps/open-app.hook.ts +18 -0
- package/src/application/index.ts +22 -0
- package/src/application/roles.hook.ts +41 -0
- package/src/application/spaces/current-space.hook.ts +43 -0
- package/src/application/spaces/current-space.store.ts +110 -0
- package/src/application/spaces/space.hook.ts +41 -0
- package/src/application/spaces/spaces.hook.ts +113 -0
- package/src/application/user/user.hook.ts +27 -0
- package/src/application/user/user.store.ts +85 -0
- package/src/auth/act-as.context.tsx +124 -0
- package/src/auth/auth.hook.ts +36 -0
- package/src/auth/can.hook.ts +149 -0
- package/src/auth/index.ts +6 -0
- package/src/auth/self.hook.ts +14 -0
- package/src/auth/session-kernel.ts +24 -0
- package/src/graph/action.ts +127 -0
- package/src/graph/async.ts +87 -0
- package/src/graph/call/call.hook.ts +42 -0
- package/src/graph/call/stream.hook.ts +200 -0
- package/src/graph/graph.context.ts +14 -0
- package/src/graph/guards.error.ts +58 -0
- package/src/graph/index.ts +20 -0
- package/src/graph/memory/base.ts +50 -0
- package/src/graph/memory/read/envelope.ts +98 -0
- package/src/graph/memory/read/idle.ts +24 -0
- package/src/graph/memory/read/read.ts +103 -0
- package/src/graph/memory/scope.ts +29 -0
- package/src/graph/mutate/move.hook.ts +70 -0
- package/src/graph/mutate/mutate.hook.ts +69 -0
- package/src/graph/query/children.hook.ts +136 -0
- package/src/graph/query/graph.hook.ts +13 -0
- package/src/graph/query/neighbors.hook.ts +124 -0
- package/src/graph/query/node.hook.ts +83 -0
- package/src/graph/query/query.hook.ts +42 -0
- package/src/index.ts +202 -0
- package/src/intents/envelope.ts +52 -0
- package/src/intents/index.ts +6 -0
- package/src/intents/listen-intent.hook.ts +92 -0
- package/src/intents/reply.ts +24 -0
- package/src/intents/send-intent.hook.ts +111 -0
- package/src/navigation/adapters/adapter.ts +13 -0
- package/src/navigation/adapters/memory.adapter.ts +22 -0
- package/src/navigation/adapters/url.adapter.ts +61 -0
- package/src/navigation/history.ts +170 -0
- package/src/navigation/hooks/ancestors.hook.ts +71 -0
- package/src/navigation/hooks/location.hook.ts +21 -0
- package/src/navigation/hooks/navigate.hook.ts +43 -0
- package/src/navigation/hooks/scope.hook.ts +34 -0
- package/src/navigation/index.ts +17 -0
- package/src/navigation/link.tsx +110 -0
- package/src/navigation/location.ts +122 -0
- package/src/navigation/scope.context.tsx +267 -0
- package/src/navigation/snapshot.ts +79 -0
- package/src/schema/bind-read.ts +319 -0
- package/src/schema/binding.ts +91 -0
- package/src/schema/domain.hook.ts +16 -0
- package/src/schema/function.hook.ts +68 -0
- package/src/schema/index.ts +25 -0
- package/src/schema/kernel.hook.ts +17 -0
- package/src/schema/method.hook.ts +103 -0
- package/src/schema/tag.ts +19 -0
- package/src/schema/types.ts +150 -0
- package/src/schema/write.ts +80 -0
- package/src/selection/index.ts +5 -0
- package/src/selection/live.ts +142 -0
- package/src/selection/scope.context.tsx +151 -0
- package/src/selection/selection.hook.ts +18 -0
- package/src/session/boot/boot.ts +57 -0
- package/src/session/boot/ownership.ts +54 -0
- package/src/session/boot/status.ts +32 -0
- package/src/session/connection/connection.hook.ts +16 -0
- package/src/session/connection/connection.store.ts +58 -0
- package/src/session/index.ts +8 -0
- package/src/session/kernel-client.hook.ts +13 -0
- package/src/session/kernel-instance.hook.ts +22 -0
- package/src/session/provider.tsx +205 -0
- package/src/session/session.context.ts +27 -0
- package/src/session/shell.hook.ts +19 -0
- package/src/window/adopt.ts +57 -0
- package/src/window/hooks/close-guard.hook.ts +100 -0
- package/src/window/hooks/target-node.hook.ts +94 -0
- package/src/window/hooks/views-for.hook.ts +82 -0
- package/src/window/hooks/window.hook.ts +18 -0
- package/src/window/hooks/windows.hook.ts +40 -0
- package/src/window/host.tsx +347 -0
- package/src/window/hub.ts +156 -0
- package/src/window/index.ts +17 -0
- package/src/window/lifecycle/mount.ts +134 -0
- package/src/window/lifecycle/states.ts +23 -0
- package/src/window/placement.ts +84 -0
- 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
|
+
}
|