From 7e55f07689f2f3d25409bb75ef8a06f9230c3026 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 31 May 2026 11:06:21 +1000 Subject: [PATCH] feat(auth): owner-only middleware for single-user bearer auth --- lib/auth/owner.js | 17 +++++++++++++++ tests/auth/owner.test.js | 47 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 lib/auth/owner.js create mode 100644 tests/auth/owner.test.js diff --git a/lib/auth/owner.js b/lib/auth/owner.js new file mode 100644 index 0000000..cefb44a --- /dev/null +++ b/lib/auth/owner.js @@ -0,0 +1,17 @@ +export function ownerOnly(req, res, next) { + const expected = process.env.OWNER_TOKEN; + if (!expected) { + return res.status(500).json({ + error: { code: 'no_owner_token', message: 'OWNER_TOKEN not configured' } + }); + } + const auth = req.headers.authorization || ''; + const [scheme, token] = auth.split(' '); + if (scheme !== 'Bearer' || token !== expected) { + return res.status(401).json({ + error: { code: 'unauthorized', message: 'invalid token' } + }); + } + req.actor = { kind: 'user', id: null }; + next(); +} diff --git a/tests/auth/owner.test.js b/tests/auth/owner.test.js new file mode 100644 index 0000000..5e715c1 --- /dev/null +++ b/tests/auth/owner.test.js @@ -0,0 +1,47 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { ownerOnly } from '../../lib/auth/owner.js'; + +function mockReq(token) { + return { headers: { authorization: token ? `Bearer ${token}` : undefined } }; +} +function mockRes() { + const r = { status: vi.fn().mockReturnThis(), json: vi.fn().mockReturnThis(), end: vi.fn() }; + return r; +} + +describe('ownerOnly middleware', () => { + beforeEach(() => { process.env.OWNER_TOKEN = 'test-token'; }); + + it('rejects missing token', () => { + const res = mockRes(); + const next = vi.fn(); + ownerOnly(mockReq(null), res, next); + expect(res.status).toHaveBeenCalledWith(401); + expect(next).not.toHaveBeenCalled(); + }); + + it('rejects wrong token', () => { + const res = mockRes(); + const next = vi.fn(); + ownerOnly(mockReq('wrong'), res, next); + expect(res.status).toHaveBeenCalledWith(401); + }); + + it('accepts correct token + attaches actor', () => { + const res = mockRes(); + const next = vi.fn(); + const req = mockReq('test-token'); + ownerOnly(req, res, next); + expect(next).toHaveBeenCalled(); + expect(req.actor).toEqual({ kind: 'user', id: null }); + }); + + it('returns 500 when OWNER_TOKEN unset', () => { + delete process.env.OWNER_TOKEN; + const res = mockRes(); + const next = vi.fn(); + ownerOnly(mockReq('anything'), res, next); + expect(res.status).toHaveBeenCalledWith(500); + expect(next).not.toHaveBeenCalled(); + }); +});