@simple-login/sdk 0.1.4 → 1.0.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.
- package/dist/index.cjs +61 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -2
- package/dist/index.d.ts +23 -2
- package/dist/index.js +61 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -51,12 +51,21 @@ var TokenError = class extends SousaError {
|
|
|
51
51
|
|
|
52
52
|
// src/client.ts
|
|
53
53
|
var BASE_URL = "https://app.simple-login.com";
|
|
54
|
+
function generateState() {
|
|
55
|
+
const array = new Uint8Array(32);
|
|
56
|
+
crypto.getRandomValues(array);
|
|
57
|
+
return Array.from(array, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
58
|
+
}
|
|
54
59
|
function getEnv(key) {
|
|
55
60
|
if (typeof process !== "undefined" && process.env) {
|
|
56
61
|
return process.env[key];
|
|
57
62
|
}
|
|
58
63
|
return void 0;
|
|
59
64
|
}
|
|
65
|
+
function parseCookie(cookieHeader, name) {
|
|
66
|
+
const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`));
|
|
67
|
+
return match ? decodeURIComponent(match[1]) : void 0;
|
|
68
|
+
}
|
|
60
69
|
var SimpleLogin = class {
|
|
61
70
|
clientId;
|
|
62
71
|
clientSecret;
|
|
@@ -83,22 +92,69 @@ var SimpleLogin = class {
|
|
|
83
92
|
);
|
|
84
93
|
}
|
|
85
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Returns a Response that redirects to the authorization URL with the state cookie set.
|
|
97
|
+
* @param options - Optional scopes or custom state
|
|
98
|
+
* @returns A 302 redirect Response ready to be returned from your route handler
|
|
99
|
+
*/
|
|
100
|
+
redirectToAuth(options = {}) {
|
|
101
|
+
const { url, cookie } = this.getAuthorizationUrl(options);
|
|
102
|
+
return new Response(null, {
|
|
103
|
+
status: 302,
|
|
104
|
+
headers: {
|
|
105
|
+
Location: url,
|
|
106
|
+
"Set-Cookie": cookie
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
86
110
|
/**
|
|
87
111
|
* Generate the authorization URL to redirect users for sign-in
|
|
112
|
+
* @returns The authorization URL, state parameter, and a ready-to-use Set-Cookie header value
|
|
88
113
|
*/
|
|
89
114
|
getAuthorizationUrl(options = {}) {
|
|
115
|
+
const state = options.state ?? generateState();
|
|
90
116
|
const params = new URLSearchParams({
|
|
91
117
|
client_id: this.clientId,
|
|
92
118
|
redirect_uri: this.redirectUri,
|
|
93
|
-
response_type: "code"
|
|
119
|
+
response_type: "code",
|
|
120
|
+
state
|
|
94
121
|
});
|
|
95
|
-
if (options.state) {
|
|
96
|
-
params.set("state", options.state);
|
|
97
|
-
}
|
|
98
122
|
if (options.scopes?.length) {
|
|
99
123
|
params.set("scope", options.scopes.join(" "));
|
|
100
124
|
}
|
|
101
|
-
|
|
125
|
+
const cookie = `SIMPLELOGIN_STATE=${state}; HttpOnly; Secure; SameSite=Lax; Max-Age=600; Path=/`;
|
|
126
|
+
return {
|
|
127
|
+
url: `${this.baseUrl}/v1/auth/authorize?${params.toString()}`,
|
|
128
|
+
state,
|
|
129
|
+
cookie
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Verify the OAuth callback request and exchange the code for tokens.
|
|
134
|
+
* Automatically extracts state from SIMPLELOGIN_STATE cookie and code/state from URL.
|
|
135
|
+
* @param request - The incoming Request object from the callback
|
|
136
|
+
* @returns The token response if verification succeeds
|
|
137
|
+
* @throws AuthorizationError if state is missing or doesn't match
|
|
138
|
+
*/
|
|
139
|
+
async handleCallback(request) {
|
|
140
|
+
const url = new URL(request.url);
|
|
141
|
+
const code = url.searchParams.get("code");
|
|
142
|
+
const state = url.searchParams.get("state");
|
|
143
|
+
if (!code) {
|
|
144
|
+
throw new AuthorizationError("Missing authorization code in callback");
|
|
145
|
+
}
|
|
146
|
+
if (!state) {
|
|
147
|
+
throw new AuthorizationError("Missing state parameter in callback");
|
|
148
|
+
}
|
|
149
|
+
const cookieHeader = request.headers.get("cookie") ?? "";
|
|
150
|
+
const expectedState = parseCookie(cookieHeader, "SIMPLELOGIN_STATE");
|
|
151
|
+
if (!expectedState) {
|
|
152
|
+
throw new AuthorizationError("Missing SIMPLELOGIN_STATE cookie");
|
|
153
|
+
}
|
|
154
|
+
if (state !== expectedState) {
|
|
155
|
+
throw new AuthorizationError("Invalid state parameter");
|
|
156
|
+
}
|
|
157
|
+
return this.exchangeCode(code);
|
|
102
158
|
}
|
|
103
159
|
/**
|
|
104
160
|
* Exchange an authorization code for access and refresh tokens
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/client.ts"],"sourcesContent":["export { SimpleLogin } from './client'\nexport { AuthorizationError, SousaError, TokenError } from './errors'\nexport type { AuthorizationUrlOptions, SimpleLoginConfig, TokenResponse, UserInfo } from './types'\n","export class SousaError extends Error {\n constructor(\n message: string,\n public code: string,\n public statusCode?: number\n ) {\n super(message);\n this.name = \"SousaError\";\n }\n}\n\nexport class AuthorizationError extends SousaError {\n constructor(message: string) {\n super(message, \"AUTHORIZATION_ERROR\", 401);\n this.name = \"AuthorizationError\";\n }\n}\n\nexport class TokenError extends SousaError {\n constructor(message: string) {\n super(message, \"TOKEN_ERROR\", 400);\n this.name = \"TokenError\";\n }\n}\n","import { AuthorizationError, TokenError } from './errors'\nimport type { AuthorizationUrlOptions, SimpleLoginConfig, TokenResponse, UserInfo } from './types'\n\ndeclare const __SIMPLELOGIN_BASE_URL__: string\n\nconst BASE_URL = __SIMPLELOGIN_BASE_URL__\n\nfunction getEnv(key: string): string | undefined {\n if (typeof process !== 'undefined' && process.env) {\n return process.env[key]\n }\n return undefined\n}\n\nexport class SimpleLogin {\n private clientId: string\n private clientSecret: string\n private redirectUri: string\n private baseUrl: string\n\n constructor(config: SimpleLoginConfig = {}) {\n this.clientId = config.clientId ?? getEnv('SIMPLELOGIN_CLIENT_ID') ?? ''\n this.clientSecret = config.clientSecret ?? getEnv('SIMPLELOGIN_CLIENT_SECRET') ?? ''\n this.redirectUri = config.redirectUri ?? getEnv('SIMPLELOGIN_REDIRECT_URI') ?? ''\n this.baseUrl = BASE_URL\n\n if (!this.clientId) {\n throw new Error(\n 'clientId is required. Pass it in config or set SIMPLELOGIN_CLIENT_ID env var.',\n )\n }\n if (!this.clientSecret) {\n throw new Error(\n 'clientSecret is required. Pass it in config or set SIMPLELOGIN_CLIENT_SECRET env var.',\n )\n }\n if (!this.redirectUri) {\n throw new Error(\n 'redirectUri is required. Pass it in config or set SIMPLELOGIN_REDIRECT_URI env var.',\n )\n }\n }\n\n /**\n * Generate the authorization URL to redirect users for sign-in\n */\n getAuthorizationUrl(options: AuthorizationUrlOptions = {}): string {\n const params = new URLSearchParams({\n client_id: this.clientId,\n redirect_uri: this.redirectUri,\n response_type: 'code',\n })\n\n if (options.state) {\n params.set('state', options.state)\n }\n\n if (options.scopes?.length) {\n params.set('scope', options.scopes.join(' '))\n }\n\n return `${this.baseUrl}/v1/auth/authorize?${params.toString()}`\n }\n\n /**\n * Exchange an authorization code for access and refresh tokens\n */\n async exchangeCode(code: string): Promise<TokenResponse> {\n const response = await fetch(`${this.baseUrl}/v1/auth/token`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n client_id: this.clientId,\n client_secret: this.clientSecret,\n redirect_uri: this.redirectUri,\n code,\n }),\n })\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}))\n throw new TokenError(error.error_description || 'Failed to exchange authorization code')\n }\n\n return response.json()\n }\n\n /**\n * Refresh an access token using a refresh token\n */\n async refreshToken(refreshToken: string): Promise<TokenResponse> {\n const response = await fetch(`${this.baseUrl}/v1/auth/token`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n client_id: this.clientId,\n client_secret: this.clientSecret,\n refresh_token: refreshToken,\n }),\n })\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}))\n throw new TokenError(error.error_description || 'Failed to refresh token')\n }\n\n return response.json()\n }\n\n /**\n * Get user information using an access token\n */\n async getUserInfo(accessToken: string): Promise<UserInfo> {\n const response = await fetch(`${this.baseUrl}/v1/auth/userinfo`, {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n },\n })\n\n if (!response.ok) {\n throw new AuthorizationError('Failed to fetch user info')\n }\n\n return response.json()\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/client.ts"],"sourcesContent":["export { SimpleLogin } from './client'\nexport { AuthorizationError, SousaError, TokenError } from './errors'\nexport type {\n AuthorizationUrlOptions,\n AuthorizationUrlResult,\n SimpleLoginConfig,\n TokenResponse,\n UserInfo,\n} from './types'\n","export class SousaError extends Error {\n constructor(\n message: string,\n public code: string,\n public statusCode?: number\n ) {\n super(message);\n this.name = \"SousaError\";\n }\n}\n\nexport class AuthorizationError extends SousaError {\n constructor(message: string) {\n super(message, \"AUTHORIZATION_ERROR\", 401);\n this.name = \"AuthorizationError\";\n }\n}\n\nexport class TokenError extends SousaError {\n constructor(message: string) {\n super(message, \"TOKEN_ERROR\", 400);\n this.name = \"TokenError\";\n }\n}\n","import { AuthorizationError, TokenError } from './errors'\nimport type {\n AuthorizationUrlOptions,\n AuthorizationUrlResult,\n SimpleLoginConfig,\n TokenResponse,\n UserInfo,\n} from './types'\n\ndeclare const __SIMPLELOGIN_BASE_URL__: string\n\nconst BASE_URL = __SIMPLELOGIN_BASE_URL__\n\nfunction generateState(): string {\n const array = new Uint8Array(32)\n crypto.getRandomValues(array)\n return Array.from(array, (b) => b.toString(16).padStart(2, '0')).join('')\n}\n\nfunction getEnv(key: string): string | undefined {\n if (typeof process !== 'undefined' && process.env) {\n return process.env[key]\n }\n return undefined\n}\n\nfunction parseCookie(cookieHeader: string, name: string): string | undefined {\n const match = cookieHeader.match(new RegExp(`(?:^|;\\\\s*)${name}=([^;]*)`))\n return match ? decodeURIComponent(match[1]) : undefined\n}\n\nexport class SimpleLogin {\n private clientId: string\n private clientSecret: string\n private redirectUri: string\n private baseUrl: string\n\n constructor(config: SimpleLoginConfig = {}) {\n this.clientId = config.clientId ?? getEnv('SIMPLELOGIN_CLIENT_ID') ?? ''\n this.clientSecret = config.clientSecret ?? getEnv('SIMPLELOGIN_CLIENT_SECRET') ?? ''\n this.redirectUri = config.redirectUri ?? getEnv('SIMPLELOGIN_REDIRECT_URI') ?? ''\n this.baseUrl = BASE_URL\n\n if (!this.clientId) {\n throw new Error(\n 'clientId is required. Pass it in config or set SIMPLELOGIN_CLIENT_ID env var.',\n )\n }\n if (!this.clientSecret) {\n throw new Error(\n 'clientSecret is required. Pass it in config or set SIMPLELOGIN_CLIENT_SECRET env var.',\n )\n }\n if (!this.redirectUri) {\n throw new Error(\n 'redirectUri is required. Pass it in config or set SIMPLELOGIN_REDIRECT_URI env var.',\n )\n }\n }\n\n /**\n * Returns a Response that redirects to the authorization URL with the state cookie set.\n * @param options - Optional scopes or custom state\n * @returns A 302 redirect Response ready to be returned from your route handler\n */\n redirectToAuth(options: AuthorizationUrlOptions = {}): Response {\n const { url, cookie } = this.getAuthorizationUrl(options)\n return new Response(null, {\n status: 302,\n headers: {\n Location: url,\n 'Set-Cookie': cookie,\n },\n })\n }\n\n /**\n * Generate the authorization URL to redirect users for sign-in\n * @returns The authorization URL, state parameter, and a ready-to-use Set-Cookie header value\n */\n getAuthorizationUrl(options: AuthorizationUrlOptions = {}): AuthorizationUrlResult {\n const state = options.state ?? generateState()\n\n const params = new URLSearchParams({\n client_id: this.clientId,\n redirect_uri: this.redirectUri,\n response_type: 'code',\n state,\n })\n\n if (options.scopes?.length) {\n params.set('scope', options.scopes.join(' '))\n }\n\n const cookie = `SIMPLELOGIN_STATE=${state}; HttpOnly; Secure; SameSite=Lax; Max-Age=600; Path=/`\n\n return {\n url: `${this.baseUrl}/v1/auth/authorize?${params.toString()}`,\n state,\n cookie,\n }\n }\n\n /**\n * Verify the OAuth callback request and exchange the code for tokens.\n * Automatically extracts state from SIMPLELOGIN_STATE cookie and code/state from URL.\n * @param request - The incoming Request object from the callback\n * @returns The token response if verification succeeds\n * @throws AuthorizationError if state is missing or doesn't match\n */\n async handleCallback(request: Request): Promise<TokenResponse> {\n const url = new URL(request.url)\n const code = url.searchParams.get('code')\n const state = url.searchParams.get('state')\n\n if (!code) {\n throw new AuthorizationError('Missing authorization code in callback')\n }\n\n if (!state) {\n throw new AuthorizationError('Missing state parameter in callback')\n }\n\n const cookieHeader = request.headers.get('cookie') ?? ''\n const expectedState = parseCookie(cookieHeader, 'SIMPLELOGIN_STATE')\n\n if (!expectedState) {\n throw new AuthorizationError('Missing SIMPLELOGIN_STATE cookie')\n }\n\n if (state !== expectedState) {\n throw new AuthorizationError('Invalid state parameter')\n }\n\n return this.exchangeCode(code)\n }\n\n /**\n * Exchange an authorization code for access and refresh tokens\n */\n async exchangeCode(code: string): Promise<TokenResponse> {\n const response = await fetch(`${this.baseUrl}/v1/auth/token`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n client_id: this.clientId,\n client_secret: this.clientSecret,\n redirect_uri: this.redirectUri,\n code,\n }),\n })\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}))\n throw new TokenError(error.error_description || 'Failed to exchange authorization code')\n }\n\n return response.json()\n }\n\n /**\n * Refresh an access token using a refresh token\n */\n async refreshToken(refreshToken: string): Promise<TokenResponse> {\n const response = await fetch(`${this.baseUrl}/v1/auth/token`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n client_id: this.clientId,\n client_secret: this.clientSecret,\n refresh_token: refreshToken,\n }),\n })\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}))\n throw new TokenError(error.error_description || 'Failed to refresh token')\n }\n\n return response.json()\n }\n\n /**\n * Get user information using an access token\n */\n async getUserInfo(accessToken: string): Promise<UserInfo> {\n const response = await fetch(`${this.baseUrl}/v1/auth/userinfo`, {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n },\n })\n\n if (!response.ok) {\n throw new AuthorizationError('Failed to fetch user info')\n }\n\n return response.json()\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -7,6 +7,12 @@ interface AuthorizationUrlOptions {
|
|
|
7
7
|
state?: string;
|
|
8
8
|
scopes?: string[];
|
|
9
9
|
}
|
|
10
|
+
interface AuthorizationUrlResult {
|
|
11
|
+
url: string;
|
|
12
|
+
state: string;
|
|
13
|
+
/** Ready-to-use Set-Cookie header value for the state parameter */
|
|
14
|
+
cookie: string;
|
|
15
|
+
}
|
|
10
16
|
interface TokenResponse {
|
|
11
17
|
access_token: string;
|
|
12
18
|
refresh_token: string;
|
|
@@ -41,10 +47,25 @@ declare class SimpleLogin {
|
|
|
41
47
|
private redirectUri;
|
|
42
48
|
private baseUrl;
|
|
43
49
|
constructor(config?: SimpleLoginConfig);
|
|
50
|
+
/**
|
|
51
|
+
* Returns a Response that redirects to the authorization URL with the state cookie set.
|
|
52
|
+
* @param options - Optional scopes or custom state
|
|
53
|
+
* @returns A 302 redirect Response ready to be returned from your route handler
|
|
54
|
+
*/
|
|
55
|
+
redirectToAuth(options?: AuthorizationUrlOptions): Response;
|
|
44
56
|
/**
|
|
45
57
|
* Generate the authorization URL to redirect users for sign-in
|
|
58
|
+
* @returns The authorization URL, state parameter, and a ready-to-use Set-Cookie header value
|
|
59
|
+
*/
|
|
60
|
+
getAuthorizationUrl(options?: AuthorizationUrlOptions): AuthorizationUrlResult;
|
|
61
|
+
/**
|
|
62
|
+
* Verify the OAuth callback request and exchange the code for tokens.
|
|
63
|
+
* Automatically extracts state from SIMPLELOGIN_STATE cookie and code/state from URL.
|
|
64
|
+
* @param request - The incoming Request object from the callback
|
|
65
|
+
* @returns The token response if verification succeeds
|
|
66
|
+
* @throws AuthorizationError if state is missing or doesn't match
|
|
46
67
|
*/
|
|
47
|
-
|
|
68
|
+
handleCallback(request: Request): Promise<TokenResponse>;
|
|
48
69
|
/**
|
|
49
70
|
* Exchange an authorization code for access and refresh tokens
|
|
50
71
|
*/
|
|
@@ -71,4 +92,4 @@ declare class TokenError extends SousaError {
|
|
|
71
92
|
constructor(message: string);
|
|
72
93
|
}
|
|
73
94
|
|
|
74
|
-
export { AuthorizationError, type AuthorizationUrlOptions, SimpleLogin, type SimpleLoginConfig, SousaError, TokenError, type TokenResponse, type UserInfo };
|
|
95
|
+
export { AuthorizationError, type AuthorizationUrlOptions, type AuthorizationUrlResult, SimpleLogin, type SimpleLoginConfig, SousaError, TokenError, type TokenResponse, type UserInfo };
|
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,12 @@ interface AuthorizationUrlOptions {
|
|
|
7
7
|
state?: string;
|
|
8
8
|
scopes?: string[];
|
|
9
9
|
}
|
|
10
|
+
interface AuthorizationUrlResult {
|
|
11
|
+
url: string;
|
|
12
|
+
state: string;
|
|
13
|
+
/** Ready-to-use Set-Cookie header value for the state parameter */
|
|
14
|
+
cookie: string;
|
|
15
|
+
}
|
|
10
16
|
interface TokenResponse {
|
|
11
17
|
access_token: string;
|
|
12
18
|
refresh_token: string;
|
|
@@ -41,10 +47,25 @@ declare class SimpleLogin {
|
|
|
41
47
|
private redirectUri;
|
|
42
48
|
private baseUrl;
|
|
43
49
|
constructor(config?: SimpleLoginConfig);
|
|
50
|
+
/**
|
|
51
|
+
* Returns a Response that redirects to the authorization URL with the state cookie set.
|
|
52
|
+
* @param options - Optional scopes or custom state
|
|
53
|
+
* @returns A 302 redirect Response ready to be returned from your route handler
|
|
54
|
+
*/
|
|
55
|
+
redirectToAuth(options?: AuthorizationUrlOptions): Response;
|
|
44
56
|
/**
|
|
45
57
|
* Generate the authorization URL to redirect users for sign-in
|
|
58
|
+
* @returns The authorization URL, state parameter, and a ready-to-use Set-Cookie header value
|
|
59
|
+
*/
|
|
60
|
+
getAuthorizationUrl(options?: AuthorizationUrlOptions): AuthorizationUrlResult;
|
|
61
|
+
/**
|
|
62
|
+
* Verify the OAuth callback request and exchange the code for tokens.
|
|
63
|
+
* Automatically extracts state from SIMPLELOGIN_STATE cookie and code/state from URL.
|
|
64
|
+
* @param request - The incoming Request object from the callback
|
|
65
|
+
* @returns The token response if verification succeeds
|
|
66
|
+
* @throws AuthorizationError if state is missing or doesn't match
|
|
46
67
|
*/
|
|
47
|
-
|
|
68
|
+
handleCallback(request: Request): Promise<TokenResponse>;
|
|
48
69
|
/**
|
|
49
70
|
* Exchange an authorization code for access and refresh tokens
|
|
50
71
|
*/
|
|
@@ -71,4 +92,4 @@ declare class TokenError extends SousaError {
|
|
|
71
92
|
constructor(message: string);
|
|
72
93
|
}
|
|
73
94
|
|
|
74
|
-
export { AuthorizationError, type AuthorizationUrlOptions, SimpleLogin, type SimpleLoginConfig, SousaError, TokenError, type TokenResponse, type UserInfo };
|
|
95
|
+
export { AuthorizationError, type AuthorizationUrlOptions, type AuthorizationUrlResult, SimpleLogin, type SimpleLoginConfig, SousaError, TokenError, type TokenResponse, type UserInfo };
|
package/dist/index.js
CHANGED
|
@@ -22,12 +22,21 @@ var TokenError = class extends SousaError {
|
|
|
22
22
|
|
|
23
23
|
// src/client.ts
|
|
24
24
|
var BASE_URL = "https://app.simple-login.com";
|
|
25
|
+
function generateState() {
|
|
26
|
+
const array = new Uint8Array(32);
|
|
27
|
+
crypto.getRandomValues(array);
|
|
28
|
+
return Array.from(array, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
29
|
+
}
|
|
25
30
|
function getEnv(key) {
|
|
26
31
|
if (typeof process !== "undefined" && process.env) {
|
|
27
32
|
return process.env[key];
|
|
28
33
|
}
|
|
29
34
|
return void 0;
|
|
30
35
|
}
|
|
36
|
+
function parseCookie(cookieHeader, name) {
|
|
37
|
+
const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`));
|
|
38
|
+
return match ? decodeURIComponent(match[1]) : void 0;
|
|
39
|
+
}
|
|
31
40
|
var SimpleLogin = class {
|
|
32
41
|
clientId;
|
|
33
42
|
clientSecret;
|
|
@@ -54,22 +63,69 @@ var SimpleLogin = class {
|
|
|
54
63
|
);
|
|
55
64
|
}
|
|
56
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Returns a Response that redirects to the authorization URL with the state cookie set.
|
|
68
|
+
* @param options - Optional scopes or custom state
|
|
69
|
+
* @returns A 302 redirect Response ready to be returned from your route handler
|
|
70
|
+
*/
|
|
71
|
+
redirectToAuth(options = {}) {
|
|
72
|
+
const { url, cookie } = this.getAuthorizationUrl(options);
|
|
73
|
+
return new Response(null, {
|
|
74
|
+
status: 302,
|
|
75
|
+
headers: {
|
|
76
|
+
Location: url,
|
|
77
|
+
"Set-Cookie": cookie
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
57
81
|
/**
|
|
58
82
|
* Generate the authorization URL to redirect users for sign-in
|
|
83
|
+
* @returns The authorization URL, state parameter, and a ready-to-use Set-Cookie header value
|
|
59
84
|
*/
|
|
60
85
|
getAuthorizationUrl(options = {}) {
|
|
86
|
+
const state = options.state ?? generateState();
|
|
61
87
|
const params = new URLSearchParams({
|
|
62
88
|
client_id: this.clientId,
|
|
63
89
|
redirect_uri: this.redirectUri,
|
|
64
|
-
response_type: "code"
|
|
90
|
+
response_type: "code",
|
|
91
|
+
state
|
|
65
92
|
});
|
|
66
|
-
if (options.state) {
|
|
67
|
-
params.set("state", options.state);
|
|
68
|
-
}
|
|
69
93
|
if (options.scopes?.length) {
|
|
70
94
|
params.set("scope", options.scopes.join(" "));
|
|
71
95
|
}
|
|
72
|
-
|
|
96
|
+
const cookie = `SIMPLELOGIN_STATE=${state}; HttpOnly; Secure; SameSite=Lax; Max-Age=600; Path=/`;
|
|
97
|
+
return {
|
|
98
|
+
url: `${this.baseUrl}/v1/auth/authorize?${params.toString()}`,
|
|
99
|
+
state,
|
|
100
|
+
cookie
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Verify the OAuth callback request and exchange the code for tokens.
|
|
105
|
+
* Automatically extracts state from SIMPLELOGIN_STATE cookie and code/state from URL.
|
|
106
|
+
* @param request - The incoming Request object from the callback
|
|
107
|
+
* @returns The token response if verification succeeds
|
|
108
|
+
* @throws AuthorizationError if state is missing or doesn't match
|
|
109
|
+
*/
|
|
110
|
+
async handleCallback(request) {
|
|
111
|
+
const url = new URL(request.url);
|
|
112
|
+
const code = url.searchParams.get("code");
|
|
113
|
+
const state = url.searchParams.get("state");
|
|
114
|
+
if (!code) {
|
|
115
|
+
throw new AuthorizationError("Missing authorization code in callback");
|
|
116
|
+
}
|
|
117
|
+
if (!state) {
|
|
118
|
+
throw new AuthorizationError("Missing state parameter in callback");
|
|
119
|
+
}
|
|
120
|
+
const cookieHeader = request.headers.get("cookie") ?? "";
|
|
121
|
+
const expectedState = parseCookie(cookieHeader, "SIMPLELOGIN_STATE");
|
|
122
|
+
if (!expectedState) {
|
|
123
|
+
throw new AuthorizationError("Missing SIMPLELOGIN_STATE cookie");
|
|
124
|
+
}
|
|
125
|
+
if (state !== expectedState) {
|
|
126
|
+
throw new AuthorizationError("Invalid state parameter");
|
|
127
|
+
}
|
|
128
|
+
return this.exchangeCode(code);
|
|
73
129
|
}
|
|
74
130
|
/**
|
|
75
131
|
* Exchange an authorization code for access and refresh tokens
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/errors.ts","../src/client.ts"],"sourcesContent":["export class SousaError extends Error {\n constructor(\n message: string,\n public code: string,\n public statusCode?: number\n ) {\n super(message);\n this.name = \"SousaError\";\n }\n}\n\nexport class AuthorizationError extends SousaError {\n constructor(message: string) {\n super(message, \"AUTHORIZATION_ERROR\", 401);\n this.name = \"AuthorizationError\";\n }\n}\n\nexport class TokenError extends SousaError {\n constructor(message: string) {\n super(message, \"TOKEN_ERROR\", 400);\n this.name = \"TokenError\";\n }\n}\n","import { AuthorizationError, TokenError } from './errors'\nimport type { AuthorizationUrlOptions, SimpleLoginConfig, TokenResponse, UserInfo } from './types'\n\ndeclare const __SIMPLELOGIN_BASE_URL__: string\n\nconst BASE_URL = __SIMPLELOGIN_BASE_URL__\n\nfunction getEnv(key: string): string | undefined {\n if (typeof process !== 'undefined' && process.env) {\n return process.env[key]\n }\n return undefined\n}\n\nexport class SimpleLogin {\n private clientId: string\n private clientSecret: string\n private redirectUri: string\n private baseUrl: string\n\n constructor(config: SimpleLoginConfig = {}) {\n this.clientId = config.clientId ?? getEnv('SIMPLELOGIN_CLIENT_ID') ?? ''\n this.clientSecret = config.clientSecret ?? getEnv('SIMPLELOGIN_CLIENT_SECRET') ?? ''\n this.redirectUri = config.redirectUri ?? getEnv('SIMPLELOGIN_REDIRECT_URI') ?? ''\n this.baseUrl = BASE_URL\n\n if (!this.clientId) {\n throw new Error(\n 'clientId is required. Pass it in config or set SIMPLELOGIN_CLIENT_ID env var.',\n )\n }\n if (!this.clientSecret) {\n throw new Error(\n 'clientSecret is required. Pass it in config or set SIMPLELOGIN_CLIENT_SECRET env var.',\n )\n }\n if (!this.redirectUri) {\n throw new Error(\n 'redirectUri is required. Pass it in config or set SIMPLELOGIN_REDIRECT_URI env var.',\n )\n }\n }\n\n /**\n * Generate the authorization URL to redirect users for sign-in\n */\n getAuthorizationUrl(options: AuthorizationUrlOptions = {}): string {\n const params = new URLSearchParams({\n client_id: this.clientId,\n redirect_uri: this.redirectUri,\n response_type: 'code',\n })\n\n if (options.state) {\n params.set('state', options.state)\n }\n\n if (options.scopes?.length) {\n params.set('scope', options.scopes.join(' '))\n }\n\n return `${this.baseUrl}/v1/auth/authorize?${params.toString()}`\n }\n\n /**\n * Exchange an authorization code for access and refresh tokens\n */\n async exchangeCode(code: string): Promise<TokenResponse> {\n const response = await fetch(`${this.baseUrl}/v1/auth/token`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n client_id: this.clientId,\n client_secret: this.clientSecret,\n redirect_uri: this.redirectUri,\n code,\n }),\n })\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}))\n throw new TokenError(error.error_description || 'Failed to exchange authorization code')\n }\n\n return response.json()\n }\n\n /**\n * Refresh an access token using a refresh token\n */\n async refreshToken(refreshToken: string): Promise<TokenResponse> {\n const response = await fetch(`${this.baseUrl}/v1/auth/token`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n client_id: this.clientId,\n client_secret: this.clientSecret,\n refresh_token: refreshToken,\n }),\n })\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}))\n throw new TokenError(error.error_description || 'Failed to refresh token')\n }\n\n return response.json()\n }\n\n /**\n * Get user information using an access token\n */\n async getUserInfo(accessToken: string): Promise<UserInfo> {\n const response = await fetch(`${this.baseUrl}/v1/auth/userinfo`, {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n },\n })\n\n if (!response.ok) {\n throw new AuthorizationError('Failed to fetch user info')\n }\n\n return response.json()\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/client.ts"],"sourcesContent":["export class SousaError extends Error {\n constructor(\n message: string,\n public code: string,\n public statusCode?: number\n ) {\n super(message);\n this.name = \"SousaError\";\n }\n}\n\nexport class AuthorizationError extends SousaError {\n constructor(message: string) {\n super(message, \"AUTHORIZATION_ERROR\", 401);\n this.name = \"AuthorizationError\";\n }\n}\n\nexport class TokenError extends SousaError {\n constructor(message: string) {\n super(message, \"TOKEN_ERROR\", 400);\n this.name = \"TokenError\";\n }\n}\n","import { AuthorizationError, TokenError } from './errors'\nimport type {\n AuthorizationUrlOptions,\n AuthorizationUrlResult,\n SimpleLoginConfig,\n TokenResponse,\n UserInfo,\n} from './types'\n\ndeclare const __SIMPLELOGIN_BASE_URL__: string\n\nconst BASE_URL = __SIMPLELOGIN_BASE_URL__\n\nfunction generateState(): string {\n const array = new Uint8Array(32)\n crypto.getRandomValues(array)\n return Array.from(array, (b) => b.toString(16).padStart(2, '0')).join('')\n}\n\nfunction getEnv(key: string): string | undefined {\n if (typeof process !== 'undefined' && process.env) {\n return process.env[key]\n }\n return undefined\n}\n\nfunction parseCookie(cookieHeader: string, name: string): string | undefined {\n const match = cookieHeader.match(new RegExp(`(?:^|;\\\\s*)${name}=([^;]*)`))\n return match ? decodeURIComponent(match[1]) : undefined\n}\n\nexport class SimpleLogin {\n private clientId: string\n private clientSecret: string\n private redirectUri: string\n private baseUrl: string\n\n constructor(config: SimpleLoginConfig = {}) {\n this.clientId = config.clientId ?? getEnv('SIMPLELOGIN_CLIENT_ID') ?? ''\n this.clientSecret = config.clientSecret ?? getEnv('SIMPLELOGIN_CLIENT_SECRET') ?? ''\n this.redirectUri = config.redirectUri ?? getEnv('SIMPLELOGIN_REDIRECT_URI') ?? ''\n this.baseUrl = BASE_URL\n\n if (!this.clientId) {\n throw new Error(\n 'clientId is required. Pass it in config or set SIMPLELOGIN_CLIENT_ID env var.',\n )\n }\n if (!this.clientSecret) {\n throw new Error(\n 'clientSecret is required. Pass it in config or set SIMPLELOGIN_CLIENT_SECRET env var.',\n )\n }\n if (!this.redirectUri) {\n throw new Error(\n 'redirectUri is required. Pass it in config or set SIMPLELOGIN_REDIRECT_URI env var.',\n )\n }\n }\n\n /**\n * Returns a Response that redirects to the authorization URL with the state cookie set.\n * @param options - Optional scopes or custom state\n * @returns A 302 redirect Response ready to be returned from your route handler\n */\n redirectToAuth(options: AuthorizationUrlOptions = {}): Response {\n const { url, cookie } = this.getAuthorizationUrl(options)\n return new Response(null, {\n status: 302,\n headers: {\n Location: url,\n 'Set-Cookie': cookie,\n },\n })\n }\n\n /**\n * Generate the authorization URL to redirect users for sign-in\n * @returns The authorization URL, state parameter, and a ready-to-use Set-Cookie header value\n */\n getAuthorizationUrl(options: AuthorizationUrlOptions = {}): AuthorizationUrlResult {\n const state = options.state ?? generateState()\n\n const params = new URLSearchParams({\n client_id: this.clientId,\n redirect_uri: this.redirectUri,\n response_type: 'code',\n state,\n })\n\n if (options.scopes?.length) {\n params.set('scope', options.scopes.join(' '))\n }\n\n const cookie = `SIMPLELOGIN_STATE=${state}; HttpOnly; Secure; SameSite=Lax; Max-Age=600; Path=/`\n\n return {\n url: `${this.baseUrl}/v1/auth/authorize?${params.toString()}`,\n state,\n cookie,\n }\n }\n\n /**\n * Verify the OAuth callback request and exchange the code for tokens.\n * Automatically extracts state from SIMPLELOGIN_STATE cookie and code/state from URL.\n * @param request - The incoming Request object from the callback\n * @returns The token response if verification succeeds\n * @throws AuthorizationError if state is missing or doesn't match\n */\n async handleCallback(request: Request): Promise<TokenResponse> {\n const url = new URL(request.url)\n const code = url.searchParams.get('code')\n const state = url.searchParams.get('state')\n\n if (!code) {\n throw new AuthorizationError('Missing authorization code in callback')\n }\n\n if (!state) {\n throw new AuthorizationError('Missing state parameter in callback')\n }\n\n const cookieHeader = request.headers.get('cookie') ?? ''\n const expectedState = parseCookie(cookieHeader, 'SIMPLELOGIN_STATE')\n\n if (!expectedState) {\n throw new AuthorizationError('Missing SIMPLELOGIN_STATE cookie')\n }\n\n if (state !== expectedState) {\n throw new AuthorizationError('Invalid state parameter')\n }\n\n return this.exchangeCode(code)\n }\n\n /**\n * Exchange an authorization code for access and refresh tokens\n */\n async exchangeCode(code: string): Promise<TokenResponse> {\n const response = await fetch(`${this.baseUrl}/v1/auth/token`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n client_id: this.clientId,\n client_secret: this.clientSecret,\n redirect_uri: this.redirectUri,\n code,\n }),\n })\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}))\n throw new TokenError(error.error_description || 'Failed to exchange authorization code')\n }\n\n return response.json()\n }\n\n /**\n * Refresh an access token using a refresh token\n */\n async refreshToken(refreshToken: string): Promise<TokenResponse> {\n const response = await fetch(`${this.baseUrl}/v1/auth/token`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n client_id: this.clientId,\n client_secret: this.clientSecret,\n refresh_token: refreshToken,\n }),\n })\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}))\n throw new TokenError(error.error_description || 'Failed to refresh token')\n }\n\n return response.json()\n }\n\n /**\n * Get user information using an access token\n */\n async getUserInfo(accessToken: string): Promise<UserInfo> {\n const response = await fetch(`${this.baseUrl}/v1/auth/userinfo`, {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n },\n })\n\n if (!response.ok) {\n throw new AuthorizationError('Failed to fetch user info')\n }\n\n return response.json()\n }\n}\n"],"mappings":"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","names":[]}
|