@teamscala/service-boot 0.23.523 → 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.
- package/package.json +1 -9
- package/src/agreement-gate/agreement-lookup.ts +0 -7
- package/src/agreement-gate/authorized-emails.ts +0 -14
- package/src/agreement-gate/placeholder-strip.ts +0 -10
- package/src/agreement-gate/resolution-caches.ts +0 -5
- package/src/agreement-gate/settings.ts +0 -22
- package/src/agreement-gate/signing-session.ts +0 -8
- package/src/agreement-gate/signing-shell.ts +0 -9
- package/src/agreement-gate.ts +0 -52
- package/src/apply-logger-config.ts +1 -9
- package/src/asset-cache-key.ts +1 -5
- package/src/asset-optimize.ts +1 -7
- package/src/asset-proxy.ts +1 -10
- package/src/asset-social-card.ts +1 -9
- package/src/attach-agreement-magic.ts +0 -14
- package/src/attach-portal-members.ts +0 -17
- package/src/attach-proposal-accept.ts +1 -21
- package/src/attach-proposal-magic.ts +1 -13
- package/src/attach-transfers-upload.ts +0 -21
- package/src/auth/sign-out.ts +1 -44
- package/src/auth-gate/public-paths.ts +1 -96
- package/src/auth-gate.ts +1 -41
- package/src/bind-page-handler.ts +1 -83
- package/src/build-client-assets/build-lock.ts +1 -11
- package/src/build-client-assets/hashed-output.ts +1 -11
- package/src/build-client-assets/hydrate-bundle.ts +1 -137
- package/src/build-client-assets/manifest.ts +1 -30
- package/src/build-client-assets/source-candidates.ts +0 -51
- package/src/build-client-assets/sourcemaps-upload.ts +1 -52
- package/src/build-client-assets/styles-build.ts +1 -71
- package/src/build-client-assets.ts +1 -30
- package/src/capability-slug.ts +1 -45
- package/src/compose-page-title.ts +1 -34
- package/src/content-gate/access.ts +0 -34
- package/src/content-gate/catalog.ts +0 -52
- package/src/content-gate/config-types.ts +3 -90
- package/src/content-gate/gate-entry-clears.ts +0 -18
- package/src/content-gate/gate.ts +0 -27
- package/src/content-gate/parse-cookie.ts +0 -4
- package/src/content-page-floor.ts +0 -28
- package/src/csrf-protection.ts +1 -28
- package/src/error-page-logo.ts +2 -35
- package/src/expand-roster.ts +1 -25
- package/src/gate-ttls.ts +0 -12
- package/src/gpt-gate.ts +1 -19
- package/src/hono-error-handler.ts +0 -19
- package/src/install-harness.ts +1 -12
- package/src/install-logging.ts +1 -47
- package/src/load-page-manifest.ts +1 -29
- package/src/load-service-install-row.ts +42 -10
- package/src/module-not-found.ts +0 -17
- package/src/mount-orpc-router.ts +1 -94
- package/src/mount-server-packages.ts +1 -37
- package/src/mount.ts +1 -4
- package/src/mutable-app-ref.ts +0 -21
- package/src/orpc-contract-rows.json +289 -0
- package/src/orpc-openapi-document.ts +1 -19
- package/src/orpc-openapi-gate.ts +1 -23
- package/src/page-route-shell-types.ts +1 -8
- package/src/page-server-loader-graph.ts +0 -10
- package/src/page-server-orpc/server-client.ts +1 -23
- package/src/pg-boss-handler-registry.ts +1 -24
- package/src/pg-boss-mount.ts +1 -109
- package/src/proposal-authorized-emails.ts +0 -32
- package/src/proposal-gate.ts +1 -52
- package/src/quiz-gate.ts +1 -31
- package/src/read-html-template.ts +1 -49
- package/src/readiness-gate.ts +0 -8
- package/src/register-api-routes.ts +1 -11
- package/src/register-crash-handlers.ts +1 -6
- package/src/register-page-routes.ts +1 -12
- package/src/register-page-server-client.ts +1 -66
- package/src/register-registry-page-route/ai-chat-prefetch.ts +1 -24
- package/src/register-registry-page-route/apply-page-branding.ts +1 -100
- package/src/register-registry-page-route/captured-loader-config.ts +1 -12
- package/src/register-registry-page-route/graph-cache.ts +1 -38
- package/src/register-registry-page-route/live-loader-config.ts +1 -22
- package/src/register-registry-page-route/meta-title.ts +1 -36
- package/src/register-registry-page-route/page-data-shared.ts +1 -18
- package/src/register-registry-page-route/prefetch-org-id.ts +1 -24
- package/src/register-registry-page-route/proposal-approval-email-token.ts +1 -22
- package/src/register-registry-page-route/proposal-brand-stamping.ts +1 -31
- package/src/register-registry-page-route/public-shell-page-data.ts +1 -34
- package/src/register-registry-page-route/region-slots-page-data.ts +1 -8
- package/src/register-registry-page-route/resolve-page-data.ts +1 -10
- package/src/register-registry-page-route/resolve-panel-page.ts +1 -10
- package/src/register-registry-page-route/resolve-request-graph.ts +1 -68
- package/src/register-registry-page-route/run-content-gates.ts +3 -64
- package/src/register-registry-page-route/single-page-data.ts +1 -9
- package/src/register-registry-page-route/stamp-session-context.ts +1 -10
- package/src/register-registry-page-route/warn-uncovered-blocks.ts +1 -18
- package/src/register-registry-page-route.ts +1 -93
- package/src/register-static-assets.ts +1 -53
- package/src/register-ws-routes.ts +1 -21
- package/src/resolve-brand-favicon.ts +0 -22
- package/src/resolve-service-port.ts +1 -61
- package/src/resolve-theme-mode.ts +1 -30
- package/src/resolvers/app-header.ts +1 -20
- package/src/resolvers/fetch-org-brand.ts +1 -11
- package/src/resolvers/portal-route-paths.ts +1 -29
- package/src/resolvers/website-by-host-map.ts +1 -7
- package/src/resource-not-found.ts +0 -48
- package/src/run-boot-hook-list.ts +1 -13
- package/src/run-boot-hooks.ts +1 -19
- package/src/scala-tools-currency-watchdog.ts +1 -7
- package/src/security-headers.ts +1 -7
- package/src/select-service-routes.ts +1 -31
- package/src/server-error-page.ts +0 -18
- package/src/theme-fonts-link.ts +1 -21
- package/src/transfers/presigned-upload.ts +1 -27
- package/src/two-phase-full-app.ts +0 -82
- package/src/two-phase-minimal-app.ts +1 -111
- package/src/two-phase-readiness.ts +0 -6
- package/src/types.ts +0 -17
- package/src/ui-service.ts +2 -48
- package/src/ws-boot-self-test.ts +1 -14
- package/src/content-gate/gate-entry-marker.test.ts +0 -111
- package/src/content-gate/token.segmentation.test.ts +0 -76
- package/src/content-gate/token.test.ts +0 -65
- package/src/content-gate/token.ts +0 -219
- package/src/is-react-element-type.test.ts +0 -45
- package/src/is-react-element-type.ts +0 -20
- package/src/page-server-orpc/hub-mcp-client.ts +0 -20
- package/src/pg-boss-mount.test.ts +0 -101
- package/src/resource-chat-inject.page-assistant.test.ts +0 -145
- package/src/resource-chat-inject.test.ts +0 -240
- package/src/resource-chat-inject.ts +0 -374
|
@@ -1,28 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Block-coverage guard (protective-layers-fail-explicitly) for registered
|
|
7
|
-
* page routes.
|
|
8
|
-
*
|
|
9
|
-
* Split out of register-registry-page-route.ts (924 lines) — byte-for-byte
|
|
10
|
-
* move; only the import paths adjusted.
|
|
11
4
|
*/
|
|
12
5
|
|
|
13
6
|
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
14
7
|
import type { RendererComponentEntry } from "../types.ts";
|
|
15
8
|
|
|
16
|
-
/**
|
|
17
|
-
* A page that renders a block whose component is NOT registered — because
|
|
18
|
-
* its package is missing from service_install.config.block_packages —
|
|
19
|
-
* silently renders BLANK. That is the exact failure behind the 2026-06-04
|
|
20
|
-
* scala-sign regression (sign-in-block dropped from block_packages → the
|
|
21
|
-
* gate + signature fields vanished) which left only a benign boot WARN.
|
|
22
|
-
* This turns that silent blank into a loud, GlitchTip-visible ERROR naming
|
|
23
|
-
* the page + block, throttled to once per (service, block type) so a hot
|
|
24
|
-
* path can never spam it.
|
|
25
|
-
*/
|
|
26
9
|
const uncoveredBlocksLogged = new Set<string>();
|
|
27
10
|
|
|
28
11
|
function collectBlockTypes(node: unknown, out: Set<string>): void {
|
|
@@ -1,23 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Reads `page_route` registry rows for the current service slug and registers
|
|
7
|
-
* Hono routes that resolve data via `resource_loader` graph-based sources.
|
|
8
|
-
*
|
|
9
|
-
* Every page_route uses pattern `/:id` and every resource_loader uses a
|
|
10
|
-
* `sources[]` dependency graph. When header/footer sources are present in
|
|
11
|
-
* the resolved graph, the output includes a `regions` array for shell rendering.
|
|
12
|
-
* Otherwise it produces single-page puckData.
|
|
13
|
-
*
|
|
14
|
-
* This module is the ROUTE-REGISTRATION unit: row loading, specificity
|
|
15
|
-
* ordering, handler binding. The per-request loadData pipeline segments live
|
|
16
|
-
* as siblings under ./register-registry-page-route/ (graph-cache,
|
|
17
|
-
* resolve-request-graph, stamp-session-context, run-content-gates,
|
|
18
|
-
* apply-page-branding, resolve-panel-page, warn-uncovered-blocks,
|
|
19
|
-
* resolve-page-data, meta-title, ai-chat-prefetch, captured-loader-config,
|
|
20
|
-
* proposal-*) — split when the parent held them all at 924 lines.
|
|
21
4
|
*/
|
|
22
5
|
|
|
23
6
|
import type { InferOutput } from "valibot";
|
|
@@ -139,22 +122,11 @@ export async function registerRegistryPageRoute(
|
|
|
139
122
|
for (const routeRow of serviceRoutes) {
|
|
140
123
|
const routeConfig = routeRow.config;
|
|
141
124
|
const pattern = routeConfig.pattern;
|
|
142
|
-
// loader_slug is a page_route runtime value and resource_loader is runtime
|
|
143
|
-
// data: a row created after this build is exactly the live-edit case this
|
|
144
|
-
// module serves, so the generated slug union (a build-time snapshot) can
|
|
145
|
-
// only be asserted, never enforced, here. An unknown slug still fails
|
|
146
|
-
// closed below (RegistryConfigMissingError skips the route).
|
|
147
125
|
const loaderSlug = routeConfig.loader_slug as ResourceLoaderKey;
|
|
148
126
|
const routeProviders = routeConfig.providers ?? baseProviders;
|
|
149
127
|
|
|
150
128
|
let loaderConfig: LoaderConfig | null = null;
|
|
151
129
|
try {
|
|
152
|
-
// Boot-time warm + validate: a missing row skips the route (warn),
|
|
153
|
-
// an invalid row fails closed. resource_loader is NOT on the
|
|
154
|
-
// deploy-time bake list (constitution installer-and-runtime) — it
|
|
155
|
-
// is re-read per request via readLiveLoaderConfig below — so this
|
|
156
|
-
// boot read exists ONLY to catch a misconfigured row early, never
|
|
157
|
-
// as the source of truth for live rendering.
|
|
158
130
|
loaderConfig = await loadRegistryConfig(
|
|
159
131
|
"resource_loader",
|
|
160
132
|
loaderSlug,
|
|
@@ -173,22 +145,12 @@ export async function registerRegistryPageRoute(
|
|
|
173
145
|
throw err;
|
|
174
146
|
}
|
|
175
147
|
|
|
176
|
-
// Gate for the static-shell graph cache (see module header).
|
|
177
148
|
const isStaticShell =
|
|
178
149
|
!routeConfig.resource_type &&
|
|
179
150
|
!pattern.includes(":") &&
|
|
180
151
|
!pattern.includes("*");
|
|
181
152
|
|
|
182
153
|
const loadData = async (request: Request) => {
|
|
183
|
-
// resource_loader is runtime data, not deploy-time baked. Re-read it
|
|
184
|
-
// per request — loadRegistryConfig's SWR cache makes the fresh-window
|
|
185
|
-
// hit a memory read, not a DB round trip. Throws into the branded
|
|
186
|
-
// error path on a vanished or invalid row: the boot-time snapshot
|
|
187
|
-
// must NEVER silently outlive the row it captured. This is the fix
|
|
188
|
-
// for the loader-row-edits-look-like-they-don't-work defect (every
|
|
189
|
-
// edit used to need a service restart; the sign-page-with-brand
|
|
190
|
-
// `agreement` source added at 22:16 still hadn't reached
|
|
191
|
-
// __PAGE_DATA__.loaderData at 22:29 — 13 minutes, and never).
|
|
192
154
|
const liveLoaderConfig = await readLiveLoaderConfig(loaderSlug);
|
|
193
155
|
const url = new URL(request.url);
|
|
194
156
|
const params = extractUrlParams(pattern, url.pathname);
|
|
@@ -202,10 +164,6 @@ export async function registerRegistryPageRoute(
|
|
|
202
164
|
params,
|
|
203
165
|
isStaticShell,
|
|
204
166
|
serviceSlug,
|
|
205
|
-
// Folds the live config into the per-route graphKey so a row edit
|
|
206
|
-
// invalidates the cached graph immediately (the graph is a
|
|
207
|
-
// function of the config; without this the SWR graph cache
|
|
208
|
-
// would still hold the old shape until its fresh window lapsed).
|
|
209
167
|
configFingerprint: fingerprintLoaderConfig(liveLoaderConfig),
|
|
210
168
|
});
|
|
211
169
|
const { sessionUserId, sessionOrgId } = await stampSessionContext(
|
|
@@ -220,8 +178,6 @@ export async function registerRegistryPageRoute(
|
|
|
220
178
|
resolved,
|
|
221
179
|
gateCookie,
|
|
222
180
|
Date.now(),
|
|
223
|
-
routeConfig.assistant,
|
|
224
|
-
regionSlots,
|
|
225
181
|
);
|
|
226
182
|
const meta = buildMetaFromContext(routeConfig.meta_context, resolved);
|
|
227
183
|
await applySignInBrandOverride(resolved, request);
|
|
@@ -233,13 +189,6 @@ export async function registerRegistryPageRoute(
|
|
|
233
189
|
) {
|
|
234
190
|
meta.title = pageMetaTitle.trim();
|
|
235
191
|
}
|
|
236
|
-
// Org-prefixed page titles (feature flag page-title-org-prefix, read
|
|
237
|
-
// LIVE per request — feature flags are never cached). The title prefix
|
|
238
|
-
// is the OWNING ORG'S name, never the internal website label
|
|
239
|
-
// ("Public Pages: <subject>" on a client-facing document was the
|
|
240
|
-
// defect; operator ruling 2026-08-17). Absent or unreadable flag ⇒
|
|
241
|
-
// legacy <website.name>: <subject> derivation — the switch fails toward
|
|
242
|
-
// the pre-flag behaviour, and flipping it back is one registry edit.
|
|
243
192
|
let orgTitlePrefix = false;
|
|
244
193
|
try {
|
|
245
194
|
orgTitlePrefix = (
|
|
@@ -250,14 +199,8 @@ export async function registerRegistryPageRoute(
|
|
|
250
199
|
)
|
|
251
200
|
).enabled;
|
|
252
201
|
} catch {
|
|
253
|
-
// Flag row absent — legacy derivation.
|
|
254
202
|
}
|
|
255
203
|
const themeParam = url.searchParams.get("theme");
|
|
256
|
-
// ?theme is the EMBED override — only honor it when this page is
|
|
257
|
-
// actually framed (a form/quiz in a GPT tool panel, sec-fetch-dest:
|
|
258
|
-
// iframe). A DIRECT visit carrying ?theme= (e.g. copied from a GPT
|
|
259
|
-
// embed URL) must NOT override the org's DB theme — what the visitor
|
|
260
|
-
// sees is org-driven, never URL-driven.
|
|
261
204
|
const isFramedEmbed =
|
|
262
205
|
request.headers.get("sec-fetch-dest") === "iframe";
|
|
263
206
|
const themeOverride =
|
|
@@ -274,12 +217,6 @@ export async function registerRegistryPageRoute(
|
|
|
274
217
|
themeOverride,
|
|
275
218
|
orgTitlePrefix,
|
|
276
219
|
);
|
|
277
|
-
// The matched route params (extractUrlParams + host/path) ride the
|
|
278
|
-
// per-request pageData so the SSR resolve ctx can scope PUBLIC
|
|
279
|
-
// block data per request — data_source loaders read ctx.pathParams
|
|
280
|
-
// (query-data-source); without them a public table would return
|
|
281
|
-
// every person's rows. Attached AFTER the graph cache (per-request,
|
|
282
|
-
// never cached with the graph).
|
|
283
220
|
(pageData as Record<string, unknown>).pathParams = params;
|
|
284
221
|
const panelSlug = new URL(request.url).searchParams.get("panel");
|
|
285
222
|
await attachPanelPage(pageData as Record<string, unknown>, panelSlug);
|
|
@@ -301,39 +238,22 @@ export async function registerRegistryPageRoute(
|
|
|
301
238
|
await applyBrandOrgPath(
|
|
302
239
|
pageData as Record<string, unknown>,
|
|
303
240
|
resolved,
|
|
304
|
-
// brand_org_path is the row schema's own field now — the derived
|
|
305
|
-
// type sees it, so the narrowing cast the mirror needed is gone.
|
|
306
241
|
routeConfig.brand_org_path,
|
|
307
242
|
params,
|
|
308
243
|
serviceSlug,
|
|
309
244
|
);
|
|
310
245
|
warnUncoveredBlocks(pageData, components, serviceSlug, params.path, log);
|
|
311
|
-
// Detached (NOT awaited into the response). prefetchAiChatSession -> createOrRestore is DB-heavy (~1.1s), per-user, and uncached, and was
|
|
312
|
-
// awaited on EVERY loadData — the dominant board loadMs cost (confirmed
|
|
313
|
-
// aiChatPrefetch=1145ms = the full loadMs). Fire it in the background so
|
|
314
|
-
// it still warms the server-side session (users who open chat find it
|
|
315
|
-
// warm via their live fetch) without blocking the page response. The SSR
|
|
316
|
-
// chat seed (pageData.aiChatSession) is sacrificed — chat opens with a
|
|
317
|
-
// live fetch instead of SSR-instant — the right trade: most loads never
|
|
318
|
-
// open chat, so eager-awaited prefetch was the waste. orgId is the
|
|
319
|
-
// URL-derived org (resolveAiOrgId) — see prefetch-org-id.ts for why
|
|
320
|
-
// segment 1 is preferred over segment 0.
|
|
321
246
|
const aiOrgId = resolveAiOrgId(new URL(request.url).pathname);
|
|
322
247
|
void prefetchAiChatSession(resolved, sessionUserId, aiOrgId).catch((err) =>
|
|
323
248
|
log.warn(
|
|
324
249
|
`[scala-install:${serviceSlug}] background ai-chat prefetch failed: ${err instanceof Error ? err.message : err}`,
|
|
325
250
|
),
|
|
326
251
|
);
|
|
327
|
-
// Proposal pages: resolve the {{approvalEmail}} button token to the
|
|
328
|
-
// org's registered company email (creator email as fallback).
|
|
329
252
|
if (resolved.proposal) {
|
|
330
253
|
await applyApprovalEmailToken(
|
|
331
254
|
pageData as Record<string, unknown>,
|
|
332
255
|
resolved.proposal as { created_by?: string } | undefined,
|
|
333
256
|
);
|
|
334
|
-
// Stash the partner logo from the partnerOrg loader source (CRM record
|
|
335
|
-
// loaded by partner_id) onto pageData.partnerLogo — header fills
|
|
336
|
-
// automatically, no inline body-Hero prop needed.
|
|
337
257
|
applyPartnerBrand(
|
|
338
258
|
pageData as Record<string, unknown>,
|
|
339
259
|
resolved.partnerOrg as
|
|
@@ -341,10 +261,6 @@ export async function registerRegistryPageRoute(
|
|
|
341
261
|
| undefined
|
|
342
262
|
| null,
|
|
343
263
|
);
|
|
344
|
-
// Auto-resolve the client's company logo from the clientOrg loader
|
|
345
|
-
// source (contact → company → crm_organisations.logo_url). Only
|
|
346
|
-
// injects when the Hero's clientLogo prop is empty — a manually set
|
|
347
|
-
// logo always wins.
|
|
348
264
|
applyClientLogo(
|
|
349
265
|
pageData as Record<string, unknown>,
|
|
350
266
|
resolved.clientOrg as
|
|
@@ -370,10 +286,6 @@ export async function registerRegistryPageRoute(
|
|
|
370
286
|
providers: routeProviders,
|
|
371
287
|
renderMode: routeRenderMode,
|
|
372
288
|
loadData: loadData as never,
|
|
373
|
-
// The portal home redirect follows the DECLARED portal_scoped service
|
|
374
|
-
// fact (see RegistryRouteArgs.portalScoped) — a signed-in user at "/" of
|
|
375
|
-
// an org-scoped portal belongs at /<websiteId>/<orgId>/dashboard; a
|
|
376
|
-
// flat-path service (the fleet dashboard) serves its real page at "/".
|
|
377
289
|
portalHomeRedirect: portalScoped,
|
|
378
290
|
serviceSlug,
|
|
379
291
|
});
|
|
@@ -391,10 +303,6 @@ export async function registerRegistryPageRoute(
|
|
|
391
303
|
});
|
|
392
304
|
|
|
393
305
|
registeredPatterns.push(pattern);
|
|
394
|
-
// Boot-validated snapshot (the LIVE read is per request — see
|
|
395
|
-
// readLiveLoaderConfig); name the shape so a misconfigured row is
|
|
396
|
-
// visible at boot, and note the live contract so "my loader edit
|
|
397
|
-
// didn't take effect" routes to the graph SWR window, not a restart.
|
|
398
306
|
log.info(
|
|
399
307
|
`[scala-install:${serviceSlug}] registered registry page route "${pattern}" → resource_loader "${loaderSlug}" (boot-validated: ${describeCapturedLoaderConfig(loaderConfig)}; re-read live per request)`,
|
|
400
308
|
);
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Routes every service exposes regardless of registry config: /health
|
|
7
|
-
* (liveness probe), /favicon.svg (default brand mark), /api/assets/*
|
|
8
|
-
* (S3 asset proxy), and /assets/* (client bundle + stylesheet produced
|
|
9
|
-
* by build-client-assets.ts into dist/client/, served identically in
|
|
10
|
-
* dev and prod per scala-os/ui.md `bun-build-only`).
|
|
11
4
|
*/
|
|
12
5
|
|
|
13
6
|
import { join } from "node:path";
|
|
@@ -21,10 +14,6 @@ import { isLoopbackHost } from "@teamscala/os/observability/loopback-host";
|
|
|
21
14
|
import { handleAssetProxy } from "./asset-proxy";
|
|
22
15
|
import { resolveBrandFaviconPath, warmBrandFaviconCache } from "./resolve-brand-favicon";
|
|
23
16
|
|
|
24
|
-
// Served ONLY when the org has no brand favicon, so the correct colour is the
|
|
25
|
-
// platform brand default rather than a literal. This changes the generic
|
|
26
|
-
// fallback mark from indigo to the canonical slate; any org with its own
|
|
27
|
-
// favicon is unaffected (resolveBrandFaviconPath wins before this is reached).
|
|
28
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>`;
|
|
29
18
|
|
|
30
19
|
export async function registerStaticAssets(
|
|
@@ -32,9 +21,6 @@ export async function registerStaticAssets(
|
|
|
32
21
|
serviceSlug: string,
|
|
33
22
|
brandOrg?: string,
|
|
34
23
|
): Promise<void> {
|
|
35
|
-
// Warm the brand-favicon cache at boot — the S3 existence probe is a 6-21s
|
|
36
|
-
// cold call that otherwise stalls the first /favicon.ico after every
|
|
37
|
-
// restart. Fire-and-forget; never blocks boot or a request.
|
|
38
24
|
warmBrandFaviconCache(brandOrg);
|
|
39
25
|
|
|
40
26
|
app.get("/health", (c) =>
|
|
@@ -45,16 +31,6 @@ export async function registerStaticAssets(
|
|
|
45
31
|
}),
|
|
46
32
|
);
|
|
47
33
|
|
|
48
|
-
// Localhost-gated heap-attribution probe — the heapSize-vs-RSS split (JS heap
|
|
49
|
-
// vs native: OTel/Prisma-engine/buffers/compiled code) plus the top object
|
|
50
|
-
// types by count. Read-only, no secrets; gated to a localhost Host so it never
|
|
51
|
-
// answers over the public domain. Registered here (before auth + page routes)
|
|
52
|
-
// so it is reachable on every service kind, like /health
|
|
53
|
-
// (`verify-via-127.0.0.1`, sibling of `runtime-metrics-via-railway-cli`).
|
|
54
|
-
//
|
|
55
|
-
// The payload shape lives in @teamscala/os so standalone `Bun.serve` daemons
|
|
56
|
-
// (scala-guard) answer this probe identically — `scala-tools heap <slug>` must
|
|
57
|
-
// not mean different things depending on which process replied.
|
|
58
34
|
app.get("/_heap", async (c) => {
|
|
59
35
|
if (!isLoopbackHost(c.req.header("host"))) return c.json({ error: "localhost only" }, 403);
|
|
60
36
|
try {
|
|
@@ -68,16 +44,6 @@ export async function registerStaticAssets(
|
|
|
68
44
|
}
|
|
69
45
|
});
|
|
70
46
|
|
|
71
|
-
// 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
|
|
72
|
-
// 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
|
|
73
|
-
// from the build the tab booted with, a new build shipped and the open tab is
|
|
74
|
-
// stale → it reloads. no-store HTML + immutable hashed assets already prevent
|
|
75
|
-
// staleness on every FRESH load; this is what closes the one remaining hole —
|
|
76
|
-
// a tab left OPEN across a deploy, holding old HTML pinned to old asset
|
|
77
|
-
// hashes, with nothing telling it to re-fetch (the "my tablet still shows the
|
|
78
|
-
// old broken page" class). The id derives from hydratePath exactly as the
|
|
79
|
-
// SSR-stamped window.__SCALA_BUILD__ does (render-page-html), so they can
|
|
80
|
-
// never structurally disagree and trigger a reload loop.
|
|
81
47
|
app.get("/api/build-id", async (c) => {
|
|
82
48
|
const path = join(process.cwd(), "dist", "client", "asset-manifest.json");
|
|
83
49
|
let build = "dev";
|
|
@@ -91,10 +57,6 @@ export async function registerStaticAssets(
|
|
|
91
57
|
return c.json({ build }, 200, { "cache-control": "no-store" });
|
|
92
58
|
});
|
|
93
59
|
|
|
94
|
-
// A branded host (service_install.config.brand_org) never serves the
|
|
95
|
-
// generic platform mark — both favicon paths resolve the owning org's
|
|
96
|
-
// icon from its asset store; the generic SVG remains only for hosts
|
|
97
|
-
// with no declared owner (multi-tenant / infra).
|
|
98
60
|
app.get("/favicon.svg", async (_c) => {
|
|
99
61
|
const brand = await resolveBrandFaviconPath(brandOrg);
|
|
100
62
|
if (brand)
|
|
@@ -112,18 +74,10 @@ export async function registerStaticAssets(
|
|
|
112
74
|
|
|
113
75
|
app.get("/favicon.ico", async (c) => {
|
|
114
76
|
const brand = await resolveBrandFaviconPath(brandOrg);
|
|
115
|
-
// 302 (not 301): the brand asset is row-driven and must stay
|
|
116
|
-
// re-resolvable — a permanent redirect would pin stale targets in
|
|
117
|
-
// browser caches, the exact failure this route exists to end.
|
|
118
|
-
// cache-control lets the browser cache the redirect (5min) so it
|
|
119
|
-
// doesn't re-hit the origin per page load — mirrors /favicon.svg.
|
|
120
77
|
c.header("cache-control", "public, max-age=300");
|
|
121
78
|
return c.redirect(brand ?? "/favicon.svg", 302);
|
|
122
79
|
});
|
|
123
80
|
|
|
124
|
-
// Same-origin S3 asset proxy: /api/assets/<key> → bucket. Public,
|
|
125
|
-
// immutable cache, no auth. Canonical URL form for every logo, favicon,
|
|
126
|
-
// avatar, uploaded asset — emitted by toAssetPath() in @teamscala/core-ui/theme.
|
|
127
81
|
app.get("/api/assets/*", (c) => handleAssetProxy(c.req.raw));
|
|
128
82
|
|
|
129
83
|
app.use("/assets/*", etag());
|
|
@@ -144,12 +98,6 @@ export async function registerStaticAssets(
|
|
|
144
98
|
return c.notFound();
|
|
145
99
|
});
|
|
146
100
|
|
|
147
|
-
// Extension auto-update files (.crx + updates.xml) served from ./ext/.
|
|
148
|
-
// Chrome's extension updater fetches updates.xml over plain HTTPS with no
|
|
149
|
-
// auth, so this route is registered before any auth gate and on every UI
|
|
150
|
-
// service. Files are served with short-lived cache so updates propagate
|
|
151
|
-
// promptly. The ./ext/ directory is gitignored and populated by the
|
|
152
|
-
// extension build pipeline.
|
|
153
101
|
app.get("/ext/*", async (c) => {
|
|
154
102
|
const filePath = join(
|
|
155
103
|
process.cwd(),
|
|
@@ -1,26 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Registers each `service_install.websocket_routes[]` entry as a Hono WS route.
|
|
7
|
-
* The ORPC WS (`/api/rpc/ws`) is mounted by the canonical primitive
|
|
8
|
-
* `mountOrpcWs` (@teamscala/orpc/ws/server) over the FULLY-COMPOSED app router —
|
|
9
|
-
* this runs AFTER the app router composition, so the eager RPCHandler snapshots every
|
|
10
|
-
* namespace, and any `output: eventIterator(...)` procedure dispatches (rule
|
|
11
|
-
* `orpc-ws-via-mount-orpc-ws`). It is NOT a server_package WsRouteHandler — it
|
|
12
|
-
* is exempt from the legacy invariant below.
|
|
13
|
-
*
|
|
14
|
-
* The legacy `WsRouteHandler` interface (open/message/close on a Bun
|
|
15
|
-
* ServerWebSocket) is preserved by an adapter for already-migrated
|
|
16
|
-
* server_packages (terminal-ws); new WS handlers should use Hono's WSEvents
|
|
17
|
-
* directly.
|
|
18
|
-
*
|
|
19
|
-
* INVARIANT (legacy paths only): every handler registered via
|
|
20
|
-
* mountCtx.registerWsRoute MUST have a matching entry in config.websocket_routes
|
|
21
|
-
* (minus /api/rpc/ws), and vice versa. A mismatch is fatal — the service cannot
|
|
22
|
-
* boot with handlers declared but never wired (silent 404 on upgrade), or routes
|
|
23
|
-
* that point at handlers no server_package registered.
|
|
24
4
|
*/
|
|
25
5
|
|
|
26
6
|
import { getAppRouter } from "@teamscala/orpc/app-router";
|
|
@@ -1,19 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Host-level brand favicon resolution. A single-brand UI service's host has
|
|
7
|
-
* a KNOWN owner (`service_install.config.brand_org`) — serving the generic
|
|
8
|
-
* platform "S" at the implicit /favicon.ico path there is a defect: browsers
|
|
9
|
-
* and crawlers hit that path directly and their favicon caches are sticky
|
|
10
|
-
* (operator-observed 2026-06-05 on launcha's proposal pages).
|
|
11
|
-
*
|
|
12
|
-
* Resolution is by convention from the org's asset store:
|
|
13
|
-
* `<brand_org>/logos/favicon.png`. Existence is probed once through the
|
|
14
|
-
* in-process asset proxy and cached (positive AND negative) so the favicon
|
|
15
|
-
* route never adds S3 latency per request. No brand_org, or no asset at the
|
|
16
|
-
* convention key → null, and the caller serves the generic fallback.
|
|
17
4
|
*/
|
|
18
5
|
|
|
19
6
|
import { createCache } from "@teamscala/cache/create-cache";
|
|
@@ -50,15 +37,6 @@ export async function resolveBrandFaviconPath(
|
|
|
50
37
|
});
|
|
51
38
|
}
|
|
52
39
|
|
|
53
|
-
/**
|
|
54
|
-
* Eagerly resolve + cache the favicon at boot so the FIRST user request never
|
|
55
|
-
* pays the S3 existence-probe latency. The probe (`handleAssetProxy` →
|
|
56
|
-
* `s3.file(key).stat()`) is a COLD S3 call — the connection setup to the asset
|
|
57
|
-
* store takes 6s locally / up to 21s in prod, and the in-process cache resets
|
|
58
|
-
* on every restart (deploys, dep-sync). Without warming, the first
|
|
59
|
-
* /favicon.ico after each restart stalls for that duration (operator-reported
|
|
60
|
-
* 23s). Fire-and-forget — runs in the background during boot, never blocks.
|
|
61
|
-
*/
|
|
62
40
|
export function warmBrandFaviconCache(brandOrg: string | undefined): void {
|
|
63
41
|
if (!brandOrg) return;
|
|
64
42
|
void resolveBrandFaviconPath(brandOrg);
|
|
@@ -1,63 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system service-
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Decides which port a service binds, from the two sources that can claim one:
|
|
7
|
-
* the PORT environment variable and the port the registry declares for that
|
|
8
|
-
* service (`service_routing.local_port` for a backend, `service_install.port`
|
|
9
|
-
* for a UI service).
|
|
10
|
-
*
|
|
11
|
-
* This is the resolver for BOTH kinds. It lives in service-boot rather than
|
|
12
|
-
* service-runtime because the UI branch boots from here and may not import
|
|
13
|
-
* upward; keeping one copy is the point, since the second copy is exactly what
|
|
14
|
-
* went wrong (below).
|
|
15
|
-
*
|
|
16
|
-
* The rule, and why it is not "env always wins":
|
|
17
|
-
*
|
|
18
|
-
* Railway injects PORT and healthchecks that exact port, reporting the service
|
|
19
|
-
* unavailable if it listens elsewhere. So on Railway the injected PORT is
|
|
20
|
-
* authoritative period — a `service_install`/`service_routing` row carries the
|
|
21
|
-
* service's LOCAL dev port (e.g. portal 5200), and on Railway binding that
|
|
22
|
-
* instead of PORT means Railway probes a port nothing listens on and the deploy
|
|
23
|
-
* fails healthcheck. The registry-wins rule below exists to stop LOCAL port
|
|
24
|
-
* collisions (one host, many services); inside a Railway container there are no
|
|
25
|
-
* sibling services to collide with, so PORT must win. `isRailway` is the caller's
|
|
26
|
-
* assertion that it is booting inside Railway (from `RAILWAY_ENVIRONMENT`).
|
|
27
|
-
*
|
|
28
|
-
* Off Railway, a `service_routing`/`service_install` row means this service has
|
|
29
|
-
* a DECLARED port on this host. There, a PORT that disagrees is a polluted launcher environment, never
|
|
30
|
-
* an intention, and obeying it binds someone else's port. That is not a
|
|
31
|
-
* hypothetical: mcp-ai-chat-system booted onto 3021 — session-picker's port —
|
|
32
|
-
* while its own MCP_SERVER_URL still pointed at 8080. Both processes bound 3021,
|
|
33
|
-
* SO_REUSEPORT load-balanced between them, and every symptom appeared somewhere
|
|
34
|
-
* else: a chat loopback that resolved to nothing, tool-description curation
|
|
35
|
-
* failing for every tool and disabling itself via its circuit breaker, RPC calls
|
|
36
|
-
* landing in the wrong service, and 59 restarts. Nothing failed loudly because
|
|
37
|
-
* nothing ever compared the two numbers.
|
|
38
|
-
*
|
|
39
|
-
* So the registry wins wherever it has an opinion, and the disagreement is
|
|
40
|
-
* REPORTED rather than silently resolved — a wrong port that boots cleanly is
|
|
41
|
-
* the expensive shape, because the damage surfaces far from the cause.
|
|
42
|
-
*
|
|
43
|
-
* That reasoning was written for the backend branch and wired only there, so
|
|
44
|
-
* the UI branch kept taking `process.env.PORT` unconditionally — and on
|
|
45
|
-
* 2026-07-31 reproduced the identical incident in the other direction:
|
|
46
|
-
* scala-quiz (kind:ui, declared 5203) inherited a leaked PORT=3021 from the
|
|
47
|
-
* tmux server environment and bound session-picker's port. Half of every RPC
|
|
48
|
-
* to the session daemon was answered by a quiz service with no /rpc routes.
|
|
49
|
-
*
|
|
50
|
-
* A rule enforced on one of two branches is not enforced. Both branches now
|
|
51
|
-
* call this function, and `port-resolution-is-registry-authoritative` fails
|
|
52
|
-
* the push if either stops.
|
|
53
4
|
*/
|
|
54
5
|
|
|
55
6
|
export interface PortResolution {
|
|
56
|
-
/** The port to bind. */
|
|
57
7
|
port: number;
|
|
58
|
-
/** Which source decided it — for the boot log, so the choice is auditable. */
|
|
59
8
|
source: "env" | "service_routing";
|
|
60
|
-
/** Set when PORT disagreed with the row; the caller logs it. */
|
|
61
9
|
conflict?: { envPort: number; declaredPort: number };
|
|
62
10
|
}
|
|
63
11
|
|
|
@@ -65,14 +13,6 @@ function isBindablePort(value: number | undefined): value is number {
|
|
|
65
13
|
return value !== undefined && Number.isFinite(value) && value > 0 && value <= 65535;
|
|
66
14
|
}
|
|
67
15
|
|
|
68
|
-
/**
|
|
69
|
-
* @param envPort parsed PORT env, or undefined when unset/unparseable
|
|
70
|
-
* @param declaredPort `service_routing.local_port`, or undefined when the
|
|
71
|
-
* service has no routing row (the Railway-only case)
|
|
72
|
-
* @param isRailway true when booting inside Railway (caller derives from
|
|
73
|
-
* `RAILWAY_ENVIRONMENT`); there the injected PORT is authoritative
|
|
74
|
-
* regardless of the row, because Railway healthchecks it
|
|
75
|
-
*/
|
|
76
16
|
export function resolveServicePort(
|
|
77
17
|
envPort: number | undefined,
|
|
78
18
|
declaredPort: number | undefined,
|
|
@@ -1,32 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* The ONE theme-mode decider for every page render, across every UI app
|
|
7
|
-
* (portal, proposals, sign, forms, quiz, gpt, public). Every route handler in service-boot
|
|
8
|
-
* resolves light/dark through this single function — never its own inline
|
|
9
|
-
* `(... === "dark" ? "dark" : "light")` chain. That inline form was duplicated
|
|
10
|
-
* across every route branch, so flipping one app's theme required editing
|
|
11
|
-
* every copy (the heavy lifting this removes).
|
|
12
|
-
*
|
|
13
|
-
* Resolution order (first match wins):
|
|
14
|
-
* 1. ?theme=light|dark on the URL — the embed override. Set when a page is
|
|
15
|
-
* framed inside another shell (a form in a GPT tool panel) so the framed
|
|
16
|
-
* page matches the host's scheme. Also marks the render `embedded` so the
|
|
17
|
-
* page can drop its own brand page-surface and render the host's clean
|
|
18
|
-
* surface.
|
|
19
|
-
* 2. candidates[] — page rootProps.theme, brand theme, website default, in
|
|
20
|
-
* whatever priority the caller passes them.
|
|
21
|
-
* 3. "light".
|
|
22
|
-
*
|
|
23
|
-
* Pairs with the brand/token decider (resolveBrand → computeThemeVars,
|
|
24
|
-
* `unified-brand-theme`): that owns the SURFACE, this owns the MODE.
|
|
25
4
|
*/
|
|
26
5
|
|
|
27
6
|
export interface ResolvedThemeMode {
|
|
28
7
|
theme: "light" | "dark";
|
|
29
|
-
/** True iff the mode came from the ?theme= embed override. */
|
|
30
8
|
embedded: boolean;
|
|
31
9
|
}
|
|
32
10
|
|
|
@@ -35,15 +13,8 @@ function asMode(value: unknown): "light" | "dark" | undefined {
|
|
|
35
13
|
}
|
|
36
14
|
|
|
37
15
|
export function resolveThemeMode(opts: {
|
|
38
|
-
/** Request URL (or its href) — read for the ?theme= embed override. */
|
|
39
16
|
url?: URL | string | null;
|
|
40
|
-
/**
|
|
41
|
-
* Explicit embed override, when the caller already extracted ?theme= from
|
|
42
|
-
* the URL upstream (e.g. a pure data-shaper that has no Request). Same
|
|
43
|
-
* effect and priority as the URL form.
|
|
44
|
-
*/
|
|
45
17
|
override?: unknown;
|
|
46
|
-
/** Theme candidates in priority order: rootTheme, brandTheme, websiteTheme. */
|
|
47
18
|
candidates?: Array<unknown>;
|
|
48
19
|
}): ResolvedThemeMode {
|
|
49
20
|
let override = asMode(opts.override);
|
|
@@ -1,27 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* app-header.ts — shared application header resolver_fn.
|
|
7
|
-
*
|
|
8
|
-
* Shapes an app's per-row resolver_input into the canonical header-data object
|
|
9
|
-
* the ONE shared header Puck page (`/shared-app-header`) binds through the
|
|
10
|
-
* unified page-data surface (`dataSource:"page-data"` + `fieldPath:"headerData.<key>"`).
|
|
11
|
-
* The loader row owns the mapping (title field path, label/value pairs — the
|
|
12
|
-
* per-app variation); this executor owns the shape and the two coercions the
|
|
13
|
-
* rows cannot express:
|
|
14
|
-
*
|
|
15
|
-
* - `Date` values (Prisma returns Date objects) → ISO strings, so
|
|
16
|
-
* __PAGE_DATA__ serialises them the same way the client reads them
|
|
17
|
-
* (server-serializes-client-reads-verbatim).
|
|
18
|
-
* - the literal `"today"` → a noon-UTC ISO instant of the AEST calendar date.
|
|
19
|
-
* Noon keeps the calendar date stable under every timezone offset, so the
|
|
20
|
-
* header shows the same day on the server and on any visitor's client —
|
|
21
|
-
* the deterministic replacement for the old `format:"today"` clock block.
|
|
22
4
|
*/
|
|
23
5
|
|
|
24
|
-
/** AEST calendar date (the operator timezone) as `YYYY-MM-DD`. */
|
|
25
6
|
function aestCalendarDate(now: Date): string {
|
|
26
7
|
return new Intl.DateTimeFormat("en-CA", {
|
|
27
8
|
timeZone: "Australia/Sydney",
|
|
@@ -1,21 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* fetch-org-brand.ts — org brand resolver with page-org-first fallback.
|
|
7
|
-
*
|
|
8
|
-
* Wraps @teamscala/resolver-engine's fetchOrgBrand with the page-org-first
|
|
9
|
-
* fallback (portal-branding-resolves-page-org-first): if the page (center) org
|
|
10
|
-
* carried no brand row, fall back to the website owning-org so pages with no
|
|
11
|
-
* tenant org render the website's brand, not a blank. Extracted from the
|
|
12
|
-
* hardcoded `fetchOrgBrand` arm in resolve-from-loader-config.ts.
|
|
13
4
|
*/
|
|
14
5
|
import { fetchOrgBrand } from "@teamscala/resolver-engine/data-resolvers/fetch-org-brand";
|
|
15
6
|
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
16
7
|
import type { ResolverArgs } from "./portal-route-paths.ts";
|
|
17
8
|
|
|
18
|
-
/** A null brand renders a page with no logo, colours or name — so each failure names which of the three it was. */
|
|
19
9
|
export async function fetchOrgBrandWithFallback({ input, client }: ResolverArgs): Promise<unknown> {
|
|
20
10
|
const orgId = input.orgId as string | undefined;
|
|
21
11
|
const fallbackOrgId = input.fallbackOrgId as string | undefined;
|
|
@@ -1,34 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* portal-route-paths.ts — pure path mapper for portal region routing.
|
|
7
|
-
*
|
|
8
|
-
* Extracted from resolve-from-loader-config.ts (the hardcoded
|
|
9
|
-
* `resolvePortalRoutePaths` arm) so it can be invoked via a resolver_fn
|
|
10
|
-
* registry row instead of an if-chain. Faithful extraction — no logic change.
|
|
11
|
-
* The request path is the center page's builda path ("/" falls back to
|
|
12
|
-
* home_path), and the per-route toolbar page follows the flat naming convention
|
|
13
|
-
* `/<last-segment>-toolbar`. Pages without a matching toolbar page simply
|
|
14
|
-
* resolve an empty toolbar region (the source is declared optional).
|
|
15
|
-
*/
|
|
16
|
-
/**
|
|
17
|
-
* Extract the organisation id from a portal-shaped callback/path — the single
|
|
18
|
-
* authority for "which segment is the org" so the sign-in brand resolver and the
|
|
19
|
-
* route resolver cannot drift on the answer (portal-branding-resolves-page-org-first).
|
|
20
|
-
*
|
|
21
|
-
* The canonical portal shape is `/<websiteId>/<orgId>/<path>` — the org is
|
|
22
|
-
* segment 1, NOT segment 0. Reading segment 0 fed the WEBSITE id into brand
|
|
23
|
-
* resolution, which resolved no org and fell back to the super-admin org — the
|
|
24
|
-
* exact cross-tenant mis-branding this exists to prevent (bind-page-handler.ts,
|
|
25
|
-
* found 2026-07-21).
|
|
26
|
-
*
|
|
27
|
-
* Single-UUID hosts keep segment 0 unchanged: scala-projects routes `/<orgId>/…`
|
|
28
|
-
* and proposal/form/quiz hosts route `/<resourceId>/…` (a non-org first segment
|
|
29
|
-
* that resolves to no brand and falls back as before — no regression, only the
|
|
30
|
-
* correct-org improvement for the two-UUID portal shape). Returns undefined when
|
|
31
|
-
* neither leading segment is a UUID.
|
|
32
4
|
*/
|
|
33
5
|
|
|
34
6
|
export interface ResolverArgs {
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* website-by-host-map.ts — resolves the website + body path for a host.
|
|
7
|
-
*
|
|
8
|
-
* Extracted from resolve-from-loader-config.ts (the hardcoded
|
|
9
|
-
* `resolveWebsiteByHostMap` arm). Faithful extraction — no logic change.
|
|
10
4
|
*/
|
|
11
5
|
import type { ResolverArgs } from "./portal-route-paths.ts";
|
|
12
6
|
|