import { DomainError } from '@devrunbook/domain' import type { InstanceRole, WorkspaceRole, } from '../auth/workspace-authorization' import type { JobJsonValue, JobRecord, JobState } from '../jobs/job-queue' export interface OperationsActor { readonly userId: string readonly instanceRole: InstanceRole readonly workspaceId: string | null readonly workspaceRole: WorkspaceRole | null } export interface OperationsActorLookup { findOperationsActor(userId: string): Promise } export type OperationsScope = | { readonly kind: 'instance' } | { readonly kind: 'workspace'; readonly workspaceId: string } export interface OperationsJob { readonly id: string readonly workspaceId: string | null readonly type: string readonly state: JobState readonly progress: JobJsonValue readonly attemptCount: number readonly maxAttempts: number readonly errorCode: string | null readonly errorDetail: string | null readonly retryable: boolean readonly createdAt: Date readonly updatedAt: Date } export interface AuditEventRecord { readonly id: string readonly occurredAt: Date readonly actorUserId: string | null readonly workspaceId: string | null readonly action: string readonly resourceType: string readonly resourceId: string | null readonly outcome: 'success' | 'denied' | 'failed' readonly metadata: Readonly> } export interface OperationsPage { readonly items: readonly T[] readonly nextCursor: string | null } export interface OperationsStore { listJobs(request: { scope: OperationsScope cursor: string | null limit: number state?: JobState }): Promise> findJob(scope: OperationsScope, jobId: string): Promise retryJob(request: { scope: OperationsScope jobId: string actorUserId: string workspaceId: string | null }): Promise<'retried' | 'not-found' | 'not-retryable'> listAuditEvents(request: { scope: OperationsScope cursor: string | null limit: number action?: string workspaceId?: string }): Promise> } export function operationsActor(context: OperationsActor): OperationsActor { return context } function deny(): never { throw new DomainError( 'operations_access_denied', 'Operations access is not permitted', ) } export function authorizeOperations( actor: OperationsActor, action: 'read' | 'retry', ): OperationsScope { if ( actor.instanceRole === 'instance_owner' || actor.instanceRole === 'instance_admin' ) { return { kind: 'instance' } } if (!actor.workspaceId || !actor.workspaceRole) return deny() if ( action === 'retry' && actor.workspaceRole !== 'editor' && actor.workspaceRole !== 'owner' ) { return deny() } return { kind: 'workspace', workspaceId: actor.workspaceId } } function validLimit(limit: number): number { if (!Number.isInteger(limit) || limit < 1 || limit > 100) { throw new DomainError( 'operations_limit_invalid', 'Operations page limit must be an integer from 1 through 100', ) } return limit } export function listOperationsJobs( store: OperationsStore, actor: OperationsActor, request: { cursor?: string; limit?: number; state?: JobState } = {}, ) { return store.listJobs({ scope: authorizeOperations(actor, 'read'), cursor: request.cursor ?? null, limit: validLimit(request.limit ?? 25), ...(request.state ? { state: request.state } : {}), }) } export function getOperationsJob( store: OperationsStore, actor: OperationsActor, jobId: string, ) { return store.findJob(authorizeOperations(actor, 'read'), jobId) } export async function retryOperationsJob( store: OperationsStore, actor: OperationsActor, jobId: string, ): Promise { const result = await store.retryJob({ scope: authorizeOperations(actor, 'retry'), jobId, actorUserId: actor.userId, workspaceId: actor.workspaceId, }) if (result === 'not-found') { throw new DomainError('operations_job_not_found', 'Job was not found') } if (result === 'not-retryable') { throw new DomainError( 'operations_job_not_retryable', 'Only retryable terminal jobs can be retried', ) } } export function listOperationsAuditEvents( store: OperationsStore, actor: OperationsActor, request: { cursor?: string limit?: number action?: string workspaceId?: string } = {}, ) { const scope = authorizeOperations(actor, 'read') if ( scope.kind === 'workspace' && request.workspaceId !== undefined && request.workspaceId !== scope.workspaceId ) { return deny() } return store.listAuditEvents({ scope, cursor: request.cursor ?? null, limit: validLimit(request.limit ?? 25), ...(request.action ? { action: request.action } : {}), ...(request.workspaceId ? { workspaceId: request.workspaceId } : {}), }) } export function projectOperationsJob(job: JobRecord): OperationsJob { return { id: job.id, workspaceId: job.workspaceId, type: job.type, state: job.state, progress: job.progress, attemptCount: job.attemptCount, maxAttempts: job.maxAttempts, errorCode: job.errorCode, errorDetail: job.errorDetailRedacted, retryable: false, createdAt: job.createdAt, updatedAt: job.updatedAt, } }