@stacksjs/socials 0.70.23
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/README.md +65 -0
- package/dist/index.js +2 -0
- package/dist/src/drivers/facebook.d.ts +11 -0
- package/dist/src/drivers/github.d.ts +12 -0
- package/dist/src/drivers/google.d.ts +11 -0
- package/dist/src/drivers/index.d.ts +4 -0
- package/dist/src/drivers/twitter.d.ts +11 -0
- package/dist/src/index.d.ts +1 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Stacks Socials
|
|
2
|
+
|
|
3
|
+
A simple and elegant social authentication package for Stacks, inspired by Laravel's Socialite.
|
|
4
|
+
|
|
5
|
+
## โ๏ธ Features
|
|
6
|
+
|
|
7
|
+
- ๐ Easy OAuth Authentication
|
|
8
|
+
- ๐ Support for Multiple Providers (GitHub, Google, Facebook, etc.)
|
|
9
|
+
- โก๏ธ Simple & Intuitive API
|
|
10
|
+
- ๐ ๏ธ Customizable Provider Configuration
|
|
11
|
+
- ๐ Secure Token Handling
|
|
12
|
+
- ๐ฏ TypeScript Support
|
|
13
|
+
|
|
14
|
+
## ๐ค Usage
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
bun install -d @stacksjs/socials
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Now, you can use it in your project:
|
|
21
|
+
|
|
22
|
+
```js
|
|
23
|
+
import { Socials } from '@stacksjs/socials'
|
|
24
|
+
|
|
25
|
+
// Configure your social provider
|
|
26
|
+
const github = Socials.driver('github')
|
|
27
|
+
|
|
28
|
+
// Redirect to provider
|
|
29
|
+
const authUrl = await github.redirect()
|
|
30
|
+
|
|
31
|
+
// Handle callback
|
|
32
|
+
const user = await github.user()
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Learn more in the docs.
|
|
36
|
+
|
|
37
|
+
## ๐งช Testing
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
bun test
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## ๐ Changelog
|
|
44
|
+
|
|
45
|
+
Please see our [releases](https://github.com/stacksjs/stacks/releases) page for more information on what has changed recently.
|
|
46
|
+
|
|
47
|
+
## ๐ Contributing
|
|
48
|
+
|
|
49
|
+
Please review the [Contributing Guide](https://github.com/stacksjs/contributing) for details.
|
|
50
|
+
|
|
51
|
+
## ๐ Community
|
|
52
|
+
|
|
53
|
+
For help, discussion about best practices, or any other conversation that would benefit from being searchable:
|
|
54
|
+
|
|
55
|
+
[Discussions on GitHub](https://github.com/stacksjs/stacks/discussions)
|
|
56
|
+
|
|
57
|
+
For casual chit-chat with others using this package:
|
|
58
|
+
|
|
59
|
+
[Join the Stacks Discord Server](https://discord.gg/stacksjs)
|
|
60
|
+
|
|
61
|
+
## ๐ License
|
|
62
|
+
|
|
63
|
+
The MIT License (MIT). Please see [LICENSE](https://github.com/stacksjs/stacks/tree/main/LICENSE.md) for more information.
|
|
64
|
+
|
|
65
|
+
Made with ๐
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var X=import.meta.require;import{fetcher as Y}from"@stacksjs/api";import{config as M}from"@stacksjs/config";class K{clientId;clientSecret;redirectUrl;parameters={};_scopes=[];scopeSeparator=",";_stateless=!1;_usesPKCE=!1;user=null;constructor(w){this.clientId=w.clientId,this.clientSecret=w.clientSecret,this.redirectUrl=w.redirectUrl}getCodeFields(w=null){let z={client_id:this.clientId,redirect_uri:this.redirectUrl,scope:this.formatScopes(this.getScopes(),this.scopeSeparator),response_type:"code"};if(this.usesState())z.state=w;if(this.usesPKCE())z.code_challenge=this.getCodeChallenge(),z.code_challenge_method=this.getCodeChallengeMethod();return{...z,...this.parameters}}formatScopes(w,z){return w.join(z)}async userFromToken(w){return{...await this.getUserByToken(w),token:w}}scopes(w){let z=Array.isArray(w)?w:[w];return this._scopes=[...new Set([...this._scopes,...z])],this}setScopes(w){let z=Array.isArray(w)?w:[w];return this._scopes=[...new Set(z)],this}getScopes(){return this._scopes}setRedirectUrl(w){if(typeof w!=="string"||w.length===0)throw Error("[socials] setRedirectUrl requires a non-empty string");let z;try{z=new URL(w)}catch{throw Error(`[socials] setRedirectUrl: invalid URL: ${w}`)}if(z.protocol!=="https:"&&z.protocol!=="http:")throw Error(`[socials] setRedirectUrl protocol must be http(s)://, got ${z.protocol}`);return this.redirectUrl=w,this}usesState(){return!this._stateless}validateState(w,z){if(typeof w!=="string"||typeof z!=="string")return!1;if(w.length===0||z.length===0)return!1;if(w.length!==z.length)return!1;try{let{timingSafeEqual:D}=X("crypto");return D(Buffer.from(w,"utf8"),Buffer.from(z,"utf8"))}catch{let D=0;for(let F=0;F<w.length;F++)D|=w.charCodeAt(F)^z.charCodeAt(F);return D===0}}isStateless(){return this._stateless}stateless(){return this._stateless=!0,this}getState(){let w=new Uint8Array(32);return crypto.getRandomValues(w),Array.from(w).map((z)=>z.toString(16).padStart(2,"0")).join("")}usesPKCE(){return this._usesPKCE}enablePKCE(){return this._usesPKCE=!0,this}getCodeVerifier(){let w=new Uint8Array(48);return crypto.getRandomValues(w),Array.from(w).map((z)=>z.toString(16).padStart(2,"0")).join("")}async getCodeChallenge(){let z=new TextEncoder().encode(this.getCodeVerifier()),D=await crypto.subtle.digest("SHA-256",z),{Buffer:F}=await import("buffer");return F.from(new Uint8Array(D)).toString("base64url")}getCodeChallengeMethod(){return"S256"}with(w){return this.parameters=w,this}buildAuthUrlFromBase(w,z){let D=new URLSearchParams(this.getCodeFields(z));return`${w}?${D.toString()}`}}class G extends Error{constructor(w){super(w);this.name="ConfigException"}}class $ extends K{baseUrl="https://www.facebook.com";apiUrl="https://graph.facebook.com";getConfig(){let w={clientId:M.services.facebook?.clientId??"",clientSecret:M.services.facebook?.clientSecret??"",redirectUrl:M.services.facebook?.redirectUrl??"",scopes:M.services.facebook?.scopes??["email","public_profile"]};return this.setScopes(w.scopes),w}async getAuthUrl(){let w=this.getState(),{clientId:z,redirectUrl:D,scopes:F}=this.getConfig();return this.validateConfig(),`${this.baseUrl}/v18.0/dialog/oauth?${new URLSearchParams({client_id:z,redirect_uri:D,scope:F.join(","),state:w,response_type:"code"}).toString()}`}async getAccessToken(w){let{clientId:z,clientSecret:D,redirectUrl:F}=this.getConfig();this.validateConfig();let J=await Y.get(`${this.apiUrl}/v18.0/oauth/access_token?${new URLSearchParams({client_id:z,client_secret:D,redirect_uri:F,code:w}).toString()}`);if(J.data.error)throw Error(`Facebook OAuth error: ${J.data.error.message}`);return J.data.access_token}async getUserByToken(w){let z=await Y.get(`${this.apiUrl}/v18.0/me?${new URLSearchParams({access_token:w,fields:"id,name,email,picture"}).toString()}`);return{id:z.data.id,nickname:null,name:z.data.name,email:z.data.email??null,avatar:z.data.picture?.data.url??null,token:w,raw:z.data}}validateConfig(){let{clientId:w,clientSecret:z,redirectUrl:D}=this.getConfig();if(!w)throw new G("Facebook client ID not provided");if(!z)throw new G("Facebook client secret not provided");if(!D)throw new G("Facebook redirect URL not provided")}getTokenUrl(){return`${this.apiUrl}/v18.0/oauth/access_token`}}import{fetcher as W}from"@stacksjs/api";import{config as N}from"@stacksjs/config";class H extends K{baseUrl="https://github.com";apiUrl="https://api.github.com";getConfig(){let w={clientId:N.services.github?.clientId??"",clientSecret:N.services.github?.clientSecret??"",redirectUrl:N.services.github?.redirectUrl??"",scopes:N.services.github?.scopes??["read:user","user:email"]};return this.setScopes(w.scopes),w}async getAuthUrl(){let w=this.getState(),{clientId:z,redirectUrl:D,scopes:F}=this.getConfig();return this.validateConfig(),`${this.baseUrl}/login/oauth/authorize?${new URLSearchParams({client_id:z,redirect_uri:D,scope:F.join(" "),state:w,response_type:"code"}).toString()}`}async getAccessToken(w){let{clientId:z,clientSecret:D,redirectUrl:F}=this.getConfig();this.validateConfig();let J=await W.post(`${this.baseUrl}/login/oauth/access_token`,{client_id:z,client_secret:D,code:w,redirect_uri:F});if(J.data.error)throw Error(`GitHub OAuth error: ${J.data.error_description}`);return J.data.access_token}async getUserByToken(w){let[z,D]=await Promise.all([W.withHeaders({Accept:"application/vnd.github.v3+json",Authorization:`token ${w}`}).get(`${this.apiUrl}/user`),W.withHeaders({Accept:"application/vnd.github.v3+json",Authorization:`token ${w}`}).get(`${this.apiUrl}/user/emails`)]);return{id:z.data.id.toString(),nickname:z.data.login,name:z.data.name,email:this.getEmail(D.data)??z.data.email??null,avatar:z.data.avatar_url,token:w,raw:z.data}}getEmail(w){if(!Array.isArray(w)||w.length===0)return null;let z=w.find((F)=>F.primary&&F.verified),D=w.find((F)=>F.verified);return(z||D||w.find((F)=>F.primary)||w[0])?.email??null}validateConfig(){let{clientId:w,clientSecret:z,redirectUrl:D}=this.getConfig();if(!w)throw new G("GitHub client ID not provided");if(!z)throw new G("GitHub client secret not provided");if(!D)throw new G("GitHub redirect URL not provided")}getTokenUrl(){return`${this.baseUrl}/login/oauth/access_token`}}import{fetcher as Z}from"@stacksjs/api";import{config as O}from"@stacksjs/config";class q extends K{baseUrl="https://accounts.google.com";apiUrl="https://www.googleapis.com";getConfig(){let w={clientId:O.services.google?.clientId??"",clientSecret:O.services.google?.clientSecret??"",redirectUrl:O.services.google?.redirectUrl??"",scopes:O.services.google?.scopes??["openid","email"]};return this.setScopes(w.scopes),w}async getAuthUrl(){let w=this.getState(),{clientId:z,redirectUrl:D,scopes:F}=this.getConfig();return this.validateConfig(),`${this.baseUrl}/o/oauth2/v2/auth?${new URLSearchParams({client_id:z,redirect_uri:D,scope:F.join(" "),state:w,response_type:"code",access_type:"offline",prompt:"consent"}).toString()}`}async getAccessToken(w){let{clientId:z,clientSecret:D,redirectUrl:F}=this.getConfig();this.validateConfig();let J=await Z.post(`${this.baseUrl}/oauth2/v4/token`,{client_id:z,client_secret:D,code:w,redirect_uri:F,grant_type:"authorization_code"});if(J.data.error)throw Error(`Google OAuth error: ${J.data.error_description}`);return J.data.access_token}async getUserByToken(w){let z=await Z.withHeaders({Authorization:`Bearer ${w}`}).get(`${this.apiUrl}/oauth2/v2/userinfo`);return{id:z.data.id,nickname:z.data.given_name,name:z.data.name,email:z.data.email,avatar:z.data.picture,token:w,raw:z.data}}validateConfig(){let{clientId:w,clientSecret:z,redirectUrl:D}=this.getConfig();if(!w)throw new G("Google client ID not provided");if(!z)throw new G("Google client secret not provided");if(!D)throw new G("Google redirect URL not provided")}getTokenUrl(){return`${this.baseUrl}/oauth2/v4/token`}}import{Buffer as L}from"buffer";import{createHash as B,randomBytes as P}from"crypto";import{fetcher as _}from"@stacksjs/api";import{config as Q}from"@stacksjs/config";class E extends K{baseUrl="https://twitter.com";apiUrl="https://api.twitter.com";codeVerifier=null;getConfig(){let w={clientId:Q.services.twitter?.clientId??"",clientSecret:Q.services.twitter?.clientSecret??"",redirectUrl:Q.services.twitter?.redirectUrl??"",scopes:Q.services.twitter?.scopes??["users.read","tweet.read"]};return this.setScopes(w.scopes),w}generateCodeVerifier(){return P(32).toString("base64").replace(/[^a-z0-9]/gi,"").substring(0,128)}generateCodeChallenge(w){return B("sha256").update(w).digest("base64").replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}async getAuthUrl(){let w=this.getState(),{clientId:z,redirectUrl:D,scopes:F}=this.getConfig();this.validateConfig(),this.codeVerifier=this.generateCodeVerifier();let J=this.generateCodeChallenge(this.codeVerifier);return`${this.baseUrl}/i/oauth2/authorize?${new URLSearchParams({client_id:z,redirect_uri:D,scope:F.join(" "),state:w,response_type:"code",code_challenge:J,code_challenge_method:"S256"}).toString()}`}async getAccessToken(w){let{clientId:z,clientSecret:D,redirectUrl:F}=this.getConfig();if(this.validateConfig(),!this.codeVerifier)throw Error("Code verifier not found. Please ensure getAuthUrl() is called first.");let J=L.from(`${z}:${D}`).toString("base64"),T=await _.withHeaders({Authorization:`Basic ${J}`,"Content-Type":"application/x-www-form-urlencoded"}).post(`${this.apiUrl}/2/oauth2/token`,{code:w,grant_type:"authorization_code",redirect_uri:F,code_verifier:this.codeVerifier});if(T.data.error)throw Error(`Twitter OAuth error: ${T.data.error_description}`);return T.data.access_token}async getUserByToken(w){let z=await _.withHeaders({Authorization:`Bearer ${w}`}).get(`${this.apiUrl}/2/users/me?user.fields=profile_image_url`);return{id:z.data.id,nickname:z.data.username,name:z.data.name,email:z.data.email??null,avatar:z.data.profile_image_url??null,token:w,raw:z.data}}validateConfig(){let{clientId:w,clientSecret:z,redirectUrl:D}=this.getConfig();if(!w)throw new G("Twitter client ID not provided");if(!z)throw new G("Twitter client secret not provided");if(!D)throw new G("Twitter redirect URL not provided")}getTokenUrl(){return`${this.apiUrl}/2/oauth2/token`}}export{E as TwitterProvider,q as GoogleProvider,H as GitHubProvider,$ as FacebookProvider};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AbstractProvider } from '../abstract';
|
|
2
|
+
import type { ProviderInterface, SocialUser } from '../types';
|
|
3
|
+
export declare class FacebookProvider extends AbstractProvider implements ProviderInterface {
|
|
4
|
+
protected baseUrl: string;
|
|
5
|
+
protected apiUrl: string;
|
|
6
|
+
getAuthUrl(): Promise<string>;
|
|
7
|
+
getAccessToken(code: string): Promise<string>;
|
|
8
|
+
getUserByToken(token: string): Promise<SocialUser>;
|
|
9
|
+
protected validateConfig(): void;
|
|
10
|
+
protected getTokenUrl(): string;
|
|
11
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { AbstractProvider } from '../abstract';
|
|
2
|
+
import type { GitHubEmail, ProviderInterface, SocialUser } from '../types';
|
|
3
|
+
export declare class GitHubProvider extends AbstractProvider implements ProviderInterface {
|
|
4
|
+
protected baseUrl: string;
|
|
5
|
+
protected apiUrl: string;
|
|
6
|
+
getAuthUrl(): Promise<string>;
|
|
7
|
+
getAccessToken(code: string): Promise<string>;
|
|
8
|
+
getUserByToken(token: string): Promise<SocialUser>;
|
|
9
|
+
protected getEmail(emails: GitHubEmail[]): string | null;
|
|
10
|
+
protected validateConfig(): void;
|
|
11
|
+
protected getTokenUrl(): string;
|
|
12
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AbstractProvider } from '../abstract';
|
|
2
|
+
import type { ProviderInterface, SocialUser } from '../types';
|
|
3
|
+
export declare class GoogleProvider extends AbstractProvider implements ProviderInterface {
|
|
4
|
+
protected baseUrl: string;
|
|
5
|
+
protected apiUrl: string;
|
|
6
|
+
getAuthUrl(): Promise<string>;
|
|
7
|
+
getAccessToken(code: string): Promise<string>;
|
|
8
|
+
getUserByToken(token: string): Promise<SocialUser>;
|
|
9
|
+
protected validateConfig(): void;
|
|
10
|
+
protected getTokenUrl(): string;
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AbstractProvider } from '../abstract';
|
|
2
|
+
import type { ProviderInterface, SocialUser } from '../types';
|
|
3
|
+
export declare class TwitterProvider extends AbstractProvider implements ProviderInterface {
|
|
4
|
+
protected baseUrl: string;
|
|
5
|
+
protected apiUrl: string;
|
|
6
|
+
getAuthUrl(): Promise<string>;
|
|
7
|
+
getAccessToken(code: string): Promise<string>;
|
|
8
|
+
getUserByToken(token: string): Promise<SocialUser>;
|
|
9
|
+
protected validateConfig(): void;
|
|
10
|
+
protected getTokenUrl(): string;
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './drivers';
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stacksjs/socials",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.70.23",
|
|
5
|
+
"description": "A simple and elegant social authentication package for Stacks.",
|
|
6
|
+
"author": "Chris Breuer",
|
|
7
|
+
"contributors": [
|
|
8
|
+
"Chris Breuer <chris@stacksjs.com>"
|
|
9
|
+
],
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"funding": "https://github.com/sponsors/chrisbbreuer",
|
|
12
|
+
"homepage": "https://github.com/stacksjs/stacks/tree/main/storage/framework/core/socials#readme",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/stacksjs/stacks.git",
|
|
16
|
+
"directory": "./storage/framework/core/socials"
|
|
17
|
+
},
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/stacksjs/stacks/issues"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"social-auth",
|
|
23
|
+
"oauth",
|
|
24
|
+
"socials",
|
|
25
|
+
"stacks"
|
|
26
|
+
],
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"bun": "./src/index.ts",
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"import": "./dist/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./*": {
|
|
34
|
+
"bun": "./src/*",
|
|
35
|
+
"import": "./dist/*"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"module": "dist/index.js",
|
|
39
|
+
"types": "dist/index.d.ts",
|
|
40
|
+
"files": [
|
|
41
|
+
"README.md",
|
|
42
|
+
"dist"
|
|
43
|
+
],
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "bun build.ts",
|
|
46
|
+
"typecheck": "bun tsc --noEmit",
|
|
47
|
+
"prepublishOnly": "bun run build"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"better-dx": "^0.2.12",
|
|
51
|
+
"@stacksjs/error-handling": "0.70.23",
|
|
52
|
+
"@stacksjs/router": "0.70.23"
|
|
53
|
+
}
|
|
54
|
+
}
|