@lowdefy/build 5.6.0 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +40 -0
- package/dist/build/buildApi/validateEndpoint.js +96 -1
- package/dist/build/buildApp.js +13 -20
- package/dist/build/buildAppMeta.js +138 -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/buildEnvironments.js +261 -0
- 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/buildLogger.js +22 -6
- 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 +84 -3
- 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 +6 -1
- package/dist/build/validateEnvironmentGuards.js +69 -0
- package/dist/build/validateRenderNotificationSteps.js +50 -0
- package/dist/build/writeApi.js +98 -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 +688 -388
- package/dist/dynamic/buildDynamicBlocks.js +120 -0
- package/dist/index.js +81 -10
- package/dist/indexDev.js +4 -0
- package/dist/lowdefySchema.js +969 -6
- package/dist/scripts/generateDefaultTypes.js +5 -2
- package/dist/test-utils/expectTerminates.js +33 -0
- package/dist/test-utils/runBuild.js +15 -1
- package/dist/test-utils/runBuildForSnapshots.js +37 -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/getEnvironmentNames.js +26 -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 +67 -60
- 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
|
}
|
|
@@ -0,0 +1,261 @@
|
|
|
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 { ConfigError, ConfigWarning } from '@lowdefy/errors';
|
|
17
|
+
import checkEnvironmentGuards from '@lowdefy/node-utils/checkEnvironmentGuards.js';
|
|
18
|
+
import getEnvironmentNames from '../utils/getEnvironmentNames.js';
|
|
19
|
+
import validateEnvironmentGuards from './validateEnvironmentGuards.js';
|
|
20
|
+
// Environment names become a path segment (/api/cron-forward/<environment>/<endpointId>).
|
|
21
|
+
const environmentNamePattern = /^[A-Za-z0-9\-_]+$/;
|
|
22
|
+
// Features an environment can switch off with `<feature>.enabled: false`. Logging has no switch.
|
|
23
|
+
const switchableFeatures = [
|
|
24
|
+
'cron',
|
|
25
|
+
'email',
|
|
26
|
+
'sentry'
|
|
27
|
+
];
|
|
28
|
+
function isAbsoluteHttpUrl(value) {
|
|
29
|
+
try {
|
|
30
|
+
const url = new URL(value);
|
|
31
|
+
return url.protocol === 'http:' || url.protocol === 'https:';
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function validateEmailFilter({ name, filter, configKey }) {
|
|
37
|
+
const where = `config.environments.${name}.email.filter`;
|
|
38
|
+
if (!type.isObject(filter)) {
|
|
39
|
+
throw new ConfigError(`App "${where}" should be an object.`, {
|
|
40
|
+
received: filter,
|
|
41
|
+
configKey
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
const { replaceAddress, allowlist, regex } = filter;
|
|
45
|
+
if (!type.isNone(replaceAddress) && (!type.isString(replaceAddress) || replaceAddress === '')) {
|
|
46
|
+
throw new ConfigError(`App "${where}.replaceAddress" should be a non-empty string.`, {
|
|
47
|
+
received: replaceAddress,
|
|
48
|
+
configKey
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (!type.isNone(allowlist) && (!type.isArray(allowlist) || !allowlist.every((domain)=>type.isString(domain)))) {
|
|
52
|
+
throw new ConfigError(`App "${where}.allowlist" should be an array of strings.`, {
|
|
53
|
+
received: allowlist,
|
|
54
|
+
configKey
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
if (!type.isNone(regex)) {
|
|
58
|
+
if (!type.isString(regex)) {
|
|
59
|
+
throw new ConfigError(`App "${where}.regex" should be a string.`, {
|
|
60
|
+
received: regex,
|
|
61
|
+
configKey
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
// Compiled here so a broken pattern fails the build, not every mail request in the environment.
|
|
65
|
+
try {
|
|
66
|
+
new RegExp(regex);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
throw new ConfigError(`App "${where}.regex" is not a valid regular expression: ${error.message}`, {
|
|
69
|
+
configKey
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function validateEnvironment({ name, environment, configKey }) {
|
|
75
|
+
if (name === 'default') {
|
|
76
|
+
throw new ConfigError('Environment name "default" is reserved: it names the value every other environment inherits.', {
|
|
77
|
+
configKey
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
if (!environmentNamePattern.test(name)) {
|
|
81
|
+
throw new ConfigError(`Environment name "${name}" is invalid. Environment names must only contain A-Z, a-z, 0-9, "-" and "_".`, {
|
|
82
|
+
configKey
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (!type.isObject(environment)) {
|
|
86
|
+
throw new ConfigError(`App "config.environments.${name}" should be an object.`, {
|
|
87
|
+
received: environment,
|
|
88
|
+
configKey
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
const key = environment['~k'] ?? configKey;
|
|
92
|
+
const { url, cron, email, sentry } = environment;
|
|
93
|
+
if (!type.isUndefined(url) && (!type.isString(url) || !isAbsoluteHttpUrl(url))) {
|
|
94
|
+
throw new ConfigError(`App "config.environments.${name}.url" should be an absolute http(s) URL, e.g. "https://staging.example.com".`, {
|
|
95
|
+
received: url,
|
|
96
|
+
configKey: key
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (!type.isUndefined(cron)) {
|
|
100
|
+
if (!type.isObject(cron)) {
|
|
101
|
+
throw new ConfigError(`App "config.environments.${name}.cron" should be an object.`, {
|
|
102
|
+
received: cron,
|
|
103
|
+
configKey: key
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
if (!type.isUndefined(cron.secret)) {
|
|
107
|
+
if (!type.isString(cron.secret) || cron.secret === '') {
|
|
108
|
+
throw new ConfigError(`App "config.environments.${name}.cron.secret" should be the name of the Lowdefy secret holding the environment's CRON_SECRET.`, {
|
|
109
|
+
received: cron.secret,
|
|
110
|
+
configKey: cron['~k'] ?? key
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
if (type.isUndefined(url)) {
|
|
114
|
+
throw new ConfigError(`Environment "${name}" has a cron.secret but no url. Crons are forwarded to an environment's url, so set the environment's deployment origin.`, {
|
|
115
|
+
configKey: key
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (!type.isUndefined(sentry) && !type.isObject(sentry)) {
|
|
121
|
+
throw new ConfigError(`App "config.environments.${name}.sentry" should be an object.`, {
|
|
122
|
+
received: sentry,
|
|
123
|
+
configKey: key
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
switchableFeatures.forEach((feature)=>{
|
|
127
|
+
const enabled = environment[feature]?.enabled;
|
|
128
|
+
if (!type.isUndefined(enabled) && !type.isBoolean(enabled)) {
|
|
129
|
+
throw new ConfigError(`App "config.environments.${name}.${feature}.enabled" should be a boolean.`, {
|
|
130
|
+
received: enabled,
|
|
131
|
+
configKey: environment[feature]['~k'] ?? key
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
validateEnvironmentGuards({
|
|
136
|
+
name,
|
|
137
|
+
guards: environment.guards,
|
|
138
|
+
configKey: key
|
|
139
|
+
});
|
|
140
|
+
if (!type.isUndefined(email)) {
|
|
141
|
+
if (!type.isObject(email)) {
|
|
142
|
+
throw new ConfigError(`App "config.environments.${name}.email" should be an object.`, {
|
|
143
|
+
received: email,
|
|
144
|
+
configKey: key
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
if (!type.isUndefined(email.filter)) {
|
|
148
|
+
validateEmailFilter({
|
|
149
|
+
name,
|
|
150
|
+
filter: email.filter,
|
|
151
|
+
configKey: email['~k'] ?? key
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// LOWDEFY_ENVIRONMENT wins over config.environment: the variable is set per deployment, while a
|
|
157
|
+
// literal in config is shared by every deployment and would force them all into one environment.
|
|
158
|
+
function getCurrentEnvironmentName({ config }) {
|
|
159
|
+
if (!type.isNone(config.environment)) {
|
|
160
|
+
if (!type.isString(config.environment) || config.environment === '') {
|
|
161
|
+
throw new ConfigError('App "config.environment" should be a non-empty string.', {
|
|
162
|
+
received: config.environment,
|
|
163
|
+
configKey: config['~k']
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const fromEnv = process.env.LOWDEFY_ENVIRONMENT?.trim();
|
|
168
|
+
if (!type.isNone(fromEnv) && fromEnv !== '') return fromEnv;
|
|
169
|
+
return config.environment ?? undefined;
|
|
170
|
+
}
|
|
171
|
+
// Resolves the deployment environments once for the whole build. After this step:
|
|
172
|
+
// - config.environments holds every declared environment ({ url, cron, email }), validated;
|
|
173
|
+
// - config.environment names the environment this build is for (the LOWDEFY_ENVIRONMENT
|
|
174
|
+
// variable, else config.environment), and must be declared when environments are.
|
|
175
|
+
// Everything environment-specific (cron registration and forwarding, notification links, the auth
|
|
176
|
+
// base URL, the email delivery filter, the Sentry environment) reads the current environment from
|
|
177
|
+
// there instead of from its own environment variable. buildLogger, which runs next, applies the
|
|
178
|
+
// Sentry settings.
|
|
179
|
+
function buildEnvironments({ components, context }) {
|
|
180
|
+
const config = components.config;
|
|
181
|
+
const configKey = config['~k'];
|
|
182
|
+
const current = getCurrentEnvironmentName({
|
|
183
|
+
config
|
|
184
|
+
});
|
|
185
|
+
// config.cron.environments (6.0) is replaced by config.environments. Fail with the migration
|
|
186
|
+
// rather than run two shapes side by side.
|
|
187
|
+
if (!type.isUndefined(config.cron)) {
|
|
188
|
+
throw new ConfigError('App "config.cron" is replaced by "config.environments". Declare the deployment environments under "config.environments": give every environment its "url", move each "secret" to "cron.secret" and "enabled" to "cron.enabled", and set LOWDEFY_ENVIRONMENT on each deployment.', {
|
|
189
|
+
configKey: config.cron?.['~k'] ?? configKey
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
const environments = config.environments;
|
|
193
|
+
if (!type.isUndefined(environments)) {
|
|
194
|
+
if (!type.isObject(environments)) {
|
|
195
|
+
throw new ConfigError('App "config.environments" should be an object.', {
|
|
196
|
+
received: environments,
|
|
197
|
+
configKey
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
const envKey = environments['~k'] ?? configKey;
|
|
201
|
+
const names = getEnvironmentNames(environments);
|
|
202
|
+
if (names.length === 0) {
|
|
203
|
+
throw new ConfigError('App "config.environments" declares no environments.', {
|
|
204
|
+
configKey: envKey
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
names.forEach((name)=>validateEnvironment({
|
|
208
|
+
name,
|
|
209
|
+
environment: environments[name],
|
|
210
|
+
configKey: envKey
|
|
211
|
+
}));
|
|
212
|
+
if (!type.isUndefined(current) && !names.includes(current)) {
|
|
213
|
+
throw new ConfigError(`The current environment "${current}" (LOWDEFY_ENVIRONMENT) is not declared in "config.environments". Declared environments: ${names.join(', ')}.`, {
|
|
214
|
+
configKey: envKey
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
// Without a current environment every environment setting fails open: no email filter, no
|
|
218
|
+
// forwarded crons, no guards. That is an error in a prod build, and a warning on the dev server.
|
|
219
|
+
if (type.isUndefined(current)) {
|
|
220
|
+
context.handleWarning(new ConfigWarning(`App "config.environments" is declared but no current environment is set. Set LOWDEFY_ENVIRONMENT to the environment this deployment is (${names.join(', ')}): until then no environment setting (url, cron, email filter, guards) applies and only the "default" schedules are registered.`, {
|
|
221
|
+
configKey: envKey,
|
|
222
|
+
prodError: true
|
|
223
|
+
}));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// The current environment's guards are checked against this build's variables and kept for the
|
|
227
|
+
// production server, which checks them again at startup; other environments' guards are dropped.
|
|
228
|
+
if (!type.isUndefined(environments)) {
|
|
229
|
+
if (!type.isUndefined(current)) {
|
|
230
|
+
checkEnvironmentGuards({
|
|
231
|
+
name: current,
|
|
232
|
+
guards: environments[current].guards,
|
|
233
|
+
configKey: environments[current]['~k'] ?? configKey
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
getEnvironmentNames(environments).filter((name)=>name !== current).forEach((name)=>{
|
|
237
|
+
delete environments[name].guards;
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
if (type.isUndefined(current)) {
|
|
241
|
+
delete config.environment;
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
config.environment = current;
|
|
245
|
+
const settings = config.environments?.[current] ?? {};
|
|
246
|
+
// The dev server is the local app, not the deployment the environment's url names, so the url is
|
|
247
|
+
// dropped: auth and notification links stay on the request origin even when a local .env names a
|
|
248
|
+
// deployed environment.
|
|
249
|
+
if (context.stage === 'dev') {
|
|
250
|
+
delete settings.url;
|
|
251
|
+
}
|
|
252
|
+
const disabled = switchableFeatures.filter((feature)=>settings[feature]?.enabled === false);
|
|
253
|
+
// App metadata is the deploy identity the server stamps on every log line and the client reads
|
|
254
|
+
// with _app, so the environment and the features it switches off travel with it — the browser
|
|
255
|
+
// has no other view of config.environments.
|
|
256
|
+
if (type.isObject(components.appMeta)) {
|
|
257
|
+
components.appMeta.environment = current;
|
|
258
|
+
components.appMeta.disabled = disabled;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
export default buildEnvironments;
|
|
@@ -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 }`
|