@cosmicdrift/kumiko-bundled-features 0.102.2 → 0.105.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.
Files changed (23) hide show
  1. package/package.json +6 -6
  2. package/src/auth-email-password/__tests__/email-templates.test.ts +44 -47
  3. package/src/auth-email-password/__tests__/email-verification.integration.test.ts +41 -21
  4. package/src/auth-email-password/__tests__/invite-flow.integration.test.ts +62 -32
  5. package/src/auth-email-password/__tests__/password-reset.integration.test.ts +39 -20
  6. package/src/auth-email-password/__tests__/public-routes-rate-limit.integration.test.ts +39 -8
  7. package/src/auth-email-password/__tests__/signup-flow.integration.test.ts +53 -36
  8. package/src/auth-email-password/auth-paths.ts +24 -0
  9. package/src/auth-email-password/email-templates.ts +59 -118
  10. package/src/auth-email-password/feature.ts +19 -1
  11. package/src/auth-email-password/handlers/invite-create.write.ts +33 -5
  12. package/src/auth-email-password/handlers/request-email-verification.write.ts +5 -0
  13. package/src/auth-email-password/handlers/request-password-reset.write.ts +5 -0
  14. package/src/auth-email-password/handlers/signup-request.write.ts +38 -8
  15. package/src/auth-email-password/handlers/token-request-handler.ts +31 -0
  16. package/src/auth-email-password/index.ts +9 -20
  17. package/src/auth-email-password/magic-link-mail.ts +62 -0
  18. package/src/renderer-simple/__tests__/adapter.test.ts +38 -19
  19. package/src/renderer-simple/__tests__/template-resolver.integration.test.ts +102 -0
  20. package/src/renderer-simple/feature.ts +14 -3
  21. package/src/renderer-simple/resolve-variables.ts +55 -0
  22. package/src/auth-email-password/__tests__/auth-mailer.test.ts +0 -138
  23. package/src/auth-email-password/auth-mailer.ts +0 -153
@@ -1,138 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { createInMemoryTransport } from "../../channel-email";
3
- import { createAuthMailerConfig } from "../auth-mailer";
4
-
5
- function makeArgs(overrides?: Record<string, unknown>) {
6
- const mailSender = createInMemoryTransport();
7
- return {
8
- mailSender,
9
- hmacSecret: "test-hmac-secret",
10
- baseUrl: "https://admin.example.com",
11
- paths: {
12
- resetPassword: "/reset-password",
13
- verifyEmail: "/verify-email",
14
- signupComplete: "/signup/complete",
15
- inviteAccept: "/invite/accept",
16
- },
17
- appName: "TestApp",
18
- locale: "en" as const,
19
- ...overrides,
20
- };
21
- }
22
-
23
- describe("createAuthMailerConfig", () => {
24
- test("returns all 4 setups", () => {
25
- const config = createAuthMailerConfig(makeArgs());
26
- expect(config.passwordReset).toBeDefined();
27
- expect(config.emailVerification).toBeDefined();
28
- expect(config.signup).toBeDefined();
29
- expect(config.invite).toBeDefined();
30
- });
31
-
32
- test("constructs URLs from baseUrl + paths", () => {
33
- const config = createAuthMailerConfig(makeArgs());
34
- expect(config.passwordReset.appResetUrl).toBe("https://admin.example.com/reset-password");
35
- expect(config.emailVerification.appVerifyUrl).toBe("https://admin.example.com/verify-email");
36
- expect(config.signup.appActivationUrl).toBe("https://admin.example.com/signup/complete");
37
- expect(config.invite.appAcceptUrl).toBe("https://admin.example.com/invite/accept");
38
- });
39
-
40
- test("forwards hmacSecret", () => {
41
- const config = createAuthMailerConfig(makeArgs());
42
- expect(config.passwordReset.hmacSecret).toBe("test-hmac-secret");
43
- expect(config.emailVerification.hmacSecret).toBe("test-hmac-secret");
44
- });
45
-
46
- test("sendResetEmail calls mailSender.send with rendered content", async () => {
47
- const args = makeArgs();
48
- const config = createAuthMailerConfig(args);
49
-
50
- await config.passwordReset.sendResetEmail({
51
- email: "user@example.com",
52
- resetUrl: "https://admin.example.com/reset?token=abc",
53
- expiresAt: "2026-06-09T12:00:00.000Z",
54
- });
55
-
56
- expect(args.mailSender.sent).toHaveLength(1);
57
- expect(args.mailSender.sent[0]!.to).toBe("user@example.com");
58
- expect(args.mailSender.sent[0]!.subject).toContain("TestApp");
59
- expect(args.mailSender.sent[0]!.subject).toContain("Reset");
60
- expect(args.mailSender.sent[0]!.html).toContain("https://admin.example.com/reset?token=abc");
61
- });
62
-
63
- test("sendVerificationEmail calls mailSender.send", async () => {
64
- const args = makeArgs();
65
- const config = createAuthMailerConfig(args);
66
-
67
- await config.emailVerification.sendVerificationEmail({
68
- email: "user@example.com",
69
- verificationUrl: "https://admin.example.com/verify?token=abc",
70
- expiresAt: "2026-06-09T12:00:00.000Z",
71
- });
72
-
73
- expect(args.mailSender.sent).toHaveLength(1);
74
- expect(args.mailSender.sent[0]!.to).toBe("user@example.com");
75
- expect(args.mailSender.sent[0]!.subject).toContain("TestApp");
76
- expect(args.mailSender.sent[0]!.subject).toContain("Verify");
77
- });
78
-
79
- test("sendActivationEmail calls mailSender.send", async () => {
80
- const args = makeArgs();
81
- const config = createAuthMailerConfig(args);
82
-
83
- await config.signup.sendActivationEmail({
84
- email: "user@example.com",
85
- activationUrl: "https://admin.example.com/signup/complete?token=abc",
86
- expiresAt: "2026-06-09T12:00:00.000Z",
87
- });
88
-
89
- expect(args.mailSender.sent).toHaveLength(1);
90
- expect(args.mailSender.sent[0]!.to).toBe("user@example.com");
91
- expect(args.mailSender.sent[0]!.subject).toContain("TestApp");
92
- });
93
-
94
- test("sendInviteEmail calls mailSender.send with role", async () => {
95
- const args = makeArgs();
96
- const config = createAuthMailerConfig(args);
97
-
98
- await config.invite.sendInviteEmail({
99
- email: "user@example.com",
100
- inviteUrl: "https://admin.example.com/invite/accept?token=abc",
101
- expiresAt: "2026-06-09T12:00:00.000Z",
102
- role: "Admin",
103
- });
104
-
105
- expect(args.mailSender.sent).toHaveLength(1);
106
- expect(args.mailSender.sent[0]!.to).toBe("user@example.com");
107
- expect(args.mailSender.sent[0]!.subject).toContain("TestApp");
108
- expect(args.mailSender.sent[0]!.html).toContain("Admin");
109
- });
110
-
111
- test("uses defaults when appName is omitted", () => {
112
- const config = createAuthMailerConfig(makeArgs({ appName: undefined }));
113
- expect(config.passwordReset.appResetUrl).toBeDefined();
114
- });
115
-
116
- test("emailVerificationMode is absent when not provided", () => {
117
- const config = createAuthMailerConfig(makeArgs());
118
- expect("mode" in config.emailVerification).toBe(false);
119
- });
120
-
121
- test("emailVerificationMode is set when provided", () => {
122
- const config = createAuthMailerConfig(makeArgs({ emailVerificationMode: "strict" }));
123
- expect(config.emailVerification).toHaveProperty("mode", "strict");
124
- });
125
-
126
- test("locale 'de' renders German subject", async () => {
127
- const args = makeArgs({ locale: "de" });
128
- const config = createAuthMailerConfig(args);
129
-
130
- await config.passwordReset.sendResetEmail({
131
- email: "user@example.com",
132
- resetUrl: "https://example.com/reset?token=abc",
133
- expiresAt: "2026-06-09T12:00:00.000Z",
134
- });
135
-
136
- expect(args.mailSender.sent[0]!.subject).toContain("Passwort");
137
- });
138
- });
@@ -1,153 +0,0 @@
1
- import type { EmailTransport } from "../channel-email";
2
- import type { AuthMailLocale } from "./email-templates";
3
- import {
4
- renderActivationEmail,
5
- renderInviteEmail,
6
- renderResetPasswordEmail,
7
- renderVerifyEmail,
8
- } from "./email-templates";
9
- import type {
10
- EmailVerificationOptions,
11
- InviteOptions,
12
- PasswordResetOptions,
13
- SignupOptions,
14
- } from "./feature";
15
-
16
- /**
17
- * Komplette Konfiguration für die 4 Auth-Mail-Flows einer App.
18
- *
19
- * Strukturell kompatibel mit den `*Setup`-Typen aus
20
- * `@cosmicdrift/kumiko-dev-server` (`PasswordResetSetup`,
21
- * `EmailVerificationSetup`, `SignupSetup`, `InviteSetup`).
22
- */
23
- export type AuthMailerConfig = {
24
- readonly passwordReset: PasswordResetOptions & {
25
- readonly appResetUrl: string;
26
- readonly sendResetEmail: (args: {
27
- email: string;
28
- resetUrl: string;
29
- expiresAt: string;
30
- }) => Promise<void>;
31
- };
32
- readonly emailVerification: EmailVerificationOptions & {
33
- readonly appVerifyUrl: string;
34
- readonly sendVerificationEmail: (args: {
35
- email: string;
36
- verificationUrl: string;
37
- expiresAt: string;
38
- }) => Promise<void>;
39
- };
40
- readonly signup: SignupOptions & {
41
- readonly appActivationUrl: string;
42
- readonly sendActivationEmail: (args: {
43
- email: string;
44
- activationUrl: string;
45
- expiresAt: string;
46
- }) => Promise<void>;
47
- };
48
- readonly invite: InviteOptions & {
49
- readonly appAcceptUrl: string;
50
- readonly sendInviteEmail: (args: {
51
- email: string;
52
- inviteUrl: string;
53
- expiresAt: string;
54
- role: string;
55
- }) => Promise<void>;
56
- };
57
- };
58
-
59
- /** Pfad-Konstanten der 4 Auth-Seiten (relativ zur App-baseUrl). */
60
- export type AuthPaths = {
61
- readonly resetPassword: string;
62
- readonly verifyEmail: string;
63
- readonly signupComplete: string;
64
- readonly inviteAccept: string;
65
- };
66
-
67
- /** Konventions-Pfade — alle Kumiko-Apps nutzen dieselben. Apps überschreiben
68
- * nur die Ausnahme via `makeAuthPaths({ ... })`. */
69
- export const DEFAULT_AUTH_PATHS: AuthPaths = {
70
- resetPassword: "/reset-password",
71
- verifyEmail: "/verify-email",
72
- signupComplete: "/signup/complete",
73
- inviteAccept: "/invite/accept",
74
- };
75
-
76
- export function makeAuthPaths(overrides: Partial<AuthPaths> = {}): AuthPaths {
77
- return { ...DEFAULT_AUTH_PATHS, ...overrides };
78
- }
79
-
80
- export type CreateAuthMailerConfigArgs = {
81
- readonly mailSender: EmailTransport;
82
- readonly hmacSecret: string;
83
- /** Basis-URL der App inkl. Schema (z.B. "https://admin.example.com").
84
- * Die factory hängt paths.resetPassword etc. an. */
85
- readonly baseUrl: string;
86
- /** Pfad-Konstanten für die Auth-Seiten. Default: DEFAULT_AUTH_PATHS. */
87
- readonly paths?: AuthPaths;
88
- /** App-Name für Mail-Subject + Body. Default "Account". */
89
- readonly appName?: string;
90
- /** Locale für die Mail-Templates. Default "de". */
91
- readonly locale?: AuthMailLocale;
92
- /** Email-verification mode. Default undefined (kein Gate).
93
- * "strict" blockt Login solange `emailVerified=false`.
94
- * "off" mountet die Routes ohne login-gating. */
95
- readonly emailVerificationMode?: "strict" | "off";
96
- };
97
-
98
- /**
99
- * Factory für `AuthMailerConfig` — baut die 4 Auth-Mail-Setups
100
- * (passwordReset / emailVerification / signup / invite) inklusive
101
- * `send*Email`-Wrapper + URL-Konstruktion.
102
- *
103
- * Jede App ruft das einmal auf und spreadet das Resultat in die
104
- * `auth`-Option von `runProdApp` / `runDevApp`.
105
- */
106
- export function createAuthMailerConfig(args: CreateAuthMailerConfigArgs): AuthMailerConfig {
107
- const appName = args.appName ?? "Account";
108
- const locale = args.locale ?? "de";
109
- const paths = args.paths ?? DEFAULT_AUTH_PATHS;
110
- return {
111
- passwordReset: {
112
- hmacSecret: args.hmacSecret,
113
- appResetUrl: `${args.baseUrl}${paths.resetPassword}`,
114
- sendResetEmail: async ({ email, resetUrl, expiresAt }) => {
115
- await args.mailSender.send({
116
- to: email,
117
- ...renderResetPasswordEmail({ resetUrl, expiresAt, locale, appName }),
118
- });
119
- },
120
- },
121
- emailVerification: {
122
- hmacSecret: args.hmacSecret,
123
- ...(args.emailVerificationMode !== undefined && {
124
- mode: args.emailVerificationMode,
125
- }),
126
- appVerifyUrl: `${args.baseUrl}${paths.verifyEmail}`,
127
- sendVerificationEmail: async ({ email, verificationUrl, expiresAt }) => {
128
- await args.mailSender.send({
129
- to: email,
130
- ...renderVerifyEmail({ verificationUrl, expiresAt, locale, appName }),
131
- });
132
- },
133
- },
134
- signup: {
135
- appActivationUrl: `${args.baseUrl}${paths.signupComplete}`,
136
- sendActivationEmail: async ({ email, activationUrl, expiresAt }) => {
137
- await args.mailSender.send({
138
- to: email,
139
- ...renderActivationEmail({ activationUrl, expiresAt, locale, appName }),
140
- });
141
- },
142
- },
143
- invite: {
144
- appAcceptUrl: `${args.baseUrl}${paths.inviteAccept}`,
145
- sendInviteEmail: async ({ email, inviteUrl, expiresAt, role }) => {
146
- await args.mailSender.send({
147
- to: email,
148
- ...renderInviteEmail({ inviteUrl, expiresAt, role, locale, appName }),
149
- });
150
- },
151
- },
152
- };
153
- }