@flowsta/auth 2.1.0 → 2.1.1
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/chunk-FOKPJDDJ.mjs +267 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/react.js +1 -1
- package/dist/react.mjs +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,267 @@
|
|
|
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 || ["openid", "email", "display_name"],
|
|
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
|
+
const vault = await this.detectVault();
|
|
108
|
+
this.accessToken = access_token;
|
|
109
|
+
this.user = {
|
|
110
|
+
id: userData.sub || userData.id,
|
|
111
|
+
email: userData.email,
|
|
112
|
+
username: userData.preferred_username,
|
|
113
|
+
displayName: userData.display_name || userData.name,
|
|
114
|
+
profilePicture: userData.picture || userData.profile_picture,
|
|
115
|
+
agentPubKey: userData.agent_pub_key,
|
|
116
|
+
did: userData.did,
|
|
117
|
+
signingMode: vault.running ? "ipc" : "remote"
|
|
118
|
+
};
|
|
119
|
+
localStorage.setItem("flowsta_access_token", access_token);
|
|
120
|
+
localStorage.setItem("flowsta_user", JSON.stringify(this.user));
|
|
121
|
+
if (refresh_token) {
|
|
122
|
+
localStorage.setItem("flowsta_refresh_token", refresh_token);
|
|
123
|
+
}
|
|
124
|
+
return this.user;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Log out the current user
|
|
128
|
+
*/
|
|
129
|
+
logout() {
|
|
130
|
+
this.accessToken = null;
|
|
131
|
+
this.user = null;
|
|
132
|
+
localStorage.removeItem("flowsta_access_token");
|
|
133
|
+
localStorage.removeItem("flowsta_user");
|
|
134
|
+
localStorage.removeItem("flowsta_refresh_token");
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Check if user is authenticated
|
|
138
|
+
*/
|
|
139
|
+
isAuthenticated() {
|
|
140
|
+
return !!this.accessToken && !!this.user;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Get the current user
|
|
144
|
+
*/
|
|
145
|
+
getUser() {
|
|
146
|
+
return this.user;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Get the current access token
|
|
150
|
+
*/
|
|
151
|
+
getAccessToken() {
|
|
152
|
+
return this.accessToken;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Get current auth state
|
|
156
|
+
*/
|
|
157
|
+
getState() {
|
|
158
|
+
return {
|
|
159
|
+
isAuthenticated: this.isAuthenticated(),
|
|
160
|
+
user: this.user,
|
|
161
|
+
accessToken: this.accessToken,
|
|
162
|
+
isLoading: false,
|
|
163
|
+
error: null
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
// ── Vault Detection ──────────────────────────────────────────────
|
|
167
|
+
/**
|
|
168
|
+
* Detect whether Flowsta Vault (desktop app) is running.
|
|
169
|
+
*
|
|
170
|
+
* Probes the IPC server at localhost:27777. If running and unlocked,
|
|
171
|
+
* signing can be done locally instead of via the API.
|
|
172
|
+
*
|
|
173
|
+
* @returns Detection result with running status and agent info
|
|
174
|
+
*/
|
|
175
|
+
async detectVault() {
|
|
176
|
+
try {
|
|
177
|
+
const controller = new AbortController();
|
|
178
|
+
const timeout = setTimeout(() => controller.abort(), 2e3);
|
|
179
|
+
const response = await fetch("http://127.0.0.1:27777/status", {
|
|
180
|
+
signal: controller.signal
|
|
181
|
+
});
|
|
182
|
+
clearTimeout(timeout);
|
|
183
|
+
if (!response.ok) {
|
|
184
|
+
return { running: false };
|
|
185
|
+
}
|
|
186
|
+
const data = await response.json();
|
|
187
|
+
return {
|
|
188
|
+
running: true,
|
|
189
|
+
agentPubKey: data.agent_pub_key || data.agentPubKey,
|
|
190
|
+
did: data.did
|
|
191
|
+
};
|
|
192
|
+
} catch {
|
|
193
|
+
return { running: false };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// ── Agent Linking ────────────────────────────────────────────────
|
|
197
|
+
/**
|
|
198
|
+
* Get agents linked to a specific agent (or the current user's agent).
|
|
199
|
+
*
|
|
200
|
+
* Queries the API which reads from the DHT (IsSamePersonEntry).
|
|
201
|
+
*
|
|
202
|
+
* @param agentPubKey Optional specific agent to query. Defaults to current user's agent.
|
|
203
|
+
* @returns List of linked agent public keys
|
|
204
|
+
*/
|
|
205
|
+
async getLinkedAgents(agentPubKey) {
|
|
206
|
+
const token = this.accessToken;
|
|
207
|
+
if (!token) {
|
|
208
|
+
throw new Error("Not authenticated");
|
|
209
|
+
}
|
|
210
|
+
const url = new URL(`${this.config.apiUrl}/auth/linked-agents`);
|
|
211
|
+
if (agentPubKey) {
|
|
212
|
+
url.searchParams.set("agent_pub_key", agentPubKey);
|
|
213
|
+
}
|
|
214
|
+
const response = await fetch(url.toString(), {
|
|
215
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
216
|
+
});
|
|
217
|
+
if (!response.ok) {
|
|
218
|
+
const data2 = await response.json().catch(() => ({}));
|
|
219
|
+
throw new Error(data2.error || "Failed to get linked agents");
|
|
220
|
+
}
|
|
221
|
+
const data = await response.json();
|
|
222
|
+
return data.linked_agents || [];
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Check if two agents are linked (verified on the DHT).
|
|
226
|
+
*
|
|
227
|
+
* @param agentA First agent's public key
|
|
228
|
+
* @param agentB Second agent's public key
|
|
229
|
+
* @returns true if the agents are linked via an IsSamePersonEntry
|
|
230
|
+
*/
|
|
231
|
+
async areAgentsLinked(agentA, agentB) {
|
|
232
|
+
const token = this.accessToken;
|
|
233
|
+
if (!token) {
|
|
234
|
+
throw new Error("Not authenticated");
|
|
235
|
+
}
|
|
236
|
+
const url = new URL(`${this.config.apiUrl}/auth/are-agents-linked`);
|
|
237
|
+
url.searchParams.set("agent_a", agentA);
|
|
238
|
+
url.searchParams.set("agent_b", agentB);
|
|
239
|
+
const response = await fetch(url.toString(), {
|
|
240
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
241
|
+
});
|
|
242
|
+
if (!response.ok) {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
const data = await response.json();
|
|
246
|
+
return data.linked === true;
|
|
247
|
+
}
|
|
248
|
+
restoreSession() {
|
|
249
|
+
if (typeof localStorage === "undefined") return;
|
|
250
|
+
const token = localStorage.getItem("flowsta_access_token");
|
|
251
|
+
const userJson = localStorage.getItem("flowsta_user");
|
|
252
|
+
if (token && userJson) {
|
|
253
|
+
try {
|
|
254
|
+
this.accessToken = token;
|
|
255
|
+
this.user = JSON.parse(userJson);
|
|
256
|
+
} catch {
|
|
257
|
+
this.logout();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
var index_default = FlowstaAuth;
|
|
263
|
+
|
|
264
|
+
export {
|
|
265
|
+
FlowstaAuth,
|
|
266
|
+
index_default
|
|
267
|
+
};
|
package/dist/index.d.mts
CHANGED
|
@@ -17,7 +17,7 @@ interface FlowstaAuthConfig {
|
|
|
17
17
|
clientId: string;
|
|
18
18
|
/** The URI to redirect back to after authentication */
|
|
19
19
|
redirectUri: string;
|
|
20
|
-
/** OAuth scopes to request. Default: ['
|
|
20
|
+
/** OAuth scopes to request. Default: ['openid', 'email', 'display_name'] */
|
|
21
21
|
scopes?: string[];
|
|
22
22
|
/** The Flowsta login URL. Default: 'https://login.flowsta.com' */
|
|
23
23
|
loginUrl?: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ interface FlowstaAuthConfig {
|
|
|
17
17
|
clientId: string;
|
|
18
18
|
/** The URI to redirect back to after authentication */
|
|
19
19
|
redirectUri: string;
|
|
20
|
-
/** OAuth scopes to request. Default: ['
|
|
20
|
+
/** OAuth scopes to request. Default: ['openid', 'email', 'display_name'] */
|
|
21
21
|
scopes?: string[];
|
|
22
22
|
/** The Flowsta login URL. Default: 'https://login.flowsta.com' */
|
|
23
23
|
loginUrl?: string;
|
package/dist/index.js
CHANGED
|
@@ -53,7 +53,7 @@ var FlowstaAuth = class {
|
|
|
53
53
|
this.config = {
|
|
54
54
|
clientId: config.clientId,
|
|
55
55
|
redirectUri: config.redirectUri,
|
|
56
|
-
scopes: config.scopes || ["
|
|
56
|
+
scopes: config.scopes || ["openid", "email", "display_name"],
|
|
57
57
|
loginUrl: config.loginUrl || "https://login.flowsta.com",
|
|
58
58
|
apiUrl: config.apiUrl || "https://auth-api.flowsta.com"
|
|
59
59
|
};
|
package/dist/index.mjs
CHANGED
package/dist/react.js
CHANGED
|
@@ -59,7 +59,7 @@ var FlowstaAuth = class {
|
|
|
59
59
|
this.config = {
|
|
60
60
|
clientId: config.clientId,
|
|
61
61
|
redirectUri: config.redirectUri,
|
|
62
|
-
scopes: config.scopes || ["
|
|
62
|
+
scopes: config.scopes || ["openid", "email", "display_name"],
|
|
63
63
|
loginUrl: config.loginUrl || "https://login.flowsta.com",
|
|
64
64
|
apiUrl: config.apiUrl || "https://auth-api.flowsta.com"
|
|
65
65
|
};
|
package/dist/react.mjs
CHANGED