@teamscala/service-boot 0.23.284
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 +1016 -0
- package/src/agreement-gate.test.ts +118 -0
- package/src/agreement-gate.ts +887 -0
- package/src/asset-cache-key.ts +59 -0
- package/src/asset-optimize.ts +93 -0
- package/src/asset-proxy.ts +148 -0
- package/src/asset-social-card.ts +58 -0
- package/src/attach-agreement-magic.ts +62 -0
- package/src/attach-portal-members.ts +107 -0
- package/src/attach-proposal-accept.ts +123 -0
- package/src/attach-proposal-magic.ts +55 -0
- package/src/attach-transfers-upload.ts +100 -0
- package/src/auth-gate.ts +385 -0
- package/src/bind-page-handler.ts +223 -0
- package/src/build-client-assets/input-hashing.ts +206 -0
- package/src/build-client-assets/source-candidates.ts +115 -0
- package/src/build-client-assets.test.ts +216 -0
- package/src/build-client-assets.ts +962 -0
- package/src/compose-page-title.test.ts +88 -0
- package/src/compose-page-title.ts +73 -0
- package/src/content-gate/access.ts +89 -0
- package/src/content-gate/config-types.ts +108 -0
- package/src/content-gate/gate.ts +74 -0
- package/src/content-gate/parse-cookie.ts +23 -0
- package/src/content-gate/token.test.ts +70 -0
- package/src/content-gate/token.ts +148 -0
- package/src/content-page-floor.test.ts +127 -0
- package/src/content-page-floor.ts +70 -0
- package/src/discover-blocks.ts +228 -0
- package/src/expand-roster.ts +197 -0
- package/src/fn-routers/agreement-completion-email.ts +115 -0
- package/src/fn-routers/agreement-send-router.ts +303 -0
- package/src/fn-routers/ai-health-router.ts +64 -0
- package/src/fn-routers/auth-router.ts +131 -0
- package/src/fn-routers/cache-admin-router.ts +50 -0
- package/src/fn-routers/chat-session-router.ts +455 -0
- package/src/fn-routers/email-defaults.ts +22 -0
- package/src/fn-routers/email-link-ttl.ts +23 -0
- package/src/fn-routers/index.ts +70 -0
- package/src/fn-routers/panel-data-router.ts +93 -0
- package/src/fn-routers/persist-chat-messages.test.ts +86 -0
- package/src/fn-routers/persist-chat-messages.ts +96 -0
- package/src/fn-routers/plans-router.ts +141 -0
- package/src/fn-routers/proposal-send-router.ts +207 -0
- package/src/fn-routers/public-agreement-router.ts +865 -0
- package/src/fn-routers/public-assessment-router.ts +421 -0
- package/src/fn-routers/public-gpt-session-router.ts +373 -0
- package/src/fn-routers/sign-in-flow-router.ts +196 -0
- package/src/fn-routers/task-sync-bind.test.ts +105 -0
- package/src/fn-routers/task-sync-classify.test.ts +109 -0
- package/src/fn-routers/task-sync-dispatch.test.ts +44 -0
- package/src/fn-routers/task-sync-dispatch.ts +124 -0
- package/src/fn-routers/task-sync-router.test.ts +279 -0
- package/src/fn-routers/task-sync-router.ts +1036 -0
- package/src/fn-routers/tasks-router.ts +77 -0
- package/src/fn-routers/user-preferences-router.ts +139 -0
- package/src/fn-routers/work-items-router.ts +341 -0
- package/src/gate-ttls.ts +56 -0
- package/src/gpt-gate.ts +76 -0
- package/src/handlers/bun-stable-watch.ts +89 -0
- package/src/handlers/projects-digest.ts +198 -0
- package/src/handlers/prune-stale-data.ts +110 -0
- package/src/handlers/registry-stale-inactive-sweep.ts +82 -0
- package/src/handlers/scala-tools-currency-watch.test.ts +60 -0
- package/src/handlers/scala-tools-currency-watch.ts +171 -0
- package/src/handlers/scheduled-browser-tests.ts +85 -0
- package/src/handlers/sweep-event-log.ts +98 -0
- package/src/handlers/sweep-host-cache.ts +211 -0
- package/src/handlers/upstream-issue-sync.ts +210 -0
- package/src/handlers/whatsapp-liveness-handler.test.ts +59 -0
- package/src/handlers/whatsapp-liveness-logic.ts +53 -0
- package/src/handlers/whatsapp-liveness.test.ts +78 -0
- package/src/handlers/whatsapp-liveness.ts +125 -0
- package/src/hono-error-handler.ts +55 -0
- package/src/install-harness.ts +80 -0
- package/src/install-logging.ts +182 -0
- package/src/load-service-install-row.ts +98 -0
- package/src/load-service-prisma-client.ts +53 -0
- package/src/mount-orpc-router.ts +148 -0
- package/src/mount-server-packages.ts +155 -0
- package/src/mount.ts +18 -0
- package/src/mutable-app-ref.ts +68 -0
- package/src/page-route-shell-types.ts +16 -0
- package/src/page-server-orpc/audit-revert-router.ts +102 -0
- package/src/page-server-orpc/hub-mcp-client.ts +20 -0
- package/src/page-server-orpc/portal-diagnostics-router.ts +170 -0
- package/src/page-server-orpc/puck-pages-router.ts +272 -0
- package/src/page-server-orpc/server-client.ts +31 -0
- package/src/page-server-orpc/templates-router.ts +116 -0
- package/src/page-server-orpc/user-preferences-router.ts +132 -0
- package/src/pg-boss-handler-registry.ts +36 -0
- package/src/pg-boss-mount.ts +295 -0
- package/src/proposal-authorized-emails.ts +65 -0
- package/src/proposal-chat-inject.ts +141 -0
- package/src/proposal-gate.ts +175 -0
- package/src/read-html-template.test.ts +74 -0
- package/src/read-html-template.ts +102 -0
- package/src/read-viewport-from-request.ts +36 -0
- package/src/readiness-gate.ts +39 -0
- package/src/register-api-routes.ts +127 -0
- package/src/register-crash-handlers.ts +39 -0
- package/src/register-page-routes.ts +62 -0
- package/src/register-page-server-client.ts +109 -0
- package/src/register-registry-page-route/ai-chat-prefetch.ts +106 -0
- package/src/register-registry-page-route/graph-cache.test.ts +84 -0
- package/src/register-registry-page-route/graph-cache.ts +90 -0
- package/src/register-registry-page-route/meta-title.ts +73 -0
- package/src/register-registry-page-route/proposal-approval-email-token.ts +78 -0
- package/src/register-registry-page-route/proposal-brand-stamping.ts +60 -0
- package/src/register-registry-page-route/resolve-page-data.ts +518 -0
- package/src/register-registry-page-route.ts +765 -0
- package/src/register-static-assets.ts +180 -0
- package/src/register-ws-routes.ts +180 -0
- package/src/resolve-app-base-url.ts +28 -0
- package/src/resolve-brand-favicon.ts +65 -0
- package/src/resolve-from-loader-config.test.ts +44 -0
- package/src/resolve-from-loader-config.ts +291 -0
- package/src/resolve-service-context.ts +67 -0
- package/src/resolve-theme-mode.ts +61 -0
- package/src/resolvers/fetch-org-brand.ts +28 -0
- package/src/resolvers/portal-route-paths.test.ts +102 -0
- package/src/resolvers/portal-route-paths.ts +83 -0
- package/src/resolvers/website-by-host-map.ts +49 -0
- package/src/resource-chat-inject.ts +168 -0
- package/src/resource-not-found.ts +263 -0
- package/src/run-boot-hook-list.ts +75 -0
- package/src/run-boot-hooks.ts +97 -0
- package/src/scala-tools-currency-watchdog.ts +160 -0
- package/src/security-headers.ts +59 -0
- package/src/serve/serve-ws.test.ts +135 -0
- package/src/serve/serve.ts +190 -0
- package/src/server-error-page.test.ts +102 -0
- package/src/server-error-page.ts +95 -0
- package/src/start-scheduled-jobs.ts +84 -0
- package/src/two-phase-boot.ts +449 -0
- package/src/types.ts +52 -0
- package/src/ui-service.ts +171 -0
- package/src/ws-boot-self-test.ts +114 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system ui-service-boot
|
|
3
|
+
* @status handwritten @indivisible-unit
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* Meta/title helpers for the route registrar: interpolate the route's
|
|
7
|
+
* meta_context against the resolved graph, and resolve a curated
|
|
8
|
+
* page_meta_template title. Extracted from register-registry-page-route.ts.
|
|
9
|
+
*/
|
|
10
|
+
import { interpolateMeta } from "../resolve-from-loader-config.ts";
|
|
11
|
+
import { getEntriesByType } from "@teamscala/db/registry/accessors";
|
|
12
|
+
|
|
13
|
+
export function buildMetaFromContext(
|
|
14
|
+
metaContext: Record<string, Record<string, string>> | undefined,
|
|
15
|
+
resolved: Record<string, unknown>,
|
|
16
|
+
): Record<string, unknown> {
|
|
17
|
+
if (!metaContext) return {};
|
|
18
|
+
return interpolateMeta(metaContext as Record<string, unknown>, resolved);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function resolveTemplateTitle(
|
|
22
|
+
service: string,
|
|
23
|
+
resolved: Record<string, unknown>,
|
|
24
|
+
): string | undefined {
|
|
25
|
+
const rows = getEntriesByType("page_meta_template") as Array<{
|
|
26
|
+
slug: string;
|
|
27
|
+
config: Record<string, unknown>;
|
|
28
|
+
}>;
|
|
29
|
+
const bySlug = new Map<string, Record<string, unknown>>();
|
|
30
|
+
for (const row of rows) {
|
|
31
|
+
bySlug.set(row.slug, row.config);
|
|
32
|
+
}
|
|
33
|
+
const merged = (bySlug.get("default") ?? null) as Record<string, unknown> | null;
|
|
34
|
+
const serviceTmpl = (bySlug.get(service) ?? null) as Record<string, unknown> | null;
|
|
35
|
+
const tmpl = serviceTmpl ?? merged;
|
|
36
|
+
if (!tmpl) return undefined;
|
|
37
|
+
const titleTemplate = tmpl.titleTemplate as string | undefined;
|
|
38
|
+
if (!titleTemplate) return undefined;
|
|
39
|
+
const brand = (resolved.brand as Record<string, unknown>) ?? {};
|
|
40
|
+
const website = (resolved.website as Record<string, unknown>) ?? {};
|
|
41
|
+
const page = (resolved.page as Record<string, unknown>) ?? {};
|
|
42
|
+
// The full placeholder set the curated resource templates use:
|
|
43
|
+
// {org.name}, {website.name}, {page.name}, {form.name}, {quiz.name},
|
|
44
|
+
// {proposal.name}, {agent.name}. Previously only {org.name} resolved, so
|
|
45
|
+
// every resource template ("{agent.name} — {org.name}", "{form.name} by
|
|
46
|
+
// {org.name}") shipped with a blank subject. Resolving the graph here is
|
|
47
|
+
// the executor half of composePageTitle (business-logic-is-rows-plus-
|
|
48
|
+
// executors): the template ROWS carry the shape, this carries the values.
|
|
49
|
+
const context: Record<string, unknown> = {
|
|
50
|
+
org: { name: (brand.companyName as string) || undefined },
|
|
51
|
+
website: { name: (website.name as string) || undefined },
|
|
52
|
+
page: { name: (page.name as string) || undefined },
|
|
53
|
+
form: { name: ((resolved.form as Record<string, unknown>)?.name as string) || undefined },
|
|
54
|
+
quiz: { name: ((resolved.quiz as Record<string, unknown>)?.name as string) || undefined },
|
|
55
|
+
proposal: {
|
|
56
|
+
name: ((resolved.proposal as Record<string, unknown>)?.name as string) || undefined,
|
|
57
|
+
},
|
|
58
|
+
agent: { name: ((resolved.agent as Record<string, unknown>)?.name as string) || undefined },
|
|
59
|
+
};
|
|
60
|
+
const result = titleTemplate.replace(/\{([^}]+)\}/g, (_m, path: string) => {
|
|
61
|
+
const keys = path.trim().split(".");
|
|
62
|
+
let cur: unknown = context;
|
|
63
|
+
for (const k of keys) {
|
|
64
|
+
if (cur && typeof cur === "object") {
|
|
65
|
+
cur = (cur as Record<string, unknown>)[k];
|
|
66
|
+
} else {
|
|
67
|
+
return "";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return cur === null || cur === undefined ? "" : String(cur);
|
|
71
|
+
});
|
|
72
|
+
return result || undefined;
|
|
73
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system ui-service-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* Resolve the `{{approvalEmail}}` token on a proposal page, after pageData is
|
|
7
|
+
* built.
|
|
8
|
+
*
|
|
9
|
+
* Split from proposal-post-processors.ts, which single-purpose-file correctly
|
|
10
|
+
* flagged as two unrelated export groups. This unit is an async ORPC lookup
|
|
11
|
+
* that rewrites button props; its former co-tenants are synchronous brand
|
|
12
|
+
* stamping that touch no network. Sharing a file made an I/O-bound resolver
|
|
13
|
+
* look like a pure formatter.
|
|
14
|
+
*/
|
|
15
|
+
import { getServerClient } from "../page-server-orpc/server-client.ts";
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the `{{approvalEmail}}` placeholder in a proposal page's button props
|
|
20
|
+
* (e.g. the Approve CTA) in place. Routing: the organisation's registered
|
|
21
|
+
* company email FIRST (organisation_profile.contact_email, already resolved
|
|
22
|
+
* into pageData.brand.contactEmail); the proposal creator's user email only as
|
|
23
|
+
* a fallback when the org has none — so a cloned proposal can never carry a
|
|
24
|
+
* stale hard-coded address. The creator lookup is null-safe and adds no user
|
|
25
|
+
* exposure: it resolves only where the in-process ORPC client exposes `user`
|
|
26
|
+
* (super-admin), degrading to the org email elsewhere. Called only for proposal
|
|
27
|
+
* pages; other registry pages are untouched.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
export async function applyApprovalEmailToken(
|
|
31
|
+
pageData: Record<string, unknown>,
|
|
32
|
+
proposal: { created_by?: string } | undefined,
|
|
33
|
+
): Promise<void> {
|
|
34
|
+
const brand = pageData.brand as { contactEmail?: string | null } | undefined;
|
|
35
|
+
let approvalEmail: string | null = brand?.contactEmail ?? null;
|
|
36
|
+
const createdBy = proposal?.created_by;
|
|
37
|
+
if (!approvalEmail && createdBy && createdBy !== "system") {
|
|
38
|
+
try {
|
|
39
|
+
const client = getServerClient() as {
|
|
40
|
+
user?: {
|
|
41
|
+
findFirst?: (
|
|
42
|
+
a: unknown,
|
|
43
|
+
) => Promise<{ email?: string | null } | null>;
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
const creator = await client.user?.findFirst?.({
|
|
47
|
+
where: { id: createdBy },
|
|
48
|
+
select: { email: true },
|
|
49
|
+
});
|
|
50
|
+
if (creator?.email) approvalEmail = creator.email;
|
|
51
|
+
} catch {}
|
|
52
|
+
}
|
|
53
|
+
if (!approvalEmail) return;
|
|
54
|
+
const email = approvalEmail;
|
|
55
|
+
const walk = (value: unknown): unknown => {
|
|
56
|
+
if (typeof value === "string")
|
|
57
|
+
return value.split("{{approvalEmail}}").join(email);
|
|
58
|
+
if (Array.isArray(value)) return value.map(walk);
|
|
59
|
+
if (value && typeof value === "object") {
|
|
60
|
+
const obj = value as Record<string, unknown>;
|
|
61
|
+
for (const key of Object.keys(obj)) obj[key] = walk(obj[key]);
|
|
62
|
+
return obj;
|
|
63
|
+
}
|
|
64
|
+
return value;
|
|
65
|
+
};
|
|
66
|
+
const regions = pageData.regions as
|
|
67
|
+
| { puckData?: { content?: unknown[] } }[]
|
|
68
|
+
| undefined;
|
|
69
|
+
if (Array.isArray(regions)) {
|
|
70
|
+
for (const region of regions) {
|
|
71
|
+
const content = region?.puckData?.content;
|
|
72
|
+
if (Array.isArray(content)) for (const block of content) walk(block);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const single = (pageData.puckData as { content?: unknown[] } | undefined)
|
|
76
|
+
?.content;
|
|
77
|
+
if (Array.isArray(single)) for (const block of single) walk(block);
|
|
78
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system ui-service-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* Stamp partner and client brand assets onto a proposal page's pageData, after
|
|
7
|
+
* it is built.
|
|
8
|
+
*
|
|
9
|
+
* Split from proposal-post-processors.ts (single-purpose-file: two unrelated
|
|
10
|
+
* export groups). Both helpers here are synchronous, take an already-resolved
|
|
11
|
+
* loader-graph org, and only map a logo/colour onto pageData — no network, no
|
|
12
|
+
* ORPC. The approval-email token resolver they used to share a file with is an
|
|
13
|
+
* async ORPC lookup, which is the distinction that was invisible while they sat
|
|
14
|
+
* together.
|
|
15
|
+
*/
|
|
16
|
+
import { toAssetPath } from "@teamscala/formatters/asset-url";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Stash a proposal's connected partner logo/colour on `pageData` from the
|
|
20
|
+
* `partnerOrg` loader source (`crm_organisations` loaded by the proposal's
|
|
21
|
+
* `partner_id`, via the SAME proven ORPC loader path that loads the proposal
|
|
22
|
+
* itself). resolve-page-brand (service-runtime) prefers `pageData.partnerLogo`
|
|
23
|
+
* over any inline body-Hero prop and fans it into the shared `/header` so a
|
|
24
|
+
* `source: "partner"` OrgLogo fills automatically — the partner logo lives on
|
|
25
|
+
* the CRM record, never duplicated per proposal. No partner → partnerOrg is
|
|
26
|
+
* null → nothing set → the partner logo block renders blank. Reading the
|
|
27
|
+
* already-resolved loader value (not a second hand-rolled getServerClient call)
|
|
28
|
+
* is what makes this reliable: the earlier direct `crm_organisations.findFirst`
|
|
29
|
+
* with a Prisma-style object `select` resolved intermittently/never against the
|
|
30
|
+
* generated ORPC, leaving the header partner blank (2026-06-12).
|
|
31
|
+
*/
|
|
32
|
+
export function applyPartnerBrand(
|
|
33
|
+
pageData: Record<string, unknown>,
|
|
34
|
+
partnerOrg:
|
|
35
|
+
| { logo_url?: string | null; brand_primary_color?: string | null }
|
|
36
|
+
| undefined
|
|
37
|
+
| null,
|
|
38
|
+
): void {
|
|
39
|
+
if (!partnerOrg) return;
|
|
40
|
+
if (partnerOrg.logo_url) pageData.partnerLogo = toAssetPath(partnerOrg.logo_url);
|
|
41
|
+
if (partnerOrg.brand_primary_color)
|
|
42
|
+
pageData.partnerBrandColor = partnerOrg.brand_primary_color;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Auto-resolve the client's company logo from the `clientOrg` loader source
|
|
47
|
+
* (contact_id → company_id → crm_organisations.logo_url). Injects into the
|
|
48
|
+
* Hero block's clientLogo prop when it's empty — no manual logo URL needed
|
|
49
|
+
* per proposal when the contact's company profile has a logo.
|
|
50
|
+
*/
|
|
51
|
+
export function applyClientLogo(
|
|
52
|
+
pageData: Record<string, unknown>,
|
|
53
|
+
clientOrg:
|
|
54
|
+
| { logo_url?: string | null; name?: string | null }
|
|
55
|
+
| undefined
|
|
56
|
+
| null,
|
|
57
|
+
): void {
|
|
58
|
+
if (!clientOrg?.logo_url) return;
|
|
59
|
+
pageData.clientLogo = toAssetPath(clientOrg.logo_url);
|
|
60
|
+
}
|