@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
|
@@ -0,0 +1,117 @@
|
|
|
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 { type } from '@lowdefy/helpers';
|
|
16
|
+
import { ConfigError } from '@lowdefy/errors';
|
|
17
|
+
const strategyTypes = [
|
|
18
|
+
'apiKey',
|
|
19
|
+
'jwt'
|
|
20
|
+
];
|
|
21
|
+
// Prototype-pollution vectors rejected as claimMapping path segments.
|
|
22
|
+
const reservedKeys = new Set([
|
|
23
|
+
'__proto__',
|
|
24
|
+
'constructor',
|
|
25
|
+
'prototype',
|
|
26
|
+
'__defineGetter__',
|
|
27
|
+
'__defineSetter__',
|
|
28
|
+
'__lookupGetter__',
|
|
29
|
+
'__lookupSetter__'
|
|
30
|
+
]);
|
|
31
|
+
function setDefault(object, key, value) {
|
|
32
|
+
if (type.isNone(object[key])) {
|
|
33
|
+
object[key] = value;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function validateApiKeyStrategy({ strategy, configKey }) {
|
|
37
|
+
const properties = strategy.properties ?? {};
|
|
38
|
+
if (!type.isArray(properties.keys) || properties.keys.length === 0) {
|
|
39
|
+
throw new ConfigError(`Auth strategy "${strategy.id}" requires "properties.keys" to be a non-empty array of keys.`, {
|
|
40
|
+
configKey
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
properties.keys.forEach((key, index)=>{
|
|
44
|
+
if (type.isNone(key.value)) {
|
|
45
|
+
throw new ConfigError(`Auth strategy "${strategy.id}" key at index ${index} is missing "value". Reference the key with the _secret operator.`, {
|
|
46
|
+
configKey
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function validateJwtStrategy({ strategy, configKey }) {
|
|
52
|
+
const properties = strategy.properties ?? {};
|
|
53
|
+
const hasSecret = !type.isNone(properties.secret);
|
|
54
|
+
const hasJwksUri = !type.isNone(properties.jwksUri);
|
|
55
|
+
if (hasSecret === hasJwksUri) {
|
|
56
|
+
throw new ConfigError(`Auth strategy "${strategy.id}" requires exactly one of "properties.secret" or "properties.jwksUri" to verify token signatures.`, {
|
|
57
|
+
configKey
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
// The algorithms allowlist restricts which signing algorithms are accepted,
|
|
61
|
+
// preventing "alg: none" and algorithm-confusion downgrade attacks.
|
|
62
|
+
if (!type.isArray(properties.algorithms) || properties.algorithms.length === 0) {
|
|
63
|
+
throw new ConfigError(`Auth strategy "${strategy.id}" requires "properties.algorithms" to be a non-empty array restricting the accepted signing algorithms.`, {
|
|
64
|
+
configKey
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
// claimMapping field names become object keys on the user and attributes
|
|
68
|
+
// objects the jwt verifier builds per request - gated here where the config
|
|
69
|
+
// location is available, so a pollution-vector key never reaches runtime.
|
|
70
|
+
Object.keys(properties.claimMapping ?? {}).forEach((field)=>{
|
|
71
|
+
const reservedSegment = field.split('.').find((segment)=>reservedKeys.has(segment));
|
|
72
|
+
if (!type.isNone(reservedSegment)) {
|
|
73
|
+
throw new ConfigError(`Auth strategy "${strategy.id}" claimMapping field "${field}" uses reserved name "${reservedSegment}".`, {
|
|
74
|
+
configKey
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
// Validates auth.strategies and writes per-entry defaults so the runtime
|
|
80
|
+
// never falls back. Objects are mutated in place to preserve ~k markers.
|
|
81
|
+
function buildAuthStrategies({ components }) {
|
|
82
|
+
const seenIds = {};
|
|
83
|
+
(components.auth.strategies ?? []).forEach((strategy)=>{
|
|
84
|
+
const configKey = strategy['~k'];
|
|
85
|
+
if (seenIds[strategy.id] === true) {
|
|
86
|
+
throw new ConfigError(`Duplicate auth strategy id "${strategy.id}".`, {
|
|
87
|
+
configKey
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
seenIds[strategy.id] = true;
|
|
91
|
+
if (!strategyTypes.includes(strategy.type)) {
|
|
92
|
+
throw new ConfigError(`Auth strategy "${strategy.id}" has unknown type "${strategy.type}". Valid types are: ${strategyTypes.join(', ')}.`, {
|
|
93
|
+
received: strategy.type,
|
|
94
|
+
configKey
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (strategy.type === 'apiKey') {
|
|
98
|
+
validateApiKeyStrategy({
|
|
99
|
+
strategy,
|
|
100
|
+
configKey
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
if (strategy.type === 'jwt') {
|
|
104
|
+
validateJwtStrategy({
|
|
105
|
+
strategy,
|
|
106
|
+
configKey
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
setDefault(strategy, 'roles', []);
|
|
110
|
+
setDefault(strategy, 'attributes', {});
|
|
111
|
+
if (strategy.type === 'apiKey') {
|
|
112
|
+
setDefault(strategy.properties, 'headerName', 'X-API-Key');
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
return components;
|
|
116
|
+
}
|
|
117
|
+
export default buildAuthStrategies;
|
|
@@ -0,0 +1,55 @@
|
|
|
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 { type } from '@lowdefy/helpers';
|
|
16
|
+
import { ConfigError } from '@lowdefy/errors';
|
|
17
|
+
import getProtectedWebsockets from './getProtectedWebsockets.js';
|
|
18
|
+
import getWebsocketRoles from './getWebsocketRoles.js';
|
|
19
|
+
import { isInPatternList } from './matchPattern.js';
|
|
20
|
+
function buildWebsocketAuth({ components, context }) {
|
|
21
|
+
const protectedWebsockets = getProtectedWebsockets({
|
|
22
|
+
components
|
|
23
|
+
});
|
|
24
|
+
const websocketRoles = getWebsocketRoles({
|
|
25
|
+
components
|
|
26
|
+
});
|
|
27
|
+
let configPublicWebsockets = [];
|
|
28
|
+
if (type.isArray(components.auth.websockets.public)) {
|
|
29
|
+
configPublicWebsockets = components.auth.websockets.public;
|
|
30
|
+
}
|
|
31
|
+
(components.websockets || []).forEach((websocket)=>{
|
|
32
|
+
if (websocketRoles[websocket.id]) {
|
|
33
|
+
if (isInPatternList(websocket.id, configPublicWebsockets)) {
|
|
34
|
+
throw new ConfigError(`Websocket "${websocket.id}" is both protected by roles and public.`, {
|
|
35
|
+
received: websocketRoles[websocket.id],
|
|
36
|
+
configKey: websocket['~k']
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
websocket.auth = {
|
|
40
|
+
public: false,
|
|
41
|
+
roles: websocketRoles[websocket.id]
|
|
42
|
+
};
|
|
43
|
+
} else if (protectedWebsockets.includes(websocket.id)) {
|
|
44
|
+
websocket.auth = {
|
|
45
|
+
public: false
|
|
46
|
+
};
|
|
47
|
+
} else {
|
|
48
|
+
websocket.auth = {
|
|
49
|
+
public: true
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
return components;
|
|
54
|
+
}
|
|
55
|
+
export default buildWebsocketAuth;
|
|
@@ -0,0 +1,29 @@
|
|
|
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 { isInPatternList } from './matchPattern.js';
|
|
17
|
+
function getProtectedWebsockets({ components }) {
|
|
18
|
+
const websocketIds = (components.websockets ?? []).map((websocket)=>websocket.id);
|
|
19
|
+
let protectedWebsockets = [];
|
|
20
|
+
if (type.isArray(components.auth.websockets.public)) {
|
|
21
|
+
protectedWebsockets = websocketIds.filter((websocketId)=>!isInPatternList(websocketId, components.auth.websockets.public));
|
|
22
|
+
} else if (components.auth.websockets.protected === true) {
|
|
23
|
+
protectedWebsockets = websocketIds;
|
|
24
|
+
} else if (type.isArray(components.auth.websockets.protected)) {
|
|
25
|
+
protectedWebsockets = websocketIds.filter((websocketId)=>isInPatternList(websocketId, components.auth.websockets.protected));
|
|
26
|
+
}
|
|
27
|
+
return protectedWebsockets;
|
|
28
|
+
}
|
|
29
|
+
export default getProtectedWebsockets;
|
|
@@ -0,0 +1,39 @@
|
|
|
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 { matchesPattern } from './matchPattern.js';
|
|
16
|
+
function getWebsocketRoles({ components }) {
|
|
17
|
+
const roles = components.auth.websockets.roles;
|
|
18
|
+
const websocketIds = (components.websockets ?? []).map((websocket)=>websocket.id);
|
|
19
|
+
const websocketRoles = {};
|
|
20
|
+
Object.keys(roles).forEach((roleName)=>{
|
|
21
|
+
roles[roleName].forEach((pattern)=>{
|
|
22
|
+
websocketIds.forEach((websocketId)=>{
|
|
23
|
+
if (matchesPattern(websocketId, pattern)) {
|
|
24
|
+
if (!websocketRoles[websocketId]) {
|
|
25
|
+
websocketRoles[websocketId] = new Set();
|
|
26
|
+
}
|
|
27
|
+
websocketRoles[websocketId].add(roleName);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
Object.keys(websocketRoles).forEach((websocketId)=>{
|
|
33
|
+
websocketRoles[websocketId] = [
|
|
34
|
+
...websocketRoles[websocketId]
|
|
35
|
+
];
|
|
36
|
+
});
|
|
37
|
+
return websocketRoles;
|
|
38
|
+
}
|
|
39
|
+
export default getWebsocketRoles;
|
|
@@ -32,6 +32,12 @@ function validateAuthConfig({ components, context }) {
|
|
|
32
32
|
if (type.isNone(components.auth.api.roles)) {
|
|
33
33
|
components.auth.api.roles = {};
|
|
34
34
|
}
|
|
35
|
+
if (type.isNone(components.auth.websockets)) {
|
|
36
|
+
components.auth.websockets = {};
|
|
37
|
+
}
|
|
38
|
+
if (type.isNone(components.auth.websockets.roles)) {
|
|
39
|
+
components.auth.websockets.roles = {};
|
|
40
|
+
}
|
|
35
41
|
if (type.isNone(components.auth.authPages)) {
|
|
36
42
|
components.auth.authPages = {};
|
|
37
43
|
}
|
|
@@ -93,9 +99,15 @@ function validateAuthConfig({ components, context }) {
|
|
|
93
99
|
context,
|
|
94
100
|
entity: 'pages'
|
|
95
101
|
});
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
102
|
+
validateMutualExclusivity({
|
|
103
|
+
components,
|
|
104
|
+
context,
|
|
105
|
+
entity: 'websockets'
|
|
106
|
+
});
|
|
107
|
+
// Validate AUTH_SECRET is set when auth providers are configured. Auth.js v5
|
|
108
|
+
// reads AUTH_SECRET; the v4 NEXTAUTH_SECRET variable is not supported.
|
|
109
|
+
if (components.auth.providers.length > 0 && type.isNone(process.env.AUTH_SECRET)) {
|
|
110
|
+
throw new ConfigError('Auth providers are configured but AUTH_SECRET environment variable is not set. ' + 'Set AUTH_SECRET to a secure random string (e.g., generate with `openssl rand -base64 32`).', {
|
|
99
111
|
configKey: components.auth.providers['~k'] ?? components.auth['~k']
|
|
100
112
|
});
|
|
101
113
|
}
|
|
@@ -27,9 +27,12 @@ function getPluginPackages({ components }) {
|
|
|
27
27
|
getPackages(components.types.auth.callbacks);
|
|
28
28
|
getPackages(components.types.auth.events);
|
|
29
29
|
getPackages(components.types.auth.providers);
|
|
30
|
+
getPackages(components.types.auth.strategies);
|
|
30
31
|
getPackages(components.types.blocks);
|
|
31
32
|
getPackages(components.types.connections);
|
|
33
|
+
getPackages(components.types.notifications);
|
|
32
34
|
getPackages(components.types.requests);
|
|
35
|
+
getPackages(components.types.websockets);
|
|
33
36
|
getPackages(components.types.operators.client);
|
|
34
37
|
getPackages(components.types.operators.server);
|
|
35
38
|
return pluginPackages;
|
|
@@ -74,6 +77,10 @@ function buildImportsDev({ components, context }) {
|
|
|
74
77
|
providers: buildImportClassDev({
|
|
75
78
|
pluginPackages,
|
|
76
79
|
map: context.typesMap.auth.providers
|
|
80
|
+
}),
|
|
81
|
+
strategies: buildImportClassDev({
|
|
82
|
+
pluginPackages,
|
|
83
|
+
map: context.typesMap.auth.strategies
|
|
77
84
|
})
|
|
78
85
|
},
|
|
79
86
|
blocks,
|
|
@@ -87,10 +94,18 @@ function buildImportsDev({ components, context }) {
|
|
|
87
94
|
context,
|
|
88
95
|
defaults: defaultIconsDev
|
|
89
96
|
}),
|
|
97
|
+
notifications: buildImportClassDev({
|
|
98
|
+
pluginPackages,
|
|
99
|
+
map: context.typesMap.notifications
|
|
100
|
+
}),
|
|
90
101
|
requests: buildImportClassDev({
|
|
91
102
|
pluginPackages,
|
|
92
103
|
map: context.typesMap.requests
|
|
93
104
|
}),
|
|
105
|
+
websockets: buildImportClassDev({
|
|
106
|
+
pluginPackages,
|
|
107
|
+
map: context.typesMap.websockets
|
|
108
|
+
}),
|
|
94
109
|
operators: {
|
|
95
110
|
client: buildImportClassDev({
|
|
96
111
|
pluginPackages,
|
|
@@ -30,7 +30,8 @@ function buildImportsProd({ components, context }) {
|
|
|
30
30
|
adapters: buildImportClassProd(components.types.auth.adapters),
|
|
31
31
|
callbacks: buildImportClassProd(components.types.auth.callbacks),
|
|
32
32
|
events: buildImportClassProd(components.types.auth.events),
|
|
33
|
-
providers: buildImportClassProd(components.types.auth.providers)
|
|
33
|
+
providers: buildImportClassProd(components.types.auth.providers),
|
|
34
|
+
strategies: buildImportClassProd(components.types.auth.strategies)
|
|
34
35
|
},
|
|
35
36
|
blocks,
|
|
36
37
|
connections: buildImportClassProd(components.types.connections),
|
|
@@ -40,7 +41,9 @@ function buildImportsProd({ components, context }) {
|
|
|
40
41
|
context,
|
|
41
42
|
defaults: defaultIconsProd
|
|
42
43
|
}),
|
|
44
|
+
notifications: buildImportClassProd(components.types.notifications),
|
|
43
45
|
requests: buildImportClassProd(components.types.requests),
|
|
46
|
+
websockets: buildImportClassProd(components.types.websockets),
|
|
44
47
|
operators: {
|
|
45
48
|
client: buildImportClassProd(components.types.operators.client),
|
|
46
49
|
server: buildImportClassProd(components.types.operators.server)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import generateJsFile from './generateJsFile.js';
|
|
16
|
+
// Single definition of the client _js function prototype. Both the full build
|
|
17
|
+
// (writeJs) and the dev server's per-page fold (getPageJitEnrichment) generate
|
|
18
|
+
// client jsMap module text through here, so the destructured argument list has
|
|
19
|
+
// exactly one source of truth.
|
|
20
|
+
const CLIENT_JS_FUNCTION_PROTOTYPE = `{ actions, args, event, input, location, lowdefyApp, lowdefyGlobal, request, state, urlQuery, user }`;
|
|
21
|
+
function generateClientJsModule(map) {
|
|
22
|
+
return generateJsFile({
|
|
23
|
+
map,
|
|
24
|
+
functionPrototype: CLIENT_JS_FUNCTION_PROTOTYPE
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export default generateClientJsModule;
|
|
@@ -12,12 +12,10 @@
|
|
|
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 generateClientJsModule from './generateClientJsModule.js';
|
|
16
|
+
import generateJsFile from './generateJsFile.js';
|
|
16
17
|
async function writeJs({ context }) {
|
|
17
|
-
await context.writeBuildArtifact('plugins/operators/clientJsMap.js',
|
|
18
|
-
map: context.jsMap.client,
|
|
19
|
-
functionPrototype: `{ actions, args, event, input, location, lowdefyApp, lowdefyGlobal, request, state, urlQuery, user }`
|
|
20
|
-
}));
|
|
18
|
+
await context.writeBuildArtifact('plugins/operators/clientJsMap.js', generateClientJsModule(context.jsMap.client));
|
|
21
19
|
await context.writeBuildArtifact('plugins/operators/serverJsMap.js', generateJsFile({
|
|
22
20
|
map: context.jsMap.server,
|
|
23
21
|
functionPrototype: `{ args, item, lowdefyApp, payload, secret, state, step, user }`
|
|
@@ -0,0 +1,73 @@
|
|
|
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
|
+
*/ /* eslint-disable no-param-reassign */ import { type } from '@lowdefy/helpers';
|
|
16
|
+
import { ConfigError } from '@lowdefy/errors';
|
|
17
|
+
// Validates the mcp block and writes its defaults. Endpoints are matched via
|
|
18
|
+
// the same dual id/endpointId match buildApi uses. Referenced endpoints must
|
|
19
|
+
// carry the metadata an MCP client needs: a description and a payloadSchema
|
|
20
|
+
// (the tool inputSchema).
|
|
21
|
+
function buildMcp({ components }) {
|
|
22
|
+
if (type.isNone(components.mcp)) {
|
|
23
|
+
components.mcp = {};
|
|
24
|
+
}
|
|
25
|
+
const mcp = components.mcp;
|
|
26
|
+
const configKey = mcp['~k'] ?? components['~k'];
|
|
27
|
+
// Agent tools were removed from mcp; schema validation only warns on
|
|
28
|
+
// unknown properties, so a leftover "agents" key would otherwise build
|
|
29
|
+
// silently instead of surfacing the removal.
|
|
30
|
+
if (!type.isNone(mcp.agents)) {
|
|
31
|
+
throw new ConfigError('MCP agent tools are not supported. Remove "mcp.agents" from your config.', {
|
|
32
|
+
configKey
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
mcp.name = mcp.name ?? 'lowdefy';
|
|
36
|
+
mcp.version = mcp.version ?? '1.0.0';
|
|
37
|
+
mcp.endpoints = mcp.endpoints ?? [];
|
|
38
|
+
mcp.configured = mcp.endpoints.length > 0;
|
|
39
|
+
const seen = new Set();
|
|
40
|
+
mcp.endpoints.forEach((endpointId)=>{
|
|
41
|
+
if (seen.has(endpointId)) {
|
|
42
|
+
throw new ConfigError(`Duplicate MCP tool "${endpointId}".`, {
|
|
43
|
+
configKey
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
seen.add(endpointId);
|
|
47
|
+
const endpoint = (components.api ?? []).find((e)=>e.id === endpointId || e.endpointId === endpointId);
|
|
48
|
+
if (type.isNone(endpoint)) {
|
|
49
|
+
throw new ConfigError(`MCP endpoint "${endpointId}" does not reference a defined api endpoint.`, {
|
|
50
|
+
configKey
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
// MCP is an external transport - InternalApi endpoints are not addressable
|
|
54
|
+
// from outside the server, matching the HTTP endpoint route.
|
|
55
|
+
if (endpoint.type === 'InternalApi') {
|
|
56
|
+
throw new ConfigError(`MCP endpoint "${endpointId}" is an InternalApi endpoint. Only "Api" endpoints can be exposed as MCP tools.`, {
|
|
57
|
+
configKey
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (type.isNone(endpoint.description)) {
|
|
61
|
+
throw new ConfigError(`Endpoint "${endpointId}" is exposed as an MCP tool but does not have a "description".`, {
|
|
62
|
+
configKey: endpoint['~k'] ?? configKey
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (type.isNone(endpoint.payloadSchema)) {
|
|
66
|
+
throw new ConfigError(`Endpoint "${endpointId}" is exposed as an MCP tool but does not have a "payloadSchema".`, {
|
|
67
|
+
configKey: endpoint['~k'] ?? configKey
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
return components;
|
|
72
|
+
}
|
|
73
|
+
export default buildMcp;
|
|
@@ -14,13 +14,13 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import operators from '@lowdefy/operators-js/operators/build';
|
|
16
16
|
import { resolve, WalkContext } from './buildRefs/walker.js';
|
|
17
|
+
import { assertNoPlaceholderLeaks } from './buildRefs/deferredRegistry.js';
|
|
17
18
|
import getRefContent from './buildRefs/getRefContent.js';
|
|
18
19
|
import makeRefDefinition from './buildRefs/makeRefDefinition.js';
|
|
19
|
-
import evaluateStaticOperators from './buildRefs/evaluateStaticOperators.js';
|
|
20
20
|
import collectDynamicIdentifiers from './collectDynamicIdentifiers.js';
|
|
21
21
|
import validateOperatorsDynamic from './validateOperatorsDynamic.js';
|
|
22
22
|
import fetchModules from './fetchModules.js';
|
|
23
|
-
import { resolveLocalManifest, resolveFullManifest, validateRequiredVars } from './registerModules.js';
|
|
23
|
+
import { resolveLocalManifest, recordifyExportables, resolveFullManifest, validateRequiredVars } from './registerModules.js';
|
|
24
24
|
import resolveModuleDependencies from './resolveModuleDependencies.js';
|
|
25
25
|
validateOperatorsDynamic({
|
|
26
26
|
operators
|
|
@@ -50,64 +50,86 @@ async function parseLowdefyYaml({ context }) {
|
|
|
50
50
|
] : []),
|
|
51
51
|
operators,
|
|
52
52
|
env: process.env,
|
|
53
|
+
lowdefyApp: context.appMeta,
|
|
53
54
|
dynamicIdentifiers,
|
|
54
55
|
shouldStop: (path)=>{
|
|
55
56
|
// Defer entry vars and connections: they may contain cross-module
|
|
56
|
-
// refs that require modules to be registered first.
|
|
57
|
-
|
|
58
|
-
if (/^modules\.\d+\.
|
|
57
|
+
// refs that require modules to be registered first. Non-modules keys
|
|
58
|
+
// stay raw for the app buildRefs pass.
|
|
59
|
+
if (/^modules\.\d+\.vars$/.test(path)) return 'skip';
|
|
60
|
+
if (/^modules\.\d+\.connections$/.test(path)) return 'skip';
|
|
59
61
|
if (path.startsWith('modules')) return false;
|
|
60
|
-
return '
|
|
62
|
+
return 'skip';
|
|
61
63
|
}
|
|
62
64
|
});
|
|
63
|
-
|
|
64
|
-
config = evaluateStaticOperators({
|
|
65
|
-
context,
|
|
66
|
-
input: config,
|
|
67
|
-
refDef
|
|
68
|
-
});
|
|
65
|
+
const config = await resolve(content, ctx);
|
|
69
66
|
return config ?? {};
|
|
70
67
|
}
|
|
71
|
-
|
|
72
|
-
const moduleEntry = context.modules[entry.id];
|
|
68
|
+
function makeAppLevelCtx({ context, deferModuleRefs = false, entryId, entrySection }) {
|
|
73
69
|
const lowdefyYamlRefDef = context.lowdefyYamlRefDef;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
let resolvedVars = await resolve(moduleEntry.consumerVars, makeAppLevelCtx());
|
|
92
|
-
resolvedVars = evaluateStaticOperators({
|
|
93
|
-
context,
|
|
94
|
-
input: resolvedVars,
|
|
95
|
-
refDef
|
|
70
|
+
return new WalkContext({
|
|
71
|
+
buildContext: context,
|
|
72
|
+
refId: lowdefyYamlRefDef.id,
|
|
73
|
+
sourceRefId: null,
|
|
74
|
+
vars: {},
|
|
75
|
+
path: '',
|
|
76
|
+
currentFile: lowdefyYamlRefDef.path,
|
|
77
|
+
refChain: new Set(lowdefyYamlRefDef.path ? [
|
|
78
|
+
lowdefyYamlRefDef.path
|
|
79
|
+
] : []),
|
|
80
|
+
operators,
|
|
81
|
+
env: process.env,
|
|
82
|
+
lowdefyApp: context.appMeta,
|
|
83
|
+
dynamicIdentifiers,
|
|
84
|
+
deferModuleRefs,
|
|
85
|
+
entryId,
|
|
86
|
+
entrySection
|
|
96
87
|
});
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
88
|
+
}
|
|
89
|
+
// Stage 1 — prepare: resolve entry vars/connections in the enclosing (app)
|
|
90
|
+
// scope; cross-module refs become entryRef records with the prepared refDef as
|
|
91
|
+
// body (their dynamic parts already resolved), leaving placeholders in the
|
|
92
|
+
// blobs. Order-free: nothing is consumed here.
|
|
93
|
+
async function prepareEntryConfig({ moduleEntry, context }) {
|
|
94
|
+
const varsResult = await resolve(moduleEntry.consumerVars, makeAppLevelCtx({
|
|
100
95
|
context,
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
96
|
+
deferModuleRefs: true,
|
|
97
|
+
entryId: moduleEntry.id,
|
|
98
|
+
entrySection: 'consumerVars'
|
|
99
|
+
}));
|
|
100
|
+
moduleEntry.consumerVars = varsResult ?? {};
|
|
101
|
+
const connectionsResult = await resolve(moduleEntry.connections, makeAppLevelCtx({
|
|
102
|
+
context,
|
|
103
|
+
deferModuleRefs: true,
|
|
104
|
+
entryId: moduleEntry.id,
|
|
105
|
+
entrySection: 'connections'
|
|
106
|
+
}));
|
|
107
|
+
moduleEntry.connections = connectionsResult ?? {};
|
|
108
|
+
}
|
|
109
|
+
// Final sweep: re-walk each entry's blobs with deferral off. Placeholders
|
|
110
|
+
// dispatch into resolveDeferred (value-granular demand, wait-graph cycle
|
|
111
|
+
// detection — order immaterial, memo skips already-demanded records), and the
|
|
112
|
+
// _module.connectionId object form (deferred during prepare) resolves against
|
|
113
|
+
// now-prepared targets. In-place walk mutation is the write-back, so resolved
|
|
114
|
+
// values land in consumerVars/connections with consumption provenance.
|
|
115
|
+
async function sweepEntryConfig({ moduleEntry, context }) {
|
|
116
|
+
const varsResult = await resolve(moduleEntry.consumerVars, makeAppLevelCtx({
|
|
117
|
+
context
|
|
118
|
+
}));
|
|
119
|
+
moduleEntry.consumerVars = varsResult ?? {};
|
|
120
|
+
const connectionsResult = await resolve(moduleEntry.connections, makeAppLevelCtx({
|
|
121
|
+
context
|
|
122
|
+
}));
|
|
123
|
+
moduleEntry.connections = connectionsResult ?? {};
|
|
124
|
+
validateRequiredVars(moduleEntry.varDefs, moduleEntry.consumerVars, moduleEntry.id, moduleEntry.source);
|
|
106
125
|
}
|
|
107
126
|
async function buildModuleDefs({ context }) {
|
|
108
127
|
const lowdefyConfig = await parseLowdefyYaml({
|
|
109
128
|
context
|
|
110
129
|
});
|
|
130
|
+
// Stash for index.js to compute appMeta before buildRefs, so the _build.app
|
|
131
|
+
// operator can resolve app metadata during ref resolution.
|
|
132
|
+
context.lowdefyConfig = lowdefyConfig;
|
|
111
133
|
context.plugins = lowdefyConfig.plugins ?? [];
|
|
112
134
|
const moduleEntries = lowdefyConfig.modules ?? [];
|
|
113
135
|
if (moduleEntries.length === 0) {
|
|
@@ -129,11 +151,28 @@ async function buildModuleDefs({ context }) {
|
|
|
129
151
|
resolveModuleDependencies({
|
|
130
152
|
context
|
|
131
153
|
});
|
|
132
|
-
// Step 2.
|
|
133
|
-
// then validate required vars against the resolved values.
|
|
154
|
+
// Step 2.5a — prepare all entries. No cross-entry work; order-free.
|
|
134
155
|
for (const entry of moduleEntries){
|
|
135
|
-
await
|
|
136
|
-
entry,
|
|
156
|
+
await prepareEntryConfig({
|
|
157
|
+
moduleEntry: context.modules[entry.id],
|
|
158
|
+
context
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
// Phase C.5 — record-ify exportables (components, menu links) for every
|
|
162
|
+
// entry BEFORE anything can consume them: record creation must precede all
|
|
163
|
+
// consumption, or cross-entry refs would race manifest resolve order.
|
|
164
|
+
for (const entry of moduleEntries){
|
|
165
|
+
await recordifyExportables({
|
|
166
|
+
entryId: entry.id,
|
|
167
|
+
context
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
// Step 2.5b — final sweep. Per-record demand through the wait-graph makes
|
|
171
|
+
// the order immaterial; required-var validation runs per entry after its
|
|
172
|
+
// blobs are concrete.
|
|
173
|
+
for (const entry of moduleEntries){
|
|
174
|
+
await sweepEntryConfig({
|
|
175
|
+
moduleEntry: context.modules[entry.id],
|
|
137
176
|
context
|
|
138
177
|
});
|
|
139
178
|
}
|
|
@@ -144,5 +183,8 @@ async function buildModuleDefs({ context }) {
|
|
|
144
183
|
context
|
|
145
184
|
});
|
|
146
185
|
}
|
|
186
|
+
// Post-sweep invariant: no deferred placeholder survives outside the
|
|
187
|
+
// per-consumer slots (manifest component/menu bodies, varDefs defaults).
|
|
188
|
+
assertNoPlaceholderLeaks(context);
|
|
147
189
|
}
|
|
148
190
|
export default buildModuleDefs;
|