@teamscala/service-boot 0.23.431 → 0.23.432
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 +12 -19
- package/src/agreement-gate/signing-shell.ts +19 -15
- package/src/attach-proposal-accept.ts +5 -2
- package/src/auth-gate/public-paths.ts +7 -16
- package/src/auth-gate.ts +54 -42
- package/src/bind-page-handler.ts +20 -32
- package/src/build-client-assets/build-lock.ts +15 -6
- package/src/build-client-assets/hashed-output.ts +3 -1
- package/src/build-client-assets/hydrate-bundle.ts +59 -57
- package/src/build-client-assets/input-fingerprint.ts +50 -57
- package/src/build-client-assets/input-hashing.ts +2 -4
- package/src/build-client-assets/manifest.ts +8 -11
- package/src/build-client-assets/source-candidates.ts +8 -16
- package/src/build-client-assets/sourcemaps-upload.ts +17 -10
- package/src/build-client-assets/styles-build.ts +17 -19
- package/src/build-client-assets.test.ts +2 -2
- package/src/build-client-assets.ts +16 -22
- package/src/content-gate/access.ts +5 -2
- package/src/content-gate/catalog.ts +26 -18
- package/src/content-gate/gate.ts +7 -3
- package/src/discover-blocks.ts +5 -7
- package/src/fn-routers/agreement-certificate.ts +1 -1
- package/src/fn-routers/agreement-send-router.ts +5 -4
- package/src/fn-routers/agreement-sign-side-effects.ts +27 -22
- package/src/fn-routers/assessment-report-email.ts +3 -2
- package/src/fn-routers/auth-router.ts +20 -21
- package/src/fn-routers/chat-session-router-shared.ts +12 -3
- package/src/fn-routers/chat-session-router.ts +8 -6
- package/src/fn-routers/conversation-ownership.ts +3 -2
- package/src/fn-routers/messaging-whatsapp-router.test.ts +3 -1
- package/src/fn-routers/messaging-whatsapp-router.ts +17 -3
- package/src/fn-routers/persist-chat-messages.ts +6 -4
- package/src/fn-routers/proposal-send-router.ts +4 -3
- package/src/fn-routers/public-agreement-router.ts +5 -4
- package/src/fn-routers/public-assessment-router.ts +5 -4
- package/src/fn-routers/public-gpt-session-router.ts +17 -12
- package/src/fn-routers/user-preferences-router.ts +6 -15
- package/src/fn-routers/work-items-router.ts +21 -17
- package/src/handlers/platform-fault-escalation/orpc-client.ts +38 -18
- package/src/handlers/projects-digest.ts +1 -1
- package/src/handlers/scala-tools-currency-watch.test.ts +1 -5
- package/src/handlers/scala-tools-currency-watch.ts +23 -32
- package/src/handlers/singleton-tools-currency-watch.test.ts +1 -5
- package/src/handlers/singleton-tools-currency-watch.ts +27 -19
- package/src/handlers/upstream-issue-sync.ts +11 -4
- package/src/handlers/whatsapp-liveness.ts +14 -4
- package/src/install-harness.ts +1 -1
- package/src/install-logging.ts +1 -1
- package/src/mount-orpc-router.test.ts +56 -10
- package/src/mount-orpc-router.ts +23 -9
- package/src/mount-server-packages.ts +17 -15
- package/src/pg-boss-mount.ts +5 -12
- package/src/proposal-gate.ts +7 -2
- package/src/read-html-template.test.ts +12 -15
- package/src/read-html-template.ts +2 -1
- package/src/readiness-gate.ts +2 -1
- package/src/register-crash-handlers.ts +4 -8
- package/src/register-page-server-client.ts +15 -19
- package/src/register-registry-page-route/ai-chat-prefetch.ts +16 -12
- package/src/register-registry-page-route/apply-page-branding.ts +68 -56
- package/src/register-registry-page-route/graph-cache.ts +22 -18
- package/src/register-registry-page-route/meta-title.ts +24 -20
- package/src/register-registry-page-route/prefetch-org-id.ts +6 -2
- package/src/register-registry-page-route/proposal-approval-email-token.ts +5 -3
- package/src/register-registry-page-route/run-content-gates.ts +2 -4
- package/src/register-registry-page-route.ts +1 -2
- package/src/register-static-assets.ts +6 -5
- package/src/resolve-app-base-url.ts +12 -5
- package/src/resolvers/portal-route-paths.ts +15 -11
- package/src/resource-chat-inject.ts +84 -72
- package/src/resource-not-found.ts +23 -19
- package/src/scala-tools-currency-watchdog.ts +41 -12
- package/src/two-phase-full-app.ts +61 -29
- package/src/two-phase-minimal-app.ts +39 -31
- package/src/two-phase-readiness.ts +50 -38
- package/src/ui-service.ts +50 -38
- package/src/handlers/pane-outbox-drain.ts +0 -39
- package/src/handlers/prune-stale-data.ts +0 -110
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
},
|
|
7
7
|
"name": "@teamscala/service-boot",
|
|
8
8
|
"type": "module",
|
|
9
|
-
"version": "0.23.
|
|
9
|
+
"version": "0.23.432",
|
|
10
10
|
"sideEffects": false,
|
|
11
11
|
"imports": {
|
|
12
12
|
"#service-boot/*": "./src/*",
|
|
@@ -16,11 +16,11 @@
|
|
|
16
16
|
"src"
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
|
+
"@orpc/server": "*",
|
|
19
20
|
"@teamscala/agreement-generation": "*",
|
|
20
21
|
"@teamscala/api-resend": "*",
|
|
21
22
|
"@teamscala/auth": "*",
|
|
22
23
|
"@teamscala/cache": "*",
|
|
23
|
-
"@teamscala/channel": "*",
|
|
24
24
|
"@teamscala/chat-completion": "*",
|
|
25
25
|
"@teamscala/db": "*",
|
|
26
26
|
"@teamscala/db-validation": "*",
|
|
@@ -32,8 +32,10 @@
|
|
|
32
32
|
"@teamscala/file-lock": "*",
|
|
33
33
|
"@teamscala/formatters": "*",
|
|
34
34
|
"@teamscala/generic-registry": "*",
|
|
35
|
+
"@teamscala/host-command-bus": "*",
|
|
35
36
|
"@teamscala/http": "*",
|
|
36
37
|
"@teamscala/logger": "*",
|
|
38
|
+
"@teamscala/mcp-multi-session": "*",
|
|
37
39
|
"@teamscala/notify-cache": "*",
|
|
38
40
|
"@teamscala/object-storage": "*",
|
|
39
41
|
"@teamscala/orpc": "*",
|
|
@@ -45,7 +47,6 @@
|
|
|
45
47
|
"@teamscala/timing": "*",
|
|
46
48
|
"@teamscala/tool-executor": "*",
|
|
47
49
|
"@teamscala/watchdog": "*",
|
|
48
|
-
"@orpc/server": "*",
|
|
49
50
|
"hono": "*",
|
|
50
51
|
"pg-boss": "*",
|
|
51
52
|
"tailwindcss": "*",
|
|
@@ -653,14 +654,6 @@
|
|
|
653
654
|
"types": "./src/handlers/messaging-send-sweep-selection.d.ts",
|
|
654
655
|
"default": "./src/handlers/messaging-send-sweep-selection.ts"
|
|
655
656
|
},
|
|
656
|
-
"./handlers/pane-outbox-drain": {
|
|
657
|
-
"types": "./src/handlers/pane-outbox-drain.d.ts",
|
|
658
|
-
"default": "./src/handlers/pane-outbox-drain.ts"
|
|
659
|
-
},
|
|
660
|
-
"./handlers/pane-outbox-drain.ts": {
|
|
661
|
-
"types": "./src/handlers/pane-outbox-drain.d.ts",
|
|
662
|
-
"default": "./src/handlers/pane-outbox-drain.ts"
|
|
663
|
-
},
|
|
664
657
|
"./handlers/platform-fault-escalation": {
|
|
665
658
|
"types": "./src/handlers/platform-fault-escalation.d.ts",
|
|
666
659
|
"default": "./src/handlers/platform-fault-escalation.ts"
|
|
@@ -709,6 +702,14 @@
|
|
|
709
702
|
"types": "./src/handlers/platform-fault-escalation/orpc-client.d.ts",
|
|
710
703
|
"default": "./src/handlers/platform-fault-escalation/orpc-client.ts"
|
|
711
704
|
},
|
|
705
|
+
"./handlers/platform-fault-escalation/probe": {
|
|
706
|
+
"types": "./src/handlers/platform-fault-escalation/probe.d.ts",
|
|
707
|
+
"default": "./src/handlers/platform-fault-escalation/probe.ts"
|
|
708
|
+
},
|
|
709
|
+
"./handlers/platform-fault-escalation/probe.ts": {
|
|
710
|
+
"types": "./src/handlers/platform-fault-escalation/probe.d.ts",
|
|
711
|
+
"default": "./src/handlers/platform-fault-escalation/probe.ts"
|
|
712
|
+
},
|
|
712
713
|
"./handlers/projects-digest": {
|
|
713
714
|
"types": "./src/handlers/projects-digest.d.ts",
|
|
714
715
|
"default": "./src/handlers/projects-digest.ts"
|
|
@@ -717,14 +718,6 @@
|
|
|
717
718
|
"types": "./src/handlers/projects-digest.d.ts",
|
|
718
719
|
"default": "./src/handlers/projects-digest.ts"
|
|
719
720
|
},
|
|
720
|
-
"./handlers/prune-stale-data": {
|
|
721
|
-
"types": "./src/handlers/prune-stale-data.d.ts",
|
|
722
|
-
"default": "./src/handlers/prune-stale-data.ts"
|
|
723
|
-
},
|
|
724
|
-
"./handlers/prune-stale-data.ts": {
|
|
725
|
-
"types": "./src/handlers/prune-stale-data.d.ts",
|
|
726
|
-
"default": "./src/handlers/prune-stale-data.ts"
|
|
727
|
-
},
|
|
728
721
|
"./handlers/registry-stale-inactive-sweep": {
|
|
729
722
|
"types": "./src/handlers/registry-stale-inactive-sweep.d.ts",
|
|
730
723
|
"default": "./src/handlers/registry-stale-inactive-sweep.ts"
|
|
@@ -12,6 +12,25 @@
|
|
|
12
12
|
* paths adjusted only.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* The body content array for the agreement. The public shell composes the body
|
|
17
|
+
* region FROM resolved.page (register-registry-page-route.ts Branch 2:
|
|
18
|
+
* `bodyPuck = extractPuckData(resolved.page)`, returned un-cloned), so this IS
|
|
19
|
+
* the live array whose mutation reaches the SSR render. Header/footer are
|
|
20
|
+
* separate region sources (resolved.header / resolved.footer) — never the body.
|
|
21
|
+
*
|
|
22
|
+
* A prior version scored every region by top-level non-shell block count and
|
|
23
|
+
* picked the highest. That misroutes when the body wraps its clauses inside a
|
|
24
|
+
* Container (body's top-level count is ~2 while its nested clause count is ~40):
|
|
25
|
+
* a /header region with more top-level blocks out-scores the body, so
|
|
26
|
+
* onUnauthorized cleared the HEADER and injected the gate there, leaving the
|
|
27
|
+
* entire agreement body visible past the gate. The body is the resource page —
|
|
28
|
+
* identified, not heuristically guessed.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
// Why: The body content array for the agreement. The public shell c…
|
|
32
|
+
// (full note hoisted to this file's leading header — kept out of the body per
|
|
33
|
+
// no-inline-prose-blocks / prose-stays-in-docs)
|
|
15
34
|
import { loadRegistryConfig } from "@teamscala/db/registry/load-config";
|
|
16
35
|
import * as v from "valibot";
|
|
17
36
|
import { findAgreementGate } from "./signing-session.ts";
|
|
@@ -29,21 +48,6 @@ export function stashAgreementMeta(
|
|
|
29
48
|
resolved[AGREEMENT_META_KEY] = { id, name };
|
|
30
49
|
}
|
|
31
50
|
|
|
32
|
-
/**
|
|
33
|
-
* The body content array for the agreement. The public shell composes the body
|
|
34
|
-
* region FROM resolved.page (register-registry-page-route.ts Branch 2:
|
|
35
|
-
* `bodyPuck = extractPuckData(resolved.page)`, returned un-cloned), so this IS
|
|
36
|
-
* the live array whose mutation reaches the SSR render. Header/footer are
|
|
37
|
-
* separate region sources (resolved.header / resolved.footer) — never the body.
|
|
38
|
-
*
|
|
39
|
-
* A prior version scored every region by top-level non-shell block count and
|
|
40
|
-
* picked the highest. That misroutes when the body wraps its clauses inside a
|
|
41
|
-
* Container (body's top-level count is ~2 while its nested clause count is ~40):
|
|
42
|
-
* a /header region with more top-level blocks out-scores the body, so
|
|
43
|
-
* onUnauthorized cleared the HEADER and injected the gate there, leaving the
|
|
44
|
-
* entire agreement body visible past the gate. The body is the resource page —
|
|
45
|
-
* identified, not heuristically guessed.
|
|
46
|
-
*/
|
|
47
51
|
export function getBodyContentArray(
|
|
48
52
|
resolved: Record<string, unknown>,
|
|
49
53
|
): ContentBlock[] | null {
|
|
@@ -57,6 +57,7 @@ export async function attachProposalAccept(
|
|
|
57
57
|
app.post("/api/proposal-accept", async (c) => {
|
|
58
58
|
let proposalId = "";
|
|
59
59
|
let cartSelection: unknown = null;
|
|
60
|
+
let bodyFailed = false;
|
|
60
61
|
try {
|
|
61
62
|
const form = await c.req.parseBody();
|
|
62
63
|
proposalId = String(form.proposalId ?? "").trim();
|
|
@@ -64,9 +65,11 @@ export async function attachProposalAccept(
|
|
|
64
65
|
if (typeof raw === "string" && raw.trim()) {
|
|
65
66
|
cartSelection = JSON.parse(raw);
|
|
66
67
|
}
|
|
67
|
-
} catch {
|
|
68
|
-
|
|
68
|
+
} catch (error) {
|
|
69
|
+
log.warn(`[proposal-accept] body parse failed — redirecting home: ${String(error)}`);
|
|
70
|
+
bodyFailed = true;
|
|
69
71
|
}
|
|
72
|
+
if (bodyFailed) return c.redirect("/", 302);
|
|
70
73
|
const back = backToProposal(proposalId);
|
|
71
74
|
if (!proposalId) return c.redirect("/", 302);
|
|
72
75
|
|
|
@@ -22,22 +22,13 @@ export const SIGN_IN_PATHS = new Set([
|
|
|
22
22
|
"/magic-link",
|
|
23
23
|
]);
|
|
24
24
|
|
|
25
|
-
/// A STATIC ASSET IS NEVER A PROTECTED RESOURCE, so it is decided by EXTENSION
|
|
26
|
-
///
|
|
27
|
-
///
|
|
28
|
-
///
|
|
29
|
-
///
|
|
30
|
-
///
|
|
31
|
-
///
|
|
32
|
-
/// so `/logos/cropped.png` answered 302 → /sign-in and the browser rendered a
|
|
33
|
-
/// blank logo under a "starting up" splash. Favicons are on the list ONE FILE AT
|
|
34
|
-
/// A TIME, which is the same defect already visible.
|
|
35
|
-
///
|
|
36
|
-
/// This is why the operator reports it "keeps regressing": nothing is broken by
|
|
37
|
-
/// a code change, the allowlist simply never covered the path, and any build that
|
|
38
|
-
/// moves images re-creates it. Enumerating what is public is `prevention-over-
|
|
39
|
-
/// detection` inverted — the cure is a predicate, not a longer list.
|
|
40
|
-
///
|
|
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. /
|
|
41
32
|
/// Redirecting an <img> to a login page is never useful behaviour either: the
|
|
42
33
|
/// browser cannot act on the redirect, so it silently paints nothing. Failing
|
|
43
34
|
/// closed on a page is protection; failing closed on an image is just a blank.
|
package/src/auth-gate.ts
CHANGED
|
@@ -24,24 +24,6 @@
|
|
|
24
24
|
* the local better-auth handler is used (dev-host fallback).
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
import type { ServiceInstallConfig } from "@teamscala/db/registry/loader/load-service-install-row";
|
|
28
|
-
import { getPrisma } from "@teamscala/db/prisma-registry";
|
|
29
|
-
import { loadRegistryConfig } from "@teamscala/db/registry/load-config";
|
|
30
|
-
import { resolveAccess } from "@teamscala/auth/resolve-access";
|
|
31
|
-
import * as v from "valibot";
|
|
32
|
-
import { configure as configureEventLog } from "@teamscala/event-log/configure";
|
|
33
|
-
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
34
|
-
import type { Hono } from "hono";
|
|
35
|
-
|
|
36
|
-
import {
|
|
37
|
-
isStaticAssetPath,
|
|
38
|
-
PUBLIC_INFRA_PATHS,
|
|
39
|
-
PUBLIC_INFRA_PREFIXES,
|
|
40
|
-
SIGN_IN_PATHS,
|
|
41
|
-
} from "./auth-gate/public-paths.ts";
|
|
42
|
-
|
|
43
|
-
export { isStaticAssetPath } from "./auth-gate/public-paths.ts";
|
|
44
|
-
|
|
45
27
|
/**
|
|
46
28
|
* Point the event-log primitive at the process's Prisma singleton.
|
|
47
29
|
*
|
|
@@ -61,6 +43,60 @@ export { isStaticAssetPath } from "./auth-gate/public-paths.ts";
|
|
|
61
43
|
* unrelated edit to this file. Naming the boot step honestly is the fix. The
|
|
62
44
|
* registry refused a path exemption for it, correctly — `guard-exemptions-to-zero`.
|
|
63
45
|
*/
|
|
46
|
+
|
|
47
|
+
// Why: Point the event-log primitive at the process's Prisma single…
|
|
48
|
+
// (full note hoisted to this file's leading header — kept out of the body per
|
|
49
|
+
// no-inline-prose-blocks / prose-stays-in-docs)
|
|
50
|
+
// Eager-warm the local BetterAuth runtime at boot whenever a request path
|
|
51
|
+
// will build it: the session-READ path (getSession, better-auth-cookie mode)
|
|
52
|
+
// always uses the local instance — even when /api/auth/* forwards to
|
|
53
|
+
// scala-otp — and the local /api/auth handler uses it when no otpUrl is set.
|
|
54
|
+
// Building lazily on the first such request races a node_modules rebuild
|
|
55
|
+
// window: Bun transpiles better-auth on demand, and against a half-written
|
|
56
|
+
// tree that throws ENOENT → 500 on every auth + session check while /health
|
|
57
|
+
// stays green (nothing restarts it) — the 2026-06-12 login outage. Building
|
|
58
|
+
// here resolves the better-auth module graph once while node_modules is
|
|
59
|
+
// stable, and getAuth() memoizes it for the process lifetime. Best-effort and
|
|
60
|
+
// non-fatal: getAuth() never caches a failed build, so if this warm fails
|
|
61
|
+
// (e.g. mid-install) the first successful request rebuilds and caches — the
|
|
62
|
+
// service is never left wedged, and never boot-loops on a transient.
|
|
63
|
+
|
|
64
|
+
// Why: Eager-warm the local BetterAuth runtime at boot whenever a r…
|
|
65
|
+
// (full note hoisted to this file's leading header — kept out of the body per
|
|
66
|
+
// no-inline-prose-blocks / prose-stays-in-docs)
|
|
67
|
+
// NO localhost bypass: a 127.0.0.1 page request is gated EXACTLY like a
|
|
68
|
+
// tunnel request (no session → redirect to /sign-in). The old
|
|
69
|
+
// `if (isLocalhost) return next()` let any tool — and the agent's own
|
|
70
|
+
// chrome-devtools/curl — see authed pages on localhost WITHOUT a session,
|
|
71
|
+
// so verification on 127.0.0.1 diverged from what a signed-in user sees
|
|
72
|
+
// through the domain (the CSS regression that hid for three runs,
|
|
73
|
+
// 2026-06-13). The bypass is redundant: anything needing an authed view
|
|
74
|
+
// signs in (the OTP is DB-readable on the dev host) — host-side harnesses
|
|
75
|
+
// use getSuperAdminSessionCookie (scala-tools/lib). Internal
|
|
76
|
+
// service-to-service auth still flows through the cookie/secret on the
|
|
77
|
+
// request, never the source IP.
|
|
78
|
+
|
|
79
|
+
// Why: NO localhost bypass: a 127.0.0.1 page request is gated EXACT…
|
|
80
|
+
// (full note hoisted to this file's leading header — kept out of the body per
|
|
81
|
+
// no-inline-prose-blocks / prose-stays-in-docs)
|
|
82
|
+
import type { ServiceInstallConfig } from "@teamscala/db/registry/loader/load-service-install-row";
|
|
83
|
+
import { getPrisma } from "@teamscala/db/prisma-registry";
|
|
84
|
+
import { loadRegistryConfig } from "@teamscala/db/registry/load-config";
|
|
85
|
+
import { resolveAccess } from "@teamscala/auth/resolve-access";
|
|
86
|
+
import * as v from "valibot";
|
|
87
|
+
import { configure as configureEventLog } from "@teamscala/event-log/configure";
|
|
88
|
+
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
89
|
+
import type { Hono } from "hono";
|
|
90
|
+
|
|
91
|
+
import {
|
|
92
|
+
isStaticAssetPath,
|
|
93
|
+
PUBLIC_INFRA_PATHS,
|
|
94
|
+
PUBLIC_INFRA_PREFIXES,
|
|
95
|
+
SIGN_IN_PATHS,
|
|
96
|
+
} from "./auth-gate/public-paths.ts";
|
|
97
|
+
|
|
98
|
+
export { isStaticAssetPath } from "./auth-gate/public-paths.ts";
|
|
99
|
+
|
|
64
100
|
function initGateDependencies(): { prisma: ReturnType<typeof getPrisma> } {
|
|
65
101
|
const prisma = getPrisma();
|
|
66
102
|
configureEventLog({ prisma });
|
|
@@ -104,19 +140,6 @@ export async function attachAuthGate(
|
|
|
104
140
|
|
|
105
141
|
const otpUrl = process.env.SCALA_OTP_URL;
|
|
106
142
|
|
|
107
|
-
// Eager-warm the local BetterAuth runtime at boot whenever a request path
|
|
108
|
-
// will build it: the session-READ path (getSession, better-auth-cookie mode)
|
|
109
|
-
// always uses the local instance — even when /api/auth/* forwards to
|
|
110
|
-
// scala-otp — and the local /api/auth handler uses it when no otpUrl is set.
|
|
111
|
-
// Building lazily on the first such request races a node_modules rebuild
|
|
112
|
-
// window: Bun transpiles better-auth on demand, and against a half-written
|
|
113
|
-
// tree that throws ENOENT → 500 on every auth + session check while /health
|
|
114
|
-
// stays green (nothing restarts it) — the 2026-06-12 login outage. Building
|
|
115
|
-
// here resolves the better-auth module graph once while node_modules is
|
|
116
|
-
// stable, and getAuth() memoizes it for the process lifetime. Best-effort and
|
|
117
|
-
// non-fatal: getAuth() never caches a failed build, so if this warm fails
|
|
118
|
-
// (e.g. mid-install) the first successful request rebuilds and caches — the
|
|
119
|
-
// service is never left wedged, and never boot-loops on a transient.
|
|
120
143
|
if (authMode === "better-auth-cookie" || !otpUrl) {
|
|
121
144
|
try {
|
|
122
145
|
await getAuth();
|
|
@@ -225,17 +248,6 @@ export async function attachAuthGate(
|
|
|
225
248
|
isStaticAssetPath(url.pathname);
|
|
226
249
|
if (isInfra) return next();
|
|
227
250
|
|
|
228
|
-
// NO localhost bypass: a 127.0.0.1 page request is gated EXACTLY like a
|
|
229
|
-
// tunnel request (no session → redirect to /sign-in). The old
|
|
230
|
-
// `if (isLocalhost) return next()` let any tool — and the agent's own
|
|
231
|
-
// chrome-devtools/curl — see authed pages on localhost WITHOUT a session,
|
|
232
|
-
// so verification on 127.0.0.1 diverged from what a signed-in user sees
|
|
233
|
-
// through the domain (the CSS regression that hid for three runs,
|
|
234
|
-
// 2026-06-13). The bypass is redundant: anything needing an authed view
|
|
235
|
-
// signs in (the OTP is DB-readable on the dev host) — host-side harnesses
|
|
236
|
-
// use getSuperAdminSessionCookie (scala-tools/lib). Internal
|
|
237
|
-
// service-to-service auth still flows through the cookie/secret on the
|
|
238
|
-
// request, never the source IP.
|
|
239
251
|
const host = c.req.header("host") ?? "";
|
|
240
252
|
const cookie = c.req.header("cookie") ?? "";
|
|
241
253
|
const tokenMatch = cookie.match(/better-auth\.session_token=([^;]+)/);
|
package/src/bind-page-handler.ts
CHANGED
|
@@ -9,6 +9,21 @@
|
|
|
9
9
|
* and page_slug shell variants.
|
|
10
10
|
*/
|
|
11
11
|
|
|
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
|
+
|
|
24
|
+
// Why: Per-page root.props.maxWidth (the canonical mechanism) cente…
|
|
25
|
+
// (full note hoisted to this file's leading header — kept out of the body per
|
|
26
|
+
// no-inline-prose-blocks / prose-stays-in-docs)
|
|
12
27
|
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
13
28
|
import type { Context } from "hono";
|
|
14
29
|
import { readHtmlTemplate } from "./read-html-template.ts";
|
|
@@ -93,16 +108,11 @@ export async function bindPageHandler(
|
|
|
93
108
|
// logo/colours, not the viewer's.
|
|
94
109
|
if (resourceOrgId) orgId = resourceOrgId;
|
|
95
110
|
|
|
96
|
-
// Unauthed /sign-in (no session, no resource) would otherwise resolve no
|
|
97
|
-
// org
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
// from it so the sign-in page's brand/title/og reflect the destination org.
|
|
102
|
-
// The canonical portal shape is /<websiteId>/<orgId>/<path> — the org is
|
|
103
|
-
// segment 1, NOT segment 0. Reading segment 0 (the website id) fed a website
|
|
104
|
-
// id into brand resolution, which resolved no org and fell back to the
|
|
105
|
-
// super-admin org — the exact cross-tenant mis-branding this block exists to
|
|
111
|
+
// 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)
|
|
112
|
+
// — 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.
|
|
113
|
+
// 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.
|
|
114
|
+
// 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
|
|
115
|
+
// 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
|
|
106
116
|
// fix, still live until 2026-07-21; violates portal-branding-resolves-page-org-first.
|
|
107
117
|
// scala-projects routes /<orgId>/… (single UUID) and proposal/form/quiz hosts
|
|
108
118
|
// route /<resourceId>/… (a non-org first segment) — both keep segment 0 via
|
|
@@ -124,28 +134,6 @@ export async function bindPageHandler(
|
|
|
124
134
|
htmlTemplate: await readHtmlTemplate(),
|
|
125
135
|
nonce,
|
|
126
136
|
});
|
|
127
|
-
// Per-page root.props.maxWidth (the canonical mechanism) centers any
|
|
128
|
-
// page at the chosen width via inline styles — already hydration-safe
|
|
129
|
-
// and page-scoped. Bind-page-handler MUST NOT inject unscoped CSS
|
|
130
|
-
// selectors: a global `.puck-root-slot { max-width }` rule shrinks
|
|
131
|
-
// every portal pane that hosts a Puck root (the 2026-06-17 dashboard
|
|
132
|
-
// sliver bug — `.puck-root-slot` matches every Puck page in every
|
|
133
|
-
// region, including the dashboard center pane). The `no-bind-page-handler-css-injection`
|
|
134
|
-
// guard enforces this; this comment is the load-bearing rationale.
|
|
135
|
-
// Live dashboards must never be served from a stale cache (intermediary,
|
|
136
|
-
// browser back-forward, mobile Safari aggressive caching). Force every
|
|
137
|
-
// SSR HTML response to be revalidated. Static assets (CSS/JS bundles)
|
|
138
|
-
// keep their hash-busted caching — those are content-addressed.
|
|
139
|
-
// no-transform: intermediaries must not mutate SSR HTML — Cloudflare's
|
|
140
|
-
// Email Address Obfuscation rewrites mailto: hrefs at the edge, which
|
|
141
|
-
// breaks the server-serializes-client-reads-verbatim hydration contract
|
|
142
|
-
// (React recoverable-error on every page with a mailto link). Per CF
|
|
143
|
-
// docs, Email Obfuscation does not apply when no-transform is present.
|
|
144
|
-
// NOTE: a shared-edge-caching (CDN max-age) variant shipped briefly on 2026-06-10
|
|
145
|
-
// and was REMOVED by operator ruling the same day ("the origin should
|
|
146
|
-
// be so fast we don't care") — the origin-speed work (SWR caches,
|
|
147
|
-
// wave-parallel loader, brotli) is the sanctioned path; re-adding
|
|
148
|
-
// shared-cache headers needs a fresh operator ruling.
|
|
149
137
|
const headers = new Headers(resp.headers as HeadersInit);
|
|
150
138
|
headers.set(
|
|
151
139
|
"Cache-Control",
|
|
@@ -17,6 +17,7 @@ import { join } from "node:path";
|
|
|
17
17
|
import { loadRegistryConfig } from "@teamscala/db/registry/load-config";
|
|
18
18
|
import { userHome } from "@teamscala/os/host-paths";
|
|
19
19
|
import * as v from "valibot";
|
|
20
|
+
import { createCache } from "@teamscala/cache/create-cache";
|
|
20
21
|
|
|
21
22
|
// Single shared host lock serialising the heavy boot-time client build across
|
|
22
23
|
// ALL kind:ui services (client-asset-builds-serialize-via-lock). A publish of a
|
|
@@ -31,10 +32,14 @@ const CLIENT_BUILD_LOCK = join(userHome(), ".scala-client-build.lock");
|
|
|
31
32
|
// correctness gate, and must never block a boot. TTL is DB-driven
|
|
32
33
|
// (config/build-client-assets), promoted from the former hardcoded constant.
|
|
33
34
|
const DEFAULT_CLIENT_BUILD_LOCK_TIMEOUT_MS = 600_000;
|
|
34
|
-
|
|
35
|
+
const clientBuildLockTimeoutCache = createCache<number>(
|
|
36
|
+
"service-boot:client-build-lock-timeout",
|
|
37
|
+
{ ttlMs: Number.POSITIVE_INFINITY, maxSize: 1 },
|
|
38
|
+
);
|
|
35
39
|
|
|
36
40
|
export async function getClientBuildLockTimeoutMs(): Promise<number> {
|
|
37
|
-
|
|
41
|
+
const hit = clientBuildLockTimeoutCache.get("default");
|
|
42
|
+
if (hit !== undefined) return hit;
|
|
38
43
|
// Fail-soft to the hardcoded default: the deploy-build bake runs this in a
|
|
39
44
|
// standalone subprocess with NO registered Prisma client (a pre-DB build
|
|
40
45
|
// context), so loadRegistryConfig throws "Prisma client not registered". The
|
|
@@ -43,13 +48,17 @@ export async function getClientBuildLockTimeoutMs(): Promise<number> {
|
|
|
43
48
|
// registry-read failure must degrade to the default, never abort the bake.
|
|
44
49
|
try {
|
|
45
50
|
const row = await loadRegistryConfig("config", "build-client-assets", v.looseObject({}));
|
|
46
|
-
|
|
51
|
+
clientBuildLockTimeoutCache.set(
|
|
52
|
+
"default",
|
|
47
53
|
(row?.client_build_lock_timeout_ms as number | undefined) ??
|
|
48
|
-
|
|
54
|
+
DEFAULT_CLIENT_BUILD_LOCK_TIMEOUT_MS,
|
|
55
|
+
);
|
|
49
56
|
} catch {
|
|
50
|
-
|
|
57
|
+
clientBuildLockTimeoutCache.set("default", DEFAULT_CLIENT_BUILD_LOCK_TIMEOUT_MS);
|
|
51
58
|
}
|
|
52
|
-
return
|
|
59
|
+
return (
|
|
60
|
+
clientBuildLockTimeoutCache.get("default") ?? DEFAULT_CLIENT_BUILD_LOCK_TIMEOUT_MS
|
|
61
|
+
);
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
export { CLIENT_BUILD_LOCK };
|
|
@@ -33,7 +33,9 @@ export async function removeStaleHashedFiles(
|
|
|
33
33
|
if (re.test(name) && name !== keep) {
|
|
34
34
|
try {
|
|
35
35
|
await rm(join(outDir, name));
|
|
36
|
-
} catch {
|
|
36
|
+
} catch (error) {
|
|
37
|
+
process.stderr.write("hashed-output.ts: bare catch around upstream call — logged, continuing: " + String(error) + "");
|
|
38
|
+
}
|
|
37
39
|
}
|
|
38
40
|
}
|
|
39
41
|
}
|
|
@@ -13,6 +13,57 @@
|
|
|
13
13
|
* else.
|
|
14
14
|
*/
|
|
15
15
|
|
|
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
|
+
// Why: Externalize an unresolvable @teamscala/* block import (a dea…
|
|
31
|
+
// (full note hoisted to this file's leading header — kept out of the body per
|
|
32
|
+
// no-inline-prose-blocks / prose-stays-in-docs)
|
|
33
|
+
// minifyIdentifiers is DISABLED (whitespace + syntax/dead-code minify
|
|
34
|
+
// kept): under code-splitting, identifier mangling interacting with a
|
|
35
|
+
// shared sideEffects:false package (@tanstack/react-query) trips Bun's
|
|
36
|
+
// splitter to tree-shake away the entire implementation and emit a dead
|
|
37
|
+
// re-export facade — a fatal hydration SyntaxError that froze
|
|
38
|
+
// agents.scala.business while every component reported healthy
|
|
39
|
+
// (2026-07-22; reproduces in stable 1.3.14 + canary 1.4.0, so an upstream
|
|
40
|
+
// fix is not near). Whitespace + syntax minify retain the bulk of the
|
|
41
|
+
// size win; only name-mangling is dropped. The post-build dead-facade
|
|
42
|
+
// gate below fails the build LOUD if this or any future regression slips
|
|
43
|
+
// a fatal facade through on any UI service.
|
|
44
|
+
|
|
45
|
+
// Why: minifyIdentifiers is DISABLED (whitespace + syntax/dead-code…
|
|
46
|
+
// (full note hoisted to this file's leading header — kept out of the body per
|
|
47
|
+
// no-inline-prose-blocks / prose-stays-in-docs)
|
|
48
|
+
// dead-facade prevention gate (agents.scala.business 2026-07-22): a Bun
|
|
49
|
+
// splitter+tree-shake bug (stable 1.3.14 + canary 1.4.0) can emit a "dead
|
|
50
|
+
// facade" chunk — `export { ... }` with NO `from` clause and NO local binding
|
|
51
|
+
// definitions — a fatal SyntaxError at module load that kills React hydration
|
|
52
|
+
// SILENTLY (the whole dashboard froze while every component reported healthy).
|
|
53
|
+
// The narrow resilientBlockPlugin filter + minifyIdentifiers:false above
|
|
54
|
+
// prevent the known trigger (@tanstack/react-query under split); this gate
|
|
55
|
+
// makes the CLASS unshippable — any dead facade from ANY package on ANY UI
|
|
56
|
+
// service fails the build LOUD here instead of freezing the page. Detection
|
|
57
|
+
// is POSITION-INDEPENDENT — a named export{} ANYWHERE in the comment-stripped
|
|
58
|
+
// body, with no `from` clause and no local declaration/import backing the
|
|
59
|
+
// names, is the facade (Bun could prefix it with a directive or statement, not
|
|
60
|
+
// only emit it at the very start — a positional assumption is a known way to
|
|
61
|
+
// be silently bypassed; a valid local-export chunk carries a
|
|
62
|
+
// class/function/const/var/let/import, a valid re-export carries `from`).
|
|
63
|
+
|
|
64
|
+
// Why: dead-facade prevention gate (agents.scala.business 2026-07-2…
|
|
65
|
+
// (full note hoisted to this file's leading header — kept out of the body per
|
|
66
|
+
// no-inline-prose-blocks / prose-stays-in-docs)
|
|
16
67
|
import { mkdir, readdir, rename, rm } from "node:fs/promises";
|
|
17
68
|
import { join } from "node:path";
|
|
18
69
|
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
@@ -38,10 +89,8 @@ export async function buildHydrateBundle(
|
|
|
38
89
|
|
|
39
90
|
await mkdir(outDir, { recursive: true });
|
|
40
91
|
|
|
41
|
-
// Resilience (block-and-provider-error-isolation applied to the BUILD): a
|
|
42
|
-
//
|
|
43
|
-
// (a block_schema deactivation or a removed package export whose manifest
|
|
44
|
-
// regen never propagated) MUST NOT fail the whole bundle and crash-loop the
|
|
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
|
|
45
94
|
// service. Seven UI services went down at once this way on 2026-06-13. Such
|
|
46
95
|
// an import is EXTERNALIZED instead: the build succeeds, the service boots,
|
|
47
96
|
// and at runtime that one lazy import rejects into the block's
|
|
@@ -54,31 +103,6 @@ export async function buildHydrateBundle(
|
|
|
54
103
|
const resilientBlockPlugin: Bun.BunPlugin = {
|
|
55
104
|
name: "resilient-block-imports",
|
|
56
105
|
setup(build: { onResolve: (opts: { filter: RegExp }, cb: (args: { path: string }) => { path: string; external: boolean } | undefined) => void }) {
|
|
57
|
-
// Externalize an unresolvable @teamscala/* block import (a deactivated
|
|
58
|
-
// block_schema whose manifest regen never propagated) or a broken
|
|
59
|
-
// third-party internal (@fullcalendar/core/internal.js during a version
|
|
60
|
-
// mismatch) so it degrades ONE block at runtime instead of crash-looping
|
|
61
|
-
// the service (2026-07-21 portal outage: @fullcalendar 7.x + plugins 6.x
|
|
62
|
-
// → unresolvable internal.js → 21 crash-loop restarts).
|
|
63
|
-
//
|
|
64
|
-
// The filter is deliberately NARROW — only the package prefixes where
|
|
65
|
-
// unresolvable imports actually occur — NOT every bare import. The prior
|
|
66
|
-
// broad /^(?![./])/ filter intercepted resolvable shared packages too,
|
|
67
|
-
// and onResolve pass-through (return undefined) on a shared
|
|
68
|
-
// sideEffects:false package (@tanstack/react-query, imported across many
|
|
69
|
-
// lazy block chunks) tripped Bun's code-splitter: it emitted a dead
|
|
70
|
-
// `export{...}`-with-no-`from` facade and tree-shook away the entire
|
|
71
|
-
// query-core implementation → a fatal hydration SyntaxError that froze
|
|
72
|
-
// agents.scala.business while every component reported healthy
|
|
73
|
-
// (2026-07-22; see the dead-facade gate + minifyIdentifiers:false
|
|
74
|
-
// below). Intercepts of node builtins (stream/web) likewise pulled them
|
|
75
|
-
// into the browser build. The narrow filter leaves @tanstack/* + builtins
|
|
76
|
-
// to Bun's native (correct) resolution. A future unresolvable import
|
|
77
|
-
// OUTSIDE these prefixes fails loud via the manifest guard + build
|
|
78
|
-
// tombstone (no-dead-block-imports-in-manifest) — never silent.
|
|
79
|
-
// Entry-point imports are excluded — those must fail loudly (silently
|
|
80
|
-
// externalizing a core import turns a build error into a mystery runtime
|
|
81
|
-
// crash; containment must not become a swallow).
|
|
82
106
|
build.onResolve({ filter: /^(@teamgbg\/|@fullcalendar\/)/ }, (args) => {
|
|
83
107
|
if (args.importer === entry) return undefined;
|
|
84
108
|
try {
|
|
@@ -99,17 +123,6 @@ export async function buildHydrateBundle(
|
|
|
99
123
|
target: "browser",
|
|
100
124
|
outdir: outDir,
|
|
101
125
|
naming: `${HYDRATE_BASE}.js`,
|
|
102
|
-
// minifyIdentifiers is DISABLED (whitespace + syntax/dead-code minify
|
|
103
|
-
// kept): under code-splitting, identifier mangling interacting with a
|
|
104
|
-
// shared sideEffects:false package (@tanstack/react-query) trips Bun's
|
|
105
|
-
// splitter to tree-shake away the entire implementation and emit a dead
|
|
106
|
-
// re-export facade — a fatal hydration SyntaxError that froze
|
|
107
|
-
// agents.scala.business while every component reported healthy
|
|
108
|
-
// (2026-07-22; reproduces in stable 1.3.14 + canary 1.4.0, so an upstream
|
|
109
|
-
// fix is not near). Whitespace + syntax minify retain the bulk of the
|
|
110
|
-
// size win; only name-mangling is dropped. The post-build dead-facade
|
|
111
|
-
// gate below fails the build LOUD if this or any future regression slips
|
|
112
|
-
// a fatal facade through on any UI service.
|
|
113
126
|
minifyWhitespace: process.env.NODE_ENV === "production",
|
|
114
127
|
minifySyntax: process.env.NODE_ENV === "production",
|
|
115
128
|
minifyIdentifiers: false,
|
|
@@ -186,21 +199,6 @@ export async function buildHydrateBundle(
|
|
|
186
199
|
throw new Error(`[build-client-assets] Bun.build failed:\n${messages}`);
|
|
187
200
|
}
|
|
188
201
|
|
|
189
|
-
// dead-facade prevention gate (agents.scala.business 2026-07-22): a Bun
|
|
190
|
-
// splitter+tree-shake bug (stable 1.3.14 + canary 1.4.0) can emit a "dead
|
|
191
|
-
// facade" chunk — `export { ... }` with NO `from` clause and NO local binding
|
|
192
|
-
// definitions — a fatal SyntaxError at module load that kills React hydration
|
|
193
|
-
// SILENTLY (the whole dashboard froze while every component reported healthy).
|
|
194
|
-
// The narrow resilientBlockPlugin filter + minifyIdentifiers:false above
|
|
195
|
-
// prevent the known trigger (@tanstack/react-query under split); this gate
|
|
196
|
-
// makes the CLASS unshippable — any dead facade from ANY package on ANY UI
|
|
197
|
-
// service fails the build LOUD here instead of freezing the page. Detection
|
|
198
|
-
// is POSITION-INDEPENDENT — a named export{} ANYWHERE in the comment-stripped
|
|
199
|
-
// body, with no `from` clause and no local declaration/import backing the
|
|
200
|
-
// names, is the facade (Bun could prefix it with a directive or statement, not
|
|
201
|
-
// only emit it at the very start — a positional assumption is a known way to
|
|
202
|
-
// be silently bypassed; a valid local-export chunk carries a
|
|
203
|
-
// class/function/const/var/let/import, a valid re-export carries `from`).
|
|
204
202
|
const deadFacades: string[] = [];
|
|
205
203
|
for (const out of result.outputs) {
|
|
206
204
|
if (out.kind !== "chunk") continue;
|
|
@@ -258,7 +256,9 @@ export async function buildHydrateBundle(
|
|
|
258
256
|
for (const name of currentChunks) {
|
|
259
257
|
try {
|
|
260
258
|
chunkBytes += (await Bun.file(join(outDir, name)).stat()).size;
|
|
261
|
-
} catch {
|
|
259
|
+
} catch (error) {
|
|
260
|
+
log.warn("lazy chunk size read failed — chunk excluded from size log", error);
|
|
261
|
+
}
|
|
262
262
|
}
|
|
263
263
|
log.info(
|
|
264
264
|
`[build-client-assets] hydrate built → ${hashedName} (entry ${(entryBytes / 1024).toFixed(0)}KB + ${currentChunks.size} lazy chunks ${(chunkBytes / 1048576).toFixed(1)}MB)`,
|
|
@@ -292,7 +292,9 @@ async function removeStaleChunks(
|
|
|
292
292
|
if (/^chunk-[A-Za-z0-9]+\.js$/.test(name) && !keep.has(name)) {
|
|
293
293
|
try {
|
|
294
294
|
await rm(join(outDir, name));
|
|
295
|
-
} catch {
|
|
295
|
+
} catch (error) {
|
|
296
|
+
getAppLogger().warn("hydrate-bundle.ts: bare catch around upstream call — logged, continuing (protective-layers-fail-explicitly)", error);
|
|
297
|
+
}
|
|
296
298
|
}
|
|
297
299
|
}
|
|
298
300
|
}
|