@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,38 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Resolves the set of emails authorized to view a proposal. A proposal is
|
|
7
|
-
* gated (proposal-access) — only these emails can unlock it:
|
|
8
|
-
*
|
|
9
|
-
* - the issuing-org team: every member of the proposal's organisation
|
|
10
|
-
* (better_auth_member → user.email). Lets the agency team open any of their
|
|
11
|
-
* own proposals AND covers existing proposals whose created_by is
|
|
12
|
-
* "system"/null.
|
|
13
|
-
* - the recipient: the proposal's CRM contact (client_database.primary_email),
|
|
14
|
-
* when contact_id is set.
|
|
15
|
-
* - the partner team: every CRM contact belonging to the proposal's partner
|
|
16
|
-
* org (client_database.company_id = proposals.partner_id → primary_email),
|
|
17
|
-
* when partner_id is set. Lets the co-delivery partner's team open a
|
|
18
|
-
* co-branded proposal. The partner is still branding-first; this is the one
|
|
19
|
-
* access edge it carries.
|
|
20
|
-
*
|
|
21
|
-
* The partner clause casts to ::uuid because the two sides disagree in the
|
|
22
|
-
* database: client_database.company_id is `uuid` while proposals.partner_id is
|
|
23
|
-
* `text`. Postgres resolves operators at PLAN time, so `uuid = text` raises
|
|
24
|
-
* "operator does not exist" for EVERY proposal — including ones with no partner
|
|
25
|
-
* — the catch below swallows it, and the fail-closed empty Set then denies
|
|
26
|
-
* access to everyone. Measured 2026-09-10: no proposal could be unlocked by any
|
|
27
|
-
* email. The cast is correct once partner_id becomes `uuid` too (a no-op then);
|
|
28
|
-
* the column type is the real fix and is tracked separately.
|
|
29
|
-
*
|
|
30
|
-
* Uses $queryRaw (raw SQL) deliberately: the per-service Prisma client is scoped
|
|
31
|
-
* to that service's own models, so the proposals service's client has no
|
|
32
|
-
* `member`/`user` model methods — but $queryRaw runs against the shared DB
|
|
33
|
-
* regardless of the client's model scope. Returns a lowercased Set, checked
|
|
34
|
-
* server-side only (emails never sent to the client). Fail-closed: any error
|
|
35
|
-
* yields an empty set (no access).
|
|
36
4
|
*/
|
|
37
5
|
|
|
38
6
|
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
package/src/proposal-gate.ts
CHANGED
|
@@ -1,33 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* The PROPOSAL content gate, expressed as config for the content-gate primitive
|
|
7
|
-
* (content-gate/). No bespoke gate plumbing — just the proposal-specific bits:
|
|
8
|
-
* which resource (resolved.proposal.id), who's authorized (issuing-org team +
|
|
9
|
-
* recipient), what to seed (initialVisitor for the chat), and how to withhold
|
|
10
|
-
* (replace the proposal body with an injected SignIn gate block + neutralize the
|
|
11
|
-
* meta tags). The engine handles cookie/token/verify/orchestration.
|
|
12
|
-
*
|
|
13
|
-
* Universal: every proposal route exposes resolved.proposal, so this gates every
|
|
14
|
-
* proposal (existing + new) with no per-page block or migration.
|
|
15
4
|
*/
|
|
16
5
|
|
|
17
6
|
import type { AccessConfig, ContentGateConfig } from "./content-gate/config-types.ts";
|
|
18
|
-
import { signAccessToken, verifyAccessToken } from "
|
|
7
|
+
import { signAccessToken, verifyAccessToken } from "@teamscala/agreement-generation/content-gate-token";
|
|
19
8
|
import { resolveProposalAuthorizedEmails } from "./proposal-authorized-emails.ts";
|
|
20
9
|
import { propTtlMs } from "./gate-ttls.ts";
|
|
21
10
|
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
22
11
|
import { loadRegistryConfig } from "@teamscala/db/registry/load-config";
|
|
23
12
|
import * as v from "valibot";
|
|
24
13
|
|
|
25
|
-
/**
|
|
26
|
-
* Proposal gate display + behaviour, DB-driven (config/content-gate-proposal),
|
|
27
|
-
* never hardcoded. requireOtp is the per-gate OTP switch (proposals default
|
|
28
|
-
* off — email-only; agreements opt in). A missing/invalid row falls back to the
|
|
29
|
-
* proposal-appropriate defaults so the gate UI ALWAYS renders.
|
|
30
|
-
*/
|
|
31
14
|
interface ProposalGateSettings {
|
|
32
15
|
title: string;
|
|
33
16
|
subtitle: string;
|
|
@@ -89,8 +72,6 @@ function buildGateBlock(
|
|
|
89
72
|
gateContextField: "proposalId",
|
|
90
73
|
gateContextValue: proposalId,
|
|
91
74
|
gateAction: "/api/proposal-access",
|
|
92
|
-
// Display + behaviour are DB-driven (config/content-gate-proposal),
|
|
93
|
-
// not hardcoded. requireOtp is the per-gate OTP switch.
|
|
94
75
|
title: settings.title,
|
|
95
76
|
subtitle: settings.subtitle,
|
|
96
77
|
gateRequireOtp: settings.requireOtp,
|
|
@@ -111,26 +92,14 @@ export const proposalGateConfig: ContentGateConfig = {
|
|
|
111
92
|
const email = verifyAccessToken(token, PROP_COOKIE, resourceId, nowMs);
|
|
112
93
|
return email ? { email } : null;
|
|
113
94
|
},
|
|
114
|
-
// Baked-in unified auth (engine default): a logged-in viewer whose session
|
|
115
|
-
// email is in this set (issuing-org team ∪ recipient ∪ partner) is released
|
|
116
|
-
// with no email/OTP entry. Just exposing the resolver opts this gate into the
|
|
117
|
-
// platform-wide behaviour — no per-gate session code.
|
|
118
95
|
resolveAuthorizedEmails: resolveProposalAuthorizedEmails,
|
|
119
96
|
async onAuthorized(resolved, _resourceId, { email }) {
|
|
120
|
-
// Seed the verified viewer identity (building-applications initialVisitor).
|
|
121
97
|
(resolved as { initialVisitor?: { email: string } }).initialVisitor = {
|
|
122
98
|
email,
|
|
123
99
|
};
|
|
124
|
-
// THE ASSISTANT IS NO LONGER INJECTED HERE. It is declared on the
|
|
125
|
-
// page_route row (`assistant`) and applied once, centrally, after every
|
|
126
|
-
// gate has run — so the bubble is a setting rather than a line of code
|
|
127
|
-
// inside one resource type's gate. This call was the reason a third
|
|
128
|
-
// resource type could not have an assistant without editing service-boot.
|
|
129
100
|
},
|
|
130
101
|
async onUnauthorized(resolved, resourceId) {
|
|
131
102
|
const settings = await loadProposalGateSettings();
|
|
132
|
-
// Replace the proposal BODY with the gate block — no proposal content
|
|
133
|
-
// reaches the HTML. Header/footer chrome stays branded.
|
|
134
103
|
const page = resolved.page as
|
|
135
104
|
| (PuckHolder & {
|
|
136
105
|
meta_title?: unknown;
|
|
@@ -144,18 +113,11 @@ export const proposalGateConfig: ContentGateConfig = {
|
|
|
144
113
|
buildGateBlock(resourceId, brandPrimaryColor(resolved), settings),
|
|
145
114
|
];
|
|
146
115
|
}
|
|
147
|
-
// Neutralize the PAGE record's meta too: page.meta_title/meta_description
|
|
148
|
-
// feed pageData.meta → the <title>/og tags AND the __PAGE_DATA__ hydration
|
|
149
|
-
// JSON. Without this the proposal topic + partner name leak via meta even
|
|
150
|
-
// though the body is withheld (the 2026-06-13 meta-leak: a gated proposal
|
|
151
|
-
// still exposed "Webinar Campaign · in partnership with <partner>").
|
|
152
116
|
if (page) {
|
|
153
117
|
page.meta_title = "Private proposal";
|
|
154
118
|
page.meta_description = "";
|
|
155
119
|
page.name = "Private proposal";
|
|
156
120
|
}
|
|
157
|
-
// Neutralize the proposal record so the meta_context resolver + any block
|
|
158
|
-
// reading it can't leak name/partner/cover/recipient.
|
|
159
121
|
const prop = resolved.proposal as Record<string, unknown> | undefined;
|
|
160
122
|
if (prop) {
|
|
161
123
|
prop.name = "Private proposal";
|
|
@@ -164,13 +126,6 @@ export const proposalGateConfig: ContentGateConfig = {
|
|
|
164
126
|
prop.prepared_for = "";
|
|
165
127
|
prop.cover_image_url = "";
|
|
166
128
|
}
|
|
167
|
-
// Neutralize the resolved headerData too: it carries the recipient's
|
|
168
|
-
// name (row1Value = prepared_for) and title, resolved during the loader
|
|
169
|
-
// phase BEFORE this gate ran — without this, __PAGE_DATA__ ships the
|
|
170
|
-
// recipient name to every unauthorised visitor while the body is
|
|
171
|
-
// withheld. Labels stay (generic words); values go. Measured 2026-09-03:
|
|
172
|
-
// the gate floor's hydration payload carried the recipient name to
|
|
173
|
-
// anonymous visitors on the exact page this outage was about.
|
|
174
129
|
const headerData = resolved.headerData as Record<string, unknown> | undefined;
|
|
175
130
|
if (headerData) {
|
|
176
131
|
headerData.title = "";
|
|
@@ -178,12 +133,6 @@ export const proposalGateConfig: ContentGateConfig = {
|
|
|
178
133
|
headerData.row2Value = "";
|
|
179
134
|
headerData.row3Value = "";
|
|
180
135
|
}
|
|
181
|
-
// Neutralize the CRM contact record too. The v8 adapter ignores select
|
|
182
|
-
// (the full record returns), so without this the gate floor's hydration
|
|
183
|
-
// payload ships the recipient's whole contact row — first/last/full
|
|
184
|
-
// name, emails, phones, CRM ids — to every unauthorised visitor. The
|
|
185
|
-
// company fields stay: the header's Organisation row shows the company,
|
|
186
|
-
// which is brand chrome, not recipient identity.
|
|
187
136
|
const contact = resolved.contact as Record<string, unknown> | undefined;
|
|
188
137
|
if (contact) {
|
|
189
138
|
const keep = new Set(["id", "company_id", "company_name", "company_website"]);
|
package/src/quiz-gate.ts
CHANGED
|
@@ -1,46 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* The PUBLIC QUIZ identity gate, expressed as config for the content-gate
|
|
7
|
-
* primitive. Like the GPT gate this is lead capture, not access control: every
|
|
8
|
-
* email is a valid visitor. Its job is to make the visitor KNOWN AT SSR so the
|
|
9
|
-
* page's identity blocks hide themselves and the quiz blocks reveal themselves
|
|
10
|
-
* — by block condition on `initialVisitor`, never by a component deciding which
|
|
11
|
-
* screen to render.
|
|
12
|
-
*
|
|
13
|
-
* That is the whole point of this file existing. The quiz identity form used to
|
|
14
|
-
* be a composite React component that hand-assembled a logo, a heading, three
|
|
15
|
-
* inputs and a button, and then branched in TypeScript on whether a visitor was
|
|
16
|
-
* known. Every one of those pieces is already a Puck block, and the branch is
|
|
17
|
-
* already expressible as a block condition — what was missing was only the
|
|
18
|
-
* server half: something to turn a submitted email into a visitor the next
|
|
19
|
-
* render can see. This is that half, and it is 60 lines of config because the
|
|
20
|
-
* engine (content-gate/gate.ts + access.ts) already does the work.
|
|
21
|
-
*
|
|
22
|
-
* Flow: the composed form POSTs {quizId, name, email, phone} to
|
|
23
|
-
* /api/quiz-access (openCapture → any email matches); the access endpoint signs
|
|
24
|
-
* a `quiz_token` cookie and redirects back. On the next request runContentGate
|
|
25
|
-
* verifies it for this assessment and seeds resolved.initialVisitor.
|
|
26
4
|
*/
|
|
27
5
|
|
|
28
6
|
import type { AccessConfig, ContentGateConfig } from "./content-gate/config-types.ts";
|
|
29
|
-
import { signAccessToken, verifyAccessTokenIdentity } from "
|
|
7
|
+
import { signAccessToken, verifyAccessTokenIdentity } from "@teamscala/agreement-generation/content-gate-token";
|
|
30
8
|
import { quizTtlMs } from "./gate-ttls.ts";
|
|
31
9
|
|
|
32
10
|
const QUIZ_COOKIE = "quiz_token";
|
|
33
|
-
/**
|
|
34
|
-
* The ENTRY MARKER: proves this request is the redirect the identity submit
|
|
35
|
-
* produced, not a later load. Operator ruling 2026-09-14: a refresh must return
|
|
36
|
-
* to the identity gate — the visitor starts at Name/Email on every fresh load,
|
|
37
|
-
* and only the submit's own redirect releases the quiz. The marker is set by
|
|
38
|
-
* the access endpoint with the token and cleared by the render it authorized.
|
|
39
|
-
*/
|
|
40
11
|
const QUIZ_ENTRY_COOKIE = "quiz_token_entered";
|
|
41
12
|
const QUIZ_ACCESS_PATH = "/api/quiz-access";
|
|
42
13
|
|
|
43
|
-
/** The assessment this page is for, from the quiz-with-brand loader's graph. */
|
|
44
14
|
function quizId(resolved: Record<string, unknown>): string {
|
|
45
15
|
const quiz = resolved.quiz as { id?: unknown } | null | undefined;
|
|
46
16
|
return typeof quiz?.id === "string" ? quiz.id : "";
|
|
@@ -1,54 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Builds the shared React/Puck document envelope and substitutes the current
|
|
7
|
-
* content-hashed asset URLs from `dist/client/asset-manifest.json`.
|
|
8
|
-
*
|
|
9
|
-
* The envelope is runtime-owned. UI services are Puck compositions and must
|
|
10
|
-
* not carry or generate a per-service `src/index.html`; requiring that file
|
|
11
|
-
* creates a second rendering surface that can disappear independently of the
|
|
12
|
-
* Hono + React/Puck runtime.
|
|
13
|
-
*
|
|
14
|
-
* Build-time produces hashed filenames (e.g. `styles-abc123.css`); the
|
|
15
|
-
* static-asset handler serves them with `cache-control: immutable`. New
|
|
16
|
-
* deployments emit new hashes → new URLs → browsers refetch without ever
|
|
17
|
-
* holding a stale copy. Without the substitution, a fixed
|
|
18
|
-
* `/assets/styles.css` paired with `immutable` would pin every visitor to
|
|
19
|
-
* whatever version they first downloaded.
|
|
20
|
-
*
|
|
21
|
-
* Cached per service process, keyed on the manifest's mtime. The cache MUST
|
|
22
|
-
* invalidate when `asset-manifest.json` changes: `buildClientAssets` can
|
|
23
|
-
* rebuild the bundle AFTER the first request — producing a new content hash,
|
|
24
|
-
* rewriting the manifest, AND deleting the old hashed bundle
|
|
25
|
-
* (`removeStaleHashedFiles`). A lifetime-pinned HTML cache then keeps serving
|
|
26
|
-
* the OLD hash, which now 404s → hydration never runs → the page renders SSR
|
|
27
|
-
* HTML but is dead client-side (the doc-editor overlay never mounts; scala-docs
|
|
28
|
-
* 2026-06-13, exposed by the ui-foundation publish-storm's repeated rebuilds).
|
|
29
|
-
* Re-stat'ing the tiny manifest per request is negligible (OS-cached, no
|
|
30
|
-
* network); the substituted HTML is reused while the mtime is unchanged.
|
|
31
|
-
*
|
|
32
|
-
* NO SERVED HTML WITHOUT A VERIFIED MANIFEST: when the manifest is absent,
|
|
33
|
-
* unreadable, or names files that do not exist, this module THROWS rather
|
|
34
|
-
* than emitting HTML — the fixed-name fallback (`/assets/styles.css`,
|
|
35
|
-
* `/assets/hydrate.js`) it once served names files NOTHING builds, so a
|
|
36
|
-
* 200 page with those URLs was a stylesheet 404 wearing a working page's
|
|
37
|
-
* clothes. Measured 2026-09-01: proposals.scala.business served a
|
|
38
|
-
* client-facing proposal unstyled while Railway reported the deploy
|
|
39
|
-
* SUCCESS. The readiness flip now gates on `verifyClientBuildAssets`
|
|
40
|
-
* (build-client-assets/manifest.ts), so this throw is defence in depth for
|
|
41
|
-
* a manifest that vanishes mid-process; the page-server render catch turns
|
|
42
|
-
* it into the branded error page, never a 200 with dead URLs.
|
|
43
|
-
*
|
|
44
|
-
* NO-SILENT-REVERTS: when the manifest is a TOMBSTONE (the last client build
|
|
45
|
-
* failed), the served HTML is stamped with a `<meta name="x-client-build-status"
|
|
46
|
-
* content="failed">` tag in <head> so any operator / chrome-devtools / theme
|
|
47
|
-
* probe inspecting the page can see the served bundle is STALE (a published fix
|
|
48
|
-
* that reached node_modules but failed to bundle is silently served from the
|
|
49
|
-
* prior bundle otherwise — scala-agents 2026-07-18, the white-on-white toolbar
|
|
50
|
-
* regression that looked "not fixed" while the bundle kept serving pre-fix
|
|
51
|
-
* bytes across restarts).
|
|
52
4
|
*/
|
|
53
5
|
|
|
54
6
|
import { createCache } from "@teamscala/cache/create-cache";
|
package/src/readiness-gate.ts
CHANGED
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Readiness gate — returns 503 + Retry-After: 1 until the boot pipeline
|
|
7
|
-
* flips the ready flag true as its last step. Registered as the FIRST
|
|
8
|
-
* middleware in both UI and backend boot paths per readiness-gated-boot.
|
|
9
|
-
*
|
|
10
|
-
* The gate fails open: if it ever throws, the request passes through to
|
|
11
|
-
* the normal handler rather than producing a hard 500.
|
|
12
4
|
*/
|
|
13
5
|
|
|
14
6
|
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
@@ -1,16 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Walks `service_install.api_routes[]` and binds each entry to its
|
|
7
|
-
* registered handler (via mountRegistry.handlers, populated by server_packages
|
|
8
|
-
* during their mount phase). Unresolved handler_slug values are fatal — the
|
|
9
|
-
* service cannot boot with declared routes pointing at nothing.
|
|
10
|
-
*
|
|
11
|
-
* Injects x-remote-port header from Bun's getConnInfo so downstream
|
|
12
|
-
* handlers (MCP route) can resolve caller identity via process-tree
|
|
13
|
-
* walking when explicit caller headers are absent.
|
|
14
4
|
*/
|
|
15
5
|
|
|
16
6
|
import type { ServiceInstallConfig } from "./load-service-install-row";
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Registers process-level crash handlers (unhandledRejection, uncaughtException)
|
|
7
|
-
* for a given service. Each handler logs the error with the service slug for
|
|
8
|
-
* identification. Per constitution crash-isolation.
|
|
9
4
|
*/
|
|
10
5
|
|
|
11
6
|
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit directly
|
|
5
|
-
*
|
|
6
|
-
* Page route registration — reads `page_route` registry rows for the
|
|
7
|
-
* current service and delegates to `registerRegistryPageRoute`.
|
|
8
|
-
*
|
|
9
|
-
* All routes are registry-driven (Phase 1): each `page_route` row pairs
|
|
10
|
-
* a URL pattern with a `resource_loader` graph that resolves data in
|
|
11
|
-
* wave-parallel via `resolveFromLoaderConfig`. No deploy-time shell
|
|
12
|
-
* variant routes — the old `service_install.page_routes[]` Phase 2 path
|
|
13
|
-
* was removed when every active service migrated to registry-driven
|
|
14
|
-
* `page_route` rows (all 9 services carry empty arrays).
|
|
15
4
|
*/
|
|
16
5
|
|
|
17
6
|
import { BootFatalError } from "@teamscala/os/errors/boot-error";
|
|
@@ -1,31 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Registers the system server-client for page-server SSR rendering. Composes
|
|
7
|
-
* the platform-owned routers into the service's generated `appRouter` and
|
|
8
|
-
* stashes the resulting client via setServerClient. Called once at boot before
|
|
9
|
-
* any page route handler fires.
|
|
10
|
-
*
|
|
11
|
-
* Handwritten because no machine-readable contract generates it: this is the
|
|
12
|
-
* EXECUTOR half of a rows+executor pair (`business-logic-is-rows-plus-executors`),
|
|
13
|
-
* whose declared half is the `service_install`/`service_schema` rows read at
|
|
14
|
-
* boot. There is no supplier spec, CLI or registry row that could emit this
|
|
15
|
-
* boot-time wiring, so it stays hand-written; the row half remains the source.
|
|
16
4
|
*/
|
|
17
5
|
|
|
18
|
-
// REFUSE, DO NOT SKIP. `existsSync(routerPath)` being false used to fall straight through with appRouter=null and still register a client + log
|
|
19
|
-
// success. That client carries only the three platform routers below and NO CRUD models, which is indistinguishable from a healthy one until a
|
|
20
|
-
// page asks it for a model — at which point resolve-from-loader-config throws MissingLoaderModelCapabilityError, far from the actual fault.
|
|
21
|
-
//
|
|
22
|
-
// Measured 2026-08-20: scala-quiz and scala-forms served 500s on every data-backed page while /health returned 200 and this line logged "ORPC
|
|
23
|
-
// serverClient registered". The build had produced no generated router (its railpack.json ran registry codegen against an unreachable
|
|
24
|
-
// *.railway.internal host), and nothing anywhere said so.
|
|
25
|
-
//
|
|
26
|
-
// A service whose service_schema DECLARES models and has no generated router is not degraded, it is non-functional — so this fails the boot
|
|
27
|
-
// rather than reporting a state it has not verified (`a-component-may-not-report-a-state-it-has-not-verified`). A service
|
|
28
|
-
// that declares no models (daemon/portal shells owning no DB schema) still passes through cleanly.
|
|
29
6
|
|
|
30
7
|
import { join } from "node:path";
|
|
31
8
|
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
@@ -43,16 +20,6 @@ export async function registerPageServerClient(
|
|
|
43
20
|
};
|
|
44
21
|
const { createRouterClient } = await import("@orpc/server");
|
|
45
22
|
const { getPrisma } = await import("@teamscala/db/prisma-registry");
|
|
46
|
-
// Load the composed CRUD router via getAppRouter() — the SAME registered
|
|
47
|
-
// instance the /api/rpc mount uses (mount-orpc-router.ts). The previous code
|
|
48
|
-
// did a raw second `import(cwd + "prisma/generated/orpc/routers/index.ts")`,
|
|
49
|
-
// re-loading the generated routers under a different module specifier than
|
|
50
|
-
// the mount already used; in the full boot process that re-execution throws,
|
|
51
|
-
// and the throw was swallowed by a SILENT catch — leaving the page-server
|
|
52
|
-
// client without the CRUD models, so every data-backed page 500s
|
|
53
|
-
// ("client.<model> is undefined", the scala-sign prod outage). existsSync
|
|
54
|
-
// gates the legitimate no-router case (daemon/portal services that own no DB
|
|
55
|
-
// schema) so it stays a clean skip, never a misleading warn.
|
|
56
23
|
const { existsSync } = await import("node:fs");
|
|
57
24
|
const routerPath = join(
|
|
58
25
|
process.cwd(),
|
|
@@ -64,8 +31,6 @@ export async function registerPageServerClient(
|
|
|
64
31
|
const { getAppRouter } = await import("@teamscala/orpc/app-router");
|
|
65
32
|
appRouter = ((await getAppRouter()) as Record<string, unknown>) ?? null;
|
|
66
33
|
} catch (err) {
|
|
67
|
-
// A genuine load failure here means every data-backed page 500s, so it
|
|
68
|
-
// must surface, never be swallowed (protective-layers-fail-explicitly).
|
|
69
34
|
log.warn(
|
|
70
35
|
`[scala-install:${serviceSlug}] appRouter load failed — data-backed pages will 500: ${err instanceof Error ? `${err.message}\n${err.stack ?? ""}` : String(err)}`,
|
|
71
36
|
);
|
|
@@ -73,28 +38,6 @@ export async function registerPageServerClient(
|
|
|
73
38
|
}
|
|
74
39
|
const declaredModels = await (async (): Promise<string[]> => {
|
|
75
40
|
const capSlug = toCapabilitySlug(serviceSlug);
|
|
76
|
-
// LOOK IT UP BY CAPABILITY, NOT BY DEPLOYABLE NAME. A service_schema
|
|
77
|
-
// row describes the APPLICATION's data surface, so there is ONE row
|
|
78
|
-
// for both lanes; the dev lane arrives here as
|
|
79
|
-
// `scala-projects-bun-dev`. Both sides normalize through
|
|
80
|
-
// toCapabilitySlug, so the join is tier-agnostic by construction.
|
|
81
|
-
// See ./capability-slug.ts.
|
|
82
|
-
//
|
|
83
|
-
// READ WITH THE REAL SCHEMA, NOT `undefined`. The previous call passed
|
|
84
|
-
// `undefined as never`, so v.parse(undefined, raw) threw a TypeError
|
|
85
|
-
// (schema is not a schema) on EVERY service, the bare catch below turned
|
|
86
|
-
// that into "declares nothing", and the refusal at the bottom NEVER
|
|
87
|
-
// fired — a service declaring 18 models booted "0 declared model(s)"
|
|
88
|
-
// and served 500s. (measured scala-sign 2026-09-24/25: row declares 18,
|
|
89
|
-
// boot logged 0.) loadOptionalRegistryConfig parses against
|
|
90
|
-
// ServiceSchemaConfigSchema and returns null ONLY for a missing row.
|
|
91
|
-
//
|
|
92
|
-
// A MISSING ROW is legitimately "declares nothing" (daemon/portal
|
|
93
|
-
// shells own no DB models) and passes through clean. A READ FAILURE is
|
|
94
|
-
// NOT "declares nothing" — it fails the boot naming the error, so the
|
|
95
|
-
// database can never quietly become "no models" and let a router-less,
|
|
96
|
-
// data-backed service boot and serve 500s
|
|
97
|
-
// (`a-component-may-not-report-a-state-it-has-not-verified`).
|
|
98
41
|
const { loadOptionalRegistryConfig } = await import(
|
|
99
42
|
"@teamscala/db/registry/load-config"
|
|
100
43
|
);
|
|
@@ -145,8 +88,6 @@ export async function registerPageServerClient(
|
|
|
145
88
|
const { PlatformIdentitySchema } = await import(
|
|
146
89
|
"@teamscala/db-validation/registry-schemas/platform-identity"
|
|
147
90
|
);
|
|
148
|
-
// Platform org id is a registry value (registries-are-source-of-truth),
|
|
149
|
-
// not a hardcoded literal — read from config/platform-identity.
|
|
150
91
|
const platformIdentity = await loadRegistryConfig(
|
|
151
92
|
"config",
|
|
152
93
|
"platform-identity",
|
|
@@ -166,19 +107,13 @@ export async function registerPageServerClient(
|
|
|
166
107
|
} as never,
|
|
167
108
|
);
|
|
168
109
|
mod.setServerClient(client);
|
|
169
|
-
// Record declared-vs-present capability for /health. Property lookups only.
|
|
170
110
|
(mod as unknown as { recordServerClientCapability?: (m: readonly string[]) => void })
|
|
171
111
|
.recordServerClientCapability?.(declaredModels);
|
|
172
|
-
// State the model count. "registered" alone was true in the outage and told
|
|
173
|
-
// nobody anything; a zero here is now visible in the boot log.
|
|
174
112
|
log.info(
|
|
175
113
|
`[scala-install:${serviceSlug}] ORPC serverClient registered (system:super-admin) — ` +
|
|
176
114
|
`${Object.keys(composed).length} router(s), ${declaredModels.length} declared model(s)`,
|
|
177
115
|
);
|
|
178
116
|
} catch (err) {
|
|
179
|
-
// RETHROW. Swallowing this into a warn is what let a service boot, pass its
|
|
180
|
-
// healthcheck and serve 500s on every page. The boot must fail so the
|
|
181
|
-
// supervisor and the deploy healthcheck can see it.
|
|
182
117
|
log.error(
|
|
183
118
|
`[scala-install:${serviceSlug}] serverClient registration FAILED — data-backed pages cannot render: ${err instanceof Error ? `${err.message}\n${err.stack ?? ""}` : String(err)}`,
|
|
184
119
|
);
|
|
@@ -1,33 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* SSR-prefetch the signed-in user's AI-chat conversation so a click-opened AI
|
|
7
|
-
* panel renders the real conversation on first paint. Extracted from
|
|
8
|
-
* register-registry-page-route.ts (single-purpose-file split).
|
|
9
|
-
*/
|
|
10
|
-
/**
|
|
11
|
-
* SSR-prefetch the signed-in user's AI-chat conversation so a CLICK-opened AI
|
|
12
|
-
* panel renders the real conversation on first paint — never a loading gate
|
|
13
|
-
* (ssr-then-live-handoff). The AiChat block lives on the /ai PANEL page, which
|
|
14
|
-
* isn't rendered when the panel is closed, so its block serverLoader can't run
|
|
15
|
-
* at page-load; this prefetches server-side (regardless of panel open-state) +
|
|
16
|
-
* stamps pageData.aiChatSession, which AiChat reads on mount. The agent is
|
|
17
|
-
* resolved from the /ai panel page's own AiChat block (single source of truth —
|
|
18
|
-
* no duplicate config to drift). Fired in parallel with the post-processors +
|
|
19
|
-
* bounded by their window; a failure degrades silently to the client-side
|
|
20
|
-
* createOrRestore fallback, never an error page.
|
|
21
4
|
*/
|
|
22
5
|
|
|
23
6
|
import { getServerClient } from "../page-server-orpc/server-client.ts";
|
|
24
7
|
|
|
25
|
-
/**
|
|
26
|
-
* Deep-find the serving `website_id` in the loader graph (resolved). The
|
|
27
|
-
* Utility/Portal shell carries the website id (not a slug) under a loader
|
|
28
|
-
* source key; the client RegionPageSwap reads it from loaderData.*.website_id
|
|
29
|
-
* the same way. Bounded walk (cycle-safe) over the resolved loader results.
|
|
30
|
-
*/
|
|
31
8
|
function findWebsiteId(root: unknown): string | undefined {
|
|
32
9
|
if (!root || typeof root !== "object") return undefined;
|
|
33
10
|
const seen = new Set<unknown>();
|
|
@@ -1,75 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* The three brand-override paths of a registered page route: the /sign-in
|
|
7
|
-
* callback-org override, the signed-in session-org personalisation
|
|
8
|
-
* (platform pages only), and the brand_org_path resource-owner override.
|
|
9
|
-
*
|
|
10
|
-
* Split out of register-registry-page-route.ts (924 lines) — the loadData
|
|
11
|
-
* closure segments moved byte-identical; only the closure variables became
|
|
12
|
-
* explicit parameters.
|
|
13
4
|
*/
|
|
14
5
|
|
|
15
|
-
/**
|
|
16
|
-
* /sign-in brand override: a shared org-scoped link 302→/sign-in for the
|
|
17
|
-
* unauthed crawler (WhatsApp), which saw the service's default website
|
|
18
|
-
* brand (the platform owner). The callbackURL carries the destination org
|
|
19
|
-
* — override resolved.brand (before the title/meta/og are built from it)
|
|
20
|
-
* so the page reflects the org the visitor is signing in to. Scoped to
|
|
21
|
-
* /sign-in; resource pages keep their own brand.
|
|
22
|
-
*
|
|
23
|
-
* The org is segment 1 of the canonical portal shape /<websiteId>/<orgId>/
|
|
24
|
-
* <path>, NOT segment 0 (the website id): reading segment 0 fed a website id
|
|
25
|
-
* into fetchOrganisationPageBranding, which resolved no org and left the
|
|
26
|
-
* default (super-admin) brand — the cross-tenant mis-branding this override
|
|
27
|
-
* exists to prevent (portal-branding-resolves-page-org-first, 2026-07-21).
|
|
28
|
-
* Shared helper so this and bind-page-handler cannot drift on the answer.
|
|
29
|
-
*/
|
|
30
6
|
|
|
31
|
-
/**
|
|
32
|
-
* brand-is-org-owned-full-scheme, reconciled with page-org-first
|
|
33
|
-
* (portal-branding-resolves-page-org-first): the session override now
|
|
34
|
-
* personalizes ONLY platform pages (page org === website owning-org).
|
|
35
|
-
* Tenant pages keep their resolved page-org brand — the page is
|
|
36
|
-
* authoritative, the menu switcher changes which tenant's pages you
|
|
37
|
-
* view. Anonymous/public (no session) keeps the resolved brand. Same
|
|
38
|
-
* fetchOrganisationPageBranding pattern as the /sign-in brand override.
|
|
39
|
-
*
|
|
40
|
-
* A PUBLIC PAGE IS NEVER PERSONALISED TO THE READER'S ORG. Personalising
|
|
41
|
-
* the brand is right for a portal — you are looking at your own
|
|
42
|
-
* workspace. It is wrong for a public document, which belongs to the org
|
|
43
|
-
* that PUBLISHED it and must look identical to every reader.
|
|
44
|
-
*
|
|
45
|
-
* Measured 2026-08-17: the Launcha Authority Blueprint, owned by GMG
|
|
46
|
-
* APAC, rendered correctly for an anonymous customer and in GBG Indo's
|
|
47
|
-
* navy with the scala logo for any signed-in staff member — so the two
|
|
48
|
-
* people checking it before it went to clients were the only two who
|
|
49
|
-
* could not see what a client sees. Silent, and inverted: the reviewers
|
|
50
|
-
* got the wrong page.
|
|
51
|
-
*/
|
|
52
7
|
|
|
53
|
-
/**
|
|
54
|
-
* BRAND FROM THE ORG THAT OWNS THE RESOURCE, not the website's owner.
|
|
55
|
-
* A public resource page (an assessment report, a shared document) is
|
|
56
|
-
* hosted on a shared website but BELONGS to a tenant. resolvePageBrand
|
|
57
|
-
* reads pageData.orgId, which is the WEBSITE's org, so every tenant's
|
|
58
|
-
* report rendered the host org's logo and palette.
|
|
59
|
-
*
|
|
60
|
-
* Measured 2026-08-17: the Launcha Authority Blueprint is owned by GMG
|
|
61
|
-
* APAC (#FF4500, launcha logo) and rendered with GBG Indo's navy and
|
|
62
|
-
* scala mark, because both pages live on the shared "Public Pages"
|
|
63
|
-
* site. The loader had already resolved the right org — nothing
|
|
64
|
-
* consumed it.
|
|
65
|
-
*
|
|
66
|
-
* `brand_org_path` names where the owning org id lives in the resolved
|
|
67
|
-
* loader data (e.g. "quiz.organisation_id"), so which resource owns the
|
|
68
|
-
* brand is a ROW, never a per-service branch
|
|
69
|
-
* (`business-logic-is-rows-plus-executors`). Absent or unresolvable,
|
|
70
|
-
* the website's org stands — this can only ever narrow the brand to a
|
|
71
|
-
* more specific owner, never blank it.
|
|
72
|
-
*/
|
|
73
8
|
|
|
74
9
|
import { fetchOrganisationPageBranding } from "@teamscala/db/fetch-organisation-page-branding";
|
|
75
10
|
import { getPlatformOrgId } from "@teamscala/db/registry/platform";
|
|
@@ -86,9 +21,6 @@ function spreadBranding(
|
|
|
86
21
|
return {
|
|
87
22
|
...existing,
|
|
88
23
|
companyName: branding.company_name,
|
|
89
|
-
// Friendly name is the customer-facing one; falling back to the
|
|
90
|
-
// legal entity fed "GMG APAC" to every consumer that correctly
|
|
91
|
-
// read companyFriendlyName (e.g. logoAlt in the Puck host codegen).
|
|
92
24
|
companyFriendlyName:
|
|
93
25
|
branding.company_friendly_name ?? branding.company_name,
|
|
94
26
|
primaryColor: branding.brand_primary_color ?? existing?.primaryColor,
|
|
@@ -126,7 +58,6 @@ export async function applySignInBrandOverride(
|
|
|
126
58
|
}
|
|
127
59
|
}
|
|
128
60
|
} catch {
|
|
129
|
-
// Branding fetch failed — keep the default brand (no regression).
|
|
130
61
|
}
|
|
131
62
|
}
|
|
132
63
|
|
|
@@ -140,31 +71,9 @@ export async function applySessionOrgBranding(
|
|
|
140
71
|
renderMode !== "public" &&
|
|
141
72
|
sessionOrgId &&
|
|
142
73
|
sessionOrgId !== (pageData.orgId as string | undefined) &&
|
|
143
|
-
// Page-org-first (portal-branding-resolves-page-org-first): the
|
|
144
|
-
// session override personalizes ONLY the shared PLATFORM website
|
|
145
|
-
// (super-admin org). A TENANT-owned site (a customer GPT/quiz/portal)
|
|
146
|
-
// keeps its page-org brand regardless of the viewer session org. The
|
|
147
|
-
// old pageData.orgId===website.org mis-fired for a tenant whose page org equals their own website org.
|
|
148
74
|
platformOrgId ===
|
|
149
75
|
((pageData.website as { organisation_id?: string | null } | undefined)
|
|
150
76
|
?.organisation_id ?? undefined) &&
|
|
151
|
-
// ...AND the PAGE itself must be platform-owned. A platform-owned
|
|
152
|
-
// WEBSITE also hosts tenant DOCUMENTS — an agreement, a proposal, a
|
|
153
|
-
// signed contract — and those belong to the org that published them.
|
|
154
|
-
// Testing only the website let the viewer's own org repaint a tenant
|
|
155
|
-
// document, which is the same defect this rule already fixed once at
|
|
156
|
-
// the website level (2026-08-17, the Launcha Authority Blueprint).
|
|
157
|
-
//
|
|
158
|
-
// It recurred on sign.scala.business, whose website IS owned by the
|
|
159
|
-
// platform org (c503629a), so the website test passed and every
|
|
160
|
-
// signed-in staff viewer saw a GMG APAC agreement painted in their own
|
|
161
|
-
// org's brand — while an anonymous client saw it correctly. That is
|
|
162
|
-
// the worst possible shape: the only people who cannot see what the
|
|
163
|
-
// client sees are the people reviewing it before it is sent.
|
|
164
|
-
//
|
|
165
|
-
// A public document belongs to the org that PUBLISHED it and must look
|
|
166
|
-
// identical to every reader, so a page whose org is not the platform's
|
|
167
|
-
// is never personalised.
|
|
168
77
|
platformOrgId === (pageData.orgId as string | undefined)
|
|
169
78
|
) {
|
|
170
79
|
try {
|
|
@@ -178,7 +87,6 @@ export async function applySessionOrgBranding(
|
|
|
178
87
|
pageData.brand = spreadBranding(existing, branding);
|
|
179
88
|
}
|
|
180
89
|
} catch {
|
|
181
|
-
// Branding fetch failed — keep the default brand (no regression).
|
|
182
90
|
}
|
|
183
91
|
}
|
|
184
92
|
}
|
|
@@ -202,12 +110,6 @@ export async function applyBrandOrgPath(
|
|
|
202
110
|
}
|
|
203
111
|
if (typeof cursor === "string" && cursor.length > 0) {
|
|
204
112
|
pageData.orgId = cursor;
|
|
205
|
-
// orgId alone is not enough. resolvePageBrand fills only the
|
|
206
|
-
// accent/secondary/muted gaps from the org; primaryColor, logo and
|
|
207
|
-
// companyName come from pageData.brand, which was built from the
|
|
208
|
-
// website's org. Replace those too, the same way the session
|
|
209
|
-
// override above does, or the page keeps the host's logo while
|
|
210
|
-
// claiming the tenant's org.
|
|
211
113
|
try {
|
|
212
114
|
const owner = await fetchOrganisationPageBranding(
|
|
213
115
|
getServerClient() as never,
|
|
@@ -218,7 +120,6 @@ export async function applyBrandOrgPath(
|
|
|
218
120
|
pageData.brand = spreadBranding(existing, owner);
|
|
219
121
|
}
|
|
220
122
|
} catch {
|
|
221
|
-
// Branding fetch failed — keep the website brand (no regression).
|
|
222
123
|
}
|
|
223
124
|
} else {
|
|
224
125
|
log.warn(
|
|
@@ -1,19 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Summarises the resource_loader config a route captured at boot, for the
|
|
7
|
-
* route-registration log line. Registration reads the loader row ONCE into
|
|
8
|
-
* the route closure, so an edit to the row silently does not reach the
|
|
9
|
-
* running service (measured 2026-08-18: a footer region added to a report
|
|
10
|
-
* produced no footer and no diagnostic until a cold wake). Naming the
|
|
11
|
-
* captured shape in the boot log makes the row-vs-running-service gap a
|
|
12
|
-
* one-glance check in the service logs, and the restart contract is stated
|
|
13
|
-
* where the operator will be looking when the edit "doesn't work".
|
|
14
4
|
*/
|
|
15
5
|
|
|
16
|
-
/** Structural slice of the ResourceLoaderConfigSchema output this unit reads. */
|
|
17
6
|
interface LoaderConfigSlice {
|
|
18
7
|
sources?: { key: string }[];
|
|
19
8
|
region_slots?: string[];
|