Files
wehub-resource-sync d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

213 lines
7.3 KiB
TypeScript

import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
export type MothershipEnv = 'default' | 'dev' | 'staging' | 'prod'
const BASE = '/api/admin/mothership'
/**
* Same-origin proxy to the mothership admin API. Both the request body and
* the response shape vary per upstream `endpoint` query parameter, so a
* single contract cannot capture the union; the proxy returns the upstream
* JSON verbatim. `requestJson` would force a fixed response schema, so this
* hook stays on raw `fetch` and surfaces upstream errors through `adminError`.
*/
async function mothershipPost(
endpoint: string,
environment: MothershipEnv,
body?: Record<string, unknown>,
signal?: AbortSignal
) {
const qs = new URLSearchParams({ env: environment, endpoint })
// boundary-raw-fetch: same-origin proxy whose response shape varies per upstream endpoint
const res = await fetch(`${BASE}?${qs.toString()}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
...(body ? { body: JSON.stringify(body) } : {}),
signal,
})
if (!res.ok) {
const err = await res.json().catch(() => ({ error: res.statusText }))
throw new Error(err.message || err.error || `Request failed (${res.status})`)
}
return res.json()
}
/**
* Same-origin proxy GET for the mothership admin API. See `mothershipPost`
* for the rationale on staying with raw `fetch`.
*/
async function mothershipGet(
endpoint: string,
environment: MothershipEnv,
params?: Record<string, string>,
signal?: AbortSignal
) {
const qs = new URLSearchParams({ env: environment, endpoint, ...params })
// boundary-raw-fetch: same-origin proxy whose response shape varies per upstream endpoint
const res = await fetch(`${BASE}?${qs.toString()}`, { method: 'GET', signal })
if (!res.ok) {
const err = await res.json().catch(() => ({ error: res.statusText }))
throw new Error(err.message || err.error || `Request failed (${res.status})`)
}
return res.json()
}
// Enterprise BYOK does NOT use the cross-env admin proxy. It talks to the
// workspace's own copilot (SIM_AGENT_API_URL — local in dev, prod copilot in
// prod) via a dedicated same-origin route that authenticates with the hosted
// internal key. So it always targets the copilot the mothership actually runs
// on, never a deployed dev/staging URL.
const BYOK_BASE = '/api/copilot/byok'
async function byokFetch(url: string, init?: RequestInit) {
// boundary-raw-fetch: thin same-origin proxy to copilot; response shape is the
// upstream copilot JSON.
const res = await fetch(url, init)
if (!res.ok) {
const err = await res.json().catch(() => ({ error: res.statusText }))
throw new Error(err.message || err.error || `Request failed (${res.status})`)
}
return res.json()
}
export const MOTHERSHIP_BYOK_STALE_TIME = 30 * 1000
export const MOTHERSHIP_REQUESTS_STALE_TIME = 60 * 1000
export const MOTHERSHIP_USER_BREAKDOWN_STALE_TIME = 60 * 1000
export const MOTHERSHIP_LICENSE_LIST_STALE_TIME = 60 * 1000
export const MOTHERSHIP_LICENSE_DETAIL_STALE_TIME = 60 * 1000
export const mothershipKeys = {
all: ['mothership-admin'] as const,
requests: (env: MothershipEnv, start: string, end: string, userId?: string) =>
[...mothershipKeys.all, 'requests', env, start, end, userId] as const,
userBreakdown: (env: MothershipEnv, start: string, end: string) =>
[...mothershipKeys.all, 'user-breakdown', env, start, end] as const,
licenses: (env: MothershipEnv) => [...mothershipKeys.all, 'licenses', env] as const,
licenseDetails: (env: MothershipEnv, id?: string, name?: string) =>
[...mothershipKeys.all, 'license-details', env, id, name] as const,
byok: (workspaceId: string) => [...mothershipKeys.all, 'byok', workspaceId] as const,
}
export interface MothershipByokKey {
provider: string
keyLastFour: string
createdBy: string
createdAt: string
updatedAt: string
}
/** List the enterprise BYOK keys stored for a workspace (metadata only). */
export function useMothershipByokKeys(workspaceId: string) {
return useQuery({
queryKey: mothershipKeys.byok(workspaceId),
queryFn: ({ signal }) =>
byokFetch(`${BYOK_BASE}?workspaceId=${encodeURIComponent(workspaceId)}`, { signal }),
enabled: !!workspaceId,
staleTime: MOTHERSHIP_BYOK_STALE_TIME,
})
}
/** Store (or replace) a workspace's enterprise BYOK key for a provider. */
export function useUpsertMothershipByok() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (params: { workspaceId: string; provider: string; apiKey: string }) =>
byokFetch(BYOK_BASE, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
}),
onSettled: (_data, _error, params) =>
queryClient.invalidateQueries({ queryKey: mothershipKeys.byok(params.workspaceId) }),
})
}
/** Delete a workspace's enterprise BYOK key for a provider. */
export function useDeleteMothershipByok() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (params: { workspaceId: string; provider: string }) =>
byokFetch(
`${BYOK_BASE}?${new URLSearchParams({
workspaceId: params.workspaceId,
provider: params.provider,
}).toString()}`,
{ method: 'DELETE' }
),
onSettled: (_data, _error, params) =>
queryClient.invalidateQueries({ queryKey: mothershipKeys.byok(params.workspaceId) }),
})
}
export function useMothershipRequests(
environment: MothershipEnv,
start: string,
end: string,
userId?: string
) {
return useQuery({
queryKey: mothershipKeys.requests(environment, start, end, userId),
queryFn: ({ signal }) =>
mothershipPost(
'requests',
environment,
{
start,
end,
...(userId ? { userId } : {}),
},
signal
),
enabled: !!start && !!end,
staleTime: MOTHERSHIP_REQUESTS_STALE_TIME,
placeholderData: keepPreviousData,
})
}
export function useMothershipUserBreakdown(environment: MothershipEnv, start: string, end: string) {
return useQuery({
queryKey: mothershipKeys.userBreakdown(environment, start, end),
queryFn: ({ signal }) => mothershipPost('user-breakdown', environment, { start, end }, signal),
enabled: !!start && !!end,
staleTime: MOTHERSHIP_USER_BREAKDOWN_STALE_TIME,
placeholderData: keepPreviousData,
})
}
export function useMothershipLicenses(environment: MothershipEnv) {
return useQuery({
queryKey: mothershipKeys.licenses(environment),
queryFn: ({ signal }) => mothershipGet('licenses', environment, undefined, signal),
staleTime: MOTHERSHIP_LICENSE_LIST_STALE_TIME,
})
}
export function useMothershipLicenseDetails(
environment: MothershipEnv,
id?: string,
name?: string
) {
return useQuery({
queryKey: mothershipKeys.licenseDetails(environment, id, name),
queryFn: ({ signal }) =>
mothershipPost(
'licenses/details',
environment,
{
...(id ? { id } : {}),
...(name ? { name } : {}),
},
signal
),
enabled: !!(id || name),
staleTime: MOTHERSHIP_LICENSE_DETAIL_STALE_TIME,
})
}
export function useGenerateLicense(environment: MothershipEnv) {
return useMutation({
mutationFn: (params: { name: string; expirationDate?: string }) =>
mothershipPost('licenses/generate', environment, params),
})
}