@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.
Files changed (72) hide show
  1. package/dist/context/createAuthorize.js +4 -1
  2. package/dist/context/createEvaluateOperators.js +3 -1
  3. package/dist/context/getCurrentEnvironment.js +27 -0
  4. package/dist/context/resolveStrategyCaller.js +56 -0
  5. package/dist/index.js +13 -2
  6. package/dist/response/buildEndpointResult.js +3 -1
  7. package/dist/response/createWireProjection.js +88 -0
  8. package/dist/response/normalizeErrorSources.js +10 -13
  9. package/dist/response/redactErrorResponse.js +42 -8
  10. package/dist/response/redactResponse.js +10 -10
  11. package/dist/routes/agent/callAgent.js +7 -184
  12. package/dist/routes/agent/prepareAgent.js +179 -0
  13. package/dist/routes/auth/createLogger.js +2 -5
  14. package/dist/routes/auth/createPrefixedCookies.js +52 -0
  15. package/dist/routes/auth/defaultAuthUrl.js +31 -0
  16. package/dist/routes/auth/{getNextAuthConfig.js → getAuthConfig.js} +31 -16
  17. package/dist/routes/auth/resolveCookies.js +37 -0
  18. package/dist/routes/auth/strategies/createAuthStrategies.js +77 -0
  19. package/dist/routes/auth/strategies/getAuthStrategies.js +31 -0
  20. package/dist/routes/endpoints/authorizeApiEndpoint.js +8 -2
  21. package/dist/routes/endpoints/callEndpoint.js +27 -3
  22. package/dist/routes/endpoints/control/controlFor.js +4 -7
  23. package/dist/routes/endpoints/control/controlIf.js +4 -8
  24. package/dist/routes/endpoints/control/controlLog.js +6 -14
  25. package/dist/routes/endpoints/control/controlParallelFor.js +4 -7
  26. package/dist/routes/endpoints/control/controlReject.js +16 -16
  27. package/dist/routes/endpoints/control/controlReturn.js +4 -9
  28. package/dist/routes/endpoints/control/controlSetState.js +4 -8
  29. package/dist/routes/endpoints/control/controlSwitch.js +4 -8
  30. package/dist/routes/endpoints/control/controlThrow.js +21 -20
  31. package/dist/routes/endpoints/control/controlTry.js +20 -10
  32. package/dist/routes/endpoints/evaluateRoutineOperators.js +26 -0
  33. package/dist/routes/endpoints/findSchedule.js +35 -0
  34. package/dist/routes/endpoints/forwardScheduledEndpoint.js +111 -0
  35. package/dist/routes/endpoints/getEndpointConfig.js +9 -3
  36. package/dist/routes/endpoints/getEnvironmentSchedules.js +27 -0
  37. package/dist/routes/endpoints/handleAgentCall.js +80 -0
  38. package/dist/routes/endpoints/handleEndpointCall.js +53 -8
  39. package/dist/routes/endpoints/handleRenderNotification.js +196 -0
  40. package/dist/routes/endpoints/handleRequest.js +1 -5
  41. package/dist/routes/endpoints/handleValidateSchema.js +14 -10
  42. package/dist/routes/endpoints/invokeEndpoint.js +1 -0
  43. package/dist/routes/endpoints/isUnauthenticatedHuman.js +31 -0
  44. package/dist/routes/endpoints/operatorScope.js +27 -0
  45. package/dist/routes/endpoints/resolveCronEnvironment.js +38 -0
  46. package/dist/routes/endpoints/runDetachedEndpoint.js +67 -0
  47. package/dist/routes/endpoints/runRoutine.js +12 -0
  48. package/dist/routes/endpoints/runScheduledEndpoint.js +105 -0
  49. package/dist/routes/endpoints/runWebhookEndpoint.js +84 -0
  50. package/dist/routes/endpoints/scheduleBackground.js +48 -0
  51. package/dist/routes/endpoints/test/runTest.js +17 -2
  52. package/dist/routes/mcp/createMcpServer.js +178 -0
  53. package/dist/routes/notifications/derivePreview.js +30 -0
  54. package/dist/routes/notifications/getNotificationConfig.js +32 -0
  55. package/dist/routes/notifications/resolveNotificationLinks.js +72 -0
  56. package/dist/routes/notifications/resolveThemeLogo.js +35 -0
  57. package/dist/routes/page/dynamic/resolveDynamicContent.js +172 -0
  58. package/dist/routes/page/dynamic/unescapeOperators.js +36 -0
  59. package/dist/routes/page/dynamic/validateFragment.js +104 -0
  60. package/dist/routes/page/getPageConfig.js +16 -6
  61. package/dist/routes/request/callRequest.js +9 -3
  62. package/dist/routes/request/callRequestResolver.js +6 -0
  63. package/dist/routes/request/evaluateOperators.js +8 -12
  64. package/dist/routes/websocket/authorizeWebsocket.js +27 -0
  65. package/dist/routes/websocket/createChannelRegistry.js +283 -0
  66. package/dist/routes/websocket/createWebSocketConnection.js +143 -0
  67. package/dist/routes/websocket/getWebsocketConfig.js +30 -0
  68. package/dist/routes/websocket/getWebsocketResolver.js +33 -0
  69. package/dist/routes/websocket/prepareChannel.js +72 -0
  70. package/dist/test/testContext.js +7 -3
  71. package/package.json +12 -10
  72. 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, evaluateOperators } = context;
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 = evaluateOperators({
24
+ const log = evaluateRoutineOperators(context, routineContext, {
25
25
  input: control[':log'],
26
- items,
27
- location,
28
- payload: routineContext.payload,
29
- state: routineContext.state,
30
- steps: routineContext.steps
26
+ location
31
27
  });
32
- const logLevel = evaluateOperators({
28
+ const logLevel = evaluateRoutineOperators(context, routineContext, {
33
29
  input: control[':level'],
34
- items,
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, evaluateOperators } = context;
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 = evaluateOperators({
25
+ const array = evaluateRoutineOperators(context, routineContext, {
25
26
  input: control[':in'],
26
- items,
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 = evaluateOperators({
21
+ const message = evaluateRoutineOperators(context, routineContext, {
21
22
  input: control[':reject'],
22
- items,
23
- location,
24
- payload: routineContext.payload,
25
- state: routineContext.state,
26
- steps: routineContext.steps
23
+ location
27
24
  });
28
- const cause = evaluateOperators({
25
+ const cause = evaluateRoutineOperators(context, routineContext, {
29
26
  input: control[':cause'],
30
- items,
31
- location,
32
- payload: routineContext.payload,
33
- state: routineContext.state,
34
- steps: routineContext.steps
27
+ location
35
28
  });
36
- const error = new UserError(message, {
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
- */ async function controlReturn(context, routineContext, { control }) {
16
- const { evaluateOperators } = context;
17
- const { items } = routineContext;
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
- items,
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, evaluateOperators } = context;
19
- const { items } = routineContext;
20
- const evaluatedSetState = evaluateOperators({
19
+ const { logger } = context;
20
+ const evaluatedSetState = evaluateRoutineOperators(context, routineContext, {
21
21
  input: control[':set_state'],
22
- items,
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, evaluateOperators } = context;
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 = evaluateOperators({
24
+ const evaluatedCase = evaluateRoutineOperators(context, routineContext, {
25
25
  input: caseObj[':case'],
26
- items,
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 = evaluateOperators({
20
+ const message = evaluateRoutineOperators(context, routineContext, {
21
21
  input: control[':throw'],
22
- items,
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 runRoutine from '../runRoutine.js';
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
- if (control[':catch']) {
25
- context.logger.debug({
26
- event: 'debug_control_catch'
27
- });
28
- res = await runRoutine(context, routineContext, {
29
- routine: control[':catch']
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
- async function getEndpointConfig({ logger, readConfigFile }, { endpointId }) {
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;