@intlayer/backend 8.12.2 → 8.12.4-canary.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 (415) hide show
  1. package/dist/assets/utils/AI/askDocQuestion/embeddings/docs/en/bundle_optimization.json +9954 -6953
  2. package/dist/assets/utils/AI/askDocQuestion/embeddings/docs/en/configuration.json +1 -1
  3. package/dist/esm/controllers/ai.controller.mjs.map +1 -1
  4. package/dist/esm/controllers/audit.controller.mjs.map +1 -1
  5. package/dist/esm/controllers/bitbucket.controller.mjs.map +1 -1
  6. package/dist/esm/controllers/cliSessionToken.controller.mjs.map +1 -1
  7. package/dist/esm/controllers/demo.controller.mjs +32 -25
  8. package/dist/esm/controllers/demo.controller.mjs.map +1 -1
  9. package/dist/esm/controllers/dictionary.controller.mjs +1 -0
  10. package/dist/esm/controllers/dictionary.controller.mjs.map +1 -1
  11. package/dist/esm/controllers/environment.controller.mjs.map +1 -1
  12. package/dist/esm/controllers/eventListener.controller.mjs.map +1 -1
  13. package/dist/esm/controllers/github.controller.mjs.map +1 -1
  14. package/dist/esm/controllers/gitlab.controller.mjs.map +1 -1
  15. package/dist/esm/controllers/newsletter.controller.mjs.map +1 -1
  16. package/dist/esm/controllers/oAuth2.controller.mjs.map +1 -1
  17. package/dist/esm/controllers/organization.controller.mjs.map +1 -1
  18. package/dist/esm/controllers/project.controller.mjs.map +1 -1
  19. package/dist/esm/controllers/projectAccessKey.controller.mjs.map +1 -1
  20. package/dist/esm/controllers/projectMemberAccess.controller.mjs.map +1 -1
  21. package/dist/esm/controllers/recursiveAudit.controller.mjs.map +1 -1
  22. package/dist/esm/controllers/reviewer.controller.mjs.map +1 -1
  23. package/dist/esm/controllers/searchDoc.controller.mjs.map +1 -1
  24. package/dist/esm/controllers/showcaseProject.controller.mjs.map +1 -1
  25. package/dist/esm/controllers/stripe.controller.mjs.map +1 -1
  26. package/dist/esm/controllers/tag.controller.mjs.map +1 -1
  27. package/dist/esm/controllers/translation.controller.mjs.map +1 -1
  28. package/dist/esm/controllers/user.controller.mjs.map +1 -1
  29. package/dist/esm/emails/AffiliateActivatedEmail.mjs.map +1 -1
  30. package/dist/esm/emails/AffiliateConversionEmail.mjs.map +1 -1
  31. package/dist/esm/emails/AffiliateInvitationEmail.mjs.map +1 -1
  32. package/dist/esm/emails/AffiliateWelcomeEmail.mjs.map +1 -1
  33. package/dist/esm/emails/InviteUserEmail.mjs.map +1 -1
  34. package/dist/esm/emails/MagicLinkEmail.mjs.map +1 -1
  35. package/dist/esm/emails/MissionRequestedClientEmail.mjs.map +1 -1
  36. package/dist/esm/emails/MissionRequestedReviewerEmail.mjs.map +1 -1
  37. package/dist/esm/emails/OAuthTokenCreatedEmail.mjs.map +1 -1
  38. package/dist/esm/emails/PasswordChangeConfirmation.mjs.map +1 -1
  39. package/dist/esm/emails/ResetUserPassword.mjs.map +1 -1
  40. package/dist/esm/emails/ReviewerApplicationEmail.mjs.map +1 -1
  41. package/dist/esm/emails/ReviewerApprovedEmail.mjs.map +1 -1
  42. package/dist/esm/emails/ReviewerContactEmail.mjs.map +1 -1
  43. package/dist/esm/emails/SubscriptionPaymentCancellation.mjs.map +1 -1
  44. package/dist/esm/emails/SubscriptionPaymentError.mjs.map +1 -1
  45. package/dist/esm/emails/SubscriptionPaymentSuccess.mjs.map +1 -1
  46. package/dist/esm/emails/ValidateUserEmail.mjs.map +1 -1
  47. package/dist/esm/emails/Welcome.mjs.map +1 -1
  48. package/dist/esm/index.mjs +1 -1
  49. package/dist/esm/index.mjs.map +1 -1
  50. package/dist/esm/logger/index.mjs.map +1 -1
  51. package/dist/esm/middlewares/oAuth2.middleware.mjs.map +1 -1
  52. package/dist/esm/middlewares/sessionAuth.middleware.mjs.map +1 -1
  53. package/dist/esm/routes/ai.routes.mjs.map +1 -1
  54. package/dist/esm/routes/audit.routes.mjs.map +1 -1
  55. package/dist/esm/routes/bitbucket.routes.mjs.map +1 -1
  56. package/dist/esm/routes/demo.routes.mjs.map +1 -1
  57. package/dist/esm/routes/dictionary.routes.mjs.map +1 -1
  58. package/dist/esm/routes/environment.routes.mjs.map +1 -1
  59. package/dist/esm/routes/eventListener.routes.mjs.map +1 -1
  60. package/dist/esm/routes/github.routes.mjs.map +1 -1
  61. package/dist/esm/routes/gitlab.routes.mjs.map +1 -1
  62. package/dist/esm/routes/newsletter.routes.mjs.map +1 -1
  63. package/dist/esm/routes/organization.routes.mjs.map +1 -1
  64. package/dist/esm/routes/paramsSchemas.mjs.map +1 -1
  65. package/dist/esm/routes/project.routes.mjs.map +1 -1
  66. package/dist/esm/routes/reviewer.routes.mjs.map +1 -1
  67. package/dist/esm/routes/search.routes.mjs.map +1 -1
  68. package/dist/esm/routes/showcaseProject.routes.mjs.map +1 -1
  69. package/dist/esm/routes/stripe.routes.mjs.map +1 -1
  70. package/dist/esm/routes/tags.routes.mjs.map +1 -1
  71. package/dist/esm/routes/translate.routes.mjs.map +1 -1
  72. package/dist/esm/routes/user.routes.mjs.map +1 -1
  73. package/dist/esm/schemas/account.schema.mjs.map +1 -1
  74. package/dist/esm/schemas/affiliate.schema.mjs.map +1 -1
  75. package/dist/esm/schemas/affiliateInvitation.schema.mjs.map +1 -1
  76. package/dist/esm/schemas/audit.schema.mjs.map +1 -1
  77. package/dist/esm/schemas/auditJob.schema.mjs.map +1 -1
  78. package/dist/esm/schemas/auditPage.schema.mjs.map +1 -1
  79. package/dist/esm/schemas/cliSessionToken.schema.mjs.map +1 -1
  80. package/dist/esm/schemas/dictionary.schema.mjs.map +1 -1
  81. package/dist/esm/schemas/discussion.schema.mjs.map +1 -1
  82. package/dist/esm/schemas/oAuth2.schema.mjs.map +1 -1
  83. package/dist/esm/schemas/organization.schema.mjs.map +1 -1
  84. package/dist/esm/schemas/plans.schema.mjs.map +1 -1
  85. package/dist/esm/schemas/project.schema.mjs.map +1 -1
  86. package/dist/esm/schemas/promoCode.schema.mjs.map +1 -1
  87. package/dist/esm/schemas/reviewer.schema.mjs.map +1 -1
  88. package/dist/esm/schemas/session.schema.mjs.map +1 -1
  89. package/dist/esm/schemas/showcaseProject.schema.mjs.map +1 -1
  90. package/dist/esm/schemas/tag.schema.mjs.map +1 -1
  91. package/dist/esm/schemas/user.schema.mjs.map +1 -1
  92. package/dist/esm/services/affiliate.service.mjs.map +1 -1
  93. package/dist/esm/services/audit/analysis/analyzeBundleContent.mjs.map +1 -1
  94. package/dist/esm/services/audit/analysis/analyzeLinguisticStructure.mjs.map +1 -1
  95. package/dist/esm/services/audit/analysis/analyzeMetadata.mjs.map +1 -1
  96. package/dist/esm/services/audit/analysis/analyzeRobots.mjs.map +1 -1
  97. package/dist/esm/services/audit/analysis/analyzeSitemap.mjs.map +1 -1
  98. package/dist/esm/services/audit/analysis/analyzeUrlStructure.mjs.map +1 -1
  99. package/dist/esm/services/audit/analysis/calculateScore.mjs.map +1 -1
  100. package/dist/esm/services/audit/checkers/bundleChecker.mjs.map +1 -1
  101. package/dist/esm/services/audit/checkers/linguisticChecker.mjs.map +1 -1
  102. package/dist/esm/services/audit/checkers/metadataChecker.mjs.map +1 -1
  103. package/dist/esm/services/audit/checkers/pageChecker.mjs.map +1 -1
  104. package/dist/esm/services/audit/checkers/robotsChecker.mjs.map +1 -1
  105. package/dist/esm/services/audit/checkers/sitemapChecker.mjs.map +1 -1
  106. package/dist/esm/services/audit/checkers/urlChecker.mjs.map +1 -1
  107. package/dist/esm/services/audit/recursiveAudit.service.mjs.map +1 -1
  108. package/dist/esm/services/audit/seoAudit.service.mjs.map +1 -1
  109. package/dist/esm/services/bitbucket.service.mjs.map +1 -1
  110. package/dist/esm/services/ci.service.mjs.map +1 -1
  111. package/dist/esm/services/cliSessionToken.service.mjs.map +1 -1
  112. package/dist/esm/services/dictionary.service.mjs.map +1 -1
  113. package/dist/esm/services/email.service.mjs.map +1 -1
  114. package/dist/esm/services/environment.service.mjs.map +1 -1
  115. package/dist/esm/services/github.service.mjs.map +1 -1
  116. package/dist/esm/services/gitlab.service.mjs.map +1 -1
  117. package/dist/esm/services/oAuth2.service.mjs.map +1 -1
  118. package/dist/esm/services/organization.service.mjs.map +1 -1
  119. package/dist/esm/services/project/projectScreenshot.service.mjs.map +1 -1
  120. package/dist/esm/services/project.service.mjs.map +1 -1
  121. package/dist/esm/services/projectAccessKey.service.mjs.map +1 -1
  122. package/dist/esm/services/promoCode.service.mjs.map +1 -1
  123. package/dist/esm/services/reviewer/pictureUpload.service.mjs.map +1 -1
  124. package/dist/esm/services/reviewer.service.mjs.map +1 -1
  125. package/dist/esm/services/reviewerMessage.service.mjs.map +1 -1
  126. package/dist/esm/services/reviewerMission.service.mjs.map +1 -1
  127. package/dist/esm/services/session.service.mjs.map +1 -1
  128. package/dist/esm/services/showcase/showcaseProject.service.mjs.map +1 -1
  129. package/dist/esm/services/showcase/showcaseScan.service.mjs.map +1 -1
  130. package/dist/esm/services/showcase/showcaseUploadScreenshot.service.mjs.map +1 -1
  131. package/dist/esm/services/showcase/showcaseVerifyBundle.service.mjs.map +1 -1
  132. package/dist/esm/services/showcase/showcaseVerifyGithub.service.mjs.map +1 -1
  133. package/dist/esm/services/subscription.service.mjs.map +1 -1
  134. package/dist/esm/services/tag.service.mjs.map +1 -1
  135. package/dist/esm/services/translationQueue.service.mjs.map +1 -1
  136. package/dist/esm/services/translationWorker.service.mjs.map +1 -1
  137. package/dist/esm/services/user/avatarUpload.service.mjs.map +1 -1
  138. package/dist/esm/services/user.service.mjs.map +1 -1
  139. package/dist/esm/services/webhook.service.mjs.map +1 -1
  140. package/dist/esm/types/user.types.mjs.map +1 -1
  141. package/dist/esm/utils/AI/askDocQuestion/askDocQuestion.mjs.map +1 -1
  142. package/dist/esm/utils/AI/askDocQuestion/embeddings/docs/en/bundle_optimization.json +9954 -6953
  143. package/dist/esm/utils/AI/askDocQuestion/embeddings/docs/en/configuration.json +1 -1
  144. package/dist/esm/utils/AI/askDocQuestion/indexMarkdownFiles.mjs.map +1 -1
  145. package/dist/esm/utils/AI/auditDictionary/index.mjs.map +1 -1
  146. package/dist/esm/utils/AI/auditDictionaryField/index.mjs.map +1 -1
  147. package/dist/esm/utils/AI/auditDictionaryMetadata/index.mjs.map +1 -1
  148. package/dist/esm/utils/AI/auditTag/index.mjs.map +1 -1
  149. package/dist/esm/utils/AI/autocomplete/index.mjs.map +1 -1
  150. package/dist/esm/utils/AI/chat/index.mjs.map +1 -1
  151. package/dist/esm/utils/AI/chat/mcpInProcessTools.mjs.map +1 -1
  152. package/dist/esm/utils/AI/chat/sessionTools.mjs.map +1 -1
  153. package/dist/esm/utils/AI/customQuery/index.mjs.map +1 -1
  154. package/dist/esm/utils/AI/getProjectAIOptions.mjs.map +1 -1
  155. package/dist/esm/utils/AI/translateDictionaryDB.mjs.map +1 -1
  156. package/dist/esm/utils/AI/translateJSON/index.mjs.map +1 -1
  157. package/dist/esm/utils/accessControl.mjs.map +1 -1
  158. package/dist/esm/utils/auth/getAuth.mjs.map +1 -1
  159. package/dist/esm/utils/cors.mjs +2 -13
  160. package/dist/esm/utils/cors.mjs.map +1 -1
  161. package/dist/esm/utils/demoDictionaries.mjs.map +1 -1
  162. package/dist/esm/utils/ensureArrayQueryFilter.mjs.map +1 -1
  163. package/dist/esm/utils/ensureMongoDocumentToObject.mjs.map +1 -1
  164. package/dist/esm/utils/errors/ErrorHandler.mjs.map +1 -1
  165. package/dist/esm/utils/errors/ErrorsClass.mjs.map +1 -1
  166. package/dist/esm/utils/errors/errorCodes.mjs.map +1 -1
  167. package/dist/esm/utils/errors/index.mjs +1 -0
  168. package/dist/esm/utils/filtersAndPagination/getDictionaryFiltersAndPagination.mjs.map +1 -1
  169. package/dist/esm/utils/filtersAndPagination/getDiscussionFiltersAndPagination.mjs.map +1 -1
  170. package/dist/esm/utils/filtersAndPagination/getFiltersAndPaginationFromBody.mjs.map +1 -1
  171. package/dist/esm/utils/filtersAndPagination/getOrganizationFiltersAndPagination.mjs.map +1 -1
  172. package/dist/esm/utils/filtersAndPagination/getProjectFiltersAndPagination.mjs.map +1 -1
  173. package/dist/esm/utils/filtersAndPagination/getTagFiltersAndPagination.mjs.map +1 -1
  174. package/dist/esm/utils/filtersAndPagination/getUserFiltersAndPagination.mjs.map +1 -1
  175. package/dist/esm/utils/getFaviconUrl.mjs.map +1 -1
  176. package/dist/esm/utils/github/connectGithub.mjs.map +1 -1
  177. package/dist/esm/utils/httpStatusCodes.mjs.map +1 -1
  178. package/dist/esm/utils/image/resizeImage.mjs.map +1 -1
  179. package/dist/esm/utils/mapper/dictionary.mjs.map +1 -1
  180. package/dist/esm/utils/mapper/organization.mjs.map +1 -1
  181. package/dist/esm/utils/mapper/project.mjs.map +1 -1
  182. package/dist/esm/utils/mapper/session.mjs.map +1 -1
  183. package/dist/esm/utils/mapper/showcaseProject.mjs.map +1 -1
  184. package/dist/esm/utils/mapper/tag.mjs.map +1 -1
  185. package/dist/esm/utils/mapper/user.mjs.map +1 -1
  186. package/dist/esm/utils/mongoDB/connectDB.mjs.map +1 -1
  187. package/dist/esm/utils/oAuth2.mjs.map +1 -1
  188. package/dist/esm/utils/permissions.mjs.map +1 -1
  189. package/dist/esm/utils/plan.mjs.map +1 -1
  190. package/dist/esm/utils/puppeteer/launchBrowser.mjs.map +1 -1
  191. package/dist/esm/utils/rateLimiter.mjs.map +1 -1
  192. package/dist/esm/utils/redis/connectRedis.mjs.map +1 -1
  193. package/dist/esm/utils/removeObjectKeys.mjs.map +1 -1
  194. package/dist/esm/utils/responseData.mjs.map +1 -1
  195. package/dist/esm/utils/s3/s3Client.mjs.map +1 -1
  196. package/dist/esm/utils/validation/validateDictionary.mjs.map +1 -1
  197. package/dist/esm/utils/validation/validateOrganization.mjs.map +1 -1
  198. package/dist/esm/utils/validation/validateProject.mjs.map +1 -1
  199. package/dist/esm/utils/validation/validateTag.mjs.map +1 -1
  200. package/dist/esm/utils/validation/validateUser.mjs.map +1 -1
  201. package/dist/esm/webhooks/stripe.webhook.mjs.map +1 -1
  202. package/dist/types/controllers/ai.controller.d.ts.map +1 -1
  203. package/dist/types/controllers/bitbucket.controller.d.ts.map +1 -1
  204. package/dist/types/controllers/cliSessionToken.controller.d.ts.map +1 -1
  205. package/dist/types/controllers/demo.controller.d.ts.map +1 -1
  206. package/dist/types/controllers/dictionary.controller.d.ts.map +1 -1
  207. package/dist/types/controllers/environment.controller.d.ts.map +1 -1
  208. package/dist/types/controllers/eventListener.controller.d.ts.map +1 -1
  209. package/dist/types/controllers/github.controller.d.ts.map +1 -1
  210. package/dist/types/controllers/gitlab.controller.d.ts.map +1 -1
  211. package/dist/types/controllers/newsletter.controller.d.ts.map +1 -1
  212. package/dist/types/controllers/oAuth2.controller.d.ts.map +1 -1
  213. package/dist/types/controllers/organization.controller.d.ts.map +1 -1
  214. package/dist/types/controllers/project.controller.d.ts.map +1 -1
  215. package/dist/types/controllers/projectAccessKey.controller.d.ts.map +1 -1
  216. package/dist/types/controllers/projectMemberAccess.controller.d.ts.map +1 -1
  217. package/dist/types/controllers/recursiveAudit.controller.d.ts.map +1 -1
  218. package/dist/types/controllers/reviewer.controller.d.ts.map +1 -1
  219. package/dist/types/controllers/searchDoc.controller.d.ts.map +1 -1
  220. package/dist/types/controllers/showcaseProject.controller.d.ts.map +1 -1
  221. package/dist/types/controllers/stripe.controller.d.ts.map +1 -1
  222. package/dist/types/controllers/tag.controller.d.ts.map +1 -1
  223. package/dist/types/controllers/translation.controller.d.ts.map +1 -1
  224. package/dist/types/controllers/user.controller.d.ts.map +1 -1
  225. package/dist/types/emails/AffiliateActivatedEmail.d.ts +20 -18
  226. package/dist/types/emails/AffiliateActivatedEmail.d.ts.map +1 -1
  227. package/dist/types/emails/AffiliateConversionEmail.d.ts +21 -19
  228. package/dist/types/emails/AffiliateConversionEmail.d.ts.map +1 -1
  229. package/dist/types/emails/AffiliateInvitationEmail.d.ts +20 -18
  230. package/dist/types/emails/AffiliateInvitationEmail.d.ts.map +1 -1
  231. package/dist/types/emails/AffiliateWelcomeEmail.d.ts +20 -18
  232. package/dist/types/emails/AffiliateWelcomeEmail.d.ts.map +1 -1
  233. package/dist/types/emails/InviteUserEmail.d.ts +20 -18
  234. package/dist/types/emails/InviteUserEmail.d.ts.map +1 -1
  235. package/dist/types/emails/MagicLinkEmail.d.ts +20 -18
  236. package/dist/types/emails/MagicLinkEmail.d.ts.map +1 -1
  237. package/dist/types/emails/MissionRequestedClientEmail.d.ts +20 -18
  238. package/dist/types/emails/MissionRequestedClientEmail.d.ts.map +1 -1
  239. package/dist/types/emails/MissionRequestedReviewerEmail.d.ts +20 -18
  240. package/dist/types/emails/MissionRequestedReviewerEmail.d.ts.map +1 -1
  241. package/dist/types/emails/OAuthTokenCreatedEmail.d.ts +20 -18
  242. package/dist/types/emails/OAuthTokenCreatedEmail.d.ts.map +1 -1
  243. package/dist/types/emails/PasswordChangeConfirmation.d.ts +20 -18
  244. package/dist/types/emails/PasswordChangeConfirmation.d.ts.map +1 -1
  245. package/dist/types/emails/ResetUserPassword.d.ts +20 -18
  246. package/dist/types/emails/ResetUserPassword.d.ts.map +1 -1
  247. package/dist/types/emails/ReviewerApplicationEmail.d.ts +20 -18
  248. package/dist/types/emails/ReviewerApplicationEmail.d.ts.map +1 -1
  249. package/dist/types/emails/ReviewerApprovedEmail.d.ts +20 -18
  250. package/dist/types/emails/ReviewerApprovedEmail.d.ts.map +1 -1
  251. package/dist/types/emails/ReviewerContactEmail.d.ts +3 -1
  252. package/dist/types/emails/ReviewerContactEmail.d.ts.map +1 -1
  253. package/dist/types/emails/SubscriptionPaymentCancellation.d.ts +20 -18
  254. package/dist/types/emails/SubscriptionPaymentCancellation.d.ts.map +1 -1
  255. package/dist/types/emails/SubscriptionPaymentError.d.ts +20 -18
  256. package/dist/types/emails/SubscriptionPaymentError.d.ts.map +1 -1
  257. package/dist/types/emails/SubscriptionPaymentSuccess.d.ts +20 -18
  258. package/dist/types/emails/SubscriptionPaymentSuccess.d.ts.map +1 -1
  259. package/dist/types/emails/ValidateUserEmail.d.ts +20 -18
  260. package/dist/types/emails/ValidateUserEmail.d.ts.map +1 -1
  261. package/dist/types/emails/Welcome.d.ts +20 -18
  262. package/dist/types/emails/Welcome.d.ts.map +1 -1
  263. package/dist/types/export.d.ts +1 -1
  264. package/dist/types/logger/index.d.ts +3 -1
  265. package/dist/types/logger/index.d.ts.map +1 -1
  266. package/dist/types/middlewares/oAuth2.middleware.d.ts.map +1 -1
  267. package/dist/types/middlewares/sessionAuth.middleware.d.ts.map +1 -1
  268. package/dist/types/routes/demo.routes.d.ts.map +1 -1
  269. package/dist/types/schemas/account.schema.d.ts +35 -34
  270. package/dist/types/schemas/account.schema.d.ts.map +1 -1
  271. package/dist/types/schemas/affiliate.schema.d.ts +109 -108
  272. package/dist/types/schemas/affiliate.schema.d.ts.map +1 -1
  273. package/dist/types/schemas/affiliateInvitation.schema.d.ts +49 -48
  274. package/dist/types/schemas/affiliateInvitation.schema.d.ts.map +1 -1
  275. package/dist/types/schemas/audit.schema.d.ts.map +1 -1
  276. package/dist/types/schemas/auditJob.schema.d.ts +6 -6
  277. package/dist/types/schemas/auditPage.schema.d.ts +6 -6
  278. package/dist/types/schemas/cliSessionToken.schema.d.ts +14 -13
  279. package/dist/types/schemas/cliSessionToken.schema.d.ts.map +1 -1
  280. package/dist/types/schemas/dictionary.schema.d.ts +60 -59
  281. package/dist/types/schemas/dictionary.schema.d.ts.map +1 -1
  282. package/dist/types/schemas/discussion.schema.d.ts +51 -50
  283. package/dist/types/schemas/discussion.schema.d.ts.map +1 -1
  284. package/dist/types/schemas/oAuth2.schema.d.ts +18 -17
  285. package/dist/types/schemas/oAuth2.schema.d.ts.map +1 -1
  286. package/dist/types/schemas/organization.schema.d.ts +45 -44
  287. package/dist/types/schemas/organization.schema.d.ts.map +1 -1
  288. package/dist/types/schemas/plans.schema.d.ts +45 -44
  289. package/dist/types/schemas/plans.schema.d.ts.map +1 -1
  290. package/dist/types/schemas/project.schema.d.ts +73 -72
  291. package/dist/types/schemas/project.schema.d.ts.map +1 -1
  292. package/dist/types/schemas/promoCode.schema.d.ts +61 -60
  293. package/dist/types/schemas/promoCode.schema.d.ts.map +1 -1
  294. package/dist/types/schemas/reviewer.schema.d.ts +221 -220
  295. package/dist/types/schemas/reviewer.schema.d.ts.map +1 -1
  296. package/dist/types/schemas/session.schema.d.ts +54 -52
  297. package/dist/types/schemas/session.schema.d.ts.map +1 -1
  298. package/dist/types/schemas/showcaseProject.schema.d.ts +61 -60
  299. package/dist/types/schemas/showcaseProject.schema.d.ts.map +1 -1
  300. package/dist/types/schemas/tag.schema.d.ts +45 -44
  301. package/dist/types/schemas/tag.schema.d.ts.map +1 -1
  302. package/dist/types/schemas/user.schema.d.ts +71 -70
  303. package/dist/types/schemas/user.schema.d.ts.map +1 -1
  304. package/dist/types/services/affiliate.service.d.ts.map +1 -1
  305. package/dist/types/services/audit/analysis/analyzeBundleContent.d.ts.map +1 -1
  306. package/dist/types/services/audit/analysis/analyzeLinguisticStructure.d.ts.map +1 -1
  307. package/dist/types/services/audit/analysis/analyzeRobots.d.ts.map +1 -1
  308. package/dist/types/services/audit/analysis/analyzeSitemap.d.ts.map +1 -1
  309. package/dist/types/services/audit/analysis/calculateScore.d.ts.map +1 -1
  310. package/dist/types/services/audit/checkers/bundleChecker.d.ts.map +1 -1
  311. package/dist/types/services/audit/recursiveAudit.service.d.ts +5 -4
  312. package/dist/types/services/audit/recursiveAudit.service.d.ts.map +1 -1
  313. package/dist/types/services/audit/types.d.ts.map +1 -1
  314. package/dist/types/services/bitbucket.service.d.ts.map +1 -1
  315. package/dist/types/services/cliSessionToken.service.d.ts.map +1 -1
  316. package/dist/types/services/dictionary.service.d.ts.map +1 -1
  317. package/dist/types/services/email.service.d.ts.map +1 -1
  318. package/dist/types/services/github.service.d.ts.map +1 -1
  319. package/dist/types/services/gitlab.service.d.ts.map +1 -1
  320. package/dist/types/services/oAuth2.service.d.ts.map +1 -1
  321. package/dist/types/services/organization.service.d.ts.map +1 -1
  322. package/dist/types/services/project/projectScreenshot.service.d.ts.map +1 -1
  323. package/dist/types/services/project.service.d.ts.map +1 -1
  324. package/dist/types/services/promoCode.service.d.ts.map +1 -1
  325. package/dist/types/services/reviewer/pictureUpload.service.d.ts.map +1 -1
  326. package/dist/types/services/reviewer.service.d.ts.map +1 -1
  327. package/dist/types/services/reviewerMessage.service.d.ts.map +1 -1
  328. package/dist/types/services/reviewerMission.service.d.ts.map +1 -1
  329. package/dist/types/services/session.service.d.ts.map +1 -1
  330. package/dist/types/services/showcase/showcaseProject.service.d.ts.map +1 -1
  331. package/dist/types/services/showcase/showcaseScan.service.d.ts.map +1 -1
  332. package/dist/types/services/showcase/showcaseUploadScreenshot.service.d.ts.map +1 -1
  333. package/dist/types/services/showcase/showcaseVerifyBundle.service.d.ts.map +1 -1
  334. package/dist/types/services/showcase/showcaseVerifyGithub.service.d.ts.map +1 -1
  335. package/dist/types/services/subscription.service.d.ts.map +1 -1
  336. package/dist/types/services/tag.service.d.ts.map +1 -1
  337. package/dist/types/services/translationQueue.service.d.ts +2 -1
  338. package/dist/types/services/translationQueue.service.d.ts.map +1 -1
  339. package/dist/types/services/translationWorker.service.d.ts.map +1 -1
  340. package/dist/types/services/user/avatarUpload.service.d.ts.map +1 -1
  341. package/dist/types/services/user.service.d.ts.map +1 -1
  342. package/dist/types/services/webhook.service.d.ts.map +1 -1
  343. package/dist/types/types/Routes.d.ts.map +1 -1
  344. package/dist/types/types/account.types.d.ts.map +1 -1
  345. package/dist/types/types/affiliate.types.d.ts.map +1 -1
  346. package/dist/types/types/affiliateInvitation.types.d.ts.map +1 -1
  347. package/dist/types/types/dictionary.types.d.ts.map +1 -1
  348. package/dist/types/types/discussion.types.d.ts.map +1 -1
  349. package/dist/types/types/oAuth2.types.d.ts.map +1 -1
  350. package/dist/types/types/organization.types.d.ts.map +1 -1
  351. package/dist/types/types/plan.types.d.ts.map +1 -1
  352. package/dist/types/types/project.types.d.ts.map +1 -1
  353. package/dist/types/types/promoCode.types.d.ts.map +1 -1
  354. package/dist/types/types/reviewer.types.d.ts.map +1 -1
  355. package/dist/types/types/session.types.d.ts.map +1 -1
  356. package/dist/types/types/showcaseProject.types.d.ts.map +1 -1
  357. package/dist/types/types/tag.types.d.ts.map +1 -1
  358. package/dist/types/types/user.types.d.ts.map +1 -1
  359. package/dist/types/utils/AI/askDocQuestion/askDocQuestion.d.ts.map +1 -1
  360. package/dist/types/utils/AI/askDocQuestion/indexMarkdownFiles.d.ts.map +1 -1
  361. package/dist/types/utils/AI/auditDictionary/index.d.ts.map +1 -1
  362. package/dist/types/utils/AI/auditDictionaryField/index.d.ts.map +1 -1
  363. package/dist/types/utils/AI/auditDictionaryMetadata/index.d.ts.map +1 -1
  364. package/dist/types/utils/AI/auditTag/index.d.ts.map +1 -1
  365. package/dist/types/utils/AI/autocomplete/index.d.ts.map +1 -1
  366. package/dist/types/utils/AI/chat/index.d.ts.map +1 -1
  367. package/dist/types/utils/AI/chat/mcpInProcessTools.d.ts.map +1 -1
  368. package/dist/types/utils/AI/chat/sessionTools.d.ts +25 -24
  369. package/dist/types/utils/AI/chat/sessionTools.d.ts.map +1 -1
  370. package/dist/types/utils/AI/customQuery/index.d.ts.map +1 -1
  371. package/dist/types/utils/AI/translateDictionaryDB.d.ts.map +1 -1
  372. package/dist/types/utils/AI/translateJSON/index.d.ts.map +1 -1
  373. package/dist/types/utils/auth/getAuth.d.ts.map +1 -1
  374. package/dist/types/utils/cors.d.ts +1 -7
  375. package/dist/types/utils/cors.d.ts.map +1 -1
  376. package/dist/types/utils/demoDictionaries.d.ts.map +1 -1
  377. package/dist/types/utils/ensureArrayQueryFilter.d.ts.map +1 -1
  378. package/dist/types/utils/errors/ErrorHandler.d.ts +8 -6
  379. package/dist/types/utils/errors/ErrorHandler.d.ts.map +1 -1
  380. package/dist/types/utils/errors/ErrorsClass.d.ts.map +1 -1
  381. package/dist/types/utils/errors/errorCodes.d.ts.map +1 -1
  382. package/dist/types/utils/filtersAndPagination/getDictionaryFiltersAndPagination.d.ts.map +1 -1
  383. package/dist/types/utils/filtersAndPagination/getDiscussionFiltersAndPagination.d.ts.map +1 -1
  384. package/dist/types/utils/filtersAndPagination/getFiltersAndPaginationFromBody.d.ts.map +1 -1
  385. package/dist/types/utils/filtersAndPagination/getOrganizationFiltersAndPagination.d.ts.map +1 -1
  386. package/dist/types/utils/filtersAndPagination/getProjectFiltersAndPagination.d.ts.map +1 -1
  387. package/dist/types/utils/filtersAndPagination/getTagFiltersAndPagination.d.ts +7 -6
  388. package/dist/types/utils/filtersAndPagination/getTagFiltersAndPagination.d.ts.map +1 -1
  389. package/dist/types/utils/filtersAndPagination/getUserFiltersAndPagination.d.ts.map +1 -1
  390. package/dist/types/utils/getFaviconUrl.d.ts.map +1 -1
  391. package/dist/types/utils/github/connectGithub.d.ts.map +1 -1
  392. package/dist/types/utils/httpStatusCodes.d.ts.map +1 -1
  393. package/dist/types/utils/image/resizeImage.d.ts.map +1 -1
  394. package/dist/types/utils/mapper/dictionary.d.ts.map +1 -1
  395. package/dist/types/utils/mapper/project.d.ts.map +1 -1
  396. package/dist/types/utils/mapper/session.d.ts.map +1 -1
  397. package/dist/types/utils/mapper/showcaseProject.d.ts.map +1 -1
  398. package/dist/types/utils/mapper/tag.d.ts.map +1 -1
  399. package/dist/types/utils/mergeFunctionTypes.d.ts.map +1 -1
  400. package/dist/types/utils/mongoDB/connectDB.d.ts.map +1 -1
  401. package/dist/types/utils/mongoDB/types.d.ts.map +1 -1
  402. package/dist/types/utils/oAuth2.d.ts.map +1 -1
  403. package/dist/types/utils/permissions.d.ts +2 -1
  404. package/dist/types/utils/permissions.d.ts.map +1 -1
  405. package/dist/types/utils/plan.d.ts.map +1 -1
  406. package/dist/types/utils/rateLimiter.d.ts.map +1 -1
  407. package/dist/types/utils/redis/connectRedis.d.ts.map +1 -1
  408. package/dist/types/utils/responseData.d.ts.map +1 -1
  409. package/dist/types/utils/s3/s3Client.d.ts.map +1 -1
  410. package/dist/types/utils/validation/validateDictionary.d.ts.map +1 -1
  411. package/dist/types/utils/validation/validateOrganization.d.ts.map +1 -1
  412. package/dist/types/utils/validation/validateProject.d.ts.map +1 -1
  413. package/dist/types/utils/validation/validateTag.d.ts.map +1 -1
  414. package/dist/types/utils/validation/validateUser.d.ts.map +1 -1
  415. package/package.json +17 -17
@@ -1 +1 @@
1
- {"version":3,"file":"project.service.mjs","names":[],"sources":["../../../src/services/project.service.ts"],"sourcesContent":["import { ProjectModel } from '@schemas/project.schema';\nimport { ensureMongoDocumentToObject } from '@utils/ensureMongoDocumentToObject';\nimport { GenericError } from '@utils/errors';\nimport type { ProjectFilters } from '@utils/filtersAndPagination/getProjectFiltersAndPagination';\nimport { removeObjectKeys } from '@utils/removeObjectKeys';\nimport {\n type ProjectFields,\n validateProject,\n} from '@utils/validation/validateProject';\nimport type { Types } from 'mongoose';\nimport type {\n Project,\n ProjectAPI,\n ProjectData,\n ProjectDocument,\n} from '@/types/project.types';\n\n/**\n * Finds projects based on filters and pagination options.\n * @param filters - MongoDB filter query.\n * @param skip - Number of documents to skip.\n * @param limit - Number of documents to limit.\n * @returns List of projects matching the filters.\n */\nexport const findProjects = async (\n filters: ProjectFilters,\n skip = 0,\n limit = 100,\n sortOptions?: Record<string, 1 | -1>\n): Promise<ProjectDocument[]> => {\n let query = ProjectModel.find(filters).skip(skip).limit(limit);\n\n if (sortOptions && Object.keys(sortOptions).length > 0) {\n query = query.sort(sortOptions);\n }\n\n return await query;\n};\n\n/**\n * Finds a project by its ID.\n * @param projectId - The ID of the project to find.\n * @returns The project matching the ID.\n */\nexport const getProjectById = async (\n projectId: string | Types.ObjectId\n): Promise<ProjectDocument> => {\n const project = await ProjectModel.findById(projectId);\n\n if (!project) {\n throw new GenericError('PROJECT_NOT_DEFINED', { projectId });\n }\n\n return project;\n};\n\n/**\n * Counts the total number of projects that match the filters.\n * @param filters - MongoDB filter query.\n * @returns Total number of projects.\n */\nexport const countProjects = async (\n filters: ProjectFilters\n): Promise<number> => {\n const result = await ProjectModel.countDocuments(filters);\n\n if (typeof result === 'undefined') {\n throw new GenericError('PROJECT_COUNT_FAILED', { filters });\n }\n\n return result;\n};\n\n/**\n * Creates a new project in the database.\n * @param project - The project data to create.\n * @returns The created project.\n */\nexport const createProject = async (\n project: ProjectData\n): Promise<ProjectDocument> => {\n if ((project as Partial<Project>).oAuth2Access) {\n (project as Partial<Project>).oAuth2Access = undefined;\n }\n\n const errors = await validateProject(project, ['name']);\n\n if (Object.keys(errors).length > 0) {\n throw new GenericError('PROJECT_INVALID_FIELDS', { errors });\n }\n\n // Ensure a default production environment always exists\n const projectWithDefaultEnvironment: ProjectData = {\n ...project,\n environments:\n project.environments && project.environments.length > 0\n ? project.environments\n : [{ name: 'production', isDefault: true } as any],\n };\n\n return await ProjectModel.create(projectWithDefaultEnvironment);\n};\n\n/**\n * Updates an existing project in the database by its ID.\n * @param projectId - The ID of the project to update.\n * @param project - The updated project data.\n * @returns The updated project.\n */\nexport const updateProjectById = async (\n projectId: string | Types.ObjectId,\n project: Partial<Project | ProjectAPI>\n): Promise<ProjectDocument> => {\n const projectObject = ensureMongoDocumentToObject(project);\n const projectToUpdate = removeObjectKeys(projectObject, [\n 'id',\n 'oAuth2Access',\n 'organizationId',\n ]);\n\n const updatedKeys = Object.keys(projectToUpdate) as ProjectFields;\n\n const errors = await validateProject(project, updatedKeys);\n\n if (Object.keys(errors).length > 0) {\n throw new GenericError('PROJECT_INVALID_FIELDS', {\n projectId,\n errors,\n });\n }\n\n const result = await ProjectModel.updateOne(\n { _id: projectId },\n projectToUpdate\n );\n\n if (result.matchedCount === 0) {\n throw new GenericError('PROJECT_UPDATE_FAILED', { projectId });\n }\n\n return await getProjectById(projectId);\n};\n\n/**\n * Deletes a project from the database by its ID.\n * @param projectId - The ID of the project to delete.\n * @returns The result of the deletion operation.\n */\nexport const deleteProjectById = async (\n projectId: string | Types.ObjectId\n): Promise<ProjectDocument> => {\n const project = await ProjectModel.findByIdAndDelete(projectId);\n\n if (!project) {\n throw new GenericError('PROJECT_NOT_DEFINED', { projectId });\n }\n\n return project;\n};\n"],"mappings":";;;;;;;;;;;;;;AAwBA,MAAa,eAAe,OAC1B,SACA,OAAO,GACP,QAAQ,KACR,gBAC+B;CAC/B,IAAI,QAAQ,aAAa,KAAK,OAAO,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,MAAM,KAAK;CAE7D,IAAI,eAAe,OAAO,KAAK,WAAW,CAAC,CAAC,SAAS,GACnD,QAAQ,MAAM,KAAK,WAAW;CAGhC,OAAO,MAAM;AACf;;;;;;AAOA,MAAa,iBAAiB,OAC5B,cAC6B;CAC7B,MAAM,UAAU,MAAM,aAAa,SAAS,SAAS;CAErD,IAAI,CAAC,SACH,MAAM,IAAI,aAAa,uBAAuB,EAAE,UAAU,CAAC;CAG7D,OAAO;AACT;;;;;;AAOA,MAAa,gBAAgB,OAC3B,YACoB;CACpB,MAAM,SAAS,MAAM,aAAa,eAAe,OAAO;CAExD,IAAI,OAAO,WAAW,aACpB,MAAM,IAAI,aAAa,wBAAwB,EAAE,QAAQ,CAAC;CAG5D,OAAO;AACT;;;;;;AAOA,MAAa,gBAAgB,OAC3B,YAC6B;CAC7B,IAAK,QAA6B,cAChC,AAAC,QAA6B,eAAe;CAG/C,MAAM,SAAS,MAAM,gBAAgB,SAAS,CAAC,MAAM,CAAC;CAEtD,IAAI,OAAO,KAAK,MAAM,CAAC,CAAC,SAAS,GAC/B,MAAM,IAAI,aAAa,0BAA0B,EAAE,OAAO,CAAC;CAI7D,MAAM,gCAA6C;EACjD,GAAG;EACH,cACE,QAAQ,gBAAgB,QAAQ,aAAa,SAAS,IAClD,QAAQ,eACR,CAAC;GAAE,MAAM;GAAc,WAAW;EAAK,CAAQ;CACvD;CAEA,OAAO,MAAM,aAAa,OAAO,6BAA6B;AAChE;;;;;;;AAQA,MAAa,oBAAoB,OAC/B,WACA,YAC6B;CAE7B,MAAM,kBAAkB,iBADF,4BAA4B,OACG,GAAG;EACtD;EACA;EACA;CACF,CAAC;CAID,MAAM,SAAS,MAAM,gBAAgB,SAFjB,OAAO,KAAK,eAEwB,CAAC;CAEzD,IAAI,OAAO,KAAK,MAAM,CAAC,CAAC,SAAS,GAC/B,MAAM,IAAI,aAAa,0BAA0B;EAC/C;EACA;CACF,CAAC;CAQH,KAAI,MALiB,aAAa,UAChC,EAAE,KAAK,UAAU,GACjB,eACF,EAEU,CAAC,iBAAiB,GAC1B,MAAM,IAAI,aAAa,yBAAyB,EAAE,UAAU,CAAC;CAG/D,OAAO,MAAM,eAAe,SAAS;AACvC;;;;;;AAOA,MAAa,oBAAoB,OAC/B,cAC6B;CAC7B,MAAM,UAAU,MAAM,aAAa,kBAAkB,SAAS;CAE9D,IAAI,CAAC,SACH,MAAM,IAAI,aAAa,uBAAuB,EAAE,UAAU,CAAC;CAG7D,OAAO;AACT"}
1
+ {"version":3,"file":"project.service.mjs","names":[],"sources":["../../../src/services/project.service.ts"],"sourcesContent":["import { ProjectModel } from '@schemas/project.schema';\nimport { ensureMongoDocumentToObject } from '@utils/ensureMongoDocumentToObject';\nimport { GenericError } from '@utils/errors';\nimport type { ProjectFilters } from '@utils/filtersAndPagination/getProjectFiltersAndPagination';\nimport { removeObjectKeys } from '@utils/removeObjectKeys';\nimport {\n type ProjectFields,\n validateProject,\n} from '@utils/validation/validateProject';\nimport type { Types } from 'mongoose';\nimport type {\n Project,\n ProjectAPI,\n ProjectData,\n ProjectDocument,\n} from '@/types/project.types';\n\n/**\n * Finds projects based on filters and pagination options.\n * @param filters - MongoDB filter query.\n * @param skip - Number of documents to skip.\n * @param limit - Number of documents to limit.\n * @returns List of projects matching the filters.\n */\nexport const findProjects = async (\n filters: ProjectFilters,\n skip = 0,\n limit = 100,\n sortOptions?: Record<string, 1 | -1>\n): Promise<ProjectDocument[]> => {\n let query = ProjectModel.find(filters).skip(skip).limit(limit);\n\n if (sortOptions && Object.keys(sortOptions).length > 0) {\n query = query.sort(sortOptions);\n }\n\n return await query;\n};\n\n/**\n * Finds a project by its ID.\n * @param projectId - The ID of the project to find.\n * @returns The project matching the ID.\n */\nexport const getProjectById = async (\n projectId: string | Types.ObjectId\n): Promise<ProjectDocument> => {\n const project = await ProjectModel.findById(projectId);\n\n if (!project) {\n throw new GenericError('PROJECT_NOT_DEFINED', { projectId });\n }\n\n return project;\n};\n\n/**\n * Counts the total number of projects that match the filters.\n * @param filters - MongoDB filter query.\n * @returns Total number of projects.\n */\nexport const countProjects = async (\n filters: ProjectFilters\n): Promise<number> => {\n const result = await ProjectModel.countDocuments(filters);\n\n if (typeof result === 'undefined') {\n throw new GenericError('PROJECT_COUNT_FAILED', { filters });\n }\n\n return result;\n};\n\n/**\n * Creates a new project in the database.\n * @param project - The project data to create.\n * @returns The created project.\n */\nexport const createProject = async (\n project: ProjectData\n): Promise<ProjectDocument> => {\n if ((project as Partial<Project>).oAuth2Access) {\n (project as Partial<Project>).oAuth2Access = undefined;\n }\n\n const errors = await validateProject(project, ['name']);\n\n if (Object.keys(errors).length > 0) {\n throw new GenericError('PROJECT_INVALID_FIELDS', { errors });\n }\n\n // Ensure a default production environment always exists\n const projectWithDefaultEnvironment: ProjectData = {\n ...project,\n environments:\n project.environments && project.environments.length > 0\n ? project.environments\n : [{ name: 'production', isDefault: true } as any],\n };\n\n return await ProjectModel.create(projectWithDefaultEnvironment);\n};\n\n/**\n * Updates an existing project in the database by its ID.\n * @param projectId - The ID of the project to update.\n * @param project - The updated project data.\n * @returns The updated project.\n */\nexport const updateProjectById = async (\n projectId: string | Types.ObjectId,\n project: Partial<Project | ProjectAPI>\n): Promise<ProjectDocument> => {\n const projectObject = ensureMongoDocumentToObject(project);\n const projectToUpdate = removeObjectKeys(projectObject, [\n 'id',\n 'oAuth2Access',\n 'organizationId',\n ]);\n\n const updatedKeys = Object.keys(projectToUpdate) as ProjectFields;\n\n const errors = await validateProject(project, updatedKeys);\n\n if (Object.keys(errors).length > 0) {\n throw new GenericError('PROJECT_INVALID_FIELDS', {\n projectId,\n errors,\n });\n }\n\n const result = await ProjectModel.updateOne(\n { _id: projectId },\n projectToUpdate\n );\n\n if (result.matchedCount === 0) {\n throw new GenericError('PROJECT_UPDATE_FAILED', { projectId });\n }\n\n return await getProjectById(projectId);\n};\n\n/**\n * Deletes a project from the database by its ID.\n * @param projectId - The ID of the project to delete.\n * @returns The result of the deletion operation.\n */\nexport const deleteProjectById = async (\n projectId: string | Types.ObjectId\n): Promise<ProjectDocument> => {\n const project = await ProjectModel.findByIdAndDelete(projectId);\n\n if (!project) {\n throw new GenericError('PROJECT_NOT_DEFINED', { projectId });\n }\n\n return project;\n};\n"],"mappings":";;;;;;;;;;;;;;AAwBA,MAAa,eAAe,OAC1B,SACA,OAAO,GACP,QAAQ,KACR,gBAC+B;CAC/B,IAAI,QAAQ,aAAa,KAAK,QAAQ,CAAC,KAAK,KAAK,CAAC,MAAM,MAAM;AAE9D,KAAI,eAAe,OAAO,KAAK,YAAY,CAAC,SAAS,EACnD,SAAQ,MAAM,KAAK,YAAY;AAGjC,QAAO,MAAM;;;;;;;AAQf,MAAa,iBAAiB,OAC5B,cAC6B;CAC7B,MAAM,UAAU,MAAM,aAAa,SAAS,UAAU;AAEtD,KAAI,CAAC,QACH,OAAM,IAAI,aAAa,uBAAuB,EAAE,WAAW,CAAC;AAG9D,QAAO;;;;;;;AAQT,MAAa,gBAAgB,OAC3B,YACoB;CACpB,MAAM,SAAS,MAAM,aAAa,eAAe,QAAQ;AAEzD,KAAI,OAAO,WAAW,YACpB,OAAM,IAAI,aAAa,wBAAwB,EAAE,SAAS,CAAC;AAG7D,QAAO;;;;;;;AAQT,MAAa,gBAAgB,OAC3B,YAC6B;AAC7B,KAAK,QAA6B,aAChC,CAAC,QAA6B,eAAe;CAG/C,MAAM,SAAS,MAAM,gBAAgB,SAAS,CAAC,OAAO,CAAC;AAEvD,KAAI,OAAO,KAAK,OAAO,CAAC,SAAS,EAC/B,OAAM,IAAI,aAAa,0BAA0B,EAAE,QAAQ,CAAC;CAI9D,MAAM,gCAA6C;EACjD,GAAG;EACH,cACE,QAAQ,gBAAgB,QAAQ,aAAa,SAAS,IAClD,QAAQ,eACR,CAAC;GAAE,MAAM;GAAc,WAAW;GAAM,CAAQ;EACvD;AAED,QAAO,MAAM,aAAa,OAAO,8BAA8B;;;;;;;;AASjE,MAAa,oBAAoB,OAC/B,WACA,YAC6B;CAE7B,MAAM,kBAAkB,iBADF,4BAA4B,QACI,EAAE;EACtD;EACA;EACA;EACD,CAAC;CAIF,MAAM,SAAS,MAAM,gBAAgB,SAFjB,OAAO,KAAK,gBAEyB,CAAC;AAE1D,KAAI,OAAO,KAAK,OAAO,CAAC,SAAS,EAC/B,OAAM,IAAI,aAAa,0BAA0B;EAC/C;EACA;EACD,CAAC;AAQJ,MAAI,MALiB,aAAa,UAChC,EAAE,KAAK,WAAW,EAClB,gBACD,EAEU,iBAAiB,EAC1B,OAAM,IAAI,aAAa,yBAAyB,EAAE,WAAW,CAAC;AAGhE,QAAO,MAAM,eAAe,UAAU;;;;;;;AAQxC,MAAa,oBAAoB,OAC/B,cAC6B;CAC7B,MAAM,UAAU,MAAM,aAAa,kBAAkB,UAAU;AAE/D,KAAI,CAAC,QACH,OAAM,IAAI,aAAa,uBAAuB,EAAE,WAAW,CAAC;AAG9D,QAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"projectAccessKey.service.mjs","names":[],"sources":["../../../src/services/projectAccessKey.service.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport { ProjectModel } from '@schemas/project.schema';\nimport { GenericError } from '@utils/errors';\nimport type { Types } from 'mongoose';\nimport type {\n AccessKeyData,\n OAuth2Access,\n OAuth2AccessData,\n Project,\n} from '@/types/project.types';\nimport type { User } from '@/types/user.types';\nimport { getProjectById } from './project.service';\n\n/**\n * Generates cryptographically secure OAuth2 client credentials\n *\n * @returns Object containing clientId and clientSecret\n *\n * Security improvements:\n * - clientId: 32 characters (128 bits of entropy)\n * - clientSecret: 64 characters (256 bits of entropy)\n * - Uses crypto.randomBytes for cryptographically secure random generation\n * - Follows OAuth2 best practices for credential strength\n */\nconst generateClientCredentials = () => ({\n clientId: randomBytes(16).toString('hex'), // 32 character hexadecimal string\n clientSecret: randomBytes(32).toString('hex'), // 64 character hexadecimal string\n});\n\n/**\n * Adds a new access key to a project.\n *\n * @param accessKeyData - The access key data.\n * @param projectId - The ID of the project to add the access key to.\n * @param user - The user adding the access key.\n * @returns The new access key.\n *\n */\nexport const addNewAccessKey = async (\n accessKeyData: AccessKeyData,\n projectId: string | Types.ObjectId,\n user: User\n): Promise<OAuth2Access> => {\n const { clientId, clientSecret } = generateClientCredentials();\n\n const newAccessKey: OAuth2AccessData = {\n ...accessKeyData,\n clientId,\n clientSecret,\n userId: user.id,\n accessToken: [],\n grants: accessKeyData.grants,\n allowedEnvironmentIds: accessKeyData.allowedEnvironmentIds ?? null,\n allowedLocales: accessKeyData.allowedLocales ?? null,\n };\n\n const result = await ProjectModel.updateOne(\n { _id: projectId },\n { $push: { oAuth2Access: newAccessKey } }\n );\n\n if (result.modifiedCount === 0) {\n throw new GenericError('ACCESS_KEY_CREATION_FAILED', {\n accessKeyData,\n projectId,\n userId: user.id,\n });\n }\n\n const updatedProject = await getProjectById(projectId);\n\n const newAccessKeyId = updatedProject.oAuth2Access.find(\n (access) => access.clientId === clientId\n );\n\n if (!newAccessKeyId) {\n throw new GenericError('ACCESS_KEY_CREATION_FAILED', {\n accessKeyData,\n projectId,\n userId: user.id,\n });\n }\n\n return newAccessKeyId;\n};\n\nexport const deleteAccessKey = async (\n clientId: string | Types.ObjectId,\n project: Project,\n userId: string | Types.ObjectId\n) => {\n const projectAccess = project.oAuth2Access.find(\n (access) =>\n access.clientId === clientId && String(access.userId) === String(userId)\n );\n\n if (!projectAccess) {\n throw new GenericError('ACCESS_KEY_NOT_FOUND', {\n clientId,\n projectId: project.id,\n });\n }\n\n const result = await ProjectModel.updateOne(\n {\n 'oAuth2Access.clientId': String(clientId),\n 'oAuth2Access.userId': String(userId),\n },\n { $pull: { oAuth2Access: { clientId: String(clientId) } } }\n );\n\n if (result.modifiedCount === 0) {\n throw new GenericError('ACCESS_KEY_DELETION_FAILED', {\n clientId,\n projectId: project.id,\n });\n }\n\n return projectAccess;\n};\n\nexport const refreshAccessKey = async (\n clientId: string | Types.ObjectId,\n projectId: string | Types.ObjectId,\n userId: string | Types.ObjectId\n): Promise<OAuth2Access> => {\n const project = await ProjectModel.findOne({\n _id: projectId,\n 'oAuth2Access.clientId': String(clientId),\n 'oAuth2Access.userId': String(userId),\n });\n\n if (!project) {\n throw new GenericError('PROJECT_NOT_FOUND', {\n clientId,\n projectId,\n userId,\n });\n }\n\n const projectAccess = project.oAuth2Access.find(\n (access) => access.clientId === clientId\n );\n\n if (!projectAccess) {\n throw new GenericError('ACCESS_KEY_NOT_FOUND', {\n clientId,\n projectId: project.id,\n });\n }\n\n const { clientSecret } = generateClientCredentials();\n\n const result = await ProjectModel.updateOne(\n {\n 'oAuth2Access.clientId': String(clientId),\n 'oAuth2Access.userId': String(userId),\n },\n {\n $set: {\n 'oAuth2Access.$.clientId': projectAccess.clientId,\n 'oAuth2Access.$.clientSecret': clientSecret,\n },\n }\n );\n\n if (result.modifiedCount === 0) {\n throw new GenericError('ACCESS_KEY_UPDATE_FAILED', {\n clientId,\n projectId,\n });\n }\n\n const updatedProject = await getProjectById(projectId);\n\n const newAccessKeyId = updatedProject.oAuth2Access.find(\n (access) => access.clientId === projectAccess.clientId\n );\n\n if (!newAccessKeyId) {\n throw new GenericError('ACCESS_KEY_CREATION_FAILED', {\n accessKeyData: updatedProject.oAuth2Access,\n projectId,\n userId,\n });\n }\n\n return newAccessKeyId;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;AAwBA,MAAM,mCAAmC;CACvC,UAAU,YAAY,EAAE,CAAC,CAAC,SAAS,KAAK;CACxC,cAAc,YAAY,EAAE,CAAC,CAAC,SAAS,KAAK;AAC9C;;;;;;;;;;AAWA,MAAa,kBAAkB,OAC7B,eACA,WACA,SAC0B;CAC1B,MAAM,EAAE,UAAU,iBAAiB,0BAA0B;CAE7D,MAAM,eAAiC;EACrC,GAAG;EACH;EACA;EACA,QAAQ,KAAK;EACb,aAAa,CAAC;EACd,QAAQ,cAAc;EACtB,uBAAuB,cAAc,yBAAyB;EAC9D,gBAAgB,cAAc,kBAAkB;CAClD;CAOA,KAAI,MALiB,aAAa,UAChC,EAAE,KAAK,UAAU,GACjB,EAAE,OAAO,EAAE,cAAc,aAAa,EAAE,CAC1C,EAEU,CAAC,kBAAkB,GAC3B,MAAM,IAAI,aAAa,8BAA8B;EACnD;EACA;EACA,QAAQ,KAAK;CACf,CAAC;CAKH,MAAM,kBAAiB,MAFM,eAAe,SAAS,EAEhB,CAAC,aAAa,MAChD,WAAW,OAAO,aAAa,QAClC;CAEA,IAAI,CAAC,gBACH,MAAM,IAAI,aAAa,8BAA8B;EACnD;EACA;EACA,QAAQ,KAAK;CACf,CAAC;CAGH,OAAO;AACT;AAEA,MAAa,kBAAkB,OAC7B,UACA,SACA,WACG;CACH,MAAM,gBAAgB,QAAQ,aAAa,MACxC,WACC,OAAO,aAAa,YAAY,OAAO,OAAO,MAAM,MAAM,OAAO,MAAM,CAC3E;CAEA,IAAI,CAAC,eACH,MAAM,IAAI,aAAa,wBAAwB;EAC7C;EACA,WAAW,QAAQ;CACrB,CAAC;CAWH,KAAI,MARiB,aAAa,UAChC;EACE,yBAAyB,OAAO,QAAQ;EACxC,uBAAuB,OAAO,MAAM;CACtC,GACA,EAAE,OAAO,EAAE,cAAc,EAAE,UAAU,OAAO,QAAQ,EAAE,EAAE,EAAE,CAC5D,EAEU,CAAC,kBAAkB,GAC3B,MAAM,IAAI,aAAa,8BAA8B;EACnD;EACA,WAAW,QAAQ;CACrB,CAAC;CAGH,OAAO;AACT;AAEA,MAAa,mBAAmB,OAC9B,UACA,WACA,WAC0B;CAC1B,MAAM,UAAU,MAAM,aAAa,QAAQ;EACzC,KAAK;EACL,yBAAyB,OAAO,QAAQ;EACxC,uBAAuB,OAAO,MAAM;CACtC,CAAC;CAED,IAAI,CAAC,SACH,MAAM,IAAI,aAAa,qBAAqB;EAC1C;EACA;EACA;CACF,CAAC;CAGH,MAAM,gBAAgB,QAAQ,aAAa,MACxC,WAAW,OAAO,aAAa,QAClC;CAEA,IAAI,CAAC,eACH,MAAM,IAAI,aAAa,wBAAwB;EAC7C;EACA,WAAW,QAAQ;CACrB,CAAC;CAGH,MAAM,EAAE,iBAAiB,0BAA0B;CAenD,KAAI,MAbiB,aAAa,UAChC;EACE,yBAAyB,OAAO,QAAQ;EACxC,uBAAuB,OAAO,MAAM;CACtC,GACA,EACE,MAAM;EACJ,2BAA2B,cAAc;EACzC,+BAA+B;CACjC,EACF,CACF,EAEU,CAAC,kBAAkB,GAC3B,MAAM,IAAI,aAAa,4BAA4B;EACjD;EACA;CACF,CAAC;CAGH,MAAM,iBAAiB,MAAM,eAAe,SAAS;CAErD,MAAM,iBAAiB,eAAe,aAAa,MAChD,WAAW,OAAO,aAAa,cAAc,QAChD;CAEA,IAAI,CAAC,gBACH,MAAM,IAAI,aAAa,8BAA8B;EACnD,eAAe,eAAe;EAC9B;EACA;CACF,CAAC;CAGH,OAAO;AACT"}
1
+ {"version":3,"file":"projectAccessKey.service.mjs","names":[],"sources":["../../../src/services/projectAccessKey.service.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport { ProjectModel } from '@schemas/project.schema';\nimport { GenericError } from '@utils/errors';\nimport type { Types } from 'mongoose';\nimport type {\n AccessKeyData,\n OAuth2Access,\n OAuth2AccessData,\n Project,\n} from '@/types/project.types';\nimport type { User } from '@/types/user.types';\nimport { getProjectById } from './project.service';\n\n/**\n * Generates cryptographically secure OAuth2 client credentials\n *\n * @returns Object containing clientId and clientSecret\n *\n * Security improvements:\n * - clientId: 32 characters (128 bits of entropy)\n * - clientSecret: 64 characters (256 bits of entropy)\n * - Uses crypto.randomBytes for cryptographically secure random generation\n * - Follows OAuth2 best practices for credential strength\n */\nconst generateClientCredentials = () => ({\n clientId: randomBytes(16).toString('hex'), // 32 character hexadecimal string\n clientSecret: randomBytes(32).toString('hex'), // 64 character hexadecimal string\n});\n\n/**\n * Adds a new access key to a project.\n *\n * @param accessKeyData - The access key data.\n * @param projectId - The ID of the project to add the access key to.\n * @param user - The user adding the access key.\n * @returns The new access key.\n *\n */\nexport const addNewAccessKey = async (\n accessKeyData: AccessKeyData,\n projectId: string | Types.ObjectId,\n user: User\n): Promise<OAuth2Access> => {\n const { clientId, clientSecret } = generateClientCredentials();\n\n const newAccessKey: OAuth2AccessData = {\n ...accessKeyData,\n clientId,\n clientSecret,\n userId: user.id,\n accessToken: [],\n grants: accessKeyData.grants,\n allowedEnvironmentIds: accessKeyData.allowedEnvironmentIds ?? null,\n allowedLocales: accessKeyData.allowedLocales ?? null,\n };\n\n const result = await ProjectModel.updateOne(\n { _id: projectId },\n { $push: { oAuth2Access: newAccessKey } }\n );\n\n if (result.modifiedCount === 0) {\n throw new GenericError('ACCESS_KEY_CREATION_FAILED', {\n accessKeyData,\n projectId,\n userId: user.id,\n });\n }\n\n const updatedProject = await getProjectById(projectId);\n\n const newAccessKeyId = updatedProject.oAuth2Access.find(\n (access) => access.clientId === clientId\n );\n\n if (!newAccessKeyId) {\n throw new GenericError('ACCESS_KEY_CREATION_FAILED', {\n accessKeyData,\n projectId,\n userId: user.id,\n });\n }\n\n return newAccessKeyId;\n};\n\nexport const deleteAccessKey = async (\n clientId: string | Types.ObjectId,\n project: Project,\n userId: string | Types.ObjectId\n) => {\n const projectAccess = project.oAuth2Access.find(\n (access) =>\n access.clientId === clientId && String(access.userId) === String(userId)\n );\n\n if (!projectAccess) {\n throw new GenericError('ACCESS_KEY_NOT_FOUND', {\n clientId,\n projectId: project.id,\n });\n }\n\n const result = await ProjectModel.updateOne(\n {\n 'oAuth2Access.clientId': String(clientId),\n 'oAuth2Access.userId': String(userId),\n },\n { $pull: { oAuth2Access: { clientId: String(clientId) } } }\n );\n\n if (result.modifiedCount === 0) {\n throw new GenericError('ACCESS_KEY_DELETION_FAILED', {\n clientId,\n projectId: project.id,\n });\n }\n\n return projectAccess;\n};\n\nexport const refreshAccessKey = async (\n clientId: string | Types.ObjectId,\n projectId: string | Types.ObjectId,\n userId: string | Types.ObjectId\n): Promise<OAuth2Access> => {\n const project = await ProjectModel.findOne({\n _id: projectId,\n 'oAuth2Access.clientId': String(clientId),\n 'oAuth2Access.userId': String(userId),\n });\n\n if (!project) {\n throw new GenericError('PROJECT_NOT_FOUND', {\n clientId,\n projectId,\n userId,\n });\n }\n\n const projectAccess = project.oAuth2Access.find(\n (access) => access.clientId === clientId\n );\n\n if (!projectAccess) {\n throw new GenericError('ACCESS_KEY_NOT_FOUND', {\n clientId,\n projectId: project.id,\n });\n }\n\n const { clientSecret } = generateClientCredentials();\n\n const result = await ProjectModel.updateOne(\n {\n 'oAuth2Access.clientId': String(clientId),\n 'oAuth2Access.userId': String(userId),\n },\n {\n $set: {\n 'oAuth2Access.$.clientId': projectAccess.clientId,\n 'oAuth2Access.$.clientSecret': clientSecret,\n },\n }\n );\n\n if (result.modifiedCount === 0) {\n throw new GenericError('ACCESS_KEY_UPDATE_FAILED', {\n clientId,\n projectId,\n });\n }\n\n const updatedProject = await getProjectById(projectId);\n\n const newAccessKeyId = updatedProject.oAuth2Access.find(\n (access) => access.clientId === projectAccess.clientId\n );\n\n if (!newAccessKeyId) {\n throw new GenericError('ACCESS_KEY_CREATION_FAILED', {\n accessKeyData: updatedProject.oAuth2Access,\n projectId,\n userId,\n });\n }\n\n return newAccessKeyId;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;AAwBA,MAAM,mCAAmC;CACvC,UAAU,YAAY,GAAG,CAAC,SAAS,MAAM;CACzC,cAAc,YAAY,GAAG,CAAC,SAAS,MAAM;CAC9C;;;;;;;;;;AAWD,MAAa,kBAAkB,OAC7B,eACA,WACA,SAC0B;CAC1B,MAAM,EAAE,UAAU,iBAAiB,2BAA2B;CAE9D,MAAM,eAAiC;EACrC,GAAG;EACH;EACA;EACA,QAAQ,KAAK;EACb,aAAa,EAAE;EACf,QAAQ,cAAc;EACtB,uBAAuB,cAAc,yBAAyB;EAC9D,gBAAgB,cAAc,kBAAkB;EACjD;AAOD,MAAI,MALiB,aAAa,UAChC,EAAE,KAAK,WAAW,EAClB,EAAE,OAAO,EAAE,cAAc,cAAc,EAAE,CAC1C,EAEU,kBAAkB,EAC3B,OAAM,IAAI,aAAa,8BAA8B;EACnD;EACA;EACA,QAAQ,KAAK;EACd,CAAC;CAKJ,MAAM,kBAAiB,MAFM,eAAe,UAAU,EAEhB,aAAa,MAChD,WAAW,OAAO,aAAa,SACjC;AAED,KAAI,CAAC,eACH,OAAM,IAAI,aAAa,8BAA8B;EACnD;EACA;EACA,QAAQ,KAAK;EACd,CAAC;AAGJ,QAAO;;AAGT,MAAa,kBAAkB,OAC7B,UACA,SACA,WACG;CACH,MAAM,gBAAgB,QAAQ,aAAa,MACxC,WACC,OAAO,aAAa,YAAY,OAAO,OAAO,OAAO,KAAK,OAAO,OAAO,CAC3E;AAED,KAAI,CAAC,cACH,OAAM,IAAI,aAAa,wBAAwB;EAC7C;EACA,WAAW,QAAQ;EACpB,CAAC;AAWJ,MAAI,MARiB,aAAa,UAChC;EACE,yBAAyB,OAAO,SAAS;EACzC,uBAAuB,OAAO,OAAO;EACtC,EACD,EAAE,OAAO,EAAE,cAAc,EAAE,UAAU,OAAO,SAAS,EAAE,EAAE,EAAE,CAC5D,EAEU,kBAAkB,EAC3B,OAAM,IAAI,aAAa,8BAA8B;EACnD;EACA,WAAW,QAAQ;EACpB,CAAC;AAGJ,QAAO;;AAGT,MAAa,mBAAmB,OAC9B,UACA,WACA,WAC0B;CAC1B,MAAM,UAAU,MAAM,aAAa,QAAQ;EACzC,KAAK;EACL,yBAAyB,OAAO,SAAS;EACzC,uBAAuB,OAAO,OAAO;EACtC,CAAC;AAEF,KAAI,CAAC,QACH,OAAM,IAAI,aAAa,qBAAqB;EAC1C;EACA;EACA;EACD,CAAC;CAGJ,MAAM,gBAAgB,QAAQ,aAAa,MACxC,WAAW,OAAO,aAAa,SACjC;AAED,KAAI,CAAC,cACH,OAAM,IAAI,aAAa,wBAAwB;EAC7C;EACA,WAAW,QAAQ;EACpB,CAAC;CAGJ,MAAM,EAAE,iBAAiB,2BAA2B;AAepD,MAAI,MAbiB,aAAa,UAChC;EACE,yBAAyB,OAAO,SAAS;EACzC,uBAAuB,OAAO,OAAO;EACtC,EACD,EACE,MAAM;EACJ,2BAA2B,cAAc;EACzC,+BAA+B;EAChC,EACF,CACF,EAEU,kBAAkB,EAC3B,OAAM,IAAI,aAAa,4BAA4B;EACjD;EACA;EACD,CAAC;CAGJ,MAAM,iBAAiB,MAAM,eAAe,UAAU;CAEtD,MAAM,iBAAiB,eAAe,aAAa,MAChD,WAAW,OAAO,aAAa,cAAc,SAC/C;AAED,KAAI,CAAC,eACH,OAAM,IAAI,aAAa,8BAA8B;EACnD,eAAe,eAAe;EAC9B;EACA;EACD,CAAC;AAGJ,QAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"promoCode.service.mjs","names":[],"sources":["../../../src/services/promoCode.service.ts"],"sourcesContent":["import { PromoCodeModel } from '@schemas/promoCode.schema';\nimport Stripe from 'stripe';\nimport type { PromoCodeDocument } from '@/types/promoCode.types';\n\nexport const createPromoCode = async (body: {\n code: string;\n discountType: 'percentage' | 'amount';\n discountValue: number;\n currency?: string;\n affiliateId?: string;\n maxRedemptions?: number;\n expiresAt?: Date;\n}): Promise<PromoCodeDocument> => {\n const {\n code,\n discountType,\n discountValue,\n currency,\n affiliateId,\n maxRedemptions,\n expiresAt,\n } = body;\n\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n\n const couponPayload: Stripe.CouponCreateParams = {\n name: code.toUpperCase(),\n ...(discountType === 'percentage'\n ? { percent_off: discountValue }\n : { amount_off: discountValue, currency: currency ?? 'usd' }),\n duration: 'once',\n };\n if (maxRedemptions) couponPayload.max_redemptions = maxRedemptions;\n const coupon = await stripe.coupons.create(couponPayload);\n\n const promoPayload: Stripe.PromotionCodeCreateParams = {\n promotion: {\n type: 'coupon',\n coupon: coupon.id,\n },\n code: code.toUpperCase(),\n };\n if (maxRedemptions) promoPayload.max_redemptions = maxRedemptions;\n if (expiresAt)\n promoPayload.expires_at = Math.floor(expiresAt.getTime() / 1000);\n const promotionCode = await stripe.promotionCodes.create(promoPayload);\n\n const promoCodeDoc = await PromoCodeModel.create({\n code: code.toUpperCase(),\n stripeCouponId: coupon.id,\n stripePromotionCodeId: promotionCode.id,\n affiliateId: affiliateId ?? undefined,\n discountType,\n discountValue,\n currency: discountType === 'amount' ? (currency ?? 'usd') : undefined,\n maxRedemptions,\n timesRedeemed: 0,\n active: true,\n expiresAt,\n });\n\n return promoCodeDoc;\n};\n\nexport const getPromoCodes = async (\n skip = 0,\n limit = 20,\n affiliateId?: string\n): Promise<PromoCodeDocument[]> => {\n const query = affiliateId ? { affiliateId } : {};\n return PromoCodeModel.find(query)\n .sort({ createdAt: -1 })\n .skip(skip)\n .limit(limit);\n};\n\nexport const countPromoCodes = async (\n affiliateId?: string\n): Promise<number> => {\n const query = affiliateId ? { affiliateId } : {};\n return PromoCodeModel.countDocuments(query);\n};\n\nexport const getPromoCodeById = async (\n id: string\n): Promise<PromoCodeDocument | null> => PromoCodeModel.findById(id);\n\nexport const getPromoCodeByCode = async (\n code: string\n): Promise<PromoCodeDocument | null> =>\n PromoCodeModel.findOne({ code: code.toUpperCase() });\n\nexport const updatePromoCode = async (\n id: string,\n update: {\n affiliateId?: string | null;\n active?: boolean;\n maxRedemptions?: number;\n expiresAt?: Date;\n }\n): Promise<PromoCodeDocument | null> => {\n const $set: Record<string, unknown> = {};\n const $unset: Record<string, unknown> = {};\n\n if (update.affiliateId === null) {\n $unset.affiliateId = '';\n } else if (update.affiliateId !== undefined) {\n $set.affiliateId = update.affiliateId;\n }\n if (update.active !== undefined) $set.active = update.active;\n if (update.maxRedemptions !== undefined)\n $set.maxRedemptions = update.maxRedemptions;\n if (update.expiresAt !== undefined) $set.expiresAt = update.expiresAt;\n\n const op: Record<string, unknown> = {};\n if (Object.keys($set).length > 0) op.$set = $set;\n if (Object.keys($unset).length > 0) op.$unset = $unset;\n\n return PromoCodeModel.findByIdAndUpdate(id, op, { new: true });\n};\n\nexport const deletePromoCode = async (\n id: string\n): Promise<PromoCodeDocument | null> => {\n const promoCode = await PromoCodeModel.findById(id);\n if (!promoCode) return null;\n\n if (promoCode.stripePromotionCodeId) {\n try {\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n await stripe.promotionCodes.update(promoCode.stripePromotionCodeId, {\n active: false,\n });\n } catch {\n // best-effort — continue even if Stripe call fails\n }\n }\n\n return PromoCodeModel.findByIdAndUpdate(id, { active: false }, { new: true });\n};\n"],"mappings":";;;;AAIA,MAAa,kBAAkB,OAAO,SAQJ;CAChC,MAAM,EACJ,MACA,cACA,eACA,UACA,aACA,gBACA,cACE;CAEJ,MAAM,SAAS,IAAI,OAAO,QAAQ,IAAI,iBAAkB;CAExD,MAAM,gBAA2C;EAC/C,MAAM,KAAK,YAAY;EACvB,GAAI,iBAAiB,eACjB,EAAE,aAAa,cAAc,IAC7B;GAAE,YAAY;GAAe,UAAU,YAAY;EAAM;EAC7D,UAAU;CACZ;CACA,IAAI,gBAAgB,cAAc,kBAAkB;CACpD,MAAM,SAAS,MAAM,OAAO,QAAQ,OAAO,aAAa;CAExD,MAAM,eAAiD;EACrD,WAAW;GACT,MAAM;GACN,QAAQ,OAAO;EACjB;EACA,MAAM,KAAK,YAAY;CACzB;CACA,IAAI,gBAAgB,aAAa,kBAAkB;CACnD,IAAI,WACF,aAAa,aAAa,KAAK,MAAM,UAAU,QAAQ,IAAI,GAAI;CACjE,MAAM,gBAAgB,MAAM,OAAO,eAAe,OAAO,YAAY;CAgBrE,OAAO,MAdoB,eAAe,OAAO;EAC/C,MAAM,KAAK,YAAY;EACvB,gBAAgB,OAAO;EACvB,uBAAuB,cAAc;EACrC,aAAa,eAAe;EAC5B;EACA;EACA,UAAU,iBAAiB,WAAY,YAAY,QAAS;EAC5D;EACA,eAAe;EACf,QAAQ;EACR;CACF,CAAC;AAGH;AAEA,MAAa,gBAAgB,OAC3B,OAAO,GACP,QAAQ,IACR,gBACiC;CACjC,MAAM,QAAQ,cAAc,EAAE,YAAY,IAAI,CAAC;CAC/C,OAAO,eAAe,KAAK,KAAK,CAAC,CAC9B,KAAK,EAAE,WAAW,GAAG,CAAC,CAAC,CACvB,KAAK,IAAI,CAAC,CACV,MAAM,KAAK;AAChB;AAEA,MAAa,kBAAkB,OAC7B,gBACoB;CACpB,MAAM,QAAQ,cAAc,EAAE,YAAY,IAAI,CAAC;CAC/C,OAAO,eAAe,eAAe,KAAK;AAC5C;AAEA,MAAa,mBAAmB,OAC9B,OACsC,eAAe,SAAS,EAAE;AAElE,MAAa,qBAAqB,OAChC,SAEA,eAAe,QAAQ,EAAE,MAAM,KAAK,YAAY,EAAE,CAAC;AAErD,MAAa,kBAAkB,OAC7B,IACA,WAMsC;CACtC,MAAM,OAAgC,CAAC;CACvC,MAAM,SAAkC,CAAC;CAEzC,IAAI,OAAO,gBAAgB,MACzB,OAAO,cAAc;MAChB,IAAI,OAAO,gBAAgB,QAChC,KAAK,cAAc,OAAO;CAE5B,IAAI,OAAO,WAAW,QAAW,KAAK,SAAS,OAAO;CACtD,IAAI,OAAO,mBAAmB,QAC5B,KAAK,iBAAiB,OAAO;CAC/B,IAAI,OAAO,cAAc,QAAW,KAAK,YAAY,OAAO;CAE5D,MAAM,KAA8B,CAAC;CACrC,IAAI,OAAO,KAAK,IAAI,CAAC,CAAC,SAAS,GAAG,GAAG,OAAO;CAC5C,IAAI,OAAO,KAAK,MAAM,CAAC,CAAC,SAAS,GAAG,GAAG,SAAS;CAEhD,OAAO,eAAe,kBAAkB,IAAI,IAAI,EAAE,KAAK,KAAK,CAAC;AAC/D;AAEA,MAAa,kBAAkB,OAC7B,OACsC;CACtC,MAAM,YAAY,MAAM,eAAe,SAAS,EAAE;CAClD,IAAI,CAAC,WAAW,OAAO;CAEvB,IAAI,UAAU,uBACZ,IAAI;EAEF,MAAM,IADa,OAAO,QAAQ,IAAI,iBAC3B,CAAC,CAAC,eAAe,OAAO,UAAU,uBAAuB,EAClE,QAAQ,MACV,CAAC;CACH,QAAQ,CAER;CAGF,OAAO,eAAe,kBAAkB,IAAI,EAAE,QAAQ,MAAM,GAAG,EAAE,KAAK,KAAK,CAAC;AAC9E"}
1
+ {"version":3,"file":"promoCode.service.mjs","names":[],"sources":["../../../src/services/promoCode.service.ts"],"sourcesContent":["import { PromoCodeModel } from '@schemas/promoCode.schema';\nimport Stripe from 'stripe';\nimport type { PromoCodeDocument } from '@/types/promoCode.types';\n\nexport const createPromoCode = async (body: {\n code: string;\n discountType: 'percentage' | 'amount';\n discountValue: number;\n currency?: string;\n affiliateId?: string;\n maxRedemptions?: number;\n expiresAt?: Date;\n}): Promise<PromoCodeDocument> => {\n const {\n code,\n discountType,\n discountValue,\n currency,\n affiliateId,\n maxRedemptions,\n expiresAt,\n } = body;\n\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n\n const couponPayload: Stripe.CouponCreateParams = {\n name: code.toUpperCase(),\n ...(discountType === 'percentage'\n ? { percent_off: discountValue }\n : { amount_off: discountValue, currency: currency ?? 'usd' }),\n duration: 'once',\n };\n if (maxRedemptions) couponPayload.max_redemptions = maxRedemptions;\n const coupon = await stripe.coupons.create(couponPayload);\n\n const promoPayload: Stripe.PromotionCodeCreateParams = {\n promotion: {\n type: 'coupon',\n coupon: coupon.id,\n },\n code: code.toUpperCase(),\n };\n if (maxRedemptions) promoPayload.max_redemptions = maxRedemptions;\n if (expiresAt)\n promoPayload.expires_at = Math.floor(expiresAt.getTime() / 1000);\n const promotionCode = await stripe.promotionCodes.create(promoPayload);\n\n const promoCodeDoc = await PromoCodeModel.create({\n code: code.toUpperCase(),\n stripeCouponId: coupon.id,\n stripePromotionCodeId: promotionCode.id,\n affiliateId: affiliateId ?? undefined,\n discountType,\n discountValue,\n currency: discountType === 'amount' ? (currency ?? 'usd') : undefined,\n maxRedemptions,\n timesRedeemed: 0,\n active: true,\n expiresAt,\n });\n\n return promoCodeDoc;\n};\n\nexport const getPromoCodes = async (\n skip = 0,\n limit = 20,\n affiliateId?: string\n): Promise<PromoCodeDocument[]> => {\n const query = affiliateId ? { affiliateId } : {};\n return PromoCodeModel.find(query)\n .sort({ createdAt: -1 })\n .skip(skip)\n .limit(limit);\n};\n\nexport const countPromoCodes = async (\n affiliateId?: string\n): Promise<number> => {\n const query = affiliateId ? { affiliateId } : {};\n return PromoCodeModel.countDocuments(query);\n};\n\nexport const getPromoCodeById = async (\n id: string\n): Promise<PromoCodeDocument | null> => PromoCodeModel.findById(id);\n\nexport const getPromoCodeByCode = async (\n code: string\n): Promise<PromoCodeDocument | null> =>\n PromoCodeModel.findOne({ code: code.toUpperCase() });\n\nexport const updatePromoCode = async (\n id: string,\n update: {\n affiliateId?: string | null;\n active?: boolean;\n maxRedemptions?: number;\n expiresAt?: Date;\n }\n): Promise<PromoCodeDocument | null> => {\n const $set: Record<string, unknown> = {};\n const $unset: Record<string, unknown> = {};\n\n if (update.affiliateId === null) {\n $unset.affiliateId = '';\n } else if (update.affiliateId !== undefined) {\n $set.affiliateId = update.affiliateId;\n }\n if (update.active !== undefined) $set.active = update.active;\n if (update.maxRedemptions !== undefined)\n $set.maxRedemptions = update.maxRedemptions;\n if (update.expiresAt !== undefined) $set.expiresAt = update.expiresAt;\n\n const op: Record<string, unknown> = {};\n if (Object.keys($set).length > 0) op.$set = $set;\n if (Object.keys($unset).length > 0) op.$unset = $unset;\n\n return PromoCodeModel.findByIdAndUpdate(id, op, { new: true });\n};\n\nexport const deletePromoCode = async (\n id: string\n): Promise<PromoCodeDocument | null> => {\n const promoCode = await PromoCodeModel.findById(id);\n if (!promoCode) return null;\n\n if (promoCode.stripePromotionCodeId) {\n try {\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n await stripe.promotionCodes.update(promoCode.stripePromotionCodeId, {\n active: false,\n });\n } catch {\n // best-effort — continue even if Stripe call fails\n }\n }\n\n return PromoCodeModel.findByIdAndUpdate(id, { active: false }, { new: true });\n};\n"],"mappings":";;;;AAIA,MAAa,kBAAkB,OAAO,SAQJ;CAChC,MAAM,EACJ,MACA,cACA,eACA,UACA,aACA,gBACA,cACE;CAEJ,MAAM,SAAS,IAAI,OAAO,QAAQ,IAAI,kBAAmB;CAEzD,MAAM,gBAA2C;EAC/C,MAAM,KAAK,aAAa;EACxB,GAAI,iBAAiB,eACjB,EAAE,aAAa,eAAe,GAC9B;GAAE,YAAY;GAAe,UAAU,YAAY;GAAO;EAC9D,UAAU;EACX;AACD,KAAI,eAAgB,eAAc,kBAAkB;CACpD,MAAM,SAAS,MAAM,OAAO,QAAQ,OAAO,cAAc;CAEzD,MAAM,eAAiD;EACrD,WAAW;GACT,MAAM;GACN,QAAQ,OAAO;GAChB;EACD,MAAM,KAAK,aAAa;EACzB;AACD,KAAI,eAAgB,cAAa,kBAAkB;AACnD,KAAI,UACF,cAAa,aAAa,KAAK,MAAM,UAAU,SAAS,GAAG,IAAK;CAClE,MAAM,gBAAgB,MAAM,OAAO,eAAe,OAAO,aAAa;AAgBtE,QAAO,MAdoB,eAAe,OAAO;EAC/C,MAAM,KAAK,aAAa;EACxB,gBAAgB,OAAO;EACvB,uBAAuB,cAAc;EACrC,aAAa,eAAe;EAC5B;EACA;EACA,UAAU,iBAAiB,WAAY,YAAY,QAAS;EAC5D;EACA,eAAe;EACf,QAAQ;EACR;EACD,CAAC;;AAKJ,MAAa,gBAAgB,OAC3B,OAAO,GACP,QAAQ,IACR,gBACiC;CACjC,MAAM,QAAQ,cAAc,EAAE,aAAa,GAAG,EAAE;AAChD,QAAO,eAAe,KAAK,MAAM,CAC9B,KAAK,EAAE,WAAW,IAAI,CAAC,CACvB,KAAK,KAAK,CACV,MAAM,MAAM;;AAGjB,MAAa,kBAAkB,OAC7B,gBACoB;CACpB,MAAM,QAAQ,cAAc,EAAE,aAAa,GAAG,EAAE;AAChD,QAAO,eAAe,eAAe,MAAM;;AAG7C,MAAa,mBAAmB,OAC9B,OACsC,eAAe,SAAS,GAAG;AAEnE,MAAa,qBAAqB,OAChC,SAEA,eAAe,QAAQ,EAAE,MAAM,KAAK,aAAa,EAAE,CAAC;AAEtD,MAAa,kBAAkB,OAC7B,IACA,WAMsC;CACtC,MAAM,OAAgC,EAAE;CACxC,MAAM,SAAkC,EAAE;AAE1C,KAAI,OAAO,gBAAgB,KACzB,QAAO,cAAc;UACZ,OAAO,gBAAgB,OAChC,MAAK,cAAc,OAAO;AAE5B,KAAI,OAAO,WAAW,OAAW,MAAK,SAAS,OAAO;AACtD,KAAI,OAAO,mBAAmB,OAC5B,MAAK,iBAAiB,OAAO;AAC/B,KAAI,OAAO,cAAc,OAAW,MAAK,YAAY,OAAO;CAE5D,MAAM,KAA8B,EAAE;AACtC,KAAI,OAAO,KAAK,KAAK,CAAC,SAAS,EAAG,IAAG,OAAO;AAC5C,KAAI,OAAO,KAAK,OAAO,CAAC,SAAS,EAAG,IAAG,SAAS;AAEhD,QAAO,eAAe,kBAAkB,IAAI,IAAI,EAAE,KAAK,MAAM,CAAC;;AAGhE,MAAa,kBAAkB,OAC7B,OACsC;CACtC,MAAM,YAAY,MAAM,eAAe,SAAS,GAAG;AACnD,KAAI,CAAC,UAAW,QAAO;AAEvB,KAAI,UAAU,sBACZ,KAAI;AAEF,QAAM,IADa,OAAO,QAAQ,IAAI,kBAC1B,CAAC,eAAe,OAAO,UAAU,uBAAuB,EAClE,QAAQ,OACT,CAAC;SACI;AAKV,QAAO,eAAe,kBAAkB,IAAI,EAAE,QAAQ,OAAO,EAAE,EAAE,KAAK,MAAM,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"pictureUpload.service.mjs","names":[],"sources":["../../../../src/services/reviewer/pictureUpload.service.ts"],"sourcesContent":["import { DeleteObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';\nimport { resizeImage } from '@utils/image/resizeImage';\nimport { getS3Client } from '@utils/s3/s3Client';\n\nexport type ReviewerPictureKind = 'main' | 'cover';\n\nconst ALLOWED_MIME_TYPES = [\n 'image/jpeg',\n 'image/png',\n 'image/webp',\n 'image/gif',\n];\nconst MAX_SIZE_BYTES = 20 * 1024 * 1024; // 20 MB pre-resize\n\nconst RESIZE_CONFIG: Record<\n ReviewerPictureKind,\n { width: number; height: number; quality: number }\n> = {\n main: { width: 1280, height: 720, quality: 82 },\n cover: { width: 1500, height: 500, quality: 80 },\n};\n\nexport type PictureValidationError = 'UNSUPPORTED_TYPE' | 'TOO_LARGE';\n\nexport const validateReviewerPictureUpload = (\n contentType: string,\n contentLength: number\n): PictureValidationError | null => {\n if (!ALLOWED_MIME_TYPES.includes(contentType)) return 'UNSUPPORTED_TYPE';\n if (contentLength > MAX_SIZE_BYTES) return 'TOO_LARGE';\n return null;\n};\n\nconst getPictureKey = (reviewerId: string, kind: ReviewerPictureKind): string =>\n `reviewer-pictures/${reviewerId}-${kind}.jpg`;\n\nexport const uploadReviewerPicture = async (\n buffer: Buffer,\n reviewerId: string,\n kind: ReviewerPictureKind\n): Promise<string> => {\n const cfg = RESIZE_CONFIG[kind];\n const { buffer: resized, contentType } = await resizeImage(buffer, cfg);\n\n const key = getPictureKey(reviewerId, kind);\n const s3Client = getS3Client();\n\n await s3Client.send(\n new PutObjectCommand({\n Bucket: process.env.S3_BUCKET_NAME,\n Key: key,\n Body: resized,\n ContentType: contentType,\n })\n );\n\n return `${process.env.S3_PUBLIC_URL}/${key}`;\n};\n\nexport const deleteReviewerPicture = async (\n imageUrl: string\n): Promise<void> => {\n const publicUrl = process.env.S3_PUBLIC_URL ?? '';\n const key = imageUrl.startsWith(publicUrl)\n ? imageUrl.slice(publicUrl.length + 1)\n : null;\n\n if (!key?.startsWith('reviewer-pictures/')) return;\n\n const s3Client = getS3Client();\n await s3Client.send(\n new DeleteObjectCommand({\n Bucket: process.env.S3_BUCKET_NAME,\n Key: key,\n })\n );\n};\n"],"mappings":";;;;;AAMA,MAAM,qBAAqB;CACzB;CACA;CACA;CACA;AACF;AACA,MAAM,iBAAiB,KAAK,OAAO;AAEnC,MAAM,gBAGF;CACF,MAAM;EAAE,OAAO;EAAM,QAAQ;EAAK,SAAS;CAAG;CAC9C,OAAO;EAAE,OAAO;EAAM,QAAQ;EAAK,SAAS;CAAG;AACjD;AAIA,MAAa,iCACX,aACA,kBACkC;CAClC,IAAI,CAAC,mBAAmB,SAAS,WAAW,GAAG,OAAO;CACtD,IAAI,gBAAgB,gBAAgB,OAAO;CAC3C,OAAO;AACT;AAEA,MAAM,iBAAiB,YAAoB,SACzC,qBAAqB,WAAW,GAAG,KAAK;AAE1C,MAAa,wBAAwB,OACnC,QACA,YACA,SACoB;CACpB,MAAM,MAAM,cAAc;CAC1B,MAAM,EAAE,QAAQ,SAAS,gBAAgB,MAAM,YAAY,QAAQ,GAAG;CAEtE,MAAM,MAAM,cAAc,YAAY,IAAI;CAG1C,MAFiB,YAEJ,CAAC,CAAC,KACb,IAAI,iBAAiB;EACnB,QAAQ,QAAQ,IAAI;EACpB,KAAK;EACL,MAAM;EACN,aAAa;CACf,CAAC,CACH;CAEA,OAAO,GAAG,QAAQ,IAAI,cAAc,GAAG;AACzC;AAEA,MAAa,wBAAwB,OACnC,aACkB;CAClB,MAAM,YAAY,QAAQ,IAAI,iBAAiB;CAC/C,MAAM,MAAM,SAAS,WAAW,SAAS,IACrC,SAAS,MAAM,UAAU,SAAS,CAAC,IACnC;CAEJ,IAAI,CAAC,KAAK,WAAW,oBAAoB,GAAG;CAG5C,MADiB,YACJ,CAAC,CAAC,KACb,IAAI,oBAAoB;EACtB,QAAQ,QAAQ,IAAI;EACpB,KAAK;CACP,CAAC,CACH;AACF"}
1
+ {"version":3,"file":"pictureUpload.service.mjs","names":[],"sources":["../../../../src/services/reviewer/pictureUpload.service.ts"],"sourcesContent":["import { DeleteObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';\nimport { resizeImage } from '@utils/image/resizeImage';\nimport { getS3Client } from '@utils/s3/s3Client';\n\nexport type ReviewerPictureKind = 'main' | 'cover';\n\nconst ALLOWED_MIME_TYPES = [\n 'image/jpeg',\n 'image/png',\n 'image/webp',\n 'image/gif',\n];\nconst MAX_SIZE_BYTES = 20 * 1024 * 1024; // 20 MB pre-resize\n\nconst RESIZE_CONFIG: Record<\n ReviewerPictureKind,\n { width: number; height: number; quality: number }\n> = {\n main: { width: 1280, height: 720, quality: 82 },\n cover: { width: 1500, height: 500, quality: 80 },\n};\n\nexport type PictureValidationError = 'UNSUPPORTED_TYPE' | 'TOO_LARGE';\n\nexport const validateReviewerPictureUpload = (\n contentType: string,\n contentLength: number\n): PictureValidationError | null => {\n if (!ALLOWED_MIME_TYPES.includes(contentType)) return 'UNSUPPORTED_TYPE';\n if (contentLength > MAX_SIZE_BYTES) return 'TOO_LARGE';\n return null;\n};\n\nconst getPictureKey = (reviewerId: string, kind: ReviewerPictureKind): string =>\n `reviewer-pictures/${reviewerId}-${kind}.jpg`;\n\nexport const uploadReviewerPicture = async (\n buffer: Buffer,\n reviewerId: string,\n kind: ReviewerPictureKind\n): Promise<string> => {\n const cfg = RESIZE_CONFIG[kind];\n const { buffer: resized, contentType } = await resizeImage(buffer, cfg);\n\n const key = getPictureKey(reviewerId, kind);\n const s3Client = getS3Client();\n\n await s3Client.send(\n new PutObjectCommand({\n Bucket: process.env.S3_BUCKET_NAME,\n Key: key,\n Body: resized,\n ContentType: contentType,\n })\n );\n\n return `${process.env.S3_PUBLIC_URL}/${key}`;\n};\n\nexport const deleteReviewerPicture = async (\n imageUrl: string\n): Promise<void> => {\n const publicUrl = process.env.S3_PUBLIC_URL ?? '';\n const key = imageUrl.startsWith(publicUrl)\n ? imageUrl.slice(publicUrl.length + 1)\n : null;\n\n if (!key?.startsWith('reviewer-pictures/')) return;\n\n const s3Client = getS3Client();\n await s3Client.send(\n new DeleteObjectCommand({\n Bucket: process.env.S3_BUCKET_NAME,\n Key: key,\n })\n );\n};\n"],"mappings":";;;;;AAMA,MAAM,qBAAqB;CACzB;CACA;CACA;CACA;CACD;AACD,MAAM,iBAAiB,KAAK,OAAO;AAEnC,MAAM,gBAGF;CACF,MAAM;EAAE,OAAO;EAAM,QAAQ;EAAK,SAAS;EAAI;CAC/C,OAAO;EAAE,OAAO;EAAM,QAAQ;EAAK,SAAS;EAAI;CACjD;AAID,MAAa,iCACX,aACA,kBACkC;AAClC,KAAI,CAAC,mBAAmB,SAAS,YAAY,CAAE,QAAO;AACtD,KAAI,gBAAgB,eAAgB,QAAO;AAC3C,QAAO;;AAGT,MAAM,iBAAiB,YAAoB,SACzC,qBAAqB,WAAW,GAAG,KAAK;AAE1C,MAAa,wBAAwB,OACnC,QACA,YACA,SACoB;CACpB,MAAM,MAAM,cAAc;CAC1B,MAAM,EAAE,QAAQ,SAAS,gBAAgB,MAAM,YAAY,QAAQ,IAAI;CAEvE,MAAM,MAAM,cAAc,YAAY,KAAK;AAG3C,OAFiB,aAEH,CAAC,KACb,IAAI,iBAAiB;EACnB,QAAQ,QAAQ,IAAI;EACpB,KAAK;EACL,MAAM;EACN,aAAa;EACd,CAAC,CACH;AAED,QAAO,GAAG,QAAQ,IAAI,cAAc,GAAG;;AAGzC,MAAa,wBAAwB,OACnC,aACkB;CAClB,MAAM,YAAY,QAAQ,IAAI,iBAAiB;CAC/C,MAAM,MAAM,SAAS,WAAW,UAAU,GACtC,SAAS,MAAM,UAAU,SAAS,EAAE,GACpC;AAEJ,KAAI,CAAC,KAAK,WAAW,qBAAqB,CAAE;AAG5C,OADiB,aACH,CAAC,KACb,IAAI,oBAAoB;EACtB,QAAQ,QAAQ,IAAI;EACpB,KAAK;EACN,CAAC,CACH"}
@@ -1 +1 @@
1
- {"version":3,"file":"reviewer.service.mjs","names":[],"sources":["../../../src/services/reviewer.service.ts"],"sourcesContent":["import {\n ReviewerProfileModel,\n ReviewerReviewModel,\n} from '@schemas/reviewer.schema';\nimport type {\n ReviewerProfile,\n ReviewerProfileData,\n ReviewerProfileDocument,\n ReviewerReviewDocument,\n} from '@/types/reviewer.types';\n\nexport const findReviewerProfiles = async (\n query: Record<string, any> = {},\n skip = 0,\n limit = 20\n): Promise<ReviewerProfileDocument[]> =>\n ReviewerProfileModel.find({\n status: 'active',\n isHidden: { $ne: true },\n ...query,\n })\n .sort({ averageRating: -1, totalMissions: -1 })\n .skip(skip)\n .limit(limit) as unknown as ReviewerProfileDocument[];\n\nexport const countReviewerProfiles = async (\n query: Record<string, any> = {}\n): Promise<number> =>\n ReviewerProfileModel.countDocuments({\n status: 'active',\n isHidden: { $ne: true },\n ...query,\n });\n\nexport const findReviewerProfilesAdmin = async (\n query: Record<string, any> = {},\n skip = 0,\n limit = 20\n): Promise<ReviewerProfileDocument[]> =>\n ReviewerProfileModel.find(query)\n .sort({ createdAt: -1 })\n .skip(skip)\n .limit(limit) as unknown as ReviewerProfileDocument[];\n\nexport const countReviewerProfilesAdmin = async (\n query: Record<string, any> = {}\n): Promise<number> => ReviewerProfileModel.countDocuments(query);\n\nexport const findReviewerById = async (\n id: string\n): Promise<ReviewerProfileDocument | null> =>\n ReviewerProfileModel.findById(\n id\n ) as unknown as ReviewerProfileDocument | null;\n\nexport const findReviewerByUserId = async (\n userId: string\n): Promise<ReviewerProfileDocument | null> =>\n ReviewerProfileModel.findOne({\n userId,\n }) as unknown as ReviewerProfileDocument | null;\n\nexport const createReviewerProfile = async (\n data: Omit<\n ReviewerProfileData,\n 'totalMissions' | 'averageRating' | 'reviewCount' | 'status'\n >\n): Promise<ReviewerProfileDocument> =>\n ReviewerProfileModel.create(data) as unknown as ReviewerProfileDocument;\n\nexport const updateReviewerProfile = async (\n id: string,\n data: Partial<ReviewerProfileData>\n): Promise<ReviewerProfileDocument | null> =>\n ReviewerProfileModel.findByIdAndUpdate(id, data, {\n new: true,\n }) as unknown as ReviewerProfileDocument | null;\n\nexport const incrementMissionCount = async (id: string): Promise<void> => {\n await ReviewerProfileModel.findByIdAndUpdate(id, {\n $inc: { totalMissions: 1 },\n });\n};\n\nexport const updateRating = async (\n reviewerId: string,\n newRating: number\n): Promise<void> => {\n const profile = await ReviewerProfileModel.findById(reviewerId);\n if (!profile) return;\n\n const currentTotal = profile.averageRating * profile.reviewCount;\n const newCount = profile.reviewCount + 1;\n const newAverage = (currentTotal + newRating) / newCount;\n\n await ReviewerProfileModel.findByIdAndUpdate(reviewerId, {\n averageRating: Math.round(newAverage * 10) / 10,\n reviewCount: newCount,\n });\n};\n\nexport const findReviewsByReviewerId = async (\n reviewerId: string,\n skip = 0,\n limit = 20\n): Promise<ReviewerReviewDocument[]> =>\n ReviewerReviewModel.find({ reviewerId })\n .sort({ createdAt: -1 })\n .skip(skip)\n .limit(limit) as unknown as ReviewerReviewDocument[];\n\nexport const countReviewsByReviewerId = async (\n reviewerId: string\n): Promise<number> => ReviewerReviewModel.countDocuments({ reviewerId });\n\nexport const findReviewByMissionId = async (\n missionId: string\n): Promise<ReviewerReviewDocument | null> =>\n ReviewerReviewModel.findOne({\n missionId,\n }) as unknown as ReviewerReviewDocument | null;\n\nexport const createReview = async (data: {\n missionId: ReviewerProfile['id'];\n reviewerId: ReviewerProfile['id'];\n rating: number;\n comment?: string;\n}): Promise<ReviewerReviewDocument> =>\n ReviewerReviewModel.create(data) as unknown as ReviewerReviewDocument;\n\nexport const deleteReviewerProfile = async (id: string): Promise<void> => {\n await ReviewerProfileModel.findByIdAndDelete(id);\n};\n\nexport const getReviewerPriceDistribution = async (\n query: Record<string, any> = {},\n bucketCount = 20\n): Promise<Array<{ min: number; max: number; count: number }>> => {\n const result = await ReviewerProfileModel.aggregate([\n { $match: { status: 'active', ...query } },\n {\n $bucketAuto: {\n groupBy: '$pricePerHour',\n buckets: bucketCount,\n output: { count: { $sum: 1 } },\n },\n },\n ]);\n return result.map((r: any) => ({\n min: r._id.min as number,\n max: r._id.max as number,\n count: r.count as number,\n }));\n};\n"],"mappings":";;;AAWA,MAAa,uBAAuB,OAClC,QAA6B,CAAC,GAC9B,OAAO,GACP,QAAQ,OAER,qBAAqB,KAAK;CACxB,QAAQ;CACR,UAAU,EAAE,KAAK,KAAK;CACtB,GAAG;AACL,CAAC,CAAC,CACC,KAAK;CAAE,eAAe;CAAI,eAAe;AAAG,CAAC,CAAC,CAC9C,KAAK,IAAI,CAAC,CACV,MAAM,KAAK;AAEhB,MAAa,wBAAwB,OACnC,QAA6B,CAAC,MAE9B,qBAAqB,eAAe;CAClC,QAAQ;CACR,UAAU,EAAE,KAAK,KAAK;CACtB,GAAG;AACL,CAAC;AAEH,MAAa,4BAA4B,OACvC,QAA6B,CAAC,GAC9B,OAAO,GACP,QAAQ,OAER,qBAAqB,KAAK,KAAK,CAAC,CAC7B,KAAK,EAAE,WAAW,GAAG,CAAC,CAAC,CACvB,KAAK,IAAI,CAAC,CACV,MAAM,KAAK;AAEhB,MAAa,6BAA6B,OACxC,QAA6B,CAAC,MACV,qBAAqB,eAAe,KAAK;AAE/D,MAAa,mBAAmB,OAC9B,OAEA,qBAAqB,SACnB,EACF;AAEF,MAAa,uBAAuB,OAClC,WAEA,qBAAqB,QAAQ,EAC3B,OACF,CAAC;AAEH,MAAa,wBAAwB,OACnC,SAKA,qBAAqB,OAAO,IAAI;AAElC,MAAa,wBAAwB,OACnC,IACA,SAEA,qBAAqB,kBAAkB,IAAI,MAAM,EAC/C,KAAK,KACP,CAAC;AAEH,MAAa,wBAAwB,OAAO,OAA8B;CACxE,MAAM,qBAAqB,kBAAkB,IAAI,EAC/C,MAAM,EAAE,eAAe,EAAE,EAC3B,CAAC;AACH;AAEA,MAAa,eAAe,OAC1B,YACA,cACkB;CAClB,MAAM,UAAU,MAAM,qBAAqB,SAAS,UAAU;CAC9D,IAAI,CAAC,SAAS;CAEd,MAAM,eAAe,QAAQ,gBAAgB,QAAQ;CACrD,MAAM,WAAW,QAAQ,cAAc;CACvC,MAAM,cAAc,eAAe,aAAa;CAEhD,MAAM,qBAAqB,kBAAkB,YAAY;EACvD,eAAe,KAAK,MAAM,aAAa,EAAE,IAAI;EAC7C,aAAa;CACf,CAAC;AACH;AAEA,MAAa,0BAA0B,OACrC,YACA,OAAO,GACP,QAAQ,OAER,oBAAoB,KAAK,EAAE,WAAW,CAAC,CAAC,CACrC,KAAK,EAAE,WAAW,GAAG,CAAC,CAAC,CACvB,KAAK,IAAI,CAAC,CACV,MAAM,KAAK;AAEhB,MAAa,2BAA2B,OACtC,eACoB,oBAAoB,eAAe,EAAE,WAAW,CAAC;AAEvE,MAAa,wBAAwB,OACnC,cAEA,oBAAoB,QAAQ,EAC1B,UACF,CAAC;AAEH,MAAa,eAAe,OAAO,SAMjC,oBAAoB,OAAO,IAAI;AAEjC,MAAa,wBAAwB,OAAO,OAA8B;CACxE,MAAM,qBAAqB,kBAAkB,EAAE;AACjD;AAEA,MAAa,+BAA+B,OAC1C,QAA6B,CAAC,GAC9B,cAAc,OACkD;CAWhE,QAAO,MAVc,qBAAqB,UAAU,CAClD,EAAE,QAAQ;EAAE,QAAQ;EAAU,GAAG;CAAM,EAAE,GACzC,EACE,aAAa;EACX,SAAS;EACT,SAAS;EACT,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE;CAC/B,EACF,CACF,CAAC,EACY,CAAC,KAAK,OAAY;EAC7B,KAAK,EAAE,IAAI;EACX,KAAK,EAAE,IAAI;EACX,OAAO,EAAE;CACX,EAAE;AACJ"}
1
+ {"version":3,"file":"reviewer.service.mjs","names":[],"sources":["../../../src/services/reviewer.service.ts"],"sourcesContent":["import {\n ReviewerProfileModel,\n ReviewerReviewModel,\n} from '@schemas/reviewer.schema';\nimport type {\n ReviewerProfile,\n ReviewerProfileData,\n ReviewerProfileDocument,\n ReviewerReviewDocument,\n} from '@/types/reviewer.types';\n\nexport const findReviewerProfiles = async (\n query: Record<string, any> = {},\n skip = 0,\n limit = 20\n): Promise<ReviewerProfileDocument[]> =>\n ReviewerProfileModel.find({\n status: 'active',\n isHidden: { $ne: true },\n ...query,\n })\n .sort({ averageRating: -1, totalMissions: -1 })\n .skip(skip)\n .limit(limit) as unknown as ReviewerProfileDocument[];\n\nexport const countReviewerProfiles = async (\n query: Record<string, any> = {}\n): Promise<number> =>\n ReviewerProfileModel.countDocuments({\n status: 'active',\n isHidden: { $ne: true },\n ...query,\n });\n\nexport const findReviewerProfilesAdmin = async (\n query: Record<string, any> = {},\n skip = 0,\n limit = 20\n): Promise<ReviewerProfileDocument[]> =>\n ReviewerProfileModel.find(query)\n .sort({ createdAt: -1 })\n .skip(skip)\n .limit(limit) as unknown as ReviewerProfileDocument[];\n\nexport const countReviewerProfilesAdmin = async (\n query: Record<string, any> = {}\n): Promise<number> => ReviewerProfileModel.countDocuments(query);\n\nexport const findReviewerById = async (\n id: string\n): Promise<ReviewerProfileDocument | null> =>\n ReviewerProfileModel.findById(\n id\n ) as unknown as ReviewerProfileDocument | null;\n\nexport const findReviewerByUserId = async (\n userId: string\n): Promise<ReviewerProfileDocument | null> =>\n ReviewerProfileModel.findOne({\n userId,\n }) as unknown as ReviewerProfileDocument | null;\n\nexport const createReviewerProfile = async (\n data: Omit<\n ReviewerProfileData,\n 'totalMissions' | 'averageRating' | 'reviewCount' | 'status'\n >\n): Promise<ReviewerProfileDocument> =>\n ReviewerProfileModel.create(data) as unknown as ReviewerProfileDocument;\n\nexport const updateReviewerProfile = async (\n id: string,\n data: Partial<ReviewerProfileData>\n): Promise<ReviewerProfileDocument | null> =>\n ReviewerProfileModel.findByIdAndUpdate(id, data, {\n new: true,\n }) as unknown as ReviewerProfileDocument | null;\n\nexport const incrementMissionCount = async (id: string): Promise<void> => {\n await ReviewerProfileModel.findByIdAndUpdate(id, {\n $inc: { totalMissions: 1 },\n });\n};\n\nexport const updateRating = async (\n reviewerId: string,\n newRating: number\n): Promise<void> => {\n const profile = await ReviewerProfileModel.findById(reviewerId);\n if (!profile) return;\n\n const currentTotal = profile.averageRating * profile.reviewCount;\n const newCount = profile.reviewCount + 1;\n const newAverage = (currentTotal + newRating) / newCount;\n\n await ReviewerProfileModel.findByIdAndUpdate(reviewerId, {\n averageRating: Math.round(newAverage * 10) / 10,\n reviewCount: newCount,\n });\n};\n\nexport const findReviewsByReviewerId = async (\n reviewerId: string,\n skip = 0,\n limit = 20\n): Promise<ReviewerReviewDocument[]> =>\n ReviewerReviewModel.find({ reviewerId })\n .sort({ createdAt: -1 })\n .skip(skip)\n .limit(limit) as unknown as ReviewerReviewDocument[];\n\nexport const countReviewsByReviewerId = async (\n reviewerId: string\n): Promise<number> => ReviewerReviewModel.countDocuments({ reviewerId });\n\nexport const findReviewByMissionId = async (\n missionId: string\n): Promise<ReviewerReviewDocument | null> =>\n ReviewerReviewModel.findOne({\n missionId,\n }) as unknown as ReviewerReviewDocument | null;\n\nexport const createReview = async (data: {\n missionId: ReviewerProfile['id'];\n reviewerId: ReviewerProfile['id'];\n rating: number;\n comment?: string;\n}): Promise<ReviewerReviewDocument> =>\n ReviewerReviewModel.create(data) as unknown as ReviewerReviewDocument;\n\nexport const deleteReviewerProfile = async (id: string): Promise<void> => {\n await ReviewerProfileModel.findByIdAndDelete(id);\n};\n\nexport const getReviewerPriceDistribution = async (\n query: Record<string, any> = {},\n bucketCount = 20\n): Promise<Array<{ min: number; max: number; count: number }>> => {\n const result = await ReviewerProfileModel.aggregate([\n { $match: { status: 'active', ...query } },\n {\n $bucketAuto: {\n groupBy: '$pricePerHour',\n buckets: bucketCount,\n output: { count: { $sum: 1 } },\n },\n },\n ]);\n return result.map((r: any) => ({\n min: r._id.min as number,\n max: r._id.max as number,\n count: r.count as number,\n }));\n};\n"],"mappings":";;;AAWA,MAAa,uBAAuB,OAClC,QAA6B,EAAE,EAC/B,OAAO,GACP,QAAQ,OAER,qBAAqB,KAAK;CACxB,QAAQ;CACR,UAAU,EAAE,KAAK,MAAM;CACvB,GAAG;CACJ,CAAC,CACC,KAAK;CAAE,eAAe;CAAI,eAAe;CAAI,CAAC,CAC9C,KAAK,KAAK,CACV,MAAM,MAAM;AAEjB,MAAa,wBAAwB,OACnC,QAA6B,EAAE,KAE/B,qBAAqB,eAAe;CAClC,QAAQ;CACR,UAAU,EAAE,KAAK,MAAM;CACvB,GAAG;CACJ,CAAC;AAEJ,MAAa,4BAA4B,OACvC,QAA6B,EAAE,EAC/B,OAAO,GACP,QAAQ,OAER,qBAAqB,KAAK,MAAM,CAC7B,KAAK,EAAE,WAAW,IAAI,CAAC,CACvB,KAAK,KAAK,CACV,MAAM,MAAM;AAEjB,MAAa,6BAA6B,OACxC,QAA6B,EAAE,KACX,qBAAqB,eAAe,MAAM;AAEhE,MAAa,mBAAmB,OAC9B,OAEA,qBAAqB,SACnB,GACD;AAEH,MAAa,uBAAuB,OAClC,WAEA,qBAAqB,QAAQ,EAC3B,QACD,CAAC;AAEJ,MAAa,wBAAwB,OACnC,SAKA,qBAAqB,OAAO,KAAK;AAEnC,MAAa,wBAAwB,OACnC,IACA,SAEA,qBAAqB,kBAAkB,IAAI,MAAM,EAC/C,KAAK,MACN,CAAC;AAEJ,MAAa,wBAAwB,OAAO,OAA8B;AACxE,OAAM,qBAAqB,kBAAkB,IAAI,EAC/C,MAAM,EAAE,eAAe,GAAG,EAC3B,CAAC;;AAGJ,MAAa,eAAe,OAC1B,YACA,cACkB;CAClB,MAAM,UAAU,MAAM,qBAAqB,SAAS,WAAW;AAC/D,KAAI,CAAC,QAAS;CAEd,MAAM,eAAe,QAAQ,gBAAgB,QAAQ;CACrD,MAAM,WAAW,QAAQ,cAAc;CACvC,MAAM,cAAc,eAAe,aAAa;AAEhD,OAAM,qBAAqB,kBAAkB,YAAY;EACvD,eAAe,KAAK,MAAM,aAAa,GAAG,GAAG;EAC7C,aAAa;EACd,CAAC;;AAGJ,MAAa,0BAA0B,OACrC,YACA,OAAO,GACP,QAAQ,OAER,oBAAoB,KAAK,EAAE,YAAY,CAAC,CACrC,KAAK,EAAE,WAAW,IAAI,CAAC,CACvB,KAAK,KAAK,CACV,MAAM,MAAM;AAEjB,MAAa,2BAA2B,OACtC,eACoB,oBAAoB,eAAe,EAAE,YAAY,CAAC;AAExE,MAAa,wBAAwB,OACnC,cAEA,oBAAoB,QAAQ,EAC1B,WACD,CAAC;AAEJ,MAAa,eAAe,OAAO,SAMjC,oBAAoB,OAAO,KAAK;AAElC,MAAa,wBAAwB,OAAO,OAA8B;AACxE,OAAM,qBAAqB,kBAAkB,GAAG;;AAGlD,MAAa,+BAA+B,OAC1C,QAA6B,EAAE,EAC/B,cAAc,OACkD;AAWhE,SAAO,MAVc,qBAAqB,UAAU,CAClD,EAAE,QAAQ;EAAE,QAAQ;EAAU,GAAG;EAAO,EAAE,EAC1C,EACE,aAAa;EACX,SAAS;EACT,SAAS;EACT,QAAQ,EAAE,OAAO,EAAE,MAAM,GAAG,EAAE;EAC/B,EACF,CACF,CAAC,EACY,KAAK,OAAY;EAC7B,KAAK,EAAE,IAAI;EACX,KAAK,EAAE,IAAI;EACX,OAAO,EAAE;EACV,EAAE"}
@@ -1 +1 @@
1
- {"version":3,"file":"reviewerMessage.service.mjs","names":[],"sources":["../../../src/services/reviewerMessage.service.ts"],"sourcesContent":["import { ReviewerMessageModel } from '@schemas/reviewer.schema';\nimport type { ReviewerMessageDocument } from '@/types/reviewer.types';\n\nexport const createMessage = async (\n missionId: string,\n senderId: string,\n content: string\n): Promise<ReviewerMessageDocument> =>\n ReviewerMessageModel.create({\n missionId,\n senderId,\n content,\n }) as unknown as ReviewerMessageDocument;\n\nexport const findMessagesByMissionId = async (\n missionId: string,\n limit = 100\n): Promise<ReviewerMessageDocument[]> =>\n ReviewerMessageModel.find({ missionId })\n .sort({ createdAt: 1 })\n .limit(limit) as unknown as ReviewerMessageDocument[];\n\nexport const findNewMessagesSince = async (\n missionId: string,\n since: Date\n): Promise<ReviewerMessageDocument[]> =>\n ReviewerMessageModel.find({\n missionId,\n createdAt: { $gt: since },\n }).sort({ createdAt: 1 }) as unknown as ReviewerMessageDocument[];\n\nexport const markMessagesRead = async (\n missionId: string,\n userId: string\n): Promise<void> => {\n await ReviewerMessageModel.updateMany(\n { missionId, senderId: { $ne: userId }, readAt: { $exists: false } },\n { readAt: new Date() }\n );\n};\n"],"mappings":";;;AAGA,MAAa,gBAAgB,OAC3B,WACA,UACA,YAEA,qBAAqB,OAAO;CAC1B;CACA;CACA;AACF,CAAC;AAEH,MAAa,0BAA0B,OACrC,WACA,QAAQ,QAER,qBAAqB,KAAK,EAAE,UAAU,CAAC,CAAC,CACrC,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC,CACtB,MAAM,KAAK;AAEhB,MAAa,uBAAuB,OAClC,WACA,UAEA,qBAAqB,KAAK;CACxB;CACA,WAAW,EAAE,KAAK,MAAM;AAC1B,CAAC,CAAC,CAAC,KAAK,EAAE,WAAW,EAAE,CAAC;AAE1B,MAAa,mBAAmB,OAC9B,WACA,WACkB;CAClB,MAAM,qBAAqB,WACzB;EAAE;EAAW,UAAU,EAAE,KAAK,OAAO;EAAG,QAAQ,EAAE,SAAS,MAAM;CAAE,GACnE,EAAE,wBAAQ,IAAI,KAAK,EAAE,CACvB;AACF"}
1
+ {"version":3,"file":"reviewerMessage.service.mjs","names":[],"sources":["../../../src/services/reviewerMessage.service.ts"],"sourcesContent":["import { ReviewerMessageModel } from '@schemas/reviewer.schema';\nimport type { ReviewerMessageDocument } from '@/types/reviewer.types';\n\nexport const createMessage = async (\n missionId: string,\n senderId: string,\n content: string\n): Promise<ReviewerMessageDocument> =>\n ReviewerMessageModel.create({\n missionId,\n senderId,\n content,\n }) as unknown as ReviewerMessageDocument;\n\nexport const findMessagesByMissionId = async (\n missionId: string,\n limit = 100\n): Promise<ReviewerMessageDocument[]> =>\n ReviewerMessageModel.find({ missionId })\n .sort({ createdAt: 1 })\n .limit(limit) as unknown as ReviewerMessageDocument[];\n\nexport const findNewMessagesSince = async (\n missionId: string,\n since: Date\n): Promise<ReviewerMessageDocument[]> =>\n ReviewerMessageModel.find({\n missionId,\n createdAt: { $gt: since },\n }).sort({ createdAt: 1 }) as unknown as ReviewerMessageDocument[];\n\nexport const markMessagesRead = async (\n missionId: string,\n userId: string\n): Promise<void> => {\n await ReviewerMessageModel.updateMany(\n { missionId, senderId: { $ne: userId }, readAt: { $exists: false } },\n { readAt: new Date() }\n );\n};\n"],"mappings":";;;AAGA,MAAa,gBAAgB,OAC3B,WACA,UACA,YAEA,qBAAqB,OAAO;CAC1B;CACA;CACA;CACD,CAAC;AAEJ,MAAa,0BAA0B,OACrC,WACA,QAAQ,QAER,qBAAqB,KAAK,EAAE,WAAW,CAAC,CACrC,KAAK,EAAE,WAAW,GAAG,CAAC,CACtB,MAAM,MAAM;AAEjB,MAAa,uBAAuB,OAClC,WACA,UAEA,qBAAqB,KAAK;CACxB;CACA,WAAW,EAAE,KAAK,OAAO;CAC1B,CAAC,CAAC,KAAK,EAAE,WAAW,GAAG,CAAC;AAE3B,MAAa,mBAAmB,OAC9B,WACA,WACkB;AAClB,OAAM,qBAAqB,WACzB;EAAE;EAAW,UAAU,EAAE,KAAK,QAAQ;EAAE,QAAQ,EAAE,SAAS,OAAO;EAAE,EACpE,EAAE,wBAAQ,IAAI,MAAM,EAAE,CACvB"}
@@ -1 +1 @@
1
- {"version":3,"file":"reviewerMission.service.mjs","names":[],"sources":["../../../src/services/reviewerMission.service.ts"],"sourcesContent":["import { DictionaryModel } from '@schemas/dictionary.schema';\nimport { TranslationMissionModel } from '@schemas/reviewer.schema';\nimport type {\n MissionEstimate,\n MissionStatus,\n TranslationMissionData,\n TranslationMissionDocument,\n} from '@/types/reviewer.types';\n\nconst WORDS_PER_HOUR = 300;\n\nconst extractStrings = (obj: any): string[] => {\n if (typeof obj === 'string') return [obj];\n if (Array.isArray(obj)) return obj.flatMap(extractStrings);\n if (obj && typeof obj === 'object')\n return Object.values(obj).flatMap(extractStrings);\n return [];\n};\n\nconst countWords = (str: string): number =>\n str.trim().split(/\\s+/).filter(Boolean).length;\n\nexport const countWordsFromDictionaries = async (\n dictionaryIds: string[],\n sourceLocale: string\n): Promise<number> => {\n if (!dictionaryIds.length) return 0;\n\n const dictionaries = await DictionaryModel.find({\n _id: { $in: dictionaryIds },\n });\n\n let total = 0;\n\n for (const dict of dictionaries) {\n const versionedContent = dict.content as Map<string, any>;\n if (!versionedContent) continue;\n\n const versions = Array.from(versionedContent.keys());\n if (!versions.length) continue;\n\n const latestVersion = versions[versions.length - 1];\n const versionEl = versionedContent.get(latestVersion);\n if (!versionEl?.content) continue;\n\n const localeContent =\n typeof versionEl.content === 'object'\n ? (versionEl.content[sourceLocale] ?? versionEl.content)\n : versionEl.content;\n\n const strings = extractStrings(localeContent);\n total += strings.reduce((sum, s) => sum + countWords(s), 0);\n }\n\n return total;\n};\n\nexport const calculateMissionEstimate = async (\n dictionaryIds: string[],\n sourceLocale: string,\n pricePerHour: number\n): Promise<MissionEstimate> => {\n const wordCount = await countWordsFromDictionaries(\n dictionaryIds,\n sourceLocale\n );\n const estimatedHours = wordCount / WORDS_PER_HOUR;\n const totalPrice = Math.ceil(estimatedHours * pricePerHour);\n\n return {\n wordCount,\n estimatedHours: Math.round(estimatedHours * 100) / 100,\n totalPrice,\n currency: 'usd',\n };\n};\n\nexport const createMission = async (\n data: Omit<TranslationMissionData, 'status'>\n): Promise<TranslationMissionDocument> =>\n TranslationMissionModel.create(data) as unknown as TranslationMissionDocument;\n\nexport const findMissionById = async (\n id: string\n): Promise<TranslationMissionDocument | null> =>\n TranslationMissionModel.findById(\n id\n ) as unknown as TranslationMissionDocument | null;\n\nexport const findMissionsForUser = async (\n userId: string,\n skip = 0,\n limit = 20\n): Promise<TranslationMissionDocument[]> =>\n TranslationMissionModel.find({ clientUserId: userId })\n .sort({ createdAt: -1 })\n .skip(skip)\n .limit(limit) as unknown as TranslationMissionDocument[];\n\nexport const findMissionsForReviewerProfile = async (\n reviewerProfileId: string,\n skip = 0,\n limit = 20\n): Promise<TranslationMissionDocument[]> =>\n TranslationMissionModel.find({ reviewerId: reviewerProfileId })\n .sort({ createdAt: -1 })\n .skip(skip)\n .limit(limit) as unknown as TranslationMissionDocument[];\n\nexport const updateMissionStatus = async (\n id: string,\n status: MissionStatus,\n extra?: Partial<\n Pick<\n TranslationMissionData,\n 'aiPreGeneratedAt' | 'completedAt' | 'canceledAt'\n >\n >\n): Promise<TranslationMissionDocument | null> => {\n const update: Record<string, any> = { status, ...extra };\n\n if (status === 'completed') update.completedAt = new Date();\n if (status === 'canceled') update.canceledAt = new Date();\n\n return TranslationMissionModel.findByIdAndUpdate(id, update, {\n new: true,\n }) as unknown as TranslationMissionDocument | null;\n};\n\nexport const countMissionsForUser = async (userId: string): Promise<number> =>\n TranslationMissionModel.countDocuments({ clientUserId: userId });\n"],"mappings":";;;;AASA,MAAM,iBAAiB;AAEvB,MAAM,kBAAkB,QAAuB;CAC7C,IAAI,OAAO,QAAQ,UAAU,OAAO,CAAC,GAAG;CACxC,IAAI,MAAM,QAAQ,GAAG,GAAG,OAAO,IAAI,QAAQ,cAAc;CACzD,IAAI,OAAO,OAAO,QAAQ,UACxB,OAAO,OAAO,OAAO,GAAG,CAAC,CAAC,QAAQ,cAAc;CAClD,OAAO,CAAC;AACV;AAEA,MAAM,cAAc,QAClB,IAAI,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC;AAE1C,MAAa,6BAA6B,OACxC,eACA,iBACoB;CACpB,IAAI,CAAC,cAAc,QAAQ,OAAO;CAElC,MAAM,eAAe,MAAM,gBAAgB,KAAK,EAC9C,KAAK,EAAE,KAAK,cAAc,EAC5B,CAAC;CAED,IAAI,QAAQ;CAEZ,KAAK,MAAM,QAAQ,cAAc;EAC/B,MAAM,mBAAmB,KAAK;EAC9B,IAAI,CAAC,kBAAkB;EAEvB,MAAM,WAAW,MAAM,KAAK,iBAAiB,KAAK,CAAC;EACnD,IAAI,CAAC,SAAS,QAAQ;EAEtB,MAAM,gBAAgB,SAAS,SAAS,SAAS;EACjD,MAAM,YAAY,iBAAiB,IAAI,aAAa;EACpD,IAAI,CAAC,WAAW,SAAS;EAOzB,MAAM,UAAU,eAJd,OAAO,UAAU,YAAY,WACxB,UAAU,QAAQ,iBAAiB,UAAU,UAC9C,UAAU,OAE4B;EAC5C,SAAS,QAAQ,QAAQ,KAAK,MAAM,MAAM,WAAW,CAAC,GAAG,CAAC;CAC5D;CAEA,OAAO;AACT;AAEA,MAAa,2BAA2B,OACtC,eACA,cACA,iBAC6B;CAC7B,MAAM,YAAY,MAAM,2BACtB,eACA,YACF;CACA,MAAM,iBAAiB,YAAY;CACnC,MAAM,aAAa,KAAK,KAAK,iBAAiB,YAAY;CAE1D,OAAO;EACL;EACA,gBAAgB,KAAK,MAAM,iBAAiB,GAAG,IAAI;EACnD;EACA,UAAU;CACZ;AACF;AAEA,MAAa,gBAAgB,OAC3B,SAEA,wBAAwB,OAAO,IAAI;AAErC,MAAa,kBAAkB,OAC7B,OAEA,wBAAwB,SACtB,EACF;AAEF,MAAa,sBAAsB,OACjC,QACA,OAAO,GACP,QAAQ,OAER,wBAAwB,KAAK,EAAE,cAAc,OAAO,CAAC,CAAC,CACnD,KAAK,EAAE,WAAW,GAAG,CAAC,CAAC,CACvB,KAAK,IAAI,CAAC,CACV,MAAM,KAAK;AAEhB,MAAa,iCAAiC,OAC5C,mBACA,OAAO,GACP,QAAQ,OAER,wBAAwB,KAAK,EAAE,YAAY,kBAAkB,CAAC,CAAC,CAC5D,KAAK,EAAE,WAAW,GAAG,CAAC,CAAC,CACvB,KAAK,IAAI,CAAC,CACV,MAAM,KAAK;AAEhB,MAAa,sBAAsB,OACjC,IACA,QACA,UAM+C;CAC/C,MAAM,SAA8B;EAAE;EAAQ,GAAG;CAAM;CAEvD,IAAI,WAAW,aAAa,OAAO,8BAAc,IAAI,KAAK;CAC1D,IAAI,WAAW,YAAY,OAAO,6BAAa,IAAI,KAAK;CAExD,OAAO,wBAAwB,kBAAkB,IAAI,QAAQ,EAC3D,KAAK,KACP,CAAC;AACH;AAEA,MAAa,uBAAuB,OAAO,WACzC,wBAAwB,eAAe,EAAE,cAAc,OAAO,CAAC"}
1
+ {"version":3,"file":"reviewerMission.service.mjs","names":[],"sources":["../../../src/services/reviewerMission.service.ts"],"sourcesContent":["import { DictionaryModel } from '@schemas/dictionary.schema';\nimport { TranslationMissionModel } from '@schemas/reviewer.schema';\nimport type {\n MissionEstimate,\n MissionStatus,\n TranslationMissionData,\n TranslationMissionDocument,\n} from '@/types/reviewer.types';\n\nconst WORDS_PER_HOUR = 300;\n\nconst extractStrings = (obj: any): string[] => {\n if (typeof obj === 'string') return [obj];\n if (Array.isArray(obj)) return obj.flatMap(extractStrings);\n if (obj && typeof obj === 'object')\n return Object.values(obj).flatMap(extractStrings);\n return [];\n};\n\nconst countWords = (str: string): number =>\n str.trim().split(/\\s+/).filter(Boolean).length;\n\nexport const countWordsFromDictionaries = async (\n dictionaryIds: string[],\n sourceLocale: string\n): Promise<number> => {\n if (!dictionaryIds.length) return 0;\n\n const dictionaries = await DictionaryModel.find({\n _id: { $in: dictionaryIds },\n });\n\n let total = 0;\n\n for (const dict of dictionaries) {\n const versionedContent = dict.content as Map<string, any>;\n if (!versionedContent) continue;\n\n const versions = Array.from(versionedContent.keys());\n if (!versions.length) continue;\n\n const latestVersion = versions[versions.length - 1];\n const versionEl = versionedContent.get(latestVersion);\n if (!versionEl?.content) continue;\n\n const localeContent =\n typeof versionEl.content === 'object'\n ? (versionEl.content[sourceLocale] ?? versionEl.content)\n : versionEl.content;\n\n const strings = extractStrings(localeContent);\n total += strings.reduce((sum, s) => sum + countWords(s), 0);\n }\n\n return total;\n};\n\nexport const calculateMissionEstimate = async (\n dictionaryIds: string[],\n sourceLocale: string,\n pricePerHour: number\n): Promise<MissionEstimate> => {\n const wordCount = await countWordsFromDictionaries(\n dictionaryIds,\n sourceLocale\n );\n const estimatedHours = wordCount / WORDS_PER_HOUR;\n const totalPrice = Math.ceil(estimatedHours * pricePerHour);\n\n return {\n wordCount,\n estimatedHours: Math.round(estimatedHours * 100) / 100,\n totalPrice,\n currency: 'usd',\n };\n};\n\nexport const createMission = async (\n data: Omit<TranslationMissionData, 'status'>\n): Promise<TranslationMissionDocument> =>\n TranslationMissionModel.create(data) as unknown as TranslationMissionDocument;\n\nexport const findMissionById = async (\n id: string\n): Promise<TranslationMissionDocument | null> =>\n TranslationMissionModel.findById(\n id\n ) as unknown as TranslationMissionDocument | null;\n\nexport const findMissionsForUser = async (\n userId: string,\n skip = 0,\n limit = 20\n): Promise<TranslationMissionDocument[]> =>\n TranslationMissionModel.find({ clientUserId: userId })\n .sort({ createdAt: -1 })\n .skip(skip)\n .limit(limit) as unknown as TranslationMissionDocument[];\n\nexport const findMissionsForReviewerProfile = async (\n reviewerProfileId: string,\n skip = 0,\n limit = 20\n): Promise<TranslationMissionDocument[]> =>\n TranslationMissionModel.find({ reviewerId: reviewerProfileId })\n .sort({ createdAt: -1 })\n .skip(skip)\n .limit(limit) as unknown as TranslationMissionDocument[];\n\nexport const updateMissionStatus = async (\n id: string,\n status: MissionStatus,\n extra?: Partial<\n Pick<\n TranslationMissionData,\n 'aiPreGeneratedAt' | 'completedAt' | 'canceledAt'\n >\n >\n): Promise<TranslationMissionDocument | null> => {\n const update: Record<string, any> = { status, ...extra };\n\n if (status === 'completed') update.completedAt = new Date();\n if (status === 'canceled') update.canceledAt = new Date();\n\n return TranslationMissionModel.findByIdAndUpdate(id, update, {\n new: true,\n }) as unknown as TranslationMissionDocument | null;\n};\n\nexport const countMissionsForUser = async (userId: string): Promise<number> =>\n TranslationMissionModel.countDocuments({ clientUserId: userId });\n"],"mappings":";;;;AASA,MAAM,iBAAiB;AAEvB,MAAM,kBAAkB,QAAuB;AAC7C,KAAI,OAAO,QAAQ,SAAU,QAAO,CAAC,IAAI;AACzC,KAAI,MAAM,QAAQ,IAAI,CAAE,QAAO,IAAI,QAAQ,eAAe;AAC1D,KAAI,OAAO,OAAO,QAAQ,SACxB,QAAO,OAAO,OAAO,IAAI,CAAC,QAAQ,eAAe;AACnD,QAAO,EAAE;;AAGX,MAAM,cAAc,QAClB,IAAI,MAAM,CAAC,MAAM,MAAM,CAAC,OAAO,QAAQ,CAAC;AAE1C,MAAa,6BAA6B,OACxC,eACA,iBACoB;AACpB,KAAI,CAAC,cAAc,OAAQ,QAAO;CAElC,MAAM,eAAe,MAAM,gBAAgB,KAAK,EAC9C,KAAK,EAAE,KAAK,eAAe,EAC5B,CAAC;CAEF,IAAI,QAAQ;AAEZ,MAAK,MAAM,QAAQ,cAAc;EAC/B,MAAM,mBAAmB,KAAK;AAC9B,MAAI,CAAC,iBAAkB;EAEvB,MAAM,WAAW,MAAM,KAAK,iBAAiB,MAAM,CAAC;AACpD,MAAI,CAAC,SAAS,OAAQ;EAEtB,MAAM,gBAAgB,SAAS,SAAS,SAAS;EACjD,MAAM,YAAY,iBAAiB,IAAI,cAAc;AACrD,MAAI,CAAC,WAAW,QAAS;EAOzB,MAAM,UAAU,eAJd,OAAO,UAAU,YAAY,WACxB,UAAU,QAAQ,iBAAiB,UAAU,UAC9C,UAAU,QAE6B;AAC7C,WAAS,QAAQ,QAAQ,KAAK,MAAM,MAAM,WAAW,EAAE,EAAE,EAAE;;AAG7D,QAAO;;AAGT,MAAa,2BAA2B,OACtC,eACA,cACA,iBAC6B;CAC7B,MAAM,YAAY,MAAM,2BACtB,eACA,aACD;CACD,MAAM,iBAAiB,YAAY;CACnC,MAAM,aAAa,KAAK,KAAK,iBAAiB,aAAa;AAE3D,QAAO;EACL;EACA,gBAAgB,KAAK,MAAM,iBAAiB,IAAI,GAAG;EACnD;EACA,UAAU;EACX;;AAGH,MAAa,gBAAgB,OAC3B,SAEA,wBAAwB,OAAO,KAAK;AAEtC,MAAa,kBAAkB,OAC7B,OAEA,wBAAwB,SACtB,GACD;AAEH,MAAa,sBAAsB,OACjC,QACA,OAAO,GACP,QAAQ,OAER,wBAAwB,KAAK,EAAE,cAAc,QAAQ,CAAC,CACnD,KAAK,EAAE,WAAW,IAAI,CAAC,CACvB,KAAK,KAAK,CACV,MAAM,MAAM;AAEjB,MAAa,iCAAiC,OAC5C,mBACA,OAAO,GACP,QAAQ,OAER,wBAAwB,KAAK,EAAE,YAAY,mBAAmB,CAAC,CAC5D,KAAK,EAAE,WAAW,IAAI,CAAC,CACvB,KAAK,KAAK,CACV,MAAM,MAAM;AAEjB,MAAa,sBAAsB,OACjC,IACA,QACA,UAM+C;CAC/C,MAAM,SAA8B;EAAE;EAAQ,GAAG;EAAO;AAExD,KAAI,WAAW,YAAa,QAAO,8BAAc,IAAI,MAAM;AAC3D,KAAI,WAAW,WAAY,QAAO,6BAAa,IAAI,MAAM;AAEzD,QAAO,wBAAwB,kBAAkB,IAAI,QAAQ,EAC3D,KAAK,MACN,CAAC;;AAGJ,MAAa,uBAAuB,OAAO,WACzC,wBAAwB,eAAe,EAAE,cAAc,QAAQ,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"session.service.mjs","names":[],"sources":["../../../src/services/session.service.ts"],"sourcesContent":["import { SessionModel } from '@schemas/session.schema';\nimport { UserModel } from '@schemas/user.schema';\nimport { Types } from 'mongoose';\nimport type { SessionDataApi } from '@/types/session.types';\n\n// ─── Context restore ──────────────────────────────────────────────────────────\n\ntype RestoreSessionContextInput = {\n sessionId: string;\n currentOrganizationId: string | null;\n currentProjectId: string | null;\n lastActiveOrganizationId: string | null | undefined;\n lastActiveProjectId: string | null | undefined;\n};\n\ntype RestoreSessionContextResult = {\n organizationId: string | null;\n projectId: string | null;\n};\n\n/**\n * When a session has no active org/project but the user document remembers\n * their last active context, restores those IDs into the session row.\n * Returns the resolved IDs (possibly unchanged) so the caller can proceed.\n */\nexport const restoreSessionContext = async ({\n sessionId,\n currentOrganizationId,\n currentProjectId,\n lastActiveOrganizationId,\n lastActiveProjectId,\n}: RestoreSessionContextInput): Promise<RestoreSessionContextResult> => {\n const restoredOrganizationId =\n currentOrganizationId ?? lastActiveOrganizationId ?? null;\n const restoredProjectId = currentProjectId ?? lastActiveProjectId ?? null;\n\n const needsWrite =\n restoredOrganizationId !== currentOrganizationId ||\n restoredProjectId !== currentProjectId;\n\n if (needsWrite) {\n await SessionModel.updateOne(\n { id: sessionId },\n {\n $set: {\n activeOrganizationId: restoredOrganizationId,\n activeProjectId: restoredProjectId,\n },\n }\n );\n }\n\n return {\n organizationId: restoredOrganizationId,\n projectId: restoredProjectId,\n };\n};\n\n// ─── Zombie cleanup ───────────────────────────────────────────────────────────\n\ntype ZombieCleanupInput = {\n sessionId: string;\n userId: string | Types.ObjectId | null;\n /** The ID stored in the session, if any. */\n storedOrganizationId: string | null | undefined;\n /** Whether the org document was found in the DB. */\n organizationExists: boolean;\n /** The ID stored in the session, if any. */\n storedProjectId: string | null | undefined;\n /** Whether the project document was found in the DB. */\n projectExists: boolean;\n};\n\nexport type ZombieCleanupResult = {\n /** True when the org ref was stale and has been cleared. */\n organizationCleared: boolean;\n /** True when the project ref was stale and has been cleared (also true when org cleared). */\n projectCleared: boolean;\n};\n\n/**\n * Detects \"zombie\" session references — IDs that were stored in the session\n * but whose documents no longer exist — and clears them from both the session\n * row and the user's last-active fields.\n *\n * Cascade rules:\n * - Stale org → clears org + project + environment\n * - Stale project only → clears project + environment (org untouched)\n *\n * Returns null when no stale references are found.\n */\nexport const clearZombieSessionContext = async ({\n sessionId,\n userId,\n storedOrganizationId,\n organizationExists,\n storedProjectId,\n projectExists,\n}: ZombieCleanupInput): Promise<ZombieCleanupResult | null> => {\n const organizationIsZombie = Boolean(\n storedOrganizationId && !organizationExists\n );\n const projectIsZombie = Boolean(storedProjectId && !projectExists);\n\n if (!organizationIsZombie && !projectIsZombie) return null;\n\n const sessionUpdate: Partial<SessionDataApi> = {};\n const userUpdate: Record<string, null> = {};\n\n if (organizationIsZombie) {\n sessionUpdate.activeOrganizationId = undefined;\n sessionUpdate.activeProjectId = undefined;\n sessionUpdate.activeEnvironmentId = undefined;\n userUpdate.lastActiveOrganizationId = null;\n userUpdate.lastActiveProjectId = null;\n } else {\n // Project-only zombie: leave org intact\n sessionUpdate.activeProjectId = undefined;\n sessionUpdate.activeEnvironmentId = undefined;\n userUpdate.lastActiveProjectId = null;\n }\n\n const writes: Promise<unknown>[] = [\n SessionModel.updateOne(\n { id: sessionId },\n // Convert undefined values → null for MongoDB\n {\n $set: Object.fromEntries(\n Object.keys(sessionUpdate).map((key) => [key, null])\n ),\n }\n ),\n ];\n\n if (userId) {\n const userObjectId =\n typeof userId === 'string' ? new Types.ObjectId(userId) : userId;\n writes.push(\n UserModel.updateOne({ _id: userObjectId }, { $set: userUpdate })\n );\n }\n\n await Promise.all(writes);\n\n return {\n organizationCleared: organizationIsZombie,\n projectCleared: organizationIsZombie || projectIsZombie,\n };\n};\n"],"mappings":";;;;;;;;;;AAyBA,MAAa,wBAAwB,OAAO,EAC1C,WACA,uBACA,kBACA,0BACA,0BACsE;CACtE,MAAM,yBACJ,yBAAyB,4BAA4B;CACvD,MAAM,oBAAoB,oBAAoB,uBAAuB;CAMrE,IAHE,2BAA2B,yBAC3B,sBAAsB,kBAGtB,MAAM,aAAa,UACjB,EAAE,IAAI,UAAU,GAChB,EACE,MAAM;EACJ,sBAAsB;EACtB,iBAAiB;CACnB,EACF,CACF;CAGF,OAAO;EACL,gBAAgB;EAChB,WAAW;CACb;AACF;;;;;;;;;;;;AAmCA,MAAa,4BAA4B,OAAO,EAC9C,WACA,QACA,sBACA,oBACA,iBACA,oBAC6D;CAC7D,MAAM,uBAAuB,QAC3B,wBAAwB,CAAC,kBAC3B;CACA,MAAM,kBAAkB,QAAQ,mBAAmB,CAAC,aAAa;CAEjE,IAAI,CAAC,wBAAwB,CAAC,iBAAiB,OAAO;CAEtD,MAAM,gBAAyC,CAAC;CAChD,MAAM,aAAmC,CAAC;CAE1C,IAAI,sBAAsB;EACxB,cAAc,uBAAuB;EACrC,cAAc,kBAAkB;EAChC,cAAc,sBAAsB;EACpC,WAAW,2BAA2B;EACtC,WAAW,sBAAsB;CACnC,OAAO;EAEL,cAAc,kBAAkB;EAChC,cAAc,sBAAsB;EACpC,WAAW,sBAAsB;CACnC;CAEA,MAAM,SAA6B,CACjC,aAAa,UACX,EAAE,IAAI,UAAU,GAEhB,EACE,MAAM,OAAO,YACX,OAAO,KAAK,aAAa,CAAC,CAAC,KAAK,QAAQ,CAAC,KAAK,IAAI,CAAC,CACrD,EACF,CACF,CACF;CAEA,IAAI,QAAQ;EACV,MAAM,eACJ,OAAO,WAAW,WAAW,IAAI,MAAM,SAAS,MAAM,IAAI;EAC5D,OAAO,KACL,UAAU,UAAU,EAAE,KAAK,aAAa,GAAG,EAAE,MAAM,WAAW,CAAC,CACjE;CACF;CAEA,MAAM,QAAQ,IAAI,MAAM;CAExB,OAAO;EACL,qBAAqB;EACrB,gBAAgB,wBAAwB;CAC1C;AACF"}
1
+ {"version":3,"file":"session.service.mjs","names":[],"sources":["../../../src/services/session.service.ts"],"sourcesContent":["import { SessionModel } from '@schemas/session.schema';\nimport { UserModel } from '@schemas/user.schema';\nimport { Types } from 'mongoose';\nimport type { SessionDataApi } from '@/types/session.types';\n\n// ─── Context restore ──────────────────────────────────────────────────────────\n\ntype RestoreSessionContextInput = {\n sessionId: string;\n currentOrganizationId: string | null;\n currentProjectId: string | null;\n lastActiveOrganizationId: string | null | undefined;\n lastActiveProjectId: string | null | undefined;\n};\n\ntype RestoreSessionContextResult = {\n organizationId: string | null;\n projectId: string | null;\n};\n\n/**\n * When a session has no active org/project but the user document remembers\n * their last active context, restores those IDs into the session row.\n * Returns the resolved IDs (possibly unchanged) so the caller can proceed.\n */\nexport const restoreSessionContext = async ({\n sessionId,\n currentOrganizationId,\n currentProjectId,\n lastActiveOrganizationId,\n lastActiveProjectId,\n}: RestoreSessionContextInput): Promise<RestoreSessionContextResult> => {\n const restoredOrganizationId =\n currentOrganizationId ?? lastActiveOrganizationId ?? null;\n const restoredProjectId = currentProjectId ?? lastActiveProjectId ?? null;\n\n const needsWrite =\n restoredOrganizationId !== currentOrganizationId ||\n restoredProjectId !== currentProjectId;\n\n if (needsWrite) {\n await SessionModel.updateOne(\n { id: sessionId },\n {\n $set: {\n activeOrganizationId: restoredOrganizationId,\n activeProjectId: restoredProjectId,\n },\n }\n );\n }\n\n return {\n organizationId: restoredOrganizationId,\n projectId: restoredProjectId,\n };\n};\n\n// ─── Zombie cleanup ───────────────────────────────────────────────────────────\n\ntype ZombieCleanupInput = {\n sessionId: string;\n userId: string | Types.ObjectId | null;\n /** The ID stored in the session, if any. */\n storedOrganizationId: string | null | undefined;\n /** Whether the org document was found in the DB. */\n organizationExists: boolean;\n /** The ID stored in the session, if any. */\n storedProjectId: string | null | undefined;\n /** Whether the project document was found in the DB. */\n projectExists: boolean;\n};\n\nexport type ZombieCleanupResult = {\n /** True when the org ref was stale and has been cleared. */\n organizationCleared: boolean;\n /** True when the project ref was stale and has been cleared (also true when org cleared). */\n projectCleared: boolean;\n};\n\n/**\n * Detects \"zombie\" session references — IDs that were stored in the session\n * but whose documents no longer exist — and clears them from both the session\n * row and the user's last-active fields.\n *\n * Cascade rules:\n * - Stale org → clears org + project + environment\n * - Stale project only → clears project + environment (org untouched)\n *\n * Returns null when no stale references are found.\n */\nexport const clearZombieSessionContext = async ({\n sessionId,\n userId,\n storedOrganizationId,\n organizationExists,\n storedProjectId,\n projectExists,\n}: ZombieCleanupInput): Promise<ZombieCleanupResult | null> => {\n const organizationIsZombie = Boolean(\n storedOrganizationId && !organizationExists\n );\n const projectIsZombie = Boolean(storedProjectId && !projectExists);\n\n if (!organizationIsZombie && !projectIsZombie) return null;\n\n const sessionUpdate: Partial<SessionDataApi> = {};\n const userUpdate: Record<string, null> = {};\n\n if (organizationIsZombie) {\n sessionUpdate.activeOrganizationId = undefined;\n sessionUpdate.activeProjectId = undefined;\n sessionUpdate.activeEnvironmentId = undefined;\n userUpdate.lastActiveOrganizationId = null;\n userUpdate.lastActiveProjectId = null;\n } else {\n // Project-only zombie: leave org intact\n sessionUpdate.activeProjectId = undefined;\n sessionUpdate.activeEnvironmentId = undefined;\n userUpdate.lastActiveProjectId = null;\n }\n\n const writes: Promise<unknown>[] = [\n SessionModel.updateOne(\n { id: sessionId },\n // Convert undefined values → null for MongoDB\n {\n $set: Object.fromEntries(\n Object.keys(sessionUpdate).map((key) => [key, null])\n ),\n }\n ),\n ];\n\n if (userId) {\n const userObjectId =\n typeof userId === 'string' ? new Types.ObjectId(userId) : userId;\n writes.push(\n UserModel.updateOne({ _id: userObjectId }, { $set: userUpdate })\n );\n }\n\n await Promise.all(writes);\n\n return {\n organizationCleared: organizationIsZombie,\n projectCleared: organizationIsZombie || projectIsZombie,\n };\n};\n"],"mappings":";;;;;;;;;;AAyBA,MAAa,wBAAwB,OAAO,EAC1C,WACA,uBACA,kBACA,0BACA,0BACsE;CACtE,MAAM,yBACJ,yBAAyB,4BAA4B;CACvD,MAAM,oBAAoB,oBAAoB,uBAAuB;AAMrE,KAHE,2BAA2B,yBAC3B,sBAAsB,iBAGtB,OAAM,aAAa,UACjB,EAAE,IAAI,WAAW,EACjB,EACE,MAAM;EACJ,sBAAsB;EACtB,iBAAiB;EAClB,EACF,CACF;AAGH,QAAO;EACL,gBAAgB;EAChB,WAAW;EACZ;;;;;;;;;;;;;AAoCH,MAAa,4BAA4B,OAAO,EAC9C,WACA,QACA,sBACA,oBACA,iBACA,oBAC6D;CAC7D,MAAM,uBAAuB,QAC3B,wBAAwB,CAAC,mBAC1B;CACD,MAAM,kBAAkB,QAAQ,mBAAmB,CAAC,cAAc;AAElE,KAAI,CAAC,wBAAwB,CAAC,gBAAiB,QAAO;CAEtD,MAAM,gBAAyC,EAAE;CACjD,MAAM,aAAmC,EAAE;AAE3C,KAAI,sBAAsB;AACxB,gBAAc,uBAAuB;AACrC,gBAAc,kBAAkB;AAChC,gBAAc,sBAAsB;AACpC,aAAW,2BAA2B;AACtC,aAAW,sBAAsB;QAC5B;AAEL,gBAAc,kBAAkB;AAChC,gBAAc,sBAAsB;AACpC,aAAW,sBAAsB;;CAGnC,MAAM,SAA6B,CACjC,aAAa,UACX,EAAE,IAAI,WAAW,EAEjB,EACE,MAAM,OAAO,YACX,OAAO,KAAK,cAAc,CAAC,KAAK,QAAQ,CAAC,KAAK,KAAK,CAAC,CACrD,EACF,CACF,CACF;AAED,KAAI,QAAQ;EACV,MAAM,eACJ,OAAO,WAAW,WAAW,IAAI,MAAM,SAAS,OAAO,GAAG;AAC5D,SAAO,KACL,UAAU,UAAU,EAAE,KAAK,cAAc,EAAE,EAAE,MAAM,YAAY,CAAC,CACjE;;AAGH,OAAM,QAAQ,IAAI,OAAO;AAEzB,QAAO;EACL,qBAAqB;EACrB,gBAAgB,wBAAwB;EACzC"}
@@ -1 +1 @@
1
- {"version":3,"file":"showcaseProject.service.mjs","names":[],"sources":["../../../../src/services/showcase/showcaseProject.service.ts"],"sourcesContent":["import { ShowcaseProjectModel } from '@schemas/showcaseProject.schema';\nimport { GenericError } from '@utils/errors';\nimport type {\n ShowcaseProjectData,\n ShowcaseProjectDocument,\n ShowcaseProjectStatus,\n} from '@/types/showcaseProject.types';\n\nexport const findShowcaseProjects = async (filters: {\n search?: string;\n selectedUseCases?: string[];\n isOpenSource?: boolean;\n page?: number;\n pageSize?: number;\n}): Promise<{\n data: ShowcaseProjectDocument[];\n total_items: number;\n total_pages: number;\n}> => {\n const {\n search,\n selectedUseCases,\n isOpenSource,\n page = 1,\n pageSize = 20,\n } = filters;\n\n const query: Record<string, unknown> = {};\n\n if (isOpenSource) {\n query.githubUrl = { $exists: true, $ne: null };\n }\n if (selectedUseCases && selectedUseCases.length > 0) {\n query.tags = { $in: selectedUseCases };\n }\n if (search?.trim()) {\n query.$or = [\n { title: { $regex: search, $options: 'i' } },\n { description: { $regex: search, $options: 'i' } },\n { tags: { $regex: search, $options: 'i' } },\n ];\n }\n\n const total_items = await ShowcaseProjectModel.countDocuments(query);\n const total_pages = Math.ceil(total_items / pageSize) || 1;\n\n const data = await ShowcaseProjectModel.aggregate([\n { $match: query },\n {\n $addFields: {\n score: {\n $subtract: [\n { $size: { $ifNull: ['$upvoters', []] } },\n { $size: { $ifNull: ['$downvoters', []] } },\n ],\n },\n },\n },\n { $sort: { score: -1, createdAt: -1 } },\n { $skip: (page - 1) * pageSize },\n { $limit: pageSize },\n ]);\n\n return {\n data: data as unknown as ShowcaseProjectDocument[],\n total_items,\n total_pages,\n };\n};\n\nexport const findShowcaseProjectById = async (\n projectId: string\n): Promise<ShowcaseProjectDocument> => {\n const project = await ShowcaseProjectModel.findById(projectId).lean();\n\n if (!project) {\n throw new GenericError('SHOWCASE_PROJECT_NOT_FOUND', { projectId });\n }\n\n return project as unknown as ShowcaseProjectDocument;\n};\n\n/**\n * Finds an existing project whose websiteUrl shares the same hostname as the\n * given URL. This treats `example.com` and `example.com/path` as duplicates\n * while allowing `sub.example.com` as a distinct entry.\n */\nexport const findShowcaseProjectByUrl = async (\n websiteUrl: string\n): Promise<ShowcaseProjectDocument | null> => {\n let hostname: string;\n try {\n hostname = new URL(websiteUrl).hostname;\n } catch {\n // Fallback to exact match if URL is unparseable\n const project = await ShowcaseProjectModel.findOne({\n websiteUrl: String(websiteUrl),\n }).lean();\n return project as unknown as ShowcaseProjectDocument | null;\n }\n\n // Match any stored URL whose authority (scheme + hostname) equals this hostname.\n // Anchored after the scheme so sub.example.com does NOT match example.com.\n const hostnameRegex = new RegExp(\n `^https?://${hostname.replace(/\\./g, '\\\\.')}(/|$)`,\n 'i'\n );\n const project = await ShowcaseProjectModel.findOne({\n websiteUrl: { $regex: hostnameRegex },\n }).lean();\n return project as unknown as ShowcaseProjectDocument | null;\n};\n\nexport const findOtherShowcaseProjects = async (\n excludeId: string,\n limit = 4\n): Promise<ShowcaseProjectDocument[]> => {\n const projects = await ShowcaseProjectModel.find({\n _id: { $ne: excludeId },\n })\n .limit(limit)\n .lean();\n\n return projects as unknown as ShowcaseProjectDocument[];\n};\n\nexport const createShowcaseProject = async (\n projectData: ShowcaseProjectData\n): Promise<ShowcaseProjectDocument> => {\n const newProject = new ShowcaseProjectModel({\n ...projectData,\n ...projectData,\n upvoters: [],\n downvoters: [],\n });\n\n await newProject.save();\n return newProject as unknown as ShowcaseProjectDocument;\n};\n\nexport type UpdateShowcaseProjectScanData = {\n title?: string;\n description?: string;\n websiteUrl?: string;\n githubUrl?: string | null;\n tags?: string[];\n intlayerVersion?: string;\n libsUsed?: string[];\n packageDetails?: Record<string, string>;\n scanDetails?: ShowcaseProjectData['scanDetails'];\n imageUrl?: string;\n isOpenSource?: boolean;\n status?: ShowcaseProjectStatus;\n lastScanDate?: Date;\n};\n\nexport const updateShowcaseProject = async (\n projectId: string,\n updates: UpdateShowcaseProjectScanData\n): Promise<ShowcaseProjectDocument> => {\n const project = await ShowcaseProjectModel.findByIdAndUpdate(\n projectId,\n { $set: updates },\n { new: true }\n ).lean();\n\n if (!project) {\n throw new GenericError('SHOWCASE_PROJECT_NOT_FOUND', { projectId });\n }\n\n return project as unknown as ShowcaseProjectDocument;\n};\n\nexport const deleteShowcaseProject = async (\n projectId: string\n): Promise<void> => {\n await ShowcaseProjectModel.findByIdAndDelete(projectId);\n};\n\ntype VoteResult = {\n upvotes: number;\n isUpVoted: boolean;\n downvotes: number;\n isDownVoted: boolean;\n};\n\nconst toggleShowcaseVote = async (\n projectId: string,\n userId: string,\n voteType: 'up' | 'down'\n): Promise<VoteResult> => {\n const project = await ShowcaseProjectModel.findById(projectId);\n\n if (!project) {\n throw new GenericError('SHOWCASE_PROJECT_NOT_FOUND', { projectId });\n }\n\n const upvoters: string[] = project.upvoters || [];\n const downvoters: string[] = project.downvoters || [];\n\n if (voteType === 'up') {\n const wasUpvoted = upvoters.includes(userId);\n\n if (wasUpvoted) {\n project.upvoters = upvoters.filter((id) => id !== userId);\n } else {\n project.upvoters.push(userId);\n if (downvoters.includes(userId)) {\n project.downvoters = downvoters.filter((id) => id !== userId);\n }\n }\n\n await project.save();\n\n return {\n upvotes: project.upvoters.length,\n isUpVoted: !wasUpvoted,\n downvotes: project.downvoters.length,\n isDownVoted: false,\n };\n } else {\n const wasDownvoted = downvoters.includes(userId);\n\n if (wasDownvoted) {\n project.downvoters = downvoters.filter((id) => id !== userId);\n } else {\n project.downvoters.push(userId);\n\n if (upvoters.includes(userId)) {\n project.upvoters = upvoters.filter((id) => id !== userId);\n }\n }\n await project.save();\n return {\n upvotes: project.upvoters.length,\n isUpVoted: false,\n downvotes: project.downvoters.length,\n isDownVoted: !wasDownvoted,\n };\n }\n};\n\nexport const toggleShowcaseUpvote = (\n projectId: string,\n userId: string\n): Promise<VoteResult> => toggleShowcaseVote(projectId, userId, 'up');\n\nexport const toggleShowcaseDownvote = (\n projectId: string,\n userId: string\n): Promise<VoteResult> => toggleShowcaseVote(projectId, userId, 'down');\n"],"mappings":"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"}
1
+ {"version":3,"file":"showcaseProject.service.mjs","names":[],"sources":["../../../../src/services/showcase/showcaseProject.service.ts"],"sourcesContent":["import { ShowcaseProjectModel } from '@schemas/showcaseProject.schema';\nimport { GenericError } from '@utils/errors';\nimport type {\n ShowcaseProjectData,\n ShowcaseProjectDocument,\n ShowcaseProjectStatus,\n} from '@/types/showcaseProject.types';\n\nexport const findShowcaseProjects = async (filters: {\n search?: string;\n selectedUseCases?: string[];\n isOpenSource?: boolean;\n page?: number;\n pageSize?: number;\n}): Promise<{\n data: ShowcaseProjectDocument[];\n total_items: number;\n total_pages: number;\n}> => {\n const {\n search,\n selectedUseCases,\n isOpenSource,\n page = 1,\n pageSize = 20,\n } = filters;\n\n const query: Record<string, unknown> = {};\n\n if (isOpenSource) {\n query.githubUrl = { $exists: true, $ne: null };\n }\n if (selectedUseCases && selectedUseCases.length > 0) {\n query.tags = { $in: selectedUseCases };\n }\n if (search?.trim()) {\n query.$or = [\n { title: { $regex: search, $options: 'i' } },\n { description: { $regex: search, $options: 'i' } },\n { tags: { $regex: search, $options: 'i' } },\n ];\n }\n\n const total_items = await ShowcaseProjectModel.countDocuments(query);\n const total_pages = Math.ceil(total_items / pageSize) || 1;\n\n const data = await ShowcaseProjectModel.aggregate([\n { $match: query },\n {\n $addFields: {\n score: {\n $subtract: [\n { $size: { $ifNull: ['$upvoters', []] } },\n { $size: { $ifNull: ['$downvoters', []] } },\n ],\n },\n },\n },\n { $sort: { score: -1, createdAt: -1 } },\n { $skip: (page - 1) * pageSize },\n { $limit: pageSize },\n ]);\n\n return {\n data: data as unknown as ShowcaseProjectDocument[],\n total_items,\n total_pages,\n };\n};\n\nexport const findShowcaseProjectById = async (\n projectId: string\n): Promise<ShowcaseProjectDocument> => {\n const project = await ShowcaseProjectModel.findById(projectId).lean();\n\n if (!project) {\n throw new GenericError('SHOWCASE_PROJECT_NOT_FOUND', { projectId });\n }\n\n return project as unknown as ShowcaseProjectDocument;\n};\n\n/**\n * Finds an existing project whose websiteUrl shares the same hostname as the\n * given URL. This treats `example.com` and `example.com/path` as duplicates\n * while allowing `sub.example.com` as a distinct entry.\n */\nexport const findShowcaseProjectByUrl = async (\n websiteUrl: string\n): Promise<ShowcaseProjectDocument | null> => {\n let hostname: string;\n try {\n hostname = new URL(websiteUrl).hostname;\n } catch {\n // Fallback to exact match if URL is unparseable\n const project = await ShowcaseProjectModel.findOne({\n websiteUrl: String(websiteUrl),\n }).lean();\n return project as unknown as ShowcaseProjectDocument | null;\n }\n\n // Match any stored URL whose authority (scheme + hostname) equals this hostname.\n // Anchored after the scheme so sub.example.com does NOT match example.com.\n const hostnameRegex = new RegExp(\n `^https?://${hostname.replace(/\\./g, '\\\\.')}(/|$)`,\n 'i'\n );\n const project = await ShowcaseProjectModel.findOne({\n websiteUrl: { $regex: hostnameRegex },\n }).lean();\n return project as unknown as ShowcaseProjectDocument | null;\n};\n\nexport const findOtherShowcaseProjects = async (\n excludeId: string,\n limit = 4\n): Promise<ShowcaseProjectDocument[]> => {\n const projects = await ShowcaseProjectModel.find({\n _id: { $ne: excludeId },\n })\n .limit(limit)\n .lean();\n\n return projects as unknown as ShowcaseProjectDocument[];\n};\n\nexport const createShowcaseProject = async (\n projectData: ShowcaseProjectData\n): Promise<ShowcaseProjectDocument> => {\n const newProject = new ShowcaseProjectModel({\n ...projectData,\n ...projectData,\n upvoters: [],\n downvoters: [],\n });\n\n await newProject.save();\n return newProject as unknown as ShowcaseProjectDocument;\n};\n\nexport type UpdateShowcaseProjectScanData = {\n title?: string;\n description?: string;\n websiteUrl?: string;\n githubUrl?: string | null;\n tags?: string[];\n intlayerVersion?: string;\n libsUsed?: string[];\n packageDetails?: Record<string, string>;\n scanDetails?: ShowcaseProjectData['scanDetails'];\n imageUrl?: string;\n isOpenSource?: boolean;\n status?: ShowcaseProjectStatus;\n lastScanDate?: Date;\n};\n\nexport const updateShowcaseProject = async (\n projectId: string,\n updates: UpdateShowcaseProjectScanData\n): Promise<ShowcaseProjectDocument> => {\n const project = await ShowcaseProjectModel.findByIdAndUpdate(\n projectId,\n { $set: updates },\n { new: true }\n ).lean();\n\n if (!project) {\n throw new GenericError('SHOWCASE_PROJECT_NOT_FOUND', { projectId });\n }\n\n return project as unknown as ShowcaseProjectDocument;\n};\n\nexport const deleteShowcaseProject = async (\n projectId: string\n): Promise<void> => {\n await ShowcaseProjectModel.findByIdAndDelete(projectId);\n};\n\ntype VoteResult = {\n upvotes: number;\n isUpVoted: boolean;\n downvotes: number;\n isDownVoted: boolean;\n};\n\nconst toggleShowcaseVote = async (\n projectId: string,\n userId: string,\n voteType: 'up' | 'down'\n): Promise<VoteResult> => {\n const project = await ShowcaseProjectModel.findById(projectId);\n\n if (!project) {\n throw new GenericError('SHOWCASE_PROJECT_NOT_FOUND', { projectId });\n }\n\n const upvoters: string[] = project.upvoters || [];\n const downvoters: string[] = project.downvoters || [];\n\n if (voteType === 'up') {\n const wasUpvoted = upvoters.includes(userId);\n\n if (wasUpvoted) {\n project.upvoters = upvoters.filter((id) => id !== userId);\n } else {\n project.upvoters.push(userId);\n if (downvoters.includes(userId)) {\n project.downvoters = downvoters.filter((id) => id !== userId);\n }\n }\n\n await project.save();\n\n return {\n upvotes: project.upvoters.length,\n isUpVoted: !wasUpvoted,\n downvotes: project.downvoters.length,\n isDownVoted: false,\n };\n } else {\n const wasDownvoted = downvoters.includes(userId);\n\n if (wasDownvoted) {\n project.downvoters = downvoters.filter((id) => id !== userId);\n } else {\n project.downvoters.push(userId);\n\n if (upvoters.includes(userId)) {\n project.upvoters = upvoters.filter((id) => id !== userId);\n }\n }\n await project.save();\n return {\n upvotes: project.upvoters.length,\n isUpVoted: false,\n downvotes: project.downvoters.length,\n isDownVoted: !wasDownvoted,\n };\n }\n};\n\nexport const toggleShowcaseUpvote = (\n projectId: string,\n userId: string\n): Promise<VoteResult> => toggleShowcaseVote(projectId, userId, 'up');\n\nexport const toggleShowcaseDownvote = (\n projectId: string,\n userId: string\n): Promise<VoteResult> => toggleShowcaseVote(projectId, userId, 'down');\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"showcaseScan.service.mjs","names":[],"sources":["../../../../src/services/showcase/showcaseScan.service.ts"],"sourcesContent":["import { ALL_LOCALES } from '@intlayer/types/allLocales';\nimport { logger } from '@logger';\nimport { launchBrowser } from '@utils/puppeteer/launchBrowser';\nimport type { ShowcaseScanDetails } from '@/types/showcaseProject.types';\n\nexport interface ShowcaseScannedInfo {\n /** Whether the intlayer bundle was detected in the page */\n hasIntlayer: boolean;\n /** Version of the core `intlayer` package if detectable from the bundle */\n intlayerVersion?: string;\n /** All detected intlayer-related packages and their versions (from bundle markers) */\n packageDetails: Record<string, string>;\n /** List of detected intlayer package names */\n libsUsed: string[];\n scanDetails: ShowcaseScanDetails;\n /** JPEG screenshot buffer taken on the same page load. Undefined if scan fails. */\n screenshotBuffer?: Buffer;\n /** Page title extracted from og:title or <title> */\n metaTitle?: string;\n /** Short description extracted from og:description or meta description */\n metaDescription?: string;\n}\n\n/**\n * Regex that matches any intlayer package name and its version from a minified bundle.\n */\nconst INTLAYER_BUNDLE_PKG_REGEX =\n /name\\s*:\\s*['\"]([^'\"]*intlayer[^'\"]*)['\"]\\s*,\\s*version\\s*:\\s*['\"]([^'\"]+)['\"]/gi;\n\n/**\n * The distinct bundle marker emitted by intlayer:\n * { name: 'Intlayer', version: '...', doc: 'https://intlayer.org/docs' }\n * Matches the exact property order from buildConfigurationFields.ts.\n */\nconst INTLAYER_BUNDLE_MARKER =\n /name\\s*:\\s*['\"]Intlayer['\"]\\s*,\\s*version\\s*:\\s*['\"][^'\"]+['\"]\\s*,\\s*doc\\s*:\\s*[`'\"]https:\\/\\/intlayer\\.org\\/docs[`'\"]/i;\n\n/**\n * Matches any assignment to window.intlayer in a bundle.\n * e.g. window.intlayer={enabled:!0} or window[\"intlayer\"]={enabled:true}\n */\nconst WINDOW_INTLAYER_PATTERN =\n /window\\s*(?:\\.\\s*intlayer|\\[['\"]intlayer['\"]\\])\\s*=/;\n\n/** Extract all intlayer packages from a script text blob. */\nexport const extractPackagesFromScript = (\n content: string\n): Record<string, string> => {\n const result: Record<string, string> = {};\n const regex = new RegExp(INTLAYER_BUNDLE_PKG_REGEX.source, 'gi');\n for (const match of content.matchAll(regex)) {\n const name = match[1];\n const version = match[2].replace(/^[^0-9]*/, '') || match[2];\n result[name] = version;\n }\n return result;\n};\n\nconst MAX_EXTERNAL_SCRIPTS = 50;\nconst MAX_SCRIPT_BYTES = 5 * 1024 * 1024; // 5 MB\n\n/**\n * Opens a single browser session, scans the page for Intlayer usage,\n * takes a screenshot, then closes the browser.\n * Combining both operations avoids launching Puppeteer twice.\n */\nexport const scanShowcaseProject = async (\n url: string\n): Promise<ShowcaseScannedInfo> => {\n logger.info(`[scanShowcaseProject] Scanning ${url}...`);\n\n const browser = await launchBrowser({ pipe: false, dumpio: false });\n\n try {\n const page = await browser.newPage();\n await page.setViewport({ width: 1280, height: 720 });\n await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45000 });\n await page.waitForSelector('body', { timeout: 10000 });\n await page\n .waitForNetworkIdle({ idleTime: 1000, timeout: 10000 })\n .catch(() => {\n /* ok if the page never fully idles (SPAs, analytics, etc.) */\n });\n\n const allLocales = Object.values(ALL_LOCALES);\n\n // Page-level details (DOM evaluation)\n const pageDetails = await page.evaluate((locales) => {\n const html = document.documentElement;\n const lang = html.getAttribute('lang') || '';\n const dir = html.getAttribute('dir') || 'ltr';\n\n const hreflangs = Array.from(\n document.querySelectorAll('link[rel=\"alternate\"][hreflang]')\n ).map((link) => link.getAttribute('hreflang') || '');\n\n const canonical =\n document.querySelector('link[rel=\"canonical\"]')?.getAttribute('href') ||\n '';\n\n const links = Array.from(document.querySelectorAll('a[href]')).map(\n (a) => a.getAttribute('href') || ''\n );\n\n const localizedPrefixes = locales.map((el) => `/${el}`);\n const internalLinks = links.filter(\n (href) =>\n href.startsWith('/') || href.startsWith(window.location.origin)\n );\n const localizedLinks = internalLinks.filter((href) =>\n localizedPrefixes.some(\n (prefix) => href.startsWith(`${prefix}/`) || href === prefix\n )\n );\n const hasLocalizedLinks = localizedLinks.length > 0;\n const allAnchorsLocalized =\n internalLinks.length > 0 &&\n localizedLinks.length === internalLinks.length;\n\n const inlineScripts = Array.from(\n document.querySelectorAll('script:not([src])')\n )\n .map((script) => script.textContent || '')\n .filter(Boolean);\n\n const externalScriptUrls = Array.from(\n document.querySelectorAll('script[src]')\n )\n .map((script) => (script as HTMLScriptElement).src)\n .filter(Boolean);\n\n const resourceScripts = (\n performance.getEntriesByType('resource') as PerformanceResourceTiming[]\n )\n .filter(\n (resource) =>\n resource.initiatorType === 'script' ||\n resource.name.match(/\\.(js|mjs|cjs)(\\?[^\"'\\s]*)?$/i)\n )\n .map((resource) => resource.name);\n\n const allScriptUrls = Array.from(\n new Set([...externalScriptUrls, ...resourceScripts])\n );\n\n // Meta tag extraction\n const metaTitle =\n document\n .querySelector('meta[property=\"og:title\"]')\n ?.getAttribute('content') ||\n document.title ||\n '';\n const metaDescription =\n document\n .querySelector('meta[property=\"og:description\"]')\n ?.getAttribute('content') ||\n document\n .querySelector('meta[name=\"description\"]')\n ?.getAttribute('content') ||\n '';\n\n const hasWindowIntlayer = Boolean((window as any).intlayer);\n\n return {\n lang,\n dir,\n hreflangs,\n canonical,\n hasLocalizedLinks,\n allAnchorsLocalized,\n inlineScripts,\n externalScriptUrls: allScriptUrls,\n metaTitle,\n metaDescription,\n hasWindowIntlayer,\n };\n }, allLocales);\n\n // Delaying screenshot until we verify the package presence\n\n // robots.txt\n let robotsAccessible = false;\n try {\n const robotsRes = await fetch(new URL('/robots.txt', url).toString());\n robotsAccessible = robotsRes.ok;\n } catch {}\n\n // sitemap.xml\n let sitemapDiscoverable = false;\n let sitemapUrlCount = 0;\n try {\n const sitemapRes = await fetch(new URL('/sitemap.xml', url).toString());\n\n if (sitemapRes.ok) {\n sitemapDiscoverable = true;\n const text = await sitemapRes.text();\n sitemapUrlCount = (text.match(/<loc>/g) || []).length;\n }\n } catch {}\n\n // ── Intlayer detection ────────────────────────────────────────────────────\n const packageDetails: Record<string, string> = {};\n let markerFoundInExternalScript = false;\n\n // Primary: window.intlayer is set — no need to scan bundles\n if (pageDetails.hasWindowIntlayer) {\n // Extract version details from inline scripts while we're here\n for (const script of pageDetails.inlineScripts) {\n Object.assign(packageDetails, extractPackagesFromScript(script));\n }\n } else {\n // Fallback: scan inline scripts\n for (const script of pageDetails.inlineScripts) {\n Object.assign(packageDetails, extractPackagesFromScript(script));\n if (\n !markerFoundInExternalScript &&\n (INTLAYER_BUNDLE_MARKER.test(script) ||\n WINDOW_INTLAYER_PATTERN.test(script))\n ) {\n markerFoundInExternalScript = true;\n }\n }\n\n // Fallback: fetch and scan external bundles\n if (\n !markerFoundInExternalScript &&\n !Object.keys(packageDetails).some((key) => key.includes('intlayer'))\n ) {\n const scriptUrls = pageDetails.externalScriptUrls.slice(\n 0,\n MAX_EXTERNAL_SCRIPTS\n );\n\n await Promise.allSettled(\n scriptUrls.map(async (src) => {\n try {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), 5000);\n\n const res = await fetch(src, {\n signal: controller.signal,\n }).finally(() => clearTimeout(timeoutId));\n\n if (!res.ok) return;\n const contentLength = Number(\n res.headers.get('content-length') || 0\n );\n if (contentLength > MAX_SCRIPT_BYTES) return;\n\n const text = await res.text();\n if (text.length > MAX_SCRIPT_BYTES) return;\n\n if (\n INTLAYER_BUNDLE_MARKER.test(text) ||\n WINDOW_INTLAYER_PATTERN.test(text)\n ) {\n markerFoundInExternalScript = true;\n Object.assign(packageDetails, extractPackagesFromScript(text));\n } else if (text.includes('intlayer')) {\n Object.assign(packageDetails, extractPackagesFromScript(text));\n }\n } catch {}\n })\n );\n }\n }\n\n const hasIntlayer =\n pageDetails.hasWindowIntlayer ||\n Object.keys(packageDetails).some(\n (key) =>\n key.toLowerCase().includes('intlayer') &&\n !key.toLowerCase().includes('@intlayer')\n ) ||\n markerFoundInExternalScript;\n\n const libsUsed = Object.keys(packageDetails);\n const intlayerVersion = packageDetails.intlayer;\n\n // SEO score\n let score = 0;\n if (pageDetails.lang) score += 10;\n if (pageDetails.canonical) score += 10;\n if (pageDetails.hreflangs.length > 0) score += 20;\n if (pageDetails.hreflangs.includes('x-default')) score += 10;\n if (robotsAccessible) score += 10;\n if (sitemapDiscoverable) score += 10;\n if (pageDetails.hasLocalizedLinks) score += 20;\n if (pageDetails.allAnchorsLocalized) score += 10;\n\n let screenshotBuffer: Buffer | undefined;\n\n if (hasIntlayer) {\n // Screenshot (same page, no extra navigation)\n logger.info(`[scanShowcaseProject] Taking screenshot of ${url}...`);\n screenshotBuffer = (await page.screenshot({\n type: 'jpeg',\n quality: 30,\n })) as Buffer;\n }\n\n await page.close();\n\n return {\n hasIntlayer,\n intlayerVersion,\n packageDetails,\n libsUsed,\n screenshotBuffer,\n metaTitle: pageDetails.metaTitle,\n metaDescription: pageDetails.metaDescription,\n scanDetails: {\n score,\n langTag: pageDetails.lang,\n htmlDir: pageDetails.dir,\n hreflangs: pageDetails.hreflangs,\n hasXDefault: pageDetails.hreflangs.includes('x-default'),\n hasCanonical: !!pageDetails.canonical,\n hasLocalizedLinks: pageDetails.hasLocalizedLinks,\n allAnchorsLocalized: pageDetails.allAnchorsLocalized,\n robotsTxt: {\n accessible: robotsAccessible,\n disallowWithoutLocaleAlternates: robotsAccessible,\n },\n sitemapXml: {\n urlsDiscoveredCount: sitemapUrlCount,\n alternatesPresent: sitemapUrlCount > 0,\n xDefaultPresent: sitemapDiscoverable,\n },\n },\n };\n } finally {\n await browser.close().catch(() => {});\n }\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"showcaseScan.service.mjs","names":[],"sources":["../../../../src/services/showcase/showcaseScan.service.ts"],"sourcesContent":["import { ALL_LOCALES } from '@intlayer/types/allLocales';\nimport { logger } from '@logger';\nimport { launchBrowser } from '@utils/puppeteer/launchBrowser';\nimport type { ShowcaseScanDetails } from '@/types/showcaseProject.types';\n\nexport interface ShowcaseScannedInfo {\n /** Whether the intlayer bundle was detected in the page */\n hasIntlayer: boolean;\n /** Version of the core `intlayer` package if detectable from the bundle */\n intlayerVersion?: string;\n /** All detected intlayer-related packages and their versions (from bundle markers) */\n packageDetails: Record<string, string>;\n /** List of detected intlayer package names */\n libsUsed: string[];\n scanDetails: ShowcaseScanDetails;\n /** JPEG screenshot buffer taken on the same page load. Undefined if scan fails. */\n screenshotBuffer?: Buffer;\n /** Page title extracted from og:title or <title> */\n metaTitle?: string;\n /** Short description extracted from og:description or meta description */\n metaDescription?: string;\n}\n\n/**\n * Regex that matches any intlayer package name and its version from a minified bundle.\n */\nconst INTLAYER_BUNDLE_PKG_REGEX =\n /name\\s*:\\s*['\"]([^'\"]*intlayer[^'\"]*)['\"]\\s*,\\s*version\\s*:\\s*['\"]([^'\"]+)['\"]/gi;\n\n/**\n * The distinct bundle marker emitted by intlayer:\n * { name: 'Intlayer', version: '...', doc: 'https://intlayer.org/docs' }\n * Matches the exact property order from buildConfigurationFields.ts.\n */\nconst INTLAYER_BUNDLE_MARKER =\n /name\\s*:\\s*['\"]Intlayer['\"]\\s*,\\s*version\\s*:\\s*['\"][^'\"]+['\"]\\s*,\\s*doc\\s*:\\s*[`'\"]https:\\/\\/intlayer\\.org\\/docs[`'\"]/i;\n\n/**\n * Matches any assignment to window.intlayer in a bundle.\n * e.g. window.intlayer={enabled:!0} or window[\"intlayer\"]={enabled:true}\n */\nconst WINDOW_INTLAYER_PATTERN =\n /window\\s*(?:\\.\\s*intlayer|\\[['\"]intlayer['\"]\\])\\s*=/;\n\n/** Extract all intlayer packages from a script text blob. */\nexport const extractPackagesFromScript = (\n content: string\n): Record<string, string> => {\n const result: Record<string, string> = {};\n const regex = new RegExp(INTLAYER_BUNDLE_PKG_REGEX.source, 'gi');\n for (const match of content.matchAll(regex)) {\n const name = match[1];\n const version = match[2].replace(/^[^0-9]*/, '') || match[2];\n result[name] = version;\n }\n return result;\n};\n\nconst MAX_EXTERNAL_SCRIPTS = 50;\nconst MAX_SCRIPT_BYTES = 5 * 1024 * 1024; // 5 MB\n\n/**\n * Opens a single browser session, scans the page for Intlayer usage,\n * takes a screenshot, then closes the browser.\n * Combining both operations avoids launching Puppeteer twice.\n */\nexport const scanShowcaseProject = async (\n url: string\n): Promise<ShowcaseScannedInfo> => {\n logger.info(`[scanShowcaseProject] Scanning ${url}...`);\n\n const browser = await launchBrowser({ pipe: false, dumpio: false });\n\n try {\n const page = await browser.newPage();\n await page.setViewport({ width: 1280, height: 720 });\n await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45000 });\n await page.waitForSelector('body', { timeout: 10000 });\n await page\n .waitForNetworkIdle({ idleTime: 1000, timeout: 10000 })\n .catch(() => {\n /* ok if the page never fully idles (SPAs, analytics, etc.) */\n });\n\n const allLocales = Object.values(ALL_LOCALES);\n\n // Page-level details (DOM evaluation)\n const pageDetails = await page.evaluate((locales) => {\n const html = document.documentElement;\n const lang = html.getAttribute('lang') || '';\n const dir = html.getAttribute('dir') || 'ltr';\n\n const hreflangs = Array.from(\n document.querySelectorAll('link[rel=\"alternate\"][hreflang]')\n ).map((link) => link.getAttribute('hreflang') || '');\n\n const canonical =\n document.querySelector('link[rel=\"canonical\"]')?.getAttribute('href') ||\n '';\n\n const links = Array.from(document.querySelectorAll('a[href]')).map(\n (a) => a.getAttribute('href') || ''\n );\n\n const localizedPrefixes = locales.map((el) => `/${el}`);\n const internalLinks = links.filter(\n (href) =>\n href.startsWith('/') || href.startsWith(window.location.origin)\n );\n const localizedLinks = internalLinks.filter((href) =>\n localizedPrefixes.some(\n (prefix) => href.startsWith(`${prefix}/`) || href === prefix\n )\n );\n const hasLocalizedLinks = localizedLinks.length > 0;\n const allAnchorsLocalized =\n internalLinks.length > 0 &&\n localizedLinks.length === internalLinks.length;\n\n const inlineScripts = Array.from(\n document.querySelectorAll('script:not([src])')\n )\n .map((script) => script.textContent || '')\n .filter(Boolean);\n\n const externalScriptUrls = Array.from(\n document.querySelectorAll('script[src]')\n )\n .map((script) => (script as HTMLScriptElement).src)\n .filter(Boolean);\n\n const resourceScripts = (\n performance.getEntriesByType('resource') as PerformanceResourceTiming[]\n )\n .filter(\n (resource) =>\n resource.initiatorType === 'script' ||\n resource.name.match(/\\.(js|mjs|cjs)(\\?[^\"'\\s]*)?$/i)\n )\n .map((resource) => resource.name);\n\n const allScriptUrls = Array.from(\n new Set([...externalScriptUrls, ...resourceScripts])\n );\n\n // Meta tag extraction\n const metaTitle =\n document\n .querySelector('meta[property=\"og:title\"]')\n ?.getAttribute('content') ||\n document.title ||\n '';\n const metaDescription =\n document\n .querySelector('meta[property=\"og:description\"]')\n ?.getAttribute('content') ||\n document\n .querySelector('meta[name=\"description\"]')\n ?.getAttribute('content') ||\n '';\n\n const hasWindowIntlayer = Boolean((window as any).intlayer);\n\n return {\n lang,\n dir,\n hreflangs,\n canonical,\n hasLocalizedLinks,\n allAnchorsLocalized,\n inlineScripts,\n externalScriptUrls: allScriptUrls,\n metaTitle,\n metaDescription,\n hasWindowIntlayer,\n };\n }, allLocales);\n\n // Delaying screenshot until we verify the package presence\n\n // robots.txt\n let robotsAccessible = false;\n try {\n const robotsRes = await fetch(new URL('/robots.txt', url).toString());\n robotsAccessible = robotsRes.ok;\n } catch {}\n\n // sitemap.xml\n let sitemapDiscoverable = false;\n let sitemapUrlCount = 0;\n try {\n const sitemapRes = await fetch(new URL('/sitemap.xml', url).toString());\n\n if (sitemapRes.ok) {\n sitemapDiscoverable = true;\n const text = await sitemapRes.text();\n sitemapUrlCount = (text.match(/<loc>/g) || []).length;\n }\n } catch {}\n\n // ── Intlayer detection ────────────────────────────────────────────────────\n const packageDetails: Record<string, string> = {};\n let markerFoundInExternalScript = false;\n\n // Primary: window.intlayer is set — no need to scan bundles\n if (pageDetails.hasWindowIntlayer) {\n // Extract version details from inline scripts while we're here\n for (const script of pageDetails.inlineScripts) {\n Object.assign(packageDetails, extractPackagesFromScript(script));\n }\n } else {\n // Fallback: scan inline scripts\n for (const script of pageDetails.inlineScripts) {\n Object.assign(packageDetails, extractPackagesFromScript(script));\n if (\n !markerFoundInExternalScript &&\n (INTLAYER_BUNDLE_MARKER.test(script) ||\n WINDOW_INTLAYER_PATTERN.test(script))\n ) {\n markerFoundInExternalScript = true;\n }\n }\n\n // Fallback: fetch and scan external bundles\n if (\n !markerFoundInExternalScript &&\n !Object.keys(packageDetails).some((key) => key.includes('intlayer'))\n ) {\n const scriptUrls = pageDetails.externalScriptUrls.slice(\n 0,\n MAX_EXTERNAL_SCRIPTS\n );\n\n await Promise.allSettled(\n scriptUrls.map(async (src) => {\n try {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), 5000);\n\n const res = await fetch(src, {\n signal: controller.signal,\n }).finally(() => clearTimeout(timeoutId));\n\n if (!res.ok) return;\n const contentLength = Number(\n res.headers.get('content-length') || 0\n );\n if (contentLength > MAX_SCRIPT_BYTES) return;\n\n const text = await res.text();\n if (text.length > MAX_SCRIPT_BYTES) return;\n\n if (\n INTLAYER_BUNDLE_MARKER.test(text) ||\n WINDOW_INTLAYER_PATTERN.test(text)\n ) {\n markerFoundInExternalScript = true;\n Object.assign(packageDetails, extractPackagesFromScript(text));\n } else if (text.includes('intlayer')) {\n Object.assign(packageDetails, extractPackagesFromScript(text));\n }\n } catch {}\n })\n );\n }\n }\n\n const hasIntlayer =\n pageDetails.hasWindowIntlayer ||\n Object.keys(packageDetails).some(\n (key) =>\n key.toLowerCase().includes('intlayer') &&\n !key.toLowerCase().includes('@intlayer')\n ) ||\n markerFoundInExternalScript;\n\n const libsUsed = Object.keys(packageDetails);\n const intlayerVersion = packageDetails.intlayer;\n\n // SEO score\n let score = 0;\n if (pageDetails.lang) score += 10;\n if (pageDetails.canonical) score += 10;\n if (pageDetails.hreflangs.length > 0) score += 20;\n if (pageDetails.hreflangs.includes('x-default')) score += 10;\n if (robotsAccessible) score += 10;\n if (sitemapDiscoverable) score += 10;\n if (pageDetails.hasLocalizedLinks) score += 20;\n if (pageDetails.allAnchorsLocalized) score += 10;\n\n let screenshotBuffer: Buffer | undefined;\n\n if (hasIntlayer) {\n // Screenshot (same page, no extra navigation)\n logger.info(`[scanShowcaseProject] Taking screenshot of ${url}...`);\n screenshotBuffer = (await page.screenshot({\n type: 'jpeg',\n quality: 30,\n })) as Buffer;\n }\n\n await page.close();\n\n return {\n hasIntlayer,\n intlayerVersion,\n packageDetails,\n libsUsed,\n screenshotBuffer,\n metaTitle: pageDetails.metaTitle,\n metaDescription: pageDetails.metaDescription,\n scanDetails: {\n score,\n langTag: pageDetails.lang,\n htmlDir: pageDetails.dir,\n hreflangs: pageDetails.hreflangs,\n hasXDefault: pageDetails.hreflangs.includes('x-default'),\n hasCanonical: !!pageDetails.canonical,\n hasLocalizedLinks: pageDetails.hasLocalizedLinks,\n allAnchorsLocalized: pageDetails.allAnchorsLocalized,\n robotsTxt: {\n accessible: robotsAccessible,\n disallowWithoutLocaleAlternates: robotsAccessible,\n },\n sitemapXml: {\n urlsDiscoveredCount: sitemapUrlCount,\n alternatesPresent: sitemapUrlCount > 0,\n xDefaultPresent: sitemapDiscoverable,\n },\n },\n };\n } finally {\n await browser.close().catch(() => {});\n }\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"showcaseUploadScreenshot.service.mjs","names":[],"sources":["../../../../src/services/showcase/showcaseUploadScreenshot.service.ts"],"sourcesContent":["import { DeleteObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';\nimport { getS3Client } from '@utils/s3/s3Client';\n\n/**\n * Derives a deterministic S3 key from a website URL.\n * e.g. \"https://intlayer.org\" → \"screenshots/intlayer_org.jpg\"\n */\nconst getScreenshotKey = (websiteUrl: string, id?: string): string => {\n const { hostname, pathname } = new URL(websiteUrl);\n const slug = (hostname + pathname)\n .replace(/\\/$/, '')\n .replaceAll('.', '_')\n .replace(/[^a-z0-9.\\-_]/gi, '_');\n\n if (id) return `screenshots/${id}/${slug}.jpg`;\n\n return `screenshots/${slug}.jpg`;\n};\n\n/**\n * Uploads a screenshot buffer to S3/R2 and returns the public URL.\n * Uses the website URL as a deterministic key so re-scans overwrite the existing image.\n */\nexport const uploadShowcaseScreenshot = async (\n screenshotBuffer: Buffer,\n websiteUrl: string,\n projectId?: string\n): Promise<string> => {\n const key = getScreenshotKey(websiteUrl, projectId);\n const s3Client = getS3Client();\n\n await s3Client.send(\n new PutObjectCommand({\n Bucket: process.env.S3_BUCKET_NAME,\n Key: key,\n Body: screenshotBuffer,\n ContentType: 'image/jpeg',\n })\n );\n\n return `${process.env.S3_PUBLIC_URL}/${key}`;\n};\n\n/**\n * Deletes a previously uploaded screenshot from S3/R2.\n * Accepts either the full public URL or just the key.\n */\nexport const deleteShowcaseScreenshot = async (\n imageUrl: string\n): Promise<void> => {\n // Extract everything after the S3_PUBLIC_URL prefix, or fall back to the last segment\n const publicUrl = process.env.S3_PUBLIC_URL ?? '';\n const key = imageUrl.startsWith(publicUrl)\n ? imageUrl.slice(publicUrl.length + 1)\n : imageUrl.split('/').pop();\n\n if (!key) return;\n\n const s3Client = getS3Client();\n await s3Client.send(\n new DeleteObjectCommand({\n Bucket: process.env.S3_BUCKET_NAME,\n Key: key,\n })\n );\n};\n"],"mappings":";;;;;;;;AAOA,MAAM,oBAAoB,YAAoB,OAAwB;CACpE,MAAM,EAAE,UAAU,aAAa,IAAI,IAAI,UAAU;CACjD,MAAM,QAAQ,WAAW,SAAQ,CAC9B,QAAQ,OAAO,EAAE,CAAC,CAClB,WAAW,KAAK,GAAG,CAAC,CACpB,QAAQ,mBAAmB,GAAG;CAEjC,IAAI,IAAI,OAAO,eAAe,GAAG,GAAG,KAAK;CAEzC,OAAO,eAAe,KAAK;AAC7B;;;;;AAMA,MAAa,2BAA2B,OACtC,kBACA,YACA,cACoB;CACpB,MAAM,MAAM,iBAAiB,YAAY,SAAS;CAGlD,MAFiB,YAEJ,CAAC,CAAC,KACb,IAAI,iBAAiB;EACnB,QAAQ,QAAQ,IAAI;EACpB,KAAK;EACL,MAAM;EACN,aAAa;CACf,CAAC,CACH;CAEA,OAAO,GAAG,QAAQ,IAAI,cAAc,GAAG;AACzC;;;;;AAMA,MAAa,2BAA2B,OACtC,aACkB;CAElB,MAAM,YAAY,QAAQ,IAAI,iBAAiB;CAC/C,MAAM,MAAM,SAAS,WAAW,SAAS,IACrC,SAAS,MAAM,UAAU,SAAS,CAAC,IACnC,SAAS,MAAM,GAAG,CAAC,CAAC,IAAI;CAE5B,IAAI,CAAC,KAAK;CAGV,MADiB,YACJ,CAAC,CAAC,KACb,IAAI,oBAAoB;EACtB,QAAQ,QAAQ,IAAI;EACpB,KAAK;CACP,CAAC,CACH;AACF"}
1
+ {"version":3,"file":"showcaseUploadScreenshot.service.mjs","names":[],"sources":["../../../../src/services/showcase/showcaseUploadScreenshot.service.ts"],"sourcesContent":["import { DeleteObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';\nimport { getS3Client } from '@utils/s3/s3Client';\n\n/**\n * Derives a deterministic S3 key from a website URL.\n * e.g. \"https://intlayer.org\" → \"screenshots/intlayer_org.jpg\"\n */\nconst getScreenshotKey = (websiteUrl: string, id?: string): string => {\n const { hostname, pathname } = new URL(websiteUrl);\n const slug = (hostname + pathname)\n .replace(/\\/$/, '')\n .replaceAll('.', '_')\n .replace(/[^a-z0-9.\\-_]/gi, '_');\n\n if (id) return `screenshots/${id}/${slug}.jpg`;\n\n return `screenshots/${slug}.jpg`;\n};\n\n/**\n * Uploads a screenshot buffer to S3/R2 and returns the public URL.\n * Uses the website URL as a deterministic key so re-scans overwrite the existing image.\n */\nexport const uploadShowcaseScreenshot = async (\n screenshotBuffer: Buffer,\n websiteUrl: string,\n projectId?: string\n): Promise<string> => {\n const key = getScreenshotKey(websiteUrl, projectId);\n const s3Client = getS3Client();\n\n await s3Client.send(\n new PutObjectCommand({\n Bucket: process.env.S3_BUCKET_NAME,\n Key: key,\n Body: screenshotBuffer,\n ContentType: 'image/jpeg',\n })\n );\n\n return `${process.env.S3_PUBLIC_URL}/${key}`;\n};\n\n/**\n * Deletes a previously uploaded screenshot from S3/R2.\n * Accepts either the full public URL or just the key.\n */\nexport const deleteShowcaseScreenshot = async (\n imageUrl: string\n): Promise<void> => {\n // Extract everything after the S3_PUBLIC_URL prefix, or fall back to the last segment\n const publicUrl = process.env.S3_PUBLIC_URL ?? '';\n const key = imageUrl.startsWith(publicUrl)\n ? imageUrl.slice(publicUrl.length + 1)\n : imageUrl.split('/').pop();\n\n if (!key) return;\n\n const s3Client = getS3Client();\n await s3Client.send(\n new DeleteObjectCommand({\n Bucket: process.env.S3_BUCKET_NAME,\n Key: key,\n })\n );\n};\n"],"mappings":";;;;;;;;AAOA,MAAM,oBAAoB,YAAoB,OAAwB;CACpE,MAAM,EAAE,UAAU,aAAa,IAAI,IAAI,WAAW;CAClD,MAAM,QAAQ,WAAW,UACtB,QAAQ,OAAO,GAAG,CAClB,WAAW,KAAK,IAAI,CACpB,QAAQ,mBAAmB,IAAI;AAElC,KAAI,GAAI,QAAO,eAAe,GAAG,GAAG,KAAK;AAEzC,QAAO,eAAe,KAAK;;;;;;AAO7B,MAAa,2BAA2B,OACtC,kBACA,YACA,cACoB;CACpB,MAAM,MAAM,iBAAiB,YAAY,UAAU;AAGnD,OAFiB,aAEH,CAAC,KACb,IAAI,iBAAiB;EACnB,QAAQ,QAAQ,IAAI;EACpB,KAAK;EACL,MAAM;EACN,aAAa;EACd,CAAC,CACH;AAED,QAAO,GAAG,QAAQ,IAAI,cAAc,GAAG;;;;;;AAOzC,MAAa,2BAA2B,OACtC,aACkB;CAElB,MAAM,YAAY,QAAQ,IAAI,iBAAiB;CAC/C,MAAM,MAAM,SAAS,WAAW,UAAU,GACtC,SAAS,MAAM,UAAU,SAAS,EAAE,GACpC,SAAS,MAAM,IAAI,CAAC,KAAK;AAE7B,KAAI,CAAC,IAAK;AAGV,OADiB,aACH,CAAC,KACb,IAAI,oBAAoB;EACtB,QAAQ,QAAQ,IAAI;EACpB,KAAK;EACN,CAAC,CACH"}
@@ -1 +1 @@
1
- {"version":3,"file":"showcaseVerifyBundle.service.mjs","names":[],"sources":["../../../../src/services/showcase/showcaseVerifyBundle.service.ts"],"sourcesContent":["import { logger } from '@logger';\n\n/**\n * Verifies that a given URL contains a valid Intlayer bundle\n * by scanning the HTML and linked scripts for Intlayer metadata.\n */\nexport const verifyIntlayerBundle = async (url: string): Promise<boolean> => {\n const fetchOptions = {\n headers: {\n 'User-Agent':\n 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',\n },\n };\n\n try {\n const response = await fetch(url, fetchOptions);\n\n if (!response.ok) {\n logger.error(\n `Failed to fetch URL ${url}: ${response.status} ${response.statusText}`\n );\n return false;\n }\n\n const html = await response.text();\n\n // Matches unminified: window.intlayer = { enabled: true }\n // Matches minified: window.intlayer={enabled:!0}\n const windowIdentifierRegex =\n /window\\.intlayer\\s*=\\s*\\{\\s*['\"]?enabled['\"]?\\s*:\\s*(true|!0)\\s*\\}/i;\n\n // Keep the metadata regex as a fallback if you also inject the standard package metadata elsewhere\n const metadataRegex = /['\"]?name['\"]?\\s*:\\s*['\"]Intlayer['\"]/i;\n\n if (windowIdentifierRegex.test(html) || metadataRegex.test(html)) {\n return true;\n }\n\n // Improved script detection: finds src=\"...\" or src='...' or src=...\n // and also handles different extensions or no extensions\n const scriptSrcRegex =\n /(?:src|href)=\\s*['\"]?([^'\"\\s>]+(?:\\.js|\\.mjs|\\.cjs)(?:\\?[^'\"\\s>]*)?)['\"]?/gi;\n\n const scriptUrls: string[] = [];\n\n // Use matchAll to avoid assignments in expressions (Biome lint)\n for (const match of html.matchAll(scriptSrcRegex)) {\n if (match[1]) {\n scriptUrls.push(match[1]);\n }\n }\n\n // Also look for imports in scripts or inline blocks\n const importRegex =\n /import\\s*\\(?\\s*['\"]?([^'\"\\s>]+\\.(?:js|mjs|cjs)[^'\"\\s>]*)['\"]?\\s*\\)?/gi;\n for (const match of html.matchAll(importRegex)) {\n if (match[1]) {\n scriptUrls.push(match[1]);\n }\n }\n\n // Look for any JS files referenced in string literals, like arrays of preloads in HTML\n const jsStringRegex =\n /['\"]([^'\"\\s>]+(?:\\.js|\\.mjs|\\.cjs)(?:\\?[^'\"\\s>]*)?)['\"]/gi;\n\n for (const match of html.matchAll(jsStringRegex)) {\n if (match[1]) {\n scriptUrls.push(match[1]);\n }\n }\n\n const uniqueScriptUrls = Array.from(new Set(scriptUrls));\n const visitedUrls = new Set<string>();\n const MAX_URLS_TO_CHECK = 50;\n\n for (let i = 0; i < uniqueScriptUrls.length && i < MAX_URLS_TO_CHECK; i++) {\n const scriptSrc = uniqueScriptUrls[i];\n try {\n const scriptUrl = new URL(scriptSrc, url).toString();\n\n if (visitedUrls.has(scriptUrl)) continue;\n visitedUrls.add(scriptUrl);\n\n const scriptResponse = await fetch(scriptUrl, fetchOptions);\n\n if (!scriptResponse.ok) continue;\n\n const scriptText = await scriptResponse.text();\n if (\n windowIdentifierRegex.test(scriptText) ||\n metadataRegex.test(scriptText)\n ) {\n return true;\n }\n\n // If not found here, maybe this is an entry file that imports chunks.\n // We can look for strings that are `.js` files from this text, and append them\n // to `uniqueScriptUrls` to be checked in subsequent iterations.\n for (const match of scriptText.matchAll(jsStringRegex)) {\n const newSrc = match[1];\n if (newSrc && !uniqueScriptUrls.includes(newSrc)) {\n uniqueScriptUrls.push(newSrc);\n }\n }\n } catch (error) {\n // ignore cross-origin or unreachable scripts\n logger.debug(`Error fetching script ${scriptSrc} for ${url}:`, error);\n }\n }\n } catch (error) {\n logger.error(`Error verifying bundle for ${url}:`, error);\n }\n\n return false;\n};\n"],"mappings":";;;;;;;AAMA,MAAa,uBAAuB,OAAO,QAAkC;CAC3E,MAAM,eAAe,EACnB,SAAS,EACP,cACE,wHACJ,EACF;CAEA,IAAI;EACF,MAAM,WAAW,MAAM,MAAM,KAAK,YAAY;EAE9C,IAAI,CAAC,SAAS,IAAI;GAChB,OAAO,MACL,uBAAuB,IAAI,IAAI,SAAS,OAAO,GAAG,SAAS,YAC7D;GACA,OAAO;EACT;EAEA,MAAM,OAAO,MAAM,SAAS,KAAK;EAIjC,MAAM,wBACJ;EAGF,MAAM,gBAAgB;EAEtB,IAAI,sBAAsB,KAAK,IAAI,KAAK,cAAc,KAAK,IAAI,GAC7D,OAAO;EAKT,MAAM,iBACJ;EAEF,MAAM,aAAuB,CAAC;EAG9B,KAAK,MAAM,SAAS,KAAK,SAAS,cAAc,GAC9C,IAAI,MAAM,IACR,WAAW,KAAK,MAAM,EAAE;EAO5B,KAAK,MAAM,SAAS,KAAK,SAAS,uEAAW,GAC3C,IAAI,MAAM,IACR,WAAW,KAAK,MAAM,EAAE;EAK5B,MAAM,gBACJ;EAEF,KAAK,MAAM,SAAS,KAAK,SAAS,aAAa,GAC7C,IAAI,MAAM,IACR,WAAW,KAAK,MAAM,EAAE;EAI5B,MAAM,mBAAmB,MAAM,KAAK,IAAI,IAAI,UAAU,CAAC;EACvD,MAAM,8BAAc,IAAI,IAAY;EACpC,MAAM,oBAAoB;EAE1B,KAAK,IAAI,IAAI,GAAG,IAAI,iBAAiB,UAAU,IAAI,mBAAmB,KAAK;GACzE,MAAM,YAAY,iBAAiB;GACnC,IAAI;IACF,MAAM,YAAY,IAAI,IAAI,WAAW,GAAG,CAAC,CAAC,SAAS;IAEnD,IAAI,YAAY,IAAI,SAAS,GAAG;IAChC,YAAY,IAAI,SAAS;IAEzB,MAAM,iBAAiB,MAAM,MAAM,WAAW,YAAY;IAE1D,IAAI,CAAC,eAAe,IAAI;IAExB,MAAM,aAAa,MAAM,eAAe,KAAK;IAC7C,IACE,sBAAsB,KAAK,UAAU,KACrC,cAAc,KAAK,UAAU,GAE7B,OAAO;IAMT,KAAK,MAAM,SAAS,WAAW,SAAS,aAAa,GAAG;KACtD,MAAM,SAAS,MAAM;KACrB,IAAI,UAAU,CAAC,iBAAiB,SAAS,MAAM,GAC7C,iBAAiB,KAAK,MAAM;IAEhC;GACF,SAAS,OAAO;IAEd,OAAO,MAAM,yBAAyB,UAAU,OAAO,IAAI,IAAI,KAAK;GACtE;EACF;CACF,SAAS,OAAO;EACd,OAAO,MAAM,8BAA8B,IAAI,IAAI,KAAK;CAC1D;CAEA,OAAO;AACT"}
1
+ {"version":3,"file":"showcaseVerifyBundle.service.mjs","names":[],"sources":["../../../../src/services/showcase/showcaseVerifyBundle.service.ts"],"sourcesContent":["import { logger } from '@logger';\n\n/**\n * Verifies that a given URL contains a valid Intlayer bundle\n * by scanning the HTML and linked scripts for Intlayer metadata.\n */\nexport const verifyIntlayerBundle = async (url: string): Promise<boolean> => {\n const fetchOptions = {\n headers: {\n 'User-Agent':\n 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',\n },\n };\n\n try {\n const response = await fetch(url, fetchOptions);\n\n if (!response.ok) {\n logger.error(\n `Failed to fetch URL ${url}: ${response.status} ${response.statusText}`\n );\n return false;\n }\n\n const html = await response.text();\n\n // Matches unminified: window.intlayer = { enabled: true }\n // Matches minified: window.intlayer={enabled:!0}\n const windowIdentifierRegex =\n /window\\.intlayer\\s*=\\s*\\{\\s*['\"]?enabled['\"]?\\s*:\\s*(true|!0)\\s*\\}/i;\n\n // Keep the metadata regex as a fallback if you also inject the standard package metadata elsewhere\n const metadataRegex = /['\"]?name['\"]?\\s*:\\s*['\"]Intlayer['\"]/i;\n\n if (windowIdentifierRegex.test(html) || metadataRegex.test(html)) {\n return true;\n }\n\n // Improved script detection: finds src=\"...\" or src='...' or src=...\n // and also handles different extensions or no extensions\n const scriptSrcRegex =\n /(?:src|href)=\\s*['\"]?([^'\"\\s>]+(?:\\.js|\\.mjs|\\.cjs)(?:\\?[^'\"\\s>]*)?)['\"]?/gi;\n\n const scriptUrls: string[] = [];\n\n // Use matchAll to avoid assignments in expressions (Biome lint)\n for (const match of html.matchAll(scriptSrcRegex)) {\n if (match[1]) {\n scriptUrls.push(match[1]);\n }\n }\n\n // Also look for imports in scripts or inline blocks\n const importRegex =\n /import\\s*\\(?\\s*['\"]?([^'\"\\s>]+\\.(?:js|mjs|cjs)[^'\"\\s>]*)['\"]?\\s*\\)?/gi;\n for (const match of html.matchAll(importRegex)) {\n if (match[1]) {\n scriptUrls.push(match[1]);\n }\n }\n\n // Look for any JS files referenced in string literals, like arrays of preloads in HTML\n const jsStringRegex =\n /['\"]([^'\"\\s>]+(?:\\.js|\\.mjs|\\.cjs)(?:\\?[^'\"\\s>]*)?)['\"]/gi;\n\n for (const match of html.matchAll(jsStringRegex)) {\n if (match[1]) {\n scriptUrls.push(match[1]);\n }\n }\n\n const uniqueScriptUrls = Array.from(new Set(scriptUrls));\n const visitedUrls = new Set<string>();\n const MAX_URLS_TO_CHECK = 50;\n\n for (let i = 0; i < uniqueScriptUrls.length && i < MAX_URLS_TO_CHECK; i++) {\n const scriptSrc = uniqueScriptUrls[i];\n try {\n const scriptUrl = new URL(scriptSrc, url).toString();\n\n if (visitedUrls.has(scriptUrl)) continue;\n visitedUrls.add(scriptUrl);\n\n const scriptResponse = await fetch(scriptUrl, fetchOptions);\n\n if (!scriptResponse.ok) continue;\n\n const scriptText = await scriptResponse.text();\n if (\n windowIdentifierRegex.test(scriptText) ||\n metadataRegex.test(scriptText)\n ) {\n return true;\n }\n\n // If not found here, maybe this is an entry file that imports chunks.\n // We can look for strings that are `.js` files from this text, and append them\n // to `uniqueScriptUrls` to be checked in subsequent iterations.\n for (const match of scriptText.matchAll(jsStringRegex)) {\n const newSrc = match[1];\n if (newSrc && !uniqueScriptUrls.includes(newSrc)) {\n uniqueScriptUrls.push(newSrc);\n }\n }\n } catch (error) {\n // ignore cross-origin or unreachable scripts\n logger.debug(`Error fetching script ${scriptSrc} for ${url}:`, error);\n }\n }\n } catch (error) {\n logger.error(`Error verifying bundle for ${url}:`, error);\n }\n\n return false;\n};\n"],"mappings":";;;;;;;AAMA,MAAa,uBAAuB,OAAO,QAAkC;CAC3E,MAAM,eAAe,EACnB,SAAS,EACP,cACE,yHACH,EACF;AAED,KAAI;EACF,MAAM,WAAW,MAAM,MAAM,KAAK,aAAa;AAE/C,MAAI,CAAC,SAAS,IAAI;AAChB,UAAO,MACL,uBAAuB,IAAI,IAAI,SAAS,OAAO,GAAG,SAAS,aAC5D;AACD,UAAO;;EAGT,MAAM,OAAO,MAAM,SAAS,MAAM;EAIlC,MAAM,wBACJ;EAGF,MAAM,gBAAgB;AAEtB,MAAI,sBAAsB,KAAK,KAAK,IAAI,cAAc,KAAK,KAAK,CAC9D,QAAO;EAKT,MAAM,iBACJ;EAEF,MAAM,aAAuB,EAAE;AAG/B,OAAK,MAAM,SAAS,KAAK,SAAS,eAAe,CAC/C,KAAI,MAAM,GACR,YAAW,KAAK,MAAM,GAAG;AAO7B,OAAK,MAAM,SAAS,KAAK,SAAS,wEAAY,CAC5C,KAAI,MAAM,GACR,YAAW,KAAK,MAAM,GAAG;EAK7B,MAAM,gBACJ;AAEF,OAAK,MAAM,SAAS,KAAK,SAAS,cAAc,CAC9C,KAAI,MAAM,GACR,YAAW,KAAK,MAAM,GAAG;EAI7B,MAAM,mBAAmB,MAAM,KAAK,IAAI,IAAI,WAAW,CAAC;EACxD,MAAM,8BAAc,IAAI,KAAa;EACrC,MAAM,oBAAoB;AAE1B,OAAK,IAAI,IAAI,GAAG,IAAI,iBAAiB,UAAU,IAAI,mBAAmB,KAAK;GACzE,MAAM,YAAY,iBAAiB;AACnC,OAAI;IACF,MAAM,YAAY,IAAI,IAAI,WAAW,IAAI,CAAC,UAAU;AAEpD,QAAI,YAAY,IAAI,UAAU,CAAE;AAChC,gBAAY,IAAI,UAAU;IAE1B,MAAM,iBAAiB,MAAM,MAAM,WAAW,aAAa;AAE3D,QAAI,CAAC,eAAe,GAAI;IAExB,MAAM,aAAa,MAAM,eAAe,MAAM;AAC9C,QACE,sBAAsB,KAAK,WAAW,IACtC,cAAc,KAAK,WAAW,CAE9B,QAAO;AAMT,SAAK,MAAM,SAAS,WAAW,SAAS,cAAc,EAAE;KACtD,MAAM,SAAS,MAAM;AACrB,SAAI,UAAU,CAAC,iBAAiB,SAAS,OAAO,CAC9C,kBAAiB,KAAK,OAAO;;YAG1B,OAAO;AAEd,WAAO,MAAM,yBAAyB,UAAU,OAAO,IAAI,IAAI,MAAM;;;UAGlE,OAAO;AACd,SAAO,MAAM,8BAA8B,IAAI,IAAI,MAAM;;AAG3D,QAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"showcaseVerifyGithub.service.mjs","names":[],"sources":["../../../../src/services/showcase/showcaseVerifyGithub.service.ts"],"sourcesContent":["import { logger } from '@logger';\nimport { Octokit } from '@octokit/rest';\n\nconst MAX_PACKAGE_JSON_FILES = 20;\n\nexport interface GithubVerifyResult {\n /** Whether the repo contains at least one intlayer-related package */\n isValid: boolean;\n /** Version of the core `intlayer` package if found */\n intlayerVersion?: string;\n /** All detected intlayer-related packages and their versions */\n packageDetails: Record<string, string>;\n}\n\n/** Intlayer package names we care about */\nconst INTLAYER_PKG_PATTERN =\n /^(@intlayer\\/[\\w-]+|intlayer|react-intlayer|next-intlayer|vue-intlayer|svelte-intlayer|express-intlayer|intlayer-editor)$/;\n\n/**\n * Parses a GitHub-like URL and returns { hostname, owner, repo }.\n * Supports:\n * - https://github.com/owner/repo\n * - https://github.example.com/owner/repo (self-hosted GitHub Enterprise)\n * - https://github.com/owner/repo.git\n * - paths with trailing slashes\n */\nexport const parseGithubUrl = (\n url: string\n): { hostname: string; owner: string; repo: string } | null => {\n try {\n const parsed = new URL(url);\n const parts = parsed.pathname\n .replace(/\\.git\\/?$/, '')\n .split('/')\n .filter(Boolean);\n if (parts.length < 2) return null;\n return {\n hostname: parsed.hostname,\n owner: parts[0],\n repo: parts[1],\n };\n } catch {\n return null;\n }\n};\n\n/**\n * Returns an Octokit client configured for the given hostname.\n * - github.com → standard GitHub API\n * - anything else → GitHub Enterprise Server at https://<hostname>/api/v3\n */\nconst getOctokitForHost = (hostname: string): Octokit => {\n const isGithubCom = hostname === 'github.com';\n return new Octokit({\n auth: process.env.GITHUB_TOKEN,\n headers: {\n 'X-GitHub-Api-Version': '2026-03-10',\n },\n ...(isGithubCom ? {} : { baseUrl: `https://${hostname}/api/v3` }),\n });\n};\n\n/**\n * Collects all intlayer-related package names + versions found in a parsed package.json.\n */\nexport const extractIntlayerPackages = (\n pkg: Record<string, unknown>\n): Record<string, string> => {\n const result: Record<string, string> = {};\n for (const depField of [\n 'dependencies',\n 'devDependencies',\n 'peerDependencies',\n ] as const) {\n const deps = pkg[depField];\n if (deps && typeof deps === 'object') {\n for (const [name, version] of Object.entries(\n deps as Record<string, string>\n )) {\n if (INTLAYER_PKG_PATTERN.test(name)) {\n const strVersion = String(version);\n\n if (strVersion === 'workspace:*') {\n result[name] = 'latest';\n } else {\n result[name] = strVersion;\n }\n }\n }\n }\n }\n return result;\n};\n\n/**\n * Verifies that a repository uses Intlayer by inspecting all package.json files\n * (root + nested, up to MAX_PACKAGE_JSON_FILES).\n *\n * Returns null if the URL cannot be parsed or the API call fails outright.\n */\nexport const verifyGithubRepo = async (\n githubUrl: string\n): Promise<GithubVerifyResult | null> => {\n const parsed = parseGithubUrl(githubUrl);\n if (!parsed) {\n logger.warn(`[verifyGithubRepo] Could not parse URL: ${githubUrl}`);\n return null;\n }\n\n const { hostname, owner, repo } = parsed;\n const octokit = getOctokitForHost(hostname);\n\n try {\n // Get default branch\n const repoInfo = await octokit.rest.repos.get({ owner, repo });\n const defaultBranch = repoInfo.data.default_branch;\n\n // Get the full recursive tree\n const treeResponse = await octokit.rest.git.getTree({\n owner,\n repo,\n tree_sha: defaultBranch,\n recursive: '1',\n });\n\n if (treeResponse.data.truncated) {\n logger.warn(`[verifyGithubRepo] Tree is truncated for ${owner}/${repo}`);\n }\n\n // 3. Find all package.json paths (root + nested)\n const packageJsonPaths = treeResponse.data.tree\n .filter(\n (item) =>\n item.type === 'blob' &&\n (item.path === 'package.json' || item.path?.endsWith('/package.json'))\n )\n .map((item) => item.path!)\n .slice(0, MAX_PACKAGE_JSON_FILES);\n\n if (packageJsonPaths.length === 0) {\n return { isValid: false, packageDetails: {} };\n }\n\n // 4. Fetch and parse each package.json\n const allPackageDetails: Record<string, string> = {};\n\n await Promise.allSettled(\n packageJsonPaths.map(async (pkgPath) => {\n try {\n const response = await octokit.rest.repos.getContent({\n owner,\n repo,\n path: pkgPath,\n headers: {\n 'X-GitHub-Api-Version': '2026-03-10',\n },\n });\n\n if (!Array.isArray(response.data) && 'content' in response.data) {\n const content = Buffer.from(\n response.data.content,\n 'base64'\n ).toString('utf-8');\n\n const pkg = JSON.parse(content) as Record<string, unknown>;\n const found = extractIntlayerPackages(pkg);\n\n Object.assign(allPackageDetails, found);\n }\n } catch (e) {\n logger.debug(\n `[verifyGithubRepo] Could not read ${pkgPath}: ${e instanceof Error ? e.message : String(e)}`\n );\n }\n })\n );\n\n const isValid = Object.keys(allPackageDetails).length > 0;\n const intlayerVersion = allPackageDetails.intlayer;\n\n return { isValid, intlayerVersion, packageDetails: allPackageDetails };\n } catch (e) {\n logger.error(\n `[verifyGithubRepo] Error for ${owner}/${repo}: ${e instanceof Error ? e.message : String(e)}`\n );\n return null;\n }\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"showcaseVerifyGithub.service.mjs","names":[],"sources":["../../../../src/services/showcase/showcaseVerifyGithub.service.ts"],"sourcesContent":["import { logger } from '@logger';\nimport { Octokit } from '@octokit/rest';\n\nconst MAX_PACKAGE_JSON_FILES = 20;\n\nexport interface GithubVerifyResult {\n /** Whether the repo contains at least one intlayer-related package */\n isValid: boolean;\n /** Version of the core `intlayer` package if found */\n intlayerVersion?: string;\n /** All detected intlayer-related packages and their versions */\n packageDetails: Record<string, string>;\n}\n\n/** Intlayer package names we care about */\nconst INTLAYER_PKG_PATTERN =\n /^(@intlayer\\/[\\w-]+|intlayer|react-intlayer|next-intlayer|vue-intlayer|svelte-intlayer|express-intlayer|intlayer-editor)$/;\n\n/**\n * Parses a GitHub-like URL and returns { hostname, owner, repo }.\n * Supports:\n * - https://github.com/owner/repo\n * - https://github.example.com/owner/repo (self-hosted GitHub Enterprise)\n * - https://github.com/owner/repo.git\n * - paths with trailing slashes\n */\nexport const parseGithubUrl = (\n url: string\n): { hostname: string; owner: string; repo: string } | null => {\n try {\n const parsed = new URL(url);\n const parts = parsed.pathname\n .replace(/\\.git\\/?$/, '')\n .split('/')\n .filter(Boolean);\n if (parts.length < 2) return null;\n return {\n hostname: parsed.hostname,\n owner: parts[0],\n repo: parts[1],\n };\n } catch {\n return null;\n }\n};\n\n/**\n * Returns an Octokit client configured for the given hostname.\n * - github.com → standard GitHub API\n * - anything else → GitHub Enterprise Server at https://<hostname>/api/v3\n */\nconst getOctokitForHost = (hostname: string): Octokit => {\n const isGithubCom = hostname === 'github.com';\n return new Octokit({\n auth: process.env.GITHUB_TOKEN,\n headers: {\n 'X-GitHub-Api-Version': '2026-03-10',\n },\n ...(isGithubCom ? {} : { baseUrl: `https://${hostname}/api/v3` }),\n });\n};\n\n/**\n * Collects all intlayer-related package names + versions found in a parsed package.json.\n */\nexport const extractIntlayerPackages = (\n pkg: Record<string, unknown>\n): Record<string, string> => {\n const result: Record<string, string> = {};\n for (const depField of [\n 'dependencies',\n 'devDependencies',\n 'peerDependencies',\n ] as const) {\n const deps = pkg[depField];\n if (deps && typeof deps === 'object') {\n for (const [name, version] of Object.entries(\n deps as Record<string, string>\n )) {\n if (INTLAYER_PKG_PATTERN.test(name)) {\n const strVersion = String(version);\n\n if (strVersion === 'workspace:*') {\n result[name] = 'latest';\n } else {\n result[name] = strVersion;\n }\n }\n }\n }\n }\n return result;\n};\n\n/**\n * Verifies that a repository uses Intlayer by inspecting all package.json files\n * (root + nested, up to MAX_PACKAGE_JSON_FILES).\n *\n * Returns null if the URL cannot be parsed or the API call fails outright.\n */\nexport const verifyGithubRepo = async (\n githubUrl: string\n): Promise<GithubVerifyResult | null> => {\n const parsed = parseGithubUrl(githubUrl);\n if (!parsed) {\n logger.warn(`[verifyGithubRepo] Could not parse URL: ${githubUrl}`);\n return null;\n }\n\n const { hostname, owner, repo } = parsed;\n const octokit = getOctokitForHost(hostname);\n\n try {\n // Get default branch\n const repoInfo = await octokit.rest.repos.get({ owner, repo });\n const defaultBranch = repoInfo.data.default_branch;\n\n // Get the full recursive tree\n const treeResponse = await octokit.rest.git.getTree({\n owner,\n repo,\n tree_sha: defaultBranch,\n recursive: '1',\n });\n\n if (treeResponse.data.truncated) {\n logger.warn(`[verifyGithubRepo] Tree is truncated for ${owner}/${repo}`);\n }\n\n // 3. Find all package.json paths (root + nested)\n const packageJsonPaths = treeResponse.data.tree\n .filter(\n (item) =>\n item.type === 'blob' &&\n (item.path === 'package.json' || item.path?.endsWith('/package.json'))\n )\n .map((item) => item.path!)\n .slice(0, MAX_PACKAGE_JSON_FILES);\n\n if (packageJsonPaths.length === 0) {\n return { isValid: false, packageDetails: {} };\n }\n\n // 4. Fetch and parse each package.json\n const allPackageDetails: Record<string, string> = {};\n\n await Promise.allSettled(\n packageJsonPaths.map(async (pkgPath) => {\n try {\n const response = await octokit.rest.repos.getContent({\n owner,\n repo,\n path: pkgPath,\n headers: {\n 'X-GitHub-Api-Version': '2026-03-10',\n },\n });\n\n if (!Array.isArray(response.data) && 'content' in response.data) {\n const content = Buffer.from(\n response.data.content,\n 'base64'\n ).toString('utf-8');\n\n const pkg = JSON.parse(content) as Record<string, unknown>;\n const found = extractIntlayerPackages(pkg);\n\n Object.assign(allPackageDetails, found);\n }\n } catch (e) {\n logger.debug(\n `[verifyGithubRepo] Could not read ${pkgPath}: ${e instanceof Error ? e.message : String(e)}`\n );\n }\n })\n );\n\n const isValid = Object.keys(allPackageDetails).length > 0;\n const intlayerVersion = allPackageDetails.intlayer;\n\n return { isValid, intlayerVersion, packageDetails: allPackageDetails };\n } catch (e) {\n logger.error(\n `[verifyGithubRepo] Error for ${owner}/${repo}: ${e instanceof Error ? e.message : String(e)}`\n );\n return null;\n }\n};\n"],"mappings":"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"}