@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.
- package/NOTICE +6 -0
- package/README.md +102 -0
- package/dist/attachments/errors.d.ts +7 -0
- package/dist/attachments/errors.d.ts.map +1 -0
- package/dist/attachments/errors.js +11 -0
- package/dist/attachments/errors.js.map +1 -0
- package/dist/attachments/index.d.ts +8 -0
- package/dist/attachments/index.d.ts.map +1 -0
- package/dist/attachments/index.js +7 -0
- package/dist/attachments/index.js.map +1 -0
- package/dist/attachments/service.d.ts +45 -0
- package/dist/attachments/service.d.ts.map +1 -0
- package/dist/attachments/service.js +100 -0
- package/dist/attachments/service.js.map +1 -0
- package/dist/attachments/types.d.ts +6 -0
- package/dist/attachments/types.d.ts.map +1 -0
- package/dist/attachments/types.js +2 -0
- package/dist/attachments/types.js.map +1 -0
- package/dist/conversations/errors.d.ts +2 -8
- package/dist/conversations/errors.d.ts.map +1 -1
- package/dist/conversations/errors.js +2 -8
- package/dist/conversations/errors.js.map +1 -1
- package/dist/conversations/index.d.ts +4 -12
- package/dist/conversations/index.d.ts.map +1 -1
- package/dist/conversations/index.js +2 -11
- package/dist/conversations/index.js.map +1 -1
- package/dist/conversations/service.d.ts +49 -61
- package/dist/conversations/service.d.ts.map +1 -1
- package/dist/conversations/service.js +110 -83
- package/dist/conversations/service.js.map +1 -1
- package/dist/conversations/types.d.ts +1 -4
- package/dist/conversations/types.d.ts.map +1 -1
- package/dist/conversations/types.js +0 -3
- package/dist/conversations/types.js.map +1 -1
- package/dist/db/client.d.ts +21 -10
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +43 -18
- package/dist/db/client.js.map +1 -1
- package/dist/db/index.d.ts +3 -3
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +3 -7
- package/dist/db/index.js.map +1 -1
- package/dist/db/schema/agents.d.ts +14 -51
- package/dist/db/schema/agents.d.ts.map +1 -1
- package/dist/db/schema/agents.js +9 -21
- package/dist/db/schema/agents.js.map +1 -1
- package/dist/db/schema/ai.d.ts +86 -1556
- package/dist/db/schema/ai.d.ts.map +1 -1
- package/dist/db/schema/ai.js +32 -236
- package/dist/db/schema/ai.js.map +1 -1
- package/dist/db/schema/auth.d.ts +5 -34
- package/dist/db/schema/auth.d.ts.map +1 -1
- package/dist/db/schema/auth.js +25 -56
- package/dist/db/schema/auth.js.map +1 -1
- package/dist/db/schema/billing.d.ts +339 -105
- package/dist/db/schema/billing.d.ts.map +1 -1
- package/dist/db/schema/billing.js +99 -58
- package/dist/db/schema/billing.js.map +1 -1
- package/dist/db/schema/feature-flags.d.ts +3 -12
- package/dist/db/schema/feature-flags.d.ts.map +1 -1
- package/dist/db/schema/feature-flags.js +3 -12
- package/dist/db/schema/feature-flags.js.map +1 -1
- package/dist/db/schema/identity.d.ts +12 -274
- package/dist/db/schema/identity.d.ts.map +1 -1
- package/dist/db/schema/identity.js +19 -104
- package/dist/db/schema/identity.js.map +1 -1
- package/dist/db/schema/index.d.ts +8 -15
- package/dist/db/schema/index.d.ts.map +1 -1
- package/dist/db/schema/index.js +12 -42
- package/dist/db/schema/index.js.map +1 -1
- package/dist/db/schema/notifications.d.ts +3 -17
- package/dist/db/schema/notifications.d.ts.map +1 -1
- package/dist/db/schema/notifications.js +3 -17
- package/dist/db/schema/notifications.js.map +1 -1
- package/dist/db/schema/usage.d.ts +94 -77
- package/dist/db/schema/usage.d.ts.map +1 -1
- package/dist/db/schema/usage.js +59 -89
- package/dist/db/schema/usage.js.map +1 -1
- package/dist/db/validate.d.ts +2 -11
- package/dist/db/validate.d.ts.map +1 -1
- package/dist/db/validate.js +6 -17
- package/dist/db/validate.js.map +1 -1
- package/dist/db/workspace-queries.d.ts +5 -29
- package/dist/db/workspace-queries.d.ts.map +1 -1
- package/dist/db/workspace-queries.js +5 -29
- package/dist/db/workspace-queries.js.map +1 -1
- package/dist/documents/classifier.d.ts +5 -12
- package/dist/documents/classifier.d.ts.map +1 -1
- package/dist/documents/classifier.js +16 -16
- package/dist/documents/classifier.js.map +1 -1
- package/dist/documents/errors.d.ts +2 -10
- package/dist/documents/errors.d.ts.map +1 -1
- package/dist/documents/errors.js +2 -10
- package/dist/documents/errors.js.map +1 -1
- package/dist/documents/index.d.ts +4 -13
- package/dist/documents/index.d.ts.map +1 -1
- package/dist/documents/index.js +3 -12
- package/dist/documents/index.js.map +1 -1
- package/dist/documents/service.d.ts +19 -26
- package/dist/documents/service.d.ts.map +1 -1
- package/dist/documents/service.js +44 -38
- package/dist/documents/service.js.map +1 -1
- package/dist/documents/types.d.ts +6 -10
- package/dist/documents/types.d.ts.map +1 -1
- package/dist/documents/types.js +0 -3
- package/dist/documents/types.js.map +1 -1
- package/dist/email/brand.d.ts +17 -0
- package/dist/email/brand.d.ts.map +1 -0
- package/dist/email/brand.js +9 -0
- package/dist/email/brand.js.map +1 -0
- package/dist/email/index.d.ts +4 -4
- package/dist/email/index.js +4 -4
- package/dist/email/provider.d.ts +1 -7
- package/dist/email/provider.d.ts.map +1 -1
- package/dist/email/provider.js +10 -19
- package/dist/email/provider.js.map +1 -1
- package/dist/email/send.d.ts +12 -12
- package/dist/email/send.d.ts.map +1 -1
- package/dist/email/send.js +5 -19
- package/dist/email/send.js.map +1 -1
- package/dist/email/senders.d.ts +17 -30
- package/dist/email/senders.d.ts.map +1 -1
- package/dist/email/senders.js +33 -47
- package/dist/email/senders.js.map +1 -1
- package/dist/email/templates/base-layout.d.ts +1 -1
- package/dist/email/templates/base-layout.d.ts.map +1 -1
- package/dist/email/templates/base-layout.js +4 -6
- package/dist/email/templates/base-layout.js.map +1 -1
- package/dist/email/templates/index.d.ts +10 -10
- package/dist/email/templates/index.d.ts.map +1 -1
- package/dist/email/templates/index.js +10 -16
- package/dist/email/templates/index.js.map +1 -1
- package/dist/email/templates/password-reset.d.ts +2 -2
- package/dist/email/templates/password-reset.d.ts.map +1 -1
- package/dist/email/templates/password-reset.js +10 -22
- package/dist/email/templates/password-reset.js.map +1 -1
- package/dist/email/templates/payment-failed.d.ts +2 -2
- package/dist/email/templates/payment-failed.d.ts.map +1 -1
- package/dist/email/templates/payment-failed.js +11 -24
- package/dist/email/templates/payment-failed.js.map +1 -1
- package/dist/email/templates/quota-warning.d.ts +4 -4
- package/dist/email/templates/quota-warning.d.ts.map +1 -1
- package/dist/email/templates/quota-warning.js +19 -39
- package/dist/email/templates/quota-warning.js.map +1 -1
- package/dist/email/templates/subscription-confirmed.d.ts +2 -2
- package/dist/email/templates/subscription-confirmed.d.ts.map +1 -1
- package/dist/email/templates/subscription-confirmed.js +13 -28
- package/dist/email/templates/subscription-confirmed.js.map +1 -1
- package/dist/email/templates/trial-expiry.d.ts +2 -2
- package/dist/email/templates/trial-expiry.d.ts.map +1 -1
- package/dist/email/templates/trial-expiry.js +12 -36
- package/dist/email/templates/trial-expiry.js.map +1 -1
- package/dist/email/templates/trial-warning.d.ts +2 -2
- package/dist/email/templates/trial-warning.d.ts.map +1 -1
- package/dist/email/templates/trial-warning.js +15 -32
- package/dist/email/templates/trial-warning.js.map +1 -1
- package/dist/email/templates/verify-email.d.ts +2 -2
- package/dist/email/templates/verify-email.d.ts.map +1 -1
- package/dist/email/templates/verify-email.js +10 -22
- package/dist/email/templates/verify-email.js.map +1 -1
- package/dist/email/templates/welcome.d.ts +2 -2
- package/dist/email/templates/welcome.d.ts.map +1 -1
- package/dist/email/templates/welcome.js +13 -20
- package/dist/email/templates/welcome.js.map +1 -1
- package/dist/email/templates/workspace-invitation.d.ts +2 -2
- package/dist/email/templates/workspace-invitation.d.ts.map +1 -1
- package/dist/email/templates/workspace-invitation.js +12 -26
- package/dist/email/templates/workspace-invitation.js.map +1 -1
- package/dist/env.d.ts +5 -10
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +41 -31
- package/dist/env.js.map +1 -1
- package/dist/identity/audit.d.ts +11 -24
- package/dist/identity/audit.d.ts.map +1 -1
- package/dist/identity/audit.js +13 -27
- package/dist/identity/audit.js.map +1 -1
- package/dist/identity/errors.d.ts +4 -12
- package/dist/identity/errors.d.ts.map +1 -1
- package/dist/identity/errors.js +2 -8
- package/dist/identity/errors.js.map +1 -1
- package/dist/identity/index.d.ts +6 -13
- package/dist/identity/index.d.ts.map +1 -1
- package/dist/identity/index.js +5 -12
- package/dist/identity/index.js.map +1 -1
- package/dist/identity/service.d.ts +29 -47
- package/dist/identity/service.d.ts.map +1 -1
- package/dist/identity/service.js +107 -79
- package/dist/identity/service.js.map +1 -1
- package/dist/identity/types.d.ts +28 -4
- package/dist/identity/types.d.ts.map +1 -1
- package/dist/identity/types.js +0 -3
- package/dist/identity/types.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -10
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +7 -18
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +45 -49
- package/dist/logger.js.map +1 -1
- package/dist/money.d.ts +95 -0
- package/dist/money.d.ts.map +1 -0
- package/dist/money.js +208 -0
- package/dist/money.js.map +1 -0
- package/dist/notifications/index.d.ts +9 -43
- package/dist/notifications/index.d.ts.map +1 -1
- package/dist/notifications/index.js +9 -49
- package/dist/notifications/index.js.map +1 -1
- package/dist/notifications/triggers.d.ts +9 -32
- package/dist/notifications/triggers.d.ts.map +1 -1
- package/dist/notifications/triggers.js +21 -58
- package/dist/notifications/triggers.js.map +1 -1
- package/dist/notifications/types.d.ts +6 -8
- package/dist/notifications/types.d.ts.map +1 -1
- package/dist/notifications/types.js +0 -9
- package/dist/notifications/types.js.map +1 -1
- package/dist/prompt.d.ts +38 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +118 -0
- package/dist/prompt.js.map +1 -0
- package/dist/registry.d.ts +32 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +60 -0
- package/dist/registry.js.map +1 -0
- package/dist/request-context.d.ts +49 -0
- package/dist/request-context.d.ts.map +1 -0
- package/dist/request-context.js +100 -0
- package/dist/request-context.js.map +1 -0
- package/dist/storage/index.d.ts +65 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +109 -0
- package/dist/storage/index.js.map +1 -0
- package/package.json +74 -24
- package/src/attachments/errors.ts +18 -0
- package/src/attachments/index.ts +22 -0
- package/src/attachments/service.ts +178 -0
- package/src/attachments/types.ts +6 -0
- package/src/conversations/errors.ts +45 -0
- package/src/conversations/index.ts +39 -0
- package/src/conversations/service.ts +594 -0
- package/src/conversations/types.ts +15 -0
- package/src/db/client.ts +91 -0
- package/src/db/index.ts +3 -0
- package/src/db/migrations/0000_baseline.sql +652 -0
- package/src/db/migrations/0001_reconcile_chain_built_databases.sql +53 -0
- package/src/db/migrations/0002_webhook_claim_lease.sql +1 -0
- package/src/db/migrations/0003_local_payments.sql +18 -0
- package/src/db/migrations/README.md +103 -36
- package/src/db/migrations/legacy-chain.json +206 -0
- package/src/db/migrations/meta/0000_snapshot.json +4182 -157
- package/src/db/migrations/meta/0002_snapshot.json +2478 -368
- package/src/db/migrations/meta/0003_snapshot.json +2567 -554
- package/src/db/migrations/meta/_journal.json +8 -274
- package/src/db/schema/agents.ts +57 -0
- package/src/db/schema/ai.ts +174 -0
- package/src/db/schema/auth.ts +183 -0
- package/src/db/schema/billing.ts +245 -0
- package/src/db/schema/feature-flags.ts +20 -0
- package/src/db/schema/identity.ts +250 -0
- package/src/db/schema/index.ts +12 -0
- package/src/db/schema/notifications.ts +31 -0
- package/src/db/schema/usage.ts +269 -0
- package/src/db/validate.ts +63 -0
- package/src/db/workspace-queries.ts +64 -0
- package/src/documents/classifier.ts +61 -0
- package/src/documents/errors.ts +44 -0
- package/src/documents/index.ts +24 -0
- package/src/documents/service.ts +256 -0
- package/src/documents/types.ts +24 -0
- package/src/email/brand.ts +24 -0
- package/src/email/index.ts +19 -0
- package/src/email/provider.ts +278 -0
- package/src/email/send.ts +169 -0
- package/src/email/senders.ts +292 -0
- package/src/email/templates/base-layout.tsx +145 -0
- package/src/email/templates/index.ts +28 -0
- package/src/email/templates/password-reset.tsx +72 -0
- package/src/email/templates/payment-failed.tsx +77 -0
- package/src/email/templates/quota-warning.tsx +118 -0
- package/src/email/templates/subscription-confirmed.tsx +91 -0
- package/src/email/templates/trial-expiry.tsx +124 -0
- package/src/email/templates/trial-warning.tsx +104 -0
- package/src/email/templates/verify-email.tsx +72 -0
- package/src/email/templates/welcome.tsx +64 -0
- package/src/email/templates/workspace-invitation.tsx +105 -0
- package/src/env.ts +180 -0
- package/src/identity/audit.ts +65 -0
- package/src/identity/errors.ts +44 -0
- package/src/identity/index.ts +29 -0
- package/src/identity/service.ts +303 -0
- package/src/identity/types.ts +58 -0
- package/src/index.ts +4 -0
- package/src/logger.ts +198 -0
- package/src/money.ts +270 -0
- package/src/notifications/index.ts +112 -0
- package/src/notifications/triggers.ts +158 -0
- package/src/notifications/types.ts +26 -0
- package/src/prompt.ts +142 -0
- package/src/registry.ts +75 -0
- package/src/request-context.ts +137 -0
- package/src/storage/index.ts +153 -0
- package/dist/db/schema/rag.d.ts +0 -171
- package/dist/db/schema/rag.d.ts.map +0 -1
- package/dist/db/schema/rag.js +0 -42
- package/dist/db/schema/rag.js.map +0 -1
- package/src/db/migrations/0000_sharp_night_nurse.sql +0 -52
- package/src/db/migrations/0001_hesitant_vapor.sql +0 -40
- package/src/db/migrations/0002_numerous_lady_vermin.sql +0 -235
- package/src/db/migrations/0003_mighty_jean_grey.sql +0 -37
- package/src/db/migrations/0004_cynical_grandmaster.sql +0 -2
- package/src/db/migrations/0005_careful_switch.sql +0 -8
- package/src/db/migrations/0006_tense_blue_marvel.sql +0 -16
- package/src/db/migrations/0007_next_squadron_sinister.sql +0 -1
- package/src/db/migrations/0008_powerful_scream.sql +0 -39
- package/src/db/migrations/0009_sad_ink.sql +0 -1
- package/src/db/migrations/0010_shocking_omega_flight.sql +0 -2
- package/src/db/migrations/0011_ambiguous_otto_octavius.sql +0 -151
- package/src/db/migrations/0011_hnsw_vector_index.sql +0 -17
- package/src/db/migrations/0012_user_profiles.sql +0 -30
- package/src/db/migrations/0013_trending_seasonal_columns.sql +0 -9
- package/src/db/migrations/0014_add_product_type_to_agents.sql +0 -6
- package/src/db/migrations/0015_new_v1_tables.sql +0 -149
- package/src/db/migrations/0016_document_types.sql +0 -30
- package/src/db/migrations/0017_document_metadata.sql +0 -2
- package/src/db/migrations/0018_identity_graph.sql +0 -200
- package/src/db/migrations/0019_db_driven_prompts.sql +0 -20
- package/src/db/migrations/0020_pending_extractions.sql +0 -35
- package/src/db/migrations/0021_rag_documents.sql +0 -39
- package/src/db/migrations/0022_user_quotas_usage_jsonb.sql +0 -70
- package/src/db/migrations/0023_user_facts_user_scoped_unique.sql +0 -17
- package/src/db/migrations/0024_user_memories_hnsw.sql +0 -23
- package/src/db/migrations/0025_audit_trigger_errcode.sql +0 -21
- package/src/db/migrations/0026_user_profile_snapshots_composite_pk.sql +0 -17
- package/src/db/migrations/0027_billing_settings_mnt_columns.sql +0 -54
- package/src/db/migrations/0028_trial_credits_normalized_email.sql +0 -37
- package/src/db/migrations/0029_usage_records_conversation_idx.sql +0 -13
- package/src/db/migrations/0030_pending_extractions_composite_idx.sql +0 -11
- package/src/db/migrations/0031_rag_documents_hnsw.sql +0 -16
- package/src/db/migrations/0032_rate_limit_unique_idx.sql +0 -16
- package/src/db/migrations/0033_pending_extractions_partial_idx.sql +0 -18
- package/src/db/migrations/0034_rate_limit_count.sql +0 -9
- package/src/db/migrations/0035_credit_reservations.sql +0 -24
- package/src/db/migrations/0036_executions_audit_jobs.sql +0 -96
- package/src/db/migrations/0037_rate_limit_minute_bucket.sql +0 -46
- package/src/db/migrations/0038_user_quotas_generic_usage.sql +0 -25
- package/src/db/migrations/0039_platform_admin_impersonation.sql +0 -27
- package/src/db/migrations/0040_ee_audit_chain.sql +0 -23
- package/src/db/migrations/meta/0001_snapshot.json +0 -631
- package/src/db/migrations/meta/0004_snapshot.json +0 -2500
- package/src/db/migrations/meta/0005_snapshot.json +0 -2548
- package/src/db/migrations/meta/0006_snapshot.json +0 -2650
- package/src/db/migrations/meta/0007_snapshot.json +0 -2657
- package/src/db/migrations/meta/0008_snapshot.json +0 -2719
- package/src/db/migrations/meta/0009_snapshot.json +0 -2726
- package/src/db/migrations/meta/0010_snapshot.json +0 -2739
- 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
|
+
}
|