devframe 0.5.4 → 0.6.0-beta.2

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 (84) hide show
  1. package/dist/{_shared-CUFqO4kJ.mjs → _shared-bWRzeSa0.mjs} +2 -3
  2. package/dist/adapters/build.d.mts +1 -1
  3. package/dist/adapters/build.mjs +7 -7
  4. package/dist/adapters/cli.d.mts +1 -1
  5. package/dist/adapters/cli.mjs +2 -2
  6. package/dist/adapters/dev.d.mts +8 -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 +1 -1
  10. package/dist/adapters/mcp.mjs +4 -4
  11. package/dist/client/index.d.mts +148 -6
  12. package/dist/client/index.mjs +257 -44
  13. package/dist/constants.d.mts +17 -1
  14. package/dist/constants.mjs +17 -1
  15. package/dist/context-3x_bgBgZ.mjs +48 -0
  16. package/dist/{context-DTRcO_UH.d.mts → context-BEVByy_Z.d.mts} +1 -1
  17. package/dist/{dev-Cv43GfqM.mjs → dev-BaX8fLH2.mjs} +39 -13
  18. package/dist/{devframe-BuR6n9ZD.d.mts → devframe-D-gr9sBx.d.mts} +365 -15
  19. package/dist/{diagnostics-GitRGKbr.mjs → diagnostics-BXwBQmoN.mjs} +1 -1
  20. package/dist/{dump-9lKIJTLh.mjs → dump-DoXkj4ku.mjs} +1 -1
  21. package/dist/helpers/vite.d.mts +1 -1
  22. package/dist/helpers/vite.mjs +11 -11
  23. package/dist/{host-h3-Dgpgr1Ul.mjs → host-h3-C5SZlk03.mjs} +134 -6
  24. package/dist/{index-C7M1hnvL.d.mts → index-CxaPKDXX.d.mts} +2 -2
  25. package/dist/{index-DH2sBIwd.d.mts → index-DXHWuwJk.d.mts} +1 -1
  26. package/dist/index.d.mts +4 -4
  27. package/dist/index.mjs +1 -1
  28. package/dist/{launch-editor-BbNhtg7b.mjs → launch-editor-DPfltGA4.mjs} +88 -13
  29. package/dist/node/auth.d.mts +43 -23
  30. package/dist/node/auth.mjs +84 -37
  31. package/dist/node/hub-internals.d.mts +2 -2
  32. package/dist/node/hub-internals.mjs +2 -2
  33. package/dist/node/index.d.mts +22 -6
  34. package/dist/node/index.mjs +4 -4
  35. package/dist/recipes/open-helpers.d.mts +1 -1
  36. package/dist/recipes/open-helpers.mjs +3 -3
  37. package/dist/revoke-ENfxWkFK.mjs +79 -0
  38. package/dist/rpc/dump.d.mts +1 -1
  39. package/dist/rpc/dump.mjs +1 -1
  40. package/dist/rpc/index.d.mts +3 -3
  41. package/dist/rpc/index.mjs +4 -4
  42. package/dist/rpc/transports/ws-client.d.mts +1 -1
  43. package/dist/rpc/transports/ws-client.mjs +2 -2
  44. package/dist/rpc/transports/ws-server.d.mts +2 -2
  45. package/dist/rpc/transports/ws-server.mjs +154 -61
  46. package/dist/{serialization-BD_qXGjd.mjs → serialization-DpLXCy13.mjs} +1 -1
  47. package/dist/{server-BBaBJaUL.mjs → server-Dl6TU5LE.mjs} +19 -14
  48. package/dist/server-rX781kz-.d.mts +90 -0
  49. package/dist/{shared-state-u0y123fi.mjs → shared-state-D4PPieA0.mjs} +4 -4
  50. package/dist/{shared-state-BlBNYziY.mjs → storage-l2ezeend.mjs} +128 -6
  51. package/dist/types/index.d.mts +4 -4
  52. package/dist/{types-BkkQ0Txg.d.mts → types-DZEx4ffs.d.mts} +9 -1
  53. package/dist/utils/crypto-token.d.mts +24 -0
  54. package/dist/utils/crypto-token.mjs +45 -0
  55. package/dist/utils/events.d.mts +1 -1
  56. package/dist/utils/hash.mjs +1 -1
  57. package/dist/utils/launch-editor.mjs +1 -1
  58. package/dist/utils/open.mjs +1 -1
  59. package/dist/utils/scope.d.mts +11 -0
  60. package/dist/utils/scope.mjs +15 -0
  61. package/dist/utils/shared-state.d.mts +1 -1
  62. package/dist/utils/shared-state.mjs +1 -1
  63. package/dist/utils/streaming-channel.d.mts +1 -1
  64. package/dist/utils/structured-clone.mjs +1 -1
  65. package/dist/{ws-client-CVYX9niP.d.mts → ws-client-D0z0pOhn.d.mts} +1 -1
  66. package/dist/ws-server-Bc2wBHl-.d.mts +119 -0
  67. package/package.json +12 -7
  68. package/skills/devframe/SKILL.md +123 -23
  69. package/skills/devframe/templates/counter-devframe.ts +14 -4
  70. package/skills/devframe/templates/spa-devframe.ts +13 -3
  71. package/skills/devframe/templates/vite-client.ts +3 -2
  72. package/dist/context-DaKmhhHY.mjs +0 -164
  73. package/dist/revoke-CL0LSAN9.mjs +0 -803
  74. package/dist/server-wHlpcdZ9.d.mts +0 -55
  75. package/dist/utils/human-id.d.mts +0 -8
  76. package/dist/utils/human-id.mjs +0 -769
  77. package/dist/ws-server-C1LjmRnp.d.mts +0 -61
  78. /package/dist/{define-CW9gLnyG.mjs → define-BLWPsH6y.mjs} +0 -0
  79. /package/dist/{diagnostics-reporter-CBBZwoMv.mjs → diagnostics-reporter-CsIG85Q5.mjs} +0 -0
  80. /package/dist/{hash-bwOD8RgU.mjs → hash-DFc5WwhO.mjs} +0 -0
  81. /package/dist/{open-DiQn6zCH.mjs → open-Dusa2Zzd.mjs} +0 -0
  82. /package/dist/{structured-clone-PdCZwt7F.mjs → structured-clone-CeZOHvkd.mjs} +0 -0
  83. /package/dist/{structured-clone-jegjz0hM.mjs → structured-clone-XpHLZ8nr.mjs} +0 -0
  84. /package/dist/{transports-DTFoMUbE.mjs → transports-BmDVn4SF.mjs} +0 -0
@@ -1,17 +1,26 @@
1
- import { t as hash } from "../hash-bwOD8RgU.mjs";
1
+ import { t as hash } from "../hash-DFc5WwhO.mjs";
2
2
  import { colors } from "../utils/colors.mjs";
3
3
  import { createEventEmitter } from "../utils/events.mjs";
4
- import { humanId } from "../utils/human-id.mjs";
5
- import { t as createSharedState } from "../shared-state-u0y123fi.mjs";
6
- import { i as structuredCloneStringify, n as structuredCloneParse, t as structuredCloneDeserialize } from "../structured-clone-PdCZwt7F.mjs";
4
+ import { t as createSharedState } from "../shared-state-D4PPieA0.mjs";
5
+ import { i as structuredCloneStringify, n as structuredCloneParse, t as structuredCloneDeserialize } from "../structured-clone-CeZOHvkd.mjs";
7
6
  import { createStreamReader, createStreamSink } from "../utils/streaming-channel.mjs";
8
7
  import { promiseWithResolver } from "../utils/promise.mjs";
8
+ import { isQualifiedName, qualifyName } from "../utils/scope.mjs";
9
9
  import { defineDiagnostics } from "nostics";
10
10
  import { ansiFormatter } from "nostics/formatters/ansi";
11
+ import { withBase, withProtocol } from "ufo";
11
12
  import { createBirpc } from "birpc";
12
13
  //#region src/constants.ts
13
14
  const DEVFRAME_CONNECTION_META_FILENAME = "__connection.json";
14
15
  const DEVFRAME_RPC_DUMP_MANIFEST_FILENAME = "__rpc-dump/index.json";
16
+ /**
17
+ * Page-URL query parameter carrying a one-time authentication code (OTP) for
18
+ * "magic link" auth. A host can print a link like `<origin>/?devframe_otp=<code>`;
19
+ * the client reads the code, exchanges it for a token, and strips the parameter
20
+ * from the URL. See `buildOtpAuthUrl` (node) and the `authenticateWithUrlOtp` /
21
+ * `consumeOtpFromUrl` client utilities (or `connectDevframe`'s `otpParam`).
22
+ */
23
+ const DEVFRAME_OTP_URL_PARAM = "devframe_otp";
15
24
  //#endregion
16
25
  //#region src/utils/diagnostics-reporter.ts
17
26
  const formatAnsi = ansiFormatter(colors);
@@ -259,6 +268,54 @@ function formatPath(parent, key) {
259
268
  return key;
260
269
  }
261
270
  //#endregion
271
+ //#region src/client/otp.ts
272
+ /**
273
+ * Read a one-time authentication code (OTP) from the current page URL's query
274
+ * string, without side effects. Returns `undefined` when the parameter is absent.
275
+ */
276
+ function readOtpFromUrl(param = DEVFRAME_OTP_URL_PARAM) {
277
+ try {
278
+ return new URLSearchParams(globalThis.location?.search).get(param) || void 0;
279
+ } catch {
280
+ return;
281
+ }
282
+ }
283
+ function stripParamFromUrl(param) {
284
+ try {
285
+ const url = new URL(globalThis.location.href);
286
+ if (!url.searchParams.has(param)) return;
287
+ url.searchParams.delete(param);
288
+ globalThis.history?.replaceState(globalThis.history.state, "", url.href);
289
+ } catch {}
290
+ }
291
+ /**
292
+ * Read the one-time code from the page URL and remove it from the address bar
293
+ * (and the current history entry), so the single-use code isn't left in the
294
+ * URL, browser history, or a `Referer`. Returns the code, or `undefined` when
295
+ * absent.
296
+ */
297
+ function consumeOtpFromUrl(param = DEVFRAME_OTP_URL_PARAM) {
298
+ const code = readOtpFromUrl(param);
299
+ if (code) stripParamFromUrl(param);
300
+ return code;
301
+ }
302
+ /**
303
+ * Consume a one-time code from the page URL (see {@link consumeOtpFromUrl}) and
304
+ * exchange it for a token via the client. Resolves `true` when the client is
305
+ * authenticated (already trusted, or the exchange succeeded), and `false` when
306
+ * no code is present or the exchange failed.
307
+ *
308
+ * Higher-level integrations (e.g. Vite DevTools) that want to drive their own
309
+ * authentication UI can disable `connectDevframe`'s built-in handling with
310
+ * `otpParam: false` and call this — or {@link consumeOtpFromUrl} — themselves.
311
+ */
312
+ async function authenticateWithUrlOtp(rpc, options = {}) {
313
+ const code = consumeOtpFromUrl(options.param ?? "devframe_otp");
314
+ if (!code) return false;
315
+ if (rpc.isTrusted) return true;
316
+ return rpc.requestTrustWithCode(code);
317
+ }
318
+ //#endregion
262
319
  //#region src/client/rpc-shared-state.ts
263
320
  function createRpcSharedStateClientHost(rpc) {
264
321
  const sharedState = /* @__PURE__ */ new Map();
@@ -425,6 +482,7 @@ async function createStaticRpcClientMode(options) {
425
482
  isTrusted: true,
426
483
  requestTrust: async () => true,
427
484
  requestTrustWithToken: async () => true,
485
+ requestTrustWithCode: async () => null,
428
486
  ensureTrusted: async () => true,
429
487
  call: (...args) => staticCaller.call(args[0], args.slice(1)),
430
488
  callEvent: (...args) => staticCaller.callEvent(args[0], args.slice(1)),
@@ -1763,15 +1821,51 @@ function parseUA(uastringOrExtensions, extensions) {
1763
1821
  }
1764
1822
  //#endregion
1765
1823
  //#region src/client/rpc-ws.ts
1766
- function isNumeric(str) {
1767
- if (str == null) return false;
1768
- return `${+str}` === `${str}`;
1824
+ /**
1825
+ * Resolve a {@link ConnectionMeta.websocket} descriptor into a concrete
1826
+ * `ws(s)://` URL.
1827
+ *
1828
+ * The object / relative-path forms connect to the page's own origin (only the
1829
+ * `http`→`ws` protocol swap is applied), resolving the path against where
1830
+ * `__connection.json` was loaded. This is deliberately host-agnostic so the
1831
+ * connection survives a reverse proxy that changes the domain or port — the
1832
+ * client trusts its own location, never a server-baked hostname. An explicit
1833
+ * `port`/`host` (or a full `ws(s)://` URL string) opts into a cross-origin
1834
+ * endpoint, e.g. a side-car server on its own port.
1835
+ */
1836
+ function resolveWsUrl(websocket, metaBaseUrl, loc) {
1837
+ const wsProtocol = loc.protocol === "https:" ? "wss:" : "ws:";
1838
+ const base = (() => {
1839
+ try {
1840
+ return new URL(metaBaseUrl, loc.href);
1841
+ } catch {
1842
+ return new URL(loc.href);
1843
+ }
1844
+ })();
1845
+ if (websocket && typeof websocket === "object") {
1846
+ if (websocket.host != null || websocket.port != null) {
1847
+ const host = websocket.host ?? `${loc.hostname}:${websocket.port}`;
1848
+ const target = new URL(websocket.path ?? "/", `${wsProtocol}//${host}`);
1849
+ target.protocol = wsProtocol;
1850
+ return target.href;
1851
+ }
1852
+ const target = new URL(websocket.path ?? "", base);
1853
+ target.protocol = wsProtocol;
1854
+ return target.href;
1855
+ }
1856
+ if (typeof websocket === "number") return `${wsProtocol}//${loc.hostname}:${websocket}`;
1857
+ const str = websocket ?? "";
1858
+ if (/^wss?:\/\//i.test(str)) return str;
1859
+ if (/^https?:\/\//i.test(str)) return withProtocol(str, /^https/i.test(str) ? "wss://" : "ws://");
1860
+ const target = new URL(str, base);
1861
+ target.protocol = wsProtocol;
1862
+ return target.href;
1769
1863
  }
1770
1864
  function createWsRpcClientMode(options) {
1771
- const { authToken, connectionMeta, events, clientRpc, rpcOptions = {}, wsOptions = {} } = options;
1865
+ const { authToken, connectionMeta, metaBaseUrl, events, clientRpc, rpcOptions = {}, wsOptions = {} } = options;
1772
1866
  let isTrusted = false;
1773
1867
  const trustedPromise = promiseWithResolver();
1774
- const url = isNumeric(connectionMeta.websocket) ? `${location.protocol.replace("http", "ws")}//${location.hostname}:${connectionMeta.websocket}` : connectionMeta.websocket;
1868
+ const url = resolveWsUrl(connectionMeta.websocket, metaBaseUrl ?? "./", location);
1775
1869
  const definitions = /* @__PURE__ */ new Map();
1776
1870
  for (const name of connectionMeta.jsonSerializableMethods ?? []) definitions.set(name, { jsonSerializable: true });
1777
1871
  const serverRpc = createRpcClient(clientRpc.functions, {
@@ -1792,10 +1886,9 @@ function createWsRpcClientMode(options) {
1792
1886
  }
1793
1887
  });
1794
1888
  let currentAuthToken = authToken;
1795
- async function requestTrustWithToken(token) {
1796
- currentAuthToken = token;
1889
+ function describeUA() {
1797
1890
  const info = parseUA(navigator.userAgent);
1798
- const ua = [
1891
+ return [
1799
1892
  info.browser.name,
1800
1893
  info.browser.version,
1801
1894
  "|",
@@ -1803,19 +1896,36 @@ function createWsRpcClientMode(options) {
1803
1896
  info.os.version,
1804
1897
  info.device.type
1805
1898
  ].filter((i) => i).join(" ");
1899
+ }
1900
+ async function requestTrustWithToken(token) {
1901
+ currentAuthToken = token;
1806
1902
  const result = await serverRpc.$call("devframe:anonymous:auth", {
1807
1903
  authToken: token,
1808
- ua,
1904
+ ua: describeUA(),
1809
1905
  origin: location.origin
1810
1906
  });
1811
1907
  isTrusted = result.isTrusted;
1812
- trustedPromise.resolve(isTrusted);
1908
+ if (isTrusted) trustedPromise.resolve(true);
1813
1909
  events.emit("rpc:is-trusted:updated", isTrusted);
1814
1910
  return result.isTrusted;
1815
1911
  }
1912
+ async function requestTrustWithCode(code) {
1913
+ const token = (await serverRpc.$call("devframe:auth:exchange", {
1914
+ code,
1915
+ ua: describeUA(),
1916
+ origin: location.origin
1917
+ }))?.authToken ?? null;
1918
+ if (token) {
1919
+ currentAuthToken = token;
1920
+ isTrusted = true;
1921
+ trustedPromise.resolve(true);
1922
+ events.emit("rpc:is-trusted:updated", true);
1923
+ }
1924
+ return token;
1925
+ }
1816
1926
  async function requestTrust() {
1817
1927
  if (isTrusted) return true;
1818
- return requestTrustWithToken(currentAuthToken);
1928
+ return requestTrustWithToken(currentAuthToken ?? "");
1819
1929
  }
1820
1930
  async function ensureTrusted(timeout = 6e4) {
1821
1931
  if (isTrusted) trustedPromise.resolve(true);
@@ -1835,6 +1945,7 @@ function createWsRpcClientMode(options) {
1835
1945
  },
1836
1946
  requestTrust,
1837
1947
  requestTrustWithToken,
1948
+ requestTrustWithCode,
1838
1949
  ensureTrusted,
1839
1950
  call: (...args) => {
1840
1951
  return serverRpc.$call(...args);
@@ -1848,26 +1959,101 @@ function createWsRpcClientMode(options) {
1848
1959
  };
1849
1960
  }
1850
1961
  //#endregion
1962
+ //#region src/client/settings.ts
1963
+ function createClientSettingsStore(rpc, namespace, scope) {
1964
+ const stateKey = `devframe:settings:${scope}:${namespace}`;
1965
+ let statePromise;
1966
+ function store() {
1967
+ if (!statePromise) statePromise = rpc.sharedState.get(stateKey, { initialValue: {} });
1968
+ return statePromise;
1969
+ }
1970
+ return {
1971
+ async get(key) {
1972
+ return (await store()).value()[key];
1973
+ },
1974
+ async set(key, value) {
1975
+ (await store()).mutate((draft) => {
1976
+ draft[key] = value;
1977
+ });
1978
+ },
1979
+ async delete(key) {
1980
+ (await store()).mutate((draft) => {
1981
+ delete draft[key];
1982
+ });
1983
+ },
1984
+ async all() {
1985
+ return (await store()).value();
1986
+ },
1987
+ async onChange(fn) {
1988
+ return (await store()).on("updated", (full) => fn(full));
1989
+ }
1990
+ };
1991
+ }
1992
+ /**
1993
+ * Build the client-side `settings` surface for a scope namespace. Mirrors
1994
+ * the node-side stores over the shared-state sync protocol.
1995
+ */
1996
+ function createClientSettings(rpc, namespace) {
1997
+ return {
1998
+ global: createClientSettingsStore(rpc, namespace, "global"),
1999
+ project: createClientSettingsStore(rpc, namespace, "project")
2000
+ };
2001
+ }
2002
+ //#endregion
2003
+ //#region src/client/scope.ts
2004
+ /**
2005
+ * Build a namespace-scoped view of a {@link DevframeRpcClient}. Every RPC
2006
+ * id, shared-state key, and streaming channel passed through the returned
2007
+ * `rpc` surface is auto-namespaced with `<namespace>:`.
2008
+ */
2009
+ function createScopedClientContext(rpc, namespace) {
2010
+ return {
2011
+ namespace,
2012
+ base: rpc,
2013
+ rpc: {
2014
+ namespace,
2015
+ register(fn) {
2016
+ if (isQualifiedName(fn.name)) throw new Error(`[devframe] Scoped client RPC registration for namespace "${namespace}" received an already-namespaced function name "${fn.name}". Pass a bare name without a ":" separator.`);
2017
+ rpc.client.register({
2018
+ ...fn,
2019
+ name: `${namespace}:${fn.name}`
2020
+ });
2021
+ },
2022
+ call: ((method, ...args) => rpc.call(qualifyName(namespace, method), ...args)),
2023
+ callEvent: ((method, ...args) => rpc.callEvent(qualifyName(namespace, method), ...args)),
2024
+ callOptional: ((method, ...args) => rpc.callOptional(qualifyName(namespace, method), ...args)),
2025
+ sharedState: ((key, options) => rpc.sharedState.get(qualifyName(namespace, key), options)),
2026
+ streaming: {
2027
+ subscribe: (channel, id, options) => rpc.streaming.subscribe(qualifyName(namespace, channel), id, options),
2028
+ upload: (channel, id) => rpc.streaming.upload(qualifyName(namespace, channel), id)
2029
+ }
2030
+ },
2031
+ settings: createClientSettings(rpc, namespace),
2032
+ scope: rpc.scope
2033
+ };
2034
+ }
2035
+ //#endregion
1851
2036
  //#region src/client/rpc.ts
1852
2037
  const CONNECTION_META_KEY = "__DEVFRAME_CONNECTION_META__";
1853
2038
  const CONNECTION_AUTH_TOKEN_KEY = "__DEVFRAME_CONNECTION_AUTH_TOKEN__";
1854
- function getConnectionAuthTokenFromWindows(userAuthToken) {
2039
+ function getStoredAuthToken(userAuthToken) {
1855
2040
  const getters = [
1856
2041
  () => userAuthToken,
1857
- () => localStorage.getItem(CONNECTION_AUTH_TOKEN_KEY),
2042
+ () => localStorage.getItem(CONNECTION_AUTH_TOKEN_KEY) ?? void 0,
1858
2043
  () => window?.[CONNECTION_AUTH_TOKEN_KEY],
1859
2044
  () => globalThis?.[CONNECTION_AUTH_TOKEN_KEY],
1860
2045
  () => parent.window?.[CONNECTION_AUTH_TOKEN_KEY]
1861
2046
  ];
1862
- let value;
1863
2047
  for (const getter of getters) try {
1864
- value = getter();
1865
- if (value) break;
2048
+ const value = getter();
2049
+ if (value) return value;
1866
2050
  } catch {}
1867
- if (!value) value = humanId();
1868
- localStorage.setItem(CONNECTION_AUTH_TOKEN_KEY, value);
1869
- globalThis[CONNECTION_AUTH_TOKEN_KEY] = value;
1870
- return value;
2051
+ }
2052
+ function persistAuthToken(token) {
2053
+ try {
2054
+ localStorage.setItem(CONNECTION_AUTH_TOKEN_KEY, token);
2055
+ } catch {}
2056
+ globalThis[CONNECTION_AUTH_TOKEN_KEY] = token;
1871
2057
  }
1872
2058
  function findConnectionMetaFromWindows() {
1873
2059
  const getters = [
@@ -1886,18 +2072,18 @@ async function getDevframeRpcClient(options = {}) {
1886
2072
  const bases = Array.isArray(baseURL) ? baseURL : [baseURL];
1887
2073
  let connectionMeta = options.connectionMeta || findConnectionMetaFromWindows();
1888
2074
  let resolvedBaseURL = bases[0] ?? "./";
1889
- function normalizeBase(base) {
1890
- return base.endsWith("/") ? base : `${base}/`;
1891
- }
1892
- function resolveBasePath(base, path) {
1893
- if (/^https?:\/\//.test(path)) return path;
1894
- if (path.startsWith("/")) return path;
1895
- return `${normalizeBase(base)}${path}`;
2075
+ function resolveMetaBaseUrl() {
2076
+ const metaPath = withBase(DEVFRAME_CONNECTION_META_FILENAME, resolvedBaseURL);
2077
+ try {
2078
+ return new URL(metaPath, globalThis.location?.href).href;
2079
+ } catch {
2080
+ return metaPath;
2081
+ }
1896
2082
  }
1897
2083
  if (!connectionMeta) {
1898
2084
  const errors = [];
1899
2085
  for (const base of bases) try {
1900
- connectionMeta = await fetch(resolveBasePath(base, DEVFRAME_CONNECTION_META_FILENAME)).then((r) => r.json());
2086
+ connectionMeta = await fetch(withBase(DEVFRAME_CONNECTION_META_FILENAME, base)).then((r) => r.json());
1901
2087
  resolvedBaseURL = base;
1902
2088
  globalThis[CONNECTION_META_KEY] = connectionMeta;
1903
2089
  break;
@@ -1911,13 +2097,14 @@ async function getDevframeRpcClient(options = {}) {
1911
2097
  ...typeof options.cacheOptions === "object" ? options.cacheOptions : {}
1912
2098
  });
1913
2099
  const context = { rpc: void 0 };
1914
- const authToken = getConnectionAuthTokenFromWindows(options.authToken);
2100
+ const authToken = getStoredAuthToken(options.authToken);
2101
+ if (authToken) persistAuthToken(authToken);
1915
2102
  const clientRpc = new RpcFunctionsCollectorBase(context);
1916
2103
  async function fetchJsonFromBases(path) {
1917
2104
  const candidates = [resolvedBaseURL, ...bases.filter((base) => base !== resolvedBaseURL)].filter((x) => x != null);
1918
2105
  const errors = [];
1919
2106
  for (const base of candidates) try {
1920
- return await fetch(resolveBasePath(base, path)).then((r) => {
2107
+ return await fetch(withBase(path, base)).then((r) => {
1921
2108
  if (!r.ok) throw new Error(`Failed to fetch ${path} from ${base}: ${r.status}`);
1922
2109
  return r.json();
1923
2110
  });
@@ -1929,6 +2116,7 @@ async function getDevframeRpcClient(options = {}) {
1929
2116
  const mode = connectionMeta.backend === "static" ? await createStaticRpcClientMode({ fetchJsonFromBases }) : createWsRpcClientMode({
1930
2117
  authToken,
1931
2118
  connectionMeta,
2119
+ metaBaseUrl: resolveMetaBaseUrl(),
1932
2120
  events,
1933
2121
  clientRpc,
1934
2122
  rpcOptions: {
@@ -1947,6 +2135,10 @@ async function getDevframeRpcClient(options = {}) {
1947
2135
  },
1948
2136
  wsOptions: options.wsOptions
1949
2137
  });
2138
+ let authChannel;
2139
+ try {
2140
+ authChannel = new BroadcastChannel("devframe-auth");
2141
+ } catch {}
1950
2142
  const rpc = {
1951
2143
  events,
1952
2144
  get isTrusted() {
@@ -1956,32 +2148,53 @@ async function getDevframeRpcClient(options = {}) {
1956
2148
  ensureTrusted: mode.ensureTrusted,
1957
2149
  requestTrust: mode.requestTrust,
1958
2150
  requestTrustWithToken: async (token) => {
1959
- localStorage.setItem(CONNECTION_AUTH_TOKEN_KEY, token);
1960
- globalThis[CONNECTION_AUTH_TOKEN_KEY] = token;
2151
+ persistAuthToken(token);
1961
2152
  return mode.requestTrustWithToken(token);
1962
2153
  },
2154
+ requestTrustWithCode: async (code) => {
2155
+ const token = await mode.requestTrustWithCode(code);
2156
+ if (!token) return false;
2157
+ persistAuthToken(token);
2158
+ try {
2159
+ authChannel?.postMessage({
2160
+ type: "auth-update",
2161
+ authToken: token
2162
+ });
2163
+ } catch {}
2164
+ return true;
2165
+ },
1963
2166
  call: mode.call,
1964
2167
  callEvent: mode.callEvent,
1965
2168
  callOptional: mode.callOptional,
1966
2169
  client: clientRpc,
1967
2170
  sharedState: void 0,
1968
2171
  streaming: void 0,
1969
- cacheManager
2172
+ cacheManager,
2173
+ scope: void 0
1970
2174
  };
1971
2175
  rpc.sharedState = createRpcSharedStateClientHost(rpc);
1972
2176
  rpc.streaming = createRpcStreamingClientHost(rpc);
2177
+ const scopedCache = /* @__PURE__ */ new Map();
2178
+ rpc.scope = ((namespace) => {
2179
+ if (!namespace) return rpc;
2180
+ let scoped = scopedCache.get(namespace);
2181
+ if (!scoped) {
2182
+ scoped = createScopedClientContext(rpc, namespace);
2183
+ scopedCache.set(namespace, scoped);
2184
+ }
2185
+ return scoped;
2186
+ });
1973
2187
  context.rpc = rpc;
1974
2188
  mode.requestTrust();
1975
- try {
1976
- const bc = new BroadcastChannel("devframe-auth");
1977
- bc.onmessage = (event) => {
1978
- if (event.data?.type === "auth-update" && event.data.authToken) rpc.requestTrustWithToken(event.data.authToken);
1979
- };
1980
- } catch {}
2189
+ const otpParam = options.otpParam ?? "devframe_otp";
2190
+ if (otpParam) authenticateWithUrlOtp(rpc, { param: otpParam });
2191
+ if (authChannel) authChannel.onmessage = (event) => {
2192
+ if (event.data?.type === "auth-update" && event.data.authToken) rpc.requestTrustWithToken(event.data.authToken);
2193
+ };
1981
2194
  return rpc;
1982
2195
  }
1983
2196
  //#endregion
1984
2197
  //#region src/client/index.ts
1985
2198
  const connectDevframe = getDevframeRpcClient;
1986
2199
  //#endregion
1987
- export { connectDevframe, createRpcStreamingClientHost, getDevframeRpcClient };
2200
+ export { authenticateWithUrlOtp, connectDevframe, consumeOtpFromUrl, createClientSettings, createRpcStreamingClientHost, createScopedClientContext, getDevframeRpcClient, readOtpFromUrl };
@@ -3,6 +3,14 @@ declare const DEVFRAME_MOUNT_PATH = "/__devframe/";
3
3
  declare const DEVFRAME_MOUNT_PATH_NO_TRAILING_SLASH = "/__devframe";
4
4
  declare const DEVFRAME_DIRNAME = "__devframe";
5
5
  declare const DEVFRAME_CONNECTION_META_FILENAME = "__connection.json";
6
+ /**
7
+ * Route the WebSocket RPC endpoint is bound to, relative to a devframe's
8
+ * base path. Sits next to `__connection.json` so the deployed SPA can reach
9
+ * it on the same origin it loaded from — the dev server shares one port for
10
+ * both HTTP and WS, and a host server (Vite, etc.) can mount the WS upgrade
11
+ * handler here without colliding with its own routes (HMR, asset serving).
12
+ */
13
+ declare const DEVFRAME_WS_ROUTE = "__devframe_ws";
6
14
  declare const DEVFRAME_RPC_DUMP_MANIFEST_FILENAME = "__rpc-dump/index.json";
7
15
  declare const DEVFRAME_DOCK_IMPORTS_FILENAME = "__client-imports.js";
8
16
  declare const DEVFRAME_DOCK_IMPORTS_VIRTUAL_ID = "/__devframe-client-imports.js";
@@ -13,5 +21,13 @@ declare const DEVFRAME_RPC_DUMP_DIRNAME = "__rpc-dump";
13
21
  * `@vitejs/devtools-kit`) injected into remote-UI iframe dock URLs.
14
22
  */
15
23
  declare const REMOTE_CONNECTION_KEY = "devframe-remote-connection";
24
+ /**
25
+ * Page-URL query parameter carrying a one-time authentication code (OTP) for
26
+ * "magic link" auth. A host can print a link like `<origin>/?devframe_otp=<code>`;
27
+ * the client reads the code, exchanges it for a token, and strips the parameter
28
+ * from the URL. See `buildOtpAuthUrl` (node) and the `authenticateWithUrlOtp` /
29
+ * `consumeOtpFromUrl` client utilities (or `connectDevframe`'s `otpParam`).
30
+ */
31
+ declare const DEVFRAME_OTP_URL_PARAM = "devframe_otp";
16
32
  //#endregion
17
- export { DEVFRAME_CONNECTION_META_FILENAME, DEVFRAME_DIRNAME, DEVFRAME_DOCK_IMPORTS_FILENAME, DEVFRAME_DOCK_IMPORTS_VIRTUAL_ID, DEVFRAME_MOUNT_PATH, DEVFRAME_MOUNT_PATH_NO_TRAILING_SLASH, DEVFRAME_RPC_DUMP_DIRNAME, DEVFRAME_RPC_DUMP_MANIFEST_FILENAME, REMOTE_CONNECTION_KEY };
33
+ export { DEVFRAME_CONNECTION_META_FILENAME, DEVFRAME_DIRNAME, DEVFRAME_DOCK_IMPORTS_FILENAME, DEVFRAME_DOCK_IMPORTS_VIRTUAL_ID, 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 };
@@ -3,6 +3,14 @@ const DEVFRAME_MOUNT_PATH = "/__devframe/";
3
3
  const DEVFRAME_MOUNT_PATH_NO_TRAILING_SLASH = "/__devframe";
4
4
  const DEVFRAME_DIRNAME = "__devframe";
5
5
  const DEVFRAME_CONNECTION_META_FILENAME = "__connection.json";
6
+ /**
7
+ * Route the WebSocket RPC endpoint is bound to, relative to a devframe's
8
+ * base path. Sits next to `__connection.json` so the deployed SPA can reach
9
+ * it on the same origin it loaded from — the dev server shares one port for
10
+ * both HTTP and WS, and a host server (Vite, etc.) can mount the WS upgrade
11
+ * handler here without colliding with its own routes (HMR, asset serving).
12
+ */
13
+ const DEVFRAME_WS_ROUTE = "__devframe_ws";
6
14
  const DEVFRAME_RPC_DUMP_MANIFEST_FILENAME = "__rpc-dump/index.json";
7
15
  const DEVFRAME_DOCK_IMPORTS_FILENAME = "__client-imports.js";
8
16
  const DEVFRAME_DOCK_IMPORTS_VIRTUAL_ID = "/__devframe-client-imports.js";
@@ -13,5 +21,13 @@ const DEVFRAME_RPC_DUMP_DIRNAME = "__rpc-dump";
13
21
  * `@vitejs/devtools-kit`) injected into remote-UI iframe dock URLs.
14
22
  */
15
23
  const REMOTE_CONNECTION_KEY = "devframe-remote-connection";
24
+ /**
25
+ * Page-URL query parameter carrying a one-time authentication code (OTP) for
26
+ * "magic link" auth. A host can print a link like `<origin>/?devframe_otp=<code>`;
27
+ * the client reads the code, exchanges it for a token, and strips the parameter
28
+ * from the URL. See `buildOtpAuthUrl` (node) and the `authenticateWithUrlOtp` /
29
+ * `consumeOtpFromUrl` client utilities (or `connectDevframe`'s `otpParam`).
30
+ */
31
+ const DEVFRAME_OTP_URL_PARAM = "devframe_otp";
16
32
  //#endregion
17
- export { DEVFRAME_CONNECTION_META_FILENAME, DEVFRAME_DIRNAME, DEVFRAME_DOCK_IMPORTS_FILENAME, DEVFRAME_DOCK_IMPORTS_VIRTUAL_ID, DEVFRAME_MOUNT_PATH, DEVFRAME_MOUNT_PATH_NO_TRAILING_SLASH, DEVFRAME_RPC_DUMP_DIRNAME, DEVFRAME_RPC_DUMP_MANIFEST_FILENAME, REMOTE_CONNECTION_KEY };
33
+ export { DEVFRAME_CONNECTION_META_FILENAME, DEVFRAME_DIRNAME, DEVFRAME_DOCK_IMPORTS_FILENAME, DEVFRAME_DOCK_IMPORTS_VIRTUAL_ID, 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 };
@@ -0,0 +1,48 @@
1
+ import { t as createStorage } from "./storage-l2ezeend.mjs";
2
+ import { i as randomToken, n as revokeAuthToken, t as revokeActiveConnectionsForToken } from "./revoke-ENfxWkFK.mjs";
3
+ import { join } from "pathe";
4
+ //#region src/node/hub-internals/context.ts
5
+ const internalContextMap = /* @__PURE__ */ new WeakMap();
6
+ function getInternalContext(context) {
7
+ if (!internalContextMap.has(context)) {
8
+ const storage = createStorage({
9
+ filepath: join(context.host.getStorageDir("global"), "auth.json"),
10
+ initialValue: { trusted: {} }
11
+ });
12
+ const remoteTokens = /* @__PURE__ */ new Map();
13
+ function revokeRemoteToken(token) {
14
+ if (!remoteTokens.delete(token)) return;
15
+ revokeActiveConnectionsForToken(context, token);
16
+ }
17
+ const internalContext = {
18
+ storage: { auth: storage },
19
+ revokeAuthToken: (token) => revokeAuthToken(context, storage, token),
20
+ remoteTokens,
21
+ allocateRemoteToken(dockId, origin, originLock) {
22
+ const token = randomToken();
23
+ remoteTokens.set(token, {
24
+ dockId,
25
+ origin,
26
+ originLock
27
+ });
28
+ return token;
29
+ },
30
+ revokeRemoteToken,
31
+ revokeRemoteTokensForDock(dockId) {
32
+ const tokensToRevoke = [];
33
+ for (const [token, record] of remoteTokens) if (record.dockId === dockId) tokensToRevoke.push(token);
34
+ for (const token of tokensToRevoke) revokeRemoteToken(token);
35
+ },
36
+ isRemoteTokenTrusted(token, requestOrigin) {
37
+ const record = remoteTokens.get(token);
38
+ if (!record) return false;
39
+ if (!record.originLock) return true;
40
+ return !!requestOrigin && record.origin === requestOrigin;
41
+ }
42
+ };
43
+ internalContextMap.set(context, internalContext);
44
+ }
45
+ return internalContextMap.get(context);
46
+ }
47
+ //#endregion
48
+ export { internalContextMap as n, getInternalContext as t };
@@ -1,4 +1,4 @@
1
- import { P as SharedState, d as DevframeNodeContext } from "./devframe-BuR6n9ZD.mjs";
1
+ import { L as SharedState, m as DevframeNodeContext } from "./devframe-D-gr9sBx.mjs";
2
2
 
3
3
  //#region src/node/hub-internals/context.d.ts
4
4
  interface InternalAnonymousAuthStorage {
@@ -1,14 +1,15 @@
1
1
  import { DEVFRAME_CONNECTION_META_FILENAME } from "./constants.mjs";
2
- import { n as createHostContext, t as createH3DevframeHost } from "./host-h3-Dgpgr1Ul.mjs";
3
- import { t as startHttpAndWs } from "./server-BBaBJaUL.mjs";
4
- import { n as resolveBasePath, t as normalizeBasePath } from "./_shared-CUFqO4kJ.mjs";
5
- import { t as open } from "./open-DiQn6zCH.mjs";
2
+ import { n as createHostContext, t as createH3DevframeHost } from "./host-h3-C5SZlk03.mjs";
3
+ import { t as startHttpAndWs } from "./server-Dl6TU5LE.mjs";
4
+ import { n as resolveBasePath, t as normalizeBasePath } from "./_shared-bWRzeSa0.mjs";
5
+ import { t as open } from "./open-Dusa2Zzd.mjs";
6
6
  import { mountStaticHandler } from "./utils/serve-static.mjs";
7
+ import { resolve } from "pathe";
7
8
  import { networkInterfaces } from "node:os";
8
9
  import process$1 from "node:process";
9
- import { resolve } from "pathe";
10
10
  import { H3 } from "h3";
11
11
  import { createServer } from "node:net";
12
+ import { joinURL, withBase as withBase$1, withLeadingSlash, withoutLeadingSlash } from "ufo";
12
13
  //#region ../../node_modules/.pnpm/get-port-please@3.2.0/node_modules/get-port-please/dist/index.mjs
13
14
  const unsafePorts = /* @__PURE__ */ new Set([
14
15
  1,
@@ -283,10 +284,11 @@ async function createDevServer(def, options = {}) {
283
284
  });
284
285
  const setupInfo = { flags };
285
286
  await def.setup(ctx, setupInfo);
286
- const connectionMetaPath = `${basePath}${DEVFRAME_CONNECTION_META_FILENAME}`;
287
+ const { bindPath, wsPort, meta } = resolveWsConnection(def, options, basePath);
288
+ const connectionMetaPath = joinURL(basePath, DEVFRAME_CONNECTION_META_FILENAME);
287
289
  app.use(connectionMetaPath, () => ({
288
290
  backend: "websocket",
289
- websocket: port
291
+ websocket: meta
290
292
  }));
291
293
  if (distDir) mountStaticHandler(app, basePath, resolve(distDir));
292
294
  return startHttpAndWs({
@@ -294,6 +296,8 @@ async function createDevServer(def, options = {}) {
294
296
  host,
295
297
  port,
296
298
  app,
299
+ path: bindPath,
300
+ wsPort,
297
301
  auth: def.cli?.auth,
298
302
  onReady: async (info) => {
299
303
  await options.onReady?.(info);
@@ -301,20 +305,42 @@ async function createDevServer(def, options = {}) {
301
305
  }
302
306
  });
303
307
  }
308
+ /**
309
+ * Resolve the three WS connection scenarios from the definition / call-site
310
+ * config into a concrete server bind path, optional dedicated port, and the
311
+ * `__connection.json` descriptor the browser resolves.
312
+ */
313
+ function resolveWsConnection(def, options, basePath) {
314
+ const ws = options.ws ?? def.cli?.ws ?? {};
315
+ const route = withoutLeadingSlash(ws.route ?? "__devframe_ws");
316
+ if (ws.url) return {
317
+ bindPath: joinURL(basePath, route),
318
+ wsPort: void 0,
319
+ meta: ws.url
320
+ };
321
+ if (ws.port != null) return {
322
+ bindPath: withLeadingSlash(route),
323
+ wsPort: ws.port,
324
+ meta: {
325
+ port: ws.port,
326
+ path: route
327
+ }
328
+ };
329
+ return {
330
+ bindPath: joinURL(basePath, route),
331
+ wsPort: void 0,
332
+ meta: { path: route }
333
+ };
334
+ }
304
335
  async function maybeOpenBrowser(def, flags, origin, override) {
305
336
  const flagsOpen = flags.open;
306
337
  const cliOpen = def.cli?.open;
307
338
  const resolved = override ?? flagsOpen ?? cliOpen;
308
339
  if (resolved === void 0 || resolved === false) return;
309
- const target = typeof resolved === "string" ? resolveOpenTarget(origin, resolved) : origin;
340
+ const target = typeof resolved === "string" ? withBase$1(resolved, origin) : origin;
310
341
  try {
311
342
  await open(target);
312
343
  } catch {}
313
344
  }
314
- function resolveOpenTarget(origin, target) {
315
- if (/^https?:/.test(target)) return target;
316
- if (target.startsWith("/")) return origin.replace(/\/$/, "") + target;
317
- return origin.replace(/\/$/, "") + (target ? `/${target}` : "");
318
- }
319
345
  //#endregion
320
346
  export { resolveDevServerPort as n, createDevServer as t };