@braedonsaunders/appkit-auth 0.1.3

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/server.js ADDED
@@ -0,0 +1,191 @@
1
+ import { betterAuth } from 'better-auth';
2
+ import { magicLink } from 'better-auth/plugins';
3
+ import { renderMagicLinkEmail, renderPasswordResetEmail } from './mail.js';
4
+ /**
5
+ * Complete persisted authentication runtime extracted from the production
6
+ * reference: password sign-in/reset, hashed one-time magic links, invitation
7
+ * activation, durable sessions/accounts/verifications, client-compatible APIs,
8
+ * and optional social-provider extension. Product delivery and domain effects
9
+ * are injected.
10
+ */
11
+ export function createAppkitAuth(options) {
12
+ const baseURL = normalizeBaseURL(options.baseURL);
13
+ const appName = options.appName.trim();
14
+ if (!appName)
15
+ throw new Error('Authentication appName is required.');
16
+ if (options.secret.length < 32) {
17
+ throw new Error('Authentication secret must contain at least 32 characters.');
18
+ }
19
+ const inviteTTL = options.inviteService?.ttlSeconds;
20
+ const magicTTL = options.magicLink?.expiresIn ?? inviteTTL ?? 15 * 60;
21
+ if (inviteTTL && magicTTL !== inviteTTL) {
22
+ throw new Error('Magic-link and invitation grant expiration must match.');
23
+ }
24
+ return betterAuth({
25
+ ...options.extend,
26
+ database: options.database,
27
+ baseURL,
28
+ secret: options.secret,
29
+ appName,
30
+ user: {
31
+ modelName: 'users',
32
+ fields: {
33
+ emailVerified: 'email_verified',
34
+ createdAt: 'created_at',
35
+ updatedAt: 'updated_at',
36
+ },
37
+ additionalFields: {
38
+ timezone: {
39
+ type: 'string',
40
+ fieldName: 'timezone',
41
+ required: true,
42
+ defaultValue: 'UTC',
43
+ },
44
+ isActive: {
45
+ type: 'boolean',
46
+ fieldName: 'is_active',
47
+ required: true,
48
+ defaultValue: true,
49
+ input: false,
50
+ },
51
+ isSuperAdmin: {
52
+ type: 'boolean',
53
+ fieldName: 'is_super_admin',
54
+ required: true,
55
+ defaultValue: false,
56
+ input: false,
57
+ },
58
+ },
59
+ },
60
+ session: {
61
+ modelName: 'sessions',
62
+ fields: {
63
+ userId: 'user_id',
64
+ expiresAt: 'expires_at',
65
+ ipAddress: 'ip_address',
66
+ userAgent: 'user_agent',
67
+ createdAt: 'created_at',
68
+ updatedAt: 'updated_at',
69
+ },
70
+ additionalFields: {
71
+ activeTenantId: { type: 'string', fieldName: 'active_tenant_id', required: false, input: false },
72
+ impersonatingUserId: { type: 'string', fieldName: 'impersonating_user_id', required: false, input: false },
73
+ impersonationTenantId: { type: 'string', fieldName: 'impersonation_tenant_id', required: false, input: false },
74
+ impersonationStartedAt: { type: 'date', fieldName: 'impersonation_started_at', required: false, input: false },
75
+ impersonationExpiresAt: { type: 'date', fieldName: 'impersonation_expires_at', required: false, input: false },
76
+ impersonationReason: { type: 'string', fieldName: 'impersonation_reason', required: false, input: false },
77
+ },
78
+ expiresIn: options.session?.expiresIn ?? 60 * 60 * 24 * 30,
79
+ updateAge: options.session?.updateAge ?? 60 * 60 * 24,
80
+ cookieCache: {
81
+ enabled: true,
82
+ maxAge: options.session?.cookieCacheMaxAge ?? 60 * 5,
83
+ },
84
+ },
85
+ account: {
86
+ modelName: 'accounts',
87
+ fields: {
88
+ userId: 'user_id',
89
+ accountId: 'account_id',
90
+ providerId: 'provider_id',
91
+ accessToken: 'access_token',
92
+ refreshToken: 'refresh_token',
93
+ idToken: 'id_token',
94
+ accessTokenExpiresAt: 'access_token_expires_at',
95
+ refreshTokenExpiresAt: 'refresh_token_expires_at',
96
+ createdAt: 'created_at',
97
+ updatedAt: 'updated_at',
98
+ },
99
+ encryptOAuthTokens: true,
100
+ },
101
+ verification: {
102
+ modelName: 'verifications',
103
+ fields: {
104
+ expiresAt: 'expires_at',
105
+ createdAt: 'created_at',
106
+ updatedAt: 'updated_at',
107
+ },
108
+ },
109
+ advanced: {
110
+ database: { generateId: 'uuid' },
111
+ },
112
+ emailAndPassword: {
113
+ enabled: true,
114
+ disableSignUp: options.password?.disableSignUp ?? true,
115
+ requireEmailVerification: options.password?.requireEmailVerification ?? false,
116
+ minPasswordLength: options.password?.minLength ?? 8,
117
+ autoSignIn: true,
118
+ resetPasswordTokenExpiresIn: options.password?.resetTokenExpiresIn ?? 60 * 60,
119
+ revokeSessionsOnPasswordReset: true,
120
+ sendResetPassword: async ({ user, url }) => {
121
+ const rendered = (options.renderEmail?.passwordReset ?? renderPasswordResetEmail)({
122
+ appName,
123
+ email: user.email,
124
+ url,
125
+ });
126
+ await options.sendEmail({ kind: 'password-reset', to: user.email, ...rendered });
127
+ },
128
+ },
129
+ plugins: [
130
+ magicLink({
131
+ disableSignUp: options.magicLink?.disableSignUp ?? true,
132
+ expiresIn: magicTTL,
133
+ storeToken: 'hashed',
134
+ sendMagicLink: async ({ email, url, metadata }) => {
135
+ const invitation = metadata?.flow === 'invite';
136
+ const tenantName = typeof metadata?.tenantName === 'string' && metadata.tenantName.trim()
137
+ ? metadata.tenantName.trim()
138
+ : null;
139
+ const rendered = (options.renderEmail?.magicLink ?? renderMagicLinkEmail)({
140
+ appName,
141
+ email,
142
+ url,
143
+ invitation,
144
+ tenantName,
145
+ });
146
+ await options.sendEmail({
147
+ kind: invitation ? 'invitation' : 'magic-link',
148
+ to: email,
149
+ ...rendered,
150
+ });
151
+ },
152
+ }),
153
+ ],
154
+ hooks: {
155
+ after: async (rawContext) => {
156
+ const context = rawContext;
157
+ if (context.path !== '/magic-link/verify' || !options.inviteService)
158
+ return {};
159
+ const sessionUserId = context.context.newSession?.user.id;
160
+ if (!sessionUserId)
161
+ return {};
162
+ const grant = options.inviteService.inviteGrantFromCallbackURL(context.query?.callbackURL, baseURL);
163
+ if (!grant)
164
+ return {};
165
+ // Preserve the valid session/callback if a downstream domain hook fails;
166
+ // the membership remains unchanged and the callback can show recovery.
167
+ await options.inviteService.acceptInviteAfterMagicLink(grant, sessionUserId).catch((error) => {
168
+ if (options.onInviteAcceptanceError)
169
+ options.onInviteAcceptanceError(error);
170
+ else
171
+ console.error('[appkit/auth] invitation acceptance failed', error);
172
+ });
173
+ return {};
174
+ },
175
+ },
176
+ trustedOrigins: options.trustedOrigins ?? [baseURL],
177
+ });
178
+ }
179
+ /** Lazy singleton pattern used by server runtimes that must import safely at build time. */
180
+ export function createLazyAuth(factory) {
181
+ let instance;
182
+ return () => (instance ??= factory());
183
+ }
184
+ function normalizeBaseURL(value) {
185
+ const url = new URL(value);
186
+ if (url.username || url.password || url.search || url.hash) {
187
+ throw new Error('Authentication baseURL must be an origin without credentials, query, or hash.');
188
+ }
189
+ return url.origin;
190
+ }
191
+ //# sourceMappingURL=server.js.map
package/server.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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","sourcesContent":["import { betterAuth } from 'better-auth'\nimport { magicLink } from 'better-auth/plugins'\nimport type { AuthEmailRenderer, AuthEmailSender } from './mail'\nimport { renderMagicLinkEmail, renderPasswordResetEmail } from './mail'\nimport type { InviteService } from './invites'\n\ntype BetterAuthOptions = Parameters<typeof betterAuth>[0]\n\nexport type AppkitAuthOptions = {\n database: NonNullable<BetterAuthOptions['database']>\n baseURL: string\n secret: string\n appName: string\n sendEmail: AuthEmailSender\n renderEmail?: AuthEmailRenderer\n inviteService?: InviteService\n onInviteAcceptanceError?: (error: unknown) => void\n trustedOrigins?: BetterAuthOptions['trustedOrigins']\n session?: {\n expiresIn?: number\n updateAge?: number\n cookieCacheMaxAge?: number\n }\n password?: {\n disableSignUp?: boolean\n requireEmailVerification?: boolean\n minLength?: number\n resetTokenExpiresIn?: number\n }\n magicLink?: {\n expiresIn?: number\n disableSignUp?: boolean\n }\n /** Additional upstream options such as social providers and database hooks. */\n extend?: Omit<\n BetterAuthOptions,\n | 'database'\n | 'baseURL'\n | 'secret'\n | 'appName'\n | 'trustedOrigins'\n | 'user'\n | 'session'\n | 'account'\n | 'verification'\n | 'emailAndPassword'\n | 'plugins'\n | 'hooks'\n | 'advanced'\n >\n}\n\n/**\n * Complete persisted authentication runtime extracted from the production\n * reference: password sign-in/reset, hashed one-time magic links, invitation\n * activation, durable sessions/accounts/verifications, client-compatible APIs,\n * and optional social-provider extension. Product delivery and domain effects\n * are injected.\n */\nexport function createAppkitAuth(options: AppkitAuthOptions) {\n const baseURL = normalizeBaseURL(options.baseURL)\n const appName = options.appName.trim()\n if (!appName) throw new Error('Authentication appName is required.')\n if (options.secret.length < 32) {\n throw new Error('Authentication secret must contain at least 32 characters.')\n }\n const inviteTTL = options.inviteService?.ttlSeconds\n const magicTTL = options.magicLink?.expiresIn ?? inviteTTL ?? 15 * 60\n if (inviteTTL && magicTTL !== inviteTTL) {\n throw new Error('Magic-link and invitation grant expiration must match.')\n }\n\n return betterAuth({\n ...options.extend,\n database: options.database,\n baseURL,\n secret: options.secret,\n appName,\n user: {\n modelName: 'users',\n fields: {\n emailVerified: 'email_verified',\n createdAt: 'created_at',\n updatedAt: 'updated_at',\n },\n additionalFields: {\n timezone: {\n type: 'string',\n fieldName: 'timezone',\n required: true,\n defaultValue: 'UTC',\n },\n isActive: {\n type: 'boolean',\n fieldName: 'is_active',\n required: true,\n defaultValue: true,\n input: false,\n },\n isSuperAdmin: {\n type: 'boolean',\n fieldName: 'is_super_admin',\n required: true,\n defaultValue: false,\n input: false,\n },\n },\n },\n session: {\n modelName: 'sessions',\n fields: {\n userId: 'user_id',\n expiresAt: 'expires_at',\n ipAddress: 'ip_address',\n userAgent: 'user_agent',\n createdAt: 'created_at',\n updatedAt: 'updated_at',\n },\n additionalFields: {\n activeTenantId: { type: 'string', fieldName: 'active_tenant_id', required: false, input: false },\n impersonatingUserId: { type: 'string', fieldName: 'impersonating_user_id', required: false, input: false },\n impersonationTenantId: { type: 'string', fieldName: 'impersonation_tenant_id', required: false, input: false },\n impersonationStartedAt: { type: 'date', fieldName: 'impersonation_started_at', required: false, input: false },\n impersonationExpiresAt: { type: 'date', fieldName: 'impersonation_expires_at', required: false, input: false },\n impersonationReason: { type: 'string', fieldName: 'impersonation_reason', required: false, input: false },\n },\n expiresIn: options.session?.expiresIn ?? 60 * 60 * 24 * 30,\n updateAge: options.session?.updateAge ?? 60 * 60 * 24,\n cookieCache: {\n enabled: true,\n maxAge: options.session?.cookieCacheMaxAge ?? 60 * 5,\n },\n },\n account: {\n modelName: 'accounts',\n fields: {\n userId: 'user_id',\n accountId: 'account_id',\n providerId: 'provider_id',\n accessToken: 'access_token',\n refreshToken: 'refresh_token',\n idToken: 'id_token',\n accessTokenExpiresAt: 'access_token_expires_at',\n refreshTokenExpiresAt: 'refresh_token_expires_at',\n createdAt: 'created_at',\n updatedAt: 'updated_at',\n },\n encryptOAuthTokens: true,\n },\n verification: {\n modelName: 'verifications',\n fields: {\n expiresAt: 'expires_at',\n createdAt: 'created_at',\n updatedAt: 'updated_at',\n },\n },\n advanced: {\n database: { generateId: 'uuid' },\n },\n emailAndPassword: {\n enabled: true,\n disableSignUp: options.password?.disableSignUp ?? true,\n requireEmailVerification: options.password?.requireEmailVerification ?? false,\n minPasswordLength: options.password?.minLength ?? 8,\n autoSignIn: true,\n resetPasswordTokenExpiresIn: options.password?.resetTokenExpiresIn ?? 60 * 60,\n revokeSessionsOnPasswordReset: true,\n sendResetPassword: async ({ user, url }) => {\n const rendered = (options.renderEmail?.passwordReset ?? renderPasswordResetEmail)({\n appName,\n email: user.email,\n url,\n })\n await options.sendEmail({ kind: 'password-reset', to: user.email, ...rendered })\n },\n },\n plugins: [\n magicLink({\n disableSignUp: options.magicLink?.disableSignUp ?? true,\n expiresIn: magicTTL,\n storeToken: 'hashed',\n sendMagicLink: async ({ email, url, metadata }) => {\n const invitation = metadata?.flow === 'invite'\n const tenantName =\n typeof metadata?.tenantName === 'string' && metadata.tenantName.trim()\n ? metadata.tenantName.trim()\n : null\n const rendered = (options.renderEmail?.magicLink ?? renderMagicLinkEmail)({\n appName,\n email,\n url,\n invitation,\n tenantName,\n })\n await options.sendEmail({\n kind: invitation ? 'invitation' : 'magic-link',\n to: email,\n ...rendered,\n })\n },\n }),\n ],\n hooks: {\n after: async (rawContext) => {\n const context = rawContext as unknown as {\n path?: string\n query?: { callbackURL?: unknown }\n context: { newSession?: { user: { id: string } } | null }\n }\n if (context.path !== '/magic-link/verify' || !options.inviteService) return {}\n const sessionUserId = context.context.newSession?.user.id\n if (!sessionUserId) return {}\n const grant = options.inviteService.inviteGrantFromCallbackURL(\n context.query?.callbackURL,\n baseURL,\n )\n if (!grant) return {}\n // Preserve the valid session/callback if a downstream domain hook fails;\n // the membership remains unchanged and the callback can show recovery.\n await options.inviteService.acceptInviteAfterMagicLink(grant, sessionUserId).catch((error) => {\n if (options.onInviteAcceptanceError) options.onInviteAcceptanceError(error)\n else console.error('[appkit/auth] invitation acceptance failed', error)\n })\n return {}\n },\n },\n trustedOrigins: options.trustedOrigins ?? [baseURL],\n })\n}\n\nexport type AppkitAuth = ReturnType<typeof createAppkitAuth>\n\n/** Lazy singleton pattern used by server runtimes that must import safely at build time. */\nexport function createLazyAuth(factory: () => AppkitAuth): () => AppkitAuth {\n let instance: AppkitAuth | undefined\n return () => (instance ??= factory())\n}\n\nfunction normalizeBaseURL(value: string): string {\n const url = new URL(value)\n if (url.username || url.password || url.search || url.hash) {\n throw new Error('Authentication baseURL must be an origin without credentials, query, or hash.')\n }\n return url.origin\n}\n"]}