@flowsta/auth 2.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/README.md +191 -0
- package/dist/chunk-NBWAVXMK.mjs +183 -0
- package/dist/index.d.mts +112 -0
- package/dist/index.d.ts +112 -0
- package/dist/index.js +207 -0
- package/dist/index.mjs +8 -0
- package/dist/react.d.mts +37 -0
- package/dist/react.d.ts +37 -0
- package/dist/react.js +316 -0
- package/dist/react.mjs +118 -0
- package/package.json +60 -0
package/README.md
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# @flowsta/auth
|
|
2
|
+
|
|
3
|
+
Flowsta Auth SDK 2.0 - OAuth-only authentication for web applications.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🔐 **OAuth 2.0 + PKCE** - Secure authentication without client secrets
|
|
8
|
+
- 🌐 **Zero-Knowledge** - Your users' data stays private
|
|
9
|
+
- ⚡ **Simple Integration** - Just a few lines of code
|
|
10
|
+
- 🎨 **Framework Support** - Vanilla JS, React, and more
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @flowsta/auth
|
|
16
|
+
# or
|
|
17
|
+
yarn add @flowsta/auth
|
|
18
|
+
# or
|
|
19
|
+
pnpm add @flowsta/auth
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
### 1. Create an App
|
|
25
|
+
|
|
26
|
+
Go to [dev.flowsta.com](https://dev.flowsta.com) and create an app to get your Client ID.
|
|
27
|
+
|
|
28
|
+
### 2. Add the Login Button
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { FlowstaAuth } from '@flowsta/auth';
|
|
32
|
+
|
|
33
|
+
const auth = new FlowstaAuth({
|
|
34
|
+
clientId: 'your-client-id',
|
|
35
|
+
redirectUri: 'https://yoursite.com/auth/callback',
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Redirect to Flowsta login
|
|
39
|
+
document.getElementById('login-btn').onclick = () => auth.login();
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 3. Handle the Callback
|
|
43
|
+
|
|
44
|
+
On your redirect URI page (`/auth/callback`):
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { FlowstaAuth } from '@flowsta/auth';
|
|
48
|
+
|
|
49
|
+
const auth = new FlowstaAuth({
|
|
50
|
+
clientId: 'your-client-id',
|
|
51
|
+
redirectUri: 'https://yoursite.com/auth/callback',
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Handle the OAuth callback
|
|
55
|
+
try {
|
|
56
|
+
const user = await auth.handleCallback();
|
|
57
|
+
console.log('Logged in as:', user.email);
|
|
58
|
+
window.location.href = '/dashboard';
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('Login failed:', error.message);
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 4. Check Authentication Status
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
if (auth.isAuthenticated()) {
|
|
68
|
+
const user = auth.getUser();
|
|
69
|
+
console.log('Welcome,', user.displayName);
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## React Integration
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
import { FlowstaAuthProvider, useFlowstaAuth } from '@flowsta/auth/react';
|
|
77
|
+
|
|
78
|
+
// Wrap your app
|
|
79
|
+
function App() {
|
|
80
|
+
return (
|
|
81
|
+
<FlowstaAuthProvider
|
|
82
|
+
clientId="your-client-id"
|
|
83
|
+
redirectUri="https://yoursite.com/auth/callback"
|
|
84
|
+
>
|
|
85
|
+
<MyApp />
|
|
86
|
+
</FlowstaAuthProvider>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Use in components
|
|
91
|
+
function LoginButton() {
|
|
92
|
+
const { isAuthenticated, user, login, logout } = useFlowstaAuth();
|
|
93
|
+
|
|
94
|
+
if (isAuthenticated) {
|
|
95
|
+
return (
|
|
96
|
+
<div>
|
|
97
|
+
<span>Hello, {user.displayName}!</span>
|
|
98
|
+
<button onClick={logout}>Logout</button>
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return <button onClick={login}>Sign in with Flowsta</button>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Callback page
|
|
107
|
+
function AuthCallback() {
|
|
108
|
+
const { handleCallback, isLoading, error } = useFlowstaAuth();
|
|
109
|
+
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
handleCallback()
|
|
112
|
+
.then(() => window.location.href = '/dashboard')
|
|
113
|
+
.catch(console.error);
|
|
114
|
+
}, []);
|
|
115
|
+
|
|
116
|
+
if (isLoading) return <p>Logging in...</p>;
|
|
117
|
+
if (error) return <p>Error: {error}</p>;
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## API Reference
|
|
123
|
+
|
|
124
|
+
### FlowstaAuth
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
const auth = new FlowstaAuth({
|
|
128
|
+
clientId: string; // Required: Your app's client ID
|
|
129
|
+
redirectUri: string; // Required: OAuth callback URL
|
|
130
|
+
scopes?: string[]; // Optional: ['profile', 'email'] (default)
|
|
131
|
+
loginUrl?: string; // Optional: Flowsta login URL
|
|
132
|
+
apiUrl?: string; // Optional: Flowsta API URL
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
#### Methods
|
|
137
|
+
|
|
138
|
+
| Method | Returns | Description |
|
|
139
|
+
|--------|---------|-------------|
|
|
140
|
+
| `login()` | `Promise<void>` | Redirect to Flowsta login |
|
|
141
|
+
| `handleCallback()` | `Promise<FlowstaUser>` | Handle OAuth callback |
|
|
142
|
+
| `logout()` | `void` | Log out the user |
|
|
143
|
+
| `isAuthenticated()` | `boolean` | Check if user is logged in |
|
|
144
|
+
| `getUser()` | `FlowstaUser \| null` | Get current user |
|
|
145
|
+
| `getAccessToken()` | `string \| null` | Get access token |
|
|
146
|
+
| `getState()` | `AuthState` | Get full auth state |
|
|
147
|
+
|
|
148
|
+
### FlowstaUser
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
interface FlowstaUser {
|
|
152
|
+
id: string;
|
|
153
|
+
email?: string; // If 'email' scope was granted
|
|
154
|
+
username?: string; // User's username (if set)
|
|
155
|
+
displayName?: string; // Display name
|
|
156
|
+
profilePicture?: string; // Profile picture URL
|
|
157
|
+
agentPubKey?: string; // Holochain agent public key
|
|
158
|
+
did?: string; // Decentralized Identifier
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Security
|
|
163
|
+
|
|
164
|
+
This SDK uses **OAuth 2.0 Authorization Code Flow with PKCE**, which means:
|
|
165
|
+
|
|
166
|
+
- ✅ No client secrets needed (safe for browser/mobile apps)
|
|
167
|
+
- ✅ Authorization codes are protected by PKCE challenge
|
|
168
|
+
- ✅ State parameter prevents CSRF attacks
|
|
169
|
+
- ✅ Tokens are securely stored in localStorage
|
|
170
|
+
|
|
171
|
+
## Migration from SDK 1.x
|
|
172
|
+
|
|
173
|
+
SDK 2.0 removes direct email/password authentication. All users now authenticate through Flowsta's hosted login page.
|
|
174
|
+
|
|
175
|
+
**Before (SDK 1.x):**
|
|
176
|
+
```typescript
|
|
177
|
+
// ❌ Deprecated - do not use
|
|
178
|
+
await auth.login(email, password);
|
|
179
|
+
await auth.register(email, password);
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**After (SDK 2.0):**
|
|
183
|
+
```typescript
|
|
184
|
+
// ✅ OAuth redirect
|
|
185
|
+
await auth.login(); // Redirects to login.flowsta.com
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## License
|
|
189
|
+
|
|
190
|
+
MIT
|
|
191
|
+
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
async function generatePKCEPair() {
|
|
3
|
+
const verifier = generateRandomString(128);
|
|
4
|
+
const encoder = new TextEncoder();
|
|
5
|
+
const data = encoder.encode(verifier);
|
|
6
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
7
|
+
const challenge = base64UrlEncode(digest);
|
|
8
|
+
return { verifier, challenge };
|
|
9
|
+
}
|
|
10
|
+
function generateRandomString(length) {
|
|
11
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
|
|
12
|
+
const array = new Uint8Array(length);
|
|
13
|
+
crypto.getRandomValues(array);
|
|
14
|
+
return Array.from(array, (byte) => chars[byte % chars.length]).join("");
|
|
15
|
+
}
|
|
16
|
+
function base64UrlEncode(buffer) {
|
|
17
|
+
const bytes = new Uint8Array(buffer);
|
|
18
|
+
let binary = "";
|
|
19
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
20
|
+
binary += String.fromCharCode(bytes[i]);
|
|
21
|
+
}
|
|
22
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
23
|
+
}
|
|
24
|
+
var FlowstaAuth = class {
|
|
25
|
+
constructor(config) {
|
|
26
|
+
this.accessToken = null;
|
|
27
|
+
this.user = null;
|
|
28
|
+
this.config = {
|
|
29
|
+
clientId: config.clientId,
|
|
30
|
+
redirectUri: config.redirectUri,
|
|
31
|
+
scopes: config.scopes || ["profile", "email"],
|
|
32
|
+
loginUrl: config.loginUrl || "https://login.flowsta.com",
|
|
33
|
+
apiUrl: config.apiUrl || "https://auth-api.flowsta.com"
|
|
34
|
+
};
|
|
35
|
+
this.restoreSession();
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Redirect user to Flowsta login page
|
|
39
|
+
* User will be redirected back to redirectUri after authentication
|
|
40
|
+
*/
|
|
41
|
+
async login() {
|
|
42
|
+
const { verifier, challenge } = await generatePKCEPair();
|
|
43
|
+
const state = generateRandomString(32);
|
|
44
|
+
sessionStorage.setItem("flowsta_code_verifier", verifier);
|
|
45
|
+
sessionStorage.setItem("flowsta_state", state);
|
|
46
|
+
const params = new URLSearchParams({
|
|
47
|
+
client_id: this.config.clientId,
|
|
48
|
+
redirect_uri: this.config.redirectUri,
|
|
49
|
+
response_type: "code",
|
|
50
|
+
scope: this.config.scopes.join(" "),
|
|
51
|
+
state,
|
|
52
|
+
code_challenge: challenge,
|
|
53
|
+
code_challenge_method: "S256"
|
|
54
|
+
});
|
|
55
|
+
window.location.href = `${this.config.loginUrl}/login?${params.toString()}`;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Handle OAuth callback after user authentication
|
|
59
|
+
* Call this on your redirect URI page
|
|
60
|
+
* @returns The authenticated user
|
|
61
|
+
*/
|
|
62
|
+
async handleCallback() {
|
|
63
|
+
const params = new URLSearchParams(window.location.search);
|
|
64
|
+
const error = params.get("error");
|
|
65
|
+
if (error) {
|
|
66
|
+
const description = params.get("error_description") || error;
|
|
67
|
+
throw new Error(description);
|
|
68
|
+
}
|
|
69
|
+
const code = params.get("code");
|
|
70
|
+
if (!code) {
|
|
71
|
+
throw new Error("No authorization code received");
|
|
72
|
+
}
|
|
73
|
+
const state = params.get("state");
|
|
74
|
+
const storedState = sessionStorage.getItem("flowsta_state");
|
|
75
|
+
if (!state || state !== storedState) {
|
|
76
|
+
throw new Error("Invalid state parameter - possible CSRF attack");
|
|
77
|
+
}
|
|
78
|
+
const codeVerifier = sessionStorage.getItem("flowsta_code_verifier");
|
|
79
|
+
if (!codeVerifier) {
|
|
80
|
+
throw new Error("Missing PKCE code verifier");
|
|
81
|
+
}
|
|
82
|
+
const tokenResponse = await fetch(`${this.config.apiUrl}/oauth/token`, {
|
|
83
|
+
method: "POST",
|
|
84
|
+
headers: { "Content-Type": "application/json" },
|
|
85
|
+
body: JSON.stringify({
|
|
86
|
+
grant_type: "authorization_code",
|
|
87
|
+
code,
|
|
88
|
+
redirect_uri: this.config.redirectUri,
|
|
89
|
+
client_id: this.config.clientId,
|
|
90
|
+
code_verifier: codeVerifier
|
|
91
|
+
})
|
|
92
|
+
});
|
|
93
|
+
if (!tokenResponse.ok) {
|
|
94
|
+
const errorData = await tokenResponse.json();
|
|
95
|
+
throw new Error(errorData.error_description || "Token exchange failed");
|
|
96
|
+
}
|
|
97
|
+
const { access_token, refresh_token } = await tokenResponse.json();
|
|
98
|
+
sessionStorage.removeItem("flowsta_code_verifier");
|
|
99
|
+
sessionStorage.removeItem("flowsta_state");
|
|
100
|
+
const userResponse = await fetch(`${this.config.apiUrl}/oauth/userinfo`, {
|
|
101
|
+
headers: { "Authorization": `Bearer ${access_token}` }
|
|
102
|
+
});
|
|
103
|
+
if (!userResponse.ok) {
|
|
104
|
+
throw new Error("Failed to fetch user info");
|
|
105
|
+
}
|
|
106
|
+
const userData = await userResponse.json();
|
|
107
|
+
this.accessToken = access_token;
|
|
108
|
+
this.user = {
|
|
109
|
+
id: userData.sub || userData.id,
|
|
110
|
+
email: userData.email,
|
|
111
|
+
username: userData.preferred_username,
|
|
112
|
+
displayName: userData.display_name || userData.name,
|
|
113
|
+
profilePicture: userData.picture || userData.profile_picture,
|
|
114
|
+
agentPubKey: userData.agent_pub_key,
|
|
115
|
+
did: userData.did
|
|
116
|
+
};
|
|
117
|
+
localStorage.setItem("flowsta_access_token", access_token);
|
|
118
|
+
localStorage.setItem("flowsta_user", JSON.stringify(this.user));
|
|
119
|
+
if (refresh_token) {
|
|
120
|
+
localStorage.setItem("flowsta_refresh_token", refresh_token);
|
|
121
|
+
}
|
|
122
|
+
return this.user;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Log out the current user
|
|
126
|
+
*/
|
|
127
|
+
logout() {
|
|
128
|
+
this.accessToken = null;
|
|
129
|
+
this.user = null;
|
|
130
|
+
localStorage.removeItem("flowsta_access_token");
|
|
131
|
+
localStorage.removeItem("flowsta_user");
|
|
132
|
+
localStorage.removeItem("flowsta_refresh_token");
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Check if user is authenticated
|
|
136
|
+
*/
|
|
137
|
+
isAuthenticated() {
|
|
138
|
+
return !!this.accessToken && !!this.user;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Get the current user
|
|
142
|
+
*/
|
|
143
|
+
getUser() {
|
|
144
|
+
return this.user;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Get the current access token
|
|
148
|
+
*/
|
|
149
|
+
getAccessToken() {
|
|
150
|
+
return this.accessToken;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Get current auth state
|
|
154
|
+
*/
|
|
155
|
+
getState() {
|
|
156
|
+
return {
|
|
157
|
+
isAuthenticated: this.isAuthenticated(),
|
|
158
|
+
user: this.user,
|
|
159
|
+
accessToken: this.accessToken,
|
|
160
|
+
isLoading: false,
|
|
161
|
+
error: null
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
restoreSession() {
|
|
165
|
+
if (typeof localStorage === "undefined") return;
|
|
166
|
+
const token = localStorage.getItem("flowsta_access_token");
|
|
167
|
+
const userJson = localStorage.getItem("flowsta_user");
|
|
168
|
+
if (token && userJson) {
|
|
169
|
+
try {
|
|
170
|
+
this.accessToken = token;
|
|
171
|
+
this.user = JSON.parse(userJson);
|
|
172
|
+
} catch {
|
|
173
|
+
this.logout();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
var index_default = FlowstaAuth;
|
|
179
|
+
|
|
180
|
+
export {
|
|
181
|
+
FlowstaAuth,
|
|
182
|
+
index_default
|
|
183
|
+
};
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flowsta Auth SDK 2.0
|
|
3
|
+
*
|
|
4
|
+
* OAuth-only authentication for partner sites.
|
|
5
|
+
* All authentication flows through login.flowsta.com
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - OAuth 2.0 Authorization Code Flow with PKCE
|
|
9
|
+
* - Zero-knowledge architecture
|
|
10
|
+
* - No client secrets needed (PKCE provides security)
|
|
11
|
+
* - Simple "Sign in with Flowsta" integration
|
|
12
|
+
*/
|
|
13
|
+
interface FlowstaAuthConfig {
|
|
14
|
+
/** Your Flowsta application client ID (from dev.flowsta.com) */
|
|
15
|
+
clientId: string;
|
|
16
|
+
/** The URI to redirect back to after authentication */
|
|
17
|
+
redirectUri: string;
|
|
18
|
+
/** OAuth scopes to request. Default: ['profile', 'email'] */
|
|
19
|
+
scopes?: string[];
|
|
20
|
+
/** The Flowsta login URL. Default: 'https://login.flowsta.com' */
|
|
21
|
+
loginUrl?: string;
|
|
22
|
+
/** The Flowsta API URL. Default: 'https://auth-api.flowsta.com' */
|
|
23
|
+
apiUrl?: string;
|
|
24
|
+
}
|
|
25
|
+
interface FlowstaUser {
|
|
26
|
+
/** User's unique ID */
|
|
27
|
+
id: string;
|
|
28
|
+
/** User's email address (if 'email' scope was granted) */
|
|
29
|
+
email?: string;
|
|
30
|
+
/** User's username (if set) */
|
|
31
|
+
username?: string;
|
|
32
|
+
/** User's display name */
|
|
33
|
+
displayName?: string;
|
|
34
|
+
/** User's profile picture URL */
|
|
35
|
+
profilePicture?: string;
|
|
36
|
+
/** User's Holochain agent public key */
|
|
37
|
+
agentPubKey?: string;
|
|
38
|
+
/** User's DID (Decentralized Identifier) */
|
|
39
|
+
did?: string;
|
|
40
|
+
}
|
|
41
|
+
interface AuthState {
|
|
42
|
+
/** Whether the user is authenticated */
|
|
43
|
+
isAuthenticated: boolean;
|
|
44
|
+
/** The current user (null if not authenticated) */
|
|
45
|
+
user: FlowstaUser | null;
|
|
46
|
+
/** The access token (null if not authenticated) */
|
|
47
|
+
accessToken: string | null;
|
|
48
|
+
/** Whether authentication is loading */
|
|
49
|
+
isLoading: boolean;
|
|
50
|
+
/** Any authentication error */
|
|
51
|
+
error: string | null;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* FlowstaAuth class - Main SDK entry point
|
|
55
|
+
*
|
|
56
|
+
* Usage:
|
|
57
|
+
* ```typescript
|
|
58
|
+
* const auth = new FlowstaAuth({
|
|
59
|
+
* clientId: 'your-client-id',
|
|
60
|
+
* redirectUri: 'https://yoursite.com/auth/callback'
|
|
61
|
+
* });
|
|
62
|
+
*
|
|
63
|
+
* // Redirect to login
|
|
64
|
+
* auth.login();
|
|
65
|
+
*
|
|
66
|
+
* // Handle callback (on your redirect URI page)
|
|
67
|
+
* await auth.handleCallback();
|
|
68
|
+
*
|
|
69
|
+
* // Get current user
|
|
70
|
+
* const user = auth.getUser();
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
declare class FlowstaAuth {
|
|
74
|
+
private config;
|
|
75
|
+
private accessToken;
|
|
76
|
+
private user;
|
|
77
|
+
constructor(config: FlowstaAuthConfig);
|
|
78
|
+
/**
|
|
79
|
+
* Redirect user to Flowsta login page
|
|
80
|
+
* User will be redirected back to redirectUri after authentication
|
|
81
|
+
*/
|
|
82
|
+
login(): Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* Handle OAuth callback after user authentication
|
|
85
|
+
* Call this on your redirect URI page
|
|
86
|
+
* @returns The authenticated user
|
|
87
|
+
*/
|
|
88
|
+
handleCallback(): Promise<FlowstaUser>;
|
|
89
|
+
/**
|
|
90
|
+
* Log out the current user
|
|
91
|
+
*/
|
|
92
|
+
logout(): void;
|
|
93
|
+
/**
|
|
94
|
+
* Check if user is authenticated
|
|
95
|
+
*/
|
|
96
|
+
isAuthenticated(): boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Get the current user
|
|
99
|
+
*/
|
|
100
|
+
getUser(): FlowstaUser | null;
|
|
101
|
+
/**
|
|
102
|
+
* Get the current access token
|
|
103
|
+
*/
|
|
104
|
+
getAccessToken(): string | null;
|
|
105
|
+
/**
|
|
106
|
+
* Get current auth state
|
|
107
|
+
*/
|
|
108
|
+
getState(): AuthState;
|
|
109
|
+
private restoreSession;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export { type AuthState, FlowstaAuth, type FlowstaAuthConfig, type FlowstaUser, FlowstaAuth as default };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flowsta Auth SDK 2.0
|
|
3
|
+
*
|
|
4
|
+
* OAuth-only authentication for partner sites.
|
|
5
|
+
* All authentication flows through login.flowsta.com
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - OAuth 2.0 Authorization Code Flow with PKCE
|
|
9
|
+
* - Zero-knowledge architecture
|
|
10
|
+
* - No client secrets needed (PKCE provides security)
|
|
11
|
+
* - Simple "Sign in with Flowsta" integration
|
|
12
|
+
*/
|
|
13
|
+
interface FlowstaAuthConfig {
|
|
14
|
+
/** Your Flowsta application client ID (from dev.flowsta.com) */
|
|
15
|
+
clientId: string;
|
|
16
|
+
/** The URI to redirect back to after authentication */
|
|
17
|
+
redirectUri: string;
|
|
18
|
+
/** OAuth scopes to request. Default: ['profile', 'email'] */
|
|
19
|
+
scopes?: string[];
|
|
20
|
+
/** The Flowsta login URL. Default: 'https://login.flowsta.com' */
|
|
21
|
+
loginUrl?: string;
|
|
22
|
+
/** The Flowsta API URL. Default: 'https://auth-api.flowsta.com' */
|
|
23
|
+
apiUrl?: string;
|
|
24
|
+
}
|
|
25
|
+
interface FlowstaUser {
|
|
26
|
+
/** User's unique ID */
|
|
27
|
+
id: string;
|
|
28
|
+
/** User's email address (if 'email' scope was granted) */
|
|
29
|
+
email?: string;
|
|
30
|
+
/** User's username (if set) */
|
|
31
|
+
username?: string;
|
|
32
|
+
/** User's display name */
|
|
33
|
+
displayName?: string;
|
|
34
|
+
/** User's profile picture URL */
|
|
35
|
+
profilePicture?: string;
|
|
36
|
+
/** User's Holochain agent public key */
|
|
37
|
+
agentPubKey?: string;
|
|
38
|
+
/** User's DID (Decentralized Identifier) */
|
|
39
|
+
did?: string;
|
|
40
|
+
}
|
|
41
|
+
interface AuthState {
|
|
42
|
+
/** Whether the user is authenticated */
|
|
43
|
+
isAuthenticated: boolean;
|
|
44
|
+
/** The current user (null if not authenticated) */
|
|
45
|
+
user: FlowstaUser | null;
|
|
46
|
+
/** The access token (null if not authenticated) */
|
|
47
|
+
accessToken: string | null;
|
|
48
|
+
/** Whether authentication is loading */
|
|
49
|
+
isLoading: boolean;
|
|
50
|
+
/** Any authentication error */
|
|
51
|
+
error: string | null;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* FlowstaAuth class - Main SDK entry point
|
|
55
|
+
*
|
|
56
|
+
* Usage:
|
|
57
|
+
* ```typescript
|
|
58
|
+
* const auth = new FlowstaAuth({
|
|
59
|
+
* clientId: 'your-client-id',
|
|
60
|
+
* redirectUri: 'https://yoursite.com/auth/callback'
|
|
61
|
+
* });
|
|
62
|
+
*
|
|
63
|
+
* // Redirect to login
|
|
64
|
+
* auth.login();
|
|
65
|
+
*
|
|
66
|
+
* // Handle callback (on your redirect URI page)
|
|
67
|
+
* await auth.handleCallback();
|
|
68
|
+
*
|
|
69
|
+
* // Get current user
|
|
70
|
+
* const user = auth.getUser();
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
declare class FlowstaAuth {
|
|
74
|
+
private config;
|
|
75
|
+
private accessToken;
|
|
76
|
+
private user;
|
|
77
|
+
constructor(config: FlowstaAuthConfig);
|
|
78
|
+
/**
|
|
79
|
+
* Redirect user to Flowsta login page
|
|
80
|
+
* User will be redirected back to redirectUri after authentication
|
|
81
|
+
*/
|
|
82
|
+
login(): Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* Handle OAuth callback after user authentication
|
|
85
|
+
* Call this on your redirect URI page
|
|
86
|
+
* @returns The authenticated user
|
|
87
|
+
*/
|
|
88
|
+
handleCallback(): Promise<FlowstaUser>;
|
|
89
|
+
/**
|
|
90
|
+
* Log out the current user
|
|
91
|
+
*/
|
|
92
|
+
logout(): void;
|
|
93
|
+
/**
|
|
94
|
+
* Check if user is authenticated
|
|
95
|
+
*/
|
|
96
|
+
isAuthenticated(): boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Get the current user
|
|
99
|
+
*/
|
|
100
|
+
getUser(): FlowstaUser | null;
|
|
101
|
+
/**
|
|
102
|
+
* Get the current access token
|
|
103
|
+
*/
|
|
104
|
+
getAccessToken(): string | null;
|
|
105
|
+
/**
|
|
106
|
+
* Get current auth state
|
|
107
|
+
*/
|
|
108
|
+
getState(): AuthState;
|
|
109
|
+
private restoreSession;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export { type AuthState, FlowstaAuth, type FlowstaAuthConfig, type FlowstaUser, FlowstaAuth as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
FlowstaAuth: () => FlowstaAuth,
|
|
24
|
+
default: () => index_default
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
async function generatePKCEPair() {
|
|
28
|
+
const verifier = generateRandomString(128);
|
|
29
|
+
const encoder = new TextEncoder();
|
|
30
|
+
const data = encoder.encode(verifier);
|
|
31
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
32
|
+
const challenge = base64UrlEncode(digest);
|
|
33
|
+
return { verifier, challenge };
|
|
34
|
+
}
|
|
35
|
+
function generateRandomString(length) {
|
|
36
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
|
|
37
|
+
const array = new Uint8Array(length);
|
|
38
|
+
crypto.getRandomValues(array);
|
|
39
|
+
return Array.from(array, (byte) => chars[byte % chars.length]).join("");
|
|
40
|
+
}
|
|
41
|
+
function base64UrlEncode(buffer) {
|
|
42
|
+
const bytes = new Uint8Array(buffer);
|
|
43
|
+
let binary = "";
|
|
44
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
45
|
+
binary += String.fromCharCode(bytes[i]);
|
|
46
|
+
}
|
|
47
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
48
|
+
}
|
|
49
|
+
var FlowstaAuth = class {
|
|
50
|
+
constructor(config) {
|
|
51
|
+
this.accessToken = null;
|
|
52
|
+
this.user = null;
|
|
53
|
+
this.config = {
|
|
54
|
+
clientId: config.clientId,
|
|
55
|
+
redirectUri: config.redirectUri,
|
|
56
|
+
scopes: config.scopes || ["profile", "email"],
|
|
57
|
+
loginUrl: config.loginUrl || "https://login.flowsta.com",
|
|
58
|
+
apiUrl: config.apiUrl || "https://auth-api.flowsta.com"
|
|
59
|
+
};
|
|
60
|
+
this.restoreSession();
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Redirect user to Flowsta login page
|
|
64
|
+
* User will be redirected back to redirectUri after authentication
|
|
65
|
+
*/
|
|
66
|
+
async login() {
|
|
67
|
+
const { verifier, challenge } = await generatePKCEPair();
|
|
68
|
+
const state = generateRandomString(32);
|
|
69
|
+
sessionStorage.setItem("flowsta_code_verifier", verifier);
|
|
70
|
+
sessionStorage.setItem("flowsta_state", state);
|
|
71
|
+
const params = new URLSearchParams({
|
|
72
|
+
client_id: this.config.clientId,
|
|
73
|
+
redirect_uri: this.config.redirectUri,
|
|
74
|
+
response_type: "code",
|
|
75
|
+
scope: this.config.scopes.join(" "),
|
|
76
|
+
state,
|
|
77
|
+
code_challenge: challenge,
|
|
78
|
+
code_challenge_method: "S256"
|
|
79
|
+
});
|
|
80
|
+
window.location.href = `${this.config.loginUrl}/login?${params.toString()}`;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Handle OAuth callback after user authentication
|
|
84
|
+
* Call this on your redirect URI page
|
|
85
|
+
* @returns The authenticated user
|
|
86
|
+
*/
|
|
87
|
+
async handleCallback() {
|
|
88
|
+
const params = new URLSearchParams(window.location.search);
|
|
89
|
+
const error = params.get("error");
|
|
90
|
+
if (error) {
|
|
91
|
+
const description = params.get("error_description") || error;
|
|
92
|
+
throw new Error(description);
|
|
93
|
+
}
|
|
94
|
+
const code = params.get("code");
|
|
95
|
+
if (!code) {
|
|
96
|
+
throw new Error("No authorization code received");
|
|
97
|
+
}
|
|
98
|
+
const state = params.get("state");
|
|
99
|
+
const storedState = sessionStorage.getItem("flowsta_state");
|
|
100
|
+
if (!state || state !== storedState) {
|
|
101
|
+
throw new Error("Invalid state parameter - possible CSRF attack");
|
|
102
|
+
}
|
|
103
|
+
const codeVerifier = sessionStorage.getItem("flowsta_code_verifier");
|
|
104
|
+
if (!codeVerifier) {
|
|
105
|
+
throw new Error("Missing PKCE code verifier");
|
|
106
|
+
}
|
|
107
|
+
const tokenResponse = await fetch(`${this.config.apiUrl}/oauth/token`, {
|
|
108
|
+
method: "POST",
|
|
109
|
+
headers: { "Content-Type": "application/json" },
|
|
110
|
+
body: JSON.stringify({
|
|
111
|
+
grant_type: "authorization_code",
|
|
112
|
+
code,
|
|
113
|
+
redirect_uri: this.config.redirectUri,
|
|
114
|
+
client_id: this.config.clientId,
|
|
115
|
+
code_verifier: codeVerifier
|
|
116
|
+
})
|
|
117
|
+
});
|
|
118
|
+
if (!tokenResponse.ok) {
|
|
119
|
+
const errorData = await tokenResponse.json();
|
|
120
|
+
throw new Error(errorData.error_description || "Token exchange failed");
|
|
121
|
+
}
|
|
122
|
+
const { access_token, refresh_token } = await tokenResponse.json();
|
|
123
|
+
sessionStorage.removeItem("flowsta_code_verifier");
|
|
124
|
+
sessionStorage.removeItem("flowsta_state");
|
|
125
|
+
const userResponse = await fetch(`${this.config.apiUrl}/oauth/userinfo`, {
|
|
126
|
+
headers: { "Authorization": `Bearer ${access_token}` }
|
|
127
|
+
});
|
|
128
|
+
if (!userResponse.ok) {
|
|
129
|
+
throw new Error("Failed to fetch user info");
|
|
130
|
+
}
|
|
131
|
+
const userData = await userResponse.json();
|
|
132
|
+
this.accessToken = access_token;
|
|
133
|
+
this.user = {
|
|
134
|
+
id: userData.sub || userData.id,
|
|
135
|
+
email: userData.email,
|
|
136
|
+
username: userData.preferred_username,
|
|
137
|
+
displayName: userData.display_name || userData.name,
|
|
138
|
+
profilePicture: userData.picture || userData.profile_picture,
|
|
139
|
+
agentPubKey: userData.agent_pub_key,
|
|
140
|
+
did: userData.did
|
|
141
|
+
};
|
|
142
|
+
localStorage.setItem("flowsta_access_token", access_token);
|
|
143
|
+
localStorage.setItem("flowsta_user", JSON.stringify(this.user));
|
|
144
|
+
if (refresh_token) {
|
|
145
|
+
localStorage.setItem("flowsta_refresh_token", refresh_token);
|
|
146
|
+
}
|
|
147
|
+
return this.user;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Log out the current user
|
|
151
|
+
*/
|
|
152
|
+
logout() {
|
|
153
|
+
this.accessToken = null;
|
|
154
|
+
this.user = null;
|
|
155
|
+
localStorage.removeItem("flowsta_access_token");
|
|
156
|
+
localStorage.removeItem("flowsta_user");
|
|
157
|
+
localStorage.removeItem("flowsta_refresh_token");
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Check if user is authenticated
|
|
161
|
+
*/
|
|
162
|
+
isAuthenticated() {
|
|
163
|
+
return !!this.accessToken && !!this.user;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Get the current user
|
|
167
|
+
*/
|
|
168
|
+
getUser() {
|
|
169
|
+
return this.user;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Get the current access token
|
|
173
|
+
*/
|
|
174
|
+
getAccessToken() {
|
|
175
|
+
return this.accessToken;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Get current auth state
|
|
179
|
+
*/
|
|
180
|
+
getState() {
|
|
181
|
+
return {
|
|
182
|
+
isAuthenticated: this.isAuthenticated(),
|
|
183
|
+
user: this.user,
|
|
184
|
+
accessToken: this.accessToken,
|
|
185
|
+
isLoading: false,
|
|
186
|
+
error: null
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
restoreSession() {
|
|
190
|
+
if (typeof localStorage === "undefined") return;
|
|
191
|
+
const token = localStorage.getItem("flowsta_access_token");
|
|
192
|
+
const userJson = localStorage.getItem("flowsta_user");
|
|
193
|
+
if (token && userJson) {
|
|
194
|
+
try {
|
|
195
|
+
this.accessToken = token;
|
|
196
|
+
this.user = JSON.parse(userJson);
|
|
197
|
+
} catch {
|
|
198
|
+
this.logout();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
var index_default = FlowstaAuth;
|
|
204
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
205
|
+
0 && (module.exports = {
|
|
206
|
+
FlowstaAuth
|
|
207
|
+
});
|
package/dist/index.mjs
ADDED
package/dist/react.d.mts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { FlowstaAuthConfig, AuthState, FlowstaUser } from './index.mjs';
|
|
4
|
+
export { default as FlowstaAuth } from './index.mjs';
|
|
5
|
+
|
|
6
|
+
interface FlowstaAuthContextValue extends AuthState {
|
|
7
|
+
/** Redirect to Flowsta login */
|
|
8
|
+
login: () => Promise<void>;
|
|
9
|
+
/** Log out the current user */
|
|
10
|
+
logout: () => void;
|
|
11
|
+
/** Handle OAuth callback (call on redirect URI page) */
|
|
12
|
+
handleCallback: () => Promise<FlowstaUser>;
|
|
13
|
+
}
|
|
14
|
+
interface FlowstaAuthProviderProps extends FlowstaAuthConfig {
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Flowsta Auth Provider component
|
|
19
|
+
* Wrap your app with this to enable authentication
|
|
20
|
+
*/
|
|
21
|
+
declare function FlowstaAuthProvider({ children, clientId, redirectUri, scopes, loginUrl, apiUrl, }: FlowstaAuthProviderProps): react_jsx_runtime.JSX.Element;
|
|
22
|
+
/**
|
|
23
|
+
* Hook to access Flowsta Auth
|
|
24
|
+
* Must be used within a FlowstaAuthProvider
|
|
25
|
+
*/
|
|
26
|
+
declare function useFlowstaAuth(): FlowstaAuthContextValue;
|
|
27
|
+
/**
|
|
28
|
+
* Hook to protect routes/components
|
|
29
|
+
* Redirects to login if not authenticated
|
|
30
|
+
*/
|
|
31
|
+
declare function useRequireAuth(options?: {
|
|
32
|
+
redirectTo?: string;
|
|
33
|
+
}): AuthState & {
|
|
34
|
+
isReady: boolean;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export { AuthState, FlowstaAuthConfig, FlowstaAuthProvider, FlowstaUser, FlowstaAuthProvider as default, useFlowstaAuth, useRequireAuth };
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { FlowstaAuthConfig, AuthState, FlowstaUser } from './index.js';
|
|
4
|
+
export { default as FlowstaAuth } from './index.js';
|
|
5
|
+
|
|
6
|
+
interface FlowstaAuthContextValue extends AuthState {
|
|
7
|
+
/** Redirect to Flowsta login */
|
|
8
|
+
login: () => Promise<void>;
|
|
9
|
+
/** Log out the current user */
|
|
10
|
+
logout: () => void;
|
|
11
|
+
/** Handle OAuth callback (call on redirect URI page) */
|
|
12
|
+
handleCallback: () => Promise<FlowstaUser>;
|
|
13
|
+
}
|
|
14
|
+
interface FlowstaAuthProviderProps extends FlowstaAuthConfig {
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Flowsta Auth Provider component
|
|
19
|
+
* Wrap your app with this to enable authentication
|
|
20
|
+
*/
|
|
21
|
+
declare function FlowstaAuthProvider({ children, clientId, redirectUri, scopes, loginUrl, apiUrl, }: FlowstaAuthProviderProps): react_jsx_runtime.JSX.Element;
|
|
22
|
+
/**
|
|
23
|
+
* Hook to access Flowsta Auth
|
|
24
|
+
* Must be used within a FlowstaAuthProvider
|
|
25
|
+
*/
|
|
26
|
+
declare function useFlowstaAuth(): FlowstaAuthContextValue;
|
|
27
|
+
/**
|
|
28
|
+
* Hook to protect routes/components
|
|
29
|
+
* Redirects to login if not authenticated
|
|
30
|
+
*/
|
|
31
|
+
declare function useRequireAuth(options?: {
|
|
32
|
+
redirectTo?: string;
|
|
33
|
+
}): AuthState & {
|
|
34
|
+
isReady: boolean;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export { AuthState, FlowstaAuthConfig, FlowstaAuthProvider, FlowstaUser, FlowstaAuthProvider as default, useFlowstaAuth, useRequireAuth };
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/react.tsx
|
|
21
|
+
var react_exports = {};
|
|
22
|
+
__export(react_exports, {
|
|
23
|
+
FlowstaAuth: () => FlowstaAuth,
|
|
24
|
+
FlowstaAuthProvider: () => FlowstaAuthProvider,
|
|
25
|
+
default: () => react_default,
|
|
26
|
+
useFlowstaAuth: () => useFlowstaAuth,
|
|
27
|
+
useRequireAuth: () => useRequireAuth
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(react_exports);
|
|
30
|
+
var import_react = require("react");
|
|
31
|
+
|
|
32
|
+
// src/index.ts
|
|
33
|
+
async function generatePKCEPair() {
|
|
34
|
+
const verifier = generateRandomString(128);
|
|
35
|
+
const encoder = new TextEncoder();
|
|
36
|
+
const data = encoder.encode(verifier);
|
|
37
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
38
|
+
const challenge = base64UrlEncode(digest);
|
|
39
|
+
return { verifier, challenge };
|
|
40
|
+
}
|
|
41
|
+
function generateRandomString(length) {
|
|
42
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
|
|
43
|
+
const array = new Uint8Array(length);
|
|
44
|
+
crypto.getRandomValues(array);
|
|
45
|
+
return Array.from(array, (byte) => chars[byte % chars.length]).join("");
|
|
46
|
+
}
|
|
47
|
+
function base64UrlEncode(buffer) {
|
|
48
|
+
const bytes = new Uint8Array(buffer);
|
|
49
|
+
let binary = "";
|
|
50
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
51
|
+
binary += String.fromCharCode(bytes[i]);
|
|
52
|
+
}
|
|
53
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
54
|
+
}
|
|
55
|
+
var FlowstaAuth = class {
|
|
56
|
+
constructor(config) {
|
|
57
|
+
this.accessToken = null;
|
|
58
|
+
this.user = null;
|
|
59
|
+
this.config = {
|
|
60
|
+
clientId: config.clientId,
|
|
61
|
+
redirectUri: config.redirectUri,
|
|
62
|
+
scopes: config.scopes || ["profile", "email"],
|
|
63
|
+
loginUrl: config.loginUrl || "https://login.flowsta.com",
|
|
64
|
+
apiUrl: config.apiUrl || "https://auth-api.flowsta.com"
|
|
65
|
+
};
|
|
66
|
+
this.restoreSession();
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Redirect user to Flowsta login page
|
|
70
|
+
* User will be redirected back to redirectUri after authentication
|
|
71
|
+
*/
|
|
72
|
+
async login() {
|
|
73
|
+
const { verifier, challenge } = await generatePKCEPair();
|
|
74
|
+
const state = generateRandomString(32);
|
|
75
|
+
sessionStorage.setItem("flowsta_code_verifier", verifier);
|
|
76
|
+
sessionStorage.setItem("flowsta_state", state);
|
|
77
|
+
const params = new URLSearchParams({
|
|
78
|
+
client_id: this.config.clientId,
|
|
79
|
+
redirect_uri: this.config.redirectUri,
|
|
80
|
+
response_type: "code",
|
|
81
|
+
scope: this.config.scopes.join(" "),
|
|
82
|
+
state,
|
|
83
|
+
code_challenge: challenge,
|
|
84
|
+
code_challenge_method: "S256"
|
|
85
|
+
});
|
|
86
|
+
window.location.href = `${this.config.loginUrl}/login?${params.toString()}`;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Handle OAuth callback after user authentication
|
|
90
|
+
* Call this on your redirect URI page
|
|
91
|
+
* @returns The authenticated user
|
|
92
|
+
*/
|
|
93
|
+
async handleCallback() {
|
|
94
|
+
const params = new URLSearchParams(window.location.search);
|
|
95
|
+
const error = params.get("error");
|
|
96
|
+
if (error) {
|
|
97
|
+
const description = params.get("error_description") || error;
|
|
98
|
+
throw new Error(description);
|
|
99
|
+
}
|
|
100
|
+
const code = params.get("code");
|
|
101
|
+
if (!code) {
|
|
102
|
+
throw new Error("No authorization code received");
|
|
103
|
+
}
|
|
104
|
+
const state = params.get("state");
|
|
105
|
+
const storedState = sessionStorage.getItem("flowsta_state");
|
|
106
|
+
if (!state || state !== storedState) {
|
|
107
|
+
throw new Error("Invalid state parameter - possible CSRF attack");
|
|
108
|
+
}
|
|
109
|
+
const codeVerifier = sessionStorage.getItem("flowsta_code_verifier");
|
|
110
|
+
if (!codeVerifier) {
|
|
111
|
+
throw new Error("Missing PKCE code verifier");
|
|
112
|
+
}
|
|
113
|
+
const tokenResponse = await fetch(`${this.config.apiUrl}/oauth/token`, {
|
|
114
|
+
method: "POST",
|
|
115
|
+
headers: { "Content-Type": "application/json" },
|
|
116
|
+
body: JSON.stringify({
|
|
117
|
+
grant_type: "authorization_code",
|
|
118
|
+
code,
|
|
119
|
+
redirect_uri: this.config.redirectUri,
|
|
120
|
+
client_id: this.config.clientId,
|
|
121
|
+
code_verifier: codeVerifier
|
|
122
|
+
})
|
|
123
|
+
});
|
|
124
|
+
if (!tokenResponse.ok) {
|
|
125
|
+
const errorData = await tokenResponse.json();
|
|
126
|
+
throw new Error(errorData.error_description || "Token exchange failed");
|
|
127
|
+
}
|
|
128
|
+
const { access_token, refresh_token } = await tokenResponse.json();
|
|
129
|
+
sessionStorage.removeItem("flowsta_code_verifier");
|
|
130
|
+
sessionStorage.removeItem("flowsta_state");
|
|
131
|
+
const userResponse = await fetch(`${this.config.apiUrl}/oauth/userinfo`, {
|
|
132
|
+
headers: { "Authorization": `Bearer ${access_token}` }
|
|
133
|
+
});
|
|
134
|
+
if (!userResponse.ok) {
|
|
135
|
+
throw new Error("Failed to fetch user info");
|
|
136
|
+
}
|
|
137
|
+
const userData = await userResponse.json();
|
|
138
|
+
this.accessToken = access_token;
|
|
139
|
+
this.user = {
|
|
140
|
+
id: userData.sub || userData.id,
|
|
141
|
+
email: userData.email,
|
|
142
|
+
username: userData.preferred_username,
|
|
143
|
+
displayName: userData.display_name || userData.name,
|
|
144
|
+
profilePicture: userData.picture || userData.profile_picture,
|
|
145
|
+
agentPubKey: userData.agent_pub_key,
|
|
146
|
+
did: userData.did
|
|
147
|
+
};
|
|
148
|
+
localStorage.setItem("flowsta_access_token", access_token);
|
|
149
|
+
localStorage.setItem("flowsta_user", JSON.stringify(this.user));
|
|
150
|
+
if (refresh_token) {
|
|
151
|
+
localStorage.setItem("flowsta_refresh_token", refresh_token);
|
|
152
|
+
}
|
|
153
|
+
return this.user;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Log out the current user
|
|
157
|
+
*/
|
|
158
|
+
logout() {
|
|
159
|
+
this.accessToken = null;
|
|
160
|
+
this.user = null;
|
|
161
|
+
localStorage.removeItem("flowsta_access_token");
|
|
162
|
+
localStorage.removeItem("flowsta_user");
|
|
163
|
+
localStorage.removeItem("flowsta_refresh_token");
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Check if user is authenticated
|
|
167
|
+
*/
|
|
168
|
+
isAuthenticated() {
|
|
169
|
+
return !!this.accessToken && !!this.user;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Get the current user
|
|
173
|
+
*/
|
|
174
|
+
getUser() {
|
|
175
|
+
return this.user;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Get the current access token
|
|
179
|
+
*/
|
|
180
|
+
getAccessToken() {
|
|
181
|
+
return this.accessToken;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Get current auth state
|
|
185
|
+
*/
|
|
186
|
+
getState() {
|
|
187
|
+
return {
|
|
188
|
+
isAuthenticated: this.isAuthenticated(),
|
|
189
|
+
user: this.user,
|
|
190
|
+
accessToken: this.accessToken,
|
|
191
|
+
isLoading: false,
|
|
192
|
+
error: null
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
restoreSession() {
|
|
196
|
+
if (typeof localStorage === "undefined") return;
|
|
197
|
+
const token = localStorage.getItem("flowsta_access_token");
|
|
198
|
+
const userJson = localStorage.getItem("flowsta_user");
|
|
199
|
+
if (token && userJson) {
|
|
200
|
+
try {
|
|
201
|
+
this.accessToken = token;
|
|
202
|
+
this.user = JSON.parse(userJson);
|
|
203
|
+
} catch {
|
|
204
|
+
this.logout();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// src/react.tsx
|
|
211
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
212
|
+
var FlowstaAuthContext = (0, import_react.createContext)(null);
|
|
213
|
+
function FlowstaAuthProvider({
|
|
214
|
+
children,
|
|
215
|
+
clientId,
|
|
216
|
+
redirectUri,
|
|
217
|
+
scopes,
|
|
218
|
+
loginUrl,
|
|
219
|
+
apiUrl
|
|
220
|
+
}) {
|
|
221
|
+
const [auth] = (0, import_react.useState)(() => new FlowstaAuth({
|
|
222
|
+
clientId,
|
|
223
|
+
redirectUri,
|
|
224
|
+
scopes,
|
|
225
|
+
loginUrl,
|
|
226
|
+
apiUrl
|
|
227
|
+
}));
|
|
228
|
+
const [state, setState] = (0, import_react.useState)(() => auth.getState());
|
|
229
|
+
(0, import_react.useEffect)(() => {
|
|
230
|
+
setState(auth.getState());
|
|
231
|
+
}, [auth]);
|
|
232
|
+
const login = (0, import_react.useCallback)(async () => {
|
|
233
|
+
setState((prev) => ({ ...prev, isLoading: true, error: null }));
|
|
234
|
+
try {
|
|
235
|
+
await auth.login();
|
|
236
|
+
} catch (error) {
|
|
237
|
+
setState((prev) => ({
|
|
238
|
+
...prev,
|
|
239
|
+
isLoading: false,
|
|
240
|
+
error: error instanceof Error ? error.message : "Login failed"
|
|
241
|
+
}));
|
|
242
|
+
}
|
|
243
|
+
}, [auth]);
|
|
244
|
+
const logout = (0, import_react.useCallback)(() => {
|
|
245
|
+
auth.logout();
|
|
246
|
+
setState({
|
|
247
|
+
isAuthenticated: false,
|
|
248
|
+
user: null,
|
|
249
|
+
accessToken: null,
|
|
250
|
+
isLoading: false,
|
|
251
|
+
error: null
|
|
252
|
+
});
|
|
253
|
+
}, [auth]);
|
|
254
|
+
const handleCallback = (0, import_react.useCallback)(async () => {
|
|
255
|
+
setState((prev) => ({ ...prev, isLoading: true, error: null }));
|
|
256
|
+
try {
|
|
257
|
+
const user = await auth.handleCallback();
|
|
258
|
+
setState({
|
|
259
|
+
isAuthenticated: true,
|
|
260
|
+
user,
|
|
261
|
+
accessToken: auth.getAccessToken(),
|
|
262
|
+
isLoading: false,
|
|
263
|
+
error: null
|
|
264
|
+
});
|
|
265
|
+
return user;
|
|
266
|
+
} catch (error) {
|
|
267
|
+
const errorMessage = error instanceof Error ? error.message : "Authentication failed";
|
|
268
|
+
setState((prev) => ({
|
|
269
|
+
...prev,
|
|
270
|
+
isLoading: false,
|
|
271
|
+
error: errorMessage
|
|
272
|
+
}));
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
}, [auth]);
|
|
276
|
+
const value = {
|
|
277
|
+
...state,
|
|
278
|
+
login,
|
|
279
|
+
logout,
|
|
280
|
+
handleCallback
|
|
281
|
+
};
|
|
282
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FlowstaAuthContext.Provider, { value, children });
|
|
283
|
+
}
|
|
284
|
+
function useFlowstaAuth() {
|
|
285
|
+
const context = (0, import_react.useContext)(FlowstaAuthContext);
|
|
286
|
+
if (!context) {
|
|
287
|
+
throw new Error("useFlowstaAuth must be used within a FlowstaAuthProvider");
|
|
288
|
+
}
|
|
289
|
+
return context;
|
|
290
|
+
}
|
|
291
|
+
function useRequireAuth(options) {
|
|
292
|
+
const { isAuthenticated, isLoading, login, ...rest } = useFlowstaAuth();
|
|
293
|
+
const [isReady, setIsReady] = (0, import_react.useState)(false);
|
|
294
|
+
(0, import_react.useEffect)(() => {
|
|
295
|
+
if (!isLoading) {
|
|
296
|
+
if (!isAuthenticated) {
|
|
297
|
+
if (options?.redirectTo) {
|
|
298
|
+
window.location.href = options.redirectTo;
|
|
299
|
+
} else {
|
|
300
|
+
login();
|
|
301
|
+
}
|
|
302
|
+
} else {
|
|
303
|
+
setIsReady(true);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}, [isAuthenticated, isLoading, login, options?.redirectTo]);
|
|
307
|
+
return { isAuthenticated, isLoading, isReady, ...rest };
|
|
308
|
+
}
|
|
309
|
+
var react_default = FlowstaAuthProvider;
|
|
310
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
311
|
+
0 && (module.exports = {
|
|
312
|
+
FlowstaAuth,
|
|
313
|
+
FlowstaAuthProvider,
|
|
314
|
+
useFlowstaAuth,
|
|
315
|
+
useRequireAuth
|
|
316
|
+
});
|
package/dist/react.mjs
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FlowstaAuth
|
|
3
|
+
} from "./chunk-NBWAVXMK.mjs";
|
|
4
|
+
|
|
5
|
+
// src/react.tsx
|
|
6
|
+
import {
|
|
7
|
+
createContext,
|
|
8
|
+
useContext,
|
|
9
|
+
useState,
|
|
10
|
+
useEffect,
|
|
11
|
+
useCallback
|
|
12
|
+
} from "react";
|
|
13
|
+
import { jsx } from "react/jsx-runtime";
|
|
14
|
+
var FlowstaAuthContext = createContext(null);
|
|
15
|
+
function FlowstaAuthProvider({
|
|
16
|
+
children,
|
|
17
|
+
clientId,
|
|
18
|
+
redirectUri,
|
|
19
|
+
scopes,
|
|
20
|
+
loginUrl,
|
|
21
|
+
apiUrl
|
|
22
|
+
}) {
|
|
23
|
+
const [auth] = useState(() => new FlowstaAuth({
|
|
24
|
+
clientId,
|
|
25
|
+
redirectUri,
|
|
26
|
+
scopes,
|
|
27
|
+
loginUrl,
|
|
28
|
+
apiUrl
|
|
29
|
+
}));
|
|
30
|
+
const [state, setState] = useState(() => auth.getState());
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
setState(auth.getState());
|
|
33
|
+
}, [auth]);
|
|
34
|
+
const login = useCallback(async () => {
|
|
35
|
+
setState((prev) => ({ ...prev, isLoading: true, error: null }));
|
|
36
|
+
try {
|
|
37
|
+
await auth.login();
|
|
38
|
+
} catch (error) {
|
|
39
|
+
setState((prev) => ({
|
|
40
|
+
...prev,
|
|
41
|
+
isLoading: false,
|
|
42
|
+
error: error instanceof Error ? error.message : "Login failed"
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
}, [auth]);
|
|
46
|
+
const logout = useCallback(() => {
|
|
47
|
+
auth.logout();
|
|
48
|
+
setState({
|
|
49
|
+
isAuthenticated: false,
|
|
50
|
+
user: null,
|
|
51
|
+
accessToken: null,
|
|
52
|
+
isLoading: false,
|
|
53
|
+
error: null
|
|
54
|
+
});
|
|
55
|
+
}, [auth]);
|
|
56
|
+
const handleCallback = useCallback(async () => {
|
|
57
|
+
setState((prev) => ({ ...prev, isLoading: true, error: null }));
|
|
58
|
+
try {
|
|
59
|
+
const user = await auth.handleCallback();
|
|
60
|
+
setState({
|
|
61
|
+
isAuthenticated: true,
|
|
62
|
+
user,
|
|
63
|
+
accessToken: auth.getAccessToken(),
|
|
64
|
+
isLoading: false,
|
|
65
|
+
error: null
|
|
66
|
+
});
|
|
67
|
+
return user;
|
|
68
|
+
} catch (error) {
|
|
69
|
+
const errorMessage = error instanceof Error ? error.message : "Authentication failed";
|
|
70
|
+
setState((prev) => ({
|
|
71
|
+
...prev,
|
|
72
|
+
isLoading: false,
|
|
73
|
+
error: errorMessage
|
|
74
|
+
}));
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}, [auth]);
|
|
78
|
+
const value = {
|
|
79
|
+
...state,
|
|
80
|
+
login,
|
|
81
|
+
logout,
|
|
82
|
+
handleCallback
|
|
83
|
+
};
|
|
84
|
+
return /* @__PURE__ */ jsx(FlowstaAuthContext.Provider, { value, children });
|
|
85
|
+
}
|
|
86
|
+
function useFlowstaAuth() {
|
|
87
|
+
const context = useContext(FlowstaAuthContext);
|
|
88
|
+
if (!context) {
|
|
89
|
+
throw new Error("useFlowstaAuth must be used within a FlowstaAuthProvider");
|
|
90
|
+
}
|
|
91
|
+
return context;
|
|
92
|
+
}
|
|
93
|
+
function useRequireAuth(options) {
|
|
94
|
+
const { isAuthenticated, isLoading, login, ...rest } = useFlowstaAuth();
|
|
95
|
+
const [isReady, setIsReady] = useState(false);
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (!isLoading) {
|
|
98
|
+
if (!isAuthenticated) {
|
|
99
|
+
if (options?.redirectTo) {
|
|
100
|
+
window.location.href = options.redirectTo;
|
|
101
|
+
} else {
|
|
102
|
+
login();
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
setIsReady(true);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}, [isAuthenticated, isLoading, login, options?.redirectTo]);
|
|
109
|
+
return { isAuthenticated, isLoading, isReady, ...rest };
|
|
110
|
+
}
|
|
111
|
+
var react_default = FlowstaAuthProvider;
|
|
112
|
+
export {
|
|
113
|
+
FlowstaAuth,
|
|
114
|
+
FlowstaAuthProvider,
|
|
115
|
+
react_default as default,
|
|
116
|
+
useFlowstaAuth,
|
|
117
|
+
useRequireAuth
|
|
118
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flowsta/auth",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Flowsta Auth SDK 2.0 - OAuth-only authentication for web applications",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./react": {
|
|
15
|
+
"types": "./dist/react.d.ts",
|
|
16
|
+
"import": "./dist/react.mjs",
|
|
17
|
+
"require": "./dist/react.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsup src/index.ts src/react.tsx --format cjs,esm --dts",
|
|
25
|
+
"dev": "tsup src/index.ts src/react.tsx --format cjs,esm --dts --watch",
|
|
26
|
+
"test": "vitest",
|
|
27
|
+
"lint": "eslint src"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"flowsta",
|
|
31
|
+
"auth",
|
|
32
|
+
"authentication",
|
|
33
|
+
"oauth",
|
|
34
|
+
"sso",
|
|
35
|
+
"pkce",
|
|
36
|
+
"zero-knowledge"
|
|
37
|
+
],
|
|
38
|
+
"author": "Flowsta",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "https://github.com/WeAreFlowsta/flowsta-sdk.git",
|
|
43
|
+
"directory": "packages/auth"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"react": ">=17.0.0"
|
|
47
|
+
},
|
|
48
|
+
"peerDependenciesMeta": {
|
|
49
|
+
"react": {
|
|
50
|
+
"optional": true
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"tsup": "^8.0.0",
|
|
55
|
+
"typescript": "^5.3.0",
|
|
56
|
+
"vitest": "^1.0.0",
|
|
57
|
+
"@types/react": "^18.2.0"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|