@open-mercato/enterprise 0.6.6-develop.6465.1.019f0fb26f → 0.6.6-develop.6472.1.1673e7e66b
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/dist/modules/record_locks/subscribers/conflict-detected-notification.js +3 -1
- package/dist/modules/record_locks/subscribers/conflict-detected-notification.js.map +2 -2
- package/dist/modules/record_locks/subscribers/conflict-resolved-notification.js +3 -1
- package/dist/modules/record_locks/subscribers/conflict-resolved-notification.js.map +2 -2
- package/dist/modules/record_locks/subscribers/lock-contended-notification.js +3 -1
- package/dist/modules/record_locks/subscribers/lock-contended-notification.js.map +2 -2
- package/dist/modules/record_locks/subscribers/lock-force-released-notification.js +3 -1
- package/dist/modules/record_locks/subscribers/lock-force-released-notification.js.map +2 -2
- package/dist/modules/record_locks/widgets/injection/record-locking/widget.client.js +4 -2
- package/dist/modules/record_locks/widgets/injection/record-locking/widget.client.js.map +2 -2
- package/dist/modules/security/api/enforcement/_shared.js +3 -1
- package/dist/modules/security/api/enforcement/_shared.js.map +2 -2
- package/dist/modules/security/api/mfa/_shared.js +3 -1
- package/dist/modules/security/api/mfa/_shared.js.map +2 -2
- package/dist/modules/security/api/profile/password/route.js +3 -1
- package/dist/modules/security/api/profile/password/route.js.map +2 -2
- package/dist/modules/security/api/sudo/_shared.js +3 -1
- package/dist/modules/security/api/sudo/_shared.js.map +2 -2
- package/dist/modules/security/api/users/_shared.js +3 -1
- package/dist/modules/security/api/users/_shared.js.map +2 -2
- package/dist/modules/sso/api/callback/oidc/route.js +5 -3
- package/dist/modules/sso/api/callback/oidc/route.js.map +2 -2
- package/dist/modules/sso/api/error-handler.js +4 -2
- package/dist/modules/sso/api/error-handler.js.map +2 -2
- package/dist/modules/sso/api/hrd/route.js +3 -1
- package/dist/modules/sso/api/hrd/route.js.map +2 -2
- package/dist/modules/sso/api/initiate/route.js +4 -2
- package/dist/modules/sso/api/initiate/route.js.map +2 -2
- package/dist/modules/sso/backend/sso/config/[id]/page.js +2 -2
- package/dist/modules/sso/backend/sso/config/[id]/page.js.map +2 -2
- package/dist/modules/sso/i18n/de.json +2 -2
- package/dist/modules/sso/i18n/en.json +2 -2
- package/dist/modules/sso/i18n/es.json +2 -2
- package/dist/modules/sso/i18n/pl.json +2 -2
- package/dist/modules/sso/services/accountLinkingService.js +9 -46
- package/dist/modules/sso/services/accountLinkingService.js.map +2 -2
- package/dist/modules/sso/services/ssoConfigService.js +9 -7
- package/dist/modules/sso/services/ssoConfigService.js.map +2 -2
- package/dist/modules/sso/services/ssoService.js +4 -2
- package/dist/modules/sso/services/ssoService.js.map +2 -2
- package/dist/modules/system_status_overlays/subscribers/application-bootstrap-enterprise-warning.js +4 -2
- package/dist/modules/system_status_overlays/subscribers/application-bootstrap-enterprise-warning.js.map +2 -2
- package/package.json +5 -5
- package/src/modules/record_locks/subscribers/conflict-detected-notification.ts +4 -1
- package/src/modules/record_locks/subscribers/conflict-resolved-notification.ts +4 -1
- package/src/modules/record_locks/subscribers/lock-contended-notification.ts +4 -1
- package/src/modules/record_locks/subscribers/lock-force-released-notification.ts +4 -1
- package/src/modules/record_locks/widgets/injection/record-locking/widget.client.tsx +5 -2
- package/src/modules/security/api/enforcement/_shared.ts +4 -1
- package/src/modules/security/api/mfa/_shared.ts +4 -1
- package/src/modules/security/api/profile/password/route.ts +4 -1
- package/src/modules/security/api/sudo/_shared.ts +4 -1
- package/src/modules/security/api/users/_shared.ts +4 -1
- package/src/modules/sso/api/callback/oidc/route.ts +6 -3
- package/src/modules/sso/api/error-handler.ts +5 -2
- package/src/modules/sso/api/hrd/route.ts +4 -1
- package/src/modules/sso/api/initiate/route.ts +5 -2
- package/src/modules/sso/backend/sso/config/[id]/page.tsx +2 -2
- package/src/modules/sso/i18n/de.json +2 -2
- package/src/modules/sso/i18n/en.json +2 -2
- package/src/modules/sso/i18n/es.json +2 -2
- package/src/modules/sso/i18n/pl.json +2 -2
- package/src/modules/sso/services/accountLinkingService.ts +10 -52
- package/src/modules/sso/services/ssoConfigService.ts +10 -7
- package/src/modules/sso/services/ssoService.ts +5 -2
- package/src/modules/system_status_overlays/subscribers/application-bootstrap-enterprise-warning.ts +6 -2
|
@@ -3,6 +3,8 @@ import { escapeLikePattern } from "@open-mercato/shared/lib/db/escapeLikePattern
|
|
|
3
3
|
import { SsoConfig, ScimToken } from "../data/entities.js";
|
|
4
4
|
import { emitSsoEvent } from "../events.js";
|
|
5
5
|
import { validateDomain, normalizeDomain, uniqueDomains, checkDomainLimit } from "../lib/domains.js";
|
|
6
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
7
|
+
const logger = createLogger("sso").child({ component: "config" });
|
|
6
8
|
class SsoConfigService {
|
|
7
9
|
constructor(em, tenantEncryptionService, ssoProviderRegistry) {
|
|
8
10
|
this.em = em;
|
|
@@ -89,7 +91,7 @@ class SsoConfigService {
|
|
|
89
91
|
id: config.id,
|
|
90
92
|
tenantId: config.tenantId,
|
|
91
93
|
organizationId: config.organizationId
|
|
92
|
-
}).catch((
|
|
94
|
+
}).catch((eventError) => logger.error("SSO event emit failed", { err: eventError }));
|
|
93
95
|
return this.toPublic(config);
|
|
94
96
|
}
|
|
95
97
|
async update(scope, id, input) {
|
|
@@ -123,7 +125,7 @@ class SsoConfigService {
|
|
|
123
125
|
id: config.id,
|
|
124
126
|
tenantId: config.tenantId,
|
|
125
127
|
organizationId: config.organizationId
|
|
126
|
-
}).catch((
|
|
128
|
+
}).catch((eventError) => logger.error("SSO event emit failed", { err: eventError }));
|
|
127
129
|
return this.toPublic(config);
|
|
128
130
|
}
|
|
129
131
|
async delete(scope, id) {
|
|
@@ -137,7 +139,7 @@ class SsoConfigService {
|
|
|
137
139
|
id: config.id,
|
|
138
140
|
tenantId: config.tenantId,
|
|
139
141
|
organizationId: config.organizationId
|
|
140
|
-
}).catch((
|
|
142
|
+
}).catch((eventError) => logger.error("SSO event emit failed", { err: eventError }));
|
|
141
143
|
}
|
|
142
144
|
async activate(scope, id, active) {
|
|
143
145
|
return this.em.transactional(async (txEm) => {
|
|
@@ -163,13 +165,13 @@ class SsoConfigService {
|
|
|
163
165
|
id: config.id,
|
|
164
166
|
tenantId: config.tenantId,
|
|
165
167
|
organizationId: config.organizationId
|
|
166
|
-
}).catch((
|
|
168
|
+
}).catch((eventError) => logger.error("SSO event emit failed", { err: eventError }));
|
|
167
169
|
} else if (!active && wasActive) {
|
|
168
170
|
void emitSsoEvent("sso.config.deactivated", {
|
|
169
171
|
id: config.id,
|
|
170
172
|
tenantId: config.tenantId,
|
|
171
173
|
organizationId: config.organizationId
|
|
172
|
-
}).catch((
|
|
174
|
+
}).catch((eventError) => logger.error("SSO event emit failed", { err: eventError }));
|
|
173
175
|
}
|
|
174
176
|
return this.toPublic(config);
|
|
175
177
|
});
|
|
@@ -201,7 +203,7 @@ class SsoConfigService {
|
|
|
201
203
|
tenantId: config.tenantId,
|
|
202
204
|
organizationId: config.organizationId,
|
|
203
205
|
domain: normalized
|
|
204
|
-
}).catch((
|
|
206
|
+
}).catch((eventError) => logger.error("SSO event emit failed", { err: eventError }));
|
|
205
207
|
return this.toPublic(config);
|
|
206
208
|
});
|
|
207
209
|
}
|
|
@@ -215,7 +217,7 @@ class SsoConfigService {
|
|
|
215
217
|
tenantId: config.tenantId,
|
|
216
218
|
organizationId: config.organizationId,
|
|
217
219
|
domain: normalized
|
|
218
|
-
}).catch((
|
|
220
|
+
}).catch((eventError) => logger.error("SSO event emit failed", { err: eventError }));
|
|
219
221
|
return this.toPublic(config);
|
|
220
222
|
}
|
|
221
223
|
toPublic(config) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/sso/services/ssoConfigService.ts"],
|
|
4
|
-
"sourcesContent": ["import type { FilterQuery, RequiredEntityData } from '@mikro-orm/core'\nimport { EntityManager } from '@mikro-orm/postgresql'\nimport { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { SsoConfig, ScimToken } from '../data/entities'\nimport type { SsoConfigAdminCreateInput, SsoConfigAdminUpdateInput, SsoConfigListQuery } from '../data/validators'\nimport { emitSsoEvent } from '../events'\nimport { validateDomain, normalizeDomain, uniqueDomains, checkDomainLimit } from '../lib/domains'\nimport type { SsoProviderRegistry } from '../lib/registry'\n\nexport interface SsoAdminScope {\n isSuperAdmin: boolean\n organizationId: string | null\n tenantId: string | null\n}\n\nexport interface SsoConfigPublic {\n id: string\n name: string | null\n tenantId: string | null\n organizationId: string\n protocol: string\n issuer: string | null\n clientId: string | null\n hasClientSecret: boolean\n allowedDomains: string[]\n jitEnabled: boolean\n autoLinkByEmail: boolean\n isActive: boolean\n ssoRequired: boolean\n appRoleMappings: Record<string, string>\n createdAt: Date\n updatedAt: Date\n}\n\nexport class SsoConfigService {\n constructor(\n private em: EntityManager,\n private tenantEncryptionService: TenantDataEncryptionService,\n private ssoProviderRegistry: SsoProviderRegistry,\n ) {}\n\n async list(scope: SsoAdminScope, query: SsoConfigListQuery): Promise<{\n items: SsoConfigPublic[]\n total: number\n totalPages: number\n }> {\n const where: FilterQuery<SsoConfig> = { deletedAt: null }\n\n if (!scope.isSuperAdmin) {\n if (!scope.organizationId) {\n throw new SsoConfigError('Organization context is required', 403)\n }\n where.organizationId = scope.organizationId\n } else {\n if (query.organizationId) where.organizationId = query.organizationId\n if (query.tenantId) where.tenantId = query.tenantId\n }\n\n if (query.search) {\n const pattern = `%${escapeLikePattern(query.search)}%`\n where.$or = [\n { name: { $ilike: pattern } },\n { issuer: { $ilike: pattern } },\n { clientId: { $ilike: pattern } },\n ]\n }\n\n const [configs, total] = await this.em.findAndCount(SsoConfig, where, {\n orderBy: { createdAt: 'desc' },\n limit: query.pageSize,\n offset: (query.page - 1) * query.pageSize,\n })\n\n return {\n items: configs.map((c) => this.toPublic(c)),\n total,\n totalPages: Math.ceil(total / query.pageSize) || 1,\n }\n }\n\n async getById(scope: SsoAdminScope, id: string): Promise<SsoConfigPublic | null> {\n const where: FilterQuery<SsoConfig> = { id, deletedAt: null }\n if (!scope.isSuperAdmin) {\n if (!scope.organizationId) throw new SsoConfigError('Organization context is required', 403)\n where.organizationId = scope.organizationId\n }\n\n const config = await this.em.findOne(SsoConfig, where)\n return config ? this.toPublic(config) : null\n }\n\n async create(scope: SsoAdminScope, input: SsoConfigAdminCreateInput): Promise<SsoConfigPublic> {\n const orgId = scope.isSuperAdmin ? input.organizationId : scope.organizationId!\n const tenId = scope.isSuperAdmin ? (input.tenantId ?? null) : scope.tenantId\n\n const existing = await this.em.findOne(SsoConfig, {\n organizationId: orgId,\n deletedAt: null,\n })\n if (existing) {\n throw new SsoConfigError('An SSO configuration already exists for this organization', 409)\n }\n\n const domains = uniqueDomains(input.allowedDomains)\n for (const d of domains) {\n const result = validateDomain(d)\n if (!result.valid) throw new SsoConfigError(`Invalid domain \"${d}\": ${result.error}`, 400)\n }\n\n const encrypted = await this.tenantEncryptionService.encryptEntityPayload(\n 'SsoConfig',\n { clientSecretEnc: input.clientSecret },\n tenId,\n orgId,\n )\n\n const config = this.em.create(SsoConfig, {\n name: input.name,\n tenantId: tenId,\n organizationId: orgId,\n protocol: input.protocol,\n issuer: input.issuer,\n clientId: input.clientId,\n clientSecretEnc: encrypted.clientSecretEnc as string,\n allowedDomains: domains,\n jitEnabled: input.jitEnabled,\n autoLinkByEmail: input.autoLinkByEmail,\n isActive: false,\n ssoRequired: false,\n appRoleMappings: input.appRoleMappings ?? {},\n } as RequiredEntityData<SsoConfig>)\n\n await this.em.persist(config).flush()\n\n void emitSsoEvent('sso.config.created', {\n id: config.id,\n tenantId: config.tenantId,\n organizationId: config.organizationId,\n }).catch((e) => console.error('[SSO Event]', e))\n\n return this.toPublic(config)\n }\n\n async update(scope: SsoAdminScope, id: string, input: SsoConfigAdminUpdateInput): Promise<SsoConfigPublic> {\n const config = await this.resolveConfig(scope, id)\n\n if (input.name !== undefined) config.name = input.name\n if (input.protocol !== undefined) config.protocol = input.protocol\n if (input.issuer !== undefined) config.issuer = input.issuer\n if (input.clientId !== undefined) config.clientId = input.clientId\n if (input.jitEnabled !== undefined) {\n if (input.jitEnabled) {\n const activeScimCount = await this.em.count(ScimToken, { ssoConfigId: id, isActive: true })\n if (activeScimCount > 0) {\n throw new SsoConfigError('Cannot enable JIT provisioning while SCIM directory sync is active. Revoke all SCIM tokens first.', 409)\n }\n }\n config.jitEnabled = input.jitEnabled\n }\n if (input.autoLinkByEmail !== undefined) config.autoLinkByEmail = input.autoLinkByEmail\n if (input.appRoleMappings !== undefined) config.appRoleMappings = input.appRoleMappings\n\n if (input.clientSecret !== undefined) {\n const encrypted = await this.tenantEncryptionService.encryptEntityPayload(\n 'SsoConfig',\n { clientSecretEnc: input.clientSecret },\n config.tenantId,\n config.organizationId,\n )\n config.clientSecretEnc = encrypted.clientSecretEnc as string\n }\n\n await this.em.flush()\n\n void emitSsoEvent('sso.config.updated', {\n id: config.id,\n tenantId: config.tenantId,\n organizationId: config.organizationId,\n }).catch((e) => console.error('[SSO Event]', e))\n\n return this.toPublic(config)\n }\n\n async delete(scope: SsoAdminScope, id: string): Promise<void> {\n const config = await this.resolveConfig(scope, id)\n\n if (config.isActive) {\n throw new SsoConfigError('Cannot delete an active SSO configuration \u2014 deactivate it first', 400)\n }\n\n config.deletedAt = new Date()\n await this.em.flush()\n\n void emitSsoEvent('sso.config.deleted', {\n id: config.id,\n tenantId: config.tenantId,\n organizationId: config.organizationId,\n }).catch((e) => console.error('[SSO Event]', e))\n }\n\n async activate(scope: SsoAdminScope, id: string, active: boolean): Promise<SsoConfigPublic> {\n return this.em.transactional(async (txEm) => {\n const tx = txEm as EntityManager\n await this.lockActiveDomainMutation(tx, id)\n const config = await this.resolveConfig(scope, id, tx)\n\n if (active) {\n if (config.allowedDomains.length === 0) {\n throw new SsoConfigError('Cannot activate SSO configuration with no allowed domains', 400)\n }\n\n await this.lockActiveDomains(tx, config.allowedDomains)\n await this.assertActiveDomainAvailability(tx, config.allowedDomains, config.id)\n\n const testResult = await this.testConnectionInternal(config)\n if (!testResult.ok) {\n throw new SsoConfigError(`Cannot activate \u2014 discovery failed: ${testResult.error}`, 400)\n }\n }\n\n const wasActive = config.isActive\n config.isActive = active\n await tx.flush()\n\n if (active && !wasActive) {\n void emitSsoEvent('sso.config.activated', {\n id: config.id,\n tenantId: config.tenantId,\n organizationId: config.organizationId,\n }).catch((e) => console.error('[SSO Event]', e))\n } else if (!active && wasActive) {\n void emitSsoEvent('sso.config.deactivated', {\n id: config.id,\n tenantId: config.tenantId,\n organizationId: config.organizationId,\n }).catch((e) => console.error('[SSO Event]', e))\n }\n\n return this.toPublic(config)\n })\n }\n\n async testConnection(scope: SsoAdminScope, id: string): Promise<{ ok: boolean; error?: string }> {\n const config = await this.resolveConfig(scope, id)\n return this.testConnectionInternal(config)\n }\n\n async addDomain(scope: SsoAdminScope, id: string, domain: string): Promise<SsoConfigPublic> {\n const normalized = normalizeDomain(domain)\n const validation = validateDomain(normalized)\n if (!validation.valid) throw new SsoConfigError(validation.error!, 400)\n\n return this.em.transactional(async (txEm) => {\n const tx = txEm as EntityManager\n await this.lockActiveDomainMutation(tx, id, [normalized])\n const config = await this.resolveConfig(scope, id, tx)\n\n if (config.allowedDomains.some((d) => normalizeDomain(d) === normalized)) {\n return this.toPublic(config)\n }\n\n const limitCheck = checkDomainLimit(config.allowedDomains.length, 1)\n if (!limitCheck.ok) throw new SsoConfigError(limitCheck.error!, 400)\n\n if (config.isActive) {\n await this.assertActiveDomainAvailability(tx, [...config.allowedDomains, normalized], config.id)\n }\n\n config.allowedDomains = [...config.allowedDomains, normalized]\n await tx.flush()\n\n void emitSsoEvent('sso.domain.added', {\n id: config.id,\n tenantId: config.tenantId,\n organizationId: config.organizationId,\n domain: normalized,\n }).catch((e) => console.error('[SSO Event]', e))\n\n return this.toPublic(config)\n })\n }\n\n async removeDomain(scope: SsoAdminScope, id: string, domain: string): Promise<SsoConfigPublic> {\n const normalized = normalizeDomain(domain)\n const config = await this.resolveConfig(scope, id)\n\n config.allowedDomains = config.allowedDomains.filter((d) => d !== normalized)\n await this.em.flush()\n\n void emitSsoEvent('sso.domain.removed', {\n id: config.id,\n tenantId: config.tenantId,\n organizationId: config.organizationId,\n domain: normalized,\n }).catch((e) => console.error('[SSO Event]', e))\n\n return this.toPublic(config)\n }\n\n toPublic(config: SsoConfig): SsoConfigPublic {\n return {\n id: config.id,\n name: config.name ?? null,\n tenantId: config.tenantId ?? null,\n organizationId: config.organizationId,\n protocol: config.protocol,\n issuer: config.issuer ?? null,\n clientId: config.clientId ?? null,\n hasClientSecret: !!config.clientSecretEnc,\n allowedDomains: config.allowedDomains,\n jitEnabled: config.jitEnabled,\n autoLinkByEmail: config.autoLinkByEmail,\n isActive: config.isActive,\n ssoRequired: config.ssoRequired,\n appRoleMappings: config.appRoleMappings ?? {},\n createdAt: config.createdAt,\n updatedAt: config.updatedAt,\n }\n }\n\n private async resolveConfig(scope: SsoAdminScope, id: string, em: EntityManager = this.em): Promise<SsoConfig> {\n const where: FilterQuery<SsoConfig> = { id, deletedAt: null }\n if (!scope.isSuperAdmin) {\n if (!scope.organizationId) throw new SsoConfigError('Organization context is required', 403)\n where.organizationId = scope.organizationId\n }\n\n const config = await em.findOne(SsoConfig, where)\n if (!config) throw new SsoConfigError('SSO configuration not found', 404)\n\n return config\n }\n\n private async testConnectionInternal(config: SsoConfig): Promise<{ ok: boolean; error?: string }> {\n const provider = this.ssoProviderRegistry.resolve(config.protocol)\n if (!provider) return { ok: false, error: `No provider for protocol: ${config.protocol}` }\n\n return provider.validateConfig(config)\n }\n\n private async lockActiveDomainMutation(em: EntityManager, configId: string, domains: string[] = []): Promise<void> {\n const lockKeys = [\n `sso:sso_config:${configId}`,\n ...uniqueDomains(domains)\n .sort((a, b) => a.localeCompare(b))\n .map((domain) => `sso:allowed_domain:${domain}`),\n ]\n\n await this.lockKeys(em, lockKeys)\n }\n\n private async lockActiveDomains(em: EntityManager, domains: string[]): Promise<void> {\n const lockKeys = uniqueDomains(domains)\n .sort((a, b) => a.localeCompare(b))\n .map((domain) => `sso:allowed_domain:${domain}`)\n await this.lockKeys(em, lockKeys)\n }\n\n private async lockKeys(em: EntityManager, lockKeys: string[]): Promise<void> {\n for (const lockKey of lockKeys) {\n await em.getConnection().execute('select pg_advisory_xact_lock(hashtext(?::text))', [lockKey])\n }\n }\n\n private async assertActiveDomainAvailability(\n em: EntityManager,\n domains: string[],\n currentConfigId: string,\n ): Promise<void> {\n const requestedDomains = new Set(uniqueDomains(domains))\n if (requestedDomains.size === 0) return\n\n const activeConfigs = await findWithDecryption(\n em,\n SsoConfig,\n { isActive: true, deletedAt: null },\n {},\n { tenantId: null },\n )\n const conflictDomain = activeConfigs\n .filter((config) => config.id !== currentConfigId)\n .flatMap((config) => config.allowedDomains.map(normalizeDomain))\n .find((domain) => requestedDomains.has(domain))\n\n if (conflictDomain) {\n throw new SsoConfigError(\n `SSO domain \"${conflictDomain}\" is already claimed by another active SSO configuration`,\n 409,\n )\n }\n }\n}\n\nexport class SsoConfigError extends Error {\n constructor(\n message: string,\n public readonly statusCode: number,\n ) {\n super(message)\n this.name = 'SsoConfigError'\n }\n}\n"],
|
|
5
|
-
"mappings": "AAEA,SAAS,0BAAiD;AAE1D,SAAS,yBAAyB;AAClC,SAAS,WAAW,iBAAiB;AAErC,SAAS,oBAAoB;AAC7B,SAAS,gBAAgB,iBAAiB,eAAe,wBAAwB;
|
|
4
|
+
"sourcesContent": ["import type { FilterQuery, RequiredEntityData } from '@mikro-orm/core'\nimport { EntityManager } from '@mikro-orm/postgresql'\nimport { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { SsoConfig, ScimToken } from '../data/entities'\nimport type { SsoConfigAdminCreateInput, SsoConfigAdminUpdateInput, SsoConfigListQuery } from '../data/validators'\nimport { emitSsoEvent } from '../events'\nimport { validateDomain, normalizeDomain, uniqueDomains, checkDomainLimit } from '../lib/domains'\nimport type { SsoProviderRegistry } from '../lib/registry'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('sso').child({ component: 'config' })\n\nexport interface SsoAdminScope {\n isSuperAdmin: boolean\n organizationId: string | null\n tenantId: string | null\n}\n\nexport interface SsoConfigPublic {\n id: string\n name: string | null\n tenantId: string | null\n organizationId: string\n protocol: string\n issuer: string | null\n clientId: string | null\n hasClientSecret: boolean\n allowedDomains: string[]\n jitEnabled: boolean\n autoLinkByEmail: boolean\n isActive: boolean\n ssoRequired: boolean\n appRoleMappings: Record<string, string>\n createdAt: Date\n updatedAt: Date\n}\n\nexport class SsoConfigService {\n constructor(\n private em: EntityManager,\n private tenantEncryptionService: TenantDataEncryptionService,\n private ssoProviderRegistry: SsoProviderRegistry,\n ) {}\n\n async list(scope: SsoAdminScope, query: SsoConfigListQuery): Promise<{\n items: SsoConfigPublic[]\n total: number\n totalPages: number\n }> {\n const where: FilterQuery<SsoConfig> = { deletedAt: null }\n\n if (!scope.isSuperAdmin) {\n if (!scope.organizationId) {\n throw new SsoConfigError('Organization context is required', 403)\n }\n where.organizationId = scope.organizationId\n } else {\n if (query.organizationId) where.organizationId = query.organizationId\n if (query.tenantId) where.tenantId = query.tenantId\n }\n\n if (query.search) {\n const pattern = `%${escapeLikePattern(query.search)}%`\n where.$or = [\n { name: { $ilike: pattern } },\n { issuer: { $ilike: pattern } },\n { clientId: { $ilike: pattern } },\n ]\n }\n\n const [configs, total] = await this.em.findAndCount(SsoConfig, where, {\n orderBy: { createdAt: 'desc' },\n limit: query.pageSize,\n offset: (query.page - 1) * query.pageSize,\n })\n\n return {\n items: configs.map((c) => this.toPublic(c)),\n total,\n totalPages: Math.ceil(total / query.pageSize) || 1,\n }\n }\n\n async getById(scope: SsoAdminScope, id: string): Promise<SsoConfigPublic | null> {\n const where: FilterQuery<SsoConfig> = { id, deletedAt: null }\n if (!scope.isSuperAdmin) {\n if (!scope.organizationId) throw new SsoConfigError('Organization context is required', 403)\n where.organizationId = scope.organizationId\n }\n\n const config = await this.em.findOne(SsoConfig, where)\n return config ? this.toPublic(config) : null\n }\n\n async create(scope: SsoAdminScope, input: SsoConfigAdminCreateInput): Promise<SsoConfigPublic> {\n const orgId = scope.isSuperAdmin ? input.organizationId : scope.organizationId!\n const tenId = scope.isSuperAdmin ? (input.tenantId ?? null) : scope.tenantId\n\n const existing = await this.em.findOne(SsoConfig, {\n organizationId: orgId,\n deletedAt: null,\n })\n if (existing) {\n throw new SsoConfigError('An SSO configuration already exists for this organization', 409)\n }\n\n const domains = uniqueDomains(input.allowedDomains)\n for (const d of domains) {\n const result = validateDomain(d)\n if (!result.valid) throw new SsoConfigError(`Invalid domain \"${d}\": ${result.error}`, 400)\n }\n\n const encrypted = await this.tenantEncryptionService.encryptEntityPayload(\n 'SsoConfig',\n { clientSecretEnc: input.clientSecret },\n tenId,\n orgId,\n )\n\n const config = this.em.create(SsoConfig, {\n name: input.name,\n tenantId: tenId,\n organizationId: orgId,\n protocol: input.protocol,\n issuer: input.issuer,\n clientId: input.clientId,\n clientSecretEnc: encrypted.clientSecretEnc as string,\n allowedDomains: domains,\n jitEnabled: input.jitEnabled,\n autoLinkByEmail: input.autoLinkByEmail,\n isActive: false,\n ssoRequired: false,\n appRoleMappings: input.appRoleMappings ?? {},\n } as RequiredEntityData<SsoConfig>)\n\n await this.em.persist(config).flush()\n\n void emitSsoEvent('sso.config.created', {\n id: config.id,\n tenantId: config.tenantId,\n organizationId: config.organizationId,\n }).catch((eventError) => logger.error('SSO event emit failed', { err: eventError }))\n\n return this.toPublic(config)\n }\n\n async update(scope: SsoAdminScope, id: string, input: SsoConfigAdminUpdateInput): Promise<SsoConfigPublic> {\n const config = await this.resolveConfig(scope, id)\n\n if (input.name !== undefined) config.name = input.name\n if (input.protocol !== undefined) config.protocol = input.protocol\n if (input.issuer !== undefined) config.issuer = input.issuer\n if (input.clientId !== undefined) config.clientId = input.clientId\n if (input.jitEnabled !== undefined) {\n if (input.jitEnabled) {\n const activeScimCount = await this.em.count(ScimToken, { ssoConfigId: id, isActive: true })\n if (activeScimCount > 0) {\n throw new SsoConfigError('Cannot enable JIT provisioning while SCIM directory sync is active. Revoke all SCIM tokens first.', 409)\n }\n }\n config.jitEnabled = input.jitEnabled\n }\n if (input.autoLinkByEmail !== undefined) config.autoLinkByEmail = input.autoLinkByEmail\n if (input.appRoleMappings !== undefined) config.appRoleMappings = input.appRoleMappings\n\n if (input.clientSecret !== undefined) {\n const encrypted = await this.tenantEncryptionService.encryptEntityPayload(\n 'SsoConfig',\n { clientSecretEnc: input.clientSecret },\n config.tenantId,\n config.organizationId,\n )\n config.clientSecretEnc = encrypted.clientSecretEnc as string\n }\n\n await this.em.flush()\n\n void emitSsoEvent('sso.config.updated', {\n id: config.id,\n tenantId: config.tenantId,\n organizationId: config.organizationId,\n }).catch((eventError) => logger.error('SSO event emit failed', { err: eventError }))\n\n return this.toPublic(config)\n }\n\n async delete(scope: SsoAdminScope, id: string): Promise<void> {\n const config = await this.resolveConfig(scope, id)\n\n if (config.isActive) {\n throw new SsoConfigError('Cannot delete an active SSO configuration \u2014 deactivate it first', 400)\n }\n\n config.deletedAt = new Date()\n await this.em.flush()\n\n void emitSsoEvent('sso.config.deleted', {\n id: config.id,\n tenantId: config.tenantId,\n organizationId: config.organizationId,\n }).catch((eventError) => logger.error('SSO event emit failed', { err: eventError }))\n }\n\n async activate(scope: SsoAdminScope, id: string, active: boolean): Promise<SsoConfigPublic> {\n return this.em.transactional(async (txEm) => {\n const tx = txEm as EntityManager\n await this.lockActiveDomainMutation(tx, id)\n const config = await this.resolveConfig(scope, id, tx)\n\n if (active) {\n if (config.allowedDomains.length === 0) {\n throw new SsoConfigError('Cannot activate SSO configuration with no allowed domains', 400)\n }\n\n await this.lockActiveDomains(tx, config.allowedDomains)\n await this.assertActiveDomainAvailability(tx, config.allowedDomains, config.id)\n\n const testResult = await this.testConnectionInternal(config)\n if (!testResult.ok) {\n throw new SsoConfigError(`Cannot activate \u2014 discovery failed: ${testResult.error}`, 400)\n }\n }\n\n const wasActive = config.isActive\n config.isActive = active\n await tx.flush()\n\n if (active && !wasActive) {\n void emitSsoEvent('sso.config.activated', {\n id: config.id,\n tenantId: config.tenantId,\n organizationId: config.organizationId,\n }).catch((eventError) => logger.error('SSO event emit failed', { err: eventError }))\n } else if (!active && wasActive) {\n void emitSsoEvent('sso.config.deactivated', {\n id: config.id,\n tenantId: config.tenantId,\n organizationId: config.organizationId,\n }).catch((eventError) => logger.error('SSO event emit failed', { err: eventError }))\n }\n\n return this.toPublic(config)\n })\n }\n\n async testConnection(scope: SsoAdminScope, id: string): Promise<{ ok: boolean; error?: string }> {\n const config = await this.resolveConfig(scope, id)\n return this.testConnectionInternal(config)\n }\n\n async addDomain(scope: SsoAdminScope, id: string, domain: string): Promise<SsoConfigPublic> {\n const normalized = normalizeDomain(domain)\n const validation = validateDomain(normalized)\n if (!validation.valid) throw new SsoConfigError(validation.error!, 400)\n\n return this.em.transactional(async (txEm) => {\n const tx = txEm as EntityManager\n await this.lockActiveDomainMutation(tx, id, [normalized])\n const config = await this.resolveConfig(scope, id, tx)\n\n if (config.allowedDomains.some((d) => normalizeDomain(d) === normalized)) {\n return this.toPublic(config)\n }\n\n const limitCheck = checkDomainLimit(config.allowedDomains.length, 1)\n if (!limitCheck.ok) throw new SsoConfigError(limitCheck.error!, 400)\n\n if (config.isActive) {\n await this.assertActiveDomainAvailability(tx, [...config.allowedDomains, normalized], config.id)\n }\n\n config.allowedDomains = [...config.allowedDomains, normalized]\n await tx.flush()\n\n void emitSsoEvent('sso.domain.added', {\n id: config.id,\n tenantId: config.tenantId,\n organizationId: config.organizationId,\n domain: normalized,\n }).catch((eventError) => logger.error('SSO event emit failed', { err: eventError }))\n\n return this.toPublic(config)\n })\n }\n\n async removeDomain(scope: SsoAdminScope, id: string, domain: string): Promise<SsoConfigPublic> {\n const normalized = normalizeDomain(domain)\n const config = await this.resolveConfig(scope, id)\n\n config.allowedDomains = config.allowedDomains.filter((d) => d !== normalized)\n await this.em.flush()\n\n void emitSsoEvent('sso.domain.removed', {\n id: config.id,\n tenantId: config.tenantId,\n organizationId: config.organizationId,\n domain: normalized,\n }).catch((eventError) => logger.error('SSO event emit failed', { err: eventError }))\n\n return this.toPublic(config)\n }\n\n toPublic(config: SsoConfig): SsoConfigPublic {\n return {\n id: config.id,\n name: config.name ?? null,\n tenantId: config.tenantId ?? null,\n organizationId: config.organizationId,\n protocol: config.protocol,\n issuer: config.issuer ?? null,\n clientId: config.clientId ?? null,\n hasClientSecret: !!config.clientSecretEnc,\n allowedDomains: config.allowedDomains,\n jitEnabled: config.jitEnabled,\n autoLinkByEmail: config.autoLinkByEmail,\n isActive: config.isActive,\n ssoRequired: config.ssoRequired,\n appRoleMappings: config.appRoleMappings ?? {},\n createdAt: config.createdAt,\n updatedAt: config.updatedAt,\n }\n }\n\n private async resolveConfig(scope: SsoAdminScope, id: string, em: EntityManager = this.em): Promise<SsoConfig> {\n const where: FilterQuery<SsoConfig> = { id, deletedAt: null }\n if (!scope.isSuperAdmin) {\n if (!scope.organizationId) throw new SsoConfigError('Organization context is required', 403)\n where.organizationId = scope.organizationId\n }\n\n const config = await em.findOne(SsoConfig, where)\n if (!config) throw new SsoConfigError('SSO configuration not found', 404)\n\n return config\n }\n\n private async testConnectionInternal(config: SsoConfig): Promise<{ ok: boolean; error?: string }> {\n const provider = this.ssoProviderRegistry.resolve(config.protocol)\n if (!provider) return { ok: false, error: `No provider for protocol: ${config.protocol}` }\n\n return provider.validateConfig(config)\n }\n\n private async lockActiveDomainMutation(em: EntityManager, configId: string, domains: string[] = []): Promise<void> {\n const lockKeys = [\n `sso:sso_config:${configId}`,\n ...uniqueDomains(domains)\n .sort((a, b) => a.localeCompare(b))\n .map((domain) => `sso:allowed_domain:${domain}`),\n ]\n\n await this.lockKeys(em, lockKeys)\n }\n\n private async lockActiveDomains(em: EntityManager, domains: string[]): Promise<void> {\n const lockKeys = uniqueDomains(domains)\n .sort((a, b) => a.localeCompare(b))\n .map((domain) => `sso:allowed_domain:${domain}`)\n await this.lockKeys(em, lockKeys)\n }\n\n private async lockKeys(em: EntityManager, lockKeys: string[]): Promise<void> {\n for (const lockKey of lockKeys) {\n await em.getConnection().execute('select pg_advisory_xact_lock(hashtext(?::text))', [lockKey])\n }\n }\n\n private async assertActiveDomainAvailability(\n em: EntityManager,\n domains: string[],\n currentConfigId: string,\n ): Promise<void> {\n const requestedDomains = new Set(uniqueDomains(domains))\n if (requestedDomains.size === 0) return\n\n const activeConfigs = await findWithDecryption(\n em,\n SsoConfig,\n { isActive: true, deletedAt: null },\n {},\n { tenantId: null },\n )\n const conflictDomain = activeConfigs\n .filter((config) => config.id !== currentConfigId)\n .flatMap((config) => config.allowedDomains.map(normalizeDomain))\n .find((domain) => requestedDomains.has(domain))\n\n if (conflictDomain) {\n throw new SsoConfigError(\n `SSO domain \"${conflictDomain}\" is already claimed by another active SSO configuration`,\n 409,\n )\n }\n }\n}\n\nexport class SsoConfigError extends Error {\n constructor(\n message: string,\n public readonly statusCode: number,\n ) {\n super(message)\n this.name = 'SsoConfigError'\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -4,6 +4,8 @@ import { signJwt } from "@open-mercato/shared/lib/auth/jwt";
|
|
|
4
4
|
import { SsoConfig } from "../data/entities.js";
|
|
5
5
|
import { encryptStateCookie, decryptStateCookie, createFlowState } from "../lib/state-cookie.js";
|
|
6
6
|
import { emitSsoEvent } from "../events.js";
|
|
7
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
8
|
+
const logger = createLogger("sso").child({ component: "service" });
|
|
7
9
|
class SsoService {
|
|
8
10
|
constructor(em, ssoProviderRegistry, accountLinkingService, tenantEncryptionService, authService, rbacService) {
|
|
9
11
|
this.em = em;
|
|
@@ -42,7 +44,7 @@ class SsoService {
|
|
|
42
44
|
void emitSsoEvent("sso.login.initiated", {
|
|
43
45
|
tenantId: config.tenantId,
|
|
44
46
|
organizationId: config.organizationId
|
|
45
|
-
}).catch((
|
|
47
|
+
}).catch((eventError) => logger.error("SSO event emit failed", { err: eventError }));
|
|
46
48
|
const authUrl = await provider.buildAuthUrl(config, {
|
|
47
49
|
state: state.state,
|
|
48
50
|
nonce: state.nonce,
|
|
@@ -100,7 +102,7 @@ class SsoService {
|
|
|
100
102
|
id: String(user.id),
|
|
101
103
|
tenantId: config.tenantId,
|
|
102
104
|
organizationId: config.organizationId
|
|
103
|
-
}).catch((
|
|
105
|
+
}).catch((eventError) => logger.error("SSO event emit failed", { err: eventError }));
|
|
104
106
|
return {
|
|
105
107
|
token,
|
|
106
108
|
sessionToken: sessionRefreshToken,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/sso/services/ssoService.ts"],
|
|
4
|
-
"sourcesContent": ["import crypto from 'node:crypto'\nimport { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { signJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport type { AuthService } from '@open-mercato/core/modules/auth/services/authService'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { SsoConfig } from '../data/entities'\nimport type { SsoProviderRegistry } from '../lib/registry'\nimport type { AccountLinkingService } from './accountLinkingService'\nimport { encryptStateCookie, decryptStateCookie, createFlowState } from '../lib/state-cookie'\nimport { emitSsoEvent } from '../events'\nimport type { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\n\nexport class SsoService {\n constructor(\n private em: EntityManager,\n private ssoProviderRegistry: SsoProviderRegistry,\n private accountLinkingService: AccountLinkingService,\n private tenantEncryptionService: TenantDataEncryptionService,\n private authService: AuthService,\n private rbacService: RbacService,\n ) {}\n\n async findConfigByEmail(email: string): Promise<SsoConfig | null> {\n const domain = email.split('@')[1]?.toLowerCase()\n if (!domain) return null\n\n const configs = await findWithDecryption(\n this.em,\n SsoConfig,\n { isActive: true, deletedAt: null },\n {},\n { tenantId: null },\n )\n const matches = configs.filter((c) => c.allowedDomains.some((d) => d.toLowerCase() === domain))\n return matches.length === 1 ? matches[0]! : null\n }\n\n async initiateLogin(\n configId: string,\n returnUrl: string,\n redirectUri: string,\n ): Promise<{ redirectUrl: string; stateCookie: string }> {\n const config = await findOneWithDecryption(\n this.em,\n SsoConfig,\n { id: configId, isActive: true, deletedAt: null },\n {},\n { tenantId: null },\n )\n if (!config) throw new Error('SSO configuration not found or inactive')\n\n const provider = this.ssoProviderRegistry.resolve(config.protocol)\n if (!provider) throw new Error(`No provider registered for protocol: ${config.protocol}`)\n\n const clientSecret = await this.decryptClientSecret(config)\n\n const { state } = createFlowState({ configId, returnUrl })\n\n void emitSsoEvent('sso.login.initiated', {\n tenantId: config.tenantId,\n organizationId: config.organizationId,\n }).catch((
|
|
5
|
-
"mappings": "AAAA,OAAO,YAAY;AAEnB,SAAS,uBAAuB,0BAA0B;AAC1D,SAAS,eAAe;AAGxB,SAAS,iBAAiB;AAG1B,SAAS,oBAAoB,oBAAoB,uBAAuB;AACxE,SAAS,oBAAoB;
|
|
4
|
+
"sourcesContent": ["import crypto from 'node:crypto'\nimport { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { signJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport type { AuthService } from '@open-mercato/core/modules/auth/services/authService'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { SsoConfig } from '../data/entities'\nimport type { SsoProviderRegistry } from '../lib/registry'\nimport type { AccountLinkingService } from './accountLinkingService'\nimport { encryptStateCookie, decryptStateCookie, createFlowState } from '../lib/state-cookie'\nimport { emitSsoEvent } from '../events'\nimport type { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('sso').child({ component: 'service' })\n\nexport class SsoService {\n constructor(\n private em: EntityManager,\n private ssoProviderRegistry: SsoProviderRegistry,\n private accountLinkingService: AccountLinkingService,\n private tenantEncryptionService: TenantDataEncryptionService,\n private authService: AuthService,\n private rbacService: RbacService,\n ) {}\n\n async findConfigByEmail(email: string): Promise<SsoConfig | null> {\n const domain = email.split('@')[1]?.toLowerCase()\n if (!domain) return null\n\n const configs = await findWithDecryption(\n this.em,\n SsoConfig,\n { isActive: true, deletedAt: null },\n {},\n { tenantId: null },\n )\n const matches = configs.filter((c) => c.allowedDomains.some((d) => d.toLowerCase() === domain))\n return matches.length === 1 ? matches[0]! : null\n }\n\n async initiateLogin(\n configId: string,\n returnUrl: string,\n redirectUri: string,\n ): Promise<{ redirectUrl: string; stateCookie: string }> {\n const config = await findOneWithDecryption(\n this.em,\n SsoConfig,\n { id: configId, isActive: true, deletedAt: null },\n {},\n { tenantId: null },\n )\n if (!config) throw new Error('SSO configuration not found or inactive')\n\n const provider = this.ssoProviderRegistry.resolve(config.protocol)\n if (!provider) throw new Error(`No provider registered for protocol: ${config.protocol}`)\n\n const clientSecret = await this.decryptClientSecret(config)\n\n const { state } = createFlowState({ configId, returnUrl })\n\n void emitSsoEvent('sso.login.initiated', {\n tenantId: config.tenantId,\n organizationId: config.organizationId,\n }).catch((eventError) => logger.error('SSO event emit failed', { err: eventError }))\n\n const authUrl = await provider.buildAuthUrl(config, {\n state: state.state,\n nonce: state.nonce,\n redirectUri,\n codeVerifier: state.codeVerifier,\n clientSecret,\n })\n\n const stateCookie = encryptStateCookie(state)\n return { redirectUrl: authUrl, stateCookie }\n }\n\n async handleOidcCallback(\n callbackParams: Record<string, string>,\n stateCookie: string,\n redirectUri: string,\n ): Promise<{\n token: string\n sessionToken: string\n sessionExpiresAt: Date\n redirectUrl: string\n tenantId: string | null\n organizationId: string\n }> {\n const flowState = decryptStateCookie(stateCookie)\n if (!flowState) throw new Error('Invalid or expired SSO state')\n\n const receivedState = Buffer.from(callbackParams.state || '')\n const expectedState = Buffer.from(flowState.state)\n if (receivedState.length !== expectedState.length || !crypto.timingSafeEqual(receivedState, expectedState)) {\n throw new Error('State mismatch \u2014 possible CSRF attack')\n }\n\n const config = await findOneWithDecryption(\n this.em,\n SsoConfig,\n { id: flowState.configId, isActive: true, deletedAt: null },\n {},\n { tenantId: null },\n )\n if (!config) throw new Error('SSO configuration no longer active')\n\n const provider = this.ssoProviderRegistry.resolve(config.protocol)\n if (!provider) throw new Error(`No provider for protocol: ${config.protocol}`)\n\n const clientSecret = await this.decryptClientSecret(config)\n\n const idpPayload = await provider.handleCallback(config, {\n callbackParams,\n redirectUri,\n expectedState: flowState.state,\n expectedNonce: flowState.nonce,\n codeVerifier: flowState.codeVerifier,\n clientSecret,\n })\n\n const tenantId = config.tenantId ?? ''\n const { user } = await this.accountLinkingService.resolveUser(config, idpPayload, tenantId)\n\n await this.rbacService.invalidateUserCache(String(user.id))\n\n const roles = await this.authService.getUserRoles(user, tenantId || null)\n\n await this.authService.updateLastLoginAt(user)\n\n const days = Number(process.env.REMEMBER_ME_DAYS || '30')\n const sessionExpiresAt = new Date(Date.now() + days * 24 * 60 * 60 * 1000)\n const { session, token: sessionRefreshToken } = await this.authService.createSession(user, sessionExpiresAt)\n\n const token = signJwt({\n sub: String(user.id),\n sid: String(session.id),\n tenantId: tenantId || null,\n orgId: user.organizationId ? String(user.organizationId) : null,\n email: user.email,\n roles,\n })\n\n void emitSsoEvent('sso.login.completed', {\n id: String(user.id),\n tenantId: config.tenantId,\n organizationId: config.organizationId,\n }).catch((eventError) => logger.error('SSO event emit failed', { err: eventError }))\n\n return {\n token,\n sessionToken: sessionRefreshToken,\n sessionExpiresAt,\n redirectUrl: flowState.returnUrl || '/backend',\n tenantId: config.tenantId ?? null,\n organizationId: config.organizationId,\n }\n }\n\n private async decryptClientSecret(config: SsoConfig): Promise<string | undefined> {\n if (!config.clientSecretEnc) return undefined\n\n const decrypted = await this.tenantEncryptionService.decryptEntityPayload(\n config.id,\n { clientSecretEnc: config.clientSecretEnc },\n config.tenantId,\n config.organizationId,\n )\n return (decrypted.clientSecretEnc as string) ?? undefined\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/modules/system_status_overlays/subscribers/application-bootstrap-enterprise-warning.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
2
|
+
const logger = createLogger("system_status_overlays");
|
|
1
3
|
const APP_BOOTSTRAP_EVENT = "application.bootstrap.completed";
|
|
2
4
|
const ENTERPRISE_WARNING_GLOBAL_KEY = "__openMercatoEnterpriseLicenseWarningShown__";
|
|
3
5
|
const metadata = {
|
|
@@ -8,8 +10,8 @@ const metadata = {
|
|
|
8
10
|
async function handle() {
|
|
9
11
|
if (globalThis[ENTERPRISE_WARNING_GLOBAL_KEY] === true) return;
|
|
10
12
|
globalThis[ENTERPRISE_WARNING_GLOBAL_KEY] = true;
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
+
logger.warn(
|
|
14
|
+
"Enterprise modules are enabled. Developer preview is free, but production usage requires a commercial enterprise license. See: https://github.com/open-mercato/open-mercato/blob/main/packages/enterprise/README.md"
|
|
13
15
|
);
|
|
14
16
|
}
|
|
15
17
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/system_status_overlays/subscribers/application-bootstrap-enterprise-warning.ts"],
|
|
4
|
-
"sourcesContent": ["
|
|
5
|
-
"mappings": "AAAA,MAAM,sBAAsB;AAC5B,MAAM,gCAAgC;AAE/B,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAEA,eAAO,SAAgC;AACrC,MAAK,WAAuC,6BAA6B,MAAM,KAAM;AAEpF,EAAC,WAAuC,6BAA6B,IAAI;AAC1E,
|
|
4
|
+
"sourcesContent": ["import { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('system_status_overlays')\n\nconst APP_BOOTSTRAP_EVENT = 'application.bootstrap.completed'\nconst ENTERPRISE_WARNING_GLOBAL_KEY = '__openMercatoEnterpriseLicenseWarningShown__'\n\nexport const metadata = {\n event: APP_BOOTSTRAP_EVENT,\n persistent: false,\n id: 'system_status_overlays:application-bootstrap-enterprise-warning',\n}\n\nexport default async function handle() {\n if ((globalThis as Record<string, unknown>)[ENTERPRISE_WARNING_GLOBAL_KEY] === true) return\n\n ;(globalThis as Record<string, unknown>)[ENTERPRISE_WARNING_GLOBAL_KEY] = true\n logger.warn(\n 'Enterprise modules are enabled. Developer preview is free, but production usage requires a commercial enterprise license. See: https://github.com/open-mercato/open-mercato/blob/main/packages/enterprise/README.md',\n )\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,wBAAwB;AAEpD,MAAM,sBAAsB;AAC5B,MAAM,gCAAgC;AAE/B,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAEA,eAAO,SAAgC;AACrC,MAAK,WAAuC,6BAA6B,MAAM,KAAM;AAEpF,EAAC,WAAuC,6BAA6B,IAAI;AAC1E,SAAO;AAAA,IACL;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/enterprise",
|
|
3
|
-
"version": "0.6.6-develop.
|
|
3
|
+
"version": "0.6.6-develop.6472.1.1673e7e66b",
|
|
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.6.6-develop.
|
|
69
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
68
|
+
"@open-mercato/core": "0.6.6-develop.6472.1.1673e7e66b",
|
|
69
|
+
"@open-mercato/ui": "0.6.6-develop.6472.1.1673e7e66b",
|
|
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.6.6-develop.
|
|
79
|
+
"@open-mercato/shared": "0.6.6-develop.6472.1.1673e7e66b",
|
|
80
80
|
"react": "^19.0.0",
|
|
81
81
|
"react-dom": "^19.0.0"
|
|
82
82
|
},
|
|
83
83
|
"devDependencies": {
|
|
84
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
84
|
+
"@open-mercato/shared": "0.6.6-develop.6472.1.1673e7e66b",
|
|
85
85
|
"@types/jest": "^30.0.0",
|
|
86
86
|
"@types/react": "^19.2.17",
|
|
87
87
|
"@types/react-dom": "^19.2.3",
|
|
@@ -9,6 +9,9 @@ import {
|
|
|
9
9
|
resolveRecordLockNotificationType,
|
|
10
10
|
resolveRecordResourceLink,
|
|
11
11
|
} from '../lib/notificationHelpers'
|
|
12
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
13
|
+
|
|
14
|
+
const logger = createLogger('record_locks').child({ subscriber: 'conflict-detected-notification' })
|
|
12
15
|
|
|
13
16
|
export const metadata = {
|
|
14
17
|
event: 'record_locks.conflict.detected',
|
|
@@ -109,6 +112,6 @@ export default async function handle(payload: Payload, ctx: ResolverContext) {
|
|
|
109
112
|
organizationId: payload.organizationId ?? null,
|
|
110
113
|
})
|
|
111
114
|
} catch (error) {
|
|
112
|
-
|
|
115
|
+
logger.error('Failed to create notification', { err: error })
|
|
113
116
|
}
|
|
114
117
|
}
|
|
@@ -5,6 +5,9 @@ import {
|
|
|
5
5
|
resolveRecordLockNotificationType,
|
|
6
6
|
resolveRecordResourceLink,
|
|
7
7
|
} from '../lib/notificationHelpers'
|
|
8
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
9
|
+
|
|
10
|
+
const logger = createLogger('record_locks').child({ subscriber: 'conflict-resolved-notification' })
|
|
8
11
|
|
|
9
12
|
export const metadata = {
|
|
10
13
|
event: 'record_locks.conflict.resolved',
|
|
@@ -59,6 +62,6 @@ export default async function handle(payload: Payload, ctx: ResolverContext) {
|
|
|
59
62
|
organizationId: payload.organizationId ?? null,
|
|
60
63
|
})
|
|
61
64
|
} catch (error) {
|
|
62
|
-
|
|
65
|
+
logger.error('Failed to create notification', { err: error })
|
|
63
66
|
}
|
|
64
67
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { buildNotificationFromType } from '@open-mercato/core/modules/notifications/lib/notificationBuilder'
|
|
2
2
|
import { resolveNotificationService } from '@open-mercato/core/modules/notifications/lib/notificationService'
|
|
3
3
|
import { resolveRecordLockNotificationType, resolveRecordResourceLink } from '../lib/notificationHelpers'
|
|
4
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
5
|
+
|
|
6
|
+
const logger = createLogger('record_locks').child({ subscriber: 'lock-contended-notification' })
|
|
4
7
|
|
|
5
8
|
export const metadata = {
|
|
6
9
|
event: 'record_locks.lock.contended',
|
|
@@ -47,6 +50,6 @@ export default async function handle(payload: Payload, ctx: ResolverContext) {
|
|
|
47
50
|
organizationId: payload.organizationId ?? null,
|
|
48
51
|
})
|
|
49
52
|
} catch (error) {
|
|
50
|
-
|
|
53
|
+
logger.error('Failed to create notification', { err: error })
|
|
51
54
|
}
|
|
52
55
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { buildNotificationFromType } from '@open-mercato/core/modules/notifications/lib/notificationBuilder'
|
|
2
2
|
import { resolveNotificationService } from '@open-mercato/core/modules/notifications/lib/notificationService'
|
|
3
3
|
import { resolveRecordLockNotificationType, resolveRecordResourceLink } from '../lib/notificationHelpers'
|
|
4
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
5
|
+
|
|
6
|
+
const logger = createLogger('record_locks').child({ subscriber: 'lock-force-released-notification' })
|
|
4
7
|
|
|
5
8
|
export const metadata = {
|
|
6
9
|
event: 'record_locks.lock.force_released',
|
|
@@ -46,6 +49,6 @@ export default async function handle(payload: Payload, ctx: ResolverContext) {
|
|
|
46
49
|
organizationId: payload.organizationId ?? null,
|
|
47
50
|
})
|
|
48
51
|
} catch (error) {
|
|
49
|
-
|
|
52
|
+
logger.error('Failed to create notification', { err: error })
|
|
50
53
|
}
|
|
51
54
|
}
|
|
@@ -34,6 +34,9 @@ import {
|
|
|
34
34
|
} from '@open-mercato/enterprise/modules/record_locks/lib/clientLockStore'
|
|
35
35
|
import { isUuid, resolveConflictId, runAcceptIncoming } from './conflictResolution'
|
|
36
36
|
import { isOptimisticLockFloorConflict } from '@open-mercato/enterprise/modules/record_locks/lib/optimisticLockFloor'
|
|
37
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
38
|
+
|
|
39
|
+
const logger = createLogger('record_locks').child({ component: 'record-locking-widget' })
|
|
37
40
|
|
|
38
41
|
type CrudInjectionContext = {
|
|
39
42
|
formId?: string
|
|
@@ -556,7 +559,7 @@ function releaseLockWithKeepalive(state: {
|
|
|
556
559
|
keepalive: true,
|
|
557
560
|
credentials: 'include',
|
|
558
561
|
}).catch((error) => {
|
|
559
|
-
|
|
562
|
+
logger.warn('Failed to release lock with keepalive fallback', { err: error })
|
|
560
563
|
})
|
|
561
564
|
}
|
|
562
565
|
|
|
@@ -661,7 +664,7 @@ export default function RecordLockingWidget({
|
|
|
661
664
|
token: current.lock.token,
|
|
662
665
|
reason: 'cancelled',
|
|
663
666
|
}).catch((error) => {
|
|
664
|
-
|
|
667
|
+
logger.warn('Failed to release lock while demoting owner', { err: error })
|
|
665
668
|
})
|
|
666
669
|
clearRecordLockFormState(formId)
|
|
667
670
|
}, [formId, isPrimaryInstance])
|
|
@@ -14,6 +14,9 @@ import type {
|
|
|
14
14
|
MfaEnforcementService,
|
|
15
15
|
} from '../../services/MfaEnforcementService'
|
|
16
16
|
import { localizeSecurityApiBody, securityApiError } from '../i18n'
|
|
17
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
18
|
+
|
|
19
|
+
const logger = createLogger('security').child({ component: 'enforcement' })
|
|
17
20
|
|
|
18
21
|
type RequestContainer = Awaited<ReturnType<typeof createRequestContainer>>
|
|
19
22
|
type Auth = NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>
|
|
@@ -128,7 +131,7 @@ export async function mapEnforcementError(error: unknown): Promise<NextResponse>
|
|
|
128
131
|
if (isMfaEnforcementServiceError(error)) {
|
|
129
132
|
return securityApiError(error.statusCode, error.message)
|
|
130
133
|
}
|
|
131
|
-
|
|
134
|
+
logger.error('Enforcement route failure', { err: error })
|
|
132
135
|
return securityApiError(500, 'Failed to process enforcement request.')
|
|
133
136
|
}
|
|
134
137
|
|
|
@@ -8,6 +8,9 @@ import { signJwt } from '@open-mercato/shared/lib/auth/jwt'
|
|
|
8
8
|
import type { MfaService, MfaServiceError } from '../../services/MfaService'
|
|
9
9
|
import type { MfaVerificationService, MfaVerificationServiceError } from '../../services/MfaVerificationService'
|
|
10
10
|
import { localizeSecurityApiBody, securityApiError } from '../i18n'
|
|
11
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
12
|
+
|
|
13
|
+
const logger = createLogger('security').child({ component: 'mfa' })
|
|
11
14
|
|
|
12
15
|
const jsonRecordSchema = z.record(z.string(), z.unknown())
|
|
13
16
|
|
|
@@ -70,7 +73,7 @@ export async function mapMfaError(error: unknown): Promise<NextResponse> {
|
|
|
70
73
|
if (isMfaServiceError(error) || isMfaVerificationServiceError(error)) {
|
|
71
74
|
return securityApiError(error.statusCode, error.message)
|
|
72
75
|
}
|
|
73
|
-
|
|
76
|
+
logger.error('MFA route failure', { err: error })
|
|
74
77
|
return securityApiError(500, 'Failed to process MFA request.')
|
|
75
78
|
}
|
|
76
79
|
|
|
@@ -8,6 +8,9 @@ import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
|
8
8
|
import { changePasswordSchema } from '../../../data/validators'
|
|
9
9
|
import { buildSecurityOpenApi, securityErrorSchema } from '../../openapi'
|
|
10
10
|
import { localizeSecurityApiBody } from '../../i18n'
|
|
11
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
12
|
+
|
|
13
|
+
const logger = createLogger('security').child({ component: 'profile-password' })
|
|
11
14
|
|
|
12
15
|
const changePasswordResponseSchema = z.object({
|
|
13
16
|
ok: z.literal(true),
|
|
@@ -68,7 +71,7 @@ export async function PUT(req: Request) {
|
|
|
68
71
|
if (error instanceof CrudHttpError) {
|
|
69
72
|
return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.status })
|
|
70
73
|
}
|
|
71
|
-
|
|
74
|
+
logger.error('Profile password update failed', { err: error })
|
|
72
75
|
return NextResponse.json(
|
|
73
76
|
{ error: translate('security.profile.password.form.errors.save', 'Failed to update password.') },
|
|
74
77
|
{ status: 400 },
|
|
@@ -13,6 +13,9 @@ import type {
|
|
|
13
13
|
} from '../../services/SudoChallengeService'
|
|
14
14
|
import { isSudoRequiredError } from '../../lib/sudo-middleware'
|
|
15
15
|
import { localizeSecurityApiBody, securityApiError } from '../i18n'
|
|
16
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
17
|
+
|
|
18
|
+
const logger = createLogger('security').child({ component: 'sudo' })
|
|
16
19
|
|
|
17
20
|
type RequestContainer = Awaited<ReturnType<typeof createRequestContainer>>
|
|
18
21
|
type Auth = NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>
|
|
@@ -135,7 +138,7 @@ export async function mapSudoError(error: unknown): Promise<NextResponse> {
|
|
|
135
138
|
if (isSudoChallengeServiceError(error)) {
|
|
136
139
|
return securityApiError(error.statusCode, error.message)
|
|
137
140
|
}
|
|
138
|
-
|
|
141
|
+
logger.error('Sudo route failure', { err: error })
|
|
139
142
|
return securityApiError(500, 'Failed to process sudo request.')
|
|
140
143
|
}
|
|
141
144
|
|
|
@@ -11,6 +11,9 @@ import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacS
|
|
|
11
11
|
import { isSudoRequiredError } from '../../lib/sudo-middleware'
|
|
12
12
|
import type { MfaAdminService, MfaAdminServiceError } from '../../services/MfaAdminService'
|
|
13
13
|
import { localizeSecurityApiBody, securityApiError } from '../i18n'
|
|
14
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
15
|
+
|
|
16
|
+
const logger = createLogger('security').child({ component: 'users' })
|
|
14
17
|
|
|
15
18
|
type RequestContainer = Awaited<ReturnType<typeof createRequestContainer>>
|
|
16
19
|
type Auth = NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>
|
|
@@ -120,7 +123,7 @@ export async function mapSecurityUsersError(error: unknown): Promise<NextRespons
|
|
|
120
123
|
return securityApiError(error.statusCode, error.message)
|
|
121
124
|
}
|
|
122
125
|
|
|
123
|
-
|
|
126
|
+
logger.error('Users route failure', { err: error })
|
|
124
127
|
return securityApiError(500, 'Failed to process user security request.')
|
|
125
128
|
}
|
|
126
129
|
|
|
@@ -5,6 +5,9 @@ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
|
5
5
|
import { SsoService } from '../../../services/ssoService'
|
|
6
6
|
import { emitSsoEvent } from '../../../events'
|
|
7
7
|
import { resolveSsoCallbackErrorCode } from '../../../lib/errors'
|
|
8
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
9
|
+
|
|
10
|
+
const logger = createLogger('sso').child({ component: 'callback' })
|
|
8
11
|
|
|
9
12
|
export const metadata = {
|
|
10
13
|
GET: { requireAuth: false },
|
|
@@ -43,7 +46,7 @@ async function handleCallback(req: Request): Promise<NextResponse> {
|
|
|
43
46
|
if (callbackParams.error) {
|
|
44
47
|
void emitSsoEvent('sso.login.failed', {
|
|
45
48
|
reason: callbackParams.error,
|
|
46
|
-
}).catch((
|
|
49
|
+
}).catch((eventError) => logger.error('SSO event emit failed', { err: eventError }))
|
|
47
50
|
return NextResponse.redirect(toAbsoluteUrl(req, '/login?error=sso_idp_error'))
|
|
48
51
|
}
|
|
49
52
|
|
|
@@ -79,10 +82,10 @@ async function handleCallback(req: Request): Promise<NextResponse> {
|
|
|
79
82
|
|
|
80
83
|
return res
|
|
81
84
|
} catch (err) {
|
|
82
|
-
|
|
85
|
+
logger.error('SSO callback error', { err })
|
|
83
86
|
void emitSsoEvent('sso.login.failed', {
|
|
84
87
|
reason: err instanceof Error ? err.message : 'callback_failed',
|
|
85
|
-
}).catch((
|
|
88
|
+
}).catch((eventError) => logger.error('SSO event emit failed', { err: eventError }))
|
|
86
89
|
const errorCode = resolveSsoCallbackErrorCode(err)
|
|
87
90
|
return NextResponse.redirect(toAbsoluteUrl(req, `/login?error=${errorCode}`))
|
|
88
91
|
}
|
|
@@ -4,6 +4,9 @@ import { SsoConfigError } from '../services/ssoConfigService'
|
|
|
4
4
|
import { ScimTokenError } from '../services/scimTokenService'
|
|
5
5
|
import { ScimServiceError } from '../services/scimService'
|
|
6
6
|
import { scimJson, buildScimError } from '../lib/scim-response'
|
|
7
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
8
|
+
|
|
9
|
+
const logger = createLogger('sso')
|
|
7
10
|
|
|
8
11
|
interface SsoHttpError {
|
|
9
12
|
message: string
|
|
@@ -23,7 +26,7 @@ export function handleSsoAdminApiError(err: unknown, label: string): NextRespons
|
|
|
23
26
|
if (isSsoHttpError(err)) {
|
|
24
27
|
return NextResponse.json({ error: err.message }, { status: err.statusCode })
|
|
25
28
|
}
|
|
26
|
-
|
|
29
|
+
logger.error('SSO admin API error', { label, err })
|
|
27
30
|
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
|
28
31
|
}
|
|
29
32
|
|
|
@@ -31,6 +34,6 @@ export function handleScimApiError(err: unknown, label: string): Response {
|
|
|
31
34
|
if (err instanceof ScimServiceError) {
|
|
32
35
|
return scimJson(buildScimError(err.statusCode, err.message), err.statusCode)
|
|
33
36
|
}
|
|
34
|
-
|
|
37
|
+
logger.error('SCIM API error', { label, err })
|
|
35
38
|
return scimJson(buildScimError(500, 'Internal server error'), 500)
|
|
36
39
|
}
|
|
@@ -3,6 +3,9 @@ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
|
3
3
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
4
4
|
import { HrdService } from '../../services/hrdService'
|
|
5
5
|
import { hrdRequestSchema } from '../../data/validators'
|
|
6
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
7
|
+
|
|
8
|
+
const logger = createLogger('sso').child({ component: 'hrd' })
|
|
6
9
|
|
|
7
10
|
export const metadata = {
|
|
8
11
|
POST: { requireAuth: false },
|
|
@@ -31,7 +34,7 @@ export async function POST(req: Request) {
|
|
|
31
34
|
protocol: config.protocol,
|
|
32
35
|
})
|
|
33
36
|
} catch (err) {
|
|
34
|
-
|
|
37
|
+
logger.error('SSO HRD error', { err })
|
|
35
38
|
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
|
36
39
|
}
|
|
37
40
|
}
|