@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,25 @@
1
+ /**
2
+ * @system service-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * ORPC procedures for task creation with initial comment.
7
+ * Extracted from @teamscala/orpc/fn-router — primitives tier must not
8
+ * contain application-specific work_items logic.
9
+ */
10
+ import { type Context } from "./auth-router.ts";
11
+ export declare const tasksRouter: {
12
+ createWithComment: import("@orpc/server").DecoratedProcedure<import("@orpc/server").MergedInitialContext<Context & Record<never, never>, Context, Context>, import("@orpc/server").MergedCurrentContext<Context, {
13
+ headers: Headers;
14
+ resolveUser?: (headers: Headers, prisma: any) => Promise<import("./auth-router.ts").User | null>;
15
+ prisma: any;
16
+ user: import("./auth-router.ts").User;
17
+ orgId: string | null | undefined;
18
+ }>, import("@orpc/contract").Schema<unknown, unknown>, import("@orpc/contract").Schema<{
19
+ success: boolean;
20
+ data: any;
21
+ }, {
22
+ success: boolean;
23
+ data: any;
24
+ }>, Record<never, never>, Record<never, never>>;
25
+ };
@@ -0,0 +1,77 @@
1
+ /**
2
+ * @system service-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * ORPC procedures for a GENERIC per-user preference store backed by the
7
+ * `user_preferences` table. The user is resolved from the ORPC session context
8
+ * (`authed` middleware → `context.user.id`); the client supplies the org + key
9
+ * + value. This is the DB-durable, user-scoped key/value surface any feature
10
+ * can use — the AI browser-control focus (key `ai_ui_focus`) is the first
11
+ * consumer; it is NOT scala-projects-bespoke.
12
+ *
13
+ * Why this exists (over the generated `user_preferences` CRUD): the generated
14
+ * create/update do NOT resolve `user_id` server-side (only `organisation_id`
15
+ * from ctx.orgId), so a client cannot correctly write a per-user row through
16
+ * them. This proc stamps `user_id` from the authenticated caller, making the
17
+ * write safe + per-user. It also emits the `data_change` notify so the
18
+ * realtime surface (regions.watchDataChange) pushes updates to subscribed
19
+ * clients — the cross-region channel for the AI focus (the AiChat panel writes
20
+ * a focus here; the project-list-view in a different region subscribes +
21
+ * reacts, no shared React-context ancestor needed).
22
+ *
23
+ * Ownership is user_id: a caller reads/writes ONLY their own preferences. The
24
+ * org is part of the compound key (per-org isolation) but is client-supplied
25
+ * (the session carries the user, not the active org).
26
+ *
27
+ * DECISION — host-scoping of `user_preferences`: the table is NOT yet a shared
28
+ * `@teamscala/db` model (it lives in scala-projects's schema today), so this proc
29
+ * is genuinely host-scoped: it runs correctly on services whose Prisma client
30
+ * declares `user_preferences` (scala-projects is the first host) + is inert
31
+ * elsewhere. `Context.prisma` is `any`-typed, so the proc COMPILES workspace-
32
+ * wide (no type error on a model a host may not declare) — the `any` is the
33
+ * deliberate seam that lets one generic proc serve hosts incrementally as they
34
+ * adopt the table. Per-user preferences are platform-universal, so the tracked
35
+ * follow-on is to promote `user_preferences` to a shared `@teamscala/db` model
36
+ * (open-migration: `user_preferences→shared-model`, exit: every kind:ui host
37
+ * can mount this proc with no schema precondition) — at which point the proc is
38
+ * universally hostable + the `any` seam can tighten to the shared client. Until
39
+ * then the proc is opt-in per host (declare the table → get the capability).
40
+ */
41
+ import * as v from "valibot";
42
+ import { type Context } from "./auth-router.ts";
43
+ export declare const userPreferencesRouter: {
44
+ upsert: import("@orpc/server").DecoratedProcedure<import("@orpc/server").MergedInitialContext<Context & Record<never, never>, Context, Context>, import("@orpc/server").MergedCurrentContext<Context, {
45
+ headers: Headers;
46
+ resolveUser?: (headers: Headers, prisma: any) => Promise<import("./auth-router.ts").User | null>;
47
+ prisma: any;
48
+ user: import("./auth-router.ts").User;
49
+ orgId: string | null | undefined;
50
+ }>, v.ObjectSchema<{
51
+ readonly organisationId: v.StringSchema<undefined>;
52
+ /** The preference key (e.g. `ai_ui_focus`). Namespaced by feature. */
53
+ readonly key: v.StringSchema<undefined>;
54
+ /** The JSON value. Arbitrary per feature — typed by the caller. */
55
+ readonly value: v.AnySchema;
56
+ }, undefined>, import("@orpc/contract").Schema<{
57
+ success: boolean;
58
+ }, {
59
+ success: boolean;
60
+ }>, Record<never, never>, Record<never, never>>;
61
+ get: import("@orpc/server").DecoratedProcedure<import("@orpc/server").MergedInitialContext<Context & Record<never, never>, Context, Context>, import("@orpc/server").MergedCurrentContext<Context, {
62
+ headers: Headers;
63
+ resolveUser?: (headers: Headers, prisma: any) => Promise<import("./auth-router.ts").User | null>;
64
+ prisma: any;
65
+ user: import("./auth-router.ts").User;
66
+ orgId: string | null | undefined;
67
+ }>, v.ObjectSchema<{
68
+ readonly organisationId: v.StringSchema<undefined>;
69
+ readonly key: v.StringSchema<undefined>;
70
+ }, undefined>, import("@orpc/contract").Schema<{
71
+ value: any;
72
+ updatedAt: any;
73
+ }, {
74
+ value: any;
75
+ updatedAt: any;
76
+ }>, Record<never, never>, Record<never, never>>;
77
+ };
@@ -0,0 +1,102 @@
1
+ /**
2
+ * @system service-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * ORPC procedure for safely deleting a work_item (initiative/project/task).
7
+ * Refuses the delete when the row still has child rows (parent_id = id), so a
8
+ * delete can NEVER orphan its descendants — the tree must be emptied bottom-up.
9
+ *
10
+ * Initiatives, projects and tasks are all rows of the ONE `work_items` table
11
+ * discriminated by `kind`, and `parent_id` carries no DB-level foreign key, so
12
+ * a plain `work_items.delete` leaves dangling children with no referential
13
+ * integrity to stop it. This one guard covers every level (you may only delete
14
+ * a leaf — a node with no children), which is exactly the "empty it first"
15
+ * behaviour the board wants.
16
+ */
17
+ import { type Context } from "./auth-router.ts";
18
+ export declare const workItemsRouter: {
19
+ /**
20
+ * Fired client-side right after a task's status is set to
21
+ * "pending_approval" (RaCI: the task's assignee is Responsible; the
22
+ * project's Accountable person is the approver). Walks parent_id up to
23
+ * the nearest project-kind ancestor, emails every accountable person on
24
+ * it. Never throws to the caller — a notification failure must not block
25
+ * the status change that already committed.
26
+ */
27
+ requestApproval: import("@orpc/server").DecoratedProcedure<import("@orpc/server").MergedInitialContext<Context & Record<never, never>, Context, Context>, import("@orpc/server").MergedCurrentContext<Context, {
28
+ headers: Headers;
29
+ resolveUser?: (headers: Headers, prisma: any) => Promise<import("./auth-router.ts").User | null>;
30
+ prisma: any;
31
+ user: import("./auth-router.ts").User;
32
+ orgId: string | null | undefined;
33
+ }>, import("@orpc/contract").Schema<unknown, unknown>, import("@orpc/contract").Schema<{
34
+ notified: number;
35
+ }, {
36
+ notified: number;
37
+ }>, Record<never, never>, Record<never, never>>;
38
+ /**
39
+ * Fired client-side right after a comment is created, when the comment
40
+ * text contains an @mention the client already resolved to a team-member
41
+ * id (client does the name matching against the people list it already
42
+ * has loaded; this handler only sends). Never throws to the caller for
43
+ * same reason as requestApproval -- a notification failure must not
44
+ * surface as a failed comment post.
45
+ */
46
+ notifyMentions: import("@orpc/server").DecoratedProcedure<import("@orpc/server").MergedInitialContext<Context & Record<never, never>, Context, Context>, import("@orpc/server").MergedCurrentContext<Context, {
47
+ headers: Headers;
48
+ resolveUser?: (headers: Headers, prisma: any) => Promise<import("./auth-router.ts").User | null>;
49
+ prisma: any;
50
+ user: import("./auth-router.ts").User;
51
+ orgId: string | null | undefined;
52
+ }>, import("@orpc/contract").Schema<unknown, unknown>, import("@orpc/contract").Schema<{
53
+ notified: number;
54
+ }, {
55
+ notified: number;
56
+ }>, Record<never, never>, Record<never, never>>;
57
+ /**
58
+ * Org-scoped member list for the task-comments panel — @mention matching AND
59
+ * comment-sender display (name + avatar). ChatPanel.tsx has no
60
+ * serverLoader/orgId prop (client-only overlay content), so it can't do the
61
+ * SSR raw-SQL org-scope join load-project-list-view.ts uses; this exposes
62
+ * the same result via a custom procedure instead of widening service_schema
63
+ * to the raw better_auth_member table.
64
+ *
65
+ * This is the SINGLE source of member display info for the comments panel:
66
+ * it coalesces BOTH name/email/avatar families on the user model into one
67
+ * shape (the `full_name`/`primary_email`/`avatar_url` family AND the
68
+ * better-auth `name`/`email`/`image` family). Returning avatar here lets the
69
+ * client resolve sender name+avatar from this one list and drop the separate
70
+ * `user.findMany` enrichment fetch it previously ran as a second waterfall
71
+ * hop after `comments.findMany` (loading-shape: one wave-parallel hit, not a
72
+ * dependent client-side round-trip chain).
73
+ */
74
+ orgMembers: import("@orpc/server").DecoratedProcedure<import("@orpc/server").MergedInitialContext<Context & Record<never, never>, Context, Context>, import("@orpc/server").MergedCurrentContext<Context, {
75
+ headers: Headers;
76
+ resolveUser?: (headers: Headers, prisma: any) => Promise<import("./auth-router.ts").User | null>;
77
+ prisma: any;
78
+ user: import("./auth-router.ts").User;
79
+ orgId: string | null | undefined;
80
+ }>, import("@orpc/contract").Schema<unknown, unknown>, import("@orpc/contract").Schema<{
81
+ id: string;
82
+ name: string;
83
+ email: string;
84
+ avatar: string | null;
85
+ }[], {
86
+ id: string;
87
+ name: string;
88
+ email: string;
89
+ avatar: string | null;
90
+ }[]>, Record<never, never>, Record<never, never>>;
91
+ deleteEmpty: import("@orpc/server").DecoratedProcedure<import("@orpc/server").MergedInitialContext<Context & Record<never, never>, Context, Context>, import("@orpc/server").MergedCurrentContext<Context, {
92
+ headers: Headers;
93
+ resolveUser?: (headers: Headers, prisma: any) => Promise<import("./auth-router.ts").User | null>;
94
+ prisma: any;
95
+ user: import("./auth-router.ts").User;
96
+ orgId: string | null | undefined;
97
+ }>, import("@orpc/contract").Schema<unknown, unknown>, import("@orpc/contract").Schema<{
98
+ success: boolean;
99
+ }, {
100
+ success: boolean;
101
+ }>, Record<never, never>, Record<never, never>>;
102
+ };
@@ -0,0 +1,6 @@
1
+ /** Populate the TTL cache from the registry rows. Idempotent; keeps defaults on
2
+ * any miss so the gates ALWAYS render (protective-layers-fail-explicitly). */
3
+ export declare function initGateTtls(): Promise<void>;
4
+ export declare function agTtlMs(): number;
5
+ export declare function propTtlMs(): number;
6
+ export declare function gptTtlMs(): number;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @system service-boot
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
+ */
23
+ import type { AccessConfig, ContentGateConfig } from "./content-gate/config-types.ts";
24
+ export declare const gptGateConfig: ContentGateConfig;
25
+ export declare const gptAccessConfig: AccessConfig;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @system server-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * The `projects-daily-digest` / `projects-weekly-digest` pg-boss handlers for
7
+ * scala-projects. Same fixed-query shape as prune-stale-data: no row-driven
8
+ * SQL, every query is a literal Prisma call against work_items; the
9
+ * scheduled_job row only carries the cron.
10
+ *
11
+ * Daily digest: per assignee, tasks assigned to them created in the last 24h.
12
+ * Weekly digest: per assignee, their overdue tasks + tasks due in the next 7
13
+ * days. Both group work_items by assignee_id so one org-wide query covers
14
+ * every member, then send one email per assignee via the
15
+ * projects-daily-digest / projects-weekly-digest email_template rows.
16
+ *
17
+ * Registered under `@teamscala/service-boot:projects-daily-digest` and
18
+ * `@teamscala/service-boot:projects-weekly-digest`.
19
+ */
20
+ import type { ScheduledJobConfig } from "@teamscala/db-validation/registry-schemas/scheduled-job";
21
+ export declare function sendDailyDigest(_ctx: {
22
+ config: ScheduledJobConfig;
23
+ }): Promise<void>;
24
+ export declare function sendWeeklyDigest(_ctx: {
25
+ config: ScheduledJobConfig;
26
+ }): Promise<void>;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @system server-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * The `db-prune-stale-data` pg-boss handler. Reads the `retention` map from the
7
+ * `scheduled_job` row config and prunes stale rows per entry.
8
+ *
9
+ * Writes use Prisma ORM methods (deleteMany/updateMany) via the prisma-registry
10
+ * client — NEVER raw SQL. The `@teamscala/db` client carries the `blockRawSqlWrites`
11
+ * audit extension: `$executeRaw`/`$executeRawUnsafe` are blocked so every write
12
+ * is audited; ORM methods are the canonical audited path.
13
+ *
14
+ * SAFETY — identifier allowlist: each retention key selects a FIXED, hardcoded
15
+ * Prisma model + field (table/field names are literal in code, never derived
16
+ * from the row); the row's value (a duration string) is parsed to a cutoff Date.
17
+ * A row can therefore never inject arbitrary SQL or address an unvetted model.
18
+ * Keys not in the allowlist are skipped + logged.
19
+ *
20
+ * `audit_trail.*` is INTENTIONALLY EXCLUDED: `audit-trail-permanent` (critical)
21
+ * forbids any DELETE FROM audit_trail. The legacy row's
22
+ * `audit_trail.sync_entities: "delete all"` entry is stale drift from before
23
+ * the 2026-06-19 hardening — surfaced (skipped + warned), never executed.
24
+ *
25
+ * Registered under slug `@teamscala/service-boot:prune-stale-data`.
26
+ */
27
+ import type { ScheduledJobConfig } from "@teamscala/db-validation/registry-schemas/scheduled-job";
28
+ export declare function pruneStaleData({ config, }: {
29
+ config: ScheduledJobConfig;
30
+ }): Promise<void>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -16,14 +16,8 @@
16
16
  * Path convention (config.retention keys, substituted at runtime via `userHome()`
17
17
  * / `workspaceRoot()` per `host-literals-via-substitution` — source never
18
18
  * hardcodes host paths):
19
- * "${HOME}/.bun/install/cache" → "30 days"
20
- * "${HOME}/.config/*.retired-*" → "14 days" (glob leaf, optional)
21
- *
22
- * config.retention_dirs — whole-DIRECTORY prune for build caches (Rust
23
- * `target/`) whose unit is the dir, not the file (file-by-mtime would corrupt
24
- * the cache). Keys support `*` globs; a matched dir is deleted ENTIRELY only
25
- * when its newest file is older than the cutoff:
26
- * "${WORKSPACE}/<repo>/target" → "30 days" (glob wildcard, whole-dir delete)
19
+ * "${HOME}/.bun/install/cache" → "30 days"
20
+ * "${HOME}/.config/*.retired-*" → "14 days" (glob leaf, optional)
27
21
  *
28
22
  * Deletion runs through Bun fs APIs — pure filesystem deletion needs no shell,
29
23
  * so `spawn-is-the-only-spawn` does not apply (that governs shell commands).
@@ -40,7 +34,7 @@
40
34
  * Registered under slug `@teamscala/service-boot:sweep-host-cache`.
41
35
  */
42
36
  import { rm, readdir, stat } from "node:fs/promises";
43
- import { readdirSync, type Dirent } from "node:fs";
37
+ import type { Dirent } from "node:fs";
44
38
  import { join } from "node:path";
45
39
  import { spawnSync } from "@teamscala/os/spawn/spawn";
46
40
  import { userHome, workspaceRoot } from "@teamscala/os/host-paths";
@@ -123,97 +117,11 @@ async function sweepPath(
123
117
  return { path: rootDir, scanned, deleted, bytesFreed };
124
118
  }
125
119
 
126
- /**
127
- * Expand a path pattern containing single-segment `*` wildcards into concrete
128
- * paths (files or dirs). Used by `retention_dirs` so a single config key like
129
- * a pattern like `${WORKSPACE}/<star>/target` reaches every repo's target dir.
130
- * Sync + bounded: the
131
- * pattern is shallow (3 segments), and this runs once
132
- * nightly. Non-existent segments yield no matches (the caller stats each result).
133
- */
134
- export function expandGlob(pattern: string): string[] {
135
- const absolute = pattern.startsWith("/");
136
- const segments = (absolute ? pattern.slice(1) : pattern)
137
- .split("/")
138
- .filter((s) => s.length > 0);
139
- let current: string[] = [absolute ? "/" : "."];
140
- for (const seg of segments) {
141
- if (current.length === 0) break;
142
- const next: string[] = [];
143
- for (const dir of current) {
144
- let names: string[];
145
- try {
146
- names = readdirSync(dir);
147
- } catch {
148
- continue;
149
- }
150
- for (const name of names) {
151
- if (seg === "*" || seg === name) next.push(join(dir, name));
152
- }
153
- }
154
- current = next;
155
- }
156
- return current;
157
- }
158
-
159
- /**
160
- * Is `dir` stale — i.e. NO file under it is newer than `cutoffMs`? Walks
161
- * recursively and SHORT-CIRCUITS the instant it finds a fresh file (so an
162
- * actively-built target dir returns fast without walking its full tree). On a
163
- * stale dir the walk is complete and returns the total size + file count, so
164
- * the caller can report bytes-freed before deleting the dir whole.
165
- */
166
- export async function checkDirStale(
167
- dir: string,
168
- cutoffMs: number,
169
- ): Promise<{ stale: boolean; bytes: number; fileCount: number }> {
170
- const stack: string[] = [dir];
171
- let bytes = 0;
172
- let fileCount = 0;
173
- while (stack.length > 0) {
174
- const d = stack.pop();
175
- if (d === undefined) break;
176
- let entries: Dirent[];
177
- try {
178
- entries = await readdir(d, { withFileTypes: true });
179
- } catch {
180
- continue;
181
- }
182
- for (const entry of entries) {
183
- const full = join(d, entry.name);
184
- if (entry.isDirectory()) {
185
- stack.push(full);
186
- } else if (entry.isFile()) {
187
- try {
188
- const st = await stat(full);
189
- if (st.mtimeMs >= cutoffMs) {
190
- return { stale: false, bytes: 0, fileCount: 0 };
191
- }
192
- bytes += st.size;
193
- fileCount++;
194
- } catch {
195
- // vanished/unreadable — skip
196
- }
197
- }
198
- }
199
- }
200
- return { stale: true, bytes, fileCount };
201
- }
202
-
203
- /** Parse the root-fs Use% from `df --output=pcent,target` output (pure, for tests).
204
- * Returns null if no root-mount line is found. */
205
- export function parseRootDiskPct(out: string): number | null {
206
- const rootLine = out.split("\n").find((l) => l.trim().endsWith("/"));
207
- const m = rootLine?.match(/(\d+)%/);
208
- return m ? Number(m[1]) : null;
209
- }
210
-
211
120
  async function sweepHostCache(ctx: {
212
- config: { retention?: Record<string, string>; retention_dirs?: Record<string, string> };
121
+ config: { retention?: Record<string, string> };
213
122
  }): Promise<void> {
214
123
  const log = getAppLogger();
215
124
  const retention = ctx.config.retention ?? {};
216
- const retentionDirs = ctx.config.retention_dirs ?? {};
217
125
  const now = Date.now();
218
126
  const results: SweepResult[] = [];
219
127
 
@@ -246,36 +154,6 @@ async function sweepHostCache(ctx: {
246
154
  );
247
155
  }
248
156
 
249
- // retention_dirs — whole-directory prune for build caches (Rust target/).
250
- // A matched dir is deleted ENTIRELY only when its newest file is older than
251
- // the cutoff, so an actively-built repo keeps its cache and an abandoned one
252
- // is reclaimed whole (never the partial slice that would corrupt the cache).
253
- for (const [rawPattern, rawDuration] of Object.entries(retentionDirs)) {
254
- const days = parseDays(rawDuration);
255
- if (days === null || days <= 0) {
256
- log.warn(
257
- `[host-cache-sweep] skipping retention_dirs "${rawPattern}" — value "${rawDuration}" is not a day-granularity duration (expected e.g. "30 days")`,
258
- );
259
- continue;
260
- }
261
- const cutoffMs = now - days * MS_PER_DAY;
262
- for (const dir of expandGlob(resolveHostPath(rawPattern))) {
263
- try {
264
- const st = await stat(dir);
265
- if (!st.isDirectory()) continue;
266
- } catch {
267
- continue; // glob match that is not a dir (or vanished) — skip
268
- }
269
- const { stale, bytes, fileCount } = await checkDirStale(dir, cutoffMs);
270
- if (!stale) continue;
271
- await rm(dir, { recursive: true, force: true });
272
- results.push({ path: dir, scanned: fileCount, deleted: fileCount, bytesFreed: bytes });
273
- log.info(
274
- `[host-cache-sweep] ${dir}: no build in ${rawDuration}; reclaimed whole dir — ${fileCount} file(s), ${formatBytes(bytes)}`,
275
- );
276
- }
277
- }
278
-
279
157
  const totalDeleted = results.reduce((sum, r) => sum + r.deleted, 0);
280
158
  const totalBytes = results.reduce((sum, r) => sum + r.bytesFreed, 0);
281
159
  log.info(
@@ -298,27 +176,32 @@ async function sweepHostCache(ctx: {
298
176
  },
299
177
  });
300
178
 
301
- // Disk-threshold observability — rides this periodic sweep (NOT an interval:
302
- // this job is genuinely periodic application-DATA work — deletion — and the
303
- // df read piggybacks once per run). Emits through the event-driven surface:
304
- // logger.warn -> GlitchTip (the paging path, same as host.pressure) +
305
- // recordEvent for the durable event_log record. Catches a fill faster than
306
- // the nightly prune can absorb (a cache with no retention entry, an external
307
- // dump, a runaway log) so the operator learns it within a day, not at 100%.
308
- const diskHighPct = 90;
179
+ // Disk-threshold monitor — piggybacks on this nightly sweep event (no separate
180
+ // interval/infra: `guards-never-run-on-interval`). If `/` usage crosses the
181
+ // configured threshold, emit `host.disk.high` (-> event-log/GlitchTip) so a fill
182
+ // the sweep can't absorb (external dump, runaway log) is caught within a day.
183
+ const highPercent = 90;
309
184
  try {
310
- const dfRes = spawnSync({ name: "df:/", command: ["df", "--output=pcent,target"], timeoutMs: 5_000 });
185
+ const dfRes = spawnSync({ name: "df:/", command: ["df", "--output=pcent,target"], timeoutMs: 5000 });
311
186
  const dfOut = (dfRes.stdout ?? "").trim();
312
- const usePct = parseRootDiskPct(dfOut);
313
- if (usePct !== null && usePct >= diskHighPct) {
314
- log.warn("host.disk.high", { usePct, threshold: diskHighPct, df: dfOut });
315
- recordEvent({
316
- kind: "host.disk.high",
317
- payload: { usePct, threshold: diskHighPct, df: dfOut, completedAt: new Date().toISOString() },
318
- });
187
+ const rootLine = dfOut
188
+ .split("\n")
189
+ .find((l) => l.trim().endsWith("/"));
190
+ const pctMatch = rootLine?.match(/(\d+)%/);
191
+ if (pctMatch) {
192
+ const usePct = Number(pctMatch[1]);
193
+ if (usePct >= highPercent) {
194
+ log.warn(
195
+ `[host-cache-sweep] disk usage ${usePct}% >= ${highPercent}% threshold — emitting host.disk.high`,
196
+ );
197
+ recordEvent({
198
+ kind: "host.disk.high",
199
+ payload: { usePct, threshold: highPercent, df: dfOut, completedAt: new Date().toISOString() },
200
+ });
201
+ }
319
202
  }
320
203
  } catch (error) {
321
- log.warn("[host-cache-sweep] disk-threshold read failed: " + String(error));
204
+ log.warn(`[host-cache-sweep] disk-threshold check failed: ${String(error)}`);
322
205
  }
323
206
  }
324
207
 
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @system server-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * Pure transition-detection logic for the WhatsApp linked-device liveness monitor
7
+ * (`whatsapp-liveness` handler). Split out from the GOWA-coupled handler so the
8
+ * logic is unit-testable with zero adapter dependencies, and the handler is a thin
9
+ * poll→detect→emit shell. The connection signal is the GOWA device `state` field
10
+ * (`"logged_in"` == connected) — see `whatsapp-remote-logout-monitor`.
11
+ */
12
+ /** GOWA device `state` value meaning the linked phone is connected + authenticated. */
13
+ export declare const GOWA_CONNECTED_STATE = "logged_in";
14
+ export interface WhatsAppDeviceState {
15
+ id: string;
16
+ state: string;
17
+ jid?: string;
18
+ display_name?: string;
19
+ }
20
+ export interface LivenessTransitions {
21
+ disconnected: WhatsAppDeviceState[];
22
+ reconnected: WhatsAppDeviceState[];
23
+ }
24
+ /**
25
+ * Detect connected↔disconnected transitions between the last-known and the freshly
26
+ * observed device states.
27
+ *
28
+ * `prev` is the last-known `state` per device id. A device ABSENT from `prev` is a
29
+ * first sighting: it seeds the baseline and yields NO transition (so the monitor
30
+ * never alerts on a state it never observed the device leave, and a process restart
31
+ * re-baselines without false positives). A device present in `prev` is compared:
32
+ * connected→not is `disconnected`; not→connected is `reconnected`.
33
+ */
34
+ export declare function detectLivenessTransitions(prev: Map<string, string>, curr: WhatsAppDeviceState[]): LivenessTransitions;
@@ -0,0 +1,4 @@
1
+ import type { ScheduledJobConfig } from "@teamscala/db-validation/registry-schemas/scheduled-job";
2
+ export declare function pollWhatsAppLiveness({ config: _config, }: {
3
+ config: ScheduledJobConfig;
4
+ }): Promise<void>;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @system service-boot
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
+ */
20
+ import type { Context } from "hono";
21
+ /**
22
+ * Returns an `app.onError` handler tagged with `label` (used in the log line so
23
+ * the boot site that surfaced the error is identifiable).
24
+ */
25
+ export declare function createRequestErrorHandler(label: string): (err: unknown, c: Context) => Response;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * @system server-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ * @indivisible-unit
6
+ *
7
+ * Install harness — sandbox-mounts each declared package and captures
8
+ * registry.require() calls via AsyncLocalStorage. Validates all required
9
+ * rows exist before returning. Throws HarnessInstallError with full
10
+ * context on failure. Per Constitution Rule 2 step 3.
11
+ *
12
+ * Marked @indivisible-unit: HarnessInstallError is the error class
13
+ * validatePackageDependencies (might) throw — they form one canonical
14
+ * "install harness validation" unit (the validator + the failure-mode
15
+ * class it produces).
16
+ */
17
+ export interface PackageDescriptor {
18
+ name: string;
19
+ initPath: string;
20
+ }
21
+ export interface HarnessValidationResult {
22
+ valid: true;
23
+ dependencies: Array<{
24
+ slug: string;
25
+ }>;
26
+ }
27
+ export interface HarnessValidationError {
28
+ valid: false;
29
+ missing: Array<{
30
+ slug: string;
31
+ packageName: string;
32
+ }>;
33
+ }
34
+ export type HarnessOutcome = HarnessValidationResult | HarnessValidationError;
35
+ export declare class HarnessInstallError extends Error {
36
+ readonly missing: Array<{
37
+ slug: string;
38
+ packageName: string;
39
+ }>;
40
+ constructor(missing: Array<{
41
+ slug: string;
42
+ packageName: string;
43
+ }>);
44
+ }
45
+ export declare function validatePackageDependencies(packages: PackageDescriptor[], mountFn: (pkg: PackageDescriptor) => Promise<void>): Promise<HarnessOutcome>;