@lowdefy/api 6.0.0 → 6.1.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/createEvaluateOperators.js +3 -1
- package/dist/context/getCurrentEnvironment.js +27 -0
- package/dist/index.js +2 -1
- package/dist/response/buildEndpointResult.js +3 -1
- package/dist/response/createWireProjection.js +88 -0
- package/dist/response/normalizeErrorSources.js +10 -13
- package/dist/response/redactErrorResponse.js +42 -8
- package/dist/response/redactResponse.js +10 -10
- package/dist/routes/agent/prepareAgent.js +6 -0
- package/dist/routes/auth/defaultAuthUrl.js +31 -0
- package/dist/routes/auth/getAuthConfig.js +5 -1
- package/dist/routes/endpoints/callEndpoint.js +1 -0
- package/dist/routes/endpoints/control/controlFor.js +4 -7
- package/dist/routes/endpoints/control/controlIf.js +4 -8
- package/dist/routes/endpoints/control/controlLog.js +6 -14
- package/dist/routes/endpoints/control/controlParallelFor.js +4 -7
- package/dist/routes/endpoints/control/controlReject.js +13 -15
- package/dist/routes/endpoints/control/controlReturn.js +4 -9
- package/dist/routes/endpoints/control/controlSetState.js +4 -8
- package/dist/routes/endpoints/control/controlSwitch.js +4 -8
- package/dist/routes/endpoints/control/controlThrow.js +17 -19
- package/dist/routes/endpoints/control/controlTry.js +20 -10
- package/dist/routes/endpoints/evaluateRoutineOperators.js +26 -0
- package/dist/routes/endpoints/forwardScheduledEndpoint.js +19 -14
- package/dist/routes/endpoints/getEnvironmentSchedules.js +5 -7
- package/dist/routes/endpoints/handleAgentCall.js +4 -7
- package/dist/routes/endpoints/handleEndpointCall.js +4 -7
- package/dist/routes/endpoints/handleRenderNotification.js +15 -8
- package/dist/routes/endpoints/handleRequest.js +1 -5
- package/dist/routes/endpoints/handleValidateSchema.js +12 -10
- package/dist/routes/endpoints/invokeEndpoint.js +1 -0
- package/dist/routes/endpoints/operatorScope.js +27 -0
- package/dist/routes/endpoints/resolveCronEnvironment.js +7 -6
- package/dist/routes/endpoints/runDetachedEndpoint.js +1 -0
- package/dist/routes/endpoints/runScheduledEndpoint.js +1 -0
- package/dist/routes/endpoints/runWebhookEndpoint.js +1 -0
- package/dist/routes/endpoints/test/runTest.js +17 -2
- package/dist/routes/mcp/createMcpServer.js +20 -2
- package/dist/routes/request/callRequest.js +9 -3
- package/dist/routes/request/callRequestResolver.js +6 -0
- package/dist/routes/request/evaluateOperators.js +8 -12
- package/dist/routes/websocket/createChannelRegistry.js +19 -5
- package/dist/routes/websocket/createWebSocketConnection.js +15 -3
- package/dist/test/testContext.js +4 -2
- package/package.json +9 -9
- package/dist/response/omitErrorProps.js +0 -51
|
@@ -12,7 +12,8 @@
|
|
|
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 { projectCaughtError } from '@lowdefy/helpers';
|
|
16
|
+
import runRoutine from '../runRoutine.js';
|
|
16
17
|
async function controlTry(context, routineContext, { control }) {
|
|
17
18
|
context.logger.debug({
|
|
18
19
|
event: 'debug_control_try'
|
|
@@ -20,15 +21,24 @@ async function controlTry(context, routineContext, { control }) {
|
|
|
20
21
|
let res = await runRoutine(context, routineContext, {
|
|
21
22
|
routine: control[':try']
|
|
22
23
|
});
|
|
23
|
-
if (res.status === 'error') {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
if (res.status === 'error' && control[':catch']) {
|
|
25
|
+
context.logger.debug({
|
|
26
|
+
event: 'debug_control_catch'
|
|
27
|
+
});
|
|
28
|
+
// A child context rather than a field set on routineContext: the same object is handed to
|
|
29
|
+
// :try, :finally and every :parallel branch, so writing to it would let an inner :try clobber
|
|
30
|
+
// the outer catch's error and parallel branches read each other's. steps and state are still
|
|
31
|
+
// shared because routines mutate them in place. The projection is built once here, never the
|
|
32
|
+
// raw error, so no operator in the catch can read its received value or source.
|
|
33
|
+
const error = projectCaughtError(res.error, {
|
|
34
|
+
scrub: context.scrubSecrets
|
|
35
|
+
});
|
|
36
|
+
res = await runRoutine(context, {
|
|
37
|
+
...routineContext,
|
|
38
|
+
error
|
|
39
|
+
}, {
|
|
40
|
+
routine: control[':catch']
|
|
41
|
+
});
|
|
32
42
|
}
|
|
33
43
|
if (control[':finally']) {
|
|
34
44
|
context.logger.debug({
|
|
@@ -0,0 +1,26 @@
|
|
|
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 operatorScope from './operatorScope.js';
|
|
16
|
+
// Every routine step evaluates against the same frame. Taking the frame from routineContext in one
|
|
17
|
+
// place means a step handler cannot drop part of it (state, items, loop indices).
|
|
18
|
+
function evaluateRoutineOperators(context, routineContext, { input, location }) {
|
|
19
|
+
return context.evaluateOperators({
|
|
20
|
+
arrayIndices: routineContext.arrayIndices,
|
|
21
|
+
input,
|
|
22
|
+
location,
|
|
23
|
+
...operatorScope(routineContext)
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
export default evaluateRoutineOperators;
|
|
@@ -18,29 +18,34 @@ import findSchedule from './findSchedule.js';
|
|
|
18
18
|
import getEndpointConfig from './getEndpointConfig.js';
|
|
19
19
|
import getEnvironmentSchedules from './getEnvironmentSchedules.js';
|
|
20
20
|
import scheduleBackground from './scheduleBackground.js';
|
|
21
|
-
// Vercel fires cron jobs only on the production deployment, so the schedules of every
|
|
22
|
-
//
|
|
23
|
-
// fires, this pings the environment's own /api/cron/<endpointId> with that
|
|
24
|
-
// CRON_SECRET (
|
|
25
|
-
// code, and answers Vercel immediately: the ping is fire-and-forget,
|
|
26
|
-
// and bounded by the function duration, and its outcome exists only
|
|
21
|
+
// Vercel fires cron jobs only on the production deployment, so the schedules of every environment
|
|
22
|
+
// with a cron.secret are registered there as /api/cron-forward/<environment>/<endpointId> jobs. When
|
|
23
|
+
// one fires, this pings the environment's own /api/cron/<endpointId> at its url with that
|
|
24
|
+
// environment's CRON_SECRET (the Lowdefy secret named by config.environments.<env>.cron.secret) so
|
|
25
|
+
// the environment runs its own code, and answers Vercel immediately: the ping is fire-and-forget,
|
|
26
|
+
// kept alive by scheduleBackground and bounded by the function duration, and its outcome exists only
|
|
27
|
+
// in the logs.
|
|
27
28
|
async function forwardScheduledEndpoint(context, { environment, endpointId, cron }) {
|
|
28
29
|
const { config, logger, secrets } = context;
|
|
29
|
-
const target = config?.
|
|
30
|
+
const target = config?.environments?.[environment];
|
|
30
31
|
if (!type.isObject(target)) {
|
|
31
|
-
throw new ConfigError(`Cron environment "${environment}" is not declared in
|
|
32
|
+
throw new ConfigError(`Cron environment "${environment}" is not declared in config.environments.`);
|
|
32
33
|
}
|
|
33
|
-
if (
|
|
34
|
-
throw new ConfigError(`Cron environment "${environment}"
|
|
34
|
+
if (environment === config.environment) {
|
|
35
|
+
throw new ConfigError(`Cron environment "${environment}" is this deployment's own environment: its crons run here, not forwarded.`);
|
|
35
36
|
}
|
|
36
|
-
if (target.enabled === false) {
|
|
37
|
+
if (target.cron?.enabled === false) {
|
|
37
38
|
throw new ConfigError(`Cron environment "${environment}" is disabled.`);
|
|
38
39
|
}
|
|
39
|
-
const
|
|
40
|
+
const secretName = target.cron?.secret;
|
|
41
|
+
if (type.isUndefined(secretName)) {
|
|
42
|
+
throw new ConfigError(`Cron environment "${environment}" has no cron.secret: crons are only forwarded to environments with one.`);
|
|
43
|
+
}
|
|
44
|
+
const secret = secrets?.[secretName];
|
|
40
45
|
if (!type.isString(secret) || secret === '') {
|
|
41
|
-
throw new ConfigError(`Secret "${
|
|
46
|
+
throw new ConfigError(`Secret "${secretName}" holding the CRON_SECRET of cron environment "${environment}" is not set. Set the LOWDEFY_SECRET_${secretName} environment variable on this deployment.`);
|
|
42
47
|
}
|
|
43
|
-
//
|
|
48
|
+
// Every environment builds from the same config, so the local artifact says whether the target
|
|
44
49
|
// declares the firing schedule: fail here with a config error instead of having the target 500.
|
|
45
50
|
const endpointConfig = await getEndpointConfig(context, {
|
|
46
51
|
endpointId
|
|
@@ -13,17 +13,15 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import { type } from '@lowdefy/helpers';
|
|
16
|
-
// With config.
|
|
17
|
-
// (schedules.<name>, defaults inherited), so an environment run reads its
|
|
18
|
-
//
|
|
16
|
+
// With config.environments declared the build resolves `schedules` onto every environment
|
|
17
|
+
// (schedules.<name>, defaults inherited, plus schedules.default), so an environment run reads its
|
|
18
|
+
// own list and a deployment with no current environment reads the default; without environments
|
|
19
|
+
// the endpoint carries a plain array.
|
|
19
20
|
function getEnvironmentSchedules({ endpointConfig, environment }) {
|
|
20
21
|
const schedules = endpointConfig.schedules;
|
|
21
22
|
if (type.isArray(schedules)) {
|
|
22
23
|
return schedules;
|
|
23
24
|
}
|
|
24
|
-
|
|
25
|
-
return [];
|
|
26
|
-
}
|
|
27
|
-
return schedules?.[environment] ?? [];
|
|
25
|
+
return schedules?.[environment ?? 'default'] ?? [];
|
|
28
26
|
}
|
|
29
27
|
export default getEnvironmentSchedules;
|
|
@@ -16,20 +16,17 @@
|
|
|
16
16
|
import { type } from '@lowdefy/helpers';
|
|
17
17
|
import addStepResult from './addStepResult.js';
|
|
18
18
|
import prepareAgent from '../agent/prepareAgent.js';
|
|
19
|
+
import evaluateRoutineOperators from './evaluateRoutineOperators.js';
|
|
19
20
|
async function handleAgentCall(context, routineContext, { step }) {
|
|
20
|
-
const { logger
|
|
21
|
+
const { logger } = context;
|
|
21
22
|
logger.debug({
|
|
22
23
|
event: 'debug_start_agent_call',
|
|
23
24
|
step
|
|
24
25
|
});
|
|
25
26
|
// Evaluate operators in step.properties (resolves agentId, prompt)
|
|
26
|
-
const evaluatedProperties =
|
|
27
|
+
const evaluatedProperties = evaluateRoutineOperators(context, routineContext, {
|
|
27
28
|
input: step.properties,
|
|
28
|
-
|
|
29
|
-
location: step.stepId,
|
|
30
|
-
payload: routineContext.payload,
|
|
31
|
-
state: routineContext.state,
|
|
32
|
-
steps: routineContext.steps
|
|
29
|
+
location: step.stepId
|
|
33
30
|
});
|
|
34
31
|
const { agentId, prompt } = evaluatedProperties;
|
|
35
32
|
if (!type.isString(agentId)) {
|
|
@@ -17,20 +17,17 @@ import { ConfigError } from '@lowdefy/errors';
|
|
|
17
17
|
import addStepResult from './addStepResult.js';
|
|
18
18
|
import invokeEndpoint from './invokeEndpoint.js';
|
|
19
19
|
import scheduleBackground from './scheduleBackground.js';
|
|
20
|
+
import evaluateRoutineOperators from './evaluateRoutineOperators.js';
|
|
20
21
|
async function handleEndpointCall(context, routineContext, { step }) {
|
|
21
|
-
const { logger
|
|
22
|
+
const { logger } = context;
|
|
22
23
|
logger.debug({
|
|
23
24
|
event: 'debug_start_endpoint_call',
|
|
24
25
|
step
|
|
25
26
|
});
|
|
26
27
|
// Evaluate operators in step.properties (resolves endpointId, payload)
|
|
27
|
-
const evaluatedProperties =
|
|
28
|
+
const evaluatedProperties = evaluateRoutineOperators(context, routineContext, {
|
|
28
29
|
input: step.properties,
|
|
29
|
-
|
|
30
|
-
location: step.stepId,
|
|
31
|
-
payload: routineContext.payload,
|
|
32
|
-
state: routineContext.state,
|
|
33
|
-
steps: routineContext.steps
|
|
30
|
+
location: step.stepId
|
|
34
31
|
});
|
|
35
32
|
// detached: true — fire-and-forget the call back through the deployment's
|
|
36
33
|
// /api/detached route, so the target runs in its OWN function invocation
|
|
@@ -16,10 +16,12 @@
|
|
|
16
16
|
import { ConfigError } from '@lowdefy/errors';
|
|
17
17
|
import { type } from '@lowdefy/helpers';
|
|
18
18
|
import addStepResult from './addStepResult.js';
|
|
19
|
+
import getCurrentEnvironment from '../../context/getCurrentEnvironment.js';
|
|
19
20
|
import derivePreview from '../notifications/derivePreview.js';
|
|
20
21
|
import getNotificationConfig from '../notifications/getNotificationConfig.js';
|
|
21
22
|
import resolveNotificationLinks from '../notifications/resolveNotificationLinks.js';
|
|
22
23
|
import resolveThemeLogo from '../notifications/resolveThemeLogo.js';
|
|
24
|
+
import evaluateRoutineOperators from './evaluateRoutineOperators.js';
|
|
23
25
|
function itemHasPageLinks(item, dataKeys) {
|
|
24
26
|
const links = Object.values(item.links ?? {});
|
|
25
27
|
const arrayLinks = dataKeys.flatMap((key)=>(type.isArray(item[key]) ? item[key] : []).map((entry)=>entry?.link));
|
|
@@ -29,18 +31,14 @@ function itemHasPageLinks(item, dataKeys) {
|
|
|
29
31
|
].some((link)=>type.isObject(link) && !type.isNone(link.pageId));
|
|
30
32
|
}
|
|
31
33
|
async function handleRenderNotification(context, routineContext, { step }) {
|
|
32
|
-
const { logger
|
|
34
|
+
const { logger } = context;
|
|
33
35
|
logger.debug({
|
|
34
36
|
event: 'debug_start_render_notification',
|
|
35
37
|
step
|
|
36
38
|
});
|
|
37
|
-
const evaluatedProperties =
|
|
39
|
+
const evaluatedProperties = evaluateRoutineOperators(context, routineContext, {
|
|
38
40
|
input: step.properties,
|
|
39
|
-
|
|
40
|
-
location: step.stepId,
|
|
41
|
-
payload: routineContext.payload,
|
|
42
|
-
state: routineContext.state,
|
|
43
|
-
steps: routineContext.steps
|
|
41
|
+
location: step.stepId
|
|
44
42
|
});
|
|
45
43
|
const { notificationId, data, landingPage, recordId } = evaluatedProperties;
|
|
46
44
|
let { serverUrl } = evaluatedProperties;
|
|
@@ -65,6 +63,15 @@ async function handleRenderNotification(context, routineContext, { step }) {
|
|
|
65
63
|
configKey: step['~k']
|
|
66
64
|
});
|
|
67
65
|
}
|
|
66
|
+
} else {
|
|
67
|
+
// The current environment's url is the deployment origin, so links need no per-step wiring.
|
|
68
|
+
// On the dev server the request origin is the local app; in production it is never used, since
|
|
69
|
+
// a spoofed Host header would then steer the links in outgoing mail.
|
|
70
|
+
serverUrl = getCurrentEnvironment({
|
|
71
|
+
config: context.config
|
|
72
|
+
})?.url ?? (context.dev === true ? context.origin : undefined);
|
|
73
|
+
}
|
|
74
|
+
if (!type.isNone(serverUrl)) {
|
|
68
75
|
serverUrl = serverUrl.replace(/\/$/, '');
|
|
69
76
|
}
|
|
70
77
|
if (!type.isNone(landingPage) && !type.isString(landingPage)) {
|
|
@@ -111,7 +118,7 @@ async function handleRenderNotification(context, routineContext, { step }) {
|
|
|
111
118
|
});
|
|
112
119
|
if (itemHasPageLinks(data, Template.dataKeys ?? [])) {
|
|
113
120
|
if (type.isNone(serverUrl)) {
|
|
114
|
-
throw new ConfigError(`Notification "${notificationId}" has links but no server URL is available. Set the serverUrl step property.`, {
|
|
121
|
+
throw new ConfigError(`Notification "${notificationId}" has links but no server URL is available. Set the serverUrl step property, or the url of the current environment in config.environments.`, {
|
|
115
122
|
configKey: step['~k']
|
|
116
123
|
});
|
|
117
124
|
}
|
|
@@ -23,7 +23,6 @@ import getRequestResolver from '../request/getRequestResolver.js';
|
|
|
23
23
|
import validateSchemas from '../request/validateSchemas.js';
|
|
24
24
|
async function handleRequest(context, routineContext, { request }) {
|
|
25
25
|
const { logger } = context;
|
|
26
|
-
const { items } = routineContext;
|
|
27
26
|
logger.debug({
|
|
28
27
|
event: 'debug_start_request',
|
|
29
28
|
request
|
|
@@ -42,11 +41,8 @@ async function handleRequest(context, routineContext, { request }) {
|
|
|
42
41
|
});
|
|
43
42
|
const { connectionProperties, requestProperties } = evaluateOperators(context, {
|
|
44
43
|
connectionConfig,
|
|
45
|
-
items,
|
|
46
|
-
payload: routineContext.payload,
|
|
47
44
|
requestConfig,
|
|
48
|
-
|
|
49
|
-
steps: routineContext.steps
|
|
45
|
+
routineContext
|
|
50
46
|
});
|
|
51
47
|
checkConnectionRead(context, {
|
|
52
48
|
connectionConfig,
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import { validate } from '@lowdefy/ajv';
|
|
16
|
+
import { UserError } from '@lowdefy/errors';
|
|
16
17
|
import addStepResult from './addStepResult.js';
|
|
18
|
+
import evaluateRoutineOperators from './evaluateRoutineOperators.js';
|
|
17
19
|
function buildErrorMessage(errors, stepId) {
|
|
18
20
|
const first = errors?.[0];
|
|
19
21
|
if (!first) return `ValidateSchema step "${stepId}" failed.`;
|
|
@@ -21,17 +23,14 @@ function buildErrorMessage(errors, stepId) {
|
|
|
21
23
|
return `ValidateSchema step "${stepId}" failed at ${path}: ${first.message}.`;
|
|
22
24
|
}
|
|
23
25
|
async function handleValidateSchema(context, routineContext, { step }) {
|
|
24
|
-
const { logger
|
|
26
|
+
const { logger } = context;
|
|
25
27
|
logger.debug({
|
|
26
28
|
event: 'debug_start_validate_schema',
|
|
27
29
|
step
|
|
28
30
|
});
|
|
29
|
-
const evaluatedProperties =
|
|
31
|
+
const evaluatedProperties = evaluateRoutineOperators(context, routineContext, {
|
|
30
32
|
input: step.properties,
|
|
31
|
-
|
|
32
|
-
location: step.stepId,
|
|
33
|
-
payload: routineContext.payload,
|
|
34
|
-
steps: routineContext.steps
|
|
33
|
+
location: step.stepId
|
|
35
34
|
});
|
|
36
35
|
const { schema, data, throwOnInvalid = true } = evaluatedProperties;
|
|
37
36
|
const { valid, errors } = validate({
|
|
@@ -48,15 +47,18 @@ async function handleValidateSchema(context, routineContext, { step }) {
|
|
|
48
47
|
stepId: step.stepId
|
|
49
48
|
});
|
|
50
49
|
if (!valid && throwOnInvalid) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
// A UserError, so the caller sees which field failed: the message is built from the schema
|
|
51
|
+
// path and rule, never the data, and every other server error reaches the client generic.
|
|
52
|
+
// No cause: a UserError's cause crosses the wire as author data, and the ajv errors carry
|
|
53
|
+
// server schema detail. They stay in the step result and the log.
|
|
54
|
+
const error = new UserError(buildErrorMessage(result.errors, step.stepId));
|
|
54
55
|
// Log under `err` — see controlThrow: only the `err` key runs the pino error
|
|
55
56
|
// serializer, so `error` would drop the message from the log line.
|
|
56
57
|
logger.error({
|
|
57
58
|
event: 'error_validate_schema',
|
|
58
59
|
stepId: step.stepId,
|
|
59
|
-
err: error
|
|
60
|
+
err: error,
|
|
61
|
+
errors: result.errors
|
|
60
62
|
});
|
|
61
63
|
return {
|
|
62
64
|
status: 'error',
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
*/ // The routine fields operators can read. A field added to the routine frame goes here, so every
|
|
16
|
+
// routine step and control sees it rather than only the ones that remembered to pass it.
|
|
17
|
+
function operatorScope(routineContext) {
|
|
18
|
+
const { error, items, payload, state, steps } = routineContext;
|
|
19
|
+
return {
|
|
20
|
+
error: error ?? null,
|
|
21
|
+
items,
|
|
22
|
+
payload,
|
|
23
|
+
state,
|
|
24
|
+
steps
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export default operatorScope;
|
|
@@ -16,22 +16,23 @@
|
|
|
16
16
|
import { ConfigError } from '@lowdefy/errors';
|
|
17
17
|
// The environment a cron run resolves its schedules for. A forwarded request names it in the
|
|
18
18
|
// x-lowdefy-cron-environment header; a request without one is the deployment running its own crons,
|
|
19
|
-
// which is the environment
|
|
20
|
-
//
|
|
19
|
+
// which is the current environment (config.environment). Without config.environments there are no
|
|
20
|
+
// environments and the endpoint's plain schedules apply; without a current environment the
|
|
21
|
+
// `default` schedules apply.
|
|
21
22
|
function resolveCronEnvironment({ config, environment }) {
|
|
22
|
-
const environments = config?.
|
|
23
|
+
const environments = config?.environments;
|
|
23
24
|
if (type.isNone(environments)) {
|
|
24
25
|
if (!type.isNone(environment)) {
|
|
25
|
-
throw new ConfigError(`Cron environment "${environment}" is not configured:
|
|
26
|
+
throw new ConfigError(`Cron environment "${environment}" is not configured: config.environments is not defined.`);
|
|
26
27
|
}
|
|
27
28
|
return undefined;
|
|
28
29
|
}
|
|
29
30
|
if (!type.isNone(environment)) {
|
|
30
31
|
if (!type.isObject(environments[environment])) {
|
|
31
|
-
throw new ConfigError(`Cron environment "${environment}" is not declared in
|
|
32
|
+
throw new ConfigError(`Cron environment "${environment}" is not declared in config.environments.`);
|
|
32
33
|
}
|
|
33
34
|
return environment;
|
|
34
35
|
}
|
|
35
|
-
return
|
|
36
|
+
return config.environment;
|
|
36
37
|
}
|
|
37
38
|
export default resolveCronEnvironment;
|
|
@@ -1,4 +1,18 @@
|
|
|
1
|
-
|
|
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 { jest } from '@jest/globals';
|
|
2
16
|
import { wait } from '@lowdefy/helpers';
|
|
3
17
|
import { operatorsServer } from '@lowdefy/operators-js';
|
|
4
18
|
import createEvaluateOperators from '../../../context/createEvaluateOperators.js';
|
|
@@ -7,7 +21,7 @@ import testContext from '../../../test/testContext.js';
|
|
|
7
21
|
// const { _date, _eq, _payload, _secret, _user } = operatorsServer;
|
|
8
22
|
const operators = {
|
|
9
23
|
...operatorsServer,
|
|
10
|
-
|
|
24
|
+
_throw_test: ()=>{
|
|
11
25
|
throw new Error('Test error.');
|
|
12
26
|
}
|
|
13
27
|
};
|
|
@@ -89,6 +103,7 @@ async function runTest({ routine, payload = {} }) {
|
|
|
89
103
|
const routineContext = {
|
|
90
104
|
steps: {},
|
|
91
105
|
payload,
|
|
106
|
+
error: null,
|
|
92
107
|
items: {},
|
|
93
108
|
arrayIndices: [],
|
|
94
109
|
state: {},
|
|
@@ -17,6 +17,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot
|
|
|
17
17
|
import { AuthenticationError } from '@lowdefy/errors';
|
|
18
18
|
import { serializer, type } from '@lowdefy/helpers';
|
|
19
19
|
import callEndpoint from '../endpoints/callEndpoint.js';
|
|
20
|
+
import createWireProjection from '../../response/createWireProjection.js';
|
|
20
21
|
import isUnauthenticatedHuman from '../endpoints/isUnauthenticatedHuman.js';
|
|
21
22
|
// LLM-safe tool names use the same rule as buildAgents tool naming.
|
|
22
23
|
function toToolName(id) {
|
|
@@ -102,11 +103,25 @@ async function createMcpServer({ context }) {
|
|
|
102
103
|
});
|
|
103
104
|
if (!success) {
|
|
104
105
|
const deserialized = serializer.deserialize(error);
|
|
106
|
+
let text = deserialized?.message ?? 'Endpoint failed.';
|
|
107
|
+
// The dev MCP route serves a coding agent, which is a dev tool: it gets the real
|
|
108
|
+
// message and the config location so it can go straight to the YAML. A prod MCP
|
|
109
|
+
// client is an end-user reader and keeps the wire message.
|
|
110
|
+
if (context.mode === 'dev' && !type.isNone(error?.devError)) {
|
|
111
|
+
const devError = serializer.deserialize(error.devError);
|
|
112
|
+
text = devError.message;
|
|
113
|
+
if (!type.isNone(devError.source)) {
|
|
114
|
+
text = `${text} (at ${devError.source})`;
|
|
115
|
+
}
|
|
116
|
+
if (!type.isNone(devError.hint)) {
|
|
117
|
+
text = `${text} Hint: ${devError.hint}`;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
105
120
|
return {
|
|
106
121
|
content: [
|
|
107
122
|
{
|
|
108
123
|
type: 'text',
|
|
109
|
-
text
|
|
124
|
+
text
|
|
110
125
|
}
|
|
111
126
|
],
|
|
112
127
|
isError: true
|
|
@@ -144,11 +159,14 @@ async function createMcpServer({ context }) {
|
|
|
144
159
|
} else {
|
|
145
160
|
context.logger.error(error);
|
|
146
161
|
}
|
|
162
|
+
// This error never went through an endpoint result, so it has not been projected yet.
|
|
163
|
+
// The projection keeps an AuthenticationError's message.
|
|
164
|
+
const text = context.mode === 'dev' ? error.message : createWireProjection(context)(error).message;
|
|
147
165
|
return {
|
|
148
166
|
content: [
|
|
149
167
|
{
|
|
150
168
|
type: 'text',
|
|
151
|
-
text
|
|
169
|
+
text
|
|
152
170
|
}
|
|
153
171
|
],
|
|
154
172
|
isError: true
|
|
@@ -59,10 +59,16 @@ async function callRequest(context, { blockId, pageId, payload, requestId }) {
|
|
|
59
59
|
});
|
|
60
60
|
const { connectionProperties, requestProperties } = evaluateOperators(context, {
|
|
61
61
|
connectionConfig,
|
|
62
|
-
payload: requestPayload,
|
|
63
62
|
requestConfig,
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
// A page request runs outside any routine, so it evaluates against an empty frame.
|
|
64
|
+
routineContext: {
|
|
65
|
+
arrayIndices: [],
|
|
66
|
+
error: null,
|
|
67
|
+
items: {},
|
|
68
|
+
payload: requestPayload,
|
|
69
|
+
state: {},
|
|
70
|
+
steps: {}
|
|
71
|
+
}
|
|
66
72
|
});
|
|
67
73
|
checkConnectionRead(context, {
|
|
68
74
|
connectionConfig,
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import { RequestError, ServiceError } from '@lowdefy/errors';
|
|
16
|
+
import getCurrentEnvironment from '../../context/getCurrentEnvironment.js';
|
|
16
17
|
import invokeEndpoint from '../endpoints/invokeEndpoint.js';
|
|
17
18
|
async function callRequestResolver(context, { connectionProperties, endpointDepth, requestConfig, requestProperties, requestResolver }) {
|
|
18
19
|
const { blockId, endpointId, logger, pageId, payload } = context;
|
|
@@ -49,6 +50,11 @@ async function callRequestResolver(context, { connectionProperties, endpointDept
|
|
|
49
50
|
connection: connectionProperties,
|
|
50
51
|
connectionId: requestConfig.connectionId,
|
|
51
52
|
endpointId,
|
|
53
|
+
// The deployment's current environment ({ name, url, email, ... }) or null, so connection
|
|
54
|
+
// types can apply environment settings such as the email delivery filter.
|
|
55
|
+
environment: getCurrentEnvironment({
|
|
56
|
+
config: context.config
|
|
57
|
+
}),
|
|
52
58
|
pageId,
|
|
53
59
|
payload,
|
|
54
60
|
request: requestProperties,
|
|
@@ -12,21 +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
|
-
*/
|
|
16
|
-
|
|
15
|
+
*/ import evaluateRoutineOperators from '../endpoints/evaluateRoutineOperators.js';
|
|
16
|
+
// Connection and request properties evaluate against the same frame, so `_item` in a connection's
|
|
17
|
+
// properties resolves inside a `:for` loop just like it does in the request's.
|
|
18
|
+
function evaluateOperators(context, { connectionConfig, requestConfig, routineContext }) {
|
|
19
|
+
const connectionProperties = evaluateRoutineOperators(context, routineContext, {
|
|
17
20
|
input: connectionConfig.properties || {},
|
|
18
|
-
location: connectionConfig.connectionId
|
|
19
|
-
payload,
|
|
20
|
-
state,
|
|
21
|
-
steps
|
|
21
|
+
location: connectionConfig.connectionId
|
|
22
22
|
});
|
|
23
|
-
const requestProperties =
|
|
23
|
+
const requestProperties = evaluateRoutineOperators(context, routineContext, {
|
|
24
24
|
input: requestConfig.properties || {},
|
|
25
|
-
|
|
26
|
-
location: requestConfig.stepId ?? requestConfig.requestId,
|
|
27
|
-
payload,
|
|
28
|
-
state,
|
|
29
|
-
steps
|
|
25
|
+
location: requestConfig.stepId ?? requestConfig.requestId
|
|
30
26
|
});
|
|
31
27
|
return {
|
|
32
28
|
connectionProperties,
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/ import crypto from 'crypto';
|
|
16
16
|
import { serializer } from '@lowdefy/helpers';
|
|
17
17
|
import { ConfigError, PluginError, ServiceError } from '@lowdefy/errors';
|
|
18
|
+
import createWireProjection from '../../response/createWireProjection.js';
|
|
18
19
|
import prepareChannel from './prepareChannel.js';
|
|
19
20
|
const MAX_RETRIES = 5;
|
|
20
21
|
const RETRY_BASE_MS = 1000;
|
|
@@ -39,6 +40,23 @@ function createChannelRegistry() {
|
|
|
39
40
|
subscriber.send(message);
|
|
40
41
|
});
|
|
41
42
|
}
|
|
43
|
+
// Config reads a broadcast error only as a message string, so it gets the wire
|
|
44
|
+
// message alone - no requestId, since no single request raised it, and no
|
|
45
|
+
// devError, since the dev terminal already logs it in full. The channel's
|
|
46
|
+
// context belongs to whoever subscribed first, so the message is translated
|
|
47
|
+
// with each subscriber's own locale.
|
|
48
|
+
function broadcastError(channel, error) {
|
|
49
|
+
channel.subscribers.forEach((subscriber)=>{
|
|
50
|
+
const { message } = createWireProjection({
|
|
51
|
+
i18n: subscriber.i18n
|
|
52
|
+
})(error);
|
|
53
|
+
subscriber.send(JSON.stringify({
|
|
54
|
+
type: 'error',
|
|
55
|
+
websocketId: channel.websocketId,
|
|
56
|
+
message
|
|
57
|
+
}));
|
|
58
|
+
});
|
|
59
|
+
}
|
|
42
60
|
function wrapResolverError({ channel, error }) {
|
|
43
61
|
if (!error.configKey) {
|
|
44
62
|
error.configKey = channel.websocketConfig['~k'];
|
|
@@ -86,11 +104,7 @@ function createChannelRegistry() {
|
|
|
86
104
|
err: wrapped
|
|
87
105
|
}, wrapped.message);
|
|
88
106
|
channel.context.handleError(wrapped);
|
|
89
|
-
|
|
90
|
-
type: 'error',
|
|
91
|
-
websocketId: channel.websocketId,
|
|
92
|
-
message: wrapped.message
|
|
93
|
-
});
|
|
107
|
+
broadcastError(channel, wrapped);
|
|
94
108
|
// A run that stayed healthy resets the backoff window.
|
|
95
109
|
if (Date.now() - startedAt > HEALTHY_RESET_MS) {
|
|
96
110
|
channel.retryCount = 0;
|