@tstdl/base 0.93.84 → 0.93.86

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.
@@ -84,10 +84,10 @@ export declare class AuthenticationClientService<AdditionalTokenPayload extends
84
84
  */
85
85
  get definedTenantId(): string;
86
86
  /**
87
- * Get current subject or throw if not available
87
+ * Get current subjectId or throw if not available
88
88
  * @throws Will throw if subject is not available
89
89
  */
90
- get definedSubject(): string;
90
+ get definedSubjectId(): string;
91
91
  /** Whether a valid token is available (not undefined and not expired) */
92
92
  get hasValidToken(): boolean;
93
93
  constructor();
@@ -140,10 +140,10 @@ let AuthenticationClientService = class AuthenticationClientService {
140
140
  return this.definedToken.tenant;
141
141
  }
142
142
  /**
143
- * Get current subject or throw if not available
143
+ * Get current subjectId or throw if not available
144
144
  * @throws Will throw if subject is not available
145
145
  */
146
- get definedSubject() {
146
+ get definedSubjectId() {
147
147
  return this.definedToken.subject;
148
148
  }
149
149
  /** Whether a valid token is available (not undefined and not expired) */
@@ -1,13 +1,10 @@
1
1
  import { TenantEntity, type Uuid } from '../../orm/index.js';
2
2
  export declare class AuthenticationCredentials extends TenantEntity {
3
- subject: Uuid;
3
+ subjectId: Uuid;
4
+ /** The version of the hash algorithm used. */
4
5
  hashVersion: number;
5
- /**
6
- * The salt used to hash the secret.
7
- */
6
+ /** The salt used to hash the secret. */
8
7
  salt: Uint8Array<ArrayBuffer>;
9
- /**
10
- * The hashed secret.
11
- */
8
+ /** The hashed secret. */
12
9
  hash: Uint8Array<ArrayBuffer>;
13
10
  }
@@ -11,22 +11,19 @@ import { Table, TenantEntity, TenantReference, Unique, UuidProperty } from '../.
11
11
  import { Integer, Uint8ArrayProperty } from '../../schema/index.js';
12
12
  import { Subject } from './subject.model.js';
13
13
  let AuthenticationCredentials = class AuthenticationCredentials extends TenantEntity {
14
- subject;
14
+ subjectId;
15
+ /** The version of the hash algorithm used. */
15
16
  hashVersion;
16
- /**
17
- * The salt used to hash the secret.
18
- */
17
+ /** The salt used to hash the secret. */
19
18
  salt;
20
- /**
21
- * The hashed secret.
22
- */
19
+ /** The hashed secret. */
23
20
  hash;
24
21
  };
25
22
  __decorate([
26
23
  TenantReference(() => Subject),
27
24
  UuidProperty(),
28
25
  __metadata("design:type", String)
29
- ], AuthenticationCredentials.prototype, "subject", void 0);
26
+ ], AuthenticationCredentials.prototype, "subjectId", void 0);
30
27
  __decorate([
31
28
  Integer(),
32
29
  __metadata("design:type", Number)
@@ -41,6 +38,6 @@ __decorate([
41
38
  ], AuthenticationCredentials.prototype, "hash", void 0);
42
39
  AuthenticationCredentials = __decorate([
43
40
  Table('credentials', { schema: 'authentication' }),
44
- Unique(['tenantId', 'subject'])
41
+ Unique(['tenantId', 'subjectId'])
45
42
  ], AuthenticationCredentials);
46
43
  export { AuthenticationCredentials };
@@ -1,16 +1,13 @@
1
1
  import type { Timestamp, Uuid } from '../../orm/index.js';
2
2
  import { TenantEntity } from '../../orm/index.js';
3
3
  export declare class AuthenticationSession extends TenantEntity {
4
- subject: Uuid;
4
+ subjectId: Uuid;
5
5
  begin: Timestamp;
6
6
  end: Timestamp;
7
+ /** The version of the hash algorithm used. */
7
8
  refreshTokenHashVersion: number;
8
- /**
9
- * The salt used to hash the refresh token.
10
- */
9
+ /** The salt used to hash the refresh token. */
11
10
  refreshTokenSalt: Uint8Array<ArrayBuffer>;
12
- /**
13
- * The hashed refresh token.
14
- */
11
+ /** The hashed refresh token. */
15
12
  refreshTokenHash: Uint8Array<ArrayBuffer>;
16
13
  }
@@ -11,24 +11,21 @@ import { Table, TenantEntity, TenantReference, TimestampProperty, UuidProperty }
11
11
  import { Integer, Uint8ArrayProperty } from '../../schema/index.js';
12
12
  import { Subject } from './subject.model.js';
13
13
  let AuthenticationSession = class AuthenticationSession extends TenantEntity {
14
- subject;
14
+ subjectId;
15
15
  begin;
16
16
  end;
17
+ /** The version of the hash algorithm used. */
17
18
  refreshTokenHashVersion;
18
- /**
19
- * The salt used to hash the refresh token.
20
- */
19
+ /** The salt used to hash the refresh token. */
21
20
  refreshTokenSalt;
22
- /**
23
- * The hashed refresh token.
24
- */
21
+ /** The hashed refresh token. */
25
22
  refreshTokenHash;
26
23
  };
27
24
  __decorate([
28
25
  TenantReference(() => Subject),
29
26
  UuidProperty(),
30
27
  __metadata("design:type", String)
31
- ], AuthenticationSession.prototype, "subject", void 0);
28
+ ], AuthenticationSession.prototype, "subjectId", void 0);
32
29
  __decorate([
33
30
  TimestampProperty(),
34
31
  __metadata("design:type", Number)
@@ -2,9 +2,7 @@ import type { Record } from '../../types/index.js';
2
2
  import type { JwtToken, JwtTokenHeader } from '../../utils/jwt.js';
3
3
  import type { TokenPayloadBase } from './token-payload-base.model.js';
4
4
  export type TokenHeader = {
5
- /**
6
- * Token version.
7
- */
5
+ /** Token version. */
8
6
  v: number;
9
7
  };
10
8
  /**
@@ -18,42 +16,26 @@ export type Token<AdditionalTokenPayload extends Record = Record<never>> = JwtTo
18
16
  */
19
17
  export type TokenPayload<T extends Record = Record<never>> = T & TokenPayloadBase;
20
18
  export type RefreshToken = JwtToken<{
21
- /**
22
- * Expiration timestamp in seconds.
23
- */
19
+ /** Expiration timestamp in seconds. */
24
20
  exp: number;
25
- /**
26
- * The tenant id.
27
- */
21
+ /** The tenant id. */
28
22
  tenant: string;
29
- /**
30
- * The subject of the token.
31
- */
23
+ /** The subject of the token. */
32
24
  subject: string;
33
- /**
34
- * The subject of the impersonator, if any.
35
- */
25
+ /** The subject of the impersonator, if any. */
36
26
  impersonator?: string;
37
- /**
38
- * The id of the session.
39
- */
27
+ /** The id of the session. */
40
28
  session: string;
41
- /**
42
- * The secret to use for refreshing the token.
43
- */
29
+ /** The secret to use for refreshing the token. */
44
30
  secret: string;
45
31
  }>;
46
32
  export type SecretResetToken = JwtToken<{
47
- /**
48
- * Expiration timestamp in seconds.
49
- */
33
+ /** Issued at timestamp in seconds. */
34
+ iat: number;
35
+ /** Expiration timestamp in seconds. */
50
36
  exp: number;
51
- /**
52
- * The tenant id.
53
- */
37
+ /** The tenant id. */
54
38
  tenant: string;
55
- /**
56
- * The subject for which to reset the secret.
57
- */
39
+ /** The subject for which to reset the secret. */
58
40
  subject: string;
59
41
  }>;
@@ -15,6 +15,7 @@ export type AuthenticationAuditEvents = {
15
15
  };
16
16
  'refresh-failure': {
17
17
  reason: string;
18
+ sessionId: string | null;
18
19
  };
19
20
  'impersonate-success': {
20
21
  impersonatedSubjectId: string;
@@ -25,7 +25,7 @@ import { currentTimestamp, timestampToTimestampSeconds } from '../../utils/date-
25
25
  import { timingSafeBinaryEquals } from '../../utils/equals.js';
26
26
  import { createJwtTokenString } from '../../utils/jwt.js';
27
27
  import { getRandomBytes, getRandomString } from '../../utils/random.js';
28
- import { assertDefinedPass, isBinaryData, isString, isUndefined } from '../../utils/type-guards.js';
28
+ import { isBinaryData, isDefined, isString, isUndefined } from '../../utils/type-guards.js';
29
29
  import { millisecondsPerDay, millisecondsPerMinute } from '../../utils/units.js';
30
30
  import { AuthenticationCredentials, AuthenticationSession, Subject, User } from '../models/index.js';
31
31
  import { AuthenticationAncillaryService, GetTokenPayloadContextAction } from './authentication-ancillary.service.js';
@@ -63,6 +63,13 @@ export class AuthenticationServiceOptions {
63
63
  */
64
64
  secretResetTokenTimeToLive;
65
65
  }
66
+ const HASH_ITERATIONS = 250000;
67
+ const HASH_LENGTH_BITS = 512;
68
+ const HASH_LENGTH_BYTES = HASH_LENGTH_BITS / 8;
69
+ const JWT_ID_LENGTH = 24;
70
+ const REFRESH_TOKEN_SECRET_LENGTH = 64;
71
+ const SALT_LENGTH = 32;
72
+ const SIGNING_SECRETS_DERIVATION_ITERATIONS = 500000;
66
73
  const SIGNING_SECRETS_LENGTH = 64;
67
74
  /**
68
75
  * Handles authentication on server side.
@@ -136,18 +143,18 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
136
143
  if (options?.skipValidation != true) {
137
144
  await this.#authenticationSecretRequirementsValidator.validateSecretRequirements(secret);
138
145
  }
139
- const salt = getRandomBytes(32);
146
+ const salt = getRandomBytes(SALT_LENGTH);
140
147
  const hash = await this.getHash(secret, salt);
141
148
  await this.#credentialsRepository.transaction(async (tx) => {
142
- await this.#credentialsRepository.withTransaction(tx).upsert('subject', {
149
+ await this.#credentialsRepository.withTransaction(tx).upsert(['tenantId', 'subjectId'], {
143
150
  tenantId: subject.tenantId,
144
- subject: subject.id,
151
+ subjectId: subject.id,
145
152
  hashVersion: 1,
146
153
  salt,
147
154
  hash,
148
155
  });
149
156
  if (options?.skipSessionInvalidation != true) {
150
- await this.#sessionRepository.withTransaction(tx).updateManyByQuery({ tenantId: subject.tenantId, subject: subject.id }, { end: currentTimestamp() });
157
+ await this.#sessionRepository.withTransaction(tx).updateManyByQuery({ tenantId: subject.tenantId, subjectId: subject.id }, { end: currentTimestamp() });
151
158
  }
152
159
  });
153
160
  }
@@ -159,15 +166,18 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
159
166
  */
160
167
  async authenticate(subject, secret) {
161
168
  const actualSubject = await this.tryResolveSubject(subject);
162
- // Always try to load credentials, even if the subject is not resolved, to reduce information leakage.
163
- // If the subject is not resolved, we will create a new credentials entry with an empty salt and hash.
164
- // This way, we do not leak if the subject exists or not via timing attacks.
165
- const credentials = await this.#credentialsRepository.tryLoadByQuery({ tenantId: actualSubject?.tenantId ?? NIL_UUID, subject: actualSubject?.id ?? NIL_UUID })
166
- ?? { subject: actualSubject, salt: new Uint8Array(), hash: new Uint8Array() };
169
+ // we use a random uuid instead of null here to reduce timing attack surface by DB optimiziations
170
+ const queryTenantId = actualSubject?.tenantId ?? crypto.randomUUID();
171
+ const querySubjectId = actualSubject?.id ?? crypto.randomUUID();
172
+ const loadedCredentials = await this.#credentialsRepository.tryLoadByQuery({
173
+ tenantId: queryTenantId,
174
+ subjectId: querySubjectId,
175
+ });
176
+ const credentials = loadedCredentials ?? { salt: new Uint8Array(SALT_LENGTH), hash: new Uint8Array(HASH_LENGTH_BYTES) };
167
177
  const hash = await this.getHash(secret, credentials.salt);
168
178
  const valid = timingSafeBinaryEquals(hash, credentials.hash);
169
- if (valid) {
170
- return { success: true, subject: assertDefinedPass(actualSubject, 'Subject should be defined if authentication is valid but it is not.') };
179
+ if (valid && isDefined(actualSubject) && isDefined(loadedCredentials)) {
180
+ return { success: true, subject: actualSubject };
171
181
  }
172
182
  return { success: false };
173
183
  }
@@ -184,7 +194,7 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
184
194
  return await this.#sessionRepository.transaction(async (tx) => {
185
195
  const session = await this.#sessionRepository.withTransaction(tx).insert({
186
196
  tenantId: subject.tenantId,
187
- subject: subject.id,
197
+ subjectId: subject.id,
188
198
  begin: now,
189
199
  end,
190
200
  refreshTokenHashVersion: 0,
@@ -255,9 +265,9 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
255
265
  await this.#sessionRepository.update(sessionId, { end: now });
256
266
  await authAuditor.info('logout', {
257
267
  tenantId: session.tenantId,
258
- actor: session.subject,
268
+ actor: session.subjectId,
259
269
  actorType: ActorType.User,
260
- targetId: session.subject,
270
+ targetId: session.subjectId,
261
271
  targetType: 'User',
262
272
  details: { sessionId },
263
273
  });
@@ -282,12 +292,18 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
282
292
  throw new InvalidTokenError('Session is expired.');
283
293
  }
284
294
  if (!timingSafeBinaryEquals(hash, session.refreshTokenHash)) {
295
+ await this.endSession(sessionId, auditor);
296
+ await authAuditor.warn('refresh-failure', {
297
+ targetId: session.tenantId,
298
+ targetType: 'User',
299
+ details: { sessionId, reason: 'Token reuse detected. Session revoked.' },
300
+ });
285
301
  throw new InvalidTokenError('Invalid refresh token.');
286
302
  }
287
303
  const now = currentTimestamp();
288
304
  const impersonator = (options.omitImpersonator == true) ? undefined : validatedRefreshToken.payload.impersonator;
289
305
  const newEnd = now + this.refreshTokenTimeToLive;
290
- const subject = await this.#subjectRepository.loadByQuery({ tenantId: session.tenantId, id: session.subject });
306
+ const subject = await this.#subjectRepository.loadByQuery({ tenantId: session.tenantId, id: session.subjectId });
291
307
  const tokenPayload = await this.#authenticationAncillaryService?.getTokenPayload(subject, authenticationData, { action: GetTokenPayloadContextAction.Refresh });
292
308
  const { token, jsonToken } = await this.createToken({ additionalTokenPayload: tokenPayload, subject, sessionId, refreshTokenExpiration: newEnd, impersonator, timestamp: now });
293
309
  const newRefreshToken = await this.createRefreshToken(subject, sessionId, newEnd, { impersonator });
@@ -299,9 +315,9 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
299
315
  });
300
316
  await authAuditor.info('refresh-success', {
301
317
  tenantId: session.tenantId,
302
- actor: session.subject,
318
+ actor: session.subjectId,
303
319
  actorType: ActorType.User,
304
- targetId: session.subject,
320
+ targetId: session.subjectId,
305
321
  targetType: 'User',
306
322
  details: { sessionId },
307
323
  });
@@ -311,7 +327,7 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
311
327
  await authAuditor.warn('refresh-failure', {
312
328
  targetId: NIL_UUID,
313
329
  targetType: 'User',
314
- details: { reason: error.message },
330
+ details: { sessionId: null, reason: error.message },
315
331
  });
316
332
  throw error;
317
333
  }
@@ -458,6 +474,21 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
458
474
  const authAuditor = auditor.fork(AuthenticationService_1.name);
459
475
  try {
460
476
  const token = await this.validateSecretResetToken(tokenString);
477
+ const credentials = await this.#credentialsRepository.tryLoadByQuery({
478
+ tenantId: token.payload.tenant,
479
+ subjectId: token.payload.subject,
480
+ });
481
+ if (isDefined(credentials)) {
482
+ const lastUpdateSeconds = timestampToTimestampSeconds(credentials.metadata.revisionTimestamp);
483
+ if (token.payload.iat < lastUpdateSeconds) {
484
+ await authAuditor.info('reset-secret-failure', {
485
+ targetId: token.payload.subject,
486
+ targetType: 'User',
487
+ details: { reason: 'Token is invalid (credentials have already been changed).' },
488
+ });
489
+ throw new InvalidTokenError();
490
+ }
491
+ }
461
492
  const subject = await this.#subjectRepository.loadByQuery({ tenantId: token.payload.tenant, id: token.payload.subject });
462
493
  await this.setCredentials(subject, newSecret);
463
494
  await authAuditor.info('reset-secret-success', {
@@ -580,7 +611,7 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
580
611
  typ: 'JWT',
581
612
  };
582
613
  const payload = {
583
- jti: jwtId ?? getRandomString(24, Alphabet.LowerUpperCaseNumbers),
614
+ jti: jwtId ?? getRandomString(JWT_ID_LENGTH, Alphabet.LowerUpperCaseNumbers),
584
615
  iat: issuedAt ?? timestampToTimestampSeconds(timestamp),
585
616
  exp: expiration ?? timestampToTimestampSeconds(timestamp + this.tokenTimeToLive),
586
617
  refreshTokenExp: timestampToTimestampSeconds(refreshTokenExpiration),
@@ -607,8 +638,8 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
607
638
  * @returns The created refresh token.
608
639
  */
609
640
  async createRefreshToken(subject, sessionId, expirationTimestamp, options) {
610
- const secret = getRandomString(64, Alphabet.LowerUpperCaseNumbers);
611
- const salt = getRandomBytes(32);
641
+ const secret = getRandomString(REFRESH_TOKEN_SECRET_LENGTH, Alphabet.LowerUpperCaseNumbers);
642
+ const salt = getRandomBytes(SALT_LENGTH);
612
643
  const hash = await this.getHash(secret, salt);
613
644
  const jsonToken = {
614
645
  header: {
@@ -647,12 +678,14 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
647
678
  return subjects[0];
648
679
  }
649
680
  async createSecretResetToken(subject, expirationTimestamp) {
681
+ const iat = timestampToTimestampSeconds(currentTimestamp());
650
682
  const jsonToken = {
651
683
  header: {
652
684
  alg: 'HS256',
653
685
  typ: 'JWT',
654
686
  },
655
687
  payload: {
688
+ iat,
656
689
  exp: timestampToTimestampSeconds(expirationTimestamp),
657
690
  subject: subject.id,
658
691
  tenant: subject.tenantId,
@@ -663,9 +696,9 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
663
696
  }
664
697
  async deriveSigningSecrets(secret) {
665
698
  const key = await importPbkdf2Key(secret);
666
- const saltBase64 = await this.#keyValueStore.getOrSet('derivationSalt', encodeBase64(getRandomBytes(32)));
699
+ const saltBase64 = await this.#keyValueStore.getOrSet('derivationSalt', encodeBase64(getRandomBytes(SALT_LENGTH)));
667
700
  const salt = decodeBase64(saltBase64);
668
- const algorithm = { name: 'PBKDF2', hash: 'SHA-512', iterations: 500000, salt };
701
+ const algorithm = { name: 'PBKDF2', hash: 'SHA-512', iterations: SIGNING_SECRETS_DERIVATION_ITERATIONS, salt };
669
702
  const [derivedTokenSigningSecret, derivedRefreshTokenSigningSecret, derivedSecretResetTokenSigningSecret] = await deriveBytesMultiple(algorithm, key, 3, SIGNING_SECRETS_LENGTH);
670
703
  this.derivedTokenSigningSecret = derivedTokenSigningSecret;
671
704
  this.derivedRefreshTokenSigningSecret = derivedRefreshTokenSigningSecret;
@@ -673,7 +706,7 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
673
706
  }
674
707
  async getHash(secret, salt) {
675
708
  const key = await importPbkdf2Key(secret);
676
- const hash = await globalThis.crypto.subtle.deriveBits({ name: 'PBKDF2', hash: 'SHA-512', iterations: 250000, salt }, key, 512);
709
+ const hash = await globalThis.crypto.subtle.deriveBits({ name: 'PBKDF2', hash: 'SHA-512', iterations: HASH_ITERATIONS, salt }, key, HASH_LENGTH_BITS);
677
710
  return new Uint8Array(hash);
678
711
  }
679
712
  };
@@ -3,7 +3,7 @@ CREATE TYPE "authentication"."user_status" AS ENUM('active', 'suspended', 'pendi
3
3
  CREATE TABLE "authentication"."credentials" (
4
4
  "id" uuid DEFAULT gen_random_uuid() NOT NULL,
5
5
  "tenant_id" uuid NOT NULL,
6
- "subject" uuid NOT NULL,
6
+ "subject_id" uuid NOT NULL,
7
7
  "hash_version" integer NOT NULL,
8
8
  "salt" "bytea" NOT NULL,
9
9
  "hash" "bytea" NOT NULL,
@@ -13,13 +13,13 @@ CREATE TABLE "authentication"."credentials" (
13
13
  "delete_timestamp" timestamp with time zone,
14
14
  "attributes" jsonb DEFAULT '{}'::jsonb NOT NULL,
15
15
  CONSTRAINT "credentials_tenant_id_id_pk" PRIMARY KEY("tenant_id","id"),
16
- CONSTRAINT "credentials_tenant_id_subject_unique" UNIQUE("tenant_id","subject")
16
+ CONSTRAINT "credentials_tenant_id_subject_id_unique" UNIQUE("tenant_id","subject_id")
17
17
  );
18
18
  --> statement-breakpoint
19
19
  CREATE TABLE "authentication"."session" (
20
20
  "id" uuid DEFAULT gen_random_uuid() NOT NULL,
21
21
  "tenant_id" uuid NOT NULL,
22
- "subject" uuid NOT NULL,
22
+ "subject_id" uuid NOT NULL,
23
23
  "begin" timestamp with time zone NOT NULL,
24
24
  "end" timestamp with time zone NOT NULL,
25
25
  "refresh_token_hash_version" integer NOT NULL,
@@ -96,8 +96,8 @@ CREATE TABLE "authentication"."user" (
96
96
  CONSTRAINT "user_tenant_id_email_unique" UNIQUE("tenant_id","email")
97
97
  );
98
98
  --> statement-breakpoint
99
- ALTER TABLE "authentication"."credentials" ADD CONSTRAINT "credentials_id_subject_fkey" FOREIGN KEY ("tenant_id","subject") REFERENCES "authentication"."subject"("tenant_id","id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
100
- ALTER TABLE "authentication"."session" ADD CONSTRAINT "session_id_subject_fkey" FOREIGN KEY ("tenant_id","subject") REFERENCES "authentication"."subject"("tenant_id","id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
99
+ ALTER TABLE "authentication"."credentials" ADD CONSTRAINT "credentials_id_subject_fkey" FOREIGN KEY ("tenant_id","subject_id") REFERENCES "authentication"."subject"("tenant_id","id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
100
+ ALTER TABLE "authentication"."session" ADD CONSTRAINT "session_id_subject_fkey" FOREIGN KEY ("tenant_id","subject_id") REFERENCES "authentication"."subject"("tenant_id","id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
101
101
  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
102
102
  ALTER TABLE "authentication"."subject" ADD CONSTRAINT "subject_id_system_account_fkey" FOREIGN KEY ("tenant_id","system_account_id") REFERENCES "authentication"."system_account"("tenant_id","id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
103
103
  ALTER TABLE "authentication"."subject" ADD CONSTRAINT "subject_id_user_fkey" FOREIGN KEY ("tenant_id","user_id") REFERENCES "authentication"."user"("tenant_id","id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "9385eff0-25d7-4ef6-be4b-8019af0a1424",
2
+ "id": "685f89da-0f2a-4523-9cbc-c9daea5f70c2",
3
3
  "prevId": "00000000-0000-0000-0000-000000000000",
4
4
  "version": "7",
5
5
  "dialect": "postgresql",
@@ -21,8 +21,8 @@
21
21
  "primaryKey": false,
22
22
  "notNull": true
23
23
  },
24
- "subject": {
25
- "name": "subject",
24
+ "subject_id": {
25
+ "name": "subject_id",
26
26
  "type": "uuid",
27
27
  "primaryKey": false,
28
28
  "notNull": true
@@ -86,7 +86,7 @@
86
86
  "schemaTo": "authentication",
87
87
  "columnsFrom": [
88
88
  "tenant_id",
89
- "subject"
89
+ "subject_id"
90
90
  ],
91
91
  "columnsTo": [
92
92
  "tenant_id",
@@ -106,12 +106,12 @@
106
106
  }
107
107
  },
108
108
  "uniqueConstraints": {
109
- "credentials_tenant_id_subject_unique": {
110
- "name": "credentials_tenant_id_subject_unique",
109
+ "credentials_tenant_id_subject_id_unique": {
110
+ "name": "credentials_tenant_id_subject_id_unique",
111
111
  "nullsNotDistinct": false,
112
112
  "columns": [
113
113
  "tenant_id",
114
- "subject"
114
+ "subject_id"
115
115
  ]
116
116
  }
117
117
  },
@@ -136,8 +136,8 @@
136
136
  "primaryKey": false,
137
137
  "notNull": true
138
138
  },
139
- "subject": {
140
- "name": "subject",
139
+ "subject_id": {
140
+ "name": "subject_id",
141
141
  "type": "uuid",
142
142
  "primaryKey": false,
143
143
  "notNull": true
@@ -213,7 +213,7 @@
213
213
  "schemaTo": "authentication",
214
214
  "columnsFrom": [
215
215
  "tenant_id",
216
- "subject"
216
+ "subject_id"
217
217
  ],
218
218
  "columnsTo": [
219
219
  "tenant_id",
@@ -5,8 +5,8 @@
5
5
  {
6
6
  "idx": 0,
7
7
  "version": "7",
8
- "when": 1767838186408,
9
- "tag": "0000_majestic_proudstar",
8
+ "when": 1767839299143,
9
+ "tag": "0000_normal_paper_doll",
10
10
  "breakpoints": true
11
11
  }
12
12
  ]
@@ -28,6 +28,7 @@ import { configurePostgresQueue, migratePostgresQueueSchema } from '../../queue/
28
28
  import { configureDefaultSignalsImplementation } from '../../signals/implementation/configure.js';
29
29
  import { boolean, positiveInteger, string } from '../../utils/config-parser.js';
30
30
  import { TstdlCategoryParents, TstdlDocumentCategoryLabels, TstdlDocumentPropertyConfiguration, TstdlDocumentTypeCategories, TstdlDocumentTypeLabels, TstdlDocumentTypeProperties } from './categories-and-types.js';
31
+ import { configurePostgresCircuitBreaker, migratePostgresCircuitBreaker } from '../../circuit-breaker/postgres/module.js';
31
32
  const config = {
32
33
  database: {
33
34
  host: string('DATABASE_HOST', '127.0.0.1'),
@@ -87,6 +88,7 @@ async function bootstrap() {
87
88
  const injector = inject(Injector);
88
89
  configureNodeHttpServer();
89
90
  configurePostgresQueue();
91
+ configurePostgresCircuitBreaker();
90
92
  configureLocalMessageBus();
91
93
  configureDefaultSignalsImplementation();
92
94
  configureGenkit({
@@ -134,8 +136,9 @@ async function bootstrap() {
134
136
  apiKey: config.ai.apiKey,
135
137
  keyFile: config.ai.keyFile,
136
138
  });
137
- await runInInjectionContext(injector, async () => await migrateDocumentManagementSchema());
138
- await runInInjectionContext(injector, async () => await migratePostgresQueueSchema());
139
+ await runInInjectionContext(injector, migrateDocumentManagementSchema);
140
+ await runInInjectionContext(injector, migratePostgresCircuitBreaker);
141
+ await runInInjectionContext(injector, migratePostgresQueueSchema);
139
142
  }
140
143
  async function main() {
141
144
  const tenantId = '00000000-0000-0000-0000-000000000000';
package/orm/sqls.d.ts CHANGED
@@ -91,7 +91,7 @@ export declare function exclusiveNotNull(...columns: Column[]): SQL;
91
91
  * that defines the default condition to apply when a `Column` is provided in `conditionMapping`.
92
92
  * By default, it generates an `IS NOT NULL` check.
93
93
  */
94
- export declare function enumerationCaseWhen<T extends EnumerationObject>(enumeration: T, discriminator: Column, conditionMapping: Record<EnumerationValue<T>, Column | boolean | SQL>, defaultColumnCondition?: (column: Column) => SQL<unknown>): SQL;
94
+ export declare function enumerationCaseWhen<T extends EnumerationObject>(enumeration: T, discriminator: Column, conditionMapping: Record<EnumerationValue<T>, Column | [Column, ...Column[]] | boolean | SQL>, defaultColumnCondition?: (column: [Column, ...Column[]]) => SQL<unknown> | undefined): SQL;
95
95
  export declare function array<T>(values: SQL<T>[]): SQL<T[]>;
96
96
  export declare function array<T = unknown>(values: SQLChunk[]): SQL<T[]>;
97
97
  export declare function autoAlias<T>(column: AnyColumn<{
package/orm/sqls.js CHANGED
@@ -68,7 +68,7 @@ export function exclusiveNotNull(...columns) {
68
68
  * that defines the default condition to apply when a `Column` is provided in `conditionMapping`.
69
69
  * By default, it generates an `IS NOT NULL` check.
70
70
  */
71
- export function enumerationCaseWhen(enumeration, discriminator, conditionMapping, defaultColumnCondition = (column) => sqlIsNotNull(column)) {
71
+ export function enumerationCaseWhen(enumeration, discriminator, conditionMapping, defaultColumnCondition = (column) => isArray(column) ? and(...column.map((col) => sqlIsNotNull(col))) : sqlIsNotNull(column)) {
72
72
  const whens = [];
73
73
  for (const [key, value] of objectEntries(conditionMapping)) {
74
74
  const condition = match(value)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tstdl/base",
3
- "version": "0.93.84",
3
+ "version": "0.93.86",
4
4
  "author": "Patrick Hein",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -145,7 +145,7 @@
145
145
  "peerDependencies": {
146
146
  "@genkit-ai/google-genai": "^1.27",
147
147
  "@google-cloud/storage": "^7.18",
148
- "@google/genai": "^1.34",
148
+ "@google/genai": "^1.35",
149
149
  "@toon-format/toon": "^2.1.0",
150
150
  "@tstdl/angular": "^0.93",
151
151
  "@zxcvbn-ts/core": "^3.0",
@@ -174,7 +174,7 @@
174
174
  }
175
175
  },
176
176
  "devDependencies": {
177
- "@stylistic/eslint-plugin": "5.6",
177
+ "@stylistic/eslint-plugin": "5.7",
178
178
  "@types/koa__router": "12.0",
179
179
  "@types/luxon": "3.7",
180
180
  "@types/mjml": "4.7",