@rovela-ai/sdk 0.22.0 → 0.24.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.
- package/dist/admin/api/email-sending.d.ts +24 -0
- package/dist/admin/api/email-sending.d.ts.map +1 -0
- package/dist/admin/api/email-sending.js +155 -0
- package/dist/admin/api/email-sending.js.map +1 -0
- package/dist/admin/api/index.d.ts +2 -0
- package/dist/admin/api/index.d.ts.map +1 -1
- package/dist/admin/api/index.js +4 -0
- package/dist/admin/api/index.js.map +1 -1
- package/dist/admin/api/integrations-brevo.d.ts +24 -0
- package/dist/admin/api/integrations-brevo.d.ts.map +1 -0
- package/dist/admin/api/integrations-brevo.js +286 -0
- package/dist/admin/api/integrations-brevo.js.map +1 -0
- package/dist/admin/api/integrations.d.ts +2 -0
- package/dist/admin/api/integrations.d.ts.map +1 -1
- package/dist/admin/api/integrations.js +1 -0
- package/dist/admin/api/integrations.js.map +1 -1
- package/dist/admin/api/settings.d.ts.map +1 -1
- package/dist/admin/api/settings.js +4 -1
- package/dist/admin/api/settings.js.map +1 -1
- package/dist/admin/components/EmailsManager.d.ts.map +1 -1
- package/dist/admin/components/EmailsManager.js +2 -1
- package/dist/admin/components/EmailsManager.js.map +1 -1
- package/dist/admin/components/IntegrationsManager.d.ts.map +1 -1
- package/dist/admin/components/IntegrationsManager.js +98 -2
- package/dist/admin/components/IntegrationsManager.js.map +1 -1
- package/dist/admin/components/OrderDetails.d.ts.map +1 -1
- package/dist/admin/components/OrderDetails.js +1 -1
- package/dist/admin/components/OrderDetails.js.map +1 -1
- package/dist/admin/components/ProductForm.d.ts.map +1 -1
- package/dist/admin/components/ProductForm.js +12 -1
- package/dist/admin/components/ProductForm.js.map +1 -1
- package/dist/admin/components/TaxSettings.js +3 -3
- package/dist/admin/components/TaxSettings.js.map +1 -1
- package/dist/admin/components/emails/EmailSendingPanel.d.ts +2 -0
- package/dist/admin/components/emails/EmailSendingPanel.d.ts.map +1 -0
- package/dist/admin/components/emails/EmailSendingPanel.js +131 -0
- package/dist/admin/components/emails/EmailSendingPanel.js.map +1 -0
- package/dist/admin/integrations/registry.d.ts +2 -0
- package/dist/admin/integrations/registry.d.ts.map +1 -1
- package/dist/admin/integrations/registry.js +41 -0
- package/dist/admin/integrations/registry.js.map +1 -1
- package/dist/admin/types.d.ts +10 -0
- package/dist/admin/types.d.ts.map +1 -1
- package/dist/core/db/queries.d.ts +82 -29
- package/dist/core/db/queries.d.ts.map +1 -1
- package/dist/core/db/queries.js +44 -0
- package/dist/core/db/queries.js.map +1 -1
- package/dist/core/db/schema.d.ts +176 -1
- package/dist/core/db/schema.d.ts.map +1 -1
- package/dist/core/db/schema.js +29 -0
- package/dist/core/db/schema.js.map +1 -1
- package/dist/emails/index.d.ts +2 -0
- package/dist/emails/index.d.ts.map +1 -1
- package/dist/emails/index.js +1 -0
- package/dist/emails/index.js.map +1 -1
- package/dist/emails/sender.d.ts.map +1 -1
- package/dist/emails/sender.js +30 -5
- package/dist/emails/sender.js.map +1 -1
- package/dist/emails/transport-smtp.d.ts +58 -0
- package/dist/emails/transport-smtp.d.ts.map +1 -0
- package/dist/emails/transport-smtp.js +97 -0
- package/dist/emails/transport-smtp.js.map +1 -0
- package/dist/emails/transport.d.ts +77 -0
- package/dist/emails/transport.d.ts.map +1 -0
- package/dist/emails/transport.js +154 -0
- package/dist/emails/transport.js.map +1 -0
- package/docs/changelog/epoch-10-email-transport.md +97 -0
- package/docs/changelog/epoch-11-order-extras.md +42 -0
- package/package.json +2 -1
- package/templates/store-template/app/api/admin/emails/sending/route.ts +9 -0
- package/templates/store-template/app/api/admin/integrations/brevo/backfill/route.ts +9 -0
- package/templates/store-template/app/api/admin/integrations/brevo/route.ts +9 -0
- package/templates/store-template/next.config.js +4 -1
- package/templates/store-template/package.json +2 -1
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
export interface SmtpConfig {
|
|
29
|
+
host: string;
|
|
30
|
+
port: number;
|
|
31
|
+
secure: boolean;
|
|
32
|
+
user: string | null;
|
|
33
|
+
password: string | null;
|
|
34
|
+
}
|
|
35
|
+
export interface SmtpSendInput {
|
|
36
|
+
fromAddress: string;
|
|
37
|
+
fromName: string;
|
|
38
|
+
to: string;
|
|
39
|
+
subject: string;
|
|
40
|
+
html: string;
|
|
41
|
+
replyTo?: string;
|
|
42
|
+
}
|
|
43
|
+
export interface SmtpSendResult {
|
|
44
|
+
success: boolean;
|
|
45
|
+
messageId?: string;
|
|
46
|
+
error?: string;
|
|
47
|
+
}
|
|
48
|
+
export declare function sendViaSmtp(input: SmtpSendInput, cfg: SmtpConfig): Promise<SmtpSendResult>;
|
|
49
|
+
/**
|
|
50
|
+
* Save-time SMTP validation — a real handshake (connect + EHLO + auth), no
|
|
51
|
+
* email sent. Used by the admin Sending panel so misconfiguration surfaces
|
|
52
|
+
* at save time instead of silently pushing every send onto the fallback.
|
|
53
|
+
*/
|
|
54
|
+
export declare function verifySmtp(cfg: SmtpConfig): Promise<{
|
|
55
|
+
ok: boolean;
|
|
56
|
+
error?: string;
|
|
57
|
+
}>;
|
|
58
|
+
//# sourceMappingURL=transport-smtp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport-smtp.d.ts","sourceRoot":"","sources":["../../src/emails/transport-smtp.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAWH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,OAAO,CAAA;IACf,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,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,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAA;IAChB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AA2CD,wBAAsB,WAAW,CAAC,KAAK,EAAE,aAAa,EAAE,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,cAAc,CAAC,CAgBhG;AAED;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAS1F"}
|
|
@@ -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.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# E11 Order custom fields — 0.24.0..0.24.0
|
|
2
|
+
|
|
3
|
+
## What landed
|
|
4
|
+
Orders gained a custom-fields slot: `orders.metafields` — the exact mirror of
|
|
5
|
+
`products.metafields` (`[{ key, label, value }]` jsonb). It exists so
|
|
6
|
+
order-level details from an imported platform have somewhere to live (the
|
|
7
|
+
PrestaShop importer maps extras like the recyclable-packaging flag, gift
|
|
8
|
+
message, payment-method label, and the merchant's ORIGINAL order reference
|
|
9
|
+
into it). Import-owned in v1: the admin renders it read-only, there is no
|
|
10
|
+
editing UI, and native Rovela checkout does not write it.
|
|
11
|
+
|
|
12
|
+
## SDK-internal (free via pin bump)
|
|
13
|
+
- `core/db/schema.ts` — `orders.metafields jsonb NOT NULL DEFAULT '[]'`,
|
|
14
|
+
self-healed via `ensurePaymentMethodsSchema` (the orders chokepoint that
|
|
15
|
+
guards every full-row order read), parent-branch ALTER run at ship time.
|
|
16
|
+
- `admin/components/OrderDetails.tsx` — an "Additional details" card in the
|
|
17
|
+
right column (below Payment): label → value rows, hidden entirely when the
|
|
18
|
+
order has no metafields.
|
|
19
|
+
- `admin/types.ts` — `AdminOrderDetail.metafields`.
|
|
20
|
+
|
|
21
|
+
## Template contract
|
|
22
|
+
No new mounts, no new routes, no new dependencies — the card renders from the
|
|
23
|
+
existing `/api/admin/orders/[id]` GET (a full-row select). A plain pin bump
|
|
24
|
+
is the whole update.
|
|
25
|
+
|
|
26
|
+
## Behavior contracts
|
|
27
|
+
- Orders without metafields render byte-identically to 0.23 — the card only
|
|
28
|
+
appears when the array is non-empty.
|
|
29
|
+
- The platform's export API (`/api/v1/orders`) serves `metafields` as an
|
|
30
|
+
optional newer-store column (null/empty on branches that predate it).
|
|
31
|
+
|
|
32
|
+
## Custom-code hazards (audit greps for CC)
|
|
33
|
+
- None specific to this epoch. Stores with custom order-detail pages built
|
|
34
|
+
from the same GET simply ignore the new field.
|
|
35
|
+
|
|
36
|
+
## MERCHANT-DATA HAZARD
|
|
37
|
+
None — purely additive; no existing data changes meaning.
|
|
38
|
+
|
|
39
|
+
## Verify
|
|
40
|
+
- Open any order in /admin — no "Additional details" card (empty metafields).
|
|
41
|
+
- After a PrestaShop import with order fields mapped, imported orders show
|
|
42
|
+
the card with the mapped values (e.g. "Recyclable packaging: Yes").
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rovela-ai/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.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
|
-
|
|
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.
|
|
12
|
+
"@rovela-ai/sdk": "0.24.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",
|