@open-mercato/enterprise 0.7.0 → 0.7.1-develop.7103.1.41ff100d93

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 (33) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/security/api/interceptors.js +5 -2
  3. package/dist/modules/security/api/interceptors.js.map +2 -2
  4. package/dist/modules/security/api/mfa/_shared.js +1 -0
  5. package/dist/modules/security/api/mfa/_shared.js.map +2 -2
  6. package/dist/modules/security/data/entities.js +18 -0
  7. package/dist/modules/security/data/entities.js.map +2 -2
  8. package/dist/modules/security/index.js +3 -0
  9. package/dist/modules/security/index.js.map +2 -2
  10. package/dist/modules/security/lib/enforcement-redirect.js +28 -2
  11. package/dist/modules/security/lib/enforcement-redirect.js.map +2 -2
  12. package/dist/modules/security/lib/mfaCompletionRoutes.js +17 -0
  13. package/dist/modules/security/lib/mfaCompletionRoutes.js.map +7 -0
  14. package/dist/modules/security/lib/security-config.js +27 -0
  15. package/dist/modules/security/lib/security-config.js.map +2 -2
  16. package/dist/modules/security/migrations/Migration20260811143757_security.js +13 -0
  17. package/dist/modules/security/migrations/Migration20260811143757_security.js.map +7 -0
  18. package/dist/modules/security/services/SudoChallengeService.js +130 -41
  19. package/dist/modules/security/services/SudoChallengeService.js.map +2 -2
  20. package/dist/modules/sso/backend/page.js +1 -0
  21. package/dist/modules/sso/backend/page.js.map +2 -2
  22. package/package.json +7 -6
  23. package/src/modules/security/api/interceptors.ts +6 -2
  24. package/src/modules/security/api/mfa/_shared.ts +1 -0
  25. package/src/modules/security/data/entities.ts +18 -0
  26. package/src/modules/security/index.ts +4 -0
  27. package/src/modules/security/lib/enforcement-redirect.ts +30 -2
  28. package/src/modules/security/lib/mfaCompletionRoutes.ts +25 -0
  29. package/src/modules/security/lib/security-config.ts +29 -0
  30. package/src/modules/security/migrations/.snapshot-open-mercato.json +848 -275
  31. package/src/modules/security/migrations/Migration20260811143757_security.ts +13 -0
  32. package/src/modules/security/services/SudoChallengeService.ts +170 -49
  33. package/src/modules/sso/backend/page.tsx +2 -0
@@ -3,6 +3,7 @@ import { z } from 'zod'
3
3
  import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
4
4
  import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
5
5
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
6
+ import '../../lib/mfaCompletionRoutes'
6
7
  import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
7
8
  import { signJwt } from '@open-mercato/shared/lib/auth/jwt'
8
9
  import type { MfaService, MfaServiceError } from '../../services/MfaService'
@@ -174,6 +174,21 @@ export class SudoSession {
174
174
  @Property({ name: 'tenant_id', type: 'uuid' })
175
175
  tenantId!: string
176
176
 
177
+ @Property({ name: 'scope_tenant_id', type: 'uuid', nullable: true })
178
+ scopeTenantId?: string | null
179
+
180
+ @Property({ name: 'scope_organization_id', type: 'uuid', nullable: true })
181
+ scopeOrganizationId?: string | null
182
+
183
+ @Property({ name: 'target_identifier', type: 'text', nullable: true })
184
+ targetIdentifier?: string | null
185
+
186
+ @Property({ name: 'sudo_config_id', type: 'uuid', nullable: true })
187
+ sudoConfigId?: string | null
188
+
189
+ @Property({ name: 'sudo_config_updated_at', type: Date, nullable: true })
190
+ sudoConfigUpdatedAt?: Date | null
191
+
177
192
  @Property({ name: 'session_token', type: 'text' })
178
193
  sessionToken!: string
179
194
 
@@ -183,6 +198,9 @@ export class SudoSession {
183
198
  @Property({ name: 'expires_at', type: Date })
184
199
  expiresAt!: Date
185
200
 
201
+ @Property({ name: 'verified_at', type: Date, nullable: true })
202
+ verifiedAt?: Date | null
203
+
186
204
  @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
187
205
  createdAt: Date = new Date()
188
206
  }
@@ -1,3 +1,7 @@
1
+ import './lib/mfaCompletionRoutes'
2
+
3
+ export { registerCanonicalMfaCompletionRoutes } from './lib/mfaCompletionRoutes'
4
+
1
5
  export const metadata = {
2
6
  id: 'security',
3
7
  version: '0.1.0',
@@ -1,7 +1,7 @@
1
1
  import type { AuthContext } from '@open-mercato/shared/lib/auth/server'
2
2
  import { createLogger } from '@open-mercato/shared/lib/logger'
3
3
  import { isEnforcementDeadlineOverdue } from '../services/MfaEnforcementService'
4
- import { readSecurityModuleConfig } from './security-config'
4
+ import { emitMfaEmergencyBypassActiveWarning, readSecurityModuleConfig } from './security-config'
5
5
 
6
6
  const MFA_ENROLLMENT_PATH = '/backend/profile/security/mfa'
7
7
  const logger = createLogger('security').child({ component: 'mfa-enforcement-redirect' })
@@ -93,11 +93,20 @@ export async function resolveMfaEnrollmentRedirect(args: {
93
93
  if (!auth || typeof auth.sub !== 'string' || auth.sub.length === 0) return null
94
94
  if (auth.mfa_pending === true) return null
95
95
  if (isExemptPath(pathname)) return null
96
- if (readSecurityModuleConfig().mfa.emergencyBypass) return null
96
+
97
+ const emergencyBypass = readSecurityModuleConfig().mfa.emergencyBypass
97
98
 
98
99
  const enforcementService = resolveEnforcementService(container)
99
100
  if (!enforcementService.service) {
100
101
  if (!canFailClosed(auth)) return null
102
+ if (emergencyBypass) {
103
+ emitMfaEmergencyBypassActiveWarning('mfa-enrollment-redirect bypassed', {
104
+ userId: auth.sub,
105
+ pathname,
106
+ reason: 'enforcement-service-unavailable',
107
+ })
108
+ return null
109
+ }
101
110
  logger.error('Unable to resolve MFA enforcement service; redirecting to enrollment', {
102
111
  err: enforcementService.error,
103
112
  })
@@ -111,9 +120,28 @@ export async function resolveMfaEnrollmentRedirect(args: {
111
120
  const deadlineState = resolveDeadlineRedirectState(compliance.deadline)
112
121
  if (!deadlineState.shouldRedirect) return null
113
122
 
123
+ if (emergencyBypass) {
124
+ emitMfaEmergencyBypassActiveWarning('mfa-enrollment-redirect bypassed', {
125
+ userId: auth.sub,
126
+ pathname,
127
+ enforced: compliance.enforced,
128
+ compliant: compliance.compliant,
129
+ overdue: deadlineState.overdue,
130
+ })
131
+ return null
132
+ }
133
+
114
134
  return createEnrollmentRedirect(pathname, deadlineState.overdue)
115
135
  } catch (error) {
116
136
  if (canFailClosed(auth)) {
137
+ if (emergencyBypass) {
138
+ emitMfaEmergencyBypassActiveWarning('mfa-enrollment-redirect bypassed', {
139
+ userId: auth.sub,
140
+ pathname,
141
+ reason: 'compliance-check-failed',
142
+ })
143
+ return null
144
+ }
117
145
  logger.error('Unable to verify MFA enforcement compliance; redirecting to enrollment', { err: error })
118
146
  return createEnrollmentRedirect(pathname)
119
147
  }
@@ -0,0 +1,25 @@
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,4 +1,5 @@
1
1
  import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'
2
+ import { createLogger } from '@open-mercato/shared/lib/logger'
2
3
 
3
4
  const TOTP_PERIOD_SECONDS = 30
4
5
  const TOTP_DIGITS = 6
@@ -7,6 +8,9 @@ const DEFAULT_WEBAUTHN_CHALLENGE_TTL_MS = 5 * 60 * 1000
7
8
  const DEFAULT_SUDO_PENDING_CHALLENGE_TTL_MS = 10 * 60 * 1000
8
9
  const MIN_SUDO_TTL_SECONDS = 30
9
10
 
11
+ const securityBypassLogger = createLogger('security').child({ component: 'mfa-emergency-bypass' })
12
+ let startupBypassWarningEmitted = false
13
+
10
14
  export type SecurityModuleConfig = {
11
15
  totp: {
12
16
  issuer: string
@@ -170,6 +174,31 @@ export function resolveSecurityModuleConfigForRequest(
170
174
  }
171
175
  }
172
176
 
177
+ export function emitMfaEmergencyBypassActiveWarning(context: string, extra?: Record<string, unknown>): void {
178
+ securityBypassLogger.warn('MFA emergency bypass is active', {
179
+ emergencyBypass: true,
180
+ context,
181
+ ...extra,
182
+ })
183
+ }
184
+
185
+ export function emitMfaEmergencyBypassStartupWarningIfNeeded(env: NodeJS.ProcessEnv = process.env): void {
186
+ if (startupBypassWarningEmitted) return
187
+ const bypass = parseBooleanWithDefault(
188
+ env.OM_SECURITY_MFA_EMERGENCY_BYPASS,
189
+ defaultSecurityModuleConfig.mfa.emergencyBypass,
190
+ )
191
+ if (!bypass) return
192
+ startupBypassWarningEmitted = true
193
+ emitMfaEmergencyBypassActiveWarning('startup', { startup: true })
194
+ }
195
+
196
+ export function resetMfaEmergencyBypassStartupWarningForTests(): void {
197
+ startupBypassWarningEmitted = false
198
+ }
199
+
200
+ emitMfaEmergencyBypassStartupWarningIfNeeded()
201
+
173
202
  export function readSecuritySetupTokenSecret(env: NodeJS.ProcessEnv = process.env): string {
174
203
  const secret = readText(env.OM_SECURITY_MFA_SETUP_SECRET)
175
204
  ?? readText(env.AUTH_JWT_SECRET)