@brightchain/brightchain-api-lib 0.23.25 → 0.24.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 (438) hide show
  1. package/package.json +5 -5
  2. package/src/lib/__tests__/fixtures/mock-backend-brightchain-member.d.ts.map +1 -1
  3. package/src/lib/__tests__/fixtures/mock-backend-brightchain-member.js +11 -0
  4. package/src/lib/__tests__/fixtures/mock-backend-brightchain-member.js.map +1 -1
  5. package/src/lib/__tests__/fixtures/mocked-model.js +63 -60
  6. package/src/lib/__tests__/fixtures/mocked-model.js.map +1 -1
  7. package/src/lib/application.d.ts +1 -1
  8. package/src/lib/application.d.ts.map +1 -1
  9. package/src/lib/application.js +103 -34
  10. package/src/lib/application.js.map +1 -1
  11. package/src/lib/auth/aclEnforcedAvailability.js +4 -0
  12. package/src/lib/auth/aclEnforcedAvailability.js.map +1 -1
  13. package/src/lib/auth/aclEnforcedBlockStore.js +7 -0
  14. package/src/lib/auth/aclEnforcedBlockStore.js.map +1 -1
  15. package/src/lib/auth/ecdsaNodeAuthenticator.js +1 -0
  16. package/src/lib/auth/ecdsaNodeAuthenticator.js.map +1 -1
  17. package/src/lib/auth/poolAclBootstrap.js +2 -0
  18. package/src/lib/auth/poolAclBootstrap.js.map +1 -1
  19. package/src/lib/auth/poolAclStore.js +2 -1
  20. package/src/lib/auth/poolAclStore.js.map +1 -1
  21. package/src/lib/auth/poolAclUpdater.js +4 -0
  22. package/src/lib/auth/poolAclUpdater.js.map +1 -1
  23. package/src/lib/availability/availabilityMetrics.js +43 -45
  24. package/src/lib/availability/availabilityMetrics.js.map +1 -1
  25. package/src/lib/availability/availabilityService.js +26 -20
  26. package/src/lib/availability/availabilityService.js.map +1 -1
  27. package/src/lib/availability/blockRegistry.js +46 -25
  28. package/src/lib/availability/blockRegistry.js.map +1 -1
  29. package/src/lib/availability/discoveryProtocol.js +10 -8
  30. package/src/lib/availability/discoveryProtocol.js.map +1 -1
  31. package/src/lib/availability/gossipService.js +56 -45
  32. package/src/lib/availability/gossipService.js.map +1 -1
  33. package/src/lib/availability/heartbeatMonitor.js +22 -20
  34. package/src/lib/availability/heartbeatMonitor.js.map +1 -1
  35. package/src/lib/availability/poolDiscoveryService.js +7 -1
  36. package/src/lib/availability/poolDiscoveryService.js.map +1 -1
  37. package/src/lib/availability/quorumGossipHandler.js +13 -6
  38. package/src/lib/availability/quorumGossipHandler.js.map +1 -1
  39. package/src/lib/availability/reconciliationService.js +18 -12
  40. package/src/lib/availability/reconciliationService.js.map +1 -1
  41. package/src/lib/blockFetch/blockFetcher.js +14 -8
  42. package/src/lib/blockFetch/blockFetcher.js.map +1 -1
  43. package/src/lib/blockFetch/fetchQueue.js +9 -7
  44. package/src/lib/blockFetch/fetchQueue.js.map +1 -1
  45. package/src/lib/blockFetch/httpBlockFetchTransport.js +2 -0
  46. package/src/lib/blockFetch/httpBlockFetchTransport.js.map +1 -1
  47. package/src/lib/browserKeyring.js +5 -2
  48. package/src/lib/browserKeyring.js.map +1 -1
  49. package/src/lib/controllers/api/blocks.d.ts.map +1 -1
  50. package/src/lib/controllers/api/blocks.js +9 -3
  51. package/src/lib/controllers/api/blocks.js.map +1 -1
  52. package/src/lib/controllers/api/brighthub/connectionController.d.ts +80 -0
  53. package/src/lib/controllers/api/brighthub/connectionController.d.ts.map +1 -0
  54. package/src/lib/controllers/api/brighthub/connectionController.js +890 -0
  55. package/src/lib/controllers/api/brighthub/connectionController.js.map +1 -0
  56. package/src/lib/controllers/api/brighthub/index.d.ts +9 -0
  57. package/src/lib/controllers/api/brighthub/index.d.ts.map +1 -0
  58. package/src/lib/controllers/api/brighthub/index.js +12 -0
  59. package/src/lib/controllers/api/brighthub/index.js.map +1 -0
  60. package/src/lib/controllers/api/brighthub/messagingController.d.ts +84 -0
  61. package/src/lib/controllers/api/brighthub/messagingController.d.ts.map +1 -0
  62. package/src/lib/controllers/api/brighthub/messagingController.js +1077 -0
  63. package/src/lib/controllers/api/brighthub/messagingController.js.map +1 -0
  64. package/src/lib/controllers/api/brighthub/notificationController.d.ts +89 -0
  65. package/src/lib/controllers/api/brighthub/notificationController.d.ts.map +1 -0
  66. package/src/lib/controllers/api/brighthub/notificationController.js +444 -0
  67. package/src/lib/controllers/api/brighthub/notificationController.js.map +1 -0
  68. package/src/lib/controllers/api/brighthub/postController.d.ts +75 -0
  69. package/src/lib/controllers/api/brighthub/postController.d.ts.map +1 -0
  70. package/src/lib/controllers/api/brighthub/postController.js +388 -0
  71. package/src/lib/controllers/api/brighthub/postController.js.map +1 -0
  72. package/src/lib/controllers/api/brighthub/timelineController.d.ts +74 -0
  73. package/src/lib/controllers/api/brighthub/timelineController.d.ts.map +1 -0
  74. package/src/lib/controllers/api/brighthub/timelineController.js +418 -0
  75. package/src/lib/controllers/api/brighthub/timelineController.js.map +1 -0
  76. package/src/lib/controllers/api/brightpass.d.ts.map +1 -1
  77. package/src/lib/controllers/api/brightpass.js +46 -4
  78. package/src/lib/controllers/api/brightpass.js.map +1 -1
  79. package/src/lib/controllers/api/cbl.js +1 -0
  80. package/src/lib/controllers/api/cbl.js.map +1 -1
  81. package/src/lib/controllers/api/channels.js +2 -2
  82. package/src/lib/controllers/api/channels.js.map +1 -1
  83. package/src/lib/controllers/api/conversations.js +1 -1
  84. package/src/lib/controllers/api/conversations.js.map +1 -1
  85. package/src/lib/controllers/api/docs.js +2 -1
  86. package/src/lib/controllers/api/docs.js.map +1 -1
  87. package/src/lib/controllers/api/emails.d.ts.map +1 -1
  88. package/src/lib/controllers/api/emails.js +16 -2
  89. package/src/lib/controllers/api/emails.js.map +1 -1
  90. package/src/lib/controllers/api/explodingMessages.js +2 -4
  91. package/src/lib/controllers/api/explodingMessages.js.map +1 -1
  92. package/src/lib/controllers/api/groups.js +2 -2
  93. package/src/lib/controllers/api/groups.js.map +1 -1
  94. package/src/lib/controllers/api/health.d.ts +9 -2
  95. package/src/lib/controllers/api/health.d.ts.map +1 -1
  96. package/src/lib/controllers/api/health.js +48 -6
  97. package/src/lib/controllers/api/health.js.map +1 -1
  98. package/src/lib/controllers/api/index.d.ts +1 -0
  99. package/src/lib/controllers/api/index.d.ts.map +1 -1
  100. package/src/lib/controllers/api/index.js +1 -0
  101. package/src/lib/controllers/api/index.js.map +1 -1
  102. package/src/lib/controllers/api/introspection.js +2 -0
  103. package/src/lib/controllers/api/introspection.js.map +1 -1
  104. package/src/lib/controllers/api/messages.js +1 -1
  105. package/src/lib/controllers/api/messages.js.map +1 -1
  106. package/src/lib/controllers/api/nodes.js +6 -6
  107. package/src/lib/controllers/api/nodes.js.map +1 -1
  108. package/src/lib/controllers/api/quorum.d.ts +1 -1
  109. package/src/lib/controllers/api/quorum.d.ts.map +1 -1
  110. package/src/lib/controllers/api/quorum.js +3 -2
  111. package/src/lib/controllers/api/quorum.js.map +1 -1
  112. package/src/lib/controllers/api/scbl.js +3 -0
  113. package/src/lib/controllers/api/scbl.js.map +1 -1
  114. package/src/lib/controllers/api/sessions.d.ts +18 -2
  115. package/src/lib/controllers/api/sessions.d.ts.map +1 -1
  116. package/src/lib/controllers/api/sessions.js +60 -3
  117. package/src/lib/controllers/api/sessions.js.map +1 -1
  118. package/src/lib/controllers/api/sync.js +4 -4
  119. package/src/lib/controllers/api/sync.js.map +1 -1
  120. package/src/lib/controllers/api/user.d.ts +2 -0
  121. package/src/lib/controllers/api/user.d.ts.map +1 -1
  122. package/src/lib/controllers/api/user.js +95 -3
  123. package/src/lib/controllers/api/user.js.map +1 -1
  124. package/src/lib/controllers/identity/deviceController.js +2 -2
  125. package/src/lib/controllers/identity/deviceController.js.map +1 -1
  126. package/src/lib/controllers/identity/directoryController.js +1 -1
  127. package/src/lib/controllers/identity/directoryController.js.map +1 -1
  128. package/src/lib/controllers/identity/identityProofController.js +4 -6
  129. package/src/lib/controllers/identity/identityProofController.js.map +1 -1
  130. package/src/lib/databaseInit.d.ts +3 -3
  131. package/src/lib/databaseInit.js +5 -5
  132. package/src/lib/databaseInit.js.map +1 -1
  133. package/src/lib/datastore/block-document-store.d.ts.map +1 -1
  134. package/src/lib/datastore/block-document-store.js +18 -6
  135. package/src/lib/datastore/block-document-store.js.map +1 -1
  136. package/src/lib/datastore/document-store.d.ts.map +1 -1
  137. package/src/lib/datastore/memory-document-store.js +5 -2
  138. package/src/lib/datastore/memory-document-store.js.map +1 -1
  139. package/src/lib/encryption/encryptedMetadataService.js +2 -0
  140. package/src/lib/encryption/encryptedMetadataService.js.map +1 -1
  141. package/src/lib/encryption/encryptionAwareReplication.js +3 -0
  142. package/src/lib/encryption/encryptionAwareReplication.js.map +1 -1
  143. package/src/lib/encryption/errors.d.ts.map +1 -1
  144. package/src/lib/encryption/errors.js +8 -0
  145. package/src/lib/encryption/errors.js.map +1 -1
  146. package/src/lib/encryption/poolKeyManager.js +2 -0
  147. package/src/lib/encryption/poolKeyManager.js.map +1 -1
  148. package/src/lib/environment.js +10 -0
  149. package/src/lib/environment.js.map +1 -1
  150. package/src/lib/errors/express-validation.js +1 -0
  151. package/src/lib/errors/express-validation.js.map +1 -1
  152. package/src/lib/errors/invalid-backup-code-version.js +1 -0
  153. package/src/lib/errors/invalid-backup-code-version.js.map +1 -1
  154. package/src/lib/errors/memberIndexSchemaValidationError.js +1 -0
  155. package/src/lib/errors/memberIndexSchemaValidationError.js.map +1 -1
  156. package/src/lib/errors/missing-validated-data.js +2 -0
  157. package/src/lib/errors/missing-validated-data.js.map +1 -1
  158. package/src/lib/errors/token-not-found.js +1 -0
  159. package/src/lib/errors/token-not-found.js.map +1 -1
  160. package/src/lib/errors/typed-error-local.js +3 -0
  161. package/src/lib/errors/typed-error-local.js.map +1 -1
  162. package/src/lib/interfaces/brightpass/index.d.ts +1 -1
  163. package/src/lib/interfaces/brightpass/index.d.ts.map +1 -1
  164. package/src/lib/interfaces/environment.d.ts +1 -1
  165. package/src/lib/interfaces/environment.d.ts.map +1 -1
  166. package/src/lib/interfaces/member/memberProfileResponse.d.ts.map +1 -1
  167. package/src/lib/interfaces/member/operational.d.ts.map +1 -1
  168. package/src/lib/interfaces/member-init-config.d.ts +1 -1
  169. package/src/lib/interfaces/member-init-config.d.ts.map +1 -1
  170. package/src/lib/interfaces/responses/api-backup-codes-response.d.ts.map +1 -1
  171. package/src/lib/interfaces/responses/api-challenge-response.d.ts.map +1 -1
  172. package/src/lib/interfaces/responses/api-code-count-response.d.ts.map +1 -1
  173. package/src/lib/interfaces/responses/api-detailed-health-response.d.ts.map +1 -1
  174. package/src/lib/interfaces/responses/api-discover-block-response.d.ts.map +1 -1
  175. package/src/lib/interfaces/responses/api-express-validation-error.d.ts.map +1 -1
  176. package/src/lib/interfaces/responses/api-get-block-response.d.ts.map +1 -1
  177. package/src/lib/interfaces/responses/api-get-node-response.d.ts.map +1 -1
  178. package/src/lib/interfaces/responses/api-health-response.d.ts.map +1 -1
  179. package/src/lib/interfaces/responses/api-list-nodes-response.d.ts.map +1 -1
  180. package/src/lib/interfaces/responses/api-login-response.d.ts.map +1 -1
  181. package/src/lib/interfaces/responses/api-members-response.d.ts.map +1 -1
  182. package/src/lib/interfaces/responses/api-mnemonic-response.d.ts.map +1 -1
  183. package/src/lib/interfaces/responses/api-reconcile-response.d.ts.map +1 -1
  184. package/src/lib/interfaces/responses/api-register-node-response.d.ts.map +1 -1
  185. package/src/lib/interfaces/responses/api-registration-response.d.ts.map +1 -1
  186. package/src/lib/interfaces/responses/api-replicate-block-response.d.ts.map +1 -1
  187. package/src/lib/interfaces/responses/api-request-user-response.d.ts.map +1 -1
  188. package/src/lib/interfaces/responses/api-store-block-response.d.ts.map +1 -1
  189. package/src/lib/interfaces/responses/api-store-cbl-response.d.ts.map +1 -1
  190. package/src/lib/interfaces/responses/api-sync-request-response.d.ts.map +1 -1
  191. package/src/lib/interfaces/responses/brighthub/api-connection-response.d.ts +21 -0
  192. package/src/lib/interfaces/responses/brighthub/api-connection-response.d.ts.map +1 -0
  193. package/src/lib/interfaces/responses/brighthub/api-connection-response.js +3 -0
  194. package/src/lib/interfaces/responses/brighthub/api-connection-response.js.map +1 -0
  195. package/src/lib/interfaces/responses/brighthub/api-messaging-response.d.ts +39 -0
  196. package/src/lib/interfaces/responses/brighthub/api-messaging-response.d.ts.map +1 -0
  197. package/src/lib/interfaces/responses/brighthub/api-messaging-response.js +3 -0
  198. package/src/lib/interfaces/responses/brighthub/api-messaging-response.js.map +1 -0
  199. package/src/lib/interfaces/responses/brighthub/api-notification-response.d.ts +30 -0
  200. package/src/lib/interfaces/responses/brighthub/api-notification-response.d.ts.map +1 -0
  201. package/src/lib/interfaces/responses/brighthub/api-notification-response.js +3 -0
  202. package/src/lib/interfaces/responses/brighthub/api-notification-response.js.map +1 -0
  203. package/src/lib/interfaces/responses/brighthub/api-post-response.d.ts +21 -0
  204. package/src/lib/interfaces/responses/brighthub/api-post-response.d.ts.map +1 -0
  205. package/src/lib/interfaces/responses/brighthub/api-post-response.js +3 -0
  206. package/src/lib/interfaces/responses/brighthub/api-post-response.js.map +1 -0
  207. package/src/lib/interfaces/responses/brighthub/api-timeline-response.d.ts +14 -0
  208. package/src/lib/interfaces/responses/brighthub/api-timeline-response.d.ts.map +1 -0
  209. package/src/lib/interfaces/responses/brighthub/api-timeline-response.js +3 -0
  210. package/src/lib/interfaces/responses/brighthub/api-timeline-response.js.map +1 -0
  211. package/src/lib/interfaces/responses/brighthub/api-user-profile-response.d.ts +10 -0
  212. package/src/lib/interfaces/responses/brighthub/api-user-profile-response.d.ts.map +1 -0
  213. package/src/lib/interfaces/responses/brighthub/api-user-profile-response.js +3 -0
  214. package/src/lib/interfaces/responses/brighthub/api-user-profile-response.js.map +1 -0
  215. package/src/lib/interfaces/responses/brighthub/index.d.ts +13 -0
  216. package/src/lib/interfaces/responses/brighthub/index.d.ts.map +1 -0
  217. package/src/lib/interfaces/responses/brighthub/index.js +9 -0
  218. package/src/lib/interfaces/responses/brighthub/index.js.map +1 -0
  219. package/src/lib/interfaces/responses/index.d.ts +1 -0
  220. package/src/lib/interfaces/responses/index.d.ts.map +1 -1
  221. package/src/lib/interfaces/storage/storedDocumentTypes.d.ts +1 -1
  222. package/src/lib/interfaces/storage/storedDocumentTypes.js +1 -1
  223. package/src/lib/interfaces/storage/userRoleSchema.d.ts.map +1 -1
  224. package/src/lib/interfaces/storage/userRoleSchema.js +1 -3
  225. package/src/lib/interfaces/storage/userRoleSchema.js.map +1 -1
  226. package/src/lib/middlewares.js +31 -31
  227. package/src/lib/middlewares.js.map +1 -1
  228. package/src/lib/nodeKeyring.js +2 -0
  229. package/src/lib/nodeKeyring.js.map +1 -1
  230. package/src/lib/plugins/brightchain-database-plugin.d.ts +14 -14
  231. package/src/lib/plugins/brightchain-database-plugin.d.ts.map +1 -1
  232. package/src/lib/plugins/brightchain-database-plugin.js +36 -34
  233. package/src/lib/plugins/brightchain-database-plugin.js.map +1 -1
  234. package/src/lib/routers/api.d.ts +62 -1
  235. package/src/lib/routers/api.d.ts.map +1 -1
  236. package/src/lib/routers/api.js +123 -2
  237. package/src/lib/routers/api.js.map +1 -1
  238. package/src/lib/routers/app.d.ts.map +1 -1
  239. package/src/lib/routers/app.js +10 -1
  240. package/src/lib/routers/app.js.map +1 -1
  241. package/src/lib/routers/base.js +2 -0
  242. package/src/lib/routers/base.js.map +1 -1
  243. package/src/lib/secureEnclaveKeyring.js +4 -2
  244. package/src/lib/secureEnclaveKeyring.js.map +1 -1
  245. package/src/lib/services/auth.d.ts.map +1 -1
  246. package/src/lib/services/auth.js +11 -2
  247. package/src/lib/services/auth.js.map +1 -1
  248. package/src/lib/services/base.js +1 -0
  249. package/src/lib/services/base.js.map +1 -1
  250. package/src/lib/services/blockServiceFactory.js +2 -0
  251. package/src/lib/services/blockServiceFactory.js.map +1 -1
  252. package/src/lib/services/blockStore.js +3 -2
  253. package/src/lib/services/blockStore.js.map +1 -1
  254. package/src/lib/services/blocks.js +1 -0
  255. package/src/lib/services/blocks.js.map +1 -1
  256. package/src/lib/services/brightChainBackupCodeService.d.ts +114 -0
  257. package/src/lib/services/brightChainBackupCodeService.d.ts.map +1 -0
  258. package/src/lib/services/brightChainBackupCodeService.js +303 -0
  259. package/src/lib/services/brightChainBackupCodeService.js.map +1 -0
  260. package/src/lib/services/brightchain-authentication-provider.d.ts.map +1 -1
  261. package/src/lib/services/brightchain-authentication-provider.js +40 -9
  262. package/src/lib/services/brightchain-authentication-provider.js.map +1 -1
  263. package/src/lib/services/brightchain-member-init.service.d.ts +17 -17
  264. package/src/lib/services/brightchain-member-init.service.d.ts.map +1 -1
  265. package/src/lib/services/brightchain-member-init.service.js +12 -9
  266. package/src/lib/services/brightchain-member-init.service.js.map +1 -1
  267. package/src/lib/services/brighthub/connectionService.d.ts +286 -0
  268. package/src/lib/services/brighthub/connectionService.d.ts.map +1 -0
  269. package/src/lib/services/brighthub/connectionService.js +1887 -0
  270. package/src/lib/services/brighthub/connectionService.js.map +1 -0
  271. package/src/lib/services/brighthub/discoveryService.d.ts +110 -0
  272. package/src/lib/services/brighthub/discoveryService.d.ts.map +1 -0
  273. package/src/lib/services/brighthub/discoveryService.js +528 -0
  274. package/src/lib/services/brighthub/discoveryService.js.map +1 -0
  275. package/src/lib/services/brighthub/feedService.d.ts +141 -0
  276. package/src/lib/services/brighthub/feedService.d.ts.map +1 -0
  277. package/src/lib/services/brighthub/feedService.js +418 -0
  278. package/src/lib/services/brighthub/feedService.js.map +1 -0
  279. package/src/lib/services/brighthub/index.d.ts +11 -0
  280. package/src/lib/services/brighthub/index.d.ts.map +1 -0
  281. package/src/lib/services/brighthub/index.js +14 -0
  282. package/src/lib/services/brighthub/index.js.map +1 -0
  283. package/src/lib/services/brighthub/messagingService.d.ts +109 -0
  284. package/src/lib/services/brighthub/messagingService.d.ts.map +1 -0
  285. package/src/lib/services/brighthub/messagingService.js +947 -0
  286. package/src/lib/services/brighthub/messagingService.js.map +1 -0
  287. package/src/lib/services/brighthub/messagingService.test-helpers.d.ts +75 -0
  288. package/src/lib/services/brighthub/messagingService.test-helpers.d.ts.map +1 -0
  289. package/src/lib/services/brighthub/messagingService.test-helpers.js +237 -0
  290. package/src/lib/services/brighthub/messagingService.test-helpers.js.map +1 -0
  291. package/src/lib/services/brighthub/notificationService.d.ts +172 -0
  292. package/src/lib/services/brighthub/notificationService.d.ts.map +1 -0
  293. package/src/lib/services/brighthub/notificationService.js +768 -0
  294. package/src/lib/services/brighthub/notificationService.js.map +1 -0
  295. package/src/lib/services/brighthub/notificationService.test-helpers.d.ts +75 -0
  296. package/src/lib/services/brighthub/notificationService.test-helpers.d.ts.map +1 -0
  297. package/src/lib/services/brighthub/notificationService.test-helpers.js +230 -0
  298. package/src/lib/services/brighthub/notificationService.test-helpers.js.map +1 -0
  299. package/src/lib/services/brighthub/postService.d.ts +129 -0
  300. package/src/lib/services/brighthub/postService.d.ts.map +1 -0
  301. package/src/lib/services/brighthub/postService.js +470 -0
  302. package/src/lib/services/brighthub/postService.js.map +1 -0
  303. package/src/lib/services/brighthub/postService.test-helpers.d.ts +40 -0
  304. package/src/lib/services/brighthub/postService.test-helpers.d.ts.map +1 -0
  305. package/src/lib/services/brighthub/postService.test-helpers.js +84 -0
  306. package/src/lib/services/brighthub/postService.test-helpers.js.map +1 -0
  307. package/src/lib/services/brighthub/textFormatter.d.ts +64 -0
  308. package/src/lib/services/brighthub/textFormatter.d.ts.map +1 -0
  309. package/src/lib/services/brighthub/textFormatter.js +256 -0
  310. package/src/lib/services/brighthub/textFormatter.js.map +1 -0
  311. package/src/lib/services/brighthub/threadService.d.ts +79 -0
  312. package/src/lib/services/brighthub/threadService.d.ts.map +1 -0
  313. package/src/lib/services/brighthub/threadService.js +246 -0
  314. package/src/lib/services/brighthub/threadService.js.map +1 -0
  315. package/src/lib/services/brighthub/userProfileService.d.ts +203 -0
  316. package/src/lib/services/brighthub/userProfileService.d.ts.map +1 -0
  317. package/src/lib/services/brighthub/userProfileService.js +868 -0
  318. package/src/lib/services/brighthub/userProfileService.js.map +1 -0
  319. package/src/lib/services/brighthub/userProfileService.test-helpers.d.ts +86 -0
  320. package/src/lib/services/brighthub/userProfileService.test-helpers.d.ts.map +1 -0
  321. package/src/lib/services/brighthub/userProfileService.test-helpers.js +169 -0
  322. package/src/lib/services/brighthub/userProfileService.test-helpers.js.map +1 -0
  323. package/src/lib/services/brighthub/webSocketServer.d.ts +68 -0
  324. package/src/lib/services/brighthub/webSocketServer.d.ts.map +1 -0
  325. package/src/lib/services/brighthub/webSocketServer.js +194 -0
  326. package/src/lib/services/brighthub/webSocketServer.js.map +1 -0
  327. package/src/lib/services/brightpass/auditLogger.js +10 -5
  328. package/src/lib/services/brightpass/auditLogger.js.map +1 -1
  329. package/src/lib/services/brightpass/vaultEncryption.js +8 -8
  330. package/src/lib/services/brightpass/vaultEncryption.js.map +1 -1
  331. package/src/lib/services/brightpass.js +16 -8
  332. package/src/lib/services/brightpass.js.map +1 -1
  333. package/src/lib/services/cliOperatorPrompt.js +5 -2
  334. package/src/lib/services/cliOperatorPrompt.js.map +1 -1
  335. package/src/lib/services/clientWebSocketServer.d.ts +69 -4
  336. package/src/lib/services/clientWebSocketServer.d.ts.map +1 -1
  337. package/src/lib/services/clientWebSocketServer.js +188 -10
  338. package/src/lib/services/clientWebSocketServer.js.map +1 -1
  339. package/src/lib/services/contentAwareBlocksService.js +2 -0
  340. package/src/lib/services/contentAwareBlocksService.js.map +1 -1
  341. package/src/lib/services/contentIngestionService.d.ts.map +1 -1
  342. package/src/lib/services/contentIngestionService.js +2 -0
  343. package/src/lib/services/contentIngestionService.js.map +1 -1
  344. package/src/lib/services/diskQuorumService.js +3 -0
  345. package/src/lib/services/diskQuorumService.js.map +1 -1
  346. package/src/lib/services/email.js +5 -0
  347. package/src/lib/services/email.js.map +1 -1
  348. package/src/lib/services/eventNotificationSystem.d.ts +51 -10
  349. package/src/lib/services/eventNotificationSystem.d.ts.map +1 -1
  350. package/src/lib/services/eventNotificationSystem.js +76 -23
  351. package/src/lib/services/eventNotificationSystem.js.map +1 -1
  352. package/src/lib/services/expirationScheduler.js +6 -3
  353. package/src/lib/services/expirationScheduler.js.map +1 -1
  354. package/src/lib/services/fakeEmailService.js +3 -4
  355. package/src/lib/services/fakeEmailService.js.map +1 -1
  356. package/src/lib/services/fec.js +1 -3
  357. package/src/lib/services/fec.js.map +1 -1
  358. package/src/lib/services/fecServiceFactory.js +2 -2
  359. package/src/lib/services/fecServiceFactory.js.map +1 -1
  360. package/src/lib/services/fecUsageExample.js +1 -3
  361. package/src/lib/services/fecUsageExample.js.map +1 -1
  362. package/src/lib/services/identityExpirationScheduler.d.ts.map +1 -1
  363. package/src/lib/services/identityExpirationScheduler.js +7 -2
  364. package/src/lib/services/identityExpirationScheduler.js.map +1 -1
  365. package/src/lib/services/index.d.ts +3 -2
  366. package/src/lib/services/index.d.ts.map +1 -1
  367. package/src/lib/services/index.js +3 -2
  368. package/src/lib/services/index.js.map +1 -1
  369. package/src/lib/services/messageEventsWebSocketHandler.js +1 -0
  370. package/src/lib/services/messageEventsWebSocketHandler.js.map +1 -1
  371. package/src/lib/services/messagePassingService.js +5 -0
  372. package/src/lib/services/messagePassingService.js.map +1 -1
  373. package/src/lib/services/nativeRsFecService.js +3 -5
  374. package/src/lib/services/nativeRsFecService.js.map +1 -1
  375. package/src/lib/services/presenceService.js +5 -4
  376. package/src/lib/services/presenceService.js.map +1 -1
  377. package/src/lib/services/quorum.js +3 -2
  378. package/src/lib/services/quorum.js.map +1 -1
  379. package/src/lib/services/quorumDatabaseAdapter.d.ts +5 -5
  380. package/src/lib/services/quorumDatabaseAdapter.d.ts.map +1 -1
  381. package/src/lib/services/quorumDatabaseAdapter.js +5 -3
  382. package/src/lib/services/quorumDatabaseAdapter.js.map +1 -1
  383. package/src/lib/services/rbac-input-builder.js +5 -0
  384. package/src/lib/services/rbac-input-builder.js.map +1 -1
  385. package/src/lib/services/secureKeyStorage.js +3 -0
  386. package/src/lib/services/secureKeyStorage.js.map +1 -1
  387. package/src/lib/services/sessionAdapter.d.ts +4 -4
  388. package/src/lib/services/sessionAdapter.d.ts.map +1 -1
  389. package/src/lib/services/sessionAdapter.js +3 -2
  390. package/src/lib/services/sessionAdapter.js.map +1 -1
  391. package/src/lib/services/webSocketMessageServer.js +7 -4
  392. package/src/lib/services/webSocketMessageServer.js.map +1 -1
  393. package/src/lib/services/webSocketPeerProvider.js +2 -1
  394. package/src/lib/services/webSocketPeerProvider.js.map +1 -1
  395. package/src/lib/services/websocketHandler.js +9 -1
  396. package/src/lib/services/websocketHandler.js.map +1 -1
  397. package/src/lib/shared-types.d.ts +1 -2
  398. package/src/lib/shared-types.d.ts.map +1 -1
  399. package/src/lib/stores/__tests__/helpers/mockCloudBlockStore.d.ts +63 -0
  400. package/src/lib/stores/__tests__/helpers/mockCloudBlockStore.d.ts.map +1 -0
  401. package/src/lib/stores/__tests__/helpers/mockCloudBlockStore.js +160 -0
  402. package/src/lib/stores/__tests__/helpers/mockCloudBlockStore.js.map +1 -0
  403. package/src/lib/stores/availabilityAwareBlockStore.js +34 -5
  404. package/src/lib/stores/availabilityAwareBlockStore.js.map +1 -1
  405. package/src/lib/stores/cloudBlockStoreBase.d.ts +121 -0
  406. package/src/lib/stores/cloudBlockStoreBase.d.ts.map +1 -0
  407. package/src/lib/stores/cloudBlockStoreBase.js +1165 -0
  408. package/src/lib/stores/cloudBlockStoreBase.js.map +1 -0
  409. package/src/lib/stores/diskBlockAsyncStore.js +9 -5
  410. package/src/lib/stores/diskBlockAsyncStore.js.map +1 -1
  411. package/src/lib/stores/diskBlockMetadataStore.js +2 -0
  412. package/src/lib/stores/diskBlockMetadataStore.js.map +1 -1
  413. package/src/lib/stores/diskBlockStore.js +10 -8
  414. package/src/lib/stores/diskBlockStore.js.map +1 -1
  415. package/src/lib/stores/diskCBLStore.d.ts.map +1 -1
  416. package/src/lib/stores/diskCBLStore.js +8 -0
  417. package/src/lib/stores/diskCBLStore.js.map +1 -1
  418. package/src/lib/stores/index.d.ts +1 -0
  419. package/src/lib/stores/index.d.ts.map +1 -1
  420. package/src/lib/stores/index.js +1 -0
  421. package/src/lib/stores/index.js.map +1 -1
  422. package/src/lib/systemKeyring.d.ts.map +1 -1
  423. package/src/lib/systemKeyring.js +5 -4
  424. package/src/lib/systemKeyring.js.map +1 -1
  425. package/src/lib/transforms/checksumTransform.js +1 -0
  426. package/src/lib/transforms/checksumTransform.js.map +1 -1
  427. package/src/lib/transforms/memoryWritableStream.js +1 -0
  428. package/src/lib/transforms/memoryWritableStream.js.map +1 -1
  429. package/src/lib/transforms/xorMultipleTransform.js +3 -0
  430. package/src/lib/transforms/xorMultipleTransform.js.map +1 -1
  431. package/src/lib/utils/rehydration.d.ts +1 -1
  432. package/src/lib/utils/rehydration.js +1 -1
  433. package/src/lib/utils/serialization.d.ts +1 -1
  434. package/src/lib/utils/serialization.js +1 -1
  435. package/src/lib/services/backupCodeService.d.ts +0 -35
  436. package/src/lib/services/backupCodeService.d.ts.map +0 -1
  437. package/src/lib/services/backupCodeService.js +0 -109
  438. package/src/lib/services/backupCodeService.js.map +0 -1
@@ -0,0 +1,1887 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ConnectionService = void 0;
4
+ exports.createConnectionService = createConnectionService;
5
+ const brighthub_lib_1 = require("@brightchain/brighthub-lib");
6
+ const crypto_1 = require("crypto");
7
+ /**
8
+ * Default pagination limit
9
+ */
10
+ const DEFAULT_PAGE_LIMIT = 20;
11
+ /**
12
+ * Maximum pagination limit
13
+ */
14
+ const MAX_PAGE_LIMIT = 100;
15
+ /**
16
+ * Connection_Service implementation (list management)
17
+ * Handles connection lists, membership, and list lifecycle
18
+ * @see Requirements: 19.1-19.12
19
+ */
20
+ class ConnectionService {
21
+ listsCollection;
22
+ listMembersCollection;
23
+ userProfilesCollection;
24
+ categoriesCollection;
25
+ categoryAssignmentsCollection;
26
+ notesCollection;
27
+ followsCollection;
28
+ metadataCollection;
29
+ temporaryMutesCollection;
30
+ hubsCollection;
31
+ hubMembersCollection;
32
+ blocksCollection;
33
+ listFollowersCollection;
34
+ /** Tracks last import timestamp per user for rate limiting */
35
+ lastImportTimestamps = new Map();
36
+ /** Cache for mutual connection counts with TTL of 5 minutes */
37
+ mutualConnectionCache = new Map();
38
+ /** TTL for mutual connection cache entries in milliseconds (5 minutes) */
39
+ static MUTUAL_CACHE_TTL_MS = 5 * 60 * 1000;
40
+ constructor(application) {
41
+ this.listsCollection = application.getModel('brighthub_connection_lists');
42
+ this.listMembersCollection =
43
+ application.getModel('brighthub_connection_list_members');
44
+ this.userProfilesCollection = application.getModel('brighthub_user_profiles');
45
+ this.categoriesCollection = application.getModel('brighthub_connection_categories');
46
+ this.categoryAssignmentsCollection =
47
+ application.getModel('brighthub_connection_category_assignments');
48
+ this.notesCollection = application.getModel('brighthub_connection_notes');
49
+ this.followsCollection =
50
+ application.getModel('brighthub_follows');
51
+ this.metadataCollection = application.getModel('brighthub_connection_metadata');
52
+ this.temporaryMutesCollection = application.getModel('brighthub_temporary_mutes');
53
+ this.hubsCollection = application.getModel('brighthub_hubs');
54
+ this.hubMembersCollection = application.getModel('brighthub_hub_members');
55
+ this.blocksCollection =
56
+ application.getModel('brighthub_blocks');
57
+ this.listFollowersCollection = application.getModel('brighthub_connection_list_followers');
58
+ }
59
+ /**
60
+ * Convert a database record to the API response format
61
+ */
62
+ recordToList(record) {
63
+ return {
64
+ _id: record._id,
65
+ ownerId: record.ownerId,
66
+ name: record.name,
67
+ description: record.description,
68
+ visibility: record.visibility,
69
+ memberCount: record.memberCount,
70
+ followerCount: record.followerCount,
71
+ createdAt: record.createdAt,
72
+ updatedAt: record.updatedAt,
73
+ };
74
+ }
75
+ /**
76
+ * Convert a user profile record to the API response format
77
+ */
78
+ recordToProfile(record) {
79
+ return {
80
+ _id: record._id,
81
+ username: record.username,
82
+ displayName: record.displayName,
83
+ bio: record.bio,
84
+ profilePictureUrl: record.profilePictureUrl,
85
+ headerImageUrl: record.headerImageUrl,
86
+ location: record.location,
87
+ websiteUrl: record.websiteUrl,
88
+ followerCount: record.followerCount,
89
+ followingCount: record.followingCount,
90
+ postCount: record.postCount,
91
+ isVerified: record.isVerified,
92
+ isProtected: record.isProtected,
93
+ approveFollowersMode: record.approveFollowersMode,
94
+ privacySettings: record.privacySettings,
95
+ createdAt: record.createdAt,
96
+ };
97
+ }
98
+ /**
99
+ * Convert a category database record to the API response format
100
+ */
101
+ recordToCategory(record) {
102
+ return {
103
+ _id: record._id,
104
+ ownerId: record.ownerId,
105
+ name: record.name,
106
+ color: record.color,
107
+ icon: record.icon,
108
+ isDefault: record.isDefault,
109
+ createdAt: record.createdAt,
110
+ };
111
+ }
112
+ /**
113
+ * Get effective pagination limit
114
+ */
115
+ getLimit(options) {
116
+ const limit = options?.limit ?? DEFAULT_PAGE_LIMIT;
117
+ return Math.min(Math.max(1, limit), MAX_PAGE_LIMIT);
118
+ }
119
+ /**
120
+ * Validate and retrieve a list, ensuring the caller is the owner
121
+ */
122
+ async getOwnedList(listId, ownerId) {
123
+ const list = await this.listsCollection
124
+ .findOne({ _id: listId })
125
+ .exec();
126
+ if (!list) {
127
+ throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.ListNotFound, `List with ID '${listId}' not found`);
128
+ }
129
+ if (list.ownerId !== ownerId) {
130
+ throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.ListNotAuthorized, 'Not authorized to modify this list');
131
+ }
132
+ return list;
133
+ }
134
+ // ═══════════════════════════════════════════════════════
135
+ // List CRUD Operations
136
+ // ═══════════════════════════════════════════════════════
137
+ async createList(ownerId, name, options) {
138
+ // Validate name
139
+ if (!name || !name.trim()) {
140
+ throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.InvalidListName, 'List name cannot be empty');
141
+ }
142
+ // Enforce 100 lists per user limit
143
+ const existingLists = await this.listsCollection
144
+ .find({ ownerId })
145
+ .exec();
146
+ if (existingLists.length >= brighthub_lib_1.MAX_LISTS_PER_USER) {
147
+ throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.ListLimitExceeded, `Cannot create more than ${brighthub_lib_1.MAX_LISTS_PER_USER} lists`);
148
+ }
149
+ const now = new Date().toISOString();
150
+ const record = {
151
+ _id: (0, crypto_1.randomUUID)(),
152
+ ownerId,
153
+ name: name.trim(),
154
+ description: options?.description,
155
+ visibility: options?.visibility ?? brighthub_lib_1.ConnectionVisibility.Private,
156
+ memberCount: 0,
157
+ followerCount: 0,
158
+ createdAt: now,
159
+ updatedAt: now,
160
+ };
161
+ await this.listsCollection.create(record);
162
+ return this.recordToList(record);
163
+ }
164
+ async updateList(listId, ownerId, updates) {
165
+ const list = await this.getOwnedList(listId, ownerId);
166
+ // Validate name if being updated
167
+ if (updates.name !== undefined && (!updates.name || !updates.name.trim())) {
168
+ throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.InvalidListName, 'List name cannot be empty');
169
+ }
170
+ const now = new Date().toISOString();
171
+ const updateFields = {
172
+ updatedAt: now,
173
+ };
174
+ if (updates.name !== undefined) {
175
+ updateFields.name = updates.name.trim();
176
+ }
177
+ if (updates.description !== undefined) {
178
+ updateFields.description = updates.description;
179
+ }
180
+ if (updates.visibility !== undefined) {
181
+ updateFields.visibility = updates.visibility;
182
+ }
183
+ await this.listsCollection
184
+ .updateOne({ _id: listId }, updateFields)
185
+ .exec();
186
+ // When visibility changes to Private, remove all external followers
187
+ if (updates.visibility === brighthub_lib_1.ConnectionVisibility.Private &&
188
+ list.visibility !== brighthub_lib_1.ConnectionVisibility.Private) {
189
+ await this.removeNonOwnerFollowersOnPrivate(listId, ownerId);
190
+ }
191
+ // Return updated list
192
+ return this.recordToList({
193
+ ...list,
194
+ ...updateFields,
195
+ });
196
+ }
197
+ async deleteList(listId, ownerId) {
198
+ await this.getOwnedList(listId, ownerId);
199
+ // Cascade: remove all memberships for this list
200
+ const members = await this.listMembersCollection
201
+ .find({ listId })
202
+ .exec();
203
+ for (const member of members) {
204
+ await this.listMembersCollection
205
+ .deleteOne({ _id: member._id })
206
+ .exec();
207
+ }
208
+ // Cascade: remove all followers for this list
209
+ const followers = await this.listFollowersCollection
210
+ .find({ listId })
211
+ .exec();
212
+ for (const follower of followers) {
213
+ await this.listFollowersCollection
214
+ .deleteOne({ _id: follower._id })
215
+ .exec();
216
+ }
217
+ // Delete the list itself
218
+ await this.listsCollection
219
+ .deleteOne({ _id: listId })
220
+ .exec();
221
+ }
222
+ // ═══════════════════════════════════════════════════════
223
+ // List Membership Operations
224
+ // ═══════════════════════════════════════════════════════
225
+ async addMembersToList(listId, ownerId, userIds) {
226
+ const list = await this.getOwnedList(listId, ownerId);
227
+ if (userIds.length === 0) {
228
+ return;
229
+ }
230
+ // Check member limit
231
+ const newTotal = list.memberCount + userIds.length;
232
+ if (newTotal > brighthub_lib_1.MAX_MEMBERS_PER_LIST) {
233
+ throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.ListMemberLimitExceeded, `Cannot exceed ${brighthub_lib_1.MAX_MEMBERS_PER_LIST} members per list`);
234
+ }
235
+ const now = new Date().toISOString();
236
+ let addedCount = 0;
237
+ for (const userId of userIds) {
238
+ // Check if already a member (skip duplicates)
239
+ const existing = await this.listMembersCollection
240
+ .findOne({
241
+ listId,
242
+ userId,
243
+ })
244
+ .exec();
245
+ if (!existing) {
246
+ await this.listMembersCollection.create({
247
+ _id: (0, crypto_1.randomUUID)(),
248
+ listId,
249
+ userId,
250
+ addedAt: now,
251
+ });
252
+ addedCount++;
253
+ }
254
+ }
255
+ // Update member count
256
+ if (addedCount > 0) {
257
+ await this.listsCollection
258
+ .updateOne({ _id: listId }, {
259
+ memberCount: list.memberCount + addedCount,
260
+ updatedAt: now,
261
+ })
262
+ .exec();
263
+ }
264
+ }
265
+ async removeMembersFromList(listId, ownerId, userIds) {
266
+ const list = await this.getOwnedList(listId, ownerId);
267
+ if (userIds.length === 0) {
268
+ return;
269
+ }
270
+ let removedCount = 0;
271
+ for (const userId of userIds) {
272
+ const result = await this.listMembersCollection
273
+ .deleteOne({
274
+ listId,
275
+ userId,
276
+ })
277
+ .exec();
278
+ if (result.deletedCount > 0) {
279
+ removedCount++;
280
+ }
281
+ }
282
+ // Update member count
283
+ if (removedCount > 0) {
284
+ const newCount = Math.max(0, list.memberCount - removedCount);
285
+ await this.listsCollection
286
+ .updateOne({ _id: listId }, {
287
+ memberCount: newCount,
288
+ updatedAt: new Date().toISOString(),
289
+ })
290
+ .exec();
291
+ }
292
+ }
293
+ async getListMembers(listId, options) {
294
+ // Verify list exists
295
+ const list = await this.listsCollection
296
+ .findOne({ _id: listId })
297
+ .exec();
298
+ if (!list) {
299
+ throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.ListNotFound, `List with ID '${listId}' not found`);
300
+ }
301
+ const limit = this.getLimit(options);
302
+ // Fetch one extra to determine hasMore
303
+ const queryLimit = limit + 1;
304
+ let query = this.listMembersCollection.find({
305
+ listId,
306
+ });
307
+ if (query.sort) {
308
+ query = query.sort({ addedAt: -1 });
309
+ }
310
+ // Apply cursor-based pagination (cursor is the addedAt timestamp)
311
+ const allMembers = await query.exec();
312
+ let filteredMembers = allMembers;
313
+ if (options?.cursor) {
314
+ filteredMembers = allMembers.filter((m) => m.addedAt < options.cursor);
315
+ }
316
+ const paginatedMembers = filteredMembers.slice(0, queryLimit);
317
+ const hasMore = paginatedMembers.length > limit;
318
+ const resultMembers = hasMore
319
+ ? paginatedMembers.slice(0, limit)
320
+ : paginatedMembers;
321
+ // Look up user profiles for each member
322
+ const profiles = [];
323
+ for (const member of resultMembers) {
324
+ const userRecord = await this.userProfilesCollection
325
+ .findOne({ _id: member.userId })
326
+ .exec();
327
+ if (userRecord) {
328
+ profiles.push(this.recordToProfile(userRecord));
329
+ }
330
+ }
331
+ const lastMember = resultMembers[resultMembers.length - 1];
332
+ return {
333
+ items: profiles,
334
+ cursor: hasMore && lastMember ? lastMember.addedAt : undefined,
335
+ hasMore,
336
+ };
337
+ }
338
+ // ═══════════════════════════════════════════════════════
339
+ // List Query Operations
340
+ // ═══════════════════════════════════════════════════════
341
+ async getUserLists(ownerId, options) {
342
+ const limit = this.getLimit(options);
343
+ const queryLimit = limit + 1;
344
+ let query = this.listsCollection.find({
345
+ ownerId,
346
+ });
347
+ if (query.sort) {
348
+ query = query.sort({ createdAt: -1 });
349
+ }
350
+ const allLists = await query.exec();
351
+ let filteredLists = allLists;
352
+ if (options?.cursor) {
353
+ filteredLists = allLists.filter((l) => l.createdAt < options.cursor);
354
+ }
355
+ const paginatedLists = filteredLists.slice(0, queryLimit);
356
+ const hasMore = paginatedLists.length > limit;
357
+ const resultLists = hasMore
358
+ ? paginatedLists.slice(0, limit)
359
+ : paginatedLists;
360
+ const lastList = resultLists[resultLists.length - 1];
361
+ return {
362
+ items: resultLists.map((r) => this.recordToList(r)),
363
+ cursor: hasMore && lastList ? lastList.createdAt : undefined,
364
+ hasMore,
365
+ };
366
+ }
367
+ // ═══════════════════════════════════════════════════════
368
+ // Category Helper Methods
369
+ // ═══════════════════════════════════════════════════════
370
+ /**
371
+ * Validate and retrieve a category, ensuring the caller is the owner
372
+ */
373
+ async getOwnedCategory(categoryId, ownerId) {
374
+ const category = await this.categoriesCollection
375
+ .findOne({ _id: categoryId })
376
+ .exec();
377
+ if (!category) {
378
+ throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.CategoryNotFound, `Category with ID '${categoryId}' not found`);
379
+ }
380
+ if (category.ownerId !== ownerId) {
381
+ throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.CategoryNotAuthorized, 'Not authorized to modify this category');
382
+ }
383
+ return category;
384
+ }
385
+ // ═══════════════════════════════════════════════════════
386
+ // Category CRUD Operations (Requirements 20.1-20.8)
387
+ // ═══════════════════════════════════════════════════════
388
+ async createCategory(ownerId, name, options) {
389
+ // Validate name
390
+ if (!name || !name.trim()) {
391
+ throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.InvalidCategoryName, 'Category name cannot be empty');
392
+ }
393
+ // Check for duplicate name
394
+ const existingByName = await this.categoriesCollection
395
+ .find({ ownerId })
396
+ .exec();
397
+ const duplicateName = existingByName.find((c) => c.name.toLowerCase() === name.trim().toLowerCase());
398
+ if (duplicateName) {
399
+ throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.DuplicateCategoryName, `A category named '${name.trim()}' already exists`);
400
+ }
401
+ // Enforce 20 custom categories per user limit (default categories don't count)
402
+ const customCategories = existingByName.filter((c) => !c.isDefault);
403
+ if (customCategories.length >= brighthub_lib_1.MAX_CATEGORIES_PER_USER) {
404
+ throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.CategoryLimitExceeded, `Cannot create more than ${brighthub_lib_1.MAX_CATEGORIES_PER_USER} custom categories`);
405
+ }
406
+ const now = new Date().toISOString();
407
+ const record = {
408
+ _id: (0, crypto_1.randomUUID)(),
409
+ ownerId,
410
+ name: name.trim(),
411
+ color: options?.color,
412
+ icon: options?.icon,
413
+ isDefault: false,
414
+ createdAt: now,
415
+ };
416
+ await this.categoriesCollection.create(record);
417
+ return this.recordToCategory(record);
418
+ }
419
+ async assignCategory(connectionId, categoryId, ownerId) {
420
+ // Verify category exists and belongs to the owner
421
+ await this.getOwnedCategory(categoryId, ownerId);
422
+ // Check if already assigned
423
+ const existing = await this.categoryAssignmentsCollection
424
+ .findOne({
425
+ connectionId,
426
+ categoryId,
427
+ })
428
+ .exec();
429
+ if (existing) {
430
+ throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.CategoryAlreadyAssigned, 'Category is already assigned to this connection');
431
+ }
432
+ const now = new Date().toISOString();
433
+ await this.categoryAssignmentsCollection.create({
434
+ _id: (0, crypto_1.randomUUID)(),
435
+ ownerId,
436
+ connectionId,
437
+ categoryId,
438
+ assignedAt: now,
439
+ });
440
+ }
441
+ async unassignCategory(connectionId, categoryId, ownerId) {
442
+ // Verify category exists and belongs to the owner
443
+ await this.getOwnedCategory(categoryId, ownerId);
444
+ const result = await this.categoryAssignmentsCollection
445
+ .deleteOne({
446
+ connectionId,
447
+ categoryId,
448
+ })
449
+ .exec();
450
+ if (result.deletedCount === 0) {
451
+ throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.CategoryNotAssigned, 'Category is not assigned to this connection');
452
+ }
453
+ }
454
+ async bulkAssignCategory(connectionIds, categoryId, ownerId) {
455
+ if (connectionIds.length === 0) {
456
+ return;
457
+ }
458
+ // Verify category exists and belongs to the owner
459
+ await this.getOwnedCategory(categoryId, ownerId);
460
+ const now = new Date().toISOString();
461
+ for (const connectionId of connectionIds) {
462
+ // Skip if already assigned
463
+ const existing = await this.categoryAssignmentsCollection
464
+ .findOne({
465
+ connectionId,
466
+ categoryId,
467
+ })
468
+ .exec();
469
+ if (!existing) {
470
+ await this.categoryAssignmentsCollection.create({
471
+ _id: (0, crypto_1.randomUUID)(),
472
+ ownerId,
473
+ connectionId,
474
+ categoryId,
475
+ assignedAt: now,
476
+ });
477
+ }
478
+ }
479
+ }
480
+ async getConnectionsByCategory(ownerId, categoryId, options) {
481
+ // Verify category exists and belongs to the owner
482
+ await this.getOwnedCategory(categoryId, ownerId);
483
+ const limit = this.getLimit(options);
484
+ const queryLimit = limit + 1;
485
+ // Get all assignments for this category
486
+ let query = this.categoryAssignmentsCollection.find({
487
+ ownerId,
488
+ categoryId,
489
+ });
490
+ if (query.sort) {
491
+ query = query.sort({ assignedAt: -1 });
492
+ }
493
+ const allAssignments = await query.exec();
494
+ let filteredAssignments = allAssignments;
495
+ if (options?.cursor) {
496
+ filteredAssignments = allAssignments.filter((a) => a.assignedAt < options.cursor);
497
+ }
498
+ const paginatedAssignments = filteredAssignments.slice(0, queryLimit);
499
+ const hasMore = paginatedAssignments.length > limit;
500
+ const resultAssignments = hasMore
501
+ ? paginatedAssignments.slice(0, limit)
502
+ : paginatedAssignments;
503
+ // Look up user profiles for each connection
504
+ const profiles = [];
505
+ for (const assignment of resultAssignments) {
506
+ const userRecord = await this.userProfilesCollection
507
+ .findOne({
508
+ _id: assignment.connectionId,
509
+ })
510
+ .exec();
511
+ if (userRecord) {
512
+ profiles.push(this.recordToProfile(userRecord));
513
+ }
514
+ }
515
+ const lastAssignment = resultAssignments[resultAssignments.length - 1];
516
+ return {
517
+ items: profiles,
518
+ cursor: hasMore && lastAssignment ? lastAssignment.assignedAt : undefined,
519
+ hasMore,
520
+ };
521
+ }
522
+ async deleteCategory(categoryId, ownerId) {
523
+ const category = await this.getOwnedCategory(categoryId, ownerId);
524
+ // Prevent deleting default categories
525
+ if (category.isDefault) {
526
+ throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.CannotDeleteDefaultCategory, 'Cannot delete a default category');
527
+ }
528
+ // Remove all assignments for this category
529
+ const assignments = await this.categoryAssignmentsCollection
530
+ .find({ categoryId })
531
+ .exec();
532
+ for (const assignment of assignments) {
533
+ await this.categoryAssignmentsCollection
534
+ .deleteOne({
535
+ _id: assignment._id,
536
+ })
537
+ .exec();
538
+ }
539
+ // Delete the category itself
540
+ await this.categoriesCollection
541
+ .deleteOne({ _id: categoryId })
542
+ .exec();
543
+ }
544
+ async getDefaultCategories(ownerId) {
545
+ // Check if default categories already exist for this user
546
+ const existingCategories = await this.categoriesCollection
547
+ .find({ ownerId, isDefault: true })
548
+ .exec();
549
+ if (existingCategories.length > 0) {
550
+ return existingCategories.map((r) => this.recordToCategory(r));
551
+ }
552
+ // Create default categories
553
+ const now = new Date().toISOString();
554
+ const created = [];
555
+ for (const def of brighthub_lib_1.DEFAULT_CONNECTION_CATEGORIES) {
556
+ const record = {
557
+ _id: (0, crypto_1.randomUUID)(),
558
+ ownerId,
559
+ name: def.name,
560
+ color: def.color,
561
+ icon: def.icon,
562
+ isDefault: true,
563
+ createdAt: now,
564
+ };
565
+ await this.categoriesCollection.create(record);
566
+ created.push(this.recordToCategory(record));
567
+ }
568
+ return created;
569
+ }
570
+ // ═══════════════════════════════════════════════════════
571
+ // Connection Note Operations (Requirements 21.1-21.7)
572
+ // ═══════════════════════════════════════════════════════
573
+ /**
574
+ * Validate note content
575
+ * @throws ConnectionServiceError if note is empty or exceeds limit
576
+ */
577
+ validateNoteContent(note) {
578
+ if (!note || !note.trim()) {
579
+ throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.InvalidNoteContent, 'Note content cannot be empty');
580
+ }
581
+ if (note.length > brighthub_lib_1.MAX_NOTE_LENGTH) {
582
+ throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.NoteTooLong, `Note cannot exceed ${brighthub_lib_1.MAX_NOTE_LENGTH} characters`);
583
+ }
584
+ }
585
+ /**
586
+ * Convert a note record to the API response format
587
+ */
588
+ recordToNote(record) {
589
+ return {
590
+ _id: record._id,
591
+ userId: record.userId,
592
+ connectionId: record.connectionId,
593
+ note: record.note,
594
+ createdAt: record.createdAt,
595
+ updatedAt: record.updatedAt,
596
+ };
597
+ }
598
+ async addNote(userId, connectionId, note) {
599
+ this.validateNoteContent(note);
600
+ // Check if a note already exists for this connection (unique constraint)
601
+ const existing = await this.notesCollection
602
+ .findOne({
603
+ userId,
604
+ connectionId,
605
+ })
606
+ .exec();
607
+ if (existing) {
608
+ // Update existing note instead of creating duplicate
609
+ const now = new Date().toISOString();
610
+ await this.notesCollection
611
+ .updateOne({ _id: existing._id }, {
612
+ note: note.trim(),
613
+ updatedAt: now,
614
+ })
615
+ .exec();
616
+ return this.recordToNote({
617
+ ...existing,
618
+ note: note.trim(),
619
+ updatedAt: now,
620
+ });
621
+ }
622
+ const now = new Date().toISOString();
623
+ const record = {
624
+ _id: (0, crypto_1.randomUUID)(),
625
+ userId,
626
+ connectionId,
627
+ note: note.trim(),
628
+ createdAt: now,
629
+ updatedAt: now,
630
+ };
631
+ await this.notesCollection.create(record);
632
+ return this.recordToNote(record);
633
+ }
634
+ async updateNote(userId, connectionId, note) {
635
+ this.validateNoteContent(note);
636
+ const existing = await this.notesCollection
637
+ .findOne({
638
+ userId,
639
+ connectionId,
640
+ })
641
+ .exec();
642
+ if (!existing) {
643
+ throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.NoteNotFound, 'No note found for this connection');
644
+ }
645
+ const now = new Date().toISOString();
646
+ await this.notesCollection
647
+ .updateOne({ _id: existing._id }, {
648
+ note: note.trim(),
649
+ updatedAt: now,
650
+ })
651
+ .exec();
652
+ return this.recordToNote({
653
+ ...existing,
654
+ note: note.trim(),
655
+ updatedAt: now,
656
+ });
657
+ }
658
+ async deleteNote(userId, connectionId) {
659
+ const result = await this.notesCollection
660
+ .deleteOne({
661
+ userId,
662
+ connectionId,
663
+ })
664
+ .exec();
665
+ if (result.deletedCount === 0) {
666
+ throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.NoteNotFound, 'No note found for this connection');
667
+ }
668
+ }
669
+ async searchConnectionsByNote(userId, searchTerm, options) {
670
+ const limit = this.getLimit(options);
671
+ const skip = options?.cursor ? parseInt(options.cursor, 10) : 0;
672
+ // Find all notes for this user (we filter in-memory for text matching
673
+ // since the mock collection doesn't support regex/text search)
674
+ const allNotes = await this.notesCollection
675
+ .find({ userId })
676
+ .exec();
677
+ // Filter by search term (case-insensitive)
678
+ const searchLower = searchTerm.toLowerCase();
679
+ const matchingNotes = allNotes.filter((n) => n.note.toLowerCase().includes(searchLower));
680
+ // Apply pagination
681
+ const paginatedNotes = matchingNotes.slice(skip, skip + limit + 1);
682
+ const hasMore = paginatedNotes.length > limit;
683
+ const results = paginatedNotes.slice(0, limit);
684
+ return {
685
+ items: results.map((r) => this.recordToNote(r)),
686
+ cursor: hasMore ? String(skip + limit) : undefined,
687
+ hasMore,
688
+ };
689
+ }
690
+ // ═══════════════════════════════════════════════════════
691
+ // Import/Export Operations (Requirements 22.1-22.7)
692
+ // ═══════════════════════════════════════════════════════
693
+ /**
694
+ * Check and enforce rate limiting for import operations
695
+ * @throws ConnectionServiceError if rate limited
696
+ */
697
+ checkImportRateLimit(userId) {
698
+ const lastImport = this.lastImportTimestamps.get(userId);
699
+ if (lastImport !== undefined) {
700
+ const elapsed = Date.now() - lastImport;
701
+ if (elapsed < brighthub_lib_1.IMPORT_RATE_LIMIT_MS) {
702
+ throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.ImportRateLimited, `Import rate limited. Please wait ${Math.ceil((brighthub_lib_1.IMPORT_RATE_LIMIT_MS - elapsed) / 1000)} seconds before importing again.`);
703
+ }
704
+ }
705
+ }
706
+ /**
707
+ * Parse CSV data into an array of usernames (one per line)
708
+ */
709
+ parseCsvUsernames(data) {
710
+ return data
711
+ .split(/\r?\n/)
712
+ .map((line) => line.trim())
713
+ .filter((line) => line.length > 0 && !line.startsWith('#'));
714
+ }
715
+ /**
716
+ * Look up a user profile by username
717
+ */
718
+ async findUserByUsername(username) {
719
+ return this.userProfilesCollection
720
+ .findOne({ username })
721
+ .exec();
722
+ }
723
+ async exportConnections(userId) {
724
+ // Get all follows for this user
725
+ const follows = await this.followsCollection
726
+ .find({ followerId: userId })
727
+ .exec();
728
+ const followedIds = follows.map((f) => f.followedId);
729
+ // Get all category assignments for this user
730
+ const allAssignments = await this.categoryAssignmentsCollection
731
+ .find({ ownerId: userId })
732
+ .exec();
733
+ // Get all categories for this user
734
+ const allCategories = await this.categoriesCollection
735
+ .find({ ownerId: userId })
736
+ .exec();
737
+ const categoryMap = new Map(allCategories.map((c) => [c._id, c.name]));
738
+ // Get all notes for this user
739
+ const allNotes = await this.notesCollection
740
+ .find({ userId })
741
+ .exec();
742
+ const noteMap = new Map(allNotes.map((n) => [n.connectionId, n.note]));
743
+ // Get all list memberships for lists owned by this user
744
+ const userLists = await this.listsCollection
745
+ .find({ ownerId: userId })
746
+ .exec();
747
+ const listNameMap = new Map(userLists.map((l) => [l._id, l.name]));
748
+ // Build a map of connectionId -> list names
749
+ const connectionListNames = new Map();
750
+ for (const list of userLists) {
751
+ const members = await this.listMembersCollection
752
+ .find({ listId: list._id })
753
+ .exec();
754
+ for (const member of members) {
755
+ const existing = connectionListNames.get(member.userId) || [];
756
+ const listName = listNameMap.get(list._id);
757
+ if (listName) {
758
+ existing.push(listName);
759
+ }
760
+ connectionListNames.set(member.userId, existing);
761
+ }
762
+ }
763
+ // Build connection entries
764
+ const connections = [];
765
+ for (const followedId of followedIds) {
766
+ const profile = await this.userProfilesCollection
767
+ .findOne({ _id: followedId })
768
+ .exec();
769
+ if (!profile)
770
+ continue;
771
+ // Get categories for this connection
772
+ const assignments = allAssignments.filter((a) => a.connectionId === followedId);
773
+ const categories = assignments
774
+ .map((a) => categoryMap.get(a.categoryId))
775
+ .filter((name) => name !== undefined);
776
+ connections.push({
777
+ username: profile.username,
778
+ displayName: profile.displayName,
779
+ categories,
780
+ note: noteMap.get(followedId),
781
+ lists: connectionListNames.get(followedId) || [],
782
+ });
783
+ }
784
+ return {
785
+ version: '1.0',
786
+ exportedAt: new Date().toISOString(),
787
+ userId,
788
+ connectionCount: connections.length,
789
+ connections,
790
+ };
791
+ }
792
+ async exportList(listId, ownerId) {
793
+ const list = await this.getOwnedList(listId, ownerId);
794
+ // Get all members
795
+ const memberRecords = await this.listMembersCollection
796
+ .find({ listId })
797
+ .exec();
798
+ const members = [];
799
+ for (const memberRecord of memberRecords) {
800
+ const profile = await this.userProfilesCollection
801
+ .findOne({ _id: memberRecord.userId })
802
+ .exec();
803
+ if (profile) {
804
+ members.push({
805
+ username: profile.username,
806
+ displayName: profile.displayName,
807
+ });
808
+ }
809
+ }
810
+ return {
811
+ version: '1.0',
812
+ exportedAt: new Date().toISOString(),
813
+ listName: list.name,
814
+ description: list.description,
815
+ visibility: list.visibility,
816
+ memberCount: members.length,
817
+ members,
818
+ };
819
+ }
820
+ async importConnections(userId, data, format) {
821
+ this.checkImportRateLimit(userId);
822
+ let usernames;
823
+ if (format === 'csv') {
824
+ usernames = this.parseCsvUsernames(data);
825
+ }
826
+ else {
827
+ try {
828
+ const parsed = JSON.parse(data);
829
+ if (parsed.connections && Array.isArray(parsed.connections)) {
830
+ usernames = parsed.connections
831
+ .map((c) => c.username || '')
832
+ .filter((u) => u.length > 0);
833
+ }
834
+ else if (Array.isArray(parsed)) {
835
+ usernames = parsed
836
+ .map((entry) => typeof entry === 'string' ? entry : entry.username || '')
837
+ .filter((u) => u.length > 0);
838
+ }
839
+ else {
840
+ throw new Error('Unrecognized JSON structure');
841
+ }
842
+ }
843
+ catch {
844
+ throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.InvalidImportFormat, 'Invalid JSON format. Expected { connections: [{ username }] } or an array of usernames.');
845
+ }
846
+ }
847
+ // Deduplicate
848
+ const uniqueUsernames = [...new Set(usernames)];
849
+ const result = {
850
+ successCount: 0,
851
+ skippedCount: 0,
852
+ totalCount: uniqueUsernames.length,
853
+ skippedUsernames: [],
854
+ errors: [],
855
+ };
856
+ for (const username of uniqueUsernames) {
857
+ const profile = await this.findUserByUsername(username);
858
+ if (!profile) {
859
+ result.skippedCount++;
860
+ result.skippedUsernames.push(username);
861
+ continue;
862
+ }
863
+ // Don't follow yourself
864
+ if (profile._id === userId) {
865
+ result.skippedCount++;
866
+ result.errors.push(`Cannot follow yourself (${username})`);
867
+ continue;
868
+ }
869
+ // Skip blocked users (Requirement 32.5)
870
+ const isBlocked = await this.isBlockedFromList(profile._id, userId);
871
+ const isBlockedBy = await this.blocksCollection
872
+ .findOne({
873
+ blockerId: profile._id,
874
+ blockedId: userId,
875
+ })
876
+ .exec();
877
+ if (isBlocked || isBlockedBy) {
878
+ result.skippedCount++;
879
+ result.errors.push(`Skipped blocked user: ${username}`);
880
+ continue;
881
+ }
882
+ // Check if already following
883
+ const existingFollow = await this.followsCollection
884
+ .findOne({
885
+ followerId: userId,
886
+ followedId: profile._id,
887
+ })
888
+ .exec();
889
+ if (existingFollow) {
890
+ // Already following, count as success (idempotent)
891
+ result.successCount++;
892
+ continue;
893
+ }
894
+ // Create follow relationship
895
+ await this.followsCollection.create({
896
+ _id: (0, crypto_1.randomUUID)(),
897
+ followerId: userId,
898
+ followedId: profile._id,
899
+ createdAt: new Date().toISOString(),
900
+ });
901
+ result.successCount++;
902
+ }
903
+ // Record import timestamp for rate limiting
904
+ this.lastImportTimestamps.set(userId, Date.now());
905
+ return result;
906
+ }
907
+ async importList(userId, data, format) {
908
+ this.checkImportRateLimit(userId);
909
+ let listName;
910
+ let description;
911
+ let visibility = brighthub_lib_1.ConnectionVisibility.Private;
912
+ let usernames;
913
+ if (format === 'csv') {
914
+ // CSV: first line is list name, rest are usernames
915
+ const lines = data
916
+ .split(/\r?\n/)
917
+ .map((line) => line.trim())
918
+ .filter((line) => line.length > 0 && !line.startsWith('#'));
919
+ if (lines.length === 0) {
920
+ throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.InvalidImportFormat, 'CSV data is empty. Expected at least a list name on the first line.');
921
+ }
922
+ listName = lines[0];
923
+ usernames = lines.slice(1);
924
+ }
925
+ else {
926
+ try {
927
+ const parsed = JSON.parse(data);
928
+ listName = parsed.listName || parsed.name;
929
+ if (!listName || typeof listName !== 'string') {
930
+ throw new Error('Missing list name');
931
+ }
932
+ description = parsed.description;
933
+ if (parsed.visibility &&
934
+ Object.values(brighthub_lib_1.ConnectionVisibility).includes(parsed.visibility)) {
935
+ visibility = parsed.visibility;
936
+ }
937
+ if (parsed.members && Array.isArray(parsed.members)) {
938
+ usernames = parsed.members
939
+ .map((m) => typeof m === 'string' ? m : m.username || '')
940
+ .filter((u) => u.length > 0);
941
+ }
942
+ else {
943
+ usernames = [];
944
+ }
945
+ }
946
+ catch {
947
+ throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.InvalidImportFormat, 'Invalid JSON format. Expected { listName, members: [{ username }] }.');
948
+ }
949
+ }
950
+ // Create the list
951
+ const list = await this.createList(userId, listName, {
952
+ description,
953
+ visibility,
954
+ });
955
+ // Deduplicate
956
+ const uniqueUsernames = [...new Set(usernames)];
957
+ const result = {
958
+ successCount: 0,
959
+ skippedCount: 0,
960
+ totalCount: uniqueUsernames.length,
961
+ skippedUsernames: [],
962
+ errors: [],
963
+ };
964
+ const validUserIds = [];
965
+ for (const username of uniqueUsernames) {
966
+ const profile = await this.findUserByUsername(username);
967
+ if (!profile) {
968
+ result.skippedCount++;
969
+ result.skippedUsernames.push(username);
970
+ continue;
971
+ }
972
+ // Skip blocked users during list import (Requirement 32.5)
973
+ const isBlocked = await this.isBlockedFromList(profile._id, userId);
974
+ if (isBlocked) {
975
+ result.skippedCount++;
976
+ result.errors.push(`Skipped blocked user: ${username}`);
977
+ continue;
978
+ }
979
+ // Check if this user is a connection (followed by the importer)
980
+ const existingFollow = await this.followsCollection
981
+ .findOne({
982
+ followerId: userId,
983
+ followedId: profile._id,
984
+ })
985
+ .exec();
986
+ if (!existingFollow) {
987
+ result.skippedCount++;
988
+ result.errors.push(`${username} is not a connection; only existing connections can be added to lists`);
989
+ continue;
990
+ }
991
+ validUserIds.push(profile._id);
992
+ result.successCount++;
993
+ }
994
+ // Add valid users to the list in bulk
995
+ if (validUserIds.length > 0) {
996
+ await this.addMembersToList(list._id, userId, validUserIds);
997
+ }
998
+ // Record import timestamp for rate limiting
999
+ this.lastImportTimestamps.set(userId, Date.now());
1000
+ return result;
1001
+ }
1002
+ // ═══════════════════════════════════════════════════════
1003
+ // Priority Connection Operations (Requirements 23.1-23.5)
1004
+ // ═══════════════════════════════════════════════════════
1005
+ async setPriority(userId, connectionId, isPriority) {
1006
+ // If setting priority=true, enforce the 50 limit
1007
+ if (isPriority) {
1008
+ const allMetadata = await this.metadataCollection
1009
+ .find({ userId, isPriority: true })
1010
+ .exec();
1011
+ // Check if this connection is already priority (don't count it against the limit)
1012
+ const existingForConnection = allMetadata.find((m) => m.connectionId === connectionId);
1013
+ const currentPriorityCount = allMetadata.length;
1014
+ if (!existingForConnection?.isPriority &&
1015
+ currentPriorityCount >= brighthub_lib_1.MAX_PRIORITY_CONNECTIONS) {
1016
+ throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.PriorityLimitExceeded, `Cannot have more than ${brighthub_lib_1.MAX_PRIORITY_CONNECTIONS} priority connections`);
1017
+ }
1018
+ }
1019
+ // Check if metadata record already exists for this user+connection
1020
+ const existing = await this.metadataCollection
1021
+ .findOne({
1022
+ userId,
1023
+ connectionId,
1024
+ })
1025
+ .exec();
1026
+ const now = new Date().toISOString();
1027
+ if (existing) {
1028
+ // Update existing record
1029
+ await this.metadataCollection
1030
+ .updateOne({ _id: existing._id }, {
1031
+ isPriority,
1032
+ updatedAt: now,
1033
+ })
1034
+ .exec();
1035
+ }
1036
+ else {
1037
+ // Create new metadata record (upsert)
1038
+ await this.metadataCollection.create({
1039
+ _id: (0, crypto_1.randomUUID)(),
1040
+ userId,
1041
+ connectionId,
1042
+ isPriority,
1043
+ isQuiet: false,
1044
+ createdAt: now,
1045
+ updatedAt: now,
1046
+ });
1047
+ }
1048
+ }
1049
+ async getPriorityConnections(userId, options) {
1050
+ const limit = this.getLimit(options);
1051
+ const queryLimit = limit + 1;
1052
+ // Get all priority metadata records for this user
1053
+ let query = this.metadataCollection.find({
1054
+ userId,
1055
+ isPriority: true,
1056
+ });
1057
+ if (query.sort) {
1058
+ query = query.sort({ updatedAt: -1 });
1059
+ }
1060
+ const allRecords = await query.exec();
1061
+ let filteredRecords = allRecords;
1062
+ if (options?.cursor) {
1063
+ filteredRecords = allRecords.filter((r) => r.updatedAt < options.cursor);
1064
+ }
1065
+ const paginatedRecords = filteredRecords.slice(0, queryLimit);
1066
+ const hasMore = paginatedRecords.length > limit;
1067
+ const resultRecords = hasMore
1068
+ ? paginatedRecords.slice(0, limit)
1069
+ : paginatedRecords;
1070
+ // Look up user profiles for each priority connection
1071
+ const profiles = [];
1072
+ for (const record of resultRecords) {
1073
+ const userRecord = await this.userProfilesCollection
1074
+ .findOne({
1075
+ _id: record.connectionId,
1076
+ })
1077
+ .exec();
1078
+ if (userRecord) {
1079
+ profiles.push(this.recordToProfile(userRecord));
1080
+ }
1081
+ }
1082
+ const lastRecord = resultRecords[resultRecords.length - 1];
1083
+ return {
1084
+ items: profiles,
1085
+ cursor: hasMore && lastRecord ? lastRecord.updatedAt : undefined,
1086
+ hasMore,
1087
+ };
1088
+ }
1089
+ async setQuietMode(userId, connectionId, isQuiet) {
1090
+ // Check if metadata record already exists for this user+connection
1091
+ const existing = await this.metadataCollection
1092
+ .findOne({
1093
+ userId,
1094
+ connectionId,
1095
+ })
1096
+ .exec();
1097
+ const now = new Date().toISOString();
1098
+ if (existing) {
1099
+ // Update existing record
1100
+ await this.metadataCollection
1101
+ .updateOne({ _id: existing._id }, {
1102
+ isQuiet,
1103
+ updatedAt: now,
1104
+ })
1105
+ .exec();
1106
+ }
1107
+ else {
1108
+ // Create new metadata record (upsert)
1109
+ await this.metadataCollection.create({
1110
+ _id: (0, crypto_1.randomUUID)(),
1111
+ userId,
1112
+ connectionId,
1113
+ isPriority: false,
1114
+ isQuiet,
1115
+ createdAt: now,
1116
+ updatedAt: now,
1117
+ });
1118
+ }
1119
+ }
1120
+ async getQuietConnections(userId, options) {
1121
+ const limit = this.getLimit(options);
1122
+ const queryLimit = limit + 1;
1123
+ // Get all quiet metadata records for this user
1124
+ let query = this.metadataCollection.find({
1125
+ userId,
1126
+ isQuiet: true,
1127
+ });
1128
+ if (query.sort) {
1129
+ query = query.sort({ updatedAt: -1 });
1130
+ }
1131
+ const allRecords = await query.exec();
1132
+ let filteredRecords = allRecords;
1133
+ if (options?.cursor) {
1134
+ filteredRecords = allRecords.filter((r) => r.updatedAt < options.cursor);
1135
+ }
1136
+ const paginatedRecords = filteredRecords.slice(0, queryLimit);
1137
+ const hasMore = paginatedRecords.length > limit;
1138
+ const resultRecords = hasMore
1139
+ ? paginatedRecords.slice(0, limit)
1140
+ : paginatedRecords;
1141
+ // Look up user profiles for each quiet connection
1142
+ const profiles = [];
1143
+ for (const record of resultRecords) {
1144
+ const userRecord = await this.userProfilesCollection
1145
+ .findOne({
1146
+ _id: record.connectionId,
1147
+ })
1148
+ .exec();
1149
+ if (userRecord) {
1150
+ profiles.push(this.recordToProfile(userRecord));
1151
+ }
1152
+ }
1153
+ const lastRecord = resultRecords[resultRecords.length - 1];
1154
+ return {
1155
+ items: profiles,
1156
+ cursor: hasMore && lastRecord ? lastRecord.updatedAt : undefined,
1157
+ hasMore,
1158
+ };
1159
+ }
1160
+ /**
1161
+ * Calculate the expiration date for a mute duration.
1162
+ * @param duration The mute duration
1163
+ * @returns ISO string of the expiration date
1164
+ */
1165
+ calculateExpiresAt(duration) {
1166
+ const durationToMs = {
1167
+ '1h': 3600000,
1168
+ '8h': 28800000,
1169
+ '24h': 86400000,
1170
+ '7d': 604800000,
1171
+ '30d': 2592000000,
1172
+ // ~100 years for permanent mutes
1173
+ permanent: 100 * 365.25 * 24 * 60 * 60 * 1000,
1174
+ };
1175
+ const ms = durationToMs[duration];
1176
+ return new Date(Date.now() + ms).toISOString();
1177
+ }
1178
+ /**
1179
+ * Set a temporary mute on a connection with a specified duration.
1180
+ * Creates or updates a mute record with a calculated expiration timestamp.
1181
+ * @see Requirements: 25.1, 25.2
1182
+ */
1183
+ async setTemporaryMute(userId, connectionId, duration) {
1184
+ const now = new Date().toISOString();
1185
+ const expiresAt = this.calculateExpiresAt(duration);
1186
+ // Check if a mute already exists for this user+connection
1187
+ const existing = await this.temporaryMutesCollection
1188
+ .findOne({
1189
+ userId,
1190
+ connectionId,
1191
+ })
1192
+ .exec();
1193
+ if (existing) {
1194
+ // Update existing mute with new duration and expiration
1195
+ await this.temporaryMutesCollection
1196
+ .updateOne({ _id: existing._id }, {
1197
+ duration,
1198
+ expiresAt,
1199
+ createdAt: now,
1200
+ })
1201
+ .exec();
1202
+ }
1203
+ else {
1204
+ // Create new mute record
1205
+ await this.temporaryMutesCollection.create({
1206
+ _id: (0, crypto_1.randomUUID)(),
1207
+ userId,
1208
+ connectionId,
1209
+ duration,
1210
+ expiresAt,
1211
+ createdAt: now,
1212
+ });
1213
+ }
1214
+ }
1215
+ /**
1216
+ * Remove a temporary mute before it expires (early unmute).
1217
+ * @see Requirements: 25.7
1218
+ */
1219
+ async removeTemporaryMute(userId, connectionId) {
1220
+ await this.temporaryMutesCollection
1221
+ .deleteOne({
1222
+ userId,
1223
+ connectionId,
1224
+ })
1225
+ .exec();
1226
+ }
1227
+ /**
1228
+ * Convert a temporary mute to a permanent mute.
1229
+ * @see Requirements: 25.6
1230
+ */
1231
+ async convertToPermanentMute(userId, connectionId) {
1232
+ const existing = await this.temporaryMutesCollection
1233
+ .findOne({
1234
+ userId,
1235
+ connectionId,
1236
+ })
1237
+ .exec();
1238
+ if (!existing) {
1239
+ // No existing mute — create a permanent one
1240
+ await this.setTemporaryMute(userId, connectionId, brighthub_lib_1.MuteDuration.Permanent);
1241
+ return;
1242
+ }
1243
+ const permanentExpiresAt = this.calculateExpiresAt(brighthub_lib_1.MuteDuration.Permanent);
1244
+ await this.temporaryMutesCollection
1245
+ .updateOne({ _id: existing._id }, {
1246
+ duration: brighthub_lib_1.MuteDuration.Permanent,
1247
+ expiresAt: permanentExpiresAt,
1248
+ })
1249
+ .exec();
1250
+ }
1251
+ /**
1252
+ * Get all muted connections for a user with their expiration info.
1253
+ * Only returns currently active (non-expired) mutes.
1254
+ * @see Requirements: 25.5
1255
+ */
1256
+ async getMutedConnections(userId, options) {
1257
+ const limit = this.getLimit(options);
1258
+ const queryLimit = limit + 1;
1259
+ const now = new Date().toISOString();
1260
+ // Get all mute records for this user
1261
+ let query = this.temporaryMutesCollection.find({
1262
+ userId,
1263
+ });
1264
+ if (query.sort) {
1265
+ query = query.sort({ expiresAt: 1 });
1266
+ }
1267
+ const allRecords = await query.exec();
1268
+ // Filter to only active (non-expired) mutes
1269
+ let filteredRecords = allRecords.filter((r) => r.expiresAt > now);
1270
+ // Apply cursor-based pagination
1271
+ if (options?.cursor) {
1272
+ filteredRecords = filteredRecords.filter((r) => r.expiresAt > options.cursor);
1273
+ }
1274
+ const paginatedRecords = filteredRecords.slice(0, queryLimit);
1275
+ const hasMore = paginatedRecords.length > limit;
1276
+ const resultRecords = hasMore
1277
+ ? paginatedRecords.slice(0, limit)
1278
+ : paginatedRecords;
1279
+ // Look up user profiles and attach mute info
1280
+ const results = [];
1281
+ for (const record of resultRecords) {
1282
+ const userRecord = await this.userProfilesCollection
1283
+ .findOne({
1284
+ _id: record.connectionId,
1285
+ })
1286
+ .exec();
1287
+ if (userRecord) {
1288
+ results.push({
1289
+ ...this.recordToProfile(userRecord),
1290
+ expiresAt: record.expiresAt,
1291
+ duration: record.duration,
1292
+ });
1293
+ }
1294
+ }
1295
+ const lastRecord = resultRecords[resultRecords.length - 1];
1296
+ return {
1297
+ items: results,
1298
+ cursor: hasMore && lastRecord ? lastRecord.expiresAt : undefined,
1299
+ hasMore,
1300
+ };
1301
+ }
1302
+ // ═══════════════════════════════════════════════════════
1303
+ // Hub Operations (Requirements 30.1-30.10)
1304
+ // ═══════════════════════════════════════════════════════
1305
+ /**
1306
+ * Convert a hub database record to the API response format
1307
+ */
1308
+ recordToHub(record) {
1309
+ return {
1310
+ _id: record._id,
1311
+ ownerId: record.ownerId,
1312
+ name: record.name,
1313
+ memberCount: record.memberCount,
1314
+ isDefault: record.isDefault,
1315
+ createdAt: record.createdAt,
1316
+ };
1317
+ }
1318
+ /**
1319
+ * Validate hub name
1320
+ */
1321
+ validateHubName(name) {
1322
+ if (!name || name.trim().length === 0) {
1323
+ throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.InvalidHubName, 'Hub name cannot be empty');
1324
+ }
1325
+ if (name.trim().length > 50) {
1326
+ throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.InvalidHubName, 'Hub name cannot exceed 50 characters');
1327
+ }
1328
+ }
1329
+ /**
1330
+ * Get an owned hub or throw
1331
+ */
1332
+ async getOwnedHub(hubId, ownerId) {
1333
+ const hub = await this.hubsCollection
1334
+ .findOne({ _id: hubId })
1335
+ .exec();
1336
+ if (!hub) {
1337
+ throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.HubNotFound, `Hub ${hubId} not found`);
1338
+ }
1339
+ if (hub.ownerId !== ownerId) {
1340
+ throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.HubNotAuthorized, 'Not authorized to modify this hub');
1341
+ }
1342
+ return hub;
1343
+ }
1344
+ /**
1345
+ * Create a new hub
1346
+ * @see Requirements: 30.1, 30.9
1347
+ */
1348
+ async createHub(ownerId, name) {
1349
+ this.validateHubName(name);
1350
+ // Check hub limit
1351
+ const existingHubs = await this.hubsCollection
1352
+ .find({ ownerId })
1353
+ .exec();
1354
+ if (existingHubs.length >= brighthub_lib_1.MAX_HUBS_PER_USER) {
1355
+ throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.HubLimitExceeded, `Cannot create more than ${brighthub_lib_1.MAX_HUBS_PER_USER} hubs`);
1356
+ }
1357
+ const now = new Date().toISOString();
1358
+ const record = {
1359
+ _id: (0, crypto_1.randomUUID)(),
1360
+ ownerId,
1361
+ name: name.trim(),
1362
+ memberCount: 0,
1363
+ isDefault: false,
1364
+ createdAt: now,
1365
+ };
1366
+ const created = await this.hubsCollection.create(record);
1367
+ return this.recordToHub(created);
1368
+ }
1369
+ /**
1370
+ * Delete a hub and all its memberships.
1371
+ * Cannot delete the default "Close Friends" hub.
1372
+ * @see Requirements: 30.1
1373
+ */
1374
+ async deleteHub(hubId, ownerId) {
1375
+ const hub = await this.getOwnedHub(hubId, ownerId);
1376
+ if (hub.isDefault) {
1377
+ throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.CannotDeleteDefaultHub, 'Cannot delete the default hub');
1378
+ }
1379
+ // Delete all memberships
1380
+ const members = await this.hubMembersCollection
1381
+ .find({ hubId })
1382
+ .exec();
1383
+ for (const member of members) {
1384
+ await this.hubMembersCollection
1385
+ .deleteOne({ _id: member._id })
1386
+ .exec();
1387
+ }
1388
+ // Delete the hub
1389
+ await this.hubsCollection
1390
+ .deleteOne({ _id: hubId })
1391
+ .exec();
1392
+ }
1393
+ /**
1394
+ * Add members to a hub (bulk support)
1395
+ * @see Requirements: 30.2, 30.10
1396
+ */
1397
+ async addToHub(hubId, ownerId, userIds) {
1398
+ const hub = await this.getOwnedHub(hubId, ownerId);
1399
+ if (hub.memberCount + userIds.length > brighthub_lib_1.MAX_MEMBERS_PER_HUB) {
1400
+ throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.HubMemberLimitExceeded, `Cannot exceed ${brighthub_lib_1.MAX_MEMBERS_PER_HUB} members per hub`);
1401
+ }
1402
+ const now = new Date().toISOString();
1403
+ let addedCount = 0;
1404
+ for (const userId of userIds) {
1405
+ // Check if already a member
1406
+ const existing = await this.hubMembersCollection
1407
+ .findOne({
1408
+ hubId,
1409
+ userId,
1410
+ })
1411
+ .exec();
1412
+ if (existing) {
1413
+ // Skip duplicates silently for bulk operations
1414
+ continue;
1415
+ }
1416
+ await this.hubMembersCollection.create({
1417
+ _id: (0, crypto_1.randomUUID)(),
1418
+ hubId,
1419
+ userId,
1420
+ addedAt: now,
1421
+ });
1422
+ addedCount++;
1423
+ }
1424
+ // Update member count
1425
+ if (addedCount > 0) {
1426
+ await this.hubsCollection
1427
+ .updateOne({ _id: hubId }, {
1428
+ memberCount: hub.memberCount + addedCount,
1429
+ })
1430
+ .exec();
1431
+ }
1432
+ }
1433
+ /**
1434
+ * Remove members from a hub (bulk support)
1435
+ * @see Requirements: 30.2
1436
+ */
1437
+ async removeFromHub(hubId, ownerId, userIds) {
1438
+ const hub = await this.getOwnedHub(hubId, ownerId);
1439
+ let removedCount = 0;
1440
+ for (const userId of userIds) {
1441
+ const result = await this.hubMembersCollection
1442
+ .deleteOne({
1443
+ hubId,
1444
+ userId,
1445
+ })
1446
+ .exec();
1447
+ if (result.deletedCount > 0) {
1448
+ removedCount++;
1449
+ }
1450
+ }
1451
+ // Update member count
1452
+ if (removedCount > 0) {
1453
+ const newCount = Math.max(0, hub.memberCount - removedCount);
1454
+ await this.hubsCollection
1455
+ .updateOne({ _id: hubId }, { memberCount: newCount })
1456
+ .exec();
1457
+ }
1458
+ }
1459
+ /**
1460
+ * Get all hubs owned by a user
1461
+ * @see Requirements: 30.1
1462
+ */
1463
+ async getHubs(ownerId) {
1464
+ const records = await this.hubsCollection
1465
+ .find({ ownerId })
1466
+ .exec();
1467
+ return records.map((r) => this.recordToHub(r));
1468
+ }
1469
+ /**
1470
+ * Get members of a hub with pagination
1471
+ * @see Requirements: 30.2
1472
+ */
1473
+ async getHubMembers(hubId, options) {
1474
+ const limit = this.getLimit(options);
1475
+ let query = this.hubMembersCollection.find({
1476
+ hubId,
1477
+ });
1478
+ if (options?.cursor && query.sort) {
1479
+ // Use cursor-based pagination by filtering addedAt
1480
+ query = this.hubMembersCollection.find({
1481
+ hubId,
1482
+ });
1483
+ }
1484
+ if (query.sort) {
1485
+ query = query.sort({ addedAt: -1 });
1486
+ }
1487
+ if (query.skip && options?.cursor) {
1488
+ // Simple offset approach: count records before cursor
1489
+ }
1490
+ if (query.limit) {
1491
+ query = query.limit(limit + 1);
1492
+ }
1493
+ const memberRecords = await query.exec();
1494
+ // Filter by cursor if provided
1495
+ const filteredRecords = options?.cursor
1496
+ ? memberRecords.filter((r) => r.addedAt < options.cursor)
1497
+ : memberRecords;
1498
+ const hasMore = filteredRecords.length > limit;
1499
+ const resultRecords = hasMore
1500
+ ? filteredRecords.slice(0, limit)
1501
+ : filteredRecords;
1502
+ // Look up user profiles
1503
+ const results = [];
1504
+ for (const record of resultRecords) {
1505
+ const userRecord = await this.userProfilesCollection
1506
+ .findOne({ _id: record.userId })
1507
+ .exec();
1508
+ if (userRecord) {
1509
+ results.push(this.recordToProfile(userRecord));
1510
+ }
1511
+ }
1512
+ const lastRecord = resultRecords[resultRecords.length - 1];
1513
+ return {
1514
+ items: results,
1515
+ cursor: hasMore && lastRecord ? lastRecord.addedAt : undefined,
1516
+ hasMore,
1517
+ };
1518
+ }
1519
+ /**
1520
+ * Get or create the default "Close Friends" hub for a user
1521
+ * @see Requirements: 30.6
1522
+ */
1523
+ async getOrCreateDefaultHub(ownerId) {
1524
+ // Try to find existing default hub
1525
+ const existing = await this.hubsCollection
1526
+ .findOne({
1527
+ ownerId,
1528
+ isDefault: true,
1529
+ })
1530
+ .exec();
1531
+ if (existing) {
1532
+ return this.recordToHub(existing);
1533
+ }
1534
+ // Create the default hub
1535
+ const now = new Date().toISOString();
1536
+ const record = {
1537
+ _id: (0, crypto_1.randomUUID)(),
1538
+ ownerId,
1539
+ name: brighthub_lib_1.DEFAULT_HUB_NAME,
1540
+ memberCount: 0,
1541
+ isDefault: true,
1542
+ createdAt: now,
1543
+ };
1544
+ const created = await this.hubsCollection.create(record);
1545
+ return this.recordToHub(created);
1546
+ }
1547
+ // ── Mutual Connections ──────────────────────────────────────────────
1548
+ /**
1549
+ * Build a sorted cache key so that (A,B) and (B,A) share the same entry.
1550
+ */
1551
+ mutualCacheKey(userId, otherUserId) {
1552
+ return [userId, otherUserId].sort().join(':');
1553
+ }
1554
+ /**
1555
+ * Get the set of user IDs that a given user follows.
1556
+ */
1557
+ async getFollowedIds(userId) {
1558
+ const follows = await this.followsCollection
1559
+ .find({ followerId: userId })
1560
+ .exec();
1561
+ return new Set(follows.map((f) => f.followedId));
1562
+ }
1563
+ /**
1564
+ * Get mutual connections between two users with pagination.
1565
+ * Mutual connections are users that both userId AND otherUserId follow.
1566
+ * @see Requirements: 28.1, 28.2
1567
+ */
1568
+ async getMutualConnections(userId, otherUserId, options) {
1569
+ const limit = this.getLimit(options);
1570
+ // Get follows for both users
1571
+ const [userFollows, otherFollows] = await Promise.all([
1572
+ this.getFollowedIds(userId),
1573
+ this.getFollowedIds(otherUserId),
1574
+ ]);
1575
+ // Compute intersection
1576
+ const mutualIds = [];
1577
+ for (const id of userFollows) {
1578
+ if (otherFollows.has(id)) {
1579
+ mutualIds.push(id);
1580
+ }
1581
+ }
1582
+ // Sort for deterministic pagination
1583
+ mutualIds.sort();
1584
+ // Apply cursor-based pagination
1585
+ let startIndex = 0;
1586
+ if (options?.cursor) {
1587
+ const cursorIdx = mutualIds.indexOf(options.cursor);
1588
+ if (cursorIdx !== -1) {
1589
+ startIndex = cursorIdx + 1;
1590
+ }
1591
+ }
1592
+ const page = mutualIds.slice(startIndex, startIndex + limit);
1593
+ const hasMore = startIndex + limit < mutualIds.length;
1594
+ // Look up profiles for the page
1595
+ const profiles = [];
1596
+ for (const id of page) {
1597
+ const record = await this.userProfilesCollection
1598
+ .findOne({ _id: id })
1599
+ .exec();
1600
+ if (record) {
1601
+ profiles.push(this.recordToProfile(record));
1602
+ }
1603
+ }
1604
+ return {
1605
+ items: profiles,
1606
+ cursor: hasMore && page.length > 0 ? page[page.length - 1] : undefined,
1607
+ hasMore,
1608
+ };
1609
+ }
1610
+ /**
1611
+ * Get the count of mutual connections between two users.
1612
+ * Uses an in-memory cache with a 5-minute TTL for performance.
1613
+ * @see Requirements: 28.1, 28.5
1614
+ */
1615
+ async getMutualConnectionCount(userId, otherUserId) {
1616
+ const key = this.mutualCacheKey(userId, otherUserId);
1617
+ const cached = this.mutualConnectionCache.get(key);
1618
+ const now = Date.now();
1619
+ if (cached &&
1620
+ now - cached.timestamp < ConnectionService.MUTUAL_CACHE_TTL_MS) {
1621
+ return cached.count;
1622
+ }
1623
+ // Cache miss – compute intersection count
1624
+ const [userFollows, otherFollows] = await Promise.all([
1625
+ this.getFollowedIds(userId),
1626
+ this.getFollowedIds(otherUserId),
1627
+ ]);
1628
+ let count = 0;
1629
+ for (const id of userFollows) {
1630
+ if (otherFollows.has(id)) {
1631
+ count++;
1632
+ }
1633
+ }
1634
+ this.mutualConnectionCache.set(key, { count, timestamp: now });
1635
+ return count;
1636
+ }
1637
+ // ═══════════════════════════════════════════════════════
1638
+ // Block/Mute Inheritance for Lists (Requirements 32.1-32.5)
1639
+ // ═══════════════════════════════════════════════════════
1640
+ /**
1641
+ * Remove a blocked user from ALL lists owned by the blocker.
1642
+ * Finds every list owned by ownerId, removes blockedUserId from each,
1643
+ * and decrements member counts accordingly.
1644
+ * @see Requirements: 32.1
1645
+ */
1646
+ async removeBlockedUserFromLists(ownerId, blockedUserId) {
1647
+ // Get all lists owned by the blocker
1648
+ const lists = await this.listsCollection
1649
+ .find({ ownerId })
1650
+ .exec();
1651
+ for (const list of lists) {
1652
+ // Try to remove the blocked user from this list's members
1653
+ const result = await this.listMembersCollection
1654
+ .deleteOne({
1655
+ listId: list._id,
1656
+ userId: blockedUserId,
1657
+ })
1658
+ .exec();
1659
+ // Decrement member count if a record was actually removed
1660
+ if (result.deletedCount > 0) {
1661
+ const newCount = Math.max(0, list.memberCount - 1);
1662
+ await this.listsCollection
1663
+ .updateOne({ _id: list._id }, {
1664
+ memberCount: newCount,
1665
+ updatedAt: new Date().toISOString(),
1666
+ })
1667
+ .exec();
1668
+ }
1669
+ }
1670
+ }
1671
+ /**
1672
+ * Check if a user is blocked by the list owner.
1673
+ * Used to prevent blocked users from viewing or following lists.
1674
+ * @see Requirements: 32.2, 32.3
1675
+ */
1676
+ async isBlockedFromList(userId, listOwnerId) {
1677
+ const block = await this.blocksCollection
1678
+ .findOne({
1679
+ blockerId: listOwnerId,
1680
+ blockedId: userId,
1681
+ })
1682
+ .exec();
1683
+ return block !== null;
1684
+ }
1685
+ // ═══════════════════════════════════════════════════════
1686
+ // List Following Operations
1687
+ // ═══════════════════════════════════════════════════════
1688
+ /**
1689
+ * Follow a public list. Verifies the list exists, is public,
1690
+ * the user is not blocked, and is not already following.
1691
+ * Increments the list's followerCount.
1692
+ * @see Requirements: 33.1, 33.3
1693
+ */
1694
+ async followList(userId, listId) {
1695
+ // Verify list exists
1696
+ const list = await this.listsCollection
1697
+ .findOne({ _id: listId })
1698
+ .exec();
1699
+ if (!list) {
1700
+ throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.ListNotFound, 'List not found');
1701
+ }
1702
+ // Only public lists can be followed
1703
+ if (list.visibility !== brighthub_lib_1.ConnectionVisibility.Public) {
1704
+ throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.ListNotPublic, 'Only public lists can be followed');
1705
+ }
1706
+ // Check if user is blocked by the list owner
1707
+ const isBlocked = await this.isBlockedFromList(userId, list.ownerId);
1708
+ if (isBlocked) {
1709
+ throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.ListNotFound, 'List not found');
1710
+ }
1711
+ // Check if already following
1712
+ const existing = await this.listFollowersCollection
1713
+ .findOne({
1714
+ listId,
1715
+ userId,
1716
+ })
1717
+ .exec();
1718
+ if (existing) {
1719
+ throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.AlreadyFollowingList, 'Already following this list');
1720
+ }
1721
+ // Create follower record
1722
+ const now = new Date().toISOString();
1723
+ await this.listFollowersCollection.create({
1724
+ _id: (0, crypto_1.randomUUID)(),
1725
+ listId,
1726
+ userId,
1727
+ followedAt: now,
1728
+ });
1729
+ // Increment followerCount
1730
+ await this.listsCollection
1731
+ .updateOne({ _id: listId }, {
1732
+ followerCount: list.followerCount + 1,
1733
+ updatedAt: now,
1734
+ })
1735
+ .exec();
1736
+ }
1737
+ /**
1738
+ * Unfollow a list, removing the subscription and decrementing followerCount.
1739
+ * @see Requirements: 33.5
1740
+ */
1741
+ async unfollowList(userId, listId) {
1742
+ // Verify the follower record exists
1743
+ const existing = await this.listFollowersCollection
1744
+ .findOne({
1745
+ listId,
1746
+ userId,
1747
+ })
1748
+ .exec();
1749
+ if (!existing) {
1750
+ throw new brighthub_lib_1.ConnectionServiceError(brighthub_lib_1.ConnectionServiceErrorCode.NotFollowingList, 'Not following this list');
1751
+ }
1752
+ // Delete follower record
1753
+ await this.listFollowersCollection
1754
+ .deleteOne({ _id: existing._id })
1755
+ .exec();
1756
+ // Decrement followerCount
1757
+ const list = await this.listsCollection
1758
+ .findOne({ _id: listId })
1759
+ .exec();
1760
+ if (list) {
1761
+ const newCount = Math.max(0, list.followerCount - 1);
1762
+ await this.listsCollection
1763
+ .updateOne({ _id: listId }, {
1764
+ followerCount: newCount,
1765
+ updatedAt: new Date().toISOString(),
1766
+ })
1767
+ .exec();
1768
+ }
1769
+ }
1770
+ /**
1771
+ * Get all followers of a list with pagination.
1772
+ * @see Requirements: 33.6
1773
+ */
1774
+ async getListFollowers(listId, options) {
1775
+ const limit = this.getLimit(options);
1776
+ const queryLimit = limit + 1;
1777
+ let query = this.listFollowersCollection.find({
1778
+ listId,
1779
+ });
1780
+ if (query.sort) {
1781
+ query = query.sort({ followedAt: -1 });
1782
+ }
1783
+ const allFollowers = await query.exec();
1784
+ let filteredFollowers = allFollowers;
1785
+ if (options?.cursor) {
1786
+ filteredFollowers = allFollowers.filter((f) => f.followedAt < options.cursor);
1787
+ }
1788
+ const paginatedFollowers = filteredFollowers.slice(0, queryLimit);
1789
+ const hasMore = paginatedFollowers.length > limit;
1790
+ const resultFollowers = hasMore
1791
+ ? paginatedFollowers.slice(0, limit)
1792
+ : paginatedFollowers;
1793
+ // Resolve user profiles
1794
+ const items = [];
1795
+ for (const record of resultFollowers) {
1796
+ const profile = await this.userProfilesCollection
1797
+ .findOne({ _id: record.userId })
1798
+ .exec();
1799
+ if (profile) {
1800
+ items.push(this.recordToProfile(profile));
1801
+ }
1802
+ }
1803
+ const lastFollower = resultFollowers[resultFollowers.length - 1];
1804
+ return {
1805
+ items,
1806
+ cursor: hasMore && lastFollower ? lastFollower.followedAt : undefined,
1807
+ hasMore,
1808
+ };
1809
+ }
1810
+ /**
1811
+ * Get all lists a user follows with pagination.
1812
+ * @see Requirements: 33.6
1813
+ */
1814
+ async getFollowedLists(userId, options) {
1815
+ const limit = this.getLimit(options);
1816
+ const queryLimit = limit + 1;
1817
+ let query = this.listFollowersCollection.find({
1818
+ userId,
1819
+ });
1820
+ if (query.sort) {
1821
+ query = query.sort({ followedAt: -1 });
1822
+ }
1823
+ const allFollowed = await query.exec();
1824
+ let filteredFollowed = allFollowed;
1825
+ if (options?.cursor) {
1826
+ filteredFollowed = allFollowed.filter((f) => f.followedAt < options.cursor);
1827
+ }
1828
+ const paginatedFollowed = filteredFollowed.slice(0, queryLimit);
1829
+ const hasMore = paginatedFollowed.length > limit;
1830
+ const resultFollowed = hasMore
1831
+ ? paginatedFollowed.slice(0, limit)
1832
+ : paginatedFollowed;
1833
+ // Resolve list details
1834
+ const items = [];
1835
+ for (const record of resultFollowed) {
1836
+ const list = await this.listsCollection
1837
+ .findOne({ _id: record.listId })
1838
+ .exec();
1839
+ if (list) {
1840
+ items.push(this.recordToList(list));
1841
+ }
1842
+ }
1843
+ const lastFollowed = resultFollowed[resultFollowed.length - 1];
1844
+ return {
1845
+ items,
1846
+ cursor: hasMore && lastFollowed ? lastFollowed.followedAt : undefined,
1847
+ hasMore,
1848
+ };
1849
+ }
1850
+ /**
1851
+ * Remove all non-owner followers when a list becomes private.
1852
+ * Deletes all follower records and resets followerCount to 0.
1853
+ * @see Requirements: 33.7
1854
+ */
1855
+ async removeNonOwnerFollowersOnPrivate(listId, ownerId) {
1856
+ // Get all followers for this list
1857
+ const followers = await this.listFollowersCollection
1858
+ .find({ listId })
1859
+ .exec();
1860
+ // Delete all follower records (owner wouldn't normally follow their own list,
1861
+ // but skip them just in case)
1862
+ for (const follower of followers) {
1863
+ if (follower.userId !== ownerId) {
1864
+ await this.listFollowersCollection
1865
+ .deleteOne({ _id: follower._id })
1866
+ .exec();
1867
+ }
1868
+ }
1869
+ // Reset followerCount to 0
1870
+ await this.listsCollection
1871
+ .updateOne({ _id: listId }, {
1872
+ followerCount: 0,
1873
+ updatedAt: new Date().toISOString(),
1874
+ })
1875
+ .exec();
1876
+ }
1877
+ }
1878
+ exports.ConnectionService = ConnectionService;
1879
+ /**
1880
+ * Factory function to create a ConnectionService instance
1881
+ * @param application Application with database collection access
1882
+ * @returns A new ConnectionService instance
1883
+ */
1884
+ function createConnectionService(application) {
1885
+ return new ConnectionService(application);
1886
+ }
1887
+ //# sourceMappingURL=connectionService.js.map