@teamscala/service-boot 0.23.523 → 0.23.525

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/package.json +1 -9
  2. package/src/agreement-gate/agreement-lookup.ts +0 -7
  3. package/src/agreement-gate/authorized-emails.ts +0 -14
  4. package/src/agreement-gate/placeholder-strip.ts +0 -10
  5. package/src/agreement-gate/resolution-caches.ts +0 -5
  6. package/src/agreement-gate/settings.ts +0 -22
  7. package/src/agreement-gate/signing-session.ts +0 -8
  8. package/src/agreement-gate/signing-shell.ts +0 -9
  9. package/src/agreement-gate.ts +0 -52
  10. package/src/apply-logger-config.ts +1 -9
  11. package/src/asset-cache-key.ts +1 -5
  12. package/src/asset-optimize.ts +1 -7
  13. package/src/asset-proxy.ts +1 -10
  14. package/src/asset-social-card.ts +1 -9
  15. package/src/attach-agreement-magic.ts +0 -14
  16. package/src/attach-portal-members.ts +0 -17
  17. package/src/attach-proposal-accept.ts +1 -21
  18. package/src/attach-proposal-magic.ts +1 -13
  19. package/src/attach-transfers-upload.ts +0 -21
  20. package/src/auth/sign-out.ts +1 -44
  21. package/src/auth-gate/public-paths.ts +1 -96
  22. package/src/auth-gate.ts +1 -41
  23. package/src/bind-page-handler.ts +1 -83
  24. package/src/build-client-assets/build-lock.ts +1 -11
  25. package/src/build-client-assets/hashed-output.ts +1 -11
  26. package/src/build-client-assets/hydrate-bundle.ts +1 -137
  27. package/src/build-client-assets/manifest.ts +1 -30
  28. package/src/build-client-assets/source-candidates.ts +0 -51
  29. package/src/build-client-assets/sourcemaps-upload.ts +1 -52
  30. package/src/build-client-assets/styles-build.ts +1 -71
  31. package/src/build-client-assets.ts +1 -30
  32. package/src/capability-slug.ts +1 -45
  33. package/src/compose-page-title.ts +1 -34
  34. package/src/content-gate/access.ts +0 -34
  35. package/src/content-gate/catalog.ts +0 -52
  36. package/src/content-gate/config-types.ts +3 -90
  37. package/src/content-gate/gate-entry-clears.ts +0 -18
  38. package/src/content-gate/gate.ts +0 -27
  39. package/src/content-gate/parse-cookie.ts +0 -4
  40. package/src/content-page-floor.ts +0 -28
  41. package/src/csrf-protection.ts +1 -28
  42. package/src/error-page-logo.ts +2 -35
  43. package/src/expand-roster.ts +1 -25
  44. package/src/gate-ttls.ts +0 -12
  45. package/src/gpt-gate.ts +1 -19
  46. package/src/hono-error-handler.ts +0 -19
  47. package/src/install-harness.ts +1 -12
  48. package/src/install-logging.ts +1 -47
  49. package/src/load-page-manifest.ts +1 -29
  50. package/src/load-service-install-row.ts +42 -10
  51. package/src/module-not-found.ts +0 -17
  52. package/src/mount-orpc-router.ts +1 -94
  53. package/src/mount-server-packages.ts +1 -37
  54. package/src/mount.ts +1 -4
  55. package/src/mutable-app-ref.ts +0 -21
  56. package/src/orpc-contract-rows.json +289 -0
  57. package/src/orpc-openapi-document.ts +1 -19
  58. package/src/orpc-openapi-gate.ts +1 -23
  59. package/src/page-route-shell-types.ts +1 -8
  60. package/src/page-server-loader-graph.ts +0 -10
  61. package/src/page-server-orpc/server-client.ts +1 -23
  62. package/src/pg-boss-handler-registry.ts +1 -24
  63. package/src/pg-boss-mount.ts +1 -109
  64. package/src/proposal-authorized-emails.ts +0 -32
  65. package/src/proposal-gate.ts +1 -52
  66. package/src/quiz-gate.ts +1 -31
  67. package/src/read-html-template.ts +1 -49
  68. package/src/readiness-gate.ts +0 -8
  69. package/src/register-api-routes.ts +1 -11
  70. package/src/register-crash-handlers.ts +1 -6
  71. package/src/register-page-routes.ts +1 -12
  72. package/src/register-page-server-client.ts +1 -66
  73. package/src/register-registry-page-route/ai-chat-prefetch.ts +1 -24
  74. package/src/register-registry-page-route/apply-page-branding.ts +1 -100
  75. package/src/register-registry-page-route/captured-loader-config.ts +1 -12
  76. package/src/register-registry-page-route/graph-cache.ts +1 -38
  77. package/src/register-registry-page-route/live-loader-config.ts +1 -22
  78. package/src/register-registry-page-route/meta-title.ts +1 -36
  79. package/src/register-registry-page-route/page-data-shared.ts +1 -18
  80. package/src/register-registry-page-route/prefetch-org-id.ts +1 -24
  81. package/src/register-registry-page-route/proposal-approval-email-token.ts +1 -22
  82. package/src/register-registry-page-route/proposal-brand-stamping.ts +1 -31
  83. package/src/register-registry-page-route/public-shell-page-data.ts +1 -34
  84. package/src/register-registry-page-route/region-slots-page-data.ts +1 -8
  85. package/src/register-registry-page-route/resolve-page-data.ts +1 -10
  86. package/src/register-registry-page-route/resolve-panel-page.ts +1 -10
  87. package/src/register-registry-page-route/resolve-request-graph.ts +1 -68
  88. package/src/register-registry-page-route/run-content-gates.ts +3 -64
  89. package/src/register-registry-page-route/single-page-data.ts +1 -9
  90. package/src/register-registry-page-route/stamp-session-context.ts +1 -10
  91. package/src/register-registry-page-route/warn-uncovered-blocks.ts +1 -18
  92. package/src/register-registry-page-route.ts +1 -93
  93. package/src/register-static-assets.ts +1 -53
  94. package/src/register-ws-routes.ts +1 -21
  95. package/src/resolve-brand-favicon.ts +0 -22
  96. package/src/resolve-service-port.ts +1 -61
  97. package/src/resolve-theme-mode.ts +1 -30
  98. package/src/resolvers/app-header.ts +1 -20
  99. package/src/resolvers/fetch-org-brand.ts +1 -11
  100. package/src/resolvers/portal-route-paths.ts +1 -29
  101. package/src/resolvers/website-by-host-map.ts +1 -7
  102. package/src/resource-not-found.ts +0 -48
  103. package/src/run-boot-hook-list.ts +1 -13
  104. package/src/run-boot-hooks.ts +1 -19
  105. package/src/scala-tools-currency-watchdog.ts +1 -7
  106. package/src/security-headers.ts +1 -7
  107. package/src/select-service-routes.ts +1 -31
  108. package/src/server-error-page.ts +0 -18
  109. package/src/theme-fonts-link.ts +1 -21
  110. package/src/transfers/presigned-upload.ts +1 -27
  111. package/src/two-phase-full-app.ts +0 -82
  112. package/src/two-phase-minimal-app.ts +1 -111
  113. package/src/two-phase-readiness.ts +0 -6
  114. package/src/types.ts +0 -17
  115. package/src/ui-service.ts +2 -48
  116. package/src/ws-boot-self-test.ts +1 -14
  117. package/src/content-gate/gate-entry-marker.test.ts +0 -111
  118. package/src/content-gate/token.segmentation.test.ts +0 -76
  119. package/src/content-gate/token.test.ts +0 -65
  120. package/src/content-gate/token.ts +0 -219
  121. package/src/is-react-element-type.test.ts +0 -45
  122. package/src/is-react-element-type.ts +0 -20
  123. package/src/page-server-orpc/hub-mcp-client.ts +0 -20
  124. package/src/pg-boss-mount.test.ts +0 -101
  125. package/src/resource-chat-inject.page-assistant.test.ts +0 -145
  126. package/src/resource-chat-inject.test.ts +0 -240
  127. package/src/resource-chat-inject.ts +0 -374
@@ -1,71 +1,23 @@
1
1
  /**
2
2
  * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * The config contracts that PROGRAM a content gate. Each gated content type
7
- * (agreement, proposal, …) is one ContentGateConfig + one AccessConfig — no
8
- * bespoke gate functions per type; the engine (gate.ts) and the access mounter
9
- * (access.ts) are the single shared primitive driven by these.
10
4
  */
11
5
 
6
+ import type { GateSegmentation } from "@teamscala/agreement-generation/content-gate-token";
7
+
12
8
  type Resolved = Record<string, unknown>;
13
9
 
14
- /**
15
- * Optional prospect-segmentation a lead-capture gate may capture alongside the
16
- * identity. The quiz gate collects these on its composed identity screen; the
17
- * token carries them so the next render and the final submit know the visitor
18
- * fully, with NO client storage (same reasoning as the captured name/phone).
19
- */
20
- export interface GateSegmentation {
21
- company?: string;
22
- linkedinUrl?: string;
23
- website?: string;
24
- teamSize?: string;
25
- }
10
+ export type { GateSegmentation };
26
11
 
27
- /** Drives the SSR-time gate (gate.ts / runContentGate). */
28
12
  export interface ContentGateConfig {
29
- /** Cookie that carries this gate's access token (e.g. "ag_token"). */
30
13
  cookieName: string;
31
- /**
32
- * The gated resource id from the resolved graph, or "" when this gate does
33
- * not apply to the current page (the engine then no-ops). This is also how a
34
- * gate detects "is this my kind of page". Async to allow DB-backed resolution
35
- * (e.g. agreement gate resolving the UUID from the page→agreement association
36
- * when no pre-placed gate block exists).
37
- */
38
14
  getResourceId(resolved: Resolved): string | Promise<string>;
39
- /**
40
- * Verify the cookie's token for this resource → the verified identity
41
- * ({email, optional display name}), or null. Token-agnostic by design:
42
- * proposals use the shared content-gate token; agreements keep their existing
43
- * token (also minted by the signing ORPC), so the engine never assumes one
44
- * token format. Allow-list gates return just {email}; the GPT lead-capture
45
- * gate also returns the captured name (carried in its token).
46
- */
47
15
  verifyToken(
48
16
  token: string | undefined,
49
17
  resourceId: string,
50
18
  nowMs: number,
51
19
  ): { email: string; name?: string; phone?: string; segmentation?: GateSegmentation } | null;
52
- /**
53
- * BAKED-IN unified auth. When a gate provides this resolver, the engine
54
- * AUTOMATICALLY releases a logged-in viewer whose session email is in the
55
- * authorized set — with no valid token and no per-gate code. This is the
56
- * platform default for EVERY gated resource: any application surface with
57
- * authentication gets "a signed-in, authorized viewer is never re-asked for
58
- * their email/OTP" for free, just by exposing its authorized-email set (the
59
- * same resolver its AccessConfig uses). Not called when the session is
60
- * anonymous. Omit only for open-capture gates that have no authorized set.
61
- */
62
20
  resolveAuthorizedEmails?(resourceId: string): Promise<Set<string>>;
63
- /**
64
- * OPTIONAL OVERRIDE of the baked-in session check above — for the rare gate
65
- * whose session→identity rule isn't simply "session email ∈ authorized set".
66
- * When present it wins; otherwise the engine uses `resolveAuthorizedEmails`.
67
- * Called only when no valid access token is present.
68
- */
69
21
  sessionAuthorize?(
70
22
  resolved: Resolved,
71
23
  resourceId: string,
@@ -73,62 +25,23 @@ export interface ContentGateConfig {
73
25
  | { email: string; name?: string }
74
26
  | null
75
27
  | Promise<{ email: string; name?: string } | null>;
76
- /** Reveal + seed the verified viewer's identity/session (token was valid). */
77
28
  onAuthorized(
78
29
  resolved: Resolved,
79
30
  resourceId: string,
80
31
  identity: { email: string; name?: string; phone?: string; segmentation?: GateSegmentation },
81
32
  ): void | Promise<void>;
82
- /** Withhold content (strip/inject the gate) — token absent/invalid. Async-
83
- * capable so a gate can load its display/behaviour settings from a registry
84
- * row (DB-driven, not hardcoded); sync implementations remain valid. */
85
33
  onUnauthorized(resolved: Resolved, resourceId: string): void | Promise<void>;
86
- /**
87
- * OPT-IN REQUIRED-GATE-ENTRY. When set, a valid token authorizes ONLY on a
88
- * request that also carries this marker cookie — set by the access endpoint
89
- * alongside the token and consumed (cleared) by the render it authorizes, so
90
- * the marker lives exactly one view: the submit's own redirect releases the
91
- * content, and every later load (a refresh) is unauthorized, showing the
92
- * gate again. Omit for gates whose token is session-scoped by design.
93
- */
94
34
  entryCookieName?: string;
95
35
  }
96
36
 
97
- /** Drives the public access endpoint (access.ts / attachContentGateAccess). */
98
37
  export interface AccessConfig {
99
- /** Route to mount, e.g. "/api/proposal-access". */
100
38
  path: string;
101
- /** Cookie to set on a match. */
102
39
  cookieName: string;
103
- /** Cookie lifetime in ms (Max-Age). */
104
40
  ttlMs: number;
105
- /**
106
- * Marker cookie set alongside the token on a match. The gate requires it
107
- * (ContentGateConfig.entryCookieName) and the render it authorizes clears
108
- * it, so it authorizes exactly the submit's own redirect.
109
- */
110
41
  entryCookieName?: string;
111
- /** Form field carrying the resource id, e.g. "proposalId". */
112
42
  recordIdField: string;
113
- /**
114
- * Open lead-capture gate: ANY non-empty email is accepted (no allow-list).
115
- * For surfaces whose "gate" captures a visitor rather than authorizing one
116
- * against a fixed set (the public GPT). When true, `resolveAuthorizedEmails`
117
- * is not called. Default/omitted = allow-list mode (proposals, agreements).
118
- */
119
43
  openCapture?: boolean;
120
- /**
121
- * The authorized email set for a resource (lowercased), checked server-side.
122
- * Not called when `openCapture` is true.
123
- */
124
44
  resolveAuthorizedEmails(resourceId: string): Promise<Set<string>>;
125
- /**
126
- * Mint the access token for a matched email (token-agnostic per gate).
127
- * `name` and `phone` are the optional captured identity, `segmentation` the
128
- * optional captured prospect fields (open-capture gates carry them in the
129
- * token; allow-list gates ignore them). They exist so a capture gate needs
130
- * no browser storage to remember who this is.
131
- */
132
45
  signToken(
133
46
  resourceId: string,
134
47
  email: string,
@@ -1,24 +1,6 @@
1
1
  /**
2
2
  * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * REQUEST-SCOPED channel for gate-entry marker clears. A gate that declares an
7
- * entry marker (config.entryCookieName) consumes it on the render that the
8
- * access endpoint's own redirect produces; that render's response must clear
9
- * the marker, or the next load (a refresh) would still carry it and skip the
10
- * gate. The gate writes the name here; bindPageHandler — which owns the page
11
- * Response — drains it into Set-Cookie, so the marker lives exactly one view.
12
- *
13
- * THE CLEARS COME BACK AS THE CALL'S RETURN VALUE, never from a post-hoc
14
- * read: the store only exists inside the run() scope, so a reader called after
15
- * the awaited call has already left it and would see an empty list — which is
16
- * exactly how the first version of this file shipped a marker that was never
17
- * cleared (measured live 2026-09-14: the cookie survived the render).
18
- *
19
- * AsyncLocalStorage rather than a module-level map: request-scoped by
20
- * construction, no accumulated process state that a restart would silently
21
- * reset (in-memory-state-is-not-durable).
22
4
  */
23
5
  import { AsyncLocalStorage } from "node:async_hooks";
24
6
 
@@ -1,29 +1,8 @@
1
1
  /**
2
2
  * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * The content-gate ENGINE. Runs in the page loader after the resource graph
7
- * resolves, BEFORE SSR. Orchestration only — the per-type behaviour (which
8
- * resource, who's authorized, what to seed, how to withhold) is supplied by a
9
- * ContentGateConfig, so agreements/proposals/future gates are configs, not new
10
- * functions. The single place that decides whether gated bytes reach the HTML.
11
4
  */
12
5
 
13
- /**
14
- * What a gate decided, so a caller can act on it without re-deriving it.
15
- *
16
- * THE ASSISTANT MUST NOT OUTRUN THE GATE. Its runtime context is the whole
17
- * document serialised, so injecting it for an unauthorized viewer would hand
18
- * the gated bytes to the chat -- the exact thing the gate exists to withhold.
19
- * The gate already knows the answer; returning it means the page-route
20
- * assistant setting reads ONE decision instead of inventing a second notion of
21
- * "is this viewer allowed" that could drift from this one.
22
- *
23
- * `applies: false` means the gate did not govern this page at all (a public
24
- * page), which is different from an unauthorized viewer and must not be
25
- * confused with one.
26
- */
27
6
 
28
7
  import type { ContentGateConfig } from "./config-types.ts";
29
8
  import { parseCookie } from "./parse-cookie.ts";
@@ -88,12 +67,6 @@ export async function runContentGate(
88
67
  return { applies: true, authorized: false };
89
68
  }
90
69
 
91
- /**
92
- * The signed-in viewer's authorized identity for this resource, or null. The
93
- * default (used by every gate) is "session email ∈ the gate's authorized set";
94
- * a gate may supply a custom sessionAuthorize to override. Anonymous sessions
95
- * and gates with no authorized-email set return null (stay token-only).
96
- */
97
70
  async function resolveSessionIdentity(
98
71
  resolved: Record<string, unknown>,
99
72
  resourceId: string,
@@ -1,10 +1,6 @@
1
1
  /**
2
2
  * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * Parse a single cookie value out of a raw Cookie header — shared by the
7
- * content-gate engine and any gate config.
8
4
  */
9
5
 
10
6
  export function parseCookie(
@@ -1,31 +1,9 @@
1
1
  /**
2
2
  * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * Pure decision: does a CONTENT route (no resource_type) whose loader resolved
7
- * a `page` body source warrant the branded not-found floor? Extracted from
8
- * register-registry-page-route so the rule is unit-testable independent of the
9
- * Hono/DB/graph machinery that wraps it (the fix's correctness does not depend
10
- * on the publish — verify-before-done).
11
- *
12
- * Three outcomes:
13
- * - no page row (deleted/unmatched path, e.g. /roadmap) → dead-link 404 floor
14
- * - page row exists but zero content blocks → empty-page 200 floor
15
- * - otherwise → null (render normally)
16
- *
17
- * The "Vision Toolbar" leak this closes: a content catchall synthesized an empty
18
- * page for an unmatched path and served a cacheable 200; resource-not-found-is-
19
- * the-root-floor only fired for resource_type routes, so dead content links fell
20
- * through. See resource-not-found.ts for the floor + operator ruling 2026-07-21.
21
4
  */
22
5
  import { PrimaryResourceMissingError } from "./resource-not-found.ts";
23
6
 
24
- /**
25
- * A page ROW exists but its body has zero content blocks — a content gap, not a
26
- * dead link. True only when the row is present yet `puck_data` is absent or its
27
- * `content` array is empty. Handles string (raw) and object (normalized) puck_data.
28
- */
29
7
  export function pageBodyIsEmpty(page: unknown): boolean {
30
8
  if (!page || typeof page !== "object") return false;
31
9
  const obj = page as Record<string, unknown>;
@@ -44,12 +22,6 @@ export function pageBodyIsEmpty(page: unknown): boolean {
44
22
  return !Array.isArray(data?.content) || (data.content as unknown[]).length === 0;
45
23
  }
46
24
 
47
- /**
48
- * Decide whether a content route should fall to the branded not-found floor.
49
- * Returns the error to throw (mapped to the floor by bind-page-handler), or null
50
- * to render normally. `hasResourceType` routes are handled by the existing
51
- * resource_type missing check (caller-side) — they return null here.
52
- */
53
25
  export function decideContentPageFloor(opts: {
54
26
  hasResourceType: boolean;
55
27
  hasPageContentSource: boolean;
@@ -1,33 +1,6 @@
1
1
  /**
2
- * @system server-boot
2
+ * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * csrf-protection.ts — platform CSRF middleware (`security-csrf-protection`).
7
- *
8
- * Hono's csrf() with a DERIVED trusted-origin set, never a hand-authored
9
- * allowlist: every trusted origin comes from ONE source per
10
- * `offered-is-derived` —
11
- * - the managed base domain (registry portal-domains) covers the platform's
12
- * own `*.scala.business` origins, INCLUDING the cross-service form POST
13
- * redirect chains CSP form-action allowlists (proposals → sign etc.), so
14
- * the CSRF origin set and the CSP form-action set cannot disagree about
15
- * what "one of ours" means;
16
- * - customer custom domains resolve through `isTrustedAuthOrigin`
17
- * (builda_websites lookup) — the SAME lookup better-auth's trustedOrigins
18
- * uses in @teamscala/auth, so a new custom domain is trusted everywhere at
19
- * once or nowhere.
20
- *
21
- * hono csrf() arms only on form-element content types (urlencoded, multipart,
22
- * text/plain) — browsers cannot produce a cross-site application/json request
23
- * from a form, so the JSON API surface (ORPC) is protected by cookie
24
- * SameSite=Lax + this middleware's text/plain coverage.
25
- *
26
- * Custom-domain lookups are cached per-hostname with a TTL: a POSITIVE result
27
- * is cached for the full TTL (a domain that stopped being ours keeps working
28
- * for at most the TTL), a NEGATIVE result for a short window (a brand-new
29
- * custom domain becomes trusted within seconds). The cache is a request
30
- * accelerator over a DB fact, never the source of truth.
31
4
  */
32
5
 
33
6
  import { getManagedBaseDomain } from "@teamscala/auth/auth-env-resolution";
@@ -1,29 +1,6 @@
1
1
  /**
2
2
  * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * The error/holding page's brand logo: resolve the host org's dark-theme
7
- * wordmark from its install row and inline it as a data URI. Split from
8
- * resource-not-found.ts under single-purpose-file (the builder consumes a
9
- * logoUrl argument; only these resolvers touch storage and the registry).
10
- *
11
- * The host org's LOGO (wordmark) for the dark error page — the favicon is a
12
- * tab glyph, the wrong asset at page scale (operator-observed: a lone "h"
13
- * where the launcha wordmark belonged). Resolution: the service's own
14
- * install row names its owner (brand_org); the org's dark-theme logo (a
15
- * light-coloured mark, correct on the dark page) is preferred, light-theme
16
- * second. Every step skew-tolerant: any miss → null → the builder renders a
17
- * logo-less dark page.
18
- *
19
- * Returns the logo as an INLINE data URI (not a `/api/assets/` proxy URL) so
20
- * the branded holding/error page ships the logo IN the initial HTML bytes —
21
- * no second fetch, no post-paint pop-in, no dependency on the asset proxy the
22
- * degraded service is failing to serve (incident 2026-07-27: the logo on a
23
- * DEGRADED render-layer holding page appeared after the page painted). The
24
- * logo sibling of icons-render-ssr-inline. Size-capped: a pathological
25
- * (>MAX_INLINE_LOGO_BYTES) logo falls back to null rather than bloating the
26
- * HTML; the page renders logo-less, never broken.
27
4
  */
28
5
 
29
6
  import { getClient } from "@teamscala/object-storage/create/client/get-client";
@@ -32,7 +9,7 @@ import { registerCacheInvalidation } from "@teamscala/db/notify-listener/cache-i
32
9
  import { getServerClient } from "@teamscala/orpc/server-client-registry";
33
10
  import { loadRegistryConfig } from "@teamscala/db/registry/load-config";
34
11
  import { getAppLogger } from "@teamscala/logger/app-loggers";
35
- import { ServiceInstallConfigSchema } from "./load-service-install-row.ts";
12
+ import { ServiceInstallBootSchema } from "./load-service-install-row.ts";
36
13
 
37
14
  const logoCache = createCache<string | null>("service-boot:error-page-logo", {
38
15
  ttlMs: 5 * 60_000,
@@ -48,7 +25,7 @@ export async function resolveErrorPageLogo(
48
25
  const cfg = await loadRegistryConfig(
49
26
  "service_install",
50
27
  serviceSlug,
51
- ServiceInstallConfigSchema,
28
+ ServiceInstallBootSchema,
52
29
  );
53
30
  const org = (cfg as { brand_org?: string }).brand_org;
54
31
  if (!org) {
@@ -95,20 +72,10 @@ export async function resolveErrorPageLogo(
95
72
  });
96
73
  }
97
74
 
98
- /** Above this byte size, inlining bloats the HTML more than it helps — fall
99
- * back to a logo-less page. Typical org logos are 5–30 KB, so this only
100
- * catches a genuinely oversized upload. */
101
75
  const MAX_INLINE_LOGO_BYTES = 100 * 1024;
102
76
 
103
77
  const ASSETS_PREFIX = "/api/assets/";
104
78
 
105
- /**
106
- * Inline a stored logo value as a `data:<mime>;base64,…` URI by fetching its
107
- * bytes from S3 (the same store `/api/assets/` proxies to). The stored value
108
- * IS the servable form — `/api/assets/<key>` — so the bucket key is the path
109
- * segment after that prefix (single-logo-pathway). Skew-tolerant: any miss →
110
- * null.
111
- */
112
79
  type LogoStorage = {
113
80
  file(key: string): {
114
81
  stat(): Promise<unknown>;
@@ -1,21 +1,6 @@
1
1
  /**
2
- * @system server-boot
2
+ * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * Server-side roster expansion — the executor that handles "render N of block X
7
- * per data row" so the page never needs a domain-specific panel/list block. A
8
- * block authored with `props.dataRepeat: "parties"` is a TEMPLATE; before SSR
9
- * this clones it once per roster item into its parent slot (top-level content or
10
- * any Container's `content` slot), then drops the marker. Each clone:
11
- * - sets `role` + `dataKey` to the item's key, so a prop-driven block (the
12
- * SignatureField cell) finds its own party with no per-clone wiring;
13
- * - substitutes `{{name}}` / `{{role}}` / `{{company}}` / `{{date}}` /
14
- * `{{status}}` placeholders in any string prop (so a recipient row is plain
15
- * `Container` + `Text` primitives — the copy lives on the page, not here).
16
- * `{{status}}` resolves from the template's own `signedLabel` /
17
- * `awaitingLabel` props (DB-driven copy), never hardcoded.
18
- * Generic over any roster; the agreement page passes its parties.
19
4
  */
20
5
 
21
6
  export interface RosterItem {
@@ -46,7 +31,6 @@ function formatDate(iso: string | null): string {
46
31
  }
47
32
  }
48
33
 
49
- /** Resolve a `{{field}}` token for one roster item. */
50
34
  function tokenValue(
51
35
  field: string,
52
36
  item: RosterItem,
@@ -70,7 +54,6 @@ function tokenValue(
70
54
  }
71
55
  }
72
56
 
73
- /** Replace every `{{field}}` token in a string with the item's value. */
74
57
  function substitute(
75
58
  value: string,
76
59
  item: RosterItem,
@@ -81,7 +64,6 @@ function substitute(
81
64
  );
82
65
  }
83
66
 
84
- /** The `content` slot arrays a block exposes (Container.props.content, …). */
85
67
  function slotArrays(block: Block): unknown[][] {
86
68
  const out: unknown[][] = [];
87
69
  const props = block.props;
@@ -94,7 +76,6 @@ function slotArrays(block: Block): unknown[][] {
94
76
  return out;
95
77
  }
96
78
 
97
- /** Deep-clone a template block, binding it to one roster item. */
98
79
  function cloneForItem(template: Block, item: RosterItem): Block {
99
80
  const labels = {
100
81
  signed: String(template.props?.signedLabel ?? ""),
@@ -125,7 +106,6 @@ function cloneForItem(template: Block, item: RosterItem): Block {
125
106
  return clone;
126
107
  }
127
108
 
128
- /** Is this block a per-party template to expand? */
129
109
  function isPartyTemplate(block: Block): boolean {
130
110
  // Generic marker (any block that declares `dataRepeat`)…
131
111
  if (block?.props?.dataRepeat === "parties") return true;
@@ -139,7 +119,6 @@ function isPartyTemplate(block: Block): boolean {
139
119
  return false;
140
120
  }
141
121
 
142
- /** Expand every per-party template in a content array, recursing into slots. */
143
122
  function expandArray(content: unknown[], parties: RosterItem[]): unknown[] {
144
123
  const out: unknown[] = [];
145
124
  for (const raw of content) {
@@ -159,12 +138,10 @@ function expandArray(content: unknown[], parties: RosterItem[]): unknown[] {
159
138
  return out;
160
139
  }
161
140
 
162
- /** Agreement-level tokens (one substitution, not per-party). */
163
141
  export interface RosterMeta {
164
142
  [token: string]: string;
165
143
  }
166
144
 
167
- /** Substitute `{{agreement.token}}` placeholders in every string prop, in place. */
168
145
  function substituteMeta(node: unknown, meta: RosterMeta): unknown {
169
146
  if (typeof node === "string") {
170
147
  return node.replace(/\{\{\s*agreement\.(\w+)\s*\}\}/g, (_m, t) =>
@@ -181,7 +158,6 @@ function substituteMeta(node: unknown, meta: RosterMeta): unknown {
181
158
  return node;
182
159
  }
183
160
 
184
- /** Expand roster templates + bind agreement-meta placeholders across regions. */
185
161
  export function expandRosterTemplates(
186
162
  resolved: Record<string, unknown>,
187
163
  parties: RosterItem[],
package/src/gate-ttls.ts CHANGED
@@ -1,18 +1,6 @@
1
1
  /**
2
2
  * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * Cached readers for the content-gate access-token TTL config rows
7
- * (agreement-gate / proposal-gate / gpt-gate / quiz-gate). initGateTtls() runs once at boot
8
- * (two-phase-boot, before the gates mount) to populate the cache from the
9
- * registry rows; the sync getters (agTtlMs/propTtlMs/gptTtlMs) return the
10
- * cached value — or the former hardcoded default before init / on row-miss — so
11
- * the sync AccessConfig.ttlMs fields + sync hono magic-link handlers read them
12
- * without async. Replaces the per-file AG_TTL_MS / PROP_TTL_MS / GPT_TTL_MS
13
- * constants (config-constants: the registry row is the source, dedup'd across
14
- * each gate + its attach-* magic counterpart so token TTL and cookie maxAge
15
- * never drift).
16
4
  */
17
5
  import { loadRegistryConfig } from "@teamscala/db/registry/load-config";
18
6
  import * as v from "valibot";
package/src/gpt-gate.ts CHANGED
@@ -1,28 +1,10 @@
1
1
  /**
2
2
  * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * The PUBLIC GPT identity gate, expressed as config for the content-gate
7
- * primitive (content-gate/). Unlike proposals/agreements this is NOT access
8
- * control — every email is a valid visitor (lead capture). Its whole job is to
9
- * make a returning visitor's identity known AT SSR so the GPT body renders the
10
- * real conversation on first paint instead of server-rendering the capture gate
11
- * and then swapping to the chat after hydration (the ssr-then-live-handoff +
12
- * "know the visitor at SSR first paint" violation: building-applications).
13
- *
14
- * Flow: the capture form POSTs {gptId, email} to /api/gpt-access (openCapture →
15
- * any email matches); the access endpoint sets a signed `gpt_token` cookie. On
16
- * the next request runContentGate verifies that cookie for this agent and seeds
17
- * resolved.initialVisitor — which the GPT body serverLoader (ssrGptSession) then
18
- * uses to SSR-restore the conversation, and GptBodyBlock uses to skip the gate.
19
- * onUnauthorized is a no-op: an un-gated (first-time) visitor sees the capture
20
- * form the block renders itself when no visitor is known — that is correct, not
21
- * a flash (they ARE anonymous), so there is nothing to withhold.
22
4
  */
23
5
 
24
6
  import type { AccessConfig, ContentGateConfig } from "./content-gate/config-types.ts";
25
- import { signAccessToken, verifyAccessTokenIdentity } from "./content-gate/token.ts";
7
+ import { signAccessToken, verifyAccessTokenIdentity } from "@teamscala/agreement-generation/content-gate-token";
26
8
  import { gptTtlMs } from "./gate-ttls.ts";
27
9
 
28
10
  const GPT_COOKIE = "gpt_token";
@@ -1,21 +1,6 @@
1
1
  /**
2
2
  * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * The single app.onError factory for every Hono app (UI + backend). Two error
7
- * classes, deliberately separate:
8
- * - HTTPException: a DELIBERATE status-bearing throw (e.g. hono bodyLimit's
9
- * 413). Preserve its status + message verbatim — buildErrorReport only knows
10
- * 500/503 and would erase a deliberate 413/401/etc.
11
- * - anything else: an UNEXPECTED error → buildErrorReport (errorId correlation,
12
- * transient-vs-bug classification, 503 + Retry-After when retryable, else 500).
13
- *
14
- * Resolves the apparent tension between `hono-global-error-handler` (HTTPException
15
- * → status + JSON) and `error-responses-via-error-report` (every error response
16
- * via buildErrorReport): the latter governs UNEXPECTED errors; HTTPException is
17
- * the documented deliberate-status carve-out. The handler is a consumer of
18
- * buildErrorReport, never a second classifier for the unexpected branch.
19
4
  */
20
5
 
21
6
  import { getAppLogger } from "@teamscala/logger/app-loggers";
@@ -23,10 +8,6 @@ import { buildErrorReport } from "@teamscala/os/errors/error-report";
23
8
  import type { Context } from "hono";
24
9
  import { HTTPException } from "hono/http-exception";
25
10
 
26
- /**
27
- * Returns an `app.onError` handler tagged with `label` (used in the log line so
28
- * the boot site that surfaced the error is identifiable).
29
- */
30
11
  export function createRequestErrorHandler(label: string) {
31
12
  return (err: unknown, c: Context): Response => {
32
13
  // Errors must never be swallowed (errors-always-logged): every unhandled
@@ -1,17 +1,6 @@
1
1
  /**
2
- * @system server-boot
2
+ * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * Install harness — sandbox-mounts each declared package and captures
7
- * registry.require() calls via AsyncLocalStorage. Validates all required
8
- * rows exist before returning. Throws HarnessInstallError with full
9
- * context on failure. Per Constitution Rule 2 step 3.
10
- *
11
- * Marked: HarnessInstallError is the error class
12
- * validatePackageDependencies (might) throw — they form one canonical
13
- * "install harness validation" unit (the validator + the failure-mode
14
- * class it produces).
15
4
  */
16
5
 
17
6
  import {
@@ -1,20 +1,6 @@
1
1
  /**
2
- * @system server-boot
2
+ * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * One-call install of the entire logging pipeline. After installLogging()
7
- * runs, every error in the service — process-level, HTTP middleware, Hono
8
- * handlers, Puck blocks, even third-party packages calling console.* — is
9
- * captured by @teamscala/logger and flows to Sentry/GlitchTip (prod) or
10
- * loga (dev) with full context. Application code never imports the logger
11
- * or wraps anything in try/catch for telemetry purposes.
12
- *
13
- * Two phases, two files (single-purpose-file: the guard counts them as two
14
- * export groups in one file): installLogging (early bootstrap, hardcoded
15
- * defaults) lives here; applyLoggerConfig (late-stage, DB-driven) lives in
16
- * apply-logger-config.ts. The service boot pipeline calls them in sequence
17
- * with the same serviceSlug.
18
4
  */
19
5
 
20
6
  import { ManagedCache } from "@teamscala/cache/cache";
@@ -31,16 +17,9 @@ import { loadRegistryConfig as dbLoadRegistryConfig } from "@teamscala/db/regist
31
17
  import { configure as httpConfigure } from "@teamscala/http/configure";
32
18
  import { getAppLogger } from "@teamscala/logger/app-loggers";
33
19
  import { initSentry } from "@teamscala/logger/init-sentry";
34
- import { attachRequestLogger } from "@teamscala/logger/install-logging";
35
20
  import { getRequestContext } from "@teamscala/logger/request-context-async";
36
21
  import { configure as orpcConfigure } from "@teamscala/orpc/configure";
37
22
 
38
- // Re-exported from @teamscala/logger (the canonical home, per
39
- // vertical-dependency-only). service-boot consumers that previously
40
- // imported from service-boot/install-logging continue to work; the
41
- // implementation lives once in @teamscala/logger.
42
- export { attachRequestLogger };
43
-
44
23
  const CONSOLE_METHODS = [
45
24
  "log",
46
25
  "info",
@@ -65,29 +44,13 @@ export function installLogging(opts: InstallLoggingOptions): void {
65
44
  installConsoleShim(serviceSlug);
66
45
  installProcessHandlers(serviceSlug);
67
46
 
68
- // Bootloader injection per `bootloader-injection-contract`. After
69
- // initSentry + the console redirect, the logger surface is live; inject it
70
- // into every configurable primitive that defaults to noop. The list is
71
- // enumerated here; it becomes registry-driven once #57 lands.
72
47
  const log = getAppLogger();
73
- // Persisted last-known-good tier for registry config reads — created here
74
- // (runtime tier) so host-path resolution stays out of the foundation-tier
75
- // db package, which cannot import @teamscala/cache or resolve userHome itself.
76
- // load-config serves this on a transient DB failure instead of crash-looping.
77
- // The root is stateRoot(), not userHome(): a container that declares a
78
- // read-only HOME (scala-sign prod: HOME=/var/empty) made this cache EACCES on
79
- // every request — the tier died in exactly the outage class it exists for.
80
48
  const registryConfigDiskCache = createDiskCache("db:registry-config", {
81
49
  dir: join(stateRoot(), ".scala-db-cache", "registry-config"),
82
50
  ttlMs: 24 * 60 * 60 * 1000,
83
51
  });
84
52
  dbConfigure({
85
53
  logger: log,
86
- // `as never` bridges the configured-primitives type gap: db holds
87
- // structural approximations of these foundation factories (it cannot
88
- // import @teamscala/cache/timing for the real CacheOptions/TimerOptions
89
- // without re-creating the same-tier edge). The runtime values are the
90
- // real factories; the cast only reconciles the approximate signatures.
91
54
  ManagedCache: ManagedCache as never,
92
55
  createCache: createCache as never,
93
56
  createTimer: createTimer as never,
@@ -106,10 +69,6 @@ export function installLogging(opts: InstallLoggingOptions): void {
106
69
  logger: log,
107
70
  requestContextProvider: () => getRequestContext(),
108
71
  });
109
- // @teamscala/mcp-multi-session and @teamscala/tool-executor are MCP-only;
110
- // their configure() calls moved to @teamscala/mcp-tool-runtime/mount.ts
111
- // per `runtime-deps-universal-only`. MCP services declare mcp-tool-runtime
112
- // in service_install.server_packages and pick up the configures there.
113
72
  }
114
73
 
115
74
  function installConsoleShim(serviceSlug: string): void {
@@ -163,8 +122,3 @@ function safeStringify(v: unknown): string {
163
122
  return String(v);
164
123
  }
165
124
  }
166
- // No event-driven invalidation subscription here: this module caches nothing.
167
- // The removed block opened a dedicated LISTEN connection whose handler was
168
- // empty `() => {}` — a connection held for the process lifetime to run no
169
- // code. Registering a no-op against the shared listener would be the same
170
- // defect one layer down, so it is deleted rather than migrated.