@unoverse-platform/studio 0.1.56 → 0.1.58
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 +5 -66
- package/local/sample-template/chat/components/assistant-turn.yaml +40 -0
- package/local/sample-template/chat/components/core-focus.yaml +46 -0
- package/local/sample-template/chat/components/core.yaml +41 -0
- package/local/sample-template/chat/components/focus-overlay.yaml +20 -0
- package/local/sample-template/chat/components/header.yaml +53 -0
- package/local/sample-template/chat/components/suggestions.yaml +142 -0
- package/local/sample-template/chat/components/thinking-dots.yaml +38 -0
- package/local/sample-template/chat/components/user-turn.yaml +22 -0
- package/local/sample-template/chat/layouts/focused.yaml +12 -0
- package/local/sample-template/chat/layouts/standard.yaml +9 -0
- package/local/sample-template/chat/manifest.yaml +41 -0
- package/local/sample-template/chat/states/conversation.yaml +21 -0
- package/local/sample-template/chat/states/welcome.yaml +119 -0
- package/local/scaffold.mjs +15 -2
- package/package.json +1 -1
- package/screens/DesignSystemView.tsx +4 -1
- package/screens/NativeAppHost.tsx +2 -2
- package/screens/TemplatesView.tsx +28 -4
- package/src/App.tsx +14 -70
- package/src/OfflineSession.tsx +50 -0
- package/vendor/sdk/appEngine.tsx +15 -2
- package/vendor/sdk/lib/core/connection.ts +11 -0
- package/vendor/sdk/lib/core/store.ts +17 -0
- package/src/ConnectUniverse.tsx +0 -232
- package/src/PublishDialog.tsx +0 -309
- package/src/UniverseSession.tsx +0 -121
- package/src/universes.ts +0 -210
package/src/universes.ts
DELETED
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The universes this developer can publish to.
|
|
3
|
-
*
|
|
4
|
-
* A UNIVERSE NAMES ITS OWN IDENTITY PROVIDER. Studio hardcodes no issuer and no vendor:
|
|
5
|
-
* it asks a universe at `/.well-known/unoverse-universe` and follows the answer, so
|
|
6
|
-
* Auth0, Okta, Entra and Keycloak all work with no change here. That is the whole reason
|
|
7
|
-
* login lives in Studio (a browser, where OIDC is a redirect) rather than in the CLI.
|
|
8
|
-
*
|
|
9
|
-
* THERE IS NO CENTRAL DIRECTORY, and there cannot be. Every universe is independent with
|
|
10
|
-
* its own database and its own IdP (docs/AUTH_TOKEN_FLOW.md: "OIDC-provider-agnostic").
|
|
11
|
-
* Nothing sits above them to enumerate. So "the universes I can deploy to" means the ones
|
|
12
|
-
* this developer has connected to, held here on their machine. That is the honest answer,
|
|
13
|
-
* not a placeholder for a registry.
|
|
14
|
-
*
|
|
15
|
-
* NO TOKEN IS EVER WRITTEN HERE. Only the URL, a label, and the public config. The access
|
|
16
|
-
* token stays wherever the OIDC library keeps it for the session. Local Studio already
|
|
17
|
-
* holds this line for credentials (`local/keys.ts`: "Studio therefore never WRITES a
|
|
18
|
-
* secret") and connecting to a universe must not be the thing that breaks it.
|
|
19
|
-
*
|
|
20
|
-
* See docs/architecture/LOCAL_STUDIO.md.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
/** What a universe publishes about itself. Mirrors `getPublicAuthConfig()` on the server. */
|
|
24
|
-
export interface UniverseConfig {
|
|
25
|
-
/** False means the universe is not gated at all: do not send anyone to log in. */
|
|
26
|
-
authEnabled: boolean;
|
|
27
|
-
issuer: string | null;
|
|
28
|
-
clientId: string | null;
|
|
29
|
-
audience: string | null;
|
|
30
|
-
/** The permission a token needs before this universe accepts a publish. */
|
|
31
|
-
publishPermission: string;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface Universe {
|
|
35
|
-
/** Origin, normalised. The identity of the entry: one row per universe. */
|
|
36
|
-
url: string;
|
|
37
|
-
/** What the developer calls it. Defaults to the host. */
|
|
38
|
-
label: string;
|
|
39
|
-
config: UniverseConfig;
|
|
40
|
-
/** When it was last reachable, ISO. Purely informational. */
|
|
41
|
-
connectedAt?: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const STORAGE_KEY = "studio:universes";
|
|
45
|
-
|
|
46
|
-
/** The path a universe serves its public config on. */
|
|
47
|
-
export const DISCOVERY_PATH = "/.well-known/unoverse-universe";
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Turn whatever a developer typed into an origin.
|
|
51
|
-
*
|
|
52
|
-
* People type `localhost:4105`, `example.com/`, and `https://example.com/canvas`. All
|
|
53
|
-
* three mean one universe, and storing them as three entries would let the same universe
|
|
54
|
-
* hold three different logins. So this is the identity function for the list.
|
|
55
|
-
*
|
|
56
|
-
* Bare hosts default to https, EXCEPT localhost and 127.0.0.1, which are overwhelmingly
|
|
57
|
-
* a developer's own machine over http and would otherwise fail confusingly.
|
|
58
|
-
*/
|
|
59
|
-
export function normaliseUrl(input: string): string {
|
|
60
|
-
const trimmed = (input ?? "").trim();
|
|
61
|
-
if (!trimmed) throw new Error("Enter a universe address");
|
|
62
|
-
|
|
63
|
-
const isLocal = /^(https?:\/\/)?(localhost|127\.0\.0\.1)(:|\/|$)/i.test(trimmed);
|
|
64
|
-
const withScheme = /^https?:\/\//i.test(trimmed) ? trimmed : `${isLocal ? "http" : "https"}://${trimmed}`;
|
|
65
|
-
|
|
66
|
-
let parsed: URL;
|
|
67
|
-
try {
|
|
68
|
-
parsed = new URL(withScheme);
|
|
69
|
-
} catch {
|
|
70
|
-
throw new Error(`"${input}" is not a valid address`);
|
|
71
|
-
}
|
|
72
|
-
return parsed.origin;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** A readable default name: the host, without the port for a real domain. */
|
|
76
|
-
export function defaultLabel(url: string): string {
|
|
77
|
-
try {
|
|
78
|
-
return new URL(url).host;
|
|
79
|
-
} catch {
|
|
80
|
-
return url;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Ask a universe how to log in.
|
|
86
|
-
*
|
|
87
|
-
* Failure here is the common case, not the exception: wrong address, universe down, a
|
|
88
|
-
* plain website that is not a universe at all. Each gets a message a developer can act
|
|
89
|
-
* on, because "Failed to fetch" tells them nothing about which of the three it was.
|
|
90
|
-
*/
|
|
91
|
-
export async function discover(
|
|
92
|
-
url: string,
|
|
93
|
-
fetchImpl: typeof fetch = fetch,
|
|
94
|
-
): Promise<UniverseConfig> {
|
|
95
|
-
let response: Response;
|
|
96
|
-
try {
|
|
97
|
-
response = await fetchImpl(`${url}${DISCOVERY_PATH}`, { headers: { accept: "application/json" } });
|
|
98
|
-
} catch {
|
|
99
|
-
throw new Error(`Could not reach ${url}. Check the address, and that the universe is running.`);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (response.status === 404) {
|
|
103
|
-
throw new Error(`Something answered at ${url}, but it is not a universe.`);
|
|
104
|
-
}
|
|
105
|
-
if (!response.ok) {
|
|
106
|
-
throw new Error(`${url} answered with an error (${response.status}).`);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
let body: unknown;
|
|
110
|
-
try {
|
|
111
|
-
body = await response.json();
|
|
112
|
-
} catch {
|
|
113
|
-
throw new Error(`${url} answered, but not like a universe. If it is one, it may need restarting.`);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const cfg = body as Partial<UniverseConfig>;
|
|
117
|
-
// authEnabled is the field that decides whether we send someone to log in, so its
|
|
118
|
-
// absence is a broken response rather than a default worth guessing at.
|
|
119
|
-
if (typeof cfg?.authEnabled !== "boolean") {
|
|
120
|
-
throw new Error(`${url} answered, but not like a universe. If it is one, it may need restarting.`);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return {
|
|
124
|
-
authEnabled: cfg.authEnabled,
|
|
125
|
-
issuer: cfg.issuer ?? null,
|
|
126
|
-
clientId: cfg.clientId ?? null,
|
|
127
|
-
audience: cfg.audience ?? null,
|
|
128
|
-
publishPermission: cfg.publishPermission ?? "marketplace:publish",
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Can this universe actually be logged into?
|
|
134
|
-
*
|
|
135
|
-
* A universe can say `authEnabled: true` and still be missing an issuer or a client id,
|
|
136
|
-
* which is a misconfigured universe rather than a Studio problem. Saying so beats
|
|
137
|
-
* redirecting the browser to `undefined`.
|
|
138
|
-
*/
|
|
139
|
-
export function loginReady(config: UniverseConfig): { ok: true } | { ok: false; reason: string } {
|
|
140
|
-
if (!config.authEnabled) return { ok: false, reason: "This universe has authentication switched off." };
|
|
141
|
-
if (!config.issuer) return { ok: false, reason: "This universe has no identity provider configured." };
|
|
142
|
-
if (!config.clientId) return { ok: false, reason: "This universe has no client id configured for sign-in." };
|
|
143
|
-
return { ok: true };
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Does this token allow publishing here?
|
|
148
|
-
*
|
|
149
|
-
* Checked so Studio can say "signed in, but not allowed to publish" up front, instead of
|
|
150
|
-
* letting someone build for an hour and fail on the push. Namespace-aware for the same
|
|
151
|
-
* reason the server is: an IdP may emit `permissions` under a namespaced claim, and a
|
|
152
|
-
* plain read silently returns nothing (docs/AUTH_TOKEN_FLOW.md).
|
|
153
|
-
*/
|
|
154
|
-
export function canPublish(claims: Record<string, unknown> | null | undefined, config: UniverseConfig): boolean {
|
|
155
|
-
if (!claims) return false;
|
|
156
|
-
const direct = claims.permissions;
|
|
157
|
-
const namespacedKey = Object.keys(claims).find(
|
|
158
|
-
(k) => k.endsWith("/permissions") || k.endsWith("/claims/permissions"),
|
|
159
|
-
);
|
|
160
|
-
const list = Array.isArray(direct) ? direct : namespacedKey && Array.isArray(claims[namespacedKey]) ? (claims[namespacedKey] as unknown[]) : [];
|
|
161
|
-
|
|
162
|
-
// TRIMMED, because a permission is typed into an identity provider by a person and a
|
|
163
|
-
// stray space is invisible everywhere it is displayed. One arrived as " marketplace:publish"
|
|
164
|
-
// and cost a long debugging session: the Auth0 screen showed it assigned, the token
|
|
165
|
-
// carried it, the label printed it, and the comparison correctly said no. Nothing about
|
|
166
|
-
// surrounding whitespace is ever meaningful in a permission name.
|
|
167
|
-
const wanted = String(config.publishPermission).trim();
|
|
168
|
-
return list.some((p) => String(p).trim() === wanted);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// ── The stored list ───────────────────────────────────────────────────────
|
|
172
|
-
|
|
173
|
-
type Store = Pick<Storage, "getItem" | "setItem">;
|
|
174
|
-
|
|
175
|
-
function store(): Store | null {
|
|
176
|
-
try {
|
|
177
|
-
return typeof localStorage === "undefined" ? null : localStorage;
|
|
178
|
-
} catch {
|
|
179
|
-
return null; // storage blocked (private mode, embedded contexts)
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/** Every universe this developer has connected to. Never throws: a corrupt entry is
|
|
184
|
-
* dropped rather than taking the whole list, and the app down with it. */
|
|
185
|
-
export function loadUniverses(storage: Store | null = store()): Universe[] {
|
|
186
|
-
const raw = storage?.getItem(STORAGE_KEY);
|
|
187
|
-
if (!raw) return [];
|
|
188
|
-
try {
|
|
189
|
-
const parsed = JSON.parse(raw);
|
|
190
|
-
if (!Array.isArray(parsed)) return [];
|
|
191
|
-
return parsed.filter((u): u is Universe => typeof u?.url === "string" && typeof u?.config?.authEnabled === "boolean");
|
|
192
|
-
} catch {
|
|
193
|
-
return [];
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/** Add or update one, keyed on url so reconnecting refreshes rather than duplicates. */
|
|
198
|
-
export function saveUniverse(universe: Universe, storage: Store | null = store()): Universe[] {
|
|
199
|
-
const next = [...loadUniverses(storage).filter((u) => u.url !== universe.url), universe].sort((a, b) =>
|
|
200
|
-
a.label.localeCompare(b.label),
|
|
201
|
-
);
|
|
202
|
-
storage?.setItem(STORAGE_KEY, JSON.stringify(next));
|
|
203
|
-
return next;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export function removeUniverse(url: string, storage: Store | null = store()): Universe[] {
|
|
207
|
-
const next = loadUniverses(storage).filter((u) => u.url !== url);
|
|
208
|
-
storage?.setItem(STORAGE_KEY, JSON.stringify(next));
|
|
209
|
-
return next;
|
|
210
|
-
}
|