/** * Node-App SDK for TypeScript/JavaScript * * Provides the NodeApp abstract class and runNodeApp() entry point * for building scripted node-app plugins that communicate with the * host backend via Unix Socket IPC using a JSON-newline protocol. * * Logs are written to files in the NODE_APP_LOG_DIR directory for * persistence, while IPC messages are used for real-time streaming. * * @example * ```typescript * import { NodeApp, runNodeApp, host } from "@econ-v1/app-sdk"; * * class MyApp extends NodeApp { * readonly metadata = { * name: "my-app", * version: "0.1.0", * author: "Me", * description: "My app", * capabilities: ["http_handler"], * }; * * async handleRequest(request: AppRequest): Promise { * return { status: 200, headers: {}, body: { hello: "world" } }; * } * } * * runNodeApp(new MyApp()); * ``` */ import { Database } from "bun:sqlite"; import type { AppMemorySample, NodeAppCtx } from "./platform/types.js"; export type { AppMemorySample } from "./platform/types.js"; export declare const initAppTelemetry: (appName: string) => Promise; export declare const telemetryActive: () => boolean; export declare const incrementCounter: (name: string, value?: number, attributes?: Record) => void; /** * Open an app-owned SQLite database with a bounded page cache. * * Bun does not expose SQLite's live cache-use counter, so this is explicitly * a configured upper bound rather than a fabricated memory measurement. */ export declare function openDatabase(path: string, options?: { cacheSizeKb?: number; }): Database; /** @internal exported for unit tests; not part of the public SDK surface. */ export declare const _resolveCtx: () => NodeAppCtx; /** @internal test-only export */ export declare const _testRunNodeAppPath: () => string | undefined; /** @internal test-only export; also the production exit pathway. */ export declare const _testExitWorker: (code: number, mainThread: boolean, exit?: (code: number) => never) => void; export interface AppMetadata { name: string; version: string; author: string; description: string; capabilities: string[]; } /** * Caller identity set by the host proxy after validating the scoped JWT. * Only present on requests that arrived through the proxy route. * Apps should trust this struct — it is injected by the host over the Unix IPC socket, * not derived from HTTP headers (which are stripped by the proxy). */ export interface CallerContext { /** App name whose scoped JWT was used — always equals this app's name */ app_name: string; /** Permissions granted at consent time and embedded in the scoped JWT */ granted_permissions: string[]; } export interface AppRequest { id: string; method: string; path: string; /** HTTP headers forwarded from the client (x-node-* headers are stripped by the proxy) */ headers: Record; body: unknown; /** * Caller identity injected by the host proxy after JWT validation. * null for direct/internal requests (e.g. capability invocations from the host). * Use this for auth context — do NOT read x-node-* headers. */ caller: CallerContext | null; } export interface AppResponse { status: number; headers: Record; body: unknown; } export interface AppEvent { name: string; data: unknown; } export interface CapabilityRequest { id: string; capability: string; payload: unknown; /** Host-side IPC deadline in milliseconds. When set, the host dispatches the * capability to the provider app with THIS timeout instead of its 30s default. * `invokeCapability` fills this from its `timeoutMs` argument automatically. */ timeout_ms?: number; /** W3C trace-context carrier (`traceparent`/`tracestate`) for distributed * tracing across the host IPC boundary (#991). `invokeCapability` injects the * active context here automatically when telemetry is enabled; the receiving * app extracts it to continue the trace. Absent/ignored when OTel is off. */ trace_context?: Record; /** Opaque host-issued correlator copied only across nested capability calls. */ invocation_context_id?: string; /** Lease duration in seconds for the app's lifetime, as granted by the * host's lease engine. Absent/`undefined` means an unbounded lease * (T=∞ — today's behavior). Purely additive wire field. */ lease_secs?: number; } export interface CapabilityResponse { id: string; success: boolean; payload: unknown; } export interface CapabilityExample { label: string; request: unknown; } export interface ProvidedCapability { name: string; description: string; request_schema?: unknown; response_schema?: unknown; priority?: number; examples?: CapabilityExample[]; } export interface HeapSnapshotRequest { type: "heap_snapshot_request"; id: string; path: string; } export interface HeapSnapshotResponse { type: "heap_snapshot_response"; id: string; success: boolean; error?: string; size_bytes?: number; } /** * Host interaction helper for logging from within app code. * Logs are written to files for persistence and sent via IPC for real-time streaming. */ export declare const host: { trace: (message: string) => void; debug: (message: string) => void; info: (message: string) => void; warn: (message: string) => void; error: (message: string) => void; /** * Hold the app's lease open for work that outlives the frame that started * it (e.g. a detached background task). Increments the same in-flight * counter that brackets `handleCapability`/`handleRequest`/`handleEvent`, * which blocks the lease timer from arming while the hold is outstanding. * * Returns a release closure. Idempotent: calling it more than once has no * further effect after the first call. */ holdLease: () => (() => void); }; /** * Invoke a capability on the host via the capability router. * Returns a promise that resolves with the capability response. * * @param request - The capability request to invoke * @param timeoutMs - Optional timeout in milliseconds (default: 30000) */ export declare function invokeCapability(request: CapabilityRequest, timeoutMs?: number): Promise; /** * Publish an event to the host event bus. * * The event_name MUST be namespaced with the app name prefix * (e.g., "my-app.status.updated"). The host validates the namespace * and rejects events that do not start with "{app_name}.". * * This is a fire-and-forget operation — no response is returned. */ export declare function publishEvent(eventName: string, data: unknown): void; /** @internal test-only export; not part of the public SDK surface. */ export declare function _testResetLeaseState(): void; /** @internal test-only export; not part of the public SDK surface. */ export declare function _testSetLeaseExitFn(fn: () => void): void; /** @internal test-only export; not part of the public SDK surface. */ export declare function _testLeaseState(): { inFlight: number; rememberedSecs: number | undefined; timerArmed: boolean; draining: boolean; }; /** @internal Exact runtime heap sample used by the IPC timer and unit tests. */ export declare const _sampleMemory: () => AppMemorySample; export declare const _captureHeapSnapshotTo: (path: string, generate?: () => ArrayBuffer, write?: (path: string, bytes: ArrayBuffer) => Promise) => Promise<{ size_bytes: number; }>; export declare function _handleHeapSnapshotRequest(request: HeapSnapshotRequest, send: (message: HeapSnapshotResponse) => void, capture?: (path: string) => Promise<{ size_bytes: number; }>): Promise; interface RoutePolicy { /** All listed permissions must be present in request.caller.granted_permissions */ requiredPermissions?: string[]; } type RouteHandler = (req: AppRequest) => Promise; /** * Abstract base class for TypeScript/JavaScript node-apps. * * Subclass this and implement handleRequest/handleEvent as needed. * Prefer using `route()` for HTTP handler registration — it provides * declarative scope enforcement as a defense-in-depth layer on top of * the host-side manifest endpoint_policies check. */ export declare abstract class NodeApp { abstract readonly metadata: AppMetadata; private readonly _routes; /** * Register a declarative route handler with an optional scope policy. * * The host already enforces `endpoint_policies` from the manifest before the * request reaches the app. This method provides a defense-in-depth layer: * the SDK re-checks `request.caller.granted_permissions` before dispatching. * * @param method HTTP method to match ("GET", "POST", "*" for any) * @param path Path glob relative to app root ("/data", "/admin/**") * @param policy Optional scope requirements * @param handler Async handler called when route + policy match * * @example * ```typescript * this.route("GET", "/data", { requiredPermissions: ["payments:read"] }, * async (req) => this.json(await fetchData())); * ``` */ protected route(method: string, path: string, policy: RoutePolicy, handler: RouteHandler): void; /** * Initialize the app with configuration from the host. * Override for custom initialization logic. * If using `route()`, register routes here. */ init(_config: Record): Promise; /** * Shut down the app gracefully. * Override for custom cleanup logic. */ shutdown(): Promise; /** * Handle an incoming HTTP request proxied from the host. * * If routes are registered via `route()`, the base implementation dispatches * automatically (with scope enforcement). Override this only for fully custom * routing logic — the override bypasses the SDK-level scope check. */ handleRequest(request: AppRequest): Promise; /** Internal route dispatcher — checks scope then calls handler */ private _dispatchRoute; /** * Handle a domain event forwarded from the host. * Override if the app declares "event_listener" capability. */ handleEvent(_event: AppEvent): Promise; /** * Return the list of service capabilities this app provides. * Override to declare capabilities for the capability registry. * Default returns an empty list (no capabilities provided). */ providedCapabilities(): ProvidedCapability[]; /** * Handle a capability invocation from another app via the capability router. * Override to implement capability handling logic. * Default returns an error response. */ handleCapability(_request: CapabilityRequest): Promise; protected json(body: unknown, status?: number): AppResponse; protected error(message: string, status?: number): AppResponse; protected notFound(): AppResponse; } /** * Connect the app to the host backend via Unix Socket IPC. * * The socket path is provided via the `NODE_APP_SOCKET` environment variable. * The host creates the socket and waits for the app to connect. * * Protocol: * 1. App connects to Unix socket * 2. Host sends init message with config * 3. App sends ready message with metadata * 4. Host sends request/event messages, app responds * 5. Host sends shutdown, app cleans up and exits */ export declare function runNodeApp(app: NodeApp): void; /** @internal Test-only inbound IPC seam. */ export declare function _testHandleMessage(app: NodeApp, message: unknown, send: (message: unknown) => void): Promise;