feat(platform-service): add scim provisioning foundation
This commit is contained in:
parent
66d0bf53a9
commit
8b99b7a9a7
@ -22,6 +22,7 @@ const CONTAINER_DEFS: Record<string, ContainerConfig> = {
|
||||
usage_daily: { partitionKeyPath: '/userId' },
|
||||
// API tokens
|
||||
api_tokens: { partitionKeyPath: '/id' },
|
||||
rate_limit_entries: { partitionKeyPath: '/id', defaultTtl: 24 * 3600 },
|
||||
// Tracker modules
|
||||
tracker_items: { partitionKeyPath: '/id' },
|
||||
comments: { partitionKeyPath: '/itemId' },
|
||||
@ -87,6 +88,11 @@ const CONTAINER_DEFS: Record<string, ContainerConfig> = {
|
||||
knowledge_bases: { partitionKeyPath: '/productId' },
|
||||
knowledge_sources: { partitionKeyPath: '/knowledgeBaseId' },
|
||||
knowledge_chunks: { partitionKeyPath: '/knowledgeBaseId', defaultTtl: 90 * 86400 },
|
||||
// Enterprise provisioning / SCIM
|
||||
scim_connectors: { partitionKeyPath: '/orgId' },
|
||||
scim_user_sync: { partitionKeyPath: '/connectorId' },
|
||||
scim_group_sync: { partitionKeyPath: '/connectorId' },
|
||||
scim_events: { partitionKeyPath: '/connectorId', defaultTtl: 90 * 86400 },
|
||||
// Telemetry (client diagnostics — see docs/WINDSURF/CLIENT_TELEMETRY_DESIGN.md)
|
||||
telemetry_events: { partitionKeyPath: '/pk', defaultTtl: 30 * 86400 },
|
||||
telemetry_error_clusters: { partitionKeyPath: '/pk', defaultTtl: 90 * 86400 },
|
||||
|
||||
@ -0,0 +1,59 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { MemoryDatastoreProvider } from '@bytelyst/datastore';
|
||||
import { _resetDatastoreProvider, setProvider } from '../../lib/datastore.js';
|
||||
import * as repo from './repository.js';
|
||||
|
||||
describe('scim repository', () => {
|
||||
beforeEach(() => {
|
||||
setProvider(new MemoryDatastoreProvider());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
_resetDatastoreProvider();
|
||||
});
|
||||
|
||||
it('stores connectors, sync state, and provisioning events', async () => {
|
||||
await repo.createConnector({
|
||||
id: 'scim_1',
|
||||
orgId: 'org_1',
|
||||
productId: 'lysnrai',
|
||||
name: 'Okta Provisioning',
|
||||
authMode: 'bearer',
|
||||
status: 'active',
|
||||
createdBy: 'admin_1',
|
||||
createdAt: '2026-03-15T00:00:00.000Z',
|
||||
updatedAt: '2026-03-15T00:00:00.000Z',
|
||||
});
|
||||
|
||||
await repo.upsertUserSync({
|
||||
id: 'scim_1:user:ext_1',
|
||||
orgId: 'org_1',
|
||||
connectorId: 'scim_1',
|
||||
productId: 'lysnrai',
|
||||
externalUserId: 'ext_1',
|
||||
email: 'member@acme.com',
|
||||
status: 'provisioned',
|
||||
lastSyncedAt: '2026-03-15T00:05:00.000Z',
|
||||
});
|
||||
|
||||
await repo.createEvent({
|
||||
id: 'scimevt_1',
|
||||
orgId: 'org_1',
|
||||
connectorId: 'scim_1',
|
||||
productId: 'lysnrai',
|
||||
entityType: 'user',
|
||||
entityId: 'ext_1',
|
||||
action: 'provision',
|
||||
status: 'success',
|
||||
createdAt: '2026-03-15T00:06:00.000Z',
|
||||
});
|
||||
|
||||
const connectors = await repo.listConnectors('org_1');
|
||||
const users = await repo.listUserSync('scim_1');
|
||||
const events = await repo.listEvents('scim_1');
|
||||
|
||||
expect(connectors).toHaveLength(1);
|
||||
expect(users[0].email).toBe('member@acme.com');
|
||||
expect(events[0].action).toBe('provision');
|
||||
});
|
||||
});
|
||||
93
services/platform-service/src/modules/scim/repository.ts
Normal file
93
services/platform-service/src/modules/scim/repository.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import { NotFoundError } from '../../lib/errors.js';
|
||||
import { getCollection } from '../../lib/datastore.js';
|
||||
import type {
|
||||
ScimConnectorDoc,
|
||||
ScimGroupSyncDoc,
|
||||
ScimProvisioningEventDoc,
|
||||
ScimUserSyncDoc,
|
||||
} from './types.js';
|
||||
|
||||
function connectorCollection() {
|
||||
return getCollection<ScimConnectorDoc>('scim_connectors', '/orgId');
|
||||
}
|
||||
|
||||
function userSyncCollection() {
|
||||
return getCollection<ScimUserSyncDoc>('scim_user_sync', '/connectorId');
|
||||
}
|
||||
|
||||
function groupSyncCollection() {
|
||||
return getCollection<ScimGroupSyncDoc>('scim_group_sync', '/connectorId');
|
||||
}
|
||||
|
||||
function eventCollection() {
|
||||
return getCollection<ScimProvisioningEventDoc>('scim_events', '/connectorId');
|
||||
}
|
||||
|
||||
export async function createConnector(doc: ScimConnectorDoc): Promise<ScimConnectorDoc> {
|
||||
return connectorCollection().create(doc);
|
||||
}
|
||||
|
||||
export async function listConnectors(orgId: string): Promise<ScimConnectorDoc[]> {
|
||||
return connectorCollection().findMany({
|
||||
filter: { orgId },
|
||||
sort: { createdAt: -1 },
|
||||
limit: 100,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getConnector(id: string, orgId: string): Promise<ScimConnectorDoc> {
|
||||
const connector = await connectorCollection().findById(id, orgId);
|
||||
if (!connector) throw new NotFoundError(`SCIM connector '${id}' not found`);
|
||||
return connector;
|
||||
}
|
||||
|
||||
export async function updateConnector(
|
||||
id: string,
|
||||
orgId: string,
|
||||
updates: Partial<ScimConnectorDoc>
|
||||
): Promise<ScimConnectorDoc> {
|
||||
const updated = await connectorCollection().update(id, orgId, {
|
||||
...updates,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
if (!updated) throw new NotFoundError(`SCIM connector '${id}' not found`);
|
||||
return updated;
|
||||
}
|
||||
|
||||
export async function upsertUserSync(doc: ScimUserSyncDoc): Promise<ScimUserSyncDoc> {
|
||||
return userSyncCollection().upsert(doc);
|
||||
}
|
||||
|
||||
export async function listUserSync(connectorId: string): Promise<ScimUserSyncDoc[]> {
|
||||
return userSyncCollection().findMany({
|
||||
filter: { connectorId },
|
||||
sort: { lastSyncedAt: -1 },
|
||||
limit: 500,
|
||||
});
|
||||
}
|
||||
|
||||
export async function upsertGroupSync(doc: ScimGroupSyncDoc): Promise<ScimGroupSyncDoc> {
|
||||
return groupSyncCollection().upsert(doc);
|
||||
}
|
||||
|
||||
export async function listGroupSync(connectorId: string): Promise<ScimGroupSyncDoc[]> {
|
||||
return groupSyncCollection().findMany({
|
||||
filter: { connectorId },
|
||||
sort: { lastSyncedAt: -1 },
|
||||
limit: 500,
|
||||
});
|
||||
}
|
||||
|
||||
export async function createEvent(
|
||||
doc: ScimProvisioningEventDoc
|
||||
): Promise<ScimProvisioningEventDoc> {
|
||||
return eventCollection().create(doc);
|
||||
}
|
||||
|
||||
export async function listEvents(connectorId: string): Promise<ScimProvisioningEventDoc[]> {
|
||||
return eventCollection().findMany({
|
||||
filter: { connectorId },
|
||||
sort: { createdAt: -1 },
|
||||
limit: 500,
|
||||
});
|
||||
}
|
||||
119
services/platform-service/src/modules/scim/routes.test.ts
Normal file
119
services/platform-service/src/modules/scim/routes.test.ts
Normal file
@ -0,0 +1,119 @@
|
||||
import Fastify from 'fastify';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const repoMock = {
|
||||
listConnectors: vi.fn(),
|
||||
createConnector: vi.fn(),
|
||||
getConnector: vi.fn(),
|
||||
updateConnector: vi.fn(),
|
||||
upsertUserSync: vi.fn(),
|
||||
listUserSync: vi.fn(),
|
||||
upsertGroupSync: vi.fn(),
|
||||
listGroupSync: vi.fn(),
|
||||
createEvent: vi.fn(),
|
||||
listEvents: vi.fn(),
|
||||
};
|
||||
|
||||
const orgRepoMock = {
|
||||
getOrganization: vi.fn(),
|
||||
};
|
||||
|
||||
vi.mock('./repository.js', () => repoMock);
|
||||
vi.mock('../orgs/repository.js', () => orgRepoMock);
|
||||
|
||||
async function buildApp(payload?: { sub: string; productId: string; role?: string }) {
|
||||
const { scimRoutes } = await import('./routes.js');
|
||||
const app = Fastify({ logger: false });
|
||||
if (payload) {
|
||||
app.addHook('onRequest', async req => {
|
||||
req.jwtPayload = payload;
|
||||
});
|
||||
}
|
||||
await app.register(scimRoutes, { prefix: '/api' });
|
||||
return app;
|
||||
}
|
||||
|
||||
describe('scimRoutes', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('POST /scim/connectors creates a connector for an existing org', async () => {
|
||||
orgRepoMock.getOrganization.mockResolvedValue({ id: 'org_1' });
|
||||
repoMock.createConnector.mockResolvedValue({ id: 'scim_1' });
|
||||
|
||||
const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });
|
||||
const res = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/scim/connectors',
|
||||
payload: {
|
||||
orgId: 'org_1',
|
||||
name: 'Okta',
|
||||
baseUrl: 'https://okta.example.com/scim/v2',
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(orgRepoMock.getOrganization).toHaveBeenCalledWith('org_1', 'lysnrai');
|
||||
expect(repoMock.createConnector).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
orgId: 'org_1',
|
||||
name: 'Okta',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('POST /scim/connectors/:orgId/:id/users upserts user sync state', async () => {
|
||||
repoMock.getConnector.mockResolvedValue({ id: 'scim_1' });
|
||||
repoMock.upsertUserSync.mockResolvedValue({ id: 'scim_1:user:ext_1' });
|
||||
|
||||
const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });
|
||||
const res = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/scim/connectors/org_1/scim_1/users',
|
||||
payload: {
|
||||
externalUserId: 'ext_1',
|
||||
email: 'member@acme.com',
|
||||
status: 'provisioned',
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(repoMock.upsertUserSync).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
connectorId: 'scim_1',
|
||||
externalUserId: 'ext_1',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('POST /scim/connectors/:orgId/:id/events records provisioning events', async () => {
|
||||
repoMock.getConnector.mockResolvedValue({ id: 'scim_1' });
|
||||
repoMock.createEvent.mockResolvedValue({ id: 'scimevt_1' });
|
||||
|
||||
const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });
|
||||
const res = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/scim/connectors/org_1/scim_1/events',
|
||||
payload: {
|
||||
entityType: 'user',
|
||||
entityId: 'ext_1',
|
||||
action: 'provision',
|
||||
status: 'success',
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(repoMock.createEvent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
connectorId: 'scim_1',
|
||||
entityType: 'user',
|
||||
action: 'provision',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
178
services/platform-service/src/modules/scim/routes.ts
Normal file
178
services/platform-service/src/modules/scim/routes.ts
Normal file
@ -0,0 +1,178 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { BadRequestError, ForbiddenError } from '../../lib/errors.js';
|
||||
import * as orgRepo from '../orgs/repository.js';
|
||||
import {
|
||||
CreateScimConnectorSchema,
|
||||
RecordScimEventSchema,
|
||||
RecordScimGroupSyncSchema,
|
||||
RecordScimUserSyncSchema,
|
||||
ScimConnectorDoc,
|
||||
ScimGroupSyncDoc,
|
||||
ScimProvisioningEventDoc,
|
||||
ScimUserSyncDoc,
|
||||
UpdateScimConnectorSchema,
|
||||
} from './types.js';
|
||||
import * as repo from './repository.js';
|
||||
|
||||
function requireAdmin(req: { jwtPayload?: { sub?: string; role?: string; productId?: string } }): {
|
||||
userId: string;
|
||||
productId: string;
|
||||
} {
|
||||
const payload = req.jwtPayload;
|
||||
if (!payload?.sub) throw new ForbiddenError('Authentication required');
|
||||
if (!payload.role || !['super_admin', 'admin'].includes(payload.role)) {
|
||||
throw new ForbiddenError('Admin access required');
|
||||
}
|
||||
return {
|
||||
userId: payload.sub,
|
||||
productId: payload.productId ?? process.env.DEFAULT_PRODUCT_ID ?? 'lysnrai',
|
||||
};
|
||||
}
|
||||
|
||||
function validationError(message: string): never {
|
||||
throw new BadRequestError(message);
|
||||
}
|
||||
|
||||
export async function scimRoutes(app: FastifyInstance) {
|
||||
app.get('/scim/connectors/:orgId', async req => {
|
||||
requireAdmin(req);
|
||||
const { orgId } = req.params as { orgId: string };
|
||||
return repo.listConnectors(orgId);
|
||||
});
|
||||
|
||||
app.post('/scim/connectors', async req => {
|
||||
const access = requireAdmin(req);
|
||||
const parsed = CreateScimConnectorSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
validationError(parsed.error.issues.map(issue => issue.message).join('; '));
|
||||
}
|
||||
await orgRepo.getOrganization(parsed.data.orgId, access.productId);
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const connector: ScimConnectorDoc = {
|
||||
id: `scim_${randomUUID()}`,
|
||||
orgId: parsed.data.orgId,
|
||||
productId: access.productId,
|
||||
idpId: parsed.data.idpId,
|
||||
name: parsed.data.name,
|
||||
baseUrl: parsed.data.baseUrl,
|
||||
authMode: parsed.data.authMode,
|
||||
status: 'active',
|
||||
metadata: parsed.data.metadata,
|
||||
createdBy: access.userId,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
return repo.createConnector(connector);
|
||||
});
|
||||
|
||||
app.get('/scim/connectors/:orgId/:id', async req => {
|
||||
requireAdmin(req);
|
||||
const { orgId, id } = req.params as { orgId: string; id: string };
|
||||
return repo.getConnector(id, orgId);
|
||||
});
|
||||
|
||||
app.patch('/scim/connectors/:orgId/:id', async req => {
|
||||
requireAdmin(req);
|
||||
const { orgId, id } = req.params as { orgId: string; id: string };
|
||||
const parsed = UpdateScimConnectorSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
validationError(parsed.error.issues.map(issue => issue.message).join('; '));
|
||||
}
|
||||
return repo.updateConnector(id, orgId, parsed.data);
|
||||
});
|
||||
|
||||
app.get('/scim/connectors/:orgId/:id/users', async req => {
|
||||
requireAdmin(req);
|
||||
const { orgId, id } = req.params as { orgId: string; id: string };
|
||||
await repo.getConnector(id, orgId);
|
||||
return repo.listUserSync(id);
|
||||
});
|
||||
|
||||
app.post('/scim/connectors/:orgId/:id/users', async req => {
|
||||
const access = requireAdmin(req);
|
||||
const { orgId, id } = req.params as { orgId: string; id: string };
|
||||
await repo.getConnector(id, orgId);
|
||||
const parsed = RecordScimUserSyncSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
validationError(parsed.error.issues.map(issue => issue.message).join('; '));
|
||||
}
|
||||
const doc: ScimUserSyncDoc = {
|
||||
id: `${id}:user:${parsed.data.externalUserId}`,
|
||||
orgId,
|
||||
connectorId: id,
|
||||
productId: access.productId,
|
||||
externalUserId: parsed.data.externalUserId,
|
||||
userId: parsed.data.userId,
|
||||
email: parsed.data.email,
|
||||
status: parsed.data.status,
|
||||
lastSyncedAt: new Date().toISOString(),
|
||||
errorMessage: parsed.data.errorMessage,
|
||||
metadata: parsed.data.metadata,
|
||||
};
|
||||
return repo.upsertUserSync(doc);
|
||||
});
|
||||
|
||||
app.get('/scim/connectors/:orgId/:id/groups', async req => {
|
||||
requireAdmin(req);
|
||||
const { orgId, id } = req.params as { orgId: string; id: string };
|
||||
await repo.getConnector(id, orgId);
|
||||
return repo.listGroupSync(id);
|
||||
});
|
||||
|
||||
app.post('/scim/connectors/:orgId/:id/groups', async req => {
|
||||
const access = requireAdmin(req);
|
||||
const { orgId, id } = req.params as { orgId: string; id: string };
|
||||
await repo.getConnector(id, orgId);
|
||||
const parsed = RecordScimGroupSyncSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
validationError(parsed.error.issues.map(issue => issue.message).join('; '));
|
||||
}
|
||||
const doc: ScimGroupSyncDoc = {
|
||||
id: `${id}:group:${parsed.data.externalGroupId}`,
|
||||
orgId,
|
||||
connectorId: id,
|
||||
productId: access.productId,
|
||||
externalGroupId: parsed.data.externalGroupId,
|
||||
workspaceId: parsed.data.workspaceId,
|
||||
displayName: parsed.data.displayName,
|
||||
memberCount: parsed.data.memberCount,
|
||||
status: parsed.data.status,
|
||||
lastSyncedAt: new Date().toISOString(),
|
||||
errorMessage: parsed.data.errorMessage,
|
||||
metadata: parsed.data.metadata,
|
||||
};
|
||||
return repo.upsertGroupSync(doc);
|
||||
});
|
||||
|
||||
app.get('/scim/connectors/:orgId/:id/events', async req => {
|
||||
requireAdmin(req);
|
||||
const { orgId, id } = req.params as { orgId: string; id: string };
|
||||
await repo.getConnector(id, orgId);
|
||||
return repo.listEvents(id);
|
||||
});
|
||||
|
||||
app.post('/scim/connectors/:orgId/:id/events', async req => {
|
||||
const access = requireAdmin(req);
|
||||
const { orgId, id } = req.params as { orgId: string; id: string };
|
||||
await repo.getConnector(id, orgId);
|
||||
const parsed = RecordScimEventSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
validationError(parsed.error.issues.map(issue => issue.message).join('; '));
|
||||
}
|
||||
const doc: ScimProvisioningEventDoc = {
|
||||
id: `scimevt_${randomUUID()}`,
|
||||
orgId,
|
||||
connectorId: id,
|
||||
productId: access.productId,
|
||||
entityType: parsed.data.entityType,
|
||||
entityId: parsed.data.entityId,
|
||||
action: parsed.data.action,
|
||||
status: parsed.data.status,
|
||||
message: parsed.data.message,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
return repo.createEvent(doc);
|
||||
});
|
||||
}
|
||||
128
services/platform-service/src/modules/scim/types.ts
Normal file
128
services/platform-service/src/modules/scim/types.ts
Normal file
@ -0,0 +1,128 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ScimConnectorStatusSchema = z.enum(['active', 'paused', 'archived']);
|
||||
export const ScimUserSyncStatusSchema = z.enum(['provisioned', 'updated', 'disabled', 'failed']);
|
||||
export const ScimGroupSyncStatusSchema = z.enum(['provisioned', 'updated', 'disabled', 'failed']);
|
||||
export const ScimEventStatusSchema = z.enum(['success', 'failed']);
|
||||
|
||||
export const ScimConnectorSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
orgId: z.string().min(1),
|
||||
productId: z.string().min(1),
|
||||
idpId: z.string().optional(),
|
||||
name: z.string().min(1),
|
||||
baseUrl: z.string().url().optional(),
|
||||
authMode: z.enum(['bearer', 'oauth']).default('bearer'),
|
||||
status: ScimConnectorStatusSchema,
|
||||
metadata: z.record(z.unknown()).optional(),
|
||||
createdBy: z.string().min(1),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
});
|
||||
|
||||
export type ScimConnectorDoc = z.infer<typeof ScimConnectorSchema> & {
|
||||
_ts?: number;
|
||||
_etag?: string;
|
||||
};
|
||||
|
||||
export const ScimUserSyncSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
orgId: z.string().min(1),
|
||||
connectorId: z.string().min(1),
|
||||
productId: z.string().min(1),
|
||||
externalUserId: z.string().min(1),
|
||||
userId: z.string().optional(),
|
||||
email: z.string().email(),
|
||||
status: ScimUserSyncStatusSchema,
|
||||
lastSyncedAt: z.string(),
|
||||
errorMessage: z.string().optional(),
|
||||
metadata: z.record(z.unknown()).optional(),
|
||||
});
|
||||
|
||||
export type ScimUserSyncDoc = z.infer<typeof ScimUserSyncSchema> & {
|
||||
_ts?: number;
|
||||
_etag?: string;
|
||||
};
|
||||
|
||||
export const ScimGroupSyncSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
orgId: z.string().min(1),
|
||||
connectorId: z.string().min(1),
|
||||
productId: z.string().min(1),
|
||||
externalGroupId: z.string().min(1),
|
||||
workspaceId: z.string().optional(),
|
||||
displayName: z.string().min(1),
|
||||
memberCount: z.number().int().min(0).default(0),
|
||||
status: ScimGroupSyncStatusSchema,
|
||||
lastSyncedAt: z.string(),
|
||||
errorMessage: z.string().optional(),
|
||||
metadata: z.record(z.unknown()).optional(),
|
||||
});
|
||||
|
||||
export type ScimGroupSyncDoc = z.infer<typeof ScimGroupSyncSchema> & {
|
||||
_ts?: number;
|
||||
_etag?: string;
|
||||
};
|
||||
|
||||
export const ScimProvisioningEventSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
orgId: z.string().min(1),
|
||||
connectorId: z.string().min(1),
|
||||
productId: z.string().min(1),
|
||||
entityType: z.enum(['user', 'group']),
|
||||
entityId: z.string().min(1),
|
||||
action: z.enum(['provision', 'update', 'deprovision', 'sync']),
|
||||
status: ScimEventStatusSchema,
|
||||
message: z.string().optional(),
|
||||
createdAt: z.string(),
|
||||
});
|
||||
|
||||
export type ScimProvisioningEventDoc = z.infer<typeof ScimProvisioningEventSchema> & {
|
||||
_ts?: number;
|
||||
_etag?: string;
|
||||
};
|
||||
|
||||
export const CreateScimConnectorSchema = z.object({
|
||||
orgId: z.string().min(1),
|
||||
idpId: z.string().optional(),
|
||||
name: z.string().min(1),
|
||||
baseUrl: z.string().url().optional(),
|
||||
authMode: z.enum(['bearer', 'oauth']).default('bearer'),
|
||||
metadata: z.record(z.unknown()).optional(),
|
||||
});
|
||||
|
||||
export const UpdateScimConnectorSchema = z.object({
|
||||
idpId: z.string().optional(),
|
||||
name: z.string().min(1).optional(),
|
||||
baseUrl: z.string().url().optional(),
|
||||
authMode: z.enum(['bearer', 'oauth']).optional(),
|
||||
status: ScimConnectorStatusSchema.optional(),
|
||||
metadata: z.record(z.unknown()).optional(),
|
||||
});
|
||||
|
||||
export const RecordScimUserSyncSchema = z.object({
|
||||
externalUserId: z.string().min(1),
|
||||
userId: z.string().optional(),
|
||||
email: z.string().email(),
|
||||
status: ScimUserSyncStatusSchema,
|
||||
errorMessage: z.string().optional(),
|
||||
metadata: z.record(z.unknown()).optional(),
|
||||
});
|
||||
|
||||
export const RecordScimGroupSyncSchema = z.object({
|
||||
externalGroupId: z.string().min(1),
|
||||
workspaceId: z.string().optional(),
|
||||
displayName: z.string().min(1),
|
||||
memberCount: z.number().int().min(0).default(0),
|
||||
status: ScimGroupSyncStatusSchema,
|
||||
errorMessage: z.string().optional(),
|
||||
metadata: z.record(z.unknown()).optional(),
|
||||
});
|
||||
|
||||
export const RecordScimEventSchema = z.object({
|
||||
entityType: z.enum(['user', 'group']),
|
||||
entityId: z.string().min(1),
|
||||
action: z.enum(['provision', 'update', 'deprovision', 'sync']),
|
||||
status: ScimEventStatusSchema,
|
||||
message: z.string().optional(),
|
||||
});
|
||||
@ -39,6 +39,7 @@ import { agentRoutes } from './modules/agents/routes.js';
|
||||
import { agentEvalRoutes } from './modules/agent-evals/routes.js';
|
||||
import { aiBudgetRoutes } from './modules/ai-budgets/routes.js';
|
||||
import { knowledgeRoutes } from './modules/knowledge/routes.js';
|
||||
import { scimRoutes } from './modules/scim/routes.js';
|
||||
import { notificationRoutes } from './modules/notifications/routes.js';
|
||||
import { flagRoutes } from './modules/flags/routes.js';
|
||||
import { rateLimitRoutes } from './modules/ratelimit/routes.js';
|
||||
@ -147,6 +148,7 @@ await app.register(agentRoutes, { prefix: '/api' });
|
||||
await app.register(agentEvalRoutes, { prefix: '/api' });
|
||||
await app.register(aiBudgetRoutes, { prefix: '/api' });
|
||||
await app.register(knowledgeRoutes, { prefix: '/api' });
|
||||
await app.register(scimRoutes, { prefix: '/api' });
|
||||
await app.register(notificationRoutes, { prefix: '/api' });
|
||||
await app.register(flagRoutes, { prefix: '/api' });
|
||||
await app.register(rateLimitRoutes, { prefix: '/api' });
|
||||
|
||||
Loading…
Reference in New Issue
Block a user