@tstdl/base 0.93.188 → 0.93.190

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.
@@ -15,8 +15,9 @@ export function vertexAiMultiLocation(options) {
15
15
  const tokenLimitThreshold = options.tokenLimitThreshold ?? defaultTokenLimitThreshold;
16
16
  const locationConfigs = options.locations.map((location) => {
17
17
  const circuitBreakerNamespace = `genkit:vertex-ai:location`;
18
+ const tokenLimitCircuitBreakerNamespace = `genkit:vertex-ai:location-token-limit`;
18
19
  const resource = location;
19
- const tokenLimitResource = `${location}:token-limit`;
20
+ const tokenLimitResource = location;
20
21
  return {
21
22
  location,
22
23
  resource,
@@ -26,7 +27,7 @@ export function vertexAiMultiLocation(options) {
26
27
  resetTimeout: 30 * millisecondsPerSecond,
27
28
  ...options.circuitBreakerConfig,
28
29
  }),
29
- tokenLimitCircuitBreaker: options.circuitBreakerProvider.provide(circuitBreakerNamespace, {
30
+ tokenLimitCircuitBreaker: options.circuitBreakerProvider.provide(tokenLimitCircuitBreakerNamespace, {
30
31
  threshold: 1,
31
32
  resetTimeout: 15 * millisecondsPerMinute,
32
33
  ...options.tokenLimitCircuitBreakerConfig,
@@ -27,6 +27,7 @@ export declare class AuthenticationClientService<AdditionalTokenPayload extends
27
27
  private readonly logger;
28
28
  private readonly disposeSignal;
29
29
  private readonly forceRefreshRequested;
30
+ private readonly totpStatusReloadNotifier;
30
31
  private readonly forceRefreshSubject;
31
32
  private clockOffset;
32
33
  private initialized;
@@ -193,6 +194,10 @@ export declare class AuthenticationClientService<AdditionalTokenPayload extends
193
194
  * @param newPassword The new password
194
195
  */
195
196
  resetPassword(token: string, newPassword: string): Promise<void>;
197
+ /**
198
+ * Reload TOTP status.
199
+ */
200
+ reloadTotpStatus(): void;
196
201
  /**
197
202
  * Initiate TOTP enrollment.
198
203
  * @returns The secret and URI for enrollment.
@@ -7,7 +7,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  var __metadata = (this && this.__metadata) || function (k, v) {
8
8
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
9
  };
10
- import { Subject, filter, firstValueFrom, from, race, takeUntil, tap, timer } from 'rxjs';
10
+ import { Subject, filter, firstValueFrom, from, race, takeUntil, timer } from 'rxjs';
11
11
  import { CancellationSignal } from '../../cancellation/token.js';
12
12
  import { BadRequestError } from '../../errors/bad-request.error.js';
13
13
  import { ForbiddenError } from '../../errors/forbidden.error.js';
@@ -22,7 +22,7 @@ import { Lock } from '../../lock/index.js';
22
22
  import { Logger } from '../../logger/index.js';
23
23
  import { MessageBus } from '../../message-bus/index.js';
24
24
  import { computed, signal, toObservable } from '../../signals/api.js';
25
- import { deriveAsync } from '../../signals/index.js';
25
+ import { createNotifier, deriveAsync } from '../../signals/index.js';
26
26
  import { currentTimestampSeconds } from '../../utils/date-time.js';
27
27
  import { clamp } from '../../utils/math.js';
28
28
  import { timeout } from '../../utils/timing.js';
@@ -74,6 +74,7 @@ let AuthenticationClientService = class AuthenticationClientService {
74
74
  logger = inject(Logger, 'AuthenticationService');
75
75
  disposeSignal = inject(CancellationSignal).fork();
76
76
  forceRefreshRequested = signal(false);
77
+ totpStatusReloadNotifier = createNotifier();
77
78
  forceRefreshSubject = new Subject();
78
79
  clockOffset = 0;
79
80
  initialized = false;
@@ -107,6 +108,7 @@ let AuthenticationClientService = class AuthenticationClientService {
107
108
  /** Whether the user is impersonated */
108
109
  impersonated = computed(() => isDefined(this.impersonator()));
109
110
  totpStatus = deriveAsync(async () => {
111
+ this.totpStatusReloadNotifier.listen();
110
112
  const token = this.token();
111
113
  if (isUndefined(token)) {
112
114
  return undefined;
@@ -403,6 +405,12 @@ let AuthenticationClientService = class AuthenticationClientService {
403
405
  async resetPassword(token, newPassword) {
404
406
  await this.client.resetPassword({ token, newPassword });
405
407
  }
408
+ /**
409
+ * Reload TOTP status.
410
+ */
411
+ reloadTotpStatus() {
412
+ this.totpStatusReloadNotifier.notify();
413
+ }
406
414
  /**
407
415
  * Initiate TOTP enrollment.
408
416
  * @returns The secret and URI for enrollment.
@@ -416,7 +424,9 @@ let AuthenticationClientService = class AuthenticationClientService {
416
424
  * @returns The recovery codes.
417
425
  */
418
426
  async completeEnrollTotp(token) {
419
- return await this.client.completeEnrollTotp({ token });
427
+ const result = await this.client.completeEnrollTotp({ token });
428
+ this.reloadTotpStatus();
429
+ return result;
420
430
  }
421
431
  /**
422
432
  * Disable TOTP.
@@ -424,6 +434,7 @@ let AuthenticationClientService = class AuthenticationClientService {
424
434
  */
425
435
  async disableTotp(token) {
426
436
  await this.client.disableTotp({ token });
437
+ this.reloadTotpStatus();
427
438
  }
428
439
  /**
429
440
  * Disable TOTP using a recovery code.
@@ -431,6 +442,7 @@ let AuthenticationClientService = class AuthenticationClientService {
431
442
  */
432
443
  async disableTotpWithRecoveryCode(recoveryCode) {
433
444
  await this.client.disableTotpWithRecoveryCode({ recoveryCode });
445
+ this.reloadTotpStatus();
434
446
  }
435
447
  /**
436
448
  * Regenerate recovery codes.
@@ -439,7 +451,9 @@ let AuthenticationClientService = class AuthenticationClientService {
439
451
  * @returns The new recovery codes.
440
452
  */
441
453
  async regenerateRecoveryCodes(token, options) {
442
- return await this.client.regenerateRecoveryCodes({ token, ...options });
454
+ const result = await this.client.regenerateRecoveryCodes({ token, ...options });
455
+ this.reloadTotpStatus();
456
+ return result;
443
457
  }
444
458
  /**
445
459
  * Get TOTP activation status.
@@ -500,29 +514,23 @@ let AuthenticationClientService = class AuthenticationClientService {
500
514
  this.tokenUpdateBus.publishAndForget(token);
501
515
  }
502
516
  async refreshLoop() {
503
- this.logger.trace('[refreshLoop] Starting refresh loop...');
504
517
  if (this.isLoggedIn()) {
505
- this.logger.trace('[refreshLoop] User is logged in, syncing clock...');
506
518
  await this.syncClock();
507
519
  }
508
520
  // Helper to sleep until the delay passes OR a vital state change occurs
509
- const waitForNextAction = async (delayMs) => {
510
- this.logger.trace(`[waitForNextAction] Waiting ${delayMs}ms for next action...`);
511
- return await firstValueFrom(race([
512
- timer(Math.max(10, delayMs)).pipe(tap(() => this.logger.trace('[waitForNextAction] Timer expired'))),
513
- from(this.disposeSignal).pipe(tap(() => this.logger.trace('[waitForNextAction] Dispose signal received'))),
514
- this.tokenUpdateBus.allMessages$.pipe(tap(() => this.logger.trace('[waitForNextAction] Token update message received'))),
515
- this.forceRefreshSubject.pipe(tap(() => this.logger.trace('[waitForNextAction] Force refresh requested'))),
516
- ]), { defaultValue: undefined });
517
- };
521
+ const waitForNextAction = async (delayMs) => await firstValueFrom(race([
522
+ timer(Math.max(10, delayMs)),
523
+ from(this.disposeSignal),
524
+ this.tokenUpdateBus.allMessages$,
525
+ this.forceRefreshSubject,
526
+ ]), { defaultValue: undefined });
518
527
  while (this.disposeSignal.isUnset) {
519
528
  const token = this.token();
520
529
  // 1. Wait for login/token if none is present
521
530
  if (isUndefined(token)) {
522
- this.logger.trace('[refreshLoop] No token found, waiting for login...');
523
531
  await firstValueFrom(race([
524
- this.definedToken$.pipe(tap(() => this.logger.trace('[refreshLoop] Token became defined'))),
525
- from(this.disposeSignal).pipe(tap(() => this.logger.trace('[refreshLoop] Dispose signal received'))),
532
+ this.tokenUpdateBus.allMessages$.pipe(filter(isDefined)),
533
+ from(this.disposeSignal),
526
534
  ]), { defaultValue: undefined });
527
535
  continue;
528
536
  }
@@ -530,16 +538,12 @@ let AuthenticationClientService = class AuthenticationClientService {
530
538
  const now = this.estimatedServerTimestampSeconds();
531
539
  const buffer = calculateRefreshBufferSeconds(token);
532
540
  const needsRefresh = this.forceRefreshRequested() || (now >= token.exp - buffer);
533
- this.logger.trace(`[refreshLoop] Checking token refresh status: now=${now}, exp=${token.exp}, buffer=${buffer}, needsRefresh=${needsRefresh}, force=${this.forceRefreshRequested()}`);
534
541
  // 2. Handle token refresh
535
542
  if (needsRefresh) {
536
- this.logger.trace('[refreshLoop] Attempting to acquire refresh lock...');
537
543
  const lockResult = await this.lock.tryUse(undefined, async () => {
538
- this.logger.trace('[refreshLoop] Refresh lock acquired, evaluating if refresh is still needed...');
539
544
  this.loadToken();
540
545
  const currentToken = this.token();
541
546
  if (isUndefined(currentToken)) {
542
- this.logger.trace('[refreshLoop] Token became undefined while waiting for lock, skipping refresh.');
543
547
  return;
544
548
  }
545
549
  // Passive Sync: Verify if refresh is still needed once lock is acquired
@@ -547,24 +551,13 @@ let AuthenticationClientService = class AuthenticationClientService {
547
551
  const currentBuffer = calculateRefreshBufferSeconds(currentToken);
548
552
  const stillNeedsRefresh = this.forceRefreshRequested() || (currentNow >= currentToken.exp - currentBuffer);
549
553
  if (stillNeedsRefresh) {
550
- this.logger.trace('[refreshLoop] Refresh is still needed, performing refresh...');
551
554
  await this.refresh();
552
555
  this.forceRefreshRequested.set(false);
553
- this.logger.trace('[refreshLoop] Refresh completed successfully.');
554
- }
555
- else {
556
- this.logger.trace('[refreshLoop] Token already refreshed by another instance, skipping.');
557
556
  }
558
557
  });
559
558
  // If lock is held by another instance/tab, wait briefly for it to finish (passive sync)
560
559
  if (!lockResult.success) {
561
- this.logger.trace('[refreshLoop] Refresh lock held by another instance, waiting 5000ms for passive sync...');
562
560
  await waitForNextAction(5000);
563
- // If another tab successfully refreshed, the expiration will have changed
564
- if (this.token()?.exp !== token.exp) {
565
- this.logger.trace('[refreshLoop] Token refreshed by another instance (expiration changed), clearing force refresh flag.');
566
- this.forceRefreshRequested.set(false);
567
- }
568
561
  }
569
562
  // Protection against tight loops (e.g. if server clock is ahead and sync failed)
570
563
  const newToken = this.token();
@@ -573,11 +566,10 @@ let AuthenticationClientService = class AuthenticationClientService {
573
566
  const newBuffer = calculateRefreshBufferSeconds(newToken);
574
567
  const stillNeedsRefresh = this.forceRefreshRequested() || (newNow >= newToken.exp - newBuffer);
575
568
  if (stillNeedsRefresh) {
576
- this.logger.warn('[refreshLoop] Token still needs refresh after attempt. Waiting briefly to avoid tight loop.');
569
+ this.logger.warn('Token still needs refresh after attempt. Waiting briefly to avoid tight loop.');
577
570
  await waitForNextAction(refreshLoopTightLoopDelay);
578
571
  }
579
572
  }
580
- this.logger.trace('[refreshLoop] Completing refresh evaluation cycle, waiting 100ms...');
581
573
  await waitForNextAction(100);
582
574
  continue; // Re-evaluate the loop with the newly refreshed (or synced) token
583
575
  }
@@ -585,19 +577,15 @@ let AuthenticationClientService = class AuthenticationClientService {
585
577
  const timeUntilRefreshMs = (token.exp - this.estimatedServerTimestampSeconds() - buffer) * millisecondsPerSecond;
586
578
  let delay = clamp(timeUntilRefreshMs, minRefreshDelay, maxRefreshDelay);
587
579
  if (Number.isNaN(delay)) {
588
- this.logger.trace(`[refreshLoop] Calculated delay is NaN, using minRefreshDelay (${minRefreshDelay}ms)`);
589
580
  delay = minRefreshDelay;
590
581
  }
591
- this.logger.trace(`[refreshLoop] Scheduling next refresh in ${delay}ms...`);
592
582
  await waitForNextAction(delay);
593
583
  }
594
584
  catch (error) {
595
585
  this.logger.error(error);
596
- this.logger.trace(`[refreshLoop] Refresh loop encountered error, waiting ${refreshLoopTightLoopDelay}ms before retry...`);
597
586
  await waitForNextAction(refreshLoopTightLoopDelay);
598
587
  }
599
588
  }
600
- this.logger.trace('[refreshLoop] Refresh loop exited (dispose signal set).');
601
589
  }
602
590
  async handleRefreshError(error) {
603
591
  this.logger.error(error);
@@ -1,7 +1,6 @@
1
1
  import { firstValueFrom, race, timeout as rxjsTimeout } from 'rxjs';
2
2
  import { HttpError } from '../../http/index.js';
3
3
  import { supportsCookies } from '../../supports.js';
4
- import { timeout } from '../../utils/timing.js';
5
4
  import { isDefined } from '../../utils/type-guards.js';
6
5
  import { cacheValueOrAsyncProvider } from '../../utils/value-or-provider.js';
7
6
  import { dontWaitForValidToken } from '../authentication.api.js';
@@ -19,15 +18,13 @@ export function waitForAuthenticationMiddleware(authenticationServiceOrProvider)
19
18
  while (!authenticationService.hasValidToken && authenticationService.isLoggedIn()) {
20
19
  const race$ = race([
21
20
  authenticationService.validToken$,
21
+ authenticationService.loggedOut$,
22
22
  request.cancellationSignal,
23
23
  ]);
24
24
  await firstValueFrom(race$.pipe(rxjsTimeout(30000))).catch(() => undefined);
25
- if (request.cancellationSignal.isSet) {
25
+ if (request.cancellationSignal.isSet || !authenticationService.isLoggedIn()) {
26
26
  break;
27
27
  }
28
- if (!authenticationService.hasValidToken && authenticationService.isLoggedIn()) {
29
- await timeout(100);
30
- }
31
28
  }
32
29
  }
33
30
  await next();
@@ -8,4 +8,5 @@ export declare class TotpRecoveryCodesResult {
8
8
  }
9
9
  export declare class TotpStatusResult {
10
10
  active: boolean;
11
+ remainingRecoveryCodes: number | null;
11
12
  }
@@ -8,7 +8,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
8
8
  var __metadata = (this && this.__metadata) || function (k, v) {
9
9
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
10
  };
11
- import { Array, BooleanProperty, StringProperty } from '../../schema/index.js';
11
+ import { Array, BooleanProperty, NumberProperty, StringProperty } from '../../schema/index.js';
12
12
  export class TotpEnrollmentInitResult {
13
13
  secret;
14
14
  uri;
@@ -30,8 +30,13 @@ __decorate([
30
30
  ], TotpRecoveryCodesResult.prototype, "recoveryCodes", void 0);
31
31
  export class TotpStatusResult {
32
32
  active;
33
+ remainingRecoveryCodes;
33
34
  }
34
35
  __decorate([
35
36
  BooleanProperty(),
36
37
  __metadata("design:type", Boolean)
37
38
  ], TotpStatusResult.prototype, "active", void 0);
39
+ __decorate([
40
+ NumberProperty({ nullable: true }),
41
+ __metadata("design:type", Object)
42
+ ], TotpStatusResult.prototype, "remainingRecoveryCodes", void 0);
@@ -864,8 +864,13 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
864
864
  }
865
865
  async getTotpStatus(tenantId, subjectId) {
866
866
  const totp = await this.tryGetTotp(tenantId, subjectId);
867
+ const active = totp?.status == TotpStatus.Active;
868
+ const remainingRecoveryCodes = active
869
+ ? await this.#totpRecoveryCodeRepository.countByQuery({ tenantId, totpId: totp.id, usedTimestamp: null })
870
+ : null;
867
871
  return {
868
- active: totp?.status == TotpStatus.Active,
872
+ active,
873
+ remainingRecoveryCodes,
869
874
  };
870
875
  }
871
876
  async initEnrollTotp(tenantId, subjectId, auditor) {
@@ -75,10 +75,11 @@ export declare function generateTotpUri(encodedSecret: string, accountName: stri
75
75
  /**
76
76
  * Generates a set of random recovery codes.
77
77
  * @param count The number of codes to generate. Defaults to 10.
78
- * @param length The length of each code. Defaults to 8.
78
+ * @param length The length of each code. Defaults to 12.
79
79
  * @returns An array of generated recovery codes.
80
80
  */
81
81
  export declare function generateTotpRecoveryCodes(count?: number, length?: number): string[];
82
+ export declare function generateTotpRecoveryCode(length?: number): string;
82
83
  /**
83
84
  * Hashes a recovery code.
84
85
  * @param code The recovery code to hash.
@@ -1,5 +1,6 @@
1
1
  /** biome-ignore-all lint/suspicious/noBitwiseOperators: ok */
2
2
  import { Alphabet } from '../utils/alphabet.js';
3
+ import { createArray } from '../utils/array/array.js';
3
4
  import { encodeBase32 } from '../utils/base32.js';
4
5
  import { currentTimestampSeconds } from '../utils/date-time.js';
5
6
  import { encodeUtf8 } from '../utils/encoding.js';
@@ -88,15 +89,21 @@ export function generateTotpUri(encodedSecret, accountName, issuer, options) {
88
89
  /**
89
90
  * Generates a set of random recovery codes.
90
91
  * @param count The number of codes to generate. Defaults to 10.
91
- * @param length The length of each code. Defaults to 8.
92
+ * @param length The length of each code. Defaults to 12.
92
93
  * @returns An array of generated recovery codes.
93
94
  */
94
- export function generateTotpRecoveryCodes(count = 10, length = 8) {
95
- const codes = [];
96
- for (let i = 0; i < count; i++) {
97
- codes.push(getRandomString(length, Alphabet.Base32));
95
+ export function generateTotpRecoveryCodes(count = 10, length = 12) {
96
+ return createArray(count, () => generateTotpRecoveryCode(length));
97
+ }
98
+ export function generateTotpRecoveryCode(length = 12) {
99
+ const code = getRandomString(length, Alphabet.Base32);
100
+ if (length % 4 == 0) {
101
+ return code.match(/.{1,4}/g).join('-');
102
+ }
103
+ if (length % 6 == 0) {
104
+ return code.match(/.{1,6}/g).join('-');
98
105
  }
99
- return codes;
106
+ return code;
100
107
  }
101
108
  /**
102
109
  * Hashes a recovery code.
@@ -106,7 +113,8 @@ export function generateTotpRecoveryCodes(count = 10, length = 8) {
106
113
  */
107
114
  export async function hashTotpRecoveryCode(code, options) {
108
115
  const { length, algorithm } = options;
109
- const keyMaterial = encodeUtf8(code);
116
+ const normalizedCode = code.replace(/[-\s]/g, '').toUpperCase();
117
+ const keyMaterial = encodeUtf8(normalizedCode);
110
118
  const key = await importKey('raw-secret', keyMaterial, algorithm, false, ['deriveBits']);
111
119
  return await deriveBytes(algorithm, key, length);
112
120
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tstdl/base",
3
- "version": "0.93.188",
3
+ "version": "0.93.190",
4
4
  "author": "Patrick Hein",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -1,13 +1,16 @@
1
1
  import { computed, signal } from './api.js';
2
2
  export function createNotifier() {
3
3
  const sourceSignal = signal(0);
4
- const notifier = {
4
+ return {
5
5
  listen: sourceSignal.asReadonly(),
6
- notify: () => sourceSignal.update((v) => v + 1),
7
- computed: (computation, options) => computed(() => {
8
- notifier.listen();
9
- return computation();
10
- }, options)
6
+ notify() {
7
+ sourceSignal.update((v) => v + 1);
8
+ },
9
+ computed(computation, options) {
10
+ return computed(() => {
11
+ sourceSignal();
12
+ return computation();
13
+ }, options);
14
+ },
11
15
  };
12
- return notifier;
13
16
  }