166 lines
5.4 KiB
TypeScript
166 lines
5.4 KiB
TypeScript
import { describe, expect, it, beforeEach, afterEach } from 'vitest';
|
|
import Fastify from 'fastify';
|
|
import { securityMiddleware } from '../../src/local/security-middleware.js';
|
|
import { browserExtRoutes } from '../../src/local/routes/browser-ext.js';
|
|
|
|
const TEST_TOKEN = 'browser-ext-session-token';
|
|
const EXTENSION_ID = 'abcdefghijklmnopabcdefghijklmnop';
|
|
const EXTENSION_ORIGIN = `chrome-extension://${EXTENSION_ID}`;
|
|
const OTHER_EXTENSION_ORIGIN = 'chrome-extension://ponmlkjihgfedcbaponmlkjihgfedcba';
|
|
|
|
async function createBrowserExtServer() {
|
|
const server = Fastify({ logger: false });
|
|
server.decorate('agentState', {
|
|
wsSessionToken: TEST_TOKEN,
|
|
activeWorkspaceId: 'workspace-1',
|
|
});
|
|
await server.register(securityMiddleware, { sessionToken: TEST_TOKEN });
|
|
server.get('/api/memory/frames', async () => ({ ok: true }));
|
|
await server.register(browserExtRoutes);
|
|
await server.ready();
|
|
return server;
|
|
}
|
|
|
|
describe('Browser Companion auth bootstrap', () => {
|
|
const originalExtIds = process.env.WAGGLE_BROWSER_EXT_IDS;
|
|
const originalTrustLocalhost = process.env.WAGGLE_TRUST_LOCALHOST;
|
|
const originalDevAllow = process.env.WAGGLE_DEV_ALLOW_ANY_EXTENSION;
|
|
|
|
beforeEach(() => {
|
|
process.env.WAGGLE_TRUST_LOCALHOST = '0';
|
|
process.env.WAGGLE_BROWSER_EXT_IDS = EXTENSION_ID;
|
|
delete process.env.WAGGLE_DEV_ALLOW_ANY_EXTENSION;
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (originalExtIds === undefined) delete process.env.WAGGLE_BROWSER_EXT_IDS;
|
|
else process.env.WAGGLE_BROWSER_EXT_IDS = originalExtIds;
|
|
if (originalTrustLocalhost === undefined) delete process.env.WAGGLE_TRUST_LOCALHOST;
|
|
else process.env.WAGGLE_TRUST_LOCALHOST = originalTrustLocalhost;
|
|
if (originalDevAllow === undefined) delete process.env.WAGGLE_DEV_ALLOW_ANY_EXTENSION;
|
|
else process.env.WAGGLE_DEV_ALLOW_ANY_EXTENSION = originalDevAllow;
|
|
});
|
|
|
|
it('returns the session token to an explicitly allowlisted extension origin without an existing bearer', async () => {
|
|
const server = await createBrowserExtServer();
|
|
try {
|
|
const res = await server.inject({
|
|
method: 'GET',
|
|
url: '/api/browser-ext/session-token',
|
|
headers: { origin: EXTENSION_ORIGIN },
|
|
});
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
expect(res.json()).toEqual({ token: TEST_TOKEN });
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it('returns the session token to an allowlisted MV3 service-worker request without an Origin header', async () => {
|
|
const server = await createBrowserExtServer();
|
|
try {
|
|
const res = await server.inject({
|
|
method: 'GET',
|
|
url: '/api/browser-ext/session-token',
|
|
headers: {
|
|
'x-waggle-extension-id': EXTENSION_ID,
|
|
'sec-fetch-site': 'none',
|
|
},
|
|
});
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
expect(res.json()).toEqual({ token: TEST_TOKEN });
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it('rejects the token bootstrap for unallowlisted extension origins', async () => {
|
|
const server = await createBrowserExtServer();
|
|
try {
|
|
const res = await server.inject({
|
|
method: 'GET',
|
|
url: '/api/browser-ext/session-token',
|
|
headers: { origin: OTHER_EXTENSION_ORIGIN },
|
|
});
|
|
|
|
expect(res.statusCode).toBe(403);
|
|
expect(res.json().code).toBe('EXTENSION_NOT_ALLOWLISTED');
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it('rejects MV3 service-worker token bootstrap when the extension id header is missing', async () => {
|
|
const server = await createBrowserExtServer();
|
|
try {
|
|
const res = await server.inject({
|
|
method: 'GET',
|
|
url: '/api/browser-ext/session-token',
|
|
headers: { 'sec-fetch-site': 'none' },
|
|
});
|
|
|
|
expect(res.statusCode).toBe(403);
|
|
expect(res.json().code).toBe('EXTENSION_NOT_ALLOWLISTED');
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it('rejects MV3 service-worker token bootstrap with an unallowlisted extension id header', async () => {
|
|
const server = await createBrowserExtServer();
|
|
try {
|
|
const res = await server.inject({
|
|
method: 'GET',
|
|
url: '/api/browser-ext/session-token',
|
|
headers: {
|
|
'x-waggle-extension-id': 'ponmlkjihgfedcbaponmlkjihgfedcba',
|
|
'sec-fetch-site': 'none',
|
|
},
|
|
});
|
|
|
|
expect(res.statusCode).toBe(403);
|
|
expect(res.json().code).toBe('EXTENSION_NOT_ALLOWLISTED');
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it('does not let an allowlisted extension skip bearer auth on normal API routes', async () => {
|
|
const server = await createBrowserExtServer();
|
|
try {
|
|
const res = await server.inject({
|
|
method: 'GET',
|
|
url: '/api/memory/frames',
|
|
headers: { origin: EXTENSION_ORIGIN },
|
|
});
|
|
|
|
expect(res.statusCode).toBe(401);
|
|
expect(res.json().code).toBe('MISSING_TOKEN');
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
it('labels the health workspace value as an id while preserving the legacy field', async () => {
|
|
const server = await createBrowserExtServer();
|
|
try {
|
|
const res = await server.inject({
|
|
method: 'GET',
|
|
url: '/api/browser-ext/health',
|
|
headers: { authorization: `Bearer ${TEST_TOKEN}` },
|
|
});
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
expect(res.json()).toMatchObject({
|
|
ok: true,
|
|
activeWorkspaceId: 'workspace-1',
|
|
activeWorkspace: 'workspace-1',
|
|
});
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
});
|