@lowdefy/build 6.0.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/buildApi/buildApi.js +2 -2
- package/dist/build/buildApi/buildEndpoint.js +3 -3
- package/dist/build/buildApi/resolveEndpointSchedules.js +12 -9
- package/dist/build/buildApi/validateEndpoint.js +9 -9
- package/dist/build/buildAppMeta.js +9 -1
- package/dist/build/buildEnvironments.js +261 -0
- package/dist/build/buildLogger.js +22 -6
- package/dist/build/jit/shallowBuild.js +3 -2
- package/dist/build/validateConfig.js +5 -4
- package/dist/build/validateEnvironmentGuards.js +69 -0
- package/dist/build/writeApi.js +59 -19
- package/dist/defaultTypesMap.js +444 -422
- package/dist/index.js +3 -2
- package/dist/lowdefySchema.js +142 -34
- package/dist/test-utils/runBuild.js +3 -0
- package/dist/test-utils/runBuildForSnapshots.js +3 -0
- package/dist/utils/{getCronEnvironmentNames.js → getEnvironmentNames.js} +5 -4
- package/package.json +58 -58
- package/dist/build/validateCronConfig.js +0 -110
|
@@ -27,7 +27,7 @@ function buildApi({ components, context }) {
|
|
|
27
27
|
message: 'Duplicate endpointId "{{ id }}".'
|
|
28
28
|
});
|
|
29
29
|
// Validated by validateConfig, which runs before buildApi.
|
|
30
|
-
const
|
|
30
|
+
const environments = components.config?.environments;
|
|
31
31
|
// Wrap each endpoint build to collect errors instead of stopping on first error
|
|
32
32
|
api.forEach((endpoint, index)=>{
|
|
33
33
|
try {
|
|
@@ -36,7 +36,7 @@ function buildApi({ components, context }) {
|
|
|
36
36
|
index,
|
|
37
37
|
context,
|
|
38
38
|
checkDuplicateEndpointId,
|
|
39
|
-
|
|
39
|
+
environments
|
|
40
40
|
});
|
|
41
41
|
} catch (error) {
|
|
42
42
|
// Skip suppressed ConfigErrors (via ~ignoreBuildChecks)
|
|
@@ -16,17 +16,17 @@
|
|
|
16
16
|
import resolveEndpointSchedules from './resolveEndpointSchedules.js';
|
|
17
17
|
import validateEndpoint from './validateEndpoint.js';
|
|
18
18
|
import validateStepReferences from './validateStepReferences.js';
|
|
19
|
-
function buildEndpoint({ endpoint, index, context, checkDuplicateEndpointId,
|
|
19
|
+
function buildEndpoint({ endpoint, index, context, checkDuplicateEndpointId, environments }) {
|
|
20
20
|
validateEndpoint({
|
|
21
21
|
endpoint,
|
|
22
22
|
index,
|
|
23
23
|
checkDuplicateEndpointId,
|
|
24
|
-
|
|
24
|
+
environments
|
|
25
25
|
});
|
|
26
26
|
endpoint.endpointId = endpoint.id;
|
|
27
27
|
resolveEndpointSchedules({
|
|
28
28
|
endpoint,
|
|
29
|
-
|
|
29
|
+
environments
|
|
30
30
|
});
|
|
31
31
|
buildRoutine(endpoint.routine, {
|
|
32
32
|
endpointId: endpoint.endpointId,
|
|
@@ -13,24 +13,27 @@
|
|
|
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
|
|
17
|
-
// With config.
|
|
16
|
+
import getEnvironmentNames from '../../utils/getEnvironmentNames.js';
|
|
17
|
+
// With config.environments declared, the artifact carries `schedules` as an object with one
|
|
18
18
|
// explicit list per declared environment: the environment's own entry when the author keyed the
|
|
19
19
|
// schedules by environment, else the `default` entry, else nothing (`[]` turns the crons off). An
|
|
20
|
-
// array applies as-is to every environment.
|
|
21
|
-
//
|
|
22
|
-
// keeps its array.
|
|
23
|
-
function resolveEndpointSchedules({ endpoint,
|
|
24
|
-
const names =
|
|
20
|
+
// array applies as-is to every environment. `default` is kept too, for a build with no current
|
|
21
|
+
// environment. The runtime and the schedules manifest then read `schedules.<environment>` without
|
|
22
|
+
// repeating the inheritance. Without config.environments the endpoint keeps its array.
|
|
23
|
+
function resolveEndpointSchedules({ endpoint, environments }) {
|
|
24
|
+
const names = getEnvironmentNames(environments);
|
|
25
25
|
if (names.length === 0 || type.isUndefined(endpoint.schedules)) return;
|
|
26
26
|
const authored = endpoint.schedules;
|
|
27
|
-
const
|
|
27
|
+
const inherited = type.isArray(authored) ? authored : authored.default ?? [];
|
|
28
|
+
const resolved = {
|
|
29
|
+
default: inherited
|
|
30
|
+
};
|
|
28
31
|
names.forEach((name)=>{
|
|
29
32
|
if (type.isArray(authored)) {
|
|
30
33
|
resolved[name] = authored;
|
|
31
34
|
return;
|
|
32
35
|
}
|
|
33
|
-
resolved[name] = authored[name] ??
|
|
36
|
+
resolved[name] = authored[name] ?? inherited;
|
|
34
37
|
});
|
|
35
38
|
endpoint.schedules = resolved;
|
|
36
39
|
}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import { ConfigError } from '@lowdefy/errors';
|
|
17
17
|
import validateId from '../../utils/validateId.js';
|
|
18
18
|
import validateCronExpression from '../../utils/validateCronExpression.js';
|
|
19
|
-
import
|
|
19
|
+
import getEnvironmentNames from '../../utils/getEnvironmentNames.js';
|
|
20
20
|
// `where` names the schedules list in messages: "" for the endpoint's own schedules, or
|
|
21
21
|
// ` for environment "staging"` for an override.
|
|
22
22
|
function validateSchedules({ schedules, endpoint, configKey, where = '' }) {
|
|
@@ -66,18 +66,18 @@ function validateSchedules({ schedules, endpoint, configKey, where = '' }) {
|
|
|
66
66
|
});
|
|
67
67
|
}
|
|
68
68
|
// The object form of `schedules`: lists keyed by environment name plus an optional `default` the
|
|
69
|
-
// other environments inherit. Every key must be declared in config.
|
|
69
|
+
// other environments inherit. Every key must be declared in config.environments, so a typo
|
|
70
70
|
// cannot silently leave an environment on the defaults.
|
|
71
|
-
function validateEnvironmentSchedules({ endpoint, configKey,
|
|
72
|
-
const declared =
|
|
71
|
+
function validateEnvironmentSchedules({ endpoint, configKey, environments }) {
|
|
72
|
+
const declared = getEnvironmentNames(environments);
|
|
73
73
|
if (declared.length === 0) {
|
|
74
|
-
throw new ConfigError(`Endpoint "${endpoint.id}" keys schedules by environment but
|
|
74
|
+
throw new ConfigError(`Endpoint "${endpoint.id}" keys schedules by environment but config.environments is not defined.`, {
|
|
75
75
|
configKey
|
|
76
76
|
});
|
|
77
77
|
}
|
|
78
|
-
|
|
78
|
+
getEnvironmentNames(endpoint.schedules).forEach((name)=>{
|
|
79
79
|
if (name !== 'default' && !declared.includes(name)) {
|
|
80
|
-
throw new ConfigError(`Endpoint schedules environment "${name}" at "${endpoint.id}" is not declared in
|
|
80
|
+
throw new ConfigError(`Endpoint schedules environment "${name}" at "${endpoint.id}" is not declared in config.environments. Declared environments: ${declared.join(', ')}.`, {
|
|
81
81
|
configKey
|
|
82
82
|
});
|
|
83
83
|
}
|
|
@@ -97,7 +97,7 @@ function validateEnvironmentSchedules({ endpoint, configKey, cronEnvironments })
|
|
|
97
97
|
});
|
|
98
98
|
});
|
|
99
99
|
}
|
|
100
|
-
function validateEndpoint({ endpoint, index, checkDuplicateEndpointId,
|
|
100
|
+
function validateEndpoint({ endpoint, index, checkDuplicateEndpointId, environments }) {
|
|
101
101
|
const configKey = endpoint['~k'];
|
|
102
102
|
if (type.isUndefined(endpoint.id)) {
|
|
103
103
|
throw new ConfigError(`Endpoint id missing at endpoint ${index}.`, {
|
|
@@ -144,7 +144,7 @@ function validateEndpoint({ endpoint, index, checkDuplicateEndpointId, cronEnvir
|
|
|
144
144
|
validateEnvironmentSchedules({
|
|
145
145
|
endpoint,
|
|
146
146
|
configKey,
|
|
147
|
-
|
|
147
|
+
environments
|
|
148
148
|
});
|
|
149
149
|
} else {
|
|
150
150
|
validateSchedules({
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import { execSync } from 'child_process';
|
|
16
|
+
import { randomUUID } from 'crypto';
|
|
16
17
|
import { ConfigError } from '@lowdefy/errors';
|
|
17
18
|
import { type } from '@lowdefy/helpers';
|
|
18
19
|
import { evaluateOperators } from '@lowdefy/operators';
|
|
@@ -123,7 +124,14 @@ async function buildAppMeta({ context }) {
|
|
|
123
124
|
field: 'lowdefy',
|
|
124
125
|
value: content.lowdefy
|
|
125
126
|
}),
|
|
126
|
-
gitSha: computeGitSha()
|
|
127
|
+
gitSha: computeGitSha(),
|
|
128
|
+
// Identity of this build, unique per run. The client bundle and the server
|
|
129
|
+
// both read it from appMeta.json, so an open tab can tell after a deploy
|
|
130
|
+
// that the page config it just fetched came from a newer build than the
|
|
131
|
+
// bundle it is running, and reload instead of failing on missing _js
|
|
132
|
+
// functions. gitSha is not enough: it is null on most deploy platforms and
|
|
133
|
+
// the same commit can be rebuilt with different config.
|
|
134
|
+
buildId: randomUUID()
|
|
127
135
|
};
|
|
128
136
|
return context.appMeta;
|
|
129
137
|
}
|
|
@@ -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;
|
|
@@ -25,17 +25,33 @@ const sentryDefaults = {
|
|
|
25
25
|
'_id'
|
|
26
26
|
]
|
|
27
27
|
};
|
|
28
|
+
// Runs after validateConfig, so config.environment and config.environments are resolved.
|
|
28
29
|
function buildLogger({ components }) {
|
|
29
30
|
if (type.isNone(components.logger)) {
|
|
30
31
|
components.logger = {};
|
|
31
32
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
};
|
|
33
|
+
const current = components.config.environment;
|
|
34
|
+
// Sentry is enabled by SENTRY_DSN, not by the config being present, so a current environment
|
|
35
|
+
// creates the Sentry config to carry its name. Without either, the runtime defaults apply.
|
|
36
|
+
if (type.isNone(components.logger.sentry) && type.isNone(current)) {
|
|
37
|
+
return components;
|
|
38
38
|
}
|
|
39
|
+
const sentry = {
|
|
40
|
+
...sentryDefaults,
|
|
41
|
+
...components.logger.sentry ?? {}
|
|
42
|
+
};
|
|
43
|
+
if (!type.isNone(current)) {
|
|
44
|
+
// Sentry reports under the environment name unless the app names one itself, and is off on
|
|
45
|
+
// both sides when the environment switches it off.
|
|
46
|
+
if (type.isNone(sentry.environment)) {
|
|
47
|
+
sentry.environment = current;
|
|
48
|
+
}
|
|
49
|
+
if (components.config.environments?.[current]?.sentry?.enabled === false) {
|
|
50
|
+
sentry.client = false;
|
|
51
|
+
sentry.server = false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
components.logger.sentry = sentry;
|
|
39
55
|
return components;
|
|
40
56
|
}
|
|
41
57
|
export default buildLogger;
|
|
@@ -157,11 +157,12 @@ async function shallowBuild(options) {
|
|
|
157
157
|
// appMeta is computed in Phase 0; attach it here (where buildApp used to
|
|
158
158
|
// create it) so the following addKeys pass keys it identically.
|
|
159
159
|
components.appMeta = context.appMeta;
|
|
160
|
-
|
|
160
|
+
// validateConfig resolves the current environment, which buildLogger reads for Sentry.
|
|
161
|
+
tryBuildStep(validateConfig, 'validateConfig', {
|
|
161
162
|
components,
|
|
162
163
|
context
|
|
163
164
|
});
|
|
164
|
-
tryBuildStep(
|
|
165
|
+
tryBuildStep(buildLogger, 'buildLogger', {
|
|
165
166
|
components,
|
|
166
167
|
context
|
|
167
168
|
});
|
|
@@ -13,8 +13,8 @@
|
|
|
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
|
|
17
|
-
function validateConfig({ components }) {
|
|
16
|
+
import buildEnvironments from './buildEnvironments.js';
|
|
17
|
+
function validateConfig({ components, context }) {
|
|
18
18
|
if (type.isNone(components.config)) {
|
|
19
19
|
components.config = {};
|
|
20
20
|
}
|
|
@@ -26,8 +26,9 @@ function validateConfig({ components }) {
|
|
|
26
26
|
throw new Error('Base path must start with "/".');
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
-
|
|
30
|
-
components
|
|
29
|
+
buildEnvironments({
|
|
30
|
+
components,
|
|
31
|
+
context
|
|
31
32
|
});
|
|
32
33
|
return components;
|
|
33
34
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
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 } from '@lowdefy/errors';
|
|
17
|
+
import collectEnvironmentGuards from '@lowdefy/node-utils/collectEnvironmentGuards.js';
|
|
18
|
+
const guardKinds = [
|
|
19
|
+
'secrets',
|
|
20
|
+
'env'
|
|
21
|
+
];
|
|
22
|
+
// Guard shape and patterns are validated for every environment, not only the current one, so a
|
|
23
|
+
// broken guard fails the build that introduces it.
|
|
24
|
+
function validateEnvironmentGuards({ name, guards, configKey }) {
|
|
25
|
+
if (type.isUndefined(guards)) return;
|
|
26
|
+
const where = `config.environments.${name}.guards`;
|
|
27
|
+
if (!type.isObject(guards)) {
|
|
28
|
+
throw new ConfigError(`App "${where}" should be an object.`, {
|
|
29
|
+
received: guards,
|
|
30
|
+
configKey
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
// An unknown key (a `secret:` typo for `secrets:`) would otherwise check nothing and let the
|
|
34
|
+
// value it was meant to pin through, so it fails the build.
|
|
35
|
+
Object.keys(guards).forEach((kind)=>{
|
|
36
|
+
if (kind.startsWith('~') || guardKinds.includes(kind)) return;
|
|
37
|
+
throw new ConfigError(`App "${where}.${kind}" is not a guard kind. Guards are "secrets" (Lowdefy secret names) and "env" (environment variable names).`, {
|
|
38
|
+
configKey
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
guardKinds.forEach((kind)=>{
|
|
42
|
+
const map = guards[kind];
|
|
43
|
+
if (type.isUndefined(map)) return;
|
|
44
|
+
if (!type.isObject(map)) {
|
|
45
|
+
throw new ConfigError(`App "${where}.${kind}" should be an object.`, {
|
|
46
|
+
received: map,
|
|
47
|
+
configKey
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
collectEnvironmentGuards({
|
|
52
|
+
guards
|
|
53
|
+
}).forEach(({ label, pattern })=>{
|
|
54
|
+
if (!type.isString(pattern) || pattern === '') {
|
|
55
|
+
throw new ConfigError(`App "${where}" ${label} should be a regular expression string.`, {
|
|
56
|
+
received: pattern,
|
|
57
|
+
configKey
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
new RegExp(pattern);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
throw new ConfigError(`App "${where}" ${label} is not a valid regular expression: ${error.message}`, {
|
|
64
|
+
configKey
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
export default validateEnvironmentGuards;
|