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:
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;