@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,15 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* The auth gate's public-path surface: sign-in paths, the static-asset
|
|
7
|
-
* extension predicate, and the public infra prefixes/paths (self-authenticating
|
|
8
|
-
* service-to-service surfaces, dev-host infra reads, and the access-gate
|
|
9
|
-
* rejection target).
|
|
10
|
-
*
|
|
11
|
-
* Split out of auth-gate.ts (436 lines) — byte-for-byte move; import paths
|
|
12
|
-
* adjusted only.
|
|
13
4
|
*/
|
|
14
5
|
|
|
15
6
|
export const SIGN_IN_PATHS = new Set([
|
|
@@ -22,16 +13,6 @@ export const SIGN_IN_PATHS = new Set([
|
|
|
22
13
|
"/magic-link",
|
|
23
14
|
]);
|
|
24
15
|
|
|
25
|
-
/// A STATIC ASSET IS NEVER A PROTECTED RESOURCE, so it is decided by EXTENSION / rather than by remembering to list it.
|
|
26
|
-
/// / The allowlist below enumerates PREFIXES, and every path outside it is gated. / That makes a new public asset directory private BY DEFAULT, and the failure
|
|
27
|
-
/// lands exactly where it hurts most: on the SIGNED-OUT screen, which is the one / page guaranteed to need branding. `/assets/` was listed; `/logos/` never was,
|
|
28
|
-
/// so `/logos/cropped.png` answered 302 → /sign-in and the browser rendered a / blank logo under a "starting up" splash. Favicons are on the list ONE FILE AT
|
|
29
|
-
/// A TIME, which is the same defect already visible. / / This is why the operator reports it "keeps regressing": nothing is broken by
|
|
30
|
-
/// a code change, the allowlist simply never covered the path, and any build that / moves images re-creates it. Enumerating what is public is `prevention-over-
|
|
31
|
-
/// detection` inverted — the cure is a predicate, not a longer list. /
|
|
32
|
-
/// Redirecting an <img> to a login page is never useful behaviour either: the
|
|
33
|
-
/// browser cannot act on the redirect, so it silently paints nothing. Failing
|
|
34
|
-
/// closed on a page is protection; failing closed on an image is just a blank.
|
|
35
16
|
const STATIC_ASSET_EXTENSIONS = [
|
|
36
17
|
".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif", ".svg", ".ico", ".bmp",
|
|
37
18
|
".woff", ".woff2", ".ttf", ".otf", ".eot",
|
|
@@ -47,17 +28,7 @@ export const PUBLIC_INFRA_PREFIXES = [
|
|
|
47
28
|
"/assets/",
|
|
48
29
|
"/api/assets/",
|
|
49
30
|
"/api/auth/",
|
|
50
|
-
// fn.signInFlow procedures serve the unauthenticated sign-in flow (config
|
|
51
|
-
// load, session check, portal list) — anonymous-allowed by design (return {}
|
|
52
|
-
// for no session). They replaced the former /api/data/sign-in-flow-config,
|
|
53
|
-
// /check-session, /portals HANDLER_MAP entries (handler-map-retirement).
|
|
54
31
|
"/api/rpc/fn/signInFlow/",
|
|
55
|
-
// scala-enroll device-onboarding: the machine-facing installer + register
|
|
56
|
-
// routes are TOKEN-authenticated (a single-use enrollment token issued by a
|
|
57
|
-
// logged-in, dev_access-approved operator), fetched by the onboardee via
|
|
58
|
-
// `irm`/`curl` with NO session cookie — so they must skip the page login
|
|
59
|
-
// gate, exactly like /mcp (Bearer) and /api/rpc/ws self-authenticate. The
|
|
60
|
-
// TOKEN is the delegated capability; issuance (/issue) stays login-gated.
|
|
61
32
|
"/provision/",
|
|
62
33
|
];
|
|
63
34
|
|
|
@@ -67,81 +38,15 @@ export const PUBLIC_INFRA_PATHS = new Set([
|
|
|
67
38
|
"/favicon.svg",
|
|
68
39
|
"/api/session",
|
|
69
40
|
"/api/rpc/ws",
|
|
70
|
-
// The MCP endpoint is a service-to-service surface with its OWN Bearer auth
|
|
71
|
-
// (the gateway sends `Authorization: Bearer SCALA_DEV_KEY`, validated by the
|
|
72
|
-
// MCP route handler). It must never be session-page-gated: on a kind:ui
|
|
73
|
-
// service that also mounts an MCP server (scala-agents — the fleet
|
|
74
|
-
// orchestration + cross-pane channel), the auth-gate would 302 /mcp to
|
|
75
|
-
// /sign-in, so the gateway sees toolCount=0 and the whole channel goes
|
|
76
|
-
// degraded. Backend-only MCP hosts have no auth-gate, so this only bit the
|
|
77
|
-
// one UI MCP host once the localhost super-admin bypass was removed
|
|
78
|
-
// (the bypass had been masking it). Exempt alongside /api/rpc/ws and
|
|
79
|
-
// /api/auth/, which self-authenticate the same way.
|
|
80
41
|
"/mcp",
|
|
81
|
-
// build-id is polled by the client self-heal; it must always return JSON
|
|
82
|
-
// (a tiny bundle hash) regardless of session, never a /sign-in redirect —
|
|
83
|
-
// otherwise an expired-session tab would compare a redirect body to its
|
|
84
|
-
// booted build and never heal.
|
|
85
42
|
"/api/build-id",
|
|
86
|
-
// /_heap is the localhost-gated heap-attribution probe (register-static-assets);
|
|
87
|
-
// like /health it is infra, not a page, so it must never 302 to /sign-in on an
|
|
88
|
-
// auth-gated service. The probe's own localhost Host check is the real gate.
|
|
89
43
|
"/_heap",
|
|
90
|
-
// /api/fleet/events is the SSE fleet event stream consumed self-contained by
|
|
91
|
-
// the generated opencode scala-hooks plugin (and any future client without
|
|
92
|
-
// an @orpc/client dep) for native cross-pane channel delivery. The plugin
|
|
93
|
-
// runs in the opencode process with no scala-agents session cookie, so the
|
|
94
|
-
// route must be public; the stream is keyed by the orchestratorSessionId
|
|
95
|
-
// query param (the subscriber's own tmux pane id) and carries no sensitive
|
|
96
|
-
// data, and dispatch only emits events targeted at that identity.
|
|
97
44
|
"/api/fleet/events",
|
|
98
|
-
// /api/fleet/claude-sessions is a non-sensitive lane-identity read (active
|
|
99
|
-
// Claude lane sessionIDs + cwds) consumed by the session-picker daemon to
|
|
100
|
-
// RESTORE transcript tails after its own restart. The picker runs with no
|
|
101
|
-
// scala-agents session cookie; the data is less sensitive than the
|
|
102
|
-
// /api/fleet/events stream above (no message content), so it is public like
|
|
103
|
-
// the other dev-host infra reads.
|
|
104
45
|
"/api/fleet/claude-sessions",
|
|
105
|
-
// /api/fleet/ingest-event receives parsed lane-action events
|
|
106
|
-
// (message.part.updated etc.) from the cli-session transcript-tail — a
|
|
107
|
-
// dev-host INTERNAL machine caller that POSTs with no session cookie, exactly
|
|
108
|
-
// like the lane-event hook. Without this exemption the page auth-gate 302s
|
|
109
|
-
// every POST and the lane's tool/text action stream never reaches the
|
|
110
|
-
// ingestEvent pipeline. A first-class
|
|
111
|
-
// api_route (sibling to lane-event); security is the handler's loopback gate
|
|
112
|
-
// on the platform's x-caller-remote-address (set by register-api-routes).
|
|
113
46
|
"/api/fleet/ingest-event",
|
|
114
|
-
// /api/fleet/lane-event receives Claude Code http lifecycle hooks
|
|
115
|
-
// (Stop/UserPromptSubmit/Pre*ToolUse) for live-lane visibility. Like /mcp it
|
|
116
|
-
// is a service-to-service surface with its OWN Bearer auth (the hook sends
|
|
117
|
-
// `Authorization: Bearer SCALA_DEV_KEY`, validated by the handler); the hook
|
|
118
|
-
// runs in the Claude process with no scala-agents session cookie, so it must
|
|
119
|
-
// not be session-page-gated. The handler filters to fleet lanes by workspace
|
|
120
|
-
// cwd + resolves the lane via the session-picker, so an unauth'd caller
|
|
121
|
-
// writes no lane state.
|
|
122
47
|
"/api/fleet/lane-event",
|
|
123
|
-
// /api/fleet/sweep-executor receives the loga monitor engine's sweep dispatch
|
|
124
|
-
// (POST {slug}) -- a service-to-service surface like /api/fleet/lane-event /
|
|
125
|
-
// /api/fleet/ingest-event (loga -> scala-agents, localhost). Security is the
|
|
126
|
-
// handler's loopback gate (x-caller-remote-address), identical to its siblings.
|
|
127
48
|
"/api/fleet/sweep-executor",
|
|
128
|
-
// /api/terminal-ws is the in-browser terminal WebSocket bridge (a real PTY
|
|
129
|
-
// login shell). It is a dev-tool surface analogous to /api/fleet/events:
|
|
130
|
-
// the operator's own shell, no sensitive data beyond what the operator
|
|
131
|
-
// already has in their terminal. Must not be session-gated — the test
|
|
132
|
-
// harness and the terminal block both connect without a session cookie.
|
|
133
49
|
"/api/terminal-ws",
|
|
134
|
-
// /unauthorised is the access-gate's rejection target (the PageGuard access
|
|
135
|
-
// page). It MUST be exempt: an authed-but-not-allowed viewer is redirected
|
|
136
|
-
// here, so gating it would re-reject and loop. It renders no sensitive data.
|
|
137
50
|
"/unauthorised",
|
|
138
|
-
// The OpenAPI contract for the ORPC router — a service-to-service surface
|
|
139
|
-
// with its OWN Bearer auth (`Authorization: Bearer SCALA_DEV_KEY`,
|
|
140
|
-
// validated by the mount-orpc-router handler, fail-closed), exactly like
|
|
141
|
-
// /mcp and /api/fleet/lane-event above. The MCP gateway fetches it at boot
|
|
142
|
-
// (SCALA_OPENAPI_SPEC_URL) with no session cookie; without this exemption
|
|
143
|
-
// the page auth-gate 302s the fetch to /sign-in and the gateway reads an
|
|
144
|
-
// HTML sign-in page where the JSON document belongs (observed probing
|
|
145
|
-
// scala-portal.internal:5200/api/rpc/openapi.json on 2026-09-30).
|
|
146
51
|
"/api/rpc/openapi.json",
|
|
147
52
|
]);
|
package/src/auth-gate.ts
CHANGED
|
@@ -1,48 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Auth middleware factory. Three modes, same BetterAuth email-OTP mechanism:
|
|
7
|
-
*
|
|
8
|
-
* service_install.auth values:
|
|
9
|
-
* - omitted | "none" → no gate, no handler (scala-public, scala-sign)
|
|
10
|
-
* - "better-auth-cookie" → full session auth: mount handler + session gate
|
|
11
|
-
* - "email-otp-only" → mount handler (OTP send/verify), no session gate
|
|
12
|
-
*
|
|
13
|
-
* The sign-in block determines the UX per mode:
|
|
14
|
-
* - "full" → email → OTP → session → portal selection (portal, agents, docs, gpt)
|
|
15
|
-
* - "email-gate" → email → OTP → verify-only, no session (forms, proposals, quiz)
|
|
16
|
-
* - "capture-only" → email field only, no OTP (sign)
|
|
17
|
-
*
|
|
18
|
-
* There is NO localhost bypass: 127.0.0.1 is gated identically to the tunnel
|
|
19
|
-
* (no session → /sign-in). Tools needing an authed view sign in
|
|
20
|
-
* (getSuperAdminSessionCookie); the dev OTP is DB-readable.
|
|
21
|
-
*
|
|
22
|
-
* When SCALA_OTP_URL is set, /api/auth/* forwards to the centralised
|
|
23
|
-
* scala-otp service (per otp-centralised-via-otp-service). Otherwise
|
|
24
|
-
* the local better-auth handler is used (dev-host fallback).
|
|
25
4
|
*/
|
|
26
5
|
|
|
27
|
-
/**
|
|
28
|
-
* Point the event-log primitive at the process's Prisma singleton.
|
|
29
|
-
*
|
|
30
|
-
* Driver-agnostic `write` callback backed by the Prisma singleton
|
|
31
|
-
* (`one-db-surface` — no second postgres). Lazy, and it takes precedence over
|
|
32
|
-
* any `prisma:` configure, so this writer is authoritative for authed services.
|
|
33
|
-
* The previous form passed the ORPC server client (`getServerClient()`) cast
|
|
34
|
-
* `as never` as `prisma:` — that client has no `$executeRaw`, so `recordEvent`
|
|
35
|
-
* silently failed. Routing it through ORPC would reintroduce exactly that.
|
|
36
|
-
* Retention is owned by the event-log-sweep pg-boss job.
|
|
37
|
-
*
|
|
38
|
-
* EXTRACTED so its name states what it is. This is a `configured-primitives`
|
|
39
|
-
* INJECTION — handing a client to a primitive's configure() — not a query, and
|
|
40
|
-
* `no-direct-prisma-outside-orpc` cannot tell the two apart by AST. It exempts
|
|
41
|
-
* initialization, which is precisely what this is; buried inside
|
|
42
|
-
* `attachAuthGate` the call read as an arbitrary getPrisma() and blocked every
|
|
43
|
-
* unrelated edit to this file. Naming the boot step honestly is the fix. The
|
|
44
|
-
* registry refused a path exemption for it, correctly — `guard-exemptions-to-zero`.
|
|
45
|
-
*/
|
|
46
6
|
|
|
47
7
|
// Eager-warm the local BetterAuth runtime at boot whenever a request path
|
|
48
8
|
// will build it: the session-READ path (getSession, better-auth-cookie mode)
|
package/src/bind-page-handler.ts
CHANGED
|
@@ -1,25 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Calls a createPageHandler() handler with the per-request extras the
|
|
7
|
-
* SSR pipeline needs: the resolved userId/orgId (from localhost-bypass
|
|
8
|
-
* or better-auth-cookie), and the htmlTemplate. Shared by the regions
|
|
9
|
-
* and page_slug shell variants.
|
|
10
4
|
*/
|
|
11
5
|
|
|
12
|
-
// Per-page root.props.maxWidth (the canonical mechanism) centers any page at the chosen width via inline styles — already hydration-safe
|
|
13
|
-
// and page-scoped. Bind-page-handler MUST NOT inject unscoped CSS selectors: a global `.puck-root-slot { max-width }` rule shrinks
|
|
14
|
-
// every portal pane that hosts a Puck root (the 2026-06-17 dashboard sliver bug — `.puck-root-slot` matches every Puck page in every
|
|
15
|
-
// region, including the dashboard center pane). The `no-bind-page-handler-css-injection` guard enforces this; this comment is the load-bearing rationale.
|
|
16
|
-
// Live dashboards must never be served from a stale cache (intermediary, browser back-forward, mobile Safari aggressive caching). Force every
|
|
17
|
-
// SSR HTML response to be revalidated. Static assets (CSS/JS bundles) keep their hash-busted caching — those are content-addressed.
|
|
18
|
-
// no-transform: intermediaries must not mutate SSR HTML — Cloudflare's Email Address Obfuscation rewrites mailto: hrefs at the edge, which
|
|
19
|
-
// breaks the server-serializes-client-reads-verbatim hydration contract (React recoverable-error on every page with a mailto link). Per CF
|
|
20
|
-
// docs, Email Obfuscation does not apply when no-transform is present. NOTE: a shared-edge-caching (CDN max-age) variant shipped briefly on 2026-06-10
|
|
21
|
-
// and was REMOVED by operator ruling the same day ("the origin should be so fast we don't care") — the origin-speed work (SWR caches,
|
|
22
|
-
// wave-parallel loader, brotli) is the sanctioned path; re-adding shared-cache headers needs a fresh operator ruling.
|
|
23
6
|
|
|
24
7
|
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
25
8
|
import { withGateEntryClears } from "./content-gate/gate-entry-clears.ts";
|
|
@@ -48,34 +31,12 @@ export async function bindPageHandler(
|
|
|
48
31
|
let orgId: string | undefined;
|
|
49
32
|
let visitor: { id: string; name: string; email: string } | null = null;
|
|
50
33
|
|
|
51
|
-
// NO localhost super-admin bypass: identity comes ONLY from the session
|
|
52
|
-
// (the auth-gate already forces sign-in for authed services on every
|
|
53
|
-
// host, 127.0.0.1 included). The old `if (isLocalhost) userId="super-admin"`
|
|
54
|
-
// rendered authed pages as a privileged user on localhost regardless of
|
|
55
|
-
// who (if anyone) was signed in — so agent/tool verification on 127.0.0.1
|
|
56
|
-
// saw a view that diverged from the real signed-in user's. Tools that need
|
|
57
|
-
// an authed render sign in and carry the cookie (getSuperAdminSessionCookie).
|
|
58
34
|
|
|
59
|
-
// The .scala.business session cookie reaches EVERY app on the domain,
|
|
60
|
-
// so a signed-in platform user is identifiable at SSR regardless of
|
|
61
|
-
// this service's own auth mode. Resolving it per-request (never via
|
|
62
|
-
// the cross-request loader graph) lets identity-gated blocks
|
|
63
|
-
// (PublicGptBlock's SignInBlock gate) first-paint the real experience
|
|
64
|
-
// instead of a client-side "checking who you are" loading pass
|
|
65
|
-
// (ssr-then-live-handoff). authMode still governs AUTHORIZATION;
|
|
66
|
-
// this is read-only identity convenience.
|
|
67
35
|
const hasSessionCookie = /better-auth\.session_token=/.test(
|
|
68
36
|
c.req.header("cookie") ?? "",
|
|
69
37
|
);
|
|
70
38
|
|
|
71
39
|
if (authMode === "better-auth-cookie" || hasSessionCookie) {
|
|
72
|
-
// Visitor identity is an ENHANCEMENT: on auth:none services
|
|
73
|
-
// (scala-public et al.) no auth factory is registered, and a
|
|
74
|
-
// signed-in operator's domain-wide cookie reaches every
|
|
75
|
-
// *.scala.business site — getSession throwing "Auth factory not
|
|
76
|
-
// registered" 500'd every page for signed-in visitors
|
|
77
|
-
// (public-dev outage, 2026-06-11). Degrade to anonymous, never
|
|
78
|
-
// fail the render (block-and-provider-error-isolation).
|
|
79
40
|
try {
|
|
80
41
|
const { getSession } = await import("@teamscala/auth/get-session.ts");
|
|
81
42
|
const session = await getSession(c.req.raw.headers);
|
|
@@ -92,27 +53,11 @@ export async function bindPageHandler(
|
|
|
92
53
|
}
|
|
93
54
|
} catch (err) {
|
|
94
55
|
if (authMode === "better-auth-cookie") throw err;
|
|
95
|
-
// cookie-only enhancement path: anonymous render proceeds
|
|
96
56
|
}
|
|
97
57
|
}
|
|
98
58
|
|
|
99
|
-
// Anonymous dynamic routes (proposal/form/quiz/agent) resolve a
|
|
100
|
-
// resource whose owning org is known from the loader. That org owns
|
|
101
|
-
// the brand the block-level serverLoaders read — override the
|
|
102
|
-
// viewer's org (if any) so the SSR injects the resource owner's
|
|
103
|
-
// logo/colours, not the viewer's.
|
|
104
59
|
if (resourceOrgId) orgId = resourceOrgId;
|
|
105
60
|
|
|
106
|
-
// Unauthed /sign-in (no session, no resource) would otherwise resolve no org, and the brand resolver falls back to the super-admin org (GBG Indo)
|
|
107
|
-
// — so a link shared to org A (e.g. GMG APAC) previews as org B's brand on WhatsApp/crawlers, because the link 302→/sign-in for the unauthed crawler.
|
|
108
|
-
// The callbackURL carries the org the visitor is signing in to; derive orgId from it so the sign-in page's brand/title/og reflect the destination org.
|
|
109
|
-
// The canonical portal shape is /<websiteId>/<orgId>/<path> — the org is segment 1, NOT segment 0. Reading segment 0 (the website id) fed a website
|
|
110
|
-
// id into brand resolution, which resolved no org and fell back to the super-admin org — the exact cross-tenant mis-branding this block exists to
|
|
111
|
-
// fix, still live until 2026-07-21; violates portal-branding-resolves-page-org-first.
|
|
112
|
-
// scala-projects routes /<orgId>/… (single UUID) and proposal/form/quiz hosts
|
|
113
|
-
// route /<resourceId>/… (a non-org first segment) — both keep segment 0 via
|
|
114
|
-
// the helper's single-UUID fallback (resolves to no brand, falls back as
|
|
115
|
-
// before). The helper is the single authority shared with the route resolver.
|
|
116
61
|
if (!orgId) {
|
|
117
62
|
const cb = c.req.query("callbackURL") ?? "";
|
|
118
63
|
const resolved = extractOrgIdFromCallbackPath(cb);
|
|
@@ -132,10 +77,6 @@ export async function bindPageHandler(
|
|
|
132
77
|
}),
|
|
133
78
|
);
|
|
134
79
|
const headers = new Headers(resp.headers as HeadersInit);
|
|
135
|
-
// Gate-entry markers are ONE-VIEW state: the render that consumed one
|
|
136
|
-
// clears it here, so the next load (a refresh) is unauthorized and shows
|
|
137
|
-
// the gate again. Without this the marker would outlive the view that
|
|
138
|
-
// used it and every load would skip the gate.
|
|
139
80
|
for (const name of gateEntryClears) {
|
|
140
81
|
headers.append(
|
|
141
82
|
"Set-Cookie",
|
|
@@ -148,26 +89,9 @@ export async function bindPageHandler(
|
|
|
148
89
|
);
|
|
149
90
|
headers.set("Pragma", "no-cache");
|
|
150
91
|
headers.set("Expires", "0");
|
|
151
|
-
// Viewport-aware slot loading varies the response by these headers.
|
|
152
|
-
// Even with Cache-Control: no-store, browsers and intermediaries
|
|
153
|
-
// honour Vary on back/forward navigation and BFCache; Accept-CH
|
|
154
|
-
// asks the browser to send Sec-CH-UA-Mobile on subsequent
|
|
155
|
-
// navigations (Chromium-family auto-sends, others opt in).
|
|
156
92
|
headers.set("Vary", "Sec-CH-UA-Mobile, User-Agent");
|
|
157
93
|
headers.set("Accept-CH", "Sec-CH-UA-Mobile");
|
|
158
|
-
// Origin-side gzip. Cache-Control: no-transform (load-bearing above —
|
|
159
|
-
// it disables Cloudflare HTML rewrites that corrupt hydration) ALSO
|
|
160
|
-
// makes hono's compress() middleware skip this response, which shipped
|
|
161
|
-
// 1.2MB of raw SSR HTML per page load through the dev tunnel (~3s of
|
|
162
|
-
// pure transfer; the operator's tablet timed out — 2026-06-06).
|
|
163
|
-
// no-transform forbids INTERMEDIARY transforms; origin-applied
|
|
164
|
-
// content-coding is part of the representation (RFC 9110 §8.4.1), so
|
|
165
|
-
// compressing here honors both. Bun.gzipSync on ~1.2MB costs ~10ms.
|
|
166
94
|
const acceptEnc = c.req.header("Accept-Encoding") ?? "";
|
|
167
|
-
// Brotli preferred when offered (every modern browser): ~15-20%
|
|
168
|
-
// smaller than gzip on SSR HTML. Quality 4, NOT the library default
|
|
169
|
-
// 11 — q11 costs hundreds of ms on a ~1MB page (defeats the point);
|
|
170
|
-
// q4 compresses in single-digit ms and still beats gzip.
|
|
171
95
|
if (/\bbr\b/.test(acceptEnc) && resp.body) {
|
|
172
96
|
const { brotliCompressSync, constants } = await import("node:zlib");
|
|
173
97
|
const raw = new Uint8Array(await new Response(resp.body as never).arrayBuffer());
|
|
@@ -187,8 +111,6 @@ export async function bindPageHandler(
|
|
|
187
111
|
}
|
|
188
112
|
return new Response(resp.body as never, { status: resp.status, headers });
|
|
189
113
|
} catch (err) {
|
|
190
|
-
// Dead entity link (wrong id, deleted row) → the branded not-found
|
|
191
|
-
// page with a real 404 — never a 500, never default-content leakage.
|
|
192
114
|
if (err instanceof PrimaryResourceMissingError) {
|
|
193
115
|
log.warn(
|
|
194
116
|
`[scala-install:${serviceSlug}] 404 ${err.resourceType} not found (${label}) for ${new URL(c.req.url).pathname}`,
|
|
@@ -197,10 +119,6 @@ export async function bindPageHandler(
|
|
|
197
119
|
err.resourceType,
|
|
198
120
|
await resolveErrorPageLogo(serviceSlug),
|
|
199
121
|
err.httpStatus,
|
|
200
|
-
// "Take me home" → the host root (relative "/" resolves to the
|
|
201
|
-
// current site's home; the portal redirects / to its dashboard).
|
|
202
|
-
// empty → the page-row-exists-but-zero-blocks "This page is empty"
|
|
203
|
-
// surface, distinct from a dead link (operator ruling 2026-07-21).
|
|
204
122
|
{ homeUrl: "/", empty: err.empty },
|
|
205
123
|
);
|
|
206
124
|
}
|
|
@@ -1,16 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* The host-wide client-build lock of the client-asset build: serialises the
|
|
7
|
-
* heavy boot-time client build across ALL kind:ui services
|
|
8
|
-
* (client-asset-builds-serialize-via-lock), with a DB-driven TTL that
|
|
9
|
-
* fail-softs to a hardcoded default.
|
|
10
|
-
*
|
|
11
|
-
* Split out of build-client-assets.ts (962 lines) alongside its build
|
|
12
|
-
* siblings. Byte-for-byte move from the parent — imports adjusted, nothing
|
|
13
|
-
* else.
|
|
14
4
|
*/
|
|
15
5
|
|
|
16
6
|
import { join } from "node:path";
|
|
@@ -1,16 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Content-hashed output housekeeping for the client-asset build: the
|
|
7
|
-
* short content hash stamped into hashed filenames, and removal of stale
|
|
8
|
-
* hashed files from a previous build so dist/client/ never accumulates
|
|
9
|
-
* dead outputs.
|
|
10
|
-
*
|
|
11
|
-
* Split out of build-client-assets.ts (962 lines) alongside its build
|
|
12
|
-
* siblings. Byte-for-byte move from the parent except createHash →
|
|
13
|
-
* Bun.CryptoHasher (bun-native-crypto guard; identical digest output).
|
|
14
4
|
*/
|
|
15
5
|
|
|
16
6
|
import { readdir, rm } from "node:fs/promises";
|
|
@@ -1,60 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* The hydrate JS bundle unit of the client-asset build: Bun.build over
|
|
7
|
-
* src/entry-client.ts with the resilient-block-imports plugin (dead block
|
|
8
|
-
* imports degrade at runtime instead of crash-looping the service), the
|
|
9
|
-
* post-build dead-facade gate, stale-chunk removal, and source-map upload.
|
|
10
|
-
*
|
|
11
|
-
* Split out of build-client-assets.ts (962 lines) alongside its build
|
|
12
|
-
* siblings. Byte-for-byte move from the parent — imports adjusted, nothing
|
|
13
|
-
* else.
|
|
14
4
|
*/
|
|
15
5
|
|
|
16
|
-
// Externalize an unresolvable @teamscala/* block import (a deactivated block_schema whose manifest regen never propagated) or a broken
|
|
17
|
-
// third-party internal (@fullcalendar/core/internal.js during a version mismatch) so it degrades ONE block at runtime instead of crash-looping
|
|
18
|
-
// the service (2026-07-21 portal outage: @fullcalendar 7.x + plugins 6.x → unresolvable internal.js → 21 crash-loop restarts).
|
|
19
|
-
//
|
|
20
|
-
// The filter is deliberately NARROW — only the package prefixes where unresolvable imports actually occur — NOT every bare import. The prior
|
|
21
|
-
// broad /^(?![./])/ filter intercepted resolvable shared packages too, and onResolve pass-through (return undefined) on a shared
|
|
22
|
-
// sideEffects:false package (@tanstack/react-query, imported across many lazy block chunks) tripped Bun's code-splitter: it emitted a dead
|
|
23
|
-
// `export{...}`-with-no-`from` facade and tree-shook away the entire query-core implementation → a fatal hydration SyntaxError that froze
|
|
24
|
-
// agents.scala.business while every component reported healthy (2026-07-22; see the dead-facade gate + minifyIdentifiers:false
|
|
25
|
-
// below). Intercepts of node builtins (stream/web) likewise pulled them into the browser build. The narrow filter leaves @tanstack/* + builtins
|
|
26
|
-
// to Bun's native (correct) resolution. A future unresolvable import OUTSIDE these prefixes fails loud via the manifest guard + build
|
|
27
|
-
// tombstone (no-dead-block-imports-in-manifest) — never silent. Entry-point imports are excluded — those must fail loudly (silently
|
|
28
|
-
// externalizing a core import turns a build error into a mystery runtime crash; containment must not become a swallow).
|
|
29
|
-
|
|
30
|
-
// minifyIdentifiers is DISABLED (whitespace + syntax/dead-code minify
|
|
31
|
-
// kept): under code-splitting, identifier mangling interacting with a
|
|
32
|
-
// shared sideEffects:false package (@tanstack/react-query) trips Bun's
|
|
33
|
-
// splitter to tree-shake away the entire implementation and emit a dead
|
|
34
|
-
// re-export facade — a fatal hydration SyntaxError that froze
|
|
35
|
-
// agents.scala.business while every component reported healthy
|
|
36
|
-
// (2026-07-22; reproduces in stable 1.3.14 + canary 1.4.0, so an upstream
|
|
37
|
-
// fix is not near). Whitespace + syntax minify retain the bulk of the
|
|
38
|
-
// size win; only name-mangling is dropped. The post-build dead-facade
|
|
39
|
-
// gate below fails the build LOUD if this or any future regression slips
|
|
40
|
-
// a fatal facade through on any UI service.
|
|
41
|
-
|
|
42
|
-
// dead-facade prevention gate (agents.scala.business 2026-07-22): a Bun
|
|
43
|
-
// splitter+tree-shake bug (stable 1.3.14 + canary 1.4.0) can emit a "dead
|
|
44
|
-
// facade" chunk — `export { ... }` with NO `from` clause and NO local binding
|
|
45
|
-
// definitions — a fatal SyntaxError at module load that kills React hydration
|
|
46
|
-
// SILENTLY (the whole dashboard froze while every component reported healthy).
|
|
47
|
-
// The narrow resilientBlockPlugin filter + minifyIdentifiers:false above
|
|
48
|
-
// prevent the known trigger (@tanstack/react-query under split); this gate
|
|
49
|
-
// makes the CLASS unshippable — any dead facade from ANY package on ANY UI
|
|
50
|
-
// service fails the build LOUD here instead of freezing the page. Detection
|
|
51
|
-
// is POSITION-INDEPENDENT — a named export{} ANYWHERE in the comment-stripped
|
|
52
|
-
// body, with no `from` clause and no local declaration/import backing the
|
|
53
|
-
// names, is the facade (Bun could prefix it with a directive or statement, not
|
|
54
|
-
// only emit it at the very start — a positional assumption is a known way to
|
|
55
|
-
// be silently bypassed; a valid local-export chunk carries a
|
|
56
|
-
// class/function/const/var/let/import, a valid re-export carries `from`).
|
|
57
|
-
|
|
58
6
|
import { mkdir, readdir, rm } from "node:fs/promises";
|
|
59
7
|
import { join } from "node:path";
|
|
60
8
|
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
@@ -89,16 +37,6 @@ export async function buildHydrateBundle(
|
|
|
89
37
|
|
|
90
38
|
await mkdir(outDir, { recursive: true });
|
|
91
39
|
|
|
92
|
-
// Resilience (block-and-provider-error-isolation applied to the BUILD): a page-manifest.ts lazy import of a block whose component no longer resolves
|
|
93
|
-
// (a block_schema deactivation or a removed package export whose manifest regen never propagated) MUST NOT fail the whole bundle and crash-loop the
|
|
94
|
-
// service. Seven UI services went down at once this way on 2026-06-13. Such
|
|
95
|
-
// an import is EXTERNALIZED instead: the build succeeds, the service boots,
|
|
96
|
-
// and at runtime that one lazy import rejects into the block's
|
|
97
|
-
// BlockErrorBoundary — the dead block degrades, the page + service stay up.
|
|
98
|
-
// Only UNRESOLVABLE @teamgbg specifiers are touched (resolvable ones fall
|
|
99
|
-
// through to Bun's normal resolution unchanged). Surfaced via warn so it is
|
|
100
|
-
// never silent; the `no-dead-block-imports-in-manifest` guard still flags the
|
|
101
|
-
// stale manifest so it gets regenerated.
|
|
102
40
|
const externalizedBlocks: string[] = [];
|
|
103
41
|
const resilientBlockPlugin: Bun.BunPlugin = {
|
|
104
42
|
name: "resilient-block-imports",
|
|
@@ -122,21 +60,6 @@ export async function buildHydrateBundle(
|
|
|
122
60
|
entrypoints: [entry],
|
|
123
61
|
target: "browser",
|
|
124
62
|
outdir: outDir,
|
|
125
|
-
// THE BUNDLER OWNS THE HASH, because it also writes the references.
|
|
126
|
-
// Under `splitting: true` + `publicPath`, Bun bakes the entry's
|
|
127
|
-
// build-time name into every chunk that imports it. Naming the entry
|
|
128
|
-
// `hydrate.js` here and RENAMING it to `hydrate-<hash>.js` afterwards
|
|
129
|
-
// moved the file but could not rewrite the specifiers already emitted
|
|
130
|
-
// into the chunks — so every chunk imported `/assets/hydrate.js`, which
|
|
131
|
-
// no longer existed. 404 on the module, the importing block dies, and
|
|
132
|
-
// the page still returns 200 with every guard green.
|
|
133
|
-
//
|
|
134
|
-
// MEASURED 2026-09-08 on quiz.scala.business: `AssessmentFlowBlock`
|
|
135
|
-
// failed with "Failed to fetch dynamically imported module"; crawling
|
|
136
|
-
// the served graph (142 level-1 + 76 level-2 assets) found exactly one
|
|
137
|
-
// broken URL — `/assets/hydrate.js` — imported by the entry itself and
|
|
138
|
-
// by most chunks. Hash applied OUTSIDE the generator is two artefacts
|
|
139
|
-
// that must agree with nothing making them (`one-generator-pattern`).
|
|
140
63
|
naming: {
|
|
141
64
|
entry: `${HYDRATE_BASE}-[hash].[ext]`,
|
|
142
65
|
chunk: "chunk-[hash].[ext]",
|
|
@@ -145,36 +68,13 @@ export async function buildHydrateBundle(
|
|
|
145
68
|
minifyWhitespace: process.env.NODE_ENV === "production",
|
|
146
69
|
minifySyntax: process.env.NODE_ENV === "production",
|
|
147
70
|
minifyIdentifiers: false,
|
|
148
|
-
// Bun plugin type
|
|
149
71
|
plugins: [resilientBlockPlugin],
|
|
150
|
-
// Code-splitting honours `lazy-blocks-per-page`: page-manifest.ts wraps
|
|
151
|
-
// every block in `lazy(() => import())`, so splitting emits the heavy
|
|
152
|
-
// Plate editor + each lazy block as a SEPARATE chunk loaded on demand
|
|
153
|
-
// instead of inlining all of them into one ~12.5MB entry. The initial
|
|
154
|
-
// download drops ~10× (entry ~1.2MB; chunks lazy). splitting:false was
|
|
155
|
-
// drift that defeated the lazy manifest. SSR safety is intact — the
|
|
156
|
-
// page-server awaits stream.allReady before flushing (render-page-html),
|
|
157
|
-
// so every block is server-rendered and hydration is consistent.
|
|
158
72
|
splitting: true,
|
|
159
|
-
// External source maps so minified client stacks resolve to source in
|
|
160
|
-
// GlitchTip (uploaded post-build by uploadSourceMaps via the official
|
|
161
|
-
// glitchtip-cli debug-ID flow). Maps are deleted after upload — never
|
|
162
|
-
// served; the debug IDs injected into the bundles are all the SDK needs.
|
|
163
73
|
sourcemap: "external",
|
|
164
|
-
// Chunks are referenced by absolute /assets/ URL (served by
|
|
165
|
-
// register-static-assets' /assets/* handler), never a relative path that
|
|
166
|
-
// could mis-resolve under a record route like /?id=<uuid>.
|
|
167
74
|
publicPath: "/assets/",
|
|
168
75
|
external: ["*.css"],
|
|
169
76
|
});
|
|
170
77
|
} catch (e) {
|
|
171
|
-
// Bun.build can THROW (not return {success:false}) on certain failure
|
|
172
|
-
// modes — e.g. entrypoint resolves to a path Bun can't read, or an
|
|
173
|
-
// internal panic. Without this catch the boot path swallows the
|
|
174
|
-
// useful diagnostic and the operator sees only "Bundle failed".
|
|
175
|
-
// AggregateError carries the per-file resolution list in .errors —
|
|
176
|
-
// without serializing it the operator sees only "Bundle failed"
|
|
177
|
-
// (cost a full Railway round-trip per guess on 2026-06-05).
|
|
178
78
|
const subErrors =
|
|
179
79
|
e instanceof AggregateError
|
|
180
80
|
? e.errors.map((se) => (se instanceof Error ? se.message : String(se)))
|
|
@@ -192,12 +92,6 @@ export async function buildHydrateBundle(
|
|
|
192
92
|
}
|
|
193
93
|
|
|
194
94
|
if (!result.success) {
|
|
195
|
-
// Bun.build's `.logs` is the verbose-build-message list. Each log is an
|
|
196
|
-
// object with at minimum `{ level, message, position }`. Some failures
|
|
197
|
-
// surface with an empty logs array — happens when Bun bails before
|
|
198
|
-
// resolution (e.g. missing entrypoint after symlink-walk). Always emit
|
|
199
|
-
// the full JSON form via the logger so service-runtime's structured
|
|
200
|
-
// error trail captures every field, not just a `.message`-joined string.
|
|
201
95
|
log.error(`[build-client-assets] Bun.build failed`, {
|
|
202
96
|
logs: result.logs.map((l) => ({
|
|
203
97
|
level: (l as { level?: string }).level,
|
|
@@ -250,11 +144,6 @@ export async function buildHydrateBundle(
|
|
|
250
144
|
);
|
|
251
145
|
}
|
|
252
146
|
|
|
253
|
-
// Bun emitted the hashed entry itself, so the name it wrote into every
|
|
254
|
-
// chunk IS the name on disk. Read it back from the build's own output
|
|
255
|
-
// list rather than reconstructing it — a reconstructed name is a second
|
|
256
|
-
// copy of a fact the bundler already owns, which is the defect this
|
|
257
|
-
// change removes (`a-mutating-verb-reads-back-what-it-claims`).
|
|
258
147
|
const entryOutput = result.outputs.find(
|
|
259
148
|
(o) => o.kind === "entry-point",
|
|
260
149
|
);
|
|
@@ -265,12 +154,6 @@ export async function buildHydrateBundle(
|
|
|
265
154
|
}
|
|
266
155
|
const hashedName = entryOutput.path.split("/").pop() as string;
|
|
267
156
|
const hashedPath = join(outDir, hashedName);
|
|
268
|
-
// The bundler OWNS this name (naming.entry above). Read the release hash
|
|
269
|
-
// out of it; a name that no longer matches the declared
|
|
270
|
-
// hydrate-<hash>.js shape means the naming contract changed underneath
|
|
271
|
-
// us — refuse rather than re-derive a second hash from file bytes
|
|
272
|
-
// (`one-generator-pattern`): a fallback here would produce a release id
|
|
273
|
-
// the URL does not carry.
|
|
274
157
|
const hashMatch = /^hydrate-([A-Za-z0-9]+)\.js$/.exec(hashedName);
|
|
275
158
|
if (!hashMatch) {
|
|
276
159
|
throw new Error(
|
|
@@ -280,9 +163,6 @@ export async function buildHydrateBundle(
|
|
|
280
163
|
const hash = hashMatch[1];
|
|
281
164
|
removeStaleHashedFiles(outDir, HYDRATE_BASE, "js", hashedName);
|
|
282
165
|
|
|
283
|
-
// Splitting emits one chunk per lazy import() alongside the entry. Keep the
|
|
284
|
-
// chunks THIS build produced; drop stale ones so dist/client/ doesn't
|
|
285
|
-
// accumulate hundreds of orphaned chunks per rebuild.
|
|
286
166
|
const currentChunks = new Set(
|
|
287
167
|
result.outputs
|
|
288
168
|
.filter((o) => o.kind === "chunk")
|
|
@@ -290,10 +170,6 @@ export async function buildHydrateBundle(
|
|
|
290
170
|
);
|
|
291
171
|
await removeStaleChunks(outDir, currentChunks);
|
|
292
172
|
|
|
293
|
-
// Bundle telemetry → logger (single-logger-entry-point → GlitchTip): the
|
|
294
|
-
// initial-download size + lazy-chunk footprint are visible per build, so a
|
|
295
|
-
// regression (splitting silently off, or a heavy dep landing in the entry)
|
|
296
|
-
// surfaces as a number instead of a mystery "slow page".
|
|
297
173
|
const entryBytes = (await Bun.file(hashedPath).stat()).size;
|
|
298
174
|
let chunkBytes = 0;
|
|
299
175
|
for (const name of currentChunks) {
|
|
@@ -308,19 +184,11 @@ export async function buildHydrateBundle(
|
|
|
308
184
|
{ entryBytes, chunkCount: currentChunks.size, chunkBytes },
|
|
309
185
|
);
|
|
310
186
|
if (externalizedBlocks.length > 0) {
|
|
311
|
-
// The service boots; these blocks degrade at runtime (BlockErrorBoundary)
|
|
312
|
-
// instead of crash-looping the whole service. The stale manifest still
|
|
313
|
-
// needs regenerating — surfaced here AND by no-dead-block-imports-in-manifest.
|
|
314
187
|
log.warn(
|
|
315
188
|
`[build-client-assets] ${externalizedBlocks.length} unresolvable block import(s) externalized (degraded, not fatal) — regenerate this service's page-manifest: ${externalizedBlocks.join(", ")}`,
|
|
316
189
|
{ externalizedBlocks },
|
|
317
190
|
);
|
|
318
191
|
}
|
|
319
|
-
// Upload source maps to GlitchTip (best-effort) + RETAIN the .map files
|
|
320
|
-
// outside the served root so a failed upload retries next boot — source
|
|
321
|
-
// never ships publicly. The currentOutputs set names the build's own
|
|
322
|
-
// bundles (entry + chunks) so maps from an older build don't pollute
|
|
323
|
-
// this release's cache and re-upload as stale artifacts.
|
|
324
192
|
const currentOutputs = new Set(
|
|
325
193
|
result.outputs
|
|
326
194
|
.filter((o) => o.kind === "chunk")
|
|
@@ -330,10 +198,6 @@ export async function buildHydrateBundle(
|
|
|
330
198
|
return { hydratePath: `/assets/${hashedName}`, sourceMaps };
|
|
331
199
|
}
|
|
332
200
|
|
|
333
|
-
// Remove hashed code-split chunks no longer referenced by the current entry.
|
|
334
|
-
// Keyed off the build's own output list (the only authoritative current set),
|
|
335
|
-
// never a name pattern guess — a build emits N `chunk-*.js`; the previous
|
|
336
|
-
// build's are orphaned the moment the entry's hash changes.
|
|
337
201
|
async function removeStaleChunks(
|
|
338
202
|
outDir: string,
|
|
339
203
|
keep: Set<string>,
|
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* The asset-manifest unit of the client-asset build: the AssetManifest shape,
|
|
7
|
-
* its read/write, the last-failure tombstone status read the runtime surfaces,
|
|
8
|
-
* and the first-boot fallback paths.
|
|
9
|
-
*
|
|
10
|
-
* Split out of build-client-assets.ts (962 lines) alongside its build
|
|
11
|
-
* siblings — the file held six concerns (manifest, hashed output, hydrate
|
|
12
|
-
* bundle, source-map upload, stylesheet build, fingerprint, lock, orchestration).
|
|
13
|
-
*
|
|
14
|
-
* Byte-for-byte move from the parent — imports adjusted, nothing else.
|
|
15
4
|
*/
|
|
16
5
|
|
|
17
6
|
import { join } from "node:path";
|
|
@@ -46,13 +35,6 @@ export interface AssetManifest {
|
|
|
46
35
|
sourceMaps?: SourceMapUploadResult;
|
|
47
36
|
}
|
|
48
37
|
|
|
49
|
-
/**
|
|
50
|
-
* Read ONLY the tombstone state of the current manifest — the runtime
|
|
51
|
-
* (read-html-template, ui-service) calls this to decide whether to surface a
|
|
52
|
-
* build-failure signal. Returns null when the manifest is fresh (no
|
|
53
|
-
* `buildFailed`). Decoupled from the full AssetManifest read so the runtime
|
|
54
|
-
* never depends on the build internals.
|
|
55
|
-
*/
|
|
56
38
|
export async function readClientBuildStatus(
|
|
57
39
|
cwd = process.cwd(),
|
|
58
40
|
): Promise<{ failed: true; reason?: string; at?: string } | { failed: false }> {
|
|
@@ -104,17 +86,6 @@ export async function writeManifest(
|
|
|
104
86
|
);
|
|
105
87
|
}
|
|
106
88
|
|
|
107
|
-
/**
|
|
108
|
-
* Verify the CURRENT manifest against the files it names — the capability
|
|
109
|
-
* probe the readiness flip gates on (`a-component-may-not-report-a-state-it-
|
|
110
|
-
* has-not-verified`). A manifest is servable only when BOTH referenced assets
|
|
111
|
-
* exist on disk. An absent or unreadable manifest, a first-boot tombstone
|
|
112
|
-
* carrying no paths, and any manifest naming files that do not exist must all
|
|
113
|
-
* read as NOT built, never as a legitimate bundle. Measured 2026-09-01:
|
|
114
|
-
* proposals.scala.business served a client-facing proposal unstyled while
|
|
115
|
-
* Railway reported the deploy SUCCESS, because "client build never ran" was
|
|
116
|
-
* indistinguishable from "client build fine" at every layer that reports.
|
|
117
|
-
*/
|
|
118
89
|
export async function verifyClientBuildAssets(
|
|
119
90
|
cwd = process.cwd(),
|
|
120
91
|
): Promise<{ ok: true } | { ok: false; reason: string }> {
|