@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/record_locks/backend/settings/record-locks/page.js +2 -2
- package/dist/modules/record_locks/backend/settings/record-locks/page.js.map +2 -2
- package/dist/modules/security/api/interceptors.js +2 -5
- package/dist/modules/security/api/interceptors.js.map +2 -2
- package/dist/modules/security/api/mfa/_shared.js +0 -1
- package/dist/modules/security/api/mfa/_shared.js.map +2 -2
- package/dist/modules/security/data/entities.js +0 -18
- package/dist/modules/security/data/entities.js.map +2 -2
- package/dist/modules/security/index.js +0 -3
- package/dist/modules/security/index.js.map +2 -2
- package/dist/modules/security/lib/enforcement-redirect.js +2 -28
- package/dist/modules/security/lib/enforcement-redirect.js.map +2 -2
- package/dist/modules/security/lib/security-config.js +0 -27
- package/dist/modules/security/lib/security-config.js.map +2 -2
- package/dist/modules/security/services/SudoChallengeService.js +41 -130
- package/dist/modules/security/services/SudoChallengeService.js.map +2 -2
- package/dist/modules/sso/backend/page.js +0 -1
- package/dist/modules/sso/backend/page.js.map +2 -2
- package/package.json +6 -7
- package/src/modules/record_locks/backend/settings/record-locks/page.tsx +4 -4
- package/src/modules/security/api/interceptors.ts +2 -6
- package/src/modules/security/api/mfa/_shared.ts +0 -1
- package/src/modules/security/data/entities.ts +0 -18
- package/src/modules/security/index.ts +0 -4
- package/src/modules/security/lib/enforcement-redirect.ts +2 -30
- package/src/modules/security/lib/security-config.ts +0 -29
- package/src/modules/security/migrations/.snapshot-open-mercato.json +276 -849
- package/src/modules/security/services/SudoChallengeService.ts +49 -170
- package/src/modules/sso/backend/page.tsx +0 -2
- package/dist/modules/security/lib/mfaCompletionRoutes.js +0 -17
- package/dist/modules/security/lib/mfaCompletionRoutes.js.map +0 -7
- package/dist/modules/security/migrations/Migration20260811143757_security.js +0 -13
- package/dist/modules/security/migrations/Migration20260811143757_security.js.map +0 -7
- package/src/modules/security/lib/mfaCompletionRoutes.ts +0 -25
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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
|
|
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
|
|
262
|
-
const
|
|
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
|
-
|
|
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:
|
|
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
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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<
|
|
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
|
|
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
|
-
}
|