@draftlab/auth 0.11.0 → 0.13.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.
@@ -174,22 +174,49 @@ interface PasswordConfig {
174
174
  * Callback for sending verification codes to users via email.
175
175
  * Implement this to integrate with your email service provider.
176
176
  *
177
+ * The context parameter indicates why the code is being sent:
178
+ * - "register": User is registering for the first time
179
+ * - "register:resend": User requested to resend registration code
180
+ * - "reset": User is resetting their password
181
+ * - "reset:resend": User requested to resend password reset code
182
+ *
177
183
  * @param email - The recipient's email address
178
184
  * @param code - The verification code to send
185
+ * @param context - The context of why the code is being sent
179
186
  * @returns Promise that resolves when email is sent
180
187
  *
181
188
  * @example
182
189
  * ```ts
183
- * sendCode: async (email, code) => {
190
+ * sendCode: async (email, code, context) => {
191
+ * const templates = {
192
+ * "register": {
193
+ * subject: "Welcome! Verify your email",
194
+ * body: `Welcome! Your verification code is: ${code}`
195
+ * },
196
+ * "register:resend": {
197
+ * subject: "Your verification code (resent)",
198
+ * body: `Here's your code again: ${code}`
199
+ * },
200
+ * "reset": {
201
+ * subject: "Reset your password",
202
+ * body: `Your password reset code is: ${code}`
203
+ * },
204
+ * "reset:resend": {
205
+ * subject: "Password reset code (resent)",
206
+ * body: `Here's your reset code again: ${code}`
207
+ * }
208
+ * }
209
+ *
210
+ * const template = templates[context]
184
211
  * await emailService.send({
185
212
  * to: email,
186
- * subject: 'Your verification code',
187
- * text: `Your verification code is: ${code}`
213
+ * subject: template.subject,
214
+ * text: template.body
188
215
  * })
189
216
  * }
190
217
  * ```
191
218
  */
192
- sendCode: (email: string, code: string) => Promise<void>;
219
+ sendCode: (email: string, code: string, context: "register" | "register:resend" | "reset" | "reset:resend") => Promise<void>;
193
220
  /**
194
221
  * Optional password validation function or schema.
195
222
  * Can be either a validation function or a standard-schema validator.
@@ -129,7 +129,7 @@ const PasswordProvider = (config) => {
129
129
  "password"
130
130
  ])) return transition(provider, { type: "email_taken" });
131
131
  const code = generateCode();
132
- await config.sendCode(email, code);
132
+ await config.sendCode(email, code, "register");
133
133
  return transition({
134
134
  type: "code",
135
135
  code,
@@ -139,7 +139,7 @@ const PasswordProvider = (config) => {
139
139
  }
140
140
  if (action === "register" && provider.type === "code") {
141
141
  const code = generateCode();
142
- await config.sendCode(provider.email, code);
142
+ await config.sendCode(provider.email, code, "register:resend");
143
143
  return transition({
144
144
  type: "code",
145
145
  code,
@@ -170,7 +170,7 @@ const PasswordProvider = (config) => {
170
170
  routes.get("/change", async (c) => {
171
171
  const state = {
172
172
  type: "start",
173
- redirect: c.query("redirect_uri") || getRelativeUrl(c, "/authorize")
173
+ redirect: c.query("redirect_uri") || getRelativeUrl(c, "./authorize")
174
174
  };
175
175
  await ctx.set(c, "provider", 3600 * 24, state);
176
176
  return ctx.forward(c, await config.change(c.request, state));
@@ -194,7 +194,8 @@ const PasswordProvider = (config) => {
194
194
  redirect: provider.redirect
195
195
  }, { type: "invalid_email" });
196
196
  const code = generateCode();
197
- await config.sendCode(email, code);
197
+ const context = provider.type === "code" && provider.email === email ? "reset:resend" : "reset";
198
+ await config.sendCode(email, code, context);
198
199
  return transition({
199
200
  type: "code",
200
201
  code,
@@ -0,0 +1,169 @@
1
+ import { OAuthStrategy } from "./providers/strategy.mjs";
2
+ import { AuthStorage } from "./storage.mjs";
3
+
4
+ //#region src/toolkit/client.d.ts
5
+
6
+ /**
7
+ * Configuration for a single OAuth provider.
8
+ */
9
+ interface ProviderConfig<TStrategy extends OAuthStrategy> {
10
+ /** OAuth strategy defining endpoints and defaults */
11
+ readonly strategy: TStrategy;
12
+ /** OAuth client ID from provider */
13
+ readonly clientId: string;
14
+ /** OAuth client secret from provider */
15
+ readonly clientSecret: string;
16
+ /** Redirect URI registered with provider */
17
+ readonly redirectUri: string;
18
+ /** Optional default scopes for this provider */
19
+ readonly scopes?: string[];
20
+ }
21
+ /**
22
+ * Options for initiating OAuth authorization flow.
23
+ */
24
+ interface AuthorizeOptions {
25
+ /** Optional scopes to request (overrides provider defaults) */
26
+ readonly scopes?: string[];
27
+ /** Optional additional parameters to include in authorization URL */
28
+ readonly params?: Record<string, string>;
29
+ /** Optional nonce for additional security */
30
+ readonly nonce?: string;
31
+ }
32
+ /**
33
+ * Result of successful OAuth callback handling.
34
+ */
35
+ interface CallbackResult {
36
+ /** OAuth provider that was used */
37
+ readonly provider: string;
38
+ /** Access token from provider */
39
+ readonly accessToken: string;
40
+ /** Optional refresh token from provider */
41
+ readonly refreshToken?: string;
42
+ /** Token expiration time in seconds */
43
+ readonly expiresIn?: number;
44
+ /** Token type (usually "Bearer") */
45
+ readonly tokenType?: string;
46
+ /** Optional ID token (OpenID Connect) */
47
+ readonly idToken?: string;
48
+ }
49
+ /**
50
+ * OAuth 2.0 client configuration.
51
+ */
52
+ interface OAuthClientConfig<TProviders extends Record<string, ProviderConfig<OAuthStrategy>>> {
53
+ /** Provider configurations keyed by provider name */
54
+ readonly providers: TProviders;
55
+ /** Storage adapter for PKCE state (defaults to sessionStorage in browser) */
56
+ readonly storage?: AuthStorage;
57
+ }
58
+ /**
59
+ * OAuth 2.0 client for managing authentication flows.
60
+ */
61
+ interface OAuthClient<TProviders extends Record<string, ProviderConfig<OAuthStrategy>>> {
62
+ /**
63
+ * Initiate OAuth authorization flow.
64
+ *
65
+ * @param provider - Provider name (key from providers config)
66
+ * @param options - Authorization options
67
+ * @returns Authorization URL to redirect user to
68
+ *
69
+ * @example
70
+ * ```ts
71
+ * // Basic usage
72
+ * const { url } = await client.authorize('github')
73
+ * window.location.href = url
74
+ *
75
+ * // With custom scopes
76
+ * const { url } = await client.authorize('google', {
77
+ * scopes: ['openid', 'email', 'profile']
78
+ * })
79
+ *
80
+ * // With additional params
81
+ * const { url } = await client.authorize('github', {
82
+ * params: { prompt: 'consent' }
83
+ * })
84
+ * ```
85
+ */
86
+ authorize(provider: (string & {}) | keyof TProviders, options?: AuthorizeOptions): Promise<{
87
+ url: string;
88
+ state: string;
89
+ }>;
90
+ /**
91
+ * Handle OAuth callback and exchange code for tokens.
92
+ *
93
+ * @param callbackUrl - Full callback URL with query parameters
94
+ * @returns Token exchange result
95
+ *
96
+ * @throws {Error} If callback URL is invalid, state mismatch, or token exchange fails
97
+ *
98
+ * @example
99
+ * ```ts
100
+ * // Client-side
101
+ * const result = await client.handleCallback(window.location.href)
102
+ * console.log(result.accessToken, result.provider)
103
+ *
104
+ * // Server-side (Next.js)
105
+ * export async function GET(req: Request) {
106
+ * const result = await client.handleCallback(req.url)
107
+ * // Store tokens, create session, etc.
108
+ * return Response.redirect('/')
109
+ * }
110
+ * ```
111
+ */
112
+ handleCallback(callbackUrl: string): Promise<CallbackResult>;
113
+ /**
114
+ * Get user info from OAuth provider using access token.
115
+ *
116
+ * @param provider - Provider name
117
+ * @param accessToken - Access token from provider
118
+ * @returns User info from provider
119
+ *
120
+ * @example
121
+ * ```ts
122
+ * const userInfo = await client.getUserInfo('github', accessToken)
123
+ * console.log(userInfo.email, userInfo.name)
124
+ * ```
125
+ */
126
+ getUserInfo(provider: (string & {}) | keyof TProviders, accessToken: string): Promise<Record<string, unknown>>;
127
+ }
128
+ /**
129
+ * Creates an OAuth 2.0 client for managing authentication flows.
130
+ *
131
+ * Supports PKCE (Proof Key for Code Exchange) for enhanced security.
132
+ * Works in both client-side (browser) and server-side (Node.js) environments.
133
+ *
134
+ * @param config - OAuth client configuration
135
+ * @returns OAuth client instance
136
+ *
137
+ * @example
138
+ * ```ts
139
+ * import { createOAuthClient } from '@draftlab/auth/toolkit/client'
140
+ * import { GitHubStrategy, GoogleStrategy } from '@draftlab/auth/toolkit/providers'
141
+ *
142
+ * const client = createOAuthClient({
143
+ * providers: {
144
+ * github: {
145
+ * strategy: GitHubStrategy,
146
+ * clientId: 'YOUR_CLIENT_ID',
147
+ * clientSecret: 'YOUR_CLIENT_SECRET',
148
+ * redirectUri: 'http://localhost:3000/auth/callback'
149
+ * },
150
+ * google: {
151
+ * strategy: GoogleStrategy,
152
+ * clientId: 'YOUR_CLIENT_ID',
153
+ * clientSecret: 'YOUR_CLIENT_SECRET',
154
+ * redirectUri: 'http://localhost:3000/auth/callback',
155
+ * scopes: ['openid', 'email', 'profile']
156
+ * }
157
+ * }
158
+ * })
159
+ *
160
+ * // Initiate login
161
+ * const { url } = await client.authorize('github')
162
+ *
163
+ * // Handle callback
164
+ * const result = await client.handleCallback(callbackUrl)
165
+ * ```
166
+ */
167
+ declare const createOAuthClient: <TProviders extends Record<string, ProviderConfig<OAuthStrategy>>>(config: OAuthClientConfig<TProviders>) => OAuthClient<TProviders>;
168
+ //#endregion
169
+ export { AuthorizeOptions, CallbackResult, OAuthClient, OAuthClientConfig, ProviderConfig, createOAuthClient };
@@ -0,0 +1,209 @@
1
+ import { generatePKCE } from "../pkce.mjs";
2
+ import { createSessionStorage } from "./storage.mjs";
3
+ import { generateSecureRandom } from "./utils.mjs";
4
+
5
+ //#region src/toolkit/client.ts
6
+ /**
7
+ * Lightweight OAuth 2.0 client toolkit for DraftAuth.
8
+ *
9
+ * Provides a simple, framework-agnostic way to implement OAuth 2.0 authentication
10
+ * with PKCE support. Works in both client-side (SPA) and server-side (Next.js, Remix) environments.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * // Client-side SPA (React, Vue, Solid, etc.)
15
+ * import { createOAuthClient } from '@draftlab/auth/toolkit/client'
16
+ * import { GitHubStrategy, GoogleStrategy } from '@draftlab/auth/toolkit/providers'
17
+ * import { createSessionStorage } from '@draftlab/auth/toolkit/storage'
18
+ *
19
+ * const client = createOAuthClient({
20
+ * providers: {
21
+ * github: {
22
+ * strategy: GitHubStrategy,
23
+ * clientId: 'YOUR_CLIENT_ID',
24
+ * clientSecret: 'YOUR_CLIENT_SECRET',
25
+ * redirectUri: 'http://localhost:3000/auth/callback'
26
+ * },
27
+ * google: {
28
+ * strategy: GoogleStrategy,
29
+ * clientId: 'YOUR_CLIENT_ID',
30
+ * clientSecret: 'YOUR_CLIENT_SECRET',
31
+ * redirectUri: 'http://localhost:3000/auth/callback'
32
+ * }
33
+ * },
34
+ * storage: createSessionStorage()
35
+ * })
36
+ *
37
+ * // Initiate login
38
+ * const { url } = await client.authorize('github', { scopes: ['user:email'] })
39
+ * window.location.href = url
40
+ *
41
+ * // Handle callback
42
+ * const result = await client.handleCallback(window.location.href)
43
+ * console.log(result.accessToken, result.provider)
44
+ * ```
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * // Server-side (Next.js App Router)
49
+ * import { createOAuthClient } from '@draftlab/auth/toolkit/client'
50
+ * import { GitHubStrategy } from '@draftlab/auth/toolkit/providers'
51
+ * import { createCookieStorage } from '@draftlab/auth/toolkit/storage'
52
+ * import { cookies } from 'next/headers'
53
+ *
54
+ * export async function GET(req: Request) {
55
+ * const client = createOAuthClient({
56
+ * providers: {
57
+ * github: {
58
+ * strategy: GitHubStrategy,
59
+ * clientId: process.env.GITHUB_CLIENT_ID!,
60
+ * clientSecret: process.env.GITHUB_CLIENT_SECRET!,
61
+ * redirectUri: 'https://myapp.com/auth/callback'
62
+ * }
63
+ * },
64
+ * storage: createCookieStorage({
65
+ * getCookie: (name) => cookies().get(name)?.value ?? null,
66
+ * setCookie: (name, value, opts) => cookies().set(name, value, opts),
67
+ * deleteCookie: (name) => cookies().delete(name)
68
+ * })
69
+ * })
70
+ *
71
+ * const { url } = await client.authorize('github')
72
+ * return Response.redirect(url)
73
+ * }
74
+ * ```
75
+ */
76
+ /**
77
+ * Creates an OAuth 2.0 client for managing authentication flows.
78
+ *
79
+ * Supports PKCE (Proof Key for Code Exchange) for enhanced security.
80
+ * Works in both client-side (browser) and server-side (Node.js) environments.
81
+ *
82
+ * @param config - OAuth client configuration
83
+ * @returns OAuth client instance
84
+ *
85
+ * @example
86
+ * ```ts
87
+ * import { createOAuthClient } from '@draftlab/auth/toolkit/client'
88
+ * import { GitHubStrategy, GoogleStrategy } from '@draftlab/auth/toolkit/providers'
89
+ *
90
+ * const client = createOAuthClient({
91
+ * providers: {
92
+ * github: {
93
+ * strategy: GitHubStrategy,
94
+ * clientId: 'YOUR_CLIENT_ID',
95
+ * clientSecret: 'YOUR_CLIENT_SECRET',
96
+ * redirectUri: 'http://localhost:3000/auth/callback'
97
+ * },
98
+ * google: {
99
+ * strategy: GoogleStrategy,
100
+ * clientId: 'YOUR_CLIENT_ID',
101
+ * clientSecret: 'YOUR_CLIENT_SECRET',
102
+ * redirectUri: 'http://localhost:3000/auth/callback',
103
+ * scopes: ['openid', 'email', 'profile']
104
+ * }
105
+ * }
106
+ * })
107
+ *
108
+ * // Initiate login
109
+ * const { url } = await client.authorize('github')
110
+ *
111
+ * // Handle callback
112
+ * const result = await client.handleCallback(callbackUrl)
113
+ * ```
114
+ */
115
+ const createOAuthClient = (config) => {
116
+ const storage = config.storage || (typeof sessionStorage !== "undefined" ? createSessionStorage() : null);
117
+ if (!storage) throw new Error("No storage adapter provided. Please provide a storage adapter for server-side environments.");
118
+ return {
119
+ async authorize(provider, options) {
120
+ const providerConfig = config.providers[provider];
121
+ if (!providerConfig) throw new Error(`Provider '${String(provider)}' not configured`);
122
+ const pkce = await generatePKCE();
123
+ const state = generateSecureRandom(16);
124
+ await storage.set({
125
+ state,
126
+ verifier: pkce.verifier,
127
+ provider: String(provider),
128
+ nonce: options?.nonce
129
+ });
130
+ const scopes = options?.scopes || providerConfig.scopes || providerConfig.strategy.scopes;
131
+ const params = new URLSearchParams({
132
+ client_id: providerConfig.clientId,
133
+ redirect_uri: providerConfig.redirectUri,
134
+ response_type: "code",
135
+ scope: Array.isArray(scopes) ? scopes.join(" ") : scopes,
136
+ state,
137
+ code_challenge: pkce.challenge,
138
+ code_challenge_method: pkce.method,
139
+ ...options?.params
140
+ });
141
+ return {
142
+ url: `${providerConfig.strategy.authorizationEndpoint}?${params.toString()}`,
143
+ state
144
+ };
145
+ },
146
+ async handleCallback(callbackUrl) {
147
+ const url = new URL(callbackUrl);
148
+ const code = url.searchParams.get("code");
149
+ const state = url.searchParams.get("state");
150
+ const error = url.searchParams.get("error");
151
+ const errorDescription = url.searchParams.get("error_description");
152
+ if (error) throw new Error(`OAuth error: ${error}${errorDescription ? ` - ${errorDescription}` : ""}`);
153
+ if (!code || !state) throw new Error("Invalid callback URL: missing code or state parameter");
154
+ const storedState = await storage.get();
155
+ await storage.clear();
156
+ if (!storedState) throw new Error("No stored PKCE state found. OAuth flow may have expired or been tampered with.");
157
+ if (state !== storedState.state) throw new Error("State mismatch. Possible CSRF attack detected.");
158
+ const providerConfig = config.providers[storedState.provider];
159
+ if (!providerConfig) throw new Error(`Provider '${storedState.provider}' from callback not configured`);
160
+ const tokenParams = new URLSearchParams({
161
+ grant_type: "authorization_code",
162
+ code,
163
+ redirect_uri: providerConfig.redirectUri,
164
+ client_id: providerConfig.clientId,
165
+ client_secret: providerConfig.clientSecret,
166
+ code_verifier: storedState.verifier
167
+ });
168
+ const tokenResponse = await fetch(providerConfig.strategy.tokenEndpoint, {
169
+ method: "POST",
170
+ headers: {
171
+ "Content-Type": "application/x-www-form-urlencoded",
172
+ Accept: "application/json"
173
+ },
174
+ body: tokenParams
175
+ });
176
+ if (!tokenResponse.ok) {
177
+ const errorText = await tokenResponse.text();
178
+ throw new Error(`Token exchange failed (${tokenResponse.status}): ${errorText}`);
179
+ }
180
+ const tokenData = await tokenResponse.json();
181
+ if (!tokenData.access_token) throw new Error("No access token in provider response");
182
+ return {
183
+ provider: storedState.provider,
184
+ accessToken: tokenData.access_token,
185
+ refreshToken: tokenData.refresh_token,
186
+ expiresIn: tokenData.expires_in,
187
+ tokenType: tokenData.token_type,
188
+ idToken: tokenData.id_token
189
+ };
190
+ },
191
+ async getUserInfo(provider, accessToken) {
192
+ const providerConfig = config.providers[provider];
193
+ if (!providerConfig) throw new Error(`Provider '${String(provider)}' not configured`);
194
+ if (!providerConfig.strategy.userInfoEndpoint) throw new Error(`Provider '${String(provider)}' does not support user info endpoint`);
195
+ const response = await fetch(providerConfig.strategy.userInfoEndpoint, { headers: {
196
+ Authorization: `Bearer ${accessToken}`,
197
+ Accept: "application/json"
198
+ } });
199
+ if (!response.ok) {
200
+ const errorText = await response.text();
201
+ throw new Error(`Failed to fetch user info (${response.status}): ${errorText}`);
202
+ }
203
+ return response.json();
204
+ }
205
+ };
206
+ };
207
+
208
+ //#endregion
209
+ export { createOAuthClient };
@@ -0,0 +1,9 @@
1
+ import { generatePKCE } from "../pkce.mjs";
2
+ import { OAuth2TokenResponse, OAuthStrategy } from "./providers/strategy.mjs";
3
+ import { AuthStorage, PKCEState, createCookieStorage, createLocalStorage, createMemoryStorage, createSessionStorage } from "./storage.mjs";
4
+ import { AuthorizeOptions, CallbackResult, OAuthClient, OAuthClientConfig, ProviderConfig, createOAuthClient } from "./client.mjs";
5
+ import { FacebookStrategy } from "./providers/facebook.mjs";
6
+ import { GitHubStrategy } from "./providers/github.mjs";
7
+ import { GoogleStrategy } from "./providers/google.mjs";
8
+ import { generateSecureRandom } from "./utils.mjs";
9
+ export { type AuthStorage, type AuthorizeOptions, type CallbackResult, FacebookStrategy, GitHubStrategy, GoogleStrategy, type OAuth2TokenResponse, type OAuthClient, type OAuthClientConfig, type OAuthStrategy, type PKCEState, type ProviderConfig, createCookieStorage, createLocalStorage, createMemoryStorage, createOAuthClient, createSessionStorage, generatePKCE, generateSecureRandom };
@@ -0,0 +1,9 @@
1
+ import { generatePKCE } from "../pkce.mjs";
2
+ import { createCookieStorage, createLocalStorage, createMemoryStorage, createSessionStorage } from "./storage.mjs";
3
+ import { generateSecureRandom } from "./utils.mjs";
4
+ import { createOAuthClient } from "./client.mjs";
5
+ import { FacebookStrategy } from "./providers/facebook.mjs";
6
+ import { GitHubStrategy } from "./providers/github.mjs";
7
+ import { GoogleStrategy } from "./providers/google.mjs";
8
+
9
+ export { FacebookStrategy, GitHubStrategy, GoogleStrategy, createCookieStorage, createLocalStorage, createMemoryStorage, createOAuthClient, createSessionStorage, generatePKCE, generateSecureRandom };
@@ -0,0 +1,12 @@
1
+ import { OAuthStrategy } from "./strategy.mjs";
2
+
3
+ //#region src/toolkit/providers/facebook.d.ts
4
+
5
+ /**
6
+ * Facebook OAuth 2.0 strategy.
7
+ *
8
+ * @see https://developers.facebook.com/docs/facebook-login/guides/advanced/manual-flow
9
+ */
10
+ declare const FacebookStrategy: OAuthStrategy;
11
+ //#endregion
12
+ export { FacebookStrategy };
@@ -0,0 +1,16 @@
1
+ //#region src/toolkit/providers/facebook.ts
2
+ /**
3
+ * Facebook OAuth 2.0 strategy.
4
+ *
5
+ * @see https://developers.facebook.com/docs/facebook-login/guides/advanced/manual-flow
6
+ */
7
+ const FacebookStrategy = {
8
+ name: "facebook",
9
+ authorizationEndpoint: "https://www.facebook.com/v23.0/dialog/oauth",
10
+ tokenEndpoint: "https://graph.facebook.com/v23.0/oauth/access_token",
11
+ userInfoEndpoint: "https://graph.facebook.com/me?fields=id,name,email",
12
+ scopes: ["public_profile", "email"]
13
+ };
14
+
15
+ //#endregion
16
+ export { FacebookStrategy };
@@ -0,0 +1,12 @@
1
+ import { OAuthStrategy } from "./strategy.mjs";
2
+
3
+ //#region src/toolkit/providers/github.d.ts
4
+
5
+ /**
6
+ * GitHub OAuth 2.0 strategy.
7
+ *
8
+ * @see https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps
9
+ */
10
+ declare const GitHubStrategy: OAuthStrategy;
11
+ //#endregion
12
+ export { GitHubStrategy };
@@ -0,0 +1,16 @@
1
+ //#region src/toolkit/providers/github.ts
2
+ /**
3
+ * GitHub OAuth 2.0 strategy.
4
+ *
5
+ * @see https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps
6
+ */
7
+ const GitHubStrategy = {
8
+ name: "github",
9
+ authorizationEndpoint: "https://github.com/login/oauth/authorize",
10
+ tokenEndpoint: "https://github.com/login/oauth/access_token",
11
+ userInfoEndpoint: "https://api.github.com/user",
12
+ scopes: ["read:user", "user:email"]
13
+ };
14
+
15
+ //#endregion
16
+ export { GitHubStrategy };
@@ -0,0 +1,12 @@
1
+ import { OAuthStrategy } from "./strategy.mjs";
2
+
3
+ //#region src/toolkit/providers/google.d.ts
4
+
5
+ /**
6
+ * Google OAuth 2.0 / OpenID Connect strategy.
7
+ *
8
+ * @see https://developers.google.com/identity/protocols/oauth2
9
+ */
10
+ declare const GoogleStrategy: OAuthStrategy;
11
+ //#endregion
12
+ export { GoogleStrategy };
@@ -0,0 +1,20 @@
1
+ //#region src/toolkit/providers/google.ts
2
+ /**
3
+ * Google OAuth 2.0 / OpenID Connect strategy.
4
+ *
5
+ * @see https://developers.google.com/identity/protocols/oauth2
6
+ */
7
+ const GoogleStrategy = {
8
+ name: "google",
9
+ authorizationEndpoint: "https://accounts.google.com/o/oauth2/v2/auth",
10
+ tokenEndpoint: "https://oauth2.googleapis.com/token",
11
+ userInfoEndpoint: "https://www.googleapis.com/oauth2/v3/userinfo",
12
+ scopes: [
13
+ "openid",
14
+ "email",
15
+ "profile"
16
+ ]
17
+ };
18
+
19
+ //#endregion
20
+ export { GoogleStrategy };
@@ -0,0 +1,40 @@
1
+ //#region src/toolkit/providers/strategy.d.ts
2
+ /**
3
+ * OAuth 2.0 strategy interface and types for provider configurations.
4
+ */
5
+ /**
6
+ * OAuth 2.0 token response from provider's token endpoint.
7
+ * Based on RFC 6749 Section 5.1.
8
+ */
9
+ interface OAuth2TokenResponse {
10
+ /** The access token issued by the authorization server */
11
+ access_token: string;
12
+ /** The type of token (usually "Bearer") */
13
+ token_type?: string;
14
+ /** The lifetime in seconds of the access token */
15
+ expires_in?: number;
16
+ /** The refresh token for obtaining new access tokens */
17
+ refresh_token?: string;
18
+ /** The scope of the access token (space-separated list) */
19
+ scope?: string;
20
+ /** OpenID Connect ID token (JWT) */
21
+ id_token?: string;
22
+ }
23
+ /**
24
+ * OAuth 2.0 provider strategy definition.
25
+ * Defines the endpoints and default configuration for an OAuth provider.
26
+ */
27
+ interface OAuthStrategy {
28
+ /** Provider name (e.g., "github", "google") */
29
+ readonly name: string;
30
+ /** OAuth authorization endpoint URL */
31
+ readonly authorizationEndpoint: string;
32
+ /** OAuth token exchange endpoint URL */
33
+ readonly tokenEndpoint: string;
34
+ /** Optional user info endpoint URL (for OpenID Connect) */
35
+ readonly userInfoEndpoint?: string;
36
+ /** Default scopes to request */
37
+ readonly scopes: string[];
38
+ }
39
+ //#endregion
40
+ export { OAuth2TokenResponse, OAuthStrategy };
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,145 @@
1
+ //#region src/toolkit/storage.d.ts
2
+ /**
3
+ * Storage adapters for PKCE state management across different runtime environments.
4
+ * Provides implementations for browser (sessionStorage/localStorage), server (cookies),
5
+ * and custom storage backends.
6
+ */
7
+ /**
8
+ * PKCE state data stored during OAuth flow.
9
+ */
10
+ interface PKCEState {
11
+ /** Random state parameter for CSRF protection */
12
+ readonly state: string;
13
+ /** PKCE code verifier for token exchange */
14
+ readonly verifier: string;
15
+ /** OAuth provider identifier */
16
+ readonly provider: string;
17
+ /** Optional nonce for additional security */
18
+ readonly nonce?: string;
19
+ }
20
+ /**
21
+ * Storage interface for persisting PKCE state during OAuth flow.
22
+ * Implement this interface to provide custom storage backends.
23
+ */
24
+ interface AuthStorage {
25
+ /**
26
+ * Store PKCE state data.
27
+ * @param state - PKCE state to persist
28
+ */
29
+ set(state: PKCEState): void | Promise<void>;
30
+ /**
31
+ * Retrieve stored PKCE state data.
32
+ * @returns Stored state or null if not found
33
+ */
34
+ get(): PKCEState | null | Promise<PKCEState | null>;
35
+ /**
36
+ * Clear stored PKCE state data.
37
+ */
38
+ clear(): void | Promise<void>;
39
+ }
40
+ /**
41
+ * Creates a browser sessionStorage adapter for PKCE state.
42
+ * Suitable for client-side SPAs where state should only persist during the browser session.
43
+ *
44
+ * @returns AuthStorage implementation using sessionStorage
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * const storage = createSessionStorage()
49
+ *
50
+ * // Use in toolkit client
51
+ * const client = createOAuthClient({
52
+ * storage,
53
+ * providers: { ... }
54
+ * })
55
+ * ```
56
+ */
57
+ declare const createSessionStorage: () => AuthStorage;
58
+ /**
59
+ * Creates a browser localStorage adapter for PKCE state.
60
+ * Suitable for client-side SPAs where state should persist across browser sessions.
61
+ *
62
+ * ⚠️ Warning: localStorage persists data indefinitely. Consider using sessionStorage
63
+ * for better security, as it automatically clears on browser close.
64
+ *
65
+ * @returns AuthStorage implementation using localStorage
66
+ *
67
+ * @example
68
+ * ```ts
69
+ * const storage = createLocalStorage()
70
+ *
71
+ * // Use in toolkit client
72
+ * const client = createOAuthClient({
73
+ * storage,
74
+ * providers: { ... }
75
+ * })
76
+ * ```
77
+ */
78
+ declare const createLocalStorage: () => AuthStorage;
79
+ /**
80
+ * Creates a memory-based storage adapter for PKCE state.
81
+ * Suitable for server-side rendering or testing environments.
82
+ * State is lost when the process terminates or page reloads.
83
+ *
84
+ * @returns AuthStorage implementation using in-memory storage
85
+ *
86
+ * @example
87
+ * ```ts
88
+ * // Server-side or testing
89
+ * const storage = createMemoryStorage()
90
+ *
91
+ * const client = createOAuthClient({
92
+ * storage,
93
+ * providers: { ... }
94
+ * })
95
+ * ```
96
+ */
97
+ declare const createMemoryStorage: () => AuthStorage;
98
+ /**
99
+ * Creates a cookie-based storage adapter for PKCE state.
100
+ * Suitable for server-side OAuth flows (Next.js, Remix, etc.) where you need
101
+ * to persist state across requests.
102
+ *
103
+ * @param options - Cookie configuration options
104
+ * @returns AuthStorage implementation using cookies
105
+ *
106
+ * @example
107
+ * ```ts
108
+ * // Next.js App Router
109
+ * import { cookies } from 'next/headers'
110
+ *
111
+ * const storage = createCookieStorage({
112
+ * getCookie: (name) => cookies().get(name)?.value ?? null,
113
+ * setCookie: (name, value, opts) => {
114
+ * cookies().set(name, value, {
115
+ * httpOnly: true,
116
+ * secure: true,
117
+ * sameSite: 'lax',
118
+ * maxAge: opts.maxAge
119
+ * })
120
+ * },
121
+ * deleteCookie: (name) => cookies().delete(name)
122
+ * })
123
+ *
124
+ * // TanStack Start
125
+ * import { getCookie, setCookie, deleteCookie } from '@tanstack/react-start/server'
126
+ *
127
+ * const storage = createCookieStorage({
128
+ * getCookie,
129
+ * setCookie: (name, value, opts) => setCookie(name, value, opts),
130
+ * deleteCookie
131
+ * })
132
+ * ```
133
+ */
134
+ declare const createCookieStorage: (options: {
135
+ getCookie: (name: string) => string | null | Promise<string | null>;
136
+ setCookie: (name: string, value: string, options: {
137
+ maxAge?: number;
138
+ httpOnly?: boolean;
139
+ secure?: boolean;
140
+ sameSite?: string;
141
+ }) => void | Promise<void>;
142
+ deleteCookie: (name: string) => void | Promise<void>;
143
+ }) => AuthStorage;
144
+ //#endregion
145
+ export { AuthStorage, PKCEState, createCookieStorage, createLocalStorage, createMemoryStorage, createSessionStorage };
@@ -0,0 +1,157 @@
1
+ //#region src/toolkit/storage.ts
2
+ const STORAGE_KEY = "draftauth.pkce";
3
+ /**
4
+ * Creates a browser sessionStorage adapter for PKCE state.
5
+ * Suitable for client-side SPAs where state should only persist during the browser session.
6
+ *
7
+ * @returns AuthStorage implementation using sessionStorage
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * const storage = createSessionStorage()
12
+ *
13
+ * // Use in toolkit client
14
+ * const client = createOAuthClient({
15
+ * storage,
16
+ * providers: { ... }
17
+ * })
18
+ * ```
19
+ */
20
+ const createSessionStorage = () => ({
21
+ set: (data) => {
22
+ if (typeof sessionStorage === "undefined") throw new Error("sessionStorage is not available in this environment");
23
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify(data));
24
+ },
25
+ get: () => {
26
+ if (typeof sessionStorage === "undefined") return null;
27
+ const data = sessionStorage.getItem(STORAGE_KEY);
28
+ return data ? JSON.parse(data) : null;
29
+ },
30
+ clear: () => {
31
+ if (typeof sessionStorage === "undefined") return;
32
+ sessionStorage.removeItem(STORAGE_KEY);
33
+ }
34
+ });
35
+ /**
36
+ * Creates a browser localStorage adapter for PKCE state.
37
+ * Suitable for client-side SPAs where state should persist across browser sessions.
38
+ *
39
+ * ⚠️ Warning: localStorage persists data indefinitely. Consider using sessionStorage
40
+ * for better security, as it automatically clears on browser close.
41
+ *
42
+ * @returns AuthStorage implementation using localStorage
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * const storage = createLocalStorage()
47
+ *
48
+ * // Use in toolkit client
49
+ * const client = createOAuthClient({
50
+ * storage,
51
+ * providers: { ... }
52
+ * })
53
+ * ```
54
+ */
55
+ const createLocalStorage = () => ({
56
+ set: (data) => {
57
+ if (typeof localStorage === "undefined") throw new Error("localStorage is not available in this environment");
58
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
59
+ },
60
+ get: () => {
61
+ if (typeof localStorage === "undefined") return null;
62
+ const data = localStorage.getItem(STORAGE_KEY);
63
+ return data ? JSON.parse(data) : null;
64
+ },
65
+ clear: () => {
66
+ if (typeof localStorage === "undefined") return;
67
+ localStorage.removeItem(STORAGE_KEY);
68
+ }
69
+ });
70
+ /**
71
+ * Creates a memory-based storage adapter for PKCE state.
72
+ * Suitable for server-side rendering or testing environments.
73
+ * State is lost when the process terminates or page reloads.
74
+ *
75
+ * @returns AuthStorage implementation using in-memory storage
76
+ *
77
+ * @example
78
+ * ```ts
79
+ * // Server-side or testing
80
+ * const storage = createMemoryStorage()
81
+ *
82
+ * const client = createOAuthClient({
83
+ * storage,
84
+ * providers: { ... }
85
+ * })
86
+ * ```
87
+ */
88
+ const createMemoryStorage = () => {
89
+ let state = null;
90
+ return {
91
+ set: (data) => {
92
+ state = data;
93
+ },
94
+ get: () => {
95
+ return state;
96
+ },
97
+ clear: () => {
98
+ state = null;
99
+ }
100
+ };
101
+ };
102
+ /**
103
+ * Creates a cookie-based storage adapter for PKCE state.
104
+ * Suitable for server-side OAuth flows (Next.js, Remix, etc.) where you need
105
+ * to persist state across requests.
106
+ *
107
+ * @param options - Cookie configuration options
108
+ * @returns AuthStorage implementation using cookies
109
+ *
110
+ * @example
111
+ * ```ts
112
+ * // Next.js App Router
113
+ * import { cookies } from 'next/headers'
114
+ *
115
+ * const storage = createCookieStorage({
116
+ * getCookie: (name) => cookies().get(name)?.value ?? null,
117
+ * setCookie: (name, value, opts) => {
118
+ * cookies().set(name, value, {
119
+ * httpOnly: true,
120
+ * secure: true,
121
+ * sameSite: 'lax',
122
+ * maxAge: opts.maxAge
123
+ * })
124
+ * },
125
+ * deleteCookie: (name) => cookies().delete(name)
126
+ * })
127
+ *
128
+ * // TanStack Start
129
+ * import { getCookie, setCookie, deleteCookie } from '@tanstack/react-start/server'
130
+ *
131
+ * const storage = createCookieStorage({
132
+ * getCookie,
133
+ * setCookie: (name, value, opts) => setCookie(name, value, opts),
134
+ * deleteCookie
135
+ * })
136
+ * ```
137
+ */
138
+ const createCookieStorage = (options) => ({
139
+ set: async (data) => {
140
+ await options.setCookie(STORAGE_KEY, JSON.stringify(data), {
141
+ maxAge: 600,
142
+ httpOnly: true,
143
+ secure: process.env.NODE_ENV === "production",
144
+ sameSite: "lax"
145
+ });
146
+ },
147
+ get: async () => {
148
+ const value = await options.getCookie(STORAGE_KEY);
149
+ return value ? JSON.parse(value) : null;
150
+ },
151
+ clear: async () => {
152
+ await options.deleteCookie(STORAGE_KEY);
153
+ }
154
+ });
155
+
156
+ //#endregion
157
+ export { createCookieStorage, createLocalStorage, createMemoryStorage, createSessionStorage };
@@ -0,0 +1,21 @@
1
+ //#region src/toolkit/utils.d.ts
2
+ /**
3
+ * Universal utilities for the OAuth toolkit.
4
+ * These functions work in both browser and Node.js environments.
5
+ */
6
+ /**
7
+ * Generates a cryptographically secure random string using Web Crypto API.
8
+ * Works in both browser and Node.js environments (Node 15+).
9
+ *
10
+ * @param length - Length of random data in bytes (default: 32 for 256-bit security)
11
+ * @returns Base64url-encoded secure random string
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * const state = generateSecureRandom(16) // 128-bit random state
16
+ * const token = generateSecureRandom(32) // 256-bit random token
17
+ * ```
18
+ */
19
+ declare const generateSecureRandom: (length?: number) => string;
20
+ //#endregion
21
+ export { generateSecureRandom };
@@ -0,0 +1,30 @@
1
+ //#region src/toolkit/utils.ts
2
+ /**
3
+ * Universal utilities for the OAuth toolkit.
4
+ * These functions work in both browser and Node.js environments.
5
+ */
6
+ /**
7
+ * Generates a cryptographically secure random string using Web Crypto API.
8
+ * Works in both browser and Node.js environments (Node 15+).
9
+ *
10
+ * @param length - Length of random data in bytes (default: 32 for 256-bit security)
11
+ * @returns Base64url-encoded secure random string
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * const state = generateSecureRandom(16) // 128-bit random state
16
+ * const token = generateSecureRandom(32) // 256-bit random token
17
+ * ```
18
+ */
19
+ const generateSecureRandom = (length = 32) => {
20
+ if (length <= 0 || !Number.isInteger(length)) throw new RangeError("Length must be a positive integer");
21
+ const randomBytes = new Uint8Array(length);
22
+ crypto.getRandomValues(randomBytes);
23
+ let base64 = "";
24
+ if (typeof btoa !== "undefined") base64 = btoa(String.fromCharCode(...randomBytes));
25
+ else base64 = Buffer.from(randomBytes).toString("base64");
26
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
27
+ };
28
+
29
+ //#endregion
30
+ export { generateSecureRandom };
@@ -159,7 +159,7 @@ const PasswordUI = (options) => {
159
159
  ] })
160
160
  })
161
161
  ]
162
- }) : /* @__PURE__ */ jsxs("form", {
162
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("form", {
163
163
  "data-component": "form",
164
164
  method: "post",
165
165
  children: [
@@ -188,7 +188,47 @@ const PasswordUI = (options) => {
188
188
  children: copy.button_continue
189
189
  })
190
190
  ]
191
- }) });
191
+ }), /* @__PURE__ */ jsxs("form", {
192
+ method: "post",
193
+ children: [
194
+ /* @__PURE__ */ jsx("input", {
195
+ name: "action",
196
+ type: "hidden",
197
+ value: "register"
198
+ }),
199
+ /* @__PURE__ */ jsx("input", {
200
+ name: "email",
201
+ type: "hidden",
202
+ value: state.email
203
+ }),
204
+ /* @__PURE__ */ jsx("input", {
205
+ name: "password",
206
+ type: "hidden",
207
+ value: ""
208
+ }),
209
+ /* @__PURE__ */ jsx("input", {
210
+ name: "repeat",
211
+ type: "hidden",
212
+ value: ""
213
+ }),
214
+ /* @__PURE__ */ jsxs("div", {
215
+ "data-component": "form-footer",
216
+ children: [/* @__PURE__ */ jsxs("span", { children: [
217
+ copy.code_return,
218
+ " ",
219
+ /* @__PURE__ */ jsx("a", {
220
+ "data-component": "link",
221
+ href: "./authorize",
222
+ children: copy.login
223
+ })
224
+ ] }), /* @__PURE__ */ jsx("button", {
225
+ type: "submit",
226
+ "data-component": "link",
227
+ children: copy.code_resend
228
+ })]
229
+ })
230
+ ]
231
+ })] }) });
192
232
  };
193
233
  /**
194
234
  * Renders the password change form based on current state
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@draftlab/auth",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "type": "module",
5
5
  "description": "Core implementation for @draftlab/auth",
6
6
  "author": "Matheus Pergoli",