mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-16 10:59: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>
59 lines
1.5 KiB
TypeScript
59 lines
1.5 KiB
TypeScript
/**
|
|
* Access tier helpers for server-side gating.
|
|
*
|
|
* Mirrors the hierarchy in @mana/shared-branding/mana-apps.ts. Kept here so
|
|
* Bun/Hono services don't need to pull in a Svelte-facing package.
|
|
*
|
|
* If you change the hierarchy, update BOTH files.
|
|
*/
|
|
|
|
import type { Context, Next } from 'hono';
|
|
import { HTTPException } from 'hono/http-exception';
|
|
import type { AccessTier } from './types';
|
|
|
|
const TIER_LEVELS: Record<AccessTier, number> = {
|
|
guest: 0,
|
|
public: 1,
|
|
beta: 2,
|
|
alpha: 3,
|
|
founder: 4,
|
|
};
|
|
|
|
function normalizeTier(value: unknown): AccessTier {
|
|
if (typeof value === 'string' && value in TIER_LEVELS) {
|
|
return value as AccessTier;
|
|
}
|
|
return 'public';
|
|
}
|
|
|
|
export function getTierLevel(tier: string | undefined): number {
|
|
if (!tier) return 0;
|
|
return TIER_LEVELS[tier as AccessTier] ?? 0;
|
|
}
|
|
|
|
export function hasTier(userTier: string | undefined, minTier: AccessTier): boolean {
|
|
return getTierLevel(userTier) >= TIER_LEVELS[minTier];
|
|
}
|
|
|
|
/**
|
|
* Require a minimum access tier on a Hono route.
|
|
*
|
|
* Must run AFTER `authMiddleware()` so `userTier` is set on the context.
|
|
*
|
|
* Usage:
|
|
* ```ts
|
|
* app.use('/api/*', authMiddleware());
|
|
* app.post('/api/v1/ai/generate', requireTier('alpha'), handler);
|
|
* ```
|
|
*/
|
|
export function requireTier(minTier: AccessTier) {
|
|
return async (c: Context, next: Next) => {
|
|
const userTier = normalizeTier(c.get('userTier'));
|
|
if (!hasTier(userTier, minTier)) {
|
|
throw new HTTPException(403, {
|
|
message: `Requires access tier '${minTier}' — current tier '${userTier}' is insufficient.`,
|
|
});
|
|
}
|
|
return next();
|
|
};
|
|
}
|