@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.
@@ -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
+ }