mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-17 04:19:39 +02:00
The JWT already carried a `tier` claim but nothing on the server read it
— AuthGate enforcement was client-only, so a valid JWT could hit paid
LLM/research endpoints regardless of the user's access tier.
- shared-hono authMiddleware now extracts `tier` into `c.userTier`,
defaulting unknown/missing claims to `public` (never silently grants
higher access).
- New `requireTier(minTier)` middleware + `hasTier`/`getTierLevel`
helpers. Tier hierarchy (guest < public < beta < alpha < founder) is
mirrored locally to avoid pulling the Svelte-facing shared-branding
package into Bun services.
- Applied `requireTier('beta')` as defense-in-depth on resource-heavy
apps/api modules (chat, context, food, guides, news-research, picture,
plants, research, traces, who) and the MCP endpoint. Pure CRUD modules
stay auth-only — access there is gated by ownership, not tier.
- DEV_BYPASS_AUTH now injects `userTier` (defaults to founder, override
via DEV_USER_TIER).
- Authentication guideline documents the pattern + test suite covers
hierarchy, passes-at-minimum, and rejection paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
63 lines
2.2 KiB
TypeScript
63 lines
2.2 KiB
TypeScript
import { describe, it, expect } from 'bun:test';
|
|
import { Hono } from 'hono';
|
|
import type { AccessTier, AuthVariables } from './types';
|
|
import { getTierLevel, hasTier, requireTier } from './tier';
|
|
|
|
describe('tier helpers', () => {
|
|
it('orders tiers guest < public < beta < alpha < founder', () => {
|
|
expect(getTierLevel('guest')).toBeLessThan(getTierLevel('public'));
|
|
expect(getTierLevel('public')).toBeLessThan(getTierLevel('beta'));
|
|
expect(getTierLevel('beta')).toBeLessThan(getTierLevel('alpha'));
|
|
expect(getTierLevel('alpha')).toBeLessThan(getTierLevel('founder'));
|
|
});
|
|
|
|
it('treats unknown and missing tiers as 0', () => {
|
|
expect(getTierLevel(undefined)).toBe(0);
|
|
expect(getTierLevel('nobody')).toBe(0);
|
|
});
|
|
|
|
it('hasTier returns true when user tier meets or exceeds min', () => {
|
|
expect(hasTier('founder', 'alpha')).toBe(true);
|
|
expect(hasTier('alpha', 'alpha')).toBe(true);
|
|
expect(hasTier('beta', 'alpha')).toBe(false);
|
|
expect(hasTier(undefined, 'public')).toBe(false);
|
|
});
|
|
});
|
|
|
|
function buildApp(presetTier: AccessTier | undefined) {
|
|
const app = new Hono<{ Variables: AuthVariables }>();
|
|
app.use('*', async (c, next) => {
|
|
if (presetTier !== undefined) c.set('userTier', presetTier);
|
|
return next();
|
|
});
|
|
app.get('/protected', requireTier('beta'), (c) => c.text('ok'));
|
|
return app;
|
|
}
|
|
|
|
describe('requireTier middleware', () => {
|
|
it('passes when the user tier exceeds the minimum', async () => {
|
|
const res = await buildApp('alpha').request('/protected');
|
|
expect(res.status).toBe(200);
|
|
expect(await res.text()).toBe('ok');
|
|
});
|
|
|
|
it('passes when the user tier matches the minimum exactly', async () => {
|
|
const res = await buildApp('beta').request('/protected');
|
|
expect(res.status).toBe(200);
|
|
});
|
|
|
|
it('rejects with 403 when the user tier is below the minimum', async () => {
|
|
const res = await buildApp('public').request('/protected');
|
|
expect(res.status).toBe(403);
|
|
});
|
|
|
|
it('rejects with 403 when no tier is set on the context (defensive default)', async () => {
|
|
const res = await buildApp(undefined).request('/protected');
|
|
expect(res.status).toBe(403);
|
|
});
|
|
|
|
it('rejects a guest caller', async () => {
|
|
const res = await buildApp('guest').request('/protected');
|
|
expect(res.status).toBe(403);
|
|
});
|
|
});
|