@originals/auth 1.5.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 (59) hide show
  1. package/.turbo/turbo-build.log +1 -0
  2. package/dist/client/index.d.ts +22 -0
  3. package/dist/client/index.d.ts.map +1 -0
  4. package/dist/client/index.js +22 -0
  5. package/dist/client/index.js.map +1 -0
  6. package/dist/client/turnkey-client.d.ts +53 -0
  7. package/dist/client/turnkey-client.d.ts.map +1 -0
  8. package/dist/client/turnkey-client.js +268 -0
  9. package/dist/client/turnkey-client.js.map +1 -0
  10. package/dist/client/turnkey-did-signer.d.ts +54 -0
  11. package/dist/client/turnkey-did-signer.d.ts.map +1 -0
  12. package/dist/client/turnkey-did-signer.js +125 -0
  13. package/dist/client/turnkey-did-signer.js.map +1 -0
  14. package/dist/index.d.ts +23 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +27 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/server/email-auth.d.ts +42 -0
  19. package/dist/server/email-auth.d.ts.map +1 -0
  20. package/dist/server/email-auth.js +187 -0
  21. package/dist/server/email-auth.js.map +1 -0
  22. package/dist/server/index.d.ts +22 -0
  23. package/dist/server/index.d.ts.map +1 -0
  24. package/dist/server/index.js +22 -0
  25. package/dist/server/index.js.map +1 -0
  26. package/dist/server/jwt.d.ts +49 -0
  27. package/dist/server/jwt.d.ts.map +1 -0
  28. package/dist/server/jwt.js +113 -0
  29. package/dist/server/jwt.js.map +1 -0
  30. package/dist/server/middleware.d.ts +39 -0
  31. package/dist/server/middleware.d.ts.map +1 -0
  32. package/dist/server/middleware.js +110 -0
  33. package/dist/server/middleware.js.map +1 -0
  34. package/dist/server/turnkey-client.d.ts +24 -0
  35. package/dist/server/turnkey-client.d.ts.map +1 -0
  36. package/dist/server/turnkey-client.js +118 -0
  37. package/dist/server/turnkey-client.js.map +1 -0
  38. package/dist/server/turnkey-signer.d.ts +40 -0
  39. package/dist/server/turnkey-signer.d.ts.map +1 -0
  40. package/dist/server/turnkey-signer.js +121 -0
  41. package/dist/server/turnkey-signer.js.map +1 -0
  42. package/dist/types.d.ts +155 -0
  43. package/dist/types.d.ts.map +1 -0
  44. package/dist/types.js +5 -0
  45. package/dist/types.js.map +1 -0
  46. package/package.json +79 -0
  47. package/src/client/index.ts +37 -0
  48. package/src/client/turnkey-client.ts +340 -0
  49. package/src/client/turnkey-did-signer.ts +189 -0
  50. package/src/index.ts +32 -0
  51. package/src/server/email-auth.ts +258 -0
  52. package/src/server/index.ts +38 -0
  53. package/src/server/jwt.ts +154 -0
  54. package/src/server/middleware.ts +136 -0
  55. package/src/server/turnkey-client.ts +152 -0
  56. package/src/server/turnkey-signer.ts +170 -0
  57. package/src/types.ts +168 -0
  58. package/tests/index.test.ts +25 -0
  59. package/tsconfig.json +28 -0
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Turnkey Email Authentication Service
3
+ * Implements email-based authentication using Turnkey's OTP flow
4
+ */
5
+
6
+ import { Turnkey } from '@turnkey/sdk-server';
7
+ import { sha256 } from '@noble/hashes/sha2.js';
8
+ import { bytesToHex } from '@noble/hashes/utils.js';
9
+ import type { EmailAuthSession, InitiateAuthResult, VerifyAuthResult } from '../types';
10
+ import { getOrCreateTurnkeySubOrg } from './turnkey-client';
11
+
12
+ // Session timeout (15 minutes to match Turnkey OTP)
13
+ const SESSION_TIMEOUT = 15 * 60 * 1000;
14
+
15
+ /**
16
+ * Session storage interface for pluggable session management
17
+ */
18
+ export interface SessionStorage {
19
+ get(sessionId: string): EmailAuthSession | undefined;
20
+ set(sessionId: string, session: EmailAuthSession): void;
21
+ delete(sessionId: string): void;
22
+ cleanup(): void;
23
+ }
24
+
25
+ /**
26
+ * Create an in-memory session storage
27
+ * For production, consider using Redis or a database
28
+ */
29
+ export function createInMemorySessionStorage(): SessionStorage {
30
+ const sessions = new Map<string, EmailAuthSession>();
31
+
32
+ // Start cleanup interval
33
+ const cleanupInterval = setInterval(() => {
34
+ const now = Date.now();
35
+ for (const [sessionId, session] of sessions.entries()) {
36
+ if (now - session.timestamp > SESSION_TIMEOUT) {
37
+ sessions.delete(sessionId);
38
+ }
39
+ }
40
+ }, 60 * 1000);
41
+
42
+ // Keep the interval from preventing process exit
43
+ if (cleanupInterval.unref) {
44
+ cleanupInterval.unref();
45
+ }
46
+
47
+ return {
48
+ get: (sessionId: string) => sessions.get(sessionId),
49
+ set: (sessionId: string, session: EmailAuthSession) => sessions.set(sessionId, session),
50
+ delete: (sessionId: string) => sessions.delete(sessionId),
51
+ cleanup: () => {
52
+ clearInterval(cleanupInterval);
53
+ sessions.clear();
54
+ },
55
+ };
56
+ }
57
+
58
+ // Default session storage
59
+ let defaultSessionStorage: SessionStorage | null = null;
60
+
61
+ function getDefaultSessionStorage(): SessionStorage {
62
+ if (!defaultSessionStorage) {
63
+ defaultSessionStorage = createInMemorySessionStorage();
64
+ }
65
+ return defaultSessionStorage;
66
+ }
67
+
68
+ /**
69
+ * Generate a random session ID
70
+ */
71
+ function generateSessionId(): string {
72
+ return `session_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
73
+ }
74
+
75
+ /**
76
+ * Initiate email authentication using Turnkey OTP
77
+ * Sends a 6-digit OTP code to the user's email
78
+ */
79
+ export async function initiateEmailAuth(
80
+ email: string,
81
+ turnkeyClient: Turnkey,
82
+ sessionStorage?: SessionStorage
83
+ ): Promise<InitiateAuthResult> {
84
+ const storage = sessionStorage ?? getDefaultSessionStorage();
85
+
86
+ // Validate email format
87
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
88
+ if (!emailRegex.test(email)) {
89
+ throw new Error('Invalid email format');
90
+ }
91
+
92
+ console.log(`\n🚀 Initiating email auth for: ${email}`);
93
+
94
+ // Step 1: Get or create Turnkey sub-organization
95
+ const subOrgId = await getOrCreateTurnkeySubOrg(email, turnkeyClient);
96
+
97
+ // Step 2: Send OTP via Turnkey
98
+ console.log(`📨 Sending OTP to ${email} via Turnkey...`);
99
+
100
+ // Generate a unique user identifier for rate limiting
101
+ const data = new TextEncoder().encode(email);
102
+ const hash = sha256(data);
103
+ const userIdentifier = bytesToHex(hash);
104
+
105
+ const otpResult = await turnkeyClient.apiClient().initOtp({
106
+ otpType: 'OTP_TYPE_EMAIL',
107
+ contact: email,
108
+ userIdentifier: userIdentifier,
109
+ appName: 'Originals',
110
+ otpLength: 6,
111
+ alphanumeric: false,
112
+ });
113
+
114
+ const otpId = otpResult.otpId;
115
+
116
+ if (!otpId) {
117
+ throw new Error('Failed to initiate OTP - no OTP ID returned');
118
+ }
119
+
120
+ console.log(`✅ OTP sent! OTP ID: ${otpId}`);
121
+
122
+ // Create auth session
123
+ const sessionId = generateSessionId();
124
+ storage.set(sessionId, {
125
+ email,
126
+ subOrgId,
127
+ otpId,
128
+ timestamp: Date.now(),
129
+ verified: false,
130
+ });
131
+
132
+ console.log('='.repeat(60));
133
+ console.log(`📧 Check ${email} for the verification code!`);
134
+ console.log(` Session ID: ${sessionId}`);
135
+ console.log(` Valid for: 15 minutes`);
136
+ console.log('='.repeat(60) + '\n');
137
+
138
+ return {
139
+ sessionId,
140
+ message: 'Verification code sent to your email. Check your inbox!',
141
+ };
142
+ }
143
+
144
+ /**
145
+ * Verify email authentication code using Turnkey OTP
146
+ */
147
+ export async function verifyEmailAuth(
148
+ sessionId: string,
149
+ code: string,
150
+ turnkeyClient: Turnkey,
151
+ sessionStorage?: SessionStorage
152
+ ): Promise<VerifyAuthResult> {
153
+ const storage = sessionStorage ?? getDefaultSessionStorage();
154
+ const session = storage.get(sessionId);
155
+
156
+ if (!session) {
157
+ throw new Error('Invalid or expired session');
158
+ }
159
+
160
+ // Check if session has expired
161
+ if (Date.now() - session.timestamp > SESSION_TIMEOUT) {
162
+ storage.delete(sessionId);
163
+ throw new Error('Session expired. Please request a new code.');
164
+ }
165
+
166
+ if (!session.otpId) {
167
+ throw new Error('OTP ID not found in session');
168
+ }
169
+
170
+ if (!session.subOrgId) {
171
+ throw new Error('Sub-organization ID not found');
172
+ }
173
+
174
+ console.log(`\n🔐 Verifying OTP for session ${sessionId}...`);
175
+
176
+ try {
177
+ // Verify the OTP code with Turnkey
178
+ const verifyResult = await turnkeyClient.apiClient().verifyOtp({
179
+ otpId: session.otpId,
180
+ otpCode: code,
181
+ expirationSeconds: '900', // 15 minutes
182
+ });
183
+
184
+ if (!verifyResult.verificationToken) {
185
+ throw new Error('OTP verification failed - no verification token returned');
186
+ }
187
+
188
+ console.log(`✅ OTP verified successfully!`);
189
+
190
+ // Mark session as verified
191
+ session.verified = true;
192
+ storage.set(sessionId, session);
193
+
194
+ return {
195
+ verified: true,
196
+ email: session.email,
197
+ subOrgId: session.subOrgId,
198
+ };
199
+ } catch (error) {
200
+ console.error('❌ OTP verification failed:', error);
201
+ throw new Error(
202
+ `Invalid verification code: ${error instanceof Error ? error.message : String(error)}`
203
+ );
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Check if a session is verified
209
+ */
210
+ export function isSessionVerified(
211
+ sessionId: string,
212
+ sessionStorage?: SessionStorage
213
+ ): boolean {
214
+ const storage = sessionStorage ?? getDefaultSessionStorage();
215
+ const session = storage.get(sessionId);
216
+
217
+ if (!session) return false;
218
+
219
+ if (Date.now() - session.timestamp > SESSION_TIMEOUT) {
220
+ storage.delete(sessionId);
221
+ return false;
222
+ }
223
+
224
+ return session.verified;
225
+ }
226
+
227
+ /**
228
+ * Clean up a session after successful login
229
+ */
230
+ export function cleanupSession(
231
+ sessionId: string,
232
+ sessionStorage?: SessionStorage
233
+ ): void {
234
+ const storage = sessionStorage ?? getDefaultSessionStorage();
235
+ storage.delete(sessionId);
236
+ }
237
+
238
+ /**
239
+ * Get session data
240
+ */
241
+ export function getSession(
242
+ sessionId: string,
243
+ sessionStorage?: SessionStorage
244
+ ): EmailAuthSession | undefined {
245
+ const storage = sessionStorage ?? getDefaultSessionStorage();
246
+ const session = storage.get(sessionId);
247
+
248
+ if (!session) return undefined;
249
+
250
+ // Check if expired
251
+ if (Date.now() - session.timestamp > SESSION_TIMEOUT) {
252
+ storage.delete(sessionId);
253
+ return undefined;
254
+ }
255
+
256
+ return session;
257
+ }
258
+
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Server-side authentication utilities
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * import {
7
+ * createAuthMiddleware,
8
+ * initiateEmailAuth,
9
+ * verifyEmailAuth,
10
+ * signToken,
11
+ * verifyToken,
12
+ * createTurnkeyClient,
13
+ * TurnkeyWebVHSigner
14
+ * } from '@originals/auth/server';
15
+ * ```
16
+ */
17
+
18
+ export { createTurnkeyClient, getOrCreateTurnkeySubOrg } from './turnkey-client';
19
+ export {
20
+ initiateEmailAuth,
21
+ verifyEmailAuth,
22
+ isSessionVerified,
23
+ cleanupSession,
24
+ getSession,
25
+ type SessionStorage,
26
+ createInMemorySessionStorage,
27
+ } from './email-auth';
28
+ export {
29
+ signToken,
30
+ verifyToken,
31
+ getAuthCookieConfig,
32
+ getClearAuthCookieConfig,
33
+ } from './jwt';
34
+ export { createAuthMiddleware } from './middleware';
35
+ export { TurnkeyWebVHSigner, createTurnkeySigner } from './turnkey-signer';
36
+
37
+
38
+
@@ -0,0 +1,154 @@
1
+ /**
2
+ * JWT Authentication Module
3
+ * Implements secure token issuance and validation with HTTP-only cookies
4
+ */
5
+
6
+ import jwt from 'jsonwebtoken';
7
+ import type { TokenPayload, AuthCookieConfig } from '../types';
8
+
9
+ // 7 days in seconds
10
+ const DEFAULT_JWT_EXPIRES_IN = 7 * 24 * 60 * 60;
11
+
12
+ /**
13
+ * Get JWT secret from config or environment
14
+ */
15
+ function getJwtSecret(configSecret?: string): string {
16
+ const secret = configSecret ?? process.env.JWT_SECRET;
17
+ if (!secret) {
18
+ throw new Error('JWT_SECRET environment variable is required');
19
+ }
20
+ return secret;
21
+ }
22
+
23
+ /**
24
+ * Sign a JWT token for a user
25
+ * @param subOrgId - Turnkey sub-organization ID (stable identifier)
26
+ * @param email - User email (metadata)
27
+ * @param sessionToken - Optional Turnkey session token for user authentication
28
+ * @param options - Additional options
29
+ * @returns Signed JWT token string
30
+ */
31
+ export function signToken(
32
+ subOrgId: string,
33
+ email: string,
34
+ sessionToken?: string,
35
+ options?: {
36
+ secret?: string;
37
+ expiresIn?: number;
38
+ issuer?: string;
39
+ audience?: string;
40
+ }
41
+ ): string {
42
+ if (!subOrgId) {
43
+ throw new Error('Sub-organization ID is required for token signing');
44
+ }
45
+
46
+ const secret = getJwtSecret(options?.secret);
47
+
48
+ const payload: Record<string, unknown> = {
49
+ sub: subOrgId,
50
+ email,
51
+ };
52
+
53
+ if (sessionToken) {
54
+ payload.sessionToken = sessionToken;
55
+ }
56
+
57
+ const signOptions: jwt.SignOptions = {
58
+ expiresIn: options?.expiresIn ?? DEFAULT_JWT_EXPIRES_IN,
59
+ issuer: options?.issuer ?? 'originals-auth',
60
+ audience: options?.audience ?? 'originals-api',
61
+ };
62
+
63
+ return jwt.sign(payload, secret, signOptions);
64
+ }
65
+
66
+ /**
67
+ * Verify and decode a JWT token
68
+ * @param token - JWT token string
69
+ * @param options - Additional options
70
+ * @returns Decoded token payload
71
+ * @throws Error if token is invalid or expired
72
+ */
73
+ export function verifyToken(
74
+ token: string,
75
+ options?: {
76
+ secret?: string;
77
+ issuer?: string;
78
+ audience?: string;
79
+ }
80
+ ): TokenPayload {
81
+ const secret = getJwtSecret(options?.secret);
82
+
83
+ try {
84
+ const payload = jwt.verify(token, secret, {
85
+ issuer: options?.issuer ?? 'originals-auth',
86
+ audience: options?.audience ?? 'originals-api',
87
+ }) as TokenPayload;
88
+
89
+ if (!payload.sub) {
90
+ throw new Error('Token missing sub-organization ID');
91
+ }
92
+
93
+ return payload;
94
+ } catch (error) {
95
+ if (error instanceof jwt.TokenExpiredError) {
96
+ throw new Error('Token has expired');
97
+ }
98
+ if (error instanceof jwt.JsonWebTokenError) {
99
+ throw new Error('Invalid token');
100
+ }
101
+ throw error;
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Generate a secure cookie configuration for authentication tokens
107
+ * @param token - JWT token to set in cookie
108
+ * @param options - Cookie options
109
+ * @returns Cookie configuration object
110
+ */
111
+ export function getAuthCookieConfig(
112
+ token: string,
113
+ options?: {
114
+ cookieName?: string;
115
+ maxAge?: number;
116
+ secure?: boolean;
117
+ }
118
+ ): AuthCookieConfig {
119
+ const isProduction = process.env.NODE_ENV === 'production';
120
+
121
+ return {
122
+ name: options?.cookieName ?? 'auth_token',
123
+ value: token,
124
+ options: {
125
+ httpOnly: true, // Cannot be accessed by JavaScript (XSS protection)
126
+ secure: options?.secure ?? isProduction, // HTTPS only in production
127
+ sameSite: 'strict', // CSRF protection
128
+ maxAge: options?.maxAge ?? 7 * 24 * 60 * 60 * 1000, // 7 days in milliseconds
129
+ path: '/', // Available for all routes
130
+ },
131
+ };
132
+ }
133
+
134
+ /**
135
+ * Get cookie configuration for logout (clears the auth cookie)
136
+ * @param cookieName - Name of the cookie to clear
137
+ * @returns Cookie configuration for clearing
138
+ */
139
+ export function getClearAuthCookieConfig(cookieName?: string): AuthCookieConfig {
140
+ const isProduction = process.env.NODE_ENV === 'production';
141
+
142
+ return {
143
+ name: cookieName ?? 'auth_token',
144
+ value: '',
145
+ options: {
146
+ httpOnly: true,
147
+ secure: isProduction,
148
+ sameSite: 'strict',
149
+ maxAge: 0, // Expire immediately
150
+ path: '/',
151
+ },
152
+ };
153
+ }
154
+
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Express authentication middleware factory
3
+ */
4
+
5
+ import type { Request, Response, NextFunction } from 'express';
6
+ import { verifyToken } from './jwt';
7
+ import type { AuthMiddlewareOptions, AuthUser, AuthenticatedRequest } from '../types';
8
+
9
+ /**
10
+ * Create an authentication middleware for Express
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * import { createAuthMiddleware } from '@originals/auth/server';
15
+ *
16
+ * const authenticateUser = createAuthMiddleware({
17
+ * getUserByTurnkeyId: async (turnkeyId) => {
18
+ * return db.query.users.findFirst({
19
+ * where: eq(users.turnkeySubOrgId, turnkeyId)
20
+ * });
21
+ * },
22
+ * createUser: async (turnkeyId, email, temporaryDid) => {
23
+ * return db.insert(users).values({
24
+ * turnkeySubOrgId: turnkeyId,
25
+ * email,
26
+ * did: temporaryDid,
27
+ * }).returning().then(rows => rows[0]);
28
+ * }
29
+ * });
30
+ *
31
+ * app.get('/api/protected', authenticateUser, (req, res) => {
32
+ * res.json({ user: req.user });
33
+ * });
34
+ * ```
35
+ */
36
+ export function createAuthMiddleware(
37
+ options: AuthMiddlewareOptions
38
+ ): (req: Request, res: Response, next: NextFunction) => Promise<void | Response> {
39
+ const cookieName = options.cookieName ?? 'auth_token';
40
+
41
+ return async (req: Request, res: Response, next: NextFunction): Promise<void | Response> => {
42
+ try {
43
+ // Get JWT token from HTTP-only cookie
44
+ const token = req.cookies?.[cookieName];
45
+
46
+ if (!token) {
47
+ return res.status(401).json({ error: 'Not authenticated' });
48
+ }
49
+
50
+ // Verify JWT token
51
+ const payload = verifyToken(token, { secret: options.jwtSecret });
52
+ const turnkeySubOrgId = payload.sub;
53
+ const email = payload.email;
54
+
55
+ // Check if user already exists
56
+ let user: AuthUser | null = await options.getUserByTurnkeyId(turnkeySubOrgId);
57
+
58
+ // If user doesn't exist and createUser is provided, create user
59
+ if (!user && options.createUser) {
60
+ console.log(`Creating user record for ${email}...`);
61
+
62
+ // Use temporary DID as placeholder until user creates real DID
63
+ const temporaryDid = `temp:turnkey:${turnkeySubOrgId}`;
64
+
65
+ user = await options.createUser(turnkeySubOrgId, email, temporaryDid);
66
+
67
+ console.log(`✅ User created: ${email}`);
68
+ console.log(` Turnkey sub-org ID: ${turnkeySubOrgId}`);
69
+ console.log(` Temporary DID: ${temporaryDid}`);
70
+ }
71
+
72
+ if (!user) {
73
+ return res.status(401).json({ error: 'User not found' });
74
+ }
75
+
76
+ // Add user info to request
77
+ (req as Request & AuthenticatedRequest).user = {
78
+ id: user.id,
79
+ turnkeySubOrgId,
80
+ email,
81
+ did: user.did,
82
+ sessionToken: payload.sessionToken,
83
+ };
84
+
85
+ next();
86
+ } catch (error) {
87
+ console.error('Authentication error:', error);
88
+ return res.status(401).json({ error: 'Invalid or expired token' });
89
+ }
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Optional authentication middleware - doesn't fail if not authenticated
95
+ * Attaches user to request if valid token exists, otherwise continues without user
96
+ */
97
+ export function createOptionalAuthMiddleware(
98
+ options: AuthMiddlewareOptions
99
+ ): (req: Request, res: Response, next: NextFunction) => Promise<void> {
100
+ const cookieName = options.cookieName ?? 'auth_token';
101
+
102
+ return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
103
+ try {
104
+ const token = req.cookies?.[cookieName];
105
+
106
+ if (!token) {
107
+ next();
108
+ return;
109
+ }
110
+
111
+ const payload = verifyToken(token, { secret: options.jwtSecret });
112
+ const turnkeySubOrgId = payload.sub;
113
+ const email = payload.email;
114
+
115
+ const user = await options.getUserByTurnkeyId(turnkeySubOrgId);
116
+
117
+ if (user) {
118
+ (req as Request & AuthenticatedRequest).user = {
119
+ id: user.id,
120
+ turnkeySubOrgId,
121
+ email,
122
+ did: user.did,
123
+ sessionToken: payload.sessionToken,
124
+ };
125
+ }
126
+
127
+ next();
128
+ } catch {
129
+ // Token invalid or expired, continue without user
130
+ next();
131
+ }
132
+ };
133
+ }
134
+
135
+
136
+