devframe 0.7.2 → 0.7.4

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.
Files changed (43) hide show
  1. package/dist/adapters/build.d.mts +1 -1
  2. package/dist/adapters/build.mjs +1 -1
  3. package/dist/adapters/cac.d.mts +1 -1
  4. package/dist/adapters/cac.mjs +1 -1
  5. package/dist/adapters/cli.d.mts +1 -1
  6. package/dist/adapters/cli.mjs +1 -1
  7. package/dist/adapters/dev.d.mts +2 -2
  8. package/dist/adapters/dev.mjs +1 -1
  9. package/dist/adapters/embedded.d.mts +1 -1
  10. package/dist/adapters/mcp.d.mts +1 -1
  11. package/dist/adapters/mcp.mjs +1 -1
  12. package/dist/{build-server-Bo0G5XcS.mjs → build-server-vGNtqlOd.mjs} +7 -3
  13. package/dist/{cac-BFCgKymz.mjs → cac-CZk1y4rm.mjs} +2 -2
  14. package/dist/client/index.d.mts +18 -2
  15. package/dist/client/index.mjs +27 -4
  16. package/dist/{context-CKJheEtf.mjs → context-ClRdkpJ7.mjs} +1 -1
  17. package/dist/{context-B1sekkgB.d.mts → context-DxFsOWkc.d.mts} +1 -1
  18. package/dist/{dev-Bs4oRVnI.mjs → dev-CicFHIt8.mjs} +18 -5
  19. package/dist/{devframe-CnTVuGw9.d.mts → devframe-WHtcqYlZ.d.mts} +705 -554
  20. package/dist/helpers/vite.d.mts +1 -1
  21. package/dist/helpers/vite.mjs +2 -2
  22. package/dist/{host-h3-Dsf-Tgwx.mjs → host-h3-DBzbpjXk.mjs} +51 -4
  23. package/dist/{http-BirzlyE9.mjs → http-DHckZCYZ.mjs} +1 -1
  24. package/dist/{index-Dpsr-8dD.d.mts → index-Cj0M3G3F.d.mts} +3 -47
  25. package/dist/index.d.mts +3 -3
  26. package/dist/node/auth.d.mts +2 -1
  27. package/dist/node/hub-internals.d.mts +2 -2
  28. package/dist/node/hub-internals.mjs +1 -1
  29. package/dist/node/index.d.mts +23 -6
  30. package/dist/node/index.mjs +4 -4
  31. package/dist/recipes/interactive-auth.d.mts +2 -2
  32. package/dist/recipes/interactive-auth.mjs +1 -1
  33. package/dist/recipes/open-helpers.d.mts +1 -1
  34. package/dist/rpc/transports/ws-server.d.mts +1 -1
  35. package/dist/{server-4QH37sb-.d.mts → server-BRxA2OAh.d.mts} +3 -3
  36. package/dist/{server-CRFYfRqK.mjs → server-DvrOR1LG.mjs} +2 -2
  37. package/dist/{storage-yrUZaiib.mjs → storage-E9KrCsLr.mjs} +4 -0
  38. package/dist/types/index.d.mts +3 -3
  39. package/dist/utils/events.d.mts +1 -1
  40. package/dist/utils/shared-state.d.mts +1 -1
  41. package/dist/utils/streaming-channel.d.mts +1 -1
  42. package/dist/{ws-server-CceOy6He.d.mts → ws-server-9-wn7MNQ.d.mts} +1 -1
  43. package/package.json +1 -1
@@ -1,8 +1,9 @@
1
- import { _ as RpcFunctionDefinition } from "./types-CrzNxXKq.mjs";
1
+ import { _ as RpcFunctionDefinition, v as RpcFunctionDefinitionAny } from "./types-CrzNxXKq.mjs";
2
2
  import { S as RpcFunctionsCollectorBase } from "./index-DgsLFhZg.mjs";
3
- import { t as DevframeNodeRpcSessionMeta } from "./ws-server-CceOy6He.mjs";
3
+ import { t as DevframeNodeRpcSessionMeta } from "./ws-server-9-wn7MNQ.mjs";
4
4
  import { CAC } from "cac";
5
5
  import { GenericSchema, InferOutput } from "valibot";
6
+ import { Peer } from "crossws";
6
7
  import { BirpcReturn } from "birpc";
7
8
  import { Diagnostic, DiagnosticDefinition, defineDiagnostics } from "nostics";
8
9
  //#region src/adapters/flags.d.ts
@@ -284,134 +285,185 @@ interface DevframeAgentHost {
284
285
  getResource: (id: string) => AgentResource | undefined;
285
286
  }
286
287
  //#endregion
287
- //#region src/types/diagnostics.d.ts
288
- /**
289
- * A diagnostics definition object built with `defineDiagnostics`. Typed as
290
- * `unknown` because each integration's definition has a distinct narrow shape
291
- * (e.g. specific code keys like `DF0001` / `MYP0001`), and TypeScript's mapped
292
- * types don't allow assigning a narrow-keyed result to a generically-keyed
293
- * one. The host stores them in a heterogeneous registry.
294
- */
295
- type DevframeDiagnosticsDefinition = ReturnType<typeof defineDiagnostics<any, any>>;
296
- /**
297
- * The shared diagnostics lookup exposed by the host. A `Proxy` that resolves
298
- * any registered code name to its `nostics` handle (a callable that builds
299
- * a diagnostic and routes it through registered reporters). Typed loosely
300
- * because it spans heterogeneous definitions registered by different
301
- * integrations.
302
- */
303
- type DevframeDiagnosticsLogger = Record<string, any>;
288
+ //#region src/utils/shared-state.d.ts
289
+ type ImmutablePrimitive = undefined | null | boolean | string | number | Function;
290
+ type Immutable<T> = T extends ImmutablePrimitive ? T : T extends Array<infer U> ? ImmutableArray<U> : T extends Map<infer K, infer V> ? ImmutableMap<K, V> : T extends Set<infer M> ? ImmutableSet<M> : ImmutableObject<T>;
291
+ type ImmutableArray<T> = ReadonlyArray<Immutable<T>>;
292
+ type ImmutableMap<K, V> = ReadonlyMap<Immutable<K>, Immutable<V>>;
293
+ type ImmutableSet<T> = ReadonlySet<Immutable<T>>;
294
+ type ImmutableObject<T> = { readonly [K in keyof T]: Immutable<T[K]>; };
304
295
  /**
305
- * Options accepted by the host's `defineDiagnostics()` factory — mirrors
306
- * `nostics`'s shape but the host pre-wires its ANSI console reporter, so
307
- * plugins typically omit `reporters`.
296
+ * Serializable patch describing a single mutation to a `SharedState`.
297
+ * Structurally compatible with JSON-Patch and is safe to send over RPC.
308
298
  */
309
- interface DevframeDefineDiagnosticsOptions<Codes extends Record<string, DiagnosticDefinition>> {
310
- docsBase?: string | ((code: keyof Codes) => string | undefined);
311
- codes: Codes;
312
- reporters?: ReadonlyArray<(d: Diagnostic, o?: any) => void>;
299
+ interface SharedStatePatch {
300
+ op: 'add' | 'remove' | 'replace';
301
+ path: readonly (string | number)[];
302
+ value?: unknown;
313
303
  }
314
304
  /**
315
- * Host for structured diagnostics — a thin layer over `nostics` that lets
316
- * integrations register their own coded errors/warnings into a shared
317
- * registry without taking a direct dependency on `nostics`.
318
- *
319
- * Typical usage from a plugin's `setup(ctx)`:
320
- *
321
- * ```ts
322
- * const myDiagnostics = ctx.diagnostics.defineDiagnostics({
323
- * docsBase: 'https://example.com/errors',
324
- * codes: {
325
- * MYP0001: { why: 'Something went wrong' },
326
- * },
327
- * })
328
- * ctx.diagnostics.register(myDiagnostics)
329
- *
330
- * // Through the shared lookup (loose typing):
331
- * throw ctx.diagnostics.logger.MYP0001()
332
- *
333
- * // Or directly on the typed handle returned from `defineDiagnostics`:
334
- * throw myDiagnostics.MYP0001()
335
- * ```
305
+ * State host that is immutable by default with explicit mutate.
336
306
  */
337
- interface DevframeDiagnosticsHost {
307
+ interface SharedState<T> {
338
308
  /**
339
- * Proxy-backed lookup of every registered diagnostic handle by code name.
340
- * Resolves to a `nostics` `DiagnosticHandle` — a callable that builds a
341
- * diagnostic and routes it through registered reporters; prefix with
342
- * `throw` to raise. Loosely typed — for autocompletion, keep a reference
343
- * to the typed result of `defineDiagnostics()` instead.
309
+ * Get the current state. Immutable.
344
310
  */
345
- readonly logger: DevframeDiagnosticsLogger;
311
+ value: () => Immutable<T>;
346
312
  /**
347
- * Register additional diagnostic definitions with this host. After
348
- * registration, codes from the new definition are reachable via
349
- * `host.logger.CODE`. Plugins that want shared output formatting should
350
- * build their diagnostics via `host.defineDiagnostics()` first — that
351
- * factory pre-wires the host's ANSI console reporter.
313
+ * Subscribe to state changes.
352
314
  */
353
- register: (definitions: Record<string, unknown>) => void;
315
+ on: EventEmitter<SharedStateEvents<T>>['on'];
354
316
  /**
355
- * Build a typed diagnostics object with the host's ANSI console reporter
356
- * pre-wired. Mirrors `nostics`'s `defineDiagnostics` so integrations don't
357
- * need to take a direct dependency on `nostics`.
317
+ * Mutate the state.
358
318
  */
359
- defineDiagnostics: <const Codes extends Record<string, DiagnosticDefinition>>(options: DevframeDefineDiagnosticsOptions<Codes>) => ReturnType<typeof defineDiagnostics<Codes, any>>;
319
+ mutate: (fn: (state: T) => void, syncId?: string) => void;
320
+ /**
321
+ * Apply patches to the state.
322
+ */
323
+ patch: (patches: SharedStatePatch[], syncId?: string) => void;
324
+ /**
325
+ * Sync IDs that have been applied to the state.
326
+ */
327
+ syncIds: Set<string>;
360
328
  }
361
- //#endregion
362
- //#region src/types/host.d.ts
363
- interface DevframeHost {
329
+ interface SharedStateEvents<T> {
330
+ updated: (fullState: T, patches: SharedStatePatch[] | undefined, syncId: string) => void;
331
+ }
332
+ interface SharedStateOptions<T> {
364
333
  /**
365
- * Serve a static directory at the given URL base. Called by
366
- * `DevframeViewHost.hostStatic`. Implementations map this to whatever
367
- * the underlying runtime expects (Vite middleware, h3 handler, no-op
368
- * for build snapshots).
334
+ * Initial state.
369
335
  */
370
- mountStatic: (base: string, distDir: string) => void | Promise<void>;
336
+ initialValue: T;
371
337
  /**
372
- * Serve the host's connection meta (`__connection.json`) at the given URL
373
- * base, so a devframe SPA mounted there can discover the RPC/WS endpoint
374
- * via `connectDevframe()`'s relative `./__connection.json` fetch.
375
- *
376
- * Called by `mountDevframe` for each mounted devframe (alongside
377
- * `mountStatic`). Without it, an embedded SPA can only discover the
378
- * endpoint by inheriting it from a same-origin parent window — which fails
379
- * for cross-origin or sandboxed iframes. Implementations serve the same
380
- * meta they expose at the hub's own base.
338
+ * Enable patches.
381
339
  *
382
- * Optional in the type, but a host that mounts a devframe with a servable
383
- * `distDir` yet omits this hook triggers a `DF8106` diagnostic, since the
384
- * SPA's `./__connection.json` fetch would otherwise fall through and break
385
- * silently. A static-snapshot host that bakes the meta into its served files
386
- * can implement it as a no-op to acknowledge this intentionally.
340
+ * @default false
387
341
  */
388
- mountConnectionMeta?: (base: string) => void | Promise<void>;
342
+ enablePatches?: boolean;
343
+ }
344
+ declare function createSharedState<T extends object>(options: SharedStateOptions<T>): SharedState<T>;
345
+ //#endregion
346
+ //#region src/utils/streaming-channel.d.ts
347
+ /**
348
+ * Serialized error shape sent over the wire when a stream ends with a failure.
349
+ * Stays JSON-safe so the strict-JSON encoder can carry it without coercion.
350
+ */
351
+ interface StreamErrorPayload {
352
+ name: string;
353
+ message: string;
354
+ }
355
+ /**
356
+ * Single buffered chunk in the server-side ring buffer.
357
+ *
358
+ * Sequence numbers start at 1 and increment per write. Subscribers track
359
+ * `lastSeenSeq` and ask for `afterSeq` on resubscribe so the server can
360
+ * replay any chunks the client missed during a brief disconnect.
361
+ */
362
+ interface BufferedChunk<T> {
363
+ seq: number;
364
+ chunk: T;
365
+ }
366
+ interface StreamSinkEvents<T> {
367
+ /** Fired for each `write()`. The RPC layer subscribes and broadcasts. */
368
+ chunk: (seq: number, chunk: T) => void;
369
+ /** Terminal — fired exactly once per sink lifetime. */
370
+ end: (error?: StreamErrorPayload) => void;
371
+ }
372
+ interface CreateStreamSinkOptions {
373
+ id?: string;
389
374
  /**
390
- * Return the public origin the host is reachable at, e.g.
391
- * `http://localhost:5173`. Used by the dock host to enrich remote
392
- * iframe URLs with a full `origin`. Called only when a dock needs an
393
- * absolute URL; hosts that never serve remote docks can return any
394
- * reasonable value.
375
+ * Size of the per-stream ring buffer kept for replay-on-resubscribe.
376
+ * `0` (default) disables replay.
395
377
  */
396
- resolveOrigin: () => string;
378
+ replayWindow?: number;
379
+ }
380
+ /**
381
+ * Server-side producer handle. Two equivalent surfaces share one piece of
382
+ * state: the imperative `write/error/close` triple, and a `WritableStream<T>`
383
+ * for `pipeTo`-style consumption.
384
+ */
385
+ interface StreamSink<T> {
386
+ /** Stable id used by clients to subscribe. */
387
+ readonly id: string;
397
388
  /**
398
- * Resolve a directory the host owns for persisted devframe state.
399
- * Each host picks its own app-name namespace so storage doesn't
400
- * collide between, say, the Vite host (`.vite/devframe`) and a
401
- * standalone CLI host (`.<appName>/devframe`).
389
+ * Aborts when the consumer cancels (server-side) or when the transport
390
+ * loses every subscriber. Producers should poll `signal.aborted` and exit
391
+ * cleanly.
392
+ */
393
+ readonly signal: AbortSignal;
394
+ /** `true` after `close()` / `error()`. Further writes throw. */
395
+ readonly closed: boolean;
396
+ /** Last allocated sequence number. `0` until the first write. */
397
+ readonly lastSeq: number;
398
+ write: (chunk: T) => void;
399
+ error: (reason: unknown) => void;
400
+ close: () => void;
401
+ /** External-cancel path. Aborts the signal so handlers can short-circuit. */
402
+ abort: (reason?: unknown) => void;
403
+ /** `WritableStream<T>` adapter — same in-memory state as the imperative API. */
404
+ readonly writable: WritableStream<T>;
405
+ /**
406
+ * Internal — RPC layer subscribes to receive chunk/end notifications.
407
+ * Not part of the public contract; do not call directly.
402
408
  *
403
- * - `workspace` — per-project state (settings, caches). Typically
404
- * under `${workspaceRoot}/node_modules/.<appName>/devframe/`.
405
- * - `global` — per-user state (auth tokens, machine-wide
406
- * preferences). Typically under
407
- * `${homedir()}/.<appName>/devframe/`.
409
+ * @internal
410
+ */
411
+ readonly events: EventEmitter<StreamSinkEvents<T>>;
412
+ /**
413
+ * Internal — replay buffer. RPC layer reads on (re)subscribe to feed
414
+ * missed chunks before going live.
408
415
  *
409
- * Implementations should ensure the directory exists or be safe to
410
- * pass to a downstream `createStorage(...)` call that creates it
411
- * lazily.
416
+ * @internal
412
417
  */
413
- getStorageDir: (scope: 'workspace' | 'global') => string;
418
+ readonly buffer: ReadonlyArray<BufferedChunk<T>>;
419
+ }
420
+ interface CreateStreamReaderOptions {
421
+ id?: string;
422
+ /**
423
+ * Maximum number of buffered chunks held client-side while the consumer
424
+ * isn't draining. On overflow, the oldest chunk is dropped.
425
+ */
426
+ highWaterMark?: number;
427
+ /**
428
+ * Called when the chunk queue overflows the high-water mark. The RPC
429
+ * layer wires this to a coded warning; the primitive itself is
430
+ * RPC-agnostic.
431
+ */
432
+ onOverflow?: (dropped: number) => void;
433
+ /** Called when the consumer cancels — the RPC layer sends `:cancel` upstream. */
434
+ onCancel?: () => void;
435
+ }
436
+ /**
437
+ * Client-side consumer handle. Both an `AsyncIterable<T>` (for `for await`)
438
+ * and exposes `readable: ReadableStream<T>` (for `pipeTo`). Pick one — they
439
+ * share a single internal queue, so concurrent draining will race.
440
+ */
441
+ interface StreamReader<T> extends AsyncIterable<T> {
442
+ readonly id: string;
443
+ readonly cancelled: boolean;
444
+ readonly done: boolean;
445
+ /** Highest `seq` observed. Used for replay on reconnect. */
446
+ readonly lastSeenSeq: number;
447
+ /** `ReadableStream<T>` adapter for `pipeTo`-style consumption. */
448
+ readonly readable: ReadableStream<T>;
449
+ cancel: () => void;
450
+ /** @internal */
451
+ _push: (seq: number, chunk: T) => void;
452
+ /** @internal */
453
+ _end: (error?: StreamErrorPayload) => void;
414
454
  }
455
+ /**
456
+ * Build a server-side stream sink. RPC-agnostic — the RPC host wires
457
+ * `events.on('chunk' | 'end')` to broadcast, and reads `buffer` to replay
458
+ * for late or reconnecting subscribers.
459
+ */
460
+ declare function createStreamSink<T>(options?: CreateStreamSinkOptions): StreamSink<T>;
461
+ /**
462
+ * Build a client-side stream reader. RPC-agnostic — the RPC host calls
463
+ * `_push(seq, chunk)` on each incoming chunk and `_end(error?)` on the
464
+ * terminal frame. Consumers iterate with `for await` or pipe `readable`.
465
+ */
466
+ declare function createStreamReader<T>(options?: CreateStreamReaderOptions): StreamReader<T>;
415
467
  //#endregion
416
468
  //#region src/types/rpc-augments.d.ts
417
469
  /**
@@ -588,543 +640,555 @@ interface DevframeRpcServerFunctions {
588
640
  */
589
641
  interface DevframeRpcSharedStates {}
590
642
  //#endregion
591
- //#region src/types/views.d.ts
592
- interface DevframeViewHost {
643
+ //#region src/types/rpc.d.ts
644
+ interface DevframeNodeRpcSession {
645
+ meta: DevframeNodeRpcSessionMeta;
646
+ rpc: BirpcReturn<DevframeRpcClientFunctions, DevframeRpcServerFunctions, false>;
647
+ }
648
+ interface RpcBroadcastOptions<METHOD, Args extends any[]> {
649
+ method: METHOD;
650
+ args: Args;
651
+ optional?: boolean;
652
+ event?: boolean;
653
+ filter?: (client: BirpcReturn<DevframeRpcClientFunctions, DevframeRpcServerFunctions, false>) => boolean | void;
654
+ }
655
+ type RpcFunctionsHost = RpcFunctionsCollectorBase<DevframeRpcServerFunctions, DevframeNodeContext> & {
593
656
  /**
594
- * @internal
657
+ * Invoke a locally registered server RPC function directly.
658
+ *
659
+ * This bypasses transport and is useful for server-side cross-function calls.
595
660
  */
596
- buildStaticDirs: {
597
- baseUrl: string;
598
- distDir: string;
599
- }[];
661
+ invokeLocal: <T extends keyof DevframeRpcServerFunctions, Args extends Parameters<DevframeRpcServerFunctions[T]>>(method: T, ...args: Args) => Promise<Awaited<ReturnType<DevframeRpcServerFunctions[T]>>>;
600
662
  /**
601
- * Helper to host static files
602
- * - In `dev` mode, it will register middleware to `viteServer.middlewares` to host the static files
603
- * - In `build` mode, it will copy the static files to the dist directory
663
+ * Broadcast a message to all connected clients
604
664
  */
605
- hostStatic: (baseUrl: string, distDir: string) => void;
606
- }
607
- //#endregion
608
- //#region src/types/scope.d.ts
609
- type AnyRpcFn = (...args: any[]) => any;
610
- /**
611
- * Augmentable registry mapping a scope namespace to the shape of its
612
- * persisted settings. Tools augment it so `ctx.scope('my-plugin')`
613
- * gets a fully-typed `settings.global` / `settings.project`:
614
- *
615
- * ```ts
616
- * declare module 'devframe' {
617
- * interface DevframeSettingsRegistry {
618
- * 'my-plugin': { theme: 'light' | 'dark', recentFiles: string[] }
619
- * }
620
- * }
621
- * ```
622
- */
623
- interface DevframeSettingsRegistry {}
624
- /**
625
- * Resolve the settings shape for a namespace from
626
- * {@link DevframeSettingsRegistry}, falling back to an open record when
627
- * the namespace hasn't been augmented.
628
- */
629
- type SettingsForNamespace<NS extends string> = NS extends keyof DevframeSettingsRegistry ? DevframeSettingsRegistry[NS] extends Record<string, any> ? DevframeSettingsRegistry[NS] : Record<string, any> : Record<string, any>;
630
- /**
631
- * A persisted key-value settings store for one scope (`global` or
632
- * `project`). Backed by a file on the node side and by the shared-state
633
- * sync protocol on the client, so a `set` on either side propagates to
634
- * every connected peer and survives restarts.
635
- *
636
- * Every method is async because the underlying shared state is resolved
637
- * lazily on first access.
638
- */
639
- interface DevframeSettingsStore<T extends Record<string, any> = Record<string, any>> {
640
- /** Read a single setting. Resolves to `undefined` when unset. */
641
- get: <K extends keyof T>(key: K) => Promise<T[K] | undefined>;
642
- /** Write a single setting. */
643
- set: <K extends keyof T>(key: K, value: T[K]) => Promise<void>;
644
- /** Remove a single setting. */
645
- delete: <K extends keyof T>(key: K) => Promise<void>;
646
- /** Read the whole settings object (immutable snapshot). */
647
- all: () => Promise<Readonly<T>>;
665
+ broadcast: <T extends keyof DevframeRpcClientFunctions, Args extends Parameters<DevframeRpcClientFunctions[T]>>(options: RpcBroadcastOptions<T, Args>) => Promise<void>;
648
666
  /**
649
- * Subscribe to settings changes. Resolves to an unsubscribe function.
650
- * Fires on every local or remote mutation with the full snapshot.
667
+ * Get the current RPC client
668
+ *
669
+ * Available in RPC functions to get the current RPC client
651
670
  */
652
- onChange: (fn: (value: Readonly<T>) => void) => Promise<() => void>;
653
- }
654
- /**
655
- * The two settings scopes available on a scoped context.
656
- *
657
- * - `project` — per-workspace state, persisted under the host's
658
- * `workspace` storage dir. Project-local settings.
659
- * - `global` — per-user state, persisted under the host's `global`
660
- * storage dir. Machine-wide preferences.
661
- */
662
- interface DevframeSettings<T extends Record<string, any> = Record<string, any>> {
663
- global: DevframeSettingsStore<T>;
664
- project: DevframeSettingsStore<T>;
665
- }
666
- /**
667
- * Map the server-side RPC registry down to the bare names owned by a
668
- * namespace, so a scoped `call('get-cwd')` is typed without the
669
- * `my-plugin:` prefix.
670
- */
671
- type ScopedServerFunctions<NS extends string> = { [K in keyof DevframeRpcServerFunctions as K extends `${NS}:${infer R}` ? R : never]: DevframeRpcServerFunctions[K]; };
672
- /**
673
- * Map the client-side RPC registry down to the bare names owned by a
674
- * namespace, used by scoped `broadcast`.
675
- */
676
- type ScopedClientFunctions<NS extends string> = { [K in keyof DevframeRpcClientFunctions as K extends `${NS}:${infer R}` ? R : never]: DevframeRpcClientFunctions[K]; };
677
- /**
678
- * Map the shared-state registry down to the bare keys owned by a
679
- * namespace, so scoped `sharedState('messages')` is typed without the
680
- * `my-plugin:` prefix.
681
- */
682
- type ScopedSharedStates<NS extends string> = { [K in keyof DevframeRpcSharedStates as K extends `${NS}:${infer R}` ? R : never]: DevframeRpcSharedStates[K]; };
683
- /**
684
- * Broadcast options for a scoped client method (bare name).
685
- */
686
- interface ScopedBroadcastOptions<METHOD, Args extends any[]> {
687
- method: METHOD;
688
- args: Args;
689
- optional?: boolean;
690
- event?: boolean;
671
+ getCurrentRpcSession: () => DevframeNodeRpcSession | undefined;
672
+ /**
673
+ * The shared state host
674
+ */
675
+ sharedState: RpcSharedStateHost;
676
+ /**
677
+ * The streaming channel host. Provides per-channel `start()` /
678
+ * `pipeFrom()` producers; clients consume via `rpc.streaming.subscribe()`.
679
+ *
680
+ * @see RpcStreamingHost
681
+ */
682
+ streaming: RpcStreamingHost;
683
+ };
684
+ interface RpcSharedStateGetOptions<T> {
685
+ sharedState?: SharedState<T>;
686
+ initialValue?: T;
691
687
  }
692
- /**
693
- * Node-side streaming host scoped to a namespace. Channel names are
694
- * auto-prefixed with `<namespace>:`.
695
- */
696
- interface DevframeScopedStreamingHost {
688
+ interface RpcSharedStateHost {
689
+ get: <T extends object = any>(key: string, options?: RpcSharedStateGetOptions<T>) => Promise<SharedState<T>>;
690
+ keys: () => string[];
697
691
  /**
698
- * Register a streaming channel. The bare `name` is auto-prefixed with
699
- * the scope namespace (`<namespace>:<name>`); pass an already-qualified
700
- * name (containing `:`) to opt out.
692
+ * Subscribe to new shared-state keys becoming available. Fires when
693
+ * `get(key, ...)` creates a fresh entry (not on subsequent gets).
694
+ * Useful for protocol adapters (e.g. MCP) that surface shared state
695
+ * as dynamic resources.
701
696
  */
702
- create: <T = unknown>(name: string, opts?: RpcStreamingChannelOptions) => RpcStreamingChannel<T>;
697
+ onKeyAdded: (fn: (key: string) => void) => () => void;
703
698
  }
704
699
  /**
705
- * The scoped node RPC surface exposed on `ctx.scope('my-plugin').rpc`.
706
- * IDs and keys you pass are auto-namespaced with `my-plugin:`.
700
+ * Options for `RpcStreamingHost.create()`.
707
701
  */
708
- interface DevframeScopedNodeRpc<NS extends string = string> {
709
- /** The namespace this surface is scoped to. */
710
- readonly namespace: NS;
702
+ interface RpcStreamingChannelOptions {
711
703
  /**
712
- * Register a server RPC function. The definition's `name` must be bare
713
- * (no `:`); it is stored as `<namespace>:<name>`. Throws `DF0034` if an
714
- * already-namespaced name is passed.
704
+ * Size of the per-stream ring buffer kept on the server for
705
+ * replay-on-resubscribe. `0` (default) disables replay; on reconnect
706
+ * the consumer only sees chunks that arrive after subscribing.
707
+ *
708
+ * The buffer is per stream id, not per channel — each `channel.start()`
709
+ * gets its own.
715
710
  */
716
- register: (fn: RpcFunctionDefinition<string, any, any, any, any, any, DevframeNodeContext>, force?: boolean) => void;
711
+ replayWindow?: number;
717
712
  /**
718
- * Update a previously registered server RPC function. Same naming rules
719
- * as {@link DevframeScopedNodeRpc.register}.
713
+ * Milliseconds a closed stream is retained on the server after its
714
+ * last subscriber leaves (or if no subscriber ever arrived). During
715
+ * this window, late subscribers can still join and replay the buffer
716
+ * + receive the `end` frame.
717
+ *
718
+ * Defaults to `30_000` (30 s) when `replayWindow > 0`, else `0`
719
+ * (immediate free). Set to `0` to opt out, or higher for longer
720
+ * post-mortem replay.
720
721
  */
721
- update: (fn: RpcFunctionDefinition<string, any, any, any, any, any, DevframeNodeContext>, force?: boolean) => void;
722
+ closedStreamRetention?: number;
723
+ }
724
+ /**
725
+ * Channel handle returned by `ctx.rpc.streaming.create(name, opts)`. A
726
+ * channel owns a wire namespace; calling `start()` produces individual
727
+ * streams keyed by id.
728
+ *
729
+ * @see {@link https://devfra.me/guide/streaming Streaming guide}
730
+ */
731
+ interface RpcStreamingChannel<T = unknown> {
732
+ /** Channel name as registered with `ctx.rpc.streaming.create()`. */
733
+ readonly name: string;
722
734
  /**
723
- * Invoke a locally registered server RPC function. Bare names are
724
- * resolved within this namespace; pass a fully-qualified name
725
- * (containing `:`) to call another scope's function.
735
+ * Start a new stream. Returns a server-side sink with both an imperative
736
+ * (`write` / `close` / `error`) surface and a `WritableStream<T>` for
737
+ * `pipeTo` consumption. The sink's `signal` aborts when every subscriber
738
+ * disconnects or cancels.
726
739
  */
727
- call: {
728
- <T extends keyof ScopedServerFunctions<NS> & string>(method: T, ...args: Parameters<Extract<ScopedServerFunctions<NS>[T], AnyRpcFn>>): Promise<Awaited<ReturnType<Extract<ScopedServerFunctions<NS>[T], AnyRpcFn>>>>;
729
- <T extends keyof DevframeRpcServerFunctions & string>(method: T, ...args: Parameters<Extract<DevframeRpcServerFunctions[T], AnyRpcFn>>): Promise<Awaited<ReturnType<Extract<DevframeRpcServerFunctions[T], AnyRpcFn>>>>;
730
- (method: string, ...args: any[]): Promise<any>;
731
- };
740
+ start: (opts?: {
741
+ id?: string;
742
+ }) => StreamSink<T>;
732
743
  /**
733
- * Broadcast a client RPC event to every connected client. Bare method
734
- * names are resolved within this namespace.
744
+ * Convenience: start a stream and pipe a `ReadableStream<T>` into it.
745
+ * The pipe uses `sink.signal` so cancellation propagates upstream.
746
+ *
747
+ * Node-stream interop: convert a `Readable` with `Readable.toWeb(node)`
748
+ * before passing it here.
735
749
  */
736
- broadcast: {
737
- <T extends keyof ScopedClientFunctions<NS> & string>(options: ScopedBroadcastOptions<T, Parameters<Extract<ScopedClientFunctions<NS>[T], AnyRpcFn>>>): Promise<void>;
738
- (options: ScopedBroadcastOptions<string, any[]>): Promise<void>;
739
- };
750
+ pipeFrom: (readable: ReadableStream<T>, opts?: {
751
+ id?: string;
752
+ }) => Promise<StreamSink<T>>;
753
+ /** Look up an active stream by id. Returns `undefined` if none. */
754
+ get: (id: string) => StreamSink<T> | undefined;
755
+ /** All active outbound stream ids on this channel. */
756
+ ids: () => string[];
740
757
  /**
741
- * Resolve a namespaced shared state. Bare keys are resolved within this
742
- * namespace; pass a fully-qualified key (containing `:`) to opt out.
758
+ * Open an inbound stream — the server side of a client-to-server
759
+ * upload. Allocates an id, returns a `StreamReader<T>` that fills as
760
+ * the client writes chunks. Typical pattern is to call this from an
761
+ * action handler, kick off background processing, and return the id
762
+ * so the caller can start uploading:
763
+ *
764
+ * ```ts
765
+ * handler: async () => {
766
+ * const reader = channel.openInbound()
767
+ * ;(async () => {
768
+ * for await (const chunk of reader) processChunk(chunk)
769
+ * })()
770
+ * return { uploadId: reader.id }
771
+ * }
772
+ * ```
773
+ *
774
+ * Calling `reader.cancel()` on the server sends an `upload-cancel` to
775
+ * the uploading client, which aborts its sink.
743
776
  */
744
- sharedState: {
745
- <T extends keyof ScopedSharedStates<NS> & string>(key: T, options?: RpcSharedStateGetOptions<ScopedSharedStates<NS>[T]>): Promise<SharedState<ScopedSharedStates<NS>[T]>>;
746
- <T extends Record<string, any> = Record<string, any>>(key: string, options?: RpcSharedStateGetOptions<T>): Promise<SharedState<T>>;
747
- };
748
- /** Streaming host scoped to this namespace. */
749
- streaming: DevframeScopedStreamingHost;
750
- /** Pass-through to the current RPC session, when called inside a handler. */
751
- getCurrentRpcSession: () => DevframeNodeRpcSession | undefined;
777
+ openInbound: (opts?: {
778
+ id?: string;
779
+ }) => StreamReader<T>;
752
780
  }
753
781
  /**
754
- * A namespace-scoped view of {@link DevframeNodeContext}. Returned by
755
- * `ctx.scope('my-plugin')`. Re-exposes the unscoped surfaces (`views`,
756
- * `diagnostics`, `agent`, …), replaces `rpc` with the auto-namespaced
757
- * {@link DevframeScopedNodeRpc}, and adds a top-level `settings` store.
782
+ * Server-side streaming host. Lives on `ctx.rpc.streaming` alongside
783
+ * `ctx.rpc.sharedState`. Each named channel owns its own stream registry
784
+ * and wire namespace.
758
785
  */
759
- interface DevframeScopedNodeContext<NS extends string = string, Settings extends Record<string, any> = Record<string, any>> {
760
- /** The namespace this context is scoped to. */
761
- readonly namespace: NS;
762
- /** The underlying unscoped context. */
763
- readonly base: DevframeNodeContext;
764
- readonly workspaceRoot: string;
765
- readonly cwd: string;
766
- readonly mode: 'dev' | 'build';
767
- host: DevframeHost;
768
- rpc: DevframeScopedNodeRpc<NS>;
769
- /** Persisted settings for this namespace (`global` + `project`). */
770
- settings: DevframeSettings<Settings>;
771
- views: DevframeViewHost;
772
- diagnostics: DevframeDiagnosticsHost;
773
- agent: DevframeAgentHost;
786
+ interface RpcStreamingHost {
774
787
  /**
775
- * Return a new scoped context, replacing the current scope. Pass `null`
776
- * or `''` to un-scope and get the base context.
788
+ * Register a streaming channel. Names follow the `<plugin-id>:<channel>`
789
+ * convention (e.g. `'my-devtool:chat-stream'`). Throws `DF0032` if the
790
+ * name is already taken.
777
791
  */
778
- scope: DevframeNodeContext['scope'];
792
+ create: <T = unknown>(name: string, opts?: RpcStreamingChannelOptions) => RpcStreamingChannel<T>;
793
+ /**
794
+ * Adapters call this when a session disconnects so the host can drop
795
+ * subscribers and abort orphaned streams. Most users do not need this;
796
+ * it's wired by `startHttpAndWs` automatically.
797
+ *
798
+ * @internal
799
+ */
800
+ _onSessionDisconnected: (meta: DevframeNodeRpcSessionMeta) => void;
779
801
  }
780
802
  //#endregion
781
- //#region src/types/utils.d.ts
782
- type Thenable<T> = T | Promise<T>;
783
- type EntriesToObject<T extends readonly [string, any][]> = { [K in T[number] as K[0]]: K[1]; };
784
- type PartialWithoutId<T extends {
785
- id: string;
786
- }> = Partial<Omit<T, 'id'>> & {
787
- id: string;
788
- };
789
- //#endregion
790
- //#region src/utils/shared-state.d.ts
791
- type ImmutablePrimitive = undefined | null | boolean | string | number | Function;
792
- type Immutable<T> = T extends ImmutablePrimitive ? T : T extends Array<infer U> ? ImmutableArray<U> : T extends Map<infer K, infer V> ? ImmutableMap<K, V> : T extends Set<infer M> ? ImmutableSet<M> : ImmutableObject<T>;
793
- type ImmutableArray<T> = ReadonlyArray<Immutable<T>>;
794
- type ImmutableMap<K, V> = ReadonlyMap<Immutable<K>, Immutable<V>>;
795
- type ImmutableSet<T> = ReadonlySet<Immutable<T>>;
796
- type ImmutableObject<T> = { readonly [K in keyof T]: Immutable<T[K]>; };
803
+ //#region src/types/diagnostics.d.ts
797
804
  /**
798
- * Serializable patch describing a single mutation to a `SharedState`.
799
- * Structurally compatible with JSON-Patch and is safe to send over RPC.
805
+ * A diagnostics definition object built with `defineDiagnostics`. Typed as
806
+ * `unknown` because each integration's definition has a distinct narrow shape
807
+ * (e.g. specific code keys like `DF0001` / `MYP0001`), and TypeScript's mapped
808
+ * types don't allow assigning a narrow-keyed result to a generically-keyed
809
+ * one. The host stores them in a heterogeneous registry.
800
810
  */
801
- interface SharedStatePatch {
802
- op: 'add' | 'remove' | 'replace';
803
- path: readonly (string | number)[];
804
- value?: unknown;
811
+ type DevframeDiagnosticsDefinition = ReturnType<typeof defineDiagnostics<any, any>>;
812
+ /**
813
+ * The shared diagnostics lookup exposed by the host. A `Proxy` that resolves
814
+ * any registered code name to its `nostics` handle (a callable that builds
815
+ * a diagnostic and routes it through registered reporters). Typed loosely
816
+ * because it spans heterogeneous definitions registered by different
817
+ * integrations.
818
+ */
819
+ type DevframeDiagnosticsLogger = Record<string, any>;
820
+ /**
821
+ * Options accepted by the host's `defineDiagnostics()` factory — mirrors
822
+ * `nostics`'s shape but the host pre-wires its ANSI console reporter, so
823
+ * plugins typically omit `reporters`.
824
+ */
825
+ interface DevframeDefineDiagnosticsOptions<Codes extends Record<string, DiagnosticDefinition>> {
826
+ docsBase?: string | ((code: keyof Codes) => string | undefined);
827
+ codes: Codes;
828
+ reporters?: ReadonlyArray<(d: Diagnostic, o?: any) => void>;
805
829
  }
806
830
  /**
807
- * State host that is immutable by default with explicit mutate.
831
+ * Host for structured diagnostics — a thin layer over `nostics` that lets
832
+ * integrations register their own coded errors/warnings into a shared
833
+ * registry without taking a direct dependency on `nostics`.
834
+ *
835
+ * Typical usage from a plugin's `setup(ctx)`:
836
+ *
837
+ * ```ts
838
+ * const myDiagnostics = ctx.diagnostics.defineDiagnostics({
839
+ * docsBase: 'https://example.com/errors',
840
+ * codes: {
841
+ * MYP0001: { why: 'Something went wrong' },
842
+ * },
843
+ * })
844
+ * ctx.diagnostics.register(myDiagnostics)
845
+ *
846
+ * // Through the shared lookup (loose typing):
847
+ * throw ctx.diagnostics.logger.MYP0001()
848
+ *
849
+ * // Or directly on the typed handle returned from `defineDiagnostics`:
850
+ * throw myDiagnostics.MYP0001()
851
+ * ```
808
852
  */
809
- interface SharedState<T> {
853
+ interface DevframeDiagnosticsHost {
810
854
  /**
811
- * Get the current state. Immutable.
855
+ * Proxy-backed lookup of every registered diagnostic handle by code name.
856
+ * Resolves to a `nostics` `DiagnosticHandle` — a callable that builds a
857
+ * diagnostic and routes it through registered reporters; prefix with
858
+ * `throw` to raise. Loosely typed — for autocompletion, keep a reference
859
+ * to the typed result of `defineDiagnostics()` instead.
812
860
  */
813
- value: () => Immutable<T>;
861
+ readonly logger: DevframeDiagnosticsLogger;
814
862
  /**
815
- * Subscribe to state changes.
863
+ * Register additional diagnostic definitions with this host. After
864
+ * registration, codes from the new definition are reachable via
865
+ * `host.logger.CODE`. Plugins that want shared output formatting should
866
+ * build their diagnostics via `host.defineDiagnostics()` first — that
867
+ * factory pre-wires the host's ANSI console reporter.
816
868
  */
817
- on: EventEmitter<SharedStateEvents<T>>['on'];
869
+ register: (definitions: Record<string, unknown>) => void;
818
870
  /**
819
- * Mutate the state.
871
+ * Build a typed diagnostics object with the host's ANSI console reporter
872
+ * pre-wired. Mirrors `nostics`'s `defineDiagnostics` so integrations don't
873
+ * need to take a direct dependency on `nostics`.
820
874
  */
821
- mutate: (fn: (state: T) => void, syncId?: string) => void;
875
+ defineDiagnostics: <const Codes extends Record<string, DiagnosticDefinition>>(options: DevframeDefineDiagnosticsOptions<Codes>) => ReturnType<typeof defineDiagnostics<Codes, any>>;
876
+ }
877
+ //#endregion
878
+ //#region src/types/host.d.ts
879
+ interface DevframeHost {
822
880
  /**
823
- * Apply patches to the state.
881
+ * Serve a static directory at the given URL base. Called by
882
+ * `DevframeViewHost.hostStatic`. Implementations map this to whatever
883
+ * the underlying runtime expects (Vite middleware, h3 handler, no-op
884
+ * for build snapshots).
824
885
  */
825
- patch: (patches: SharedStatePatch[], syncId?: string) => void;
886
+ mountStatic: (base: string, distDir: string) => void | Promise<void>;
826
887
  /**
827
- * Sync IDs that have been applied to the state.
888
+ * Serve the host's connection meta (`__connection.json`) at the given URL
889
+ * base, so a devframe SPA mounted there can discover the RPC/WS endpoint
890
+ * via `connectDevframe()`'s relative `./__connection.json` fetch.
891
+ *
892
+ * Called by `mountDevframe` for each mounted devframe (alongside
893
+ * `mountStatic`). Without it, an embedded SPA can only discover the
894
+ * endpoint by inheriting it from a same-origin parent window — which fails
895
+ * for cross-origin or sandboxed iframes. Implementations serve the same
896
+ * meta they expose at the hub's own base.
897
+ *
898
+ * Optional in the type, but a host that mounts a devframe with a servable
899
+ * `distDir` yet omits this hook triggers a `DF8106` diagnostic, since the
900
+ * SPA's `./__connection.json` fetch would otherwise fall through and break
901
+ * silently. A static-snapshot host that bakes the meta into its served files
902
+ * can implement it as a no-op to acknowledge this intentionally.
828
903
  */
829
- syncIds: Set<string>;
830
- }
831
- interface SharedStateEvents<T> {
832
- updated: (fullState: T, patches: SharedStatePatch[] | undefined, syncId: string) => void;
833
- }
834
- interface SharedStateOptions<T> {
904
+ mountConnectionMeta?: (base: string) => void | Promise<void>;
835
905
  /**
836
- * Initial state.
906
+ * Return the public origin the host is reachable at, e.g.
907
+ * `http://localhost:5173`. Used by the dock host to enrich remote
908
+ * iframe URLs with a full `origin`. Called only when a dock needs an
909
+ * absolute URL; hosts that never serve remote docks can return any
910
+ * reasonable value.
837
911
  */
838
- initialValue: T;
912
+ resolveOrigin: () => string;
839
913
  /**
840
- * Enable patches.
914
+ * Resolve a directory the host owns for persisted devframe state.
915
+ * Each host picks its own app-name namespace so storage doesn't
916
+ * collide between, say, the Vite host (`.vite/devframe`) and a
917
+ * standalone CLI host (`.<appName>/devframe`).
841
918
  *
842
- * @default false
843
- */
844
- enablePatches?: boolean;
845
- }
846
- declare function createSharedState<T extends object>(options: SharedStateOptions<T>): SharedState<T>;
847
- //#endregion
848
- //#region src/utils/streaming-channel.d.ts
849
- /**
850
- * Serialized error shape sent over the wire when a stream ends with a failure.
851
- * Stays JSON-safe so the strict-JSON encoder can carry it without coercion.
852
- */
853
- interface StreamErrorPayload {
854
- name: string;
855
- message: string;
856
- }
857
- /**
858
- * Single buffered chunk in the server-side ring buffer.
859
- *
860
- * Sequence numbers start at 1 and increment per write. Subscribers track
861
- * `lastSeenSeq` and ask for `afterSeq` on resubscribe so the server can
862
- * replay any chunks the client missed during a brief disconnect.
863
- */
864
- interface BufferedChunk<T> {
865
- seq: number;
866
- chunk: T;
867
- }
868
- interface StreamSinkEvents<T> {
869
- /** Fired for each `write()`. The RPC layer subscribes and broadcasts. */
870
- chunk: (seq: number, chunk: T) => void;
871
- /** Terminal — fired exactly once per sink lifetime. */
872
- end: (error?: StreamErrorPayload) => void;
873
- }
874
- interface CreateStreamSinkOptions {
875
- id?: string;
876
- /**
877
- * Size of the per-stream ring buffer kept for replay-on-resubscribe.
878
- * `0` (default) disables replay.
919
+ * - `workspace` — state shared with the whole team through version
920
+ * control (saved queries, shared presets). Conventionally
921
+ * `${workspaceRoot}/.devframe/`; hosts must place it somewhere
922
+ * committable.
923
+ * - `project` — per-checkout private state (caches, personal
924
+ * settings). Typically under
925
+ * `${cwd}/node_modules/.<appName>/devframe/`, which version
926
+ * control ignores.
927
+ * - `global` — per-user state (auth tokens, machine-wide
928
+ * preferences). Typically under
929
+ * `${homedir()}/.<appName>/devframe/`.
930
+ *
931
+ * Implementations should ensure the directory exists or be safe to
932
+ * pass to a downstream `createStorage(...)` call that creates it
933
+ * lazily.
879
934
  */
880
- replayWindow?: number;
935
+ getStorageDir: (scope: DevframeStorageScope) => string;
881
936
  }
882
937
  /**
883
- * Server-side producer handle. Two equivalent surfaces share one piece of
884
- * state: the imperative `write/error/close` triple, and a `WritableStream<T>`
885
- * for `pipeTo`-style consumption.
938
+ * Storage placement classes for {@link DevframeHost.getStorageDir}:
939
+ * `workspace` is committable and team-shared, `project` is per-checkout
940
+ * private, `global` is per-user.
886
941
  */
887
- interface StreamSink<T> {
888
- /** Stable id used by clients to subscribe. */
889
- readonly id: string;
890
- /**
891
- * Aborts when the consumer cancels (server-side) or when the transport
892
- * loses every subscriber. Producers should poll `signal.aborted` and exit
893
- * cleanly.
894
- */
895
- readonly signal: AbortSignal;
896
- /** `true` after `close()` / `error()`. Further writes throw. */
897
- readonly closed: boolean;
898
- /** Last allocated sequence number. `0` until the first write. */
899
- readonly lastSeq: number;
900
- write: (chunk: T) => void;
901
- error: (reason: unknown) => void;
902
- close: () => void;
903
- /** External-cancel path. Aborts the signal so handlers can short-circuit. */
904
- abort: (reason?: unknown) => void;
905
- /** `WritableStream<T>` adapter — same in-memory state as the imperative API. */
906
- readonly writable: WritableStream<T>;
942
+ type DevframeStorageScope = 'workspace' | 'project' | 'global';
943
+ //#endregion
944
+ //#region src/types/views.d.ts
945
+ interface DevframeViewHost {
907
946
  /**
908
- * Internal — RPC layer subscribes to receive chunk/end notifications.
909
- * Not part of the public contract; do not call directly.
910
- *
911
947
  * @internal
912
948
  */
913
- readonly events: EventEmitter<StreamSinkEvents<T>>;
949
+ buildStaticDirs: {
950
+ baseUrl: string;
951
+ distDir: string;
952
+ }[];
914
953
  /**
915
- * Internal — replay buffer. RPC layer reads on (re)subscribe to feed
916
- * missed chunks before going live.
917
- *
918
- * @internal
954
+ * Helper to host static files
955
+ * - In `dev` mode, it will register middleware to `viteServer.middlewares` to host the static files
956
+ * - In `build` mode, it will copy the static files to the dist directory
919
957
  */
920
- readonly buffer: ReadonlyArray<BufferedChunk<T>>;
958
+ hostStatic: (baseUrl: string, distDir: string) => void;
921
959
  }
922
- interface CreateStreamReaderOptions {
923
- id?: string;
924
- /**
925
- * Maximum number of buffered chunks held client-side while the consumer
926
- * isn't draining. On overflow, the oldest chunk is dropped.
927
- */
928
- highWaterMark?: number;
960
+ //#endregion
961
+ //#region src/types/scope.d.ts
962
+ type AnyRpcFn = (...args: any[]) => any;
963
+ /**
964
+ * Augmentable registry mapping a scope namespace to the shape of its
965
+ * persisted settings. Tools augment it so `ctx.scope('my-plugin')`
966
+ * gets a fully-typed `settings.global` / `settings.project`:
967
+ *
968
+ * ```ts
969
+ * declare module 'devframe' {
970
+ * interface DevframeSettingsRegistry {
971
+ * 'my-plugin': { theme: 'light' | 'dark', recentFiles: string[] }
972
+ * }
973
+ * }
974
+ * ```
975
+ */
976
+ interface DevframeSettingsRegistry {}
977
+ /**
978
+ * Resolve the settings shape for a namespace from
979
+ * {@link DevframeSettingsRegistry}, falling back to an open record when
980
+ * the namespace hasn't been augmented.
981
+ */
982
+ type SettingsForNamespace<NS extends string> = NS extends keyof DevframeSettingsRegistry ? DevframeSettingsRegistry[NS] extends Record<string, any> ? DevframeSettingsRegistry[NS] : Record<string, any> : Record<string, any>;
983
+ /**
984
+ * A persisted key-value settings store for one scope (`global` or
985
+ * `project`). Backed by a file on the node side and by the shared-state
986
+ * sync protocol on the client, so a `set` on either side propagates to
987
+ * every connected peer and survives restarts.
988
+ *
989
+ * Every method is async because the underlying shared state is resolved
990
+ * lazily on first access.
991
+ */
992
+ interface DevframeSettingsStore<T extends Record<string, any> = Record<string, any>> {
993
+ /** Read a single setting. Resolves to `undefined` when unset. */
994
+ get: <K extends keyof T>(key: K) => Promise<T[K] | undefined>;
995
+ /** Write a single setting. */
996
+ set: <K extends keyof T>(key: K, value: T[K]) => Promise<void>;
997
+ /** Remove a single setting. */
998
+ delete: <K extends keyof T>(key: K) => Promise<void>;
999
+ /** Read the whole settings object (immutable snapshot). */
1000
+ all: () => Promise<Readonly<T>>;
929
1001
  /**
930
- * Called when the chunk queue overflows the high-water mark. The RPC
931
- * layer wires this to a coded warning; the primitive itself is
932
- * RPC-agnostic.
1002
+ * Subscribe to settings changes. Resolves to an unsubscribe function.
1003
+ * Fires on every local or remote mutation with the full snapshot.
933
1004
  */
934
- onOverflow?: (dropped: number) => void;
935
- /** Called when the consumer cancels — the RPC layer sends `:cancel` upstream. */
936
- onCancel?: () => void;
1005
+ onChange: (fn: (value: Readonly<T>) => void) => Promise<() => void>;
937
1006
  }
938
1007
  /**
939
- * Client-side consumer handle. Both an `AsyncIterable<T>` (for `for await`)
940
- * and exposes `readable: ReadableStream<T>` (for `pipeTo`). Pick one — they
941
- * share a single internal queue, so concurrent draining will race.
1008
+ * The two settings scopes available on a scoped context.
1009
+ *
1010
+ * - `project` — per-workspace state, persisted under the host's
1011
+ * `workspace` storage dir. Project-local settings.
1012
+ * - `global` — per-user state, persisted under the host's `global`
1013
+ * storage dir. Machine-wide preferences.
942
1014
  */
943
- interface StreamReader<T> extends AsyncIterable<T> {
944
- readonly id: string;
945
- readonly cancelled: boolean;
946
- readonly done: boolean;
947
- /** Highest `seq` observed. Used for replay on reconnect. */
948
- readonly lastSeenSeq: number;
949
- /** `ReadableStream<T>` adapter for `pipeTo`-style consumption. */
950
- readonly readable: ReadableStream<T>;
951
- cancel: () => void;
952
- /** @internal */
953
- _push: (seq: number, chunk: T) => void;
954
- /** @internal */
955
- _end: (error?: StreamErrorPayload) => void;
1015
+ interface DevframeSettings<T extends Record<string, any> = Record<string, any>> {
1016
+ global: DevframeSettingsStore<T>;
1017
+ project: DevframeSettingsStore<T>;
956
1018
  }
957
1019
  /**
958
- * Build a server-side stream sink. RPC-agnostic — the RPC host wires
959
- * `events.on('chunk' | 'end')` to broadcast, and reads `buffer` to replay
960
- * for late or reconnecting subscribers.
1020
+ * Map the server-side RPC registry down to the bare names owned by a
1021
+ * namespace, so a scoped `call('get-cwd')` is typed without the
1022
+ * `my-plugin:` prefix.
1023
+ */
1024
+ type ScopedServerFunctions<NS extends string> = { [K in keyof DevframeRpcServerFunctions as K extends `${NS}:${infer R}` ? R : never]: DevframeRpcServerFunctions[K]; };
1025
+ /**
1026
+ * Map the client-side RPC registry down to the bare names owned by a
1027
+ * namespace, used by scoped `broadcast`.
1028
+ */
1029
+ type ScopedClientFunctions<NS extends string> = { [K in keyof DevframeRpcClientFunctions as K extends `${NS}:${infer R}` ? R : never]: DevframeRpcClientFunctions[K]; };
1030
+ /**
1031
+ * Map the shared-state registry down to the bare keys owned by a
1032
+ * namespace, so scoped `sharedState('messages')` is typed without the
1033
+ * `my-plugin:` prefix.
961
1034
  */
962
- declare function createStreamSink<T>(options?: CreateStreamSinkOptions): StreamSink<T>;
1035
+ type ScopedSharedStates<NS extends string> = { [K in keyof DevframeRpcSharedStates as K extends `${NS}:${infer R}` ? R : never]: DevframeRpcSharedStates[K]; };
963
1036
  /**
964
- * Build a client-side stream reader. RPC-agnostic — the RPC host calls
965
- * `_push(seq, chunk)` on each incoming chunk and `_end(error?)` on the
966
- * terminal frame. Consumers iterate with `for await` or pipe `readable`.
1037
+ * Broadcast options for a scoped client method (bare name).
967
1038
  */
968
- declare function createStreamReader<T>(options?: CreateStreamReaderOptions): StreamReader<T>;
969
- //#endregion
970
- //#region src/types/rpc.d.ts
971
- interface DevframeNodeRpcSession {
972
- meta: DevframeNodeRpcSessionMeta;
973
- rpc: BirpcReturn<DevframeRpcClientFunctions, DevframeRpcServerFunctions, false>;
974
- }
975
- interface RpcBroadcastOptions<METHOD, Args extends any[]> {
1039
+ interface ScopedBroadcastOptions<METHOD, Args extends any[]> {
976
1040
  method: METHOD;
977
1041
  args: Args;
978
1042
  optional?: boolean;
979
1043
  event?: boolean;
980
- filter?: (client: BirpcReturn<DevframeRpcClientFunctions, DevframeRpcServerFunctions, false>) => boolean | void;
981
1044
  }
982
- type RpcFunctionsHost = RpcFunctionsCollectorBase<DevframeRpcServerFunctions, DevframeNodeContext> & {
1045
+ /**
1046
+ * Node-side streaming host scoped to a namespace. Channel names are
1047
+ * auto-prefixed with `<namespace>:`.
1048
+ */
1049
+ interface DevframeScopedStreamingHost {
983
1050
  /**
984
- * Invoke a locally registered server RPC function directly.
985
- *
986
- * This bypasses transport and is useful for server-side cross-function calls.
1051
+ * Register a streaming channel. The bare `name` is auto-prefixed with
1052
+ * the scope namespace (`<namespace>:<name>`); pass an already-qualified
1053
+ * name (containing `:`) to opt out.
987
1054
  */
988
- invokeLocal: <T extends keyof DevframeRpcServerFunctions, Args extends Parameters<DevframeRpcServerFunctions[T]>>(method: T, ...args: Args) => Promise<Awaited<ReturnType<DevframeRpcServerFunctions[T]>>>;
1055
+ create: <T = unknown>(name: string, opts?: RpcStreamingChannelOptions) => RpcStreamingChannel<T>;
1056
+ }
1057
+ /**
1058
+ * The scoped node RPC surface exposed on `ctx.scope('my-plugin').rpc`.
1059
+ * IDs and keys you pass are auto-namespaced with `my-plugin:`.
1060
+ */
1061
+ interface DevframeScopedNodeRpc<NS extends string = string> {
1062
+ /** The namespace this surface is scoped to. */
1063
+ readonly namespace: NS;
989
1064
  /**
990
- * Broadcast a message to all connected clients
1065
+ * Register a server RPC function. The definition's `name` must be bare
1066
+ * (no `:`); it is stored as `<namespace>:<name>`. Throws `DF0034` if an
1067
+ * already-namespaced name is passed.
991
1068
  */
992
- broadcast: <T extends keyof DevframeRpcClientFunctions, Args extends Parameters<DevframeRpcClientFunctions[T]>>(options: RpcBroadcastOptions<T, Args>) => Promise<void>;
1069
+ register: (fn: RpcFunctionDefinition<string, any, any, any, any, any, DevframeNodeContext>, force?: boolean) => void;
993
1070
  /**
994
- * Get the current RPC client
995
- *
996
- * Available in RPC functions to get the current RPC client
1071
+ * Update a previously registered server RPC function. Same naming rules
1072
+ * as {@link DevframeScopedNodeRpc.register}.
997
1073
  */
998
- getCurrentRpcSession: () => DevframeNodeRpcSession | undefined;
1074
+ update: (fn: RpcFunctionDefinition<string, any, any, any, any, any, DevframeNodeContext>, force?: boolean) => void;
999
1075
  /**
1000
- * The shared state host
1076
+ * Invoke a locally registered server RPC function. Bare names are
1077
+ * resolved within this namespace; pass a fully-qualified name
1078
+ * (containing `:`) to call another scope's function.
1001
1079
  */
1002
- sharedState: RpcSharedStateHost;
1080
+ call: {
1081
+ <T extends keyof ScopedServerFunctions<NS> & string>(method: T, ...args: Parameters<Extract<ScopedServerFunctions<NS>[T], AnyRpcFn>>): Promise<Awaited<ReturnType<Extract<ScopedServerFunctions<NS>[T], AnyRpcFn>>>>;
1082
+ <T extends keyof DevframeRpcServerFunctions & string>(method: T, ...args: Parameters<Extract<DevframeRpcServerFunctions[T], AnyRpcFn>>): Promise<Awaited<ReturnType<Extract<DevframeRpcServerFunctions[T], AnyRpcFn>>>>;
1083
+ (method: string, ...args: any[]): Promise<any>;
1084
+ };
1003
1085
  /**
1004
- * The streaming channel host. Provides per-channel `start()` /
1005
- * `pipeFrom()` producers; clients consume via `rpc.streaming.subscribe()`.
1006
- *
1007
- * @see RpcStreamingHost
1086
+ * Broadcast a client RPC event to every connected client. Bare method
1087
+ * names are resolved within this namespace.
1008
1088
  */
1009
- streaming: RpcStreamingHost;
1010
- };
1011
- interface RpcSharedStateGetOptions<T> {
1012
- sharedState?: SharedState<T>;
1013
- initialValue?: T;
1014
- }
1015
- interface RpcSharedStateHost {
1016
- get: <T extends object = any>(key: string, options?: RpcSharedStateGetOptions<T>) => Promise<SharedState<T>>;
1017
- keys: () => string[];
1089
+ broadcast: {
1090
+ <T extends keyof ScopedClientFunctions<NS> & string>(options: ScopedBroadcastOptions<T, Parameters<Extract<ScopedClientFunctions<NS>[T], AnyRpcFn>>>): Promise<void>;
1091
+ (options: ScopedBroadcastOptions<string, any[]>): Promise<void>;
1092
+ };
1018
1093
  /**
1019
- * Subscribe to new shared-state keys becoming available. Fires when
1020
- * `get(key, ...)` creates a fresh entry (not on subsequent gets).
1021
- * Useful for protocol adapters (e.g. MCP) that surface shared state
1022
- * as dynamic resources.
1094
+ * Resolve a namespaced shared state. Bare keys are resolved within this
1095
+ * namespace; pass a fully-qualified key (containing `:`) to opt out.
1023
1096
  */
1024
- onKeyAdded: (fn: (key: string) => void) => () => void;
1097
+ sharedState: {
1098
+ <T extends keyof ScopedSharedStates<NS> & string>(key: T, options?: RpcSharedStateGetOptions<ScopedSharedStates<NS>[T]>): Promise<SharedState<ScopedSharedStates<NS>[T]>>;
1099
+ <T extends Record<string, any> = Record<string, any>>(key: string, options?: RpcSharedStateGetOptions<T>): Promise<SharedState<T>>;
1100
+ };
1101
+ /** Streaming host scoped to this namespace. */
1102
+ streaming: DevframeScopedStreamingHost;
1103
+ /** Pass-through to the current RPC session, when called inside a handler. */
1104
+ getCurrentRpcSession: () => DevframeNodeRpcSession | undefined;
1025
1105
  }
1026
1106
  /**
1027
- * Options for `RpcStreamingHost.create()`.
1107
+ * A namespace-scoped view of {@link DevframeNodeContext}. Returned by
1108
+ * `ctx.scope('my-plugin')`. Re-exposes the unscoped surfaces (`views`,
1109
+ * `diagnostics`, `agent`, …), replaces `rpc` with the auto-namespaced
1110
+ * {@link DevframeScopedNodeRpc}, and adds a top-level `settings` store.
1028
1111
  */
1029
- interface RpcStreamingChannelOptions {
1030
- /**
1031
- * Size of the per-stream ring buffer kept on the server for
1032
- * replay-on-resubscribe. `0` (default) disables replay; on reconnect
1033
- * the consumer only sees chunks that arrive after subscribing.
1034
- *
1035
- * The buffer is per stream id, not per channel — each `channel.start()`
1036
- * gets its own.
1037
- */
1038
- replayWindow?: number;
1112
+ interface DevframeScopedNodeContext<NS extends string = string, Settings extends Record<string, any> = Record<string, any>> {
1113
+ /** The namespace this context is scoped to. */
1114
+ readonly namespace: NS;
1115
+ /** The underlying unscoped context. */
1116
+ readonly base: DevframeNodeContext;
1117
+ readonly workspaceRoot: string;
1118
+ readonly cwd: string;
1119
+ readonly mode: 'dev' | 'build';
1120
+ host: DevframeHost;
1121
+ rpc: DevframeScopedNodeRpc<NS>;
1122
+ /** Persisted settings for this namespace (`global` + `project`). */
1123
+ settings: DevframeSettings<Settings>;
1124
+ views: DevframeViewHost;
1125
+ diagnostics: DevframeDiagnosticsHost;
1126
+ agent: DevframeAgentHost;
1039
1127
  /**
1040
- * Milliseconds a closed stream is retained on the server after its
1041
- * last subscriber leaves (or if no subscriber ever arrived). During
1042
- * this window, late subscribers can still join and replay the buffer
1043
- * + receive the `end` frame.
1044
- *
1045
- * Defaults to `30_000` (30 s) when `replayWindow > 0`, else `0`
1046
- * (immediate free). Set to `0` to opt out, or higher for longer
1047
- * post-mortem replay.
1128
+ * Return a new scoped context, replacing the current scope. Pass `null`
1129
+ * or `''` to un-scope and get the base context.
1048
1130
  */
1049
- closedStreamRetention?: number;
1131
+ scope: DevframeNodeContext['scope'];
1050
1132
  }
1133
+ //#endregion
1134
+ //#region src/types/services.d.ts
1051
1135
  /**
1052
- * Channel handle returned by `ctx.rpc.streaming.create(name, opts)`. A
1053
- * channel owns a wire namespace; calling `start()` produces individual
1054
- * streams keyed by id.
1136
+ * Cross-plugin services — a typed, namespaced registry on the shared node
1137
+ * context through which one integration exposes a capability and others
1138
+ * consume it without a hard package dependency.
1055
1139
  *
1056
- * @see {@link https://devfra.me/guide/streaming Streaming guide}
1140
+ * A provider ships a *types-only* augmentation of {@link DevframeServicesRegistry}
1141
+ * (so consumers get full typing from `import type`), then provides the
1142
+ * implementation at setup time:
1143
+ *
1144
+ * ```ts
1145
+ * // provider (e.g. the data-inspector plugin)
1146
+ * declare module 'devframe' {
1147
+ * interface DevframeServicesRegistry {
1148
+ * 'devframes:plugin:data-inspector:sources': DataSourcesService
1149
+ * }
1150
+ * }
1151
+ * ctx.services.provide('devframes:plugin:data-inspector:sources', host)
1152
+ *
1153
+ * // consumer (another plugin) — no runtime dependency on the provider
1154
+ * ctx.services.whenAvailable('devframes:plugin:data-inspector:sources', (sources) => {
1155
+ * sources.register({ id: 'my-plugin:state', title: 'My state', data: () => state })
1156
+ * })
1157
+ * ```
1158
+ *
1159
+ * Service ids follow the same namespacing rule as RPC functions: prefix with
1160
+ * the providing plugin's id.
1057
1161
  */
1058
- interface RpcStreamingChannel<T = unknown> {
1059
- /** Channel name as registered with `ctx.rpc.streaming.create()`. */
1060
- readonly name: string;
1061
- /**
1062
- * Start a new stream. Returns a server-side sink with both an imperative
1063
- * (`write` / `close` / `error`) surface and a `WritableStream<T>` for
1064
- * `pipeTo` consumption. The sink's `signal` aborts when every subscriber
1065
- * disconnects or cancels.
1066
- */
1067
- start: (opts?: {
1068
- id?: string;
1069
- }) => StreamSink<T>;
1070
- /**
1071
- * Convenience: start a stream and pipe a `ReadableStream<T>` into it.
1072
- * The pipe uses `sink.signal` so cancellation propagates upstream.
1073
- *
1074
- * Node-stream interop: convert a `Readable` with `Readable.toWeb(node)`
1075
- * before passing it here.
1076
- */
1077
- pipeFrom: (readable: ReadableStream<T>, opts?: {
1078
- id?: string;
1079
- }) => Promise<StreamSink<T>>;
1080
- /** Look up an active stream by id. Returns `undefined` if none. */
1081
- get: (id: string) => StreamSink<T> | undefined;
1082
- /** All active outbound stream ids on this channel. */
1083
- ids: () => string[];
1084
- /**
1085
- * Open an inbound stream — the server side of a client-to-server
1086
- * upload. Allocates an id, returns a `StreamReader<T>` that fills as
1087
- * the client writes chunks. Typical pattern is to call this from an
1088
- * action handler, kick off background processing, and return the id
1089
- * so the caller can start uploading:
1090
- *
1091
- * ```ts
1092
- * handler: async () => {
1093
- * const reader = channel.openInbound()
1094
- * ;(async () => {
1095
- * for await (const chunk of reader) processChunk(chunk)
1096
- * })()
1097
- * return { uploadId: reader.id }
1098
- * }
1099
- * ```
1100
- *
1101
- * Calling `reader.cancel()` on the server sends an `upload-cancel` to
1102
- * the uploading client, which aborts its sink.
1103
- */
1104
- openInbound: (opts?: {
1105
- id?: string;
1106
- }) => StreamReader<T>;
1107
- }
1108
1162
  /**
1109
- * Server-side streaming host. Lives on `ctx.rpc.streaming` alongside
1110
- * `ctx.rpc.sharedState`. Each named channel owns its own stream registry
1111
- * and wire namespace.
1163
+ * Augmentation point mapping service ids to their implementation types.
1164
+ * Providers extend this via `declare module 'devframe'`.
1112
1165
  */
1113
- interface RpcStreamingHost {
1114
- /**
1115
- * Register a streaming channel. Names follow the `<plugin-id>:<channel>`
1116
- * convention (e.g. `'my-devtool:chat-stream'`). Throws `DF0032` if the
1117
- * name is already taken.
1118
- */
1119
- create: <T = unknown>(name: string, opts?: RpcStreamingChannelOptions) => RpcStreamingChannel<T>;
1120
- /**
1121
- * Adapters call this when a session disconnects so the host can drop
1122
- * subscribers and abort orphaned streams. Most users do not need this;
1123
- * it's wired by `startHttpAndWs` automatically.
1124
- *
1125
- * @internal
1126
- */
1127
- _onSessionDisconnected: (meta: DevframeNodeRpcSessionMeta) => void;
1166
+ interface DevframeServicesRegistry {}
1167
+ /**
1168
+ * A known (augmented) service id, or any namespaced string for services
1169
+ * without a published type augmentation.
1170
+ */
1171
+ type DevframeServiceId = keyof DevframeServicesRegistry | (string & {});
1172
+ /** Resolved service type for an id: augmented type, or `unknown`. */
1173
+ type DevframeServiceOf<ID> = ID extends keyof DevframeServicesRegistry ? DevframeServicesRegistry[ID] : unknown;
1174
+ interface DevframeServicesHost {
1175
+ /**
1176
+ * Publish a service under a namespaced id. Throws `DF0037` when the id is
1177
+ * already provided (revoke first to replace). Returns a revoke function.
1178
+ */
1179
+ provide: <ID extends DevframeServiceId>(id: ID, service: DevframeServiceOf<ID>) => () => void;
1180
+ /** The service currently provided under `id`, or `undefined`. */
1181
+ get: <ID extends DevframeServiceId>(id: ID) => DevframeServiceOf<ID> | undefined;
1182
+ has: (id: DevframeServiceId) => boolean;
1183
+ /**
1184
+ * Run `callback` with the service as soon as it is available — immediately
1185
+ * when already provided, otherwise on `provide`. Survives provider/consumer
1186
+ * setup-order differences. The callback also re-fires if the service is
1187
+ * revoked and provided again. Returns an unsubscribe function.
1188
+ */
1189
+ whenAvailable: <ID extends DevframeServiceId>(id: ID, callback: (service: DevframeServiceOf<ID>) => void) => () => void;
1190
+ /** Ids of every currently-provided service. */
1191
+ keys: () => string[];
1128
1192
  }
1129
1193
  //#endregion
1130
1194
  //#region src/types/context.d.ts
@@ -1175,6 +1239,15 @@ interface DevframeNodeContext {
1175
1239
  * @experimental
1176
1240
  */
1177
1241
  agent: DevframeAgentHost;
1242
+ /**
1243
+ * Cross-plugin services — a typed, namespaced registry through which one
1244
+ * integration exposes a capability (e.g. a data-source registry) and
1245
+ * others consume it without a hard package dependency. Ids follow the RPC
1246
+ * namespacing rule (`<plugin-id>:<service>`); types come from augmenting
1247
+ * the `DevframeServicesRegistry` interface. `whenAvailable` subscriptions
1248
+ * absorb setup-order differences between provider and consumer.
1249
+ */
1250
+ services: DevframeServicesHost;
1178
1251
  /**
1179
1252
  * Create a namespace-scoped view of this context. The returned
1180
1253
  * `ctx.scope('my-plugin')` auto-namespaces every RPC id, shared-state
@@ -1252,6 +1325,71 @@ interface ConnectionMeta {
1252
1325
  * endpoint rather than resolving the path against its own mount.
1253
1326
  */
1254
1327
  baseUrl?: string;
1328
+ /**
1329
+ * A pre-issued bearer token embedded in the meta so a client trusts the
1330
+ * server on connect without any prompt or `localStorage` lookup. Only a
1331
+ * **hub** serving a **per-frame** connection meta populates this — it
1332
+ * authenticates once at the top level and bakes the resulting token into
1333
+ * the meta each plugin iframe fetches, so a cross-origin frame (which
1334
+ * cannot read the hub's `localStorage`) is still pre-authorized. The
1335
+ * standalone `__connection.json` never carries a token.
1336
+ *
1337
+ * > [!WARNING]
1338
+ * > A token in a fetchable JSON is only as protected as the URL serving
1339
+ * > it. Emit it exclusively from hub-controlled, per-frame meta — never
1340
+ * > from a publicly reachable static `__connection.json`.
1341
+ */
1342
+ authToken?: string;
1343
+ }
1344
+ //#endregion
1345
+ //#region src/types/utils.d.ts
1346
+ type Thenable<T> = T | Promise<T>;
1347
+ type EntriesToObject<T extends readonly [string, any][]> = { [K in T[number] as K[0]]: K[1]; };
1348
+ type PartialWithoutId<T extends {
1349
+ id: string;
1350
+ }> = Partial<Omit<T, 'id'>> & {
1351
+ id: string;
1352
+ };
1353
+ //#endregion
1354
+ //#region src/node/auth/handler.d.ts
1355
+ /**
1356
+ * A ready-made pre-auth RPC handler, as produced by
1357
+ * `devframe/recipes/interactive-auth`'s `createInteractiveAuth`. Bundles
1358
+ * everything a host adapter needs to wire an authenticated server:
1359
+ * the handshake RPC functions, the resolver gate, the connect-time trust
1360
+ * hook, and the startup banner.
1361
+ *
1362
+ * `startHttpAndWs` accepts one of these directly via its `auth` option —
1363
+ * see {@link https://devfra.me | devframe}'s server docs — or a host can
1364
+ * wire the four pieces itself against a lower-level transport.
1365
+ */
1366
+ interface DevframeAuthHandler {
1367
+ /**
1368
+ * `anonymous:devframe:auth` + `anonymous:devframe:auth:exchange` (the
1369
+ * handshake) and `devframe:auth:revoke` (self-revoke) — register these on
1370
+ * the RPC host (e.g. `rpcHost.register(fn)` for each).
1371
+ */
1372
+ rpcFunctions: RpcFunctionDefinitionAny[];
1373
+ /**
1374
+ * Resolver gate: whether `methodName` is callable given `session`'s
1375
+ * current trust state. Defaults to allowing any `anonymous:`-prefixed
1376
+ * method (see `isAnonymousRpcMethod`) plus anything once the session is
1377
+ * trusted.
1378
+ */
1379
+ authorize: (methodName: string, session: DevframeNodeRpcSession) => boolean;
1380
+ /**
1381
+ * Connect-time trust: reads a bearer token off the peer's upgrade request
1382
+ * (an `Authorization: Bearer <token>` header, or a static/pre-shared
1383
+ * token from `clientAuthTokens`) and, when valid, marks the session
1384
+ * trusted immediately — before the client's own handshake call.
1385
+ */
1386
+ onConnect: (peer: Peer, session: DevframeNodeRpcSession) => void;
1387
+ /**
1388
+ * Print the current one-time code and its magic-link URL. Devframe stays
1389
+ * headless — call this yourself once the server is listening. Safe to
1390
+ * call repeatedly; it only prints once per code.
1391
+ */
1392
+ printBanner: () => void;
1255
1393
  }
1256
1394
  //#endregion
1257
1395
  //#region src/types/devframe.d.ts
@@ -1356,14 +1494,27 @@ interface DevframeCliOptions {
1356
1494
  */
1357
1495
  open?: boolean | string;
1358
1496
  /**
1359
- * Skip the RPC trust handshake. Set to `false` for trusted
1360
- * single-user localhost tools. Default `true`.
1497
+ * Authentication for the standalone dev server.
1498
+ *
1499
+ * - `undefined` / `true` — the standalone adapters (`cli` / `spa` /
1500
+ * served `build`) auto-wire devframe's interactive OTP auth
1501
+ * (`createInteractiveAuth`): an untrusted client can only reach
1502
+ * `anonymous:` methods until it exchanges the printed one-time code.
1503
+ * The adapter prints the code + magic-link banner once the server is
1504
+ * listening.
1505
+ * - `false` — no gate, for trusted single-user localhost tools where an
1506
+ * auth round-trip only gets in the way (the built-in plugins set this).
1507
+ * The `--no-auth` CLI flag maps here for one-off runs.
1508
+ * - A {@link DevframeAuthHandler} — a custom handler (e.g. a tuned
1509
+ * `createInteractiveAuth`, or an entirely different scheme) passed
1510
+ * straight through to `startHttpAndWs`.
1511
+ *
1512
+ * Hosted adapters (`vite`, `embedded`) ignore this and defer to the host's
1513
+ * auth; `@vitejs/devtools` honors the equivalent `devtools.clientAuth`.
1361
1514
  *
1362
- * Forwarded to `startHttpAndWs` as a no-op placeholder until devframe
1363
- * ships its own auth layer; `@vitejs/devtools` honors the equivalent
1364
- * `devtools.clientAuth` today.
1515
+ * @default true
1365
1516
  */
1366
- auth?: boolean;
1517
+ auth?: boolean | DevframeAuthHandler;
1367
1518
  /**
1368
1519
  * Expose a route-based MCP server alongside the dev server, speaking the
1369
1520
  * MCP Streamable-HTTP transport at `/__mcp` (relative to the base path).
@@ -1538,4 +1689,4 @@ interface DevframeDefinition {
1538
1689
  }
1539
1690
  declare function defineDevframe(d: DevframeDefinition): DevframeDefinition;
1540
1691
  //#endregion
1541
- export { ScopedBroadcastOptions as $, StreamSink as A, SharedStateEvents as B, RpcStreamingChannelOptions as C, EventUnsubscribe as Ct, CreateStreamSinkOptions as D, defineCliFlags as Dt, CreateStreamReaderOptions as E, InferCliFlags as Et, ImmutableArray as F, PartialWithoutId as G, SharedStatePatch as H, ImmutableMap as I, DevframeScopedNodeRpc as J, Thenable as K, ImmutableObject as L, createStreamReader as M, createStreamSink as N, StreamErrorPayload as O, parseCliFlags as Ot, Immutable as P, DevframeSettingsStore as Q, ImmutableSet as R, RpcStreamingChannel as S, EventEmitter as St, BufferedChunk as T, CliFlagsSchema as Tt, createSharedState as U, SharedStateOptions as V, EntriesToObject as W, DevframeSettings as X, DevframeScopedStreamingHost as Y, DevframeSettingsRegistry as Z, DevframeNodeRpcSession as _, AgentResourceInput as _t, DevframeDockDefaults as a, DevframeRpcClientFunctions as at, RpcSharedStateGetOptions as b, DevframeAgentHost as bt, DevframeSetupInfo as c, DevframeHost as ct, McpRouteOptions as d, DevframeDiagnosticsHost as dt, ScopedClientFunctions as et, defineDevframe as f, DevframeDiagnosticsLogger as ft, DevframeNodeContext as g, AgentResourceContent as gt, DevframeCapabilities as h, AgentResource as ht, DevframeDeploymentKind as i, DevframeViewHost as it, StreamSinkEvents as j, StreamReader as k, DevframeSpaOptions as l, DevframeDefineDiagnosticsOptions as lt, ConnectionMetaWebsocket as m, AgentManifest as mt, DevframeCliOptions as n, ScopedSharedStates as nt, DevframeDuplicationStrategy as o, DevframeRpcServerFunctions as ot, ConnectionMeta as p, AgentHandle as pt, DevframeScopedNodeContext as q, DevframeDefinition as r, SettingsForNamespace as rt, DevframeRuntime as s, DevframeRpcSharedStates as st, DevframeBrowserContext as t, ScopedServerFunctions as tt, DevframeWsOptions as u, DevframeDiagnosticsDefinition as ut, RpcBroadcastOptions as v, AgentTool as vt, RpcStreamingHost as w, EventsMap as wt, RpcSharedStateHost as x, DevframeAgentHostEvents as xt, RpcFunctionsHost as y, AgentToolInput as yt, SharedState as z };
1692
+ export { DevframeRpcSharedStates as $, DevframeSettingsStore as A, EventsMap as At, DevframeDiagnosticsDefinition as B, DevframeServicesHost as C, AgentResourceInput as Ct, DevframeScopedStreamingHost as D, DevframeAgentHostEvents as Dt, DevframeScopedNodeRpc as E, DevframeAgentHost as Et, SettingsForNamespace as F, RpcFunctionsHost as G, DevframeDiagnosticsLogger as H, DevframeViewHost as I, RpcStreamingChannel as J, RpcSharedStateGetOptions as K, DevframeHost as L, ScopedClientFunctions as M, InferCliFlags as Mt, ScopedServerFunctions as N, defineCliFlags as Nt, DevframeSettings as O, EventEmitter as Ot, ScopedSharedStates as P, parseCliFlags as Pt, DevframeRpcServerFunctions as Q, DevframeStorageScope as R, DevframeServiceOf as S, AgentResourceContent as St, DevframeScopedNodeContext as T, AgentToolInput as Tt, DevframeNodeRpcSession as U, DevframeDiagnosticsHost as V, RpcBroadcastOptions as W, RpcStreamingHost as X, RpcStreamingChannelOptions as Y, DevframeRpcClientFunctions as Z, ConnectionMeta as _, SharedStatePatch as _t, DevframeDockDefaults as a, StreamSink as at, DevframeNodeContext as b, AgentManifest as bt, DevframeSetupInfo as c, createStreamSink as ct, McpRouteOptions as d, ImmutableMap as dt, BufferedChunk as et, defineDevframe as f, ImmutableObject as ft, Thenable as g, SharedStateOptions as gt, PartialWithoutId as h, SharedStateEvents as ht, DevframeDeploymentKind as i, StreamReader as it, ScopedBroadcastOptions as j, CliFlagsSchema as jt, DevframeSettingsRegistry as k, EventUnsubscribe as kt, DevframeSpaOptions as l, Immutable as lt, EntriesToObject as m, SharedState as mt, DevframeCliOptions as n, CreateStreamSinkOptions as nt, DevframeDuplicationStrategy as o, StreamSinkEvents as ot, DevframeAuthHandler as p, ImmutableSet as pt, RpcSharedStateHost as q, DevframeDefinition as r, StreamErrorPayload as rt, DevframeRuntime as s, createStreamReader as st, DevframeBrowserContext as t, CreateStreamReaderOptions as tt, DevframeWsOptions as u, ImmutableArray as ut, ConnectionMetaWebsocket as v, createSharedState as vt, DevframeServicesRegistry as w, AgentTool as wt, DevframeServiceId as x, AgentResource as xt, DevframeCapabilities as y, AgentHandle as yt, DevframeDefineDiagnosticsOptions as z };