@app-vitals/agent-admin 0.0.1 → 0.1.1

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 (325) hide show
  1. package/Dockerfile +43 -0
  2. package/README.md +72 -2
  3. package/e2e/agents-page.e2e.ts +338 -0
  4. package/e2e/chat-degraded.e2e.ts +153 -0
  5. package/e2e/chat-page.e2e.ts +169 -0
  6. package/e2e/chat-progress.e2e.ts +206 -0
  7. package/e2e/chat-thread-page.e2e.ts +444 -0
  8. package/e2e/login-page.e2e.ts +148 -0
  9. package/e2e/test-server.ts +427 -0
  10. package/openapi.json +6098 -0
  11. package/openapi.platform.json +5553 -0
  12. package/package.json +62 -4
  13. package/playwright.config.ts +25 -0
  14. package/prisma/client/browser.ts +134 -0
  15. package/prisma/client/client.ts +158 -0
  16. package/prisma/client/commonInputTypes.ts +578 -0
  17. package/prisma/client/enums.ts +15 -0
  18. package/prisma/client/internal/class.ts +424 -0
  19. package/prisma/client/internal/prismaNamespace.ts +2898 -0
  20. package/prisma/client/internal/prismaNamespaceBrowser.ts +476 -0
  21. package/prisma/client/models/Account.ts +1774 -0
  22. package/prisma/client/models/AccountInvite.ts +1386 -0
  23. package/prisma/client/models/AccountMember.ts +1351 -0
  24. package/prisma/client/models/Agent.ts +4554 -0
  25. package/prisma/client/models/AgentAsk.ts +1373 -0
  26. package/prisma/client/models/AgentChatTokenUsageDailyByModel.ts +1638 -0
  27. package/prisma/client/models/AgentCronJob.ts +2416 -0
  28. package/prisma/client/models/AgentCronRun.ts +2858 -0
  29. package/prisma/client/models/AgentCronRunModelBreakdown.ts +1532 -0
  30. package/prisma/client/models/AgentCronRunSkillUsage.ts +1642 -0
  31. package/prisma/client/models/AgentEnv.ts +1421 -0
  32. package/prisma/client/models/AgentGitHubInstallationsSnapshot.ts +1289 -0
  33. package/prisma/client/models/AgentMember.ts +1316 -0
  34. package/prisma/client/models/AgentPhaseMethodology.ts +1355 -0
  35. package/prisma/client/models/AgentPlugin.ts +1421 -0
  36. package/prisma/client/models/AgentToken.ts +1380 -0
  37. package/prisma/client/models/AgentTool.ts +1351 -0
  38. package/prisma/client/models/AgentWorkQueueSnapshot.ts +1297 -0
  39. package/prisma/client/models/ChatThreadWatch.ts +1183 -0
  40. package/prisma/client/models/PushSubscription.ts +1233 -0
  41. package/prisma/client/models/SessionAlertState.ts +1187 -0
  42. package/prisma/client/models/SessionFollow.ts +1187 -0
  43. package/prisma/client/models/UserNotificationPrefs.ts +1221 -0
  44. package/prisma/client/models.ts +34 -0
  45. package/prisma/migrations/20260608000000_init/migration.sql +112 -0
  46. package/prisma/migrations/20260616000000_agent_members/migration.sql +18 -0
  47. package/prisma/migrations/20260624000000_add_agent_self_hosted/migration.sql +2 -0
  48. package/prisma/migrations/20260624120000_add_agent_repos/migration.sql +3 -0
  49. package/prisma/migrations/20260625000000_add_agent_cron_run/migration.sql +26 -0
  50. package/prisma/migrations/20260626000000_add_cron_run_token_fields/migration.sql +10 -0
  51. package/prisma/migrations/20260627000000_add_agent_chat_token_usage_daily/migration.sql +18 -0
  52. package/prisma/migrations/20260627010000_add_agent_chat_token_usage_fk/migration.sql +2 -0
  53. package/prisma/migrations/20260701000000_add_chat_threads_messages/migration.sql +37 -0
  54. package/prisma/migrations/20260701010000_drop_chat_threads_messages/migration.sql +17 -0
  55. package/prisma/migrations/20260701020000_add_cron_run_model_breakdown/migration.sql +23 -0
  56. package/prisma/migrations/20260701020000_chat_token_by_model/migration.sql +25 -0
  57. package/prisma/migrations/20260701030000_add_secret_flag_to_agent_env/migration.sql +2 -0
  58. package/prisma/migrations/20260701030000_drop_cron_run_legacy_columns/migration.sql +5 -0
  59. package/prisma/migrations/20260706000000_consolidate_cron_run_token_accounting/migration.sql +54 -0
  60. package/prisma/migrations/20260709000000_add_phase_to_agent_cron_run/migration.sql +8 -0
  61. package/prisma/migrations/20260717000000_add_item_tracking_to_agent_cron_run/migration.sql +10 -0
  62. package/prisma/migrations/20260717010000_add_agent_work_queue_snapshot/migration.sql +20 -0
  63. package/prisma/migrations/20260718000000_add_cron_phase_columns/migration.sql +25 -0
  64. package/prisma/migrations/20260718100000_drop_agent_cron_run_phase_column/migration.sql +8 -0
  65. package/prisma/migrations/20260724000000_add_agent_type_name/migration.sql +3 -0
  66. package/prisma/migrations/20260725232433_add_agent_author_allowlist/migration.sql +2 -0
  67. package/prisma/migrations/20260727120000_add_not_null_to_agent_author_allowlist/migration.sql +3 -0
  68. package/prisma/migrations/20260731000000_add_session_id_to_agent_cron_run/migration.sql +2 -0
  69. package/prisma/migrations/20260821000000_add_agent_restrict_slack_to_members/migration.sql +3 -0
  70. package/prisma/migrations/20260901000000_add_push_subscription_and_chat_thread_watch/migration.sql +40 -0
  71. package/prisma/migrations/20260903000000_add_agent_patch_author_allowlist/migration.sql +2 -0
  72. package/prisma/migrations/20260903120000_add_agent_review_author_allowlist/migration.sql +18 -0
  73. package/prisma/migrations/20260903150000_drop_agent_author_allowlist/migration.sql +14 -0
  74. package/prisma/migrations/20260905000000_add_started_at_index_to_agent_cron_run/migration.sql +11 -0
  75. package/prisma/migrations/20260906000000_add_session_follow_notification_prefs_alert_state/migration.sql +47 -0
  76. package/prisma/migrations/20260911000000_backfill_auto_follow_since/migration.sql +16 -0
  77. package/prisma/migrations/20260923115814_add_last_heartbeat_at_to_agent_cron_run/migration.sql +2 -0
  78. package/prisma/migrations/20260924120000_add_trial_expiry_fields_to_agent/migration.sql +6 -0
  79. package/prisma/migrations/20260929000000_add_agent_phase_methodology/migration.sql +21 -0
  80. package/prisma/migrations/20260929120000_add_agent_policy_fields/migration.sql +15 -0
  81. package/prisma/migrations/20261005000000_add_agent_github_installations_snapshot/migration.sql +21 -0
  82. package/prisma/migrations/20261008000000_add_cron_run_telemetry/migration.sql +50 -0
  83. package/prisma/migrations/20261008010000_add_account_models/migration.sql +48 -0
  84. package/prisma/migrations/20261008020000_add_cron_lockdown_disabled_at/migration.sql +3 -0
  85. package/prisma/migrations/20261009000000_chat_token_usage_bigint/migration.sql +7 -0
  86. package/prisma/migrations/20261010000000_add_cron_run_command_variant/migration.sql +2 -0
  87. package/prisma/migrations/20261019000000_add_agent_anthropic_identity/migration.sql +9 -0
  88. package/prisma/migrations/20261025000000_add_agent_ask/migration.sql +23 -0
  89. package/prisma/migrations/migration_lock.toml +3 -0
  90. package/prisma/schema/coding.prisma +85 -0
  91. package/prisma/schema/schema.prisma +506 -0
  92. package/prisma.config.ts +34 -0
  93. package/pwa-assets/icons/apple-touch-icon.png +0 -0
  94. package/pwa-assets/icons/favicon-32.png +0 -0
  95. package/pwa-assets/icons/icon-192.png +0 -0
  96. package/pwa-assets/icons/icon-512.png +0 -0
  97. package/pwa-assets/icons/icon-maskable-192.png +0 -0
  98. package/pwa-assets/icons/icon-maskable-512.png +0 -0
  99. package/src/account-agent-create.ts +42 -0
  100. package/src/account-agent-create.unit.test.ts +28 -0
  101. package/src/account-created-notifier.ts +71 -0
  102. package/src/account-created-notifier.unit.test.ts +70 -0
  103. package/src/account-email.ts +9 -0
  104. package/src/account-email.unit.test.ts +21 -0
  105. package/src/account-invites.ts +70 -0
  106. package/src/account-lifecycle.integration.test.ts +329 -0
  107. package/src/account-lifecycle.ts +206 -0
  108. package/src/account-lifecycle.unit.test.ts +108 -0
  109. package/src/account-members.ts +128 -0
  110. package/src/account-onboarding.integration.test.ts +101 -0
  111. package/src/account-onboarding.ts +95 -0
  112. package/src/account-trial-expiry-sweeper.ts +85 -0
  113. package/src/account-trial-warning-sweeper.integration.test.ts +228 -0
  114. package/src/account-trial-warning-sweeper.ts +216 -0
  115. package/src/accounts-api.integration.test.ts +196 -0
  116. package/src/accounts-api.smoke.test.ts +292 -0
  117. package/src/accounts-api.ts +289 -0
  118. package/src/accounts-api.unit.test.ts +93 -0
  119. package/src/accounts.integration.test.ts +178 -0
  120. package/src/accounts.ts +208 -0
  121. package/src/admin-boot-config.ts +574 -0
  122. package/src/admin-boot-config.unit.test.ts +28 -0
  123. package/src/admin-boot.boot-order.unit.test.ts +91 -0
  124. package/src/admin-boot.ts +690 -0
  125. package/src/admin-dev-login.smoke.test.ts +317 -0
  126. package/src/admin-extension.ts +432 -0
  127. package/src/admin-extension.unit.test.ts +532 -0
  128. package/src/admin-local-agent.smoke.test.ts +1643 -0
  129. package/src/admin-spec.smoke.test.ts +96 -0
  130. package/src/admin-spec.ts +186 -0
  131. package/src/admin-ui-account-nav.ts +20 -0
  132. package/src/admin-ui-account-pages.ts +131 -0
  133. package/src/admin-ui-account-pages.unit.test.ts +112 -0
  134. package/src/admin-ui-account.smoke.test.ts +286 -0
  135. package/src/admin-ui-account.ts +185 -0
  136. package/src/admin-ui-accounts-pages.ts +202 -0
  137. package/src/admin-ui-accounts-pages.unit.test.ts +138 -0
  138. package/src/admin-ui-accounts.smoke.test.ts +434 -0
  139. package/src/admin-ui-accounts.ts +266 -0
  140. package/src/admin-ui-layout.ts +60 -0
  141. package/src/admin-ui-layout.unit.test.ts +140 -0
  142. package/src/admin-ui-pages.ts +3489 -0
  143. package/src/admin-ui-pages.unit.test.ts +4355 -0
  144. package/src/admin-ui-push-test.smoke.test.ts +125 -0
  145. package/src/admin-ui-push-test.ts +102 -0
  146. package/src/admin-ui-styles.ts +656 -0
  147. package/src/admin-ui-styles.unit.test.ts +103 -0
  148. package/src/admin-ui.fixtures.ts +457 -0
  149. package/src/admin-ui.smoke.test.ts +7227 -0
  150. package/src/admin-ui.ts +3824 -0
  151. package/src/agent-account-assignment.ts +49 -0
  152. package/src/agent-account-assignment.unit.test.ts +99 -0
  153. package/src/agent-anthropic-identity.integration.test.ts +99 -0
  154. package/src/agent-anthropic-identity.ts +78 -0
  155. package/src/agent-anthropic-identity.unit.test.ts +77 -0
  156. package/src/agent-ask-config.ts +59 -0
  157. package/src/agent-ask-config.unit.test.ts +70 -0
  158. package/src/agent-ask.integration.test.ts +175 -0
  159. package/src/agent-ask.smoke.test.ts +457 -0
  160. package/src/agent-ask.ts +378 -0
  161. package/src/agent-chat-tokens.integration.test.ts +420 -0
  162. package/src/agent-chat-tokens.smoke.test.ts +551 -0
  163. package/src/agent-chat-tokens.ts +259 -0
  164. package/src/agent-cron-jobs.integration.test.ts +582 -0
  165. package/src/agent-cron-jobs.ts +615 -0
  166. package/src/agent-cron-jobs.unit.test.ts +420 -0
  167. package/src/agent-cron-run-stats.integration.test.ts +1551 -0
  168. package/src/agent-cron-run-stats.smoke.test.ts +953 -0
  169. package/src/agent-cron-run-stats.ts +752 -0
  170. package/src/agent-cron-runs.integration.test.ts +1491 -0
  171. package/src/agent-cron-runs.ts +489 -0
  172. package/src/agent-deletion-checklist.ts +128 -0
  173. package/src/agent-deletion-checklist.unit.test.ts +201 -0
  174. package/src/agent-deletion-per-agent.integration.test.ts +323 -0
  175. package/src/agent-deletion.integration.test.ts +323 -0
  176. package/src/agent-deletion.ts +329 -0
  177. package/src/agent-deletion.unit.test.ts +412 -0
  178. package/src/agent-envs.integration.test.ts +323 -0
  179. package/src/agent-envs.ts +244 -0
  180. package/src/agent-github-installations.integration.test.ts +115 -0
  181. package/src/agent-github-installations.ts +52 -0
  182. package/src/agent-manifest.ts +685 -0
  183. package/src/agent-manifest.unit.test.ts +870 -0
  184. package/src/agent-members.ts +69 -0
  185. package/src/agent-members.unit.test.ts +250 -0
  186. package/src/agent-patch-author-allowlist.integration.test.ts +218 -0
  187. package/src/agent-plugins.integration.test.ts +157 -0
  188. package/src/agent-plugins.ts +95 -0
  189. package/src/agent-plugins.unit.test.ts +43 -0
  190. package/src/agent-provisioner-per-agent.integration.test.ts +949 -0
  191. package/src/agent-provisioner.integration.test.ts +1245 -0
  192. package/src/agent-provisioner.ts +1650 -0
  193. package/src/agent-provisioner.unit.test.ts +1852 -0
  194. package/src/agent-tokens.integration.test.ts +150 -0
  195. package/src/agent-tokens.ts +112 -0
  196. package/src/agent-tools.integration.test.ts +129 -0
  197. package/src/agent-tools.ts +111 -0
  198. package/src/agents-api.integration.test.ts +705 -0
  199. package/src/agents-api.sentry.smoke.test.ts +478 -0
  200. package/src/agents-api.smoke.test.ts +5986 -0
  201. package/src/agents-api.ts +2826 -0
  202. package/src/agents.integration.test.ts +512 -0
  203. package/src/agents.ts +1097 -0
  204. package/src/agents.unit.test.ts +1736 -0
  205. package/src/anthropic-credential-guard.ts +44 -0
  206. package/src/anthropic-credential-guard.unit.test.ts +88 -0
  207. package/src/anthropic-federated-token-provider.integration.test.ts +248 -0
  208. package/src/anthropic-federated-token-provider.ts +249 -0
  209. package/src/anthropic-identity-client.fixtures.ts +237 -0
  210. package/src/anthropic-identity-client.integration.test.ts +342 -0
  211. package/src/anthropic-identity-client.ts +473 -0
  212. package/src/anthropic-identity-fixture-spec.integration.test.ts +169 -0
  213. package/src/anthropic-identity-name.integration.test.ts +83 -0
  214. package/src/anthropic-identity-name.ts +163 -0
  215. package/src/anthropic-identity-name.unit.test.ts +345 -0
  216. package/src/anthropic-openapi-contract.fixtures.ts +192 -0
  217. package/src/api-auth.ts +383 -0
  218. package/src/api-auth.unit.test.ts +837 -0
  219. package/src/api.smoke.test.ts +1225 -0
  220. package/src/api.ts +376 -0
  221. package/src/attachment-validation.ts +124 -0
  222. package/src/attachment-validation.unit.test.ts +218 -0
  223. package/src/caller-scope.ts +131 -0
  224. package/src/caller-scope.unit.test.ts +116 -0
  225. package/src/chat-credential-config.ts +22 -0
  226. package/src/chat-credentials-logging.unit.test.ts +92 -0
  227. package/src/chat-markers.ts +114 -0
  228. package/src/chat-markers.unit.test.ts +116 -0
  229. package/src/chat-service-provisioning-client.integration.test.ts +412 -0
  230. package/src/chat-service-provisioning-client.ts +221 -0
  231. package/src/chat.smoke.test.ts +1875 -0
  232. package/src/clock.ts +47 -0
  233. package/src/coding-task-store-provisioning-client.integration.test.ts +372 -0
  234. package/src/coding-task-store-provisioning-client.ts +156 -0
  235. package/src/crypto.ts +83 -0
  236. package/src/dev-auth-guard.ts +29 -0
  237. package/src/dev-auth-guard.unit.test.ts +51 -0
  238. package/src/errors.ts +77 -0
  239. package/src/errors.unit.test.ts +52 -0
  240. package/src/fixtures/anthropic-identity-recorded-fixtures.json +850 -0
  241. package/src/fixtures/anthropic-openapi/SDK_VERSION +1 -0
  242. package/src/fixtures/anthropic-openapi/mock-spec.json.gz +0 -0
  243. package/src/fixtures/chat-service-provisioning-cassette.json +156 -0
  244. package/src/fixtures/github-app-manifest-cassette.json +44 -0
  245. package/src/fixtures/google-auth-cassette.json +36 -0
  246. package/src/fixtures/http-chat-client-cassette.json +226 -0
  247. package/src/fixtures/k8s-cassette.json +470 -0
  248. package/src/fixtures/okta-auth-cassette.json +36 -0
  249. package/src/fixtures/slack-provision-cassette.json +12 -0
  250. package/src/fixtures/slack-provisioning-http-cassette.json +98 -0
  251. package/src/fixtures/task-store-provisioning-cassette.json +85 -0
  252. package/src/github-app-provisioning-client.integration.test.ts +149 -0
  253. package/src/github-app-provisioning-client.ts +161 -0
  254. package/src/github-app-provisioning-client.unit.test.ts +114 -0
  255. package/src/github-provisioning-service.ts +569 -0
  256. package/src/github-provisioning-service.unit.test.ts +123 -0
  257. package/src/google-auth-client.integration.test.ts +195 -0
  258. package/src/google-auth-client.ts +121 -0
  259. package/src/http-chat-client.integration.test.ts +554 -0
  260. package/src/http-chat-client.ts +533 -0
  261. package/src/http-chat-client.unit.test.ts +119 -0
  262. package/src/import-boundary.unit.test.ts +240 -0
  263. package/src/kubernetes-client.integration.test.ts +697 -0
  264. package/src/kubernetes-client.ts +1027 -0
  265. package/src/kubernetes-client.unit.test.ts +726 -0
  266. package/src/main.smoke.test.ts +91 -0
  267. package/src/main.ts +21 -0
  268. package/src/main.unit.test.ts +801 -0
  269. package/src/okta-auth-client.integration.test.ts +263 -0
  270. package/src/okta-auth-client.ts +152 -0
  271. package/src/openapi-schemas.ts +1310 -0
  272. package/src/openapi-schemas.unit.test.ts +348 -0
  273. package/src/per-agent-identity.fixtures.ts +305 -0
  274. package/src/platform-doc-route.ts +40 -0
  275. package/src/prisma-client.integration.test.ts +57 -0
  276. package/src/prisma-client.ts +187 -0
  277. package/src/prisma-client.unit.test.ts +178 -0
  278. package/src/prisma-tx.ts +23 -0
  279. package/src/push-content.ts +177 -0
  280. package/src/push-content.unit.test.ts +213 -0
  281. package/src/push-crypto.ts +135 -0
  282. package/src/push-crypto.unit.test.ts +66 -0
  283. package/src/push-sender.ts +169 -0
  284. package/src/push-sender.unit.test.ts +107 -0
  285. package/src/push-service.ts +332 -0
  286. package/src/push-service.unit.test.ts +679 -0
  287. package/src/push-toggle.ts +135 -0
  288. package/src/push-toggle.unit.test.ts +78 -0
  289. package/src/pwa-https-gating.smoke.test.ts +229 -0
  290. package/src/pwa-icons.unit.test.ts +41 -0
  291. package/src/pwa-routes.smoke.test.ts +276 -0
  292. package/src/pwa.ts +380 -0
  293. package/src/pwa.unit.test.ts +321 -0
  294. package/src/schema.unit.test.ts +122 -0
  295. package/src/self-serve-config.ts +52 -0
  296. package/src/self-serve-config.unit.test.ts +79 -0
  297. package/src/server.smoke.test.ts +273 -0
  298. package/src/server.ts +50 -0
  299. package/src/service-integration.ts +67 -0
  300. package/src/service-integration.unit.test.ts +129 -0
  301. package/src/slack-provisioning-client.integration.test.ts +308 -0
  302. package/src/slack-provisioning-client.ts +411 -0
  303. package/src/slack-provisioning-client.unit.test.ts +80 -0
  304. package/src/slack-provisioning-service.integration.test.ts +637 -0
  305. package/src/slack-provisioning-service.ts +451 -0
  306. package/src/slack-provisioning.integration.test.ts +395 -0
  307. package/src/specialty-manifest-loader.fixture.integration.test.ts +63 -0
  308. package/src/specialty-manifest-loader.ts +255 -0
  309. package/src/specialty-manifest-loader.unit.test.ts +301 -0
  310. package/src/specialty-registry.schema.unit.test.ts +35 -0
  311. package/src/specialty-registry.ts +236 -0
  312. package/src/specialty-registry.unit.test.ts +272 -0
  313. package/src/startup-policy-reconcile.ts +77 -0
  314. package/src/startup-policy-reconcile.unit.test.ts +251 -0
  315. package/src/theme-tokens.json +12 -0
  316. package/src/token-crypto.ts +60 -0
  317. package/src/token-crypto.unit.test.ts +182 -0
  318. package/src/trial-expiry-sweeper.integration.test.ts +318 -0
  319. package/src/trial-expiry-sweeper.ts +132 -0
  320. package/src/trial-expiry-sweeper.unit.test.ts +391 -0
  321. package/src/trial-expiry-warning-sweeper.integration.test.ts +468 -0
  322. package/src/trial-expiry-warning-sweeper.ts +447 -0
  323. package/src/trial-expiry-warning-sweeper.unit.test.ts +148 -0
  324. package/src/voice-contract.unit.test.ts +102 -0
  325. package/tsconfig.json +9 -0
@@ -0,0 +1,332 @@
1
+ /**
2
+ * admin/src/push-service.ts
3
+ * PushService — orchestrates a "your agent replied" notification.
4
+ *
5
+ * Targeting flows through ChatThreadWatch (NOT a fan-out to every agent
6
+ * member): the users watching a thread are exactly the ones who sent a message
7
+ * in it, so only they are notified when the agent replies. Each of their
8
+ * subscriptions gets a payload built at min(operator ceiling, that
9
+ * subscription's opt-in). Subscriptions the sender reports gone (404/410) are
10
+ * pruned immediately.
11
+ *
12
+ * Constructed with an injected fetchImpl (passed straight to PushSender) — no
13
+ * global.fetch override. A partial VAPID config never reaches here: main.ts
14
+ * only constructs a PushService when isPushEnabled() is true.
15
+ */
16
+
17
+ import {
18
+ type NotificationThread,
19
+ type PushDetailLevel,
20
+ type SessionNotificationKind,
21
+ buildNotificationPayload,
22
+ buildSessionNotificationPayload,
23
+ resolveDetailLevel,
24
+ } from "./push-content.ts";
25
+ import {
26
+ PushSender,
27
+ type PushSubscriptionLike,
28
+ type VapidConfig,
29
+ } from "./push-sender.ts";
30
+
31
+ // The narrow slice of PrismaClient this service touches. Injected so the unit
32
+ // tests use a plain object double instead of a real client.
33
+ interface PushPrismaLike {
34
+ chatThreadWatch: {
35
+ findMany(args: {
36
+ where: { threadId: string };
37
+ select: { userEmail: true };
38
+ }): Promise<Array<{ userEmail: string }>>;
39
+ upsert(args: {
40
+ where: { userEmail_threadId: { userEmail: string; threadId: string } };
41
+ create: { userEmail: string; threadId: string; agentId: string };
42
+ update: { agentId: string };
43
+ }): Promise<{ id: string }>;
44
+ };
45
+ pushSubscription: {
46
+ findMany(args: {
47
+ where: { userEmail: { in: string[] } };
48
+ }): Promise<
49
+ Array<{
50
+ userEmail: string;
51
+ endpoint: string;
52
+ p256dh: string;
53
+ auth: string;
54
+ detailOptIn: string;
55
+ }>
56
+ >;
57
+ upsert(args: {
58
+ where: { endpoint: string };
59
+ create: {
60
+ userEmail: string;
61
+ endpoint: string;
62
+ p256dh: string;
63
+ auth: string;
64
+ };
65
+ update: { userEmail: string; p256dh: string; auth: string };
66
+ }): Promise<{ id: string }>;
67
+ deleteMany(
68
+ args:
69
+ | { where: { endpoint: { in: string[] } } }
70
+ | { where: { endpoint: string; userEmail?: string } },
71
+ ): Promise<{ count: number }>;
72
+ };
73
+ }
74
+
75
+ /** Outcome of PushService.subscribe — distinguishes the three response shapes
76
+ * the route handler maps to HTTP status (503 unavailable / 500 store_failed /
77
+ * 200 ok). */
78
+ type PushSubscribeResult =
79
+ | { ok: true }
80
+ | { ok: false; reason: "unavailable" | "store_failed" };
81
+
82
+ /** Outcome of PushService.unsubscribe — delete failures are swallowed/logged
83
+ * inside the service (best-effort), so only "unavailable" (model missing) is
84
+ * distinguished from success. */
85
+ type PushUnsubscribeResult =
86
+ | { ok: true }
87
+ | { ok: false; reason: "unavailable" };
88
+
89
+ /**
90
+ * A minimal carrier of "who to notify" for a session-based notification.
91
+ * There is no real Session/SessionFollow Prisma model yet (that lands with
92
+ * sibling tasks SES-1.1/SES-6.1) — notifySession only needs the resolved
93
+ * list of emails to notify plus enough identifying info for the payload, not
94
+ * a Prisma-backed lookup. Callers resolve `emails` themselves. `title` is
95
+ * optional so a caller that hasn't resolved it yet still satisfies this type;
96
+ * it structurally matches push-content.ts's `NotificationSession` (which
97
+ * `buildSessionNotificationPayload` consumes), minus that type's `reason`
98
+ * field — no caller here has one to offer yet.
99
+ */
100
+ export interface NotificationSession {
101
+ slug: string;
102
+ emails: string[];
103
+ title?: string | null;
104
+ }
105
+
106
+ export class PushService {
107
+ private readonly sender: PushSender;
108
+
109
+ constructor(
110
+ private readonly prisma: PushPrismaLike,
111
+ vapid: VapidConfig,
112
+ fetchImpl: typeof fetch,
113
+ private readonly maxDetail: PushDetailLevel | string,
114
+ ) {
115
+ this.sender = new PushSender(vapid, fetchImpl);
116
+ }
117
+
118
+ /** The deduped set of emails watching a thread. */
119
+ private async watchersForThread(threadId: string): Promise<string[]> {
120
+ const watchers = await this.prisma.chatThreadWatch.findMany({
121
+ where: { threadId },
122
+ select: { userEmail: true },
123
+ });
124
+ return [...new Set(watchers.map((w) => w.userEmail))];
125
+ }
126
+
127
+ /** All push subscriptions belonging to the given emails. */
128
+ private async subscriptionsForEmails(emails: string[]) {
129
+ if (emails.length === 0) return [];
130
+ return this.prisma.pushSubscription.findMany({
131
+ where: { userEmail: { in: emails } },
132
+ });
133
+ }
134
+
135
+ /** All subscriptions belonging to the users watching a thread. */
136
+ private async subscriptionsForThread(threadId: string) {
137
+ const emails = await this.watchersForThread(threadId);
138
+ return this.subscriptionsForEmails(emails);
139
+ }
140
+
141
+ /**
142
+ * The effective detail level per watching subscription — exposed for the
143
+ * dedicated content-policy audit and for tests.
144
+ */
145
+ async resolveLevelsForThread(threadId: string): Promise<PushDetailLevel[]> {
146
+ const subs = await this.subscriptionsForThread(threadId);
147
+ return subs.map((s) => resolveDetailLevel(this.maxDetail, s.detailOptIn));
148
+ }
149
+
150
+ /**
151
+ * The reusable core of every "notify these users" flow: resolves push
152
+ * subscriptions for the given emails, groups them by effective detail
153
+ * level (min(operator ceiling, each subscription's opt-in)), builds one
154
+ * payload per group via `buildPayload`, sends, then prunes any endpoints
155
+ * the sender reports gone. Does NOT swallow errors itself — that's each
156
+ * public entry point's job (notifyThreadReply, notifySession), so the
157
+ * "never throws" contract lives at exactly one place per caller instead of
158
+ * being duplicated or accidentally omitted here.
159
+ */
160
+ async sendToUsers(
161
+ emails: string[],
162
+ buildPayload: (level: PushDetailLevel) => string,
163
+ ): Promise<{ delivered: number; pruned: number }> {
164
+ const subs = await this.subscriptionsForEmails(emails);
165
+ if (subs.length === 0) return { delivered: 0, pruned: 0 };
166
+
167
+ // Group subscriptions by their effective detail level so each group gets
168
+ // a single payload build. Different opt-ins → different visible content.
169
+ const byPayload = new Map<string, PushSubscriptionLike[]>();
170
+ for (const s of subs) {
171
+ const level = resolveDetailLevel(this.maxDetail, s.detailOptIn);
172
+ const payload = buildPayload(level);
173
+ if (!byPayload.has(payload)) {
174
+ byPayload.set(payload, []);
175
+ }
176
+ byPayload.get(payload)?.push({
177
+ endpoint: s.endpoint,
178
+ p256dh: s.p256dh,
179
+ auth: s.auth,
180
+ });
181
+ }
182
+
183
+ let delivered = 0;
184
+ const prunedEndpoints: string[] = [];
185
+ for (const [payload, group] of byPayload) {
186
+ const res = await this.sender.sendToMany(group, payload);
187
+ delivered += res.delivered;
188
+ prunedEndpoints.push(...res.prunedEndpoints);
189
+ }
190
+
191
+ if (prunedEndpoints.length > 0) {
192
+ await this.prisma.pushSubscription.deleteMany({
193
+ where: { endpoint: { in: prunedEndpoints } },
194
+ });
195
+ }
196
+ return { delivered, pruned: prunedEndpoints.length };
197
+ }
198
+
199
+ /**
200
+ * Sends the "agent replied" notification to every subscription watching the
201
+ * thread, then prunes any the sender reported gone. Never throws into the
202
+ * caller's request/response cycle — a push failure must not fail the reply.
203
+ */
204
+ async notifyThreadReply(
205
+ thread: NotificationThread,
206
+ ): Promise<{ delivered: number; pruned: number }> {
207
+ try {
208
+ const emails = await this.watchersForThread(thread.threadId);
209
+ if (emails.length === 0) return { delivered: 0, pruned: 0 };
210
+ return await this.sendToUsers(emails, (level) =>
211
+ JSON.stringify(buildNotificationPayload(level, thread)),
212
+ );
213
+ } catch (err) {
214
+ // Push is a convenience layer over polling, never a replacement — a
215
+ // failure here must never surface to the reply flow.
216
+ console.error("[push] notifyThreadReply failed:", err);
217
+ return { delivered: 0, pruned: 0 };
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Sends a session-lifecycle notification (SESH-7.1) to every email on
223
+ * `session.emails`, then prunes any endpoints the sender reports gone.
224
+ * Never throws into the caller — same convenience-layer contract as
225
+ * notifyThreadReply. The payload content is built by
226
+ * `buildSessionNotificationPayload` (push-content.ts's session policy),
227
+ * mirroring exactly how notifyThreadReply builds its own via
228
+ * `buildNotificationPayload`. The detail level honors the same privacy
229
+ * invariant notifyThreadReply does: `level` is the caller's per-call
230
+ * ceiling, and the effective level of each subscription's payload is
231
+ * min(caller ceiling, operator ceiling, that subscription's opt-in). A
232
+ * caller can ask for less detail, never more.
233
+ */
234
+ async notifySession(
235
+ session: NotificationSession,
236
+ level: PushDetailLevel,
237
+ kind: SessionNotificationKind,
238
+ ): Promise<{ delivered: number; pruned: number }> {
239
+ try {
240
+ if (session.emails.length === 0) return { delivered: 0, pruned: 0 };
241
+ // `subLevel` already is min(this.maxDetail, subscription opt-in);
242
+ // resolveDetailLevel caps it once more against the caller's request.
243
+ return await this.sendToUsers(session.emails, (subLevel) =>
244
+ JSON.stringify(
245
+ buildSessionNotificationPayload(
246
+ resolveDetailLevel(level, subLevel),
247
+ kind,
248
+ session,
249
+ ),
250
+ ),
251
+ );
252
+ } catch (err) {
253
+ console.error("[push] notifySession failed:", err);
254
+ return { delivered: 0, pruned: 0 };
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Best-effort upsert of a ChatThreadWatch row (CFB-4.2). Never throws into
260
+ * a request cycle — push is a convenience layer, so a watch-write failure
261
+ * must not fail the message send it rides along with. The `!this.prisma.
262
+ * chatThreadWatch` guard is load-bearing: main.ts constructs this service
263
+ * with `prisma as never`, bypassing type-checking, so the real client may
264
+ * genuinely lack this model if a migration hasn't landed yet.
265
+ */
266
+ async recordWatch(
267
+ userEmail: string,
268
+ agentId: string,
269
+ threadId: string,
270
+ ): Promise<void> {
271
+ if (!this.prisma.chatThreadWatch) return;
272
+ try {
273
+ await this.prisma.chatThreadWatch.upsert({
274
+ where: { userEmail_threadId: { userEmail, threadId } },
275
+ create: { userEmail, threadId, agentId },
276
+ update: { agentId },
277
+ });
278
+ } catch (err) {
279
+ console.error("[push] watchThread upsert failed:", err);
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Upserts a browser push subscription for userEmail. The `unavailable`
285
+ * reason covers the same not-yet-migrated case as recordWatch's guard;
286
+ * `store_failed` surfaces an upsert error to the caller (unlike
287
+ * recordWatch/unsubscribe, this one isn't swallowed — the client needs to
288
+ * know its subscription didn't stick).
289
+ */
290
+ async subscribe(
291
+ userEmail: string,
292
+ endpoint: string,
293
+ p256dh: string,
294
+ auth: string,
295
+ ): Promise<PushSubscribeResult> {
296
+ if (!this.prisma.pushSubscription)
297
+ return { ok: false, reason: "unavailable" };
298
+ try {
299
+ await this.prisma.pushSubscription.upsert({
300
+ where: { endpoint },
301
+ create: { userEmail, endpoint, p256dh, auth },
302
+ update: { userEmail, p256dh, auth },
303
+ });
304
+ return { ok: true };
305
+ } catch (err) {
306
+ console.error("[push] subscribe upsert failed:", err);
307
+ return { ok: false, reason: "store_failed" };
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Deletes a browser push subscription, scoped to both endpoint and
313
+ * userEmail — this scoping prevents a stale endpoint from pruning another
314
+ * user's subscription. Delete failures are best-effort: logged and
315
+ * swallowed, never surfaced as a distinct outcome.
316
+ */
317
+ async unsubscribe(
318
+ userEmail: string,
319
+ endpoint: string,
320
+ ): Promise<PushUnsubscribeResult> {
321
+ if (!this.prisma.pushSubscription)
322
+ return { ok: false, reason: "unavailable" };
323
+ try {
324
+ await this.prisma.pushSubscription.deleteMany({
325
+ where: { endpoint, userEmail },
326
+ });
327
+ } catch (err) {
328
+ console.error("[push] unsubscribe delete failed:", err);
329
+ }
330
+ return { ok: true };
331
+ }
332
+ }