@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,43 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* The two SWR loader-graph caches (static-shell + parameterised routes) for the
|
|
7
|
-
* registry page registrar, plus readCachedGraph — the testable stale-while-
|
|
8
|
-
* revalidate read. Extracted from register-registry-page-route.ts so the SWR
|
|
9
|
-
* decision is unit-testable independent of the Hono/DB/graph machinery.
|
|
10
|
-
*
|
|
11
|
-
* The SWR contract rides @teamscala/cache directly (the hand-rolled `at` timestamp,
|
|
12
|
-
* FRESH_MS window, and graphRefreshInflight single-flight Set are gone): each
|
|
13
|
-
* cache is configured ttlMs = the fresh window (serve, no refresh) + staleTtlMs =
|
|
14
|
-
* the rest (serve stale + fire ONE background refresh). readCachedGraph uses
|
|
15
|
-
* getWithMeta for the fresh/stale/miss decision and cache.refreshInBackground
|
|
16
|
-
* for the fire-and-forget refresh — deduped against the cache's own in-flight map
|
|
17
|
-
* (so delete/clear/disable drop its flights, and a refresh in flight across an
|
|
18
|
-
* invalidation can no longer resurrect the dropped entry).
|
|
19
|
-
*
|
|
20
|
-
* Graphs deep-clone on BOTH set and get at the caller (registerRegistryPageRoute):
|
|
21
|
-
* gateAgreementContent + downstream block resolution MUTATE the object in place,
|
|
22
|
-
* so handing a cached tree out twice would ship request A's resolved data to
|
|
23
|
-
* request B (ssr-first-tick-parity). Clone-on-get stays local to the caller.
|
|
24
|
-
*/
|
|
25
|
-
/**
|
|
26
|
-
* Stale-while-revalidate read of a cached loader graph.
|
|
27
|
-
*
|
|
28
|
-
* - Fresh hit (within ttlMs): return the cached graph, no refresh.
|
|
29
|
-
* - Stale hit (within ttlMs + staleTtlMs): return the cached graph AND fire ONE
|
|
30
|
-
* background refresh via cache.refreshInBackground — single-flight (deduped
|
|
31
|
-
* against the cache's in-flight map), never rejects, leaves the stale entry in
|
|
32
|
-
* place on failure. The caller deep-clones the returned value before use.
|
|
33
|
-
* - Miss (past staleTtlMs, or never cached): return undefined — the caller
|
|
34
|
-
* resolves synchronously and cache.set()s the clone.
|
|
35
|
-
*
|
|
36
|
-
* The refresh callback must return a graph safe to store. To SKIP caching a
|
|
37
|
-
* refresh result (e.g. a primary-missing / dead-link graph that must stay
|
|
38
|
-
* per-request so a just-created record appears immediately), the callback THROWS
|
|
39
|
-
* — refreshInBackground catches it, leaves the stale entry, and logs one line
|
|
40
|
-
* (an observable dead-link signal, not a hard error).
|
|
41
4
|
*/
|
|
42
5
|
|
|
43
6
|
import { createCache } from "@teamscala/cache/create-cache";
|
|
@@ -1,27 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* live-loader-config.ts — the per-request resource_loader read.
|
|
7
|
-
*
|
|
8
|
-
* The constitution's deploy-time bake list (installer-and-runtime) names
|
|
9
|
-
* service_install, service_manifest, block_schema, page_route, api_route and
|
|
10
|
-
* scheduled_job — resource_loader is NOT among it, so a loader row is runtime
|
|
11
|
-
* data: a registry edit must reach the running service on a subsequent request
|
|
12
|
-
* without a restart. This module is that contract: `readLiveLoaderConfig`
|
|
13
|
-
* re-reads the row through `loadRegistryConfig` on EVERY request (its SWR
|
|
14
|
-
* cache makes the fresh-window hit a memory read, not a DB round trip) and
|
|
15
|
-
* FAILS CLOSED — a row that vanished or stopped validating mid-flight throws
|
|
16
|
-
* into the branded error path rather than silently serving the boot-time
|
|
17
|
-
* snapshot of a row that no longer exists.
|
|
18
|
-
*
|
|
19
|
-
* `fingerprintLoaderConfig` gives the per-request graph cache a key component
|
|
20
|
-
* derived from the LIVE config, so a row edit invalidates cached graphs for
|
|
21
|
-
* that route immediately (the graph cache keys on route+host+path alone would
|
|
22
|
-
* otherwise serve a stale graph until its SWR window lapsed — right cache,
|
|
23
|
-
* wrong key: the graph is a function of the config, so the config belongs in
|
|
24
|
-
* the key).
|
|
25
4
|
*/
|
|
26
5
|
|
|
27
6
|
import { loadRegistryConfig } from "@teamscala/db/registry/load-config";
|
|
@@ -1,36 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Meta/title helpers for the route registrar: interpolate the route's
|
|
7
|
-
* meta_context against the resolved graph. Extracted from
|
|
8
|
-
* register-registry-page-route.ts.
|
|
9
|
-
*
|
|
10
|
-
* resolveTemplateTitle (the page_meta_template title reader) was DELETED
|
|
11
|
-
* 2026-08-17: it only ever applied when NO resource entity resolved, and on a
|
|
12
|
-
* resource page an entity ALWAYS resolves — the curated template was discarded
|
|
13
|
-
* by construction, dead on exactly the pages the type exists for (Valencia's
|
|
14
|
-
* finding). composePageTitle (+ the org-prefix flag) owns titles end to end.
|
|
15
|
-
*/
|
|
16
|
-
/**
|
|
17
|
-
* THE ONE SUBJECT DERIVATION. What a page is ABOUT — the entity whose name the
|
|
18
|
-
* browser tab should carry.
|
|
19
|
-
*
|
|
20
|
-
* This existed as THREE hand-written `||` chains, one per render branch in
|
|
21
|
-
* resolve-page-data, and they had already diverged: two ordered form before
|
|
22
|
-
* proposal, the third ordered proposal first. So which resource named the page
|
|
23
|
-
* depended on which branch happened to render it, silently.
|
|
24
|
-
*
|
|
25
|
-
* The divergence is the cheap half of the cost. The expensive half is that a
|
|
26
|
-
* correction has to be applied three times to work, and applying it twice looks
|
|
27
|
-
* exactly like applying it correctly — measured 2026-08-17, when a fix to the
|
|
28
|
-
* quiz subject was shipped, installed and verified present in the bundle while
|
|
29
|
-
* the page kept its old title, because the branch that rendered it held the
|
|
30
|
-
* second copy.
|
|
31
|
-
*
|
|
32
|
-
* One chain, one precedence, one place to change. Adding a resource type is an
|
|
33
|
-
* edit here rather than a fourth branch in three files.
|
|
34
4
|
*/
|
|
35
5
|
|
|
36
6
|
import { interpolateMeta } from "@teamscala/resolver-engine/loader-config/resolve-from-loader-config";
|
|
@@ -43,11 +13,6 @@ export function buildMetaFromContext(
|
|
|
43
13
|
return interpolateMeta(metaContext as Record<string, unknown>, resolved);
|
|
44
14
|
}
|
|
45
15
|
|
|
46
|
-
/**
|
|
47
|
-
* The page's own meta_title row (loaderData.page first, resolved.page second)
|
|
48
|
-
* — used only on /sign-in, where the org's sign-in page title overrides the
|
|
49
|
-
* derived one. Moved from register-registry-page-route.ts.
|
|
50
|
-
*/
|
|
51
16
|
export function resolvePageMetaTitle(
|
|
52
17
|
resolved: Record<string, unknown>,
|
|
53
18
|
): unknown {
|
|
@@ -1,27 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Shared Puck-page-data helpers for the resolved-graph → pageData conversion:
|
|
7
|
-
* the PuckData alias of the canonical boundary Data, JSON extraction with an
|
|
8
|
-
* honest null on
|
|
9
|
-
* malformed input, layout-default application, region root-prop picking, the
|
|
10
|
-
* RegionEntry shape, and the unresolved-region-slot warn. Plus the brand-token
|
|
11
|
-
* prologue shared by all three render branches.
|
|
12
|
-
*
|
|
13
|
-
* Split out of resolve-page-data.ts (547 lines) — byte-for-byte moves; only
|
|
14
|
-
* closure variables became explicit parameters.
|
|
15
4
|
*/
|
|
16
5
|
|
|
17
6
|
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
18
7
|
import type { Data } from "@teamscala/puck-boundary/data";
|
|
19
8
|
|
|
20
|
-
/**
|
|
21
|
-
* The canonical identity from the generated boundary (the platform's one
|
|
22
|
-
* declaration site for framework shapes) — never a hand-spelled structural
|
|
23
|
-
* duplicate of it.
|
|
24
|
-
*/
|
|
25
9
|
export type PuckData = Data;
|
|
26
10
|
|
|
27
11
|
export const EMPTY_PUCK_DATA: PuckData = { root: { props: {} }, content: [] };
|
|
@@ -97,7 +81,6 @@ export function warnUnresolvedRegionSlot(slot: string): void {
|
|
|
97
81
|
);
|
|
98
82
|
}
|
|
99
83
|
|
|
100
|
-
/** The resolved brand tokens every branch reads (the old file's shared prologue). */
|
|
101
84
|
export function readBrandTokens(resolved: Record<string, unknown>): {
|
|
102
85
|
brand: Record<string, unknown>;
|
|
103
86
|
primaryColor: string;
|
|
@@ -1,31 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Resolves the organisation id for the background AI-chat session prefetch
|
|
7
|
-
* from the request path. A canonical portal URL is /<websiteId>/<orgId>/
|
|
8
|
-
* <path> — the org is segment 1, NOT segment 0. Moved from
|
|
9
|
-
* register-registry-page-route.ts.
|
|
10
4
|
*/
|
|
11
5
|
|
|
12
|
-
/**
|
|
13
|
-
* Prefer segment 1; fall back to segment 0 only when segment 1 is not a
|
|
14
|
-
* UUID (the single-UUID shape, which is live and supported — NOT
|
|
15
|
-
* transitional). The fallback is deliberately kept
|
|
16
|
-
* UNCHANGED-BEHAVIOUR rather than "fixed": a UUID in segment 0 is
|
|
17
|
-
* genuinely ambiguous — of the 40 UUID-first page paths in builda_pages,
|
|
18
|
-
* ZERO are websites and ZERO are organisations, so segment 0 can also be
|
|
19
|
-
* an unrelated record id. Position alone cannot resolve that; only
|
|
20
|
-
* validating the id against organisation_profile can, and that is a DB
|
|
21
|
-
* read this hot prefetch path should not take without caching
|
|
22
|
-
* (`hot-path-resolutions-are-cached`). Preferring segment 1 is strictly
|
|
23
|
-
* better for the canonical shape and no worse for any other. Reading
|
|
24
|
-
* segment 0 first captured the WEBSITE id and passed it as an
|
|
25
|
-
* organisation id, which failed silently because both are well-formed
|
|
26
|
-
* UUIDs: the prefetch simply warmed a session against the wrong tenant
|
|
27
|
-
* (found 2026-07-21).
|
|
28
|
-
*/
|
|
29
6
|
|
|
30
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;
|
|
31
8
|
|
|
@@ -1,27 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Resolve the `{{approvalEmail}}` token on a proposal page, after pageData is
|
|
7
|
-
* built.
|
|
8
|
-
*
|
|
9
|
-
* Split from proposal-post-processors.ts, which single-purpose-file correctly
|
|
10
|
-
* flagged as two unrelated export groups. This unit is an async ORPC lookup
|
|
11
|
-
* that rewrites button props; its former co-tenants are synchronous brand
|
|
12
|
-
* stamping that touch no network. Sharing a file made an I/O-bound resolver
|
|
13
|
-
* look like a pure formatter.
|
|
14
|
-
*/
|
|
15
|
-
/**
|
|
16
|
-
* Resolve the `{{approvalEmail}}` placeholder in a proposal page's button props
|
|
17
|
-
* (e.g. the Approve CTA) in place. Routing: the organisation's registered
|
|
18
|
-
* company email FIRST (organisation_profile.contact_email, already resolved
|
|
19
|
-
* into pageData.brand.contactEmail); the proposal creator's user email only as
|
|
20
|
-
* a fallback when the org has none — so a cloned proposal can never carry a
|
|
21
|
-
* stale hard-coded address. The creator lookup is null-safe and adds no user
|
|
22
|
-
* exposure: it resolves only where the in-process ORPC client exposes `user`
|
|
23
|
-
* (super-admin), degrading to the org email elsewhere. Called only for proposal
|
|
24
|
-
* pages; other registry pages are untouched.
|
|
25
4
|
*/
|
|
26
5
|
|
|
27
6
|
import { getServerClient } from "../page-server-orpc/server-client.ts";
|
|
@@ -1,32 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Stamp partner and client brand assets onto a proposal page's pageData, after
|
|
7
|
-
* it is built.
|
|
8
|
-
* Split from proposal-post-processors.ts (single-purpose-file: two unrelated
|
|
9
|
-
* export groups). Both helpers here are synchronous, take an already-resolved
|
|
10
|
-
* loader-graph org, and only map a logo/colour onto pageData — no network, no
|
|
11
|
-
* ORPC. The approval-email token resolver they used to share a file with is an
|
|
12
|
-
* async ORPC lookup, which is the distinction that was invisible while they sat
|
|
13
|
-
* together.
|
|
14
4
|
*/
|
|
15
5
|
|
|
16
|
-
/**
|
|
17
|
-
* Stash a proposal's connected partner logo/colour on `pageData` from the
|
|
18
|
-
* `partnerOrg` loader source (`crm_organisations` loaded by the proposal's
|
|
19
|
-
* `partner_id`, via the SAME proven ORPC loader path that loads the proposal
|
|
20
|
-
* itself). resolve-page-brand (service-runtime) prefers `pageData.partnerLogo`
|
|
21
|
-
* over any inline body-Hero prop and fans it into the shared `/header` so a
|
|
22
|
-
* `source: "partner"` OrgLogo fills automatically — the partner logo lives on
|
|
23
|
-
* the CRM record, never duplicated per proposal. No partner → partnerOrg is
|
|
24
|
-
* null → nothing set → the partner logo block renders blank. Reading the
|
|
25
|
-
* already-resolved loader value (not a second hand-rolled getServerClient call)
|
|
26
|
-
* is what makes this reliable: the earlier direct `crm_organisations.findFirst`
|
|
27
|
-
* with a Prisma-style object `select` resolved intermittently/never against the
|
|
28
|
-
* generated ORPC, leaving the header partner blank (2026-06-12).
|
|
29
|
-
*/
|
|
30
6
|
export function applyPartnerBrand(
|
|
31
7
|
pageData: Record<string, unknown>,
|
|
32
8
|
partnerOrg:
|
|
@@ -40,12 +16,6 @@ export function applyPartnerBrand(
|
|
|
40
16
|
pageData.partnerBrandColor = partnerOrg.brand_primary_color;
|
|
41
17
|
}
|
|
42
18
|
|
|
43
|
-
/**
|
|
44
|
-
* Auto-resolve the client's company logo from the `clientOrg` loader source
|
|
45
|
-
* (contact_id → company_id → crm_organisations.logo_url). Injects into the
|
|
46
|
-
* Hero block's clientLogo prop when it's empty — no manual logo URL needed
|
|
47
|
-
* per proposal when the contact's company profile has a logo.
|
|
48
|
-
*/
|
|
49
19
|
export function applyClientLogo(
|
|
50
20
|
pageData: Record<string, unknown>,
|
|
51
21
|
clientOrg:
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Branch 2 of the resolved-graph → pageData conversion: the public shell
|
|
7
|
-
* (header/body/footer regions from header/footer sources).
|
|
8
|
-
*
|
|
9
|
-
* Split out of resolve-page-data.ts (547 lines) — byte-identical body; only
|
|
10
|
-
* the closure variables became explicit parameters.
|
|
11
4
|
*/
|
|
12
5
|
|
|
13
6
|
import { resolveThemeMode } from "../resolve-theme-mode.ts";
|
|
@@ -95,12 +88,6 @@ export function publicShellPageData(
|
|
|
95
88
|
const overrideHeading = pickRootProp<string>(regions, "headingFont");
|
|
96
89
|
const overrideBody = pickRootProp<string>(regions, "bodyFont");
|
|
97
90
|
|
|
98
|
-
// The page's own screenMode, threaded the same way Branch 1 threads it
|
|
99
|
-
// (single-page-data.ts). Dropping it here is not neutral: build-page-tree
|
|
100
|
-
// falls back to `fullScreen: renderMode === "public" || "gpts"`, and for a
|
|
101
|
-
// shell page that resolves false — which wraps the body in a centred card
|
|
102
|
-
// and puts the site header INSIDE that card instead of across the top of
|
|
103
|
-
// the page. A page that declares "full-screen" was rendering as a form.
|
|
104
91
|
const screenMode = pickRootProp<string>(regions, "screenMode");
|
|
105
92
|
|
|
106
93
|
const pageName = (page.name as string) || null;
|
|
@@ -115,22 +102,11 @@ export function publicShellPageData(
|
|
|
115
102
|
return {
|
|
116
103
|
puckData: { root: { props: {} }, content: [] } as PuckData,
|
|
117
104
|
regions,
|
|
118
|
-
// Region-shell blocks (e.g. the header Navigation deriving the body's
|
|
119
|
-
// document sections) read the resolved entity graph from
|
|
120
|
-
// pageData.loaderData — the same surface Branch 1 exposes. Without it a
|
|
121
|
-
// header/footer block serverLoader gets ctx.loaderData=undefined and
|
|
122
|
-
// cannot see the body page, so a server-derived nav produced no items
|
|
123
|
-
// and the menu was client-only (vanished on stale tabs / narrow views).
|
|
124
105
|
loaderData: resolved,
|
|
125
106
|
website: {
|
|
126
107
|
slug: website.slug as string,
|
|
127
108
|
name: website.name as string,
|
|
128
109
|
organisation_id: website.organisation_id as string | null,
|
|
129
|
-
// header_settings carries the DB-driven header config (e.g.
|
|
130
|
-
// backdropBlur) the public shell applies — PublicTemplate reads
|
|
131
|
-
// pageData.website.header_settings.backdropBlur. Dropping it here
|
|
132
|
-
// silently kills the header blur even though the DB switch is set
|
|
133
|
-
// (the 2026-06-17 regression). Pass it through verbatim.
|
|
134
110
|
header_settings:
|
|
135
111
|
(website.header_settings as Record<string, unknown> | null) ?? null,
|
|
136
112
|
},
|
|
@@ -139,10 +115,6 @@ export function publicShellPageData(
|
|
|
139
115
|
null,
|
|
140
116
|
brand: {
|
|
141
117
|
primaryColor: overrideColor ?? primaryColor,
|
|
142
|
-
// Thread the org brand's dark surface (brand_background_color /
|
|
143
|
-
// brand_dark_color) when the page sets no explicit override — so
|
|
144
|
-
// dark-mode pages use the brand's real dark colour, not a
|
|
145
|
-
// primary-derived tint. Uses the org brand's real dark colour.
|
|
146
118
|
backgroundColor:
|
|
147
119
|
overrideBg ?? (brand.backgroundColor as string | null) ?? null,
|
|
148
120
|
lightSurface: (brand.lightSurface as string | null) ?? null,
|
|
@@ -166,11 +138,6 @@ export function publicShellPageData(
|
|
|
166
138
|
pageMetaTitle: bodyMetaTitle,
|
|
167
139
|
templateTitle: (meta.title as string) || undefined,
|
|
168
140
|
websiteName: (website.name as string) || undefined,
|
|
169
|
-
// The FRIENDLY name is what the public sees — the trading brand,
|
|
170
|
-
// not the legal entity. Reading companyName here put "GMG APAC"
|
|
171
|
-
// in the title of every launcha page, which the operator has
|
|
172
|
-
// corrected repeatedly; the friendly name is "launcha". Falls
|
|
173
|
-
// back to companyName only where no friendly name is set.
|
|
174
141
|
organisationName:
|
|
175
142
|
(brand.companyFriendlyName as string) ||
|
|
176
143
|
(brand.companyName as string) ||
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Branch 1 of the resolved-graph → pageData conversion: arbitrary region-slots
|
|
7
|
-
* (portal/agents/docs) driven by resource_loader.region_slots.
|
|
8
|
-
*
|
|
9
|
-
* Split out of resolve-page-data.ts (547 lines) — byte-identical body; only
|
|
10
|
-
* the closure variables became explicit parameters.
|
|
11
4
|
*/
|
|
12
5
|
|
|
13
6
|
import { resolveThemeMode } from "../resolve-theme-mode.ts";
|
|
@@ -1,15 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Converts the resolved loader-graph into the pageData shape the renderer
|
|
7
|
-
* consumes — three branches: arbitrary region-slots (portal/utility), the
|
|
8
|
-
* header/body/footer public shell, and the single-page fallback. Extracted
|
|
9
|
-
* from register-registry-page-route.ts (single-purpose-file split); the three
|
|
10
|
-
* branches + shared helpers were split out of THIS file at 547 lines
|
|
11
|
-
* (region-slots-page-data.ts, public-shell-page-data.ts, single-page-data.ts,
|
|
12
|
-
* page-data-shared.ts).
|
|
13
4
|
*/
|
|
14
5
|
|
|
15
6
|
import { regionSlotsPageData } from "./region-slots-page-data.ts";
|
|
@@ -1,15 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* ?panel=<slug> resolution: reads the panel_view row, follows its
|
|
7
|
-
* content.page_slug to the builda_pages row, and stashes the panel page
|
|
8
|
-
* data + active panel slug on pageData.
|
|
9
|
-
*
|
|
10
|
-
* Split out of register-registry-page-route.ts (924 lines) — the loadData
|
|
11
|
-
* closure segment moved byte-identical; only the closure variables became
|
|
12
|
-
* explicit parameters.
|
|
13
4
|
*/
|
|
14
5
|
|
|
15
6
|
import { getServerClient } from "../page-server-orpc/server-client.ts";
|
|
@@ -1,16 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Per-request loader-graph resolution for a registered page route: SWR cache
|
|
7
|
-
* read (static-shell vs param-route cache), clone-on-get, defensive clone
|
|
8
|
-
* with logged fallbacks, primary-resource-missing dead-link 404s, and the
|
|
9
|
-
* content-page floor.
|
|
10
|
-
*
|
|
11
|
-
* Split out of register-registry-page-route.ts (924 lines) — the loadData
|
|
12
|
-
* closure segments moved byte-identical; only the closure variables became
|
|
13
|
-
* explicit parameters.
|
|
14
4
|
*/
|
|
15
5
|
|
|
16
6
|
import type { PageRouteConfig } from "../register-registry-page-route.ts";
|
|
@@ -71,8 +61,6 @@ function cloneForCache(
|
|
|
71
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}`,
|
|
72
62
|
);
|
|
73
63
|
try {
|
|
74
|
-
// JSON-clone preserves the data graph, dropping the functions and
|
|
75
|
-
// undefined a loader graph should not be carrying anyway.
|
|
76
64
|
return JSON.parse(JSON.stringify(graph)) as unknown;
|
|
77
65
|
} catch (err2) {
|
|
78
66
|
log.warn(
|
|
@@ -88,19 +76,7 @@ export async function resolveRequestGraph(
|
|
|
88
76
|
): Promise<Record<string, unknown>> {
|
|
89
77
|
const { pattern, routeSlug, routeConfig, loaderConfig, params, isStaticShell, serviceSlug, configFingerprint } = args;
|
|
90
78
|
|
|
91
|
-
// A bare root ("/") that declares a resource_type is a UUID-driven
|
|
92
|
-
// public site (proposals/forms/quiz) with no resource selected — it
|
|
93
|
-
// must render the branded holding/not-found page ("no <resource> at
|
|
94
|
-
// this link"), never fall through the shared public loader to the
|
|
95
|
-
// marketing site. PrimaryResourceMissingError is the existing signal
|
|
96
|
-
// the page binder maps to buildResourceNotFoundResponse. A "/" route
|
|
97
|
-
// WITHOUT a resource_type (e.g. scala-public's marketing root) is
|
|
98
|
-
// unaffected and still renders its content.
|
|
99
79
|
if (pattern === "/" && routeConfig.resource_type) {
|
|
100
|
-
// Status 200: the bare root is a valid "no <resource> selected"
|
|
101
|
-
// landing, not an error — and kind:ui services must answer GET /
|
|
102
|
-
// with 200 (verify-prod-persistent-slot). An invalid /:id keeps
|
|
103
|
-
// the default 404 (genuinely not found).
|
|
104
80
|
throw new PrimaryResourceMissingError(routeConfig.resource_type, 200);
|
|
105
81
|
}
|
|
106
82
|
const graphKey = `graph:${routeSlug}:${params.host}:${params.path}${configFingerprint ? `:${configFingerprint}` : ""}`;
|
|
@@ -121,13 +97,6 @@ export async function resolveRequestGraph(
|
|
|
121
97
|
>;
|
|
122
98
|
};
|
|
123
99
|
let resolved: Record<string, unknown>;
|
|
124
|
-
// SWR read over @teamscala/cache: fresh hit serves the cached graph, stale
|
|
125
|
-
// hit serves it AND fires ONE background refresh (refreshInBackground,
|
|
126
|
-
// single-flight via the cache's in-flight map), miss returns undefined.
|
|
127
|
-
// The refresh callback re-resolves + stores a clone; a primary-missing
|
|
128
|
-
// (dead-link) graph THROWS so the cache is not updated — a dead link must
|
|
129
|
-
// stay per-request so a just-created record appears immediately
|
|
130
|
-
// (refreshInBackground catches, leaves the stale entry, logs one line).
|
|
131
100
|
const cachedGraph = readCachedGraph(
|
|
132
101
|
graphCache,
|
|
133
102
|
graphKey,
|
|
@@ -143,16 +112,6 @@ export async function resolveRequestGraph(
|
|
|
143
112
|
`primary resource "${primaryType}" missing — skip cache update for ${graphKey}`,
|
|
144
113
|
);
|
|
145
114
|
}
|
|
146
|
-
// SAME DEFENSIVE CLONE AS THE COLD PATH. This used to call
|
|
147
|
-
// structuredClone bare, so a non-cloneable value in the loader
|
|
148
|
-
// graph threw on EVERY background refresh: refreshInBackground
|
|
149
|
-
// caught it, kept the stale entry, and logged one line — so the
|
|
150
|
-
// cache could never update, expired at staleTtlMs, and the next
|
|
151
|
-
// request re-resolved the whole graph in-request. Measured
|
|
152
|
-
// 2026-09-03 on the live proposal page: "The object can not be
|
|
153
|
-
// cloned" every ~13 minutes on the same graphKey, indefinitely.
|
|
154
|
-
// The cold path was hardened for exactly this and the refresh
|
|
155
|
-
// path was not — one defect, two clone sites, one protected.
|
|
156
115
|
const refreshed = cloneForCache(freshGraph, graphKey, serviceSlug);
|
|
157
116
|
if (refreshed === undefined) {
|
|
158
117
|
throw new Error(
|
|
@@ -164,47 +123,21 @@ export async function resolveRequestGraph(
|
|
|
164
123
|
),
|
|
165
124
|
);
|
|
166
125
|
if (cachedGraph !== undefined) {
|
|
167
|
-
// Clone-on-get: gateAgreementContent + block resolution MUTATE the
|
|
168
|
-
// graph in place, so each request gets its own copy.
|
|
169
126
|
resolved = structuredClone(cachedGraph) as Record<string, unknown>;
|
|
170
127
|
} else {
|
|
171
128
|
resolved = await resolveFromLoaderConfig(typedLoaderConfig, params);
|
|
172
129
|
const primaryMissing =
|
|
173
130
|
hasPrimarySource && primaryType && resolved[primaryType] == null;
|
|
174
131
|
if (!primaryMissing) {
|
|
175
|
-
// Defensive clone: structuredClone throws on non-cloneable loader
|
|
176
|
-
// graphs (functions/Proxies/Prisma instances). A swallowed throw
|
|
177
|
-
// here used to SILENTLY disable the graph cache for this key
|
|
178
|
-
// (fail-open-silent — forbidden by protective-layers-fail-explicitly),
|
|
179
|
-
// which left affected routes re-resolving the loader graph every
|
|
180
|
-
// request (measured ~1.2s on the projects utility shell). JSON-clone
|
|
181
|
-
// preserves the data graph (dropping functions/undefined a loader
|
|
182
|
-
// graph shouldn't carry) so the cache stays effective; both fallbacks
|
|
183
|
-
// are LOGGED so the offending non-cloneable source is observable +
|
|
184
|
-
// fixable, never silently disabling the cache again.
|
|
185
132
|
const cloned = cloneForCache(resolved, graphKey, serviceSlug);
|
|
186
133
|
if (cloned !== undefined) {
|
|
187
134
|
graphCache.set(graphKey, cloned);
|
|
188
135
|
}
|
|
189
136
|
}
|
|
190
137
|
}
|
|
191
|
-
// A route that declares resource_type is meaningless without that
|
|
192
|
-
// entity: a dead link (wrong id, deleted row) must 404 with the
|
|
193
|
-
// branded not-found page — never fall through to default content
|
|
194
|
-
// (the "Vision Toolbar" leak, operator-observed 2026-06-05).
|
|
195
138
|
if (hasPrimarySource && primaryType && resolved[primaryType] == null) {
|
|
196
139
|
throw new PrimaryResourceMissingError(primaryType);
|
|
197
140
|
}
|
|
198
|
-
// A CONTENT route (no resource_type) whose loader resolves a `page`
|
|
199
|
-
// body source must not synthesize an empty page for an unmatched path
|
|
200
|
-
// and serve a cacheable 200 — the generalized "Vision Toolbar" leak
|
|
201
|
-
// (operator-reported 5×: a deleted page, e.g. /roadmap, returned 200
|
|
202
|
-
// with an empty body instead of not-found). No page row → dead link →
|
|
203
|
-
// branded 404 floor. A page row that exists but has zero blocks → a
|
|
204
|
-
// content gap → the floor's empty-page surface (200). Both carry the
|
|
205
|
-
// "Take me home" action. The pure decision lives in content-page-floor.ts
|
|
206
|
-
// (unit-tested without the Hono/DB/graph machinery). Region-slot
|
|
207
|
-
// (utility/portal) shells without a `page` source are untouched.
|
|
208
141
|
const contentFloorError = decideContentPageFloor({
|
|
209
142
|
hasResourceType: !!primaryType,
|
|
210
143
|
hasPageContentSource: !!(loaderConfig as { sources?: { key: string }[] }).sources?.some(
|
|
@@ -1,78 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Runs every registered content gate in catalog order (sequential, never
|
|
7
|
-
* parallel — gates depend on prior gates' graph mutations) and applies the
|
|
8
|
-
* page's declared assistant where authorized.
|
|
9
|
-
*
|
|
10
|
-
* Split out of register-registry-page-route.ts (924 lines) — the loadData
|
|
11
|
-
* closure segment moved byte-identical; only the closure variables became
|
|
12
|
-
* explicit parameters.
|
|
13
4
|
*/
|
|
14
5
|
|
|
15
|
-
import type { PageRouteConfig } from "../register-registry-page-route.ts";
|
|
16
6
|
import { runContentGate } from "../content-gate/gate.ts";
|
|
17
|
-
import
|
|
18
|
-
import { CONTENT_GATES, withholdsContent } from "../content-gate/catalog.ts";
|
|
19
|
-
import { injectResourceChatLauncher } from "../resource-chat-inject.ts";
|
|
7
|
+
import { CONTENT_GATES } from "../content-gate/catalog.ts";
|
|
20
8
|
|
|
21
9
|
export async function runContentGatesForPage(
|
|
22
10
|
resolved: Record<string, unknown>,
|
|
23
11
|
gateCookie: string | null,
|
|
24
12
|
gateNow: number,
|
|
25
|
-
assistant: PageRouteConfig["assistant"],
|
|
26
|
-
regionSlots: string[] | undefined,
|
|
27
13
|
): Promise<void> {
|
|
28
|
-
// EVERY REGISTERED GATE, IN CATALOG ORDER (content-gate/catalog.ts).
|
|
29
|
-
// This used to name proposalGateConfig, agreementGateConfig and
|
|
30
|
-
// gptGateConfig by hand, so a fourth gated resource type meant editing
|
|
31
|
-
// this file AND the second hand-written list in two-phase-boot.ts.
|
|
32
|
-
//
|
|
33
|
-
// SEQUENTIAL, NEVER PARALLEL. An authorized proposal viewer has their
|
|
34
|
-
// gate block removed from the resolved graph, and that removal is what
|
|
35
|
-
// makes the agreement gate find no resource and no-op on a page that
|
|
36
|
-
// carries both. Awaiting these together would race that dependency.
|
|
37
|
-
const gateOutcomes: ContentGateOutcome[] = [];
|
|
38
14
|
for (const entry of CONTENT_GATES) {
|
|
39
|
-
|
|
40
|
-
resolved,
|
|
41
|
-
gateCookie,
|
|
42
|
-
gateNow,
|
|
43
|
-
entry.gate,
|
|
44
|
-
);
|
|
45
|
-
// A seed-only gate (open capture — the public GPT identity gate)
|
|
46
|
-
// withholds nothing, so its verdict is not evidence about whether
|
|
47
|
-
// this viewer may see the page and must not reach the assistant
|
|
48
|
-
// decision below. See withholdsContent, which derives that from the
|
|
49
|
-
// access config rather than restating it here.
|
|
50
|
-
if (withholdsContent(entry)) gateOutcomes.push(outcome);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// 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
|
|
54
|
-
// in the hardcoded gate sequence above, so a third resource type meant editing this file. Now any page declaring `assistant` gets
|
|
55
|
-
// one and no page without the key does.
|
|
56
|
-
//
|
|
57
|
-
// GATED PAGES ONLY WHEN AUTHORIZED. The launcher's runtime context is
|
|
58
|
-
// the whole document serialised, so injecting past a closed gate
|
|
59
|
-
// would hand withheld bytes to the chat. A page no gate governs is
|
|
60
|
-
// public and gets the assistant unconditionally -- that is a
|
|
61
|
-
// different state from an unauthorized viewer, which is why the gate
|
|
62
|
-
// reports `applies` separately from `authorized`.
|
|
63
|
-
if (assistant) {
|
|
64
|
-
const governing = gateOutcomes.filter((outcome) => outcome.applies);
|
|
65
|
-
const authorized = governing.find((outcome) => outcome.authorized);
|
|
66
|
-
if (governing.length === 0 || authorized) {
|
|
67
|
-
await injectResourceChatLauncher(resolved, authorized?.email ?? "", {
|
|
68
|
-
resourceKey: assistant.resource_key,
|
|
69
|
-
agentConfigSlug: assistant.agent_config_slug as never,
|
|
70
|
-
launchLabel: assistant.launch_label,
|
|
71
|
-
placeholder: assistant.placeholder,
|
|
72
|
-
// From the loader row, so the injector never has to guess
|
|
73
|
-
// which resolved keys hold rendered content.
|
|
74
|
-
regionSlots,
|
|
75
|
-
});
|
|
76
|
-
}
|
|
15
|
+
await runContentGate(resolved, gateCookie, gateNow, entry.gate);
|
|
77
16
|
}
|
|
78
17
|
}
|
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Branch 3 of the resolved-graph → pageData conversion: the single-page
|
|
7
|
-
* fallback (form/quiz/proposal/gpt pages with no region slots and no
|
|
8
|
-
* header/footer sources).
|
|
9
|
-
*
|
|
10
|
-
* Split out of resolve-page-data.ts (547 lines) — byte-identical body; only
|
|
11
|
-
* the closure variables became explicit parameters.
|
|
12
4
|
*/
|
|
13
5
|
|
|
14
6
|
import { resolveThemeMode } from "../resolve-theme-mode.ts";
|
|
@@ -1,15 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Stamps the resolved-in-progress loader graph with the request's session
|
|
7
|
-
* identity: sessionVisitor, portalContext (user/orgId/pane widths from the
|
|
8
|
-
* portal cookie and the user's saved preferences). Anonymous when no session.
|
|
9
|
-
*
|
|
10
|
-
* Split out of register-registry-page-route.ts (924 lines) — the loadData
|
|
11
|
-
* closure segment moved byte-identical; only the closure variables became
|
|
12
|
-
* explicit parameters.
|
|
13
4
|
*/
|
|
14
5
|
|
|
15
6
|
import { getServerClient } from "../page-server-orpc/server-client.ts";
|