@teamscala/service-boot 0.23.284 → 0.23.285
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 +1006 -1014
- package/src/agreement-gate.d.ts +19 -0
- package/src/asset-cache-key.d.ts +11 -0
- package/src/asset-optimize.d.ts +17 -0
- package/src/asset-proxy.d.ts +11 -0
- package/src/asset-social-card.d.ts +17 -0
- package/src/attach-agreement-magic.d.ts +20 -0
- package/src/attach-portal-members.d.ts +23 -0
- package/src/attach-proposal-accept.d.ts +26 -0
- package/src/attach-proposal-magic.d.ts +18 -0
- package/src/attach-transfers-upload.d.ts +27 -0
- package/src/auth-gate.d.ts +28 -0
- package/src/bind-page-handler.d.ts +18 -0
- package/src/build-client-assets/input-hashing.d.ts +25 -0
- package/src/build-client-assets/source-candidates.d.ts +27 -0
- package/src/build-client-assets.d.ts +50 -0
- package/src/compose-page-title.d.ts +41 -0
- package/src/content-gate/access.d.ts +15 -0
- package/src/content-gate/config-types.d.ts +99 -0
- package/src/content-gate/gate.d.ts +13 -0
- package/src/content-gate/parse-cookie.d.ts +9 -0
- package/src/content-gate/token.d.ts +48 -0
- package/src/content-page-floor.d.ts +39 -0
- package/src/discover-blocks.d.ts +28 -0
- package/src/expand-roster.d.ts +34 -0
- package/src/fn-routers/agreement-completion-email.d.ts +14 -0
- package/src/fn-routers/agreement-send-router.d.ts +82 -0
- package/src/fn-routers/ai-health-router.d.ts +35 -0
- package/src/fn-routers/auth-router.d.ts +47 -0
- package/src/fn-routers/cache-admin-router.d.ts +57 -0
- package/src/fn-routers/chat-session-router.d.ts +219 -0
- package/src/fn-routers/email-defaults.d.ts +1 -0
- package/src/fn-routers/email-link-ttl.d.ts +1 -0
- package/src/fn-routers/index.d.ts +18 -0
- package/src/fn-routers/panel-data-router.d.ts +19 -0
- package/src/fn-routers/persist-chat-messages.d.ts +36 -0
- package/src/fn-routers/plans-router.d.ts +25 -0
- package/src/fn-routers/proposal-send-router.d.ts +58 -0
- package/src/fn-routers/public-agreement-router.d.ts +281 -0
- package/src/fn-routers/public-assessment-router.d.ts +68 -0
- package/src/fn-routers/public-gpt-session-router.d.ts +125 -0
- package/src/fn-routers/sign-in-flow-router.d.ts +75 -0
- package/src/fn-routers/task-sync-dispatch.d.ts +61 -0
- package/src/fn-routers/task-sync-router.d.ts +376 -0
- package/src/fn-routers/task-sync-router.test.ts +32 -0
- package/src/fn-routers/task-sync-router.ts +27 -1
- package/src/fn-routers/tasks-router.d.ts +25 -0
- package/src/fn-routers/user-preferences-router.d.ts +77 -0
- package/src/fn-routers/work-items-router.d.ts +102 -0
- package/src/gate-ttls.d.ts +6 -0
- package/src/gpt-gate.d.ts +25 -0
- package/src/handlers/bun-stable-watch.d.ts +1 -0
- package/src/handlers/projects-digest.d.ts +26 -0
- package/src/handlers/prune-stale-data.d.ts +30 -0
- package/src/handlers/registry-stale-inactive-sweep.d.ts +1 -0
- package/src/handlers/scala-tools-currency-watch.d.ts +1 -0
- package/src/handlers/scheduled-browser-tests.d.ts +1 -0
- package/src/handlers/sweep-event-log.d.ts +1 -0
- package/src/handlers/sweep-host-cache.d.ts +1 -0
- package/src/handlers/upstream-issue-sync.d.ts +1 -0
- package/src/handlers/whatsapp-liveness-logic.d.ts +34 -0
- package/src/handlers/whatsapp-liveness.d.ts +4 -0
- package/src/hono-error-handler.d.ts +25 -0
- package/src/install-harness.d.ts +45 -0
- package/src/install-logging.d.ts +34 -0
- package/src/load-service-install-row.d.ts +24 -0
- package/src/load-service-prisma-client.d.ts +18 -0
- package/src/mount-orpc-router.d.ts +15 -0
- package/src/mount-server-packages.d.ts +29 -0
- package/src/mount.d.ts +9 -0
- package/src/mutable-app-ref.d.ts +38 -0
- package/src/page-route-shell-types.d.ts +12 -0
- package/src/page-server-orpc/audit-revert-router.d.ts +37 -0
- package/src/page-server-orpc/hub-mcp-client.d.ts +12 -0
- package/src/page-server-orpc/portal-diagnostics-router.d.ts +49 -0
- package/src/page-server-orpc/puck-pages-router.d.ts +58 -0
- package/src/page-server-orpc/server-client.d.ts +16 -0
- package/src/page-server-orpc/templates-router.d.ts +34 -0
- package/src/page-server-orpc/user-preferences-router.d.ts +79 -0
- package/src/pg-boss-handler-registry.d.ts +25 -0
- package/src/pg-boss-mount.d.ts +39 -0
- package/src/proposal-authorized-emails.d.ts +28 -0
- package/src/proposal-chat-inject.d.ts +17 -0
- package/src/proposal-gate.d.ts +18 -0
- package/src/read-html-template.d.ts +38 -0
- package/src/read-viewport-from-request.d.ts +18 -0
- package/src/readiness-gate.d.ts +16 -0
- package/src/register-api-routes.d.ts +18 -0
- package/src/register-crash-handlers.d.ts +15 -0
- package/src/register-page-routes.d.ts +27 -0
- package/src/register-page-server-client.d.ts +11 -0
- package/src/register-registry-page-route/ai-chat-prefetch.d.ts +13 -0
- package/src/register-registry-page-route/graph-cache.d.ts +30 -0
- package/src/register-registry-page-route/meta-title.d.ts +2 -0
- package/src/register-registry-page-route/proposal-approval-email-token.d.ts +14 -0
- package/src/register-registry-page-route/proposal-brand-stamping.d.ts +28 -0
- package/src/register-registry-page-route/resolve-page-data.d.ts +1 -0
- package/src/register-registry-page-route.d.ts +27 -0
- package/src/register-static-assets.d.ts +13 -0
- package/src/register-ws-routes.d.ts +28 -0
- package/src/resolve-app-base-url.d.ts +13 -0
- package/src/resolve-brand-favicon.d.ts +28 -0
- package/src/resolve-from-loader-config.d.ts +32 -0
- package/src/resolve-service-context.d.ts +17 -0
- package/src/resolve-theme-mode.d.ts +42 -0
- package/src/resolvers/fetch-org-brand.d.ts +2 -0
- package/src/resolvers/portal-route-paths.d.ts +39 -0
- package/src/resolvers/website-by-host-map.d.ts +12 -0
- package/src/resource-chat-inject.d.ts +46 -0
- package/src/resource-not-found.d.ts +60 -0
- package/src/run-boot-hook-list.d.ts +19 -0
- package/src/run-boot-hooks.d.ts +31 -0
- package/src/scala-tools-currency-watchdog.d.ts +10 -0
- package/src/security-headers.d.ts +12 -0
- package/src/serve/serve.d.ts +46 -0
- package/src/server-error-page.d.ts +29 -0
- package/src/start-scheduled-jobs.d.ts +18 -0
- package/src/two-phase-boot.d.ts +93 -0
- package/src/types.d.ts +46 -0
- package/src/ui-service.d.ts +46 -0
- package/src/ws-boot-self-test.d.ts +19 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system server-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* Registers each `service_install.websocket_routes[]` entry as a Hono WS route.
|
|
7
|
+
* The ORPC WS (`/api/rpc/ws`) is mounted by the canonical primitive
|
|
8
|
+
* `mountOrpcWs` (@teamscala/orpc/ws/server) over the FULLY-COMPOSED app router —
|
|
9
|
+
* this runs AFTER registerFnRouters, so the eager RPCHandler snapshots every
|
|
10
|
+
* namespace, and any `output: eventIterator(...)` procedure dispatches (rule
|
|
11
|
+
* `orpc-ws-via-mount-orpc-ws`). It is NOT a server_package WsRouteHandler — it
|
|
12
|
+
* is exempt from the legacy invariant below.
|
|
13
|
+
*
|
|
14
|
+
* The legacy `WsRouteHandler` interface (open/message/close on a Bun
|
|
15
|
+
* ServerWebSocket) is preserved by an adapter for already-migrated
|
|
16
|
+
* server_packages (terminal-ws); new WS handlers should use Hono's WSEvents
|
|
17
|
+
* directly.
|
|
18
|
+
*
|
|
19
|
+
* INVARIANT (legacy paths only): every handler registered via
|
|
20
|
+
* mountCtx.registerWsRoute MUST have a matching entry in config.websocket_routes
|
|
21
|
+
* (minus /api/rpc/ws), and vice versa. A mismatch is fatal — the service cannot
|
|
22
|
+
* boot with handlers declared but never wired (silent 404 on upgrade), or routes
|
|
23
|
+
* that point at handlers no server_package registered.
|
|
24
|
+
*/
|
|
25
|
+
import type { ServiceInstallConfig } from "@teamscala/db/registry/loader/load-service-install-row";
|
|
26
|
+
import type { MountRegistry } from "@teamscala/os/runtime-contracts/mount-context";
|
|
27
|
+
import type { Hono } from "hono";
|
|
28
|
+
export declare function registerWsRoutes(app: Hono, config: ServiceInstallConfig, mountRegistry: MountRegistry, serviceSlug: string): Promise<number>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system service-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* The scala-projects app's own public hostname, read from its `service_install`
|
|
7
|
+
* row rather than hardcoded — a service knows its own deploy config only
|
|
8
|
+
* through the registry (registries-are-source-of-truth), never a literal in
|
|
9
|
+
* source. Cached for the process lifetime: this changes only on a redeploy,
|
|
10
|
+
* not per-request. Shared by every server-side path that builds a link back
|
|
11
|
+
* into the app (approval emails, mention emails, digest emails).
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveAppBaseUrl(): Promise<string>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system service-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* Host-level brand favicon resolution. A single-brand UI service's host has
|
|
7
|
+
* a KNOWN owner (`service_install.config.brand_org`) — serving the generic
|
|
8
|
+
* platform "S" at the implicit /favicon.ico path there is a defect: browsers
|
|
9
|
+
* and crawlers hit that path directly and their favicon caches are sticky
|
|
10
|
+
* (operator-observed 2026-06-05 on launcha's proposal pages).
|
|
11
|
+
*
|
|
12
|
+
* Resolution is by convention from the org's asset store:
|
|
13
|
+
* `<brand_org>/logos/favicon.png`. Existence is probed once through the
|
|
14
|
+
* in-process asset proxy and cached (positive AND negative) so the favicon
|
|
15
|
+
* route never adds S3 latency per request. No brand_org, or no asset at the
|
|
16
|
+
* convention key → null, and the caller serves the generic fallback.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveBrandFaviconPath(brandOrg: string | undefined): Promise<string | null>;
|
|
19
|
+
/**
|
|
20
|
+
* Eagerly resolve + cache the favicon at boot so the FIRST user request never
|
|
21
|
+
* pays the S3 existence-probe latency. The probe (`handleAssetProxy` →
|
|
22
|
+
* `s3.file(key).stat()`) is a COLD S3 call — the connection setup to the asset
|
|
23
|
+
* store takes 6s locally / up to 21s in prod, and the in-process cache resets
|
|
24
|
+
* on every restart (deploys, package-poller). Without warming, the first
|
|
25
|
+
* /favicon.ico after each restart stalls for that duration (operator-reported
|
|
26
|
+
* 23s). Fire-and-forget — runs in the background during boot, never blocks.
|
|
27
|
+
*/
|
|
28
|
+
export declare function warmBrandFaviconCache(brandOrg: string | undefined): void;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system ui-service-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* Walks a resource_loader.sources[] graph and resolves each source in
|
|
7
|
+
* dependency order. Returns an assembled data object keyed by source key.
|
|
8
|
+
*
|
|
9
|
+
* Source types: "orpc", "prisma", "resolver_fn".
|
|
10
|
+
* Each source may declare depends_on for ordering.
|
|
11
|
+
* Params are interpolated from URL params and prior resolved data.
|
|
12
|
+
*/
|
|
13
|
+
interface SourceConfig {
|
|
14
|
+
key: string;
|
|
15
|
+
source: "orpc" | "prisma" | "resolver_fn";
|
|
16
|
+
model?: string;
|
|
17
|
+
where?: Record<string, unknown>;
|
|
18
|
+
select?: string[];
|
|
19
|
+
depends_on?: string[];
|
|
20
|
+
optional?: boolean;
|
|
21
|
+
resolver_fn?: string;
|
|
22
|
+
resolver_input?: Record<string, unknown>;
|
|
23
|
+
registry_filter?: Record<string, unknown>;
|
|
24
|
+
transforms?: string[];
|
|
25
|
+
}
|
|
26
|
+
interface LoaderConfig {
|
|
27
|
+
sources: SourceConfig[];
|
|
28
|
+
merge_strategy?: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
export declare function resolveFromLoaderConfig(config: LoaderConfig, params: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
31
|
+
export declare function interpolateMeta(template: Record<string, unknown>, data: Record<string, unknown>): Record<string, unknown>;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system server-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* Resolves the minimum service context the bin needs to choose its launch
|
|
7
|
+
* path: connect DB, load registry, read service_install row, return kind +
|
|
8
|
+
* port. Caller is responsible for disconnecting.
|
|
9
|
+
*/
|
|
10
|
+
import type { ServiceInstallConfig } from "@teamscala/db/registry/loader/load-service-install-row";
|
|
11
|
+
export interface ServiceContext {
|
|
12
|
+
kind: "ui" | "backend" | "runtime";
|
|
13
|
+
port: number;
|
|
14
|
+
config: ServiceInstallConfig;
|
|
15
|
+
disconnect: () => Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
export declare function resolveServiceContext(serviceSlug: string, databaseUrl: string): Promise<ServiceContext>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system ui-service-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* The ONE theme-mode decider for every page render, across every UI app
|
|
7
|
+
* (portal, proposals, sign, forms, quiz, gpt, public). Every route handler in service-boot
|
|
8
|
+
* resolves light/dark through this single function — never its own inline
|
|
9
|
+
* `(... === "dark" ? "dark" : "light")` chain. That inline form was duplicated
|
|
10
|
+
* across every route branch, so flipping one app's theme required editing
|
|
11
|
+
* every copy (the heavy lifting this removes).
|
|
12
|
+
*
|
|
13
|
+
* Resolution order (first match wins):
|
|
14
|
+
* 1. ?theme=light|dark on the URL — the embed override. Set when a page is
|
|
15
|
+
* framed inside another shell (a form in a GPT tool panel) so the framed
|
|
16
|
+
* page matches the host's scheme. Also marks the render `embedded` so the
|
|
17
|
+
* page can drop its own brand page-surface and render the host's clean
|
|
18
|
+
* surface.
|
|
19
|
+
* 2. candidates[] — page rootProps.theme, brand theme, website default, in
|
|
20
|
+
* whatever priority the caller passes them.
|
|
21
|
+
* 3. "light".
|
|
22
|
+
*
|
|
23
|
+
* Pairs with the brand/token decider (resolveBrand → computeThemeVars,
|
|
24
|
+
* `unified-brand-theme`): that owns the SURFACE, this owns the MODE.
|
|
25
|
+
*/
|
|
26
|
+
export interface ResolvedThemeMode {
|
|
27
|
+
theme: "light" | "dark";
|
|
28
|
+
/** True iff the mode came from the ?theme= embed override. */
|
|
29
|
+
embedded: boolean;
|
|
30
|
+
}
|
|
31
|
+
export declare function resolveThemeMode(opts: {
|
|
32
|
+
/** Request URL (or its href) — read for the ?theme= embed override. */
|
|
33
|
+
url?: URL | string | null;
|
|
34
|
+
/**
|
|
35
|
+
* Explicit embed override, when the caller already extracted ?theme= from
|
|
36
|
+
* the URL upstream (e.g. a pure data-shaper that has no Request). Same
|
|
37
|
+
* effect and priority as the URL form.
|
|
38
|
+
*/
|
|
39
|
+
override?: unknown;
|
|
40
|
+
/** Theme candidates in priority order: rootTheme, brandTheme, websiteTheme. */
|
|
41
|
+
candidates?: Array<unknown>;
|
|
42
|
+
}): ResolvedThemeMode;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system ui-service-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* portal-route-paths.ts — pure path mapper for portal region routing.
|
|
7
|
+
*
|
|
8
|
+
* Extracted from resolve-from-loader-config.ts (the hardcoded
|
|
9
|
+
* `resolvePortalRoutePaths` arm) so it can be invoked via a resolver_fn
|
|
10
|
+
* registry row instead of an if-chain. Faithful extraction — no logic change.
|
|
11
|
+
* The request path is the center page's builda path ("/" falls back to
|
|
12
|
+
* home_path), and the per-route toolbar page follows the flat naming convention
|
|
13
|
+
* `/<last-segment>-toolbar`. Pages without a matching toolbar page simply
|
|
14
|
+
* resolve an empty toolbar region (the source is declared optional).
|
|
15
|
+
*/
|
|
16
|
+
export interface ResolverArgs {
|
|
17
|
+
input: Record<string, unknown>;
|
|
18
|
+
client: unknown;
|
|
19
|
+
params: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Extract the organisation id from a portal-shaped callback/path — the single
|
|
23
|
+
* authority for "which segment is the org" so the sign-in brand resolver and the
|
|
24
|
+
* route resolver cannot drift on the answer (portal-branding-resolves-page-org-first).
|
|
25
|
+
*
|
|
26
|
+
* The canonical portal shape is `/<websiteId>/<orgId>/<path>` — the org is
|
|
27
|
+
* segment 1, NOT segment 0. Reading segment 0 fed the WEBSITE id into brand
|
|
28
|
+
* resolution, which resolved no org and fell back to the super-admin org — the
|
|
29
|
+
* exact cross-tenant mis-branding this exists to prevent (bind-page-handler.ts,
|
|
30
|
+
* found 2026-07-21).
|
|
31
|
+
*
|
|
32
|
+
* Single-UUID hosts keep segment 0 unchanged: scala-projects routes `/<orgId>/…`
|
|
33
|
+
* and proposal/form/quiz hosts route `/<resourceId>/…` (a non-org first segment
|
|
34
|
+
* that resolves to no brand and falls back as before — no regression, only the
|
|
35
|
+
* correct-org improvement for the two-UUID portal shape). Returns undefined when
|
|
36
|
+
* neither leading segment is a UUID.
|
|
37
|
+
*/
|
|
38
|
+
export declare function extractOrgIdFromCallbackPath(callbackUrl: string): string | undefined;
|
|
39
|
+
export declare function resolvePortalRoutePaths({ input, params }: ResolverArgs): Record<string, unknown>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system ui-service-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* website-by-host-map.ts — resolves the website + body path for a host.
|
|
7
|
+
*
|
|
8
|
+
* Extracted from resolve-from-loader-config.ts (the hardcoded
|
|
9
|
+
* `resolveWebsiteByHostMap` arm). Faithful extraction — no logic change.
|
|
10
|
+
*/
|
|
11
|
+
import type { ResolverArgs } from "./portal-route-paths.ts";
|
|
12
|
+
export declare function resolveWebsiteByHostMap({ input, client, params }: ResolverArgs): Promise<Record<string, unknown> | null>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system service-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* Injects the generic ResourceChatLauncher block into an AUTHORISED resource
|
|
7
|
+
* page (proposal, agreement, or any gated document) — called from a gate's
|
|
8
|
+
* onAuthorized, so the floating GPT bubble lives strictly behind the email gate
|
|
9
|
+
* and every resource of that type gets it with no per-page Puck edit. It builds
|
|
10
|
+
* the resource's page content into a runtime-context string and passes it to the
|
|
11
|
+
* launcher, which forwards it to the chat as <runtime_context> — so ONE shared
|
|
12
|
+
* assistant agent answers about whichever resource is being viewed.
|
|
13
|
+
*
|
|
14
|
+
* RESOURCE-AGNOSTIC by design (one-ai-chat-system / chat-ui-is-one-canonical-block):
|
|
15
|
+
* the launcher, the chat engine, and this injector are generic; only the
|
|
16
|
+
* per-resource CONFIG differs (which agent, the copy, which resolved key holds
|
|
17
|
+
* the record). The agent id is config, not code (registries-are-source-of-truth):
|
|
18
|
+
* each resource type names a `config / <slug>-assistant-agent` row carrying
|
|
19
|
+
* { agentId }. No row (or no agent) → no injection, page renders unchanged
|
|
20
|
+
* (fail-soft).
|
|
21
|
+
*/
|
|
22
|
+
/** Per-resource-type injection config — the only thing that varies. */
|
|
23
|
+
export interface ResourceChatOptions {
|
|
24
|
+
/** Key in `resolved` holding the record (e.g. "proposal", "agreement"). */
|
|
25
|
+
resourceKey: string;
|
|
26
|
+
/** `config` row slug carrying { agentId } (e.g. "agreement-assistant-agent"). */
|
|
27
|
+
agentConfigSlug: string;
|
|
28
|
+
/** Org id to scope the chat. If omitted, read from resolved[resourceKey].organisation_id
|
|
29
|
+
* (proposals); agreements pass it explicitly (org isn't on the resolved record). */
|
|
30
|
+
organisationId?: string;
|
|
31
|
+
/** Overlay title-bar label (e.g. "Agreement Assistant"). */
|
|
32
|
+
title: string;
|
|
33
|
+
/** Floating button label / speech bubble (e.g. "Ask about this agreement"). */
|
|
34
|
+
launchLabel: string;
|
|
35
|
+
/** First assistant message. */
|
|
36
|
+
welcomeMessage?: string;
|
|
37
|
+
/** Input placeholder. */
|
|
38
|
+
placeholder?: string;
|
|
39
|
+
/** Suggested starter questions. */
|
|
40
|
+
conversationStarters?: string[];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Inject the generic ResourceChatLauncher behind the gate. `opts` is the only
|
|
44
|
+
* per-resource-type variation; everything else is shared.
|
|
45
|
+
*/
|
|
46
|
+
export declare function injectResourceChatLauncher(resolved: Record<string, unknown>, viewerEmail: string, opts: ResourceChatOptions): Promise<void>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system service-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
* @indivisible-unit
|
|
6
|
+
*
|
|
7
|
+
* One cohesive unit — the branded resource-not-found / holding page: the
|
|
8
|
+
* PrimaryResourceMissingError trigger, the org-logo resolver the page
|
|
9
|
+
* renders, and the HTML builder are tightly coupled (the builder consumes
|
|
10
|
+
* the logo; the error drives the page) and cannot be split without churn.
|
|
11
|
+
*
|
|
12
|
+
* Branded 404 for registry page routes whose PRIMARY resource is missing.
|
|
13
|
+
*
|
|
14
|
+
* A route that declares `resource_type` (proposal, form, quiz, agreement…)
|
|
15
|
+
* is meaningless without that entity. Before this existed, a dead link
|
|
16
|
+
* (wrong id, deleted row) fell through the loader with nulls and rendered
|
|
17
|
+
* whatever default content the shell could scrape together — leaking
|
|
18
|
+
* internal pages (the "Vision Toolbar" incident, operator-observed
|
|
19
|
+
* 2026-06-05) on the org's own branded host, with a 200 status that
|
|
20
|
+
* search engines and link unfurlers would happily cache.
|
|
21
|
+
*
|
|
22
|
+
* The replacement: loadData throws PrimaryResourceMissingError; the page
|
|
23
|
+
* binder maps it to a minimal branded 404 — correct status, noindex, the
|
|
24
|
+
* host's own favicon (which resolves the owning org per brand_org), and a
|
|
25
|
+
* plain human sentence. Deliberately NOT a puck page: the error surface
|
|
26
|
+
* must render even when data loading is the thing that failed. A
|
|
27
|
+
* website-editable /404 puck page can layer on top later; this is the
|
|
28
|
+
* floor that must always exist.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* The host org's LOGO (wordmark) for the dark error page — the favicon is a
|
|
32
|
+
* tab glyph, the wrong asset at page scale (operator-observed: a lone "h"
|
|
33
|
+
* where the launcha wordmark belonged). Resolution: the service's own
|
|
34
|
+
* install row names its owner (brand_org); the org's dark-theme logo (a
|
|
35
|
+
* light-coloured mark, correct on the dark page) is preferred, light-theme
|
|
36
|
+
* second. Every step skew-tolerant: any miss → null → the builder renders a
|
|
37
|
+
* logo-less dark page.
|
|
38
|
+
*
|
|
39
|
+
* Returns the logo as an INLINE data URI (not a `/api/assets/` proxy URL) so
|
|
40
|
+
* the branded holding/error page ships the logo IN the initial HTML bytes —
|
|
41
|
+
* no second fetch, no post-paint pop-in, no dependency on the asset proxy the
|
|
42
|
+
* degraded service is failing to serve (incident 2026-07-27: the logo on a
|
|
43
|
+
* DEGRADED render-layer holding page appeared after the page painted). The
|
|
44
|
+
* logo sibling of icons-render-ssr-inline. Size-capped: a pathological
|
|
45
|
+
* (>MAX_INLINE_LOGO_BYTES) logo falls back to null rather than bloating the
|
|
46
|
+
* HTML; the page renders logo-less, never broken.
|
|
47
|
+
*/
|
|
48
|
+
export declare function resolveErrorPageLogo(serviceSlug: string): Promise<string | null>;
|
|
49
|
+
export declare class PrimaryResourceMissingError extends Error {
|
|
50
|
+
readonly resourceType: string;
|
|
51
|
+
readonly httpStatus: number;
|
|
52
|
+
readonly empty: boolean;
|
|
53
|
+
constructor(resourceType: string, httpStatus?: number, opts?: {
|
|
54
|
+
empty?: boolean;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
export declare function buildResourceNotFoundResponse(resourceType: string, logoUrl?: string | null, status?: number, opts?: {
|
|
58
|
+
homeUrl?: string;
|
|
59
|
+
empty?: boolean;
|
|
60
|
+
}): Response;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system server-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* Runs the service's boot hooks. A server_package registers a hook via
|
|
7
|
+
* ctx.registerBootHook during the mount phase; a hook registered by an
|
|
8
|
+
* INSTALLED server_package is intended to run (the package is installed
|
|
9
|
+
* because the service wants it). This therefore invokes EVERY registered
|
|
10
|
+
* hook — those declared in `service_install.boot_hooks[]` first (preserving
|
|
11
|
+
* their declared order), then any registered-but-undeclared hooks. Requiring a
|
|
12
|
+
* hook to be BOTH registered by its package AND re-declared in boot_hooks is a
|
|
13
|
+
* double-declaration footgun — it left the realtime-utils pg-notify bridge
|
|
14
|
+
* registered-but-never-invoked (boot_hooks was null), so the live-refresh
|
|
15
|
+
* relay never started (2026-06-29).
|
|
16
|
+
*/
|
|
17
|
+
import type { ServiceInstallConfig } from "@teamscala/db/registry/loader/load-service-install-row";
|
|
18
|
+
import type { MountRegistry } from "@teamscala/os/runtime-contracts/mount-context";
|
|
19
|
+
export declare function runBootHookList(config: ServiceInstallConfig, mountRegistry: MountRegistry, serviceSlug: string): Promise<void>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system server-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* Runs service_install.boot_hooks in declaration order. A boot hook is a
|
|
7
|
+
* registry-declared, one-shot startup function that needs to execute after
|
|
8
|
+
* prisma + registry are ready but before the HTTP server starts serving.
|
|
9
|
+
* Used for custom startup logic (stale-run reconciliation, cache hydration,
|
|
10
|
+
* ALS seeding, long-running watcher start) that doesn't fit the pg-boss
|
|
11
|
+
* recurring model and is too specific to be a generic installer step.
|
|
12
|
+
*
|
|
13
|
+
* Each hook is resolved by dynamic-importing its handler_slug (or slug when
|
|
14
|
+
* handler_slug is absent) and awaiting its default export. The export must
|
|
15
|
+
* be an async function accepting a BootHookContext. Hooks are invoked in
|
|
16
|
+
* ascending order of their `order` field (default 100).
|
|
17
|
+
*
|
|
18
|
+
* Failures are fatal. The installer has no partial-boot concept: if a
|
|
19
|
+
* declared hook cannot run, the service is broken, and booting anyway would
|
|
20
|
+
* mask the regression. BootFatalError is thrown to propagate to the
|
|
21
|
+
* top-level boot-error handler.
|
|
22
|
+
*/
|
|
23
|
+
import type { ServiceInstallConfig } from "@teamscala/db/registry/loader/load-service-install-row";
|
|
24
|
+
export interface BootHookContext {
|
|
25
|
+
serviceSlug: string;
|
|
26
|
+
config: ServiceInstallConfig;
|
|
27
|
+
}
|
|
28
|
+
export type BootHookHandler = (ctx: BootHookContext) => Promise<void> | void;
|
|
29
|
+
export declare function runBootHooks(serviceSlug: string, config: ServiceInstallConfig): Promise<{
|
|
30
|
+
ran: string[];
|
|
31
|
+
}>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system server-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* Polls the installed scala-tools package against GitHub Packages and repairs
|
|
7
|
+
* stale daemon tooling by reinstalling and letting PM2 restart the process.
|
|
8
|
+
*/
|
|
9
|
+
export declare function startScalaToolsCurrencyWatchdog(): void;
|
|
10
|
+
export declare function stopScalaToolsCurrencyWatchdog(): void;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system server-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* Middleware that injects security headers into every response.
|
|
7
|
+
* Uses Hono's secureHeaders as the base for standard headers (X-Content-Type-Options,
|
|
8
|
+
* X-Frame-Options, Referrer-Policy, etc.), then layers registry-driven CSP with
|
|
9
|
+
* nonce injection on top.
|
|
10
|
+
*/
|
|
11
|
+
import type { Context, Next } from "hono";
|
|
12
|
+
export declare function injectSecurityHeaders(c: Context, next: Next): Promise<void>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system runtime-serve
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* Listener primitives. Provides the Bun.serve production listener (serveStart)
|
|
7
|
+
* and the WebSocket upgrade helper (upgradeWebSocket) that work identically
|
|
8
|
+
* dev and prod.
|
|
9
|
+
*/
|
|
10
|
+
import type { Hono } from "hono";
|
|
11
|
+
/**
|
|
12
|
+
* Hono `upgradeWebSocket` helper bound to the same `createBunWebSocket()`
|
|
13
|
+
* pair that drives Bun.serve.websocket. Use exclusively from this package so
|
|
14
|
+
* the upgrade path and the message dispatch share state.
|
|
15
|
+
*/
|
|
16
|
+
export declare const upgradeWebSocket: import("hono/ws").UpgradeWebSocket<unknown, any, import("hono/ws").WSEvents<unknown>>;
|
|
17
|
+
export interface ServeOptions {
|
|
18
|
+
/** TCP port to bind. */
|
|
19
|
+
port: number;
|
|
20
|
+
/** For logging only. */
|
|
21
|
+
serviceSlug: string;
|
|
22
|
+
/**
|
|
23
|
+
* Optional logger; defaults to console.
|
|
24
|
+
*/
|
|
25
|
+
logger?: {
|
|
26
|
+
info(msg: string): void;
|
|
27
|
+
warn(msg: string): void;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export interface ServeHandle {
|
|
31
|
+
server: ReturnType<typeof Bun.serve>;
|
|
32
|
+
stop(): void;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Start Bun.serve listening on the configured port. Used by:
|
|
36
|
+
* - bootUiService when running the production bundle (`bun dist/server/index.js`)
|
|
37
|
+
* - bootBackendService for non-UI services
|
|
38
|
+
*
|
|
39
|
+
* UI services in dev mode also call this — they run `bun src/index.ts`
|
|
40
|
+
* directly under PM2 with the same Bun.serve listener.
|
|
41
|
+
*
|
|
42
|
+
* Two-phase boot variant: when useMutableRef is true, `app` is used as the
|
|
43
|
+
* initial app for the mutable reference (minimal tool-only surface), and the
|
|
44
|
+
* caller can swap to the full app later via swapApp().
|
|
45
|
+
*/
|
|
46
|
+
export declare function serveStart(app: Hono, options: ServeOptions, useMutableRef?: boolean): ServeHandle;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system service-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* The branded HTML error page for a FAILED PAGE RENDER — the page-server
|
|
7
|
+
* render catch's response surface (constitution `error-responses-via-error-
|
|
8
|
+
* report`). When SSR resolve+render fails (render budget exhausted, DEGRADED
|
|
9
|
+
* protective layer, a thrown loader), the browser is mid-navigation: serving
|
|
10
|
+
* a JSON body there is the violation this closes (2026-07-24: the operator
|
|
11
|
+
* saw a raw `{"error":"Request failed"}` blob where the dashboard should be).
|
|
12
|
+
*
|
|
13
|
+
* Classification stays in `buildErrorReport` (`@teamscala/os`) — the ONE
|
|
14
|
+
* classifier; this file is ONLY the HTML shaper for the page-navigation
|
|
15
|
+
* surface (API/ORPC `onError` keeps the JSON body, consumed by code). It
|
|
16
|
+
* renders the report's already-decided status + Retry-After + message, so the
|
|
17
|
+
* screen, the log line, and the GlitchTip event all share one `errorId`.
|
|
18
|
+
*
|
|
19
|
+
* Deliberately NOT a Puck page: it must render even when data loading is what
|
|
20
|
+
* failed. Sibling of `resource-not-found.ts` (the missing-resource floor) —
|
|
21
|
+
* same dark branded template + org-logo resolver, distinct trigger.
|
|
22
|
+
*/
|
|
23
|
+
import type { ErrorReport } from "@teamscala/os/errors/error-report";
|
|
24
|
+
/**
|
|
25
|
+
* Render an `ErrorReport` as a branded HTML page. Pure shaper — the caller
|
|
26
|
+
* classifies via `buildErrorReport` and resolves the logo first. Skew-tolerant
|
|
27
|
+
* by construction: a missing logo degrades to a plain branded dark page.
|
|
28
|
+
*/
|
|
29
|
+
export declare function buildServerErrorPageResponse(report: ErrorReport, logoUrl?: string | null): Response;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system server-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* Starts `service_install.scheduled_jobs[]`. Two kinds:
|
|
7
|
+
*
|
|
8
|
+
* - `long_running`: handler invoked once at boot, runs for the lifetime of
|
|
9
|
+
* the process (e.g. SSE subscribers, WAL watchers). Failures are logged.
|
|
10
|
+
* - `recurring`: declared here but cron scheduling lands via pg-boss in a
|
|
11
|
+
* follow-up — currently a no-op log.
|
|
12
|
+
*
|
|
13
|
+
* Implementations are registered by server_packages via
|
|
14
|
+
* ctx.registerScheduledJob during the mount phase.
|
|
15
|
+
*/
|
|
16
|
+
import type { ServiceInstallConfig } from "@teamscala/db/registry/loader/load-service-install-row";
|
|
17
|
+
import type { MountRegistry } from "@teamscala/os/runtime-contracts/mount-context";
|
|
18
|
+
export declare function startScheduledJobs(config: ServiceInstallConfig, mountRegistry: MountRegistry, serviceSlug: string): void;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system service-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* Two-phase boot for UI services. Splits the boot process into:
|
|
7
|
+
* 1. Minimal app (tool-only surface): ORPC, data routes, auth gates
|
|
8
|
+
* 2. Full app (with pages and hydration): adds block discovery, page routes,
|
|
9
|
+
* client asset building, boot hooks, scheduled jobs
|
|
10
|
+
*
|
|
11
|
+
* The minimal app starts serving immediately (<10s), then the full app is
|
|
12
|
+
* built in the background and atomically swapped in. This eliminates the
|
|
13
|
+
* ~25s channel outage window during scala-agents restart.
|
|
14
|
+
*/
|
|
15
|
+
import { Hono } from "hono";
|
|
16
|
+
import type { ServiceInstallConfig } from "./load-service-install-row.ts";
|
|
17
|
+
import { mountServerPackages } from "./mount-server-packages.ts";
|
|
18
|
+
import type { PageServerDeps } from "./ui-service.ts";
|
|
19
|
+
import type { CreatePageHandler } from "./types.ts";
|
|
20
|
+
/**
|
|
21
|
+
* Set the real page handler (called when Phase 2 completes).
|
|
22
|
+
*/
|
|
23
|
+
export declare function setPageHandler(handler: CreatePageHandler): void;
|
|
24
|
+
/**
|
|
25
|
+
* Get the current page handler state.
|
|
26
|
+
*/
|
|
27
|
+
export declare function getPageHandlerState(): {
|
|
28
|
+
ready: boolean;
|
|
29
|
+
hasHandler: boolean;
|
|
30
|
+
};
|
|
31
|
+
export interface MinimalAppResult {
|
|
32
|
+
app: Hono;
|
|
33
|
+
mountRegistry: Awaited<ReturnType<typeof mountServerPackages>>;
|
|
34
|
+
apiRouteCount: number;
|
|
35
|
+
wsRouteCount: number;
|
|
36
|
+
pageRouteCount: number;
|
|
37
|
+
config: ServiceInstallConfig;
|
|
38
|
+
serviceSlug: string;
|
|
39
|
+
databaseUrl: string;
|
|
40
|
+
blockPackages: string[];
|
|
41
|
+
components: Record<string, unknown>;
|
|
42
|
+
loaded: string[];
|
|
43
|
+
skipped: string[];
|
|
44
|
+
}
|
|
45
|
+
export interface FullAppResult {
|
|
46
|
+
app: Hono;
|
|
47
|
+
blocks: {
|
|
48
|
+
slug: string;
|
|
49
|
+
packagePath: string;
|
|
50
|
+
}[];
|
|
51
|
+
routes: string[];
|
|
52
|
+
pgBossJobs: Array<{
|
|
53
|
+
handlerSlug: string;
|
|
54
|
+
}>;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Build the minimal tool-only app. This excludes the expensive operations:
|
|
58
|
+
* - Client asset building (Vite build)
|
|
59
|
+
* - Boot hooks (may have expensive initialization)
|
|
60
|
+
* - Scheduled jobs (can start later)
|
|
61
|
+
* - pg-boss (can start later)
|
|
62
|
+
*
|
|
63
|
+
* CRITICAL: All route registration MUST happen here before serving, because
|
|
64
|
+
* Hono's RegExpRouter compiles on the first request and becomes immutable.
|
|
65
|
+
* Once compiled, no new routes can be added. So we include:
|
|
66
|
+
* - Block discovery and page routes (registers all page paths)
|
|
67
|
+
* - All other routes (API, WS, data, static assets)
|
|
68
|
+
*
|
|
69
|
+
* The minimal app includes everything needed for MCP tools and ORPC:
|
|
70
|
+
* - Error handling, readiness gate, health check
|
|
71
|
+
* - Security headers, CORS, compression, timing
|
|
72
|
+
* - Content gate access, auth gates, static assets
|
|
73
|
+
* - ORPC router, API routes, WS routes, data routes, page routes
|
|
74
|
+
*/
|
|
75
|
+
export declare function buildMinimalApp(config: ServiceInstallConfig, serviceSlug: string, databaseUrl: string): Promise<MinimalAppResult>;
|
|
76
|
+
/**
|
|
77
|
+
* Build the full app with hydration. This adds the expensive operations that
|
|
78
|
+
* were deferred from the minimal app:
|
|
79
|
+
* - Client asset building (Vite build)
|
|
80
|
+
* - Boot hooks (may have expensive initialization)
|
|
81
|
+
* - Scheduled jobs (can start later)
|
|
82
|
+
* - pg-boss (can start later)
|
|
83
|
+
*
|
|
84
|
+
* This runs in the background after the minimal app is already serving.
|
|
85
|
+
* NOTE: All routes (including page routes) are already registered in Phase 1,
|
|
86
|
+
* so we don't need to modify the app here. The app reference stays the same.
|
|
87
|
+
*/
|
|
88
|
+
export declare function buildFullApp(minimalResult: MinimalAppResult, deps: PageServerDeps): Promise<FullAppResult>;
|
|
89
|
+
/**
|
|
90
|
+
* Swap the minimal app for the full app. This atomically replaces the
|
|
91
|
+
* app reference that Bun.serve is using.
|
|
92
|
+
*/
|
|
93
|
+
export declare function swapToFullApp(fullApp: Hono, serviceSlug: string): void;
|
package/src/types.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system service-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* Shared type definitions for service-boot boot helpers.
|
|
7
|
+
*/
|
|
8
|
+
type RendererComponent = (props: Record<string, unknown>) => unknown;
|
|
9
|
+
export interface RendererComponentEntry {
|
|
10
|
+
render: RendererComponent;
|
|
11
|
+
fields?: Record<string, {
|
|
12
|
+
type: string;
|
|
13
|
+
}>;
|
|
14
|
+
/** block_schema.defaults, carried onto the entry by discoverBlocks so
|
|
15
|
+
* buildPuckConfig merges them under authored props at SSR
|
|
16
|
+
* (blocks-fully-programmable). */
|
|
17
|
+
defaultProps?: Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
export interface PageHandlerExtras {
|
|
20
|
+
htmlTemplate: string;
|
|
21
|
+
userId?: string;
|
|
22
|
+
orgId?: string;
|
|
23
|
+
searchParams?: Record<string, string>;
|
|
24
|
+
nonce?: string;
|
|
25
|
+
}
|
|
26
|
+
export type CreatePageHandler = (config: PageHandlerConfig) => (request: Request, extras: PageHandlerExtras) => Promise<Response>;
|
|
27
|
+
export interface PageHandlerConfig {
|
|
28
|
+
components: Record<string, RendererComponentEntry>;
|
|
29
|
+
providers?: string[];
|
|
30
|
+
loadData: (request: Request) => Promise<unknown>;
|
|
31
|
+
renderMode?: "public" | "portals" | "utility";
|
|
32
|
+
/**
|
|
33
|
+
* Owning service slug — used by the page-server render catch to resolve
|
|
34
|
+
* the branded logo on the render-error page. Mirrors service-runtime's
|
|
35
|
+
* PageHandlerConfig.
|
|
36
|
+
*/
|
|
37
|
+
serviceSlug?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Redirect a signed-in user's `/` to their org-scoped portal dashboard.
|
|
40
|
+
* Set only for services serving a `/*` catchall page_route (the portal);
|
|
41
|
+
* single-site portal-mode services serve their real page at `/`.
|
|
42
|
+
* Mirrors service-runtime's PageHandlerConfig (1.15.82).
|
|
43
|
+
*/
|
|
44
|
+
portalHomeRedirect?: boolean;
|
|
45
|
+
}
|
|
46
|
+
export type { ServiceInstallConfig } from "@teamscala/db/registry/loader/load-service-install-row";
|