@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.
Files changed (138) hide show
  1. package/package.json +1016 -0
  2. package/src/agreement-gate.test.ts +118 -0
  3. package/src/agreement-gate.ts +887 -0
  4. package/src/asset-cache-key.ts +59 -0
  5. package/src/asset-optimize.ts +93 -0
  6. package/src/asset-proxy.ts +148 -0
  7. package/src/asset-social-card.ts +58 -0
  8. package/src/attach-agreement-magic.ts +62 -0
  9. package/src/attach-portal-members.ts +107 -0
  10. package/src/attach-proposal-accept.ts +123 -0
  11. package/src/attach-proposal-magic.ts +55 -0
  12. package/src/attach-transfers-upload.ts +100 -0
  13. package/src/auth-gate.ts +385 -0
  14. package/src/bind-page-handler.ts +223 -0
  15. package/src/build-client-assets/input-hashing.ts +206 -0
  16. package/src/build-client-assets/source-candidates.ts +115 -0
  17. package/src/build-client-assets.test.ts +216 -0
  18. package/src/build-client-assets.ts +962 -0
  19. package/src/compose-page-title.test.ts +88 -0
  20. package/src/compose-page-title.ts +73 -0
  21. package/src/content-gate/access.ts +89 -0
  22. package/src/content-gate/config-types.ts +108 -0
  23. package/src/content-gate/gate.ts +74 -0
  24. package/src/content-gate/parse-cookie.ts +23 -0
  25. package/src/content-gate/token.test.ts +70 -0
  26. package/src/content-gate/token.ts +148 -0
  27. package/src/content-page-floor.test.ts +127 -0
  28. package/src/content-page-floor.ts +70 -0
  29. package/src/discover-blocks.ts +228 -0
  30. package/src/expand-roster.ts +197 -0
  31. package/src/fn-routers/agreement-completion-email.ts +115 -0
  32. package/src/fn-routers/agreement-send-router.ts +303 -0
  33. package/src/fn-routers/ai-health-router.ts +64 -0
  34. package/src/fn-routers/auth-router.ts +131 -0
  35. package/src/fn-routers/cache-admin-router.ts +50 -0
  36. package/src/fn-routers/chat-session-router.ts +455 -0
  37. package/src/fn-routers/email-defaults.ts +22 -0
  38. package/src/fn-routers/email-link-ttl.ts +23 -0
  39. package/src/fn-routers/index.ts +70 -0
  40. package/src/fn-routers/panel-data-router.ts +93 -0
  41. package/src/fn-routers/persist-chat-messages.test.ts +86 -0
  42. package/src/fn-routers/persist-chat-messages.ts +96 -0
  43. package/src/fn-routers/plans-router.ts +141 -0
  44. package/src/fn-routers/proposal-send-router.ts +207 -0
  45. package/src/fn-routers/public-agreement-router.ts +865 -0
  46. package/src/fn-routers/public-assessment-router.ts +421 -0
  47. package/src/fn-routers/public-gpt-session-router.ts +373 -0
  48. package/src/fn-routers/sign-in-flow-router.ts +196 -0
  49. package/src/fn-routers/task-sync-bind.test.ts +105 -0
  50. package/src/fn-routers/task-sync-classify.test.ts +109 -0
  51. package/src/fn-routers/task-sync-dispatch.test.ts +44 -0
  52. package/src/fn-routers/task-sync-dispatch.ts +124 -0
  53. package/src/fn-routers/task-sync-router.test.ts +279 -0
  54. package/src/fn-routers/task-sync-router.ts +1036 -0
  55. package/src/fn-routers/tasks-router.ts +77 -0
  56. package/src/fn-routers/user-preferences-router.ts +139 -0
  57. package/src/fn-routers/work-items-router.ts +341 -0
  58. package/src/gate-ttls.ts +56 -0
  59. package/src/gpt-gate.ts +76 -0
  60. package/src/handlers/bun-stable-watch.ts +89 -0
  61. package/src/handlers/projects-digest.ts +198 -0
  62. package/src/handlers/prune-stale-data.ts +110 -0
  63. package/src/handlers/registry-stale-inactive-sweep.ts +82 -0
  64. package/src/handlers/scala-tools-currency-watch.test.ts +60 -0
  65. package/src/handlers/scala-tools-currency-watch.ts +171 -0
  66. package/src/handlers/scheduled-browser-tests.ts +85 -0
  67. package/src/handlers/sweep-event-log.ts +98 -0
  68. package/src/handlers/sweep-host-cache.ts +211 -0
  69. package/src/handlers/upstream-issue-sync.ts +210 -0
  70. package/src/handlers/whatsapp-liveness-handler.test.ts +59 -0
  71. package/src/handlers/whatsapp-liveness-logic.ts +53 -0
  72. package/src/handlers/whatsapp-liveness.test.ts +78 -0
  73. package/src/handlers/whatsapp-liveness.ts +125 -0
  74. package/src/hono-error-handler.ts +55 -0
  75. package/src/install-harness.ts +80 -0
  76. package/src/install-logging.ts +182 -0
  77. package/src/load-service-install-row.ts +98 -0
  78. package/src/load-service-prisma-client.ts +53 -0
  79. package/src/mount-orpc-router.ts +148 -0
  80. package/src/mount-server-packages.ts +155 -0
  81. package/src/mount.ts +18 -0
  82. package/src/mutable-app-ref.ts +68 -0
  83. package/src/page-route-shell-types.ts +16 -0
  84. package/src/page-server-orpc/audit-revert-router.ts +102 -0
  85. package/src/page-server-orpc/hub-mcp-client.ts +20 -0
  86. package/src/page-server-orpc/portal-diagnostics-router.ts +170 -0
  87. package/src/page-server-orpc/puck-pages-router.ts +272 -0
  88. package/src/page-server-orpc/server-client.ts +31 -0
  89. package/src/page-server-orpc/templates-router.ts +116 -0
  90. package/src/page-server-orpc/user-preferences-router.ts +132 -0
  91. package/src/pg-boss-handler-registry.ts +36 -0
  92. package/src/pg-boss-mount.ts +295 -0
  93. package/src/proposal-authorized-emails.ts +65 -0
  94. package/src/proposal-chat-inject.ts +141 -0
  95. package/src/proposal-gate.ts +175 -0
  96. package/src/read-html-template.test.ts +74 -0
  97. package/src/read-html-template.ts +102 -0
  98. package/src/read-viewport-from-request.ts +36 -0
  99. package/src/readiness-gate.ts +39 -0
  100. package/src/register-api-routes.ts +127 -0
  101. package/src/register-crash-handlers.ts +39 -0
  102. package/src/register-page-routes.ts +62 -0
  103. package/src/register-page-server-client.ts +109 -0
  104. package/src/register-registry-page-route/ai-chat-prefetch.ts +106 -0
  105. package/src/register-registry-page-route/graph-cache.test.ts +84 -0
  106. package/src/register-registry-page-route/graph-cache.ts +90 -0
  107. package/src/register-registry-page-route/meta-title.ts +73 -0
  108. package/src/register-registry-page-route/proposal-approval-email-token.ts +78 -0
  109. package/src/register-registry-page-route/proposal-brand-stamping.ts +60 -0
  110. package/src/register-registry-page-route/resolve-page-data.ts +518 -0
  111. package/src/register-registry-page-route.ts +765 -0
  112. package/src/register-static-assets.ts +180 -0
  113. package/src/register-ws-routes.ts +180 -0
  114. package/src/resolve-app-base-url.ts +28 -0
  115. package/src/resolve-brand-favicon.ts +65 -0
  116. package/src/resolve-from-loader-config.test.ts +44 -0
  117. package/src/resolve-from-loader-config.ts +291 -0
  118. package/src/resolve-service-context.ts +67 -0
  119. package/src/resolve-theme-mode.ts +61 -0
  120. package/src/resolvers/fetch-org-brand.ts +28 -0
  121. package/src/resolvers/portal-route-paths.test.ts +102 -0
  122. package/src/resolvers/portal-route-paths.ts +83 -0
  123. package/src/resolvers/website-by-host-map.ts +49 -0
  124. package/src/resource-chat-inject.ts +168 -0
  125. package/src/resource-not-found.ts +263 -0
  126. package/src/run-boot-hook-list.ts +75 -0
  127. package/src/run-boot-hooks.ts +97 -0
  128. package/src/scala-tools-currency-watchdog.ts +160 -0
  129. package/src/security-headers.ts +59 -0
  130. package/src/serve/serve-ws.test.ts +135 -0
  131. package/src/serve/serve.ts +190 -0
  132. package/src/server-error-page.test.ts +102 -0
  133. package/src/server-error-page.ts +95 -0
  134. package/src/start-scheduled-jobs.ts +84 -0
  135. package/src/two-phase-boot.ts +449 -0
  136. package/src/types.ts +52 -0
  137. package/src/ui-service.ts +171 -0
  138. 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
+ }