@betterportal/framework 10.1.43 → 10.1.45

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.
Files changed (70) hide show
  1. package/lib/adapters/h3.d.ts.map +1 -1
  2. package/lib/adapters/h3.js +94 -79
  3. package/lib/adapters/h3.js.map +1 -1
  4. package/lib/cli/bp.js +2 -2
  5. package/lib/cli/bp.js.map +1 -1
  6. package/lib/codegen/cli.js +11 -5
  7. package/lib/codegen/cli.js.map +1 -1
  8. package/lib/codegen/emitter.d.ts.map +1 -1
  9. package/lib/codegen/emitter.js +78 -55
  10. package/lib/codegen/emitter.js.map +1 -1
  11. package/lib/codegen/scanner.d.ts +14 -7
  12. package/lib/codegen/scanner.d.ts.map +1 -1
  13. package/lib/codegen/scanner.js +53 -35
  14. package/lib/codegen/scanner.js.map +1 -1
  15. package/lib/codegen/validate.d.ts.map +1 -1
  16. package/lib/codegen/validate.js +31 -14
  17. package/lib/codegen/validate.js.map +1 -1
  18. package/lib/contracts/binding.d.ts +3 -1
  19. package/lib/contracts/binding.d.ts.map +1 -1
  20. package/lib/contracts/binding.js +1 -1
  21. package/lib/contracts/binding.js.map +1 -1
  22. package/lib/contracts/controlPlane.d.ts +3 -3
  23. package/lib/contracts/controlPlane.d.ts.map +1 -1
  24. package/lib/contracts/manifest.d.ts +50 -8
  25. package/lib/contracts/manifest.d.ts.map +1 -1
  26. package/lib/contracts/manifest.js +16 -3
  27. package/lib/contracts/manifest.js.map +1 -1
  28. package/lib/contracts/platformConfig.d.ts +126 -4
  29. package/lib/contracts/platformConfig.d.ts.map +1 -1
  30. package/lib/contracts/platformConfig.js +28 -2
  31. package/lib/contracts/platformConfig.js.map +1 -1
  32. package/lib/contracts/registry.d.ts +59 -19
  33. package/lib/contracts/registry.d.ts.map +1 -1
  34. package/lib/contracts/route.d.ts +3 -3
  35. package/lib/contracts/route.d.ts.map +1 -1
  36. package/lib/contracts/streaming.d.ts +2 -2
  37. package/lib/contracts/view.d.ts +4 -4
  38. package/lib/contracts/view.d.ts.map +1 -1
  39. package/lib/contracts/view.js +2 -2
  40. package/lib/contracts/view.js.map +1 -1
  41. package/lib/index.d.ts +1 -0
  42. package/lib/index.d.ts.map +1 -1
  43. package/lib/index.js +1 -0
  44. package/lib/index.js.map +1 -1
  45. package/lib/runtime/configProvider.d.ts +5 -0
  46. package/lib/runtime/configProvider.d.ts.map +1 -1
  47. package/lib/runtime/configProvider.js +30 -1
  48. package/lib/runtime/configProvider.js.map +1 -1
  49. package/lib/runtime/element.d.ts +13 -0
  50. package/lib/runtime/element.d.ts.map +1 -0
  51. package/lib/runtime/element.js +39 -0
  52. package/lib/runtime/element.js.map +1 -0
  53. package/lib/runtime/llms.js +1 -1
  54. package/lib/runtime/media.d.ts +0 -1
  55. package/lib/runtime/media.d.ts.map +1 -1
  56. package/lib/runtime/media.js +0 -1
  57. package/lib/runtime/media.js.map +1 -1
  58. package/lib/runtime/registry.d.ts +8 -7
  59. package/lib/runtime/registry.d.ts.map +1 -1
  60. package/lib/runtime/registry.js +49 -33
  61. package/lib/runtime/registry.js.map +1 -1
  62. package/lib/runtime/statusViews.d.ts +3 -3
  63. package/lib/runtime/statusViews.d.ts.map +1 -1
  64. package/lib/runtime/statusViews.js +5 -5
  65. package/lib/runtime/statusViews.js.map +1 -1
  66. package/lib/runtime/view.d.ts +1 -1
  67. package/lib/runtime/view.d.ts.map +1 -1
  68. package/lib/runtime/view.js +7 -13
  69. package/lib/runtime/view.js.map +1 -1
  70. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"h3.d.ts","sourceRoot":"","sources":["../../src/adapters/h3.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC/E,OAAO,KAAK,EAAE,yBAAyB,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AACxG,OAAO,KAAK,EAAE,oBAAoB,EAAE,eAAe,EAAwD,MAAM,0BAA0B,CAAC;AAC5I,OAAO,KAAK,EAGV,WAAW,EAIX,mBAAmB,EACnB,oBAAoB,EAIrB,MAAM,uBAAuB,CAAC;AAI/B,OAAO,KAAK,EAAE,aAAa,EAAa,MAAM,sBAAsB,CAAC;AAErE,OAAO,EAKL,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACvB,MAAM,kBAAkB,CAAC;AA0B1B,MAAM,WAAW,4BAA4B;IAC3C,0BAA0B,CAAC,EAAE,CAC3B,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,uBAAuB,KAChC,yBAAyB,CAAC;IAC/B,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yGAAyG;IACzG,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,iBAAiB,EAAE,KAAK,EAAE,eAAe,KAAK,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,GAAG,aAAa,GAAG,SAAS,CAAC;IACnI;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,sBAAsB,EAAE,mBAAmB,CAAC,GAAG,OAAO,sBAAsB,EAAE,mBAAmB,CAAC;IAC1K,qEAAqE;IACrE,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,iBAAiB,EAAE,KAAK,EAAE,eAAe,KAAK,OAAO,CAAC,OAAO,CAAC,mBAAmB,CAAC,CAAC,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;CAC7I;AAOD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,WAAW,CAAC;IAChC,QAAQ,CAAC,eAAe,CAAC,EAAE,oBAAoB,CAAC;IAChD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC;IACvC;;;;OAIG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC7D,QAAQ,CAAC,YAAY,CAAC,EAAE;QACtB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;CACH;AA0GD,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAKvE;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,aAAa,CAAC,eAAe,CAAC,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAExG;AAuCD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,oBAAoB,EAC9B,GAAG,EAAE,iBAAiB,EACtB,OAAO,GAAE,4BAAiC,GACzC,IAAI,CA4HN;AA2uCD;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,iBAAiB,EACtB,QAAQ,EAAE,cAAc,EACxB,QAAQ,EAAE,cAAc,EACxB,OAAO,GAAE;IACP,MAAM,CAAC,EAAE,MAAM,QAAQ,GAAG,SAAS,CAAC;CAChC,GACL,IAAI,CAyCN"}
1
+ {"version":3,"file":"h3.d.ts","sourceRoot":"","sources":["../../src/adapters/h3.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC/E,OAAO,KAAK,EAAE,yBAAyB,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AACxG,OAAO,KAAK,EAAsB,oBAAoB,EAAE,eAAe,EAAwD,MAAM,0BAA0B,CAAC;AAChK,OAAO,KAAK,EAGV,WAAW,EAIX,mBAAmB,EACnB,oBAAoB,EAIrB,MAAM,uBAAuB,CAAC;AAI/B,OAAO,KAAK,EAAE,aAAa,EAAa,MAAM,sBAAsB,CAAC;AAErE,OAAO,EAKL,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACvB,MAAM,kBAAkB,CAAC;AA0B1B,MAAM,WAAW,4BAA4B;IAC3C,0BAA0B,CAAC,EAAE,CAC3B,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,uBAAuB,KAChC,yBAAyB,CAAC;IAC/B,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yGAAyG;IACzG,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,iBAAiB,EAAE,KAAK,EAAE,eAAe,KAAK,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,GAAG,aAAa,GAAG,SAAS,CAAC;IACnI;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,sBAAsB,EAAE,mBAAmB,CAAC,GAAG,OAAO,sBAAsB,EAAE,mBAAmB,CAAC;IAC1K,qEAAqE;IACrE,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,iBAAiB,EAAE,KAAK,EAAE,eAAe,KAAK,OAAO,CAAC,OAAO,CAAC,mBAAmB,CAAC,CAAC,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;CAC7I;AAOD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,WAAW,CAAC;IAChC,QAAQ,CAAC,eAAe,CAAC,EAAE,oBAAoB,CAAC;IAChD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC;IACvC;;;;OAIG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC7D,QAAQ,CAAC,YAAY,CAAC,EAAE;QACtB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;CACH;AA0GD,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAKvE;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,aAAa,CAAC,eAAe,CAAC,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAExG;AA2CD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,oBAAoB,EAC9B,GAAG,EAAE,iBAAiB,EACtB,OAAO,GAAE,4BAAiC,GACzC,IAAI,CAmGN;AA8wCD;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,iBAAiB,EACtB,QAAQ,EAAE,cAAc,EACxB,QAAQ,EAAE,cAAc,EACxB,OAAO,GAAE;IACP,MAAM,CAAC,EAAE,MAAM,QAAQ,GAAG,SAAS,CAAC;CAChC,GACL,IAAI,CAyCN"}
@@ -145,8 +145,11 @@ function chromeContentTypeParams(chrome) {
145
145
  }
146
146
  return params.length ? `; ${params.join("; ")}` : "";
147
147
  }
148
- function htmlContentType(themeId, mode, chrome) {
149
- return `text/html; theme=${themeId}; mode=${mode}${chromeContentTypeParams(chrome)}`;
148
+ function htmlContentType(mode, chrome) {
149
+ return `text/html; mode=${mode}${chromeContentTypeParams(chrome)}`;
150
+ }
151
+ function rendererFromEvent(event) {
152
+ return event.__bpApp?.shell?.renderer;
150
153
  }
151
154
  // -- Router registration ----------------------------------------------
152
155
  /**
@@ -162,7 +165,7 @@ export function createH3Router(registry, app, options = {}) {
162
165
  for (const method of route.methods) {
163
166
  const register = methodRegistrar(app, method);
164
167
  register(route.path, async (event) => {
165
- return withRequestObservability(event, route, method, options, (obs) => handleRouteRequest(registry.routes, route, method, event, obs, options));
168
+ return withRequestObservability(event, route, method, options, (obs) => handleRouteRequest(registry.routes, registry.dependencies ?? {}, route, method, event, obs, options));
166
169
  });
167
170
  }
168
171
  // Streaming routes (createStreamHandler) expose their frame stream at
@@ -198,40 +201,17 @@ export function createH3Router(registry, app, options = {}) {
198
201
  }
199
202
  // Generator path: framework drives the stream.
200
203
  if (typeof result === "object" && result !== null && Symbol.asyncIterator in result) {
201
- // Resolve theme renderer if `?_f=loc.frag` provided. The theme MUST be
202
- // disambiguated - with multiple themes registered, picking the first
203
- // match would silently render another theme's fragment. Prefer the
204
- // theme resolved from request context (__bpThemeId), then an explicit
205
- // `?_theme=` pin; only fall back to a cross-theme scan when exactly one
206
- // theme provides the fragment.
204
+ // Renderer identity comes only from the server-resolved app shell.
207
205
  const fragmentKey = rawQuery._f ?? undefined;
208
206
  let sseRender;
209
207
  if (fragmentKey) {
210
- const themeId = event.__bpThemeId
211
- ?? rawQuery._theme;
212
- if (themeId) {
213
- const resolved = resolveRenderer(route, themeId, "fragment", "GET", undefined, fragmentKey);
208
+ const renderer = rendererFromEvent(event);
209
+ if (renderer) {
210
+ const resolved = resolveRenderer(route, renderer, "fragment", "GET", undefined, fragmentKey);
214
211
  if (resolved?.renderer.sseRender) {
215
212
  sseRender = resolved.renderer.sseRender;
216
213
  }
217
214
  }
218
- else {
219
- // No theme context. Only render if the match is unambiguous across
220
- // themes; otherwise leave it to the JSON passthrough rather than guess.
221
- const matches = [];
222
- for (const candidateThemeId of Object.keys(route.themeRenderers)) {
223
- const resolved = resolveRenderer(route, candidateThemeId, "fragment", "GET", undefined, fragmentKey);
224
- if (resolved?.renderer.sseRender) {
225
- matches.push(resolved.renderer.sseRender);
226
- }
227
- }
228
- if (matches.length === 1) {
229
- sseRender = matches[0];
230
- }
231
- else if (matches.length > 1) {
232
- obs?.logger.warn("BP SSE: ambiguous fragment '{fragmentKey}' across {count} themes and no theme context; sending raw ticks", { fragmentKey, count: matches.length });
233
- }
234
- }
235
215
  }
236
216
  const stream = createEventStream(event);
237
217
  const iterable = result;
@@ -322,7 +302,7 @@ function routePathsMatch(left, right) {
322
302
  return false;
323
303
  return a.every((segment, index) => {
324
304
  const other = b[index];
325
- return segment === other || segment.startsWith(":") || other.startsWith(":");
305
+ return segment === other || pathParamName(segment) !== null || pathParamName(other) !== null;
326
306
  });
327
307
  }
328
308
  function routeMountServicePath(routeMount) {
@@ -431,7 +411,45 @@ function createRouteUiAttributes(url, options = {}, form = false) {
431
411
  attrs["hx-push-url"] = String(options.push);
432
412
  return attrs;
433
413
  }
434
- function createViewRenderContext(route, ctx, theme, mode, kind, key, status) {
414
+ function createElementResolver(extraContext, dependencyAliases) {
415
+ return (reference) => {
416
+ if (!reference.fragment.trim())
417
+ return { unavailable: "fragment_required" };
418
+ if (reference.service === "shell") {
419
+ const shellServiceId = extraContext.app.shell?.serviceId;
420
+ const origin = shellServiceId ? serviceOrigin(extraContext, shellServiceId) : null;
421
+ if (!shellServiceId || !origin)
422
+ return { unavailable: "shell_unavailable" };
423
+ const path = `/.well-known/bp/shell/fragment/${encodeURIComponent(reference.fragment)}`;
424
+ return { serviceId: shellServiceId, url: appendQuery(path, reference.args?.query, origin) };
425
+ }
426
+ if (!reference.path?.startsWith("/"))
427
+ return { unavailable: "service_path_required" };
428
+ const pluginId = dependencyAliases[reference.service] ?? reference.service;
429
+ const serviceIds = new Set(extraContext.tenant.services
430
+ .filter((service) => service.enabled && (service.serviceId === pluginId || service.id === pluginId))
431
+ .map((service) => service.id));
432
+ const mounts = extraContext.app.routes.filter((mount) => {
433
+ const servicePath = routeMountServicePath(mount);
434
+ return mount.enabled !== false
435
+ && serviceIds.has(mount.serviceId)
436
+ && Boolean(servicePath)
437
+ && routePathsMatch(servicePath, reference.path);
438
+ });
439
+ if (mounts.length !== 1)
440
+ return { unavailable: mounts.length ? "ambiguous_provider" : "service_unavailable" };
441
+ const mount = mounts[0];
442
+ const origin = serviceOrigin(extraContext, mount.serviceId);
443
+ if (!origin)
444
+ return { unavailable: "service_unavailable" };
445
+ const servicePath = fillAppPath(routeMountServicePath(mount), reference.args?.params);
446
+ return {
447
+ serviceId: mount.serviceId,
448
+ url: renderUrl(servicePath, { absolute: true, origin, query: reference.args?.query, fragment: reference.fragment })
449
+ };
450
+ };
451
+ }
452
+ function createViewRenderContext(route, ctx, dependencyAliases, renderer, mode, kind, key, status) {
435
453
  const current = (options = {}) => renderUrl(ctx.path, options);
436
454
  const path = (value, options = {}) => renderUrl(value, options);
437
455
  const routeUrl = (viewId, options) => ctx.routeUrl?.(viewId, options) ?? null;
@@ -440,14 +458,15 @@ function createViewRenderContext(route, ctx, theme, mode, kind, key, status) {
440
458
  const form = (url, options) => createRouteUiAttributes(url, options, true);
441
459
  return {
442
460
  request: { method: ctx.method, path: ctx.path, params: ctx.params, query: ctx.query },
443
- route: { viewId: route.viewId, path: route.path, theme, mode, kind, key, status },
461
+ route: { viewId: route.viewId, path: route.path, renderer, mode, kind, key, status },
444
462
  url: { current, path, route: routeUrl, uiRoute },
445
463
  routeUi: {
446
464
  link: ui,
447
465
  current: (options = {}) => ui(current(options), options),
448
466
  fragment: ui,
449
467
  form
450
- }
468
+ },
469
+ element: createElementResolver(ctx, dependencyAliases)
451
470
  };
452
471
  }
453
472
  function createServiceRouteUrlBuilder(routes, extraContext, currentServiceId) {
@@ -570,7 +589,7 @@ async function withSpan(obs, name, attributes, handler) {
570
589
  throw error;
571
590
  }
572
591
  }
573
- async function handleRouteRequest(registryRoutes, route, method, event, obs, routerOptions = {}) {
592
+ async function handleRouteRequest(registryRoutes, dependencyAliases, route, method, event, obs, routerOptions = {}) {
574
593
  const methodRoute = route.methodRoutes?.[method];
575
594
  const handler = methodRoute?.handler ?? route.handlers[method];
576
595
  const schemas = methodRoute?.schemas ?? route.schemas;
@@ -756,33 +775,31 @@ async function handleRouteRequest(registryRoutes, route, method, event, obs, rou
756
775
  if (representation.kind === "json") {
757
776
  return jsonResponse(data, handlerStatus);
758
777
  }
759
- // HTML - resolve theme from request context (hostname -> app config), Accept header as fallback
760
- const themeId = event.__bpThemeId
761
- ?? representation.theme;
762
- if (!themeId) {
763
- logNegotiationFailure(obs, route, method, "theme_not_resolved", {
778
+ const renderer = rendererFromEvent(event);
779
+ if (!renderer) {
780
+ logNegotiationFailure(obs, route, method, "renderer_not_resolved", {
764
781
  "http.request.accept": acceptHeader ?? "",
765
782
  "bp.representation.kind": representation.kind
766
783
  });
767
- return jsonResponse({ error: "Theme could not be resolved from app config or request" }, 406);
784
+ return jsonResponse({ error: "Renderer could not be resolved from the app shell" }, 406);
768
785
  }
769
786
  // Determine the renderer kind requested
770
787
  const fragmentKey = url.searchParams.get("_f") ?? fragmentFromAcceptHeader(acceptHeader);
771
788
  const componentId = url.searchParams.get("_c");
772
789
  const requestedKind = fragmentKey ? "fragment" : componentId ? "component" : "page";
773
790
  const requestedKey = fragmentKey ?? componentId ?? undefined;
774
- const renderContext = createViewRenderContext(route, ctx, themeId, representation.mode ?? "page", requestedKind, requestedKey, handlerStatus);
791
+ const renderContext = createViewRenderContext(route, ctx, dependencyAliases, renderer, representation.mode ?? "page", requestedKind, requestedKey, handlerStatus);
775
792
  // Status-specific renderer lookup (any non-undefined status code)
776
793
  if (handlerStatus !== 200) {
777
- const statusRenderer = resolveStatusRenderer(route, themeId, handlerStatus, requestedKind, requestedKey, method);
794
+ const statusRenderer = resolveStatusRenderer(route, renderer, handlerStatus, requestedKind, requestedKey, method);
778
795
  if (statusRenderer) {
779
796
  const html = await withSpan(obs, "bp.view.render", {
780
797
  "bp.route.view_id": route.viewId,
781
- "bp.view.theme_id": themeId,
798
+ "bp.view.renderer": renderer,
782
799
  "bp.view.kind": requestedKind,
783
800
  "bp.view.status": handlerStatus
784
801
  }, () => statusRenderer.render(data, renderContext));
785
- return htmlResponse(rewriteServiceRouteTokens(toHtmlString(html), ctx.routeUrl, obs), handlerStatus, htmlContentType(themeId, "status", route.chrome));
802
+ return htmlResponse(rewriteServiceRouteTokens(toHtmlString(html), ctx.routeUrl, obs), handlerStatus, htmlContentType("status", route.chrome));
786
803
  }
787
804
  // No specific renderer found.
788
805
  if (!shouldFallThroughToDefaultRenderer(handlerStatus)) {
@@ -793,67 +810,67 @@ async function handleRouteRequest(registryRoutes, route, method, event, obs, rou
793
810
  }
794
811
  // Fragment request via `_f` query param or Accept header
795
812
  if (fragmentKey) {
796
- const resolved = resolveRenderer(route, themeId, "fragment", method, undefined, fragmentKey);
813
+ const resolved = resolveRenderer(route, renderer, "fragment", method, undefined, fragmentKey);
797
814
  if (!resolved) {
798
815
  logNegotiationFailure(obs, route, method, "fragment_renderer_not_found", {
799
816
  "http.request.accept": acceptHeader ?? "",
800
- "bp.view.theme_id": themeId,
817
+ "bp.view.renderer": renderer,
801
818
  "bp.view.kind": "fragment",
802
819
  "bp.view.key": fragmentKey
803
820
  });
804
821
  return jsonResponse({
805
- error: `No fragment renderer found for fragment="${fragmentKey}" in theme "${themeId}"`
822
+ error: `No fragment renderer found for fragment="${fragmentKey}" in renderer "${renderer}"`
806
823
  }, 406);
807
824
  }
808
825
  const html = await withSpan(obs, "bp.view.render", {
809
826
  "bp.route.view_id": route.viewId,
810
- "bp.view.theme_id": themeId,
827
+ "bp.view.renderer": renderer,
811
828
  "bp.view.kind": "fragment",
812
829
  "bp.view.key": fragmentKey
813
830
  }, () => resolved.renderer.render(data, renderContext));
814
- return htmlResponse(rewriteServiceRouteTokens(toHtmlString(html), ctx.routeUrl, obs), handlerStatus, htmlContentType(themeId, "fragment", route.chrome));
831
+ return htmlResponse(rewriteServiceRouteTokens(toHtmlString(html), ctx.routeUrl, obs), handlerStatus, htmlContentType("fragment", route.chrome));
815
832
  }
816
833
  // Component request via `_c` query param
817
834
  if (componentId) {
818
- const resolved = resolveRenderer(route, themeId, "component", method, componentId);
835
+ const resolved = resolveRenderer(route, renderer, "component", method, componentId);
819
836
  if (!resolved) {
820
837
  logNegotiationFailure(obs, route, method, "component_renderer_not_found", {
821
838
  "http.request.accept": acceptHeader ?? "",
822
- "bp.view.theme_id": themeId,
839
+ "bp.view.renderer": renderer,
823
840
  "bp.view.kind": "component",
824
841
  "bp.view.key": componentId
825
842
  });
826
843
  return jsonResponse({
827
- error: `No component renderer found for _c="${componentId}" in theme "${themeId}"`
844
+ error: `No component renderer found for _c="${componentId}" in renderer "${renderer}"`
828
845
  }, 406);
829
846
  }
830
847
  const html = await withSpan(obs, "bp.view.render", {
831
848
  "bp.route.view_id": route.viewId,
832
- "bp.view.theme_id": themeId,
849
+ "bp.view.renderer": renderer,
833
850
  "bp.view.kind": "component",
834
851
  "bp.view.key": componentId
835
852
  }, () => resolved.renderer.render(data, renderContext));
836
- return htmlResponse(rewriteServiceRouteTokens(toHtmlString(html), ctx.routeUrl, obs), handlerStatus, htmlContentType(themeId, "fragment", route.chrome));
853
+ return htmlResponse(rewriteServiceRouteTokens(toHtmlString(html), ctx.routeUrl, obs), handlerStatus, htmlContentType("fragment", route.chrome));
837
854
  }
838
855
  // Page request - only page renderers allowed
839
- const resolved = resolveRenderer(route, themeId, "page", method);
856
+ const resolved = resolveRenderer(route, renderer, "page", method);
840
857
  if (!resolved) {
841
858
  logNegotiationFailure(obs, route, method, "page_renderer_not_found", {
842
859
  "http.request.accept": acceptHeader ?? "",
843
- "bp.view.theme_id": themeId,
860
+ "bp.view.renderer": renderer,
844
861
  "bp.view.kind": "page"
845
862
  });
846
863
  return jsonResponse({
847
- error: `No page renderer found for theme "${themeId}"`
864
+ error: `No page renderer found for renderer "${renderer}"`
848
865
  }, 406);
849
866
  }
850
867
  const html = await withSpan(obs, "bp.view.render", {
851
868
  "bp.route.view_id": route.viewId,
852
- "bp.view.theme_id": themeId,
869
+ "bp.view.renderer": renderer,
853
870
  "bp.view.kind": "page"
854
871
  }, () => resolved.renderer.render(data, renderContext));
855
872
  const mode = representation.mode ?? "page";
856
- return htmlResponse(rewriteServiceRouteTokens(toHtmlString(html), ctx.routeUrl, obs), handlerStatus, htmlContentType(themeId, mode, route.chrome));
873
+ return htmlResponse(rewriteServiceRouteTokens(toHtmlString(html), ctx.routeUrl, obs), handlerStatus, htmlContentType(mode, route.chrome));
857
874
  }
858
875
  /**
859
876
  * Handle representations that stream, returning null for buffered ones so the
@@ -870,16 +887,15 @@ async function handleStreamRepresentation(route, handler, ctx, event, url, metho
870
887
  // Fragment/component selectors render over the buffered data set.
871
888
  if (url.searchParams.get("_f") || url.searchParams.get("_c"))
872
889
  return null;
873
- const themeId = event.__bpThemeId
874
- ?? representation.theme;
875
- if (!themeId)
890
+ const renderer = rendererFromEvent(event);
891
+ if (!renderer)
876
892
  return null;
877
- const streamSet = route.themeRenderers[themeId]?.stream;
893
+ const streamSet = route.renderers[renderer]?.stream;
878
894
  if (!streamSet)
879
895
  return null;
880
896
  // Full-page request with a page renderer available -> buffered render of the
881
897
  // complete data set (crawlers, no-SSE clients). Fragment swaps stream.
882
- if (representation.mode === "page" && resolveRenderer(route, themeId, "page", method)) {
898
+ if (representation.mode === "page" && resolveRenderer(route, renderer, "page", method)) {
883
899
  return null;
884
900
  }
885
901
  const shellCtx = {
@@ -889,10 +905,10 @@ async function handleStreamRepresentation(route, handler, ctx, event, url, metho
889
905
  };
890
906
  const html = await withSpan(obs, "bp.view.render", {
891
907
  "bp.route.view_id": route.viewId,
892
- "bp.view.theme_id": themeId,
908
+ "bp.view.renderer": renderer,
893
909
  "bp.view.kind": "stream-shell"
894
910
  }, () => streamSet.renderShell(shellCtx));
895
- return htmlResponse(rewriteServiceRouteTokens(toHtmlString(html), ctx.routeUrl, obs), 200, htmlContentType(themeId, "fragment", route.chrome));
911
+ return htmlResponse(rewriteServiceRouteTokens(toHtmlString(html), ctx.routeUrl, obs), 200, htmlContentType("fragment", route.chrome));
896
912
  }
897
913
  /**
898
914
  * SSE delivery of the frame stream at `{path}/__sse`. With a theme context and
@@ -936,9 +952,8 @@ async function handleStreamSse(registryRoutes, route, handler, event, obs, route
936
952
  file: fileResponseHelper,
937
953
  ...(obs ? { obs } : {})
938
954
  };
939
- const themeId = event.__bpThemeId
940
- ?? rawQuery._theme;
941
- const streamSet = themeId ? route.themeRenderers[themeId]?.stream : undefined;
955
+ const renderer = rendererFromEvent(event);
956
+ const streamSet = renderer ? route.renderers[renderer]?.stream : undefined;
942
957
  const stream = createEventStream(event);
943
958
  (async () => {
944
959
  try {
@@ -1209,7 +1224,7 @@ function formatRequiredPermissions(requiredPermissions = []) {
1209
1224
  .join("; ");
1210
1225
  }
1211
1226
  function renderAuthError(route, event, status, message, requiredPermissions = []) {
1212
- const themeId = event.__bpThemeId;
1227
+ const renderer = rendererFromEvent(event);
1213
1228
  const acceptHeader = acceptHeaderFromEvent(event);
1214
1229
  const representation = resolveRequestedRepresentation(acceptHeader);
1215
1230
  const corsHeaders = corsHeadersFromEvent(event);
@@ -1221,14 +1236,14 @@ function renderAuthError(route, event, status, message, requiredPermissions = []
1221
1236
  // app.auth config and redirects on seeing this 401. Services just report status.
1222
1237
  // Prefer a route/theme status view so the body swaps cleanly into the htmx
1223
1238
  // target as a fragment rather than replacing the shell.
1224
- if (themeId && (representation.kind === "html")) {
1225
- const statusRenderer = resolveStatusRenderer(route, themeId, status, "page", undefined, "GET");
1239
+ if (renderer && (representation.kind === "html")) {
1240
+ const statusRenderer = resolveStatusRenderer(route, renderer, status, "page", undefined, "GET");
1226
1241
  if (statusRenderer) {
1227
1242
  try {
1228
1243
  const html = statusRenderer.render({ error: message, status, requiredPermissions });
1229
1244
  return new Response(toHtmlString(html), {
1230
1245
  status,
1231
- headers: { ...corsHeaders, "content-type": htmlContentType(themeId, "status", route.chrome) }
1246
+ headers: { ...corsHeaders, "content-type": htmlContentType("status", route.chrome) }
1232
1247
  });
1233
1248
  }
1234
1249
  catch {
@@ -1249,7 +1264,7 @@ function renderAuthError(route, event, status, message, requiredPermissions = []
1249
1264
  `;
1250
1265
  return new Response(html, {
1251
1266
  status,
1252
- headers: { ...corsHeaders, "content-type": themeId ? htmlContentType(themeId, "status", route.chrome) : "text/html; charset=utf-8" }
1267
+ headers: { ...corsHeaders, "content-type": renderer ? htmlContentType("status", route.chrome) : "text/html; charset=utf-8" }
1253
1268
  });
1254
1269
  }
1255
1270
  return jsonResponse({ error: message, status, requiredPermissions }, status, corsHeaders);
@@ -1261,7 +1276,7 @@ function readTenantAppFromEvent(event) {
1261
1276
  return { tenantId: ctx.__bpTenantId, appId: ctx.__bpAppId };
1262
1277
  }
1263
1278
  function renderUpgradeRequired(route, event, validation) {
1264
- const themeId = event.__bpThemeId;
1279
+ const renderer = rendererFromEvent(event);
1265
1280
  const acceptHeader = acceptHeaderFromEvent(event);
1266
1281
  const representation = resolveRequestedRepresentation(acceptHeader);
1267
1282
  const status = 426;
@@ -1270,8 +1285,8 @@ function renderUpgradeRequired(route, event, validation) {
1270
1285
  if (validation.retryAfterSeconds) {
1271
1286
  extraHeaders["retry-after"] = String(validation.retryAfterSeconds);
1272
1287
  }
1273
- if (themeId && representation.kind === "html") {
1274
- const statusRenderer = resolveStatusRenderer(route, themeId, status, "page", undefined, "GET");
1288
+ if (renderer && representation.kind === "html") {
1289
+ const statusRenderer = resolveStatusRenderer(route, renderer, status, "page", undefined, "GET");
1275
1290
  if (statusRenderer) {
1276
1291
  try {
1277
1292
  const html = statusRenderer.render({
@@ -1279,7 +1294,7 @@ function renderUpgradeRequired(route, event, validation) {
1279
1294
  reason: validation.reason,
1280
1295
  upgradeUrl: validation.upgradeUrl
1281
1296
  });
1282
- return htmlResponse(toHtmlString(html), status, htmlContentType(themeId, "status", route.chrome));
1297
+ return htmlResponse(toHtmlString(html), status, htmlContentType("status", route.chrome));
1283
1298
  }
1284
1299
  catch {
1285
1300
  // fall through to JSON