@open-mercato/enterprise 0.4.6-develop-15c18897fc → 0.4.6-develop-34aa847ce6

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 (195) hide show
  1. package/dist/index.js +1 -1
  2. package/dist/index.js.map +2 -2
  3. package/dist/modules/sso/acl.js +11 -0
  4. package/dist/modules/sso/acl.js.map +7 -0
  5. package/dist/modules/sso/api/admin-context.js +27 -0
  6. package/dist/modules/sso/api/admin-context.js.map +7 -0
  7. package/dist/modules/sso/api/callback/oidc/route.js +103 -0
  8. package/dist/modules/sso/api/callback/oidc/route.js.map +7 -0
  9. package/dist/modules/sso/api/config/[id]/activate/route.js +49 -0
  10. package/dist/modules/sso/api/config/[id]/activate/route.js.map +7 -0
  11. package/dist/modules/sso/api/config/[id]/domains/route.js +96 -0
  12. package/dist/modules/sso/api/config/[id]/domains/route.js.map +7 -0
  13. package/dist/modules/sso/api/config/[id]/route.js +103 -0
  14. package/dist/modules/sso/api/config/[id]/route.js.map +7 -0
  15. package/dist/modules/sso/api/config/[id]/test/route.js +41 -0
  16. package/dist/modules/sso/api/config/[id]/test/route.js.map +7 -0
  17. package/dist/modules/sso/api/config/route.js +83 -0
  18. package/dist/modules/sso/api/config/route.js.map +7 -0
  19. package/dist/modules/sso/api/error-handler.js +28 -0
  20. package/dist/modules/sso/api/error-handler.js.map +7 -0
  21. package/dist/modules/sso/api/hrd/route.js +52 -0
  22. package/dist/modules/sso/api/hrd/route.js.map +7 -0
  23. package/dist/modules/sso/api/initiate/route.js +66 -0
  24. package/dist/modules/sso/api/initiate/route.js.map +7 -0
  25. package/dist/modules/sso/api/scim/context.js +68 -0
  26. package/dist/modules/sso/api/scim/context.js.map +7 -0
  27. package/dist/modules/sso/api/scim/logs/route.js +65 -0
  28. package/dist/modules/sso/api/scim/logs/route.js.map +7 -0
  29. package/dist/modules/sso/api/scim/tokens/[id]/route.js +42 -0
  30. package/dist/modules/sso/api/scim/tokens/[id]/route.js.map +7 -0
  31. package/dist/modules/sso/api/scim/tokens/route.js +83 -0
  32. package/dist/modules/sso/api/scim/tokens/route.js.map +7 -0
  33. package/dist/modules/sso/api/scim/v2/ServiceProviderConfig/route.js +42 -0
  34. package/dist/modules/sso/api/scim/v2/ServiceProviderConfig/route.js.map +7 -0
  35. package/dist/modules/sso/api/scim/v2/Users/[id]/route.js +94 -0
  36. package/dist/modules/sso/api/scim/v2/Users/[id]/route.js.map +7 -0
  37. package/dist/modules/sso/api/scim/v2/Users/route.js +86 -0
  38. package/dist/modules/sso/api/scim/v2/Users/route.js.map +7 -0
  39. package/dist/modules/sso/backend/page.js +173 -0
  40. package/dist/modules/sso/backend/page.js.map +7 -0
  41. package/dist/modules/sso/backend/page.meta.js +31 -0
  42. package/dist/modules/sso/backend/page.meta.js.map +7 -0
  43. package/dist/modules/sso/backend/sso/config/[id]/page.js +749 -0
  44. package/dist/modules/sso/backend/sso/config/[id]/page.js.map +7 -0
  45. package/dist/modules/sso/backend/sso/config/[id]/page.meta.js +19 -0
  46. package/dist/modules/sso/backend/sso/config/[id]/page.meta.js.map +7 -0
  47. package/dist/modules/sso/backend/sso/config/new/page.js +381 -0
  48. package/dist/modules/sso/backend/sso/config/new/page.js.map +7 -0
  49. package/dist/modules/sso/backend/sso/config/new/page.meta.js +19 -0
  50. package/dist/modules/sso/backend/sso/config/new/page.meta.js.map +7 -0
  51. package/dist/modules/sso/data/entities.js +299 -0
  52. package/dist/modules/sso/data/entities.js.map +7 -0
  53. package/dist/modules/sso/data/validators.js +114 -0
  54. package/dist/modules/sso/data/validators.js.map +7 -0
  55. package/dist/modules/sso/di.js +26 -0
  56. package/dist/modules/sso/di.js.map +7 -0
  57. package/dist/modules/sso/events.js +24 -0
  58. package/dist/modules/sso/events.js.map +7 -0
  59. package/dist/modules/sso/i18n/de.json +146 -0
  60. package/dist/modules/sso/i18n/en.json +146 -0
  61. package/dist/modules/sso/i18n/es.json +146 -0
  62. package/dist/modules/sso/i18n/pl.json +146 -0
  63. package/dist/modules/sso/index.js +11 -0
  64. package/dist/modules/sso/index.js.map +7 -0
  65. package/dist/modules/sso/lib/domains.js +30 -0
  66. package/dist/modules/sso/lib/domains.js.map +7 -0
  67. package/dist/modules/sso/lib/oidc-provider.js +140 -0
  68. package/dist/modules/sso/lib/oidc-provider.js.map +7 -0
  69. package/dist/modules/sso/lib/registry.js +15 -0
  70. package/dist/modules/sso/lib/registry.js.map +7 -0
  71. package/dist/modules/sso/lib/scim-filter.js +43 -0
  72. package/dist/modules/sso/lib/scim-filter.js.map +7 -0
  73. package/dist/modules/sso/lib/scim-mapper.js +49 -0
  74. package/dist/modules/sso/lib/scim-mapper.js.map +7 -0
  75. package/dist/modules/sso/lib/scim-patch.js +63 -0
  76. package/dist/modules/sso/lib/scim-patch.js.map +7 -0
  77. package/dist/modules/sso/lib/scim-response.js +34 -0
  78. package/dist/modules/sso/lib/scim-response.js.map +7 -0
  79. package/dist/modules/sso/lib/scim-utils.js +9 -0
  80. package/dist/modules/sso/lib/scim-utils.js.map +7 -0
  81. package/dist/modules/sso/lib/state-cookie.js +67 -0
  82. package/dist/modules/sso/lib/state-cookie.js.map +7 -0
  83. package/dist/modules/sso/lib/types.js +1 -0
  84. package/dist/modules/sso/lib/types.js.map +7 -0
  85. package/dist/modules/sso/migrations/Migration20260219000000_sso.js +20 -0
  86. package/dist/modules/sso/migrations/Migration20260219000000_sso.js.map +7 -0
  87. package/dist/modules/sso/migrations/Migration20260222000000_sso_add_name.js +13 -0
  88. package/dist/modules/sso/migrations/Migration20260222000000_sso_add_name.js.map +7 -0
  89. package/dist/modules/sso/migrations/Migration20260222000001_sso_partial_unique_org.js +15 -0
  90. package/dist/modules/sso/migrations/Migration20260222000001_sso_partial_unique_org.js.map +7 -0
  91. package/dist/modules/sso/migrations/Migration20260223000000_scim_tables.js +22 -0
  92. package/dist/modules/sso/migrations/Migration20260223000000_scim_tables.js.map +7 -0
  93. package/dist/modules/sso/migrations/Migration20260224000000_sso_external_id.js +15 -0
  94. package/dist/modules/sso/migrations/Migration20260224000000_sso_external_id.js.map +7 -0
  95. package/dist/modules/sso/migrations/Migration20260224100000_sso_role_grants.js +17 -0
  96. package/dist/modules/sso/migrations/Migration20260224100000_sso_role_grants.js.map +7 -0
  97. package/dist/modules/sso/migrations/Migration20260224200000_drop_default_role_id.js +13 -0
  98. package/dist/modules/sso/migrations/Migration20260224200000_drop_default_role_id.js.map +7 -0
  99. package/dist/modules/sso/migrations/Migration20260225000000_sso_identities_partial_unique.js +23 -0
  100. package/dist/modules/sso/migrations/Migration20260225000000_sso_identities_partial_unique.js.map +7 -0
  101. package/dist/modules/sso/migrations/Migration20260305000000_sso_role_grants_org_id.js +14 -0
  102. package/dist/modules/sso/migrations/Migration20260305000000_sso_role_grants_org_id.js.map +7 -0
  103. package/dist/modules/sso/services/accountLinkingService.js +298 -0
  104. package/dist/modules/sso/services/accountLinkingService.js.map +7 -0
  105. package/dist/modules/sso/services/hrdService.js +18 -0
  106. package/dist/modules/sso/services/hrdService.js.map +7 -0
  107. package/dist/modules/sso/services/scimService.js +372 -0
  108. package/dist/modules/sso/services/scimService.js.map +7 -0
  109. package/dist/modules/sso/services/scimTokenService.js +94 -0
  110. package/dist/modules/sso/services/scimTokenService.js.map +7 -0
  111. package/dist/modules/sso/services/ssoConfigService.js +254 -0
  112. package/dist/modules/sso/services/ssoConfigService.js.map +7 -0
  113. package/dist/modules/sso/services/ssoService.js +125 -0
  114. package/dist/modules/sso/services/ssoService.js.map +7 -0
  115. package/dist/modules/sso/setup.js +47 -0
  116. package/dist/modules/sso/setup.js.map +7 -0
  117. package/dist/modules/sso/subscribers/user-deleted-cleanup.js +21 -0
  118. package/dist/modules/sso/subscribers/user-deleted-cleanup.js.map +7 -0
  119. package/dist/modules/sso/widgets/injection/login-sso/widget.client.js +106 -0
  120. package/dist/modules/sso/widgets/injection/login-sso/widget.client.js.map +7 -0
  121. package/dist/modules/sso/widgets/injection/login-sso/widget.js +16 -0
  122. package/dist/modules/sso/widgets/injection/login-sso/widget.js.map +7 -0
  123. package/dist/modules/sso/widgets/injection-table.js +14 -0
  124. package/dist/modules/sso/widgets/injection-table.js.map +7 -0
  125. package/package.json +5 -4
  126. package/src/index.ts +1 -1
  127. package/src/modules/sso/acl.ts +7 -0
  128. package/src/modules/sso/api/admin-context.ts +36 -0
  129. package/src/modules/sso/api/callback/oidc/route.ts +115 -0
  130. package/src/modules/sso/api/config/[id]/activate/route.ts +53 -0
  131. package/src/modules/sso/api/config/[id]/domains/route.ts +107 -0
  132. package/src/modules/sso/api/config/[id]/route.ts +114 -0
  133. package/src/modules/sso/api/config/[id]/test/route.ts +44 -0
  134. package/src/modules/sso/api/config/route.ts +88 -0
  135. package/src/modules/sso/api/error-handler.ts +36 -0
  136. package/src/modules/sso/api/hrd/route.ts +55 -0
  137. package/src/modules/sso/api/initiate/route.ts +70 -0
  138. package/src/modules/sso/api/scim/context.ts +85 -0
  139. package/src/modules/sso/api/scim/logs/route.ts +69 -0
  140. package/src/modules/sso/api/scim/tokens/[id]/route.ts +45 -0
  141. package/src/modules/sso/api/scim/tokens/route.ts +89 -0
  142. package/src/modules/sso/api/scim/v2/ServiceProviderConfig/route.ts +40 -0
  143. package/src/modules/sso/api/scim/v2/Users/[id]/route.ts +103 -0
  144. package/src/modules/sso/api/scim/v2/Users/route.ts +94 -0
  145. package/src/modules/sso/backend/page.meta.ts +29 -0
  146. package/src/modules/sso/backend/page.tsx +232 -0
  147. package/src/modules/sso/backend/sso/config/[id]/page.meta.ts +15 -0
  148. package/src/modules/sso/backend/sso/config/[id]/page.tsx +1024 -0
  149. package/src/modules/sso/backend/sso/config/new/page.meta.ts +15 -0
  150. package/src/modules/sso/backend/sso/config/new/page.tsx +463 -0
  151. package/src/modules/sso/data/entities.ts +240 -0
  152. package/src/modules/sso/data/validators.ts +140 -0
  153. package/src/modules/sso/di.ts +25 -0
  154. package/src/modules/sso/docs/entra-id-setup.md +281 -0
  155. package/src/modules/sso/docs/google-workspace-setup.md +174 -0
  156. package/src/modules/sso/docs/sso-overview.md +218 -0
  157. package/src/modules/sso/docs/sso-security-audit-2026-02-27.md +118 -0
  158. package/src/modules/sso/docs/zitadel-setup.md +195 -0
  159. package/src/modules/sso/events.ts +21 -0
  160. package/src/modules/sso/i18n/de.json +146 -0
  161. package/src/modules/sso/i18n/en.json +146 -0
  162. package/src/modules/sso/i18n/es.json +146 -0
  163. package/src/modules/sso/i18n/pl.json +146 -0
  164. package/src/modules/sso/index.ts +7 -0
  165. package/src/modules/sso/lib/domains.ts +31 -0
  166. package/src/modules/sso/lib/oidc-provider.ts +196 -0
  167. package/src/modules/sso/lib/registry.ts +13 -0
  168. package/src/modules/sso/lib/scim-filter.ts +62 -0
  169. package/src/modules/sso/lib/scim-mapper.ts +88 -0
  170. package/src/modules/sso/lib/scim-patch.ts +88 -0
  171. package/src/modules/sso/lib/scim-response.ts +40 -0
  172. package/src/modules/sso/lib/scim-utils.ts +5 -0
  173. package/src/modules/sso/lib/state-cookie.ts +79 -0
  174. package/src/modules/sso/lib/types.ts +50 -0
  175. package/src/modules/sso/migrations/.snapshot-open-mercato.json +912 -0
  176. package/src/modules/sso/migrations/Migration20260219000000_sso.ts +21 -0
  177. package/src/modules/sso/migrations/Migration20260222000000_sso_add_name.ts +13 -0
  178. package/src/modules/sso/migrations/Migration20260222000001_sso_partial_unique_org.ts +15 -0
  179. package/src/modules/sso/migrations/Migration20260223000000_scim_tables.ts +24 -0
  180. package/src/modules/sso/migrations/Migration20260224000000_sso_external_id.ts +15 -0
  181. package/src/modules/sso/migrations/Migration20260224100000_sso_role_grants.ts +18 -0
  182. package/src/modules/sso/migrations/Migration20260224200000_drop_default_role_id.ts +13 -0
  183. package/src/modules/sso/migrations/Migration20260225000000_sso_identities_partial_unique.ts +25 -0
  184. package/src/modules/sso/migrations/Migration20260305000000_sso_role_grants_org_id.ts +14 -0
  185. package/src/modules/sso/services/accountLinkingService.ts +386 -0
  186. package/src/modules/sso/services/hrdService.ts +22 -0
  187. package/src/modules/sso/services/scimService.ts +461 -0
  188. package/src/modules/sso/services/scimTokenService.ts +136 -0
  189. package/src/modules/sso/services/ssoConfigService.ts +337 -0
  190. package/src/modules/sso/services/ssoService.ts +167 -0
  191. package/src/modules/sso/setup.ts +56 -0
  192. package/src/modules/sso/subscribers/user-deleted-cleanup.ts +33 -0
  193. package/src/modules/sso/widgets/injection/login-sso/widget.client.tsx +130 -0
  194. package/src/modules/sso/widgets/injection/login-sso/widget.ts +16 -0
  195. package/src/modules/sso/widgets/injection-table.ts +12 -0
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../src/modules/sso/widgets/injection/login-sso/widget.client.tsx"],
4
+ "sourcesContent": ["\"use client\"\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport type { InjectionWidgetComponentProps } from '@open-mercato/shared/modules/widgets/injection'\nimport type { LoginFormWidgetContext } from '@open-mercato/core/modules/auth/frontend/login-injection'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'\n\nconst SSO_ERROR_CODES = [\n 'sso_failed',\n 'sso_missing_config',\n 'sso_email_not_verified',\n 'sso_state_missing',\n 'sso_idp_error',\n 'sso_missing_params',\n] as const\n\nconst HRD_DEBOUNCE_MS = 300\n\ntype HrdResponse = {\n hasSso: boolean\n configId?: string\n protocol?: string\n}\n\nexport default function SsoLoginWidget({ context }: InjectionWidgetComponentProps<LoginFormWidgetContext>) {\n const t = useT()\n const translate = useCallback(\n (key: string, fallback: string) => translateWithFallback(t, key, fallback),\n [t],\n )\n const [ssoActive, setSsoActive] = useState(false)\n const lastCheckedEmail = useRef('')\n const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null)\n\n useEffect(() => {\n const errorParam = context.searchParams.get('error')\n if (!errorParam) return\n\n const errorMessages: Record<string, string> = {\n sso_failed: translate('sso.login.errors.failed', 'SSO login failed. Please try again.'),\n sso_missing_config: translate('sso.login.errors.missingConfig', 'SSO is not configured for this account.'),\n sso_email_not_verified: translate('sso.login.errors.emailNotVerified', 'Your email address is not verified by the identity provider. Please verify your email and try again.'),\n sso_state_missing: translate('sso.login.errors.stateMissing', 'SSO session expired. Please try again.'),\n sso_idp_error: translate('sso.login.errors.idpError', 'The identity provider returned an error. Please try again or contact your administrator.'),\n sso_missing_params: translate('sso.login.errors.missingParams', 'SSO callback was incomplete. Please try again.'),\n }\n\n if (SSO_ERROR_CODES.includes(errorParam as typeof SSO_ERROR_CODES[number])) {\n context.setError(errorMessages[errorParam] ?? errorMessages.sso_failed)\n }\n }, [context.searchParams, context.setError, translate])\n\n const checkHrd = useCallback(async (email: string) => {\n if (!email || !email.includes('@')) {\n context.setAuthOverride(null)\n setSsoActive(false)\n lastCheckedEmail.current = ''\n return\n }\n\n if (email === lastCheckedEmail.current) return\n lastCheckedEmail.current = email\n\n try {\n const body: Record<string, string> = { email }\n if (context.tenantId) {\n body.tenantId = context.tenantId\n }\n\n const res = await apiCall<HrdResponse>(\n '/api/sso/hrd',\n { method: 'POST', body: JSON.stringify(body), headers: { 'Content-Type': 'application/json' } },\n )\n\n if (res.result?.hasSso && res.result.configId) {\n const configId = res.result.configId\n setSsoActive(true)\n context.setAuthOverride({\n providerId: 'sso',\n providerLabel: translate('sso.login.continueWithSso', 'Continue with SSO'),\n onSubmit: () => {\n const returnUrl = context.searchParams.get('returnUrl') || '/backend'\n window.location.href = `/api/sso/initiate?configId=${encodeURIComponent(configId)}&returnUrl=${encodeURIComponent(returnUrl)}`\n },\n hidePassword: true,\n hideRememberMe: true,\n hideForgotPassword: true,\n })\n } else {\n setSsoActive(false)\n context.setAuthOverride(null)\n }\n } catch {\n setSsoActive(false)\n context.setAuthOverride(null)\n }\n }, [context, translate])\n\n useEffect(() => {\n if (debounceTimer.current) {\n clearTimeout(debounceTimer.current)\n }\n\n if (!context.email) {\n setSsoActive(false)\n context.setAuthOverride(null)\n lastCheckedEmail.current = ''\n return\n }\n\n debounceTimer.current = setTimeout(() => {\n checkHrd(context.email)\n }, HRD_DEBOUNCE_MS)\n\n return () => {\n if (debounceTimer.current) {\n clearTimeout(debounceTimer.current)\n }\n }\n }, [context.email, checkHrd])\n\n if (!ssoActive) return null\n\n return (\n <div className=\"rounded-md border border-blue-200 bg-blue-50 px-3 py-2 text-center text-xs text-blue-800\">\n {translate('sso.login.ssoEnabled', 'SSO is enabled for this account')}\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,16 @@
1
+ import Widget from "./widget.client.js";
2
+ const widgetModule = {
3
+ metadata: {
4
+ id: "sso.injection.login-sso",
5
+ title: "SSO Login",
6
+ features: [],
7
+ priority: 100,
8
+ enabled: true
9
+ },
10
+ Widget
11
+ };
12
+ var widget_default = widgetModule;
13
+ export {
14
+ widget_default as default
15
+ };
16
+ //# sourceMappingURL=widget.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../src/modules/sso/widgets/injection/login-sso/widget.ts"],
4
+ "sourcesContent": ["import type { InjectionWidgetModule } from '@open-mercato/shared/modules/widgets/injection'\nimport type { LoginFormWidgetContext } from '@open-mercato/core/modules/auth/frontend/login-injection'\nimport Widget from './widget.client'\n\nconst widgetModule: InjectionWidgetModule<LoginFormWidgetContext> = {\n metadata: {\n id: 'sso.injection.login-sso',\n title: 'SSO Login',\n features: [],\n priority: 100,\n enabled: true,\n },\n Widget,\n}\n\nexport default widgetModule\n"],
5
+ "mappings": "AAEA,OAAO,YAAY;AAEnB,MAAM,eAA8D;AAAA,EAClE,UAAU;AAAA,IACR,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,UAAU,CAAC;AAAA,IACX,UAAU;AAAA,IACV,SAAS;AAAA,EACX;AAAA,EACA;AACF;AAEA,IAAO,iBAAQ;",
6
+ "names": []
7
+ }
@@ -0,0 +1,14 @@
1
+ const injectionTable = {
2
+ "auth.login:form": [
3
+ {
4
+ widgetId: "sso.injection.login-sso",
5
+ priority: 100
6
+ }
7
+ ]
8
+ };
9
+ var injection_table_default = injectionTable;
10
+ export {
11
+ injection_table_default as default,
12
+ injectionTable
13
+ };
14
+ //# sourceMappingURL=injection-table.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/sso/widgets/injection-table.ts"],
4
+ "sourcesContent": ["import type { ModuleInjectionTable } from '@open-mercato/shared/modules/widgets/injection'\n\nexport const injectionTable: ModuleInjectionTable = {\n 'auth.login:form': [\n {\n widgetId: 'sso.injection.login-sso',\n priority: 100,\n },\n ],\n}\n\nexport default injectionTable\n"],
5
+ "mappings": "AAEO,MAAM,iBAAuC;AAAA,EAClD,mBAAmB;AAAA,IACjB;AAAA,MACE,UAAU;AAAA,MACV,UAAU;AAAA,IACZ;AAAA,EACF;AACF;AAEA,IAAO,0BAAQ;",
6
+ "names": []
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/enterprise",
3
- "version": "0.4.6-develop-15c18897fc",
3
+ "version": "0.4.6-develop-34aa847ce6",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -64,9 +64,10 @@
64
64
  }
65
65
  },
66
66
  "dependencies": {
67
- "@open-mercato/core": "0.4.6-develop-15c18897fc",
68
- "@open-mercato/shared": "0.4.6-develop-15c18897fc",
69
- "@open-mercato/ui": "0.4.6-develop-15c18897fc"
67
+ "@open-mercato/core": "0.4.6-develop-34aa847ce6",
68
+ "@open-mercato/shared": "0.4.6-develop-34aa847ce6",
69
+ "@open-mercato/ui": "0.4.6-develop-34aa847ce6",
70
+ "openid-client": "^6.3.3"
70
71
  },
71
72
  "devDependencies": {
72
73
  "@types/jest": "^30.0.0",
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export const enterprisePackage = {
2
2
  id: 'enterprise',
3
3
  description: 'Optional enterprise overlays and modules for Open Mercato.',
4
- modules: ['security', 'record_locks'],
4
+ modules: ['security', 'sso','record_locks'],
5
5
  } as const
6
6
 
7
7
  export default enterprisePackage
@@ -0,0 +1,7 @@
1
+ export const features = [
2
+ { id: 'sso.config.view', title: 'View SSO configuration', module: 'sso' },
3
+ { id: 'sso.config.manage', title: 'Manage SSO configuration', module: 'sso' },
4
+ { id: 'sso.scim.manage', title: 'Manage SCIM provisioning tokens', module: 'sso' },
5
+ ]
6
+
7
+ export default features
@@ -0,0 +1,36 @@
1
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
2
+ import type { SsoAdminScope } from '../services/ssoConfigService'
3
+
4
+ export async function resolveSsoAdminContext(req: Request): Promise<{
5
+ auth: Awaited<ReturnType<typeof getAuthFromRequest>>
6
+ scope: SsoAdminScope
7
+ }> {
8
+ const auth = await getAuthFromRequest(req)
9
+ if (!auth?.sub) throw new SsoAdminAuthError('Unauthorized', 401)
10
+
11
+ const isSuperAdmin = !!(auth as Record<string, unknown>).isSuperAdmin
12
+ const url = new URL(req.url)
13
+
14
+ return {
15
+ auth,
16
+ scope: {
17
+ isSuperAdmin,
18
+ organizationId: isSuperAdmin
19
+ ? url.searchParams.get('organizationId') ?? null
20
+ : auth.orgId ?? null,
21
+ tenantId: isSuperAdmin
22
+ ? url.searchParams.get('tenantId') ?? null
23
+ : auth.tenantId ?? null,
24
+ },
25
+ }
26
+ }
27
+
28
+ export class SsoAdminAuthError extends Error {
29
+ constructor(
30
+ message: string,
31
+ public readonly statusCode: number,
32
+ ) {
33
+ super(message)
34
+ this.name = 'SsoAdminAuthError'
35
+ }
36
+ }
@@ -0,0 +1,115 @@
1
+ import { NextResponse } from 'next/server'
2
+ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
3
+ import { toAbsoluteUrl } from '@open-mercato/shared/lib/url'
4
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
5
+ import { SsoService } from '../../../services/ssoService'
6
+ import { emitSsoEvent } from '../../../events'
7
+
8
+ function parseCookie(req: Request, name: string): string | null {
9
+ const cookie = req.headers.get('cookie') || ''
10
+ const m = cookie.match(new RegExp('(?:^|;\\s*)' + name + '=([^;]+)'))
11
+ return m ? decodeURIComponent(m[1]) : null
12
+ }
13
+
14
+ async function handleCallback(req: Request): Promise<NextResponse> {
15
+ try {
16
+ const url = new URL(req.url)
17
+ const callbackParams: Record<string, string> = {}
18
+ url.searchParams.forEach((value, key) => {
19
+ callbackParams[key] = value
20
+ })
21
+
22
+ if (req.method === 'POST') {
23
+ const contentType = req.headers.get('content-type') || ''
24
+ if (contentType.includes('application/x-www-form-urlencoded')) {
25
+ const form = await req.formData()
26
+ form.forEach((value, key) => {
27
+ callbackParams[key] = String(value)
28
+ })
29
+ }
30
+ }
31
+
32
+ const stateCookie = parseCookie(req, 'sso_state')
33
+ if (!stateCookie) {
34
+ return NextResponse.redirect(toAbsoluteUrl(req, '/login?error=sso_state_missing'))
35
+ }
36
+
37
+ if (callbackParams.error) {
38
+ void emitSsoEvent('sso.login.failed', {
39
+ reason: callbackParams.error,
40
+ }).catch((e) => console.error('[SSO Event]', e))
41
+ return NextResponse.redirect(toAbsoluteUrl(req, '/login?error=sso_idp_error'))
42
+ }
43
+
44
+ if (!callbackParams.code || !callbackParams.state) {
45
+ return NextResponse.redirect(toAbsoluteUrl(req, '/login?error=sso_missing_params'))
46
+ }
47
+
48
+ const redirectUri = toAbsoluteUrl(req, '/api/sso/callback/oidc')
49
+ const container = await createRequestContainer()
50
+ const ssoService = container.resolve<SsoService>('ssoService')
51
+
52
+ const result = await ssoService.handleOidcCallback(callbackParams, stateCookie, redirectUri)
53
+
54
+ const res = NextResponse.redirect(toAbsoluteUrl(req, result.redirectUrl))
55
+
56
+ res.cookies.set('auth_token', result.token, {
57
+ httpOnly: true,
58
+ path: '/',
59
+ sameSite: 'lax',
60
+ secure: process.env.NODE_ENV !== 'development',
61
+ maxAge: 60 * 60 * 8,
62
+ })
63
+
64
+ res.cookies.set('session_token', result.sessionToken, {
65
+ httpOnly: true,
66
+ path: '/',
67
+ sameSite: 'lax',
68
+ secure: process.env.NODE_ENV !== 'development',
69
+ expires: result.sessionExpiresAt,
70
+ })
71
+
72
+ res.cookies.set('sso_state', '', { path: '/', maxAge: 0 })
73
+
74
+ return res
75
+ } catch (err) {
76
+ console.error('[SSO Callback] Error:', err)
77
+ void emitSsoEvent('sso.login.failed', {
78
+ reason: err instanceof Error ? err.message : 'callback_failed',
79
+ }).catch((e) => console.error('[SSO Event]', e))
80
+ const message = err instanceof Error ? err.message : ''
81
+ const errorCode = message.includes('email is not verified') ? 'sso_email_not_verified' : 'sso_failed'
82
+ return NextResponse.redirect(toAbsoluteUrl(req, `/login?error=${errorCode}`))
83
+ }
84
+ }
85
+
86
+ export async function GET(req: Request) {
87
+ return handleCallback(req)
88
+ }
89
+
90
+ export async function POST(req: Request) {
91
+ return handleCallback(req)
92
+ }
93
+
94
+ export const openApi: OpenApiRouteDoc = {
95
+ tag: 'SSO',
96
+ summary: 'OIDC callback',
97
+ methods: {
98
+ GET: {
99
+ summary: 'Handle OIDC callback (GET)',
100
+ description: 'Receives the authorization code from the IdP, exchanges it for tokens, resolves the user, and issues auth cookies.',
101
+ tags: ['SSO'],
102
+ responses: [
103
+ { status: 302, description: 'Redirect to application with auth cookies set', mediaType: 'text/html' },
104
+ ],
105
+ },
106
+ POST: {
107
+ summary: 'Handle OIDC callback (POST)',
108
+ description: 'Some IdPs send the callback as a POST (form_post response mode). Handles the same flow as the GET variant.',
109
+ tags: ['SSO'],
110
+ responses: [
111
+ { status: 302, description: 'Redirect to application with auth cookies set', mediaType: 'text/html' },
112
+ ],
113
+ },
114
+ },
115
+ }
@@ -0,0 +1,53 @@
1
+ import { NextResponse } from 'next/server'
2
+ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
3
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
4
+ import { SsoConfigService } from '../../../../services/ssoConfigService'
5
+ import { ssoActivateSchema } from '../../../../data/validators'
6
+ import { resolveSsoAdminContext } from '../../../admin-context'
7
+ import { handleSsoAdminApiError } from '../../../error-handler'
8
+
9
+ type RouteContext = { params: Promise<{ id: string }> }
10
+
11
+ export const metadata = {
12
+ POST: { requireAuth: true, requireFeatures: ['sso.config.manage'] },
13
+ }
14
+
15
+ export async function POST(req: Request, ctx: RouteContext) {
16
+ try {
17
+ const { id } = await ctx.params
18
+ const { scope } = await resolveSsoAdminContext(req)
19
+
20
+ const body = await req.json()
21
+ const parsed = ssoActivateSchema.safeParse(body)
22
+ if (!parsed.success) {
23
+ return NextResponse.json({ error: 'Invalid request' }, { status: 400 })
24
+ }
25
+
26
+ const container = await createRequestContainer()
27
+ const service = container.resolve<SsoConfigService>('ssoConfigService')
28
+ const config = await service.activate(scope, id, parsed.data.active)
29
+
30
+ return NextResponse.json(config)
31
+ } catch (err) {
32
+ return handleSsoAdminApiError(err, 'SSO Config API')
33
+ }
34
+ }
35
+
36
+ export const openApi: OpenApiRouteDoc = {
37
+ tag: 'SSO',
38
+ summary: 'Activate/Deactivate SSO Configuration',
39
+ methods: {
40
+ POST: {
41
+ summary: 'Activate or deactivate SSO configuration',
42
+ description: 'Activation requires at least one domain and a successful OIDC discovery test.',
43
+ tags: ['SSO'],
44
+ requestBody: { contentType: 'application/json', schema: ssoActivateSchema },
45
+ responses: [{ status: 200, description: 'Config activation status updated' }],
46
+ errors: [
47
+ { status: 400, description: 'Activation failed — no domains or discovery failed' },
48
+ { status: 401, description: 'Unauthorized' },
49
+ { status: 404, description: 'Config not found' },
50
+ ],
51
+ },
52
+ },
53
+ }
@@ -0,0 +1,107 @@
1
+ import { NextResponse } from 'next/server'
2
+ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
3
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
4
+ import { SsoConfigService } from '../../../../services/ssoConfigService'
5
+ import { ssoDomainAddSchema } from '../../../../data/validators'
6
+ import { resolveSsoAdminContext } from '../../../admin-context'
7
+ import { handleSsoAdminApiError } from '../../../error-handler'
8
+
9
+ type RouteContext = { params: Promise<{ id: string }> }
10
+
11
+ export const metadata = {
12
+ GET: { requireAuth: true, requireFeatures: ['sso.config.view'] },
13
+ POST: { requireAuth: true, requireFeatures: ['sso.config.manage'] },
14
+ DELETE: { requireAuth: true, requireFeatures: ['sso.config.manage'] },
15
+ }
16
+
17
+ export async function GET(req: Request, ctx: RouteContext) {
18
+ try {
19
+ const { id } = await ctx.params
20
+ const { scope } = await resolveSsoAdminContext(req)
21
+
22
+ const container = await createRequestContainer()
23
+ const service = container.resolve<SsoConfigService>('ssoConfigService')
24
+ const config = await service.getById(scope, id)
25
+
26
+ if (!config) {
27
+ return NextResponse.json({ error: 'SSO configuration not found' }, { status: 404 })
28
+ }
29
+
30
+ return NextResponse.json({ domains: config.allowedDomains })
31
+ } catch (err) {
32
+ return handleSsoAdminApiError(err, 'SSO Config API')
33
+ }
34
+ }
35
+
36
+ export async function POST(req: Request, ctx: RouteContext) {
37
+ try {
38
+ const { id } = await ctx.params
39
+ const { scope } = await resolveSsoAdminContext(req)
40
+
41
+ const body = await req.json()
42
+ const parsed = ssoDomainAddSchema.safeParse(body)
43
+ if (!parsed.success) {
44
+ return NextResponse.json({ error: 'Invalid request', details: parsed.error.flatten() }, { status: 400 })
45
+ }
46
+
47
+ const container = await createRequestContainer()
48
+ const service = container.resolve<SsoConfigService>('ssoConfigService')
49
+ const config = await service.addDomain(scope, id, parsed.data.domain)
50
+
51
+ return NextResponse.json({ domains: config.allowedDomains })
52
+ } catch (err) {
53
+ return handleSsoAdminApiError(err, 'SSO Config API')
54
+ }
55
+ }
56
+
57
+ export async function DELETE(req: Request, ctx: RouteContext) {
58
+ try {
59
+ const { id } = await ctx.params
60
+ const { scope } = await resolveSsoAdminContext(req)
61
+
62
+ const url = new URL(req.url)
63
+ const domain = url.searchParams.get('domain')
64
+ if (!domain) {
65
+ return NextResponse.json({ error: 'Missing domain query parameter' }, { status: 400 })
66
+ }
67
+
68
+ const container = await createRequestContainer()
69
+ const service = container.resolve<SsoConfigService>('ssoConfigService')
70
+ const config = await service.removeDomain(scope, id, domain)
71
+
72
+ return NextResponse.json({ domains: config.allowedDomains })
73
+ } catch (err) {
74
+ return handleSsoAdminApiError(err, 'SSO Config API')
75
+ }
76
+ }
77
+
78
+
79
+ export const openApi: OpenApiRouteDoc = {
80
+ tag: 'SSO',
81
+ summary: 'SSO Domain Management',
82
+ methods: {
83
+ GET: {
84
+ summary: 'List allowed domains',
85
+ tags: ['SSO'],
86
+ responses: [{ status: 200, description: 'List of allowed domains' }],
87
+ errors: [{ status: 404, description: 'Config not found' }],
88
+ },
89
+ POST: {
90
+ summary: 'Add an allowed domain',
91
+ tags: ['SSO'],
92
+ requestBody: { contentType: 'application/json', schema: ssoDomainAddSchema },
93
+ responses: [{ status: 200, description: 'Domain added' }],
94
+ errors: [
95
+ { status: 400, description: 'Invalid domain or limit reached' },
96
+ { status: 404, description: 'Config not found' },
97
+ ],
98
+ },
99
+ DELETE: {
100
+ summary: 'Remove an allowed domain',
101
+ description: 'Pass domain as query parameter: ?domain=example.com',
102
+ tags: ['SSO'],
103
+ responses: [{ status: 200, description: 'Domain removed' }],
104
+ errors: [{ status: 404, description: 'Config not found' }],
105
+ },
106
+ },
107
+ }
@@ -0,0 +1,114 @@
1
+ import { NextResponse } from 'next/server'
2
+ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
3
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
4
+ import { SsoConfigService } from '../../../services/ssoConfigService'
5
+ import { ssoConfigAdminUpdateSchema } from '../../../data/validators'
6
+ import { resolveSsoAdminContext } from '../../admin-context'
7
+ import { handleSsoAdminApiError } from '../../error-handler'
8
+ import { ScimToken } from '../../../data/entities'
9
+ import type { EntityManager } from '@mikro-orm/postgresql'
10
+
11
+ type RouteContext = { params: Promise<{ id: string }> }
12
+
13
+ export const metadata = {
14
+ GET: { requireAuth: true, requireFeatures: ['sso.config.view'] },
15
+ PUT: { requireAuth: true, requireFeatures: ['sso.config.manage'] },
16
+ DELETE: { requireAuth: true, requireFeatures: ['sso.config.manage'] },
17
+ }
18
+
19
+ export async function GET(req: Request, ctx: RouteContext) {
20
+ try {
21
+ const { id } = await ctx.params
22
+ const { scope } = await resolveSsoAdminContext(req)
23
+
24
+ const container = await createRequestContainer()
25
+ const service = container.resolve<SsoConfigService>('ssoConfigService')
26
+ const config = await service.getById(scope, id)
27
+
28
+ if (!config) {
29
+ return NextResponse.json({ error: 'SSO configuration not found' }, { status: 404 })
30
+ }
31
+
32
+ const em = container.resolve<EntityManager>('em')
33
+ const activeScimCount = await em.count(ScimToken, { ssoConfigId: id, isActive: true })
34
+
35
+ return NextResponse.json({ ...config, hasActiveScimTokens: activeScimCount > 0 })
36
+ } catch (err) {
37
+ return handleSsoAdminApiError(err, 'SSO Config API')
38
+ }
39
+ }
40
+
41
+ export async function PUT(req: Request, ctx: RouteContext) {
42
+ try {
43
+ const { id } = await ctx.params
44
+ const { scope } = await resolveSsoAdminContext(req)
45
+
46
+ const body = await req.json()
47
+ const parsed = ssoConfigAdminUpdateSchema.safeParse(body)
48
+ if (!parsed.success) {
49
+ return NextResponse.json({ error: 'Invalid request', details: parsed.error.flatten() }, { status: 400 })
50
+ }
51
+
52
+ const container = await createRequestContainer()
53
+ const service = container.resolve<SsoConfigService>('ssoConfigService')
54
+ const config = await service.update(scope, id, parsed.data)
55
+
56
+ return NextResponse.json(config)
57
+ } catch (err) {
58
+ return handleSsoAdminApiError(err, 'SSO Config API')
59
+ }
60
+ }
61
+
62
+ export async function DELETE(req: Request, ctx: RouteContext) {
63
+ try {
64
+ const { id } = await ctx.params
65
+ const { scope } = await resolveSsoAdminContext(req)
66
+
67
+ const container = await createRequestContainer()
68
+ const service = container.resolve<SsoConfigService>('ssoConfigService')
69
+ await service.delete(scope, id)
70
+
71
+ return NextResponse.json({ ok: true })
72
+ } catch (err) {
73
+ return handleSsoAdminApiError(err, 'SSO Config API')
74
+ }
75
+ }
76
+
77
+ export const openApi: OpenApiRouteDoc = {
78
+ tag: 'SSO',
79
+ summary: 'SSO Configuration Detail',
80
+ methods: {
81
+ GET: {
82
+ summary: 'Get SSO configuration by ID',
83
+ tags: ['SSO'],
84
+ responses: [{ status: 200, description: 'SSO config detail' }],
85
+ errors: [
86
+ { status: 401, description: 'Unauthorized' },
87
+ { status: 404, description: 'Config not found' },
88
+ ],
89
+ },
90
+ PUT: {
91
+ summary: 'Update SSO configuration',
92
+ tags: ['SSO'],
93
+ requestBody: { contentType: 'application/json', schema: ssoConfigAdminUpdateSchema },
94
+ responses: [{ status: 200, description: 'SSO config updated' }],
95
+ errors: [
96
+ { status: 400, description: 'Invalid input' },
97
+ { status: 401, description: 'Unauthorized' },
98
+ { status: 404, description: 'Config not found' },
99
+ { status: 409, description: 'Conflict — JIT and SCIM are mutually exclusive' },
100
+ ],
101
+ },
102
+ DELETE: {
103
+ summary: 'Delete SSO configuration',
104
+ description: 'Soft-deletes the config. Must be deactivated first.',
105
+ tags: ['SSO'],
106
+ responses: [{ status: 200, description: 'Config deleted' }],
107
+ errors: [
108
+ { status: 400, description: 'Cannot delete active config' },
109
+ { status: 401, description: 'Unauthorized' },
110
+ { status: 404, description: 'Config not found' },
111
+ ],
112
+ },
113
+ },
114
+ }
@@ -0,0 +1,44 @@
1
+ import { NextResponse } from 'next/server'
2
+ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
3
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
4
+ import { SsoConfigService } from '../../../../services/ssoConfigService'
5
+ import { resolveSsoAdminContext } from '../../../admin-context'
6
+ import { handleSsoAdminApiError } from '../../../error-handler'
7
+
8
+ type RouteContext = { params: Promise<{ id: string }> }
9
+
10
+ export const metadata = {
11
+ POST: { requireAuth: true, requireFeatures: ['sso.config.manage'] },
12
+ }
13
+
14
+ export async function POST(req: Request, ctx: RouteContext) {
15
+ try {
16
+ const { id } = await ctx.params
17
+ const { scope } = await resolveSsoAdminContext(req)
18
+
19
+ const container = await createRequestContainer()
20
+ const service = container.resolve<SsoConfigService>('ssoConfigService')
21
+ const result = await service.testConnection(scope, id)
22
+
23
+ return NextResponse.json(result)
24
+ } catch (err) {
25
+ return handleSsoAdminApiError(err, 'SSO Config API')
26
+ }
27
+ }
28
+
29
+ export const openApi: OpenApiRouteDoc = {
30
+ tag: 'SSO',
31
+ summary: 'Test SSO Connection',
32
+ methods: {
33
+ POST: {
34
+ summary: 'Test OIDC discovery',
35
+ description: 'Verifies that the issuer URL is reachable and returns a valid OIDC discovery document. Does not verify client credentials.',
36
+ tags: ['SSO'],
37
+ responses: [{ status: 200, description: 'Test result with ok/error' }],
38
+ errors: [
39
+ { status: 401, description: 'Unauthorized' },
40
+ { status: 404, description: 'Config not found' },
41
+ ],
42
+ },
43
+ },
44
+ }