@intelligo-dev/core 1.0.0-beta.1 → 1.0.0-beta.14

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 (356) hide show
  1. package/NOTICE +6 -0
  2. package/README.md +102 -0
  3. package/dist/attachments/errors.d.ts +7 -0
  4. package/dist/attachments/errors.d.ts.map +1 -0
  5. package/dist/attachments/errors.js +11 -0
  6. package/dist/attachments/errors.js.map +1 -0
  7. package/dist/attachments/index.d.ts +8 -0
  8. package/dist/attachments/index.d.ts.map +1 -0
  9. package/dist/attachments/index.js +7 -0
  10. package/dist/attachments/index.js.map +1 -0
  11. package/dist/attachments/service.d.ts +45 -0
  12. package/dist/attachments/service.d.ts.map +1 -0
  13. package/dist/attachments/service.js +100 -0
  14. package/dist/attachments/service.js.map +1 -0
  15. package/dist/attachments/types.d.ts +6 -0
  16. package/dist/attachments/types.d.ts.map +1 -0
  17. package/dist/attachments/types.js +2 -0
  18. package/dist/attachments/types.js.map +1 -0
  19. package/dist/conversations/errors.d.ts +2 -8
  20. package/dist/conversations/errors.d.ts.map +1 -1
  21. package/dist/conversations/errors.js +2 -8
  22. package/dist/conversations/errors.js.map +1 -1
  23. package/dist/conversations/index.d.ts +4 -12
  24. package/dist/conversations/index.d.ts.map +1 -1
  25. package/dist/conversations/index.js +2 -11
  26. package/dist/conversations/index.js.map +1 -1
  27. package/dist/conversations/service.d.ts +49 -61
  28. package/dist/conversations/service.d.ts.map +1 -1
  29. package/dist/conversations/service.js +110 -83
  30. package/dist/conversations/service.js.map +1 -1
  31. package/dist/conversations/types.d.ts +1 -4
  32. package/dist/conversations/types.d.ts.map +1 -1
  33. package/dist/conversations/types.js +0 -3
  34. package/dist/conversations/types.js.map +1 -1
  35. package/dist/db/client.d.ts +21 -10
  36. package/dist/db/client.d.ts.map +1 -1
  37. package/dist/db/client.js +43 -18
  38. package/dist/db/client.js.map +1 -1
  39. package/dist/db/index.d.ts +3 -3
  40. package/dist/db/index.d.ts.map +1 -1
  41. package/dist/db/index.js +3 -7
  42. package/dist/db/index.js.map +1 -1
  43. package/dist/db/schema/agents.d.ts +14 -51
  44. package/dist/db/schema/agents.d.ts.map +1 -1
  45. package/dist/db/schema/agents.js +9 -21
  46. package/dist/db/schema/agents.js.map +1 -1
  47. package/dist/db/schema/ai.d.ts +86 -1556
  48. package/dist/db/schema/ai.d.ts.map +1 -1
  49. package/dist/db/schema/ai.js +32 -236
  50. package/dist/db/schema/ai.js.map +1 -1
  51. package/dist/db/schema/auth.d.ts +5 -34
  52. package/dist/db/schema/auth.d.ts.map +1 -1
  53. package/dist/db/schema/auth.js +25 -56
  54. package/dist/db/schema/auth.js.map +1 -1
  55. package/dist/db/schema/billing.d.ts +339 -105
  56. package/dist/db/schema/billing.d.ts.map +1 -1
  57. package/dist/db/schema/billing.js +99 -58
  58. package/dist/db/schema/billing.js.map +1 -1
  59. package/dist/db/schema/feature-flags.d.ts +3 -12
  60. package/dist/db/schema/feature-flags.d.ts.map +1 -1
  61. package/dist/db/schema/feature-flags.js +3 -12
  62. package/dist/db/schema/feature-flags.js.map +1 -1
  63. package/dist/db/schema/identity.d.ts +12 -274
  64. package/dist/db/schema/identity.d.ts.map +1 -1
  65. package/dist/db/schema/identity.js +19 -104
  66. package/dist/db/schema/identity.js.map +1 -1
  67. package/dist/db/schema/index.d.ts +8 -15
  68. package/dist/db/schema/index.d.ts.map +1 -1
  69. package/dist/db/schema/index.js +12 -42
  70. package/dist/db/schema/index.js.map +1 -1
  71. package/dist/db/schema/notifications.d.ts +3 -17
  72. package/dist/db/schema/notifications.d.ts.map +1 -1
  73. package/dist/db/schema/notifications.js +3 -17
  74. package/dist/db/schema/notifications.js.map +1 -1
  75. package/dist/db/schema/usage.d.ts +94 -77
  76. package/dist/db/schema/usage.d.ts.map +1 -1
  77. package/dist/db/schema/usage.js +59 -89
  78. package/dist/db/schema/usage.js.map +1 -1
  79. package/dist/db/validate.d.ts +2 -11
  80. package/dist/db/validate.d.ts.map +1 -1
  81. package/dist/db/validate.js +6 -17
  82. package/dist/db/validate.js.map +1 -1
  83. package/dist/db/workspace-queries.d.ts +5 -29
  84. package/dist/db/workspace-queries.d.ts.map +1 -1
  85. package/dist/db/workspace-queries.js +5 -29
  86. package/dist/db/workspace-queries.js.map +1 -1
  87. package/dist/documents/classifier.d.ts +5 -12
  88. package/dist/documents/classifier.d.ts.map +1 -1
  89. package/dist/documents/classifier.js +16 -16
  90. package/dist/documents/classifier.js.map +1 -1
  91. package/dist/documents/errors.d.ts +2 -10
  92. package/dist/documents/errors.d.ts.map +1 -1
  93. package/dist/documents/errors.js +2 -10
  94. package/dist/documents/errors.js.map +1 -1
  95. package/dist/documents/index.d.ts +4 -13
  96. package/dist/documents/index.d.ts.map +1 -1
  97. package/dist/documents/index.js +3 -12
  98. package/dist/documents/index.js.map +1 -1
  99. package/dist/documents/service.d.ts +19 -26
  100. package/dist/documents/service.d.ts.map +1 -1
  101. package/dist/documents/service.js +44 -38
  102. package/dist/documents/service.js.map +1 -1
  103. package/dist/documents/types.d.ts +6 -10
  104. package/dist/documents/types.d.ts.map +1 -1
  105. package/dist/documents/types.js +0 -3
  106. package/dist/documents/types.js.map +1 -1
  107. package/dist/email/brand.d.ts +17 -0
  108. package/dist/email/brand.d.ts.map +1 -0
  109. package/dist/email/brand.js +9 -0
  110. package/dist/email/brand.js.map +1 -0
  111. package/dist/email/index.d.ts +4 -4
  112. package/dist/email/index.js +4 -4
  113. package/dist/email/provider.d.ts +1 -7
  114. package/dist/email/provider.d.ts.map +1 -1
  115. package/dist/email/provider.js +10 -19
  116. package/dist/email/provider.js.map +1 -1
  117. package/dist/email/send.d.ts +12 -12
  118. package/dist/email/send.d.ts.map +1 -1
  119. package/dist/email/send.js +5 -19
  120. package/dist/email/send.js.map +1 -1
  121. package/dist/email/senders.d.ts +17 -30
  122. package/dist/email/senders.d.ts.map +1 -1
  123. package/dist/email/senders.js +33 -47
  124. package/dist/email/senders.js.map +1 -1
  125. package/dist/email/templates/base-layout.d.ts +1 -1
  126. package/dist/email/templates/base-layout.d.ts.map +1 -1
  127. package/dist/email/templates/base-layout.js +4 -6
  128. package/dist/email/templates/base-layout.js.map +1 -1
  129. package/dist/email/templates/index.d.ts +10 -10
  130. package/dist/email/templates/index.d.ts.map +1 -1
  131. package/dist/email/templates/index.js +10 -16
  132. package/dist/email/templates/index.js.map +1 -1
  133. package/dist/email/templates/password-reset.d.ts +2 -2
  134. package/dist/email/templates/password-reset.d.ts.map +1 -1
  135. package/dist/email/templates/password-reset.js +10 -22
  136. package/dist/email/templates/password-reset.js.map +1 -1
  137. package/dist/email/templates/payment-failed.d.ts +2 -2
  138. package/dist/email/templates/payment-failed.d.ts.map +1 -1
  139. package/dist/email/templates/payment-failed.js +11 -24
  140. package/dist/email/templates/payment-failed.js.map +1 -1
  141. package/dist/email/templates/quota-warning.d.ts +4 -4
  142. package/dist/email/templates/quota-warning.d.ts.map +1 -1
  143. package/dist/email/templates/quota-warning.js +19 -39
  144. package/dist/email/templates/quota-warning.js.map +1 -1
  145. package/dist/email/templates/subscription-confirmed.d.ts +2 -2
  146. package/dist/email/templates/subscription-confirmed.d.ts.map +1 -1
  147. package/dist/email/templates/subscription-confirmed.js +13 -28
  148. package/dist/email/templates/subscription-confirmed.js.map +1 -1
  149. package/dist/email/templates/trial-expiry.d.ts +2 -2
  150. package/dist/email/templates/trial-expiry.d.ts.map +1 -1
  151. package/dist/email/templates/trial-expiry.js +12 -36
  152. package/dist/email/templates/trial-expiry.js.map +1 -1
  153. package/dist/email/templates/trial-warning.d.ts +2 -2
  154. package/dist/email/templates/trial-warning.d.ts.map +1 -1
  155. package/dist/email/templates/trial-warning.js +15 -32
  156. package/dist/email/templates/trial-warning.js.map +1 -1
  157. package/dist/email/templates/verify-email.d.ts +2 -2
  158. package/dist/email/templates/verify-email.d.ts.map +1 -1
  159. package/dist/email/templates/verify-email.js +10 -22
  160. package/dist/email/templates/verify-email.js.map +1 -1
  161. package/dist/email/templates/welcome.d.ts +2 -2
  162. package/dist/email/templates/welcome.d.ts.map +1 -1
  163. package/dist/email/templates/welcome.js +13 -20
  164. package/dist/email/templates/welcome.js.map +1 -1
  165. package/dist/email/templates/workspace-invitation.d.ts +2 -2
  166. package/dist/email/templates/workspace-invitation.d.ts.map +1 -1
  167. package/dist/email/templates/workspace-invitation.js +12 -26
  168. package/dist/email/templates/workspace-invitation.js.map +1 -1
  169. package/dist/env.d.ts +5 -10
  170. package/dist/env.d.ts.map +1 -1
  171. package/dist/env.js +41 -31
  172. package/dist/env.js.map +1 -1
  173. package/dist/identity/audit.d.ts +11 -24
  174. package/dist/identity/audit.d.ts.map +1 -1
  175. package/dist/identity/audit.js +13 -27
  176. package/dist/identity/audit.js.map +1 -1
  177. package/dist/identity/errors.d.ts +4 -12
  178. package/dist/identity/errors.d.ts.map +1 -1
  179. package/dist/identity/errors.js +2 -8
  180. package/dist/identity/errors.js.map +1 -1
  181. package/dist/identity/index.d.ts +6 -13
  182. package/dist/identity/index.d.ts.map +1 -1
  183. package/dist/identity/index.js +5 -12
  184. package/dist/identity/index.js.map +1 -1
  185. package/dist/identity/service.d.ts +29 -47
  186. package/dist/identity/service.d.ts.map +1 -1
  187. package/dist/identity/service.js +107 -79
  188. package/dist/identity/service.js.map +1 -1
  189. package/dist/identity/types.d.ts +28 -4
  190. package/dist/identity/types.d.ts.map +1 -1
  191. package/dist/identity/types.js +0 -3
  192. package/dist/identity/types.js.map +1 -1
  193. package/dist/index.d.ts +1 -2
  194. package/dist/index.d.ts.map +1 -1
  195. package/dist/index.js +3 -10
  196. package/dist/index.js.map +1 -1
  197. package/dist/logger.d.ts +7 -18
  198. package/dist/logger.d.ts.map +1 -1
  199. package/dist/logger.js +45 -49
  200. package/dist/logger.js.map +1 -1
  201. package/dist/money.d.ts +95 -0
  202. package/dist/money.d.ts.map +1 -0
  203. package/dist/money.js +208 -0
  204. package/dist/money.js.map +1 -0
  205. package/dist/notifications/index.d.ts +9 -43
  206. package/dist/notifications/index.d.ts.map +1 -1
  207. package/dist/notifications/index.js +9 -49
  208. package/dist/notifications/index.js.map +1 -1
  209. package/dist/notifications/triggers.d.ts +9 -32
  210. package/dist/notifications/triggers.d.ts.map +1 -1
  211. package/dist/notifications/triggers.js +21 -58
  212. package/dist/notifications/triggers.js.map +1 -1
  213. package/dist/notifications/types.d.ts +6 -8
  214. package/dist/notifications/types.d.ts.map +1 -1
  215. package/dist/notifications/types.js +0 -9
  216. package/dist/notifications/types.js.map +1 -1
  217. package/dist/prompt.d.ts +38 -0
  218. package/dist/prompt.d.ts.map +1 -0
  219. package/dist/prompt.js +118 -0
  220. package/dist/prompt.js.map +1 -0
  221. package/dist/registry.d.ts +32 -0
  222. package/dist/registry.d.ts.map +1 -0
  223. package/dist/registry.js +60 -0
  224. package/dist/registry.js.map +1 -0
  225. package/dist/request-context.d.ts +49 -0
  226. package/dist/request-context.d.ts.map +1 -0
  227. package/dist/request-context.js +100 -0
  228. package/dist/request-context.js.map +1 -0
  229. package/dist/storage/index.d.ts +65 -0
  230. package/dist/storage/index.d.ts.map +1 -0
  231. package/dist/storage/index.js +109 -0
  232. package/dist/storage/index.js.map +1 -0
  233. package/package.json +74 -24
  234. package/src/attachments/errors.ts +18 -0
  235. package/src/attachments/index.ts +22 -0
  236. package/src/attachments/service.ts +178 -0
  237. package/src/attachments/types.ts +6 -0
  238. package/src/conversations/errors.ts +45 -0
  239. package/src/conversations/index.ts +39 -0
  240. package/src/conversations/service.ts +594 -0
  241. package/src/conversations/types.ts +15 -0
  242. package/src/db/client.ts +91 -0
  243. package/src/db/index.ts +3 -0
  244. package/src/db/migrations/0000_baseline.sql +652 -0
  245. package/src/db/migrations/0001_reconcile_chain_built_databases.sql +53 -0
  246. package/src/db/migrations/0002_webhook_claim_lease.sql +1 -0
  247. package/src/db/migrations/0003_local_payments.sql +18 -0
  248. package/src/db/migrations/README.md +103 -36
  249. package/src/db/migrations/legacy-chain.json +206 -0
  250. package/src/db/migrations/meta/0000_snapshot.json +4182 -157
  251. package/src/db/migrations/meta/0002_snapshot.json +2478 -368
  252. package/src/db/migrations/meta/0003_snapshot.json +2567 -554
  253. package/src/db/migrations/meta/_journal.json +8 -274
  254. package/src/db/schema/agents.ts +57 -0
  255. package/src/db/schema/ai.ts +174 -0
  256. package/src/db/schema/auth.ts +183 -0
  257. package/src/db/schema/billing.ts +245 -0
  258. package/src/db/schema/feature-flags.ts +20 -0
  259. package/src/db/schema/identity.ts +250 -0
  260. package/src/db/schema/index.ts +12 -0
  261. package/src/db/schema/notifications.ts +31 -0
  262. package/src/db/schema/usage.ts +269 -0
  263. package/src/db/validate.ts +63 -0
  264. package/src/db/workspace-queries.ts +64 -0
  265. package/src/documents/classifier.ts +61 -0
  266. package/src/documents/errors.ts +44 -0
  267. package/src/documents/index.ts +24 -0
  268. package/src/documents/service.ts +256 -0
  269. package/src/documents/types.ts +24 -0
  270. package/src/email/brand.ts +24 -0
  271. package/src/email/index.ts +19 -0
  272. package/src/email/provider.ts +278 -0
  273. package/src/email/send.ts +169 -0
  274. package/src/email/senders.ts +292 -0
  275. package/src/email/templates/base-layout.tsx +145 -0
  276. package/src/email/templates/index.ts +28 -0
  277. package/src/email/templates/password-reset.tsx +72 -0
  278. package/src/email/templates/payment-failed.tsx +77 -0
  279. package/src/email/templates/quota-warning.tsx +118 -0
  280. package/src/email/templates/subscription-confirmed.tsx +91 -0
  281. package/src/email/templates/trial-expiry.tsx +124 -0
  282. package/src/email/templates/trial-warning.tsx +104 -0
  283. package/src/email/templates/verify-email.tsx +72 -0
  284. package/src/email/templates/welcome.tsx +64 -0
  285. package/src/email/templates/workspace-invitation.tsx +105 -0
  286. package/src/env.ts +180 -0
  287. package/src/identity/audit.ts +65 -0
  288. package/src/identity/errors.ts +44 -0
  289. package/src/identity/index.ts +29 -0
  290. package/src/identity/service.ts +303 -0
  291. package/src/identity/types.ts +58 -0
  292. package/src/index.ts +4 -0
  293. package/src/logger.ts +198 -0
  294. package/src/money.ts +270 -0
  295. package/src/notifications/index.ts +112 -0
  296. package/src/notifications/triggers.ts +158 -0
  297. package/src/notifications/types.ts +26 -0
  298. package/src/prompt.ts +142 -0
  299. package/src/registry.ts +75 -0
  300. package/src/request-context.ts +137 -0
  301. package/src/storage/index.ts +153 -0
  302. package/dist/db/schema/rag.d.ts +0 -171
  303. package/dist/db/schema/rag.d.ts.map +0 -1
  304. package/dist/db/schema/rag.js +0 -42
  305. package/dist/db/schema/rag.js.map +0 -1
  306. package/src/db/migrations/0000_sharp_night_nurse.sql +0 -52
  307. package/src/db/migrations/0001_hesitant_vapor.sql +0 -40
  308. package/src/db/migrations/0002_numerous_lady_vermin.sql +0 -235
  309. package/src/db/migrations/0003_mighty_jean_grey.sql +0 -37
  310. package/src/db/migrations/0004_cynical_grandmaster.sql +0 -2
  311. package/src/db/migrations/0005_careful_switch.sql +0 -8
  312. package/src/db/migrations/0006_tense_blue_marvel.sql +0 -16
  313. package/src/db/migrations/0007_next_squadron_sinister.sql +0 -1
  314. package/src/db/migrations/0008_powerful_scream.sql +0 -39
  315. package/src/db/migrations/0009_sad_ink.sql +0 -1
  316. package/src/db/migrations/0010_shocking_omega_flight.sql +0 -2
  317. package/src/db/migrations/0011_ambiguous_otto_octavius.sql +0 -151
  318. package/src/db/migrations/0011_hnsw_vector_index.sql +0 -17
  319. package/src/db/migrations/0012_user_profiles.sql +0 -30
  320. package/src/db/migrations/0013_trending_seasonal_columns.sql +0 -9
  321. package/src/db/migrations/0014_add_product_type_to_agents.sql +0 -6
  322. package/src/db/migrations/0015_new_v1_tables.sql +0 -149
  323. package/src/db/migrations/0016_document_types.sql +0 -30
  324. package/src/db/migrations/0017_document_metadata.sql +0 -2
  325. package/src/db/migrations/0018_identity_graph.sql +0 -200
  326. package/src/db/migrations/0019_db_driven_prompts.sql +0 -20
  327. package/src/db/migrations/0020_pending_extractions.sql +0 -35
  328. package/src/db/migrations/0021_rag_documents.sql +0 -39
  329. package/src/db/migrations/0022_user_quotas_usage_jsonb.sql +0 -70
  330. package/src/db/migrations/0023_user_facts_user_scoped_unique.sql +0 -17
  331. package/src/db/migrations/0024_user_memories_hnsw.sql +0 -23
  332. package/src/db/migrations/0025_audit_trigger_errcode.sql +0 -21
  333. package/src/db/migrations/0026_user_profile_snapshots_composite_pk.sql +0 -17
  334. package/src/db/migrations/0027_billing_settings_mnt_columns.sql +0 -54
  335. package/src/db/migrations/0028_trial_credits_normalized_email.sql +0 -37
  336. package/src/db/migrations/0029_usage_records_conversation_idx.sql +0 -13
  337. package/src/db/migrations/0030_pending_extractions_composite_idx.sql +0 -11
  338. package/src/db/migrations/0031_rag_documents_hnsw.sql +0 -16
  339. package/src/db/migrations/0032_rate_limit_unique_idx.sql +0 -16
  340. package/src/db/migrations/0033_pending_extractions_partial_idx.sql +0 -18
  341. package/src/db/migrations/0034_rate_limit_count.sql +0 -9
  342. package/src/db/migrations/0035_credit_reservations.sql +0 -24
  343. package/src/db/migrations/0036_executions_audit_jobs.sql +0 -96
  344. package/src/db/migrations/0037_rate_limit_minute_bucket.sql +0 -46
  345. package/src/db/migrations/0038_user_quotas_generic_usage.sql +0 -25
  346. package/src/db/migrations/0039_platform_admin_impersonation.sql +0 -27
  347. package/src/db/migrations/0040_ee_audit_chain.sql +0 -23
  348. package/src/db/migrations/meta/0001_snapshot.json +0 -631
  349. package/src/db/migrations/meta/0004_snapshot.json +0 -2500
  350. package/src/db/migrations/meta/0005_snapshot.json +0 -2548
  351. package/src/db/migrations/meta/0006_snapshot.json +0 -2650
  352. package/src/db/migrations/meta/0007_snapshot.json +0 -2657
  353. package/src/db/migrations/meta/0008_snapshot.json +0 -2719
  354. package/src/db/migrations/meta/0009_snapshot.json +0 -2726
  355. package/src/db/migrations/meta/0010_snapshot.json +0 -2739
  356. package/src/db/migrations/meta/0011_snapshot.json +0 -3886
@@ -0,0 +1,278 @@
1
+ import { Resend } from "resend";
2
+
3
+ // The provider is chosen at runtime from EMAIL_PROVIDER or the API keys set:
4
+ // - ResendProvider sends the rendered React Email HTML.
5
+ // - LoopsProvider sends by template id (Loops accepts no raw HTML).
6
+ // - ConsoleProvider is the development fallback when no key is set.
7
+
8
+ /**
9
+ * Provider-neutral template descriptor attached to every framework email.
10
+ *
11
+ * HTML-capable providers (Resend) ignore it — they send the rendered HTML.
12
+ * Template-based providers (Loops) require it: `key` is resolved to the
13
+ * provider-side template id and `variables` become the template's dynamic
14
+ * data. Keys are stable framework identifiers, e.g. "verify-email".
15
+ */
16
+ export interface EmailTemplateRef {
17
+ key: string;
18
+ variables?: Record<string, string | number>;
19
+ }
20
+
21
+ export interface EmailSendParams {
22
+ to: string | string[];
23
+ subject: string;
24
+ html: string;
25
+ from?: string;
26
+ replyTo?: string;
27
+ template?: EmailTemplateRef;
28
+ }
29
+
30
+ export interface EmailProvider {
31
+ send(params: EmailSendParams): Promise<{ id: string }>;
32
+ }
33
+
34
+ /** Error with an HTTP-ish status code so send.ts can decide retryability. */
35
+ function statusError(message: string, statusCode: number): Error {
36
+ const err = new Error(message) as Error & { statusCode?: number };
37
+ err.statusCode = statusCode;
38
+ return err;
39
+ }
40
+
41
+ export class ResendProvider implements EmailProvider {
42
+ private client: Resend;
43
+
44
+ constructor(apiKey: string) {
45
+ this.client = new Resend(apiKey);
46
+ }
47
+
48
+ async send(params: EmailSendParams): Promise<{ id: string }> {
49
+ // A sender is an address on a domain the deployment verified with
50
+ // Resend, which only the deployment knows.
51
+ if (!params.from) {
52
+ throw statusError(
53
+ `No sender for "${params.subject}" — set EMAIL_FROM to an address on a domain verified with Resend, e.g. "Acme <noreply@acme.com>"`,
54
+ 400
55
+ );
56
+ }
57
+ const { data, error } = await this.client.emails.send({
58
+ from: params.from,
59
+ to: Array.isArray(params.to) ? params.to : [params.to],
60
+ subject: params.subject,
61
+ html: params.html,
62
+ replyTo: params.replyTo,
63
+ });
64
+
65
+ if (error) {
66
+ const err = new Error(error.message) as Error & { statusCode?: number };
67
+ // Resend error names map to HTTP status categories
68
+ if (
69
+ error.name === "validation_error" ||
70
+ error.name === "missing_required_field"
71
+ ) {
72
+ err.statusCode = 400;
73
+ }
74
+ throw err;
75
+ }
76
+
77
+ return { id: data?.id ?? "unknown" };
78
+ }
79
+ }
80
+
81
+ // Loops transactional emails are designed in the Loops dashboard and sent by
82
+ // template id — the API accepts no raw HTML and exactly one recipient per
83
+ // call. Template keys resolve to ids through (in order):
84
+ // 1. the `transactionalIds` map passed to the constructor
85
+ // 2. env: LOOPS_TRANSACTIONAL_ID_<KEY> (key upper-snaked,
86
+ // e.g. "verify-email" → LOOPS_TRANSACTIONAL_ID_VERIFY_EMAIL)
87
+ // `from`/`replyTo` are ignored — the sender is configured per template in
88
+ // Loops itself.
89
+
90
+ const LOOPS_API_URL = "https://app.loops.so/api/v1/transactional";
91
+
92
+ export class LoopsProvider implements EmailProvider {
93
+ constructor(
94
+ private apiKey: string,
95
+ private transactionalIds?: Record<string, string>
96
+ ) {}
97
+
98
+ private resolveTransactionalId(key: string): string | undefined {
99
+ const mapped = this.transactionalIds?.[key];
100
+ if (mapped) {
101
+ return mapped;
102
+ }
103
+ const envName = `LOOPS_TRANSACTIONAL_ID_${key
104
+ .toUpperCase()
105
+ .replace(/-/g, "_")}`;
106
+ return process.env[envName];
107
+ }
108
+
109
+ async send(params: EmailSendParams): Promise<{ id: string }> {
110
+ const template = params.template;
111
+ if (!template) {
112
+ throw statusError(
113
+ `Loops requires a template key (subject: "${params.subject}") — Loops cannot send raw HTML. Use a framework sender or pass { template }.`,
114
+ 400
115
+ );
116
+ }
117
+
118
+ const transactionalId = this.resolveTransactionalId(template.key);
119
+ if (!transactionalId) {
120
+ throw statusError(
121
+ `No Loops transactional id mapped for template "${template.key}". Set LOOPS_TRANSACTIONAL_ID_${template.key
122
+ .toUpperCase()
123
+ .replace(
124
+ /-/g,
125
+ "_"
126
+ )} or pass a transactionalIds map to LoopsProvider.`,
127
+ 400
128
+ );
129
+ }
130
+
131
+ const recipients = Array.isArray(params.to) ? params.to : [params.to];
132
+
133
+ // Loops accepts one recipient per request. Sequential, so a failure
134
+ // surfaces with normal retry semantics in send.ts.
135
+ for (const email of recipients) {
136
+ const response = await fetch(LOOPS_API_URL, {
137
+ method: "POST",
138
+ headers: {
139
+ Authorization: `Bearer ${this.apiKey}`,
140
+ "Content-Type": "application/json",
141
+ },
142
+ body: JSON.stringify({
143
+ transactionalId,
144
+ email,
145
+ dataVariables: template.variables ?? {},
146
+ }),
147
+ });
148
+
149
+ if (!response.ok) {
150
+ let detail = "";
151
+ try {
152
+ const body = (await response.json()) as {
153
+ message?: string;
154
+ error?: { message?: string };
155
+ };
156
+ detail = body.message ?? body.error?.message ?? "";
157
+ } catch {
158
+ // Non-JSON error body — status code alone is enough
159
+ }
160
+ throw statusError(
161
+ `Loops send failed (${response.status}) for template "${template.key}"${detail ? `: ${detail}` : ""}`,
162
+ response.status
163
+ );
164
+ }
165
+ }
166
+
167
+ // Loops returns { success: true } with no message id
168
+ return { id: `loops:${transactionalId}` };
169
+ }
170
+ }
171
+
172
+ export class ConsoleProvider implements EmailProvider {
173
+ async send(params: EmailSendParams): Promise<{ id: string }> {
174
+ const recipient = Array.isArray(params.to)
175
+ ? params.to.join(", ")
176
+ : params.to;
177
+
178
+ console.log(
179
+ `[Email] Would send to: ${recipient}, subject: ${params.subject}`
180
+ );
181
+
182
+ return { id: `console-${Date.now()}` };
183
+ }
184
+ }
185
+
186
+ export type EmailProviderName = "resend" | "loops" | "console";
187
+
188
+ let cachedProvider: EmailProvider | null = null;
189
+
190
+ /** Test-only: clear the cached provider so env changes take effect. */
191
+ export function resetEmailProviderCache(): void {
192
+ cachedProvider = null;
193
+ }
194
+
195
+ /**
196
+ * The email provider for this environment, cached after the first call.
197
+ *
198
+ * Priority:
199
+ * 1. EMAIL_PROVIDER env var → explicit selection (with API key validation)
200
+ * 2. RESEND_API_KEY → ResendProvider (auto-detect; preferred because it can
201
+ * send every framework email without per-template setup)
202
+ * 3. LOOPS_API_KEY → LoopsProvider (auto-detect; needs
203
+ * LOOPS_TRANSACTIONAL_ID_* mappings for the templates you use)
204
+ * 4. (none) → ConsoleProvider
205
+ */
206
+ export function getEmailProvider(): EmailProvider {
207
+ if (cachedProvider) {
208
+ return cachedProvider;
209
+ }
210
+
211
+ const explicit = process.env.EMAIL_PROVIDER?.toLowerCase();
212
+
213
+ if (explicit) {
214
+ cachedProvider = resolveExplicitProvider(explicit);
215
+ if (cachedProvider) {
216
+ return cachedProvider;
217
+ }
218
+ // Unknown value — warn and fall through to auto-detect
219
+ console.warn(
220
+ `[Email] Unknown EMAIL_PROVIDER="${process.env.EMAIL_PROVIDER}", falling back to auto-detect`
221
+ );
222
+ }
223
+
224
+ // Auto-detect from the API keys set
225
+ const resendKey = process.env.RESEND_API_KEY;
226
+ if (resendKey) {
227
+ console.log("[Email] Using Resend provider");
228
+ cachedProvider = new ResendProvider(resendKey);
229
+ return cachedProvider;
230
+ }
231
+
232
+ const loopsKey = process.env.LOOPS_API_KEY;
233
+ if (loopsKey) {
234
+ console.log("[Email] Using Loops provider");
235
+ cachedProvider = new LoopsProvider(loopsKey);
236
+ return cachedProvider;
237
+ }
238
+
239
+ console.log("[Email] Using console provider (no API key set)");
240
+ cachedProvider = new ConsoleProvider();
241
+ return cachedProvider;
242
+ }
243
+
244
+ /**
245
+ * Resolve an explicit EMAIL_PROVIDER value to a provider instance.
246
+ * Returns null for unknown values so the caller can fall through to auto-detect.
247
+ */
248
+ function resolveExplicitProvider(provider: string): EmailProvider | null {
249
+ switch (provider) {
250
+ case "resend": {
251
+ const key = process.env.RESEND_API_KEY;
252
+ if (!key) {
253
+ console.warn(
254
+ "[Email] EMAIL_PROVIDER=resend but RESEND_API_KEY is not set, falling back to console"
255
+ );
256
+ return new ConsoleProvider();
257
+ }
258
+ console.log("[Email] Using Resend provider (explicit)");
259
+ return new ResendProvider(key);
260
+ }
261
+ case "loops": {
262
+ const key = process.env.LOOPS_API_KEY;
263
+ if (!key) {
264
+ console.warn(
265
+ "[Email] EMAIL_PROVIDER=loops but LOOPS_API_KEY is not set, falling back to console"
266
+ );
267
+ return new ConsoleProvider();
268
+ }
269
+ console.log("[Email] Using Loops provider (explicit)");
270
+ return new LoopsProvider(key);
271
+ }
272
+ case "console":
273
+ console.log("[Email] Using console provider (explicit)");
274
+ return new ConsoleProvider();
275
+ default:
276
+ return null;
277
+ }
278
+ }
@@ -0,0 +1,169 @@
1
+ import { render } from "@react-email/components";
2
+ import type React from "react";
3
+ import { getEmailProvider, type EmailTemplateRef } from "./provider";
4
+
5
+ // Wraps the provider with the deployment's sender (EMAIL_FROM), React Email
6
+ // rendering, and up to 3 attempts with exponential backoff (1s, 2s, 4s) on
7
+ // 5xx, 429 and network errors. Never throws; returns a result object.
8
+
9
+ export interface SendEmailParams {
10
+ to: string | string[];
11
+ subject: string;
12
+ html: string;
13
+ /**
14
+ * Defaults to the EMAIL_FROM env var. There is no built-in sender: a
15
+ * provider that needs one refuses the send when neither is set.
16
+ */
17
+ from?: string;
18
+ replyTo?: string;
19
+ /**
20
+ * Provider-neutral template descriptor. Required by template-based
21
+ * providers (Loops); ignored by HTML-capable providers (Resend).
22
+ */
23
+ template?: EmailTemplateRef;
24
+ }
25
+
26
+ export interface SendEmailWithComponentParams {
27
+ to: string | string[];
28
+ subject: string;
29
+ /** A React Email component element. Will be rendered to HTML before sending. */
30
+ react: React.ReactElement;
31
+ /**
32
+ * Defaults to the EMAIL_FROM env var. There is no built-in sender: a
33
+ * provider that needs one refuses the send when neither is set.
34
+ */
35
+ from?: string;
36
+ replyTo?: string;
37
+ /**
38
+ * Provider-neutral template descriptor. Required by template-based
39
+ * providers (Loops); ignored by HTML-capable providers (Resend).
40
+ */
41
+ template?: EmailTemplateRef;
42
+ }
43
+
44
+ export interface SendEmailResult {
45
+ success: boolean;
46
+ id?: string;
47
+ error?: string;
48
+ }
49
+
50
+ const MAX_ATTEMPTS = 3;
51
+ const BASE_DELAY_MS = 1000;
52
+
53
+ /**
54
+ * Returns true if the error is a client error (4xx) that should NOT be retried.
55
+ */
56
+ function isClientError(error: unknown): boolean {
57
+ if (error && typeof error === "object") {
58
+ const err = error as Record<string, unknown>;
59
+
60
+ // 429 is excluded: rate limits are transient and worth retrying.
61
+ if (typeof err.statusCode === "number") {
62
+ return (
63
+ err.statusCode >= 400 && err.statusCode < 500 && err.statusCode !== 429
64
+ );
65
+ }
66
+
67
+ if (typeof err.status === "number") {
68
+ return err.status >= 400 && err.status < 500 && err.status !== 429;
69
+ }
70
+
71
+ // Resend SDK error names that indicate client errors
72
+ const name = err.name as string | undefined;
73
+ if (
74
+ name === "validation_error" ||
75
+ name === "missing_required_field" ||
76
+ name === "invalid_api_Key"
77
+ ) {
78
+ return true;
79
+ }
80
+ }
81
+
82
+ return false;
83
+ }
84
+
85
+ function delay(ms: number): Promise<void> {
86
+ return new Promise((resolve) => setTimeout(resolve, ms));
87
+ }
88
+
89
+ /**
90
+ * Sends an email through the configured provider, retrying transient
91
+ * failures. Takes raw HTML via `{ html }` or a React Email element via
92
+ * `{ react }`. Never throws: failures return `{ success: false, error }`.
93
+ */
94
+ export async function sendEmail(
95
+ params: SendEmailParams | SendEmailWithComponentParams
96
+ ): Promise<SendEmailResult> {
97
+ try {
98
+ const provider = getEmailProvider();
99
+ const from = params.from ?? (process.env.EMAIL_FROM?.trim() || undefined);
100
+
101
+ const recipient = Array.isArray(params.to)
102
+ ? params.to.join(", ")
103
+ : params.to;
104
+
105
+ let html: string;
106
+ if ("react" in params) {
107
+ try {
108
+ html = await render(params.react);
109
+ } catch (renderError) {
110
+ const message =
111
+ renderError instanceof Error
112
+ ? renderError.message
113
+ : String(renderError);
114
+ console.error(`[Email] Failed to render React component: ${message}`);
115
+ return { success: false, error: `Template render failed: ${message}` };
116
+ }
117
+ } else {
118
+ html = params.html;
119
+ }
120
+
121
+ let lastError: unknown;
122
+
123
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
124
+ try {
125
+ const result = await provider.send({
126
+ to: params.to,
127
+ subject: params.subject,
128
+ html,
129
+ from,
130
+ replyTo: params.replyTo,
131
+ template: params.template,
132
+ });
133
+
134
+ return { success: true, id: result.id };
135
+ } catch (error) {
136
+ lastError = error;
137
+ const message = error instanceof Error ? error.message : String(error);
138
+
139
+ if (isClientError(error)) {
140
+ console.error(
141
+ `[Email] Send failed for ${recipient} (client error, not retrying): ${message}`
142
+ );
143
+ return { success: false, error: message };
144
+ }
145
+
146
+ console.warn(
147
+ `[Email] Send attempt ${attempt}/${MAX_ATTEMPTS} failed for ${recipient}: ${message}`
148
+ );
149
+
150
+ if (attempt < MAX_ATTEMPTS) {
151
+ await delay(BASE_DELAY_MS * Math.pow(2, attempt - 1));
152
+ }
153
+ }
154
+ }
155
+
156
+ const finalMessage =
157
+ lastError instanceof Error ? lastError.message : String(lastError);
158
+ console.error(
159
+ `[Email] All ${MAX_ATTEMPTS} attempts failed for ${recipient}: ${params.subject}`
160
+ );
161
+
162
+ return { success: false, error: finalMessage };
163
+ } catch (error) {
164
+ // Catch-all for unexpected errors (e.g., getEmailProvider() failing)
165
+ const message = error instanceof Error ? error.message : String(error);
166
+ console.error(`[Email] Unexpected error: ${message}`);
167
+ return { success: false, error: message };
168
+ }
169
+ }