@dloizides/auth-client 1.0.0 → 2.0.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/dist/index.js CHANGED
@@ -81,19 +81,89 @@ function parseBaseUrlFromIssuer(issuerUrl) {
81
81
  return issuerUrl.substring(0, idx).replace(/\/$/, "");
82
82
  }
83
83
 
84
+ // src/utils/normalizeTokenResponse.ts
85
+ function asString(value) {
86
+ return typeof value === "string" && value !== "" ? value : void 0;
87
+ }
88
+ function asNumber(value) {
89
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
90
+ }
91
+ function normalizeTokenResponse(raw) {
92
+ const accessToken = asString(raw.access_token);
93
+ if (accessToken === void 0) {
94
+ throw new Error("Token response missing access_token");
95
+ }
96
+ return {
97
+ accessToken,
98
+ refreshToken: asString(raw.refresh_token),
99
+ idToken: asString(raw.id_token),
100
+ expiresIn: asNumber(raw.expires_in),
101
+ tokenType: asString(raw.token_type),
102
+ scope: asString(raw.scope)
103
+ };
104
+ }
105
+ function tokenResponseToAuthTokens(response, now = Date.now()) {
106
+ return {
107
+ accessToken: response.accessToken,
108
+ refreshToken: response.refreshToken,
109
+ idToken: response.idToken,
110
+ expiresAt: computeExpiresAt(response.expiresIn, now)
111
+ };
112
+ }
113
+
114
+ // src/events/AuthEventEmitter.ts
115
+ var AuthEventEmitter = class {
116
+ constructor() {
117
+ this.listeners = /* @__PURE__ */ new Map();
118
+ }
119
+ on(event, listener) {
120
+ let bucket = this.listeners.get(event);
121
+ if (bucket === void 0) {
122
+ bucket = /* @__PURE__ */ new Set();
123
+ this.listeners.set(event, bucket);
124
+ }
125
+ bucket.add(listener);
126
+ return () => {
127
+ const current = this.listeners.get(event);
128
+ if (current !== void 0) {
129
+ current.delete(listener);
130
+ }
131
+ };
132
+ }
133
+ emit(event) {
134
+ const bucket = this.listeners.get(event);
135
+ if (bucket === void 0) {
136
+ return;
137
+ }
138
+ const snapshot = Array.from(bucket);
139
+ for (const listener of snapshot) {
140
+ listener();
141
+ }
142
+ }
143
+ /** Remove all listeners. Useful for `AuthClient.dispose()` and tests. */
144
+ clear() {
145
+ this.listeners.clear();
146
+ }
147
+ };
148
+
84
149
  // src/AuthClient.ts
85
150
  var DEFAULT_SCOPE = "openid profile email";
151
+ var OFFLINE_ACCESS_SCOPE = "offline_access";
86
152
  var AuthClient = class _AuthClient {
87
153
  /**
88
154
  * @throws Error when `baseUrl`, `realm`, or `clientId` is missing or empty.
89
155
  */
90
- constructor(config, storage) {
156
+ constructor(config, storage, collaborators = {}) {
91
157
  _AuthClient.validateConfig(config);
92
158
  this.config = {
93
159
  ...config,
94
160
  scope: config.scope ?? DEFAULT_SCOPE
95
161
  };
96
162
  this.tokenStorage = storage;
163
+ this.api = collaborators.api;
164
+ this.interceptor = collaborators.interceptor;
165
+ this.inactivityTracker = collaborators.inactivityTracker;
166
+ this.events = collaborators.events ?? new AuthEventEmitter();
97
167
  }
98
168
  /**
99
169
  * Build an {@link AuthClient} from a standalone issuer URL by parsing the
@@ -102,7 +172,7 @@ var AuthClient = class _AuthClient {
102
172
  *
103
173
  * @throws Error when the issuer URL doesn't match `{base}/realms/{realm}`.
104
174
  */
105
- static fromIssuerUrl(input, storage) {
175
+ static fromIssuerUrl(input, storage, collaborators = {}) {
106
176
  const realm = parseRealmFromIssuer(input.issuerUrl);
107
177
  const baseUrl = parseBaseUrlFromIssuer(input.issuerUrl);
108
178
  if (realm === null || baseUrl === null || baseUrl === "") {
@@ -116,7 +186,8 @@ var AuthClient = class _AuthClient {
116
186
  redirectUri: input.redirectUri,
117
187
  scope: input.scope
118
188
  },
119
- storage
189
+ storage,
190
+ collaborators
120
191
  );
121
192
  }
122
193
  static validateConfig(config) {
@@ -175,7 +246,7 @@ var AuthClient = class _AuthClient {
175
246
  realm: this.realm,
176
247
  clientId: this.clientId,
177
248
  redirectUri: this.config.redirectUri,
178
- scope: this.scope,
249
+ scope: this.resolveScope(input.offlineAccess),
179
250
  state: input.state,
180
251
  codeChallenge: input.codeChallenge,
181
252
  codeChallengeMethod: input.codeChallengeMethod
@@ -204,6 +275,103 @@ var AuthClient = class _AuthClient {
204
275
  }
205
276
  return tokens.accessToken;
206
277
  }
278
+ /** Subscribe to lifecycle events (currently `sessionExpired` only). */
279
+ on(event, listener) {
280
+ return this.events.on(event, listener);
281
+ }
282
+ /**
283
+ * Boot-time wiring. Checks the inactivity tracker; if expired, clears
284
+ * tokens and emits `sessionExpired`. Returns whether a usable session
285
+ * survived.
286
+ */
287
+ async init() {
288
+ if (this.inactivityTracker !== void 0) {
289
+ const expired = await this.inactivityTracker.isExpired();
290
+ if (expired) {
291
+ await this.tokenStorage.clear();
292
+ await this.inactivityTracker.clear();
293
+ this.events.emit("sessionExpired");
294
+ return { hasSession: false };
295
+ }
296
+ }
297
+ const tokens = await this.tokenStorage.read();
298
+ return { hasSession: tokens !== null };
299
+ }
300
+ /**
301
+ * Trigger a refresh via the configured interceptor. Returns the new tokens
302
+ * or `null` when the refresh failed (in which case `sessionExpired` has
303
+ * already fired).
304
+ *
305
+ * @throws Error when no interceptor is configured.
306
+ */
307
+ async refresh() {
308
+ if (this.interceptor === void 0) {
309
+ throw new Error("AuthClient.refresh: no RefreshInterceptor configured");
310
+ }
311
+ return this.interceptor.refreshTokens();
312
+ }
313
+ async loginWithOtp(input) {
314
+ return this.runLogin(this.requireApi().loginWithOtp({
315
+ email: input.email,
316
+ otp: input.otp,
317
+ tenantId: input.tenantId,
318
+ offlineAccess: input.offlineAccess ?? false
319
+ }));
320
+ }
321
+ async loginWithPassword(input) {
322
+ return this.runLogin(this.requireApi().loginWithPassword({
323
+ email: input.email,
324
+ password: input.password,
325
+ tenantId: input.tenantId,
326
+ offlineAccess: input.offlineAccess ?? false
327
+ }));
328
+ }
329
+ async logout(options = {}) {
330
+ const api = this.requireApi();
331
+ try {
332
+ await api.logout(options.everywhere ?? false);
333
+ } finally {
334
+ await this.tokenStorage.clear();
335
+ if (this.inactivityTracker !== void 0) {
336
+ await this.inactivityTracker.clear();
337
+ }
338
+ }
339
+ }
340
+ async requestPasswordReset(input) {
341
+ return this.requireApi().forgotPassword({ email: input.email, tenantId: input.tenantId });
342
+ }
343
+ async confirmPasswordReset(input) {
344
+ return this.requireApi().resetPassword({ token: input.token, newPassword: input.newPassword });
345
+ }
346
+ /** Internal: run a login HTTP call, persist tokens, mark inactivity-active. */
347
+ async runLogin(promise) {
348
+ const raw = await promise;
349
+ if (typeof raw.access_token !== "string" || raw.access_token === "") {
350
+ throw new Error("AuthClient: login response missing access_token");
351
+ }
352
+ const normalized = normalizeTokenResponse({ ...raw, access_token: raw.access_token });
353
+ const tokens = tokenResponseToAuthTokens(normalized);
354
+ await this.tokenStorage.write(tokens);
355
+ if (this.inactivityTracker !== void 0) {
356
+ await this.inactivityTracker.markActive();
357
+ }
358
+ return tokens;
359
+ }
360
+ requireApi() {
361
+ if (this.api === void 0) {
362
+ throw new Error("AuthClient: no AuthApiClient configured");
363
+ }
364
+ return this.api;
365
+ }
366
+ resolveScope(offlineAccess) {
367
+ if (offlineAccess !== true) {
368
+ return this.scope;
369
+ }
370
+ if (this.scope.includes(OFFLINE_ACCESS_SCOPE)) {
371
+ return this.scope;
372
+ }
373
+ return `${this.scope} ${OFFLINE_ACCESS_SCOPE}`.trim();
374
+ }
207
375
  };
208
376
 
209
377
  // src/types/KeycloakRoles.ts
@@ -279,6 +447,413 @@ var InMemoryTokenStorage = class {
279
447
  }
280
448
  };
281
449
 
450
+ // src/storage/CookieTokenStorage.ts
451
+ var CookieTokenStorage = class {
452
+ constructor() {
453
+ this.accessToken = null;
454
+ this.idToken = void 0;
455
+ this.expiresAt = 0;
456
+ }
457
+ read() {
458
+ if (this.accessToken === null) {
459
+ return Promise.resolve(null);
460
+ }
461
+ const tokens = {
462
+ accessToken: this.accessToken,
463
+ idToken: this.idToken,
464
+ expiresAt: this.expiresAt
465
+ };
466
+ return Promise.resolve(tokens);
467
+ }
468
+ write(tokens) {
469
+ this.accessToken = tokens.accessToken;
470
+ this.idToken = tokens.idToken;
471
+ this.expiresAt = tokens.expiresAt;
472
+ return Promise.resolve();
473
+ }
474
+ clear() {
475
+ this.accessToken = null;
476
+ this.idToken = void 0;
477
+ this.expiresAt = 0;
478
+ return Promise.resolve();
479
+ }
480
+ };
481
+
482
+ // src/storage/SecureStoreTokenStorage.ts
483
+ var DEFAULT_PREFIX = "auth";
484
+ var ACCESS_KEY = "access";
485
+ var REFRESH_KEY = "refresh";
486
+ var ID_KEY = "id";
487
+ var EXPIRES_KEY = "expiresAt";
488
+ var SecureStoreTokenStorage = class {
489
+ constructor(options) {
490
+ this.secureStore = options.secureStore;
491
+ this.prefix = options.keyPrefix ?? DEFAULT_PREFIX;
492
+ this.requireAuthentication = options.requireAuthentication ?? false;
493
+ this.biometricGate = options.biometricGate;
494
+ }
495
+ async read() {
496
+ if (this.shouldRunBiometricGate()) {
497
+ await this.biometricGate.unlock();
498
+ }
499
+ const readOptions = this.requireAuthentication ? { requireAuthentication: true } : void 0;
500
+ const accessToken = await this.secureStore.getItemAsync(this.fullKey(ACCESS_KEY), readOptions);
501
+ if (accessToken === null) {
502
+ return null;
503
+ }
504
+ const refreshTokenRaw = await this.secureStore.getItemAsync(this.fullKey(REFRESH_KEY), readOptions);
505
+ const idTokenRaw = await this.secureStore.getItemAsync(this.fullKey(ID_KEY));
506
+ const expiresAtRaw = await this.secureStore.getItemAsync(this.fullKey(EXPIRES_KEY));
507
+ const expiresAt = parseExpiresAt(expiresAtRaw);
508
+ return {
509
+ accessToken,
510
+ refreshToken: refreshTokenRaw === null ? void 0 : refreshTokenRaw,
511
+ idToken: idTokenRaw === null ? void 0 : idTokenRaw,
512
+ expiresAt
513
+ };
514
+ }
515
+ async write(tokens) {
516
+ await this.secureStore.setItemAsync(this.fullKey(ACCESS_KEY), tokens.accessToken);
517
+ if (tokens.refreshToken !== void 0 && tokens.refreshToken !== "") {
518
+ await this.secureStore.setItemAsync(this.fullKey(REFRESH_KEY), tokens.refreshToken);
519
+ } else {
520
+ await this.secureStore.deleteItemAsync(this.fullKey(REFRESH_KEY));
521
+ }
522
+ if (tokens.idToken !== void 0 && tokens.idToken !== "") {
523
+ await this.secureStore.setItemAsync(this.fullKey(ID_KEY), tokens.idToken);
524
+ } else {
525
+ await this.secureStore.deleteItemAsync(this.fullKey(ID_KEY));
526
+ }
527
+ await this.secureStore.setItemAsync(this.fullKey(EXPIRES_KEY), String(tokens.expiresAt));
528
+ }
529
+ async clear() {
530
+ await this.secureStore.deleteItemAsync(this.fullKey(ACCESS_KEY));
531
+ await this.secureStore.deleteItemAsync(this.fullKey(REFRESH_KEY));
532
+ await this.secureStore.deleteItemAsync(this.fullKey(ID_KEY));
533
+ await this.secureStore.deleteItemAsync(this.fullKey(EXPIRES_KEY));
534
+ }
535
+ shouldRunBiometricGate() {
536
+ return this.biometricGate !== void 0 && this.biometricGate.isEnabled();
537
+ }
538
+ fullKey(slot) {
539
+ return `${this.prefix}.${slot}`;
540
+ }
541
+ };
542
+ function parseExpiresAt(raw) {
543
+ if (raw === null || raw === "") {
544
+ return 0;
545
+ }
546
+ const parsed = Number(raw);
547
+ return Number.isFinite(parsed) ? parsed : 0;
548
+ }
549
+
550
+ // src/biometric/BiometricGate.ts
551
+ var DEFAULT_PROMPT = "Unlock to continue";
552
+ var DEFAULT_MAX_FAILURES = 3;
553
+ var BiometricGate = class {
554
+ constructor(options) {
555
+ this.enabled = false;
556
+ this.failureCount = 0;
557
+ this.hydrated = false;
558
+ this.localAuth = options.localAuth;
559
+ this.flagStore = options.flagStore;
560
+ this.promptMessage = options.promptMessage ?? DEFAULT_PROMPT;
561
+ this.maxFailures = options.maxFailures ?? DEFAULT_MAX_FAILURES;
562
+ }
563
+ /** Hardware present AND a fingerprint/face ID is enrolled. */
564
+ async isAvailable() {
565
+ const hasHardware = await this.localAuth.hasHardwareAsync();
566
+ if (!hasHardware) {
567
+ return false;
568
+ }
569
+ return this.localAuth.isEnrolledAsync();
570
+ }
571
+ /** Synchronous read of the current enabled flag (post-hydration). */
572
+ isEnabled() {
573
+ return this.enabled;
574
+ }
575
+ /** Read the persisted opt-in flag once at app boot. Idempotent. */
576
+ async hydrate() {
577
+ if (this.hydrated) {
578
+ return;
579
+ }
580
+ this.hydrated = true;
581
+ if (this.flagStore !== void 0) {
582
+ this.enabled = await this.flagStore.read();
583
+ }
584
+ }
585
+ /**
586
+ * Toggle biometric requirement. Persists via {@link BiometricFlagStore} when
587
+ * configured. Resets the failure counter so a re-enable starts fresh.
588
+ */
589
+ async setEnabled(enabled) {
590
+ this.enabled = enabled;
591
+ this.failureCount = 0;
592
+ if (this.flagStore !== void 0) {
593
+ await this.flagStore.write(enabled);
594
+ }
595
+ }
596
+ /** Reset the failure counter. Tests + consumer recovery flows. */
597
+ resetFailures() {
598
+ this.failureCount = 0;
599
+ }
600
+ /**
601
+ * One-shot biometric prompt. Returns `true` on success. Does NOT throw on
602
+ * failure or update the failure counter — useful for action confirmation.
603
+ */
604
+ async prompt() {
605
+ const result = await this.localAuth.authenticateAsync({
606
+ promptMessage: this.promptMessage
607
+ });
608
+ return result.success;
609
+ }
610
+ /**
611
+ * Required pre-condition for sensitive token reads. No-op when disabled.
612
+ *
613
+ * @throws Error after {@link maxFailures} consecutive failures.
614
+ * @throws Error on a single failure (lower in the count, but still throws so
615
+ * `SecureStoreTokenStorage.read()` short-circuits).
616
+ */
617
+ async unlock() {
618
+ if (!this.enabled) {
619
+ return;
620
+ }
621
+ const result = await this.localAuth.authenticateAsync({
622
+ promptMessage: this.promptMessage
623
+ });
624
+ if (result.success) {
625
+ this.failureCount = 0;
626
+ return;
627
+ }
628
+ this.failureCount += 1;
629
+ if (this.failureCount >= this.maxFailures) {
630
+ throw new Error("Biometric authentication failed; locked out");
631
+ }
632
+ throw new Error("Biometric authentication failed");
633
+ }
634
+ };
635
+
636
+ // src/interceptor/RefreshInterceptor.ts
637
+ var RefreshInterceptor = class {
638
+ constructor(options) {
639
+ this.inflight = null;
640
+ this.storage = options.storage;
641
+ this.refresh = options.refresh;
642
+ this.events = options.events;
643
+ this.onRefreshSuccess = options.onRefreshSuccess;
644
+ }
645
+ /**
646
+ * Trigger (or join) a refresh. Returns the new tokens, or `null` if the
647
+ * refresh failed — in which case storage has already been cleared and
648
+ * `sessionExpired` already fired.
649
+ */
650
+ async refreshTokens() {
651
+ if (this.inflight !== null) {
652
+ return this.inflight;
653
+ }
654
+ this.inflight = this.runRefresh();
655
+ try {
656
+ return await this.inflight;
657
+ } finally {
658
+ this.inflight = null;
659
+ }
660
+ }
661
+ /**
662
+ * Whether a refresh is currently in flight. Exposed for tests / debug.
663
+ */
664
+ get isRefreshing() {
665
+ return this.inflight !== null;
666
+ }
667
+ async runRefresh() {
668
+ const current = await this.storage.read();
669
+ let next;
670
+ try {
671
+ next = await this.refresh(current);
672
+ } catch {
673
+ await this.failHard();
674
+ return null;
675
+ }
676
+ if (next === null) {
677
+ await this.failHard();
678
+ return null;
679
+ }
680
+ await this.storage.write(next);
681
+ if (this.onRefreshSuccess !== void 0) {
682
+ await this.onRefreshSuccess(next);
683
+ }
684
+ return next;
685
+ }
686
+ async failHard() {
687
+ await this.storage.clear();
688
+ this.events.emit("sessionExpired");
689
+ }
690
+ };
691
+
692
+ // src/inactivity/InactivityTracker.ts
693
+ var DEFAULT_MAX_DAYS = 90;
694
+ var MS_PER_DAY = 24 * 60 * 60 * 1e3;
695
+ var InactivityTracker = class {
696
+ constructor(options) {
697
+ this.store = options.store;
698
+ const days = options.maxInactivityDays ?? DEFAULT_MAX_DAYS;
699
+ this.maxInactivityMs = days * MS_PER_DAY;
700
+ this.now = options.now ?? Date.now;
701
+ }
702
+ async markActive(timestamp) {
703
+ await this.store.write(timestamp ?? this.now());
704
+ }
705
+ async getLastActive() {
706
+ return this.store.read();
707
+ }
708
+ async isExpired() {
709
+ const last = await this.store.read();
710
+ if (last === null) {
711
+ return false;
712
+ }
713
+ return this.now() - last > this.maxInactivityMs;
714
+ }
715
+ async clear() {
716
+ await this.store.clear();
717
+ }
718
+ };
719
+
720
+ // src/http/HttpClient.ts
721
+ function createFetchHttpClient(fetchImpl) {
722
+ return async (request) => {
723
+ const init = {
724
+ method: request.method,
725
+ headers: request.headers,
726
+ body: request.body
727
+ };
728
+ if (request.credentials !== void 0) {
729
+ init.credentials = request.credentials;
730
+ }
731
+ const response = await fetchImpl(request.url, init);
732
+ let data;
733
+ if (response.status !== 204) {
734
+ const contentType = response.headers.get("content-type") ?? "";
735
+ if (contentType.includes("application/json")) {
736
+ data = await response.json();
737
+ }
738
+ }
739
+ return {
740
+ status: response.status,
741
+ ok: response.ok,
742
+ data
743
+ };
744
+ };
745
+ }
746
+
747
+ // src/api/AuthApiClient.ts
748
+ var AuthApiClient = class {
749
+ constructor(options) {
750
+ this.http = options.http;
751
+ this.baseUrl = options.baseUrl.replace(/\/$/, "");
752
+ this.getAccessToken = options.getAccessToken;
753
+ this.useCredentials = options.useCredentials ?? false;
754
+ }
755
+ loginWithOtp(request) {
756
+ return this.postLogin("/auth/verify-otp", request);
757
+ }
758
+ loginWithPassword(request) {
759
+ return this.postLogin("/auth/login", request);
760
+ }
761
+ /** Web cookie-flow refresh. Sends no body; cookie travels via `credentials`. */
762
+ async refreshCookie() {
763
+ const response = await this.http({
764
+ url: `${this.baseUrl}/auth/refresh-cookie`,
765
+ method: "POST",
766
+ headers: { "content-type": "application/json" },
767
+ credentials: this.useCredentials ? "include" : void 0
768
+ });
769
+ if (!response.ok) {
770
+ throw new Error(`refresh-cookie failed with status ${response.status}`);
771
+ }
772
+ return response.data ?? {};
773
+ }
774
+ async logout(everywhere = false) {
775
+ const url = everywhere ? `${this.baseUrl}/auth/logout?everywhere=true` : `${this.baseUrl}/auth/logout`;
776
+ const response = await this.http({
777
+ url,
778
+ method: "POST",
779
+ headers: await this.authHeaders(),
780
+ credentials: this.useCredentials ? "include" : void 0
781
+ });
782
+ if (!response.ok) {
783
+ throw new Error(`logout failed with status ${response.status}`);
784
+ }
785
+ }
786
+ async forgotPassword(request) {
787
+ const response = await this.http({
788
+ url: `${this.baseUrl}/auth/forgot-password`,
789
+ method: "POST",
790
+ headers: { "content-type": "application/json" },
791
+ body: JSON.stringify(request)
792
+ });
793
+ if (!response.ok) {
794
+ throw new Error(`forgot-password failed with status ${response.status}`);
795
+ }
796
+ }
797
+ async resetPassword(request) {
798
+ const response = await this.http({
799
+ url: `${this.baseUrl}/auth/reset-password`,
800
+ method: "POST",
801
+ headers: { "content-type": "application/json" },
802
+ body: JSON.stringify(request)
803
+ });
804
+ if (!response.ok) {
805
+ throw new Error(`reset-password failed with status ${response.status}`);
806
+ }
807
+ }
808
+ async listSessions() {
809
+ const response = await this.http({
810
+ url: `${this.baseUrl}/me/sessions`,
811
+ method: "GET",
812
+ headers: await this.authHeaders(),
813
+ credentials: this.useCredentials ? "include" : void 0
814
+ });
815
+ if (!response.ok) {
816
+ throw new Error(`listSessions failed with status ${response.status}`);
817
+ }
818
+ return Array.isArray(response.data) ? response.data : [];
819
+ }
820
+ async revokeSession(sessionId) {
821
+ const response = await this.http({
822
+ url: `${this.baseUrl}/me/sessions/${encodeURIComponent(sessionId)}/revoke`,
823
+ method: "POST",
824
+ headers: await this.authHeaders(),
825
+ credentials: this.useCredentials ? "include" : void 0
826
+ });
827
+ if (!response.ok) {
828
+ throw new Error(`revokeSession failed with status ${response.status}`);
829
+ }
830
+ }
831
+ async postLogin(path, body) {
832
+ const response = await this.http({
833
+ url: `${this.baseUrl}${path}`,
834
+ method: "POST",
835
+ headers: { "content-type": "application/json" },
836
+ body: JSON.stringify(body),
837
+ credentials: this.useCredentials ? "include" : void 0
838
+ });
839
+ if (!response.ok) {
840
+ throw new Error(`login failed with status ${response.status}`);
841
+ }
842
+ return response.data ?? {};
843
+ }
844
+ async authHeaders() {
845
+ const headers = { "content-type": "application/json" };
846
+ if (this.getAccessToken === void 0) {
847
+ return headers;
848
+ }
849
+ const token = await this.getAccessToken();
850
+ if (token !== null && token !== "") {
851
+ headers.authorization = `Bearer ${token}`;
852
+ }
853
+ return headers;
854
+ }
855
+ };
856
+
282
857
  // src/utils/normalizeKeycloakUser.ts
283
858
  function isNonEmptyString(value) {
284
859
  return typeof value === "string" && value !== "";
@@ -407,40 +982,17 @@ function decodeUtf8(binary) {
407
982
  return new TextDecoder("utf-8").decode(bytes);
408
983
  }
409
984
 
410
- // src/utils/normalizeTokenResponse.ts
411
- function asString(value) {
412
- return typeof value === "string" && value !== "" ? value : void 0;
413
- }
414
- function asNumber(value) {
415
- return typeof value === "number" && Number.isFinite(value) ? value : void 0;
416
- }
417
- function normalizeTokenResponse(raw) {
418
- const accessToken = asString(raw.access_token);
419
- if (accessToken === void 0) {
420
- throw new Error("Token response missing access_token");
421
- }
422
- return {
423
- accessToken,
424
- refreshToken: asString(raw.refresh_token),
425
- idToken: asString(raw.id_token),
426
- expiresIn: asNumber(raw.expires_in),
427
- tokenType: asString(raw.token_type),
428
- scope: asString(raw.scope)
429
- };
430
- }
431
- function tokenResponseToAuthTokens(response, now = Date.now()) {
432
- return {
433
- accessToken: response.accessToken,
434
- refreshToken: response.refreshToken,
435
- idToken: response.idToken,
436
- expiresAt: computeExpiresAt(response.expiresIn, now)
437
- };
438
- }
439
-
985
+ exports.AuthApiClient = AuthApiClient;
440
986
  exports.AuthClient = AuthClient;
987
+ exports.AuthEventEmitter = AuthEventEmitter;
988
+ exports.BiometricGate = BiometricGate;
441
989
  exports.BrowserStorageTokenStorage = BrowserStorageTokenStorage;
990
+ exports.CookieTokenStorage = CookieTokenStorage;
442
991
  exports.InMemoryTokenStorage = InMemoryTokenStorage;
992
+ exports.InactivityTracker = InactivityTracker;
443
993
  exports.KeycloakRoles = KeycloakRoles;
994
+ exports.RefreshInterceptor = RefreshInterceptor;
995
+ exports.SecureStoreTokenStorage = SecureStoreTokenStorage;
444
996
  exports.buildAuthorizationCodeBody = buildAuthorizationCodeBody;
445
997
  exports.buildAuthorizationEndpoint = buildAuthorizationEndpoint;
446
998
  exports.buildAuthorizationUrl = buildAuthorizationUrl;
@@ -450,6 +1002,7 @@ exports.buildRefreshTokenBody = buildRefreshTokenBody;
450
1002
  exports.buildTokenEndpoint = buildTokenEndpoint;
451
1003
  exports.buildUserInfoEndpoint = buildUserInfoEndpoint;
452
1004
  exports.computeExpiresAt = computeExpiresAt;
1005
+ exports.createFetchHttpClient = createFetchHttpClient;
453
1006
  exports.decodeJwt = decodeJwt;
454
1007
  exports.extractAuthCode = extractAuthCode;
455
1008
  exports.isKeycloakRole = isKeycloakRole;