@tstdl/base 0.93.187 → 0.93.189

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.
Files changed (45) hide show
  1. package/ai/genkit/multi-region.plugin.js +15 -11
  2. package/ai/genkit/types.d.ts +1 -1
  3. package/ai/genkit/types.js +1 -1
  4. package/authentication/client/authentication.service.d.ts +5 -0
  5. package/authentication/client/authentication.service.js +21 -4
  6. package/authentication/models/authentication-totp-recovery-code.model.js +3 -2
  7. package/authentication/models/totp-results.model.d.ts +1 -0
  8. package/authentication/models/totp-results.model.js +6 -1
  9. package/authentication/server/authentication.api-controller.d.ts +1 -0
  10. package/authentication/server/authentication.api-controller.js +9 -2
  11. package/authentication/server/authentication.service.js +114 -92
  12. package/authentication/server/drizzle/{0000_odd_echo.sql → 0000_dry_stepford_cuckoos.sql} +2 -1
  13. package/authentication/server/drizzle/meta/0000_snapshot.json +24 -2
  14. package/authentication/server/drizzle/meta/_journal.json +2 -2
  15. package/circuit-breaker/circuit-breaker.d.ts +22 -10
  16. package/circuit-breaker/postgres/circuit-breaker.d.ts +5 -4
  17. package/circuit-breaker/postgres/circuit-breaker.js +21 -19
  18. package/circuit-breaker/postgres/drizzle/{0000_same_captain_cross.sql → 0000_dapper_hercules.sql} +3 -2
  19. package/circuit-breaker/postgres/drizzle/meta/0000_snapshot.json +13 -6
  20. package/circuit-breaker/postgres/drizzle/meta/_journal.json +2 -2
  21. package/circuit-breaker/postgres/model.d.ts +2 -1
  22. package/circuit-breaker/postgres/model.js +9 -4
  23. package/circuit-breaker/postgres/provider.d.ts +1 -1
  24. package/circuit-breaker/postgres/provider.js +2 -2
  25. package/circuit-breaker/provider.d.ts +6 -1
  26. package/cryptography/totp.d.ts +2 -1
  27. package/cryptography/totp.js +15 -7
  28. package/orm/sqls/sqls.d.ts +5 -3
  29. package/orm/sqls/sqls.js +5 -3
  30. package/package.json +4 -4
  31. package/rate-limit/postgres/drizzle/{0000_serious_sauron.sql → 0000_previous_zeigeist.sql} +2 -1
  32. package/rate-limit/postgres/drizzle/meta/0000_snapshot.json +10 -3
  33. package/rate-limit/postgres/drizzle/meta/_journal.json +2 -2
  34. package/rate-limit/postgres/postgres-rate-limiter.d.ts +1 -0
  35. package/rate-limit/postgres/postgres-rate-limiter.js +3 -2
  36. package/rate-limit/postgres/rate-limit.model.d.ts +1 -0
  37. package/rate-limit/postgres/rate-limit.model.js +6 -1
  38. package/rate-limit/postgres/rate-limiter.provider.d.ts +1 -1
  39. package/rate-limit/postgres/rate-limiter.provider.js +2 -2
  40. package/rate-limit/provider.d.ts +3 -3
  41. package/rate-limit/rate-limiter.d.ts +2 -1
  42. package/signals/notifier.js +1 -1
  43. package/signals/operators/derive-async.js +11 -6
  44. package/task-queue/postgres/task-queue.js +8 -8
  45. package/testing/integration-setup.js +4 -0
@@ -14,16 +14,20 @@ export function vertexAiMultiLocation(options) {
14
14
  }
15
15
  const tokenLimitThreshold = options.tokenLimitThreshold ?? defaultTokenLimitThreshold;
16
16
  const locationConfigs = options.locations.map((location) => {
17
- const circuitBreakerKey = `genkit:vertex-ai:location:${location}`;
18
- const tokenLimitCircuitBreakerKey = `${circuitBreakerKey}:token-limit`;
17
+ const circuitBreakerNamespace = `genkit:vertex-ai:location`;
18
+ const tokenLimitCircuitBreakerNamespace = `genkit:vertex-ai:location-token-limit`;
19
+ const resource = location;
20
+ const tokenLimitResource = location;
19
21
  return {
20
22
  location,
21
- circuitBreaker: options.circuitBreakerProvider.provide(circuitBreakerKey, {
23
+ resource,
24
+ tokenLimitResource,
25
+ circuitBreaker: options.circuitBreakerProvider.provide(circuitBreakerNamespace, {
22
26
  threshold: 1,
23
27
  resetTimeout: 30 * millisecondsPerSecond,
24
28
  ...options.circuitBreakerConfig,
25
29
  }),
26
- tokenLimitCircuitBreaker: options.circuitBreakerProvider.provide(tokenLimitCircuitBreakerKey, {
30
+ tokenLimitCircuitBreaker: options.circuitBreakerProvider.provide(tokenLimitCircuitBreakerNamespace, {
27
31
  threshold: 1,
28
32
  resetTimeout: 15 * millisecondsPerMinute,
29
33
  ...options.tokenLimitCircuitBreakerConfig,
@@ -48,15 +52,15 @@ export function vertexAiMultiLocation(options) {
48
52
  let lastError;
49
53
  let isLargeRequest = false;
50
54
  const skippedLocations = [];
51
- for (const { location, circuitBreaker, tokenLimitCircuitBreaker } of shuffledConfigs) {
52
- const check = await circuitBreaker.check();
55
+ for (const { location, resource, tokenLimitResource, circuitBreaker, tokenLimitCircuitBreaker } of shuffledConfigs) {
56
+ const check = await circuitBreaker.check(resource);
53
57
  if (!check.allowed) {
54
58
  options.logger.warn(`Location ${location} is currently unhealthy. Skipping...`);
55
59
  skippedLocations.push({ location, reason: 'unhealthy' });
56
60
  continue;
57
61
  }
58
62
  if (isLargeRequest) {
59
- const tokenCheck = await tokenLimitCircuitBreaker.check();
63
+ const tokenCheck = await tokenLimitCircuitBreaker.check(tokenLimitResource);
60
64
  if (!tokenCheck.allowed) {
61
65
  options.logger.warn(`Location ${location} is known to have a low token limit. Skipping for this large request...`);
62
66
  skippedLocations.push({ location, reason: 'known to have low token limits' });
@@ -73,10 +77,10 @@ export function vertexAiMultiLocation(options) {
73
77
  }, {
74
78
  onChunk: streamingCallback,
75
79
  });
76
- await circuitBreaker.recordSuccess();
80
+ await circuitBreaker.recordSuccess(resource);
77
81
  const isLargeSuccess = isLargeRequest || ((result.usage?.inputTokens ?? 0) > tokenLimitThreshold);
78
82
  if (isLargeSuccess) {
79
- await tokenLimitCircuitBreaker.recordSuccess();
83
+ await tokenLimitCircuitBreaker.recordSuccess(tokenLimitResource);
80
84
  }
81
85
  return result;
82
86
  }
@@ -93,11 +97,11 @@ export function vertexAiMultiLocation(options) {
93
97
  if (isTokenLimitError) {
94
98
  options.logger.warn(`Location ${location} responded with token limit error. Trying next location...`);
95
99
  isLargeRequest = true;
96
- await tokenLimitCircuitBreaker.recordFailure();
100
+ await tokenLimitCircuitBreaker.recordFailure(tokenLimitResource);
97
101
  }
98
102
  else {
99
103
  options.logger.warn(`Location ${location} responded with ${error.status}. Tripping circuit breaker and trying next location...`);
100
- await circuitBreaker.recordFailure();
104
+ await circuitBreaker.recordFailure(resource);
101
105
  }
102
106
  }
103
107
  }
@@ -430,7 +430,7 @@ export declare abstract class VertexAiMultiLocationOptions {
430
430
  /**
431
431
  * Threshold of input tokens after which a request is considered large.
432
432
  * A successful request with more tokens than this threshold will record a success for the token limit circuit breaker.
433
- * Defaults to 131,072.
433
+ * Defaults to 131_072.
434
434
  */
435
435
  tokenLimitThreshold?: number;
436
436
  }
@@ -16,7 +16,7 @@ export class VertexAiMultiLocationOptions {
16
16
  /**
17
17
  * Threshold of input tokens after which a request is considered large.
18
18
  * A successful request with more tokens than this threshold will record a success for the token limit circuit breaker.
19
- * Defaults to 131,072.
19
+ * Defaults to 131_072.
20
20
  */
21
21
  tokenLimitThreshold;
22
22
  }
@@ -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.
@@ -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.
@@ -514,7 +528,10 @@ let AuthenticationClientService = class AuthenticationClientService {
514
528
  const token = this.token();
515
529
  // 1. Wait for login/token if none is present
516
530
  if (isUndefined(token)) {
517
- await firstValueFrom(race([this.definedToken$, from(this.disposeSignal)]), { defaultValue: undefined });
531
+ await firstValueFrom(race([
532
+ this.tokenUpdateBus.allMessages$.pipe(filter(isDefined)),
533
+ from(this.disposeSignal),
534
+ ]), { defaultValue: undefined });
518
535
  continue;
519
536
  }
520
537
  try {
@@ -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 { Table, TenantEntity, TenantReference, TimestampProperty, UuidProperty } from '../../orm/index.js';
10
+ import { Index, Table, TenantEntity, TenantReference, TimestampProperty, UuidProperty } from '../../orm/index.js';
11
11
  import { Uint8ArrayProperty } from '../../schema/index.js';
12
12
  import { AuthenticationTotp } from './authentication-totp.model.js';
13
13
  let AuthenticationTotpRecoveryCode = class AuthenticationTotpRecoveryCode extends TenantEntity {
@@ -29,6 +29,7 @@ __decorate([
29
29
  __metadata("design:type", Object)
30
30
  ], AuthenticationTotpRecoveryCode.prototype, "usedTimestamp", void 0);
31
31
  AuthenticationTotpRecoveryCode = __decorate([
32
- Table('totp_recovery_code', { schema: 'authentication' })
32
+ Table('totp_recovery_code', { schema: 'authentication' }),
33
+ Index(['tenantId', 'totpId'])
33
34
  ], AuthenticationTotpRecoveryCode);
34
35
  export { AuthenticationTotpRecoveryCode };
@@ -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);
@@ -99,6 +99,7 @@ export declare class AuthenticationApiController<AdditionalTokenPayload extends
99
99
  */
100
100
  invalidateAllOtherSessions({ getToken, getAuditor }: ApiRequestContext<AuthenticationApiDefinition<AdditionalTokenPayload, AuthenticationData, AdditionalInitPasswordResetData>, 'invalidateAllOtherSessions'>): Promise<ApiServerResult<AuthenticationApiDefinition<AdditionalTokenPayload, AuthenticationData, AdditionalInitPasswordResetData>, 'invalidateAllOtherSessions'>>;
101
101
  protected enforceRateLimit(ip: string, subjectResource: string, auditor: Auditor, targetId: string, action: string): Promise<void>;
102
+ protected refundRateLimit(ip: string, subjectResource: string): Promise<void>;
102
103
  protected getTokenResponse<T>(result: TokenResult<AdditionalTokenPayload>, body: NoInfer<T>): HttpServerResponse;
103
104
  protected getLoginResponse(result: LoginResult<AdditionalTokenPayload>): ApiServerResult<AuthenticationApiDefinition<AdditionalTokenPayload, AuthenticationData, AdditionalInitPasswordResetData>, 'login'>;
104
105
  }
@@ -35,12 +35,12 @@ let AuthenticationApiController = AuthenticationApiController_1 = class Authenti
35
35
  authenticationService = inject((AuthenticationService));
36
36
  options = inject(AuthenticationServiceOptions, undefined, { optional: true }) ?? {};
37
37
  subjectRateLimiter = inject(RateLimiter, {
38
- resource: 'authentication:subject',
38
+ namespace: 'authentication:subject',
39
39
  burstCapacity: this.options.bruteForceProtection?.subjectBurstCapacity ?? 10,
40
40
  refillInterval: this.options.bruteForceProtection?.subjectRefillInterval ?? (30 * millisecondsPerMinute),
41
41
  });
42
42
  ipRateLimiter = inject(RateLimiter, {
43
- resource: 'authentication:ip',
43
+ namespace: 'authentication:ip',
44
44
  burstCapacity: this.options.bruteForceProtection?.ipBurstCapacity ?? 20,
45
45
  refillInterval: this.options.bruteForceProtection?.ipRefillInterval ?? (5 * millisecondsPerMinute),
46
46
  });
@@ -154,6 +154,7 @@ let AuthenticationApiController = AuthenticationApiController_1 = class Authenti
154
154
  const subjectResource = `${validatedToken.payload.tenant}:${validatedToken.payload.subject}`;
155
155
  await this.enforceRateLimit(request.ip, subjectResource, auditor, validatedToken.payload.subject, 'refresh');
156
156
  const result = await this.authenticationService.refreshAlreadyValidatedToken(validatedToken, parameters.data, undefined, auditor);
157
+ await this.refundRateLimit(request.ip, subjectResource);
157
158
  return this.getTokenResponse(result, result.jsonToken.payload);
158
159
  }
159
160
  /**
@@ -332,6 +333,12 @@ let AuthenticationApiController = AuthenticationApiController_1 = class Authenti
332
333
  throw new TooManyRequestsError();
333
334
  }
334
335
  }
336
+ async refundRateLimit(ip, subjectResource) {
337
+ await Promise.all([
338
+ this.ipRateLimiter.refund(ip),
339
+ this.subjectRateLimiter.refund(subjectResource),
340
+ ]);
341
+ }
335
342
  getTokenResponse(result, body) {
336
343
  const { token, jsonToken, refreshToken, remember, omitImpersonatorRefreshToken, impersonatorRefreshToken, impersonatorRefreshTokenExpiration } = result;
337
344
  const options = {
@@ -294,14 +294,15 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
294
294
  const authAuditor = auditor.fork(AuthenticationService_1.name);
295
295
  await this.#endSession(sessionId, authAuditor);
296
296
  }
297
- async #endSession(sessionId, authAuditor) {
298
- const session = await this.#sessionRepository.tryLoad(sessionId);
297
+ async #endSession(sessionId, authAuditor, tx) {
298
+ const sessionRepository = this.#sessionRepository.withOptionalTransaction(tx);
299
+ const session = await sessionRepository.tryLoad(sessionId);
299
300
  if (isUndefined(session)) {
300
301
  this.#logger.warn(`Session "${sessionId}" not found for logout.`);
301
302
  return;
302
303
  }
303
304
  const now = currentTimestamp();
304
- await this.#sessionRepository.update(sessionId, { end: now });
305
+ await sessionRepository.update(sessionId, { end: now });
305
306
  await authAuditor.info('logout', {
306
307
  tenantId: session.tenantId,
307
308
  actor: session.subjectId,
@@ -411,53 +412,55 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
411
412
  let session;
412
413
  try {
413
414
  const sessionId = validatedRefreshToken.payload.session;
414
- session = await this.#sessionRepository.load(sessionId);
415
- const hash = await this.getHash(validatedRefreshToken.payload.secret, session.refreshTokenSalt);
416
- if (session.end <= currentTimestamp()) {
417
- throw new InvalidTokenError('Session is expired.');
418
- }
419
- if (!timingSafeBinaryEquals(hash, session.refreshTokenHash)) {
420
- await this.#endSession(sessionId, authAuditor);
421
- await authAuditor.warn('refresh-failure', {
422
- actorType: ActorType.Anonymous,
415
+ return await this.#sessionRepository.transaction(async (tx) => {
416
+ session = await this.#sessionRepository.withTransaction(tx).load(sessionId);
417
+ const hash = await this.getHash(validatedRefreshToken.payload.secret, session.refreshTokenSalt);
418
+ if (session.end <= currentTimestamp()) {
419
+ throw new InvalidTokenError('Session is expired.');
420
+ }
421
+ if (!timingSafeBinaryEquals(hash, session.refreshTokenHash)) {
422
+ await this.#endSession(sessionId, authAuditor, tx);
423
+ await authAuditor.warn('refresh-failure', {
424
+ actorType: ActorType.Anonymous,
425
+ tenantId: session.tenantId,
426
+ targetId: session.subjectId,
427
+ targetType: 'Subject',
428
+ details: { sessionId, reason: 'Token reuse detected. Session revoked.' },
429
+ });
430
+ throw new InvalidTokenError('Invalid refresh token.');
431
+ }
432
+ const now = currentTimestamp();
433
+ const impersonator = (options.omitImpersonator == true) ? undefined : validatedRefreshToken.payload.impersonator;
434
+ const remember = validatedRefreshToken.payload.remember;
435
+ const ttl = remember ? this.rememberRefreshTokenTimeToLive : this.refreshTokenTimeToLive;
436
+ const newEnd = now + ttl;
437
+ const subject = await this.#subjectRepository.loadByQuery({ tenantId: session.tenantId, id: session.subjectId });
438
+ this.ensureNotSuspended(subject);
439
+ const tokenPayload = await this.#authenticationAncillaryService?.getTokenPayload(subject, authenticationData, { action: GetTokenPayloadContextAction.Refresh });
440
+ const { token, jsonToken } = await this.createToken({ additionalTokenPayload: tokenPayload, subject, sessionId, refreshTokenExpiration: newEnd, impersonator, timestamp: now });
441
+ const newRefreshToken = await this.createRefreshToken(subject, sessionId, newEnd, { impersonator, remember });
442
+ await this.#sessionRepository.withTransaction(tx).update(sessionId, {
443
+ end: newEnd,
444
+ refreshTokenSalt: newRefreshToken.salt,
445
+ refreshTokenHash: newRefreshToken.hash,
446
+ });
447
+ await authAuditor.info('refresh-success', {
423
448
  tenantId: session.tenantId,
449
+ actor: session.subjectId,
450
+ actorType: ActorType.Subject,
424
451
  targetId: session.subjectId,
425
452
  targetType: 'Subject',
426
- details: { sessionId, reason: 'Token reuse detected. Session revoked.' },
453
+ details: { sessionId, remember },
427
454
  });
428
- throw new InvalidTokenError('Invalid refresh token.');
429
- }
430
- const now = currentTimestamp();
431
- const impersonator = (options.omitImpersonator == true) ? undefined : validatedRefreshToken.payload.impersonator;
432
- const remember = validatedRefreshToken.payload.remember;
433
- const ttl = remember ? this.rememberRefreshTokenTimeToLive : this.refreshTokenTimeToLive;
434
- const newEnd = now + ttl;
435
- const subject = await this.#subjectRepository.loadByQuery({ tenantId: session.tenantId, id: session.subjectId });
436
- this.ensureNotSuspended(subject);
437
- const tokenPayload = await this.#authenticationAncillaryService?.getTokenPayload(subject, authenticationData, { action: GetTokenPayloadContextAction.Refresh });
438
- const { token, jsonToken } = await this.createToken({ additionalTokenPayload: tokenPayload, subject, sessionId, refreshTokenExpiration: newEnd, impersonator, timestamp: now });
439
- const newRefreshToken = await this.createRefreshToken(subject, sessionId, newEnd, { impersonator, remember });
440
- await this.#sessionRepository.update(sessionId, {
441
- end: newEnd,
442
- refreshTokenSalt: newRefreshToken.salt,
443
- refreshTokenHash: newRefreshToken.hash,
444
- });
445
- await authAuditor.info('refresh-success', {
446
- tenantId: session.tenantId,
447
- actor: session.subjectId,
448
- actorType: ActorType.Subject,
449
- targetId: session.subjectId,
450
- targetType: 'Subject',
451
- details: { sessionId, remember },
455
+ return { token, jsonToken, refreshToken: newRefreshToken.token, remember, omitImpersonatorRefreshToken: options.omitImpersonator };
452
456
  });
453
- return { token, jsonToken, refreshToken: newRefreshToken.token, remember, omitImpersonatorRefreshToken: options.omitImpersonator };
454
457
  }
455
458
  catch (error) {
456
459
  await authAuditor.warn('refresh-failure', {
457
460
  actorType: ActorType.Anonymous,
458
461
  targetId: session?.subjectId ?? NIL_UUID,
459
462
  targetType: 'Subject',
460
- details: { sessionId: null, reason: error.message },
463
+ details: { sessionId: session?.id ?? null, reason: error.message },
461
464
  });
462
465
  throw error;
463
466
  }
@@ -861,8 +864,13 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
861
864
  }
862
865
  async getTotpStatus(tenantId, subjectId) {
863
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;
864
871
  return {
865
- active: totp?.status == TotpStatus.Active,
872
+ active,
873
+ remainingRecoveryCodes,
866
874
  };
867
875
  }
868
876
  async initEnrollTotp(tenantId, subjectId, auditor) {
@@ -995,7 +1003,7 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
995
1003
  }));
996
1004
  await this.#totpRepository.transaction(async (transaction) => {
997
1005
  const totpRecoveryCodeRepository = this.#totpRecoveryCodeRepository.withTransaction(transaction);
998
- await totpRecoveryCodeRepository.deleteByQuery({ tenantId, totpId: totp.id });
1006
+ await totpRecoveryCodeRepository.hardDeleteManyByQuery({ tenantId, totpId: totp.id });
999
1007
  await totpRecoveryCodeRepository.insertMany(hashedRecoveryCodes);
1000
1008
  if (options?.invalidateOtherSessions == true) {
1001
1009
  await this.#invalidateAllOtherSessions(tenantId, subjectId, NIL_UUID, authAuditor);
@@ -1028,7 +1036,10 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
1028
1036
  });
1029
1037
  throw new ForbiddenError('Invalid TOTP token');
1030
1038
  }
1031
- await this.#totpRepository.deleteByQuery({ tenantId, id: totp.id });
1039
+ await this.#totpRepository.transaction(async (tx) => {
1040
+ await this.#totpRecoveryCodeRepository.withTransaction(tx).hardDeleteManyByQuery({ tenantId, totpId: totp.id });
1041
+ await this.#totpRepository.withTransaction(tx).hardDeleteByQuery({ tenantId, id: totp.id });
1042
+ });
1032
1043
  await authAuditor.info('totp-disable-success', {
1033
1044
  tenantId,
1034
1045
  actor: subjectId,
@@ -1054,7 +1065,10 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
1054
1065
  });
1055
1066
  throw new ForbiddenError('Invalid recovery code');
1056
1067
  }
1057
- await this.#totpRepository.deleteByQuery({ tenantId, id: totp.id });
1068
+ await this.#totpRepository.transaction(async (tx) => {
1069
+ await this.#totpRecoveryCodeRepository.withTransaction(tx).hardDeleteManyByQuery({ tenantId, totpId: totp.id });
1070
+ await this.#totpRepository.withTransaction(tx).hardDeleteByQuery({ tenantId, id: totp.id });
1071
+ });
1058
1072
  await authAuditor.info('totp-disable-success', {
1059
1073
  tenantId,
1060
1074
  actor: subjectId,
@@ -1075,58 +1089,63 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
1075
1089
  const challengeToken = await this.validateTotpChallengeToken(challengeTokenString);
1076
1090
  const { tenant, subject: subjectId, remember, data } = challengeToken.payload;
1077
1091
  const totp = await this.#totpRepository.loadByQuery({ tenantId: tenant, subjectId });
1078
- const isRecoveryCodeValid = await this.verifyAndUseRecoveryCode(totp, recoveryCode);
1079
- if (!isRecoveryCodeValid) {
1080
- await authAuditor.warn('recovery-login-failure', {
1081
- actorType: ActorType.Anonymous,
1082
- tenantId: tenant,
1083
- targetId: subjectId,
1084
- targetType: 'Subject',
1085
- details: {
1086
- subjectInput: { tenantId: tenant, subject: subjectId },
1087
- resolvedSubjectId: subjectId,
1088
- },
1089
- });
1090
- throw new ForbiddenError('Invalid recovery code');
1091
- }
1092
- const subject = await this.#subjectRepository.loadByQuery({ tenantId: tenant, id: subjectId });
1093
- const loginResult = await this.#loginAlreadyValidatedSubject(subject, data, authAuditor, remember);
1094
- const unusedRecoveryCodesCount = await this.#totpRecoveryCodeRepository.countByQuery({ tenantId: tenant, totpId: totp.id, usedTimestamp: null });
1092
+ const result = await this.#totpRepository.transaction(async (tx) => {
1093
+ const isRecoveryCodeValid = await this.verifyAndUseRecoveryCode(totp, recoveryCode, tx);
1094
+ if (!isRecoveryCodeValid) {
1095
+ await authAuditor.warn('recovery-login-failure', {
1096
+ actorType: ActorType.Anonymous,
1097
+ tenantId: tenant,
1098
+ targetId: subjectId,
1099
+ targetType: 'Subject',
1100
+ details: {
1101
+ subjectInput: { tenantId: tenant, subject: subjectId },
1102
+ resolvedSubjectId: subjectId,
1103
+ },
1104
+ });
1105
+ throw new ForbiddenError('Invalid recovery code');
1106
+ }
1107
+ const subject = await this.#subjectRepository.loadByQuery({ tenantId: tenant, id: subjectId });
1108
+ const loginResult = await this.#loginAlreadyValidatedSubject(subject, data, authAuditor, remember);
1109
+ const unusedRecoveryCodesCount = await this.#totpRecoveryCodeRepository.withTransaction(tx).countByQuery({ tenantId: tenant, totpId: totp.id, usedTimestamp: null });
1110
+ return { loginResult, unusedRecoveryCodesCount };
1111
+ });
1095
1112
  await authAuditor.info('recovery-login-success', {
1096
1113
  tenantId: tenant,
1097
1114
  actor: subjectId,
1098
1115
  actorType: ActorType.Subject,
1099
1116
  targetId: subjectId,
1100
1117
  targetType: 'Subject',
1101
- network: { sessionId: loginResult.result.jsonToken.payload.session },
1118
+ network: { sessionId: result.loginResult.result.jsonToken.payload.session },
1102
1119
  details: {
1103
- sessionId: loginResult.result.jsonToken.payload.session,
1120
+ sessionId: result.loginResult.result.jsonToken.payload.session,
1104
1121
  remember,
1105
1122
  },
1106
1123
  });
1107
1124
  return {
1108
- ...loginResult,
1109
- lowRecoveryCodesWarning: unusedRecoveryCodesCount <= 3,
1125
+ ...result.loginResult,
1126
+ lowRecoveryCodesWarning: result.unusedRecoveryCodesCount <= 3,
1110
1127
  };
1111
1128
  }
1112
1129
  async #loginVerifyTotp(challengeTokenString, token, authAuditor) {
1113
1130
  const challengeToken = await this.validateTotpChallengeToken(challengeTokenString);
1114
1131
  const { tenant, subject: subjectId, remember, data } = challengeToken.payload;
1115
- const totp = await this.#totpRepository.loadByQuery({ tenantId: tenant, subjectId });
1116
- const isValid = await this.verifyAndRecordTotpToken(totp, token, authAuditor);
1117
- if (!isValid) {
1118
- await authAuditor.warn('totp-verify-failure', {
1119
- tenantId: tenant,
1120
- actor: subjectId,
1121
- actorType: ActorType.Subject,
1122
- targetId: subjectId,
1123
- targetType: 'Subject',
1124
- details: { reason: 'Invalid token' },
1125
- });
1126
- throw new ForbiddenError('Invalid TOTP token');
1127
- }
1128
- const subject = await this.#subjectRepository.loadByQuery({ tenantId: tenant, id: subjectId });
1129
- return await this.#loginAlreadyValidatedSubject(subject, data, authAuditor, remember);
1132
+ return await this.#totpRepository.transaction(async (tx) => {
1133
+ const totp = await this.#totpRepository.withTransaction(tx).loadByQuery({ tenantId: tenant, subjectId });
1134
+ const isValid = await this.verifyAndRecordTotpToken(totp, token, authAuditor, tx);
1135
+ if (!isValid) {
1136
+ await authAuditor.warn('totp-verify-failure', {
1137
+ tenantId: tenant,
1138
+ actor: subjectId,
1139
+ actorType: ActorType.Subject,
1140
+ targetId: subjectId,
1141
+ targetType: 'Subject',
1142
+ details: { reason: 'Invalid token' },
1143
+ });
1144
+ throw new ForbiddenError('Invalid TOTP token');
1145
+ }
1146
+ const subject = await this.#subjectRepository.loadByQuery({ tenantId: tenant, id: subjectId });
1147
+ return await this.#loginAlreadyValidatedSubject(subject, data, authAuditor, remember);
1148
+ });
1130
1149
  }
1131
1150
  async validateTotpChallengeToken(tokenString) {
1132
1151
  const validatedToken = await parseAndValidateJwtTokenString(tokenString, 'KMAC256', await this.#totpChallengeSigningKey.getKey());
@@ -1155,26 +1174,29 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
1155
1174
  };
1156
1175
  return await createJwtTokenString(jsonToken, await this.#totpChallengeSigningKey.getKey());
1157
1176
  }
1158
- async verifyAndUseRecoveryCode(totp, code) {
1159
- const recoveryCodes = await this.#totpRecoveryCodeRepository.loadManyByQuery({ tenantId: totp.tenantId, totpId: totp.id, usedTimestamp: null });
1160
- const hashingOptions = this.#getTotpRecoveryCodeHashingOptions(totp.recoveryCodeSalt);
1161
- const hash = await hashTotpRecoveryCode(code, hashingOptions);
1162
- for (const recoveryCode of recoveryCodes) {
1163
- const isValid = await verifyTotpRecoveryCode(hash, recoveryCode.code, hashingOptions);
1164
- if (isValid) {
1165
- await this.#totpRecoveryCodeRepository.updateByQuery({ tenantId: recoveryCode.tenantId, id: recoveryCode.id }, { usedTimestamp: TRANSACTION_TIMESTAMP });
1166
- return true;
1177
+ async verifyAndUseRecoveryCode(totp, code, tx) {
1178
+ return await this.#totpRecoveryCodeRepository.useTransaction(tx, async (tx) => {
1179
+ const totpRecoveryCodeRepository = this.#totpRecoveryCodeRepository.withTransaction(tx);
1180
+ const recoveryCodes = await totpRecoveryCodeRepository.loadManyByQuery({ tenantId: totp.tenantId, totpId: totp.id, usedTimestamp: null });
1181
+ const hashingOptions = this.#getTotpRecoveryCodeHashingOptions(totp.recoveryCodeSalt);
1182
+ const hash = await hashTotpRecoveryCode(code, hashingOptions);
1183
+ for (const recoveryCode of recoveryCodes) {
1184
+ const isValid = await verifyTotpRecoveryCode(hash, recoveryCode.code, hashingOptions);
1185
+ if (isValid) {
1186
+ await totpRecoveryCodeRepository.updateByQuery({ tenantId: recoveryCode.tenantId, id: recoveryCode.id, usedTimestamp: null }, { usedTimestamp: TRANSACTION_TIMESTAMP });
1187
+ return true;
1188
+ }
1167
1189
  }
1168
- }
1169
- return false;
1190
+ return false;
1191
+ });
1170
1192
  }
1171
- async verifyAndRecordTotpToken(totp, token, authAuditor) {
1193
+ async verifyAndRecordTotpToken(totp, token, authAuditor, tx) {
1172
1194
  const secret = await importHmacKey('raw-secret', this.totpOptions.codeHashAlgorithm, totp.secret, false);
1173
1195
  const isValid = await verifyTotpToken(secret, token, this.#getTotpOptions(totp.recoveryCodeSalt));
1174
1196
  if (!isValid) {
1175
1197
  return false;
1176
1198
  }
1177
- const inserted = await this.#usedTotpTokenRepository.tryInsert({
1199
+ const inserted = await this.#usedTotpTokenRepository.withOptionalTransaction(tx).tryInsert({
1178
1200
  tenantId: totp.tenantId,
1179
1201
  subjectId: totp.subjectId,
1180
1202
  token,
@@ -140,4 +140,5 @@ ALTER TABLE "authentication"."used_totp_tokens" ADD CONSTRAINT "used_totp_tokens
140
140
  ALTER TABLE "authentication"."service_account" ADD CONSTRAINT "service_account_tenantId_type_id_subject_fkey" FOREIGN KEY ("tenant_id","type","id") REFERENCES "authentication"."subject"("tenant_id","type","id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
141
141
  ALTER TABLE "authentication"."service_account" ADD CONSTRAINT "service_account_id_subject_fkey" FOREIGN KEY ("tenant_id","parent") REFERENCES "authentication"."subject"("tenant_id","id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
142
142
  ALTER TABLE "authentication"."system_account" ADD CONSTRAINT "system_account_tenantId_type_id_subject_fkey" FOREIGN KEY ("tenant_id","type","id") REFERENCES "authentication"."subject"("tenant_id","type","id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
143
- ALTER TABLE "authentication"."user" ADD CONSTRAINT "user_tenantId_type_id_subject_fkey" FOREIGN KEY ("tenant_id","type","id") REFERENCES "authentication"."subject"("tenant_id","type","id") ON DELETE cascade ON UPDATE no action;
143
+ ALTER TABLE "authentication"."user" ADD CONSTRAINT "user_tenantId_type_id_subject_fkey" FOREIGN KEY ("tenant_id","type","id") REFERENCES "authentication"."subject"("tenant_id","type","id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
144
+ CREATE INDEX "totp_recovery_code_tenant_id_totp_id_idx" ON "authentication"."totp_recovery_code" USING btree ("tenant_id","totp_id");