fix: revalidate authentication before bot start
This commit is contained in:
1 parent
8ea1cf16d4
commit
11b6300113
5 files changed
+101
-13
No files matched your search
@@ -75,6 +75,12 @@ Success should distinguish:
|
||||
- no crash loop or new high-severity errors;
|
||||
- requested workload actually completed.
|
||||
|
||||
The authentication lease deliberately expires while an account is idle. A
|
||||
start request revalidates the persisted Reddit and Matrix credentials before
|
||||
changing the run state, so an otherwise valid paused account does not become
|
||||
stuck behind the start safety gate. A failed revalidation still returns a
|
||||
conflict and requires a fresh extension session import.
|
||||
|
||||
## Account session operations
|
||||
|
||||
The supported workflow is:
|
||||
|
||||
+14
-3
@@ -12,12 +12,13 @@ export function botControlStatus(ctx: WebContext): ControlResponse {
|
||||
};
|
||||
}
|
||||
|
||||
export function setBotRunning(
|
||||
export async function setBotRunning(
|
||||
ctx: WebContext,
|
||||
running: boolean,
|
||||
actor: 'dashboard' | 'agent' = 'dashboard',
|
||||
): ControlResponse {
|
||||
const authenticated = hasAuthenticatedAccount(ctx);
|
||||
): Promise<ControlResponse> {
|
||||
const authenticated = hasAuthenticatedAccount(ctx)
|
||||
|| (running && await ensureAuthenticatedAccount(ctx));
|
||||
if (running && !authenticated) {
|
||||
ctx.logger.warn(`Bot start blocked for ${actor} until full account authentication`);
|
||||
return {
|
||||
@@ -60,3 +61,13 @@ function hasAuthenticatedAccount(ctx: WebContext): boolean {
|
||||
return ctx.services.accountRuntimes?.get(snapshot.accountId)?.resources.accountAuth.isFullyAuthenticated() ?? false;
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureAuthenticatedAccount(ctx: WebContext): Promise<boolean> {
|
||||
if (ctx.services.accountAuth && await ctx.services.accountAuth.ensureFullyAuthenticated()) return true;
|
||||
for (const snapshot of ctx.services.accountRuntimes?.list() ?? []) {
|
||||
if (snapshot.state !== 'running') continue;
|
||||
const auth = ctx.services.accountRuntimes?.get(snapshot.accountId)?.resources.accountAuth;
|
||||
if (auth && await auth.ensureFullyAuthenticated()) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -54,20 +54,20 @@ export function createAgentRouter(ctx: WebContext): Router {
|
||||
});
|
||||
|
||||
router.get('/v1/control', (_req, res) => res.json(botControlStatus(ctx)));
|
||||
router.post('/v1/control', (req, res) => {
|
||||
router.post('/v1/control', async (req, res) => {
|
||||
const body = ControlRequestSchema.safeParse(req.body);
|
||||
if (!body.success) {
|
||||
res.status(400).json({ error: 'Invalid control request', detail: body.error.format() });
|
||||
return;
|
||||
}
|
||||
const result = setBotRunning(ctx, body.data.running, 'agent');
|
||||
const result = await setBotRunning(ctx, body.data.running, 'agent');
|
||||
res.status(result.success ? 200 : 409).json(result);
|
||||
});
|
||||
router.post('/v1/control/start', (_req, res) => {
|
||||
const result = setBotRunning(ctx, true, 'agent');
|
||||
router.post('/v1/control/start', async (_req, res) => {
|
||||
const result = await setBotRunning(ctx, true, 'agent');
|
||||
res.status(result.success ? 200 : 409).json(result);
|
||||
});
|
||||
router.post('/v1/control/stop', (_req, res) => res.json(setBotRunning(ctx, false, 'agent')));
|
||||
router.post('/v1/control/stop', async (_req, res) => res.json(await setBotRunning(ctx, false, 'agent')));
|
||||
|
||||
router.get('/v1/jobs', (req, res) => {
|
||||
const limit = Number.parseInt(String(req.query.limit ?? '20'), 10);
|
||||
|
||||
@@ -5,15 +5,15 @@ import { setBotRunning } from '../bot-control.js';
|
||||
export function createControlRouter(ctx: WebContext): Router {
|
||||
const router = Router();
|
||||
|
||||
router.post('/start', (_req, res) => {
|
||||
const result = setBotRunning(ctx, true);
|
||||
router.post('/start', async (_req, res) => {
|
||||
const result = await setBotRunning(ctx, true);
|
||||
res.status(result.success ? 200 : 409).json(result.success
|
||||
? { success: true, running: result.running }
|
||||
: { success: false, running: false, authenticated: false });
|
||||
});
|
||||
|
||||
router.post('/stop', (_req, res) => {
|
||||
const result = setBotRunning(ctx, false);
|
||||
router.post('/stop', async (_req, res) => {
|
||||
const result = await setBotRunning(ctx, false);
|
||||
res.json({ success: true, running: result.running });
|
||||
});
|
||||
|
||||
|
||||
@@ -16,7 +16,12 @@ describe('bot control account authentication gate', () => {
|
||||
const setBotRunning = vi.fn();
|
||||
const app = express();
|
||||
app.use(createControlRouter({
|
||||
services: { accountAuth: { isFullyAuthenticated: () => false } },
|
||||
services: {
|
||||
accountAuth: {
|
||||
isFullyAuthenticated: () => false,
|
||||
ensureFullyAuthenticated: vi.fn(async () => false),
|
||||
},
|
||||
},
|
||||
setBotRunning,
|
||||
activity: { push: vi.fn() },
|
||||
logger: { warn: vi.fn(), info: vi.fn() },
|
||||
@@ -32,4 +37,70 @@ describe('bot control account authentication gate', () => {
|
||||
expect(await response.json()).toMatchObject({ success: false, authenticated: false });
|
||||
expect(setBotRunning).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renews an expired authentication lease before starting the bot', async () => {
|
||||
let running = false;
|
||||
const ensureFullyAuthenticated = vi.fn(async () => true);
|
||||
const app = express();
|
||||
app.use(createControlRouter({
|
||||
config: { browser: { dryRun: false } },
|
||||
services: {
|
||||
accountAuth: {
|
||||
isFullyAuthenticated: () => false,
|
||||
ensureFullyAuthenticated,
|
||||
},
|
||||
},
|
||||
getBotRunning: () => running,
|
||||
setBotRunning: (next: boolean) => { running = next; },
|
||||
activity: { push: vi.fn() },
|
||||
logger: { warn: vi.fn(), info: vi.fn() },
|
||||
broadcaster: { dashboardEvents: { bot: vi.fn() } },
|
||||
} as never));
|
||||
const server = app.listen(0);
|
||||
servers.push(server);
|
||||
await new Promise<void>((resolve) => server.once('listening', resolve));
|
||||
const { port } = server.address() as AddressInfo;
|
||||
|
||||
const response = await fetch(`http://127.0.0.1:${port}/start`, { method: 'POST' });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(await response.json()).toMatchObject({ success: true, running: true });
|
||||
expect(ensureFullyAuthenticated).toHaveBeenCalledOnce();
|
||||
expect(running).toBe(true);
|
||||
});
|
||||
|
||||
it('renews a running account runtime when the legacy auth context is unavailable', async () => {
|
||||
let running = false;
|
||||
const ensureFullyAuthenticated = vi.fn(async () => true);
|
||||
const runtime = {
|
||||
resources: {
|
||||
accountAuth: { isFullyAuthenticated: () => false, ensureFullyAuthenticated },
|
||||
},
|
||||
};
|
||||
const app = express();
|
||||
app.use(createControlRouter({
|
||||
config: { browser: { dryRun: false } },
|
||||
services: {
|
||||
accountRuntimes: {
|
||||
list: () => [{ accountId: '@t2_account:reddit.com', state: 'running' }],
|
||||
get: () => runtime,
|
||||
},
|
||||
},
|
||||
getBotRunning: () => running,
|
||||
setBotRunning: (next: boolean) => { running = next; },
|
||||
activity: { push: vi.fn() },
|
||||
logger: { warn: vi.fn(), info: vi.fn() },
|
||||
broadcaster: { dashboardEvents: { bot: vi.fn() } },
|
||||
} as never));
|
||||
const server = app.listen(0);
|
||||
servers.push(server);
|
||||
await new Promise<void>((resolve) => server.once('listening', resolve));
|
||||
const { port } = server.address() as AddressInfo;
|
||||
|
||||
const response = await fetch(`http://127.0.0.1:${port}/start`, { method: 'POST' });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(ensureFullyAuthenticated).toHaveBeenCalledOnce();
|
||||
expect(running).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user