@ubean/server 0.1.13 → 0.2.1

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 (56) hide show
  1. package/dist/analytics-entry.d.ts +2 -0
  2. package/dist/analytics-entry.js +2 -0
  3. package/dist/cache-C84ix1Vq.js +173 -0
  4. package/dist/cache-b-MZlyv0.d.ts +48 -0
  5. package/dist/cache-directive-C1Nekkza.js +304 -0
  6. package/dist/cache-directive-CAxJAQyE.d.ts +175 -0
  7. package/dist/cache-directive.d.ts +2 -0
  8. package/dist/cache-directive.js +2 -0
  9. package/dist/cache-entry.d.ts +3 -0
  10. package/dist/cache-entry.js +3 -0
  11. package/dist/cron-entry.d.ts +2 -0
  12. package/dist/cron-entry.js +2 -0
  13. package/dist/cron-scheduler-BF33PPn4.d.ts +77 -0
  14. package/dist/cron-scheduler-BVuXv7nn.js +258 -0
  15. package/dist/database-CfpFznl-.d.ts +67 -0
  16. package/dist/database-DNrY44SQ.js +352 -0
  17. package/dist/database.d.ts +2 -0
  18. package/dist/database.js +2 -0
  19. package/dist/email-BjfRiR9b.js +354 -0
  20. package/dist/email-BvpEuNn_.d.ts +226 -0
  21. package/dist/email.d.ts +2 -0
  22. package/dist/email.js +2 -0
  23. package/dist/feature-flags-CdLwsMD2.js +657 -0
  24. package/dist/feature-flags-DWkS6p0D.d.ts +386 -0
  25. package/dist/fetch-memo-rbkxxnW4.js +338 -0
  26. package/dist/index.d.ts +183 -488
  27. package/dist/index.js +352 -2023
  28. package/dist/middleware.d.ts +2 -0
  29. package/dist/middleware.js +3 -0
  30. package/dist/observability-Cio6Qq1H.js +339 -0
  31. package/dist/observability-DUNUEjj3.d.ts +70 -0
  32. package/dist/observability.d.ts +2 -0
  33. package/dist/observability.js +2 -0
  34. package/dist/queue-Bwzi3mhK.js +210 -0
  35. package/dist/queue-GOfTAWlz.d.ts +55 -0
  36. package/dist/queue.d.ts +2 -0
  37. package/dist/queue.js +2 -0
  38. package/dist/realtime.d.ts +2 -0
  39. package/dist/realtime.js +2 -0
  40. package/dist/security.d.ts +2 -0
  41. package/dist/security.js +2 -0
  42. package/dist/sessions-BLqFFQTL.d.ts +217 -0
  43. package/dist/sessions-BsBsyFAG.js +450 -0
  44. package/dist/single-flight-BJyhDLdU.d.ts +422 -0
  45. package/dist/single-flight-mJ4ZKbx1.js +715 -0
  46. package/dist/sse-Ct72zhic.d.ts +95 -0
  47. package/dist/sse-a6Ky9Vcl.js +310 -0
  48. package/dist/static-DPHaovQe.js +90 -0
  49. package/dist/static-K2dRvjpS.d.ts +11 -0
  50. package/dist/static.d.ts +2 -0
  51. package/dist/static.js +2 -0
  52. package/dist/storage-BZLMaqHr.js +162 -0
  53. package/dist/storage-QdlPtPtR.d.ts +48 -0
  54. package/dist/storage.d.ts +2 -0
  55. package/dist/storage.js +2 -0
  56. package/package.json +68 -6
@@ -0,0 +1,352 @@
1
+ import { createHooks } from "hookable";
2
+ //#region src/database.ts
3
+ const _global = globalThis;
4
+ const dbRegistry = /* @__PURE__ */ new Map();
5
+ const dbHooks = createHooks();
6
+ let defaultDatabase = null;
7
+ const RAW_SQL = Symbol("rawSql");
8
+ function rawSql(str) {
9
+ return {
10
+ [RAW_SQL]: true,
11
+ value: str
12
+ };
13
+ }
14
+ function createInMemoryDatabase() {
15
+ const tables = /* @__PURE__ */ new Map();
16
+ function getTable(name) {
17
+ return tables.get(name.toLowerCase());
18
+ }
19
+ function parseSqlValue(val) {
20
+ const trimmed = val.trim();
21
+ const upper = trimmed.toUpperCase();
22
+ if (upper === "NULL") return null;
23
+ if (upper === "TRUE") return true;
24
+ if (upper === "FALSE") return false;
25
+ if (/^-?\d+$/.test(trimmed)) return parseInt(trimmed, 10);
26
+ if (/^-?\d+\.\d+$/.test(trimmed)) return parseFloat(trimmed);
27
+ if (trimmed.startsWith("'") && trimmed.endsWith("'") || trimmed.startsWith("\"") && trimmed.endsWith("\"")) {
28
+ const quote = trimmed[0];
29
+ return trimmed.slice(1, -1).replace(new RegExp(quote + quote, "g"), quote);
30
+ }
31
+ return trimmed;
32
+ }
33
+ function splitByCommaRespectingStrings(str) {
34
+ const parts = [];
35
+ let current = "";
36
+ let inString = false;
37
+ let stringChar = "";
38
+ let depth = 0;
39
+ for (let i = 0; i < str.length; i++) {
40
+ const ch = str[i];
41
+ const next = str[i + 1];
42
+ if (!inString && (ch === "'" || ch === "\"")) {
43
+ inString = true;
44
+ stringChar = ch;
45
+ current += ch;
46
+ } else if (inString && ch === stringChar) {
47
+ if (next === stringChar) {
48
+ current += ch + ch;
49
+ i++;
50
+ } else {
51
+ inString = false;
52
+ current += ch;
53
+ }
54
+ } else if (!inString && ch === "(") {
55
+ depth++;
56
+ current += ch;
57
+ } else if (!inString && ch === ")") {
58
+ depth--;
59
+ current += ch;
60
+ } else if (!inString && depth === 0 && ch === ",") {
61
+ parts.push(current.trim());
62
+ current = "";
63
+ } else current += ch;
64
+ }
65
+ if (current.trim()) parts.push(current.trim());
66
+ return parts;
67
+ }
68
+ function parseCreateTable(query) {
69
+ const ifNotExists = /IF\s+NOT\s+EXISTS/i.test(query);
70
+ const match = query.match(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["`]?(\w+)["`]?\s*\(([\s\S]+)\)\s*;?\s*$/i);
71
+ if (!match) return;
72
+ const [, tableName, colsStr] = match;
73
+ const lowerName = tableName.toLowerCase();
74
+ if (ifNotExists && tables.has(lowerName)) return;
75
+ const table = {
76
+ name: tableName,
77
+ columns: /* @__PURE__ */ new Map(),
78
+ rows: []
79
+ };
80
+ const colDefs = splitByCommaRespectingStrings(colsStr);
81
+ for (const colDef of colDefs) {
82
+ const upperDef = colDef.toUpperCase().trim();
83
+ if (upperDef.startsWith("PRIMARY KEY") || upperDef.startsWith("CONSTRAINT") || upperDef.startsWith("FOREIGN") || upperDef.startsWith("UNIQUE") || upperDef.startsWith("INDEX")) continue;
84
+ const match2 = colDef.match(/^["`]?(\w+)["`]?\s+(\w+)?(.*)$/);
85
+ if (!match2) continue;
86
+ const [, colNameRaw, colTypeRaw = "TEXT", rest] = match2;
87
+ const colName = colNameRaw.replace(/["`]/g, "");
88
+ const colType = (colTypeRaw || "TEXT").toUpperCase();
89
+ const isPK = /PRIMARY\s+KEY/i.test(rest);
90
+ const notNull = /NOT\s+NULL/i.test(rest);
91
+ table.columns.set(colName, {
92
+ type: colType,
93
+ primaryKey: isPK,
94
+ notNull
95
+ });
96
+ }
97
+ tables.set(lowerName, table);
98
+ }
99
+ function parseInsert(query) {
100
+ const insertMatch = query.match(/INSERT\s+INTO\s+["`]?(\w+)["`]?\s*(?:\(([^)]+)\))?\s*VALUES\s*\(([\s\S]+)\)\s*;?\s*$/i);
101
+ if (!insertMatch) return null;
102
+ const [, tableName, colsStr, valuesStr] = insertMatch;
103
+ const table = getTable(tableName);
104
+ if (!table) return null;
105
+ const columns = colsStr ? splitByCommaRespectingStrings(colsStr).map((c) => c.trim().replace(/["`]/g, "")) : Array.from(table.columns.keys());
106
+ const values = splitByCommaRespectingStrings(valuesStr).map((s) => parseSqlValue(s));
107
+ return {
108
+ table: tableName.toLowerCase(),
109
+ columns,
110
+ values
111
+ };
112
+ }
113
+ function parseSelect(query) {
114
+ const simpleMatch = query.match(/SELECT\s+([\s\S]+?)\s+FROM\s+["`]?(\w+)["`]?/i);
115
+ if (!simpleMatch) return null;
116
+ const tableName = simpleMatch[2];
117
+ let where;
118
+ const whereIdx = query.toUpperCase().indexOf("WHERE");
119
+ if (whereIdx !== -1) {
120
+ let wherePart = query.slice(whereIdx + 5);
121
+ const orderIdx = wherePart.toUpperCase().indexOf("ORDER");
122
+ const limitIdx = wherePart.toUpperCase().indexOf("LIMIT");
123
+ let endIdx = wherePart.length;
124
+ if (orderIdx !== -1) endIdx = Math.min(endIdx, orderIdx);
125
+ if (limitIdx !== -1) endIdx = Math.min(endIdx, limitIdx);
126
+ wherePart = wherePart.slice(0, endIdx).trim().replace(/;$/, "");
127
+ const eqIdx = wherePart.indexOf("=");
128
+ if (eqIdx !== -1) where = {
129
+ col: wherePart.slice(0, eqIdx).trim().replace(/["`]/g, ""),
130
+ val: parseSqlValue(wherePart.slice(eqIdx + 1).trim())
131
+ };
132
+ }
133
+ return {
134
+ table: tableName.toLowerCase(),
135
+ where
136
+ };
137
+ }
138
+ function escapeValue(val) {
139
+ if (val !== null && typeof val === "object" && RAW_SQL in val) return val.value;
140
+ if (val === null || val === void 0) return "NULL";
141
+ if (typeof val === "number") return String(val);
142
+ if (typeof val === "boolean") return val ? "1" : "0";
143
+ if (val instanceof Date) return `'${val.toISOString()}'`;
144
+ return `'${String(val).replace(/'/g, "''")}'`;
145
+ }
146
+ function buildQueryFromTemplate(strings, values) {
147
+ let query = "";
148
+ for (let i = 0; i < strings.length; i++) {
149
+ query += strings[i];
150
+ if (i < values.length) query += escapeValue(values[i]);
151
+ }
152
+ return query;
153
+ }
154
+ function executeRaw(query) {
155
+ const upperQuery = query.toUpperCase().trim();
156
+ if (upperQuery.startsWith("CREATE TABLE")) {
157
+ parseCreateTable(query);
158
+ return [];
159
+ }
160
+ if (upperQuery.startsWith("INSERT INTO")) {
161
+ const parsed = parseInsert(query);
162
+ if (parsed) {
163
+ const table = getTable(parsed.table);
164
+ if (table) {
165
+ const row = {};
166
+ for (let i = 0; i < parsed.columns.length; i++) row[parsed.columns[i]] = parsed.values[i] ?? null;
167
+ table.rows.push(row);
168
+ }
169
+ }
170
+ return [];
171
+ }
172
+ if (upperQuery.startsWith("SELECT")) {
173
+ const parsed = parseSelect(query);
174
+ if (parsed) {
175
+ const table = getTable(parsed.table);
176
+ if (!table) return [];
177
+ let rows = [...table.rows];
178
+ if (parsed.where) rows = rows.filter((row) => row[parsed.where.col] == parsed.where.val);
179
+ return rows;
180
+ }
181
+ return [];
182
+ }
183
+ if (upperQuery.startsWith("DELETE FROM")) {
184
+ const match = query.match(/DELETE\s+FROM\s+["`]?(\w+)["`]?\s*(?:WHERE\s+(.+))?/i);
185
+ if (match) {
186
+ const table = getTable(match[1]);
187
+ if (table) {
188
+ const whereStr = match[2];
189
+ if (whereStr) {
190
+ const eqIdx = whereStr.indexOf("=");
191
+ if (eqIdx !== -1) {
192
+ const col = whereStr.slice(0, eqIdx).trim().replace(/["`]/g, "");
193
+ const val = parseSqlValue(whereStr.slice(eqIdx + 1).trim());
194
+ table.rows = table.rows.filter((row) => row[col] != val);
195
+ } else table.rows = [];
196
+ } else table.rows = [];
197
+ }
198
+ }
199
+ return [];
200
+ }
201
+ if (upperQuery.startsWith("DROP TABLE")) {
202
+ const match = query.match(/DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?["`]?(\w+)["`]?/i);
203
+ if (match) tables.delete(match[1].toLowerCase());
204
+ return [];
205
+ }
206
+ return [];
207
+ }
208
+ return {
209
+ async sql(strings, ...values) {
210
+ const query = buildQueryFromTemplate(strings, values);
211
+ try {
212
+ await dbHooks.callHook("db:query", query, values);
213
+ return { rows: executeRaw(query) };
214
+ } catch (err) {
215
+ await dbHooks.callHook("db:error", err instanceof Error ? err : new Error(String(err)), query);
216
+ throw err;
217
+ }
218
+ },
219
+ async exec(query) {
220
+ const statements = splitSqlStatements(query);
221
+ for (const stmt of statements) {
222
+ const trimmed = stmt.trim();
223
+ if (!trimmed) continue;
224
+ try {
225
+ await dbHooks.callHook("db:query", trimmed);
226
+ executeRaw(trimmed);
227
+ } catch (err) {
228
+ await dbHooks.callHook("db:error", err instanceof Error ? err : new Error(String(err)), trimmed);
229
+ throw err;
230
+ }
231
+ }
232
+ },
233
+ async close() {
234
+ tables.clear();
235
+ }
236
+ };
237
+ }
238
+ function splitSqlStatements(sql) {
239
+ const statements = [];
240
+ let current = "";
241
+ let inString = false;
242
+ let stringChar = "";
243
+ for (let i = 0; i < sql.length; i++) {
244
+ const ch = sql[i];
245
+ if (!inString && (ch === "'" || ch === "\"")) {
246
+ inString = true;
247
+ stringChar = ch;
248
+ current += ch;
249
+ } else if (inString && ch === stringChar) {
250
+ if (sql[i + 1] === stringChar) {
251
+ current += ch + ch;
252
+ i++;
253
+ } else {
254
+ inString = false;
255
+ current += ch;
256
+ }
257
+ } else if (!inString && ch === ";") {
258
+ statements.push(current);
259
+ current = "";
260
+ } else current += ch;
261
+ }
262
+ if (current.trim()) statements.push(current);
263
+ return statements;
264
+ }
265
+ function wrapDatabase(raw, name) {
266
+ const db = {
267
+ sql: raw.sql,
268
+ exec: raw.exec,
269
+ async close() {
270
+ await raw.close();
271
+ if (name) dbRegistry.delete(name);
272
+ if (defaultDatabase === db) defaultDatabase = null;
273
+ await dbHooks.callHook("db:disconnect", db);
274
+ }
275
+ };
276
+ return db;
277
+ }
278
+ function defineDatabase(options = {}) {
279
+ if (!options.connector && !options.connectors) {
280
+ const db = wrapDatabase(createInMemoryDatabase());
281
+ if (!defaultDatabase) defaultDatabase = db;
282
+ dbRegistry.set("default", db);
283
+ dbHooks.callHook("db:connect", db);
284
+ return db;
285
+ }
286
+ const createFn = _global.$db0Create;
287
+ let raw;
288
+ if (options.connector) {
289
+ if (createFn) try {
290
+ raw = createFn(options.connector);
291
+ } catch {
292
+ raw = createInMemoryDatabase();
293
+ }
294
+ else raw = createInMemoryDatabase();
295
+ } else if (options.connectors) {
296
+ const defaultName = options.default || Object.keys(options.connectors)[0];
297
+ const connector = options.connectors[defaultName];
298
+ if (connector && createFn) try {
299
+ raw = createFn(connector);
300
+ } catch {
301
+ raw = createInMemoryDatabase();
302
+ }
303
+ else raw = createInMemoryDatabase();
304
+ } else raw = createInMemoryDatabase();
305
+ const dbName = options.default || "default";
306
+ const db = wrapDatabase(raw, dbName);
307
+ dbRegistry.set(dbName, db);
308
+ if (!defaultDatabase) defaultDatabase = db;
309
+ dbHooks.callHook("db:connect", db);
310
+ return db;
311
+ }
312
+ function useDatabase(name) {
313
+ if (name) {
314
+ const db = dbRegistry.get(name);
315
+ if (!db) throw new Error(`Database '${name}' not found. Call defineDatabase() first.`);
316
+ return db;
317
+ }
318
+ if (!defaultDatabase) defaultDatabase = defineDatabase();
319
+ return defaultDatabase;
320
+ }
321
+ async function closeDatabases() {
322
+ const dbs = Array.from(dbRegistry.values());
323
+ dbRegistry.clear();
324
+ defaultDatabase = null;
325
+ for (const db of dbs) await db.close();
326
+ }
327
+ function getDatabaseHooks() {
328
+ return dbHooks;
329
+ }
330
+ function registerDb0Create(fn) {
331
+ _global.$db0Create = fn;
332
+ }
333
+ async function migrateDatabase(db, migrations) {
334
+ for (const migration of migrations) await db.exec(migration);
335
+ }
336
+ async function runMigrations(db, migrations, options = {}) {
337
+ const tableName = options.table || "_migrations";
338
+ const applied = [];
339
+ await db.exec(`CREATE TABLE IF NOT EXISTS ${tableName} (name TEXT PRIMARY KEY, applied_at INTEGER)`);
340
+ const { rows } = await db.sql`SELECT name FROM ${rawSql(tableName)}`;
341
+ const appliedNames = new Set(rows.map((r) => r.name));
342
+ for (const migration of migrations) {
343
+ if (appliedNames.has(migration.name)) continue;
344
+ await db.exec(migration.up);
345
+ await db.sql`INSERT INTO ${rawSql(tableName)} (name, applied_at) VALUES (${migration.name}, ${Date.now()})`;
346
+ applied.push(migration.name);
347
+ if (options.log) console.log(`[db] Applied migration: ${migration.name}`);
348
+ }
349
+ return { applied };
350
+ }
351
+ //#endregion
352
+ export { rawSql as a, useDatabase as c, migrateDatabase as i, defineDatabase as n, registerDb0Create as o, getDatabaseHooks as r, runMigrations as s, closeDatabases as t };
@@ -0,0 +1,2 @@
1
+ import { a as DatabaseOptions, c as closeDatabases, d as migrateDatabase, f as rawSql, h as useDatabase, i as DatabaseHooks, l as defineDatabase, m as runMigrations, n as DatabaseConnector, o as DrizzleConfig, p as registerDb0Create, r as DatabaseConnectorInstance, s as Migration, t as Database, u as getDatabaseHooks } from "./database-CfpFznl-.js";
2
+ export { Database, DatabaseConnector, DatabaseConnectorInstance, DatabaseHooks, DatabaseOptions, DrizzleConfig, Migration, closeDatabases, defineDatabase, getDatabaseHooks, migrateDatabase, rawSql as raw, rawSql, rawSql as sqlRaw, registerDb0Create, runMigrations, useDatabase };
@@ -0,0 +1,2 @@
1
+ import { a as rawSql, c as useDatabase, i as migrateDatabase, n as defineDatabase, o as registerDb0Create, r as getDatabaseHooks, s as runMigrations, t as closeDatabases } from "./database-DNrY44SQ.js";
2
+ export { closeDatabases, defineDatabase, getDatabaseHooks, migrateDatabase, rawSql as raw, rawSql, rawSql as sqlRaw, registerDb0Create, runMigrations, useDatabase };
@@ -0,0 +1,354 @@
1
+ import { getLogger } from "@ubean/logger";
2
+ //#region src/email.ts
3
+ /**
4
+ * P9-25 邮件发送原语 —— Email sending primitives
5
+ *
6
+ * 内置邮件发送原语,提供 provider 抽象层。支持三种内置 provider:
7
+ * - `log` 开发环境,将邮件内容输出到 console(或自定义 logger)
8
+ * - `smtp` 生产环境,通过 nodemailer(可选 peer dependency)配置 SMTP/sendmail
9
+ * - `mock` 测试环境,捕获发送的邮件以便断言
10
+ *
11
+ * 设计对齐 `storage.ts` 的 driver 抽象与 `queue.ts` 的全局 driver 注册模式:
12
+ * - `defineEmailProvider(options)` 定义并注册一个 provider
13
+ * - `createEmailTransport(provider)` 从 provider 配置创建 transport
14
+ * - `sendEmail(options)` 使用全局默认 transport 发送邮件
15
+ * - `renderEmailTemplate(template, data)` 简单字符串插值渲染
16
+ *
17
+ * 用法:
18
+ * ```typescript
19
+ * import { defineEmailProvider, sendEmail, renderEmailTemplate } from '@ubean/server';
20
+ *
21
+ * // 定义默认 provider(开发环境用 log)
22
+ * defineEmailProvider({ type: 'log', default: true });
23
+ *
24
+ * // 发送邮件
25
+ * await sendEmail({
26
+ * to: 'user@example.com',
27
+ * from: 'noreply@example.com',
28
+ * subject: 'Welcome',
29
+ * html: renderEmailTemplate('<h1>Hello {{name}}</h1>', { name: 'Alice' }),
30
+ * text: 'Hello Alice'
31
+ * });
32
+ * ```
33
+ */
34
+ function generateMessageId(prefix) {
35
+ if (typeof crypto !== "undefined" && crypto.randomUUID) return `${prefix}-${crypto.randomUUID()}`;
36
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
37
+ }
38
+ function joinRecipients(to) {
39
+ return Array.isArray(to) ? to.join(", ") : to;
40
+ }
41
+ /**
42
+ * 从对象中按点号路径读取嵌套值
43
+ */
44
+ function getNestedValue(obj, path) {
45
+ const parts = path.split(".");
46
+ let current = obj;
47
+ for (const part of parts) {
48
+ if (current === null || current === void 0) return void 0;
49
+ current = current[part];
50
+ }
51
+ return current;
52
+ }
53
+ function createLogTransport(config) {
54
+ const logger = config.logger || ((msg) => getLogger("email").info(msg));
55
+ return {
56
+ async send(options) {
57
+ const lines = [
58
+ `[ubean-email] ${options.subject}`,
59
+ ` From: ${options.from}`,
60
+ ` To: ${joinRecipients(options.to)}`
61
+ ];
62
+ if (options.cc) lines.push(` Cc: ${joinRecipients(options.cc)}`);
63
+ if (options.bcc) lines.push(` Bcc: ${joinRecipients(options.bcc)}`);
64
+ if (options.replyTo) lines.push(` Reply-To: ${options.replyTo}`);
65
+ if (options.text) lines.push(` Text: ${options.text}`);
66
+ if (options.html) {
67
+ const preview = options.html.length > 100 ? `${options.html.slice(0, 100)}...` : options.html;
68
+ lines.push(` HTML: ${preview}`);
69
+ }
70
+ if (options.attachments && options.attachments.length > 0) lines.push(` Attachments: ${options.attachments.map((a) => a.filename).join(", ")}`);
71
+ logger(lines.join("\n"));
72
+ return {
73
+ messageId: generateMessageId("log"),
74
+ provider: "log"
75
+ };
76
+ },
77
+ async verify() {
78
+ return true;
79
+ }
80
+ };
81
+ }
82
+ function createMockTransport(_config) {
83
+ const sent = [];
84
+ return {
85
+ sent,
86
+ clear() {
87
+ sent.length = 0;
88
+ },
89
+ async send(options) {
90
+ sent.push(options);
91
+ return {
92
+ messageId: generateMessageId("mock"),
93
+ provider: "mock"
94
+ };
95
+ },
96
+ async verify() {
97
+ return true;
98
+ }
99
+ };
100
+ }
101
+ function createSmtpTransport(config) {
102
+ let nodemailerTransport = null;
103
+ async function getNodemailerTransport() {
104
+ if (nodemailerTransport) return nodemailerTransport;
105
+ let nodemailer;
106
+ try {
107
+ nodemailer = await import("nodemailer");
108
+ } catch {
109
+ throw new Error("[ubean] nodemailer is not installed. Install it with: pnpm add nodemailer");
110
+ }
111
+ nodemailerTransport = nodemailer.createTransport({
112
+ host: config.host,
113
+ port: config.port,
114
+ secure: config.secure,
115
+ auth: config.auth,
116
+ ...config.extra
117
+ });
118
+ return nodemailerTransport;
119
+ }
120
+ return {
121
+ async send(options) {
122
+ const result = await (await getNodemailerTransport()).sendMail({
123
+ from: options.from,
124
+ to: joinRecipients(options.to),
125
+ cc: options.cc ? joinRecipients(options.cc) : void 0,
126
+ bcc: options.bcc ? joinRecipients(options.bcc) : void 0,
127
+ replyTo: options.replyTo,
128
+ subject: options.subject,
129
+ text: options.text,
130
+ html: options.html,
131
+ headers: options.headers,
132
+ attachments: options.attachments?.map((a) => ({
133
+ filename: a.filename,
134
+ content: a.content,
135
+ contentType: a.contentType,
136
+ encoding: a.encoding,
137
+ cid: a.cid
138
+ }))
139
+ });
140
+ return {
141
+ messageId: result.messageId,
142
+ response: result.response,
143
+ provider: "smtp"
144
+ };
145
+ },
146
+ async verify() {
147
+ return (await getNodemailerTransport()).verify();
148
+ },
149
+ async close() {
150
+ if (nodemailerTransport) {
151
+ nodemailerTransport.close();
152
+ nodemailerTransport = null;
153
+ }
154
+ }
155
+ };
156
+ }
157
+ function createSendmailTransport(config) {
158
+ let nodemailerTransport = null;
159
+ async function getNodemailerTransport() {
160
+ if (nodemailerTransport) return nodemailerTransport;
161
+ let nodemailer;
162
+ try {
163
+ nodemailer = await import("nodemailer");
164
+ } catch {
165
+ throw new Error("[ubean] nodemailer is not installed. Install it with: pnpm add nodemailer");
166
+ }
167
+ nodemailerTransport = nodemailer.createTransport({
168
+ sendmail: true,
169
+ path: config.path || "/usr/sbin/sendmail",
170
+ args: config.args || ["-i"]
171
+ });
172
+ return nodemailerTransport;
173
+ }
174
+ return {
175
+ async send(options) {
176
+ const result = await (await getNodemailerTransport()).sendMail({
177
+ from: options.from,
178
+ to: joinRecipients(options.to),
179
+ cc: options.cc ? joinRecipients(options.cc) : void 0,
180
+ bcc: options.bcc ? joinRecipients(options.bcc) : void 0,
181
+ replyTo: options.replyTo,
182
+ subject: options.subject,
183
+ text: options.text,
184
+ html: options.html,
185
+ headers: options.headers,
186
+ attachments: options.attachments?.map((a) => ({
187
+ filename: a.filename,
188
+ content: a.content,
189
+ contentType: a.contentType,
190
+ encoding: a.encoding,
191
+ cid: a.cid
192
+ }))
193
+ });
194
+ return {
195
+ messageId: result.messageId,
196
+ response: result.response,
197
+ provider: "sendmail"
198
+ };
199
+ },
200
+ async close() {
201
+ if (nodemailerTransport) {
202
+ nodemailerTransport.close();
203
+ nodemailerTransport = null;
204
+ }
205
+ }
206
+ };
207
+ }
208
+ const providerRegistry = /* @__PURE__ */ new Map();
209
+ let globalProvider = null;
210
+ let globalTransport = null;
211
+ /**
212
+ * 定义并注册一个邮件 provider
213
+ *
214
+ * @example
215
+ * ```typescript
216
+ * // 开发环境
217
+ * defineEmailProvider({ type: 'log', default: true });
218
+ *
219
+ * // 生产环境
220
+ * defineEmailProvider({
221
+ * type: 'smtp',
222
+ * name: 'primary',
223
+ * default: true,
224
+ * host: 'smtp.example.com',
225
+ * port: 587,
226
+ * auth: { user: 'postmaster', pass: 'secret' }
227
+ * });
228
+ * ```
229
+ */
230
+ function defineEmailProvider(options) {
231
+ const { name, default: isDefault, ...config } = options;
232
+ const provider = {
233
+ type: config.type,
234
+ name,
235
+ config
236
+ };
237
+ if (name) providerRegistry.set(name, provider);
238
+ if (isDefault) setDefaultEmailProvider(provider);
239
+ return provider;
240
+ }
241
+ /**
242
+ * 从 provider 配置创建 transport
243
+ */
244
+ function createEmailTransport(provider) {
245
+ switch (provider.config.type) {
246
+ case "log": return createLogTransport(provider.config);
247
+ case "mock": return createMockTransport(provider.config);
248
+ case "smtp": return createSmtpTransport(provider.config);
249
+ case "sendmail": return createSendmailTransport(provider.config);
250
+ default: {
251
+ const exhaustive = provider.config;
252
+ throw new Error(`[ubean] Unknown email provider type: ${exhaustive.type}`);
253
+ }
254
+ }
255
+ }
256
+ /**
257
+ * 设置全局默认 provider(并立即创建其 transport)
258
+ */
259
+ function setDefaultEmailProvider(provider) {
260
+ globalProvider = provider;
261
+ globalTransport = createEmailTransport(provider);
262
+ }
263
+ /**
264
+ * 获取全局默认 provider。若尚未设置,则惰性创建一个 `log` provider。
265
+ */
266
+ function useEmailProvider() {
267
+ if (!globalProvider) {
268
+ globalProvider = defineEmailProvider({ type: "log" });
269
+ globalTransport = createEmailTransport(globalProvider);
270
+ }
271
+ return globalProvider;
272
+ }
273
+ /**
274
+ * 获取全局默认 transport。若尚未设置,则惰性创建一个 `log` transport。
275
+ */
276
+ function getEmailTransport() {
277
+ if (!globalTransport) useEmailProvider();
278
+ return globalTransport;
279
+ }
280
+ /**
281
+ * 按名称获取已注册的 provider
282
+ */
283
+ function getEmailProvider(name) {
284
+ return providerRegistry.get(name);
285
+ }
286
+ /**
287
+ * 获取所有已注册的 provider 名称
288
+ */
289
+ function getEmailProviderNames() {
290
+ return Array.from(providerRegistry.keys());
291
+ }
292
+ /**
293
+ * 使用全局默认 transport 发送邮件
294
+ *
295
+ * @example
296
+ * ```typescript
297
+ * await sendEmail({
298
+ * to: 'user@example.com',
299
+ * from: 'noreply@example.com',
300
+ * subject: 'Welcome',
301
+ * html: '<h1>Welcome!</h1>',
302
+ * text: 'Welcome!'
303
+ * });
304
+ * ```
305
+ */
306
+ async function sendEmail(options) {
307
+ if (!options.to) throw new Error("[ubean] sendEmail: \"to\" is required");
308
+ if (!options.from) throw new Error("[ubean] sendEmail: \"from\" is required");
309
+ if (!options.subject) throw new Error("[ubean] sendEmail: \"subject\" is required");
310
+ return getEmailTransport().send(options);
311
+ }
312
+ /**
313
+ * 简单模板渲染 —— `{{key}}` 字符串插值,支持点号嵌套路径
314
+ *
315
+ * @example
316
+ * ```typescript
317
+ * renderEmailTemplate('Hello {{user.name}}, your code is {{code}}', {
318
+ * user: { name: 'Alice' },
319
+ * code: '123456'
320
+ * });
321
+ * // => 'Hello Alice, your code is 123456'
322
+ * ```
323
+ */
324
+ function renderEmailTemplate(template, data) {
325
+ return template.replace(/\{\{\s*([^}]+?)\s*\}\}/g, (_match, key) => {
326
+ const value = getNestedValue(data, key.trim());
327
+ return value !== void 0 && value !== null ? String(value) : "";
328
+ });
329
+ }
330
+ /**
331
+ * 获取全局 mock transport 捕获的已发送邮件。
332
+ * 若全局 transport 不是 mock,则返回空数组。
333
+ */
334
+ function getSentEmails() {
335
+ if (globalTransport && "sent" in globalTransport) return globalTransport.sent;
336
+ return [];
337
+ }
338
+ /**
339
+ * 清空全局 mock transport 捕获的已发送邮件。
340
+ * 若全局 transport 不是 mock,则不做任何操作。
341
+ */
342
+ function clearSentEmails() {
343
+ if (globalTransport && "sent" in globalTransport) globalTransport.clear();
344
+ }
345
+ /**
346
+ * 清除所有 provider 注册表与全局状态(主要用于测试隔离)
347
+ */
348
+ function clearEmailProviders() {
349
+ providerRegistry.clear();
350
+ globalProvider = null;
351
+ globalTransport = null;
352
+ }
353
+ //#endregion
354
+ export { getEmailProvider as a, getSentEmails as c, setDefaultEmailProvider as d, useEmailProvider as f, defineEmailProvider as i, renderEmailTemplate as l, clearSentEmails as n, getEmailProviderNames as o, createEmailTransport as r, getEmailTransport as s, clearEmailProviders as t, sendEmail as u };