- CMS
- 后端
- 前端
【免费下载链接】webiny-js
Open-source, self-hosted CMS platform on AWS serverless (Lambda, DynamoDB, S3). TypeScript framework with multi-tenancy, lifecycle hooks, GraphQL API, and AI-assisted development via MCP server. Built for developers at large organizations.
本篇指南完整解析 Webiny 开源仓库中packages/api-mailer邮件模块的一次纯删除型重构:将 SMTP 密码加密从 mailer 自带的异步Encryption抽象,切换为@webiny/api-core/features/encryption中由平台统一注册的共享加密能力,并彻底移除WEBINY_API_MAILER_*全部环境变量(含加密密钥与 SMTP 传输回退)。读者读完将掌握本次重构的架构动机、分 5 个任务推进的实施路径、每条关键代码改动的准确形态,以及升级后运维侧必须执行的迁移动作。
依据文档:2026-04-19-mailer-encryption-removal.md,配套设计说明:2026-04-19-mailer-encryption-removal-design.md。
一、背景与目标:为什么移除 mailer 自己的加密层
1.1 旧架构的问题
在重构之前,packages/api-mailer内部维护了一套独立于平台之外的加密体系,由以下文件构成(本次全部删除):
src/domain/Encryption/abstractions.ts—— mailer 本地Encryption抽象,encrypt/decrypt为异步签名;src/features/Encryption/PasswordEncryption.ts—— 基于 Nodecrypto的 AES-256-GCM 实现;src/features/Encryption/feature.ts—— 在createMailerContext中的 DI 注册;src/features/Encryption/utils/password.ts—— 密码加解密工具;src/features/Encryption/utils/secret.ts—— 读取WEBINY_API_MAILER_PASSWORD_SECRET的密钥读取器。
与此同时,src/features/MailerService/TransportFactory.ts中的getDefaultSettingsFromEnv()会在 KV 存储没有持久化设置时,从WEBINY_API_MAILER_HOST / PORT / USER / PASSWORD / REPLY_TO / FROM环境变量回退读取 SMTP 传输配置。
由此暴露三个问题:
- 加密能力重复实现。平台层
@webiny/api-core/features/encryption已提供统一加解密,mailer 却各自为政,密钥来源(WEBINY_API_MAILER_PASSWORD_SECRET)与平台密钥体系(Infra.Encryption.Key)割裂; - 配置来源分裂。同一套 SMTP 设置既可以来自 KV 持久化存储,又可以来自环境变量回退,行为难以预测,也违背"设置只存一处"的原则;
- 职责越界。GraphQL
getSettings解析器中存在getSecret()短路逻辑,在没有密钥时直接抛错,与平台加密的"无密钥即明文透传"语义冲突。
1.2 目标形态
- 删除 mailer 本地
Encryption抽象与实现,SaveSettings/GetSettings两个仓库改为依赖@webiny/api-core/features/encryption/index.js; - 删除
MailerService的环境变量回退:只要 KV 中没有持久化设置,sendMail立即以NoSettingsConfiguredError失败; - 删除
WEBINY_API_MAILER_*全部 6 个环境变量(HOST、USER、PASSWORD、REPLY_TO、FROM、PASSWORD_SECRET)的类型声明。
架构定性:纯删除 + 依赖替换。由于 api-core 的Encryption已由ApiCoreFeature注册,且该 Feature 在createMailerContext之前运行,mailer 侧不需要新增任何注册,只需把依赖指向平台 token 即可。
1.3 接受的风险(Non-goals)
设计文档明确列出三项不做的内容,理解这些边界有助于避免误解:
- 可插拔传输抽象不在本次范围,
MailTransportFactory形状保持不变; - 无密钥时拒绝保存不在本次范围:平台
Encryption在EncryptionPassphrase未设置时是无操作透传(见 commite693256292),意味着未配置密钥的项目会把 SMTP 密码明文写入 KV 存储,该问题由后续 PR 通过把Infra.Encryption.Key设为敏感设置持久化的硬性要求来兜底; - 不做向后兼容解密:旧密文使用不同的盐且环境变量来源已被移除,升级后无法用新实现解密旧数据,运维需手动重新保存一次 SMTP 凭据(详见第五节迁移说明)。
二、前置条件与工作纪律
实施前必须满足的条件:
- 依赖就绪:每个任务开始前确保仓库根目录至少执行过一次
yarn > /dev/null 2>&1,保证依赖是最新的; - 提交策略:按仓库 CLAUDE.md 约定,提交由用户执行而非 Agent。每个任务以checkpoint 结束——停下、运行验证命令、向用户呈现 diff 摘要,不要运行
git commit; - TDD 说明:这是删除型重构而非新行为,模式是"改代码 → 跑既有测试套件 → 确认全绿"。除重写一个依赖环境变量传输配置的测试外,不引入新测试;Task 2 与 Task 3 各删除一个原本就
skip的测试。
三、分任务实施详解
Task 1:将SaveSettingsRepository与GetSettingsRepository切换到 api-coreEncryption
为什么先做这一步:这两个仓库是 mailer 侧仅有的本地Encryption消费方。切换后本地PasswordEncryption即成为不可达的死代码;而旧测试仍能通过,是因为beforeEach中仍设置着WEBINY_API_MAILER_PASSWORD_SECRET,api-core 加密忽略该变量并按无操作透传运行——对测试断言而言仍是合法的往返(round-trip)。
改动一:src/features/SaveSettings/SaveSettingsRepository.ts
导入行由
import { Encryption } from "~/domain/Encryption/abstractions.js";改为
import { Encryption } from "@webiny/api-core/features/encryption/index.js";同时删除execute()内两处await(encrypt/decrypt由异步变为同步),改后方法体(类型不变)为:
async execute(input: SaveSettingsInput): SaveSettingsRepository.Return { // Check if settings exist const existingResult = await this.keyValueStore.get<TransportSettings>(MAILER_TRANSPORT_SETTINGS); const existingSettings = existingResult.isOk() ? existingResult.value : null; const transportSettings: Partial<TransportSettings> = existingSettings ?? {}; // If updating and no password provided, keep the existing password let passwordToStore = input.password || ""; if (!input.password && existingSettings) { passwordToStore = this.encryption.decrypt(transportSettings.password || ""); } // Encrypt password const encryptedPassword = this.encryption.encrypt(passwordToStore); // Prepare data const data = { host: input.host ?? transportSettings.host, port: input.port ?? transportSettings.port ?? DEFAULT_PORT, user: input.user ?? transportSettings.user, password: encryptedPassword, from: input.from ?? transportSettings.from, replyTo: input.replyTo ?? transportSettings.replyTo }; // Save settings const result = await this.keyValueStore.set(MAILER_TRANSPORT_SETTINGS, data); if (result.isFail()) { return Result.fail(new SettingsPersistenceError(result.error)); } // Return without encrypted password const returnSettings: TransportSettings = { ...data, password: "" // Don't return password }; return Result.ok(returnSettings); }构造器与createImplementation调用保持不变——Encryptiontoken 的形态一致(仅接口层面同步/异步不同),依赖装配无需变动。
改动二:src/features/GetSettings/GetSettingsRepository.ts
同样的导入替换,并删除get()中this.encryption.decrypt(...)前的await,改后方法体为:
async get(): Promise<Result<TransportSettings | null>> { const result = await this.keyValueStore.get<TransportSettings>(MAILER_TRANSPORT_SETTINGS); if (result.isFail()) { return Result.ok(null); } const settings = result.value; if (!settings) { return Result.ok(null); } // Decrypt password if present const password = settings.password ? this.encryption.decrypt(String(settings.password)) : ""; const transportSettings: TransportSettings = { host: String(settings.host || ""), port: Number(settings.port || 25), user: String(settings.user || ""), password, from: String(settings.from || ""), replyTo: settings.replyTo ? String(settings.replyTo) : undefined }; return Result.ok(transportSettings); }验证(Step 3–4):从仓库根目录执行
yarn build -p @webiny/api-mailer 2>&1 | tail -30 yarn test packages/api-mailer 2>&1 | tail -50构建成功预期;若 TypeScript 报Encryption类型不匹配,说明旧的本地异步签名仍在别处泄漏——用grep搜索~/domain/Encryption/abstractions.js的导入,确认只有 Save/Get 两个仓库引用过它。测试应保持与改动前相同的绿色数量,旧PasswordEncryption虽仍注册但已无处注入。
Checkpoint:向用户呈现 diff 摘要,建议提交信息refactor(api-mailer): switch Save/Get settings repositories to api-core Encryption。
Task 2:删除本地加密 Feature、移除 GraphQLgetSecret短路、清理 env-var 测试
为什么紧随其后:Task 1 落地后本地PasswordEncryption及其工具函数即为死代码。删除它们必须与 GraphQL 解析器清理(仍引用getSecret)放在同一任务内,否则构建直接断裂;设置WEBINY_API_MAILER_PASSWORD_SECRET的测试写入也要同步剔除,因为环境变量读取已不存在。
Step 1:src/index.ts移除注册
删除导入import { EncryptionFeature } from "~/features/Encryption/feature.js";与createMailerContext内的EncryptionFeature.register(context.container);一行,改后文件必须与以下完全一致:
import { createContextPlugin } from "@webiny/api"; import { GetSettingsFeature } from "~/features/GetSettings/feature.js"; import { SaveSettingsFeature } from "~/features/SaveSettings/feature.js"; import { DummyTransportFeature } from "~/features/DummyTransport/feature.js"; import { SmtpTransportFeature } from "~/features/SmtpTransport/feature.js"; import { MailerServiceFeature } from "~/features/MailerService/feature.js"; import { SendMailFeature } from "~/features/SendMail/feature.js"; import { createSettingsGraphQL } from "~/graphql/settings.js"; export { MailerService } from "./domain/MailerService/abstractions.js"; export type { IMailerService, IMailerServiceErrors } from "./domain/MailerService/abstractions.js"; export const createMailerContext = () => { return createContextPlugin(context => { // Register all features DummyTransportFeature.register(context.container); SmtpTransportFeature.register(context.container); GetSettingsFeature.register(context.container); SaveSettingsFeature.register(context.container); MailerServiceFeature.register(context.container); SendMailFeature.register(context.container); }); }; export const createMailerGraphQL = () => { return createSettingsGraphQL(); };Step 2:graphql/settings.ts移除getSecret短路
删除导入import { getSecret } from "~/features/Encryption/utils/secret.js";,并删除MailerQuery.getSettings解析器内这段逻辑:
// First check of encryption key is set! // If not, this function will throw an error. // TODO: refactor this to make more sense. getSecret();改后解析器为:
getSettings: async (_, __, context) => { try { const getSettings = context.container.resolve(GetSettingsUseCase); const result = await getSettings.execute(); const settings = result.value; // Remove password from response if (settings?.password) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { password, ...settingsWithoutPassword } = settings; return new Response(settingsWithoutPassword); } return new Response(settings); } catch (ex) { return new ErrorResponse(ex); } }文件其余部分不动。
Step 3:删除本地加密文件夹
rm -rf packages/api-mailer/src/domain/Encryption rm -rf packages/api-mailer/src/features/Encryption验证:
ls packages/api-mailer/src/domain 2>&1 ls packages/api-mailer/src/features 2>&1预期:domain/仅含MailerService、MailTransport、errors.ts;features/仅含DummyTransport、GetSettings、MailerService、SaveSettings、SendMail、SmtpTransport。
Step 4:package.json清空adio.ignore.src
由
"adio": { "ignore": { "src": [ "node:crypto" ] } }改为
"adio": { "ignore": { "src": [] } }保留空的adio块而非整体删除——其他包沿用同样模式,保持一致性可避免yarn adio配置层面的意外。
Step 5/6:清理测试中的 env-var 写入
__tests__/settings.crud.test.ts:删除整个beforeEach块
beforeEach(() => { delete process.env["WEBINY_API_MAILER_PASSWORD_SECRET"]; });若beforeEach导入因此不再使用,从import { describe, it, expect, beforeEach, vi } from "vitest";中一并剔除,得到import { describe, it, expect, vi } from "vitest";。删除已 skip 的测试it.skip("should not be possible to get or save settings without secret", ...)(连同其前的// TODO: @bruno - the \catch` block is no longer triggered注释)。删除其余所有形如process.env.WEBINY_API_MAILER_PASSWORD_SECRET = "really secret secret";的行(共 6 处,每处为测试体首行单行)。编辑后用grep -n WEBINY_API_MAILER_PASSWORD_SECRET packages/api-mailer/tests/settings.crud.test.ts` 验证,预期无输出。
__tests__/settings.graphql.test.ts:删除整个beforeEach块
beforeEach(() => { process.env.WEBINY_API_MAILER_PASSWORD_SECRET = "really secret secret"; });同步清理beforeEach导入,删除已 skip 的测试it.skip("should not be possible to get or save settings when no secret is available", ...)(含// TODO: @bruno - this test is no longer failing注释),最后验证grep -n WEBINY_API_MAILER packages/api-mailer/__tests__/settings.graphql.test.ts无输出。
Step 7:构建与测试
yarn build -p @webiny/api-mailer 2>&1 | tail -30 yarn test packages/api-mailer --testPathPattern=settings 2>&1 | tail -50若 Vitest 配置不接受--testPathPattern,则跑完整套件yarn test packages/api-mailer——此时transporter.crud.test.ts仍依赖 env-var 传输配置是正常的,因为getDefaultSettingsFromEnv()尚未被改动(Task 3 才动MailerService)。
Checkpoint:建议提交信息refactor(api-mailer): remove local Encryption feature and WEBINY_API_MAILER_PASSWORD_SECRET。
Task 3:移除MailerService的 env-var 传输回退,重写transporter.crud.test.ts
为什么留到最后处理:Task 2 刻意保留getDefaultSettingsFromEnv(),是为了让transporter.crud.test.ts::should send an email无需改动即可继续通过;本任务才删除回退,并把那个唯一依赖 env 配置的测试改为通过SaveSettingsUseCase持久化设置。
Step 1:MailerService.ts去除回退
删除导入import { getDefaultSettingsFromEnv } from "./TransportFactory.js";,并让sendMail在无持久化设置时快速失败,改后完整文件为:
import { Result } from "@webiny/feature/api"; import { MailerService as Abstraction } from "~/domain/MailerService/abstractions.js"; import { NoTransportAvailableError, NoSettingsConfiguredError, TransportSendError } from "~/domain/MailerService/errors.js"; import { MailTransport, MailTransportFactory } from "~/domain/MailTransport/abstractions.js"; import { GetSettingsRepository } from "../GetSettings/abstractions.js"; import type { TransportSettings, TransportSendData } from "~/types.js"; class MailerServiceImpl implements Abstraction.Interface { constructor( private getSettingsRepository: GetSettingsRepository.Interface, private transportFactories: MailTransportFactory.Interface[] ) {} async sendMail<T = any>(data: TransportSendData): Abstraction.Return<T> { const result = await this.getSettingsRepository.get(); const settings = result.value; if (!settings) { return Result.fail(new NoSettingsConfiguredError()); } const transport = await this.getTransport(settings); if (!transport) { return Result.fail(new NoTransportAvailableError()); } try { const response = await transport.send(data); if (response.error) { return Result.fail(new TransportSendError(response.error)); } return Result.ok(response); } catch (error) { return Result.fail(new TransportSendError(error)); } } private async getTransport( settings: TransportSettings ): Promise<MailTransport.Interface | null> { if (this.transportFactories.length === 0) { return null; } const factory = this.transportFactories[this.transportFactories.length - 1]; return factory.createTransport(settings); } } export const MailerService = Abstraction.createImplementation({ implementation: MailerServiceImpl, dependencies: [GetSettingsRepository, [MailTransportFactory, { multiple: true }]] });Step 2:删除TransportFactory.ts
rm packages/api-mailer/src/features/MailerService/TransportFactory.tsStep 3:重写transporter.crud.test.ts
完整改后文件为(注意SaveSettingsUseCase的导入与新增的persistTransportSettings辅助函数):
import { describe, it, expect, vi } from "vitest"; import { createContextHandler } from "./contextHandler"; import { SendMailUseCase } from "~/features/SendMail/abstractions.js"; import { SaveSettingsUseCase } from "~/features/SaveSettings/abstractions.js"; import { TransportSendData } from "~/types"; vi.mock("nodemailer", () => { return { default: { createTransport: () => { return { sendMail: async (params: TransportSendData) => { return { envelope: "envelope", messageId: "123", accepted: [params.to], rejected: [], pending: [], response: "ok" }; } }; } } }; }); const to = ["to@dummy-host.webiny"]; const cc = ["cc@dummy-host.webiny"]; const bcc = ["bcc@dummy-host.webiny"]; const from = "from@dummy-host.webiny"; const replyTo = "replyTo@dummy-host.webiny"; const subject = "Some dummy subject"; const text = "Some dummy body"; const html = "<p>Some dummy body</p>"; const persistTransportSettings = async ( context: Awaited<ReturnType<ReturnType<typeof createContextHandler>["handle"]>> ) => { const saveSettings = context.container.resolve(SaveSettingsUseCase); const result = await saveSettings.execute({ host: "dummy-host.webiny", user: "user", password: "password", from, replyTo }); if (result.isFail()) { throw new Error( `Failed to persist mailer settings for test setup: ${result.error.message}` ); } }; describe("Mailer Transporter CRUD", () => { const { handle } = createContextHandler(); it(`should throw error before sending because of missing "to"`, async () => { const context = await handle(); const params: TransportSendData = { to: [""], cc, bcc, from, replyTo, subject, text, html }; const sendMail = context.container.resolve(SendMailUseCase); const result = await sendMail.execute(params); expect(result.isFail()).toBe(true); expect(result.error.code).toBe("Mailer/SendMail/Validation"); }); it(`should throw error before sending because of missing "from"`, async () => { const context = await handle(); const params: TransportSendData = { to, cc, bcc, from: "", replyTo, subject, text, html }; const sendMail = context.container.resolve(SendMailUseCase); const result = await sendMail.execute(params); expect(result.isFail()).toBe(true); expect(result.error.code).toBe("Mailer/SendMail/Validation"); }); it(`should throw error before sending because of missing "subject"`, async () => { const context = await handle(); const params: TransportSendData = { to, cc, bcc, from, replyTo, subject: "", text, html }; const sendMail = context.container.resolve(SendMailUseCase); const result = await sendMail.execute(params); expect(result.isFail()).toBe(true); expect(result.error.code).toBe("Mailer/SendMail/Validation"); }); it(`should throw error before sending because of missing both "text" and "html"`, async () => { const context = await handle(); const params: TransportSendData = { to, cc, bcc, from, replyTo, subject, text: "", html: "" }; const sendMail = context.container.resolve(SendMailUseCase); const result = await sendMail.execute(params); expect(result.isFail()).toBe(true); expect(result.error.code).toBe("Mailer/SendMail/Validation"); }); it("should send an email", async () => { const context = await handle(); await persistTransportSettings(context); const params: TransportSendData = { to, cc, bcc, from, replyTo, subject, text, html }; const sendMail = context.container.resolve(SendMailUseCase); const result = await sendMail.execute(params); expect(result.isOk()).toBe(true); expect(result.value).toEqual({ result: "ok", error: null }); }); });与旧版的关键差异:
beforeEach的 env-var 写入整个移除(钩子整体消失);beforeEach从vitest导入中剔除;- 顶部新增
SaveSettingsUseCase导入; - 新增
persistTransportSettings辅助函数,在发送测试前保存设置; "should send an email"测试在调用SendMailUseCase前先调用该辅助函数。
Step 4:构建与测试
yarn build -p @webiny/api-mailer 2>&1 | tail -30 yarn test packages/api-mailer 2>&1 | tail -50预期全绿。should send an email现在完整走通SaveSettings → GetSettings → MailerService → mocked nodemailer的真实往返链路。
Checkpoint:建议提交信息refactor(api-mailer): drop env-var transport fallback, require persisted settings。
Task 4:删除WEBINY_API_MAILER_*环境变量类型声明
为什么最后单独处理:已无任何代码读取这些变量。删除类型声明可防止未来误用,并能在构建期暴露任何遗漏的引用。
编辑typings/env/index.d.ts,删除整个api-mailer块(next分支上位于 65–73 行,若文件被编辑过行号可能漂移):
/** * api-mailer */ WEBINY_API_MAILER_HOST?: string; WEBINY_API_MAILER_USER?: string; WEBINY_API_MAILER_PASSWORD?: string; WEBINY_API_MAILER_REPLY_TO?: string; WEBINY_API_MAILER_FROM?: string; WEBINY_API_MAILER_PASSWORD_SECRET?: string;文件其余声明保持不动。随后从仓库根目录验证:
grep -rn "WEBINY_API_MAILER" packages typings 2>&1 | head -20预期无任何匹配;若有残留则先修复再继续。最后构建并跑完整测试套件:
yarn build -p @webiny/api-mailer 2>&1 | tail -30 yarn test packages/api-mailer 2>&1 | tail -50Checkpoint:建议提交信息chore(typings): remove WEBINY_API_MAILER_* environment variables。
Task 5:最终提交前的完整预检
按仓库 CLAUDE.md 强制要求,每次提交前必须按此序列执行一遍,以捕捉所有删除之后的 TS 配置漂移、依赖声明、格式、lint 与工作区依赖同步问题。
# 1. 暂存变更 git add . # 2. 确保 yarn.lock 最新 yarn > /dev/null 2>&1 # 3. 重新生成各包 tsconfig node scripts/generateTsConfigsInPackages.js # 4. adio 依赖声明检查 yarn adio 2>&1 | tail -20 # 5. 格式化 yarn prettier:fix > /dev/null 2>&1 # 6. Lint y eslint 2>&1 | tail -30 # 7. 工作区依赖同步 yarn webiny sync-dependencies 2>&1 | tail -20 # 8. 重新暂存 git add .关于 Step 4 的重要提示:如果yarn adio将@webiny/api-core标记为api-mailer的缺失依赖,请注意当前状态——@webiny/api-core在packages/api-mailer/package.json中仅列于devDependencies。此前这可以被接受,因为 mailer 只导入 token(视为 peer)。Task 1 新增的@webiny/api-core/features/encryption/index.js导入与既有的EventPublisher、IdentityContext、KeyValueStore导入属于完全相同的运行时 token 模式,不应改变依赖状态;若yarn adio不认同,以其建议为准(它是权威工具)。
Step 9 最终 checkpoint:向用户展示git status输出,并汇总各任务的提交信息建议,若用户选择 squash 可直接复用:
refactor(api-mailer): drop local encryption, rely on api-core Encryption + persisted settings * Swap SaveSettings/GetSettings repositories to @webiny/api-core/features/encryption (sync encrypt/decrypt). * Delete local Encryption feature, domain, and password-secret env var. * Remove env-var transport fallback; sendMail now requires persisted settings. * Remove all WEBINY_API_MAILER_* env typings. * Tests updated: the transporter send test persists settings via SaveSettingsUseCase. Operators must re-save their mailer SMTP settings after this upgrade — the previously stored ciphertext is no longer decryptable with the new (shared) encryption.等待用户提交。
四、成功验收标准
全部任务落地后逐项核对:
packages/api-mailer/src/domain/Encryption/与packages/api-mailer/src/features/Encryption/已不存在;SaveSettingsRepository与GetSettingsRepository依赖@webiny/api-core/features/encryption并使用同步encrypt/decrypt;MailerService无环境变量回退;未持久化设置时sendMail以NoSettingsConfiguredError快速失败;graphql/settings.ts无getSecret导入、无短路、无关于加密的 TODO;packages/api-mailer/**与typings/env/index.d.ts中无任何WEBINY_API_MAILER_*残留;packages/api-mailer下所有__tests__/*.ts在不设置任何WEBINY_API_MAILER_*环境变量的情况下全部通过;yarn build -p @webiny/api-mailer成功;yarn adio通过。
五、运维迁移说明(升级后必读)
升级后 KV 存储(
Mailer/Settings/Transport)中已存在的邮件设置将无法再用新实现解密。运维人员必须通过管理后台(或 GraphQLmailer.saveSettings变更)重新保存一次 SMTP 设置。以下环境变量不再被读取,应从项目配置中移除:WEBINY_API_MAILER_PASSWORD_SECRET、WEBINY_API_MAILER_HOST、WEBINY_API_MAILER_PORT、WEBINY_API_MAILER_USER、WEBINY_API_MAILER_PASSWORD、WEBINY_API_MAILER_REPLY_TO、WEBINY_API_MAILER_FROM。
六、仓库现状对照:重构已在主线落地
阅读本文时可直接对照当前仓库源码验证结果(注意:本次重构之后主线又在此基础上继续演进,引入了CodeMailerSettings代码源设置、secure端口安全字段、按传输名解析等新能力,但本文的核心结论——本地Encryption已删除、依赖已切换到 api-core、WEBINY_API_MAILER_*已全部移除——依然成立):
- 共享加密 token 定义位于 abstractions.ts,
IEncryption当前接口为encrypt(value: string): Promise<string>/decrypt(value: string): Promise<string>,由createAbstraction<IEncryption>("Encryption")创建,配套实现与注册见同一目录下的EncryptionService.ts、feature.ts与index.ts。设计文档描述的是"同步"接口形态,而主线演进后接口再次改为异步——实施任何调用时务必以当前接口签名为准(相应地在仓库源码中,两个仓库的实现都保留了await this.encryption.*); - mailer 两个仓库的当前实现:SaveSettingsRepository.ts 与 GetSettingsRepository.ts 均从
@webiny/api-core/features/encryption/index.js导入Encryption,依赖注入数组为[KeyValueStore, Encryption]与[KeyValueStore, Encryption, CodeMailerSettings],不再引用任何~/domain/Encryption路径; MailerService当前形态:MailerService.ts 中已无getDefaultSettingsFromEnv与TransportFactory导入,sendMail经由resolveTransport()基于工厂名逐项匹配可用传输,错误路径返回NoTransportAvailableError/TransportSendError/TransportCreateError;- env 变量零残留:对
typings/env/index.d.ts与packages/api-mailer全目录搜索WEBINY_API_MAILER,均无任何匹配——Task 4 的验收标准在主线已满足; - 测试套件现状:
packages/api-mailer/__tests__/下已无settings.graphql.test.ts(被后续演进合并/重命名),现存settings.crud.test.ts、transporter.crud.test.ts、saveSettingsEvents.test.ts、settings.codeSource.test.ts、codeSettings.test.ts等不再设置任何WEBINY_API_MAILER_*环境变量,且transporter.crud.test.ts已采用"通过SaveSettingsUseCase持久化设置后再发邮件"的写法——与本文 Task 3 的落地形态一致。
七、结语
本次重构是 Webiny "平台能力收敛、删除重复实现" 的典型样例:mailer 从自带加密、自带配置来源,收敛为完全依赖@webiny/api-core的共享Encryption与 KV 持久化设置,配置只存一处、密钥只认平台一套。对开发者而言,最有价值的可复用经验是:删除型重构也可以像新增功能一样分任务推进、以 checkpoint 控制节奏、用既有测试套件兜底;对运维而言,升级后重新保存一次 SMTP 凭据是唯一必须的手动动作。
- CMS
- 后端
- 前端
【免费下载链接】webiny-js
Open-source, self-hosted CMS platform on AWS serverless (Lambda, DynamoDB, S3). TypeScript framework with multi-tenancy, lifecycle hooks, GraphQL API, and AI-assisted development via MCP server. Built for developers at large organizations.
相关推荐
Webiny Headless CMS 存储层重构:移除废弃的 `register*StorageOperations` 包装器,全面落地 DI Feature 注册模式
Webiny Headless CMS 存储层重构:移除废弃的 register StorageOperations 包装器,全面落地 DI Feature 注
CMS后端前端electron-builder Toolset 体系重构:ToolsetCustom 自定义工具包、环境变量移除与 PlatformPackager.info 弃用
electron builder Toolset 体系重构:ToolsetCustom 自定义工具包、环境变量移除与 PlatformPackager.info
构建工具桌面应用开发工具Sphinx 废弃 API 完全指南:废弃策略、RemovedInSphinxXXWarning 机制与全量迁移清单
Sphinx 废弃 API 完全指南:废弃策略、RemovedInSphinxXXWarning 机制与全量迁移清单 本文是 Sphinx 扩展开发者与深度用户
文档开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考