@unoverse-platform/studio 0.1.98 → 0.1.100
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/package.json
CHANGED
|
@@ -41,11 +41,33 @@ const getIssuerCandidates = (): string[] => {
|
|
|
41
41
|
};
|
|
42
42
|
const getAudience = (): string | undefined => process.env.AUTH_AUDIENCE;
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
// WHERE THE SIGNING KEYS ARE IS THE PROVIDER'S TO SAY, in its discovery document.
|
|
45
|
+
//
|
|
46
|
+
// This read `${issuer}/.well-known/jwks.json`, which is where Auth0 and Cognito happen to keep
|
|
47
|
+
// them. Microsoft Entra keeps them under /discovery/v2.0/keys, Keycloak under
|
|
48
|
+
// /protocol/openid-connect/certs, WorkOS AuthKit under /oauth2/jwks, so a bank's Entra or a
|
|
49
|
+
// WorkOS login was refused on every request with a key that could not be found. OIDC
|
|
50
|
+
// discovery names the place (`jwks_uri`). With no discovery document the old location is
|
|
51
|
+
// used, and not remembered, so a transient failure is retried on the next request.
|
|
52
|
+
type KeySet = ReturnType<typeof createRemoteJWKSet>;
|
|
53
|
+
let jwks: Promise<KeySet> | null = null;
|
|
54
|
+
async function loadJWKS(base: string): Promise<{ set: KeySet; discovered: boolean }> {
|
|
55
|
+
try {
|
|
56
|
+
const res = await fetch(`${base}/.well-known/openid-configuration`, { signal: AbortSignal.timeout(5000) });
|
|
57
|
+
const doc: any = res.ok ? await res.json() : null;
|
|
58
|
+
if (typeof doc?.jwks_uri === "string") return { set: createRemoteJWKSet(new URL(doc.jwks_uri)), discovered: true };
|
|
59
|
+
} catch {
|
|
60
|
+
// fall through to the conventional location
|
|
61
|
+
}
|
|
62
|
+
return { set: createRemoteJWKSet(new URL(`${base}/.well-known/jwks.json`)), discovered: false };
|
|
63
|
+
}
|
|
64
|
+
function getJWKS(): Promise<KeySet> | null {
|
|
46
65
|
const base = getIssuerBase();
|
|
47
66
|
if (!jwks && base) {
|
|
48
|
-
jwks =
|
|
67
|
+
jwks = loadJWKS(base).then(({ set, discovered }) => {
|
|
68
|
+
if (!discovered) jwks = null;
|
|
69
|
+
return set;
|
|
70
|
+
});
|
|
49
71
|
}
|
|
50
72
|
return jwks;
|
|
51
73
|
}
|
|
@@ -69,6 +91,14 @@ function parseArrayClaim(payload: JWTPayload, claimName: string): string[] {
|
|
|
69
91
|
return [];
|
|
70
92
|
}
|
|
71
93
|
|
|
94
|
+
/** Roles as a list. Most providers send a `roles` list (Auth0 namespaced, Entra app roles);
|
|
95
|
+
* WorkOS sends one `role`, the role of the organization membership signed in with. */
|
|
96
|
+
export function parseRoles(payload: JWTPayload): string[] {
|
|
97
|
+
const roles = parseArrayClaim(payload, "roles");
|
|
98
|
+
const one = payload.role;
|
|
99
|
+
return typeof one === "string" && one && !roles.includes(one) ? [...roles, one] : roles;
|
|
100
|
+
}
|
|
101
|
+
|
|
72
102
|
/** Parse a string claim, handling Auth0-style namespaced claims (e.g. https://…/email).
|
|
73
103
|
* Auth0 emits custom claims (email on the access token) ONLY namespaced, so a plain
|
|
74
104
|
* `payload.email` read returns undefined and silently no-ops every email-keyed feature
|
|
@@ -114,7 +144,7 @@ export async function validateJWT(token: string): Promise<AuthUser | null> {
|
|
|
114
144
|
}
|
|
115
145
|
|
|
116
146
|
async function verifyJWT(token: string): Promise<AuthUser | null> {
|
|
117
|
-
const jwksSet = getJWKS();
|
|
147
|
+
const jwksSet = await getJWKS();
|
|
118
148
|
const issuers = getIssuerCandidates();
|
|
119
149
|
if (!jwksSet || issuers.length === 0) {
|
|
120
150
|
console.warn("[auth] OIDC not configured (AUTH_ISSUER missing)");
|
|
@@ -136,8 +166,11 @@ async function verifyJWT(token: string): Promise<AuthUser | null> {
|
|
|
136
166
|
const { payload } = await jwtVerify(token, jwksSet, { issuer: issuers });
|
|
137
167
|
const expected = getAudience();
|
|
138
168
|
if (expected) {
|
|
139
|
-
|
|
140
|
-
|
|
169
|
+
// Both places are read, not the first one present: WorkOS sends its environment in `aud`
|
|
170
|
+
// and the signed-in app in `client_id`, and a cell names its own app.
|
|
171
|
+
const aud = payload.aud;
|
|
172
|
+
const clientId = (payload as Record<string, unknown>).client_id;
|
|
173
|
+
const values = [...(Array.isArray(aud) ? aud : aud ? [aud] : []), ...(clientId ? [clientId] : [])].map(String);
|
|
141
174
|
if (!values.includes(expected)) {
|
|
142
175
|
console.error(
|
|
143
176
|
`[auth] JWT validation failed: audience mismatch (expected ${expected}, token had ${values.join(", ") || "none"})`,
|
|
@@ -148,7 +181,7 @@ async function verifyJWT(token: string): Promise<AuthUser | null> {
|
|
|
148
181
|
return {
|
|
149
182
|
id: payload.sub || "unknown",
|
|
150
183
|
email: parseStringClaim(payload, "email"),
|
|
151
|
-
roles:
|
|
184
|
+
roles: parseRoles(payload),
|
|
152
185
|
permissions: parseArrayClaim(payload, "permissions"),
|
|
153
186
|
raw: payload,
|
|
154
187
|
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AN ORG IS A WALL (docs/unoverse/UNOVERSE_ORGANISATION.md §11, part 1).
|
|
3
|
+
*
|
|
4
|
+
* Anything built from many orgs' definitions (an MCP session's tools, the spatial
|
|
5
|
+
* registry, the Studio lists) builds each item through here. One that throws is left
|
|
6
|
+
* out and named, `org/item: reason`, and every other org is still served. Observed live
|
|
7
|
+
* 2026-09-23: one org's stale template threw inside MCP session construction and every
|
|
8
|
+
* signed-in session, for every org, answered -32603.
|
|
9
|
+
*
|
|
10
|
+
* The one door, so no builder writes its own try and no builder forgets one.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Name what was left out and why. The label is `org/item` (or `kind org/item`). */
|
|
14
|
+
export function reportLeftOut(label: string, err: unknown): void {
|
|
15
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
16
|
+
console.warn(`[unoverse] left out ${label}: ${reason}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Build one item on its own. Undefined when it failed; the failure is reported. */
|
|
20
|
+
export function isolated<T>(label: string, build: () => T): T | undefined {
|
|
21
|
+
try {
|
|
22
|
+
return build();
|
|
23
|
+
} catch (err) {
|
|
24
|
+
reportLeftOut(label, err);
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The same, for a build that awaits. */
|
|
30
|
+
export async function isolatedAsync<T>(label: string, build: () => Promise<T>): Promise<T | undefined> {
|
|
31
|
+
try {
|
|
32
|
+
return await build();
|
|
33
|
+
} catch (err) {
|
|
34
|
+
reportLeftOut(label, err);
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -19,6 +19,7 @@ import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
|
19
19
|
import { join } from "node:path";
|
|
20
20
|
import { parse } from "yaml";
|
|
21
21
|
import { listOrgs, projectDir } from "./homes.js";
|
|
22
|
+
import { isolated } from "../isolated.js";
|
|
22
23
|
|
|
23
24
|
export const AGENT_CARD = "unoverse://components/agent-card";
|
|
24
25
|
|
|
@@ -88,7 +89,12 @@ export function listAgentsIn(root: string, org: string): AgentFile[] {
|
|
|
88
89
|
if (!existsSync(dir)) return [];
|
|
89
90
|
return readdirSync(dir, { withFileTypes: true })
|
|
90
91
|
.filter((e) => e.isFile() && isDef(e.name))
|
|
91
|
-
|
|
92
|
+
// One malformed Agent file leaves out that Agent, never the org's others or any other
|
|
93
|
+
// org's: this list is read inside MCP session construction (UNOVERSE_ORGANISATION.md §11).
|
|
94
|
+
.map((e) => {
|
|
95
|
+
const id = e.name.replace(/\.ya?ml$/, "");
|
|
96
|
+
return isolated(`agent ${org}/${id}`, () => readAgent(root, org, id)) ?? null;
|
|
97
|
+
})
|
|
92
98
|
.filter((a): a is AgentFile => a !== null)
|
|
93
99
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
94
100
|
}
|
|
@@ -13,6 +13,7 @@ import type { UnoverseDefinition } from "../model.js";
|
|
|
13
13
|
import { normalizeDeclaredKind, defaultLayoutOf } from "../model.js";
|
|
14
14
|
import { loadDefinition } from "../load/load.js";
|
|
15
15
|
import { expandNode } from "../load/expand/index.js";
|
|
16
|
+
import { isolated } from "../isolated.js";
|
|
16
17
|
|
|
17
18
|
/** List definitions of a kind. Atoms come from the ONE shared home. Components come
|
|
18
19
|
* from the marketplace PLUS the org packs — every org's when `org` is omitted
|
|
@@ -404,12 +405,8 @@ function walkApps(org: string | undefined, orgsToWalk: string[]): AppManifest[]
|
|
|
404
405
|
// dropped the whole universe, 2026-08-20). One broken app degrades to a skipped
|
|
405
406
|
// entry with a warning, never a dead platform.
|
|
406
407
|
const push = (load: () => AppManifest | null, label: string) => {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
if (m) out.push(m);
|
|
410
|
-
} catch (err) {
|
|
411
|
-
console.warn(`[unoverse] skipping app '${label}' — failed to load: ${(err as Error)?.message ?? err}`);
|
|
412
|
-
}
|
|
408
|
+
const m = isolated(`app ${label}`, load);
|
|
409
|
+
if (m) out.push(m);
|
|
413
410
|
};
|
|
414
411
|
// APP homes, not template homes: the 2026-08-28 rename split templateDirs (the new
|
|
415
412
|
// Template Model kind) from appDirs (the org apps/ folders) — this walk kept calling
|
|
@@ -657,13 +654,16 @@ export function listAppsForWorkbench(org?: string): (UnoverseDefinition & {
|
|
|
657
654
|
app?: AppManifest;
|
|
658
655
|
})[] {
|
|
659
656
|
return listDefinitions("app", org)
|
|
660
|
-
.
|
|
657
|
+
.flatMap((def) => {
|
|
661
658
|
const ref = `${def.org}/${def.name}`; // folder id = lowercased template name, org-scoped
|
|
662
|
-
const
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
659
|
+
const entry = isolated(`app ${ref}`, () => {
|
|
660
|
+
const app = loadAppManifest(ref);
|
|
661
|
+
const states = listStates("app", ref);
|
|
662
|
+
return app
|
|
663
|
+
? { ...def, states, binding: app.binding, appName: app.name, autoTrigger: app.autoTrigger, previewComponents: app.previewComponents, fluidHeight: app.fluidHeight, app }
|
|
664
|
+
: { ...def, states };
|
|
665
|
+
});
|
|
666
|
+
return entry ? [entry] : [];
|
|
667
667
|
})
|
|
668
668
|
// Templates view shows only apps that ARE surfaces (manifest defaultState
|
|
669
669
|
// "template", or a plain layout with no manifest). Component apps (focus /
|