@unoverse-platform/studio 0.1.99 → 0.1.101
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
|
@@ -325,7 +325,7 @@ export function TemplatesView({
|
|
|
325
325
|
// A real JWT makes the workbench a proper authenticated channel: it rides the
|
|
326
326
|
// MCP stream's `Authorization` header and the REST `Authorization` header, so the
|
|
327
327
|
// workflow gets full user context (incl. the email claim → CRM join). See
|
|
328
|
-
// docs/
|
|
328
|
+
// docs/auth-security/IDENTITY.md. Blank → no token (server must run with DISABLE_AUTH=true).
|
|
329
329
|
getAccessToken: token ? async () => token : undefined,
|
|
330
330
|
}),
|
|
331
331
|
[liveBase, token],
|
|
@@ -393,7 +393,7 @@ export function TemplatesView({
|
|
|
393
393
|
const binding = selectedDef?.binding;
|
|
394
394
|
// The session userId MUST be the token's `sub` when a token rides the connection —
|
|
395
395
|
// the server binds the audio socket to the verified identity and REJECTS an
|
|
396
|
-
// INIT_SESSION claiming anyone else (
|
|
396
|
+
// INIT_SESSION claiming anyone else (docs/auth-security/IDENTITY.md; the June audit's WS
|
|
397
397
|
// finding). "dev" is only for a universe with auth off, where no token exists.
|
|
398
398
|
const tokenSub = useMemo(() => {
|
|
399
399
|
if (!token) return undefined;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* JWT authentication for the Unoverse MCP server.
|
|
3
3
|
*
|
|
4
4
|
* Mirrors apps/mcp-server/src/server/auth/auth.ts — same OIDC/JWT flow as the rest
|
|
5
|
-
* of the platform (docs/
|
|
5
|
+
* of the platform (docs/auth-security/IDENTITY.md): validate the access token against the
|
|
6
6
|
* shared AUTH_ISSUER/AUTH_AUDIENCE via JWKS. The only difference is transport: this
|
|
7
7
|
* server speaks MCP over uWS + Web `Request`/`Response`, so the gate is a pure
|
|
8
8
|
* function over the Authorization header rather than node req/res.
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { jwtVerify, createRemoteJWKSet, JWTPayload } from "jose";
|
|
15
15
|
import { isAuthEnabled, devIdentity } from "./authConfig.js";
|
|
16
|
+
import { grantedTo } from "../security/permissions.js";
|
|
16
17
|
|
|
17
18
|
// Read env lazily (not at module load) so it's correct regardless of when .env loads.
|
|
18
19
|
//
|
|
@@ -102,7 +103,7 @@ export function parseRoles(payload: JWTPayload): string[] {
|
|
|
102
103
|
/** Parse a string claim, handling Auth0-style namespaced claims (e.g. https://…/email).
|
|
103
104
|
* Auth0 emits custom claims (email on the access token) ONLY namespaced, so a plain
|
|
104
105
|
* `payload.email` read returns undefined and silently no-ops every email-keyed feature
|
|
105
|
-
* (CRM resolve). Same namespace-aware read as parseArrayClaim. See docs/
|
|
106
|
+
* (CRM resolve). Same namespace-aware read as parseArrayClaim. See docs/auth-security/IDENTITY.md. */
|
|
106
107
|
function parseStringClaim(payload: JWTPayload, claimName: string): string | undefined {
|
|
107
108
|
const direct = payload[claimName];
|
|
108
109
|
if (typeof direct === "string") return direct;
|
|
@@ -166,8 +167,11 @@ async function verifyJWT(token: string): Promise<AuthUser | null> {
|
|
|
166
167
|
const { payload } = await jwtVerify(token, jwksSet, { issuer: issuers });
|
|
167
168
|
const expected = getAudience();
|
|
168
169
|
if (expected) {
|
|
169
|
-
|
|
170
|
-
|
|
170
|
+
// Both places are read, not the first one present: WorkOS sends its environment in `aud`
|
|
171
|
+
// and the signed-in app in `client_id`, and a cell names its own app.
|
|
172
|
+
const aud = payload.aud;
|
|
173
|
+
const clientId = (payload as Record<string, unknown>).client_id;
|
|
174
|
+
const values = [...(Array.isArray(aud) ? aud : aud ? [aud] : []), ...(clientId ? [clientId] : [])].map(String);
|
|
171
175
|
if (!values.includes(expected)) {
|
|
172
176
|
console.error(
|
|
173
177
|
`[auth] JWT validation failed: audience mismatch (expected ${expected}, token had ${values.join(", ") || "none"})`,
|
|
@@ -209,7 +213,7 @@ export async function authorize(authHeader: string | null | undefined): Promise<
|
|
|
209
213
|
* this exact string (Auth0 API permission on the gravity-api audience), NOT on a
|
|
210
214
|
* role name — end-user chat tokens are valid on the same audience, so "authenticated"
|
|
211
215
|
* is not enough; only "authenticated AND may author workflows" passes. This is the
|
|
212
|
-
* platform's first authZ check (docs/
|
|
216
|
+
* platform's first authZ check (docs/auth-security/legacy/AUTH_REMEDIATION_2026.md Phase 1b).
|
|
213
217
|
*/
|
|
214
218
|
export const BUILDER_PERMISSION = "workflow:author";
|
|
215
219
|
|
|
@@ -221,7 +225,8 @@ export type BuilderAuthResult = { ok: true; user: AuthUser } | { ok: false; reas
|
|
|
221
225
|
* - FAIL-CLOSED when auth is disabled: this route must never be open, so an
|
|
222
226
|
* AUTH_ENABLED=false deployment simply cannot serve it (returns forbidden).
|
|
223
227
|
* Local dev uses the ungated LOOPBACK route (:4106) instead — never this one.
|
|
224
|
-
* - Requires
|
|
228
|
+
* - Requires BUILDER_PERMISSION, not just a valid bearer: granted by a role on the token
|
|
229
|
+
* (ROLE_GRANTS, the one role-to-permission rule) or by a permission claim.
|
|
225
230
|
* Returns the full user so a later step can bind the session to a workflow grant.
|
|
226
231
|
*/
|
|
227
232
|
export async function authorizeBuilder(authHeader: string | null | undefined): Promise<BuilderAuthResult> {
|
|
@@ -232,7 +237,7 @@ export async function authorizeBuilder(authHeader: string | null | undefined): P
|
|
|
232
237
|
if (!token) return { ok: false, reason: "unauthenticated" };
|
|
233
238
|
const user = await validateJWT(token);
|
|
234
239
|
if (!user) return { ok: false, reason: "unauthenticated" };
|
|
235
|
-
if (!user.
|
|
240
|
+
if (!grantedTo(user).has(BUILDER_PERMISSION)) return { ok: false, reason: "forbidden" };
|
|
236
241
|
return { ok: true, user };
|
|
237
242
|
}
|
|
238
243
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* MIRRORED across services (apps/server, apps/mcp-server, apps/memory-server, and
|
|
5
5
|
* now apps/unoverse) — keep the copies in sync. Single source of truth:
|
|
6
|
-
* docs/
|
|
6
|
+
* docs/auth-security/legacy/AUTH_REMEDIATION_2026.md (Phase 0).
|
|
7
7
|
*
|
|
8
8
|
* Rules:
|
|
9
9
|
* - Default: auth ENABLED. Only an explicit AUTH_ENABLED=false (or the deprecated
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* calls execute, so {{signal.x}} and {{prompt.y}} an author wrote in a config field are
|
|
6
6
|
* plain text by the time they reach here.
|
|
7
7
|
*
|
|
8
|
-
* `executionContext.auth.accessToken` — the CALLER'S JWT (
|
|
8
|
+
* `executionContext.auth.accessToken` — the CALLER'S JWT (docs/auth-security/IDENTITY.md) — is
|
|
9
9
|
* DELIBERATELY WITHHELD, and must stay that way. A code node may reach it because a code
|
|
10
10
|
* node is bounded by provenance; a manifest is data that can arrive by paste or database
|
|
11
11
|
* row. The blast radii are not comparable: a third-party API key reaches that vendor
|
|
@@ -80,7 +80,7 @@ const UNOVERSE_EVENT = "notifications/unoverse/event";
|
|
|
80
80
|
/**
|
|
81
81
|
* Guest identity — CONTAINED HERE so hosts never write guest logic. A token-less
|
|
82
82
|
* session is a GUEST to the platform's public-entry gate, which only admits
|
|
83
|
-
* `guest-`-prefixed ids (Run Authorization,
|
|
83
|
+
* `guest-`-prefixed ids (Run Authorization, docs/auth-security/IDENTITY.md). Minted once per
|
|
84
84
|
* browser and persisted so the visitor's conversation and memory cohere across
|
|
85
85
|
* reloads. A host that supplies its own guest id wins (two apps in one page share
|
|
86
86
|
* one visitor); a host with a token is untouched.
|
package/vendor/sdk/main.tsx
CHANGED
|
@@ -42,7 +42,7 @@ function render() {
|
|
|
42
42
|
// tokenless boot made the SDK skip its guest-id substitution (connection.tsx), so
|
|
43
43
|
// the session registered a non-guest id and the public-entry gate refused it.
|
|
44
44
|
// No token ⇒ no accessor ⇒ the SDK mints/persists `guest-<uuid>` per the contract
|
|
45
|
-
// (UNOVERSE_MCP_APP_PROTOCOL §5,
|
|
45
|
+
// (UNOVERSE_MCP_APP_PROTOCOL §5, docs/auth-security/IDENTITY.md Run Authorization).
|
|
46
46
|
getAccessToken={current.token ? readToken : undefined}
|
|
47
47
|
/>,
|
|
48
48
|
);
|