@flowsta/auth 2.0.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/chunk-HXNYC5IQ.mjs +267 -0
- package/dist/index.d.mts +52 -2
- package/dist/index.d.ts +52 -2
- package/dist/index.js +87 -3
- package/dist/index.mjs +1 -1
- package/dist/react.d.mts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +87 -3
- package/dist/react.mjs +1 -1
- package/package.json +6 -5
|
@@ -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
|
+
};
|
|
@@ -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,13 +9,15 @@
|
|
|
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) */
|
|
15
17
|
clientId: string;
|
|
16
18
|
/** The URI to redirect back to after authentication */
|
|
17
19
|
redirectUri: string;
|
|
18
|
-
/** OAuth scopes to request. Default: ['
|
|
20
|
+
/** OAuth scopes to request. Default: ['openid', 'email', 'display_name'] */
|
|
19
21
|
scopes?: string[];
|
|
20
22
|
/** The Flowsta login URL. Default: 'https://login.flowsta.com' */
|
|
21
23
|
loginUrl?: string;
|
|
@@ -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,13 +9,15 @@
|
|
|
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) */
|
|
15
17
|
clientId: string;
|
|
16
18
|
/** The URI to redirect back to after authentication */
|
|
17
19
|
redirectUri: string;
|
|
18
|
-
/** OAuth scopes to request. Default: ['
|
|
20
|
+
/** OAuth scopes to request. Default: ['openid', 'email', 'display_name'] */
|
|
19
21
|
scopes?: string[];
|
|
20
22
|
/** The Flowsta login URL. Default: 'https://login.flowsta.com' */
|
|
21
23
|
loginUrl?: string;
|
|
@@ -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
|
@@ -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
|
};
|
|
@@ -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: {
|
|
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
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 {
|
|
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 {
|
|
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
|
@@ -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
|
};
|
|
@@ -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: {
|
|
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
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flowsta/auth",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Flowsta Auth SDK
|
|
3
|
+
"version": "2.1.1",
|
|
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
|
-
"
|
|
57
|
-
"
|
|
58
|
+
"vite": "7.3.1",
|
|
59
|
+
"vitest": "^4.0.18"
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
|
-
|