@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.
- package/dist/build/buildAgents.js +61 -16
- package/dist/build/buildApi/buildApi.js +4 -1
- package/dist/build/buildApi/buildEndpoint.js +8 -2
- package/dist/build/buildApi/buildRoutine/countStepTypes.js +1 -1
- package/dist/build/buildApi/buildRoutine/setStepId.js +2 -0
- package/dist/build/buildApi/buildRoutine/validateStep.js +61 -0
- package/dist/build/buildApi/resolveEndpointSchedules.js +37 -0
- package/dist/build/buildApi/validateEndpoint.js +96 -1
- package/dist/build/buildApp.js +13 -20
- package/dist/build/buildAppMeta.js +130 -0
- package/dist/build/buildAuth/buildAgentAuth.js +14 -1
- package/dist/build/buildAuth/buildAuth.js +8 -3
- package/dist/build/buildAuth/buildAuthPlugins.js +6 -0
- package/dist/build/buildAuth/buildAuthStrategies.js +117 -0
- package/dist/build/buildAuth/buildWebsocketAuth.js +55 -0
- package/dist/build/buildAuth/getProtectedWebsockets.js +29 -0
- package/dist/build/buildAuth/getWebsocketRoles.js +39 -0
- package/dist/build/buildAuth/validateAuthConfig.js +15 -3
- package/dist/build/buildImports/buildImportsDev.js +15 -0
- package/dist/build/buildImports/buildImportsProd.js +4 -1
- package/dist/build/buildJs/generateClientJsModule.js +27 -0
- package/dist/build/buildJs/writeJs.js +3 -5
- package/dist/build/buildMcp.js +73 -0
- package/dist/build/buildModuleDefs.js +90 -48
- package/dist/build/buildModules.js +61 -14
- package/dist/build/buildNotifications.js +98 -0
- package/dist/build/buildPages/buildBlock/buildBlock.js +3 -1
- package/dist/build/buildPages/buildBlock/buildDynamicBlock.js +131 -0
- package/dist/build/buildPages/buildBlock/buildEvents.js +186 -21
- package/dist/build/buildPages/buildBlock/buildRequests.js +9 -0
- package/dist/build/buildPages/buildBlock/normalizeClassAndStyles.js +2 -11
- package/dist/build/buildPages/buildBlock/setBlockId.js +5 -2
- package/dist/build/buildPages/buildBlock/validateBlock.js +7 -0
- package/dist/build/buildPages/buildPage.js +27 -3
- package/dist/build/buildPages/buildSubscriptions.js +145 -0
- package/dist/build/buildPages/validateDynamicBlockRefs.js +30 -0
- package/dist/build/buildPages/validateStateReferences.js +4 -2
- package/dist/build/buildPages/validateWebsocketRefs.js +30 -0
- package/dist/build/buildRefs/buildRefs.js +21 -12
- package/dist/build/buildRefs/cloneWithMarkers.js +57 -0
- package/dist/build/buildRefs/deferredRegions.js +80 -0
- package/dist/build/buildRefs/deferredRegistry.js +292 -0
- package/dist/build/buildRefs/getModuleRefContent.js +21 -6
- package/dist/build/buildRefs/getUserJavascriptFunction.js +1 -1
- package/dist/build/buildRefs/parseRefContent.js +24 -0
- package/dist/build/buildRefs/{evaluateStaticOperators.js → precomputeRuntimeOperators.js} +7 -6
- package/dist/build/buildRefs/walker.js +471 -207
- package/dist/build/buildTypes.js +22 -1
- package/dist/build/buildWebsockets.js +99 -0
- package/dist/build/collectPageContent.js +7 -1
- package/dist/build/copyAgentFileSystems.js +2 -2
- package/dist/build/full/buildPages.js +16 -0
- package/dist/build/full/updateServerPackageJson.js +21 -0
- package/dist/build/jit/buildPageJit.js +46 -6
- package/dist/build/jit/collectSkeletonSourceFiles.js +14 -0
- package/dist/build/jit/pageContentKeys.js +1 -0
- package/dist/build/jit/shallowBuild.js +81 -1
- package/dist/build/jit/writePageJit.js +11 -8
- package/dist/build/registerModules.js +125 -24
- package/dist/build/validateCallAgentSteps.js +58 -0
- package/dist/build/validateConfig.js +4 -0
- package/dist/build/validateCronConfig.js +110 -0
- package/dist/build/validateRenderNotificationSteps.js +50 -0
- package/dist/build/writeApi.js +58 -1
- package/dist/build/writeMcp.js +21 -0
- package/dist/build/writeNotifications.js +30 -0
- package/dist/build/writePluginImports/importPluginModule.js +39 -0
- package/dist/build/writePluginImports/writeActionSchemaMap.js +6 -7
- package/dist/build/writePluginImports/writeAuthImports.js +4 -0
- package/dist/build/writePluginImports/writeAvailableTypes.js +99 -0
- package/dist/build/writePluginImports/writeBlockSchemaMap.js +10 -9
- package/dist/build/writePluginImports/writeConnectionSchemaMap.js +101 -0
- package/dist/build/writePluginImports/writeGlobalsCss.js +20 -10
- package/dist/build/writePluginImports/writeNotificationImports.js +31 -0
- package/dist/build/writePluginImports/writeOperatorSchemaMap.js +6 -7
- package/dist/build/writePluginImports/writePluginImports.js +18 -4
- package/dist/build/writePluginImports/writeWebsocketImports.js +22 -0
- package/dist/build/writeWebsockets.js +30 -0
- package/dist/createContext.js +6 -1
- package/dist/defaultPackages.js +7 -1
- package/dist/defaultTypesMap.js +666 -388
- package/dist/dynamic/buildDynamicBlocks.js +120 -0
- package/dist/index.js +78 -8
- package/dist/indexDev.js +4 -0
- package/dist/lowdefySchema.js +861 -6
- package/dist/scripts/generateDefaultTypes.js +5 -2
- package/dist/test-utils/expectTerminates.js +33 -0
- package/dist/test-utils/runBuild.js +12 -1
- package/dist/test-utils/runBuildForSnapshots.js +34 -4
- package/dist/test-utils/testContext.js +8 -2
- package/dist/utils/countOperators.js +5 -17
- package/dist/utils/createPluginTypesMap.js +21 -0
- package/dist/utils/getCronEnvironmentNames.js +25 -0
- package/dist/utils/logCollectedErrors.js +15 -9
- package/dist/utils/serializeBuildException.js +29 -0
- package/dist/utils/validateCronExpression.js +131 -0
- package/dist/utils/writeBuildArtifact.js +5 -2
- package/package.json +66 -59
- package/dist/build/writePluginImports/writeServerExternalPackages.js +0 -121
- /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
|
-
//
|
|
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 (
|
|
107
|
-
|
|
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
|
-
|
|
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;
|
package/dist/build/buildApp.js
CHANGED
|
@@ -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 {
|
|
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.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
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
|
-
|
|
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;
|