@horizonmedia/blu-sdk 0.0.0-stage → 1.8.4

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/index.js ADDED
@@ -0,0 +1,34 @@
1
+ import {
2
+ ApiError,
3
+ beginBluSignIn,
4
+ completeBluSignIn,
5
+ useApiClient,
6
+ useAuthToken,
7
+ useClient,
8
+ useEnvironment,
9
+ useEventBus,
10
+ useHost,
11
+ useOptionalHost,
12
+ useRoles,
13
+ useRoute,
14
+ useShell,
15
+ useTheme,
16
+ useUser
17
+ } from "./chunks/chunk-M7GVSOGQ.js";
18
+ export {
19
+ ApiError,
20
+ beginBluSignIn,
21
+ completeBluSignIn,
22
+ useApiClient,
23
+ useAuthToken,
24
+ useClient,
25
+ useEnvironment,
26
+ useEventBus,
27
+ useHost,
28
+ useOptionalHost,
29
+ useRoles,
30
+ useRoute,
31
+ useShell,
32
+ useTheme,
33
+ useUser
34
+ };
package/package.json CHANGED
@@ -1,6 +1,56 @@
1
1
  {
2
2
  "name": "@horizonmedia/blu-sdk",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.8.4",
4
+ "description": "The Blu SDK: a module's front end (@blu/sdk), isolated iframe modules (@blu/sdk/frame), the module's API (@blu/sdk/server) and running a module on its own (@blu/sdk/dev).",
5
+ "type": "module",
6
+ "license": "UNLICENSED",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/HorizonMedia/blu-composable-experience.git",
10
+ "directory": "packages/sdk"
11
+ },
12
+ "homepage": "https://github.com/HorizonMedia/blu-composable-experience/tree/main/packages/sdk#readme",
13
+ "publishConfig": {
14
+ "registry": "https://registry.npmjs.org",
15
+ "access": "public"
16
+ },
17
+ "sideEffects": false,
18
+ "bin": {
19
+ "blu": "bin/blu.js"
20
+ },
21
+ "exports": {
22
+ ".": {
23
+ "types": "./types/index.d.ts",
24
+ "default": "./index.js"
25
+ },
26
+ "./frame": {
27
+ "types": "./types/frame.d.ts",
28
+ "default": "./frame.js"
29
+ },
30
+ "./server": {
31
+ "types": "./types/server.d.ts",
32
+ "default": "./server.js"
33
+ },
34
+ "./dev": {
35
+ "types": "./types/dev.d.ts",
36
+ "default": "./dev.js"
37
+ }
38
+ },
39
+ "dependencies": {
40
+ "@modelcontextprotocol/sdk": "^1.32.2",
41
+ "jose": "^6.0.0",
42
+ "zod": "^4.6.7"
43
+ },
44
+ "peerDependencies": {
45
+ "react": "^18.2.0 || ^19.0.0",
46
+ "react-dom": "^18.2.0 || ^19.0.0"
47
+ },
48
+ "peerDependenciesMeta": {
49
+ "react": {
50
+ "optional": true
51
+ },
52
+ "react-dom": {
53
+ "optional": true
54
+ }
55
+ }
56
+ }
package/server.js ADDED
@@ -0,0 +1,466 @@
1
+ import {
2
+ REGISTRY_URL,
3
+ bluIssuer,
4
+ bluShellOrigins,
5
+ buildRoutePath,
6
+ diffToolsAgainstManifest,
7
+ isLocalUrl
8
+ } from "./chunks/chunk-CSQHQLB7.js";
9
+
10
+ // src/server/index.js
11
+ import { createHash, timingSafeEqual } from "node:crypto";
12
+ import { createRemoteJWKSet, decodeJwt, errors as joseErrors, jwtVerify } from "jose";
13
+
14
+ // src/server/mcp.js
15
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
16
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
17
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
18
+ var link = (route, params, label) => ({ route, params, label });
19
+ function offeredTo(tools, principal) {
20
+ return tools.filter((t) => policyAllows(t.policy, principal) && (!principal?.actor || t.annotations?.readOnlyHint === true));
21
+ }
22
+ function policyAllows(policy, principal) {
23
+ const required = policy[principal.authMode];
24
+ return Boolean(required) && required.every((p) => principal.permissions.includes("*") || principal.permissions.includes(p));
25
+ }
26
+ function createMcpEndpoint({ name, version, authenticate, tools, manifest }) {
27
+ if (manifest) {
28
+ const drift = diffToolsAgainstManifest(tools, manifest);
29
+ if (drift.length) {
30
+ throw new Error(`[${name}] MCP tools don't match blu.module.json:
31
+ - ${drift.join("\n - ")}`);
32
+ }
33
+ }
34
+ const routes = manifest?.routes ?? [];
35
+ const validLinks = (links) => links.filter((l) => {
36
+ const route = routes.find((r) => r.name === l?.route);
37
+ const ok = Boolean(route && buildRoutePath(route, l.params ?? {}));
38
+ if (!ok) console.warn(`[${name}] dropped link to undeclared or incomplete route "${l?.route}"`);
39
+ return ok;
40
+ });
41
+ async function handle(req, res) {
42
+ const principal = req.principal;
43
+ const server = new McpServer({ name, version });
44
+ const visible = offeredTo(tools, principal);
45
+ for (const t of visible) {
46
+ server.registerTool(
47
+ t.name,
48
+ { title: t.title, description: t.description, inputSchema: t.input ?? {}, annotations: t.annotations },
49
+ async (args) => {
50
+ try {
51
+ const out = await t.run(principal, args ?? {});
52
+ return {
53
+ content: [
54
+ { type: "text", text: out.summary },
55
+ { type: "text", text: `data: ${JSON.stringify(out.data)}` }
56
+ ],
57
+ // Links travel as data beside the text (not in it), and only to declared routes.
58
+ ...out.links?.length ? { _meta: { "blu/links": validLinks(out.links) } } : {}
59
+ };
60
+ } catch (e) {
61
+ return { isError: true, content: [{ type: "text", text: e.message }] };
62
+ }
63
+ }
64
+ );
65
+ }
66
+ if (visible.length === 0) {
67
+ server.server.registerCapabilities({ tools: {} });
68
+ server.server.setRequestHandler((await import("@modelcontextprotocol/sdk/types.js")).ListToolsRequestSchema, () => ({ tools: [] }));
69
+ }
70
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0, enableJsonResponse: true });
71
+ res.on("close", () => {
72
+ transport.close();
73
+ server.close();
74
+ });
75
+ await server.connect(transport);
76
+ await transport.handleRequest(req, res, req.body);
77
+ }
78
+ return [
79
+ authenticate,
80
+ (req, res, next) => handle(req, res).catch(next)
81
+ ];
82
+ }
83
+ function createMcpWebHandler({ name, version, verify, tools, manifest }) {
84
+ if (manifest) {
85
+ const drift = diffToolsAgainstManifest(tools, manifest);
86
+ if (drift.length) throw new Error(`[${name}] MCP tools don't match blu.module.json:
87
+ - ${drift.join("\n - ")}`);
88
+ }
89
+ const routes = manifest?.routes ?? [];
90
+ const validLinks = (links) => links.filter((l) => {
91
+ const route = routes.find((r) => r.name === l?.route);
92
+ return Boolean(route && buildRoutePath(route, l.params ?? {}));
93
+ });
94
+ return async function handle(request) {
95
+ let principal;
96
+ try {
97
+ principal = await verify(request);
98
+ } catch (e) {
99
+ return Response.json({ error: e.code ?? "unauthorized", detail: e.message }, { status: e.status ?? 401 });
100
+ }
101
+ const server = new McpServer({ name, version });
102
+ const visible = offeredTo(tools, principal);
103
+ for (const t of visible) {
104
+ server.registerTool(t.name, { title: t.title, description: t.description, inputSchema: t.input ?? {}, annotations: t.annotations }, async (args) => {
105
+ try {
106
+ const out = await t.run(principal, args ?? {});
107
+ return {
108
+ content: [
109
+ { type: "text", text: out.summary },
110
+ { type: "text", text: `data: ${JSON.stringify(out.data)}` }
111
+ ],
112
+ ...out.links?.length ? { _meta: { "blu/links": validLinks(out.links) } } : {}
113
+ };
114
+ } catch (e) {
115
+ return { isError: true, content: [{ type: "text", text: e.message }] };
116
+ }
117
+ });
118
+ }
119
+ if (visible.length === 0) {
120
+ server.server.registerCapabilities({ tools: {} });
121
+ server.server.setRequestHandler((await import("@modelcontextprotocol/sdk/types.js")).ListToolsRequestSchema, () => ({ tools: [] }));
122
+ }
123
+ const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: void 0, enableJsonResponse: true });
124
+ await server.connect(transport);
125
+ try {
126
+ return await transport.handleRequest(request);
127
+ } finally {
128
+ queueMicrotask(() => server.close().catch(() => {
129
+ }));
130
+ }
131
+ };
132
+ }
133
+
134
+ // src/server/index.js
135
+ var ENTRY_POINTS = ["blu", "service", "team", "api-key"];
136
+ var AuthError = class extends Error {
137
+ constructor(status, code, detail) {
138
+ super(detail);
139
+ this.status = status;
140
+ this.code = code;
141
+ }
142
+ };
143
+ function jwtFailure(err, issuerLabel) {
144
+ if (err instanceof AuthError) return err;
145
+ const detail = err instanceof joseErrors.JWTExpired ? `${issuerLabel} token expired` : err instanceof joseErrors.JWSSignatureVerificationFailed ? `signature does not verify against the ${issuerLabel} JWKS (tampered or foreign token)` : `${issuerLabel}: ${err.code ?? err.message}`;
146
+ return new AuthError(401, "invalid_token", detail);
147
+ }
148
+ var principalKey = (authMode, issuer, subject) => `${authMode}|${issuer}|${subject}`;
149
+ var isDevIdentity = (payload) => Array.isArray(payload.amr) && payload.amr.includes("mock");
150
+ function bluPlatformAuth({ issuer, jwksUrl, moduleId, roleMap }) {
151
+ const audience = `blu-module:${moduleId}`;
152
+ const jwks = createRemoteJWKSet(new URL(jwksUrl), { cooldownDuration: 5e3, cacheMaxAge: 10 * 6e4 });
153
+ return {
154
+ mode: "blu",
155
+ kind: "bearer",
156
+ issuer,
157
+ accepts: (claims) => (claims.token_use ?? "user") === "user",
158
+ async authenticate(token) {
159
+ let payload;
160
+ try {
161
+ ({ payload } = await jwtVerify(token, jwks, { issuer, audience, algorithms: ["RS256"] }));
162
+ } catch (e) {
163
+ throw jwtFailure(e, "Blu");
164
+ }
165
+ const scope = Array.isArray(payload.tenant_scope) ? payload.tenant_scope : [];
166
+ if (!payload.cid || !scope.includes(payload.cid)) {
167
+ throw new AuthError(403, "no_active_client", "Blu token has no active client in scope.");
168
+ }
169
+ const roles = payload.roles ?? [];
170
+ return {
171
+ authMode: "blu",
172
+ kind: "user",
173
+ issuer,
174
+ subject: payload.sub,
175
+ key: principalKey("blu", issuer, payload.sub),
176
+ devIdentity: isDevIdentity(payload),
177
+ displayName: payload.name ?? payload.email ?? payload.sub,
178
+ email: payload.email,
179
+ provider: payload.provider,
180
+ // how Blu authenticated the user (e.g. "entra")
181
+ via: payload.azp,
182
+ actor: payload.act?.sub,
183
+ clientId: payload.cid,
184
+ allowedClients: scope,
185
+ roles,
186
+ permissions: [...new Set(roles.flatMap((r) => roleMap[r] ?? []))]
187
+ };
188
+ }
189
+ };
190
+ }
191
+ function bluServiceAuth({ issuer, jwksUrl, moduleId }) {
192
+ const jwks = createRemoteJWKSet(new URL(jwksUrl), { cooldownDuration: 5e3, cacheMaxAge: 10 * 6e4 });
193
+ return {
194
+ mode: "service",
195
+ kind: "bearer",
196
+ issuer,
197
+ accepts: (claims) => claims.token_use === "service",
198
+ async authenticate(token, req) {
199
+ let payload;
200
+ try {
201
+ ({ payload } = await jwtVerify(token, jwks, { issuer, audience: `blu-module:${moduleId}`, algorithms: ["RS256"] }));
202
+ } catch (e) {
203
+ throw jwtFailure(e, "Blu service");
204
+ }
205
+ const clients = payload.clients ?? [];
206
+ const asked = req.get("x-client-id") ?? req.query.clientId;
207
+ const clientId = asked ?? (clients.length === 1 ? clients[0] : null);
208
+ if (!clientId) throw new AuthError(400, "client_required", "Service calls must name a client with X-Client-Id.");
209
+ if (!clients.includes(clientId)) {
210
+ throw new AuthError(403, "client_not_granted", `Module "${moduleId}" isn't enabled for client "${clientId}", so service "${payload.sub}" can't act for it.`);
211
+ }
212
+ return {
213
+ authMode: "service",
214
+ kind: "service",
215
+ issuer,
216
+ subject: payload.sub,
217
+ key: principalKey("service", issuer, payload.sub),
218
+ displayName: payload.name ?? payload.sub,
219
+ clientId,
220
+ allowedClients: clients,
221
+ roles: [],
222
+ permissions: String(payload.scope ?? "").split(" ").filter(Boolean),
223
+ clientFromRequest: true
224
+ };
225
+ }
226
+ };
227
+ }
228
+ function teamIdpAuth({ issuer, jwksUrl, audience, toPrincipal }) {
229
+ const jwks = createRemoteJWKSet(new URL(jwksUrl), { cooldownDuration: 5e3 });
230
+ const map = toPrincipal ?? ((p) => ({
231
+ subject: p.sub,
232
+ displayName: p.name ?? p.sub,
233
+ email: p.email,
234
+ clientId: p.cid,
235
+ allowedClients: p.clients ?? [],
236
+ roles: p.role ? [p.role] : [],
237
+ permissions: p.perms ?? []
238
+ }));
239
+ return {
240
+ mode: "team",
241
+ kind: "bearer",
242
+ issuer,
243
+ accepts: () => true,
244
+ async authenticate(token) {
245
+ let payload;
246
+ try {
247
+ ({ payload } = await jwtVerify(token, jwks, { issuer, audience }));
248
+ } catch (e) {
249
+ throw jwtFailure(e, "team IdP");
250
+ }
251
+ const mapped = map(payload);
252
+ const principal = { authMode: "team", kind: "user", issuer, key: principalKey("team", issuer, mapped.subject), devIdentity: isDevIdentity(payload), ...mapped };
253
+ if (!principal.clientId || !principal.allowedClients.includes(principal.clientId)) {
254
+ throw new AuthError(403, "no_active_client", "Team token has no active client in scope.");
255
+ }
256
+ return principal;
257
+ }
258
+ };
259
+ }
260
+ var hashApiKey = (key) => createHash("sha256").update(key).digest("hex");
261
+ function apiKeyAuth({ keys }) {
262
+ const byHash = new Map(keys.map((k) => [k.hash, k]));
263
+ const windows = /* @__PURE__ */ new Map();
264
+ return {
265
+ mode: "api-key",
266
+ kind: "api-key",
267
+ async authenticate(presented, req) {
268
+ if (req.get("origin")) {
269
+ throw new AuthError(403, "api_key_from_browser", "API keys are server-to-server only; browser requests (with an Origin header) are refused.");
270
+ }
271
+ const digest = hashApiKey(presented);
272
+ let match = null;
273
+ for (const [hash, entry] of byHash) {
274
+ if (timingSafeEqual(Buffer.from(hash, "hex"), Buffer.from(digest, "hex"))) match = entry;
275
+ }
276
+ if (!match) throw new AuthError(401, "invalid_api_key", "Unknown or revoked API key.");
277
+ const limit = match.ratePerMinute ?? 60;
278
+ const now = Date.now();
279
+ const w = windows.get(match.id);
280
+ if (!w || now - w.start > 6e4) windows.set(match.id, { start: now, count: 1 });
281
+ else if (++w.count > limit) throw new AuthError(429, "rate_limited", `API key "${match.id}" exceeded ${limit}/min.`);
282
+ const asked = req.get("x-client-id") ?? req.query.clientId;
283
+ const clientId = asked ?? (match.clients.length === 1 ? match.clients[0] : null);
284
+ if (!clientId) throw new AuthError(400, "client_required", `Key covers [${match.clients.join(", ")}] \u2014 send X-Client-Id.`);
285
+ if (!match.clients.includes(clientId)) {
286
+ throw new AuthError(403, "client_not_granted", `API key "${match.id}" is not granted client "${clientId}".`);
287
+ }
288
+ return {
289
+ authMode: "api-key",
290
+ kind: "service",
291
+ issuer: "api-key",
292
+ subject: match.id,
293
+ key: principalKey("api-key", "api-key", match.id),
294
+ displayName: match.name,
295
+ clientId,
296
+ allowedClients: match.clients,
297
+ roles: [],
298
+ permissions: match.permissions,
299
+ clientFromRequest: true
300
+ };
301
+ }
302
+ };
303
+ }
304
+ function createModuleAuth({ strategies, apiUrl }) {
305
+ const bearer = strategies.filter((s) => s.kind === "bearer");
306
+ const apiKey = strategies.find((s) => s.kind === "api-key");
307
+ const modes = strategies.map((s) => s.mode);
308
+ const acceptsDevIdentities = Boolean(apiUrl) && isLocalUrl(apiUrl);
309
+ async function identify(req) {
310
+ {
311
+ const key = req.get("x-api-key");
312
+ const bearerToken = /^Bearer (.+)$/i.exec(req.get("authorization") ?? "")?.[1];
313
+ if (key && bearerToken) throw new AuthError(400, "ambiguous_credentials", "Send a bearer token OR an API key, not both.");
314
+ if (key) {
315
+ if (!apiKey) throw new AuthError(401, "mode_disabled", "API-key access is not enabled on this API.");
316
+ return apiKey.authenticate(key, req);
317
+ }
318
+ if (!bearerToken) {
319
+ throw new AuthError(401, "missing_credentials", `Send Authorization: Bearer <token> or X-Api-Key. Enabled modes: ${modes.join(", ")}. Cookies are never accepted.`);
320
+ }
321
+ let claims;
322
+ try {
323
+ claims = decodeJwt(bearerToken);
324
+ } catch {
325
+ throw new AuthError(401, "invalid_token", "Bearer token is not a JWT.");
326
+ }
327
+ const fromIssuer = bearer.filter((s) => s.issuer === claims.iss);
328
+ if (fromIssuer.length === 0) {
329
+ throw new AuthError(401, "untrusted_issuer", `Tokens from "${claims.iss}" are not accepted here. Trusted: ${[...new Set(bearer.map((s) => s.issuer))].join(", ") || "(none)"}.`);
330
+ }
331
+ const strategy = fromIssuer.find((s) => s.accepts(claims));
332
+ if (!strategy) {
333
+ throw new AuthError(401, "mode_disabled", `"${claims.token_use ?? "user"}" tokens from ${claims.iss} are not enabled on this API.`);
334
+ }
335
+ const principal = await strategy.authenticate(bearerToken, req);
336
+ if (principal.devIdentity && !acceptsDevIdentities) {
337
+ throw new AuthError(401, "dev_identity_refused", "This token comes from a local-only sign-in (no password). Only APIs running on a developer's machine accept it.");
338
+ }
339
+ return principal;
340
+ }
341
+ }
342
+ async function authenticate(req, res, next) {
343
+ try {
344
+ req.principal = await identify(req);
345
+ return next();
346
+ } catch (e) {
347
+ if (!(e instanceof AuthError)) return next(e);
348
+ return res.status(e.status).json({ error: e.code, detail: e.message });
349
+ }
350
+ }
351
+ function verifyRequest(request) {
352
+ const url = new URL(request.url);
353
+ return identify({ get: (h) => request.headers.get(h) ?? void 0, query: Object.fromEntries(url.searchParams) });
354
+ }
355
+ function allows(policy, principal, askedClientId) {
356
+ const required = policy[principal.authMode];
357
+ if (!required) return false;
358
+ if (!principal.clientFromRequest && askedClientId && askedClientId !== principal.clientId) return false;
359
+ return required.every((perm) => principal.permissions.includes("*") || principal.permissions.includes(perm));
360
+ }
361
+ function authorize(policy) {
362
+ for (const k of Object.keys(policy)) if (!ENTRY_POINTS.includes(k)) throw new Error(`authorize(): unknown entry point "${k}"`);
363
+ return (req, res, next) => {
364
+ const p = req.principal;
365
+ const required = policy[p.authMode];
366
+ if (!required) {
367
+ return res.status(403).json({
368
+ error: "entry_point_not_allowed",
369
+ detail: `This endpoint doesn't accept ${p.authMode} callers (allowed: ${Object.keys(policy).join(", ")}).`
370
+ });
371
+ }
372
+ const asked = req.query.clientId ?? req.get("x-client-id");
373
+ if (!p.clientFromRequest && asked && asked !== p.clientId) {
374
+ return res.status(403).json({
375
+ error: "client_mismatch",
376
+ detail: `Request asked for client "${asked}" but the credential is scoped to "${p.clientId}".`
377
+ });
378
+ }
379
+ const missing = required.filter((perm) => !p.permissions.includes("*") && !p.permissions.includes(perm));
380
+ if (missing.length) {
381
+ const via = p.authMode === "blu" ? ` (Blu role "${p.roles.join(", ")}")` : "";
382
+ return res.status(403).json({
383
+ error: "forbidden",
384
+ detail: `${p.authMode} principal "${p.subject}"${via} lacks "${missing.join(", ")}" on client "${p.clientId}".`
385
+ });
386
+ }
387
+ return next();
388
+ };
389
+ }
390
+ const requirePermission = (permission) => authorize(Object.fromEntries(modes.map((m) => [m, [permission]])));
391
+ return { authenticate, verifyRequest, allows, authorize, requirePermission, modes };
392
+ }
393
+ function createPrincipalDirectory() {
394
+ const people = /* @__PURE__ */ new Map();
395
+ function remember(req, _res, next) {
396
+ const p = req.principal;
397
+ if (p?.kind === "user") {
398
+ const now = (/* @__PURE__ */ new Date()).toISOString();
399
+ let entry = people.get(p.key);
400
+ p.isNew = !entry;
401
+ if (!entry) {
402
+ entry = { key: p.key, authMode: p.authMode, issuer: p.issuer, subject: p.subject, firstSeen: now };
403
+ people.set(p.key, entry);
404
+ }
405
+ Object.assign(entry, { displayName: p.displayName, email: p.email, lastSeen: now });
406
+ p.firstSeen = entry.firstSeen;
407
+ }
408
+ next();
409
+ }
410
+ return { remember, get: (key) => people.get(key) ?? null, list: () => [...people.values()] };
411
+ }
412
+ function enabledModes(value, all = ENTRY_POINTS) {
413
+ const set = new Set((value ?? all.join(",")).split(",").map((s) => s.trim()).filter(Boolean));
414
+ for (const m of set) if (!all.includes(m)) throw new Error(`Unknown auth mode "${m}"`);
415
+ return set;
416
+ }
417
+ function corsAllowlist({ origins, credentials = false }) {
418
+ const resolve = typeof origins === "function" ? origins : () => origins;
419
+ return async (req, res, next) => {
420
+ const origin = req.get("origin");
421
+ res.append("Vary", "Origin");
422
+ const allowed = origin ? new Set(await resolve()).has(origin) : false;
423
+ if (allowed) {
424
+ res.set("Access-Control-Allow-Origin", origin);
425
+ if (credentials) res.set("Access-Control-Allow-Credentials", "true");
426
+ if (req.method === "OPTIONS") {
427
+ res.set("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS");
428
+ res.set("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Client-Id");
429
+ res.set("Access-Control-Max-Age", "600");
430
+ return res.status(204).end();
431
+ }
432
+ } else if (req.method === "OPTIONS") {
433
+ return res.status(403).end();
434
+ }
435
+ return next();
436
+ };
437
+ }
438
+ function originLogger(name) {
439
+ return (req, _res, next) => {
440
+ const origin = req.get("origin") ?? "(no origin / server-to-server)";
441
+ const auth = req.get("x-api-key") ? "api-key" : req.get("authorization") ? "bearer" : "no-auth";
442
+ if (req.method === "OPTIONS") return next();
443
+ console.log(`[${name}] ${req.method} ${req.originalUrl} origin=${origin} ${auth}`);
444
+ next();
445
+ };
446
+ }
447
+ export {
448
+ AuthError,
449
+ REGISTRY_URL,
450
+ apiKeyAuth,
451
+ bluIssuer,
452
+ bluPlatformAuth,
453
+ bluServiceAuth,
454
+ bluShellOrigins,
455
+ corsAllowlist,
456
+ createMcpEndpoint,
457
+ createMcpWebHandler,
458
+ createModuleAuth,
459
+ createPrincipalDirectory,
460
+ enabledModes,
461
+ hashApiKey,
462
+ link,
463
+ originLogger,
464
+ policyAllows,
465
+ teamIdpAuth
466
+ };
package/types/dev.d.ts ADDED
@@ -0,0 +1,48 @@
1
+ import { ComponentType } from 'react';
2
+
3
+ /**
4
+ * Sign-in providers for a module's standalone app. Both end in the same thing:
5
+ * a bearer token the module's own API accepts.
6
+ *
7
+ * team — the team's own IdP (personas, signed in right here)
8
+ * blu — "Sign in with Blu": a redirect to Blu's own sign-in page (OAuth code
9
+ * + PKCE). The app gets a token for THIS module only and a refresh
10
+ * token bound to it — never a Blu session.
11
+ *
12
+ * Two runtime files configure the standalone app (no rebuild to change either):
13
+ * standalone.config.json — team-authored: title, which sign-in options to offer
14
+ * deployment.json — written per deployment: stage + this stage's URLs
15
+ */
16
+
17
+ interface StandaloneConfig {
18
+ moduleId: string;
19
+ title: string;
20
+ auth: {
21
+ /** The team's own IdP. Omit to disable. */
22
+ team?: {
23
+ label: string;
24
+ };
25
+ /** "Sign in with Blu" (Blu's sign-in page: Entra, or mock accounts locally). Omit to disable. */
26
+ blu?: {
27
+ label: string;
28
+ };
29
+ };
30
+ }
31
+ interface Deployment {
32
+ stage: string;
33
+ apiBaseUrl: string;
34
+ teamAuthUrl: string;
35
+ bluHostApi: string;
36
+ }
37
+
38
+ /**
39
+ * Mount the standalone app. Both config files are fetched at runtime, so the
40
+ * same build runs in every stage and auth options change without a rebuild.
41
+ */
42
+ declare function mountStandalone({ Page, rootId }: {
43
+ Page: ComponentType;
44
+ rootId?: string;
45
+ }): Promise<void>;
46
+
47
+ export { mountStandalone };
48
+ export type { Deployment, StandaloneConfig };