fix(oauth): purge tokens whose refresh token expired a month ago

Every refresh writes a new oauth_tokens row and nothing ever deleted the
old ones, so an MCP client refreshing hourly grew the table, and every
backup with it, without end. A nightly job now deletes tokens whose
refresh token expired more than 30 days ago, leaves first, and keeps any
row a live chain still points at for replay detection. Also types the
backup's per-entry store flag.
This commit is contained in:
Maurice
2026-10-07 16:45:21 +02:00
parent a869269591
commit 76df44d8de
7 changed files with 146 additions and 4 deletions
@@ -352,6 +352,35 @@ export class OauthTokensRepository extends TrekRepository<OauthTokens> {
return rows.map((r) => r.id);
}
// ---------------------------------------------------------------------
// Retention
// ---------------------------------------------------------------------
/**
* Delete tokens whose refresh token expired before `cutoffIso`, oldest
* generation first. A row is only deleted once no other token names it as
* parent: `parent_token_id` has no ON DELETE action, and a token still in a
* live chain is what replay detection walks. Each pass removes the current
* leaves, so a whole expired chain goes over a few passes; `maxPasses`
* bounds a chain that is still growing. Returns the number deleted.
*/
async deleteExpiredBefore(cutoffIso: string, maxPasses = 50): Promise<number> {
let total = 0;
for (let pass = 0; pass < maxPasses; pass++) {
const result = await this.kysely<OauthTokensKyselyDB>()
.deleteFrom('oauth_tokens')
.where('refresh_token_expires_at', '<', cutoffIso)
.where('id', 'not in', (eb) =>
eb.selectFrom('oauth_tokens as child').select('child.parent_token_id').where('child.parent_token_id', 'is not', null),
)
.executeTakeFirst();
const deleted = Number(result.numDeletedRows ?? 0);
total += deleted;
if (deleted === 0) break;
}
return total;
}
// ---------------------------------------------------------------------
// OA19 — chain-wide revoke
// ---------------------------------------------------------------------