@astrale-os/shell 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/dist/adapters/iframe.shell-adapter.d.ts +10 -0
- package/dist/adapters/iframe.shell-adapter.d.ts.map +1 -0
- package/dist/adapters/iframe.shell-adapter.js +55 -0
- package/dist/adapters/kernel-client.adapter.d.ts +53 -0
- package/dist/adapters/kernel-client.adapter.d.ts.map +1 -0
- package/dist/adapters/kernel-client.adapter.js +69 -0
- package/dist/adapters/message-port.transport.d.ts +7 -0
- package/dist/adapters/message-port.transport.d.ts.map +1 -0
- package/dist/adapters/message-port.transport.js +42 -0
- package/dist/application/channels/children-channel.d.ts +21 -0
- package/dist/application/channels/children-channel.d.ts.map +1 -0
- package/dist/application/channels/children-channel.js +82 -0
- package/dist/application/channels/observer.d.ts +9 -0
- package/dist/application/channels/observer.d.ts.map +1 -0
- package/dist/application/channels/observer.js +23 -0
- package/dist/application/channels/parent-channel.d.ts +17 -0
- package/dist/application/channels/parent-channel.d.ts.map +1 -0
- package/dist/application/channels/parent-channel.js +21 -0
- package/dist/application/channels/types.d.ts +12 -0
- package/dist/application/channels/types.d.ts.map +1 -0
- package/dist/application/channels/types.js +29 -0
- package/dist/application/intent/pipeline-runner.d.ts +12 -0
- package/dist/application/intent/pipeline-runner.d.ts.map +1 -0
- package/dist/application/intent/pipeline-runner.js +27 -0
- package/dist/application/intent/request.d.ts +34 -0
- package/dist/application/intent/request.d.ts.map +1 -0
- package/dist/application/intent/request.js +70 -0
- package/dist/application/intent/router.d.ts +56 -0
- package/dist/application/intent/router.d.ts.map +1 -0
- package/dist/application/intent/router.js +94 -0
- package/dist/application/kernel-facade.d.ts +33 -0
- package/dist/application/kernel-facade.d.ts.map +1 -0
- package/dist/application/kernel-facade.js +77 -0
- package/dist/application/shell.d.ts +100 -0
- package/dist/application/shell.d.ts.map +1 -0
- package/dist/application/shell.js +256 -0
- package/dist/application/windowing/child-init.d.ts +23 -0
- package/dist/application/windowing/child-init.d.ts.map +1 -0
- package/dist/application/windowing/child-init.js +101 -0
- package/dist/application/windowing/handshake.d.ts +30 -0
- package/dist/application/windowing/handshake.d.ts.map +1 -0
- package/dist/application/windowing/handshake.js +63 -0
- package/dist/application/windowing/lifecycle-controller.d.ts +10 -0
- package/dist/application/windowing/lifecycle-controller.d.ts.map +1 -0
- package/dist/application/windowing/lifecycle-controller.js +30 -0
- package/dist/application/windowing/mount-controller.d.ts +25 -0
- package/dist/application/windowing/mount-controller.d.ts.map +1 -0
- package/dist/application/windowing/mount-controller.js +277 -0
- package/dist/application/windowing/protocol.d.ts +21 -0
- package/dist/application/windowing/protocol.d.ts.map +1 -0
- package/dist/application/windowing/protocol.js +19 -0
- package/dist/application/windowing/token-refresh.d.ts +19 -0
- package/dist/application/windowing/token-refresh.d.ts.map +1 -0
- package/dist/application/windowing/token-refresh.js +60 -0
- package/dist/domain/identity/delegation.d.ts +15 -0
- package/dist/domain/identity/delegation.d.ts.map +1 -0
- package/dist/domain/identity/delegation.js +12 -0
- package/dist/domain/identity/path.d.ts +11 -0
- package/dist/domain/identity/path.d.ts.map +1 -0
- package/dist/domain/identity/path.js +13 -0
- package/dist/domain/identity/resolve-self.d.ts +21 -0
- package/dist/domain/identity/resolve-self.d.ts.map +1 -0
- package/dist/domain/identity/resolve-self.js +36 -0
- package/dist/domain/intent/middleware/capabilities.d.ts +28 -0
- package/dist/domain/intent/middleware/capabilities.d.ts.map +1 -0
- package/dist/domain/intent/middleware/capabilities.js +18 -0
- package/dist/domain/intent/middleware/logging.d.ts +14 -0
- package/dist/domain/intent/middleware/logging.d.ts.map +1 -0
- package/dist/domain/intent/middleware/logging.js +21 -0
- package/dist/domain/intent/middleware/rate-limit.d.ts +20 -0
- package/dist/domain/intent/middleware/rate-limit.d.ts.map +1 -0
- package/dist/domain/intent/middleware/rate-limit.js +33 -0
- package/dist/domain/intent/pipeline.d.ts +3 -0
- package/dist/domain/intent/pipeline.d.ts.map +1 -0
- package/dist/domain/intent/pipeline.js +1 -0
- package/dist/domain/intent/router.d.ts +6 -0
- package/dist/domain/intent/router.d.ts.map +1 -0
- package/dist/domain/intent/router.js +1 -0
- package/dist/domain/message/errors.d.ts +2 -0
- package/dist/domain/message/errors.d.ts.map +1 -0
- package/dist/domain/message/errors.js +1 -0
- package/dist/domain/message/registries.d.ts +145 -0
- package/dist/domain/message/registries.d.ts.map +1 -0
- package/dist/domain/message/registries.js +1 -0
- package/dist/domain/message/types.d.ts +30 -0
- package/dist/domain/message/types.d.ts.map +1 -0
- package/dist/domain/message/types.js +1 -0
- package/dist/domain/window/config.d.ts +26 -0
- package/dist/domain/window/config.d.ts.map +1 -0
- package/dist/domain/window/config.js +1 -0
- package/dist/domain/window/lifecycle.d.ts +27 -0
- package/dist/domain/window/lifecycle.d.ts.map +1 -0
- package/dist/domain/window/lifecycle.js +36 -0
- package/dist/domain/window/window.d.ts +12 -0
- package/dist/domain/window/window.d.ts.map +1 -0
- package/dist/domain/window/window.js +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/ports/kernel-client.d.ts +33 -0
- package/dist/ports/kernel-client.d.ts.map +1 -0
- package/dist/ports/kernel-client.js +1 -0
- package/dist/ports/shell-adapter.d.ts +13 -0
- package/dist/ports/shell-adapter.d.ts.map +1 -0
- package/dist/ports/shell-adapter.js +1 -0
- package/dist/ports/transport.d.ts +7 -0
- package/dist/ports/transport.d.ts.map +1 -0
- package/dist/ports/transport.js +1 -0
- package/package.json +41 -0
- package/src/adapters/iframe.shell-adapter.ts +68 -0
- package/src/adapters/kernel-client.adapter.ts +138 -0
- package/src/adapters/message-port.transport.ts +50 -0
- package/src/application/channels/children-channel.ts +112 -0
- package/src/application/channels/observer.ts +33 -0
- package/src/application/channels/parent-channel.ts +43 -0
- package/src/application/channels/types.ts +41 -0
- package/src/application/intent/pipeline-runner.ts +36 -0
- package/src/application/intent/request.ts +87 -0
- package/src/application/intent/router.ts +161 -0
- package/src/application/kernel-facade.ts +143 -0
- package/src/application/shell.ts +433 -0
- package/src/application/windowing/child-init.ts +137 -0
- package/src/application/windowing/handshake.ts +98 -0
- package/src/application/windowing/lifecycle-controller.ts +40 -0
- package/src/application/windowing/mount-controller.ts +335 -0
- package/src/application/windowing/protocol.ts +37 -0
- package/src/application/windowing/token-refresh.ts +78 -0
- package/src/domain/identity/delegation.ts +25 -0
- package/src/domain/identity/path.ts +14 -0
- package/src/domain/identity/resolve-self.ts +38 -0
- package/src/domain/intent/middleware/capabilities.ts +53 -0
- package/src/domain/intent/middleware/logging.ts +45 -0
- package/src/domain/intent/middleware/rate-limit.ts +54 -0
- package/src/domain/intent/pipeline.ts +3 -0
- package/src/domain/intent/router.ts +6 -0
- package/src/domain/message/errors.ts +9 -0
- package/src/domain/message/registries.ts +76 -0
- package/src/domain/message/types.ts +32 -0
- package/src/domain/window/config.ts +28 -0
- package/src/domain/window/lifecycle.ts +39 -0
- package/src/domain/window/window.ts +13 -0
- package/src/index.ts +103 -0
- package/src/ports/kernel-client.ts +32 -0
- package/src/ports/shell-adapter.ts +11 -0
- package/src/ports/transport.ts +7 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure state-machine transition. Used by the lifecycle controller —
|
|
3
|
+
* no timers, no side effects here.
|
|
4
|
+
*/
|
|
5
|
+
export function transition(state, event) {
|
|
6
|
+
switch (state) {
|
|
7
|
+
case 'active':
|
|
8
|
+
if (event.kind === 'hide')
|
|
9
|
+
return { ok: true, next: 'hidden' };
|
|
10
|
+
if (event.kind === 'requestClose')
|
|
11
|
+
return { ok: true, next: 'closing' };
|
|
12
|
+
if (event.kind === 'forceClose')
|
|
13
|
+
return { ok: true, next: 'closed' };
|
|
14
|
+
break;
|
|
15
|
+
case 'hidden':
|
|
16
|
+
if (event.kind === 'restore')
|
|
17
|
+
return { ok: true, next: 'active' };
|
|
18
|
+
if (event.kind === 'requestClose')
|
|
19
|
+
return { ok: true, next: 'closing' };
|
|
20
|
+
if (event.kind === 'forceClose')
|
|
21
|
+
return { ok: true, next: 'closed' };
|
|
22
|
+
break;
|
|
23
|
+
case 'closing':
|
|
24
|
+
if (event.kind === 'closeAck')
|
|
25
|
+
return { ok: true, next: 'closed' };
|
|
26
|
+
if (event.kind === 'closeRefuse')
|
|
27
|
+
return { ok: true, next: 'active' };
|
|
28
|
+
if (event.kind === 'forceClose')
|
|
29
|
+
return { ok: true, next: 'closed' };
|
|
30
|
+
break;
|
|
31
|
+
case 'closed':
|
|
32
|
+
// Terminal — no transitions out.
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
return { ok: false, reason: `invalid transition ${state}.${event.kind}` };
|
|
36
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { WindowCapabilities } from './config';
|
|
2
|
+
export type WindowState = 'active' | 'hidden' | 'closing' | 'closed';
|
|
3
|
+
export interface Window {
|
|
4
|
+
readonly windowId: string;
|
|
5
|
+
readonly functionId: string;
|
|
6
|
+
readonly targetNodeId?: string;
|
|
7
|
+
readonly isolation: 'sandboxed' | 'shared';
|
|
8
|
+
readonly state: WindowState;
|
|
9
|
+
readonly delegationToken: string | null;
|
|
10
|
+
readonly capabilities: WindowCapabilities;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=window.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"window.d.ts","sourceRoot":"","sources":["../../../src/domain/window/window.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAA;AAElD,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAA;AAEpE,MAAM,WAAW,MAAM;IACrB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,SAAS,EAAE,WAAW,GAAG,QAAQ,CAAA;IAC1C,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAA;IAC3B,QAAQ,CAAC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IACvC,QAAQ,CAAC,YAAY,EAAE,kBAAkB,CAAA;CAC1C"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type { ErrorCode } from './domain/message/errors';
|
|
2
|
+
export type { IntentRegistry, CtrlRegistry, ViewRef } from './domain/message/registries';
|
|
3
|
+
export type { ShellMessage, IntentMessage, IntentEnvelope, CtrlMessage, ErrorMessage, } from './domain/message/types';
|
|
4
|
+
export type { WindowConfig, WindowCapabilities, SandboxProfile } from './domain/window/config';
|
|
5
|
+
export type { Window, WindowState } from './domain/window/window';
|
|
6
|
+
export { transition as lifecycleTransition } from './domain/window/lifecycle';
|
|
7
|
+
export type { LifecycleEvent, TransitionResult } from './domain/window/lifecycle';
|
|
8
|
+
export type { IntentMiddleware } from './domain/intent/pipeline';
|
|
9
|
+
export type { IntentRouter } from './domain/intent/router';
|
|
10
|
+
export { loggingMiddleware } from './domain/intent/middleware/logging';
|
|
11
|
+
export type { LoggingConfig, LoggingEntry, LoggingSink } from './domain/intent/middleware/logging';
|
|
12
|
+
export { capabilitiesMiddleware } from './domain/intent/middleware/capabilities';
|
|
13
|
+
export type { CapabilitiesConfig, CapabilityLookup, CapabilityViolation, } from './domain/intent/middleware/capabilities';
|
|
14
|
+
export { rateLimitMiddleware } from './domain/intent/middleware/rate-limit';
|
|
15
|
+
export type { RateLimitConfig, RateLimitViolation } from './domain/intent/middleware/rate-limit';
|
|
16
|
+
export type { DelegationScope, DelegationToken, TokenState } from './domain/identity/delegation';
|
|
17
|
+
export { stateOf, isExpired } from './domain/identity/delegation';
|
|
18
|
+
export type { Transport } from './ports/transport';
|
|
19
|
+
export type { ShellAdapter, WindowHandle } from './ports/shell-adapter';
|
|
20
|
+
export type { KernelClient } from './ports/kernel-client';
|
|
21
|
+
export { createKernelClientAdapter } from './adapters/kernel-client.adapter';
|
|
22
|
+
export type { KernelClientAdapter, CreateKernelClientAdapterOpts, } from './adapters/kernel-client.adapter';
|
|
23
|
+
export { createMessagePortTransport } from './adapters/message-port.transport';
|
|
24
|
+
export type { MessagePortTransportConfig } from './adapters/message-port.transport';
|
|
25
|
+
export { createIframeShellAdapter } from './adapters/iframe.shell-adapter';
|
|
26
|
+
export type { IframeShellAdapterConfig } from './adapters/iframe.shell-adapter';
|
|
27
|
+
export type { Shell, ShellConfig, StandaloneConfig, SandboxedConfig, MountParams, MountedWindow, CloseResult, ParentChannel, ChildrenChannel, MessageDirection, MessageHandler, ObserverHandler, } from './application/shell';
|
|
28
|
+
export { createShell } from './application/shell';
|
|
29
|
+
export { runParentHandshake } from './application/windowing/handshake';
|
|
30
|
+
export type { HandshakeParams, HandshakeError, HandshakeResult, } from './application/windowing/handshake';
|
|
31
|
+
export { performChildInit } from './application/windowing/child-init';
|
|
32
|
+
export type { ChildInitParams, ChildInitResult } from './application/windowing/child-init';
|
|
33
|
+
export { createTokenRefresher } from './application/windowing/token-refresh';
|
|
34
|
+
export type { TokenRefreshConfig, TokenRefresher } from './application/windowing/token-refresh';
|
|
35
|
+
export { createIntentPipeline } from './application/intent/pipeline-runner';
|
|
36
|
+
export type { IntentPipeline } from './application/intent/pipeline-runner';
|
|
37
|
+
export { createIntentRouter } from './application/intent/router';
|
|
38
|
+
export type { IntentRouterController, IntentRouterConfig, IntentDecision, IntentOrigin, IntentPolicy, IntentContext, LocalIntentHandler, } from './application/intent/router';
|
|
39
|
+
export { requestIntent, replyToIntent, requestAccess } from './application/intent/request';
|
|
40
|
+
export { INIT_REQUEST_TYPE, INIT_RESPONSE_TYPE, isInitRequest, isInitResponse, } from './application/windowing/protocol';
|
|
41
|
+
export type { InitRequestMessage, InitResponseMessage } from './application/windowing/protocol';
|
|
42
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { transition as lifecycleTransition } from './domain/window/lifecycle';
|
|
2
|
+
export { loggingMiddleware } from './domain/intent/middleware/logging';
|
|
3
|
+
export { capabilitiesMiddleware } from './domain/intent/middleware/capabilities';
|
|
4
|
+
export { rateLimitMiddleware } from './domain/intent/middleware/rate-limit';
|
|
5
|
+
export { stateOf, isExpired } from './domain/identity/delegation';
|
|
6
|
+
// ─── adapters ───────────────────────────────────────────────────────────────
|
|
7
|
+
export { createKernelClientAdapter } from './adapters/kernel-client.adapter';
|
|
8
|
+
export { createMessagePortTransport } from './adapters/message-port.transport';
|
|
9
|
+
export { createIframeShellAdapter } from './adapters/iframe.shell-adapter';
|
|
10
|
+
export { createShell } from './application/shell';
|
|
11
|
+
// ─── application/windowing (advanced / testing) ─────────────────────────────
|
|
12
|
+
export { runParentHandshake } from './application/windowing/handshake';
|
|
13
|
+
export { performChildInit } from './application/windowing/child-init';
|
|
14
|
+
export { createTokenRefresher } from './application/windowing/token-refresh';
|
|
15
|
+
// ─── application/intent (advanced / testing) ────────────────────────────────
|
|
16
|
+
export { createIntentPipeline } from './application/intent/pipeline-runner';
|
|
17
|
+
export { createIntentRouter } from './application/intent/router';
|
|
18
|
+
export { requestIntent, replyToIntent, requestAccess } from './application/intent/request';
|
|
19
|
+
// ─── Init-request protocol (shared with host pages) ─────────────────────────
|
|
20
|
+
export { INIT_REQUEST_TYPE, INIT_RESPONSE_TYPE, isInitRequest, isInitResponse, } from './application/windowing/protocol';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface KernelClient {
|
|
2
|
+
call(path: string, params: unknown): Promise<unknown>;
|
|
3
|
+
/**
|
|
4
|
+
* Produce a credential usable by a child shell.
|
|
5
|
+
*
|
|
6
|
+
* v1: returns a fresh identity credential scoped to the current principal.
|
|
7
|
+
* Future (SDK v2): will accept `{ functionId }` to scope the delegation
|
|
8
|
+
* to a specific Function — tracked in `.dev/todo/shell-sdk-v2-delegation.md`.
|
|
9
|
+
*/
|
|
10
|
+
mintDelegation(): Promise<string>;
|
|
11
|
+
/**
|
|
12
|
+
* Return the JWT this shell currently uses to authenticate `call`. Iframes
|
|
13
|
+
* use it to make their own authenticated HTTP requests to the kernel —
|
|
14
|
+
* e.g. when a third-party SDK like Vercel AI's `useChat` issues a raw
|
|
15
|
+
* `fetch()` that can't run through `call()`. Callers form the Authorization
|
|
16
|
+
* header themselves: `Authorization: \`Bearer ${shell.kernel.authToken()}\``.
|
|
17
|
+
*
|
|
18
|
+
* The value is the *current* token; the parent shell auto-refreshes it
|
|
19
|
+
* before expiry, so always read fresh on each request rather than caching.
|
|
20
|
+
*/
|
|
21
|
+
authToken(): string;
|
|
22
|
+
/**
|
|
23
|
+
* URL of the kernel this shell talks to. Pair with `authToken()` when an
|
|
24
|
+
* iframe needs to make a raw `fetch()` from a third-party SDK (e.g.
|
|
25
|
+
* Vercel AI's `useChat`): the kernel-issued JWT's `aud` matches the
|
|
26
|
+
* kernel's own issuer, so requests must target the kernel directly. If
|
|
27
|
+
* a sandboxed iframe POSTs to its own `window.location.origin` (the
|
|
28
|
+
* worker hosting the iframe), the worker's audience check rejects the
|
|
29
|
+
* cred with "Credential audience mismatch".
|
|
30
|
+
*/
|
|
31
|
+
readonly url: string;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=kernel-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kernel-client.d.ts","sourceRoot":"","sources":["../../src/ports/kernel-client.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IACrD;;;;;;OAMG;IACH,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC,CAAA;IACjC;;;;;;;;;OASG;IACH,SAAS,IAAI,MAAM,CAAA;IACnB;;;;;;;;OAQG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CACrB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { WindowConfig } from '../domain/window/config';
|
|
2
|
+
export interface WindowHandle {
|
|
3
|
+
readonly element: HTMLIFrameElement | HTMLElement;
|
|
4
|
+
readonly port: MessagePort | null;
|
|
5
|
+
readonly origin: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ShellAdapter {
|
|
8
|
+
mount(config: WindowConfig): {
|
|
9
|
+
handle: WindowHandle;
|
|
10
|
+
dispose: () => void;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=shell-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shell-adapter.d.ts","sourceRoot":"","sources":["../../src/ports/shell-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AAE3D,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,OAAO,EAAE,iBAAiB,GAAG,WAAW,CAAA;IACjD,QAAQ,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAAA;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,MAAM,EAAE,YAAY,GAAG;QAAE,MAAM,EAAE,YAAY,CAAC;QAAC,OAAO,EAAE,MAAM,IAAI,CAAA;KAAE,CAAA;CAC3E"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/ports/transport.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AAE3D,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAAA;IACjC,SAAS,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,IAAI,GAAG,IAAI,CAAA;IACzD,KAAK,IAAI,IAAI,CAAA;CACd"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@astrale-os/shell",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Astrale Shell SDK — parent/child/kernel communication",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/astrale-os/shell.git",
|
|
8
|
+
"directory": "packages/shell"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"src"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "restricted",
|
|
25
|
+
"registry": "https://npm.pkg.github.com"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@astrale-os/kernel-client": ">=0.1.0 <1.0.0",
|
|
29
|
+
"@astrale-os/kernel-core": ">=0.4.1 <1.0.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@typescript/native-preview": "latest",
|
|
33
|
+
"typescript": "~6.0.1-rc"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsgo",
|
|
37
|
+
"clean": "rm -rf dist *.tsbuildinfo",
|
|
38
|
+
"typecheck": "tsgo --noEmit",
|
|
39
|
+
"preinstall": "node .check-workspace.cjs"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { SandboxProfile } from '../domain/window/config'
|
|
2
|
+
import type { ShellAdapter, WindowHandle } from '../ports/shell-adapter'
|
|
3
|
+
|
|
4
|
+
export type IframeShellAdapterConfig = {
|
|
5
|
+
/**
|
|
6
|
+
* Optional class list applied to every iframe created by this adapter.
|
|
7
|
+
* Useful for consistent styling without leaking styling into the SDK.
|
|
8
|
+
*/
|
|
9
|
+
readonly className?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const DEFAULT_SANDBOX: SandboxProfile = {
|
|
13
|
+
allowScripts: true,
|
|
14
|
+
allowForms: false,
|
|
15
|
+
allowPopups: false,
|
|
16
|
+
allowSameOrigin: false,
|
|
17
|
+
allowModals: false,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function createIframeShellAdapter(config: IframeShellAdapterConfig = {}): ShellAdapter {
|
|
21
|
+
return {
|
|
22
|
+
mount(windowConfig) {
|
|
23
|
+
if (windowConfig.isolation !== 'sandboxed') {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`iframe adapter only supports sandboxed windows, got '${windowConfig.isolation}'`,
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const element = document.createElement('iframe')
|
|
30
|
+
element.dataset.astraleFunctionId = windowConfig.functionId
|
|
31
|
+
if (config.className) element.className = config.className
|
|
32
|
+
|
|
33
|
+
// `sandbox: null` is an explicit opt-out — leave the iframe without a
|
|
34
|
+
// `sandbox` attribute (only cross-origin isolation applies). Used by
|
|
35
|
+
// the GUI for first-party trusted views (see trusted-view-origins).
|
|
36
|
+
if (windowConfig.sandbox !== null) {
|
|
37
|
+
const profile = windowConfig.sandbox ?? DEFAULT_SANDBOX
|
|
38
|
+
element.setAttribute('sandbox', sandboxAttrFor(profile))
|
|
39
|
+
}
|
|
40
|
+
element.setAttribute('allow', 'clipboard-read; clipboard-write')
|
|
41
|
+
element.setAttribute('loading', 'eager')
|
|
42
|
+
element.setAttribute('referrerpolicy', 'no-referrer')
|
|
43
|
+
|
|
44
|
+
const handle: WindowHandle = {
|
|
45
|
+
element,
|
|
46
|
+
port: null,
|
|
47
|
+
origin: '',
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
handle,
|
|
52
|
+
dispose: () => {
|
|
53
|
+
element.remove()
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function sandboxAttrFor(profile: SandboxProfile): string {
|
|
61
|
+
const tokens: string[] = []
|
|
62
|
+
if (profile.allowScripts) tokens.push('allow-scripts')
|
|
63
|
+
if (profile.allowForms) tokens.push('allow-forms')
|
|
64
|
+
if (profile.allowPopups) tokens.push('allow-popups')
|
|
65
|
+
if (profile.allowSameOrigin) tokens.push('allow-same-origin')
|
|
66
|
+
if (profile.allowModals) tokens.push('allow-modals')
|
|
67
|
+
return tokens.join(' ')
|
|
68
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CallOptions,
|
|
3
|
+
type CredentialInput,
|
|
4
|
+
type FnMap,
|
|
5
|
+
type FunctionSchema,
|
|
6
|
+
} from '@astrale-os/kernel-client'
|
|
7
|
+
import { ClientSession, type ClientSessionConfig } from '@astrale-os/kernel-client/session'
|
|
8
|
+
|
|
9
|
+
import { identityMethod } from '../domain/identity/path'
|
|
10
|
+
import { createSelfResolver, WHOAMI_PATH } from '../domain/identity/resolve-self'
|
|
11
|
+
|
|
12
|
+
// Untyped FnMap — the shell uses string paths, not typed method keys.
|
|
13
|
+
type AnyFnMap = FnMap
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Low-level kernel adapter — wraps a `ClientSession` (the post-v2 orchestration
|
|
17
|
+
* layer above `KernelClient`).
|
|
18
|
+
*
|
|
19
|
+
* Methods take an explicit credential. The application-layer facade
|
|
20
|
+
* manages which credential to use.
|
|
21
|
+
*
|
|
22
|
+
* Remote-domain redirects are followed transparently: when the kernel
|
|
23
|
+
* responds with a `redirect` result (pointing at an external worker URL),
|
|
24
|
+
* the ClientSession mints a worker-scoped delegation credential — for the
|
|
25
|
+
* audience the kernel carries on the redirect (`CallRedirection.iss`, the
|
|
26
|
+
* target's own identity) — and re-dials the target URL with it. So
|
|
27
|
+
* `shell.kernel.call(...)` "just works" for methods whose Function has a
|
|
28
|
+
* `binding.remoteUrl`.
|
|
29
|
+
*/
|
|
30
|
+
export type KernelClientAdapter = {
|
|
31
|
+
call(
|
|
32
|
+
method: string,
|
|
33
|
+
params: unknown,
|
|
34
|
+
credential: CredentialInput,
|
|
35
|
+
opts?: CallOptions,
|
|
36
|
+
): Promise<unknown>
|
|
37
|
+
load(schemas: FunctionSchema[]): void
|
|
38
|
+
disconnect(): void
|
|
39
|
+
readonly url: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type CreateKernelClientAdapterOpts = {
|
|
43
|
+
url: string
|
|
44
|
+
schema?: FunctionSchema[]
|
|
45
|
+
requestTimeout?: number
|
|
46
|
+
/**
|
|
47
|
+
* Returns the current base credential. When provided, the adapter wires a
|
|
48
|
+
* `delegation: { mint }` on the ClientSession so cross-origin redirects
|
|
49
|
+
* (kernel → worker) are followed with a fresh worker-scoped delegation
|
|
50
|
+
* minted from the active identity. Without it,
|
|
51
|
+
* ClientSession falls back to the base credential and workers reject
|
|
52
|
+
* with `Credential audience mismatch`.
|
|
53
|
+
*/
|
|
54
|
+
getCredential?: () => CredentialInput
|
|
55
|
+
/**
|
|
56
|
+
* Explicit identity node/path to use as the delegation mint subject. When
|
|
57
|
+
* omitted, the authenticated caller is resolved via `whoami` (so a cookie-fronted
|
|
58
|
+
* SPA mints as the logged-in user). Set this only to pin a subject; if set it
|
|
59
|
+
* MUST match the principal `getCredential()` authenticates as, or
|
|
60
|
+
* `mintDelegationCredential`'s self-invariant rejects the mint.
|
|
61
|
+
*/
|
|
62
|
+
mintIdentity?: string
|
|
63
|
+
/**
|
|
64
|
+
* Wire-layer `fetch` shim, forwarded to the underlying `KernelClient`s.
|
|
65
|
+
* Lets a same-origin SPA inject `credentials:'include'` + refresh-on-401 so
|
|
66
|
+
* a router can unseal a cookie into a Bearer (the parent holds no token).
|
|
67
|
+
* Default: global fetch — sandboxed child views never set it.
|
|
68
|
+
*/
|
|
69
|
+
fetch?: typeof fetch
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function createKernelClientAdapter(
|
|
73
|
+
opts: CreateKernelClientAdapterOpts,
|
|
74
|
+
): KernelClientAdapter {
|
|
75
|
+
let session: ClientSession<AnyFnMap> | null = null
|
|
76
|
+
const config: ClientSessionConfig<AnyFnMap> = {
|
|
77
|
+
default: opts.url,
|
|
78
|
+
// Identity is supplied per-call via `opts.credential`; no picker bound.
|
|
79
|
+
// No `delegationPolicy` → ClientSession's default `allowAllDelegations`
|
|
80
|
+
// mints for the audience the kernel carries on the redirect
|
|
81
|
+
// (`CallRedirection.iss`), which is exactly what the target worker verifies.
|
|
82
|
+
identity: undefined,
|
|
83
|
+
}
|
|
84
|
+
if (opts.schema) config.schema = opts.schema
|
|
85
|
+
if (opts.fetch) config.fetch = opts.fetch
|
|
86
|
+
if (opts.getCredential) {
|
|
87
|
+
const getCred = opts.getCredential
|
|
88
|
+
const explicitMintIdentity = opts.mintIdentity
|
|
89
|
+
const resolveSelf = createSelfResolver()
|
|
90
|
+
config.delegation = {
|
|
91
|
+
mint: async (audience, delegation) => {
|
|
92
|
+
if (!session) throw new Error('ClientSession not initialised')
|
|
93
|
+
const s = session
|
|
94
|
+
const subject =
|
|
95
|
+
explicitMintIdentity ??
|
|
96
|
+
(await resolveSelf(() =>
|
|
97
|
+
s.call(WHOAMI_PATH, {}, { credential: getCred(), skipDelegation: true }),
|
|
98
|
+
))
|
|
99
|
+
const result = await s.call(
|
|
100
|
+
identityMethod(subject, 'mintDelegationCredential'),
|
|
101
|
+
{ audience, delegation, ttl: 3600 },
|
|
102
|
+
{ credential: getCred(), skipDelegation: true },
|
|
103
|
+
)
|
|
104
|
+
if (typeof result !== 'string') {
|
|
105
|
+
throw new Error(`mintDelegationCredential returned non-string: ${typeof result}`)
|
|
106
|
+
}
|
|
107
|
+
return { credential: result, ttl: 3600 }
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
session = new ClientSession<AnyFnMap>(config)
|
|
112
|
+
const ready = session
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
url: opts.url,
|
|
116
|
+
|
|
117
|
+
call(method, params, credential, callOpts) {
|
|
118
|
+
const sessionOpts: Parameters<typeof ready.call>[2] = { credential }
|
|
119
|
+
if (callOpts?.timeout !== undefined) sessionOpts.timeout = callOpts.timeout
|
|
120
|
+
if (callOpts?.signal !== undefined) sessionOpts.signal = callOpts.signal
|
|
121
|
+
if (callOpts?.class !== undefined) sessionOpts.class = callOpts.class
|
|
122
|
+
if (callOpts?.transport !== undefined) sessionOpts.transport = callOpts.transport
|
|
123
|
+
if (callOpts?.protocol !== undefined) sessionOpts.protocol = callOpts.protocol
|
|
124
|
+
if (callOpts?.correlationId !== undefined) sessionOpts.correlationId = callOpts.correlationId
|
|
125
|
+
if (callOpts?.self !== undefined) sessionOpts.self = callOpts.self
|
|
126
|
+
if (callOpts?.url !== undefined) sessionOpts.url = callOpts.url
|
|
127
|
+
return ready.call(method, params, sessionOpts)
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
load(schemas) {
|
|
131
|
+
ready.registry.load(schemas)
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
disconnect() {
|
|
135
|
+
ready.disconnect()
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ShellMessage } from '../domain/message/types'
|
|
2
|
+
import type { Transport } from '../ports/transport'
|
|
3
|
+
|
|
4
|
+
export type MessagePortTransportConfig = {
|
|
5
|
+
port: MessagePort
|
|
6
|
+
onClose?: () => void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function createMessagePortTransport(config: MessagePortTransportConfig): Transport {
|
|
10
|
+
const { port, onClose } = config
|
|
11
|
+
|
|
12
|
+
let closed = false
|
|
13
|
+
const handlers = new Set<(message: ShellMessage) => void>()
|
|
14
|
+
|
|
15
|
+
port.onmessage = (event: MessageEvent) => {
|
|
16
|
+
if (closed) return
|
|
17
|
+
const message = event.data as ShellMessage
|
|
18
|
+
for (const handler of handlers) handler(message)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
port.onmessageerror = () => {
|
|
22
|
+
if (closed) return
|
|
23
|
+
close()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
port.start()
|
|
27
|
+
|
|
28
|
+
function close() {
|
|
29
|
+
if (closed) return
|
|
30
|
+
closed = true
|
|
31
|
+
handlers.clear()
|
|
32
|
+
try {
|
|
33
|
+
port.close()
|
|
34
|
+
} catch {
|
|
35
|
+
// port may already be closed
|
|
36
|
+
}
|
|
37
|
+
onClose?.()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
send(message: ShellMessage) {
|
|
42
|
+
if (closed) return
|
|
43
|
+
port.postMessage(message)
|
|
44
|
+
},
|
|
45
|
+
onMessage(handler) {
|
|
46
|
+
handlers.add(handler)
|
|
47
|
+
},
|
|
48
|
+
close,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { ShellMessage } from '../../domain/message/types'
|
|
2
|
+
import type { Transport } from '../../ports/transport'
|
|
3
|
+
import type { MessageObserver } from './observer'
|
|
4
|
+
import type { MessageOf, MessageType } from './types'
|
|
5
|
+
|
|
6
|
+
export interface ChildrenChannel {
|
|
7
|
+
send(windowId: string, message: ShellMessage): void
|
|
8
|
+
on<T extends MessageType>(
|
|
9
|
+
type: T,
|
|
10
|
+
handler: (windowId: string, message: MessageOf<T>) => void,
|
|
11
|
+
): () => void
|
|
12
|
+
has(windowId: string): boolean
|
|
13
|
+
onClose(handler: (windowId: string) => void): () => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ChildrenChannelController extends ChildrenChannel {
|
|
17
|
+
attach(windowId: string, transport: Transport): void
|
|
18
|
+
detach(windowId: string): void
|
|
19
|
+
dispose(): void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type ChildrenChannelConfig = {
|
|
23
|
+
observer: MessageObserver
|
|
24
|
+
onDeliveryFailed?: (windowId: string, message: ShellMessage) => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type ChildListener = (windowId: string, message: ShellMessage) => void
|
|
28
|
+
|
|
29
|
+
export function createChildrenChannel(config: ChildrenChannelConfig): ChildrenChannelController {
|
|
30
|
+
const { observer, onDeliveryFailed } = config
|
|
31
|
+
const entries = new Map<string, Transport>()
|
|
32
|
+
const byType = new Map<MessageType, Set<ChildListener>>()
|
|
33
|
+
const closeListeners = new Set<(windowId: string) => void>()
|
|
34
|
+
|
|
35
|
+
function notifyListeners(windowId: string, message: ShellMessage) {
|
|
36
|
+
const bucket = byType.get(message.type)
|
|
37
|
+
if (!bucket || bucket.size === 0) return
|
|
38
|
+
for (const listener of bucket) {
|
|
39
|
+
try {
|
|
40
|
+
listener(windowId, message)
|
|
41
|
+
} catch {
|
|
42
|
+
// one rogue listener must not break siblings
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
attach(windowId, transport) {
|
|
49
|
+
const existing = entries.get(windowId)
|
|
50
|
+
if (existing) throw new Error(`Child '${windowId}' already attached`)
|
|
51
|
+
entries.set(windowId, transport)
|
|
52
|
+
transport.onMessage((message) => {
|
|
53
|
+
observer.notify('inbound', message)
|
|
54
|
+
notifyListeners(windowId, message)
|
|
55
|
+
})
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
detach(windowId) {
|
|
59
|
+
const transport = entries.get(windowId)
|
|
60
|
+
if (!transport) return
|
|
61
|
+
entries.delete(windowId)
|
|
62
|
+
transport.close()
|
|
63
|
+
// `entries.delete` ran first, so `has(windowId)` is already false here —
|
|
64
|
+
// a listener replying to the gone child is a best-effort no-op.
|
|
65
|
+
for (const listener of closeListeners) {
|
|
66
|
+
try {
|
|
67
|
+
listener(windowId)
|
|
68
|
+
} catch {
|
|
69
|
+
// one rogue listener must not break siblings
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
dispose() {
|
|
75
|
+
for (const transport of entries.values()) transport.close()
|
|
76
|
+
entries.clear()
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
send(windowId, message) {
|
|
80
|
+
const transport = entries.get(windowId)
|
|
81
|
+
if (!transport) {
|
|
82
|
+
onDeliveryFailed?.(windowId, message)
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
observer.notify('outbound', message)
|
|
86
|
+
transport.send(message)
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
on(type, handler) {
|
|
90
|
+
let bucket = byType.get(type)
|
|
91
|
+
if (!bucket) {
|
|
92
|
+
bucket = new Set()
|
|
93
|
+
byType.set(type, bucket)
|
|
94
|
+
}
|
|
95
|
+
bucket.add(handler as ChildListener)
|
|
96
|
+
return () => {
|
|
97
|
+
bucket.delete(handler as ChildListener)
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
has(windowId) {
|
|
102
|
+
return entries.has(windowId)
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
onClose(handler) {
|
|
106
|
+
closeListeners.add(handler)
|
|
107
|
+
return () => {
|
|
108
|
+
closeListeners.delete(handler)
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ShellMessage } from '../../domain/message/types'
|
|
2
|
+
|
|
3
|
+
export type MessageDirection = 'inbound' | 'outbound'
|
|
4
|
+
|
|
5
|
+
export type ObserverHandler = (message: ShellMessage, direction: MessageDirection) => void
|
|
6
|
+
|
|
7
|
+
export interface MessageObserver {
|
|
8
|
+
notify(direction: MessageDirection, message: ShellMessage): void
|
|
9
|
+
subscribe(handler: ObserverHandler): () => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createMessageObserver(): MessageObserver {
|
|
13
|
+
const handlers = new Set<ObserverHandler>()
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
notify(direction, message) {
|
|
17
|
+
if (handlers.size === 0) return
|
|
18
|
+
for (const handler of handlers) {
|
|
19
|
+
try {
|
|
20
|
+
handler(message, direction)
|
|
21
|
+
} catch {
|
|
22
|
+
// one rogue observer must not break siblings
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
subscribe(handler) {
|
|
27
|
+
handlers.add(handler)
|
|
28
|
+
return () => {
|
|
29
|
+
handlers.delete(handler)
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
}
|