Integra Products e adapters S3/SMTP; corrige desvios pós-merge dos agentes

- Wire ProductsModule em app.module.ts; troca o import "deep path" que o
  agente usou (packages/database/dist/...) pela exportação correta via
  barrel (@eden/database), já que packages/database/src/index.ts é meu
  para editar durante a integração.
- S3/SMTP: troca leitura direta de process.env por loadEnv()/@eden/config
  (que não existia ainda quando o agente rodou no worktree dele).
- Resolve conflito de pnpm-lock.yaml (regenerado via pnpm install).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-03 09:44:25 -03:00
8 changed files with 545 additions and 17 deletions

View File

@@ -1,7 +1,8 @@
/**
* External adapters (Focus NFe, SaperX, Control iD, S3, SMTP) — each
* isolated behind a port per Master Prompt §13. Empty placeholder; the
* first real adapter (S3 or SMTP) lands with the rest of Fase 1's
* platform bootstrap.
* isolated behind a port per Master Prompt §13. S3 and SMTP adapters land
* first (platform bootstrap); the others follow per-domain as their
* modules are built.
*/
export const INTEGRATIONS_PACKAGE_VERSION = "0.1.0";
export * from "./s3/client.js";
export * from "./smtp/mailer.js";

View File

@@ -0,0 +1,128 @@
/**
* S3 adapter — port of the legacy `server/src/lib/s3.js` (eden.md §5
* "Armazenamento S3"). Compatible with any S3-like endpoint (MinIO, AWS S3),
* not exclusive to AWS. Storage failures are never swallowed: every
* function throws a clear error when S3 isn't configured, unlike the SMTP
* adapter (see `../smtp/mailer.ts`) which fails silently by design.
*/
import {
DeleteObjectCommand,
GetObjectCommand,
PutObjectCommand,
S3Client,
} from "@aws-sdk/client-s3";
import type { Readable } from "node:stream";
import { loadEnv } from "@eden/config";
const NOT_CONFIGURED_MESSAGE = "S3 não configurado (S3_ENDPOINT ausente)";
interface S3Config {
endpoint: string;
region: string;
forcePathStyle: boolean;
accessKeyId: string | undefined;
secretAccessKey: string | undefined;
bucket: string | undefined;
}
let client: S3Client | undefined;
let config: S3Config | undefined;
function loadConfig(): S3Config | undefined {
const env = loadEnv();
if (!env.S3_ENDPOINT) {
return undefined;
}
return {
endpoint: env.S3_ENDPOINT,
region: env.S3_REGION,
forcePathStyle: env.S3_FORCE_PATH_STYLE,
accessKeyId: env.S3_ACCESS_KEY_ID,
secretAccessKey: env.S3_SECRET_ACCESS_KEY,
bucket: env.S3_BUCKET,
};
}
function requireConfig(): S3Config {
if (!config) {
config = loadConfig();
}
if (!config) {
throw new Error(NOT_CONFIGURED_MESSAGE);
}
return config;
}
/**
* Raw S3 client, exported for callers (e.g. a future Backup module) that
* need multipart streaming upload via `@aws-sdk/lib-storage`'s `Upload`
* class directly instead of `uploadObject` (which buffers the whole file
* in memory — fine for small files, not for a Postgres dump).
*/
export function getClient(): S3Client {
const cfg = requireConfig();
if (!client) {
client = new S3Client({
endpoint: cfg.endpoint,
region: cfg.region,
forcePathStyle: cfg.forcePathStyle,
...(cfg.accessKeyId && cfg.secretAccessKey
? {
credentials: {
accessKeyId: cfg.accessKeyId,
secretAccessKey: cfg.secretAccessKey,
},
}
: {}),
});
}
return client;
}
/** Single bucket used for all data (no bucket-per-data-type). */
export function getBucket(): string {
const cfg = requireConfig();
return cfg.bucket ?? "";
}
/**
* Simple PUT, whole buffer in memory — adequate for small/medium files
* (images, PDFs, attachments), not for large streaming uploads.
*/
export async function uploadObject(
key: string,
buffer: Buffer,
mimeType: string,
): Promise<void> {
const s3 = getClient();
const bucket = getBucket();
await s3.send(
new PutObjectCommand({
Bucket: bucket,
Key: key,
Body: buffer,
ContentType: mimeType,
}),
);
}
/**
* GET, returning the object body as a Node stream — used both to serve a
* download and to pipe directly to an HTTP response. On Node.js, the SDK
* v3 response `.Body` is already a `Readable` (with extra helper mixins);
* this just types it correctly for callers.
*/
export async function getObjectStream(key: string): Promise<Readable> {
const s3 = getClient();
const bucket = getBucket();
const response = await s3.send(
new GetObjectCommand({ Bucket: bucket, Key: key }),
);
return response.Body as Readable;
}
export async function deleteObject(key: string): Promise<void> {
const s3 = getClient();
const bucket = getBucket();
await s3.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));
}

View File

@@ -0,0 +1,76 @@
/**
* SMTP adapter — port of the legacy `server/src/lib/mailer.js` (eden.md §4
* "Envio de e-mail"). Generic SMTP via `nodemailer`, not a direct
* integration with SES/SendGrid/etc.
*
* Deliberately fail-soft, unlike the S3 adapter (`../s3/client.ts`): no
* business flow may ever fail because of email. If SMTP isn't configured,
* or if sending fails, `sendMail` logs and returns normally — it never
* throws into the caller.
*/
import nodemailer, { type Transporter } from "nodemailer";
import { loadEnv } from "@eden/config";
export interface SendMailInput {
to: string;
subject: string;
html: string;
from?: string;
}
let transporter: Transporter | undefined;
let transporterBuilt = false;
function buildTransporter(): Transporter | undefined {
const env = loadEnv();
if (!env.SMTP_HOST) {
return undefined;
}
return nodemailer.createTransport({
host: env.SMTP_HOST,
port: env.SMTP_PORT,
// secure: true automatically when the port is 465 (implicit SMTPS).
secure: env.SMTP_PORT === 465,
...(env.SMTP_USER ? { auth: { user: env.SMTP_USER, pass: env.SMTP_PASS } } : {}),
});
}
function getTransporter(): Transporter | undefined {
// Built once, lazily, on first use — not rebuilt afterwards even if env
// vars change at runtime (simplification acceptable for this MVP).
if (!transporterBuilt) {
transporter = buildTransporter();
transporterBuilt = true;
}
return transporter;
}
function defaultFrom(): string | undefined {
const env = loadEnv();
return env.SMTP_FROM ?? env.SMTP_USER;
}
export async function sendMail({
to,
subject,
html,
from,
}: SendMailInput): Promise<void> {
const t = getTransporter();
if (!t) {
console.log(
`[mailer] SMTP não configurado — e-mail para ${to} não enviado`,
);
return;
}
try {
await t.sendMail({
from: from ?? defaultFrom(),
to,
subject,
html,
});
} catch (err) {
console.error(`[mailer] falha ao enviar e-mail para ${to}:`, err);
}
}