@teamscala/service-boot 0.23.413 → 0.23.414
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 +57 -1
- package/src/register-registry-page-route/apply-page-branding.ts +207 -0
- package/src/register-registry-page-route/meta-title.ts +15 -0
- package/src/register-registry-page-route/prefetch-org-id.ts +38 -0
- package/src/register-registry-page-route/resolve-panel-page.ts +45 -0
- package/src/register-registry-page-route/resolve-request-graph.ts +170 -0
- package/src/register-registry-page-route/run-content-gates.ts +80 -0
- package/src/register-registry-page-route/stamp-session-context.ts +121 -0
- package/src/register-registry-page-route/warn-uncovered-blocks.ts +64 -0
- package/src/register-registry-page-route.ts +64 -599
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system ui-service-boot
|
|
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
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { getServerClient } from "../page-server-orpc/server-client.ts";
|
|
16
|
+
|
|
17
|
+
export interface SessionIdentity {
|
|
18
|
+
sessionUserId: string | undefined;
|
|
19
|
+
sessionOrgId: string | undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function stampSessionContext(
|
|
23
|
+
resolved: Record<string, unknown>,
|
|
24
|
+
request: Request,
|
|
25
|
+
): Promise<SessionIdentity> {
|
|
26
|
+
const gateCookie = request.headers.get("cookie");
|
|
27
|
+
// Resolve the signed-in viewer from the unified auth session (local
|
|
28
|
+
// verify, works on Railway) and stamp it on the graph, so a gate can
|
|
29
|
+
// release on a standard OTP-login session (config.sessionAuthorize) —
|
|
30
|
+
// the unified path, no per-gate OTP code. Anonymous if no session.
|
|
31
|
+
let sessionUserId: string | undefined;
|
|
32
|
+
let sessionOrgId: string | undefined;
|
|
33
|
+
try {
|
|
34
|
+
const { getSession } = await import("@teamscala/auth/get-session.ts");
|
|
35
|
+
const sess = await getSession(request.headers);
|
|
36
|
+
if (sess?.user?.email) {
|
|
37
|
+
(resolved as { sessionVisitor?: { email: string; name?: string } }).sessionVisitor =
|
|
38
|
+
{
|
|
39
|
+
email: sess.user.email,
|
|
40
|
+
name: sess.user.name ?? undefined,
|
|
41
|
+
};
|
|
42
|
+
sessionUserId = sess.user.id;
|
|
43
|
+
sessionOrgId = sess.session?.activeOrganizationId ?? undefined;
|
|
44
|
+
// Stamp the per-request portal identity so block portal context
|
|
45
|
+
// (useBlockPortal().userId) is populated for authed users on
|
|
46
|
+
// org/hub pages — the SSR-side WRITER the connections-block reader
|
|
47
|
+
// (ClientBlockServicesProvider → __PAGE_DATA__.portalContext.user.id)
|
|
48
|
+
// depends on. Rides the same per-request path as sessionVisitor
|
|
49
|
+
// above (never the cached loader graph); merge preserves any
|
|
50
|
+
// portalContext fields set elsewhere (e.g. viewAsMembers). Without
|
|
51
|
+
// this, connections-block short-circuits at SignInPrompt.
|
|
52
|
+
const rc = resolved as {
|
|
53
|
+
portalContext?: {
|
|
54
|
+
user?: { id?: string } | null;
|
|
55
|
+
orgId?: string;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
rc.portalContext = {
|
|
59
|
+
...(rc.portalContext ?? {}),
|
|
60
|
+
user: sessionUserId ? { id: sessionUserId } : null,
|
|
61
|
+
orgId: sessionOrgId,
|
|
62
|
+
};
|
|
63
|
+
// Read pane widths from the portal cookie (the client writes
|
|
64
|
+
// it immediately on resize via setPortalCookie; the cookie is
|
|
65
|
+
// sent with every HTTP request so the SSR reads it here).
|
|
66
|
+
// This renders the correct pane width from the first byte —
|
|
67
|
+
// no post-hydration shift, no client-side state read.
|
|
68
|
+
const paneWidthsMatch = gateCookie?.match(
|
|
69
|
+
/portal_pane_widths=([^;]+)/,
|
|
70
|
+
);
|
|
71
|
+
if (paneWidthsMatch) {
|
|
72
|
+
try {
|
|
73
|
+
const parsed = JSON.parse(
|
|
74
|
+
decodeURIComponent(paneWidthsMatch[1]),
|
|
75
|
+
);
|
|
76
|
+
if (parsed.left)
|
|
77
|
+
(rc.portalContext as Record<string, unknown>).paneLeftWidth =
|
|
78
|
+
parsed.left;
|
|
79
|
+
if (parsed.right)
|
|
80
|
+
(rc.portalContext as Record<string, unknown>).paneRightWidth =
|
|
81
|
+
parsed.right;
|
|
82
|
+
} catch {
|
|
83
|
+
// Malformed cookie — fall back to defaults.
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (sessionUserId) {
|
|
87
|
+
try {
|
|
88
|
+
const dbClient = getServerClient() as unknown as {
|
|
89
|
+
user: {
|
|
90
|
+
findUnique: (args: {
|
|
91
|
+
where: { id: string };
|
|
92
|
+
select: { preferences: boolean };
|
|
93
|
+
}) => Promise<{ preferences: unknown } | null>;
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
const userRow = await dbClient.user.findUnique({
|
|
97
|
+
where: { id: sessionUserId },
|
|
98
|
+
select: { preferences: true },
|
|
99
|
+
});
|
|
100
|
+
const portalPrefs = (
|
|
101
|
+
(userRow?.preferences as Record<string, unknown> | null)
|
|
102
|
+
?.portal as Record<string, unknown> | undefined
|
|
103
|
+
);
|
|
104
|
+
if (portalPrefs?.paneLeftWidth) {
|
|
105
|
+
(rc.portalContext as Record<string, unknown>).paneLeftWidth =
|
|
106
|
+
portalPrefs.paneLeftWidth;
|
|
107
|
+
}
|
|
108
|
+
if (portalPrefs?.paneRightWidth) {
|
|
109
|
+
(rc.portalContext as Record<string, unknown>).paneRightWidth =
|
|
110
|
+
portalPrefs.paneRightWidth;
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
// Pane widths are non-critical — fall back to defaults.
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
// No auth factory / no session → anonymous; gates fall back to token.
|
|
119
|
+
}
|
|
120
|
+
return { sessionUserId, sessionOrgId };
|
|
121
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system ui-service-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* Block-coverage guard (protective-layers-fail-explicitly) for registered
|
|
7
|
+
* page routes.
|
|
8
|
+
*
|
|
9
|
+
* Split out of register-registry-page-route.ts (924 lines) — byte-for-byte
|
|
10
|
+
* move; only the import paths adjusted.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
14
|
+
import type { RendererComponentEntry } from "../types.ts";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A page that renders a block whose component is NOT registered — because
|
|
18
|
+
* its package is missing from service_install.config.block_packages —
|
|
19
|
+
* silently renders BLANK. That is the exact failure behind the 2026-06-04
|
|
20
|
+
* scala-sign regression (sign-in-block dropped from block_packages → the
|
|
21
|
+
* gate + signature fields vanished) which left only a benign boot WARN.
|
|
22
|
+
* This turns that silent blank into a loud, GlitchTip-visible ERROR naming
|
|
23
|
+
* the page + block, throttled to once per (service, block type) so a hot
|
|
24
|
+
* path can never spam it.
|
|
25
|
+
*/
|
|
26
|
+
const uncoveredBlocksLogged = new Set<string>();
|
|
27
|
+
|
|
28
|
+
function collectBlockTypes(node: unknown, out: Set<string>): void {
|
|
29
|
+
if (Array.isArray(node)) {
|
|
30
|
+
for (const child of node) collectBlockTypes(child, out);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (node && typeof node === "object") {
|
|
34
|
+
const obj = node as Record<string, unknown>;
|
|
35
|
+
if (
|
|
36
|
+
typeof obj.type === "string" &&
|
|
37
|
+
obj.props &&
|
|
38
|
+
typeof obj.props === "object"
|
|
39
|
+
) {
|
|
40
|
+
out.add(obj.type);
|
|
41
|
+
}
|
|
42
|
+
for (const value of Object.values(obj)) collectBlockTypes(value, out);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function warnUncoveredBlocks(
|
|
47
|
+
pageData: unknown,
|
|
48
|
+
components: Record<string, RendererComponentEntry>,
|
|
49
|
+
serviceSlug: string,
|
|
50
|
+
path: unknown,
|
|
51
|
+
log: ReturnType<typeof getAppLogger>,
|
|
52
|
+
): void {
|
|
53
|
+
const used = new Set<string>();
|
|
54
|
+
collectBlockTypes(pageData, used);
|
|
55
|
+
for (const type of used) {
|
|
56
|
+
if (type in components) continue;
|
|
57
|
+
const key = `${serviceSlug}:${type}`;
|
|
58
|
+
if (uncoveredBlocksLogged.has(key)) continue;
|
|
59
|
+
uncoveredBlocksLogged.add(key);
|
|
60
|
+
log.error(
|
|
61
|
+
`[scala-install:${serviceSlug}] page "${String(path)}" renders block "${type}" but NO component is registered for it — its package is almost certainly missing from service_install.config.block_packages, so it renders BLANK. Add the owning @teamscala/* package to block_packages and restart. (block-coverage guard)`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|