@authcore/core 0.7.0 → 0.12.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 (87) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +141 -125
  3. package/dist/auth.d.ts +140 -12
  4. package/dist/auth.d.ts.map +1 -1
  5. package/dist/auth.js +265 -7
  6. package/dist/auth.js.map +1 -1
  7. package/dist/features/emailVerification.d.ts +6 -1
  8. package/dist/features/emailVerification.d.ts.map +1 -1
  9. package/dist/features/emailVerification.js +7 -10
  10. package/dist/features/emailVerification.js.map +1 -1
  11. package/dist/features/invitation.d.ts +7 -1
  12. package/dist/features/invitation.d.ts.map +1 -1
  13. package/dist/features/invitation.js +7 -9
  14. package/dist/features/invitation.js.map +1 -1
  15. package/dist/features/magicLink.d.ts +56 -0
  16. package/dist/features/magicLink.d.ts.map +1 -0
  17. package/dist/features/magicLink.js +88 -0
  18. package/dist/features/magicLink.js.map +1 -0
  19. package/dist/features/oauth.d.ts +39 -0
  20. package/dist/features/oauth.d.ts.map +1 -0
  21. package/dist/features/oauth.js +161 -0
  22. package/dist/features/oauth.js.map +1 -0
  23. package/dist/features/passwordReset.d.ts +6 -1
  24. package/dist/features/passwordReset.d.ts.map +1 -1
  25. package/dist/features/passwordReset.js +7 -10
  26. package/dist/features/passwordReset.js.map +1 -1
  27. package/dist/features/refresh.d.ts +41 -0
  28. package/dist/features/refresh.d.ts.map +1 -0
  29. package/dist/features/refresh.js +58 -0
  30. package/dist/features/refresh.js.map +1 -0
  31. package/dist/features/templates.d.ts +46 -0
  32. package/dist/features/templates.d.ts.map +1 -0
  33. package/dist/features/templates.js +67 -0
  34. package/dist/features/templates.js.map +1 -0
  35. package/dist/features/twoFactor.d.ts +72 -0
  36. package/dist/features/twoFactor.d.ts.map +1 -0
  37. package/dist/features/twoFactor.js +119 -0
  38. package/dist/features/twoFactor.js.map +1 -0
  39. package/dist/index.d.ts +21 -5
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +13 -2
  42. package/dist/index.js.map +1 -1
  43. package/dist/oauth/apple.d.ts +80 -0
  44. package/dist/oauth/apple.d.ts.map +1 -0
  45. package/dist/oauth/apple.js +148 -0
  46. package/dist/oauth/apple.js.map +1 -0
  47. package/dist/oauth/discord.d.ts +32 -0
  48. package/dist/oauth/discord.d.ts.map +1 -0
  49. package/dist/oauth/discord.js +86 -0
  50. package/dist/oauth/discord.js.map +1 -0
  51. package/dist/oauth/github.d.ts +35 -0
  52. package/dist/oauth/github.d.ts.map +1 -0
  53. package/dist/oauth/github.js +114 -0
  54. package/dist/oauth/github.js.map +1 -0
  55. package/dist/oauth/google.d.ts +21 -0
  56. package/dist/oauth/google.d.ts.map +1 -0
  57. package/dist/oauth/google.js +76 -0
  58. package/dist/oauth/google.js.map +1 -0
  59. package/dist/oauth/microsoft.d.ts +40 -0
  60. package/dist/oauth/microsoft.d.ts.map +1 -0
  61. package/dist/oauth/microsoft.js +126 -0
  62. package/dist/oauth/microsoft.js.map +1 -0
  63. package/dist/utils/token.d.ts +37 -0
  64. package/dist/utils/token.d.ts.map +1 -1
  65. package/dist/utils/token.js +53 -0
  66. package/dist/utils/token.js.map +1 -1
  67. package/dist/utils/totp.d.ts +59 -0
  68. package/dist/utils/totp.d.ts.map +1 -0
  69. package/dist/utils/totp.js +176 -0
  70. package/dist/utils/totp.js.map +1 -0
  71. package/dist/utils/validation.d.ts +18 -0
  72. package/dist/utils/validation.d.ts.map +1 -1
  73. package/dist/utils/validation.js +8 -0
  74. package/dist/utils/validation.js.map +1 -1
  75. package/package.json +2 -2
  76. package/dist/adapters/database.interface.d.ts +0 -42
  77. package/dist/adapters/database.interface.d.ts.map +0 -1
  78. package/dist/adapters/database.interface.js +0 -2
  79. package/dist/adapters/database.interface.js.map +0 -1
  80. package/dist/adapters/email.interface.d.ts +0 -31
  81. package/dist/adapters/email.interface.d.ts.map +0 -1
  82. package/dist/adapters/email.interface.js +0 -2
  83. package/dist/adapters/email.interface.js.map +0 -1
  84. package/dist/types.d.ts +0 -76
  85. package/dist/types.d.ts.map +0 -1
  86. package/dist/types.js +0 -6
  87. package/dist/types.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"magicLink.d.ts","sourceRoot":"","sources":["../../src/features/magicLink.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,aAAa,EAAS,IAAI,EAAE,MAAM,iBAAiB,CAAA;AAOhG;;;;;;;GAOG;AACH,eAAO,MAAM,+BAA+B,4BAA4B,CAAA;AAExE;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE;IAC1C,KAAK,EAAE,MAAM,CAAA;IACb,EAAE,EAAE,eAAe,CAAA;IACnB,aAAa,EAAE,YAAY,CAAA;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,YAAY,EAAE,MAAM,CAAA;IACpB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,aAAa,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAC9E,GAAG,OAAO,CAAC,OAAO,CAAC,CA6CnB;AAED;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE;IAC7C,QAAQ,EAAE,MAAM,CAAA;IAChB,EAAE,EAAE,eAAe,CAAA;CACpB,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,IAAI,CAAA;CAAE,CAAC,CAqB1B"}
@@ -0,0 +1,88 @@
1
+ import { generateOpaqueToken, hashToken } from '../utils/token.js';
2
+ import { defaultMagicLinkTemplate } from './templates.js';
3
+ const MAGIC_LINK_TTL_MS = 15 * 60 * 1000; // 15 minutes
4
+ const MAGIC_LINK_TTL_MINUTES = 15;
5
+ /**
6
+ * Sentinel passwordHash used when magic-link creates a brand-new user.
7
+ *
8
+ * Same shape as the OAuth sentinel — the user has no password initially and
9
+ * can claim one via the standard forgot-password flow. This means a magic-link
10
+ * signup is functionally identical to OAuth signup: an authenticated session
11
+ * with no password set, and the standard "set a password" recovery path open.
12
+ */
13
+ export const MAGIC_LINK_NO_PASSWORD_SENTINEL = '!MAGIC_LINK_NO_PASSWORD';
14
+ /**
15
+ * Send a magic-link email. Always returns successfully (whether the user
16
+ * exists or not) to prevent email enumeration.
17
+ *
18
+ * If `autoCreate` is true (the default) and no user exists for the email,
19
+ * a new user is created with a sentinel password hash. The user's email is
20
+ * marked verified — clicking a magic link from the inbox proves email
21
+ * ownership the same way clicking a verification link does.
22
+ *
23
+ * @returns `true` if an email was sent, `false` otherwise. Callers should
24
+ * not surface this to the client — return 200 either way.
25
+ */
26
+ export async function sendMagicLink(params) {
27
+ const { email, db, emailProvider, from, magicLinkUrl, autoCreate = true, defaultRole = 'user', template = defaultMagicLinkTemplate, } = params;
28
+ let user = await db.findUserByEmail(email);
29
+ if (!user) {
30
+ if (!autoCreate)
31
+ return false;
32
+ user = await db.createUser({
33
+ email,
34
+ passwordHash: MAGIC_LINK_NO_PASSWORD_SENTINEL,
35
+ role: defaultRole,
36
+ });
37
+ // Magic-link signup verifies the email by definition (the link arrives in
38
+ // the inbox, proving ownership). Mark verified up front so the user isn't
39
+ // gated on a second verification email.
40
+ user = await db.updateUser(user.id, { emailVerified: true });
41
+ }
42
+ const rawToken = generateOpaqueToken();
43
+ await db.createToken({
44
+ userId: user.id,
45
+ type: 'MAGIC_LINK',
46
+ token: hashToken(rawToken),
47
+ expiresAt: new Date(Date.now() + MAGIC_LINK_TTL_MS),
48
+ });
49
+ const link = `${magicLinkUrl}?token=${rawToken}`;
50
+ const rendered = template({ email, link, ttlMinutes: MAGIC_LINK_TTL_MINUTES });
51
+ await emailProvider.send({
52
+ from,
53
+ to: email,
54
+ subject: rendered.subject,
55
+ html: rendered.html,
56
+ text: rendered.text,
57
+ });
58
+ return true;
59
+ }
60
+ /**
61
+ * Consume a magic-link token. Returns the user (still as a stored DB record).
62
+ * Caller is responsible for minting the session JWT + refresh token.
63
+ *
64
+ * Idempotency: tokens are single-use. After consumption the token row is
65
+ * deleted, so a second call with the same raw token throws.
66
+ *
67
+ * @throws Error if the token is invalid or expired
68
+ */
69
+ export async function consumeMagicLink(params) {
70
+ const { rawToken, db } = params;
71
+ const tokenRecord = await db.findToken(rawToken, 'MAGIC_LINK');
72
+ if (!tokenRecord) {
73
+ throw new Error('Invalid or expired magic-link token');
74
+ }
75
+ if (tokenRecord.expiresAt < new Date()) {
76
+ await db.deleteToken(tokenRecord.id);
77
+ throw new Error('Invalid or expired magic-link token');
78
+ }
79
+ const user = await db.findUserById(tokenRecord.userId);
80
+ if (!user) {
81
+ await db.deleteToken(tokenRecord.id);
82
+ throw new Error('Invalid or expired magic-link token');
83
+ }
84
+ // Single-use: delete before returning so a replay returns the same error.
85
+ await db.deleteToken(tokenRecord.id);
86
+ return { user };
87
+ }
88
+ //# sourceMappingURL=magicLink.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"magicLink.js","sourceRoot":"","sources":["../../src/features/magicLink.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ import type { DatabaseAdapter, OAuthAccount, OAuthProvider, User } from '@authcore/types';
2
+ /**
3
+ * Begin an OAuth Authorization Code + PKCE flow.
4
+ *
5
+ * Generates a state envelope (nonce, provider, PKCE verifier, redirectUri, issuedAt)
6
+ * HMAC-signed with the AuthCore secret so the callback can verify integrity without
7
+ * a DB round-trip. Returns the provider's authorization URL.
8
+ */
9
+ export declare function startOAuth(params: {
10
+ provider: OAuthProvider;
11
+ redirectUri: string;
12
+ secret: string;
13
+ }): Promise<{
14
+ authorizationUrl: string;
15
+ state: string;
16
+ }>;
17
+ /**
18
+ * Complete an OAuth callback. Verifies the HMAC-signed state, exchanges the code with
19
+ * the provider for tokens, fetches the user profile, and applies the auto-link policy:
20
+ *
21
+ * - existing OAuthAccount (provider, providerId) → load user, return.
22
+ * - no OAuthAccount, no local user → create user (sentinel passwordHash) + link.
23
+ * - no OAuthAccount, local user, emailVerified=true → link to existing user.
24
+ * - no OAuthAccount, local user, emailVerified=false → throw EMAIL_NOT_VERIFIED_BY_PROVIDER (409).
25
+ */
26
+ export declare function completeOAuth(params: {
27
+ provider: OAuthProvider;
28
+ state: string;
29
+ code: string;
30
+ redirectUri: string;
31
+ secret: string;
32
+ db: DatabaseAdapter;
33
+ defaultRole: string;
34
+ }): Promise<{
35
+ user: User;
36
+ oauthAccount: OAuthAccount;
37
+ isNewUser: boolean;
38
+ }>;
39
+ //# sourceMappingURL=oauth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../../src/features/oauth.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,eAAe,EACf,YAAY,EACZ,aAAa,EACb,IAAI,EACL,MAAM,iBAAiB,CAAA;AA0DxB;;;;;;GAMG;AACH,wBAAsB,UAAU,CAAC,MAAM,EAAE;IACvC,QAAQ,EAAE,aAAa,CAAA;IACvB,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;CACf,GAAG,OAAO,CAAC;IAAE,gBAAgB,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAiBvD;AAED;;;;;;;;GAQG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE;IAC1C,QAAQ,EAAE,aAAa,CAAA;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,EAAE,EAAE,eAAe,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;CACpB,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,YAAY,EAAE,YAAY,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAAC,CAqF1E"}
@@ -0,0 +1,161 @@
1
+ import { createHmac, timingSafeEqual } from 'node:crypto';
2
+ import { generateOpaqueToken, generatePkceVerifier, pkceChallenge } from '../utils/token.js';
3
+ const OAUTH_STATE_TTL_MS = 10 * 60 * 1000; // 10 minutes
4
+ const OAUTH_NO_PASSWORD_SENTINEL = '!OAUTH_NO_PASSWORD';
5
+ /** base64url(envelope) + '.' + base64url(hmac(envelope)) */
6
+ function signEnvelope(env, secret) {
7
+ const json = JSON.stringify(env);
8
+ const payload = Buffer.from(json, 'utf8').toString('base64url');
9
+ const sig = createHmac('sha256', secret).update(payload).digest('base64url');
10
+ return `${payload}.${sig}`;
11
+ }
12
+ function verifyEnvelope(signed, secret) {
13
+ const dot = signed.lastIndexOf('.');
14
+ if (dot < 0)
15
+ return null;
16
+ const payload = signed.slice(0, dot);
17
+ const sig = signed.slice(dot + 1);
18
+ const expected = createHmac('sha256', secret).update(payload).digest('base64url');
19
+ // Timing-safe compare
20
+ const sigBuf = Buffer.from(sig, 'utf8');
21
+ const expBuf = Buffer.from(expected, 'utf8');
22
+ if (sigBuf.length !== expBuf.length || !timingSafeEqual(sigBuf, expBuf))
23
+ return null;
24
+ try {
25
+ const env = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
26
+ if (typeof env.nonce !== 'string' ||
27
+ typeof env.provider !== 'string' ||
28
+ typeof env.codeVerifier !== 'string' ||
29
+ typeof env.redirectUri !== 'string' ||
30
+ typeof env.issuedAt !== 'number')
31
+ return null;
32
+ return env;
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ /**
39
+ * Begin an OAuth Authorization Code + PKCE flow.
40
+ *
41
+ * Generates a state envelope (nonce, provider, PKCE verifier, redirectUri, issuedAt)
42
+ * HMAC-signed with the AuthCore secret so the callback can verify integrity without
43
+ * a DB round-trip. Returns the provider's authorization URL.
44
+ */
45
+ export async function startOAuth(params) {
46
+ const { provider, redirectUri, secret } = params;
47
+ const env = {
48
+ nonce: generateOpaqueToken(),
49
+ provider: provider.id,
50
+ codeVerifier: generatePkceVerifier(),
51
+ redirectUri,
52
+ issuedAt: Date.now(),
53
+ };
54
+ const state = signEnvelope(env, secret);
55
+ const authorizationUrl = provider.authorize({
56
+ state,
57
+ codeChallenge: pkceChallenge(env.codeVerifier),
58
+ redirectUri,
59
+ });
60
+ return { authorizationUrl, state };
61
+ }
62
+ /**
63
+ * Complete an OAuth callback. Verifies the HMAC-signed state, exchanges the code with
64
+ * the provider for tokens, fetches the user profile, and applies the auto-link policy:
65
+ *
66
+ * - existing OAuthAccount (provider, providerId) → load user, return.
67
+ * - no OAuthAccount, no local user → create user (sentinel passwordHash) + link.
68
+ * - no OAuthAccount, local user, emailVerified=true → link to existing user.
69
+ * - no OAuthAccount, local user, emailVerified=false → throw EMAIL_NOT_VERIFIED_BY_PROVIDER (409).
70
+ */
71
+ export async function completeOAuth(params) {
72
+ const { provider, state, code, redirectUri, secret, db, defaultRole } = params;
73
+ // 1. Verify HMAC envelope
74
+ const env = verifyEnvelope(state, secret);
75
+ if (!env)
76
+ throw oauthError('Invalid OAuth state', 'INVALID_TOKEN', 401);
77
+ if (env.provider !== provider.id)
78
+ throw oauthError('OAuth state provider mismatch', 'INVALID_TOKEN', 401);
79
+ if (env.redirectUri !== redirectUri)
80
+ throw oauthError('OAuth state redirectUri mismatch', 'INVALID_TOKEN', 401);
81
+ if (Date.now() - env.issuedAt > OAUTH_STATE_TTL_MS) {
82
+ throw oauthError('OAuth state expired', 'INVALID_TOKEN', 401);
83
+ }
84
+ // 2. Exchange code for tokens
85
+ let tokens;
86
+ try {
87
+ tokens = await provider.exchangeCode({ code, codeVerifier: env.codeVerifier, redirectUri });
88
+ }
89
+ catch (err) {
90
+ const message = err instanceof Error ? err.message : 'Provider exchange failed';
91
+ throw oauthError(message, 'OAUTH_EXCHANGE_FAILED', 502);
92
+ }
93
+ // 3. Fetch user info
94
+ let profile;
95
+ try {
96
+ profile = await provider.getUserInfo(tokens.accessToken, tokens.idToken);
97
+ }
98
+ catch (err) {
99
+ const message = err instanceof Error ? err.message : 'Provider userinfo failed';
100
+ throw oauthError(message, 'OAUTH_USERINFO_FAILED', 502);
101
+ }
102
+ // 4. Existing OAuth account? Update tokens and return user.
103
+ const existing = await db.findOAuthAccount(provider.id, profile.id);
104
+ if (existing) {
105
+ const updated = await db.updateOAuthAccount(existing.id, {
106
+ accessToken: tokens.accessToken,
107
+ refreshToken: tokens.refreshToken ?? null,
108
+ expiresAt: tokens.expiresIn ? new Date(Date.now() + tokens.expiresIn * 1000) : null,
109
+ });
110
+ const user = await db.findUserById(existing.userId);
111
+ if (!user) {
112
+ throw oauthError('Linked user no longer exists', 'INVALID_TOKEN', 401);
113
+ }
114
+ return { user, oauthAccount: updated, isNewUser: false };
115
+ }
116
+ // 5. No OAuth account. Try to link by email.
117
+ const localUser = await db.findUserByEmail(profile.email);
118
+ if (localUser) {
119
+ if (!profile.emailVerified) {
120
+ throw oauthError('Email not verified by provider; sign in with your password to link this OAuth account', 'EMAIL_NOT_VERIFIED_BY_PROVIDER', 409);
121
+ }
122
+ const oauthAccount = await db.createOAuthAccount({
123
+ userId: localUser.id,
124
+ provider: provider.id,
125
+ providerAccountId: profile.id,
126
+ accessToken: tokens.accessToken,
127
+ refreshToken: tokens.refreshToken ?? null,
128
+ expiresAt: tokens.expiresIn ? new Date(Date.now() + tokens.expiresIn * 1000) : null,
129
+ });
130
+ return { user: localUser, oauthAccount, isNewUser: false };
131
+ }
132
+ // 6. Brand new user — create with sentinel passwordHash.
133
+ const newUser = await db.createUser({
134
+ email: profile.email,
135
+ passwordHash: OAUTH_NO_PASSWORD_SENTINEL,
136
+ role: defaultRole,
137
+ });
138
+ if (profile.emailVerified) {
139
+ await db.updateUser(newUser.id, { emailVerified: true });
140
+ newUser.emailVerified = true;
141
+ }
142
+ const oauthAccount = await db.createOAuthAccount({
143
+ userId: newUser.id,
144
+ provider: provider.id,
145
+ providerAccountId: profile.id,
146
+ accessToken: tokens.accessToken,
147
+ refreshToken: tokens.refreshToken ?? null,
148
+ expiresAt: tokens.expiresIn ? new Date(Date.now() + tokens.expiresIn * 1000) : null,
149
+ });
150
+ return { user: newUser, oauthAccount, isNewUser: true };
151
+ }
152
+ /** Local helper — kept decoupled from auth.ts's AuthError so feature stays portable. */
153
+ function oauthError(message, code, statusCode) {
154
+ const err = new Error(message);
155
+ err.name = 'OAuthError';
156
+ err.code = code;
157
+ err.statusCode = statusCode;
158
+ err.isOAuthError = true;
159
+ return err;
160
+ }
161
+ //# sourceMappingURL=oauth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth.js","sourceRoot":"","sources":["../../src/features/oauth.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import type { DatabaseAdapter, EmailAdapter } from '@authcore/types';
1
+ import type { DatabaseAdapter, EmailAdapter, EmailTemplate } from '@authcore/types';
2
2
  /**
3
3
  * Initiate a password reset flow.
4
4
  * Always returns successfully even if the email is not found — prevents email enumeration.
@@ -10,6 +10,11 @@ export declare function createPasswordReset(params: {
10
10
  emailProvider: EmailAdapter;
11
11
  from: string;
12
12
  resetUrl: string;
13
+ template?: EmailTemplate<{
14
+ email: string;
15
+ link: string;
16
+ ttlHours: number;
17
+ }>;
13
18
  }): Promise<void>;
14
19
  /**
15
20
  * Complete a password reset using the raw token from the reset email.
@@ -1 +1 @@
1
- {"version":3,"file":"passwordReset.d.ts","sourceRoot":"","sources":["../../src/features/passwordReset.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAS,MAAM,iBAAiB,CAAA;AAM3E;;;;GAIG;AACH,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,KAAK,EAAE,MAAM,CAAA;IACb,EAAE,EAAE,eAAe,CAAA;IACnB,aAAa,EAAE,YAAY,CAAA;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;CACjB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgChB;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE;IAC1C,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,EAAE,EAAE,eAAe,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBhB"}
1
+ {"version":3,"file":"passwordReset.d.ts","sourceRoot":"","sources":["../../src/features/passwordReset.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,aAAa,EAAS,MAAM,iBAAiB,CAAA;AAQ1F;;;;GAIG;AACH,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,KAAK,EAAE,MAAM,CAAA;IACb,EAAE,EAAE,eAAe,CAAA;IACnB,aAAa,EAAE,YAAY,CAAA;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,CAAC,EAAE,aAAa,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAC5E,GAAG,OAAO,CAAC,IAAI,CAAC,CA2BhB;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE;IAC1C,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,EAAE,EAAE,eAAe,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBhB"}
@@ -1,13 +1,15 @@
1
1
  import { generateOpaqueToken, hashToken } from '../utils/token.js';
2
2
  import { hashPassword } from '../utils/password.js';
3
+ import { defaultResetPasswordTemplate } from './templates.js';
3
4
  const PASSWORD_RESET_TTL_MS = 60 * 60 * 1000; // 1 hour
5
+ const PASSWORD_RESET_TTL_HOURS = 1;
4
6
  /**
5
7
  * Initiate a password reset flow.
6
8
  * Always returns successfully even if the email is not found — prevents email enumeration.
7
9
  * Callers should catch and swallow errors from the email send step if needed.
8
10
  */
9
11
  export async function createPasswordReset(params) {
10
- const { email, db, emailProvider, from, resetUrl } = params;
12
+ const { email, db, emailProvider, from, resetUrl, template = defaultResetPasswordTemplate } = params;
11
13
  // Always returns 200 — do not reveal whether the email exists
12
14
  const user = await db.findUserByEmail(email);
13
15
  if (!user)
@@ -21,18 +23,13 @@ export async function createPasswordReset(params) {
21
23
  expiresAt: new Date(Date.now() + PASSWORD_RESET_TTL_MS),
22
24
  });
23
25
  const link = `${resetUrl}?token=${rawToken}`;
26
+ const rendered = template({ email, link, ttlHours: PASSWORD_RESET_TTL_HOURS });
24
27
  await emailProvider.send({
25
28
  from,
26
29
  to: email,
27
- subject: 'Reset your password',
28
- html: `
29
- <p>Hello,</p>
30
- <p>We received a request to reset your password. Click the link below to proceed:</p>
31
- <p><a href="${link}">${link}</a></p>
32
- <p>This link expires in 1 hour.</p>
33
- <p>If you did not request a password reset, you can ignore this email.</p>
34
- `,
35
- text: `Reset your password by visiting: ${link}\n\nThis link expires in 1 hour.`,
30
+ subject: rendered.subject,
31
+ html: rendered.html,
32
+ text: rendered.text,
36
33
  });
37
34
  }
38
35
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"passwordReset.js","sourceRoot":"","sources":["../../src/features/passwordReset.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAEnD,MAAM,qBAAqB,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA,CAAC,SAAS;AAEtD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,MAMzC;IACC,MAAM,EAAE,KAAK,EAAE,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAA;IAE3D,8DAA8D;IAC9D,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,eAAe,CAAC,KAAK,CAAC,CAAA;IAC5C,IAAI,CAAC,IAAI;QAAE,OAAM;IAEjB,MAAM,QAAQ,GAAG,mBAAmB,EAAE,CAAA;IACtC,MAAM,WAAW,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAA;IAEvC,MAAM,EAAE,CAAC,WAAW,CAAC;QACnB,MAAM,EAAE,IAAI,CAAC,EAAE;QACf,IAAI,EAAE,gBAAgB;QACtB,KAAK,EAAE,WAAW;QAClB,SAAS,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,qBAAqB,CAAC;KACxD,CAAC,CAAA;IAEF,MAAM,IAAI,GAAG,GAAG,QAAQ,UAAU,QAAQ,EAAE,CAAA;IAE5C,MAAM,aAAa,CAAC,IAAI,CAAC;QACvB,IAAI;QACJ,EAAE,EAAE,KAAK;QACT,OAAO,EAAE,qBAAqB;QAC9B,IAAI,EAAE;;;oBAGU,IAAI,KAAK,IAAI;;;KAG5B;QACD,IAAI,EAAE,oCAAoC,IAAI,kCAAkC;KACjF,CAAC,CAAA;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,MAKnC;IACC,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,EAAE,EAAE,UAAU,EAAE,GAAG,MAAM,CAAA;IAExD,MAAM,WAAW,GAAiB,MAAM,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAAA;IAEhF,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAA;IACnD,CAAC;IAED,IAAI,WAAW,CAAC,SAAS,GAAG,IAAI,IAAI,EAAE,EAAE,CAAC;QACvC,MAAM,EAAE,CAAC,WAAW,CAAC,WAAW,CAAC,EAAE,CAAC,CAAA;QACpC,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAA;IACnD,CAAC;IAED,MAAM,YAAY,GAAG,MAAM,YAAY,CAAC,WAAW,EAAE,UAAU,CAAC,CAAA;IAChE,MAAM,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,MAAM,EAAE,EAAE,YAAY,EAAE,CAAC,CAAA;IACzD,MAAM,EAAE,CAAC,WAAW,CAAC,WAAW,CAAC,EAAE,CAAC,CAAA;AACtC,CAAC"}
1
+ {"version":3,"file":"passwordReset.js","sourceRoot":"","sources":["../../src/features/passwordReset.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AACnD,OAAO,EAAE,4BAA4B,EAAE,MAAM,gBAAgB,CAAA;AAE7D,MAAM,qBAAqB,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA,CAAC,SAAS;AACtD,MAAM,wBAAwB,GAAG,CAAC,CAAA;AAElC;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,MAOzC;IACC,MAAM,EAAE,KAAK,EAAE,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,GAAG,4BAA4B,EAAE,GAAG,MAAM,CAAA;IAEpG,8DAA8D;IAC9D,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,eAAe,CAAC,KAAK,CAAC,CAAA;IAC5C,IAAI,CAAC,IAAI;QAAE,OAAM;IAEjB,MAAM,QAAQ,GAAG,mBAAmB,EAAE,CAAA;IACtC,MAAM,WAAW,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAA;IAEvC,MAAM,EAAE,CAAC,WAAW,CAAC;QACnB,MAAM,EAAE,IAAI,CAAC,EAAE;QACf,IAAI,EAAE,gBAAgB;QACtB,KAAK,EAAE,WAAW;QAClB,SAAS,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,qBAAqB,CAAC;KACxD,CAAC,CAAA;IAEF,MAAM,IAAI,GAAG,GAAG,QAAQ,UAAU,QAAQ,EAAE,CAAA;IAC5C,MAAM,QAAQ,GAAG,QAAQ,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,wBAAwB,EAAE,CAAC,CAAA;IAE9E,MAAM,aAAa,CAAC,IAAI,CAAC;QACvB,IAAI;QACJ,EAAE,EAAE,KAAK;QACT,OAAO,EAAE,QAAQ,CAAC,OAAO;QACzB,IAAI,EAAE,QAAQ,CAAC,IAAI;QACnB,IAAI,EAAE,QAAQ,CAAC,IAAI;KACpB,CAAC,CAAA;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,MAKnC;IACC,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,EAAE,EAAE,UAAU,EAAE,GAAG,MAAM,CAAA;IAExD,MAAM,WAAW,GAAiB,MAAM,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAAA;IAEhF,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAA;IACnD,CAAC;IAED,IAAI,WAAW,CAAC,SAAS,GAAG,IAAI,IAAI,EAAE,EAAE,CAAC;QACvC,MAAM,EAAE,CAAC,WAAW,CAAC,WAAW,CAAC,EAAE,CAAC,CAAA;QACpC,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAA;IACnD,CAAC;IAED,MAAM,YAAY,GAAG,MAAM,YAAY,CAAC,WAAW,EAAE,UAAU,CAAC,CAAA;IAChE,MAAM,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,MAAM,EAAE,EAAE,YAAY,EAAE,CAAC,CAAA;IACzD,MAAM,EAAE,CAAC,WAAW,CAAC,WAAW,CAAC,EAAE,CAAC,CAAA;AACtC,CAAC"}
@@ -0,0 +1,41 @@
1
+ import type { DatabaseAdapter } from '@authcore/types';
2
+ /**
3
+ * Issue a new refresh token for the given user.
4
+ * Returns the raw token (caller sends this to the client). The DB stores only the SHA-256 hash.
5
+ */
6
+ export declare function issueRefreshToken(params: {
7
+ userId: string;
8
+ db: DatabaseAdapter;
9
+ ttlMs?: number;
10
+ }): Promise<string>;
11
+ /**
12
+ * Rotate a refresh token: validate the incoming raw token, delete it, and issue a fresh one.
13
+ * Throws on invalid/expired/already-rotated input — these are the same failure mode for the
14
+ * client (they need to re-authenticate).
15
+ */
16
+ export declare function rotateRefreshToken(params: {
17
+ rawToken: string;
18
+ db: DatabaseAdapter;
19
+ ttlMs?: number;
20
+ }): Promise<{
21
+ userId: string;
22
+ newRawToken: string;
23
+ }>;
24
+ /**
25
+ * Revoke a single refresh token by its raw value. Idempotent — if the token
26
+ * doesn't exist (already revoked or never issued), this is a no-op.
27
+ */
28
+ export declare function revokeRefreshToken(params: {
29
+ rawToken: string;
30
+ db: DatabaseAdapter;
31
+ }): Promise<void>;
32
+ /**
33
+ * Revoke every outstanding refresh token for a user. Used by "log out everywhere"
34
+ * and by password-reset / security-event flows where you want to invalidate all
35
+ * device sessions.
36
+ */
37
+ export declare function revokeAllRefreshTokensForUser(params: {
38
+ userId: string;
39
+ db: DatabaseAdapter;
40
+ }): Promise<void>;
41
+ //# sourceMappingURL=refresh.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refresh.d.ts","sourceRoot":"","sources":["../../src/features/refresh.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAS,MAAM,iBAAiB,CAAA;AAK7D;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE;IAC9C,MAAM,EAAE,MAAM,CAAA;IACd,EAAE,EAAE,eAAe,CAAA;IACnB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,GAAG,OAAO,CAAC,MAAM,CAAC,CAUlB;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE;IAC/C,QAAQ,EAAE,MAAM,CAAA;IAChB,EAAE,EAAE,eAAe,CAAA;IACnB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAAC,CAiBnD;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE;IAC/C,QAAQ,EAAE,MAAM,CAAA;IAChB,EAAE,EAAE,eAAe,CAAA;CACpB,GAAG,OAAO,CAAC,IAAI,CAAC,CAMhB;AAED;;;;GAIG;AACH,wBAAsB,6BAA6B,CAAC,MAAM,EAAE;IAC1D,MAAM,EAAE,MAAM,CAAA;IACd,EAAE,EAAE,eAAe,CAAA;CACpB,GAAG,OAAO,CAAC,IAAI,CAAC,CAGhB"}
@@ -0,0 +1,58 @@
1
+ import { generateOpaqueToken, hashToken } from '../utils/token.js';
2
+ const DEFAULT_REFRESH_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
3
+ /**
4
+ * Issue a new refresh token for the given user.
5
+ * Returns the raw token (caller sends this to the client). The DB stores only the SHA-256 hash.
6
+ */
7
+ export async function issueRefreshToken(params) {
8
+ const { userId, db, ttlMs = DEFAULT_REFRESH_TTL_MS } = params;
9
+ const rawToken = generateOpaqueToken();
10
+ await db.createToken({
11
+ userId,
12
+ type: 'REFRESH',
13
+ token: hashToken(rawToken),
14
+ expiresAt: new Date(Date.now() + ttlMs),
15
+ });
16
+ return rawToken;
17
+ }
18
+ /**
19
+ * Rotate a refresh token: validate the incoming raw token, delete it, and issue a fresh one.
20
+ * Throws on invalid/expired/already-rotated input — these are the same failure mode for the
21
+ * client (they need to re-authenticate).
22
+ */
23
+ export async function rotateRefreshToken(params) {
24
+ const { rawToken, db, ttlMs = DEFAULT_REFRESH_TTL_MS } = params;
25
+ const tokenRecord = await db.findToken(rawToken, 'REFRESH');
26
+ if (!tokenRecord) {
27
+ throw new Error('Invalid or expired refresh token');
28
+ }
29
+ if (tokenRecord.expiresAt < new Date()) {
30
+ await db.deleteToken(tokenRecord.id);
31
+ throw new Error('Invalid or expired refresh token');
32
+ }
33
+ // Rotate: delete old, issue new
34
+ await db.deleteToken(tokenRecord.id);
35
+ const newRawToken = await issueRefreshToken({ userId: tokenRecord.userId, db, ttlMs });
36
+ return { userId: tokenRecord.userId, newRawToken };
37
+ }
38
+ /**
39
+ * Revoke a single refresh token by its raw value. Idempotent — if the token
40
+ * doesn't exist (already revoked or never issued), this is a no-op.
41
+ */
42
+ export async function revokeRefreshToken(params) {
43
+ const { rawToken, db } = params;
44
+ const tokenRecord = await db.findToken(rawToken, 'REFRESH');
45
+ if (tokenRecord) {
46
+ await db.deleteToken(tokenRecord.id);
47
+ }
48
+ }
49
+ /**
50
+ * Revoke every outstanding refresh token for a user. Used by "log out everywhere"
51
+ * and by password-reset / security-event flows where you want to invalidate all
52
+ * device sessions.
53
+ */
54
+ export async function revokeAllRefreshTokensForUser(params) {
55
+ const { userId, db } = params;
56
+ await db.deleteTokensByUserAndType(userId, 'REFRESH');
57
+ }
58
+ //# sourceMappingURL=refresh.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refresh.js","sourceRoot":"","sources":["../../src/features/refresh.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAElE,MAAM,sBAAsB,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA,CAAC,UAAU;AAElE;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,MAIvC;IACC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,GAAG,sBAAsB,EAAE,GAAG,MAAM,CAAA;IAC7D,MAAM,QAAQ,GAAG,mBAAmB,EAAE,CAAA;IACtC,MAAM,EAAE,CAAC,WAAW,CAAC;QACnB,MAAM;QACN,IAAI,EAAE,SAAS;QACf,KAAK,EAAE,SAAS,CAAC,QAAQ,CAAC;QAC1B,SAAS,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC;KACxC,CAAC,CAAA;IACF,OAAO,QAAQ,CAAA;AACjB,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,MAIxC;IACC,MAAM,EAAE,QAAQ,EAAE,EAAE,EAAE,KAAK,GAAG,sBAAsB,EAAE,GAAG,MAAM,CAAA;IAE/D,MAAM,WAAW,GAAiB,MAAM,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAA;IACzE,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAA;IACrD,CAAC;IACD,IAAI,WAAW,CAAC,SAAS,GAAG,IAAI,IAAI,EAAE,EAAE,CAAC;QACvC,MAAM,EAAE,CAAC,WAAW,CAAC,WAAW,CAAC,EAAE,CAAC,CAAA;QACpC,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAA;IACrD,CAAC;IAED,gCAAgC;IAChC,MAAM,EAAE,CAAC,WAAW,CAAC,WAAW,CAAC,EAAE,CAAC,CAAA;IACpC,MAAM,WAAW,GAAG,MAAM,iBAAiB,CAAC,EAAE,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,CAAA;IAEtF,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,WAAW,EAAE,CAAA;AACpD,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,MAGxC;IACC,MAAM,EAAE,QAAQ,EAAE,EAAE,EAAE,GAAG,MAAM,CAAA;IAC/B,MAAM,WAAW,GAAG,MAAM,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAA;IAC3D,IAAI,WAAW,EAAE,CAAC;QAChB,MAAM,EAAE,CAAC,WAAW,CAAC,WAAW,CAAC,EAAE,CAAC,CAAA;IACtC,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,6BAA6B,CAAC,MAGnD;IACC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,GAAG,MAAM,CAAA;IAC7B,MAAM,EAAE,CAAC,yBAAyB,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;AACvD,CAAC"}
@@ -0,0 +1,46 @@
1
+ import type { EmailTemplate } from '@authcore/types';
2
+ /**
3
+ * Default email-template render functions. These match the inline content
4
+ * shipped before 0.10 byte-for-byte so apps that don't override anything see
5
+ * no behavior change.
6
+ *
7
+ * Consumers override individual templates via `EmailConfig.templates`:
8
+ *
9
+ * ```ts
10
+ * createAuth({
11
+ * email: {
12
+ * provider: resendAdapter(...),
13
+ * from: 'auth@app.com',
14
+ * templates: {
15
+ * resetPassword: ({ link, email, ttlHours }) => ({
16
+ * subject: 'Reset your password',
17
+ * html: `<p>Hi ${email},</p><p><a href="${link}">Reset</a></p>`,
18
+ * text: `Reset your password: ${link}`,
19
+ * }),
20
+ * },
21
+ * },
22
+ * })
23
+ * ```
24
+ */
25
+ export declare const defaultVerifyEmailTemplate: EmailTemplate<{
26
+ email: string;
27
+ link: string;
28
+ ttlHours: number;
29
+ }>;
30
+ export declare const defaultResetPasswordTemplate: EmailTemplate<{
31
+ email: string;
32
+ link: string;
33
+ ttlHours: number;
34
+ }>;
35
+ export declare const defaultInvitationTemplate: EmailTemplate<{
36
+ email: string;
37
+ link: string;
38
+ ttlHours: number;
39
+ role: string;
40
+ }>;
41
+ export declare const defaultMagicLinkTemplate: EmailTemplate<{
42
+ email: string;
43
+ link: string;
44
+ ttlMinutes: number;
45
+ }>;
46
+ //# sourceMappingURL=templates.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../../src/features/templates.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAEpD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,eAAO,MAAM,0BAA0B,EAAE,aAAa,CAAC;IACrD,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;CACjB,CAUC,CAAA;AAEF,eAAO,MAAM,4BAA4B,EAAE,aAAa,CAAC;IACvD,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;CACjB,CAUC,CAAA;AAEF,eAAO,MAAM,yBAAyB,EAAE,aAAa,CAAC;IACpD,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;CACb,CASC,CAAA;AAEF,eAAO,MAAM,wBAAwB,EAAE,aAAa,CAAC;IACnD,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;CACnB,CAUC,CAAA"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Default email-template render functions. These match the inline content
3
+ * shipped before 0.10 byte-for-byte so apps that don't override anything see
4
+ * no behavior change.
5
+ *
6
+ * Consumers override individual templates via `EmailConfig.templates`:
7
+ *
8
+ * ```ts
9
+ * createAuth({
10
+ * email: {
11
+ * provider: resendAdapter(...),
12
+ * from: 'auth@app.com',
13
+ * templates: {
14
+ * resetPassword: ({ link, email, ttlHours }) => ({
15
+ * subject: 'Reset your password',
16
+ * html: `<p>Hi ${email},</p><p><a href="${link}">Reset</a></p>`,
17
+ * text: `Reset your password: ${link}`,
18
+ * }),
19
+ * },
20
+ * },
21
+ * })
22
+ * ```
23
+ */
24
+ export const defaultVerifyEmailTemplate = ({ link, ttlHours }) => ({
25
+ subject: 'Verify your email address',
26
+ html: `
27
+ <p>Hello,</p>
28
+ <p>Please verify your email address by clicking the link below:</p>
29
+ <p><a href="${link}">${link}</a></p>
30
+ <p>This link expires in ${ttlHours} hours.</p>
31
+ <p>If you did not create an account, you can ignore this email.</p>
32
+ `,
33
+ text: `Please verify your email by visiting: ${link}\n\nThis link expires in ${ttlHours} hours.`,
34
+ });
35
+ export const defaultResetPasswordTemplate = ({ link, ttlHours }) => ({
36
+ subject: 'Reset your password',
37
+ html: `
38
+ <p>Hello,</p>
39
+ <p>We received a request to reset your password. Click the link below to proceed:</p>
40
+ <p><a href="${link}">${link}</a></p>
41
+ <p>This link expires in ${ttlHours} hour${ttlHours === 1 ? '' : 's'}.</p>
42
+ <p>If you did not request a password reset, you can ignore this email.</p>
43
+ `,
44
+ text: `Reset your password by visiting: ${link}\n\nThis link expires in ${ttlHours} hour${ttlHours === 1 ? '' : 's'}.`,
45
+ });
46
+ export const defaultInvitationTemplate = ({ link, ttlHours }) => ({
47
+ subject: 'You have been invited',
48
+ html: `
49
+ <p>Hello,</p>
50
+ <p>You have been invited to create an account. Click the link below to set your password:</p>
51
+ <p><a href="${link}">${link}</a></p>
52
+ <p>This link expires in ${ttlHours} hours.</p>
53
+ `,
54
+ text: `You have been invited. Set your password by visiting: ${link}\n\nThis link expires in ${ttlHours} hours.`,
55
+ });
56
+ export const defaultMagicLinkTemplate = ({ link, ttlMinutes }) => ({
57
+ subject: 'Sign in to your account',
58
+ html: `
59
+ <p>Hello,</p>
60
+ <p>Click the link below to sign in:</p>
61
+ <p><a href="${link}">${link}</a></p>
62
+ <p>This link expires in ${ttlMinutes} minutes and can only be used once.</p>
63
+ <p>If you did not request this email, you can safely ignore it.</p>
64
+ `,
65
+ text: `Sign in by visiting: ${link}\n\nThis link expires in ${ttlMinutes} minutes and can only be used once.\n\nIf you did not request this email, you can safely ignore it.`,
66
+ });
67
+ //# sourceMappingURL=templates.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"templates.js","sourceRoot":"","sources":["../../src/features/templates.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,MAAM,CAAC,MAAM,0BAA0B,GAIlC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC;IAC5B,OAAO,EAAE,2BAA2B;IACpC,IAAI,EAAE;;;oBAGY,IAAI,KAAK,IAAI;gCACD,QAAQ;;KAEnC;IACH,IAAI,EAAE,yCAAyC,IAAI,4BAA4B,QAAQ,SAAS;CACjG,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,4BAA4B,GAIpC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC;IAC5B,OAAO,EAAE,qBAAqB;IAC9B,IAAI,EAAE;;;oBAGY,IAAI,KAAK,IAAI;gCACD,QAAQ,QAAQ,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG;;KAEpE;IACH,IAAI,EAAE,oCAAoC,IAAI,4BAA4B,QAAQ,QAAQ,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG;CACvH,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,yBAAyB,GAKjC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC;IAC5B,OAAO,EAAE,uBAAuB;IAChC,IAAI,EAAE;;;oBAGY,IAAI,KAAK,IAAI;gCACD,QAAQ;KACnC;IACH,IAAI,EAAE,yDAAyD,IAAI,4BAA4B,QAAQ,SAAS;CACjH,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,wBAAwB,GAIhC,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,CAAC;IAC9B,OAAO,EAAE,yBAAyB;IAClC,IAAI,EAAE;;;oBAGY,IAAI,KAAK,IAAI;gCACD,UAAU;;KAErC;IACH,IAAI,EAAE,wBAAwB,IAAI,4BAA4B,UAAU,qGAAqG;CAC9K,CAAC,CAAA"}