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.
- package/dist/{_shared-CUFqO4kJ.mjs → _shared-bWRzeSa0.mjs} +2 -3
- package/dist/adapters/build.d.mts +1 -1
- package/dist/adapters/build.mjs +7 -7
- package/dist/adapters/cli.d.mts +1 -1
- package/dist/adapters/cli.mjs +2 -2
- package/dist/adapters/dev.d.mts +8 -2
- package/dist/adapters/dev.mjs +1 -1
- package/dist/adapters/embedded.d.mts +1 -1
- package/dist/adapters/mcp.d.mts +1 -1
- package/dist/adapters/mcp.mjs +4 -4
- package/dist/client/index.d.mts +148 -6
- package/dist/client/index.mjs +272 -49
- package/dist/constants.d.mts +17 -1
- package/dist/constants.mjs +17 -1
- package/dist/context-3x_bgBgZ.mjs +48 -0
- package/dist/{context-DTRcO_UH.d.mts → context-o-TwncyP.d.mts} +1 -1
- package/dist/{dev-Cv43GfqM.mjs → dev-CSpLSEVh.mjs} +39 -13
- package/dist/{devframe-BuR6n9ZD.d.mts → devframe-BwfavB78.d.mts} +365 -15
- package/dist/{diagnostics-GitRGKbr.mjs → diagnostics-BXwBQmoN.mjs} +1 -1
- package/dist/{dump-9lKIJTLh.mjs → dump-DoXkj4ku.mjs} +1 -1
- package/dist/helpers/vite.d.mts +1 -1
- package/dist/helpers/vite.mjs +11 -11
- package/dist/{host-h3-Dgpgr1Ul.mjs → host-h3-C5SZlk03.mjs} +134 -6
- package/dist/{index-C7M1hnvL.d.mts → index-CxaPKDXX.d.mts} +2 -2
- package/dist/{index-DH2sBIwd.d.mts → index-DXHWuwJk.d.mts} +1 -1
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +1 -1
- package/dist/node/auth.d.mts +43 -23
- package/dist/node/auth.mjs +84 -37
- package/dist/node/hub-internals.d.mts +2 -2
- package/dist/node/hub-internals.mjs +2 -2
- package/dist/node/index.d.mts +22 -6
- package/dist/node/index.mjs +4 -4
- package/dist/recipes/open-helpers.d.mts +1 -1
- package/dist/recipes/open-helpers.mjs +3 -3
- package/dist/revoke-ENfxWkFK.mjs +79 -0
- package/dist/rpc/dump.d.mts +1 -1
- package/dist/rpc/dump.mjs +1 -1
- package/dist/rpc/index.d.mts +3 -3
- package/dist/rpc/index.mjs +4 -4
- package/dist/rpc/transports/ws-client.d.mts +1 -1
- package/dist/rpc/transports/ws-client.mjs +2 -2
- package/dist/rpc/transports/ws-server.d.mts +2 -2
- package/dist/rpc/transports/ws-server.mjs +124 -60
- package/dist/{serialization-BD_qXGjd.mjs → serialization-DpLXCy13.mjs} +1 -1
- package/dist/{server-wHlpcdZ9.d.mts → server-oju7PTi2.d.mts} +30 -3
- package/dist/{server-BBaBJaUL.mjs → server-wyY3zh67.mjs} +18 -14
- package/dist/{shared-state-u0y123fi.mjs → shared-state-D4PPieA0.mjs} +4 -4
- package/dist/{shared-state-BlBNYziY.mjs → storage-l2ezeend.mjs} +128 -6
- package/dist/types/index.d.mts +4 -4
- package/dist/{types-BkkQ0Txg.d.mts → types-DZEx4ffs.d.mts} +9 -1
- package/dist/utils/crypto-token.d.mts +24 -0
- package/dist/utils/crypto-token.mjs +45 -0
- package/dist/utils/events.d.mts +1 -1
- package/dist/utils/hash.mjs +1 -1
- package/dist/utils/launch-editor.mjs +1 -1
- package/dist/utils/open.mjs +1 -1
- package/dist/utils/scope.d.mts +11 -0
- package/dist/utils/scope.mjs +15 -0
- package/dist/utils/shared-state.d.mts +1 -1
- package/dist/utils/shared-state.mjs +1 -1
- package/dist/utils/streaming-channel.d.mts +1 -1
- package/dist/utils/structured-clone.mjs +1 -1
- package/dist/{ws-client-CVYX9niP.d.mts → ws-client-D0z0pOhn.d.mts} +1 -1
- package/dist/ws-server-BVVMDo2n.d.mts +103 -0
- package/package.json +12 -7
- package/skills/devframe/SKILL.md +123 -23
- package/skills/devframe/templates/counter-devframe.ts +14 -4
- package/skills/devframe/templates/spa-devframe.ts +13 -3
- package/skills/devframe/templates/vite-client.ts +3 -2
- package/dist/context-DaKmhhHY.mjs +0 -164
- package/dist/revoke-CL0LSAN9.mjs +0 -803
- package/dist/utils/human-id.d.mts +0 -8
- package/dist/utils/human-id.mjs +0 -769
- package/dist/ws-server-C1LjmRnp.d.mts +0 -61
- /package/dist/{define-CW9gLnyG.mjs → define-BLWPsH6y.mjs} +0 -0
- /package/dist/{diagnostics-reporter-CBBZwoMv.mjs → diagnostics-reporter-CsIG85Q5.mjs} +0 -0
- /package/dist/{hash-bwOD8RgU.mjs → hash-DFc5WwhO.mjs} +0 -0
- /package/dist/{launch-editor-BbNhtg7b.mjs → launch-editor-CgXBgviI.mjs} +0 -0
- /package/dist/{open-DiQn6zCH.mjs → open-Dusa2Zzd.mjs} +0 -0
- /package/dist/{structured-clone-PdCZwt7F.mjs → structured-clone-CeZOHvkd.mjs} +0 -0
- /package/dist/{structured-clone-jegjz0hM.mjs → structured-clone-XpHLZ8nr.mjs} +0 -0
- /package/dist/{transports-DTFoMUbE.mjs → transports-BmDVn4SF.mjs} +0 -0
package/dist/client/index.mjs
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
|
-
import { t as hash } from "../hash-
|
|
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 {
|
|
5
|
-
import { t as
|
|
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) =>
|
|
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) =>
|
|
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
|
|
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
|
|
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
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
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 =
|
|
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
|
-
|
|
1786
|
-
currentAuthToken = token;
|
|
1889
|
+
function describeUA() {
|
|
1787
1890
|
const info = parseUA(navigator.userAgent);
|
|
1788
|
-
|
|
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(
|
|
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
|
|
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)
|
|
2048
|
+
const value = getter();
|
|
2049
|
+
if (value) return value;
|
|
1856
2050
|
} catch {}
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
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
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
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 };
|
package/dist/constants.d.mts
CHANGED
|
@@ -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 };
|
package/dist/constants.mjs
CHANGED
|
@@ -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 };
|