feat(ST-703): auth and Prisma client singleton

- src/prisma.ts: PrismaClient via PrismaPg adapter and pg.Pool,
  same pattern as Scrum4Me's lib/prisma.ts
- src/auth.ts: getAuth() resolves SCRUM4ME_TOKEN once, caches
  { userId, username, isDemo }. requireWriteAccess() throws
  PermissionDeniedError for demo tokens — write tools call this
  before any DB mutation

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-04-26 23:01:22 +02:00
parent 992a4ad5e1
commit 2b52b1cedd
2 changed files with 66 additions and 0 deletions

51
src/auth.ts Normal file
View file

@ -0,0 +1,51 @@
import { createHash } from 'crypto'
import { prisma } from './prisma.js'
export type AuthContext = {
userId: 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,
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
}

15
src/prisma.ts Normal file
View file

@ -0,0 +1,15 @@
import { PrismaClient } from '@prisma/client'
import { Pool } from 'pg'
import { PrismaPg } from '@prisma/adapter-pg'
function createClient(): PrismaClient {
const url = process.env.DATABASE_URL
if (!url) {
throw new Error('DATABASE_URL is not set — see .env.example')
}
const pool = new Pool({ connectionString: url })
const adapter = new PrismaPg(pool)
return new PrismaClient({ adapter, log: ['error'] })
}
export const prisma = createClient()