mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-09-03 14:18:02 +00:00
c677b8bb66
* fix: fall back to enroll token when pilot tunnel JWT is rejected On HTTP 401/404 upgrade rejection, delete stale pilot.jwt and retry with SENCHO_ENROLL_TOKEN. * test: import pilot agent module after DATA_DIR is set in fallback test The auth-fallback test statically imported pilot/agent, which freezes its pilot.jwt path from DATA_DIR at module load, before setupTestDb redirects DATA_DIR to a writable temp dir. On the Linux CI runner the path resolved to a non-writable /app/data, so persistToken silently failed and the round-trip assertion read null. Import the module dynamically in beforeAll after setupTestDb, matching the sibling unit test. * fix: remove unexpected-response listener that blocked pilot reconnect The ws library skips abortHandshake when an unexpected-response listener exists, so error and close never fire and the agent hangs in CONNECTING. Detect auth rejection via the abortHandshake error message instead; the close handler already performs enroll-token fallback and reconnect.
61 lines
2.3 KiB
TypeScript
61 lines
2.3 KiB
TypeScript
/**
|
|
* Regression guard for the agent-side loopback auth injection.
|
|
*
|
|
* The central proxy strips browser cookies and the (empty) pilot-agent api_token
|
|
* before forwarding through the tunnel. Without an inline auth header on the
|
|
* agent's loopback request, the local Sencho's `authMiddleware` would 401 every
|
|
* proxied call. The agent injects a `pilot_tunnel`-scoped JWT signed by the
|
|
* LOCAL `auth_jwt_secret`, which the loopback `authMiddleware` accepts via the
|
|
* existing scope branch with no special-case bypass.
|
|
*
|
|
* This test verifies:
|
|
* 1. `getLoopbackAuthHeader` returns a Bearer header backed by a JWT that
|
|
* the local secret verifies, with the expected scope.
|
|
* 2. Subsequent calls within the refresh window return the cached header
|
|
* (no re-mint per request).
|
|
*/
|
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
import jwt from 'jsonwebtoken';
|
|
import { setupTestDb, cleanupTestDb, TEST_JWT_SECRET } from './helpers/setupTestDb';
|
|
import { PilotAgent } from '../pilot/agent';
|
|
|
|
describe('PilotAgent loopback auth injection', () => {
|
|
let tmpDir: string;
|
|
let agent: PilotAgent;
|
|
let mintHeader: () => string | null;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = await setupTestDb();
|
|
// Importing the index loads DatabaseService against the seeded baseline so
|
|
// getGlobalSettings().auth_jwt_secret resolves to TEST_JWT_SECRET.
|
|
await import('../index');
|
|
agent = new PilotAgent({
|
|
primaryUrl: 'http://primary.invalid',
|
|
loopbackPort: 1,
|
|
initialToken: 'irrelevant-for-this-test',
|
|
enrollToken: null,
|
|
enrolling: false,
|
|
});
|
|
mintHeader = (agent as unknown as { getLoopbackAuthHeader: () => string | null }).getLoopbackAuthHeader.bind(agent);
|
|
});
|
|
|
|
afterAll(() => {
|
|
cleanupTestDb(tmpDir);
|
|
});
|
|
|
|
it('mints a Bearer header with a pilot_tunnel-scoped JWT signed by the local secret', () => {
|
|
const header = mintHeader();
|
|
expect(header).toMatch(/^Bearer /);
|
|
const token = header!.slice('Bearer '.length);
|
|
const decoded = jwt.verify(token, TEST_JWT_SECRET) as { scope?: string; nodeId?: number; exp?: number };
|
|
expect(decoded.scope).toBe('pilot_tunnel');
|
|
expect(typeof decoded.exp).toBe('number');
|
|
});
|
|
|
|
it('returns the cached header on a quick second call', () => {
|
|
const a = mintHeader();
|
|
const b = mintHeader();
|
|
expect(a).toBe(b);
|
|
});
|
|
});
|