@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,30 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* One cohesive unit — the branded resource-not-found / holding page FLOOR:
|
|
7
|
-
* the PrimaryResourceMissingError trigger and the HTML builder (the builder
|
|
8
|
-
* consumes a logoUrl argument; the org-logo resolvers live in
|
|
9
|
-
* error-page-logo.ts).
|
|
10
|
-
*
|
|
11
|
-
* Branded 404 for registry page routes whose PRIMARY resource is missing.
|
|
12
|
-
*
|
|
13
|
-
* A route that declares `resource_type` (proposal, form, quiz, agreement…)
|
|
14
|
-
* is meaningless without that entity. Before this existed, a dead link
|
|
15
|
-
* (wrong id, deleted row) fell through the loader with nulls and rendered
|
|
16
|
-
* whatever default content the shell could scrape together — leaking
|
|
17
|
-
* internal pages (the "Vision Toolbar" incident, operator-observed
|
|
18
|
-
* 2026-06-05) on the org's own branded host, with a 200 status that
|
|
19
|
-
* search engines and link unfurlers would happily cache.
|
|
20
|
-
*
|
|
21
|
-
* The replacement: loadData throws PrimaryResourceMissingError; the page
|
|
22
|
-
* binder maps it to a minimal branded 404 — correct status, noindex, the
|
|
23
|
-
* host's own favicon (which resolves the owning org per brand_org), and a
|
|
24
|
-
* plain human sentence. Deliberately NOT a puck page: the error surface
|
|
25
|
-
* must render even when data loading is the thing that failed. A
|
|
26
|
-
* website-editable /404 puck page can layer on top later; this is the
|
|
27
|
-
* floor that must always exist.
|
|
28
4
|
*/
|
|
29
5
|
|
|
30
6
|
import { brandLogoImgTag } from "@teamscala/os/brand/brand-logo-img-tag";
|
|
@@ -39,16 +15,7 @@ const RESOURCE_LABELS: Record<string, string> = {
|
|
|
39
15
|
|
|
40
16
|
export class PrimaryResourceMissingError extends Error {
|
|
41
17
|
readonly resourceType: string;
|
|
42
|
-
// HTTP status for the holding page. 404 for a genuinely missing/invalid
|
|
43
|
-
// resource (a dead /:id link); 200 for the bare root of a UUID-driven site
|
|
44
|
-
// ("no <resource> selected") — the root is a valid landing, not an error,
|
|
45
|
-
// and kind:ui services must answer GET / with 200 (verify-prod-persistent-slot).
|
|
46
18
|
readonly httpStatus: number;
|
|
47
|
-
// A page ROW exists but has zero content blocks — a content gap, not a dead
|
|
48
|
-
// link (operator ruling 2026-07-21: "if the page is empty, it should have a
|
|
49
|
-
// Puck block that says this page is empty... and then take me home"). The
|
|
50
|
-
// floor renders the "This page is empty" surface (200) instead of the dead-
|
|
51
|
-
// link "doesn't exist" surface (404). Distinct from a dead link (no row).
|
|
52
19
|
readonly empty: boolean;
|
|
53
20
|
constructor(resourceType: string, httpStatus = 404, opts?: { empty?: boolean }) {
|
|
54
21
|
super(`primary resource "${resourceType}" not found`);
|
|
@@ -63,23 +30,10 @@ export function buildResourceNotFoundResponse(
|
|
|
63
30
|
resourceType: string,
|
|
64
31
|
logoUrl?: string | null,
|
|
65
32
|
status = 404,
|
|
66
|
-
// Optional 4th arg keeps the existing 3-arg call sites working while letting
|
|
67
|
-
// the page binder add a "Take me home" action (operator-requested) and the
|
|
68
|
-
// empty-page variant.
|
|
69
33
|
opts?: { homeUrl?: string; empty?: boolean },
|
|
70
34
|
): Response {
|
|
71
35
|
const label = RESOURCE_LABELS[resourceType] ?? "page";
|
|
72
|
-
// Single-owner hosts show the owner's wordmark; multi-tenant hosts
|
|
73
|
-
// (no brand_org — a dead id's org is unknowable) stay platform-neutral.
|
|
74
|
-
// The helper emits the inline data-URI src with reserved width+height
|
|
75
|
-
// (logos-inline-on-branded-server-pages).
|
|
76
36
|
const logoImg = brandLogoImgTag(logoUrl, { height: 40, width: 160 });
|
|
77
|
-
// Three surfaces share this one floor (resource-not-found-is-the-root-floor):
|
|
78
|
-
// 200 root — the BARE ROOT of a UUID-driven host ("no <resource> selected").
|
|
79
|
-
// 404 dead — a content path with NO page row (a deleted/moved page, e.g.
|
|
80
|
-
// /roadmap) OR a dead /:id: a link that genuinely doesn't resolve.
|
|
81
|
-
// 200 empty — a page ROW exists but has zero blocks (a content gap, not a
|
|
82
|
-
// dead link): "This page is empty", still take-me-home.
|
|
83
37
|
const isEmpty = opts?.empty === true;
|
|
84
38
|
const isRoot = status === 200 && !isEmpty;
|
|
85
39
|
const title = isEmpty ? "Nothing here yet" : isRoot ? `No ${label} here` : "Not found";
|
|
@@ -95,8 +49,6 @@ export function buildResourceNotFoundResponse(
|
|
|
95
49
|
sent to view it — there’s nothing to see at the bare address.`
|
|
96
50
|
: `The page you’re looking for may have been moved or removed, or the
|
|
97
51
|
link may be incomplete.`;
|
|
98
|
-
// "Take me home" — operator-requested action on every not-found/empty surface.
|
|
99
|
-
// Omitted on the bare-root landing (it IS home). Defaults to the host root.
|
|
100
52
|
const homeUrl = opts?.homeUrl ?? "/";
|
|
101
53
|
const homeLink = isRoot
|
|
102
54
|
? ""
|
|
@@ -1,18 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Runs the service's boot hooks. A server_package registers a hook via
|
|
7
|
-
* ctx.registerBootHook during the mount phase; a hook registered by an
|
|
8
|
-
* INSTALLED server_package is intended to run (the package is installed
|
|
9
|
-
* because the service wants it). This therefore invokes EVERY registered
|
|
10
|
-
* hook — those declared in `service_install.boot_hooks[]` first (preserving
|
|
11
|
-
* their declared order), then any registered-but-undeclared hooks. Requiring a
|
|
12
|
-
* hook to be BOTH registered by its package AND re-declared in boot_hooks is a
|
|
13
|
-
* double-declaration footgun — it left the realtime-utils pg-notify bridge
|
|
14
|
-
* registered-but-never-invoked (boot_hooks was null), so the live-refresh
|
|
15
|
-
* relay never started (2026-06-29).
|
|
16
4
|
*/
|
|
17
5
|
|
|
18
6
|
import type { ServiceInstallConfig } from "./load-service-install-row";
|
package/src/run-boot-hooks.ts
CHANGED
|
@@ -1,24 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Runs service_install.boot_hooks in declaration order. A boot hook is a
|
|
7
|
-
* registry-declared, one-shot startup function that needs to execute after
|
|
8
|
-
* prisma + registry are ready but before the HTTP server starts serving.
|
|
9
|
-
* Used for custom startup logic (stale-run reconciliation, cache hydration,
|
|
10
|
-
* ALS seeding, long-running watcher start) that doesn't fit the pg-boss
|
|
11
|
-
* recurring model and is too specific to be a generic installer step.
|
|
12
|
-
*
|
|
13
|
-
* Each hook is resolved by dynamic-importing its handler_slug (or slug when
|
|
14
|
-
* handler_slug is absent) and awaiting its default export. The export must
|
|
15
|
-
* be an async function accepting a BootHookContext. Hooks are invoked in
|
|
16
|
-
* ascending order of their `order` field (default 100).
|
|
17
|
-
*
|
|
18
|
-
* Failures are fatal. The installer has no partial-boot concept: if a
|
|
19
|
-
* declared hook cannot run, the service is broken, and booting anyway would
|
|
20
|
-
* mask the regression. BootFatalError is thrown to propagate to the
|
|
21
|
-
* top-level boot-error handler.
|
|
22
4
|
*/
|
|
23
5
|
|
|
24
6
|
import type { ServiceInstallConfig } from "./load-service-install-row";
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Polls the installed scala-tools package against GitHub Packages and repairs
|
|
7
|
-
* stale daemon tooling by reinstalling and letting PM2 restart the process.
|
|
8
|
-
* The fetch routes through `getGithubPackagesLatestVersion` so the URL
|
|
9
|
-
* encoding and GitHub-Packages auth header live in the typed primitive.
|
|
10
4
|
*/
|
|
11
5
|
|
|
12
6
|
import { recordEvent } from "@teamscala/event-log/record-event";
|
package/src/security-headers.ts
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Middleware that injects security headers into every response.
|
|
7
|
-
* Uses Hono's secureHeaders as the base for standard headers (X-Content-Type-Options,
|
|
8
|
-
* X-Frame-Options, Referrer-Policy, etc.), then layers registry-driven CSP with
|
|
9
|
-
* nonce injection on top.
|
|
10
4
|
*/
|
|
11
5
|
|
|
12
6
|
import { getConfigObject } from "@teamscala/db/registry/config";
|
|
@@ -1,37 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Selecting and ordering the `page_route` rows one service serves.
|
|
7
|
-
*
|
|
8
|
-
* Two decisions live here, both about WHICH rows and IN WHAT ORDER — never
|
|
9
|
-
* about what a route does once matched:
|
|
10
|
-
*
|
|
11
|
-
* 1. SELECTION joins on the CAPABILITY, never on the deployable name. A
|
|
12
|
-
* service arrives under its LANE identity (`scala-projects-bun-dev` in
|
|
13
|
-
* dev, `scala-projects` in prod), while a page_route row names the
|
|
14
|
-
* APPLICATION and there is ONE row for both lanes. Raw string equality
|
|
15
|
-
* therefore matched at most one lane, silently — see `./capability-slug.ts`
|
|
16
|
-
* for the measured incident.
|
|
17
|
-
*
|
|
18
|
-
* 2. ORDERING puts specific patterns before parameterised and wildcard ones,
|
|
19
|
-
* because Hono matches in REGISTRATION order.
|
|
20
4
|
*/
|
|
21
5
|
|
|
22
6
|
import { toCapabilitySlug } from "./capability-slug.ts";
|
|
23
7
|
|
|
24
|
-
/** The shape this module needs from a loaded page_route row. */
|
|
25
8
|
export interface SelectableRoute {
|
|
26
9
|
config: { pattern: string; service: string };
|
|
27
10
|
}
|
|
28
11
|
|
|
29
|
-
/**
|
|
30
|
-
* Specificity rank for registration order. Hono matches by registration
|
|
31
|
-
* order, so `/*` (the portal catchall) registered before `/sign-in` would
|
|
32
|
-
* swallow the sign-in page and render it inside the PortalTemplate instead
|
|
33
|
-
* of the standalone PublicTemplate shell.
|
|
34
|
-
*/
|
|
35
12
|
function comparePatternSpecificity(a: string, b: string): number {
|
|
36
13
|
// Wildcard `/*` goes last.
|
|
37
14
|
if (a === "/*") return 1;
|
|
@@ -44,13 +21,6 @@ function comparePatternSpecificity(a: string, b: string): number {
|
|
|
44
21
|
return 0;
|
|
45
22
|
}
|
|
46
23
|
|
|
47
|
-
/**
|
|
48
|
-
* The page_route rows this service serves, in registration order.
|
|
49
|
-
*
|
|
50
|
-
* `serviceSlug` is the deployable identity of the calling lane; both sides
|
|
51
|
-
* of the join normalize through `toCapabilitySlug`, so a row may name either
|
|
52
|
-
* the capability or a deployable form and both lanes resolve the same set.
|
|
53
|
-
*/
|
|
54
24
|
export function selectServiceRoutes<T extends SelectableRoute>(
|
|
55
25
|
allRoutes: readonly T[],
|
|
56
26
|
serviceSlug: string,
|
package/src/server-error-page.ts
CHANGED
|
@@ -1,24 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* The branded HTML error page for a FAILED PAGE RENDER — the page-server
|
|
7
|
-
* render catch's response surface (constitution `error-responses-via-error-
|
|
8
|
-
* report`). When SSR resolve+render fails (render budget exhausted, DEGRADED
|
|
9
|
-
* protective layer, a thrown loader), the browser is mid-navigation: serving
|
|
10
|
-
* a JSON body there is the violation this closes (2026-07-24: the operator
|
|
11
|
-
* saw a raw `{"error":"Request failed"}` blob where the dashboard should be).
|
|
12
|
-
*
|
|
13
|
-
* Classification stays in `buildErrorReport` (`@teamscala/os`) — the ONE
|
|
14
|
-
* classifier; this file is ONLY the HTML shaper for the page-navigation
|
|
15
|
-
* surface (API/ORPC `onError` keeps the JSON body, consumed by code). It
|
|
16
|
-
* renders the report's already-decided status + Retry-After + message, so the
|
|
17
|
-
* screen, the log line, and the GlitchTip event all share one `errorId`.
|
|
18
|
-
*
|
|
19
|
-
* Deliberately NOT a Puck page: it must render even when data loading is what
|
|
20
|
-
* failed. Sibling of `resource-not-found.ts` (the missing-resource floor) —
|
|
21
|
-
* same dark branded template + org-logo resolver, distinct trigger.
|
|
22
4
|
*/
|
|
23
5
|
|
|
24
6
|
import { escapeHtml } from "@teamscala/formatters/escapeHtml";
|
package/src/theme-fonts-link.ts
CHANGED
|
@@ -1,21 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Derives the document head's Google Fonts link from the `css_theme` row's
|
|
7
|
-
* `fonts` map — the ONE source for the families a page's variants reference
|
|
8
|
-
* (`the-theme-decides-colour-not-the-component` extends to type). The old
|
|
9
|
-
* envelope hard-coded Inter, so a proposal heading naming a manuscript serif
|
|
10
|
-
* ("Playfair Display") never had its family loaded and fell back to Times
|
|
11
|
-
* (2026-09-02). Now every family the row declares is loaded; a family a
|
|
12
|
-
* variant names that the row does not declare is a row defect, caught at the
|
|
13
|
-
* row's schema boundary.
|
|
14
|
-
*
|
|
15
|
-
* Read from the boot registry cache (captured at service boot, the same
|
|
16
|
-
* semantics as the resource_loader capture) and guarded: a missing or
|
|
17
|
-
* unreadable row degrades to NO fonts link — system fonts render, the page
|
|
18
|
-
* still works. This module must never throw into the document builder.
|
|
19
4
|
*/
|
|
20
5
|
|
|
21
6
|
import { getEntryConfig } from "@teamscala/db/registry/accessors";
|
|
@@ -24,15 +9,11 @@ interface ThemeFontsConfig {
|
|
|
24
9
|
fonts?: Record<string, string>;
|
|
25
10
|
}
|
|
26
11
|
|
|
27
|
-
/** The primary family name of a CSS font stack: "Inter, sans-serif" →
|
|
28
|
-
* "Inter"; "Playfair Display" → "Playfair Display". */
|
|
29
12
|
export function primaryFontFamily(stack: string): string {
|
|
30
13
|
const first = stack.split(",")[0] ?? "";
|
|
31
14
|
return first.trim().replace(/^['"]|['"]$/g, "");
|
|
32
15
|
}
|
|
33
16
|
|
|
34
|
-
/** The declared theme families, deduplicated by primary family, in row
|
|
35
|
-
* order (body, heading, manuscript, mono…). Empty when the row is absent. */
|
|
36
17
|
export function themeFontFamilies(): string[] {
|
|
37
18
|
try {
|
|
38
19
|
const config = getEntryConfig<ThemeFontsConfig>("css_theme", "default");
|
|
@@ -52,7 +33,6 @@ export function themeFontFamilies(): string[] {
|
|
|
52
33
|
}
|
|
53
34
|
}
|
|
54
35
|
|
|
55
|
-
/** The <link> tag loading every declared theme family, or "" when none. */
|
|
56
36
|
export function themeFontsLink(): string {
|
|
57
37
|
const families = themeFontFamilies();
|
|
58
38
|
if (families.length === 0) return "";
|
|
@@ -1,32 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @system service-boot
|
|
3
|
-
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* The fn.transfers.presignedUpload procedure body (api_route
|
|
7
|
-
* `fn-transfers-presigned-upload`, authed: input + context). It replaces the
|
|
8
|
-
* FormData POST /api/transfers/upload mount for every UI caller: the browser
|
|
9
|
-
* asks for a SIGNED PUT URL for one key, uploads the bytes straight to the
|
|
10
|
-
* store, and persists the SERVABLE key — so the file never crosses the
|
|
11
|
-
* service and the call is one JSON request instead of a multipart body.
|
|
12
|
-
*
|
|
13
|
-
* The policy the mount carried lives here, and is the SAME policy: the
|
|
14
|
-
* MAX_FILE_SIZE / ALLOWED_TYPES refusals (with their 413/415 statuses intact
|
|
15
|
-
* through the ORPC codes below), the USER-scoped key scheme
|
|
16
|
-
* (`users/<userId>/<dir>/<dir-singular>-<ts>.<ext>`), the unknown-dir
|
|
17
|
-
* fallback to `uploads`, and the servable value returned as `url` with
|
|
18
|
-
* `thumbUrl` mirroring it (the stored value is the servable value —
|
|
19
|
-
* single-logo-pathway, so display code reads the column with no rewriting).
|
|
20
|
-
* The key is derived from `context.user.id`, never from input: a caller
|
|
21
|
-
* cannot mint a URL under another user's prefix.
|
|
22
|
-
*
|
|
23
|
-
* HONEST RESIDUAL: the size refusal reads the DECLARED fileSize, because the
|
|
24
|
-
* bytes no longer pass through here — the store enforces the signature's type
|
|
25
|
-
* and expiry, not a byte count. The mount's Content-Length check was equally
|
|
26
|
-
* a declared-value check (an attacker sent the bytes either way); what
|
|
27
|
-
* changes is that a lying client is now bounded by the URL's TTL instead of a
|
|
28
|
-
* boot-level body limit. A real byte bound belongs to the bucket, not to this
|
|
29
|
-
* handler.
|
|
3
|
+
* @status handwritten
|
|
30
4
|
*/
|
|
31
5
|
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
32
6
|
import { getPresignedUrl } from "@teamscala/object-storage/create/presigned-url";
|
|
@@ -1,60 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Phase 2 of the two-phase boot: the full app. Adds the expensive operations
|
|
7
|
-
* deferred from phase 1 (two-phase-minimal-app.ts) — client asset building
|
|
8
|
-
* (Vite build), pg-boss, the readiness/page-handler flip — and is swapped
|
|
9
|
-
* in atomically once complete.
|
|
10
4
|
*/
|
|
11
5
|
|
|
12
|
-
/**
|
|
13
|
-
* Build the full app with hydration. This adds the expensive operations that
|
|
14
|
-
* were deferred from the minimal app:
|
|
15
|
-
* - Client asset building (Vite build)
|
|
16
|
-
* - Boot hooks (may have expensive initialization)
|
|
17
|
-
* - Scheduled jobs (can start later)
|
|
18
|
-
* - pg-boss (can start later)
|
|
19
|
-
*
|
|
20
|
-
* This runs in the background after the minimal app is already serving.
|
|
21
|
-
* NOTE: All routes (including page routes) are already registered in Phase 1,
|
|
22
|
-
* so we don't need to modify the app here. The app reference stays the same.
|
|
23
|
-
*/
|
|
24
6
|
|
|
25
|
-
// Client asset building (expensive: Vite build).
|
|
26
|
-
//
|
|
27
|
-
// ISOLATED, because a PRESENTATION failure must not take out BACKGROUND WORK.
|
|
28
|
-
// This was awaited unguarded, so a throw here aborted buildFullApp before
|
|
29
|
-
// mountPgBoss ran, and the caller's catch logged
|
|
30
|
-
// "Failed to build full app, continuing with minimal app" — which reads as a
|
|
31
|
-
// UI-only degradation while having silently killed every pg-boss handler on
|
|
32
|
-
// the service.
|
|
33
|
-
//
|
|
34
|
-
// Measured 2026-07-30: a stale @teamscala/portal-ui artifact still importing
|
|
35
|
-
// the retired @teamgbg scope threw ENOENT on a missing theme CSS file in
|
|
36
|
-
// scala-agents. Zero "scheduledJobs DONE" lines appeared for ~20 minutes
|
|
37
|
-
// across repeated boots, the pane-outbox drain never mounted, and the
|
|
38
|
-
// operator's WhatsApp assistant went silent — because of a stylesheet. The
|
|
39
|
-
// service stayed `online` and healthy throughout, so nothing surfaced it.
|
|
40
|
-
//
|
|
41
|
-
// The steps after this one do not depend on client assets: pg-boss needs the
|
|
42
|
-
// registry and the DB, not a bundle. Ordering it behind a Vite build was
|
|
43
|
-
// incidental, so the coupling was never intended.
|
|
44
7
|
|
|
45
|
-
// pg-boss (mount for UI services - wasn't mounted in minimal app)
|
|
46
|
-
//
|
|
47
|
-
// Wrapped for the SAME reason buildClientAssets above is, in the other
|
|
48
|
-
// direction: a background-work failure must not cost the page. Unwrapped,
|
|
49
|
-
// anything mountPgBoss throws propagates out of the full-app build, the
|
|
50
|
-
// caller falls back to the MINIMAL app, and the service serves 0 api routes
|
|
51
|
-
// with /health 503 — so the tunnel health check fails and the site is
|
|
52
|
-
// unreachable from outside while localhost answers in 0.1s.
|
|
53
|
-
//
|
|
54
|
-
// Measured 2026-08-31 on scala-projects: a deps refresh moved pg-boss to a
|
|
55
|
-
// build expecting a newer shared pgboss schema than the database carried, its
|
|
56
|
-
// migration threw, and an outage of every page followed from a scheduled-job
|
|
57
|
-
// dependency. Jobs are the degradable half; pages are not.
|
|
58
8
|
|
|
59
9
|
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
60
10
|
import type { Hono } from "hono";
|
|
@@ -84,9 +34,6 @@ export async function buildFullApp(
|
|
|
84
34
|
await buildClientAssets(serviceSlug);
|
|
85
35
|
log.info(`[two-phase-boot:${serviceSlug}] phase-2 step: buildClientAssets DONE`);
|
|
86
36
|
} catch (error) {
|
|
87
|
-
// Loud but non-fatal: the tombstone manifest written by buildClientAssets
|
|
88
|
-
// already makes SSR serve the prior bundle, so degrading here costs the
|
|
89
|
-
// new bundle, never the background work.
|
|
90
37
|
log.error(
|
|
91
38
|
`[two-phase-boot:${serviceSlug}] phase-2 step: buildClientAssets FAILED — continuing to scheduled jobs and pg-boss (a client-asset failure must not stop background work)`,
|
|
92
39
|
{ error: error instanceof Error ? error.message : String(error) },
|
|
@@ -113,23 +60,6 @@ export async function buildFullApp(
|
|
|
113
60
|
);
|
|
114
61
|
}
|
|
115
62
|
|
|
116
|
-
// The readiness flip is gated on a VERIFIED client build, never on "phase 2
|
|
117
|
-
// got this far". buildClientAssets's failure above is deliberately non-fatal
|
|
118
|
-
// for background work — but a deployment whose client build never ran (or
|
|
119
|
-
// whose tombstone names unbuilt files) must not flip ready and serve pages:
|
|
120
|
-
// every served asset URL would 404 while /health and the deploy report ok.
|
|
121
|
-
// Measured 2026-09-01: proposals.scala.business served a client-facing
|
|
122
|
-
// proposal unstyled while Railway reported SUCCESS, because the phase-1
|
|
123
|
-
// catch let boot continue past the build and the flip never checked the
|
|
124
|
-
// build's outcome (`a-component-may-not-report-a-state-it-has-not-verified`).
|
|
125
|
-
// The throw lands in ui-service's boot.client-build-failed catch, which
|
|
126
|
-
// keeps the minimal app: pages and /health stay 503 (cleanly unavailable)
|
|
127
|
-
// while the pg-boss jobs mounted above KEEP RUNNING. On Railway the
|
|
128
|
-
// healthcheck (healthcheckPath: /health) then fails and the previous good
|
|
129
|
-
// deployment keeps traffic — the deploy reports FAILED, never SUCCESS.
|
|
130
|
-
// readiness-gated-boot: a service is "fully-ready or cleanly-unavailable";
|
|
131
|
-
// a UI service that cannot serve its bundle is not ready
|
|
132
|
-
// (`liveness-by-capability-not-process-existence`).
|
|
133
63
|
const clientBuild = await verifyClientBuildAssets();
|
|
134
64
|
if (!clientBuild.ok) {
|
|
135
65
|
throw new Error(
|
|
@@ -145,20 +75,12 @@ export async function buildFullApp(
|
|
|
145
75
|
`${Object.keys(components).length} component(s)`,
|
|
146
76
|
);
|
|
147
77
|
|
|
148
|
-
// Set the real page handler so the dynamic handler in Phase 1 can delegate to it.
|
|
149
|
-
// Client-only services (render_mode=client) have no SSR page handler — skip
|
|
150
|
-
// (they serve via the client bundle, never SSR). Guarded so undefined is a
|
|
151
|
-
// no-op rather than a crash.
|
|
152
78
|
if (deps.createPageHandler) {
|
|
153
79
|
setPageHandler(deps.createPageHandler);
|
|
154
80
|
log.info(
|
|
155
81
|
`[two-phase-boot:${serviceSlug}] Real page handler registered - page routes now delegate to full app`,
|
|
156
82
|
);
|
|
157
83
|
} else {
|
|
158
|
-
// The full app IS built — this service simply never renders server-side,
|
|
159
|
-
// so readiness is flipped directly rather than as a side effect of
|
|
160
|
-
// registering an SSR handler that will never exist. Without this a
|
|
161
|
-
// client-only service is permanently un-ready (see markFullAppReady).
|
|
162
84
|
markFullAppReady();
|
|
163
85
|
log.info(
|
|
164
86
|
`[two-phase-boot:${serviceSlug}] Client-only mode (render_mode=client) — no SSR page handler registered`,
|
|
@@ -178,10 +100,6 @@ export async function buildFullApp(
|
|
|
178
100
|
};
|
|
179
101
|
}
|
|
180
102
|
|
|
181
|
-
/**
|
|
182
|
-
* Swap the minimal app for the full app. This atomically replaces the
|
|
183
|
-
* app reference that Bun.serve is using.
|
|
184
|
-
*/
|
|
185
103
|
export function swapToFullApp(fullApp: Hono, serviceSlug: string): void {
|
|
186
104
|
const oldLabel = swapApp(fullApp, "full");
|
|
187
105
|
const log = getAppLogger();
|
|
@@ -1,47 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Phase 1 of the two-phase boot: the minimal tool-only app (ORPC, data
|
|
7
|
-
* routes, auth gates) that serves immediately (<10s); phase 2
|
|
8
|
-
* (two-phase-full-app.ts) builds the full app in the background and swaps
|
|
9
|
-
* it in atomically — eliminating the ~25s boot outage window.
|
|
10
4
|
*/
|
|
11
5
|
|
|
12
|
-
/**
|
|
13
|
-
* Build the minimal tool-only app. This excludes the expensive operations:
|
|
14
|
-
* - Client asset building (Vite build)
|
|
15
|
-
* - Boot hooks (may have expensive initialization)
|
|
16
|
-
* - Scheduled jobs (can start later)
|
|
17
|
-
* - pg-boss (can start later)
|
|
18
|
-
*
|
|
19
|
-
* CRITICAL: All route registration MUST happen here before serving, because
|
|
20
|
-
* Hono's RegExpRouter compiles on the first request and becomes immutable.
|
|
21
|
-
* Once compiled, no new routes can be added. So we include:
|
|
22
|
-
* - Block discovery and page routes (registers all page paths)
|
|
23
|
-
* - All other routes (API, WS, data, static assets)
|
|
24
|
-
*
|
|
25
|
-
* The minimal app includes everything needed for MCP tools and ORPC:
|
|
26
|
-
* - Error handling, readiness gate, health check
|
|
27
|
-
* - Security headers, CORS, compression, timing
|
|
28
|
-
* - Content gate access, auth gates, static assets
|
|
29
|
-
* - ORPC router, API routes, WS routes, data routes, page routes
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
// Fire independent I/O + compilation NOW, overlapping with the gate chain
|
|
33
|
-
// below. These three touch NO app route and have no ordering dependency on
|
|
34
|
-
// the gate/mount sequence, so they run concurrently with it:
|
|
35
|
-
// - preloadServerPackages: warms Bun's module cache (CPU-bound compilation
|
|
36
|
-
// that overlaps the gate chain's cross-internet DB I/O — same speculative
|
|
37
|
-
// pattern as create-boot-pipeline's preloads). mountServerPackages later
|
|
38
|
-
// hits the cache (instant imports), paying only the ~33ms of mount work.
|
|
39
|
-
// - initGateTtls: loads config rows into module-level vars. The TTL getters
|
|
40
|
-
// are read at REQUEST time (inside route-handler closures), not at boot,
|
|
41
|
-
// so this only needs to finish before serving — safe to run in parallel.
|
|
42
|
-
// - discoverBlocks: loads the page-manifest (no app mutation, no gate
|
|
43
|
-
// dependency). Its result is consumed by registerPageRoutes much later.
|
|
44
|
-
|
|
45
6
|
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
46
7
|
import { BootFatalError } from "@teamscala/os/errors/boot-error";
|
|
47
8
|
import { createTimer } from "@teamscala/timing/create-timer";
|
|
@@ -61,7 +22,7 @@ import { attachPortalMembers } from "./attach-portal-members.ts";
|
|
|
61
22
|
import { attachTransfersUpload } from "./attach-transfers-upload.ts";
|
|
62
23
|
import { attachAuthGate } from "./auth-gate.ts";
|
|
63
24
|
import { csrfProtection } from "./csrf-protection.ts";
|
|
64
|
-
import { attachRequestLogger } from "
|
|
25
|
+
import { attachRequestLogger } from "@teamscala/logger/install-logging";
|
|
65
26
|
import type { ServiceInstallConfig } from "./load-service-install-row.ts";
|
|
66
27
|
import { mountOrpcRouter } from "./mount-orpc-router.ts";
|
|
67
28
|
import { mountServerPackages, preloadServerPackages } from "./mount-server-packages.ts";
|
|
@@ -107,13 +68,6 @@ export async function buildMinimalApp(
|
|
|
107
68
|
|
|
108
69
|
const app = new Hono();
|
|
109
70
|
|
|
110
|
-
// Per-step instrumentation inside the boot-ui-service phase. Same
|
|
111
|
-
// createTimer + wall-clock convention as create-boot-pipeline's runPhase
|
|
112
|
-
// (timing-is-the-only-timing, per-phase-instrumentation) — NOT a second
|
|
113
|
-
// telemetry surface. The boot-ui-service phase is 60% of boot; this
|
|
114
|
-
// breakdown localises the cost so optimisation targets the real hotspot
|
|
115
|
-
// instead of guessing. Spans flow to GlitchTip (timing's trace-emitter is
|
|
116
|
-
// wired by the time we reach here); the breakdown line is `pm2 logs`-visible.
|
|
117
71
|
const stepTimings: Record<string, number> = {};
|
|
118
72
|
async function runStep<T>(step: string, fn: () => Promise<T>): Promise<T> {
|
|
119
73
|
const timer = createTimer(`${serviceSlug}:boot.minimal.${step}`);
|
|
@@ -125,7 +79,6 @@ export async function buildMinimalApp(
|
|
|
125
79
|
}
|
|
126
80
|
}
|
|
127
81
|
|
|
128
|
-
// Error handlers
|
|
129
82
|
app.onError(createRequestErrorHandler("service-boot:minimal"));
|
|
130
83
|
|
|
131
84
|
app.notFound(async (c) => {
|
|
@@ -137,7 +90,6 @@ export async function buildMinimalApp(
|
|
|
137
90
|
return c.json({ error: "Not Found" }, 404);
|
|
138
91
|
});
|
|
139
92
|
|
|
140
|
-
// Core middleware
|
|
141
93
|
app.use("*", readinessGate);
|
|
142
94
|
app.get("/health", healthHandler);
|
|
143
95
|
app.use("*", injectSecurityHeaders);
|
|
@@ -157,19 +109,7 @@ export async function buildMinimalApp(
|
|
|
157
109
|
);
|
|
158
110
|
app.use("*", compress());
|
|
159
111
|
app.use("*", timing());
|
|
160
|
-
// CSRF (`security-csrf-protection`): state-changing form-element requests
|
|
161
|
-
// must carry a trusted Origin — derived set (managed base domain +
|
|
162
|
-
// builda_websites custom domains via isTrustedAuthOrigin), never an
|
|
163
|
-
// allowlist beside better-auth's. Registered after security headers, before
|
|
164
|
-
// every route + gate.
|
|
165
112
|
app.use("*", csrfProtection());
|
|
166
|
-
// GLOBAL body limit (`security-body-limit`): every request body is bounded
|
|
167
|
-
// BEFORE any handler buffers it — a per-route file.size check AFTER
|
|
168
|
-
// formData() has buffered is a report, not a limit. 30MB sits above the
|
|
169
|
-
// 25MB transfers-upload maximum; routes wanting a smaller bound mount their
|
|
170
|
-
// own bodyLimit BEFORE this one (Hono runs matching middleware in
|
|
171
|
-
// registration order, and /api/rpc registers its 10MB limit in
|
|
172
|
-
// mount-orpc-router).
|
|
173
113
|
app.use(
|
|
174
114
|
"*",
|
|
175
115
|
bodyLimit({
|
|
@@ -186,20 +126,8 @@ export async function buildMinimalApp(
|
|
|
186
126
|
);
|
|
187
127
|
const ttlPromise = runStep("init-gate-ttls", () => initGateTtls());
|
|
188
128
|
|
|
189
|
-
// Public access endpoints — MUST be registered before the auth gate (Hono
|
|
190
|
-
// dispatches middleware + handlers in registration order; the content-gate
|
|
191
|
-
// POST routes and magic-link endpoints are public and must be reachable
|
|
192
|
-
// without a session, so they precede the auth gate's app.use("*") wildcard).
|
|
193
|
-
// These are mutually independent — distinct paths, no shared mutable state
|
|
194
|
-
// beyond the Hono app's synchronous route array — so they parallelise.
|
|
195
129
|
await runStep("public-routes", () =>
|
|
196
130
|
Promise.all([
|
|
197
|
-
// EVERY REGISTERED GATE'S ACCESS ENDPOINT (content-gate/catalog.ts).
|
|
198
|
-
// The same catalog the SSR gate sequence iterates, so the endpoint that
|
|
199
|
-
// MINTS a gate's token and the gate that VERIFIES it cannot disagree
|
|
200
|
-
// about which gates exist — this list and that one were two copies of
|
|
201
|
-
// one fact, and a gate added to one and missed in the other is a page
|
|
202
|
-
// that gates content with no way to unlock it.
|
|
203
131
|
...CONTENT_GATES.map((entry) =>
|
|
204
132
|
attachContentGateAccess(app, serviceSlug, entry.access),
|
|
205
133
|
),
|
|
@@ -211,27 +139,14 @@ export async function buildMinimalApp(
|
|
|
211
139
|
]),
|
|
212
140
|
);
|
|
213
141
|
|
|
214
|
-
// Auth gate — registers /api/auth/* + the session-check wildcard middleware.
|
|
215
|
-
// MUST follow the public routes above (load-bearing Hono registration order)
|
|
216
|
-
// and MUST precede mountServerPackages: server-package daemon routes (e.g.
|
|
217
|
-
// cli-session's /rpc/*) are subject to the session gate, which requires the
|
|
218
|
-
// gate's wildcard registered first to apply. Running these concurrently would
|
|
219
|
-
// make that ordering non-deterministic — so the gate stays serial here.
|
|
220
142
|
await runStep("attach-auth-gate", () =>
|
|
221
143
|
attachAuthGate(app, config, serviceSlug),
|
|
222
144
|
);
|
|
223
145
|
registerStaticAssets(app, serviceSlug, config.brand_org);
|
|
224
146
|
|
|
225
|
-
// Server packages — the cold imports were preloaded above and are now in
|
|
226
|
-
// Bun's module cache, so this pays only the mount work (~33ms, not ~2.8s of
|
|
227
|
-
// serial compilation). Route registration happens HERE (after the auth gate),
|
|
228
|
-
// preserving the gate-vs-daemon-route ordering.
|
|
229
147
|
const mountRegistry = await runStep("mount-server-packages", () =>
|
|
230
148
|
mountServerPackages(app, config, serviceSlug, getSharedDb),
|
|
231
149
|
);
|
|
232
|
-
// Ensure the speculative preload finished before proceeding (it populates
|
|
233
|
-
// the same cache mountServerPackages just consumed; awaiting now is a no-op
|
|
234
|
-
// if the preload already settled, and makes the ordering guarantee explicit).
|
|
235
150
|
await preloadPromise;
|
|
236
151
|
const apiRouteCount = registerApiRoutes(app, config, mountRegistry, serviceSlug);
|
|
237
152
|
|
|
@@ -239,17 +154,10 @@ export async function buildMinimalApp(
|
|
|
239
154
|
await runStep("mount-orpc-router", () => mountOrpcRouter(app, serviceSlug));
|
|
240
155
|
}
|
|
241
156
|
|
|
242
|
-
// Register the in-process ORPC server client BEFORE discoverBlocks — it
|
|
243
|
-
// calls getServerClient() which needs the client set here. Must follow
|
|
244
|
-
// mount-orpc-router (which builds the router getAppRouter uses).
|
|
245
|
-
// Previously ran inside register-page-routes (too late — discoverBlocks
|
|
246
|
-
// fired before it, hitting the empty-router fallback).
|
|
247
157
|
await runStep("register-page-server-client", () =>
|
|
248
158
|
registerPageServerClient(serviceSlug),
|
|
249
159
|
);
|
|
250
160
|
|
|
251
|
-
// NOW load the generated manifest (getServerClient is set). Awaited below
|
|
252
|
-
// before register-page-routes consumes the result.
|
|
253
161
|
const discoverPromise = runStep("load-page-manifest", () =>
|
|
254
162
|
loadPageManifest(serviceSlug),
|
|
255
163
|
);
|
|
@@ -258,14 +166,8 @@ export async function buildMinimalApp(
|
|
|
258
166
|
registerWsRoutes(app, config, mountRegistry, serviceSlug, upgradeWebSocket),
|
|
259
167
|
);
|
|
260
168
|
|
|
261
|
-
// Resolve the deferred TTL load (started at the top — should be long done,
|
|
262
|
-
// overlapped with the gate chain + mount work). The getters are read at
|
|
263
|
-
// request time, but the cache must be populated before the first request.
|
|
264
169
|
await ttlPromise;
|
|
265
170
|
|
|
266
|
-
// CRITICAL: Block discovery and page routes MUST be registered BEFORE serving,
|
|
267
|
-
// because Hono's RegExpRouter compiles on first request and becomes immutable.
|
|
268
|
-
// We defer only client asset building (expensive Vite build) to Phase 2.
|
|
269
171
|
let components: Awaited<ReturnType<typeof loadPageManifest>>;
|
|
270
172
|
try {
|
|
271
173
|
components = await discoverPromise;
|
|
@@ -282,15 +184,10 @@ export async function buildMinimalApp(
|
|
|
282
184
|
);
|
|
283
185
|
|
|
284
186
|
|
|
285
|
-
// Run boot hooks BEFORE serving - some hooks may register routes and must
|
|
286
|
-
// run before Hono's matcher is compiled by the first request
|
|
287
187
|
await runStep("run-boot-hooks", () =>
|
|
288
188
|
runBootHookList(config, mountRegistry, serviceSlug),
|
|
289
189
|
);
|
|
290
190
|
|
|
291
|
-
// Register page routes with a dynamic handler that delegates to the real handler once Phase 2 completes.
|
|
292
|
-
// All route paths must be registered before serving to avoid Hono
|
|
293
|
-
// RegExpRouter compilation issues (matcher becomes immutable).
|
|
294
191
|
const registeredPageRoutes = await runStep("register-page-routes", () =>
|
|
295
192
|
registerPageRoutes({
|
|
296
193
|
app,
|
|
@@ -305,10 +202,6 @@ export async function buildMinimalApp(
|
|
|
305
202
|
const steps = Object.entries(stepTimings)
|
|
306
203
|
.map(([step, ms]) => `${step}=${ms}ms`)
|
|
307
204
|
.join(" ");
|
|
308
|
-
// NOTE: several steps run concurrently (speculative preload, init-gate-ttls,
|
|
309
|
-
// discover-blocks overlap the gate chain), so the step sum exceeds the
|
|
310
|
-
// wall-clock buildMinimalApp duration. Compare to the boot-ui-service phase
|
|
311
|
-
// timing in the Boot complete line for the true wall-clock cost.
|
|
312
205
|
log.info(
|
|
313
206
|
`[two-phase-boot:${serviceSlug}] buildMinimalApp breakdown (steps overlap) — ${steps}`,
|
|
314
207
|
);
|
|
@@ -328,9 +221,6 @@ export async function buildMinimalApp(
|
|
|
328
221
|
databaseUrl,
|
|
329
222
|
blockPackages,
|
|
330
223
|
components,
|
|
331
|
-
// The generated manifest IS the loaded set — every entry it exports is a
|
|
332
|
-
// block this service renders, so there is no "skipped" half any more. A
|
|
333
|
-
// row that cannot produce a component fails codegen, not boot.
|
|
334
224
|
loaded: Object.keys(components),
|
|
335
225
|
skipped: [] as string[],
|
|
336
226
|
};
|