@hotmeshio/long-tail 0.24.1 → 0.24.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/lib/db/schemas/040_oauth_client_consent.sql +15 -0
- package/build/modules/config.d.ts +4 -0
- package/build/modules/config.js +4 -0
- package/build/modules/maintenance.d.ts +1 -1
- package/build/modules/maintenance.js +1 -1
- package/build/services/auth/oauth-server/index.d.ts +1 -1
- package/build/services/auth/oauth-server/index.js +2 -1
- package/build/services/auth/oauth-server/sql.d.ts +7 -4
- package/build/services/auth/oauth-server/sql.js +10 -4
- package/build/services/auth/oauth-server/store.d.ts +8 -1
- package/build/services/auth/oauth-server/store.js +31 -2
- package/build/services/maintenance/index.js +1 -1
- package/docs/maintenance.md +3 -3
- package/docs/mcp.md +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
-- Migration 040: when a person last consented to a grant for an OAuth client
|
|
2
|
+
--
|
|
3
|
+
-- Registration needs no sign-in, so a client that never reaches consent is
|
|
4
|
+
-- deleted after the retention window. A client someone has consented to is
|
|
5
|
+
-- kept, so the app can always reconnect with the client id it holds.
|
|
6
|
+
|
|
7
|
+
ALTER TABLE lt_oauth_clients ADD COLUMN IF NOT EXISTS last_consent_at TIMESTAMPTZ;
|
|
8
|
+
|
|
9
|
+
-- Clients consented to before this migration: their newest grant.
|
|
10
|
+
UPDATE lt_oauth_clients c
|
|
11
|
+
SET last_consent_at = g.created_at
|
|
12
|
+
FROM (
|
|
13
|
+
SELECT client_id, MAX(created_at) AS created_at FROM lt_oauth_grants GROUP BY client_id
|
|
14
|
+
) g
|
|
15
|
+
WHERE c.client_id = g.client_id AND c.last_consent_at IS NULL;
|
|
@@ -30,6 +30,10 @@ export declare const config: {
|
|
|
30
30
|
JWT_SECRET: string;
|
|
31
31
|
LT_SEED_SERVICE_KEYS: boolean;
|
|
32
32
|
LT_OAUTH_ISSUER: string;
|
|
33
|
+
/** Spent, expired and revoked OAuth rows are deleted once older than this many days. */
|
|
34
|
+
OAUTH_RETENTION_DAYS: number;
|
|
35
|
+
/** Least time between one process's OAuth sweeps. A sweep runs on an OAuth write. */
|
|
36
|
+
OAUTH_SWEEP_SECONDS: number;
|
|
33
37
|
HONEYCOMB_API_KEY: string;
|
|
34
38
|
ROLE_ENFORCEMENT_CACHE_TTL_MS: number;
|
|
35
39
|
LT_ANALYTICS_MAX_GROUPS: number;
|
package/build/modules/config.js
CHANGED
|
@@ -45,6 +45,10 @@ exports.config = {
|
|
|
45
45
|
LT_SEED_SERVICE_KEYS: process.env.LT_SEED_SERVICE_KEYS === 'true',
|
|
46
46
|
// Enables the OAuth authorization server for /mcp when start() passes no auth.oauthServer.
|
|
47
47
|
LT_OAUTH_ISSUER: process.env.LT_OAUTH_ISSUER || '',
|
|
48
|
+
/** Spent, expired and revoked OAuth rows are deleted once older than this many days. */
|
|
49
|
+
OAUTH_RETENTION_DAYS: positiveInt('OAUTH_RETENTION_DAYS', 7),
|
|
50
|
+
/** Least time between one process's OAuth sweeps. A sweep runs on an OAuth write. */
|
|
51
|
+
OAUTH_SWEEP_SECONDS: positiveInt('OAUTH_SWEEP_SECONDS', 600),
|
|
48
52
|
HONEYCOMB_API_KEY: process.env.HONEYCOMB_API_KEY || '',
|
|
49
53
|
// Resolver schema enforcement: TTL for the cached enforcing-role set and
|
|
50
54
|
// latest role form schemas. Bounds cross-container staleness after a role
|
|
@@ -11,7 +11,7 @@ import type { LTMaintenanceConfig } from '../types/maintenance';
|
|
|
11
11
|
* (preserves jdata, udata, jmark, hmark — exports always work)
|
|
12
12
|
* 4. Hard-delete pruned jobs older than 180 days
|
|
13
13
|
* 5. Delete OAuth server rows (expired codes and tokens, unused or revoked
|
|
14
|
-
* grants, clients
|
|
14
|
+
* grants, clients never consented to) older than 7 days
|
|
15
15
|
*
|
|
16
16
|
* Conservative defaults — execution artifacts (activity inputs, stream
|
|
17
17
|
* history) are needed for rich export data. Pruning too early strips
|
|
@@ -13,7 +13,7 @@ exports.defaultMaintenanceConfig = void 0;
|
|
|
13
13
|
* (preserves jdata, udata, jmark, hmark — exports always work)
|
|
14
14
|
* 4. Hard-delete pruned jobs older than 180 days
|
|
15
15
|
* 5. Delete OAuth server rows (expired codes and tokens, unused or revoked
|
|
16
|
-
* grants, clients
|
|
16
|
+
* grants, clients never consented to) older than 7 days
|
|
17
17
|
*
|
|
18
18
|
* Conservative defaults — execution artifacts (activity inputs, stream
|
|
19
19
|
* history) are needed for rich export data. Pruning too early strips
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { REFRESH_REUSE_GRACE_SECONDS, createSecret, hashSecret, pkceChallenge, registerClient, getClient, issueAuthorizationCode, exchangeAuthorizationCode, rotateRefreshToken, getLiveGrant, revokeByRefreshToken, revokeGrant, listGrants, pruneOAuthServer, type RevokedGrant, type OAuthPruneCounts, } from './store';
|
|
1
|
+
export { REFRESH_REUSE_GRACE_SECONDS, createSecret, hashSecret, pkceChallenge, registerClient, getClient, issueAuthorizationCode, exchangeAuthorizationCode, rotateRefreshToken, getLiveGrant, revokeByRefreshToken, revokeGrant, listGrants, pruneOAuthServer, sweepOAuthServer, type RevokedGrant, type OAuthPruneCounts, } from './store';
|
|
2
2
|
export { signAccessToken, verifyAccessToken, accessTokenPrincipal, type AccessTokenTarget, type LTAccessTokenClaims, } from './access-token';
|
|
3
3
|
export { MCP_SCOPES, PRESET_SCOPE, ACCESS_TOKEN_TYPE, OAUTH_PRINCIPAL_TYPE } from './constants';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.OAUTH_PRINCIPAL_TYPE = exports.ACCESS_TOKEN_TYPE = exports.PRESET_SCOPE = exports.MCP_SCOPES = exports.accessTokenPrincipal = exports.verifyAccessToken = exports.signAccessToken = exports.pruneOAuthServer = exports.listGrants = exports.revokeGrant = exports.revokeByRefreshToken = exports.getLiveGrant = exports.rotateRefreshToken = exports.exchangeAuthorizationCode = exports.issueAuthorizationCode = exports.getClient = exports.registerClient = exports.pkceChallenge = exports.hashSecret = exports.createSecret = exports.REFRESH_REUSE_GRACE_SECONDS = void 0;
|
|
3
|
+
exports.OAUTH_PRINCIPAL_TYPE = exports.ACCESS_TOKEN_TYPE = exports.PRESET_SCOPE = exports.MCP_SCOPES = exports.accessTokenPrincipal = exports.verifyAccessToken = exports.signAccessToken = exports.sweepOAuthServer = exports.pruneOAuthServer = exports.listGrants = exports.revokeGrant = exports.revokeByRefreshToken = exports.getLiveGrant = exports.rotateRefreshToken = exports.exchangeAuthorizationCode = exports.issueAuthorizationCode = exports.getClient = exports.registerClient = exports.pkceChallenge = exports.hashSecret = exports.createSecret = exports.REFRESH_REUSE_GRACE_SECONDS = void 0;
|
|
4
4
|
var store_1 = require("./store");
|
|
5
5
|
Object.defineProperty(exports, "REFRESH_REUSE_GRACE_SECONDS", { enumerable: true, get: function () { return store_1.REFRESH_REUSE_GRACE_SECONDS; } });
|
|
6
6
|
Object.defineProperty(exports, "createSecret", { enumerable: true, get: function () { return store_1.createSecret; } });
|
|
@@ -16,6 +16,7 @@ Object.defineProperty(exports, "revokeByRefreshToken", { enumerable: true, get:
|
|
|
16
16
|
Object.defineProperty(exports, "revokeGrant", { enumerable: true, get: function () { return store_1.revokeGrant; } });
|
|
17
17
|
Object.defineProperty(exports, "listGrants", { enumerable: true, get: function () { return store_1.listGrants; } });
|
|
18
18
|
Object.defineProperty(exports, "pruneOAuthServer", { enumerable: true, get: function () { return store_1.pruneOAuthServer; } });
|
|
19
|
+
Object.defineProperty(exports, "sweepOAuthServer", { enumerable: true, get: function () { return store_1.sweepOAuthServer; } });
|
|
19
20
|
var access_token_1 = require("./access-token");
|
|
20
21
|
Object.defineProperty(exports, "signAccessToken", { enumerable: true, get: function () { return access_token_1.signAccessToken; } });
|
|
21
22
|
Object.defineProperty(exports, "verifyAccessToken", { enumerable: true, get: function () { return access_token_1.verifyAccessToken; } });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export declare const INSERT_CLIENT = "\n INSERT INTO lt_oauth_clients (client_id, client_name, redirect_uris)\n VALUES ($1, $2, $3)\n RETURNING client_id, client_name, redirect_uris, created_at";
|
|
2
2
|
export declare const GET_CLIENT = "\n SELECT client_id, client_name, redirect_uris, created_at\n FROM lt_oauth_clients WHERE client_id = $1";
|
|
3
|
-
/** Create the grant and its authorization code together. */
|
|
4
|
-
export declare const ISSUE_CODE = "\n WITH g AS (\n INSERT INTO lt_oauth_grants (user_id, client_id, policy, scope)\n VALUES ($1, $2, $3, $4)\n RETURNING id\n )\n INSERT INTO lt_oauth_codes (code_hash, grant_id, redirect_uri, code_challenge, resource, expires_at)\n SELECT $5, g.id, $6, $7, $8, NOW() + make_interval(secs => $9) FROM g\n RETURNING grant_id";
|
|
3
|
+
/** Create the grant and its authorization code together, and stamp the client's consent. */
|
|
4
|
+
export declare const ISSUE_CODE = "\n WITH g AS (\n INSERT INTO lt_oauth_grants (user_id, client_id, policy, scope)\n VALUES ($1, $2, $3, $4)\n RETURNING id\n ),\n consented AS (\n UPDATE lt_oauth_clients SET last_consent_at = NOW() WHERE client_id = $2\n )\n INSERT INTO lt_oauth_codes (code_hash, grant_id, redirect_uri, code_challenge, resource, expires_at)\n SELECT $5, g.id, $6, $7, $8, NOW() + make_interval(secs => $9) FROM g\n RETURNING grant_id";
|
|
5
5
|
/**
|
|
6
6
|
* Consume a code and issue the first refresh token. The code must be unused,
|
|
7
7
|
* unexpired, for this client and redirect URI, and match the PKCE challenge;
|
|
@@ -46,5 +46,8 @@ export declare const PRUNE_CODES = "\n DELETE FROM lt_oauth_codes WHERE expires
|
|
|
46
46
|
* left with no refresh token and no code: never exchanged, or long unused.
|
|
47
47
|
*/
|
|
48
48
|
export declare const PRUNE_GRANTS = "\n DELETE FROM lt_oauth_grants g\n WHERE g.revoked_at < NOW() - $1::interval\n OR (g.created_at < NOW() - $1::interval\n AND NOT EXISTS (SELECT 1 FROM lt_oauth_refresh_tokens t WHERE t.grant_id = g.id)\n AND NOT EXISTS (SELECT 1 FROM lt_oauth_codes c WHERE c.grant_id = g.id))";
|
|
49
|
-
/**
|
|
50
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Clients registered longer ago than the window that no one ever consented to.
|
|
51
|
+
* A client with a consent is kept: the app holds its client id to reconnect.
|
|
52
|
+
*/
|
|
53
|
+
export declare const PRUNE_CLIENTS = "\n DELETE FROM lt_oauth_clients c\n WHERE c.last_consent_at IS NULL AND c.created_at < NOW() - $1::interval\n AND NOT EXISTS (SELECT 1 FROM lt_oauth_grants g WHERE g.client_id = c.client_id)";
|
|
@@ -17,12 +17,15 @@ exports.INSERT_CLIENT = `
|
|
|
17
17
|
exports.GET_CLIENT = `
|
|
18
18
|
SELECT client_id, client_name, redirect_uris, created_at
|
|
19
19
|
FROM lt_oauth_clients WHERE client_id = $1`;
|
|
20
|
-
/** Create the grant and its authorization code together. */
|
|
20
|
+
/** Create the grant and its authorization code together, and stamp the client's consent. */
|
|
21
21
|
exports.ISSUE_CODE = `
|
|
22
22
|
WITH g AS (
|
|
23
23
|
INSERT INTO lt_oauth_grants (user_id, client_id, policy, scope)
|
|
24
24
|
VALUES ($1, $2, $3, $4)
|
|
25
25
|
RETURNING id
|
|
26
|
+
),
|
|
27
|
+
consented AS (
|
|
28
|
+
UPDATE lt_oauth_clients SET last_consent_at = NOW() WHERE client_id = $2
|
|
26
29
|
)
|
|
27
30
|
INSERT INTO lt_oauth_codes (code_hash, grant_id, redirect_uri, code_challenge, resource, expires_at)
|
|
28
31
|
SELECT $5, g.id, $6, $7, $8, NOW() + make_interval(secs => $9) FROM g
|
|
@@ -149,8 +152,11 @@ exports.PRUNE_GRANTS = `
|
|
|
149
152
|
OR (g.created_at < NOW() - $1::interval
|
|
150
153
|
AND NOT EXISTS (SELECT 1 FROM lt_oauth_refresh_tokens t WHERE t.grant_id = g.id)
|
|
151
154
|
AND NOT EXISTS (SELECT 1 FROM lt_oauth_codes c WHERE c.grant_id = g.id))`;
|
|
152
|
-
/**
|
|
155
|
+
/**
|
|
156
|
+
* Clients registered longer ago than the window that no one ever consented to.
|
|
157
|
+
* A client with a consent is kept: the app holds its client id to reconnect.
|
|
158
|
+
*/
|
|
153
159
|
exports.PRUNE_CLIENTS = `
|
|
154
160
|
DELETE FROM lt_oauth_clients c
|
|
155
|
-
WHERE c.created_at < NOW() - $1::interval
|
|
156
|
-
AND NOT EXISTS (SELECT 1 FROM lt_oauth_grants g WHERE g.client_id = c.client_id
|
|
161
|
+
WHERE c.last_consent_at IS NULL AND c.created_at < NOW() - $1::interval
|
|
162
|
+
AND NOT EXISTS (SELECT 1 FROM lt_oauth_grants g WHERE g.client_id = c.client_id)`;
|
|
@@ -82,7 +82,14 @@ export interface OAuthPruneCounts {
|
|
|
82
82
|
/**
|
|
83
83
|
* Delete what the authorization server no longer needs, older than the
|
|
84
84
|
* window (a Postgres interval): spent and expired refresh tokens and codes,
|
|
85
|
-
* then grants with neither left or revoked, then clients
|
|
85
|
+
* then grants with neither left or revoked, then clients never consented to.
|
|
86
86
|
* Each step reads what the one before it left.
|
|
87
87
|
*/
|
|
88
88
|
export declare function pruneOAuthServer(olderThan: string): Promise<OAuthPruneCounts>;
|
|
89
|
+
/**
|
|
90
|
+
* Delete what the server no longer needs, older than OAUTH_RETENTION_DAYS.
|
|
91
|
+
* Each OAuth write calls it, and it runs at most once per OAUTH_SWEEP_SECONDS
|
|
92
|
+
* in a process, so the tables stay bounded with or without a maintenance
|
|
93
|
+
* schedule. A failed sweep is logged and never fails the write.
|
|
94
|
+
*/
|
|
95
|
+
export declare function sweepOAuthServer(): Promise<void>;
|
|
@@ -47,8 +47,11 @@ exports.revokeByRefreshToken = revokeByRefreshToken;
|
|
|
47
47
|
exports.revokeGrant = revokeGrant;
|
|
48
48
|
exports.listGrants = listGrants;
|
|
49
49
|
exports.pruneOAuthServer = pruneOAuthServer;
|
|
50
|
+
exports.sweepOAuthServer = sweepOAuthServer;
|
|
50
51
|
const crypto = __importStar(require("crypto"));
|
|
51
52
|
const db_1 = require("../../../lib/db");
|
|
53
|
+
const logger_1 = require("../../../lib/logger");
|
|
54
|
+
const config_1 = require("../../../modules/config");
|
|
52
55
|
const uuid_1 = require("../../../lib/uuid");
|
|
53
56
|
const sql_1 = require("./sql");
|
|
54
57
|
/** A refresh token reused within this many seconds is treated as a client retry. */
|
|
@@ -69,6 +72,7 @@ async function registerClient(input) {
|
|
|
69
72
|
const { rows } = await (0, db_1.getPool)().query(sql_1.INSERT_CLIENT, [
|
|
70
73
|
`ltc_${createSecret()}`, input.clientName ?? null, input.redirectUris,
|
|
71
74
|
]);
|
|
75
|
+
void sweepOAuthServer();
|
|
72
76
|
return rows[0];
|
|
73
77
|
}
|
|
74
78
|
/** A client id as registerClient issues it; anything else names no client and never reaches SQL. */
|
|
@@ -90,6 +94,7 @@ async function issueAuthorizationCode(input) {
|
|
|
90
94
|
input.userId, input.clientId, input.policy, input.scope,
|
|
91
95
|
hashSecret(code), input.redirectUri, input.codeChallenge, input.resource ?? null, input.ttlSeconds,
|
|
92
96
|
]);
|
|
97
|
+
void sweepOAuthServer();
|
|
93
98
|
return { code, grantId: rows[0].grant_id };
|
|
94
99
|
}
|
|
95
100
|
/**
|
|
@@ -110,6 +115,7 @@ async function exchangeAuthorizationCode(input) {
|
|
|
110
115
|
await pool.query(sql_1.REVOKE_ON_CODE_REUSE, [codeHash]);
|
|
111
116
|
return null;
|
|
112
117
|
}
|
|
118
|
+
void sweepOAuthServer();
|
|
113
119
|
const { resource, ...snapshot } = rows[0];
|
|
114
120
|
return { snapshot, resource, refreshToken };
|
|
115
121
|
}
|
|
@@ -124,8 +130,10 @@ async function rotateRefreshToken(input) {
|
|
|
124
130
|
const next = createSecret();
|
|
125
131
|
const oldHash = hashSecret(input.refreshToken);
|
|
126
132
|
const { rows } = await pool.query(sql_1.ROTATE_REFRESH_TOKEN, [oldHash, hashSecret(next), input.refreshTtlSeconds, input.clientId]);
|
|
127
|
-
if (rows[0])
|
|
133
|
+
if (rows[0]) {
|
|
134
|
+
void sweepOAuthServer();
|
|
128
135
|
return { snapshot: rows[0], refreshToken: next };
|
|
136
|
+
}
|
|
129
137
|
const reuse = await pool.query(sql_1.REVOKE_ON_REFRESH_REUSE, [oldHash, exports.REFRESH_REUSE_GRACE_SECONDS]);
|
|
130
138
|
return { snapshot: null, revoked: reuse.rows[0] ?? null };
|
|
131
139
|
}
|
|
@@ -158,7 +166,7 @@ async function listGrants(userId) {
|
|
|
158
166
|
/**
|
|
159
167
|
* Delete what the authorization server no longer needs, older than the
|
|
160
168
|
* window (a Postgres interval): spent and expired refresh tokens and codes,
|
|
161
|
-
* then grants with neither left or revoked, then clients
|
|
169
|
+
* then grants with neither left or revoked, then clients never consented to.
|
|
162
170
|
* Each step reads what the one before it left.
|
|
163
171
|
*/
|
|
164
172
|
async function pruneOAuthServer(olderThan) {
|
|
@@ -169,3 +177,24 @@ async function pruneOAuthServer(olderThan) {
|
|
|
169
177
|
const clients = (await pool.query(sql_1.PRUNE_CLIENTS, [olderThan])).rowCount ?? 0;
|
|
170
178
|
return { refreshTokens, codes, grants, clients };
|
|
171
179
|
}
|
|
180
|
+
let nextSweepAt = 0;
|
|
181
|
+
/**
|
|
182
|
+
* Delete what the server no longer needs, older than OAUTH_RETENTION_DAYS.
|
|
183
|
+
* Each OAuth write calls it, and it runs at most once per OAUTH_SWEEP_SECONDS
|
|
184
|
+
* in a process, so the tables stay bounded with or without a maintenance
|
|
185
|
+
* schedule. A failed sweep is logged and never fails the write.
|
|
186
|
+
*/
|
|
187
|
+
async function sweepOAuthServer() {
|
|
188
|
+
const now = Date.now();
|
|
189
|
+
if (now < nextSweepAt)
|
|
190
|
+
return;
|
|
191
|
+
nextSweepAt = now + config_1.config.OAUTH_SWEEP_SECONDS * 1000;
|
|
192
|
+
try {
|
|
193
|
+
const counts = await pruneOAuthServer(`${config_1.config.OAUTH_RETENTION_DAYS} days`);
|
|
194
|
+
if (Object.values(counts).some(Boolean))
|
|
195
|
+
logger_1.loggerRegistry.info(`[lt-oauth] swept: ${JSON.stringify(counts)}`);
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
logger_1.loggerRegistry.warn(`[lt-oauth] sweep failed: ${err?.message}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -107,7 +107,7 @@ async function executeRule(appId, rule) {
|
|
|
107
107
|
break;
|
|
108
108
|
}
|
|
109
109
|
// Drop the authorization server's dead rows: spent or expired tokens and
|
|
110
|
-
// codes, grants with neither left or long revoked, clients
|
|
110
|
+
// codes, grants with neither left or long revoked, clients never consented to.
|
|
111
111
|
case rule.target === 'oauth' && rule.action === 'delete': {
|
|
112
112
|
const { pruneOAuthServer } = await Promise.resolve().then(() => __importStar(require('../auth/oauth-server')));
|
|
113
113
|
const counts = await pruneOAuthServer(rule.olderThan);
|
package/docs/maintenance.md
CHANGED
|
@@ -62,7 +62,7 @@ Six categories of data are subject to maintenance:
|
|
|
62
62
|
| **Entity jobs** | Job rows where `entity` is set. These represent actual workflow instances -- the rows that back `Durable.Client.workflow.search()` calls and execution exports. Deleting them removes the workflow record entirely. |
|
|
63
63
|
| **Pruned jobs** | Entity jobs that have already had their execution artifacts stripped (the `pruned_at` column is not `NULL`). These retain core data but no longer carry execution scaffolding. They can be hard-deleted after a longer retention window. |
|
|
64
64
|
| **Terminal escalations** | Escalation rows in a terminal status (`resolved`, `cancelled`, `expired`). Terminal rows are inert audit records -- every engine state transition guards on `status = 'pending'`, so aging them out is safe for live waiters, claims, and signal delivery. The engine-owned prune deletes in atomic batches until the retention horizon is drained. Pending rows are never touched. |
|
|
65
|
-
| **OAuth server rows** | The `/mcp` authorization server's records. Refresh tokens and authorization codes past expiry, or rotated out, longer ago than the window; then grants left with neither, or revoked longer ago than the window; then clients registered longer ago than the window
|
|
65
|
+
| **OAuth server rows** | The `/mcp` authorization server's records. Refresh tokens and authorization codes past expiry, or rotated out, longer ago than the window; then grants left with neither, or revoked longer ago than the window; then clients registered longer ago than the window that no one ever consented to. A client still holding a live refresh token keeps its grant, and a client anyone has consented to keeps its registration, so the app can reconnect with the client id it holds. |
|
|
66
66
|
|
|
67
67
|
## Prune vs. delete
|
|
68
68
|
|
|
@@ -97,9 +97,9 @@ The default configuration (`modules/maintenance.ts`, enabled with `maintenance:
|
|
|
97
97
|
| 2. Delete transient jobs older than 30 days | Removes activity-level bookkeeping rows that have no associated workflow entity. |
|
|
98
98
|
| 3. Prune entity jobs older than 30 days | Strips execution artifacts from workflow instances while preserving core data. Exports and search remain functional. |
|
|
99
99
|
| 4. Delete pruned jobs older than 180 days | Hard-deletes workflow instances that were pruned at least 180 days ago. This is the final removal. |
|
|
100
|
-
| 5. Delete OAuth server rows older than 7 days | Removes expired codes and tokens, unused or revoked grants, and clients
|
|
100
|
+
| 5. Delete OAuth server rows older than 7 days | Removes expired codes and tokens, unused or revoked grants, and clients never consented to. |
|
|
101
101
|
|
|
102
|
-
A custom `rules` list replaces the defaults
|
|
102
|
+
A custom `rules` list replaces the defaults. The `oauth` rule is optional: the OAuth server sweeps its own tables as it is used (see [Connect with OAuth](mcp.md#connect-with-oauth)), and the rule adds a sweep on this schedule with its own window.
|
|
103
103
|
|
|
104
104
|
Rules execute sequentially. If one fails, the remaining rules still run; failures are logged and do not halt the cycle.
|
|
105
105
|
|
package/docs/mcp.md
CHANGED
|
@@ -207,6 +207,8 @@ Allow as me appears only when it grants more than read-only: when you can write
|
|
|
207
207
|
|
|
208
208
|
Access tokens last 5 minutes; the client refreshes them silently with a refresh token that rotates on each use and lasts 30 days. Every request reads the grant: a client acts with your roles as they are now, and a disconnected app, a revoked token or a deactivated account stops working on the next request. Disconnect a client under **Connected apps** in the user menu; the same happens when the client revokes its token.
|
|
209
209
|
|
|
210
|
+
The server keeps its tables bounded on its own. Each process sweeps them on an OAuth write, at most once every `OAUTH_SWEEP_SECONDS` (default 600), deleting rows dead for longer than `OAUTH_RETENTION_DAYS` (default 7): spent and expired codes and refresh tokens, revoked or unused grants, and clients registered but never consented to. A live grant and its current refresh token are kept, and so is any client someone has consented to.
|
|
211
|
+
|
|
210
212
|
**Enable it** with `auth.oauthServer` in `start()`, or `LT_OAUTH_ISSUER` in the environment:
|
|
211
213
|
|
|
212
214
|
```typescript
|
package/package.json
CHANGED