Full management surfaces for the platform-admin tenant, mirroring the existing Tenants pattern (DataTable + row actions + create/edit dialogs + ConfirmDialog for destructive ops, all data-action tagged for the command bus, useRegisterAdminContext publishing for the assistant). - Storage (/storage): backends + credentials. Write-only secret fields, Validate/Activate/Deactivate/Set-default/Mark-degraded/Maintenance. - Users (/users): tabs for Users, Invitations, Roles. Per-user View drawer with profile, role add/remove, API keys (one-time reveal on create), usage + quota. - Secrets (/secrets): /api/v1/admin/secrets — create/rotate/rollback, versions dialog, enable/disable, generate-value helper. - Webhooks (/webhooks): CRUD, pause/resume, regenerate-secret with one-time reveal, send test event, deliveries dialog. - Scheduled tasks (/scheduled-tasks): cron CRUD, run-now trigger, enable/disable, expandable run history. - Audit log (/activity): replaces the empty stub. Filter by severity, resource type, date range; click for full JSON detail. All endpoints are hand-rolled HTTP because most aren't covered by the generated OpenAPI typed paths yet — switch to arcadia.typed.* when the backend wires them into OpenApiSpex. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
172 lines
4.7 KiB
TypeScript
172 lines
4.7 KiB
TypeScript
// Arcadia secrets API helpers.
|
|
//
|
|
// Backed by /api/v1/admin/secrets — the platform Secrets Manager. Values are
|
|
// AES-encrypted at rest and never returned by index/show; only metadata is
|
|
// exposed by these endpoints. Tenant-side resolution (returning the value)
|
|
// goes through a separate runtime endpoint that's not used by the admin UI.
|
|
|
|
import type { ArcadiaClient } from "@crema/arcadia-client"
|
|
|
|
export type SecretCategory =
|
|
| "api_key"
|
|
| "smtp"
|
|
| "oauth_token"
|
|
| "webhook_secret"
|
|
| "generic"
|
|
|
|
export type SecretEnvironment = "production" | "staging" | "development" | "all"
|
|
|
|
export interface Secret {
|
|
id: string
|
|
tenant_id: string | null
|
|
name: string
|
|
description: string | null
|
|
category: SecretCategory
|
|
environment: SecretEnvironment
|
|
tags: string[]
|
|
used_by: string[]
|
|
allowed_ips: string[]
|
|
read_once: boolean
|
|
read_once_consumed: boolean
|
|
expires_at: string | null
|
|
last_rotated_at: string | null
|
|
rotation_interval_days: number | null
|
|
rotation_due: boolean
|
|
expired: boolean
|
|
enabled: boolean
|
|
inserted_at: string
|
|
updated_at: string
|
|
}
|
|
|
|
export interface SecretVersion {
|
|
id: string
|
|
secret_id: string
|
|
version: number
|
|
note: string | null
|
|
inserted_by: string | null
|
|
inserted_at: string
|
|
}
|
|
|
|
export interface SecretCreateInput {
|
|
name: string
|
|
value: string
|
|
category?: SecretCategory
|
|
description?: string | null
|
|
environment?: SecretEnvironment
|
|
tags?: string[]
|
|
used_by?: string[]
|
|
allowed_ips?: string[]
|
|
read_once?: boolean
|
|
expires_at?: string | null
|
|
rotation_interval_days?: number | null
|
|
}
|
|
|
|
export type SecretMetaInput = Omit<Partial<SecretCreateInput>, "value" | "name">
|
|
|
|
export interface RotateInput {
|
|
value: string
|
|
note?: string
|
|
}
|
|
|
|
export async function listSecrets(arcadia: ArcadiaClient): Promise<Secret[]> {
|
|
const res = await arcadia.GET<{ data: Secret[] }>("/api/v1/admin/secrets")
|
|
return res.data
|
|
}
|
|
|
|
export async function getSecret(arcadia: ArcadiaClient, id: string): Promise<Secret> {
|
|
const res = await arcadia.GET<{ data: Secret }>(`/api/v1/admin/secrets/${id}`)
|
|
return res.data
|
|
}
|
|
|
|
export async function createSecret(
|
|
arcadia: ArcadiaClient,
|
|
input: SecretCreateInput,
|
|
): Promise<Secret> {
|
|
const res = await arcadia.POST<{ data: Secret }>("/api/v1/admin/secrets", {
|
|
body: { secret: input },
|
|
})
|
|
return res.data
|
|
}
|
|
|
|
export async function updateSecretMeta(
|
|
arcadia: ArcadiaClient,
|
|
id: string,
|
|
input: SecretMetaInput,
|
|
): Promise<Secret> {
|
|
const res = await arcadia.PATCH<{ data: Secret }>(`/api/v1/admin/secrets/${id}`, {
|
|
body: { secret: input },
|
|
})
|
|
return res.data
|
|
}
|
|
|
|
export async function deleteSecret(arcadia: ArcadiaClient, id: string): Promise<void> {
|
|
await arcadia.DELETE(`/api/v1/admin/secrets/${id}`)
|
|
}
|
|
|
|
export async function rotateSecret(
|
|
arcadia: ArcadiaClient,
|
|
id: string,
|
|
input: RotateInput,
|
|
): Promise<Secret> {
|
|
const res = await arcadia.POST<{ data: Secret }>(`/api/v1/admin/secrets/${id}/rotate`, {
|
|
body: { secret: input },
|
|
})
|
|
return res.data
|
|
}
|
|
|
|
export async function rollbackSecret(
|
|
arcadia: ArcadiaClient,
|
|
id: string,
|
|
version: number,
|
|
): Promise<Secret> {
|
|
const res = await arcadia.POST<{ data: Secret }>(
|
|
`/api/v1/admin/secrets/${id}/rollback/${version}`,
|
|
)
|
|
return res.data
|
|
}
|
|
|
|
export async function enableSecret(arcadia: ArcadiaClient, id: string): Promise<Secret> {
|
|
const res = await arcadia.POST<{ data: Secret }>(`/api/v1/admin/secrets/${id}/enable`)
|
|
return res.data
|
|
}
|
|
|
|
export async function disableSecret(arcadia: ArcadiaClient, id: string): Promise<Secret> {
|
|
const res = await arcadia.POST<{ data: Secret }>(`/api/v1/admin/secrets/${id}/disable`)
|
|
return res.data
|
|
}
|
|
|
|
export async function listSecretVersions(
|
|
arcadia: ArcadiaClient,
|
|
id: string,
|
|
): Promise<SecretVersion[]> {
|
|
const res = await arcadia.GET<{ data: SecretVersion[] }>(
|
|
`/api/v1/admin/secrets/${id}/versions`,
|
|
)
|
|
return res.data
|
|
}
|
|
|
|
export async function generateSecretValue(
|
|
arcadia: ArcadiaClient,
|
|
params?: { length?: number; charset?: string },
|
|
): Promise<string> {
|
|
const res = await arcadia.GET<{ data: { value: string } }>("/api/v1/admin/secrets/generate", {
|
|
params: params as Record<string, string | number | boolean | null | undefined>,
|
|
})
|
|
return res.data.value
|
|
}
|
|
|
|
export const SECRET_CATEGORIES: { value: SecretCategory; label: string }[] = [
|
|
{ value: "api_key", label: "API key" },
|
|
{ value: "oauth_token", label: "OAuth token" },
|
|
{ value: "smtp", label: "SMTP credentials" },
|
|
{ value: "webhook_secret", label: "Webhook secret" },
|
|
{ value: "generic", label: "Generic" },
|
|
]
|
|
|
|
export const SECRET_ENVIRONMENTS: { value: SecretEnvironment; label: string }[] = [
|
|
{ value: "all", label: "All environments" },
|
|
{ value: "production", label: "Production" },
|
|
{ value: "staging", label: "Staging" },
|
|
{ value: "development", label: "Development" },
|
|
]
|