@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/sso/backend/page.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\n\nimport React from 'react'\nimport { useRouter } from 'next/navigation'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { apiCall, apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport Link from 'next/link'\n\ninterface SsoConfigRow {\n id: string\n name: string | null\n protocol: string\n issuer: string | null\n allowedDomains: string[]\n isActive: boolean\n hasClientSecret: boolean\n organizationId: string\n tenantId: string | null\n createdAt: string\n}\n\ninterface ListResponse {\n items: SsoConfigRow[]\n total: number\n totalPages: number\n
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport React from 'react'\nimport { useRouter } from 'next/navigation'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { apiCall, apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport Link from 'next/link'\n\ninterface SsoConfigRow {\n id: string\n name: string | null\n protocol: string\n issuer: string | null\n allowedDomains: string[]\n isActive: boolean\n hasClientSecret: boolean\n organizationId: string\n tenantId: string | null\n createdAt: string\n}\n\ninterface ListResponse {\n items: SsoConfigRow[]\n total: number\n totalPages: number\n isSuperAdmin?: boolean\n}\n\nconst fallback: ListResponse = { items: [], total: 0, totalPages: 1 }\n\nexport default function SsoConfigListPage() {\n const router = useRouter()\n const t = useT()\n\n const [data, setData] = React.useState<ListResponse>(fallback)\n const [isLoading, setIsLoading] = React.useState(true)\n const [page, setPage] = React.useState(1)\n const [search, setSearch] = React.useState('')\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n\n const isSuperAdmin = !!data.isSuperAdmin\n\n const fetchData = React.useCallback(async () => {\n setIsLoading(true)\n const params = new URLSearchParams()\n params.set('page', String(page))\n params.set('pageSize', '50')\n if (search) params.set('search', search)\n\n const call = await apiCall<ListResponse>(`/api/sso/config?${params}`, undefined, { fallback })\n if (call.ok && call.result) {\n setData(call.result)\n }\n setIsLoading(false)\n }, [page, search])\n\n React.useEffect(() => { fetchData() }, [fetchData])\n\n const handleDelete = async (row: SsoConfigRow) => {\n if (row.isActive) {\n flash(t('sso.admin.error.deleteActive', 'Cannot delete an active SSO configuration \u2014 deactivate it first'), 'error')\n return\n }\n\n const confirmed = await confirm({\n title: t('sso.admin.delete.title', 'Delete SSO Configuration'),\n text: t('sso.admin.delete.confirm', 'Are you sure? This will remove the SSO configuration. Users with linked SSO identities will need to use password login.'),\n confirmText: t('common.delete', 'Delete'),\n variant: 'destructive',\n })\n\n if (!confirmed) return\n\n await apiCallOrThrow(`/api/sso/config/${row.id}`, { method: 'DELETE' }, {\n errorMessage: t('sso.admin.error.deleteFailed', 'Failed to delete SSO configuration'),\n })\n flash(t('sso.admin.delete.success', 'SSO configuration deleted'), 'success')\n fetchData()\n }\n\n const handleToggleActivation = async (row: SsoConfigRow) => {\n try {\n await apiCallOrThrow(\n `/api/sso/config/${row.id}/activate`,\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ active: !row.isActive }),\n },\n { errorMessage: t('sso.admin.error.activationFailed', 'Failed to update activation status') },\n )\n flash(\n row.isActive\n ? t('sso.admin.deactivated', 'SSO configuration deactivated')\n : t('sso.admin.activated', 'SSO configuration activated'),\n 'success',\n )\n fetchData()\n } catch {\n // apiCallOrThrow already flashes the error\n }\n }\n\n const handleTestConnection = async (row: SsoConfigRow) => {\n try {\n const call = await apiCallOrThrow<{ ok: boolean; error?: string }>(\n `/api/sso/config/${row.id}/test`,\n { method: 'POST' },\n { errorMessage: t('sso.admin.error.testFailed', 'Connection test failed') },\n )\n if (call.result?.ok) {\n flash(t('sso.admin.test.success', 'Discovery successful \u2014 issuer is reachable'), 'success')\n } else {\n flash(call.result?.error || t('sso.admin.test.failed', 'Discovery failed'), 'error')\n }\n } catch {\n // apiCallOrThrow already flashes the error\n }\n }\n\n const columns = React.useMemo<ColumnDef<SsoConfigRow>[]>(() => {\n const cols: ColumnDef<SsoConfigRow>[] = [\n {\n accessorKey: 'name',\n header: t('sso.admin.column.name', 'Name'),\n cell: ({ row }) => row.original.name || row.original.issuer || '\u2014',\n },\n {\n accessorKey: 'protocol',\n header: t('sso.admin.column.protocol', 'Protocol'),\n cell: ({ row }) => row.original.protocol.toUpperCase(),\n },\n {\n accessorKey: 'allowedDomains',\n header: t('sso.admin.column.domains', 'Domains'),\n cell: ({ row }) => row.original.allowedDomains.join(', ') || '\u2014',\n },\n {\n accessorKey: 'isActive',\n header: t('sso.admin.column.status', 'Status'),\n cell: ({ row }) => (\n <span className={`inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ${row.original.isActive ? 'bg-green-50 text-green-700' : 'bg-gray-100 text-gray-600'}`}>\n {row.original.isActive\n ? t('sso.admin.status.active', 'Active')\n : t('sso.admin.status.inactive', 'Inactive')}\n </span>\n ),\n },\n {\n accessorKey: 'createdAt',\n header: t('sso.admin.column.created', 'Created'),\n cell: ({ row }) => new Date(row.original.createdAt).toLocaleDateString(),\n },\n ]\n\n return cols\n }, [t, isSuperAdmin])\n\n const hasConfigs = data.items.length > 0 || search\n const canCreateNew = isSuperAdmin || data.items.length === 0\n\n return (\n <Page>\n <PageBody>\n {!hasConfigs && !isLoading ? (\n <div className=\"flex flex-col items-center justify-center py-16 text-center\">\n <h3 className=\"text-lg font-semibold mb-2\">\n {t('sso.admin.empty.title', 'No SSO configured')}\n </h3>\n <p className=\"text-muted-foreground mb-4 max-w-md\">\n {t('sso.admin.empty.description', 'Configure Single Sign-On to let your users authenticate with your identity provider.')}\n </p>\n <Button asChild>\n <Link href=\"/backend/sso/config/new\">\n {t('sso.admin.empty.cta', 'Configure SSO')}\n </Link>\n </Button>\n </div>\n ) : (\n <DataTable<SsoConfigRow>\n title={t('sso.admin.title', 'Single Sign-On')}\n actions={canCreateNew ? (\n <Button asChild size=\"sm\">\n <Link href=\"/backend/sso/config/new\">\n {t('sso.admin.new', 'New SSO Config')}\n </Link>\n </Button>\n ) : undefined}\n columns={columns}\n data={data.items}\n searchValue={search}\n onSearchChange={(value) => { setSearch(value); setPage(1) }}\n searchPlaceholder={t('sso.admin.search', 'Search by name or issuer...')}\n onRowClick={(row) => router.push(`/backend/sso/config/${row.id}`)}\n rowActions={(row) => (\n <RowActions\n items={[\n { id: 'edit', label: t('common.edit', 'Edit'), onSelect: () => router.push(`/backend/sso/config/${row.id}`) },\n { id: 'test', label: t('sso.admin.action.test', 'Verify Discovery'), onSelect: () => handleTestConnection(row) },\n {\n id: 'toggle',\n label: row.isActive\n ? t('sso.admin.action.deactivate', 'Deactivate')\n : t('sso.admin.action.activate', 'Activate'),\n onSelect: () => handleToggleActivation(row),\n },\n { id: 'delete', label: t('common.delete', 'Delete'), destructive: true, onSelect: () => handleDelete(row) },\n ]}\n />\n )}\n pagination={{\n page,\n pageSize: 50,\n total: data.total,\n totalPages: data.totalPages,\n onPageChange: setPage,\n }}\n isLoading={isLoading}\n />\n )}\n {ConfirmDialogElement}\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/enterprise",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -65,8 +65,8 @@
|
|
|
65
65
|
}
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"@open-mercato/core": "0.
|
|
69
|
-
"@open-mercato/ui": "0.
|
|
68
|
+
"@open-mercato/core": "0.7.0",
|
|
69
|
+
"@open-mercato/ui": "0.7.0",
|
|
70
70
|
"@simplewebauthn/browser": "^13.3.0",
|
|
71
71
|
"@simplewebauthn/server": "^13.3.2",
|
|
72
72
|
"@simplewebauthn/types": "^12.0.0",
|
|
@@ -76,12 +76,12 @@
|
|
|
76
76
|
"qrcode": "^1.5.4"
|
|
77
77
|
},
|
|
78
78
|
"peerDependencies": {
|
|
79
|
-
"@open-mercato/shared": "0.
|
|
79
|
+
"@open-mercato/shared": "0.7.0",
|
|
80
80
|
"react": "^19.0.0",
|
|
81
81
|
"react-dom": "^19.0.0"
|
|
82
82
|
},
|
|
83
83
|
"devDependencies": {
|
|
84
|
-
"@open-mercato/shared": "0.
|
|
84
|
+
"@open-mercato/shared": "0.7.0",
|
|
85
85
|
"@types/jest": "^30.0.0",
|
|
86
86
|
"@types/react": "^19.2.17",
|
|
87
87
|
"@types/react-dom": "^19.2.3",
|
|
@@ -98,6 +98,5 @@
|
|
|
98
98
|
"type": "git",
|
|
99
99
|
"url": "https://github.com/open-mercato/open-mercato",
|
|
100
100
|
"directory": "packages/enterprise"
|
|
101
|
-
}
|
|
102
|
-
"stableVersion": "0.6.7"
|
|
101
|
+
}
|
|
103
102
|
}
|
|
@@ -161,20 +161,20 @@ export default function RecordLockingSettingsPage() {
|
|
|
161
161
|
</select>
|
|
162
162
|
<Alert status="information">
|
|
163
163
|
<AlertDescription>
|
|
164
|
-
<
|
|
164
|
+
<span className="block">
|
|
165
165
|
<strong>{t('record_locks.settings.strategy_optimistic', 'Optimistic')}:</strong>{' '}
|
|
166
166
|
{t(
|
|
167
167
|
'record_locks.settings.strategy_help_optimistic',
|
|
168
168
|
'Multiple users can edit at the same time; conflicts are checked on save.',
|
|
169
169
|
)}
|
|
170
|
-
</
|
|
171
|
-
<
|
|
170
|
+
</span>
|
|
171
|
+
<span className="mt-1 block">
|
|
172
172
|
<strong>{t('record_locks.settings.strategy_pessimistic', 'Pessimistic')}:</strong>{' '}
|
|
173
173
|
{t(
|
|
174
174
|
'record_locks.settings.strategy_help_pessimistic',
|
|
175
175
|
'First editor acquires the lock and blocks concurrent edits until release.',
|
|
176
176
|
)}
|
|
177
|
-
</
|
|
177
|
+
</span>
|
|
178
178
|
</AlertDescription>
|
|
179
179
|
</Alert>
|
|
180
180
|
</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ApiInterceptor } from '@open-mercato/shared/lib/crud/api-interceptor'
|
|
2
2
|
import { signJwt, verifyJwt } from '@open-mercato/shared/lib/auth/jwt'
|
|
3
|
-
import {
|
|
3
|
+
import { readSecurityModuleConfig } from '../lib/security-config'
|
|
4
4
|
|
|
5
5
|
type JwtClaims = {
|
|
6
6
|
sub: string
|
|
@@ -59,6 +59,7 @@ export const interceptors: ApiInterceptor[] = [
|
|
|
59
59
|
if (response.statusCode !== 200) return {}
|
|
60
60
|
if (response.body.ok !== true || response.body.mfa_required === true) return {}
|
|
61
61
|
if (typeof response.body.token !== 'string' || response.body.token.length === 0) return {}
|
|
62
|
+
if (readSecurityModuleConfig().mfa.emergencyBypass) return {}
|
|
62
63
|
|
|
63
64
|
const claims = readClaims(response.body.token)
|
|
64
65
|
if (!claims) return {}
|
|
@@ -66,11 +67,6 @@ export const interceptors: ApiInterceptor[] = [
|
|
|
66
67
|
const mfaVerificationService = resolveMfaVerificationService(context.container as { resolve: (name: string) => unknown })
|
|
67
68
|
if (!mfaVerificationService) return {}
|
|
68
69
|
|
|
69
|
-
if (readSecurityModuleConfig().mfa.emergencyBypass) {
|
|
70
|
-
emitMfaEmergencyBypassActiveWarning('login MFA challenge bypassed', { userId: claims.sub })
|
|
71
|
-
return {}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
70
|
try {
|
|
75
71
|
const challenge = await mfaVerificationService.createChallenge(claims.sub)
|
|
76
72
|
const pendingToken = signJwt(
|
|
@@ -3,7 +3,6 @@ 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'
|
|
7
6
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
8
7
|
import { signJwt } from '@open-mercato/shared/lib/auth/jwt'
|
|
9
8
|
import type { MfaService, MfaServiceError } from '../../services/MfaService'
|
|
@@ -174,21 +174,6 @@ 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
|
-
|
|
192
177
|
@Property({ name: 'session_token', type: 'text' })
|
|
193
178
|
sessionToken!: string
|
|
194
179
|
|
|
@@ -198,9 +183,6 @@ export class SudoSession {
|
|
|
198
183
|
@Property({ name: 'expires_at', type: Date })
|
|
199
184
|
expiresAt!: Date
|
|
200
185
|
|
|
201
|
-
@Property({ name: 'verified_at', type: Date, nullable: true })
|
|
202
|
-
verifiedAt?: Date | null
|
|
203
|
-
|
|
204
186
|
@Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
|
|
205
187
|
createdAt: Date = new Date()
|
|
206
188
|
}
|
|
@@ -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 {
|
|
4
|
+
import { 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,20 +93,11 @@ 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
|
-
|
|
97
|
-
const emergencyBypass = readSecurityModuleConfig().mfa.emergencyBypass
|
|
96
|
+
if (readSecurityModuleConfig().mfa.emergencyBypass) return null
|
|
98
97
|
|
|
99
98
|
const enforcementService = resolveEnforcementService(container)
|
|
100
99
|
if (!enforcementService.service) {
|
|
101
100
|
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
|
-
}
|
|
110
101
|
logger.error('Unable to resolve MFA enforcement service; redirecting to enrollment', {
|
|
111
102
|
err: enforcementService.error,
|
|
112
103
|
})
|
|
@@ -120,28 +111,9 @@ export async function resolveMfaEnrollmentRedirect(args: {
|
|
|
120
111
|
const deadlineState = resolveDeadlineRedirectState(compliance.deadline)
|
|
121
112
|
if (!deadlineState.shouldRedirect) return null
|
|
122
113
|
|
|
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
|
-
|
|
134
114
|
return createEnrollmentRedirect(pathname, deadlineState.overdue)
|
|
135
115
|
} catch (error) {
|
|
136
116
|
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
|
-
}
|
|
145
117
|
logger.error('Unable to verify MFA enforcement compliance; redirecting to enrollment', { err: error })
|
|
146
118
|
return createEnrollmentRedirect(pathname)
|
|
147
119
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'
|
|
2
|
-
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
3
2
|
|
|
4
3
|
const TOTP_PERIOD_SECONDS = 30
|
|
5
4
|
const TOTP_DIGITS = 6
|
|
@@ -8,9 +7,6 @@ const DEFAULT_WEBAUTHN_CHALLENGE_TTL_MS = 5 * 60 * 1000
|
|
|
8
7
|
const DEFAULT_SUDO_PENDING_CHALLENGE_TTL_MS = 10 * 60 * 1000
|
|
9
8
|
const MIN_SUDO_TTL_SECONDS = 30
|
|
10
9
|
|
|
11
|
-
const securityBypassLogger = createLogger('security').child({ component: 'mfa-emergency-bypass' })
|
|
12
|
-
let startupBypassWarningEmitted = false
|
|
13
|
-
|
|
14
10
|
export type SecurityModuleConfig = {
|
|
15
11
|
totp: {
|
|
16
12
|
issuer: string
|
|
@@ -174,31 +170,6 @@ export function resolveSecurityModuleConfigForRequest(
|
|
|
174
170
|
}
|
|
175
171
|
}
|
|
176
172
|
|
|
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
|
-
|
|
202
173
|
export function readSecuritySetupTokenSecret(env: NodeJS.ProcessEnv = process.env): string {
|
|
203
174
|
const secret = readText(env.OM_SECURITY_MFA_SETUP_SECRET)
|
|
204
175
|
?? readText(env.AUTH_JWT_SECRET)
|