@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.
@@ -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: ['profile', 'email'] */
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: ['profile', 'email'] */
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 || ["profile", "email"],
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
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  FlowstaAuth,
3
3
  index_default
4
- } from "./chunk-HXNYC5IQ.mjs";
4
+ } from "./chunk-FOKPJDDJ.mjs";
5
5
  export {
6
6
  FlowstaAuth,
7
7
  index_default as default
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 || ["profile", "email"],
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
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  FlowstaAuth
3
- } from "./chunk-HXNYC5IQ.mjs";
3
+ } from "./chunk-FOKPJDDJ.mjs";
4
4
 
5
5
  // src/react.tsx
6
6
  import {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowsta/auth",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "description": "Flowsta Auth SDK - OAuth authentication with Vault detection and agent linking",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",