Publish DevRunbook source
Managed validation / full (push) Successful in 3m18s

This commit is contained in:
DevRunbook release export
2026-09-03 04:09:17 +02:00
commit cfd2804e27
928 changed files with 161642 additions and 0 deletions
@@ -0,0 +1,206 @@
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<OperationsActor | null>
}
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<Record<string, unknown>>
}
export interface OperationsPage<T> {
readonly items: readonly T[]
readonly nextCursor: string | null
}
export interface OperationsStore {
listJobs(request: {
scope: OperationsScope
cursor: string | null
limit: number
state?: JobState
}): Promise<OperationsPage<OperationsJob>>
findJob(scope: OperationsScope, jobId: string): Promise<OperationsJob | null>
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<OperationsPage<AuditEventRecord>>
}
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<void> {
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,
}
}