@betterportal/framework 10.1.42 → 10.1.44

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 (84) hide show
  1. package/lib/adapters/h3.d.ts.map +1 -1
  2. package/lib/adapters/h3.js +165 -112
  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/m2m.d.ts +6 -0
  25. package/lib/contracts/m2m.d.ts.map +1 -1
  26. package/lib/contracts/m2m.js +5 -1
  27. package/lib/contracts/m2m.js.map +1 -1
  28. package/lib/contracts/manifest.d.ts +58 -8
  29. package/lib/contracts/manifest.d.ts.map +1 -1
  30. package/lib/contracts/manifest.js +16 -3
  31. package/lib/contracts/manifest.js.map +1 -1
  32. package/lib/contracts/platformConfig.d.ts +129 -4
  33. package/lib/contracts/platformConfig.d.ts.map +1 -1
  34. package/lib/contracts/platformConfig.js +30 -2
  35. package/lib/contracts/platformConfig.js.map +1 -1
  36. package/lib/contracts/registry.d.ts +59 -19
  37. package/lib/contracts/registry.d.ts.map +1 -1
  38. package/lib/contracts/route.d.ts +14 -5
  39. package/lib/contracts/route.d.ts.map +1 -1
  40. package/lib/contracts/route.js +2 -0
  41. package/lib/contracts/route.js.map +1 -1
  42. package/lib/contracts/streaming.d.ts +2 -2
  43. package/lib/contracts/view.d.ts +6 -4
  44. package/lib/contracts/view.d.ts.map +1 -1
  45. package/lib/contracts/view.js +2 -2
  46. package/lib/contracts/view.js.map +1 -1
  47. package/lib/index.d.ts +1 -0
  48. package/lib/index.d.ts.map +1 -1
  49. package/lib/index.js +1 -0
  50. package/lib/index.js.map +1 -1
  51. package/lib/runtime/auth/serviceToken.d.ts +3 -1
  52. package/lib/runtime/auth/serviceToken.d.ts.map +1 -1
  53. package/lib/runtime/auth/serviceToken.js +3 -0
  54. package/lib/runtime/auth/serviceToken.js.map +1 -1
  55. package/lib/runtime/configProvider.d.ts +5 -0
  56. package/lib/runtime/configProvider.d.ts.map +1 -1
  57. package/lib/runtime/configProvider.js +30 -1
  58. package/lib/runtime/configProvider.js.map +1 -1
  59. package/lib/runtime/configStore.d.ts +1 -1
  60. package/lib/runtime/configStore.d.ts.map +1 -1
  61. package/lib/runtime/configStore.js +53 -21
  62. package/lib/runtime/configStore.js.map +1 -1
  63. package/lib/runtime/element.d.ts +13 -0
  64. package/lib/runtime/element.d.ts.map +1 -0
  65. package/lib/runtime/element.js +39 -0
  66. package/lib/runtime/element.js.map +1 -0
  67. package/lib/runtime/llms.js +1 -1
  68. package/lib/runtime/media.d.ts +0 -1
  69. package/lib/runtime/media.d.ts.map +1 -1
  70. package/lib/runtime/media.js +0 -1
  71. package/lib/runtime/media.js.map +1 -1
  72. package/lib/runtime/registry.d.ts +8 -7
  73. package/lib/runtime/registry.d.ts.map +1 -1
  74. package/lib/runtime/registry.js +55 -34
  75. package/lib/runtime/registry.js.map +1 -1
  76. package/lib/runtime/statusViews.d.ts +3 -3
  77. package/lib/runtime/statusViews.d.ts.map +1 -1
  78. package/lib/runtime/statusViews.js +5 -5
  79. package/lib/runtime/statusViews.js.map +1 -1
  80. package/lib/runtime/view.d.ts +1 -1
  81. package/lib/runtime/view.d.ts.map +1 -1
  82. package/lib/runtime/view.js +7 -13
  83. package/lib/runtime/view.js.map +1 -1
  84. 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;AACrE,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;AA+rCD;;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;
@@ -673,6 +692,7 @@ async function handleRouteRequest(registryRoutes, route, method, event, obs, rou
673
692
  rawEvent: event,
674
693
  user: authResult.user,
675
694
  serviceCaller: authResult.serviceCaller,
695
+ callerMode: authResult.callerMode,
676
696
  ...extraContext,
677
697
  bpHeaders,
678
698
  responseHeaders: event.res.headers,
@@ -726,7 +746,7 @@ async function handleRouteRequest(registryRoutes, route, method, event, obs, rou
726
746
  description: route.description,
727
747
  path: route.path,
728
748
  methods: [...route.methods],
729
- auth: route.auth,
749
+ auth: { ...route.auth, callers: [...(route.auth.callers ?? ["user"])] },
730
750
  cacheHints: route.cacheHints
731
751
  }, 200, {
732
752
  "content-type": "application/vnd.betterportal.metadata+json; charset=utf-8"
@@ -755,33 +775,31 @@ async function handleRouteRequest(registryRoutes, route, method, event, obs, rou
755
775
  if (representation.kind === "json") {
756
776
  return jsonResponse(data, handlerStatus);
757
777
  }
758
- // HTML - resolve theme from request context (hostname -> app config), Accept header as fallback
759
- const themeId = event.__bpThemeId
760
- ?? representation.theme;
761
- if (!themeId) {
762
- logNegotiationFailure(obs, route, method, "theme_not_resolved", {
778
+ const renderer = rendererFromEvent(event);
779
+ if (!renderer) {
780
+ logNegotiationFailure(obs, route, method, "renderer_not_resolved", {
763
781
  "http.request.accept": acceptHeader ?? "",
764
782
  "bp.representation.kind": representation.kind
765
783
  });
766
- 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);
767
785
  }
768
786
  // Determine the renderer kind requested
769
787
  const fragmentKey = url.searchParams.get("_f") ?? fragmentFromAcceptHeader(acceptHeader);
770
788
  const componentId = url.searchParams.get("_c");
771
789
  const requestedKind = fragmentKey ? "fragment" : componentId ? "component" : "page";
772
790
  const requestedKey = fragmentKey ?? componentId ?? undefined;
773
- 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);
774
792
  // Status-specific renderer lookup (any non-undefined status code)
775
793
  if (handlerStatus !== 200) {
776
- const statusRenderer = resolveStatusRenderer(route, themeId, handlerStatus, requestedKind, requestedKey, method);
794
+ const statusRenderer = resolveStatusRenderer(route, renderer, handlerStatus, requestedKind, requestedKey, method);
777
795
  if (statusRenderer) {
778
796
  const html = await withSpan(obs, "bp.view.render", {
779
797
  "bp.route.view_id": route.viewId,
780
- "bp.view.theme_id": themeId,
798
+ "bp.view.renderer": renderer,
781
799
  "bp.view.kind": requestedKind,
782
800
  "bp.view.status": handlerStatus
783
801
  }, () => statusRenderer.render(data, renderContext));
784
- 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));
785
803
  }
786
804
  // No specific renderer found.
787
805
  if (!shouldFallThroughToDefaultRenderer(handlerStatus)) {
@@ -792,67 +810,67 @@ async function handleRouteRequest(registryRoutes, route, method, event, obs, rou
792
810
  }
793
811
  // Fragment request via `_f` query param or Accept header
794
812
  if (fragmentKey) {
795
- const resolved = resolveRenderer(route, themeId, "fragment", method, undefined, fragmentKey);
813
+ const resolved = resolveRenderer(route, renderer, "fragment", method, undefined, fragmentKey);
796
814
  if (!resolved) {
797
815
  logNegotiationFailure(obs, route, method, "fragment_renderer_not_found", {
798
816
  "http.request.accept": acceptHeader ?? "",
799
- "bp.view.theme_id": themeId,
817
+ "bp.view.renderer": renderer,
800
818
  "bp.view.kind": "fragment",
801
819
  "bp.view.key": fragmentKey
802
820
  });
803
821
  return jsonResponse({
804
- error: `No fragment renderer found for fragment="${fragmentKey}" in theme "${themeId}"`
822
+ error: `No fragment renderer found for fragment="${fragmentKey}" in renderer "${renderer}"`
805
823
  }, 406);
806
824
  }
807
825
  const html = await withSpan(obs, "bp.view.render", {
808
826
  "bp.route.view_id": route.viewId,
809
- "bp.view.theme_id": themeId,
827
+ "bp.view.renderer": renderer,
810
828
  "bp.view.kind": "fragment",
811
829
  "bp.view.key": fragmentKey
812
830
  }, () => resolved.renderer.render(data, renderContext));
813
- 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));
814
832
  }
815
833
  // Component request via `_c` query param
816
834
  if (componentId) {
817
- const resolved = resolveRenderer(route, themeId, "component", method, componentId);
835
+ const resolved = resolveRenderer(route, renderer, "component", method, componentId);
818
836
  if (!resolved) {
819
837
  logNegotiationFailure(obs, route, method, "component_renderer_not_found", {
820
838
  "http.request.accept": acceptHeader ?? "",
821
- "bp.view.theme_id": themeId,
839
+ "bp.view.renderer": renderer,
822
840
  "bp.view.kind": "component",
823
841
  "bp.view.key": componentId
824
842
  });
825
843
  return jsonResponse({
826
- error: `No component renderer found for _c="${componentId}" in theme "${themeId}"`
844
+ error: `No component renderer found for _c="${componentId}" in renderer "${renderer}"`
827
845
  }, 406);
828
846
  }
829
847
  const html = await withSpan(obs, "bp.view.render", {
830
848
  "bp.route.view_id": route.viewId,
831
- "bp.view.theme_id": themeId,
849
+ "bp.view.renderer": renderer,
832
850
  "bp.view.kind": "component",
833
851
  "bp.view.key": componentId
834
852
  }, () => resolved.renderer.render(data, renderContext));
835
- 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));
836
854
  }
837
855
  // Page request - only page renderers allowed
838
- const resolved = resolveRenderer(route, themeId, "page", method);
856
+ const resolved = resolveRenderer(route, renderer, "page", method);
839
857
  if (!resolved) {
840
858
  logNegotiationFailure(obs, route, method, "page_renderer_not_found", {
841
859
  "http.request.accept": acceptHeader ?? "",
842
- "bp.view.theme_id": themeId,
860
+ "bp.view.renderer": renderer,
843
861
  "bp.view.kind": "page"
844
862
  });
845
863
  return jsonResponse({
846
- error: `No page renderer found for theme "${themeId}"`
864
+ error: `No page renderer found for renderer "${renderer}"`
847
865
  }, 406);
848
866
  }
849
867
  const html = await withSpan(obs, "bp.view.render", {
850
868
  "bp.route.view_id": route.viewId,
851
- "bp.view.theme_id": themeId,
869
+ "bp.view.renderer": renderer,
852
870
  "bp.view.kind": "page"
853
871
  }, () => resolved.renderer.render(data, renderContext));
854
872
  const mode = representation.mode ?? "page";
855
- 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));
856
874
  }
857
875
  /**
858
876
  * Handle representations that stream, returning null for buffered ones so the
@@ -869,16 +887,15 @@ async function handleStreamRepresentation(route, handler, ctx, event, url, metho
869
887
  // Fragment/component selectors render over the buffered data set.
870
888
  if (url.searchParams.get("_f") || url.searchParams.get("_c"))
871
889
  return null;
872
- const themeId = event.__bpThemeId
873
- ?? representation.theme;
874
- if (!themeId)
890
+ const renderer = rendererFromEvent(event);
891
+ if (!renderer)
875
892
  return null;
876
- const streamSet = route.themeRenderers[themeId]?.stream;
893
+ const streamSet = route.renderers[renderer]?.stream;
877
894
  if (!streamSet)
878
895
  return null;
879
896
  // Full-page request with a page renderer available -> buffered render of the
880
897
  // complete data set (crawlers, no-SSE clients). Fragment swaps stream.
881
- if (representation.mode === "page" && resolveRenderer(route, themeId, "page", method)) {
898
+ if (representation.mode === "page" && resolveRenderer(route, renderer, "page", method)) {
882
899
  return null;
883
900
  }
884
901
  const shellCtx = {
@@ -888,10 +905,10 @@ async function handleStreamRepresentation(route, handler, ctx, event, url, metho
888
905
  };
889
906
  const html = await withSpan(obs, "bp.view.render", {
890
907
  "bp.route.view_id": route.viewId,
891
- "bp.view.theme_id": themeId,
908
+ "bp.view.renderer": renderer,
892
909
  "bp.view.kind": "stream-shell"
893
910
  }, () => streamSet.renderShell(shellCtx));
894
- 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));
895
912
  }
896
913
  /**
897
914
  * SSE delivery of the frame stream at `{path}/__sse`. With a theme context and
@@ -926,6 +943,7 @@ async function handleStreamSse(registryRoutes, route, handler, event, obs, route
926
943
  rawEvent: event,
927
944
  user: authResult.user,
928
945
  serviceCaller: authResult.serviceCaller,
946
+ callerMode: authResult.callerMode,
929
947
  ...extraContext,
930
948
  serviceId: routerOptions.serviceId,
931
949
  routeUrl: createServiceRouteUrlBuilder(registryRoutes, extraContext, routerOptions.serviceId),
@@ -934,9 +952,8 @@ async function handleStreamSse(registryRoutes, route, handler, event, obs, route
934
952
  file: fileResponseHelper,
935
953
  ...(obs ? { obs } : {})
936
954
  };
937
- const themeId = event.__bpThemeId
938
- ?? rawQuery._theme;
939
- const streamSet = themeId ? route.themeRenderers[themeId]?.stream : undefined;
955
+ const renderer = rendererFromEvent(event);
956
+ const streamSet = renderer ? route.renderers[renderer]?.stream : undefined;
940
957
  const stream = createEventStream(event);
941
958
  (async () => {
942
959
  try {
@@ -998,45 +1015,87 @@ async function loadAuthContext(event, route, routerOptions, obs) {
998
1015
  */
999
1016
  async function resolveRequestAuth(apiAuth, event, authContext, route, method, obs) {
1000
1017
  const required = apiAuth.required;
1001
- const authHeader = event.req.headers.get("authorization");
1002
- const bearer = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
1003
- // Step 1: no token
1004
- if (!bearer) {
1018
+ const primaryHeader = event.req.headers.get("authorization");
1019
+ const secondaryHeader = event.req.headers.get("x-bp-service-authorization");
1020
+ const primary = primaryHeader?.startsWith("Bearer ") ? primaryHeader.slice(7) : null;
1021
+ const secondary = secondaryHeader?.startsWith("Bearer ") ? secondaryHeader.slice(7) : null;
1022
+ const callerMode = secondaryHeader !== null
1023
+ ? "delegated"
1024
+ : primary && isServiceToken(primary)
1025
+ ? "service"
1026
+ : primary
1027
+ ? "user"
1028
+ : undefined;
1029
+ if (!primary || !callerMode) {
1005
1030
  if (required)
1006
1031
  return { status: 401, error: "Authentication required" };
1007
1032
  return { status: 200 };
1008
1033
  }
1034
+ const allowedCallers = apiAuth.callers ?? ["user"];
1035
+ if (!allowedCallers.includes(callerMode)) {
1036
+ if (required)
1037
+ return { status: 403, error: `${callerMode} callers are not allowed` };
1038
+ return { status: 200 };
1039
+ }
1009
1040
  if (!authContext) {
1010
1041
  if (required)
1011
1042
  return { status: 503, error: "Auth context unavailable" };
1012
1043
  return { status: 200 };
1013
1044
  }
1014
- if (isServiceToken(bearer)) {
1015
- if (!authContext.serviceVerifier) {
1016
- if (required)
1017
- return { status: 503, error: "Service auth context unavailable" };
1018
- return { status: 200 };
1019
- }
1045
+ if (callerMode === "user") {
1046
+ return resolveUserRequestAuth(primary, apiAuth, authContext, obs);
1047
+ }
1048
+ const sourceServiceId = event.req.headers.get("x-bp-service-id");
1049
+ const tenantId = event.req.headers.get("x-bp-tenant-id");
1050
+ const appId = event.req.headers.get("x-bp-app-id");
1051
+ if (!sourceServiceId || !tenantId || !appId) {
1052
+ return { status: 401, error: "Service, tenant, and app headers are required for S2S calls" };
1053
+ }
1054
+ if (tenantId !== authContext.tenantId || appId !== authContext.appId) {
1055
+ return { status: 401, error: "S2S headers do not match the resolved tenant/app" };
1056
+ }
1057
+ if (!authContext.serviceVerifier) {
1058
+ if (required)
1059
+ return { status: 503, error: "Service auth context unavailable" };
1060
+ return { status: 200 };
1061
+ }
1062
+ const verifyService = async (token, mode) => {
1020
1063
  try {
1021
- const claims = await authContext.serviceVerifier.verify(bearer, {
1064
+ return await authContext.serviceVerifier.verify(token, {
1022
1065
  tenantId: authContext.tenantId,
1023
1066
  appId: authContext.appId,
1024
1067
  viewId: route.viewId,
1025
1068
  method,
1069
+ mode,
1070
+ sourceServiceId,
1026
1071
  requiredPermissions: apiAuth.permissions.flatMap((requirement) => requirement.permissions)
1027
1072
  });
1028
- return { status: 200, serviceCaller: claims };
1029
1073
  }
1030
1074
  catch (err) {
1031
1075
  obs?.logger.warn("Service token verification failed: {msg}", { msg: err.message });
1032
- if (required) {
1033
- const status = err.status === 403 ? 403 : 401;
1034
- return { status, error: status === 403 ? "Service access denied" : "Invalid service token" };
1035
- }
1036
- return { status: 200 };
1076
+ const status = err.status === 403 ? 403 : 401;
1077
+ return { status, error: status === 403 ? "Service access denied" : "Invalid service token" };
1037
1078
  }
1079
+ };
1080
+ if (callerMode === "service") {
1081
+ const serviceCaller = await verifyService(primary, "service");
1082
+ if ("status" in serviceCaller)
1083
+ return required ? serviceCaller : { status: 200 };
1084
+ return { status: 200, serviceCaller, callerMode };
1038
1085
  }
1039
- // Step 2-4: verify JWT (signature + double-verify happens inside verifier)
1086
+ if (!secondary || isServiceToken(primary)) {
1087
+ return { status: 401, error: "Delegated calls require a BP user token and a service token" };
1088
+ }
1089
+ const userResult = await resolveUserRequestAuth(primary, apiAuth, authContext, obs);
1090
+ if (userResult.error || !userResult.user)
1091
+ return userResult.error ? userResult : { status: 401, error: "Valid BP user token required" };
1092
+ const serviceCaller = await verifyService(secondary, "delegated");
1093
+ if ("status" in serviceCaller)
1094
+ return required ? serviceCaller : { status: 200 };
1095
+ return { status: 200, user: userResult.user, serviceCaller, callerMode };
1096
+ }
1097
+ async function resolveUserRequestAuth(bearer, apiAuth, authContext, obs) {
1098
+ const required = apiAuth.required;
1040
1099
  const verifier = authContext.verifier;
1041
1100
  if (!verifier) {
1042
1101
  if (required)
@@ -1060,7 +1119,6 @@ async function resolveRequestAuth(apiAuth, event, authContext, route, method, ob
1060
1119
  return { status: 401, error: "Invalid token" };
1061
1120
  return { status: 200 };
1062
1121
  }
1063
- // Step 5: tenant binding
1064
1122
  if (claims.tenantId !== authContext.tenantId) {
1065
1123
  obs?.logger.warn("JWT tenantId mismatch: token={t1} request={t2}", {
1066
1124
  t1: claims.tenantId,
@@ -1070,7 +1128,6 @@ async function resolveRequestAuth(apiAuth, event, authContext, route, method, ob
1070
1128
  return { status: 401, error: "Token bound to a different tenant" };
1071
1129
  return { status: 200 };
1072
1130
  }
1073
- // Step 6: app binding
1074
1131
  if (claims.appId !== authContext.appId) {
1075
1132
  obs?.logger.warn("JWT appId mismatch: token={a1} request={a2}", {
1076
1133
  a1: claims.appId,
@@ -1080,7 +1137,6 @@ async function resolveRequestAuth(apiAuth, event, authContext, route, method, ob
1080
1137
  return { status: 401, error: "Token bound to a different app" };
1081
1138
  return { status: 200 };
1082
1139
  }
1083
- // Step 7: permission check against app.auth.roles
1084
1140
  if (apiAuth.permissions.length > 0) {
1085
1141
  const hasPlatformRootRole = claims.roles.includes(PLATFORM_ROOT_PERMISSION_ROLE_ID);
1086
1142
  if (hasPlatformRootRole) {
@@ -1089,7 +1145,7 @@ async function resolveRequestAuth(apiAuth, event, authContext, route, method, ob
1089
1145
  && claims.tenantId === authContext.platformRoot.tenantId
1090
1146
  && claims.appId === authContext.platformRoot.appId;
1091
1147
  if (rootMatches) {
1092
- return { status: 200, user: claims };
1148
+ return { status: 200, user: claims, callerMode: "user" };
1093
1149
  }
1094
1150
  obs?.logger.error("Reserved platform-root permission role used outside management app: tenant={tenantId} app={appId} rootTenant={rootTenantId} rootApp={rootAppId}", {
1095
1151
  tenantId: claims.tenantId,
@@ -1099,15 +1155,13 @@ async function resolveRequestAuth(apiAuth, event, authContext, route, method, ob
1099
1155
  });
1100
1156
  }
1101
1157
  const granted = expandRolesToPermissions(claims.roles, authContext.appAuthConfig);
1102
- // Grants reference tenant service-instance ids; route requirements are
1103
- // authored against pluginIds. Treat them as equal via the alias map.
1104
1158
  const aliases = authContext.serviceIdAliases ?? {};
1105
- const serviceIdsMatch = (grantServiceId, requiredServiceId) => grantServiceId === requiredServiceId ||
1106
- aliases[grantServiceId] === requiredServiceId ||
1107
- aliases[requiredServiceId] === grantServiceId;
1108
- const ok = apiAuth.permissions.every((requirement) => requirement.permissions.every((action) => granted.some((grant) => serviceIdsMatch(grant.serviceId, requirement.serviceId) &&
1109
- grant.viewId === requirement.viewId &&
1110
- grant.permissions.includes(action))));
1159
+ const serviceIdsMatch = (grantServiceId, requiredServiceId) => grantServiceId === requiredServiceId
1160
+ || aliases[grantServiceId] === requiredServiceId
1161
+ || aliases[requiredServiceId] === grantServiceId;
1162
+ const ok = apiAuth.permissions.every((requirement) => requirement.permissions.every((action) => granted.some((grant) => serviceIdsMatch(grant.serviceId, requirement.serviceId)
1163
+ && grant.viewId === requirement.viewId
1164
+ && grant.permissions.includes(action))));
1111
1165
  if (!ok) {
1112
1166
  if (required) {
1113
1167
  return {
@@ -1119,8 +1173,7 @@ async function resolveRequestAuth(apiAuth, event, authContext, route, method, ob
1119
1173
  return { status: 200 };
1120
1174
  }
1121
1175
  }
1122
- // Step 8: attach validated claims
1123
- return { status: 200, user: claims };
1176
+ return { status: 200, user: claims, callerMode: "user" };
1124
1177
  }
1125
1178
  function expandRolesToPermissions(roleIds, appAuthConfig) {
1126
1179
  if (!appAuthConfig)
@@ -1171,7 +1224,7 @@ function formatRequiredPermissions(requiredPermissions = []) {
1171
1224
  .join("; ");
1172
1225
  }
1173
1226
  function renderAuthError(route, event, status, message, requiredPermissions = []) {
1174
- const themeId = event.__bpThemeId;
1227
+ const renderer = rendererFromEvent(event);
1175
1228
  const acceptHeader = acceptHeaderFromEvent(event);
1176
1229
  const representation = resolveRequestedRepresentation(acceptHeader);
1177
1230
  const corsHeaders = corsHeadersFromEvent(event);
@@ -1183,14 +1236,14 @@ function renderAuthError(route, event, status, message, requiredPermissions = []
1183
1236
  // app.auth config and redirects on seeing this 401. Services just report status.
1184
1237
  // Prefer a route/theme status view so the body swaps cleanly into the htmx
1185
1238
  // target as a fragment rather than replacing the shell.
1186
- if (themeId && (representation.kind === "html")) {
1187
- 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");
1188
1241
  if (statusRenderer) {
1189
1242
  try {
1190
1243
  const html = statusRenderer.render({ error: message, status, requiredPermissions });
1191
1244
  return new Response(toHtmlString(html), {
1192
1245
  status,
1193
- headers: { ...corsHeaders, "content-type": htmlContentType(themeId, "status", route.chrome) }
1246
+ headers: { ...corsHeaders, "content-type": htmlContentType("status", route.chrome) }
1194
1247
  });
1195
1248
  }
1196
1249
  catch {
@@ -1211,7 +1264,7 @@ function renderAuthError(route, event, status, message, requiredPermissions = []
1211
1264
  `;
1212
1265
  return new Response(html, {
1213
1266
  status,
1214
- 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" }
1215
1268
  });
1216
1269
  }
1217
1270
  return jsonResponse({ error: message, status, requiredPermissions }, status, corsHeaders);
@@ -1223,7 +1276,7 @@ function readTenantAppFromEvent(event) {
1223
1276
  return { tenantId: ctx.__bpTenantId, appId: ctx.__bpAppId };
1224
1277
  }
1225
1278
  function renderUpgradeRequired(route, event, validation) {
1226
- const themeId = event.__bpThemeId;
1279
+ const renderer = rendererFromEvent(event);
1227
1280
  const acceptHeader = acceptHeaderFromEvent(event);
1228
1281
  const representation = resolveRequestedRepresentation(acceptHeader);
1229
1282
  const status = 426;
@@ -1232,8 +1285,8 @@ function renderUpgradeRequired(route, event, validation) {
1232
1285
  if (validation.retryAfterSeconds) {
1233
1286
  extraHeaders["retry-after"] = String(validation.retryAfterSeconds);
1234
1287
  }
1235
- if (themeId && representation.kind === "html") {
1236
- 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");
1237
1290
  if (statusRenderer) {
1238
1291
  try {
1239
1292
  const html = statusRenderer.render({
@@ -1241,7 +1294,7 @@ function renderUpgradeRequired(route, event, validation) {
1241
1294
  reason: validation.reason,
1242
1295
  upgradeUrl: validation.upgradeUrl
1243
1296
  });
1244
- return htmlResponse(toHtmlString(html), status, htmlContentType(themeId, "status", route.chrome));
1297
+ return htmlResponse(toHtmlString(html), status, htmlContentType("status", route.chrome));
1245
1298
  }
1246
1299
  catch {
1247
1300
  // fall through to JSON