@lowdefy/build 5.1.0 → 5.3.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 +249 -0
- package/dist/build/buildApi/buildRoutine/countStepTypes.js +3 -0
- package/dist/build/buildApi/buildRoutine/setStepId.js +3 -2
- package/dist/build/buildApi/buildRoutine/validateStep.js +19 -0
- package/dist/build/buildApi/validateEndpoint.js +10 -0
- package/dist/build/buildApi/validateStepReferences.js +4 -4
- package/dist/build/buildAuth/buildApiAuth.js +2 -1
- package/dist/build/buildAuth/buildPageAuth.js +2 -1
- package/dist/build/buildAuth/getApiRoles.js +12 -6
- package/dist/build/buildAuth/getPageRoles.js +12 -6
- package/dist/build/buildAuth/getProtectedApi.js +3 -2
- package/dist/build/buildAuth/getProtectedPages.js +3 -2
- package/dist/build/buildAuth/matchPattern.js +22 -0
- package/dist/build/buildConnections.js +42 -4
- package/dist/build/buildImports/buildImportsDev.js +5 -0
- package/dist/build/buildImports/buildImportsProd.js +1 -0
- package/dist/build/buildJs/jsMapParser.js +25 -12
- package/dist/build/buildJs/writeJs.js +2 -2
- package/dist/build/buildMenu.js +41 -0
- package/dist/build/buildModuleDefs.js +97 -0
- package/dist/build/buildModules.js +96 -0
- package/dist/build/buildPages/buildBlock/buildBlock.js +2 -2
- package/dist/build/buildPages/buildBlock/buildEvents.js +16 -1
- package/dist/build/buildPages/buildBlock/buildSubBlocks.js +2 -1
- package/dist/build/buildPages/buildBlock/validateBlock.js +3 -3
- package/dist/build/buildPages/buildPage.js +1 -0
- package/dist/build/buildPages/validateCallApiRefs.js +31 -0
- package/dist/build/buildRefs/getModuleRefContent.js +81 -0
- package/dist/build/buildRefs/makeRefDefinition.js +6 -0
- package/dist/build/buildRefs/walker.js +424 -44
- package/dist/build/buildTypes.js +7 -0
- package/dist/build/copyAgentFileSystems.js +45 -0
- package/dist/build/fetchGitHubModule.js +94 -0
- package/dist/build/fetchModules.js +60 -0
- package/dist/build/full/buildPages.js +10 -1
- package/dist/build/full/updateServerPackageJson.js +1 -0
- package/dist/build/full/writePages.js +1 -1
- package/dist/build/jit/buildPageJit.js +34 -4
- package/dist/build/jit/collectSkeletonSourceFiles.js +8 -0
- package/dist/build/jit/createPageRegistry.js +10 -1
- package/dist/build/jit/shallowBuild.js +37 -11
- package/dist/build/jit/writePageJit.js +2 -2
- package/dist/build/jit/writeSourcelessPages.js +1 -1
- package/dist/build/parseModuleSource.js +48 -0
- package/dist/build/registerModules.js +247 -0
- package/dist/build/resolveDepTarget.js +43 -0
- package/dist/build/resolveModuleDependencies.js +60 -0
- package/dist/build/resolveModuleOperators.js +27 -0
- package/dist/build/testSchema.js +22 -11
- package/dist/build/writeAgents.js +26 -0
- package/dist/build/writePluginImports/writeAgentImports.js +22 -0
- package/dist/build/writePluginImports/writeGlobalsCss.js +1 -1
- package/dist/build/writePluginImports/writePluginImports.js +5 -0
- package/dist/createContext.js +6 -0
- package/dist/defaultPackages.js +58 -0
- package/dist/defaultTypesMap.js +469 -357
- package/dist/index.js +31 -1
- package/dist/indexDev.js +3 -1
- package/dist/lowdefySchema.js +302 -0
- package/dist/scripts/generateDefaultTypes.js +2 -35
- package/dist/test-utils/testContext.js +2 -0
- package/dist/utils/createPluginTypesMap.js +7 -0
- package/package.json +53 -42
- package/dist/build/jit/stripPageContent.js +0 -29
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/* eslint-disable no-param-reassign */ /*
|
|
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 path from 'path';
|
|
16
|
+
import fs from 'fs';
|
|
17
|
+
import { type } from '@lowdefy/helpers';
|
|
18
|
+
import { ConfigError, ConfigWarning } from '@lowdefy/errors';
|
|
19
|
+
import { RESERVED_PLATFORM_TOOL_NAMES } from '@lowdefy/ai-utils';
|
|
20
|
+
import countOperators from '../utils/countOperators.js';
|
|
21
|
+
import createCheckDuplicateId from '../utils/createCheckDuplicateId.js';
|
|
22
|
+
function detectCycles(agents) {
|
|
23
|
+
const graph = {};
|
|
24
|
+
for (const agent of agents){
|
|
25
|
+
graph[agent.agentId] = (agent.agents ?? []).map((ref)=>ref.agentId);
|
|
26
|
+
}
|
|
27
|
+
const visited = new Set();
|
|
28
|
+
const inStack = new Set();
|
|
29
|
+
function dfs(id) {
|
|
30
|
+
if (inStack.has(id)) return id;
|
|
31
|
+
if (visited.has(id)) return null;
|
|
32
|
+
visited.add(id);
|
|
33
|
+
inStack.add(id);
|
|
34
|
+
for (const neighbor of graph[id] ?? []){
|
|
35
|
+
const cycleNode = dfs(neighbor);
|
|
36
|
+
if (cycleNode !== null) return cycleNode;
|
|
37
|
+
}
|
|
38
|
+
inStack.delete(id);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
for (const id of Object.keys(graph)){
|
|
42
|
+
const cycleNode = dfs(id);
|
|
43
|
+
if (cycleNode !== null) {
|
|
44
|
+
return cycleNode;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
function buildAgents({ components, context }) {
|
|
50
|
+
if (!type.isArray(components.agents)) {
|
|
51
|
+
return components;
|
|
52
|
+
}
|
|
53
|
+
context.agentIds = new Set();
|
|
54
|
+
const checkDuplicateAgentId = createCheckDuplicateId({
|
|
55
|
+
message: 'Duplicate agentId "{{ id }}".'
|
|
56
|
+
});
|
|
57
|
+
components.agents.forEach((agent)=>{
|
|
58
|
+
const configKey = agent['~k'];
|
|
59
|
+
// Check duplicates
|
|
60
|
+
checkDuplicateAgentId({
|
|
61
|
+
id: agent.id,
|
|
62
|
+
configKey
|
|
63
|
+
});
|
|
64
|
+
// Track type usage for buildTypes validation
|
|
65
|
+
context.typeCounters.agents.increment(agent.type, configKey);
|
|
66
|
+
// Validate connectionId is provided
|
|
67
|
+
if (type.isNone(agent.connectionId)) {
|
|
68
|
+
throw new ConfigError(`Agent connectionId is not defined at "${agent.id}".`, {
|
|
69
|
+
configKey
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
// Validate connectionId references an existing connection
|
|
73
|
+
// Connections may have been renamed by buildConnections:
|
|
74
|
+
// connection.connectionId = original id, connection.id = 'connection:' + original id
|
|
75
|
+
const connectionExists = (components.connections ?? []).some((c)=>c.id === agent.connectionId || c.connectionId === agent.connectionId);
|
|
76
|
+
if (!connectionExists) {
|
|
77
|
+
throw new ConfigError(`Agent "${agent.id}" references connectionId "${agent.connectionId}" which does not exist.`, {
|
|
78
|
+
configKey
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
// Validate model is defined
|
|
82
|
+
if (type.isNone(agent.properties?.model)) {
|
|
83
|
+
throw new ConfigError(`Agent "model" is not defined at "${agent.id}".`, {
|
|
84
|
+
configKey
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
// Normalize tool strings to objects
|
|
88
|
+
agent.tools = (agent.tools ?? []).map((tool)=>{
|
|
89
|
+
if (type.isString(tool)) {
|
|
90
|
+
return {
|
|
91
|
+
endpointId: tool
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return tool;
|
|
95
|
+
});
|
|
96
|
+
// Validate tools reference existing API endpoints with required tool metadata
|
|
97
|
+
agent.tools.forEach((toolConfig)=>{
|
|
98
|
+
if (RESERVED_PLATFORM_TOOL_NAMES.includes(toolConfig.endpointId)) {
|
|
99
|
+
throw new ConfigError(`Agent "${agent.id}" tool "${toolConfig.endpointId}" uses a reserved platform tool name. Reserved: ${RESERVED_PLATFORM_TOOL_NAMES.join(', ')}.`, {
|
|
100
|
+
configKey
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
const endpoint = (components.api ?? []).find((e)=>e.id === toolConfig.endpointId || e.endpointId === toolConfig.endpointId);
|
|
104
|
+
if (!endpoint) {
|
|
105
|
+
throw new ConfigError(`Agent "${agent.id}" references tool endpoint "${toolConfig.endpointId}" which does not exist.`, {
|
|
106
|
+
configKey
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (type.isNone(endpoint.description)) {
|
|
110
|
+
throw new ConfigError(`Endpoint "${toolConfig.endpointId}" is used as an agent tool but does not have a "description".`, {
|
|
111
|
+
configKey: endpoint['~k']
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (type.isNone(endpoint.payloadSchema)) {
|
|
115
|
+
throw new ConfigError(`Endpoint "${toolConfig.endpointId}" is used as an agent tool but does not have a "payloadSchema".`, {
|
|
116
|
+
configKey: endpoint['~k']
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
// Normalize MCP string shorthand to connectionId objects (same pattern as tools)
|
|
121
|
+
agent.mcp = (agent.mcp ?? []).map((mcp)=>{
|
|
122
|
+
if (type.isString(mcp)) {
|
|
123
|
+
return {
|
|
124
|
+
connectionId: mcp
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return mcp;
|
|
128
|
+
});
|
|
129
|
+
// Validate MCP sources
|
|
130
|
+
agent.mcp.forEach((mcpSource, index)=>{
|
|
131
|
+
if (!type.isNone(mcpSource.connectionId)) {
|
|
132
|
+
// Validate connectionId references an existing connection
|
|
133
|
+
const mcpConnectionExists = (components.connections ?? []).some((c)=>c.id === mcpSource.connectionId || c.connectionId === mcpSource.connectionId);
|
|
134
|
+
if (!mcpConnectionExists) {
|
|
135
|
+
throw new ConfigError(`Agent "${agent.id}" "mcp" source at index ${index} references connection "${mcpSource.connectionId}" which does not exist.`, {
|
|
136
|
+
configKey
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
} else if (mcpSource.transport === 'stdio') {
|
|
140
|
+
if (type.isNone(mcpSource.command)) {
|
|
141
|
+
throw new ConfigError(`Agent "${agent.id}" "mcp" source at index ${index} uses stdio transport but is missing "command".`, {
|
|
142
|
+
configKey
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
if (type.isNone(mcpSource.url)) {
|
|
147
|
+
throw new ConfigError(`Agent "${agent.id}" "mcp" source at index ${index} is missing "url".`, {
|
|
148
|
+
configKey
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
// Validate hooks reference existing API endpoints
|
|
154
|
+
const hookNames = [
|
|
155
|
+
'onStart',
|
|
156
|
+
'onStepStart',
|
|
157
|
+
'onToolCallStart',
|
|
158
|
+
'onToolCallFinish',
|
|
159
|
+
'onStepFinish',
|
|
160
|
+
'onFinish'
|
|
161
|
+
];
|
|
162
|
+
hookNames.forEach((hookName)=>{
|
|
163
|
+
(agent.hooks?.[hookName] ?? []).forEach((endpointId)=>{
|
|
164
|
+
const endpoint = (components.api ?? []).find((e)=>e.id === endpointId || e.endpointId === endpointId);
|
|
165
|
+
if (!endpoint) {
|
|
166
|
+
throw new ConfigError(`Agent "${agent.id}" hook "${hookName}" references endpoint "${endpointId}" which does not exist.`, {
|
|
167
|
+
configKey
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
// Normalize sub-agent strings to objects (same pattern as tools/mcp)
|
|
173
|
+
agent.agents = (agent.agents ?? []).map((ref)=>{
|
|
174
|
+
if (type.isString(ref)) {
|
|
175
|
+
return {
|
|
176
|
+
agentId: ref
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
return ref;
|
|
180
|
+
});
|
|
181
|
+
// Validate fileSystem basePath if present
|
|
182
|
+
if (agent.properties?.fileSystem) {
|
|
183
|
+
const basePath = agent.properties.fileSystem.basePath;
|
|
184
|
+
if (!type.isString(basePath)) {
|
|
185
|
+
throw new ConfigError(`Agent "${agent.id}" fileSystem.basePath is not a string.`, {
|
|
186
|
+
received: basePath,
|
|
187
|
+
configKey
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
const resolved = path.resolve(context.directories.config, basePath);
|
|
191
|
+
if (!fs.existsSync(resolved)) {
|
|
192
|
+
throw new ConfigError(`Agent "${agent.id}" fileSystem.basePath "${basePath}" does not exist.`, {
|
|
193
|
+
configKey
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// Rename id to internal format
|
|
198
|
+
agent.agentId = agent.id;
|
|
199
|
+
context.agentIds.add(agent.agentId);
|
|
200
|
+
agent.id = `agent:${agent.agentId}`;
|
|
201
|
+
// Count server operators in properties
|
|
202
|
+
countOperators(agent.properties ?? {}, {
|
|
203
|
+
counter: context.typeCounters.operators.server
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
// Second pass: validate sub-agent references (needs all agentIds collected)
|
|
207
|
+
components.agents.forEach((agent)=>{
|
|
208
|
+
const configKey = agent['~k'];
|
|
209
|
+
agent.agents.forEach((subAgentRef)=>{
|
|
210
|
+
// Validate sub-agent reference exists
|
|
211
|
+
if (!context.agentIds.has(subAgentRef.agentId)) {
|
|
212
|
+
throw new ConfigError(`Agent "${agent.agentId}" references sub-agent "${subAgentRef.agentId}" which does not exist.`, {
|
|
213
|
+
configKey
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
// Reserved platform tool name guard for sub-agents
|
|
217
|
+
if (RESERVED_PLATFORM_TOOL_NAMES.includes(subAgentRef.agentId)) {
|
|
218
|
+
throw new ConfigError(`Agent "${agent.agentId}" sub-agent "${subAgentRef.agentId}" uses a reserved platform tool name. Reserved: ${RESERVED_PLATFORM_TOOL_NAMES.join(', ')}.`, {
|
|
219
|
+
configKey
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
// Check for name collision with endpoint tools
|
|
223
|
+
const hasToolCollision = agent.tools.some((toolConfig)=>toolConfig.endpointId === subAgentRef.agentId);
|
|
224
|
+
if (hasToolCollision) {
|
|
225
|
+
throw new ConfigError(`Agent "${agent.agentId}" sub-agent "${subAgentRef.agentId}" conflicts with an endpoint tool of the same name.`, {
|
|
226
|
+
configKey
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
// Warn if sub-agent has tools with confirm: true (unsupported in sub-agent context)
|
|
230
|
+
const subAgent = components.agents.find((a)=>a.agentId === subAgentRef.agentId);
|
|
231
|
+
const hasConfirmTools = (subAgent?.tools ?? []).some((t)=>t.confirm);
|
|
232
|
+
if (hasConfirmTools) {
|
|
233
|
+
context.handleWarning(new ConfigWarning(`Agent "${subAgentRef.agentId}" has tools with confirm: true, but tool approval is not supported in sub-agent context. Tools will auto-execute when called as a sub-agent.`, {
|
|
234
|
+
configKey
|
|
235
|
+
}));
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
// Detect circular sub-agent references
|
|
240
|
+
const cycleNode = detectCycles(components.agents);
|
|
241
|
+
if (cycleNode !== null) {
|
|
242
|
+
const agent = components.agents.find((a)=>a.agentId === cycleNode);
|
|
243
|
+
throw new ConfigError(`Circular sub-agent reference detected involving "${cycleNode}".`, {
|
|
244
|
+
configKey: agent?.['~k']
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
return components;
|
|
248
|
+
}
|
|
249
|
+
export default buildAgents;
|
|
@@ -13,6 +13,9 @@
|
|
|
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') {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
16
19
|
typeCounters.requests.increment(step.type, step['~k']);
|
|
17
20
|
}
|
|
18
21
|
export default countStepTypes;
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ function setStepId(step, { endpointId }) {
|
|
16
|
-
step.
|
|
16
|
+
step.stepId = step.id;
|
|
17
17
|
step.endpointId = endpointId;
|
|
18
|
-
step.
|
|
18
|
+
const prefix = step.type === 'CallApi' ? 'endpoint' : 'request';
|
|
19
|
+
step.id = `${prefix}:${endpointId}:${step.stepId}`;
|
|
19
20
|
}
|
|
20
21
|
export default setStepId;
|
|
@@ -50,6 +50,25 @@ function validateStep(step, { endpointId }) {
|
|
|
50
50
|
configKey
|
|
51
51
|
});
|
|
52
52
|
}
|
|
53
|
+
if (step.type === 'CallApi') {
|
|
54
|
+
if (type.isNone(step.properties?.endpointId)) {
|
|
55
|
+
throw new ConfigError(`Endpoint step "${step.id}" at endpoint "${endpointId}" requires properties.endpointId.`, {
|
|
56
|
+
configKey
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (!type.isString(step.properties.endpointId) && !type.isObject(step.properties.endpointId)) {
|
|
60
|
+
throw new ConfigError(`Endpoint step "${step.id}" at endpoint "${endpointId}" properties.endpointId is not a string.`, {
|
|
61
|
+
received: step.properties.endpointId,
|
|
62
|
+
configKey
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (!type.isNone(step.connectionId)) {
|
|
66
|
+
throw new ConfigError(`Endpoint step "${step.id}" at endpoint "${endpointId}" should not have a connectionId.`, {
|
|
67
|
+
configKey
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
53
72
|
if (type.isUndefined(step.connectionId)) {
|
|
54
73
|
throw new ConfigError(`Step connectionId missing at endpoint "${endpointId}".`, {
|
|
55
74
|
configKey
|
|
@@ -44,6 +44,16 @@ function validateEndpoint({ endpoint, index, checkDuplicateEndpointId }) {
|
|
|
44
44
|
configKey
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
|
+
const validEndpointTypes = [
|
|
48
|
+
'Api',
|
|
49
|
+
'InternalApi'
|
|
50
|
+
];
|
|
51
|
+
if (!validEndpointTypes.includes(endpoint.type)) {
|
|
52
|
+
throw new ConfigError(`Endpoint type "${endpoint.type}" is not valid at "${endpoint.id}". Must be one of: ${validEndpointTypes.join(', ')}.`, {
|
|
53
|
+
received: endpoint.type,
|
|
54
|
+
configKey
|
|
55
|
+
});
|
|
56
|
+
}
|
|
47
57
|
checkDuplicateEndpointId({
|
|
48
58
|
id: endpoint.id,
|
|
49
59
|
configKey
|
|
@@ -17,16 +17,16 @@ import { type } from '@lowdefy/helpers';
|
|
|
17
17
|
import extractOperatorKey from '../../utils/extractOperatorKey.js';
|
|
18
18
|
import traverseConfig from '../../utils/traverseConfig.js';
|
|
19
19
|
// Collect all step IDs from a routine (including nested control structures)
|
|
20
|
-
// Note: After buildRoutine, steps have
|
|
20
|
+
// Note: After buildRoutine, steps have stepId (original id) and id is modified
|
|
21
21
|
function collectStepIds(routine, stepIds) {
|
|
22
22
|
if (type.isArray(routine)) {
|
|
23
23
|
routine.forEach((item)=>collectStepIds(item, stepIds));
|
|
24
24
|
return;
|
|
25
25
|
}
|
|
26
26
|
if (type.isObject(routine)) {
|
|
27
|
-
// Check if this is a step (has
|
|
28
|
-
if (routine.
|
|
29
|
-
stepIds.add(routine.
|
|
27
|
+
// Check if this is a step (has stepId after build, or id before build)
|
|
28
|
+
if (routine.stepId) {
|
|
29
|
+
stepIds.add(routine.stepId);
|
|
30
30
|
}
|
|
31
31
|
// Recurse into all values (handles control structures like :then, :else, :try, :catch)
|
|
32
32
|
Object.values(routine).forEach((value)=>collectStepIds(value, stepIds));
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { ConfigError } from '@lowdefy/errors';
|
|
17
17
|
import getApiRoles from './getApiRoles.js';
|
|
18
18
|
import getProtectedApi from './getProtectedApi.js';
|
|
19
|
+
import { isInPatternList } from './matchPattern.js';
|
|
19
20
|
function buildApiAuth({ components, context }) {
|
|
20
21
|
const protectedApiEndpoints = getProtectedApi({
|
|
21
22
|
components
|
|
@@ -29,7 +30,7 @@ function buildApiAuth({ components, context }) {
|
|
|
29
30
|
}
|
|
30
31
|
(components.api || []).forEach((endpoint)=>{
|
|
31
32
|
if (apiRoles[endpoint.id]) {
|
|
32
|
-
if (
|
|
33
|
+
if (isInPatternList(endpoint.id, configPublicApi)) {
|
|
33
34
|
throw new ConfigError(`Endpoint "${endpoint.id}" is both protected by roles and public.`, {
|
|
34
35
|
received: apiRoles[endpoint.id],
|
|
35
36
|
configKey: endpoint['~k']
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { ConfigError } from '@lowdefy/errors';
|
|
17
17
|
import getPageRoles from './getPageRoles.js';
|
|
18
18
|
import getProtectedPages from './getProtectedPages.js';
|
|
19
|
+
import { isInPatternList } from './matchPattern.js';
|
|
19
20
|
function buildPageAuth({ components, context }) {
|
|
20
21
|
const protectedPages = getProtectedPages({
|
|
21
22
|
components
|
|
@@ -36,7 +37,7 @@ function buildPageAuth({ components, context }) {
|
|
|
36
37
|
return;
|
|
37
38
|
}
|
|
38
39
|
if (pageRoles[page.id]) {
|
|
39
|
-
if (
|
|
40
|
+
if (isInPatternList(page.id, configPublicPages)) {
|
|
40
41
|
throw new ConfigError(`Page "${page.id}" is both protected by roles and public.`, {
|
|
41
42
|
received: pageRoles[page.id],
|
|
42
43
|
configKey: page['~k']
|
|
@@ -12,15 +12,21 @@
|
|
|
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
|
-
*/
|
|
15
|
+
*/ import { matchesPattern } from './matchPattern.js';
|
|
16
|
+
function getApiRoles({ components }) {
|
|
16
17
|
const roles = components.auth.api.roles;
|
|
18
|
+
const endpointIds = (components.api ?? []).map((e)=>e.id);
|
|
17
19
|
const apiRoles = {};
|
|
18
20
|
Object.keys(roles).forEach((roleName)=>{
|
|
19
|
-
roles[roleName].forEach((
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
roles[roleName].forEach((pattern)=>{
|
|
22
|
+
endpointIds.forEach((endpointId)=>{
|
|
23
|
+
if (matchesPattern(endpointId, pattern)) {
|
|
24
|
+
if (!apiRoles[endpointId]) {
|
|
25
|
+
apiRoles[endpointId] = new Set();
|
|
26
|
+
}
|
|
27
|
+
apiRoles[endpointId].add(roleName);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
24
30
|
});
|
|
25
31
|
});
|
|
26
32
|
Object.keys(apiRoles).forEach((endpointId)=>{
|
|
@@ -12,15 +12,21 @@
|
|
|
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
|
-
*/
|
|
15
|
+
*/ import { matchesPattern } from './matchPattern.js';
|
|
16
|
+
function getPageRoles({ components }) {
|
|
16
17
|
const roles = components.auth.pages.roles;
|
|
18
|
+
const pageIds = (components.pages ?? []).map((p)=>p.id);
|
|
17
19
|
const pageRoles = {};
|
|
18
20
|
Object.keys(roles).forEach((roleName)=>{
|
|
19
|
-
roles[roleName].forEach((
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
roles[roleName].forEach((pattern)=>{
|
|
22
|
+
pageIds.forEach((pageId)=>{
|
|
23
|
+
if (matchesPattern(pageId, pattern)) {
|
|
24
|
+
if (!pageRoles[pageId]) {
|
|
25
|
+
pageRoles[pageId] = new Set();
|
|
26
|
+
}
|
|
27
|
+
pageRoles[pageId].add(roleName);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
24
30
|
});
|
|
25
31
|
});
|
|
26
32
|
Object.keys(pageRoles).forEach((pageId)=>{
|
|
@@ -13,15 +13,16 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import { type } from '@lowdefy/helpers';
|
|
16
|
+
import { isInPatternList } from './matchPattern.js';
|
|
16
17
|
function getProtectedApi({ components }) {
|
|
17
18
|
const endpointIds = (components.api || []).map((endpoint)=>endpoint.id);
|
|
18
19
|
let protectedApi = [];
|
|
19
20
|
if (type.isArray(components.auth.api.public)) {
|
|
20
|
-
protectedApi = endpointIds.filter((endpointId)=>!components.auth.api.public
|
|
21
|
+
protectedApi = endpointIds.filter((endpointId)=>!isInPatternList(endpointId, components.auth.api.public));
|
|
21
22
|
} else if (components.auth.api.protected === true) {
|
|
22
23
|
protectedApi = endpointIds;
|
|
23
24
|
} else if (type.isArray(components.auth.api.protected)) {
|
|
24
|
-
protectedApi = components.auth.api.protected;
|
|
25
|
+
protectedApi = endpointIds.filter((endpointId)=>isInPatternList(endpointId, components.auth.api.protected));
|
|
25
26
|
}
|
|
26
27
|
return protectedApi;
|
|
27
28
|
}
|
|
@@ -13,15 +13,16 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import { type } from '@lowdefy/helpers';
|
|
16
|
+
import { isInPatternList } from './matchPattern.js';
|
|
16
17
|
function getProtectedPages({ components }) {
|
|
17
18
|
const pageIds = (components.pages || []).map((page)=>page.id);
|
|
18
19
|
let protectedPages = [];
|
|
19
20
|
if (type.isArray(components.auth.pages.public)) {
|
|
20
|
-
protectedPages = pageIds.filter((pageId)=>!components.auth.pages.public
|
|
21
|
+
protectedPages = pageIds.filter((pageId)=>!isInPatternList(pageId, components.auth.pages.public));
|
|
21
22
|
} else if (components.auth.pages.protected === true) {
|
|
22
23
|
protectedPages = pageIds;
|
|
23
24
|
} else if (type.isArray(components.auth.pages.protected)) {
|
|
24
|
-
protectedPages = components.auth.pages.protected;
|
|
25
|
+
protectedPages = pageIds.filter((pageId)=>isInPatternList(pageId, components.auth.pages.protected));
|
|
25
26
|
}
|
|
26
27
|
return protectedPages;
|
|
27
28
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
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 picomatch from 'picomatch';
|
|
16
|
+
function matchesPattern(id, pattern) {
|
|
17
|
+
return picomatch.isMatch(id, pattern);
|
|
18
|
+
}
|
|
19
|
+
function isInPatternList(id, patternList) {
|
|
20
|
+
return patternList.some((pattern)=>matchesPattern(id, pattern));
|
|
21
|
+
}
|
|
22
|
+
export { matchesPattern, isInPatternList };
|
|
@@ -12,20 +12,58 @@
|
|
|
12
12
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
|
-
*/ import
|
|
15
|
+
*/ import { type } from '@lowdefy/helpers';
|
|
16
|
+
import { ConfigError } from '@lowdefy/errors';
|
|
17
|
+
import collectExceptions from '../utils/collectExceptions.js';
|
|
18
|
+
import countOperators from '../utils/countOperators.js';
|
|
16
19
|
import createCheckDuplicateId from '../utils/createCheckDuplicateId.js';
|
|
17
20
|
import validateId from '../utils/validateId.js';
|
|
21
|
+
function validateConnection(connection, context) {
|
|
22
|
+
const configKey = connection?.['~k'];
|
|
23
|
+
if (!type.isObject(connection)) {
|
|
24
|
+
collectExceptions(context, new ConfigError('Connection should be an object.', {
|
|
25
|
+
received: connection,
|
|
26
|
+
configKey
|
|
27
|
+
}));
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
if (type.isUndefined(connection.id)) {
|
|
31
|
+
collectExceptions(context, new ConfigError('Connection id missing.', {
|
|
32
|
+
configKey
|
|
33
|
+
}));
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
if (!type.isString(connection.id)) {
|
|
37
|
+
collectExceptions(context, new ConfigError('Connection id is not a string.', {
|
|
38
|
+
received: connection.id,
|
|
39
|
+
configKey
|
|
40
|
+
}));
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
if (type.isNone(connection.type)) {
|
|
44
|
+
collectExceptions(context, new ConfigError(`Connection type is not defined at connection "${connection.id}".`, {
|
|
45
|
+
configKey
|
|
46
|
+
}));
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
if (!type.isString(connection.type)) {
|
|
50
|
+
collectExceptions(context, new ConfigError(`Connection type is not a string at connection "${connection.id}".`, {
|
|
51
|
+
received: connection.type,
|
|
52
|
+
configKey
|
|
53
|
+
}));
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
18
58
|
function buildConnections({ components, context }) {
|
|
19
59
|
// Store connection IDs for validation in buildRequests
|
|
20
60
|
context.connectionIds = new Set();
|
|
21
|
-
// Schema validates: id required, id is string, type is string
|
|
22
|
-
// Only check for duplicates here (schema can't do that)
|
|
23
61
|
const checkDuplicateConnectionId = createCheckDuplicateId({
|
|
24
62
|
message: 'Duplicate connectionId "{{ id }}".'
|
|
25
63
|
});
|
|
26
64
|
(components.connections ?? []).forEach((connection)=>{
|
|
65
|
+
if (!validateConnection(connection, context)) return;
|
|
27
66
|
const configKey = connection['~k'];
|
|
28
|
-
// Check duplicates (schema can't validate this)
|
|
29
67
|
checkDuplicateConnectionId({
|
|
30
68
|
id: connection.id,
|
|
31
69
|
configKey
|
|
@@ -22,6 +22,7 @@ function getPluginPackages({ components }) {
|
|
|
22
22
|
});
|
|
23
23
|
}
|
|
24
24
|
getPackages(components.types.actions);
|
|
25
|
+
getPackages(components.types.agents);
|
|
25
26
|
getPackages(components.types.auth.adapters);
|
|
26
27
|
getPackages(components.types.auth.callbacks);
|
|
27
28
|
getPackages(components.types.auth.events);
|
|
@@ -53,6 +54,10 @@ function buildImportsDev({ components, context }) {
|
|
|
53
54
|
pluginPackages,
|
|
54
55
|
map: context.typesMap.actions
|
|
55
56
|
}),
|
|
57
|
+
agents: buildImportClassDev({
|
|
58
|
+
pluginPackages,
|
|
59
|
+
map: context.typesMap.agents
|
|
60
|
+
}),
|
|
56
61
|
auth: {
|
|
57
62
|
adapters: buildImportClassDev({
|
|
58
63
|
pluginPackages,
|
|
@@ -25,6 +25,7 @@ function buildImportsProd({ components, context }) {
|
|
|
25
25
|
const blocks = buildImportClassProd(components.types.blocks);
|
|
26
26
|
return {
|
|
27
27
|
actions: buildImportClassProd(components.types.actions),
|
|
28
|
+
agents: buildImportClassProd(components.types.agents),
|
|
28
29
|
auth: {
|
|
29
30
|
adapters: buildImportClassProd(components.types.auth.adapters),
|
|
30
31
|
callbacks: buildImportClassProd(components.types.auth.callbacks),
|
|
@@ -15,12 +15,10 @@
|
|
|
15
15
|
*/ import { ConfigError } from '@lowdefy/errors';
|
|
16
16
|
import { serializer, type } from '@lowdefy/helpers';
|
|
17
17
|
import crypto from 'crypto';
|
|
18
|
-
function
|
|
18
|
+
function hashFn({ jsMap, env, value }) {
|
|
19
19
|
const hash = crypto.createHash('sha1').update(value).digest('base64');
|
|
20
20
|
jsMap[env][hash] = value;
|
|
21
|
-
return
|
|
22
|
-
_js: hash
|
|
23
|
-
};
|
|
21
|
+
return hash;
|
|
24
22
|
}
|
|
25
23
|
function JsMapParser({ input, jsMap, env }) {
|
|
26
24
|
if (!jsMap[env]) {
|
|
@@ -31,15 +29,30 @@ function JsMapParser({ input, jsMap, env }) {
|
|
|
31
29
|
if (Object.keys(value).length !== 1) return value;
|
|
32
30
|
const key = Object.keys(value)[0];
|
|
33
31
|
if (key !== '_js') return value;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
const inner = value[key];
|
|
33
|
+
if (type.isString(inner)) {
|
|
34
|
+
return {
|
|
35
|
+
_js: hashFn({
|
|
36
|
+
jsMap,
|
|
37
|
+
env,
|
|
38
|
+
value: inner
|
|
39
|
+
})
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (type.isObject(inner) && type.isString(inner.fn)) {
|
|
43
|
+
return {
|
|
44
|
+
_js: {
|
|
45
|
+
fn: hashFn({
|
|
46
|
+
jsMap,
|
|
47
|
+
env,
|
|
48
|
+
value: inner.fn
|
|
49
|
+
}),
|
|
50
|
+
args: inner.args
|
|
51
|
+
}
|
|
52
|
+
};
|
|
38
53
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
env,
|
|
42
|
-
value: value[key]
|
|
54
|
+
throw new ConfigError(`_js operator expects a JavaScript string or { fn: string, args?: object }. Received ${JSON.stringify(inner)}.`, {
|
|
55
|
+
configKey: value['~k']
|
|
43
56
|
});
|
|
44
57
|
};
|
|
45
58
|
return serializer.copy(input, {
|
|
@@ -16,11 +16,11 @@
|
|
|
16
16
|
async function writeJs({ context }) {
|
|
17
17
|
await context.writeBuildArtifact('plugins/operators/clientJsMap.js', generateJsFile({
|
|
18
18
|
map: context.jsMap.client,
|
|
19
|
-
functionPrototype: `{ actions, event, input, location, lowdefyGlobal, request, state, urlQuery, user }`
|
|
19
|
+
functionPrototype: `{ actions, args, event, input, location, lowdefyGlobal, request, state, urlQuery, user }`
|
|
20
20
|
}));
|
|
21
21
|
await context.writeBuildArtifact('plugins/operators/serverJsMap.js', generateJsFile({
|
|
22
22
|
map: context.jsMap.server,
|
|
23
|
-
functionPrototype: `{ item, payload, secrets, state, step, user }`
|
|
23
|
+
functionPrototype: `{ args, item, payload, secrets, state, step, user }`
|
|
24
24
|
}));
|
|
25
25
|
}
|
|
26
26
|
export default writeJs;
|