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;
|
- no crash loop or new high-severity errors;
|
||||||
- requested workload actually completed.
|
- 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
|
## Account session operations
|
||||||
|
|
||||||
The supported workflow is:
|
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,
|
ctx: WebContext,
|
||||||
running: boolean,
|
running: boolean,
|
||||||
actor: 'dashboard' | 'agent' = 'dashboard',
|
actor: 'dashboard' | 'agent' = 'dashboard',
|
||||||
): ControlResponse {
|
): Promise<ControlResponse> {
|
||||||
const authenticated = hasAuthenticatedAccount(ctx);
|
const authenticated = hasAuthenticatedAccount(ctx)
|
||||||
|
|| (running && await ensureAuthenticatedAccount(ctx));
|
||||||
if (running && !authenticated) {
|
if (running && !authenticated) {
|
||||||
ctx.logger.warn(`Bot start blocked for ${actor} until full account authentication`);
|
ctx.logger.warn(`Bot start blocked for ${actor} until full account authentication`);
|
||||||
return {
|
return {
|
||||||
@@ -60,3 +61,13 @@ function hasAuthenticatedAccount(ctx: WebContext): boolean {
|
|||||||
return ctx.services.accountRuntimes?.get(snapshot.accountId)?.resources.accountAuth.isFullyAuthenticated() ?? false;
|
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.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);
|
const body = ControlRequestSchema.safeParse(req.body);
|
||||||
if (!body.success) {
|
if (!body.success) {
|
||||||
res.status(400).json({ error: 'Invalid control request', detail: body.error.format() });
|
res.status(400).json({ error: 'Invalid control request', detail: body.error.format() });
|
||||||
return;
|
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);
|
res.status(result.success ? 200 : 409).json(result);
|
||||||
});
|
});
|
||||||
router.post('/v1/control/start', (_req, res) => {
|
router.post('/v1/control/start', async (_req, res) => {
|
||||||
const result = setBotRunning(ctx, true, 'agent');
|
const result = await setBotRunning(ctx, true, 'agent');
|
||||||
res.status(result.success ? 200 : 409).json(result);
|
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) => {
|
router.get('/v1/jobs', (req, res) => {
|
||||||
const limit = Number.parseInt(String(req.query.limit ?? '20'), 10);
|
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 {
|
export function createControlRouter(ctx: WebContext): Router {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.post('/start', (_req, res) => {
|
router.post('/start', async (_req, res) => {
|
||||||
const result = setBotRunning(ctx, true);
|
const result = await setBotRunning(ctx, true);
|
||||||
res.status(result.success ? 200 : 409).json(result.success
|
res.status(result.success ? 200 : 409).json(result.success
|
||||||
? { success: true, running: result.running }
|
? { success: true, running: result.running }
|
||||||
: { success: false, running: false, authenticated: false });
|
: { success: false, running: false, authenticated: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/stop', (_req, res) => {
|
router.post('/stop', async (_req, res) => {
|
||||||
const result = setBotRunning(ctx, false);
|
const result = await setBotRunning(ctx, false);
|
||||||
res.json({ success: true, running: result.running });
|
res.json({ success: true, running: result.running });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,12 @@ describe('bot control account authentication gate', () => {
|
|||||||
const setBotRunning = vi.fn();
|
const setBotRunning = vi.fn();
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(createControlRouter({
|
app.use(createControlRouter({
|
||||||
services: { accountAuth: { isFullyAuthenticated: () => false } },
|
services: {
|
||||||
|
accountAuth: {
|
||||||
|
isFullyAuthenticated: () => false,
|
||||||
|
ensureFullyAuthenticated: vi.fn(async () => false),
|
||||||
|
},
|
||||||
|
},
|
||||||
setBotRunning,
|
setBotRunning,
|
||||||
activity: { push: vi.fn() },
|
activity: { push: vi.fn() },
|
||||||
logger: { warn: vi.fn(), info: 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(await response.json()).toMatchObject({ success: false, authenticated: false });
|
||||||
expect(setBotRunning).not.toHaveBeenCalled();
|
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