@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
|
@@ -14,28 +14,20 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import { type } from '@lowdefy/helpers';
|
|
16
16
|
import { ConfigError } from '@lowdefy/errors';
|
|
17
|
+
import evaluateRoutineOperators from '../evaluateRoutineOperators.js';
|
|
17
18
|
async function controlLog(context, routineContext, { control }) {
|
|
18
|
-
const { endpointId, logger
|
|
19
|
-
const { items } = routineContext;
|
|
19
|
+
const { endpointId, logger } = context;
|
|
20
20
|
const location = control['~k'] ?? ':log';
|
|
21
21
|
logger.debug({
|
|
22
22
|
event: 'debug_control_log'
|
|
23
23
|
});
|
|
24
|
-
const log =
|
|
24
|
+
const log = evaluateRoutineOperators(context, routineContext, {
|
|
25
25
|
input: control[':log'],
|
|
26
|
-
|
|
27
|
-
location,
|
|
28
|
-
payload: routineContext.payload,
|
|
29
|
-
state: routineContext.state,
|
|
30
|
-
steps: routineContext.steps
|
|
26
|
+
location
|
|
31
27
|
});
|
|
32
|
-
const logLevel =
|
|
28
|
+
const logLevel = evaluateRoutineOperators(context, routineContext, {
|
|
33
29
|
input: control[':level'],
|
|
34
|
-
|
|
35
|
-
location,
|
|
36
|
-
payload: routineContext.payload,
|
|
37
|
-
state: routineContext.state,
|
|
38
|
-
steps: routineContext.steps
|
|
30
|
+
location
|
|
39
31
|
}) ?? 'info';
|
|
40
32
|
if (!type.isString(logLevel)) {
|
|
41
33
|
throw new ConfigError(`Invalid :log in endpoint "${endpointId}" - :level must be a string.`, {
|
|
@@ -14,20 +14,17 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import { ConfigError } from '@lowdefy/errors';
|
|
16
16
|
import runRoutine from '../runRoutine.js';
|
|
17
|
+
import evaluateRoutineOperators from '../evaluateRoutineOperators.js';
|
|
17
18
|
async function controlParallelFor(context, routineContext, { control }) {
|
|
18
|
-
const { endpointId, logger
|
|
19
|
+
const { endpointId, logger } = context;
|
|
19
20
|
const { items } = routineContext;
|
|
20
21
|
const itemName = control[':parallel_for'];
|
|
21
22
|
if (!itemName) {
|
|
22
23
|
throw new Error(`Invalid :parallel_for in endpoint "${endpointId}" - missing variable name in :parallel_for.`);
|
|
23
24
|
}
|
|
24
|
-
const array =
|
|
25
|
+
const array = evaluateRoutineOperators(context, routineContext, {
|
|
25
26
|
input: control[':in'],
|
|
26
|
-
|
|
27
|
-
location: control['~k'] ?? ':parallel_for',
|
|
28
|
-
payload: routineContext.payload,
|
|
29
|
-
state: routineContext.state,
|
|
30
|
-
steps: routineContext.steps
|
|
27
|
+
location: control['~k'] ?? ':parallel_for'
|
|
31
28
|
});
|
|
32
29
|
logger.debug({
|
|
33
30
|
event: 'debug_control_parallel',
|
|
@@ -13,33 +13,33 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import { UserError } from '@lowdefy/errors';
|
|
16
|
+
import { type } from '@lowdefy/helpers';
|
|
17
|
+
import createWireProjection from '../../../response/createWireProjection.js';
|
|
18
|
+
import evaluateRoutineOperators from '../evaluateRoutineOperators.js';
|
|
16
19
|
async function controlReject(context, routineContext, { control }) {
|
|
17
|
-
const { evaluateOperators } = context;
|
|
18
|
-
const { items } = routineContext;
|
|
19
20
|
const location = control['~k'] ?? ':reject';
|
|
20
|
-
const message =
|
|
21
|
+
const message = evaluateRoutineOperators(context, routineContext, {
|
|
21
22
|
input: control[':reject'],
|
|
22
|
-
|
|
23
|
-
location,
|
|
24
|
-
payload: routineContext.payload,
|
|
25
|
-
state: routineContext.state,
|
|
26
|
-
steps: routineContext.steps
|
|
23
|
+
location
|
|
27
24
|
});
|
|
28
|
-
const cause =
|
|
25
|
+
const cause = evaluateRoutineOperators(context, routineContext, {
|
|
29
26
|
input: control[':cause'],
|
|
30
|
-
|
|
31
|
-
location,
|
|
32
|
-
payload: routineContext.payload,
|
|
33
|
-
state: routineContext.state,
|
|
34
|
-
steps: routineContext.steps
|
|
27
|
+
location
|
|
35
28
|
});
|
|
36
|
-
|
|
29
|
+
// A reject is the author telling the user something, so it stays a UserError, whose message the
|
|
30
|
+
// wire passes through as written. An Error given as the message is reduced to what the wire
|
|
31
|
+
// would show for it - the author's text for a UserError, the generic message for a plugin error
|
|
32
|
+
// - rather than stringified with its library text; `{ _error: message }` opts into the real text.
|
|
33
|
+
const text = type.isError(message) ? createWireProjection(context)(message).message : message;
|
|
34
|
+
const error = new UserError(text, {
|
|
37
35
|
cause,
|
|
38
36
|
isReject: true
|
|
39
37
|
});
|
|
38
|
+
// Log under `err` — see controlThrow: only the `err` key runs the pino error
|
|
39
|
+
// serializer, so `error` would drop the message from the log line.
|
|
40
40
|
context.logger.warn({
|
|
41
41
|
event: 'warn_control_reject',
|
|
42
|
-
error
|
|
42
|
+
err: error
|
|
43
43
|
});
|
|
44
44
|
return {
|
|
45
45
|
status: 'reject',
|
|
@@ -12,16 +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
|
-
*/
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
const response = evaluateOperators({
|
|
15
|
+
*/ import evaluateRoutineOperators from '../evaluateRoutineOperators.js';
|
|
16
|
+
async function controlReturn(context, routineContext, { control }) {
|
|
17
|
+
const response = evaluateRoutineOperators(context, routineContext, {
|
|
19
18
|
input: control[':return'],
|
|
20
|
-
|
|
21
|
-
location: control['~k'] ?? ':return',
|
|
22
|
-
payload: routineContext.payload,
|
|
23
|
-
state: routineContext.state,
|
|
24
|
-
steps: routineContext.steps
|
|
19
|
+
location: control['~k'] ?? ':return'
|
|
25
20
|
});
|
|
26
21
|
context.logger.debug({
|
|
27
22
|
event: 'debug_control_return',
|
|
@@ -14,16 +14,12 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import { ConfigError } from '@lowdefy/errors';
|
|
16
16
|
import { ReservedKeyError, set } from '@lowdefy/helpers';
|
|
17
|
+
import evaluateRoutineOperators from '../evaluateRoutineOperators.js';
|
|
17
18
|
function controlSetState(context, routineContext, { control }) {
|
|
18
|
-
const { logger
|
|
19
|
-
const
|
|
20
|
-
const evaluatedSetState = evaluateOperators({
|
|
19
|
+
const { logger } = context;
|
|
20
|
+
const evaluatedSetState = evaluateRoutineOperators(context, routineContext, {
|
|
21
21
|
input: control[':set_state'],
|
|
22
|
-
|
|
23
|
-
location: control['~k'] ?? ':set_state',
|
|
24
|
-
payload: routineContext.payload,
|
|
25
|
-
state: routineContext.state,
|
|
26
|
-
steps: routineContext.steps
|
|
22
|
+
location: control['~k'] ?? ':set_state'
|
|
27
23
|
});
|
|
28
24
|
logger.debug({
|
|
29
25
|
event: 'debug_control_set_state',
|
|
@@ -13,21 +13,17 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import runRoutine from '../runRoutine.js';
|
|
16
|
+
import evaluateRoutineOperators from '../evaluateRoutineOperators.js';
|
|
16
17
|
async function controlSwitch(context, routineContext, { control }) {
|
|
17
|
-
const { endpointId, logger
|
|
18
|
-
const { items } = routineContext;
|
|
18
|
+
const { endpointId, logger } = context;
|
|
19
19
|
const cases = control[':switch'];
|
|
20
20
|
logger.debug({
|
|
21
21
|
event: 'debug_control_switch'
|
|
22
22
|
});
|
|
23
23
|
for (const caseObj of cases){
|
|
24
|
-
const evaluatedCase =
|
|
24
|
+
const evaluatedCase = evaluateRoutineOperators(context, routineContext, {
|
|
25
25
|
input: caseObj[':case'],
|
|
26
|
-
|
|
27
|
-
location: caseObj['~k'] ?? control['~k'] ?? ':switch',
|
|
28
|
-
payload: routineContext.payload,
|
|
29
|
-
state: routineContext.state,
|
|
30
|
-
steps: routineContext.steps
|
|
26
|
+
location: caseObj['~k'] ?? control['~k'] ?? ':switch'
|
|
31
27
|
});
|
|
32
28
|
logger.debug({
|
|
33
29
|
event: 'debug_control_switch_case',
|
|
@@ -13,32 +13,33 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import { UserError } from '@lowdefy/errors';
|
|
16
|
+
import { type } from '@lowdefy/helpers';
|
|
17
|
+
import evaluateRoutineOperators from '../evaluateRoutineOperators.js';
|
|
16
18
|
async function controlThrow(context, routineContext, { control }) {
|
|
17
|
-
const { evaluateOperators } = context;
|
|
18
|
-
const { items } = routineContext;
|
|
19
19
|
const location = control['~k'] ?? ':throw';
|
|
20
|
-
const message =
|
|
20
|
+
const message = evaluateRoutineOperators(context, routineContext, {
|
|
21
21
|
input: control[':throw'],
|
|
22
|
-
|
|
23
|
-
location,
|
|
24
|
-
payload: routineContext.payload,
|
|
25
|
-
state: routineContext.state,
|
|
26
|
-
steps: routineContext.steps
|
|
27
|
-
});
|
|
28
|
-
const cause = evaluateOperators({
|
|
29
|
-
input: control[':cause'],
|
|
30
|
-
items,
|
|
31
|
-
location,
|
|
32
|
-
payload: routineContext.payload,
|
|
33
|
-
state: routineContext.state,
|
|
34
|
-
steps: routineContext.steps
|
|
35
|
-
});
|
|
36
|
-
const error = new UserError(message, {
|
|
37
|
-
cause
|
|
22
|
+
location
|
|
38
23
|
});
|
|
24
|
+
// An Error, such as a rethrown `_error`, goes on unchanged. Wrapped in a UserError it would be
|
|
25
|
+
// stringified into the message, which the wire sends as author text; under its own class the
|
|
26
|
+
// wire makes it generic unless it was a UserError. It carries its own cause, so :cause is unused.
|
|
27
|
+
let error = message;
|
|
28
|
+
if (!type.isError(message)) {
|
|
29
|
+
const cause = evaluateRoutineOperators(context, routineContext, {
|
|
30
|
+
input: control[':cause'],
|
|
31
|
+
location
|
|
32
|
+
});
|
|
33
|
+
error = new UserError(message, {
|
|
34
|
+
cause
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
// Log under `err` — the pino error serializer (createNodeLogger) is registered
|
|
38
|
+
// for the `err` key only; an Error passed as `error` is JSON-dumped without its
|
|
39
|
+
// non-enumerable `message`/`stack`, producing a log line with no message.
|
|
39
40
|
context.logger.error({
|
|
40
41
|
event: 'error_control_throw',
|
|
41
|
-
error
|
|
42
|
+
err: error
|
|
42
43
|
});
|
|
43
44
|
return {
|
|
44
45
|
status: 'error',
|
|
@@ -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;
|
|
@@ -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,111 @@
|
|
|
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 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.
|
|
28
|
+
async function forwardScheduledEndpoint(context, { environment, endpointId, cron }) {
|
|
29
|
+
const { config, logger, secrets } = context;
|
|
30
|
+
const target = config?.environments?.[environment];
|
|
31
|
+
if (!type.isObject(target)) {
|
|
32
|
+
throw new ConfigError(`Cron environment "${environment}" is not declared in config.environments.`);
|
|
33
|
+
}
|
|
34
|
+
if (environment === config.environment) {
|
|
35
|
+
throw new ConfigError(`Cron environment "${environment}" is this deployment's own environment: its crons run here, not forwarded.`);
|
|
36
|
+
}
|
|
37
|
+
if (target.cron?.enabled === false) {
|
|
38
|
+
throw new ConfigError(`Cron environment "${environment}" is disabled.`);
|
|
39
|
+
}
|
|
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];
|
|
45
|
+
if (!type.isString(secret) || 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.`);
|
|
47
|
+
}
|
|
48
|
+
// Every environment builds from the same config, so the local artifact says whether the target
|
|
49
|
+
// declares the firing schedule: fail here with a config error instead of having the target 500.
|
|
50
|
+
const endpointConfig = await getEndpointConfig(context, {
|
|
51
|
+
endpointId
|
|
52
|
+
});
|
|
53
|
+
const schedules = getEnvironmentSchedules({
|
|
54
|
+
endpointConfig,
|
|
55
|
+
environment
|
|
56
|
+
});
|
|
57
|
+
findSchedule({
|
|
58
|
+
schedules,
|
|
59
|
+
cron,
|
|
60
|
+
endpointId,
|
|
61
|
+
environment
|
|
62
|
+
});
|
|
63
|
+
// Trim trailing slashes with a loop rather than a regex: CodeQL flags `/\/+$/` on config input.
|
|
64
|
+
let origin = target.url;
|
|
65
|
+
while(origin.endsWith('/')){
|
|
66
|
+
origin = origin.slice(0, -1);
|
|
67
|
+
}
|
|
68
|
+
const url = `${origin}/api/cron/${endpointId}`;
|
|
69
|
+
const timeoutMs = (config?.vercel?.maxDuration ?? 60) * 1000;
|
|
70
|
+
const headers = {
|
|
71
|
+
authorization: `Bearer ${secret}`
|
|
72
|
+
};
|
|
73
|
+
if (cron) headers['x-vercel-cron-schedule'] = cron;
|
|
74
|
+
headers['x-lowdefy-cron-environment'] = environment;
|
|
75
|
+
logger.info({
|
|
76
|
+
event: 'forward_scheduled_endpoint',
|
|
77
|
+
endpointId,
|
|
78
|
+
environment,
|
|
79
|
+
cron,
|
|
80
|
+
url
|
|
81
|
+
});
|
|
82
|
+
scheduleBackground(context, {
|
|
83
|
+
event: 'forward_scheduled_endpoint',
|
|
84
|
+
endpointId
|
|
85
|
+
}, async ()=>{
|
|
86
|
+
const response = await fetch(url, {
|
|
87
|
+
method: 'GET',
|
|
88
|
+
headers,
|
|
89
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
90
|
+
});
|
|
91
|
+
// Drain the body so the connection is released; the target logs its own outcome.
|
|
92
|
+
await response.arrayBuffer();
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
throw new Error(`Forwarded cron for environment "${environment}" at ${url} responded ${response.status}.`);
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
status: response.status
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
return {
|
|
101
|
+
error: null,
|
|
102
|
+
response: serializer.serialize({
|
|
103
|
+
accepted: true,
|
|
104
|
+
environment,
|
|
105
|
+
endpointId
|
|
106
|
+
}),
|
|
107
|
+
status: 'accepted',
|
|
108
|
+
success: true
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
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,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
|
+
*/ import { type } from '@lowdefy/helpers';
|
|
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.
|
|
20
|
+
function getEnvironmentSchedules({ endpointConfig, environment }) {
|
|
21
|
+
const schedules = endpointConfig.schedules;
|
|
22
|
+
if (type.isArray(schedules)) {
|
|
23
|
+
return schedules;
|
|
24
|
+
}
|
|
25
|
+
return schedules?.[environment ?? 'default'] ?? [];
|
|
26
|
+
}
|
|
27
|
+
export default getEnvironmentSchedules;
|
|
@@ -0,0 +1,80 @@
|
|
|
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
|
+
import evaluateRoutineOperators from './evaluateRoutineOperators.js';
|
|
20
|
+
async function handleAgentCall(context, routineContext, { step }) {
|
|
21
|
+
const { logger } = context;
|
|
22
|
+
logger.debug({
|
|
23
|
+
event: 'debug_start_agent_call',
|
|
24
|
+
step
|
|
25
|
+
});
|
|
26
|
+
// Evaluate operators in step.properties (resolves agentId, prompt)
|
|
27
|
+
const evaluatedProperties = evaluateRoutineOperators(context, routineContext, {
|
|
28
|
+
input: step.properties,
|
|
29
|
+
location: step.stepId
|
|
30
|
+
});
|
|
31
|
+
const { agentId, prompt } = evaluatedProperties;
|
|
32
|
+
if (!type.isString(agentId)) {
|
|
33
|
+
throw new ConfigError(`CallAgent step "${step.stepId}" properties.agentId must evaluate to a string. Received ${JSON.stringify(agentId)}.`, {
|
|
34
|
+
configKey: step['~k']
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
if (!type.isString(prompt)) {
|
|
38
|
+
throw new ConfigError(`CallAgent step "${step.stepId}" properties.prompt must evaluate to a string. Received ${JSON.stringify(prompt)}.`, {
|
|
39
|
+
configKey: step['~k']
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
// Headless agent context — no page, no conversation, no sharedState (which
|
|
43
|
+
// also excludes the client-only update-page-state tool). userId is null
|
|
44
|
+
// under scheduled (system) context.
|
|
45
|
+
const agentContext = {
|
|
46
|
+
conversationId: null,
|
|
47
|
+
pageId: null,
|
|
48
|
+
sharedState: undefined,
|
|
49
|
+
urlQuery: {},
|
|
50
|
+
userId: context.user?.sub ?? context.user?.id ?? null
|
|
51
|
+
};
|
|
52
|
+
const { agentConfig, connectionInstance, agentType, resolverContext } = await prepareAgent(context, {
|
|
53
|
+
agentId,
|
|
54
|
+
agentContext,
|
|
55
|
+
endpointDepth: routineContext.endpointDepth,
|
|
56
|
+
mode: 'generate'
|
|
57
|
+
});
|
|
58
|
+
const { result } = await agentType.resolver({
|
|
59
|
+
connection: connectionInstance,
|
|
60
|
+
properties: {
|
|
61
|
+
agent: agentConfig,
|
|
62
|
+
prompt
|
|
63
|
+
},
|
|
64
|
+
context: resolverContext
|
|
65
|
+
});
|
|
66
|
+
addStepResult(context, routineContext, {
|
|
67
|
+
result,
|
|
68
|
+
stepId: step.stepId
|
|
69
|
+
});
|
|
70
|
+
logger.debug({
|
|
71
|
+
event: 'debug_end_agent_call',
|
|
72
|
+
stepId: step.stepId,
|
|
73
|
+
targetAgentId: agentId,
|
|
74
|
+
finishReason: result?.finishReason
|
|
75
|
+
});
|
|
76
|
+
return {
|
|
77
|
+
status: 'continue'
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export default handleAgentCall;
|