@constructive-io/oauth 0.2.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 (43) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +187 -0
  3. package/esm/index.d.ts +4 -0
  4. package/esm/index.js +4 -0
  5. package/esm/middleware/express.d.ts +51 -0
  6. package/esm/middleware/express.js +172 -0
  7. package/esm/oauth-client.d.ts +16 -0
  8. package/esm/oauth-client.js +146 -0
  9. package/esm/providers/facebook.d.ts +2 -0
  10. package/esm/providers/facebook.js +21 -0
  11. package/esm/providers/github.d.ts +10 -0
  12. package/esm/providers/github.js +30 -0
  13. package/esm/providers/google.d.ts +2 -0
  14. package/esm/providers/google.js +20 -0
  15. package/esm/providers/index.d.ts +9 -0
  16. package/esm/providers/index.js +17 -0
  17. package/esm/providers/linkedin.d.ts +2 -0
  18. package/esm/providers/linkedin.js +20 -0
  19. package/esm/types.d.ts +55 -0
  20. package/esm/types.js +7 -0
  21. package/esm/utils/state.d.ts +1 -0
  22. package/esm/utils/state.js +1 -0
  23. package/index.d.ts +4 -0
  24. package/index.js +20 -0
  25. package/middleware/express.d.ts +51 -0
  26. package/middleware/express.js +177 -0
  27. package/oauth-client.d.ts +16 -0
  28. package/oauth-client.js +151 -0
  29. package/package.json +51 -0
  30. package/providers/facebook.d.ts +2 -0
  31. package/providers/facebook.js +24 -0
  32. package/providers/github.d.ts +10 -0
  33. package/providers/github.js +34 -0
  34. package/providers/google.d.ts +2 -0
  35. package/providers/google.js +23 -0
  36. package/providers/index.d.ts +9 -0
  37. package/providers/index.js +27 -0
  38. package/providers/linkedin.d.ts +2 -0
  39. package/providers/linkedin.js +23 -0
  40. package/types.d.ts +55 -0
  41. package/types.js +10 -0
  42. package/utils/state.d.ts +1 -0
  43. package/utils/state.js +6 -0
@@ -0,0 +1,2 @@
1
+ import { OAuthProviderConfig } from '../types';
2
+ export declare const googleProvider: OAuthProviderConfig;
@@ -0,0 +1,20 @@
1
+ export const googleProvider = {
2
+ id: 'google',
3
+ name: 'Google',
4
+ authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
5
+ tokenUrl: 'https://oauth2.googleapis.com/token',
6
+ userInfoUrl: 'https://openidconnect.googleapis.com/v1/userinfo',
7
+ scopes: ['openid', 'email', 'profile'],
8
+ tokenRequestContentType: 'form',
9
+ mapProfile: (data) => {
10
+ const profile = data;
11
+ return {
12
+ provider: 'google',
13
+ providerId: profile.sub,
14
+ email: profile.email || null,
15
+ name: profile.name || null,
16
+ picture: profile.picture || null,
17
+ raw: data,
18
+ };
19
+ },
20
+ };
@@ -0,0 +1,9 @@
1
+ import { OAuthProviderConfig } from '../types';
2
+ import { googleProvider } from './google';
3
+ import { githubProvider, GITHUB_EMAILS_URL, extractPrimaryEmail } from './github';
4
+ import { facebookProvider } from './facebook';
5
+ import { linkedinProvider } from './linkedin';
6
+ export declare const providers: Record<string, OAuthProviderConfig>;
7
+ export declare function getProvider(id: string): OAuthProviderConfig | undefined;
8
+ export declare function getProviderIds(): string[];
9
+ export { googleProvider, githubProvider, facebookProvider, linkedinProvider, GITHUB_EMAILS_URL, extractPrimaryEmail, };
@@ -0,0 +1,17 @@
1
+ import { googleProvider } from './google';
2
+ import { githubProvider, GITHUB_EMAILS_URL, extractPrimaryEmail } from './github';
3
+ import { facebookProvider } from './facebook';
4
+ import { linkedinProvider } from './linkedin';
5
+ export const providers = {
6
+ google: googleProvider,
7
+ github: githubProvider,
8
+ facebook: facebookProvider,
9
+ linkedin: linkedinProvider,
10
+ };
11
+ export function getProvider(id) {
12
+ return providers[id];
13
+ }
14
+ export function getProviderIds() {
15
+ return Object.keys(providers);
16
+ }
17
+ export { googleProvider, githubProvider, facebookProvider, linkedinProvider, GITHUB_EMAILS_URL, extractPrimaryEmail, };
@@ -0,0 +1,2 @@
1
+ import { OAuthProviderConfig } from '../types';
2
+ export declare const linkedinProvider: OAuthProviderConfig;
@@ -0,0 +1,20 @@
1
+ export const linkedinProvider = {
2
+ id: 'linkedin',
3
+ name: 'LinkedIn',
4
+ authorizationUrl: 'https://www.linkedin.com/oauth/v2/authorization',
5
+ tokenUrl: 'https://www.linkedin.com/oauth/v2/accessToken',
6
+ userInfoUrl: 'https://api.linkedin.com/v2/userinfo',
7
+ scopes: ['openid', 'profile', 'email'],
8
+ tokenRequestContentType: 'form',
9
+ mapProfile: (data) => {
10
+ const profile = data;
11
+ return {
12
+ provider: 'linkedin',
13
+ providerId: profile.sub,
14
+ email: profile.email || null,
15
+ name: profile.name || null,
16
+ picture: profile.picture || null,
17
+ raw: data,
18
+ };
19
+ },
20
+ };
package/esm/types.d.ts ADDED
@@ -0,0 +1,55 @@
1
+ export interface OAuthProviderConfig {
2
+ id: string;
3
+ name: string;
4
+ authorizationUrl: string;
5
+ tokenUrl: string;
6
+ userInfoUrl: string;
7
+ scopes: string[];
8
+ tokenRequestContentType?: 'json' | 'form';
9
+ userInfoMethod?: 'GET' | 'POST';
10
+ mapProfile: (data: unknown) => OAuthProfile;
11
+ }
12
+ export interface OAuthProfile {
13
+ provider: string;
14
+ providerId: string;
15
+ email: string | null;
16
+ name: string | null;
17
+ picture: string | null;
18
+ raw: unknown;
19
+ }
20
+ export interface OAuthCredentials {
21
+ clientId: string;
22
+ clientSecret: string;
23
+ redirectUri?: string;
24
+ }
25
+ export interface OAuthClientConfig {
26
+ providers: Record<string, OAuthCredentials>;
27
+ baseUrl: string;
28
+ callbackPath?: string;
29
+ stateCookieName?: string;
30
+ stateCookieMaxAge?: number;
31
+ }
32
+ export interface TokenResponse {
33
+ access_token: string;
34
+ token_type: string;
35
+ expires_in?: number;
36
+ refresh_token?: string;
37
+ scope?: string;
38
+ }
39
+ export interface AuthorizationUrlParams {
40
+ provider: string;
41
+ state?: string;
42
+ redirectUri?: string;
43
+ scopes?: string[];
44
+ }
45
+ export interface CallbackParams {
46
+ provider: string;
47
+ code: string;
48
+ redirectUri?: string;
49
+ }
50
+ export interface OAuthError extends Error {
51
+ code: string;
52
+ provider?: string;
53
+ statusCode?: number;
54
+ }
55
+ export declare function createOAuthError(message: string, code: string, provider?: string, statusCode?: number): OAuthError;
package/esm/types.js ADDED
@@ -0,0 +1,7 @@
1
+ export function createOAuthError(message, code, provider, statusCode) {
2
+ const error = new Error(message);
3
+ error.code = code;
4
+ error.provider = provider;
5
+ error.statusCode = statusCode;
6
+ return error;
7
+ }
@@ -0,0 +1 @@
1
+ export { generateToken as generateState, verifyToken as verifyState } from '@constructive-io/csrf';
@@ -0,0 +1 @@
1
+ export { generateToken as generateState, verifyToken as verifyState } from '@constructive-io/csrf';
package/index.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { OAuthProviderConfig, OAuthProfile, OAuthCredentials, OAuthClientConfig, TokenResponse, AuthorizationUrlParams, CallbackParams, OAuthError, createOAuthError, } from './types';
2
+ export { OAuthClient, createOAuthClient } from './oauth-client';
3
+ export { providers, getProvider, getProviderIds, googleProvider, githubProvider, facebookProvider, linkedinProvider, } from './providers';
4
+ export { createOAuthMiddleware, OAuthMiddlewareConfig, OAuthCallbackContext, OAuthErrorContext, OAuthRouteHandlers, generateState, verifyState, } from './middleware/express';
package/index.js ADDED
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.verifyState = exports.generateState = exports.createOAuthMiddleware = exports.linkedinProvider = exports.facebookProvider = exports.githubProvider = exports.googleProvider = exports.getProviderIds = exports.getProvider = exports.providers = exports.createOAuthClient = exports.OAuthClient = exports.createOAuthError = void 0;
4
+ var types_1 = require("./types");
5
+ Object.defineProperty(exports, "createOAuthError", { enumerable: true, get: function () { return types_1.createOAuthError; } });
6
+ var oauth_client_1 = require("./oauth-client");
7
+ Object.defineProperty(exports, "OAuthClient", { enumerable: true, get: function () { return oauth_client_1.OAuthClient; } });
8
+ Object.defineProperty(exports, "createOAuthClient", { enumerable: true, get: function () { return oauth_client_1.createOAuthClient; } });
9
+ var providers_1 = require("./providers");
10
+ Object.defineProperty(exports, "providers", { enumerable: true, get: function () { return providers_1.providers; } });
11
+ Object.defineProperty(exports, "getProvider", { enumerable: true, get: function () { return providers_1.getProvider; } });
12
+ Object.defineProperty(exports, "getProviderIds", { enumerable: true, get: function () { return providers_1.getProviderIds; } });
13
+ Object.defineProperty(exports, "googleProvider", { enumerable: true, get: function () { return providers_1.googleProvider; } });
14
+ Object.defineProperty(exports, "githubProvider", { enumerable: true, get: function () { return providers_1.githubProvider; } });
15
+ Object.defineProperty(exports, "facebookProvider", { enumerable: true, get: function () { return providers_1.facebookProvider; } });
16
+ Object.defineProperty(exports, "linkedinProvider", { enumerable: true, get: function () { return providers_1.linkedinProvider; } });
17
+ var express_1 = require("./middleware/express");
18
+ Object.defineProperty(exports, "createOAuthMiddleware", { enumerable: true, get: function () { return express_1.createOAuthMiddleware; } });
19
+ Object.defineProperty(exports, "generateState", { enumerable: true, get: function () { return express_1.generateState; } });
20
+ Object.defineProperty(exports, "verifyState", { enumerable: true, get: function () { return express_1.verifyState; } });
@@ -0,0 +1,51 @@
1
+ import { OAuthClientConfig, OAuthProfile } from '../types';
2
+ import { generateState, verifyState } from '../utils/state';
3
+ export interface OAuthMiddlewareConfig extends OAuthClientConfig {
4
+ onSuccess: (profile: OAuthProfile, context: OAuthCallbackContext) => Promise<unknown>;
5
+ onError?: (error: Error, context: OAuthErrorContext) => void;
6
+ successRedirect?: string;
7
+ errorRedirect?: string;
8
+ }
9
+ export interface OAuthCallbackContext {
10
+ provider: string;
11
+ profile: OAuthProfile;
12
+ query: Record<string, string>;
13
+ }
14
+ export interface OAuthErrorContext {
15
+ provider?: string;
16
+ error: Error;
17
+ query: Record<string, string>;
18
+ }
19
+ export interface OAuthRouteHandlers {
20
+ initiateAuth: (req: {
21
+ params: {
22
+ provider: string;
23
+ };
24
+ query: Record<string, unknown>;
25
+ }, res: {
26
+ redirect: (url: string) => void;
27
+ cookie: (name: string, value: string, options: Record<string, unknown>) => void;
28
+ status: (code: number) => {
29
+ json: (data: unknown) => void;
30
+ };
31
+ }) => void;
32
+ handleCallback: (req: {
33
+ params: {
34
+ provider: string;
35
+ };
36
+ query: Record<string, unknown>;
37
+ cookies: Record<string, string>;
38
+ }, res: {
39
+ redirect: (url: string) => void;
40
+ clearCookie: (name: string) => void;
41
+ status: (code: number) => {
42
+ json: (data: unknown) => void;
43
+ };
44
+ json: (data: unknown) => void;
45
+ }) => Promise<void>;
46
+ getProviders: (req: unknown, res: {
47
+ json: (data: unknown) => void;
48
+ }) => void;
49
+ }
50
+ export declare function createOAuthMiddleware(config: OAuthMiddlewareConfig): OAuthRouteHandlers;
51
+ export { generateState, verifyState };
@@ -0,0 +1,177 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.verifyState = exports.generateState = void 0;
4
+ exports.createOAuthMiddleware = createOAuthMiddleware;
5
+ const oauth_client_1 = require("../oauth-client");
6
+ const types_1 = require("../types");
7
+ const state_1 = require("../utils/state");
8
+ Object.defineProperty(exports, "generateState", { enumerable: true, get: function () { return state_1.generateState; } });
9
+ Object.defineProperty(exports, "verifyState", { enumerable: true, get: function () { return state_1.verifyState; } });
10
+ const providers_1 = require("../providers");
11
+ function createOAuthMiddleware(config) {
12
+ const client = new oauth_client_1.OAuthClient(config);
13
+ const clientConfig = client.getConfig();
14
+ const initiateAuth = (req, res) => {
15
+ const { provider } = req.params;
16
+ try {
17
+ const { url, state } = client.getAuthorizationUrl({ provider });
18
+ res.cookie(clientConfig.stateCookieName, state, {
19
+ httpOnly: true,
20
+ secure: process.env.NODE_ENV === 'production',
21
+ maxAge: (clientConfig.stateCookieMaxAge || 600) * 1000,
22
+ sameSite: 'lax',
23
+ });
24
+ res.redirect(url);
25
+ }
26
+ catch (error) {
27
+ if (config.onError) {
28
+ config.onError(error, {
29
+ provider,
30
+ error: error,
31
+ query: req.query,
32
+ });
33
+ }
34
+ if (config.errorRedirect) {
35
+ const errorUrl = new URL(config.errorRedirect);
36
+ errorUrl.searchParams.set('error', error.message);
37
+ errorUrl.searchParams.set('provider', provider);
38
+ res.redirect(errorUrl.toString());
39
+ }
40
+ else {
41
+ res.status(400).json({
42
+ error: 'oauth_error',
43
+ message: error.message,
44
+ provider,
45
+ });
46
+ }
47
+ }
48
+ };
49
+ const handleCallback = async (req, res) => {
50
+ const { provider } = req.params;
51
+ const { code, state, error: oauthError, error_description } = req.query;
52
+ const storedState = req.cookies[clientConfig.stateCookieName];
53
+ res.clearCookie(clientConfig.stateCookieName);
54
+ if (oauthError) {
55
+ const error = (0, types_1.createOAuthError)(error_description || oauthError, 'OAUTH_PROVIDER_ERROR', provider);
56
+ if (config.onError) {
57
+ config.onError(error, {
58
+ provider,
59
+ error,
60
+ query: req.query,
61
+ });
62
+ }
63
+ if (config.errorRedirect) {
64
+ const errorUrl = new URL(config.errorRedirect);
65
+ errorUrl.searchParams.set('error', oauthError);
66
+ if (error_description) {
67
+ errorUrl.searchParams.set('error_description', error_description);
68
+ }
69
+ errorUrl.searchParams.set('provider', provider);
70
+ res.redirect(errorUrl.toString());
71
+ }
72
+ else {
73
+ res.status(400).json({
74
+ error: 'oauth_error',
75
+ message: error_description || oauthError,
76
+ provider,
77
+ });
78
+ }
79
+ return;
80
+ }
81
+ if (!(0, state_1.verifyState)(storedState, state)) {
82
+ const error = (0, types_1.createOAuthError)('Invalid state parameter', 'INVALID_STATE', provider);
83
+ if (config.onError) {
84
+ config.onError(error, {
85
+ provider,
86
+ error,
87
+ query: req.query,
88
+ });
89
+ }
90
+ if (config.errorRedirect) {
91
+ const errorUrl = new URL(config.errorRedirect);
92
+ errorUrl.searchParams.set('error', 'invalid_state');
93
+ errorUrl.searchParams.set('provider', provider);
94
+ res.redirect(errorUrl.toString());
95
+ }
96
+ else {
97
+ res.status(400).json({
98
+ error: 'invalid_state',
99
+ message: 'Invalid state parameter',
100
+ provider,
101
+ });
102
+ }
103
+ return;
104
+ }
105
+ if (!code) {
106
+ const error = (0, types_1.createOAuthError)('Missing authorization code', 'MISSING_CODE', provider);
107
+ if (config.onError) {
108
+ config.onError(error, {
109
+ provider,
110
+ error,
111
+ query: req.query,
112
+ });
113
+ }
114
+ if (config.errorRedirect) {
115
+ const errorUrl = new URL(config.errorRedirect);
116
+ errorUrl.searchParams.set('error', 'missing_code');
117
+ errorUrl.searchParams.set('provider', provider);
118
+ res.redirect(errorUrl.toString());
119
+ }
120
+ else {
121
+ res.status(400).json({
122
+ error: 'missing_code',
123
+ message: 'Missing authorization code',
124
+ provider,
125
+ });
126
+ }
127
+ return;
128
+ }
129
+ try {
130
+ const profile = await client.handleCallback({ provider, code });
131
+ const result = await config.onSuccess(profile, {
132
+ provider,
133
+ profile,
134
+ query: req.query,
135
+ });
136
+ if (config.successRedirect) {
137
+ res.redirect(config.successRedirect);
138
+ }
139
+ else {
140
+ res.json({ success: true, data: result });
141
+ }
142
+ }
143
+ catch (error) {
144
+ if (config.onError) {
145
+ config.onError(error, {
146
+ provider,
147
+ error: error,
148
+ query: req.query,
149
+ });
150
+ }
151
+ if (config.errorRedirect) {
152
+ const errorUrl = new URL(config.errorRedirect);
153
+ errorUrl.searchParams.set('error', 'callback_failed');
154
+ errorUrl.searchParams.set('message', error.message);
155
+ errorUrl.searchParams.set('provider', provider);
156
+ res.redirect(errorUrl.toString());
157
+ }
158
+ else {
159
+ res.status(500).json({
160
+ error: 'callback_failed',
161
+ message: error.message,
162
+ provider,
163
+ });
164
+ }
165
+ }
166
+ };
167
+ const getProviders = (_req, res) => {
168
+ const configuredProviders = Object.keys(config.providers);
169
+ const availableProviders = (0, providers_1.getProviderIds)().filter((id) => configuredProviders.includes(id));
170
+ res.json({ providers: availableProviders });
171
+ };
172
+ return {
173
+ initiateAuth,
174
+ handleCallback,
175
+ getProviders,
176
+ };
177
+ }
@@ -0,0 +1,16 @@
1
+ import { OAuthClientConfig, OAuthProfile, TokenResponse, AuthorizationUrlParams, CallbackParams } from './types';
2
+ export declare class OAuthClient {
3
+ private config;
4
+ constructor(config: OAuthClientConfig);
5
+ getAuthorizationUrl(params: AuthorizationUrlParams): {
6
+ url: string;
7
+ state: string;
8
+ };
9
+ exchangeCode(params: CallbackParams): Promise<TokenResponse>;
10
+ getUserProfile(providerId: string, accessToken: string): Promise<OAuthProfile>;
11
+ handleCallback(params: CallbackParams): Promise<OAuthProfile>;
12
+ private fetchGitHubEmail;
13
+ private getCallbackUrl;
14
+ getConfig(): OAuthClientConfig;
15
+ }
16
+ export declare function createOAuthClient(config: OAuthClientConfig): OAuthClient;
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OAuthClient = void 0;
4
+ exports.createOAuthClient = createOAuthClient;
5
+ const types_1 = require("./types");
6
+ const providers_1 = require("./providers");
7
+ const state_1 = require("./utils/state");
8
+ class OAuthClient {
9
+ config;
10
+ constructor(config) {
11
+ this.config = {
12
+ callbackPath: '/auth/{provider}/callback',
13
+ stateCookieName: 'oauth_state',
14
+ stateCookieMaxAge: 600,
15
+ ...config,
16
+ };
17
+ }
18
+ getAuthorizationUrl(params) {
19
+ const { provider: providerId, state: customState, redirectUri, scopes } = params;
20
+ const provider = (0, providers_1.getProvider)(providerId);
21
+ if (!provider) {
22
+ throw (0, types_1.createOAuthError)(`Unknown provider: ${providerId}`, 'UNKNOWN_PROVIDER', providerId);
23
+ }
24
+ const credentials = this.config.providers[providerId];
25
+ if (!credentials) {
26
+ throw (0, types_1.createOAuthError)(`No credentials configured for provider: ${providerId}`, 'MISSING_CREDENTIALS', providerId);
27
+ }
28
+ const state = customState || (0, state_1.generateState)();
29
+ const callbackUrl = this.getCallbackUrl(providerId, redirectUri || credentials.redirectUri);
30
+ const effectiveScopes = scopes || provider.scopes;
31
+ const url = new URL(provider.authorizationUrl);
32
+ url.searchParams.set('client_id', credentials.clientId);
33
+ url.searchParams.set('redirect_uri', callbackUrl);
34
+ url.searchParams.set('response_type', 'code');
35
+ url.searchParams.set('scope', effectiveScopes.join(' '));
36
+ url.searchParams.set('state', state);
37
+ return { url: url.toString(), state };
38
+ }
39
+ async exchangeCode(params) {
40
+ const { provider: providerId, code, redirectUri } = params;
41
+ const provider = (0, providers_1.getProvider)(providerId);
42
+ if (!provider) {
43
+ throw (0, types_1.createOAuthError)(`Unknown provider: ${providerId}`, 'UNKNOWN_PROVIDER', providerId);
44
+ }
45
+ const credentials = this.config.providers[providerId];
46
+ if (!credentials) {
47
+ throw (0, types_1.createOAuthError)(`No credentials configured for provider: ${providerId}`, 'MISSING_CREDENTIALS', providerId);
48
+ }
49
+ const callbackUrl = this.getCallbackUrl(providerId, redirectUri || credentials.redirectUri);
50
+ const body = {
51
+ client_id: credentials.clientId,
52
+ client_secret: credentials.clientSecret,
53
+ code,
54
+ redirect_uri: callbackUrl,
55
+ grant_type: 'authorization_code',
56
+ };
57
+ const headers = {
58
+ Accept: 'application/json',
59
+ };
60
+ let requestBody;
61
+ if (provider.tokenRequestContentType === 'json') {
62
+ headers['Content-Type'] = 'application/json';
63
+ requestBody = JSON.stringify(body);
64
+ }
65
+ else {
66
+ headers['Content-Type'] = 'application/x-www-form-urlencoded';
67
+ requestBody = new URLSearchParams(body).toString();
68
+ }
69
+ const response = await fetch(provider.tokenUrl, {
70
+ method: 'POST',
71
+ headers,
72
+ body: requestBody,
73
+ });
74
+ if (!response.ok) {
75
+ const errorText = await response.text();
76
+ throw (0, types_1.createOAuthError)(`Token exchange failed: ${errorText}`, 'TOKEN_EXCHANGE_FAILED', providerId, response.status);
77
+ }
78
+ const data = await response.json();
79
+ if (data.error) {
80
+ throw (0, types_1.createOAuthError)(`Token exchange error: ${data.error_description || data.error}`, 'TOKEN_EXCHANGE_ERROR', providerId);
81
+ }
82
+ return data;
83
+ }
84
+ async getUserProfile(providerId, accessToken) {
85
+ const provider = (0, providers_1.getProvider)(providerId);
86
+ if (!provider) {
87
+ throw (0, types_1.createOAuthError)(`Unknown provider: ${providerId}`, 'UNKNOWN_PROVIDER', providerId);
88
+ }
89
+ const headers = {
90
+ Authorization: `Bearer ${accessToken}`,
91
+ Accept: 'application/json',
92
+ };
93
+ if (providerId === 'github') {
94
+ headers['User-Agent'] = 'Constructive-OAuth';
95
+ }
96
+ const response = await fetch(provider.userInfoUrl, {
97
+ method: provider.userInfoMethod || 'GET',
98
+ headers,
99
+ });
100
+ if (!response.ok) {
101
+ const errorText = await response.text();
102
+ throw (0, types_1.createOAuthError)(`Failed to fetch user profile: ${errorText}`, 'USER_PROFILE_FAILED', providerId, response.status);
103
+ }
104
+ const data = await response.json();
105
+ let profile = provider.mapProfile(data);
106
+ if (providerId === 'github' && !profile.email) {
107
+ profile = await this.fetchGitHubEmail(accessToken, profile);
108
+ }
109
+ return profile;
110
+ }
111
+ async handleCallback(params) {
112
+ const tokens = await this.exchangeCode(params);
113
+ return this.getUserProfile(params.provider, tokens.access_token);
114
+ }
115
+ async fetchGitHubEmail(accessToken, profile) {
116
+ try {
117
+ const response = await fetch(providers_1.GITHUB_EMAILS_URL, {
118
+ headers: {
119
+ Authorization: `Bearer ${accessToken}`,
120
+ Accept: 'application/json',
121
+ 'User-Agent': 'Constructive-OAuth',
122
+ },
123
+ });
124
+ if (response.ok) {
125
+ const emails = await response.json();
126
+ const email = (0, providers_1.extractPrimaryEmail)(emails);
127
+ if (email) {
128
+ return { ...profile, email };
129
+ }
130
+ }
131
+ }
132
+ catch {
133
+ // Ignore email fetch errors, return profile without email
134
+ }
135
+ return profile;
136
+ }
137
+ getCallbackUrl(providerId, customRedirectUri) {
138
+ if (customRedirectUri) {
139
+ return customRedirectUri;
140
+ }
141
+ const path = this.config.callbackPath.replace('{provider}', providerId);
142
+ return `${this.config.baseUrl}${path}`;
143
+ }
144
+ getConfig() {
145
+ return this.config;
146
+ }
147
+ }
148
+ exports.OAuthClient = OAuthClient;
149
+ function createOAuthClient(config) {
150
+ return new OAuthClient(config);
151
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@constructive-io/oauth",
3
+ "version": "0.2.0",
4
+ "author": "Constructive <developers@constructive.io>",
5
+ "description": "OAuth 2.0 client for social authentication (Google, GitHub, Facebook, LinkedIn)",
6
+ "main": "index.js",
7
+ "module": "esm/index.js",
8
+ "types": "index.d.ts",
9
+ "homepage": "https://github.com/constructive-io/constructive",
10
+ "license": "MIT",
11
+ "publishConfig": {
12
+ "access": "public",
13
+ "directory": "dist"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/constructive-io/constructive"
18
+ },
19
+ "bugs": {
20
+ "url": "https://github.com/constructive-io/constructive/issues"
21
+ },
22
+ "scripts": {
23
+ "clean": "makage clean",
24
+ "prepack": "npm run build",
25
+ "build": "makage build",
26
+ "build:dev": "makage build --dev",
27
+ "lint": "eslint . --fix",
28
+ "test": "jest --passWithNoTests",
29
+ "test:watch": "jest --watch"
30
+ },
31
+ "dependencies": {
32
+ "@constructive-io/csrf": "^0.2.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^20.12.7",
36
+ "makage": "^0.1.10",
37
+ "ts-node": "^10.9.2"
38
+ },
39
+ "keywords": [
40
+ "oauth",
41
+ "oauth2",
42
+ "authentication",
43
+ "google",
44
+ "github",
45
+ "facebook",
46
+ "linkedin",
47
+ "social-login",
48
+ "constructive"
49
+ ],
50
+ "gitHead": "2f09348be8b309eba14b5ed16d391986ecff3296"
51
+ }
@@ -0,0 +1,2 @@
1
+ import { OAuthProviderConfig } from '../types';
2
+ export declare const facebookProvider: OAuthProviderConfig;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.facebookProvider = void 0;
4
+ const FACEBOOK_API_VERSION = 'v18.0';
5
+ exports.facebookProvider = {
6
+ id: 'facebook',
7
+ name: 'Facebook',
8
+ authorizationUrl: `https://www.facebook.com/${FACEBOOK_API_VERSION}/dialog/oauth`,
9
+ tokenUrl: `https://graph.facebook.com/${FACEBOOK_API_VERSION}/oauth/access_token`,
10
+ userInfoUrl: `https://graph.facebook.com/me?fields=id,name,email,picture`,
11
+ scopes: ['email', 'public_profile'],
12
+ tokenRequestContentType: 'form',
13
+ mapProfile: (data) => {
14
+ const profile = data;
15
+ return {
16
+ provider: 'facebook',
17
+ providerId: profile.id,
18
+ email: profile.email || null,
19
+ name: profile.name || null,
20
+ picture: profile.picture?.data?.url || null,
21
+ raw: data,
22
+ };
23
+ },
24
+ };
@@ -0,0 +1,10 @@
1
+ import { OAuthProviderConfig } from '../types';
2
+ interface GitHubEmail {
3
+ email: string;
4
+ primary: boolean;
5
+ verified: boolean;
6
+ }
7
+ export declare const githubProvider: OAuthProviderConfig;
8
+ export declare const GITHUB_EMAILS_URL = "https://api.github.com/user/emails";
9
+ export declare function extractPrimaryEmail(emails: GitHubEmail[]): string | null;
10
+ export {};