@teamscala/service-boot 0.23.524 → 0.23.526
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 -11
- package/src/agreement-gate/authorized-emails.ts +0 -7
- package/src/agreement-gate/placeholder-strip.ts +0 -2
- package/src/agreement-gate/settings.ts +0 -15
- package/src/agreement-gate.ts +0 -34
- package/src/attach-proposal-accept.ts +1 -1
- package/src/attach-proposal-magic.ts +1 -1
- package/src/auth/sign-out.ts +0 -2
- package/src/auth-gate/public-paths.ts +0 -86
- package/src/auth-gate.ts +0 -19
- package/src/bind-page-handler.ts +0 -76
- package/src/build-client-assets/hydrate-bundle.ts +0 -126
- package/src/build-client-assets/manifest.ts +0 -18
- package/src/build-client-assets/source-candidates.ts +0 -34
- package/src/build-client-assets/sourcemaps-upload.ts +0 -42
- package/src/build-client-assets/styles-build.ts +0 -50
- package/src/capability-slug.ts +0 -10
- package/src/compose-page-title.ts +0 -9
- package/src/content-gate/access.ts +0 -26
- package/src/content-gate/catalog.ts +0 -29
- package/src/content-gate/config-types.ts +3 -84
- package/src/content-gate/gate.ts +0 -20
- package/src/content-page-floor.ts +0 -11
- package/src/error-page-logo.ts +2 -12
- package/src/expand-roster.ts +0 -9
- package/src/gpt-gate.ts +1 -1
- package/src/hono-error-handler.ts +0 -4
- package/src/install-logging.ts +0 -32
- package/src/load-page-manifest.ts +0 -6
- package/src/load-service-install-row.ts +41 -4
- package/src/module-not-found.ts +0 -11
- package/src/mount-orpc-router.ts +1 -88
- package/src/mount-server-packages.ts +0 -29
- package/src/mutable-app-ref.ts +0 -11
- package/src/orpc-contract-rows.json +4102 -61
- package/src/orpc-openapi-document.ts +0 -6
- package/src/orpc-openapi-gate.ts +0 -10
- package/src/pg-boss-handler-registry.ts +0 -12
- package/src/pg-boss-mount.ts +0 -101
- package/src/proposal-gate.ts +1 -41
- package/src/quiz-gate.ts +1 -9
- package/src/register-page-server-client.ts +0 -53
- package/src/register-registry-page-route/ai-chat-prefetch.ts +0 -18
- package/src/register-registry-page-route/apply-page-branding.ts +0 -90
- package/src/register-registry-page-route/captured-loader-config.ts +0 -1
- package/src/register-registry-page-route/live-loader-config.ts +0 -16
- package/src/register-registry-page-route/meta-title.ts +0 -24
- package/src/register-registry-page-route/page-data-shared.ts +0 -6
- package/src/register-registry-page-route/prefetch-org-id.ts +0 -17
- package/src/register-registry-page-route/proposal-approval-email-token.ts +0 -11
- package/src/register-registry-page-route/proposal-brand-stamping.ts +0 -20
- package/src/register-registry-page-route/public-shell-page-data.ts +0 -26
- package/src/register-registry-page-route/resolve-request-graph.ts +2 -155
- package/src/register-registry-page-route/run-content-gates.ts +2 -54
- package/src/register-registry-page-route/warn-uncovered-blocks.ts +0 -10
- package/src/register-registry-page-route.ts +0 -85
- package/src/register-static-assets.ts +0 -45
- package/src/resolve-brand-favicon.ts +0 -9
- package/src/resolve-service-port.ts +0 -11
- package/src/resolve-theme-mode.ts +0 -8
- package/src/resolvers/app-header.ts +0 -1
- package/src/resolvers/fetch-org-brand.ts +0 -1
- package/src/resolvers/portal-route-paths.ts +0 -17
- package/src/resource-not-found.ts +0 -24
- package/src/select-service-routes.ts +0 -14
- package/src/theme-fonts-link.ts +0 -5
- package/src/two-phase-full-app.ts +0 -76
- package/src/two-phase-minimal-app.ts +1 -105
- package/src/types.ts +0 -14
- package/src/ui-service.ts +1 -42
- 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 -197
- package/src/is-react-element-type.test.ts +0 -45
- package/src/is-react-element-type.ts +0 -10
- package/src/page-server-orpc/hub-mcp-client.ts +0 -14
- package/src/pg-boss-mount.test.ts +0 -101
- package/src/register-registry-page-route/graph-cache.test.ts +0 -81
- package/src/register-registry-page-route/graph-cache.ts +0 -71
- 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 -357
package/src/mount-orpc-router.ts
CHANGED
|
@@ -26,22 +26,12 @@ export async function mountOrpcRouter(
|
|
|
26
26
|
serviceSlug: string,
|
|
27
27
|
): Promise<OrpcMountResult | null> {
|
|
28
28
|
const log = getAppLogger();
|
|
29
|
-
// registerFnRouters() is retired: the row-composed bundle (orpc-bundle from
|
|
30
|
-
// api_route rows) is the sole fn source. The handwritten registration is
|
|
31
|
-
// fully shadowed by generated.fn (all families have rows) — keeping it
|
|
32
|
-
// would block generated-only procedures (auth, cacheAdmin, regions,
|
|
33
|
-
// realtime) from serving.
|
|
34
29
|
const routerPath = join(
|
|
35
30
|
process.cwd(),
|
|
36
31
|
"prisma/generated/orpc/routers/index.ts",
|
|
37
32
|
);
|
|
38
33
|
const schemaPath = join(process.cwd(), "prisma/schema.prisma");
|
|
39
34
|
|
|
40
|
-
// ORPC forward mode: when SCALA_ORPC_FORWARD_URL is set and there is no
|
|
41
|
-
// local bundle, /api/rpc/* proxies to the central ORPC surface instead of
|
|
42
|
-
// mounting a per-service router. This is the collapse's consolidation path —
|
|
43
|
-
// services shed their local bundle and the central surface serves all
|
|
44
|
-
// row-composed procedures.
|
|
45
35
|
const forwardUrl = process.env.SCALA_ORPC_FORWARD_URL;
|
|
46
36
|
if (!await Bun.file(routerPath).exists() && forwardUrl) {
|
|
47
37
|
const base = forwardUrl.replace(/\/+$/, "");
|
|
@@ -52,7 +42,7 @@ export async function mountOrpcRouter(
|
|
|
52
42
|
method: c.req.method,
|
|
53
43
|
headers: c.req.raw.headers,
|
|
54
44
|
body: c.req.raw.body,
|
|
55
|
-
// @ts-ignore
|
|
45
|
+
// @ts-ignore duplex is required for streaming bodies in Bun
|
|
56
46
|
duplex: "half",
|
|
57
47
|
});
|
|
58
48
|
return new Response(res.body, {
|
|
@@ -67,14 +57,6 @@ export async function mountOrpcRouter(
|
|
|
67
57
|
return { routeCount: 0 };
|
|
68
58
|
}
|
|
69
59
|
|
|
70
|
-
// No generated ORPC router is the EXPECTED state for daemon-only / boot-hook
|
|
71
|
-
// services (session-picker, dep-sync, etc.) that own no DB schema or
|
|
72
|
-
// api_routes — they serve no /api/rpc surface. Skip silently: importing a
|
|
73
|
-
// nonexistent module would throw into the catch below and surface a
|
|
74
|
-
// misleading "ORPC router not mounted" WARN on every boot. The catch is
|
|
75
|
-
// reserved for GENUINE mount failures (router present but fails to load).
|
|
76
|
-
// A service that SHOULD have a router but doesn't is a codegen gap caught by
|
|
77
|
-
// API verification, not a boot warning.
|
|
78
60
|
if (!await Bun.file(routerPath).exists()) {
|
|
79
61
|
if (await Bun.file(schemaPath).exists()) {
|
|
80
62
|
throw new Error(
|
|
@@ -91,12 +73,6 @@ export async function mountOrpcRouter(
|
|
|
91
73
|
const router = await getAppRouter();
|
|
92
74
|
if (!router || Object.keys(router).length === 0) return null;
|
|
93
75
|
|
|
94
|
-
// The mount error hook: every procedure error crossing this boundary is
|
|
95
|
-
// shaped by buildErrorReport (errorId, class, stack head) and recorded
|
|
96
|
-
// durably via event-log before ORPC swallows it into a stackless 500.
|
|
97
|
-
// The recorder is wired HERE because orpc is primitives tier and cannot
|
|
98
|
-
// import @teamscala/event-log across a same-tier edge — the boot layer
|
|
99
|
-
// owns that dependency.
|
|
100
76
|
const { procedureErrorReportInterceptor } = await import(
|
|
101
77
|
"@teamscala/orpc/procedure-error-report"
|
|
102
78
|
);
|
|
@@ -114,24 +90,12 @@ export async function mountOrpcRouter(
|
|
|
114
90
|
interceptors: [procedureErrorReportInterceptor()] as never,
|
|
115
91
|
});
|
|
116
92
|
|
|
117
|
-
// The bootloader injects the service Prisma client into ORPC's configured
|
|
118
|
-
// primitive. Reading it through that surface keeps this mount on the same
|
|
119
|
-
// dependency boundary as the generated procedures.
|
|
120
93
|
const { getPrisma: getConfiguredPrisma } = await import(
|
|
121
94
|
"@teamscala/orpc/configure"
|
|
122
95
|
);
|
|
123
96
|
|
|
124
|
-
// Coexistence (Prisma 8 migration): create the v8 chained client when
|
|
125
|
-
// the shared platform contract ships inside @teamscala/db. Absence is
|
|
126
|
-
// the EXPECTED state for services that have not migrated — db8 stays
|
|
127
|
-
// undefined and v7 procedures are untouched. One boot-time creation,
|
|
128
|
-
// closed over by the request handler (connects lazily on first query).
|
|
129
97
|
let db8: Db8Client | undefined;
|
|
130
98
|
try {
|
|
131
|
-
// Named-need address (wrong-database guard, 2026-09-27): the tenant
|
|
132
|
-
// plane by name through @teamscala/db's resolver, never an env key
|
|
133
|
-
// read at this call site. Null = the deploy wrote nothing for this
|
|
134
|
-
// plane, and db8 stays undefined on the existing absence path.
|
|
135
99
|
const databaseUrl = resolveDbUrlOrNull("tenant");
|
|
136
100
|
if (databaseUrl) {
|
|
137
101
|
const { createDbClientV8 } = await import(
|
|
@@ -141,9 +105,6 @@ export async function mountOrpcRouter(
|
|
|
141
105
|
connectionString: databaseUrl,
|
|
142
106
|
serviceName: serviceSlug,
|
|
143
107
|
})) as never;
|
|
144
|
-
// Register for db-core internals (registry loaders, raw fallbacks)
|
|
145
|
-
// routing v8-first via getDb8() — the same coexistence shape as
|
|
146
|
-
// the RPC context injection above.
|
|
147
108
|
const { registerDb8 } = await import("@teamscala/db/db8-registry");
|
|
148
109
|
registerDb8(db8 as never);
|
|
149
110
|
}
|
|
@@ -154,26 +115,9 @@ export async function mountOrpcRouter(
|
|
|
154
115
|
db8 = undefined;
|
|
155
116
|
}
|
|
156
117
|
|
|
157
|
-
// Bridge the composed router into tool-executor so in-process tool
|
|
158
|
-
// dispatch (chat agents' work_items/meetings/agreements/proposals
|
|
159
|
-
// tools, etc.) resolves against this service's ORPC router + Prisma
|
|
160
|
-
// client. Without this every ORPC tool call throws
|
|
161
|
-
// "app router provider not configured". Parallel to dev-mcp-boot's
|
|
162
|
-
// configureToolExecutor. Calls getAppRouter() fresh each time (not a
|
|
163
|
-
// boot-time capture) so the tool-executor always resolves the CURRENT
|
|
164
|
-
// memoized router — the tool-generator listener may regenerate the
|
|
165
|
-
// ORPC router and call clearAppRouterCache() after boot, producing a
|
|
166
|
-
// new router object that a captured closure would miss.
|
|
167
118
|
const { configure: configureToolExecutor } = await import(
|
|
168
119
|
"@teamscala/tool-executor/configure"
|
|
169
120
|
);
|
|
170
|
-
// The caller-identity and secret collaborators arrive here (the boot
|
|
171
|
-
// layer) — tool-executor cannot import its tier-2 siblings statically
|
|
172
|
-
// (vertical-dependency-only: a same-tier edge). Resolved eagerly at
|
|
173
|
-
// mount so the injected factories stay synchronous. The retired
|
|
174
|
-
// @teamscala/host-command-bus invoker injection no longer happens: the
|
|
175
|
-
// bus is retired, and host_command tools execute through the native
|
|
176
|
-
// in-process coding path inside tool-executor.
|
|
177
121
|
const [{ getCallerContext }, { decryptSecretConfig }] =
|
|
178
122
|
await Promise.all([
|
|
179
123
|
import("@teamscala/mcp-multi-session/caller-context"),
|
|
@@ -186,19 +130,12 @@ export async function mountOrpcRouter(
|
|
|
186
130
|
decryptSecretConfig: (raw) => decryptSecretConfig(raw as never),
|
|
187
131
|
});
|
|
188
132
|
|
|
189
|
-
// The fn capability primitives (foundation tier) cannot import the email
|
|
190
|
-
// client, so the boot layer injects the real sender — the same shape the
|
|
191
|
-
// bootloader already uses for getPrisma. Until configured, fn.workItems
|
|
192
|
-
// notification procedures degrade to { notified: 0 } with an error log
|
|
193
|
-
// (never throw).
|
|
194
133
|
try {
|
|
195
134
|
const [{ configureWorkItems }, { createEmailClient }] = await Promise.all([
|
|
196
135
|
import("@teamscala/db/work-items"),
|
|
197
136
|
import("@teamscala/email-client/client"),
|
|
198
137
|
]);
|
|
199
138
|
configureWorkItems({
|
|
200
|
-
// The injected contract is the template-send subset; the real
|
|
201
|
-
// client's send agrees at every call site ({to, template, vars}).
|
|
202
139
|
emailSender: createEmailClient() as never,
|
|
203
140
|
logger: log,
|
|
204
141
|
});
|
|
@@ -226,17 +163,7 @@ export async function mountOrpcRouter(
|
|
|
226
163
|
id: session.user.id,
|
|
227
164
|
name: session.user.name ?? undefined,
|
|
228
165
|
email: session.user.email,
|
|
229
|
-
// The active organisation lives on the SESSION
|
|
230
|
-
// (activeOrganizationId, set at sign-in by scala-otp's
|
|
231
|
-
// session.create hook), NOT on the user object — getSession's
|
|
232
|
-
// ResolvedSession.user carries no organisationId. Reading the
|
|
233
|
-
// user field left organisationId always null, so the generated
|
|
234
|
-
// router's org-scope gate 403'd every non-super-admin write
|
|
235
|
-
// ("Organization membership required") on the public-HTTP path.
|
|
236
166
|
organisationId: session.session.activeOrganizationId ?? null,
|
|
237
|
-
// better-auth's admin plugin issues role "admin"; the platform
|
|
238
|
-
// also uses "super-admin"/"super_admin". All three are platform
|
|
239
|
-
// super-admins for ORPC org-scope bypass.
|
|
240
167
|
isSuperAdmin:
|
|
241
168
|
role === "super_admin" ||
|
|
242
169
|
role === "super-admin" ||
|
|
@@ -260,10 +187,6 @@ export async function mountOrpcRouter(
|
|
|
260
187
|
context: { prisma, db8, headers: c.req.raw.headers, resolveUser },
|
|
261
188
|
});
|
|
262
189
|
if (!result?.matched) return c.notFound();
|
|
263
|
-
// Response-side signal only (a fleet-wide 500 spike with no
|
|
264
|
-
// traceback — measured 2026-09-04). The real error, its class and
|
|
265
|
-
// its stack are captured by the mount error hook above; this line
|
|
266
|
-
// stays for failures that reach the boundary without a throw.
|
|
267
190
|
if (result.response.status >= 500) {
|
|
268
191
|
log.error(
|
|
269
192
|
`[ORPC] ${c.req.method} ${c.req.path} → ${result.response.status}`,
|
|
@@ -273,16 +196,6 @@ export async function mountOrpcRouter(
|
|
|
273
196
|
},
|
|
274
197
|
);
|
|
275
198
|
|
|
276
|
-
// THE OpenAPI CONTRACT, served BESIDE the RPC — a GET on the same prefix,
|
|
277
|
-
// generated from the router object mounted below, so the document and the
|
|
278
|
-
// handler cannot describe different surfaces. Order is free: the methods
|
|
279
|
-
// never overlap (`app.post` matches POST alone).
|
|
280
|
-
//
|
|
281
|
-
// Gated by openApiContractAuthorised, which refuses a proxied request as
|
|
282
|
-
// well as a wrong key (guard auth-helpers-fail-closed), so only an
|
|
283
|
-
// internal caller — the MCP gateway — reaches this. Its own facts come
|
|
284
|
-
// from openApiContractOptions; both units carry the reasoning, and
|
|
285
|
-
// contract rows state their behaviour.
|
|
286
199
|
app.get("/api/rpc/openapi.json", async (c) => {
|
|
287
200
|
const header = (name: string) => c.req.header(name) ?? null;
|
|
288
201
|
if (
|
|
@@ -89,9 +89,6 @@ export async function mountServerPackages(
|
|
|
89
89
|
lastNotFound = err;
|
|
90
90
|
continue;
|
|
91
91
|
}
|
|
92
|
-
// Real error (syntax, runtime, transitive resolution failure inside the
|
|
93
|
-
// loaded module) — surface it directly. Swallowing here is what hides
|
|
94
|
-
// the true cause behind a misleading "cannot find <pkg>" further down.
|
|
95
92
|
throw new BootFatalError(
|
|
96
93
|
"mount-server-packages",
|
|
97
94
|
serviceSlug,
|
|
@@ -122,30 +119,6 @@ export async function mountServerPackages(
|
|
|
122
119
|
}
|
|
123
120
|
} catch (err) {
|
|
124
121
|
if (err instanceof BootFatalError) throw err;
|
|
125
|
-
// A PLUGIN THAT FAILS TO MOUNT IS UNAVAILABLE, NOT FATAL. `crash-isolation`
|
|
126
|
-
// states the law directly — "a failure in one package does not crash the
|
|
127
|
-
// host" — and names the plugin failure domain: "a broken plugin degrades
|
|
128
|
-
// to 'that plugin unavailable'". This arm used to wrap ANY mount error in
|
|
129
|
-
// a BootFatalError, so one plugin's missing config killed a whole service.
|
|
130
|
-
//
|
|
131
|
-
// MEASURED 2026-09-07: scala-agents-ui, the operator's fleet monitoring UI,
|
|
132
|
-
// booted through db, auth, routes and every other server_package, then died
|
|
133
|
-
// at @teamscala/opencode because OPENCODE_SERVER_URL was absent. systemd
|
|
134
|
-
// retried five times and gave up, agents.scala.business served 502, and the
|
|
135
|
-
// operator lost sight of his fleet — over one optional integration missing
|
|
136
|
-
// one variable, on a page that needs no opencode connection to render.
|
|
137
|
-
//
|
|
138
|
-
// THE ASYMMETRY DECIDES IT. A service that boots without one plugin still
|
|
139
|
-
// serves every route that plugin did not own, and the gap is loud in this
|
|
140
|
-
// log line and counted in the summary below. A service that refuses to boot
|
|
141
|
-
// serves NOTHING — including the surface that would have explained why.
|
|
142
|
-
//
|
|
143
|
-
// NOT-FOUND STAYS FATAL, deliberately: that arm is above and untouched. A
|
|
144
|
-
// package that LOADS and then throws is a broken plugin, which is this
|
|
145
|
-
// case. A package that cannot be resolved at all means the declared
|
|
146
|
-
// dependency set was never installed — an install-integrity failure, where
|
|
147
|
-
// booting a half-installed service would serve wrong results while looking
|
|
148
|
-
// healthy.
|
|
149
122
|
degraded.push(pkg);
|
|
150
123
|
log.error(
|
|
151
124
|
`[scala-install:${serviceSlug}] server_package "${pkg}" UNAVAILABLE — mount failed; ` +
|
|
@@ -158,8 +131,6 @@ export async function mountServerPackages(
|
|
|
158
131
|
`[scala-install:${serviceSlug}] Registered ${mountRegistry.handlers.size} handler(s), ` +
|
|
159
132
|
`${mountRegistry.wsRoutes.size} ws route(s), ` +
|
|
160
133
|
`${mountRegistry.bootHooks.size} boot hook(s)` +
|
|
161
|
-
// NAMED, NEVER COUNTED. A bare "1 degraded" sends the reader hunting for
|
|
162
|
-
// which one; the name is the whole value of the line.
|
|
163
134
|
(degraded.length > 0
|
|
164
135
|
? `, ${degraded.length} server_package(s) UNAVAILABLE: ${degraded.join(", ")}`
|
|
165
136
|
: ""),
|
package/src/mutable-app-ref.ts
CHANGED
|
@@ -7,18 +7,11 @@ import type { Hono } from "hono";
|
|
|
7
7
|
let currentApp: Hono | null = null;
|
|
8
8
|
let appLabel = "initial";
|
|
9
9
|
|
|
10
|
-
/**
|
|
11
|
-
* Create a new mutable app reference initialized with the provided app.
|
|
12
|
-
* This should be called once at boot time.
|
|
13
|
-
*/
|
|
14
10
|
export function initMutableAppRef(initialApp: Hono, label: string): void {
|
|
15
11
|
currentApp = initialApp;
|
|
16
12
|
appLabel = label;
|
|
17
13
|
}
|
|
18
14
|
|
|
19
|
-
/**
|
|
20
|
-
* Get the current app. Throws if the ref hasn't been initialized.
|
|
21
|
-
*/
|
|
22
15
|
export function getApp(): Hono {
|
|
23
16
|
if (!currentApp) {
|
|
24
17
|
throw new Error("Mutable app reference not initialized");
|
|
@@ -26,10 +19,6 @@ export function getApp(): Hono {
|
|
|
26
19
|
return currentApp;
|
|
27
20
|
}
|
|
28
21
|
|
|
29
|
-
/**
|
|
30
|
-
* Atomically swap the app to a new instance. This is called after the
|
|
31
|
-
* full app is built in the background. Returns the previous label for logging.
|
|
32
|
-
*/
|
|
33
22
|
export function swapApp(newApp: Hono, newLabel: string): string {
|
|
34
23
|
const oldLabel = appLabel;
|
|
35
24
|
currentApp = newApp;
|