- wait_for_job: blocks ≤600s, claims QUEUED job atomically via FOR UPDATE SKIP LOCKED, resets stale CLAIMED jobs (>30min), registers ClaudeWorker presence with heartbeat, emits worker_connected/disconnected via NOTIFY - update_job_status: agent reports running|done|failed, validates token ownership (claimed_by_token_id), emits claude_job_status via NOTIFY - auth.ts extended with tokenId so tools can set claimed_by_token_id Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
53 lines
1.2 KiB
TypeScript
53 lines
1.2 KiB
TypeScript
import { createHash } from 'crypto'
|
|
import { prisma } from './prisma.js'
|
|
|
|
export type AuthContext = {
|
|
userId: string
|
|
tokenId: string
|
|
username: string
|
|
isDemo: boolean
|
|
}
|
|
|
|
let cached: AuthContext | null = null
|
|
|
|
export async function getAuth(): Promise<AuthContext> {
|
|
if (cached) return cached
|
|
|
|
const token = process.env.SCRUM4ME_TOKEN
|
|
if (!token) {
|
|
throw new Error('SCRUM4ME_TOKEN is not set — see .env.example')
|
|
}
|
|
|
|
const tokenHash = createHash('sha256').update(token).digest('hex')
|
|
const apiToken = await prisma.apiToken.findUnique({
|
|
where: { token_hash: tokenHash },
|
|
include: { user: true },
|
|
})
|
|
|
|
if (!apiToken || apiToken.revoked_at) {
|
|
throw new Error('SCRUM4ME_TOKEN is invalid or revoked')
|
|
}
|
|
|
|
cached = {
|
|
userId: apiToken.user_id,
|
|
tokenId: apiToken.id,
|
|
username: apiToken.user.username,
|
|
isDemo: apiToken.user.is_demo,
|
|
}
|
|
return cached
|
|
}
|
|
|
|
export class PermissionDeniedError extends Error {
|
|
constructor(message = 'Demo accounts cannot perform write operations') {
|
|
super(message)
|
|
this.name = 'PermissionDeniedError'
|
|
}
|
|
}
|
|
|
|
export async function requireWriteAccess(): Promise<AuthContext> {
|
|
const auth = await getAuth()
|
|
if (auth.isDemo) {
|
|
throw new PermissionDeniedError()
|
|
}
|
|
return auth
|
|
}
|