@tstdl/base 0.93.186 → 0.93.188
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/ai/genkit/multi-region.plugin.js +14 -11
- package/ai/genkit/types.d.ts +1 -1
- package/ai/genkit/types.js +1 -1
- package/authentication/client/authentication.service.js +40 -15
- package/authentication/models/authentication-totp-recovery-code.model.js +3 -2
- package/authentication/server/authentication.api-controller.d.ts +1 -0
- package/authentication/server/authentication.api-controller.js +9 -2
- package/authentication/server/authentication.service.js +108 -91
- package/authentication/server/drizzle/{0000_odd_echo.sql → 0000_dry_stepford_cuckoos.sql} +2 -1
- package/authentication/server/drizzle/meta/0000_snapshot.json +24 -2
- package/authentication/server/drizzle/meta/_journal.json +2 -2
- package/circuit-breaker/circuit-breaker.d.ts +22 -10
- package/circuit-breaker/postgres/circuit-breaker.d.ts +5 -4
- package/circuit-breaker/postgres/circuit-breaker.js +21 -19
- package/circuit-breaker/postgres/drizzle/{0000_same_captain_cross.sql → 0000_dapper_hercules.sql} +3 -2
- package/circuit-breaker/postgres/drizzle/meta/0000_snapshot.json +13 -6
- package/circuit-breaker/postgres/drizzle/meta/_journal.json +2 -2
- package/circuit-breaker/postgres/model.d.ts +2 -1
- package/circuit-breaker/postgres/model.js +9 -4
- package/circuit-breaker/postgres/provider.d.ts +1 -1
- package/circuit-breaker/postgres/provider.js +2 -2
- package/circuit-breaker/provider.d.ts +6 -1
- package/orm/sqls/sqls.d.ts +5 -3
- package/orm/sqls/sqls.js +5 -3
- package/package.json +4 -4
- package/rate-limit/postgres/drizzle/{0000_serious_sauron.sql → 0000_previous_zeigeist.sql} +2 -1
- package/rate-limit/postgres/drizzle/meta/0000_snapshot.json +10 -3
- package/rate-limit/postgres/drizzle/meta/_journal.json +2 -2
- package/rate-limit/postgres/postgres-rate-limiter.d.ts +1 -0
- package/rate-limit/postgres/postgres-rate-limiter.js +3 -2
- package/rate-limit/postgres/rate-limit.model.d.ts +1 -0
- package/rate-limit/postgres/rate-limit.model.js +6 -1
- package/rate-limit/postgres/rate-limiter.provider.d.ts +1 -1
- package/rate-limit/postgres/rate-limiter.provider.js +2 -2
- package/rate-limit/provider.d.ts +3 -3
- package/rate-limit/rate-limiter.d.ts +2 -1
- package/signals/operators/derive-async.js +11 -6
- package/task-queue/postgres/task-queue.js +8 -8
- package/testing/integration-setup.js +4 -0
|
@@ -14,16 +14,19 @@ export function vertexAiMultiLocation(options) {
|
|
|
14
14
|
}
|
|
15
15
|
const tokenLimitThreshold = options.tokenLimitThreshold ?? defaultTokenLimitThreshold;
|
|
16
16
|
const locationConfigs = options.locations.map((location) => {
|
|
17
|
-
const
|
|
18
|
-
const
|
|
17
|
+
const circuitBreakerNamespace = `genkit:vertex-ai:location`;
|
|
18
|
+
const resource = location;
|
|
19
|
+
const tokenLimitResource = `${location}:token-limit`;
|
|
19
20
|
return {
|
|
20
21
|
location,
|
|
21
|
-
|
|
22
|
+
resource,
|
|
23
|
+
tokenLimitResource,
|
|
24
|
+
circuitBreaker: options.circuitBreakerProvider.provide(circuitBreakerNamespace, {
|
|
22
25
|
threshold: 1,
|
|
23
26
|
resetTimeout: 30 * millisecondsPerSecond,
|
|
24
27
|
...options.circuitBreakerConfig,
|
|
25
28
|
}),
|
|
26
|
-
tokenLimitCircuitBreaker: options.circuitBreakerProvider.provide(
|
|
29
|
+
tokenLimitCircuitBreaker: options.circuitBreakerProvider.provide(circuitBreakerNamespace, {
|
|
27
30
|
threshold: 1,
|
|
28
31
|
resetTimeout: 15 * millisecondsPerMinute,
|
|
29
32
|
...options.tokenLimitCircuitBreakerConfig,
|
|
@@ -48,15 +51,15 @@ export function vertexAiMultiLocation(options) {
|
|
|
48
51
|
let lastError;
|
|
49
52
|
let isLargeRequest = false;
|
|
50
53
|
const skippedLocations = [];
|
|
51
|
-
for (const { location, circuitBreaker, tokenLimitCircuitBreaker } of shuffledConfigs) {
|
|
52
|
-
const check = await circuitBreaker.check();
|
|
54
|
+
for (const { location, resource, tokenLimitResource, circuitBreaker, tokenLimitCircuitBreaker } of shuffledConfigs) {
|
|
55
|
+
const check = await circuitBreaker.check(resource);
|
|
53
56
|
if (!check.allowed) {
|
|
54
57
|
options.logger.warn(`Location ${location} is currently unhealthy. Skipping...`);
|
|
55
58
|
skippedLocations.push({ location, reason: 'unhealthy' });
|
|
56
59
|
continue;
|
|
57
60
|
}
|
|
58
61
|
if (isLargeRequest) {
|
|
59
|
-
const tokenCheck = await tokenLimitCircuitBreaker.check();
|
|
62
|
+
const tokenCheck = await tokenLimitCircuitBreaker.check(tokenLimitResource);
|
|
60
63
|
if (!tokenCheck.allowed) {
|
|
61
64
|
options.logger.warn(`Location ${location} is known to have a low token limit. Skipping for this large request...`);
|
|
62
65
|
skippedLocations.push({ location, reason: 'known to have low token limits' });
|
|
@@ -73,10 +76,10 @@ export function vertexAiMultiLocation(options) {
|
|
|
73
76
|
}, {
|
|
74
77
|
onChunk: streamingCallback,
|
|
75
78
|
});
|
|
76
|
-
await circuitBreaker.recordSuccess();
|
|
79
|
+
await circuitBreaker.recordSuccess(resource);
|
|
77
80
|
const isLargeSuccess = isLargeRequest || ((result.usage?.inputTokens ?? 0) > tokenLimitThreshold);
|
|
78
81
|
if (isLargeSuccess) {
|
|
79
|
-
await tokenLimitCircuitBreaker.recordSuccess();
|
|
82
|
+
await tokenLimitCircuitBreaker.recordSuccess(tokenLimitResource);
|
|
80
83
|
}
|
|
81
84
|
return result;
|
|
82
85
|
}
|
|
@@ -93,11 +96,11 @@ export function vertexAiMultiLocation(options) {
|
|
|
93
96
|
if (isTokenLimitError) {
|
|
94
97
|
options.logger.warn(`Location ${location} responded with token limit error. Trying next location...`);
|
|
95
98
|
isLargeRequest = true;
|
|
96
|
-
await tokenLimitCircuitBreaker.recordFailure();
|
|
99
|
+
await tokenLimitCircuitBreaker.recordFailure(tokenLimitResource);
|
|
97
100
|
}
|
|
98
101
|
else {
|
|
99
102
|
options.logger.warn(`Location ${location} responded with ${error.status}. Tripping circuit breaker and trying next location...`);
|
|
100
|
-
await circuitBreaker.recordFailure();
|
|
103
|
+
await circuitBreaker.recordFailure(resource);
|
|
101
104
|
}
|
|
102
105
|
}
|
|
103
106
|
}
|
package/ai/genkit/types.d.ts
CHANGED
|
@@ -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
|
|
433
|
+
* Defaults to 131_072.
|
|
434
434
|
*/
|
|
435
435
|
tokenLimitThreshold?: number;
|
|
436
436
|
}
|
package/ai/genkit/types.js
CHANGED
|
@@ -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
|
|
19
|
+
* Defaults to 131_072.
|
|
20
20
|
*/
|
|
21
21
|
tokenLimitThreshold;
|
|
22
22
|
}
|
|
@@ -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, timer } from 'rxjs';
|
|
10
|
+
import { Subject, filter, firstValueFrom, from, race, takeUntil, tap, 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';
|
|
@@ -204,7 +204,7 @@ let AuthenticationClientService = class AuthenticationClientService {
|
|
|
204
204
|
this.loadToken();
|
|
205
205
|
this.tokenUpdateBus.messages$
|
|
206
206
|
.pipe(takeUntil(this.disposeSignal))
|
|
207
|
-
.subscribe((
|
|
207
|
+
.subscribe(() => this.loadToken());
|
|
208
208
|
this.refreshLoopPromise ??= this.refreshLoop();
|
|
209
209
|
}
|
|
210
210
|
/** @internal */
|
|
@@ -500,33 +500,46 @@ let AuthenticationClientService = class AuthenticationClientService {
|
|
|
500
500
|
this.tokenUpdateBus.publishAndForget(token);
|
|
501
501
|
}
|
|
502
502
|
async refreshLoop() {
|
|
503
|
+
this.logger.trace('[refreshLoop] Starting refresh loop...');
|
|
503
504
|
if (this.isLoggedIn()) {
|
|
505
|
+
this.logger.trace('[refreshLoop] User is logged in, syncing clock...');
|
|
504
506
|
await this.syncClock();
|
|
505
507
|
}
|
|
506
508
|
// Helper to sleep until the delay passes OR a vital state change occurs
|
|
507
|
-
const waitForNextAction = async (delayMs
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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
|
+
};
|
|
513
518
|
while (this.disposeSignal.isUnset) {
|
|
514
519
|
const token = this.token();
|
|
515
520
|
// 1. Wait for login/token if none is present
|
|
516
521
|
if (isUndefined(token)) {
|
|
517
|
-
|
|
522
|
+
this.logger.trace('[refreshLoop] No token found, waiting for login...');
|
|
523
|
+
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'))),
|
|
526
|
+
]), { defaultValue: undefined });
|
|
518
527
|
continue;
|
|
519
528
|
}
|
|
520
529
|
try {
|
|
521
530
|
const now = this.estimatedServerTimestampSeconds();
|
|
522
531
|
const buffer = calculateRefreshBufferSeconds(token);
|
|
523
532
|
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()}`);
|
|
524
534
|
// 2. Handle token refresh
|
|
525
535
|
if (needsRefresh) {
|
|
536
|
+
this.logger.trace('[refreshLoop] Attempting to acquire refresh lock...');
|
|
526
537
|
const lockResult = await this.lock.tryUse(undefined, async () => {
|
|
538
|
+
this.logger.trace('[refreshLoop] Refresh lock acquired, evaluating if refresh is still needed...');
|
|
527
539
|
this.loadToken();
|
|
528
540
|
const currentToken = this.token();
|
|
529
541
|
if (isUndefined(currentToken)) {
|
|
542
|
+
this.logger.trace('[refreshLoop] Token became undefined while waiting for lock, skipping refresh.');
|
|
530
543
|
return;
|
|
531
544
|
}
|
|
532
545
|
// Passive Sync: Verify if refresh is still needed once lock is acquired
|
|
@@ -534,15 +547,22 @@ let AuthenticationClientService = class AuthenticationClientService {
|
|
|
534
547
|
const currentBuffer = calculateRefreshBufferSeconds(currentToken);
|
|
535
548
|
const stillNeedsRefresh = this.forceRefreshRequested() || (currentNow >= currentToken.exp - currentBuffer);
|
|
536
549
|
if (stillNeedsRefresh) {
|
|
550
|
+
this.logger.trace('[refreshLoop] Refresh is still needed, performing refresh...');
|
|
537
551
|
await this.refresh();
|
|
538
552
|
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.');
|
|
539
557
|
}
|
|
540
558
|
});
|
|
541
559
|
// If lock is held by another instance/tab, wait briefly for it to finish (passive sync)
|
|
542
560
|
if (!lockResult.success) {
|
|
543
|
-
|
|
561
|
+
this.logger.trace('[refreshLoop] Refresh lock held by another instance, waiting 5000ms for passive sync...');
|
|
562
|
+
await waitForNextAction(5000);
|
|
544
563
|
// If another tab successfully refreshed, the expiration will have changed
|
|
545
564
|
if (this.token()?.exp !== token.exp) {
|
|
565
|
+
this.logger.trace('[refreshLoop] Token refreshed by another instance (expiration changed), clearing force refresh flag.');
|
|
546
566
|
this.forceRefreshRequested.set(false);
|
|
547
567
|
}
|
|
548
568
|
}
|
|
@@ -553,26 +573,31 @@ let AuthenticationClientService = class AuthenticationClientService {
|
|
|
553
573
|
const newBuffer = calculateRefreshBufferSeconds(newToken);
|
|
554
574
|
const stillNeedsRefresh = this.forceRefreshRequested() || (newNow >= newToken.exp - newBuffer);
|
|
555
575
|
if (stillNeedsRefresh) {
|
|
556
|
-
this.logger.warn('Token still needs refresh after attempt. Waiting briefly to avoid tight loop.');
|
|
557
|
-
await waitForNextAction(refreshLoopTightLoopDelay
|
|
576
|
+
this.logger.warn('[refreshLoop] Token still needs refresh after attempt. Waiting briefly to avoid tight loop.');
|
|
577
|
+
await waitForNextAction(refreshLoopTightLoopDelay);
|
|
558
578
|
}
|
|
559
579
|
}
|
|
560
|
-
|
|
580
|
+
this.logger.trace('[refreshLoop] Completing refresh evaluation cycle, waiting 100ms...');
|
|
581
|
+
await waitForNextAction(100);
|
|
561
582
|
continue; // Re-evaluate the loop with the newly refreshed (or synced) token
|
|
562
583
|
}
|
|
563
584
|
// 3. Calculate delay and sleep until the next scheduled refresh window
|
|
564
585
|
const timeUntilRefreshMs = (token.exp - this.estimatedServerTimestampSeconds() - buffer) * millisecondsPerSecond;
|
|
565
586
|
let delay = clamp(timeUntilRefreshMs, minRefreshDelay, maxRefreshDelay);
|
|
566
587
|
if (Number.isNaN(delay)) {
|
|
588
|
+
this.logger.trace(`[refreshLoop] Calculated delay is NaN, using minRefreshDelay (${minRefreshDelay}ms)`);
|
|
567
589
|
delay = minRefreshDelay;
|
|
568
590
|
}
|
|
569
|
-
|
|
591
|
+
this.logger.trace(`[refreshLoop] Scheduling next refresh in ${delay}ms...`);
|
|
592
|
+
await waitForNextAction(delay);
|
|
570
593
|
}
|
|
571
594
|
catch (error) {
|
|
572
595
|
this.logger.error(error);
|
|
573
|
-
|
|
596
|
+
this.logger.trace(`[refreshLoop] Refresh loop encountered error, waiting ${refreshLoopTightLoopDelay}ms before retry...`);
|
|
597
|
+
await waitForNextAction(refreshLoopTightLoopDelay);
|
|
574
598
|
}
|
|
575
599
|
}
|
|
600
|
+
this.logger.trace('[refreshLoop] Refresh loop exited (dispose signal set).');
|
|
576
601
|
}
|
|
577
602
|
async handleRefreshError(error) {
|
|
578
603
|
this.logger.error(error);
|
|
@@ -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 };
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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,
|
|
453
|
+
details: { sessionId, remember },
|
|
427
454
|
});
|
|
428
|
-
|
|
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
|
}
|
|
@@ -995,7 +998,7 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
|
|
|
995
998
|
}));
|
|
996
999
|
await this.#totpRepository.transaction(async (transaction) => {
|
|
997
1000
|
const totpRecoveryCodeRepository = this.#totpRecoveryCodeRepository.withTransaction(transaction);
|
|
998
|
-
await totpRecoveryCodeRepository.
|
|
1001
|
+
await totpRecoveryCodeRepository.hardDeleteManyByQuery({ tenantId, totpId: totp.id });
|
|
999
1002
|
await totpRecoveryCodeRepository.insertMany(hashedRecoveryCodes);
|
|
1000
1003
|
if (options?.invalidateOtherSessions == true) {
|
|
1001
1004
|
await this.#invalidateAllOtherSessions(tenantId, subjectId, NIL_UUID, authAuditor);
|
|
@@ -1028,7 +1031,10 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
|
|
|
1028
1031
|
});
|
|
1029
1032
|
throw new ForbiddenError('Invalid TOTP token');
|
|
1030
1033
|
}
|
|
1031
|
-
await this.#totpRepository.
|
|
1034
|
+
await this.#totpRepository.transaction(async (tx) => {
|
|
1035
|
+
await this.#totpRecoveryCodeRepository.withTransaction(tx).hardDeleteManyByQuery({ tenantId, totpId: totp.id });
|
|
1036
|
+
await this.#totpRepository.withTransaction(tx).hardDeleteByQuery({ tenantId, id: totp.id });
|
|
1037
|
+
});
|
|
1032
1038
|
await authAuditor.info('totp-disable-success', {
|
|
1033
1039
|
tenantId,
|
|
1034
1040
|
actor: subjectId,
|
|
@@ -1054,7 +1060,10 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
|
|
|
1054
1060
|
});
|
|
1055
1061
|
throw new ForbiddenError('Invalid recovery code');
|
|
1056
1062
|
}
|
|
1057
|
-
await this.#totpRepository.
|
|
1063
|
+
await this.#totpRepository.transaction(async (tx) => {
|
|
1064
|
+
await this.#totpRecoveryCodeRepository.withTransaction(tx).hardDeleteManyByQuery({ tenantId, totpId: totp.id });
|
|
1065
|
+
await this.#totpRepository.withTransaction(tx).hardDeleteByQuery({ tenantId, id: totp.id });
|
|
1066
|
+
});
|
|
1058
1067
|
await authAuditor.info('totp-disable-success', {
|
|
1059
1068
|
tenantId,
|
|
1060
1069
|
actor: subjectId,
|
|
@@ -1075,58 +1084,63 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
|
|
|
1075
1084
|
const challengeToken = await this.validateTotpChallengeToken(challengeTokenString);
|
|
1076
1085
|
const { tenant, subject: subjectId, remember, data } = challengeToken.payload;
|
|
1077
1086
|
const totp = await this.#totpRepository.loadByQuery({ tenantId: tenant, subjectId });
|
|
1078
|
-
const
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1087
|
+
const result = await this.#totpRepository.transaction(async (tx) => {
|
|
1088
|
+
const isRecoveryCodeValid = await this.verifyAndUseRecoveryCode(totp, recoveryCode, tx);
|
|
1089
|
+
if (!isRecoveryCodeValid) {
|
|
1090
|
+
await authAuditor.warn('recovery-login-failure', {
|
|
1091
|
+
actorType: ActorType.Anonymous,
|
|
1092
|
+
tenantId: tenant,
|
|
1093
|
+
targetId: subjectId,
|
|
1094
|
+
targetType: 'Subject',
|
|
1095
|
+
details: {
|
|
1096
|
+
subjectInput: { tenantId: tenant, subject: subjectId },
|
|
1097
|
+
resolvedSubjectId: subjectId,
|
|
1098
|
+
},
|
|
1099
|
+
});
|
|
1100
|
+
throw new ForbiddenError('Invalid recovery code');
|
|
1101
|
+
}
|
|
1102
|
+
const subject = await this.#subjectRepository.loadByQuery({ tenantId: tenant, id: subjectId });
|
|
1103
|
+
const loginResult = await this.#loginAlreadyValidatedSubject(subject, data, authAuditor, remember);
|
|
1104
|
+
const unusedRecoveryCodesCount = await this.#totpRecoveryCodeRepository.withTransaction(tx).countByQuery({ tenantId: tenant, totpId: totp.id, usedTimestamp: null });
|
|
1105
|
+
return { loginResult, unusedRecoveryCodesCount };
|
|
1106
|
+
});
|
|
1095
1107
|
await authAuditor.info('recovery-login-success', {
|
|
1096
1108
|
tenantId: tenant,
|
|
1097
1109
|
actor: subjectId,
|
|
1098
1110
|
actorType: ActorType.Subject,
|
|
1099
1111
|
targetId: subjectId,
|
|
1100
1112
|
targetType: 'Subject',
|
|
1101
|
-
network: { sessionId: loginResult.result.jsonToken.payload.session },
|
|
1113
|
+
network: { sessionId: result.loginResult.result.jsonToken.payload.session },
|
|
1102
1114
|
details: {
|
|
1103
|
-
sessionId: loginResult.result.jsonToken.payload.session,
|
|
1115
|
+
sessionId: result.loginResult.result.jsonToken.payload.session,
|
|
1104
1116
|
remember,
|
|
1105
1117
|
},
|
|
1106
1118
|
});
|
|
1107
1119
|
return {
|
|
1108
|
-
...loginResult,
|
|
1109
|
-
lowRecoveryCodesWarning: unusedRecoveryCodesCount <= 3,
|
|
1120
|
+
...result.loginResult,
|
|
1121
|
+
lowRecoveryCodesWarning: result.unusedRecoveryCodesCount <= 3,
|
|
1110
1122
|
};
|
|
1111
1123
|
}
|
|
1112
1124
|
async #loginVerifyTotp(challengeTokenString, token, authAuditor) {
|
|
1113
1125
|
const challengeToken = await this.validateTotpChallengeToken(challengeTokenString);
|
|
1114
1126
|
const { tenant, subject: subjectId, remember, data } = challengeToken.payload;
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1127
|
+
return await this.#totpRepository.transaction(async (tx) => {
|
|
1128
|
+
const totp = await this.#totpRepository.withTransaction(tx).loadByQuery({ tenantId: tenant, subjectId });
|
|
1129
|
+
const isValid = await this.verifyAndRecordTotpToken(totp, token, authAuditor, tx);
|
|
1130
|
+
if (!isValid) {
|
|
1131
|
+
await authAuditor.warn('totp-verify-failure', {
|
|
1132
|
+
tenantId: tenant,
|
|
1133
|
+
actor: subjectId,
|
|
1134
|
+
actorType: ActorType.Subject,
|
|
1135
|
+
targetId: subjectId,
|
|
1136
|
+
targetType: 'Subject',
|
|
1137
|
+
details: { reason: 'Invalid token' },
|
|
1138
|
+
});
|
|
1139
|
+
throw new ForbiddenError('Invalid TOTP token');
|
|
1140
|
+
}
|
|
1141
|
+
const subject = await this.#subjectRepository.loadByQuery({ tenantId: tenant, id: subjectId });
|
|
1142
|
+
return await this.#loginAlreadyValidatedSubject(subject, data, authAuditor, remember);
|
|
1143
|
+
});
|
|
1130
1144
|
}
|
|
1131
1145
|
async validateTotpChallengeToken(tokenString) {
|
|
1132
1146
|
const validatedToken = await parseAndValidateJwtTokenString(tokenString, 'KMAC256', await this.#totpChallengeSigningKey.getKey());
|
|
@@ -1155,26 +1169,29 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
|
|
|
1155
1169
|
};
|
|
1156
1170
|
return await createJwtTokenString(jsonToken, await this.#totpChallengeSigningKey.getKey());
|
|
1157
1171
|
}
|
|
1158
|
-
async verifyAndUseRecoveryCode(totp, code) {
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
const
|
|
1164
|
-
|
|
1165
|
-
await
|
|
1166
|
-
|
|
1172
|
+
async verifyAndUseRecoveryCode(totp, code, tx) {
|
|
1173
|
+
return await this.#totpRecoveryCodeRepository.useTransaction(tx, async (tx) => {
|
|
1174
|
+
const totpRecoveryCodeRepository = this.#totpRecoveryCodeRepository.withTransaction(tx);
|
|
1175
|
+
const recoveryCodes = await totpRecoveryCodeRepository.loadManyByQuery({ tenantId: totp.tenantId, totpId: totp.id, usedTimestamp: null });
|
|
1176
|
+
const hashingOptions = this.#getTotpRecoveryCodeHashingOptions(totp.recoveryCodeSalt);
|
|
1177
|
+
const hash = await hashTotpRecoveryCode(code, hashingOptions);
|
|
1178
|
+
for (const recoveryCode of recoveryCodes) {
|
|
1179
|
+
const isValid = await verifyTotpRecoveryCode(hash, recoveryCode.code, hashingOptions);
|
|
1180
|
+
if (isValid) {
|
|
1181
|
+
await totpRecoveryCodeRepository.updateByQuery({ tenantId: recoveryCode.tenantId, id: recoveryCode.id, usedTimestamp: null }, { usedTimestamp: TRANSACTION_TIMESTAMP });
|
|
1182
|
+
return true;
|
|
1183
|
+
}
|
|
1167
1184
|
}
|
|
1168
|
-
|
|
1169
|
-
|
|
1185
|
+
return false;
|
|
1186
|
+
});
|
|
1170
1187
|
}
|
|
1171
|
-
async verifyAndRecordTotpToken(totp, token, authAuditor) {
|
|
1188
|
+
async verifyAndRecordTotpToken(totp, token, authAuditor, tx) {
|
|
1172
1189
|
const secret = await importHmacKey('raw-secret', this.totpOptions.codeHashAlgorithm, totp.secret, false);
|
|
1173
1190
|
const isValid = await verifyTotpToken(secret, token, this.#getTotpOptions(totp.recoveryCodeSalt));
|
|
1174
1191
|
if (!isValid) {
|
|
1175
1192
|
return false;
|
|
1176
1193
|
}
|
|
1177
|
-
const inserted = await this.#usedTotpTokenRepository.tryInsert({
|
|
1194
|
+
const inserted = await this.#usedTotpTokenRepository.withOptionalTransaction(tx).tryInsert({
|
|
1178
1195
|
tenantId: totp.tenantId,
|
|
1179
1196
|
subjectId: totp.subjectId,
|
|
1180
1197
|
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");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"id": "
|
|
2
|
+
"id": "06619eff-dfa3-4a91-b5f4-c3cfe7a78951",
|
|
3
3
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
|
4
4
|
"version": "7",
|
|
5
5
|
"dialect": "postgresql",
|
|
@@ -408,7 +408,29 @@
|
|
|
408
408
|
"default": "'{}'::jsonb"
|
|
409
409
|
}
|
|
410
410
|
},
|
|
411
|
-
"indexes": {
|
|
411
|
+
"indexes": {
|
|
412
|
+
"totp_recovery_code_tenant_id_totp_id_idx": {
|
|
413
|
+
"name": "totp_recovery_code_tenant_id_totp_id_idx",
|
|
414
|
+
"columns": [
|
|
415
|
+
{
|
|
416
|
+
"expression": "tenant_id",
|
|
417
|
+
"isExpression": false,
|
|
418
|
+
"asc": true,
|
|
419
|
+
"nulls": "last"
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
"expression": "totp_id",
|
|
423
|
+
"isExpression": false,
|
|
424
|
+
"asc": true,
|
|
425
|
+
"nulls": "last"
|
|
426
|
+
}
|
|
427
|
+
],
|
|
428
|
+
"isUnique": false,
|
|
429
|
+
"concurrently": false,
|
|
430
|
+
"method": "btree",
|
|
431
|
+
"with": {}
|
|
432
|
+
}
|
|
433
|
+
},
|
|
412
434
|
"foreignKeys": {
|
|
413
435
|
"totp_recovery_code_id_totp_fkey": {
|
|
414
436
|
"name": "totp_recovery_code_id_totp_fkey",
|