@open-mercato/enterprise 0.6.8-develop.7100.1.fbf66fca35 → 0.7.0

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 (36) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/record_locks/backend/settings/record-locks/page.js +2 -2
  3. package/dist/modules/record_locks/backend/settings/record-locks/page.js.map +2 -2
  4. package/dist/modules/security/api/interceptors.js +2 -5
  5. package/dist/modules/security/api/interceptors.js.map +2 -2
  6. package/dist/modules/security/api/mfa/_shared.js +0 -1
  7. package/dist/modules/security/api/mfa/_shared.js.map +2 -2
  8. package/dist/modules/security/data/entities.js +0 -18
  9. package/dist/modules/security/data/entities.js.map +2 -2
  10. package/dist/modules/security/index.js +0 -3
  11. package/dist/modules/security/index.js.map +2 -2
  12. package/dist/modules/security/lib/enforcement-redirect.js +2 -28
  13. package/dist/modules/security/lib/enforcement-redirect.js.map +2 -2
  14. package/dist/modules/security/lib/security-config.js +0 -27
  15. package/dist/modules/security/lib/security-config.js.map +2 -2
  16. package/dist/modules/security/services/SudoChallengeService.js +41 -130
  17. package/dist/modules/security/services/SudoChallengeService.js.map +2 -2
  18. package/dist/modules/sso/backend/page.js +0 -1
  19. package/dist/modules/sso/backend/page.js.map +2 -2
  20. package/package.json +6 -7
  21. package/src/modules/record_locks/backend/settings/record-locks/page.tsx +4 -4
  22. package/src/modules/security/api/interceptors.ts +2 -6
  23. package/src/modules/security/api/mfa/_shared.ts +0 -1
  24. package/src/modules/security/data/entities.ts +0 -18
  25. package/src/modules/security/index.ts +0 -4
  26. package/src/modules/security/lib/enforcement-redirect.ts +2 -30
  27. package/src/modules/security/lib/security-config.ts +0 -29
  28. package/src/modules/security/migrations/.snapshot-open-mercato.json +276 -849
  29. package/src/modules/security/services/SudoChallengeService.ts +49 -170
  30. package/src/modules/sso/backend/page.tsx +0 -2
  31. package/dist/modules/security/lib/mfaCompletionRoutes.js +0 -17
  32. package/dist/modules/security/lib/mfaCompletionRoutes.js.map +0 -7
  33. package/dist/modules/security/migrations/Migration20260811143757_security.js +0 -13
  34. package/dist/modules/security/migrations/Migration20260811143757_security.js.map +0 -7
  35. package/src/modules/security/lib/mfaCompletionRoutes.ts +0 -25
  36. package/src/modules/security/migrations/Migration20260811143757_security.ts +0 -13
@@ -24,7 +24,7 @@ import type { MfaService } from './MfaService'
24
24
  import type { MfaVerificationService } from './MfaVerificationService'
25
25
  import { sudoTargets as defaultSudoTargets } from '../security.sudo'
26
26
  import type { SecurityModuleConfig } from '../lib/security-config'
27
- import { emitMfaEmergencyBypassActiveWarning, readSecurityModuleConfig } from '../lib/security-config'
27
+ import { readSecurityModuleConfig } from '../lib/security-config'
28
28
 
29
29
  type SudoMethod = 'password' | 'mfa'
30
30
 
@@ -56,12 +56,6 @@ type UserScope = {
56
56
  organizationId: string | null
57
57
  }
58
58
 
59
- type BoundPendingSudoSession = SudoSession & {
60
- targetIdentifier: string
61
- sudoConfigId: string
62
- sudoConfigUpdatedAt: Date
63
- }
64
-
65
59
  export type SudoAuthScope = {
66
60
  tenantId: string | null
67
61
  organizationId?: string | null
@@ -137,7 +131,21 @@ export class SudoChallengeService {
137
131
  organizationId?: string | null,
138
132
  ): Promise<SudoProtectionResolution> {
139
133
  await this.ensureDeveloperDefaultsRegistered()
140
- return this.resolveProtection(this.em, targetIdentifier, tenantId ?? null, organizationId ?? null)
134
+
135
+ const candidates = await this.em.find(SudoChallengeConfig, {
136
+ targetIdentifier,
137
+ deletedAt: null,
138
+ })
139
+
140
+ const resolved = candidates
141
+ .filter((config) => this.matchesScope(config, tenantId ?? null, organizationId ?? null))
142
+ .sort((left, right) => this.compareConfigPriority(left, right, tenantId ?? null, organizationId ?? null))[0]
143
+
144
+ if (!resolved || !resolved.isEnabled) {
145
+ return { protected: false }
146
+ }
147
+
148
+ return { protected: true, config: resolved }
141
149
  }
142
150
 
143
151
  async initiate(
@@ -151,24 +159,23 @@ export class SudoChallengeService {
151
159
  availableMfaMethods?: SudoAvailableMethod[]
152
160
  expiresAt?: Date
153
161
  }> {
154
- const user = await this.findUserScope(userId)
155
- if (!user?.tenantId) {
156
- throw new SudoChallengeServiceError('User not found', 404)
157
- }
158
- const scopeTenantId = options?.tenantId !== undefined ? options.tenantId : user.tenantId
159
- const scopeOrganizationId = options?.organizationId !== undefined ? options.organizationId : user.organizationId
160
162
  const protection = await this.isProtected(
161
163
  targetIdentifier,
162
- scopeTenantId,
163
- scopeOrganizationId,
164
+ options?.tenantId ?? null,
165
+ options?.organizationId ?? null,
164
166
  )
165
167
 
166
168
  if (!protection.protected || !protection.config) {
167
169
  return { required: false }
168
170
  }
169
171
 
172
+ const user = await this.findUserScope(userId)
173
+ if (!user?.tenantId) {
174
+ throw new SudoChallengeServiceError('User not found', 404)
175
+ }
176
+
170
177
  const userMethods = await this.mfaService.getUserMethods(userId)
171
- const method = this.resolveChallengeMethod(protection.config.challengeMethod, userMethods.length, userId, targetIdentifier)
178
+ const method = this.resolveChallengeMethod(protection.config.challengeMethod, userMethods.length)
172
179
 
173
180
  let sessionToken = randomBytes(16).toString('hex')
174
181
  let availableMfaMethods: SudoAvailableMethod[] | undefined
@@ -182,15 +189,9 @@ export class SudoChallengeService {
182
189
  const session = this.em.create(SudoSession, {
183
190
  userId,
184
191
  tenantId: user.tenantId,
185
- scopeTenantId,
186
- scopeOrganizationId,
187
- targetIdentifier,
188
- sudoConfigId: protection.config.id,
189
- sudoConfigUpdatedAt: protection.config.updatedAt,
190
192
  sessionToken,
191
193
  challengeMethod: method,
192
194
  expiresAt,
193
- verifiedAt: null,
194
195
  createdAt: new Date(),
195
196
  })
196
197
  this.em.persist(session)
@@ -254,36 +255,17 @@ export class SudoChallengeService {
254
255
  if (!user?.tenantId) {
255
256
  throw new SudoChallengeServiceError('User not found', 404)
256
257
  }
257
- if (user.tenantId !== session.tenantId) {
258
- throw new SudoChallengeServiceError('Unable to verify sudo challenge', 403)
259
- }
260
258
 
261
- const requestedTenantId = options.tenantId !== undefined ? options.tenantId : user.tenantId
262
- const requestedOrganizationId = options.organizationId !== undefined ? options.organizationId : user.organizationId
263
- if (
264
- options.targetIdentifier !== session.targetIdentifier
265
- || requestedTenantId !== session.scopeTenantId
266
- || requestedOrganizationId !== session.scopeOrganizationId
267
- ) {
268
- throw new SudoChallengeServiceError('Unable to verify sudo challenge', 403)
269
- }
270
-
271
- const scopeTenantId = session.scopeTenantId ?? null
272
- const scopeOrganizationId = session.scopeOrganizationId ?? null
259
+ const scopeTenantId = options.tenantId !== undefined ? options.tenantId : user.tenantId
260
+ const scopeOrganizationId = options.organizationId !== undefined ? options.organizationId : user.organizationId
273
261
  const protection = await this.isProtected(
274
- session.targetIdentifier,
262
+ options.targetIdentifier,
275
263
  scopeTenantId,
276
264
  scopeOrganizationId,
277
265
  )
278
266
  if (!protection.protected || !protection.config) {
279
267
  throw new SudoChallengeServiceError('Sudo protection is not configured for this target', 404)
280
268
  }
281
- if (
282
- protection.config.id !== session.sudoConfigId
283
- || protection.config.updatedAt.getTime() !== session.sudoConfigUpdatedAt?.getTime()
284
- ) {
285
- throw new SudoChallengeServiceError('Unable to verify sudo challenge', 403)
286
- }
287
269
 
288
270
  let verified = false
289
271
  let methodUsed: string = methodType
@@ -306,73 +288,33 @@ export class SudoChallengeService {
306
288
  userId: session.userId,
307
289
  tenantId: user.tenantId,
308
290
  organizationId: user.organizationId,
309
- targetIdentifier: session.targetIdentifier,
291
+ targetIdentifier: options.targetIdentifier,
310
292
  method: methodUsed,
311
293
  })
312
294
  throw new SudoChallengeServiceError('Unable to verify sudo challenge', 401)
313
295
  }
314
296
 
315
- const pendingToken = session.sessionToken
316
- const { sudoToken, expiresAt, verifiedAt } = await this.em.transactional(async (transactionalEm) => {
317
- await transactionalEm.execute('lock table "sudo_challenge_configs" in share mode')
318
- const currentProtection = await this.resolveProtection(
319
- transactionalEm,
320
- session.targetIdentifier,
321
- scopeTenantId,
322
- scopeOrganizationId,
323
- )
324
- if (
325
- !currentProtection.protected
326
- || !currentProtection.config
327
- || currentProtection.config.id !== session.sudoConfigId
328
- || currentProtection.config.updatedAt.getTime() !== session.sudoConfigUpdatedAt.getTime()
329
- ) {
330
- throw new SudoChallengeServiceError('Unable to verify sudo challenge', 403)
331
- }
332
-
333
- const ttlSeconds = this.normalizeTtl(currentProtection.config.ttlSeconds)
334
- const currentVerifiedAt = new Date()
335
- const currentExpiresAt = new Date(Date.now() + ttlSeconds * 1000)
336
- const currentSudoToken = this.signToken({
337
- sid: session.id,
338
- sub: session.userId,
339
- tid: scopeTenantId,
340
- oid: scopeOrganizationId,
341
- tgt: session.targetIdentifier,
342
- exp: currentExpiresAt.getTime(),
343
- })
344
- const consumedRows = await transactionalEm.nativeUpdate(
345
- SudoSession,
346
- {
347
- id: session.id,
348
- sessionToken: pendingToken,
349
- verifiedAt: null,
350
- expiresAt: { $gt: currentVerifiedAt },
351
- },
352
- {
353
- sessionToken: currentSudoToken,
354
- expiresAt: currentExpiresAt,
355
- verifiedAt: currentVerifiedAt,
356
- },
357
- )
358
- if (consumedRows !== 1) {
359
- throw new SudoChallengeServiceError('Unable to verify sudo challenge', 400)
360
- }
361
- return {
362
- sudoToken: currentSudoToken,
363
- expiresAt: currentExpiresAt,
364
- verifiedAt: currentVerifiedAt,
365
- }
297
+ const ttlSeconds = this.normalizeTtl(protection.config.ttlSeconds)
298
+ const expiresAt = new Date(Date.now() + ttlSeconds * 1000)
299
+ const sudoToken = this.signToken({
300
+ sid: session.id,
301
+ sub: session.userId,
302
+ tid: scopeTenantId,
303
+ oid: scopeOrganizationId,
304
+ tgt: options.targetIdentifier,
305
+ exp: expiresAt.getTime(),
366
306
  })
307
+
367
308
  session.sessionToken = sudoToken
309
+ session.challengeMethod = methodUsed
368
310
  session.expiresAt = expiresAt
369
- session.verifiedAt = verifiedAt
311
+ await this.em.flush()
370
312
 
371
313
  await emitSecurityEvent('security.sudo.verified', {
372
314
  userId: session.userId,
373
315
  tenantId: scopeTenantId,
374
316
  organizationId: scopeOrganizationId,
375
- targetIdentifier: session.targetIdentifier,
317
+ targetIdentifier: options.targetIdentifier,
376
318
  method: methodUsed,
377
319
  expiresAt: expiresAt.toISOString(),
378
320
  })
@@ -402,10 +344,6 @@ export class SudoChallengeService {
402
344
  id: payload.sid,
403
345
  userId: payload.sub,
404
346
  sessionToken: token,
405
- targetIdentifier,
406
- scopeTenantId: payload.tid,
407
- scopeOrganizationId: payload.oid,
408
- verifiedAt: { $ne: null },
409
347
  } as FilterQuery<SudoSession>)
410
348
 
411
349
  return Boolean(session && session.expiresAt.getTime() > Date.now())
@@ -525,20 +463,12 @@ export class SudoChallengeService {
525
463
  })
526
464
 
527
465
  if (existing) {
528
- const ttlSeconds = this.normalizeTtl(input.ttlSeconds)
529
- const challengeMethod = input.challengeMethod ?? ChallengeMethod.AUTO
530
- const changed = !existing.isEnabled
531
- || Boolean(existing.deletedAt)
532
- || existing.ttlSeconds !== ttlSeconds
533
- || existing.challengeMethod !== challengeMethod
534
- if (changed) {
535
- existing.isEnabled = true
536
- existing.deletedAt = null
537
- existing.ttlSeconds = ttlSeconds
538
- existing.challengeMethod = challengeMethod
539
- existing.updatedAt = new Date()
540
- await this.em.flush()
541
- }
466
+ existing.isEnabled = true
467
+ existing.deletedAt = null
468
+ existing.ttlSeconds = this.normalizeTtl(input.ttlSeconds)
469
+ existing.challengeMethod = input.challengeMethod ?? ChallengeMethod.AUTO
470
+ existing.updatedAt = new Date()
471
+ await this.em.flush()
542
472
  return
543
473
  }
544
474
 
@@ -643,33 +573,10 @@ export class SudoChallengeService {
643
573
  private resolveChallengeMethod(
644
574
  configuredMethod: ChallengeMethod,
645
575
  availableMfaMethodCount: number,
646
- userId?: string,
647
- targetIdentifier?: string,
648
576
  ): SudoMethod {
649
577
  if (this.securityConfig.mfa.emergencyBypass) {
650
- let wouldHaveBeenMfa = false
651
- try {
652
- wouldHaveBeenMfa = this.resolveChallengeMethodWithoutBypass(configuredMethod, availableMfaMethodCount) === 'mfa'
653
- } catch {
654
- wouldHaveBeenMfa = configuredMethod === ChallengeMethod.MFA
655
- }
656
- if (wouldHaveBeenMfa) {
657
- emitMfaEmergencyBypassActiveWarning('sudo challenge downgraded to password', {
658
- configuredMethod,
659
- availableMfaMethodCount,
660
- userId,
661
- targetIdentifier,
662
- })
663
- }
664
578
  return 'password'
665
579
  }
666
- return this.resolveChallengeMethodWithoutBypass(configuredMethod, availableMfaMethodCount)
667
- }
668
-
669
- private resolveChallengeMethodWithoutBypass(
670
- configuredMethod: ChallengeMethod,
671
- availableMfaMethodCount: number,
672
- ): SudoMethod {
673
580
  if (configuredMethod === ChallengeMethod.PASSWORD) return 'password'
674
581
  if (configuredMethod === ChallengeMethod.MFA) {
675
582
  if (availableMfaMethodCount === 0) {
@@ -680,28 +587,6 @@ export class SudoChallengeService {
680
587
  return availableMfaMethodCount > 0 ? 'mfa' : 'password'
681
588
  }
682
589
 
683
- private async resolveProtection(
684
- em: EntityManager,
685
- targetIdentifier: string,
686
- tenantId: string | null,
687
- organizationId: string | null,
688
- ): Promise<SudoProtectionResolution> {
689
- const candidates = await em.find(SudoChallengeConfig, {
690
- targetIdentifier,
691
- deletedAt: null,
692
- })
693
-
694
- const resolved = candidates
695
- .filter((config) => this.matchesScope(config, tenantId, organizationId))
696
- .sort((left, right) => this.compareConfigPriority(left, right, tenantId, organizationId))[0]
697
-
698
- if (!resolved || !resolved.isEnabled) {
699
- return { protected: false }
700
- }
701
-
702
- return { protected: true, config: resolved }
703
- }
704
-
705
590
  private matchesScope(config: SudoChallengeConfig, tenantId: string | null, organizationId: string | null): boolean {
706
591
  if (config.organizationId) {
707
592
  return config.organizationId === organizationId && config.tenantId === tenantId
@@ -739,21 +624,15 @@ export class SudoChallengeService {
739
624
  return 4
740
625
  }
741
626
 
742
- private async getPendingSession(sessionId: string): Promise<BoundPendingSudoSession> {
627
+ private async getPendingSession(sessionId: string): Promise<SudoSession> {
743
628
  const session = await this.em.findOne(SudoSession, { id: sessionId })
744
629
  if (!session) {
745
630
  throw new SudoChallengeServiceError('Sudo challenge session not found', 404)
746
631
  }
747
- if (!session.targetIdentifier || !session.sudoConfigId || !session.sudoConfigUpdatedAt) {
748
- throw new SudoChallengeServiceError('Sudo challenge session not found', 404)
749
- }
750
- if (session.verifiedAt) {
751
- throw new SudoChallengeServiceError('Unable to verify sudo challenge', 400)
752
- }
753
632
  if (session.expiresAt.getTime() <= Date.now()) {
754
633
  throw new SudoChallengeServiceError('Sudo challenge session expired', 400)
755
634
  }
756
- return session as BoundPendingSudoSession
635
+ return session
757
636
  }
758
637
 
759
638
  private async findUserScope(userId: string): Promise<UserScope | null> {
@@ -30,7 +30,6 @@ interface ListResponse {
30
30
  items: SsoConfigRow[]
31
31
  total: number
32
32
  totalPages: number
33
- totalIsCapped?: boolean
34
33
  isSuperAdmin?: boolean
35
34
  }
36
35
 
@@ -221,7 +220,6 @@ export default function SsoConfigListPage() {
221
220
  pageSize: 50,
222
221
  total: data.total,
223
222
  totalPages: data.totalPages,
224
- totalIsCapped: data?.totalIsCapped === true,
225
223
  onPageChange: setPage,
226
224
  }}
227
225
  isLoading={isLoading}
@@ -1,17 +0,0 @@
1
- import { registerMfaPendingAccessRoutes } from "@open-mercato/shared/lib/auth/mfaPendingAccess";
2
- const CANONICAL_MFA_COMPLETION_ROUTES = [
3
- { path: "/api/security/mfa/prepare", methods: ["POST"] },
4
- { path: "/api/security/mfa/verify", methods: ["POST"] },
5
- { path: "/api/security/mfa/recovery", methods: ["POST"] }
6
- ];
7
- let registered = false;
8
- function registerCanonicalMfaCompletionRoutes() {
9
- if (registered) return;
10
- registerMfaPendingAccessRoutes(CANONICAL_MFA_COMPLETION_ROUTES);
11
- registered = true;
12
- }
13
- registerCanonicalMfaCompletionRoutes();
14
- export {
15
- registerCanonicalMfaCompletionRoutes
16
- };
17
- //# sourceMappingURL=mfaCompletionRoutes.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../../src/modules/security/lib/mfaCompletionRoutes.ts"],
4
- "sourcesContent": ["import { registerMfaPendingAccessRoutes } from '@open-mercato/shared/lib/auth/mfaPendingAccess'\n\nconst CANONICAL_MFA_COMPLETION_ROUTES = [\n { path: '/api/security/mfa/prepare', methods: ['POST'] },\n { path: '/api/security/mfa/verify', methods: ['POST'] },\n { path: '/api/security/mfa/recovery', methods: ['POST'] },\n]\n\nlet registered = false\n\n/**\n * Register the canonical MFA challenge-completion routes with the platform-wide pending-token\n * allowlist. The enterprise security module owns these routes, so it owns their registration:\n * shared stays generic (SPEC-ENT-007 \u00A7 Amendment 2026-08-21). Idempotent; runs once per process\n * on first import of this module \u2014 which happens at security-module bootstrap and, as a\n * belt-and-braces guarantee, on every load of the completion route modules themselves, so the\n * routes are always registered before request authentication resolves.\n */\nexport function registerCanonicalMfaCompletionRoutes(): void {\n if (registered) return\n registerMfaPendingAccessRoutes(CANONICAL_MFA_COMPLETION_ROUTES)\n registered = true\n}\n\nregisterCanonicalMfaCompletionRoutes()\n"],
5
- "mappings": "AAAA,SAAS,sCAAsC;AAE/C,MAAM,kCAAkC;AAAA,EACtC,EAAE,MAAM,6BAA6B,SAAS,CAAC,MAAM,EAAE;AAAA,EACvD,EAAE,MAAM,4BAA4B,SAAS,CAAC,MAAM,EAAE;AAAA,EACtD,EAAE,MAAM,8BAA8B,SAAS,CAAC,MAAM,EAAE;AAC1D;AAEA,IAAI,aAAa;AAUV,SAAS,uCAA6C;AAC3D,MAAI,WAAY;AAChB,iCAA+B,+BAA+B;AAC9D,eAAa;AACf;AAEA,qCAAqC;",
6
- "names": []
7
- }
@@ -1,13 +0,0 @@
1
- import { Migration } from "@mikro-orm/migrations";
2
- class Migration20260811143757_security extends Migration {
3
- up() {
4
- this.addSql(`alter table "sudo_sessions" add "scope_tenant_id" uuid null, add "scope_organization_id" uuid null, add "target_identifier" text null, add "sudo_config_id" uuid null, add "sudo_config_updated_at" timestamptz null, add "verified_at" timestamptz null;`);
5
- }
6
- down() {
7
- this.addSql(`alter table "sudo_sessions" drop column "scope_tenant_id", drop column "scope_organization_id", drop column "target_identifier", drop column "sudo_config_id", drop column "sudo_config_updated_at", drop column "verified_at";`);
8
- }
9
- }
10
- export {
11
- Migration20260811143757_security
12
- };
13
- //# sourceMappingURL=Migration20260811143757_security.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../../src/modules/security/migrations/Migration20260811143757_security.ts"],
4
- "sourcesContent": ["import { Migration } from '@mikro-orm/migrations';\n\nexport class Migration20260811143757_security extends Migration {\n\n override up(): void | Promise<void> {\n this.addSql(`alter table \"sudo_sessions\" add \"scope_tenant_id\" uuid null, add \"scope_organization_id\" uuid null, add \"target_identifier\" text null, add \"sudo_config_id\" uuid null, add \"sudo_config_updated_at\" timestamptz null, add \"verified_at\" timestamptz null;`);\n }\n\n override down(): void | Promise<void> {\n this.addSql(`alter table \"sudo_sessions\" drop column \"scope_tenant_id\", drop column \"scope_organization_id\", drop column \"target_identifier\", drop column \"sudo_config_id\", drop column \"sudo_config_updated_at\", drop column \"verified_at\";`);\n }\n\n}\n"],
5
- "mappings": "AAAA,SAAS,iBAAiB;AAEnB,MAAM,yCAAyC,UAAU;AAAA,EAErD,KAA2B;AAClC,SAAK,OAAO,2PAA2P;AAAA,EACzQ;AAAA,EAES,OAA6B;AACpC,SAAK,OAAO,iOAAiO;AAAA,EAC/O;AAEF;",
6
- "names": []
7
- }
@@ -1,25 +0,0 @@
1
- import { registerMfaPendingAccessRoutes } from '@open-mercato/shared/lib/auth/mfaPendingAccess'
2
-
3
- const CANONICAL_MFA_COMPLETION_ROUTES = [
4
- { path: '/api/security/mfa/prepare', methods: ['POST'] },
5
- { path: '/api/security/mfa/verify', methods: ['POST'] },
6
- { path: '/api/security/mfa/recovery', methods: ['POST'] },
7
- ]
8
-
9
- let registered = false
10
-
11
- /**
12
- * Register the canonical MFA challenge-completion routes with the platform-wide pending-token
13
- * allowlist. The enterprise security module owns these routes, so it owns their registration:
14
- * shared stays generic (SPEC-ENT-007 § Amendment 2026-08-21). Idempotent; runs once per process
15
- * on first import of this module — which happens at security-module bootstrap and, as a
16
- * belt-and-braces guarantee, on every load of the completion route modules themselves, so the
17
- * routes are always registered before request authentication resolves.
18
- */
19
- export function registerCanonicalMfaCompletionRoutes(): void {
20
- if (registered) return
21
- registerMfaPendingAccessRoutes(CANONICAL_MFA_COMPLETION_ROUTES)
22
- registered = true
23
- }
24
-
25
- registerCanonicalMfaCompletionRoutes()
@@ -1,13 +0,0 @@
1
- import { Migration } from '@mikro-orm/migrations';
2
-
3
- export class Migration20260811143757_security extends Migration {
4
-
5
- override up(): void | Promise<void> {
6
- this.addSql(`alter table "sudo_sessions" add "scope_tenant_id" uuid null, add "scope_organization_id" uuid null, add "target_identifier" text null, add "sudo_config_id" uuid null, add "sudo_config_updated_at" timestamptz null, add "verified_at" timestamptz null;`);
7
- }
8
-
9
- override down(): void | Promise<void> {
10
- this.addSql(`alter table "sudo_sessions" drop column "scope_tenant_id", drop column "scope_organization_id", drop column "target_identifier", drop column "sudo_config_id", drop column "sudo_config_updated_at", drop column "verified_at";`);
11
- }
12
-
13
- }