/** * Authoring a standalone remote function — a callable not bound to a class. * (The verb keeps the `defineRemoteFunction` name; the node it materializes is * the canonical kernel `Function` class, replacing the former remote-function wrapper.) * * Each entry in `defineRemoteDomain({ remoteFunctions: { ... } })` becomes: * - a graph node at `/${origin}/functions/` — a first-class domain MEMBER, * materialized as the kernel `Function` class and attached to the Domain via an * `of_domain` edge (slug `function.`), so it is addressable by the * semantic path `/:${origin}:function.` (the form a `postInstall` uses). * - a Hono route on the worker at the path implied by `binding` * (`//` POST by default — the route URL is decoupled * from the graph layout). * * The slug = the map key (single source of truth, no duplication). The member * ref (`function.`), the layout (`//functions/`), and the * `of_domain` edge slug are all DERIVED from it — there is nothing else to name. */ import type { AuthPolicy, FunctionBinding } from '@astrale-os/kernel-api/routed' import type { FnMap } from '@astrale-os/kernel-client' import type { BoundClientSessionView } from '@astrale-os/kernel-client/session' import type { Defer, Sleep } from '@astrale-os/kernel-server' import type { Context } from 'hono' import type { z } from 'zod' import type { FunctionContextApi } from '../auth/function-context.js' import type { DomainAuthority } from '../auth/issuer-mint.js' import type { AuthForPolicy, CallerContext, KernelForAuth, RemoteEnv } from '../method/context.js' import type { Step } from '../step/index.js' type RemoteFunctionBaseContext< TParams, TDeps = unknown, TKernel = BoundClientSessionView | null, TAuth extends AuthPolicy = AuthPolicy, > = { /** Validated params (Zod-checked against `inputSchema`). */ params: TParams /** Hono request context — escape hatch for headers, raw body, etc. */ c: Context /** * Resolved auth context. Its nullability follows the function's `auth` * policy: non-null for the default `'required'`, `... | null` for * `'optional'`, and `null` for `'public'`. */ auth: AuthForPolicy /** Typed dependency container injected at server startup. */ deps: TDeps /** Local serving metadata. */ env: RemoteEnv /** * `BoundClientSessionView` to the parent kernel, bound to the composed * credential `union(delegation, self)` — same shape as `RemoteContext.kernel` * for `remoteMethod`, including the typed graph read/write sugar flattened onto * it (`kernel.get` / `kernel.children` / `kernel.query`, * `kernel.createNode` / `kernel.mutate(patch)`, …) over the `function.get` / * `function.mutate` syscalls. Nullability follows {@link KernelForAuth} of the * function's `auth`: non-null for the default `'required'`, `… | null` for * `'optional'`, `null` for `'public'` (use `ctx.fn.kernel()` there, after * verifying the upstream). */ kernel: TKernel /** Inbound kernel/peer context. Present only for authenticated invocations. */ caller?: CallerContext /** Mint seam for identities THIS DOMAIN issues. */ domain: DomainAuthority /** Current function identity tools. */ fn: FunctionContextApi } export type RemoteFunctionAuthorizeContext< TParams, TDeps = unknown, TKernel = BoundClientSessionView | null, TAuth extends AuthPolicy = AuthPolicy, > = RemoteFunctionBaseContext export interface RemoteFunctionContext< TParams, TDeps = unknown, TKernel = BoundClientSessionView | null, TAuth extends AuthPolicy = AuthPolicy, > extends RemoteFunctionBaseContext { /** Durable-shaped step builder. Inline today, replayable by future backends. */ step: Step /** Run independent work without making its completion part of this result. */ defer: Defer /** Pause execution for a relative duration. Durable hosts may persist the wait. */ sleep: Sleep } export type RemoteFunctionDef< TParams = unknown, TResult = unknown, TDeps = unknown, TAuth extends AuthPolicy = 'required', > = { /** Zod schema for the call's parameters. */ inputSchema: z.ZodType /** Zod schema for the call's result. */ outputSchema: z.ZodType /** * Override the binding (URL + route shape). When absent, SDK defaults to * `{ remoteUrl: ${url}// }`. The HTTP verb (POST * for functions) is applied by the worker route mounter at mount time — it is * NOT stored on the binding, so the materialized `Function.binding` carries * `remoteUrl` only. * * Use this to bind to a custom host or REST-style path. Host + path * placeholders both supported. */ binding?: FunctionBinding /** * Authentication policy. Defaults to `'required'`. Captured as a literal type * so it drives `ctx.auth` and {@link KernelForAuth} on the * `execute`/`authorize` context: omit it (or `'required'`) makes both * non-null; `'optional'` widens them to `... | null`; `'public'` makes them * `null` (webhooks reach the graph via `ctx.fn.kernel()`). */ auth?: TAuth /** Optional pre-execute authorization. Throw to deny. */ authorize?: ( ctx: RemoteFunctionAuthorizeContext, TAuth>, ) => void | Promise /** The function body. May be async. */ execute: ( ctx: RemoteFunctionContext, TAuth>, ) => TResult | Promise /** Optional human-readable description. */ description?: string } // Loosely-typed "bag" for the per-domain function maps. The PRECISE typing is // enforced at each `defineRemoteFunction` call site; this only has to HOLD defs // of ANY auth policy. We deliberately do NOT pin a single concrete `TAuth`: // `RemoteFunctionContext` carries `kernel: KernelForAuth` (an // auth-dependent type that resolves to a concrete union), so any single concrete // policy — `'required'` OR the full `AuthPolicy` union — rejects the others when // a handler is assigned into the bag under parameter contravariance (a public // handler wants `kernel: null`, a required one `kernel: Kernel`; neither fits a // fixed bag). Relaxing the `authorize`/`execute` PARAMS to `any` is what makes // the bag accept every policy; soundness is unchanged — the bag was already // `any, any, any`, and the real contract is checked at the call site. // oxlint-disable no-explicit-any export type AnyRemoteFunctionDef = Omit< RemoteFunctionDef, 'authorize' | 'execute' > & { authorize?: (ctx: any) => void | Promise execute: (ctx: any) => any } // oxlint-enable no-explicit-any // Compile-time regression guards (pure `type` aliases — zero runtime emit, but // checked by `pnpm typecheck` since this file is under `src/`). Each asserts a // function of the given auth policy still FITS the bag. If `AnyRemoteFunctionDef` // ever narrows back and rejects a policy, the matching `_Assert` errors. type _AssertTrue = T type _FnFitsBag = D extends AnyRemoteFunctionDef ? true : false // Exported only so `noUnusedLocals` treats the assertions as used — it is NOT // re-exported by `../define`, so it never reaches the package's public surface. // A regression that makes the bag reject a policy turns the matching // `_AssertTrue` into a compile error here. export type _AuthPolicyBagGuards = [ _AssertTrue<_FnFitsBag>>, _AssertTrue<_FnFitsBag>>, _AssertTrue<_FnFitsBag>>, ] /** * Identity helper for authoring a RemoteFunction. Returns its argument * unchanged — `defineRemoteDomain` consumes the typed shape. */ export function defineRemoteFunction< TParams, TResult, TDeps = unknown, TAuth extends AuthPolicy = 'required', >( def: RemoteFunctionDef, ): RemoteFunctionDef { return def }