devframe 0.5.3 → 0.6.0-beta.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 (83) 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 +272 -49
  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-o-TwncyP.d.mts} +1 -1
  17. package/dist/{dev-Cv43GfqM.mjs → dev-CSpLSEVh.mjs} +39 -13
  18. package/dist/{devframe-BuR6n9ZD.d.mts → devframe-BwfavB78.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/node/auth.d.mts +43 -23
  29. package/dist/node/auth.mjs +84 -37
  30. package/dist/node/hub-internals.d.mts +2 -2
  31. package/dist/node/hub-internals.mjs +2 -2
  32. package/dist/node/index.d.mts +22 -6
  33. package/dist/node/index.mjs +4 -4
  34. package/dist/recipes/open-helpers.d.mts +1 -1
  35. package/dist/recipes/open-helpers.mjs +3 -3
  36. package/dist/revoke-ENfxWkFK.mjs +79 -0
  37. package/dist/rpc/dump.d.mts +1 -1
  38. package/dist/rpc/dump.mjs +1 -1
  39. package/dist/rpc/index.d.mts +3 -3
  40. package/dist/rpc/index.mjs +4 -4
  41. package/dist/rpc/transports/ws-client.d.mts +1 -1
  42. package/dist/rpc/transports/ws-client.mjs +2 -2
  43. package/dist/rpc/transports/ws-server.d.mts +2 -2
  44. package/dist/rpc/transports/ws-server.mjs +124 -60
  45. package/dist/{serialization-BD_qXGjd.mjs → serialization-DpLXCy13.mjs} +1 -1
  46. package/dist/{server-wHlpcdZ9.d.mts → server-oju7PTi2.d.mts} +30 -3
  47. package/dist/{server-BBaBJaUL.mjs → server-wyY3zh67.mjs} +18 -14
  48. package/dist/{shared-state-u0y123fi.mjs → shared-state-D4PPieA0.mjs} +4 -4
  49. package/dist/{shared-state-BlBNYziY.mjs → storage-l2ezeend.mjs} +128 -6
  50. package/dist/types/index.d.mts +4 -4
  51. package/dist/{types-BkkQ0Txg.d.mts → types-DZEx4ffs.d.mts} +9 -1
  52. package/dist/utils/crypto-token.d.mts +24 -0
  53. package/dist/utils/crypto-token.mjs +45 -0
  54. package/dist/utils/events.d.mts +1 -1
  55. package/dist/utils/hash.mjs +1 -1
  56. package/dist/utils/launch-editor.mjs +1 -1
  57. package/dist/utils/open.mjs +1 -1
  58. package/dist/utils/scope.d.mts +11 -0
  59. package/dist/utils/scope.mjs +15 -0
  60. package/dist/utils/shared-state.d.mts +1 -1
  61. package/dist/utils/shared-state.mjs +1 -1
  62. package/dist/utils/streaming-channel.d.mts +1 -1
  63. package/dist/utils/structured-clone.mjs +1 -1
  64. package/dist/{ws-client-CVYX9niP.d.mts → ws-client-D0z0pOhn.d.mts} +1 -1
  65. package/dist/ws-server-BVVMDo2n.d.mts +103 -0
  66. package/package.json +12 -7
  67. package/skills/devframe/SKILL.md +123 -23
  68. package/skills/devframe/templates/counter-devframe.ts +14 -4
  69. package/skills/devframe/templates/spa-devframe.ts +13 -3
  70. package/skills/devframe/templates/vite-client.ts +3 -2
  71. package/dist/context-DaKmhhHY.mjs +0 -164
  72. package/dist/revoke-CL0LSAN9.mjs +0 -803
  73. package/dist/utils/human-id.d.mts +0 -8
  74. package/dist/utils/human-id.mjs +0 -769
  75. package/dist/ws-server-C1LjmRnp.d.mts +0 -61
  76. /package/dist/{define-CW9gLnyG.mjs → define-BLWPsH6y.mjs} +0 -0
  77. /package/dist/{diagnostics-reporter-CBBZwoMv.mjs → diagnostics-reporter-CsIG85Q5.mjs} +0 -0
  78. /package/dist/{hash-bwOD8RgU.mjs → hash-DFc5WwhO.mjs} +0 -0
  79. /package/dist/{launch-editor-BbNhtg7b.mjs → launch-editor-CgXBgviI.mjs} +0 -0
  80. /package/dist/{open-DiQn6zCH.mjs → open-Dusa2Zzd.mjs} +0 -0
  81. /package/dist/{structured-clone-PdCZwt7F.mjs → structured-clone-CeZOHvkd.mjs} +0 -0
  82. /package/dist/{structured-clone-jegjz0hM.mjs → structured-clone-XpHLZ8nr.mjs} +0 -0
  83. /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();
@@ -364,28 +421,38 @@ function resolveRecordOutput(record) {
364
421
  if (record.error) throw reviveDumpError(record.error);
365
422
  return record.output;
366
423
  }
424
+ function hasMeaningfulArgs(args) {
425
+ return args.some((arg) => arg !== null && arg !== void 0);
426
+ }
427
+ function unwrapEnvelope(raw) {
428
+ if (raw !== null && typeof raw === "object" && "serialization" in raw && "data" in raw) return raw.data;
429
+ return raw;
430
+ }
367
431
  function createStaticRpcCaller(manifest, fetchJson) {
368
432
  const staticCache = /* @__PURE__ */ new Map();
369
433
  const queryRecordCache = /* @__PURE__ */ new Map();
370
434
  function reviveIfStructuredClone(value, serialization) {
371
- if (serialization === "structured-clone") return structuredCloneDeserialize(value);
435
+ if (serialization === "structured-clone" && Array.isArray(value)) return structuredCloneDeserialize(value);
372
436
  return value;
373
437
  }
438
+ function decode(raw, serialization) {
439
+ return reviveIfStructuredClone(unwrapEnvelope(raw), serialization);
440
+ }
374
441
  async function loadStatic(entry) {
375
- if (!staticCache.has(entry.path)) staticCache.set(entry.path, fetchJson(entry.path).then((raw) => reviveIfStructuredClone(raw, entry.serialization)));
442
+ if (!staticCache.has(entry.path)) staticCache.set(entry.path, fetchJson(entry.path).then((raw) => decode(raw, entry.serialization)));
376
443
  const data = await staticCache.get(entry.path);
377
444
  if (isRecord(data)) return resolveRecordOutput(data);
378
445
  return data;
379
446
  }
380
447
  async function loadQueryRecord(path, serialization) {
381
- if (!queryRecordCache.has(path)) queryRecordCache.set(path, fetchJson(path).then((raw) => reviveIfStructuredClone(raw, serialization)));
448
+ if (!queryRecordCache.has(path)) queryRecordCache.set(path, fetchJson(path).then((raw) => decode(raw, serialization)));
382
449
  return await queryRecordCache.get(path);
383
450
  }
384
451
  async function call(functionName, args) {
385
452
  if (!(functionName in manifest)) throw new Error(`[devframe-rpc] Function "${functionName}" not found in dump store`);
386
453
  const entry = manifest[functionName];
387
454
  if (isStaticEntry(entry)) {
388
- if (args.length > 0) throw new Error(`[devframe-rpc] No dump match for "${functionName}" with args: ${JSON.stringify(args)}`);
455
+ if (hasMeaningfulArgs(args)) throw new Error(`[devframe-rpc] No dump match for "${functionName}" with args: ${JSON.stringify(args)}`);
389
456
  return await loadStatic(entry);
390
457
  }
391
458
  if (isQueryEntry(entry)) {
@@ -395,7 +462,7 @@ function createStaticRpcCaller(manifest, fetchJson) {
395
462
  if (entry.fallback) return resolveRecordOutput(await loadQueryRecord(entry.fallback, entry.serialization));
396
463
  throw new Error(`[devframe-rpc] No dump match for "${functionName}" with args: ${JSON.stringify(args)}`);
397
464
  }
398
- if (args.length === 0) return entry;
465
+ if (!hasMeaningfulArgs(args)) return entry;
399
466
  throw new Error(`[devframe-rpc] No dump match for "${functionName}" with args: ${JSON.stringify(args)}`);
400
467
  }
401
468
  return {
@@ -415,6 +482,7 @@ async function createStaticRpcClientMode(options) {
415
482
  isTrusted: true,
416
483
  requestTrust: async () => true,
417
484
  requestTrustWithToken: async () => true,
485
+ requestTrustWithCode: async () => null,
418
486
  ensureTrusted: async () => true,
419
487
  call: (...args) => staticCaller.call(args[0], args.slice(1)),
420
488
  callEvent: (...args) => staticCaller.callEvent(args[0], args.slice(1)),
@@ -1753,15 +1821,51 @@ function parseUA(uastringOrExtensions, extensions) {
1753
1821
  }
1754
1822
  //#endregion
1755
1823
  //#region src/client/rpc-ws.ts
1756
- function isNumeric(str) {
1757
- if (str == null) return false;
1758
- 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;
1759
1863
  }
1760
1864
  function createWsRpcClientMode(options) {
1761
- const { authToken, connectionMeta, events, clientRpc, rpcOptions = {}, wsOptions = {} } = options;
1865
+ const { authToken, connectionMeta, metaBaseUrl, events, clientRpc, rpcOptions = {}, wsOptions = {} } = options;
1762
1866
  let isTrusted = false;
1763
1867
  const trustedPromise = promiseWithResolver();
1764
- const url = isNumeric(connectionMeta.websocket) ? `${location.protocol.replace("http", "ws")}//${location.hostname}:${connectionMeta.websocket}` : connectionMeta.websocket;
1868
+ const url = resolveWsUrl(connectionMeta.websocket, metaBaseUrl ?? "./", location);
1765
1869
  const definitions = /* @__PURE__ */ new Map();
1766
1870
  for (const name of connectionMeta.jsonSerializableMethods ?? []) definitions.set(name, { jsonSerializable: true });
1767
1871
  const serverRpc = createRpcClient(clientRpc.functions, {
@@ -1782,10 +1886,9 @@ function createWsRpcClientMode(options) {
1782
1886
  }
1783
1887
  });
1784
1888
  let currentAuthToken = authToken;
1785
- async function requestTrustWithToken(token) {
1786
- currentAuthToken = token;
1889
+ function describeUA() {
1787
1890
  const info = parseUA(navigator.userAgent);
1788
- const ua = [
1891
+ return [
1789
1892
  info.browser.name,
1790
1893
  info.browser.version,
1791
1894
  "|",
@@ -1793,19 +1896,36 @@ function createWsRpcClientMode(options) {
1793
1896
  info.os.version,
1794
1897
  info.device.type
1795
1898
  ].filter((i) => i).join(" ");
1899
+ }
1900
+ async function requestTrustWithToken(token) {
1901
+ currentAuthToken = token;
1796
1902
  const result = await serverRpc.$call("devframe:anonymous:auth", {
1797
1903
  authToken: token,
1798
- ua,
1904
+ ua: describeUA(),
1799
1905
  origin: location.origin
1800
1906
  });
1801
1907
  isTrusted = result.isTrusted;
1802
- trustedPromise.resolve(isTrusted);
1908
+ if (isTrusted) trustedPromise.resolve(true);
1803
1909
  events.emit("rpc:is-trusted:updated", isTrusted);
1804
1910
  return result.isTrusted;
1805
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
+ }
1806
1926
  async function requestTrust() {
1807
1927
  if (isTrusted) return true;
1808
- return requestTrustWithToken(currentAuthToken);
1928
+ return requestTrustWithToken(currentAuthToken ?? "");
1809
1929
  }
1810
1930
  async function ensureTrusted(timeout = 6e4) {
1811
1931
  if (isTrusted) trustedPromise.resolve(true);
@@ -1825,6 +1945,7 @@ function createWsRpcClientMode(options) {
1825
1945
  },
1826
1946
  requestTrust,
1827
1947
  requestTrustWithToken,
1948
+ requestTrustWithCode,
1828
1949
  ensureTrusted,
1829
1950
  call: (...args) => {
1830
1951
  return serverRpc.$call(...args);
@@ -1838,26 +1959,101 @@ function createWsRpcClientMode(options) {
1838
1959
  };
1839
1960
  }
1840
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
1841
2036
  //#region src/client/rpc.ts
1842
2037
  const CONNECTION_META_KEY = "__DEVFRAME_CONNECTION_META__";
1843
2038
  const CONNECTION_AUTH_TOKEN_KEY = "__DEVFRAME_CONNECTION_AUTH_TOKEN__";
1844
- function getConnectionAuthTokenFromWindows(userAuthToken) {
2039
+ function getStoredAuthToken(userAuthToken) {
1845
2040
  const getters = [
1846
2041
  () => userAuthToken,
1847
- () => localStorage.getItem(CONNECTION_AUTH_TOKEN_KEY),
2042
+ () => localStorage.getItem(CONNECTION_AUTH_TOKEN_KEY) ?? void 0,
1848
2043
  () => window?.[CONNECTION_AUTH_TOKEN_KEY],
1849
2044
  () => globalThis?.[CONNECTION_AUTH_TOKEN_KEY],
1850
2045
  () => parent.window?.[CONNECTION_AUTH_TOKEN_KEY]
1851
2046
  ];
1852
- let value;
1853
2047
  for (const getter of getters) try {
1854
- value = getter();
1855
- if (value) break;
2048
+ const value = getter();
2049
+ if (value) return value;
1856
2050
  } catch {}
1857
- if (!value) value = humanId();
1858
- localStorage.setItem(CONNECTION_AUTH_TOKEN_KEY, value);
1859
- globalThis[CONNECTION_AUTH_TOKEN_KEY] = value;
1860
- 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;
1861
2057
  }
1862
2058
  function findConnectionMetaFromWindows() {
1863
2059
  const getters = [
@@ -1876,18 +2072,18 @@ async function getDevframeRpcClient(options = {}) {
1876
2072
  const bases = Array.isArray(baseURL) ? baseURL : [baseURL];
1877
2073
  let connectionMeta = options.connectionMeta || findConnectionMetaFromWindows();
1878
2074
  let resolvedBaseURL = bases[0] ?? "./";
1879
- function normalizeBase(base) {
1880
- return base.endsWith("/") ? base : `${base}/`;
1881
- }
1882
- function resolveBasePath(base, path) {
1883
- if (/^https?:\/\//.test(path)) return path;
1884
- if (path.startsWith("/")) return path;
1885
- 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
+ }
1886
2082
  }
1887
2083
  if (!connectionMeta) {
1888
2084
  const errors = [];
1889
2085
  for (const base of bases) try {
1890
- 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());
1891
2087
  resolvedBaseURL = base;
1892
2088
  globalThis[CONNECTION_META_KEY] = connectionMeta;
1893
2089
  break;
@@ -1901,13 +2097,14 @@ async function getDevframeRpcClient(options = {}) {
1901
2097
  ...typeof options.cacheOptions === "object" ? options.cacheOptions : {}
1902
2098
  });
1903
2099
  const context = { rpc: void 0 };
1904
- const authToken = getConnectionAuthTokenFromWindows(options.authToken);
2100
+ const authToken = getStoredAuthToken(options.authToken);
2101
+ if (authToken) persistAuthToken(authToken);
1905
2102
  const clientRpc = new RpcFunctionsCollectorBase(context);
1906
2103
  async function fetchJsonFromBases(path) {
1907
2104
  const candidates = [resolvedBaseURL, ...bases.filter((base) => base !== resolvedBaseURL)].filter((x) => x != null);
1908
2105
  const errors = [];
1909
2106
  for (const base of candidates) try {
1910
- return await fetch(resolveBasePath(base, path)).then((r) => {
2107
+ return await fetch(withBase(path, base)).then((r) => {
1911
2108
  if (!r.ok) throw new Error(`Failed to fetch ${path} from ${base}: ${r.status}`);
1912
2109
  return r.json();
1913
2110
  });
@@ -1919,6 +2116,7 @@ async function getDevframeRpcClient(options = {}) {
1919
2116
  const mode = connectionMeta.backend === "static" ? await createStaticRpcClientMode({ fetchJsonFromBases }) : createWsRpcClientMode({
1920
2117
  authToken,
1921
2118
  connectionMeta,
2119
+ metaBaseUrl: resolveMetaBaseUrl(),
1922
2120
  events,
1923
2121
  clientRpc,
1924
2122
  rpcOptions: {
@@ -1937,6 +2135,10 @@ async function getDevframeRpcClient(options = {}) {
1937
2135
  },
1938
2136
  wsOptions: options.wsOptions
1939
2137
  });
2138
+ let authChannel;
2139
+ try {
2140
+ authChannel = new BroadcastChannel("devframe-auth");
2141
+ } catch {}
1940
2142
  const rpc = {
1941
2143
  events,
1942
2144
  get isTrusted() {
@@ -1946,32 +2148,53 @@ async function getDevframeRpcClient(options = {}) {
1946
2148
  ensureTrusted: mode.ensureTrusted,
1947
2149
  requestTrust: mode.requestTrust,
1948
2150
  requestTrustWithToken: async (token) => {
1949
- localStorage.setItem(CONNECTION_AUTH_TOKEN_KEY, token);
1950
- globalThis[CONNECTION_AUTH_TOKEN_KEY] = token;
2151
+ persistAuthToken(token);
1951
2152
  return mode.requestTrustWithToken(token);
1952
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
+ },
1953
2166
  call: mode.call,
1954
2167
  callEvent: mode.callEvent,
1955
2168
  callOptional: mode.callOptional,
1956
2169
  client: clientRpc,
1957
2170
  sharedState: void 0,
1958
2171
  streaming: void 0,
1959
- cacheManager
2172
+ cacheManager,
2173
+ scope: void 0
1960
2174
  };
1961
2175
  rpc.sharedState = createRpcSharedStateClientHost(rpc);
1962
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
+ });
1963
2187
  context.rpc = rpc;
1964
2188
  mode.requestTrust();
1965
- try {
1966
- const bc = new BroadcastChannel("devframe-auth");
1967
- bc.onmessage = (event) => {
1968
- if (event.data?.type === "auth-update" && event.data.authToken) rpc.requestTrustWithToken(event.data.authToken);
1969
- };
1970
- } 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
+ };
1971
2194
  return rpc;
1972
2195
  }
1973
2196
  //#endregion
1974
2197
  //#region src/client/index.ts
1975
2198
  const connectDevframe = getDevframeRpcClient;
1976
2199
  //#endregion
1977
- 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-BwfavB78.mjs";
2
2
 
3
3
  //#region src/node/hub-internals/context.d.ts
4
4
  interface InternalAnonymousAuthStorage {