@teamscala/service-boot 0.23.329 → 0.23.331

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 (157) hide show
  1. package/package.json +538 -522
  2. package/src/agreement-gate.d.ts +19 -0
  3. package/src/asset-cache-key.d.ts +11 -0
  4. package/src/asset-optimize.d.ts +17 -0
  5. package/src/asset-proxy.d.ts +11 -0
  6. package/src/asset-social-card.d.ts +17 -0
  7. package/src/attach-agreement-magic.d.ts +20 -0
  8. package/src/attach-portal-members.d.ts +23 -0
  9. package/src/attach-proposal-accept.d.ts +26 -0
  10. package/src/attach-proposal-magic.d.ts +18 -0
  11. package/src/attach-transfers-upload.d.ts +27 -0
  12. package/src/auth-gate.d.ts +28 -0
  13. package/src/auth-gate.ts +6 -68
  14. package/src/bind-page-handler.d.ts +18 -0
  15. package/src/build-client-assets/input-hashing.d.ts +25 -0
  16. package/src/build-client-assets/source-candidates.d.ts +27 -0
  17. package/src/build-client-assets.d.ts +50 -0
  18. package/src/build-client-assets.test.ts +1 -1
  19. package/src/build-client-assets.ts +1 -1
  20. package/src/compose-page-title.d.ts +41 -0
  21. package/src/content-gate/access.d.ts +15 -0
  22. package/src/content-gate/config-types.d.ts +99 -0
  23. package/src/content-gate/gate.d.ts +13 -0
  24. package/src/content-gate/parse-cookie.d.ts +9 -0
  25. package/src/content-gate/token.d.ts +48 -0
  26. package/src/content-page-floor.d.ts +39 -0
  27. package/src/discover-blocks.d.ts +28 -0
  28. package/src/discover-blocks.ts +1 -1
  29. package/src/expand-roster.d.ts +34 -0
  30. package/src/fn-routers/agreement-completion-email.d.ts +14 -0
  31. package/src/fn-routers/agreement-send-router.d.ts +82 -0
  32. package/src/fn-routers/ai-health-router.d.ts +35 -0
  33. package/src/fn-routers/auth-router.d.ts +47 -0
  34. package/src/fn-routers/cache-admin-router.d.ts +57 -0
  35. package/src/fn-routers/chat-session-router.d.ts +219 -0
  36. package/src/fn-routers/email-defaults.d.ts +1 -0
  37. package/src/fn-routers/email-link-ttl.d.ts +1 -0
  38. package/src/fn-routers/index.d.ts +18 -0
  39. package/src/fn-routers/index.ts +0 -2
  40. package/src/fn-routers/panel-data-router.d.ts +19 -0
  41. package/src/fn-routers/persist-chat-messages.d.ts +36 -0
  42. package/src/fn-routers/plans-router.d.ts +25 -0
  43. package/src/fn-routers/proposal-send-router.d.ts +58 -0
  44. package/src/fn-routers/public-agreement-router.d.ts +289 -0
  45. package/src/fn-routers/public-assessment-router.d.ts +68 -0
  46. package/src/fn-routers/public-gpt-session-router.d.ts +143 -0
  47. package/src/fn-routers/sign-in-flow-router.d.ts +107 -0
  48. package/src/fn-routers/task-sync-assign.d.ts +305 -0
  49. package/src/fn-routers/task-sync-assign.test.ts +1 -2
  50. package/src/fn-routers/task-sync-assign.ts +4 -4
  51. package/src/fn-routers/task-sync-bind.test.ts +18 -18
  52. package/src/fn-routers/task-sync-brief.d.ts +96 -0
  53. package/src/fn-routers/task-sync-brief.test.ts +17 -20
  54. package/src/fn-routers/task-sync-brief.ts +14 -15
  55. package/src/fn-routers/task-sync-close.d.ts +41 -0
  56. package/src/fn-routers/task-sync-dispatch.d.ts +61 -0
  57. package/src/fn-routers/task-sync-dispatch.test.ts +33 -51
  58. package/src/fn-routers/task-sync-dispatch.ts +124 -0
  59. package/src/fn-routers/task-sync-lane.d.ts +49 -0
  60. package/src/fn-routers/task-sync-lane.ts +125 -0
  61. package/src/fn-routers/task-sync-projection.d.ts +121 -0
  62. package/src/fn-routers/task-sync-projection.test.ts +0 -26
  63. package/src/fn-routers/task-sync-projection.ts +0 -51
  64. package/src/fn-routers/task-sync-rename.d.ts +91 -0
  65. package/src/fn-routers/task-sync-router.d.ts +444 -0
  66. package/src/fn-routers/task-sync-router.test.ts +2 -14
  67. package/src/fn-routers/task-sync-router.ts +1111 -0
  68. package/src/fn-routers/tasks-router.d.ts +25 -0
  69. package/src/fn-routers/user-preferences-router.d.ts +77 -0
  70. package/src/fn-routers/work-items-router.d.ts +102 -0
  71. package/src/gate-ttls.d.ts +6 -0
  72. package/src/gpt-gate.d.ts +25 -0
  73. package/src/handlers/bun-stable-watch.d.ts +1 -0
  74. package/src/handlers/pane-outbox-drain.d.ts +1 -0
  75. package/src/handlers/projects-digest.d.ts +26 -0
  76. package/src/handlers/prune-stale-data.d.ts +30 -0
  77. package/src/handlers/registry-stale-inactive-sweep.d.ts +1 -0
  78. package/src/handlers/scala-tools-currency-watch.d.ts +1 -0
  79. package/src/handlers/singleton-tools-currency-watch.d.ts +1 -0
  80. package/src/handlers/sweep-event-log.d.ts +1 -0
  81. package/src/handlers/sweep-host-cache.d.ts +1 -0
  82. package/src/handlers/sweep-host-cache.ts +26 -143
  83. package/src/handlers/upstream-issue-sync.d.ts +1 -0
  84. package/src/handlers/whatsapp-liveness-logic.d.ts +34 -0
  85. package/src/handlers/whatsapp-liveness.d.ts +4 -0
  86. package/src/hono-error-handler.d.ts +25 -0
  87. package/src/install-harness.d.ts +45 -0
  88. package/src/install-logging.d.ts +34 -0
  89. package/src/load-service-install-row.d.ts +24 -0
  90. package/src/load-service-prisma-client.d.ts +18 -0
  91. package/src/mount-orpc-router.d.ts +15 -0
  92. package/src/mount-orpc-router.ts +1 -1
  93. package/src/mount-server-packages.d.ts +29 -0
  94. package/src/mount.d.ts +9 -0
  95. package/src/mutable-app-ref.d.ts +44 -0
  96. package/src/page-route-shell-types.d.ts +12 -0
  97. package/src/page-server-orpc/audit-revert-router.d.ts +37 -0
  98. package/src/page-server-orpc/hub-mcp-client.d.ts +12 -0
  99. package/src/page-server-orpc/portal-diagnostics-router.d.ts +49 -0
  100. package/src/page-server-orpc/puck-pages-router.d.ts +58 -0
  101. package/src/page-server-orpc/server-client.d.ts +16 -0
  102. package/src/page-server-orpc/templates-router.d.ts +34 -0
  103. package/src/page-server-orpc/user-preferences-router.d.ts +79 -0
  104. package/src/pg-boss-handler-registry.d.ts +27 -0
  105. package/src/pg-boss-mount.d.ts +42 -0
  106. package/src/proposal-authorized-emails.d.ts +28 -0
  107. package/src/proposal-chat-inject.d.ts +17 -0
  108. package/src/proposal-gate.d.ts +18 -0
  109. package/src/read-html-template.d.ts +38 -0
  110. package/src/read-viewport-from-request.d.ts +18 -0
  111. package/src/readiness-gate.d.ts +16 -0
  112. package/src/register-api-routes.d.ts +18 -0
  113. package/src/register-crash-handlers.d.ts +15 -0
  114. package/src/register-page-routes.d.ts +27 -0
  115. package/src/register-page-routes.ts +3 -0
  116. package/src/register-page-server-client.d.ts +11 -0
  117. package/src/register-registry-page-route/ai-chat-prefetch.d.ts +13 -0
  118. package/src/register-registry-page-route/graph-cache.d.ts +30 -0
  119. package/src/register-registry-page-route/meta-title.d.ts +2 -0
  120. package/src/register-registry-page-route/proposal-approval-email-token.d.ts +14 -0
  121. package/src/register-registry-page-route/proposal-brand-stamping.d.ts +28 -0
  122. package/src/register-registry-page-route/resolve-page-data.d.ts +1 -0
  123. package/src/register-registry-page-route.d.ts +27 -0
  124. package/src/register-static-assets.d.ts +13 -0
  125. package/src/register-ws-routes.d.ts +28 -0
  126. package/src/resolve-app-base-url.d.ts +13 -0
  127. package/src/resolve-brand-favicon.d.ts +28 -0
  128. package/src/resolve-brand-favicon.ts +1 -1
  129. package/src/resolve-from-loader-config.d.ts +32 -0
  130. package/src/resolve-service-context.d.ts +17 -0
  131. package/src/resolve-service-port.d.ts +75 -0
  132. package/src/resolve-theme-mode.d.ts +42 -0
  133. package/src/resolvers/fetch-org-brand.d.ts +2 -0
  134. package/src/resolvers/portal-route-paths.d.ts +39 -0
  135. package/src/resolvers/website-by-host-map.d.ts +12 -0
  136. package/src/resource-chat-inject.d.ts +46 -0
  137. package/src/resource-not-found.d.ts +60 -0
  138. package/src/run-boot-hook-list.d.ts +19 -0
  139. package/src/run-boot-hooks.d.ts +31 -0
  140. package/src/scala-tools-currency-watchdog.d.ts +10 -0
  141. package/src/security-headers.d.ts +12 -0
  142. package/src/serve/serve.d.ts +46 -0
  143. package/src/serve/stamp-transport-identity.d.ts +32 -0
  144. package/src/serve/stamp-transport-identity.ts +2 -14
  145. package/src/server-error-page.d.ts +29 -0
  146. package/src/start-scheduled-jobs.d.ts +18 -0
  147. package/src/two-phase-boot.d.ts +136 -0
  148. package/src/two-phase-boot.ts +17 -85
  149. package/src/two-phase-health-gate.test.ts +2 -13
  150. package/src/types.d.ts +46 -0
  151. package/src/ui-service.d.ts +46 -0
  152. package/src/ws-boot-self-test.d.ts +19 -0
  153. package/src/auth-gate.test.ts +0 -70
  154. package/src/fn-routers/task-sync-lane.test.ts +0 -36
  155. package/src/handlers/sweep-host-cache.test.ts +0 -133
  156. package/src/serve/stamp-transport-identity.test.ts +0 -24
  157. package/src/two-phase-health-gate-client-only.test.ts +0 -77
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @system service-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * The AGREEMENT content gate, expressed as config for the content-gate primitive
7
+ * (content-gate/). The agreement-specific bits: which resource (the placed
8
+ * SignIn gate block's id), who's authorized (the signer roster), what to seed
9
+ * (the verified signer's signing session into every SignatureField), and how to
10
+ * withhold (strip the gate region to gate-only + strip {{agreement.*}} chrome).
11
+ * The engine handles cookie/token/verify/orchestration.
12
+ *
13
+ * The signing logic (buildSigningSession, seedSigningBlocks, roster expansion,
14
+ * placeholder strip) is preserved verbatim from the former gate-agreement-
15
+ * content.ts — only the cookie/token orchestration moved into the shared engine.
16
+ */
17
+ import type { AccessConfig, ContentGateConfig } from "./content-gate/config-types.ts";
18
+ export declare const agreementGateConfig: ContentGateConfig;
19
+ export declare const agreementAccessConfig: AccessConfig;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @system server-handlers
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * ETag derivation for the asset proxy that incorporates the negotiated
7
+ * image format so per-Accept variants cache independently.
8
+ */
9
+ export type TargetFormat = "webp" | "avif" | "jpeg" | "png" | "gif" | "svg" | "ico" | "original";
10
+ export declare function negotiateFormat(acceptHeader: string | null, storedContentType: string): TargetFormat;
11
+ export declare function buildEtag(baseEtag: string, format: TargetFormat): string;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @system server-handlers
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * On-the-fly image optimisation for the asset proxy. Converts stored
7
+ * PNG/JPEG to WebP or AVIF when the browser signals support, using
8
+ * @teamscala/image-optimizer's compressImage primitive. Returns null
9
+ * on failure so the caller can fall back to the original stream.
10
+ */
11
+ import type { TargetFormat } from "./asset-cache-key.ts";
12
+ export interface OptimizeResult {
13
+ data: Buffer;
14
+ format: string;
15
+ processedSize: number;
16
+ }
17
+ export declare function optimizeImage(buffer: Buffer, targetFormat: TargetFormat): Promise<OptimizeResult | null>;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @system server-handlers
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * Same-origin S3 asset proxy with on-the-fly image optimisation.
7
+ * Streams objects from the configured S3 bucket, converting PNG/JPEG
8
+ * to WebP/AVIF when the browser declares support via Accept header.
9
+ * Returns 404 for missing keys, 502 for S3 failures. Never redirects.
10
+ */
11
+ export declare function handleAssetProxy(req: Request): Promise<Response>;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @system server-handlers
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * Dynamic-import wrapper around image-optimizer's in-process sharp
7
+ * social-card op. Mirrors asset-optimize.ts's optional-dependency shape:
8
+ * services without image-optimizer installed transparently get null and
9
+ * fall back to the raw asset stream. In-process (not HTTP) because the
10
+ * og:image transform is a cold path per shared link — no auth handshake,
11
+ * no per-consumer service config.
12
+ */
13
+ export interface SocialCardOutcome {
14
+ data: Buffer;
15
+ processedSize: number;
16
+ }
17
+ export declare function renderSocialCard(buffer: Buffer): Promise<SocialCardOutcome | null>;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @system service-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * The agreement MAGIC-LINK endpoint. A recipient who clicks an emailed
7
+ * "Review & sign" link — which carries a pre-authorised, signed ag_token for
8
+ * THEIR party — lands here: the token is verified for the agreement, the
9
+ * ag_token cookie is set on the shared parent domain, and the browser is
10
+ * 302-redirected straight into the signing page. No email-entry gate, no OTP —
11
+ * "came from our email" IS the proof of identity, exactly like the
12
+ * proposal-accept hand-off (attach-proposal-accept.ts).
13
+ *
14
+ * The token is the same HMAC ag_token the gate verifies (30-min TTL, scoped to
15
+ * {agreementId, email}); an invalid/expired/forged token sets no cookie and the
16
+ * destination page simply shows the normal email gate. GET so it is a plain
17
+ * clickable link in an email.
18
+ */
19
+ import type { Hono } from "hono";
20
+ export declare function attachAgreementMagic(app: Hono, serviceSlug: string): Promise<void>;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @system service-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * Portal-secured member directory endpoint. Resolves members of an org via a
7
+ * parameterized raw join over better_auth_member + "user" — NOT the typed
8
+ * `member` ORPC model, which is deliberately omitted from the portal's
9
+ * service_schema (security: the raw table is not exposed as a typed CRUD
10
+ * surface). Mirrors the agreement-gate.ts:109-150 precedent.
11
+ *
12
+ * The portal's auth middleware (better-auth-cookie) gates the endpoint — only
13
+ * authenticated users can call it. The orgId comes from the caller's context
14
+ * (the portal resolves it from the session + passes it).
15
+ *
16
+ * Endpoint: GET /api/portal/members?orgId=<uuid>
17
+ * - orgId omitted: super-admin all-orgs mode (returns every member across
18
+ * all orgs, strictly gated to super-admin via getSession; used by the
19
+ * "View As" impersonation dropdown).
20
+ * Response: { members: Array<{ id, user_id, organization_id, role, email, name, image }> }
21
+ */
22
+ import type { Hono } from "hono";
23
+ export declare function attachPortalMembers(app: Hono): Promise<void>;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @system service-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * The proposal ACCEPT endpoint. A viewer who has passed the proposal gate (holds
7
+ * a valid prop_token) POSTs {proposalId} here to accept; the server generates a
8
+ * signing document from the proposal (createAgreementFromProposal) and
9
+ * 302-redirects them straight into signing it on the sign site. This replaces
10
+ * the old mailto CTA. Mounted BEFORE the auth gate so it is publicly reachable —
11
+ * the prop_token (not a login) is the proof of identity, exactly like the
12
+ * content-gate access endpoint.
13
+ *
14
+ * Single sign-on across the journey: the viewer already proved their email at
15
+ * the proposal gate (prop_token), so we MUST NOT make them re-enter it on the
16
+ * sign site. Before redirecting, we mint the agreement's ag_token for that same
17
+ * verified email and set it on the SHARED parent domain (Domain=.scala.business)
18
+ * so the browser carries it from proposals.scala.business to sign.scala.business
19
+ * — the agreement gate then auto-reveals + seeds the signing session with zero
20
+ * extra prompts. This requires both hosts to resolve the SAME token secret
21
+ * (AUTH_SECRET→ENCRYPTION_KEY→DATABASE_URL): scala-sign must carry ENCRYPTION_KEY
22
+ * (also required to decrypt the org's Stripe key on full sign), so both hosts
23
+ * land on ENCRYPTION_KEY and the carried token verifies.
24
+ */
25
+ import type { Hono } from "hono";
26
+ export declare function attachProposalAccept(app: Hono, serviceSlug: string): Promise<void>;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @system service-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * The proposal MAGIC-LINK endpoint — the proposal twin of attach-agreement-magic.
7
+ * A recipient who clicks an emailed "View your proposal" link (carrying a signed,
8
+ * per-recipient prop_token) lands here: the token is verified for the proposal,
9
+ * the prop_token cookie is set, and the browser is 302-redirected straight into
10
+ * the proposal page. No email-entry gate, no OTP. An invalid/expired token sets
11
+ * no cookie and the page falls through to the normal email gate.
12
+ *
13
+ * The prop_token is host-scoped (proposals are served on the proposals host only),
14
+ * unlike the agreement ag_token which spans .scala.business for the proposal→sign
15
+ * hand-off. GET so it is a plain clickable link in an email.
16
+ */
17
+ import type { Hono } from "hono";
18
+ export declare function attachProposalMagic(app: Hono, serviceSlug: string): Promise<void>;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @system service-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * Authenticated asset upload — POST /api/transfers/upload. Restores the
7
+ * handler deleted with @teamscala/server-files (commit ca5643ef); avatar/logo
8
+ * upload had 404'd on every service since. Writes to S3 via
9
+ * @teamscala/object-storage/create/write-ops and returns the STORED-form URL
10
+ * (`/api/files/<key>`) the client persists into avatar_url/logo_url — the
11
+ * display path rewrites it to the public `/api/assets/<encoded-key>` proxy
12
+ * via `toAssetPath` (@teamscala/formatters/asset-url). Self-authenticates
13
+ * (getSession) so it mounts BEFORE the auth gate.
14
+ *
15
+ * Key scheme is USER-scoped (`users/<userId>/<type>/<file>-<ts>.<ext>`) per
16
+ * the operator ruling: a profile picture belongs to the individual, not any
17
+ * org (the prior handler's org-scoped `<orgId>/avatars/...` encoded the
18
+ * uploading org into a personal asset). Spec + history:
19
+ * `reference/asset-upload-system.md`.
20
+ *
21
+ * `thumbUrl` mirrors `url` — the full image is served for both; a separate
22
+ * generated thumbnail is a future refinement (the prior handler generated
23
+ * one, but the image-resize dependency is not wired here yet and a missing
24
+ * thumb must not block the upload restoring).
25
+ */
26
+ import type { Hono } from "hono";
27
+ export declare function attachTransfersUpload(app: Hono): Promise<void>;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @system server-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * Auth middleware factory. Three modes, same BetterAuth email-OTP mechanism:
7
+ *
8
+ * service_install.auth values:
9
+ * - omitted | "none" → no gate, no handler (scala-public, scala-sign)
10
+ * - "better-auth-cookie" → full session auth: mount handler + session gate
11
+ * - "email-otp-only" → mount handler (OTP send/verify), no session gate
12
+ *
13
+ * The sign-in block determines the UX per mode:
14
+ * - "full" → email → OTP → session → portal selection (portal, agents, docs, gpt)
15
+ * - "email-gate" → email → OTP → verify-only, no session (forms, proposals, quiz)
16
+ * - "capture-only" → email field only, no OTP (sign)
17
+ *
18
+ * There is NO localhost bypass: 127.0.0.1 is gated identically to the tunnel
19
+ * (no session → /sign-in). Tools needing an authed view sign in
20
+ * (getSuperAdminSessionCookie); the dev OTP is DB-readable.
21
+ *
22
+ * When SCALA_OTP_URL is set, /api/auth/* forwards to the centralised
23
+ * scala-otp service (per otp-centralised-via-otp-service). Otherwise
24
+ * the local better-auth handler is used (dev-host fallback).
25
+ */
26
+ import type { ServiceInstallConfig } from "@teamscala/db/registry/loader/load-service-install-row";
27
+ import type { Hono } from "hono";
28
+ export declare function attachAuthGate(app: Hono, config: ServiceInstallConfig, serviceSlug: string): Promise<void>;
package/src/auth-gate.ts CHANGED
@@ -43,61 +43,6 @@ const SIGN_IN_PATHS = new Set([
43
43
  "/magic-link",
44
44
  ]);
45
45
 
46
- /// A STATIC ASSET IS NEVER A PROTECTED RESOURCE, so it is decided by EXTENSION
47
- /// rather than by remembering to list it.
48
- ///
49
- /// The allowlist below enumerates PREFIXES, and every path outside it is gated.
50
- /// That makes a new public asset directory private BY DEFAULT, and the failure
51
- /// lands exactly where it hurts most: on the SIGNED-OUT screen, which is the one
52
- /// page guaranteed to need branding. `/assets/` was listed; `/logos/` never was,
53
- /// so `/logos/cropped.png` answered 302 → /sign-in and the browser rendered a
54
- /// blank logo under a "starting up" splash. Favicons are on the list ONE FILE AT
55
- /// A TIME, which is the same defect already visible.
56
- ///
57
- /// This is why the operator reports it "keeps regressing": nothing is broken by
58
- /// a code change, the allowlist simply never covered the path, and any build that
59
- /// moves images re-creates it. Enumerating what is public is `prevention-over-
60
- /// detection` inverted — the cure is a predicate, not a longer list.
61
- ///
62
- /// Redirecting an <img> to a login page is never useful behaviour either: the
63
- /// browser cannot act on the redirect, so it silently paints nothing. Failing
64
- /// closed on a page is protection; failing closed on an image is just a blank.
65
- const STATIC_ASSET_EXTENSIONS = [
66
- ".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif", ".svg", ".ico", ".bmp",
67
- ".woff", ".woff2", ".ttf", ".otf", ".eot",
68
- ".css", ".js", ".mjs", ".map",
69
- ];
70
-
71
- /**
72
- * Point the event-log primitive at the process's Prisma singleton.
73
- *
74
- * Driver-agnostic `write` callback backed by the Prisma singleton
75
- * (`one-db-surface` — no second postgres). Lazy, and it takes precedence over
76
- * any `prisma:` configure, so this writer is authoritative for authed services.
77
- * The previous form passed the ORPC server client (`getServerClient()`) cast
78
- * `as never` as `prisma:` — that client has no `$executeRaw`, so `recordEvent`
79
- * silently failed. Routing it through ORPC would reintroduce exactly that.
80
- * Retention is owned by the event-log-sweep pg-boss job.
81
- *
82
- * EXTRACTED so its name states what it is. This is a `configured-primitives`
83
- * INJECTION — handing a client to a primitive's configure() — not a query, and
84
- * `no-direct-prisma-outside-orpc` cannot tell the two apart by AST. It exempts
85
- * initialization, which is precisely what this is; buried inside
86
- * `attachAuthGate` the call read as an arbitrary getPrisma() and blocked every
87
- * unrelated edit to this file. Naming the boot step honestly is the fix. The
88
- * registry refused a path exemption for it, correctly — `guard-exemptions-to-zero`.
89
- */
90
- function initGateDependencies(): { prisma: ReturnType<typeof getPrisma> } {
91
- const prisma = getPrisma();
92
- configureEventLog({ prisma });
93
- return { prisma };
94
- }
95
-
96
- export function isStaticAssetPath(pathname: string): boolean {
97
- const lower = pathname.toLowerCase();
98
- return STATIC_ASSET_EXTENSIONS.some((ext) => lower.endsWith(ext));
99
- }
100
-
101
46
  const PUBLIC_INFRA_PREFIXES = [
102
47
  "/assets/",
103
48
  "/api/assets/",
@@ -169,11 +114,12 @@ const PUBLIC_INFRA_PATHS = new Set([
169
114
  // the other dev-host infra reads.
170
115
  "/api/fleet/claude-sessions",
171
116
  // /api/fleet/ingest-event receives parsed lane-action events
172
- // (message.part.updated etc.) from the cli-session transcript-tail — a
117
+ // (message.part.updated etc.) from the cli-session Claude transcript-tail — a
173
118
  // dev-host INTERNAL machine caller that POSTs with no session cookie, exactly
174
119
  // like the lane-event hook. Without this exemption the page auth-gate 302s
175
120
  // every POST and the lane's tool/text action stream never reaches the
176
- // ingestEvent pipeline. A first-class
121
+ // ingestEvent pipeline (fleet_run_events stays empty → the dashboard console
122
+ // shows only lifecycle lines, never the per-tab action feed). A first-class
177
123
  // api_route (sibling to lane-event); security is the handler's loopback gate
178
124
  // on the platform's x-caller-remote-address (set by register-api-routes).
179
125
  "/api/fleet/ingest-event",
@@ -218,10 +164,7 @@ export async function attachAuthGate(
218
164
  // The previous form passed the ORPC server client (`getServerClient()`) cast
219
165
  // `as never` as `prisma:` — no `$executeRaw`, so recordEvent silently failed.
220
166
  // Retention is owned by the event-log-sweep pg-boss job.
221
- // Resolved ONCE at boot and closed over by the request path below.
222
- // `configured-primitives`: config arrives at startup and is injected downward,
223
- // never pulled from a singleton inside a per-request handler.
224
- const { prisma: gatePrisma } = initGateDependencies();
167
+ configureEventLog({ prisma: getPrisma() });
225
168
  const { registerAuthFactory, getAuth } = await import(
226
169
  "@teamscala/auth/auth-registry.ts"
227
170
  );
@@ -353,12 +296,7 @@ export async function attachAuthGate(
353
296
  const url = new URL(c.req.url);
354
297
  const isInfra =
355
298
  PUBLIC_INFRA_PATHS.has(url.pathname) ||
356
- PUBLIC_INFRA_PREFIXES.some((p) => url.pathname.startsWith(p)) ||
357
- // Decided by EXTENSION so a new asset directory cannot be private by
358
- // default. The enumerated prefix list is what left /logos/*.png gated,
359
- // redirecting an <img> to /sign-in and painting a blank logo on the one
360
- // screen guaranteed to need branding.
361
- isStaticAssetPath(url.pathname);
299
+ PUBLIC_INFRA_PREFIXES.some((p) => url.pathname.startsWith(p));
362
300
  if (isInfra) return next();
363
301
 
364
302
  // NO localhost bypass: a 127.0.0.1 page request is gated EXACTLY like a
@@ -422,7 +360,7 @@ export async function attachAuthGate(
422
360
  // caller of the single permission decision point (unified permission system
423
361
  // foundation, work_items 019f7d6b) — replaces the bespoke inline query.
424
362
  const decision = await resolveAccess({
425
- prisma: gatePrisma as never,
363
+ prisma: getPrisma() as never,
426
364
  userId: session.user.id,
427
365
  allowedOrganisationIds: allowed,
428
366
  });
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @system ui-service-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * Calls a createPageHandler() handler with the per-request extras the
7
+ * SSR pipeline needs: the resolved userId/orgId (from localhost-bypass
8
+ * or better-auth-cookie), and the htmlTemplate. Shared by the regions
9
+ * and page_slug shell variants.
10
+ */
11
+ import type { Context } from "hono";
12
+ type PageHandler = (req: Request, extras: Record<string, unknown>) => Promise<{
13
+ body: unknown;
14
+ status: number;
15
+ headers: HeadersInit;
16
+ }>;
17
+ export declare function bindPageHandler(c: Context, handler: PageHandler, authMode: string, serviceSlug: string, label: string, resourceOrgId?: string): Promise<Response>;
18
+ export {};
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @system service-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * Input hashing for the client-asset build fingerprint.
7
+ *
8
+ * Split out of build-client-assets.ts, which had grown to 1221 lines holding
9
+ * six distinct concerns (lock + status, manifest, hydrate bundle, source maps,
10
+ * stylesheet/tailwind, and this). `single-purpose-file` did not catch it: that
11
+ * rule flags a file whose top-level exports share no common name root, which
12
+ * measures export-NAME cohesion rather than how many concerns a file holds — so
13
+ * a large file with few tidily-named exports stays invisible to it.
14
+ *
15
+ * This module owns ONE concern: turning the build's real inputs into a stable
16
+ * hash. `computeInputFingerprint` (still in the parent, and still the only
17
+ * exported entry point) composes the two functions exported here; everything
18
+ * else is module-private because nothing outside needs it.
19
+ *
20
+ * Byte-for-byte move — imports adjusted, nothing else. No renames, no type
21
+ * changes (`migrations-copy-not-rewrite`).
22
+ */
23
+ import { createHash } from "node:crypto";
24
+ export declare function hashResolvedPackageStylesheets(cwd: string, h: ReturnType<typeof createHash>): Promise<void>;
25
+ export declare function hashTeamgbgJsGraph(cwd: string, h: ReturnType<typeof createHash>): Promise<void>;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @system service-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * Tailwind source-candidate scanning for the client CSS build.
7
+ *
8
+ * Split out of build-client-assets.ts (1221 lines, six concerns). Owns ONE
9
+ * concern: turning the resolved `@source` directories into the candidate token
10
+ * set Tailwind's `build()` consumes.
11
+ *
12
+ * Why it exists at all, preserved from the original: Tailwind v4's low-level
13
+ * `compile()` does NOT scan content, and @tailwindcss/oxide's native Scanner is
14
+ * not a dependency of the bare `tailwindcss` lib — only of the integration
15
+ * packages. So the tokens are extracted here. Over-extraction is safe, because
16
+ * `build()` ignores any token that is not a valid utility. Dropping this scan is
17
+ * what shipped an unstyled page to the operator's tablet on 2026-06-13; it is
18
+ * enforced by `css-build-scans-source-candidates`.
19
+ *
20
+ * Byte-for-byte move — imports adjusted, nothing else.
21
+ */
22
+ import { getAppLogger } from "@teamscala/logger/app-loggers";
23
+ export declare function scanSourceCandidates(sources: Array<{
24
+ base: string;
25
+ pattern: string;
26
+ negated?: boolean;
27
+ }>, log: ReturnType<typeof getAppLogger>): Promise<string[]>;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @system server-handlers
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * Builds the per-service client bundle and stylesheet into `dist/client/`
7
+ * at UI-service boot time. Outputs are content-hashed so deployments
8
+ * naturally cache-bust:
9
+ *
10
+ * src/entry-client.ts ──bun build──> dist/client/hydrate-<hash>.js
11
+ * src/app.css ──tailwindcss──> dist/client/styles-<hash>.css
12
+ * (and) dist/client/asset-manifest.json
13
+ *
14
+ * The HTML template references `{{stylesPath}}` / `{{hydratePath}}` —
15
+ * read-html-template.ts substitutes from the manifest at template-read
16
+ * time. Bytes-level changes → new hash → new URL → browser refetch.
17
+ * Without this, `cache-control: immutable` on a fixed filename means
18
+ * returning visitors keep their stale copy forever.
19
+ *
20
+ * Content-fingerprint cache: the CLIENT-BUNDLE DEPENDENCY CLOSURE (source
21
+ * entrypoints + every @teamscala/* module/stylesheet the bundle reaches + the
22
+ * resolved versions of the third-party deps the closure imports) is hashed by
23
+ * CONTENT and stored in the manifest; the rebuild is skipped only when the
24
+ * current closure hashes to the same fingerprint (cache-key-hashes-source-
25
+ * inputs-only — never mtime; see computeInputFingerprint for why mtime silently
26
+ * served stale bundles on Railway). bun.lock is deliberately NOT hashed — it
27
+ * encodes every @teamscala/* version including backend packages outside this
28
+ * service's client bundle, so folding it in would rebuild on every publish
29
+ * (the publish→rebuild storm; `no-uncontrolled-repetition-or-cascade`). First
30
+ * pm2 restart after a real closure change pays the full build cost; unchanged
31
+ * restarts skip. Stale hashed outputs from a prior build are removed in the
32
+ * same pass so dist/client/ doesn't accumulate dead files.
33
+ */
34
+ /**
35
+ * Read ONLY the tombstone state of the current manifest — the runtime
36
+ * (read-html-template, ui-service) calls this to decide whether to surface a
37
+ * build-failure signal. Returns null when the manifest is fresh (no
38
+ * `buildFailed`). Decoupled from the full AssetManifest read so the runtime
39
+ * never depends on the build internals.
40
+ */
41
+ export declare function readClientBuildStatus(cwd?: string): Promise<{
42
+ failed: true;
43
+ reason?: string;
44
+ at?: string;
45
+ } | {
46
+ failed: false;
47
+ }>;
48
+ export declare function stripRemoteImports(css: string): string;
49
+ export declare function computeInputFingerprint(cwd: string): Promise<string>;
50
+ export declare function buildClientAssets(serviceSlug: string): Promise<void>;
@@ -167,7 +167,7 @@ describe("computeInputFingerprint", () => {
167
167
  });
168
168
 
169
169
  // Regression guard for the 2026-07-12 scala-agents stale-bundle bug. A boot
170
- // that builds MID-INSTALL (dep-sync restarts after writing the new
170
+ // that builds MID-INSTALL (package-poller restarts after writing the new
171
171
  // bun.lock but before node_modules extraction finishes) bakes OLD @teamscala/*
172
172
  // JS while the lock-derived fingerprint records "current" — so every later
173
173
  // restart recomputes the same fingerprint, skips the rebuild, and serves the
@@ -84,7 +84,7 @@ export async function readClientBuildStatus(
84
84
 
85
85
  // Single shared host lock serialising the heavy boot-time client build across
86
86
  // ALL kind:ui services (client-asset-builds-serialize-via-lock). A publish of a
87
- // widely-consumed package makes the dep-sync restart every consumer at
87
+ // widely-consumed package makes the package-poller restart every consumer at
88
88
  // once; without this lock each one rebuilds its hydrate bundle + Tailwind CSS
89
89
  // simultaneously and N concurrent builds saturate the host (load 36, each ~15s
90
90
  // build starved to ~10min, services offline — 2026-06-13). The build sibling of
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @system ui-service-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * The single canonical page-title composer. Every SSR page handler routes
7
+ * through this (portal/region-shell, public shell, single-page resource). A
8
+ * page title is DERIVED from data that already exists — the website name +
9
+ * the page/entity name — with `builda_pages.meta_title` as an explicit
10
+ * per-page OVERRIDE only, never the primary source.
11
+ *
12
+ * Mirrors the brand-is-org-owned-full-scheme precedent: a value that should
13
+ * be DERIVED was instead hand-set per row, and when unset it silently fell
14
+ * back to something wrong. For themes the wrong fallback was the generic
15
+ * light palette; for titles it was the bare org name — the `default`
16
+ * page_meta_template `titleTemplate: "{org.name}"` produced "GBG Indo" with
17
+ * no site or page context, and per-row `meta_title` carried stale product
18
+ * literals like "Dashboard | scala Hub". The fix is identical in shape:
19
+ * derive by default, override explicitly.
20
+ *
21
+ * Canonical format: `<website.name>: <subject>` (subject = entity title or
22
+ * page name), site-first so a browser tab reads the surface then the page.
23
+ * A resource surface (forms/quiz/gpt) may supply a curated `templateTitle`
24
+ * (e.g. "{agent.name} — {org.name}"); it wins over the default derivation
25
+ * but NOT over an explicit per-page override. The composer NEVER returns a
26
+ * bare org name — that is the regression this closes, and the
27
+ * `page-title-not-bare-org` guard backstops it at the template layer.
28
+ */
29
+ export interface TitleContext {
30
+ /** Per-page override (builda_pages.meta_title). Wins outright when set. */
31
+ pageMetaTitle?: string | null;
32
+ /** Resolved resource-template string (forms/quiz/gpt). Curated per-surface shape. */
33
+ templateTitle?: string | null;
34
+ /** builda_websites.name — the surface, the primary site context. */
35
+ websiteName?: string | null;
36
+ /** builda_pages.name — the page (e.g. "Dashboard"). */
37
+ pageName?: string | null;
38
+ /** Resource entity title (proposal/form/quiz/agent name) — preferred subject. */
39
+ entityTitle?: string | null;
40
+ }
41
+ export declare function composePageTitle(ctx: TitleContext): string;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @system service-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * The content-gate ACCESS endpoint mounter. One generic POST handler for every
7
+ * gate: a plain <form> posts {<recordIdField>, email}; on a match against the
8
+ * resource's authorized set it sets the signed access cookie and redirects back,
9
+ * so the page loader's gate then releases the content. No bespoke endpoint per
10
+ * type — each gate is one AccessConfig. Mounted BEFORE the auth gate so it is
11
+ * publicly reachable.
12
+ */
13
+ import type { Hono } from "hono";
14
+ import type { AccessConfig } from "./config-types.ts";
15
+ export declare function attachContentGateAccess(app: Hono, serviceSlug: string, config: AccessConfig): Promise<void>;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * @system service-boot
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
+ */
11
+ type Resolved = Record<string, unknown>;
12
+ /** Drives the SSR-time gate (gate.ts / runContentGate). */
13
+ export interface ContentGateConfig {
14
+ /** Cookie that carries this gate's access token (e.g. "ag_token"). */
15
+ cookieName: string;
16
+ /**
17
+ * The gated resource id from the resolved graph, or "" when this gate does
18
+ * not apply to the current page (the engine then no-ops). This is also how a
19
+ * gate detects "is this my kind of page". Async to allow DB-backed resolution
20
+ * (e.g. agreement gate resolving the UUID from the page→agreement association
21
+ * when no pre-placed gate block exists).
22
+ */
23
+ getResourceId(resolved: Resolved): string | Promise<string>;
24
+ /**
25
+ * Verify the cookie's token for this resource → the verified identity
26
+ * ({email, optional display name}), or null. Token-agnostic by design:
27
+ * proposals use the shared content-gate token; agreements keep their existing
28
+ * token (also minted by the signing ORPC), so the engine never assumes one
29
+ * token format. Allow-list gates return just {email}; the GPT lead-capture
30
+ * gate also returns the captured name (carried in its token).
31
+ */
32
+ verifyToken(token: string | undefined, resourceId: string, nowMs: number): {
33
+ email: string;
34
+ name?: string;
35
+ } | null;
36
+ /**
37
+ * BAKED-IN unified auth. When a gate provides this resolver, the engine
38
+ * AUTOMATICALLY releases a logged-in viewer whose session email is in the
39
+ * authorized set — with no valid token and no per-gate code. This is the
40
+ * platform default for EVERY gated resource: any application surface with
41
+ * authentication gets "a signed-in, authorized viewer is never re-asked for
42
+ * their email/OTP" for free, just by exposing its authorized-email set (the
43
+ * same resolver its AccessConfig uses). Not called when the session is
44
+ * anonymous. Omit only for open-capture gates that have no authorized set.
45
+ */
46
+ resolveAuthorizedEmails?(resourceId: string): Promise<Set<string>>;
47
+ /**
48
+ * OPTIONAL OVERRIDE of the baked-in session check above — for the rare gate
49
+ * whose session→identity rule isn't simply "session email ∈ authorized set".
50
+ * When present it wins; otherwise the engine uses `resolveAuthorizedEmails`.
51
+ * Called only when no valid access token is present.
52
+ */
53
+ sessionAuthorize?(resolved: Resolved, resourceId: string): {
54
+ email: string;
55
+ name?: string;
56
+ } | null | Promise<{
57
+ email: string;
58
+ name?: string;
59
+ } | null>;
60
+ /** Reveal + seed the verified viewer's identity/session (token was valid). */
61
+ onAuthorized(resolved: Resolved, resourceId: string, identity: {
62
+ email: string;
63
+ name?: string;
64
+ }): void | Promise<void>;
65
+ /** Withhold content (strip/inject the gate) — token absent/invalid. Async-
66
+ * capable so a gate can load its display/behaviour settings from a registry
67
+ * row (DB-driven, not hardcoded); sync implementations remain valid. */
68
+ onUnauthorized(resolved: Resolved, resourceId: string): void | Promise<void>;
69
+ }
70
+ /** Drives the public access endpoint (access.ts / attachContentGateAccess). */
71
+ export interface AccessConfig {
72
+ /** Route to mount, e.g. "/api/proposal-access". */
73
+ path: string;
74
+ /** Cookie to set on a match. */
75
+ cookieName: string;
76
+ /** Cookie lifetime in ms (Max-Age). */
77
+ ttlMs: number;
78
+ /** Form field carrying the resource id, e.g. "proposalId". */
79
+ recordIdField: string;
80
+ /**
81
+ * Open lead-capture gate: ANY non-empty email is accepted (no allow-list).
82
+ * For surfaces whose "gate" captures a visitor rather than authorizing one
83
+ * against a fixed set (the public GPT). When true, `resolveAuthorizedEmails`
84
+ * is not called. Default/omitted = allow-list mode (proposals, agreements).
85
+ */
86
+ openCapture?: boolean;
87
+ /**
88
+ * The authorized email set for a resource (lowercased), checked server-side.
89
+ * Not called when `openCapture` is true.
90
+ */
91
+ resolveAuthorizedEmails(resourceId: string): Promise<Set<string>>;
92
+ /**
93
+ * Mint the access token for a matched email (token-agnostic per gate).
94
+ * `name` is the optional captured display name (open-capture gates carry it
95
+ * in the token; allow-list gates ignore it).
96
+ */
97
+ signToken(resourceId: string, email: string, nowMs: number, name?: string): string | null;
98
+ }
99
+ export {};