alepha 0.14.4 → 0.15.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/README.md +1 -4
- package/dist/api/audits/index.d.ts +619 -731
- package/dist/api/audits/index.d.ts.map +1 -1
- package/dist/api/files/index.d.ts +185 -298
- package/dist/api/files/index.d.ts.map +1 -1
- package/dist/api/files/index.js +0 -1
- package/dist/api/files/index.js.map +1 -1
- package/dist/api/jobs/index.d.ts +245 -356
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/notifications/index.d.ts +238 -350
- package/dist/api/notifications/index.d.ts.map +1 -1
- package/dist/api/parameters/index.d.ts +499 -611
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/users/index.browser.js +1 -2
- package/dist/api/users/index.browser.js.map +1 -1
- package/dist/api/users/index.d.ts +1697 -1804
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +178 -151
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/verifications/index.d.ts +132 -132
- package/dist/api/verifications/index.d.ts.map +1 -1
- package/dist/batch/index.d.ts +122 -122
- package/dist/batch/index.d.ts.map +1 -1
- package/dist/batch/index.js +1 -2
- package/dist/batch/index.js.map +1 -1
- package/dist/bucket/index.d.ts +163 -163
- package/dist/bucket/index.d.ts.map +1 -1
- package/dist/cache/core/index.d.ts +46 -46
- package/dist/cache/core/index.d.ts.map +1 -1
- package/dist/cache/redis/index.d.ts.map +1 -1
- package/dist/cli/index.d.ts +302 -299
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +966 -564
- package/dist/cli/index.js.map +1 -1
- package/dist/command/index.d.ts +303 -299
- package/dist/command/index.d.ts.map +1 -1
- package/dist/command/index.js +11 -7
- package/dist/command/index.js.map +1 -1
- package/dist/core/index.browser.js +419 -99
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +718 -625
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +420 -99
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +419 -99
- package/dist/core/index.native.js.map +1 -1
- package/dist/datetime/index.d.ts +44 -44
- package/dist/datetime/index.d.ts.map +1 -1
- package/dist/datetime/index.js +4 -4
- package/dist/datetime/index.js.map +1 -1
- package/dist/email/index.d.ts +97 -50
- package/dist/email/index.d.ts.map +1 -1
- package/dist/email/index.js +129 -33
- package/dist/email/index.js.map +1 -1
- package/dist/fake/index.d.ts +7981 -14
- package/dist/fake/index.d.ts.map +1 -1
- package/dist/file/index.d.ts +523 -390
- package/dist/file/index.d.ts.map +1 -1
- package/dist/file/index.js +253 -1
- package/dist/file/index.js.map +1 -1
- package/dist/lock/core/index.d.ts +208 -208
- package/dist/lock/core/index.d.ts.map +1 -1
- package/dist/lock/redis/index.d.ts.map +1 -1
- package/dist/logger/index.d.ts +25 -26
- package/dist/logger/index.d.ts.map +1 -1
- package/dist/mcp/index.d.ts +197 -197
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/orm/chunk-DtkW-qnP.js +38 -0
- package/dist/orm/index.browser.js.map +1 -1
- package/dist/orm/index.bun.js +2814 -0
- package/dist/orm/index.bun.js.map +1 -0
- package/dist/orm/index.d.ts +1205 -1057
- package/dist/orm/index.d.ts.map +1 -1
- package/dist/orm/index.js +2056 -1753
- package/dist/orm/index.js.map +1 -1
- package/dist/queue/core/index.d.ts +248 -248
- package/dist/queue/core/index.d.ts.map +1 -1
- package/dist/queue/redis/index.d.ts.map +1 -1
- package/dist/redis/index.bun.js +285 -0
- package/dist/redis/index.bun.js.map +1 -0
- package/dist/redis/index.d.ts +118 -136
- package/dist/redis/index.d.ts.map +1 -1
- package/dist/redis/index.js +18 -38
- package/dist/redis/index.js.map +1 -1
- package/dist/retry/index.d.ts +69 -69
- package/dist/retry/index.d.ts.map +1 -1
- package/dist/router/index.d.ts +6 -6
- package/dist/router/index.d.ts.map +1 -1
- package/dist/scheduler/index.d.ts +25 -25
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/security/index.browser.js +5 -1
- package/dist/security/index.browser.js.map +1 -1
- package/dist/security/index.d.ts +417 -254
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +386 -86
- package/dist/security/index.js.map +1 -1
- package/dist/server/auth/index.d.ts +277 -277
- package/dist/server/auth/index.d.ts.map +1 -1
- package/dist/server/auth/index.js +20 -20
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/cache/index.d.ts +60 -57
- package/dist/server/cache/index.d.ts.map +1 -1
- package/dist/server/cache/index.js +1 -1
- package/dist/server/cache/index.js.map +1 -1
- package/dist/server/compress/index.d.ts +3 -3
- package/dist/server/compress/index.d.ts.map +1 -1
- package/dist/server/cookies/index.d.ts +6 -6
- package/dist/server/cookies/index.d.ts.map +1 -1
- package/dist/server/cookies/index.js +3 -3
- package/dist/server/cookies/index.js.map +1 -1
- package/dist/server/core/index.d.ts +242 -150
- package/dist/server/core/index.d.ts.map +1 -1
- package/dist/server/core/index.js +288 -122
- package/dist/server/core/index.js.map +1 -1
- package/dist/server/cors/index.d.ts +11 -12
- package/dist/server/cors/index.d.ts.map +1 -1
- package/dist/server/health/index.d.ts +0 -1
- package/dist/server/health/index.d.ts.map +1 -1
- package/dist/server/helmet/index.d.ts +2 -2
- package/dist/server/helmet/index.d.ts.map +1 -1
- package/dist/server/links/index.browser.js.map +1 -1
- package/dist/server/links/index.d.ts +84 -85
- package/dist/server/links/index.d.ts.map +1 -1
- package/dist/server/links/index.js +1 -2
- package/dist/server/links/index.js.map +1 -1
- package/dist/server/metrics/index.d.ts.map +1 -1
- package/dist/server/multipart/index.d.ts +6 -6
- package/dist/server/multipart/index.d.ts.map +1 -1
- package/dist/server/proxy/index.d.ts +102 -103
- package/dist/server/proxy/index.d.ts.map +1 -1
- package/dist/server/rate-limit/index.d.ts +16 -16
- package/dist/server/rate-limit/index.d.ts.map +1 -1
- package/dist/server/static/index.d.ts +44 -44
- package/dist/server/static/index.d.ts.map +1 -1
- package/dist/server/swagger/index.d.ts +48 -49
- package/dist/server/swagger/index.d.ts.map +1 -1
- package/dist/server/swagger/index.js +1 -2
- package/dist/server/swagger/index.js.map +1 -1
- package/dist/sms/index.d.ts +13 -11
- package/dist/sms/index.d.ts.map +1 -1
- package/dist/sms/index.js +7 -7
- package/dist/sms/index.js.map +1 -1
- package/dist/thread/index.d.ts +71 -72
- package/dist/thread/index.d.ts.map +1 -1
- package/dist/topic/core/index.d.ts +318 -318
- package/dist/topic/core/index.d.ts.map +1 -1
- package/dist/topic/redis/index.d.ts +6 -6
- package/dist/topic/redis/index.d.ts.map +1 -1
- package/dist/vite/index.d.ts +5720 -159
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/index.js +41 -18
- package/dist/vite/index.js.map +1 -1
- package/dist/websocket/index.browser.js +6 -6
- package/dist/websocket/index.browser.js.map +1 -1
- package/dist/websocket/index.d.ts +247 -247
- package/dist/websocket/index.d.ts.map +1 -1
- package/dist/websocket/index.js +6 -6
- package/dist/websocket/index.js.map +1 -1
- package/package.json +9 -14
- package/src/api/files/controllers/AdminFileStatsController.ts +0 -1
- package/src/api/users/atoms/realmAuthSettingsAtom.ts +5 -0
- package/src/api/users/controllers/{UserRealmController.ts → RealmController.ts} +11 -11
- package/src/api/users/entities/users.ts +1 -1
- package/src/api/users/index.ts +8 -8
- package/src/api/users/primitives/{$userRealm.ts → $realm.ts} +17 -19
- package/src/api/users/providers/{UserRealmProvider.ts → RealmProvider.ts} +26 -30
- package/src/api/users/schemas/{userRealmConfigSchema.ts → realmConfigSchema.ts} +2 -2
- package/src/api/users/services/CredentialService.ts +7 -7
- package/src/api/users/services/IdentityService.ts +4 -4
- package/src/api/users/services/RegistrationService.spec.ts +25 -27
- package/src/api/users/services/RegistrationService.ts +38 -27
- package/src/api/users/services/SessionCrudService.ts +3 -3
- package/src/api/users/services/SessionService.spec.ts +3 -3
- package/src/api/users/services/SessionService.ts +28 -9
- package/src/api/users/services/UserService.ts +7 -7
- package/src/batch/providers/BatchProvider.ts +1 -2
- package/src/cli/apps/AlephaPackageBuilderCli.ts +38 -19
- package/src/cli/assets/apiHelloControllerTs.ts +18 -0
- package/src/cli/assets/apiIndexTs.ts +16 -0
- package/src/cli/assets/claudeMd.ts +303 -0
- package/src/cli/assets/mainBrowserTs.ts +2 -2
- package/src/cli/assets/mainServerTs.ts +24 -0
- package/src/cli/assets/webAppRouterTs.ts +15 -0
- package/src/cli/assets/webHelloComponentTsx.ts +16 -0
- package/src/cli/assets/webIndexTs.ts +16 -0
- package/src/cli/commands/build.ts +41 -21
- package/src/cli/commands/db.ts +21 -18
- package/src/cli/commands/deploy.ts +17 -5
- package/src/cli/commands/dev.ts +13 -17
- package/src/cli/commands/format.ts +8 -2
- package/src/cli/commands/init.ts +74 -29
- package/src/cli/commands/lint.ts +8 -2
- package/src/cli/commands/test.ts +8 -2
- package/src/cli/commands/typecheck.ts +5 -1
- package/src/cli/commands/verify.ts +4 -2
- package/src/cli/services/AlephaCliUtils.ts +39 -600
- package/src/cli/services/PackageManagerUtils.ts +301 -0
- package/src/cli/services/ProjectScaffolder.ts +306 -0
- package/src/command/helpers/Runner.ts +15 -3
- package/src/core/__tests__/Alepha-graph.spec.ts +4 -0
- package/src/core/index.shared.ts +1 -0
- package/src/core/index.ts +2 -0
- package/src/core/primitives/$hook.ts +6 -2
- package/src/core/primitives/$module.spec.ts +4 -0
- package/src/core/providers/AlsProvider.ts +1 -1
- package/src/core/providers/CodecManager.spec.ts +12 -6
- package/src/core/providers/CodecManager.ts +26 -6
- package/src/core/providers/EventManager.ts +169 -13
- package/src/core/providers/KeylessJsonSchemaCodec.spec.ts +621 -0
- package/src/core/providers/KeylessJsonSchemaCodec.ts +407 -0
- package/src/core/providers/StateManager.spec.ts +27 -16
- package/src/email/providers/LocalEmailProvider.spec.ts +111 -87
- package/src/email/providers/LocalEmailProvider.ts +52 -15
- package/src/email/providers/NodemailerEmailProvider.ts +167 -56
- package/src/file/errors/FileError.ts +7 -0
- package/src/file/index.ts +9 -1
- package/src/file/providers/MemoryFileSystemProvider.ts +393 -0
- package/src/orm/index.browser.ts +1 -19
- package/src/orm/index.bun.ts +77 -0
- package/src/orm/index.shared-server.ts +22 -0
- package/src/orm/index.shared.ts +15 -0
- package/src/orm/index.ts +19 -39
- package/src/orm/providers/drivers/BunPostgresProvider.ts +3 -5
- package/src/orm/providers/drivers/BunSqliteProvider.ts +1 -1
- package/src/orm/providers/drivers/CloudflareD1Provider.ts +4 -0
- package/src/orm/providers/drivers/DatabaseProvider.ts +4 -0
- package/src/orm/providers/drivers/PglitePostgresProvider.ts +4 -0
- package/src/orm/services/Repository.ts +8 -0
- package/src/redis/index.bun.ts +35 -0
- package/src/redis/providers/BunRedisProvider.ts +12 -43
- package/src/redis/providers/BunRedisSubscriberProvider.ts +2 -3
- package/src/redis/providers/NodeRedisProvider.ts +16 -34
- package/src/{server/security → security}/__tests__/BasicAuth.spec.ts +11 -11
- package/src/{server/security → security}/__tests__/ServerSecurityProvider-realm.spec.ts +21 -16
- package/src/{server/security/providers → security/__tests__}/ServerSecurityProvider.spec.ts +5 -5
- package/src/security/index.browser.ts +5 -0
- package/src/security/index.ts +90 -7
- package/src/security/primitives/{$realm.spec.ts → $issuer.spec.ts} +11 -11
- package/src/security/primitives/{$realm.ts → $issuer.ts} +20 -17
- package/src/security/primitives/$role.ts +5 -5
- package/src/security/primitives/$serviceAccount.spec.ts +5 -5
- package/src/security/primitives/$serviceAccount.ts +3 -3
- package/src/{server/security → security}/providers/ServerSecurityProvider.ts +5 -7
- package/src/server/auth/primitives/$auth.ts +10 -10
- package/src/server/auth/primitives/$authCredentials.ts +3 -3
- package/src/server/auth/primitives/$authGithub.ts +3 -3
- package/src/server/auth/primitives/$authGoogle.ts +3 -3
- package/src/server/auth/providers/ServerAuthProvider.ts +13 -13
- package/src/server/cache/providers/ServerCacheProvider.ts +1 -1
- package/src/server/cookies/providers/ServerCookiesProvider.ts +3 -3
- package/src/server/core/providers/NodeHttpServerProvider.ts +25 -6
- package/src/server/core/providers/ServerBodyParserProvider.ts +19 -23
- package/src/server/core/providers/ServerLoggerProvider.ts +23 -19
- package/src/server/core/providers/ServerProvider.ts +144 -21
- package/src/server/core/providers/ServerRouterProvider.ts +259 -115
- package/src/server/core/providers/ServerTimingProvider.ts +2 -2
- package/src/server/links/index.ts +1 -1
- package/src/server/links/providers/LinkProvider.ts +1 -1
- package/src/server/swagger/index.ts +1 -1
- package/src/sms/providers/LocalSmsProvider.spec.ts +153 -111
- package/src/sms/providers/LocalSmsProvider.ts +8 -7
- package/src/vite/helpers/boot.ts +28 -17
- package/src/vite/tasks/buildServer.ts +12 -1
- package/src/vite/tasks/devServer.ts +3 -1
- package/src/vite/tasks/generateCloudflare.ts +7 -0
- package/dist/server/security/index.browser.js +0 -13
- package/dist/server/security/index.browser.js.map +0 -1
- package/dist/server/security/index.d.ts +0 -173
- package/dist/server/security/index.d.ts.map +0 -1
- package/dist/server/security/index.js +0 -311
- package/dist/server/security/index.js.map +0 -1
- package/src/cli/assets/appRouterTs.ts +0 -9
- package/src/cli/assets/mainTs.ts +0 -13
- package/src/server/security/index.browser.ts +0 -10
- package/src/server/security/index.ts +0 -94
- /package/src/{server/security → security}/primitives/$basicAuth.ts +0 -0
- /package/src/{server/security → security}/providers/ServerBasicAuthProvider.ts +0 -0
|
@@ -1,42 +1,27 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import { Alepha } from "alepha";
|
|
2
|
+
import { FileSystemProvider, MemoryFileSystemProvider } from "alepha/file";
|
|
3
3
|
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
4
4
|
import { EmailError } from "../errors/EmailError.ts";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
LocalEmailProvider,
|
|
7
|
+
localEmailOptions,
|
|
8
|
+
} from "../providers/LocalEmailProvider.ts";
|
|
6
9
|
|
|
7
|
-
//
|
|
8
|
-
vi.mock("node:fs/promises");
|
|
9
|
-
vi.mock("node:path");
|
|
10
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
vi.mock("alepha/logger", () => ({
|
|
13
|
-
$logger: () => ({
|
|
14
|
-
debug: vi.fn(),
|
|
15
|
-
info: vi.fn(),
|
|
16
|
-
error: vi.fn(),
|
|
17
|
-
}),
|
|
18
|
-
}));
|
|
19
|
-
|
|
20
|
-
const mockedFs = vi.mocked(fs);
|
|
21
|
-
const mockedPath = vi.mocked(path);
|
|
12
|
+
const DEFAULT_DIRECTORY = localEmailOptions.options.default.directory;
|
|
22
13
|
|
|
23
14
|
describe("LocalEmailProvider", () => {
|
|
24
|
-
let provider: LocalEmailProvider;
|
|
25
|
-
|
|
26
|
-
beforeEach(() => {
|
|
27
|
-
vi.clearAllMocks();
|
|
28
|
-
// Setup default path.join mock
|
|
29
|
-
mockedPath.join.mockImplementation((...args) => args.join("/"));
|
|
30
|
-
});
|
|
31
|
-
|
|
32
15
|
describe("send", () => {
|
|
33
|
-
beforeEach(() => {
|
|
34
|
-
provider = new LocalEmailProvider({ directory: "test-emails" });
|
|
35
|
-
});
|
|
36
|
-
|
|
37
16
|
test("should successfully send email to local file", async () => {
|
|
38
|
-
|
|
39
|
-
|
|
17
|
+
const alepha = Alepha.create().with({
|
|
18
|
+
provide: FileSystemProvider,
|
|
19
|
+
use: MemoryFileSystemProvider,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const provider = alepha.inject(LocalEmailProvider);
|
|
23
|
+
const memoryFs = alepha.inject(MemoryFileSystemProvider);
|
|
24
|
+
await alepha.start();
|
|
40
25
|
|
|
41
26
|
const to = "test@example.com";
|
|
42
27
|
const subject = "Test Subject";
|
|
@@ -48,19 +33,20 @@ describe("LocalEmailProvider", () => {
|
|
|
48
33
|
body,
|
|
49
34
|
});
|
|
50
35
|
|
|
51
|
-
expect(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
expect(mockedFs.writeFile).toHaveBeenCalledWith(
|
|
55
|
-
expect.stringContaining("test@example.com"),
|
|
56
|
-
expect.stringContaining(subject),
|
|
57
|
-
"utf8",
|
|
58
|
-
);
|
|
36
|
+
expect(memoryFs.writeFileCalls).toHaveLength(1);
|
|
37
|
+
expect(memoryFs.writeFileCalls[0].path).toContain("test@example.com");
|
|
38
|
+
expect(memoryFs.writeFileCalls[0].data).toContain(subject);
|
|
59
39
|
});
|
|
60
40
|
|
|
61
41
|
test("should create proper filename with sanitized email and timestamp", async () => {
|
|
62
|
-
|
|
63
|
-
|
|
42
|
+
const alepha = Alepha.create().with({
|
|
43
|
+
provide: FileSystemProvider,
|
|
44
|
+
use: MemoryFileSystemProvider,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const provider = alepha.inject(LocalEmailProvider);
|
|
48
|
+
const memoryFs = alepha.inject(MemoryFileSystemProvider);
|
|
49
|
+
await alepha.start();
|
|
64
50
|
|
|
65
51
|
const to = "user+test@example.com";
|
|
66
52
|
const subject = "Test Subject";
|
|
@@ -76,17 +62,24 @@ describe("LocalEmailProvider", () => {
|
|
|
76
62
|
body,
|
|
77
63
|
});
|
|
78
64
|
|
|
79
|
-
expect(
|
|
80
|
-
|
|
65
|
+
expect(memoryFs.joinCalls).toHaveLength(1);
|
|
66
|
+
expect(memoryFs.joinCalls[0]).toEqual([
|
|
67
|
+
DEFAULT_DIRECTORY,
|
|
81
68
|
"user_test@example.com+2023-01-01T12-00-00-000Z.html",
|
|
82
|
-
);
|
|
69
|
+
]);
|
|
83
70
|
|
|
84
71
|
vi.useRealTimers();
|
|
85
72
|
});
|
|
86
73
|
|
|
87
74
|
test("should sanitize special characters in email address", async () => {
|
|
88
|
-
|
|
89
|
-
|
|
75
|
+
const alepha = Alepha.create().with({
|
|
76
|
+
provide: FileSystemProvider,
|
|
77
|
+
use: MemoryFileSystemProvider,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const provider = alepha.inject(LocalEmailProvider);
|
|
81
|
+
const memoryFs = alepha.inject(MemoryFileSystemProvider);
|
|
82
|
+
await alepha.start();
|
|
90
83
|
|
|
91
84
|
const to = "user<script>@example.com";
|
|
92
85
|
const subject = "Test Subject";
|
|
@@ -98,15 +91,21 @@ describe("LocalEmailProvider", () => {
|
|
|
98
91
|
body,
|
|
99
92
|
});
|
|
100
93
|
|
|
101
|
-
expect(
|
|
102
|
-
|
|
103
|
-
|
|
94
|
+
expect(memoryFs.joinCalls).toHaveLength(1);
|
|
95
|
+
expect(memoryFs.joinCalls[0][1]).toMatch(
|
|
96
|
+
/user_script_@example\.com\+.+\.html/,
|
|
104
97
|
);
|
|
105
98
|
});
|
|
106
99
|
|
|
107
100
|
test("should create proper HTML content", async () => {
|
|
108
|
-
|
|
109
|
-
|
|
101
|
+
const alepha = Alepha.create().with({
|
|
102
|
+
provide: FileSystemProvider,
|
|
103
|
+
use: MemoryFileSystemProvider,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const provider = alepha.inject(LocalEmailProvider);
|
|
107
|
+
const memoryFs = alepha.inject(MemoryFileSystemProvider);
|
|
108
|
+
await alepha.start();
|
|
110
109
|
|
|
111
110
|
const to = "test@example.com";
|
|
112
111
|
const subject = "Test <Subject>";
|
|
@@ -118,8 +117,7 @@ describe("LocalEmailProvider", () => {
|
|
|
118
117
|
body,
|
|
119
118
|
});
|
|
120
119
|
|
|
121
|
-
const
|
|
122
|
-
const htmlContent = writeCall[1] as string;
|
|
120
|
+
const htmlContent = memoryFs.writeFileCalls[0].data;
|
|
123
121
|
|
|
124
122
|
expect(htmlContent).toContain("<!DOCTYPE html>");
|
|
125
123
|
expect(htmlContent).toContain("Test <Subject>"); // escaped subject
|
|
@@ -130,36 +128,17 @@ describe("LocalEmailProvider", () => {
|
|
|
130
128
|
expect(htmlContent).toContain("Sent:");
|
|
131
129
|
});
|
|
132
130
|
|
|
133
|
-
test("should throw EmailError when
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const subject = "Test Subject";
|
|
139
|
-
const body = "<p>Test body</p>";
|
|
131
|
+
test("should throw EmailError when writeFile fails", async () => {
|
|
132
|
+
const alepha = Alepha.create().with({
|
|
133
|
+
provide: FileSystemProvider,
|
|
134
|
+
use: MemoryFileSystemProvider,
|
|
135
|
+
});
|
|
140
136
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
subject,
|
|
145
|
-
body,
|
|
146
|
-
}),
|
|
147
|
-
).rejects.toThrow(EmailError);
|
|
148
|
-
await expect(
|
|
149
|
-
provider.send({
|
|
150
|
-
to,
|
|
151
|
-
subject,
|
|
152
|
-
body,
|
|
153
|
-
}),
|
|
154
|
-
).rejects.toThrow(
|
|
155
|
-
"Failed to save email to local file: Permission denied",
|
|
156
|
-
);
|
|
157
|
-
});
|
|
137
|
+
const provider = alepha.inject(LocalEmailProvider);
|
|
138
|
+
const memoryFs = alepha.inject(MemoryFileSystemProvider);
|
|
139
|
+
await alepha.start();
|
|
158
140
|
|
|
159
|
-
|
|
160
|
-
mockedFs.mkdir.mockResolvedValue(undefined);
|
|
161
|
-
const writeError = new Error("Disk full");
|
|
162
|
-
mockedFs.writeFile.mockRejectedValue(writeError);
|
|
141
|
+
memoryFs.writeFileError = new Error("Disk full");
|
|
163
142
|
|
|
164
143
|
const to = "test@example.com";
|
|
165
144
|
const subject = "Test Subject";
|
|
@@ -172,6 +151,7 @@ describe("LocalEmailProvider", () => {
|
|
|
172
151
|
body,
|
|
173
152
|
}),
|
|
174
153
|
).rejects.toThrow(EmailError);
|
|
154
|
+
|
|
175
155
|
await expect(
|
|
176
156
|
provider.send({
|
|
177
157
|
to,
|
|
@@ -182,8 +162,16 @@ describe("LocalEmailProvider", () => {
|
|
|
182
162
|
});
|
|
183
163
|
|
|
184
164
|
test("should handle non-Error exceptions", async () => {
|
|
185
|
-
|
|
186
|
-
|
|
165
|
+
const alepha = Alepha.create().with({
|
|
166
|
+
provide: FileSystemProvider,
|
|
167
|
+
use: MemoryFileSystemProvider,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const provider = alepha.inject(LocalEmailProvider);
|
|
171
|
+
const memoryFs = alepha.inject(MemoryFileSystemProvider);
|
|
172
|
+
await alepha.start();
|
|
173
|
+
|
|
174
|
+
memoryFs.writeFileError = "String error" as unknown as Error;
|
|
187
175
|
|
|
188
176
|
const to = "test@example.com";
|
|
189
177
|
const subject = "Test Subject";
|
|
@@ -196,6 +184,7 @@ describe("LocalEmailProvider", () => {
|
|
|
196
184
|
body,
|
|
197
185
|
}),
|
|
198
186
|
).rejects.toThrow(EmailError);
|
|
187
|
+
|
|
199
188
|
await expect(
|
|
200
189
|
provider.send({
|
|
201
190
|
to,
|
|
@@ -204,11 +193,39 @@ describe("LocalEmailProvider", () => {
|
|
|
204
193
|
}),
|
|
205
194
|
).rejects.toThrow("Failed to save email to local file: String error");
|
|
206
195
|
});
|
|
196
|
+
|
|
197
|
+
test("should handle multiple recipients", async () => {
|
|
198
|
+
const alepha = Alepha.create().with({
|
|
199
|
+
provide: FileSystemProvider,
|
|
200
|
+
use: MemoryFileSystemProvider,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const provider = alepha.inject(LocalEmailProvider);
|
|
204
|
+
const memoryFs = alepha.inject(MemoryFileSystemProvider);
|
|
205
|
+
await alepha.start();
|
|
206
|
+
|
|
207
|
+
await provider.send({
|
|
208
|
+
to: ["user1@example.com", "user2@example.com"],
|
|
209
|
+
subject: "Broadcast",
|
|
210
|
+
body: "<p>Hello all</p>",
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
expect(memoryFs.writeFileCalls).toHaveLength(2);
|
|
214
|
+
expect(memoryFs.writeFileCalls[0].path).toContain("user1@example.com");
|
|
215
|
+
expect(memoryFs.writeFileCalls[1].path).toContain("user2@example.com");
|
|
216
|
+
});
|
|
207
217
|
});
|
|
208
218
|
|
|
209
219
|
describe("createEmailHtml", () => {
|
|
210
|
-
|
|
211
|
-
|
|
220
|
+
let provider: LocalEmailProvider;
|
|
221
|
+
|
|
222
|
+
beforeEach(async () => {
|
|
223
|
+
const alepha = Alepha.create().with({
|
|
224
|
+
provide: FileSystemProvider,
|
|
225
|
+
use: MemoryFileSystemProvider,
|
|
226
|
+
});
|
|
227
|
+
provider = alepha.inject(LocalEmailProvider);
|
|
228
|
+
await alepha.start();
|
|
212
229
|
});
|
|
213
230
|
|
|
214
231
|
test("should create proper HTML structure", () => {
|
|
@@ -290,8 +307,15 @@ describe("LocalEmailProvider", () => {
|
|
|
290
307
|
});
|
|
291
308
|
|
|
292
309
|
describe("escapeHtml", () => {
|
|
293
|
-
|
|
294
|
-
|
|
310
|
+
let provider: LocalEmailProvider;
|
|
311
|
+
|
|
312
|
+
beforeEach(async () => {
|
|
313
|
+
const alepha = Alepha.create().with({
|
|
314
|
+
provide: FileSystemProvider,
|
|
315
|
+
use: MemoryFileSystemProvider,
|
|
316
|
+
});
|
|
317
|
+
provider = alepha.inject(LocalEmailProvider);
|
|
318
|
+
await alepha.start();
|
|
295
319
|
});
|
|
296
320
|
|
|
297
321
|
test("should escape ampersands", () => {
|
|
@@ -1,24 +1,64 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import { $atom, $hook, $inject, $use, type Static, t } from "alepha";
|
|
2
|
+
import { FileSystemProvider } from "alepha/file";
|
|
3
3
|
import { $logger } from "alepha/logger";
|
|
4
4
|
import { EmailError } from "../errors/EmailError.ts";
|
|
5
5
|
import type { EmailProvider, EmailSendOptions } from "./EmailProvider.ts";
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Local email provider configuration atom
|
|
11
|
+
*/
|
|
12
|
+
export const localEmailOptions = $atom({
|
|
13
|
+
name: "alepha.email.local.options",
|
|
14
|
+
schema: t.object({
|
|
15
|
+
directory: t.string({
|
|
16
|
+
description: "Directory path where email files will be stored",
|
|
17
|
+
}),
|
|
18
|
+
}),
|
|
19
|
+
default: {
|
|
20
|
+
directory: "node_modules/.alepha/emails",
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export type LocalEmailProviderOptions = Static<typeof localEmailOptions.schema>;
|
|
25
|
+
|
|
26
|
+
declare module "alepha" {
|
|
27
|
+
interface State {
|
|
28
|
+
[localEmailOptions.key]: LocalEmailProviderOptions;
|
|
29
|
+
}
|
|
12
30
|
}
|
|
13
31
|
|
|
32
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
33
|
+
|
|
14
34
|
export class LocalEmailProvider implements EmailProvider {
|
|
15
35
|
protected readonly log = $logger();
|
|
16
|
-
protected readonly
|
|
36
|
+
protected readonly fs = $inject(FileSystemProvider);
|
|
37
|
+
protected readonly options = $use(localEmailOptions);
|
|
17
38
|
|
|
18
|
-
|
|
19
|
-
this.
|
|
39
|
+
protected get directory(): string {
|
|
40
|
+
return this.options.directory;
|
|
20
41
|
}
|
|
21
42
|
|
|
43
|
+
protected onStart = $hook({
|
|
44
|
+
on: "start",
|
|
45
|
+
handler: async () => {
|
|
46
|
+
try {
|
|
47
|
+
await this.fs.mkdir(this.directory, { recursive: true });
|
|
48
|
+
this.log.info("Email directory OK", {
|
|
49
|
+
directory: this.directory,
|
|
50
|
+
});
|
|
51
|
+
} catch (error) {
|
|
52
|
+
const message = `Failed to create email directory: ${error instanceof Error ? error.message : String(error)}`;
|
|
53
|
+
this.log.error(message, { directory: this.directory });
|
|
54
|
+
throw new EmailError(
|
|
55
|
+
message,
|
|
56
|
+
error instanceof Error ? error : undefined,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
22
62
|
public async send(options: EmailSendOptions): Promise<void> {
|
|
23
63
|
const { to, subject, body } = options;
|
|
24
64
|
|
|
@@ -29,15 +69,12 @@ export class LocalEmailProvider implements EmailProvider {
|
|
|
29
69
|
});
|
|
30
70
|
|
|
31
71
|
try {
|
|
32
|
-
// Ensure directory exists
|
|
33
|
-
await fs.mkdir(this.directory, { recursive: true });
|
|
34
|
-
|
|
35
72
|
// Create filename: emailcontact+date
|
|
36
73
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
37
74
|
for (const recipient of Array.isArray(to) ? to : [to]) {
|
|
38
75
|
const sanitizedEmail = recipient.replace(/[^a-zA-Z0-9@.-]/g, "_");
|
|
39
76
|
const filename = `${sanitizedEmail}+${timestamp}.html`;
|
|
40
|
-
const filepath =
|
|
77
|
+
const filepath = this.fs.join(this.directory, filename);
|
|
41
78
|
|
|
42
79
|
// Create HTML content
|
|
43
80
|
const htmlContent = this.createEmailHtml({
|
|
@@ -47,7 +84,7 @@ export class LocalEmailProvider implements EmailProvider {
|
|
|
47
84
|
});
|
|
48
85
|
|
|
49
86
|
// Write to file
|
|
50
|
-
await fs.writeFile(filepath, htmlContent
|
|
87
|
+
await this.fs.writeFile(filepath, htmlContent);
|
|
51
88
|
|
|
52
89
|
this.log.info("Email saved to local file", { filepath, to, subject });
|
|
53
90
|
}
|
|
@@ -1,69 +1,173 @@
|
|
|
1
|
-
import { $env, $hook, t } from "alepha";
|
|
1
|
+
import { $atom, $env, $hook, $use, type Static, t } from "alepha";
|
|
2
2
|
import { $logger } from "alepha/logger";
|
|
3
3
|
import type { Transporter } from "nodemailer";
|
|
4
4
|
import nodemailer from "nodemailer";
|
|
5
5
|
import { EmailError } from "../errors/EmailError.ts";
|
|
6
6
|
import type { EmailProvider, EmailSendOptions } from "./EmailProvider.ts";
|
|
7
7
|
|
|
8
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Environment variables for nodemailer configuration
|
|
12
|
+
*/
|
|
8
13
|
const envSchema = t.object({
|
|
9
|
-
EMAIL_HOST: t.
|
|
10
|
-
|
|
11
|
-
|
|
14
|
+
EMAIL_HOST: t.optional(
|
|
15
|
+
t.text({
|
|
16
|
+
description: "SMTP server host",
|
|
17
|
+
}),
|
|
18
|
+
),
|
|
12
19
|
EMAIL_PORT: t.number({
|
|
13
20
|
default: 587,
|
|
14
21
|
description: "SMTP server port",
|
|
15
22
|
}),
|
|
16
|
-
EMAIL_USER: t.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
EMAIL_USER: t.optional(
|
|
24
|
+
t.text({
|
|
25
|
+
description: "SMTP authentication username",
|
|
26
|
+
}),
|
|
27
|
+
),
|
|
28
|
+
EMAIL_PASS: t.optional(
|
|
29
|
+
t.text({
|
|
30
|
+
description: "SMTP authentication password",
|
|
31
|
+
}),
|
|
32
|
+
),
|
|
33
|
+
EMAIL_FROM: t.optional(
|
|
34
|
+
t.text({
|
|
35
|
+
description: "Default from email address",
|
|
36
|
+
}),
|
|
37
|
+
),
|
|
25
38
|
EMAIL_SECURE: t.boolean({
|
|
26
39
|
default: false,
|
|
27
40
|
description: "Use secure connection (TLS)",
|
|
28
41
|
}),
|
|
29
42
|
});
|
|
30
43
|
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Custom transporter configuration.
|
|
34
|
-
* If provided, will override environment variables.
|
|
35
|
-
*/
|
|
36
|
-
transporter?: Transporter;
|
|
44
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
37
45
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Nodemailer connection pooling and rate limiting options
|
|
48
|
+
*/
|
|
49
|
+
export const nodemailerEmailOptions = $atom({
|
|
50
|
+
name: "alepha.email.nodemailer.options",
|
|
51
|
+
schema: t.object({
|
|
52
|
+
pool: t.optional(
|
|
53
|
+
t.boolean({
|
|
54
|
+
description: "Enable connection pooling",
|
|
55
|
+
}),
|
|
56
|
+
),
|
|
57
|
+
maxConnections: t.optional(
|
|
58
|
+
t.number({
|
|
59
|
+
description: "Maximum number of connections in pool",
|
|
60
|
+
}),
|
|
61
|
+
),
|
|
62
|
+
maxMessages: t.optional(
|
|
63
|
+
t.number({
|
|
64
|
+
description: "Maximum messages per connection",
|
|
65
|
+
}),
|
|
66
|
+
),
|
|
67
|
+
rateDelta: t.optional(
|
|
68
|
+
t.number({
|
|
69
|
+
description: "Time in milliseconds between message sends",
|
|
70
|
+
}),
|
|
71
|
+
),
|
|
72
|
+
rateLimit: t.optional(
|
|
73
|
+
t.number({
|
|
74
|
+
description: "Maximum number of messages per rateDelta",
|
|
75
|
+
}),
|
|
76
|
+
),
|
|
77
|
+
}),
|
|
78
|
+
default: {},
|
|
79
|
+
});
|
|
43
80
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
rateLimit?: number;
|
|
53
|
-
};
|
|
81
|
+
export type NodemailerEmailProviderOptions = Static<
|
|
82
|
+
typeof nodemailerEmailOptions.schema
|
|
83
|
+
>;
|
|
84
|
+
|
|
85
|
+
declare module "alepha" {
|
|
86
|
+
interface State {
|
|
87
|
+
[nodemailerEmailOptions.key]: NodemailerEmailProviderOptions;
|
|
88
|
+
}
|
|
54
89
|
}
|
|
55
90
|
|
|
91
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Email provider using Nodemailer for SMTP transport.
|
|
95
|
+
*
|
|
96
|
+
* Configuration is provided via environment variables:
|
|
97
|
+
* - EMAIL_HOST: SMTP server host
|
|
98
|
+
* - EMAIL_PORT: SMTP server port (default: 587)
|
|
99
|
+
* - EMAIL_USER: SMTP authentication username
|
|
100
|
+
* - EMAIL_PASS: SMTP authentication password
|
|
101
|
+
* - EMAIL_FROM: Default from email address
|
|
102
|
+
* - EMAIL_SECURE: Use secure connection (default: false)
|
|
103
|
+
*
|
|
104
|
+
* Advanced pooling/rate limiting options can be configured via atom:
|
|
105
|
+
* @see {@link nodemailerEmailOptions}
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```typescript
|
|
109
|
+
* // Configure via environment variables
|
|
110
|
+
* // EMAIL_HOST=smtp.example.com
|
|
111
|
+
* // EMAIL_PORT=587
|
|
112
|
+
* // EMAIL_USER=user@example.com
|
|
113
|
+
* // EMAIL_PASS=secret
|
|
114
|
+
* // EMAIL_FROM=noreply@example.com
|
|
115
|
+
*
|
|
116
|
+
* // Optionally configure pooling via atom
|
|
117
|
+
* alepha.state.set(nodemailerEmailOptions.key, {
|
|
118
|
+
* pool: true,
|
|
119
|
+
* maxConnections: 5,
|
|
120
|
+
* rateLimit: 10,
|
|
121
|
+
* });
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
56
124
|
export class NodemailerEmailProvider implements EmailProvider {
|
|
57
125
|
protected readonly env = $env(envSchema);
|
|
58
126
|
protected readonly log = $logger();
|
|
59
|
-
protected
|
|
60
|
-
protected
|
|
127
|
+
protected readonly options = $use(nodemailerEmailOptions);
|
|
128
|
+
protected transporter: Transporter | null = null;
|
|
61
129
|
|
|
62
|
-
|
|
130
|
+
protected get host(): string {
|
|
131
|
+
const host = this.env.EMAIL_HOST;
|
|
132
|
+
if (!host) {
|
|
133
|
+
throw new EmailError(
|
|
134
|
+
"Email host not configured. Set EMAIL_HOST env var.",
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
return host;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
protected get port(): number {
|
|
141
|
+
return this.env.EMAIL_PORT;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
protected get secure(): boolean {
|
|
145
|
+
return this.env.EMAIL_SECURE;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
protected get user(): string | undefined {
|
|
149
|
+
return this.env.EMAIL_USER;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
protected get pass(): string | undefined {
|
|
153
|
+
return this.env.EMAIL_PASS;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
protected get fromAddress(): string {
|
|
157
|
+
const from = this.env.EMAIL_FROM;
|
|
158
|
+
if (!from) {
|
|
159
|
+
throw new EmailError(
|
|
160
|
+
"Email from address not configured. Set EMAIL_FROM env var.",
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
return from;
|
|
164
|
+
}
|
|
63
165
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
166
|
+
protected getTransporter(): Transporter {
|
|
167
|
+
if (!this.transporter) {
|
|
168
|
+
this.transporter = this.createTransporter();
|
|
169
|
+
}
|
|
170
|
+
return this.transporter;
|
|
67
171
|
}
|
|
68
172
|
|
|
69
173
|
public async send(options: EmailSendOptions): Promise<void> {
|
|
@@ -71,7 +175,7 @@ export class NodemailerEmailProvider implements EmailProvider {
|
|
|
71
175
|
this.log.debug("Sending email via Nodemailer", { to, subject });
|
|
72
176
|
|
|
73
177
|
try {
|
|
74
|
-
const result = await this.
|
|
178
|
+
const result = await this.getTransporter().sendMail({
|
|
75
179
|
from: this.fromAddress,
|
|
76
180
|
to,
|
|
77
181
|
subject,
|
|
@@ -92,26 +196,30 @@ export class NodemailerEmailProvider implements EmailProvider {
|
|
|
92
196
|
}
|
|
93
197
|
|
|
94
198
|
protected createTransporter(): Transporter {
|
|
95
|
-
if (this.options.transporter) {
|
|
96
|
-
return this.options.transporter;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
199
|
const transporterConfig = {
|
|
100
|
-
host: this.
|
|
101
|
-
port: this.
|
|
102
|
-
secure: this.
|
|
103
|
-
auth:
|
|
104
|
-
user
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
200
|
+
host: this.host,
|
|
201
|
+
port: this.port,
|
|
202
|
+
secure: this.secure,
|
|
203
|
+
auth:
|
|
204
|
+
this.user && this.pass
|
|
205
|
+
? {
|
|
206
|
+
user: this.user,
|
|
207
|
+
pass: this.pass,
|
|
208
|
+
}
|
|
209
|
+
: undefined,
|
|
210
|
+
pool: this.options.pool,
|
|
211
|
+
maxConnections: this.options.maxConnections,
|
|
212
|
+
maxMessages: this.options.maxMessages,
|
|
213
|
+
rateDelta: this.options.rateDelta,
|
|
214
|
+
rateLimit: this.options.rateLimit,
|
|
108
215
|
};
|
|
109
216
|
|
|
110
217
|
this.log.debug("Creating Nodemailer transporter", {
|
|
111
218
|
host: transporterConfig.host,
|
|
112
219
|
port: transporterConfig.port,
|
|
113
220
|
secure: transporterConfig.secure,
|
|
114
|
-
user: transporterConfig.auth
|
|
221
|
+
user: transporterConfig.auth?.user,
|
|
222
|
+
pool: transporterConfig.pool,
|
|
115
223
|
});
|
|
116
224
|
|
|
117
225
|
return nodemailer.createTransport(transporterConfig);
|
|
@@ -122,7 +230,7 @@ export class NodemailerEmailProvider implements EmailProvider {
|
|
|
122
230
|
*/
|
|
123
231
|
public async verify(): Promise<boolean> {
|
|
124
232
|
try {
|
|
125
|
-
await this.
|
|
233
|
+
await this.getTransporter().verify();
|
|
126
234
|
this.log.info("Email server connection verified");
|
|
127
235
|
return true;
|
|
128
236
|
} catch (error) {
|
|
@@ -135,7 +243,10 @@ export class NodemailerEmailProvider implements EmailProvider {
|
|
|
135
243
|
* Close the transporter connection.
|
|
136
244
|
*/
|
|
137
245
|
public close(): void {
|
|
138
|
-
this.transporter
|
|
246
|
+
if (this.transporter) {
|
|
247
|
+
this.transporter.close();
|
|
248
|
+
this.transporter = null;
|
|
249
|
+
}
|
|
139
250
|
}
|
|
140
251
|
|
|
141
252
|
protected readonly onStart = $hook({
|