@unoverse-platform/studio 0.1.100 → 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unoverse-platform/studio",
3
- "version": "0.1.100",
3
+ "version": "0.1.101",
4
4
  "type": "module",
5
5
  "description": "Unoverse Studio: the local authoring app. Reads your own rx/ folder, no backend.",
6
6
  "scripts": {
@@ -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/AUTH_TOKEN_FLOW.md. Blank → no token (server must run with DISABLE_AUTH=true).
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 (AUTH_TOKEN_FLOW.md; the June audit's WS
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/AUTH_TOKEN_FLOW.md): validate the access token against the
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/AUTH_TOKEN_FLOW.md. */
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;
@@ -212,7 +213,7 @@ export async function authorize(authHeader: string | null | undefined): Promise<
212
213
  * this exact string (Auth0 API permission on the gravity-api audience), NOT on a
213
214
  * role name — end-user chat tokens are valid on the same audience, so "authenticated"
214
215
  * is not enough; only "authenticated AND may author workflows" passes. This is the
215
- * platform's first authZ check (docs/AUTH_REMEDIATION.md Phase 1b).
216
+ * platform's first authZ check (docs/auth-security/legacy/AUTH_REMEDIATION_2026.md Phase 1b).
216
217
  */
217
218
  export const BUILDER_PERMISSION = "workflow:author";
218
219
 
@@ -224,7 +225,8 @@ export type BuilderAuthResult = { ok: true; user: AuthUser } | { ok: false; reas
224
225
  * - FAIL-CLOSED when auth is disabled: this route must never be open, so an
225
226
  * AUTH_ENABLED=false deployment simply cannot serve it (returns forbidden).
226
227
  * Local dev uses the ungated LOOPBACK route (:4106) instead — never this one.
227
- * - Requires the BUILDER_PERMISSION claim, not just a valid bearer.
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.
228
230
  * Returns the full user so a later step can bind the session to a workflow grant.
229
231
  */
230
232
  export async function authorizeBuilder(authHeader: string | null | undefined): Promise<BuilderAuthResult> {
@@ -235,7 +237,7 @@ export async function authorizeBuilder(authHeader: string | null | undefined): P
235
237
  if (!token) return { ok: false, reason: "unauthenticated" };
236
238
  const user = await validateJWT(token);
237
239
  if (!user) return { ok: false, reason: "unauthenticated" };
238
- if (!user.permissions.includes(BUILDER_PERMISSION)) return { ok: false, reason: "forbidden" };
240
+ if (!grantedTo(user).has(BUILDER_PERMISSION)) return { ok: false, reason: "forbidden" };
239
241
  return { ok: true, user };
240
242
  }
241
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/AUTH_REMEDIATION.md (Phase 0).
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 (AUTH_TOKEN_FLOW.md) — is
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, AUTH_TOKEN_FLOW.md). Minted once per
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.
@@ -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, AUTH_TOKEN_FLOW Run Authorization).
45
+ // (UNOVERSE_MCP_APP_PROTOCOL §5, docs/auth-security/IDENTITY.md Run Authorization).
46
46
  getAccessToken={current.token ? readToken : undefined}
47
47
  />,
48
48
  );