@unoverse-platform/studio 0.1.60 → 0.1.62

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/local/plugin.ts CHANGED
@@ -253,6 +253,32 @@ export function localStudioHost(): Plugin {
253
253
  // runs the same base items/publish functions directly, and no token ever
254
254
  // nears the browser because the browser no longer takes part.
255
255
 
256
+ /**
257
+ * THE CLI'S SESSION, SERVED TO THE PREVIEW. `unoverse login` keeps sessions
258
+ * in ~/.unoverse/sessions.json keyed by universe origin (cli lib/session.mjs
259
+ * — that file IS the contract). Studio used to know nothing of it, so a
260
+ * developer who had just logged in still saw "Offline" and a blank Live
261
+ * preview with no hint why (observed live 2026-08-21). The preview already
262
+ * carries a bearer in the browser by design (the pasted-JWT field), so
263
+ * serving the session token here changes what the browser holds not at all —
264
+ * it only spares the developer pasting what the CLI already has. Expired or
265
+ * absent answers {}: the UI then says to run unoverse login, by name.
266
+ */
267
+ case path === "/local/session": {
268
+ const origin = new URL(req.url ?? "/", "http://x").searchParams.get("origin") ?? "";
269
+ try {
270
+ const { readFileSync } = await import("node:fs");
271
+ const { homedir } = await import("node:os");
272
+ const { join } = await import("node:path");
273
+ const all = JSON.parse(readFileSync(join(homedir(), ".unoverse", "sessions.json"), "utf8"));
274
+ const sess = all[origin.replace(/\/$/, "")];
275
+ if (sess?.access_token && (!sess.expires_at || sess.expires_at > Date.now() + 60_000)) {
276
+ return json(res, { token: sess.access_token, email: sess.email ?? "" });
277
+ }
278
+ } catch { /* no file, unreadable, malformed: same answer as signed out */ }
279
+ return json(res, {});
280
+ }
281
+
256
282
  case path === "/dev/orgs":
257
283
  return json(res, { orgs: c.themeOrgs() });
258
284
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unoverse-platform/studio",
3
- "version": "0.1.60",
3
+ "version": "0.1.62",
4
4
  "type": "module",
5
5
  "description": "Unoverse Studio: the local authoring app. Reads your own rx/ folder, no backend.",
6
6
  "scripts": {
@@ -80,7 +80,21 @@ export function NativeAppHost({
80
80
  if (!text) throw new Error(`${APP_UI_URI} returned no content — is the webSDK built on the universe?`);
81
81
  if (!cancelled) setHtml(text);
82
82
  } catch (e) {
83
- if (!cancelled) setError(e instanceof Error ? e.message : String(e));
83
+ const msg = e instanceof Error ? e.message : String(e);
84
+ // AUTH FAILURES SAY SO, BY NAME. A 401 here rendered as a bare transport error
85
+ // (or worse, an eternal loading ghost), and "Offline" told the developer
86
+ // nothing. The fix is one terminal command; the error is the place to say it.
87
+ const authShaped = /401|unauthori[sz]ed|jwt|token/i.test(msg);
88
+ if (!cancelled)
89
+ setError(
90
+ authShaped
91
+ ? `This universe requires sign-in.
92
+
93
+ Run: unoverse login ${server}
94
+
95
+ then come back — Studio picks the session up by itself.`
96
+ : msg,
97
+ );
84
98
  } finally {
85
99
  client.close().catch(() => {});
86
100
  }
@@ -26,7 +26,7 @@ import { ComponentStore } from "@unoverse/sdk";
26
26
  import { StreamedUnoverseTemplate, useUnoverseConnection, computeAppWidth, resolveActiveLayout, visible, type ResolvedTheme, type UseVoiceServiceConfig } from "@unoverse/sdk";
27
27
  import { NativeAppHost } from "./NativeAppHost";
28
28
  import { inArrangement, viewOfSlice } from "./states";
29
- import { client, authedFetch, hasAuth, UNOVERSE_BASE, liveUniverseUrl } from "studio-host";
29
+ import { client, authedFetch, hasAuth, hasSourceAccess, UNOVERSE_BASE, liveUniverseUrl } from "studio-host";
30
30
  import { NAV, NAV_SECTION, SCREEN, navItem, navSubItem } from "./chrome";
31
31
 
32
32
  // Per-APP test conversation: each app chats in its own conversation id, so testing
@@ -243,9 +243,18 @@ export function TemplatesView({
243
243
 
244
244
  // OIDC session (undefined when no AuthProvider is mounted — env unset).
245
245
  const auth = useAuth();
246
- // Token source: prefer the real OIDC access token; fall back to a pasted JWT.
246
+ /**
247
+ * THE TERMINAL LOGIN COUNTS HERE. `unoverse login` keeps the session; the local host
248
+ * serves its token per origin (/local/session). Without this a developer who had just
249
+ * logged in still saw Offline and a blank preview with no hint why. Refetched when the
250
+ * server changes and when the tab regains focus, so a login run in the terminal is
251
+ * picked up on the next glance at Studio. Empty when signed out or expired — then the
252
+ * UI says to run unoverse login rather than showing a mute Offline.
253
+ */
254
+ const [cliSession, setCliSession] = useState<{ token?: string; email?: string }>({});
255
+ // Token source: the real OIDC access token, else the terminal login, else a pasted JWT.
247
256
  const authToken = auth?.user?.access_token;
248
- const token = authToken || connCfg.token;
257
+ const token = authToken || cliSession.token || connCfg.token;
249
258
 
250
259
  // The live server: the signed-in universe, else the local dev platform (see the
251
260
  // CONN_DEFAULTS note). A universe URL is already an https origin; the bare local
@@ -253,6 +262,21 @@ export function TemplatesView({
253
262
  const universeUrl = liveUniverseUrl();
254
263
  const liveServer = universeUrl ?? LOCAL_DEV_SERVER;
255
264
  const liveBase = liveServer.includes("://") ? liveServer : `http://${liveServer}`;
265
+ useEffect(() => {
266
+ if (!hasSourceAccess) return;
267
+ let live = true;
268
+ const read = () =>
269
+ authedFetch(`/local/session?origin=${encodeURIComponent(liveBase.replace(/\/$/, ""))}`)
270
+ .then((r) => r.json())
271
+ .then((d) => live && setCliSession(d ?? {}))
272
+ .catch(() => live && setCliSession({}));
273
+ read();
274
+ window.addEventListener("focus", read);
275
+ return () => {
276
+ live = false;
277
+ window.removeEventListener("focus", read);
278
+ };
279
+ }, [liveBase]);
256
280
 
257
281
  const config = useMemo(
258
282
  () => ({