@goscribe/server 1.3.4 → 1.6.0

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 (383) hide show
  1. package/.env.example +12 -0
  2. package/.vscode/settings.json +3 -0
  3. package/REFACTOR_NOTES.md +60 -0
  4. package/TESTING_PROMPT.md +225 -0
  5. package/dist/context.d.ts +14 -1
  6. package/dist/context.js +23 -2
  7. package/dist/controllers/admin.controller.d.ts +715 -0
  8. package/dist/controllers/admin.controller.js +9 -0
  9. package/dist/controllers/annotations.controller.d.ts +439 -0
  10. package/dist/controllers/annotations.controller.js +9 -0
  11. package/dist/controllers/app-router.controller.d.ts +3011 -0
  12. package/dist/controllers/app-router.controller.js +38 -0
  13. package/dist/controllers/app-router.controller.test.d.ts +1 -0
  14. package/dist/controllers/app-router.controller.test.js +36 -0
  15. package/dist/controllers/auth.controller.d.ts +323 -0
  16. package/dist/controllers/auth.controller.js +9 -0
  17. package/dist/controllers/base.controller.d.ts +4 -0
  18. package/dist/controllers/base.controller.js +5 -0
  19. package/dist/controllers/chat.controller.d.ts +341 -0
  20. package/dist/controllers/chat.controller.js +9 -0
  21. package/dist/controllers/copilot.controller.d.ts +397 -0
  22. package/dist/controllers/copilot.controller.js +9 -0
  23. package/dist/controllers/flashcards.controller.d.ts +651 -0
  24. package/dist/controllers/flashcards.controller.js +9 -0
  25. package/dist/controllers/members.controller.d.ts +339 -0
  26. package/dist/controllers/members.controller.js +9 -0
  27. package/dist/controllers/notifications.controller.d.ts +199 -0
  28. package/dist/controllers/notifications.controller.js +9 -0
  29. package/dist/controllers/payment.controller.d.ts +181 -0
  30. package/dist/controllers/payment.controller.js +9 -0
  31. package/dist/controllers/podcast.controller.d.ts +575 -0
  32. package/dist/controllers/podcast.controller.js +9 -0
  33. package/dist/controllers/router-module.controller.d.ts +5 -0
  34. package/dist/controllers/router-module.controller.js +6 -0
  35. package/dist/controllers/studyguide.controller.d.ts +73 -0
  36. package/dist/controllers/studyguide.controller.js +9 -0
  37. package/dist/controllers/worksheets.controller.d.ts +829 -0
  38. package/dist/controllers/worksheets.controller.js +9 -0
  39. package/dist/controllers/workspace.controller.d.ts +1207 -0
  40. package/dist/controllers/workspace.controller.js +9 -0
  41. package/dist/lib/activity_human_description.test.js +16 -15
  42. package/dist/lib/activity_log_service.test.js +28 -23
  43. package/dist/lib/ai/config.d.ts +20 -0
  44. package/dist/lib/ai/config.js +31 -0
  45. package/dist/lib/ai/embedding-client.d.ts +8 -0
  46. package/dist/lib/ai/embedding-client.js +30 -0
  47. package/dist/lib/ai/index.d.ts +48 -0
  48. package/dist/lib/ai/index.js +29 -0
  49. package/dist/lib/ai/inference-backend/client.d.ts +28 -0
  50. package/dist/lib/ai/inference-backend/client.js +301 -0
  51. package/dist/lib/ai/inference-backend/mocks.d.ts +12 -0
  52. package/dist/lib/ai/inference-backend/mocks.js +133 -0
  53. package/dist/lib/ai/inference-backend/types.d.ts +44 -0
  54. package/dist/lib/ai/inference-backend/types.js +1 -0
  55. package/dist/lib/ai/json-parse.d.ts +2 -0
  56. package/dist/lib/ai/json-parse.js +34 -0
  57. package/dist/lib/ai/llm-client.d.ts +7 -0
  58. package/dist/lib/ai/llm-client.js +36 -0
  59. package/dist/lib/ai/mock.d.ts +2 -0
  60. package/dist/lib/ai/mock.js +10 -0
  61. package/dist/lib/ai/types.d.ts +9 -0
  62. package/dist/lib/ai/types.js +1 -0
  63. package/dist/lib/chunking.d.ts +19 -0
  64. package/dist/lib/chunking.js +47 -0
  65. package/dist/lib/curated-kb-seed.d.ts +12 -0
  66. package/dist/lib/curated-kb-seed.js +155 -0
  67. package/dist/lib/email.js +67 -108
  68. package/dist/lib/embeddings.d.ts +2 -0
  69. package/dist/lib/embeddings.js +1 -0
  70. package/dist/lib/ensure-curated-kb-catalog.d.ts +6 -0
  71. package/dist/lib/ensure-curated-kb-catalog.js +53 -0
  72. package/dist/lib/env.d.ts +1 -5
  73. package/dist/lib/env.js +2 -7
  74. package/dist/lib/inference.d.ts +1 -8
  75. package/dist/lib/inference.js +1 -19
  76. package/dist/lib/kb-meta.d.ts +8 -0
  77. package/dist/lib/kb-meta.js +77 -0
  78. package/dist/lib/note-text.d.ts +1 -0
  79. package/dist/lib/note-text.js +47 -0
  80. package/dist/lib/notification-service.test.js +37 -36
  81. package/dist/lib/pdf.d.ts +11 -0
  82. package/dist/lib/pdf.js +11 -0
  83. package/dist/lib/usage_service.d.ts +2 -1
  84. package/dist/lib/usage_service.js +30 -12
  85. package/dist/lib/worksheet-generation.js +4 -4
  86. package/dist/lib/worksheet-generation.test.js +32 -17
  87. package/dist/lib/workspace-kb.d.ts +5 -0
  88. package/dist/lib/workspace-kb.js +7 -0
  89. package/dist/models/controller-context.model.d.ts +8 -0
  90. package/dist/models/controller-context.model.js +1 -0
  91. package/dist/repositories/artifact.repository.d.ts +60 -0
  92. package/dist/repositories/artifact.repository.js +40 -0
  93. package/dist/repositories/base.repository.d.ts +14 -0
  94. package/dist/repositories/base.repository.js +14 -0
  95. package/dist/repositories/invitation.repository.d.ts +94 -0
  96. package/dist/repositories/invitation.repository.js +44 -0
  97. package/dist/repositories/notification.repository.d.ts +72 -0
  98. package/dist/repositories/notification.repository.js +44 -0
  99. package/dist/repositories/router-module.repository.d.ts +10 -0
  100. package/dist/repositories/router-module.repository.js +14 -0
  101. package/dist/repositories/user.repository.d.ts +74 -0
  102. package/dist/repositories/user.repository.js +37 -0
  103. package/dist/repositories/workspace-member.repository.d.ts +31 -0
  104. package/dist/repositories/workspace-member.repository.js +31 -0
  105. package/dist/repositories/workspace.repository.d.ts +97 -0
  106. package/dist/repositories/workspace.repository.js +79 -0
  107. package/dist/routers/_app.d.ts +566 -111
  108. package/dist/routers/_app.js +4 -0
  109. package/dist/routers/admin.d.ts +0 -4
  110. package/dist/routers/admin.js +21 -549
  111. package/dist/routers/annotations.js +12 -170
  112. package/dist/routers/artifactVersions.d.ts +65 -0
  113. package/dist/routers/artifactVersions.js +14 -0
  114. package/dist/routers/auth.d.ts +0 -6
  115. package/dist/routers/auth.js +37 -422
  116. package/dist/routers/chat.js +15 -229
  117. package/dist/routers/copilot.d.ts +14 -13
  118. package/dist/routers/copilot.js +13 -532
  119. package/dist/routers/flashcards.d.ts +17 -6
  120. package/dist/routers/flashcards.js +23 -349
  121. package/dist/routers/knowledgeBase.d.ts +421 -0
  122. package/dist/routers/knowledgeBase.js +118 -0
  123. package/dist/routers/members.d.ts +0 -41
  124. package/dist/routers/members.js +22 -710
  125. package/dist/routers/notes.d.ts +94 -0
  126. package/dist/routers/notes.js +37 -0
  127. package/dist/routers/notifications.js +7 -109
  128. package/dist/routers/payment.d.ts +2 -12
  129. package/dist/routers/payment.js +11 -393
  130. package/dist/routers/podcast.d.ts +1 -1
  131. package/dist/routers/podcast.js +11 -784
  132. package/dist/routers/studyguide.js +3 -129
  133. package/dist/routers/worksheets.d.ts +29 -14
  134. package/dist/routers/worksheets.js +49 -628
  135. package/dist/routers/workspace.d.ts +27 -71
  136. package/dist/routers/workspace.js +29 -922
  137. package/dist/scripts/purge-deleted-users.js +2 -2
  138. package/dist/server.js +10 -3
  139. package/dist/services/activity/activity-human-description.service.d.ts +13 -0
  140. package/dist/services/activity/activity-human-description.service.js +221 -0
  141. package/dist/services/activity/activity-human-description.service.test.d.ts +1 -0
  142. package/dist/services/activity/activity-human-description.service.test.js +16 -0
  143. package/dist/services/activity/activity-log.service.d.ts +87 -0
  144. package/dist/services/activity/activity-log.service.js +276 -0
  145. package/dist/services/activity/activity-log.service.test.d.ts +1 -0
  146. package/dist/services/activity/activity-log.service.test.js +27 -0
  147. package/dist/services/activity-human-description.service.d.ts +13 -0
  148. package/dist/services/activity-human-description.service.js +221 -0
  149. package/dist/services/activity-human-description.service.test.d.ts +1 -0
  150. package/dist/services/activity-human-description.service.test.js +16 -0
  151. package/dist/services/activity-log.service.d.ts +87 -0
  152. package/dist/services/activity-log.service.js +276 -0
  153. package/dist/services/activity-log.service.test.d.ts +1 -0
  154. package/dist/services/activity-log.service.test.js +27 -0
  155. package/dist/services/admin/admin.service.d.ts +270 -0
  156. package/dist/services/admin/admin.service.js +476 -0
  157. package/dist/services/admin.service.d.ts +270 -0
  158. package/dist/services/admin.service.js +476 -0
  159. package/dist/services/ai/ai-session.service.d.ts +5 -0
  160. package/dist/services/ai/ai-session.service.js +4 -0
  161. package/dist/services/ai-session.service.d.ts +60 -0
  162. package/dist/services/ai-session.service.js +561 -0
  163. package/dist/services/annotation.service.d.ts +177 -0
  164. package/dist/services/annotation.service.js +154 -0
  165. package/dist/services/artifact-notification.service.d.ts +14 -0
  166. package/dist/services/artifact-notification.service.js +20 -0
  167. package/dist/services/artifact-version.service.d.ts +38 -0
  168. package/dist/services/artifact-version.service.js +129 -0
  169. package/dist/services/artifacts/annotation.service.d.ts +177 -0
  170. package/dist/services/artifacts/annotation.service.js +154 -0
  171. package/dist/services/artifacts/artifact-version.service.d.ts +38 -0
  172. package/dist/services/artifacts/artifact-version.service.js +129 -0
  173. package/dist/services/artifacts/chat.service.d.ts +127 -0
  174. package/dist/services/artifacts/chat.service.js +182 -0
  175. package/dist/services/artifacts/study-guide.service.d.ts +18 -0
  176. package/dist/services/artifacts/study-guide.service.js +65 -0
  177. package/dist/services/auth/auth.service.d.ts +94 -0
  178. package/dist/services/auth/auth.service.js +368 -0
  179. package/dist/services/auth.service.d.ts +94 -0
  180. package/dist/services/auth.service.js +368 -0
  181. package/dist/services/base.service.d.ts +14 -0
  182. package/dist/services/base.service.js +14 -0
  183. package/dist/services/billing/payment.service.d.ts +44 -0
  184. package/dist/services/billing/payment.service.js +365 -0
  185. package/dist/services/billing/subscription.service.d.ts +37 -0
  186. package/dist/services/billing/subscription.service.js +654 -0
  187. package/dist/services/billing/usage.service.d.ts +47 -0
  188. package/dist/services/billing/usage.service.js +149 -0
  189. package/dist/services/chat.service.d.ts +127 -0
  190. package/dist/services/chat.service.js +182 -0
  191. package/dist/services/content/copilot.service.d.ts +113 -0
  192. package/dist/services/content/copilot.service.js +439 -0
  193. package/dist/services/content/flashcard-progress.service.d.ts +159 -0
  194. package/dist/services/content/flashcard-progress.service.js +432 -0
  195. package/dist/services/content/flashcard.service.d.ts +184 -0
  196. package/dist/services/content/flashcard.service.js +339 -0
  197. package/dist/services/content/media-analysis.service.d.ts +23 -0
  198. package/dist/services/content/media-analysis.service.js +404 -0
  199. package/dist/services/content/podcast.service.d.ts +267 -0
  200. package/dist/services/content/podcast.service.js +653 -0
  201. package/dist/services/content/worksheet-content.service.d.ts +37 -0
  202. package/dist/services/content/worksheet-content.service.js +84 -0
  203. package/dist/services/content/worksheet-content.service.test.d.ts +1 -0
  204. package/dist/services/content/worksheet-content.service.test.js +69 -0
  205. package/dist/services/content/worksheet-generation.service.d.ts +91 -0
  206. package/dist/services/content/worksheet-generation.service.js +95 -0
  207. package/dist/services/content/worksheet-generation.service.test.d.ts +1 -0
  208. package/dist/services/content/worksheet-generation.service.test.js +20 -0
  209. package/dist/services/content/worksheet.service.d.ts +347 -0
  210. package/dist/services/content/worksheet.service.js +599 -0
  211. package/dist/services/copilot.service.d.ts +116 -0
  212. package/dist/services/copilot.service.js +447 -0
  213. package/dist/services/flashcard-progress.service.d.ts +2 -2
  214. package/dist/services/flashcard-progress.service.js +3 -2
  215. package/dist/services/flashcard.service.d.ts +140 -0
  216. package/dist/services/flashcard.service.js +325 -0
  217. package/dist/services/invitation.service.d.ts +66 -0
  218. package/dist/services/invitation.service.js +348 -0
  219. package/dist/services/knowledge/knowledge-base.service.d.ts +316 -0
  220. package/dist/services/knowledge/knowledge-base.service.js +544 -0
  221. package/dist/services/knowledge-base.service.d.ts +316 -0
  222. package/dist/services/knowledge-base.service.js +536 -0
  223. package/dist/services/media-analysis.service.d.ts +23 -0
  224. package/dist/services/media-analysis.service.js +384 -0
  225. package/dist/services/member.service.d.ts +36 -0
  226. package/dist/services/member.service.js +193 -0
  227. package/dist/services/members/invitation.service.d.ts +66 -0
  228. package/dist/services/members/invitation.service.js +348 -0
  229. package/dist/services/members/member.service.d.ts +36 -0
  230. package/dist/services/members/member.service.js +193 -0
  231. package/dist/services/note.service.d.ts +55 -0
  232. package/dist/services/note.service.js +111 -0
  233. package/dist/services/notification.service.d.ts +214 -0
  234. package/dist/services/notification.service.js +550 -0
  235. package/dist/services/notification.service.test.d.ts +1 -0
  236. package/dist/services/notification.service.test.js +87 -0
  237. package/dist/services/notifications/notification.service.d.ts +214 -0
  238. package/dist/services/notifications/notification.service.js +550 -0
  239. package/dist/services/notifications/notification.service.test.d.ts +1 -0
  240. package/dist/services/notifications/notification.service.test.js +87 -0
  241. package/dist/services/payment.service.d.ts +55 -0
  242. package/dist/services/payment.service.js +368 -0
  243. package/dist/services/podcast.service.d.ts +267 -0
  244. package/dist/services/podcast.service.js +654 -0
  245. package/dist/services/router-module.service.d.ts +7 -0
  246. package/dist/services/router-module.service.js +10 -0
  247. package/dist/services/study-guide.service.d.ts +18 -0
  248. package/dist/services/study-guide.service.js +65 -0
  249. package/dist/services/subscription.service.d.ts +37 -0
  250. package/dist/services/subscription.service.js +654 -0
  251. package/dist/services/usage-limit-policy.service.d.ts +12 -0
  252. package/dist/services/usage-limit-policy.service.js +22 -0
  253. package/dist/services/usage-limit-policy.service.test.d.ts +1 -0
  254. package/dist/services/usage-limit-policy.service.test.js +46 -0
  255. package/dist/services/usage.service.d.ts +27 -0
  256. package/dist/services/usage.service.js +77 -0
  257. package/dist/services/worksheet-content.service.d.ts +42 -0
  258. package/dist/services/worksheet-content.service.js +84 -0
  259. package/dist/services/worksheet-content.service.test.d.ts +1 -0
  260. package/dist/services/worksheet-content.service.test.js +69 -0
  261. package/dist/services/worksheet-generation.service.d.ts +91 -0
  262. package/dist/services/worksheet-generation.service.js +95 -0
  263. package/dist/services/worksheet-generation.service.test.d.ts +1 -0
  264. package/dist/services/worksheet-generation.service.test.js +20 -0
  265. package/dist/services/worksheet.service.d.ts +385 -0
  266. package/dist/services/worksheet.service.js +596 -0
  267. package/dist/services/workspace/workspace-analytics.service.d.ts +24 -0
  268. package/dist/services/workspace/workspace-analytics.service.js +95 -0
  269. package/dist/services/workspace/workspace-kb.service.d.ts +40 -0
  270. package/dist/services/workspace/workspace-kb.service.js +184 -0
  271. package/dist/services/workspace/workspace.service.d.ts +263 -0
  272. package/dist/services/workspace/workspace.service.js +401 -0
  273. package/dist/services/workspace-analytics.service.d.ts +24 -0
  274. package/dist/services/workspace-analytics.service.js +95 -0
  275. package/dist/services/workspace-kb.service.d.ts +40 -0
  276. package/dist/services/workspace-kb.service.js +184 -0
  277. package/dist/services/workspace-progress.service.d.ts +27 -0
  278. package/dist/services/workspace-progress.service.js +56 -0
  279. package/dist/services/workspace-progress.service.test.d.ts +1 -0
  280. package/dist/services/workspace-progress.service.test.js +49 -0
  281. package/dist/services/workspace.service.d.ts +307 -0
  282. package/dist/services/workspace.service.js +390 -0
  283. package/dist/trpc.d.ts +12 -4
  284. package/dist/trpc.js +7 -13
  285. package/package.json +5 -6
  286. package/prisma/migrations/20260509000001_add_knowledge_base/migration.sql +99 -0
  287. package/prisma/migrations/20260509000002_curate_knowledge_base/migration.sql +52 -0
  288. package/prisma/migrations/20260522000000_add_notes/migration.sql +27 -0
  289. package/prisma/migrations/20260524000000_remove_notes/migration.sql +3 -0
  290. package/prisma/schema.prisma +150 -48
  291. package/prisma/seed.mjs +67 -0
  292. package/scripts/debug/README.md +4 -0
  293. package/src/README.md +63 -0
  294. package/src/context.ts +33 -3
  295. package/src/lib/ai/config.ts +34 -0
  296. package/src/lib/ai/embedding-client.ts +47 -0
  297. package/src/lib/ai/index.ts +65 -0
  298. package/src/lib/ai/inference-backend/client.ts +479 -0
  299. package/src/lib/ai/inference-backend/mocks.ts +171 -0
  300. package/src/lib/ai/inference-backend/types.ts +50 -0
  301. package/src/lib/ai/json-parse.ts +35 -0
  302. package/src/lib/ai/llm-client.ts +54 -0
  303. package/src/lib/ai/mock.ts +12 -0
  304. package/src/lib/ai/types.ts +11 -0
  305. package/src/lib/chunking.ts +81 -0
  306. package/src/lib/curated-kb-seed.ts +164 -0
  307. package/src/lib/email.ts +77 -115
  308. package/src/lib/embeddings.ts +9 -0
  309. package/src/lib/ensure-curated-kb-catalog.ts +60 -0
  310. package/src/lib/env.ts +2 -7
  311. package/src/lib/inference.ts +1 -21
  312. package/src/lib/kb-meta.ts +81 -0
  313. package/src/lib/pdf.ts +23 -0
  314. package/src/lib/workspace-kb.ts +7 -0
  315. package/src/repositories/artifact.repository.ts +55 -0
  316. package/src/repositories/base.repository.ts +19 -0
  317. package/src/repositories/invitation.repository.ts +53 -0
  318. package/src/repositories/notification.repository.ts +53 -0
  319. package/src/repositories/user.repository.ts +44 -0
  320. package/src/repositories/workspace-member.repository.ts +38 -0
  321. package/src/repositories/workspace.repository.ts +89 -0
  322. package/src/routers/_app.ts +4 -0
  323. package/src/routers/admin.ts +124 -692
  324. package/src/routers/annotations.ts +25 -203
  325. package/src/routers/artifactVersions.ts +32 -0
  326. package/src/routers/auth.ts +82 -520
  327. package/src/routers/chat.ts +42 -245
  328. package/src/routers/copilot.ts +41 -666
  329. package/src/routers/flashcards.ts +108 -404
  330. package/src/routers/knowledgeBase.ts +216 -0
  331. package/src/routers/members.ts +60 -782
  332. package/src/routers/notifications.ts +15 -117
  333. package/src/routers/payment.ts +37 -446
  334. package/src/routers/podcast.ts +36 -898
  335. package/src/routers/studyguide.ts +5 -144
  336. package/src/routers/worksheets.ts +171 -735
  337. package/src/routers/workspace.ts +141 -1108
  338. package/src/scripts/purge-deleted-users.ts +2 -2
  339. package/src/server.ts +10 -3
  340. package/src/{lib/activity_human_description.test.ts → services/activity/activity-human-description.service.test.ts} +1 -1
  341. package/src/{lib/activity_log_service.test.ts → services/activity/activity-log.service.test.ts} +1 -1
  342. package/src/{lib/activity_log_service.ts → services/activity/activity-log.service.ts} +2 -2
  343. package/src/services/admin/admin.service.ts +612 -0
  344. package/src/services/ai/ai-session.service.ts +5 -0
  345. package/src/services/artifacts/annotation.service.ts +189 -0
  346. package/src/services/artifacts/artifact-version.service.ts +151 -0
  347. package/src/services/artifacts/chat.service.ts +197 -0
  348. package/src/services/artifacts/study-guide.service.ts +72 -0
  349. package/src/services/auth/auth.service.ts +473 -0
  350. package/src/services/base.service.ts +19 -0
  351. package/src/services/billing/payment.service.ts +433 -0
  352. package/src/{lib/subscription_service.ts → services/billing/subscription.service.ts} +5 -5
  353. package/src/services/billing/usage.service.ts +207 -0
  354. package/src/services/content/copilot.service.ts +587 -0
  355. package/src/services/{flashcard-progress.service.ts → content/flashcard-progress.service.ts} +18 -12
  356. package/src/services/content/flashcard.service.ts +417 -0
  357. package/src/services/content/media-analysis.service.ts +561 -0
  358. package/src/services/content/podcast.service.ts +777 -0
  359. package/src/services/content/worksheet-content.service.test.ts +83 -0
  360. package/src/services/content/worksheet-content.service.ts +117 -0
  361. package/src/{lib/worksheet-generation.test.ts → services/content/worksheet-generation.service.test.ts} +3 -3
  362. package/src/services/content/worksheet.service.ts +751 -0
  363. package/src/services/knowledge/knowledge-base.service.ts +705 -0
  364. package/src/services/members/invitation.service.ts +427 -0
  365. package/src/services/members/member.service.ts +241 -0
  366. package/src/{lib/notification-service.test.ts → services/notifications/notification.service.test.ts} +2 -2
  367. package/src/{lib/notification-service.ts → services/notifications/notification.service.ts} +102 -1
  368. package/src/services/workspace/workspace-analytics.service.ts +107 -0
  369. package/src/services/workspace/workspace-kb.service.ts +273 -0
  370. package/src/services/workspace/workspace.service.ts +488 -0
  371. package/src/trpc.ts +7 -15
  372. package/src/lib/ai-session.ts +0 -704
  373. package/src/lib/usage_service.ts +0 -74
  374. package/src/lib/workspace-access.ts +0 -13
  375. /package/{check-difficulty.cjs → scripts/debug/check-difficulty.cjs} +0 -0
  376. /package/{check-questions.cjs → scripts/debug/check-questions.cjs} +0 -0
  377. /package/{db-summary.cjs → scripts/debug/db-summary.cjs} +0 -0
  378. /package/{mcq-test.cjs → scripts/debug/mcq-test.cjs} +0 -0
  379. /package/{test-generate.js → scripts/debug/test-generate.js} +0 -0
  380. /package/{test-ratio.cjs → scripts/debug/test-ratio.cjs} +0 -0
  381. /package/{zod-test.cjs → scripts/debug/zod-test.cjs} +0 -0
  382. /package/src/{lib/activity_human_description.ts → services/activity/activity-human-description.service.ts} +0 -0
  383. /package/src/{lib/worksheet-generation.ts → services/content/worksheet-generation.service.ts} +0 -0
@@ -0,0 +1,27 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { ActivityLogCategory, ActivityLogStatus } from "@prisma/client";
4
+ import { buildActivityLogWhere, inferCategoryFromTrpcPath, redactSensitive, } from "./activity-log.service.js";
5
+ test("redactSensitive redacts sensitive keys", () => {
6
+ const out = redactSensitive({
7
+ email: "a@b.com",
8
+ password: "secret",
9
+ nested: { authToken: "t", safe: 1 },
10
+ });
11
+ assert.equal(out.password, "[REDACTED]");
12
+ const nested = out.nested;
13
+ assert.equal(nested.authToken, "[REDACTED]");
14
+ assert.equal(nested.safe, 1);
15
+ });
16
+ test("inferCategoryFromTrpcPath maps routers", () => {
17
+ assert.equal(inferCategoryFromTrpcPath("auth.login"), ActivityLogCategory.AUTH);
18
+ assert.equal(inferCategoryFromTrpcPath("payment.checkout"), ActivityLogCategory.BILLING);
19
+ assert.equal(inferCategoryFromTrpcPath("admin.listUsers"), ActivityLogCategory.ADMIN);
20
+ assert.equal(inferCategoryFromTrpcPath("workspace.list"), ActivityLogCategory.WORKSPACE);
21
+ assert.equal(inferCategoryFromTrpcPath("flashcards.list"), ActivityLogCategory.CONTENT);
22
+ });
23
+ test("buildActivityLogWhere forces actor for user scope", () => {
24
+ const w = buildActivityLogWhere({ status: ActivityLogStatus.SUCCESS }, { forceActorUserId: "user-1" });
25
+ assert.equal(w.actorUserId, "user-1");
26
+ assert.equal(w.status, ActivityLogStatus.SUCCESS);
27
+ });
@@ -0,0 +1,13 @@
1
+ /**
2
+ * User-facing labels for tRPC paths shown in activity logs.
3
+ * Unknown paths get a best-effort title from the procedure name.
4
+ */
5
+ /**
6
+ * Paths whose curated description contains `query` (case-insensitive).
7
+ * Used so activity log search matches human-readable text, not only raw paths.
8
+ */
9
+ export declare function getTrpcPathsMatchingDescriptionSearch(query: string): string[];
10
+ /**
11
+ * Stable, human-readable line for UI and CSV exports.
12
+ */
13
+ export declare function getActivityHumanDescription(trpcPath: string | null | undefined, actionFallback?: string | null): string;
@@ -0,0 +1,221 @@
1
+ /**
2
+ * User-facing labels for tRPC paths shown in activity logs.
3
+ * Unknown paths get a best-effort title from the procedure name.
4
+ */
5
+ const PATH_LABELS = {
6
+ // Admin
7
+ "admin.getSystemStats": "Viewed system dashboard statistics",
8
+ "admin.listUsers": "Listed users (admin)",
9
+ "admin.listWorkspaces": "Listed workspaces (admin)",
10
+ "admin.updateUserRole": "Changed a user’s role",
11
+ "admin.listPlans": "Listed subscription plans",
12
+ "admin.upsertPlan": "Created or updated a plan",
13
+ "admin.deletePlan": "Deleted or deactivated a plan",
14
+ "admin.getUserInvoices": "Viewed a user’s invoices",
15
+ "admin.getUserDetailedInfo": "Viewed a user’s profile (admin)",
16
+ "admin.debugInvoices": "Ran invoice debug (admin)",
17
+ "admin.listResourcePrices": "Listed resource prices",
18
+ "admin.upsertResourcePrice": "Updated a resource price",
19
+ "admin.listRecentInvoices": "Viewed recent invoices",
20
+ "admin.activityList": "Viewed activity logs (admin)",
21
+ "admin.activityExportCsv": "Exported activity logs to CSV",
22
+ "admin.activityPurgeRetention": "Purged old activity logs (retention)",
23
+ // Workspace
24
+ "workspace.list": "Listed your workspaces",
25
+ "workspace.getTree": "Loaded folders and workspace tree",
26
+ "workspace.create": "Created a workspace",
27
+ "workspace.createFolder": "Created a folder",
28
+ "workspace.updateFolder": "Updated a folder",
29
+ "workspace.deleteFolder": "Deleted a folder",
30
+ "workspace.get": "Opened a workspace",
31
+ "workspace.getStats": "Loaded storage and usage stats",
32
+ "workspace.getStudyAnalytics": "Loaded study streak and analytics",
33
+ "workspace.update": "Updated workspace settings",
34
+ "workspace.delete": "Deleted a workspace",
35
+ "workspace.getFolderInformation": "Loaded folder details",
36
+ "workspace.getSharedWith": "Loaded who a workspace is shared with",
37
+ "workspace.uploadFiles": "Uploaded files",
38
+ "workspace.deleteFiles": "Deleted files",
39
+ "workspace.getFileUploadUrl": "Requested an upload URL",
40
+ "workspace.uploadAndAnalyzeMedia": "Uploaded and analyzed media",
41
+ "workspace.search": "Searched workspaces and content",
42
+ // Payment
43
+ "payment.getPlans": "Viewed available plans",
44
+ "payment.createCheckoutSession": "Started checkout",
45
+ "payment.confirmCheckoutSuccess": "Confirmed checkout",
46
+ "payment.createResourcePurchaseSession": "Started a resource purchase",
47
+ "payment.getUsageOverview": "Viewed plan usage and limits",
48
+ "payment.getResourcePrices": "Viewed resource prices",
49
+ // Notifications
50
+ "notifications.list": "Loaded notifications",
51
+ "notifications.unreadCount": "Checked unread notifications",
52
+ "notifications.markRead": "Marked a notification as read",
53
+ "notifications.markManyRead": "Marked notifications as read",
54
+ "notifications.markAllRead": "Marked all notifications as read",
55
+ "notifications.delete": "Deleted a notification",
56
+ // Members
57
+ "members.getMembers": "Listed workspace members",
58
+ "members.getCurrentUserRole": "Checked your role in a workspace",
59
+ "members.inviteMember": "Sent a workspace invite",
60
+ "members.getInvitations": "Listed pending invites",
61
+ "members.acceptInvite": "Accepted a workspace invite",
62
+ "members.changeMemberRole": "Changed a member’s role",
63
+ "members.removeMember": "Removed a workspace member",
64
+ "members.getPendingInvitations": "Listed invitations",
65
+ "members.cancelInvitation": "Canceled an invitation",
66
+ "members.resendInvitation": "Resent an invitation",
67
+ "members.getAllInvitationsDebug": "Listed invitations (debug)",
68
+ "member.acceptInvite": "Accepted a workspace invite",
69
+ // Flashcards
70
+ "flashcards.listSets": "Listed flashcard sets",
71
+ "flashcards.listCards": "Listed flashcards",
72
+ "flashcards.isGenerating": "Checked flashcard generation status",
73
+ "flashcards.createCard": "Created a flashcard",
74
+ "flashcards.updateCard": "Updated a flashcard",
75
+ "flashcards.gradeTypedAnswer": "Graded a flashcard answer",
76
+ "flashcards.deleteCard": "Deleted a flashcard",
77
+ "flashcards.deleteSet": "Deleted a flashcard set",
78
+ "flashcards.generateFromPrompt": "Generated flashcards from a prompt",
79
+ "flashcards.recordStudyAttempt": "Recorded a flashcard review",
80
+ "flashcards.getSetProgress": "Viewed flashcard progress",
81
+ "flashcards.getDueFlashcards": "Loaded due flashcards",
82
+ "flashcards.getSetStatistics": "Viewed flashcard statistics",
83
+ "flashcards.resetProgress": "Reset flashcard progress",
84
+ "flashcards.recordStudySession": "Recorded a study session",
85
+ // Worksheets
86
+ "worksheets.list": "Listed worksheets",
87
+ "worksheets.listPresets": "Listed worksheet presets",
88
+ "worksheets.createPreset": "Created a worksheet preset",
89
+ "worksheets.updatePreset": "Updated a worksheet preset",
90
+ "worksheets.deletePreset": "Deleted a worksheet preset",
91
+ "worksheets.create": "Created a worksheet",
92
+ "worksheets.get": "Opened a worksheet",
93
+ "worksheets.createWorksheetQuestion": "Added a worksheet question",
94
+ "worksheets.updateWorksheetQuestion": "Updated a worksheet question",
95
+ "worksheets.deleteWorksheetQuestion": "Deleted a worksheet question",
96
+ "worksheets.updateProblemStatus": "Updated worksheet problem status",
97
+ "worksheets.getProgress": "Viewed worksheet progress",
98
+ "worksheets.update": "Updated a worksheet",
99
+ "worksheets.delete": "Deleted a worksheet",
100
+ "worksheets.generateFromPrompt": "Generated a worksheet from a prompt",
101
+ "worksheets.checkAnswer": "Checked a worksheet answer",
102
+ // Podcast
103
+ "podcast.listEpisodes": "Listed podcast episodes",
104
+ "podcast.getEpisode": "Opened a podcast episode",
105
+ "podcast.generateEpisode": "Generated a podcast episode",
106
+ "podcast.deleteSegment": "Deleted a podcast segment",
107
+ "podcast.getEpisodeSchema": "Loaded podcast episode schema",
108
+ "podcast.updateEpisode": "Updated a podcast episode",
109
+ "podcast.deleteEpisode": "Deleted a podcast episode",
110
+ "podcast.getSegment": "Opened a podcast segment",
111
+ "podcast.getAvailableVoices": "Listed podcast voices",
112
+ // Study guide
113
+ "studyguide.get": "Opened or loaded a study guide",
114
+ // Chat
115
+ "chat.getChannels": "Listed chat channels",
116
+ "chat.getChannel": "Opened a chat channel",
117
+ "chat.removeChannel": "Removed a chat channel",
118
+ "chat.editChannel": "Renamed a chat channel",
119
+ "chat.createChannel": "Created a chat channel",
120
+ "chat.postMessage": "Sent a chat message",
121
+ "chat.editMessage": "Edited a chat message",
122
+ "chat.deleteMessage": "Deleted a chat message",
123
+ // Annotations
124
+ "annotations.listHighlights": "Listed study guide highlights",
125
+ "annotations.createHighlight": "Created a highlight",
126
+ "annotations.deleteHighlight": "Deleted a highlight",
127
+ "annotations.addComment": "Added a comment",
128
+ "annotations.updateComment": "Updated a comment",
129
+ "annotations.deleteComment": "Deleted a comment",
130
+ // Copilot
131
+ "copilot.listConversations": "Listed Copilot chats",
132
+ "copilot.getConversation": "Opened a Copilot chat",
133
+ "copilot.createConversation": "Started a Copilot chat",
134
+ "copilot.deleteConversation": "Deleted a Copilot chat",
135
+ "copilot.ask": "Sent a Copilot message",
136
+ "copilot.explainSelection": "Asked Copilot to explain a selection",
137
+ "copilot.suggestHighlights": "Asked Copilot for highlight suggestions",
138
+ "copilot.generateFlashcards": "Generated flashcards with Copilot",
139
+ // Auth (if ever logged)
140
+ "auth.updateProfile": "Updated profile",
141
+ "auth.uploadProfilePicture": "Uploaded a profile picture",
142
+ "auth.confirmProfileUpdate": "Confirmed profile update",
143
+ "auth.signup": "Created an account",
144
+ "auth.verifyEmail": "Verified email",
145
+ "auth.resendVerification": "Resent verification email",
146
+ "auth.login": "Signed in",
147
+ "auth.getSession": "Loaded session",
148
+ "auth.requestAccountDeletion": "Requested account deletion",
149
+ "auth.restoreAccount": "Restored account",
150
+ "auth.logout": "Signed out",
151
+ };
152
+ function splitCamelCase(s) {
153
+ const spaced = s.replace(/([a-z])([A-Z])/g, "$1 $2");
154
+ return spaced.replace(/^\w/, (c) => c.toUpperCase());
155
+ }
156
+ const ROUTER_PREFIX = {
157
+ workspace: "Workspace",
158
+ payment: "Billing",
159
+ notifications: "Notifications",
160
+ admin: "Admin",
161
+ auth: "Account",
162
+ flashcards: "Flashcards",
163
+ worksheets: "Worksheets",
164
+ podcast: "Podcast",
165
+ studyguide: "Study guide",
166
+ chat: "Chat",
167
+ annotations: "Annotations",
168
+ copilot: "Copilot",
169
+ members: "Members",
170
+ };
171
+ function titleCaseRouter(router) {
172
+ return ROUTER_PREFIX[router] ?? splitCamelCase(router);
173
+ }
174
+ /**
175
+ * Paths whose curated description contains `query` (case-insensitive).
176
+ * Used so activity log search matches human-readable text, not only raw paths.
177
+ */
178
+ export function getTrpcPathsMatchingDescriptionSearch(query) {
179
+ const q = query.trim().toLowerCase();
180
+ if (!q)
181
+ return [];
182
+ const paths = [];
183
+ for (const [path, label] of Object.entries(PATH_LABELS)) {
184
+ if (label.toLowerCase().includes(q)) {
185
+ paths.push(path);
186
+ }
187
+ }
188
+ return paths;
189
+ }
190
+ /**
191
+ * Stable, human-readable line for UI and CSV exports.
192
+ */
193
+ export function getActivityHumanDescription(trpcPath, actionFallback) {
194
+ const path = (trpcPath ?? "").trim();
195
+ if (path) {
196
+ const exact = PATH_LABELS[path];
197
+ if (exact)
198
+ return exact;
199
+ const parts = path.split(".").filter(Boolean);
200
+ if (parts.length >= 2) {
201
+ const proc = parts[parts.length - 1];
202
+ const ns = parts[0];
203
+ const rest = parts.slice(1, -1);
204
+ const action = splitCamelCase(proc);
205
+ const area = titleCaseRouter(ns);
206
+ if (rest.length) {
207
+ return `${area} (${rest.join(" › ")}): ${action}`;
208
+ }
209
+ return `${area}: ${action}`;
210
+ }
211
+ return path;
212
+ }
213
+ const act = (actionFallback ?? "").replace(/^trpc\./, "");
214
+ if (act) {
215
+ const exact = PATH_LABELS[act];
216
+ if (exact)
217
+ return exact;
218
+ return act.replace(/\./g, " › ");
219
+ }
220
+ return "Activity";
221
+ }
@@ -0,0 +1,16 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { getActivityHumanDescription, getTrpcPathsMatchingDescriptionSearch, } from "./activity-human-description.service.js";
4
+ test("description search resolves paths by label substring", () => {
5
+ const paths = getTrpcPathsMatchingDescriptionSearch("study streak");
6
+ assert.ok(paths.includes("workspace.getStudyAnalytics"));
7
+ });
8
+ test("known path returns curated label", () => {
9
+ assert.equal(getActivityHumanDescription("workspace.getStudyAnalytics"), "Loaded study streak and analytics");
10
+ assert.equal(getActivityHumanDescription("payment.getUsageOverview"), "Viewed plan usage and limits");
11
+ });
12
+ test("unknown path gets formatted fallback", () => {
13
+ const d = getActivityHumanDescription("someRouter.unknownProcedureName");
14
+ assert.ok(d.includes("Some Router"));
15
+ assert.ok(d.includes("Unknown"));
16
+ });
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Activity log persistence (append-only). Env:
3
+ * - ACTIVITY_LOG_ENABLED=true|false (default true)
4
+ * - ACTIVITY_LOG_SAMPLE_RATE=0..1 (default 1)
5
+ * - ACTIVITY_LOG_RETENTION_DAYS (default 365; used by admin.activityPurgeRetention)
6
+ */
7
+ import type { Prisma, PrismaClient } from "@prisma/client";
8
+ import { ActivityLogCategory, ActivityLogStatus } from "@prisma/client";
9
+ import type { IncomingMessage } from "node:http";
10
+ export declare function isActivityLogEnabled(): boolean;
11
+ export declare function shouldSampleActivity(): boolean;
12
+ /** Recursive redaction for JSON-safe metadata (never log raw credentials). */
13
+ export declare function redactSensitive(value: unknown): unknown;
14
+ export declare function inferCategoryFromTrpcPath(path: string): ActivityLogCategory;
15
+ /** Best-effort workspace id from tRPC input (nested objects included). */
16
+ export declare function extractWorkspaceIdFromInput(raw: unknown): string | undefined;
17
+ export declare function truncateUserAgent(ua: string | undefined, max?: number): string | undefined;
18
+ export declare function getClientIp(req: IncomingMessage): string | undefined;
19
+ export type RecordActivityInput = {
20
+ db: PrismaClient;
21
+ actorUserId: string;
22
+ actorEmailSnapshot?: string | null;
23
+ path: string;
24
+ type: "query" | "mutation" | "subscription";
25
+ status: ActivityLogStatus;
26
+ durationMs: number;
27
+ errorCode?: string | null;
28
+ rawInput?: unknown;
29
+ ipAddress?: string | null;
30
+ userAgent?: string | null;
31
+ httpMethod?: string | null;
32
+ };
33
+ export type RecordExplicitActivityInput = {
34
+ db: PrismaClient;
35
+ actorUserId?: string | null;
36
+ actorEmailSnapshot?: string | null;
37
+ /**
38
+ * A stable action label shown in admin UI/CSV.
39
+ * Example: `cron.purgeDeletedUsers`, `stripe.webhook.checkout.session.completed`
40
+ */
41
+ action: string;
42
+ category: ActivityLogCategory;
43
+ resourceType?: string | null;
44
+ resourceId?: string | null;
45
+ workspaceId?: string | null;
46
+ trpcPath?: string | null;
47
+ httpMethod?: string | null;
48
+ status: ActivityLogStatus;
49
+ errorCode?: string | null;
50
+ durationMs: number;
51
+ ipAddress?: string | null;
52
+ userAgent?: string | null;
53
+ metadata?: unknown;
54
+ /**
55
+ * When true, bypasses ACTIVITY_LOG_SAMPLE_RATE sampling.
56
+ * Useful for low-volume admin-auditable system events (cron/webhooks).
57
+ */
58
+ forceWrite?: boolean;
59
+ };
60
+ /**
61
+ * Persists one activity row. Prefer `scheduleRecordActivity` from request path to avoid blocking.
62
+ */
63
+ export declare function recordActivity(input: RecordActivityInput): Promise<void>;
64
+ export declare function scheduleRecordActivity(input: RecordActivityInput): void;
65
+ /**
66
+ * Persists one explicit (non-tRPC) activity row.
67
+ * This is used for cron jobs, webhooks, and other system-wide background operations.
68
+ */
69
+ export declare function recordExplicitActivity(input: RecordExplicitActivityInput): Promise<void>;
70
+ export declare function scheduleRecordExplicitActivity(input: RecordExplicitActivityInput): void;
71
+ /** Default retention: 365 days. Override with ACTIVITY_LOG_RETENTION_DAYS. */
72
+ export declare function getActivityRetentionDays(): number;
73
+ export declare function deleteActivityLogsOlderThan(db: PrismaClient, cutoff: Date): Promise<{
74
+ deleted: number;
75
+ }>;
76
+ export type ActivityLogFilter = {
77
+ actorUserId?: string;
78
+ workspaceId?: string;
79
+ from?: Date;
80
+ to?: Date;
81
+ category?: ActivityLogCategory;
82
+ status?: ActivityLogStatus;
83
+ search?: string;
84
+ };
85
+ export declare function buildActivityLogWhere(filter: ActivityLogFilter, options?: {
86
+ forceActorUserId?: string;
87
+ }): Prisma.ActivityLogWhereInput;
@@ -0,0 +1,276 @@
1
+ import { ActivityLogCategory } from "@prisma/client";
2
+ import { logger } from "../lib/logger.js";
3
+ import { getTrpcPathsMatchingDescriptionSearch } from "./activity-human-description.service.js";
4
+ const SENSITIVE_KEY_FRAGMENTS = [
5
+ "password",
6
+ "token",
7
+ "secret",
8
+ "authorization",
9
+ "cookie",
10
+ "credit",
11
+ "card",
12
+ "cvv",
13
+ "ssn",
14
+ ];
15
+ export function isActivityLogEnabled() {
16
+ const v = process.env.ACTIVITY_LOG_ENABLED;
17
+ if (v === undefined || v === "")
18
+ return true;
19
+ return v === "1" || v.toLowerCase() === "true";
20
+ }
21
+ function activitySampleRate() {
22
+ const raw = process.env.ACTIVITY_LOG_SAMPLE_RATE;
23
+ if (!raw)
24
+ return 1;
25
+ const n = Number(raw);
26
+ if (!Number.isFinite(n) || n <= 0)
27
+ return 0;
28
+ if (n > 1)
29
+ return 1;
30
+ return n;
31
+ }
32
+ export function shouldSampleActivity() {
33
+ const rate = activitySampleRate();
34
+ if (rate >= 1)
35
+ return true;
36
+ if (rate <= 0)
37
+ return false;
38
+ return Math.random() < rate;
39
+ }
40
+ /** Recursive redaction for JSON-safe metadata (never log raw credentials). */
41
+ export function redactSensitive(value) {
42
+ if (value === null || value === undefined)
43
+ return value;
44
+ if (typeof value === "string") {
45
+ if (value.length > 2000)
46
+ return `${value.slice(0, 2000)}…[truncated]`;
47
+ return value;
48
+ }
49
+ if (typeof value !== "object")
50
+ return value;
51
+ if (Array.isArray(value)) {
52
+ return value.map((v) => redactSensitive(v));
53
+ }
54
+ const out = {};
55
+ for (const [k, v] of Object.entries(value)) {
56
+ const lower = k.toLowerCase();
57
+ if (SENSITIVE_KEY_FRAGMENTS.some((f) => lower.includes(f))) {
58
+ out[k] = "[REDACTED]";
59
+ continue;
60
+ }
61
+ out[k] = redactSensitive(v);
62
+ }
63
+ return out;
64
+ }
65
+ export function inferCategoryFromTrpcPath(path) {
66
+ const p = path.toLowerCase();
67
+ if (p.startsWith("auth."))
68
+ return ActivityLogCategory.AUTH;
69
+ if (p.startsWith("payment."))
70
+ return ActivityLogCategory.BILLING;
71
+ if (p.startsWith("admin."))
72
+ return ActivityLogCategory.ADMIN;
73
+ if (p.startsWith("workspace.") || p.startsWith("members.") || p.startsWith("member."))
74
+ return ActivityLogCategory.WORKSPACE;
75
+ if (p.startsWith("flashcards.") ||
76
+ p.startsWith("worksheets.") ||
77
+ p.startsWith("studyguide.") ||
78
+ p.startsWith("podcast.") ||
79
+ p.startsWith("annotations.") ||
80
+ p.startsWith("chat.") ||
81
+ p.startsWith("copilot."))
82
+ return ActivityLogCategory.CONTENT;
83
+ if (p.startsWith("notifications."))
84
+ return ActivityLogCategory.SYSTEM;
85
+ return ActivityLogCategory.SYSTEM;
86
+ }
87
+ /** Best-effort workspace id from tRPC input (nested objects included). */
88
+ export function extractWorkspaceIdFromInput(raw) {
89
+ if (raw === null || raw === undefined)
90
+ return undefined;
91
+ if (typeof raw === "object" && !Array.isArray(raw)) {
92
+ const o = raw;
93
+ const direct = o.workspaceId ?? o.workspace_id;
94
+ if (typeof direct === "string" && direct.length > 0)
95
+ return direct;
96
+ for (const v of Object.values(o)) {
97
+ const nested = extractWorkspaceIdFromInput(v);
98
+ if (nested)
99
+ return nested;
100
+ }
101
+ }
102
+ if (Array.isArray(raw)) {
103
+ for (const item of raw) {
104
+ const nested = extractWorkspaceIdFromInput(item);
105
+ if (nested)
106
+ return nested;
107
+ }
108
+ }
109
+ return undefined;
110
+ }
111
+ export function truncateUserAgent(ua, max = 512) {
112
+ if (!ua)
113
+ return undefined;
114
+ return ua.length > max ? ua.slice(0, max) : ua;
115
+ }
116
+ export function getClientIp(req) {
117
+ const xf = req.headers["x-forwarded-for"];
118
+ if (typeof xf === "string")
119
+ return xf.split(",")[0]?.trim();
120
+ if (Array.isArray(xf))
121
+ return xf[0]?.split(",")[0]?.trim();
122
+ const ra = req.socket?.remoteAddress;
123
+ return ra ?? undefined;
124
+ }
125
+ function buildMetadata(path, rawInput) {
126
+ if (rawInput === undefined)
127
+ return undefined;
128
+ try {
129
+ const redacted = redactSensitive(rawInput);
130
+ return { trpcInputPreview: redacted };
131
+ }
132
+ catch {
133
+ return { trpcInputPreview: "[unserializable]" };
134
+ }
135
+ }
136
+ function buildExplicitMetadata(metadata) {
137
+ if (metadata === undefined)
138
+ return undefined;
139
+ try {
140
+ return redactSensitive(metadata);
141
+ }
142
+ catch {
143
+ return { metadata: "[unserializable]" };
144
+ }
145
+ }
146
+ /**
147
+ * Persists one activity row. Prefer `scheduleRecordActivity` from request path to avoid blocking.
148
+ */
149
+ export async function recordActivity(input) {
150
+ if (!isActivityLogEnabled())
151
+ return;
152
+ if (!shouldSampleActivity())
153
+ return;
154
+ const category = inferCategoryFromTrpcPath(input.path);
155
+ const workspaceId = extractWorkspaceIdFromInput(input.rawInput) ?? undefined;
156
+ const action = `trpc.${input.path}`;
157
+ const metadata = buildMetadata(input.path, input.rawInput);
158
+ try {
159
+ await input.db.activityLog.create({
160
+ data: {
161
+ actorUserId: input.actorUserId,
162
+ actorEmailSnapshot: input.actorEmailSnapshot ?? undefined,
163
+ action,
164
+ category,
165
+ trpcPath: input.path,
166
+ httpMethod: input.httpMethod ?? undefined,
167
+ status: input.status,
168
+ errorCode: input.errorCode ?? undefined,
169
+ durationMs: input.durationMs,
170
+ workspaceId,
171
+ ipAddress: input.ipAddress ?? undefined,
172
+ userAgent: truncateUserAgent(input.userAgent ?? undefined),
173
+ metadata: metadata ?? undefined,
174
+ },
175
+ });
176
+ }
177
+ catch (e) {
178
+ logger.error(`ActivityLog write failed: ${e instanceof Error ? e.message : String(e)}`, "ACTIVITY");
179
+ }
180
+ }
181
+ export function scheduleRecordActivity(input) {
182
+ void Promise.resolve()
183
+ .then(() => recordActivity(input))
184
+ .catch((e) => logger.error(`ActivityLog async error: ${e instanceof Error ? e.message : String(e)}`, "ACTIVITY"));
185
+ }
186
+ /**
187
+ * Persists one explicit (non-tRPC) activity row.
188
+ * This is used for cron jobs, webhooks, and other system-wide background operations.
189
+ */
190
+ export async function recordExplicitActivity(input) {
191
+ if (!isActivityLogEnabled())
192
+ return;
193
+ if (!input.forceWrite && !shouldSampleActivity())
194
+ return;
195
+ const metadata = buildExplicitMetadata(input.metadata);
196
+ try {
197
+ await input.db.activityLog.create({
198
+ data: {
199
+ actorUserId: input.actorUserId ?? undefined,
200
+ actorEmailSnapshot: input.actorEmailSnapshot ?? undefined,
201
+ action: input.action,
202
+ category: input.category,
203
+ resourceType: input.resourceType ?? undefined,
204
+ resourceId: input.resourceId ?? undefined,
205
+ workspaceId: input.workspaceId ?? undefined,
206
+ trpcPath: input.trpcPath ?? undefined,
207
+ httpMethod: input.httpMethod ?? undefined,
208
+ status: input.status,
209
+ errorCode: input.errorCode ?? undefined,
210
+ durationMs: input.durationMs,
211
+ ipAddress: input.ipAddress ?? undefined,
212
+ userAgent: truncateUserAgent(input.userAgent ?? undefined),
213
+ metadata: metadata ?? undefined,
214
+ },
215
+ });
216
+ }
217
+ catch (e) {
218
+ logger.error(`ActivityLog explicit write failed: ${e instanceof Error ? e.message : String(e)}`, "ACTIVITY");
219
+ }
220
+ }
221
+ export function scheduleRecordExplicitActivity(input) {
222
+ void Promise.resolve()
223
+ .then(() => recordExplicitActivity(input))
224
+ .catch((e) => logger.error(`ActivityLog explicit async error: ${e instanceof Error ? e.message : String(e)}`, "ACTIVITY"));
225
+ }
226
+ /** Default retention: 365 days. Override with ACTIVITY_LOG_RETENTION_DAYS. */
227
+ export function getActivityRetentionDays() {
228
+ const raw = process.env.ACTIVITY_LOG_RETENTION_DAYS;
229
+ if (!raw)
230
+ return 365;
231
+ const n = Number(raw);
232
+ if (!Number.isFinite(n) || n < 1)
233
+ return 365;
234
+ return Math.min(Math.floor(n), 3650);
235
+ }
236
+ export async function deleteActivityLogsOlderThan(db, cutoff) {
237
+ const result = await db.activityLog.deleteMany({
238
+ where: { createdAt: { lt: cutoff } },
239
+ });
240
+ return { deleted: result.count };
241
+ }
242
+ export function buildActivityLogWhere(filter, options) {
243
+ const where = {};
244
+ if (options?.forceActorUserId) {
245
+ where.actorUserId = options.forceActorUserId;
246
+ }
247
+ else if (filter.actorUserId) {
248
+ where.actorUserId = filter.actorUserId;
249
+ }
250
+ if (filter.workspaceId)
251
+ where.workspaceId = filter.workspaceId;
252
+ if (filter.category)
253
+ where.category = filter.category;
254
+ if (filter.status)
255
+ where.status = filter.status;
256
+ if (filter.from || filter.to) {
257
+ where.createdAt = {};
258
+ if (filter.from)
259
+ where.createdAt.gte = filter.from;
260
+ if (filter.to)
261
+ where.createdAt.lte = filter.to;
262
+ }
263
+ if (filter.search?.trim()) {
264
+ const s = filter.search.trim();
265
+ const pathsFromDescription = getTrpcPathsMatchingDescriptionSearch(s);
266
+ where.OR = [
267
+ { action: { contains: s, mode: "insensitive" } },
268
+ { trpcPath: { contains: s, mode: "insensitive" } },
269
+ { errorCode: { contains: s, mode: "insensitive" } },
270
+ ...(pathsFromDescription.length > 0
271
+ ? [{ trpcPath: { in: pathsFromDescription } }]
272
+ : []),
273
+ ];
274
+ }
275
+ return where;
276
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { ActivityLogCategory, ActivityLogStatus } from "@prisma/client";
4
+ import { buildActivityLogWhere, inferCategoryFromTrpcPath, redactSensitive, } from "./activity-log.service.js";
5
+ test("redactSensitive redacts sensitive keys", () => {
6
+ const out = redactSensitive({
7
+ email: "a@b.com",
8
+ password: "secret",
9
+ nested: { authToken: "t", safe: 1 },
10
+ });
11
+ assert.equal(out.password, "[REDACTED]");
12
+ const nested = out.nested;
13
+ assert.equal(nested.authToken, "[REDACTED]");
14
+ assert.equal(nested.safe, 1);
15
+ });
16
+ test("inferCategoryFromTrpcPath maps routers", () => {
17
+ assert.equal(inferCategoryFromTrpcPath("auth.login"), ActivityLogCategory.AUTH);
18
+ assert.equal(inferCategoryFromTrpcPath("payment.checkout"), ActivityLogCategory.BILLING);
19
+ assert.equal(inferCategoryFromTrpcPath("admin.listUsers"), ActivityLogCategory.ADMIN);
20
+ assert.equal(inferCategoryFromTrpcPath("workspace.list"), ActivityLogCategory.WORKSPACE);
21
+ assert.equal(inferCategoryFromTrpcPath("flashcards.list"), ActivityLogCategory.CONTENT);
22
+ });
23
+ test("buildActivityLogWhere forces actor for user scope", () => {
24
+ const w = buildActivityLogWhere({ status: ActivityLogStatus.SUCCESS }, { forceActorUserId: "user-1" });
25
+ assert.equal(w.actorUserId, "user-1");
26
+ assert.equal(w.status, ActivityLogStatus.SUCCESS);
27
+ });