@astrale-os/shell 0.1.1 → 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.
- package/LICENSE +202 -0
- package/dist/application/apps.d.ts +18 -0
- package/dist/application/apps.d.ts.map +1 -0
- package/dist/application/apps.js +35 -0
- package/dist/application/namespaces.d.ts +21 -0
- package/dist/application/namespaces.d.ts.map +1 -0
- package/dist/application/namespaces.js +12 -0
- package/dist/application/session/open.d.ts +19 -0
- package/dist/application/session/open.d.ts.map +1 -0
- package/dist/application/session/open.js +46 -0
- package/dist/application/session/session.d.ts +41 -0
- package/dist/application/session/session.d.ts.map +1 -0
- package/dist/application/session/session.js +9 -0
- package/dist/application/session/token.d.ts +24 -0
- package/dist/application/session/token.d.ts.map +1 -0
- package/dist/application/session/token.js +51 -0
- package/dist/application/spaces.d.ts +17 -0
- package/dist/application/spaces.d.ts.map +1 -0
- package/dist/application/spaces.js +30 -0
- package/dist/application/user.d.ts +20 -0
- package/dist/application/user.d.ts.map +1 -0
- package/dist/application/user.js +26 -0
- package/dist/application/view.d.ts +20 -0
- package/dist/application/view.d.ts.map +1 -0
- package/dist/application/view.js +18 -0
- package/dist/{application/channels/children-channel.d.ts → channel/children.d.ts} +4 -4
- package/dist/channel/children.d.ts.map +1 -0
- package/dist/channel/listeners.d.ts +8 -0
- package/dist/channel/listeners.d.ts.map +1 -0
- package/dist/{adapters/message-port.transport.d.ts → channel/message-port.d.ts} +2 -2
- package/dist/channel/message-port.d.ts.map +1 -0
- package/dist/{application/channels → channel}/observer.d.ts +2 -1
- package/dist/channel/observer.d.ts.map +1 -0
- package/dist/{application/channels/parent-channel.d.ts → channel/parent.d.ts} +4 -4
- package/dist/channel/parent.d.ts.map +1 -0
- package/dist/{application/channels/parent-channel.js → channel/parent.js} +1 -1
- package/dist/channel/transport.d.ts +8 -0
- package/dist/channel/transport.d.ts.map +1 -0
- package/dist/index.d.ts +56 -41
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -20
- package/dist/{domain/intent → intent}/middleware/capabilities.d.ts +2 -2
- package/dist/intent/middleware/capabilities.d.ts.map +1 -0
- package/dist/intent/middleware/logging.d.ts.map +1 -0
- package/dist/{domain/intent → intent}/middleware/rate-limit.d.ts +1 -1
- package/dist/intent/middleware/rate-limit.d.ts.map +1 -0
- package/dist/{application/intent/pipeline-runner.d.ts → intent/pipeline.d.ts} +3 -3
- package/dist/intent/pipeline.d.ts.map +1 -0
- package/dist/intent/policy.d.ts +40 -0
- package/dist/intent/policy.d.ts.map +1 -0
- package/dist/intent/policy.js +15 -0
- package/dist/{application/intent → intent}/request.d.ts +5 -14
- package/dist/intent/request.d.ts.map +1 -0
- package/dist/{application/intent → intent}/request.js +6 -20
- package/dist/intent/router.d.ts +31 -0
- package/dist/intent/router.d.ts.map +1 -0
- package/dist/{application/intent → intent}/router.js +1 -15
- package/dist/kernel/client.d.ts +33 -0
- package/dist/kernel/client.d.ts.map +1 -0
- package/dist/kernel/credential.d.ts +35 -0
- package/dist/kernel/credential.d.ts.map +1 -0
- package/dist/kernel/credential.js +43 -0
- package/dist/kernel/delegated.d.ts +17 -0
- package/dist/kernel/delegated.d.ts.map +1 -0
- package/dist/kernel/delegated.js +29 -0
- package/dist/{application/windowing/token-refresh.d.ts → kernel/delegation/refresh.d.ts} +2 -2
- package/dist/kernel/delegation/refresh.d.ts.map +1 -0
- package/dist/{application/windowing/token-refresh.js → kernel/delegation/refresh.js} +1 -1
- package/dist/{domain/identity/delegation.d.ts → kernel/delegation/token.d.ts} +1 -1
- package/dist/kernel/delegation/token.d.ts.map +1 -0
- package/dist/{adapters/kernel-client.adapter.d.ts → kernel/session.d.ts} +15 -12
- package/dist/kernel/session.d.ts.map +1 -0
- package/dist/{adapters/kernel-client.adapter.js → kernel/session.js} +10 -15
- package/dist/message/ctrl.d.ts +34 -0
- package/dist/message/ctrl.d.ts.map +1 -0
- package/dist/message/error.d.ts +9 -0
- package/dist/message/error.d.ts.map +1 -0
- package/dist/{application/windowing/protocol.d.ts → message/init.d.ts} +1 -1
- package/dist/message/init.d.ts.map +1 -0
- package/dist/{domain/message/registries.d.ts → message/intent.d.ts} +45 -37
- package/dist/message/intent.d.ts.map +1 -0
- package/dist/message/message.d.ts +10 -0
- package/dist/message/message.d.ts.map +1 -0
- package/dist/shell/access.d.ts +10 -0
- package/dist/shell/access.d.ts.map +1 -0
- package/dist/shell/access.js +19 -0
- package/dist/shell/build.d.ts +28 -0
- package/dist/shell/build.d.ts.map +1 -0
- package/dist/shell/build.js +58 -0
- package/dist/shell/config.d.ts +29 -0
- package/dist/shell/config.d.ts.map +1 -0
- package/dist/shell/create.d.ts +4 -0
- package/dist/shell/create.d.ts.map +1 -0
- package/dist/shell/create.js +5 -0
- package/dist/{application/windowing/child-init.d.ts → shell/sandboxed/boot.d.ts} +5 -5
- package/dist/shell/sandboxed/boot.d.ts.map +1 -0
- package/dist/{application/windowing/child-init.js → shell/sandboxed/boot.js} +5 -6
- package/dist/shell/sandboxed/guarded.d.ts +13 -0
- package/dist/shell/sandboxed/guarded.d.ts.map +1 -0
- package/dist/shell/sandboxed/guarded.js +28 -0
- package/dist/shell/sandboxed/pending.d.ts +14 -0
- package/dist/shell/sandboxed/pending.d.ts.map +1 -0
- package/dist/shell/sandboxed/pending.js +30 -0
- package/dist/shell/sandboxed/sandboxed.d.ts +4 -0
- package/dist/shell/sandboxed/sandboxed.d.ts.map +1 -0
- package/dist/shell/sandboxed/sandboxed.js +100 -0
- package/dist/shell/shell.d.ts +36 -0
- package/dist/shell/shell.d.ts.map +1 -0
- package/dist/shell/standalone.d.ts +4 -0
- package/dist/shell/standalone.d.ts.map +1 -0
- package/dist/shell/standalone.js +85 -0
- package/dist/{domain/window/config.d.ts → window/capabilities.d.ts} +3 -3
- package/dist/window/capabilities.d.ts.map +1 -0
- package/dist/{ports/shell-adapter.d.ts → window/embed/adapter.d.ts} +3 -2
- package/dist/window/embed/adapter.d.ts.map +1 -0
- package/dist/{adapters/iframe.shell-adapter.d.ts → window/embed/iframe.d.ts} +2 -2
- package/dist/window/embed/iframe.d.ts.map +1 -0
- package/dist/{application/windowing/lifecycle-controller.d.ts → window/lifecycle/states.d.ts} +4 -3
- package/dist/window/lifecycle/states.d.ts.map +1 -0
- package/dist/{application/windowing/lifecycle-controller.js → window/lifecycle/states.js} +1 -1
- package/dist/{domain/window/lifecycle.d.ts → window/lifecycle/transition.d.ts} +3 -3
- package/dist/window/lifecycle/transition.d.ts.map +1 -0
- package/dist/{domain/window/lifecycle.js → window/lifecycle/transition.js} +1 -1
- package/dist/window/mount/close.d.ts +27 -0
- package/dist/window/mount/close.d.ts.map +1 -0
- package/dist/window/mount/close.js +74 -0
- package/dist/window/mount/credential.d.ts +41 -0
- package/dist/window/mount/credential.d.ts.map +1 -0
- package/dist/window/mount/credential.js +74 -0
- package/dist/window/mount/handshake.d.ts.map +1 -0
- package/dist/{application/windowing → window/mount}/handshake.js +1 -1
- package/dist/{application/windowing/mount-controller.d.ts → window/mount/mount.d.ts} +7 -7
- package/dist/window/mount/mount.d.ts.map +1 -0
- package/dist/window/mount/mount.js +143 -0
- package/dist/window/mount/params.d.ts +73 -0
- package/dist/window/mount/params.d.ts.map +1 -0
- package/dist/window/mount/params.js +1 -0
- package/dist/window/mount/plain.d.ts +20 -0
- package/dist/window/mount/plain.d.ts.map +1 -0
- package/dist/window/mount/plain.js +46 -0
- package/dist/{domain/window → window}/window.d.ts +1 -1
- package/dist/window/window.d.ts.map +1 -0
- package/dist/window/window.js +1 -0
- package/package.json +11 -6
- package/src/application/apps.ts +60 -0
- package/src/application/namespaces.ts +38 -0
- package/src/application/session/open.ts +73 -0
- package/src/application/session/session.ts +46 -0
- package/src/application/session/token.ts +87 -0
- package/src/application/spaces.ts +52 -0
- package/src/application/user.ts +54 -0
- package/src/application/view.ts +42 -0
- package/src/{application/channels/children-channel.ts → channel/children.ts} +3 -3
- package/src/{application/channels/types.ts → channel/listeners.ts} +2 -7
- package/src/{adapters/message-port.transport.ts → channel/message-port.ts} +2 -2
- package/src/{application/channels → channel}/observer.ts +2 -1
- package/src/{application/channels/parent-channel.ts → channel/parent.ts} +4 -4
- package/src/{ports → channel}/transport.ts +2 -1
- package/src/index.ts +80 -88
- package/src/{domain/intent → intent}/middleware/capabilities.ts +2 -3
- package/src/{domain/intent → intent}/middleware/logging.ts +1 -1
- package/src/{domain/intent → intent}/middleware/rate-limit.ts +1 -1
- package/src/{application/intent/pipeline-runner.ts → intent/pipeline.ts} +3 -2
- package/src/intent/policy.ts +46 -0
- package/src/{application/intent → intent}/request.ts +14 -34
- package/src/{application/intent → intent}/router.ts +12 -49
- package/src/kernel/client.ts +33 -0
- package/src/kernel/credential.ts +78 -0
- package/src/kernel/delegated.ts +49 -0
- package/src/{application/windowing/token-refresh.ts → kernel/delegation/refresh.ts} +2 -2
- package/src/{adapters/kernel-client.adapter.ts → kernel/session.ts} +26 -33
- package/src/message/ctrl.ts +25 -0
- package/src/{domain/message/errors.ts → message/error.ts} +8 -0
- package/src/message/intent.ts +98 -0
- package/src/message/message.ts +11 -0
- package/src/shell/access.ts +26 -0
- package/src/shell/build.ts +95 -0
- package/src/shell/config.ts +32 -0
- package/src/shell/create.ts +9 -0
- package/src/{application/windowing/child-init.ts → shell/sandboxed/boot.ts} +10 -11
- package/src/shell/sandboxed/guarded.ts +32 -0
- package/src/shell/sandboxed/pending.ts +52 -0
- package/src/shell/sandboxed/sandboxed.ts +131 -0
- package/src/shell/shell.ts +37 -0
- package/src/shell/standalone.ts +97 -0
- package/src/{domain/window/config.ts → window/capabilities.ts} +2 -2
- package/src/{ports/shell-adapter.ts → window/embed/adapter.ts} +2 -1
- package/src/{adapters/iframe.shell-adapter.ts → window/embed/iframe.ts} +2 -2
- package/src/{application/windowing/lifecycle-controller.ts → window/lifecycle/states.ts} +3 -2
- package/src/{domain/window/lifecycle.ts → window/lifecycle/transition.ts} +2 -2
- package/src/window/mount/close.ts +111 -0
- package/src/window/mount/credential.ts +101 -0
- package/src/{application/windowing → window/mount}/handshake.ts +3 -2
- package/src/window/mount/mount.ts +185 -0
- package/src/window/mount/params.ts +64 -0
- package/src/window/mount/plain.ts +68 -0
- package/src/{domain/window → window}/window.ts +1 -1
- package/dist/adapters/iframe.shell-adapter.d.ts.map +0 -1
- package/dist/adapters/kernel-client.adapter.d.ts.map +0 -1
- package/dist/adapters/message-port.transport.d.ts.map +0 -1
- package/dist/application/channels/children-channel.d.ts.map +0 -1
- package/dist/application/channels/observer.d.ts.map +0 -1
- package/dist/application/channels/parent-channel.d.ts.map +0 -1
- package/dist/application/channels/types.d.ts +0 -12
- package/dist/application/channels/types.d.ts.map +0 -1
- package/dist/application/intent/pipeline-runner.d.ts.map +0 -1
- package/dist/application/intent/request.d.ts.map +0 -1
- package/dist/application/intent/router.d.ts +0 -56
- package/dist/application/intent/router.d.ts.map +0 -1
- package/dist/application/kernel-facade.d.ts +0 -33
- package/dist/application/kernel-facade.d.ts.map +0 -1
- package/dist/application/kernel-facade.js +0 -77
- package/dist/application/shell.d.ts +0 -100
- package/dist/application/shell.d.ts.map +0 -1
- package/dist/application/shell.js +0 -256
- package/dist/application/windowing/child-init.d.ts.map +0 -1
- package/dist/application/windowing/handshake.d.ts.map +0 -1
- package/dist/application/windowing/lifecycle-controller.d.ts.map +0 -1
- package/dist/application/windowing/mount-controller.d.ts.map +0 -1
- package/dist/application/windowing/mount-controller.js +0 -277
- package/dist/application/windowing/protocol.d.ts.map +0 -1
- package/dist/application/windowing/token-refresh.d.ts.map +0 -1
- package/dist/domain/identity/delegation.d.ts.map +0 -1
- package/dist/domain/identity/path.d.ts +0 -11
- package/dist/domain/identity/path.d.ts.map +0 -1
- package/dist/domain/identity/path.js +0 -13
- package/dist/domain/identity/resolve-self.d.ts +0 -21
- package/dist/domain/identity/resolve-self.d.ts.map +0 -1
- package/dist/domain/identity/resolve-self.js +0 -36
- package/dist/domain/intent/middleware/capabilities.d.ts.map +0 -1
- package/dist/domain/intent/middleware/logging.d.ts.map +0 -1
- package/dist/domain/intent/middleware/rate-limit.d.ts.map +0 -1
- package/dist/domain/intent/pipeline.d.ts +0 -3
- package/dist/domain/intent/pipeline.d.ts.map +0 -1
- package/dist/domain/intent/router.d.ts +0 -6
- package/dist/domain/intent/router.d.ts.map +0 -1
- package/dist/domain/message/errors.d.ts +0 -2
- package/dist/domain/message/errors.d.ts.map +0 -1
- package/dist/domain/message/registries.d.ts.map +0 -1
- package/dist/domain/message/types.d.ts +0 -30
- package/dist/domain/message/types.d.ts.map +0 -1
- package/dist/domain/window/config.d.ts.map +0 -1
- package/dist/domain/window/lifecycle.d.ts.map +0 -1
- package/dist/domain/window/window.d.ts.map +0 -1
- package/dist/ports/kernel-client.d.ts +0 -33
- package/dist/ports/kernel-client.d.ts.map +0 -1
- package/dist/ports/shell-adapter.d.ts.map +0 -1
- package/dist/ports/transport.d.ts +0 -7
- package/dist/ports/transport.d.ts.map +0 -1
- package/src/application/kernel-facade.ts +0 -143
- package/src/application/shell.ts +0 -433
- package/src/application/windowing/mount-controller.ts +0 -335
- package/src/domain/identity/path.ts +0 -14
- package/src/domain/identity/resolve-self.ts +0 -38
- package/src/domain/intent/pipeline.ts +0 -3
- package/src/domain/intent/router.ts +0 -6
- package/src/domain/message/registries.ts +0 -76
- package/src/domain/message/types.ts +0 -32
- package/src/ports/kernel-client.ts +0 -32
- /package/dist/{application/channels/children-channel.js → channel/children.js} +0 -0
- /package/dist/{application/channels/types.js → channel/listeners.js} +0 -0
- /package/dist/{adapters/message-port.transport.js → channel/message-port.js} +0 -0
- /package/dist/{application/channels → channel}/observer.js +0 -0
- /package/dist/{ports → channel}/transport.js +0 -0
- /package/dist/{domain/intent → intent}/middleware/capabilities.js +0 -0
- /package/dist/{domain/intent → intent}/middleware/logging.d.ts +0 -0
- /package/dist/{domain/intent → intent}/middleware/logging.js +0 -0
- /package/dist/{domain/intent → intent}/middleware/rate-limit.js +0 -0
- /package/dist/{application/intent/pipeline-runner.js → intent/pipeline.js} +0 -0
- /package/dist/{domain/intent/pipeline.js → kernel/client.js} +0 -0
- /package/dist/{domain/identity/delegation.js → kernel/delegation/token.js} +0 -0
- /package/dist/{domain/intent/router.js → message/ctrl.js} +0 -0
- /package/dist/{domain/message/errors.js → message/error.js} +0 -0
- /package/dist/{application/windowing/protocol.js → message/init.js} +0 -0
- /package/dist/{domain/message/registries.js → message/intent.js} +0 -0
- /package/dist/{domain/message/types.js → message/message.js} +0 -0
- /package/dist/{domain/window → shell}/config.js +0 -0
- /package/dist/{domain/window/window.js → shell/shell.js} +0 -0
- /package/dist/{ports/kernel-client.js → window/capabilities.js} +0 -0
- /package/dist/{ports/shell-adapter.js → window/embed/adapter.js} +0 -0
- /package/dist/{adapters/iframe.shell-adapter.js → window/embed/iframe.js} +0 -0
- /package/dist/{application/windowing → window/mount}/handshake.d.ts +0 -0
- /package/src/{domain/identity/delegation.ts → kernel/delegation/token.ts} +0 -0
- /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/
|
|
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
|
|
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 '../
|
|
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 '../
|
|
2
|
-
import type { ShellAdapter, WindowHandle } from '
|
|
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 '
|
|
1
|
+
import type { WindowState } from '../window'
|
|
2
2
|
|
|
3
|
-
import { transition, type LifecycleEvent } from '
|
|
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 '
|
|
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
|
|
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
|
|
1
|
+
import type { CtrlMessage } from '../../message/ctrl'
|
|
2
|
+
import type { ShellMessage } from '../../message/message'
|
|
2
3
|
|
|
3
|
-
import { INIT_RESPONSE_TYPE, isInitRequest } from '
|
|
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
|
+
}
|