@lowdefy/api 5.6.0 → 6.0.0
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/dist/context/createAuthorize.js +4 -1
- package/dist/context/resolveStrategyCaller.js +56 -0
- package/dist/index.js +12 -2
- package/dist/routes/agent/callAgent.js +7 -184
- package/dist/routes/agent/prepareAgent.js +173 -0
- package/dist/routes/auth/createLogger.js +2 -5
- package/dist/routes/auth/createPrefixedCookies.js +52 -0
- package/dist/routes/auth/{getNextAuthConfig.js → getAuthConfig.js} +27 -16
- package/dist/routes/auth/resolveCookies.js +37 -0
- package/dist/routes/auth/strategies/createAuthStrategies.js +77 -0
- package/dist/routes/auth/strategies/getAuthStrategies.js +31 -0
- package/dist/routes/endpoints/authorizeApiEndpoint.js +8 -2
- package/dist/routes/endpoints/callEndpoint.js +26 -3
- package/dist/routes/endpoints/control/controlReject.js +3 -1
- package/dist/routes/endpoints/control/controlThrow.js +4 -1
- package/dist/routes/endpoints/findSchedule.js +35 -0
- package/dist/routes/endpoints/forwardScheduledEndpoint.js +106 -0
- package/dist/routes/endpoints/getEndpointConfig.js +9 -3
- package/dist/routes/endpoints/getEnvironmentSchedules.js +29 -0
- package/dist/routes/endpoints/handleAgentCall.js +83 -0
- package/dist/routes/endpoints/handleEndpointCall.js +49 -1
- package/dist/routes/endpoints/handleRenderNotification.js +189 -0
- package/dist/routes/endpoints/handleValidateSchema.js +3 -1
- package/dist/routes/endpoints/isUnauthenticatedHuman.js +31 -0
- package/dist/routes/endpoints/resolveCronEnvironment.js +37 -0
- package/dist/routes/endpoints/runDetachedEndpoint.js +66 -0
- package/dist/routes/endpoints/runRoutine.js +12 -0
- package/dist/routes/endpoints/runScheduledEndpoint.js +104 -0
- package/dist/routes/endpoints/runWebhookEndpoint.js +83 -0
- package/dist/routes/endpoints/scheduleBackground.js +48 -0
- package/dist/routes/mcp/createMcpServer.js +160 -0
- package/dist/routes/notifications/derivePreview.js +30 -0
- package/dist/routes/notifications/getNotificationConfig.js +32 -0
- package/dist/routes/notifications/resolveNotificationLinks.js +72 -0
- package/dist/routes/notifications/resolveThemeLogo.js +35 -0
- package/dist/routes/page/dynamic/resolveDynamicContent.js +172 -0
- package/dist/routes/page/dynamic/unescapeOperators.js +36 -0
- package/dist/routes/page/dynamic/validateFragment.js +104 -0
- package/dist/routes/page/getPageConfig.js +16 -6
- package/dist/routes/websocket/authorizeWebsocket.js +27 -0
- package/dist/routes/websocket/createChannelRegistry.js +269 -0
- package/dist/routes/websocket/createWebSocketConnection.js +131 -0
- package/dist/routes/websocket/getWebsocketConfig.js +30 -0
- package/dist/routes/websocket/getWebsocketResolver.js +33 -0
- package/dist/routes/websocket/prepareChannel.js +72 -0
- package/dist/test/testContext.js +4 -2
- package/package.json +12 -10
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import { ServerParser } from '@lowdefy/operators';
|
|
16
|
+
import { _app, _secret } from '@lowdefy/operators-js/operators/server';
|
|
17
|
+
import { type } from '@lowdefy/helpers';
|
|
18
|
+
import { ConfigError } from '@lowdefy/errors';
|
|
19
|
+
// Builds the API auth strategy verifiers from the auth.json build artifact.
|
|
20
|
+
// Build has validated the config and written all defaults, so this resolves
|
|
21
|
+
// the _secret operators and constructs each strategy type's verifier.
|
|
22
|
+
// Verifier factories warn on short static keys at construction, so key
|
|
23
|
+
// strength surfaces at startup, never as a build failure.
|
|
24
|
+
function createAuthStrategies({ appMeta, authJson, logger, plugins, secrets }) {
|
|
25
|
+
if (!type.isArray(authJson.strategies) || authJson.strategies.length === 0) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
const operatorsParser = new ServerParser({
|
|
29
|
+
lowdefyApp: appMeta,
|
|
30
|
+
operators: {
|
|
31
|
+
_app,
|
|
32
|
+
_secret
|
|
33
|
+
},
|
|
34
|
+
secrets,
|
|
35
|
+
user: {}
|
|
36
|
+
});
|
|
37
|
+
const { output: strategiesConfig, errors: operatorErrors } = operatorsParser.parse({
|
|
38
|
+
input: authJson.strategies,
|
|
39
|
+
location: 'auth.strategies',
|
|
40
|
+
payload: {}
|
|
41
|
+
});
|
|
42
|
+
if (operatorErrors.length > 0) {
|
|
43
|
+
// Startup fails on the first error; log the rest so they can all be
|
|
44
|
+
// fixed in one pass instead of one boot per error.
|
|
45
|
+
operatorErrors.slice(1).forEach((error)=>logger.error(error));
|
|
46
|
+
throw operatorErrors[0];
|
|
47
|
+
}
|
|
48
|
+
return strategiesConfig.map((strategy)=>{
|
|
49
|
+
const strategyPlugin = plugins.strategies[strategy.type];
|
|
50
|
+
if (type.isNone(strategyPlugin)) {
|
|
51
|
+
throw new ConfigError(`Auth strategy type "${strategy.type}" not found at strategy "${strategy.id}".`, {
|
|
52
|
+
configKey: strategy['~k']
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
let verify;
|
|
56
|
+
try {
|
|
57
|
+
verify = strategyPlugin({
|
|
58
|
+
logger,
|
|
59
|
+
properties: strategy.properties,
|
|
60
|
+
strategyId: strategy.id
|
|
61
|
+
});
|
|
62
|
+
} catch (error) {
|
|
63
|
+
throw new ConfigError(error.message, {
|
|
64
|
+
cause: error,
|
|
65
|
+
configKey: strategy['~k']
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
attributes: strategy.attributes,
|
|
70
|
+
id: strategy.id,
|
|
71
|
+
roles: strategy.roles,
|
|
72
|
+
type: strategy.type,
|
|
73
|
+
verify
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
export default createAuthStrategies;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import createAuthStrategies from './createAuthStrategies.js';
|
|
16
|
+
let strategies;
|
|
17
|
+
// The strategy verifiers are constructed once per process at first use -
|
|
18
|
+
// the server middleware tries them in config order on every request that
|
|
19
|
+
// resolves no session.
|
|
20
|
+
function getAuthStrategies({ appMeta, authJson, logger, plugins, secrets }) {
|
|
21
|
+
if (strategies) return strategies;
|
|
22
|
+
strategies = createAuthStrategies({
|
|
23
|
+
appMeta,
|
|
24
|
+
authJson,
|
|
25
|
+
logger,
|
|
26
|
+
plugins,
|
|
27
|
+
secrets
|
|
28
|
+
});
|
|
29
|
+
return strategies;
|
|
30
|
+
}
|
|
31
|
+
export default getAuthStrategies;
|
|
@@ -12,14 +12,20 @@
|
|
|
12
12
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
|
-
*/ import { ConfigError } from '@lowdefy/errors';
|
|
16
|
-
|
|
15
|
+
*/ import { AuthenticationError, ConfigError } from '@lowdefy/errors';
|
|
16
|
+
import { type } from '@lowdefy/helpers';
|
|
17
|
+
function authorizeApiEndpoint({ authorize, logger, user }, { endpointConfig }) {
|
|
17
18
|
if (!authorize(endpointConfig)) {
|
|
18
19
|
logger.debug({
|
|
19
20
|
event: 'debug_api_authorize',
|
|
20
21
|
authorized: false,
|
|
21
22
|
auth_config: endpointConfig.auth
|
|
22
23
|
});
|
|
24
|
+
// Unauthenticated on a protected endpoint - 401 tells the caller to fix
|
|
25
|
+
// its credentials. Wrong roles stay opaque below.
|
|
26
|
+
if (type.isNone(user)) {
|
|
27
|
+
throw new AuthenticationError(`Authentication required for API endpoint "${endpointConfig.endpointId}".`);
|
|
28
|
+
}
|
|
23
29
|
throw new ConfigError(`API Endpoint "${endpointConfig.endpointId}" does not exist.`);
|
|
24
30
|
}
|
|
25
31
|
logger.debug({
|
|
@@ -13,12 +13,14 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import { serializer } from '@lowdefy/helpers';
|
|
16
|
-
import { ConfigError } from '@lowdefy/errors';
|
|
16
|
+
import { AuthenticationError, ConfigError } from '@lowdefy/errors';
|
|
17
17
|
import authorizeApiEndpoint from './authorizeApiEndpoint.js';
|
|
18
18
|
import buildEndpointResult from '../../response/buildEndpointResult.js';
|
|
19
19
|
import createEvaluateOperators from '../../context/createEvaluateOperators.js';
|
|
20
20
|
import getEndpointConfig from './getEndpointConfig.js';
|
|
21
|
+
import isUnauthenticatedHuman from './isUnauthenticatedHuman.js';
|
|
21
22
|
import runRoutine from './runRoutine.js';
|
|
23
|
+
import scheduleBackground from './scheduleBackground.js';
|
|
22
24
|
async function callEndpoint(context, { blockId, endpointId, pageId, payload }) {
|
|
23
25
|
const { logger } = context;
|
|
24
26
|
context.blockId = blockId;
|
|
@@ -35,9 +37,11 @@ async function callEndpoint(context, { blockId, endpointId, pageId, payload }) {
|
|
|
35
37
|
const endpointConfig = await getEndpointConfig(context, {
|
|
36
38
|
endpointId
|
|
37
39
|
});
|
|
38
|
-
// Block HTTP access to InternalApi endpoints
|
|
40
|
+
// Block HTTP access to InternalApi endpoints - same error as a missing
|
|
41
|
+
// endpoint, including the unauthenticated fork, so an internal endpoint is
|
|
42
|
+
// indistinguishable from one that does not exist on both paths.
|
|
39
43
|
if (endpointConfig.type === 'InternalApi') {
|
|
40
|
-
const err = new ConfigError(`API Endpoint "${endpointId}" does not exist.`);
|
|
44
|
+
const err = await isUnauthenticatedHuman(context) ? new AuthenticationError(`Authentication required for API endpoint "${endpointId}".`) : new ConfigError(`API Endpoint "${endpointId}" does not exist.`);
|
|
41
45
|
logger.debug({
|
|
42
46
|
params: {
|
|
43
47
|
endpointId
|
|
@@ -57,6 +61,25 @@ async function callEndpoint(context, { blockId, endpointId, pageId, payload }) {
|
|
|
57
61
|
state: {},
|
|
58
62
|
endpointDepth: 0
|
|
59
63
|
};
|
|
64
|
+
// async: true — acknowledge now, run the routine in the background.
|
|
65
|
+
// Auth was already checked above; the outcome lands in logs (scheduleBackground)
|
|
66
|
+
// and in whatever the routine itself records.
|
|
67
|
+
if (endpointConfig.async === true) {
|
|
68
|
+
scheduleBackground(context, {
|
|
69
|
+
event: 'background_endpoint',
|
|
70
|
+
endpointId
|
|
71
|
+
}, ()=>runRoutine(context, routineContext, {
|
|
72
|
+
routine: endpointConfig.routine
|
|
73
|
+
}));
|
|
74
|
+
return {
|
|
75
|
+
error: null,
|
|
76
|
+
response: serializer.serialize({
|
|
77
|
+
accepted: true
|
|
78
|
+
}),
|
|
79
|
+
status: 'accepted',
|
|
80
|
+
success: true
|
|
81
|
+
};
|
|
82
|
+
}
|
|
60
83
|
const { error, response, status } = await runRoutine(context, routineContext, {
|
|
61
84
|
routine: endpointConfig.routine
|
|
62
85
|
});
|
|
@@ -37,9 +37,11 @@ async function controlReject(context, routineContext, { control }) {
|
|
|
37
37
|
cause,
|
|
38
38
|
isReject: true
|
|
39
39
|
});
|
|
40
|
+
// Log under `err` — see controlThrow: only the `err` key runs the pino error
|
|
41
|
+
// serializer, so `error` would drop the message from the log line.
|
|
40
42
|
context.logger.warn({
|
|
41
43
|
event: 'warn_control_reject',
|
|
42
|
-
error
|
|
44
|
+
err: error
|
|
43
45
|
});
|
|
44
46
|
return {
|
|
45
47
|
status: 'reject',
|
|
@@ -36,9 +36,12 @@ async function controlThrow(context, routineContext, { control }) {
|
|
|
36
36
|
const error = new UserError(message, {
|
|
37
37
|
cause
|
|
38
38
|
});
|
|
39
|
+
// Log under `err` — the pino error serializer (createNodeLogger) is registered
|
|
40
|
+
// for the `err` key only; an Error passed as `error` is JSON-dumped without its
|
|
41
|
+
// non-enumerable `message`/`stack`, producing a log line with no message.
|
|
39
42
|
context.logger.error({
|
|
40
43
|
event: 'error_control_throw',
|
|
41
|
-
error
|
|
44
|
+
err: error
|
|
42
45
|
});
|
|
43
46
|
return {
|
|
44
47
|
status: 'error',
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import { ConfigError } from '@lowdefy/errors';
|
|
16
|
+
// Vercel sends the firing expression in x-vercel-cron-schedule; cron expressions are unique per
|
|
17
|
+
// endpoint and environment (enforced at build) so this is unambiguous. Without a cron (e.g. local
|
|
18
|
+
// testing) fall back to the single schedule, otherwise require it to disambiguate the payload.
|
|
19
|
+
function findSchedule({ schedules, cron, endpointId, environment }) {
|
|
20
|
+
const where = environment === undefined ? '' : ` for environment "${environment}"`;
|
|
21
|
+
if (!Array.isArray(schedules) || schedules.length === 0) {
|
|
22
|
+
throw new ConfigError(`API Endpoint "${endpointId}" is not scheduled${where}.`);
|
|
23
|
+
}
|
|
24
|
+
let schedule;
|
|
25
|
+
if (cron) {
|
|
26
|
+
schedule = schedules.find((s)=>s.cron === cron);
|
|
27
|
+
} else if (schedules.length === 1) {
|
|
28
|
+
schedule = schedules[0];
|
|
29
|
+
}
|
|
30
|
+
if (!schedule) {
|
|
31
|
+
throw new ConfigError(`No schedule matching cron "${cron}" for API Endpoint "${endpointId}"${where}.`);
|
|
32
|
+
}
|
|
33
|
+
return schedule;
|
|
34
|
+
}
|
|
35
|
+
export default findSchedule;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import { serializer, type } from '@lowdefy/helpers';
|
|
16
|
+
import { ConfigError } from '@lowdefy/errors';
|
|
17
|
+
import findSchedule from './findSchedule.js';
|
|
18
|
+
import getEndpointConfig from './getEndpointConfig.js';
|
|
19
|
+
import getEnvironmentSchedules from './getEnvironmentSchedules.js';
|
|
20
|
+
import scheduleBackground from './scheduleBackground.js';
|
|
21
|
+
// Vercel fires cron jobs only on the production deployment, so the schedules of every other
|
|
22
|
+
// environment are registered there as /api/cron-forward/<environment>/<endpointId> jobs. When one
|
|
23
|
+
// fires, this pings the environment's own /api/cron/<endpointId> with that environment's
|
|
24
|
+
// CRON_SECRET (a Lowdefy secret named in config.cron.environments) so the environment runs its own
|
|
25
|
+
// code, and answers Vercel immediately: the ping is fire-and-forget, kept alive by scheduleBackground
|
|
26
|
+
// and bounded by the function duration, and its outcome exists only in the logs.
|
|
27
|
+
async function forwardScheduledEndpoint(context, { environment, endpointId, cron }) {
|
|
28
|
+
const { config, logger, secrets } = context;
|
|
29
|
+
const target = config?.cron?.environments?.[environment];
|
|
30
|
+
if (!type.isObject(target)) {
|
|
31
|
+
throw new ConfigError(`Cron environment "${environment}" is not declared in lowdefy.config.cron.environments.`);
|
|
32
|
+
}
|
|
33
|
+
if (type.isUndefined(target.url)) {
|
|
34
|
+
throw new ConfigError(`Cron environment "${environment}" has no url to forward to: it is the environment that runs the crons.`);
|
|
35
|
+
}
|
|
36
|
+
if (target.enabled === false) {
|
|
37
|
+
throw new ConfigError(`Cron environment "${environment}" is disabled.`);
|
|
38
|
+
}
|
|
39
|
+
const secret = secrets?.[target.secret];
|
|
40
|
+
if (!type.isString(secret) || secret === '') {
|
|
41
|
+
throw new ConfigError(`Secret "${target.secret}" holding the CRON_SECRET of cron environment "${environment}" is not set. Set the LOWDEFY_SECRET_${target.secret} environment variable on this deployment.`);
|
|
42
|
+
}
|
|
43
|
+
// The same build is deployed to every environment, so the local artifact says whether the target
|
|
44
|
+
// declares the firing schedule: fail here with a config error instead of having the target 500.
|
|
45
|
+
const endpointConfig = await getEndpointConfig(context, {
|
|
46
|
+
endpointId
|
|
47
|
+
});
|
|
48
|
+
const schedules = getEnvironmentSchedules({
|
|
49
|
+
endpointConfig,
|
|
50
|
+
environment
|
|
51
|
+
});
|
|
52
|
+
findSchedule({
|
|
53
|
+
schedules,
|
|
54
|
+
cron,
|
|
55
|
+
endpointId,
|
|
56
|
+
environment
|
|
57
|
+
});
|
|
58
|
+
// Trim trailing slashes with a loop rather than a regex: CodeQL flags `/\/+$/` on config input.
|
|
59
|
+
let origin = target.url;
|
|
60
|
+
while(origin.endsWith('/')){
|
|
61
|
+
origin = origin.slice(0, -1);
|
|
62
|
+
}
|
|
63
|
+
const url = `${origin}/api/cron/${endpointId}`;
|
|
64
|
+
const timeoutMs = (config?.vercel?.maxDuration ?? 60) * 1000;
|
|
65
|
+
const headers = {
|
|
66
|
+
authorization: `Bearer ${secret}`
|
|
67
|
+
};
|
|
68
|
+
if (cron) headers['x-vercel-cron-schedule'] = cron;
|
|
69
|
+
headers['x-lowdefy-cron-environment'] = environment;
|
|
70
|
+
logger.info({
|
|
71
|
+
event: 'forward_scheduled_endpoint',
|
|
72
|
+
endpointId,
|
|
73
|
+
environment,
|
|
74
|
+
cron,
|
|
75
|
+
url
|
|
76
|
+
});
|
|
77
|
+
scheduleBackground(context, {
|
|
78
|
+
event: 'forward_scheduled_endpoint',
|
|
79
|
+
endpointId
|
|
80
|
+
}, async ()=>{
|
|
81
|
+
const response = await fetch(url, {
|
|
82
|
+
method: 'GET',
|
|
83
|
+
headers,
|
|
84
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
85
|
+
});
|
|
86
|
+
// Drain the body so the connection is released; the target logs its own outcome.
|
|
87
|
+
await response.arrayBuffer();
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
throw new Error(`Forwarded cron for environment "${environment}" at ${url} responded ${response.status}.`);
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
status: response.status
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
return {
|
|
96
|
+
error: null,
|
|
97
|
+
response: serializer.serialize({
|
|
98
|
+
accepted: true,
|
|
99
|
+
environment,
|
|
100
|
+
endpointId
|
|
101
|
+
}),
|
|
102
|
+
status: 'accepted',
|
|
103
|
+
success: true
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
export default forwardScheduledEndpoint;
|
|
@@ -12,11 +12,17 @@
|
|
|
12
12
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
|
-
*/ import { ConfigError } from '@lowdefy/errors';
|
|
16
|
-
|
|
15
|
+
*/ import { AuthenticationError, ConfigError } from '@lowdefy/errors';
|
|
16
|
+
import isUnauthenticatedHuman from './isUnauthenticatedHuman.js';
|
|
17
|
+
// A missing endpoint answers the same as a protected one for an anonymous
|
|
18
|
+
// caller on an auth'd app - the message is byte-identical to the one
|
|
19
|
+
// authorizeApiEndpoint throws, so present and absent are indistinguishable
|
|
20
|
+
// before authenticating. Every other caller keeps the opaque does-not-exist.
|
|
21
|
+
async function getEndpointConfig(context, { endpointId }) {
|
|
22
|
+
const { logger, readConfigFile } = context;
|
|
17
23
|
const endpoint = await readConfigFile(`api/${endpointId}.json`);
|
|
18
24
|
if (!endpoint) {
|
|
19
|
-
const err = new ConfigError(`API Endpoint "${endpointId}" does not exist.`);
|
|
25
|
+
const err = await isUnauthenticatedHuman(context) ? new AuthenticationError(`Authentication required for API endpoint "${endpointId}".`) : new ConfigError(`API Endpoint "${endpointId}" does not exist.`);
|
|
20
26
|
logger.debug({
|
|
21
27
|
params: {
|
|
22
28
|
endpointId
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import { type } from '@lowdefy/helpers';
|
|
16
|
+
// With config.cron.environments declared the build resolves `schedules` onto every environment
|
|
17
|
+
// (schedules.<name>, defaults inherited), so an environment run reads its own list; without
|
|
18
|
+
// environments the endpoint carries a plain array.
|
|
19
|
+
function getEnvironmentSchedules({ endpointConfig, environment }) {
|
|
20
|
+
const schedules = endpointConfig.schedules;
|
|
21
|
+
if (type.isArray(schedules)) {
|
|
22
|
+
return schedules;
|
|
23
|
+
}
|
|
24
|
+
if (environment === undefined) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
return schedules?.[environment] ?? [];
|
|
28
|
+
}
|
|
29
|
+
export default getEnvironmentSchedules;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import { ConfigError } from '@lowdefy/errors';
|
|
16
|
+
import { type } from '@lowdefy/helpers';
|
|
17
|
+
import addStepResult from './addStepResult.js';
|
|
18
|
+
import prepareAgent from '../agent/prepareAgent.js';
|
|
19
|
+
async function handleAgentCall(context, routineContext, { step }) {
|
|
20
|
+
const { logger, evaluateOperators } = context;
|
|
21
|
+
logger.debug({
|
|
22
|
+
event: 'debug_start_agent_call',
|
|
23
|
+
step
|
|
24
|
+
});
|
|
25
|
+
// Evaluate operators in step.properties (resolves agentId, prompt)
|
|
26
|
+
const evaluatedProperties = evaluateOperators({
|
|
27
|
+
input: step.properties,
|
|
28
|
+
items: routineContext.items,
|
|
29
|
+
location: step.stepId,
|
|
30
|
+
payload: routineContext.payload,
|
|
31
|
+
state: routineContext.state,
|
|
32
|
+
steps: routineContext.steps
|
|
33
|
+
});
|
|
34
|
+
const { agentId, prompt } = evaluatedProperties;
|
|
35
|
+
if (!type.isString(agentId)) {
|
|
36
|
+
throw new ConfigError(`CallAgent step "${step.stepId}" properties.agentId must evaluate to a string. Received ${JSON.stringify(agentId)}.`, {
|
|
37
|
+
configKey: step['~k']
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
if (!type.isString(prompt)) {
|
|
41
|
+
throw new ConfigError(`CallAgent step "${step.stepId}" properties.prompt must evaluate to a string. Received ${JSON.stringify(prompt)}.`, {
|
|
42
|
+
configKey: step['~k']
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
// Headless agent context — no page, no conversation, no sharedState (which
|
|
46
|
+
// also excludes the client-only update-page-state tool). userId is null
|
|
47
|
+
// under scheduled (system) context.
|
|
48
|
+
const agentContext = {
|
|
49
|
+
conversationId: null,
|
|
50
|
+
pageId: null,
|
|
51
|
+
sharedState: undefined,
|
|
52
|
+
urlQuery: {},
|
|
53
|
+
userId: context.user?.sub ?? context.user?.id ?? null
|
|
54
|
+
};
|
|
55
|
+
const { agentConfig, connectionInstance, agentType, resolverContext } = await prepareAgent(context, {
|
|
56
|
+
agentId,
|
|
57
|
+
agentContext,
|
|
58
|
+
endpointDepth: routineContext.endpointDepth,
|
|
59
|
+
mode: 'generate'
|
|
60
|
+
});
|
|
61
|
+
const { result } = await agentType.resolver({
|
|
62
|
+
connection: connectionInstance,
|
|
63
|
+
properties: {
|
|
64
|
+
agent: agentConfig,
|
|
65
|
+
prompt
|
|
66
|
+
},
|
|
67
|
+
context: resolverContext
|
|
68
|
+
});
|
|
69
|
+
addStepResult(context, routineContext, {
|
|
70
|
+
result,
|
|
71
|
+
stepId: step.stepId
|
|
72
|
+
});
|
|
73
|
+
logger.debug({
|
|
74
|
+
event: 'debug_end_agent_call',
|
|
75
|
+
stepId: step.stepId,
|
|
76
|
+
targetAgentId: agentId,
|
|
77
|
+
finishReason: result?.finishReason
|
|
78
|
+
});
|
|
79
|
+
return {
|
|
80
|
+
status: 'continue'
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export default handleAgentCall;
|
|
@@ -12,8 +12,11 @@
|
|
|
12
12
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
|
-
*/ import
|
|
15
|
+
*/ import { serializer } from '@lowdefy/helpers';
|
|
16
|
+
import { ConfigError } from '@lowdefy/errors';
|
|
17
|
+
import addStepResult from './addStepResult.js';
|
|
16
18
|
import invokeEndpoint from './invokeEndpoint.js';
|
|
19
|
+
import scheduleBackground from './scheduleBackground.js';
|
|
17
20
|
async function handleEndpointCall(context, routineContext, { step }) {
|
|
18
21
|
const { logger, evaluateOperators } = context;
|
|
19
22
|
logger.debug({
|
|
@@ -29,6 +32,51 @@ async function handleEndpointCall(context, routineContext, { step }) {
|
|
|
29
32
|
state: routineContext.state,
|
|
30
33
|
steps: routineContext.steps
|
|
31
34
|
});
|
|
35
|
+
// detached: true — fire-and-forget the call back through the deployment's
|
|
36
|
+
// /api/detached route, so the target runs in its OWN function invocation
|
|
37
|
+
// with a fresh duration budget (chainable bounded work without a queue).
|
|
38
|
+
// At-most-once, no retry: targets must be idempotent. The dispatch promise
|
|
39
|
+
// rides the platform request context so the outgoing request always leaves
|
|
40
|
+
// before the invocation is reaped; the target's own outcome exists only in
|
|
41
|
+
// its logs and whatever its routine writes.
|
|
42
|
+
if (evaluatedProperties.detached === true) {
|
|
43
|
+
if (!process.env.CRON_SECRET) {
|
|
44
|
+
throw new ConfigError('CallApi with "detached: true" requires the CRON_SECRET environment variable — the /api/detached route fails closed without it.');
|
|
45
|
+
}
|
|
46
|
+
if (!context.origin) {
|
|
47
|
+
throw new ConfigError('Detached endpoint calls require the request origin on context.');
|
|
48
|
+
}
|
|
49
|
+
const targetEndpointId = evaluatedProperties.endpointId;
|
|
50
|
+
scheduleBackground(context, {
|
|
51
|
+
event: 'detached_dispatch',
|
|
52
|
+
endpointId: targetEndpointId
|
|
53
|
+
}, ()=>fetch(`${context.origin}/api/detached/${targetEndpointId}`, {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: {
|
|
56
|
+
'content-type': 'application/json',
|
|
57
|
+
authorization: `Bearer ${process.env.CRON_SECRET}`
|
|
58
|
+
},
|
|
59
|
+
body: JSON.stringify({
|
|
60
|
+
payload: serializer.serialize(evaluatedProperties.payload ?? {})
|
|
61
|
+
})
|
|
62
|
+
}));
|
|
63
|
+
addStepResult(context, routineContext, {
|
|
64
|
+
result: {
|
|
65
|
+
detached: true,
|
|
66
|
+
endpointId: targetEndpointId
|
|
67
|
+
},
|
|
68
|
+
stepId: step.stepId
|
|
69
|
+
});
|
|
70
|
+
logger.debug({
|
|
71
|
+
event: 'debug_end_endpoint_call',
|
|
72
|
+
stepId: step.stepId,
|
|
73
|
+
targetEndpointId,
|
|
74
|
+
detached: true
|
|
75
|
+
});
|
|
76
|
+
return {
|
|
77
|
+
status: 'continue'
|
|
78
|
+
};
|
|
79
|
+
}
|
|
32
80
|
const result = await invokeEndpoint(context, {
|
|
33
81
|
endpointId: evaluatedProperties.endpointId,
|
|
34
82
|
payload: evaluatedProperties.payload,
|