@lowdefy/api 5.6.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/createAuthorize.js +4 -1
- package/dist/context/createEvaluateOperators.js +3 -1
- package/dist/context/getCurrentEnvironment.js +27 -0
- package/dist/context/resolveStrategyCaller.js +56 -0
- package/dist/index.js +13 -2
- 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/callAgent.js +7 -184
- package/dist/routes/agent/prepareAgent.js +179 -0
- package/dist/routes/auth/createLogger.js +2 -5
- package/dist/routes/auth/createPrefixedCookies.js +52 -0
- package/dist/routes/auth/defaultAuthUrl.js +31 -0
- package/dist/routes/auth/{getNextAuthConfig.js → getAuthConfig.js} +31 -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 +27 -3
- 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 +16 -16
- 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 +21 -20
- package/dist/routes/endpoints/control/controlTry.js +20 -10
- package/dist/routes/endpoints/evaluateRoutineOperators.js +26 -0
- package/dist/routes/endpoints/findSchedule.js +35 -0
- package/dist/routes/endpoints/forwardScheduledEndpoint.js +111 -0
- package/dist/routes/endpoints/getEndpointConfig.js +9 -3
- package/dist/routes/endpoints/getEnvironmentSchedules.js +27 -0
- package/dist/routes/endpoints/handleAgentCall.js +80 -0
- package/dist/routes/endpoints/handleEndpointCall.js +53 -8
- package/dist/routes/endpoints/handleRenderNotification.js +196 -0
- package/dist/routes/endpoints/handleRequest.js +1 -5
- package/dist/routes/endpoints/handleValidateSchema.js +14 -10
- package/dist/routes/endpoints/invokeEndpoint.js +1 -0
- package/dist/routes/endpoints/isUnauthenticatedHuman.js +31 -0
- package/dist/routes/endpoints/operatorScope.js +27 -0
- package/dist/routes/endpoints/resolveCronEnvironment.js +38 -0
- package/dist/routes/endpoints/runDetachedEndpoint.js +67 -0
- package/dist/routes/endpoints/runRoutine.js +12 -0
- package/dist/routes/endpoints/runScheduledEndpoint.js +105 -0
- package/dist/routes/endpoints/runWebhookEndpoint.js +84 -0
- package/dist/routes/endpoints/scheduleBackground.js +48 -0
- package/dist/routes/endpoints/test/runTest.js +17 -2
- package/dist/routes/mcp/createMcpServer.js +178 -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/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/authorizeWebsocket.js +27 -0
- package/dist/routes/websocket/createChannelRegistry.js +283 -0
- package/dist/routes/websocket/createWebSocketConnection.js +143 -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 +7 -3
- package/package.json +12 -10
- package/dist/response/omitErrorProps.js +0 -51
|
@@ -12,23 +12,68 @@
|
|
|
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';
|
|
20
|
+
import evaluateRoutineOperators from './evaluateRoutineOperators.js';
|
|
17
21
|
async function handleEndpointCall(context, routineContext, { step }) {
|
|
18
|
-
const { logger
|
|
22
|
+
const { logger } = context;
|
|
19
23
|
logger.debug({
|
|
20
24
|
event: 'debug_start_endpoint_call',
|
|
21
25
|
step
|
|
22
26
|
});
|
|
23
27
|
// Evaluate operators in step.properties (resolves endpointId, payload)
|
|
24
|
-
const evaluatedProperties =
|
|
28
|
+
const evaluatedProperties = evaluateRoutineOperators(context, routineContext, {
|
|
25
29
|
input: step.properties,
|
|
26
|
-
|
|
27
|
-
location: step.stepId,
|
|
28
|
-
payload: routineContext.payload,
|
|
29
|
-
state: routineContext.state,
|
|
30
|
-
steps: routineContext.steps
|
|
30
|
+
location: step.stepId
|
|
31
31
|
});
|
|
32
|
+
// detached: true — fire-and-forget the call back through the deployment's
|
|
33
|
+
// /api/detached route, so the target runs in its OWN function invocation
|
|
34
|
+
// with a fresh duration budget (chainable bounded work without a queue).
|
|
35
|
+
// At-most-once, no retry: targets must be idempotent. The dispatch promise
|
|
36
|
+
// rides the platform request context so the outgoing request always leaves
|
|
37
|
+
// before the invocation is reaped; the target's own outcome exists only in
|
|
38
|
+
// its logs and whatever its routine writes.
|
|
39
|
+
if (evaluatedProperties.detached === true) {
|
|
40
|
+
if (!process.env.CRON_SECRET) {
|
|
41
|
+
throw new ConfigError('CallApi with "detached: true" requires the CRON_SECRET environment variable — the /api/detached route fails closed without it.');
|
|
42
|
+
}
|
|
43
|
+
if (!context.origin) {
|
|
44
|
+
throw new ConfigError('Detached endpoint calls require the request origin on context.');
|
|
45
|
+
}
|
|
46
|
+
const targetEndpointId = evaluatedProperties.endpointId;
|
|
47
|
+
scheduleBackground(context, {
|
|
48
|
+
event: 'detached_dispatch',
|
|
49
|
+
endpointId: targetEndpointId
|
|
50
|
+
}, ()=>fetch(`${context.origin}/api/detached/${targetEndpointId}`, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: {
|
|
53
|
+
'content-type': 'application/json',
|
|
54
|
+
authorization: `Bearer ${process.env.CRON_SECRET}`
|
|
55
|
+
},
|
|
56
|
+
body: JSON.stringify({
|
|
57
|
+
payload: serializer.serialize(evaluatedProperties.payload ?? {})
|
|
58
|
+
})
|
|
59
|
+
}));
|
|
60
|
+
addStepResult(context, routineContext, {
|
|
61
|
+
result: {
|
|
62
|
+
detached: true,
|
|
63
|
+
endpointId: targetEndpointId
|
|
64
|
+
},
|
|
65
|
+
stepId: step.stepId
|
|
66
|
+
});
|
|
67
|
+
logger.debug({
|
|
68
|
+
event: 'debug_end_endpoint_call',
|
|
69
|
+
stepId: step.stepId,
|
|
70
|
+
targetEndpointId,
|
|
71
|
+
detached: true
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
status: 'continue'
|
|
75
|
+
};
|
|
76
|
+
}
|
|
32
77
|
const result = await invokeEndpoint(context, {
|
|
33
78
|
endpointId: evaluatedProperties.endpointId,
|
|
34
79
|
payload: evaluatedProperties.payload,
|
|
@@ -0,0 +1,196 @@
|
|
|
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 { validate } from '@lowdefy/ajv';
|
|
16
|
+
import { ConfigError } from '@lowdefy/errors';
|
|
17
|
+
import { type } from '@lowdefy/helpers';
|
|
18
|
+
import addStepResult from './addStepResult.js';
|
|
19
|
+
import getCurrentEnvironment from '../../context/getCurrentEnvironment.js';
|
|
20
|
+
import derivePreview from '../notifications/derivePreview.js';
|
|
21
|
+
import getNotificationConfig from '../notifications/getNotificationConfig.js';
|
|
22
|
+
import resolveNotificationLinks from '../notifications/resolveNotificationLinks.js';
|
|
23
|
+
import resolveThemeLogo from '../notifications/resolveThemeLogo.js';
|
|
24
|
+
import evaluateRoutineOperators from './evaluateRoutineOperators.js';
|
|
25
|
+
function itemHasPageLinks(item, dataKeys) {
|
|
26
|
+
const links = Object.values(item.links ?? {});
|
|
27
|
+
const arrayLinks = dataKeys.flatMap((key)=>(type.isArray(item[key]) ? item[key] : []).map((entry)=>entry?.link));
|
|
28
|
+
return [
|
|
29
|
+
...links,
|
|
30
|
+
...arrayLinks
|
|
31
|
+
].some((link)=>type.isObject(link) && !type.isNone(link.pageId));
|
|
32
|
+
}
|
|
33
|
+
async function handleRenderNotification(context, routineContext, { step }) {
|
|
34
|
+
const { logger } = context;
|
|
35
|
+
logger.debug({
|
|
36
|
+
event: 'debug_start_render_notification',
|
|
37
|
+
step
|
|
38
|
+
});
|
|
39
|
+
const evaluatedProperties = evaluateRoutineOperators(context, routineContext, {
|
|
40
|
+
input: step.properties,
|
|
41
|
+
location: step.stepId
|
|
42
|
+
});
|
|
43
|
+
const { notificationId, data, landingPage, recordId } = evaluatedProperties;
|
|
44
|
+
let { serverUrl } = evaluatedProperties;
|
|
45
|
+
if (!type.isString(notificationId)) {
|
|
46
|
+
throw new ConfigError(`RenderNotification step "${step.stepId}" properties.notificationId must evaluate to a string. Received ${JSON.stringify(notificationId)}.`, {
|
|
47
|
+
configKey: step['~k']
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
if (type.isArray(data)) {
|
|
51
|
+
throw new ConfigError(`RenderNotification step "${step.stepId}" properties.data must evaluate to an object. Received an array — iterate with a ":for" control and render one item per step.`, {
|
|
52
|
+
configKey: step['~k']
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (!type.isObject(data)) {
|
|
56
|
+
throw new ConfigError(`RenderNotification step "${step.stepId}" properties.data must evaluate to an object. Received ${JSON.stringify(data)}.`, {
|
|
57
|
+
configKey: step['~k']
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (!type.isNone(serverUrl)) {
|
|
61
|
+
if (!type.isString(serverUrl) || serverUrl === '') {
|
|
62
|
+
throw new ConfigError(`RenderNotification step "${step.stepId}" properties.serverUrl must evaluate to a non-empty string. Received ${JSON.stringify(serverUrl)}.`, {
|
|
63
|
+
configKey: step['~k']
|
|
64
|
+
});
|
|
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)) {
|
|
75
|
+
serverUrl = serverUrl.replace(/\/$/, '');
|
|
76
|
+
}
|
|
77
|
+
if (!type.isNone(landingPage) && !type.isString(landingPage)) {
|
|
78
|
+
throw new ConfigError(`RenderNotification step "${step.stepId}" properties.landingPage must evaluate to a string. Received ${JSON.stringify(landingPage)}.`, {
|
|
79
|
+
configKey: step['~k']
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (!type.isNone(recordId) && !type.isString(recordId)) {
|
|
83
|
+
throw new ConfigError(`RenderNotification step "${step.stepId}" properties.recordId must evaluate to a string. Received ${JSON.stringify(recordId)}.`, {
|
|
84
|
+
configKey: step['~k']
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const notificationConfig = await getNotificationConfig(context, {
|
|
88
|
+
notificationId,
|
|
89
|
+
configKey: step['~k']
|
|
90
|
+
});
|
|
91
|
+
const configKey = notificationConfig['~k'];
|
|
92
|
+
const Template = context.notifications[notificationConfig.type];
|
|
93
|
+
if (!Template) {
|
|
94
|
+
throw new ConfigError(`Notification template type "${notificationConfig.type}" can not be found.`, {
|
|
95
|
+
configKey
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
const { renderEmail, interpolateProperties } = context;
|
|
99
|
+
if (!type.isFunction(renderEmail) || !type.isFunction(interpolateProperties)) {
|
|
100
|
+
// plugins/notifications.js exports real implementations whenever the app
|
|
101
|
+
// has notifications built — reaching this means the build artifact and
|
|
102
|
+
// config are out of sync.
|
|
103
|
+
throw new ConfigError('Email rendering is not available. Rebuild the app — @lowdefy/email-templates is installed when "notifications:" is configured.', {
|
|
104
|
+
configKey
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
const app = await context.readConfigFile('app.json') ?? {};
|
|
108
|
+
const basePath = context.config?.basePath ?? '';
|
|
109
|
+
// Resolve after the merge so a per-notification theme.logo override also
|
|
110
|
+
// resolves; precedence stays notification theme over app.email.
|
|
111
|
+
const theme = resolveThemeLogo({
|
|
112
|
+
theme: {
|
|
113
|
+
...app.email ?? {},
|
|
114
|
+
...notificationConfig.theme ?? {}
|
|
115
|
+
},
|
|
116
|
+
serverUrl,
|
|
117
|
+
basePath
|
|
118
|
+
});
|
|
119
|
+
if (itemHasPageLinks(data, Template.dataKeys ?? [])) {
|
|
120
|
+
if (type.isNone(serverUrl)) {
|
|
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.`, {
|
|
122
|
+
configKey: step['~k']
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
// Landing URLs embed the record id — without it the landing page cannot
|
|
126
|
+
// resolve the record and every link in the email would be dead.
|
|
127
|
+
if (!type.isNone(landingPage) && type.isNone(recordId)) {
|
|
128
|
+
throw new ConfigError(`RenderNotification step "${step.stepId}" properties.landingPage requires properties.recordId to compose landing URLs.`, {
|
|
129
|
+
configKey: step['~k']
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const resolvedItem = resolveNotificationLinks({
|
|
134
|
+
item: data,
|
|
135
|
+
dataKeys: Template.dataKeys ?? [],
|
|
136
|
+
serverUrl: serverUrl ?? '',
|
|
137
|
+
basePath,
|
|
138
|
+
landingPage,
|
|
139
|
+
recordId
|
|
140
|
+
});
|
|
141
|
+
let interpolated;
|
|
142
|
+
try {
|
|
143
|
+
interpolated = interpolateProperties({
|
|
144
|
+
properties: notificationConfig.properties,
|
|
145
|
+
data: resolvedItem,
|
|
146
|
+
markdownProperties: Template.markdownProperties ?? []
|
|
147
|
+
});
|
|
148
|
+
} catch (error) {
|
|
149
|
+
throw new ConfigError(`Notification "${notificationId}" template interpolation failed: ${error.message}`, {
|
|
150
|
+
configKey,
|
|
151
|
+
cause: error
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
if (Template.schema) {
|
|
155
|
+
const { valid, errors } = validate({
|
|
156
|
+
schema: Template.schema,
|
|
157
|
+
data: interpolated,
|
|
158
|
+
returnErrors: true
|
|
159
|
+
});
|
|
160
|
+
if (!valid) {
|
|
161
|
+
throw new ConfigError(`Notification "${notificationId}" properties do not match template "${notificationConfig.type}" schema: ${errors?.[0]?.message}.`, {
|
|
162
|
+
configKey
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const { html, text } = await renderEmail({
|
|
167
|
+
Template,
|
|
168
|
+
properties: interpolated,
|
|
169
|
+
data: resolvedItem,
|
|
170
|
+
theme,
|
|
171
|
+
links: resolvedItem.links ?? {}
|
|
172
|
+
});
|
|
173
|
+
const result = {
|
|
174
|
+
subject: interpolated.subject,
|
|
175
|
+
title: interpolated.title ?? interpolated.subject,
|
|
176
|
+
preview: derivePreview({
|
|
177
|
+
properties: interpolated
|
|
178
|
+
}),
|
|
179
|
+
html,
|
|
180
|
+
text,
|
|
181
|
+
data: resolvedItem
|
|
182
|
+
};
|
|
183
|
+
addStepResult(context, routineContext, {
|
|
184
|
+
result,
|
|
185
|
+
stepId: step.stepId
|
|
186
|
+
});
|
|
187
|
+
logger.debug({
|
|
188
|
+
event: 'debug_end_render_notification',
|
|
189
|
+
stepId: step.stepId,
|
|
190
|
+
notificationId
|
|
191
|
+
});
|
|
192
|
+
return {
|
|
193
|
+
status: 'continue'
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
export default handleRenderNotification;
|
|
@@ -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,13 +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));
|
|
55
|
+
// Log under `err` — see controlThrow: only the `err` key runs the pino error
|
|
56
|
+
// serializer, so `error` would drop the message from the log line.
|
|
54
57
|
logger.error({
|
|
55
58
|
event: 'error_validate_schema',
|
|
56
59
|
stepId: step.stepId,
|
|
57
|
-
error
|
|
60
|
+
err: error,
|
|
61
|
+
errors: result.errors
|
|
58
62
|
});
|
|
59
63
|
return {
|
|
60
64
|
status: 'error',
|
|
@@ -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 { type } from '@lowdefy/helpers';
|
|
16
|
+
// True for a session-less caller on an app that has auth configured, outside a
|
|
17
|
+
// system run. This is the one caller who must get the same answer for a missing
|
|
18
|
+
// id as for a protected one - "authenticate" - or a logged-out client can
|
|
19
|
+
// enumerate endpoint ids by response difference (500/does-not-exist for a miss
|
|
20
|
+
// versus 401 for a protected id). "No user" is not enough on its own: it is also
|
|
21
|
+
// "no auth in this app" and "the engine talking to itself" (scheduled, webhook
|
|
22
|
+
// and detached runs), and neither should be told to authenticate against
|
|
23
|
+
// nothing. auth.json is a build artifact the build always writes.
|
|
24
|
+
async function isUnauthenticatedHuman({ readConfigFile, system, user }) {
|
|
25
|
+
if (!type.isNone(user) || system === true) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
const authConfig = await readConfigFile('auth.json');
|
|
29
|
+
return authConfig?.configured === true;
|
|
30
|
+
}
|
|
31
|
+
export default isUnauthenticatedHuman;
|
|
@@ -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;
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
import { ConfigError } from '@lowdefy/errors';
|
|
17
|
+
// The environment a cron run resolves its schedules for. A forwarded request names it in the
|
|
18
|
+
// x-lowdefy-cron-environment header; a request without one is the deployment running its own crons,
|
|
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.
|
|
22
|
+
function resolveCronEnvironment({ config, environment }) {
|
|
23
|
+
const environments = config?.environments;
|
|
24
|
+
if (type.isNone(environments)) {
|
|
25
|
+
if (!type.isNone(environment)) {
|
|
26
|
+
throw new ConfigError(`Cron environment "${environment}" is not configured: config.environments is not defined.`);
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
if (!type.isNone(environment)) {
|
|
31
|
+
if (!type.isObject(environments[environment])) {
|
|
32
|
+
throw new ConfigError(`Cron environment "${environment}" is not declared in config.environments.`);
|
|
33
|
+
}
|
|
34
|
+
return environment;
|
|
35
|
+
}
|
|
36
|
+
return config.environment;
|
|
37
|
+
}
|
|
38
|
+
export default resolveCronEnvironment;
|
|
@@ -0,0 +1,67 @@
|
|
|
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 } from '@lowdefy/helpers';
|
|
16
|
+
import buildEndpointResult from '../../response/buildEndpointResult.js';
|
|
17
|
+
import createAuthorize from '../../context/createAuthorize.js';
|
|
18
|
+
import createEvaluateOperators from '../../context/createEvaluateOperators.js';
|
|
19
|
+
import getEndpointConfig from './getEndpointConfig.js';
|
|
20
|
+
import runRoutine from './runRoutine.js';
|
|
21
|
+
// Runs an endpoint invoked through the detached route (a CallApi step with
|
|
22
|
+
// `detached: true` fire-and-forgets an HTTP call back to the deployment, so the
|
|
23
|
+
// target runs in its OWN function invocation with a fresh duration budget).
|
|
24
|
+
// Like a cron run this is authorized by the transport layer (CRON_SECRET) and
|
|
25
|
+
// executes as a system context: no user session, `_user` resolves to undefined,
|
|
26
|
+
// and InternalApi endpoints are callable. Payload arrives serialized (dates
|
|
27
|
+
// etc. survive the HTTP hop via @lowdefy/helpers serializer).
|
|
28
|
+
async function runDetachedEndpoint(context, { endpointId, payload }) {
|
|
29
|
+
const { logger } = context;
|
|
30
|
+
context.endpointId = endpointId;
|
|
31
|
+
context.evaluateOperators = createEvaluateOperators(context);
|
|
32
|
+
logger.debug({
|
|
33
|
+
event: 'debug_detached_endpoint',
|
|
34
|
+
endpointId
|
|
35
|
+
});
|
|
36
|
+
const endpointConfig = await getEndpointConfig(context, {
|
|
37
|
+
endpointId
|
|
38
|
+
});
|
|
39
|
+
// Force a system context regardless of any session cookie sent with the request.
|
|
40
|
+
// system: true — nested CallApi steps are authorized like function calls (the run
|
|
41
|
+
// was already authorized at the transport layer), not re-gated on a user session.
|
|
42
|
+
context.session = undefined;
|
|
43
|
+
context.user = undefined;
|
|
44
|
+
context.system = true;
|
|
45
|
+
context.authorize = createAuthorize({
|
|
46
|
+
session: undefined,
|
|
47
|
+
system: true
|
|
48
|
+
});
|
|
49
|
+
const routineContext = {
|
|
50
|
+
steps: {},
|
|
51
|
+
payload: serializer.deserialize(payload ?? {}),
|
|
52
|
+
arrayIndices: [],
|
|
53
|
+
error: null,
|
|
54
|
+
items: {},
|
|
55
|
+
state: {},
|
|
56
|
+
endpointDepth: 0
|
|
57
|
+
};
|
|
58
|
+
const { error, response, status } = await runRoutine(context, routineContext, {
|
|
59
|
+
routine: endpointConfig.routine
|
|
60
|
+
});
|
|
61
|
+
return buildEndpointResult(context, {
|
|
62
|
+
error,
|
|
63
|
+
response,
|
|
64
|
+
status
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
export default runDetachedEndpoint;
|
|
@@ -13,8 +13,10 @@
|
|
|
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
|
+
import handleAgentCall from './handleAgentCall.js';
|
|
16
17
|
import handleControl from './control/handleControl.js';
|
|
17
18
|
import handleEndpointCall from './handleEndpointCall.js';
|
|
19
|
+
import handleRenderNotification from './handleRenderNotification.js';
|
|
18
20
|
import handleRequest from './handleRequest.js';
|
|
19
21
|
import handleValidateSchema from './handleValidateSchema.js';
|
|
20
22
|
async function runRoutine(context, routineContext, { routine }) {
|
|
@@ -35,6 +37,16 @@ async function runRoutine(context, routineContext, { routine }) {
|
|
|
35
37
|
step: routine
|
|
36
38
|
});
|
|
37
39
|
}
|
|
40
|
+
if (routine.id?.startsWith?.('agent:')) {
|
|
41
|
+
return await handleAgentCall(context, routineContext, {
|
|
42
|
+
step: routine
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
if (routine.id?.startsWith?.('notification:')) {
|
|
46
|
+
return await handleRenderNotification(context, routineContext, {
|
|
47
|
+
step: routine
|
|
48
|
+
});
|
|
49
|
+
}
|
|
38
50
|
return await handleControl(context, routineContext, {
|
|
39
51
|
control: routine
|
|
40
52
|
});
|
|
@@ -0,0 +1,105 @@
|
|
|
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 } from '@lowdefy/helpers';
|
|
16
|
+
import createAuthorize from '../../context/createAuthorize.js';
|
|
17
|
+
import buildEndpointResult from '../../response/buildEndpointResult.js';
|
|
18
|
+
import createEvaluateOperators from '../../context/createEvaluateOperators.js';
|
|
19
|
+
import findSchedule from './findSchedule.js';
|
|
20
|
+
import getEndpointConfig from './getEndpointConfig.js';
|
|
21
|
+
import getEnvironmentSchedules from './getEnvironmentSchedules.js';
|
|
22
|
+
import resolveCronEnvironment from './resolveCronEnvironment.js';
|
|
23
|
+
import runRoutine from './runRoutine.js';
|
|
24
|
+
import scheduleBackground from './scheduleBackground.js';
|
|
25
|
+
// Runs an endpoint routine on a schedule (cron). Unlike callEndpoint this does NOT check the
|
|
26
|
+
// endpoint's `auth` config and does NOT block InternalApi: a cron run is authorized by the transport
|
|
27
|
+
// layer (CRON_SECRET) plus the endpoint declaring the firing schedule, and runs as a system context
|
|
28
|
+
// (no user session, so `_user` resolves to undefined). An InternalApi with schedules is therefore a
|
|
29
|
+
// cron-only endpoint that is never client-callable.
|
|
30
|
+
// `environment` (x-lowdefy-cron-environment, set on requests forwarded from the production
|
|
31
|
+
// deployment) selects which environment's schedules apply; see resolveCronEnvironment.
|
|
32
|
+
async function runScheduledEndpoint(context, { endpointId, cron, environment }) {
|
|
33
|
+
const { logger } = context;
|
|
34
|
+
context.endpointId = endpointId;
|
|
35
|
+
context.evaluateOperators = createEvaluateOperators(context);
|
|
36
|
+
const cronEnvironment = resolveCronEnvironment({
|
|
37
|
+
config: context.config,
|
|
38
|
+
environment
|
|
39
|
+
});
|
|
40
|
+
logger.debug({
|
|
41
|
+
event: 'debug_scheduled_endpoint',
|
|
42
|
+
endpointId,
|
|
43
|
+
cron,
|
|
44
|
+
environment: cronEnvironment
|
|
45
|
+
});
|
|
46
|
+
const endpointConfig = await getEndpointConfig(context, {
|
|
47
|
+
endpointId
|
|
48
|
+
});
|
|
49
|
+
const schedules = getEnvironmentSchedules({
|
|
50
|
+
endpointConfig,
|
|
51
|
+
environment: cronEnvironment
|
|
52
|
+
});
|
|
53
|
+
const schedule = findSchedule({
|
|
54
|
+
schedules,
|
|
55
|
+
cron,
|
|
56
|
+
endpointId,
|
|
57
|
+
environment: cronEnvironment
|
|
58
|
+
});
|
|
59
|
+
// Force a system context regardless of any session cookie sent with the request.
|
|
60
|
+
// system: true — nested CallApi steps are authorized like function calls (the run
|
|
61
|
+
// was already authorized at the transport layer), not re-gated on a user session.
|
|
62
|
+
context.session = undefined;
|
|
63
|
+
context.user = undefined;
|
|
64
|
+
context.system = true;
|
|
65
|
+
context.authorize = createAuthorize({
|
|
66
|
+
session: undefined,
|
|
67
|
+
system: true
|
|
68
|
+
});
|
|
69
|
+
const routineContext = {
|
|
70
|
+
steps: {},
|
|
71
|
+
payload: schedule.payload ?? {},
|
|
72
|
+
arrayIndices: [],
|
|
73
|
+
error: null,
|
|
74
|
+
items: {},
|
|
75
|
+
state: {},
|
|
76
|
+
endpointDepth: 0
|
|
77
|
+
};
|
|
78
|
+
// async: true — acknowledge the cron trigger immediately and run in the
|
|
79
|
+
// background; transport auth (CRON_SECRET) already passed at the route.
|
|
80
|
+
if (endpointConfig.async === true) {
|
|
81
|
+
scheduleBackground(context, {
|
|
82
|
+
event: 'background_scheduled_endpoint',
|
|
83
|
+
endpointId
|
|
84
|
+
}, ()=>runRoutine(context, routineContext, {
|
|
85
|
+
routine: endpointConfig.routine
|
|
86
|
+
}));
|
|
87
|
+
return {
|
|
88
|
+
error: null,
|
|
89
|
+
response: serializer.serialize({
|
|
90
|
+
accepted: true
|
|
91
|
+
}),
|
|
92
|
+
status: 'accepted',
|
|
93
|
+
success: true
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const { error, response, status } = await runRoutine(context, routineContext, {
|
|
97
|
+
routine: endpointConfig.routine
|
|
98
|
+
});
|
|
99
|
+
return buildEndpointResult(context, {
|
|
100
|
+
error,
|
|
101
|
+
response,
|
|
102
|
+
status
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
export default runScheduledEndpoint;
|