@hotmeshio/long-tail 0.24.0 → 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/adapters/express.d.ts +3 -1
- package/build/adapters/express.js +11 -1
- package/build/api/yaml-workflows/cron.d.ts +3 -2
- package/build/api/yaml-workflows/cron.js +6 -1
- package/build/api/yaml-workflows/deploy.d.ts +9 -4
- package/build/api/yaml-workflows/deploy.js +39 -12
- package/build/lib/db/schemas/039_nats_ws_tickets.sql +12 -0
- package/build/lib/db/schemas/040_oauth_client_consent.sql +15 -0
- package/build/lib/events/browser-view.d.ts +5 -0
- package/build/lib/events/browser-view.js +16 -0
- package/build/lib/events/nats-ws-filter.d.ts +8 -8
- package/build/lib/events/nats-ws-filter.js +49 -34
- package/build/lib/events/nats-ws-proxy.d.ts +5 -0
- package/build/lib/events/nats-ws-proxy.js +63 -18
- package/build/lib/events/nats-ws-ticket.d.ts +12 -3
- package/build/lib/events/nats-ws-ticket.js +25 -4
- package/build/modules/config.d.ts +9 -0
- package/build/modules/config.js +11 -0
- package/build/modules/maintenance.d.ts +2 -0
- package/build/modules/maintenance.js +7 -0
- package/build/routes/agents.js +4 -3
- package/build/routes/oauth-server/authorize.js +6 -4
- package/build/routes/yaml-workflows/cron.js +1 -1
- package/build/services/agent/authority.d.ts +1 -0
- package/build/services/agent/authority.js +7 -1
- package/build/services/agent/trigger-registry.js +7 -2
- package/build/services/auth/oauth-server/index.d.ts +1 -1
- package/build/services/auth/oauth-server/index.js +3 -1
- package/build/services/auth/oauth-server/sql.d.ts +21 -2
- package/build/services/auth/oauth-server/sql.js +41 -2
- package/build/services/auth/oauth-server/store.d.ts +22 -1
- package/build/services/auth/oauth-server/store.js +54 -5
- package/build/services/cron/index.js +5 -3
- package/build/services/maintenance/index.js +8 -0
- package/build/services/mcp/seed-service-account.d.ts +4 -1
- package/build/services/mcp/seed-service-account.js +11 -6
- package/build/services/yaml-workflow/invoke.js +7 -3
- package/build/services/yaml-workflow/workers/register.js +3 -13
- package/build/services/yaml-workflow/workers/step-tool.d.ts +7 -0
- package/build/services/yaml-workflow/workers/step-tool.js +69 -0
- package/build/system/mcp-servers/admin/agent-subscriptions.js +2 -1
- package/build/system/mcp-servers/admin/schemas.d.ts +6 -6
- package/build/system/mcp-servers/admin/yaml-workflows.js +6 -20
- package/build/system/mcp-servers/workflow.js +13 -25
- package/build/tsconfig.tsbuildinfo +1 -1
- package/build/types/maintenance.d.ts +2 -2
- package/docs/api/http/mcp-endpoint.md +3 -3
- package/docs/maintenance.md +51 -28
- package/docs/mcp.md +2 -0
- package/package.json +1 -1
|
@@ -6,15 +6,28 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.NATS_WS_TICKET_PARAM = exports.NATS_WS_TICKET_TYPE = void 0;
|
|
7
7
|
exports.signNatsWsTicket = signNatsWsTicket;
|
|
8
8
|
exports.verifyNatsWsTicket = verifyNatsWsTicket;
|
|
9
|
+
exports.spendNatsWsTicket = spendNatsWsTicket;
|
|
10
|
+
const crypto_1 = require("crypto");
|
|
9
11
|
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
12
|
+
const db_1 = require("../db");
|
|
10
13
|
const config_1 = require("../../modules/config");
|
|
11
14
|
/**
|
|
12
15
|
* A browser's pass to the NATS WebSocket proxy, in place of the NATS server
|
|
13
16
|
* credential. It names the person in `sub` and carries no `userId`, so it
|
|
14
|
-
* authenticates nothing but the proxy upgrade.
|
|
17
|
+
* authenticates nothing but the proxy upgrade. Each ticket admits one
|
|
18
|
+
* upgrade: the dashboard fetches a fresh one for every connection attempt.
|
|
15
19
|
*/
|
|
16
20
|
exports.NATS_WS_TICKET_TYPE = 'nats-ws+jwt';
|
|
17
21
|
exports.NATS_WS_TICKET_PARAM = 'ticket';
|
|
22
|
+
/** Record the ticket as spent, and drop expired records. True only for its first use. */
|
|
23
|
+
const SPEND_TICKET = `
|
|
24
|
+
WITH expired AS (
|
|
25
|
+
DELETE FROM lt_nats_ws_tickets WHERE expires_at < NOW()
|
|
26
|
+
)
|
|
27
|
+
INSERT INTO lt_nats_ws_tickets (jti, expires_at)
|
|
28
|
+
VALUES ($1, to_timestamp($2))
|
|
29
|
+
ON CONFLICT (jti) DO NOTHING
|
|
30
|
+
RETURNING jti`;
|
|
18
31
|
function signNatsWsTicket(userId) {
|
|
19
32
|
if (!config_1.config.JWT_SECRET)
|
|
20
33
|
return null;
|
|
@@ -22,10 +35,11 @@ function signNatsWsTicket(userId) {
|
|
|
22
35
|
algorithm: 'HS256',
|
|
23
36
|
header: { alg: 'HS256', typ: exports.NATS_WS_TICKET_TYPE },
|
|
24
37
|
subject: userId,
|
|
38
|
+
jwtid: (0, crypto_1.randomUUID)(),
|
|
25
39
|
expiresIn: config_1.config.NATS_WS_TICKET_TTL_SECONDS,
|
|
26
40
|
});
|
|
27
41
|
}
|
|
28
|
-
/** The
|
|
42
|
+
/** The claims of a valid ticket; null for anything else. Does not spend it. */
|
|
29
43
|
function verifyNatsWsTicket(ticket) {
|
|
30
44
|
if (!ticket || !config_1.config.JWT_SECRET)
|
|
31
45
|
return null;
|
|
@@ -33,10 +47,17 @@ function verifyNatsWsTicket(ticket) {
|
|
|
33
47
|
const decoded = jsonwebtoken_1.default.verify(ticket, config_1.config.JWT_SECRET, { algorithms: ['HS256'], complete: true });
|
|
34
48
|
if (decoded.header.typ !== exports.NATS_WS_TICKET_TYPE)
|
|
35
49
|
return null;
|
|
36
|
-
const
|
|
37
|
-
|
|
50
|
+
const { sub, jti, exp } = decoded.payload;
|
|
51
|
+
if (typeof sub !== 'string' || typeof jti !== 'string' || typeof exp !== 'number')
|
|
52
|
+
return null;
|
|
53
|
+
return { userId: sub, jti, exp };
|
|
38
54
|
}
|
|
39
55
|
catch {
|
|
40
56
|
return null;
|
|
41
57
|
}
|
|
42
58
|
}
|
|
59
|
+
/** Spend a verified ticket. False when it was already spent. */
|
|
60
|
+
async function spendNatsWsTicket(ticket) {
|
|
61
|
+
const { rowCount } = await (0, db_1.getPool)().query(SPEND_TICKET, [ticket.jti, ticket.exp]);
|
|
62
|
+
return rowCount === 1;
|
|
63
|
+
}
|
|
@@ -12,6 +12,10 @@ export declare const config: {
|
|
|
12
12
|
NATS_DASHBOARD_TOKEN: string;
|
|
13
13
|
/** How long a browser's WebSocket proxy ticket admits an upgrade. The dashboard fetches one per attempt. */
|
|
14
14
|
NATS_WS_TICKET_TTL_SECONDS: number;
|
|
15
|
+
/** Largest WebSocket frame the proxy accepts from a browser, near NATS's own max_payload. */
|
|
16
|
+
NATS_WS_MAX_PAYLOAD_BYTES: number;
|
|
17
|
+
/** How often the proxy rechecks a connection holder's account; a change closes the connection. */
|
|
18
|
+
NATS_WS_RECHECK_SECONDS: number;
|
|
15
19
|
/**
|
|
16
20
|
* Dashboard reconnect: first backoff delay, delay cap, how long a drop lasts
|
|
17
21
|
* before the banner shows, and the window the first retry and catch-up
|
|
@@ -24,7 +28,12 @@ export declare const config: {
|
|
|
24
28
|
PORT: number;
|
|
25
29
|
NODE_ENV: string;
|
|
26
30
|
JWT_SECRET: string;
|
|
31
|
+
LT_SEED_SERVICE_KEYS: boolean;
|
|
27
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;
|
|
28
37
|
HONEYCOMB_API_KEY: string;
|
|
29
38
|
ROLE_ENFORCEMENT_CACHE_TTL_MS: number;
|
|
30
39
|
LT_ANALYTICS_MAX_GROUPS: number;
|
package/build/modules/config.js
CHANGED
|
@@ -24,6 +24,10 @@ exports.config = {
|
|
|
24
24
|
NATS_DASHBOARD_TOKEN: process.env.NATS_DASHBOARD_TOKEN || '',
|
|
25
25
|
/** How long a browser's WebSocket proxy ticket admits an upgrade. The dashboard fetches one per attempt. */
|
|
26
26
|
NATS_WS_TICKET_TTL_SECONDS: positiveInt('NATS_WS_TICKET_TTL_SECONDS', 300),
|
|
27
|
+
/** Largest WebSocket frame the proxy accepts from a browser, near NATS's own max_payload. */
|
|
28
|
+
NATS_WS_MAX_PAYLOAD_BYTES: positiveInt('NATS_WS_MAX_PAYLOAD_BYTES', 1024 * 1024),
|
|
29
|
+
/** How often the proxy rechecks a connection holder's account; a change closes the connection. */
|
|
30
|
+
NATS_WS_RECHECK_SECONDS: positiveInt('NATS_WS_RECHECK_SECONDS', 60),
|
|
27
31
|
/**
|
|
28
32
|
* Dashboard reconnect: first backoff delay, delay cap, how long a drop lasts
|
|
29
33
|
* before the banner shows, and the window the first retry and catch-up
|
|
@@ -36,8 +40,15 @@ exports.config = {
|
|
|
36
40
|
PORT: parseInt(process.env.PORT || '3000', 10),
|
|
37
41
|
NODE_ENV: process.env.NODE_ENV || 'development',
|
|
38
42
|
JWT_SECRET: process.env.JWT_SECRET || '',
|
|
43
|
+
// Mint the seeded mcp-service account's API keys and print them to stdout once, at first seed.
|
|
44
|
+
// Local docker compose sets it; elsewhere a superadmin mints keys from the dashboard.
|
|
45
|
+
LT_SEED_SERVICE_KEYS: process.env.LT_SEED_SERVICE_KEYS === 'true',
|
|
39
46
|
// Enables the OAuth authorization server for /mcp when start() passes no auth.oauthServer.
|
|
40
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),
|
|
41
52
|
HONEYCOMB_API_KEY: process.env.HONEYCOMB_API_KEY || '',
|
|
42
53
|
// Resolver schema enforcement: TTL for the cached enforcing-role set and
|
|
43
54
|
// latest role form schemas. Bounds cross-container staleness after a role
|
|
@@ -10,6 +10,8 @@ import type { LTMaintenanceConfig } from '../types/maintenance';
|
|
|
10
10
|
* 3. Strip execution artifacts from entity jobs older than 30 days
|
|
11
11
|
* (preserves jdata, udata, jmark, hmark — exports always work)
|
|
12
12
|
* 4. Hard-delete pruned jobs older than 180 days
|
|
13
|
+
* 5. Delete OAuth server rows (expired codes and tokens, unused or revoked
|
|
14
|
+
* grants, clients never consented to) older than 7 days
|
|
13
15
|
*
|
|
14
16
|
* Conservative defaults — execution artifacts (activity inputs, stream
|
|
15
17
|
* history) are needed for rich export data. Pruning too early strips
|
|
@@ -12,6 +12,8 @@ exports.defaultMaintenanceConfig = void 0;
|
|
|
12
12
|
* 3. Strip execution artifacts from entity jobs older than 30 days
|
|
13
13
|
* (preserves jdata, udata, jmark, hmark — exports always work)
|
|
14
14
|
* 4. Hard-delete pruned jobs older than 180 days
|
|
15
|
+
* 5. Delete OAuth server rows (expired codes and tokens, unused or revoked
|
|
16
|
+
* grants, clients never consented to) older than 7 days
|
|
15
17
|
*
|
|
16
18
|
* Conservative defaults — execution artifacts (activity inputs, stream
|
|
17
19
|
* history) are needed for rich export data. Pruning too early strips
|
|
@@ -44,5 +46,10 @@ exports.defaultMaintenanceConfig = {
|
|
|
44
46
|
olderThan: '180 days',
|
|
45
47
|
pruned: true,
|
|
46
48
|
},
|
|
49
|
+
{
|
|
50
|
+
target: 'oauth',
|
|
51
|
+
action: 'delete',
|
|
52
|
+
olderThan: '7 days',
|
|
53
|
+
},
|
|
47
54
|
],
|
|
48
55
|
};
|
package/build/routes/agents.js
CHANGED
|
@@ -124,13 +124,13 @@ router.get('/:agentId/subscriptions', async (req, res) => {
|
|
|
124
124
|
* Create an event subscription for an agent.
|
|
125
125
|
*/
|
|
126
126
|
router.post('/:agentId/subscriptions', auth_1.requireBuilder, async (req, res) => {
|
|
127
|
-
const { topic, reaction_type, execute_as } = req.body;
|
|
127
|
+
const { topic, reaction_type, workflow_type, execute_as } = req.body;
|
|
128
128
|
if (!topic || !reaction_type) {
|
|
129
129
|
res.status(400).json({ error: 'topic and reaction_type are required' });
|
|
130
130
|
return;
|
|
131
131
|
}
|
|
132
132
|
const agentId = String(req.params.agentId);
|
|
133
|
-
const check = () => (0, authority_1.assertMayConfigureSubscription)(req.auth.userId, { agentId, reaction_type, execute_as });
|
|
133
|
+
const check = () => (0, authority_1.assertMayConfigureSubscription)(req.auth.userId, { agentId, reaction_type, workflow_type, execute_as });
|
|
134
134
|
if (!(await authorized(check, res)))
|
|
135
135
|
return;
|
|
136
136
|
const result = await subApi.createSubscription({ ...req.body, agentId });
|
|
@@ -144,7 +144,8 @@ router.put('/:agentId/subscriptions/:subId', auth_1.requireBuilder, async (req,
|
|
|
144
144
|
const agentId = String(req.params.agentId);
|
|
145
145
|
const id = String(req.params.subId);
|
|
146
146
|
const check = () => (0, authority_1.assertMayConfigureSubscription)(req.auth.userId, {
|
|
147
|
-
agentId, subscriptionId: id, reaction_type: req.body?.reaction_type,
|
|
147
|
+
agentId, subscriptionId: id, reaction_type: req.body?.reaction_type,
|
|
148
|
+
workflow_type: req.body?.workflow_type, execute_as: req.body?.execute_as,
|
|
148
149
|
});
|
|
149
150
|
if (!(await authorized(check, res)))
|
|
150
151
|
return;
|
|
@@ -51,9 +51,11 @@ function refuseBots(req, res) {
|
|
|
51
51
|
* POST /api/oauth/authorize
|
|
52
52
|
* The person approves. Body: the authorization request plus
|
|
53
53
|
* { preset: 'read_only' | 'just_me' }. Returns { redirect } for the consent
|
|
54
|
-
* page to navigate to, carrying the code.
|
|
54
|
+
* page to navigate to, carrying the code. Only the dashboard's own bearer
|
|
55
|
+
* credential approves: a host session cookie, which any same-site script
|
|
56
|
+
* could send, does not.
|
|
55
57
|
*/
|
|
56
|
-
router.post('/authorize', auth_1.
|
|
58
|
+
router.post('/authorize', auth_1.requireCredentialAuth, async (req, res) => {
|
|
57
59
|
if (refuseBots(req, res))
|
|
58
60
|
return;
|
|
59
61
|
const settings = (0, oauth_server_1.getOAuthServerSettings)();
|
|
@@ -79,9 +81,9 @@ router.post('/authorize', auth_1.requireAuth, async (req, res) => {
|
|
|
79
81
|
/**
|
|
80
82
|
* POST /api/oauth/deny
|
|
81
83
|
* The person declines. Body: { client_id, redirect_uri, state? }. Returns
|
|
82
|
-
* { redirect } carrying error=access_denied.
|
|
84
|
+
* { redirect } carrying error=access_denied. Bearer credential only, as authorize.
|
|
83
85
|
*/
|
|
84
|
-
router.post('/deny', auth_1.
|
|
86
|
+
router.post('/deny', auth_1.requireCredentialAuth, async (req, res) => {
|
|
85
87
|
const settings = (0, oauth_server_1.getOAuthServerSettings)();
|
|
86
88
|
const clientId = typeof req.body?.client_id === 'string' ? req.body.client_id : '';
|
|
87
89
|
const client = clientId ? await (0, oauth_server_2.getClient)(clientId) : null;
|
|
@@ -59,7 +59,7 @@ router.put('/:id/cron', auth_1.requireBuilder, async (req, res) => {
|
|
|
59
59
|
cron_schedule: req.body.cron_schedule,
|
|
60
60
|
cron_envelope: req.body.cron_envelope,
|
|
61
61
|
execute_as: req.body.execute_as,
|
|
62
|
-
});
|
|
62
|
+
}, req.auth);
|
|
63
63
|
res.status(result.status).json(result.data ?? { error: result.error });
|
|
64
64
|
});
|
|
65
65
|
/**
|
|
@@ -8,13 +8,16 @@ exports.assertMayConfigureSubscription = assertMayConfigureSubscription;
|
|
|
8
8
|
* Whoever configures one must be able to run as that identity: the same rule
|
|
9
9
|
* as an execute_as override. Automation that would run as lt-system, or a
|
|
10
10
|
* capability subscription (a tool called with lt-system authority), is a
|
|
11
|
-
* superadmin's to configure.
|
|
11
|
+
* superadmin's to configure. A durable subscription starts its workflow
|
|
12
|
+
* under that workflow's invocation rules, for the configurer here and for
|
|
13
|
+
* the run identity each time it fires.
|
|
12
14
|
*/
|
|
13
15
|
const workflow_invocation_1 = require("../workflow-invocation");
|
|
14
16
|
const user_1 = require("../user");
|
|
15
17
|
const index_1 = require("./index");
|
|
16
18
|
const subscriptions_1 = require("./subscriptions");
|
|
17
19
|
const SYSTEM_AUTHORITY = 'runs as lt-system; only superadmin can configure it';
|
|
20
|
+
const DURABLE_REACTION = 'durable';
|
|
18
21
|
async function assertIdentity(callerId, identity, what) {
|
|
19
22
|
if (!identity)
|
|
20
23
|
throw new workflow_invocation_1.InvocationError(`${what} has no identity, so it ${SYSTEM_AUTHORITY}`, 403);
|
|
@@ -47,6 +50,9 @@ async function assertMayConfigureSubscription(callerId, input) {
|
|
|
47
50
|
if (reaction === 'capability') {
|
|
48
51
|
throw new workflow_invocation_1.InvocationError(`a capability subscription calls a tool directly, so it ${SYSTEM_AUTHORITY}`, 403);
|
|
49
52
|
}
|
|
53
|
+
const workflowType = input.workflow_type !== undefined ? input.workflow_type : existing?.workflow_type;
|
|
54
|
+
if (reaction === DURABLE_REACTION && workflowType)
|
|
55
|
+
await (0, workflow_invocation_1.checkInvocationRoles)(workflowType, callerId);
|
|
50
56
|
const agent = await (0, index_1.getAgent)(input.agentId);
|
|
51
57
|
const executeAs = input.execute_as !== undefined ? input.execute_as : existing?.execute_as;
|
|
52
58
|
await assertIdentity(callerId, executeAs || agent?.user_id, 'the subscription');
|
|
@@ -245,12 +245,17 @@ class AgentTriggerRegistry {
|
|
|
245
245
|
async executeReaction(sub, mapped, deterministicId) {
|
|
246
246
|
switch (sub.reaction_type) {
|
|
247
247
|
case 'durable': {
|
|
248
|
-
const { invokeWorkflow } = await Promise.resolve().then(() => __importStar(require('../workflow-invocation')));
|
|
248
|
+
const { invokeWorkflow, checkInvocationRoles } = await Promise.resolve().then(() => __importStar(require('../workflow-invocation')));
|
|
249
|
+
// The run identity must be able to start this workflow now. Only a
|
|
250
|
+
// superadmin configures a subscription with no identity.
|
|
251
|
+
const runAs = sub.execute_as ?? sub.agent_user_id ?? undefined;
|
|
252
|
+
if (runAs)
|
|
253
|
+
await checkInvocationRoles(sub.workflow_type, runAs);
|
|
249
254
|
await invokeWorkflow({
|
|
250
255
|
workflowType: sub.workflow_type,
|
|
251
256
|
data: mapped.data ?? mapped,
|
|
252
257
|
metadata: mapped.metadata ?? { source: 'agent', certified: true },
|
|
253
|
-
executeAs:
|
|
258
|
+
executeAs: runAs,
|
|
254
259
|
options: { workflowId: deterministicId },
|
|
255
260
|
auth: { userId: sub.agent_user_id || 'lt-system', role: 'admin' },
|
|
256
261
|
});
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { REFRESH_REUSE_GRACE_SECONDS, createSecret, hashSecret, pkceChallenge, registerClient, getClient, issueAuthorizationCode, exchangeAuthorizationCode, rotateRefreshToken, getLiveGrant, revokeByRefreshToken, revokeGrant, listGrants, type RevokedGrant, } 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.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; } });
|
|
@@ -15,6 +15,8 @@ Object.defineProperty(exports, "getLiveGrant", { enumerable: true, get: function
|
|
|
15
15
|
Object.defineProperty(exports, "revokeByRefreshToken", { enumerable: true, get: function () { return store_1.revokeByRefreshToken; } });
|
|
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
|
+
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; } });
|
|
18
20
|
var access_token_1 = require("./access-token");
|
|
19
21
|
Object.defineProperty(exports, "signAccessToken", { enumerable: true, get: function () { return access_token_1.signAccessToken; } });
|
|
20
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;
|
|
@@ -26,9 +26,28 @@ export declare const ROTATE_REFRESH_TOKEN = "\n WITH old AS (\n UPDATE lt_oa
|
|
|
26
26
|
* outside the retry grace window: the token has leaked.
|
|
27
27
|
*/
|
|
28
28
|
export declare const REVOKE_ON_REFRESH_REUSE = "\n UPDATE lt_oauth_grants g SET revoked_at = NOW()\n FROM lt_oauth_refresh_tokens t\n WHERE t.token_hash = $1 AND t.used_at IS NOT NULL\n AND t.used_at < NOW() - make_interval(secs => $2)\n AND g.id = t.grant_id AND g.revoked_at IS NULL\n RETURNING g.id AS grant_id, g.user_id";
|
|
29
|
+
/**
|
|
30
|
+
* Revoke the grant of an authorization code presented again after it was
|
|
31
|
+
* exchanged: the code has leaked (RFC 6749 §4.1.2).
|
|
32
|
+
*/
|
|
33
|
+
export declare const REVOKE_ON_CODE_REUSE = "\n UPDATE lt_oauth_grants g SET revoked_at = NOW()\n FROM lt_oauth_codes c\n WHERE c.code_hash = $1 AND c.used_at IS NOT NULL\n AND g.id = c.grant_id AND g.revoked_at IS NULL\n RETURNING g.id AS grant_id, g.user_id";
|
|
29
34
|
/** Revoke the grant a refresh token belongs to, for the client presenting it. */
|
|
30
35
|
export declare const REVOKE_BY_REFRESH_TOKEN = "\n UPDATE lt_oauth_grants g SET revoked_at = NOW()\n FROM lt_oauth_refresh_tokens t\n WHERE t.token_hash = $1 AND g.id = t.grant_id AND g.client_id = $2 AND g.revoked_at IS NULL\n RETURNING g.id AS grant_id, g.user_id";
|
|
31
36
|
/** Revoke a grant by id, optionally only when it belongs to the given user. */
|
|
32
37
|
export declare const REVOKE_GRANT = "\n UPDATE lt_oauth_grants SET revoked_at = NOW()\n WHERE id = $1 AND ($2::uuid IS NULL OR user_id = $2) AND revoked_at IS NULL\n RETURNING id AS grant_id, user_id";
|
|
33
38
|
/** A person's live grants that a client has put to use, newest first. */
|
|
34
39
|
export declare const LIST_GRANTS = "\n SELECT g.id AS grant_id, g.client_id, c.client_name, g.policy, g.scope, g.created_at,\n (SELECT MAX(t.created_at) FROM lt_oauth_refresh_tokens t WHERE t.grant_id = g.id) AS last_used_at\n FROM lt_oauth_grants g\n JOIN lt_oauth_clients c ON c.client_id = g.client_id\n WHERE g.user_id = $1 AND g.revoked_at IS NULL\n AND EXISTS (SELECT 1 FROM lt_oauth_refresh_tokens t WHERE t.grant_id = g.id)\n ORDER BY g.created_at DESC";
|
|
40
|
+
/** Refresh tokens past expiry, or rotated out, longer ago than the window. */
|
|
41
|
+
export declare const PRUNE_REFRESH_TOKENS = "\n DELETE FROM lt_oauth_refresh_tokens\n WHERE expires_at < NOW() - $1::interval OR used_at < NOW() - $1::interval";
|
|
42
|
+
/** Authorization codes past expiry longer ago than the window, exchanged or not. */
|
|
43
|
+
export declare const PRUNE_CODES = "\n DELETE FROM lt_oauth_codes WHERE expires_at < NOW() - $1::interval";
|
|
44
|
+
/**
|
|
45
|
+
* Grants revoked longer ago than the window, and grants older than the window
|
|
46
|
+
* left with no refresh token and no code: never exchanged, or long unused.
|
|
47
|
+
*/
|
|
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
|
+
* 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)";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// ── OAuth authorization server queries ─────────────────────────────────────
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
exports.LIST_GRANTS = exports.REVOKE_GRANT = exports.REVOKE_BY_REFRESH_TOKEN = exports.REVOKE_ON_REFRESH_REUSE = exports.ROTATE_REFRESH_TOKEN = exports.GET_LIVE_GRANT = exports.EXCHANGE_CODE = exports.ISSUE_CODE = exports.GET_CLIENT = exports.INSERT_CLIENT = void 0;
|
|
4
|
+
exports.PRUNE_CLIENTS = exports.PRUNE_GRANTS = exports.PRUNE_CODES = exports.PRUNE_REFRESH_TOKENS = exports.LIST_GRANTS = exports.REVOKE_GRANT = exports.REVOKE_BY_REFRESH_TOKEN = exports.REVOKE_ON_CODE_REUSE = exports.REVOKE_ON_REFRESH_REUSE = exports.ROTATE_REFRESH_TOKEN = exports.GET_LIVE_GRANT = exports.EXCHANGE_CODE = exports.ISSUE_CODE = exports.GET_CLIENT = exports.INSERT_CLIENT = void 0;
|
|
5
5
|
/** The grant owner's current memberships, as a JSON array. Expects alias `g`. */
|
|
6
6
|
const GRANT_ROLES = `
|
|
7
7
|
COALESCE((
|
|
@@ -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
|
|
@@ -101,6 +104,16 @@ exports.REVOKE_ON_REFRESH_REUSE = `
|
|
|
101
104
|
AND t.used_at < NOW() - make_interval(secs => $2)
|
|
102
105
|
AND g.id = t.grant_id AND g.revoked_at IS NULL
|
|
103
106
|
RETURNING g.id AS grant_id, g.user_id`;
|
|
107
|
+
/**
|
|
108
|
+
* Revoke the grant of an authorization code presented again after it was
|
|
109
|
+
* exchanged: the code has leaked (RFC 6749 §4.1.2).
|
|
110
|
+
*/
|
|
111
|
+
exports.REVOKE_ON_CODE_REUSE = `
|
|
112
|
+
UPDATE lt_oauth_grants g SET revoked_at = NOW()
|
|
113
|
+
FROM lt_oauth_codes c
|
|
114
|
+
WHERE c.code_hash = $1 AND c.used_at IS NOT NULL
|
|
115
|
+
AND g.id = c.grant_id AND g.revoked_at IS NULL
|
|
116
|
+
RETURNING g.id AS grant_id, g.user_id`;
|
|
104
117
|
/** Revoke the grant a refresh token belongs to, for the client presenting it. */
|
|
105
118
|
exports.REVOKE_BY_REFRESH_TOKEN = `
|
|
106
119
|
UPDATE lt_oauth_grants g SET revoked_at = NOW()
|
|
@@ -121,3 +134,29 @@ exports.LIST_GRANTS = `
|
|
|
121
134
|
WHERE g.user_id = $1 AND g.revoked_at IS NULL
|
|
122
135
|
AND EXISTS (SELECT 1 FROM lt_oauth_refresh_tokens t WHERE t.grant_id = g.id)
|
|
123
136
|
ORDER BY g.created_at DESC`;
|
|
137
|
+
// ── Maintenance: each statement takes the retention window as $1 (an interval) ──
|
|
138
|
+
/** Refresh tokens past expiry, or rotated out, longer ago than the window. */
|
|
139
|
+
exports.PRUNE_REFRESH_TOKENS = `
|
|
140
|
+
DELETE FROM lt_oauth_refresh_tokens
|
|
141
|
+
WHERE expires_at < NOW() - $1::interval OR used_at < NOW() - $1::interval`;
|
|
142
|
+
/** Authorization codes past expiry longer ago than the window, exchanged or not. */
|
|
143
|
+
exports.PRUNE_CODES = `
|
|
144
|
+
DELETE FROM lt_oauth_codes WHERE expires_at < NOW() - $1::interval`;
|
|
145
|
+
/**
|
|
146
|
+
* Grants revoked longer ago than the window, and grants older than the window
|
|
147
|
+
* left with no refresh token and no code: never exchanged, or long unused.
|
|
148
|
+
*/
|
|
149
|
+
exports.PRUNE_GRANTS = `
|
|
150
|
+
DELETE FROM lt_oauth_grants g
|
|
151
|
+
WHERE g.revoked_at < NOW() - $1::interval
|
|
152
|
+
OR (g.created_at < NOW() - $1::interval
|
|
153
|
+
AND NOT EXISTS (SELECT 1 FROM lt_oauth_refresh_tokens t WHERE t.grant_id = g.id)
|
|
154
|
+
AND NOT EXISTS (SELECT 1 FROM lt_oauth_codes c WHERE c.grant_id = g.id))`;
|
|
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
|
+
*/
|
|
159
|
+
exports.PRUNE_CLIENTS = `
|
|
160
|
+
DELETE FROM lt_oauth_clients c
|
|
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)`;
|
|
@@ -34,7 +34,8 @@ export declare function issueAuthorizationCode(input: {
|
|
|
34
34
|
/**
|
|
35
35
|
* Exchange a code for the grant and its first refresh token. Null when the
|
|
36
36
|
* code is unknown, used, expired, for another client or redirect URI, fails
|
|
37
|
-
* PKCE, or its grant or owner is no longer active.
|
|
37
|
+
* PKCE, or its grant or owner is no longer active. A code presented again
|
|
38
|
+
* after its exchange revokes its grant.
|
|
38
39
|
*/
|
|
39
40
|
export declare function exchangeAuthorizationCode(input: {
|
|
40
41
|
code: string;
|
|
@@ -72,3 +73,23 @@ export declare function revokeByRefreshToken(refreshToken: string, clientId: str
|
|
|
72
73
|
export declare function revokeGrant(grantId: string, userId?: string): Promise<RevokedGrant | null>;
|
|
73
74
|
/** The person's live grants that a client has put to use. */
|
|
74
75
|
export declare function listGrants(userId: string): Promise<LTGrantSummary[]>;
|
|
76
|
+
export interface OAuthPruneCounts {
|
|
77
|
+
refreshTokens: number;
|
|
78
|
+
codes: number;
|
|
79
|
+
grants: number;
|
|
80
|
+
clients: number;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Delete what the authorization server no longer needs, older than the
|
|
84
|
+
* window (a Postgres interval): spent and expired refresh tokens and codes,
|
|
85
|
+
* then grants with neither left or revoked, then clients never consented to.
|
|
86
|
+
* Each step reads what the one before it left.
|
|
87
|
+
*/
|
|
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>;
|
|
@@ -46,8 +46,12 @@ exports.getLiveGrant = getLiveGrant;
|
|
|
46
46
|
exports.revokeByRefreshToken = revokeByRefreshToken;
|
|
47
47
|
exports.revokeGrant = revokeGrant;
|
|
48
48
|
exports.listGrants = listGrants;
|
|
49
|
+
exports.pruneOAuthServer = pruneOAuthServer;
|
|
50
|
+
exports.sweepOAuthServer = sweepOAuthServer;
|
|
49
51
|
const crypto = __importStar(require("crypto"));
|
|
50
52
|
const db_1 = require("../../../lib/db");
|
|
53
|
+
const logger_1 = require("../../../lib/logger");
|
|
54
|
+
const config_1 = require("../../../modules/config");
|
|
51
55
|
const uuid_1 = require("../../../lib/uuid");
|
|
52
56
|
const sql_1 = require("./sql");
|
|
53
57
|
/** A refresh token reused within this many seconds is treated as a client retry. */
|
|
@@ -68,6 +72,7 @@ async function registerClient(input) {
|
|
|
68
72
|
const { rows } = await (0, db_1.getPool)().query(sql_1.INSERT_CLIENT, [
|
|
69
73
|
`ltc_${createSecret()}`, input.clientName ?? null, input.redirectUris,
|
|
70
74
|
]);
|
|
75
|
+
void sweepOAuthServer();
|
|
71
76
|
return rows[0];
|
|
72
77
|
}
|
|
73
78
|
/** A client id as registerClient issues it; anything else names no client and never reaches SQL. */
|
|
@@ -89,21 +94,28 @@ async function issueAuthorizationCode(input) {
|
|
|
89
94
|
input.userId, input.clientId, input.policy, input.scope,
|
|
90
95
|
hashSecret(code), input.redirectUri, input.codeChallenge, input.resource ?? null, input.ttlSeconds,
|
|
91
96
|
]);
|
|
97
|
+
void sweepOAuthServer();
|
|
92
98
|
return { code, grantId: rows[0].grant_id };
|
|
93
99
|
}
|
|
94
100
|
/**
|
|
95
101
|
* Exchange a code for the grant and its first refresh token. Null when the
|
|
96
102
|
* code is unknown, used, expired, for another client or redirect URI, fails
|
|
97
|
-
* PKCE, or its grant or owner is no longer active.
|
|
103
|
+
* PKCE, or its grant or owner is no longer active. A code presented again
|
|
104
|
+
* after its exchange revokes its grant.
|
|
98
105
|
*/
|
|
99
106
|
async function exchangeAuthorizationCode(input) {
|
|
107
|
+
const pool = (0, db_1.getPool)();
|
|
100
108
|
const refreshToken = createSecret();
|
|
101
|
-
const
|
|
102
|
-
|
|
109
|
+
const codeHash = hashSecret(input.code);
|
|
110
|
+
const { rows } = await pool.query(sql_1.EXCHANGE_CODE, [
|
|
111
|
+
codeHash, input.clientId, input.redirectUri, pkceChallenge(input.codeVerifier),
|
|
103
112
|
hashSecret(refreshToken), input.refreshTtlSeconds,
|
|
104
113
|
]);
|
|
105
|
-
if (!rows[0])
|
|
114
|
+
if (!rows[0]) {
|
|
115
|
+
await pool.query(sql_1.REVOKE_ON_CODE_REUSE, [codeHash]);
|
|
106
116
|
return null;
|
|
117
|
+
}
|
|
118
|
+
void sweepOAuthServer();
|
|
107
119
|
const { resource, ...snapshot } = rows[0];
|
|
108
120
|
return { snapshot, resource, refreshToken };
|
|
109
121
|
}
|
|
@@ -118,8 +130,10 @@ async function rotateRefreshToken(input) {
|
|
|
118
130
|
const next = createSecret();
|
|
119
131
|
const oldHash = hashSecret(input.refreshToken);
|
|
120
132
|
const { rows } = await pool.query(sql_1.ROTATE_REFRESH_TOKEN, [oldHash, hashSecret(next), input.refreshTtlSeconds, input.clientId]);
|
|
121
|
-
if (rows[0])
|
|
133
|
+
if (rows[0]) {
|
|
134
|
+
void sweepOAuthServer();
|
|
122
135
|
return { snapshot: rows[0], refreshToken: next };
|
|
136
|
+
}
|
|
123
137
|
const reuse = await pool.query(sql_1.REVOKE_ON_REFRESH_REUSE, [oldHash, exports.REFRESH_REUSE_GRACE_SECONDS]);
|
|
124
138
|
return { snapshot: null, revoked: reuse.rows[0] ?? null };
|
|
125
139
|
}
|
|
@@ -149,3 +163,38 @@ async function listGrants(userId) {
|
|
|
149
163
|
const { rows } = await (0, db_1.getPool)().query(sql_1.LIST_GRANTS, [userId]);
|
|
150
164
|
return rows;
|
|
151
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Delete what the authorization server no longer needs, older than the
|
|
168
|
+
* window (a Postgres interval): spent and expired refresh tokens and codes,
|
|
169
|
+
* then grants with neither left or revoked, then clients never consented to.
|
|
170
|
+
* Each step reads what the one before it left.
|
|
171
|
+
*/
|
|
172
|
+
async function pruneOAuthServer(olderThan) {
|
|
173
|
+
const pool = (0, db_1.getPool)();
|
|
174
|
+
const refreshTokens = (await pool.query(sql_1.PRUNE_REFRESH_TOKENS, [olderThan])).rowCount ?? 0;
|
|
175
|
+
const codes = (await pool.query(sql_1.PRUNE_CODES, [olderThan])).rowCount ?? 0;
|
|
176
|
+
const grants = (await pool.query(sql_1.PRUNE_GRANTS, [olderThan])).rowCount ?? 0;
|
|
177
|
+
const clients = (await pool.query(sql_1.PRUNE_CLIENTS, [olderThan])).rowCount ?? 0;
|
|
178
|
+
return { refreshTokens, codes, grants, clients };
|
|
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
|
+
}
|
|
@@ -65,6 +65,7 @@ function validateCronSchedule(expr) {
|
|
|
65
65
|
const CRON_TOPIC_PREFIX = 'lt.cron';
|
|
66
66
|
const CRON_ID_PREFIX = 'lt-cron';
|
|
67
67
|
const YAML_CRON_TOPIC_PREFIX = 'lt.cron.yaml';
|
|
68
|
+
const CRON_SOURCE = 'cron';
|
|
68
69
|
const YAML_CRON_ID_PREFIX = 'lt-cron-yaml';
|
|
69
70
|
/**
|
|
70
71
|
* Singleton registry for workflow cron schedules.
|
|
@@ -241,9 +242,10 @@ class LTCronRegistry {
|
|
|
241
242
|
try {
|
|
242
243
|
cronCallCount++;
|
|
243
244
|
logger_1.loggerRegistry.info(`[lt-cron] tick #${cronCallCount} at ${new Date().toISOString()} — pub(${appId}/${graphTopic})`);
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
245
|
+
// Through the invoke service, so the run carries only the scope the
|
|
246
|
+
// server resolves for execute_as, never one written in the envelope.
|
|
247
|
+
const { invokeYamlWorkflow } = await Promise.resolve().then(() => __importStar(require('../yaml-workflow/invoke')));
|
|
248
|
+
await invokeYamlWorkflow(wf, { data: cronEnvelope, execute_as: wf.execute_as ?? undefined, source: CRON_SOURCE });
|
|
247
249
|
logger_1.loggerRegistry.info(`[lt-cron] tick #${cronCallCount} published successfully`);
|
|
248
250
|
}
|
|
249
251
|
catch (err) {
|
|
@@ -106,6 +106,14 @@ async function executeRule(appId, rule) {
|
|
|
106
106
|
}
|
|
107
107
|
break;
|
|
108
108
|
}
|
|
109
|
+
// Drop the authorization server's dead rows: spent or expired tokens and
|
|
110
|
+
// codes, grants with neither left or long revoked, clients never consented to.
|
|
111
|
+
case rule.target === 'oauth' && rule.action === 'delete': {
|
|
112
|
+
const { pruneOAuthServer } = await Promise.resolve().then(() => __importStar(require('../auth/oauth-server')));
|
|
113
|
+
const counts = await pruneOAuthServer(rule.olderThan);
|
|
114
|
+
logger_1.loggerRegistry.info(`[lt-maintenance] oauth pruned: ${JSON.stringify(counts)} (olderThan: ${rule.olderThan})`);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
109
117
|
default:
|
|
110
118
|
logger_1.loggerRegistry.warn(`[lt-maintenance] unknown rule, skipping: ${JSON.stringify(rule)}`);
|
|
111
119
|
}
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
* - `mcp:full` — all tools (automation, orchestration)
|
|
7
7
|
*
|
|
8
8
|
* Idempotent — skips creation if the account already exists.
|
|
9
|
-
*
|
|
9
|
+
* With LT_SEED_SERVICE_KEYS=true (local docker compose) the keys are
|
|
10
|
+
* generated once and printed to stdout, never to the logger; they cannot be
|
|
11
|
+
* retrieved again. Otherwise the account is created without keys and a
|
|
12
|
+
* superadmin mints them from the dashboard or the bot-accounts API.
|
|
10
13
|
*/
|
|
11
14
|
export declare function seedMcpServiceAccount(): Promise<void>;
|