@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,21 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+
3
+ export class Migration20260219000000_sso extends Migration {
4
+
5
+ override async up(): Promise<void> {
6
+ this.addSql(`create table "sso_configs" ("id" uuid not null default gen_random_uuid(), "tenant_id" uuid null, "organization_id" uuid not null, "protocol" text not null, "issuer" text null, "client_id" text null, "client_secret_enc" text null, "allowed_domains" jsonb not null default '[]', "jit_enabled" boolean not null default true, "auto_link_by_email" boolean not null default true, "is_active" boolean not null default false, "sso_required" boolean not null default false, "default_role_id" uuid null, "created_at" timestamptz not null, "updated_at" timestamptz not null, "deleted_at" timestamptz null, constraint "sso_configs_pkey" primary key ("id"));`);
7
+ this.addSql(`alter table "sso_configs" add constraint "sso_configs_organization_id_unique" unique ("organization_id");`);
8
+
9
+ this.addSql(`create table "sso_identities" ("id" uuid not null default gen_random_uuid(), "tenant_id" uuid null, "organization_id" uuid not null, "sso_config_id" uuid not null, "user_id" uuid not null, "idp_subject" text not null, "idp_email" text not null, "idp_name" text null, "idp_groups" jsonb not null default '[]', "provisioning_method" text not null, "first_login_at" timestamptz null, "last_login_at" timestamptz null, "created_at" timestamptz not null, "updated_at" timestamptz not null, "deleted_at" timestamptz null, constraint "sso_identities_pkey" primary key ("id"));`);
10
+ this.addSql(`alter table "sso_identities" add constraint "sso_identities_config_user_unique" unique ("sso_config_id", "user_id");`);
11
+ this.addSql(`alter table "sso_identities" add constraint "sso_identities_config_subject_unique" unique ("sso_config_id", "idp_subject");`);
12
+ this.addSql(`create index "sso_identities_config_id_idx" on "sso_identities" ("sso_config_id");`);
13
+ this.addSql(`create index "sso_identities_user_id_idx" on "sso_identities" ("user_id");`);
14
+ }
15
+
16
+ override async down(): Promise<void> {
17
+ this.addSql(`drop table if exists "sso_configs" cascade;`);
18
+ this.addSql(`drop table if exists "sso_identities" cascade;`);
19
+ }
20
+
21
+ }
@@ -0,0 +1,13 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+
3
+ export class Migration20260222000000_sso_add_name extends Migration {
4
+
5
+ override async up(): Promise<void> {
6
+ this.addSql(`alter table "sso_configs" add column "name" text null;`);
7
+ }
8
+
9
+ override async down(): Promise<void> {
10
+ this.addSql(`alter table "sso_configs" drop column "name";`);
11
+ }
12
+
13
+ }
@@ -0,0 +1,15 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+
3
+ export class Migration20260222000001_sso_partial_unique_org extends Migration {
4
+
5
+ override async up(): Promise<void> {
6
+ this.addSql(`alter table "sso_configs" drop constraint "sso_configs_organization_id_unique";`);
7
+ this.addSql(`create unique index "sso_configs_organization_id_unique" on "sso_configs" ("organization_id") where "deleted_at" is null;`);
8
+ }
9
+
10
+ override async down(): Promise<void> {
11
+ this.addSql(`drop index "sso_configs_organization_id_unique";`);
12
+ this.addSql(`alter table "sso_configs" add constraint "sso_configs_organization_id_unique" unique ("organization_id");`);
13
+ }
14
+
15
+ }
@@ -0,0 +1,24 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+
3
+ export class Migration20260223000000_scim_tables extends Migration {
4
+
5
+ override async up(): Promise<void> {
6
+ this.addSql(`create table "scim_tokens" ("id" uuid not null default gen_random_uuid(), "tenant_id" uuid null, "organization_id" uuid not null, "sso_config_id" uuid not null, "name" text not null, "token_hash" text not null, "token_prefix" text not null, "is_active" boolean not null default true, "created_by" uuid null, "created_at" timestamptz not null, "updated_at" timestamptz not null, constraint "scim_tokens_pkey" primary key ("id"));`);
7
+ this.addSql(`create index "scim_tokens_sso_config_id_idx" on "scim_tokens" ("sso_config_id");`);
8
+ this.addSql(`create index "scim_tokens_token_prefix_idx" on "scim_tokens" ("token_prefix");`);
9
+
10
+ this.addSql(`create table "sso_user_deactivations" ("id" uuid not null default gen_random_uuid(), "tenant_id" uuid null, "organization_id" uuid not null, "user_id" uuid not null, "sso_config_id" uuid not null, "deactivated_at" timestamptz not null, "reactivated_at" timestamptz null, "created_at" timestamptz not null, constraint "sso_user_deactivations_pkey" primary key ("id"));`);
11
+ this.addSql(`create index "sso_user_deactivations_user_id_idx" on "sso_user_deactivations" ("user_id");`);
12
+ this.addSql(`alter table "sso_user_deactivations" add constraint "sso_user_deactivations_user_config_unique" unique ("user_id", "sso_config_id");`);
13
+
14
+ this.addSql(`create table "scim_provisioning_log" ("id" uuid not null default gen_random_uuid(), "tenant_id" uuid null, "organization_id" uuid not null, "sso_config_id" uuid not null, "operation" text not null, "resource_type" text not null, "resource_id" uuid null, "scim_external_id" text null, "response_status" int not null, "error_message" text null, "created_at" timestamptz not null, constraint "scim_provisioning_log_pkey" primary key ("id"));`);
15
+ this.addSql(`create index "scim_provisioning_log_config_created_idx" on "scim_provisioning_log" ("sso_config_id", "created_at");`);
16
+ }
17
+
18
+ override async down(): Promise<void> {
19
+ this.addSql(`drop table if exists "scim_provisioning_log" cascade;`);
20
+ this.addSql(`drop table if exists "sso_user_deactivations" cascade;`);
21
+ this.addSql(`drop table if exists "scim_tokens" cascade;`);
22
+ }
23
+
24
+ }
@@ -0,0 +1,15 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+
3
+ export class Migration20260224000000_sso_external_id extends Migration {
4
+
5
+ override async up(): Promise<void> {
6
+ this.addSql(`alter table "sso_identities" add column "external_id" text null;`);
7
+ this.addSql(`alter table "sso_identities" add constraint "sso_identities_config_external_id_unique" unique ("sso_config_id", "external_id");`);
8
+ }
9
+
10
+ override async down(): Promise<void> {
11
+ this.addSql(`alter table "sso_identities" drop constraint if exists "sso_identities_config_external_id_unique";`);
12
+ this.addSql(`alter table "sso_identities" drop column if exists "external_id";`);
13
+ }
14
+
15
+ }
@@ -0,0 +1,18 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+
3
+ export class Migration20260224100000_sso_role_grants extends Migration {
4
+
5
+ override async up(): Promise<void> {
6
+ this.addSql(`create table "sso_role_grants" ("id" uuid not null default gen_random_uuid(), "tenant_id" uuid null, "user_id" uuid not null, "role_id" uuid not null, "sso_config_id" uuid not null, "created_at" timestamptz not null, constraint "sso_role_grants_pkey" primary key ("id"));`);
7
+ this.addSql(`create index "sso_role_grants_user_id_idx" on "sso_role_grants" ("user_id");`);
8
+ this.addSql(`alter table "sso_role_grants" add constraint "sso_role_grants_user_role_config_unique" unique ("user_id", "role_id", "sso_config_id");`);
9
+
10
+ this.addSql(`alter table "sso_configs" add column "app_role_mappings" jsonb not null default '{}';`);
11
+ }
12
+
13
+ override async down(): Promise<void> {
14
+ this.addSql(`drop table if exists "sso_role_grants";`);
15
+ this.addSql(`alter table "sso_configs" drop column "app_role_mappings";`);
16
+ }
17
+
18
+ }
@@ -0,0 +1,13 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+
3
+ export class Migration20260224200000_drop_default_role_id extends Migration {
4
+
5
+ override async up(): Promise<void> {
6
+ this.addSql(`alter table "sso_configs" drop column if exists "default_role_id";`);
7
+ }
8
+
9
+ override async down(): Promise<void> {
10
+ this.addSql(`alter table "sso_configs" add column "default_role_id" uuid null;`);
11
+ }
12
+
13
+ }
@@ -0,0 +1,25 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+
3
+ export class Migration20260225000000_sso_identities_partial_unique extends Migration {
4
+
5
+ override async up(): Promise<void> {
6
+ this.addSql(`alter table "sso_identities" drop constraint "sso_identities_config_user_unique";`);
7
+ this.addSql(`alter table "sso_identities" drop constraint "sso_identities_config_subject_unique";`);
8
+ this.addSql(`alter table "sso_identities" drop constraint "sso_identities_config_external_id_unique";`);
9
+
10
+ this.addSql(`create unique index "sso_identities_config_user_unique" on "sso_identities" ("sso_config_id", "user_id") where "deleted_at" is null;`);
11
+ this.addSql(`create unique index "sso_identities_config_subject_unique" on "sso_identities" ("sso_config_id", "idp_subject") where "deleted_at" is null;`);
12
+ this.addSql(`create unique index "sso_identities_config_external_id_unique" on "sso_identities" ("sso_config_id", "external_id") where "deleted_at" is null;`);
13
+ }
14
+
15
+ override async down(): Promise<void> {
16
+ this.addSql(`drop index "sso_identities_config_user_unique";`);
17
+ this.addSql(`drop index "sso_identities_config_subject_unique";`);
18
+ this.addSql(`drop index "sso_identities_config_external_id_unique";`);
19
+
20
+ this.addSql(`alter table "sso_identities" add constraint "sso_identities_config_user_unique" unique ("sso_config_id", "user_id");`);
21
+ this.addSql(`alter table "sso_identities" add constraint "sso_identities_config_subject_unique" unique ("sso_config_id", "idp_subject");`);
22
+ this.addSql(`alter table "sso_identities" add constraint "sso_identities_config_external_id_unique" unique ("sso_config_id", "external_id");`);
23
+ }
24
+
25
+ }
@@ -0,0 +1,14 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+
3
+ export class Migration20260305000000_sso_role_grants_org_id extends Migration {
4
+
5
+ override async up(): Promise<void> {
6
+ this.addSql(`alter table "sso_role_grants" add column "organization_id" uuid not null default '00000000-0000-0000-0000-000000000000';`);
7
+ this.addSql(`alter table "sso_role_grants" alter column "organization_id" drop default;`);
8
+ }
9
+
10
+ override async down(): Promise<void> {
11
+ this.addSql(`alter table "sso_role_grants" drop column "organization_id";`);
12
+ }
13
+
14
+ }
@@ -0,0 +1,386 @@
1
+ import { EntityManager, type FilterQuery, type RequiredEntityData } from '@mikro-orm/postgresql'
2
+ import { User, UserRole, Role } from '@open-mercato/core/modules/auth/data/entities'
3
+ import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
4
+ import { computeEmailHash } from '@open-mercato/core/modules/auth/lib/emailHash'
5
+ import { SsoConfig, SsoIdentity, SsoRoleGrant, ScimToken } from '../data/entities'
6
+ import { emitSsoEvent } from '../events'
7
+ import type { SsoIdentityPayload } from '../lib/types'
8
+
9
+ export class AccountLinkingService {
10
+ constructor(private em: EntityManager) {}
11
+
12
+ async resolveUser(
13
+ config: SsoConfig,
14
+ idpPayload: SsoIdentityPayload,
15
+ tenantId: string,
16
+ ): Promise<{ user: User; identity: SsoIdentity }> {
17
+ const existing = await this.findExistingLink(config.id, idpPayload.subject, tenantId, config.organizationId)
18
+ if (existing) {
19
+ await this.assignRolesFromSso(this.em, existing.user, config, tenantId, idpPayload.groups)
20
+ return existing
21
+ }
22
+
23
+ if (idpPayload.emailVerified === false) {
24
+ throw new Error('IdP explicitly reported email as unverified — cannot link or provision account')
25
+ }
26
+
27
+ const emailDomain = idpPayload.email.split('@')[1]?.toLowerCase()
28
+ if (!emailDomain || !config.allowedDomains.some((d) => d.toLowerCase() === emailDomain)) {
29
+ throw new Error('Email domain is not in the allowed domains for this SSO configuration')
30
+ }
31
+
32
+ const emailLinked = config.autoLinkByEmail
33
+ ? await this.linkByEmail(config, idpPayload, tenantId)
34
+ : null
35
+ if (emailLinked) {
36
+ await this.assignRolesFromSso(this.em, emailLinked.user, config, tenantId, idpPayload.groups)
37
+ return emailLinked
38
+ }
39
+
40
+ if (config.jitEnabled) {
41
+ const scimActive = await this.em.count(ScimToken, { ssoConfigId: config.id, isActive: true }) > 0
42
+ if (scimActive) {
43
+ throw new Error('JIT provisioning is disabled because SCIM directory sync is active')
44
+ }
45
+ return this.jitProvision(config, idpPayload, tenantId)
46
+ }
47
+
48
+ throw new Error('No matching user found and JIT provisioning is disabled')
49
+ }
50
+
51
+ private async findExistingLink(
52
+ ssoConfigId: string,
53
+ idpSubject: string,
54
+ tenantId: string,
55
+ organizationId: string,
56
+ ): Promise<{ user: User; identity: SsoIdentity } | null> {
57
+ const identity = await findOneWithDecryption(
58
+ this.em,
59
+ SsoIdentity,
60
+ { ssoConfigId, idpSubject, deletedAt: null },
61
+ {},
62
+ { tenantId, organizationId },
63
+ )
64
+ if (!identity) return null
65
+
66
+ const user = await findOneWithDecryption(
67
+ this.em,
68
+ User,
69
+ { id: identity.userId, deletedAt: null },
70
+ {},
71
+ { tenantId, organizationId },
72
+ )
73
+ if (!user) {
74
+ identity.deletedAt = new Date()
75
+ await this.em.flush()
76
+ return null
77
+ }
78
+
79
+ identity.lastLoginAt = new Date()
80
+ await this.em.flush()
81
+
82
+ return { user, identity }
83
+ }
84
+
85
+ private async linkByEmail(
86
+ config: SsoConfig,
87
+ idpPayload: SsoIdentityPayload,
88
+ tenantId: string,
89
+ ): Promise<{ user: User; identity: SsoIdentity } | null> {
90
+ const emailHash = computeEmailHash(idpPayload.email)
91
+ const user = await findOneWithDecryption(
92
+ this.em,
93
+ User,
94
+ {
95
+ organizationId: config.organizationId,
96
+ deletedAt: null,
97
+ $or: [
98
+ { email: idpPayload.email },
99
+ { emailHash },
100
+ ],
101
+ } as FilterQuery<User>,
102
+ {},
103
+ { tenantId, organizationId: config.organizationId },
104
+ )
105
+ if (!user) return null
106
+
107
+ const now = new Date()
108
+ const identity = this.em.create(SsoIdentity, {
109
+ tenantId,
110
+ organizationId: config.organizationId,
111
+ ssoConfigId: config.id,
112
+ userId: user.id,
113
+ idpSubject: idpPayload.subject,
114
+ idpEmail: idpPayload.email,
115
+ idpName: idpPayload.name ?? null,
116
+ idpGroups: idpPayload.groups ?? [],
117
+ provisioningMethod: 'manual',
118
+ firstLoginAt: now,
119
+ lastLoginAt: now,
120
+ createdAt: now,
121
+ updatedAt: now,
122
+ } as RequiredEntityData<SsoIdentity>)
123
+ await this.em.persistAndFlush(identity)
124
+
125
+ void emitSsoEvent('sso.identity.linked', {
126
+ id: identity.id,
127
+ tenantId,
128
+ organizationId: config.organizationId,
129
+ }).catch((e) => console.error('[SSO Event]', e))
130
+
131
+ return { user, identity }
132
+ }
133
+
134
+ private async jitProvision(
135
+ config: SsoConfig,
136
+ idpPayload: SsoIdentityPayload,
137
+ tenantId: string,
138
+ ): Promise<{ user: User; identity: SsoIdentity }> {
139
+ return this.em.transactional(async (txEm) => {
140
+ const user = txEm.create(User, {
141
+ tenantId,
142
+ organizationId: config.organizationId,
143
+ email: idpPayload.email,
144
+ emailHash: computeEmailHash(idpPayload.email),
145
+ name: idpPayload.name ?? null,
146
+ passwordHash: null,
147
+ isConfirmed: true,
148
+ createdAt: new Date(),
149
+ })
150
+ await txEm.persistAndFlush(user)
151
+
152
+ await this.assignRolesFromSso(txEm, user, config, tenantId, idpPayload.groups)
153
+
154
+ const now = new Date()
155
+ const identity = txEm.create(SsoIdentity, {
156
+ tenantId,
157
+ organizationId: config.organizationId,
158
+ ssoConfigId: config.id,
159
+ userId: user.id,
160
+ idpSubject: idpPayload.subject,
161
+ idpEmail: idpPayload.email,
162
+ idpName: idpPayload.name ?? null,
163
+ idpGroups: idpPayload.groups ?? [],
164
+ provisioningMethod: 'jit',
165
+ firstLoginAt: now,
166
+ lastLoginAt: now,
167
+ createdAt: now,
168
+ updatedAt: now,
169
+ } as RequiredEntityData<SsoIdentity>)
170
+ await txEm.persistAndFlush(identity)
171
+
172
+ void emitSsoEvent('sso.identity.created', {
173
+ id: identity.id,
174
+ tenantId,
175
+ organizationId: config.organizationId,
176
+ }).catch((e) => console.error('[SSO Event]', e))
177
+
178
+ return { user, identity }
179
+ })
180
+ }
181
+
182
+ private async assignRolesFromSso(
183
+ em: EntityManager,
184
+ user: User,
185
+ config: SsoConfig,
186
+ tenantId: string,
187
+ idpGroups?: string[],
188
+ ): Promise<void> {
189
+ const hasMappings = config.appRoleMappings && Object.keys(config.appRoleMappings).length > 0
190
+ if (!hasMappings) return
191
+
192
+ await this.syncMappedRoles(em, user, config, tenantId, idpGroups)
193
+
194
+ const hasAnySsoRole = await em.findOne(SsoRoleGrant, {
195
+ userId: user.id,
196
+ ssoConfigId: config.id,
197
+ })
198
+ if (!hasAnySsoRole) {
199
+ throw new Error('No roles could be resolved from IdP groups — login denied. Configure role mappings or ensure the IdP sends matching group claims.')
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Sync/replace SSO-sourced roles: on each login, SSO-managed roles are replaced
205
+ * with what the IdP sends, while manually-assigned roles are preserved.
206
+ */
207
+ private async syncMappedRoles(
208
+ em: EntityManager,
209
+ user: User,
210
+ config: SsoConfig,
211
+ tenantId: string,
212
+ idpGroups?: string[],
213
+ ): Promise<void> {
214
+ const resolvedTenantId = tenantId || user.tenantId || ''
215
+ if (!resolvedTenantId) return
216
+
217
+ const allRoles = await em.find(Role, { tenantId: resolvedTenantId, deletedAt: null } as FilterQuery<Role>)
218
+ const roleByNormalizedName = new Map<string, Role>()
219
+ for (const role of allRoles) {
220
+ const normalized = normalizeToken(role.name)
221
+ if (normalized) roleByNormalizedName.set(normalized, role)
222
+ }
223
+
224
+ // Resolve desired role IDs from IdP groups using merged mappings
225
+ const desiredRoleNames = resolveRoleNamesFromIdpGroups(idpGroups, config.appRoleMappings)
226
+ const desiredRoleIds = new Set<string>()
227
+ for (const roleName of desiredRoleNames) {
228
+ const role = roleByNormalizedName.get(roleName)
229
+ if (role) desiredRoleIds.add(role.id)
230
+ }
231
+
232
+ // Query current SSO grants for this user+config
233
+ const existingGrants = await em.find(SsoRoleGrant, {
234
+ userId: user.id,
235
+ ssoConfigId: config.id,
236
+ })
237
+ const existingGrantedRoleIds = new Set(existingGrants.map((g) => g.roleId))
238
+
239
+ // Compute diff
240
+ const toAdd = [...desiredRoleIds].filter((id) => !existingGrantedRoleIds.has(id))
241
+ const toRemove = existingGrants.filter((g) => !desiredRoleIds.has(g.roleId))
242
+
243
+ // Add new roles
244
+ for (const roleId of toAdd) {
245
+ const role = allRoles.find((r) => r.id === roleId)
246
+ if (!role) continue
247
+ await this.ensureUserRole(em, user, role)
248
+ const grant = em.create(SsoRoleGrant, {
249
+ tenantId: resolvedTenantId,
250
+ organizationId: config.organizationId,
251
+ userId: user.id,
252
+ roleId,
253
+ ssoConfigId: config.id,
254
+ } as RequiredEntityData<SsoRoleGrant>)
255
+ em.persist(grant)
256
+ }
257
+
258
+ // Remove stale SSO-sourced roles
259
+ for (const grant of toRemove) {
260
+ const userRole = await em.findOne(UserRole, {
261
+ user: user.id,
262
+ role: grant.roleId,
263
+ deletedAt: null,
264
+ } as FilterQuery<UserRole>)
265
+ if (userRole) {
266
+ em.remove(userRole)
267
+ }
268
+ em.remove(grant)
269
+ }
270
+
271
+ // Clean up orphaned soft-deleted UserRole rows (ghost rows from previous soft-delete logic)
272
+ const allUserRoles = await em.find(UserRole, { user: user.id } as FilterQuery<UserRole>)
273
+ for (const ur of allUserRoles) {
274
+ if (ur.deletedAt) {
275
+ em.remove(ur)
276
+ }
277
+ }
278
+
279
+ if (toAdd.length > 0 || toRemove.length > 0 || allUserRoles.some((ur) => ur.deletedAt)) {
280
+ await em.flush()
281
+ }
282
+ }
283
+
284
+ private async ensureUserRole(em: EntityManager, user: User, role: Role): Promise<void> {
285
+ const existingLink = await em.findOne(UserRole, {
286
+ user: user.id,
287
+ role: role.id,
288
+ deletedAt: null,
289
+ } as FilterQuery<UserRole>)
290
+ if (existingLink) return
291
+
292
+ const userRole = em.create(UserRole, { user, role, createdAt: new Date() })
293
+ await em.persistAndFlush(userRole)
294
+ }
295
+ }
296
+
297
+ function resolveRoleNamesFromIdpGroups(
298
+ idpGroups?: string[],
299
+ configMappings?: Record<string, string>,
300
+ ): string[] {
301
+ if (!Array.isArray(idpGroups) || idpGroups.length === 0) return []
302
+
303
+ const normalizedGroups = idpGroups
304
+ .map((group) => normalizeToken(group))
305
+ .filter((group): group is string => group !== null)
306
+ if (normalizedGroups.length === 0) return []
307
+
308
+ const mergedMappings = loadMergedMappings(configMappings)
309
+ const roleNames = new Set<string>()
310
+
311
+ for (const group of normalizedGroups) {
312
+ const mapped = mergedMappings.get(group)
313
+ if (mapped?.length) {
314
+ for (const role of mapped) roleNames.add(role)
315
+ continue
316
+ }
317
+
318
+ roleNames.add(group)
319
+ const segmented = group.split(/[\\/:]/).map((part) => normalizeToken(part)).filter((part): part is string => part !== null)
320
+ for (const candidate of segmented) {
321
+ roleNames.add(candidate)
322
+ }
323
+ }
324
+
325
+ return Array.from(roleNames)
326
+ }
327
+
328
+ function loadMergedMappings(configMappings?: Record<string, string>): Map<string, string[]> {
329
+ const envMappings = loadGroupRoleMappingsFromEnv()
330
+
331
+ // Per-config mappings take precedence over env var
332
+ if (configMappings && Object.keys(configMappings).length > 0) {
333
+ for (const [group, roleName] of Object.entries(configMappings)) {
334
+ const normalizedGroup = normalizeToken(group)
335
+ if (!normalizedGroup) continue
336
+ const normalizedRole = normalizeToken(roleName)
337
+ if (!normalizedRole) continue
338
+ envMappings.set(normalizedGroup, [normalizedRole])
339
+ }
340
+ }
341
+
342
+ return envMappings
343
+ }
344
+
345
+ function loadGroupRoleMappingsFromEnv(): Map<string, string[]> {
346
+ const raw = process.env.SSO_GROUP_ROLE_MAP
347
+ if (!raw) return new Map()
348
+
349
+ try {
350
+ const parsed = JSON.parse(raw) as Record<string, unknown>
351
+ const out = new Map<string, string[]>()
352
+ for (const [group, roleValue] of Object.entries(parsed)) {
353
+ const normalizedGroup = normalizeToken(group)
354
+ if (!normalizedGroup) continue
355
+ const roles = normalizeRoleList(roleValue)
356
+ if (roles.length > 0) out.set(normalizedGroup, roles)
357
+ }
358
+ return out
359
+ } catch {
360
+ return new Map()
361
+ }
362
+ }
363
+
364
+ function normalizeRoleList(value: unknown): string[] {
365
+ if (typeof value === 'string') {
366
+ const token = normalizeToken(value)
367
+ return token ? [token] : []
368
+ }
369
+
370
+ if (Array.isArray(value)) {
371
+ const out = new Set<string>()
372
+ for (const entry of value) {
373
+ const token = normalizeToken(entry)
374
+ if (token) out.add(token)
375
+ }
376
+ return Array.from(out)
377
+ }
378
+
379
+ return []
380
+ }
381
+
382
+ function normalizeToken(value: unknown): string | null {
383
+ if (typeof value !== 'string') return null
384
+ const normalized = value.trim().toLowerCase()
385
+ return normalized.length > 0 ? normalized : null
386
+ }
@@ -0,0 +1,22 @@
1
+ import { EntityManager } from '@mikro-orm/postgresql'
2
+ import { SsoConfig } from '../data/entities'
3
+
4
+ export class HrdService {
5
+ constructor(private em: EntityManager) {}
6
+
7
+ async findActiveConfigByEmailDomain(email: string): Promise<SsoConfig | null> {
8
+ const domain = email.split('@')[1]?.toLowerCase()
9
+ if (!domain) return null
10
+
11
+ const knex = this.em.getKnex()
12
+ const row = await knex('sso_configs')
13
+ .whereRaw("allowed_domains @> ?::jsonb", [JSON.stringify([domain])])
14
+ .where('is_active', true)
15
+ .whereNull('deleted_at')
16
+ .first()
17
+
18
+ if (!row) return null
19
+
20
+ return this.em.map(SsoConfig, row)
21
+ }
22
+ }