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

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 (355) 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 +136 -107
  56. package/dist/db/schema/billing.d.ts.map +1 -1
  57. package/dist/db/schema/billing.js +72 -60
  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 +5 -13
  182. package/dist/identity/index.d.ts.map +1 -1
  183. package/dist/identity/index.js +4 -12
  184. package/dist/identity/index.js.map +1 -1
  185. package/dist/identity/service.d.ts +13 -47
  186. package/dist/identity/service.d.ts.map +1 -1
  187. package/dist/identity/service.js +31 -78
  188. package/dist/identity/service.js.map +1 -1
  189. package/dist/identity/types.d.ts +1 -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 +1 -10
  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/README.md +74 -36
  248. package/src/db/migrations/legacy-chain.json +197 -0
  249. package/src/db/migrations/meta/0000_snapshot.json +4182 -157
  250. package/src/db/migrations/meta/0002_snapshot.json +2478 -368
  251. package/src/db/migrations/meta/_journal.json +6 -279
  252. package/src/db/schema/agents.ts +57 -0
  253. package/src/db/schema/ai.ts +174 -0
  254. package/src/db/schema/auth.ts +183 -0
  255. package/src/db/schema/billing.ts +209 -0
  256. package/src/db/schema/feature-flags.ts +20 -0
  257. package/src/db/schema/identity.ts +250 -0
  258. package/src/db/schema/index.ts +12 -0
  259. package/src/db/schema/notifications.ts +31 -0
  260. package/src/db/schema/usage.ts +269 -0
  261. package/src/db/validate.ts +63 -0
  262. package/src/db/workspace-queries.ts +64 -0
  263. package/src/documents/classifier.ts +61 -0
  264. package/src/documents/errors.ts +44 -0
  265. package/src/documents/index.ts +24 -0
  266. package/src/documents/service.ts +256 -0
  267. package/src/documents/types.ts +24 -0
  268. package/src/email/brand.ts +24 -0
  269. package/src/email/index.ts +19 -0
  270. package/src/email/provider.ts +278 -0
  271. package/src/email/send.ts +169 -0
  272. package/src/email/senders.ts +292 -0
  273. package/src/email/templates/base-layout.tsx +145 -0
  274. package/src/email/templates/index.ts +28 -0
  275. package/src/email/templates/password-reset.tsx +72 -0
  276. package/src/email/templates/payment-failed.tsx +77 -0
  277. package/src/email/templates/quota-warning.tsx +118 -0
  278. package/src/email/templates/subscription-confirmed.tsx +91 -0
  279. package/src/email/templates/trial-expiry.tsx +124 -0
  280. package/src/email/templates/trial-warning.tsx +104 -0
  281. package/src/email/templates/verify-email.tsx +72 -0
  282. package/src/email/templates/welcome.tsx +64 -0
  283. package/src/email/templates/workspace-invitation.tsx +105 -0
  284. package/src/env.ts +180 -0
  285. package/src/identity/audit.ts +65 -0
  286. package/src/identity/errors.ts +44 -0
  287. package/src/identity/index.ts +26 -0
  288. package/src/identity/service.ts +202 -0
  289. package/src/identity/types.ts +28 -0
  290. package/src/index.ts +4 -0
  291. package/src/logger.ts +198 -0
  292. package/src/money.ts +270 -0
  293. package/src/notifications/index.ts +108 -0
  294. package/src/notifications/triggers.ts +158 -0
  295. package/src/notifications/types.ts +18 -0
  296. package/src/prompt.ts +142 -0
  297. package/src/registry.ts +75 -0
  298. package/src/request-context.ts +137 -0
  299. package/src/storage/index.ts +153 -0
  300. package/dist/db/schema/rag.d.ts +0 -171
  301. package/dist/db/schema/rag.d.ts.map +0 -1
  302. package/dist/db/schema/rag.js +0 -42
  303. package/dist/db/schema/rag.js.map +0 -1
  304. package/src/db/migrations/0000_sharp_night_nurse.sql +0 -52
  305. package/src/db/migrations/0001_hesitant_vapor.sql +0 -40
  306. package/src/db/migrations/0002_numerous_lady_vermin.sql +0 -235
  307. package/src/db/migrations/0003_mighty_jean_grey.sql +0 -37
  308. package/src/db/migrations/0004_cynical_grandmaster.sql +0 -2
  309. package/src/db/migrations/0005_careful_switch.sql +0 -8
  310. package/src/db/migrations/0006_tense_blue_marvel.sql +0 -16
  311. package/src/db/migrations/0007_next_squadron_sinister.sql +0 -1
  312. package/src/db/migrations/0008_powerful_scream.sql +0 -39
  313. package/src/db/migrations/0009_sad_ink.sql +0 -1
  314. package/src/db/migrations/0010_shocking_omega_flight.sql +0 -2
  315. package/src/db/migrations/0011_ambiguous_otto_octavius.sql +0 -151
  316. package/src/db/migrations/0011_hnsw_vector_index.sql +0 -17
  317. package/src/db/migrations/0012_user_profiles.sql +0 -30
  318. package/src/db/migrations/0013_trending_seasonal_columns.sql +0 -9
  319. package/src/db/migrations/0014_add_product_type_to_agents.sql +0 -6
  320. package/src/db/migrations/0015_new_v1_tables.sql +0 -149
  321. package/src/db/migrations/0016_document_types.sql +0 -30
  322. package/src/db/migrations/0017_document_metadata.sql +0 -2
  323. package/src/db/migrations/0018_identity_graph.sql +0 -200
  324. package/src/db/migrations/0019_db_driven_prompts.sql +0 -20
  325. package/src/db/migrations/0020_pending_extractions.sql +0 -35
  326. package/src/db/migrations/0021_rag_documents.sql +0 -39
  327. package/src/db/migrations/0022_user_quotas_usage_jsonb.sql +0 -70
  328. package/src/db/migrations/0023_user_facts_user_scoped_unique.sql +0 -17
  329. package/src/db/migrations/0024_user_memories_hnsw.sql +0 -23
  330. package/src/db/migrations/0025_audit_trigger_errcode.sql +0 -21
  331. package/src/db/migrations/0026_user_profile_snapshots_composite_pk.sql +0 -17
  332. package/src/db/migrations/0027_billing_settings_mnt_columns.sql +0 -54
  333. package/src/db/migrations/0028_trial_credits_normalized_email.sql +0 -37
  334. package/src/db/migrations/0029_usage_records_conversation_idx.sql +0 -13
  335. package/src/db/migrations/0030_pending_extractions_composite_idx.sql +0 -11
  336. package/src/db/migrations/0031_rag_documents_hnsw.sql +0 -16
  337. package/src/db/migrations/0032_rate_limit_unique_idx.sql +0 -16
  338. package/src/db/migrations/0033_pending_extractions_partial_idx.sql +0 -18
  339. package/src/db/migrations/0034_rate_limit_count.sql +0 -9
  340. package/src/db/migrations/0035_credit_reservations.sql +0 -24
  341. package/src/db/migrations/0036_executions_audit_jobs.sql +0 -96
  342. package/src/db/migrations/0037_rate_limit_minute_bucket.sql +0 -46
  343. package/src/db/migrations/0038_user_quotas_generic_usage.sql +0 -25
  344. package/src/db/migrations/0039_platform_admin_impersonation.sql +0 -27
  345. package/src/db/migrations/0040_ee_audit_chain.sql +0 -23
  346. package/src/db/migrations/meta/0001_snapshot.json +0 -631
  347. package/src/db/migrations/meta/0003_snapshot.json +0 -2488
  348. package/src/db/migrations/meta/0004_snapshot.json +0 -2500
  349. package/src/db/migrations/meta/0005_snapshot.json +0 -2548
  350. package/src/db/migrations/meta/0006_snapshot.json +0 -2650
  351. package/src/db/migrations/meta/0007_snapshot.json +0 -2657
  352. package/src/db/migrations/meta/0008_snapshot.json +0 -2719
  353. package/src/db/migrations/meta/0009_snapshot.json +0 -2726
  354. package/src/db/migrations/meta/0010_snapshot.json +0 -2739
  355. package/src/db/migrations/meta/0011_snapshot.json +0 -3886
@@ -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
+ }
@@ -0,0 +1,292 @@
1
+ /**
2
+ * Typed wrappers that render a React Email template and send it. They never
3
+ * throw; each returns a SendEmailResult.
4
+ *
5
+ * Every sender attaches a stable `template` key and flat variables so
6
+ * template-based providers (Loops) can send the same email; HTML providers
7
+ * ignore it. Boolean variants use distinct keys (quota-warning /
8
+ * quota-exceeded, trial-warning / trial-depleted) because template providers
9
+ * branch per template, not per variable.
10
+ */
11
+
12
+ import * as React from "react";
13
+ import { emailBrand } from "./brand";
14
+ import { sendEmail, type SendEmailResult } from "./send";
15
+ import { formatMoney, type Money } from "../money";
16
+ import {
17
+ WelcomeEmail,
18
+ VerifyEmailEmail,
19
+ PasswordResetEmail,
20
+ WorkspaceInvitationEmail,
21
+ QuotaWarningEmail,
22
+ TrialWarningEmail,
23
+ TrialExpiryEmail,
24
+ PaymentFailedEmail,
25
+ SubscriptionConfirmedEmail,
26
+ } from "./templates";
27
+
28
+ /** The locale the email copy is written in; amounts are formatted to match. */
29
+ const EMAIL_LOCALE = "en";
30
+
31
+ export async function sendWelcomeEmail(params: {
32
+ to: string;
33
+ userName: string;
34
+ dashboardUrl: string;
35
+ }): Promise<SendEmailResult> {
36
+ return sendEmail({
37
+ to: params.to,
38
+ subject: emailBrand().name
39
+ ? `Welcome to ${emailBrand().name}!`
40
+ : "Welcome!",
41
+ react: React.createElement(WelcomeEmail, {
42
+ userName: params.userName,
43
+ dashboardUrl: params.dashboardUrl,
44
+ }),
45
+ template: {
46
+ key: "welcome",
47
+ variables: {
48
+ userName: params.userName,
49
+ dashboardUrl: params.dashboardUrl,
50
+ },
51
+ },
52
+ });
53
+ }
54
+
55
+ export async function sendVerifyEmail(params: {
56
+ to: string;
57
+ userName: string;
58
+ verificationUrl: string;
59
+ }): Promise<SendEmailResult> {
60
+ return sendEmail({
61
+ to: params.to,
62
+ subject: "Verify your email address",
63
+ react: React.createElement(VerifyEmailEmail, {
64
+ userName: params.userName,
65
+ verificationUrl: params.verificationUrl,
66
+ }),
67
+ template: {
68
+ key: "verify-email",
69
+ variables: {
70
+ userName: params.userName,
71
+ verificationUrl: params.verificationUrl,
72
+ },
73
+ },
74
+ });
75
+ }
76
+
77
+ export async function sendPasswordResetEmail(params: {
78
+ to: string;
79
+ userName: string;
80
+ resetUrl: string;
81
+ }): Promise<SendEmailResult> {
82
+ return sendEmail({
83
+ to: params.to,
84
+ subject: "Reset your password",
85
+ react: React.createElement(PasswordResetEmail, {
86
+ userName: params.userName,
87
+ resetUrl: params.resetUrl,
88
+ }),
89
+ template: {
90
+ key: "password-reset",
91
+ variables: {
92
+ userName: params.userName,
93
+ resetUrl: params.resetUrl,
94
+ },
95
+ },
96
+ });
97
+ }
98
+
99
+ export async function sendInvitationEmail(params: {
100
+ to: string;
101
+ inviterName: string;
102
+ workspaceName: string;
103
+ role: string;
104
+ acceptUrl: string;
105
+ declineUrl: string;
106
+ }): Promise<SendEmailResult> {
107
+ return sendEmail({
108
+ to: params.to,
109
+ subject: `${params.inviterName} invited you to ${params.workspaceName}`,
110
+ react: React.createElement(WorkspaceInvitationEmail, {
111
+ inviterName: params.inviterName,
112
+ workspaceName: params.workspaceName,
113
+ role: params.role,
114
+ acceptUrl: params.acceptUrl,
115
+ declineUrl: params.declineUrl,
116
+ }),
117
+ template: {
118
+ key: "workspace-invitation",
119
+ variables: {
120
+ inviterName: params.inviterName,
121
+ workspaceName: params.workspaceName,
122
+ role: params.role,
123
+ acceptUrl: params.acceptUrl,
124
+ declineUrl: params.declineUrl,
125
+ },
126
+ },
127
+ });
128
+ }
129
+
130
+ /**
131
+ * Send the allowance warning or used-up email. `used` and `allowance`
132
+ * are money in the deployment's billing currency; the template and the
133
+ * provider variables receive them formatted.
134
+ */
135
+ export async function sendQuotaWarningEmail(params: {
136
+ to: string;
137
+ workspaceName: string;
138
+ percentageUsed: number;
139
+ used: Money;
140
+ allowance: Money;
141
+ upgradeUrl: string;
142
+ isExceeded: boolean;
143
+ }): Promise<SendEmailResult> {
144
+ const used = formatMoney(params.used, EMAIL_LOCALE);
145
+ const allowance = formatMoney(params.allowance, EMAIL_LOCALE);
146
+
147
+ return sendEmail({
148
+ to: params.to,
149
+ subject: params.isExceeded
150
+ ? "Monthly allowance used up"
151
+ : `Monthly allowance at ${params.percentageUsed}%`,
152
+ react: React.createElement(QuotaWarningEmail, {
153
+ workspaceName: params.workspaceName,
154
+ percentageUsed: params.percentageUsed,
155
+ used,
156
+ allowance,
157
+ upgradeUrl: params.upgradeUrl,
158
+ isExceeded: params.isExceeded,
159
+ }),
160
+ template: {
161
+ key: params.isExceeded ? "quota-exceeded" : "quota-warning",
162
+ variables: {
163
+ workspaceName: params.workspaceName,
164
+ percentageUsed: params.percentageUsed,
165
+ used,
166
+ allowance,
167
+ currency: params.allowance.currency,
168
+ upgradeUrl: params.upgradeUrl,
169
+ },
170
+ },
171
+ });
172
+ }
173
+
174
+ /** Send trial warning or depleted email. */
175
+ export async function sendTrialWarningEmail(params: {
176
+ to: string;
177
+ workspaceName: string;
178
+ creditsRemaining: number;
179
+ totalCredits: number;
180
+ percentageRemaining: number;
181
+ upgradeUrl: string;
182
+ isDepleted: boolean;
183
+ }): Promise<SendEmailResult> {
184
+ return sendEmail({
185
+ to: params.to,
186
+ subject: params.isDepleted
187
+ ? "Trial credits depleted"
188
+ : "Trial credits running low",
189
+ react: React.createElement(TrialWarningEmail, {
190
+ workspaceName: params.workspaceName,
191
+ creditsRemaining: params.creditsRemaining,
192
+ totalCredits: params.totalCredits,
193
+ percentageRemaining: params.percentageRemaining,
194
+ upgradeUrl: params.upgradeUrl,
195
+ isDepleted: params.isDepleted,
196
+ }),
197
+ template: {
198
+ key: params.isDepleted ? "trial-depleted" : "trial-warning",
199
+ variables: {
200
+ workspaceName: params.workspaceName,
201
+ creditsRemaining: params.creditsRemaining,
202
+ totalCredits: params.totalCredits,
203
+ percentageRemaining: params.percentageRemaining,
204
+ upgradeUrl: params.upgradeUrl,
205
+ },
206
+ },
207
+ });
208
+ }
209
+
210
+ export async function sendPaymentFailedEmail(params: {
211
+ to: string;
212
+ workspaceName: string;
213
+ amount: string;
214
+ updatePaymentUrl: string;
215
+ }): Promise<SendEmailResult> {
216
+ return sendEmail({
217
+ to: params.to,
218
+ subject: `Payment failed for ${params.workspaceName}`,
219
+ react: React.createElement(PaymentFailedEmail, {
220
+ workspaceName: params.workspaceName,
221
+ amount: params.amount,
222
+ updatePaymentUrl: params.updatePaymentUrl,
223
+ }),
224
+ template: {
225
+ key: "payment-failed",
226
+ variables: {
227
+ workspaceName: params.workspaceName,
228
+ amount: params.amount,
229
+ updatePaymentUrl: params.updatePaymentUrl,
230
+ },
231
+ },
232
+ });
233
+ }
234
+
235
+ export async function sendSubscriptionConfirmedEmail(params: {
236
+ to: string;
237
+ workspaceName: string;
238
+ planName: string;
239
+ amount: string;
240
+ dashboardUrl: string;
241
+ }): Promise<SendEmailResult> {
242
+ return sendEmail({
243
+ to: params.to,
244
+ subject: `Subscription confirmed - ${params.planName} plan`,
245
+ react: React.createElement(SubscriptionConfirmedEmail, {
246
+ workspaceName: params.workspaceName,
247
+ planName: params.planName,
248
+ amount: params.amount,
249
+ dashboardUrl: params.dashboardUrl,
250
+ }),
251
+ template: {
252
+ key: "subscription-confirmed",
253
+ variables: {
254
+ workspaceName: params.workspaceName,
255
+ planName: params.planName,
256
+ amount: params.amount,
257
+ dashboardUrl: params.dashboardUrl,
258
+ },
259
+ },
260
+ });
261
+ }
262
+
263
+ export async function sendTrialExpiryEmail(params: {
264
+ to: string;
265
+ workspaceName: string;
266
+ expiryDate: string;
267
+ daysRemaining: number;
268
+ upgradeUrl: string;
269
+ }): Promise<SendEmailResult> {
270
+ return sendEmail({
271
+ to: params.to,
272
+ subject:
273
+ params.daysRemaining <= 0
274
+ ? `Your trial for ${params.workspaceName} has ended`
275
+ : `Your trial ends in ${params.daysRemaining} day${params.daysRemaining === 1 ? "" : "s"}`,
276
+ react: React.createElement(TrialExpiryEmail, {
277
+ workspaceName: params.workspaceName,
278
+ expiryDate: params.expiryDate,
279
+ daysRemaining: params.daysRemaining,
280
+ upgradeUrl: params.upgradeUrl,
281
+ }),
282
+ template: {
283
+ key: "trial-expiry",
284
+ variables: {
285
+ workspaceName: params.workspaceName,
286
+ expiryDate: params.expiryDate,
287
+ daysRemaining: params.daysRemaining,
288
+ upgradeUrl: params.upgradeUrl,
289
+ },
290
+ },
291
+ });
292
+ }
@@ -0,0 +1,145 @@
1
+ import {
2
+ Body,
3
+ Container,
4
+ Head,
5
+ Hr,
6
+ Html,
7
+ Link,
8
+ Preview,
9
+ Section,
10
+ Text,
11
+ } from "@react-email/components";
12
+ import * as React from "react";
13
+
14
+ import { emailBrand } from "../brand";
15
+
16
+ // Inline styles only: email clients do not reliably support CSS classes.
17
+ // Colors are literal values, not CSS variables, so they survive dark-mode
18
+ // email clients.
19
+
20
+ export interface BaseLayoutProps {
21
+ preview: string;
22
+ children: React.ReactNode;
23
+ }
24
+
25
+ const colors = {
26
+ background: "#f4f4f5", // zinc-100
27
+ container: "#ffffff",
28
+ text: "#18181b", // zinc-900
29
+ muted: "#71717a", // zinc-500
30
+ brand: "#7c3aed", // violet-600
31
+ border: "#e4e4e7", // zinc-200
32
+ } as const;
33
+
34
+ export function BaseLayout({ preview, children }: BaseLayoutProps) {
35
+ const brand = emailBrand();
36
+ return (
37
+ <Html lang="en">
38
+ <Head>
39
+ <meta charSet="utf-8" />
40
+ </Head>
41
+ <Preview>{preview}</Preview>
42
+ <Body style={bodyStyle}>
43
+ <Container style={containerStyle}>
44
+ {brand.name ? (
45
+ <Section style={headerStyle}>
46
+ <Text style={logoStyle}>{brand.name}</Text>
47
+ </Section>
48
+ ) : null}
49
+
50
+ <Hr style={hrStyle} />
51
+
52
+ <Section style={contentStyle}>{children}</Section>
53
+
54
+ <Hr style={hrStyle} />
55
+
56
+ <Section style={footerStyle}>
57
+ {brand.name ? (
58
+ <Text style={footerTextStyle}>{brand.name}</Text>
59
+ ) : null}
60
+ <Text style={footerMutedStyle}>
61
+ This is a transactional email
62
+ {brand.name ? ` from ${brand.name}` : ""}.
63
+ {brand.supportEmail ? (
64
+ <>
65
+ {" "}
66
+ If you believe you received this email in error, please
67
+ contact{" "}
68
+ <Link
69
+ href={`mailto:${brand.supportEmail}`}
70
+ style={footerLinkStyle}
71
+ >
72
+ {brand.supportEmail}
73
+ </Link>
74
+ .
75
+ </>
76
+ ) : null}
77
+ </Text>
78
+ </Section>
79
+ </Container>
80
+ </Body>
81
+ </Html>
82
+ );
83
+ }
84
+
85
+ const bodyStyle: React.CSSProperties = {
86
+ backgroundColor: colors.background,
87
+ fontFamily:
88
+ '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
89
+ margin: 0,
90
+ padding: "40px 0",
91
+ };
92
+
93
+ const containerStyle: React.CSSProperties = {
94
+ backgroundColor: colors.container,
95
+ borderRadius: "8px",
96
+ maxWidth: "600px",
97
+ margin: "0 auto",
98
+ border: `1px solid ${colors.border}`,
99
+ };
100
+
101
+ const headerStyle: React.CSSProperties = {
102
+ padding: "32px 40px 16px",
103
+ };
104
+
105
+ const logoStyle: React.CSSProperties = {
106
+ color: colors.brand,
107
+ fontSize: "24px",
108
+ fontWeight: 700,
109
+ margin: 0,
110
+ lineHeight: "32px",
111
+ };
112
+
113
+ const hrStyle: React.CSSProperties = {
114
+ borderColor: colors.border,
115
+ borderTop: "none",
116
+ margin: 0,
117
+ };
118
+
119
+ const contentStyle: React.CSSProperties = {
120
+ padding: "24px 40px",
121
+ };
122
+
123
+ const footerStyle: React.CSSProperties = {
124
+ padding: "16px 40px 32px",
125
+ };
126
+
127
+ const footerTextStyle: React.CSSProperties = {
128
+ color: colors.muted,
129
+ fontSize: "13px",
130
+ lineHeight: "20px",
131
+ margin: "0 0 8px",
132
+ fontWeight: 600,
133
+ };
134
+
135
+ const footerMutedStyle: React.CSSProperties = {
136
+ color: colors.muted,
137
+ fontSize: "12px",
138
+ lineHeight: "18px",
139
+ margin: 0,
140
+ };
141
+
142
+ const footerLinkStyle: React.CSSProperties = {
143
+ color: colors.brand,
144
+ textDecoration: "underline",
145
+ };
@@ -0,0 +1,28 @@
1
+ export { BaseLayout, type BaseLayoutProps } from "./base-layout";
2
+ export { WelcomeEmail, type WelcomeEmailProps } from "./welcome";
3
+ export { VerifyEmailEmail, type VerifyEmailEmailProps } from "./verify-email";
4
+ export {
5
+ PasswordResetEmail,
6
+ type PasswordResetEmailProps,
7
+ } from "./password-reset";
8
+ export {
9
+ WorkspaceInvitationEmail,
10
+ type WorkspaceInvitationEmailProps,
11
+ } from "./workspace-invitation";
12
+ export {
13
+ QuotaWarningEmail,
14
+ type QuotaWarningEmailProps,
15
+ } from "./quota-warning";
16
+ export {
17
+ TrialWarningEmail,
18
+ type TrialWarningEmailProps,
19
+ } from "./trial-warning";
20
+ export {
21
+ PaymentFailedEmail,
22
+ type PaymentFailedEmailProps,
23
+ } from "./payment-failed";
24
+ export {
25
+ SubscriptionConfirmedEmail,
26
+ type SubscriptionConfirmedEmailProps,
27
+ } from "./subscription-confirmed";
28
+ export { TrialExpiryEmail, type TrialExpiryEmailProps } from "./trial-expiry";
@@ -0,0 +1,72 @@
1
+ import { Button, Text } from "@react-email/components";
2
+ import * as React from "react";
3
+ import { BaseLayout } from "./base-layout";
4
+
5
+ export interface PasswordResetEmailProps {
6
+ userName: string;
7
+ resetUrl: string;
8
+ }
9
+
10
+ export function PasswordResetEmail({
11
+ userName,
12
+ resetUrl,
13
+ }: PasswordResetEmailProps) {
14
+ const text = {
15
+ preview: "Reset your password",
16
+ heading: "Reset your password",
17
+ greeting: "Hi",
18
+ body: "We received a request to reset your password. Click the button below to choose a new one.",
19
+ button: "Reset Password",
20
+ note: "This link expires in 1 hour. If you didn't request a password reset, you can safely ignore this email.",
21
+ };
22
+
23
+ return (
24
+ <BaseLayout preview={text.preview}>
25
+ <Text style={headingStyle}>{text.heading}</Text>
26
+ <Text style={bodyTextStyle}>
27
+ {text.greeting} {userName},
28
+ </Text>
29
+ <Text style={bodyTextStyle}>{text.body}</Text>
30
+ <Button href={resetUrl} style={buttonStyle}>
31
+ {text.button}
32
+ </Button>
33
+ <Text style={noteStyle}>{text.note}</Text>
34
+ </BaseLayout>
35
+ );
36
+ }
37
+
38
+ const headingStyle: React.CSSProperties = {
39
+ color: "#18181b",
40
+ fontSize: "22px",
41
+ fontWeight: 700,
42
+ lineHeight: "30px",
43
+ margin: "0 0 16px",
44
+ };
45
+
46
+ const bodyTextStyle: React.CSSProperties = {
47
+ color: "#18181b",
48
+ fontSize: "15px",
49
+ lineHeight: "24px",
50
+ margin: "0 0 12px",
51
+ };
52
+
53
+ const buttonStyle: React.CSSProperties = {
54
+ backgroundColor: "#7c3aed",
55
+ borderRadius: "6px",
56
+ color: "#ffffff",
57
+ display: "inline-block",
58
+ fontSize: "15px",
59
+ fontWeight: 600,
60
+ lineHeight: "100%",
61
+ padding: "12px 24px",
62
+ textDecoration: "none",
63
+ textAlign: "center" as const,
64
+ marginTop: "8px",
65
+ };
66
+
67
+ const noteStyle: React.CSSProperties = {
68
+ color: "#71717a",
69
+ fontSize: "13px",
70
+ lineHeight: "20px",
71
+ marginTop: "20px",
72
+ };