conductor

CI task system
git clone git://git.finwo.net/app/conductor
Log | Files | Refs | README | LICENSE

commit 1a5080ded7478813abcf176ecc86ee6c9fbcfcf7
parent 5f8b53de26012cc3e3ae1c69b9050a56d8f41cb7
Author: finwo <finwo@pm.me>
Date:   Sun, 27 Sep 2026 19:36:16 +0200

Sweep refresh tokens through their own store instead of a second query

Diffstat:
Mdeploy/worker/docker-compose.yml | 4++--
Msrc/conductor/app.js | 2+-
Msrc/conductor/index.js | 4----
Msrc/conductor/retention.js | 11+++--------
Msrc/conductor/routes/envelope.js | 3+--
Mtest/dialects.test.js | 5++++-
Mtest/oidc.test.js | 4----
7 files changed, 11 insertions(+), 22 deletions(-)

diff --git a/deploy/worker/docker-compose.yml b/deploy/worker/docker-compose.yml @@ -4,8 +4,8 @@ # worker needs no inbound connectivity, so this works from behind NAT. # # Setup: -# 1. Ask for a worker token, or mint one yourself in the interface under -# Workers. It is shown once. +# 1. Ask for a worker token, or mint one yourself on the Workers page. +# It is shown once. # 2. Put it in .env next to this file: # CONDUCTOR_URL=https://ci.example.com # CONDUCTOR_WORKER_TOKEN=... diff --git a/src/conductor/app.js b/src/conductor/app.js @@ -48,7 +48,7 @@ export async function createServices(cfg, options = {}) { const auth = createAuth({ cfg, users, logger }); const scheduler = createScheduler({ cfg, db, git, logs, storage, projects, secrets, variables, logger }); - const retention = createRetention({ cfg, db, storage, logs, logger }); + const retention = createRetention({ cfg, db, storage, logs, refreshTokens, logger }); // Without a first administrator there is no way into the admin surface. // Only done for local accounts; with a provider it owns identity. diff --git a/src/conductor/index.js b/src/conductor/index.js @@ -1,8 +1,4 @@ // src/conductor/index.js - entry point -// -// One service: it accepts triggers, schedules runs, serves the worker API, -// and renders the interface. Anonymous visitors see whatever the projects -// have declared public, and nothing else. import { loadConfig } from '../lib/config.js'; import { createServices, buildServer, startReaper, startRetention } from './app.js'; diff --git a/src/conductor/retention.js b/src/conductor/retention.js @@ -33,7 +33,7 @@ function setting(projectValue, fallback) { return projectValue === null || projectValue === undefined ? fallback : projectValue; } -export function createRetention({ cfg, db, storage, logs, logger = {} }) { +export function createRetention({ cfg, db, storage, logs, refreshTokens, logger = {} }) { const defaults = cfg.retention; async function deleteObject(key, what) { @@ -183,14 +183,9 @@ export function createRetention({ cfg, db, storage, logs, logger = {} }) { return res.changes ?? 0; } - // A session nobody can present any more, expired rather than revoked, so - // there is a window in which it can still be listed as ended. + // A day past expiry, so a session stays listable as ended for a while. async function sweepRefreshTokens(now) { - const res = await db.run( - 'DELETE FROM refresh_tokens WHERE expires_at < {cutoff}', - { cutoff: now - DAY } - ); - return res.changes ?? 0; + return (await refreshTokens.sweep(now - DAY)) ?? 0; } return { diff --git a/src/conductor/routes/envelope.js b/src/conductor/routes/envelope.js @@ -15,8 +15,7 @@ export function ok(reply, data, pagination = null) { return reply.send(body); } -// Installed on the API plugin alone, so a mistyped page url still reaches -// the interface and gets HTML rather than being answered in JSON. +// API plugin only; page urls must not be answered in JSON. export function envelopeFailures(fastify) { fastify.setNotFoundHandler((req, reply) => fail(req, reply, new NotFound())); fastify.setErrorHandler((e, req, reply) => fail(req, reply, e)); diff --git a/test/dialects.test.js b/test/dialects.test.js @@ -23,6 +23,7 @@ import { createStorage } from '../src/lib/storage/index.js'; import { createLogStore } from '../src/lib/log.js'; import { createRetention } from '../src/conductor/retention.js'; import { createWorkerTokens } from '../src/lib/workers.js'; +import { createRefreshTokens } from '../src/lib/auth/refresh.js'; const DAY = 24 * 60 * 60 * 1000; const NOW = 1_800_000_000_000; @@ -156,7 +157,9 @@ for (const dialect of ['postgres', 'mysql']) { { then: NOW - 1000 } ); - const retention = createRetention({ cfg, db, storage, logs, logger: {} }); + const retention = createRetention({ + cfg, db, storage, logs, refreshTokens: createRefreshTokens({ db }), logger: {}, + }); const first = await retention.sweep({ now: NOW }); diff --git a/test/oidc.test.js b/test/oidc.test.js @@ -34,10 +34,6 @@ function dockerAvailableSync() { const available = dockerAvailableSync(); const opts = { skip: available ? false : 'docker is not available' }; -// These exercised the JSON API, which has been removed. Kept until the -// suite is rebuilt against the interface. -const apiGone = { skip: 'the JSON API was removed' }; - // One provider for the whole file; each test gets a fresh conductor. let oidc = null;