@flowsta/auth 2.0.0 → 2.1.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.
@@ -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 || ["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
+ 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
@@ -9,6 +9,8 @@
9
9
  * - Zero-knowledge architecture
10
10
  * - No client secrets needed (PKCE provides security)
11
11
  * - Simple "Sign in with Flowsta" integration
12
+ * - Flowsta Vault detection (desktop app IPC)
13
+ * - Agent linking queries (DHT-verified identity proofs)
12
14
  */
13
15
  interface FlowstaAuthConfig {
14
16
  /** Your Flowsta application client ID (from dev.flowsta.com) */
@@ -37,6 +39,28 @@ interface FlowstaUser {
37
39
  agentPubKey?: string;
38
40
  /** User's DID (Decentralized Identifier) */
39
41
  did?: string;
42
+ /** Agents linked to this user (from DHT IsSamePersonEntry) */
43
+ linkedAgents?: LinkedAgent[];
44
+ /** Current signing mode ('remote' = API, 'ipc' = Flowsta Vault) */
45
+ signingMode?: 'remote' | 'ipc';
46
+ }
47
+ /** A linked agent (verified on the DHT via IsSamePersonEntry) */
48
+ interface LinkedAgent {
49
+ /** The linked agent's public key */
50
+ agentPubKey: string;
51
+ /** When the link was created */
52
+ linkedAt?: string;
53
+ /** Whether the link has been revoked */
54
+ isRevoked: boolean;
55
+ }
56
+ /** Flowsta Vault desktop app status */
57
+ interface VaultDetectionResult {
58
+ /** Whether Flowsta Vault is running and reachable on localhost */
59
+ running: boolean;
60
+ /** The vault agent's public key (if unlocked) */
61
+ agentPubKey?: string;
62
+ /** The vault agent's DID (if unlocked) */
63
+ did?: string;
40
64
  }
41
65
  interface AuthState {
42
66
  /** Whether the user is authenticated */
@@ -106,7 +130,33 @@ declare class FlowstaAuth {
106
130
  * Get current auth state
107
131
  */
108
132
  getState(): AuthState;
133
+ /**
134
+ * Detect whether Flowsta Vault (desktop app) is running.
135
+ *
136
+ * Probes the IPC server at localhost:27777. If running and unlocked,
137
+ * signing can be done locally instead of via the API.
138
+ *
139
+ * @returns Detection result with running status and agent info
140
+ */
141
+ detectVault(): Promise<VaultDetectionResult>;
142
+ /**
143
+ * Get agents linked to a specific agent (or the current user's agent).
144
+ *
145
+ * Queries the API which reads from the DHT (IsSamePersonEntry).
146
+ *
147
+ * @param agentPubKey Optional specific agent to query. Defaults to current user's agent.
148
+ * @returns List of linked agent public keys
149
+ */
150
+ getLinkedAgents(agentPubKey?: string): Promise<string[]>;
151
+ /**
152
+ * Check if two agents are linked (verified on the DHT).
153
+ *
154
+ * @param agentA First agent's public key
155
+ * @param agentB Second agent's public key
156
+ * @returns true if the agents are linked via an IsSamePersonEntry
157
+ */
158
+ areAgentsLinked(agentA: string, agentB: string): Promise<boolean>;
109
159
  private restoreSession;
110
160
  }
111
161
 
112
- export { type AuthState, FlowstaAuth, type FlowstaAuthConfig, type FlowstaUser, FlowstaAuth as default };
162
+ export { type AuthState, FlowstaAuth, type FlowstaAuthConfig, type FlowstaUser, type LinkedAgent, type VaultDetectionResult, FlowstaAuth as default };
package/dist/index.d.ts CHANGED
@@ -9,6 +9,8 @@
9
9
  * - Zero-knowledge architecture
10
10
  * - No client secrets needed (PKCE provides security)
11
11
  * - Simple "Sign in with Flowsta" integration
12
+ * - Flowsta Vault detection (desktop app IPC)
13
+ * - Agent linking queries (DHT-verified identity proofs)
12
14
  */
13
15
  interface FlowstaAuthConfig {
14
16
  /** Your Flowsta application client ID (from dev.flowsta.com) */
@@ -37,6 +39,28 @@ interface FlowstaUser {
37
39
  agentPubKey?: string;
38
40
  /** User's DID (Decentralized Identifier) */
39
41
  did?: string;
42
+ /** Agents linked to this user (from DHT IsSamePersonEntry) */
43
+ linkedAgents?: LinkedAgent[];
44
+ /** Current signing mode ('remote' = API, 'ipc' = Flowsta Vault) */
45
+ signingMode?: 'remote' | 'ipc';
46
+ }
47
+ /** A linked agent (verified on the DHT via IsSamePersonEntry) */
48
+ interface LinkedAgent {
49
+ /** The linked agent's public key */
50
+ agentPubKey: string;
51
+ /** When the link was created */
52
+ linkedAt?: string;
53
+ /** Whether the link has been revoked */
54
+ isRevoked: boolean;
55
+ }
56
+ /** Flowsta Vault desktop app status */
57
+ interface VaultDetectionResult {
58
+ /** Whether Flowsta Vault is running and reachable on localhost */
59
+ running: boolean;
60
+ /** The vault agent's public key (if unlocked) */
61
+ agentPubKey?: string;
62
+ /** The vault agent's DID (if unlocked) */
63
+ did?: string;
40
64
  }
41
65
  interface AuthState {
42
66
  /** Whether the user is authenticated */
@@ -106,7 +130,33 @@ declare class FlowstaAuth {
106
130
  * Get current auth state
107
131
  */
108
132
  getState(): AuthState;
133
+ /**
134
+ * Detect whether Flowsta Vault (desktop app) is running.
135
+ *
136
+ * Probes the IPC server at localhost:27777. If running and unlocked,
137
+ * signing can be done locally instead of via the API.
138
+ *
139
+ * @returns Detection result with running status and agent info
140
+ */
141
+ detectVault(): Promise<VaultDetectionResult>;
142
+ /**
143
+ * Get agents linked to a specific agent (or the current user's agent).
144
+ *
145
+ * Queries the API which reads from the DHT (IsSamePersonEntry).
146
+ *
147
+ * @param agentPubKey Optional specific agent to query. Defaults to current user's agent.
148
+ * @returns List of linked agent public keys
149
+ */
150
+ getLinkedAgents(agentPubKey?: string): Promise<string[]>;
151
+ /**
152
+ * Check if two agents are linked (verified on the DHT).
153
+ *
154
+ * @param agentA First agent's public key
155
+ * @param agentB Second agent's public key
156
+ * @returns true if the agents are linked via an IsSamePersonEntry
157
+ */
158
+ areAgentsLinked(agentA: string, agentB: string): Promise<boolean>;
109
159
  private restoreSession;
110
160
  }
111
161
 
112
- export { type AuthState, FlowstaAuth, type FlowstaAuthConfig, type FlowstaUser, FlowstaAuth as default };
162
+ export { type AuthState, FlowstaAuth, type FlowstaAuthConfig, type FlowstaUser, type LinkedAgent, type VaultDetectionResult, FlowstaAuth as default };
package/dist/index.js CHANGED
@@ -123,12 +123,13 @@ var FlowstaAuth = class {
123
123
  sessionStorage.removeItem("flowsta_code_verifier");
124
124
  sessionStorage.removeItem("flowsta_state");
125
125
  const userResponse = await fetch(`${this.config.apiUrl}/oauth/userinfo`, {
126
- headers: { "Authorization": `Bearer ${access_token}` }
126
+ headers: { Authorization: `Bearer ${access_token}` }
127
127
  });
128
128
  if (!userResponse.ok) {
129
129
  throw new Error("Failed to fetch user info");
130
130
  }
131
131
  const userData = await userResponse.json();
132
+ const vault = await this.detectVault();
132
133
  this.accessToken = access_token;
133
134
  this.user = {
134
135
  id: userData.sub || userData.id,
@@ -137,7 +138,8 @@ var FlowstaAuth = class {
137
138
  displayName: userData.display_name || userData.name,
138
139
  profilePicture: userData.picture || userData.profile_picture,
139
140
  agentPubKey: userData.agent_pub_key,
140
- did: userData.did
141
+ did: userData.did,
142
+ signingMode: vault.running ? "ipc" : "remote"
141
143
  };
142
144
  localStorage.setItem("flowsta_access_token", access_token);
143
145
  localStorage.setItem("flowsta_user", JSON.stringify(this.user));
@@ -186,6 +188,88 @@ var FlowstaAuth = class {
186
188
  error: null
187
189
  };
188
190
  }
191
+ // ── Vault Detection ──────────────────────────────────────────────
192
+ /**
193
+ * Detect whether Flowsta Vault (desktop app) is running.
194
+ *
195
+ * Probes the IPC server at localhost:27777. If running and unlocked,
196
+ * signing can be done locally instead of via the API.
197
+ *
198
+ * @returns Detection result with running status and agent info
199
+ */
200
+ async detectVault() {
201
+ try {
202
+ const controller = new AbortController();
203
+ const timeout = setTimeout(() => controller.abort(), 2e3);
204
+ const response = await fetch("http://127.0.0.1:27777/status", {
205
+ signal: controller.signal
206
+ });
207
+ clearTimeout(timeout);
208
+ if (!response.ok) {
209
+ return { running: false };
210
+ }
211
+ const data = await response.json();
212
+ return {
213
+ running: true,
214
+ agentPubKey: data.agent_pub_key || data.agentPubKey,
215
+ did: data.did
216
+ };
217
+ } catch {
218
+ return { running: false };
219
+ }
220
+ }
221
+ // ── Agent Linking ────────────────────────────────────────────────
222
+ /**
223
+ * Get agents linked to a specific agent (or the current user's agent).
224
+ *
225
+ * Queries the API which reads from the DHT (IsSamePersonEntry).
226
+ *
227
+ * @param agentPubKey Optional specific agent to query. Defaults to current user's agent.
228
+ * @returns List of linked agent public keys
229
+ */
230
+ async getLinkedAgents(agentPubKey) {
231
+ const token = this.accessToken;
232
+ if (!token) {
233
+ throw new Error("Not authenticated");
234
+ }
235
+ const url = new URL(`${this.config.apiUrl}/auth/linked-agents`);
236
+ if (agentPubKey) {
237
+ url.searchParams.set("agent_pub_key", agentPubKey);
238
+ }
239
+ const response = await fetch(url.toString(), {
240
+ headers: { Authorization: `Bearer ${token}` }
241
+ });
242
+ if (!response.ok) {
243
+ const data2 = await response.json().catch(() => ({}));
244
+ throw new Error(data2.error || "Failed to get linked agents");
245
+ }
246
+ const data = await response.json();
247
+ return data.linked_agents || [];
248
+ }
249
+ /**
250
+ * Check if two agents are linked (verified on the DHT).
251
+ *
252
+ * @param agentA First agent's public key
253
+ * @param agentB Second agent's public key
254
+ * @returns true if the agents are linked via an IsSamePersonEntry
255
+ */
256
+ async areAgentsLinked(agentA, agentB) {
257
+ const token = this.accessToken;
258
+ if (!token) {
259
+ throw new Error("Not authenticated");
260
+ }
261
+ const url = new URL(`${this.config.apiUrl}/auth/are-agents-linked`);
262
+ url.searchParams.set("agent_a", agentA);
263
+ url.searchParams.set("agent_b", agentB);
264
+ const response = await fetch(url.toString(), {
265
+ headers: { Authorization: `Bearer ${token}` }
266
+ });
267
+ if (!response.ok) {
268
+ return false;
269
+ }
270
+ const data = await response.json();
271
+ return data.linked === true;
272
+ }
189
273
  restoreSession() {
190
274
  if (typeof localStorage === "undefined") return;
191
275
  const token = localStorage.getItem("flowsta_access_token");
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  FlowstaAuth,
3
3
  index_default
4
- } from "./chunk-NBWAVXMK.mjs";
4
+ } from "./chunk-HXNYC5IQ.mjs";
5
5
  export {
6
6
  FlowstaAuth,
7
7
  index_default as default
package/dist/react.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ReactNode } from 'react';
3
3
  import { FlowstaAuthConfig, AuthState, FlowstaUser } from './index.mjs';
4
- export { default as FlowstaAuth } from './index.mjs';
4
+ export { FlowstaAuth } from './index.mjs';
5
5
 
6
6
  interface FlowstaAuthContextValue extends AuthState {
7
7
  /** Redirect to Flowsta login */
package/dist/react.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ReactNode } from 'react';
3
3
  import { FlowstaAuthConfig, AuthState, FlowstaUser } from './index.js';
4
- export { default as FlowstaAuth } from './index.js';
4
+ export { FlowstaAuth } from './index.js';
5
5
 
6
6
  interface FlowstaAuthContextValue extends AuthState {
7
7
  /** Redirect to Flowsta login */
package/dist/react.js CHANGED
@@ -129,12 +129,13 @@ var FlowstaAuth = class {
129
129
  sessionStorage.removeItem("flowsta_code_verifier");
130
130
  sessionStorage.removeItem("flowsta_state");
131
131
  const userResponse = await fetch(`${this.config.apiUrl}/oauth/userinfo`, {
132
- headers: { "Authorization": `Bearer ${access_token}` }
132
+ headers: { Authorization: `Bearer ${access_token}` }
133
133
  });
134
134
  if (!userResponse.ok) {
135
135
  throw new Error("Failed to fetch user info");
136
136
  }
137
137
  const userData = await userResponse.json();
138
+ const vault = await this.detectVault();
138
139
  this.accessToken = access_token;
139
140
  this.user = {
140
141
  id: userData.sub || userData.id,
@@ -143,7 +144,8 @@ var FlowstaAuth = class {
143
144
  displayName: userData.display_name || userData.name,
144
145
  profilePicture: userData.picture || userData.profile_picture,
145
146
  agentPubKey: userData.agent_pub_key,
146
- did: userData.did
147
+ did: userData.did,
148
+ signingMode: vault.running ? "ipc" : "remote"
147
149
  };
148
150
  localStorage.setItem("flowsta_access_token", access_token);
149
151
  localStorage.setItem("flowsta_user", JSON.stringify(this.user));
@@ -192,6 +194,88 @@ var FlowstaAuth = class {
192
194
  error: null
193
195
  };
194
196
  }
197
+ // ── Vault Detection ──────────────────────────────────────────────
198
+ /**
199
+ * Detect whether Flowsta Vault (desktop app) is running.
200
+ *
201
+ * Probes the IPC server at localhost:27777. If running and unlocked,
202
+ * signing can be done locally instead of via the API.
203
+ *
204
+ * @returns Detection result with running status and agent info
205
+ */
206
+ async detectVault() {
207
+ try {
208
+ const controller = new AbortController();
209
+ const timeout = setTimeout(() => controller.abort(), 2e3);
210
+ const response = await fetch("http://127.0.0.1:27777/status", {
211
+ signal: controller.signal
212
+ });
213
+ clearTimeout(timeout);
214
+ if (!response.ok) {
215
+ return { running: false };
216
+ }
217
+ const data = await response.json();
218
+ return {
219
+ running: true,
220
+ agentPubKey: data.agent_pub_key || data.agentPubKey,
221
+ did: data.did
222
+ };
223
+ } catch {
224
+ return { running: false };
225
+ }
226
+ }
227
+ // ── Agent Linking ────────────────────────────────────────────────
228
+ /**
229
+ * Get agents linked to a specific agent (or the current user's agent).
230
+ *
231
+ * Queries the API which reads from the DHT (IsSamePersonEntry).
232
+ *
233
+ * @param agentPubKey Optional specific agent to query. Defaults to current user's agent.
234
+ * @returns List of linked agent public keys
235
+ */
236
+ async getLinkedAgents(agentPubKey) {
237
+ const token = this.accessToken;
238
+ if (!token) {
239
+ throw new Error("Not authenticated");
240
+ }
241
+ const url = new URL(`${this.config.apiUrl}/auth/linked-agents`);
242
+ if (agentPubKey) {
243
+ url.searchParams.set("agent_pub_key", agentPubKey);
244
+ }
245
+ const response = await fetch(url.toString(), {
246
+ headers: { Authorization: `Bearer ${token}` }
247
+ });
248
+ if (!response.ok) {
249
+ const data2 = await response.json().catch(() => ({}));
250
+ throw new Error(data2.error || "Failed to get linked agents");
251
+ }
252
+ const data = await response.json();
253
+ return data.linked_agents || [];
254
+ }
255
+ /**
256
+ * Check if two agents are linked (verified on the DHT).
257
+ *
258
+ * @param agentA First agent's public key
259
+ * @param agentB Second agent's public key
260
+ * @returns true if the agents are linked via an IsSamePersonEntry
261
+ */
262
+ async areAgentsLinked(agentA, agentB) {
263
+ const token = this.accessToken;
264
+ if (!token) {
265
+ throw new Error("Not authenticated");
266
+ }
267
+ const url = new URL(`${this.config.apiUrl}/auth/are-agents-linked`);
268
+ url.searchParams.set("agent_a", agentA);
269
+ url.searchParams.set("agent_b", agentB);
270
+ const response = await fetch(url.toString(), {
271
+ headers: { Authorization: `Bearer ${token}` }
272
+ });
273
+ if (!response.ok) {
274
+ return false;
275
+ }
276
+ const data = await response.json();
277
+ return data.linked === true;
278
+ }
195
279
  restoreSession() {
196
280
  if (typeof localStorage === "undefined") return;
197
281
  const token = localStorage.getItem("flowsta_access_token");
package/dist/react.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  FlowstaAuth
3
- } from "./chunk-NBWAVXMK.mjs";
3
+ } from "./chunk-HXNYC5IQ.mjs";
4
4
 
5
5
  // src/react.tsx
6
6
  import {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@flowsta/auth",
3
- "version": "2.0.0",
4
- "description": "Flowsta Auth SDK 2.0 - OAuth-only authentication for web applications",
3
+ "version": "2.1.0",
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",
7
7
  "types": "./dist/index.d.ts",
@@ -51,10 +51,11 @@
51
51
  }
52
52
  },
53
53
  "devDependencies": {
54
+ "@types/react": "^18.2.0",
55
+ "happy-dom": "20.5.0",
54
56
  "tsup": "^8.0.0",
55
57
  "typescript": "^5.3.0",
56
- "vitest": "^1.0.0",
57
- "@types/react": "^18.2.0"
58
+ "vite": "7.3.1",
59
+ "vitest": "^4.0.18"
58
60
  }
59
61
  }
60
-