devframe 0.8.0 → 0.8.1

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 (49) hide show
  1. package/dist/adapters/build.d.mts +1 -1
  2. package/dist/adapters/cac.d.mts +1 -1
  3. package/dist/adapters/cac.mjs +1 -1
  4. package/dist/adapters/cli.d.mts +1 -1
  5. package/dist/adapters/cli.mjs +1 -1
  6. package/dist/adapters/dev.d.mts +2 -2
  7. package/dist/adapters/dev.mjs +1 -1
  8. package/dist/adapters/embedded.d.mts +1 -1
  9. package/dist/adapters/mcp.d.mts +11 -5
  10. package/dist/adapters/mcp.mjs +1 -1
  11. package/dist/{cac-B-R_tDGJ.mjs → cac-B1LM3zit.mjs} +1 -1
  12. package/dist/cli/main.mjs +2 -1
  13. package/dist/client/index.d.mts +37 -8
  14. package/dist/client/index.mjs +41 -15
  15. package/dist/constants.d.mts +13 -6
  16. package/dist/constants.mjs +13 -6
  17. package/dist/{context-C9Cgm1hP.mjs → context-CQefP2jR.mjs} +2 -1
  18. package/dist/{context-7BbaIUSI.d.mts → context-DFzmxCLa.d.mts} +1 -1
  19. package/dist/{revoke-BtQDKTp7.mjs → crypto-token-XCqTSMg9.mjs} +1 -35
  20. package/dist/{dev-D4M7Veo7.mjs → dev-BSHFZGZr.mjs} +2 -2
  21. package/dist/{devframe-Dsjn_Xtq.d.mts → devframe-qgCKL683.d.mts} +10 -1
  22. package/dist/{fetch-BZyK4v6W.mjs → fetch-CK0S253E.mjs} +5 -4
  23. package/dist/helpers/vite.d.mts +15 -14
  24. package/dist/helpers/vite.mjs +7 -7
  25. package/dist/{http-BghOqfTD.mjs → http-Dvgxujcu.mjs} +1 -1
  26. package/dist/{index-CeBbry0R.d.mts → index-B7RgLw--.d.mts} +11 -6
  27. package/dist/index.d.mts +2 -2
  28. package/dist/node/auth.d.mts +2 -2
  29. package/dist/node/auth.mjs +2 -2
  30. package/dist/node/hub-internals.d.mts +2 -2
  31. package/dist/node/hub-internals.mjs +1 -1
  32. package/dist/node/index.d.mts +23 -3
  33. package/dist/node/index.mjs +3 -3
  34. package/dist/recipes/common-rpc-functions.d.mts +1 -1
  35. package/dist/recipes/interactive-auth.d.mts +2 -2
  36. package/dist/recipes/interactive-auth.mjs +11 -3
  37. package/dist/revoke-DZbJ7Cl0.mjs +35 -0
  38. package/dist/rpc/transports/ws-server.d.mts +2 -2
  39. package/dist/rpc/transports/ws-server.mjs +49 -2
  40. package/dist/{server-CcAPxuoT.mjs → server-CXaDDUl0.mjs} +1 -1
  41. package/dist/{server-VLQJouOO.d.mts → server-E9z4ZWbd.d.mts} +4 -3
  42. package/dist/{state-WX3HT5a3.mjs → state-BeUHDDjk.mjs} +13 -6
  43. package/dist/types/index.d.mts +2 -2
  44. package/dist/utils/events.d.mts +1 -1
  45. package/dist/utils/shared-state.d.mts +1 -1
  46. package/dist/utils/streaming-channel.d.mts +1 -1
  47. package/dist/{ws-server-D_Vjtums.d.mts → ws-server-J-zkiOyp.d.mts} +23 -2
  48. package/package.json +1 -1
  49. package/skills/devframe/SKILL.md +3 -2
@@ -1,4 +1,4 @@
1
- import { r as DevframeDefinition } from "../devframe-Dsjn_Xtq.mjs";
1
+ import { r as DevframeDefinition } from "../devframe-qgCKL683.mjs";
2
2
  //#region src/adapters/build.d.ts
3
3
  interface CreateBuildOptions {
4
4
  /** Output directory. Defaults to `dist-static`. */
@@ -1,4 +1,4 @@
1
- import { Ft as InferCliFlags, It as defineCliFlags, Lt as parseCliFlags, Pt as CliFlagsSchema, r as DevframeDefinition } from "../devframe-Dsjn_Xtq.mjs";
1
+ import { Ft as InferCliFlags, It as defineCliFlags, Lt as parseCliFlags, Pt as CliFlagsSchema, r as DevframeDefinition } from "../devframe-qgCKL683.mjs";
2
2
  import { CAC } from "cac";
3
3
  import { H3 } from "h3";
4
4
  //#region src/adapters/cac.d.ts
@@ -1,2 +1,2 @@
1
- import { n as defineCliFlags, r as parseCliFlags, t as createCac } from "../cac-B-R_tDGJ.mjs";
1
+ import { n as defineCliFlags, r as parseCliFlags, t as createCac } from "../cac-B1LM3zit.mjs";
2
2
  export { createCac, defineCliFlags, parseCliFlags };
@@ -1,4 +1,4 @@
1
- import { Ft as InferCliFlags, It as defineCliFlags, Lt as parseCliFlags, Pt as CliFlagsSchema } from "../devframe-Dsjn_Xtq.mjs";
1
+ import { Ft as InferCliFlags, It as defineCliFlags, Lt as parseCliFlags, Pt as CliFlagsSchema } from "../devframe-qgCKL683.mjs";
2
2
  import { CacHandle, CreateCacOptions, createCac } from "./cac.mjs";
3
3
  //#region src/adapters/cli.d.ts
4
4
  /** @deprecated Use `createCac` from `devframe/adapters/cac` instead. */
@@ -1,4 +1,4 @@
1
- import { n as defineCliFlags, r as parseCliFlags, t as createCac } from "../cac-B-R_tDGJ.mjs";
1
+ import { n as defineCliFlags, r as parseCliFlags, t as createCac } from "../cac-B1LM3zit.mjs";
2
2
  //#region src/adapters/cli.ts
3
3
  /** @deprecated Use `createCac` from `devframe/adapters/cac` instead. */
4
4
  const createCli = createCac;
@@ -1,5 +1,5 @@
1
- import { _ as ConnectionMeta, d as McpRouteOptions, p as DevframeAuthHandler, r as DevframeDefinition, u as DevframeWsOptions } from "../devframe-Dsjn_Xtq.mjs";
2
- import { n as StartedServer } from "../server-VLQJouOO.mjs";
1
+ import { _ as ConnectionMeta, d as McpRouteOptions, p as DevframeAuthHandler, r as DevframeDefinition, u as DevframeWsOptions } from "../devframe-qgCKL683.mjs";
2
+ import { n as StartedServer } from "../server-E9z4ZWbd.mjs";
3
3
  import { H3 } from "h3";
4
4
  //#region src/adapters/dev.d.ts
5
5
  interface CreateDevServerOptions {
@@ -1,2 +1,2 @@
1
- import { n as resolveDevServerPort, r as resolveMcpConnectionMeta, t as createDevServer } from "../dev-D4M7Veo7.mjs";
1
+ import { n as resolveDevServerPort, r as resolveMcpConnectionMeta, t as createDevServer } from "../dev-BSHFZGZr.mjs";
2
2
  export { createDevServer, resolveDevServerPort, resolveMcpConnectionMeta };
@@ -1,4 +1,4 @@
1
- import { b as DevframeNodeContext, r as DevframeDefinition } from "../devframe-Dsjn_Xtq.mjs";
1
+ import { b as DevframeNodeContext, r as DevframeDefinition } from "../devframe-qgCKL683.mjs";
2
2
  //#region src/adapters/embedded.d.ts
3
3
  interface CreateEmbeddedOptions {
4
4
  /** Target context the devframe is registered into. Required. */
@@ -1,4 +1,4 @@
1
- import { b as DevframeNodeContext, r as DevframeDefinition } from "../devframe-Dsjn_Xtq.mjs";
1
+ import { b as DevframeNodeContext, r as DevframeDefinition } from "../devframe-qgCKL683.mjs";
2
2
  import "@modelcontextprotocol/server";
3
3
  //#region src/adapters/mcp/build-server.d.ts
4
4
  interface CreateMcpServerOptions {
@@ -47,7 +47,12 @@ interface CreateMcpFetchHandlerOptions {
47
47
  exposeSharedState: boolean | ((key: string) => boolean);
48
48
  /**
49
49
  * Origin allow-list beyond the loopback default. `false` disables the
50
- * origin gate entirely. Default: loopback-only (mirrors the WS transport).
50
+ * origin gate entirely. Default: loopback-only.
51
+ *
52
+ * Unlike the WS transport, the MCP route does **not** allow `Origin`-less
53
+ * requests: a route-based endpoint is reachable by any local process, so a
54
+ * request must carry an `Origin` that passes the gate. Native clients
55
+ * (e.g. `devframe connect`) send their loopback origin explicitly.
51
56
  */
52
57
  allowedOrigins?: readonly string[] | false;
53
58
  }
@@ -71,9 +76,10 @@ interface McpFetchHandler {
71
76
  * and MCP server (built from the shared, live `ctx` via
72
77
  * `buildMcpServerFromContext`), correlated by the `Mcp-Session-Id` header: an
73
78
  * `initialize` POST spins up a session; later requests route to it; a `DELETE`
74
- * (or client disconnect) tears it down. The origin gate applies devframe's
75
- * loopback-default DNS-rebinding protection (identical semantics to the WS
76
- * upgrade's `isAllowedOrigin`).
79
+ * (or client disconnect) tears it down. The origin gate guards every request:
80
+ * loopback-default DNS-rebinding protection that — unlike the WS upgrade's
81
+ * `isAllowedOrigin` — also rejects `Origin`-less requests, so a route-based
82
+ * endpoint isn't reachable by an arbitrary local process.
77
83
  *
78
84
  * @experimental
79
85
  */
@@ -1,2 +1,2 @@
1
- import { n as createMcpServer, t as createMcpFetchHandler } from "../fetch-BZyK4v6W.mjs";
1
+ import { n as createMcpServer, t as createMcpFetchHandler } from "../fetch-CK0S253E.mjs";
2
2
  export { createMcpFetchHandler, createMcpServer };
@@ -1,6 +1,6 @@
1
1
  import { n as colors } from "./diagnostics-reporter-CsIG85Q5.mjs";
2
2
  import { createBuild } from "./adapters/build.mjs";
3
- import { n as resolveDevServerPort, t as createDevServer } from "./dev-D4M7Veo7.mjs";
3
+ import { n as resolveDevServerPort, t as createDevServer } from "./dev-BSHFZGZr.mjs";
4
4
  import process from "node:process";
5
5
  import cac$1 from "cac";
6
6
  //#region src/adapters/flags.ts
package/dist/cli/main.mjs CHANGED
@@ -200,7 +200,8 @@ async function call(sdk, options, args) {
200
200
  });
201
201
  }
202
202
  async function withInstanceClient(sdk, url, fn) {
203
- const transport = new sdk.StreamableHTTPClientTransport(new URL(url));
203
+ const origin = new URL(url).origin;
204
+ const transport = new sdk.StreamableHTTPClientTransport(new URL(url), { requestInit: { headers: { origin } } });
204
205
  const client = new sdk.Client({
205
206
  name: "devframe-connect",
206
207
  version: "0.0.0"
@@ -1,4 +1,4 @@
1
- import { $ as DevframeRpcServerFunctions, F as ScopedSharedStates, I as SettingsForNamespace, J as RpcSharedStateHost, N as ScopedRpcFn, O as DevframeSettings, P as ScopedServerFunctions, Q as DevframeRpcClientFunctions, _ as ConnectionMeta, at as StreamReader, ht as SharedState, jt as EventEmitter, ot as StreamSink, q as RpcSharedStateGetOptions } from "../devframe-Dsjn_Xtq.mjs";
1
+ import { $ as DevframeRpcServerFunctions, F as ScopedSharedStates, I as SettingsForNamespace, J as RpcSharedStateHost, N as ScopedRpcFn, O as DevframeSettings, P as ScopedServerFunctions, Q as DevframeRpcClientFunctions, _ as ConnectionMeta, at as StreamReader, ht as SharedState, jt as EventEmitter, ot as StreamSink, q as RpcSharedStateGetOptions } from "../devframe-qgCKL683.mjs";
2
2
  import { _ as RpcFunctionDefinition, w as RpcFunctionsCollector } from "../types-CnJSgRVa.mjs";
3
3
  import { E as RpcCacheOptions, T as RpcCacheManager } from "../index-Dbw1p5ch.mjs";
4
4
  import { t as WsRpcChannelOptions } from "../ws-client-B6Tnr0ic.mjs";
@@ -26,6 +26,12 @@ interface SetupDevframeConnectionOptions {
26
26
  /** Override the locally stored auth token. */
27
27
  authToken?: string;
28
28
  }
29
+ /**
30
+ * Allow an external viewer to connect by registering its browser origin with
31
+ * the Devframe host. Returns `false` if the host did not provide an origin
32
+ * registration token.
33
+ */
34
+ declare function registerDevframeViewerOrigin(connection: DevframeConnection, origin?: any): Promise<boolean>;
29
35
  /**
30
36
  * Return connection information previously prepared in this window or an
31
37
  * accessible parent window.
@@ -377,15 +383,16 @@ declare function getDevframeRpcClient(options?: DevframeRpcClientOptions): Promi
377
383
  //#endregion
378
384
  //#region src/client/otp.d.ts
379
385
  /**
380
- * Read a one-time authentication code (OTP) from the current page URL's query
381
- * string, without side effects. Returns `undefined` when the parameter is absent.
386
+ * Read a one-time authentication code (OTP) from the current page URL's
387
+ * fragment, without side effects. Returns `undefined` when the parameter is
388
+ * absent.
382
389
  */
383
390
  declare function readOtpFromUrl(param?: string): string | undefined;
384
391
  /**
385
- * Read the one-time code from the page URL and remove it from the address bar
386
- * (and the current history entry), so the single-use code isn't left in the
387
- * URL, browser history, or a `Referer`. Returns the code, or `undefined` when
388
- * absent.
392
+ * Read the one-time code from the page URL fragment and remove it from the
393
+ * address bar (and the current history entry), so the single-use code isn't
394
+ * left in the URL, browser history, or a `Referer`. Returns the code, or
395
+ * `undefined` when absent.
389
396
  */
390
397
  declare function consumeOtpFromUrl(param?: string): string | undefined;
391
398
  /**
@@ -402,6 +409,28 @@ declare function authenticateWithUrlOtp(rpc: Pick<DevframeRpcClient, 'isTrusted'
402
409
  param?: string;
403
410
  }): Promise<boolean>;
404
411
  //#endregion
412
+ //#region src/client/rpc-ws.d.ts
413
+ /** Minimal subset of `window.location` needed to resolve a WS URL. */
414
+ interface WsUrlLocation {
415
+ protocol: string;
416
+ host: string;
417
+ hostname: string;
418
+ href: string;
419
+ }
420
+ /**
421
+ * Resolve a {@link ConnectionMeta.websocket} descriptor into a concrete
422
+ * `ws(s)://` URL.
423
+ *
424
+ * The object / relative-path forms connect to the page's own origin (only the
425
+ * `http`→`ws` protocol swap is applied), resolving the path against where
426
+ * `__connection.json` was loaded. This is deliberately host-agnostic so the
427
+ * connection survives a reverse proxy that changes the domain or port — the
428
+ * client trusts its own location, never a server-baked hostname. An explicit
429
+ * `port`/`host` (or a full `ws(s)://` URL string) opts into a cross-origin
430
+ * endpoint, e.g. a side-car server on its own port.
431
+ */
432
+ declare function resolveWsUrl(websocket: ConnectionMeta['websocket'], metaBaseUrl: string, loc: WsUrlLocation): string;
433
+ //#endregion
405
434
  //#region src/client/settings.d.ts
406
435
  /**
407
436
  * Build the client-side `settings` surface for a scope namespace. Mirrors
@@ -412,4 +441,4 @@ declare function createClientSettings<T extends Record<string, any> = Record<str
412
441
  //#region src/client/index.d.ts
413
442
  declare const connectDevframe: typeof getDevframeRpcClient;
414
443
  //#endregion
415
- export { DevframeClientRpcHost, DevframeConnection, DevframeConnectionError, DevframeConnectionErrorKind, DevframeConnectionStatus, DevframeRpcClient, DevframeRpcClientCall, DevframeRpcClientCallEvent, DevframeRpcClientCallOptional, DevframeRpcClientMode, DevframeRpcClientOptions, DevframeRpcContext, DevframeScopedClientContext, DevframeScopedClientRpc, DevframeScopedClientStreamingHost, RpcClientEvents, RpcStreamingClientHost, SetupDevframeConnectionOptions, StreamingSubscribeOptions, authenticateWithUrlOtp, connectDevframe, consumeOtpFromUrl, createClientSettings, createRpcStreamingClientHost, createScopedClientContext, getDevframeConnection, getDevframeRpcClient, isCallableStatus, readOtpFromUrl, setupDevframeConnection };
444
+ export { DevframeClientRpcHost, DevframeConnection, DevframeConnectionError, DevframeConnectionErrorKind, DevframeConnectionStatus, DevframeRpcClient, DevframeRpcClientCall, DevframeRpcClientCallEvent, DevframeRpcClientCallOptional, DevframeRpcClientMode, DevframeRpcClientOptions, DevframeRpcContext, DevframeScopedClientContext, DevframeScopedClientRpc, DevframeScopedClientStreamingHost, RpcClientEvents, RpcStreamingClientHost, SetupDevframeConnectionOptions, StreamingSubscribeOptions, type WsUrlLocation, authenticateWithUrlOtp, connectDevframe, consumeOtpFromUrl, createClientSettings, createRpcStreamingClientHost, createScopedClientContext, getDevframeConnection, getDevframeRpcClient, isCallableStatus, readOtpFromUrl, registerDevframeViewerOrigin, resolveWsUrl, setupDevframeConnection };
@@ -20,11 +20,14 @@ const DEVFRAME_CONNECTION_META_FILENAME = "__connection.json";
20
20
  const DEVFRAME_CONNECTION_KEY = "__DEVFRAME_CONNECTION__";
21
21
  const DEVFRAME_RPC_DUMP_MANIFEST_FILENAME = "__rpc-dump/index.json";
22
22
  /**
23
- * Page-URL query parameter carrying a one-time authentication code (OTP) for
24
- * "magic link" auth. A host can print a link like `<origin>/?devframe_otp=<code>`;
25
- * the client reads the code, exchanges it for a token, and strips the parameter
26
- * from the URL. See `buildOtpAuthUrl` (node) and the `authenticateWithUrlOtp` /
27
- * `consumeOtpFromUrl` client utilities (or `connectDevframe`'s `otpParam`).
23
+ * Page-URL **fragment** parameter carrying a one-time authentication code (OTP)
24
+ * for "magic link" auth. A host can print a link like
25
+ * `<origin>/#devframe_otp=<code>`; the client reads the code, exchanges it for a
26
+ * token, and strips the parameter from the URL. The code rides the fragment
27
+ * (never the query string) so the browser never transmits it to the server,
28
+ * keeping it out of access logs and `Referer` headers. See `buildOtpAuthUrl`
29
+ * (node) and the `authenticateWithUrlOtp` / `consumeOtpFromUrl` client utilities
30
+ * (or `connectDevframe`'s `otpParam`).
28
31
  */
29
32
  const DEVFRAME_OTP_URL_PARAM = "devframe_otp";
30
33
  /**
@@ -35,6 +38,10 @@ const DEVFRAME_OTP_URL_PARAM = "devframe_otp";
35
38
  * trusted before its own `anonymous:devframe:auth` handshake call arrives.
36
39
  */
37
40
  const DEVFRAME_AUTH_TOKEN_QUERY_PARAM = "devframe_auth_token";
41
+ /** External viewer origin requested during connection bootstrap. */
42
+ const DEVFRAME_VIEWER_ORIGIN_QUERY_PARAM = "devframe_viewer_origin";
43
+ /** Token that authorizes an external viewer origin registration. */
44
+ const DEVFRAME_VIEWER_ORIGIN_TOKEN_QUERY_PARAM = "devframe_viewer_origin_token";
38
45
  //#endregion
39
46
  //#region src/utils/diagnostics-reporter.ts
40
47
  const formatAnsi = ansiFormatter(colors);
@@ -428,6 +435,21 @@ function storeAuthToken(token) {
428
435
  }
429
436
  //#endregion
430
437
  //#region src/client/connection.ts
438
+ /**
439
+ * Allow an external viewer to connect by registering its browser origin with
440
+ * the Devframe host. Returns `false` if the host did not provide an origin
441
+ * registration token.
442
+ */
443
+ async function registerDevframeViewerOrigin(connection, origin = globalThis.location?.origin) {
444
+ const token = connection.connectionMeta.viewerOriginToken;
445
+ if (!token || !origin) return false;
446
+ const url = new URL(connection.metaBaseUrl);
447
+ url.searchParams.set(DEVFRAME_VIEWER_ORIGIN_QUERY_PARAM, origin);
448
+ url.searchParams.set(DEVFRAME_VIEWER_ORIGIN_TOKEN_QUERY_PARAM, token);
449
+ const response = await fetch(url, { cache: "no-store" });
450
+ if (!response.ok) throw new Error(`Failed to register external viewer origin (${response.status}).`);
451
+ return true;
452
+ }
431
453
  function resolveMetaBaseUrl(baseURL) {
432
454
  const metaPath = withBase(DEVFRAME_CONNECTION_META_FILENAME, baseURL);
433
455
  try {
@@ -530,12 +552,14 @@ function isCallableStatus(status) {
530
552
  //#endregion
531
553
  //#region src/client/otp.ts
532
554
  /**
533
- * Read a one-time authentication code (OTP) from the current page URL's query
534
- * string, without side effects. Returns `undefined` when the parameter is absent.
555
+ * Read a one-time authentication code (OTP) from the current page URL's
556
+ * fragment, without side effects. Returns `undefined` when the parameter is
557
+ * absent.
535
558
  */
536
559
  function readOtpFromUrl(param = DEVFRAME_OTP_URL_PARAM) {
537
560
  try {
538
- return new URLSearchParams(globalThis.location?.search).get(param) || void 0;
561
+ const hash = globalThis.location?.hash?.replace(/^#/, "") ?? "";
562
+ return new URLSearchParams(hash).get(param) || void 0;
539
563
  } catch {
540
564
  return;
541
565
  }
@@ -543,16 +567,18 @@ function readOtpFromUrl(param = DEVFRAME_OTP_URL_PARAM) {
543
567
  function stripParamFromUrl(param) {
544
568
  try {
545
569
  const url = new URL(globalThis.location.href);
546
- if (!url.searchParams.has(param)) return;
547
- url.searchParams.delete(param);
570
+ const fragment = new URLSearchParams(url.hash.replace(/^#/, ""));
571
+ if (!fragment.has(param)) return;
572
+ fragment.delete(param);
573
+ url.hash = fragment.toString();
548
574
  globalThis.history?.replaceState(globalThis.history.state, "", url.href);
549
575
  } catch {}
550
576
  }
551
577
  /**
552
- * Read the one-time code from the page URL and remove it from the address bar
553
- * (and the current history entry), so the single-use code isn't left in the
554
- * URL, browser history, or a `Referer`. Returns the code, or `undefined` when
555
- * absent.
578
+ * Read the one-time code from the page URL fragment and remove it from the
579
+ * address bar (and the current history entry), so the single-use code isn't
580
+ * left in the URL, browser history, or a `Referer`. Returns the code, or
581
+ * `undefined` when absent.
556
582
  */
557
583
  function consumeOtpFromUrl(param = DEVFRAME_OTP_URL_PARAM) {
558
584
  const code = readOtpFromUrl(param);
@@ -2588,4 +2614,4 @@ async function getDevframeRpcClient(options = {}) {
2588
2614
  //#region src/client/index.ts
2589
2615
  const connectDevframe = getDevframeRpcClient;
2590
2616
  //#endregion
2591
- export { DevframeConnectionError, authenticateWithUrlOtp, connectDevframe, consumeOtpFromUrl, createClientSettings, createRpcStreamingClientHost, createScopedClientContext, getDevframeConnection, getDevframeRpcClient, isCallableStatus, readOtpFromUrl, setupDevframeConnection };
2617
+ export { DevframeConnectionError, authenticateWithUrlOtp, connectDevframe, consumeOtpFromUrl, createClientSettings, createRpcStreamingClientHost, createScopedClientContext, getDevframeConnection, getDevframeRpcClient, isCallableStatus, readOtpFromUrl, registerDevframeViewerOrigin, resolveWsUrl, setupDevframeConnection };
@@ -35,11 +35,14 @@ declare const DEVFRAME_RPC_DUMP_DIRNAME = "__rpc-dump";
35
35
  */
36
36
  declare const REMOTE_CONNECTION_KEY = "devframe-remote-connection";
37
37
  /**
38
- * Page-URL query parameter carrying a one-time authentication code (OTP) for
39
- * "magic link" auth. A host can print a link like `<origin>/?devframe_otp=<code>`;
40
- * the client reads the code, exchanges it for a token, and strips the parameter
41
- * from the URL. See `buildOtpAuthUrl` (node) and the `authenticateWithUrlOtp` /
42
- * `consumeOtpFromUrl` client utilities (or `connectDevframe`'s `otpParam`).
38
+ * Page-URL **fragment** parameter carrying a one-time authentication code (OTP)
39
+ * for "magic link" auth. A host can print a link like
40
+ * `<origin>/#devframe_otp=<code>`; the client reads the code, exchanges it for a
41
+ * token, and strips the parameter from the URL. The code rides the fragment
42
+ * (never the query string) so the browser never transmits it to the server,
43
+ * keeping it out of access logs and `Referer` headers. See `buildOtpAuthUrl`
44
+ * (node) and the `authenticateWithUrlOtp` / `consumeOtpFromUrl` client utilities
45
+ * (or `connectDevframe`'s `otpParam`).
43
46
  */
44
47
  declare const DEVFRAME_OTP_URL_PARAM = "devframe_otp";
45
48
  /**
@@ -50,6 +53,10 @@ declare const DEVFRAME_OTP_URL_PARAM = "devframe_otp";
50
53
  * trusted before its own `anonymous:devframe:auth` handshake call arrives.
51
54
  */
52
55
  declare const DEVFRAME_AUTH_TOKEN_QUERY_PARAM = "devframe_auth_token";
56
+ /** External viewer origin requested during connection bootstrap. */
57
+ declare const DEVFRAME_VIEWER_ORIGIN_QUERY_PARAM = "devframe_viewer_origin";
58
+ /** Token that authorizes an external viewer origin registration. */
59
+ declare const DEVFRAME_VIEWER_ORIGIN_TOKEN_QUERY_PARAM = "devframe_viewer_origin_token";
53
60
  /**
54
61
  * Prefix that marks an RPC method as callable before a connection is
55
62
  * trusted. This is the *only* rule the pre-trust gate applies — there is no
@@ -66,4 +73,4 @@ declare const ANONYMOUS_RPC_PREFIX = "anonymous:";
66
73
  */
67
74
  declare function isAnonymousRpcMethod(name: string): boolean;
68
75
  //#endregion
69
- export { ANONYMOUS_RPC_PREFIX, DEVFRAME_AUTH_TOKEN_QUERY_PARAM, DEVFRAME_CONNECTION_KEY, DEVFRAME_CONNECTION_META_FILENAME, DEVFRAME_DIRNAME, DEVFRAME_DOCK_IMPORTS_FILENAME, DEVFRAME_DOCK_IMPORTS_VIRTUAL_ID, DEVFRAME_MCP_ROUTE, DEVFRAME_MOUNT_PATH, DEVFRAME_MOUNT_PATH_NO_TRAILING_SLASH, DEVFRAME_OTP_URL_PARAM, DEVFRAME_RPC_DUMP_DIRNAME, DEVFRAME_RPC_DUMP_MANIFEST_FILENAME, DEVFRAME_WS_ROUTE, REMOTE_CONNECTION_KEY, isAnonymousRpcMethod };
76
+ export { ANONYMOUS_RPC_PREFIX, DEVFRAME_AUTH_TOKEN_QUERY_PARAM, DEVFRAME_CONNECTION_KEY, DEVFRAME_CONNECTION_META_FILENAME, DEVFRAME_DIRNAME, DEVFRAME_DOCK_IMPORTS_FILENAME, DEVFRAME_DOCK_IMPORTS_VIRTUAL_ID, DEVFRAME_MCP_ROUTE, DEVFRAME_MOUNT_PATH, DEVFRAME_MOUNT_PATH_NO_TRAILING_SLASH, DEVFRAME_OTP_URL_PARAM, DEVFRAME_RPC_DUMP_DIRNAME, DEVFRAME_RPC_DUMP_MANIFEST_FILENAME, DEVFRAME_VIEWER_ORIGIN_QUERY_PARAM, DEVFRAME_VIEWER_ORIGIN_TOKEN_QUERY_PARAM, DEVFRAME_WS_ROUTE, REMOTE_CONNECTION_KEY, isAnonymousRpcMethod };
@@ -35,11 +35,14 @@ const DEVFRAME_RPC_DUMP_DIRNAME = "__rpc-dump";
35
35
  */
36
36
  const REMOTE_CONNECTION_KEY = "devframe-remote-connection";
37
37
  /**
38
- * Page-URL query parameter carrying a one-time authentication code (OTP) for
39
- * "magic link" auth. A host can print a link like `<origin>/?devframe_otp=<code>`;
40
- * the client reads the code, exchanges it for a token, and strips the parameter
41
- * from the URL. See `buildOtpAuthUrl` (node) and the `authenticateWithUrlOtp` /
42
- * `consumeOtpFromUrl` client utilities (or `connectDevframe`'s `otpParam`).
38
+ * Page-URL **fragment** parameter carrying a one-time authentication code (OTP)
39
+ * for "magic link" auth. A host can print a link like
40
+ * `<origin>/#devframe_otp=<code>`; the client reads the code, exchanges it for a
41
+ * token, and strips the parameter from the URL. The code rides the fragment
42
+ * (never the query string) so the browser never transmits it to the server,
43
+ * keeping it out of access logs and `Referer` headers. See `buildOtpAuthUrl`
44
+ * (node) and the `authenticateWithUrlOtp` / `consumeOtpFromUrl` client utilities
45
+ * (or `connectDevframe`'s `otpParam`).
43
46
  */
44
47
  const DEVFRAME_OTP_URL_PARAM = "devframe_otp";
45
48
  /**
@@ -50,6 +53,10 @@ const DEVFRAME_OTP_URL_PARAM = "devframe_otp";
50
53
  * trusted before its own `anonymous:devframe:auth` handshake call arrives.
51
54
  */
52
55
  const DEVFRAME_AUTH_TOKEN_QUERY_PARAM = "devframe_auth_token";
56
+ /** External viewer origin requested during connection bootstrap. */
57
+ const DEVFRAME_VIEWER_ORIGIN_QUERY_PARAM = "devframe_viewer_origin";
58
+ /** Token that authorizes an external viewer origin registration. */
59
+ const DEVFRAME_VIEWER_ORIGIN_TOKEN_QUERY_PARAM = "devframe_viewer_origin_token";
53
60
  /**
54
61
  * Prefix that marks an RPC method as callable before a connection is
55
62
  * trusted. This is the *only* rule the pre-trust gate applies — there is no
@@ -68,4 +75,4 @@ function isAnonymousRpcMethod(name) {
68
75
  return name.startsWith(ANONYMOUS_RPC_PREFIX);
69
76
  }
70
77
  //#endregion
71
- export { ANONYMOUS_RPC_PREFIX, DEVFRAME_AUTH_TOKEN_QUERY_PARAM, DEVFRAME_CONNECTION_KEY, DEVFRAME_CONNECTION_META_FILENAME, DEVFRAME_DIRNAME, DEVFRAME_DOCK_IMPORTS_FILENAME, DEVFRAME_DOCK_IMPORTS_VIRTUAL_ID, DEVFRAME_MCP_ROUTE, DEVFRAME_MOUNT_PATH, DEVFRAME_MOUNT_PATH_NO_TRAILING_SLASH, DEVFRAME_OTP_URL_PARAM, DEVFRAME_RPC_DUMP_DIRNAME, DEVFRAME_RPC_DUMP_MANIFEST_FILENAME, DEVFRAME_WS_ROUTE, REMOTE_CONNECTION_KEY, isAnonymousRpcMethod };
78
+ export { ANONYMOUS_RPC_PREFIX, DEVFRAME_AUTH_TOKEN_QUERY_PARAM, DEVFRAME_CONNECTION_KEY, DEVFRAME_CONNECTION_META_FILENAME, DEVFRAME_DIRNAME, DEVFRAME_DOCK_IMPORTS_FILENAME, DEVFRAME_DOCK_IMPORTS_VIRTUAL_ID, DEVFRAME_MCP_ROUTE, DEVFRAME_MOUNT_PATH, DEVFRAME_MOUNT_PATH_NO_TRAILING_SLASH, DEVFRAME_OTP_URL_PARAM, DEVFRAME_RPC_DUMP_DIRNAME, DEVFRAME_RPC_DUMP_MANIFEST_FILENAME, DEVFRAME_VIEWER_ORIGIN_QUERY_PARAM, DEVFRAME_VIEWER_ORIGIN_TOKEN_QUERY_PARAM, DEVFRAME_WS_ROUTE, REMOTE_CONNECTION_KEY, isAnonymousRpcMethod };
@@ -1,5 +1,6 @@
1
+ import { n as randomToken } from "./crypto-token-XCqTSMg9.mjs";
1
2
  import { t as createStorage } from "./storage-Dzoc3NVC.mjs";
2
- import { i as randomToken, n as revokeAuthToken, t as revokeActiveConnectionsForToken } from "./revoke-BtQDKTp7.mjs";
3
+ import { n as revokeAuthToken, t as revokeActiveConnectionsForToken } from "./revoke-DZbJ7Cl0.mjs";
3
4
  import { join } from "pathe";
4
5
  //#region src/node/hub-internals/context.ts
5
6
  const internalContextMap = /* @__PURE__ */ new WeakMap();
@@ -1,4 +1,4 @@
1
- import { b as DevframeNodeContext, ht as SharedState } from "./devframe-Dsjn_Xtq.mjs";
1
+ import { b as DevframeNodeContext, ht as SharedState } from "./devframe-qgCKL683.mjs";
2
2
  //#region src/node/hub-internals/context.d.ts
3
3
  interface InternalAnonymousAuthStorage {
4
4
  trusted: Record<string, {
@@ -42,38 +42,4 @@ function timingSafeEqual(a, b) {
42
42
  return mismatch === 0;
43
43
  }
44
44
  //#endregion
45
- //#region src/node/auth/revoke.ts
46
- /**
47
- * Flip `isTrusted` to false on any live WS clients connected with `token`
48
- * and broadcast the `auth:revoked` event so they can react.
49
- *
50
- * Shared between persisted-auth revocation and remote-dock token revocation.
51
- */
52
- async function revokeActiveConnectionsForToken(context, token) {
53
- const rpcHost = context.rpc;
54
- if (!rpcHost?._rpcGroup) return;
55
- const affectedSessionIds = /* @__PURE__ */ new Set();
56
- for (const client of rpcHost._rpcGroup.clients) if (client.$meta.clientAuthToken === token) {
57
- affectedSessionIds.add(client.$meta.id);
58
- client.$meta.isTrusted = false;
59
- client.$meta.clientAuthToken = void 0;
60
- }
61
- if (affectedSessionIds.size === 0) return;
62
- await rpcHost.broadcast({
63
- method: "devframe:auth:revoked",
64
- args: [],
65
- filter: (client) => affectedSessionIds.has(client.$meta.id)
66
- });
67
- }
68
- /**
69
- * Revoke an auth token: remove from storage and notify all connected clients
70
- * using this token that they are no longer trusted.
71
- */
72
- async function revokeAuthToken(context, storage, token) {
73
- storage.mutate((state) => {
74
- delete state.trusted[token];
75
- });
76
- await revokeActiveConnectionsForToken(context, token);
77
- }
78
- //#endregion
79
- export { timingSafeEqual as a, randomToken as i, revokeAuthToken as n, randomDigits as r, revokeActiveConnectionsForToken as t };
45
+ export { randomToken as n, timingSafeEqual as r, randomDigits as t };
@@ -2,7 +2,7 @@ import { DEVFRAME_CONNECTION_META_FILENAME } from "./constants.mjs";
2
2
  import { n as createHostContext, t as createH3DevframeHost } from "./host-h3-Kz7t5Xab.mjs";
3
3
  import { t as diagnostics } from "./diagnostics-B5-qHeqD.mjs";
4
4
  import { r as registerDevframeInstance } from "./instance-registry-D6fxYu36.mjs";
5
- import { i as normalizeHttpServerUrl, t as startHttpAndWs } from "./server-CcAPxuoT.mjs";
5
+ import { i as normalizeHttpServerUrl, t as startHttpAndWs } from "./server-CXaDDUl0.mjs";
6
6
  import { n as resolveBasePath, t as normalizeBasePath } from "./_shared-bWRzeSa0.mjs";
7
7
  import { t as open } from "./open-Deb5xmIT.mjs";
8
8
  import { mountStaticHandler } from "./utils/serve-static.mjs";
@@ -295,7 +295,7 @@ async function createDevServer(def, options = {}) {
295
295
  const mcpPath = joinURL(basePath, mcpRoute);
296
296
  let mountMcpHttp;
297
297
  try {
298
- ({mountMcpHttp} = await import("./http-BghOqfTD.mjs"));
298
+ ({mountMcpHttp} = await import("./http-Dvgxujcu.mjs"));
299
299
  } catch (error) {
300
300
  const reason = error instanceof Error ? error.message : String(error);
301
301
  throw diagnostics.DF0017({
@@ -1,6 +1,6 @@
1
1
  import { _ as RpcFunctionDefinition, v as RpcFunctionDefinitionAny } from "./types-CnJSgRVa.mjs";
2
2
  import { w as RpcFunctionsCollectorBase } from "./index-Dbw1p5ch.mjs";
3
- import { t as DevframeNodeRpcSessionMeta } from "./ws-server-D_Vjtums.mjs";
3
+ import { n as DevframeNodeRpcSessionMeta } from "./ws-server-J-zkiOyp.mjs";
4
4
  import { CAC } from "cac";
5
5
  import { StandardSchemaV1 } from "@standard-schema/spec";
6
6
  import { Peer } from "crossws";
@@ -1414,6 +1414,15 @@ interface ConnectionMeta {
1414
1414
  * > from a publicly reachable static `__connection.json`.
1415
1415
  */
1416
1416
  authToken?: string;
1417
+ /**
1418
+ * Session-scoped token that lets a trusted external viewer register its
1419
+ * browser origin before opening the WebSocket. The host includes it in the
1420
+ * connection metadata, which the viewer obtains through the host page.
1421
+ *
1422
+ * Treat this as a bearer credential. Keep connection metadata containing the
1423
+ * token same-origin until the requesting origin has been verified.
1424
+ */
1425
+ viewerOriginToken?: string;
1417
1426
  }
1418
1427
  //#endregion
1419
1428
  //#region src/types/utils.d.ts
@@ -457,9 +457,10 @@ function parseResourceUri(uri) {
457
457
  * and MCP server (built from the shared, live `ctx` via
458
458
  * `buildMcpServerFromContext`), correlated by the `Mcp-Session-Id` header: an
459
459
  * `initialize` POST spins up a session; later requests route to it; a `DELETE`
460
- * (or client disconnect) tears it down. The origin gate applies devframe's
461
- * loopback-default DNS-rebinding protection (identical semantics to the WS
462
- * upgrade's `isAllowedOrigin`).
460
+ * (or client disconnect) tears it down. The origin gate guards every request:
461
+ * loopback-default DNS-rebinding protection that — unlike the WS upgrade's
462
+ * `isAllowedOrigin` — also rejects `Origin`-less requests, so a route-based
463
+ * endpoint isn't reachable by an arbitrary local process.
463
464
  *
464
465
  * @experimental
465
466
  */
@@ -503,7 +504,7 @@ function createMcpFetchHandler(ctx, options) {
503
504
  }
504
505
  async function handle(req) {
505
506
  const origin = req.headers.get("origin") ?? void 0;
506
- if (allowedOrigins !== false && !isAllowedOrigin(origin, allowedOrigins ?? [])) return new Response("Forbidden: origin not allowed", { status: 403 });
507
+ if (allowedOrigins !== false && (origin === void 0 || !isAllowedOrigin(origin, allowedOrigins ?? []))) return new Response("Forbidden: origin required", { status: 403 });
507
508
  const sessionId = req.headers.get("mcp-session-id") ?? void 0;
508
509
  let session = sessionId ? sessions.get(sessionId) : void 0;
509
510
  if (!session && req.method === "POST") {
@@ -1,4 +1,4 @@
1
- import { d as McpRouteOptions, p as DevframeAuthHandler, r as DevframeDefinition } from "../devframe-Dsjn_Xtq.mjs";
1
+ import { d as McpRouteOptions, p as DevframeAuthHandler, r as DevframeDefinition } from "../devframe-qgCKL683.mjs";
2
2
  //#region src/helpers/vite.d.ts
3
3
  interface ViteDevBridgeOptions {
4
4
  /**
@@ -31,16 +31,17 @@ interface ViteDevBridgeOptions {
31
31
  flags?: Record<string, unknown>;
32
32
  };
33
33
  /**
34
- * Whether the bridged devframe runs its own auth gate. This is a **hosted**
35
- * adapter — the devframe shares the host app's origin and the host owns
36
- * authentication — so it defaults to `false`: the plugin's own gate never
37
- * fires and its `cli.auth` default is ignored (matching devframe's
38
- * hosted-deployment contract). Pass `true` to force devframe's interactive
39
- * OTP gate on, or a {@link DevframeAuthHandler} to install a custom scheme.
40
- * Only applies in bridge mode (`devMiddleware`); the static-mount mode
34
+ * Whether the bridged devframe runs its own auth gate. The side-car RPC
35
+ * server is reachable by anything that can open its socket, so it **gates by
36
+ * default**: when unset, authentication resolves through `createDevServer`
37
+ * (devframe's interactive OTP gate unless the definition's `cli.auth` opts
38
+ * out), and the side-car prints its code/link banner to stdout. Pass a
39
+ * {@link DevframeAuthHandler} to install a custom scheme, or `false` to opt
40
+ * out for a single-user localhost host that owns the trust boundary another
41
+ * way. Only applies in bridge mode (`devMiddleware`); the static-mount mode
41
42
  * starts no RPC server.
42
43
  *
43
- * @default false
44
+ * @default gated (devframe's interactive OTP, unless `cli.auth` opts out)
44
45
  */
45
46
  auth?: boolean | DevframeAuthHandler;
46
47
  /**
@@ -84,11 +85,11 @@ interface DevframeVitePlugin {
84
85
  * host-served SPA can discover the WS endpoint via
85
86
  * {@link connectDevframe}.
86
87
  *
87
- * As a hosted adapter the bridge defers authentication to the host: its
88
- * side-car RPC server runs with the plugin's own auth gate **off** by
89
- * default (ignoring `def.cli?.auth`), so a plugin mounted this way never
90
- * triggers its standalone OTP prompt. Opt back in per-mount with
91
- * `options.auth` (`true` for devframe's interactive gate, or a handler).
88
+ * The side-car RPC server **gates by default** (devframe's interactive OTP
89
+ * unless the definition's `cli.auth` opts out), printing its code/link banner
90
+ * to stdout, so a bridged devframe isn't silently reachable by anything that
91
+ * can open its socket. Pass `options.auth: false` to opt out for a single-user
92
+ * localhost host, or a {@link DevframeAuthHandler} for a custom scheme.
92
93
  *
93
94
  * Use bridge mode when integrating with frameworks that own the SPA
94
95
  * (Nuxt, Astro, SolidStart, plain Vite apps). For the all-in-one
@@ -2,7 +2,7 @@ import { DEVFRAME_CONNECTION_META_FILENAME, DEVFRAME_WS_ROUTE } from "../constan
2
2
  import { t as diagnostics } from "../diagnostics-B5-qHeqD.mjs";
3
3
  import { n as resolveBasePath, t as normalizeBasePath } from "../_shared-bWRzeSa0.mjs";
4
4
  import { serveStaticNodeMiddleware } from "../utils/serve-static.mjs";
5
- import { n as resolveDevServerPort, r as resolveMcpConnectionMeta, t as createDevServer } from "../dev-D4M7Veo7.mjs";
5
+ import { n as resolveDevServerPort, r as resolveMcpConnectionMeta, t as createDevServer } from "../dev-BSHFZGZr.mjs";
6
6
  import { resolve } from "pathe";
7
7
  //#region src/helpers/vite.ts
8
8
  /**
@@ -19,11 +19,11 @@ import { resolve } from "pathe";
19
19
  * host-served SPA can discover the WS endpoint via
20
20
  * {@link connectDevframe}.
21
21
  *
22
- * As a hosted adapter the bridge defers authentication to the host: its
23
- * side-car RPC server runs with the plugin's own auth gate **off** by
24
- * default (ignoring `def.cli?.auth`), so a plugin mounted this way never
25
- * triggers its standalone OTP prompt. Opt back in per-mount with
26
- * `options.auth` (`true` for devframe's interactive gate, or a handler).
22
+ * The side-car RPC server **gates by default** (devframe's interactive OTP
23
+ * unless the definition's `cli.auth` opts out), printing its code/link banner
24
+ * to stdout, so a bridged devframe isn't silently reachable by anything that
25
+ * can open its socket. Pass `options.auth: false` to opt out for a single-user
26
+ * localhost host, or a {@link DevframeAuthHandler} for a custom scheme.
27
27
  *
28
28
  * Use bridge mode when integrating with frameworks that own the SPA
29
29
  * (Nuxt, Astro, SolidStart, plain Vite apps). For the all-in-one
@@ -58,7 +58,7 @@ function viteDevBridge(d, options = {}) {
58
58
  port,
59
59
  flags: mw.flags,
60
60
  openBrowser: false,
61
- auth: options.auth ?? false,
61
+ auth: options.auth,
62
62
  mcp: options.mcp
63
63
  });
64
64
  } catch (e) {
@@ -1,4 +1,4 @@
1
- import { t as createMcpFetchHandler } from "./fetch-BZyK4v6W.mjs";
1
+ import { t as createMcpFetchHandler } from "./fetch-CK0S253E.mjs";
2
2
  import { defineHandler } from "h3";
3
3
  //#region src/adapters/mcp/http.ts
4
4
  /**
@@ -1,5 +1,5 @@
1
- import { W as DevframeNodeRpcSession, b as DevframeNodeContext, ht as SharedState } from "./devframe-Dsjn_Xtq.mjs";
2
- import { n as InternalAnonymousAuthStorage } from "./context-7BbaIUSI.mjs";
1
+ import { W as DevframeNodeRpcSession, b as DevframeNodeContext, ht as SharedState } from "./devframe-qgCKL683.mjs";
2
+ import { n as InternalAnonymousAuthStorage } from "./context-DFzmxCLa.mjs";
3
3
  //#region src/node/auth/revoke.d.ts
4
4
  /**
5
5
  * Flip `isTrusted` to false on any live WS clients connected with `token`
@@ -28,10 +28,15 @@ declare function getTempAuthCode(): string;
28
28
  */
29
29
  declare function refreshTempAuthCode(): string;
30
30
  /**
31
- * Build a "magic link" authentication URL that embeds a one-time code (OTP) as
32
- * a query parameter. Opening it authenticates the client without typing — print
33
- * it on startup (devframe stays headless, so the host prints its own banner).
34
- * Defaults to the current code; the link is subject to the same TTL.
31
+ * Build a "magic link" authentication URL that embeds a one-time code (OTP) in
32
+ * the URL **fragment**. Opening it authenticates the client without typing —
33
+ * print it on startup (devframe stays headless, so the host prints its own
34
+ * banner). Defaults to the current code; the link is subject to the same TTL.
35
+ *
36
+ * The code rides the fragment (`#devframe_otp=…`), not the query string, so it
37
+ * is never sent to the server, written to an access log, or leaked in a
38
+ * `Referer` header — the browser client reads it locally (see
39
+ * `consumeOtpFromUrl`). Any existing fragment parameters are preserved.
35
40
  */
36
41
  declare function buildOtpAuthUrl(baseUrl: string, code?: string): string;
37
42
  /**
package/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
- import { $ as DevframeRpcServerFunctions, A as DevframeSettingsStore, At as DevframeAgentHostEvents, B as DevframeDefineDiagnosticsOptions, C as DevframeServicesHost, Ct as AgentResourceContent, D as DevframeScopedStreamingHost, Dt as AgentToolProvider, E as DevframeScopedNodeRpc, Et as AgentToolInput, F as ScopedSharedStates, G as RpcBroadcastOptions, H as DevframeDiagnosticsHost, I as SettingsForNamespace, J as RpcSharedStateHost, K as RpcFunctionsHost, L as DevframeViewHost, M as ScopedClientFunctions, Mt as EventUnsubscribe, N as ScopedRpcFn, Nt as EventsMap, O as DevframeSettings, Ot as AgentToolProviderHandle, P as ScopedServerFunctions, Q as DevframeRpcClientFunctions, R as DevframeHost, S as DevframeServiceOf, St as AgentResource, T as DevframeScopedNodeContext, Tt as AgentTool, U as DevframeDiagnosticsLogger, V as DevframeDiagnosticsDefinition, W as DevframeNodeRpcSession, X as RpcStreamingChannelOptions, Y as RpcStreamingChannel, Z as RpcStreamingHost, _ as ConnectionMeta, a as DevframeDockDefaults, b as DevframeNodeContext, bt as AgentHandle, c as DevframeSetupInfo, d as McpRouteOptions, et as DevframeRpcSharedStates, f as defineDevframe, g as Thenable, h as PartialWithoutId, i as DevframeDeploymentKind, j as ScopedBroadcastOptions, jt as EventEmitter, k as DevframeSettingsRegistry, kt as DevframeAgentHost, l as DevframeSpaOptions, m as EntriesToObject, n as DevframeCliOptions, o as DevframeDuplicationStrategy, q as RpcSharedStateGetOptions, r as DevframeDefinition, s as DevframeRuntime, t as DevframeBrowserContext, u as DevframeWsOptions, v as ConnectionMetaWebsocket, w as DevframeServicesRegistry, wt as AgentResourceInput, x as DevframeServiceId, xt as AgentManifest, y as DevframeCapabilities, z as DevframeStorageScope } from "./devframe-Dsjn_Xtq.mjs";
1
+ import { $ as DevframeRpcServerFunctions, A as DevframeSettingsStore, At as DevframeAgentHostEvents, B as DevframeDefineDiagnosticsOptions, C as DevframeServicesHost, Ct as AgentResourceContent, D as DevframeScopedStreamingHost, Dt as AgentToolProvider, E as DevframeScopedNodeRpc, Et as AgentToolInput, F as ScopedSharedStates, G as RpcBroadcastOptions, H as DevframeDiagnosticsHost, I as SettingsForNamespace, J as RpcSharedStateHost, K as RpcFunctionsHost, L as DevframeViewHost, M as ScopedClientFunctions, Mt as EventUnsubscribe, N as ScopedRpcFn, Nt as EventsMap, O as DevframeSettings, Ot as AgentToolProviderHandle, P as ScopedServerFunctions, Q as DevframeRpcClientFunctions, R as DevframeHost, S as DevframeServiceOf, St as AgentResource, T as DevframeScopedNodeContext, Tt as AgentTool, U as DevframeDiagnosticsLogger, V as DevframeDiagnosticsDefinition, W as DevframeNodeRpcSession, X as RpcStreamingChannelOptions, Y as RpcStreamingChannel, Z as RpcStreamingHost, _ as ConnectionMeta, a as DevframeDockDefaults, b as DevframeNodeContext, bt as AgentHandle, c as DevframeSetupInfo, d as McpRouteOptions, et as DevframeRpcSharedStates, f as defineDevframe, g as Thenable, h as PartialWithoutId, i as DevframeDeploymentKind, j as ScopedBroadcastOptions, jt as EventEmitter, k as DevframeSettingsRegistry, kt as DevframeAgentHost, l as DevframeSpaOptions, m as EntriesToObject, n as DevframeCliOptions, o as DevframeDuplicationStrategy, q as RpcSharedStateGetOptions, r as DevframeDefinition, s as DevframeRuntime, t as DevframeBrowserContext, u as DevframeWsOptions, v as ConnectionMetaWebsocket, w as DevframeServicesRegistry, wt as AgentResourceInput, x as DevframeServiceId, xt as AgentManifest, y as DevframeCapabilities, z as DevframeStorageScope } from "./devframe-qgCKL683.mjs";
2
2
  import { C as RpcFunctionType, T as RpcReturnSchema, _ as RpcFunctionDefinition, g as RpcFunctionAgentOptions, i as RpcArgsSchema } from "./types-CnJSgRVa.mjs";
3
3
  import "./index-Dbw1p5ch.mjs";
4
- import { t as DevframeNodeRpcSessionMeta } from "./ws-server-D_Vjtums.mjs";
4
+ import { n as DevframeNodeRpcSessionMeta } from "./ws-server-J-zkiOyp.mjs";
5
5
  //#region src/define.d.ts
6
6
  declare const defineRpcFunction: <NAME extends string, TYPE extends RpcFunctionType, ARGS extends any[], RETURN = void, const AS extends RpcArgsSchema | undefined = undefined, const RS extends RpcReturnSchema | undefined = undefined>(definition: RpcFunctionDefinition<NAME, TYPE, ARGS, RETURN, AS, RS, DevframeNodeContext>) => RpcFunctionDefinition<NAME, TYPE, ARGS, RETURN, AS, RS, DevframeNodeContext>;
7
7
  //#endregion
@@ -1,3 +1,3 @@
1
- import { p as DevframeAuthHandler } from "../devframe-Dsjn_Xtq.mjs";
2
- import { a as verifyAuthToken, i as refreshTempAuthCode, n as exchangeTempAuthCode, o as revokeActiveConnectionsForToken, r as getTempAuthCode, s as revokeAuthToken, t as buildOtpAuthUrl } from "../index-CeBbry0R.mjs";
1
+ import { p as DevframeAuthHandler } from "../devframe-qgCKL683.mjs";
2
+ import { a as verifyAuthToken, i as refreshTempAuthCode, n as exchangeTempAuthCode, o as revokeActiveConnectionsForToken, r as getTempAuthCode, s as revokeAuthToken, t as buildOtpAuthUrl } from "../index-B7RgLw--.mjs";
3
3
  export { DevframeAuthHandler, buildOtpAuthUrl, exchangeTempAuthCode, getTempAuthCode, refreshTempAuthCode, revokeActiveConnectionsForToken, revokeAuthToken, verifyAuthToken };
@@ -1,3 +1,3 @@
1
- import { n as revokeAuthToken, t as revokeActiveConnectionsForToken } from "../revoke-BtQDKTp7.mjs";
2
- import { a as verifyAuthToken, i as refreshTempAuthCode, n as exchangeTempAuthCode, r as getTempAuthCode, t as buildOtpAuthUrl } from "../state-WX3HT5a3.mjs";
1
+ import { n as revokeAuthToken, t as revokeActiveConnectionsForToken } from "../revoke-DZbJ7Cl0.mjs";
2
+ import { a as verifyAuthToken, i as refreshTempAuthCode, n as exchangeTempAuthCode, r as getTempAuthCode, t as buildOtpAuthUrl } from "../state-BeUHDDjk.mjs";
3
3
  export { buildOtpAuthUrl, exchangeTempAuthCode, getTempAuthCode, refreshTempAuthCode, revokeActiveConnectionsForToken, revokeAuthToken, verifyAuthToken };
@@ -1,5 +1,5 @@
1
- import { i as DevframeDeploymentKind, r as DevframeDefinition } from "../devframe-Dsjn_Xtq.mjs";
2
- import { a as internalContextMap, i as getInternalContext, n as InternalAnonymousAuthStorage, r as RemoteTokenRecord, t as DevframeInternalContext } from "../context-7BbaIUSI.mjs";
1
+ import { i as DevframeDeploymentKind, r as DevframeDefinition } from "../devframe-qgCKL683.mjs";
2
+ import { a as internalContextMap, i as getInternalContext, n as InternalAnonymousAuthStorage, r as RemoteTokenRecord, t as DevframeInternalContext } from "../context-DFzmxCLa.mjs";
3
3
  //#region src/adapters/_shared.d.ts
4
4
  /**
5
5
  * Resolve the mount base path for a devframe's SPA. Hosted adapters
@@ -1,3 +1,3 @@
1
- import { n as internalContextMap, t as getInternalContext } from "../context-C9Cgm1hP.mjs";
1
+ import { n as internalContextMap, t as getInternalContext } from "../context-CQefP2jR.mjs";
2
2
  import { n as resolveBasePath, t as normalizeBasePath } from "../_shared-bWRzeSa0.mjs";
3
3
  export { getInternalContext, internalContextMap, normalizeBasePath, resolveBasePath };
@@ -1,7 +1,7 @@
1
- import { At as DevframeAgentHostEvents, C as DevframeServicesHost, Ct as AgentResourceContent, Dt as AgentToolProvider, Et as AgentToolInput, H as DevframeDiagnosticsHost$1, J as RpcSharedStateHost, K as RpcFunctionsHost, L as DevframeViewHost$1, O as DevframeSettings, Ot as AgentToolProviderHandle, R as DevframeHost, S as DevframeServiceOf, St as AgentResource, T as DevframeScopedNodeContext, Tt as AgentTool, U as DevframeDiagnosticsLogger, Z as RpcStreamingHost, b as DevframeNodeContext, bt as AgentHandle, ht as SharedState, jt as EventEmitter, kt as DevframeAgentHost$1, wt as AgentResourceInput, x as DevframeServiceId, xt as AgentManifest } from "../devframe-Dsjn_Xtq.mjs";
1
+ import { At as DevframeAgentHostEvents, C as DevframeServicesHost, Ct as AgentResourceContent, Dt as AgentToolProvider, Et as AgentToolInput, H as DevframeDiagnosticsHost$1, J as RpcSharedStateHost, K as RpcFunctionsHost, L as DevframeViewHost$1, O as DevframeSettings, Ot as AgentToolProviderHandle, R as DevframeHost, S as DevframeServiceOf, St as AgentResource, T as DevframeScopedNodeContext, Tt as AgentTool, U as DevframeDiagnosticsLogger, Z as RpcStreamingHost, b as DevframeNodeContext, bt as AgentHandle, ht as SharedState, jt as EventEmitter, kt as DevframeAgentHost$1, wt as AgentResourceInput, x as DevframeServiceId, xt as AgentManifest } from "../devframe-qgCKL683.mjs";
2
2
  import { v as RpcFunctionDefinitionAny } from "../types-CnJSgRVa.mjs";
3
3
  import "../index-Dbw1p5ch.mjs";
4
- import { n as StartedServer, r as startHttpAndWs, t as StartHttpAndWsOptions } from "../server-VLQJouOO.mjs";
4
+ import { n as StartedServer, r as startHttpAndWs, t as StartHttpAndWsOptions } from "../server-E9z4ZWbd.mjs";
5
5
  import { BirpcGroup } from "birpc";
6
6
  //#region src/node/agent-args.d.ts
7
7
  /**
@@ -232,6 +232,26 @@ interface DevframeInstanceRegistration {
232
232
  declare function registerDevframeInstance(record: DevframeInstanceRecord, options?: {
233
233
  instancesDir?: string;
234
234
  }): DevframeInstanceRegistration;
235
+ /**
236
+ * Read the registry and split records into live and dead by probing each
237
+ * one's `__connection.json`, deleting dead records (prune-on-read). Live
238
+ * records carry the dialable origin the probe confirmed (a `localhost`
239
+ * record may come back as `127.0.0.1` / `[::1]`).
240
+ *
241
+ * A liveness probe only proves *something* answers on the record's port, so
242
+ * records left behind by killed processes shadow the server currently bound
243
+ * there: per `(port, basePath)` only the newest record survives, older
244
+ * ghosts are pruned with the dead.
245
+ *
246
+ * @experimental
247
+ */
248
+ declare function listLiveDevframeInstances(options?: {
249
+ instancesDir?: string;
250
+ timeoutMs?: number;
251
+ }): Promise<{
252
+ live: DevframeInstanceRecord[];
253
+ pruned: DevframeInstanceRecord[];
254
+ }>;
235
255
  //#endregion
236
256
  //#region src/node/rpc-shared-state.d.ts
237
257
  declare function createRpcSharedStateServerHost(rpc: RpcFunctionsHost): RpcSharedStateHost;
@@ -278,4 +298,4 @@ declare function toDialableHost(host: string): string;
278
298
  declare function formatHostForUrl(host: string): string;
279
299
  declare function normalizeHttpServerUrl(host: string, port: number | string): string;
280
300
  //#endregion
281
- export { AgentArgsFallback, CreateH3DevframeHostOptions, CreateHostContextOptions, CreateStorageOptions, DevframeAgentHost, DevframeDiagnosticsHost, type DevframeInstanceRecord, type DevframeInstanceRegistration, DevframeServicesHostImpl, DevframeViewHost, type RpcFunctionsHost, StartHttpAndWsOptions, StartedServer, coerceAgentPositionalArgs, createH3DevframeHost, createHostContext, createNodeSettings, createRpcSharedStateServerHost, createRpcStreamingServerHost, createScopedNodeContext, createStorage, formatHostForUrl, isObject, normalizeHttpServerUrl, registerDevframeInstance, startHttpAndWs, toDialableHost };
301
+ export { AgentArgsFallback, CreateH3DevframeHostOptions, CreateHostContextOptions, CreateStorageOptions, DevframeAgentHost, DevframeDiagnosticsHost, type DevframeInstanceRecord, type DevframeInstanceRegistration, DevframeServicesHostImpl, DevframeViewHost, type RpcFunctionsHost, StartHttpAndWsOptions, StartedServer, coerceAgentPositionalArgs, createH3DevframeHost, createHostContext, createNodeSettings, createRpcSharedStateServerHost, createRpcStreamingServerHost, createScopedNodeContext, createStorage, formatHostForUrl, isObject, listLiveDevframeInstances, normalizeHttpServerUrl, registerDevframeInstance, startHttpAndWs, toDialableHost };
@@ -1,5 +1,5 @@
1
1
  import { a as DevframeViewHost, c as createRpcSharedStateServerHost, d as coerceAgentPositionalArgs, i as createNodeSettings, l as DevframeDiagnosticsHost, n as createHostContext, o as DevframeServicesHostImpl, r as createScopedNodeContext, s as createRpcStreamingServerHost, t as createH3DevframeHost, u as DevframeAgentHost } from "../host-h3-Kz7t5Xab.mjs";
2
2
  import { t as createStorage } from "../storage-Dzoc3NVC.mjs";
3
- import { r as registerDevframeInstance } from "../instance-registry-D6fxYu36.mjs";
4
- import { a as toDialableHost, i as normalizeHttpServerUrl, n as formatHostForUrl, r as isObject, t as startHttpAndWs } from "../server-CcAPxuoT.mjs";
5
- export { DevframeAgentHost, DevframeDiagnosticsHost, DevframeServicesHostImpl, DevframeViewHost, coerceAgentPositionalArgs, createH3DevframeHost, createHostContext, createNodeSettings, createRpcSharedStateServerHost, createRpcStreamingServerHost, createScopedNodeContext, createStorage, formatHostForUrl, isObject, normalizeHttpServerUrl, registerDevframeInstance, startHttpAndWs, toDialableHost };
3
+ import { r as registerDevframeInstance, t as listLiveDevframeInstances } from "../instance-registry-D6fxYu36.mjs";
4
+ import { a as toDialableHost, i as normalizeHttpServerUrl, n as formatHostForUrl, r as isObject, t as startHttpAndWs } from "../server-CXaDDUl0.mjs";
5
+ export { DevframeAgentHost, DevframeDiagnosticsHost, DevframeServicesHostImpl, DevframeViewHost, coerceAgentPositionalArgs, createH3DevframeHost, createHostContext, createNodeSettings, createRpcSharedStateServerHost, createRpcStreamingServerHost, createScopedNodeContext, createStorage, formatHostForUrl, isObject, listLiveDevframeInstances, normalizeHttpServerUrl, registerDevframeInstance, startHttpAndWs, toDialableHost };
@@ -1,4 +1,4 @@
1
- import "../devframe-Dsjn_Xtq.mjs";
1
+ import "../devframe-qgCKL683.mjs";
2
2
  import { E as Thenable, S as RpcFunctionSetupResult, c as RpcDump, g as RpcFunctionAgentOptions } from "../types-CnJSgRVa.mjs";
3
3
  import "../index-Dbw1p5ch.mjs";
4
4
  import { t as SimpleSchema } from "../simple-schema-BDzLeJDk.mjs";
@@ -1,5 +1,5 @@
1
- import { b as DevframeNodeContext, p as DevframeAuthHandler } from "../devframe-Dsjn_Xtq.mjs";
2
- import "../index-CeBbry0R.mjs";
1
+ import { b as DevframeNodeContext, p as DevframeAuthHandler } from "../devframe-qgCKL683.mjs";
2
+ import "../index-B7RgLw--.mjs";
3
3
  //#region src/recipes/interactive-auth.d.ts
4
4
  interface CreateInteractiveAuthOptions {
5
5
  /**
@@ -1,8 +1,8 @@
1
1
  import { n as colors } from "../diagnostics-reporter-CsIG85Q5.mjs";
2
2
  import { isAnonymousRpcMethod } from "../constants.mjs";
3
3
  import { n as defineRpcFunction } from "../define-BLWPsH6y.mjs";
4
- import { t as getInternalContext } from "../context-C9Cgm1hP.mjs";
5
- import { a as verifyAuthToken, n as exchangeTempAuthCode, r as getTempAuthCode, t as buildOtpAuthUrl } from "../state-WX3HT5a3.mjs";
4
+ import { t as getInternalContext } from "../context-CQefP2jR.mjs";
5
+ import { a as verifyAuthToken, n as exchangeTempAuthCode, r as getTempAuthCode, t as buildOtpAuthUrl } from "../state-BeUHDDjk.mjs";
6
6
  import { t as s } from "../simple-schema-DQPZrAaZ.mjs";
7
7
  //#region src/recipes/interactive-auth.ts
8
8
  function defaultBanner(info) {
@@ -107,16 +107,24 @@ function createInteractiveAuth(context, options = {}) {
107
107
  }
108
108
  function onConnect(peer, session) {
109
109
  let token;
110
+ let requestOrigin;
110
111
  try {
111
112
  token = new URL(peer.request?.url ?? "", "http://localhost").searchParams.get("devframe_auth_token") ?? void 0;
112
113
  } catch {}
114
+ try {
115
+ requestOrigin = peer.request?.headers?.get?.("origin") ?? void 0;
116
+ } catch {}
113
117
  if (!token) return;
114
118
  if (isStaticToken(token)) {
115
119
  session.meta.clientAuthToken = token;
116
120
  session.meta.isTrusted = true;
117
121
  return;
118
122
  }
119
- verifyAuthToken(token, session, storage);
123
+ if (verifyAuthToken(token, session, storage)) return;
124
+ if (internal.isRemoteTokenTrusted(token, requestOrigin)) {
125
+ session.meta.clientAuthToken = token;
126
+ session.meta.isTrusted = true;
127
+ }
120
128
  }
121
129
  function buildOpenUrl(url) {
122
130
  return buildOtpAuthUrl(url);
@@ -0,0 +1,35 @@
1
+ //#region src/node/auth/revoke.ts
2
+ /**
3
+ * Flip `isTrusted` to false on any live WS clients connected with `token`
4
+ * and broadcast the `auth:revoked` event so they can react.
5
+ *
6
+ * Shared between persisted-auth revocation and remote-dock token revocation.
7
+ */
8
+ async function revokeActiveConnectionsForToken(context, token) {
9
+ const rpcHost = context.rpc;
10
+ if (!rpcHost?._rpcGroup) return;
11
+ const affectedSessionIds = /* @__PURE__ */ new Set();
12
+ for (const client of rpcHost._rpcGroup.clients) if (client.$meta.clientAuthToken === token) {
13
+ affectedSessionIds.add(client.$meta.id);
14
+ client.$meta.isTrusted = false;
15
+ client.$meta.clientAuthToken = void 0;
16
+ }
17
+ if (affectedSessionIds.size === 0) return;
18
+ await rpcHost.broadcast({
19
+ method: "devframe:auth:revoked",
20
+ args: [],
21
+ filter: (client) => affectedSessionIds.has(client.$meta.id)
22
+ });
23
+ }
24
+ /**
25
+ * Revoke an auth token: remove from storage and notify all connected clients
26
+ * using this token that they are no longer trusted.
27
+ */
28
+ async function revokeAuthToken(context, storage, token) {
29
+ storage.mutate((state) => {
30
+ delete state.trusted[token];
31
+ });
32
+ await revokeActiveConnectionsForToken(context, token);
33
+ }
34
+ //#endregion
35
+ export { revokeAuthToken as n, revokeActiveConnectionsForToken as t };
@@ -1,2 +1,2 @@
1
- import { a as isAllowedOrigin, i as attachWsRpcTransport, n as WsRpcTransport, o as isLoopbackHostname, r as WsRpcTransportOptions, t as DevframeNodeRpcSessionMeta } from "../../ws-server-D_Vjtums.mjs";
2
- export { DevframeNodeRpcSessionMeta, WsRpcTransport, WsRpcTransportOptions, attachWsRpcTransport, isAllowedOrigin, isLoopbackHostname };
1
+ import { a as WsRpcTransportOptions, c as isAllowedOrigin, i as WsRpcTransport, l as isLoopbackHostname, n as DevframeNodeRpcSessionMeta, o as attachWsRpcTransport, r as WsOriginRegistry, s as createWsOriginRegistry, t as CreateWsOriginRegistryOptions } from "../../ws-server-J-zkiOyp.mjs";
2
+ export { CreateWsOriginRegistryOptions, DevframeNodeRpcSessionMeta, WsOriginRegistry, WsRpcTransport, WsRpcTransportOptions, attachWsRpcTransport, createWsOriginRegistry, isAllowedOrigin, isLoopbackHostname };
@@ -1,9 +1,52 @@
1
+ import "../../constants.mjs";
1
2
  import { n as strictJsonStringify } from "../../serialization-C8Mnw9hK.mjs";
2
3
  import { n as structuredCloneStringify, t as structuredCloneParse } from "../../structured-clone-CbAV5rFI.mjs";
4
+ import { n as randomToken, r as timingSafeEqual } from "../../crypto-token-XCqTSMg9.mjs";
3
5
  import { createServer } from "node:http";
4
6
  import { createServer as createServer$1 } from "node:https";
5
7
  import crossws from "crossws/adapters/node";
6
8
  //#region src/rpc/transports/ws-server.ts
9
+ /**
10
+ * Create a live, token-protected origin allowlist for external browser
11
+ * viewers. Pass it to {@link WsRpcTransportOptions.allowedOrigins}, then use
12
+ * `registerFromUrl()` in the connection metadata handler to authorize a
13
+ * viewer without sharing a mutable array or disabling DNS-rebinding protection.
14
+ */
15
+ function createWsOriginRegistry(options = {}) {
16
+ const token = randomToken();
17
+ const origins = new Set(options.allowedOrigins ?? []);
18
+ function normalizeOrigin(origin) {
19
+ if (!origin) return;
20
+ try {
21
+ const url = new URL(origin);
22
+ const normalized = url.origin === "null" ? `${url.protocol}//${url.host}` : url.origin;
23
+ return origin === normalized ? normalized : void 0;
24
+ } catch {}
25
+ }
26
+ function registerOrigin(origin, candidateToken) {
27
+ const normalized = normalizeOrigin(origin);
28
+ if (!normalized || !candidateToken || !timingSafeEqual(token, candidateToken)) return false;
29
+ if (options.validateOrigin && !options.validateOrigin(normalized)) return false;
30
+ origins.add(normalized);
31
+ return true;
32
+ }
33
+ return {
34
+ token,
35
+ registerFromUrl(url) {
36
+ let parsed;
37
+ try {
38
+ parsed = new URL(url, "http://localhost");
39
+ } catch {
40
+ return;
41
+ }
42
+ const origin = parsed.searchParams.get("devframe_viewer_origin") ?? void 0;
43
+ return registerOrigin(origin, parsed.searchParams.get("devframe_viewer_origin_token") ?? void 0) ? origin : void 0;
44
+ },
45
+ isAllowed(origin) {
46
+ return isAllowedOrigin(origin, [...origins]);
47
+ }
48
+ };
49
+ }
7
50
  let sessionId = 0;
8
51
  const EMPTY_DEFS = /* @__PURE__ */ new Map();
9
52
  function NOOP() {}
@@ -32,6 +75,9 @@ function isAllowedOrigin(origin, allowedOrigins) {
32
75
  return false;
33
76
  }
34
77
  }
78
+ function isWsOriginRegistry(value) {
79
+ return !!value && !Array.isArray(value);
80
+ }
35
81
  /**
36
82
  * Route `upgrade` events on a server to the crossws adapter, optionally
37
83
  * filtered to a single `path`. Non-matching requests are left untouched so
@@ -55,7 +101,8 @@ function routeUpgrades(server, ws, path, destroyUnmatched, allowedOrigins) {
55
101
  return;
56
102
  }
57
103
  }
58
- if (allowedOrigins !== false && !isAllowedOrigin(req.headers.origin, allowedOrigins ?? [])) {
104
+ const originAllowed = isWsOriginRegistry(allowedOrigins) ? allowedOrigins.isAllowed(req.headers.origin) : isAllowedOrigin(req.headers.origin, allowedOrigins || []);
105
+ if (allowedOrigins !== false && !originAllowed) {
59
106
  socket.write("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
60
107
  socket.destroy();
61
108
  return;
@@ -164,4 +211,4 @@ function attachWsRpcTransport(rpcGroup, options = {}) {
164
211
  };
165
212
  }
166
213
  //#endregion
167
- export { attachWsRpcTransport, isAllowedOrigin, isLoopbackHostname };
214
+ export { attachWsRpcTransport, createWsOriginRegistry, isAllowedOrigin, isLoopbackHostname };
@@ -1,7 +1,7 @@
1
1
  import { createRpcServer } from "./rpc/server.mjs";
2
2
  import { attachWsRpcTransport } from "./rpc/transports/ws-server.mjs";
3
3
  import { t as diagnostics } from "./diagnostics-B5-qHeqD.mjs";
4
- import { t as getInternalContext } from "./context-C9Cgm1hP.mjs";
4
+ import { t as getInternalContext } from "./context-CQefP2jR.mjs";
5
5
  import { createServer } from "node:http";
6
6
  import { AsyncLocalStorage } from "node:async_hooks";
7
7
  import { H3, toNodeHandler } from "h3";
@@ -1,5 +1,6 @@
1
- import { $ as DevframeRpcServerFunctions, Q as DevframeRpcClientFunctions, W as DevframeNodeRpcSession, _ as ConnectionMeta, b as DevframeNodeContext, p as DevframeAuthHandler } from "./devframe-Dsjn_Xtq.mjs";
2
- import "./index-CeBbry0R.mjs";
1
+ import { $ as DevframeRpcServerFunctions, Q as DevframeRpcClientFunctions, W as DevframeNodeRpcSession, _ as ConnectionMeta, b as DevframeNodeContext, p as DevframeAuthHandler } from "./devframe-qgCKL683.mjs";
2
+ import { r as WsOriginRegistry } from "./ws-server-J-zkiOyp.mjs";
3
+ import "./index-B7RgLw--.mjs";
3
4
  import { Peer } from "crossws";
4
5
  import { BirpcGroup, EventOptions } from "birpc";
5
6
  import { NodeAdapter } from "crossws/adapters/node";
@@ -91,7 +92,7 @@ interface StartHttpAndWsOptions {
91
92
  * from another host. Pass `false` to disable origin checking entirely
92
93
  * (not recommended). Default: loopback-only.
93
94
  */
94
- allowedOrigins?: readonly string[] | false;
95
+ allowedOrigins?: readonly string[] | WsOriginRegistry | false;
95
96
  /**
96
97
  * Called once the WS server is bound so callers can mount static
97
98
  * handlers whose origin depends on the resolved port, or print their
@@ -1,5 +1,5 @@
1
1
  import { DEVFRAME_OTP_URL_PARAM } from "./constants.mjs";
2
- import { a as timingSafeEqual, i as randomToken, r as randomDigits } from "./revoke-BtQDKTp7.mjs";
2
+ import { n as randomToken, r as timingSafeEqual, t as randomDigits } from "./crypto-token-XCqTSMg9.mjs";
3
3
  //#region src/node/auth/state.ts
4
4
  /** Number of decimal digits in a human-typed one-time authentication code. */
5
5
  const TEMP_AUTH_CODE_LENGTH = 6;
@@ -37,14 +37,21 @@ function refreshTempAuthCode() {
37
37
  return tempAuthCode;
38
38
  }
39
39
  /**
40
- * Build a "magic link" authentication URL that embeds a one-time code (OTP) as
41
- * a query parameter. Opening it authenticates the client without typing — print
42
- * it on startup (devframe stays headless, so the host prints its own banner).
43
- * Defaults to the current code; the link is subject to the same TTL.
40
+ * Build a "magic link" authentication URL that embeds a one-time code (OTP) in
41
+ * the URL **fragment**. Opening it authenticates the client without typing —
42
+ * print it on startup (devframe stays headless, so the host prints its own
43
+ * banner). Defaults to the current code; the link is subject to the same TTL.
44
+ *
45
+ * The code rides the fragment (`#devframe_otp=…`), not the query string, so it
46
+ * is never sent to the server, written to an access log, or leaked in a
47
+ * `Referer` header — the browser client reads it locally (see
48
+ * `consumeOtpFromUrl`). Any existing fragment parameters are preserved.
44
49
  */
45
50
  function buildOtpAuthUrl(baseUrl, code = tempAuthCode) {
46
51
  const url = new URL(baseUrl);
47
- url.searchParams.set(DEVFRAME_OTP_URL_PARAM, code);
52
+ const fragment = new URLSearchParams(url.hash.replace(/^#/, ""));
53
+ fragment.set(DEVFRAME_OTP_URL_PARAM, code);
54
+ url.hash = fragment.toString();
48
55
  return url.href;
49
56
  }
50
57
  /**
@@ -1,4 +1,4 @@
1
- import { $ as DevframeRpcServerFunctions, A as DevframeSettingsStore, At as DevframeAgentHostEvents, B as DevframeDefineDiagnosticsOptions, C as DevframeServicesHost, Ct as AgentResourceContent, D as DevframeScopedStreamingHost, Dt as AgentToolProvider, E as DevframeScopedNodeRpc, Et as AgentToolInput, F as ScopedSharedStates, G as RpcBroadcastOptions, H as DevframeDiagnosticsHost, I as SettingsForNamespace, J as RpcSharedStateHost, K as RpcFunctionsHost, L as DevframeViewHost, M as ScopedClientFunctions, Mt as EventUnsubscribe, N as ScopedRpcFn, Nt as EventsMap, O as DevframeSettings, Ot as AgentToolProviderHandle, P as ScopedServerFunctions, Q as DevframeRpcClientFunctions, R as DevframeHost, S as DevframeServiceOf, St as AgentResource, T as DevframeScopedNodeContext, Tt as AgentTool, U as DevframeDiagnosticsLogger, V as DevframeDiagnosticsDefinition, W as DevframeNodeRpcSession, X as RpcStreamingChannelOptions, Y as RpcStreamingChannel, Z as RpcStreamingHost, _ as ConnectionMeta, a as DevframeDockDefaults, b as DevframeNodeContext, bt as AgentHandle, c as DevframeSetupInfo, d as McpRouteOptions, et as DevframeRpcSharedStates, f as defineDevframe, g as Thenable, h as PartialWithoutId, i as DevframeDeploymentKind, j as ScopedBroadcastOptions, jt as EventEmitter, k as DevframeSettingsRegistry, kt as DevframeAgentHost, l as DevframeSpaOptions, m as EntriesToObject, n as DevframeCliOptions, o as DevframeDuplicationStrategy, q as RpcSharedStateGetOptions, r as DevframeDefinition, s as DevframeRuntime, t as DevframeBrowserContext, u as DevframeWsOptions, v as ConnectionMetaWebsocket, w as DevframeServicesRegistry, wt as AgentResourceInput, x as DevframeServiceId, xt as AgentManifest, y as DevframeCapabilities, z as DevframeStorageScope } from "../devframe-Dsjn_Xtq.mjs";
1
+ import { $ as DevframeRpcServerFunctions, A as DevframeSettingsStore, At as DevframeAgentHostEvents, B as DevframeDefineDiagnosticsOptions, C as DevframeServicesHost, Ct as AgentResourceContent, D as DevframeScopedStreamingHost, Dt as AgentToolProvider, E as DevframeScopedNodeRpc, Et as AgentToolInput, F as ScopedSharedStates, G as RpcBroadcastOptions, H as DevframeDiagnosticsHost, I as SettingsForNamespace, J as RpcSharedStateHost, K as RpcFunctionsHost, L as DevframeViewHost, M as ScopedClientFunctions, Mt as EventUnsubscribe, N as ScopedRpcFn, Nt as EventsMap, O as DevframeSettings, Ot as AgentToolProviderHandle, P as ScopedServerFunctions, Q as DevframeRpcClientFunctions, R as DevframeHost, S as DevframeServiceOf, St as AgentResource, T as DevframeScopedNodeContext, Tt as AgentTool, U as DevframeDiagnosticsLogger, V as DevframeDiagnosticsDefinition, W as DevframeNodeRpcSession, X as RpcStreamingChannelOptions, Y as RpcStreamingChannel, Z as RpcStreamingHost, _ as ConnectionMeta, a as DevframeDockDefaults, b as DevframeNodeContext, bt as AgentHandle, c as DevframeSetupInfo, d as McpRouteOptions, et as DevframeRpcSharedStates, f as defineDevframe, g as Thenable, h as PartialWithoutId, i as DevframeDeploymentKind, j as ScopedBroadcastOptions, jt as EventEmitter, k as DevframeSettingsRegistry, kt as DevframeAgentHost, l as DevframeSpaOptions, m as EntriesToObject, n as DevframeCliOptions, o as DevframeDuplicationStrategy, q as RpcSharedStateGetOptions, r as DevframeDefinition, s as DevframeRuntime, t as DevframeBrowserContext, u as DevframeWsOptions, v as ConnectionMetaWebsocket, w as DevframeServicesRegistry, wt as AgentResourceInput, x as DevframeServiceId, xt as AgentManifest, y as DevframeCapabilities, z as DevframeStorageScope } from "../devframe-qgCKL683.mjs";
2
2
  import { g as RpcFunctionAgentOptions } from "../types-CnJSgRVa.mjs";
3
- import { t as DevframeNodeRpcSessionMeta } from "../ws-server-D_Vjtums.mjs";
3
+ import { n as DevframeNodeRpcSessionMeta } from "../ws-server-J-zkiOyp.mjs";
4
4
  export { AgentHandle, AgentManifest, AgentResource, AgentResourceContent, AgentResourceInput, AgentTool, AgentToolInput, AgentToolProvider, AgentToolProviderHandle, ConnectionMeta, ConnectionMetaWebsocket, DevframeAgentHost, DevframeAgentHostEvents, DevframeBrowserContext, DevframeCapabilities, DevframeCliOptions, DevframeDefineDiagnosticsOptions, DevframeDefinition, DevframeDeploymentKind, DevframeDiagnosticsDefinition, DevframeDiagnosticsHost, DevframeDiagnosticsLogger, DevframeDockDefaults, DevframeDuplicationStrategy, DevframeHost, DevframeNodeContext, DevframeNodeRpcSession, type DevframeNodeRpcSessionMeta, DevframeRpcClientFunctions, DevframeRpcServerFunctions, DevframeRpcSharedStates, DevframeRuntime, DevframeScopedNodeContext, DevframeScopedNodeRpc, DevframeScopedStreamingHost, DevframeServiceId, DevframeServiceOf, DevframeServicesHost, DevframeServicesRegistry, DevframeSettings, DevframeSettingsRegistry, DevframeSettingsStore, DevframeSetupInfo, DevframeSpaOptions, DevframeStorageScope, DevframeViewHost, DevframeWsOptions, EntriesToObject, EventEmitter, EventUnsubscribe, EventsMap, McpRouteOptions, PartialWithoutId, RpcBroadcastOptions, type RpcFunctionAgentOptions, RpcFunctionsHost, RpcSharedStateGetOptions, RpcSharedStateHost, RpcStreamingChannel, RpcStreamingChannelOptions, RpcStreamingHost, ScopedBroadcastOptions, ScopedClientFunctions, ScopedRpcFn, ScopedServerFunctions, ScopedSharedStates, SettingsForNamespace, Thenable, defineDevframe };
@@ -1,4 +1,4 @@
1
- import { Nt as EventsMap, jt as EventEmitter } from "../devframe-Dsjn_Xtq.mjs";
1
+ import { Nt as EventsMap, jt as EventEmitter } from "../devframe-qgCKL683.mjs";
2
2
  //#region src/utils/events.d.ts
3
3
  /**
4
4
  * Create event emitter.
@@ -1,2 +1,2 @@
1
- import { _t as SharedStateOptions, dt as ImmutableArray, ft as ImmutableMap, gt as SharedStateEvents, ht as SharedState, mt as ImmutableSet, pt as ImmutableObject, ut as Immutable, vt as SharedStatePatch, yt as createSharedState } from "../devframe-Dsjn_Xtq.mjs";
1
+ import { _t as SharedStateOptions, dt as ImmutableArray, ft as ImmutableMap, gt as SharedStateEvents, ht as SharedState, mt as ImmutableSet, pt as ImmutableObject, ut as Immutable, vt as SharedStatePatch, yt as createSharedState } from "../devframe-qgCKL683.mjs";
2
2
  export { Immutable, ImmutableArray, ImmutableMap, ImmutableObject, ImmutableSet, SharedState, SharedStateEvents, SharedStateOptions, SharedStatePatch, createSharedState };
@@ -1,2 +1,2 @@
1
- import { at as StreamReader, ct as createStreamReader, it as StreamErrorPayload, lt as createStreamSink, nt as CreateStreamReaderOptions, ot as StreamSink, rt as CreateStreamSinkOptions, st as StreamSinkEvents, tt as BufferedChunk } from "../devframe-Dsjn_Xtq.mjs";
1
+ import { at as StreamReader, ct as createStreamReader, it as StreamErrorPayload, lt as createStreamSink, nt as CreateStreamReaderOptions, ot as StreamSink, rt as CreateStreamSinkOptions, st as StreamSinkEvents, tt as BufferedChunk } from "../devframe-qgCKL683.mjs";
2
2
  export { BufferedChunk, CreateStreamReaderOptions, CreateStreamSinkOptions, StreamErrorPayload, StreamReader, StreamSink, StreamSinkEvents, createStreamReader, createStreamSink };
@@ -62,7 +62,7 @@ interface WsRpcTransportOptions {
62
62
  * Pass `false` to disable origin checking entirely (not recommended).
63
63
  * Default: loopback-only.
64
64
  */
65
- allowedOrigins?: readonly string[] | false;
65
+ allowedOrigins?: readonly string[] | WsOriginRegistry | false;
66
66
  /**
67
67
  * RPC function definitions, used by the per-call wire serializer to
68
68
  * dispatch between strict-JSON and structured-clone encoding based
@@ -79,6 +79,27 @@ interface WsRpcTransportOptions {
79
79
  /** Override the default per-call deserializer. Most callers should leave this unset. */
80
80
  deserialize?: ChannelOptions['deserialize'];
81
81
  }
82
+ interface CreateWsOriginRegistryOptions {
83
+ /** Origins allowed before any external viewers are registered. */
84
+ allowedOrigins?: readonly string[];
85
+ /** Additional validation to run after the registration token is verified. */
86
+ validateOrigin?: (origin: string) => boolean;
87
+ }
88
+ interface WsOriginRegistry {
89
+ /** Registration token to include in connection metadata. */
90
+ readonly token: string;
91
+ /** Read and register an origin from a connection bootstrap URL. */
92
+ registerFromUrl: (url: string) => string | undefined;
93
+ /** Check whether an origin is currently allowed. */
94
+ isAllowed: (origin: string | undefined) => boolean;
95
+ }
96
+ /**
97
+ * Create a live, token-protected origin allowlist for external browser
98
+ * viewers. Pass it to {@link WsRpcTransportOptions.allowedOrigins}, then use
99
+ * `registerFromUrl()` in the connection metadata handler to authorize a
100
+ * viewer without sharing a mutable array or disabling DNS-rebinding protection.
101
+ */
102
+ declare function createWsOriginRegistry(options?: CreateWsOriginRegistryOptions): WsOriginRegistry;
82
103
  interface WsRpcTransport {
83
104
  /**
84
105
  * The crossws node adapter driving the socket — exposes the connected
@@ -115,4 +136,4 @@ declare function isAllowedOrigin(origin: string | undefined, allowedOrigins: rea
115
136
  */
116
137
  declare function attachWsRpcTransport<ClientFunctions extends object, ServerFunctions extends object>(rpcGroup: BirpcGroup<ClientFunctions, ServerFunctions, false>, options?: WsRpcTransportOptions): WsRpcTransport;
117
138
  //#endregion
118
- export { isAllowedOrigin as a, attachWsRpcTransport as i, WsRpcTransport as n, isLoopbackHostname as o, WsRpcTransportOptions as r, DevframeNodeRpcSessionMeta as t };
139
+ export { WsRpcTransportOptions as a, isAllowedOrigin as c, WsRpcTransport as i, isLoopbackHostname as l, DevframeNodeRpcSessionMeta as n, attachWsRpcTransport as o, WsOriginRegistry as r, createWsOriginRegistry as s, CreateWsOriginRegistryOptions as t };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "devframe",
3
3
  "type": "module",
4
- "version": "0.8.0",
4
+ "version": "0.8.1",
5
5
  "description": "Framework for building generic devframes",
6
6
  "author": "Anthony Fu <anthonyfu117@hotmail.com>",
7
7
  "license": "MIT",
@@ -569,10 +569,11 @@ RPC handlers run with the full privileges of the host process, so the boundary t
569
569
  - **`auth` defaults to `true`** — dev-mode connections must authenticate before calls are accepted. Devframe ships the node primitives (`exchangeTempAuthCode`, `verifyAuthToken` in `devframe/node/auth`); the host adapter (e.g. Vite DevTools) provides the interactive `devframe:anonymous:auth` + `devframe:auth:exchange` handlers and auth UI.
570
570
  - **`auth: false` trusts every reachable connection.** Use it only for single-user `localhost` tools. Never combine it with a non-loopback bind host, a tunnel, or a shared/CI environment. The default bind host is already `localhost`.
571
571
  - **Authentication** exchanges a 6-digit one-time code (shown in the developer's terminal) for a node-issued bearer token via `requestTrustWithCode(code)`. The code is single-use, expires in 5 min, compared in constant time, and rotates after repeated failures — show it only in the terminal, never over the network.
572
- - **Magic-link (optional):** print `buildOtpAuthUrl(origin)` — `<origin>/?devframe_otp=<code>`. `connectDevframe` reads the code, exchanges it, and strips it from the URL. Integrations can opt out (`otpParam: false`) and drive it via the exposed `authenticateWithUrlOtp(rpc)` / `consumeOtpFromUrl()` client utilities. Only the single-use code rides the URL, never the bearer; treat the printed link like the code itself. The standalone CLI's `--open` does this automatically via `DevframeAuthHandler.buildOpenUrl` — the launched tab already carries the OTP, no prompt needed.
572
+ - **Magic-link (optional):** print `buildOtpAuthUrl(origin)` — `<origin>/#devframe_otp=<code>`. The code rides the URL **fragment**, which the browser never sends to the server, so it stays out of access logs and `Referer`. `connectDevframe` reads the code, exchanges it, and strips it from the URL. Integrations can opt out (`otpParam: false`) and drive it via the exposed `authenticateWithUrlOtp(rpc)` / `consumeOtpFromUrl()` client utilities. Only the single-use code rides the URL, never the bearer; treat the printed link like the code itself. The standalone CLI's `--open` does this automatically via `DevframeAuthHandler.buildOpenUrl` — the launched tab already carries the OTP, no prompt needed.
573
573
  - **Tokens are secrets.** The bearer token rides the WS URL (`?devframe_auth_token=…`) — serve over `wss://`/`https://` beyond loopback. Never log the token or code, never bake them into build output. Revoke via `revokeAuthToken(...)`; clients drop to untrusted on `devframe:auth:revoked`.
574
574
  - **Authorize handlers.** Any trusted client can call any registered function — validate inputs, and mark state-changing functions `type: 'destructive'` so MCP/agent clients prompt first.
575
- - **Origin-lock remote docks** (`originLock`) so a dock token is honored only from its expected origin.
575
+ - **Origin-lock remote docks** (`originLock`, on by default) so a dock's session token is honored only on a connection whose `Origin` matches the dock — the connect-time gate enforces it.
576
+ - **The MCP route requires an Origin.** The route-based MCP server (`cli.mcp`, `viteDevBridge`/Next handler `mcp`, `createMcpFetchHandler`) rejects `Origin`-less requests — a request must carry a loopback (or allow-listed) `Origin`, so it isn't reachable by an arbitrary local process. `devframe connect` sends each instance's own loopback origin automatically.
576
577
 
577
578
  See [Security](https://devfra.me/security) for the full reference.
578
579