@rovela-ai/sdk 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/admin/api/email-sending.d.ts +24 -0
  2. package/dist/admin/api/email-sending.d.ts.map +1 -0
  3. package/dist/admin/api/email-sending.js +155 -0
  4. package/dist/admin/api/email-sending.js.map +1 -0
  5. package/dist/admin/api/index.d.ts +2 -0
  6. package/dist/admin/api/index.d.ts.map +1 -1
  7. package/dist/admin/api/index.js +4 -0
  8. package/dist/admin/api/index.js.map +1 -1
  9. package/dist/admin/api/integrations-brevo.d.ts +24 -0
  10. package/dist/admin/api/integrations-brevo.d.ts.map +1 -0
  11. package/dist/admin/api/integrations-brevo.js +286 -0
  12. package/dist/admin/api/integrations-brevo.js.map +1 -0
  13. package/dist/admin/api/integrations.d.ts +2 -0
  14. package/dist/admin/api/integrations.d.ts.map +1 -1
  15. package/dist/admin/api/integrations.js +1 -0
  16. package/dist/admin/api/integrations.js.map +1 -1
  17. package/dist/admin/api/settings.d.ts.map +1 -1
  18. package/dist/admin/api/settings.js +4 -1
  19. package/dist/admin/api/settings.js.map +1 -1
  20. package/dist/admin/components/EmailsManager.d.ts.map +1 -1
  21. package/dist/admin/components/EmailsManager.js +2 -1
  22. package/dist/admin/components/EmailsManager.js.map +1 -1
  23. package/dist/admin/components/IntegrationsManager.d.ts.map +1 -1
  24. package/dist/admin/components/IntegrationsManager.js +98 -2
  25. package/dist/admin/components/IntegrationsManager.js.map +1 -1
  26. package/dist/admin/components/ProductForm.d.ts.map +1 -1
  27. package/dist/admin/components/ProductForm.js +12 -1
  28. package/dist/admin/components/ProductForm.js.map +1 -1
  29. package/dist/admin/components/TaxSettings.js +3 -3
  30. package/dist/admin/components/TaxSettings.js.map +1 -1
  31. package/dist/admin/components/emails/EmailSendingPanel.d.ts +2 -0
  32. package/dist/admin/components/emails/EmailSendingPanel.d.ts.map +1 -0
  33. package/dist/admin/components/emails/EmailSendingPanel.js +131 -0
  34. package/dist/admin/components/emails/EmailSendingPanel.js.map +1 -0
  35. package/dist/admin/integrations/registry.d.ts +2 -0
  36. package/dist/admin/integrations/registry.d.ts.map +1 -1
  37. package/dist/admin/integrations/registry.js +41 -0
  38. package/dist/admin/integrations/registry.js.map +1 -1
  39. package/dist/core/db/queries.d.ts +8 -0
  40. package/dist/core/db/queries.d.ts.map +1 -1
  41. package/dist/core/db/queries.js +41 -0
  42. package/dist/core/db/queries.js.map +1 -1
  43. package/dist/core/db/schema.d.ts +148 -0
  44. package/dist/core/db/schema.d.ts.map +1 -1
  45. package/dist/core/db/schema.js +16 -0
  46. package/dist/core/db/schema.js.map +1 -1
  47. package/dist/emails/index.d.ts +2 -0
  48. package/dist/emails/index.d.ts.map +1 -1
  49. package/dist/emails/index.js +1 -0
  50. package/dist/emails/index.js.map +1 -1
  51. package/dist/emails/sender.d.ts.map +1 -1
  52. package/dist/emails/sender.js +30 -5
  53. package/dist/emails/sender.js.map +1 -1
  54. package/dist/emails/transport-smtp.d.ts +58 -0
  55. package/dist/emails/transport-smtp.d.ts.map +1 -0
  56. package/dist/emails/transport-smtp.js +97 -0
  57. package/dist/emails/transport-smtp.js.map +1 -0
  58. package/dist/emails/transport.d.ts +77 -0
  59. package/dist/emails/transport.d.ts.map +1 -0
  60. package/dist/emails/transport.js +154 -0
  61. package/dist/emails/transport.js.map +1 -0
  62. package/docs/changelog/epoch-10-email-transport.md +97 -0
  63. package/package.json +2 -1
  64. package/templates/store-template/app/api/admin/emails/sending/route.ts +9 -0
  65. package/templates/store-template/app/api/admin/integrations/brevo/backfill/route.ts +9 -0
  66. package/templates/store-template/app/api/admin/integrations/brevo/route.ts +9 -0
  67. package/templates/store-template/next.config.js +4 -1
  68. package/templates/store-template/package.json +2 -1
@@ -0,0 +1,97 @@
1
+ /**
2
+ * @rovela/sdk/emails/transport-smtp
3
+ *
4
+ * The nodemailer-touching half of the email transport (SDK 0.23.0),
5
+ * isolated in its own module. nodemailer pulls node builtins (net/tls/dns),
6
+ * and the emails graph is CLIENT-REACHABLE (the §62 invariant: auth barrel →
7
+ * send → sender → transport) — so it is loaded through a BUNDLER-IGNORED
8
+ * dynamic import:
9
+ *
10
+ * import(\/* webpackIgnore *\/ \/* turbopackIgnore *\/ 'nodemailer')
11
+ *
12
+ * Both bundlers skip the specifier entirely (no client-side resolution, no
13
+ * chunk), and at server runtime it is a NATIVE import with real Node
14
+ * resolution from the executing module's location — no path guessing, no
15
+ * eval tricks. Two consequences, both handled at the packaging layer:
16
+ *
17
+ * - nodemailer is a TEMPLATE dependency (store package.json), so it sits
18
+ * hoisted at the app root deterministically (an SDK-transitive-only
19
+ * install can nest it where a bundled chunk can't resolve it).
20
+ * - Vercel's file tracer can't see an ignored import — the template's
21
+ * next.config pins outputFileTracingIncludes: node_modules/nodemailer/**
22
+ * (the E4 blueprint.json precedent).
23
+ *
24
+ * A load failure logs LOUDLY (fail-safe rule: the send falls back to the
25
+ * platform path, but ops must see WHY smtp was unavailable) and the cached
26
+ * promise resets so a transient failure can retry.
27
+ */
28
+ // =============================================================================
29
+ // Loader
30
+ // =============================================================================
31
+ let nodemailerPromise = null;
32
+ function loadNodemailer() {
33
+ if (!nodemailerPromise) {
34
+ nodemailerPromise = import(
35
+ /* webpackIgnore: true */ /* turbopackIgnore: true */ 'nodemailer')
36
+ .then((m) => (m.default ??
37
+ m))
38
+ .catch((err) => {
39
+ console.error('[Email] nodemailer not loadable:', err);
40
+ nodemailerPromise = null; // allow retry on transient failures
41
+ return null;
42
+ });
43
+ }
44
+ return nodemailerPromise;
45
+ }
46
+ function buildTransport(nm, cfg) {
47
+ return nm.createTransport({
48
+ host: cfg.host,
49
+ port: cfg.port,
50
+ secure: cfg.secure,
51
+ ...(cfg.user ? { auth: { user: cfg.user, pass: cfg.password ?? '' } } : {}),
52
+ connectionTimeout: 10000,
53
+ greetingTimeout: 10000,
54
+ socketTimeout: 15000,
55
+ });
56
+ }
57
+ // =============================================================================
58
+ // Send + verify
59
+ // =============================================================================
60
+ export async function sendViaSmtp(input, cfg) {
61
+ const nm = await loadNodemailer();
62
+ if (!nm)
63
+ return { success: false, error: 'SMTP transport unavailable (nodemailer not loadable)' };
64
+ if (!input.fromAddress)
65
+ return { success: false, error: 'No sender address configured' };
66
+ try {
67
+ const info = await buildTransport(nm, cfg).sendMail({
68
+ from: `${input.fromName} <${input.fromAddress}>`,
69
+ to: input.to,
70
+ subject: input.subject,
71
+ html: input.html,
72
+ ...(input.replyTo ? { replyTo: input.replyTo } : {}),
73
+ });
74
+ return { success: true, messageId: info?.messageId };
75
+ }
76
+ catch (err) {
77
+ return { success: false, error: err instanceof Error ? err.message : 'SMTP send failed' };
78
+ }
79
+ }
80
+ /**
81
+ * Save-time SMTP validation — a real handshake (connect + EHLO + auth), no
82
+ * email sent. Used by the admin Sending panel so misconfiguration surfaces
83
+ * at save time instead of silently pushing every send onto the fallback.
84
+ */
85
+ export async function verifySmtp(cfg) {
86
+ const nm = await loadNodemailer();
87
+ if (!nm)
88
+ return { ok: false, error: 'SMTP transport unavailable (nodemailer not loadable)' };
89
+ try {
90
+ await buildTransport(nm, cfg).verify();
91
+ return { ok: true };
92
+ }
93
+ catch (err) {
94
+ return { ok: false, error: err instanceof Error ? err.message : 'SMTP verification failed' };
95
+ }
96
+ }
97
+ //# sourceMappingURL=transport-smtp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport-smtp.js","sourceRoot":"","sources":["../../src/emails/transport-smtp.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * @rovela/sdk/emails/transport
3
+ *
4
+ * Transactional-email transport resolution (SDK 0.23.0). Every send funnels
5
+ * through sendEmail() (sender.ts), which asks this module HOW to deliver:
6
+ *
7
+ * - 'platform' (default, also NULL) — the platform Resend sender. The
8
+ * entire pre-0.23 fleet stays byte-identical on this path.
9
+ * - 'brevo' — Brevo's transactional HTTP API (POST /v3/smtp/email)
10
+ * using the SAME store_settings.brevo_api_key the marketing integration
11
+ * stores. No SMTP key needed — this is what makes the integrations-page
12
+ * toggle seamless. Sender address must be a VERIFIED Brevo sender or
13
+ * Brevo rejects the send (surfaced at save time by the admin UI).
14
+ * - 'smtp' — the merchant's own SMTP server via nodemailer.
15
+ *
16
+ * FAIL-SAFE DELIVERY RULE (load-bearing): a non-platform transport failure
17
+ * falls back to the platform Resend path and logs loudly — a merchant's
18
+ * SMTP typo must never eat an order confirmation. Misconfiguration is
19
+ * caught at SAVE time by the admin Sending panel, not at send time.
20
+ *
21
+ * CLIENT-REACHABILITY (load-bearing, the §62 emails-graph invariant): this
22
+ * file is reachable from client bundles (auth barrel → send → sender →
23
+ * transport). All nodemailer-touching code lives in ./transport-smtp, which
24
+ * loads it via a BUNDLER-IGNORED dynamic import (webpackIgnore +
25
+ * turbopackIgnore) — native server-runtime resolution, nothing for the
26
+ * client bundler to resolve. Fails open to the platform path when the
27
+ * module can't load. See transport-smtp.ts for the packaging contract
28
+ * (template dependency + tracing include).
29
+ */
30
+ import { type StoreSettingsData } from '../core/db/queries';
31
+ import { type SmtpConfig as SmtpConfigType } from './transport-smtp';
32
+ export type EmailTransportKind = 'platform' | 'brevo' | 'smtp';
33
+ export interface TransportSendInput {
34
+ from: string;
35
+ fromAddress: string;
36
+ fromName: string;
37
+ to: string;
38
+ subject: string;
39
+ html: string;
40
+ replyTo?: string;
41
+ }
42
+ export interface TransportSendResult {
43
+ success: boolean;
44
+ messageId?: string;
45
+ error?: string;
46
+ }
47
+ /** The store's configured transport ('platform' when unset/unknown). */
48
+ export declare function resolveTransportKind(settings: Pick<StoreSettingsData, 'emailTransport'> | null): EmailTransportKind;
49
+ /**
50
+ * Sender identity for non-platform transports. The platform path keeps its
51
+ * existing RESEND_FROM_EMAIL resolution untouched; brevo/smtp REQUIRE a
52
+ * merchant-owned from-address (their domain, their reputation) and fall
53
+ * back to the store contact email when the dedicated field is empty.
54
+ */
55
+ export declare function resolveFromIdentity(settings: Pick<StoreSettingsData, 'emailFromAddress' | 'emailFromName' | 'storeEmail' | 'storeName'> | null): {
56
+ address: string | null;
57
+ name: string;
58
+ };
59
+ /** Server-only read of the marketing integration's key (card-added columns). */
60
+ export declare function getBrevoApiKey(): Promise<string | null>;
61
+ export { verifySmtp } from './transport-smtp';
62
+ export type { SmtpConfig } from './transport-smtp';
63
+ export declare function smtpConfigFromSettings(settings: Pick<StoreSettingsData, 'smtpHost' | 'smtpPort' | 'smtpSecure' | 'smtpUser' | 'smtpPassword'> | null): SmtpConfigType | null;
64
+ export interface TransportDispatch {
65
+ kind: EmailTransportKind;
66
+ /** null → caller proceeds with the platform Resend path. */
67
+ send: ((input: TransportSendInput) => Promise<TransportSendResult>) | null;
68
+ /** Merchant sender identity for non-platform transports. */
69
+ fromAddress: string | null;
70
+ fromName: string;
71
+ }
72
+ /**
73
+ * Resolve the store's transport once per send. Any resolution failure
74
+ * degrades to the platform path (fail-safe delivery rule).
75
+ */
76
+ export declare function resolveTransport(): Promise<TransportDispatch>;
77
+ //# sourceMappingURL=transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/emails/transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EAAgB,KAAK,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAEzE,OAAO,EAAe,KAAK,UAAU,IAAI,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAMjF,MAAM,MAAM,kBAAkB,GAAG,UAAU,GAAG,OAAO,GAAG,MAAM,CAAA;AAE9D,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,EAAE,EAAE,MAAM,CAAA;IACV,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,OAAO,CAAA;IAChB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAMD,wEAAwE;AACxE,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,IAAI,CAAC,iBAAiB,EAAE,gBAAgB,CAAC,GAAG,IAAI,GACzD,kBAAkB,CAGpB;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,IAAI,CAAC,iBAAiB,EAAE,kBAAkB,GAAG,eAAe,GAAG,YAAY,GAAG,WAAW,CAAC,GAAG,IAAI,GAC1G;IAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAI1C;AAMD,gFAAgF;AAChF,wBAAsB,cAAc,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAY7D;AAkCD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAC7C,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAElD,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,IAAI,CAAC,iBAAiB,EAAE,UAAU,GAAG,UAAU,GAAG,YAAY,GAAG,UAAU,GAAG,cAAc,CAAC,GAAG,IAAI,GAC7G,cAAc,GAAG,IAAI,CAYvB;AAMD,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,kBAAkB,CAAA;IACxB,4DAA4D;IAC5D,IAAI,EAAE,CAAC,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC,mBAAmB,CAAC,CAAC,GAAG,IAAI,CAAA;IAC1E,4DAA4D;IAC5D,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAoCnE"}
@@ -0,0 +1,154 @@
1
+ /**
2
+ * @rovela/sdk/emails/transport
3
+ *
4
+ * Transactional-email transport resolution (SDK 0.23.0). Every send funnels
5
+ * through sendEmail() (sender.ts), which asks this module HOW to deliver:
6
+ *
7
+ * - 'platform' (default, also NULL) — the platform Resend sender. The
8
+ * entire pre-0.23 fleet stays byte-identical on this path.
9
+ * - 'brevo' — Brevo's transactional HTTP API (POST /v3/smtp/email)
10
+ * using the SAME store_settings.brevo_api_key the marketing integration
11
+ * stores. No SMTP key needed — this is what makes the integrations-page
12
+ * toggle seamless. Sender address must be a VERIFIED Brevo sender or
13
+ * Brevo rejects the send (surfaced at save time by the admin UI).
14
+ * - 'smtp' — the merchant's own SMTP server via nodemailer.
15
+ *
16
+ * FAIL-SAFE DELIVERY RULE (load-bearing): a non-platform transport failure
17
+ * falls back to the platform Resend path and logs loudly — a merchant's
18
+ * SMTP typo must never eat an order confirmation. Misconfiguration is
19
+ * caught at SAVE time by the admin Sending panel, not at send time.
20
+ *
21
+ * CLIENT-REACHABILITY (load-bearing, the §62 emails-graph invariant): this
22
+ * file is reachable from client bundles (auth barrel → send → sender →
23
+ * transport). All nodemailer-touching code lives in ./transport-smtp, which
24
+ * loads it via a BUNDLER-IGNORED dynamic import (webpackIgnore +
25
+ * turbopackIgnore) — native server-runtime resolution, nothing for the
26
+ * client bundler to resolve. Fails open to the platform path when the
27
+ * module can't load. See transport-smtp.ts for the packaging contract
28
+ * (template dependency + tracing include).
29
+ */
30
+ import { findSettings } from '../core/db/queries';
31
+ import { getSql } from '../core/db/client';
32
+ import { sendViaSmtp } from './transport-smtp';
33
+ // =============================================================================
34
+ // Resolution
35
+ // =============================================================================
36
+ /** The store's configured transport ('platform' when unset/unknown). */
37
+ export function resolveTransportKind(settings) {
38
+ const t = settings?.emailTransport;
39
+ return t === 'brevo' || t === 'smtp' ? t : 'platform';
40
+ }
41
+ /**
42
+ * Sender identity for non-platform transports. The platform path keeps its
43
+ * existing RESEND_FROM_EMAIL resolution untouched; brevo/smtp REQUIRE a
44
+ * merchant-owned from-address (their domain, their reputation) and fall
45
+ * back to the store contact email when the dedicated field is empty.
46
+ */
47
+ export function resolveFromIdentity(settings) {
48
+ const address = settings?.emailFromAddress?.trim() || settings?.storeEmail?.trim() || null;
49
+ const name = settings?.emailFromName?.trim() || settings?.storeName?.trim() || 'Our Store';
50
+ return { address, name };
51
+ }
52
+ // =============================================================================
53
+ // Brevo transport (HTTP API — no SMTP key needed)
54
+ // =============================================================================
55
+ /** Server-only read of the marketing integration's key (card-added columns). */
56
+ export async function getBrevoApiKey() {
57
+ try {
58
+ const sql = getSql();
59
+ const rows = (await sql `
60
+ SELECT brevo_api_key FROM store_settings LIMIT 1
61
+ `);
62
+ return rows[0]?.brevo_api_key || null;
63
+ }
64
+ catch {
65
+ // Column absent (integration never installed) or transient DB error —
66
+ // the caller falls back to the platform path.
67
+ return null;
68
+ }
69
+ }
70
+ async function sendViaBrevo(input) {
71
+ const apiKey = await getBrevoApiKey();
72
+ if (!apiKey)
73
+ return { success: false, error: 'Brevo API key is not configured' };
74
+ if (!input.fromAddress)
75
+ return { success: false, error: 'No sender address configured' };
76
+ try {
77
+ const res = await fetch('https://api.brevo.com/v3/smtp/email', {
78
+ method: 'POST',
79
+ headers: { 'api-key': apiKey, 'content-type': 'application/json', accept: 'application/json' },
80
+ body: JSON.stringify({
81
+ sender: { email: input.fromAddress, name: input.fromName },
82
+ to: [{ email: input.to }],
83
+ subject: input.subject,
84
+ htmlContent: input.html,
85
+ ...(input.replyTo ? { replyTo: { email: input.replyTo } } : {}),
86
+ }),
87
+ signal: AbortSignal.timeout(15000),
88
+ });
89
+ const body = (await res.json().catch(() => null));
90
+ if (!res.ok) {
91
+ return { success: false, error: body?.message || `Brevo HTTP ${res.status}` };
92
+ }
93
+ return { success: true, messageId: body?.messageId };
94
+ }
95
+ catch (err) {
96
+ return { success: false, error: err instanceof Error ? err.message : 'Brevo send failed' };
97
+ }
98
+ }
99
+ // =============================================================================
100
+ // SMTP transport — nodemailer isolated in ./transport-smtp
101
+ // =============================================================================
102
+ export { verifySmtp } from './transport-smtp';
103
+ export function smtpConfigFromSettings(settings) {
104
+ const host = settings?.smtpHost?.trim();
105
+ if (!host)
106
+ return null;
107
+ const port = settings?.smtpPort ?? 587;
108
+ return {
109
+ host,
110
+ port,
111
+ // Convention: explicit flag wins; otherwise 465 implies implicit TLS.
112
+ secure: settings?.smtpSecure ?? port === 465,
113
+ user: settings?.smtpUser?.trim() || null,
114
+ password: settings?.smtpPassword || null,
115
+ };
116
+ }
117
+ /**
118
+ * Resolve the store's transport once per send. Any resolution failure
119
+ * degrades to the platform path (fail-safe delivery rule).
120
+ */
121
+ export async function resolveTransport() {
122
+ let settings = null;
123
+ try {
124
+ settings = await findSettings();
125
+ }
126
+ catch {
127
+ settings = null;
128
+ }
129
+ const kind = resolveTransportKind(settings);
130
+ const identity = resolveFromIdentity(settings);
131
+ if (kind === 'brevo') {
132
+ return { kind, send: sendViaBrevo, fromAddress: identity.address, fromName: identity.name };
133
+ }
134
+ if (kind === 'smtp') {
135
+ const cfg = smtpConfigFromSettings(settings);
136
+ if (cfg) {
137
+ return {
138
+ kind,
139
+ send: (input) => sendViaSmtp({
140
+ fromAddress: input.fromAddress,
141
+ fromName: input.fromName,
142
+ to: input.to,
143
+ subject: input.subject,
144
+ html: input.html,
145
+ replyTo: input.replyTo,
146
+ }, cfg),
147
+ fromAddress: identity.address,
148
+ fromName: identity.name,
149
+ };
150
+ }
151
+ }
152
+ return { kind: 'platform', send: null, fromAddress: identity.address, fromName: identity.name };
153
+ }
154
+ //# sourceMappingURL=transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../../src/emails/transport.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,97 @@
1
+ # E10 Email sending & Brevo — 0.23.0..0.23.0
2
+
3
+ ## What landed
4
+ Transactional-email delivery became merchant-configurable. Every store email
5
+ (order confirmations, account emails, custom emails) already funnels through
6
+ the SDK's one `sendEmail()` chokepoint; it now resolves a per-store
7
+ TRANSPORT: `'platform'` (default — the pre-0.23 Resend path, byte-identical
8
+ for the whole fleet), `'brevo'` (the store's connected Brevo account over
9
+ its HTTP API — same API key as the marketing integration, no SMTP key
10
+ needed), or `'smtp'` (the merchant's own server). The Brevo integration
11
+ also gained a live status echo and a one-click, resumable HISTORY IMPORT
12
+ that replaces per-store backfill scripts.
13
+
14
+ ## SDK-internal (free via pin bump)
15
+ - `emails/transport.ts` — transport resolution + the two senders. **FAIL-SAFE
16
+ DELIVERY RULE**: a non-platform transport failure falls back to the
17
+ platform Resend path and logs loudly; a merchant's SMTP typo can never eat
18
+ an order confirmation. Misconfiguration surfaces at SAVE time instead
19
+ (real SMTP `verify()` handshake; Brevo verified-sender check).
20
+ - New `store_settings` columns (drizzle + `ensureStoreSettingsSchema` +
21
+ parent-branch ALTER at ship time): `email_transport`,
22
+ `email_from_address`, `email_from_name`, `smtp_host`, `smtp_port`,
23
+ `smtp_secure`, `smtp_user`, `smtp_password` (SERVER-ONLY secret — stripped
24
+ from the admin settings GET like `colissimo_password`; write-only, blank
25
+ keeps the stored value).
26
+ - `nodemailer` (pinned to the 7.x line — next-auth 4.x declares
27
+ `peerOptional nodemailer@^7` and npm refuses to hoist a different major)
28
+ became BOTH an SDK dependency and a TEMPLATE dependency (root-hoisted,
29
+ deterministic location). It loads through a BUNDLER-IGNORED dynamic
30
+ import (`webpackIgnore` + `turbopackIgnore`, isolated in
31
+ `emails/transport-smtp.ts`) — the emails graph is client-reachable, so a
32
+ static import breaks `next build`; the ignored import is native
33
+ server-runtime resolution, no path guessing.
34
+ - `EmailSendingPanel` (Sending card on /admin/emails) + `EmailsManager`
35
+ mounts it; the "Send a test" button exercises the REAL saved transport.
36
+ - Integrations registry entries gained `details` bullets (what each
37
+ integration actually does, rendered in the configure modal — all 10
38
+ entries authored).
39
+ - Brevo configure modal extras: live "Connected as x@y · plan" echo,
40
+ verified-sender status, the "send transactional emails through Brevo"
41
+ toggle (writes the same `email_transport` column as the Sending panel —
42
+ the two surfaces can never disagree), and the history import UI
43
+ (progress bar, resumable, include-orders switch defaulted OFF when
44
+ Brevo's eCommerce module already holds orders — the double-count guard).
45
+
46
+ ## Template contract
47
+ - `app/api/admin/emails/sending/route.ts` (NEW) — re-export
48
+ `getEmailSending as GET, putEmailSending as PUT`. Without it the Sending
49
+ panel shows a load error and the Brevo toggle cannot save.
50
+ - `app/api/admin/integrations/brevo/route.ts` (NEW) — `getBrevoStatus as
51
+ GET`; `app/api/admin/integrations/brevo/backfill/route.ts` (NEW) —
52
+ `postBrevoBackfill as POST`. Without them the Brevo modal's extras show
53
+ an error state (credentials editing keeps working).
54
+ - **Component-dependency note (the E4 precedent)**: two per-store
55
+ requirements for the Custom-SMTP option — `package.json` gains
56
+ `"nodemailer": "7.0.13"` (MUST stay on 7.x: next-auth's peerOptional
57
+ range; a 8/9 pin makes npm nest it where the runtime can't resolve it)
58
+ and `next.config.js` gains
59
+ `outputFileTracingIncludes: ['./node_modules/nodemailer/**']` (an ignored
60
+ import is invisible to Vercel's tracer). A store updating the SDK WITHOUT
61
+ these loses only the Custom-SMTP option at runtime (platform + Brevo
62
+ transports are unaffected — Brevo is plain fetch), with a loud
63
+ `[Email] nodemailer not loadable` log.
64
+
65
+ ## Behavior contracts
66
+ - `sendEmail()` behavior is UNCHANGED while `email_transport` is NULL /
67
+ 'platform' — the entire existing fleet keeps its exact sending path.
68
+ - The Brevo history import is idempotent + resumable: contacts upsert by
69
+ email (`updateEnabled` — no duplicates, existing attributes/lists/consent
70
+ untouched, NO listIds ever sent), orders push with `historical: true`
71
+ (populates dashboards, NEVER fires an automation), chunked per request
72
+ (serverless-safe; the modal loops on the returned cursor).
73
+ - Per-store backfill scripts are RETIRED: the connect-brevo card + the
74
+ credentials contract now forbid writing one-off sync scripts into the
75
+ repo — history imports live in the admin.
76
+
77
+ ## Custom-code hazards (audit greps for CC)
78
+ - Grep `scripts/` for hand-written Brevo/marketing backfill or sync scripts
79
+ (e.g. EasyElec's `scripts/brevo-backfill.mjs`) — superseded by
80
+ Integrations → Brevo → "Import history"; delete them.
81
+ - Grep for direct `getResend()` / `resend.emails.send` calls in store code —
82
+ they bypass the transport layer; route them through `sendEmail()` from
83
+ `@rovela-ai/sdk/emails`.
84
+
85
+ ## MERCHANT-DATA HAZARD
86
+ None — no existing data changes meaning. The default transport is the
87
+ existing behavior; Brevo/SMTP are opt-in.
88
+
89
+ ## Verify
90
+ - /admin/emails shows the Sending card; saving Custom SMTP with a wrong
91
+ host is REJECTED at save time with the handshake error.
92
+ - With Brevo connected: the integration modal shows "Connected as …", the
93
+ transactional toggle flips `email_transport`, and "Send a test" delivers
94
+ through Brevo (check the Brevo → Transactional log).
95
+ - The history import runs to 100% with a progress bar, is resumable after
96
+ an interruption, and fires zero automations (historical flag).
97
+ - A store with `email_transport` unset sends exactly as before.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rovela-ai/sdk",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "Rovela SDK - Pre-built e-commerce components for AI-powered store generation",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -320,6 +320,7 @@
320
320
  "lucide-react": "0.555.0",
321
321
  "nanoid": "5.1.6",
322
322
  "next-auth": "4.24.13",
323
+ "nodemailer": "7.0.13",
323
324
  "react-colorful": "^5.6.1",
324
325
  "recharts": "3.7.0",
325
326
  "resend": "6.9.1",
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Email Sending Configuration Route
3
+ *
4
+ * GET /api/admin/emails/sending - Current transactional-email transport
5
+ * PUT /api/admin/emails/sending - Save it (SMTP handshake / Brevo verified-
6
+ * sender check run BEFORE persisting)
7
+ */
8
+
9
+ export { getEmailSending as GET, putEmailSending as PUT } from '@rovela-ai/sdk/admin/api'
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Brevo History Import Route
3
+ *
4
+ * POST /api/admin/integrations/brevo/backfill - One chunk of the resumable
5
+ * history import (contacts upsert, then orders with historical:true so no
6
+ * automation fires). The integrations modal loops on the returned cursor.
7
+ */
8
+
9
+ export { postBrevoBackfill as POST } from '@rovela-ai/sdk/admin/api'
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Brevo Integration Status Route
3
+ *
4
+ * GET /api/admin/integrations/brevo - Live Brevo status for the
5
+ * integrations modal (account identity, sender verification, whether the
6
+ * eCommerce module already holds orders, current email transport).
7
+ */
8
+
9
+ export { getBrevoStatus as GET } from '@rovela-ai/sdk/admin/api'
@@ -50,7 +50,10 @@ const nextConfig = {
50
50
  // the SDK's core/config get the file, and every other function (checkout,
51
51
  // webhooks, auth, admin emails) silently falls back to default styling.
52
52
  outputFileTracingIncludes: {
53
- '/**': ['./.rovela/blueprint.json'],
53
+ // nodemailer is loaded via a bundler-invisible require (the emails graph
54
+ // is client-reachable, a static import breaks the build) so the tracer
55
+ // can't see it — force-include it or the SMTP transport 500s on Vercel.
56
+ '/**': ['./.rovela/blueprint.json', './node_modules/nodemailer/**'],
54
57
  },
55
58
 
56
59
  // Image domains for Unsplash, CDNs, and R2 storage
@@ -9,13 +9,14 @@
9
9
  "lint": "eslint ."
10
10
  },
11
11
  "dependencies": {
12
- "@rovela-ai/sdk": "0.22.0",
12
+ "@rovela-ai/sdk": "0.23.0",
13
13
  "@tailwindcss/forms": "0.5.11",
14
14
  "@tailwindcss/typography": "0.5.19",
15
15
  "autoprefixer": "10.4.24",
16
16
  "lucide-react": "0.555.0",
17
17
  "next": "16.2.10",
18
18
  "next-auth": "4.24.13",
19
+ "nodemailer": "7.0.13",
19
20
  "postcss": "8.5.6",
20
21
  "react": "19.2.4",
21
22
  "react-dom": "19.2.4",