@unoverse-platform/studio 0.1.90 → 0.1.91
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 +20 -12
- package/package.json +13 -3
- package/vendor/base/src/agent-mcp/discovery.ts +203 -0
- package/vendor/base/src/agent-mcp/handoff.ts +56 -0
- package/vendor/base/src/agent-mcp/index.ts +44 -0
- package/vendor/base/src/agent-mcp/invoke.ts +140 -0
- package/vendor/base/src/agent-mcp/lean.ts +111 -0
- package/vendor/base/src/agent-mcp/memoryContext.ts +104 -0
- package/vendor/base/src/agent-mcp/memoryIngest.ts +88 -0
- package/vendor/base/src/agent-mcp/rowComponents.ts +172 -0
- package/vendor/base/src/agent-mcp/tools.ts +134 -0
- package/vendor/base/src/agent-mcp/types.ts +130 -0
- package/vendor/base/src/auth/auth.ts +278 -0
- package/vendor/base/src/auth/authConfig.ts +73 -0
- package/vendor/base/src/boot.ts +245 -0
- package/vendor/base/src/definitions/definitions.ts +18 -0
- package/vendor/base/src/definitions/dsPackage.ts +45 -0
- package/vendor/base/src/definitions/fsCache.ts +128 -0
- package/vendor/base/src/definitions/index.ts +19 -0
- package/vendor/base/src/definitions/inputs.ts +30 -0
- package/vendor/base/src/definitions/load/compile/component.ts +80 -0
- package/vendor/base/src/definitions/load/compile/index.ts +12 -0
- package/vendor/base/src/definitions/load/compile/sections.ts +132 -0
- package/vendor/base/src/definitions/load/compile/template.ts +153 -0
- package/vendor/base/src/definitions/load/expand/includes.ts +27 -0
- package/vendor/base/src/definitions/load/expand/index.ts +17 -0
- package/vendor/base/src/definitions/load/expand/inherit.ts +169 -0
- package/vendor/base/src/definitions/load/expand/referenced.ts +164 -0
- package/vendor/base/src/definitions/load/expand/refs.ts +106 -0
- package/vendor/base/src/definitions/load/expand/walk.ts +91 -0
- package/vendor/base/src/definitions/load/load.ts +487 -0
- package/vendor/base/src/definitions/model.ts +238 -0
- package/vendor/base/src/definitions/registry/catalog.ts +643 -0
- package/vendor/base/src/definitions/registry/homes.ts +252 -0
- package/vendor/base/src/definitions/theme.ts +383 -0
- package/vendor/base/src/executor.ts +284 -0
- package/vendor/base/src/http/uwsFetchAdapter.ts +260 -0
- package/vendor/base/src/items/baseVersion.ts +96 -0
- package/vendor/base/src/items/collect.ts +199 -0
- package/vendor/base/src/items/fingerprint.ts +34 -0
- package/vendor/base/src/items/publish.ts +198 -0
- package/vendor/base/src/lint/context.mjs +56 -0
- package/vendor/base/src/lint/design/defs.mjs +46 -0
- package/vendor/base/src/lint/design/file.mjs +737 -0
- package/vendor/base/src/lint/design/index.d.mts +30 -0
- package/vendor/base/src/lint/design/index.mjs +996 -0
- package/vendor/base/src/lint/design/tokens.mjs +128 -0
- package/vendor/base/src/lint/design/vocabulary.mjs +181 -0
- package/vendor/base/src/lint/design/walk.mjs +444 -0
- package/vendor/base/src/lint/index.mjs +28 -0
- package/vendor/base/src/lint/nodes/blocks.mjs +63 -0
- package/vendor/base/src/lint/nodes/index.mjs +64 -0
- package/vendor/base/src/lint/nodes/load.mjs +126 -0
- package/vendor/base/src/lint/nodes/node.mjs +764 -0
- package/vendor/base/src/lint/nodes/package.mjs +112 -0
- package/vendor/base/src/lint/nodes/schema.mjs +75 -0
- package/vendor/base/src/logger.ts +33 -0
- package/vendor/base/src/manifests/compose.ts +539 -0
- package/vendor/base/src/manifests/executor/callback.ts +107 -0
- package/vendor/base/src/manifests/executor/context.ts +107 -0
- package/vendor/base/src/manifests/executor/index.ts +47 -0
- package/vendor/base/src/manifests/executor/lookup.ts +21 -0
- package/vendor/base/src/manifests/executor/memory.ts +62 -0
- package/vendor/base/src/manifests/executor/promise.ts +71 -0
- package/vendor/base/src/manifests/executor/refire.ts +34 -0
- package/vendor/base/src/manifests/executor/send.ts +65 -0
- package/vendor/base/src/manifests/executor/session.ts +46 -0
- package/vendor/base/src/manifests/executor/toolBridge.ts +266 -0
- package/vendor/base/src/manifests/index.ts +280 -0
- package/vendor/base/src/manifests/integrity.ts +93 -0
- package/vendor/base/src/manifests/rows.ts +33 -0
- package/vendor/base/src/manifests/runtime/auth/aws.ts +240 -0
- package/vendor/base/src/manifests/runtime/auth/oauth.ts +76 -0
- package/vendor/base/src/manifests/runtime/authorize.ts +159 -0
- package/vendor/base/src/manifests/runtime/channels/final.ts +171 -0
- package/vendor/base/src/manifests/runtime/channels/service.ts +125 -0
- package/vendor/base/src/manifests/runtime/channels/workflow.ts +310 -0
- package/vendor/base/src/manifests/runtime/context.ts +150 -0
- package/vendor/base/src/manifests/runtime/docstore/handlers.ts +547 -0
- package/vendor/base/src/manifests/runtime/docstore/index.ts +135 -0
- package/vendor/base/src/manifests/runtime/docstore/sectionizer.ts +184 -0
- package/vendor/base/src/manifests/runtime/docstore/store.ts +172 -0
- package/vendor/base/src/manifests/runtime/docstore/types.ts +60 -0
- package/vendor/base/src/manifests/runtime/duplex/audioLane.ts +165 -0
- package/vendor/base/src/manifests/runtime/duplex/session.ts +511 -0
- package/vendor/base/src/manifests/runtime/events.ts +223 -0
- package/vendor/base/src/manifests/runtime/helpers.ts +53 -0
- package/vendor/base/src/manifests/runtime/http/allowedHosts.ts +118 -0
- package/vendor/base/src/manifests/runtime/http/request.ts +637 -0
- package/vendor/base/src/manifests/runtime/http/response.ts +162 -0
- package/vendor/base/src/manifests/runtime/index.ts +57 -0
- package/vendor/base/src/manifests/runtime/loops/chunk.ts +98 -0
- package/vendor/base/src/manifests/runtime/loops/loop.ts +191 -0
- package/vendor/base/src/manifests/runtime/loops/paginate.ts +143 -0
- package/vendor/base/src/manifests/runtime/loops/poll.ts +169 -0
- package/vendor/base/src/manifests/runtime/narrate.ts +53 -0
- package/vendor/base/src/manifests/runtime/state.ts +171 -0
- package/vendor/base/src/manifests/runtime/templating.ts +110 -0
- package/vendor/base/src/manifests/runtime/tools/regionSkills.ts +125 -0
- package/vendor/base/src/manifests/runtime/tools/toolloop.ts +542 -0
- package/vendor/base/src/manifests/runtime/usage.ts +87 -0
- package/vendor/base/src/manifests/source.ts +187 -0
- package/vendor/base/src/manifests/watch.ts +67 -0
- package/vendor/base/src/markdown/document.ts +141 -0
- package/vendor/base/src/paths.ts +180 -0
- package/vendor/base/src/platform/audioManager.ts +100 -0
- package/vendor/base/src/platform/clientTransport.ts +65 -0
- package/vendor/base/src/platform/executionContext.ts +154 -0
- package/vendor/base/src/platform/index.ts +10 -0
- package/vendor/base/src/platform/pluginAPI.ts +146 -0
- package/vendor/base/src/platform/redis.ts +54 -0
- package/vendor/base/src/platform/serviceCalls.ts +222 -0
- package/vendor/base/src/pluginBase.ts +248 -0
- package/vendor/base/src/plugins/README.md +57 -0
- package/vendor/base/src/plugins/discovery.ts +87 -0
- package/vendor/base/src/plugins/index.ts +16 -0
- package/vendor/base/src/plugins/loader.ts +179 -0
- package/vendor/base/src/plugins/redis.ts +36 -0
- package/vendor/base/src/plugins/state.ts +53 -0
- package/vendor/base/src/registry.ts +95 -0
- package/vendor/base/src/security/permissions.ts +93 -0
- package/vendor/base/src/security/rules.ts +73 -0
- package/vendor/base/src/template/SafeExpression.ts +556 -0
- package/vendor/base/src/template/StringTemplateResolver.ts +75 -0
- package/vendor/base/src/template/index.ts +22 -0
- package/vendor/base/src/types.ts +50 -0
- package/vendor/base/src/workflow/GetMemoryService.ts +158 -0
- package/vendor/base/src/workflow/PlatformMemoryCore.ts +438 -0
- package/vendor/base/src/workflow/routing.ts +122 -0
- package/vendor/sdk/lib/core/store.ts +26 -6
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JWT authentication for the Unoverse MCP server.
|
|
3
|
+
*
|
|
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
|
|
6
|
+
* shared AUTH_ISSUER/AUTH_AUDIENCE via JWKS. The only difference is transport: this
|
|
7
|
+
* server speaks MCP over uWS + Web `Request`/`Response`, so the gate is a pure
|
|
8
|
+
* function over the Authorization header rather than node req/res.
|
|
9
|
+
*
|
|
10
|
+
* Secure-MCP per the MCP best-practices/authorization specs: default-deny, 401 +
|
|
11
|
+
* `WWW-Authenticate` pointing at RFC 9728 protected-resource-metadata.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { jwtVerify, createRemoteJWKSet, JWTPayload } from "jose";
|
|
15
|
+
import { isAuthEnabled, devIdentity } from "./authConfig.js";
|
|
16
|
+
|
|
17
|
+
// Read env lazily (not at module load) so it's correct regardless of when .env loads.
|
|
18
|
+
//
|
|
19
|
+
// THE ISSUER SPELLING IS PROVIDER-SPECIFIC AND jose COMPARES `iss` EXACTLY.
|
|
20
|
+
//
|
|
21
|
+
// Auth0 mints `iss` WITH a trailing slash; Cognito mints it WITHOUT one (verified against
|
|
22
|
+
// the pool's own discovery document: issuer =
|
|
23
|
+
// https://cognito-idp.<region>.amazonaws.com/<poolId>, no slash). This used to append a
|
|
24
|
+
// slash unconditionally, which matches Auth0 and can therefore NEVER match Cognito: every
|
|
25
|
+
// authenticated request on an AWS universe failed with `unexpected "iss" claim value` while
|
|
26
|
+
// sign-in, the pool, the client and the token were all correct (BPP, 2026-08-17).
|
|
27
|
+
//
|
|
28
|
+
// So accept BOTH spellings of the one configured issuer rather than one provider's
|
|
29
|
+
// convention. This is the same shape as the `aud`/`client_id` comparison below: it looks in
|
|
30
|
+
// two places, it does not accept fewer. A token from any other issuer is rejected exactly as
|
|
31
|
+
// before, because both candidates are built from AUTH_ISSUER itself.
|
|
32
|
+
const getIssuerBase = (): string => {
|
|
33
|
+
const raw = process.env.AUTH_ISSUER;
|
|
34
|
+
if (!raw || raw === "undefined") return "";
|
|
35
|
+
return raw.endsWith("/") ? raw.slice(0, -1) : raw;
|
|
36
|
+
};
|
|
37
|
+
/** Both spellings of the configured issuer, for jose's exact `iss` comparison. */
|
|
38
|
+
const getIssuerCandidates = (): string[] => {
|
|
39
|
+
const base = getIssuerBase();
|
|
40
|
+
return base ? [base, `${base}/`] : [];
|
|
41
|
+
};
|
|
42
|
+
const getAudience = (): string | undefined => process.env.AUTH_AUDIENCE;
|
|
43
|
+
|
|
44
|
+
let jwks: ReturnType<typeof createRemoteJWKSet> | null = null;
|
|
45
|
+
function getJWKS() {
|
|
46
|
+
const base = getIssuerBase();
|
|
47
|
+
if (!jwks && base) {
|
|
48
|
+
jwks = createRemoteJWKSet(new URL(`${base}/.well-known/jwks.json`));
|
|
49
|
+
}
|
|
50
|
+
return jwks;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface AuthUser {
|
|
54
|
+
id: string;
|
|
55
|
+
email?: string;
|
|
56
|
+
roles: string[];
|
|
57
|
+
permissions: string[];
|
|
58
|
+
raw: JWTPayload;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Parse array claims, handling Auth0-style namespaced claims (e.g. https://…/roles). */
|
|
62
|
+
function parseArrayClaim(payload: JWTPayload, claimName: string): string[] {
|
|
63
|
+
const direct = payload[claimName];
|
|
64
|
+
if (Array.isArray(direct)) return direct as string[];
|
|
65
|
+
const namespaced = Object.keys(payload).find(
|
|
66
|
+
(k) => k.endsWith(`/${claimName}`) || k.endsWith(`/claims/${claimName}`),
|
|
67
|
+
);
|
|
68
|
+
if (namespaced && Array.isArray(payload[namespaced])) return payload[namespaced] as string[];
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Parse a string claim, handling Auth0-style namespaced claims (e.g. https://…/email).
|
|
73
|
+
* Auth0 emits custom claims (email on the access token) ONLY namespaced, so a plain
|
|
74
|
+
* `payload.email` read returns undefined and silently no-ops every email-keyed feature
|
|
75
|
+
* (CRM resolve). Same namespace-aware read as parseArrayClaim. See docs/AUTH_TOKEN_FLOW.md. */
|
|
76
|
+
function parseStringClaim(payload: JWTPayload, claimName: string): string | undefined {
|
|
77
|
+
const direct = payload[claimName];
|
|
78
|
+
if (typeof direct === "string") return direct;
|
|
79
|
+
const namespaced = Object.keys(payload).find(
|
|
80
|
+
(k) => k.endsWith(`/${claimName}`) || k.endsWith(`/claims/${claimName}`),
|
|
81
|
+
);
|
|
82
|
+
if (namespaced && typeof payload[namespaced] === "string") return payload[namespaced] as string;
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Validate a bearer token and extract the user, or null if invalid/unconfigured. */
|
|
87
|
+
export async function validateJWT(token: string): Promise<AuthUser | null> {
|
|
88
|
+
const jwksSet = getJWKS();
|
|
89
|
+
const issuers = getIssuerCandidates();
|
|
90
|
+
if (!jwksSet || issuers.length === 0) {
|
|
91
|
+
console.warn("[auth] OIDC not configured (AUTH_ISSUER missing)");
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
// AUDIENCE IS CHECKED HERE, NOT BY jose, BECAUSE PROVIDERS NAME IT DIFFERENTLY.
|
|
96
|
+
//
|
|
97
|
+
// Auth0 access tokens carry the API identifier in `aud`. Cognito access tokens have NO
|
|
98
|
+
// `aud` at all — the same value lives in `client_id`, and `aud` appears only on its ID
|
|
99
|
+
// tokens. Verified 2026-08-03 against a real token: {iss, client_id, token_use:"access",
|
|
100
|
+
// ...} with no aud. So `jwtVerify(..., { audience })` can never pass on AWS, and every
|
|
101
|
+
// authenticated request there failed with `missing required "aud" claim` on a
|
|
102
|
+
// deployment that was otherwise entirely correct.
|
|
103
|
+
//
|
|
104
|
+
// Signature, issuer and expiry are still jose's and unchanged. Only the comparison moves
|
|
105
|
+
// out, so it can read whichever claim the provider used. A token matching neither is
|
|
106
|
+
// rejected exactly as before — this looks in two places, it does not accept fewer.
|
|
107
|
+
const { payload } = await jwtVerify(token, jwksSet, { issuer: issuers });
|
|
108
|
+
const expected = getAudience();
|
|
109
|
+
if (expected) {
|
|
110
|
+
const claimed = payload.aud ?? (payload as Record<string, unknown>).client_id;
|
|
111
|
+
const values = Array.isArray(claimed) ? claimed.map(String) : claimed ? [String(claimed)] : [];
|
|
112
|
+
if (!values.includes(expected)) {
|
|
113
|
+
console.error(
|
|
114
|
+
`[auth] JWT validation failed: audience mismatch (expected ${expected}, token had ${values.join(", ") || "none"})`,
|
|
115
|
+
);
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
id: payload.sub || "unknown",
|
|
121
|
+
email: parseStringClaim(payload, "email"),
|
|
122
|
+
roles: parseArrayClaim(payload, "roles"),
|
|
123
|
+
permissions: parseArrayClaim(payload, "permissions"),
|
|
124
|
+
raw: payload,
|
|
125
|
+
};
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error("[auth] JWT validation failed:", error);
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export type AuthResult = { ok: true; userId: string } | { ok: false };
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Default-deny gate over the Authorization header.
|
|
136
|
+
* - auth DISABLED (dev) → pass as the fixed dev identity (never undefined).
|
|
137
|
+
* - auth ENABLED → require a valid bearer for AUTH_ISSUER/AUTH_AUDIENCE.
|
|
138
|
+
*/
|
|
139
|
+
export async function authorize(authHeader: string | null | undefined): Promise<AuthResult> {
|
|
140
|
+
if (!isAuthEnabled()) return { ok: true, userId: devIdentity().id };
|
|
141
|
+
if (!authHeader) return { ok: false };
|
|
142
|
+
const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : authHeader;
|
|
143
|
+
if (!token) return { ok: false };
|
|
144
|
+
const user = await validateJWT(token);
|
|
145
|
+
return user ? { ok: true, userId: user.id } : { ok: false };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The permission a token must carry to reach the HOSTED workflow builder. Gated on
|
|
150
|
+
* this exact string (Auth0 API permission on the gravity-api audience), NOT on a
|
|
151
|
+
* role name — end-user chat tokens are valid on the same audience, so "authenticated"
|
|
152
|
+
* is not enough; only "authenticated AND may author workflows" passes. This is the
|
|
153
|
+
* platform's first authZ check (docs/AUTH_REMEDIATION.md Phase 1b).
|
|
154
|
+
*/
|
|
155
|
+
export const BUILDER_PERMISSION = "workflow:author";
|
|
156
|
+
|
|
157
|
+
export type BuilderAuthResult = { ok: true; user: AuthUser } | { ok: false; reason: "unauthenticated" | "forbidden" };
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Gate for the hosted builder route (public :4105/mcp-builder). Stricter than
|
|
161
|
+
* `authorize` in two ways:
|
|
162
|
+
* - FAIL-CLOSED when auth is disabled: this route must never be open, so an
|
|
163
|
+
* AUTH_ENABLED=false deployment simply cannot serve it (returns forbidden).
|
|
164
|
+
* Local dev uses the ungated LOOPBACK route (:4106) instead — never this one.
|
|
165
|
+
* - Requires the BUILDER_PERMISSION claim, not just a valid bearer.
|
|
166
|
+
* Returns the full user so a later step can bind the session to a workflow grant.
|
|
167
|
+
*/
|
|
168
|
+
export async function authorizeBuilder(authHeader: string | null | undefined): Promise<BuilderAuthResult> {
|
|
169
|
+
// No fail-open: unlike `authorize`, disabled auth does NOT admit a dev identity here.
|
|
170
|
+
if (!isAuthEnabled()) return { ok: false, reason: "forbidden" };
|
|
171
|
+
if (!authHeader) return { ok: false, reason: "unauthenticated" };
|
|
172
|
+
const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : authHeader;
|
|
173
|
+
if (!token) return { ok: false, reason: "unauthenticated" };
|
|
174
|
+
const user = await validateJWT(token);
|
|
175
|
+
if (!user) return { ok: false, reason: "unauthenticated" };
|
|
176
|
+
if (!user.permissions.includes(BUILDER_PERMISSION)) return { ok: false, reason: "forbidden" };
|
|
177
|
+
return { ok: true, user };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* NO ADMIN GATE HERE, DELIBERATELY. `ADMIN_ROLE` and `authorizeAdmin` lived at this spot for
|
|
182
|
+
* one day. They were the right fix for the credential store and the wrong shape for the
|
|
183
|
+
* platform: a second bespoke gate beside `authorizeBuilder`, which is how five modules ended
|
|
184
|
+
* up holding five different answers to one question.
|
|
185
|
+
*
|
|
186
|
+
* Authorization now lives in ONE place — `apps/unoverse/server/src/security/` — where a
|
|
187
|
+
* table names every route's requirement and one function reads it. This module keeps what is
|
|
188
|
+
* genuinely shared: token validation, the builder gate that predates the table, and the
|
|
189
|
+
* discovery metadata. See security/policy.ts.
|
|
190
|
+
*/
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Public base URL for advertised OAuth metadata. Prefer the configured canonical URL
|
|
194
|
+
* (UNOVERSE_PUBLIC_URL) over the request `Host` header — the header is client-controlled,
|
|
195
|
+
* so reflecting it into discovery metadata is a (minor) spoofing vector.
|
|
196
|
+
*/
|
|
197
|
+
function resourceBase(host: string): string {
|
|
198
|
+
const configured = process.env.UNOVERSE_PUBLIC_URL?.replace(/\/$/, "");
|
|
199
|
+
if (configured) return configured;
|
|
200
|
+
const protocol = host.includes("localhost") || host.startsWith("127.") ? "http" : "https";
|
|
201
|
+
return `${protocol}://${host}`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** The 401 challenge — points clients at the RFC 9728 protected-resource-metadata. */
|
|
205
|
+
export function unauthorizedResponse(host: string): Response {
|
|
206
|
+
const resourceMetadataUrl = `${resourceBase(host)}/.well-known/oauth-protected-resource`;
|
|
207
|
+
return new Response(
|
|
208
|
+
JSON.stringify({ jsonrpc: "2.0", error: { code: -32001, message: "Unauthorized" }, id: null }),
|
|
209
|
+
{
|
|
210
|
+
status: 401,
|
|
211
|
+
headers: {
|
|
212
|
+
"content-type": "application/json",
|
|
213
|
+
"WWW-Authenticate": `Bearer resource_metadata="${resourceMetadataUrl}"`,
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** OAuth Authorization Server Metadata (for MCP Inspector / discovery). */
|
|
220
|
+
export function getOAuthAuthorizationServerMetadata(): object {
|
|
221
|
+
const issuer = process.env.AUTH_ISSUER;
|
|
222
|
+
const audience = process.env.AUTH_AUDIENCE;
|
|
223
|
+
return {
|
|
224
|
+
issuer,
|
|
225
|
+
authorization_endpoint: audience
|
|
226
|
+
? `${issuer}/authorize?audience=${encodeURIComponent(audience)}`
|
|
227
|
+
: `${issuer}/authorize`,
|
|
228
|
+
token_endpoint: `${issuer}/oauth/token`,
|
|
229
|
+
registration_endpoint: `${issuer}/oidc/register`,
|
|
230
|
+
scopes_supported: ["openid", "profile", "email", "offline_access"],
|
|
231
|
+
response_types_supported: ["code"],
|
|
232
|
+
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
233
|
+
code_challenge_methods_supported: ["S256"],
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* What a REMOTE client needs to log in to this universe, served publicly.
|
|
239
|
+
*
|
|
240
|
+
* Canvas gets this from `window.__GRAVITY_CONFIG__`, injected into its own page
|
|
241
|
+
* (`apps/canvas/src/App.jsx:14`). A developer's local Studio is a different origin on a
|
|
242
|
+
* different machine, so it cannot read that. It has to ask the universe.
|
|
243
|
+
*
|
|
244
|
+
* This is the whole reason the tooling stays provider-agnostic: Studio hardcodes no
|
|
245
|
+
* issuer and no provider. It asks a universe who authenticates it and follows the answer,
|
|
246
|
+
* so Auth0, Okta, Entra or Keycloak all work with no change here or in Studio.
|
|
247
|
+
*
|
|
248
|
+
* NOTHING SECRET. `clientId` is a public OAuth client identifier by definition, it is
|
|
249
|
+
* already shipped inside Canvas's JavaScript, and it authorises nothing on its own.
|
|
250
|
+
* There is deliberately no client secret: a desktop tool cannot keep one.
|
|
251
|
+
*
|
|
252
|
+
* `authEnabled: false` is a real answer, not a failure. A universe with auth off tells a
|
|
253
|
+
* client not to bother logging in, rather than sending it to an issuer that is not there.
|
|
254
|
+
*/
|
|
255
|
+
export function getPublicAuthConfig(): object {
|
|
256
|
+
const issuer = process.env.AUTH_ISSUER;
|
|
257
|
+
const clientId = process.env.AUTH_CLIENT_ID;
|
|
258
|
+
return {
|
|
259
|
+
authEnabled: isAuthEnabled(),
|
|
260
|
+
issuer: issuer ?? null,
|
|
261
|
+
clientId: clientId ?? null,
|
|
262
|
+
audience: process.env.AUTH_AUDIENCE ?? null,
|
|
263
|
+
// The permission a developer's token needs before this universe will accept a
|
|
264
|
+
// publish. Named here so Studio can say "you are logged in but not allowed to
|
|
265
|
+
// publish" instead of failing at the first push.
|
|
266
|
+
publishPermission: "marketplace:publish",
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** OAuth 2.0 Protected Resource Metadata (RFC 9728); canonical URL preferred over Host. */
|
|
271
|
+
export function getOAuthProtectedResourceMetadata(host: string): object {
|
|
272
|
+
return {
|
|
273
|
+
resource: resourceBase(host),
|
|
274
|
+
authorization_servers: [process.env.AUTH_ISSUER],
|
|
275
|
+
scopes_supported: ["openid", "profile", "email"],
|
|
276
|
+
bearer_methods_supported: ["header"],
|
|
277
|
+
};
|
|
278
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical auth flag + startup contract.
|
|
3
|
+
*
|
|
4
|
+
* MIRRORED across services (apps/server, apps/mcp-server, apps/memory-server, and
|
|
5
|
+
* now apps/unoverse) — keep the copies in sync. Single source of truth:
|
|
6
|
+
* docs/AUTH_REMEDIATION.md (Phase 0).
|
|
7
|
+
*
|
|
8
|
+
* Rules:
|
|
9
|
+
* - Default: auth ENABLED. Only an explicit AUTH_ENABLED=false (or the deprecated
|
|
10
|
+
* DISABLE_AUTH=true) turns it off. Missing/typo'd flags read as ENABLED.
|
|
11
|
+
* - "Not configured" is NOT "disabled": if auth is enabled but OIDC is missing,
|
|
12
|
+
* the service refuses to boot (assertAuthStartup) rather than silently allow.
|
|
13
|
+
* - Disabled: a fixed dev identity is injected so downstream code always has auth.
|
|
14
|
+
* - Disabled + NODE_ENV=production: refuse to boot.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export interface DevIdentity {
|
|
18
|
+
id: string;
|
|
19
|
+
email: string;
|
|
20
|
+
roles: string[];
|
|
21
|
+
permissions: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const isTrue = (v: string | undefined): boolean => v?.toLowerCase() === "true";
|
|
25
|
+
|
|
26
|
+
/** Single source of truth: is JWT enforcement on? Default ON; only explicit opt-out disables. */
|
|
27
|
+
export function isAuthEnabled(): boolean {
|
|
28
|
+
if (isTrue(process.env.DISABLE_AUTH)) return false; // deprecated alias, still honored
|
|
29
|
+
return (process.env.AUTH_ENABLED ?? "true").toLowerCase() !== "false";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Is an OIDC provider configured (issuer + audience present)? */
|
|
33
|
+
export function isOidcConfigured(): boolean {
|
|
34
|
+
const issuer = process.env.AUTH_ISSUER;
|
|
35
|
+
return !!issuer && issuer !== "undefined" && !!process.env.AUTH_AUDIENCE;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Fixed identity injected on every request when auth is DISABLED (dev only).
|
|
39
|
+
* Deliberately carries NO roles and NO permissions: any gate accidentally built on
|
|
40
|
+
* `authorize()` instead of a fail-closed wrapper then denies rather than escalates. */
|
|
41
|
+
export function devIdentity(): DevIdentity {
|
|
42
|
+
return { id: "dev-user", email: "dev@local", roles: [], permissions: [] };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Validate auth configuration at boot. Fails closed:
|
|
47
|
+
* - enabled + OIDC missing → throw (refuse to start)
|
|
48
|
+
* - disabled + production → throw (refuse to start)
|
|
49
|
+
* Returns the resolved mode and logs the true state.
|
|
50
|
+
*/
|
|
51
|
+
export function assertAuthStartup(log: (msg: string) => void = console.warn): "enabled" | "disabled" {
|
|
52
|
+
const enabled = isAuthEnabled();
|
|
53
|
+
const isProd = process.env.NODE_ENV === "production";
|
|
54
|
+
|
|
55
|
+
if (enabled && !isOidcConfigured()) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
"[auth] Auth is ENABLED but OIDC is not configured (AUTH_ISSUER/AUTH_AUDIENCE missing). " +
|
|
58
|
+
"Refusing to start. For local dev without auth, set AUTH_ENABLED=false.",
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
if (!enabled && isProd) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
"[auth] Auth is DISABLED (AUTH_ENABLED=false / DISABLE_AUTH=true) while NODE_ENV=production. Refusing to start.",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
log(
|
|
68
|
+
enabled
|
|
69
|
+
? "[auth] ENABLED — JWT enforced on /mcp + /dev"
|
|
70
|
+
: "[auth] DISABLED — injecting dev identity (non-production only)",
|
|
71
|
+
);
|
|
72
|
+
return enabled ? "enabled" : "disabled";
|
|
73
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boot report — ONE readout at the end of startup, instead of a line per thing loaded.
|
|
3
|
+
*
|
|
4
|
+
* Boot used to narrate itself: 84 node ticks, 24 credential ticks, 27 "local wins"
|
|
5
|
+
* lines, each registration announcing itself as it happened. Two problems with that.
|
|
6
|
+
* It reads as noise rather than as a product, and a real failure (the plugin-state
|
|
7
|
+
* fetch that fails silently below) sits in the middle of it styled exactly like a
|
|
8
|
+
* success tick, so nobody sees it.
|
|
9
|
+
*
|
|
10
|
+
* The rule here: COUNT the healthy, NAME the anomalous. A node that loaded is a
|
|
11
|
+
* number; a node that was skipped, shadowed, disabled or missing metadata is a line
|
|
12
|
+
* with its name on it. Nothing diagnostic is lost, because the per-item detail is
|
|
13
|
+
* still emitted at debug level (LOG_LEVEL=debug) and the full catalog is one
|
|
14
|
+
* `GET /nodes` away.
|
|
15
|
+
*
|
|
16
|
+
* Collect from anywhere during boot, print once from index.ts.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const DEBUG = (process.env.LOG_LEVEL ?? "").toLowerCase() === "debug";
|
|
20
|
+
|
|
21
|
+
// The readout is a product surface: labels recede, values read, addresses invite the
|
|
22
|
+
// click. One accent, no boxes, and everything still greps clean because whole segments
|
|
23
|
+
// are wrapped rather than words. NO_COLOR is the standard opt-out.
|
|
24
|
+
const COLOR = !process.env.NO_COLOR;
|
|
25
|
+
const paint = (code: string) => (s: string) => (COLOR ? `\x1b[${code}m${s}\x1b[0m` : s);
|
|
26
|
+
const bold = paint("1");
|
|
27
|
+
const dim = paint("2");
|
|
28
|
+
const cyan = paint("36");
|
|
29
|
+
const yellow = paint("33");
|
|
30
|
+
const red = paint("31");
|
|
31
|
+
|
|
32
|
+
type Notice = { level: "warn" | "error"; text: string };
|
|
33
|
+
type Endpoint = { label: string; url: string; note?: string };
|
|
34
|
+
|
|
35
|
+
const state = {
|
|
36
|
+
startedAt: Date.now(),
|
|
37
|
+
packages: 0,
|
|
38
|
+
nodes: [] as string[],
|
|
39
|
+
credentials: [] as string[],
|
|
40
|
+
credentialsSkipped: 0,
|
|
41
|
+
components: null as { node: string; count: number } | null,
|
|
42
|
+
manifests: null as { nodes: number; credentials: number; shadowed: string[]; disabled: string[] } | null,
|
|
43
|
+
pluginStates: 0,
|
|
44
|
+
pluginStateError: null as string | null,
|
|
45
|
+
pluginsLocalWin: [] as string[],
|
|
46
|
+
pluginsFromNpm: [] as string[],
|
|
47
|
+
pluginsDisabled: [] as string[],
|
|
48
|
+
auth: null as string | null,
|
|
49
|
+
redis: null as string | null,
|
|
50
|
+
engine: null as string | null,
|
|
51
|
+
watching: null as string | null,
|
|
52
|
+
endpoints: [] as Endpoint[],
|
|
53
|
+
notices: [] as Notice[],
|
|
54
|
+
printed: false,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** Per-item detail, kept for LOG_LEVEL=debug. Silent otherwise. */
|
|
58
|
+
function detail(line: string): void {
|
|
59
|
+
if (DEBUG) console.log(line);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const boot = {
|
|
63
|
+
packages(n: number) {
|
|
64
|
+
state.packages = n;
|
|
65
|
+
detail(`[boot] discovered ${n} node packages`);
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
node(type: string) {
|
|
69
|
+
state.nodes.push(type);
|
|
70
|
+
detail(`[boot] node ${type}`);
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
credential(name: string, skipped = false) {
|
|
74
|
+
if (skipped) {
|
|
75
|
+
// First-wins is the DESIGNED behaviour for a shared credential (several AWS
|
|
76
|
+
// packages legitimately declare awsCredential), so it is not a warning and
|
|
77
|
+
// never belonged in the readout. It was 48 warning lines a boot.
|
|
78
|
+
state.credentialsSkipped++;
|
|
79
|
+
detail(`[boot] credential ${name} already registered, kept the first`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
state.credentials.push(name);
|
|
83
|
+
detail(`[boot] credential ${name}`);
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
components(node: string, count: number) {
|
|
87
|
+
state.components = { node, count };
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
manifests(m: { nodes: number; credentials: number; shadowed: string[]; disabled: string[] }) {
|
|
91
|
+
state.manifests = m;
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
pluginStates(n: number) {
|
|
95
|
+
// Loaded once at boot and again after the engine comes up; the count is the same
|
|
96
|
+
// fact both times, so the report holds one of it rather than printing twice.
|
|
97
|
+
state.pluginStates = n;
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* A failed read of the plugin state table. Not a notice on its own: in merged mode the
|
|
102
|
+
* first read races the engine binding :4101 and is retried. It becomes a notice only
|
|
103
|
+
* if no state was ever loaded, which is the case where plugins really are missing.
|
|
104
|
+
*/
|
|
105
|
+
pluginStateFetchFailed(message: string) {
|
|
106
|
+
state.pluginStateError = message;
|
|
107
|
+
detail(`[boot] plugin state read failed: ${message}`);
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
pluginLocalWin(name: string, installedVersion?: string) {
|
|
111
|
+
state.pluginsLocalWin.push(name);
|
|
112
|
+
detail(`[boot] ${name}: local source wins over npm${installedVersion ? ` (npm has ${installedVersion})` : ""}`);
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
pluginFromNpm(name: string, source = "npm") {
|
|
116
|
+
state.pluginsFromNpm.push(name);
|
|
117
|
+
detail(`[boot] ${name}: loaded from ${source}`);
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
pluginDisabled(name: string) {
|
|
121
|
+
state.pluginsDisabled.push(name);
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
auth(posture: string) {
|
|
125
|
+
state.auth = posture;
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
redis(status: string) {
|
|
129
|
+
state.redis = status;
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
engine(where: string) {
|
|
133
|
+
state.engine = where;
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
/** Manifest hot reload is a dev affordance worth one line, because it changes what a save does. */
|
|
137
|
+
watching(dir: string) {
|
|
138
|
+
state.watching = dir;
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
endpoint(label: string, url: string, note?: string) {
|
|
142
|
+
state.endpoints.push({ label, url, note });
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Addresses are listed in the order someone reaches for them, not the order the
|
|
147
|
+
* listeners happened to bind. Studio is what a person opens; the internal runtime is
|
|
148
|
+
* what a machine calls.
|
|
149
|
+
*/
|
|
150
|
+
endpointOrder: ["Studio", "MCP", "Runtime", "Builder MCP"],
|
|
151
|
+
|
|
152
|
+
notice(level: "warn" | "error", text: string) {
|
|
153
|
+
state.notices.push({ level, text });
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* True until the report prints. Loaders that run BOTH at boot and later (the manifest
|
|
158
|
+
* watcher, install/uninstall) ask this: during boot they hand their result to the
|
|
159
|
+
* report, afterwards they log it themselves, because a reload with nothing else on
|
|
160
|
+
* screen is a real event and should say so.
|
|
161
|
+
*/
|
|
162
|
+
isBooting() {
|
|
163
|
+
return !state.printed;
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
/** Everything the report knows, for a status route or a test. */
|
|
167
|
+
snapshot() {
|
|
168
|
+
return { ...state };
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
print() {
|
|
172
|
+
if (state.printed) return;
|
|
173
|
+
state.printed = true;
|
|
174
|
+
const ready = ((Date.now() - state.startedAt) / 1000).toFixed(1);
|
|
175
|
+
|
|
176
|
+
const rows: Array<[string, string]> = [];
|
|
177
|
+
rows.push(["Nodes", `${state.nodes.length} registered from ${state.packages} packages`]);
|
|
178
|
+
if (state.manifests) {
|
|
179
|
+
const m = state.manifests;
|
|
180
|
+
const parts = [`${m.nodes} loaded`];
|
|
181
|
+
if (m.shadowed.length) parts.push(`${m.shadowed.length} shadowed by code`);
|
|
182
|
+
if (m.disabled.length) parts.push(`${m.disabled.length} disabled`);
|
|
183
|
+
rows.push(["Manifests", parts.join(", ")]);
|
|
184
|
+
}
|
|
185
|
+
// The skipped count is every re-declaration of a credential that already exists,
|
|
186
|
+
// which is the shared-credential design working. It is a debug fact, not a headline.
|
|
187
|
+
rows.push(["Credentials", `${state.credentials.length} types`]);
|
|
188
|
+
if (state.components) rows.push(["Components", `${state.components.count} in one ${state.components.node} node`]);
|
|
189
|
+
if (state.pluginStates) {
|
|
190
|
+
const parts = [`${state.pluginStates} states`];
|
|
191
|
+
if (state.pluginsLocalWin.length) parts.push(`${state.pluginsLocalWin.length} local over npm`);
|
|
192
|
+
if (state.pluginsFromNpm.length) parts.push(`${state.pluginsFromNpm.length} from npm`);
|
|
193
|
+
if (state.pluginsDisabled.length) parts.push(`${state.pluginsDisabled.length} disabled`);
|
|
194
|
+
rows.push(["Plugins", parts.join(", ")]);
|
|
195
|
+
}
|
|
196
|
+
if (state.engine) rows.push(["Engine", state.engine]);
|
|
197
|
+
if (state.redis) rows.push(["Redis", state.redis]);
|
|
198
|
+
// Last row before the addresses, because it qualifies every one of them.
|
|
199
|
+
if (state.auth) rows.push(["Auth", state.auth]);
|
|
200
|
+
// Trimmed to the repo-relative path: absolute would be the longest string in the
|
|
201
|
+
// block, and the identifying part is the tail. cwd is not usable for this because
|
|
202
|
+
// the server is started from several directories (turbo, docker, the CLI).
|
|
203
|
+
if (state.watching)
|
|
204
|
+
rows.push(["Watching", `${state.watching.replace(/^.*?(?=apps\/)/, "")}, manifests reload on save`]);
|
|
205
|
+
|
|
206
|
+
const labelWidth = Math.max(...rows.map((r) => r[0].length), ...state.endpoints.map((e) => e.label.length));
|
|
207
|
+
const urlWidth = Math.max(0, ...state.endpoints.map((e) => e.url.length));
|
|
208
|
+
// Pad BEFORE painting: ANSI codes have width in padEnd's eyes and none on screen.
|
|
209
|
+
const out: string[] = ["", ` ${bold("unoverse")} ${dim(`ready in ${ready}s`)}`, ""];
|
|
210
|
+
for (const [label, value] of rows) out.push(` ${dim(label.padEnd(labelWidth))} ${value}`);
|
|
211
|
+
if (state.endpoints.length) {
|
|
212
|
+
const rank = (l: string) => {
|
|
213
|
+
const i = boot.endpointOrder.indexOf(l);
|
|
214
|
+
return i === -1 ? boot.endpointOrder.length : i;
|
|
215
|
+
};
|
|
216
|
+
state.endpoints.sort((a, b) => rank(a.label) - rank(b.label));
|
|
217
|
+
out.push("");
|
|
218
|
+
for (const e of state.endpoints) {
|
|
219
|
+
out.push(` ${dim(e.label.padEnd(labelWidth))} ${cyan(e.note ? e.url.padEnd(urlWidth) : e.url)}${e.note ? ` ${dim(e.note)}` : ""}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Anomalies are the point of the readout, so they sit at the bottom where the eye
|
|
224
|
+
// lands and they are the only thing here that is not a count.
|
|
225
|
+
if (state.pluginStateError && !state.pluginStates)
|
|
226
|
+
state.notices.push({
|
|
227
|
+
level: "error",
|
|
228
|
+
text: `no plugin state could be read (${state.pluginStateError}), so npm-installed packages did not load`,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const anomalies = [
|
|
232
|
+
...state.notices.map((n) => ` ${n.level === "error" ? red("✗") : yellow("!")} ${n.text}`),
|
|
233
|
+
...(state.manifests?.shadowed.length
|
|
234
|
+
? [` ${yellow("!")} code wins over manifest for: ${state.manifests.shadowed.join(", ")}`]
|
|
235
|
+
: []),
|
|
236
|
+
...(state.pluginsDisabled.length
|
|
237
|
+
? [` ${yellow("!")} disabled, so not loaded: ${state.pluginsDisabled.join(", ")}`]
|
|
238
|
+
: []),
|
|
239
|
+
];
|
|
240
|
+
if (anomalies.length) out.push("", ...anomalies);
|
|
241
|
+
out.push("");
|
|
242
|
+
if (!DEBUG) out.push(dim(" LOG_LEVEL=debug lists every node, credential and package as it loads."), "");
|
|
243
|
+
console.log(out.join("\n"));
|
|
244
|
+
},
|
|
245
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unoverse definition store — the FAÇADE. Split 2026-08-30 into organised
|
|
3
|
+
* subfolders (owner: files stay ~500-800 lines, folders group by concern);
|
|
4
|
+
* every existing import of `definitions/definitions.js` keeps working.
|
|
5
|
+
*
|
|
6
|
+
* registry/homes.ts where every kind lives; ref → folder resolution
|
|
7
|
+
* registry/catalog.ts listing, app manifests, state order, workbench views
|
|
8
|
+
* model.ts the served shapes + kind/word normalizers
|
|
9
|
+
* load/load.ts loadDefinition — the load core
|
|
10
|
+
* load/expand.ts Ref/atom composition, $include, state inheritance
|
|
11
|
+
* load/compile/ the component-tree and template compilers, one file per job
|
|
12
|
+
*/
|
|
13
|
+
export * from "./registry/homes.js";
|
|
14
|
+
export * from "./registry/catalog.js";
|
|
15
|
+
export * from "./model.js";
|
|
16
|
+
export * from "./load/load.js";
|
|
17
|
+
export * from "./load/expand/index.js";
|
|
18
|
+
export * from "./load/compile/index.js";
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The design system as an INSTALLED PACKAGE, wherever the HOST put it.
|
|
3
|
+
*
|
|
4
|
+
* Every other tier in `definitions.ts` and `theme.ts` is a path under the content home,
|
|
5
|
+
* which is the developer's own project. That is right for a universe, where the design
|
|
6
|
+
* system is installed INTO the deployment, and wrong for Studio, which is a TOOL run
|
|
7
|
+
* against somebody else's folder. Studio depends on `@unoverse-platform/marketplace`, so
|
|
8
|
+
* it already carries the design system in its own node_modules, and then went looking for
|
|
9
|
+
* it in a project that will never have one.
|
|
10
|
+
*
|
|
11
|
+
* A new Studio project therefore had NO design system at all: zero components, zero atoms,
|
|
12
|
+
* no foundation styles (measured against an empty home: 17 components and 30 atoms resolve
|
|
13
|
+
* with this, none without). Primitives still render, so the scaffolded Welcome component
|
|
14
|
+
* looks fine and hides it; the failure surfaces later, on the first `Card` or token. And it
|
|
15
|
+
* is silent either way, because both callers fall through to a path rather than raising.
|
|
16
|
+
*
|
|
17
|
+
* ASKED OF NODE rather than walked, so it is found from wherever the host is installed.
|
|
18
|
+
* NOT a dependency: `marketplace` depends on THIS package, so declaring it would be a
|
|
19
|
+
* cycle. Absent is a normal answer (a universe resolves from a tier above), hence the
|
|
20
|
+
* catch, and the answer is memoized because a failed resolve is the expensive case.
|
|
21
|
+
*
|
|
22
|
+
* ITS OWN MODULE so `definitions.ts` and `theme.ts` share one implementation without
|
|
23
|
+
* importing each other. They resolve different kinds out of the same bundle
|
|
24
|
+
* (components/atoms, and styles), and two copies of this would drift into two answers
|
|
25
|
+
* about where the design system is.
|
|
26
|
+
*
|
|
27
|
+
* SEARCHED LAST by both callers, after every project-local tier, so nothing changes for a
|
|
28
|
+
* host that already resolves: this fires only where the alternative was nothing at all.
|
|
29
|
+
*/
|
|
30
|
+
import { join, dirname } from "path";
|
|
31
|
+
import { createRequire } from "module";
|
|
32
|
+
|
|
33
|
+
let cached: string | null | undefined;
|
|
34
|
+
|
|
35
|
+
/** The bundle's `definitions/` folder, or null when the package is not installed. */
|
|
36
|
+
export function packagedDesignSystem(): string | null {
|
|
37
|
+
if (cached !== undefined) return cached;
|
|
38
|
+
try {
|
|
39
|
+
const pkg = createRequire(import.meta.url).resolve("@unoverse-platform/marketplace/package.json");
|
|
40
|
+
cached = join(dirname(pkg), "definitions");
|
|
41
|
+
} catch {
|
|
42
|
+
cached = null;
|
|
43
|
+
}
|
|
44
|
+
return cached;
|
|
45
|
+
}
|