mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-10 18:56:53 +00:00
a6d3e5d052
After the central proxy forwards a request through the pilot tunnel, the agent receives an http_req or ws_open frame and opens a local loopback request to its own Sencho server. The forwarder previously copied the inbound headers verbatim with no Authorization header, so the local authMiddleware rejected every proxied call with 401 Authentication required. The central cannot sign the missing token because each Sencho instance has its own auth_jwt_secret generated at first-run setup. The agent process runs in the same Node process as the local Sencho server and shares the local DatabaseService, so it can mint a fresh pilot_tunnel-scoped JWT signed with the local secret. Token TTL is 5 minutes with a refresh-on-use 4-minute cache to avoid jwt.sign on every request. The header is built once per request via buildLoopbackHeaders and applied to both onHttpReq and onWsOpen so the two forwarder paths stay in lockstep. The local authMiddleware accepts the scope through its existing pilot_tunnel branch with no special-case bypass. Together with the proxy-side change in the previous commit, this completes the central, bridge, agent, local-Sencho HTTP path for pilot-agent-mode nodes.
60 lines
2.3 KiB
TypeScript
60 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',
|
|
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);
|
|
});
|
|
});
|