@matdata/yasgui 5.5.0 → 5.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/ts/src/ConfigExportImport.js +3 -0
- package/build/ts/src/ConfigExportImport.js.map +1 -1
- package/build/ts/src/OAuth2Utils.d.ts +18 -0
- package/build/ts/src/OAuth2Utils.js +214 -0
- package/build/ts/src/OAuth2Utils.js.map +1 -0
- package/build/ts/src/PersistentConfig.d.ts +3 -0
- package/build/ts/src/PersistentConfig.js +7 -0
- package/build/ts/src/PersistentConfig.js.map +1 -1
- package/build/ts/src/Tab.d.ts +1 -1
- package/build/ts/src/Tab.js +116 -85
- package/build/ts/src/Tab.js.map +1 -1
- package/build/ts/src/TabSettingsModal.d.ts +1 -0
- package/build/ts/src/TabSettingsModal.js +330 -27
- package/build/ts/src/TabSettingsModal.js.map +1 -1
- package/build/ts/src/defaults.js +1 -1
- package/build/ts/src/defaults.js.map +1 -1
- package/build/ts/src/index.d.ts +21 -6
- package/build/ts/src/index.js +7 -1
- package/build/ts/src/index.js.map +1 -1
- package/build/ts/src/version.d.ts +1 -1
- package/build/ts/src/version.js +1 -1
- package/build/yasgui.min.css +1 -1
- package/build/yasgui.min.css.map +3 -3
- package/build/yasgui.min.js +185 -157
- package/build/yasgui.min.js.map +4 -4
- package/package.json +3 -2
- package/src/ConfigExportImport.ts +3 -0
- package/src/OAuth2Utils.ts +315 -0
- package/src/PersistentConfig.ts +10 -0
- package/src/Tab.ts +191 -111
- package/src/TabSettingsModal.scss +70 -3
- package/src/TabSettingsModal.ts +400 -30
- package/src/defaults.ts +1 -1
- package/src/endpointSelect.scss +12 -0
- package/src/index.ts +42 -10
- package/src/tab.scss +1 -0
- package/src/themes.scss +1 -0
- package/src/version.ts +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matdata/yasgui",
|
|
3
3
|
"description": "Yet Another SPARQL GUI",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.7.0",
|
|
5
5
|
"main": "build/yasgui.min.js",
|
|
6
6
|
"types": "build/ts/src/index.d.ts",
|
|
7
7
|
"license": "MIT",
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
"jsuri": "^1.3.1",
|
|
34
34
|
"lodash-es": "^4.17.15",
|
|
35
35
|
"sortablejs": "^1.10.2",
|
|
36
|
-
"@matdata/yasgui-graph-plugin": "^1.
|
|
36
|
+
"@matdata/yasgui-graph-plugin": "^1.3.0",
|
|
37
|
+
"@matdata/yasgui-table-plugin": "^1.0.0"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
39
40
|
"@types/autosuggest-highlight": "^3.1.0",
|
|
@@ -305,6 +305,9 @@ export function parseFromTurtle(turtle: string): Partial<PersistedJson> {
|
|
|
305
305
|
withCredentials: false,
|
|
306
306
|
adjustQueryBeforeRequest: false,
|
|
307
307
|
basicAuth: undefined,
|
|
308
|
+
bearerAuth: undefined,
|
|
309
|
+
apiKeyAuth: undefined,
|
|
310
|
+
oauth2Auth: undefined,
|
|
308
311
|
},
|
|
309
312
|
yasr: {
|
|
310
313
|
settings: {},
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.0 Utility Functions
|
|
3
|
+
* Handles OAuth 2.0 Authorization Code flow with PKCE
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generate a random string for PKCE code verifier
|
|
8
|
+
*/
|
|
9
|
+
function generateRandomString(length: number): string {
|
|
10
|
+
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
|
|
11
|
+
const values = crypto.getRandomValues(new Uint8Array(length));
|
|
12
|
+
return Array.from(values)
|
|
13
|
+
.map((v) => possible[v % possible.length])
|
|
14
|
+
.join("");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generate SHA-256 hash and base64url encode it
|
|
19
|
+
*/
|
|
20
|
+
async function sha256(plain: string): Promise<string> {
|
|
21
|
+
const encoder = new TextEncoder();
|
|
22
|
+
const data = encoder.encode(plain);
|
|
23
|
+
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
24
|
+
return base64urlEncode(hash);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Base64url encode (without padding)
|
|
29
|
+
*/
|
|
30
|
+
function base64urlEncode(buffer: ArrayBuffer): string {
|
|
31
|
+
const bytes = new Uint8Array(buffer);
|
|
32
|
+
let binary = "";
|
|
33
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
34
|
+
binary += String.fromCharCode(bytes[i]);
|
|
35
|
+
}
|
|
36
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface OAuth2Config {
|
|
40
|
+
clientId: string;
|
|
41
|
+
authorizationEndpoint: string;
|
|
42
|
+
tokenEndpoint: string;
|
|
43
|
+
redirectUri: string;
|
|
44
|
+
scope?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface OAuth2TokenResponse {
|
|
48
|
+
access_token: string;
|
|
49
|
+
id_token?: string; // ID token for authentication (Azure AD, OIDC)
|
|
50
|
+
refresh_token?: string;
|
|
51
|
+
expires_in?: number; // seconds
|
|
52
|
+
token_type: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check if a token has expired
|
|
57
|
+
*/
|
|
58
|
+
export function isTokenExpired(tokenExpiry?: number): boolean {
|
|
59
|
+
if (!tokenExpiry) return true;
|
|
60
|
+
// Add 60 second buffer to refresh before actual expiration
|
|
61
|
+
return Date.now() >= tokenExpiry - 60000;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Start OAuth 2.0 Authorization Code flow with PKCE
|
|
66
|
+
* Opens a popup window for user authentication
|
|
67
|
+
*/
|
|
68
|
+
export async function startOAuth2Flow(config: OAuth2Config): Promise<OAuth2TokenResponse> {
|
|
69
|
+
// Generate PKCE code verifier and challenge
|
|
70
|
+
const codeVerifier = generateRandomString(128);
|
|
71
|
+
const codeChallenge = await sha256(codeVerifier);
|
|
72
|
+
|
|
73
|
+
// Generate state for CSRF protection
|
|
74
|
+
const state = generateRandomString(32);
|
|
75
|
+
|
|
76
|
+
// Generate unique flow ID to prevent collisions across multiple OAuth flows
|
|
77
|
+
const flowId = generateRandomString(16);
|
|
78
|
+
|
|
79
|
+
// Store code verifier and state in sessionStorage for later retrieval with unique keys
|
|
80
|
+
sessionStorage.setItem(`oauth2_code_verifier_${flowId}`, codeVerifier);
|
|
81
|
+
sessionStorage.setItem(`oauth2_state_${flowId}`, state);
|
|
82
|
+
|
|
83
|
+
// Build authorization URL
|
|
84
|
+
const authUrl = new URL(config.authorizationEndpoint);
|
|
85
|
+
authUrl.searchParams.set("client_id", config.clientId);
|
|
86
|
+
authUrl.searchParams.set("response_type", "code");
|
|
87
|
+
authUrl.searchParams.set("redirect_uri", config.redirectUri);
|
|
88
|
+
authUrl.searchParams.set("code_challenge", codeChallenge);
|
|
89
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
90
|
+
// Include flowId in state to link the callback to this specific flow
|
|
91
|
+
authUrl.searchParams.set("state", `${state}:${flowId}`);
|
|
92
|
+
if (config.scope) {
|
|
93
|
+
authUrl.searchParams.set("scope", config.scope);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Open popup window
|
|
97
|
+
const width = 600;
|
|
98
|
+
const height = 700;
|
|
99
|
+
const left = window.screenX + (window.outerWidth - width) / 2;
|
|
100
|
+
const top = window.screenY + (window.outerHeight - height) / 2;
|
|
101
|
+
const popup = window.open(
|
|
102
|
+
authUrl.toString(),
|
|
103
|
+
"OAuth2 Authorization",
|
|
104
|
+
`width=${width},height=${height},left=${left},top=${top},popup=yes,scrollbars=yes`,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
if (!popup) {
|
|
108
|
+
throw new Error("Failed to open OAuth 2.0 authorization popup. Please allow popups for this site.");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Wait for the OAuth callback
|
|
112
|
+
return new Promise<OAuth2TokenResponse>((resolve, reject) => {
|
|
113
|
+
// Flag to prevent race condition between polling and messageHandler
|
|
114
|
+
let flowCompleted = false;
|
|
115
|
+
|
|
116
|
+
const cleanup = () => {
|
|
117
|
+
clearInterval(checkInterval);
|
|
118
|
+
window.removeEventListener("message", messageHandler);
|
|
119
|
+
// Clean up sessionStorage
|
|
120
|
+
sessionStorage.removeItem(`oauth2_code_verifier_${flowId}`);
|
|
121
|
+
sessionStorage.removeItem(`oauth2_state_${flowId}`);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const checkInterval = setInterval(() => {
|
|
125
|
+
try {
|
|
126
|
+
// Check if popup is closed
|
|
127
|
+
if (popup.closed) {
|
|
128
|
+
if (!flowCompleted) {
|
|
129
|
+
flowCompleted = true;
|
|
130
|
+
cleanup();
|
|
131
|
+
reject(new Error("OAuth 2.0 authorization was cancelled"));
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Try to read popup URL (will throw if cross-origin)
|
|
137
|
+
try {
|
|
138
|
+
const popupUrl = popup.location.href;
|
|
139
|
+
if (popupUrl.startsWith(config.redirectUri) && !flowCompleted) {
|
|
140
|
+
flowCompleted = true;
|
|
141
|
+
cleanup();
|
|
142
|
+
popup.close();
|
|
143
|
+
|
|
144
|
+
// Parse the authorization code from URL
|
|
145
|
+
const url = new URL(popupUrl);
|
|
146
|
+
const code = url.searchParams.get("code");
|
|
147
|
+
const returnedState = url.searchParams.get("state");
|
|
148
|
+
const error = url.searchParams.get("error");
|
|
149
|
+
const errorDescription = url.searchParams.get("error_description");
|
|
150
|
+
|
|
151
|
+
if (error) {
|
|
152
|
+
reject(new Error(`OAuth 2.0 error: ${error}${errorDescription ? " - " + errorDescription : ""}`));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!code) {
|
|
157
|
+
reject(new Error("No authorization code received"));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Validate state includes our flowId
|
|
162
|
+
const expectedState = `${state}:${flowId}`;
|
|
163
|
+
if (returnedState !== expectedState) {
|
|
164
|
+
reject(new Error("State mismatch - possible CSRF attack"));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Exchange code for tokens
|
|
169
|
+
exchangeCodeForToken(config, code, codeVerifier).then(resolve).catch(reject);
|
|
170
|
+
}
|
|
171
|
+
} catch (e) {
|
|
172
|
+
// Cross-origin error is expected when on OAuth provider's domain
|
|
173
|
+
// Keep waiting
|
|
174
|
+
}
|
|
175
|
+
} catch (e) {
|
|
176
|
+
// Ignore errors while checking popup
|
|
177
|
+
}
|
|
178
|
+
}, 500);
|
|
179
|
+
|
|
180
|
+
// Also listen for postMessage from redirect page (alternative method)
|
|
181
|
+
const messageHandler = (event: MessageEvent) => {
|
|
182
|
+
// Validate origin if needed (should match redirect URI origin)
|
|
183
|
+
if (event.data && event.data.type === "oauth2_callback" && !flowCompleted) {
|
|
184
|
+
flowCompleted = true;
|
|
185
|
+
cleanup();
|
|
186
|
+
if (popup && !popup.closed) {
|
|
187
|
+
popup.close();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const { code, state: returnedState, error, error_description } = event.data;
|
|
191
|
+
|
|
192
|
+
if (error) {
|
|
193
|
+
reject(new Error(`OAuth 2.0 error: ${error}${error_description ? " - " + error_description : ""}`));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (!code) {
|
|
198
|
+
reject(new Error("No authorization code received"));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Validate state includes our flowId
|
|
203
|
+
const expectedState = `${state}:${flowId}`;
|
|
204
|
+
if (returnedState !== expectedState) {
|
|
205
|
+
reject(new Error("State mismatch - possible CSRF attack"));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Exchange code for tokens
|
|
210
|
+
exchangeCodeForToken(config, code, codeVerifier).then(resolve).catch(reject);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
window.addEventListener("message", messageHandler);
|
|
215
|
+
|
|
216
|
+
// Cleanup timeout after 5 minutes
|
|
217
|
+
setTimeout(
|
|
218
|
+
() => {
|
|
219
|
+
if (!flowCompleted) {
|
|
220
|
+
flowCompleted = true;
|
|
221
|
+
cleanup();
|
|
222
|
+
if (popup && !popup.closed) {
|
|
223
|
+
popup.close();
|
|
224
|
+
}
|
|
225
|
+
reject(new Error("OAuth 2.0 authorization timeout"));
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
5 * 60 * 1000,
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Exchange authorization code for access token
|
|
235
|
+
*/
|
|
236
|
+
async function exchangeCodeForToken(
|
|
237
|
+
config: OAuth2Config,
|
|
238
|
+
code: string,
|
|
239
|
+
codeVerifier: string,
|
|
240
|
+
): Promise<OAuth2TokenResponse> {
|
|
241
|
+
const tokenUrl = config.tokenEndpoint;
|
|
242
|
+
|
|
243
|
+
const body = new URLSearchParams();
|
|
244
|
+
body.set("grant_type", "authorization_code");
|
|
245
|
+
body.set("code", code);
|
|
246
|
+
body.set("redirect_uri", config.redirectUri);
|
|
247
|
+
body.set("client_id", config.clientId);
|
|
248
|
+
body.set("code_verifier", codeVerifier);
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const response = await fetch(tokenUrl, {
|
|
252
|
+
method: "POST",
|
|
253
|
+
headers: {
|
|
254
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
255
|
+
},
|
|
256
|
+
body: body.toString(),
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
if (!response.ok) {
|
|
260
|
+
const errorText = await response.text();
|
|
261
|
+
throw new Error(`Token exchange failed: ${response.status} ${response.statusText} - ${errorText}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const tokenResponse: OAuth2TokenResponse = await response.json();
|
|
265
|
+
|
|
266
|
+
// Note: sessionStorage cleanup is handled by the caller
|
|
267
|
+
return tokenResponse;
|
|
268
|
+
} catch (error) {
|
|
269
|
+
// Note: sessionStorage cleanup is handled by the caller
|
|
270
|
+
throw error;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Refresh an OAuth 2.0 access token using a refresh token
|
|
276
|
+
*/
|
|
277
|
+
export async function refreshOAuth2Token(
|
|
278
|
+
config: Omit<OAuth2Config, "authorizationEndpoint" | "redirectUri">,
|
|
279
|
+
refreshToken: string,
|
|
280
|
+
): Promise<OAuth2TokenResponse> {
|
|
281
|
+
const tokenUrl = config.tokenEndpoint;
|
|
282
|
+
|
|
283
|
+
const body = new URLSearchParams();
|
|
284
|
+
body.set("grant_type", "refresh_token");
|
|
285
|
+
body.set("refresh_token", refreshToken);
|
|
286
|
+
body.set("client_id", config.clientId);
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
const response = await fetch(tokenUrl, {
|
|
290
|
+
method: "POST",
|
|
291
|
+
headers: {
|
|
292
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
293
|
+
},
|
|
294
|
+
body: body.toString(),
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
if (!response.ok) {
|
|
298
|
+
const errorText = await response.text();
|
|
299
|
+
throw new Error(`Token refresh failed: ${response.status} ${response.statusText} - ${errorText}`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const tokenResponse: OAuth2TokenResponse = await response.json();
|
|
303
|
+
return tokenResponse;
|
|
304
|
+
} catch (error) {
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Calculate token expiry timestamp from expires_in value
|
|
311
|
+
*/
|
|
312
|
+
export function calculateTokenExpiry(expiresIn?: number): number | undefined {
|
|
313
|
+
if (!expiresIn) return undefined;
|
|
314
|
+
return Date.now() + expiresIn * 1000;
|
|
315
|
+
}
|
package/src/PersistentConfig.ts
CHANGED
|
@@ -14,6 +14,7 @@ export interface PersistedJson {
|
|
|
14
14
|
endpointConfigs?: EndpointConfig[]; // New endpoint-based storage with auth
|
|
15
15
|
theme?: "light" | "dark";
|
|
16
16
|
orientation?: "vertical" | "horizontal";
|
|
17
|
+
showSnippetsBar?: boolean;
|
|
17
18
|
}
|
|
18
19
|
function getDefaults(): PersistedJson {
|
|
19
20
|
return {
|
|
@@ -166,6 +167,15 @@ export default class PersistentConfig {
|
|
|
166
167
|
this.toStorage();
|
|
167
168
|
}
|
|
168
169
|
|
|
170
|
+
public getShowSnippetsBar(): boolean | undefined {
|
|
171
|
+
return this.persistedJson.showSnippetsBar;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
public setShowSnippetsBar(show: boolean) {
|
|
175
|
+
this.persistedJson.showSnippetsBar = show;
|
|
176
|
+
this.toStorage();
|
|
177
|
+
}
|
|
178
|
+
|
|
169
179
|
// New endpoint configuration methods
|
|
170
180
|
public getEndpointConfigs(): EndpointConfig[] {
|
|
171
181
|
return this.persistedJson.endpointConfigs || [];
|