docpouch-client 0.9.1 → 1.0.3
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/README.md +630 -449
- package/chat-09e440bd-c52c-44b9-bba4-01cebc84bd7b.txt +223 -0
- package/dist/index.d.ts +156 -5
- package/dist/index.js +344 -10
- package/hff +114 -0
- package/{jest.config.js → jest.config.cjs} +1 -1
- package/package.json +41 -38
- package/src/index.ts +1045 -622
- package/tests/docPouchClient.test.ts +654 -0
- package/tests/mock-server.ts +613 -0
- package/tsconfig.build.json +7 -0
- package/tsconfig.json +9 -7
package/dist/index.js
CHANGED
|
@@ -25,6 +25,14 @@ export default class docPouchClient {
|
|
|
25
25
|
* @type {string | null}
|
|
26
26
|
*/
|
|
27
27
|
this.authToken = null;
|
|
28
|
+
this.oidcConfig = null;
|
|
29
|
+
this.oidcAccessToken = null;
|
|
30
|
+
this.oidcRefreshToken = null;
|
|
31
|
+
this.oidcIdToken = null;
|
|
32
|
+
this.oidcTokenExpiry = 0;
|
|
33
|
+
this.codeVerifier = null;
|
|
34
|
+
this.oidcState = null;
|
|
35
|
+
this.authMethod = 'none';
|
|
28
36
|
/**
|
|
29
37
|
* Flag indicating whether a connection attempt is in progress.
|
|
30
38
|
*
|
|
@@ -66,7 +74,7 @@ export default class docPouchClient {
|
|
|
66
74
|
return;
|
|
67
75
|
}
|
|
68
76
|
this.realTimeSync = newRealTimeSync;
|
|
69
|
-
if (newRealTimeSync && this.authToken) {
|
|
77
|
+
if (newRealTimeSync && (this.authToken || this.oidcAccessToken)) {
|
|
70
78
|
console.log("Activating realtime updates");
|
|
71
79
|
// Ensure we're not in the middle of another connection attempt
|
|
72
80
|
if (this.connectionInProgress) {
|
|
@@ -96,6 +104,7 @@ export default class docPouchClient {
|
|
|
96
104
|
const response = await this.request('/users/login', 'POST', credentials, false);
|
|
97
105
|
if (response.token) {
|
|
98
106
|
this.authToken = response.token;
|
|
107
|
+
this.authMethod = 'jwt';
|
|
99
108
|
// Reconnect websocket with new token if realtime sync is enabled
|
|
100
109
|
if (this.realTimeSync) {
|
|
101
110
|
this.initWebSocket();
|
|
@@ -236,8 +245,11 @@ export default class docPouchClient {
|
|
|
236
245
|
*/
|
|
237
246
|
setToken(token) {
|
|
238
247
|
console.log("Setting token to:", token ? "***token***" : "null");
|
|
248
|
+
this.authMethod = token ? 'jwt' : 'none';
|
|
239
249
|
const tokenChanged = this.authToken !== token;
|
|
240
250
|
this.authToken = token;
|
|
251
|
+
if (!token)
|
|
252
|
+
this.clearOidcTokens();
|
|
241
253
|
if (!tokenChanged) {
|
|
242
254
|
console.log("Token unchanged, no need to reconnect");
|
|
243
255
|
return;
|
|
@@ -279,17 +291,244 @@ export default class docPouchClient {
|
|
|
279
291
|
console.log("Socket connection debug info:");
|
|
280
292
|
console.log("- Connected:", this.socket.connected);
|
|
281
293
|
console.log("- Socket ID:", this.socket.id);
|
|
282
|
-
console.log("- Auth token present:", !!this.authToken);
|
|
294
|
+
console.log("- Auth token present:", !!this.authToken, "(method:", this.authMethod + ")");
|
|
295
|
+
console.log("- OIDC access token present:", !!this.oidcAccessToken);
|
|
283
296
|
console.log("- Connection in progress:", this.connectionInProgress);
|
|
284
297
|
console.log("- Realtime sync enabled:", this.realTimeSync);
|
|
285
298
|
console.log("- Socket options:", this.socket.io.opts);
|
|
299
|
+
const activeToken = this.authMethod === 'oidc' ? this.oidcAccessToken : this.authToken;
|
|
286
300
|
// Try to force reconnection
|
|
287
|
-
if (!this.socket.connected &&
|
|
301
|
+
if (!this.socket.connected && activeToken && this.realTimeSync) {
|
|
288
302
|
console.log("Attempting to force reconnection...");
|
|
289
|
-
this.socket.auth = { token:
|
|
303
|
+
this.socket.auth = { token: activeToken };
|
|
290
304
|
this.socket.connect();
|
|
291
305
|
}
|
|
292
306
|
}
|
|
307
|
+
// OIDC Authentication Methods
|
|
308
|
+
/**
|
|
309
|
+
* Sets the OIDC configuration to use for the callback and token exchange.
|
|
310
|
+
* Call this before {@link handleOidcCallback} if the client was freshly
|
|
311
|
+
* instantiated after a page reload (the config is otherwise only set
|
|
312
|
+
* internally by {@link loginWithOidc} before the redirect).
|
|
313
|
+
*
|
|
314
|
+
* @param {I_OidcConfig} config - OIDC provider configuration.
|
|
315
|
+
*/
|
|
316
|
+
setOidcConfig(config) {
|
|
317
|
+
this.oidcConfig = config;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Initiates the OIDC authentication flow by redirecting to the authorization endpoint.
|
|
321
|
+
*
|
|
322
|
+
* @param {I_OidcConfig} config - OIDC provider configuration.
|
|
323
|
+
* @returns {Promise<void>}
|
|
324
|
+
*/
|
|
325
|
+
async loginWithOidc(config) {
|
|
326
|
+
this.oidcConfig = config;
|
|
327
|
+
const response = await fetch(`${config.issuer}/.well-known/openid-configuration`);
|
|
328
|
+
const discovery = await response.json();
|
|
329
|
+
this.codeVerifier = this.generateCodeVerifier();
|
|
330
|
+
this.oidcState = this.generateState();
|
|
331
|
+
const codeChallenge = await this.generateCodeChallenge(this.codeVerifier);
|
|
332
|
+
if (typeof window !== 'undefined' && window.sessionStorage) {
|
|
333
|
+
sessionStorage.setItem('docpouch_oidc_state', this.oidcState);
|
|
334
|
+
sessionStorage.setItem('docpouch_oidc_code_verifier', this.codeVerifier);
|
|
335
|
+
sessionStorage.setItem('docpouch_oidc_issuer', config.issuer);
|
|
336
|
+
sessionStorage.setItem('docpouch_oidc_client_id', config.clientId);
|
|
337
|
+
sessionStorage.setItem('docpouch_oidc_redirect_uri', config.redirectUri);
|
|
338
|
+
}
|
|
339
|
+
const scope = config.scope || config.scopes?.join(' ') || 'openid profile';
|
|
340
|
+
const params = new URLSearchParams({
|
|
341
|
+
response_type: 'code',
|
|
342
|
+
client_id: config.clientId,
|
|
343
|
+
redirect_uri: config.redirectUri,
|
|
344
|
+
scope,
|
|
345
|
+
state: this.oidcState,
|
|
346
|
+
code_challenge: codeChallenge,
|
|
347
|
+
code_challenge_method: 'S256'
|
|
348
|
+
});
|
|
349
|
+
window.location.href = `${discovery.authorization_endpoint}?${params.toString()}`;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Handles the OIDC callback by exchanging the authorization code for tokens.
|
|
353
|
+
*
|
|
354
|
+
* @returns {Promise<boolean>} True if the callback was handled successfully.
|
|
355
|
+
*/
|
|
356
|
+
async handleOidcCallback() {
|
|
357
|
+
const params = new URLSearchParams(window.location.search);
|
|
358
|
+
const code = params.get('code');
|
|
359
|
+
const state = params.get('state');
|
|
360
|
+
const error = params.get('error');
|
|
361
|
+
if (error)
|
|
362
|
+
throw new Error(`OAuth error: ${error}`);
|
|
363
|
+
if (!code || !state)
|
|
364
|
+
return false;
|
|
365
|
+
if (typeof window !== 'undefined' && window.sessionStorage) {
|
|
366
|
+
this.oidcState = sessionStorage.getItem('docpouch_oidc_state') || this.oidcState;
|
|
367
|
+
this.codeVerifier = sessionStorage.getItem('docpouch_oidc_code_verifier') || this.codeVerifier;
|
|
368
|
+
sessionStorage.removeItem('docpouch_oidc_state');
|
|
369
|
+
sessionStorage.removeItem('docpouch_oidc_code_verifier');
|
|
370
|
+
if (!this.oidcConfig) {
|
|
371
|
+
const issuer = sessionStorage.getItem('docpouch_oidc_issuer');
|
|
372
|
+
const clientId = sessionStorage.getItem('docpouch_oidc_client_id');
|
|
373
|
+
const redirectUri = sessionStorage.getItem('docpouch_oidc_redirect_uri');
|
|
374
|
+
if (issuer && clientId && redirectUri) {
|
|
375
|
+
this.oidcConfig = { issuer, clientId, redirectUri };
|
|
376
|
+
sessionStorage.removeItem('docpouch_oidc_issuer');
|
|
377
|
+
sessionStorage.removeItem('docpouch_oidc_client_id');
|
|
378
|
+
sessionStorage.removeItem('docpouch_oidc_redirect_uri');
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (state !== this.oidcState)
|
|
383
|
+
throw new Error('State mismatch');
|
|
384
|
+
const discovery = await this.discoverOidc();
|
|
385
|
+
const body = new URLSearchParams({
|
|
386
|
+
grant_type: 'authorization_code',
|
|
387
|
+
code,
|
|
388
|
+
redirect_uri: this.oidcConfig.redirectUri,
|
|
389
|
+
client_id: this.oidcConfig.clientId,
|
|
390
|
+
code_verifier: this.codeVerifier || ''
|
|
391
|
+
});
|
|
392
|
+
const response = await fetch(discovery.token_endpoint, {
|
|
393
|
+
method: 'POST',
|
|
394
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
395
|
+
body: body.toString()
|
|
396
|
+
});
|
|
397
|
+
if (!response.ok)
|
|
398
|
+
throw new Error('Token exchange failed');
|
|
399
|
+
const raw = await response.json();
|
|
400
|
+
const tokens = {
|
|
401
|
+
accessToken: raw.access_token,
|
|
402
|
+
refreshToken: raw.refresh_token,
|
|
403
|
+
idToken: raw.id_token,
|
|
404
|
+
expiresIn: raw.expires_in,
|
|
405
|
+
tokenType: raw.token_type,
|
|
406
|
+
scope: raw.scope
|
|
407
|
+
};
|
|
408
|
+
this.setOidcTokens(tokens);
|
|
409
|
+
if (this.realTimeSync)
|
|
410
|
+
this.initWebSocket();
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
restoreOidcSession() {
|
|
414
|
+
if (typeof window === 'undefined' || !window.localStorage)
|
|
415
|
+
return false;
|
|
416
|
+
const stored = localStorage.getItem('docpouch_oidc_session');
|
|
417
|
+
if (!stored)
|
|
418
|
+
return false;
|
|
419
|
+
try {
|
|
420
|
+
const session = JSON.parse(stored);
|
|
421
|
+
if (!session.accessToken || Date.now() >= session.expiry) {
|
|
422
|
+
this.clearPersistedOidcSession();
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
this.authMethod = 'oidc';
|
|
426
|
+
this.oidcAccessToken = session.accessToken;
|
|
427
|
+
this.oidcRefreshToken = session.refreshToken || null;
|
|
428
|
+
this.oidcIdToken = session.idToken || null;
|
|
429
|
+
this.oidcTokenExpiry = session.expiry;
|
|
430
|
+
return true;
|
|
431
|
+
}
|
|
432
|
+
catch {
|
|
433
|
+
this.clearPersistedOidcSession();
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Ensures the OIDC access token is valid, refreshing it if necessary.
|
|
439
|
+
*/
|
|
440
|
+
async ensureValidOidcToken() {
|
|
441
|
+
if (this.authMethod !== 'oidc' || !this.oidcAccessToken)
|
|
442
|
+
throw new Error('Not authenticated via OIDC');
|
|
443
|
+
if (Date.now() >= this.oidcTokenExpiry - 60000) {
|
|
444
|
+
await this.refreshOidcToken();
|
|
445
|
+
}
|
|
446
|
+
return this.oidcAccessToken;
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Returns the current authentication method.
|
|
450
|
+
*
|
|
451
|
+
* @returns {'jwt' | 'oidc' | 'none'} The active auth method.
|
|
452
|
+
*/
|
|
453
|
+
getAuthMethod() {
|
|
454
|
+
return this.authMethod;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Checks whether the client is currently authenticated.
|
|
458
|
+
*
|
|
459
|
+
* @returns {boolean} True if authenticated.
|
|
460
|
+
*/
|
|
461
|
+
isAuthenticated() {
|
|
462
|
+
return this.authMethod !== 'none' && (this.authMethod === 'jwt'
|
|
463
|
+
? !!this.authToken
|
|
464
|
+
: (this.oidcAccessToken !== null && Date.now() < this.oidcTokenExpiry));
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Returns the current active token, regardless of auth method.
|
|
468
|
+
*
|
|
469
|
+
* @returns {string | null} The active token or null.
|
|
470
|
+
*/
|
|
471
|
+
getToken() {
|
|
472
|
+
return this.authMethod === 'oidc' ? this.oidcAccessToken : this.authToken;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Logs out the client, clearing all tokens and disconnecting WebSocket.
|
|
476
|
+
*
|
|
477
|
+
* @returns {Promise<void>}
|
|
478
|
+
*/
|
|
479
|
+
async logout() {
|
|
480
|
+
this.authToken = null;
|
|
481
|
+
this.clearOidcTokens();
|
|
482
|
+
this.authMethod = 'none';
|
|
483
|
+
if (this.socket.connected)
|
|
484
|
+
this.socket.disconnect();
|
|
485
|
+
if (typeof window !== 'undefined' && window.location &&
|
|
486
|
+
(window.location.search.includes('code=') || window.location.search.includes('state='))) {
|
|
487
|
+
window.history.replaceState({}, '', window.location.pathname);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
// OIDC Dynamic Client Registration Methods
|
|
491
|
+
/**
|
|
492
|
+
* Registers a new OIDC client with the server (dynamic client registration).
|
|
493
|
+
*
|
|
494
|
+
* @param {I_OidcClientRegistration} registration - Client metadata for registration.
|
|
495
|
+
* @param {string} [registrationToken] - Initial registration access token. If not provided, the current auth token is used.
|
|
496
|
+
* @returns {Promise<I_OidcClientResponse>} The registered client details.
|
|
497
|
+
*/
|
|
498
|
+
async registerOidcClient(registration, registrationToken) {
|
|
499
|
+
return await this.request('/oidc/reg', 'POST', registration, true, registrationToken);
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Retrieves the registration state of an existing OIDC client.
|
|
503
|
+
*
|
|
504
|
+
* @param {string} clientId - The client identifier.
|
|
505
|
+
* @param {string} [registrationToken] - The registration access token. If not provided, the current auth token is used.
|
|
506
|
+
* @returns {Promise<I_OidcClientResponse>} The client details.
|
|
507
|
+
*/
|
|
508
|
+
async getOidcClient(clientId, registrationToken) {
|
|
509
|
+
return await this.request(`/oidc/reg/${clientId}`, 'GET', undefined, true, registrationToken);
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Updates the registration of an existing OIDC client.
|
|
513
|
+
*
|
|
514
|
+
* @param {string} clientId - The client identifier.
|
|
515
|
+
* @param {I_OidcClientRegistration} registration - Updated client metadata.
|
|
516
|
+
* @param {string} [registrationToken] - The registration access token. If not provided, the current auth token is used.
|
|
517
|
+
* @returns {Promise<I_OidcClientResponse>} The updated client details.
|
|
518
|
+
*/
|
|
519
|
+
async updateOidcClient(clientId, registration, registrationToken) {
|
|
520
|
+
return await this.request(`/oidc/reg/${clientId}`, 'PUT', registration, true, registrationToken);
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Deletes an OIDC client registration.
|
|
524
|
+
*
|
|
525
|
+
* @param {string} clientId - The client identifier.
|
|
526
|
+
* @param {string} [registrationToken] - The registration access token. If not provided, the current auth token is used.
|
|
527
|
+
* @returns {Promise<void>}
|
|
528
|
+
*/
|
|
529
|
+
async deleteOidcClient(clientId, registrationToken) {
|
|
530
|
+
await this.request(`/oidc/reg/${clientId}`, 'DELETE', undefined, true, registrationToken);
|
|
531
|
+
}
|
|
293
532
|
/**
|
|
294
533
|
* Sets up permanent socket listeners for the client.
|
|
295
534
|
*
|
|
@@ -320,8 +559,9 @@ export default class docPouchClient {
|
|
|
320
559
|
* @private
|
|
321
560
|
*/
|
|
322
561
|
initWebSocket() {
|
|
323
|
-
|
|
324
|
-
|
|
562
|
+
const token = this.authMethod === 'oidc' ? this.oidcAccessToken : this.authToken;
|
|
563
|
+
console.log("initWebSocket called. Auth token present:", !!this.authToken, "Connection in progress:", this.connectionInProgress, "Socket connected:", this.socket.connected, "Auth method:", this.authMethod);
|
|
564
|
+
if (!token) {
|
|
325
565
|
console.log("Skipping WebSocket initialization: No auth token");
|
|
326
566
|
return;
|
|
327
567
|
}
|
|
@@ -337,7 +577,7 @@ export default class docPouchClient {
|
|
|
337
577
|
try {
|
|
338
578
|
console.log("Setting up WebSocket connection with token");
|
|
339
579
|
// Update the auth token
|
|
340
|
-
this.socket.auth = { token
|
|
580
|
+
this.socket.auth = { token };
|
|
341
581
|
// Remove any dynamic event listeners that might have been added
|
|
342
582
|
this.socket.offAny();
|
|
343
583
|
// Set up event handler for application events
|
|
@@ -383,12 +623,15 @@ export default class docPouchClient {
|
|
|
383
623
|
* @returns {Promise<T>} Parsed JSON response body.
|
|
384
624
|
* @private
|
|
385
625
|
*/
|
|
386
|
-
async request(endpoint, method, body, requiresAuth = true) {
|
|
626
|
+
async request(endpoint, method, body, requiresAuth = true, authTokenOverride) {
|
|
387
627
|
const headers = {
|
|
388
628
|
'Content-Type': 'application/json',
|
|
389
629
|
};
|
|
390
|
-
|
|
391
|
-
|
|
630
|
+
const authToken = authTokenOverride ?? (this.authMethod === 'oidc'
|
|
631
|
+
? await this.ensureValidOidcToken().catch(() => null)
|
|
632
|
+
: this.authToken);
|
|
633
|
+
if (requiresAuth && authToken)
|
|
634
|
+
headers['Authorization'] = `Bearer ${authToken}`;
|
|
392
635
|
if (this.socket.id)
|
|
393
636
|
headers['X-Socket-ID'] = this.socket.id;
|
|
394
637
|
const options = {
|
|
@@ -406,6 +649,97 @@ export default class docPouchClient {
|
|
|
406
649
|
}
|
|
407
650
|
throw new Error(`API error: ${response.status} ${response.statusText}`);
|
|
408
651
|
}
|
|
652
|
+
if (response.status === 204)
|
|
653
|
+
return undefined;
|
|
409
654
|
return await response.json();
|
|
410
655
|
}
|
|
656
|
+
// OIDC Private Helpers
|
|
657
|
+
async discoverOidc() {
|
|
658
|
+
const response = await fetch(`${this.oidcConfig.issuer}/.well-known/openid-configuration`);
|
|
659
|
+
return response.json();
|
|
660
|
+
}
|
|
661
|
+
setOidcTokens(tokens) {
|
|
662
|
+
this.authMethod = 'oidc';
|
|
663
|
+
this.oidcAccessToken = tokens.accessToken;
|
|
664
|
+
this.oidcRefreshToken = tokens.refreshToken || this.oidcRefreshToken;
|
|
665
|
+
this.oidcIdToken = tokens.idToken || null;
|
|
666
|
+
this.oidcTokenExpiry = Date.now() + (tokens.expiresIn * 1000);
|
|
667
|
+
this.persistOidcSession();
|
|
668
|
+
}
|
|
669
|
+
clearOidcTokens() {
|
|
670
|
+
this.oidcAccessToken = null;
|
|
671
|
+
this.oidcRefreshToken = null;
|
|
672
|
+
this.oidcIdToken = null;
|
|
673
|
+
this.oidcTokenExpiry = 0;
|
|
674
|
+
this.codeVerifier = null;
|
|
675
|
+
this.oidcState = null;
|
|
676
|
+
if (this.authMethod === 'oidc')
|
|
677
|
+
this.authMethod = 'none';
|
|
678
|
+
this.clearPersistedOidcSession();
|
|
679
|
+
}
|
|
680
|
+
persistOidcSession() {
|
|
681
|
+
if (typeof window !== 'undefined' && window.localStorage) {
|
|
682
|
+
const session = {
|
|
683
|
+
accessToken: this.oidcAccessToken,
|
|
684
|
+
refreshToken: this.oidcRefreshToken,
|
|
685
|
+
idToken: this.oidcIdToken,
|
|
686
|
+
expiry: this.oidcTokenExpiry
|
|
687
|
+
};
|
|
688
|
+
localStorage.setItem('docpouch_oidc_session', JSON.stringify(session));
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
clearPersistedOidcSession() {
|
|
692
|
+
if (typeof window !== 'undefined' && window.localStorage) {
|
|
693
|
+
localStorage.removeItem('docpouch_oidc_session');
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
async refreshOidcToken() {
|
|
697
|
+
if (!this.oidcRefreshToken)
|
|
698
|
+
throw new Error('No refresh token available');
|
|
699
|
+
const discovery = await this.discoverOidc();
|
|
700
|
+
const body = new URLSearchParams({
|
|
701
|
+
grant_type: 'refresh_token',
|
|
702
|
+
refresh_token: this.oidcRefreshToken,
|
|
703
|
+
client_id: this.oidcConfig.clientId
|
|
704
|
+
});
|
|
705
|
+
const response = await fetch(discovery.token_endpoint, {
|
|
706
|
+
method: 'POST',
|
|
707
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
708
|
+
body: body.toString()
|
|
709
|
+
});
|
|
710
|
+
if (!response.ok) {
|
|
711
|
+
this.clearOidcTokens();
|
|
712
|
+
throw new Error('Token refresh failed');
|
|
713
|
+
}
|
|
714
|
+
const raw = await response.json();
|
|
715
|
+
const tokens = {
|
|
716
|
+
accessToken: raw.access_token,
|
|
717
|
+
refreshToken: raw.refresh_token,
|
|
718
|
+
idToken: raw.id_token,
|
|
719
|
+
expiresIn: raw.expires_in,
|
|
720
|
+
tokenType: raw.token_type,
|
|
721
|
+
scope: raw.scope
|
|
722
|
+
};
|
|
723
|
+
this.setOidcTokens(tokens);
|
|
724
|
+
}
|
|
725
|
+
generateCodeVerifier() {
|
|
726
|
+
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
|
|
727
|
+
const array = new Uint8Array(64);
|
|
728
|
+
crypto.getRandomValues(array);
|
|
729
|
+
return Array.from(array, byte => charset[byte % charset.length]).join('');
|
|
730
|
+
}
|
|
731
|
+
async generateCodeChallenge(verifier) {
|
|
732
|
+
const encoder = new TextEncoder();
|
|
733
|
+
const data = encoder.encode(verifier);
|
|
734
|
+
const digest = await crypto.subtle.digest('SHA-256', data);
|
|
735
|
+
return btoa(String.fromCharCode(...new Uint8Array(digest)))
|
|
736
|
+
.replace(/\+/g, '-')
|
|
737
|
+
.replace(/\//g, '_')
|
|
738
|
+
.replace(/=+$/, '');
|
|
739
|
+
}
|
|
740
|
+
generateState() {
|
|
741
|
+
const array = new Uint8Array(32);
|
|
742
|
+
crypto.getRandomValues(array);
|
|
743
|
+
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
|
|
744
|
+
}
|
|
411
745
|
}
|
package/hff
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it
|
|
2
|
+
warning: in the working copy of 'src/index.ts', LF will be replaced by CRLF the next time Git touches it
|
|
3
|
+
warning: in the working copy of 'tsconfig.json', LF will be replaced by CRLF the next time Git touches it
|
|
4
|
+
[1mdiff --git a/package-lock.json b/package-lock.json[m
|
|
5
|
+
[1mindex dacae59..99c15e5 100644[m
|
|
6
|
+
[1m--- a/package-lock.json[m
|
|
7
|
+
[1m+++ b/package-lock.json[m
|
|
8
|
+
[36m@@ -1,12 +1,12 @@[m
|
|
9
|
+
{[m
|
|
10
|
+
"name": "docpouch-client",[m
|
|
11
|
+
[31m- "version": "1.0.0",[m
|
|
12
|
+
[32m+[m[32m "version": "1.0.1",[m
|
|
13
|
+
"lockfileVersion": 3,[m
|
|
14
|
+
"requires": true,[m
|
|
15
|
+
"packages": {[m
|
|
16
|
+
"": {[m
|
|
17
|
+
"name": "docpouch-client",[m
|
|
18
|
+
[31m- "version": "1.0.0",[m
|
|
19
|
+
[32m+[m[32m "version": "1.0.1",[m
|
|
20
|
+
"license": "MIT",[m
|
|
21
|
+
"dependencies": {[m
|
|
22
|
+
"socket.io-client": "^4.8.3"[m
|
|
23
|
+
[1mdiff --git a/package.json b/package.json[m
|
|
24
|
+
[1mindex 47412b6..61174a1 100644[m
|
|
25
|
+
[1m--- a/package.json[m
|
|
26
|
+
[1m+++ b/package.json[m
|
|
27
|
+
[36m@@ -1,6 +1,6 @@[m
|
|
28
|
+
{[m
|
|
29
|
+
"name": "docpouch-client",[m
|
|
30
|
+
[31m- "version": "1.0.1",[m
|
|
31
|
+
[32m+[m[32m "version": "1.0.3",[m
|
|
32
|
+
"main": "dist/index.js",[m
|
|
33
|
+
"types": "dist/index.d.ts",[m
|
|
34
|
+
"exports": {[m
|
|
35
|
+
[1mdiff --git a/src/index.ts b/src/index.ts[m
|
|
36
|
+
[1mindex b9368d4..545dade 100644[m
|
|
37
|
+
[1m--- a/src/index.ts[m
|
|
38
|
+
[1m+++ b/src/index.ts[m
|
|
39
|
+
[36m@@ -358,6 +358,18 @@[m [mexport default class docPouchClient {[m
|
|
40
|
+
[m
|
|
41
|
+
// OIDC Authentication Methods[m
|
|
42
|
+
[m
|
|
43
|
+
[32m+[m[32m /**[m
|
|
44
|
+
[32m+[m[32m * Sets the OIDC configuration to use for the callback and token exchange.[m
|
|
45
|
+
[32m+[m[32m * Call this before {@link handleOidcCallback} if the client was freshly[m
|
|
46
|
+
[32m+[m[32m * instantiated after a page reload (the config is otherwise only set[m
|
|
47
|
+
[32m+[m[32m * internally by {@link loginWithOidc} before the redirect).[m
|
|
48
|
+
[32m+[m[32m *[m
|
|
49
|
+
[32m+[m[32m * @param {I_OidcConfig} config - OIDC provider configuration.[m
|
|
50
|
+
[32m+[m[32m */[m
|
|
51
|
+
[32m+[m[32m setOidcConfig(config: I_OidcConfig): void {[m
|
|
52
|
+
[32m+[m[32m this.oidcConfig = config;[m
|
|
53
|
+
[32m+[m[32m }[m
|
|
54
|
+
[32m+[m
|
|
55
|
+
/**[m
|
|
56
|
+
* Initiates the OIDC authentication flow by redirecting to the authorization endpoint.[m
|
|
57
|
+
*[m
|
|
58
|
+
[36m@@ -373,11 +385,20 @@[m [mexport default class docPouchClient {[m
|
|
59
|
+
this.oidcState = this.generateState();[m
|
|
60
|
+
const codeChallenge = await this.generateCodeChallenge(this.codeVerifier!);[m
|
|
61
|
+
[m
|
|
62
|
+
[32m+[m[32m if (typeof window !== 'undefined' && window.sessionStorage) {[m
|
|
63
|
+
[32m+[m[32m sessionStorage.setItem('docpouch_oidc_state', this.oidcState!);[m
|
|
64
|
+
[32m+[m[32m sessionStorage.setItem('docpouch_oidc_code_verifier', this.codeVerifier!);[m
|
|
65
|
+
[32m+[m[32m sessionStorage.setItem('docpouch_oidc_issuer', config.issuer);[m
|
|
66
|
+
[32m+[m[32m sessionStorage.setItem('docpouch_oidc_client_id', config.clientId);[m
|
|
67
|
+
[32m+[m[32m sessionStorage.setItem('docpouch_oidc_redirect_uri', config.redirectUri);[m
|
|
68
|
+
[32m+[m[32m }[m
|
|
69
|
+
[32m+[m
|
|
70
|
+
[32m+[m[32m const scope = config.scope || config.scopes?.join(' ') || 'openid profile';[m
|
|
71
|
+
const params = new URLSearchParams({[m
|
|
72
|
+
response_type: 'code',[m
|
|
73
|
+
client_id: config.clientId,[m
|
|
74
|
+
redirect_uri: config.redirectUri,[m
|
|
75
|
+
[31m- scope: config.scopes?.join(' ') || 'openid profile',[m
|
|
76
|
+
[32m+[m[32m scope,[m
|
|
77
|
+
state: this.oidcState!,[m
|
|
78
|
+
code_challenge: codeChallenge,[m
|
|
79
|
+
code_challenge_method: 'S256'[m
|
|
80
|
+
[36m@@ -399,6 +420,26 @@[m [mexport default class docPouchClient {[m
|
|
81
|
+
[m
|
|
82
|
+
if (error) throw new Error(`OAuth error: ${error}`);[m
|
|
83
|
+
if (!code || !state) return false;[m
|
|
84
|
+
[32m+[m
|
|
85
|
+
[32m+[m[32m if (typeof window !== 'undefined' && window.sessionStorage) {[m
|
|
86
|
+
[32m+[m[32m this.oidcState = sessionStorage.getItem('docpouch_oidc_state') || this.oidcState;[m
|
|
87
|
+
[32m+[m[32m this.codeVerifier = sessionStorage.getItem('docpouch_oidc_code_verifier') || this.codeVerifier;[m
|
|
88
|
+
[32m+[m[32m sessionStorage.removeItem('docpouch_oidc_state');[m
|
|
89
|
+
[32m+[m[32m sessionStorage.removeItem('docpouch_oidc_code_verifier');[m
|
|
90
|
+
[32m+[m
|
|
91
|
+
[32m+[m[32m if (!this.oidcConfig) {[m
|
|
92
|
+
[32m+[m[32m const issuer = sessionStorage.getItem('docpouch_oidc_issuer');[m
|
|
93
|
+
[32m+[m[32m const clientId = sessionStorage.getItem('docpouch_oidc_client_id');[m
|
|
94
|
+
[32m+[m[32m const redirectUri = sessionStorage.getItem('docpouch_oidc_redirect_uri');[m
|
|
95
|
+
[32m+[m[32m if (issuer && clientId && redirectUri) {[m
|
|
96
|
+
[32m+[m[32m this.oidcConfig = { issuer, clientId, redirectUri };[m
|
|
97
|
+
[32m+[m[32m sessionStorage.removeItem('docpouch_oidc_issuer');[m
|
|
98
|
+
[32m+[m[32m sessionStorage.removeItem('docpouch_oidc_client_id');[m
|
|
99
|
+
[32m+[m[32m sessionStorage.removeItem('docpouch_oidc_redirect_uri');[m
|
|
100
|
+
[32m+[m[32m }[m
|
|
101
|
+
[32m+[m[32m }[m
|
|
102
|
+
[32m+[m[32m }[m
|
|
103
|
+
[32m+[m
|
|
104
|
+
if (state !== this.oidcState) throw new Error('State mismatch');[m
|
|
105
|
+
[m
|
|
106
|
+
const discovery = await this.discoverOidc();[m
|
|
107
|
+
[36m@@ -861,6 +902,7 @@[m [mexport interface I_OidcConfig {[m
|
|
108
|
+
issuer: string;[m
|
|
109
|
+
clientId: string;[m
|
|
110
|
+
redirectUri: string;[m
|
|
111
|
+
[32m+[m[32m scope?: string;[m
|
|
112
|
+
scopes?: string[];[m
|
|
113
|
+
clientSecret?: string;[m
|
|
114
|
+
}[m
|
|
@@ -11,7 +11,7 @@ module.exports = {
|
|
|
11
11
|
},
|
|
12
12
|
extensionsToTreatAsEsm: ['.ts'],
|
|
13
13
|
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
|
14
|
-
testMatch: ['**/__tests__/**/*.ts?(x)', '**/?(*.)+(spec|test).ts?(x)'],
|
|
14
|
+
testMatch: ['**/__tests__/**/*.ts?(x)', '**/?(*.)+(spec|test).ts?(x)', 'tests/**/*.test.ts'],
|
|
15
15
|
collectCoverage: true,
|
|
16
16
|
coverageDirectory: 'coverage',
|
|
17
17
|
coverageReporters: ['text', 'lcov'],
|
package/package.json
CHANGED
|
@@ -1,38 +1,41 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "docpouch-client",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"main": "dist/index.js",
|
|
5
|
-
"types": "dist/index.d.ts",
|
|
6
|
-
"exports": {
|
|
7
|
-
".": {
|
|
8
|
-
"import": "./dist/index.js",
|
|
9
|
-
"require": "./dist/index.js"
|
|
10
|
-
}
|
|
11
|
-
},
|
|
12
|
-
"type": "module",
|
|
13
|
-
"scripts": {
|
|
14
|
-
"build": "tsc",
|
|
15
|
-
"test": "jest"
|
|
16
|
-
},
|
|
17
|
-
"keywords": [
|
|
18
|
-
"docPouch",
|
|
19
|
-
"API"
|
|
20
|
-
],
|
|
21
|
-
"author": "Jan Frecè",
|
|
22
|
-
"license": "MIT",
|
|
23
|
-
"description": "
|
|
24
|
-
"repository": {
|
|
25
|
-
"type": "git",
|
|
26
|
-
"url": "https://github.com/BFH-JTF/docpouch-client"
|
|
27
|
-
},
|
|
28
|
-
"devDependencies": {
|
|
29
|
-
"@types/
|
|
30
|
-
"@types/
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"
|
|
37
|
-
}
|
|
38
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "docpouch-client",
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"import": "./dist/index.js",
|
|
9
|
+
"require": "./dist/index.js"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc -p tsconfig.build.json",
|
|
15
|
+
"test": "node --experimental-vm-modules --no-warnings ./node_modules/jest/bin/jest.js --forceExit --silent"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"docPouch",
|
|
19
|
+
"API"
|
|
20
|
+
],
|
|
21
|
+
"author": "Jan Frecè",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"description": "Client SDK for DocPouch API - supports JWT and OIDC authentication",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/BFH-JTF/docpouch-client"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/express": "^5.0.6",
|
|
30
|
+
"@types/jest": "^30.0.0",
|
|
31
|
+
"@types/node": "^25.7.0",
|
|
32
|
+
"express": "^5.2.1",
|
|
33
|
+
"jest": "^30.4.2",
|
|
34
|
+
"socket.io": "^4.8.3",
|
|
35
|
+
"ts-jest": "^29.4.9",
|
|
36
|
+
"typescript": "^6.0.3"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"socket.io-client": "^4.8.3"
|
|
40
|
+
}
|
|
41
|
+
}
|