import { fetchAllCursorPages } from "../../../common/core/pagination"; import apiClient from "./apiClient"; import { userManager } from "./auth"; export type AuditLog = { event_id: string; timestamp: string; user_id: string; event_type: string; status: string; ip_address: string; user_agent: string; device_id?: string; details?: string; }; export type AuditLogListResponse = { items: AuditLog[]; limit: number; cursor?: string; next_cursor?: string; }; export type TenantSummary = { id: string; type: string; // 허용 타입: PERSONAL, COMPANY, COMPANY_GROUP, ORGANIZATION, USER_GROUP name: string; slug: string; description: string; status: string; domains?: string[]; parentId?: string; config?: Record; memberCount: number; // Added member count createdAt: string; updatedAt: string; }; export type TenantCreateRequest = { name: string; type?: string; slug?: string; parentId?: string; description?: string; status?: string; domains?: string[]; forceDomainConflicts?: string[]; config?: Record; }; export type TenantListResponse = { items: TenantSummary[]; limit: number; offset: number; total: number; cursor?: string; nextCursor?: string; next_cursor?: string; }; export type TenantUpdateRequest = { name?: string; type?: string; slug?: string; parentId?: string; description?: string; status?: string; domains?: string[]; forceDomainConflicts?: string[]; config?: Record; }; export type TenantImportResult = { created: number; updated: number; failed: number; errors: string[]; }; export type ApiKeySummary = { id: string; name: string; client_id: string; scopes: string[]; status: string; lastUsedAt?: string; createdAt: string; }; export type ApiKeyListResponse = { items: ApiKeySummary[]; total: number; }; export type RoleSummary = { id: string; name: string; description: string; permissions: string[]; createdAt: string; updatedAt: string; }; export type RoleListResponse = { items: RoleSummary[]; total: number; }; export type RPUsageDailyMetric = { date: string; tenantId: string; tenantType: string; tenantName?: string; clientId: string; clientName: string; loginRequests: number; otherRequests: number; uniqueSubjects: number; }; export type RPUsagePeriod = "day" | "week" | "month"; export type RPUsageDailyResponse = { items: RPUsageDailyMetric[]; days: number; period: RPUsagePeriod; tenantId?: string; }; export type AdminOverviewStats = { totalTenants: number; totalUsers: number; oidcClients: number; auditEvents24h: number; }; export type UserProjectionStatus = { name: string; status: "ready" | "failed" | "syncing" | string; ready: boolean; lastSyncedAt?: string; lastError?: string; updatedAt?: string; projectedUsers: number; }; export type UserProjectionActionResult = { status: string; syncedUsers: number; updatedAt: string; }; export type DataIntegrityStatus = "pass" | "warning" | "fail"; export type DataIntegrityCheck = { key: string; label: string; description: string; status: DataIntegrityStatus; severity: "info" | "warning" | "error" | string; count: number; }; export type DataIntegritySection = { key: string; label: string; status: DataIntegrityStatus; checks: DataIntegrityCheck[]; }; export type DataIntegrityReport = { status: DataIntegrityStatus; checkedAt: string; summary: { totalChecks: number; passed: number; warnings: number; failures: number; }; sections: DataIntegritySection[]; }; export type OrphanUserLoginID = { id: string; userId: string; userEmail?: string; userDeletedAt?: string; tenantId: string; tenantSlug?: string; tenantDeletedAt?: string; fieldKey: string; loginId: string; reasons: string[]; }; export type OrphanUserLoginIDListResponse = { items: OrphanUserLoginID[]; total: number; }; export type DeleteOrphanUserLoginIDsResult = { deletedCount: number; deleted: OrphanUserLoginID[]; skippedIds: string[]; }; export async function fetchAuditLogs(limit = 50, cursor?: string) { const { data } = await apiClient.get("/v1/audit", { params: { limit, cursor }, }); return data; } export async function fetchAdminOverviewStats() { const { data } = await apiClient.get("/v1/admin/stats"); return data; } export async function fetchDataIntegrityReport() { const { data } = await apiClient.get( "/v1/admin/integrity", ); return data; } export async function fetchOrphanUserLoginIDs() { const { data } = await apiClient.get( "/v1/admin/integrity/orphan-user-login-ids", ); return data; } export async function deleteOrphanUserLoginIDs(ids: string[]) { const { data } = await apiClient.delete( "/v1/admin/integrity/orphan-user-login-ids", { data: { ids } }, ); return data; } export async function fetchUserProjectionStatus() { const { data } = await apiClient.get( "/v1/admin/projections/users", ); return data; } export async function reconcileUserProjection() { const { data } = await apiClient.post( "/v1/admin/projections/users/reconcile", ); return data; } export async function resetUserProjection() { const { data } = await apiClient.post( "/v1/admin/projections/users/reset", ); return data; } export async function fetchAdminRPUsageDaily({ days = 14, period = "day", tenantId, }: { days?: number; period?: RPUsagePeriod; tenantId?: string; } = {}) { const { data } = await apiClient.get( "/v1/admin/rp-usage/daily", { params: { days, period, tenantId: tenantId || undefined }, }, ); return data; } export async function fetchTenants( limit = 50, offset = 0, parentId?: string, cursor?: string, ) { const { data } = await apiClient.get( "/v1/admin/tenants", { params: { limit, offset, parentId, cursor }, }, ); return data; } function getAdminApiBaseUrl() { if ( (window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean }) ._IS_TEST_MODE ) { return "http://playwright-mock/api"; } return import.meta.env.VITE_ADMIN_API_BASE ?? "/api"; } async function buildAdminRequestHeaders() { const headers: Record = {}; const user = await userManager.getUser(); const sessionToken = user?.access_token || window.localStorage.getItem("admin_session"); if (sessionToken) { headers.Authorization = `Bearer ${sessionToken}`; } const tenantId = window.localStorage.getItem("admin_tenant"); if (tenantId) { headers["X-Tenant-ID"] = tenantId; } const isMockRoleEnabled = window.localStorage.getItem("X-Mock-Role-Enabled") === "true"; const mockRole = window.localStorage.getItem("X-Mock-Role"); if (isMockRoleEnabled && mockRole) { headers["X-Test-Role"] = mockRole; } return headers; } export async function fetchAllTenants({ pageSize = 100, parentId, }: { pageSize?: number; parentId?: string; } = {}) { return fetchAllCursorPages({ baseUrl: getAdminApiBaseUrl(), path: "/v1/admin/tenants", pageSize, params: { parentId }, headers: await buildAdminRequestHeaders(), }) as Promise; } export async function fetchTenant(tenantId: string) { const { data } = await apiClient.get( `/v1/admin/tenants/${tenantId}`, ); return data; } export async function createTenant(payload: TenantCreateRequest) { const { data } = await apiClient.post( "/v1/admin/tenants", payload, ); return data; } export async function updateTenant( tenantId: string, payload: TenantUpdateRequest, ) { const { data } = await apiClient.put( `/v1/admin/tenants/${tenantId}`, payload, ); return data; } export async function deleteTenant(tenantId: string) { await apiClient.delete(`/v1/admin/tenants/${tenantId}`); } export async function deleteTenantsBulk(ids: string[]) { await apiClient.delete("/v1/admin/tenants/bulk", { data: { ids }, }); } export async function exportTenantsCSV(includeIds = false, parentId?: string) { const response = await apiClient.get("/v1/admin/tenants/export", { params: { includeIds, parentId: parentId || undefined }, responseType: "blob", }); const dispositionHeader = response.headers["content-disposition"]; const disposition = Array.isArray(dispositionHeader) ? dispositionHeader[0] : String(dispositionHeader ?? ""); const filenameMatch = disposition?.match(/filename="?([^"]+)"?/i); return { blob: response.data, filename: filenameMatch?.[1] ?? "tenants.csv", }; } export async function importTenantsCSV(file: File) { const formData = new FormData(); formData.append("file", file); const { data } = await apiClient.post( "/v1/admin/tenants/import", formData, { headers: { "Content-Type": "multipart/form-data", }, }, ); return data; } export async function approveTenant(tenantId: string) { const { data } = await apiClient.post( `/v1/admin/tenants/${tenantId}/approve`, ); return data; } export type TenantAdmin = { id: string; name: string; email: string; }; export async function fetchTenantAdmins(tenantId: string) { const { data } = await apiClient.get( `/v1/admin/tenants/${tenantId}/admins`, ); return data; } export async function addTenantAdmin(tenantId: string, userId: string) { await apiClient.post(`/v1/admin/tenants/${tenantId}/admins/${userId}`); } export async function removeTenantAdmin(tenantId: string, userId: string) { await apiClient.delete(`/v1/admin/tenants/${tenantId}/admins/${userId}`); } export async function fetchTenantOwners(tenantId: string) { const { data } = await apiClient.get( `/v1/admin/tenants/${tenantId}/owners`, ); return data; } export async function addTenantOwner(tenantId: string, userId: string) { await apiClient.post(`/v1/admin/tenants/${tenantId}/owners/${userId}`); } export async function removeTenantOwner(tenantId: string, userId: string) { await apiClient.delete(`/v1/admin/tenants/${tenantId}/owners/${userId}`); } // Group Management export type GroupMember = { id: string; name: string; email: string; }; export type GroupSummary = { id: string; tenantId: string; parentId?: string; name: string; description?: string; unitType?: string; members?: GroupMember[]; createdAt?: string; updatedAt?: string; }; export type GroupCreateRequest = { name: string; parentId?: string; description?: string; unitType?: string; }; export async function fetchGroups(tenantId: string) { const { data } = await apiClient.get( `/v1/admin/tenants/${tenantId}/organization`, ); return data; } export async function fetchGroup(tenantId: string, groupId: string) { const { data } = await apiClient.get( `/v1/admin/tenants/${tenantId}/organization/${groupId}`, ); return data; } export async function createGroup( tenantId: string, payload: GroupCreateRequest, ) { const { data } = await apiClient.post( `/v1/admin/tenants/${tenantId}/organization`, payload, ); return data; } export async function deleteGroup(tenantId: string, groupId: string) { await apiClient.delete( `/v1/admin/tenants/${tenantId}/organization/${groupId}`, ); } export async function addGroupMember( tenantId: string, groupId: string, userId: string, ) { await apiClient.post( `/v1/admin/tenants/${tenantId}/organization/${groupId}/members`, { userId }, ); } export async function removeGroupMember( tenantId: string, groupId: string, userId: string, ) { await apiClient.delete( `/v1/admin/tenants/${tenantId}/organization/${groupId}/members/${userId}`, ); } export type GroupRole = { tenantId: string; tenantName: string; relation: string; }; export async function fetchGroupRoles(tenantId: string, groupId: string) { const { data } = await apiClient.get( `/v1/admin/tenants/${tenantId}/organization/${groupId}/roles`, ); return data; } export async function assignGroupRole( tenantId: string, groupId: string, targetTenantId: string, relation: string, ) { await apiClient.post( `/v1/admin/tenants/${tenantId}/organization/${groupId}/roles`, { tenantId: targetTenantId, relation }, ); } export async function removeGroupRole( tenantId: string, groupId: string, targetTenantId: string, relation: string, ) { await apiClient.delete( `/v1/admin/tenants/${tenantId}/organization/${groupId}/roles/${targetTenantId}/${relation}`, ); } // API Key Management (M2M) export type ApiKeyCreateRequest = { name: string; scopes: string[]; }; export type ApiKeyCreateResponse = { apiKey: ApiKeySummary; clientSecret: string; }; export type ApiKeyUpdateScopesRequest = { scopes: string[]; }; export async function fetchApiKeys(limit = 50, offset = 0) { const { data } = await apiClient.get( "/v1/admin/api-keys", { params: { limit, offset }, }, ); return data; } export async function createApiKey(payload: ApiKeyCreateRequest) { const { data } = await apiClient.post( "/v1/admin/api-keys", payload, ); return data; } export async function updateApiKeyScopes( apiKeyId: string, payload: ApiKeyUpdateScopesRequest, ) { const { data } = await apiClient.patch( `/v1/admin/api-keys/${apiKeyId}`, payload, ); return data; } export async function rotateApiKeySecret(apiKeyId: string) { const { data } = await apiClient.post( `/v1/admin/api-keys/${apiKeyId}/secret/rotate`, ); return data; } export async function deleteApiKey(apiKeyId: string) { await apiClient.delete(`/v1/admin/api-keys/${apiKeyId}`); } // User Management export type UserSummary = { id: string; email: string; loginId?: string; name: string; phone?: string; role: string; status: string; tenantSlug?: string; companyCode?: string; tenant?: TenantSummary; joinedTenants?: TenantSummary[]; // [New] 다중 소속 테넌트 목록 metadata?: Record; department?: string; grade?: string; position?: string; jobTitle?: string; createdAt: string; updatedAt: string; }; export type UserListResponse = { items: UserSummary[]; limit: number; offset: number; total: number; }; export type UserCreateRequest = { email: string; loginId?: string; password?: string; name: string; phone?: string; role?: string; tenantSlug?: string; department?: string; grade?: string; position?: string; jobTitle?: string; primaryTenantId?: string; primaryTenantName?: string; primaryTenantIsOwner?: boolean; additionalAppointments?: UserAppointment[]; metadata?: Record; }; export type UserCreateResponse = UserSummary & { initialPassword?: string; }; export type UserUpdateRequest = { email?: string; loginId?: string; password?: string; name?: string; phone?: string; role?: string; status?: string; tenantSlug?: string; isAddTenant?: boolean; isRemoveTenant?: boolean; department?: string; grade?: string; position?: string; jobTitle?: string; primaryTenantId?: string; primaryTenantName?: string; primaryTenantIsOwner?: boolean; additionalAppointments?: UserAppointment[]; metadata?: Record; }; export type UserAppointment = { tenantId: string; tenantSlug?: string; tenantName: string; isPrimary?: boolean; isOwner?: boolean; isAdmin?: boolean; isManager?: boolean; jobTitle?: string; grade?: string; position?: string; }; export type BulkUserAppointment = { tenantId?: string; tenantSlug?: string; tenantName?: string; isPrimary?: boolean; isOwner?: boolean; isAdmin?: boolean; isManager?: boolean; department?: string; grade?: string; position?: string; jobTitle?: string; metadata?: Record; }; export type BulkUserItem = { email: string; loginId?: string; name: string; phone?: string; role?: string; tenantId?: string; tenantSlug?: string; emailDomain?: string; department?: string; grade?: string; position?: string; jobTitle?: string; additionalAppointments?: BulkUserAppointment[]; tenantImport?: { sourceTenantId?: string; slug?: string; name?: string; type?: string; parentTenantId?: string; parentTenantSlug?: string; parentTenantName?: string; memo?: string; emailDomain?: string; }; metadata: Record; importErrors?: string[]; }; export type BulkUserResult = { email: string; originalEmail?: string; suggestedEmail?: string; status?: string; warnings?: string[]; success: boolean; message?: string; userId?: string; modifiedFields?: string[]; }; export type BulkUserResponse = { results: BulkUserResult[]; }; export type WorksmobileOutboxItem = { id: string; resourceType: string; resourceId: string; action: string; payload?: Record; status: string; retryCount: number; lastError?: string; createdAt: string; updatedAt: string; }; export type WorksmobileOverview = { tenant: TenantSummary; config: { enabled: boolean; domainMappings?: Record; tokenConfigured: boolean; adminTenantId?: string; }; recentJobs: WorksmobileOutboxItem[]; }; export type WorksmobileCredentialBatch = { batchId: string; operation?: string; userCount: number; pendingCount?: number; processingCount?: number; processedCount?: number; failedCount?: number; hasPasswords: boolean; deletedAt?: string; failures?: WorksmobileCredentialBatchFailure[]; createdAt?: string; updatedAt?: string; }; export type WorksmobileCredentialBatchFailure = { userId?: string; email?: string; status: string; retryCount: number; lastError?: string; updatedAt?: string; }; export type WorksmobileComparisonItem = { resourceType: string; baronId?: string; baronSlug?: string; baronName?: string; baronEmail?: string; baronPrimaryOrgId?: string; baronPrimaryOrgSlug?: string; baronPrimaryOrgName?: string; baronParentId?: string; baronParentSlug?: string; baronParentName?: string; worksmobileId?: string; externalKey?: string; worksmobileName?: string; worksmobileEmail?: string; worksmobileLevelId?: string; worksmobileLevelName?: string; worksmobileTask?: string; worksmobileDomainId?: number; worksmobileDomainName?: string; worksmobilePrimaryOrgId?: string; worksmobilePrimaryOrgName?: string; worksmobilePrimaryOrgPositionId?: string; worksmobilePrimaryOrgPositionName?: string; worksmobilePrimaryOrgIsManager?: boolean; baronParentWorksmobileId?: string; baronParentWorksmobileName?: string; baronParentWorksmobileEmail?: string; worksmobileParentId?: string; worksmobileParentName?: string; worksmobileParentEmail?: string; worksmobileParentExternalKey?: string; worksmobileJobStatus?: string; worksmobileJobRetryCount?: number; worksmobileLastError?: string; worksmobileLastAttemptAt?: string; status: string; }; export type WorksmobileComparison = { users: WorksmobileComparisonItem[]; groups: WorksmobileComparisonItem[]; }; export async function fetchUsers( limit = 50, offset = 0, search?: string, tenantSlug?: string, ) { const { data } = await apiClient.get("/v1/admin/users", { params: { limit, offset, search, tenantSlug }, }); return data; } export async function fetchUser(userId: string) { const { data } = await apiClient.get( `/v1/admin/users/${userId}`, ); return data; } export async function createUser(payload: UserCreateRequest) { const { data } = await apiClient.post( "/v1/admin/users", payload, ); return data; } export async function exportUsersCSV( search?: string, tenantSlug?: string, includeIds = false, ) { const response = await apiClient.get("/v1/admin/users/export", { params: { search, tenantSlug, includeIds }, responseType: "blob", }); const dispositionHeader = response.headers["content-disposition"]; const disposition = Array.isArray(dispositionHeader) ? dispositionHeader[0] : String(dispositionHeader ?? ""); const filenameMatch = disposition?.match(/filename="?([^"]+)"?/i); return { blob: response.data, filename: filenameMatch?.[1] ?? "users.csv", }; } export async function bulkCreateUsers(users: BulkUserItem[]) { const { data } = await apiClient.post( "/v1/admin/users/bulk", { users }, ); return data; } export async function fetchWorksmobileOverview(tenantId: string) { const { data } = await apiClient.get( `/v1/admin/tenants/${tenantId}/worksmobile`, ); return data; } export async function fetchWorksmobileComparison( tenantId: string, includeMatched = false, ) { const { data } = await apiClient.get( `/v1/admin/tenants/${tenantId}/worksmobile/comparison`, { params: { includeMatched }, }, ); return data; } export async function fetchWorksmobileCredentialBatches(tenantId: string) { const { data } = await apiClient.get( `/v1/admin/tenants/${tenantId}/worksmobile/credential-batches`, ); return data; } export async function downloadWorksmobileInitialPasswordsCSV( tenantId: string, batchId?: string, ) { const trimmedBatchId = batchId?.trim(); const response = await apiClient.get( `/v1/admin/tenants/${tenantId}/worksmobile/initial-passwords.csv`, { ...(trimmedBatchId ? { params: { batchId: trimmedBatchId } } : {}), responseType: "blob", }, ); const dispositionHeader = response.headers["content-disposition"]; const disposition = Array.isArray(dispositionHeader) ? dispositionHeader[0] : String(dispositionHeader ?? ""); const filenameMatch = disposition?.match(/filename="?([^"]+)"?/i); return { blob: response.data, filename: filenameMatch?.[1] ?? "worksmobile_initial_passwords.csv", }; } export async function deleteWorksmobileCredentialBatchPasswords( tenantId: string, batchId: string, ) { const { data } = await apiClient.delete( `/v1/admin/tenants/${tenantId}/worksmobile/credential-batches/${encodeURIComponent(batchId)}/passwords`, ); return data; } export async function enqueueWorksmobileBackfillDryRun(tenantId: string) { const { data } = await apiClient.post( `/v1/admin/tenants/${tenantId}/worksmobile/backfill/dry-run`, ); return data; } export async function enqueueWorksmobileOrgUnitSync( tenantId: string, orgUnitId: string, ) { const { data } = await apiClient.post( `/v1/admin/tenants/${tenantId}/worksmobile/orgunits/${encodeURIComponent(orgUnitId)}/sync`, ); return data; } export async function enqueueWorksmobileOrgUnitDelete( tenantId: string, orgUnitId: string, ) { const { data } = await apiClient.post( `/v1/admin/tenants/${tenantId}/worksmobile/orgunits/${encodeURIComponent(orgUnitId)}/delete`, ); return data; } export async function enqueueWorksmobileUserSync( tenantId: string, userId: string, credentialBatchId?: string, ) { const trimmedBatchId = credentialBatchId?.trim(); const path = `/v1/admin/tenants/${tenantId}/worksmobile/users/${userId}/sync`; const { data } = trimmedBatchId ? await apiClient.post(path, { credentialBatchId: trimmedBatchId, }) : await apiClient.post(path); return data; } export async function resetWorksmobileUserPassword( tenantId: string, userId: string, credentialBatchId?: string, ) { const trimmedBatchId = credentialBatchId?.trim(); const path = `/v1/admin/tenants/${tenantId}/worksmobile/users/${userId}/password/reset`; const { data } = trimmedBatchId ? await apiClient.post(path, { credentialBatchId: trimmedBatchId, }) : await apiClient.post(path); return data; } export async function retryWorksmobileJob(tenantId: string, jobId: string) { const { data } = await apiClient.post( `/v1/admin/tenants/${tenantId}/worksmobile/jobs/${jobId}/retry`, ); return data; } export async function bulkUpdateUsers(payload: { userIds: string[]; status?: string; role?: string; tenantSlug?: string; department?: string; position?: string; grade?: string; jobTitle?: string; }) { const { data } = await apiClient.put("/v1/admin/users/bulk", payload); return data; } export async function bulkDeleteUsers(userIds: string[]) { const { data } = await apiClient.delete("/v1/admin/users/bulk", { data: { userIds }, }); return data; } export async function updateUser(userId: string, payload: UserUpdateRequest) { const { data } = await apiClient.put( `/v1/admin/users/${userId}`, payload, ); return data; } export type PasswordPolicyResponse = { minLength?: number; lowercase?: boolean; uppercase?: boolean; number?: boolean; nonAlphanumeric?: boolean; minCharacterTypes?: number; }; export async function fetchPasswordPolicy() { const { data } = await apiClient.get( "/v1/auth/password/policy", ); return data; } export async function deleteUser(userId: string) { await apiClient.delete(`/v1/admin/users/${userId}`); } export type UserRpHistoryItem = { client_id: string; client_name: string; lastLoginAt: string; status: string; }; export async function fetchUserRpHistory(userId: string) { const { data } = await apiClient.get( `/v1/admin/users/${userId}/rp-history`, ); return data; } export type UserProfileResponse = { id: string; email: string; name: string; phone: string; role: string; department: string; affiliationType: string; tenantSlug?: string; tenantId?: string; metadata?: Record; tenant?: TenantSummary; manageableTenants?: TenantSummary[]; }; export async function fetchMe() { const { data } = await apiClient.get("/v1/user/me"); return data; } // Relying Party Management export type RelyingParty = { clientId: string; tenantId: string; name: string; description: string; createdAt: string; updatedAt: string; }; export type HydraClientReq = { client_id?: string; client_name: string; client_secret?: string; redirect_uris: string[]; scope?: string; token_endpoint_auth_method?: string; grant_types?: string[]; response_types?: string[]; metadata?: Record; }; export async function fetchRelyingParties(tenantId: string) { const { data } = await apiClient.get( `/v1/admin/tenants/${tenantId}/relying-parties`, ); return data; } export async function fetchAllRelyingParties() { const { data } = await apiClient.get( "/v1/admin/relying-parties", ); return data; } export async function createRelyingParty( tenantId: string, payload: HydraClientReq, ) { const { data } = await apiClient.post( `/v1/admin/tenants/${tenantId}/relying-parties`, payload, ); return data; } export async function fetchRelyingParty(id: string) { const { data } = await apiClient.get<{ relyingParty: RelyingParty; oauth2Config: HydraClientReq; }>(`/v1/admin/relying-parties/${id}`); return data; } export async function updateRelyingParty(id: string, payload: HydraClientReq) { const { data } = await apiClient.put( `/v1/admin/relying-parties/${id}`, payload, ); return data; } export async function deleteRelyingParty(id: string) { await apiClient.delete(`/v1/admin/relying-parties/${id}`); } export type RPOwner = { subject: string; name?: string; email?: string; type: string; }; export async function fetchRPOwners(clientId: string) { const { data } = await apiClient.get( `/v1/admin/relying-parties/${clientId}/owners`, ); return data; } export async function addRPOwner(clientId: string, subject: string) { await apiClient.post( `/v1/admin/relying-parties/${clientId}/owners/${subject}`, ); } export async function removeRPOwner(clientId: string, subject: string) { await apiClient.delete( `/v1/admin/relying-parties/${clientId}/owners/${subject}`, ); }