@astrale-os/shell 0.1.0 → 0.2.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 (284) hide show
  1. package/LICENSE +202 -0
  2. package/dist/application/apps.d.ts +18 -0
  3. package/dist/application/apps.d.ts.map +1 -0
  4. package/dist/application/apps.js +35 -0
  5. package/dist/application/namespaces.d.ts +21 -0
  6. package/dist/application/namespaces.d.ts.map +1 -0
  7. package/dist/application/namespaces.js +12 -0
  8. package/dist/application/session/open.d.ts +19 -0
  9. package/dist/application/session/open.d.ts.map +1 -0
  10. package/dist/application/session/open.js +46 -0
  11. package/dist/application/session/session.d.ts +41 -0
  12. package/dist/application/session/session.d.ts.map +1 -0
  13. package/dist/application/session/session.js +9 -0
  14. package/dist/application/session/token.d.ts +24 -0
  15. package/dist/application/session/token.d.ts.map +1 -0
  16. package/dist/application/session/token.js +51 -0
  17. package/dist/application/spaces.d.ts +17 -0
  18. package/dist/application/spaces.d.ts.map +1 -0
  19. package/dist/application/spaces.js +30 -0
  20. package/dist/application/user.d.ts +20 -0
  21. package/dist/application/user.d.ts.map +1 -0
  22. package/dist/application/user.js +26 -0
  23. package/dist/application/view.d.ts +20 -0
  24. package/dist/application/view.d.ts.map +1 -0
  25. package/dist/application/view.js +18 -0
  26. package/dist/{application/channels/children-channel.d.ts → channel/children.d.ts} +4 -4
  27. package/dist/channel/children.d.ts.map +1 -0
  28. package/dist/channel/listeners.d.ts +8 -0
  29. package/dist/channel/listeners.d.ts.map +1 -0
  30. package/dist/{adapters/message-port.transport.d.ts → channel/message-port.d.ts} +2 -2
  31. package/dist/channel/message-port.d.ts.map +1 -0
  32. package/dist/{application/channels → channel}/observer.d.ts +2 -1
  33. package/dist/channel/observer.d.ts.map +1 -0
  34. package/dist/{application/channels/parent-channel.d.ts → channel/parent.d.ts} +4 -4
  35. package/dist/channel/parent.d.ts.map +1 -0
  36. package/dist/{application/channels/parent-channel.js → channel/parent.js} +1 -1
  37. package/dist/channel/transport.d.ts +8 -0
  38. package/dist/channel/transport.d.ts.map +1 -0
  39. package/dist/index.d.ts +56 -41
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +23 -20
  42. package/dist/{domain/intent → intent}/middleware/capabilities.d.ts +2 -2
  43. package/dist/intent/middleware/capabilities.d.ts.map +1 -0
  44. package/dist/intent/middleware/logging.d.ts.map +1 -0
  45. package/dist/{domain/intent → intent}/middleware/rate-limit.d.ts +1 -1
  46. package/dist/intent/middleware/rate-limit.d.ts.map +1 -0
  47. package/dist/{application/intent/pipeline-runner.d.ts → intent/pipeline.d.ts} +3 -3
  48. package/dist/intent/pipeline.d.ts.map +1 -0
  49. package/dist/intent/policy.d.ts +40 -0
  50. package/dist/intent/policy.d.ts.map +1 -0
  51. package/dist/intent/policy.js +15 -0
  52. package/dist/{application/intent → intent}/request.d.ts +5 -14
  53. package/dist/intent/request.d.ts.map +1 -0
  54. package/dist/{application/intent → intent}/request.js +6 -20
  55. package/dist/intent/router.d.ts +31 -0
  56. package/dist/intent/router.d.ts.map +1 -0
  57. package/dist/{application/intent → intent}/router.js +1 -15
  58. package/dist/kernel/client.d.ts +33 -0
  59. package/dist/kernel/client.d.ts.map +1 -0
  60. package/dist/kernel/credential.d.ts +35 -0
  61. package/dist/kernel/credential.d.ts.map +1 -0
  62. package/dist/kernel/credential.js +43 -0
  63. package/dist/kernel/delegated.d.ts +17 -0
  64. package/dist/kernel/delegated.d.ts.map +1 -0
  65. package/dist/kernel/delegated.js +29 -0
  66. package/dist/{application/windowing/token-refresh.d.ts → kernel/delegation/refresh.d.ts} +2 -2
  67. package/dist/kernel/delegation/refresh.d.ts.map +1 -0
  68. package/dist/{application/windowing/token-refresh.js → kernel/delegation/refresh.js} +1 -1
  69. package/dist/{domain/identity/delegation.d.ts → kernel/delegation/token.d.ts} +1 -1
  70. package/dist/kernel/delegation/token.d.ts.map +1 -0
  71. package/dist/{adapters/kernel-client.adapter.d.ts → kernel/session.d.ts} +15 -12
  72. package/dist/kernel/session.d.ts.map +1 -0
  73. package/dist/{adapters/kernel-client.adapter.js → kernel/session.js} +10 -15
  74. package/dist/message/ctrl.d.ts +34 -0
  75. package/dist/message/ctrl.d.ts.map +1 -0
  76. package/dist/message/error.d.ts +9 -0
  77. package/dist/message/error.d.ts.map +1 -0
  78. package/dist/{application/windowing/protocol.d.ts → message/init.d.ts} +1 -1
  79. package/dist/message/init.d.ts.map +1 -0
  80. package/dist/{domain/message/registries.d.ts → message/intent.d.ts} +45 -37
  81. package/dist/message/intent.d.ts.map +1 -0
  82. package/dist/message/message.d.ts +10 -0
  83. package/dist/message/message.d.ts.map +1 -0
  84. package/dist/shell/access.d.ts +10 -0
  85. package/dist/shell/access.d.ts.map +1 -0
  86. package/dist/shell/access.js +19 -0
  87. package/dist/shell/build.d.ts +28 -0
  88. package/dist/shell/build.d.ts.map +1 -0
  89. package/dist/shell/build.js +58 -0
  90. package/dist/shell/config.d.ts +29 -0
  91. package/dist/shell/config.d.ts.map +1 -0
  92. package/dist/shell/create.d.ts +4 -0
  93. package/dist/shell/create.d.ts.map +1 -0
  94. package/dist/shell/create.js +5 -0
  95. package/dist/{application/windowing/child-init.d.ts → shell/sandboxed/boot.d.ts} +5 -5
  96. package/dist/shell/sandboxed/boot.d.ts.map +1 -0
  97. package/dist/{application/windowing/child-init.js → shell/sandboxed/boot.js} +5 -6
  98. package/dist/shell/sandboxed/guarded.d.ts +13 -0
  99. package/dist/shell/sandboxed/guarded.d.ts.map +1 -0
  100. package/dist/shell/sandboxed/guarded.js +28 -0
  101. package/dist/shell/sandboxed/pending.d.ts +14 -0
  102. package/dist/shell/sandboxed/pending.d.ts.map +1 -0
  103. package/dist/shell/sandboxed/pending.js +30 -0
  104. package/dist/shell/sandboxed/sandboxed.d.ts +4 -0
  105. package/dist/shell/sandboxed/sandboxed.d.ts.map +1 -0
  106. package/dist/shell/sandboxed/sandboxed.js +100 -0
  107. package/dist/shell/shell.d.ts +36 -0
  108. package/dist/shell/shell.d.ts.map +1 -0
  109. package/dist/shell/standalone.d.ts +4 -0
  110. package/dist/shell/standalone.d.ts.map +1 -0
  111. package/dist/shell/standalone.js +85 -0
  112. package/dist/{domain/window/config.d.ts → window/capabilities.d.ts} +3 -3
  113. package/dist/window/capabilities.d.ts.map +1 -0
  114. package/dist/{ports/shell-adapter.d.ts → window/embed/adapter.d.ts} +3 -2
  115. package/dist/window/embed/adapter.d.ts.map +1 -0
  116. package/dist/{adapters/iframe.shell-adapter.d.ts → window/embed/iframe.d.ts} +2 -2
  117. package/dist/window/embed/iframe.d.ts.map +1 -0
  118. package/dist/{application/windowing/lifecycle-controller.d.ts → window/lifecycle/states.d.ts} +4 -3
  119. package/dist/window/lifecycle/states.d.ts.map +1 -0
  120. package/dist/{application/windowing/lifecycle-controller.js → window/lifecycle/states.js} +1 -1
  121. package/dist/{domain/window/lifecycle.d.ts → window/lifecycle/transition.d.ts} +3 -3
  122. package/dist/window/lifecycle/transition.d.ts.map +1 -0
  123. package/dist/{domain/window/lifecycle.js → window/lifecycle/transition.js} +1 -1
  124. package/dist/window/mount/close.d.ts +27 -0
  125. package/dist/window/mount/close.d.ts.map +1 -0
  126. package/dist/window/mount/close.js +74 -0
  127. package/dist/window/mount/credential.d.ts +41 -0
  128. package/dist/window/mount/credential.d.ts.map +1 -0
  129. package/dist/window/mount/credential.js +74 -0
  130. package/dist/window/mount/handshake.d.ts.map +1 -0
  131. package/dist/{application/windowing → window/mount}/handshake.js +1 -1
  132. package/dist/{application/windowing/mount-controller.d.ts → window/mount/mount.d.ts} +7 -7
  133. package/dist/window/mount/mount.d.ts.map +1 -0
  134. package/dist/window/mount/mount.js +143 -0
  135. package/dist/window/mount/params.d.ts +73 -0
  136. package/dist/window/mount/params.d.ts.map +1 -0
  137. package/dist/window/mount/params.js +1 -0
  138. package/dist/window/mount/plain.d.ts +20 -0
  139. package/dist/window/mount/plain.d.ts.map +1 -0
  140. package/dist/window/mount/plain.js +46 -0
  141. package/dist/{domain/window → window}/window.d.ts +1 -1
  142. package/dist/window/window.d.ts.map +1 -0
  143. package/dist/window/window.js +1 -0
  144. package/package.json +11 -6
  145. package/src/application/apps.ts +60 -0
  146. package/src/application/namespaces.ts +38 -0
  147. package/src/application/session/open.ts +73 -0
  148. package/src/application/session/session.ts +46 -0
  149. package/src/application/session/token.ts +87 -0
  150. package/src/application/spaces.ts +52 -0
  151. package/src/application/user.ts +54 -0
  152. package/src/application/view.ts +42 -0
  153. package/src/{application/channels/children-channel.ts → channel/children.ts} +3 -3
  154. package/src/{application/channels/types.ts → channel/listeners.ts} +2 -7
  155. package/src/{adapters/message-port.transport.ts → channel/message-port.ts} +2 -2
  156. package/src/{application/channels → channel}/observer.ts +2 -1
  157. package/src/{application/channels/parent-channel.ts → channel/parent.ts} +4 -4
  158. package/src/{ports → channel}/transport.ts +2 -1
  159. package/src/index.ts +80 -88
  160. package/src/{domain/intent → intent}/middleware/capabilities.ts +2 -3
  161. package/src/{domain/intent → intent}/middleware/logging.ts +1 -1
  162. package/src/{domain/intent → intent}/middleware/rate-limit.ts +1 -1
  163. package/src/{application/intent/pipeline-runner.ts → intent/pipeline.ts} +3 -2
  164. package/src/intent/policy.ts +46 -0
  165. package/src/{application/intent → intent}/request.ts +14 -34
  166. package/src/{application/intent → intent}/router.ts +12 -49
  167. package/src/kernel/client.ts +33 -0
  168. package/src/kernel/credential.ts +78 -0
  169. package/src/kernel/delegated.ts +49 -0
  170. package/src/{application/windowing/token-refresh.ts → kernel/delegation/refresh.ts} +2 -2
  171. package/src/{adapters/kernel-client.adapter.ts → kernel/session.ts} +26 -33
  172. package/src/message/ctrl.ts +25 -0
  173. package/src/{domain/message/errors.ts → message/error.ts} +8 -0
  174. package/src/message/intent.ts +98 -0
  175. package/src/message/message.ts +11 -0
  176. package/src/shell/access.ts +26 -0
  177. package/src/shell/build.ts +95 -0
  178. package/src/shell/config.ts +32 -0
  179. package/src/shell/create.ts +9 -0
  180. package/src/{application/windowing/child-init.ts → shell/sandboxed/boot.ts} +10 -11
  181. package/src/shell/sandboxed/guarded.ts +32 -0
  182. package/src/shell/sandboxed/pending.ts +52 -0
  183. package/src/shell/sandboxed/sandboxed.ts +131 -0
  184. package/src/shell/shell.ts +37 -0
  185. package/src/shell/standalone.ts +97 -0
  186. package/src/{domain/window/config.ts → window/capabilities.ts} +2 -2
  187. package/src/{ports/shell-adapter.ts → window/embed/adapter.ts} +2 -1
  188. package/src/{adapters/iframe.shell-adapter.ts → window/embed/iframe.ts} +2 -2
  189. package/src/{application/windowing/lifecycle-controller.ts → window/lifecycle/states.ts} +3 -2
  190. package/src/{domain/window/lifecycle.ts → window/lifecycle/transition.ts} +2 -2
  191. package/src/window/mount/close.ts +111 -0
  192. package/src/window/mount/credential.ts +101 -0
  193. package/src/{application/windowing → window/mount}/handshake.ts +3 -2
  194. package/src/window/mount/mount.ts +185 -0
  195. package/src/window/mount/params.ts +64 -0
  196. package/src/window/mount/plain.ts +68 -0
  197. package/src/{domain/window → window}/window.ts +1 -1
  198. package/dist/adapters/iframe.shell-adapter.d.ts.map +0 -1
  199. package/dist/adapters/kernel-client.adapter.d.ts.map +0 -1
  200. package/dist/adapters/message-port.transport.d.ts.map +0 -1
  201. package/dist/application/channels/children-channel.d.ts.map +0 -1
  202. package/dist/application/channels/observer.d.ts.map +0 -1
  203. package/dist/application/channels/parent-channel.d.ts.map +0 -1
  204. package/dist/application/channels/types.d.ts +0 -12
  205. package/dist/application/channels/types.d.ts.map +0 -1
  206. package/dist/application/intent/pipeline-runner.d.ts.map +0 -1
  207. package/dist/application/intent/request.d.ts.map +0 -1
  208. package/dist/application/intent/router.d.ts +0 -56
  209. package/dist/application/intent/router.d.ts.map +0 -1
  210. package/dist/application/kernel-facade.d.ts +0 -33
  211. package/dist/application/kernel-facade.d.ts.map +0 -1
  212. package/dist/application/kernel-facade.js +0 -77
  213. package/dist/application/shell.d.ts +0 -100
  214. package/dist/application/shell.d.ts.map +0 -1
  215. package/dist/application/shell.js +0 -256
  216. package/dist/application/windowing/child-init.d.ts.map +0 -1
  217. package/dist/application/windowing/handshake.d.ts.map +0 -1
  218. package/dist/application/windowing/lifecycle-controller.d.ts.map +0 -1
  219. package/dist/application/windowing/mount-controller.d.ts.map +0 -1
  220. package/dist/application/windowing/mount-controller.js +0 -277
  221. package/dist/application/windowing/protocol.d.ts.map +0 -1
  222. package/dist/application/windowing/token-refresh.d.ts.map +0 -1
  223. package/dist/domain/identity/delegation.d.ts.map +0 -1
  224. package/dist/domain/identity/path.d.ts +0 -11
  225. package/dist/domain/identity/path.d.ts.map +0 -1
  226. package/dist/domain/identity/path.js +0 -13
  227. package/dist/domain/identity/resolve-self.d.ts +0 -21
  228. package/dist/domain/identity/resolve-self.d.ts.map +0 -1
  229. package/dist/domain/identity/resolve-self.js +0 -36
  230. package/dist/domain/intent/middleware/capabilities.d.ts.map +0 -1
  231. package/dist/domain/intent/middleware/logging.d.ts.map +0 -1
  232. package/dist/domain/intent/middleware/rate-limit.d.ts.map +0 -1
  233. package/dist/domain/intent/pipeline.d.ts +0 -3
  234. package/dist/domain/intent/pipeline.d.ts.map +0 -1
  235. package/dist/domain/intent/router.d.ts +0 -6
  236. package/dist/domain/intent/router.d.ts.map +0 -1
  237. package/dist/domain/message/errors.d.ts +0 -2
  238. package/dist/domain/message/errors.d.ts.map +0 -1
  239. package/dist/domain/message/registries.d.ts.map +0 -1
  240. package/dist/domain/message/types.d.ts +0 -30
  241. package/dist/domain/message/types.d.ts.map +0 -1
  242. package/dist/domain/window/config.d.ts.map +0 -1
  243. package/dist/domain/window/lifecycle.d.ts.map +0 -1
  244. package/dist/domain/window/window.d.ts.map +0 -1
  245. package/dist/ports/kernel-client.d.ts +0 -33
  246. package/dist/ports/kernel-client.d.ts.map +0 -1
  247. package/dist/ports/shell-adapter.d.ts.map +0 -1
  248. package/dist/ports/transport.d.ts +0 -7
  249. package/dist/ports/transport.d.ts.map +0 -1
  250. package/src/application/kernel-facade.ts +0 -143
  251. package/src/application/shell.ts +0 -433
  252. package/src/application/windowing/mount-controller.ts +0 -335
  253. package/src/domain/identity/path.ts +0 -14
  254. package/src/domain/identity/resolve-self.ts +0 -38
  255. package/src/domain/intent/pipeline.ts +0 -3
  256. package/src/domain/intent/router.ts +0 -6
  257. package/src/domain/message/registries.ts +0 -76
  258. package/src/domain/message/types.ts +0 -32
  259. package/src/ports/kernel-client.ts +0 -32
  260. /package/dist/{application/channels/children-channel.js → channel/children.js} +0 -0
  261. /package/dist/{application/channels/types.js → channel/listeners.js} +0 -0
  262. /package/dist/{adapters/message-port.transport.js → channel/message-port.js} +0 -0
  263. /package/dist/{application/channels → channel}/observer.js +0 -0
  264. /package/dist/{ports → channel}/transport.js +0 -0
  265. /package/dist/{domain/intent → intent}/middleware/capabilities.js +0 -0
  266. /package/dist/{domain/intent → intent}/middleware/logging.d.ts +0 -0
  267. /package/dist/{domain/intent → intent}/middleware/logging.js +0 -0
  268. /package/dist/{domain/intent → intent}/middleware/rate-limit.js +0 -0
  269. /package/dist/{application/intent/pipeline-runner.js → intent/pipeline.js} +0 -0
  270. /package/dist/{domain/intent/pipeline.js → kernel/client.js} +0 -0
  271. /package/dist/{domain/identity/delegation.js → kernel/delegation/token.js} +0 -0
  272. /package/dist/{domain/intent/router.js → message/ctrl.js} +0 -0
  273. /package/dist/{domain/message/errors.js → message/error.js} +0 -0
  274. /package/dist/{application/windowing/protocol.js → message/init.js} +0 -0
  275. /package/dist/{domain/message/registries.js → message/intent.js} +0 -0
  276. /package/dist/{domain/message/types.js → message/message.js} +0 -0
  277. /package/dist/{domain/window → shell}/config.js +0 -0
  278. /package/dist/{domain/window/window.js → shell/shell.js} +0 -0
  279. /package/dist/{ports/kernel-client.js → window/capabilities.js} +0 -0
  280. /package/dist/{ports/shell-adapter.js → window/embed/adapter.js} +0 -0
  281. /package/dist/{adapters/iframe.shell-adapter.js → window/embed/iframe.js} +0 -0
  282. /package/dist/{application/windowing → window/mount}/handshake.d.ts +0 -0
  283. /package/src/{domain/identity/delegation.ts → kernel/delegation/token.ts} +0 -0
  284. /package/src/{application/windowing/protocol.ts → message/init.ts} +0 -0
@@ -0,0 +1,37 @@
1
+ import type { AppsNamespace } from '../application/apps'
2
+ import type { SpacesNamespace } from '../application/spaces'
3
+ import type { UserNamespace } from '../application/user'
4
+ import type { ChildrenChannel } from '../channel/children'
5
+ import type { ObserverHandler } from '../channel/observer'
6
+ import type { ParentChannel } from '../channel/parent'
7
+ import type { IntentMiddleware } from '../intent/pipeline'
8
+ import type { LocalIntentHandler } from '../intent/router'
9
+ import type { KernelClient } from '../kernel/client'
10
+ import type { IntentRegistry } from '../message/intent'
11
+ import type { CloseResult, MountedWindow, MountParams } from '../window/mount/params'
12
+ import type { Window } from '../window/window'
13
+
14
+ export interface Shell {
15
+ readonly parent: ParentChannel | null
16
+ readonly children: ChildrenChannel
17
+ readonly kernel: KernelClient
18
+ readonly windows: ReadonlyMap<string, Window>
19
+ readonly targetNodeId?: string
20
+
21
+ /** OS plane: the caller as a shell-domain `User` — self, current space, roles. */
22
+ readonly user: UserNamespace
23
+ /** OS plane: the caller's readable spaces — list, get, create. */
24
+ readonly spaces: SpacesNamespace
25
+ /** OS plane: apps — install, list per space, open (returns an `AppSession`). */
26
+ readonly apps: AppsNamespace
27
+
28
+ use(middleware: IntentMiddleware): Shell
29
+ onMessage(handler: ObserverHandler): () => void
30
+ onIntent<K extends keyof IntentRegistry>(name: K, handler: LocalIntentHandler<K>): () => void
31
+
32
+ mount(params: MountParams): Promise<MountedWindow>
33
+ close(windowId: string, opts?: { force?: boolean; timeoutMs?: number }): Promise<CloseResult>
34
+
35
+ init(): Promise<Shell>
36
+ dispose(): Promise<void>
37
+ }
@@ -0,0 +1,97 @@
1
+ import type { MountController } from '../window/mount/mount'
2
+ import type { Window } from '../window/window'
3
+ import type { StandaloneConfig } from './config'
4
+ import type { Shell } from './shell'
5
+
6
+ import { createChildrenChannel } from '../channel/children'
7
+ import { createMessageObserver } from '../channel/observer'
8
+ import { createIntentPipeline } from '../intent/pipeline'
9
+ import { createIntentRouter } from '../intent/router'
10
+ import { createKernelFacade } from '../kernel/credential'
11
+ import { createKernelClientAdapter } from '../kernel/session'
12
+ import { createLifecycleController } from '../window/lifecycle/states'
13
+ import { createMountController } from '../window/mount/mount'
14
+ import { buildShellFacade } from './build'
15
+
16
+ const DEFAULT_ROOT_WINDOW_ID = 'root'
17
+
18
+ export function createStandaloneShell(config: StandaloneConfig): Shell {
19
+ const windowId = config.windowId ?? DEFAULT_ROOT_WINDOW_ID
20
+ const observer = createMessageObserver()
21
+ const getCredential =
22
+ config.getCredential ??
23
+ (() => {
24
+ if (config.credential === undefined) {
25
+ throw new Error('Standalone shell requires credential or getCredential')
26
+ }
27
+ return config.credential
28
+ })
29
+ const adapter = createKernelClientAdapter({
30
+ url: config.kernelUrl,
31
+ schema: config.schema,
32
+ getCredential,
33
+ fetch: config.fetch,
34
+ })
35
+ const kernel = createKernelFacade({
36
+ adapter,
37
+ getCredential,
38
+ url: config.kernelUrl,
39
+ mintIdentity: config.mintIdentity,
40
+ })
41
+
42
+ const lifecycle = createLifecycleController()
43
+ const windows = new Map<string, Window>()
44
+
45
+ const childrenChannel = createChildrenChannel({
46
+ observer,
47
+ onDeliveryFailed: (toWindowId) => lifecycle.markCrashed(toWindowId),
48
+ })
49
+
50
+ const pipeline = createIntentPipeline()
51
+ const router = createIntentRouter({
52
+ selfWindowId: windowId,
53
+ pipeline,
54
+ parent: null,
55
+ children: childrenChannel,
56
+ windows,
57
+ })
58
+
59
+ childrenChannel.on('intent', (fromWindowId, message) => {
60
+ void router.handleInbound('child', message, fromWindowId)
61
+ })
62
+
63
+ let mountController: MountController | null = null
64
+ function getMountController(): MountController {
65
+ if (mountController) return mountController
66
+ if (!config.adapter) {
67
+ throw new Error('Shell.mount() requires a ShellAdapter in the config')
68
+ }
69
+ mountController = createMountController({
70
+ adapter: config.adapter,
71
+ kernel,
72
+ kernelUrl: config.kernelUrl,
73
+ mintIdentity: config.mintIdentity,
74
+ childrenChannel,
75
+ windows,
76
+ lifecycle,
77
+ })
78
+ return mountController
79
+ }
80
+
81
+ return buildShellFacade({
82
+ parent: null,
83
+ childrenChannel,
84
+ kernel,
85
+ pipeline,
86
+ observer,
87
+ lifecycle,
88
+ windows,
89
+ getMountController,
90
+ onIntent: (name, handler) => router.onLocal(name, handler),
91
+ onDispose: async () => {
92
+ if (mountController) await mountController.dispose()
93
+ childrenChannel.dispose()
94
+ adapter.disconnect()
95
+ },
96
+ })
97
+ }
@@ -1,4 +1,4 @@
1
- import type { IntentRegistry } from '../message/registries'
1
+ import type { IntentRegistry } from '../message/intent'
2
2
 
3
3
  export interface SandboxProfile {
4
4
  readonly allowScripts: boolean
@@ -18,7 +18,7 @@ export interface WindowConfig {
18
18
  readonly capabilities: WindowCapabilities
19
19
  /**
20
20
  * Sandbox profile for the iframe.
21
- * - `undefined` → adapter default (`DEFAULT_SANDBOX` in iframe.shell-adapter).
21
+ * - `undefined` → adapter default (`DEFAULT_SANDBOX` in embed/iframe).
22
22
  * - `SandboxProfile` → explicit flag set, becomes the `sandbox=…` attribute.
23
23
  * - `null` → opt out: no `sandbox` attribute on the iframe at all (the
24
24
  * browser's cross-origin policy is the only remaining isolation). Used
@@ -1,4 +1,4 @@
1
- import type { WindowConfig } from '../domain/window/config'
1
+ import type { WindowConfig } from '../capabilities'
2
2
 
3
3
  export interface WindowHandle {
4
4
  readonly element: HTMLIFrameElement | HTMLElement
@@ -6,6 +6,7 @@ export interface WindowHandle {
6
6
  readonly origin: string
7
7
  }
8
8
 
9
+ /** How a window's host element gets created — the shell's embedding extension point. */
9
10
  export interface ShellAdapter {
10
11
  mount(config: WindowConfig): { handle: WindowHandle; dispose: () => void }
11
12
  }
@@ -1,5 +1,5 @@
1
- import type { SandboxProfile } from '../domain/window/config'
2
- import type { ShellAdapter, WindowHandle } from '../ports/shell-adapter'
1
+ import type { SandboxProfile } from '../capabilities'
2
+ import type { ShellAdapter, WindowHandle } from './adapter'
3
3
 
4
4
  export type IframeShellAdapterConfig = {
5
5
  /**
@@ -1,7 +1,8 @@
1
- import type { WindowState } from '../../domain/window/window'
1
+ import type { WindowState } from '../window'
2
2
 
3
- import { transition, type LifecycleEvent } from '../../domain/window/lifecycle'
3
+ import { transition, type LifecycleEvent } from './transition'
4
4
 
5
+ /** Live per-window lifecycle states, advanced only through the transition machine. */
5
6
  export interface LifecycleController {
6
7
  register(windowId: string, initial?: WindowState): void
7
8
  apply(windowId: string, event: LifecycleEvent): WindowState | null
@@ -1,4 +1,4 @@
1
- import type { WindowState } from './window'
1
+ import type { WindowState } from '../window'
2
2
 
3
3
  export type LifecycleEvent =
4
4
  | { kind: 'hide' }
@@ -11,7 +11,7 @@ export type LifecycleEvent =
11
11
  export type TransitionResult = { ok: true; next: WindowState } | { ok: false; reason: string }
12
12
 
13
13
  /**
14
- * Pure state-machine transition. Used by the lifecycle controller
14
+ * Pure state-machine transition. Used by the lifecycle state map
15
15
  * no timers, no side effects here.
16
16
  */
17
17
  export function transition(state: WindowState, event: LifecycleEvent): TransitionResult {
@@ -0,0 +1,111 @@
1
+ import type { ChildrenChannelController } from '../../channel/children'
2
+ import type { TokenRefresher } from '../../kernel/delegation/refresh'
3
+ import type { WindowHandle } from '../embed/adapter'
4
+ import type { LifecycleController } from '../lifecycle/states'
5
+ import type { Window } from '../window'
6
+ import type { CloseResult } from './params'
7
+
8
+ const DEFAULT_CLOSE_TIMEOUT = 3_000
9
+ /** Trusted sender id stamped on the parent's own close-protocol intents. */
10
+ const PARENT_SENDER_ID = '<parent>'
11
+
12
+ /** A mounted window's runtime entry — everything `close()` must tear down. */
13
+ export type MountEntry = {
14
+ readonly handle: WindowHandle
15
+ readonly dispose: () => void
16
+ readonly refresher: TokenRefresher
17
+ /** Plain (no-handshake) mount: no transport, no token, no close ack. */
18
+ readonly plain?: boolean
19
+ }
20
+
21
+ export type CloseDeps = {
22
+ entries: Map<string, MountEntry>
23
+ children: ChildrenChannelController
24
+ windows: Map<string, Window>
25
+ lifecycle: LifecycleController
26
+ }
27
+
28
+ export type Closer = (
29
+ windowId: string,
30
+ opts?: { force?: boolean; timeoutMs?: number },
31
+ ) => Promise<CloseResult>
32
+
33
+ /** The close flow: willClose → ack/refuse (unless forced or plain), then ordered teardown. */
34
+ export function createCloser(deps: CloseDeps): Closer {
35
+ const { entries, children, windows, lifecycle } = deps
36
+
37
+ return async (windowId, opts) => {
38
+ const entry = entries.get(windowId)
39
+ if (!entry) return { kind: 'noop' }
40
+
41
+ const timeoutMs = opts?.timeoutMs ?? DEFAULT_CLOSE_TIMEOUT
42
+
43
+ // Plain mounts have no channel to ack a close; always force.
44
+ if (!opts?.force && !entry.plain) {
45
+ // Move the window into `closing` so the state machine accepts the
46
+ // subsequent `closeRefuse`/`forceClose` transition (both are only valid
47
+ // from `closing`).
48
+ lifecycle.apply(windowId, { kind: 'requestClose' })
49
+ const accepted = await requestCloseAck(children, windowId, timeoutMs)
50
+ if (accepted.kind === 'refused') {
51
+ lifecycle.apply(windowId, { kind: 'closeRefuse' })
52
+ return { kind: 'refused', reason: accepted.reason }
53
+ }
54
+ }
55
+
56
+ lifecycle.apply(windowId, { kind: 'forceClose' })
57
+ entry.refresher.stop()
58
+ // Drop the entry before detaching: `detach` synchronously closes the
59
+ // transport, whose `onClose` re-enters `close()`. Removing the entry first
60
+ // makes that re-entrant call's `entries.has(windowId)` guard short-circuit,
61
+ // so `entry.dispose()` runs exactly once (adapters need not be idempotent).
62
+ entries.delete(windowId)
63
+ children.detach(windowId)
64
+ windows.delete(windowId)
65
+ entry.dispose()
66
+ lifecycle.remove(windowId)
67
+ return { kind: 'closed' }
68
+ }
69
+ }
70
+
71
+ type CloseAck = { kind: 'ack' } | { kind: 'refused'; reason?: string } | { kind: 'timeout' }
72
+
73
+ async function requestCloseAck(
74
+ children: ChildrenChannelController,
75
+ windowId: string,
76
+ timeoutMs: number,
77
+ ): Promise<CloseAck> {
78
+ return new Promise((resolve) => {
79
+ let settled = false
80
+ const off = children.on('intent', (from, message) => {
81
+ if (from !== windowId) return
82
+ if (message.envelope.name === 'closeAck') {
83
+ settled = true
84
+ off()
85
+ clearTimeout(timer)
86
+ resolve({ kind: 'ack' })
87
+ } else if (message.envelope.name === 'closeRefuse') {
88
+ settled = true
89
+ off()
90
+ clearTimeout(timer)
91
+ const reason = (message.envelope.payload as { reason?: string }).reason
92
+ resolve({ kind: 'refused', reason })
93
+ }
94
+ })
95
+ const timer = setTimeout(() => {
96
+ if (settled) return
97
+ off()
98
+ resolve({ kind: 'timeout' })
99
+ }, timeoutMs)
100
+
101
+ children.send(windowId, {
102
+ type: 'intent',
103
+ version: 1,
104
+ envelope: {
105
+ name: 'willClose',
106
+ payload: {},
107
+ sender: { windowId: PARENT_SENDER_ID },
108
+ },
109
+ })
110
+ })
111
+ }
@@ -0,0 +1,101 @@
1
+ import type { ChildrenChannelController } from '../../channel/children'
2
+ import type { KernelClient } from '../../kernel/client'
3
+ import type { Window } from '../window'
4
+ import type { MountCredential } from './params'
5
+
6
+ import { createTokenRefresher, type TokenRefresher } from '../../kernel/delegation/refresh'
7
+
8
+ export type AcquiredCredential = { credential: string; expiresAt: number }
9
+
10
+ /** No-op refresher for windows with no token to refresh (plain mounts, no-refresh credentials). */
11
+ export const NOOP_REFRESHER: TokenRefresher = {
12
+ start: () => {},
13
+ stop: () => {},
14
+ forceRefreshNow: () => Promise.resolve(),
15
+ }
16
+
17
+ /**
18
+ * Pick where the window's credential comes from — and how it refreshes.
19
+ *
20
+ * A PROVIDED credential (the app-principal path — e.g. `app::open`'s composed
21
+ * credential) is used as-is and refreshes through its own callback (re-calling
22
+ * `app::open`); no callback → no refresh (the window lives at most until
23
+ * `expiresAt`). Otherwise the shell mints — and re-mints — its own delegation.
24
+ */
25
+ export function credentialSource(opts: {
26
+ kernel: KernelClient
27
+ ttlSeconds: number
28
+ provided?: MountCredential
29
+ }): {
30
+ acquire: () => Promise<AcquiredCredential>
31
+ refreshMint: (() => Promise<AcquiredCredential>) | null
32
+ } {
33
+ const mint = async (): Promise<AcquiredCredential> => {
34
+ const credential = await opts.kernel.mintDelegation()
35
+ return { credential, expiresAt: Date.now() + opts.ttlSeconds * 1_000 }
36
+ }
37
+
38
+ const provided = opts.provided
39
+ if (!provided) return { acquire: mint, refreshMint: mint }
40
+
41
+ return {
42
+ acquire: () => Promise.resolve({ credential: provided.token, expiresAt: provided.expiresAt }),
43
+ refreshMint: provided.refresh
44
+ ? async () => {
45
+ const next = await provided.refresh!()
46
+ return { credential: next.token, expiresAt: next.expiresAt }
47
+ }
48
+ : null,
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Wire the per-window refresher: on each refresh, update the Window store and
54
+ * push a `ctrl:tokenRefresh` down the child's channel.
55
+ */
56
+ export function wireTokenRefresher(opts: {
57
+ windowId: string
58
+ functionId: string
59
+ initial: AcquiredCredential
60
+ refreshMint: (() => Promise<AcquiredCredential>) | null
61
+ windows: Map<string, Window>
62
+ children: ChildrenChannelController
63
+ onFailure: () => void
64
+ }): TokenRefresher {
65
+ const { windowId, refreshMint } = opts
66
+ if (!refreshMint) return NOOP_REFRESHER
67
+
68
+ // The live expiry advances on every refresh. The Window store only keeps
69
+ // the credential, so track expiry here — otherwise `token()` keeps
70
+ // returning the original expiry and `schedule()` collapses its lead time
71
+ // to 0, busy-looping the mint forever after the first refresh.
72
+ let currentExpiresAt = opts.initial.expiresAt
73
+
74
+ return createTokenRefresher({
75
+ functionId: opts.functionId,
76
+ token: () => {
77
+ const stored = opts.windows.get(windowId)
78
+ return {
79
+ credential: stored?.delegationToken ?? opts.initial.credential,
80
+ expiresAt: currentExpiresAt,
81
+ issuedAt: 0,
82
+ scope: { functionId: opts.functionId },
83
+ }
84
+ },
85
+ setToken: (next) => {
86
+ currentExpiresAt = next.expiresAt
87
+ const stored = opts.windows.get(windowId)
88
+ if (stored) {
89
+ opts.windows.set(windowId, { ...stored, delegationToken: next.credential })
90
+ }
91
+ opts.children.send(windowId, {
92
+ type: 'ctrl',
93
+ version: 1,
94
+ action: 'tokenRefresh',
95
+ data: { delegationToken: next.credential, tokenExpiresAt: next.expiresAt },
96
+ })
97
+ },
98
+ mint: refreshMint,
99
+ onFailure: opts.onFailure,
100
+ })
101
+ }
@@ -1,6 +1,7 @@
1
- import type { CtrlMessage, ShellMessage } from '../../domain/message/types'
1
+ import type { CtrlMessage } from '../../message/ctrl'
2
+ import type { ShellMessage } from '../../message/message'
2
3
 
3
- import { INIT_RESPONSE_TYPE, isInitRequest } from './protocol'
4
+ import { INIT_RESPONSE_TYPE, isInitRequest } from '../../message/init'
4
5
 
5
6
  export type HandshakeParams = {
6
7
  readonly iframe: HTMLIFrameElement
@@ -0,0 +1,185 @@
1
+ import type { ChildrenChannelController } from '../../channel/children'
2
+ import type { KernelClient } from '../../kernel/client'
3
+ import type { ShellAdapter } from '../embed/adapter'
4
+ import type { LifecycleController } from '../lifecycle/states'
5
+ import type { Window } from '../window'
6
+ import type { MountEntry } from './close'
7
+ import type { CloseResult, MountedWindow, MountParams } from './params'
8
+
9
+ import { createMessagePortTransport } from '../../channel/message-port'
10
+ import { createCloser } from './close'
11
+ import { credentialSource, wireTokenRefresher } from './credential'
12
+ import { runParentHandshake } from './handshake'
13
+ import { mountPlain } from './plain'
14
+
15
+ const DEFAULT_HANDSHAKE_TIMEOUT = 5_000
16
+ const DEFAULT_DELEGATION_TTL = 3_600
17
+
18
+ export type MountControllerConfig = {
19
+ adapter: ShellAdapter
20
+ kernel: KernelClient
21
+ kernelUrl: string
22
+ mintIdentity?: string
23
+ childrenChannel: ChildrenChannelController
24
+ windows: Map<string, Window>
25
+ lifecycle: LifecycleController
26
+ }
27
+
28
+ export interface MountController {
29
+ mount(params: MountParams): Promise<MountedWindow>
30
+ close(windowId: string, opts?: { force?: boolean; timeoutMs?: number }): Promise<CloseResult>
31
+ dispose(): Promise<void>
32
+ }
33
+
34
+ export function createMountController(config: MountControllerConfig): MountController {
35
+ const { adapter, kernel, kernelUrl, mintIdentity, childrenChannel, windows, lifecycle } = config
36
+ // The child iframe is always a DIFFERENT origin from this parent window, so a
37
+ // relative kernelUrl (e.g. the SPA's same-origin `/api/`) handed across the
38
+ // handshake would resolve against the iframe's OWN origin and POST to the
39
+ // wrong host. Absolutize it against the parent's location once so every child
40
+ // calls THIS origin's kernel. (The parent's own calls keep using the relative
41
+ // form via its kernel client / fetch.)
42
+ const childKernelUrl = new URL(kernelUrl, window.location.href).href
43
+ const entries = new Map<string, MountEntry>()
44
+ let seq = 0
45
+ let disposed = false
46
+
47
+ const close = createCloser({ entries, children: childrenChannel, windows, lifecycle })
48
+
49
+ function nextWindowId(functionId: string) {
50
+ seq += 1
51
+ const slug = functionId.replace(/[^a-z0-9]/gi, '').slice(-8) || 'fn'
52
+ return `win-${slug}-${seq}`
53
+ }
54
+
55
+ function originOf(url: string): string {
56
+ try {
57
+ return new URL(url, window.location.href).origin
58
+ } catch {
59
+ throw new Error(`Invalid mount url: ${url}`)
60
+ }
61
+ }
62
+
63
+ const controller: MountController = {
64
+ async mount(params) {
65
+ if (disposed) throw new Error('MountController is disposed')
66
+
67
+ // Plain (no-handshake) path: hosted views that don't speak the shell
68
+ // protocol. Skip mint/handshake/transport/refresher; the iframe still
69
+ // renders and `close()` falls through to dispose.
70
+ if (params.handshake === 'none') {
71
+ const windowId = nextWindowId(params.functionId)
72
+ return mountPlain({ adapter, windows, lifecycle, entries, close }, windowId, params)
73
+ }
74
+
75
+ const ttlSeconds = params.delegationTtlSeconds ?? DEFAULT_DELEGATION_TTL
76
+ const handshakeTimeout = params.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT
77
+ const expectedOrigin = originOf(params.url)
78
+ const windowId = nextWindowId(params.functionId)
79
+
80
+ const { acquire, refreshMint } = credentialSource({
81
+ kernel,
82
+ ttlSeconds,
83
+ provided: params.credential,
84
+ })
85
+
86
+ // Mint the token and create the iframe in parallel — the iframe
87
+ // doesn't need the credential until the handshake fires.
88
+ const [{ credential, expiresAt }, mounted] = await Promise.all([
89
+ acquire(),
90
+ Promise.resolve(
91
+ adapter.mount({
92
+ functionId: params.functionId,
93
+ isolation: 'sandboxed',
94
+ capabilities: params.capabilities,
95
+ sandbox: params.sandbox,
96
+ }),
97
+ ),
98
+ ])
99
+ const { handle, dispose: disposeHandle } = mounted
100
+
101
+ if (!(handle.element instanceof HTMLIFrameElement)) {
102
+ disposeHandle()
103
+ throw new Error('Shell adapter did not produce an HTMLIFrameElement')
104
+ }
105
+
106
+ params.host.appendChild(handle.element)
107
+ handle.element.src = params.url
108
+
109
+ let handshakeResult
110
+ try {
111
+ handshakeResult = await runParentHandshake({
112
+ iframe: handle.element,
113
+ expectedOrigin,
114
+ windowId,
115
+ delegationToken: credential,
116
+ tokenExpiresAt: expiresAt,
117
+ kernelUrl: childKernelUrl,
118
+ functionId: params.functionId,
119
+ targetNodeId: params.targetNodeId,
120
+ mintIdentity,
121
+ timeoutMs: handshakeTimeout,
122
+ })
123
+ } catch (err) {
124
+ disposeHandle()
125
+ throw err
126
+ }
127
+
128
+ const transport = createMessagePortTransport({
129
+ port: handshakeResult.port,
130
+ onClose: () => {
131
+ if (!entries.has(windowId)) return
132
+ lifecycle.markCrashed(windowId)
133
+ void close(windowId, { force: true }).catch(() => {})
134
+ },
135
+ })
136
+
137
+ childrenChannel.attach(windowId, transport)
138
+ windows.set(windowId, {
139
+ windowId,
140
+ functionId: params.functionId,
141
+ targetNodeId: params.targetNodeId,
142
+ isolation: 'sandboxed',
143
+ state: 'active',
144
+ delegationToken: credential,
145
+ capabilities: params.capabilities,
146
+ })
147
+ lifecycle.register(windowId, 'active')
148
+
149
+ const refresher = wireTokenRefresher({
150
+ windowId,
151
+ functionId: params.functionId,
152
+ initial: { credential, expiresAt },
153
+ refreshMint,
154
+ windows,
155
+ children: childrenChannel,
156
+ onFailure: () => {
157
+ void close(windowId, { force: false }).catch(() => {})
158
+ },
159
+ })
160
+ refresher.start()
161
+
162
+ entries.set(windowId, { handle, dispose: disposeHandle, refresher })
163
+
164
+ return {
165
+ windowId,
166
+ get window() {
167
+ return windows.get(windowId)!
168
+ },
169
+ handle: { element: handle.element },
170
+ close: (opts) => close(windowId, opts),
171
+ }
172
+ },
173
+
174
+ close,
175
+
176
+ async dispose() {
177
+ if (disposed) return
178
+ disposed = true
179
+ const ids = [...entries.keys()]
180
+ await Promise.all(ids.map((id) => close(id, { force: true })))
181
+ },
182
+ }
183
+
184
+ return controller
185
+ }
@@ -0,0 +1,64 @@
1
+ import type { SandboxProfile, WindowCapabilities } from '../capabilities'
2
+ import type { Window } from '../window'
3
+
4
+ /**
5
+ * A PRE-MINTED credential to hand the mounted iframe instead of the shell's
6
+ * own delegation mint — the app-principal path. The caller composes it from
7
+ * `app::open()`'s components: the USER mints a pre-attenuated delegation
8
+ * (`union(intersect(roles, self), intersect(self, app))` — only the user can
9
+ * sign it, so the app never holds the raw user token), then the final
10
+ * envelope is minted AS the app (`session.as(appCredential)`). The iframe
11
+ * then acts as the application with exactly that authority.
12
+ */
13
+ export type MountCredential = {
14
+ /** The token the iframe authenticates with. */
15
+ readonly token: string
16
+ /** Epoch-ms expiry — drives the refresh schedule. */
17
+ readonly expiresAt: number
18
+ /**
19
+ * Re-mint before expiry (typically re-calling `app::open` and returning its
20
+ * fresh `credential`/`expiresAt`). Omit to disable refresh — the window then
21
+ * lives at most until `expiresAt`.
22
+ */
23
+ readonly refresh?: () => Promise<{ token: string; expiresAt: number }>
24
+ }
25
+
26
+ export type MountParams = {
27
+ readonly host: HTMLElement
28
+ readonly url: string
29
+ readonly functionId: string
30
+ readonly capabilities: WindowCapabilities
31
+ readonly targetNodeId?: string
32
+ /** See `WindowConfig.sandbox` — `null` opts the iframe out of the attribute. */
33
+ readonly sandbox?: SandboxProfile | null
34
+ readonly handshakeTimeoutMs?: number
35
+ readonly delegationTtlSeconds?: number
36
+ /**
37
+ * Mount with THIS credential instead of self-minting one (see
38
+ * {@link MountCredential}). When present, `delegationTtlSeconds` is inert —
39
+ * the provided `expiresAt` (and `refresh`) govern the token's lifetime.
40
+ */
41
+ readonly credential?: MountCredential
42
+ /**
43
+ * `'shell'` (default) runs the parent-side handshake, opening a MessagePort
44
+ * transport with delegation tokens and an intent channel. `'none'` mounts a
45
+ * plain iframe with no transport — used for hosted views that don't speak
46
+ * the shell protocol (`BlaxelComputer.deployFunction(kind:'view')`), so they
47
+ * render immediately instead of paying the 5 s handshake timeout. In `'none'`
48
+ * mode `capabilities` / `targetNodeId` / `delegationTtlSeconds` are inert
49
+ * (no channel to deliver them).
50
+ */
51
+ readonly handshake?: 'shell' | 'none'
52
+ }
53
+
54
+ export type CloseResult =
55
+ | { kind: 'closed' }
56
+ | { kind: 'refused'; reason?: string }
57
+ | { kind: 'noop' }
58
+
59
+ export type MountedWindow = {
60
+ readonly windowId: string
61
+ readonly window: Window
62
+ readonly handle: { element: HTMLIFrameElement | HTMLElement }
63
+ close(opts?: { force?: boolean; timeoutMs?: number }): Promise<CloseResult>
64
+ }