Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
abdc83c
feat(auth): make Sim an OAuth 2.1 provider and move the CLI onto it
waleedlatif1 Sep 4, 2026
d2c5b82
Merge remote-tracking branch 'origin/staging' into codex/review-pr-7488
waleedlatif1 Sep 4, 2026
71943ae
test: sync OAuth audit mock
waleedlatif1 Sep 4, 2026
1d33c6a
docs: clarify OAuth logout guarantees
waleedlatif1 Sep 4, 2026
d51d646
docs: scope OAuth refresh lock guarantee
waleedlatif1 Sep 4, 2026
38e2516
fix(auth): harden OAuth provider and API DX
waleedlatif1 Sep 5, 2026
357a1ac
Merge remote-tracking branch 'origin/staging' into codex/review-pr-7488
waleedlatif1 Sep 5, 2026
2c4bf1c
fix(ui): refine OAuth consent and app revocation DX
waleedlatif1 Sep 5, 2026
4750e5a
fix(helm): bump chart for OAuth cleanup job
waleedlatif1 Sep 5, 2026
eda5e21
fix(api): preserve concise table predicate guidance
waleedlatif1 Sep 5, 2026
11a93f1
docs(api): regenerate table predicate specification
waleedlatif1 Sep 5, 2026
e9ed221
fix(api): retain predicate DX across clients
waleedlatif1 Sep 5, 2026
93fd43b
feat(scim): SCIM 2.0 directory provisioning with group-to-access proj…
waleedlatif1 Sep 7, 2026
1592ef1
fix(scim): audit fixes, admin settings UI, and reversible deactivation
waleedlatif1 Sep 7, 2026
41ab8b1
fix(scim): second audit round — bugs, abstractions, tests
waleedlatif1 Sep 7, 2026
304b4e9
fix(scim): address the first review round and CI
waleedlatif1 Sep 7, 2026
16fc76e
test(audit): mirror the SCIM actions and resource types into the shar…
waleedlatif1 Sep 7, 2026
aa695d1
fix(scim): submit only the changed setting; move settings option list…
waleedlatif1 Sep 7, 2026
61a1cad
fix(scim): address cubic's second review round
waleedlatif1 Sep 7, 2026
d6b077b
fix(scim): serialize first-time configuration; reconcile members when…
waleedlatif1 Sep 7, 2026
ee52702
Merge staging into feat/oauth-provider
waleedlatif1 Sep 7, 2026
b2aea87
fix(scim): mapping provenance column, drop the unused SSO binding, an…
waleedlatif1 Sep 7, 2026
8441f5e
docs(auth): sync migration references after staging merge
waleedlatif1 Sep 7, 2026
670b288
refactor(scim): one removal invariant, honest provenance, finished pr…
waleedlatif1 Sep 7, 2026
0bdc120
fix(auth): complete OAuth integration and lifecycle guarantees
waleedlatif1 Sep 7, 2026
8a7ab21
Merge remote-tracking branch 'origin/staging' into codex/audit-pr-7488
waleedlatif1 Sep 7, 2026
c253788
test(cli): isolate OAuth process fixtures from credential overrides
waleedlatif1 Sep 7, 2026
c10ea0b
refactor(scim): move the provisioning library under ee
waleedlatif1 Sep 7, 2026
adc82a3
fix(auth): bound cleanup runtime and finish consent polish
waleedlatif1 Sep 7, 2026
7f68209
Merge remote-tracking branch 'origin/staging' into codex/audit-pr-7488
waleedlatif1 Sep 7, 2026
f2fbd76
fix(auth): unify CLI completion screens and simplify consent
waleedlatif1 Sep 7, 2026
12e9b86
fix(scim): certification pass — removal side effects, hourly sweep, d…
waleedlatif1 Sep 7, 2026
fa214ea
refactor(scim): keep the server code at ee/scim/lib, matching the oth…
waleedlatif1 Sep 8, 2026
6116e19
feat(auth): control OAuth rollout through AppConfig
waleedlatif1 Sep 8, 2026
751550d
Merge remote-tracking branch 'origin/staging' into codex/audit-pr-7488
waleedlatif1 Sep 8, 2026
d4aaf58
merge: feat/oauth-provider into feat/scim-provisioning
waleedlatif1 Sep 8, 2026
c981fbe
Merge remote-tracking branch 'origin/feat/oauth-provider' into feat/s…
waleedlatif1 Sep 8, 2026
26817ff
merge: staging (with #7488 squashed) into feat/scim-provisioning
waleedlatif1 Sep 8, 2026
dede150
fix(scim): certification audit round
waleedlatif1 Sep 8, 2026
3c6ff6f
test(scim): cover provisioning, the reconcile job, and group membersh…
waleedlatif1 Sep 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/docs/content/docs/platform/enterprise/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"index",
"self-hosted",
"sso",
"scim",
"verified-domains",
"session-policies",
"access-control",
Expand Down
198 changes: 198 additions & 0 deletions apps/docs/content/docs/platform/enterprise/scim.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
---
title: Directory provisioning (SCIM)
description: Create, update, and deactivate Sim members automatically from your identity provider
---

import { Callout } from 'fumadocs-ui/components/callout'
import { Step, Steps } from 'fumadocs-ui/components/steps'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
import { FAQ } from '@/components/ui/faq'

Directory provisioning connects your identity provider to Sim over SCIM 2.0. Your provider creates members when someone joins, updates them when their details change, and deactivates them the moment they leave — without anyone touching Sim.

It pairs with [SSO](/platform/enterprise/sso). SSO proves who someone is when they sign in. Directory provisioning decides who exists and what they can reach, before and after that.

<Callout type="info">
Included with Enterprise plans. Requires [SSO](/platform/enterprise/sso) to be enabled, because provisioning is configured from the SSO settings page, and at least one [verified domain](/platform/enterprise/verified-domains) for your organization. Self-hosted deployments get it with the other enterprise features through `ENTERPRISE_ENABLED=true`, or turn just this feature on or off with `SCIM_ENABLED` and `NEXT_PUBLIC_SCIM_ENABLED`, alongside the [SSO variables](/platform/enterprise/sso#self-hosted-setup).
</Callout>

## What it does

| Your provider does this | Sim does this |
| --- | --- |
| Assigns a person to the Sim app | Creates their account and adds them to your organization as a Member |
| Updates their name or email | Updates the Sim account, and ends their sessions if the address changed |
| Deactivates them | Blocks sign-in and stops their personal API keys. Everything they own, and every grant they hold, is left untouched; shared workspace keys keep working |
| Reactivates them | Restores access exactly as it was |
| Removes them from the app | Removes their organization membership, ends their sessions, deletes their personal API keys, and reassigns what they owned |
| Adds them to a group | Grants whatever that group maps to |

Deactivation is reversible and never destructive. Someone on leave keeps their workflows, their credentials, and their workspace history; they simply cannot sign in.

## Turn it on

<Steps>

<Step>
### Verify your domain

Sim only provisions people whose email is in a domain your organization has verified. See [Verified domains](/platform/enterprise/verified-domains).

This is what stops another tenant's directory from claiming an address it does not own.
</Step>

<Step>
### Enable directory provisioning

In **Settings → SSO → Directory provisioning**, turn it on. Sim shows your SCIM base URL:
Comment thread
waleedlatif1 marked this conversation as resolved.

```
https://<your-sim-domain>/api/scim/v2
```
</Step>

<Step>
### Issue a token

Choose whether the token expires (never, 90 days, or a year) and select **Issue token**. It appears once — copy it straight into your provider.

Two tokens can be active at a time, so you can rotate without downtime: issue the new one, update your provider, confirm a sync succeeds, then revoke the old one.
</Step>
Comment thread
waleedlatif1 marked this conversation as resolved.

<Step>
### Configure your provider

<Tabs items={['Okta', 'Microsoft Entra ID', 'OneLogin', 'JumpCloud']}>
<Tab value="Okta">

In your Okta app, open **Provisioning → Integration** and select **Configure API Integration**.

- **SCIM connector base URL**: `https://<your-sim-domain>/api/scim/v2`
- **Unique identifier field for users**: `userName`
- **Supported provisioning actions**: Push New Users, Push Profile Updates, Push Groups
- **Authentication Mode**: HTTP Header, with your Sim token

Select **Test API Credentials**, then save. Under **Provisioning → To App**, enable Create Users, Update User Attributes, and Deactivate Users.

Okta never deletes users over SCIM. Unassigning someone, or deactivating them in Okta, sends a deactivation — which Sim applies as a suspension.

</Tab>
<Tab value="Microsoft Entra ID">

In your enterprise application, open **Provisioning** and set Provisioning Mode to **Automatic**.

- **Tenant URL**: `https://<your-sim-domain>/api/scim/v2`
- **Secret Token**: your Sim token

Select **Test Connection**, then save and start provisioning.

Entra runs an initial cycle over everyone in scope, then incremental cycles roughly every 40 minutes. Removing someone from the app sends a deactivation; a permanent delete in Entra sends a removal about 30 days later.

</Tab>
<Tab value="OneLogin">

Add a **SCIM Provisioner with SAML** app.

- **SCIM Base URL**: `https://<your-sim-domain>/api/scim/v2`
- **SCIM Bearer Token**: your Sim token

Enable provisioning and choose what happens when a user is removed. Suspend maps to a Sim suspension; Delete removes their membership.

</Tab>
<Tab value="JumpCloud">

Add a **Custom SCIM** identity management integration.

- **Base URL**: `https://<your-sim-domain>/api/scim/v2`
- **Token Key**: your Sim token

Enable group sync if you plan to map groups.

</Tab>
</Tabs>
</Step>

<Step>
### Map your groups

Groups mean nothing to Sim until you say what they stand for. In **Settings → SSO → Directory provisioning → Group mappings**, point each pushed group at one or more of:

- a **permission group**, which governs models, integrations, and capabilities
- a **workspace**, at Read, Write, or Admin
- the **organization admin role**

A group can carry several mappings. When two groups grant the same workspace at different levels, the stronger one wins. The organization's default permission group cannot be a target: it governs by having no members.

Turning on **Match permission groups by name** maps a pushed group to an existing permission group of the same name automatically, and remaps it when the group is renamed. Nothing is created.

Mapping a permission group to a directory group switches that permission group to explicit membership permanently: it governs exactly the people in it, and an empty group governs nobody. A permission group that governed everyone in its workspaces stops doing so the moment it is mapped, so map groups you created for the directory rather than your organization-wide ones.

</Step>

</Steps>

## How access is withdrawn

Sim records every grant it makes on your behalf. When someone leaves a group, what the directory granted is taken back.

**Managed membership locking**, on by default, makes the directory the source of truth for provisioned members: Sim refuses invitations, workspace grants, workspace role changes, and organization role changes for them, because the next sync would revert them anyway. Access a member already held by hand when a mapping started covering it counts as directory access from then on, so it is withdrawn with the mapping. Removals stay possible so an administrator can always act in an emergency.

With locking off, manual access layers on top of directory access: access granted by hand stays when a group is left, and a workspace role raised by hand above what the directory set is left alone.

## Provisioning and SSO together

A member the directory created can sign in with SSO immediately; the two resolve to the same account through your verified domain.

If you want the directory to be the only way in, enable **Disable just-in-time provisioning** in the connection settings. Sim then refuses to create membership for someone signing in who was never provisioned.

## Watching a sync

**Settings → SSO → Directory provisioning → Activity** lists recent authenticated requests with their status and, for a failure, what was wrong. Providers report a failed cycle without saying what they sent, so this is usually the fastest way to see the cause. A request that fails to authenticate has no connection to log against, so a wrong or revoked token shows up only as your provider's own authentication error.

Sim also re-applies every group mapping once an hour, so drift cannot persist. You can run it on demand with **Reconcile now**, which is also how a change to the connection settings reaches members before the next sync.

## Reference

- Base URL: `https://<your-sim-domain>/api/scim/v2`
- Authentication: `Authorization: Bearer <token>`
- Resources: `/Users`, `/Groups`, plus `/ServiceProviderConfig`, `/ResourceTypes`, and `/Schemas`
- Filters: `eq` only, up to ten terms joined with `and`. Users: `id`, `userName`, `externalId`, `emails.value` (also `emails[type eq "work"].value`), `active`. Groups: `id`, `displayName`, `externalId`
- Limits: 1,500 requests per minute per connection, 1 MB per request, 5,000 members per group
- `userName` is stored and returned lower-cased; top-level attributes and schema extensions Sim does not model (custom attributes included) are kept and returned as sent, and a PUT preserves ones it omits
- Group display names are unique within a connection, ignoring case
- Page size: up to 100 per request

<FAQ items={[
Comment thread
waleedlatif1 marked this conversation as resolved.
{
question: "What happens to someone's workflows when they are deactivated?",
answer: "Nothing. A deactivation blocks sign-in and stops their personal API keys, and leaves every workspace, workflow, and credential they own exactly as it was. Reactivating them restores access. Only a removal — which your provider sends explicitly — reassigns what they owned. A deactivated member also keeps their seat until they are removed."
},
{
question: "Can the directory provision someone outside our verified domains?",
answer: "No. Sim refuses to create or move an account to an address whose domain your organization has not verified. This is what prevents one tenant's directory from claiming an account it does not own."
},
{
question: "What if the email already belongs to a Sim account?",
answer: "If the account is in your organization, or in none, Sim attaches to it. If it belongs to a different organization, Sim returns a conflict — a Sim account can belong to only one organization, so someone has to be removed there first."
},
{
question: "Can the directory remove our organization owner?",
answer: "No. Sim refuses, because it would leave nobody able to administer billing or transfer ownership. Transfer ownership in Sim first."
},
{
question: "What happens when we run out of seats?",
answer: "Sim refuses the new member with a message your provider shows to its administrator. Add seats in Sim and the next sync will pick them up. Team plans grow their seat count automatically; fixed-seat plans do not."
},
{
question: "Does a deactivation sign someone out immediately?",
answer: "Their sessions are deleted at once and their personal API keys stop authenticating immediately. The organization's security version changes at the same time, so a browser still holding a cached session cookie is re-checked on its next request and refused."
},
{
question: "What happens to deactivated members if we turn provisioning off?",
answer: "They stay deactivated. Disabling the connection stops the directory from making changes; it does not undo the ones it made. To restore someone without the directory, remove them from the organization and invite them again."
},
{
question: "Can we still invite people manually?",
answer: "Only with managed membership locking turned off. With it on, Sim refuses manual changes for provisioned members, because the directory would revert them on its next sync."
}
]} />
1 change: 1 addition & 0 deletions apps/docs/content/docs/platform/enterprise/self-hosted.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Three features do not need a flag at all: **custom branding**, **session policie
| Organizations | `ORGANIZATIONS_ENABLED` | `NEXT_PUBLIC_ORGANIZATIONS_ENABLED` |
| Permission groups | `ACCESS_CONTROL_ENABLED` | `NEXT_PUBLIC_ACCESS_CONTROL_ENABLED` |
| SAML and OIDC sign-in | `SSO_ENABLED` | `NEXT_PUBLIC_SSO_ENABLED` |
| Directory provisioning (SCIM) | `SCIM_ENABLED` | `NEXT_PUBLIC_SCIM_ENABLED` |
| Custom branding — on by default | `WHITELABELING_ENABLED` | `NEXT_PUBLIC_WHITELABELING_ENABLED` |
| Audit logs | `AUDIT_LOGS_ENABLED` | `NEXT_PUBLIC_AUDIT_LOGS_ENABLED` |
| Usage tracking | `USAGE_MONITORING_ENABLED` | `NEXT_PUBLIC_USAGE_MONITORING_ENABLED` |
Expand Down
8 changes: 6 additions & 2 deletions apps/docs/content/docs/platform/enterprise/sso.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ With **Automatic** provisioning, no invitation is required for organization memb
Sign-in must start from Sim. Launching from your identity provider's app portal (Microsoft's **My Apps**, Okta's dashboard tile) sends an unsolicited assertion, which Sim rejects. This is deliberate — accepting them would let anyone replay an assertion into your tenant — but it means an IdP-initiated test fails even when the configuration is correct.
</Callout>

SSO provisioning creates internal organization members but does not grant workspace access. External workspace members are different: they are invited to a specific workspace without joining your organization or consuming one of your seats. Existing invitations and external access take precedence over automatic provisioning so their intended role and workspace grants are preserved.
SSO provisioning creates internal organization members but does not grant workspace access. To grant workspace access from your identity provider, use [directory provisioning](/platform/enterprise/scim) and map a pushed group to a workspace. External workspace members are different: they are invited to a specific workspace without joining your organization or consuming one of your seats. Existing invitations and external access take precedence over automatic provisioning so their intended role and workspace grants are preserved.

<Callout type="info">
Password-based login remains available. Forcing all organization members to use SSO exclusively is not yet supported.
Expand All @@ -305,7 +305,7 @@ SSO provisioning creates internal organization members but does not grant worksp
},
{
question: "Does disabling someone in the identity provider remove their Sim access?",
answer: "No. Disabling the IdP account blocks future SSO authentication, but Sim does not currently receive SCIM deprovisioning or IdP logout events to remove membership or revoke active Sim sessions. Remove or suspend the user in Sim as part of offboarding."
answer: "With [directory provisioning](/platform/enterprise/scim) connected, yes: your identity provider sends the deactivation, and Sim blocks sign-in and stops their API keys while leaving everything they own intact. With SSO alone, disabling the IdP account only blocks future SSO authentication — remove or suspend the user in Sim as part of offboarding."
},
{
question: "Can I still use email/password login after enabling SSO?",
Expand Down Expand Up @@ -342,6 +342,10 @@ Self-hosted deployments use environment variables instead of the billing/plan ch
SSO_ENABLED=true
NEXT_PUBLIC_SSO_ENABLED=true

# Optional: directory provisioning (SCIM), configured from the SSO settings page
SCIM_ENABLED=true
NEXT_PUBLIC_SCIM_ENABLED=true

# Required if you want users auto-added to your organization on first SSO sign-in
ORGANIZATIONS_ENABLED=true
NEXT_PUBLIC_ORGANIZATIONS_ENABLED=true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ Enterprise features are unlocked by configuration rather than billing on self-ho
|----------|-------------|
| `ENTERPRISE_ENABLED`, `NEXT_PUBLIC_ENTERPRISE_ENABLED` | Enable the whole enterprise feature set |
| `SSO_ENABLED`, `NEXT_PUBLIC_SSO_ENABLED` | Enable SAML and OIDC single sign-on on its own. See [Authentication](/platform/self-hosting/authentication#sso-saml-and-oidc) |
| `SCIM_ENABLED`, `NEXT_PUBLIC_SCIM_ENABLED` | Enable directory provisioning on its own. Needs SSO. See [Directory provisioning](/platform/enterprise/scim) |
| `INSTANCE_ORG_NAME` | Name of the organization every user joins automatically at signup |
| `INSTANCE_ORG_SLUG` | Slug for that organization (derived from the name when omitted) |
| `INSTANCE_ORG_OWNER_EMAIL` | Owner of that organization (defaults to the first user to sign up) |
Expand Down
3 changes: 2 additions & 1 deletion apps/docs/openapi-v2-billing.json
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,8 @@
"CONNECTOR_MANAGED_RESOURCE_READ_ONLY",
"PERMISSION_GROUP_CAPABILITY_BLOCKED",
"INTEGRATION_NOT_ALLOWED",
"INSUFFICIENT_SCOPE"
"INSUFFICIENT_SCOPE",
"SCIM_MANAGED_MEMBERSHIP"
],
"title": "Forbidden detail code",
"description": "Stable cause code for an actionable `403` response."
Expand Down
3 changes: 2 additions & 1 deletion apps/docs/openapi-v2-files-audit.json
Original file line number Diff line number Diff line change
Expand Up @@ -3186,7 +3186,8 @@
"CONNECTOR_MANAGED_RESOURCE_READ_ONLY",
"PERMISSION_GROUP_CAPABILITY_BLOCKED",
"INTEGRATION_NOT_ALLOWED",
"INSUFFICIENT_SCOPE"
"INSUFFICIENT_SCOPE",
"SCIM_MANAGED_MEMBERSHIP"
],
"title": "Forbidden detail code",
"description": "Stable cause code for an actionable `403` response."
Expand Down
3 changes: 2 additions & 1 deletion apps/docs/openapi-v2-knowledge.json
Original file line number Diff line number Diff line change
Expand Up @@ -4618,7 +4618,8 @@
"CONNECTOR_MANAGED_RESOURCE_READ_ONLY",
"PERMISSION_GROUP_CAPABILITY_BLOCKED",
"INTEGRATION_NOT_ALLOWED",
"INSUFFICIENT_SCOPE"
"INSUFFICIENT_SCOPE",
"SCIM_MANAGED_MEMBERSHIP"
],
"title": "Forbidden detail code",
"description": "Stable cause code for an actionable `403` response."
Expand Down
3 changes: 2 additions & 1 deletion apps/docs/openapi-v2-logs.json
Original file line number Diff line number Diff line change
Expand Up @@ -824,7 +824,8 @@
"CONNECTOR_MANAGED_RESOURCE_READ_ONLY",
"PERMISSION_GROUP_CAPABILITY_BLOCKED",
"INTEGRATION_NOT_ALLOWED",
"INSUFFICIENT_SCOPE"
"INSUFFICIENT_SCOPE",
"SCIM_MANAGED_MEMBERSHIP"
],
"title": "Forbidden detail code",
"description": "Stable cause code for an actionable `403` response."
Expand Down
3 changes: 2 additions & 1 deletion apps/docs/openapi-v2-resources.json
Original file line number Diff line number Diff line change
Expand Up @@ -5023,7 +5023,8 @@
"CONNECTOR_MANAGED_RESOURCE_READ_ONLY",
"PERMISSION_GROUP_CAPABILITY_BLOCKED",
"INTEGRATION_NOT_ALLOWED",
"INSUFFICIENT_SCOPE"
"INSUFFICIENT_SCOPE",
"SCIM_MANAGED_MEMBERSHIP"
],
"title": "Forbidden detail code",
"description": "Stable cause code for an actionable `403` response."
Expand Down
3 changes: 2 additions & 1 deletion apps/docs/openapi-v2-tables.json
Original file line number Diff line number Diff line change
Expand Up @@ -5034,7 +5034,8 @@
"CONNECTOR_MANAGED_RESOURCE_READ_ONLY",
"PERMISSION_GROUP_CAPABILITY_BLOCKED",
"INTEGRATION_NOT_ALLOWED",
"INSUFFICIENT_SCOPE"
"INSUFFICIENT_SCOPE",
"SCIM_MANAGED_MEMBERSHIP"
],
"title": "Forbidden detail code",
"description": "Stable cause code for an actionable `403` response."
Expand Down
3 changes: 2 additions & 1 deletion apps/docs/openapi-v2-workflows.json
Original file line number Diff line number Diff line change
Expand Up @@ -4018,7 +4018,8 @@
"CONNECTOR_MANAGED_RESOURCE_READ_ONLY",
"PERMISSION_GROUP_CAPABILITY_BLOCKED",
"INTEGRATION_NOT_ALLOWED",
"INSUFFICIENT_SCOPE"
"INSUFFICIENT_SCOPE",
"SCIM_MANAGED_MEMBERSHIP"
],
"title": "Forbidden detail code",
"description": "Stable cause code for an actionable `403` response."
Expand Down
31 changes: 31 additions & 0 deletions apps/sim/app/api/cron/scim-reconcile/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { verifyCronAuth } from '@/lib/auth/internal'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { isScimDeploymentEnabled } from '@/ee/scim/lib/entitlement'
import { runScimReconcileSweep } from '@/ee/scim/lib/reconcile/job'

const logger = createLogger('CronScimReconcile')

/**
* Sweeps directory connections for drift between what their group mappings say
* a member should have and what SCIM actually granted them.
*/
export const GET = withRouteHandler(async (request: NextRequest) => {
const authError = verifyCronAuth(request, 'SCIM reconciliation')
if (authError) return authError

if (!isScimDeploymentEnabled()) {
return NextResponse.json({ success: true, connections: 0, skipped: 'disabled' })
}

try {
const sweep = await runScimReconcileSweep()
logger.info('SCIM reconciliation sweep complete', sweep)
return NextResponse.json({ success: true, ...sweep })
} catch (error) {
logger.error('SCIM reconciliation sweep failed', { error: getErrorMessage(error) })
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
})
Loading
Loading