@open-mercato/enterprise 0.6.6-develop.6465.1.019f0fb26f → 0.6.6-develop.6471.1.9ab5bdd79a

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 (66) hide show
  1. package/dist/modules/record_locks/subscribers/conflict-detected-notification.js +3 -1
  2. package/dist/modules/record_locks/subscribers/conflict-detected-notification.js.map +2 -2
  3. package/dist/modules/record_locks/subscribers/conflict-resolved-notification.js +3 -1
  4. package/dist/modules/record_locks/subscribers/conflict-resolved-notification.js.map +2 -2
  5. package/dist/modules/record_locks/subscribers/lock-contended-notification.js +3 -1
  6. package/dist/modules/record_locks/subscribers/lock-contended-notification.js.map +2 -2
  7. package/dist/modules/record_locks/subscribers/lock-force-released-notification.js +3 -1
  8. package/dist/modules/record_locks/subscribers/lock-force-released-notification.js.map +2 -2
  9. package/dist/modules/record_locks/widgets/injection/record-locking/widget.client.js +4 -2
  10. package/dist/modules/record_locks/widgets/injection/record-locking/widget.client.js.map +2 -2
  11. package/dist/modules/security/api/enforcement/_shared.js +3 -1
  12. package/dist/modules/security/api/enforcement/_shared.js.map +2 -2
  13. package/dist/modules/security/api/mfa/_shared.js +3 -1
  14. package/dist/modules/security/api/mfa/_shared.js.map +2 -2
  15. package/dist/modules/security/api/profile/password/route.js +3 -1
  16. package/dist/modules/security/api/profile/password/route.js.map +2 -2
  17. package/dist/modules/security/api/sudo/_shared.js +3 -1
  18. package/dist/modules/security/api/sudo/_shared.js.map +2 -2
  19. package/dist/modules/security/api/users/_shared.js +3 -1
  20. package/dist/modules/security/api/users/_shared.js.map +2 -2
  21. package/dist/modules/sso/api/callback/oidc/route.js +5 -3
  22. package/dist/modules/sso/api/callback/oidc/route.js.map +2 -2
  23. package/dist/modules/sso/api/error-handler.js +4 -2
  24. package/dist/modules/sso/api/error-handler.js.map +2 -2
  25. package/dist/modules/sso/api/hrd/route.js +3 -1
  26. package/dist/modules/sso/api/hrd/route.js.map +2 -2
  27. package/dist/modules/sso/api/initiate/route.js +4 -2
  28. package/dist/modules/sso/api/initiate/route.js.map +2 -2
  29. package/dist/modules/sso/backend/sso/config/[id]/page.js +2 -2
  30. package/dist/modules/sso/backend/sso/config/[id]/page.js.map +2 -2
  31. package/dist/modules/sso/i18n/de.json +2 -2
  32. package/dist/modules/sso/i18n/en.json +2 -2
  33. package/dist/modules/sso/i18n/es.json +2 -2
  34. package/dist/modules/sso/i18n/pl.json +2 -2
  35. package/dist/modules/sso/services/accountLinkingService.js +9 -46
  36. package/dist/modules/sso/services/accountLinkingService.js.map +2 -2
  37. package/dist/modules/sso/services/ssoConfigService.js +9 -7
  38. package/dist/modules/sso/services/ssoConfigService.js.map +2 -2
  39. package/dist/modules/sso/services/ssoService.js +4 -2
  40. package/dist/modules/sso/services/ssoService.js.map +2 -2
  41. package/dist/modules/system_status_overlays/subscribers/application-bootstrap-enterprise-warning.js +4 -2
  42. package/dist/modules/system_status_overlays/subscribers/application-bootstrap-enterprise-warning.js.map +2 -2
  43. package/package.json +5 -5
  44. package/src/modules/record_locks/subscribers/conflict-detected-notification.ts +4 -1
  45. package/src/modules/record_locks/subscribers/conflict-resolved-notification.ts +4 -1
  46. package/src/modules/record_locks/subscribers/lock-contended-notification.ts +4 -1
  47. package/src/modules/record_locks/subscribers/lock-force-released-notification.ts +4 -1
  48. package/src/modules/record_locks/widgets/injection/record-locking/widget.client.tsx +5 -2
  49. package/src/modules/security/api/enforcement/_shared.ts +4 -1
  50. package/src/modules/security/api/mfa/_shared.ts +4 -1
  51. package/src/modules/security/api/profile/password/route.ts +4 -1
  52. package/src/modules/security/api/sudo/_shared.ts +4 -1
  53. package/src/modules/security/api/users/_shared.ts +4 -1
  54. package/src/modules/sso/api/callback/oidc/route.ts +6 -3
  55. package/src/modules/sso/api/error-handler.ts +5 -2
  56. package/src/modules/sso/api/hrd/route.ts +4 -1
  57. package/src/modules/sso/api/initiate/route.ts +5 -2
  58. package/src/modules/sso/backend/sso/config/[id]/page.tsx +2 -2
  59. package/src/modules/sso/i18n/de.json +2 -2
  60. package/src/modules/sso/i18n/en.json +2 -2
  61. package/src/modules/sso/i18n/es.json +2 -2
  62. package/src/modules/sso/i18n/pl.json +2 -2
  63. package/src/modules/sso/services/accountLinkingService.ts +10 -52
  64. package/src/modules/sso/services/ssoConfigService.ts +10 -7
  65. package/src/modules/sso/services/ssoService.ts +5 -2
  66. package/src/modules/system_status_overlays/subscribers/application-bootstrap-enterprise-warning.ts +6 -2
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/security/api/mfa/_shared.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { signJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport type { MfaService, MfaServiceError } from '../../services/MfaService'\nimport type { MfaVerificationService, MfaVerificationServiceError } from '../../services/MfaVerificationService'\nimport { localizeSecurityApiBody, securityApiError } from '../i18n'\n\nconst jsonRecordSchema = z.record(z.string(), z.unknown())\n\nexport type MfaRequestContext = {\n auth: NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>\n container: Awaited<ReturnType<typeof createRequestContainer>>\n commandContext: CommandRuntimeContext\n mfaService: MfaService\n mfaVerificationService: MfaVerificationService\n}\n\nexport async function resolveMfaRequestContext(req: Request): Promise<MfaRequestContext | NextResponse> {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return securityApiError(401, 'Unauthorized')\n }\n\n const container = await createRequestContainer()\n return {\n auth,\n container,\n commandContext: {\n container,\n auth,\n organizationScope: null,\n selectedOrganizationId: auth.orgId ?? null,\n organizationIds: auth.orgId ? [auth.orgId] : null,\n request: req,\n },\n mfaService: container.resolve<MfaService>('mfaService'),\n mfaVerificationService: container.resolve<MfaVerificationService>('mfaVerificationService'),\n }\n}\n\nexport async function readJsonRecord(req: Request): Promise<Record<string, unknown>> {\n try {\n const parsed = jsonRecordSchema.safeParse(await req.json())\n return parsed.success ? parsed.data : {}\n } catch {\n return {}\n }\n}\n\nexport function readUuidParam(value: string | string[] | undefined): string | null {\n if (typeof value !== 'string') return null\n const parsed = z.string().uuid().safeParse(value)\n return parsed.success ? parsed.data : null\n}\n\nexport function readString(value: unknown): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\nexport async function mapMfaError(error: unknown): Promise<NextResponse> {\n if (error instanceof CrudHttpError) {\n return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.status })\n }\n if (isMfaServiceError(error) || isMfaVerificationServiceError(error)) {\n return securityApiError(error.statusCode, error.message)\n }\n console.error('security.mfa.route failure', error)\n return securityApiError(500, 'Failed to process MFA request.')\n}\n\nfunction isMfaServiceError(error: unknown): error is MfaServiceError {\n return error instanceof Error\n && error.name === 'MfaServiceError'\n && typeof (error as Partial<MfaServiceError>).statusCode === 'number'\n}\n\nfunction isMfaVerificationServiceError(error: unknown): error is MfaVerificationServiceError {\n return error instanceof Error\n && error.name === 'MfaVerificationServiceError'\n && typeof (error as Partial<MfaVerificationServiceError>).statusCode === 'number'\n}\n\nexport function issueVerifiedMfaToken(auth: MfaRequestContext['auth'], methods: string[]): string {\n const nextPayload: Record<string, unknown> = {\n sub: auth.sub,\n sid: typeof auth.sid === 'string' ? auth.sid : undefined,\n tenantId: auth.tenantId ?? null,\n orgId: auth.orgId ?? null,\n email: typeof auth.email === 'string' ? auth.email : null,\n roles: Array.isArray(auth.roles) ? auth.roles : [],\n mfa_pending: false,\n mfa_verified: true,\n mfa_methods: methods,\n }\n if (typeof auth.actorTenantId === 'string') {\n nextPayload.actorTenantId = auth.actorTenantId\n }\n if (typeof auth.actorOrgId === 'string') {\n nextPayload.actorOrgId = auth.actorOrgId\n }\n if (auth.isSuperAdmin === true) {\n nextPayload.isSuperAdmin = true\n }\n return signJwt({\n ...nextPayload,\n })\n}\n\nexport function setAuthCookie(response: NextResponse, token: string): void {\n response.cookies.set('auth_token', token, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 60 * 60 * 8,\n })\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { signJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport type { MfaService, MfaServiceError } from '../../services/MfaService'\nimport type { MfaVerificationService, MfaVerificationServiceError } from '../../services/MfaVerificationService'\nimport { localizeSecurityApiBody, securityApiError } from '../i18n'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('security').child({ component: 'mfa' })\n\nconst jsonRecordSchema = z.record(z.string(), z.unknown())\n\nexport type MfaRequestContext = {\n auth: NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>\n container: Awaited<ReturnType<typeof createRequestContainer>>\n commandContext: CommandRuntimeContext\n mfaService: MfaService\n mfaVerificationService: MfaVerificationService\n}\n\nexport async function resolveMfaRequestContext(req: Request): Promise<MfaRequestContext | NextResponse> {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return securityApiError(401, 'Unauthorized')\n }\n\n const container = await createRequestContainer()\n return {\n auth,\n container,\n commandContext: {\n container,\n auth,\n organizationScope: null,\n selectedOrganizationId: auth.orgId ?? null,\n organizationIds: auth.orgId ? [auth.orgId] : null,\n request: req,\n },\n mfaService: container.resolve<MfaService>('mfaService'),\n mfaVerificationService: container.resolve<MfaVerificationService>('mfaVerificationService'),\n }\n}\n\nexport async function readJsonRecord(req: Request): Promise<Record<string, unknown>> {\n try {\n const parsed = jsonRecordSchema.safeParse(await req.json())\n return parsed.success ? parsed.data : {}\n } catch {\n return {}\n }\n}\n\nexport function readUuidParam(value: string | string[] | undefined): string | null {\n if (typeof value !== 'string') return null\n const parsed = z.string().uuid().safeParse(value)\n return parsed.success ? parsed.data : null\n}\n\nexport function readString(value: unknown): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\nexport async function mapMfaError(error: unknown): Promise<NextResponse> {\n if (error instanceof CrudHttpError) {\n return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.status })\n }\n if (isMfaServiceError(error) || isMfaVerificationServiceError(error)) {\n return securityApiError(error.statusCode, error.message)\n }\n logger.error('MFA route failure', { err: error })\n return securityApiError(500, 'Failed to process MFA request.')\n}\n\nfunction isMfaServiceError(error: unknown): error is MfaServiceError {\n return error instanceof Error\n && error.name === 'MfaServiceError'\n && typeof (error as Partial<MfaServiceError>).statusCode === 'number'\n}\n\nfunction isMfaVerificationServiceError(error: unknown): error is MfaVerificationServiceError {\n return error instanceof Error\n && error.name === 'MfaVerificationServiceError'\n && typeof (error as Partial<MfaVerificationServiceError>).statusCode === 'number'\n}\n\nexport function issueVerifiedMfaToken(auth: MfaRequestContext['auth'], methods: string[]): string {\n const nextPayload: Record<string, unknown> = {\n sub: auth.sub,\n sid: typeof auth.sid === 'string' ? auth.sid : undefined,\n tenantId: auth.tenantId ?? null,\n orgId: auth.orgId ?? null,\n email: typeof auth.email === 'string' ? auth.email : null,\n roles: Array.isArray(auth.roles) ? auth.roles : [],\n mfa_pending: false,\n mfa_verified: true,\n mfa_methods: methods,\n }\n if (typeof auth.actorTenantId === 'string') {\n nextPayload.actorTenantId = auth.actorTenantId\n }\n if (typeof auth.actorOrgId === 'string') {\n nextPayload.actorOrgId = auth.actorOrgId\n }\n if (auth.isSuperAdmin === true) {\n nextPayload.isSuperAdmin = true\n }\n return signJwt({\n ...nextPayload,\n })\n}\n\nexport function setAuthCookie(response: NextResponse, token: string): void {\n response.cookies.set('auth_token', token, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 60 * 60 * 8,\n })\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -7,6 +7,8 @@ import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
7
7
  import { changePasswordSchema } from "../../../data/validators.js";
8
8
  import { buildSecurityOpenApi, securityErrorSchema } from "../../openapi.js";
9
9
  import { localizeSecurityApiBody } from "../../i18n.js";
10
+ import { createLogger } from "@open-mercato/shared/lib/logger";
11
+ const logger = createLogger("security").child({ component: "profile-password" });
10
12
  const changePasswordResponseSchema = z.object({
11
13
  ok: z.literal(true)
12
14
  });
@@ -57,7 +59,7 @@ async function PUT(req) {
57
59
  if (error instanceof CrudHttpError) {
58
60
  return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.status });
59
61
  }
60
- console.error("security.profile.password.update failed", error);
62
+ logger.error("Profile password update failed", { err: error });
61
63
  return NextResponse.json(
62
64
  { error: translate("security.profile.password.form.errors.save", "Failed to update password.") },
63
65
  { status: 400 }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/security/api/profile/password/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { changePasswordSchema } from '../../../data/validators'\nimport { buildSecurityOpenApi, securityErrorSchema } from '../../openapi'\nimport { localizeSecurityApiBody } from '../../i18n'\n\nconst changePasswordResponseSchema = z.object({\n ok: z.literal(true),\n})\n\ntype RequestContainer = Awaited<ReturnType<typeof createRequestContainer>>\ntype Auth = NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>\n\nfunction buildCommandContext(container: RequestContainer, auth: Auth, req: Request): CommandRuntimeContext {\n return {\n container,\n auth,\n organizationScope: null,\n selectedOrganizationId: auth.orgId ?? null,\n organizationIds: auth.orgId ? [auth.orgId] : null,\n request: req,\n }\n}\n\nexport const metadata = {\n PUT: { requireAuth: true, requireFeatures: ['security.profile.password'] },\n}\n\nexport async function PUT(req: Request) {\n const { translate } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json(\n { error: translate('api.errors.unauthorized', 'Unauthorized') },\n { status: 401 },\n )\n }\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n body = {}\n }\n\n const parsed = changePasswordSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json(\n { error: translate('api.errors.invalidPayload', 'Invalid payload.'), issues: parsed.error.issues },\n { status: 400 },\n )\n }\n\n try {\n const container = await createRequestContainer()\n const commandBus = container.resolve<CommandBus>('commandBus')\n await commandBus.execute('security.password.change', {\n input: parsed.data,\n ctx: buildCommandContext(container, auth, req),\n })\n return NextResponse.json({ ok: true })\n } catch (error) {\n if (error instanceof CrudHttpError) {\n return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.status })\n }\n console.error('security.profile.password.update failed', error)\n return NextResponse.json(\n { error: translate('security.profile.password.form.errors.save', 'Failed to update password.') },\n { status: 400 },\n )\n }\n}\n\nexport const openApi = buildSecurityOpenApi({\n summary: 'Security password routes',\n methods: {\n PUT: {\n summary: 'Change current user password',\n description: 'Changes password for the authenticated user and requires the current password.',\n requestBody: {\n contentType: 'application/json',\n schema: changePasswordSchema,\n },\n responses: [\n { status: 200, description: 'Password updated', schema: changePasswordResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or password validation error', schema: securityErrorSchema },\n { status: 401, description: 'Unauthorized', schema: securityErrorSchema },\n ],\n },\n },\n})\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,2BAA2B;AACpC,SAAS,qBAAqB;AAC9B,SAAS,4BAA4B;AACrC,SAAS,sBAAsB,2BAA2B;AAC1D,SAAS,+BAA+B;AAExC,MAAM,+BAA+B,EAAE,OAAO;AAAA,EAC5C,IAAI,EAAE,QAAQ,IAAI;AACpB,CAAC;AAKD,SAAS,oBAAoB,WAA6B,MAAY,KAAqC;AACzG,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,mBAAmB;AAAA,IACnB,wBAAwB,KAAK,SAAS;AAAA,IACtC,iBAAiB,KAAK,QAAQ,CAAC,KAAK,KAAK,IAAI;AAAA,IAC7C,SAAS;AAAA,EACX;AACF;AAEO,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,2BAA2B,EAAE;AAC3E;AAEA,eAAsB,IAAI,KAAc;AACtC,QAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,KAAK;AACd,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,UAAU,2BAA2B,cAAc,EAAE;AAAA,MAC9D,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AAEA,MAAI;AACJ,MAAI;AACF,WAAO,MAAM,IAAI,KAAK;AAAA,EACxB,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AAEA,QAAM,SAAS,qBAAqB,UAAU,IAAI;AAClD,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,UAAU,6BAA6B,kBAAkB,GAAG,QAAQ,OAAO,MAAM,OAAO;AAAA,MACjG,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AAEA,MAAI;AACF,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,aAAa,UAAU,QAAoB,YAAY;AAC7D,UAAM,WAAW,QAAQ,4BAA4B;AAAA,MACnD,OAAO,OAAO;AAAA,MACd,KAAK,oBAAoB,WAAW,MAAM,GAAG;AAAA,IAC/C,CAAC;AACD,WAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,EACvC,SAAS,OAAO;AACd,QAAI,iBAAiB,eAAe;AAClC,aAAO,aAAa,KAAK,MAAM,wBAAwB,MAAM,IAAI,GAAG,EAAE,QAAQ,MAAM,OAAO,CAAC;AAAA,IAC9F;AACA,YAAQ,MAAM,2CAA2C,KAAK;AAC9D,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,UAAU,8CAA8C,4BAA4B,EAAE;AAAA,MAC/F,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACF;AAEO,MAAM,UAAU,qBAAqB;AAAA,EAC1C,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,MACH,SAAS;AAAA,MACT,aAAa;AAAA,MACb,aAAa;AAAA,QACX,aAAa;AAAA,QACb,QAAQ;AAAA,MACV;AAAA,MACA,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,oBAAoB,QAAQ,6BAA6B;AAAA,MACvF;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,gDAAgD,QAAQ,oBAAoB;AAAA,QACxG,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,oBAAoB;AAAA,MAC1E;AAAA,IACF;AAAA,EACF;AACF,CAAC;",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { changePasswordSchema } from '../../../data/validators'\nimport { buildSecurityOpenApi, securityErrorSchema } from '../../openapi'\nimport { localizeSecurityApiBody } from '../../i18n'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('security').child({ component: 'profile-password' })\n\nconst changePasswordResponseSchema = z.object({\n ok: z.literal(true),\n})\n\ntype RequestContainer = Awaited<ReturnType<typeof createRequestContainer>>\ntype Auth = NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>\n\nfunction buildCommandContext(container: RequestContainer, auth: Auth, req: Request): CommandRuntimeContext {\n return {\n container,\n auth,\n organizationScope: null,\n selectedOrganizationId: auth.orgId ?? null,\n organizationIds: auth.orgId ? [auth.orgId] : null,\n request: req,\n }\n}\n\nexport const metadata = {\n PUT: { requireAuth: true, requireFeatures: ['security.profile.password'] },\n}\n\nexport async function PUT(req: Request) {\n const { translate } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json(\n { error: translate('api.errors.unauthorized', 'Unauthorized') },\n { status: 401 },\n )\n }\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n body = {}\n }\n\n const parsed = changePasswordSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json(\n { error: translate('api.errors.invalidPayload', 'Invalid payload.'), issues: parsed.error.issues },\n { status: 400 },\n )\n }\n\n try {\n const container = await createRequestContainer()\n const commandBus = container.resolve<CommandBus>('commandBus')\n await commandBus.execute('security.password.change', {\n input: parsed.data,\n ctx: buildCommandContext(container, auth, req),\n })\n return NextResponse.json({ ok: true })\n } catch (error) {\n if (error instanceof CrudHttpError) {\n return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.status })\n }\n logger.error('Profile password update failed', { err: error })\n return NextResponse.json(\n { error: translate('security.profile.password.form.errors.save', 'Failed to update password.') },\n { status: 400 },\n )\n }\n}\n\nexport const openApi = buildSecurityOpenApi({\n summary: 'Security password routes',\n methods: {\n PUT: {\n summary: 'Change current user password',\n description: 'Changes password for the authenticated user and requires the current password.',\n requestBody: {\n contentType: 'application/json',\n schema: changePasswordSchema,\n },\n responses: [\n { status: 200, description: 'Password updated', schema: changePasswordResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or password validation error', schema: securityErrorSchema },\n { status: 401, description: 'Unauthorized', schema: securityErrorSchema },\n ],\n },\n },\n})\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,2BAA2B;AACpC,SAAS,qBAAqB;AAC9B,SAAS,4BAA4B;AACrC,SAAS,sBAAsB,2BAA2B;AAC1D,SAAS,+BAA+B;AACxC,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,UAAU,EAAE,MAAM,EAAE,WAAW,mBAAmB,CAAC;AAE/E,MAAM,+BAA+B,EAAE,OAAO;AAAA,EAC5C,IAAI,EAAE,QAAQ,IAAI;AACpB,CAAC;AAKD,SAAS,oBAAoB,WAA6B,MAAY,KAAqC;AACzG,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,mBAAmB;AAAA,IACnB,wBAAwB,KAAK,SAAS;AAAA,IACtC,iBAAiB,KAAK,QAAQ,CAAC,KAAK,KAAK,IAAI;AAAA,IAC7C,SAAS;AAAA,EACX;AACF;AAEO,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,2BAA2B,EAAE;AAC3E;AAEA,eAAsB,IAAI,KAAc;AACtC,QAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,KAAK;AACd,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,UAAU,2BAA2B,cAAc,EAAE;AAAA,MAC9D,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AAEA,MAAI;AACJ,MAAI;AACF,WAAO,MAAM,IAAI,KAAK;AAAA,EACxB,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AAEA,QAAM,SAAS,qBAAqB,UAAU,IAAI;AAClD,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,UAAU,6BAA6B,kBAAkB,GAAG,QAAQ,OAAO,MAAM,OAAO;AAAA,MACjG,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AAEA,MAAI;AACF,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,aAAa,UAAU,QAAoB,YAAY;AAC7D,UAAM,WAAW,QAAQ,4BAA4B;AAAA,MACnD,OAAO,OAAO;AAAA,MACd,KAAK,oBAAoB,WAAW,MAAM,GAAG;AAAA,IAC/C,CAAC;AACD,WAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,EACvC,SAAS,OAAO;AACd,QAAI,iBAAiB,eAAe;AAClC,aAAO,aAAa,KAAK,MAAM,wBAAwB,MAAM,IAAI,GAAG,EAAE,QAAQ,MAAM,OAAO,CAAC;AAAA,IAC9F;AACA,WAAO,MAAM,kCAAkC,EAAE,KAAK,MAAM,CAAC;AAC7D,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,UAAU,8CAA8C,4BAA4B,EAAE;AAAA,MAC/F,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACF;AAEO,MAAM,UAAU,qBAAqB;AAAA,EAC1C,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,MACH,SAAS;AAAA,MACT,aAAa;AAAA,MACb,aAAa;AAAA,QACX,aAAa;AAAA,QACb,QAAQ;AAAA,MACV;AAAA,MACA,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,oBAAoB,QAAQ,6BAA6B;AAAA,MACvF;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,gDAAgD,QAAQ,oBAAoB;AAAA,QACxG,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,oBAAoB;AAAA,MAC1E;AAAA,IACF;AAAA,EACF;AACF,CAAC;",
6
6
  "names": []
7
7
  }
@@ -5,6 +5,8 @@ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
5
  import { Organization, Tenant } from "@open-mercato/core/modules/directory/data/entities";
6
6
  import { isSudoRequiredError } from "../../lib/sudo-middleware.js";
7
7
  import { localizeSecurityApiBody, securityApiError } from "../i18n.js";
8
+ import { createLogger } from "@open-mercato/shared/lib/logger";
9
+ const logger = createLogger("security").child({ component: "sudo" });
8
10
  function buildSudoAuthScope(auth) {
9
11
  return {
10
12
  tenantId: auth.tenantId ?? null,
@@ -99,7 +101,7 @@ async function mapSudoError(error) {
99
101
  if (isSudoChallengeServiceError(error)) {
100
102
  return securityApiError(error.statusCode, error.message);
101
103
  }
102
- console.error("security.sudo.route failure", error);
104
+ logger.error("Sudo route failure", { err: error });
103
105
  return securityApiError(500, "Failed to process sudo request.");
104
106
  }
105
107
  function isSudoChallengeServiceError(error) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/security/api/sudo/_shared.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { Organization, Tenant } from '@open-mercato/core/modules/directory/data/entities'\nimport type { SudoChallengeConfig } from '../../data/entities'\nimport type {\n SudoAuthScope,\n SudoChallengeService,\n SudoChallengeServiceError,\n} from '../../services/SudoChallengeService'\nimport { isSudoRequiredError } from '../../lib/sudo-middleware'\nimport { localizeSecurityApiBody, securityApiError } from '../i18n'\n\ntype RequestContainer = Awaited<ReturnType<typeof createRequestContainer>>\ntype Auth = NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>\n\nexport type SudoRequestContext = {\n auth: Auth\n container: RequestContainer\n commandContext: CommandRuntimeContext\n sudoChallengeService: SudoChallengeService\n scope: SudoAuthScope\n}\n\nexport function buildSudoAuthScope(auth: Auth): SudoAuthScope {\n return {\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n isSuperAdmin: auth.isSuperAdmin === true,\n }\n}\n\nexport function toSudoConfigResponse(config: SudoChallengeConfig) {\n return {\n id: config.id,\n tenantId: config.tenantId ?? null,\n tenantName: null as string | null,\n organizationId: config.organizationId ?? null,\n organizationName: null as string | null,\n label: config.label ?? null,\n targetIdentifier: config.targetIdentifier,\n isEnabled: config.isEnabled,\n isDeveloperDefault: config.isDeveloperDefault,\n ttlSeconds: config.ttlSeconds,\n challengeMethod: config.challengeMethod,\n configuredBy: config.configuredBy ?? null,\n createdAt: config.createdAt.toISOString(),\n updatedAt: config.updatedAt.toISOString(),\n }\n}\n\nexport async function attachSudoConfigScopeNames(\n container: RequestContainer,\n configs: SudoChallengeConfig[],\n): Promise<Array<ReturnType<typeof toSudoConfigResponse>>> {\n if (configs.length === 0) return []\n\n const em = container.resolve<EntityManager>('em')\n const tenantIds = Array.from(\n new Set(\n configs\n .map((c) => c.tenantId ?? null)\n .filter((id): id is string => typeof id === 'string' && id.length > 0),\n ),\n )\n const organizationIds = Array.from(\n new Set(\n configs\n .map((c) => c.organizationId ?? null)\n .filter((id): id is string => typeof id === 'string' && id.length > 0),\n ),\n )\n\n const [tenants, organizations] = await Promise.all([\n tenantIds.length ? em.find(Tenant, { id: { $in: tenantIds }, deletedAt: null }) : Promise.resolve([]),\n organizationIds.length ? em.find(Organization, { id: { $in: organizationIds }, deletedAt: null }) : Promise.resolve([]),\n ])\n\n const tenantMap = tenants.reduce<Record<string, string>>((acc, tenant) => {\n const id = tenant?.id ? String(tenant.id) : null\n if (!id) return acc\n acc[id] = typeof tenant.name === 'string' && tenant.name.length > 0 ? tenant.name : id\n return acc\n }, {})\n const organizationMap = organizations.reduce<Record<string, string>>((acc, org) => {\n const id = org?.id ? String(org.id) : null\n if (!id) return acc\n acc[id] = typeof org.name === 'string' && org.name.length > 0 ? org.name : id\n return acc\n }, {})\n\n return configs.map((config) => {\n const response = toSudoConfigResponse(config)\n return {\n ...response,\n tenantName: response.tenantId ? tenantMap[response.tenantId] ?? response.tenantId : null,\n organizationName: response.organizationId ? organizationMap[response.organizationId] ?? response.organizationId : null,\n }\n })\n}\n\nexport async function resolveSudoContext(req: Request): Promise<SudoRequestContext | NextResponse> {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return securityApiError(401, 'Unauthorized')\n }\n\n const container = await createRequestContainer()\n return {\n auth,\n container,\n commandContext: {\n container,\n auth,\n organizationScope: null,\n selectedOrganizationId: auth.orgId ?? null,\n organizationIds: auth.orgId ? [auth.orgId] : null,\n request: req,\n },\n sudoChallengeService: container.resolve<SudoChallengeService>('sudoChallengeService'),\n scope: buildSudoAuthScope(auth),\n }\n}\n\nexport async function mapSudoError(error: unknown): Promise<NextResponse> {\n if (error instanceof CrudHttpError) {\n return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.status })\n }\n if (isSudoRequiredError(error)) {\n return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.statusCode })\n }\n if (isSudoChallengeServiceError(error)) {\n return securityApiError(error.statusCode, error.message)\n }\n console.error('security.sudo.route failure', error)\n return securityApiError(500, 'Failed to process sudo request.')\n}\n\nfunction isSudoChallengeServiceError(error: unknown): error is SudoChallengeServiceError {\n return error instanceof Error\n && error.name === 'SudoChallengeServiceError'\n && typeof (error as Partial<SudoChallengeServiceError>).statusCode === 'number'\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { Organization, Tenant } from '@open-mercato/core/modules/directory/data/entities'\nimport type { SudoChallengeConfig } from '../../data/entities'\nimport type {\n SudoAuthScope,\n SudoChallengeService,\n SudoChallengeServiceError,\n} from '../../services/SudoChallengeService'\nimport { isSudoRequiredError } from '../../lib/sudo-middleware'\nimport { localizeSecurityApiBody, securityApiError } from '../i18n'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('security').child({ component: 'sudo' })\n\ntype RequestContainer = Awaited<ReturnType<typeof createRequestContainer>>\ntype Auth = NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>\n\nexport type SudoRequestContext = {\n auth: Auth\n container: RequestContainer\n commandContext: CommandRuntimeContext\n sudoChallengeService: SudoChallengeService\n scope: SudoAuthScope\n}\n\nexport function buildSudoAuthScope(auth: Auth): SudoAuthScope {\n return {\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n isSuperAdmin: auth.isSuperAdmin === true,\n }\n}\n\nexport function toSudoConfigResponse(config: SudoChallengeConfig) {\n return {\n id: config.id,\n tenantId: config.tenantId ?? null,\n tenantName: null as string | null,\n organizationId: config.organizationId ?? null,\n organizationName: null as string | null,\n label: config.label ?? null,\n targetIdentifier: config.targetIdentifier,\n isEnabled: config.isEnabled,\n isDeveloperDefault: config.isDeveloperDefault,\n ttlSeconds: config.ttlSeconds,\n challengeMethod: config.challengeMethod,\n configuredBy: config.configuredBy ?? null,\n createdAt: config.createdAt.toISOString(),\n updatedAt: config.updatedAt.toISOString(),\n }\n}\n\nexport async function attachSudoConfigScopeNames(\n container: RequestContainer,\n configs: SudoChallengeConfig[],\n): Promise<Array<ReturnType<typeof toSudoConfigResponse>>> {\n if (configs.length === 0) return []\n\n const em = container.resolve<EntityManager>('em')\n const tenantIds = Array.from(\n new Set(\n configs\n .map((c) => c.tenantId ?? null)\n .filter((id): id is string => typeof id === 'string' && id.length > 0),\n ),\n )\n const organizationIds = Array.from(\n new Set(\n configs\n .map((c) => c.organizationId ?? null)\n .filter((id): id is string => typeof id === 'string' && id.length > 0),\n ),\n )\n\n const [tenants, organizations] = await Promise.all([\n tenantIds.length ? em.find(Tenant, { id: { $in: tenantIds }, deletedAt: null }) : Promise.resolve([]),\n organizationIds.length ? em.find(Organization, { id: { $in: organizationIds }, deletedAt: null }) : Promise.resolve([]),\n ])\n\n const tenantMap = tenants.reduce<Record<string, string>>((acc, tenant) => {\n const id = tenant?.id ? String(tenant.id) : null\n if (!id) return acc\n acc[id] = typeof tenant.name === 'string' && tenant.name.length > 0 ? tenant.name : id\n return acc\n }, {})\n const organizationMap = organizations.reduce<Record<string, string>>((acc, org) => {\n const id = org?.id ? String(org.id) : null\n if (!id) return acc\n acc[id] = typeof org.name === 'string' && org.name.length > 0 ? org.name : id\n return acc\n }, {})\n\n return configs.map((config) => {\n const response = toSudoConfigResponse(config)\n return {\n ...response,\n tenantName: response.tenantId ? tenantMap[response.tenantId] ?? response.tenantId : null,\n organizationName: response.organizationId ? organizationMap[response.organizationId] ?? response.organizationId : null,\n }\n })\n}\n\nexport async function resolveSudoContext(req: Request): Promise<SudoRequestContext | NextResponse> {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return securityApiError(401, 'Unauthorized')\n }\n\n const container = await createRequestContainer()\n return {\n auth,\n container,\n commandContext: {\n container,\n auth,\n organizationScope: null,\n selectedOrganizationId: auth.orgId ?? null,\n organizationIds: auth.orgId ? [auth.orgId] : null,\n request: req,\n },\n sudoChallengeService: container.resolve<SudoChallengeService>('sudoChallengeService'),\n scope: buildSudoAuthScope(auth),\n }\n}\n\nexport async function mapSudoError(error: unknown): Promise<NextResponse> {\n if (error instanceof CrudHttpError) {\n return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.status })\n }\n if (isSudoRequiredError(error)) {\n return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.statusCode })\n }\n if (isSudoChallengeServiceError(error)) {\n return securityApiError(error.statusCode, error.message)\n }\n logger.error('Sudo route failure', { err: error })\n return securityApiError(500, 'Failed to process sudo request.')\n}\n\nfunction isSudoChallengeServiceError(error: unknown): error is SudoChallengeServiceError {\n return error instanceof Error\n && error.name === 'SudoChallengeServiceError'\n && typeof (error as Partial<SudoChallengeServiceError>).statusCode === 'number'\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -7,6 +7,8 @@ import { enforceTenantSelection, resolveIsSuperAdmin } from "@open-mercato/core/
7
7
  import { User } from "@open-mercato/core/modules/auth/data/entities";
8
8
  import { isSudoRequiredError } from "../../lib/sudo-middleware.js";
9
9
  import { localizeSecurityApiBody, securityApiError } from "../i18n.js";
10
+ import { createLogger } from "@open-mercato/shared/lib/logger";
11
+ const logger = createLogger("security").child({ component: "users" });
10
12
  async function resolveSecurityUsersContext(req) {
11
13
  const auth = await getAuthFromRequest(req);
12
14
  if (!auth?.sub) {
@@ -87,7 +89,7 @@ async function mapSecurityUsersError(error) {
87
89
  if (isMfaAdminServiceError(error)) {
88
90
  return securityApiError(error.statusCode, error.message);
89
91
  }
90
- console.error("security.users.route failure", error);
92
+ logger.error("Users route failure", { err: error });
91
93
  return securityApiError(500, "Failed to process user security request.");
92
94
  }
93
95
  function isMfaAdminServiceError(error) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/security/api/users/_shared.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { CrudHttpError, forbidden } from '@open-mercato/shared/lib/crud/errors'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { enforceTenantSelection, resolveIsSuperAdmin } from '@open-mercato/core/modules/auth/lib/tenantAccess'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { isSudoRequiredError } from '../../lib/sudo-middleware'\nimport type { MfaAdminService, MfaAdminServiceError } from '../../services/MfaAdminService'\nimport { localizeSecurityApiBody, securityApiError } from '../i18n'\n\ntype RequestContainer = Awaited<ReturnType<typeof createRequestContainer>>\ntype Auth = NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>\n\nexport type SecurityUsersRequestContext = {\n auth: Auth\n container: RequestContainer\n commandContext: CommandRuntimeContext\n mfaAdminService: MfaAdminService\n}\n\nexport async function resolveSecurityUsersContext(\n req: Request,\n): Promise<SecurityUsersRequestContext | NextResponse> {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return securityApiError(401, 'Unauthorized')\n }\n\n const container = await createRequestContainer()\n return {\n auth,\n container,\n commandContext: {\n container,\n auth,\n organizationScope: null,\n selectedOrganizationId: auth.orgId ?? null,\n organizationIds: auth.orgId ? [auth.orgId] : null,\n request: req,\n },\n mfaAdminService: container.resolve<MfaAdminService>('mfaAdminService'),\n }\n}\n\nfunction normalizeNullableString(value: unknown): string | null {\n return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null\n}\n\nfunction normalizeOrganizationList(values: unknown): string[] | null {\n if (values === null || values === undefined) return null\n if (!Array.isArray(values)) return null\n const result: string[] = []\n for (const value of values) {\n if (typeof value !== 'string') continue\n const trimmed = value.trim()\n if (trimmed) result.push(trimmed)\n }\n return result\n}\n\nexport async function assertActorCanAccessSecurityUserTarget(\n ctx: SecurityUsersRequestContext,\n targetUserId: string,\n): Promise<void> {\n const isSuperAdmin = await resolveIsSuperAdmin({ auth: ctx.auth, container: ctx.container })\n if (isSuperAdmin) return\n\n const em = ctx.container.resolve<EntityManager>('em')\n const target = await findOneWithDecryption(\n em,\n User,\n { id: targetUserId, deletedAt: null } as FilterQuery<User>,\n {},\n { tenantId: null, organizationId: null },\n )\n if (!target) {\n throw new CrudHttpError(404, { error: 'User not found' })\n }\n\n const actorTenantId = normalizeNullableString(ctx.auth.tenantId)\n const targetTenantId = normalizeNullableString((target as { tenantId?: string | null }).tenantId)\n if (!targetTenantId || targetTenantId !== actorTenantId) {\n throw new CrudHttpError(404, { error: 'User not found' })\n }\n\n const rbacService = ctx.container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(ctx.auth.sub, {\n tenantId: actorTenantId,\n organizationId: normalizeNullableString(ctx.auth.orgId),\n })\n const organizations = normalizeOrganizationList(acl?.organizations)\n if (organizations !== null && !organizations.includes('__all__')) {\n const targetOrganizationId = normalizeNullableString((target as { organizationId?: string | null }).organizationId)\n if (!targetOrganizationId || !organizations.includes(targetOrganizationId)) {\n throw forbidden('Not authorized to access this user.')\n }\n }\n}\n\nexport async function assertActorOwnsTenantScope(\n ctx: SecurityUsersRequestContext,\n requestedTenantId: string | null | undefined,\n): Promise<string | null> {\n const resolved = await enforceTenantSelection({ auth: ctx.auth, container: ctx.container }, requestedTenantId)\n return resolved ?? ctx.auth.tenantId ?? null\n}\n\nexport async function mapSecurityUsersError(error: unknown): Promise<NextResponse> {\n if (error instanceof CrudHttpError) {\n return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.status })\n }\n if (isSudoRequiredError(error)) {\n return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.statusCode })\n }\n if (isMfaAdminServiceError(error)) {\n return securityApiError(error.statusCode, error.message)\n }\n\n console.error('security.users.route failure', error)\n return securityApiError(500, 'Failed to process user security request.')\n}\n\nfunction isMfaAdminServiceError(error: unknown): error is MfaAdminServiceError {\n return error instanceof Error\n && error.name === 'MfaAdminServiceError'\n && typeof (error as Partial<MfaAdminServiceError>).statusCode === 'number'\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { CrudHttpError, forbidden } from '@open-mercato/shared/lib/crud/errors'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { enforceTenantSelection, resolveIsSuperAdmin } from '@open-mercato/core/modules/auth/lib/tenantAccess'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { isSudoRequiredError } from '../../lib/sudo-middleware'\nimport type { MfaAdminService, MfaAdminServiceError } from '../../services/MfaAdminService'\nimport { localizeSecurityApiBody, securityApiError } from '../i18n'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('security').child({ component: 'users' })\n\ntype RequestContainer = Awaited<ReturnType<typeof createRequestContainer>>\ntype Auth = NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>\n\nexport type SecurityUsersRequestContext = {\n auth: Auth\n container: RequestContainer\n commandContext: CommandRuntimeContext\n mfaAdminService: MfaAdminService\n}\n\nexport async function resolveSecurityUsersContext(\n req: Request,\n): Promise<SecurityUsersRequestContext | NextResponse> {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return securityApiError(401, 'Unauthorized')\n }\n\n const container = await createRequestContainer()\n return {\n auth,\n container,\n commandContext: {\n container,\n auth,\n organizationScope: null,\n selectedOrganizationId: auth.orgId ?? null,\n organizationIds: auth.orgId ? [auth.orgId] : null,\n request: req,\n },\n mfaAdminService: container.resolve<MfaAdminService>('mfaAdminService'),\n }\n}\n\nfunction normalizeNullableString(value: unknown): string | null {\n return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null\n}\n\nfunction normalizeOrganizationList(values: unknown): string[] | null {\n if (values === null || values === undefined) return null\n if (!Array.isArray(values)) return null\n const result: string[] = []\n for (const value of values) {\n if (typeof value !== 'string') continue\n const trimmed = value.trim()\n if (trimmed) result.push(trimmed)\n }\n return result\n}\n\nexport async function assertActorCanAccessSecurityUserTarget(\n ctx: SecurityUsersRequestContext,\n targetUserId: string,\n): Promise<void> {\n const isSuperAdmin = await resolveIsSuperAdmin({ auth: ctx.auth, container: ctx.container })\n if (isSuperAdmin) return\n\n const em = ctx.container.resolve<EntityManager>('em')\n const target = await findOneWithDecryption(\n em,\n User,\n { id: targetUserId, deletedAt: null } as FilterQuery<User>,\n {},\n { tenantId: null, organizationId: null },\n )\n if (!target) {\n throw new CrudHttpError(404, { error: 'User not found' })\n }\n\n const actorTenantId = normalizeNullableString(ctx.auth.tenantId)\n const targetTenantId = normalizeNullableString((target as { tenantId?: string | null }).tenantId)\n if (!targetTenantId || targetTenantId !== actorTenantId) {\n throw new CrudHttpError(404, { error: 'User not found' })\n }\n\n const rbacService = ctx.container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(ctx.auth.sub, {\n tenantId: actorTenantId,\n organizationId: normalizeNullableString(ctx.auth.orgId),\n })\n const organizations = normalizeOrganizationList(acl?.organizations)\n if (organizations !== null && !organizations.includes('__all__')) {\n const targetOrganizationId = normalizeNullableString((target as { organizationId?: string | null }).organizationId)\n if (!targetOrganizationId || !organizations.includes(targetOrganizationId)) {\n throw forbidden('Not authorized to access this user.')\n }\n }\n}\n\nexport async function assertActorOwnsTenantScope(\n ctx: SecurityUsersRequestContext,\n requestedTenantId: string | null | undefined,\n): Promise<string | null> {\n const resolved = await enforceTenantSelection({ auth: ctx.auth, container: ctx.container }, requestedTenantId)\n return resolved ?? ctx.auth.tenantId ?? null\n}\n\nexport async function mapSecurityUsersError(error: unknown): Promise<NextResponse> {\n if (error instanceof CrudHttpError) {\n return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.status })\n }\n if (isSudoRequiredError(error)) {\n return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.statusCode })\n }\n if (isMfaAdminServiceError(error)) {\n return securityApiError(error.statusCode, error.message)\n }\n\n logger.error('Users route failure', { err: error })\n return securityApiError(500, 'Failed to process user security request.')\n}\n\nfunction isMfaAdminServiceError(error: unknown): error is MfaAdminServiceError {\n return error instanceof Error\n && error.name === 'MfaAdminServiceError'\n && typeof (error as Partial<MfaAdminServiceError>).statusCode === 'number'\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -3,6 +3,8 @@ import { toAbsoluteUrl } from "@open-mercato/shared/lib/url";
3
3
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
4
4
  import { emitSsoEvent } from "../../../events.js";
5
5
  import { resolveSsoCallbackErrorCode } from "../../../lib/errors.js";
6
+ import { createLogger } from "@open-mercato/shared/lib/logger";
7
+ const logger = createLogger("sso").child({ component: "callback" });
6
8
  const metadata = {
7
9
  GET: { requireAuth: false },
8
10
  POST: { requireAuth: false }
@@ -35,7 +37,7 @@ async function handleCallback(req) {
35
37
  if (callbackParams.error) {
36
38
  void emitSsoEvent("sso.login.failed", {
37
39
  reason: callbackParams.error
38
- }).catch((e) => console.error("[SSO Event]", e));
40
+ }).catch((eventError) => logger.error("SSO event emit failed", { err: eventError }));
39
41
  return NextResponse.redirect(toAbsoluteUrl(req, "/login?error=sso_idp_error"));
40
42
  }
41
43
  if (!callbackParams.code || !callbackParams.state) {
@@ -63,10 +65,10 @@ async function handleCallback(req) {
63
65
  res.cookies.set("sso_state", "", { path: "/", maxAge: 0 });
64
66
  return res;
65
67
  } catch (err) {
66
- console.error("[SSO Callback] Error:", err);
68
+ logger.error("SSO callback error", { err });
67
69
  void emitSsoEvent("sso.login.failed", {
68
70
  reason: err instanceof Error ? err.message : "callback_failed"
69
- }).catch((e) => console.error("[SSO Event]", e));
71
+ }).catch((eventError) => logger.error("SSO event emit failed", { err: eventError }));
70
72
  const errorCode = resolveSsoCallbackErrorCode(err);
71
73
  return NextResponse.redirect(toAbsoluteUrl(req, `/login?error=${errorCode}`));
72
74
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/sso/api/callback/oidc/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { toAbsoluteUrl } from '@open-mercato/shared/lib/url'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { SsoService } from '../../../services/ssoService'\nimport { emitSsoEvent } from '../../../events'\nimport { resolveSsoCallbackErrorCode } from '../../../lib/errors'\n\nexport const metadata = {\n GET: { requireAuth: false },\n POST: { requireAuth: false },\n}\n\nfunction parseCookie(req: Request, name: string): string | null {\n const cookie = req.headers.get('cookie') || ''\n const m = cookie.match(new RegExp('(?:^|;\\\\s*)' + name + '=([^;]+)'))\n return m ? decodeURIComponent(m[1]) : null\n}\n\nasync function handleCallback(req: Request): Promise<NextResponse> {\n try {\n const url = new URL(req.url)\n const callbackParams: Record<string, string> = {}\n url.searchParams.forEach((value, key) => {\n callbackParams[key] = value\n })\n\n if (req.method === 'POST') {\n const contentType = req.headers.get('content-type') || ''\n if (contentType.includes('application/x-www-form-urlencoded')) {\n const form = await req.formData()\n form.forEach((value, key) => {\n callbackParams[key] = String(value)\n })\n }\n }\n\n const stateCookie = parseCookie(req, 'sso_state')\n if (!stateCookie) {\n return NextResponse.redirect(toAbsoluteUrl(req, '/login?error=sso_state_missing'))\n }\n\n if (callbackParams.error) {\n void emitSsoEvent('sso.login.failed', {\n reason: callbackParams.error,\n }).catch((e) => console.error('[SSO Event]', e))\n return NextResponse.redirect(toAbsoluteUrl(req, '/login?error=sso_idp_error'))\n }\n\n if (!callbackParams.code || !callbackParams.state) {\n return NextResponse.redirect(toAbsoluteUrl(req, '/login?error=sso_missing_params'))\n }\n\n const redirectUri = toAbsoluteUrl(req, '/api/sso/callback/oidc')\n const container = await createRequestContainer()\n const ssoService = container.resolve<SsoService>('ssoService')\n\n const result = await ssoService.handleOidcCallback(callbackParams, stateCookie, redirectUri)\n\n const res = NextResponse.redirect(toAbsoluteUrl(req, result.redirectUrl))\n\n res.cookies.set('auth_token', result.token, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV !== 'development',\n maxAge: 60 * 60 * 8,\n })\n\n res.cookies.set('session_token', result.sessionToken, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV !== 'development',\n expires: result.sessionExpiresAt,\n })\n\n res.cookies.set('sso_state', '', { path: '/', maxAge: 0 })\n\n return res\n } catch (err) {\n console.error('[SSO Callback] Error:', err)\n void emitSsoEvent('sso.login.failed', {\n reason: err instanceof Error ? err.message : 'callback_failed',\n }).catch((e) => console.error('[SSO Event]', e))\n const errorCode = resolveSsoCallbackErrorCode(err)\n return NextResponse.redirect(toAbsoluteUrl(req, `/login?error=${errorCode}`))\n }\n}\n\nexport async function GET(req: Request) {\n return handleCallback(req)\n}\n\nexport async function POST(req: Request) {\n return handleCallback(req)\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'SSO',\n summary: 'OIDC callback',\n methods: {\n GET: {\n summary: 'Handle OIDC callback (GET)',\n description: 'Receives the authorization code from the IdP, exchanges it for tokens, resolves the user, and issues auth cookies.',\n tags: ['SSO'],\n responses: [\n { status: 302, description: 'Redirect to application with auth cookies set', mediaType: 'text/html' },\n ],\n },\n POST: {\n summary: 'Handle OIDC callback (POST)',\n description: 'Some IdPs send the callback as a POST (form_post response mode). Handles the same flow as the GET variant.',\n tags: ['SSO'],\n responses: [\n { status: 302, description: 'Redirect to application with auth cookies set', mediaType: 'text/html' },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { toAbsoluteUrl } from '@open-mercato/shared/lib/url'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { SsoService } from '../../../services/ssoService'\nimport { emitSsoEvent } from '../../../events'\nimport { resolveSsoCallbackErrorCode } from '../../../lib/errors'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('sso').child({ component: 'callback' })\n\nexport const metadata = {\n GET: { requireAuth: false },\n POST: { requireAuth: false },\n}\n\nfunction parseCookie(req: Request, name: string): string | null {\n const cookie = req.headers.get('cookie') || ''\n const m = cookie.match(new RegExp('(?:^|;\\\\s*)' + name + '=([^;]+)'))\n return m ? decodeURIComponent(m[1]) : null\n}\n\nasync function handleCallback(req: Request): Promise<NextResponse> {\n try {\n const url = new URL(req.url)\n const callbackParams: Record<string, string> = {}\n url.searchParams.forEach((value, key) => {\n callbackParams[key] = value\n })\n\n if (req.method === 'POST') {\n const contentType = req.headers.get('content-type') || ''\n if (contentType.includes('application/x-www-form-urlencoded')) {\n const form = await req.formData()\n form.forEach((value, key) => {\n callbackParams[key] = String(value)\n })\n }\n }\n\n const stateCookie = parseCookie(req, 'sso_state')\n if (!stateCookie) {\n return NextResponse.redirect(toAbsoluteUrl(req, '/login?error=sso_state_missing'))\n }\n\n if (callbackParams.error) {\n void emitSsoEvent('sso.login.failed', {\n reason: callbackParams.error,\n }).catch((eventError) => logger.error('SSO event emit failed', { err: eventError }))\n return NextResponse.redirect(toAbsoluteUrl(req, '/login?error=sso_idp_error'))\n }\n\n if (!callbackParams.code || !callbackParams.state) {\n return NextResponse.redirect(toAbsoluteUrl(req, '/login?error=sso_missing_params'))\n }\n\n const redirectUri = toAbsoluteUrl(req, '/api/sso/callback/oidc')\n const container = await createRequestContainer()\n const ssoService = container.resolve<SsoService>('ssoService')\n\n const result = await ssoService.handleOidcCallback(callbackParams, stateCookie, redirectUri)\n\n const res = NextResponse.redirect(toAbsoluteUrl(req, result.redirectUrl))\n\n res.cookies.set('auth_token', result.token, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV !== 'development',\n maxAge: 60 * 60 * 8,\n })\n\n res.cookies.set('session_token', result.sessionToken, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV !== 'development',\n expires: result.sessionExpiresAt,\n })\n\n res.cookies.set('sso_state', '', { path: '/', maxAge: 0 })\n\n return res\n } catch (err) {\n logger.error('SSO callback error', { err })\n void emitSsoEvent('sso.login.failed', {\n reason: err instanceof Error ? err.message : 'callback_failed',\n }).catch((eventError) => logger.error('SSO event emit failed', { err: eventError }))\n const errorCode = resolveSsoCallbackErrorCode(err)\n return NextResponse.redirect(toAbsoluteUrl(req, `/login?error=${errorCode}`))\n }\n}\n\nexport async function GET(req: Request) {\n return handleCallback(req)\n}\n\nexport async function POST(req: Request) {\n return handleCallback(req)\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'SSO',\n summary: 'OIDC callback',\n methods: {\n GET: {\n summary: 'Handle OIDC callback (GET)',\n description: 'Receives the authorization code from the IdP, exchanges it for tokens, resolves the user, and issues auth cookies.',\n tags: ['SSO'],\n responses: [\n { status: 302, description: 'Redirect to application with auth cookies set', mediaType: 'text/html' },\n ],\n },\n POST: {\n summary: 'Handle OIDC callback (POST)',\n description: 'Some IdPs send the callback as a POST (form_post response mode). Handles the same flow as the GET variant.',\n tags: ['SSO'],\n responses: [\n { status: 302, description: 'Redirect to application with auth cookies set', mediaType: 'text/html' },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -4,6 +4,8 @@ import { SsoConfigError } from "../services/ssoConfigService.js";
4
4
  import { ScimTokenError } from "../services/scimTokenService.js";
5
5
  import { ScimServiceError } from "../services/scimService.js";
6
6
  import { scimJson, buildScimError } from "../lib/scim-response.js";
7
+ import { createLogger } from "@open-mercato/shared/lib/logger";
8
+ const logger = createLogger("sso");
7
9
  function isSsoHttpError(err) {
8
10
  return err instanceof SsoAdminAuthError || err instanceof SsoConfigError || err instanceof ScimTokenError || err instanceof ScimServiceError;
9
11
  }
@@ -11,14 +13,14 @@ function handleSsoAdminApiError(err, label) {
11
13
  if (isSsoHttpError(err)) {
12
14
  return NextResponse.json({ error: err.message }, { status: err.statusCode });
13
15
  }
14
- console.error(`[${label}] Error:`, err);
16
+ logger.error("SSO admin API error", { label, err });
15
17
  return NextResponse.json({ error: "Internal server error" }, { status: 500 });
16
18
  }
17
19
  function handleScimApiError(err, label) {
18
20
  if (err instanceof ScimServiceError) {
19
21
  return scimJson(buildScimError(err.statusCode, err.message), err.statusCode);
20
22
  }
21
- console.error(`[${label}] Error:`, err);
23
+ logger.error("SCIM API error", { label, err });
22
24
  return scimJson(buildScimError(500, "Internal server error"), 500);
23
25
  }
24
26
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/sso/api/error-handler.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { SsoAdminAuthError } from './admin-context'\nimport { SsoConfigError } from '../services/ssoConfigService'\nimport { ScimTokenError } from '../services/scimTokenService'\nimport { ScimServiceError } from '../services/scimService'\nimport { scimJson, buildScimError } from '../lib/scim-response'\n\ninterface SsoHttpError {\n message: string\n statusCode: number\n}\n\nfunction isSsoHttpError(err: unknown): err is SsoHttpError {\n return (\n err instanceof SsoAdminAuthError ||\n err instanceof SsoConfigError ||\n err instanceof ScimTokenError ||\n err instanceof ScimServiceError\n )\n}\n\nexport function handleSsoAdminApiError(err: unknown, label: string): NextResponse {\n if (isSsoHttpError(err)) {\n return NextResponse.json({ error: err.message }, { status: err.statusCode })\n }\n console.error(`[${label}] Error:`, err)\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n}\n\nexport function handleScimApiError(err: unknown, label: string): Response {\n if (err instanceof ScimServiceError) {\n return scimJson(buildScimError(err.statusCode, err.message), err.statusCode)\n }\n console.error(`[${label}] Error:`, err)\n return scimJson(buildScimError(500, 'Internal server error'), 500)\n}\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,yBAAyB;AAClC,SAAS,sBAAsB;AAC/B,SAAS,sBAAsB;AAC/B,SAAS,wBAAwB;AACjC,SAAS,UAAU,sBAAsB;AAOzC,SAAS,eAAe,KAAmC;AACzD,SACE,eAAe,qBACf,eAAe,kBACf,eAAe,kBACf,eAAe;AAEnB;AAEO,SAAS,uBAAuB,KAAc,OAA6B;AAChF,MAAI,eAAe,GAAG,GAAG;AACvB,WAAO,aAAa,KAAK,EAAE,OAAO,IAAI,QAAQ,GAAG,EAAE,QAAQ,IAAI,WAAW,CAAC;AAAA,EAC7E;AACA,UAAQ,MAAM,IAAI,KAAK,YAAY,GAAG;AACtC,SAAO,aAAa,KAAK,EAAE,OAAO,wBAAwB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAC9E;AAEO,SAAS,mBAAmB,KAAc,OAAyB;AACxE,MAAI,eAAe,kBAAkB;AACnC,WAAO,SAAS,eAAe,IAAI,YAAY,IAAI,OAAO,GAAG,IAAI,UAAU;AAAA,EAC7E;AACA,UAAQ,MAAM,IAAI,KAAK,YAAY,GAAG;AACtC,SAAO,SAAS,eAAe,KAAK,uBAAuB,GAAG,GAAG;AACnE;",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { SsoAdminAuthError } from './admin-context'\nimport { SsoConfigError } from '../services/ssoConfigService'\nimport { ScimTokenError } from '../services/scimTokenService'\nimport { ScimServiceError } from '../services/scimService'\nimport { scimJson, buildScimError } from '../lib/scim-response'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('sso')\n\ninterface SsoHttpError {\n message: string\n statusCode: number\n}\n\nfunction isSsoHttpError(err: unknown): err is SsoHttpError {\n return (\n err instanceof SsoAdminAuthError ||\n err instanceof SsoConfigError ||\n err instanceof ScimTokenError ||\n err instanceof ScimServiceError\n )\n}\n\nexport function handleSsoAdminApiError(err: unknown, label: string): NextResponse {\n if (isSsoHttpError(err)) {\n return NextResponse.json({ error: err.message }, { status: err.statusCode })\n }\n logger.error('SSO admin API error', { label, err })\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n}\n\nexport function handleScimApiError(err: unknown, label: string): Response {\n if (err instanceof ScimServiceError) {\n return scimJson(buildScimError(err.statusCode, err.message), err.statusCode)\n }\n logger.error('SCIM API error', { label, err })\n return scimJson(buildScimError(500, 'Internal server error'), 500)\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,yBAAyB;AAClC,SAAS,sBAAsB;AAC/B,SAAS,sBAAsB;AAC/B,SAAS,wBAAwB;AACjC,SAAS,UAAU,sBAAsB;AACzC,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,KAAK;AAOjC,SAAS,eAAe,KAAmC;AACzD,SACE,eAAe,qBACf,eAAe,kBACf,eAAe,kBACf,eAAe;AAEnB;AAEO,SAAS,uBAAuB,KAAc,OAA6B;AAChF,MAAI,eAAe,GAAG,GAAG;AACvB,WAAO,aAAa,KAAK,EAAE,OAAO,IAAI,QAAQ,GAAG,EAAE,QAAQ,IAAI,WAAW,CAAC;AAAA,EAC7E;AACA,SAAO,MAAM,uBAAuB,EAAE,OAAO,IAAI,CAAC;AAClD,SAAO,aAAa,KAAK,EAAE,OAAO,wBAAwB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAC9E;AAEO,SAAS,mBAAmB,KAAc,OAAyB;AACxE,MAAI,eAAe,kBAAkB;AACnC,WAAO,SAAS,eAAe,IAAI,YAAY,IAAI,OAAO,GAAG,IAAI,UAAU;AAAA,EAC7E;AACA,SAAO,MAAM,kBAAkB,EAAE,OAAO,IAAI,CAAC;AAC7C,SAAO,SAAS,eAAe,KAAK,uBAAuB,GAAG,GAAG;AACnE;",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,8 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
3
3
  import { hrdRequestSchema } from "../../data/validators.js";
4
+ import { createLogger } from "@open-mercato/shared/lib/logger";
5
+ const logger = createLogger("sso").child({ component: "hrd" });
4
6
  const metadata = {
5
7
  POST: { requireAuth: false }
6
8
  };
@@ -23,7 +25,7 @@ async function POST(req) {
23
25
  protocol: config.protocol
24
26
  });
25
27
  } catch (err) {
26
- console.error("[SSO HRD] Error:", err);
28
+ logger.error("SSO HRD error", { err });
27
29
  return NextResponse.json({ error: "Internal server error" }, { status: 500 });
28
30
  }
29
31
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/sso/api/hrd/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { HrdService } from '../../services/hrdService'\nimport { hrdRequestSchema } from '../../data/validators'\n\nexport const metadata = {\n POST: { requireAuth: false },\n}\n\nexport async function POST(req: Request) {\n try {\n const body = await req.json()\n const parsed = hrdRequestSchema.safeParse(body)\n\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid request' }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const hrdService = container.resolve<HrdService>('hrdService')\n const config = await hrdService.findActiveConfigByEmailDomain(parsed.data.email)\n\n if (!config) {\n return NextResponse.json({ hasSso: false })\n }\n\n return NextResponse.json({\n hasSso: true,\n configId: config.id,\n protocol: config.protocol,\n })\n } catch (err) {\n console.error('[SSO HRD] Error:', err)\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'SSO',\n summary: 'Home Realm Discovery',\n methods: {\n POST: {\n summary: 'Check if email domain has SSO configured',\n description: 'Given an email address, determines if the associated organization has an active SSO configuration. Called from the login page before authentication.',\n tags: ['SSO'],\n requestBody: {\n contentType: 'application/json',\n schema: hrdRequestSchema,\n },\n responses: [\n { status: 200, description: 'HRD result' },\n ],\n errors: [\n { status: 400, description: 'Invalid request' },\n ],\n },\n },\n}\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,8BAA8B;AAEvC,SAAS,wBAAwB;AAE1B,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM;AAC7B;AAEA,eAAsB,KAAK,KAAc;AACvC,MAAI;AACF,UAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,UAAM,SAAS,iBAAiB,UAAU,IAAI;AAE9C,QAAI,CAAC,OAAO,SAAS;AACnB,aAAO,aAAa,KAAK,EAAE,OAAO,kBAAkB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACxE;AAEA,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,aAAa,UAAU,QAAoB,YAAY;AAC7D,UAAM,SAAS,MAAM,WAAW,8BAA8B,OAAO,KAAK,KAAK;AAE/E,QAAI,CAAC,QAAQ;AACX,aAAO,aAAa,KAAK,EAAE,QAAQ,MAAM,CAAC;AAAA,IAC5C;AAEA,WAAO,aAAa,KAAK;AAAA,MACvB,QAAQ;AAAA,MACR,UAAU,OAAO;AAAA,MACjB,UAAU,OAAO;AAAA,IACnB,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,YAAQ,MAAM,oBAAoB,GAAG;AACrC,WAAO,aAAa,KAAK,EAAE,OAAO,wBAAwB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC9E;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aAAa;AAAA,MACb,MAAM,CAAC,KAAK;AAAA,MACZ,aAAa;AAAA,QACX,aAAa;AAAA,QACb,QAAQ;AAAA,MACV;AAAA,MACA,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,aAAa;AAAA,MAC3C;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,kBAAkB;AAAA,MAChD;AAAA,IACF;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { HrdService } from '../../services/hrdService'\nimport { hrdRequestSchema } from '../../data/validators'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('sso').child({ component: 'hrd' })\n\nexport const metadata = {\n POST: { requireAuth: false },\n}\n\nexport async function POST(req: Request) {\n try {\n const body = await req.json()\n const parsed = hrdRequestSchema.safeParse(body)\n\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid request' }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const hrdService = container.resolve<HrdService>('hrdService')\n const config = await hrdService.findActiveConfigByEmailDomain(parsed.data.email)\n\n if (!config) {\n return NextResponse.json({ hasSso: false })\n }\n\n return NextResponse.json({\n hasSso: true,\n configId: config.id,\n protocol: config.protocol,\n })\n } catch (err) {\n logger.error('SSO HRD error', { err })\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'SSO',\n summary: 'Home Realm Discovery',\n methods: {\n POST: {\n summary: 'Check if email domain has SSO configured',\n description: 'Given an email address, determines if the associated organization has an active SSO configuration. Called from the login page before authentication.',\n tags: ['SSO'],\n requestBody: {\n contentType: 'application/json',\n schema: hrdRequestSchema,\n },\n responses: [\n { status: 200, description: 'HRD result' },\n ],\n errors: [\n { status: 400, description: 'Invalid request' },\n ],\n },\n },\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,8BAA8B;AAEvC,SAAS,wBAAwB;AACjC,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,KAAK,EAAE,MAAM,EAAE,WAAW,MAAM,CAAC;AAEtD,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM;AAC7B;AAEA,eAAsB,KAAK,KAAc;AACvC,MAAI;AACF,UAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,UAAM,SAAS,iBAAiB,UAAU,IAAI;AAE9C,QAAI,CAAC,OAAO,SAAS;AACnB,aAAO,aAAa,KAAK,EAAE,OAAO,kBAAkB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACxE;AAEA,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,aAAa,UAAU,QAAoB,YAAY;AAC7D,UAAM,SAAS,MAAM,WAAW,8BAA8B,OAAO,KAAK,KAAK;AAE/E,QAAI,CAAC,QAAQ;AACX,aAAO,aAAa,KAAK,EAAE,QAAQ,MAAM,CAAC;AAAA,IAC5C;AAEA,WAAO,aAAa,KAAK;AAAA,MACvB,QAAQ;AAAA,MACR,UAAU,OAAO;AAAA,MACjB,UAAU,OAAO;AAAA,IACnB,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,WAAO,MAAM,iBAAiB,EAAE,IAAI,CAAC;AACrC,WAAO,aAAa,KAAK,EAAE,OAAO,wBAAwB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC9E;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aAAa;AAAA,MACb,MAAM,CAAC,KAAK;AAAA,MACZ,aAAa;AAAA,QACX,aAAa;AAAA,QACb,QAAQ;AAAA,MACV;AAAA,MACA,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,aAAa;AAAA,MAC3C;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,kBAAkB;AAAA,MAChD;AAAA,IACF;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -3,6 +3,8 @@ import { toAbsoluteUrl } from "@open-mercato/shared/lib/url";
3
3
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
4
4
  import { ssoInitiateSchema } from "../../data/validators.js";
5
5
  import { emitSsoEvent } from "../../events.js";
6
+ import { createLogger } from "@open-mercato/shared/lib/logger";
7
+ const logger = createLogger("sso").child({ component: "initiate" });
6
8
  const metadata = {
7
9
  GET: { requireAuth: false }
8
10
  };
@@ -41,10 +43,10 @@ async function GET(req) {
41
43
  });
42
44
  return res;
43
45
  } catch (err) {
44
- console.error("[SSO Initiate] Error:", err);
46
+ logger.error("SSO initiate error", { err });
45
47
  void emitSsoEvent("sso.login.failed", {
46
48
  reason: err instanceof Error ? err.message : "initiate_failed"
47
- }).catch((e) => console.error("[SSO Event]", e));
49
+ }).catch((eventError) => logger.error("SSO event emit failed", { err: eventError }));
48
50
  return NextResponse.redirect(toAbsoluteUrl(req, "/login?error=sso_failed"));
49
51
  }
50
52
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/sso/api/initiate/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { toAbsoluteUrl } from '@open-mercato/shared/lib/url'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { SsoService } from '../../services/ssoService'\nimport { ssoInitiateSchema } from '../../data/validators'\nimport { emitSsoEvent } from '../../events'\n\nexport const metadata = {\n GET: { requireAuth: false },\n}\n\nfunction sanitizeReturnUrl(raw: string | null): string {\n const value = raw || '/backend'\n if (!value.startsWith('/') || value.startsWith('//')) return '/backend'\n try {\n const parsed = new URL(value, 'http://localhost')\n if (parsed.origin !== 'http://localhost') return '/backend'\n return parsed.pathname + parsed.search + parsed.hash\n } catch {\n return '/backend'\n }\n}\n\nexport async function GET(req: Request) {\n try {\n const url = new URL(req.url)\n const configId = url.searchParams.get('configId')\n const rawReturnUrl = url.searchParams.get('returnUrl')\n\n const parsed = ssoInitiateSchema.safeParse({ configId, returnUrl: rawReturnUrl ?? undefined })\n if (!parsed.success || !parsed.data.configId) {\n return NextResponse.redirect(toAbsoluteUrl(req, '/login?error=sso_missing_config'))\n }\n\n const returnUrl = sanitizeReturnUrl(rawReturnUrl)\n const redirectUri = toAbsoluteUrl(req, '/api/sso/callback/oidc')\n const container = await createRequestContainer()\n const ssoService = container.resolve<SsoService>('ssoService')\n\n const { redirectUrl, stateCookie } = await ssoService.initiateLogin(parsed.data.configId, returnUrl, redirectUri)\n\n const res = NextResponse.redirect(redirectUrl)\n res.cookies.set('sso_state', stateCookie, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV !== 'development',\n maxAge: 300,\n })\n return res\n } catch (err) {\n console.error('[SSO Initiate] Error:', err)\n void emitSsoEvent('sso.login.failed', {\n reason: err instanceof Error ? err.message : 'initiate_failed',\n }).catch((e) => console.error('[SSO Event]', e))\n return NextResponse.redirect(toAbsoluteUrl(req, '/login?error=sso_failed'))\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'SSO',\n summary: 'Initiate SSO login',\n methods: {\n GET: {\n summary: 'Start SSO login flow',\n description: 'Redirects the browser to the configured IdP authorization endpoint. Sets an encrypted sso_state cookie for CSRF protection.',\n tags: ['SSO'],\n responses: [\n { status: 302, description: 'Redirect to IdP authorization endpoint', mediaType: 'text/html' },\n ],\n },\n },\n}\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,qBAAqB;AAC9B,SAAS,8BAA8B;AAEvC,SAAS,yBAAyB;AAClC,SAAS,oBAAoB;AAEtB,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM;AAC5B;AAEA,SAAS,kBAAkB,KAA4B;AACrD,QAAM,QAAQ,OAAO;AACrB,MAAI,CAAC,MAAM,WAAW,GAAG,KAAK,MAAM,WAAW,IAAI,EAAG,QAAO;AAC7D,MAAI;AACF,UAAM,SAAS,IAAI,IAAI,OAAO,kBAAkB;AAChD,QAAI,OAAO,WAAW,mBAAoB,QAAO;AACjD,WAAO,OAAO,WAAW,OAAO,SAAS,OAAO;AAAA,EAClD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,IAAI,KAAc;AACtC,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAC3B,UAAM,WAAW,IAAI,aAAa,IAAI,UAAU;AAChD,UAAM,eAAe,IAAI,aAAa,IAAI,WAAW;AAErD,UAAM,SAAS,kBAAkB,UAAU,EAAE,UAAU,WAAW,gBAAgB,OAAU,CAAC;AAC7F,QAAI,CAAC,OAAO,WAAW,CAAC,OAAO,KAAK,UAAU;AAC5C,aAAO,aAAa,SAAS,cAAc,KAAK,iCAAiC,CAAC;AAAA,IACpF;AAEA,UAAM,YAAY,kBAAkB,YAAY;AAChD,UAAM,cAAc,cAAc,KAAK,wBAAwB;AAC/D,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,aAAa,UAAU,QAAoB,YAAY;AAE7D,UAAM,EAAE,aAAa,YAAY,IAAI,MAAM,WAAW,cAAc,OAAO,KAAK,UAAU,WAAW,WAAW;AAEhH,UAAM,MAAM,aAAa,SAAS,WAAW;AAC7C,QAAI,QAAQ,IAAI,aAAa,aAAa;AAAA,MACxC,UAAU;AAAA,MACV,MAAM;AAAA,MACN,UAAU;AAAA,MACV,QAAQ,QAAQ,IAAI,aAAa;AAAA,MACjC,QAAQ;AAAA,IACV,CAAC;AACD,WAAO;AAAA,EACT,SAAS,KAAK;AACZ,YAAQ,MAAM,yBAAyB,GAAG;AAC1C,SAAK,aAAa,oBAAoB;AAAA,MACpC,QAAQ,eAAe,QAAQ,IAAI,UAAU;AAAA,IAC/C,CAAC,EAAE,MAAM,CAAC,MAAM,QAAQ,MAAM,eAAe,CAAC,CAAC;AAC/C,WAAO,aAAa,SAAS,cAAc,KAAK,yBAAyB,CAAC;AAAA,EAC5E;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,MACH,SAAS;AAAA,MACT,aAAa;AAAA,MACb,MAAM,CAAC,KAAK;AAAA,MACZ,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,0CAA0C,WAAW,YAAY;AAAA,MAC/F;AAAA,IACF;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { toAbsoluteUrl } from '@open-mercato/shared/lib/url'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { SsoService } from '../../services/ssoService'\nimport { ssoInitiateSchema } from '../../data/validators'\nimport { emitSsoEvent } from '../../events'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('sso').child({ component: 'initiate' })\n\nexport const metadata = {\n GET: { requireAuth: false },\n}\n\nfunction sanitizeReturnUrl(raw: string | null): string {\n const value = raw || '/backend'\n if (!value.startsWith('/') || value.startsWith('//')) return '/backend'\n try {\n const parsed = new URL(value, 'http://localhost')\n if (parsed.origin !== 'http://localhost') return '/backend'\n return parsed.pathname + parsed.search + parsed.hash\n } catch {\n return '/backend'\n }\n}\n\nexport async function GET(req: Request) {\n try {\n const url = new URL(req.url)\n const configId = url.searchParams.get('configId')\n const rawReturnUrl = url.searchParams.get('returnUrl')\n\n const parsed = ssoInitiateSchema.safeParse({ configId, returnUrl: rawReturnUrl ?? undefined })\n if (!parsed.success || !parsed.data.configId) {\n return NextResponse.redirect(toAbsoluteUrl(req, '/login?error=sso_missing_config'))\n }\n\n const returnUrl = sanitizeReturnUrl(rawReturnUrl)\n const redirectUri = toAbsoluteUrl(req, '/api/sso/callback/oidc')\n const container = await createRequestContainer()\n const ssoService = container.resolve<SsoService>('ssoService')\n\n const { redirectUrl, stateCookie } = await ssoService.initiateLogin(parsed.data.configId, returnUrl, redirectUri)\n\n const res = NextResponse.redirect(redirectUrl)\n res.cookies.set('sso_state', stateCookie, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV !== 'development',\n maxAge: 300,\n })\n return res\n } catch (err) {\n logger.error('SSO initiate error', { err })\n void emitSsoEvent('sso.login.failed', {\n reason: err instanceof Error ? err.message : 'initiate_failed',\n }).catch((eventError) => logger.error('SSO event emit failed', { err: eventError }))\n return NextResponse.redirect(toAbsoluteUrl(req, '/login?error=sso_failed'))\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'SSO',\n summary: 'Initiate SSO login',\n methods: {\n GET: {\n summary: 'Start SSO login flow',\n description: 'Redirects the browser to the configured IdP authorization endpoint. Sets an encrypted sso_state cookie for CSRF protection.',\n tags: ['SSO'],\n responses: [\n { status: 302, description: 'Redirect to IdP authorization endpoint', mediaType: 'text/html' },\n ],\n },\n },\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,qBAAqB;AAC9B,SAAS,8BAA8B;AAEvC,SAAS,yBAAyB;AAClC,SAAS,oBAAoB;AAC7B,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,KAAK,EAAE,MAAM,EAAE,WAAW,WAAW,CAAC;AAE3D,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM;AAC5B;AAEA,SAAS,kBAAkB,KAA4B;AACrD,QAAM,QAAQ,OAAO;AACrB,MAAI,CAAC,MAAM,WAAW,GAAG,KAAK,MAAM,WAAW,IAAI,EAAG,QAAO;AAC7D,MAAI;AACF,UAAM,SAAS,IAAI,IAAI,OAAO,kBAAkB;AAChD,QAAI,OAAO,WAAW,mBAAoB,QAAO;AACjD,WAAO,OAAO,WAAW,OAAO,SAAS,OAAO;AAAA,EAClD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,IAAI,KAAc;AACtC,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAC3B,UAAM,WAAW,IAAI,aAAa,IAAI,UAAU;AAChD,UAAM,eAAe,IAAI,aAAa,IAAI,WAAW;AAErD,UAAM,SAAS,kBAAkB,UAAU,EAAE,UAAU,WAAW,gBAAgB,OAAU,CAAC;AAC7F,QAAI,CAAC,OAAO,WAAW,CAAC,OAAO,KAAK,UAAU;AAC5C,aAAO,aAAa,SAAS,cAAc,KAAK,iCAAiC,CAAC;AAAA,IACpF;AAEA,UAAM,YAAY,kBAAkB,YAAY;AAChD,UAAM,cAAc,cAAc,KAAK,wBAAwB;AAC/D,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,aAAa,UAAU,QAAoB,YAAY;AAE7D,UAAM,EAAE,aAAa,YAAY,IAAI,MAAM,WAAW,cAAc,OAAO,KAAK,UAAU,WAAW,WAAW;AAEhH,UAAM,MAAM,aAAa,SAAS,WAAW;AAC7C,QAAI,QAAQ,IAAI,aAAa,aAAa;AAAA,MACxC,UAAU;AAAA,MACV,MAAM;AAAA,MACN,UAAU;AAAA,MACV,QAAQ,QAAQ,IAAI,aAAa;AAAA,MACjC,QAAQ;AAAA,IACV,CAAC;AACD,WAAO;AAAA,EACT,SAAS,KAAK;AACZ,WAAO,MAAM,sBAAsB,EAAE,IAAI,CAAC;AAC1C,SAAK,aAAa,oBAAoB;AAAA,MACpC,QAAQ,eAAe,QAAQ,IAAI,UAAU;AAAA,IAC/C,CAAC,EAAE,MAAM,CAAC,eAAe,OAAO,MAAM,yBAAyB,EAAE,KAAK,WAAW,CAAC,CAAC;AACnF,WAAO,aAAa,SAAS,cAAc,KAAK,yBAAyB,CAAC;AAAA,EAC5E;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,MACH,SAAS;AAAA,MACT,aAAa;AAAA,MACb,MAAM,CAAC,KAAK;AAAA,MACZ,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,0CAA0C,WAAW,YAAY;AAAA,MAC/F;AAAA,IACF;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -504,7 +504,7 @@ function RoleMappingsTab({
504
504
  };
505
505
  const mappingEntries = Object.entries(mappings);
506
506
  return /* @__PURE__ */ jsxs("div", { children: [
507
- /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground mb-4", children: t("sso.admin.roles.description", "Map IdP app role names to local roles. On each SSO login, SSO-sourced roles are synced \u2014 roles no longer sent by the IdP are removed, while manually-assigned roles are preserved.") }),
507
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground mb-4", children: t("sso.admin.roles.description", "Map IdP app role names to local roles. Only explicitly mapped IdP roles grant local roles; on each SSO login, SSO-sourced roles are synced while manually-assigned roles are preserved.") }),
508
508
  /* @__PURE__ */ jsxs("div", { className: "flex items-end gap-2 mb-4", children: [
509
509
  /* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
510
510
  /* @__PURE__ */ jsx("label", { className: "block text-xs font-medium mb-1", children: t("sso.admin.roles.idpRole", "IdP Role Name") }),
@@ -550,7 +550,7 @@ function RoleMappingsTab({
550
550
  /* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: localRole })
551
551
  ] }),
552
552
  /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: () => handleRemove(idpRole), children: t("common.remove", "Remove") })
553
- ] }, idpRole)) }) : /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground py-4 text-center mb-4", children: t("sso.admin.roles.empty", "No role mappings configured. IdP role names will be matched directly against local role names.") }),
553
+ ] }, idpRole)) }) : /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground py-4 text-center mb-4", children: t("sso.admin.roles.empty", "No role mappings configured. Users will not receive SSO-sourced roles until an IdP role is explicitly mapped to a local role.") }),
554
554
  /* @__PURE__ */ jsx(Button, { onClick: handleSave, disabled: isSaving, children: isSaving ? t("common.saving", "Saving...") : t("common.save", "Save") })
555
555
  ] });
556
556
  }