@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,28 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Loads the service's GENERATED `src/page-manifest.ts` — the codegen output of
|
|
7
|
-
* the `page-manifest` transform, emitted from `block_schema` rows. That file is
|
|
8
|
-
* the single artefact describing which Puck components this service renders.
|
|
9
|
-
*
|
|
10
|
-
* This REPLACES `discover-blocks.ts`, a hand-written runtime scan that queried
|
|
11
|
-
* block_schema at boot and dynamic-imported each row. Deleting it is the point.
|
|
12
|
-
* The scan was introduced to survive one dead row, and it bought that by moving
|
|
13
|
-
* a build-time, deterministic, generated fact into a runtime path with four
|
|
14
|
-
* independent ways to silently resolve nothing: a registry read that answers
|
|
15
|
-
* with an empty array instead of failing, an install probe that reports "not
|
|
16
|
-
* installed" for installed packages, a `node_modules` lookup relative to
|
|
17
|
-
* `process.cwd()`, and a per-block try/catch that swallows each failure into a
|
|
18
|
-
* string nobody reads when the boot dies first. Measured 2026-09-03: 91 active
|
|
19
|
-
* block_schema rows, a generated manifest carrying 63 blocks sitting on disk,
|
|
20
|
-
* and the scan loading ZERO — for hours, across every UI service.
|
|
21
|
-
*
|
|
22
|
-
* Generation is the platform's answer to exactly this (`one-generator-pattern`,
|
|
23
|
-
* `business-logic-is-rows-plus-executors`): a bad row fails codegen loudly, at
|
|
24
|
-
* build time, with the file in front of you. It cannot present as a service
|
|
25
|
-
* that boots and renders nothing.
|
|
26
4
|
*/
|
|
27
5
|
|
|
28
6
|
import { join } from "node:path";
|
|
@@ -32,12 +10,6 @@ interface PageManifestModule {
|
|
|
32
10
|
components?: Record<string, RendererComponentEntry>;
|
|
33
11
|
}
|
|
34
12
|
|
|
35
|
-
/**
|
|
36
|
-
* Reads the generated manifest's `components` export. Throws when the file is
|
|
37
|
-
* absent or exports nothing — an empty manifest means codegen has not run for
|
|
38
|
-
* this service, which is a build problem the operator must see named, not a
|
|
39
|
-
* degraded runtime state to paper over.
|
|
40
|
-
*/
|
|
41
13
|
export async function loadPageManifest(
|
|
42
14
|
serviceSlug: string,
|
|
43
15
|
): Promise<Record<string, RendererComponentEntry>> {
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Loads the service_install registry row for a given service slug.
|
|
7
|
-
* Uses registry.require() per Constitution Rule 11.
|
|
8
|
-
* Schema is owned by @teamscala/db-validation (Rule 7).
|
|
9
4
|
*/
|
|
10
5
|
|
|
11
6
|
import { getCache } from "@teamscala/db/registry/loader/cache/get-cache";
|
|
@@ -14,7 +9,6 @@ import {
|
|
|
14
9
|
RegistryValidationError,
|
|
15
10
|
} from "@teamscala/db/registry/require";
|
|
16
11
|
import { normalizeRegistryConfigObject } from "@teamscala/db/registry/validation";
|
|
17
|
-
import type { ServiceInstallConfig } from "@teamscala/db-validation/registry-schemas/service-install";
|
|
18
12
|
import {
|
|
19
13
|
enforceExplicitRenderMode,
|
|
20
14
|
ServiceInstallConfigSchema,
|
|
@@ -22,8 +16,32 @@ import {
|
|
|
22
16
|
} from "@teamscala/db-validation/registry-schemas/service-install";
|
|
23
17
|
import * as v from "valibot";
|
|
24
18
|
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
const BOOT_ROW_KEYS = [
|
|
20
|
+
"access",
|
|
21
|
+
"api_routes",
|
|
22
|
+
"app_port",
|
|
23
|
+
"auth",
|
|
24
|
+
"block_packages",
|
|
25
|
+
"boot_hooks",
|
|
26
|
+
"brand_org",
|
|
27
|
+
"home_path",
|
|
28
|
+
"kind",
|
|
29
|
+
"orpc_router",
|
|
30
|
+
"portal_scoped",
|
|
31
|
+
"port",
|
|
32
|
+
"providers",
|
|
33
|
+
"render_mode",
|
|
34
|
+
"server_packages",
|
|
35
|
+
"websocket_routes",
|
|
36
|
+
] as const;
|
|
37
|
+
|
|
38
|
+
export const ServiceInstallBootSchema = v.object(
|
|
39
|
+
v.pick(ServiceInstallConfigSchema.pipe[0], BOOT_ROW_KEYS).entries,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
export type ServiceInstallConfig = v.InferOutput<
|
|
43
|
+
typeof ServiceInstallBootSchema
|
|
44
|
+
>;
|
|
27
45
|
|
|
28
46
|
export interface LoadServiceInstallRowResult {
|
|
29
47
|
success: true;
|
|
@@ -54,9 +72,23 @@ export async function loadServiceInstallRow(
|
|
|
54
72
|
`service_install:${serviceSlug}`,
|
|
55
73
|
) as Record<string, unknown>;
|
|
56
74
|
enforceExplicitRenderMode(serviceSlug, rawConfig);
|
|
57
|
-
const config = v.parse(
|
|
75
|
+
const config = v.parse(ServiceInstallBootSchema, rawConfig);
|
|
58
76
|
return { success: true, config };
|
|
59
77
|
} catch (err) {
|
|
78
|
+
if (err instanceof v.ValiError) {
|
|
79
|
+
const issues = err.issues
|
|
80
|
+
.map((issue) => {
|
|
81
|
+
const path = v.getDotPath(issue);
|
|
82
|
+
return path ? `${path}: ${issue.message}` : issue.message;
|
|
83
|
+
})
|
|
84
|
+
.join("; ");
|
|
85
|
+
return {
|
|
86
|
+
success: false,
|
|
87
|
+
error: "validation_failed",
|
|
88
|
+
slug: serviceSlug,
|
|
89
|
+
message: `service_install row for "${serviceSlug}" has a key the boot reads missing or malformed: ${issues}`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
60
92
|
if (err instanceof RegistryMissingError) {
|
|
61
93
|
const sql = `INSERT INTO registry_entries (type, slug, config, is_active) VALUES ('service_install', '${serviceSlug}', '{"kind":"ui","render_mode":"public","port":null,"page_routes":[],"providers":[],"api_routes":[],"block_packages":[],"teamgbg_packages":[],"env_vars":[]}'::jsonb, true);`;
|
|
62
94
|
return {
|
package/src/module-not-found.ts
CHANGED
|
@@ -1,30 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Classifies a failed dynamic import as "this candidate is absent" vs a real
|
|
7
|
-
* error, for the server_package mount candidate chain. Extracted from
|
|
8
|
-
* mount-server-packages so the classification is testable: as a closure it
|
|
9
|
-
* could not be exercised, and it shipped wrong.
|
|
10
4
|
*/
|
|
11
5
|
|
|
12
|
-
/** `@scope/name/sub/path.ts` -> `@scope/name`; `name/sub` -> `name`. */
|
|
13
6
|
export function barePackageOf(specifier: string): string {
|
|
14
7
|
const parts = specifier.split("/");
|
|
15
8
|
return specifier.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
|
|
16
9
|
}
|
|
17
10
|
|
|
18
|
-
/**
|
|
19
|
-
* True when the import failed because `requestedPath` itself is absent.
|
|
20
|
-
*
|
|
21
|
-
* Bun reports an unexported SUBPATH as `Cannot find package '<bare>'`, naming
|
|
22
|
-
* the package rather than the subpath, so the bare name of THIS candidate
|
|
23
|
-
* counts as not-found. Without that the first candidate's miss is
|
|
24
|
-
* misclassified as fatal and the fallbacks never run (measured 2026-08-24:
|
|
25
|
-
* scala-agents-ui crash-looped because cli-session exports ./boot/mount.ts and
|
|
26
|
-
* not ./mount.ts). A transitive miss naming a DIFFERENT module still surfaces.
|
|
27
|
-
*/
|
|
28
11
|
export function isModuleNotFound(err: unknown, requestedPath: string): boolean {
|
|
29
12
|
const msg =
|
|
30
13
|
(err as { message?: string })?.message ??
|
package/src/mount-orpc-router.ts
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Mounts the composed ORPC app router (generated Prisma CRUD + custom fn routers)
|
|
7
|
-
* at /api/rpc/* over HTTP via @orpc/server/fetch's RPCHandler.
|
|
8
|
-
* Uses getAppRouter() from @teamscala/orpc/app-router which merges generated
|
|
9
|
-
* routers plus the row-composed fn namespace (orpc-bundle from api_route rows).
|
|
10
4
|
*/
|
|
11
5
|
|
|
12
6
|
import { join } from "node:path";
|
|
@@ -32,22 +26,12 @@ export async function mountOrpcRouter(
|
|
|
32
26
|
serviceSlug: string,
|
|
33
27
|
): Promise<OrpcMountResult | null> {
|
|
34
28
|
const log = getAppLogger();
|
|
35
|
-
// registerFnRouters() is retired: the row-composed bundle (orpc-bundle from
|
|
36
|
-
// api_route rows) is the sole fn source. The handwritten registration is
|
|
37
|
-
// fully shadowed by generated.fn (all families have rows) — keeping it
|
|
38
|
-
// would block generated-only procedures (auth, cacheAdmin, regions,
|
|
39
|
-
// realtime) from serving.
|
|
40
29
|
const routerPath = join(
|
|
41
30
|
process.cwd(),
|
|
42
31
|
"prisma/generated/orpc/routers/index.ts",
|
|
43
32
|
);
|
|
44
33
|
const schemaPath = join(process.cwd(), "prisma/schema.prisma");
|
|
45
34
|
|
|
46
|
-
// ORPC forward mode: when SCALA_ORPC_FORWARD_URL is set and there is no
|
|
47
|
-
// local bundle, /api/rpc/* proxies to the central ORPC surface instead of
|
|
48
|
-
// mounting a per-service router. This is the collapse's consolidation path —
|
|
49
|
-
// services shed their local bundle and the central surface serves all
|
|
50
|
-
// row-composed procedures.
|
|
51
35
|
const forwardUrl = process.env.SCALA_ORPC_FORWARD_URL;
|
|
52
36
|
if (!await Bun.file(routerPath).exists() && forwardUrl) {
|
|
53
37
|
const base = forwardUrl.replace(/\/+$/, "");
|
|
@@ -58,7 +42,7 @@ export async function mountOrpcRouter(
|
|
|
58
42
|
method: c.req.method,
|
|
59
43
|
headers: c.req.raw.headers,
|
|
60
44
|
body: c.req.raw.body,
|
|
61
|
-
// @ts-ignore
|
|
45
|
+
// @ts-ignore duplex is required for streaming bodies in Bun
|
|
62
46
|
duplex: "half",
|
|
63
47
|
});
|
|
64
48
|
return new Response(res.body, {
|
|
@@ -73,14 +57,6 @@ export async function mountOrpcRouter(
|
|
|
73
57
|
return { routeCount: 0 };
|
|
74
58
|
}
|
|
75
59
|
|
|
76
|
-
// No generated ORPC router is the EXPECTED state for daemon-only / boot-hook
|
|
77
|
-
// services (session-picker, dep-sync, etc.) that own no DB schema or
|
|
78
|
-
// api_routes — they serve no /api/rpc surface. Skip silently: importing a
|
|
79
|
-
// nonexistent module would throw into the catch below and surface a
|
|
80
|
-
// misleading "ORPC router not mounted" WARN on every boot. The catch is
|
|
81
|
-
// reserved for GENUINE mount failures (router present but fails to load).
|
|
82
|
-
// A service that SHOULD have a router but doesn't is a codegen gap caught by
|
|
83
|
-
// API verification, not a boot warning.
|
|
84
60
|
if (!await Bun.file(routerPath).exists()) {
|
|
85
61
|
if (await Bun.file(schemaPath).exists()) {
|
|
86
62
|
throw new Error(
|
|
@@ -97,12 +73,6 @@ export async function mountOrpcRouter(
|
|
|
97
73
|
const router = await getAppRouter();
|
|
98
74
|
if (!router || Object.keys(router).length === 0) return null;
|
|
99
75
|
|
|
100
|
-
// The mount error hook: every procedure error crossing this boundary is
|
|
101
|
-
// shaped by buildErrorReport (errorId, class, stack head) and recorded
|
|
102
|
-
// durably via event-log before ORPC swallows it into a stackless 500.
|
|
103
|
-
// The recorder is wired HERE because orpc is primitives tier and cannot
|
|
104
|
-
// import @teamscala/event-log across a same-tier edge — the boot layer
|
|
105
|
-
// owns that dependency.
|
|
106
76
|
const { procedureErrorReportInterceptor } = await import(
|
|
107
77
|
"@teamscala/orpc/procedure-error-report"
|
|
108
78
|
);
|
|
@@ -120,24 +90,12 @@ export async function mountOrpcRouter(
|
|
|
120
90
|
interceptors: [procedureErrorReportInterceptor()] as never,
|
|
121
91
|
});
|
|
122
92
|
|
|
123
|
-
// The bootloader injects the service Prisma client into ORPC's configured
|
|
124
|
-
// primitive. Reading it through that surface keeps this mount on the same
|
|
125
|
-
// dependency boundary as the generated procedures.
|
|
126
93
|
const { getPrisma: getConfiguredPrisma } = await import(
|
|
127
94
|
"@teamscala/orpc/configure"
|
|
128
95
|
);
|
|
129
96
|
|
|
130
|
-
// Coexistence (Prisma 8 migration): create the v8 chained client when
|
|
131
|
-
// the shared platform contract ships inside @teamscala/db. Absence is
|
|
132
|
-
// the EXPECTED state for services that have not migrated — db8 stays
|
|
133
|
-
// undefined and v7 procedures are untouched. One boot-time creation,
|
|
134
|
-
// closed over by the request handler (connects lazily on first query).
|
|
135
97
|
let db8: Db8Client | undefined;
|
|
136
98
|
try {
|
|
137
|
-
// Named-need address (wrong-database guard, 2026-09-27): the tenant
|
|
138
|
-
// plane by name through @teamscala/db's resolver, never an env key
|
|
139
|
-
// read at this call site. Null = the deploy wrote nothing for this
|
|
140
|
-
// plane, and db8 stays undefined on the existing absence path.
|
|
141
99
|
const databaseUrl = resolveDbUrlOrNull("tenant");
|
|
142
100
|
if (databaseUrl) {
|
|
143
101
|
const { createDbClientV8 } = await import(
|
|
@@ -147,9 +105,6 @@ export async function mountOrpcRouter(
|
|
|
147
105
|
connectionString: databaseUrl,
|
|
148
106
|
serviceName: serviceSlug,
|
|
149
107
|
})) as never;
|
|
150
|
-
// Register for db-core internals (registry loaders, raw fallbacks)
|
|
151
|
-
// routing v8-first via getDb8() — the same coexistence shape as
|
|
152
|
-
// the RPC context injection above.
|
|
153
108
|
const { registerDb8 } = await import("@teamscala/db/db8-registry");
|
|
154
109
|
registerDb8(db8 as never);
|
|
155
110
|
}
|
|
@@ -160,26 +115,9 @@ export async function mountOrpcRouter(
|
|
|
160
115
|
db8 = undefined;
|
|
161
116
|
}
|
|
162
117
|
|
|
163
|
-
// Bridge the composed router into tool-executor so in-process tool
|
|
164
|
-
// dispatch (chat agents' work_items/meetings/agreements/proposals
|
|
165
|
-
// tools, etc.) resolves against this service's ORPC router + Prisma
|
|
166
|
-
// client. Without this every ORPC tool call throws
|
|
167
|
-
// "app router provider not configured". Parallel to dev-mcp-boot's
|
|
168
|
-
// configureToolExecutor. Calls getAppRouter() fresh each time (not a
|
|
169
|
-
// boot-time capture) so the tool-executor always resolves the CURRENT
|
|
170
|
-
// memoized router — the tool-generator listener may regenerate the
|
|
171
|
-
// ORPC router and call clearAppRouterCache() after boot, producing a
|
|
172
|
-
// new router object that a captured closure would miss.
|
|
173
118
|
const { configure: configureToolExecutor } = await import(
|
|
174
119
|
"@teamscala/tool-executor/configure"
|
|
175
120
|
);
|
|
176
|
-
// The caller-identity and secret collaborators arrive here (the boot
|
|
177
|
-
// layer) — tool-executor cannot import its tier-2 siblings statically
|
|
178
|
-
// (vertical-dependency-only: a same-tier edge). Resolved eagerly at
|
|
179
|
-
// mount so the injected factories stay synchronous. The retired
|
|
180
|
-
// @teamscala/host-command-bus invoker injection no longer happens: the
|
|
181
|
-
// bus is retired, and host_command tools execute through the native
|
|
182
|
-
// in-process coding path inside tool-executor.
|
|
183
121
|
const [{ getCallerContext }, { decryptSecretConfig }] =
|
|
184
122
|
await Promise.all([
|
|
185
123
|
import("@teamscala/mcp-multi-session/caller-context"),
|
|
@@ -192,19 +130,12 @@ export async function mountOrpcRouter(
|
|
|
192
130
|
decryptSecretConfig: (raw) => decryptSecretConfig(raw as never),
|
|
193
131
|
});
|
|
194
132
|
|
|
195
|
-
// The fn capability primitives (foundation tier) cannot import the email
|
|
196
|
-
// client, so the boot layer injects the real sender — the same shape the
|
|
197
|
-
// bootloader already uses for getPrisma. Until configured, fn.workItems
|
|
198
|
-
// notification procedures degrade to { notified: 0 } with an error log
|
|
199
|
-
// (never throw).
|
|
200
133
|
try {
|
|
201
134
|
const [{ configureWorkItems }, { createEmailClient }] = await Promise.all([
|
|
202
135
|
import("@teamscala/db/work-items"),
|
|
203
136
|
import("@teamscala/email-client/client"),
|
|
204
137
|
]);
|
|
205
138
|
configureWorkItems({
|
|
206
|
-
// The injected contract is the template-send subset; the real
|
|
207
|
-
// client's send agrees at every call site ({to, template, vars}).
|
|
208
139
|
emailSender: createEmailClient() as never,
|
|
209
140
|
logger: log,
|
|
210
141
|
});
|
|
@@ -232,17 +163,7 @@ export async function mountOrpcRouter(
|
|
|
232
163
|
id: session.user.id,
|
|
233
164
|
name: session.user.name ?? undefined,
|
|
234
165
|
email: session.user.email,
|
|
235
|
-
// The active organisation lives on the SESSION
|
|
236
|
-
// (activeOrganizationId, set at sign-in by scala-otp's
|
|
237
|
-
// session.create hook), NOT on the user object — getSession's
|
|
238
|
-
// ResolvedSession.user carries no organisationId. Reading the
|
|
239
|
-
// user field left organisationId always null, so the generated
|
|
240
|
-
// router's org-scope gate 403'd every non-super-admin write
|
|
241
|
-
// ("Organization membership required") on the public-HTTP path.
|
|
242
166
|
organisationId: session.session.activeOrganizationId ?? null,
|
|
243
|
-
// better-auth's admin plugin issues role "admin"; the platform
|
|
244
|
-
// also uses "super-admin"/"super_admin". All three are platform
|
|
245
|
-
// super-admins for ORPC org-scope bypass.
|
|
246
167
|
isSuperAdmin:
|
|
247
168
|
role === "super_admin" ||
|
|
248
169
|
role === "super-admin" ||
|
|
@@ -266,10 +187,6 @@ export async function mountOrpcRouter(
|
|
|
266
187
|
context: { prisma, db8, headers: c.req.raw.headers, resolveUser },
|
|
267
188
|
});
|
|
268
189
|
if (!result?.matched) return c.notFound();
|
|
269
|
-
// Response-side signal only (a fleet-wide 500 spike with no
|
|
270
|
-
// traceback — measured 2026-09-04). The real error, its class and
|
|
271
|
-
// its stack are captured by the mount error hook above; this line
|
|
272
|
-
// stays for failures that reach the boundary without a throw.
|
|
273
190
|
if (result.response.status >= 500) {
|
|
274
191
|
log.error(
|
|
275
192
|
`[ORPC] ${c.req.method} ${c.req.path} → ${result.response.status}`,
|
|
@@ -279,16 +196,6 @@ export async function mountOrpcRouter(
|
|
|
279
196
|
},
|
|
280
197
|
);
|
|
281
198
|
|
|
282
|
-
// THE OpenAPI CONTRACT, served BESIDE the RPC — a GET on the same prefix,
|
|
283
|
-
// generated from the router object mounted below, so the document and the
|
|
284
|
-
// handler cannot describe different surfaces. Order is free: the methods
|
|
285
|
-
// never overlap (`app.post` matches POST alone).
|
|
286
|
-
//
|
|
287
|
-
// Gated by openApiContractAuthorised, which refuses a proxied request as
|
|
288
|
-
// well as a wrong key (guard auth-helpers-fail-closed), so only an
|
|
289
|
-
// internal caller — the MCP gateway — reaches this. Its own facts come
|
|
290
|
-
// from openApiContractOptions; both units carry the reasoning, and
|
|
291
|
-
// contract rows state their behaviour.
|
|
292
199
|
app.get("/api/rpc/openapi.json", async (c) => {
|
|
293
200
|
const header = (name: string) => c.req.header(name) ?? null;
|
|
294
201
|
if (
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @system
|
|
2
|
+
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Walks `service_install.server_packages[]`, dynamic-imports each, and calls
|
|
7
|
-
* its `mount(ctx)` function. Server packages register handlers / boot hooks /
|
|
8
|
-
* scheduled jobs / WS routes via the MountContext exposed in ctx; the
|
|
9
|
-
* resulting MountRegistry is consumed by register-api-routes /
|
|
10
|
-
* register-ws-routes / run-boot-hook-list / start-scheduled-jobs.
|
|
11
4
|
*/
|
|
12
5
|
|
|
13
6
|
/**
|
|
@@ -96,9 +89,6 @@ export async function mountServerPackages(
|
|
|
96
89
|
lastNotFound = err;
|
|
97
90
|
continue;
|
|
98
91
|
}
|
|
99
|
-
// Real error (syntax, runtime, transitive resolution failure inside the
|
|
100
|
-
// loaded module) — surface it directly. Swallowing here is what hides
|
|
101
|
-
// the true cause behind a misleading "cannot find <pkg>" further down.
|
|
102
92
|
throw new BootFatalError(
|
|
103
93
|
"mount-server-packages",
|
|
104
94
|
serviceSlug,
|
|
@@ -129,30 +119,6 @@ export async function mountServerPackages(
|
|
|
129
119
|
}
|
|
130
120
|
} catch (err) {
|
|
131
121
|
if (err instanceof BootFatalError) throw err;
|
|
132
|
-
// A PLUGIN THAT FAILS TO MOUNT IS UNAVAILABLE, NOT FATAL. `crash-isolation`
|
|
133
|
-
// states the law directly — "a failure in one package does not crash the
|
|
134
|
-
// host" — and names the plugin failure domain: "a broken plugin degrades
|
|
135
|
-
// to 'that plugin unavailable'". This arm used to wrap ANY mount error in
|
|
136
|
-
// a BootFatalError, so one plugin's missing config killed a whole service.
|
|
137
|
-
//
|
|
138
|
-
// MEASURED 2026-09-07: scala-agents-ui, the operator's fleet monitoring UI,
|
|
139
|
-
// booted through db, auth, routes and every other server_package, then died
|
|
140
|
-
// at @teamscala/opencode because OPENCODE_SERVER_URL was absent. systemd
|
|
141
|
-
// retried five times and gave up, agents.scala.business served 502, and the
|
|
142
|
-
// operator lost sight of his fleet — over one optional integration missing
|
|
143
|
-
// one variable, on a page that needs no opencode connection to render.
|
|
144
|
-
//
|
|
145
|
-
// THE ASYMMETRY DECIDES IT. A service that boots without one plugin still
|
|
146
|
-
// serves every route that plugin did not own, and the gap is loud in this
|
|
147
|
-
// log line and counted in the summary below. A service that refuses to boot
|
|
148
|
-
// serves NOTHING — including the surface that would have explained why.
|
|
149
|
-
//
|
|
150
|
-
// NOT-FOUND STAYS FATAL, deliberately: that arm is above and untouched. A
|
|
151
|
-
// package that LOADS and then throws is a broken plugin, which is this
|
|
152
|
-
// case. A package that cannot be resolved at all means the declared
|
|
153
|
-
// dependency set was never installed — an install-integrity failure, where
|
|
154
|
-
// booting a half-installed service would serve wrong results while looking
|
|
155
|
-
// healthy.
|
|
156
122
|
degraded.push(pkg);
|
|
157
123
|
log.error(
|
|
158
124
|
`[scala-install:${serviceSlug}] server_package "${pkg}" UNAVAILABLE — mount failed; ` +
|
|
@@ -165,8 +131,6 @@ export async function mountServerPackages(
|
|
|
165
131
|
`[scala-install:${serviceSlug}] Registered ${mountRegistry.handlers.size} handler(s), ` +
|
|
166
132
|
`${mountRegistry.wsRoutes.size} ws route(s), ` +
|
|
167
133
|
`${mountRegistry.bootHooks.size} boot hook(s)` +
|
|
168
|
-
// NAMED, NEVER COUNTED. A bare "1 degraded" sends the reader hunting for
|
|
169
|
-
// which one; the name is the whole value of the line.
|
|
170
134
|
(degraded.length > 0
|
|
171
135
|
? `, ${degraded.length} server_package(s) UNAVAILABLE: ${degraded.join(", ")}`
|
|
172
136
|
: ""),
|
package/src/mount.ts
CHANGED
package/src/mutable-app-ref.ts
CHANGED
|
@@ -1,34 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @system service-boot
|
|
3
3
|
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Mutable app reference for two-phase boot. Allows swapping the Hono app
|
|
7
|
-
* atomically after building the full app in the background. The initial app
|
|
8
|
-
* is a minimal tool-only surface (ORPC, data routes) that starts serving
|
|
9
|
-
* immediately; the full app (pages, hydration) is built off-to-the-side and
|
|
10
|
-
* swapped in once complete.
|
|
11
|
-
*
|
|
12
|
-
* Thread-safety: Bun is single-threaded, so a simple module-level variable
|
|
13
|
-
* is sufficient. Atomics are not needed.
|
|
14
4
|
*/
|
|
15
5
|
|
|
16
6
|
import type { Hono } from "hono";
|
|
17
7
|
let currentApp: Hono | null = null;
|
|
18
8
|
let appLabel = "initial";
|
|
19
9
|
|
|
20
|
-
/**
|
|
21
|
-
* Create a new mutable app reference initialized with the provided app.
|
|
22
|
-
* This should be called once at boot time.
|
|
23
|
-
*/
|
|
24
10
|
export function initMutableAppRef(initialApp: Hono, label: string): void {
|
|
25
11
|
currentApp = initialApp;
|
|
26
12
|
appLabel = label;
|
|
27
13
|
}
|
|
28
14
|
|
|
29
|
-
/**
|
|
30
|
-
* Get the current app. Throws if the ref hasn't been initialized.
|
|
31
|
-
*/
|
|
32
15
|
export function getApp(): Hono {
|
|
33
16
|
if (!currentApp) {
|
|
34
17
|
throw new Error("Mutable app reference not initialized");
|
|
@@ -36,10 +19,6 @@ export function getApp(): Hono {
|
|
|
36
19
|
return currentApp;
|
|
37
20
|
}
|
|
38
21
|
|
|
39
|
-
/**
|
|
40
|
-
* Atomically swap the app to a new instance. This is called after the
|
|
41
|
-
* full app is built in the background. Returns the previous label for logging.
|
|
42
|
-
*/
|
|
43
22
|
export function swapApp(newApp: Hono, newLabel: string): string {
|
|
44
23
|
const oldLabel = appLabel;
|
|
45
24
|
currentApp = newApp;
|