@hotmeshio/long-tail 0.24.0 → 0.24.1
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/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 +5 -0
- package/build/modules/config.js +7 -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 +2 -1
- package/build/services/auth/oauth-server/sql.d.ts +16 -0
- package/build/services/auth/oauth-server/sql.js +34 -1
- package/build/services/auth/oauth-server/store.d.ts +15 -1
- package/build/services/auth/oauth-server/store.js +24 -4
- 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/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,6 +28,7 @@ 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;
|
|
28
33
|
HONEYCOMB_API_KEY: string;
|
|
29
34
|
ROLE_ENFORCEMENT_CACHE_TTL_MS: 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,6 +40,9 @@ 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 || '',
|
|
41
48
|
HONEYCOMB_API_KEY: process.env.HONEYCOMB_API_KEY || '',
|
|
@@ -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 with no live grant) 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 with no live grant) 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, 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.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,7 @@ 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; } });
|
|
18
19
|
var access_token_1 = require("./access-token");
|
|
19
20
|
Object.defineProperty(exports, "signAccessToken", { enumerable: true, get: function () { return access_token_1.signAccessToken; } });
|
|
20
21
|
Object.defineProperty(exports, "verifyAccessToken", { enumerable: true, get: function () { return access_token_1.verifyAccessToken; } });
|
|
@@ -26,9 +26,25 @@ 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
|
+
/** Clients registered longer ago than the window with no live grant. */
|
|
50
|
+
export declare const PRUNE_CLIENTS = "\n DELETE FROM lt_oauth_clients c\n WHERE c.created_at < NOW() - $1::interval\n AND NOT EXISTS (SELECT 1 FROM lt_oauth_grants g WHERE g.client_id = c.client_id AND g.revoked_at IS NULL)";
|
|
@@ -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((
|
|
@@ -101,6 +101,16 @@ exports.REVOKE_ON_REFRESH_REUSE = `
|
|
|
101
101
|
AND t.used_at < NOW() - make_interval(secs => $2)
|
|
102
102
|
AND g.id = t.grant_id AND g.revoked_at IS NULL
|
|
103
103
|
RETURNING g.id AS grant_id, g.user_id`;
|
|
104
|
+
/**
|
|
105
|
+
* Revoke the grant of an authorization code presented again after it was
|
|
106
|
+
* exchanged: the code has leaked (RFC 6749 §4.1.2).
|
|
107
|
+
*/
|
|
108
|
+
exports.REVOKE_ON_CODE_REUSE = `
|
|
109
|
+
UPDATE lt_oauth_grants g SET revoked_at = NOW()
|
|
110
|
+
FROM lt_oauth_codes c
|
|
111
|
+
WHERE c.code_hash = $1 AND c.used_at IS NOT NULL
|
|
112
|
+
AND g.id = c.grant_id AND g.revoked_at IS NULL
|
|
113
|
+
RETURNING g.id AS grant_id, g.user_id`;
|
|
104
114
|
/** Revoke the grant a refresh token belongs to, for the client presenting it. */
|
|
105
115
|
exports.REVOKE_BY_REFRESH_TOKEN = `
|
|
106
116
|
UPDATE lt_oauth_grants g SET revoked_at = NOW()
|
|
@@ -121,3 +131,26 @@ exports.LIST_GRANTS = `
|
|
|
121
131
|
WHERE g.user_id = $1 AND g.revoked_at IS NULL
|
|
122
132
|
AND EXISTS (SELECT 1 FROM lt_oauth_refresh_tokens t WHERE t.grant_id = g.id)
|
|
123
133
|
ORDER BY g.created_at DESC`;
|
|
134
|
+
// ── Maintenance: each statement takes the retention window as $1 (an interval) ──
|
|
135
|
+
/** Refresh tokens past expiry, or rotated out, longer ago than the window. */
|
|
136
|
+
exports.PRUNE_REFRESH_TOKENS = `
|
|
137
|
+
DELETE FROM lt_oauth_refresh_tokens
|
|
138
|
+
WHERE expires_at < NOW() - $1::interval OR used_at < NOW() - $1::interval`;
|
|
139
|
+
/** Authorization codes past expiry longer ago than the window, exchanged or not. */
|
|
140
|
+
exports.PRUNE_CODES = `
|
|
141
|
+
DELETE FROM lt_oauth_codes WHERE expires_at < NOW() - $1::interval`;
|
|
142
|
+
/**
|
|
143
|
+
* Grants revoked longer ago than the window, and grants older than the window
|
|
144
|
+
* left with no refresh token and no code: never exchanged, or long unused.
|
|
145
|
+
*/
|
|
146
|
+
exports.PRUNE_GRANTS = `
|
|
147
|
+
DELETE FROM lt_oauth_grants g
|
|
148
|
+
WHERE g.revoked_at < NOW() - $1::interval
|
|
149
|
+
OR (g.created_at < NOW() - $1::interval
|
|
150
|
+
AND NOT EXISTS (SELECT 1 FROM lt_oauth_refresh_tokens t WHERE t.grant_id = g.id)
|
|
151
|
+
AND NOT EXISTS (SELECT 1 FROM lt_oauth_codes c WHERE c.grant_id = g.id))`;
|
|
152
|
+
/** Clients registered longer ago than the window with no live grant. */
|
|
153
|
+
exports.PRUNE_CLIENTS = `
|
|
154
|
+
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 AND g.revoked_at IS NULL)`;
|
|
@@ -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,16 @@ 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 with no live grant.
|
|
86
|
+
* Each step reads what the one before it left.
|
|
87
|
+
*/
|
|
88
|
+
export declare function pruneOAuthServer(olderThan: string): Promise<OAuthPruneCounts>;
|
|
@@ -46,6 +46,7 @@ exports.getLiveGrant = getLiveGrant;
|
|
|
46
46
|
exports.revokeByRefreshToken = revokeByRefreshToken;
|
|
47
47
|
exports.revokeGrant = revokeGrant;
|
|
48
48
|
exports.listGrants = listGrants;
|
|
49
|
+
exports.pruneOAuthServer = pruneOAuthServer;
|
|
49
50
|
const crypto = __importStar(require("crypto"));
|
|
50
51
|
const db_1 = require("../../../lib/db");
|
|
51
52
|
const uuid_1 = require("../../../lib/uuid");
|
|
@@ -94,16 +95,21 @@ async function issueAuthorizationCode(input) {
|
|
|
94
95
|
/**
|
|
95
96
|
* Exchange a code for the grant and its first refresh token. Null when the
|
|
96
97
|
* code is unknown, used, expired, for another client or redirect URI, fails
|
|
97
|
-
* PKCE, or its grant or owner is no longer active.
|
|
98
|
+
* PKCE, or its grant or owner is no longer active. A code presented again
|
|
99
|
+
* after its exchange revokes its grant.
|
|
98
100
|
*/
|
|
99
101
|
async function exchangeAuthorizationCode(input) {
|
|
102
|
+
const pool = (0, db_1.getPool)();
|
|
100
103
|
const refreshToken = createSecret();
|
|
101
|
-
const
|
|
102
|
-
|
|
104
|
+
const codeHash = hashSecret(input.code);
|
|
105
|
+
const { rows } = await pool.query(sql_1.EXCHANGE_CODE, [
|
|
106
|
+
codeHash, input.clientId, input.redirectUri, pkceChallenge(input.codeVerifier),
|
|
103
107
|
hashSecret(refreshToken), input.refreshTtlSeconds,
|
|
104
108
|
]);
|
|
105
|
-
if (!rows[0])
|
|
109
|
+
if (!rows[0]) {
|
|
110
|
+
await pool.query(sql_1.REVOKE_ON_CODE_REUSE, [codeHash]);
|
|
106
111
|
return null;
|
|
112
|
+
}
|
|
107
113
|
const { resource, ...snapshot } = rows[0];
|
|
108
114
|
return { snapshot, resource, refreshToken };
|
|
109
115
|
}
|
|
@@ -149,3 +155,17 @@ async function listGrants(userId) {
|
|
|
149
155
|
const { rows } = await (0, db_1.getPool)().query(sql_1.LIST_GRANTS, [userId]);
|
|
150
156
|
return rows;
|
|
151
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Delete what the authorization server no longer needs, older than the
|
|
160
|
+
* window (a Postgres interval): spent and expired refresh tokens and codes,
|
|
161
|
+
* then grants with neither left or revoked, then clients with no live grant.
|
|
162
|
+
* Each step reads what the one before it left.
|
|
163
|
+
*/
|
|
164
|
+
async function pruneOAuthServer(olderThan) {
|
|
165
|
+
const pool = (0, db_1.getPool)();
|
|
166
|
+
const refreshTokens = (await pool.query(sql_1.PRUNE_REFRESH_TOKENS, [olderThan])).rowCount ?? 0;
|
|
167
|
+
const codes = (await pool.query(sql_1.PRUNE_CODES, [olderThan])).rowCount ?? 0;
|
|
168
|
+
const grants = (await pool.query(sql_1.PRUNE_GRANTS, [olderThan])).rowCount ?? 0;
|
|
169
|
+
const clients = (await pool.query(sql_1.PRUNE_CLIENTS, [olderThan])).rowCount ?? 0;
|
|
170
|
+
return { refreshTokens, codes, grants, clients };
|
|
171
|
+
}
|
|
@@ -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 with no live grant.
|
|
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>;
|
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
* - `mcp:full` — all tools (automation, orchestration)
|
|
8
8
|
*
|
|
9
9
|
* Idempotent — skips creation if the account already exists.
|
|
10
|
-
*
|
|
10
|
+
* With LT_SEED_SERVICE_KEYS=true (local docker compose) the keys are
|
|
11
|
+
* generated once and printed to stdout, never to the logger; they cannot be
|
|
12
|
+
* retrieved again. Otherwise the account is created without keys and a
|
|
13
|
+
* superadmin mints them from the dashboard or the bot-accounts API.
|
|
11
14
|
*/
|
|
12
15
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
13
16
|
if (k2 === undefined) k2 = k;
|
|
@@ -45,6 +48,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
45
48
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
49
|
exports.seedMcpServiceAccount = seedMcpServiceAccount;
|
|
47
50
|
const logger_1 = require("../../lib/logger");
|
|
51
|
+
const config_1 = require("../../modules/config");
|
|
48
52
|
const crud_1 = require("../user/crud");
|
|
49
53
|
const iam = __importStar(require("../iam"));
|
|
50
54
|
const SERVICE_ACCOUNT_NAME = 'mcp-service';
|
|
@@ -63,13 +67,14 @@ async function seedMcpServiceAccount() {
|
|
|
63
67
|
display_name: 'MCP Service',
|
|
64
68
|
roles: [{ role: 'system', type: 'member' }],
|
|
65
69
|
});
|
|
66
|
-
|
|
70
|
+
if (!config_1.config.LT_SEED_SERVICE_KEYS) {
|
|
71
|
+
logger_1.loggerRegistry.info(`[seed-mcp] ${SERVICE_ACCOUNT_NAME} created; mint its API keys from the dashboard`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
67
74
|
const readKey = await iam.createBotKey(bot.id, 'read', ['mcp:read']);
|
|
68
|
-
logger_1.loggerRegistry.info(`[seed-mcp] read key: ${readKey.rawKey}`);
|
|
69
|
-
// Generate full key (scoped to mcp:full)
|
|
70
75
|
const fullKey = await iam.createBotKey(bot.id, 'full', ['mcp:read', 'mcp:full']);
|
|
71
|
-
|
|
72
|
-
logger_1.loggerRegistry.info(`[seed-mcp] ${SERVICE_ACCOUNT_NAME} created with read + full API keys`);
|
|
76
|
+
process.stdout.write(`[seed-mcp] read key: ${readKey.rawKey}\n[seed-mcp] full key: ${fullKey.rawKey}\n`);
|
|
77
|
+
logger_1.loggerRegistry.info(`[seed-mcp] ${SERVICE_ACCOUNT_NAME} created with read + full API keys (printed to stdout)`);
|
|
73
78
|
}
|
|
74
79
|
catch (err) {
|
|
75
80
|
logger_1.loggerRegistry.warn(`[seed-mcp] failed to seed: ${err.message}`);
|
|
@@ -36,11 +36,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.invokeYamlWorkflow = invokeYamlWorkflow;
|
|
37
37
|
const yamlDeployer = __importStar(require("./deployer"));
|
|
38
38
|
const principal_1 = require("../iam/principal");
|
|
39
|
+
const workflow_invocation_1 = require("../workflow-invocation");
|
|
39
40
|
const publish_1 = require("../../lib/events/publish");
|
|
40
41
|
/**
|
|
41
42
|
* The identity context compiled workflow activities run under, from the
|
|
42
43
|
* server-side identity: execute_as with the invoker as initiator, else the
|
|
43
|
-
* invoker. Never read from the caller's data.
|
|
44
|
+
* invoker. Never read from the caller's data. An identity that does not
|
|
45
|
+
* resolve is refused rather than run with none.
|
|
44
46
|
*/
|
|
45
47
|
async function resolveScope(executeAs, userId) {
|
|
46
48
|
if (executeAs) {
|
|
@@ -49,7 +51,7 @@ async function resolveScope(executeAs, userId) {
|
|
|
49
51
|
userId ? (0, principal_1.resolvePrincipal)(userId) : Promise.resolve(null),
|
|
50
52
|
]);
|
|
51
53
|
if (!botPrincipal)
|
|
52
|
-
|
|
54
|
+
throw new workflow_invocation_1.InvocationError(`execute_as principal "${executeAs}" not found`, 404);
|
|
53
55
|
return {
|
|
54
56
|
principal: botPrincipal,
|
|
55
57
|
scopes: ['mcp:tool:call'],
|
|
@@ -59,7 +61,9 @@ async function resolveScope(executeAs, userId) {
|
|
|
59
61
|
if (!userId)
|
|
60
62
|
return undefined;
|
|
61
63
|
const principal = await (0, principal_1.resolvePrincipal)(userId);
|
|
62
|
-
|
|
64
|
+
if (!principal)
|
|
65
|
+
throw new workflow_invocation_1.InvocationError('User not registered', 403);
|
|
66
|
+
return { principal, scopes: ['mcp:tool:call'] };
|
|
63
67
|
}
|
|
64
68
|
/**
|
|
65
69
|
* Invoke a YAML workflow with scope injection.
|
|
@@ -40,10 +40,9 @@ const db_1 = require("../../../lib/db");
|
|
|
40
40
|
const logger_1 = require("../../../lib/logger");
|
|
41
41
|
const system_events_1 = require("../../../lib/events/system-events");
|
|
42
42
|
const ephemeral_1 = require("../../iam/ephemeral");
|
|
43
|
-
const mcpClient = __importStar(require("../../mcp/client"));
|
|
44
|
-
const connection_1 = require("../../mcp/client/connection");
|
|
45
43
|
const yamlDb = __importStar(require("../db"));
|
|
46
44
|
const scope_1 = require("./scope");
|
|
45
|
+
const step_tool_1 = require("./step-tool");
|
|
47
46
|
const callbacks_1 = require("./callbacks");
|
|
48
47
|
const events_1 = require("./events");
|
|
49
48
|
/**
|
|
@@ -156,7 +155,7 @@ async function registerWorkersForWorkflow(workflow) {
|
|
|
156
155
|
delete mergedArgs._scope;
|
|
157
156
|
delete mergedArgs.workflowName;
|
|
158
157
|
mergedArgs = await (0, ephemeral_1.exchangeTokensInArgs)(mergedArgs);
|
|
159
|
-
const result = await
|
|
158
|
+
const result = await (0, step_tool_1.callStepTool)(dbServerId, toolName, mergedArgs);
|
|
160
159
|
logger_1.loggerRegistry.debug(`[yaml-workflow:worker] leaving db/${toolName} wf=${wfName} resultKeys=[${Object.keys(result || {}).join(',')}]`);
|
|
161
160
|
return { metadata: { ...data.metadata }, data: result };
|
|
162
161
|
}),
|
|
@@ -230,16 +229,7 @@ async function registerWorkersForWorkflow(workflow) {
|
|
|
230
229
|
}
|
|
231
230
|
const exchangedArgs = await (0, ephemeral_1.exchangeTokensInArgs)(mergedArgs);
|
|
232
231
|
const coercedArgs = coerceNumericObjects(exchangedArgs);
|
|
233
|
-
|
|
234
|
-
// Falls through to mcpClient.callServerTool() for external servers.
|
|
235
|
-
let result;
|
|
236
|
-
const builtin = await (0, connection_1.dispatchBuiltinTool)(serverId, toolName, coercedArgs);
|
|
237
|
-
if (builtin) {
|
|
238
|
-
result = builtin.result;
|
|
239
|
-
}
|
|
240
|
-
else {
|
|
241
|
-
result = await mcpClient.callServerTool(serverId, toolName, coercedArgs);
|
|
242
|
-
}
|
|
232
|
+
const result = await (0, step_tool_1.callStepTool)(serverId, toolName, coercedArgs);
|
|
243
233
|
if (result && typeof result === 'object' && 'error' in result) {
|
|
244
234
|
logger_1.loggerRegistry.error(`[yaml-workflow:worker] ${toolName} error: ${JSON.stringify(result).slice(0, 200)}`);
|
|
245
235
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Call a step's tool. A built-in tool runs as the principal the workflow was
|
|
3
|
+
* started for (its `_scope`), under the tool's manifest gate, so a step never
|
|
4
|
+
* does more than its invoker may. A run started in-process with no principal
|
|
5
|
+
* calls it as lt-system. Other servers are called as before.
|
|
6
|
+
*/
|
|
7
|
+
export declare function callStepTool(serverId: string, toolName: string, args: Record<string, unknown>): Promise<any>;
|