@lowdefy/build 5.6.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/dist/build/buildAgents.js +61 -16
  2. package/dist/build/buildApi/buildApi.js +4 -1
  3. package/dist/build/buildApi/buildEndpoint.js +8 -2
  4. package/dist/build/buildApi/buildRoutine/countStepTypes.js +1 -1
  5. package/dist/build/buildApi/buildRoutine/setStepId.js +2 -0
  6. package/dist/build/buildApi/buildRoutine/validateStep.js +61 -0
  7. package/dist/build/buildApi/resolveEndpointSchedules.js +37 -0
  8. package/dist/build/buildApi/validateEndpoint.js +96 -1
  9. package/dist/build/buildApp.js +13 -20
  10. package/dist/build/buildAppMeta.js +130 -0
  11. package/dist/build/buildAuth/buildAgentAuth.js +14 -1
  12. package/dist/build/buildAuth/buildAuth.js +8 -3
  13. package/dist/build/buildAuth/buildAuthPlugins.js +6 -0
  14. package/dist/build/buildAuth/buildAuthStrategies.js +117 -0
  15. package/dist/build/buildAuth/buildWebsocketAuth.js +55 -0
  16. package/dist/build/buildAuth/getProtectedWebsockets.js +29 -0
  17. package/dist/build/buildAuth/getWebsocketRoles.js +39 -0
  18. package/dist/build/buildAuth/validateAuthConfig.js +15 -3
  19. package/dist/build/buildImports/buildImportsDev.js +15 -0
  20. package/dist/build/buildImports/buildImportsProd.js +4 -1
  21. package/dist/build/buildJs/generateClientJsModule.js +27 -0
  22. package/dist/build/buildJs/writeJs.js +3 -5
  23. package/dist/build/buildMcp.js +73 -0
  24. package/dist/build/buildModuleDefs.js +90 -48
  25. package/dist/build/buildModules.js +61 -14
  26. package/dist/build/buildNotifications.js +98 -0
  27. package/dist/build/buildPages/buildBlock/buildBlock.js +3 -1
  28. package/dist/build/buildPages/buildBlock/buildDynamicBlock.js +131 -0
  29. package/dist/build/buildPages/buildBlock/buildEvents.js +186 -21
  30. package/dist/build/buildPages/buildBlock/buildRequests.js +9 -0
  31. package/dist/build/buildPages/buildBlock/normalizeClassAndStyles.js +2 -11
  32. package/dist/build/buildPages/buildBlock/setBlockId.js +5 -2
  33. package/dist/build/buildPages/buildBlock/validateBlock.js +7 -0
  34. package/dist/build/buildPages/buildPage.js +27 -3
  35. package/dist/build/buildPages/buildSubscriptions.js +145 -0
  36. package/dist/build/buildPages/validateDynamicBlockRefs.js +30 -0
  37. package/dist/build/buildPages/validateStateReferences.js +4 -2
  38. package/dist/build/buildPages/validateWebsocketRefs.js +30 -0
  39. package/dist/build/buildRefs/buildRefs.js +21 -12
  40. package/dist/build/buildRefs/cloneWithMarkers.js +57 -0
  41. package/dist/build/buildRefs/deferredRegions.js +80 -0
  42. package/dist/build/buildRefs/deferredRegistry.js +292 -0
  43. package/dist/build/buildRefs/getModuleRefContent.js +21 -6
  44. package/dist/build/buildRefs/getUserJavascriptFunction.js +1 -1
  45. package/dist/build/buildRefs/parseRefContent.js +24 -0
  46. package/dist/build/buildRefs/{evaluateStaticOperators.js → precomputeRuntimeOperators.js} +7 -6
  47. package/dist/build/buildRefs/walker.js +471 -207
  48. package/dist/build/buildTypes.js +22 -1
  49. package/dist/build/buildWebsockets.js +99 -0
  50. package/dist/build/collectPageContent.js +7 -1
  51. package/dist/build/copyAgentFileSystems.js +2 -2
  52. package/dist/build/full/buildPages.js +16 -0
  53. package/dist/build/full/updateServerPackageJson.js +21 -0
  54. package/dist/build/jit/buildPageJit.js +46 -6
  55. package/dist/build/jit/collectSkeletonSourceFiles.js +14 -0
  56. package/dist/build/jit/pageContentKeys.js +1 -0
  57. package/dist/build/jit/shallowBuild.js +81 -1
  58. package/dist/build/jit/writePageJit.js +11 -8
  59. package/dist/build/registerModules.js +125 -24
  60. package/dist/build/validateCallAgentSteps.js +58 -0
  61. package/dist/build/validateConfig.js +4 -0
  62. package/dist/build/validateCronConfig.js +110 -0
  63. package/dist/build/validateRenderNotificationSteps.js +50 -0
  64. package/dist/build/writeApi.js +58 -1
  65. package/dist/build/writeMcp.js +21 -0
  66. package/dist/build/writeNotifications.js +30 -0
  67. package/dist/build/writePluginImports/importPluginModule.js +39 -0
  68. package/dist/build/writePluginImports/writeActionSchemaMap.js +6 -7
  69. package/dist/build/writePluginImports/writeAuthImports.js +4 -0
  70. package/dist/build/writePluginImports/writeAvailableTypes.js +99 -0
  71. package/dist/build/writePluginImports/writeBlockSchemaMap.js +10 -9
  72. package/dist/build/writePluginImports/writeConnectionSchemaMap.js +101 -0
  73. package/dist/build/writePluginImports/writeGlobalsCss.js +20 -10
  74. package/dist/build/writePluginImports/writeNotificationImports.js +31 -0
  75. package/dist/build/writePluginImports/writeOperatorSchemaMap.js +6 -7
  76. package/dist/build/writePluginImports/writePluginImports.js +18 -4
  77. package/dist/build/writePluginImports/writeWebsocketImports.js +22 -0
  78. package/dist/build/writeWebsockets.js +30 -0
  79. package/dist/createContext.js +6 -1
  80. package/dist/defaultPackages.js +7 -1
  81. package/dist/defaultTypesMap.js +666 -388
  82. package/dist/dynamic/buildDynamicBlocks.js +120 -0
  83. package/dist/index.js +78 -8
  84. package/dist/indexDev.js +4 -0
  85. package/dist/lowdefySchema.js +861 -6
  86. package/dist/scripts/generateDefaultTypes.js +5 -2
  87. package/dist/test-utils/expectTerminates.js +33 -0
  88. package/dist/test-utils/runBuild.js +12 -1
  89. package/dist/test-utils/runBuildForSnapshots.js +34 -4
  90. package/dist/test-utils/testContext.js +8 -2
  91. package/dist/utils/countOperators.js +5 -17
  92. package/dist/utils/createPluginTypesMap.js +21 -0
  93. package/dist/utils/getCronEnvironmentNames.js +25 -0
  94. package/dist/utils/logCollectedErrors.js +15 -9
  95. package/dist/utils/serializeBuildException.js +29 -0
  96. package/dist/utils/validateCronExpression.js +131 -0
  97. package/dist/utils/writeBuildArtifact.js +5 -2
  98. package/package.json +66 -59
  99. package/dist/build/writePluginImports/writeServerExternalPackages.js +0 -121
  100. /package/dist/build/{resolveModuleOperators.js → buildRefs/scopeMenuItemIds.js} +0 -0
@@ -19,6 +19,8 @@ import { ConfigError, ConfigWarning } from '@lowdefy/errors';
19
19
  import { RESERVED_PLATFORM_TOOL_NAMES } from '@lowdefy/ai-utils';
20
20
  import countOperators from '../utils/countOperators.js';
21
21
  import createCheckDuplicateId from '../utils/createCheckDuplicateId.js';
22
+ // Provider tool-name rule (Anthropic and OpenAI both).
23
+ const TOOL_NAME_REGEX = /^[a-zA-Z0-9_-]{1,64}$/;
22
24
  function detectCycles(agents) {
23
25
  const graph = {};
24
26
  for (const agent of agents){
@@ -101,13 +103,40 @@ function buildAgents({ components, context }) {
101
103
  }
102
104
  return tool;
103
105
  });
104
- // Validate tools reference existing API endpoints with required tool metadata
106
+ // Assign LLM-safe tool names. Providers require ^[a-zA-Z0-9_-]{1,64}$ —
107
+ // module-scoped endpoint ids contain '/', so the default name replaces
108
+ // '/' with '__'; config may override with an explicit "name".
109
+ const toolNames = new Set();
105
110
  agent.tools.forEach((toolConfig)=>{
106
- if (RESERVED_PLATFORM_TOOL_NAMES.includes(toolConfig.endpointId)) {
107
- throw new ConfigError(`Agent "${agent.id}" tool "${toolConfig.endpointId}" uses a reserved platform tool name. Reserved: ${RESERVED_PLATFORM_TOOL_NAMES.join(', ')}.`, {
111
+ if (type.isNone(toolConfig.name)) {
112
+ toolConfig.name = toolConfig.endpointId.replaceAll('/', '__');
113
+ }
114
+ if (!TOOL_NAME_REGEX.test(toolConfig.name)) {
115
+ throw new ConfigError(`Agent "${agent.id}" tool name "${toolConfig.name}" is invalid. Tool names must match ^[a-zA-Z0-9_-]{1,64}$ — set "name" on the tool to override the default derived from the endpoint id.`, {
116
+ configKey
117
+ });
118
+ }
119
+ // TOOL_NAME_REGEX admits every reserved name - letters and underscores are
120
+ // in its allowed set - and tool names key the plain-object tools map.
121
+ if (isReserved(toolConfig.name)) {
122
+ throw new ConfigError(`Agent "${agent.id}" tool name "${toolConfig.name}" is a reserved name and cannot be used as a tool name.`, {
108
123
  configKey
109
124
  });
110
125
  }
126
+ if (RESERVED_PLATFORM_TOOL_NAMES.includes(toolConfig.name)) {
127
+ throw new ConfigError(`Agent "${agent.id}" tool "${toolConfig.name}" uses a reserved platform tool name. Reserved: ${RESERVED_PLATFORM_TOOL_NAMES.join(', ')}.`, {
128
+ configKey
129
+ });
130
+ }
131
+ if (toolNames.has(toolConfig.name)) {
132
+ throw new ConfigError(`Agent "${agent.id}" has duplicate tool name "${toolConfig.name}".`, {
133
+ configKey
134
+ });
135
+ }
136
+ toolNames.add(toolConfig.name);
137
+ });
138
+ // Validate tools reference existing API endpoints with required tool metadata
139
+ agent.tools.forEach((toolConfig)=>{
111
140
  const endpoint = (components.api ?? []).find((e)=>e.id === toolConfig.endpointId || e.endpointId === toolConfig.endpointId);
112
141
  if (!endpoint) {
113
142
  throw new ConfigError(`Agent "${agent.id}" references tool endpoint "${toolConfig.endpointId}" which does not exist.`, {
@@ -186,6 +215,35 @@ function buildAgents({ components, context }) {
186
215
  }
187
216
  return ref;
188
217
  });
218
+ // Sub-agents surface to the model as tools too — same naming rule.
219
+ agent.agents.forEach((ref)=>{
220
+ if (type.isNone(ref.name)) {
221
+ ref.name = ref.agentId.replaceAll('/', '__');
222
+ }
223
+ if (!TOOL_NAME_REGEX.test(ref.name)) {
224
+ throw new ConfigError(`Agent "${agent.id}" sub-agent tool name "${ref.name}" is invalid. Tool names must match ^[a-zA-Z0-9_-]{1,64}$ — set "name" on the sub-agent reference to override the default derived from the agent id.`, {
225
+ configKey
226
+ });
227
+ }
228
+ // Runs on the derived name, so "a/__proto__" -> "a____proto__" is accepted
229
+ // while an explicit reserved "name" is not.
230
+ if (isReserved(ref.name)) {
231
+ throw new ConfigError(`Agent "${agent.id}" sub-agent tool name "${ref.name}" is a reserved name and cannot be used as a tool name.`, {
232
+ configKey
233
+ });
234
+ }
235
+ if (RESERVED_PLATFORM_TOOL_NAMES.includes(ref.name)) {
236
+ throw new ConfigError(`Agent "${agent.id}" sub-agent "${ref.agentId}" uses a reserved platform tool name. Reserved: ${RESERVED_PLATFORM_TOOL_NAMES.join(', ')}.`, {
237
+ configKey
238
+ });
239
+ }
240
+ if (toolNames.has(ref.name)) {
241
+ throw new ConfigError(`Agent "${agent.id}" sub-agent "${ref.agentId}" conflicts with an endpoint tool of the same name.`, {
242
+ configKey
243
+ });
244
+ }
245
+ toolNames.add(ref.name);
246
+ });
189
247
  // Validate fileSystem basePath if present
190
248
  if (agent.properties?.fileSystem) {
191
249
  const basePath = agent.properties.fileSystem.basePath;
@@ -221,19 +279,6 @@ function buildAgents({ components, context }) {
221
279
  configKey
222
280
  });
223
281
  }
224
- // Reserved platform tool name guard for sub-agents
225
- if (RESERVED_PLATFORM_TOOL_NAMES.includes(subAgentRef.agentId)) {
226
- throw new ConfigError(`Agent "${agent.agentId}" sub-agent "${subAgentRef.agentId}" uses a reserved platform tool name. Reserved: ${RESERVED_PLATFORM_TOOL_NAMES.join(', ')}.`, {
227
- configKey
228
- });
229
- }
230
- // Check for name collision with endpoint tools
231
- const hasToolCollision = agent.tools.some((toolConfig)=>toolConfig.endpointId === subAgentRef.agentId);
232
- if (hasToolCollision) {
233
- throw new ConfigError(`Agent "${agent.agentId}" sub-agent "${subAgentRef.agentId}" conflicts with an endpoint tool of the same name.`, {
234
- configKey
235
- });
236
- }
237
282
  // Warn if sub-agent has tools with confirm: true (unsupported in sub-agent context)
238
283
  const subAgent = components.agents.find((a)=>a.agentId === subAgentRef.agentId);
239
284
  const hasConfirmTools = (subAgent?.tools ?? []).some((t)=>t.confirm);
@@ -26,6 +26,8 @@ function buildApi({ components, context }) {
26
26
  const checkDuplicateEndpointId = createCheckDuplicateId({
27
27
  message: 'Duplicate endpointId "{{ id }}".'
28
28
  });
29
+ // Validated by validateConfig, which runs before buildApi.
30
+ const cronEnvironments = components.config?.cron?.environments;
29
31
  // Wrap each endpoint build to collect errors instead of stopping on first error
30
32
  api.forEach((endpoint, index)=>{
31
33
  try {
@@ -33,7 +35,8 @@ function buildApi({ components, context }) {
33
35
  endpoint,
34
36
  index,
35
37
  context,
36
- checkDuplicateEndpointId
38
+ checkDuplicateEndpointId,
39
+ cronEnvironments
37
40
  });
38
41
  } catch (error) {
39
42
  // Skip suppressed ConfigErrors (via ~ignoreBuildChecks)
@@ -13,15 +13,21 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import buildRoutine from './buildRoutine/buildRoutine.js';
16
+ import resolveEndpointSchedules from './resolveEndpointSchedules.js';
16
17
  import validateEndpoint from './validateEndpoint.js';
17
18
  import validateStepReferences from './validateStepReferences.js';
18
- function buildEndpoint({ endpoint, index, context, checkDuplicateEndpointId }) {
19
+ function buildEndpoint({ endpoint, index, context, checkDuplicateEndpointId, cronEnvironments }) {
19
20
  validateEndpoint({
20
21
  endpoint,
21
22
  index,
22
- checkDuplicateEndpointId
23
+ checkDuplicateEndpointId,
24
+ cronEnvironments
23
25
  });
24
26
  endpoint.endpointId = endpoint.id;
27
+ resolveEndpointSchedules({
28
+ endpoint,
29
+ cronEnvironments
30
+ });
25
31
  buildRoutine(endpoint.routine, {
26
32
  endpointId: endpoint.endpointId,
27
33
  typeCounters: context.typeCounters
@@ -13,7 +13,7 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ function countStepTypes(step, { typeCounters }) {
16
- if (step.type === 'CallApi' || step.type === 'ValidateSchema') {
16
+ if (step.type === 'CallAgent' || step.type === 'CallApi' || step.type === 'RenderNotification' || step.type === 'ValidateSchema') {
17
17
  return;
18
18
  }
19
19
  typeCounters.requests.increment(step.type, step['~k']);
@@ -13,7 +13,9 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ const prefixByType = {
16
+ CallAgent: 'agent',
16
17
  CallApi: 'endpoint',
18
+ RenderNotification: 'notification',
17
19
  ValidateSchema: 'validate'
18
20
  };
19
21
  function setStepId(step, { endpointId }) {
@@ -69,6 +69,67 @@ function validateStep(step, { endpointId }) {
69
69
  }
70
70
  return;
71
71
  }
72
+ if (step.type === 'CallAgent') {
73
+ if (type.isNone(step.properties?.agentId)) {
74
+ throw new ConfigError(`CallAgent step "${step.id}" at endpoint "${endpointId}" requires properties.agentId.`, {
75
+ configKey
76
+ });
77
+ }
78
+ if (!type.isString(step.properties.agentId) && !type.isObject(step.properties.agentId)) {
79
+ throw new ConfigError(`CallAgent step "${step.id}" at endpoint "${endpointId}" properties.agentId is not a string.`, {
80
+ received: step.properties.agentId,
81
+ configKey
82
+ });
83
+ }
84
+ if (type.isNone(step.properties?.prompt)) {
85
+ throw new ConfigError(`CallAgent step "${step.id}" at endpoint "${endpointId}" requires properties.prompt.`, {
86
+ configKey
87
+ });
88
+ }
89
+ if (!type.isString(step.properties.prompt) && !type.isObject(step.properties.prompt)) {
90
+ throw new ConfigError(`CallAgent step "${step.id}" at endpoint "${endpointId}" properties.prompt is not a string.`, {
91
+ received: step.properties.prompt,
92
+ configKey
93
+ });
94
+ }
95
+ if (!type.isNone(step.connectionId)) {
96
+ throw new ConfigError(`CallAgent step "${step.id}" at endpoint "${endpointId}" should not have a connectionId.`, {
97
+ configKey
98
+ });
99
+ }
100
+ return;
101
+ }
102
+ if (step.type === 'RenderNotification') {
103
+ if (type.isNone(step.properties?.notificationId)) {
104
+ throw new ConfigError(`RenderNotification step "${step.id}" at endpoint "${endpointId}" requires properties.notificationId.`, {
105
+ configKey
106
+ });
107
+ }
108
+ if (!type.isString(step.properties.notificationId) && !type.isObject(step.properties.notificationId)) {
109
+ throw new ConfigError(`RenderNotification step "${step.id}" at endpoint "${endpointId}" properties.notificationId is not a string.`, {
110
+ received: step.properties.notificationId,
111
+ configKey
112
+ });
113
+ }
114
+ if (type.isNone(step.properties?.data)) {
115
+ throw new ConfigError(`RenderNotification step "${step.id}" at endpoint "${endpointId}" requires properties.data.`, {
116
+ configKey
117
+ });
118
+ }
119
+ // One item per render — arrays are iterated with a :for control in the routine.
120
+ if (!type.isObject(step.properties.data)) {
121
+ throw new ConfigError(`RenderNotification step "${step.id}" at endpoint "${endpointId}" properties.data is not an object.`, {
122
+ received: step.properties.data,
123
+ configKey
124
+ });
125
+ }
126
+ if (!type.isNone(step.connectionId)) {
127
+ throw new ConfigError(`RenderNotification step "${step.id}" at endpoint "${endpointId}" should not have a connectionId.`, {
128
+ configKey
129
+ });
130
+ }
131
+ return;
132
+ }
72
133
  if (step.type === 'ValidateSchema') {
73
134
  if (type.isNone(step.properties?.schema)) {
74
135
  throw new ConfigError(`ValidateSchema step "${step.id}" at endpoint "${endpointId}" requires properties.schema.`, {
@@ -0,0 +1,37 @@
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 getCronEnvironmentNames from '../../utils/getCronEnvironmentNames.js';
17
+ // With config.cron.environments declared, the artifact carries `schedules` as an object with one
18
+ // explicit list per declared environment: the environment's own entry when the author keyed the
19
+ // schedules by environment, else the `default` entry, else nothing (`[]` turns the crons off). An
20
+ // array applies as-is to every environment. The runtime and the schedules manifest then read
21
+ // `schedules.<environment>` without repeating the inheritance. Without config.cron the endpoint
22
+ // keeps its array.
23
+ function resolveEndpointSchedules({ endpoint, cronEnvironments }) {
24
+ const names = getCronEnvironmentNames(cronEnvironments);
25
+ if (names.length === 0 || type.isUndefined(endpoint.schedules)) return;
26
+ const authored = endpoint.schedules;
27
+ const resolved = {};
28
+ names.forEach((name)=>{
29
+ if (type.isArray(authored)) {
30
+ resolved[name] = authored;
31
+ return;
32
+ }
33
+ resolved[name] = authored[name] ?? authored.default ?? [];
34
+ });
35
+ endpoint.schedules = resolved;
36
+ }
37
+ export default resolveEndpointSchedules;
@@ -15,7 +15,89 @@
15
15
  */ import { type } from '@lowdefy/helpers';
16
16
  import { ConfigError } from '@lowdefy/errors';
17
17
  import validateId from '../../utils/validateId.js';
18
- function validateEndpoint({ endpoint, index, checkDuplicateEndpointId }) {
18
+ import validateCronExpression from '../../utils/validateCronExpression.js';
19
+ import getCronEnvironmentNames from '../../utils/getCronEnvironmentNames.js';
20
+ // `where` names the schedules list in messages: "" for the endpoint's own schedules, or
21
+ // ` for environment "staging"` for an override.
22
+ function validateSchedules({ schedules, endpoint, configKey, where = '' }) {
23
+ if (type.isUndefined(schedules)) return;
24
+ if (!type.isArray(schedules)) {
25
+ throw new ConfigError(`Endpoint schedules${where} is not an array${where ? '' : ' or an object keyed by environment'} at "${endpoint.id}".`, {
26
+ received: schedules,
27
+ configKey
28
+ });
29
+ }
30
+ const seenCrons = new Set();
31
+ schedules.forEach((schedule, scheduleIndex)=>{
32
+ if (!type.isObject(schedule)) {
33
+ throw new ConfigError(`Endpoint schedule ${scheduleIndex}${where} is not an object at "${endpoint.id}".`, {
34
+ received: schedule,
35
+ configKey
36
+ });
37
+ }
38
+ if (!type.isString(schedule.cron)) {
39
+ throw new ConfigError(`Endpoint schedule ${scheduleIndex}${where} cron is not a string at "${endpoint.id}".`, {
40
+ received: schedule.cron,
41
+ configKey
42
+ });
43
+ }
44
+ const reason = validateCronExpression(schedule.cron);
45
+ if (reason) {
46
+ throw new ConfigError(`Endpoint schedule ${scheduleIndex}${where} cron "${schedule.cron}" is invalid at "${endpoint.id}": ${reason}.`, {
47
+ received: schedule.cron,
48
+ configKey
49
+ });
50
+ }
51
+ // The x-vercel-cron-schedule header disambiguates which schedule fired at runtime, so an
52
+ // endpoint's cron expressions must be unique within one environment.
53
+ if (seenCrons.has(schedule.cron)) {
54
+ throw new ConfigError(`Endpoint schedule ${scheduleIndex}${where} has duplicate cron "${schedule.cron}" at "${endpoint.id}".`, {
55
+ received: schedule.cron,
56
+ configKey
57
+ });
58
+ }
59
+ seenCrons.add(schedule.cron);
60
+ if (!type.isUndefined(schedule.payload) && !type.isObject(schedule.payload)) {
61
+ throw new ConfigError(`Endpoint schedule ${scheduleIndex}${where} payload is not an object at "${endpoint.id}".`, {
62
+ received: schedule.payload,
63
+ configKey
64
+ });
65
+ }
66
+ });
67
+ }
68
+ // The object form of `schedules`: lists keyed by environment name plus an optional `default` the
69
+ // other environments inherit. Every key must be declared in config.cron.environments, so a typo
70
+ // cannot silently leave an environment on the defaults.
71
+ function validateEnvironmentSchedules({ endpoint, configKey, cronEnvironments }) {
72
+ const declared = getCronEnvironmentNames(cronEnvironments);
73
+ if (declared.length === 0) {
74
+ throw new ConfigError(`Endpoint "${endpoint.id}" keys schedules by environment but lowdefy.config.cron.environments is not defined.`, {
75
+ configKey
76
+ });
77
+ }
78
+ getCronEnvironmentNames(endpoint.schedules).forEach((name)=>{
79
+ if (name !== 'default' && !declared.includes(name)) {
80
+ throw new ConfigError(`Endpoint schedules environment "${name}" at "${endpoint.id}" is not declared in lowdefy.config.cron.environments. Declared environments: ${declared.join(', ')}.`, {
81
+ configKey
82
+ });
83
+ }
84
+ const schedules = endpoint.schedules[name];
85
+ const where = ` for environment "${name}"`;
86
+ if (!type.isArray(schedules)) {
87
+ throw new ConfigError(`Endpoint schedules${where} is not an array at "${endpoint.id}".`, {
88
+ received: schedules,
89
+ configKey
90
+ });
91
+ }
92
+ validateSchedules({
93
+ schedules,
94
+ endpoint,
95
+ configKey,
96
+ where
97
+ });
98
+ });
99
+ }
100
+ function validateEndpoint({ endpoint, index, checkDuplicateEndpointId, cronEnvironments }) {
19
101
  const configKey = endpoint['~k'];
20
102
  if (type.isUndefined(endpoint.id)) {
21
103
  throw new ConfigError(`Endpoint id missing at endpoint ${index}.`, {
@@ -58,5 +140,18 @@ function validateEndpoint({ endpoint, index, checkDuplicateEndpointId }) {
58
140
  id: endpoint.id,
59
141
  configKey
60
142
  });
143
+ if (type.isObject(endpoint.schedules)) {
144
+ validateEnvironmentSchedules({
145
+ endpoint,
146
+ configKey,
147
+ cronEnvironments
148
+ });
149
+ } else {
150
+ validateSchedules({
151
+ schedules: endpoint.schedules,
152
+ endpoint,
153
+ configKey
154
+ });
155
+ }
61
156
  }
62
157
  export default validateEndpoint;
@@ -12,17 +12,7 @@
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 { execSync } from 'child_process';
16
- import { type } from '@lowdefy/helpers';
17
- function computeGitSha() {
18
- const fromEnv = process.env.LOWDEFY_GIT_SHA?.trim();
19
- if (fromEnv) return fromEnv;
20
- try {
21
- return execSync('git rev-parse HEAD').toString().trim();
22
- } catch (_) {
23
- return null;
24
- }
25
- }
15
+ */ import { type } from '@lowdefy/helpers';
26
16
  function buildApp({ components }) {
27
17
  if (type.isNone(components.app)) {
28
18
  components.app = {};
@@ -39,15 +29,18 @@ function buildApp({ components }) {
39
29
  if (type.isNone(components.app.html.appendHead)) {
40
30
  components.app.html.appendHead = '';
41
31
  }
42
- components.appMeta = {
43
- slug: components.slug ?? null,
44
- name: components.name ?? null,
45
- version: components.version ?? null,
46
- description: components.description ?? null,
47
- license: components.license ?? null,
48
- lowdefyVersion: components.lowdefy ?? null,
49
- gitSha: computeGitSha()
50
- };
32
+ if (type.isNone(components.app.email)) {
33
+ components.app.email = {};
34
+ }
35
+ // Email branding defaults from existing app config — baked into app.json at
36
+ // build so the runtime theme merge stays a plain shallow spread.
37
+ if (type.isNone(components.app.email.companyName) && type.isString(components.name)) {
38
+ components.app.email.companyName = components.name;
39
+ }
40
+ const colorPrimary = components.theme?.antd?.token?.colorPrimary;
41
+ if (type.isNone(components.app.email.primaryColor) && type.isString(colorPrimary)) {
42
+ components.app.email.primaryColor = colorPrimary;
43
+ }
51
44
  return components;
52
45
  }
53
46
  export default buildApp;
@@ -0,0 +1,130 @@
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 { execSync } from 'child_process';
16
+ import { ConfigError } from '@lowdefy/errors';
17
+ import { type } from '@lowdefy/helpers';
18
+ import { evaluateOperators } from '@lowdefy/operators';
19
+ import operators from '@lowdefy/operators-js/operators/build';
20
+ import collectDynamicIdentifiers from './collectDynamicIdentifiers.js';
21
+ import collectExceptions from '../utils/collectExceptions.js';
22
+ import getRefContent from './buildRefs/getRefContent.js';
23
+ const dynamicIdentifiers = collectDynamicIdentifiers({
24
+ operators
25
+ });
26
+ const ROOT_METADATA_FILE = 'lowdefy.yaml';
27
+ function computeGitSha() {
28
+ const fromEnv = process.env.LOWDEFY_GIT_SHA?.trim();
29
+ if (fromEnv) return fromEnv;
30
+ try {
31
+ return execSync('git rev-parse HEAD').toString().trim();
32
+ } catch (_) {
33
+ return null;
34
+ }
35
+ }
36
+ // Returns the single operator key of value (a non-tilde key starting with '_'),
37
+ // or null when value is not a single-operator object.
38
+ function operatorKey(value) {
39
+ if (!type.isObject(value)) return null;
40
+ const keys = Object.keys(value).filter((key)=>!key.startsWith('~'));
41
+ if (keys.length !== 1) return null;
42
+ return keys[0].startsWith('_') ? keys[0] : null;
43
+ }
44
+ function collectFieldError(context, message, lineNumber) {
45
+ const error = new ConfigError(message);
46
+ error.filePath = ROOT_METADATA_FILE;
47
+ if (!type.isNone(lineNumber)) {
48
+ error.lineNumber = lineNumber;
49
+ }
50
+ collectExceptions(context, error);
51
+ }
52
+ // Resolve a single root metadata scalar. Root metadata must be final before the
53
+ // walker runs (it is what _app / _build.app read), so it can only depend on
54
+ // values available now: literals and _build.* operators. _ref/_var/static-_
55
+ // operators resolve later in buildRefs — deriving metadata from them is circular.
56
+ function resolveField({ context, field, value }) {
57
+ if (type.isNone(value)) return null;
58
+ // _build.app in root metadata is self-referential: app metadata cannot read
59
+ // itself. Caught explicitly so every field reports clearly, not only `slug`.
60
+ // Only the top-level form is special-cased; a _build.app nested deeper (e.g.
61
+ // inside _build.string.concat) resolves against an undefined lowdefyApp —
62
+ // yielding null for non-slug fields, or the "slug required" throw for slug.
63
+ if (operatorKey(value) === '_build.app') {
64
+ collectFieldError(context, `Root metadata field \`${field}\` cannot use \`_build.app\` — app metadata cannot reference itself.`, value['~l']);
65
+ return null;
66
+ }
67
+ const { output, errors } = evaluateOperators({
68
+ input: value,
69
+ operators,
70
+ operatorPrefix: '_build.',
71
+ env: process.env,
72
+ dynamicIdentifiers
73
+ });
74
+ if (errors.length > 0) {
75
+ errors.forEach((error)=>{
76
+ error.filePath = ROOT_METADATA_FILE;
77
+ collectExceptions(context, error);
78
+ });
79
+ return null;
80
+ }
81
+ if (operatorKey(output)) {
82
+ collectFieldError(context, `Root metadata field \`${field}\` must be a literal or a \`_build.*\` operator; \`_ref\`/\`_var\`/\`_*\` operators are not supported here.`, value['~l']);
83
+ return null;
84
+ }
85
+ return output ?? null;
86
+ }
87
+ async function buildAppMeta({ context }) {
88
+ const content = await getRefContent({
89
+ context,
90
+ refDef: {
91
+ path: ROOT_METADATA_FILE
92
+ },
93
+ referencedFrom: null
94
+ }) ?? {};
95
+ context.appMeta = {
96
+ slug: resolveField({
97
+ context,
98
+ field: 'slug',
99
+ value: content.slug
100
+ }),
101
+ name: resolveField({
102
+ context,
103
+ field: 'name',
104
+ value: content.name
105
+ }),
106
+ version: resolveField({
107
+ context,
108
+ field: 'version',
109
+ value: content.version
110
+ }),
111
+ description: resolveField({
112
+ context,
113
+ field: 'description',
114
+ value: content.description
115
+ }),
116
+ license: resolveField({
117
+ context,
118
+ field: 'license',
119
+ value: content.license
120
+ }),
121
+ lowdefyVersion: resolveField({
122
+ context,
123
+ field: 'lowdefy',
124
+ value: content.lowdefy
125
+ }),
126
+ gitSha: computeGitSha()
127
+ };
128
+ return context.appMeta;
129
+ }
130
+ export default buildAppMeta;
@@ -12,13 +12,26 @@
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 { type } from '@lowdefy/helpers';
15
+ */ import { isReserved, type } from '@lowdefy/helpers';
16
16
  import { ConfigError } from '@lowdefy/errors';
17
17
  import getAgentRoles from './getAgentRoles.js';
18
18
  import getProtectedAgents from './getProtectedAgents.js';
19
19
  import { isInPatternList } from './matchPattern.js';
20
20
  // Agents are served from the API surface, so auth.api patterns match agent ids too.
21
21
  function buildAgentAuth({ components, context }) {
22
+ // buildAuth runs before buildAgents, so this is the first step to read an
23
+ // agent id - and both helpers below key plain objects on it. A reserved id
24
+ // resolves through Object.prototype: `agentRoles.__proto__` is truthy for
25
+ // every app, so the agent is stamped with Object.prototype as its roles and
26
+ // the whole build writes through the global prototype from there. Gate before
27
+ // either helper runs - buildAgents' own gate never gets the chance.
28
+ (components.agents ?? []).forEach((agent)=>{
29
+ if (isReserved(agent.id)) {
30
+ throw new ConfigError(`Agent id "${agent.id}" is a reserved name and cannot be used as an id.`, {
31
+ configKey: agent['~k']
32
+ });
33
+ }
34
+ });
22
35
  const protectedAgents = getProtectedAgents({
23
36
  components
24
37
  });
@@ -15,9 +15,10 @@
15
15
  */ import { type } from '@lowdefy/helpers';
16
16
  import buildAuthPlugins from './buildAuthPlugins.js';
17
17
  import buildAgentAuth from './buildAgentAuth.js';
18
+ import buildAuthStrategies from './buildAuthStrategies.js';
18
19
  import buildApiAuth from './buildApiAuth.js';
19
20
  import buildPageAuth from './buildPageAuth.js';
20
- import rejectReservedEntityIds from './rejectReservedEntityIds.js';
21
+ import buildWebsocketAuth from './buildWebsocketAuth.js';
21
22
  import validateAuthConfig from './validateAuthConfig.js';
22
23
  function buildAuth({ components, context }) {
23
24
  const configured = !type.isNone(components.auth);
@@ -25,14 +26,18 @@ function buildAuth({ components, context }) {
25
26
  components,
26
27
  context
27
28
  });
28
- rejectReservedEntityIds({
29
+ components.auth.configured = configured;
30
+ buildAuthStrategies({
29
31
  components
30
32
  });
31
- components.auth.configured = configured;
32
33
  buildApiAuth({
33
34
  components,
34
35
  context
35
36
  });
37
+ buildWebsocketAuth({
38
+ components,
39
+ context
40
+ });
36
41
  buildAgentAuth({
37
42
  components,
38
43
  context
@@ -96,5 +96,11 @@ function buildAuthPlugins({ components, context }) {
96
96
  typeClass: 'provider',
97
97
  context
98
98
  });
99
+ buildAuthPlugin({
100
+ counter: counters.strategies,
101
+ pluginConfig: authConfig.strategies,
102
+ typeClass: 'strategy',
103
+ context
104
+ });
99
105
  }
100
106
  export default buildAuthPlugins;