@teamscala/service-boot 0.23.516 → 0.23.518
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 +232 -1127
- package/src/agreement-gate/settings.ts +8 -8
- package/src/apply-logger-config.test.ts +46 -0
- package/src/apply-logger-config.ts +31 -0
- package/src/asset-proxy.test.ts +9 -5
- package/src/auth/sign-out.ts +122 -0
- package/src/build-client-assets/hydrate-bundle.ts +14 -4
- package/src/build-client-assets/manifest.ts +5 -18
- package/src/build-client-assets/styles-build.test.ts +11 -8
- package/src/build-client-assets/styles-build.ts +14 -63
- package/src/build-client-assets.test.ts +0 -161
- package/src/build-client-assets.ts +20 -67
- package/src/content-gate/token.ts +9 -2
- package/src/install-logging.ts +10 -26
- package/src/mount-orpc-router.ts +8 -2
- package/src/mutable-app-ref.ts +0 -7
- package/src/page-server-loader-graph.test.ts +31 -0
- package/src/page-server-loader-graph.ts +21 -0
- package/src/pg-boss-mount.ts +3 -16
- package/src/readiness-gate.ts +0 -4
- package/src/register-api-routes.ts +8 -17
- package/src/register-page-server-client.ts +6 -0
- package/src/register-registry-page-route/meta-title.test.ts +50 -0
- package/src/register-registry-page-route/meta-title.ts +1 -1
- package/src/register-registry-page-route/page-data-shared.ts +7 -11
- package/src/register-registry-page-route/resolve-request-graph.test.ts +1 -1
- package/src/register-registry-page-route/resolve-request-graph.ts +1 -2
- package/src/register-registry-page-route.ts +14 -16
- package/src/resolve-brand-favicon.test.ts +63 -0
- package/src/resolve-theme-mode.test.ts +51 -0
- package/src/resolvers/fetch-org-brand.test.ts +48 -0
- package/src/resolvers/website-by-host-map.test.ts +63 -0
- package/src/resolvers/website-by-host-map.ts +15 -3
- package/src/run-boot-hook-list.test.ts +47 -0
- package/src/run-boot-hooks.test.ts +56 -0
- package/src/scala-tools-currency-watchdog.test.ts +43 -0
- package/src/scala-tools-currency-watchdog.ts +0 -5
- package/src/transfers/presigned-upload.ts +165 -0
- package/src/two-phase-minimal-app.test.ts +24 -0
- package/src/two-phase-readiness.test.ts +102 -0
- package/src/types.test.ts +20 -0
- package/src/ui-service.test.ts +24 -0
- package/src/ws-boot-self-test.test.ts +43 -0
- package/src/build-client-assets/input-fingerprint.ts +0 -92
- package/src/build-client-assets/input-hashing.ts +0 -206
- package/src/handlers/bun-stable-watch.ts +0 -86
- package/src/handlers/messaging-send-reconcile-logic.test.ts +0 -79
- package/src/handlers/messaging-send-reconcile-logic.ts +0 -83
- package/src/handlers/messaging-send-recording-logic.test.ts +0 -86
- package/src/handlers/messaging-send-recording-logic.ts +0 -68
- package/src/handlers/messaging-send-recording-watch.test.ts +0 -91
- package/src/handlers/messaging-send-recording-watch.ts +0 -244
- package/src/handlers/messaging-send-sweep-selection.test.ts +0 -47
- package/src/handlers/messaging-send-sweep-selection.ts +0 -35
- package/src/handlers/platform-fault-escalation/config.ts +0 -95
- package/src/handlers/platform-fault-escalation/delivery.ts +0 -327
- package/src/handlers/platform-fault-escalation/event-log-queries.ts +0 -182
- package/src/handlers/platform-fault-escalation/orpc-client.ts +0 -114
- package/src/handlers/platform-fault-escalation/probe.test.ts +0 -63
- package/src/handlers/platform-fault-escalation/probe.ts +0 -19
- package/src/handlers/platform-fault-escalation-logic.ts +0 -382
- package/src/handlers/platform-fault-escalation-refresh.test.ts +0 -72
- package/src/handlers/platform-fault-escalation.test.ts +0 -497
- package/src/handlers/platform-fault-escalation.ts +0 -246
- package/src/handlers/projects-digest.ts +0 -198
- package/src/handlers/quiz-research.ts +0 -178
- package/src/handlers/registry-stale-inactive-sweep.ts +0 -82
- package/src/handlers/scala-tools-currency-watch.test.ts +0 -65
- package/src/handlers/scala-tools-currency-watch.ts +0 -162
- package/src/handlers/singleton-tools-currency-watch.test.ts +0 -51
- package/src/handlers/singleton-tools-currency-watch.ts +0 -380
- package/src/handlers/sweep-event-log.ts +0 -98
- package/src/handlers/upstream-issue-sync.ts +0 -220
- package/src/handlers/whatsapp-liveness-handler.test.ts +0 -56
- package/src/handlers/whatsapp-liveness-logic.ts +0 -53
- package/src/handlers/whatsapp-liveness.test.ts +0 -78
- package/src/handlers/whatsapp-liveness.ts +0 -138
- package/src/interpolate-meta.ts +0 -123
- package/src/load-service-prisma-client.ts +0 -53
- package/src/loader-errors.ts +0 -41
- package/src/read-viewport-from-request.ts +0 -36
- package/src/register-page-server-client.test.ts +0 -185
- package/src/resolve-app-base-url.ts +0 -35
- package/src/resolve-from-loader-config.test.ts +0 -429
- package/src/resolve-from-loader-config.ts +0 -316
- package/src/resolve-service-context.ts +0 -88
|
@@ -17,25 +17,18 @@
|
|
|
17
17
|
* Without this, `cache-control: immutable` on a fixed filename means
|
|
18
18
|
* returning visitors keep their stale copy forever.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* same pass so dist/client/ doesn't accumulate dead files.
|
|
33
|
-
*
|
|
34
|
-
* This module is the ORCHESTRATION unit: fingerprint check → host-wide lock
|
|
35
|
-
* → parallel hydrate/CSS builds → manifest write (or failure tombstone). The
|
|
36
|
-
* individual concerns live as siblings under ./build-client-assets/
|
|
37
|
-
* (input-hashing, source-candidates, input-fingerprint, manifest,
|
|
38
|
-
* hashed-output, hydrate-bundle, sourcemaps-upload, styles-build, build-lock)
|
|
20
|
+
* This module is the ORCHESTRATION unit: host-wide lock → parallel
|
|
21
|
+
* hydrate/CSS builds → manifest write (or failure tombstone). The
|
|
22
|
+
* build runs on EVERY boot: the content-fingerprint skip cache and its
|
|
23
|
+
* closure-walking hashers (input-fingerprint.ts, input-hashing.ts) are
|
|
24
|
+
* deleted. A skip is the only path that can serve a stale bundle, and
|
|
25
|
+
* it has done so twice (the mtime skip that baked stale bundles until
|
|
26
|
+
* the fingerprint replaced it, 2026-06-14) — no skip is the one shape
|
|
27
|
+
* that cannot skip wrongly, at the cost of a bounded per-boot build
|
|
28
|
+
* serialised by the host-wide lock below. The individual concerns live
|
|
29
|
+
* as siblings under ./build-client-assets/
|
|
30
|
+
* (source-candidates, manifest, hashed-output, hydrate-bundle,
|
|
31
|
+
* sourcemaps-upload, styles-build, build-lock)
|
|
39
32
|
* — split when the parent held them all at 962 lines.
|
|
40
33
|
*/
|
|
41
34
|
|
|
@@ -44,9 +37,10 @@
|
|
|
44
37
|
// manifest entry that points at a file we don't actually serve. If a build fails, write a TOMBSTONE manifest (no-silent-reverts) and throw.
|
|
45
38
|
//
|
|
46
39
|
// The tombstone preserves the LAST-SUCCESSFUL paths so SSR can keep serving the old bundle (better than a 404 hydrate), but marks itself
|
|
47
|
-
// `buildFailed: true` + the error + `failedAt`, AND
|
|
48
|
-
//
|
|
49
|
-
// on-disk failure signal; ui-service-boot adds the
|
|
40
|
+
// `buildFailed: true` + the error + `failedAt`, AND — because every boot
|
|
41
|
+
// now rebuilds unconditionally — the failure is retried by construction;
|
|
42
|
+
// the tombstone is the on-disk failure signal; ui-service-boot adds the
|
|
43
|
+
// runtime signal (recordEvent → event-log → GlitchTip) and read-html-template stamps the
|
|
50
44
|
// served HTML with the failure status. Without the tombstone, a build failure left the OLD manifest untouched and the dashboard served stale
|
|
51
45
|
// code with NO signal that the published fix never reached the bundle (scala-agents 2026-07-18: ui-foundation@3.4.5 installed but the bundle
|
|
52
46
|
// kept serving the old bytes across restarts while node_modules was mid-install — invisible until the operator hit the white-on-white
|
|
@@ -58,15 +52,10 @@ import { withFileLock } from "@teamscala/file-lock/with-lock";
|
|
|
58
52
|
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
59
53
|
import { CLIENT_BUILD_LOCK, getClientBuildLockTimeoutMs } from "./build-client-assets/build-lock.ts";
|
|
60
54
|
import { buildHydrateBundle } from "./build-client-assets/hydrate-bundle.ts";
|
|
61
|
-
import { computeInputFingerprint } from "./build-client-assets/input-fingerprint.ts";
|
|
62
55
|
import {
|
|
63
56
|
readManifest,
|
|
64
57
|
writeManifest,
|
|
65
58
|
} from "./build-client-assets/manifest.ts";
|
|
66
|
-
import {
|
|
67
|
-
releaseFromHydratePath,
|
|
68
|
-
uploadCachedSourceMaps,
|
|
69
|
-
} from "./build-client-assets/sourcemaps-upload.ts";
|
|
70
59
|
import { buildTailwindCss } from "./build-client-assets/styles-build.ts";
|
|
71
60
|
|
|
72
61
|
export async function buildClientAssets(serviceSlug: string): Promise<void> {
|
|
@@ -74,41 +63,10 @@ export async function buildClientAssets(serviceSlug: string): Promise<void> {
|
|
|
74
63
|
const cwd = process.cwd();
|
|
75
64
|
const outDir = join(cwd, "dist", "client");
|
|
76
65
|
|
|
77
|
-
|
|
78
|
-
const fingerprint = await computeInputFingerprint(cwd);
|
|
79
|
-
if (existingManifest && existingManifest.inputFingerprint === fingerprint) {
|
|
80
|
-
// The bundle is fresh, but a fresh bundle with an UNVERIFIED source-map
|
|
81
|
-
// upload must not stay that way forever: retry the upload from the
|
|
82
|
-
// retained map cache on every boot until it verifies. Without this, one
|
|
83
|
-
// failed boot-time upload (GlitchTip down, token missing at that boot)
|
|
84
|
-
// permanently lost the release's maps — client stacks stayed minified
|
|
85
|
-
// until the next real input change forced a rebuild.
|
|
86
|
-
const recorded = existingManifest.sourceMaps?.status;
|
|
87
|
-
const release = existingManifest.hydratePath
|
|
88
|
-
? releaseFromHydratePath(existingManifest.hydratePath)
|
|
89
|
-
: null;
|
|
90
|
-
if (recorded !== "uploaded" && release) {
|
|
91
|
-
log.info(
|
|
92
|
-
`[build-client-assets:${serviceSlug}] manifest up-to-date (skip); source maps previously ${recorded ?? "unrecorded"} — retrying upload for release ${release}`,
|
|
93
|
-
);
|
|
94
|
-
const sourceMaps = await uploadCachedSourceMaps(cwd, release, log);
|
|
95
|
-
await writeManifest(outDir, { ...existingManifest, sourceMaps });
|
|
96
|
-
} else {
|
|
97
|
-
log.info(`[build-client-assets:${serviceSlug}] manifest up-to-date (skip)`);
|
|
98
|
-
}
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
if (existingManifest && existingManifest.inputFingerprint !== fingerprint) {
|
|
102
|
-
log.info(
|
|
103
|
-
`[build-client-assets:${serviceSlug}] inputs changed (fingerprint ${existingManifest.inputFingerprint ?? "<none>"} → ${fingerprint}) — rebuilding client bundle`,
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// A build IS needed. Serialise it across every UI service on this host
|
|
66
|
+
// Serialise the build across every UI service on this host
|
|
108
67
|
// (client-asset-builds-serialize-via-lock) so a publish-driven restart storm
|
|
109
68
|
// becomes a short queue instead of N concurrent host-saturating builds. The
|
|
110
|
-
// lock
|
|
111
|
-
// FAILS OPEN: a lock-acquisition timeout builds unlocked rather than blocking
|
|
69
|
+
// lock FAILS OPEN: a lock-acquisition timeout builds unlocked rather than blocking
|
|
112
70
|
// boot. A real build failure inside performBuild throws and propagates (the
|
|
113
71
|
// service refuses to start); only the file-lock timeout falls through.
|
|
114
72
|
const lockTimeoutMs = await getClientBuildLockTimeoutMs();
|
|
@@ -116,7 +74,7 @@ export async function buildClientAssets(serviceSlug: string): Promise<void> {
|
|
|
116
74
|
await withFileLock(
|
|
117
75
|
CLIENT_BUILD_LOCK,
|
|
118
76
|
{ timeoutMs: lockTimeoutMs },
|
|
119
|
-
() => performBuild(serviceSlug, cwd, outDir, log
|
|
77
|
+
() => performBuild(serviceSlug, cwd, outDir, log),
|
|
120
78
|
);
|
|
121
79
|
} catch (err) {
|
|
122
80
|
const isLockTimeout =
|
|
@@ -126,7 +84,7 @@ export async function buildClientAssets(serviceSlug: string): Promise<void> {
|
|
|
126
84
|
log.warn(
|
|
127
85
|
`[build-client-assets:${serviceSlug}] build lock timed out after ${lockTimeoutMs}ms — building unlocked (fail-open)`,
|
|
128
86
|
);
|
|
129
|
-
await performBuild(serviceSlug, cwd, outDir, log
|
|
87
|
+
await performBuild(serviceSlug, cwd, outDir, log);
|
|
130
88
|
}
|
|
131
89
|
}
|
|
132
90
|
|
|
@@ -135,7 +93,6 @@ async function performBuild(
|
|
|
135
93
|
cwd: string,
|
|
136
94
|
outDir: string,
|
|
137
95
|
log: ReturnType<typeof getAppLogger>,
|
|
138
|
-
inputFingerprint: string,
|
|
139
96
|
): Promise<void> {
|
|
140
97
|
const t0 = Date.now();
|
|
141
98
|
|
|
@@ -157,9 +114,6 @@ async function performBuild(
|
|
|
157
114
|
...(existingManifest?.stylesPath && existingManifest?.hydratePath
|
|
158
115
|
? { stylesPath: existingManifest.stylesPath, hydratePath: existingManifest.hydratePath }
|
|
159
116
|
: {}),
|
|
160
|
-
// Failure sentinel: the next boot's fingerprint check reads
|
|
161
|
-
// this and cannot match any real fingerprint → forces a retry.
|
|
162
|
-
inputFingerprint: `failed:${new Date().toISOString()}`,
|
|
163
117
|
buildFailed: true,
|
|
164
118
|
buildFailedReason: reason.slice(0, 2000),
|
|
165
119
|
buildFailedAt: new Date().toISOString(),
|
|
@@ -221,7 +175,6 @@ async function performBuild(
|
|
|
221
175
|
await writeManifest(outDir, {
|
|
222
176
|
stylesPath,
|
|
223
177
|
hydratePath: hydrate.hydratePath,
|
|
224
|
-
inputFingerprint,
|
|
225
178
|
sourceMaps: hydrate.sourceMaps,
|
|
226
179
|
});
|
|
227
180
|
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
* (e.g. ag_token) can never satisfy another gate's check (e.g. prop_token).
|
|
17
17
|
*
|
|
18
18
|
* Secret resolver, in preference order: AUTH_SECRET → ENCRYPTION_KEY →
|
|
19
|
-
*
|
|
19
|
+
* the tenant DB address via @teamscala/db's named-need resolver (first one
|
|
20
|
+
* ≥16 chars). A gate signs AND verifies inside one
|
|
20
21
|
* service, so it only needs a stable per-deployment secret; the fallback keeps a
|
|
21
22
|
* gate working on a service that was never given AUTH_SECRET (e.g. the former
|
|
22
23
|
* public-only proposals viewer) instead of failing closed and locking out every
|
|
@@ -25,12 +26,18 @@
|
|
|
25
26
|
*/
|
|
26
27
|
|
|
27
28
|
import type { GateSegmentation } from "./config-types.ts";
|
|
29
|
+
import { resolveDbUrlOrNull } from "@teamscala/db/db-url";
|
|
28
30
|
|
|
29
31
|
function secret(): string | null {
|
|
32
|
+
// The last resort is the tenant DB address, read through the named-need
|
|
33
|
+
// resolver (wrong-database guard, 2026-09-27) — never `process.env` here;
|
|
34
|
+
// the resolver is the only env-aware surface and it names the plane. It is
|
|
35
|
+
// used only as a stable per-deployment string (≥16 chars), never as a
|
|
36
|
+
// connection target.
|
|
30
37
|
for (const s of [
|
|
31
38
|
process.env.AUTH_SECRET,
|
|
32
39
|
process.env.ENCRYPTION_KEY,
|
|
33
|
-
|
|
40
|
+
resolveDbUrlOrNull("tenant"),
|
|
34
41
|
]) {
|
|
35
42
|
if (s && s.length >= 16) return s;
|
|
36
43
|
}
|
package/src/install-logging.ts
CHANGED
|
@@ -10,17 +10,18 @@
|
|
|
10
10
|
* loga (dev) with full context. Application code never imports the logger
|
|
11
11
|
* or wraps anything in try/catch for telemetry purposes.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* Two phases, two files (single-purpose-file: the guard counts them as two
|
|
14
|
+
* export groups in one file): installLogging (early bootstrap, hardcoded
|
|
15
|
+
* defaults) lives here; applyLoggerConfig (late-stage, DB-driven) lives in
|
|
16
|
+
* apply-logger-config.ts. The service boot pipeline calls them in sequence
|
|
17
|
+
* with the same serviceSlug.
|
|
17
18
|
*/
|
|
18
19
|
|
|
19
20
|
import { ManagedCache } from "@teamscala/cache/cache";
|
|
20
21
|
import { createCache } from "@teamscala/cache/create-cache";
|
|
21
22
|
import { createDiskCache } from "@teamscala/cache/disk-cache";
|
|
22
23
|
import { join } from "node:path";
|
|
23
|
-
import {
|
|
24
|
+
import { stateRoot } from "@teamscala/os/host-paths";
|
|
24
25
|
import { slugify } from "@teamscala/formatters/slugify";
|
|
25
26
|
import { createTimer } from "@teamscala/timing/create-timer";
|
|
26
27
|
import { configure as dbConfigure } from "@teamscala/db/configure";
|
|
@@ -29,8 +30,6 @@ import { getEntriesByType as dbGetEntriesByType } from "@teamscala/db/registry/g
|
|
|
29
30
|
import { loadRegistryConfig as dbLoadRegistryConfig } from "@teamscala/db/registry/load-config";
|
|
30
31
|
import { configure as httpConfigure } from "@teamscala/http/configure";
|
|
31
32
|
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
32
|
-
import { setMinLevel } from "@teamscala/logger/creator";
|
|
33
|
-
import { configureFileSink } from "@teamscala/logger/file-sink";
|
|
34
33
|
import { initSentry } from "@teamscala/logger/init-sentry";
|
|
35
34
|
import { attachRequestLogger } from "@teamscala/logger/install-logging";
|
|
36
35
|
import { getRequestContext } from "@teamscala/logger/request-context-async";
|
|
@@ -75,8 +74,11 @@ export function installLogging(opts: InstallLoggingOptions): void {
|
|
|
75
74
|
// (runtime tier) so host-path resolution stays out of the foundation-tier
|
|
76
75
|
// db package, which cannot import @teamscala/cache or resolve userHome itself.
|
|
77
76
|
// load-config serves this on a transient DB failure instead of crash-looping.
|
|
77
|
+
// The root is stateRoot(), not userHome(): a container that declares a
|
|
78
|
+
// read-only HOME (scala-sign prod: HOME=/var/empty) made this cache EACCES on
|
|
79
|
+
// every request — the tier died in exactly the outage class it exists for.
|
|
78
80
|
const registryConfigDiskCache = createDiskCache("db:registry-config", {
|
|
79
|
-
dir: join(
|
|
81
|
+
dir: join(stateRoot(), ".scala-db-cache", "registry-config"),
|
|
80
82
|
ttlMs: 24 * 60 * 60 * 1000,
|
|
81
83
|
});
|
|
82
84
|
dbConfigure({
|
|
@@ -110,24 +112,6 @@ export function installLogging(opts: InstallLoggingOptions): void {
|
|
|
110
112
|
// in service_install.server_packages and pick up the configures there.
|
|
111
113
|
}
|
|
112
114
|
|
|
113
|
-
/** Late-stage configuration — runs AFTER Registry is loaded.
|
|
114
|
-
* Injects DB-driven settings into the logger primitive. */
|
|
115
|
-
export function applyLoggerConfig(config: {
|
|
116
|
-
minLevel?: "debug" | "info" | "warn" | "error";
|
|
117
|
-
logDir?: string;
|
|
118
|
-
maxFileSize?: number;
|
|
119
|
-
maxFiles?: number;
|
|
120
|
-
}): void {
|
|
121
|
-
if (config.minLevel) {
|
|
122
|
-
setMinLevel(config.minLevel);
|
|
123
|
-
}
|
|
124
|
-
configureFileSink({
|
|
125
|
-
logDir: config.logDir,
|
|
126
|
-
maxFileSize: config.maxFileSize,
|
|
127
|
-
maxFiles: config.maxFiles,
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
115
|
function installConsoleShim(serviceSlug: string): void {
|
|
132
116
|
const log = getAppLogger();
|
|
133
117
|
const methodMap: Record<
|
package/src/mount-orpc-router.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
14
14
|
import type { Db8Client } from "@teamscala/db/db8-registry";
|
|
15
|
+
import { resolveDbUrlOrNull } from "@teamscala/db/db-url";
|
|
15
16
|
import type { Hono } from "hono";
|
|
16
17
|
import { bodyLimit } from "hono/body-limit";
|
|
17
18
|
|
|
@@ -131,12 +132,17 @@ export async function mountOrpcRouter(
|
|
|
131
132
|
// closed over by the request handler (connects lazily on first query).
|
|
132
133
|
let db8: Db8Client | undefined;
|
|
133
134
|
try {
|
|
134
|
-
|
|
135
|
+
// Named-need address (wrong-database guard, 2026-09-27): the tenant
|
|
136
|
+
// plane by name through @teamscala/db's resolver, never an env key
|
|
137
|
+
// read at this call site. Null = the deploy wrote nothing for this
|
|
138
|
+
// plane, and db8 stays undefined on the existing absence path.
|
|
139
|
+
const databaseUrl = resolveDbUrlOrNull("tenant");
|
|
140
|
+
if (databaseUrl) {
|
|
135
141
|
const { createDbClientV8 } = await import(
|
|
136
142
|
"@teamscala/db/create-db-client-v8"
|
|
137
143
|
);
|
|
138
144
|
db8 = (await createDbClientV8({
|
|
139
|
-
connectionString:
|
|
145
|
+
connectionString: databaseUrl,
|
|
140
146
|
serviceName: serviceSlug,
|
|
141
147
|
})) as never;
|
|
142
148
|
// Register for db-core internals (registry loaders, raw fallbacks)
|
package/src/mutable-app-ref.ts
CHANGED
|
@@ -36,13 +36,6 @@ export function getApp(): Hono {
|
|
|
36
36
|
return currentApp;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
/**
|
|
40
|
-
* Get the current app label for logging.
|
|
41
|
-
*/
|
|
42
|
-
export function getAppLabel(): string {
|
|
43
|
-
return appLabel;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
39
|
/**
|
|
47
40
|
* Atomically swap the app to a new instance. This is called after the
|
|
48
41
|
* full app is built in the background. Returns the previous label for logging.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @status generated — ts-api-smoke template (scala-test-suites
|
|
3
|
+
* templates/ts_api_smoke.pkl @1.3.0) over the exports map of @teamscala/service-boot.
|
|
4
|
+
* ./page-server-loader-graph declares 1 runtime export(s) and 1 declared behaviour vector(s);
|
|
5
|
+
* this suite asserts the import resolves, the runtime surface matches
|
|
6
|
+
* exactly, and every export carries its declared kind. Regenerate from
|
|
7
|
+
* inputs/bun-server-service-boot/api_smoke.json (scala-test-suites) — hand edits are
|
|
8
|
+
* overwritten.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, expect, it } from "bun:test";
|
|
12
|
+
|
|
13
|
+
const mod = await import("./page-server-loader-graph");
|
|
14
|
+
|
|
15
|
+
const { resolveFromLoaderConfig } = mod;
|
|
16
|
+
|
|
17
|
+
describe("page-server-loader-graph exports-map smoke", () => {
|
|
18
|
+
it("the runtime export set is exactly the declared surface", () => {
|
|
19
|
+
expect(Object.keys(mod).sort()).toEqual([
|
|
20
|
+
"resolveFromLoaderConfig",
|
|
21
|
+
]);
|
|
22
|
+
});
|
|
23
|
+
it("resolveFromLoaderConfig is a function", () => {
|
|
24
|
+
expect(typeof mod["resolveFromLoaderConfig"]).toBe("function");
|
|
25
|
+
});
|
|
26
|
+
it("the SSR binding is a resolver PRODUCT of the engine factory applied to the page-server client — never the factory itself", async () => {
|
|
27
|
+
const { createLoaderConfigResolver } = await import("@teamscala/resolver-engine/loader-config/resolve-from-loader-config");
|
|
28
|
+
expect(typeof createLoaderConfigResolver).toBe("function");
|
|
29
|
+
expect(resolveFromLoaderConfig).not.toBe(createLoaderConfigResolver);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system service-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* page-server-loader-graph.ts — binds the ONE loader-graph resolver
|
|
7
|
+
* (@teamscala/resolver-engine's createLoaderConfigResolver) to the page-server
|
|
8
|
+
* SSR client. The resolver used to be duplicated here with its own algorithm;
|
|
9
|
+
* the copies drifted (a non-UUID URL ref resolved absent on this surface and
|
|
10
|
+
* still threw into the render breaker on the fn-router surface), so the
|
|
11
|
+
* algorithm became one injected function and each surface binds its own
|
|
12
|
+
* client source. This module IS that binding for the SSR surface — the single
|
|
13
|
+
* place the page-server client and the loader-graph resolver meet.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { createLoaderConfigResolver } from "@teamscala/resolver-engine/loader-config/resolve-from-loader-config";
|
|
17
|
+
import { getServerClient } from "./page-server-orpc/server-client.ts";
|
|
18
|
+
|
|
19
|
+
export const resolveFromLoaderConfig = createLoaderConfigResolver(
|
|
20
|
+
getServerClient,
|
|
21
|
+
);
|
package/src/pg-boss-mount.ts
CHANGED
|
@@ -21,22 +21,9 @@ import type { ScheduleOptions } from "pg-boss";
|
|
|
21
21
|
import { PgBoss } from "pg-boss";
|
|
22
22
|
import { pgBossHandlerRegistry } from "./pg-boss-handler-registry.ts";
|
|
23
23
|
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
import "./handlers/sweep-event-log.ts";
|
|
28
|
-
// `sweep-host-cache` is Rust-native: loga (the job's service_slug) registers it
|
|
29
|
-
// via scala_os::host_cache_retention with the config/cleanup-allowed-roots
|
|
30
|
-
// bound. No TypeScript copy may exist beside it.
|
|
31
|
-
import "./handlers/bun-stable-watch.ts";
|
|
32
|
-
import "./handlers/whatsapp-liveness.ts";
|
|
33
|
-
import "./handlers/messaging-send-recording-watch.ts";
|
|
34
|
-
import "./handlers/upstream-issue-sync.ts";
|
|
35
|
-
import "./handlers/scala-tools-currency-watch.ts";
|
|
36
|
-
import "./handlers/singleton-tools-currency-watch.ts";
|
|
37
|
-
import "./handlers/registry-stale-inactive-sweep.ts";
|
|
38
|
-
import "./handlers/platform-fault-escalation.ts";
|
|
39
|
-
import "./handlers/quiz-research.ts";
|
|
24
|
+
// Handlers register via their package's own boot hook / subscriber import
|
|
25
|
+
// (package-subscriber shape) — no bundled copies. The mount resolves each
|
|
26
|
+
// row's handler_slug against this registry at dispatch time.
|
|
40
27
|
|
|
41
28
|
|
|
42
29
|
/**
|
package/src/readiness-gate.ts
CHANGED
|
@@ -111,23 +111,14 @@ export function registerApiRoutes(
|
|
|
111
111
|
count++;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
// CORS
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
status: 204,
|
|
123
|
-
headers: {
|
|
124
|
-
"Access-Control-Allow-Origin": origin,
|
|
125
|
-
"Access-Control-Allow-Credentials": "true",
|
|
126
|
-
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
127
|
-
"Access-Control-Allow-Headers": "Content-Type",
|
|
128
|
-
},
|
|
129
|
-
});
|
|
130
|
-
});
|
|
114
|
+
// NO hand-rolled OPTIONS/CORS here: two-phase-minimal-app registers the
|
|
115
|
+
// official hono/cors middleware GLOBALLY before this runs, and its OPTIONS
|
|
116
|
+
// branch answers preflights without calling next() — a hand preflight
|
|
117
|
+
// handler registered here is shadowed dead code (measured: zero
|
|
118
|
+
// invocations across trusted/no/foreign-origin preflights, bun probe
|
|
119
|
+
// 2026-09-27). The middleware also decorates ACTUAL cross-origin
|
|
120
|
+
// responses and reflects origins only inside the cookie domain — the
|
|
121
|
+
// dead copy reflected ANY origin with credentials.
|
|
131
122
|
|
|
132
123
|
return count;
|
|
133
124
|
}
|
|
@@ -7,6 +7,12 @@
|
|
|
7
7
|
* the platform-owned routers into the service's generated `appRouter` and
|
|
8
8
|
* stashes the resulting client via setServerClient. Called once at boot before
|
|
9
9
|
* any page route handler fires.
|
|
10
|
+
*
|
|
11
|
+
* Handwritten because no machine-readable contract generates it: this is the
|
|
12
|
+
* EXECUTOR half of a rows+executor pair (`business-logic-is-rows-plus-executors`),
|
|
13
|
+
* whose declared half is the `service_install`/`service_schema` rows read at
|
|
14
|
+
* boot. There is no supplier spec, CLI or registry row that could emit this
|
|
15
|
+
* boot-time wiring, so it stays hand-written; the row half remains the source.
|
|
10
16
|
*/
|
|
11
17
|
|
|
12
18
|
// REFUSE, DO NOT SKIP. `existsSync(routerPath)` being false used to fall straight through with appRouter=null and still register a client + log
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @status generated — ts-api-smoke template (scala-test-suites
|
|
3
|
+
* templates/ts_api_smoke.pkl @1.3.0) over the exports map of @teamscala/service-boot.
|
|
4
|
+
* ./meta-title declares 3 runtime export(s) and 4 declared behaviour vector(s);
|
|
5
|
+
* this suite asserts the import resolves, the runtime surface matches
|
|
6
|
+
* exactly, and every export carries its declared kind. Regenerate from
|
|
7
|
+
* inputs/bun-server-service-boot/api_smoke.json (scala-test-suites) — hand edits are
|
|
8
|
+
* overwritten.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, expect, it } from "bun:test";
|
|
12
|
+
|
|
13
|
+
const mod = await import("./meta-title");
|
|
14
|
+
|
|
15
|
+
const { buildMetaFromContext, resolvePageMetaTitle, resolveEntityTitle } = mod;
|
|
16
|
+
|
|
17
|
+
describe("meta-title exports-map smoke", () => {
|
|
18
|
+
it("the runtime export set is exactly the declared surface", () => {
|
|
19
|
+
expect(Object.keys(mod).sort()).toEqual([
|
|
20
|
+
"buildMetaFromContext",
|
|
21
|
+
"resolveEntityTitle",
|
|
22
|
+
"resolvePageMetaTitle",
|
|
23
|
+
]);
|
|
24
|
+
});
|
|
25
|
+
it("buildMetaFromContext is a function", () => {
|
|
26
|
+
expect(typeof mod["buildMetaFromContext"]).toBe("function");
|
|
27
|
+
});
|
|
28
|
+
it("resolvePageMetaTitle is a function", () => {
|
|
29
|
+
expect(typeof mod["resolvePageMetaTitle"]).toBe("function");
|
|
30
|
+
});
|
|
31
|
+
it("resolveEntityTitle is a function", () => {
|
|
32
|
+
expect(typeof mod["resolveEntityTitle"]).toBe("function");
|
|
33
|
+
});
|
|
34
|
+
it("an absent meta context resolves no meta at all", async () => {
|
|
35
|
+
const got = buildMetaFromContext(undefined, { page: { name: "Quiz" } });
|
|
36
|
+
expect(got).toEqual({});
|
|
37
|
+
});
|
|
38
|
+
it("the page's own meta_title wins, from loaderData first", async () => {
|
|
39
|
+
const got = resolvePageMetaTitle({ loaderData: { page: { meta_title: "Sign in" } }, page: { meta_title: "Landing" } });
|
|
40
|
+
expect(got).toBe("Sign in");
|
|
41
|
+
});
|
|
42
|
+
it("with no loaderData page, the resolved page's meta_title answers", async () => {
|
|
43
|
+
const got = resolvePageMetaTitle({ loaderData: {}, page: { meta_title: "Landing" } });
|
|
44
|
+
expect(got).toBe("Landing");
|
|
45
|
+
});
|
|
46
|
+
it("neither page carries a title, so no title resolves", async () => {
|
|
47
|
+
const got = resolvePageMetaTitle({ loaderData: {} });
|
|
48
|
+
expect(got).toBeUndefined();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
* edit here rather than a fourth branch in three files.
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
|
-
import { interpolateMeta } from "
|
|
36
|
+
import { interpolateMeta } from "@teamscala/resolver-engine/loader-config/resolve-from-loader-config";
|
|
37
37
|
|
|
38
38
|
export function buildMetaFromContext(
|
|
39
39
|
metaContext: Record<string, Record<string, string>> | undefined,
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* @edit edit directly
|
|
5
5
|
*
|
|
6
6
|
* Shared Puck-page-data helpers for the resolved-graph → pageData conversion:
|
|
7
|
-
* the
|
|
7
|
+
* the PuckData alias of the canonical boundary Data, JSON extraction with an
|
|
8
|
+
* honest null on
|
|
8
9
|
* malformed input, layout-default application, region root-prop picking, the
|
|
9
10
|
* RegionEntry shape, and the unresolved-region-slot warn. Plus the brand-token
|
|
10
11
|
* prologue shared by all three render branches.
|
|
@@ -14,19 +15,14 @@
|
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
17
|
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
18
|
+
import type { Data } from "@teamscala/puck-boundary/data";
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* installed package to resolve against at publish typecheck time. The shape used
|
|
23
|
-
* here (root/content/zones) is all this module reads or constructs.
|
|
21
|
+
* The canonical identity from the generated boundary (the platform's one
|
|
22
|
+
* declaration site for framework shapes) — never a hand-spelled structural
|
|
23
|
+
* duplicate of it.
|
|
24
24
|
*/
|
|
25
|
-
export type PuckData =
|
|
26
|
-
root: { props?: Record<string, unknown> };
|
|
27
|
-
content: unknown[];
|
|
28
|
-
zones?: Record<string, unknown>;
|
|
29
|
-
};
|
|
25
|
+
export type PuckData = Data;
|
|
30
26
|
|
|
31
27
|
export const EMPTY_PUCK_DATA: PuckData = { root: { props: {} }, content: [] };
|
|
32
28
|
|
|
@@ -20,7 +20,7 @@ mock.module("@teamscala/logger/app-loggers", () => ({
|
|
|
20
20
|
}),
|
|
21
21
|
}));
|
|
22
22
|
|
|
23
|
-
mock.module("../
|
|
23
|
+
mock.module("../page-server-loader-graph.ts", () => ({
|
|
24
24
|
resolveFromLoaderConfig: async (_cfg: unknown, params: Record<string, string>) => {
|
|
25
25
|
resolveCalls.push({ params });
|
|
26
26
|
return structuredClone(loaderGraph);
|
|
@@ -17,7 +17,7 @@ import type { PageRouteConfig } from "../register-registry-page-route.ts";
|
|
|
17
17
|
import * as v from "valibot";
|
|
18
18
|
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
19
19
|
import type { ResourceLoaderConfigSchema } from "@teamscala/db-validation/registry-schemas/resource-loader";
|
|
20
|
-
import { resolveFromLoaderConfig } from "../
|
|
20
|
+
import { resolveFromLoaderConfig } from "../page-server-loader-graph.ts";
|
|
21
21
|
import { decideContentPageFloor } from "../content-page-floor.ts";
|
|
22
22
|
import { PrimaryResourceMissingError } from "../resource-not-found.ts";
|
|
23
23
|
import {
|
|
@@ -87,7 +87,6 @@ export async function resolveRequestGraph(
|
|
|
87
87
|
args: ResolveRequestGraphArgs,
|
|
88
88
|
): Promise<Record<string, unknown>> {
|
|
89
89
|
const { pattern, routeSlug, routeConfig, loaderConfig, params, isStaticShell, serviceSlug, configFingerprint } = args;
|
|
90
|
-
const log = getAppLogger();
|
|
91
90
|
|
|
92
91
|
// A bare root ("/") that declares a resource_type is a UUID-driven
|
|
93
92
|
// public site (proposals/forms/quiz) with no resource selected — it
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* proposal-*) — split when the parent held them all at 924 lines.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
+
import type { InferOutput } from "valibot";
|
|
23
24
|
import type { RenderModeLiteral } from "./page-route-shell-types.ts";
|
|
24
25
|
import { PageRouteConfigSchema } from "@teamscala/db-validation/registry-schemas/page-route";
|
|
25
26
|
import { ResourceLoaderConfigSchema } from "@teamscala/db-validation/registry-schemas/resource-loader";
|
|
@@ -65,21 +66,16 @@ import {
|
|
|
65
66
|
} from "./register-registry-page-route/live-loader-config.ts";
|
|
66
67
|
import type { ResourceLoaderKey } from "@teamscala/db/registry/generated/resource-loader";
|
|
67
68
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
launch_label: string;
|
|
79
|
-
placeholder?: string;
|
|
80
|
-
};
|
|
81
|
-
providers?: string[];
|
|
82
|
-
}
|
|
69
|
+
/**
|
|
70
|
+
* The page_route row's config type is the SCHEMA'S output, never a hand
|
|
71
|
+
* mirror (`autosave-single-surface` rule 2: a hand-written copy of a
|
|
72
|
+
* db-validation schema drifts the first time the schema grows a field —
|
|
73
|
+
* validateRow strips undeclared keys, so the mirror cannot see what the
|
|
74
|
+
* row now carries). The runtime therefore reads what the row declares,
|
|
75
|
+
* including the optional fields the old mirror dropped (label,
|
|
76
|
+
* brand_org_path, regions, shell_type, render_mode).
|
|
77
|
+
*/
|
|
78
|
+
export type PageRouteConfig = InferOutput<typeof PageRouteConfigSchema>;
|
|
83
79
|
|
|
84
80
|
interface RegistryRouteArgs {
|
|
85
81
|
app: Hono;
|
|
@@ -305,7 +301,9 @@ export async function registerRegistryPageRoute(
|
|
|
305
301
|
await applyBrandOrgPath(
|
|
306
302
|
pageData as Record<string, unknown>,
|
|
307
303
|
resolved,
|
|
308
|
-
|
|
304
|
+
// brand_org_path is the row schema's own field now — the derived
|
|
305
|
+
// type sees it, so the narrowing cast the mirror needed is gone.
|
|
306
|
+
routeConfig.brand_org_path,
|
|
309
307
|
params,
|
|
310
308
|
serviceSlug,
|
|
311
309
|
);
|