@m5kdev/backend 0.1.4 → 0.2.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 (319) hide show
  1. package/dist/src/lib/posthog.d.ts +0 -1
  2. package/dist/src/lib/sentry.d.ts +0 -1
  3. package/dist/src/modules/access/access.repository.d.ts +0 -1
  4. package/dist/src/modules/access/access.service.d.ts +0 -1
  5. package/dist/src/modules/access/access.test.d.ts +0 -1
  6. package/dist/src/modules/access/access.utils.d.ts +0 -1
  7. package/dist/src/modules/ai/ai.db.d.ts +0 -1
  8. package/dist/src/modules/ai/ai.prompt.d.ts +0 -1
  9. package/dist/src/modules/ai/ai.repository.d.ts +0 -1
  10. package/dist/src/modules/ai/ai.router.d.ts +0 -1
  11. package/dist/src/modules/ai/ai.service.d.ts +0 -1
  12. package/dist/src/modules/ai/ai.trpc.d.ts +5 -42
  13. package/dist/src/modules/ai/ai.trpc.js +5 -5
  14. package/dist/src/modules/ai/ideogram/ideogram.constants.d.ts +0 -1
  15. package/dist/src/modules/ai/ideogram/ideogram.dto.d.ts +0 -1
  16. package/dist/src/modules/ai/ideogram/ideogram.prompt.d.ts +0 -1
  17. package/dist/src/modules/ai/ideogram/ideogram.repository.d.ts +0 -1
  18. package/dist/src/modules/ai/ideogram/ideogram.service.d.ts +0 -1
  19. package/dist/src/modules/auth/auth.db.d.ts +0 -1
  20. package/dist/src/modules/auth/auth.dto.d.ts +7 -8
  21. package/dist/src/modules/auth/auth.lib.d.ts +8 -9
  22. package/dist/src/modules/auth/auth.middleware.d.ts +0 -1
  23. package/dist/src/modules/auth/auth.repository.d.ts +0 -1
  24. package/dist/src/modules/auth/auth.service.d.ts +0 -1
  25. package/dist/src/modules/auth/auth.trpc.d.ts +52 -89
  26. package/dist/src/modules/auth/auth.trpc.js +55 -53
  27. package/dist/src/modules/auth/auth.utils.d.ts +0 -1
  28. package/dist/src/modules/base/base.abstract.d.ts +0 -1
  29. package/dist/src/modules/base/base.dto.d.ts +2 -3
  30. package/dist/src/modules/base/base.grants.d.ts +0 -1
  31. package/dist/src/modules/base/base.grants.test.d.ts +0 -1
  32. package/dist/src/modules/base/base.repository.d.ts +0 -1
  33. package/dist/src/modules/base/base.service.d.ts +2 -4
  34. package/dist/src/modules/base/base.types.d.ts +0 -1
  35. package/dist/src/modules/billing/billing.db.d.ts +0 -1
  36. package/dist/src/modules/billing/billing.repository.d.ts +0 -1
  37. package/dist/src/modules/billing/billing.router.d.ts +0 -1
  38. package/dist/src/modules/billing/billing.service.d.ts +0 -1
  39. package/dist/src/modules/billing/billing.trpc.d.ts +6 -43
  40. package/dist/src/modules/billing/billing.trpc.js +7 -7
  41. package/dist/src/modules/clay/clay.repository.d.ts +0 -1
  42. package/dist/src/modules/clay/clay.service.d.ts +0 -1
  43. package/dist/src/modules/connect/connect.db.d.ts +0 -1
  44. package/dist/src/modules/connect/connect.dto.d.ts +8 -9
  45. package/dist/src/modules/connect/connect.linkedin.d.ts +0 -1
  46. package/dist/src/modules/connect/connect.oauth.d.ts +0 -1
  47. package/dist/src/modules/connect/connect.repository.d.ts +3 -4
  48. package/dist/src/modules/connect/connect.router.d.ts +0 -1
  49. package/dist/src/modules/connect/connect.service.d.ts +6 -7
  50. package/dist/src/modules/connect/connect.trpc.d.ts +9 -46
  51. package/dist/src/modules/connect/connect.trpc.js +7 -7
  52. package/dist/src/modules/connect/connect.types.d.ts +0 -1
  53. package/dist/src/modules/crypto/crypto.db.d.ts +0 -1
  54. package/dist/src/modules/crypto/crypto.repository.d.ts +0 -1
  55. package/dist/src/modules/crypto/crypto.service.d.ts +0 -1
  56. package/dist/src/modules/email/email.service.d.ts +0 -1
  57. package/dist/src/modules/file/file.repository.d.ts +0 -1
  58. package/dist/src/modules/file/file.router.d.ts +0 -1
  59. package/dist/src/modules/file/file.service.d.ts +0 -1
  60. package/dist/src/modules/recurrence/recurrence.db.d.ts +0 -1
  61. package/dist/src/modules/recurrence/recurrence.repository.d.ts +0 -1
  62. package/dist/src/modules/recurrence/recurrence.service.d.ts +0 -1
  63. package/dist/src/modules/recurrence/recurrence.trpc.d.ts +11 -48
  64. package/dist/src/modules/recurrence/recurrence.trpc.js +17 -17
  65. package/dist/src/modules/social/social.dto.d.ts +0 -1
  66. package/dist/src/modules/social/social.linkedin.d.ts +0 -1
  67. package/dist/src/modules/social/social.linkedin.test.d.ts +0 -1
  68. package/dist/src/modules/social/social.service.d.ts +0 -1
  69. package/dist/src/modules/social/social.types.d.ts +0 -1
  70. package/dist/src/modules/tag/tag.db.d.ts +0 -1
  71. package/dist/src/modules/tag/tag.dto.d.ts +0 -1
  72. package/dist/src/modules/tag/tag.repository.d.ts +0 -1
  73. package/dist/src/modules/tag/tag.service.d.ts +0 -1
  74. package/dist/src/modules/tag/tag.trpc.d.ts +10 -47
  75. package/dist/src/modules/tag/tag.trpc.js +15 -15
  76. package/dist/src/modules/utils/applyPagination.d.ts +0 -1
  77. package/dist/src/modules/utils/applySorting.d.ts +0 -1
  78. package/dist/src/modules/utils/getConditionsFromFilters.d.ts +0 -1
  79. package/dist/src/modules/video/video.service.d.ts +0 -1
  80. package/dist/src/modules/webhook/webhook.constants.d.ts +0 -1
  81. package/dist/src/modules/webhook/webhook.db.d.ts +0 -1
  82. package/dist/src/modules/webhook/webhook.dto.d.ts +0 -1
  83. package/dist/src/modules/webhook/webhook.repository.d.ts +0 -1
  84. package/dist/src/modules/webhook/webhook.router.d.ts +0 -1
  85. package/dist/src/modules/webhook/webhook.service.d.ts +0 -1
  86. package/dist/src/modules/workflow/workflow.db.d.ts +0 -1
  87. package/dist/src/modules/workflow/workflow.repository.d.ts +0 -1
  88. package/dist/src/modules/workflow/workflow.service.d.ts +0 -1
  89. package/dist/src/modules/workflow/workflow.trpc.d.ts +6 -43
  90. package/dist/src/modules/workflow/workflow.trpc.js +7 -7
  91. package/dist/src/modules/workflow/workflow.types.d.ts +0 -1
  92. package/dist/src/modules/workflow/workflow.utils.d.ts +0 -1
  93. package/dist/src/test/stubs/utils.d.ts +0 -1
  94. package/dist/src/trpc/context.d.ts +4 -5
  95. package/dist/src/trpc/index.d.ts +0 -1
  96. package/dist/src/trpc/procedures.d.ts +24 -25
  97. package/dist/src/trpc/utils.d.ts +0 -1
  98. package/dist/src/types.d.ts +61 -209
  99. package/dist/src/types.js +4 -5
  100. package/dist/src/utils/errors.d.ts +0 -1
  101. package/dist/src/utils/logger.d.ts +0 -1
  102. package/dist/src/utils/posthog.d.ts +0 -1
  103. package/dist/src/utils/trpc.d.ts +58 -0
  104. package/dist/src/utils/trpc.js +63 -0
  105. package/dist/src/utils/types.d.ts +0 -1
  106. package/dist/tsconfig.tsbuildinfo +1 -1
  107. package/package.json +6 -8
  108. package/.cursor/rules/backend.mdc +0 -70
  109. package/.turbo/turbo-build.log +0 -5
  110. package/.turbo/turbo-check-types.log +0 -5
  111. package/.turbo/turbo-lint$colon$fix.log +0 -255
  112. package/CHANGELOG.md +0 -37
  113. package/dist/src/lib/posthog.d.ts.map +0 -1
  114. package/dist/src/lib/sentry.d.ts.map +0 -1
  115. package/dist/src/modules/access/access.repository.d.ts.map +0 -1
  116. package/dist/src/modules/access/access.service.d.ts.map +0 -1
  117. package/dist/src/modules/access/access.test.d.ts.map +0 -1
  118. package/dist/src/modules/access/access.utils.d.ts.map +0 -1
  119. package/dist/src/modules/ai/ai.db.d.ts.map +0 -1
  120. package/dist/src/modules/ai/ai.prompt.d.ts.map +0 -1
  121. package/dist/src/modules/ai/ai.repository.d.ts.map +0 -1
  122. package/dist/src/modules/ai/ai.router.d.ts.map +0 -1
  123. package/dist/src/modules/ai/ai.service.d.ts.map +0 -1
  124. package/dist/src/modules/ai/ai.trpc.d.ts.map +0 -1
  125. package/dist/src/modules/ai/ideogram/ideogram.constants.d.ts.map +0 -1
  126. package/dist/src/modules/ai/ideogram/ideogram.dto.d.ts.map +0 -1
  127. package/dist/src/modules/ai/ideogram/ideogram.prompt.d.ts.map +0 -1
  128. package/dist/src/modules/ai/ideogram/ideogram.repository.d.ts.map +0 -1
  129. package/dist/src/modules/ai/ideogram/ideogram.service.d.ts.map +0 -1
  130. package/dist/src/modules/auth/auth.db.d.ts.map +0 -1
  131. package/dist/src/modules/auth/auth.dto.d.ts.map +0 -1
  132. package/dist/src/modules/auth/auth.lib.d.ts.map +0 -1
  133. package/dist/src/modules/auth/auth.middleware.d.ts.map +0 -1
  134. package/dist/src/modules/auth/auth.repository.d.ts.map +0 -1
  135. package/dist/src/modules/auth/auth.service.d.ts.map +0 -1
  136. package/dist/src/modules/auth/auth.trpc.d.ts.map +0 -1
  137. package/dist/src/modules/auth/auth.utils.d.ts.map +0 -1
  138. package/dist/src/modules/base/base.abstract.d.ts.map +0 -1
  139. package/dist/src/modules/base/base.dto.d.ts.map +0 -1
  140. package/dist/src/modules/base/base.grants.d.ts.map +0 -1
  141. package/dist/src/modules/base/base.grants.test.d.ts.map +0 -1
  142. package/dist/src/modules/base/base.repository.d.ts.map +0 -1
  143. package/dist/src/modules/base/base.service.d.ts.map +0 -1
  144. package/dist/src/modules/base/base.types.d.ts.map +0 -1
  145. package/dist/src/modules/billing/billing.db.d.ts.map +0 -1
  146. package/dist/src/modules/billing/billing.repository.d.ts.map +0 -1
  147. package/dist/src/modules/billing/billing.router.d.ts.map +0 -1
  148. package/dist/src/modules/billing/billing.service.d.ts.map +0 -1
  149. package/dist/src/modules/billing/billing.trpc.d.ts.map +0 -1
  150. package/dist/src/modules/clay/clay.repository.d.ts.map +0 -1
  151. package/dist/src/modules/clay/clay.service.d.ts.map +0 -1
  152. package/dist/src/modules/connect/connect.db.d.ts.map +0 -1
  153. package/dist/src/modules/connect/connect.dto.d.ts.map +0 -1
  154. package/dist/src/modules/connect/connect.linkedin.d.ts.map +0 -1
  155. package/dist/src/modules/connect/connect.oauth.d.ts.map +0 -1
  156. package/dist/src/modules/connect/connect.repository.d.ts.map +0 -1
  157. package/dist/src/modules/connect/connect.router.d.ts.map +0 -1
  158. package/dist/src/modules/connect/connect.service.d.ts.map +0 -1
  159. package/dist/src/modules/connect/connect.trpc.d.ts.map +0 -1
  160. package/dist/src/modules/connect/connect.types.d.ts.map +0 -1
  161. package/dist/src/modules/crypto/crypto.db.d.ts.map +0 -1
  162. package/dist/src/modules/crypto/crypto.repository.d.ts.map +0 -1
  163. package/dist/src/modules/crypto/crypto.service.d.ts.map +0 -1
  164. package/dist/src/modules/email/email.service.d.ts.map +0 -1
  165. package/dist/src/modules/file/file.repository.d.ts.map +0 -1
  166. package/dist/src/modules/file/file.router.d.ts.map +0 -1
  167. package/dist/src/modules/file/file.service.d.ts.map +0 -1
  168. package/dist/src/modules/recurrence/recurrence.db.d.ts.map +0 -1
  169. package/dist/src/modules/recurrence/recurrence.repository.d.ts.map +0 -1
  170. package/dist/src/modules/recurrence/recurrence.service.d.ts.map +0 -1
  171. package/dist/src/modules/recurrence/recurrence.trpc.d.ts.map +0 -1
  172. package/dist/src/modules/social/social.dto.d.ts.map +0 -1
  173. package/dist/src/modules/social/social.linkedin.d.ts.map +0 -1
  174. package/dist/src/modules/social/social.linkedin.test.d.ts.map +0 -1
  175. package/dist/src/modules/social/social.service.d.ts.map +0 -1
  176. package/dist/src/modules/social/social.types.d.ts.map +0 -1
  177. package/dist/src/modules/tag/tag.db.d.ts.map +0 -1
  178. package/dist/src/modules/tag/tag.dto.d.ts.map +0 -1
  179. package/dist/src/modules/tag/tag.repository.d.ts.map +0 -1
  180. package/dist/src/modules/tag/tag.service.d.ts.map +0 -1
  181. package/dist/src/modules/tag/tag.trpc.d.ts.map +0 -1
  182. package/dist/src/modules/utils/applyPagination.d.ts.map +0 -1
  183. package/dist/src/modules/utils/applySorting.d.ts.map +0 -1
  184. package/dist/src/modules/utils/getConditionsFromFilters.d.ts.map +0 -1
  185. package/dist/src/modules/video/video.service.d.ts.map +0 -1
  186. package/dist/src/modules/webhook/webhook.constants.d.ts.map +0 -1
  187. package/dist/src/modules/webhook/webhook.db.d.ts.map +0 -1
  188. package/dist/src/modules/webhook/webhook.dto.d.ts.map +0 -1
  189. package/dist/src/modules/webhook/webhook.repository.d.ts.map +0 -1
  190. package/dist/src/modules/webhook/webhook.router.d.ts.map +0 -1
  191. package/dist/src/modules/webhook/webhook.service.d.ts.map +0 -1
  192. package/dist/src/modules/workflow/workflow.db.d.ts.map +0 -1
  193. package/dist/src/modules/workflow/workflow.repository.d.ts.map +0 -1
  194. package/dist/src/modules/workflow/workflow.service.d.ts.map +0 -1
  195. package/dist/src/modules/workflow/workflow.trpc.d.ts.map +0 -1
  196. package/dist/src/modules/workflow/workflow.types.d.ts.map +0 -1
  197. package/dist/src/modules/workflow/workflow.utils.d.ts.map +0 -1
  198. package/dist/src/test/stubs/utils.d.ts.map +0 -1
  199. package/dist/src/trpc/context.d.ts.map +0 -1
  200. package/dist/src/trpc/index.d.ts.map +0 -1
  201. package/dist/src/trpc/procedures.d.ts.map +0 -1
  202. package/dist/src/trpc/utils.d.ts.map +0 -1
  203. package/dist/src/types.d.ts.map +0 -1
  204. package/dist/src/utils/errors.d.ts.map +0 -1
  205. package/dist/src/utils/logger.d.ts.map +0 -1
  206. package/dist/src/utils/posthog.d.ts.map +0 -1
  207. package/dist/src/utils/types.d.ts.map +0 -1
  208. package/jest.config.ts +0 -19
  209. package/src/lib/posthog.ts +0 -5
  210. package/src/lib/sentry.ts +0 -8
  211. package/src/modules/access/access.repository.ts +0 -36
  212. package/src/modules/access/access.service.ts +0 -81
  213. package/src/modules/access/access.test.ts +0 -216
  214. package/src/modules/access/access.utils.ts +0 -46
  215. package/src/modules/ai/ai.db.ts +0 -38
  216. package/src/modules/ai/ai.prompt.ts +0 -47
  217. package/src/modules/ai/ai.repository.ts +0 -53
  218. package/src/modules/ai/ai.router.ts +0 -148
  219. package/src/modules/ai/ai.service.ts +0 -310
  220. package/src/modules/ai/ai.trpc.ts +0 -22
  221. package/src/modules/ai/ideogram/ideogram.constants.ts +0 -170
  222. package/src/modules/ai/ideogram/ideogram.dto.ts +0 -64
  223. package/src/modules/ai/ideogram/ideogram.prompt.ts +0 -858
  224. package/src/modules/ai/ideogram/ideogram.repository.ts +0 -39
  225. package/src/modules/ai/ideogram/ideogram.service.ts +0 -14
  226. package/src/modules/auth/auth.db.ts +0 -224
  227. package/src/modules/auth/auth.dto.ts +0 -47
  228. package/src/modules/auth/auth.lib.ts +0 -349
  229. package/src/modules/auth/auth.middleware.ts +0 -62
  230. package/src/modules/auth/auth.repository.ts +0 -672
  231. package/src/modules/auth/auth.service.ts +0 -261
  232. package/src/modules/auth/auth.trpc.ts +0 -208
  233. package/src/modules/auth/auth.utils.ts +0 -117
  234. package/src/modules/base/base.abstract.ts +0 -62
  235. package/src/modules/base/base.dto.ts +0 -206
  236. package/src/modules/base/base.grants.test.ts +0 -861
  237. package/src/modules/base/base.grants.ts +0 -199
  238. package/src/modules/base/base.repository.ts +0 -433
  239. package/src/modules/base/base.service.ts +0 -154
  240. package/src/modules/base/base.types.ts +0 -7
  241. package/src/modules/billing/billing.db.ts +0 -27
  242. package/src/modules/billing/billing.repository.ts +0 -328
  243. package/src/modules/billing/billing.router.ts +0 -77
  244. package/src/modules/billing/billing.service.ts +0 -177
  245. package/src/modules/billing/billing.trpc.ts +0 -17
  246. package/src/modules/clay/clay.repository.ts +0 -29
  247. package/src/modules/clay/clay.service.ts +0 -61
  248. package/src/modules/connect/connect.db.ts +0 -32
  249. package/src/modules/connect/connect.dto.ts +0 -44
  250. package/src/modules/connect/connect.linkedin.ts +0 -70
  251. package/src/modules/connect/connect.oauth.ts +0 -288
  252. package/src/modules/connect/connect.repository.ts +0 -65
  253. package/src/modules/connect/connect.router.ts +0 -76
  254. package/src/modules/connect/connect.service.ts +0 -171
  255. package/src/modules/connect/connect.trpc.ts +0 -26
  256. package/src/modules/connect/connect.types.ts +0 -27
  257. package/src/modules/crypto/crypto.db.ts +0 -15
  258. package/src/modules/crypto/crypto.repository.ts +0 -13
  259. package/src/modules/crypto/crypto.service.ts +0 -57
  260. package/src/modules/email/email.service.ts +0 -222
  261. package/src/modules/file/file.repository.ts +0 -95
  262. package/src/modules/file/file.router.ts +0 -108
  263. package/src/modules/file/file.service.ts +0 -186
  264. package/src/modules/recurrence/recurrence.db.ts +0 -79
  265. package/src/modules/recurrence/recurrence.repository.ts +0 -70
  266. package/src/modules/recurrence/recurrence.service.ts +0 -105
  267. package/src/modules/recurrence/recurrence.trpc.ts +0 -82
  268. package/src/modules/social/social.dto.ts +0 -22
  269. package/src/modules/social/social.linkedin.test.ts +0 -277
  270. package/src/modules/social/social.linkedin.ts +0 -593
  271. package/src/modules/social/social.service.ts +0 -112
  272. package/src/modules/social/social.types.ts +0 -43
  273. package/src/modules/tag/tag.db.ts +0 -41
  274. package/src/modules/tag/tag.dto.ts +0 -18
  275. package/src/modules/tag/tag.repository.ts +0 -222
  276. package/src/modules/tag/tag.service.ts +0 -48
  277. package/src/modules/tag/tag.trpc.ts +0 -62
  278. package/src/modules/uploads/0581796b-8845-420d-bd95-cd7de79f6d37.webm +0 -0
  279. package/src/modules/uploads/33b1e649-6727-4bd0-94d0-a0b363646865.webm +0 -0
  280. package/src/modules/uploads/49a8c4c0-54d7-4c94-bef4-c93c029f9ed0.webm +0 -0
  281. package/src/modules/uploads/50e31e38-a2f0-47ca-8b7d-2d7fcad9267d.webm +0 -0
  282. package/src/modules/uploads/72ac8cf9-c3a7-4cd8-8a78-6d8e137a4c7e.webm +0 -0
  283. package/src/modules/uploads/75293649-d966-46cd-a675-67518958ae9c.png +0 -0
  284. package/src/modules/uploads/88b7b867-ce15-4891-bf73-81305a7de1f7.wav +0 -0
  285. package/src/modules/uploads/a5d6fee8-6a59-42c6-9d4a-ac8a3c5e7245.webm +0 -0
  286. package/src/modules/uploads/c13a9785-ca5a-4983-af30-b338ed76d370.webm +0 -0
  287. package/src/modules/uploads/caa1a5a7-71ba-4381-902d-7e2cafdf6dcb.webm +0 -0
  288. package/src/modules/uploads/cbeb0b81-374d-445b-914b-40ace7c8e031.webm +0 -0
  289. package/src/modules/uploads/d626aa82-b10f-493f-aee7-87bfb3361dfc.webm +0 -0
  290. package/src/modules/uploads/d7de4c16-de0c-495d-9612-e72260a6ecca.png +0 -0
  291. package/src/modules/uploads/e532e38a-6421-400e-8a5f-8e7bc8ce411b.wav +0 -0
  292. package/src/modules/uploads/e86ec867-6adf-4c51-84e0-00b0836625e8.webm +0 -0
  293. package/src/modules/utils/applyPagination.ts +0 -13
  294. package/src/modules/utils/applySorting.ts +0 -21
  295. package/src/modules/utils/getConditionsFromFilters.ts +0 -216
  296. package/src/modules/video/video.service.ts +0 -89
  297. package/src/modules/webhook/webhook.constants.ts +0 -9
  298. package/src/modules/webhook/webhook.db.ts +0 -15
  299. package/src/modules/webhook/webhook.dto.ts +0 -9
  300. package/src/modules/webhook/webhook.repository.ts +0 -68
  301. package/src/modules/webhook/webhook.router.ts +0 -29
  302. package/src/modules/webhook/webhook.service.ts +0 -78
  303. package/src/modules/workflow/workflow.db.ts +0 -29
  304. package/src/modules/workflow/workflow.repository.ts +0 -171
  305. package/src/modules/workflow/workflow.service.ts +0 -56
  306. package/src/modules/workflow/workflow.trpc.ts +0 -26
  307. package/src/modules/workflow/workflow.types.ts +0 -30
  308. package/src/modules/workflow/workflow.utils.ts +0 -259
  309. package/src/test/stubs/utils.ts +0 -2
  310. package/src/trpc/context.ts +0 -21
  311. package/src/trpc/index.ts +0 -3
  312. package/src/trpc/procedures.ts +0 -43
  313. package/src/trpc/utils.ts +0 -20
  314. package/src/types.ts +0 -22
  315. package/src/utils/errors.ts +0 -148
  316. package/src/utils/logger.ts +0 -8
  317. package/src/utils/posthog.ts +0 -43
  318. package/src/utils/types.ts +0 -5
  319. package/tsconfig.json +0 -21
@@ -1,593 +0,0 @@
1
- import { createReadStream } from "node:fs";
2
- import { stat, unlink } from "node:fs/promises";
3
- import type { ConnectRow } from "#modules/connect/connect.repository";
4
- import type { FileService } from "#modules/file/file.service";
5
- import type { SocialMediaDescriptor, SocialPostPayload, SocialProvider } from "./social.types";
6
-
7
- const LINKEDIN_API_BASE = "https://api.linkedin.com/rest";
8
- const LINKEDIN_VERSION = "202601";
9
-
10
- const IMAGES_URL = `${LINKEDIN_API_BASE}/images?action=initializeUpload`;
11
- const VIDEOS_URL = `${LINKEDIN_API_BASE}/videos?action=initializeUpload`;
12
- const VIDEOS_FINALIZE_URL = `${LINKEDIN_API_BASE}/videos?action=finalizeUpload`;
13
- const DOCUMENTS_URL = `${LINKEDIN_API_BASE}/documents?action=initializeUpload`;
14
- const POSTS_URL = `${LINKEDIN_API_BASE}/posts`;
15
-
16
- interface LinkedInMetadata {
17
- linkedInUrn?: string;
18
- [key: string]: unknown;
19
- }
20
-
21
- interface UploadedAsset {
22
- assetUrn: string;
23
- mediaType: "image" | "video" | "document";
24
- title?: string;
25
- description?: string;
26
- }
27
-
28
- interface ImageInitResponse {
29
- value: {
30
- uploadUrlExpiresAt: number;
31
- uploadUrl: string;
32
- image: string;
33
- };
34
- }
35
-
36
- interface VideoInitResponse {
37
- value: {
38
- uploadUrlsExpireAt: number;
39
- video: string;
40
- uploadInstructions: Array<{
41
- uploadUrl: string;
42
- lastByte: number;
43
- firstByte: number;
44
- }>;
45
- uploadToken: string;
46
- };
47
- }
48
-
49
- interface DocumentInitResponse {
50
- value: {
51
- uploadUrlExpiresAt: number;
52
- uploadUrl: string;
53
- document: string;
54
- };
55
- }
56
-
57
- function getApiHeaders(accessToken: string): Record<string, string> {
58
- return {
59
- Authorization: `Bearer ${accessToken}`,
60
- "Content-Type": "application/json",
61
- "Linkedin-Version": LINKEDIN_VERSION,
62
- "X-Restli-Protocol-Version": "2.0.0",
63
- };
64
- }
65
-
66
- export function createLinkedInSocialProvider(): SocialProvider {
67
- return {
68
- id: "linkedin",
69
- async post({ deps, context, payload }) {
70
- const personUrn = resolveAuthorUrn(context.connection);
71
- const mediaDescriptors = payload.media ?? [];
72
-
73
- let uploadedAssets: UploadedAsset[] = [];
74
- if (mediaDescriptors.length > 0) {
75
- uploadedAssets = await uploadMediaAssets({
76
- accessToken: context.accessToken,
77
- fileService: deps.fileService,
78
- mediaDescriptors,
79
- personUrn,
80
- });
81
- }
82
-
83
- const response = await publishPost({
84
- accessToken: context.accessToken,
85
- personUrn,
86
- payload,
87
- uploadedAssets,
88
- });
89
-
90
- return {
91
- shareUrn: response.postUrn,
92
- rawResponse: response.raw,
93
- };
94
- },
95
- };
96
- }
97
-
98
- function resolveAuthorUrn(connection: ConnectRow): string {
99
- if (connection.metadataJson) {
100
- try {
101
- const metadata = connection.metadataJson as LinkedInMetadata;
102
- if (metadata.linkedInUrn && typeof metadata.linkedInUrn === "string") {
103
- return metadata.linkedInUrn;
104
- }
105
- } catch {
106
- throw new Error("Failed to parse LinkedIn connection metadata");
107
- }
108
- }
109
-
110
- if (connection.providerAccountId) {
111
- return `urn:li:person:${connection.providerAccountId}`;
112
- }
113
-
114
- throw new Error("LinkedIn connection is missing a person URN");
115
- }
116
-
117
- async function uploadMediaAssets(params: {
118
- accessToken: string;
119
- fileService: FileService;
120
- mediaDescriptors: readonly SocialMediaDescriptor[];
121
- personUrn: string;
122
- }): Promise<UploadedAsset[]> {
123
- const results: UploadedAsset[] = [];
124
-
125
- for (const descriptor of params.mediaDescriptors) {
126
- const download = await params.fileService.downloadS3ToFile(descriptor.s3Path);
127
- if (download.isErr()) {
128
- throw download.error;
129
- }
130
-
131
- const localPath = download.value;
132
- try {
133
- const mediaType = determineMediaType(descriptor.mediaType, localPath);
134
-
135
- let assetUrn: string;
136
- switch (mediaType) {
137
- case "image":
138
- assetUrn = await uploadImage({
139
- accessToken: params.accessToken,
140
- personUrn: params.personUrn,
141
- localPath,
142
- });
143
- break;
144
- case "video":
145
- assetUrn = await uploadVideo({
146
- accessToken: params.accessToken,
147
- personUrn: params.personUrn,
148
- localPath,
149
- });
150
- break;
151
- case "document":
152
- assetUrn = await uploadDocument({
153
- accessToken: params.accessToken,
154
- personUrn: params.personUrn,
155
- localPath,
156
- });
157
- break;
158
- }
159
-
160
- results.push({
161
- assetUrn,
162
- mediaType,
163
- title: descriptor.title,
164
- description: descriptor.description,
165
- });
166
- } finally {
167
- await unlink(localPath).catch(() => undefined);
168
- }
169
- }
170
-
171
- return results;
172
- }
173
-
174
- function determineMediaType(
175
- explicitType: SocialMediaDescriptor["mediaType"],
176
- localPath: string
177
- ): "image" | "video" | "document" {
178
- if (explicitType) {
179
- return explicitType;
180
- }
181
-
182
- const extension = localPath.split(".").pop()?.toLowerCase();
183
- if (!extension) {
184
- throw new Error("Unable to determine media type from file extension");
185
- }
186
-
187
- const imageExtensions = new Set(["jpg", "jpeg", "png", "gif", "webp"]);
188
- const videoExtensions = new Set(["mp4", "mov", "m4v", "avi", "mkv", "webm"]);
189
- const documentExtensions = new Set(["pdf", "ppt", "pptx", "doc", "docx"]);
190
-
191
- if (imageExtensions.has(extension)) {
192
- return "image";
193
- }
194
- if (videoExtensions.has(extension)) {
195
- return "video";
196
- }
197
- if (documentExtensions.has(extension)) {
198
- return "document";
199
- }
200
-
201
- throw new Error(`Unsupported media extension: ${extension}`);
202
- }
203
-
204
- async function uploadImage(params: {
205
- accessToken: string;
206
- personUrn: string;
207
- localPath: string;
208
- }): Promise<string> {
209
- const initResponse = await fetch(IMAGES_URL, {
210
- method: "POST",
211
- headers: getApiHeaders(params.accessToken),
212
- body: JSON.stringify({
213
- initializeUploadRequest: {
214
- owner: params.personUrn,
215
- },
216
- }),
217
- });
218
-
219
- if (!initResponse.ok) {
220
- const errorText = await initResponse.text().catch(() => "");
221
- throw new Error(
222
- `LinkedIn image upload initialization failed: ${initResponse.status} ${errorText}`
223
- );
224
- }
225
-
226
- const initJson = (await initResponse.json()) as ImageInitResponse;
227
- const { uploadUrl, image } = initJson.value;
228
-
229
- if (!uploadUrl || !image) {
230
- throw new Error("LinkedIn image initialization response missing required fields");
231
- }
232
-
233
- await uploadBinaryToLinkedIn({
234
- uploadUrl,
235
- localPath: params.localPath,
236
- mimeType: inferImageMime(params.localPath),
237
- });
238
-
239
- return image;
240
- }
241
-
242
- async function uploadVideo(params: {
243
- accessToken: string;
244
- personUrn: string;
245
- localPath: string;
246
- }): Promise<string> {
247
- const fileSizeBytes = await getFileSize(params.localPath);
248
-
249
- const initResponse = await fetch(VIDEOS_URL, {
250
- method: "POST",
251
- headers: getApiHeaders(params.accessToken),
252
- body: JSON.stringify({
253
- initializeUploadRequest: {
254
- owner: params.personUrn,
255
- fileSizeBytes,
256
- uploadCaptions: false,
257
- uploadThumbnail: false,
258
- },
259
- }),
260
- });
261
-
262
- if (!initResponse.ok) {
263
- const errorText = await initResponse.text().catch(() => "");
264
- throw new Error(
265
- `LinkedIn video upload initialization failed: ${initResponse.status} ${errorText}`
266
- );
267
- }
268
-
269
- const initJson = (await initResponse.json()) as VideoInitResponse;
270
- const { uploadInstructions, video, uploadToken } = initJson.value;
271
-
272
- if (!uploadInstructions || !video || !uploadToken) {
273
- throw new Error("LinkedIn video initialization response missing required fields");
274
- }
275
-
276
- const uploadedPartIds = await uploadVideoMultipart({
277
- localPath: params.localPath,
278
- uploadInstructions,
279
- });
280
-
281
- await finalizeVideoUpload({
282
- accessToken: params.accessToken,
283
- video,
284
- uploadToken,
285
- uploadedPartIds,
286
- });
287
-
288
- return video;
289
- }
290
-
291
- async function uploadVideoMultipart(params: {
292
- localPath: string;
293
- uploadInstructions: VideoInitResponse["value"]["uploadInstructions"];
294
- }): Promise<string[]> {
295
- const uploadedPartIds: string[] = [];
296
-
297
- for (const instruction of params.uploadInstructions) {
298
- const chunkStream = createReadStream(params.localPath, {
299
- start: instruction.firstByte,
300
- end: instruction.lastByte,
301
- });
302
-
303
- const uploadResponse = await fetch(instruction.uploadUrl, {
304
- method: "PUT",
305
- headers: {
306
- "Content-Type": "application/octet-stream",
307
- },
308
- body: chunkStream,
309
- duplex: "half",
310
- });
311
-
312
- if (!uploadResponse.ok) {
313
- const errorText = await uploadResponse.text().catch(() => "");
314
- throw new Error(`LinkedIn video part upload failed: ${uploadResponse.status} ${errorText}`);
315
- }
316
-
317
- const etag = uploadResponse.headers.get("etag");
318
- if (!etag) {
319
- throw new Error("LinkedIn video part upload response missing ETag header");
320
- }
321
-
322
- uploadedPartIds.push(etag.replace(/"/g, ""));
323
- }
324
-
325
- return uploadedPartIds;
326
- }
327
-
328
- async function finalizeVideoUpload(params: {
329
- accessToken: string;
330
- video: string;
331
- uploadToken: string;
332
- uploadedPartIds: string[];
333
- }): Promise<void> {
334
- const response = await fetch(VIDEOS_FINALIZE_URL, {
335
- method: "POST",
336
- headers: getApiHeaders(params.accessToken),
337
- body: JSON.stringify({
338
- finalizeUploadRequest: {
339
- video: params.video,
340
- uploadToken: params.uploadToken,
341
- uploadedPartIds: params.uploadedPartIds,
342
- },
343
- }),
344
- });
345
-
346
- if (!response.ok) {
347
- const errorText = await response.text().catch(() => "");
348
- throw new Error(`LinkedIn video finalize failed: ${response.status} ${errorText}`);
349
- }
350
- }
351
-
352
- async function uploadDocument(params: {
353
- accessToken: string;
354
- personUrn: string;
355
- localPath: string;
356
- }): Promise<string> {
357
- const initResponse = await fetch(DOCUMENTS_URL, {
358
- method: "POST",
359
- headers: getApiHeaders(params.accessToken),
360
- body: JSON.stringify({
361
- initializeUploadRequest: {
362
- owner: params.personUrn,
363
- },
364
- }),
365
- });
366
-
367
- if (!initResponse.ok) {
368
- const errorText = await initResponse.text().catch(() => "");
369
- throw new Error(
370
- `LinkedIn document upload initialization failed: ${initResponse.status} ${errorText}`
371
- );
372
- }
373
-
374
- const initJson = (await initResponse.json()) as DocumentInitResponse;
375
- const { uploadUrl, document } = initJson.value;
376
-
377
- if (!uploadUrl || !document) {
378
- throw new Error("LinkedIn document initialization response missing required fields");
379
- }
380
-
381
- await uploadBinaryToLinkedIn({
382
- uploadUrl,
383
- localPath: params.localPath,
384
- mimeType: inferDocumentMime(params.localPath),
385
- });
386
-
387
- return document;
388
- }
389
-
390
- async function uploadBinaryToLinkedIn(params: {
391
- uploadUrl: string;
392
- localPath: string;
393
- mimeType: string;
394
- }): Promise<void> {
395
- const uploadResponse = await fetch(params.uploadUrl, {
396
- method: "PUT",
397
- headers: {
398
- "Content-Type": params.mimeType,
399
- },
400
- body: createReadStream(params.localPath),
401
- duplex: "half",
402
- });
403
-
404
- if (!uploadResponse.ok) {
405
- const errorText = await uploadResponse.text().catch(() => "");
406
- throw new Error(`LinkedIn media upload failed: ${uploadResponse.status} ${errorText}`);
407
- }
408
- }
409
-
410
- async function getFileSize(filePath: string): Promise<number> {
411
- const stats = await stat(filePath);
412
- return stats.size;
413
- }
414
-
415
- function inferImageMime(localPath: string): string {
416
- const extension = localPath.split(".").pop()?.toLowerCase();
417
- switch (extension) {
418
- case "png":
419
- return "image/png";
420
- case "webp":
421
- return "image/webp";
422
- case "gif":
423
- return "image/gif";
424
- case "jpg":
425
- case "jpeg":
426
- return "image/jpeg";
427
- default:
428
- return "application/octet-stream";
429
- }
430
- }
431
-
432
- function inferDocumentMime(localPath: string): string {
433
- const extension = localPath.split(".").pop()?.toLowerCase();
434
- switch (extension) {
435
- case "pdf":
436
- return "application/pdf";
437
- case "doc":
438
- return "application/msword";
439
- case "docx":
440
- return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
441
- case "ppt":
442
- return "application/vnd.ms-powerpoint";
443
- case "pptx":
444
- return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
445
- default:
446
- return "application/octet-stream";
447
- }
448
- }
449
-
450
- async function publishPost(params: {
451
- accessToken: string;
452
- personUrn: string;
453
- payload: SocialPostPayload;
454
- uploadedAssets: UploadedAsset[];
455
- }): Promise<{ postUrn?: string; raw: unknown }> {
456
- const body = buildPostBody(params);
457
-
458
- const response = await fetch(POSTS_URL, {
459
- method: "POST",
460
- headers: getApiHeaders(params.accessToken),
461
- body: JSON.stringify(body),
462
- });
463
-
464
- if (!response.ok) {
465
- const errorText = await response.text().catch(() => "");
466
- throw new Error(`LinkedIn post failed: ${response.status} ${errorText}`);
467
- }
468
-
469
- const postUrn = response.headers.get("x-restli-id") ?? undefined;
470
- const raw = await safeJson(response);
471
-
472
- return { postUrn, raw };
473
- }
474
-
475
- /**
476
- * Escapes special characters in text for LinkedIn's post commentary field.
477
- * LinkedIn requires certain characters to be backslash-escaped.
478
- * Preserves mentions (@[Name](urn:li:...)) and hashtag templates ({hashtag|#|tag}).
479
- * Converts simple hashtags (#tag) to the template format.
480
- * @see https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/little-text-format
481
- */
482
- export function escapeLinkedInText(text: string): string {
483
- // Patterns for LinkedIn elements that should not be escaped
484
- // MentionElement: @[FallbackText](urn:li:...)
485
- const mentionPattern = /@\[[^\]]*\]\(urn:li:[^)]+\)/g;
486
- // HashtagTemplate: {hashtag|#|text} or {hashtag|#|text} (with optional escaped #)
487
- const hashtagTemplatePattern = /\{hashtag\|\\?[##]\|[^}]+\}/g;
488
- // Simple hashtag: #word (must start with a letter, not digit-only)
489
- const simpleHashtagPattern =
490
- /#([a-zA-Z\u00C0-\u024F\u1E00-\u1EFF][\w\u00C0-\u024F\u1E00-\u1EFF]*)/g;
491
-
492
- // Store matches in array, use index-based placeholders with null character delimiters
493
- const preserved: string[] = [];
494
-
495
- const createPlaceholder = (content: string): string => {
496
- const index = preserved.length;
497
- preserved.push(content);
498
- return `\x00${index}\x00`;
499
- };
500
-
501
- // Replace mentions with placeholders (preserve as-is)
502
- let result = text.replace(mentionPattern, (match) => createPlaceholder(match));
503
-
504
- // Replace hashtag templates with placeholders (preserve as-is)
505
- result = result.replace(hashtagTemplatePattern, (match) => createPlaceholder(match));
506
-
507
- // Convert simple hashtags to template format with escaped #
508
- result = result.replace(simpleHashtagPattern, (_match, tag: string) =>
509
- createPlaceholder(`{hashtag|\\#|${tag}}`)
510
- );
511
-
512
- // Escape remaining special characters (order matters: backslash first)
513
- result = result
514
- .replace(/\\/g, "\\\\")
515
- .replace(/\|/g, "\\|")
516
- .replace(/\{/g, "\\{")
517
- .replace(/\}/g, "\\}")
518
- .replace(/@/g, "\\@")
519
- .replace(/\[/g, "\\[")
520
- .replace(/\]/g, "\\]")
521
- .replace(/\(/g, "\\(")
522
- .replace(/\)/g, "\\)")
523
- .replace(/</g, "\\<")
524
- .replace(/>/g, "\\>")
525
- .replace(/#/g, "\\#")
526
- .replace(/\*/g, "\\*")
527
- .replace(/_/g, "\\_")
528
- .replace(/~/g, "\\~");
529
-
530
- // Restore placeholders with preserved/transformed content
531
- // biome-ignore lint/suspicious/noControlCharactersInRegex: null chars are intentional placeholders
532
- result = result.replace(/\x00(\d+)\x00/g, (_match, index: string) => preserved[Number(index)]);
533
-
534
- return result;
535
- }
536
-
537
- function buildPostBody(params: {
538
- personUrn: string;
539
- payload: SocialPostPayload;
540
- uploadedAssets: UploadedAsset[];
541
- }): Record<string, unknown> {
542
- const { personUrn, payload, uploadedAssets } = params;
543
-
544
- const basePost = {
545
- author: personUrn,
546
- commentary: escapeLinkedInText(payload.text),
547
- visibility: payload.visibility,
548
- distribution: {
549
- feedDistribution: "MAIN_FEED",
550
- targetEntities: [],
551
- thirdPartyDistributionChannels: [],
552
- },
553
- lifecycleState: "PUBLISHED",
554
- isReshareDisabledByAuthor: false,
555
- };
556
-
557
- if (uploadedAssets.length === 0) {
558
- return basePost;
559
- }
560
-
561
- if (uploadedAssets.length > 1) {
562
- throw new Error("LinkedIn Posts API currently supports only a single media asset per post");
563
- }
564
-
565
- const asset = uploadedAssets[0];
566
-
567
- const mediaContent: Record<string, unknown> = {
568
- id: asset.assetUrn,
569
- };
570
-
571
- if (asset.title) {
572
- mediaContent.title = asset.title;
573
- }
574
-
575
- if (asset.description && asset.mediaType === "image") {
576
- mediaContent.altText = asset.description;
577
- }
578
-
579
- return {
580
- ...basePost,
581
- content: {
582
- media: mediaContent,
583
- },
584
- };
585
- }
586
-
587
- async function safeJson(response: globalThis.Response): Promise<unknown> {
588
- try {
589
- return await response.json();
590
- } catch {
591
- return undefined;
592
- }
593
- }
@@ -1,112 +0,0 @@
1
- import { ok } from "neverthrow";
2
- import type { ServerResultAsync } from "#modules/base/base.dto";
3
- import { BaseService } from "#modules/base/base.service";
4
- import type { ConnectRepository, ConnectRow } from "#modules/connect/connect.repository";
5
- import type { ConnectService } from "#modules/connect/connect.service";
6
- import type { FileService } from "#modules/file/file.service";
7
- import type { SocialPostInput } from "./social.dto";
8
- import type { SocialPostPayload, SocialPostResult, SocialProvider } from "./social.types";
9
-
10
- export class SocialService extends BaseService<
11
- {
12
- connect: ConnectRepository;
13
- },
14
- {
15
- connect: ConnectService;
16
- file: FileService;
17
- }
18
- > {
19
- private providers = new Map<string, SocialProvider>();
20
-
21
- constructor(
22
- repositories: { connect: ConnectRepository },
23
- services: { connect: ConnectService; file: FileService },
24
- providers: SocialProvider[]
25
- ) {
26
- super(repositories, services);
27
- this.providers = new Map(providers.map((provider) => [provider.id, provider]));
28
- }
29
-
30
- getProvider(id: string): SocialProvider | null {
31
- return this.providers.get(id) ?? null;
32
- }
33
-
34
- async postToProvider(
35
- providerId: string,
36
- input: SocialPostInput,
37
- { user }: { user: { id: string } }
38
- ): ServerResultAsync<SocialPostResult> {
39
- return this.throwableAsync(async () => {
40
- const provider = this.getProvider(providerId);
41
- if (!provider) {
42
- return this.error("BAD_REQUEST", `Unknown provider: ${providerId}`);
43
- }
44
-
45
- const connectionResult = await this.repository.connect.list({
46
- userId: user.id,
47
- providers: [providerId],
48
- });
49
-
50
- if (connectionResult.isErr()) {
51
- return this.error("INTERNAL_SERVER_ERROR", "Failed to load connection", {
52
- cause: connectionResult.error,
53
- });
54
- }
55
-
56
- const activeConnection = connectionResult.value.sort(
57
- (a, b) => a.createdAt.getTime() - b.createdAt.getTime()
58
- )[0];
59
-
60
- const connection = await this.ensureFreshConnection(activeConnection);
61
- if (connection.isErr()) {
62
- return this.error("INTERNAL_SERVER_ERROR", "Failed to refresh connection", {
63
- cause: connection.error,
64
- });
65
- }
66
-
67
- const payload: SocialPostPayload = {
68
- text: input.text,
69
- media: input.media,
70
- visibility: input.visibility ?? "PUBLIC",
71
- };
72
-
73
- const accessToken = connection.value.accessToken;
74
- if (!accessToken) {
75
- return this.error("BAD_REQUEST", "Missing access token for connection");
76
- }
77
-
78
- const result = await provider.post({
79
- deps: { fileService: this.service.file },
80
- context: {
81
- userId: user.id,
82
- connection: connection.value,
83
- accessToken,
84
- },
85
- payload,
86
- });
87
-
88
- return ok(result);
89
- });
90
- }
91
-
92
- private async ensureFreshConnection(connection: ConnectRow): ServerResultAsync<ConnectRow> {
93
- if (!connection.expiresAt || !connection.refreshToken) {
94
- return ok(connection);
95
- }
96
-
97
- const expiresAt = new Date(connection.expiresAt);
98
- const bufferMs = 60 * 1000; // Refresh 1 minute before expiry
99
- if (Date.now() < expiresAt.getTime() - bufferMs) {
100
- return ok(connection);
101
- }
102
-
103
- const refreshed = await this.service.connect.refreshToken(connection.id);
104
- if (refreshed.isErr()) {
105
- return this.error("INTERNAL_SERVER_ERROR", "Failed to refresh access token", {
106
- cause: refreshed.error,
107
- });
108
- }
109
-
110
- return ok(refreshed.value);
111
- }
112
- }