mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-09 02:12:59 +00:00
797623e56f
Rename internal variant values from 'personal'/'team' to 'skipper'/'admiral', aligning code with user-facing tier names. Variant type is now resolved once at activation/validation and stored in DB via license_variant_type, instead of string-matching the Lemon Squeezy variant_name on every read. Also captures variant_id for future lookups. Pre-existing installs auto-migrate on first getVariant() call.
219 lines
8.0 KiB
TypeScript
219 lines
8.0 KiB
TypeScript
/**
|
|
* Tests for Distributed License Enforcement: the trust chain where the main
|
|
* instance asserts its license tier to remote nodes via proxy headers.
|
|
*/
|
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
import request from 'supertest';
|
|
import jwt from 'jsonwebtoken';
|
|
import { setupTestDb, cleanupTestDb, TEST_USERNAME, TEST_JWT_SECRET } from './helpers/setupTestDb';
|
|
|
|
let tmpDir: string;
|
|
let app: import('express').Express;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = await setupTestDb();
|
|
({ app } = await import('../index'));
|
|
});
|
|
|
|
afterAll(() => {
|
|
cleanupTestDb(tmpDir);
|
|
});
|
|
|
|
/** Helper: sign a token with the test JWT secret. */
|
|
const signToken = (payload: Record<string, unknown>, expiresIn: string | number = '1m') =>
|
|
jwt.sign(payload, TEST_JWT_SECRET, { expiresIn: expiresIn as jwt.SignOptions['expiresIn'] });
|
|
|
|
// We need a Paid-gated route that doesn't depend on Docker or remote nodes.
|
|
// /api/webhooks is Paid-gated and just reads from the DB; returns an empty array
|
|
// if no webhooks exist.
|
|
const PAID_ROUTE = '/api/webhooks';
|
|
|
|
// For Admiral routes, /api/audit-log is Admiral-gated and reads from the DB.
|
|
const ADMIRAL_ROUTE = '/api/audit-log';
|
|
|
|
// ─── authMiddleware: proxyTier/proxyVariant propagation ─────────────────────
|
|
|
|
describe('authMiddleware - distributed license headers', () => {
|
|
it('sets proxyTier/proxyVariant for node_proxy tokens with valid tier headers', async () => {
|
|
const token = signToken({ scope: 'node_proxy' });
|
|
// Hit a Paid-gated route with tier assertion - should be allowed
|
|
const res = await request(app)
|
|
.get(PAID_ROUTE)
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.set('x-sencho-tier', 'paid')
|
|
.set('x-sencho-variant', 'skipper');
|
|
|
|
// Should NOT get 403 PAID_REQUIRED; the proxy tier assertion grants access
|
|
expect(res.status).not.toBe(403);
|
|
});
|
|
|
|
it('ignores tier headers for user session tokens', async () => {
|
|
const token = signToken({ username: TEST_USERNAME, role: 'admin' });
|
|
// Even with tier headers set, a user session should use local license (community)
|
|
const res = await request(app)
|
|
.get(PAID_ROUTE)
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.set('x-sencho-tier', 'paid')
|
|
.set('x-sencho-variant', 'admiral');
|
|
|
|
// Local license is community in test env → should get 403
|
|
expect(res.status).toBe(403);
|
|
expect(res.body.code).toBe('PAID_REQUIRED');
|
|
});
|
|
|
|
it('ignores tier headers for malformed values on node_proxy tokens', async () => {
|
|
const token = signToken({ scope: 'node_proxy' });
|
|
const res = await request(app)
|
|
.get(PAID_ROUTE)
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.set('x-sencho-tier', 'enterprise') // invalid value
|
|
.set('x-sencho-variant', 'mega'); // invalid value
|
|
|
|
// Invalid tier header → proxyTier not set → falls back to local (community) → 403
|
|
expect(res.status).toBe(403);
|
|
expect(res.body.code).toBe('PAID_REQUIRED');
|
|
});
|
|
|
|
it('falls back to local tier when no tier headers on node_proxy token', async () => {
|
|
const token = signToken({ scope: 'node_proxy' });
|
|
const res = await request(app)
|
|
.get(PAID_ROUTE)
|
|
.set('Authorization', `Bearer ${token}`);
|
|
// No tier headers → falls back to local (community) → 403
|
|
|
|
expect(res.status).toBe(403);
|
|
expect(res.body.code).toBe('PAID_REQUIRED');
|
|
});
|
|
});
|
|
|
|
// ─── requirePaid guard ───────────────────────────────────────────────────────
|
|
|
|
describe('requirePaid - distributed license', () => {
|
|
it('allows access when proxy asserts paid tier', async () => {
|
|
const token = signToken({ scope: 'node_proxy' });
|
|
const res = await request(app)
|
|
.get(PAID_ROUTE)
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.set('x-sencho-tier', 'paid')
|
|
.set('x-sencho-variant', '');
|
|
|
|
expect(res.status).not.toBe(403);
|
|
});
|
|
|
|
it('blocks access when proxy asserts community tier', async () => {
|
|
const token = signToken({ scope: 'node_proxy' });
|
|
const res = await request(app)
|
|
.get(PAID_ROUTE)
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.set('x-sencho-tier', 'community');
|
|
|
|
expect(res.status).toBe(403);
|
|
expect(res.body.code).toBe('PAID_REQUIRED');
|
|
});
|
|
|
|
it('blocks access for direct user when local tier is community', async () => {
|
|
const token = signToken({ username: TEST_USERNAME, role: 'admin' });
|
|
const res = await request(app)
|
|
.get(PAID_ROUTE)
|
|
.set('Authorization', `Bearer ${token}`);
|
|
|
|
expect(res.status).toBe(403);
|
|
expect(res.body.code).toBe('PAID_REQUIRED');
|
|
});
|
|
});
|
|
|
|
// ─── requireAdmiral guard ───────────────────────────────────────────────────
|
|
|
|
describe('requireAdmiral - distributed license', () => {
|
|
it('allows access when proxy asserts paid tier with admiral variant', async () => {
|
|
const token = signToken({ scope: 'node_proxy' });
|
|
const res = await request(app)
|
|
.get(ADMIRAL_ROUTE)
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.set('x-sencho-tier', 'paid')
|
|
.set('x-sencho-variant', 'admiral');
|
|
|
|
expect(res.status).not.toBe(403);
|
|
});
|
|
|
|
it('blocks when proxy asserts paid tier with skipper variant', async () => {
|
|
const token = signToken({ scope: 'node_proxy' });
|
|
const res = await request(app)
|
|
.get(ADMIRAL_ROUTE)
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.set('x-sencho-tier', 'paid')
|
|
.set('x-sencho-variant', 'skipper');
|
|
|
|
expect(res.status).toBe(403);
|
|
expect(res.body.code).toBe('ADMIRAL_REQUIRED');
|
|
});
|
|
|
|
it('blocks when proxy asserts community tier', async () => {
|
|
const token = signToken({ scope: 'node_proxy' });
|
|
const res = await request(app)
|
|
.get(ADMIRAL_ROUTE)
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.set('x-sencho-tier', 'community');
|
|
|
|
expect(res.status).toBe(403);
|
|
expect(res.body.code).toBe('PAID_REQUIRED');
|
|
});
|
|
|
|
it('blocks when proxy asserts paid tier with empty variant', async () => {
|
|
const token = signToken({ scope: 'node_proxy' });
|
|
const res = await request(app)
|
|
.get(ADMIRAL_ROUTE)
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.set('x-sencho-tier', 'paid')
|
|
.set('x-sencho-variant', '');
|
|
|
|
expect(res.status).toBe(403);
|
|
expect(res.body.code).toBe('ADMIRAL_REQUIRED');
|
|
});
|
|
});
|
|
|
|
// ─── Security: header injection prevention ──────────────────────────────────
|
|
|
|
describe('Security - tier header injection', () => {
|
|
it('cannot elevate access via tier headers on a user session', async () => {
|
|
const token = signToken({ username: TEST_USERNAME, role: 'admin' });
|
|
const res = await request(app)
|
|
.get(ADMIRAL_ROUTE)
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.set('x-sencho-tier', 'paid')
|
|
.set('x-sencho-variant', 'admiral');
|
|
|
|
// User session → tier headers ignored → local community tier → 403
|
|
expect(res.status).toBe(403);
|
|
});
|
|
|
|
it('cannot elevate access via tier headers without any auth', async () => {
|
|
const res = await request(app)
|
|
.get(PAID_ROUTE)
|
|
.set('x-sencho-tier', 'paid')
|
|
.set('x-sencho-variant', 'admiral');
|
|
|
|
expect(res.status).toBe(401);
|
|
});
|
|
|
|
it('cannot elevate access with expired node_proxy token', async () => {
|
|
const token = jwt.sign({ scope: 'node_proxy' }, TEST_JWT_SECRET, { expiresIn: '-1s' });
|
|
const res = await request(app)
|
|
.get(PAID_ROUTE)
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.set('x-sencho-tier', 'paid');
|
|
|
|
expect(res.status).toBe(401);
|
|
});
|
|
|
|
it('cannot elevate access with token signed by wrong secret', async () => {
|
|
const token = jwt.sign({ scope: 'node_proxy' }, 'wrong-secret', { expiresIn: '1m' });
|
|
const res = await request(app)
|
|
.get(PAID_ROUTE)
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.set('x-sencho-tier', 'paid');
|
|
|
|
expect(res.status).toBe(401);
|
|
});
|
|
});
|