/** * Authoring a `View` — iframe-mountable callable served by the domain worker. * * Each entry in `defineRemoteDomain({ views: { ... } })` becomes both: * - a first-class domain MEMBER: a `View` node at `/${origin}/views/` * attached to the Domain via an `of_domain` edge (slug `view.`), * addressable as `/:${origin}:view.`. `` is the map key, so it * lives in exactly one place. * - a Hono route on the worker at the path implied by `binding` * (`//` by default). * * The graph layout (`//views/`) is a fixed kernel convention; the * URL path that lands in `Function.binding.remoteUrl` is * `${url}//` — DECOUPLED from the graph layout. * * The author can override the URL via `binding` — host and/or path * placeholders are supported (the kernel's `route` mechanism does the * substitution + Hono matching, same as for methods). When `render` is * omitted, no worker route is mounted — useful for Views whose iframe * lives at an external host. */ import type { AuthPolicy, FunctionBinding } from '@astrale-os/kernel-api/routed' import type { EdgeEndpoint } from '@astrale-os/kernel-dsl' import type { Context } from 'hono' import type { FunctionContextApi } from '../auth/function-context.js' import type { AuthForPolicy } from '../method/context.js' import type { RemoteEnv } from '../method/context.js' export type ViewRenderContext = { /** Hono request context — read params, headers, query, etc. */ c: Context /** * Placeholders extracted from the request URL (host + path placeholders * declared in `binding.remoteUrl` / `binding.route.path`). Empty when * the binding has no placeholders. */ params: Record /** * Resolved auth context. Its nullability follows the View's `auth` policy: * non-null for `'required'`, `... | null` for `'optional'`, and `null` for * `'public'`. */ auth: AuthForPolicy /** Typed dependency container injected at server startup. */ deps: TDeps /** Local serving metadata. */ env: RemoteEnv /** View function identity tools. Use `fn.kernel()` for server-rendered graph reads. */ fn: FunctionContextApi } export type ViewDef = { /** * Override the binding (URL + route shape). When absent, SDK defaults to * `{ remoteUrl: ${url}// }`. The HTTP verb (GET for * views) is applied by the worker route mounter at mount time — it is NOT * stored on the binding. * * Use this to bind to a custom host (e.g. `https://{tenant}.example.com`) * or REST-style path. Host + path placeholders both supported. */ binding?: FunctionBinding /** * Worker-relative path the View's iframe is served from (e.g. `'/ui/note'`), * for Views backed by the client SPA instead of a `render`. The spec producer * resolves it against the serving url (`binding.remoteUrl = ${url}${mount}`) * at materialize time — so the dev never hardcodes a URL. Mutually exclusive * with `render`; takes precedence over `binding`. */ mount?: string /** * Shell handshake mode for the mounted iframe. Omit for the common case: * SPA-mounted views (`mount`) use the shell handshake; inline `render` views * default to `'none'` because they do not include the shell client. */ handshake?: 'shell' | 'none' /** Authentication policy. Defaults to `'required'`. */ auth?: TAuth /** * Optional pre-render authorization. Runs after auth resolution; throw to * deny. SDK wraps as 403. */ authorize?: (ctx: ViewRenderContext) => void | Promise /** * Render the iframe response. May return a redirect, a proxy, or inline * HTML. When omitted, no worker route is mounted — the binding URL * points elsewhere (CDN, external service). */ render?: (ctx: ViewRenderContext) => Response | Promise /** * Optional target(s) the View attaches to via `view_for` edge(s). Typically * `selfOf(SomeClass)` to bind to a class meta-node, or a `CorePath` * pointing at a specific instance. Pass an array to attach the same View * to multiple targets (one `view_for` edge is materialized per entry). */ viewFor?: EdgeEndpoint | EdgeEndpoint[] /** Optional human-readable description. */ description?: string } /** * Identity helper for authoring a View. Returns its argument unchanged — * `defineRemoteDomain` consumes the typed shape and the author retains * full type inference on `render` / `authorize`. */ export function defineView( def: ViewDef, ): ViewDef { return def }