@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.
- 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 +136 -107
- package/dist/db/schema/billing.d.ts.map +1 -1
- package/dist/db/schema/billing.js +72 -60
- 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 +5 -13
- package/dist/identity/index.d.ts.map +1 -1
- package/dist/identity/index.js +4 -12
- package/dist/identity/index.js.map +1 -1
- package/dist/identity/service.d.ts +13 -47
- package/dist/identity/service.d.ts.map +1 -1
- package/dist/identity/service.js +31 -78
- package/dist/identity/service.js.map +1 -1
- package/dist/identity/types.d.ts +1 -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 +1 -10
- 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/README.md +74 -36
- package/src/db/migrations/legacy-chain.json +197 -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/_journal.json +6 -279
- 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 +209 -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 +26 -0
- package/src/identity/service.ts +202 -0
- package/src/identity/types.ts +28 -0
- package/src/index.ts +4 -0
- package/src/logger.ts +198 -0
- package/src/money.ts +270 -0
- package/src/notifications/index.ts +108 -0
- package/src/notifications/triggers.ts +158 -0
- package/src/notifications/types.ts +18 -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/0003_snapshot.json +0 -2488
- 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,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-only persistence for AI-generated documents.
|
|
3
|
+
*
|
|
4
|
+
* Callers pass a resolved actor (workspaceId, userId); core cannot depend on
|
|
5
|
+
* `@intelligo-dev/auth`. Every query filters by both ids — the caller is
|
|
6
|
+
* never trusted to have done it. Failures throw `DocumentServiceError`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { and, desc, eq, gt } from "drizzle-orm";
|
|
10
|
+
import { db } from "../db";
|
|
11
|
+
import { documents } from "../db/schema";
|
|
12
|
+
import { classifyDocumentTitle, isProductDocument } from "./classifier";
|
|
13
|
+
import { DocumentServiceError } from "./errors";
|
|
14
|
+
import type { DocumentActor, DocumentFilter, DocumentListItem } from "./types";
|
|
15
|
+
|
|
16
|
+
type DocumentRow = {
|
|
17
|
+
id: string;
|
|
18
|
+
title: string;
|
|
19
|
+
kind: string;
|
|
20
|
+
createdAt: Date;
|
|
21
|
+
content: string | null;
|
|
22
|
+
metadata?: unknown;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* `metadata` is untyped `jsonb` written by whoever saved the document,
|
|
27
|
+
* so it is read defensively: anything that is not a non-empty string
|
|
28
|
+
* is no link at all.
|
|
29
|
+
*/
|
|
30
|
+
function conversationIdOf(metadata: unknown): string | null {
|
|
31
|
+
if (!metadata || typeof metadata !== "object") return null;
|
|
32
|
+
const value = (metadata as { conversationId?: unknown }).conversationId;
|
|
33
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function toListItem(doc: DocumentRow): DocumentListItem {
|
|
37
|
+
return {
|
|
38
|
+
id: doc.id,
|
|
39
|
+
title: doc.title,
|
|
40
|
+
kind: doc.kind,
|
|
41
|
+
createdAt: doc.createdAt.toISOString(),
|
|
42
|
+
agentLabel: classifyDocumentTitle(doc.title).agentLabel,
|
|
43
|
+
content: doc.content,
|
|
44
|
+
conversationId: conversationIdOf(doc.metadata),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The latest version of each of the actor's documents, newest first.
|
|
50
|
+
*/
|
|
51
|
+
export async function getUserDocuments(
|
|
52
|
+
actor: DocumentActor,
|
|
53
|
+
filter?: DocumentFilter
|
|
54
|
+
): Promise<DocumentListItem[]> {
|
|
55
|
+
const uniqueDocs = await db
|
|
56
|
+
.selectDistinctOn([documents.id])
|
|
57
|
+
.from(documents)
|
|
58
|
+
.where(
|
|
59
|
+
and(
|
|
60
|
+
eq(documents.workspaceId, actor.workspaceId),
|
|
61
|
+
eq(documents.userId, actor.userId)
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
.orderBy(documents.id, desc(documents.createdAt));
|
|
65
|
+
|
|
66
|
+
let result: DocumentListItem[] = uniqueDocs.map(toListItem);
|
|
67
|
+
|
|
68
|
+
const filterType = filter?.type ?? "all";
|
|
69
|
+
if (filterType !== "all") {
|
|
70
|
+
result =
|
|
71
|
+
filterType === "report"
|
|
72
|
+
? result.filter((doc) => isProductDocument(doc.title))
|
|
73
|
+
: result.filter((doc) => doc.kind === filterType);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The latest version of one document, scoped to the actor.
|
|
81
|
+
* Throws DocumentServiceError("not_found") if no such document exists.
|
|
82
|
+
*/
|
|
83
|
+
export async function getDocument(
|
|
84
|
+
actor: DocumentActor,
|
|
85
|
+
id: string
|
|
86
|
+
): Promise<DocumentListItem> {
|
|
87
|
+
const [doc] = await db
|
|
88
|
+
.selectDistinctOn([documents.id])
|
|
89
|
+
.from(documents)
|
|
90
|
+
.where(
|
|
91
|
+
and(
|
|
92
|
+
eq(documents.id, id),
|
|
93
|
+
eq(documents.workspaceId, actor.workspaceId),
|
|
94
|
+
eq(documents.userId, actor.userId)
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
.orderBy(documents.id, desc(documents.createdAt));
|
|
98
|
+
|
|
99
|
+
if (!doc) {
|
|
100
|
+
throw new DocumentServiceError("not_found", "Document not found");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return toListItem(doc);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Every version of a document, newest first — what a canvas version
|
|
108
|
+
* picker walks. Throws ("not_found") when the actor has no such id.
|
|
109
|
+
*/
|
|
110
|
+
export async function getDocumentVersions(
|
|
111
|
+
actor: DocumentActor,
|
|
112
|
+
id: string
|
|
113
|
+
): Promise<DocumentListItem[]> {
|
|
114
|
+
const rows = await db
|
|
115
|
+
.select()
|
|
116
|
+
.from(documents)
|
|
117
|
+
.where(
|
|
118
|
+
and(
|
|
119
|
+
eq(documents.id, id),
|
|
120
|
+
eq(documents.workspaceId, actor.workspaceId),
|
|
121
|
+
eq(documents.userId, actor.userId)
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
.orderBy(desc(documents.createdAt));
|
|
125
|
+
|
|
126
|
+
if (rows.length === 0) {
|
|
127
|
+
throw new DocumentServiceError("not_found", "Document not found");
|
|
128
|
+
}
|
|
129
|
+
return rows.map(toListItem);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Saves a document as a new version.
|
|
134
|
+
* Throws DocumentServiceError("forbidden") if a document with this id
|
|
135
|
+
* already exists in the workspace under a different user.
|
|
136
|
+
*/
|
|
137
|
+
export async function saveDocument(
|
|
138
|
+
actor: DocumentActor,
|
|
139
|
+
params: {
|
|
140
|
+
id: string;
|
|
141
|
+
title: string;
|
|
142
|
+
content: string;
|
|
143
|
+
kind: string;
|
|
144
|
+
/**
|
|
145
|
+
* The conversation this was written in, stored on `metadata` so
|
|
146
|
+
* the artifacts page can link back to it. Optional: a document
|
|
147
|
+
* saved by a job or an import belongs to no conversation.
|
|
148
|
+
*/
|
|
149
|
+
conversationId?: string;
|
|
150
|
+
}
|
|
151
|
+
): Promise<DocumentListItem> {
|
|
152
|
+
const existingDocs = await db
|
|
153
|
+
.select()
|
|
154
|
+
.from(documents)
|
|
155
|
+
.where(
|
|
156
|
+
and(
|
|
157
|
+
eq(documents.id, params.id),
|
|
158
|
+
eq(documents.workspaceId, actor.workspaceId)
|
|
159
|
+
)
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
if (existingDocs.length > 0) {
|
|
163
|
+
const firstDoc = existingDocs[0];
|
|
164
|
+
if (firstDoc && firstDoc.userId !== actor.userId) {
|
|
165
|
+
throw new DocumentServiceError(
|
|
166
|
+
"forbidden",
|
|
167
|
+
"Document belongs to a different user"
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Every save writes a new version row, so an edit that does not resend
|
|
173
|
+
// the conversation link would drop it. Carry the known one forward.
|
|
174
|
+
const conversationId =
|
|
175
|
+
params.conversationId ??
|
|
176
|
+
existingDocs.map((doc) => conversationIdOf(doc.metadata)).find(Boolean) ??
|
|
177
|
+
null;
|
|
178
|
+
|
|
179
|
+
const [newDocument] = await db
|
|
180
|
+
.insert(documents)
|
|
181
|
+
.values({
|
|
182
|
+
id: params.id,
|
|
183
|
+
userId: actor.userId,
|
|
184
|
+
workspaceId: actor.workspaceId,
|
|
185
|
+
title: params.title,
|
|
186
|
+
content: params.content,
|
|
187
|
+
kind: params.kind,
|
|
188
|
+
createdAt: new Date(),
|
|
189
|
+
metadata: conversationId ? { conversationId } : null,
|
|
190
|
+
})
|
|
191
|
+
.returning();
|
|
192
|
+
|
|
193
|
+
if (!newDocument) {
|
|
194
|
+
throw new DocumentServiceError("database_error", "Failed to save document");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return toListItem(newDocument);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const MAX_DELETE_WINDOW_DAYS = 30;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Delete all versions of a document created after `timestamp`.
|
|
204
|
+
* Throws DocumentServiceError("invalid_input") for an unparsable
|
|
205
|
+
* timestamp or one older than the delete window, and ("not_found") if
|
|
206
|
+
* no document exists at that id within the actor's scope.
|
|
207
|
+
*/
|
|
208
|
+
export async function deleteDocumentVersions(
|
|
209
|
+
actor: DocumentActor,
|
|
210
|
+
id: string,
|
|
211
|
+
timestamp: string
|
|
212
|
+
): Promise<DocumentListItem[]> {
|
|
213
|
+
const parsedTimestamp = new Date(timestamp);
|
|
214
|
+
if (Number.isNaN(parsedTimestamp.getTime())) {
|
|
215
|
+
throw new DocumentServiceError("invalid_input", "Invalid timestamp");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const oldestAllowed = new Date(
|
|
219
|
+
Date.now() - MAX_DELETE_WINDOW_DAYS * 24 * 60 * 60 * 1000
|
|
220
|
+
);
|
|
221
|
+
if (parsedTimestamp < oldestAllowed) {
|
|
222
|
+
throw new DocumentServiceError(
|
|
223
|
+
"invalid_input",
|
|
224
|
+
`timestamp must be within the last ${MAX_DELETE_WINDOW_DAYS} days`
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const existingDocs = await db
|
|
229
|
+
.select()
|
|
230
|
+
.from(documents)
|
|
231
|
+
.where(
|
|
232
|
+
and(
|
|
233
|
+
eq(documents.id, id),
|
|
234
|
+
eq(documents.workspaceId, actor.workspaceId),
|
|
235
|
+
eq(documents.userId, actor.userId)
|
|
236
|
+
)
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
if (existingDocs.length === 0) {
|
|
240
|
+
throw new DocumentServiceError("not_found", "Document not found");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const deletedDocs = await db
|
|
244
|
+
.delete(documents)
|
|
245
|
+
.where(
|
|
246
|
+
and(
|
|
247
|
+
eq(documents.id, id),
|
|
248
|
+
eq(documents.workspaceId, actor.workspaceId),
|
|
249
|
+
eq(documents.userId, actor.userId),
|
|
250
|
+
gt(documents.createdAt, parsedTimestamp)
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
.returning();
|
|
254
|
+
|
|
255
|
+
return deletedDocs.map(toListItem);
|
|
256
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Resolved actor identity — every query is scoped to this pair. */
|
|
2
|
+
export type DocumentActor = {
|
|
3
|
+
workspaceId: string;
|
|
4
|
+
userId: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type DocumentFilter = {
|
|
8
|
+
type?: "all" | "report" | "text" | "code" | "sheet" | "image";
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type DocumentListItem = {
|
|
12
|
+
id: string;
|
|
13
|
+
title: string;
|
|
14
|
+
kind: string;
|
|
15
|
+
createdAt: string; // ISO string, so it serializes to the client
|
|
16
|
+
agentLabel: string;
|
|
17
|
+
content: string | null;
|
|
18
|
+
/**
|
|
19
|
+
* The conversation this document was written in, when known. Read from
|
|
20
|
+
* `metadata.conversationId` rather than a column: a document may also be
|
|
21
|
+
* written from a job or an import.
|
|
22
|
+
*/
|
|
23
|
+
conversationId: string | null;
|
|
24
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whose emails these are. Every deployment sends as itself, so the name
|
|
3
|
+
* and the support address come from its environment, never from the
|
|
4
|
+
* framework:
|
|
5
|
+
*
|
|
6
|
+
* - `APP_NAME` — the product's name. Falls back to the display name in
|
|
7
|
+
* `EMAIL_FROM` ("Acme <noreply@acme.com>" → "Acme").
|
|
8
|
+
* - `SUPPORT_EMAIL` — where a reader can write back. Without it the
|
|
9
|
+
* footer offers no address rather than someone else's.
|
|
10
|
+
*/
|
|
11
|
+
export type EmailBrand = {
|
|
12
|
+
/** Empty when neither variable names one. */
|
|
13
|
+
name: string;
|
|
14
|
+
supportEmail: string | null;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function emailBrand(): EmailBrand {
|
|
18
|
+
const from = process.env.EMAIL_FROM?.trim() ?? "";
|
|
19
|
+
const display = /^"?([^"<]+?)"?\s*</.exec(from)?.[1]?.trim() ?? "";
|
|
20
|
+
return {
|
|
21
|
+
name: process.env.APP_NAME?.trim() || display,
|
|
22
|
+
supportEmail: process.env.SUPPORT_EMAIL?.trim() || null,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export {
|
|
2
|
+
sendEmail,
|
|
3
|
+
type SendEmailParams,
|
|
4
|
+
type SendEmailWithComponentParams,
|
|
5
|
+
type SendEmailResult,
|
|
6
|
+
} from "./send";
|
|
7
|
+
export {
|
|
8
|
+
getEmailProvider,
|
|
9
|
+
resetEmailProviderCache,
|
|
10
|
+
type EmailProvider,
|
|
11
|
+
type EmailProviderName,
|
|
12
|
+
type EmailSendParams,
|
|
13
|
+
type EmailTemplateRef,
|
|
14
|
+
ResendProvider,
|
|
15
|
+
LoopsProvider,
|
|
16
|
+
ConsoleProvider,
|
|
17
|
+
} from "./provider";
|
|
18
|
+
export * from "./templates";
|
|
19
|
+
export * from "./senders";
|
|
@@ -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
|
+
}
|