@contember/engine-tenant-api 2.2.0-alpha.7 → 2.2.0-alpha.9

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 (79) hide show
  1. package/dist/development/src/TenantContainer.cjs +5 -2
  2. package/dist/development/src/TenantContainer.cjs.map +1 -1
  3. package/dist/development/src/TenantContainer.js +5 -2
  4. package/dist/development/src/TenantContainer.js.map +1 -1
  5. package/dist/development/src/model/commands/apiKey/ProlongApiKeyCommand.cjs +5 -8
  6. package/dist/development/src/model/commands/apiKey/ProlongApiKeyCommand.cjs.map +1 -1
  7. package/dist/development/src/model/commands/apiKey/ProlongApiKeyCommand.js +5 -8
  8. package/dist/development/src/model/commands/apiKey/ProlongApiKeyCommand.js.map +1 -1
  9. package/dist/development/src/model/commands/authLog/CreateAuthLogEntryCommand.cjs.map +1 -1
  10. package/dist/development/src/model/commands/authLog/CreateAuthLogEntryCommand.js.map +1 -1
  11. package/dist/development/src/model/service/IDPSignInManager.cjs +23 -15
  12. package/dist/development/src/model/service/IDPSignInManager.cjs.map +1 -1
  13. package/dist/development/src/model/service/IDPSignInManager.js +23 -15
  14. package/dist/development/src/model/service/IDPSignInManager.js.map +1 -1
  15. package/dist/development/src/model/service/apiKey/ApiKeyManager.cjs +16 -3
  16. package/dist/development/src/model/service/apiKey/ApiKeyManager.cjs.map +1 -1
  17. package/dist/development/src/model/service/apiKey/ApiKeyManager.js +16 -3
  18. package/dist/development/src/model/service/apiKey/ApiKeyManager.js.map +1 -1
  19. package/dist/development/src/model/service/idp/IdpSessionRevalidator.cjs +42 -31
  20. package/dist/development/src/model/service/idp/IdpSessionRevalidator.cjs.map +1 -1
  21. package/dist/development/src/model/service/idp/IdpSessionRevalidator.js +42 -31
  22. package/dist/development/src/model/service/idp/IdpSessionRevalidator.js.map +1 -1
  23. package/dist/production/src/TenantContainer.cjs +5 -2
  24. package/dist/production/src/TenantContainer.cjs.map +1 -1
  25. package/dist/production/src/TenantContainer.js +5 -2
  26. package/dist/production/src/TenantContainer.js.map +1 -1
  27. package/dist/production/src/model/commands/apiKey/ProlongApiKeyCommand.cjs +5 -8
  28. package/dist/production/src/model/commands/apiKey/ProlongApiKeyCommand.cjs.map +1 -1
  29. package/dist/production/src/model/commands/apiKey/ProlongApiKeyCommand.js +5 -8
  30. package/dist/production/src/model/commands/apiKey/ProlongApiKeyCommand.js.map +1 -1
  31. package/dist/production/src/model/commands/authLog/CreateAuthLogEntryCommand.cjs.map +1 -1
  32. package/dist/production/src/model/commands/authLog/CreateAuthLogEntryCommand.js.map +1 -1
  33. package/dist/production/src/model/service/IDPSignInManager.cjs +23 -15
  34. package/dist/production/src/model/service/IDPSignInManager.cjs.map +1 -1
  35. package/dist/production/src/model/service/IDPSignInManager.js +23 -15
  36. package/dist/production/src/model/service/IDPSignInManager.js.map +1 -1
  37. package/dist/production/src/model/service/apiKey/ApiKeyManager.cjs +16 -3
  38. package/dist/production/src/model/service/apiKey/ApiKeyManager.cjs.map +1 -1
  39. package/dist/production/src/model/service/apiKey/ApiKeyManager.js +16 -3
  40. package/dist/production/src/model/service/apiKey/ApiKeyManager.js.map +1 -1
  41. package/dist/production/src/model/service/idp/IdpSessionRevalidator.cjs +42 -31
  42. package/dist/production/src/model/service/idp/IdpSessionRevalidator.cjs.map +1 -1
  43. package/dist/production/src/model/service/idp/IdpSessionRevalidator.js +42 -31
  44. package/dist/production/src/model/service/idp/IdpSessionRevalidator.js.map +1 -1
  45. package/dist/tests/cases/integration/mocked/sql/sqlTransaction.d.ts +5 -0
  46. package/dist/tests/cases/integration/mocked/sql/sqlTransaction.d.ts.map +1 -1
  47. package/dist/tests/cases/unit/apiKey/verifyAndProlongReplica.test.d.ts +2 -0
  48. package/dist/tests/cases/unit/apiKey/verifyAndProlongReplica.test.d.ts.map +1 -0
  49. package/dist/tests/src/IdPMock.d.ts.map +1 -1
  50. package/dist/tests/src/testTenant.d.ts.map +1 -1
  51. package/dist/tests/tsconfig.tsbuildinfo +1 -1
  52. package/dist/types/TenantContainer.d.ts +2 -0
  53. package/dist/types/TenantContainer.d.ts.map +1 -1
  54. package/dist/types/model/commands/apiKey/ProlongApiKeyCommand.d.ts +5 -3
  55. package/dist/types/model/commands/apiKey/ProlongApiKeyCommand.d.ts.map +1 -1
  56. package/dist/types/model/commands/authLog/CreateAuthLogEntryCommand.d.ts +1 -0
  57. package/dist/types/model/commands/authLog/CreateAuthLogEntryCommand.d.ts.map +1 -1
  58. package/dist/types/model/service/IDPSignInManager.d.ts +3 -1
  59. package/dist/types/model/service/IDPSignInManager.d.ts.map +1 -1
  60. package/dist/types/model/service/apiKey/ApiKeyManager.d.ts +1 -0
  61. package/dist/types/model/service/apiKey/ApiKeyManager.d.ts.map +1 -1
  62. package/dist/types/model/service/idp/IdpSessionRevalidator.d.ts +3 -1
  63. package/dist/types/model/service/idp/IdpSessionRevalidator.d.ts.map +1 -1
  64. package/dist/types/tsconfig.tsbuildinfo +1 -1
  65. package/package.json +12 -12
  66. package/src/TenantContainer.ts +7 -2
  67. package/src/model/commands/apiKey/ProlongApiKeyCommand.ts +10 -11
  68. package/src/model/commands/authLog/CreateAuthLogEntryCommand.ts +1 -0
  69. package/src/model/service/IDPSignInManager.ts +26 -14
  70. package/src/model/service/apiKey/ApiKeyManager.ts +24 -3
  71. package/src/model/service/idp/IdpSessionRevalidator.ts +43 -28
  72. package/tests/cases/integration/mocked/signInIdpMutation.test.ts +100 -1
  73. package/tests/cases/integration/mocked/sql/sqlTransaction.ts +16 -0
  74. package/tests/cases/unit/apiKey/prolongApiKeyCommand.test.ts +19 -9
  75. package/tests/cases/unit/apiKey/unpersistedApiKeyManager.test.ts +2 -3
  76. package/tests/cases/unit/apiKey/verifyAndProlongReplica.test.ts +170 -0
  77. package/tests/cases/unit/idp/idpSessionRevalidator.test.ts +37 -4
  78. package/tests/src/IdPMock.ts +2 -0
  79. package/tests/src/testTenant.ts +1 -0
@@ -0,0 +1,170 @@
1
+ import { expect, test } from 'bun:test'
2
+ import {
3
+ ApiKey,
4
+ ApiKeyManager,
5
+ ApiKeyService,
6
+ AuthLogService,
7
+ AuthPolicyResolver,
8
+ computeTokenHash,
9
+ DatabaseContext,
10
+ IDPClaimSyncService,
11
+ IDPHandlerRegistry,
12
+ IdpSessionRevalidator,
13
+ Providers,
14
+ VerifyErrorCode,
15
+ } from '../../../../src/index.js'
16
+ import { Connection } from '@contember/database'
17
+ import { createConnectionMock, ExpectedQuery } from '@contember/database-tester'
18
+ import { createLogger, TestLoggerHandler } from '@contember/logger'
19
+ import { emptySchema } from '@contember/schema-utils'
20
+
21
+ const TOKEN = '0000000000000000000000000000000000000000'
22
+ const NOW = new Date('2026-05-21T12:00:00Z')
23
+
24
+ const providers: Providers = {
25
+ bcrypt: () => Promise.resolve('x'),
26
+ bcryptCompare: () => Promise.resolve(true),
27
+ now: () => NOW,
28
+ randomBytes: () => Promise.resolve(Buffer.alloc(0)),
29
+ uuid: () => '00000000-0000-0000-0000-000000000000',
30
+ decrypt: () => {
31
+ throw new Error('not supported')
32
+ },
33
+ encrypt: () => {
34
+ throw new Error('not supported')
35
+ },
36
+ encryptionEnabled: false,
37
+ hash: () => Buffer.alloc(0),
38
+ }
39
+
40
+ const apiKeyRow = {
41
+ id: 'api-key-id',
42
+ type: ApiKey.Type.SESSION,
43
+ identity_id: 'identity-id',
44
+ disabled_at: null,
45
+ expires_at: new Date(NOW.getTime() + 30 * 60_000),
46
+ roles: ['editor'],
47
+ expiration: 30,
48
+ person_id: 'person-id',
49
+ last_ip: null,
50
+ last_user_agent: null,
51
+ last_used_at: null,
52
+ trust_forwarded_info: false,
53
+ issued_at: null,
54
+ idle_timeout: null,
55
+ max_expires_at: null,
56
+ }
57
+
58
+ const selectByToken = (rows: (typeof apiKeyRow)[]): ExpectedQuery => ({
59
+ sql:
60
+ `select "api_key"."id", "api_key"."type", "api_key"."identity_id", "api_key"."disabled_at", "api_key"."expires_at", "identity"."roles", "api_key"."expiration", "person"."id" as "person_id", "api_key"."last_ip", "api_key"."last_user_agent", "api_key"."last_used_at", "api_key"."trust_forwarded_info", "api_key"."issued_at", "api_key"."idle_timeout", "api_key"."max_expires_at" from "tenant"."api_key" inner join "tenant"."identity" as "identity" on "api_key"."identity_id" = "identity"."id" left join "tenant"."person" as "person" on "person"."identity_id" = "identity"."id" where "token_hash" = ?`,
61
+ parameters: [computeTokenHash(TOKEN)],
62
+ response: { rows },
63
+ })
64
+
65
+ // The prolong UPDATE runs detached on the primary once a key is accepted.
66
+ const prolongUpdate: ExpectedQuery = {
67
+ sql: `update "tenant"."api_key" set "expires_at" = ? where "id" = ?`,
68
+ parameters: [(val: unknown) => val instanceof Date, 'api-key-id'],
69
+ response: { rowCount: 1 },
70
+ }
71
+
72
+ const selectIdpSession = (rows: Record<string, unknown>[]): ExpectedQuery => ({
73
+ sql:
74
+ `select "idp_session"."id", "idp_session"."identity_provider_id", "idp_session"."idp_session_id", "idp_session"."tokens", "idp_session"."tokens_version", "idp_session"."idp_expires_at", "idp_session"."token_obtained_at", "idp_session"."last_validated_at", "idp_session"."created_at", "identity_provider"."type" as "provider_type", "identity_provider"."configuration" as "provider_configuration", "identity_provider"."disabled_at" as "provider_disabled_at" from "tenant"."idp_session" inner join "tenant"."identity_provider" as "identity_provider" on "idp_session"."identity_provider_id" = "identity_provider"."id" where "api_key_id" = ?`,
75
+ parameters: ['api-key-id'],
76
+ response: { rows },
77
+ })
78
+
79
+ const disableUpdate: ExpectedQuery = {
80
+ sql: `update "tenant"."api_key" set "disabled_at" = ? where "id" = ?`,
81
+ parameters: [(val: unknown) => val instanceof Date, 'api-key-id'],
82
+ response: { rowCount: 1 },
83
+ }
84
+
85
+ const authLogInsert: ExpectedQuery = {
86
+ sql:
87
+ `insert into "tenant"."person_auth_log" ("id", "invoked_by_id", "person_id", "type", "success", "error_code", "identity_provider_id", "metadata", "event_data") values (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
88
+ response: { rowCount: 1 },
89
+ }
90
+
91
+ const createDbContext = (connection: Connection.ConnectionType) =>
92
+ new DatabaseContext(connection.createClient('tenant', { module: 'tenant' }), providers)
93
+
94
+ const verify = async (primary: Connection.ConnectionType, replica: Connection.ConnectionType, idpSessionRevalidator?: IdpSessionRevalidator) => {
95
+ const manager = new ApiKeyManager(new ApiKeyService(), new AuthPolicyResolver(), new AuthLogService(), undefined, idpSessionRevalidator)
96
+ const response = await manager.verifyAndProlong(createDbContext(primary), createDbContext(replica), TOKEN)
97
+ // Let the setImmediate-scheduled prolong run before the connection mocks are torn down.
98
+ await new Promise(resolve => setImmediate(resolve))
99
+ return response
100
+ }
101
+
102
+ test('a key not yet replicated is found on the primary', async () => {
103
+ const primary = createConnectionMock([selectByToken([apiKeyRow]), prolongUpdate])
104
+ const replica = createConnectionMock([selectByToken([])])
105
+
106
+ const response = await verify(primary, replica)
107
+ expect(response.ok).toBe(true)
108
+ if (response.ok) {
109
+ expect(response.result.apiKeyId).toBe('api-key-id')
110
+ }
111
+ })
112
+
113
+ test('a key found on the replica does not query the primary', async () => {
114
+ const primary = createConnectionMock([prolongUpdate])
115
+ const replica = createConnectionMock([selectByToken([apiKeyRow])])
116
+
117
+ const response = await verify(primary, replica)
118
+ expect(response.ok).toBe(true)
119
+ })
120
+
121
+ test('a key missing on both the replica and the primary is not found', async () => {
122
+ const primary = createConnectionMock([selectByToken([])])
123
+ const replica = createConnectionMock([selectByToken([])])
124
+
125
+ const response = await verify(primary, replica)
126
+ expect(response.ok).toBe(false)
127
+ if (!response.ok) {
128
+ expect(response.error).toBe(VerifyErrorCode.NOT_FOUND)
129
+ }
130
+ })
131
+
132
+ test('without a replica, a missing key is looked up only once', async () => {
133
+ const connection = createConnectionMock([selectByToken([])])
134
+
135
+ const response = await verify(connection, connection)
136
+ expect(response.ok).toBe(false)
137
+ })
138
+
139
+ test('a key found only on the primary reads its IdP session from the primary', async () => {
140
+ const disabledIdpSession = {
141
+ id: 'idp-session-id',
142
+ identity_provider_id: 'idp-id',
143
+ idp_session_id: null,
144
+ tokens: null,
145
+ tokens_version: null,
146
+ idp_expires_at: null,
147
+ token_obtained_at: NOW,
148
+ last_validated_at: NOW,
149
+ created_at: NOW,
150
+ provider_type: 'oidc',
151
+ provider_configuration: { revalidation: { enabled: true } },
152
+ provider_disabled_at: NOW,
153
+ }
154
+ const primary = createConnectionMock([selectByToken([apiKeyRow]), selectIdpSession([disabledIdpSession]), disableUpdate, authLogInsert])
155
+ const replica = createConnectionMock([selectByToken([]), selectIdpSession([])])
156
+ const logHandler = new TestLoggerHandler()
157
+ const idpSessionRevalidator = new IdpSessionRevalidator(
158
+ new IDPHandlerRegistry(),
159
+ new IDPClaimSyncService({ getSchema: () => Promise.resolve(emptySchema) }),
160
+ createLogger(logHandler),
161
+ )
162
+
163
+ const response = await verify(primary, replica, idpSessionRevalidator)
164
+ expect(response.ok).toBe(false)
165
+ if (!response.ok) {
166
+ expect(response.error).toBe(VerifyErrorCode.DISABLED)
167
+ }
168
+ // The revocation audit entry is best-effort and only logged on failure.
169
+ expect(logHandler.messages).toEqual([])
170
+ })
@@ -9,6 +9,7 @@ import { CreateAuthLogEntryCommand } from '../../../../src/model/commands/authLo
9
9
  import { ProjectBySlugQuery, ProjectMembershipByIdentityQuery } from '../../../../src/model/queries/index.js'
10
10
  import { Schema } from '@contember/schema'
11
11
  import { emptySchema } from '@contember/schema-utils'
12
+ import { createLogger, Logger, TestLoggerHandler } from '@contember/logger'
12
13
 
13
14
  const NOW = new Date('2026-05-26T12:00:00Z')
14
15
  const t = (iso: string) => new Date(`2026-05-26T${iso}Z`)
@@ -20,8 +21,8 @@ const t = (iso: string) => new Date(`2026-05-26T${iso}Z`)
20
21
  // happy-path claim tests below grant the plain `editor` role on the existing `demo` project, so they need a
21
22
  // resolvable schema that defines that role (otherwise the apply-time backstop would drop the grant).
22
23
  const claimSyncSchema: Schema = { ...emptySchema, acl: { roles: { editor: { stages: '*', entities: {}, variables: {} } } } }
23
- const makeRevalidator = (registry: IDPHandlerRegistry) =>
24
- new IdpSessionRevalidator(registry, new IDPClaimSyncService({ getSchema: () => Promise.resolve(claimSyncSchema) }))
24
+ const makeRevalidator = (registry: IDPHandlerRegistry, logger: Logger = createLogger(new TestLoggerHandler())) =>
25
+ new IdpSessionRevalidator(registry, new IDPClaimSyncService({ getSchema: () => Promise.resolve(claimSyncSchema) }), logger)
25
26
 
26
27
  const apiKeyRow: any = {
27
28
  id: 'api-key-1',
@@ -48,7 +49,7 @@ const baseRow = (overrides: Partial<IdpSessionRow> = {}): IdpSessionRow => ({
48
49
  ...overrides,
49
50
  })
50
51
 
51
- const createHarness = (row: IdpSessionRow | null, opts: { claim?: boolean } = {}) => {
52
+ const createHarness = (row: IdpSessionRow | null, opts: { claim?: boolean; failAudit?: boolean } = {}) => {
52
53
  const executed: any[] = []
53
54
  const commandBus = {
54
55
  execute: async (command: any) => {
@@ -56,6 +57,9 @@ const createHarness = (row: IdpSessionRow | null, opts: { claim?: boolean } = {}
56
57
  if (command instanceof ClaimIdpRevalidationCommand) {
57
58
  return opts.claim ?? true
58
59
  }
60
+ if (opts.failAudit && command instanceof CreateAuthLogEntryCommand) {
61
+ throw new Error('audit insert failed')
62
+ }
59
63
  if (command instanceof DisableApiKeyCommand) {
60
64
  return true
61
65
  }
@@ -155,7 +159,7 @@ describe('IdpSessionRevalidator — lifetime-driven phases', () => {
155
159
  expect((log as any).data.success).toBe(false)
156
160
  expect((log as any).data.errorCode).toBe('invalid_grant')
157
161
  // the reason is the action payload → event_data (where the rest of the audit system reads it)
158
- expect((log as any).data.eventData).toEqual({ reason: 'invalid_grant' })
162
+ expect((log as any).data.eventData).toEqual({ reason: 'invalid_grant', apiKeyId: 'api-key-1' })
159
163
  expect((log as any).data.metadata).toBeUndefined()
160
164
  })
161
165
 
@@ -269,6 +273,8 @@ describe('IdpSessionRevalidator — audit logging', () => {
269
273
  expect(log).toBeDefined()
270
274
  expect((log as any).data.type).toBe('idp_session_revalidated')
271
275
  expect((log as any).data.success).toBe(true)
276
+ expect((log as any).data.personTokenId).toBeUndefined()
277
+ expect((log as any).data.eventData).toEqual({ apiKeyId: 'api-key-1' })
272
278
  })
273
279
 
274
280
  test('records the originating request IP / user agent on both revoke and revalidated entries', async () => {
@@ -280,6 +286,8 @@ describe('IdpSessionRevalidator — audit logging', () => {
280
286
  const revokeLog = revoked.executed.find(c => c instanceof CreateAuthLogEntryCommand) as any
281
287
  expect(revokeLog.data.ipAddress).toBe('203.0.113.7')
282
288
  expect(revokeLog.data.userAgent).toBe('Mozilla/5.0 test')
289
+ expect(revokeLog.data.personTokenId).toBeUndefined()
290
+ expect(revokeLog.data.eventData).toEqual({ reason: 'invalid_grant', apiKeyId: 'api-key-1' })
283
291
 
284
292
  const rotated = createHarness(baseRow({ session: { tokens: { refresh_token: 'r' }, expiresAt: t('11:30:00') } }))
285
293
  await makeRevalidator(
@@ -304,6 +312,8 @@ describe('IdpSessionRevalidator — audit logging', () => {
304
312
  expect(log.data.errorCode).toBe('revalidation_error')
305
313
  expect(log.data.success).toBe(true) // fail-open marker, not a security failure
306
314
  expect(log.data.ipAddress).toBe('203.0.113.7')
315
+ expect(log.data.personTokenId).toBeUndefined()
316
+ expect(log.data.eventData).toEqual({ apiKeyId: 'api-key-1' })
307
317
  })
308
318
 
309
319
  test('corrupt stored config (fail open) is audited as idp_session_revalidation_failed / config_invalid', async () => {
@@ -352,6 +362,27 @@ describe('IdpSessionRevalidator — audit logging', () => {
352
362
  expect(out).toBe('valid')
353
363
  })
354
364
 
365
+ // person_auth_log.person_token_id references person_token(id), so an api_key id there fails the FK
366
+ // and the insert throws. The key is already disabled at that point: the request must still be
367
+ // answered as revoked, not surface the audit failure (#964).
368
+ for (
369
+ const [name, row] of [
370
+ ['blocking revocation', baseRow({ session: { tokens: { refresh_token: 'r' }, expiresAt: t('11:30:00') } })],
371
+ ['disabled provider', baseRow({ providerDisabledAt: t('11:30:00') })],
372
+ ] as const
373
+ ) {
374
+ test(`a failing audit write on ${name} still returns revoked and logs the error`, async () => {
375
+ const h = createHarness(row, { failAudit: true })
376
+ const logs = new TestLoggerHandler()
377
+ const out = await makeRevalidator(registryWith(async () => ({ status: 'revoked', reason: 'invalid_grant' })), createLogger(logs))
378
+ .revalidate(h.dbContext, h.readDbContext, apiKeyRow)
379
+ expect(out).toBe('revoked')
380
+ expect(h.executed.some(c => c instanceof DisableApiKeyCommand)).toBe(true)
381
+ expect(h.executed.some(c => c instanceof CreateAuthLogEntryCommand && (c as any).data.type === 'idp_session_revoked')).toBe(true)
382
+ expect(logs.messages.some(m => m.level.name === 'error' && m.error instanceof Error && m.error.message === 'audit insert failed')).toBe(true)
383
+ })
384
+ }
385
+
355
386
  test('a no-op probe (valid, no rotated session) is NOT logged', async () => {
356
387
  // userinfo / introspection return valid without an idpSession; logging every such tick
357
388
  // (every minInterval) would flood the audit log, so it must stay silent.
@@ -461,6 +492,7 @@ describe('IdpSessionRevalidator — A09 claim mapping on refresh', () => {
461
492
  expect(log).toBeDefined()
462
493
  expect(log.data.success).toBe(true)
463
494
  expect(log.data.eventData.after.memberships).toEqual([{ project: 'demo', role: 'editor', variables: [] }])
495
+ expect(log.data.personTokenId).toBeUndefined()
464
496
  })
465
497
 
466
498
  test('sticky mapping is skipped on refresh (always-only) — no membership write, no audit', async () => {
@@ -484,6 +516,7 @@ describe('IdpSessionRevalidator — A09 claim mapping on refresh', () => {
484
516
  const log = h.executed.find(c => c instanceof CreateAuthLogEntryCommand && (c as any).data.type === 'idp_role_mapping_failed') as any
485
517
  expect(log).toBeDefined()
486
518
  expect(log.data.success).toBe(true)
519
+ expect(log.data.personTokenId).toBeUndefined()
487
520
  })
488
521
 
489
522
  test('refresh without fresh claims does not run claim sync', async () => {
@@ -17,6 +17,8 @@ export class IdPMock implements IdentityProviderHandler<any> {
17
17
  // mirroring how real providers spread raw OIDC claims into the IDPResponse — lets tests
18
18
  // drive claim-mapping (A09). `claimMapping` itself lives on the IdP config, not here.
19
19
  ...(configuration.claims ?? {}),
20
+ // Lets tests drive the federated-session branch of sign-in (re-validation state).
21
+ ...(configuration.idpSession ? { idpSession: configuration.idpSession } : {}),
20
22
  })
21
23
  }
22
24
 
@@ -143,6 +143,7 @@ export const executeTenantTest = async (test: Test) => {
143
143
  connection,
144
144
  readConnection: connection,
145
145
  cryptoProviders: providers,
146
+ logger: createLogger(new JsonStreamLoggerHandler(process.stderr)),
146
147
  })
147
148
  .replaceService('mailer', () => mailer)
148
149
  .setupService('idpRegistry', reg => {