@teamscala/service-boot 0.23.524 → 0.23.525

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 (81) hide show
  1. package/package.json +1 -9
  2. package/src/agreement-gate/authorized-emails.ts +0 -7
  3. package/src/agreement-gate/placeholder-strip.ts +0 -2
  4. package/src/agreement-gate/settings.ts +0 -15
  5. package/src/agreement-gate.ts +0 -34
  6. package/src/attach-proposal-accept.ts +1 -1
  7. package/src/attach-proposal-magic.ts +1 -1
  8. package/src/auth/sign-out.ts +0 -2
  9. package/src/auth-gate/public-paths.ts +0 -86
  10. package/src/auth-gate.ts +0 -19
  11. package/src/bind-page-handler.ts +0 -76
  12. package/src/build-client-assets/hydrate-bundle.ts +0 -126
  13. package/src/build-client-assets/manifest.ts +0 -18
  14. package/src/build-client-assets/source-candidates.ts +0 -34
  15. package/src/build-client-assets/sourcemaps-upload.ts +0 -42
  16. package/src/build-client-assets/styles-build.ts +0 -50
  17. package/src/capability-slug.ts +0 -10
  18. package/src/compose-page-title.ts +0 -9
  19. package/src/content-gate/access.ts +0 -26
  20. package/src/content-gate/catalog.ts +0 -29
  21. package/src/content-gate/config-types.ts +3 -84
  22. package/src/content-gate/gate.ts +0 -20
  23. package/src/content-page-floor.ts +0 -11
  24. package/src/error-page-logo.ts +2 -12
  25. package/src/expand-roster.ts +0 -9
  26. package/src/gpt-gate.ts +1 -1
  27. package/src/hono-error-handler.ts +0 -4
  28. package/src/install-logging.ts +0 -32
  29. package/src/load-page-manifest.ts +0 -6
  30. package/src/load-service-install-row.ts +41 -4
  31. package/src/module-not-found.ts +0 -11
  32. package/src/mount-orpc-router.ts +1 -88
  33. package/src/mount-server-packages.ts +0 -29
  34. package/src/mutable-app-ref.ts +0 -11
  35. package/src/orpc-contract-rows.json +289 -0
  36. package/src/orpc-openapi-document.ts +0 -6
  37. package/src/orpc-openapi-gate.ts +0 -10
  38. package/src/pg-boss-handler-registry.ts +0 -12
  39. package/src/pg-boss-mount.ts +0 -101
  40. package/src/proposal-gate.ts +1 -41
  41. package/src/quiz-gate.ts +1 -9
  42. package/src/register-page-server-client.ts +0 -53
  43. package/src/register-registry-page-route/ai-chat-prefetch.ts +0 -18
  44. package/src/register-registry-page-route/apply-page-branding.ts +0 -90
  45. package/src/register-registry-page-route/captured-loader-config.ts +0 -1
  46. package/src/register-registry-page-route/graph-cache.ts +0 -17
  47. package/src/register-registry-page-route/meta-title.ts +0 -24
  48. package/src/register-registry-page-route/page-data-shared.ts +0 -6
  49. package/src/register-registry-page-route/prefetch-org-id.ts +0 -17
  50. package/src/register-registry-page-route/proposal-approval-email-token.ts +0 -11
  51. package/src/register-registry-page-route/proposal-brand-stamping.ts +0 -20
  52. package/src/register-registry-page-route/public-shell-page-data.ts +0 -26
  53. package/src/register-registry-page-route/resolve-request-graph.ts +0 -57
  54. package/src/register-registry-page-route/run-content-gates.ts +2 -54
  55. package/src/register-registry-page-route/warn-uncovered-blocks.ts +0 -10
  56. package/src/register-registry-page-route.ts +0 -75
  57. package/src/register-static-assets.ts +0 -45
  58. package/src/resolve-brand-favicon.ts +0 -9
  59. package/src/resolve-service-port.ts +0 -11
  60. package/src/resolve-theme-mode.ts +0 -8
  61. package/src/resolvers/app-header.ts +0 -1
  62. package/src/resolvers/fetch-org-brand.ts +0 -1
  63. package/src/resolvers/portal-route-paths.ts +0 -17
  64. package/src/resource-not-found.ts +0 -24
  65. package/src/select-service-routes.ts +0 -14
  66. package/src/theme-fonts-link.ts +0 -5
  67. package/src/two-phase-full-app.ts +0 -76
  68. package/src/two-phase-minimal-app.ts +1 -105
  69. package/src/types.ts +0 -14
  70. package/src/ui-service.ts +1 -42
  71. package/src/content-gate/gate-entry-marker.test.ts +0 -111
  72. package/src/content-gate/token.segmentation.test.ts +0 -76
  73. package/src/content-gate/token.test.ts +0 -65
  74. package/src/content-gate/token.ts +0 -197
  75. package/src/is-react-element-type.test.ts +0 -45
  76. package/src/is-react-element-type.ts +0 -10
  77. package/src/page-server-orpc/hub-mcp-client.ts +0 -14
  78. package/src/pg-boss-mount.test.ts +0 -101
  79. package/src/resource-chat-inject.page-assistant.test.ts +0 -145
  80. package/src/resource-chat-inject.test.ts +0 -240
  81. package/src/resource-chat-inject.ts +0 -357
@@ -6,11 +6,6 @@
6
6
  import { getAppLogger } from "@teamscala/logger/app-loggers";
7
7
  import type { Data } from "@teamscala/puck-boundary/data";
8
8
 
9
- /**
10
- * The canonical identity from the generated boundary (the platform's one
11
- * declaration site for framework shapes) — never a hand-spelled structural
12
- * duplicate of it.
13
- */
14
9
  export type PuckData = Data;
15
10
 
16
11
  export const EMPTY_PUCK_DATA: PuckData = { root: { props: {} }, content: [] };
@@ -86,7 +81,6 @@ export function warnUnresolvedRegionSlot(slot: string): void {
86
81
  );
87
82
  }
88
83
 
89
- /** The resolved brand tokens every branch reads (the old file's shared prologue). */
90
84
  export function readBrandTokens(resolved: Record<string, unknown>): {
91
85
  brand: Record<string, unknown>;
92
86
  primaryColor: string;
@@ -3,23 +3,6 @@
3
3
  * @status handwritten
4
4
  */
5
5
 
6
- /**
7
- * Prefer segment 1; fall back to segment 0 only when segment 1 is not a
8
- * UUID (the single-UUID shape, which is live and supported — NOT
9
- * transitional). The fallback is deliberately kept
10
- * UNCHANGED-BEHAVIOUR rather than "fixed": a UUID in segment 0 is
11
- * genuinely ambiguous — of the 40 UUID-first page paths in builda_pages,
12
- * ZERO are websites and ZERO are organisations, so segment 0 can also be
13
- * an unrelated record id. Position alone cannot resolve that; only
14
- * validating the id against organisation_profile can, and that is a DB
15
- * read this hot prefetch path should not take without caching
16
- * (`hot-path-resolutions-are-cached`). Preferring segment 1 is strictly
17
- * better for the canonical shape and no worse for any other. Reading
18
- * segment 0 first captured the WEBSITE id and passed it as an
19
- * organisation id, which failed silently because both are well-formed
20
- * UUIDs: the prefetch simply warmed a session against the wrong tenant
21
- * (found 2026-07-21).
22
- */
23
6
 
24
7
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
25
8
 
@@ -2,17 +2,6 @@
2
2
  * @system service-boot
3
3
  * @status handwritten
4
4
  */
5
- /**
6
- * Resolve the `{{approvalEmail}}` placeholder in a proposal page's button props
7
- * (e.g. the Approve CTA) in place. Routing: the organisation's registered
8
- * company email FIRST (organisation_profile.contact_email, already resolved
9
- * into pageData.brand.contactEmail); the proposal creator's user email only as
10
- * a fallback when the org has none — so a cloned proposal can never carry a
11
- * stale hard-coded address. The creator lookup is null-safe and adds no user
12
- * exposure: it resolves only where the in-process ORPC client exposes `user`
13
- * (super-admin), degrading to the org email elsewhere. Called only for proposal
14
- * pages; other registry pages are untouched.
15
- */
16
5
 
17
6
  import { getServerClient } from "../page-server-orpc/server-client.ts";
18
7
 
@@ -3,20 +3,6 @@
3
3
  * @status handwritten
4
4
  */
5
5
 
6
- /**
7
- * Stash a proposal's connected partner logo/colour on `pageData` from the
8
- * `partnerOrg` loader source (`crm_organisations` loaded by the proposal's
9
- * `partner_id`, via the SAME proven ORPC loader path that loads the proposal
10
- * itself). resolve-page-brand (service-runtime) prefers `pageData.partnerLogo`
11
- * over any inline body-Hero prop and fans it into the shared `/header` so a
12
- * `source: "partner"` OrgLogo fills automatically — the partner logo lives on
13
- * the CRM record, never duplicated per proposal. No partner → partnerOrg is
14
- * null → nothing set → the partner logo block renders blank. Reading the
15
- * already-resolved loader value (not a second hand-rolled getServerClient call)
16
- * is what makes this reliable: the earlier direct `crm_organisations.findFirst`
17
- * with a Prisma-style object `select` resolved intermittently/never against the
18
- * generated ORPC, leaving the header partner blank (2026-06-12).
19
- */
20
6
  export function applyPartnerBrand(
21
7
  pageData: Record<string, unknown>,
22
8
  partnerOrg:
@@ -30,12 +16,6 @@ export function applyPartnerBrand(
30
16
  pageData.partnerBrandColor = partnerOrg.brand_primary_color;
31
17
  }
32
18
 
33
- /**
34
- * Auto-resolve the client's company logo from the `clientOrg` loader source
35
- * (contact_id → company_id → crm_organisations.logo_url). Injects into the
36
- * Hero block's clientLogo prop when it's empty — no manual logo URL needed
37
- * per proposal when the contact's company profile has a logo.
38
- */
39
19
  export function applyClientLogo(
40
20
  pageData: Record<string, unknown>,
41
21
  clientOrg:
@@ -88,12 +88,6 @@ export function publicShellPageData(
88
88
  const overrideHeading = pickRootProp<string>(regions, "headingFont");
89
89
  const overrideBody = pickRootProp<string>(regions, "bodyFont");
90
90
 
91
- // The page's own screenMode, threaded the same way Branch 1 threads it
92
- // (single-page-data.ts). Dropping it here is not neutral: build-page-tree
93
- // falls back to `fullScreen: renderMode === "public" || "gpts"`, and for a
94
- // shell page that resolves false — which wraps the body in a centred card
95
- // and puts the site header INSIDE that card instead of across the top of
96
- // the page. A page that declares "full-screen" was rendering as a form.
97
91
  const screenMode = pickRootProp<string>(regions, "screenMode");
98
92
 
99
93
  const pageName = (page.name as string) || null;
@@ -108,22 +102,11 @@ export function publicShellPageData(
108
102
  return {
109
103
  puckData: { root: { props: {} }, content: [] } as PuckData,
110
104
  regions,
111
- // Region-shell blocks (e.g. the header Navigation deriving the body's
112
- // document sections) read the resolved entity graph from
113
- // pageData.loaderData — the same surface Branch 1 exposes. Without it a
114
- // header/footer block serverLoader gets ctx.loaderData=undefined and
115
- // cannot see the body page, so a server-derived nav produced no items
116
- // and the menu was client-only (vanished on stale tabs / narrow views).
117
105
  loaderData: resolved,
118
106
  website: {
119
107
  slug: website.slug as string,
120
108
  name: website.name as string,
121
109
  organisation_id: website.organisation_id as string | null,
122
- // header_settings carries the DB-driven header config (e.g.
123
- // backdropBlur) the public shell applies — PublicTemplate reads
124
- // pageData.website.header_settings.backdropBlur. Dropping it here
125
- // silently kills the header blur even though the DB switch is set
126
- // (the 2026-06-17 regression). Pass it through verbatim.
127
110
  header_settings:
128
111
  (website.header_settings as Record<string, unknown> | null) ?? null,
129
112
  },
@@ -132,10 +115,6 @@ export function publicShellPageData(
132
115
  null,
133
116
  brand: {
134
117
  primaryColor: overrideColor ?? primaryColor,
135
- // Thread the org brand's dark surface (brand_background_color /
136
- // brand_dark_color) when the page sets no explicit override — so
137
- // dark-mode pages use the brand's real dark colour, not a
138
- // primary-derived tint. Uses the org brand's real dark colour.
139
118
  backgroundColor:
140
119
  overrideBg ?? (brand.backgroundColor as string | null) ?? null,
141
120
  lightSurface: (brand.lightSurface as string | null) ?? null,
@@ -159,11 +138,6 @@ export function publicShellPageData(
159
138
  pageMetaTitle: bodyMetaTitle,
160
139
  templateTitle: (meta.title as string) || undefined,
161
140
  websiteName: (website.name as string) || undefined,
162
- // The FRIENDLY name is what the public sees — the trading brand,
163
- // not the legal entity. Reading companyName here put "GMG APAC"
164
- // in the title of every launcha page, which the operator has
165
- // corrected repeatedly; the friendly name is "launcha". Falls
166
- // back to companyName only where no friendly name is set.
167
141
  organisationName:
168
142
  (brand.companyFriendlyName as string) ||
169
143
  (brand.companyName as string) ||
@@ -61,8 +61,6 @@ function cloneForCache(
61
61
  `[scala-install:${serviceSlug}] graph-cache structuredClone failed for ${graphKey} — falling back to JSON-clone (non-cloneable value in loader graph): ${err instanceof Error ? err.message : err}`,
62
62
  );
63
63
  try {
64
- // JSON-clone preserves the data graph, dropping the functions and
65
- // undefined a loader graph should not be carrying anyway.
66
64
  return JSON.parse(JSON.stringify(graph)) as unknown;
67
65
  } catch (err2) {
68
66
  log.warn(
@@ -78,19 +76,7 @@ export async function resolveRequestGraph(
78
76
  ): Promise<Record<string, unknown>> {
79
77
  const { pattern, routeSlug, routeConfig, loaderConfig, params, isStaticShell, serviceSlug, configFingerprint } = args;
80
78
 
81
- // A bare root ("/") that declares a resource_type is a UUID-driven
82
- // public site (proposals/forms/quiz) with no resource selected — it
83
- // must render the branded holding/not-found page ("no <resource> at
84
- // this link"), never fall through the shared public loader to the
85
- // marketing site. PrimaryResourceMissingError is the existing signal
86
- // the page binder maps to buildResourceNotFoundResponse. A "/" route
87
- // WITHOUT a resource_type (e.g. scala-public's marketing root) is
88
- // unaffected and still renders its content.
89
79
  if (pattern === "/" && routeConfig.resource_type) {
90
- // Status 200: the bare root is a valid "no <resource> selected"
91
- // landing, not an error — and kind:ui services must answer GET /
92
- // with 200 (verify-prod-persistent-slot). An invalid /:id keeps
93
- // the default 404 (genuinely not found).
94
80
  throw new PrimaryResourceMissingError(routeConfig.resource_type, 200);
95
81
  }
96
82
  const graphKey = `graph:${routeSlug}:${params.host}:${params.path}${configFingerprint ? `:${configFingerprint}` : ""}`;
@@ -111,13 +97,6 @@ export async function resolveRequestGraph(
111
97
  >;
112
98
  };
113
99
  let resolved: Record<string, unknown>;
114
- // SWR read over @teamscala/cache: fresh hit serves the cached graph, stale
115
- // hit serves it AND fires ONE background refresh (refreshInBackground,
116
- // single-flight via the cache's in-flight map), miss returns undefined.
117
- // The refresh callback re-resolves + stores a clone; a primary-missing
118
- // (dead-link) graph THROWS so the cache is not updated — a dead link must
119
- // stay per-request so a just-created record appears immediately
120
- // (refreshInBackground catches, leaves the stale entry, logs one line).
121
100
  const cachedGraph = readCachedGraph(
122
101
  graphCache,
123
102
  graphKey,
@@ -133,16 +112,6 @@ export async function resolveRequestGraph(
133
112
  `primary resource "${primaryType}" missing — skip cache update for ${graphKey}`,
134
113
  );
135
114
  }
136
- // SAME DEFENSIVE CLONE AS THE COLD PATH. This used to call
137
- // structuredClone bare, so a non-cloneable value in the loader
138
- // graph threw on EVERY background refresh: refreshInBackground
139
- // caught it, kept the stale entry, and logged one line — so the
140
- // cache could never update, expired at staleTtlMs, and the next
141
- // request re-resolved the whole graph in-request. Measured
142
- // 2026-09-03 on the live proposal page: "The object can not be
143
- // cloned" every ~13 minutes on the same graphKey, indefinitely.
144
- // The cold path was hardened for exactly this and the refresh
145
- // path was not — one defect, two clone sites, one protected.
146
115
  const refreshed = cloneForCache(freshGraph, graphKey, serviceSlug);
147
116
  if (refreshed === undefined) {
148
117
  throw new Error(
@@ -154,47 +123,21 @@ export async function resolveRequestGraph(
154
123
  ),
155
124
  );
156
125
  if (cachedGraph !== undefined) {
157
- // Clone-on-get: gateAgreementContent + block resolution MUTATE the
158
- // graph in place, so each request gets its own copy.
159
126
  resolved = structuredClone(cachedGraph) as Record<string, unknown>;
160
127
  } else {
161
128
  resolved = await resolveFromLoaderConfig(typedLoaderConfig, params);
162
129
  const primaryMissing =
163
130
  hasPrimarySource && primaryType && resolved[primaryType] == null;
164
131
  if (!primaryMissing) {
165
- // Defensive clone: structuredClone throws on non-cloneable loader
166
- // graphs (functions/Proxies/Prisma instances). A swallowed throw
167
- // here used to SILENTLY disable the graph cache for this key
168
- // (fail-open-silent — forbidden by protective-layers-fail-explicitly),
169
- // which left affected routes re-resolving the loader graph every
170
- // request (measured ~1.2s on the projects utility shell). JSON-clone
171
- // preserves the data graph (dropping functions/undefined a loader
172
- // graph shouldn't carry) so the cache stays effective; both fallbacks
173
- // are LOGGED so the offending non-cloneable source is observable +
174
- // fixable, never silently disabling the cache again.
175
132
  const cloned = cloneForCache(resolved, graphKey, serviceSlug);
176
133
  if (cloned !== undefined) {
177
134
  graphCache.set(graphKey, cloned);
178
135
  }
179
136
  }
180
137
  }
181
- // A route that declares resource_type is meaningless without that
182
- // entity: a dead link (wrong id, deleted row) must 404 with the
183
- // branded not-found page — never fall through to default content
184
- // (the "Vision Toolbar" leak, operator-observed 2026-06-05).
185
138
  if (hasPrimarySource && primaryType && resolved[primaryType] == null) {
186
139
  throw new PrimaryResourceMissingError(primaryType);
187
140
  }
188
- // A CONTENT route (no resource_type) whose loader resolves a `page`
189
- // body source must not synthesize an empty page for an unmatched path
190
- // and serve a cacheable 200 — the generalized "Vision Toolbar" leak
191
- // (operator-reported 5×: a deleted page, e.g. /roadmap, returned 200
192
- // with an empty body instead of not-found). No page row → dead link →
193
- // branded 404 floor. A page row that exists but has zero blocks → a
194
- // content gap → the floor's empty-page surface (200). Both carry the
195
- // "Take me home" action. The pure decision lives in content-page-floor.ts
196
- // (unit-tested without the Hono/DB/graph machinery). Region-slot
197
- // (utility/portal) shells without a `page` source are untouched.
198
141
  const contentFloorError = decideContentPageFloor({
199
142
  hasResourceType: !!primaryType,
200
143
  hasPageContentSource: !!(loaderConfig as { sources?: { key: string }[] }).sources?.some(
@@ -3,67 +3,15 @@
3
3
  * @status handwritten
4
4
  */
5
5
 
6
- import type { PageRouteConfig } from "../register-registry-page-route.ts";
7
6
  import { runContentGate } from "../content-gate/gate.ts";
8
- import type { ContentGateOutcome } from "../content-gate/gate.ts";
9
- import { CONTENT_GATES, withholdsContent } from "../content-gate/catalog.ts";
10
- import { injectResourceChatLauncher } from "../resource-chat-inject.ts";
7
+ import { CONTENT_GATES } from "../content-gate/catalog.ts";
11
8
 
12
9
  export async function runContentGatesForPage(
13
10
  resolved: Record<string, unknown>,
14
11
  gateCookie: string | null,
15
12
  gateNow: number,
16
- assistant: PageRouteConfig["assistant"],
17
- regionSlots: string[] | undefined,
18
13
  ): Promise<void> {
19
- // EVERY REGISTERED GATE, IN CATALOG ORDER (content-gate/catalog.ts).
20
- // This used to name proposalGateConfig, agreementGateConfig and
21
- // gptGateConfig by hand, so a fourth gated resource type meant editing
22
- // this file AND the second hand-written list in two-phase-boot.ts.
23
- //
24
- // SEQUENTIAL, NEVER PARALLEL. An authorized proposal viewer has their
25
- // gate block removed from the resolved graph, and that removal is what
26
- // makes the agreement gate find no resource and no-op on a page that
27
- // carries both. Awaiting these together would race that dependency.
28
- const gateOutcomes: ContentGateOutcome[] = [];
29
14
  for (const entry of CONTENT_GATES) {
30
- const outcome = await runContentGate(
31
- resolved,
32
- gateCookie,
33
- gateNow,
34
- entry.gate,
35
- );
36
- // A seed-only gate (open capture — the public GPT identity gate)
37
- // withholds nothing, so its verdict is not evidence about whether
38
- // this viewer may see the page and must not reach the assistant
39
- // decision below. See withholdsContent, which derives that from the
40
- // access config rather than restating it here.
41
- if (withholdsContent(entry)) gateOutcomes.push(outcome);
42
- }
43
-
44
- // THE ASSISTANT IS A ROW, AND THIS IS THE ONE PLACE IT IS APPLIED. It used to be a call inside each gate's onAuthorized plus an entry
45
- // in the hardcoded gate sequence above, so a third resource type meant editing this file. Now any page declaring `assistant` gets
46
- // one and no page without the key does.
47
- //
48
- // GATED PAGES ONLY WHEN AUTHORIZED. The launcher's runtime context is
49
- // the whole document serialised, so injecting past a closed gate
50
- // would hand withheld bytes to the chat. A page no gate governs is
51
- // public and gets the assistant unconditionally -- that is a
52
- // different state from an unauthorized viewer, which is why the gate
53
- // reports `applies` separately from `authorized`.
54
- if (assistant) {
55
- const governing = gateOutcomes.filter((outcome) => outcome.applies);
56
- const authorized = governing.find((outcome) => outcome.authorized);
57
- if (governing.length === 0 || authorized) {
58
- await injectResourceChatLauncher(resolved, authorized?.email ?? "", {
59
- resourceKey: assistant.resource_key,
60
- agentConfigSlug: assistant.agent_config_slug as never,
61
- launchLabel: assistant.launch_label,
62
- placeholder: assistant.placeholder,
63
- // From the loader row, so the injector never has to guess
64
- // which resolved keys hold rendered content.
65
- regionSlots,
66
- });
67
- }
15
+ await runContentGate(resolved, gateCookie, gateNow, entry.gate);
68
16
  }
69
17
  }
@@ -6,16 +6,6 @@
6
6
  import { getAppLogger } from "@teamscala/logger/app-loggers";
7
7
  import type { RendererComponentEntry } from "../types.ts";
8
8
 
9
- /**
10
- * A page that renders a block whose component is NOT registered — because
11
- * its package is missing from service_install.config.block_packages —
12
- * silently renders BLANK. That is the exact failure behind the 2026-06-04
13
- * scala-sign regression (sign-in-block dropped from block_packages → the
14
- * gate + signature fields vanished) which left only a benign boot WARN.
15
- * This turns that silent blank into a loud, GlitchTip-visible ERROR naming
16
- * the page + block, throttled to once per (service, block type) so a hot
17
- * path can never spam it.
18
- */
19
9
  const uncoveredBlocksLogged = new Set<string>();
20
10
 
21
11
  function collectBlockTypes(node: unknown, out: Set<string>): void {
@@ -122,22 +122,11 @@ export async function registerRegistryPageRoute(
122
122
  for (const routeRow of serviceRoutes) {
123
123
  const routeConfig = routeRow.config;
124
124
  const pattern = routeConfig.pattern;
125
- // loader_slug is a page_route runtime value and resource_loader is runtime
126
- // data: a row created after this build is exactly the live-edit case this
127
- // module serves, so the generated slug union (a build-time snapshot) can
128
- // only be asserted, never enforced, here. An unknown slug still fails
129
- // closed below (RegistryConfigMissingError skips the route).
130
125
  const loaderSlug = routeConfig.loader_slug as ResourceLoaderKey;
131
126
  const routeProviders = routeConfig.providers ?? baseProviders;
132
127
 
133
128
  let loaderConfig: LoaderConfig | null = null;
134
129
  try {
135
- // Boot-time warm + validate: a missing row skips the route (warn),
136
- // an invalid row fails closed. resource_loader is NOT on the
137
- // deploy-time bake list (constitution installer-and-runtime) — it
138
- // is re-read per request via readLiveLoaderConfig below — so this
139
- // boot read exists ONLY to catch a misconfigured row early, never
140
- // as the source of truth for live rendering.
141
130
  loaderConfig = await loadRegistryConfig(
142
131
  "resource_loader",
143
132
  loaderSlug,
@@ -156,22 +145,12 @@ export async function registerRegistryPageRoute(
156
145
  throw err;
157
146
  }
158
147
 
159
- // Gate for the static-shell graph cache (see module header).
160
148
  const isStaticShell =
161
149
  !routeConfig.resource_type &&
162
150
  !pattern.includes(":") &&
163
151
  !pattern.includes("*");
164
152
 
165
153
  const loadData = async (request: Request) => {
166
- // resource_loader is runtime data, not deploy-time baked. Re-read it
167
- // per request — loadRegistryConfig's SWR cache makes the fresh-window
168
- // hit a memory read, not a DB round trip. Throws into the branded
169
- // error path on a vanished or invalid row: the boot-time snapshot
170
- // must NEVER silently outlive the row it captured. This is the fix
171
- // for the loader-row-edits-look-like-they-don't-work defect (every
172
- // edit used to need a service restart; the sign-page-with-brand
173
- // `agreement` source added at 22:16 still hadn't reached
174
- // __PAGE_DATA__.loaderData at 22:29 — 13 minutes, and never).
175
154
  const liveLoaderConfig = await readLiveLoaderConfig(loaderSlug);
176
155
  const url = new URL(request.url);
177
156
  const params = extractUrlParams(pattern, url.pathname);
@@ -185,10 +164,6 @@ export async function registerRegistryPageRoute(
185
164
  params,
186
165
  isStaticShell,
187
166
  serviceSlug,
188
- // Folds the live config into the per-route graphKey so a row edit
189
- // invalidates the cached graph immediately (the graph is a
190
- // function of the config; without this the SWR graph cache
191
- // would still hold the old shape until its fresh window lapsed).
192
167
  configFingerprint: fingerprintLoaderConfig(liveLoaderConfig),
193
168
  });
194
169
  const { sessionUserId, sessionOrgId } = await stampSessionContext(
@@ -203,8 +178,6 @@ export async function registerRegistryPageRoute(
203
178
  resolved,
204
179
  gateCookie,
205
180
  Date.now(),
206
- routeConfig.assistant,
207
- regionSlots,
208
181
  );
209
182
  const meta = buildMetaFromContext(routeConfig.meta_context, resolved);
210
183
  await applySignInBrandOverride(resolved, request);
@@ -216,13 +189,6 @@ export async function registerRegistryPageRoute(
216
189
  ) {
217
190
  meta.title = pageMetaTitle.trim();
218
191
  }
219
- // Org-prefixed page titles (feature flag page-title-org-prefix, read
220
- // LIVE per request — feature flags are never cached). The title prefix
221
- // is the OWNING ORG'S name, never the internal website label
222
- // ("Public Pages: <subject>" on a client-facing document was the
223
- // defect; operator ruling 2026-08-17). Absent or unreadable flag ⇒
224
- // legacy <website.name>: <subject> derivation — the switch fails toward
225
- // the pre-flag behaviour, and flipping it back is one registry edit.
226
192
  let orgTitlePrefix = false;
227
193
  try {
228
194
  orgTitlePrefix = (
@@ -233,14 +199,8 @@ export async function registerRegistryPageRoute(
233
199
  )
234
200
  ).enabled;
235
201
  } catch {
236
- // Flag row absent — legacy derivation.
237
202
  }
238
203
  const themeParam = url.searchParams.get("theme");
239
- // ?theme is the EMBED override — only honor it when this page is
240
- // actually framed (a form/quiz in a GPT tool panel, sec-fetch-dest:
241
- // iframe). A DIRECT visit carrying ?theme= (e.g. copied from a GPT
242
- // embed URL) must NOT override the org's DB theme — what the visitor
243
- // sees is org-driven, never URL-driven.
244
204
  const isFramedEmbed =
245
205
  request.headers.get("sec-fetch-dest") === "iframe";
246
206
  const themeOverride =
@@ -257,12 +217,6 @@ export async function registerRegistryPageRoute(
257
217
  themeOverride,
258
218
  orgTitlePrefix,
259
219
  );
260
- // The matched route params (extractUrlParams + host/path) ride the
261
- // per-request pageData so the SSR resolve ctx can scope PUBLIC
262
- // block data per request — data_source loaders read ctx.pathParams
263
- // (query-data-source); without them a public table would return
264
- // every person's rows. Attached AFTER the graph cache (per-request,
265
- // never cached with the graph).
266
220
  (pageData as Record<string, unknown>).pathParams = params;
267
221
  const panelSlug = new URL(request.url).searchParams.get("panel");
268
222
  await attachPanelPage(pageData as Record<string, unknown>, panelSlug);
@@ -284,39 +238,22 @@ export async function registerRegistryPageRoute(
284
238
  await applyBrandOrgPath(
285
239
  pageData as Record<string, unknown>,
286
240
  resolved,
287
- // brand_org_path is the row schema's own field now — the derived
288
- // type sees it, so the narrowing cast the mirror needed is gone.
289
241
  routeConfig.brand_org_path,
290
242
  params,
291
243
  serviceSlug,
292
244
  );
293
245
  warnUncoveredBlocks(pageData, components, serviceSlug, params.path, log);
294
- // Detached (NOT awaited into the response). prefetchAiChatSession -> createOrRestore is DB-heavy (~1.1s), per-user, and uncached, and was
295
- // awaited on EVERY loadData — the dominant board loadMs cost (confirmed
296
- // aiChatPrefetch=1145ms = the full loadMs). Fire it in the background so
297
- // it still warms the server-side session (users who open chat find it
298
- // warm via their live fetch) without blocking the page response. The SSR
299
- // chat seed (pageData.aiChatSession) is sacrificed — chat opens with a
300
- // live fetch instead of SSR-instant — the right trade: most loads never
301
- // open chat, so eager-awaited prefetch was the waste. orgId is the
302
- // URL-derived org (resolveAiOrgId) — see prefetch-org-id.ts for why
303
- // segment 1 is preferred over segment 0.
304
246
  const aiOrgId = resolveAiOrgId(new URL(request.url).pathname);
305
247
  void prefetchAiChatSession(resolved, sessionUserId, aiOrgId).catch((err) =>
306
248
  log.warn(
307
249
  `[scala-install:${serviceSlug}] background ai-chat prefetch failed: ${err instanceof Error ? err.message : err}`,
308
250
  ),
309
251
  );
310
- // Proposal pages: resolve the {{approvalEmail}} button token to the
311
- // org's registered company email (creator email as fallback).
312
252
  if (resolved.proposal) {
313
253
  await applyApprovalEmailToken(
314
254
  pageData as Record<string, unknown>,
315
255
  resolved.proposal as { created_by?: string } | undefined,
316
256
  );
317
- // Stash the partner logo from the partnerOrg loader source (CRM record
318
- // loaded by partner_id) onto pageData.partnerLogo — header fills
319
- // automatically, no inline body-Hero prop needed.
320
257
  applyPartnerBrand(
321
258
  pageData as Record<string, unknown>,
322
259
  resolved.partnerOrg as
@@ -324,10 +261,6 @@ export async function registerRegistryPageRoute(
324
261
  | undefined
325
262
  | null,
326
263
  );
327
- // Auto-resolve the client's company logo from the clientOrg loader
328
- // source (contact → company → crm_organisations.logo_url). Only
329
- // injects when the Hero's clientLogo prop is empty — a manually set
330
- // logo always wins.
331
264
  applyClientLogo(
332
265
  pageData as Record<string, unknown>,
333
266
  resolved.clientOrg as
@@ -353,10 +286,6 @@ export async function registerRegistryPageRoute(
353
286
  providers: routeProviders,
354
287
  renderMode: routeRenderMode,
355
288
  loadData: loadData as never,
356
- // The portal home redirect follows the DECLARED portal_scoped service
357
- // fact (see RegistryRouteArgs.portalScoped) — a signed-in user at "/" of
358
- // an org-scoped portal belongs at /<websiteId>/<orgId>/dashboard; a
359
- // flat-path service (the fleet dashboard) serves its real page at "/".
360
289
  portalHomeRedirect: portalScoped,
361
290
  serviceSlug,
362
291
  });
@@ -374,10 +303,6 @@ export async function registerRegistryPageRoute(
374
303
  });
375
304
 
376
305
  registeredPatterns.push(pattern);
377
- // Boot-validated snapshot (the LIVE read is per request — see
378
- // readLiveLoaderConfig); name the shape so a misconfigured row is
379
- // visible at boot, and note the live contract so "my loader edit
380
- // didn't take effect" routes to the graph SWR window, not a restart.
381
306
  log.info(
382
307
  `[scala-install:${serviceSlug}] registered registry page route "${pattern}" → resource_loader "${loaderSlug}" (boot-validated: ${describeCapturedLoaderConfig(loaderConfig)}; re-read live per request)`,
383
308
  );
@@ -14,10 +14,6 @@ import { isLoopbackHost } from "@teamscala/os/observability/loopback-host";
14
14
  import { handleAssetProxy } from "./asset-proxy";
15
15
  import { resolveBrandFaviconPath, warmBrandFaviconCache } from "./resolve-brand-favicon";
16
16
 
17
- // Served ONLY when the org has no brand favicon, so the correct colour is the
18
- // platform brand default rather than a literal. This changes the generic
19
- // fallback mark from indigo to the canonical slate; any org with its own
20
- // favicon is unaffected (resolveBrandFaviconPath wins before this is reached).
21
17
  const FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="6" fill="${FALLBACK_PRIMARY_COLOR}"/><text x="16" y="23" text-anchor="middle" font-size="20" font-family="system-ui" fill="white">S</text></svg>`;
22
18
 
23
19
  export async function registerStaticAssets(
@@ -25,9 +21,6 @@ export async function registerStaticAssets(
25
21
  serviceSlug: string,
26
22
  brandOrg?: string,
27
23
  ): Promise<void> {
28
- // Warm the brand-favicon cache at boot — the S3 existence probe is a 6-21s
29
- // cold call that otherwise stalls the first /favicon.ico after every
30
- // restart. Fire-and-forget; never blocks boot or a request.
31
24
  warmBrandFaviconCache(brandOrg);
32
25
 
33
26
  app.get("/health", (c) =>
@@ -38,16 +31,6 @@ export async function registerStaticAssets(
38
31
  }),
39
32
  );
40
33
 
41
- // Localhost-gated heap-attribution probe — the heapSize-vs-RSS split (JS heap
42
- // vs native: OTel/Prisma-engine/buffers/compiled code) plus the top object
43
- // types by count. Read-only, no secrets; gated to a localhost Host so it never
44
- // answers over the public domain. Registered here (before auth + page routes)
45
- // so it is reachable on every service kind, like /health
46
- // (`verify-via-127.0.0.1`, sibling of `runtime-metrics-via-railway-cli`).
47
- //
48
- // The payload shape lives in @teamscala/os so standalone `Bun.serve` daemons
49
- // (scala-guard) answer this probe identically — `scala-tools heap <slug>` must
50
- // not mean different things depending on which process replied.
51
34
  app.get("/_heap", async (c) => {
52
35
  if (!isLoopbackHost(c.req.header("host"))) return c.json({ error: "localhost only" }, 403);
53
36
  try {
@@ -61,16 +44,6 @@ export async function registerStaticAssets(
61
44
  }
62
45
  });
63
46
 
64
- // Build-id: the content hash of the CURRENT client bundle, read FRESH from the asset manifest on every call (never a process-pinned copy) so a
65
- // post-restart rebuild reports its new id immediately. The client self-heal (page-client/boot/build-self-heal) polls this on tab refocus; if it differs
66
- // from the build the tab booted with, a new build shipped and the open tab is
67
- // stale → it reloads. no-store HTML + immutable hashed assets already prevent
68
- // staleness on every FRESH load; this is what closes the one remaining hole —
69
- // a tab left OPEN across a deploy, holding old HTML pinned to old asset
70
- // hashes, with nothing telling it to re-fetch (the "my tablet still shows the
71
- // old broken page" class). The id derives from hydratePath exactly as the
72
- // SSR-stamped window.__SCALA_BUILD__ does (render-page-html), so they can
73
- // never structurally disagree and trigger a reload loop.
74
47
  app.get("/api/build-id", async (c) => {
75
48
  const path = join(process.cwd(), "dist", "client", "asset-manifest.json");
76
49
  let build = "dev";
@@ -84,10 +57,6 @@ export async function registerStaticAssets(
84
57
  return c.json({ build }, 200, { "cache-control": "no-store" });
85
58
  });
86
59
 
87
- // A branded host (service_install.config.brand_org) never serves the
88
- // generic platform mark — both favicon paths resolve the owning org's
89
- // icon from its asset store; the generic SVG remains only for hosts
90
- // with no declared owner (multi-tenant / infra).
91
60
  app.get("/favicon.svg", async (_c) => {
92
61
  const brand = await resolveBrandFaviconPath(brandOrg);
93
62
  if (brand)
@@ -105,18 +74,10 @@ export async function registerStaticAssets(
105
74
 
106
75
  app.get("/favicon.ico", async (c) => {
107
76
  const brand = await resolveBrandFaviconPath(brandOrg);
108
- // 302 (not 301): the brand asset is row-driven and must stay
109
- // re-resolvable — a permanent redirect would pin stale targets in
110
- // browser caches, the exact failure this route exists to end.
111
- // cache-control lets the browser cache the redirect (5min) so it
112
- // doesn't re-hit the origin per page load — mirrors /favicon.svg.
113
77
  c.header("cache-control", "public, max-age=300");
114
78
  return c.redirect(brand ?? "/favicon.svg", 302);
115
79
  });
116
80
 
117
- // Same-origin S3 asset proxy: /api/assets/<key> → bucket. Public,
118
- // immutable cache, no auth. Canonical URL form for every logo, favicon,
119
- // avatar, uploaded asset — emitted by toAssetPath() in @teamscala/core-ui/theme.
120
81
  app.get("/api/assets/*", (c) => handleAssetProxy(c.req.raw));
121
82
 
122
83
  app.use("/assets/*", etag());
@@ -137,12 +98,6 @@ export async function registerStaticAssets(
137
98
  return c.notFound();
138
99
  });
139
100
 
140
- // Extension auto-update files (.crx + updates.xml) served from ./ext/.
141
- // Chrome's extension updater fetches updates.xml over plain HTTPS with no
142
- // auth, so this route is registered before any auth gate and on every UI
143
- // service. Files are served with short-lived cache so updates propagate
144
- // promptly. The ./ext/ directory is gitignored and populated by the
145
- // extension build pipeline.
146
101
  app.get("/ext/*", async (c) => {
147
102
  const filePath = join(
148
103
  process.cwd(),
@@ -37,15 +37,6 @@ export async function resolveBrandFaviconPath(
37
37
  });
38
38
  }
39
39
 
40
- /**
41
- * Eagerly resolve + cache the favicon at boot so the FIRST user request never
42
- * pays the S3 existence-probe latency. The probe (`handleAssetProxy` →
43
- * `s3.file(key).stat()`) is a COLD S3 call — the connection setup to the asset
44
- * store takes 6s locally / up to 21s in prod, and the in-process cache resets
45
- * on every restart (deploys, dep-sync). Without warming, the first
46
- * /favicon.ico after each restart stalls for that duration (operator-reported
47
- * 23s). Fire-and-forget — runs in the background during boot, never blocks.
48
- */
49
40
  export function warmBrandFaviconCache(brandOrg: string | undefined): void {
50
41
  if (!brandOrg) return;
51
42
  void resolveBrandFaviconPath(brandOrg);